From c046b39e791b16a927dee61c57b9526d97ca6fb2 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 30 Aug 2014 16:55:05 -0700 Subject: [PATCH 0001/3836] Initial cookiecutter repo --- .coveragerc | 7 ++ .gitignore | 51 +++++++++++ .gitreview | 4 + .mailmap | 3 + .testr.conf | 7 ++ CONTRIBUTING.rst | 17 ++++ HACKING.rst | 4 + LICENSE | 175 ++++++++++++++++++++++++++++++++++++ MANIFEST.in | 6 ++ README.rst | 26 ++++++ doc/source/conf.py | 34 +++++++ doc/source/contributing.rst | 1 + doc/source/index.rst | 24 +++++ doc/source/installation.rst | 12 +++ doc/source/readme.rst | 1 + doc/source/usage.rst | 7 ++ requirements.txt | 7 ++ setup.cfg | 28 ++++++ setup.py | 21 +++++ shade/__init__.py | 18 ++++ shade/tests/__init__.py | 0 shade/tests/base.py | 53 +++++++++++ shade/tests/test_shade.py | 28 ++++++ test-requirements.txt | 11 +++ tox.ini | 30 +++++++ 25 files changed, 575 insertions(+) create mode 100644 .coveragerc create mode 100644 .gitignore create mode 100644 .gitreview create mode 100644 .mailmap create mode 100644 .testr.conf create mode 100644 CONTRIBUTING.rst create mode 100644 HACKING.rst create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.rst create mode 100755 doc/source/conf.py create mode 100644 doc/source/contributing.rst create mode 100644 doc/source/index.rst create mode 100644 doc/source/installation.rst create mode 100644 doc/source/readme.rst create mode 100644 doc/source/usage.rst create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100755 setup.py create mode 100644 shade/__init__.py create mode 100644 shade/tests/__init__.py create mode 100644 shade/tests/base.py create mode 100644 shade/tests/test_shade.py create mode 100644 test-requirements.txt create mode 100644 tox.ini diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..078cf7252 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,7 @@ +[run] +branch = True +source = shade +omit = shade/tests/* + +[report] +ignore-errors = True diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..1399c9819 --- /dev/null +++ b/.gitignore @@ -0,0 +1,51 @@ +*.py[cod] + +# C extensions +*.so + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg +lib +lib64 + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox +nosetests.xml +.testrepository + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Complexity +output/*.html +output/*/index.html + +# Sphinx +doc/build + +# pbr generates these +AUTHORS +ChangeLog + +# Editors +*~ +.*.swp \ No newline at end of file diff --git a/.gitreview b/.gitreview new file mode 100644 index 000000000..fd7871d20 --- /dev/null +++ b/.gitreview @@ -0,0 +1,4 @@ +[gerrit] +host=review.openstack.org +port=29418 +project=stackforge/shade.git \ No newline at end of file diff --git a/.mailmap b/.mailmap new file mode 100644 index 000000000..cc92f17b8 --- /dev/null +++ b/.mailmap @@ -0,0 +1,3 @@ +# Format is: +# +# \ No newline at end of file diff --git a/.testr.conf b/.testr.conf new file mode 100644 index 000000000..fb622677a --- /dev/null +++ b/.testr.conf @@ -0,0 +1,7 @@ +[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 ./ . $LISTOPT $IDOPTION +test_id_option=--load-list $IDFILE +test_list_option=--list \ No newline at end of file diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 000000000..5ed928270 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,17 @@ +If you would like to contribute to the development of OpenStack, +you must follow the steps in the "If you're a developer, start here" +section of this page: + + http://wiki.openstack.org/HowToContribute + +Once those steps have been completed, changes to OpenStack +should be submitted for review via the Gerrit tool, following +the workflow documented at: + + http://wiki.openstack.org/GerritWorkflow + +Pull requests submitted through GitHub will be ignored. + +Bugs should be filed on Launchpad, not GitHub: + + https://bugs.launchpad.net/shade \ No newline at end of file diff --git a/HACKING.rst b/HACKING.rst new file mode 100644 index 000000000..7f3d25675 --- /dev/null +++ b/HACKING.rst @@ -0,0 +1,4 @@ +shade Style Commandments +=============================================== + +Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..67db85882 --- /dev/null +++ b/LICENSE @@ -0,0 +1,175 @@ + + 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. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 000000000..90f8a7aef --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +include AUTHORS +include ChangeLog +exclude .gitignore +exclude .gitreview + +global-exclude *.pyc \ No newline at end of file diff --git a/README.rst b/README.rst new file mode 100644 index 000000000..9e7d32950 --- /dev/null +++ b/README.rst @@ -0,0 +1,26 @@ +shade +===== + +shade is a simple client library for operating OpenStack clouds. The +key word here is *simple*. Clouds can do many many many things - but there are +probably only about 10 of them that most people care about with any +regularity. If you want to do complicated things, you should probably use +the lower level client libraries - or even the REST API directly. However, +if what you want is to be able to write an application that talks to clouds +no matter what crazy choices the deployer has made in an attempt to be +more hipster than their self-entitled narcissist peers, then shade is for you. + +shade started its life as some code inside of ansible. ansible has a bunch +of different OpenStack related modules, and there was a ton of duplicated +code. Eventually, between refactoring that duplication into an internal +library, and adding logic and features that the OpenStack Infra team had +developed to run client applications at scale, it turned out that we'd written +nine-tenths of what we'd need to have a standalone library. + +* Free software: Apache license +* Documentation: http://docs.openstack.org/developer/shade + +Features +-------- + +* TODO diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100755 index 000000000..4be3c2e99 --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,34 @@ +import os +import sys + +sys.path.insert(0, os.path.abspath('../..')) + +# The suffix of source filenames. +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'shade' +copyright = u'2014, Monty Taylor' + +# 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 + +# Output file base name for HTML help builder. +htmlhelp_basename = '%sdoc' % project + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass +# [howto/manual]). +latex_documents = [ + ('index', + '%s.tex' % project, + u'%s Documentation' % project, + u'Monty Taylor', 'manual'), +] diff --git a/doc/source/contributing.rst b/doc/source/contributing.rst new file mode 100644 index 000000000..8cb3146fe --- /dev/null +++ b/doc/source/contributing.rst @@ -0,0 +1 @@ +.. include:: ../../CONTRIBUTING.rst \ No newline at end of file diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 000000000..e8b236528 --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,24 @@ +.. shade documentation master file, created by + sphinx-quickstart on Tue Jul 9 22:26:36 2013. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to shade's documentation! +======================================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + readme + installation + usage + contributing + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/doc/source/installation.rst b/doc/source/installation.rst new file mode 100644 index 000000000..a7b1bc3f4 --- /dev/null +++ b/doc/source/installation.rst @@ -0,0 +1,12 @@ +============ +Installation +============ + +At the command line:: + + $ pip install shade + +Or, if you have virtualenvwrapper installed:: + + $ mkvirtualenv shade + $ pip install shade \ No newline at end of file diff --git a/doc/source/readme.rst b/doc/source/readme.rst new file mode 100644 index 000000000..6b2b3ec68 --- /dev/null +++ b/doc/source/readme.rst @@ -0,0 +1 @@ +.. include:: ../README.rst \ No newline at end of file diff --git a/doc/source/usage.rst b/doc/source/usage.rst new file mode 100644 index 000000000..96c1020df --- /dev/null +++ b/doc/source/usage.rst @@ -0,0 +1,7 @@ +======== +Usage +======== + +To use shade in a project:: + + import shade \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..ed455a22b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +pbr>=0.5.21,<1.0 + +python-novaclient +python-keystoneclient +python-glanceclient +python-cinderclient +python-neutronclient diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..5d0c41551 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,28 @@ +[metadata] +name = shade +summary = Client library for operating OpenStack clouds +description-file = + README.rst +author = Monty Taylor +author-email = mordred@inaugust.com +home-page = http://www.openstack.org/ +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 :: 2 + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 2.6 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.3 + +[build_sphinx] +source-dir = doc/source +build-dir = doc/build +all_files = 1 + +[upload_sphinx] +upload-dir = doc/build/html diff --git a/setup.py b/setup.py new file mode 100755 index 000000000..c0a24eab2 --- /dev/null +++ b/setup.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +# 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. + +import setuptools + +setuptools.setup( + setup_requires=['pbr'], + pbr=True) diff --git a/shade/__init__.py b/shade/__init__.py new file mode 100644 index 000000000..e96635bb7 --- /dev/null +++ b/shade/__init__.py @@ -0,0 +1,18 @@ +# -*- 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. + +import pbr.version + + +__version__ = pbr.version.VersionInfo('shade').version_string() diff --git a/shade/tests/__init__.py b/shade/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/shade/tests/base.py b/shade/tests/base.py new file mode 100644 index 000000000..93b83262c --- /dev/null +++ b/shade/tests/base.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +# Copyright 2010-2011 OpenStack Foundation +# 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. + +import os + +import fixtures +import testtools + +_TRUE_VALUES = ('true', '1', 'yes') + + +class TestCase(testtools.TestCase): + + """Test case base class for all unit tests.""" + + def setUp(self): + """Run before each test method to initialize test environment.""" + + super(TestCase, self).setUp() + test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0) + try: + test_timeout = int(test_timeout) + except ValueError: + # If timeout value is invalid do not set a timeout. + test_timeout = 0 + if test_timeout > 0: + self.useFixture(fixtures.Timeout(test_timeout, gentle=True)) + + self.useFixture(fixtures.NestedTempfile()) + self.useFixture(fixtures.TempHomeDir()) + + if os.environ.get('OS_STDOUT_CAPTURE') in _TRUE_VALUES: + stdout = self.useFixture(fixtures.StringStream('stdout')).stream + self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout)) + if os.environ.get('OS_STDERR_CAPTURE') in _TRUE_VALUES: + stderr = self.useFixture(fixtures.StringStream('stderr')).stream + self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) + + self.log_fixture = self.useFixture(fixtures.FakeLogger()) \ No newline at end of file diff --git a/shade/tests/test_shade.py b/shade/tests/test_shade.py new file mode 100644 index 000000000..e8b0ac82f --- /dev/null +++ b/shade/tests/test_shade.py @@ -0,0 +1,28 @@ +# -*- 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. + +""" +test_shade +---------------------------------- + +Tests for `shade` module. +""" + +from shade.tests import base + + +class TestShade(base.TestCase): + + def test_something(self): + pass \ No newline at end of file diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 000000000..9dab35b8f --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,11 @@ +hacking>=0.5.6,<0.8 + +coverage>=3.6 +discover +fixtures>=0.3.14 +python-subunit +sphinx>=1.1.2 +oslo.sphinx +testrepository>=0.0.17 +testscenarios>=0.4,<0.5 +testtools>=0.9.32 diff --git a/tox.ini b/tox.ini new file mode 100644 index 000000000..a6a2f51eb --- /dev/null +++ b/tox.ini @@ -0,0 +1,30 @@ +[tox] +minversion = 1.6 +envlist = py26,py27,py33,pypy,pep8 +skipsdist = True + +[testenv] +usedevelop = True +install_command = pip install -U {opts} {packages} +setenv = + VIRTUAL_ENV={envdir} + LANG=en_US.UTF-8 + LANGUAGE=en_US:en + LC_ALL=C +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt +commands = python setup.py testr --slowest --testr-args='{posargs}' + +[testenv:pep8] +commands = flake8 + +[testenv:venv] +commands = {posargs} + +[testenv:cover] +commands = python setup.py testr --coverage --testr-args='{posargs}' + +[flake8] +show-source = True +builtins = _ +exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build From 97c316f0cc6c309041f19a8db253f8d130a4c4ad Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 30 Aug 2014 17:41:36 -0700 Subject: [PATCH 0002/3836] Add the initial library code This is the first workable piece of library code. --- shade/__init__.py | 228 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 218 insertions(+), 10 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index e96635bb7..a2a0488c9 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1,18 +1,226 @@ -# -*- 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 +# Copyright (c) 2014 Monty Taylor +# +# 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 +# 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. +# 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 glanceclient +from keystoneclient.v2_0 import client as keystone_client +from novaclient import exceptions as nova_exceptions +from novaclient.v1_1 import client as nova_client import pbr.version __version__ = pbr.version.VersionInfo('shade').version_string() + + +def find_nova_addresses(addresses, ext_tag, key_name=None): + + ret = [] + for (k, v) in addresses.iteritems(): + if key_name and k == key_name: + ret.extend([addrs['addr'] for addrs in v]) + else: + for interface_spec in v: + if ('OS-EXT-IPS:type' in interface_spec + and interface_spec['OS-EXT-IPS:type'] == ext_tag): + ret.append(interface_spec['addr']) + return ret + + +class OpenStackCloudException(Exception): + pass + + +class OpenStackCloud(object): + + def __init__(self, name, username, password, project_id, auth_url, + region_name, nova_service_type='compute', insecure=False, + endpoint_type='publicURL', token=None, image_cache=None, + flavor_cache=None, debug=False): + + self.name = name + self.username = username + self.password = password + self.project_id = project_id + self.auth_url = auth_url + self.region_name = region_name + self.nova_service_type = nova_service_type + self.insecure = insecure + self.endpoint_type = endpoint_type + self.token = token + self._image_cache = image_cache + self.flavor_cache = flavor_cache + self.debug = debug + + self._nova_client = None + self._glance_client = None + self._keystone_client = None + + self.log = logging.getLogger('shade') + self.log.setLevel(logging.INFO) + self.log.addHandler(logging.StreamHandler()) + + @property + def nova_client(self): + if self._nova_client is None: + kwargs = dict( + region_name=self.region_name, + service_type=self.nova_service_type, + insecure=self.insecure, + ) + # Try to use keystone directly first, for potential token reuse + try: + kwargs['auth_token'] = self.keystone_client.auth_token + kwargs['bypass_url'] = self.get_endpoint( + self.nova_service_type) + except OpenStackCloudException: + pass + + # Make the connection + self._nova_client = nova_client.Client( + self.username, + self.password, + self.project_id, + self.auth_url, + **kwargs + ) + + try: + self._nova_client.authenticate() + except nova_exceptions.Unauthorized as e: + self.logger.debug("nova Unauthorized", exc_info=True) + raise OpenStackCloudException( + "Invalid OpenStack Nova credentials.: %s" % e.message) + except nova_exceptions.AuthorizationFailure as e: + self.logger.debug("nova AuthorizationFailure", exc_info=True) + raise OpenStackCloudException( + "Unable to authorize user: %s" % e.message) + + if self._nova_client is None: + raise OpenStackCloudException( + "Failed to instantiate nova client." + " This could mean that your credentials are wrong.") + + return self._nova_client + + @property + def keystone_client(self): + if self._keystone_client is None: + # keystoneclient does crazy things with logging that are + # none of them interesting + keystone_logging = logging.getLogger('keystoneclient') + keystone_logging.addHandler(logging.NullHandler()) + + try: + if self.token: + self._keystone_client = keystone_client.Client( + endpoint=self.auth_url, + token=self.token) + else: + self._keystone_client = keystone_client.Client( + username=self.username, + password=self.password, + tenant_name=self.project_id, + region_name=self.region_name, + auth_url=self.auth_url) + except Exception as e: + self.logger.debug("keystone unknown issue", exc_info=True) + raise OpenStackCloudException( + "Error authenticating to the keystone: %s " % e.message) + return self._keystone_client + + @property + def glance_client(self): + if self._glance_client is None: + token = self.keystone_client.auth_token + endpoint = self.get_endpoint(service_type='image') + try: + # Seriously. I'm not kidding. The first argument is '1'. And + # no, it's not a version that's discoverable from keystone + self._glance_client = glanceclient.Client( + '1', endpoint, token=token) + except Exception as e: + self.logger.debug("glance unknown issue", exc_info=True) + raise OpenStackCloudException( + "Error in connecting to glance: %s" % e.message) + + if not self._glance_client: + raise OpenStackCloudException("Error connecting to glance") + return self._glance_client + + def get_name(self): + return self.name + + def get_region(self): + return self.region_name + + def get_flavor_name(self, flavor_id): + if not self.flavor_cache: + self.flavor_cache = dict( + [(flavor.id, flavor.name) + for flavor in self.nova_client.flavors.list()]) + return self.flavor_cache.get(flavor_id, None) + + def get_endpoint(self, service_type): + try: + endpoint = self.keystone_client.service_catalog.url_for( + service_type=service_type, endpoint_type=self.endpoint_type) + except Exception as e: + self.logger.debug("keystone cannot get endpoint", exc_info=True) + raise OpenStackCloudException( + "Error getting %s endpoint: %s" % (service_type, e.message)) + return endpoint + + def list_servers(self): + return self.nova_client.servers.list() + + def list_keypairs(self): + return self.nova_client.keypairs.list() + + def create_keypair(self, name, public_key): + return self.nova_client.keypairs.create(name, public_key) + + def delete_keypair(self, name): + return self.nova_client.keypairs.delete(name) + + def _get_images_from_cloud(self): + # First, try to actually get images from glance, it's more efficient + images = dict() + try: + # This can fail both because we don't have glanceclient installed + # and because the cloud may not expose the glance API publically + for image in self.glance_client.images.list(): + images[image.id] = image.name + except OpenStackCloudException: + # We didn't have glance, let's try nova + # If this doesn't work - we just let the exception propagate + for image in self.nova_client.images.list(): + images[image.id] = image.name + return images + + def list_images(self): + if self._image_cache is None: + self._image_cache = self._get_images_from_cloud() + return self._image_cache + + def get_image_name(self, image_id): + if image_id not in self.list_images(): + self._image_cache[image_id] = None + return self._image_cache[image_id] + + def get_image_id(self, image_name): + for (image_id, name) in self.list_images().items(): + if name == image_name: + return image_id + return None From f18e9e867efd8288eeeed2622ad1d8ad75a51c28 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 30 Aug 2014 17:50:38 -0700 Subject: [PATCH 0003/3836] Remove some extra lines from the README They are misleading - making it seem like docs are there. --- README.rst | 8 -------- 1 file changed, 8 deletions(-) diff --git a/README.rst b/README.rst index 9e7d32950..61389bdaf 100644 --- a/README.rst +++ b/README.rst @@ -16,11 +16,3 @@ code. Eventually, between refactoring that duplication into an internal library, and adding logic and features that the OpenStack Infra team had developed to run client applications at scale, it turned out that we'd written nine-tenths of what we'd need to have a standalone library. - -* Free software: Apache license -* Documentation: http://docs.openstack.org/developer/shade - -Features --------- - -* TODO From 4fd77e77cd5ff3f57fd05baee153457f4efa4e45 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 6 Sep 2014 17:51:56 -0700 Subject: [PATCH 0004/3836] Fix log invocations --- shade/__init__.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index a2a0488c9..e0eba1710 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -99,11 +99,11 @@ def nova_client(self): try: self._nova_client.authenticate() except nova_exceptions.Unauthorized as e: - self.logger.debug("nova Unauthorized", exc_info=True) + self.log.debug("nova Unauthorized", exc_info=True) raise OpenStackCloudException( "Invalid OpenStack Nova credentials.: %s" % e.message) except nova_exceptions.AuthorizationFailure as e: - self.logger.debug("nova AuthorizationFailure", exc_info=True) + self.log.debug("nova AuthorizationFailure", exc_info=True) raise OpenStackCloudException( "Unable to authorize user: %s" % e.message) @@ -135,7 +135,7 @@ def keystone_client(self): region_name=self.region_name, auth_url=self.auth_url) except Exception as e: - self.logger.debug("keystone unknown issue", exc_info=True) + self.log.debug("keystone unknown issue", exc_info=True) raise OpenStackCloudException( "Error authenticating to the keystone: %s " % e.message) return self._keystone_client @@ -151,7 +151,7 @@ def glance_client(self): self._glance_client = glanceclient.Client( '1', endpoint, token=token) except Exception as e: - self.logger.debug("glance unknown issue", exc_info=True) + self.log.debug("glance unknown issue", exc_info=True) raise OpenStackCloudException( "Error in connecting to glance: %s" % e.message) @@ -177,7 +177,7 @@ def get_endpoint(self, service_type): endpoint = self.keystone_client.service_catalog.url_for( service_type=service_type, endpoint_type=self.endpoint_type) except Exception as e: - self.logger.debug("keystone cannot get endpoint", exc_info=True) + self.log.debug("keystone cannot get endpoint", exc_info=True) raise OpenStackCloudException( "Error getting %s endpoint: %s" % (service_type, e.message)) return endpoint @@ -202,7 +202,8 @@ def _get_images_from_cloud(self): # and because the cloud may not expose the glance API publically for image in self.glance_client.images.list(): images[image.id] = image.name - except OpenStackCloudException: + except (OpenStackCloudException, + glanceclient.exc.HTTPInternalServerError): # We didn't have glance, let's try nova # If this doesn't work - we just let the exception propagate for image in self.nova_client.images.list(): From ccdd146a82b42883f27eae97cf13f9f9b957aba8 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 7 Sep 2014 17:53:31 -0700 Subject: [PATCH 0005/3836] Add volumes and config file parsing --- shade/__init__.py | 171 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 170 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index e0eba1710..2e7809a67 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -12,8 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +import ConfigParser import logging +import os +from cinderclient.v1 import client as cinder_client +from cinderclient import exceptions as cinder_exceptions import glanceclient from keystoneclient.v2_0 import client as keystone_client from novaclient import exceptions as nova_exceptions @@ -38,16 +42,117 @@ def find_nova_addresses(addresses, ext_tag, key_name=None): return ret +def openstack_clouds(): + return OpenStackConfig().get_all_clouds() + + +def openstack_cloud(cloud='openstack'): + return OpenStackConfig().get_one_cloud(cloud) + + class OpenStackCloudException(Exception): pass +class OpenStackConfig(object): + + _config_files = [ + os.getcwd() + "/openstack.ini", + os.getcwd() + "/nova.ini", + os.path.expanduser("~/openstack.ini"), + os.path.expanduser("~/nova.ini"), + "/etc/openstack/openstack.ini" + "/etc/openstack/nova.ini" + ] + + def __init__(self, config_files=None): + if config_files: + self._config_files = config_files + + OS_USERNAME = os.environ.get('OS_USERNAME', 'admin') + OS_DEFAULTS = { + 'username': OS_USERNAME, + 'password': os.environ.get('OS_PASSWORD', ''), + 'project_id': os.environ.get( + 'OS_TENANT_NAME', + os.environ.get('OS_PROJECT_ID', OS_USERNAME)), + 'auth_url': os.environ.get( + 'OS_AUTH_URL', 'https://127.0.0.1:35357/v2.0/'), + 'region_name': os.environ.get('OS_REGION_NAME', ''), + 'insecure': 'false', + # historical + 'service_type': 'compute', + 'cache_max_age': '300', + 'cache_path': '~/.cache/openstack', + } + + # use a config file if it exists where expected + self.config = self._load_config_file(OS_DEFAULTS) + + self.cloud_sections = [ + section for section in self.config.sections() + if section != 'cache' ] + if not self.cloud_sections: + # Add a default section so that our cloud defaults always work + self.config.add_section('openstack') + self.cloud_sections = ['openstack'] + + def _load_config_file(self, defaults): + p = ConfigParser.SafeConfigParser(defaults) + + for path in self._config_files: + if os.path.exists(path): + p.read(path) + return p + return p + + def _get_region(self, cloud): + return self.config.get(cloud, 'region_name') + + def get_all_clouds(self): + + clouds = [] + + for cloud in self.cloud_sections: + if cloud == 'cache': + continue + + for region in self._get_region(cloud).split(','): + clouds.append(self.get_one_cloud(cloud, region)) + return clouds + + def get_one_cloud(self, name='openstack', region=None): + + if not region: + region = self._get_region(name) + + client_params = dict(name=name) + client_params['username'] = self.config.get(name, 'username') + client_params['password'] = self.config.get(name, 'password') + client_params['project_id'] = self.config.get(name, 'project_id') + client_params['auth_url'] = self.config.get(name, 'auth_url') + client_params['region_name'] = region + client_params['nova_service_type'] = self.config.get(name, 'service_type') + client_params['insecure'] = self.config.getboolean(name, 'insecure') + # Provide backwards compat for older nova.ini files + if client_params['password'] == '': + client_params['password'] = self.config.get(name, 'api_key') + + if (client_params['username'] == "" and client_params['password'] == ""): + sys.exit( + 'Unable to find auth information for cloud %s' + ' in config files %s or environment variables' + % (name, ','.join(self._config_files))) + + return OpenStackCloud(**client_params) + + class OpenStackCloud(object): def __init__(self, name, username, password, project_id, auth_url, region_name, nova_service_type='compute', insecure=False, endpoint_type='publicURL', token=None, image_cache=None, - flavor_cache=None, debug=False): + flavor_cache=None, volume_cache=None,debug=False): self.name = name self.username = username @@ -61,11 +166,13 @@ def __init__(self, name, username, password, project_id, auth_url, self.token = token self._image_cache = image_cache self.flavor_cache = flavor_cache + self._volume_cache = volume_cache self.debug = debug self._nova_client = None self._glance_client = None self._keystone_client = None + self._cinder_client = None self.log = logging.getLogger('shade') self.log.setLevel(logging.INFO) @@ -159,6 +266,37 @@ def glance_client(self): raise OpenStackCloudException("Error connecting to glance") return self._glance_client + @property + def cinder_client(self): + + if self._cinder_client is None: + # Make the connection + self._cinder_client = cinder_client.Client( + self.username, + self.password, + self.project_id, + self.auth_url, + region_name=self.region_name, + ) + + try: + self._cinder_client.authenticate() + except cinder_exceptions.Unauthorized, e: + self.log.debug("cinder Unauthorized", exc_info=True) + raise OpenStackCloudException( + "Invalid OpenStack Cinder credentials.: %s" % e.message) + except cinder_exceptions.AuthorizationFailure, e: + self.log.debug("cinder AuthorizationFailure", exc_info=True) + raise OpenStackCloudException( + "Unable to authorize user: %s" % e.message) + + if self._cinder_client is None: + raise OpenStackCloudException( + "Failed to instantiate cinder client. This could mean that your" + " credentials are wrong.") + + return self._cinder_client + def get_name(self): return self.name @@ -225,3 +363,34 @@ def get_image_id(self, image_name): if name == image_name: return image_id return None + + def _get_volumes_from_cloud(self): + try: + return self.cinder_client.volumes.list() + except Exception: + return [] + + def list_volumes(self): + if self._volume_cache is None: + self._volume_cache = self._get_volumes_from_cloud() + return self._volume_cache + + def get_volumes(self, server): + volumes = [] + for volume in self.list_volumes(): + for attach in volume.attachments: + if attach['server_id'] == server.id: + volumes.append(volume) + return volumes + + def get_volume_id(self, volume_name): + for v in self.cinder_client.volumes.list(): + if v.display_name == volume_name: + return v.id + return None + + def get_server_id(self, server_name): + for server in self.nova_client.servers.list(): + if server.name == server_name: + return server.id + return None From 64292c803109a748e23b60e7a26aa10b619e0b94 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 7 Sep 2014 17:55:48 -0700 Subject: [PATCH 0006/3836] Add example code to README --- README.rst | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.rst b/README.rst index 61389bdaf..822e16955 100644 --- a/README.rst +++ b/README.rst @@ -16,3 +16,30 @@ code. Eventually, between refactoring that duplication into an internal library, and adding logic and features that the OpenStack Infra team had developed to run client applications at scale, it turned out that we'd written nine-tenths of what we'd need to have a standalone library. + +example +------- + +Sometimes an example is nice. +:: + + from shade import * + import time + + cloud = openstack_cloud('mordred') + + nova = cloud.nova_client + print nova.servers.list() + s = nova.servers.list()[0] + + cinder = cloud.cinder_client + volumes = cinder.volumes.list() + print volumes + volume_id = [v for v in volumes if v.status == 'available'][0].id + nova.volumes.create_server_volume(s.id, volume_id, None) + attachments = [] + while not attachments: + print "Waiting for attach to finish" + time.sleep(1) + attachments = cinder.volumes.get(volume_id).attachments + print attachments From edadf145f3846c95b7faf48c4543bbd15765231f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 20 Sep 2014 16:16:13 -0700 Subject: [PATCH 0007/3836] Initial Cookiecutter Commit. --- .coveragerc | 7 + .gitignore | 52 ++++++ .gitreview | 4 + .mailmap | 3 + .testr.conf | 7 + CONTRIBUTING.rst | 17 ++ HACKING.rst | 4 + LICENSE | 175 ++++++++++++++++++ MANIFEST.in | 6 + README.rst | 15 ++ babel.cfg | 1 + doc/source/conf.py | 75 ++++++++ doc/source/contributing.rst | 4 + doc/source/index.rst | 24 +++ doc/source/installation.rst | 12 ++ doc/source/readme.rst | 1 + doc/source/usage.rst | 7 + openstack-common.conf | 6 + os_client_config/__init__.py | 19 ++ os_client_config/tests/__init__.py | 0 os_client_config/tests/base.py | 23 +++ .../tests/test_os_client_config.py | 28 +++ requirements.txt | 6 + setup.cfg | 47 +++++ setup.py | 22 +++ test-requirements.txt | 15 ++ tox.ini | 34 ++++ 27 files changed, 614 insertions(+) create mode 100644 .coveragerc create mode 100644 .gitignore create mode 100644 .gitreview create mode 100644 .mailmap create mode 100644 .testr.conf create mode 100644 CONTRIBUTING.rst create mode 100644 HACKING.rst create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.rst create mode 100644 babel.cfg create mode 100755 doc/source/conf.py create mode 100644 doc/source/contributing.rst create mode 100644 doc/source/index.rst create mode 100644 doc/source/installation.rst create mode 100644 doc/source/readme.rst create mode 100644 doc/source/usage.rst create mode 100644 openstack-common.conf create mode 100644 os_client_config/__init__.py create mode 100644 os_client_config/tests/__init__.py create mode 100644 os_client_config/tests/base.py create mode 100644 os_client_config/tests/test_os_client_config.py create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100755 setup.py create mode 100644 test-requirements.txt create mode 100644 tox.ini diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..5f0c7fd8b --- /dev/null +++ b/.coveragerc @@ -0,0 +1,7 @@ +[run] +branch = True +source = os_client_config +omit = os_client_config/tests/*,os_client_config/openstack/* + +[report] +ignore-errors = True \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..e6a97ec61 --- /dev/null +++ b/.gitignore @@ -0,0 +1,52 @@ +*.py[cod] + +# C extensions +*.so + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg +lib +lib64 + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox +nosetests.xml +.testrepository + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Complexity +output/*.html +output/*/index.html + +# Sphinx +doc/build + +# pbr generates these +AUTHORS +ChangeLog + +# Editors +*~ +.*.swp +.*sw? \ No newline at end of file diff --git a/.gitreview b/.gitreview new file mode 100644 index 000000000..69bff3ae3 --- /dev/null +++ b/.gitreview @@ -0,0 +1,4 @@ +[gerrit] +host=review.openstack.org +port=29418 +project=openstack/os-client-config.git \ No newline at end of file diff --git a/.mailmap b/.mailmap new file mode 100644 index 000000000..cc92f17b8 --- /dev/null +++ b/.mailmap @@ -0,0 +1,3 @@ +# Format is: +# +# \ No newline at end of file diff --git a/.testr.conf b/.testr.conf new file mode 100644 index 000000000..fb622677a --- /dev/null +++ b/.testr.conf @@ -0,0 +1,7 @@ +[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 ./ . $LISTOPT $IDOPTION +test_id_option=--load-list $IDFILE +test_list_option=--list \ No newline at end of file diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 000000000..c1f3dc297 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,17 @@ +If you would like to contribute to the development of OpenStack, +you must follow the steps in the "If you're a developer, start here" +section of this page: + + http://wiki.openstack.org/HowToContribute + +Once those steps have been completed, changes to OpenStack +should be submitted for review via the Gerrit tool, following +the workflow documented at: + + http://wiki.openstack.org/GerritWorkflow + +Pull requests submitted through GitHub will be ignored. + +Bugs should be filed on Launchpad, not GitHub: + + https://bugs.launchpad.net/os-client-config \ No newline at end of file diff --git a/HACKING.rst b/HACKING.rst new file mode 100644 index 000000000..c995c5c92 --- /dev/null +++ b/HACKING.rst @@ -0,0 +1,4 @@ +os-client-config Style Commandments +=============================================== + +Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..67db85882 --- /dev/null +++ b/LICENSE @@ -0,0 +1,175 @@ + + 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. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 000000000..90f8a7aef --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +include AUTHORS +include ChangeLog +exclude .gitignore +exclude .gitreview + +global-exclude *.pyc \ No newline at end of file diff --git a/README.rst b/README.rst new file mode 100644 index 000000000..cf2315d32 --- /dev/null +++ b/README.rst @@ -0,0 +1,15 @@ +=============================== +os-client-config +=============================== + +OpenStack Client Configuation Library + +* Free software: Apache license +* Documentation: http://docs.openstack.org/developer/os-client-config +* Source: http://git.openstack.org/cgit/openstack/os-client-config +* Bugs: http://bugs.launchpad.net/os-client-config + +Features +-------- + +* TODO \ No newline at end of file diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 000000000..efceab818 --- /dev/null +++ b/babel.cfg @@ -0,0 +1 @@ +[python: **.py] diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100755 index 000000000..02be16776 --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,75 @@ +# -*- 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. + +import os +import sys + +sys.path.insert(0, os.path.abspath('../..')) +# -- General configuration ---------------------------------------------------- + +# 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.intersphinx', + 'oslosphinx' +] + +# autodoc generation is a bit aggressive and a nuisance when doing heavy +# text edit cycles. +# execute "export SPHINX_DEBUG=1" in your terminal to disable + +# The suffix of source filenames. +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'os-client-config' +copyright = u'2013, OpenStack Foundation' + +# 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 + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# -- Options for HTML output -------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. +# html_theme_path = ["."] +# html_theme = '_theme' +# html_static_path = ['static'] + +# Output file base name for HTML help builder. +htmlhelp_basename = '%sdoc' % project + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass +# [howto/manual]). +latex_documents = [ + ('index', + '%s.tex' % project, + u'%s Documentation' % project, + u'OpenStack Foundation', 'manual'), +] + +# Example configuration for intersphinx: refer to the Python standard library. +#intersphinx_mapping = {'http://docs.python.org/': None} \ No newline at end of file diff --git a/doc/source/contributing.rst b/doc/source/contributing.rst new file mode 100644 index 000000000..ed77c1262 --- /dev/null +++ b/doc/source/contributing.rst @@ -0,0 +1,4 @@ +============ +Contributing +============ +.. include:: ../../CONTRIBUTING.rst \ No newline at end of file diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 000000000..2c8b52d5e --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,24 @@ +.. os-client-config documentation master file, created by + sphinx-quickstart on Tue Jul 9 22:26:36 2013. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to os-client-config's documentation! +======================================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + readme + installation + usage + contributing + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/doc/source/installation.rst b/doc/source/installation.rst new file mode 100644 index 000000000..48bbc2f20 --- /dev/null +++ b/doc/source/installation.rst @@ -0,0 +1,12 @@ +============ +Installation +============ + +At the command line:: + + $ pip install os-client-config + +Or, if you have virtualenvwrapper installed:: + + $ mkvirtualenv os-client-config + $ pip install os-client-config \ No newline at end of file diff --git a/doc/source/readme.rst b/doc/source/readme.rst new file mode 100644 index 000000000..38ba8043d --- /dev/null +++ b/doc/source/readme.rst @@ -0,0 +1 @@ +.. include:: ../../README.rst \ No newline at end of file diff --git a/doc/source/usage.rst b/doc/source/usage.rst new file mode 100644 index 000000000..910fd0790 --- /dev/null +++ b/doc/source/usage.rst @@ -0,0 +1,7 @@ +======== +Usage +======== + +To use os-client-config in a project:: + + import os_client_config \ No newline at end of file diff --git a/openstack-common.conf b/openstack-common.conf new file mode 100644 index 000000000..e8eb2aa2c --- /dev/null +++ b/openstack-common.conf @@ -0,0 +1,6 @@ +[DEFAULT] + +# The list of modules to copy from oslo-incubator.git + +# The base module to hold the copy of openstack.common +base=os_client_config \ No newline at end of file diff --git a/os_client_config/__init__.py b/os_client_config/__init__.py new file mode 100644 index 000000000..26bdf7e34 --- /dev/null +++ b/os_client_config/__init__.py @@ -0,0 +1,19 @@ +# -*- 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. + +import pbr.version + + +__version__ = pbr.version.VersionInfo( + 'os_client_config').version_string() \ No newline at end of file diff --git a/os_client_config/tests/__init__.py b/os_client_config/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py new file mode 100644 index 000000000..185fd6fd9 --- /dev/null +++ b/os_client_config/tests/base.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +# Copyright 2010-2011 OpenStack Foundation +# 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. + +from oslotest import base + + +class TestCase(base.BaseTestCase): + + """Test case base class for all unit tests.""" \ No newline at end of file diff --git a/os_client_config/tests/test_os_client_config.py b/os_client_config/tests/test_os_client_config.py new file mode 100644 index 000000000..9b26ad547 --- /dev/null +++ b/os_client_config/tests/test_os_client_config.py @@ -0,0 +1,28 @@ +# -*- 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. + +""" +test_os_client_config +---------------------------------- + +Tests for `os_client_config` module. +""" + +from os_client_config.tests import base + + +class TestOs_client_config(base.TestCase): + + def test_something(self): + pass \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..bc7131e80 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +# 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>=0.6,!=0.7,<1.0 +Babel>=1.3 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..269052910 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,47 @@ +[metadata] +name = os-client-config +summary = OpenStack Client Configuation Library +description-file = + README.rst +author = OpenStack +author-email = openstack-dev@lists.openstack.org +home-page = http://www.openstack.org/ +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 :: 2 + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 2.6 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.3 + Programming Language :: Python :: 3.4 + +[files] +packages = + os_client_config + +[build_sphinx] +source-dir = doc/source +build-dir = doc/build +all_files = 1 + +[upload_sphinx] +upload-dir = doc/build/html + +[compile_catalog] +directory = os_client_config/locale +domain = os-client-config + +[update_catalog] +domain = os-client-config +output_dir = os_client_config/locale +input_file = os_client_config/locale/os-client-config.pot + +[extract_messages] +keywords = _ gettext ngettext l_ lazy_gettext +mapping_file = babel.cfg +output_file = os_client_config/locale/os-client-config.pot \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100755 index 000000000..7eeb36b53 --- /dev/null +++ b/setup.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +# 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 + +setuptools.setup( + setup_requires=['pbr'], + pbr=True) \ No newline at end of file diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 000000000..7b79352b4 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,15 @@ +# 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.9.2,<0.10 + +coverage>=3.6 +discover +python-subunit +sphinx>=1.1.2 +oslosphinx +oslotest>=1.1.0.0a1 +testrepository>=0.0.18 +testscenarios>=0.4 +testtools>=0.9.34 \ No newline at end of file diff --git a/tox.ini b/tox.ini new file mode 100644 index 000000000..9be310a4c --- /dev/null +++ b/tox.ini @@ -0,0 +1,34 @@ +[tox] +minversion = 1.6 +envlist = py33,py34,py26,py27,pypy,pep8 +skipsdist = True + +[testenv] +usedevelop = True +install_command = pip install -U {opts} {packages} +setenv = + VIRTUAL_ENV={envdir} +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt +commands = python setup.py testr --slowest --testr-args='{posargs}' + +[testenv:pep8] +commands = flake8 + +[testenv:venv] +commands = {posargs} + +[testenv:cover] +commands = python setup.py testr --coverage --testr-args='{posargs}' + +[testenv:docs] +commands = python setup.py build_sphinx + +[flake8] +# H803 skipped on purpose per list discussion. +# E123, E125 skipped as they are invalid PEP-8. + +show-source = True +ignore = E123,E125,H803 +builtins = _ +exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build \ No newline at end of file From 6efe00dbf3959ebfbb49045f357823da0b94c3e0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 21 Sep 2014 12:16:20 -0700 Subject: [PATCH 0008/3836] Port in config reading from shade --- README.rst | 109 ++++++++++++++++++++-- os_client_config/__init__.py | 10 +- os_client_config/cloud_config.py | 20 ++++ os_client_config/config.py | 150 ++++++++++++++++++++++++++++++ os_client_config/defaults_dict.py | 27 ++++++ os_client_config/exceptions.py | 17 ++++ os_client_config/vendors.py | 25 +++++ setup.py | 2 +- 8 files changed, 346 insertions(+), 14 deletions(-) create mode 100644 os_client_config/cloud_config.py create mode 100644 os_client_config/config.py create mode 100644 os_client_config/defaults_dict.py create mode 100644 os_client_config/exceptions.py create mode 100644 os_client_config/vendors.py diff --git a/README.rst b/README.rst index cf2315d32..6332f8391 100644 --- a/README.rst +++ b/README.rst @@ -2,14 +2,111 @@ os-client-config =============================== -OpenStack Client Configuation Library +os-client-config is a library for collecting client configuration for +using an OpenStack cloud in a consistent and comprehensive manner. It +will find cloud config for as few as 1 cloud and as many as you want to +put in a config file. It will read environment variables and config files, +and it also contains some vendor specific default values so that you don't +have to know extra info to use OpenStack + +Environment Variables +--------------------- + +os-client-config honors all of the normal `OS_*` variables. It does not +provide backwards compatibility to service-specific variables such as +`NOVA_USERNAME`. + +If you have environment variables and no config files, os-client-config +will produce a cloud config object named "openstack" containing your +values from the environment. + +Service specific settings, like the nova service type, are set with the +default service type as a prefix. For instance, to set a special service_type +for trove (because you're using Rackspace) set: +:: + + export OS_DATABASE_SERVICE_TYPE=rax:database + +Config Files +------------ + +os-client-config will for a file called clouds.yaml in the following locations: +* Current Directory +* ~/.config/openstack +* /etc/openstack + +The keys are all of the keys you'd expect from `OS_*` - except lower case +and without the OS prefix. So, username is set with `username`. + +Service specific settings, like the nova service type, are set with the +default service type as a prefix. For instance, to set a special service_type +for trove (because you're using Rackspace) set: +:: + + database_service_type: 'rax:database' + +An example config file is probably helpful: +:: + + clouds: + mordred: + cloud: hp + username: mordred@inaugust.com + password: XXXXXXXXX + project_id: mordred@inaugust.com + region_name: region-b.geo-1 + monty: + auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0 + username: monty.taylor@hp.com + password: XXXXXXXX + project_id: monty.taylor@hp.com-default-tenant + region_name: region-b.geo-1 + infra: + cloud: rackspace + username: openstackci + password: XXXXXXXX + project_id: 610275 + region_name: DFW,ORD,IAD + +You may note a few things. First, since auth_url settings are silly +and embarrasingly ugly, known cloud vendors are included and may be referrenced +by name. One of the benefits of that is that auth_url isn't the only thing +the vendor defaults contain. For instance, since Rackspace is broken and lists +`rax:database` as the service type for trove, os-client-config knows that +so that you don't have to. + +Also, region_name can be a list of regions. When you call get_all_clouds, +you'll get a cloud config object for each cloud/region combo. + +Usage +----- + +The simplest and least useful thing you can do is: +:: + + python -m os_client_config.config + +Which will print out whatever if finds for your config. If you want to use +it from python, which is much more likely what you want to do, things like: + +Get a named cloud. +:: + + import os_client_config + + cloud_config = os_client_config.OpenStackConfig().get_one_cloud( + 'hp', 'region-b.geo-1') + print(cloud_config.name, cloud_config.region, cloud_config.config) + +Or, get all of the clouds. +:: + import os_client_config + + cloud_config = os_client_config.OpenStackConfig().get_all_clouds() + for cloud in cloud_config: + print(cloud.name, cloud.region, cloud.config) * Free software: Apache license * Documentation: http://docs.openstack.org/developer/os-client-config * Source: http://git.openstack.org/cgit/openstack/os-client-config * Bugs: http://bugs.launchpad.net/os-client-config - -Features --------- - -* TODO \ No newline at end of file diff --git a/os_client_config/__init__.py b/os_client_config/__init__.py index 26bdf7e34..d5fd36cb6 100644 --- a/os_client_config/__init__.py +++ b/os_client_config/__init__.py @@ -1,5 +1,5 @@ -# -*- coding: utf-8 -*- - +# Copyright (c) 2014 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 @@ -12,8 +12,4 @@ # License for the specific language governing permissions and limitations # under the License. -import pbr.version - - -__version__ = pbr.version.VersionInfo( - 'os_client_config').version_string() \ No newline at end of file +from os_client_config.config import OpenStackConfig # noqa diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py new file mode 100644 index 000000000..d0c932f41 --- /dev/null +++ b/os_client_config/cloud_config.py @@ -0,0 +1,20 @@ +# Copyright (c) 2014 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. + + +class CloudConfig(object): + def __init__(self, name, region, config): + self.name = name + self.region = region + self.config = config diff --git a/os_client_config/config.py b/os_client_config/config.py new file mode 100644 index 000000000..1742b5b16 --- /dev/null +++ b/os_client_config/config.py @@ -0,0 +1,150 @@ +# Copyright (c) 2014 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. + + +import os + +import yaml + +from os_client_config import cloud_config +from os_client_config import defaults_dict +from os_client_config import exceptions +from os_client_config import vendors + +CONFIG_HOME = os.path.join(os.path.expanduser( + os.environ.get('XDG_CONFIG_HOME', os.path.join('~', '.config'))), + 'openstack') +CONFIG_SEARCH_PATH = [os.getcwd(), CONFIG_HOME, '/etc/openstack'] +CONFIG_FILES = [ + os.path.join(d, 'clouds.yaml') for d in CONFIG_SEARCH_PATH] +BOOL_KEYS = ('insecure', 'cache') +REQUIRED_VALUES = ('auth_url', 'username', 'password', 'project_id') +SERVICES = ( + 'compute', 'identity', 'network', 'metering', 'object-store', + 'volume', 'dns', 'image', 'database') + + +def get_boolean(value): + if value.lower() == 'true': + return True + return False + + +class OpenStackConfig(object): + + def __init__(self, config_files=None): + self._config_files = config_files or CONFIG_FILES + + defaults = defaults_dict.DefaultsDict() + defaults.add('username') + defaults.add('user_domain_name') + defaults.add('password') + defaults.add('project_id', defaults['username'], also='tenant_name') + defaults.add('project_domain_name') + defaults.add('auth_url') + defaults.add('region_name') + defaults.add('cache', 'false') + defaults.add('auth_token') + defaults.add('insecure', 'false') + defaults.add('cacert') + + for service in SERVICES: + defaults.add('service_name', prefix=service) + defaults.add('service_type', prefix=service) + defaults.add('endpoint_type', prefix=service) + defaults.add('endpoint', prefix=service) + self.defaults = defaults + + # use a config file if it exists where expected + self.cloud_config = self._load_config_file() + if not self.cloud_config: + self.cloud_config = dict( + clouds=dict(openstack=dict(self.defaults))) + + @classmethod + def get_services(klass): + return SERVICES + + def _load_config_file(self): + for path in self._config_files: + if os.path.exists(path): + return yaml.load(open(path, 'r')) + + def _get_regions(self, cloud): + return self.cloud_config['clouds'][cloud]['region_name'] + + def _get_region(self, cloud): + return self._get_regions(cloud).split(',')[0] + + def _get_cloud_sections(self): + return self.cloud_config['clouds'].keys() + + def _get_base_cloud_config(self, name): + cloud = dict() + if name in self.cloud_config['clouds']: + our_cloud = self.cloud_config['clouds'][name] + else: + our_cloud = dict() + + # yes, I know the next line looks silly + if 'cloud' in our_cloud: + cloud.update(vendors.CLOUD_DEFAULTS[our_cloud['cloud']]) + + cloud.update(self.defaults) + cloud.update(our_cloud) + if 'cloud' in cloud: + del cloud['cloud'] + return cloud + + def get_all_clouds(self): + + clouds = [] + + for cloud in self._get_cloud_sections(): + for region in self._get_regions(cloud).split(','): + clouds.append(self.get_one_cloud(cloud, region)) + return clouds + + def get_one_cloud(self, name='openstack', region=None): + + if not region: + region = self._get_region(name) + + config = self._get_base_cloud_config(name) + config['region_name'] = region + + for key in BOOL_KEYS: + if key in config: + config[key] = get_boolean(config[key]) + + for key in REQUIRED_VALUES: + if key not in config or not config[key]: + raise exceptions.OpenStackConfigException( + 'Unable to find full auth information for cloud {name} in' + ' config files {files}' + ' or environment variables.'.format( + name=name, files=','.join(self._config_files))) + + # If any of the defaults reference other values, we need to expand + for (key, value) in config.items(): + if hasattr(value, 'format'): + config[key] = value.format(**config) + + return cloud_config.CloudConfig( + name=name, region=region, config=config) + +if __name__ == '__main__': + config = OpenStackConfig().get_all_clouds() + for cloud in config: + print(cloud.name, cloud.region, cloud.config) diff --git a/os_client_config/defaults_dict.py b/os_client_config/defaults_dict.py new file mode 100644 index 000000000..43de77beb --- /dev/null +++ b/os_client_config/defaults_dict.py @@ -0,0 +1,27 @@ +# Copyright (c) 2014 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. + +import os + + +class DefaultsDict(dict): + + def add(self, key, default_value=None, also=None, prefix=None): + if prefix: + key = '%s_%s' % (prefix.replace('-', '_'), key) + if also: + value = os.environ.get(also, default_value) + value = os.environ.get('OS_%s' % key.upper(), default_value) + if value is not None: + self.__setitem__(key, value) diff --git a/os_client_config/exceptions.py b/os_client_config/exceptions.py new file mode 100644 index 000000000..ab78dc2e5 --- /dev/null +++ b/os_client_config/exceptions.py @@ -0,0 +1,17 @@ +# Copyright (c) 2014 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. + + +class OpenStackConfigException(Exception): + """Something went wrong with parsing your OpenStack Config.""" diff --git a/os_client_config/vendors.py b/os_client_config/vendors.py new file mode 100644 index 000000000..d1b29a588 --- /dev/null +++ b/os_client_config/vendors.py @@ -0,0 +1,25 @@ +# Copyright (c) 2014 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. + +CLOUD_DEFAULTS = dict( + hp=dict( + auth_url='https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0', + region_name='region-b.geo-1', + ), + rackspace=dict( + auth_url='https://identity.api.rackspacecloud.com/v2.0/', + image_endpoint='https://{region_name}.images.api.rackspacecloud.com/', + database_service_type='rax:database', + ) +) diff --git a/setup.py b/setup.py index 7eeb36b53..70c2b3f32 100755 --- a/setup.py +++ b/setup.py @@ -19,4 +19,4 @@ setuptools.setup( setup_requires=['pbr'], - pbr=True) \ No newline at end of file + pbr=True) From 9bbb4f30f49eb157d31a9aa1c37aca3004656009 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 21 Sep 2014 12:20:55 -0700 Subject: [PATCH 0009/3836] Remove babel and add pyyaml --- babel.cfg | 1 - requirements.txt | 3 ++- setup.cfg | 14 -------------- test-requirements.txt | 2 +- 4 files changed, 3 insertions(+), 17 deletions(-) delete mode 100644 babel.cfg diff --git a/babel.cfg b/babel.cfg deleted file mode 100644 index efceab818..000000000 --- a/babel.cfg +++ /dev/null @@ -1 +0,0 @@ -[python: **.py] diff --git a/requirements.txt b/requirements.txt index bc7131e80..9cf3f8ae0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ # process, which may cause wedges in the gate later. pbr>=0.6,!=0.7,<1.0 -Babel>=1.3 \ No newline at end of file + +PyYAML diff --git a/setup.cfg b/setup.cfg index 269052910..df3434f0f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,17 +31,3 @@ all_files = 1 [upload_sphinx] upload-dir = doc/build/html - -[compile_catalog] -directory = os_client_config/locale -domain = os-client-config - -[update_catalog] -domain = os-client-config -output_dir = os_client_config/locale -input_file = os_client_config/locale/os-client-config.pot - -[extract_messages] -keywords = _ gettext ngettext l_ lazy_gettext -mapping_file = babel.cfg -output_file = os_client_config/locale/os-client-config.pot \ No newline at end of file diff --git a/test-requirements.txt b/test-requirements.txt index 7b79352b4..d494076d9 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -12,4 +12,4 @@ oslosphinx oslotest>=1.1.0.0a1 testrepository>=0.0.18 testscenarios>=0.4 -testtools>=0.9.34 \ No newline at end of file +testtools>=0.9.34 From 69d2a3e0ad97a20cec498ab87c81af5e11e5c79c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 21 Sep 2014 14:17:38 -0700 Subject: [PATCH 0010/3836] Get rid of extra complexity with service values We don't need to enumerate the service types - we can simply match at consumption time on patterns. --- os_client_config/config.py | 13 +++---------- os_client_config/vendors.py | 2 +- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 1742b5b16..5cc8edd78 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -30,9 +30,6 @@ os.path.join(d, 'clouds.yaml') for d in CONFIG_SEARCH_PATH] BOOL_KEYS = ('insecure', 'cache') REQUIRED_VALUES = ('auth_url', 'username', 'password', 'project_id') -SERVICES = ( - 'compute', 'identity', 'network', 'metering', 'object-store', - 'volume', 'dns', 'image', 'database') def get_boolean(value): @@ -54,16 +51,12 @@ def __init__(self, config_files=None): defaults.add('project_domain_name') defaults.add('auth_url') defaults.add('region_name') - defaults.add('cache', 'false') + defaults.add('cache') defaults.add('auth_token') - defaults.add('insecure', 'false') + defaults.add('insecure') + defaults.add('endpoint_type') defaults.add('cacert') - for service in SERVICES: - defaults.add('service_name', prefix=service) - defaults.add('service_type', prefix=service) - defaults.add('endpoint_type', prefix=service) - defaults.add('endpoint', prefix=service) self.defaults = defaults # use a config file if it exists where expected diff --git a/os_client_config/vendors.py b/os_client_config/vendors.py index d1b29a588..71cf15896 100644 --- a/os_client_config/vendors.py +++ b/os_client_config/vendors.py @@ -16,10 +16,10 @@ hp=dict( auth_url='https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0', region_name='region-b.geo-1', + dns_service_type='hp:dns', ), rackspace=dict( auth_url='https://identity.api.rackspacecloud.com/v2.0/', - image_endpoint='https://{region_name}.images.api.rackspacecloud.com/', database_service_type='rax:database', ) ) From c938bb9dbe3dcc5c9476e3d257bfde76e6093964 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 21 Sep 2014 14:22:10 -0700 Subject: [PATCH 0011/3836] Offload config to the os-client-config library Also, while we're at it, add initial trove support. --- requirements.txt | 3 + shade/__init__.py | 223 +++++++++++++++++++++------------------------- 2 files changed, 103 insertions(+), 123 deletions(-) diff --git a/requirements.txt b/requirements.txt index ed455a22b..7bb0f8b74 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,10 @@ pbr>=0.5.21,<1.0 +os-client-config + python-novaclient python-keystoneclient python-glanceclient python-cinderclient python-neutronclient +python-troveclient diff --git a/shade/__init__.py b/shade/__init__.py index 2e7809a67..ee6adeaf7 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import ConfigParser import logging import os @@ -22,6 +21,9 @@ from keystoneclient.v2_0 import client as keystone_client from novaclient import exceptions as nova_exceptions from novaclient.v1_1 import client as nova_client +import os_client_config +import troveclient.client as trove_client +from troveclient import exceptions as trove_exceptions import pbr.version @@ -42,155 +44,83 @@ def find_nova_addresses(addresses, ext_tag, key_name=None): return ret -def openstack_clouds(): - return OpenStackConfig().get_all_clouds() +class OpenStackCloudException(Exception): + pass -def openstack_cloud(cloud='openstack'): - return OpenStackConfig().get_one_cloud(cloud) +def openstack_clouds(): + return [OpenStackCloud(f.name, f.region, **f.config) + for f in os_client_config.OpenStackConfig().get_all_clouds()] -class OpenStackCloudException(Exception): - pass +def openstack_cloud(cloud='openstack', region=None): + cloud_config = os_client_config.OpenStackConfig().get_one_cloud( + cloud, region) + return OpenStackCloud( + cloud_config.name, cloud_config.region, **cloud_config.config) -class OpenStackConfig(object): - - _config_files = [ - os.getcwd() + "/openstack.ini", - os.getcwd() + "/nova.ini", - os.path.expanduser("~/openstack.ini"), - os.path.expanduser("~/nova.ini"), - "/etc/openstack/openstack.ini" - "/etc/openstack/nova.ini" - ] - - def __init__(self, config_files=None): - if config_files: - self._config_files = config_files - - OS_USERNAME = os.environ.get('OS_USERNAME', 'admin') - OS_DEFAULTS = { - 'username': OS_USERNAME, - 'password': os.environ.get('OS_PASSWORD', ''), - 'project_id': os.environ.get( - 'OS_TENANT_NAME', - os.environ.get('OS_PROJECT_ID', OS_USERNAME)), - 'auth_url': os.environ.get( - 'OS_AUTH_URL', 'https://127.0.0.1:35357/v2.0/'), - 'region_name': os.environ.get('OS_REGION_NAME', ''), - 'insecure': 'false', - # historical - 'service_type': 'compute', - 'cache_max_age': '300', - 'cache_path': '~/.cache/openstack', - } - - # use a config file if it exists where expected - self.config = self._load_config_file(OS_DEFAULTS) - - self.cloud_sections = [ - section for section in self.config.sections() - if section != 'cache' ] - if not self.cloud_sections: - # Add a default section so that our cloud defaults always work - self.config.add_section('openstack') - self.cloud_sections = ['openstack'] - - def _load_config_file(self, defaults): - p = ConfigParser.SafeConfigParser(defaults) - - for path in self._config_files: - if os.path.exists(path): - p.read(path) - return p - return p - - def _get_region(self, cloud): - return self.config.get(cloud, 'region_name') - - def get_all_clouds(self): - - clouds = [] - - for cloud in self.cloud_sections: - if cloud == 'cache': - continue - - for region in self._get_region(cloud).split(','): - clouds.append(self.get_one_cloud(cloud, region)) - return clouds - - def get_one_cloud(self, name='openstack', region=None): - - if not region: - region = self._get_region(name) - - client_params = dict(name=name) - client_params['username'] = self.config.get(name, 'username') - client_params['password'] = self.config.get(name, 'password') - client_params['project_id'] = self.config.get(name, 'project_id') - client_params['auth_url'] = self.config.get(name, 'auth_url') - client_params['region_name'] = region - client_params['nova_service_type'] = self.config.get(name, 'service_type') - client_params['insecure'] = self.config.getboolean(name, 'insecure') - # Provide backwards compat for older nova.ini files - if client_params['password'] == '': - client_params['password'] = self.config.get(name, 'api_key') - - if (client_params['username'] == "" and client_params['password'] == ""): - sys.exit( - 'Unable to find auth information for cloud %s' - ' in config files %s or environment variables' - % (name, ','.join(self._config_files))) - - return OpenStackCloud(**client_params) +def _get_service_values(kwargs, service_key): + return { k[:-(len(service_key) + 1)] : kwargs[k] + for k in kwargs.keys() if k.endswith(service_key) } class OpenStackCloud(object): - def __init__(self, name, username, password, project_id, auth_url, - region_name, nova_service_type='compute', insecure=False, - endpoint_type='publicURL', token=None, image_cache=None, - flavor_cache=None, volume_cache=None,debug=False): + def __init__(self, name, region='', + image_cache=None, flavor_cache=None, volume_cache=None, + debug=False, **kwargs): self.name = name - self.username = username - self.password = password - self.project_id = project_id - self.auth_url = auth_url - self.region_name = region_name - self.nova_service_type = nova_service_type - self.insecure = insecure - self.endpoint_type = endpoint_type - self.token = token + self.region = region + + self.username = kwargs['username'] + self.password = kwargs['password'] + self.project_id = kwargs['project_id'] + self.auth_url = kwargs['auth_url'] + + self.region_name = kwargs.get('region_name', region) + self.auth_token = kwargs.get('auth_token', None) + + self.service_types = _get_service_values(kwargs, 'service_type') + self.endpoints = _get_service_values(kwargs, 'endpoint') + self.api_versions = _get_service_values(kwargs, 'api_version') + + self.insecure = kwargs.get('insecure', False) + self.endpoint_type = kwargs.get('endpoint_type', 'publicURL') + self._image_cache = image_cache - self.flavor_cache = flavor_cache + self._flavor_cache = flavor_cache self._volume_cache = volume_cache + self.debug = debug self._nova_client = None self._glance_client = None self._keystone_client = None self._cinder_client = None + self._trove_client = None self.log = logging.getLogger('shade') self.log.setLevel(logging.INFO) self.log.addHandler(logging.StreamHandler()) + def get_service_type(self, service): + return self.service_types.get(service, service) + @property def nova_client(self): if self._nova_client is None: kwargs = dict( region_name=self.region_name, - service_type=self.nova_service_type, + service_type=self.get_service_type('compute'), insecure=self.insecure, ) # Try to use keystone directly first, for potential token reuse try: kwargs['auth_token'] = self.keystone_client.auth_token kwargs['bypass_url'] = self.get_endpoint( - self.nova_service_type) + self.get_service_type('compute')) except OpenStackCloudException: pass @@ -230,10 +160,10 @@ def keystone_client(self): keystone_logging.addHandler(logging.NullHandler()) try: - if self.token: + if self.auth_token: self._keystone_client = keystone_client.Client( endpoint=self.auth_url, - token=self.token) + token=self.auth_token) else: self._keystone_client = keystone_client.Client( username=self.username, @@ -247,16 +177,28 @@ def keystone_client(self): "Error authenticating to the keystone: %s " % e.message) return self._keystone_client + def _get_glance_api_version(self, endpoint): + if 'image' in self.api_versions: + return self.api_versions['image'] + # Yay. We get to guess ... + # Get rid of trailing '/' if present + if endpoint.endswith('/'): + endpoint = endpoint[:-1] + url_bits = endpoint.split('/') + if url_bits[-1].startswith('v'): + return url_bits[-1][1] + return '1' # Who knows? Let's just try 1 ... + @property def glance_client(self): if self._glance_client is None: token = self.keystone_client.auth_token - endpoint = self.get_endpoint(service_type='image') + endpoint = self.get_endpoint( + service_type=self.get_service_type('image')) + glance_api_version = self._get_glance_api_version(endpoint) try: - # Seriously. I'm not kidding. The first argument is '1'. And - # no, it's not a version that's discoverable from keystone self._glance_client = glanceclient.Client( - '1', endpoint, token=token) + glance_api_version, endpoint, token=token) except Exception as e: self.log.debug("glance unknown issue", exc_info=True) raise OpenStackCloudException( @@ -297,6 +239,39 @@ def cinder_client(self): return self._cinder_client + @property + def trove_client(self): + if self._trove_client is None: + # Make the connection + self._trove_client = trove_client.Client( + '1.0', # TODO: discover this if possible + self.username, + self.password, + self.project_id, + self.auth_url, + region_name=self.region_name, + service_type=self.get_service_type('database'), + ) + + try: + self._trove_client.authenticate() + except trove_exceptions.Unauthorized, e: + self.log.debug("trove Unauthorized", exc_info=True) + raise OpenStackCloudException( + "Invalid OpenStack Trove credentials.: %s" % e.message) + except trove_exceptions.AuthorizationFailure, e: + self.log.debug("trove AuthorizationFailure", exc_info=True) + raise OpenStackCloudException( + "Unable to authorize user: %s" % e.message) + + if self._trove_client is None: + raise OpenStackCloudException( + "Failed to instantiate cinder client. This could mean that your" + " credentials are wrong.") + + return self._trove_client + + def get_name(self): return self.name @@ -304,13 +279,15 @@ def get_region(self): return self.region_name def get_flavor_name(self, flavor_id): - if not self.flavor_cache: - self.flavor_cache = dict( + if not self._flavor_cache: + self._flavor_cache = dict( [(flavor.id, flavor.name) for flavor in self.nova_client.flavors.list()]) - return self.flavor_cache.get(flavor_id, None) + return self._flavor_cache.get(flavor_id, None) def get_endpoint(self, service_type): + if service_type in self.endpoints: + return self.endpoints[service_type] try: endpoint = self.keystone_client.service_catalog.url_for( service_type=service_type, endpoint_type=self.endpoint_type) From 1277d4cfc58a0c4e7338e29ada0df9631d62da21 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 21 Sep 2014 14:33:07 -0700 Subject: [PATCH 0012/3836] Update the README file for more completeness --- README.rst | 16 ++++++++++++++-- os_client_config/vendors.py | 3 ++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 6332f8391..33327e20b 100644 --- a/README.rst +++ b/README.rst @@ -30,11 +30,15 @@ for trove (because you're using Rackspace) set: Config Files ------------ -os-client-config will for a file called clouds.yaml in the following locations: +os-client-config will look for a file called clouds.yaml in the following +locations: + * Current Directory * ~/.config/openstack * /etc/openstack +The first file found wins. + The keys are all of the keys you'd expect from `OS_*` - except lower case and without the OS prefix. So, username is set with `username`. @@ -55,12 +59,14 @@ An example config file is probably helpful: password: XXXXXXXXX project_id: mordred@inaugust.com region_name: region-b.geo-1 + dns_service_type: hpext:dns monty: auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0 username: monty.taylor@hp.com password: XXXXXXXX project_id: monty.taylor@hp.com-default-tenant region_name: region-b.geo-1 + dns_service_type: hpext:dns infra: cloud: rackspace username: openstackci @@ -71,13 +77,19 @@ An example config file is probably helpful: You may note a few things. First, since auth_url settings are silly and embarrasingly ugly, known cloud vendors are included and may be referrenced by name. One of the benefits of that is that auth_url isn't the only thing -the vendor defaults contain. For instance, since Rackspace is broken and lists +the vendor defaults contain. For instance, since Rackspace lists `rax:database` as the service type for trove, os-client-config knows that so that you don't have to. Also, region_name can be a list of regions. When you call get_all_clouds, you'll get a cloud config object for each cloud/region combo. +As seen with `dns_service_type`, any setting that makes sense to be per-service, +like `service_type` or `endpoint` or `api_version` can be set by prefixing +the setting with the default service type. That might strike you funny when +setting `service_type` and it does me too - but that's just the world we live +in. + Usage ----- diff --git a/os_client_config/vendors.py b/os_client_config/vendors.py index 71cf15896..c78aaca54 100644 --- a/os_client_config/vendors.py +++ b/os_client_config/vendors.py @@ -16,10 +16,11 @@ hp=dict( auth_url='https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0', region_name='region-b.geo-1', - dns_service_type='hp:dns', + dns_service_type='hpext:dns', ), rackspace=dict( auth_url='https://identity.api.rackspacecloud.com/v2.0/', database_service_type='rax:database', + image_api_version='2', ) ) From e3bef24a4b7fbe8ab1c49b6af08aa175ecb41644 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 21 Sep 2014 17:49:01 -0700 Subject: [PATCH 0013/3836] Discover Trove API version --- shade/__init__.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index ee6adeaf7..fa43610af 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -239,12 +239,29 @@ def cinder_client(self): return self._cinder_client + def _get_trove_api_version(self, endpoint): + if 'database' in self.api_versions: + return self.api_versions['database'] + # Yay. We get to guess ... + # Get rid of trailing '/' if present + if endpoint.endswith('/'): + endpoint = endpoint[:-1] + url_bits = endpoint.split('/') + for bit in url_bits: + if bit.startswith('v'): + return bit[1:] + return '1.0' # Who knows? Let's just try 1.0 ... + @property def trove_client(self): if self._trove_client is None: - # Make the connection + endpoint = self.get_endpoint( + service_type=self.get_service_type('database')) + trove_api_version = self._get_trove_api_version(endpoint) + # Make the connection - can't use keystone session until there + # is one self._trove_client = trove_client.Client( - '1.0', # TODO: discover this if possible + trove_api_version, self.username, self.password, self.project_id, From 1113d0523d1f35af11ac17112a6ac7cd2a1a085a Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Sun, 21 Sep 2014 18:28:15 -0500 Subject: [PATCH 0014/3836] Handle null region Not all clouds define/require region_name to be set --- os_client_config/config.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 5cc8edd78..208b18a79 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -56,6 +56,7 @@ def __init__(self, config_files=None): defaults.add('insecure') defaults.add('endpoint_type') defaults.add('cacert') + defaults.add('auth_type') self.defaults = defaults @@ -75,7 +76,11 @@ def _load_config_file(self): return yaml.load(open(path, 'r')) def _get_regions(self, cloud): - return self.cloud_config['clouds'][cloud]['region_name'] + try: + return self.cloud_config['clouds'][cloud]['region_name'] + except KeyError: + # No region configured + return '' def _get_region(self, cloud): return self._get_regions(cloud).split(',')[0] From b8382c349687b09e15f92cde9d0099fea5252657 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Sun, 21 Sep 2014 20:44:44 -0500 Subject: [PATCH 0015/3836] Make env vars lowest priority When working with multiple tools the project CLIs only know about options and environment variables. When selecting a cloud config that includes a section from vendors.py environment vars overwrite that data if they are defined. The priority order should be: * command line args * cloud config selection * environment variables --- os_client_config/config.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 5cc8edd78..64eaff789 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -90,11 +90,13 @@ def _get_base_cloud_config(self, name): else: our_cloud = dict() + # Get the defaults (including env vars) first + cloud.update(self.defaults) + # yes, I know the next line looks silly if 'cloud' in our_cloud: cloud.update(vendors.CLOUD_DEFAULTS[our_cloud['cloud']]) - cloud.update(self.defaults) cloud.update(our_cloud) if 'cloud' in cloud: del cloud['cloud'] From 7bd5ff625f9c845e1199878caf15ed128a31bb52 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Sun, 21 Sep 2014 22:16:02 -0500 Subject: [PATCH 0016/3836] Handle missing vendor key Continue on if the configured vendor config is not present --- os_client_config/config.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 5cc8edd78..5b8c53aa0 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -92,7 +92,11 @@ def _get_base_cloud_config(self, name): # yes, I know the next line looks silly if 'cloud' in our_cloud: - cloud.update(vendors.CLOUD_DEFAULTS[our_cloud['cloud']]) + try: + cloud.update(vendors.CLOUD_DEFAULTS[our_cloud['cloud']]) + except KeyError: + # Can't find the requested vendor config, go about business + pass cloud.update(self.defaults) cloud.update(our_cloud) From cb9e5059f6f9557fc9d62e12f4237719fb172e64 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 22 Sep 2014 09:01:40 -0700 Subject: [PATCH 0017/3836] Prep for move to stackforge --- .gitreview | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitreview b/.gitreview index 69bff3ae3..234652006 100644 --- a/.gitreview +++ b/.gitreview @@ -1,4 +1,4 @@ [gerrit] host=review.openstack.org port=29418 -project=openstack/os-client-config.git \ No newline at end of file +project=stackforge/os-client-config.git From 2c2a2953771d6599cbb3b65500c1f55eb44cafe9 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 22 Sep 2014 19:54:57 -0500 Subject: [PATCH 0018/3836] Add clouds-public.yaml Put vendor config outside of the code in clouds-public.yaml. Fall back to vendors.py if clouds-public.yaml not found. The search follows the same rules as clouds.yaml, the file is the same format except the top-level key is 'public-clouds'. Typically only auth_url and region_name are populated. --- os_client_config/config.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 56992df04..6612bffdd 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -30,6 +30,9 @@ os.path.join(d, 'clouds.yaml') for d in CONFIG_SEARCH_PATH] BOOL_KEYS = ('insecure', 'cache') REQUIRED_VALUES = ('auth_url', 'username', 'password', 'project_id') +VENDOR_SEARCH_PATH = [os.getcwd(), CONFIG_HOME, '/etc/openstack'] +VENDOR_FILES = [ + os.path.join(d, 'clouds-public.yaml') for d in VENDOR_SEARCH_PATH] def get_boolean(value): @@ -40,8 +43,9 @@ def get_boolean(value): class OpenStackConfig(object): - def __init__(self, config_files=None): + def __init__(self, config_files=None, vendor_files=None): self._config_files = config_files or CONFIG_FILES + self._vendor_files = vendor_files or VENDOR_FILES defaults = defaults_dict.DefaultsDict() defaults.add('username') @@ -75,6 +79,11 @@ def _load_config_file(self): if os.path.exists(path): return yaml.load(open(path, 'r')) + def _load_vendor_file(self): + for path in self._vendor_files: + if os.path.exists(path): + return yaml.load(open(path, 'r')) + def _get_regions(self, cloud): try: return self.cloud_config['clouds'][cloud]['region_name'] @@ -100,11 +109,15 @@ def _get_base_cloud_config(self, name): # yes, I know the next line looks silly if 'cloud' in our_cloud: - try: - cloud.update(vendors.CLOUD_DEFAULTS[our_cloud['cloud']]) - except KeyError: - # Can't find the requested vendor config, go about business - pass + vendor_file = self._load_vendor_file() + if our_cloud['cloud'] in vendor_file['public-clouds']: + cloud.update(vendor_file['public-clouds'][our_cloud['cloud']]) + else: + try: + cloud.update(vendors.CLOUD_DEFAULTS[our_cloud['cloud']]) + except KeyError: + # Can't find the requested vendor config, go about business + pass cloud.update(our_cloud) if 'cloud' in cloud: From 21e3ecaf5b43e949fb92c2a9d8b72fe90cf4d47c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 28 Sep 2014 12:09:33 -0700 Subject: [PATCH 0019/3836] Updates to use keystone session --- requirements.txt | 2 +- shade/__init__.py | 28 ++++++++++++++++++++-------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/requirements.txt b/requirements.txt index 7bb0f8b74..0a1905bee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ pbr>=0.5.21,<1.0 os-client-config python-novaclient -python-keystoneclient +python-keystoneclient>=0.11.0 python-glanceclient python-cinderclient python-neutronclient diff --git a/shade/__init__.py b/shade/__init__.py index fa43610af..b69d59b1c 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -18,7 +18,7 @@ from cinderclient.v1 import client as cinder_client from cinderclient import exceptions as cinder_exceptions import glanceclient -from keystoneclient.v2_0 import client as keystone_client +from keystoneclient import client as keystone_client from novaclient import exceptions as nova_exceptions from novaclient.v1_1 import client as nova_client import os_client_config @@ -26,7 +26,6 @@ from troveclient import exceptions as trove_exceptions import pbr.version - __version__ = pbr.version.VersionInfo('shade').version_string() @@ -48,9 +47,11 @@ class OpenStackCloudException(Exception): pass -def openstack_clouds(): +def openstack_clouds(config=None): + if not config: + config = os_client_config.OpenStackConfig() return [OpenStackCloud(f.name, f.region, **f.config) - for f in os_client_config.OpenStackConfig().get_all_clouds()] + for f in config.get_all_clouds()] def openstack_cloud(cloud='openstack', region=None): @@ -86,8 +87,14 @@ def __init__(self, name, region='', self.endpoints = _get_service_values(kwargs, 'endpoint') self.api_versions = _get_service_values(kwargs, 'api_version') + self.user_domain_name = kwargs.get('user_domain_name', None) + self.project_domain_name = kwargs.get('project_domain_name', None) + self.insecure = kwargs.get('insecure', False) self.endpoint_type = kwargs.get('endpoint_type', 'publicURL') + self.cert = kwargs.get('cert', None) + self.cacert = kwargs.get('cacert', None) + self.private = kwargs.get('private', False) self._image_cache = image_cache self._flavor_cache = flavor_cache @@ -133,12 +140,13 @@ def nova_client(self): **kwargs ) + self._nova_client.authenticate() try: self._nova_client.authenticate() except nova_exceptions.Unauthorized as e: self.log.debug("nova Unauthorized", exc_info=True) raise OpenStackCloudException( - "Invalid OpenStack Nova credentials.: %s" % e.message) + "Invalid OpenStack Nova credentials: %s" % e.message) except nova_exceptions.AuthorizationFailure as e: self.log.debug("nova AuthorizationFailure", exc_info=True) raise OpenStackCloudException( @@ -168,9 +176,12 @@ def keystone_client(self): self._keystone_client = keystone_client.Client( username=self.username, password=self.password, - tenant_name=self.project_id, + project_id=self.project_id, region_name=self.region_name, - auth_url=self.auth_url) + auth_url=self.auth_url, + user_domain_name=self.user_domain_name, + project_domain_name=self.project_domain_name) + self._keystone_client.authenticate() except Exception as e: self.log.debug("keystone unknown issue", exc_info=True) raise OpenStackCloudException( @@ -198,7 +209,8 @@ def glance_client(self): glance_api_version = self._get_glance_api_version(endpoint) try: self._glance_client = glanceclient.Client( - glance_api_version, endpoint, token=token) + glance_api_version, endpoint, token=token, + session=self.keystone_client.session) except Exception as e: self.log.debug("glance unknown issue", exc_info=True) raise OpenStackCloudException( From 67f1fbd22ed5d0558a92c49c5114a292d1f0cc01 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 10 Oct 2014 15:24:15 -0700 Subject: [PATCH 0020/3836] Remove unused class method get_services Change-Id: Id133bc3c39b97a4489e75c3d38df601f999e8f3a --- os_client_config/config.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 6612bffdd..8cdb06c73 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -70,10 +70,6 @@ def __init__(self, config_files=None, vendor_files=None): self.cloud_config = dict( clouds=dict(openstack=dict(self.defaults))) - @classmethod - def get_services(klass): - return SERVICES - def _load_config_file(self): for path in self._config_files: if os.path.exists(path): From 215425f421cb4b831f1a294d73a25a14f301c1f4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 28 Sep 2014 11:49:13 -0700 Subject: [PATCH 0021/3836] Handle no vendor clouds config files Change-Id: If0ab1db3df8ba3a2880473f2287ae3f85c84d9d5 --- os_client_config/config.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 8cdb06c73..ade970891 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -105,12 +105,13 @@ def _get_base_cloud_config(self, name): # yes, I know the next line looks silly if 'cloud' in our_cloud: + cloud_name = our_cloud['cloud'] vendor_file = self._load_vendor_file() - if our_cloud['cloud'] in vendor_file['public-clouds']: - cloud.update(vendor_file['public-clouds'][our_cloud['cloud']]) + if vendor_file and cloud_name in vendor_file['public-clouds']: + cloud.update(vendor_file['public-clouds'][cloud_name]) else: try: - cloud.update(vendors.CLOUD_DEFAULTS[our_cloud['cloud']]) + cloud.update(vendors.CLOUD_DEFAULTS[cloud_name]) except KeyError: # Can't find the requested vendor config, go about business pass From b1bb75a69b491b26048fd18c40a7ac87043ea93c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 28 Sep 2014 11:18:31 -0700 Subject: [PATCH 0022/3836] Add cache control settings Things need to do local caching, which means they need to control some settings about that. Add simple cache settings support. Change-Id: I7b56cc25ebe7a803816d95b79d0329f8e42025ba --- README.rst | 24 ++++++++++++++++++++++++ os_client_config/config.py | 17 +++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/README.rst b/README.rst index 33327e20b..30819f398 100644 --- a/README.rst +++ b/README.rst @@ -45,11 +45,13 @@ and without the OS prefix. So, username is set with `username`. Service specific settings, like the nova service type, are set with the default service type as a prefix. For instance, to set a special service_type for trove (because you're using Rackspace) set: + :: database_service_type: 'rax:database' An example config file is probably helpful: + :: clouds: @@ -90,6 +92,28 @@ the setting with the default service type. That might strike you funny when setting `service_type` and it does me too - but that's just the world we live in. +Cache Settings +-------------- + +Accessing a cloud is often expensive, so it's quite common to want to do some +client-side caching of those operations. To facilitate that, os-client-config +understands a simple set of cache control settings. + +:: + + cache: + path: ~/.cache/openstack + max_age: 300 + clouds: + mordred: + cloud: hp + username: mordred@inaugust.com + password: XXXXXXXXX + project_id: mordred@inaugust.com + region_name: region-b.geo-1 + dns_service_type: hpext:dns + + Usage ----- diff --git a/os_client_config/config.py b/os_client_config/config.py index ade970891..f92c7909b 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -28,6 +28,9 @@ CONFIG_SEARCH_PATH = [os.getcwd(), CONFIG_HOME, '/etc/openstack'] CONFIG_FILES = [ os.path.join(d, 'clouds.yaml') for d in CONFIG_SEARCH_PATH] +CACHE_PATH = os.path.join(os.path.expanduser( + os.environ.get('XDG_CACHE_PATH', os.path.join('~', '.cache'))), + 'openstack') BOOL_KEYS = ('insecure', 'cache') REQUIRED_VALUES = ('auth_url', 'username', 'password', 'project_id') VENDOR_SEARCH_PATH = [os.getcwd(), CONFIG_HOME, '/etc/openstack'] @@ -70,6 +73,14 @@ def __init__(self, config_files=None, vendor_files=None): self.cloud_config = dict( clouds=dict(openstack=dict(self.defaults))) + self._cache_max_age = 300 + self._cache_path = CACHE_PATH + if 'cache' in self.cloud_config: + self._cache_max_age = self.cloud_config['cache'].get( + 'max_age', self._cache_max_age) + self._cache_path = os.path.expanduser( + self.cloud_config['cache'].get('path', self._cache_path)) + def _load_config_file(self): for path in self._config_files: if os.path.exists(path): @@ -80,6 +91,12 @@ def _load_vendor_file(self): if os.path.exists(path): return yaml.load(open(path, 'r')) + def get_cache_max_age(self): + return self._cache_max_age + + def get_cache_path(self): + return self._cache_path + def _get_regions(self, cloud): try: return self.cloud_config['clouds'][cloud]['region_name'] From aa7964512b34d5d6c48d5da7067a9bf14a8cb24c Mon Sep 17 00:00:00 2001 From: Devananda van der Veen Date: Thu, 9 Oct 2014 15:45:44 -0700 Subject: [PATCH 0023/3836] add Ironic client --- requirements.txt | 1 + shade/__init__.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/requirements.txt b/requirements.txt index 0a1905bee..610303351 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ python-glanceclient python-cinderclient python-neutronclient python-troveclient +python-ironicclient diff --git a/shade/__init__.py b/shade/__init__.py index b69d59b1c..3ab9bcc9e 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -18,6 +18,8 @@ from cinderclient.v1 import client as cinder_client from cinderclient import exceptions as cinder_exceptions import glanceclient +from ironicclient import client as ironic_client +from ironicclient import exceptions as ironic_exceptions from keystoneclient import client as keystone_client from novaclient import exceptions as nova_exceptions from novaclient.v1_1 import client as nova_client @@ -104,6 +106,7 @@ def __init__(self, name, region='', self._nova_client = None self._glance_client = None + self._ironic_client = None self._keystone_client = None self._cinder_client = None self._trove_client = None @@ -300,6 +303,16 @@ def trove_client(self): return self._trove_client + @property + def ironic_client(self): + if self._ironic_client is None: + token = self.keystone_client.auth_token + endpoint = self.get_endpoint(service_type='baremetal') + try: + self._ironic_client = ironic_client.Client('1', endpoint, token=token) + except Exception as e: + raise OpenStackCloudException("Error in connecting to ironic: %s" % e.message) + return self._ironic_client def get_name(self): return self.name @@ -400,3 +413,18 @@ def get_server_id(self, server_name): if server.name == server_name: return server.id return None + + def list_ironic_nodes(self): + return self.ironic_client.node.list() + + def get_ironic_node(self, node_name): + return self.ironic_client.node.get(node_name) + + def list_ironic_ports(self): + return self.ironic_client.port.list() + + def get_ironic_port(self, port_name): + return self.ironic_client.port.get(port_name) + + def list_ironic_ports_for_node(self, node_name): + return self.ironic_client.node.list_ports(node_name) From 0aa553c4735963f06e69ada0ba8568f6fff4701d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 10 Oct 2014 15:18:55 -0700 Subject: [PATCH 0024/3836] Handle the project/tenant nonesense more cleanly devstack clouds are more strict that public ones, so it's more important to get project_name vs. project_id correct. Solve it with brute force. Change-Id: I957b19c23266d379834361ab6a5b3b2dc5d15d3d --- os_client_config/config.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index f92c7909b..315d3e0ef 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -32,7 +32,7 @@ os.environ.get('XDG_CACHE_PATH', os.path.join('~', '.cache'))), 'openstack') BOOL_KEYS = ('insecure', 'cache') -REQUIRED_VALUES = ('auth_url', 'username', 'password', 'project_id') +REQUIRED_VALUES = ('auth_url', 'username', 'password') VENDOR_SEARCH_PATH = [os.getcwd(), CONFIG_HOME, '/etc/openstack'] VENDOR_FILES = [ os.path.join(d, 'clouds-public.yaml') for d in VENDOR_SEARCH_PATH] @@ -54,7 +54,8 @@ def __init__(self, config_files=None, vendor_files=None): defaults.add('username') defaults.add('user_domain_name') defaults.add('password') - defaults.add('project_id', defaults['username'], also='tenant_name') + defaults.add('project_name', defaults['username'], also='tenant_name') + defaults.add('project_id', also='tenant_id') defaults.add('project_domain_name') defaults.add('auth_url') defaults.add('region_name') @@ -136,6 +137,17 @@ def _get_base_cloud_config(self, name): cloud.update(our_cloud) if 'cloud' in cloud: del cloud['cloud'] + + return self._fix_project_madness(cloud) + + def _fix_project_madness(self, cloud): + project_name = None + # Do the list backwards so that project_name is the ultimate winner + for key in ('tenant_id', 'project_id', 'tenant_name', 'project_name'): + if key in cloud: + project_name = cloud[key] + del cloud[key] + cloud['project_name'] = project_name return cloud def get_all_clouds(self): @@ -166,6 +178,12 @@ def get_one_cloud(self, name='openstack', region=None): ' config files {files}' ' or environment variables.'.format( name=name, files=','.join(self._config_files))) + if 'project_name' not in config and 'project_id' not in config: + raise exceptions.OpenStackConfigException( + 'Neither project_name or project_id information found' + ' for cloud {name} in config files {files}' + ' or environment variables.'.format( + name=name, files=','.join(self._config_files))) # If any of the defaults reference other values, we need to expand for (key, value) in config.items(): From 91afaeb4aeb811b28d27b9eaf3887235a841f680 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 10 Oct 2014 16:09:16 -0700 Subject: [PATCH 0025/3836] Handle lack of username for project_name defaults It's possible that there will not be a value in username, so we can't use it as a blind default. Change-Id: Iae93b9ec0e691c7b2174a0138c5455e36ad77ad7 --- os_client_config/config.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 315d3e0ef..781dc101b 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -54,7 +54,9 @@ def __init__(self, config_files=None, vendor_files=None): defaults.add('username') defaults.add('user_domain_name') defaults.add('password') - defaults.add('project_name', defaults['username'], also='tenant_name') + defaults.add( + 'project_name', defaults.get('username', None), + also='tenant_name') defaults.add('project_id', also='tenant_id') defaults.add('project_domain_name') defaults.add('auth_url') From 293745e7d75b28e8b803291672bf9b9dce938fde Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 10 Oct 2014 16:39:47 -0700 Subject: [PATCH 0026/3836] Consume project_name from os-client-config It's always project_name now. --- shade/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index b69d59b1c..256a44571 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -77,7 +77,7 @@ def __init__(self, name, region='', self.username = kwargs['username'] self.password = kwargs['password'] - self.project_id = kwargs['project_id'] + self.project_name = kwargs['project_name'] self.auth_url = kwargs['auth_url'] self.region_name = kwargs.get('region_name', region) @@ -135,7 +135,7 @@ def nova_client(self): self._nova_client = nova_client.Client( self.username, self.password, - self.project_id, + self.project_name, self.auth_url, **kwargs ) @@ -176,7 +176,7 @@ def keystone_client(self): self._keystone_client = keystone_client.Client( username=self.username, password=self.password, - project_id=self.project_id, + project_name=self.project_name, region_name=self.region_name, auth_url=self.auth_url, user_domain_name=self.user_domain_name, @@ -228,7 +228,7 @@ def cinder_client(self): self._cinder_client = cinder_client.Client( self.username, self.password, - self.project_id, + self.project_name, self.auth_url, region_name=self.region_name, ) @@ -276,7 +276,7 @@ def trove_client(self): trove_api_version, self.username, self.password, - self.project_id, + self.project_name, self.auth_url, region_name=self.region_name, service_type=self.get_service_type('database'), From a23451831ee7601fee3aba519125f028bbc1c9d9 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 11 Oct 2014 10:33:38 -0700 Subject: [PATCH 0027/3836] Plumb through a small name change for args We want to be able to sanely and safely passthrough args lists from consumers. "name" is a very common thing to want to also be in a consuming args list, whereas "cloud" is clearly something we own. --- shade/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index a1a49c884..476897d33 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -56,9 +56,9 @@ def openstack_clouds(config=None): for f in config.get_all_clouds()] -def openstack_cloud(cloud='openstack', region=None): +def openstack_cloud(**kwargs): cloud_config = os_client_config.OpenStackConfig().get_one_cloud( - cloud, region) + **kwargs) return OpenStackCloud( cloud_config.name, cloud_config.region, **cloud_config.config) @@ -70,11 +70,11 @@ def _get_service_values(kwargs, service_key): class OpenStackCloud(object): - def __init__(self, name, region='', + def __init__(self, cloud, region='', image_cache=None, flavor_cache=None, volume_cache=None, debug=False, **kwargs): - self.name = name + self.name = cloud self.region = region self.username = kwargs['username'] From b03d6e59b93cea29df30f286db02701e2e77effa Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 11 Oct 2014 09:41:48 -0700 Subject: [PATCH 0028/3836] Add support for command line argument processing Now takes the ability to pass in a dict of key/value pairs, probably from a command line processing thing like argparse, and to overlay them on the config that came from the files or env vars. Change-Id: I830699476e2340389979b34704c0dfbfe97a1e08 --- os_client_config/config.py | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 781dc101b..34de5134a 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -161,17 +161,43 @@ def get_all_clouds(self): clouds.append(self.get_one_cloud(cloud, region)) return clouds - def get_one_cloud(self, name='openstack', region=None): + def _fix_args(self, args): + '''Replace - with _ and strip os_ prefixes.''' + os_args = dict() + new_args = dict() + for (key, val) in args.iteritems(): + key = key.replace('-', '_') + if key.startswith('os'): + os_args[key[3:]] = val + else: + new_args[key] = val + new_args.update(os_args) + return new_args + + def get_one_cloud(self, **kwargs): + + args = self._fix_args(kwargs) - if not region: - region = self._get_region(name) + if 'cloud' in args: + name = args['cloud'] + del args['cloud'] + else: + name = 'openstack' + + if 'region_name' not in args: + args['region_name'] = self._get_region(name) config = self._get_base_cloud_config(name) - config['region_name'] = region + + # Can't just do update, because None values take over + for (key, val) in args.iteritems(): + if val is not None: + config[key] = val for key in BOOL_KEYS: if key in config: - config[key] = get_boolean(config[key]) + if type(config[key]) is not bool: + config[key] = get_boolean(config[key]) for key in REQUIRED_VALUES: if key not in config or not config[key]: @@ -193,7 +219,7 @@ def get_one_cloud(self, name='openstack', region=None): config[key] = value.format(**config) return cloud_config.CloudConfig( - name=name, region=region, config=config) + name=name, region=config['region_name'], config=config) if __name__ == '__main__': config = OpenStackConfig().get_all_clouds() From 470f6ea9358ac6cd6a05a96805d06c3cf8501d97 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Sat, 11 Oct 2014 14:55:30 -0500 Subject: [PATCH 0029/3836] Add support for argparse Namespace objects The Namespace objects returned by argparse contain all defined options even if they are unspecified and default to None or ''. Also it is not iterable. Change all that to add only the options presented to argparse to the cloud_config. Change-Id: Ia22fad60c81ab0b2878b404c0c8608d903ca964b --- os_client_config/config.py | 68 +++++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 19 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 34de5134a..753c80aab 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -161,8 +161,24 @@ def get_all_clouds(self): clouds.append(self.get_one_cloud(cloud, region)) return clouds - def _fix_args(self, args): - '''Replace - with _ and strip os_ prefixes.''' + def _fix_args(self, args, argparse=None): + """Massage the passed-in options + + Replace - with _ and strip os_ prefixes. + + Convert an argparse Namespace object to a dict, removing values + that are either None or ''. + """ + + if argparse: + # Convert the passed-in Namespace + o_dict = vars(argparse) + parsed_args = dict() + for k in o_dict: + if o_dict[k] is not None and o_dict[k] != '': + parsed_args[k] = o_dict[k] + args.update(parsed_args) + os_args = dict() new_args = dict() for (key, val) in args.iteritems(): @@ -174,13 +190,26 @@ def _fix_args(self, args): new_args.update(os_args) return new_args - def get_one_cloud(self, **kwargs): - - args = self._fix_args(kwargs) - - if 'cloud' in args: - name = args['cloud'] - del args['cloud'] + def get_one_cloud(self, cloud=None, validate=True, + argparse=None, **kwargs): + """Retrieve a single cloud configuration and merge additional options + + :param string cloud: + The name of the configuration to load from clouds.yaml + :param boolean validate: + Validate that required arguments are present and certain + argument combinations are valid + :param Namespace argparse: + An argparse Namespace object; allows direct passing in of + argparse options to be added to the cloud config. Values + of None and '' will be removed. + :param kwargs: Additional configuration options + """ + + args = self._fix_args(kwargs, argparse=argparse) + + if cloud: + name = cloud else: name = 'openstack' @@ -199,19 +228,20 @@ def get_one_cloud(self, **kwargs): if type(config[key]) is not bool: config[key] = get_boolean(config[key]) - for key in REQUIRED_VALUES: - if key not in config or not config[key]: + if validate: + for key in REQUIRED_VALUES: + if key not in config or not config[key]: + raise exceptions.OpenStackConfigException( + 'Unable to find full auth information for cloud' + ' {name} in config files {files}' + ' or environment variables.'.format( + name=name, files=','.join(self._config_files))) + if 'project_name' not in config and 'project_id' not in config: raise exceptions.OpenStackConfigException( - 'Unable to find full auth information for cloud {name} in' - ' config files {files}' + 'Neither project_name or project_id information found' + ' for cloud {name} in config files {files}' ' or environment variables.'.format( name=name, files=','.join(self._config_files))) - if 'project_name' not in config and 'project_id' not in config: - raise exceptions.OpenStackConfigException( - 'Neither project_name or project_id information found' - ' for cloud {name} in config files {files}' - ' or environment variables.'.format( - name=name, files=','.join(self._config_files))) # If any of the defaults reference other values, we need to expand for (key, value) in config.items(): From 7fabe4bc11fbb2fd4115ec17fd1d869105d5324b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 12 Oct 2014 12:59:43 -0700 Subject: [PATCH 0030/3836] Add in server metadata routines --- shade/__init__.py | 21 +++----- shade/meta.py | 130 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 14 deletions(-) create mode 100644 shade/meta.py diff --git a/shade/__init__.py b/shade/__init__.py index 476897d33..40d55307a 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -28,21 +28,9 @@ from troveclient import exceptions as trove_exceptions import pbr.version -__version__ = pbr.version.VersionInfo('shade').version_string() - - -def find_nova_addresses(addresses, ext_tag, key_name=None): +from shade import meta - ret = [] - for (k, v) in addresses.iteritems(): - if key_name and k == key_name: - ret.extend([addrs['addr'] for addrs in v]) - else: - for interface_spec in v: - if ('OS-EXT-IPS:type' in interface_spec - and interface_spec['OS-EXT-IPS:type'] == ext_tag): - ret.append(interface_spec['addr']) - return ret +__version__ = pbr.version.VersionInfo('shade').version_string() class OpenStackCloudException(Exception): @@ -414,6 +402,11 @@ def get_server_id(self, server_name): return server.id return None + def get_server_meta(self, server): + server_vars = meta.get_hostvars_from_server(self, server) + groups = meta.get_groups_from_server(self, server, server_vars) + return dict(server_vars=server_vars, groups=groups) + def list_ironic_nodes(self): return self.ironic_client.node.list() diff --git a/shade/meta.py b/shade/meta.py new file mode 100644 index 000000000..8a243f866 --- /dev/null +++ b/shade/meta.py @@ -0,0 +1,130 @@ +# Copyright (c) 2014 Monty Taylor +# +# 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 re +from types import NoneType + +NON_CALLABLES = (basestring, bool, dict, int, list, NoneType) + + +def find_nova_addresses(addresses, ext_tag, key_name=None): + + ret = [] + for (k, v) in addresses.iteritems(): + if key_name and k == key_name: + ret.extend([addrs['addr'] for addrs in v]) + else: + for interface_spec in v: + if ('OS-EXT-IPS:type' in interface_spec + and interface_spec['OS-EXT-IPS:type'] == ext_tag): + ret.append(interface_spec['addr']) + return ret + + +def get_groups_from_server(cloud, server, server_vars): + groups = [] + + region = cloud.region + cloud_name = cloud.name + + # Create a group for the cloud + groups.append(cloud_name) + + # Create a group on region + groups.append(region) + + # And one by cloud_region + groups.append("%s_%s" % (cloud_name, region)) + + # Check if group metadata key in servers' metadata + group = server.metadata.get('group') + if group: + groups.append(group) + + for extra_group in server.metadata.get('groups', '').split(','): + if extra_group: + groups.append(extra_group) + + groups.append('instance-%s' % server.id) + + flavor_id = server.flavor['id'] + groups.append('flavor-%s' % flavor_id) + flavor_name = cloud.get_flavor_name(flavor_id) + if flavor_name: + groups.append('flavor-%s' % flavor_name) + + image_id = server.image['id'] + groups.append('image-%s' % image_id) + image_name = cloud.get_image_name(image_id) + if image_name: + groups.append('image-%s' % image_name) + + for key, value in server.metadata.iteritems(): + groups.append('meta_%s_%s' % (key, value)) + + az = server_vars.get('az', None) + if az: + # Make groups for az, region_az and cloud_region_az + groups.append(az) + groups.append('%s_%s' % (region, az)) + groups.append('%s_%s_%s' % (cloud.name, region, az)) + return groups + + +def get_hostvars_from_server(cloud, server): + server_vars = dict() + # Fist, add an IP address + if (cloud.private): + interface_ips = find_nova_addresses( + getattr(server, 'addresses'), 'fixed', 'private') + else: + interface_ips = find_nova_addresses( + getattr(server, 'addresses'), 'floating', 'public') + # TODO: I want this to be richer + server_vars['interface_ip'] = interface_ips[0] + + server_vars.update(to_dict(server)) + + server_vars['nova_region'] = cloud.region + server_vars['openstack_cloud'] = cloud.name + + server_vars['cinder_volumes'] = [ + to_dict(f, slug=False) for f in cloud.get_volumes(server)] + + az = server_vars.get('nova_os-ext-az_availability_zone', None) + if az: + server_vars['nova_az'] = az + + return server_vars + + +def to_dict(obj, slug=True, prefix='nova'): + instance = {} + for key in dir(obj): + value = getattr(obj, key) + if (isinstance(value, NON_CALLABLES) and not key.startswith('_')): + if slug: + key = slugify(prefix, key) + instance[key] = value + + return instance + + +# TODO: this is something both various modules and plugins could use +def slugify(pre='', value=''): + sep = '' + if pre is not None and len(pre): + sep = '_' + return '%s%s%s' % ( + pre, sep, re.sub('[^\w-]', '_', value).lower().lstrip('_')) From a046b708d15e61f99ee840db1a2d2f960e0e9886 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 12 Oct 2014 13:07:35 -0700 Subject: [PATCH 0031/3836] Fixed up a bunch of flake8 warnings --- shade/__init__.py | 36 +++++++++++++++++++----------------- shade/meta.py | 7 +++---- tox.ini | 2 ++ 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 40d55307a..155599aa3 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -13,10 +13,9 @@ # limitations under the License. import logging -import os -from cinderclient.v1 import client as cinder_client from cinderclient import exceptions as cinder_exceptions +from cinderclient.v1 import client as cinder_client import glanceclient from ironicclient import client as ironic_client from ironicclient import exceptions as ironic_exceptions @@ -24,9 +23,10 @@ from novaclient import exceptions as nova_exceptions from novaclient.v1_1 import client as nova_client import os_client_config +import pbr.version import troveclient.client as trove_client from troveclient import exceptions as trove_exceptions -import pbr.version + from shade import meta @@ -52,8 +52,8 @@ def openstack_cloud(**kwargs): def _get_service_values(kwargs, service_key): - return { k[:-(len(service_key) + 1)] : kwargs[k] - for k in kwargs.keys() if k.endswith(service_key) } + return {k[:-(len(service_key) + 1)]: kwargs[k] + for k in kwargs.keys() if k.endswith(service_key)} class OpenStackCloud(object): @@ -226,19 +226,19 @@ def cinder_client(self): try: self._cinder_client.authenticate() - except cinder_exceptions.Unauthorized, e: + except cinder_exceptions.Unauthorized as e: self.log.debug("cinder Unauthorized", exc_info=True) raise OpenStackCloudException( "Invalid OpenStack Cinder credentials.: %s" % e.message) - except cinder_exceptions.AuthorizationFailure, e: + except cinder_exceptions.AuthorizationFailure as e: self.log.debug("cinder AuthorizationFailure", exc_info=True) raise OpenStackCloudException( "Unable to authorize user: %s" % e.message) if self._cinder_client is None: raise OpenStackCloudException( - "Failed to instantiate cinder client. This could mean that your" - " credentials are wrong.") + "Failed to instantiate cinder client." + " This could mean that your credentials are wrong.") return self._cinder_client @@ -271,35 +271,37 @@ def trove_client(self): self.auth_url, region_name=self.region_name, service_type=self.get_service_type('database'), - ) + ) try: self._trove_client.authenticate() - except trove_exceptions.Unauthorized, e: + except trove_exceptions.Unauthorized as e: self.log.debug("trove Unauthorized", exc_info=True) raise OpenStackCloudException( "Invalid OpenStack Trove credentials.: %s" % e.message) - except trove_exceptions.AuthorizationFailure, e: + except trove_exceptions.AuthorizationFailure as e: self.log.debug("trove AuthorizationFailure", exc_info=True) raise OpenStackCloudException( "Unable to authorize user: %s" % e.message) if self._trove_client is None: raise OpenStackCloudException( - "Failed to instantiate cinder client. This could mean that your" - " credentials are wrong.") + "Failed to instantiate Trove client." + " This could mean that your credentials are wrong.") return self._trove_client - + @property def ironic_client(self): if self._ironic_client is None: token = self.keystone_client.auth_token endpoint = self.get_endpoint(service_type='baremetal') try: - self._ironic_client = ironic_client.Client('1', endpoint, token=token) + self._ironic_client = ironic_client.Client( + '1', endpoint, token=token) except Exception as e: - raise OpenStackCloudException("Error in connecting to ironic: %s" % e.message) + raise OpenStackCloudException( + "Error in connecting to ironic: %s" % e.message) return self._ironic_client def get_name(self): diff --git a/shade/meta.py b/shade/meta.py index 8a243f866..18bc5baa1 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -13,9 +13,9 @@ # limitations under the License. import re -from types import NoneType +import types -NON_CALLABLES = (basestring, bool, dict, int, list, NoneType) +NON_CALLABLES = (basestring, bool, dict, int, list, types.NoneType) def find_nova_addresses(addresses, ext_tag, key_name=None): @@ -91,7 +91,7 @@ def get_hostvars_from_server(cloud, server): else: interface_ips = find_nova_addresses( getattr(server, 'addresses'), 'floating', 'public') - # TODO: I want this to be richer + # TODO(mordred): I want this to be richer, "first" is not best server_vars['interface_ip'] = interface_ips[0] server_vars.update(to_dict(server)) @@ -121,7 +121,6 @@ def to_dict(obj, slug=True, prefix='nova'): return instance -# TODO: this is something both various modules and plugins could use def slugify(pre='', value=''): sep = '' if pre is not None and len(pre): diff --git a/tox.ini b/tox.ini index a6a2f51eb..4649796f5 100644 --- a/tox.ini +++ b/tox.ini @@ -25,6 +25,8 @@ commands = {posargs} commands = python setup.py testr --coverage --testr-args='{posargs}' [flake8] +# We're not inside of OpenStack, so H305 makes no sense +ignore = H305 show-source = True builtins = _ exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build From ddb1118af2145f386cf769944bc7738c9e6c88ab Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 12 Oct 2014 15:13:19 -0700 Subject: [PATCH 0032/3836] Add delete and get server name --- shade/__init__.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 155599aa3..1cd541b79 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -13,6 +13,7 @@ # limitations under the License. import logging +import time from cinderclient import exceptions as cinder_exceptions from cinderclient.v1 import client as cinder_client @@ -398,10 +399,16 @@ def get_volume_id(self, volume_name): return v.id return None - def get_server_id(self, server_name): + def get_server_by_name(self, server_name): for server in self.nova_client.servers.list(): if server.name == server_name: - return server.id + return server + return None + + def get_server_id(self, server_name): + server = get_server_by_name(server_name) + if server: + return server.id return None def get_server_meta(self, server): @@ -409,6 +416,22 @@ def get_server_meta(self, server): groups = meta.get_groups_from_server(self, server, server_vars) return dict(server_vars=server_vars, groups=groups) + def delete_server(self, name, wait=False, timeout=180): + server_list = self.nova_client.servers.list(True, {'name': name}) + if server_list: + server = [x for x in server_list if x.name == module.params['name']] + self.nova_client.servers.delete(server.pop()) + if not wait: + return + expire = time.time() + timeout + while time.time() < expire: + server = nova.servers.list(True, {'name': name}) + if not server: + return + time.sleep(5) + raise OpenStackCloudException( + "Timed out waiting for server to get deleted.") + def list_ironic_nodes(self): return self.ironic_client.node.list() From 1faac01ba5acc7e726919f036acad715dd8882a7 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 13 Oct 2014 13:02:25 -0700 Subject: [PATCH 0033/3836] Make all of the compute logic work --- shade/__init__.py | 173 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 161 insertions(+), 12 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 1cd541b79..6b1b0ac55 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -13,6 +13,7 @@ # limitations under the License. import logging +import operator import time from cinderclient import exceptions as cinder_exceptions @@ -311,12 +312,30 @@ def get_name(self): def get_region(self): return self.region_name - def get_flavor_name(self, flavor_id): + @property + def flavor_cache(self): if not self._flavor_cache: - self._flavor_cache = dict( - [(flavor.id, flavor.name) - for flavor in self.nova_client.flavors.list()]) - return self._flavor_cache.get(flavor_id, None) + self._flavor_cache = { + flavor.id: flavor + for flavor in self.nova_client.flavors.list()} + return self._flavor_cache + + def get_flavor_name(self, flavor_id): + flavor = self.flavor_cache.get(flavor_id, None) + if flavor: + return flavor.name + return None + + def get_flavor_by_ram(self, ram, include=None): + for flavor in sorted( + self.flavor_cache.values(), + key=operator.attrgetter('ram')): + if (flavor.ram >= ram and + (not include or include in flavor.name)): + return flavor + raise OpenStackCloudException( + "Cloud not find a flavor with {ram} and '{include}'".format( + ram=ram, include=include)) def get_endpoint(self, service_type): if service_type in self.endpoints: @@ -349,13 +368,13 @@ def _get_images_from_cloud(self): # This can fail both because we don't have glanceclient installed # and because the cloud may not expose the glance API publically for image in self.glance_client.images.list(): - images[image.id] = image.name + images[image.id] = image except (OpenStackCloudException, glanceclient.exc.HTTPInternalServerError): # We didn't have glance, let's try nova # If this doesn't work - we just let the exception propagate for image in self.nova_client.images.list(): - images[image.id] = image.name + images[image.id] = image return images def list_images(self): @@ -366,14 +385,22 @@ def list_images(self): def get_image_name(self, image_id): if image_id not in self.list_images(): self._image_cache[image_id] = None - return self._image_cache[image_id] + return self._image_cache[image_id].name - def get_image_id(self, image_name): - for (image_id, name) in self.list_images().items(): - if name == image_name: - return image_id + def get_image_id(self, image_name, exclude=None): + image = self.get_image_by_name(image_name, exclude) + if image: + return image.id return None + def get_image_by_name(self, name, exclude=None): + for (image_id, image) in self.list_images().items(): + if (name in image.name and ( + not exclude or exclude not in image.name)): + return image + raise OpenStackCloudException( + "Error finding image id from name(%s)" % name) + def _get_volumes_from_cloud(self): try: return self.cinder_client.volumes.list() @@ -416,6 +443,128 @@ def get_server_meta(self, server): groups = meta.get_groups_from_server(self, server, server_vars) return dict(server_vars=server_vars, groups=groups) + def add_ip_from_pool(self, server, pools): + + # instantiate FloatingIPManager object + floating_ip_obj = floating_ips.FloatingIPManager(self.nova_client) + + # empty dict and list + usable_floating_ips = {} + + # get the list of all floating IPs. Mileage may + # vary according to Nova Compute configuration + # per cloud provider + all_floating_ips = floating_ip_obj.list() + + # iterate through all pools of IP address. Empty + # string means all and is the default value + for pool in pools: + # temporary list per pool + pool_ips = [] + # loop through all floating IPs + for f_ip in all_floating_ips: + # if not reserved and the correct pool, add + if f_ip.instance_id is None and (f_ip.pool == pool): + pool_ips.append(f_ip.ip) + # only need one + break + + # if the list is empty, add for this pool + if not pool_ips: + try: + new_ip = self.nova_client.floating_ips.create(pool) + except Exception as e: + raise OpenStackCloudException( + "Unable to create floating ip in pool %s" % pool) + pool_ips.append(new_ip.ip) + # Add to the main list + usable_floating_ips[pool] = pool_ips + + # finally, add ip(s) to instance for each pool + for pool in usable_floating_ips: + for ip in usable_floating_ips[pool]: + self.add_ip_list(server, [ip]) + # We only need to assign one ip - but there is an inherent + # race condition and some other cloud operation may have + # stolen an available floating ip + break + + def add_ip_list(self, server, ips): + # add ip(s) to instance + for ip in ips: + try: + server.add_floating_ip(ip) + except Exception as e: + raise OpenStackCloudException( + "Error attaching IP {ip} to instance {id}: {msg} ".format( + ip=ip, id=server.id, msg=e.message)) + + def add_auto_ip(self, server): + try: + new_ip = self.nova_client.floating_ips.create() + except Exception as e: + raise OpenStackCloudException( + "Unable to create floating ip: %s" % (e.message)) + try: + self.add_ip_list(server, [new_ip]) + except OpenStackCloudException: + # Clean up - we auto-created this ip, and it's not attached + # to the server, so the cloud will not know what to do with it + self.nova_client.floating_ips.delete(new_ip) + raise + + def add_ips_to_server(self, server, auto_ip=True, ips=None, ip_pool=None): + if ip_pool: + self.add_ip_from_pool(server, ip_pool) + elif ips: + self.add_ip_list(server, ips) + elif auto_ip: + self.add_auto_ip(server) + else: + return server + + # this may look redundant, but if there is now a + # floating IP, then it needs to be obtained from + # a recent server object if the above code path exec'd + try: + server = self.nova_client.servers.get(server.id) + except Exception as e: + raise OpenStackCloudException( + "Error in getting info from instance: %s " % e.message) + return server + + def create_server(self, bootargs, bootkwargs, + auto_ip=True, ips=None, ip_pool=None, + wait=False, timeout=180): + try: + server = self.nova_client.servers.create(*bootargs, **bootkwargs) + server = self.nova_client.servers.get(server.id) + except Exception as e: + raise OpenStackCloudException( + "Error in creating instance: %s %s %s" % e.message) + if server.status == 'ERROR': + raise OpenStackCloudException( + "Error in creating the server.") + if wait: + expire = time.time() + timeout + while time.time() < expire: + try: + server = self.nova_client.servers.get(server.id) + except Exception: + continue + + if server.status == 'ACTIVE': + return self.add_ips_to_server(server, auto_ip, ips, ip_pool) + + if server.status == 'ERROR': + raise OpenStackCloudException( + "Error in creating the server, please check logs") + time.sleep(2) + + raise OpenStackCloudException( + "Timeout waiting for the server to come up.") + return server + def delete_server(self, name, wait=False, timeout=180): server_list = self.nova_client.servers.list(True, {'name': name}) if server_list: From 0968f7694b139f28b00b99deafc2100e4e67aa66 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 13 Oct 2014 13:40:32 -0700 Subject: [PATCH 0034/3836] Don't access object members on a None --- shade/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index 6b1b0ac55..cbbf5f811 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -385,7 +385,9 @@ def list_images(self): def get_image_name(self, image_id): if image_id not in self.list_images(): self._image_cache[image_id] = None - return self._image_cache[image_id].name + if self._image_cache[image_id]: + return self._image_cache[image_id].name + return None def get_image_id(self, image_name, exclude=None): image = self.get_image_by_name(image_name, exclude) From 77fce7d0aa06e9cfe96f0ac91dd7c11b7b5d0887 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 13 Oct 2014 13:44:56 -0700 Subject: [PATCH 0035/3836] Stop prefixing values with slugify We're going to namespace this stuff properly so that we can have add_host inject things into the inventory properly. --- shade/meta.py | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/shade/meta.py b/shade/meta.py index 18bc5baa1..1fbf634c0 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -94,36 +94,25 @@ def get_hostvars_from_server(cloud, server): # TODO(mordred): I want this to be richer, "first" is not best server_vars['interface_ip'] = interface_ips[0] - server_vars.update(to_dict(server)) + server_vars.update(obj_to_dict(server)) - server_vars['nova_region'] = cloud.region - server_vars['openstack_cloud'] = cloud.name + server_vars['region'] = cloud.region + server_vars['cloud'] = cloud.name - server_vars['cinder_volumes'] = [ - to_dict(f, slug=False) for f in cloud.get_volumes(server)] + server_vars['volumes'] = [ + obj_to_dict(f) for f in cloud.get_volumes(server)] - az = server_vars.get('nova_os-ext-az_availability_zone', None) + az = server_vars.get('OS-EXT-AZ:availability_zone', None) if az: - server_vars['nova_az'] = az + server_vars['az'] = az return server_vars -def to_dict(obj, slug=True, prefix='nova'): +def obj_to_dict(obj): instance = {} for key in dir(obj): value = getattr(obj, key) if (isinstance(value, NON_CALLABLES) and not key.startswith('_')): - if slug: - key = slugify(prefix, key) instance[key] = value - return instance - - -def slugify(pre='', value=''): - sep = '' - if pre is not None and len(pre): - sep = '_' - return '%s%s%s' % ( - pre, sep, re.sub('[^\w-]', '_', value).lower().lstrip('_')) From f8a5ff706d2a0d02ef9e41dcb7a152007af83c99 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 13 Oct 2014 14:01:13 -0700 Subject: [PATCH 0036/3836] Process flavor and image names --- shade/meta.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/shade/meta.py b/shade/meta.py index 1fbf634c0..45d94869e 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -58,20 +58,12 @@ def get_groups_from_server(cloud, server, server_vars): groups.append('instance-%s' % server.id) - flavor_id = server.flavor['id'] - groups.append('flavor-%s' % flavor_id) - flavor_name = cloud.get_flavor_name(flavor_id) - if flavor_name: - groups.append('flavor-%s' % flavor_name) - - image_id = server.image['id'] - groups.append('image-%s' % image_id) - image_name = cloud.get_image_name(image_id) - if image_name: - groups.append('image-%s' % image_name) + for key in ('flavor', 'image'): + if 'name' in server_vars[key]: + groups.append('%s-%s' % (key, server_vars[key]['name'])) for key, value in server.metadata.iteritems(): - groups.append('meta_%s_%s' % (key, value)) + groups.append('meta-%s_%s' % (key, value)) az = server_vars.get('az', None) if az: @@ -99,6 +91,16 @@ def get_hostvars_from_server(cloud, server): server_vars['region'] = cloud.region server_vars['cloud'] = cloud.name + flavor_id = server.flavor['id'] + flavor_name = cloud.get_flavor_name(flavor_id) + if flavor_name: + server_vars['flavor']['name'] = flavor_name + + image_id = server.image['id'] + image_name = cloud.get_image_name(image_id) + if image_name: + server_vars['image']['name'] = image_name + server_vars['volumes'] = [ obj_to_dict(f) for f in cloud.get_volumes(server)] From 4b1cdaa318a244a2afbf1c267dbfadf9dc6624e3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 13 Oct 2014 16:06:54 -0700 Subject: [PATCH 0037/3836] Don't die if we didn't grab a floating ip --- shade/__init__.py | 6 ++++++ shade/meta.py | 6 ++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index cbbf5f811..86862088c 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -428,6 +428,12 @@ def get_volume_id(self, volume_name): return v.id return None + def get_server_by_id(self, server_id): + for server in self.nova_client.servers.list(): + if server.id == server_id: + return server + return None + def get_server_by_name(self, server_name): for server in self.nova_client.servers.list(): if server.name == server_name: diff --git a/shade/meta.py b/shade/meta.py index 45d94869e..f22d89d32 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -83,8 +83,10 @@ def get_hostvars_from_server(cloud, server): else: interface_ips = find_nova_addresses( getattr(server, 'addresses'), 'floating', 'public') - # TODO(mordred): I want this to be richer, "first" is not best - server_vars['interface_ip'] = interface_ips[0] + + if interface_ips: + # TODO(mordred): I want this to be richer, "first" is not best + server_vars['interface_ip'] = interface_ips[0] server_vars.update(obj_to_dict(server)) From caa65d79e9612587e00ebe2bfedaede1e17daabf Mon Sep 17 00:00:00 2001 From: Devananda van der Veen Date: Tue, 14 Oct 2014 02:07:34 -0700 Subject: [PATCH 0038/3836] fix typo in create_server --- shade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index 86862088c..ad3e67b6b 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -549,7 +549,7 @@ def create_server(self, bootargs, bootkwargs, server = self.nova_client.servers.get(server.id) except Exception as e: raise OpenStackCloudException( - "Error in creating instance: %s %s %s" % e.message) + "Error in creating instance: %s" % e.message) if server.status == 'ERROR': raise OpenStackCloudException( "Error in creating the server.") From 54a460673fa1b0135836290c508fb2cc89871cdf Mon Sep 17 00:00:00 2001 From: Devananda van der Veen Date: Tue, 14 Oct 2014 02:08:07 -0700 Subject: [PATCH 0039/3836] Refactor ironic commands into OperatorCloud class --- shade/__init__.py | 71 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index ad3e67b6b..d9d33c8af 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -53,6 +53,12 @@ def openstack_cloud(**kwargs): cloud_config.name, cloud_config.region, **cloud_config.config) +def operator_cloud(**kwargs): + cloud_config = os_client_config.OpenStackConfig().get_one_cloud(**kwargs) + return OperatorCloud( + cloud_config.name, cloud_config.region, **cloud_config.config) + + def _get_service_values(kwargs, service_key): return {k[:-(len(service_key) + 1)]: kwargs[k] for k in kwargs.keys() if k.endswith(service_key)} @@ -293,19 +299,6 @@ def trove_client(self): return self._trove_client - @property - def ironic_client(self): - if self._ironic_client is None: - token = self.keystone_client.auth_token - endpoint = self.get_endpoint(service_type='baremetal') - try: - self._ironic_client = ironic_client.Client( - '1', endpoint, token=token) - except Exception as e: - raise OpenStackCloudException( - "Error in connecting to ironic: %s" % e.message) - return self._ironic_client - def get_name(self): return self.name @@ -589,17 +582,51 @@ def delete_server(self, name, wait=False, timeout=180): raise OpenStackCloudException( "Timed out waiting for server to get deleted.") - def list_ironic_nodes(self): - return self.ironic_client.node.list() - def get_ironic_node(self, node_name): - return self.ironic_client.node.get(node_name) +class OperatorCloud(OpenStackCloud): + + @property + def ironic_client(self): + if self._ironic_client is None: + ironic_logging = logging.getLogger('ironicclient') + ironic_logging.addHandler(logging.NullHandler()) + token = self.keystone_client.auth_token + endpoint = self.get_endpoint(service_type='baremetal') + try: + self._ironic_client = ironic_client.Client( + '1', endpoint, token=token) + except Exception as e: + raise OpenStackCloudException( + "Error in connecting to ironic: %s" % e.message) + return self._ironic_client - def list_ironic_ports(self): + def list_nics(self): return self.ironic_client.port.list() - def get_ironic_port(self, port_name): - return self.ironic_client.port.get(port_name) + def get_nic_by_mac(self, mac): + try: + return self.ironic_client.port.get(mac) + except ironic_exceptions.ClientException: + return None + + def list_machines(self): + return self.ironic_client.node.list() + + def get_machine_by_uuid(self, uuid): + try: + return self.ironic_client.node.get(uuid) + except ironic_exceptions.ClientException: + return None + + def get_machine_by_mac(self, mac): + try: + port = self.ironic_client.port.get(mac) + return self.ironic_client.node.get(port.node_uuid) + except ironic_exceptions.ClientException: + return None + + def create_machine(self, **kwargs): + return self.ironic_client.node.create(**kwargs) - def list_ironic_ports_for_node(self, node_name): - return self.ironic_client.node.list_ports(node_name) + def delete_machine(self, uuid): + return self.ironic_client.node.delete(uuid) From 86f2c2b7a935a8f25cbf34b5328a2103d1571a61 Mon Sep 17 00:00:00 2001 From: Devananda van der Veen Date: Tue, 14 Oct 2014 09:06:14 -0700 Subject: [PATCH 0040/3836] Move ironic node create/delete logic into shade Move the logic around sanely registering and unregistering nodes, along with the associated NICs, out of shade-ansible and into shade. --- shade/__init__.py | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index d9d33c8af..599c6b914 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -603,6 +603,9 @@ def ironic_client(self): def list_nics(self): return self.ironic_client.port.list() + def list_nics_for_machine(self, uuid): + return self.ironic_client.node.list_ports(uuid) + def get_nic_by_mac(self, mac): try: return self.ironic_client.port.get(mac) @@ -625,8 +628,36 @@ def get_machine_by_mac(self, mac): except ironic_exceptions.ClientException: return None - def create_machine(self, **kwargs): - return self.ironic_client.node.create(**kwargs) + def register_machine(self, nics, **kwargs): + try: + machine = self.ironic_client.node.create(**kwargs) + except Exception as e: + raise OpenStackCloudException( + "Error registering machine with Ironic: %s" % e.message) - def delete_machine(self, uuid): - return self.ironic_client.node.delete(uuid) + created_nics = [] + try: + for row in nics: + nic = self.ironic_client.port.create(address=row['mac'], + node_uuid=machine.uuid) + created_nics.append(nic.uuid) + except Exception as e: + for uuid in created_nics: + self.ironic_client.port.delete(uuid) + self.ironic_client.node.delete(machine.uuid) + raise OpenStackCloudException( + "Error registering NICs with Ironic: %s" % e.message) + return machine + + def unregister_machine(self, nics, uuid): + for nic in nics: + try: + self.ironic_client.port.delete( + self.ironic_client.port.get_by_address(nic['mac'])) + except Exception as e: + raise OpenStackCloudException(e.message) + try: + self.ironic_client.node.delete(uuid) + except Exception as e: + raise OpenStackCloudException( + "Error unregistering machine from Ironic: %s" % e.message) From 313fb397d516146164bc2c28cbadc95dad9be6a2 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 14 Oct 2014 21:43:50 -0700 Subject: [PATCH 0041/3836] Support injecting mount-point meta info Volumes need to know where they're going to be mounted. But really, it's not the volume that needs to know that - it's the user associating volumes and servers. Make it something that can be injected into server metadata. --- shade/meta.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/shade/meta.py b/shade/meta.py index f22d89d32..4fad4a81a 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -74,7 +74,7 @@ def get_groups_from_server(cloud, server, server_vars): return groups -def get_hostvars_from_server(cloud, server): +def get_hostvars_from_server(cloud, server, mounts=None): server_vars = dict() # Fist, add an IP address if (cloud.private): @@ -105,6 +105,12 @@ def get_hostvars_from_server(cloud, server): server_vars['volumes'] = [ obj_to_dict(f) for f in cloud.get_volumes(server)] + if mounts: + for mount in mounts: + for vol in server_vars['volumes']: + if vol['display_name'] == mount['display_name']: + if 'mount' in mount: + vol['mount'] = mount['mount'] az = server_vars.get('OS-EXT-AZ:availability_zone', None) if az: From 0ed767e20937a62ed2f190c6645673236b11237a Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Sun, 12 Oct 2014 20:06:14 -0500 Subject: [PATCH 0042/3836] Map CloudConfig attributes to CloudConfig.config Treat the CloudConfig object as if it has the config attributes directly. And add some simple tests. This makes it easier to replace an argparse.Namespace() object with a CloudConfig object. It also might make initialization of some of the default attributes unnecessary. An example of this usage is in https://review.openstack.org/#/c/129795/1/openstackclient/shell.py Change-Id: I00ced540cf94742e8cb738f8f0767445ffeb4bfe --- os_client_config/cloud_config.py | 14 +++++++ os_client_config/tests/test_cloud_config.py | 44 +++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 os_client_config/tests/test_cloud_config.py diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index d0c932f41..c17bfc2b7 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -18,3 +18,17 @@ def __init__(self, name, region, config): self.name = name self.region = region self.config = config + + def __getattr__(self, key): + """Return arbitrary attributes.""" + + if key.startswith('os_'): + key = key[3:] + + if key in [attr.replace('-', '_') for attr in self.config]: + return self.config[key] + else: + return None + + def __iter__(self): + return self.config.__iter__() diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py new file mode 100644 index 000000000..36386e50e --- /dev/null +++ b/os_client_config/tests/test_cloud_config.py @@ -0,0 +1,44 @@ +# 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 os_client_config import cloud_config +from os_client_config.tests import base + + +fake_config_dict = {'a': 1, 'os_b': 2, 'c': 3, 'os_c': 4} + + +class TestCloudConfig(base.TestCase): + + def test_arbitrary_attributes(self): + cc = cloud_config.CloudConfig("test1", "region-al", fake_config_dict) + self.assertEqual("test1", cc.name) + self.assertEqual("region-al", cc.region) + + # Look up straight value + self.assertEqual(1, cc.a) + + # Look up prefixed attribute, fail - returns None + self.assertEqual(None, cc.os_b) + + # Look up straight value, then prefixed value + self.assertEqual(3, cc.c) + self.assertEqual(3, cc.os_c) + + # Lookup mystery attribute + self.assertIsNone(cc.x) + + def test_iteration(self): + cc = cloud_config.CloudConfig("test1", "region-al", fake_config_dict) + self.assertTrue('a' in cc) + self.assertFalse('x' in cc) From 9c540a224da10f26a8579140a8a85587c9407bb1 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 25 Oct 2014 18:52:05 -0700 Subject: [PATCH 0043/3836] Add some additional server meta munging --- shade/meta.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/shade/meta.py b/shade/meta.py index 4fad4a81a..de1437a54 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -103,8 +103,13 @@ def get_hostvars_from_server(cloud, server, mounts=None): if image_name: server_vars['image']['name'] = image_name - server_vars['volumes'] = [ - obj_to_dict(f) for f in cloud.get_volumes(server)] + volumes = [] + for vol in cloud.get_volumes(server): + volume = obj_to_dict(vol) + # Make things easier to consume elsewhere + volume['device'] = volume['attachments'][0]['device'] + volumes.append(volume) + server_vars['volumes'] = volumes if mounts: for mount in mounts: for vol in server_vars['volumes']: From 8f1bb101ea688456de7dcd269dc885558aaea383 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 26 Oct 2014 15:04:43 -0700 Subject: [PATCH 0044/3836] Fix a missed argument from a previous refactor Region name support got changed a little while ago, and a call that should now be a keyword argument style stayed as a positional ... which means that we lost region name support for clouds with more than one region. (it treated all of them like the first region) Change-Id: I666758a775b8fc8e03b7e9ddd3aa494c13505612 --- os_client_config/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 753c80aab..925c23f48 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -158,7 +158,7 @@ def get_all_clouds(self): for cloud in self._get_cloud_sections(): for region in self._get_regions(cloud).split(','): - clouds.append(self.get_one_cloud(cloud, region)) + clouds.append(self.get_one_cloud(cloud, region_name=region)) return clouds def _fix_args(self, args, argparse=None): From b3483829b581b92b55afa18bb4cc44615794f03e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 30 Oct 2014 20:33:48 +0100 Subject: [PATCH 0045/3836] Make get_image work on name or id There is no reason to make the user specify. It's simple, while we're looping through images, if the id is an exact match, bingo - it's the image. If it's not, but the name is and matches the other rules, bingo. Everything else is stupid. --- shade/__init__.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 599c6b914..ecf6df63c 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -376,25 +376,27 @@ def list_images(self): return self._image_cache def get_image_name(self, image_id): - if image_id not in self.list_images(): - self._image_cache[image_id] = None - if self._image_cache[image_id]: - return self._image_cache[image_id].name + image = self.get_image(image_id, exclude) + if image: + return image.id + self._image_cache[image_id] = None return None def get_image_id(self, image_name, exclude=None): - image = self.get_image_by_name(image_name, exclude) + image = self.get_image(image_name, exclude) if image: return image.id return None - def get_image_by_name(self, name, exclude=None): + def get_image(self, name_or_id, exclude=None): for (image_id, image) in self.list_images().items(): - if (name in image.name and ( + if image_id == name_or_id: + return image + if (name_or_id in image.name and ( not exclude or exclude not in image.name)): return image raise OpenStackCloudException( - "Error finding image id from name(%s)" % name) + "Error finding image from %s" % name_or_id) def _get_volumes_from_cloud(self): try: From 4673bea22d7d481a79f62949e2fd8eb97e78cdb3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 30 Oct 2014 21:16:18 +0100 Subject: [PATCH 0046/3836] Support boot from volume The outbound interface for this is CRAZY --- shade/__init__.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index ecf6df63c..f1a833a37 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -417,12 +417,19 @@ def get_volumes(self, server): volumes.append(volume) return volumes - def get_volume_id(self, volume_name): - for v in self.cinder_client.volumes.list(): - if v.display_name == volume_name: - return v.id + def get_volume_id(self, name_or_id): + image = self.get_volume(name_or_id) + if image: + return image.id return None + def get_volume(self, name_or_id): + for v in self.list_volumes(): + if name_or_id in (v.display_name, v.id): + return v + raise OpenStackCloudException( + "Error finding volume from %s" % name_or_id) + def get_server_by_id(self, server_id): for server in self.nova_client.servers.list(): if server.id == server_id: @@ -538,7 +545,19 @@ def add_ips_to_server(self, server, auto_ip=True, ips=None, ip_pool=None): def create_server(self, bootargs, bootkwargs, auto_ip=True, ips=None, ip_pool=None, + root_volume=None, terminate_volume=False, wait=False, timeout=180): + + if root_volume: + if terminate_volume: + suffix = ':::1' + else: + suffix = ':::0' + volume_id = self.get_volume_id(root_volume) + suffix + if 'block_device_mapping' not in bootkwargs: + bootkwargs['block_device_mapping'] = dict() + bootkwargs['block_device_mapping']['vda'] = volume_id + try: server = self.nova_client.servers.create(*bootargs, **bootkwargs) server = self.nova_client.servers.get(server.id) From 1c025d548f63b613c198f47065bde8cf43655fb3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 2 Nov 2014 13:44:36 +0100 Subject: [PATCH 0047/3836] Throw error if a non-existent cloud is requested The error messages when a bogus cloud is requested are very confusing. (They'll be things like "no auth url provided") Instead, be explicit on the problem. Change-Id: Idf68d1db7e5fccd712283775eb4d636d78ae5fc7 --- os_client_config/cloud_config.py | 2 +- os_client_config/config.py | 31 +++++++++++++++---------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index c17bfc2b7..6f501c692 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -15,7 +15,7 @@ class CloudConfig(object): def __init__(self, name, region, config): - self.name = name + self.name = name or 'openstack' self.region = region self.config = config diff --git a/os_client_config/config.py b/os_client_config/config.py index 925c23f48..ca3a75a73 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -115,10 +115,14 @@ def _get_cloud_sections(self): def _get_base_cloud_config(self, name): cloud = dict() - if name in self.cloud_config['clouds']: - our_cloud = self.cloud_config['clouds'][name] - else: - our_cloud = dict() + + # Only validate cloud name if one was given + if name and name not in self.cloud_config['clouds']: + raise exceptions.OpenStackConfigException( + "Named cloud {name} requested that was not found.".format( + name=name)) + + our_cloud = self.cloud_config['clouds'].get(name, dict()) # Get the defaults (including env vars) first cloud.update(self.defaults) @@ -208,15 +212,10 @@ def get_one_cloud(self, cloud=None, validate=True, args = self._fix_args(kwargs, argparse=argparse) - if cloud: - name = cloud - else: - name = 'openstack' - if 'region_name' not in args: - args['region_name'] = self._get_region(name) + args['region_name'] = self._get_region(cloud) - config = self._get_base_cloud_config(name) + config = self._get_base_cloud_config(cloud) # Can't just do update, because None values take over for (key, val) in args.iteritems(): @@ -233,15 +232,15 @@ def get_one_cloud(self, cloud=None, validate=True, if key not in config or not config[key]: raise exceptions.OpenStackConfigException( 'Unable to find full auth information for cloud' - ' {name} in config files {files}' + ' {cloud} in config files {files}' ' or environment variables.'.format( - name=name, files=','.join(self._config_files))) + cloud=cloud, files=','.join(self._config_files))) if 'project_name' not in config and 'project_id' not in config: raise exceptions.OpenStackConfigException( 'Neither project_name or project_id information found' - ' for cloud {name} in config files {files}' + ' for cloud {cloud} in config files {files}' ' or environment variables.'.format( - name=name, files=','.join(self._config_files))) + cloud=cloud, files=','.join(self._config_files))) # If any of the defaults reference other values, we need to expand for (key, value) in config.items(): @@ -249,7 +248,7 @@ def get_one_cloud(self, cloud=None, validate=True, config[key] = value.format(**config) return cloud_config.CloudConfig( - name=name, region=config['region_name'], config=config) + name=cloud, region=config['region_name'], config=config) if __name__ == '__main__': config = OpenStackConfig().get_all_clouds() From 98d3de9f08886950dec4006fdce1b1d467a724d2 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sun, 9 Nov 2014 08:18:42 -0300 Subject: [PATCH 0048/3836] Use yaml.safe_load instead of load. yaml.load will execute arbitrary code. Also use context managers to ensure files are closed Change-Id: I704baa7916ee834c12821009d8e3029b1b8fa340 --- os_client_config/config.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 925c23f48..c7119192b 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -87,12 +87,14 @@ def __init__(self, config_files=None, vendor_files=None): def _load_config_file(self): for path in self._config_files: if os.path.exists(path): - return yaml.load(open(path, 'r')) + with open(path, 'r') as f: + return yaml.safe_load(f) def _load_vendor_file(self): for path in self._vendor_files: if os.path.exists(path): - return yaml.load(open(path, 'r')) + with open(path, 'r') as f: + return yaml.safe_load(f) def get_cache_max_age(self): return self._cache_max_age From ca54d7290000f77c5382e227a7cf6e631a151174 Mon Sep 17 00:00:00 2001 From: Jeremy Stanley Date: Mon, 24 Nov 2014 17:10:43 +0000 Subject: [PATCH 0049/3836] Corrections to readme * README.rst: Remove docs and bugs links since the project is not currently publishing/consuming these anywhere that I can find. Also correct the source URL. Change-Id: I2b7002f7ea301a2c03a2e41e904de0c91a14831a --- README.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 30819f398..49814224f 100644 --- a/README.rst +++ b/README.rst @@ -143,6 +143,4 @@ Or, get all of the clouds. print(cloud.name, cloud.region, cloud.config) * Free software: Apache license -* Documentation: http://docs.openstack.org/developer/os-client-config -* Source: http://git.openstack.org/cgit/openstack/os-client-config -* Bugs: http://bugs.launchpad.net/os-client-config +* Source: http://git.openstack.org/cgit/stackforge/os-client-config From cdb3e37ccd079d10ca8fca334623a72fd260b726 Mon Sep 17 00:00:00 2001 From: Jeremy Stanley Date: Fri, 5 Dec 2014 03:30:46 +0000 Subject: [PATCH 0050/3836] Workflow documentation is now in infra-manual Replace URLs for workflow documentation to appropriate parts of the OpenStack Project Infrastructure Manual. Change-Id: I97d11d3a24a374c9212cd29f49472e199b1c8bc0 --- CONTRIBUTING.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index c1f3dc297..1990ecf25 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,14 +1,13 @@ If you would like to contribute to the development of OpenStack, -you must follow the steps in the "If you're a developer, start here" -section of this page: +you must follow the steps in this page: - http://wiki.openstack.org/HowToContribute + http://docs.openstack.org/infra/manual/developers.html Once those steps have been completed, changes to OpenStack should be submitted for review via the Gerrit tool, following the workflow documented at: - http://wiki.openstack.org/GerritWorkflow + http://docs.openstack.org/infra/manual/developers.html#development-workflow Pull requests submitted through GitHub will be ignored. From 3235321c01ad38e8ff3b170eac587cd8a175db3c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 12 Dec 2014 17:28:13 +0000 Subject: [PATCH 0051/3836] Add better caching around volumes --- shade/__init__.py | 43 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index f1a833a37..cd70c9840 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -38,6 +38,9 @@ class OpenStackCloudException(Exception): pass +class OpenStackCloudTimeout(OpenStackCloudException): + pass + def openstack_clouds(config=None): if not config: @@ -404,14 +407,14 @@ def _get_volumes_from_cloud(self): except Exception: return [] - def list_volumes(self): - if self._volume_cache is None: + def list_volumes(self, cache=True): + if self._volume_cache is None or not cache: self._volume_cache = self._get_volumes_from_cloud() return self._volume_cache - def get_volumes(self, server): + def get_volumes(self, server, cache=True): volumes = [] - for volume in self.list_volumes(): + for volume in self.list_volumes(cache=cache): for attach in volume.attachments: if attach['server_id'] == server.id: volumes.append(volume) @@ -423,12 +426,18 @@ def get_volume_id(self, name_or_id): return image.id return None - def get_volume(self, name_or_id): - for v in self.list_volumes(): + def get_volume(self, name_or_id, cache=True, error=True): + for v in self.list_volumes(cache=cache): if name_or_id in (v.display_name, v.id): return v - raise OpenStackCloudException( - "Error finding volume from %s" % name_or_id) + if error: + raise OpenStackCloudException( + "Error finding volume from %s" % name_or_id) + return None + + def volume_exists(self, name_or_id): + return self.get_volume( + name_or_id, cache=False, error=False) is not None def get_server_by_id(self, server_id): for server in self.nova_client.servers.list(): @@ -448,6 +457,12 @@ def get_server_id(self, server_name): return server.id return None + def get_server(self, name_or_id): + for server in self.list_servers(): + if name_or_id in (server.name, server.id): + return server + return None + def get_server_meta(self, server): server_vars = meta.get_hostvars_from_server(self, server) groups = meta.get_groups_from_server(self, server, server_vars) @@ -600,9 +615,19 @@ def delete_server(self, name, wait=False, timeout=180): if not server: return time.sleep(5) - raise OpenStackCloudException( + raise OpenStackCloudTimeout( "Timed out waiting for server to get deleted.") + def delete_volume(self, name_or_id, wait=False, timeout=180): + volume = self.get_volume(name_or_id) + + expire = time.time() + timeout + while time.time() < expire: + if self.volume_exists(volume.id, cache=False): + return + time.sleep(5) + raise OpenStackCloudTimeout( + "Timed out waiting for server to get deleted.") class OperatorCloud(OpenStackCloud): From 5b3c005aae0a4792458ba4b51b4bf9080a12c386 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 7 Jan 2015 12:31:15 -0500 Subject: [PATCH 0052/3836] Fix up copyright headers --- doc/source/conf.py | 2 +- shade/__init__.py | 2 +- shade/meta.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 4be3c2e99..97d3c2ff4 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -11,7 +11,7 @@ # General information about the project. project = u'shade' -copyright = u'2014, Monty Taylor' +copyright = u'2014 Hewlett-Packard Development Company, L.P.' # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True diff --git a/shade/__init__.py b/shade/__init__.py index cd70c9840..de678d9a1 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2014 Monty Taylor +# Copyright (c) 2014 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. diff --git a/shade/meta.py b/shade/meta.py index de1437a54..95849cb4d 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -1,4 +1,4 @@ -# Copyright (c) 2014 Monty Taylor +# Copyright (c) 2014 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. From a56767a865baeeb0174c03e80809467edaaca2e0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 7 Jan 2015 12:59:34 -0500 Subject: [PATCH 0053/3836] Fix flake8 errors and turn off hacking --- shade/__init__.py | 24 ++++++++++++++---------- shade/meta.py | 1 - shade/tests/base.py | 2 +- shade/tests/test_shade.py | 2 +- tox.ini | 5 +++-- 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index de678d9a1..bd9cd5e0e 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -24,6 +24,7 @@ from keystoneclient import client as keystone_client from novaclient import exceptions as nova_exceptions from novaclient.v1_1 import client as nova_client +from novaclient.v1_1 import floating_ips import os_client_config import pbr.version import troveclient.client as trove_client @@ -38,6 +39,7 @@ class OpenStackCloudException(Exception): pass + class OpenStackCloudTimeout(OpenStackCloudException): pass @@ -378,7 +380,7 @@ def list_images(self): self._image_cache = self._get_images_from_cloud() return self._image_cache - def get_image_name(self, image_id): + def get_image_name(self, image_id, exclude): image = self.get_image(image_id, exclude) if image: return image.id @@ -452,7 +454,7 @@ def get_server_by_name(self, server_name): return None def get_server_id(self, server_name): - server = get_server_by_name(server_name) + server = self.get_server_by_name(server_name) if server: return server.id return None @@ -498,7 +500,7 @@ def add_ip_from_pool(self, server, pools): if not pool_ips: try: new_ip = self.nova_client.floating_ips.create(pool) - except Exception as e: + except Exception: raise OpenStackCloudException( "Unable to create floating ip in pool %s" % pool) pool_ips.append(new_ip.ip) @@ -591,7 +593,8 @@ def create_server(self, bootargs, bootkwargs, continue if server.status == 'ACTIVE': - return self.add_ips_to_server(server, auto_ip, ips, ip_pool) + return self.add_ips_to_server( + server, auto_ip, ips, ip_pool) if server.status == 'ERROR': raise OpenStackCloudException( @@ -605,13 +608,13 @@ def create_server(self, bootargs, bootkwargs, def delete_server(self, name, wait=False, timeout=180): server_list = self.nova_client.servers.list(True, {'name': name}) if server_list: - server = [x for x in server_list if x.name == module.params['name']] + server = [x for x in server_list if x.name == name] self.nova_client.servers.delete(server.pop()) if not wait: return expire = time.time() + timeout while time.time() < expire: - server = nova.servers.list(True, {'name': name}) + server = self.nova_client.servers.list(True, {'name': name}) if not server: return time.sleep(5) @@ -629,6 +632,7 @@ def delete_volume(self, name_or_id, wait=False, timeout=180): raise OpenStackCloudTimeout( "Timed out waiting for server to get deleted.") + class OperatorCloud(OpenStackCloud): @property @@ -679,7 +683,7 @@ def register_machine(self, nics, **kwargs): machine = self.ironic_client.node.create(**kwargs) except Exception as e: raise OpenStackCloudException( - "Error registering machine with Ironic: %s" % e.message) + "Error registering machine with Ironic: %s" % e.message) created_nics = [] try: @@ -692,18 +696,18 @@ def register_machine(self, nics, **kwargs): self.ironic_client.port.delete(uuid) self.ironic_client.node.delete(machine.uuid) raise OpenStackCloudException( - "Error registering NICs with Ironic: %s" % e.message) + "Error registering NICs with Ironic: %s" % e.message) return machine def unregister_machine(self, nics, uuid): for nic in nics: try: self.ironic_client.port.delete( - self.ironic_client.port.get_by_address(nic['mac'])) + self.ironic_client.port.get_by_address(nic['mac'])) except Exception as e: raise OpenStackCloudException(e.message) try: self.ironic_client.node.delete(uuid) except Exception as e: raise OpenStackCloudException( - "Error unregistering machine from Ironic: %s" % e.message) + "Error unregistering machine from Ironic: %s" % e.message) diff --git a/shade/meta.py b/shade/meta.py index 95849cb4d..65208f39a 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import re import types NON_CALLABLES = (basestring, bool, dict, int, list, types.NoneType) diff --git a/shade/tests/base.py b/shade/tests/base.py index 93b83262c..0b5b1f446 100644 --- a/shade/tests/base.py +++ b/shade/tests/base.py @@ -50,4 +50,4 @@ def setUp(self): stderr = self.useFixture(fixtures.StringStream('stderr')).stream self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) - self.log_fixture = self.useFixture(fixtures.FakeLogger()) \ No newline at end of file + self.log_fixture = self.useFixture(fixtures.FakeLogger()) diff --git a/shade/tests/test_shade.py b/shade/tests/test_shade.py index e8b0ac82f..687129671 100644 --- a/shade/tests/test_shade.py +++ b/shade/tests/test_shade.py @@ -25,4 +25,4 @@ class TestShade(base.TestCase): def test_something(self): - pass \ No newline at end of file + pass diff --git a/tox.ini b/tox.ini index 4649796f5..5a1018329 100644 --- a/tox.ini +++ b/tox.ini @@ -25,8 +25,9 @@ commands = {posargs} commands = python setup.py testr --coverage --testr-args='{posargs}' [flake8] -# We're not inside of OpenStack, so H305 makes no sense -ignore = H305 +# Infra does not follow hacking, nor the broken E12* things +ignore = E123,E125,H +select = H231 show-source = True builtins = _ exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build From 65fe04d5c2bbe4d66c3ba9ea43467ec38316eaa0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 7 Jan 2015 13:03:32 -0500 Subject: [PATCH 0054/3836] Change meta info to be an Infra project --- .gitreview | 2 +- CONTRIBUTING.rst | 46 +++++++++++++++++++++++++++++++++++----------- setup.cfg | 9 ++++----- tox.ini | 2 +- 4 files changed, 41 insertions(+), 18 deletions(-) diff --git a/.gitreview b/.gitreview index fd7871d20..35d93597c 100644 --- a/.gitreview +++ b/.gitreview @@ -1,4 +1,4 @@ [gerrit] host=review.openstack.org port=29418 -project=stackforge/shade.git \ No newline at end of file +project=openstack-infra/shade.git diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 5ed928270..ee1e32b0e 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,17 +1,41 @@ -If you would like to contribute to the development of OpenStack, -you must follow the steps in the "If you're a developer, start here" -section of this page: +.. _contributing: - http://wiki.openstack.org/HowToContribute +===================== +Contributing to shade +===================== -Once those steps have been completed, changes to OpenStack -should be submitted for review via the Gerrit tool, following -the workflow documented at: +If you're interested in contributing to the shade project, +the following will help get you started. - http://wiki.openstack.org/GerritWorkflow +Contributor License Agreement +----------------------------- -Pull requests submitted through GitHub will be ignored. +.. index:: + single: license; agreement -Bugs should be filed on Launchpad, not GitHub: +In order to contribute to the shade project, you need to have +signed OpenStack's contributor's agreement. - https://bugs.launchpad.net/shade \ No newline at end of file +.. seealso:: + + * http://wiki.openstack.org/HowToContribute + * http://wiki.openstack.org/CLA + +Project Hosting Details +------------------------- + +Bug tracker + http://storyboard.openstack.org + +Mailing list (prefix subjects with ``[shade]`` for faster responses) + http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-infra + +Code Hosting + * https://git.openstack.org/cgit/openstack-infra/shade + +Code Review + https://review.openstack.org/#/q/status:open+project:openstack-infra/shade,n,z + + Please read `GerritWorkflow`_ before sending your first patch for review. + +.. _GerritWorkflow: https://wiki.openstack.org/wiki/GerritWorkflow diff --git a/setup.cfg b/setup.cfg index 5d0c41551..1756a9c85 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,9 +3,9 @@ name = shade summary = Client library for operating OpenStack clouds description-file = README.rst -author = Monty Taylor -author-email = mordred@inaugust.com -home-page = http://www.openstack.org/ +author = OpenStack Infrastructure Team +author-email = openstack-infra@lists.openstack.org +home-page = http://ci.openstack.org/ classifier = Environment :: OpenStack Intended Audience :: Information Technology @@ -15,9 +15,8 @@ classifier = Programming Language :: Python Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 - Programming Language :: Python :: 2.6 Programming Language :: Python :: 3 - Programming Language :: Python :: 3.3 + Programming Language :: Python :: 3.4 [build_sphinx] source-dir = doc/source diff --git a/tox.ini b/tox.ini index 5a1018329..785c44d34 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 1.6 -envlist = py26,py27,py33,pypy,pep8 +envlist = py26,py27,py34,pypy,pep8 skipsdist = True [testenv] From 88f5f2661ee2ba31056251b4cdf91949fca0a031 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 7 Jan 2015 20:08:04 -0500 Subject: [PATCH 0055/3836] Fix python3 unittests Turns out basestring and types.NoneType go away in python3 - so just do the logic a different way. Change-Id: Ic447e1f3bdf3eac7335995edd4d20b15820f78f0 --- shade/meta.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/shade/meta.py b/shade/meta.py index 65208f39a..82f36c88f 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -12,10 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import types - -NON_CALLABLES = (basestring, bool, dict, int, list, types.NoneType) - def find_nova_addresses(addresses, ext_tag, key_name=None): @@ -127,6 +123,6 @@ def obj_to_dict(obj): instance = {} for key in dir(obj): value = getattr(obj, key) - if (isinstance(value, NON_CALLABLES) and not key.startswith('_')): + if not callable(value) and not key.startswith('_'): instance[key] = value return instance From e24208741361fc167d21f1624dba761453771a51 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 12 Jan 2015 17:14:03 -0800 Subject: [PATCH 0056/3836] Explain obj_to_dict It's entirely possile that the reason for the existence of this function may not be immediately apparent. Change-Id: Ia6839740d676ca99d919af732ede3a5982f5d864 --- shade/meta.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/shade/meta.py b/shade/meta.py index 82f36c88f..db3354b06 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -120,6 +120,14 @@ def get_hostvars_from_server(cloud, server, mounts=None): def obj_to_dict(obj): + """ Turn an object with attributes into a dict suitable for serializing. + + Some of the things that are returned in OpenStack are objects with + attributes. That's awesome - except when you want to expose them as JSON + structures. We use this as the basis of get_hostvars_from_server above so + that we can just have a plain dict of all of the values that exist in the + nova metadata for a server. + """ instance = {} for key in dir(obj): value = getattr(obj, key) From f0a21c8f220450def5eee23beb17dda63712447f Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 12 Jan 2015 19:34:52 -0500 Subject: [PATCH 0057/3836] Remove py26 support The code will not pass on py26 testing, but who cares about py26 anymore anyway. Change-Id: I3009a01b392ade130f0802b3a995565d46c46b0f --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 785c44d34..f03a88529 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 1.6 -envlist = py26,py27,py34,pypy,pep8 +envlist = py27,py34,pypy,pep8 skipsdist = True [testenv] From 621a8636f505dd5c091a7fa8155ea79dc36b1b41 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 20 Jan 2015 18:04:21 -0800 Subject: [PATCH 0058/3836] Replace defaults_dict with scanning env vars All of the OS_ env vars start with OS_. Also, because of keystoneclient auth plugins, we have no idea which ones we need. Instead of positively identifying them - just grab them all - since they're all base layer and can be overridden by everything else anyway. Change-Id: I633f5e7d27c0a6a5c9b25f53cb99fe05b63c78ae --- os_client_config/config.py | 30 ++++++++++-------------------- os_client_config/defaults_dict.py | 27 --------------------------- 2 files changed, 10 insertions(+), 47 deletions(-) delete mode 100644 os_client_config/defaults_dict.py diff --git a/os_client_config/config.py b/os_client_config/config.py index 9656f6ca1..9c3ca874f 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -18,7 +18,6 @@ import yaml from os_client_config import cloud_config -from os_client_config import defaults_dict from os_client_config import exceptions from os_client_config import vendors @@ -44,31 +43,22 @@ def get_boolean(value): return False +def _get_os_environ(): + ret = dict() + for (k, v) in os.environ.items(): + if k.startswith('OS_'): + newkey = k[3:].lower() + ret[newkey] = v + return ret + + class OpenStackConfig(object): def __init__(self, config_files=None, vendor_files=None): self._config_files = config_files or CONFIG_FILES self._vendor_files = vendor_files or VENDOR_FILES - defaults = defaults_dict.DefaultsDict() - defaults.add('username') - defaults.add('user_domain_name') - defaults.add('password') - defaults.add( - 'project_name', defaults.get('username', None), - also='tenant_name') - defaults.add('project_id', also='tenant_id') - defaults.add('project_domain_name') - defaults.add('auth_url') - defaults.add('region_name') - defaults.add('cache') - defaults.add('auth_token') - defaults.add('insecure') - defaults.add('endpoint_type') - defaults.add('cacert') - defaults.add('auth_type') - - self.defaults = defaults + self.defaults = _get_os_environ() # use a config file if it exists where expected self.cloud_config = self._load_config_file() diff --git a/os_client_config/defaults_dict.py b/os_client_config/defaults_dict.py deleted file mode 100644 index 43de77beb..000000000 --- a/os_client_config/defaults_dict.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (c) 2014 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. - -import os - - -class DefaultsDict(dict): - - def add(self, key, default_value=None, also=None, prefix=None): - if prefix: - key = '%s_%s' % (prefix.replace('-', '_'), key) - if also: - value = os.environ.get(also, default_value) - value = os.environ.get('OS_%s' % key.upper(), default_value) - if value is not None: - self.__setitem__(key, value) From c75daaa1f9f0881cae63337e0d9799555f569c06 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 20 Jan 2015 19:07:08 -0800 Subject: [PATCH 0059/3836] Support keystone auth plugins in a generic way. Basically, if keystoneclient is available, validate arguments into the auth dict. If it's not - we should probably be talking about what possible use this library has - but it should degrade cleanly and treat everything as passthrough. Change-Id: Ia31039d5c724eba22d053a004eefeaf6857f500d --- README.rst | 42 ++++++++++++------ os_client_config/config.py | 87 +++++++++++++++++++++++++++++-------- os_client_config/vendors.py | 9 +++- 3 files changed, 104 insertions(+), 34 deletions(-) diff --git a/README.rst b/README.rst index 30819f398..0d04f5f04 100644 --- a/README.rst +++ b/README.rst @@ -57,23 +57,27 @@ An example config file is probably helpful: clouds: mordred: cloud: hp - username: mordred@inaugust.com - password: XXXXXXXXX - project_id: mordred@inaugust.com + auth: + username: mordred@inaugust.com + password: XXXXXXXXX + project_name: mordred@inaugust.com region_name: region-b.geo-1 dns_service_type: hpext:dns + compute_api_version: 1.1 monty: - auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0 - username: monty.taylor@hp.com - password: XXXXXXXX - project_id: monty.taylor@hp.com-default-tenant + auth: + auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0 + username: monty.taylor@hp.com + password: XXXXXXXX + project_name: monty.taylor@hp.com-default-tenant region_name: region-b.geo-1 dns_service_type: hpext:dns infra: cloud: rackspace - username: openstackci - password: XXXXXXXX - project_id: 610275 + auth: + username: openstackci + password: XXXXXXXX + project_id: 610275 region_name: DFW,ORD,IAD You may note a few things. First, since auth_url settings are silly @@ -92,6 +96,17 @@ the setting with the default service type. That might strike you funny when setting `service_type` and it does me too - but that's just the world we live in. +Auth Settings +------------- + +Keystone has auth plugins - which means it's not possible to know ahead of time +which auth settings are needed. `os-client-config` sets the default plugin type +to `password`, which is what things all were before plugins came about. In +order to facilitate validation of values, all of the parameters that exist +as a result of a chosen plugin need to go into the auth dict. For password +auth, this includes `auth_url`, `username` and `password` as well as anything +related to domains, projects and trusts. + Cache Settings -------------- @@ -107,9 +122,10 @@ understands a simple set of cache control settings. clouds: mordred: cloud: hp - username: mordred@inaugust.com - password: XXXXXXXXX - project_id: mordred@inaugust.com + auth: + username: mordred@inaugust.com + password: XXXXXXXXX + project_name: mordred@inaugust.com region_name: region-b.geo-1 dns_service_type: hpext:dns diff --git a/os_client_config/config.py b/os_client_config/config.py index 9c3ca874f..67436fad9 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -17,6 +17,11 @@ import yaml +try: + import keystoneclient.auth as ksc_auth +except ImportError: + ksc_auth = None + from os_client_config import cloud_config from os_client_config import exceptions from os_client_config import vendors @@ -31,7 +36,6 @@ os.environ.get('XDG_CACHE_PATH', os.path.join('~', '.cache'))), 'openstack') BOOL_KEYS = ('insecure', 'cache') -REQUIRED_VALUES = ('auth_url', 'username', 'password') VENDOR_SEARCH_PATH = [os.getcwd(), CONFIG_HOME, '/etc/openstack'] VENDOR_FILES = [ os.path.join(d, 'clouds-public.yaml') for d in VENDOR_SEARCH_PATH] @@ -44,7 +48,7 @@ def get_boolean(value): def _get_os_environ(): - ret = dict() + ret = dict(auth_plugin='password', auth=dict()) for (k, v) in os.environ.items(): if k.startswith('OS_'): newkey = k[3:].lower() @@ -52,6 +56,16 @@ def _get_os_environ(): return ret +def _auth_update(old_dict, new_dict): + """Like dict.update, except handling the nested dict called auth.""" + for (k, v) in new_dict.items(): + if k == 'auth': + old_dict[k].update(v) + else: + old_dict[k] = v + return old_dict + + class OpenStackConfig(object): def __init__(self, config_files=None, vendor_files=None): @@ -124,15 +138,15 @@ def _get_base_cloud_config(self, name): cloud_name = our_cloud['cloud'] vendor_file = self._load_vendor_file() if vendor_file and cloud_name in vendor_file['public-clouds']: - cloud.update(vendor_file['public-clouds'][cloud_name]) + _auth_update(cloud, vendor_file['public-clouds'][cloud_name]) else: try: - cloud.update(vendors.CLOUD_DEFAULTS[cloud_name]) + _auth_update(cloud, vendors.CLOUD_DEFAULTS[cloud_name]) except KeyError: # Can't find the requested vendor config, go about business pass - cloud.update(our_cloud) + _auth_update(cloud, our_cloud) if 'cloud' in cloud: del cloud['cloud'] @@ -186,6 +200,53 @@ def _fix_args(self, args, argparse=None): new_args.update(os_args) return new_args + def _find_winning_auth_value(self, opt, config): + opt_name = opt.name.replace('-', '_') + if opt_name in config: + return config[opt_name] + else: + for d_opt in opt.deprecated_opts: + d_opt_name = d_opt.name.replace('-', '_') + if d_opt_name in config: + return config[d_opt_name] + + def _validate_auth(self, config): + # May throw a keystoneclient.exceptions.NoMatchingPlugin + plugin_options = ksc_auth.get_plugin_class( + config['auth_plugin']).get_options() + + for p_opt in plugin_options: + # if it's in config.auth, win, kill it from config dict + # if it's in config and not in config.auth, move it + # deprecated loses to current + # provided beats default, deprecated or not + winning_value = self._find_winning_auth_value( + p_opt, config['auth']) + if not winning_value: + winning_value = self._find_winning_auth_value(p_opt, config) + + # 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: + raise exceptions.OpenStackConfigException( + 'Unable to find auth information for cloud' + ' {cloud} in config files {files}' + ' or environment variables. Missing value {auth_key}' + ' required for auth plugin {plugin}'.format( + cloud=cloud, files=','.join(self._config_files), + auth_key=p_opt.name, plugin=config['auth_plugin'])) + + # Clean up after ourselves + for opt in [p_opt.name] + [o.name for o in p_opt.deprecated_opts]: + opt = opt.replace('-', '_') + config.pop(opt, None) + config['auth'].pop(opt, None) + + if winning_value: + config['auth'][p_opt.name.replace('-', '_')] = winning_value + + return config + def get_one_cloud(self, cloud=None, validate=True, argparse=None, **kwargs): """Retrieve a single cloud configuration and merge additional options @@ -219,20 +280,8 @@ def get_one_cloud(self, cloud=None, validate=True, if type(config[key]) is not bool: config[key] = get_boolean(config[key]) - if validate: - for key in REQUIRED_VALUES: - if key not in config or not config[key]: - raise exceptions.OpenStackConfigException( - 'Unable to find full auth information for cloud' - ' {cloud} in config files {files}' - ' or environment variables.'.format( - cloud=cloud, files=','.join(self._config_files))) - if 'project_name' not in config and 'project_id' not in config: - raise exceptions.OpenStackConfigException( - 'Neither project_name or project_id information found' - ' for cloud {cloud} in config files {files}' - ' or environment variables.'.format( - cloud=cloud, files=','.join(self._config_files))) + if validate and ksc_auth: + config = self._validate_auth(config) # If any of the defaults reference other values, we need to expand for (key, value) in config.items(): diff --git a/os_client_config/vendors.py b/os_client_config/vendors.py index c78aaca54..eca437676 100644 --- a/os_client_config/vendors.py +++ b/os_client_config/vendors.py @@ -1,3 +1,4 @@ +# flake8: noqa # Copyright (c) 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -14,12 +15,16 @@ CLOUD_DEFAULTS = dict( hp=dict( - auth_url='https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0', + auth=dict( + auth_url='https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0', + ), region_name='region-b.geo-1', dns_service_type='hpext:dns', ), rackspace=dict( - auth_url='https://identity.api.rackspacecloud.com/v2.0/', + auth=dict( + auth_url='https://identity.api.rackspacecloud.com/v2.0/', + ), database_service_type='rax:database', image_api_version='2', ) From bc5608837f7d144f2f480c45c9b5499fca22098e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 20 Jan 2015 22:04:10 -0800 Subject: [PATCH 0060/3836] Start keeping default versions for all services It turns out we need to do the evil glance dance for almost everything. Change-Id: Ic0ad77ba0627bd4be88bdf0136aa04c2ba43afe6 --- os_client_config/config.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 67436fad9..bfc030efa 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -47,13 +47,12 @@ def get_boolean(value): return False -def _get_os_environ(): - ret = dict(auth_plugin='password', auth=dict()) +def _get_os_environ(defaults): for (k, v) in os.environ.items(): if k.startswith('OS_'): newkey = k[3:].lower() - ret[newkey] = v - return ret + defaults[newkey] = v + return defaults def _auth_update(old_dict, new_dict): @@ -72,7 +71,12 @@ def __init__(self, config_files=None, vendor_files=None): self._config_files = config_files or CONFIG_FILES self._vendor_files = vendor_files or VENDOR_FILES - self.defaults = _get_os_environ() + defaults = dict( + auth_plugin='password', + auth=dict(), + compute_api_version='1.1', + ) + self.defaults = _get_os_environ(defaults) # use a config file if it exists where expected self.cloud_config = self._load_config_file() From 13a455f1181bef0dfea95aa1728f7f8ba5ba32f7 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 12 Jan 2015 10:10:36 -0800 Subject: [PATCH 0061/3836] Debug log any time we re-raise an exception We're masking exceptions on purpose, which is evil of course - but the reason is that they wind up being part of the API - and the python-*client-ness of a particulate exception is largely an implementation detail that might change. Eventually we want to have better exceptions across the board - but make sure that we don't lose the ability to debug the exceptions when they happen. Change-Id: I8a972e6608e33088234d67ea24886c782a80e3b4 --- shade/__init__.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index bd9cd5e0e..54c365f02 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -501,6 +501,8 @@ def add_ip_from_pool(self, server, pools): try: new_ip = self.nova_client.floating_ips.create(pool) except Exception: + self.log.debug( + "nova floating ip create failed", exc_info=True) raise OpenStackCloudException( "Unable to create floating ip in pool %s" % pool) pool_ips.append(new_ip.ip) @@ -522,6 +524,8 @@ def add_ip_list(self, server, ips): try: server.add_floating_ip(ip) except Exception as e: + self.log.debug( + "nova floating ip add failed", exc_info=True) raise OpenStackCloudException( "Error attaching IP {ip} to instance {id}: {msg} ".format( ip=ip, id=server.id, msg=e.message)) @@ -530,6 +534,8 @@ def add_auto_ip(self, server): try: new_ip = self.nova_client.floating_ips.create() except Exception as e: + self.log.debug( + "nova floating ip create failed", exc_info=True) raise OpenStackCloudException( "Unable to create floating ip: %s" % (e.message)) try: @@ -556,6 +562,7 @@ def add_ips_to_server(self, server, auto_ip=True, ips=None, ip_pool=None): try: server = self.nova_client.servers.get(server.id) except Exception as e: + self.log.debug("nova info failed", exc_info=True) raise OpenStackCloudException( "Error in getting info from instance: %s " % e.message) return server @@ -579,6 +586,7 @@ def create_server(self, bootargs, bootkwargs, server = self.nova_client.servers.create(*bootargs, **bootkwargs) server = self.nova_client.servers.get(server.id) except Exception as e: + self.log.debug("nova instance create failed", exc_info=True) raise OpenStackCloudException( "Error in creating instance: %s" % e.message) if server.status == 'ERROR': @@ -646,6 +654,7 @@ def ironic_client(self): self._ironic_client = ironic_client.Client( '1', endpoint, token=token) except Exception as e: + self.log.debug("ironic auth failed", exc_info=True) raise OpenStackCloudException( "Error in connecting to ironic: %s" % e.message) return self._ironic_client @@ -682,6 +691,7 @@ def register_machine(self, nics, **kwargs): try: machine = self.ironic_client.node.create(**kwargs) except Exception as e: + self.log.debug("ironic machine registration failed", exc_info=True) raise OpenStackCloudException( "Error registering machine with Ironic: %s" % e.message) @@ -692,6 +702,8 @@ def register_machine(self, nics, **kwargs): node_uuid=machine.uuid) created_nics.append(nic.uuid) except Exception as e: + self.log.debug("ironic NIC registration failed", exc_info=True) + # TODO(mordred) Handle failures here for uuid in created_nics: self.ironic_client.port.delete(uuid) self.ironic_client.node.delete(machine.uuid) @@ -705,9 +717,13 @@ def unregister_machine(self, nics, uuid): self.ironic_client.port.delete( self.ironic_client.port.get_by_address(nic['mac'])) except Exception as e: + self.log.debug( + "ironic NIC unregistration failed", exc_info=True) raise OpenStackCloudException(e.message) try: self.ironic_client.node.delete(uuid) except Exception as e: + self.log.debug( + "ironic machine unregistration failed", exc_info=True) raise OpenStackCloudException( "Error unregistering machine from Ironic: %s" % e.message) From a81d18dd0e21f4c19f7a9f2cb916a03a140daaa3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 6 Jan 2015 13:08:47 -0500 Subject: [PATCH 0062/3836] Support uploading swift objects In addition to just needing to be able to upload swift objects into containers, we should be able to sanely interact with swift on general principle. Change-Id: I844ea7a26005e72ab037fe733681058944e847fa --- requirements.txt | 1 + shade/__init__.py | 132 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 130 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 610303351..c284bfde6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ python-cinderclient python-neutronclient python-troveclient python-ironicclient +python-swiftclient diff --git a/shade/__init__.py b/shade/__init__.py index 54c365f02..f43c93132 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import hashlib import logging import operator import time @@ -27,6 +28,8 @@ from novaclient.v1_1 import floating_ips import os_client_config import pbr.version +import swiftclient.client as swift_client +import swiftclient.exceptions as swift_exceptions import troveclient.client as trove_client from troveclient import exceptions as trove_exceptions @@ -34,6 +37,8 @@ from shade import meta __version__ = pbr.version.VersionInfo('shade').version_string() +OBJECT_MD5_KEY = 'x-shade-md5' +OBJECT_SHA256_KEY = 'x-shade-sha256' class OpenStackCloudException(Exception): @@ -51,11 +56,12 @@ def openstack_clouds(config=None): for f in config.get_all_clouds()] -def openstack_cloud(**kwargs): +def openstack_cloud(debug=False, **kwargs): cloud_config = os_client_config.OpenStackConfig().get_one_cloud( **kwargs) return OpenStackCloud( - cloud_config.name, cloud_config.region, **cloud_config.config) + cloud_config.name, cloud_config.region, + debug=debug, **cloud_config.config) def operator_cloud(**kwargs): @@ -102,6 +108,8 @@ def __init__(self, cloud, region='', self._image_cache = image_cache self._flavor_cache = flavor_cache self._volume_cache = volume_cache + self._container_cache = dict() + self._file_hash_cache = dict() self.debug = debug @@ -111,9 +119,13 @@ def __init__(self, cloud, region='', self._keystone_client = None self._cinder_client = None self._trove_client = None + self._swift_client = None self.log = logging.getLogger('shade') - self.log.setLevel(logging.INFO) + log_level = logging.INFO + if self.debug: + log_level = logging.DEBUG + self.log.setLevel(log_level) self.log.addHandler(logging.StreamHandler()) def get_service_type(self, service): @@ -224,6 +236,19 @@ def glance_client(self): raise OpenStackCloudException("Error connecting to glance") return self._glance_client + @property + def swift_client(self): + if self._swift_client is None: + token = self.keystone_client.auth_token + endpoint = self.get_endpoint( + service_type=self.get_service_type('object-store')) + self._swift_client = swift_client.Connection( + preauthurl=endpoint, + preauthtoken=token, + os_options=dict(region_name=self.region_name), + ) + return self._swift_client + @property def cinder_client(self): @@ -640,6 +665,107 @@ def delete_volume(self, name_or_id, wait=False, timeout=180): raise OpenStackCloudTimeout( "Timed out waiting for server to get deleted.") + def get_container(self, name, skip_cache=False): + if skip_cache or name not in self._container_cache: + try: + container = self.swift_client.head_container(name) + self._container_cache[name] = container + except swift_exceptions.ClientException as e: + if e.http_status == 404: + return None + self.log.debug("swift container fetch failed", exc_info=True) + raise OpenStackCloudException( + "Container fetch failed: %s (%s/%s)" % ( + e.http_reason, e.http_host, e.http_path)) + return self._container_cache[name] + + def create_container(self, name): + container = self.get_container(name) + if container: + return container + try: + self.swift_client.put_container(name) + return self.get_container(name, skip_cache=True) + except swift_exceptions.ClientException as e: + self.log.debug("swift container create failed", exc_info=True) + raise OpenStackCloudException( + "Container creation failed: %s (%s/%s)" % ( + e.http_reason, e.http_host, e.http_path)) + + def _get_file_hashes(self, filename): + if filename not in self._file_hash_cache: + md5 = hashlib.md5() + sha256 = hashlib.sha256() + with open(filename, 'rb') as file_obj: + for chunk in iter(lambda: file_obj.read(8192), b''): + md5.update(chunk) + sha256.update(chunk) + self._file_hash_cache[filename] = dict( + md5=md5.digest(), sha256=sha256.digest) + return (self._file_hash_cache[filename]['md5'], + self._file_hash_cache[filename]['sha256']) + + def _is_object_stale( + self, container, name, filename, file_md5=None, file_sha256=None): + + metadata = self.get_object_metadata(container, name) + if not metadata: + self.log.debug( + "swift stale check, no object: {container}/{name}".format( + container=container, name=name)) + return True + + if file_md5 is None or file_sha256 is None: + (file_md5, file_sha256) = self._get_file_hashes(filename) + + if metadata.get(OBJECT_MD5_KEY, '') != file_md5: + self.log.debug( + "swift md5 mismatch: {filename}!={container}/{name}".format( + filename=filename, container=container, name=name)) + return True + if metadata.get(OBJECT_SHA256_KEY, '') != file_sha256: + self.log.debug( + "swift sha256 mismatch: {filename}!={container}/{name}".format( + filename=filename, container=container, name=name)) + return True + + self.log.debug( + "swift object up to date: {container}/{name}".format( + container=container, name=name)) + return False + + def create_object( + self, container, name, filename=None, + md5=None, sha256=None, **headers): + if not filename: + filename = name + + if self._is_object_stale(container, name, filename, md5, sha256): + + self.create_container(container) + + with open(filename, 'r') as fileobj: + self.log.debug( + "swift uploading {filename} to {container}/{name}".format( + filename=filename, container=container, name=name)) + self.swift_client.put_object(container, name, contents=fileobj) + + (md5, sha256) = self._get_file_hashes(filename) + headers[OBJECT_MD5_KEY] = md5 + headers[OBJECT_SHA256_KEY] = sha256 + self.swift_client.post_object(container, name, headers=headers) + + def get_object_metadata(self, container, name): + try: + return self.swift_client.head_object(container, name) + except swift_exceptions.ClientException as e: + if e.http_status == 404: + return None + self.log.debug("swift metadata fetch failed", exc_info=True) + raise OpenStackCloudException( + "Object metadata fetch failed: %s (%s/%s)" % ( + e.http_reason, e.http_host, e.http_path)) + class OperatorCloud(OpenStackCloud): From abc21aaf7656147fa58c49ebccff91a3b8aaf3aa Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 7 Jan 2015 10:08:06 -0500 Subject: [PATCH 0063/3836] Refactor glance version call into method In order to switch behavior between glance v1 and glance v2 image uploading, it needs to be possible to get the version in other places other than just client initialization. Change-Id: I1c7bcc9948c6ff7de5d4e14ce31b26e83328d5b8 --- shade/__init__.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index f43c93132..822a77f26 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -115,6 +115,7 @@ def __init__(self, cloud, region='', self._nova_client = None self._glance_client = None + self._glance_endpoint = None self._ironic_client = None self._keystone_client = None self._cinder_client = None @@ -204,11 +205,12 @@ def keystone_client(self): "Error authenticating to the keystone: %s " % e.message) return self._keystone_client - def _get_glance_api_version(self, endpoint): + def _get_glance_api_version(self): if 'image' in self.api_versions: return self.api_versions['image'] # Yay. We get to guess ... # Get rid of trailing '/' if present + endpoint = self._get_glance_endpoint() if endpoint.endswith('/'): endpoint = endpoint[:-1] url_bits = endpoint.split('/') @@ -216,13 +218,18 @@ def _get_glance_api_version(self, endpoint): return url_bits[-1][1] return '1' # Who knows? Let's just try 1 ... + def _get_glance_endpoint(self): + if self._glance_endpoint is None: + self._glance_endpoint = self.get_endpoint( + service_type=self.get_service_type('image')) + return self._glance_endpoint + @property def glance_client(self): if self._glance_client is None: token = self.keystone_client.auth_token - endpoint = self.get_endpoint( - service_type=self.get_service_type('image')) - glance_api_version = self._get_glance_api_version(endpoint) + endpoint = self._get_glance_endpoint() + glance_api_version = self._get_glance_api_version() try: self._glance_client = glanceclient.Client( glance_api_version, endpoint, token=token, From 9c9de96e963824530e5c7a827465d51e2bf1ab1a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 7 Jan 2015 11:37:24 -0500 Subject: [PATCH 0064/3836] Add image upload support There are at least 2 known mechanisms in the wild for uploading images to public glance endpoints that are completely different. Based on which version of the API we're running switch between them seamlessly. Note that the wait parameter isn't going to do anything for v1 because it's a sync call - maybe we should throw an error if someone tries to leave if set to false? Change-Id: Ifccf8aaa6cd7793a1b827914e7da4b2b82248544 --- shade/__init__.py | 75 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index 822a77f26..d5247391e 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -39,6 +39,8 @@ __version__ = pbr.version.VersionInfo('shade').version_string() OBJECT_MD5_KEY = 'x-shade-md5' OBJECT_SHA256_KEY = 'x-shade-sha256' +IMAGE_MD5_KEY = 'org.openstack.shade.md5' +IMAGE_SHA256_KEY = 'org.openstack.shade.sha256' class OpenStackCloudException(Exception): @@ -412,7 +414,7 @@ def list_images(self): self._image_cache = self._get_images_from_cloud() return self._image_cache - def get_image_name(self, image_id, exclude): + def get_image_name(self, image_id, exclude=None): image = self.get_image(image_id, exclude) if image: return image.id @@ -435,6 +437,77 @@ def get_image(self, name_or_id, exclude=None): raise OpenStackCloudException( "Error finding image from %s" % name_or_id) + def create_image( + self, name, filename, container='images', + md5=None, sha256=None, + disk_format=None, container_format=None, + wait=False, timeout=3600, **kwargs): + if not md5 or not sha256: + (md5, sha256) = self._get_file_hashes(filename) + current_image = self.get_image(name) + if (current_image and current_image.get(IMAGE_MD5_KEY, '') == md5 + and current_image.get(IMAGE_SHA256_KEY, '') == sha256): + self.log.debug( + "image {name} exists and is up to date".format(name=name)) + return + kwargs[IMAGE_MD5_KEY] = md5 + kwargs[IMAGE_SHA256_KEY] = sha256 + # This makes me want to die inside + if self._get_glance_api_version() == '2': + return self._upload_image_v2( + name, filename, container, + current_image=current_image, + wait=wait, timeout=timeout, **kwargs) + else: + return self._upload_image_v1(name, filename, md5=md5) + + def _upload_image_v1( + self, name, filename, + disk_format=None, container_format=None, + **image_properties): + image = self.glance_client.images.create( + name=name, is_public=False, disk_format=disk_format, + container_format=container_format, **image_properties) + image.update(data=open(filename, 'rb')) + return image.id + + def _upload_image_v2( + self, name, filename, container, current_image=None, + wait=True, timeout=None, **image_properties): + self.create_object( + container, name, filename, + md5=image_properties['md5'], sha256=image_properties['sha256']) + if not current_image: + current_image = self.get_image(name) + # TODO(mordred): Can we do something similar to what nodepool does + # using glance properties to not delete then upload but instead make a + # new "good" image and then mark the old one as "bad" + # self.glance_client.images.delete(current_image) + image_properties['name'] = name + task = self.glance_client.tasks.create( + type='import', input=dict( + import_from='{container}/{name}'.format( + container=container, name=name), + image_properties=image_properties)) + if wait: + if timeout: + expire = time.time() + timeout + while timeout is None or time.time() < expire: + status = self.glance_client.tasks.get(task.id) + + if status.status == 'success': + return status.result['image_id'] + if status.status == 'failure': + raise OpenStackCloudException( + "Image creation failed: {message}".format( + message=status.message)) + time.sleep(10) + + raise OpenStackCloudTimeout( + "Timeout waiting for the image to import.") + else: + return None + def _get_volumes_from_cloud(self): try: return self.cinder_client.volumes.list() From 37cf9b8597b132ec0437fff357f02edd9aef741a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 12 Jan 2015 12:59:07 -0800 Subject: [PATCH 0065/3836] Don't include deleted images by default Since this is an end-user consumption library, don't put images that are invalid in the list of images when the user asks for images. An API call option is provided to disable this filtering. Co-Authored-By: David Shrewsbury Change-Id: I331b2d14199eab72f108b939ac4fbe0363565814 --- shade/__init__.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index d5247391e..dc72c586f 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -393,25 +393,35 @@ def create_keypair(self, name, public_key): def delete_keypair(self, name): return self.nova_client.keypairs.delete(name) - def _get_images_from_cloud(self): + def _get_images_from_cloud(self, filter_deleted): # First, try to actually get images from glance, it's more efficient images = dict() try: # This can fail both because we don't have glanceclient installed # and because the cloud may not expose the glance API publically - for image in self.glance_client.images.list(): - images[image.id] = image + image_list = self.glance_client.images.list() except (OpenStackCloudException, glanceclient.exc.HTTPInternalServerError): # We didn't have glance, let's try nova # If this doesn't work - we just let the exception propagate - for image in self.nova_client.images.list(): + image_list = self.nova_client.images.list() + for image in image_list: + # The cloud might return DELETED for invalid images. + # While that's cute and all, that's an implementation detail. + if not filter_deleted: + images[image.id] = image + elif image.status != 'DELETED': images[image.id] = image return images - def list_images(self): + def list_images(self, filter_deleted=True): + """Get available glance images. + + :param filter_deleted: Control whether deleted images are returned. + :returns: A dictionary of glance images indexed by image UUID. + """ if self._image_cache is None: - self._image_cache = self._get_images_from_cloud() + self._image_cache = self._get_images_from_cloud(filter_deleted) return self._image_cache def get_image_name(self, image_id, exclude=None): From 5342d25cace4b8c37c1c3b4328e77dbdd1e7342e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 12 Jan 2015 14:12:54 -0800 Subject: [PATCH 0066/3836] Remove positional args to create_server There's no good compelling reason to have them, and it just makes other places ugly and harder to use. Change-Id: I0c137c55d7f5b76f343d09f26abe926b2c44b147 --- shade/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index dc72c586f..107737aba 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -682,10 +682,9 @@ def add_ips_to_server(self, server, auto_ip=True, ips=None, ip_pool=None): "Error in getting info from instance: %s " % e.message) return server - def create_server(self, bootargs, bootkwargs, - auto_ip=True, ips=None, ip_pool=None, + def create_server(self, auto_ip=True, ips=None, ip_pool=None, root_volume=None, terminate_volume=False, - wait=False, timeout=180): + wait=False, timeout=180, **bootkwargs): if root_volume: if terminate_volume: @@ -698,7 +697,7 @@ def create_server(self, bootargs, bootkwargs, bootkwargs['block_device_mapping']['vda'] = volume_id try: - server = self.nova_client.servers.create(*bootargs, **bootkwargs) + server = self.nova_client.servers.create(**bootkwargs) server = self.nova_client.servers.get(server.id) except Exception as e: self.log.debug("nova instance create failed", exc_info=True) From ab117cfebac692343bf5929bc5d52ab3093b7e0d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 12 Jan 2015 17:09:43 -0800 Subject: [PATCH 0067/3836] Pull in improvements from nodepool As part of starting to hack on using shade in nodepool, a few inconsistencies with the compute stack in shade became clear ... fix them up. Change-Id: If3b1301fce8c2c7592258de2381dacfa082f8e49 --- shade/__init__.py | 30 ++++++++++++++++-------------- shade/meta.py | 8 +++++--- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 107737aba..1d081ee39 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -556,24 +556,26 @@ def volume_exists(self, name_or_id): return self.get_volume( name_or_id, cache=False, error=False) is not None - def get_server_by_id(self, server_id): - for server in self.nova_client.servers.list(): - if server.id == server_id: - return server - return None - - def get_server_by_name(self, server_name): - for server in self.nova_client.servers.list(): - if server.name == server_name: - return server - return None - - def get_server_id(self, server_name): - server = self.get_server_by_name(server_name) + def get_server_id(self, name_or_id): + server = self.get_server(name_or_id) if server: return server.id return None + def _get_server_ip(self, server, **kwargs): + addrs = meta.find_nova_addresses(server.addresses, *kwargs) + if not addrs: + return None + return addrs[0] + + def get_server_private_ip(self, server): + return self._get_server_ip( + server, ext_tag='fixed', key_name='private') + + def get_server_public_ip(self, server): + return self._get_server_ip( + server, ext_tag='floating', key_name='public') + def get_server(self, name_or_id): for server in self.list_servers(): if name_or_id in (server.name, server.id): diff --git a/shade/meta.py b/shade/meta.py index db3354b06..c0baa0078 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -13,16 +13,18 @@ # limitations under the License. -def find_nova_addresses(addresses, ext_tag, key_name=None): +def find_nova_addresses(addresses, ext_tag=None, key_name=None, version=4): ret = [] for (k, v) in addresses.iteritems(): if key_name and k == key_name: - ret.extend([addrs['addr'] for addrs in v]) + ret.extend([addrs['addr'] for addrs in v + if addrs['version'] == version]) else: for interface_spec in v: if ('OS-EXT-IPS:type' in interface_spec - and interface_spec['OS-EXT-IPS:type'] == ext_tag): + and interface_spec['OS-EXT-IPS:type'] == ext_tag + and interface_spec['version'] == version): ret.append(interface_spec['addr']) return ret From c22531d54bff23ebfd995a9a7795b0f86d33d7d7 Mon Sep 17 00:00:00 2001 From: Adam Gandelman Date: Thu, 15 Jan 2015 18:28:18 -0800 Subject: [PATCH 0068/3836] Adds a method to get security group Adds a method to get security group by name or id Change-Id: Ifaecf75490f45e636f00b4e7b1c02fb6af09e50e --- shade/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 1d081ee39..cc1eab08e 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -587,6 +587,12 @@ def get_server_meta(self, server): groups = meta.get_groups_from_server(self, server, server_vars) return dict(server_vars=server_vars, groups=groups) + def get_security_group(self, name_or_id): + for secgroup in self.nova_client.security_groups.list(): + if name_or_id in (secgroup.name, secgroup.id): + return secgroup + return None + def add_ip_from_pool(self, server, pools): # instantiate FloatingIPManager object From 70b58cff5c43f0c66168b51fc2584af6b9dec955 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 26 Jan 2015 16:45:18 -0500 Subject: [PATCH 0069/3836] Fix obj_to_dict type filtering Commit 88f5f2661ee2ba31056251b4cdf91949fca0a031 fixed the python 3 unit tests, but broke obj_to_dict() because client classes were no longer being filtered out (like nova's ServerManager class). Trying to remove these by using inspect.isclass() does not work as it does not recognize these types as classes (perhaps b/c they are being serialized?). This change reverts to the old style behaviour, but uses the six class to fix python3 compatibility. Change-Id: Ief7bba30d983f67cd28d886cd1d3dfd2a45e957d --- requirements.txt | 1 + shade/meta.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c284bfde6..a4fac76d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ pbr>=0.5.21,<1.0 os-client-config +six python-novaclient python-keystoneclient>=0.11.0 diff --git a/shade/meta.py b/shade/meta.py index c0baa0078..992247ec3 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -13,6 +13,11 @@ # limitations under the License. +import six + +NON_CALLABLES = (six.string_types, bool, dict, int, list, type(None)) + + def find_nova_addresses(addresses, ext_tag=None, key_name=None, version=4): ret = [] @@ -133,6 +138,6 @@ def obj_to_dict(obj): instance = {} for key in dir(obj): value = getattr(obj, key) - if not callable(value) and not key.startswith('_'): + if isinstance(value, NON_CALLABLES) and not key.startswith('_'): instance[key] = value return instance From 569197d763aec46f8fe9838e554082069c2e3cdc Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 27 Jan 2015 14:21:18 -0500 Subject: [PATCH 0070/3836] Provide Rackspace service_name override Rackspace puts two compute services in their catalog. This means that keystone session code cannot find the right one without a name match override. Change-Id: I1bc06b97261341ad01bf84ebf5a12294cd0d383c --- os_client_config/vendors.py | 1 + 1 file changed, 1 insertion(+) diff --git a/os_client_config/vendors.py b/os_client_config/vendors.py index eca437676..cab3676a1 100644 --- a/os_client_config/vendors.py +++ b/os_client_config/vendors.py @@ -26,6 +26,7 @@ auth_url='https://identity.api.rackspacecloud.com/v2.0/', ), database_service_type='rax:database', + compute_service_name='cloudServersOpenStack', image_api_version='2', ) ) From 2bbcb630144975d032ecc312177a11266c2ac57b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 27 Jan 2015 16:51:53 -0500 Subject: [PATCH 0071/3836] Remove runtime depend on pbr os-client-config is not using pbr during runtime, so the runtime requirement for it was a lie. Change-Id: I3ed57ec5c2b0fdf4060ef17d6df1fa801cfa14cd --- requirements.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 9cf3f8ae0..71026aa67 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,4 @@ # 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>=0.6,!=0.7,<1.0 - PyYAML From 4bb72f8401264d17b5b1ff9a7aa0e53235fa3c33 Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Wed, 21 Jan 2015 15:52:37 -0800 Subject: [PATCH 0072/3836] Use the "iterate timeout" idiom from nodepool When waiting for something to happen inside of a timeout loop, use a generator to simplify and standardize the pattern. Change-Id: I1a49bd4ecde98bc6bbb20d64d43447f825776807 --- shade/__init__.py | 53 +++++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index cc1eab08e..7309fd6ec 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -77,6 +77,24 @@ def _get_service_values(kwargs, service_key): for k in kwargs.keys() if k.endswith(service_key)} +def _iterate_timeout(timeout, message): + """Iterate and raise an exception on timeout. + + This is a generator that will continually yield and sleep for 2 + seconds, and if the timeout is reached, will raise an exception + with . + + """ + + start = time.time() + count = 0 + while (timeout is None) or (time.time() < start + timeout): + count += 1 + yield count + time.sleep(2) + raise OpenStackCloudTimeout(message) + + class OpenStackCloud(object): def __init__(self, cloud, region='', @@ -500,9 +518,9 @@ def _upload_image_v2( container=container, name=name), image_properties=image_properties)) if wait: - if timeout: - expire = time.time() + timeout - while timeout is None or time.time() < expire: + for count in _iterate_timeout( + timeout, + "Timeout waiting for the image to import."): status = self.glance_client.tasks.get(task.id) if status.status == 'success': @@ -511,10 +529,6 @@ def _upload_image_v2( raise OpenStackCloudException( "Image creation failed: {message}".format( message=status.message)) - time.sleep(10) - - raise OpenStackCloudTimeout( - "Timeout waiting for the image to import.") else: return None @@ -715,8 +729,9 @@ def create_server(self, auto_ip=True, ips=None, ip_pool=None, raise OpenStackCloudException( "Error in creating the server.") if wait: - expire = time.time() + timeout - while time.time() < expire: + for count in _iterate_timeout( + timeout, + "Timeout waiting for the server to come up."): try: server = self.nova_client.servers.get(server.id) except Exception: @@ -729,10 +744,6 @@ def create_server(self, auto_ip=True, ips=None, ip_pool=None, if server.status == 'ERROR': raise OpenStackCloudException( "Error in creating the server, please check logs") - time.sleep(2) - - raise OpenStackCloudException( - "Timeout waiting for the server to come up.") return server def delete_server(self, name, wait=False, timeout=180): @@ -742,25 +753,21 @@ def delete_server(self, name, wait=False, timeout=180): self.nova_client.servers.delete(server.pop()) if not wait: return - expire = time.time() + timeout - while time.time() < expire: + for count in _iterate_timeout( + timeout, + "Timed out waiting for server to get deleted."): server = self.nova_client.servers.list(True, {'name': name}) if not server: return - time.sleep(5) - raise OpenStackCloudTimeout( - "Timed out waiting for server to get deleted.") def delete_volume(self, name_or_id, wait=False, timeout=180): volume = self.get_volume(name_or_id) - expire = time.time() + timeout - while time.time() < expire: + for count in _iterate_timeout( + timeout, + "Timed out waiting for server to get deleted."): if self.volume_exists(volume.id, cache=False): return - time.sleep(5) - raise OpenStackCloudTimeout( - "Timed out waiting for server to get deleted.") def get_container(self, name, skip_cache=False): if skip_cache or name not in self._container_cache: From bea5220e5a22953f349df48fa202283f5bdf0273 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 2 Feb 2015 10:12:58 -0500 Subject: [PATCH 0073/3836] Make get_image return None To make it more consistent with the interface elsewhere, make get_image return None when an Image is not found instead of throwing an exception. Change-Id: Id420abfeb32fa5d643d78ecec72db41be05093aa --- shade/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 7309fd6ec..2d063368a 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -462,8 +462,7 @@ def get_image(self, name_or_id, exclude=None): if (name_or_id in image.name and ( not exclude or exclude not in image.name)): return image - raise OpenStackCloudException( - "Error finding image from %s" % name_or_id) + return None def create_image( self, name, filename, container='images', From 4d1fae6c792660f4c9140b7195152815af2030b0 Mon Sep 17 00:00:00 2001 From: Adam Gandelman Date: Mon, 2 Feb 2015 10:26:08 -0500 Subject: [PATCH 0074/3836] Add get_flavor method Simple utility method to get a flavor. Change-Id: I4246e91de97710e0966780cfcb085abff56c7dc0 --- shade/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 7309fd6ec..cbf579055 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -376,6 +376,12 @@ def get_flavor_name(self, flavor_id): return flavor.name return None + def get_flavor(self, name_or_id): + for id, flavor in self.flavor_cache.items(): + if name_or_id in (id, flavor.name): + return flavor + return None + def get_flavor_by_ram(self, ram, include=None): for flavor in sorted( self.flavor_cache.values(), From 8f1ca24bb83f108e302e3511b9825839e3cc9538 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 20 Jan 2015 20:15:26 -0500 Subject: [PATCH 0075/3836] Add consistent methods for returning dicts There are enough places that want dicts that we should just have API calls for it. Several of the places want to do slightly smarter things when doing that too. Co-Authored-By: David Shrewsbury Change-Id: I08245a89e5b497b9fe50af7d3538976b1ac82257 --- shade/__init__.py | 35 +++++++++++++++++++++++++---------- shade/meta.py | 36 ++++++++++++++++++++++++------------ 2 files changed, 49 insertions(+), 22 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 2d063368a..51b0e64cf 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -402,9 +402,17 @@ def get_endpoint(self, service_type): def list_servers(self): return self.nova_client.servers.list() + def list_server_dicts(self): + return [self.get_openstack_vars(server) + for server in self.list_servers()] + def list_keypairs(self): return self.nova_client.keypairs.list() + def list_keypair_dicts(self): + return [meta.obj_to_dict(keypair) + for keypair in self.list_keypairs()] + def create_keypair(self, name, public_key): return self.nova_client.keypairs.create(name, public_key) @@ -464,6 +472,12 @@ def get_image(self, name_or_id, exclude=None): return image return None + def get_image_dict(self, name_or_id, exclude=None): + image = self.get_image(name_or_id, exclude) + if not image: + return image + return meta.obj_to_dict(image) + def create_image( self, name, filename, container='images', md5=None, sha256=None, @@ -575,19 +589,11 @@ def get_server_id(self, name_or_id): return server.id return None - def _get_server_ip(self, server, **kwargs): - addrs = meta.find_nova_addresses(server.addresses, *kwargs) - if not addrs: - return None - return addrs[0] - def get_server_private_ip(self, server): - return self._get_server_ip( - server, ext_tag='fixed', key_name='private') + return meta.get_server_private_ip(server) def get_server_public_ip(self, server): - return self._get_server_ip( - server, ext_tag='floating', key_name='public') + return meta.get_server_public_ip(server) def get_server(self, name_or_id): for server in self.list_servers(): @@ -595,6 +601,12 @@ def get_server(self, name_or_id): return server return None + def get_server_dict(self, name_or_id): + server = self.get_server(name_or_id) + if not server: + return server + return self.get_openstack_vars(server) + def get_server_meta(self, server): server_vars = meta.get_hostvars_from_server(self, server) groups = meta.get_groups_from_server(self, server, server_vars) @@ -606,6 +618,9 @@ def get_security_group(self, name_or_id): return secgroup return None + def get_openstack_vars(self, server): + return meta.get_hostvars_from_server(self, server) + def add_ip_from_pool(self, server, pools): # instantiate FloatingIPManager object diff --git a/shade/meta.py b/shade/meta.py index 992247ec3..6f752fcb4 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -34,6 +34,21 @@ def find_nova_addresses(addresses, ext_tag=None, key_name=None, version=4): return ret +def get_server_ip(server, **kwargs): + addrs = find_nova_addresses(server.addresses, **kwargs) + if not addrs: + return None + return addrs[0] + + +def get_server_private_ip(server): + return get_server_ip(server, ext_tag='fixed', key_name='private') + + +def get_server_public_ip(server): + return get_server_ip(server, ext_tag='floating', key_name='public') + + def get_groups_from_server(cloud, server, server_vars): groups = [] @@ -77,20 +92,17 @@ def get_groups_from_server(cloud, server, server_vars): def get_hostvars_from_server(cloud, server, mounts=None): - server_vars = dict() + server_vars = obj_to_dict(server) + # Fist, add an IP address - if (cloud.private): - interface_ips = find_nova_addresses( - getattr(server, 'addresses'), 'fixed', 'private') + server_vars['public_v4'] = get_server_public_ip(server) + server_vars['private_v4'] = get_server_private_ip(server) + if cloud.private: + interface_ip = server_vars['private_v4'] else: - interface_ips = find_nova_addresses( - getattr(server, 'addresses'), 'floating', 'public') - - if interface_ips: - # TODO(mordred): I want this to be richer, "first" is not best - server_vars['interface_ip'] = interface_ips[0] - - server_vars.update(obj_to_dict(server)) + interface_ip = server_vars['public_v4'] + if interface_ip: + server_vars['interface_ip'] = interface_ip server_vars['region'] = cloud.region server_vars['cloud'] = cloud.name From d3daf5b896c23e113a4dfb324c24af31e276a0df Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 20 Jan 2015 21:57:21 -0800 Subject: [PATCH 0076/3836] Port to use keystone sessions and auth plugins In order to properly support the input parameters we have, we need to go all they way in to full support for keystone plugins and session objects. Amazingly, this is not terribly hard to do. Change-Id: Ia051f7c42aa3c0793cdadd5585e07c3220d5779d --- requirements.txt | 2 +- shade/__init__.py | 174 +++++++++++++++++++--------------------------- 2 files changed, 73 insertions(+), 103 deletions(-) diff --git a/requirements.txt b/requirements.txt index a4fac76d3..66116f3ba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ pbr>=0.5.21,<1.0 -os-client-config +os-client-config>=0.4.0 six python-novaclient diff --git a/shade/__init__.py b/shade/__init__.py index 51b0e64cf..a64018a1c 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -17,21 +17,19 @@ import operator import time -from cinderclient import exceptions as cinder_exceptions from cinderclient.v1 import client as cinder_client import glanceclient from ironicclient import client as ironic_client from ironicclient import exceptions as ironic_exceptions -from keystoneclient import client as keystone_client -from novaclient import exceptions as nova_exceptions -from novaclient.v1_1 import client as nova_client +from keystoneclient import auth as ksc_auth +from keystoneclient import session as ksc_session +from novaclient import client as nova_client from novaclient.v1_1 import floating_ips import os_client_config import pbr.version import swiftclient.client as swift_client import swiftclient.exceptions as swift_exceptions import troveclient.client as trove_client -from troveclient import exceptions as trove_exceptions from shade import meta @@ -98,33 +96,38 @@ def _iterate_timeout(timeout, message): class OpenStackCloud(object): def __init__(self, cloud, region='', + auth_plugin='password', + insecure=False, verify=None, cacert=None, cert=None, key=None, image_cache=None, flavor_cache=None, volume_cache=None, debug=False, **kwargs): self.name = cloud self.region = region - self.username = kwargs['username'] - self.password = kwargs['password'] - self.project_name = kwargs['project_name'] - self.auth_url = kwargs['auth_url'] + self.auth_plugin = auth_plugin + self.auth = kwargs.get('auth') self.region_name = kwargs.get('region_name', region) - self.auth_token = kwargs.get('auth_token', None) + self._auth_token = kwargs.get('auth_token', None) self.service_types = _get_service_values(kwargs, 'service_type') self.endpoints = _get_service_values(kwargs, 'endpoint') self.api_versions = _get_service_values(kwargs, 'api_version') - self.user_domain_name = kwargs.get('user_domain_name', None) - self.project_domain_name = kwargs.get('project_domain_name', None) - - self.insecure = kwargs.get('insecure', False) self.endpoint_type = kwargs.get('endpoint_type', 'publicURL') - self.cert = kwargs.get('cert', None) - self.cacert = kwargs.get('cacert', None) self.private = kwargs.get('private', False) + if verify is None: + if insecure: + verify = False + else: + verify = cacert or True + self.verify = verify + + if cert and key: + cert = (cert, key) + self.cert = cert + self._image_cache = image_cache self._flavor_cache = flavor_cache self._volume_cache = volume_cache @@ -133,11 +136,12 @@ def __init__(self, cloud, region='', self.debug = debug + self._keystone_session = None + self._nova_client = None self._glance_client = None self._glance_endpoint = None self._ironic_client = None - self._keystone_client = None self._cinder_client = None self._trove_client = None self._swift_client = None @@ -152,42 +156,22 @@ def __init__(self, cloud, region='', def get_service_type(self, service): return self.service_types.get(service, service) + def _get_nova_api_version(self): + return self.api_versions['compute'] + @property def nova_client(self): if self._nova_client is None: - kwargs = dict( - region_name=self.region_name, - service_type=self.get_service_type('compute'), - insecure=self.insecure, - ) - # Try to use keystone directly first, for potential token reuse - try: - kwargs['auth_token'] = self.keystone_client.auth_token - kwargs['bypass_url'] = self.get_endpoint( - self.get_service_type('compute')) - except OpenStackCloudException: - pass # Make the connection - self._nova_client = nova_client.Client( - self.username, - self.password, - self.project_name, - self.auth_url, - **kwargs - ) - - self._nova_client.authenticate() try: - self._nova_client.authenticate() - except nova_exceptions.Unauthorized as e: - self.log.debug("nova Unauthorized", exc_info=True) - raise OpenStackCloudException( - "Invalid OpenStack Nova credentials: %s" % e.message) - except nova_exceptions.AuthorizationFailure as e: - self.log.debug("nova AuthorizationFailure", exc_info=True) - raise OpenStackCloudException( - "Unable to authorize user: %s" % e.message) + self._nova_client = nova_client.Client( + self._get_nova_api_version(), + session=self.keystone_session, + region_name=self.region_name) + except Exception: + self.log.debug("Couldn't construct nova object", exc_info=True) + raise if self._nova_client is None: raise OpenStackCloudException( @@ -197,33 +181,46 @@ def nova_client(self): return self._nova_client @property - def keystone_client(self): - if self._keystone_client is None: + def keystone_session(self): + if self._keystone_session is None: # keystoneclient does crazy things with logging that are # none of them interesting keystone_logging = logging.getLogger('keystoneclient') keystone_logging.addHandler(logging.NullHandler()) try: - if self.auth_token: - self._keystone_client = keystone_client.Client( - endpoint=self.auth_url, - token=self.auth_token) - else: - self._keystone_client = keystone_client.Client( - username=self.username, - password=self.password, - project_name=self.project_name, - region_name=self.region_name, - auth_url=self.auth_url, - user_domain_name=self.user_domain_name, - project_domain_name=self.project_domain_name) - self._keystone_client.authenticate() + auth_plugin = ksc_auth.get_plugin_class(self.auth_plugin) + except Exception as e: + self.log.debug("keystone auth plugin failure", exc_info=True) + raise OpenStackCloudException( + "Could not find auth plugin: {plugin}".format( + plugin=self.auth_plugin)) + try: + keystone_auth = auth_plugin(**self.auth) + except Exception as e: + self.log.debug( + "keystone couldn't construct plugin", exc_info=True) + raise OpenStackCloudException( + "Error constructing auth plugin: {plugin}".format( + plugin=self.auth_plugin)) + + try: + self._keystone_session = ksc_session.Session( + auth=keystone_auth, + verify=self.verify, + cert=self.cert) + self._auth_token = self._keystone_session.get_token() except Exception as e: self.log.debug("keystone unknown issue", exc_info=True) raise OpenStackCloudException( "Error authenticating to the keystone: %s " % e.message) - return self._keystone_client + return self._keystone_session + + @property + def auth_token(self): + if not self._auth_token: + self._auth_token = self.keystone_session.get_token() + return self._auth_token def _get_glance_api_version(self): if 'image' in self.api_versions: @@ -247,13 +244,13 @@ def _get_glance_endpoint(self): @property def glance_client(self): if self._glance_client is None: - token = self.keystone_client.auth_token + token = self.auth_token endpoint = self._get_glance_endpoint() glance_api_version = self._get_glance_api_version() try: self._glance_client = glanceclient.Client( glance_api_version, endpoint, token=token, - session=self.keystone_client.session) + session=self.keystone_session) except Exception as e: self.log.debug("glance unknown issue", exc_info=True) raise OpenStackCloudException( @@ -266,7 +263,7 @@ def glance_client(self): @property def swift_client(self): if self._swift_client is None: - token = self.keystone_client.auth_token + token = self.auth_token endpoint = self.get_endpoint( service_type=self.get_service_type('object-store')) self._swift_client = swift_client.Connection( @@ -282,23 +279,8 @@ def cinder_client(self): if self._cinder_client is None: # Make the connection self._cinder_client = cinder_client.Client( - self.username, - self.password, - self.project_name, - self.auth_url, - region_name=self.region_name, - ) - - try: - self._cinder_client.authenticate() - except cinder_exceptions.Unauthorized as e: - self.log.debug("cinder Unauthorized", exc_info=True) - raise OpenStackCloudException( - "Invalid OpenStack Cinder credentials.: %s" % e.message) - except cinder_exceptions.AuthorizationFailure as e: - self.log.debug("cinder AuthorizationFailure", exc_info=True) - raise OpenStackCloudException( - "Unable to authorize user: %s" % e.message) + session=self.keystone_session, + region_name=self.region_name) if self._cinder_client is None: raise OpenStackCloudException( @@ -330,25 +312,11 @@ def trove_client(self): # is one self._trove_client = trove_client.Client( trove_api_version, - self.username, - self.password, - self.project_name, - self.auth_url, + session=self.keystone_session, region_name=self.region_name, service_type=self.get_service_type('database'), ) - try: - self._trove_client.authenticate() - except trove_exceptions.Unauthorized as e: - self.log.debug("trove Unauthorized", exc_info=True) - raise OpenStackCloudException( - "Invalid OpenStack Trove credentials.: %s" % e.message) - except trove_exceptions.AuthorizationFailure as e: - self.log.debug("trove AuthorizationFailure", exc_info=True) - raise OpenStackCloudException( - "Unable to authorize user: %s" % e.message) - if self._trove_client is None: raise OpenStackCloudException( "Failed to instantiate Trove client." @@ -391,8 +359,10 @@ def get_endpoint(self, service_type): if service_type in self.endpoints: return self.endpoints[service_type] try: - endpoint = self.keystone_client.service_catalog.url_for( - service_type=service_type, endpoint_type=self.endpoint_type) + endpoint = self.keystone_session.get_endpoint( + service_type=service_type, + interface=self.endpoint_type, + region_name=self.region_name) except Exception as e: self.log.debug("keystone cannot get endpoint", exc_info=True) raise OpenStackCloudException( @@ -892,7 +862,7 @@ def ironic_client(self): if self._ironic_client is None: ironic_logging = logging.getLogger('ironicclient') ironic_logging.addHandler(logging.NullHandler()) - token = self.keystone_client.auth_token + token = self.auth_token endpoint = self.get_endpoint(service_type='baremetal') try: self._ironic_client = ironic_client.Client( From fcc108626bca5feefe276fc3f5a94c1740e3b8f2 Mon Sep 17 00:00:00 2001 From: Adam Gandelman Date: Thu, 15 Jan 2015 15:02:12 -0800 Subject: [PATCH 0077/3836] Create a neutron client Creates a neutron_client property using token and endpoint from keystone session. Change-Id: I07199a272969c53610347f3ff05e60b0b3b8d317 --- requirements.txt | 2 +- shade/__init__.py | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 66116f3ba..10fd9c9cd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ python-novaclient python-keystoneclient>=0.11.0 python-glanceclient python-cinderclient -python-neutronclient +python-neutronclient>=2.3.10 python-troveclient python-ironicclient python-swiftclient diff --git a/shade/__init__.py b/shade/__init__.py index a64018a1c..d08b77523 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -25,6 +25,7 @@ from keystoneclient import session as ksc_session from novaclient import client as nova_client from novaclient.v1_1 import floating_ips +from neutronclient.v2_0 import client as neutron_client import os_client_config import pbr.version import swiftclient.client as swift_client @@ -138,13 +139,14 @@ def __init__(self, cloud, region='', self._keystone_session = None - self._nova_client = None + self._cinder_client = None self._glance_client = None self._glance_endpoint = None self._ironic_client = None - self._cinder_client = None - self._trove_client = None + self._neutron_client = None + self._nova_client = None self._swift_client = None + self._trove_client = None self.log = logging.getLogger('shade') log_level = logging.INFO @@ -324,6 +326,15 @@ def trove_client(self): return self._trove_client + @property + def neutron_client(self): + if self._neutron_client is None: + self._neutron_client = neutron_client.Client( + token=self.auth_token, + session=self.keystone_session, + region_name=self.region_name) + return self._neutron_client + def get_name(self): return self.name From 106e5561c9200a45d54740137d692cd8f026f986 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 27 Jan 2015 14:22:49 -0500 Subject: [PATCH 0078/3836] Pass service_name to nova_client constructor Rackspace breaks without a service name passed to it, because they put two compute services in their service catalog. If we pass a service name, then keystone session can match the right thing. Change-Id: I300017c664559fbeabde54160e6d4867abc4ce4b --- shade/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index d08b77523..c06255793 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -112,6 +112,7 @@ def __init__(self, cloud, region='', self._auth_token = kwargs.get('auth_token', None) self.service_types = _get_service_values(kwargs, 'service_type') + self.service_names = _get_service_values(kwargs, 'service_name') self.endpoints = _get_service_values(kwargs, 'endpoint') self.api_versions = _get_service_values(kwargs, 'api_version') @@ -158,6 +159,9 @@ def __init__(self, cloud, region='', def get_service_type(self, service): return self.service_types.get(service, service) + def get_service_name(self, service): + return self.service_names.get(service, None) + def _get_nova_api_version(self): return self.api_versions['compute'] @@ -170,6 +174,7 @@ def nova_client(self): self._nova_client = nova_client.Client( self._get_nova_api_version(), session=self.keystone_session, + service_name=self.get_service_name('compute'), region_name=self.region_name) except Exception: self.log.debug("Couldn't construct nova object", exc_info=True) From eeaf4730292859e591df5a7d248885435f30e116 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 4 Feb 2015 13:12:29 -0500 Subject: [PATCH 0079/3836] Get auth token lazily This addresses one of the comments from change ID Ia051f7c42aa3c0793cdadd5585e07c3220d5779d where it was suggested to get the auth token when we actually need it, and not before. Change-Id: I3fe485796a6057280933254e28ea6718e3a24d3d --- shade/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index dde0b7764..4e1f323ec 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -211,7 +211,6 @@ def keystone_session(self): auth=keystone_auth, verify=self.verify, cert=self.cert) - self._auth_token = self._keystone_session.get_token() except Exception as e: self.log.debug("keystone unknown issue", exc_info=True) raise OpenStackCloudException( From 1e572aaff584b8934ff19f36101d0ecc94b79ddd Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 4 Feb 2015 14:20:41 -0500 Subject: [PATCH 0080/3836] Add support for creating/deleting volumes This adds a new create_volume() method, and changes the existing delete_volume() method that didn't actually delete anything. Change-Id: Ic02a54bcf953c5cb124865f5e4c33cc36863aacb --- shade/__init__.py | 79 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 9 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 4e1f323ec..7aa68f498 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -531,6 +531,76 @@ def _upload_image_v2( else: return None + def create_volume(self, wait=True, timeout=None, **volkwargs): + """Create a volume. + + :param wait: If true, waits for volume to be created. + :param timeout: Seconds to wait for volume creation. None is forever. + :param volkwargs: Keyword arguments as expected for cinder client. + + :returns: The created volume object. + + :raises: OpenStackCloudTimeout if wait time exceeded. + :raises: OpenStackCloudException on operation error. + """ + + cinder = self.cinder_client + + try: + volume = cinder.volumes.create(**volkwargs) + except Exception as e: + self.log.debug("Volume creation failed", exc_info=True) + raise OpenStackCloudException( + "Error in creating volume: %s" % e.message) + + if volume.status == 'error': + raise OpenStackCloudException("Error in creating volume") + + if wait: + vol_id = volume.id + for count in _iterate_timeout( + timeout, + "Timeout waiting for the volume to be available."): + volume = self.get_volume(vol_id, cache=False, error=False) + + if not volume: + continue + + if volume.status == 'available': + return volume + + if volume.status == 'error': + raise OpenStackCloudException( + "Error in creating volume, please check logs") + + def delete_volume(self, name_or_id=None, wait=True, timeout=None): + """Delete a volume. + + :param name_or_id: Name or unique ID of the volume. + :param wait: If true, waits for volume to be deleted. + :param timeout: Seconds to wait for volume deletion. None is forever. + + :raises: OpenStackCloudTimeout if wait time exceeded. + :raises: OpenStackCloudException on operation error. + """ + + cinder = self.cinder_client + volume = self.get_volume(name_or_id, cache=False) + + try: + cinder.volumes.delete(volume.id) + except Exception as e: + self.log.debug("Volume deletion failed", exc_info=True) + raise OpenStackCloudException( + "Error in deleting volume: %s" % e.message) + + if wait: + for count in _iterate_timeout( + timeout, + "Timeout waiting for the volume to be deleted."): + if not self.volume_exists(volume.id): + return + def _get_volumes_from_cloud(self): try: return self.cinder_client.volumes.list() @@ -760,15 +830,6 @@ def delete_server(self, name, wait=False, timeout=180): if not server: return - def delete_volume(self, name_or_id, wait=False, timeout=180): - volume = self.get_volume(name_or_id) - - for count in _iterate_timeout( - timeout, - "Timed out waiting for server to get deleted."): - if self.volume_exists(volume.id, cache=False): - return - def get_container(self, name, skip_cache=False): if skip_cache or name not in self._container_cache: try: From 0cba0b8b1263b89124ee67e8a9c07c3240afc5e5 Mon Sep 17 00:00:00 2001 From: Adam Gandelman Date: Mon, 26 Jan 2015 17:31:26 -0800 Subject: [PATCH 0081/3836] Adds get_network() and list_networks function Adds new utility functions. Change-Id: I923b6cef5d671c5eb6bac2b1d197c034ae99f0fd --- shade/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 4e1f323ec..09ba192f1 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -405,6 +405,15 @@ def create_keypair(self, name, public_key): def delete_keypair(self, name): return self.nova_client.keypairs.delete(name) + def list_networks(self): + return self.neutron_client.list_networks()['networks'] + + def get_network(self, name_or_id): + for network in self.list_networks(): + if name_or_id in (network['id'], network['name']): + return network + return None + def _get_images_from_cloud(self, filter_deleted): # First, try to actually get images from glance, it's more efficient images = dict() From 37069cb7c3b9d3102aac78f7bc10d7a32f9a62b5 Mon Sep 17 00:00:00 2001 From: Adam Gandelman Date: Tue, 27 Jan 2015 16:47:35 -0800 Subject: [PATCH 0082/3836] Adds some more swift operations This adds some more operations for swift, mostly around the ability to set a container as public private, both during creation and after. Change-Id: I1dbc02a61bd8642963bb6dafe62e150b023679b7 --- shade/__init__.py | 60 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index 4e1f323ec..fcb61e2a0 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -42,6 +42,12 @@ IMAGE_SHA256_KEY = 'org.openstack.shade.sha256' +OBJECT_CONTAINER_ACLS = { + 'public': ".r:*,.rlistings", + 'private': '', +} + + class OpenStackCloudException(Exception): pass @@ -783,12 +789,14 @@ def get_container(self, name, skip_cache=False): e.http_reason, e.http_host, e.http_path)) return self._container_cache[name] - def create_container(self, name): + def create_container(self, name, public=False): container = self.get_container(name) if container: return container try: self.swift_client.put_container(name) + if public: + self.set_container_access(name, 'public') return self.get_container(name, skip_cache=True) except swift_exceptions.ClientException as e: self.log.debug("swift container create failed", exc_info=True) @@ -796,6 +804,46 @@ def create_container(self, name): "Container creation failed: %s (%s/%s)" % ( e.http_reason, e.http_host, e.http_path)) + def delete_container(self, name): + try: + self.swift_client.delete_container(name) + except swift_exceptions.ClientException as e: + if e.http_status == 404: + return + self.log.debug("swift container delete failed", exc_info=True) + raise OpenStackCloudException( + "Container deletion failed: %s (%s/%s)" % ( + e.http_reason, e.http_host, e.http_path)) + + def update_container(self, name, headers): + try: + self.swift_client.post_container(name, headers) + except swift_exceptions.ClientException as e: + self.log.debug("swift container update failed", exc_info=True) + raise OpenStackCloudException( + "Container update failed: %s (%s/%s)" % ( + e.http_reason, e.http_host, e.http_path)) + + def set_container_access(self, name, access): + if access not in OBJECT_CONTAINER_ACLS: + raise OpenStackCloudException( + "Invalid container access specified: %s. Must be one of %s" + % (access, list(OBJECT_CONTAINER_ACLS.keys()))) + header = {'x-container-read': OBJECT_CONTAINER_ACLS[access]} + self.update_container(name, header) + + def get_container_access(self, name): + container = self.get_container(name, skip_cache=True) + if not container: + raise OpenStackCloudException("Container not found: %s" % name) + acl = container.get('x-container-read', '') + try: + return [p for p, a in OBJECT_CONTAINER_ACLS.items() + if acl == a].pop() + except IndexError: + raise OpenStackCloudException( + "Could not determine container access for ACL: %s." % acl) + def _get_file_hashes(self, filename): if filename not in self._file_hash_cache: md5 = hashlib.md5() @@ -859,6 +907,16 @@ def create_object( headers[OBJECT_SHA256_KEY] = sha256 self.swift_client.post_object(container, name, headers=headers) + def delete_object(self, container, name): + if not self.get_object_metadata(container, name): + return + try: + self.swift_client.delete_object(container, name) + except swift_exceptions.ClientException as e: + raise OpenStackCloudException( + "Object deletion failed: %s (%s/%s)" % ( + e.http_reason, e.http_host, e.http_path)) + def get_object_metadata(self, container, name): try: return self.swift_client.head_object(container, name) From 2067982c3d9654103ccebb0c2043c63047bb28f1 Mon Sep 17 00:00:00 2001 From: Adam Gandelman Date: Thu, 5 Feb 2015 13:41:32 +0000 Subject: [PATCH 0083/3836] Fix broken object hashing This fixes two issues around checksum accounting for objects. It properly calls the correct hexdigest() functions when calculating hashes locally. Also, it uses the correct swift headers that allow these to do be stored on in the object's metadata. Change-Id: I55ea5aff0a4712e7b9f13e912008c3c98576414a --- shade/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index fcb61e2a0..765e18bd2 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -36,8 +36,8 @@ from shade import meta __version__ = pbr.version.VersionInfo('shade').version_string() -OBJECT_MD5_KEY = 'x-shade-md5' -OBJECT_SHA256_KEY = 'x-shade-sha256' +OBJECT_MD5_KEY = 'x-object-meta-x-shade-md5' +OBJECT_SHA256_KEY = 'x-object-meta-x-shade-sha256' IMAGE_MD5_KEY = 'org.openstack.shade.md5' IMAGE_SHA256_KEY = 'org.openstack.shade.sha256' @@ -853,7 +853,7 @@ def _get_file_hashes(self, filename): md5.update(chunk) sha256.update(chunk) self._file_hash_cache[filename] = dict( - md5=md5.digest(), sha256=sha256.digest) + md5=md5.hexdigest(), sha256=sha256.hexdigest()) return (self._file_hash_cache[filename]['md5'], self._file_hash_cache[filename]['sha256']) From 01d7728504a77bdf03d6e4aa98edd66d55fa13f0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 11 Feb 2015 08:32:48 -0500 Subject: [PATCH 0084/3836] Make sure we're deep-copying the auth dict Having a defaults dict that has an empty dict and then does a bunch of updates means you have ONE instance of a dict that all of the other instances have references to. Change-Id: Id008f7ec98ff7b392553cebca5a5b301330e67a3 --- os_client_config/config.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index bfc030efa..95dfaf7e1 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -59,7 +59,10 @@ def _auth_update(old_dict, new_dict): """Like dict.update, except handling the nested dict called auth.""" for (k, v) in new_dict.items(): if k == 'auth': - old_dict[k].update(v) + if k in old_dict: + old_dict[k].update(v) + else: + old_dict[k] = v.copy() else: old_dict[k] = v return old_dict @@ -73,7 +76,6 @@ def __init__(self, config_files=None, vendor_files=None): defaults = dict( auth_plugin='password', - auth=dict(), compute_api_version='1.1', ) self.defaults = _get_os_environ(defaults) @@ -273,6 +275,8 @@ def get_one_cloud(self, cloud=None, validate=True, args['region_name'] = self._get_region(cloud) config = self._get_base_cloud_config(cloud) + if 'auth' not in config: + config['auth'] = dict() # Can't just do update, because None values take over for (key, val) in args.iteritems(): From 89d1e4d3c4cbbe67d60c5b96cfb8ae6762737fd2 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 11 Feb 2015 09:30:22 -0500 Subject: [PATCH 0085/3836] Don't return the auth dict inside the loop It turns out that when you're looping over a set of params to move them into an internal dict, returning inside the loop results in processing exactly one of them. This is not, it turns out, what you wanted. Change-Id: If1bf0c22b758e7238846b08991f4b0d25c841583 --- os_client_config/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 95dfaf7e1..72c24f974 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -251,7 +251,7 @@ def _validate_auth(self, config): if winning_value: config['auth'][p_opt.name.replace('-', '_')] = winning_value - return config + return config def get_one_cloud(self, cloud=None, validate=True, argparse=None, **kwargs): From 56b75154fb4858fad5f1d7a910d5d7fbf785ef67 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Sun, 8 Feb 2015 13:06:49 -0500 Subject: [PATCH 0086/3836] Allow region_name to be None When using the shade library with ansible playbooks and classical environment variables for authentication, it is possible for a region_name value to be passed in with the value of None. When the value is set to and preserved as None, the logic later on in the method will fail to create a config key/value entry for region_name which is required by code later on in the method. Modified the region_name check to not only check for the presence of a missing region_name value, but to check to see if that value is set to None. This allows the value to be reset preventing the method from failing on the return call due to a missing key. Change-Id: Id8a3edf53ac751f0c6ee4d71405a926ba90c0602 --- os_client_config/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 72c24f974..ae19621ef 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -271,7 +271,7 @@ def get_one_cloud(self, cloud=None, validate=True, args = self._fix_args(kwargs, argparse=argparse) - if 'region_name' not in args: + if 'region_name' not in args or args['region_name'] is None: args['region_name'] = self._get_region(cloud) config = self._get_base_cloud_config(cloud) From 7385528671ea61617d7027fc33897ea850e7364c Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Wed, 11 Feb 2015 13:09:21 -0500 Subject: [PATCH 0087/3836] Prefer dest value when option is depricated Changed logic that re-assigns depricated key names while preserving their values, to prefer the value stored in the dest key if it exists, instead of attempting to generate the new key name. Change-Id: Ibe961688cdb6bd4c9b2dbd27b08c722c3c741586 --- os_client_config/config.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index ae19621ef..f8adc047d 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -249,7 +249,13 @@ def _validate_auth(self, config): config['auth'].pop(opt, None) if winning_value: - config['auth'][p_opt.name.replace('-', '_')] = 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 return config From 7e4927e7b30a26cd440218acfdd798376fb3c1af Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Sun, 8 Feb 2015 17:20:08 -0500 Subject: [PATCH 0088/3836] Make is_object_stale() a public method The _is_object_stale() is meant to be public, so remove the leading underscore. Change-Id: Ib4f3be43e563ce3109cceae14ffe69e1976fc4c8 --- shade/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 15970fa57..b17bfd15b 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -932,7 +932,7 @@ def _get_file_hashes(self, filename): return (self._file_hash_cache[filename]['md5'], self._file_hash_cache[filename]['sha256']) - def _is_object_stale( + def is_object_stale( self, container, name, filename, file_md5=None, file_sha256=None): metadata = self.get_object_metadata(container, name) @@ -967,7 +967,7 @@ def create_object( if not filename: filename = name - if self._is_object_stale(container, name, filename, md5, sha256): + if self.is_object_stale(container, name, filename, md5, sha256): self.create_container(container) From 91bc405359d601c2eb0b9d6967af464705932b04 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 11 Feb 2015 13:28:30 -0800 Subject: [PATCH 0089/3836] Remove unnecessary container creation While putting an object, if you specify a container that does not exist, the container will be created for you. Change-Id: Icfb9eed1e661360e6a7610c99b3d68e439e36dc3 --- shade/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index b17bfd15b..a1b7357e4 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -968,9 +968,6 @@ def create_object( filename = name if self.is_object_stale(container, name, filename, md5, sha256): - - self.create_container(container) - with open(filename, 'r') as fileobj: self.log.debug( "swift uploading {filename} to {container}/{name}".format( From 23ab18efdf3fba068e0ebd7d32d3ad3a2813bcff Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 12 Feb 2015 08:29:12 -0500 Subject: [PATCH 0090/3836] Add service_catalog property Add a property for getting the service catalog. Although most of the time a user should not need to grab this directly, it can be invaluable in debugging situations - and is a public method because there is an ansible module that wants to directly expose it. Change-Id: If54cb94fc9a80f1dabe90b46ed2f62d8fcb8460f --- shade/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 15970fa57..e8daf1e03 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -228,6 +228,11 @@ def keystone_session(self): "Error authenticating to the keystone: %s " % e.message) return self._keystone_session + @property + def service_catalog(self): + return self.keystone_session.auth.get_access( + self.keystone_session).service_catalog.get_data() + @property def auth_token(self): if not self._auth_token: From 2bad9dacbbd98705a7e10a520dc90162a275ce6e Mon Sep 17 00:00:00 2001 From: Yolanda Robla Date: Wed, 18 Feb 2015 17:20:22 +0100 Subject: [PATCH 0091/3836] Don't compare images when image is None Image cache can set images to None under certain workflow, and then we are accessing this None entry as it was an object. Add an extra check for that. Change-Id: Ia9b737c81ed8133ff5a6ba26f98b7f55fca095de --- shade/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index 6c9a37696..be84057ad 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -478,7 +478,8 @@ def get_image(self, name_or_id, exclude=None): for (image_id, image) in self.list_images().items(): if image_id == name_or_id: return image - if (name_or_id in image.name and ( + if (image is not None and + name_or_id in image.name and ( not exclude or exclude not in image.name)): return image return None From 3d165aee7890ca822f33e118b9fa9d1f6a0109a7 Mon Sep 17 00:00:00 2001 From: Yolanda Robla Date: Wed, 18 Feb 2015 12:44:20 +0100 Subject: [PATCH 0092/3836] Add hasExtension method to check cloud capabilities This method will be using an extension_cache that will query for the extensions to the cloud, then check if the extension asked is present on the list or not. Change-Id: I6f018d03e6314579435b042faf5d24b1f0473685 --- shade/__init__.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 6c9a37696..a04787eb4 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -25,6 +25,7 @@ from keystoneclient import session as ksc_session from novaclient import client as nova_client from novaclient.v1_1 import floating_ips +from novaclient import exceptions as nova_exceptions from neutronclient.v2_0 import client as neutron_client import os_client_config import pbr.version @@ -138,6 +139,7 @@ def __init__(self, cloud, region='', self._image_cache = image_cache self._flavor_cache = flavor_cache + self._extension_cache = None self._volume_cache = volume_cache self._container_cache = dict() self._file_hash_cache = dict() @@ -421,6 +423,23 @@ def create_keypair(self, name, public_key): def delete_keypair(self, name): return self.nova_client.keypairs.delete(name) + @property + def extension_cache(self): + if not self._extension_cache: + self._extension_cache = set() + + try: + resp, body = self.nova_client.client.get('/extensions') + if resp.status_code == 200: + for x in body['extensions']: + self._extension_cache.add(x['alias']) + except nova_exceptions.NotFound: + pass + return self._extension_cache + + def has_extension(self, extension_name): + return extension_name in self.extension_cache + def list_networks(self): return self.neutron_client.list_networks()['networks'] From 084df16c9b9d36571081bb07b336fb902ebc4dad Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Thu, 19 Feb 2015 18:40:39 +0100 Subject: [PATCH 0093/3836] Revamp README file The README used only python-*client objects, thus defeating the purpose of shade, which is about putting a facade for all those interfaces to simplify things. README now shows you can use shade simplified interface and also access the underlying python-*client API objects. Change-Id: I0c638beab615177fc6ce3704c5443a3394a87ed0 --- README.rst | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 822e16955..4f8766073 100644 --- a/README.rst +++ b/README.rst @@ -26,18 +26,22 @@ Sometimes an example is nice. from shade import * import time + # Initialize cloud + # Cloud configs are read with os-client-config cloud = openstack_cloud('mordred') - nova = cloud.nova_client - print nova.servers.list() - s = nova.servers.list()[0] + # OpenStackCloud object has an interface exposing OpenStack services methods + print cloud.list_servers() + s = cloud.list_servers()[0] + # But you can also access the underlying python-*client objects cinder = cloud.cinder_client volumes = cinder.volumes.list() - print volumes volume_id = [v for v in volumes if v.status == 'available'][0].id - nova.volumes.create_server_volume(s.id, volume_id, None) + nova = cloud.nova_client + print nova.volumes.create_server_volume(s.id, volume_id, None) attachments = [] + print volume_id while not attachments: print "Waiting for attach to finish" time.sleep(1) From 804b2f3a4e6d0f096c868cce10003bfcdee8fddd Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 15 Feb 2015 17:00:51 -0500 Subject: [PATCH 0094/3836] Don't try to add an IP if there is one On Rackspace there is an automatic IP. The code before inappropriately attempted to add a floating IP anyway. Change-Id: I63fd7ec0abc06a24b1869aa35cb1084cb228e4ca --- shade/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 6c9a37696..ea5caf1dd 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -784,6 +784,8 @@ def add_ips_to_server(self, server, auto_ip=True, ips=None, ip_pool=None): elif ips: self.add_ip_list(server, ips) elif auto_ip: + if self.get_server_public_ip(server): + return server self.add_auto_ip(server) else: return server From 76c77d1a6a53ee678f56e5f5be1b615212599e71 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 16 Feb 2015 16:47:12 -0500 Subject: [PATCH 0095/3836] Return extra information for debugging on failures When spinning up a server, it's helpful to know things about why things went wrong. Change-Id: I9d78f3359801378f51a19ced25b58e17bd9e629d --- shade/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index ea5caf1dd..79f7b695f 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -840,7 +840,9 @@ def create_server(self, auto_ip=True, ips=None, ip_pool=None, if server.status == 'ERROR': raise OpenStackCloudException( - "Error in creating the server, please check logs") + "Error in creating the server", + extra_data=dict( + server=meta.obj_to_dict(server))) return server def delete_server(self, name, wait=False, timeout=180): From 7d39596bf60b1f5395bcc715175fcf84075ba921 Mon Sep 17 00:00:00 2001 From: Yolanda Robla Date: Sun, 22 Feb 2015 12:48:44 +0100 Subject: [PATCH 0096/3836] Add a method to create image snapshots from nova. It encapsulate the novaclient.servers.create_image so we do not need to access directly to the python client library. Change-Id: I214993210cc3b46037c482dda815450446223850 --- shade/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 6c9a37696..a5f2123a4 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -489,6 +489,13 @@ def get_image_dict(self, name_or_id, exclude=None): return image return meta.obj_to_dict(image) + def create_image_snapshot(self, name, **metadata): + image = self.nova_client.servers.create_image( + name, **metadata) + if image: + return meta.obj_to_dict(image) + return None + def create_image( self, name, filename, container='images', md5=None, sha256=None, From 47435af70b41adc9b5a387c7436263b0eef8ff29 Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Tue, 24 Feb 2015 15:44:30 -0800 Subject: [PATCH 0097/3836] Add unit tests for meta module Just adding basic unit tests for getting server addresses. In the process of adding these we found a few py34 failures. Change-Id: Ifd99e3ac71c0939cc48cf2e952719fd591077e32 --- shade/meta.py | 4 ++-- shade/tests/test_meta.py | 48 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 shade/tests/test_meta.py diff --git a/shade/meta.py b/shade/meta.py index 6f752fcb4..edb83ad8f 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -21,7 +21,7 @@ def find_nova_addresses(addresses, ext_tag=None, key_name=None, version=4): ret = [] - for (k, v) in addresses.iteritems(): + for (k, v) in iter(addresses.items()): if key_name and k == key_name: ret.extend([addrs['addr'] for addrs in v if addrs['version'] == version]) @@ -79,7 +79,7 @@ def get_groups_from_server(cloud, server, server_vars): if 'name' in server_vars[key]: groups.append('%s-%s' % (key, server_vars[key]['name'])) - for key, value in server.metadata.iteritems(): + for key, value in iter(server.metadata.items()): groups.append('meta-%s_%s' % (key, value)) az = server_vars.get('az', None) diff --git a/shade/tests/test_meta.py b/shade/tests/test_meta.py new file mode 100644 index 000000000..31710550e --- /dev/null +++ b/shade/tests/test_meta.py @@ -0,0 +1,48 @@ +# Copyright (c) 2015 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. + +import testtools + +from shade import meta + + +class TestMeta(testtools.TestCase): + def test_find_nova_addresses_key_name(self): + # Note 198.51.100.0/24 is TEST-NET-2 from rfc5737 + addrs = {'public': [{'addr': '198.51.100.1', 'version': 4}], + 'private': [{'addr': '192.0.2.5', 'version': 4}]} + self.assertEqual( + ['198.51.100.1'], + meta.find_nova_addresses(addrs, key_name='public')) + self.assertEqual([], meta.find_nova_addresses(addrs, key_name='foo')) + + def test_find_nova_addresses_ext_tag(self): + addrs = {'public': [{'OS-EXT-IPS:type': 'fixed', + 'addr': '198.51.100.2', + 'version': 4}]} + self.assertEqual( + ['198.51.100.2'], meta.find_nova_addresses(addrs, ext_tag='fixed')) + self.assertEqual([], meta.find_nova_addresses(addrs, ext_tag='foo')) + + def test_get_server_ip(self): + class Server(object): + addresses = {'private': [{'OS-EXT-IPS:type': 'fixed', + 'addr': '198.51.100.3', + 'version': 4}], + 'public': [{'OS-EXT-IPS:type': 'floating', + 'addr': '192.0.2.99', + 'version': 4}]} + srv = Server() + self.assertEqual('198.51.100.3', meta.get_server_private_ip(srv)) + self.assertEqual('192.0.2.99', meta.get_server_public_ip(srv)) From 637b5b264e4dc21148233a9afbbd7a3ea54e1c38 Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Wed, 25 Feb 2015 05:09:19 -0800 Subject: [PATCH 0098/3836] Add unit test for meta.get_groups_from_server Change-Id: I01389ff20ecc37c7368702f6e3c6e904edde40fe --- shade/tests/test_meta.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/shade/tests/test_meta.py b/shade/tests/test_meta.py index 31710550e..142211cb3 100644 --- a/shade/tests/test_meta.py +++ b/shade/tests/test_meta.py @@ -46,3 +46,27 @@ class Server(object): srv = Server() self.assertEqual('198.51.100.3', meta.get_server_private_ip(srv)) self.assertEqual('192.0.2.99', meta.get_server_public_ip(srv)) + + def test_get_groups_from_server(self): + class Cloud(object): + region = 'test-region' + name = 'test-name' + + class Server(object): + id = 'test-id-0' + metadata = {'group': 'test-group'} + + server_vars = {'flavor': 'test-flavor', + 'image': 'test-image', + 'az': 'test-az'} + self.assertEqual( + ['test-name', + 'test-region', + 'test-name_test-region', + 'test-group', + 'instance-test-id-0', + 'meta-group_test-group', + 'test-az', + 'test-region_test-az', + 'test-name_test-region_test-az'], + meta.get_groups_from_server(Cloud(), Server(), server_vars)) From cec22cd55ec4dfeb562d3801e58fc26f10ddd61b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 19 Feb 2015 12:03:06 -0800 Subject: [PATCH 0099/3836] Import from v2 instead of v1_1 novaclient has moved its API, because that provides tons of value. Change import lines to match. Change-Id: I299696fde803d619ca9408091c1ab8aa8f13ad8f --- requirements.txt | 2 +- shade/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 10fd9c9cd..3392d6a3c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ pbr>=0.5.21,<1.0 os-client-config>=0.4.0 six -python-novaclient +python-novaclient>=2.21.0 python-keystoneclient>=0.11.0 python-glanceclient python-cinderclient diff --git a/shade/__init__.py b/shade/__init__.py index 9ad114301..3fcbe5c90 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -24,8 +24,8 @@ from keystoneclient import auth as ksc_auth from keystoneclient import session as ksc_session from novaclient import client as nova_client -from novaclient.v1_1 import floating_ips from novaclient import exceptions as nova_exceptions +from novaclient.v2 import floating_ips from neutronclient.v2_0 import client as neutron_client import os_client_config import pbr.version From 93ce50c040dddf1ad91a929223342fd0cf459adb Mon Sep 17 00:00:00 2001 From: Yolanda Robla Date: Fri, 20 Feb 2015 14:13:30 +0100 Subject: [PATCH 0100/3836] Do not allow to pass *-cache on init Caches will be useful to query for facts once and then store them. But allowing to pass a list of known facts could lead in shade consuming facts that aren't reflecting the cloud behaviour, so removing that possibility. Change-Id: Ib3ea94ec3d014f4fe5d4480d779056bc5722de13 --- shade/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 3fcbe5c90..84107fe6f 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -106,7 +106,6 @@ class OpenStackCloud(object): def __init__(self, cloud, region='', auth_plugin='password', insecure=False, verify=None, cacert=None, cert=None, key=None, - image_cache=None, flavor_cache=None, volume_cache=None, debug=False, **kwargs): self.name = cloud @@ -137,10 +136,10 @@ def __init__(self, cloud, region='', cert = (cert, key) self.cert = cert - self._image_cache = image_cache - self._flavor_cache = flavor_cache self._extension_cache = None - self._volume_cache = volume_cache + self._flavor_cache = None + self._image_cache = None + self._volume_cache = None self._container_cache = dict() self._file_hash_cache = dict() From 6d31937a5a67c635ce84dbc6766fac0dd91cf59f Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 6 Feb 2015 11:24:18 -0500 Subject: [PATCH 0101/3836] Add support for volume attach/detach Two new API methods for volume attaching and detaching. Change-Id: I25bca4d53fd0d77ee1c8a5344e6d6c3163955ac4 --- shade/__init__.py | 135 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 84107fe6f..5880f8b22 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -690,6 +690,141 @@ def volume_exists(self, name_or_id): return self.get_volume( name_or_id, cache=False, error=False) is not None + def get_volume_attach_device(self, volume, server_id): + """Return the device name a volume is attached to for a server. + + This can also be used to verify if a volume is attached to + a particular server. + + :param volume: Volume object + :param server_id: ID of server to check + + :returns: Device name if attached, None if volume is not attached. + """ + for attach in volume.attachments: + if server_id == attach['server_id']: + return attach['device'] + return None + + def detach_volume(self, server, volume, wait=True, timeout=None): + """Detach a volume from a server. + + :param server: The server object to detach from. + :param volume: The volume object to detach. + :param wait: If true, waits for volume to be detached. + :param timeout: Seconds to wait for volume detachment. None is forever. + + :raises: OpenStackCloudTimeout if wait time exceeded. + :raises: OpenStackCloudException on operation error. + """ + nova = self.nova_client + + dev = self.get_volume_attach_device(volume, server.id) + if not dev: + raise OpenStackCloudException( + "Volume %s is not attached to server %s" + % (volume.id, server.id) + ) + + try: + nova.volumes.delete_server_volume(server.id, volume.id) + except Exception as e: + self.log.debug("nova volume detach failed", exc_info=True) + raise OpenStackCloudException( + "Error detaching volume %s from server %s: %s" % + (volume.id, server.id, e) + ) + + if wait: + for count in _iterate_timeout( + timeout, + "Timeout waiting for volume %s to detach." % volume.id): + try: + vol = self.get_volume(volume.id, cache=False) + except Exception: + self.log.debug( + "Error getting volume info %s" % volume.id, + exc_info=True) + continue + + if vol.status == 'available': + return + + if vol.status == 'error': + raise OpenStackCloudException( + "Error in detaching volume %s" % volume.id + ) + + def attach_volume(self, server, volume, device=None, + wait=True, timeout=None): + """Attach a volume to a server. + + This will attach a volume, described by the passed in volume + object (as returned by get_volume()), to the server described by + the passed in server object (as returned by get_server()) on the + named device on the server. + + If the volume is already attached to the server, or generally not + available, then an exception is raised. To re-attach to a server, + but under a different device, the user must detach it first. + + :param server: The server object to attach to. + :param volume: The volume object to attach. + :param device: The device name where the volume will attach. + :param wait: If true, waits for volume to be attached. + :param timeout: Seconds to wait for volume attachment. None is forever. + + :raises: OpenStackCloudTimeout if wait time exceeded. + :raises: OpenStackCloudException on operation error. + """ + nova = self.nova_client + + dev = self.get_volume_attach_device(volume, server.id) + if dev: + raise OpenStackCloudException( + "Volume %s already attached to server %s on device %s" + % (volume.id, server.id, dev) + ) + + if volume.status != 'available': + raise OpenStackCloudException( + "Volume %s is not available. Status is '%s'" + % (volume.id, volume.status) + ) + + try: + nova.volumes.create_server_volume(server.id, volume.id, device) + except Exception as e: + self.log.debug( + "nova volume attach of %s failed" % volume.id, exc_info=True) + raise OpenStackCloudException( + "Error attaching volume %s to server %s: %s" % + (volume.id, server.id, e) + ) + + if wait: + for count in _iterate_timeout( + timeout, + "Timeout waiting for volume %s to attach." % volume.id): + try: + vol = self.get_volume(volume.id, cache=False) + except Exception: + self.log.debug( + "Error getting volume info %s" % volume.id, + exc_info=True) + continue + + if self.get_volume_attach_device(vol, server.id): + return + + # TODO(Shrews) check to see if a volume can be in error status + # and also attached. If so, we should move this + # above the get_volume_attach_device call + if vol.status == 'error': + raise OpenStackCloudException( + "Error in attaching volume %s" % volume.id + ) + def get_server_id(self, name_or_id): server = self.get_server(name_or_id) if server: From 5bffc4e07124417460ba1db295c9c521057fbe04 Mon Sep 17 00:00:00 2001 From: Yolanda Robla Date: Tue, 24 Feb 2015 14:50:08 -0800 Subject: [PATCH 0102/3836] Utilize dogpile.cache for caching In the future we can make the backend more configurable which will help users build more powerful things on top of shade with better caching available to them. Shade is caching elements like images, volumes and flavors that are subject to change (flavor the less of them). Storing items in a cache makes that changes to elements, like adding or deleting, or changes of status, are not perceived by the cache. However, the cache is still needed to improve performance when several concurrent calls are done. Change-Id: If072bcdbdaa2b0a021e26b1ea2211b1dfcc75880 Co-Authored-By: Clint Byrum --- requirements.txt | 2 ++ shade/__init__.py | 33 +++++++++++++++++---------------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/requirements.txt b/requirements.txt index 3392d6a3c..46d428806 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,5 @@ python-neutronclient>=2.3.10 python-troveclient python-ironicclient python-swiftclient + +dogpile.cache>=0.5.3 diff --git a/shade/__init__.py b/shade/__init__.py index 5880f8b22..cd16fb350 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -18,6 +18,7 @@ import time from cinderclient.v1 import client as cinder_client +from dogpile import cache import glanceclient from ironicclient import client as ironic_client from ironicclient import exceptions as ironic_exceptions @@ -106,7 +107,7 @@ class OpenStackCloud(object): def __init__(self, cloud, region='', auth_plugin='password', insecure=False, verify=None, cacert=None, cert=None, key=None, - debug=False, **kwargs): + debug=False, cache_interval=None, **kwargs): self.name = cloud self.region = region @@ -136,10 +137,8 @@ def __init__(self, cloud, region='', cert = (cert, key) self.cert = cert - self._extension_cache = None - self._flavor_cache = None - self._image_cache = None - self._volume_cache = None + self._cache = cache.make_region().configure( + 'dogpile.cache.memory', expiration_time=cache_interval) self._container_cache = dict() self._file_hash_cache = dict() @@ -359,11 +358,11 @@ def get_region(self): @property def flavor_cache(self): - if not self._flavor_cache: - self._flavor_cache = { - flavor.id: flavor - for flavor in self.nova_client.flavors.list()} - return self._flavor_cache + @self._cache.cache_on_arguments() + def _flavor_cache(): + return {flavor.id: flavor for flavor in + self.nova_client.flavors.list()} + return _flavor_cache() def get_flavor_name(self, flavor_id): flavor = self.flavor_cache.get(flavor_id, None) @@ -475,9 +474,10 @@ def list_images(self, filter_deleted=True): :param filter_deleted: Control whether deleted images are returned. :returns: A dictionary of glance images indexed by image UUID. """ - if self._image_cache is None: - self._image_cache = self._get_images_from_cloud(filter_deleted) - return self._image_cache + @self._cache.cache_on_arguments() + def _list_images(): + return self._get_images_from_cloud(filter_deleted) + return _list_images() def get_image_name(self, image_id, exclude=None): image = self.get_image(image_id, exclude) @@ -659,9 +659,10 @@ def _get_volumes_from_cloud(self): return [] def list_volumes(self, cache=True): - if self._volume_cache is None or not cache: - self._volume_cache = self._get_volumes_from_cloud() - return self._volume_cache + @self._cache.cache_on_arguments() + def _list_volumes(): + return self._get_volumes_from_cloud() + return _list_volumes() def get_volumes(self, server, cache=True): volumes = [] From acfcf5562b90312f873f595154297a6edd3834a0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 15 Feb 2015 11:15:31 -0500 Subject: [PATCH 0103/3836] Make image processing work for v2 It turns out, not only is image _uploading_ different for v1 and v2, image metadata processing is completely different too. And, Rackspace disallows image properties with certain prefixes, the task checking loop can fail some times. Also, glance v2 warlock objects work differently. Change-Id: I37ac719270bde259ab1cf59763d3b949990bf497 --- shade/__init__.py | 109 ++++++++++++++++++++++++++++++++++++---------- shade/meta.py | 10 +++++ 2 files changed, 95 insertions(+), 24 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 588d1588d..f5621d814 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -19,6 +19,7 @@ from cinderclient.v1 import client as cinder_client import glanceclient +import glanceclient.exc from ironicclient import client as ironic_client from ironicclient import exceptions as ironic_exceptions from keystoneclient import auth as ksc_auth @@ -39,8 +40,8 @@ __version__ = pbr.version.VersionInfo('shade').version_string() OBJECT_MD5_KEY = 'x-object-meta-x-shade-md5' OBJECT_SHA256_KEY = 'x-object-meta-x-shade-sha256' -IMAGE_MD5_KEY = 'org.openstack.shade.md5' -IMAGE_SHA256_KEY = 'org.openstack.shade.sha256' +IMAGE_MD5_KEY = 'owner_specified.shade.md5' +IMAGE_SHA256_KEY = 'owner_specified.shade.sha256' OBJECT_CONTAINER_ACLS = { @@ -50,7 +51,9 @@ class OpenStackCloudException(Exception): - pass + def __init__(self, message, extra_data=None): + self.message = message + self.extra_data = extra_data class OpenStackCloudTimeout(OpenStackCloudException): @@ -469,6 +472,9 @@ def _get_images_from_cloud(self, filter_deleted): images[image.id] = image return images + def _reset_image_cache(self): + self._image_cache = None + def list_images(self, filter_deleted=True): """Get available glance images. @@ -506,7 +512,12 @@ def get_image_dict(self, name_or_id, exclude=None): image = self.get_image(name_or_id, exclude) if not image: return image - return meta.obj_to_dict(image) + if getattr(image, 'validate', None): + # glanceclient returns a "warlock" object if you use v2 + return meta.warlock_to_dict(image) + else: + # glanceclient returns a normal object if you use v1 + return meta.obj_to_dict(image) def create_image_snapshot(self, name, **metadata): image = self.nova_client.servers.create_image( @@ -522,65 +533,115 @@ def create_image( wait=False, timeout=3600, **kwargs): if not md5 or not sha256: (md5, sha256) = self._get_file_hashes(filename) - current_image = self.get_image(name) + current_image = self.get_image_dict(name) if (current_image and current_image.get(IMAGE_MD5_KEY, '') == md5 and current_image.get(IMAGE_SHA256_KEY, '') == sha256): self.log.debug( "image {name} exists and is up to date".format(name=name)) - return + return current_image kwargs[IMAGE_MD5_KEY] = md5 kwargs[IMAGE_SHA256_KEY] = sha256 # This makes me want to die inside - if self._get_glance_api_version() == '2': + glance_api_version = self._get_glance_api_version() + if glance_api_version == '2': return self._upload_image_v2( name, filename, container, current_image=current_image, wait=wait, timeout=timeout, **kwargs) - else: - return self._upload_image_v1(name, filename, md5=md5) + elif glance_api_version == '1': + image_kwargs = dict(properties=kwargs) + if disk_format: + image_kwargs['disk_format'] = disk_format + if container_format: + image_kwargs['container_format'] = container_format - def _upload_image_v1( - self, name, filename, - disk_format=None, container_format=None, - **image_properties): - image = self.glance_client.images.create( - name=name, is_public=False, disk_format=disk_format, - container_format=container_format, **image_properties) + return self._upload_image_v1(name, filename, **image_kwargs) + + def _upload_image_v1(self, name, filename, **image_kwargs): + image = self.glance_client.images.create(name=name, **image_kwargs) image.update(data=open(filename, 'rb')) - return image.id + return self.get_image_dict(image.id) def _upload_image_v2( self, name, filename, container, current_image=None, wait=True, timeout=None, **image_properties): self.create_object( container, name, filename, - md5=image_properties['md5'], sha256=image_properties['sha256']) + md5=image_properties.get('md5', None), + sha256=image_properties.get('sha256', None)) if not current_image: current_image = self.get_image(name) # TODO(mordred): Can we do something similar to what nodepool does # using glance properties to not delete then upload but instead make a # new "good" image and then mark the old one as "bad" # self.glance_client.images.delete(current_image) - image_properties['name'] = name task = self.glance_client.tasks.create( type='import', input=dict( import_from='{container}/{name}'.format( container=container, name=name), - image_properties=image_properties)) + image_properties=dict(name=name))) if wait: for count in _iterate_timeout( timeout, "Timeout waiting for the image to import."): - status = self.glance_client.tasks.get(task.id) + try: + status = self.glance_client.tasks.get(task.id) + except glanceclient.exc.HTTPServiceUnavailable: + # Intermittent failure - catch and try again + continue if status.status == 'success': - return status.result['image_id'] + self._reset_image_cache() + image = self.get_image(status.result['image_id']) + self.update_image_properties( + image=image, + **image_properties) + return self.get_image_dict(status.result['image_id']) if status.status == 'failure': raise OpenStackCloudException( "Image creation failed: {message}".format( - message=status.message)) + message=status.message), + extra_data=status) else: - return None + return meta.warlock_to_dict(task) + + def update_image_properties( + self, image=None, name_or_id=None, **properties): + if not image: + image = self.get_image(name_or_id) + + img_props = {} + for k, v in properties.iteritems(): + if v and k in ['ramdisk', 'kernel']: + v = self.get_image_id(v) + k = '{0}_id'.format(k) + img_props[k] = v + + # This makes me want to die inside + if self._get_glance_api_version() == '2': + return self._update_image_properties_v2(image, img_props) + else: + return self._update_image_properties_v1(image, img_props) + + def _update_image_properties_v2(self, image, properties): + img_props = {} + for k, v in properties.iteritems(): + if image.get(k, None) != v: + img_props[k] = str(v) + if not img_props: + return False + self.glance_client.images.update(image.id, **img_props) + return True + + def _update_image_properties_v1(self, image, properties): + img_props = {} + for k, v in properties.iteritems(): + if image.properties.get(k, None) != v: + img_props[k] = v + if not img_props: + return False + image.update(properties=img_props) + return True def create_volume(self, wait=True, timeout=None, **volkwargs): """Create a volume. diff --git a/shade/meta.py b/shade/meta.py index edb83ad8f..90a60d511 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -153,3 +153,13 @@ def obj_to_dict(obj): if isinstance(value, NON_CALLABLES) and not key.startswith('_'): instance[key] = value return instance + + +def warlock_to_dict(obj): + # glanceclient v2 uses warlock to construct its objects. Warlock does + # deep black magic to attribute look up to support validation things that + # means we cannot use normal obj_to_dict + obj_dict = {} + for key in obj.keys(): + obj_dict[key] = obj[key] + return obj_dict From 1d0e7fdc72a761e1062fddb503a032177322ae1b Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Thu, 26 Feb 2015 17:08:56 +0000 Subject: [PATCH 0104/3836] Reorder envlist to avoid the rm -fr .testrepository when running tox -epy34 Change-Id: Ibe34867caf97e05bc13614b3c685e8cfb0d6220a --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index f03a88529..e0ff3d6e6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 1.6 -envlist = py27,py34,pypy,pep8 +envlist = py34,py27,pypy,pep8 skipsdist = True [testenv] From 076e9bd9bece61db43137eab553c1545e6701fca Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Thu, 26 Feb 2015 16:12:14 -0800 Subject: [PATCH 0105/3836] Add basic unit test for config Adding this exposed some python3 compatibility issues with iteritems. Change-Id: Ia78bd8edd17c7d2360ad958b3de734503d400774 --- os_client_config/config.py | 4 ++-- os_client_config/tests/test_config.py | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 os_client_config/tests/test_config.py diff --git a/os_client_config/config.py b/os_client_config/config.py index f8adc047d..7c50b5de4 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -197,7 +197,7 @@ def _fix_args(self, args, argparse=None): os_args = dict() new_args = dict() - for (key, val) in args.iteritems(): + for (key, val) in iter(args.items()): key = key.replace('-', '_') if key.startswith('os'): os_args[key[3:]] = val @@ -285,7 +285,7 @@ def get_one_cloud(self, cloud=None, validate=True, config['auth'] = dict() # Can't just do update, because None values take over - for (key, val) in args.iteritems(): + for (key, val) in iter(args.items()): if val is not None: config[key] = val diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py new file mode 100644 index 000000000..2b1821eec --- /dev/null +++ b/os_client_config/tests/test_config.py @@ -0,0 +1,24 @@ +# Copyright (c) 2014 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. + +import testtools + +from os_client_config import cloud_config +from os_client_config import config + + +class TestConfig(testtools.TestCase): + def test_get_one_cloud(self): + c = config.OpenStackConfig() + self.assertIsInstance(c.get_one_cloud(), cloud_config.CloudConfig) From 9752febeb6abd7d975af53e95ef1aed7c7d4cb56 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Thu, 26 Feb 2015 13:14:17 -0500 Subject: [PATCH 0106/3836] Allow keystone validation bypass for noauth usage To enable the eventual use of the shade library for communication with ironic in situations where authentication is set to noauth, it is necessary to not attempt to validate a user's credentials. Added a check to disable authentication validation when the auth_plugin is set to '' or 'None' or None. Change-Id: I3807b4724ce5e204b5857c1dbf5325f0e3f4a78d --- os_client_config/config.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/os_client_config/config.py b/os_client_config/config.py index f8adc047d..771a1c957 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -294,6 +294,10 @@ def get_one_cloud(self, cloud=None, validate=True, if type(config[key]) is not bool: config[key] = get_boolean(config[key]) + if 'auth_plugin' in config: + if config['auth_plugin'] in ('', 'None', None): + validate = False + if validate and ksc_auth: config = self._validate_auth(config) From e483abccbb84a9dd49b266105309610ca0924fb2 Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Fri, 27 Feb 2015 11:23:16 -0800 Subject: [PATCH 0107/3836] More comprehensive unit tests for os-client-config This includes ensuring that yaml of a sane format can be loaded. Change-Id: I698b3139b7e44f000d2a413d17e79914ef542a22 --- os_client_config/tests/test_config.py | 41 +++++++++++++++++++++++++++ test-requirements.txt | 2 ++ 2 files changed, 43 insertions(+) diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 2b1821eec..47e2b9a91 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -12,13 +12,54 @@ # License for the specific language governing permissions and limitations # under the License. +import tempfile + +import extras +import fixtures import testtools +import yaml from os_client_config import cloud_config from os_client_config import config class TestConfig(testtools.TestCase): + def get_config(self): + config = { + 'clouds': { + '_test_cloud_': { + 'auth': { + 'username': 'testuser', + 'password': 'testpass', + 'project_name': 'testproject', + }, + 'region_name': 'test-region', + }, + }, + 'cache': {'max_age': 1}, + } + tdir = self.useFixture(fixtures.TempDir()) + config['cache']['path'] = tdir.path + return config + def test_get_one_cloud(self): c = config.OpenStackConfig() self.assertIsInstance(c.get_one_cloud(), cloud_config.CloudConfig) + + def test_get_one_cloud_with_config_files(self): + self.useFixture(fixtures.NestedTempfile()) + with tempfile.NamedTemporaryFile() as cloud_yaml: + cloud_yaml.write(yaml.safe_dump(self.get_config()).encode('utf-8')) + cloud_yaml.flush() + c = config.OpenStackConfig(config_files=[cloud_yaml.name]) + self.assertIsInstance(c.cloud_config, dict) + self.assertIn('cache', c.cloud_config) + self.assertIsInstance(c.cloud_config['cache'], dict) + self.assertIn('max_age', c.cloud_config['cache']) + self.assertIn('path', c.cloud_config['cache']) + cc = c.get_one_cloud('_test_cloud_') + self.assertIsInstance(cc, cloud_config.CloudConfig) + self.assertTrue(extras.safe_hasattr(cc, 'auth')) + self.assertIsInstance(cc.auth, dict) + self.assertIn('username', cc.auth) + self.assertEqual('testuser', cc.auth['username']) diff --git a/test-requirements.txt b/test-requirements.txt index d494076d9..03e01941e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,6 +5,8 @@ hacking>=0.9.2,<0.10 coverage>=3.6 +extras +fixtures>=0.3.14 discover python-subunit sphinx>=1.1.2 From 15cbdf7947d106c472181aa2ebc67a17974f54e3 Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Fri, 27 Feb 2015 11:23:16 -0800 Subject: [PATCH 0108/3836] Add more testing of vendor yaml loading Adding coverage for vendor yaml loading and refactoring some tests to make the structure for that sane. Change-Id: I7aca0fcc0b04371f9a71e71c0224897b19cb04af --- os_client_config/tests/test_config.py | 76 +++++++++++++++++++-------- 1 file changed, 54 insertions(+), 22 deletions(-) diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 47e2b9a91..b19899a5d 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -22,44 +22,76 @@ from os_client_config import cloud_config from os_client_config import config - -class TestConfig(testtools.TestCase): - def get_config(self): - config = { - 'clouds': { - '_test_cloud_': { - 'auth': { - 'username': 'testuser', - 'password': 'testpass', - 'project_name': 'testproject', - }, - 'region_name': 'test-region', - }, +VENDOR_CONF = { + 'public-clouds': { + '_test_cloud_in_our_cloud': { + 'auth': { + 'username': 'testotheruser', + 'project_name': 'testproject', }, - 'cache': {'max_age': 1}, - } - tdir = self.useFixture(fixtures.TempDir()) - config['cache']['path'] = tdir.path - return config + }, + } +} +USER_CONF = { + 'clouds': { + '_test_cloud_': { + 'cloud': '_test_cloud_in_our_cloud', + 'auth': { + 'username': 'testuser', + 'password': 'testpass', + }, + 'region_name': 'test-region', + }, + '_test_cloud_no_vendor': { + 'cloud': '_test_non_existant_cloud', + 'auth': { + 'username': 'testuser', + 'password': 'testpass', + 'project_name': 'testproject', + }, + 'region_name': 'test-region', + }, + }, + 'cache': {'max_age': 1}, +} + +def _write_yaml(obj): + # Assume NestedTempfile so we don't have to cleanup + with tempfile.NamedTemporaryFile(delete=False) as obj_yaml: + obj_yaml.write(yaml.safe_dump(obj).encode('utf-8')) + return obj_yaml.name + + +class TestConfig(testtools.TestCase): def test_get_one_cloud(self): c = config.OpenStackConfig() self.assertIsInstance(c.get_one_cloud(), cloud_config.CloudConfig) def test_get_one_cloud_with_config_files(self): self.useFixture(fixtures.NestedTempfile()) - with tempfile.NamedTemporaryFile() as cloud_yaml: - cloud_yaml.write(yaml.safe_dump(self.get_config()).encode('utf-8')) - cloud_yaml.flush() - c = config.OpenStackConfig(config_files=[cloud_yaml.name]) + conf = dict(USER_CONF) + tdir = self.useFixture(fixtures.TempDir()) + conf['cache']['path'] = tdir.path + cloud_yaml = _write_yaml(conf) + vendor_yaml = _write_yaml(VENDOR_CONF) + c = config.OpenStackConfig(config_files=[cloud_yaml], + vendor_files=[vendor_yaml]) self.assertIsInstance(c.cloud_config, dict) self.assertIn('cache', c.cloud_config) self.assertIsInstance(c.cloud_config['cache'], dict) self.assertIn('max_age', c.cloud_config['cache']) self.assertIn('path', c.cloud_config['cache']) cc = c.get_one_cloud('_test_cloud_') + self._assert_cloud_details(cc) + cc = c.get_one_cloud('_test_cloud_no_vendor') + self._assert_cloud_details(cc) + + def _assert_cloud_details(self, cc): self.assertIsInstance(cc, cloud_config.CloudConfig) self.assertTrue(extras.safe_hasattr(cc, 'auth')) self.assertIsInstance(cc.auth, dict) + self.assertIsNone(cc.cloud) self.assertIn('username', cc.auth) self.assertEqual('testuser', cc.auth['username']) + self.assertEqual('testproject', cc.auth['project_name']) From f99dc5f293bb71185be2102ea646b07159faf0a0 Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Sat, 28 Feb 2015 07:45:01 -0800 Subject: [PATCH 0109/3836] Fix coverage report Coverage can't handle package names that have dashes, as it uses the package name to look for the base module name. So we need to pass in the base module name as it is imported. Change-Id: I2840eea85acaee2d05cab47fb67010e002a14bc0 --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 9be310a4c..860e3378b 100644 --- a/tox.ini +++ b/tox.ini @@ -19,7 +19,7 @@ commands = flake8 commands = {posargs} [testenv:cover] -commands = python setup.py testr --coverage --testr-args='{posargs}' +commands = python setup.py test --coverage --coverage-package-name=os_client_config --testr-args='{posargs}' [testenv:docs] commands = python setup.py build_sphinx @@ -31,4 +31,4 @@ commands = python setup.py build_sphinx show-source = True ignore = E123,E125,H803 builtins = _ -exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build \ No newline at end of file +exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build From 1ccad8c41c9cab2f577e6505daab858a2c9d22b5 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 26 Feb 2015 08:57:46 -0500 Subject: [PATCH 0110/3836] Add support for configuring dogpile.cache shade has added dogpile.cache support. Since os-client-config already supports processing global cache settings, go ahead and add support for a couple of settings that can be used to feed dogpile.cache. Change-Id: I4d40753b83041c8a48b5c0a6d446f9e0de68220a --- os_client_config/config.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/os_client_config/config.py b/os_client_config/config.py index f8adc047d..83ec6c49d 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -88,11 +88,17 @@ def __init__(self, config_files=None, vendor_files=None): self._cache_max_age = 300 self._cache_path = CACHE_PATH + self._cache_class = 'dogpile.cache.memory' + self._cache_arguments = {} if 'cache' in self.cloud_config: self._cache_max_age = self.cloud_config['cache'].get( 'max_age', self._cache_max_age) self._cache_path = os.path.expanduser( self.cloud_config['cache'].get('path', self._cache_path)) + self._cache_class = self.cloud_config['cache'].get( + 'class', self._cache_class) + self._cache_arguments = self.cloud_config['cache'].get( + 'arguments', self._cache_arguments) def _load_config_file(self): for path in self._config_files: @@ -112,6 +118,12 @@ def get_cache_max_age(self): def get_cache_path(self): return self._cache_path + def get_cache_class(self): + return self._cache_class + + def get_cache_arguments(self): + return self._cache_arguments + def _get_regions(self, cloud): try: return self.cloud_config['clouds'][cloud]['region_name'] From 62616f3eaaff708c583919b55093410a0179c940 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Thu, 26 Feb 2015 16:01:50 -0500 Subject: [PATCH 0111/3836] Add behavior to enable ironic noauth mode Added behavior to enable users to utilize the ironic functionality with noauth mode set on the server side. This logic is utilized if the auth_plugin is defined as: None or "None" or ''. Bumped os-client-config requirement because this working requires the pass-through support that got added there in 0.5.0. Change-Id: I3f8a9e0a9952be2d24c6cb655dfed9519134102e --- requirements.txt | 2 +- shade/__init__.py | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 46d428806..95dabd739 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ pbr>=0.5.21,<1.0 -os-client-config>=0.4.0 +os-client-config>=0.5.0 six python-novaclient>=2.21.0 diff --git a/shade/__init__.py b/shade/__init__.py index b854c966d..049f9c895 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1235,13 +1235,31 @@ def get_object_metadata(self, container, name): class OperatorCloud(OpenStackCloud): + @property + def auth_token(self): + if self.auth_plugin in (None, "None", ''): + return self._auth_token + if not self._auth_token: + self._auth_token = self.keystone_session.get_token() + return self._auth_token + @property def ironic_client(self): if self._ironic_client is None: ironic_logging = logging.getLogger('ironicclient') ironic_logging.addHandler(logging.NullHandler()) token = self.auth_token - endpoint = self.get_endpoint(service_type='baremetal') + if self.auth_plugin in (None, "None", ''): + # TODO: This needs to be improved logic wise, perhaps a list, + # or enhancement of the data stuctures with-in the library + # to allow for things aside password authentication, or no + # authentication if so desired by the user. + # + # Attempt to utilize a pre-stored endpoint in the auth + # dict as the endpoint. + endpoint = self.auth['endpoint'] + else: + endpoint = self.get_endpoint(service_type='baremetal') try: self._ironic_client = ironic_client.Client( '1', endpoint, token=token) From 5ce3b222ab823802aae520ba965a8db7d1bf9420 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 20 Feb 2015 15:23:14 -0500 Subject: [PATCH 0112/3836] Add methods to create and delete networks The create_network() method currently does not support admin-level operations. This will be added real soon now. Change-Id: I5909b3cafd9c418f953a76cfaaf5c5708ad7e945 --- shade/__init__.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 6c9a37696..98e2e33f9 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -430,6 +430,52 @@ def get_network(self, name_or_id): return network return None + # TODO(Shrews): This will eventually need to support tenant ID and + # provider networks, which are admin-level params. + def create_network(self, name, shared=False, admin_state_up=True): + """Create a network. + + :param name: Name of the network being created. + :param shared: Set the network as shared. + :param admin_state_up: Set the network administrative state to up. + + :returns: The network object. + :raises: OpenStackCloudException on operation error. + """ + neutron = self.neutron_client + + network = { + 'name': name, + 'shared': shared, + 'admin_state_up': admin_state_up + } + + try: + net = neutron.create_network({'network': network}) + except Exception as e: + self.log.debug("Network creation failed", exc_info=True) + raise OpenStackCloudException( + "Error in creating network %s: %s" % (name, e.message)) + # Turns out neutron returns an actual dict, so no need for the + # use of meta.obj_to_dict() here (which would not work against + # a dict). + return net['network'] + + def delete_network(self, name_or_id): + """Delete a network. + + :param name_or_id: Name or ID of the network being deleted. + :raises: OpenStackCloudException on operation error. + """ + neutron = self.neutron_client + network = self.get_network(name_or_id) + try: + neutron.delete_network(network['id']) + except Exception as e: + self.log.debug("Network deletion failed", exc_info=True) + raise OpenStackCloudException( + "Error in deleting network %s: %s" % (name_or_id, e.message)) + def _get_images_from_cloud(self, filter_deleted): # First, try to actually get images from glance, it's more efficient images = dict() From 4ad95b33852fc99046a0d9f0ad75e2369ac747d3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 1 Mar 2015 11:13:01 -0500 Subject: [PATCH 0113/3836] Align cert, key, cacert and verify with requests The semantics of these values are specific, and an interface we'd carried over from the old ansible modules really just makes things more confusing. Requests expects that verify can either be True/False or can be the path to a cacert bundle. That makes it very hard to do input parameter validation in ansible. So - split that into verify which takes a boolean and a path to a cacert bundle. Requests also expects that cert can either be a path to a cert/key bundle, or a tuple of two paths. I'm not really sure why it thinks that's clever, but I don't. Change-Id: Ie96e7afe3ef056499477b746d9262975081206d7 --- shade/__init__.py | 54 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index b854c966d..bb2ba76fa 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -15,6 +15,7 @@ import hashlib import logging import operator +import os import time from cinderclient.v1 import client as cinder_client @@ -106,10 +107,40 @@ def _iterate_timeout(timeout, message): class OpenStackCloud(object): + """Represent a connection to an OpenStack Cloud. + + OpenStackCloud is the entry point for all cloud operations, regardless + of which OpenStack service those operations may ultimately come from. + The operations on an OpenStackCloud are resource oriented rather than + REST API operation oriented. For instance, one will request a Floating IP + and that Floating IP will be actualized either via neutron or via nova + depending on how this particular cloud has decided to arrange itself. + + :param cloud: A Cloud Configuration object, obtained from os-client-config + :type cloud: :py:class:`os_client_config.config.OpenStackConfig` + :param string region: The region of the cloud that all operations should + be performed against. + :param string auth_plugin: The name of the keystone auth_plugin to be used + :param bool verify: The verification arguments to pass to requests. True + tells requests to verify SSL requests, False to not + verify. (optional, defaults to True) + :param string cacert: A path to a CA Cert bundle that can be used as part + of verifying SSL requests. If this is set, verify + is set to True. (optional) + :param string cert: A path to a client certificate to pass to requests. + (optional) + :param string key: A path to a client key to pass to requests. (optional) + :param bool debug: Enable or disable debug logging (optional, defaults to + False) + :param int cache_interval: How long to cache items fetched from the cloud. + Value will be passed to dogpile.cache. None + means to just use the default in dogpile.cache. + (optional, defaults to None) + """ def __init__(self, cloud, region='', auth_plugin='password', - insecure=False, verify=None, cacert=None, cert=None, key=None, + verify=True, cacert=None, cert=None, key=None, debug=False, cache_interval=None, **kwargs): self.name = cloud @@ -129,15 +160,22 @@ def __init__(self, cloud, region='', self.endpoint_type = kwargs.get('endpoint_type', 'publicURL') self.private = kwargs.get('private', False) - if verify is None: - if insecure: - verify = False - else: - verify = cacert or True + if cacert: + if not os.path.exists(cacert): + raise OpenStackCloudException( + "CA Cert {0} does not exist".format(cacert)) + verify = cacert self.verify = verify - if cert and key: - cert = (cert, key) + if cert: + if not os.path.exists(cert): + raise OpenStackCloudException( + "Client Cert {0} does not exist".format(cert)) + if key: + if not os.path.exists(key): + raise OpenStackCloudException( + "Client key {0} does not exist".format(key)) + cert = (cert, key) self.cert = cert self._cache = cache.make_region().configure( From 55d04bfcff21ee74a2d4380a1d6e22d8f1ec10b2 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Tue, 3 Mar 2015 11:58:45 -0500 Subject: [PATCH 0114/3836] Add ironic node deployment support Add methods required for deployment of nodes via ironic for the under development os_ironic_node ansible module. Change-Id: I42dcac0478706ffc7244387840cc6740fc3cfb79 --- shade/__init__.py | 50 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 049f9c895..48567d66a 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1337,3 +1337,53 @@ def unregister_machine(self, nics, uuid): "ironic machine unregistration failed", exc_info=True) raise OpenStackCloudException( "Error unregistering machine from Ironic: %s" % e.message) + + def validate_node(self, uuid): + try: + ifaces = self.ironic_client.node.validate(uuid) + except Exception as e: + self.log.debug( + "ironic node validation call failed", exc_info=True) + raise OpenStackCloudException(e.message) + + if not ifaces.deploy or not ifaces.power: + raise OpenStackCloudException( + "ironic node %s failed to validate. " + "(deploy: %s, power: %s)" % (ifaces.deploy, ifaces.power)) + + def node_set_provision_state(self, uuid, state, configdrive=None): + try: + self.ironic_client.node.set_provision_state( + uuid, + state, + configdrive + ) + except Exception as e: + self.log.debug( + "ironic node failed change provision state to %s" % state, + exc_info=True) + raise OpenStackCloudException(e.message) + + def activate_node(self, uuid, configdrive=None): + self.node_set_provision_state(uuid, 'active', configdrive) + + def deactivate_node(self, uuid): + self.node_set_provision_state(uuid, 'deleted') + + def set_node_instance_info(self, uuid, patch): + try: + self.ironic_client.node.update(uuid, patch) + except Exception as e: + self.log.debug( + "Failed to update instance_info", exc_info=True) + raise OpenStackCloudException(e.message) + + def purge_node_instance_info(self, uuid): + patch = [] + patch.append({'op': 'remove', 'path': '/instance_info'}) + try: + return self.ironic_client.node.update(uuid, patch) + except Exception as e: + self.log.debug( + "Failed to delete instance_info", exc_info=True) + raise OpenStackCloudException(e.message) From 6fff9fe2b41656739eca0a47d61e17e12e114f4f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 3 Mar 2015 07:31:32 -0500 Subject: [PATCH 0115/3836] Rename auth_plugin to auth_type To match with python-openstackclient is doing, change auth_plugin to auth_type. Since this is out in the wild already, add it to the backwards compat matrix. Change-Id: I36b3f18d57fa827028194f8af91ea309b53b6ee3 --- os_client_config/config.py | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 6bc84e26c..ecf463a5e 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -75,7 +75,7 @@ def __init__(self, config_files=None, vendor_files=None): self._vendor_files = vendor_files or VENDOR_FILES defaults = dict( - auth_plugin='password', + auth_type='password', compute_api_version='1.1', ) self.defaults = _get_os_environ(defaults) @@ -168,16 +168,22 @@ def _get_base_cloud_config(self, name): if 'cloud' in cloud: del cloud['cloud'] - return self._fix_project_madness(cloud) - - def _fix_project_madness(self, cloud): - project_name = None - # Do the list backwards so that project_name is the ultimate winner - for key in ('tenant_id', 'project_id', 'tenant_name', 'project_name'): - if key in cloud: - project_name = cloud[key] - del cloud[key] - cloud['project_name'] = project_name + return self._fix_backwards_madness(cloud) + + def _fix_backwards_madness(self, cloud): + # Do the lists backwards so that project_name is the ultimate winner + mappings = { + 'project_name': ('tenant_id', 'project_id', + 'tenant_name', 'project_name'), + 'auth_type': ('auth_plugin', 'auth_type'), + } + for target_key, possible_values in mappings.items(): + target = None + for key in possible_values: + if key in cloud: + target = cloud[key] + del cloud[key] + cloud[target_key] = target return cloud def get_all_clouds(self): @@ -231,7 +237,7 @@ def _find_winning_auth_value(self, opt, config): def _validate_auth(self, config): # May throw a keystoneclient.exceptions.NoMatchingPlugin plugin_options = ksc_auth.get_plugin_class( - config['auth_plugin']).get_options() + config['auth_type']).get_options() for p_opt in plugin_options: # if it's in config.auth, win, kill it from config dict @@ -252,7 +258,7 @@ def _validate_auth(self, config): ' or environment variables. Missing value {auth_key}' ' required for auth plugin {plugin}'.format( cloud=cloud, files=','.join(self._config_files), - auth_key=p_opt.name, plugin=config['auth_plugin'])) + 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_opts]: @@ -306,8 +312,8 @@ def get_one_cloud(self, cloud=None, validate=True, if type(config[key]) is not bool: config[key] = get_boolean(config[key]) - if 'auth_plugin' in config: - if config['auth_plugin'] in ('', 'None', None): + if 'auth_type' in config: + if config['auth_type'] in ('', 'None', None): validate = False if validate and ksc_auth: From df91dcf111a7aeecad948481fc102bedd3fad05d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 3 Mar 2015 16:34:19 -0500 Subject: [PATCH 0116/3836] Add two newlines to the ends of files flake8 bites it for me on these locally. Change-Id: I99291c64fafff423aa720da7dba030526ca0cb50 --- os_client_config/tests/base.py | 2 +- os_client_config/tests/test_os_client_config.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py index 185fd6fd9..1c30cdb56 100644 --- a/os_client_config/tests/base.py +++ b/os_client_config/tests/base.py @@ -20,4 +20,4 @@ class TestCase(base.BaseTestCase): - """Test case base class for all unit tests.""" \ No newline at end of file + """Test case base class for all unit tests.""" diff --git a/os_client_config/tests/test_os_client_config.py b/os_client_config/tests/test_os_client_config.py index 9b26ad547..7421b6fe4 100644 --- a/os_client_config/tests/test_os_client_config.py +++ b/os_client_config/tests/test_os_client_config.py @@ -25,4 +25,4 @@ class TestOs_client_config(base.TestCase): def test_something(self): - pass \ No newline at end of file + pass From 3b5a20ea037c8e01f5da2f98e9e47082cefad89e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 3 Mar 2015 16:09:00 -0500 Subject: [PATCH 0117/3836] Handle project_name/tenant_name in the auth dict We were doing backwards compat for project/tenant in a way that didn't notice anything in the auth dict - which is there project/tenant info goes. Ooops. Change-Id: I141c1d99f31f381898bf993c4e7fcab1078f40c6 --- os_client_config/config.py | 26 ++++++++++++++++++++++++-- os_client_config/tests/test_config.py | 22 +++++++++++++--------- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index ecf463a5e..3565fa684 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -164,6 +164,9 @@ def _get_base_cloud_config(self, name): # Can't find the requested vendor config, go about business pass + if 'auth' not in cloud: + cloud['auth'] = dict() + _auth_update(cloud, our_cloud) if 'cloud' in cloud: del cloud['cloud'] @@ -171,10 +174,31 @@ def _get_base_cloud_config(self, name): return self._fix_backwards_madness(cloud) def _fix_backwards_madness(self, cloud): + cloud = self._fix_backwards_project(cloud) + cloud = self._fix_backwards_auth_plugin(cloud) + return cloud + + def _fix_backwards_project(self, cloud): # Do the lists backwards so that project_name is the ultimate winner mappings = { 'project_name': ('tenant_id', 'project_id', 'tenant_name', 'project_name'), + } + for target_key, possible_values in mappings.items(): + target = None + for key in possible_values: + if key in cloud: + target = cloud[key] + del cloud[key] + if key in cloud['auth']: + target = cloud['auth'][key] + del cloud['auth'][key] + cloud['auth'][target_key] = target + return cloud + + def _fix_backwards_auth_plugin(self, cloud): + # Do the lists backwards so that auth_type is the ultimate winner + mappings = { 'auth_type': ('auth_plugin', 'auth_type'), } for target_key, possible_values in mappings.items(): @@ -299,8 +323,6 @@ def get_one_cloud(self, cloud=None, validate=True, args['region_name'] = self._get_region(cloud) config = self._get_base_cloud_config(cloud) - if 'auth' not in config: - config['auth'] = dict() # Can't just do update, because None values take over for (key, val) in iter(args.items()): diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index b19899a5d..5ef3048f0 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -64,19 +64,23 @@ def _write_yaml(obj): class TestConfig(testtools.TestCase): - def test_get_one_cloud(self): - c = config.OpenStackConfig() - self.assertIsInstance(c.get_one_cloud(), cloud_config.CloudConfig) - - def test_get_one_cloud_with_config_files(self): + def setUp(self): + super(TestConfig, self).setUp() self.useFixture(fixtures.NestedTempfile()) conf = dict(USER_CONF) tdir = self.useFixture(fixtures.TempDir()) conf['cache']['path'] = tdir.path - cloud_yaml = _write_yaml(conf) - vendor_yaml = _write_yaml(VENDOR_CONF) - c = config.OpenStackConfig(config_files=[cloud_yaml], - vendor_files=[vendor_yaml]) + self.cloud_yaml = _write_yaml(conf) + self.vendor_yaml = _write_yaml(VENDOR_CONF) + + def test_get_one_cloud(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + self.assertIsInstance(c.get_one_cloud(), cloud_config.CloudConfig) + + def test_get_one_cloud_with_config_files(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) self.assertIsInstance(c.cloud_config, dict) self.assertIn('cache', c.cloud_config) self.assertIsInstance(c.cloud_config['cache'], dict) From d5931d4658b43b5451adc38be5d6b7f257291098 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 5 Mar 2015 08:51:18 -0500 Subject: [PATCH 0118/3836] Flesh out api version defaults openstackclient needs a bit richer support for api version defaults. Namely, it knows what defaults it wants to have - but we need to do defaults processing in os-client-config to get sequencing correct. So provide an API call to set new defaults that can be used before config processing. Also, flesh out the dict of known default values with good defaults to match osc behavior, and add the known v1 default of HP to the vendors.py values. Change-Id: I45e2550af58aee616ca168d20a557077beeab007 --- os_client_config/config.py | 18 ++++++++++-------- os_client_config/defaults.py | 23 +++++++++++++++++++++++ os_client_config/vendors.py | 1 + 3 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 os_client_config/defaults.py diff --git a/os_client_config/config.py b/os_client_config/config.py index 3565fa684..48b99f591 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -23,6 +23,7 @@ ksc_auth = None from os_client_config import cloud_config +from os_client_config import defaults from os_client_config import exceptions from os_client_config import vendors @@ -41,18 +42,23 @@ os.path.join(d, 'clouds-public.yaml') for d in VENDOR_SEARCH_PATH] +def set_default(key, value): + defaults._defaults[key] = value + + def get_boolean(value): if value.lower() == 'true': return True return False -def _get_os_environ(defaults): +def _get_os_environ(): + ret = dict(defaults._defaults) for (k, v) in os.environ.items(): if k.startswith('OS_'): newkey = k[3:].lower() - defaults[newkey] = v - return defaults + ret[newkey] = v + return ret def _auth_update(old_dict, new_dict): @@ -74,11 +80,7 @@ def __init__(self, config_files=None, vendor_files=None): self._config_files = config_files or CONFIG_FILES self._vendor_files = vendor_files or VENDOR_FILES - defaults = dict( - auth_type='password', - compute_api_version='1.1', - ) - self.defaults = _get_os_environ(defaults) + self.defaults = _get_os_environ() # use a config file if it exists where expected self.cloud_config = self._load_config_file() diff --git a/os_client_config/defaults.py b/os_client_config/defaults.py new file mode 100644 index 000000000..5f70d6ee4 --- /dev/null +++ b/os_client_config/defaults.py @@ -0,0 +1,23 @@ +# Copyright (c) 2014 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. + +_defaults = dict( + auth_type='password', + compute_api_version='2', + identity_api_version='2', + image_api_version='1', + network_api_version='2', + object_api_version='1', + volume_api_version='1', +) diff --git a/os_client_config/vendors.py b/os_client_config/vendors.py index cab3676a1..eb08bbdbd 100644 --- a/os_client_config/vendors.py +++ b/os_client_config/vendors.py @@ -20,6 +20,7 @@ ), region_name='region-b.geo-1', dns_service_type='hpext:dns', + image_api_version='1', ), rackspace=dict( auth=dict( From 99e85402b660a768071bc771cb3fd8cf9babb6af Mon Sep 17 00:00:00 2001 From: Ghe Rivero Date: Fri, 6 Mar 2015 10:22:48 +0100 Subject: [PATCH 0119/3836] Add cover to .gitignore Cover reports in the cover directory are not ignored by git Change-Id: If1f473a9e453c53d1ace9efd6935cf5e7fb84e50 --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1399c9819..ed8833456 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ pip-log.txt # Unit test / coverage reports .coverage +cover .tox nosetests.xml .testrepository @@ -48,4 +49,4 @@ ChangeLog # Editors *~ -.*.swp \ No newline at end of file +.*.swp From e5d2782cef302dbc677ebba06f5f7f3e520b1031 Mon Sep 17 00:00:00 2001 From: Ghe Rivero Date: Tue, 10 Mar 2015 19:00:50 +0100 Subject: [PATCH 0120/3836] Add cover to .gitignore Coverage reports in the cover directory should be ignored by git Change-Id: I4cde6b0b8fa2fa04ce6a4c308af3c37dfcdf1b5d --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e6a97ec61..837459343 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ lib64 pip-log.txt # Unit test / coverage reports +cover .coverage .tox nosetests.xml @@ -49,4 +50,4 @@ ChangeLog # Editors *~ .*.swp -.*sw? \ No newline at end of file +.*sw? From 738d215661c18aa3d37dd1262d6ca28045da5a7b Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Thu, 12 Mar 2015 17:18:36 +0100 Subject: [PATCH 0121/3836] Add initial compute functional tests to Shade Also, modified tox.ini to add a 'functional' stanza and the testenv now runs only unit tests. Change-Id: Id8a1193c829f7f3ab764c334b1f796f19f447fdd Depends-On: I1cf58e6a4cf728c5d2a32d602d9dfae1d4dfd62c --- .testr.conf | 4 +- shade/tests/functional/__init__.py | 0 .../tests/functional/hooks/post_test_hook.sh | 29 +++++++++ shade/tests/functional/test_compute.py | 59 +++++++++++++++++++ shade/tests/functional/util.py | 37 ++++++++++++ shade/tests/unit/__init__.py | 0 shade/tests/{ => unit}/test_meta.py | 0 shade/tests/{ => unit}/test_shade.py | 0 tox.ini | 4 ++ 9 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 shade/tests/functional/__init__.py create mode 100755 shade/tests/functional/hooks/post_test_hook.sh create mode 100644 shade/tests/functional/test_compute.py create mode 100644 shade/tests/functional/util.py create mode 100644 shade/tests/unit/__init__.py rename shade/tests/{ => unit}/test_meta.py (100%) rename shade/tests/{ => unit}/test_shade.py (100%) diff --git a/.testr.conf b/.testr.conf index fb622677a..3ce7ec2a9 100644 --- a/.testr.conf +++ b/.testr.conf @@ -2,6 +2,6 @@ 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 ./ . $LISTOPT $IDOPTION + ${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./shade/tests/unit} $LISTOPT $IDOPTION test_id_option=--load-list $IDFILE -test_list_option=--list \ No newline at end of file +test_list_option=--list diff --git a/shade/tests/functional/__init__.py b/shade/tests/functional/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/shade/tests/functional/hooks/post_test_hook.sh b/shade/tests/functional/hooks/post_test_hook.sh new file mode 100755 index 000000000..ccf477455 --- /dev/null +++ b/shade/tests/functional/hooks/post_test_hook.sh @@ -0,0 +1,29 @@ +# -*- 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. + +export SHADE_DIR="$BASE/new/shade" + +cd $BASE/new/devstack +source openrc admin admin +unset OS_CACERT + +cd $SHADE_DIR +sudo chown -R jenkins:stack $SHADE_DIR +echo "Running shade functional test suite" +set +e +sudo -E -H -u jenkins tox -efunctional +EXIT_CODE=$? +set -e + +exit $EXIT_CODE diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py new file mode 100644 index 000000000..6e456b1a8 --- /dev/null +++ b/shade/tests/functional/test_compute.py @@ -0,0 +1,59 @@ +# -*- 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. + +""" +test_compute +---------------------------------- + +Functional tests for `shade` compute methods. +""" + +from novaclient.v2.servers import Server +from shade import openstack_cloud +from shade.tests import base +from shade.tests.functional.util import pick_flavor, pick_image + + +class TestCompute(base.TestCase): + def setUp(self): + super(TestCompute, self).setUp() + # Shell should have OS-* envvars from openrc, typically loaded by job + self.cloud = openstack_cloud() + self.nova = self.cloud.nova_client + self.flavor = pick_flavor(self.nova.flavors.list()) + if self.flavor is None: + self.addDetail('pick_flavor', 'no sensible flavor available') + self.image = pick_image(self.nova.images.list()) + if self.image is None: + self.addDetail('pick_image', 'no sensible image available') + self.addCleanup(self._cleanup_servers) + + def _cleanup_servers(self): + for i in self.nova.servers.list(): + if i.name.startswith('test'): + self.nova.servers.delete(i) + + def test_create_server(self): + server = self.cloud.create_server(name='test_create_server', + image=self.image, flavor=self.flavor) + self.assertIsInstance(server, Server) + self.assertEquals(server.name, 'test_create_server') + self.assertEquals(server.image['id'], self.image.id) + self.assertEquals(server.flavor['id'], self.flavor.id) + + def test_delete_server(self): + self.cloud.create_server(name='test_delete_server', + image=self.image, flavor=self.flavor) + server_deleted = self.cloud.delete_server('test_delete_server') + self.assertIsNone(server_deleted) diff --git a/shade/tests/functional/util.py b/shade/tests/functional/util.py new file mode 100644 index 000000000..bddb57f3e --- /dev/null +++ b/shade/tests/functional/util.py @@ -0,0 +1,37 @@ +# -*- 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. + +""" +util +-------------------------------- + +Util methods for functional tests +""" + + +def pick_flavor(flavors): + """Given a flavor list pick a reasonable one.""" + for flavor in flavors: + if flavor.name == 'm1.tiny': + return flavor + + for flavor in flavors: + if flavor.name == 'm1.small': + return flavor + + +def pick_image(images): + for image in images: + if image.name.startswith('cirros') and image.name.endswith('-uec'): + return image diff --git a/shade/tests/unit/__init__.py b/shade/tests/unit/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/shade/tests/test_meta.py b/shade/tests/unit/test_meta.py similarity index 100% rename from shade/tests/test_meta.py rename to shade/tests/unit/test_meta.py diff --git a/shade/tests/test_shade.py b/shade/tests/unit/test_shade.py similarity index 100% rename from shade/tests/test_shade.py rename to shade/tests/unit/test_shade.py diff --git a/tox.ini b/tox.ini index e0ff3d6e6..71cbd48c8 100644 --- a/tox.ini +++ b/tox.ini @@ -15,6 +15,10 @@ deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = python setup.py testr --slowest --testr-args='{posargs}' +[testenv:functional] +setenv = + OS_TEST_PATH = ./shade/tests/functional + [testenv:pep8] commands = flake8 From 63e1630f2b44e1f867ed887ea7d66b76daca671a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 6 Mar 2015 08:21:43 -0500 Subject: [PATCH 0122/3836] Change dogpile cache defaults Memory cache can grow unbounded, so it should really be opt in. Change to match shade with the following defaults: - If you specify nothing, you get null cache - If you specify an expiration time and nothing else, you get memory cache - If you specify an explicit cache class, you will get that class Change-Id: I6c9eab71a88a534de7e52ad2a564450c44aacc1d --- README.rst | 14 +++++++++++--- os_client_config/config.py | 6 ++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index e6dbed072..4c67acfab 100644 --- a/README.rst +++ b/README.rst @@ -112,13 +112,21 @@ Cache Settings Accessing a cloud is often expensive, so it's quite common to want to do some client-side caching of those operations. To facilitate that, os-client-config -understands a simple set of cache control settings. +understands passing through cache settings to dogpile.cache, with the following +behaviors: + +* Listing no config settings means you get a null cache. +* `cache.max_age` and nothing else gets you memory cache. +* Otherwise, `cache.class` and `cache.arguments` are passed in :: cache: - path: ~/.cache/openstack - max_age: 300 + class: dogpile.cache.pylibmc + max_age: 3600 + arguments: + url: + - 127.0.0.1 clouds: mordred: cloud: hp diff --git a/os_client_config/config.py b/os_client_config/config.py index 48b99f591..84940436e 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -88,13 +88,15 @@ def __init__(self, config_files=None, vendor_files=None): self.cloud_config = dict( clouds=dict(openstack=dict(self.defaults))) - self._cache_max_age = 300 + self._cache_max_age = None self._cache_path = CACHE_PATH - self._cache_class = 'dogpile.cache.memory' + self._cache_class = 'dogpile.cache.null' self._cache_arguments = {} if 'cache' in self.cloud_config: self._cache_max_age = self.cloud_config['cache'].get( 'max_age', self._cache_max_age) + if self._cache_max_age: + self._cache_class = 'dogpile.cache.memory' self._cache_path = os.path.expanduser( self.cloud_config['cache'].get('path', self._cache_path)) self._cache_class = self.cloud_config['cache'].get( From 7cb9d2b46e88b8ddd3582ea229bd929c31be3832 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 16 Mar 2015 12:04:27 -0400 Subject: [PATCH 0123/3836] Clean up race condition in functional tests This could all be made much fancier - but in general, for now, kill concurrency until we can get much richer isolation put in - and make sure that we're only cleaning up servers that we created in the test. Change-Id: Iea341076f55f41573ee2bb0f378c8602d21b39a2 --- shade/tests/functional/test_compute.py | 4 ++-- tox.ini | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index 6e456b1a8..44f6e4725 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -37,14 +37,14 @@ def setUp(self): self.image = pick_image(self.nova.images.list()) if self.image is None: self.addDetail('pick_image', 'no sensible image available') - self.addCleanup(self._cleanup_servers) def _cleanup_servers(self): for i in self.nova.servers.list(): - if i.name.startswith('test'): + if i.name.startswith('test_create'): self.nova.servers.delete(i) def test_create_server(self): + self.addCleanup(self._cleanup_servers) server = self.cloud.create_server(name='test_create_server', image=self.image, flavor=self.flavor) self.assertIsInstance(server, Server) diff --git a/tox.ini b/tox.ini index 71cbd48c8..52264275c 100644 --- a/tox.ini +++ b/tox.ini @@ -18,6 +18,7 @@ commands = python setup.py testr --slowest --testr-args='{posargs}' [testenv:functional] setenv = OS_TEST_PATH = ./shade/tests/functional +commands = python setup.py testr --slowest --testr-args='--concurrency=1 {posargs}' [testenv:pep8] commands = flake8 From 74b90e8e9e9b5e11a7975c055838f7ed94949065 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 16 Mar 2015 09:24:12 -0400 Subject: [PATCH 0124/3836] Handle image name for boot from volume OpenStack apparenty has moments where it can return the image info as a unicode string, primarily when the server was booted from volume. Change-Id: I40c6796ab9d0cb2bc999b0d91313e62f10b1fccf --- shade/meta.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/shade/meta.py b/shade/meta.py index 90a60d511..55f13760b 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -112,10 +112,15 @@ def get_hostvars_from_server(cloud, server, mounts=None): if flavor_name: server_vars['flavor']['name'] = flavor_name - image_id = server.image['id'] - image_name = cloud.get_image_name(image_id) - if image_name: - server_vars['image']['name'] = image_name + # OpenStack can return image as a string when you've booted from volume + if unicode(server.image) == server.image: + image_id = server.image + else: + image_id = server.image.get('id', None) + if image_id: + image_name = cloud.get_image_name(image_id) + if image_name: + server_vars['image']['name'] = image_name volumes = [] for vol in cloud.get_volumes(server): From 52a28ba7a57098adf068db6f8dc5b43a9e15e462 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 1 Mar 2015 14:38:00 -0500 Subject: [PATCH 0125/3836] Fix up and document input parameters In walking through fixing SSL parameters, it became clear that we have several confusing or vestigal parameters. Walk through and fix them all to make them clearer. Also - let people pass through params through the openstack_clouds code path. Change-Id: Idc5823ed470e3f973a23504db9d7b179396690e8 --- shade/__init__.py | 88 ++++++++++++++++++++++++++--------------------- 1 file changed, 48 insertions(+), 40 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 30c37a6ad..5e90850a9 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -62,10 +62,10 @@ class OpenStackCloudTimeout(OpenStackCloudException): pass -def openstack_clouds(config=None): +def openstack_clouds(config=None, debug=False, **kwargs): if not config: config = os_client_config.OpenStackConfig() - return [OpenStackCloud(f.name, f.region, **f.config) + return [OpenStackCloud(name=f.name, debug=debug, **f.config) for f in config.get_all_clouds()] @@ -73,14 +73,33 @@ def openstack_cloud(debug=False, **kwargs): cloud_config = os_client_config.OpenStackConfig().get_one_cloud( **kwargs) return OpenStackCloud( - cloud_config.name, cloud_config.region, + name=cloud_config.name, debug=debug, **cloud_config.config) -def operator_cloud(**kwargs): +def operator_cloud(debug=False, **kwargs): cloud_config = os_client_config.OpenStackConfig().get_one_cloud(**kwargs) return OperatorCloud( - cloud_config.name, cloud_config.region, **cloud_config.config) + cloud_config.name, debug=debug, **cloud_config.config) + + +def _ssl_args(verify, cacert, cert, key): + if cacert: + if not os.path.exists(cacert): + raise OpenStackCloudException( + "CA Cert {0} does not exist".format(cacert)) + verify = cacert + + if cert: + if not os.path.exists(cert): + raise OpenStackCloudException( + "Client Cert {0} does not exist".format(cert)) + if key: + if not os.path.exists(key): + raise OpenStackCloudException( + "Client key {0} does not exist".format(key)) + cert = (cert, key) + return (verify, cert) def _get_service_values(kwargs, service_key): @@ -116,11 +135,20 @@ class OpenStackCloud(object): and that Floating IP will be actualized either via neutron or via nova depending on how this particular cloud has decided to arrange itself. - :param cloud: A Cloud Configuration object, obtained from os-client-config - :type cloud: :py:class:`os_client_config.config.OpenStackConfig` - :param string region: The region of the cloud that all operations should - be performed against. + :param string name: The name of the cloud + :param dict auth: Dictionary containing authentication information. + Depending on the value of auth_plugin, the contents + of this dict can vary wildly. + :param string region_name: The region of the cloud that all operations + should be performed against. + (optional, default '') :param string auth_plugin: The name of the keystone auth_plugin to be used + :param string endpoint_type: The type of endpoint to get for services + from the service catalog. Valid types are + `public` ,`internal` or `admin`. (optional, + defaults to `public`) + :param bool private: Whether to return or use private IPs by default for + servers. (optional, defaults to False) :param bool verify: The verification arguments to pass to requests. True tells requests to verify SSL requests, False to not verify. (optional, defaults to True) @@ -138,53 +166,33 @@ class OpenStackCloud(object): (optional, defaults to None) """ - def __init__(self, cloud, region='', + def __init__(self, name, auth, + region_name='', auth_plugin='password', + endpoint_type='public', + private=False, verify=True, cacert=None, cert=None, key=None, debug=False, cache_interval=None, **kwargs): - self.name = cloud - self.region = region - + self.name = name + self.auth = auth + self.region_name = region_name self.auth_plugin = auth_plugin - self.auth = kwargs.get('auth') - - self.region_name = kwargs.get('region_name', region) - self._auth_token = kwargs.get('auth_token', None) + self.endpoint_type = endpoint_type + self.private = private self.service_types = _get_service_values(kwargs, 'service_type') self.service_names = _get_service_values(kwargs, 'service_name') self.endpoints = _get_service_values(kwargs, 'endpoint') self.api_versions = _get_service_values(kwargs, 'api_version') - self.endpoint_type = kwargs.get('endpoint_type', 'publicURL') - self.private = kwargs.get('private', False) - - if cacert: - if not os.path.exists(cacert): - raise OpenStackCloudException( - "CA Cert {0} does not exist".format(cacert)) - verify = cacert - self.verify = verify - - if cert: - if not os.path.exists(cert): - raise OpenStackCloudException( - "Client Cert {0} does not exist".format(cert)) - if key: - if not os.path.exists(key): - raise OpenStackCloudException( - "Client key {0} does not exist".format(key)) - cert = (cert, key) - self.cert = cert + (self.verify, self.cert) = _ssl_args(verify, cacert, cert, key) self._cache = cache.make_region().configure( 'dogpile.cache.memory', expiration_time=cache_interval) self._container_cache = dict() self._file_hash_cache = dict() - self.debug = debug - self._keystone_session = None self._cinder_client = None @@ -198,7 +206,7 @@ def __init__(self, cloud, region='', self.log = logging.getLogger('shade') log_level = logging.INFO - if self.debug: + if debug: log_level = logging.DEBUG self.log.setLevel(log_level) self.log.addHandler(logging.StreamHandler()) From 9869fa342a1e40c81277a19a3c08e20510dab260 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 27 Feb 2015 11:22:43 -0500 Subject: [PATCH 0126/3836] Add support for keystone projects Lo and behold, there are potential differences in the field in what one has to do to manage keystone projects. Change-Id: Iced2c5dde8a91451e2cfeed50742ed3d6fdbf2f4 --- shade/__init__.py | 149 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 5e90850a9..b018555ef 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -26,6 +26,7 @@ from ironicclient import exceptions as ironic_exceptions from keystoneclient import auth as ksc_auth from keystoneclient import session as ksc_session +from keystoneclient import client as keystone_client from novaclient import client as nova_client from novaclient import exceptions as nova_exceptions from novaclient.v2 import floating_ips @@ -188,6 +189,8 @@ def __init__(self, name, auth, (self.verify, self.cert) = _ssl_args(verify, cacert, cert, key) + # dogpile.cache.memory does not clear things out of the memory dict, + # so there is a possibility of memory bloat over time. self._cache = cache.make_region().configure( 'dogpile.cache.memory', expiration_time=cache_interval) self._container_cache = dict() @@ -199,6 +202,7 @@ def __init__(self, name, auth, self._glance_client = None self._glance_endpoint = None self._ironic_client = None + self._keystone_client = None self._neutron_client = None self._nova_client = None self._swift_client = None @@ -277,6 +281,21 @@ def keystone_session(self): "Error authenticating to the keystone: %s " % e.message) return self._keystone_session + @property + def keystone_client(self): + if self._keystone_client is None: + try: + self._keystone_client = keystone_client.Client( + session=self.keystone_session, + auth_url=self.keystone_session.get_endpoint( + interface=ksc_auth.AUTH_INTERFACE)) + except Exception as e: + self.log.debug( + "Couldn't construct keystone object", exc_info=True) + raise OpenStackCloudException( + "Error constructing keystone client: %s" % e.message) + return self._keystone_client + @property def service_catalog(self): return self.keystone_session.auth.get_access( @@ -288,6 +307,136 @@ def auth_token(self): self._auth_token = self.keystone_session.get_token() return self._auth_token + @property + def project_cache(self): + @self._cache.cache_on_arguments() + def _project_cache(): + return {project.id: project for project in + self._project_manager.list()} + return _project_cache() + + @property + def _project_manager(self): + # Keystone v2 calls this attribute tenants + # Keystone v3 calls it projects + # Yay for usable APIs! + return getattr( + self.keystone_client, 'projects', self.keystone_client.tenants) + + def _get_project(self, name_or_id): + """Retrieve a project by name or id.""" + + # TODO(mordred): This, and other keystone operations, need to have + # domain information passed in. When there is no + # available domain information, we should default to + # the currently scoped domain which we can requset from + # the session. + for id, project in self.project_cache.items(): + if name_or_id in (id, project.name): + return project + return None + + def get_project(self, name_or_id): + """Retrieve a project by name or id.""" + project = self._get_project(name_or_id) + if project: + return meta.obj_to_dict(project) + return None + + def update_project(self, name_or_id, description=None, enabled=True): + try: + project = self._get_project(name_or_id) + return meta.obj_to_dict( + project.update(description=description, enabled=enabled)) + except Exception as e: + self.log.debug("keystone update project issue", exc_info=True) + raise OpenStackCloudException( + "Error in updating project {project}: {message}".format( + project=name_or_id, message=e.message)) + + def create_project(self, name, description=None, enabled=True): + """Create a project.""" + try: + self._project_manager.create( + project_name=name, description=description, enabled=enabled) + except Exception as e: + self.log.debug("keystone create project issue", exc_info=True) + raise OpenStackCloudException( + "Error in creating project {project}: {message}".format( + project=name, message=e.message)) + + def delete_project(self, name_or_id): + try: + project = self._get_project(name_or_id) + self._project_manager.delete(project.id) + except Exception as e: + self.log.debug("keystone delete project issue", exc_info=True) + raise OpenStackCloudException( + "Error in deleting project {project}: {message}".format( + project=name_or_id, message=e.message)) + + @property + def user_cache(self): + @self._cache.cache_on_arguments() + def _user_cache(): + return {user.id: user for user in + self.keystone_client.users.list()} + return _user_cache() + + def _get_user(self, name_or_id): + """Retrieve a user by name or id.""" + + for id, user in self.user_cache.items(): + if name_or_id in (id, user.name): + return user + return None + + def get_user(self, name_or_id): + """Retrieve a user by name or id.""" + user = self._get_user(name_or_id) + if user: + return meta.obj_to_dict(user) + return None + + def update_user(self, name_or_id, email=None, enabled=True): + try: + user = self._get_user(name_or_id) + return meta.obj_to_dict( + user.update(email=email, enabled=enabled)) + except Exception as e: + self.log.debug("keystone update user issue", exc_info=True) + raise OpenStackCloudException( + "Error in updating user {user}: {message}".format( + user=name_or_id, message=e.message)) + + def create_user( + self, name, password=None, email=None, project=None, + enabled=True): + """Create a user.""" + try: + if project: + project_id = self._get_project(project).id + else: + project_id = None + self.keystone_client.users.create( + user_name=name, password=password, email=email, + project=project_id, enabled=enabled) + except Exception as e: + self.log.debug("keystone create user issue", exc_info=True) + raise OpenStackCloudException( + "Error in creating user {user}: {message}".format( + user=name, message=e.message)) + + def delete_user(self, name_or_id): + try: + user = self._get_user(name_or_id) + self._user_manager.delete(user.id) + except Exception as e: + self.log.debug("keystone delete user issue", exc_info=True) + raise OpenStackCloudException( + "Error in deleting user {user}: {message}".format( + user=name_or_id, message=e.message)) + def _get_glance_api_version(self): if 'image' in self.api_versions: return self.api_versions['image'] From 792703e0168586d1393e28900b6ec7b3bba7276f Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Thu, 5 Mar 2015 16:39:18 -0800 Subject: [PATCH 0127/3836] Disable dogpile.cache if cache_interval is None Since we only use the in-memory dogpile.cache, there's a real danger of a long running process leaking indefinitely and running out of memory. By not specifying an interval, a user will end up with a noop cache region which does not waste memory. Change-Id: I26887caf1f1146ece9785366724fb450db0107a4 --- shade/__init__.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index b018555ef..7e8034253 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -163,7 +163,7 @@ class OpenStackCloud(object): False) :param int cache_interval: How long to cache items fetched from the cloud. Value will be passed to dogpile.cache. None - means to just use the default in dogpile.cache. + means do not cache at all. (optional, defaults to None) """ @@ -190,9 +190,15 @@ def __init__(self, name, auth, (self.verify, self.cert) = _ssl_args(verify, cacert, cert, key) # dogpile.cache.memory does not clear things out of the memory dict, - # so there is a possibility of memory bloat over time. + # so there is a possibility of memory bloat over time. By passing + # None a user will not get any in-memory cache and thus not have + # a large memory leak. + if cache_interval is None: + backend_name = 'dogpile.cache.null' + else: + backend_name = 'dogpile.cache.memory' self._cache = cache.make_region().configure( - 'dogpile.cache.memory', expiration_time=cache_interval) + backend_name, expiration_time=cache_interval) self._container_cache = dict() self._file_hash_cache = dict() From 85cac59fe245685beffa3aaf82adf861a0098557 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 6 Mar 2015 08:10:52 -0500 Subject: [PATCH 0128/3836] Small fixes found working on ansible modules Renaming cloud to name was a bad idea - undo it. Also, for now, the image cache needs to be invalidated when you upload an image, otherwise everything breaks. We should do that more fine grained in the future. Change-Id: I72da02bdb6e4c0c9378136e5794f7640f0e6b8f6 --- shade/__init__.py | 13 +++++++------ shade/meta.py | 4 ++-- shade/tests/unit/test_meta.py | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index b018555ef..c3e7a2861 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -63,10 +63,10 @@ class OpenStackCloudTimeout(OpenStackCloudException): pass -def openstack_clouds(config=None, debug=False, **kwargs): +def openstack_clouds(config=None, debug=False): if not config: config = os_client_config.OpenStackConfig() - return [OpenStackCloud(name=f.name, debug=debug, **f.config) + return [OpenStackCloud(cloud=f.name, debug=debug, **f.config) for f in config.get_all_clouds()] @@ -74,7 +74,7 @@ def openstack_cloud(debug=False, **kwargs): cloud_config = os_client_config.OpenStackConfig().get_one_cloud( **kwargs) return OpenStackCloud( - name=cloud_config.name, + cloud=cloud_config.name, debug=debug, **cloud_config.config) @@ -167,7 +167,7 @@ class OpenStackCloud(object): (optional, defaults to None) """ - def __init__(self, name, auth, + def __init__(self, cloud, auth, region_name='', auth_plugin='password', endpoint_type='public', @@ -175,7 +175,7 @@ def __init__(self, name, auth, verify=True, cacert=None, cert=None, key=None, debug=False, cache_interval=None, **kwargs): - self.name = name + self.name = cloud self.auth = auth self.region_name = region_name self.auth_plugin = auth_plugin @@ -197,6 +197,7 @@ def __init__(self, name, auth, self._file_hash_cache = dict() self._keystone_session = None + self._auth_token = None self._cinder_client = None self._glance_client = None @@ -684,7 +685,6 @@ def get_image_name(self, image_id, exclude=None): image = self.get_image(image_id, exclude) if image: return image.id - self._image_cache[image_id] = None return None def get_image_id(self, image_name, exclude=None): @@ -755,6 +755,7 @@ def create_image( def _upload_image_v1(self, name, filename, **image_kwargs): image = self.glance_client.images.create(name=name, **image_kwargs) image.update(data=open(filename, 'rb')) + self._cache.invalidate() return self.get_image_dict(image.id) def _upload_image_v2( diff --git a/shade/meta.py b/shade/meta.py index 90a60d511..5ba434824 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -52,7 +52,7 @@ def get_server_public_ip(server): def get_groups_from_server(cloud, server, server_vars): groups = [] - region = cloud.region + region = cloud.region_name cloud_name = cloud.name # Create a group for the cloud @@ -104,7 +104,7 @@ def get_hostvars_from_server(cloud, server, mounts=None): if interface_ip: server_vars['interface_ip'] = interface_ip - server_vars['region'] = cloud.region + server_vars['region'] = cloud.region_name server_vars['cloud'] = cloud.name flavor_id = server.flavor['id'] diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 142211cb3..72859fee2 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -49,7 +49,7 @@ class Server(object): def test_get_groups_from_server(self): class Cloud(object): - region = 'test-region' + region_name = 'test-region' name = 'test-name' class Server(object): From 924be2b545a4d00b9eacc5aa1c974e8ebf407c2f Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Thu, 26 Feb 2015 16:14:29 -0800 Subject: [PATCH 0129/3836] Add basic unit test for shade.openstack_cloud Just getting basic minimal surface area unit tests to help when making changes. This exposed some python3 incompatibilities in os-client-config, which is why the change Depends on Ia78bd8edd17c7d2360ad958b3de734503d400774. Change-Id: I9cf5082d01861a0b6b372728a33ce9df9ee8d300 Depends-On: Ia78bd8edd17c7d2360ad958b3de734503d400774 --- shade/tests/unit/test_shade.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 687129671..bf222e6f6 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -12,17 +12,11 @@ # License for the specific language governing permissions and limitations # under the License. -""" -test_shade ----------------------------------- - -Tests for `shade` module. -""" - +import shade from shade.tests import base class TestShade(base.TestCase): - def test_something(self): - pass + def test_openstack_cloud(self): + self.assertIsInstance(shade.openstack_cloud(), shade.OpenStackCloud) From 0dc916570848d149fd92afe4f1173c7efd780273 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 25 Mar 2015 17:34:41 +0000 Subject: [PATCH 0130/3836] Custom exception needs str representation Raising an unhandled OpenStackCloudException should produce output with the error message. This doesn't happen unless we have a string representation for our custom exception. Change-Id: I6b7eab28ffa9385eea9233b0053ade5e3396c9c9 --- shade/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index db28a15c7..ad5eee2f5 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -58,6 +58,9 @@ def __init__(self, message, extra_data=None): self.message = message self.extra_data = extra_data + def __str__(self): + return "%s (Extra: %s)" % (self.message, self.extra_data) + class OpenStackCloudTimeout(OpenStackCloudException): pass From 79a1c489a2fa72ee26f7434dd8b443b69c403a4d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 28 Mar 2015 11:36:44 -0400 Subject: [PATCH 0131/3836] Update .gitreview for git section rename Change-Id: I4a19b251c5fdf76ffb10387002a13336203a7b72 --- .gitreview | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitreview b/.gitreview index 234652006..5ba7eddc1 100644 --- a/.gitreview +++ b/.gitreview @@ -1,4 +1,4 @@ [gerrit] host=review.openstack.org port=29418 -project=stackforge/os-client-config.git +project=openstack/os-client-config.git From a517079c44ae8bb256f95a8275cb966af5fb8778 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 28 Mar 2015 12:51:58 -0400 Subject: [PATCH 0132/3836] Add api-level timeout parameter Some of the underlying client libraries support a configurable api timeout parameter. Add support for configuring the Cloud object to pass those through. Change-Id: Ibfdde8ff86db848e9ce601b4f1bb31e1237c9534 --- shade/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index db28a15c7..526315b2f 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -150,6 +150,8 @@ class OpenStackCloud(object): defaults to `public`) :param bool private: Whether to return or use private IPs by default for servers. (optional, defaults to False) + :param float api_timeout: A timeout to pass to REST client constructors + indicating network-level timeouts. (optional) :param bool verify: The verification arguments to pass to requests. True tells requests to verify SSL requests, False to not verify. (optional, defaults to True) @@ -173,6 +175,7 @@ def __init__(self, cloud, auth, endpoint_type='public', private=False, verify=True, cacert=None, cert=None, key=None, + api_timeout=None, debug=False, cache_interval=None, **kwargs): self.name = cloud @@ -181,6 +184,7 @@ def __init__(self, cloud, auth, self.auth_plugin = auth_plugin self.endpoint_type = endpoint_type self.private = private + self.api_timeout = api_timeout self.service_types = _get_service_values(kwargs, 'service_type') self.service_names = _get_service_values(kwargs, 'service_name') @@ -241,7 +245,8 @@ def nova_client(self): self._get_nova_api_version(), session=self.keystone_session, service_name=self.get_service_name('compute'), - region_name=self.region_name) + region_name=self.region_name, + timeout=self.api_timeout) except Exception: self.log.debug("Couldn't construct nova object", exc_info=True) raise From ff25b8eb3bff5b162fc112a694f3838928f524ec Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 24 Mar 2015 19:07:31 +0000 Subject: [PATCH 0133/3836] Add methods for logical router management Adds API methods to list existing routers, create a new logical router, update an existing router, and delete an existing logical router by name or UUID. It is considered an error if more than one router with the same name exists and you attempt to delete by name. Also... MOAR TESTS!!! ZOMG Change-Id: Ie6ea4eb5f2322bdda07e6db87e2cdbabea492ee9 --- shade/__init__.py | 100 +++++++++++++++++++++++++++++++++ shade/tests/unit/test_shade.py | 70 ++++++++++++++++++++++- test-requirements.txt | 1 + 3 files changed, 170 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index db28a15c7..51f0d9649 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -652,6 +652,15 @@ def get_network(self, name_or_id): return network return None + def list_routers(self): + return self.neutron_client.list_routers()['routers'] + + def get_router(self, name_or_id): + for router in self.list_routers(): + if name_or_id in (router['id'], router['name']): + return router + return None + # TODO(Shrews): This will eventually need to support tenant ID and # provider networks, which are admin-level params. def create_network(self, name, shared=False, admin_state_up=True): @@ -698,6 +707,97 @@ def delete_network(self, name_or_id): raise OpenStackCloudException( "Error in deleting network %s: %s" % (name_or_id, e.message)) + def create_router(self, name=None, admin_state_up=True): + """Create a logical router. + + :param name: The router name. + :param admin_state_up: The administrative state of the router. + + :returns: The router object. + :raises: OpenStackCloudException on operation error. + """ + neutron = self.neutron_client + router = { + 'admin_state_up': admin_state_up + } + if name: + router['name'] = name + + try: + new_router = neutron.create_router(dict(router=router)) + except Exception as e: + self.log.debug("Router create failed", exc_info=True) + raise OpenStackCloudException( + "Error creating router %s: %s" % (name, e)) + # Turns out neutron returns an actual dict, so no need for the + # use of meta.obj_to_dict() here (which would not work against + # a dict). + return new_router['router'] + + def update_router(self, router_id, name=None, admin_state_up=None): + """Update an existing logical router. + + :param router_id: The router UUID. + :param name: The router name. + :param admin_state_up: The administrative state of the router. + + :returns: The router object. + :raises: OpenStackCloudException on operation error. + """ + neutron = self.neutron_client + router = {} + if name: + router['name'] = name + if admin_state_up: + router['admin_state_up'] = admin_state_up + + if not router: + self.log.debug("No router data to update") + return + + try: + new_router = neutron.update_router(router_id, dict(router=router)) + except Exception as e: + self.log.debug("Router update failed", exc_info=True) + raise OpenStackCloudException( + "Error updating router %s: %s" % (name, e)) + # Turns out neutron returns an actual dict, so no need for the + # use of meta.obj_to_dict() here (which would not work against + # a dict). + return new_router['router'] + + def delete_router(self, name_or_id): + """Delete a logical router. + + If a name, instead of a unique UUID, is supplied, it is possible + that we could find more than one matching router since names are + not required to be unique. An error will be raised in this case. + + :param name_or_id: Name or ID of the router being deleted. + :raises: OpenStackCloudException on operation error. + """ + neutron = self.neutron_client + + routers = [] + for router in self.list_routers(): + if name_or_id in (router['id'], router['name']): + routers.append(router) + + if not routers: + raise OpenStackCloudException( + "Router %s not found." % name_or_id) + + if len(routers) > 1: + raise OpenStackCloudException( + "More than one router named %s. Use ID." % name_or_id) + + try: + neutron.delete_router(routers[0]['id']) + except Exception as e: + self.log.debug("Router delete failed", exc_info=True) + raise OpenStackCloudException( + "Error deleting router %s: %s" % (name_or_id, e)) + def _get_images_from_cloud(self, filter_deleted): # First, try to actually get images from glance, it's more efficient images = dict() diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index bf222e6f6..305ec61ce 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -12,11 +12,79 @@ # License for the specific language governing permissions and limitations # under the License. +import mock + import shade from shade.tests import base class TestShade(base.TestCase): + def setUp(self): + super(TestShade, self).setUp() + self.cloud = shade.openstack_cloud() + def test_openstack_cloud(self): - self.assertIsInstance(shade.openstack_cloud(), shade.OpenStackCloud) + self.assertIsInstance(self.cloud, shade.OpenStackCloud) + + @mock.patch.object(shade.OpenStackCloud, 'list_routers') + def test_get_router(self, mock_list): + router1 = dict(id='123', name='mickey') + mock_list.return_value = [router1] + r = self.cloud.get_router('mickey') + self.assertIsNotNone(r) + self.assertDictEqual(router1, r) + + @mock.patch.object(shade.OpenStackCloud, 'list_routers') + def test_get_router_not_found(self, mock_list): + mock_list.return_value = [] + r = self.cloud.get_router('goofy') + self.assertIsNone(r) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_router(self, mock_client): + self.cloud.create_router(name='goofy', admin_state_up=True) + self.assertTrue(mock_client.create_router.called) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_update_router(self, mock_client): + self.cloud.update_router(router_id=123, name='goofy') + self.assertTrue(mock_client.update_router.called) + + @mock.patch.object(shade.OpenStackCloud, 'list_routers') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_delete_router(self, mock_client, mock_list): + router1 = dict(id='123', name='mickey') + mock_list.return_value = [router1] + self.cloud.delete_router('mickey') + self.assertTrue(mock_client.delete_router.called) + + @mock.patch.object(shade.OpenStackCloud, 'list_routers') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_delete_router_not_found(self, mock_client, mock_list): + router1 = dict(id='123', name='mickey') + mock_list.return_value = [router1] + self.assertRaises(shade.OpenStackCloudException, + self.cloud.delete_router, + 'goofy') + self.assertFalse(mock_client.delete_router.called) + + @mock.patch.object(shade.OpenStackCloud, 'list_routers') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_delete_router_multiple_found(self, mock_client, mock_list): + router1 = dict(id='123', name='mickey') + router2 = dict(id='456', name='mickey') + mock_list.return_value = [router1, router2] + self.assertRaises(shade.OpenStackCloudException, + self.cloud.delete_router, + 'mickey') + self.assertFalse(mock_client.delete_router.called) + + @mock.patch.object(shade.OpenStackCloud, 'list_routers') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_delete_router_multiple_using_id(self, mock_client, mock_list): + router1 = dict(id='123', name='mickey') + router2 = dict(id='456', name='mickey') + mock_list.return_value = [router1, router2] + self.cloud.delete_router('123') + self.assertTrue(mock_client.delete_router.called) diff --git a/test-requirements.txt b/test-requirements.txt index 9dab35b8f..6957d91b4 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,6 +3,7 @@ hacking>=0.5.6,<0.8 coverage>=3.6 discover fixtures>=0.3.14 +mock>=1.0 python-subunit sphinx>=1.1.2 oslo.sphinx From 23d38f9b2ea81ea6f1bff29b4b0a74ab39e1651a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 29 Mar 2015 01:36:29 -0400 Subject: [PATCH 0134/3836] Pass socket timeout to all of the Client objects So far we've only been passing timeout to novaclient, but all of the Client objects support it as a pass through to requests. Go ahead and pass it. Change-Id: I095c1240693abf024bda2315dd77f4400b24a45b --- shade/__init__.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 526315b2f..f5d9b9d29 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -300,7 +300,8 @@ def keystone_client(self): self._keystone_client = keystone_client.Client( session=self.keystone_session, auth_url=self.keystone_session.get_endpoint( - interface=ksc_auth.AUTH_INTERFACE)) + interface=ksc_auth.AUTH_INTERFACE), + timeout=self.api_timeout) except Exception as e: self.log.debug( "Couldn't construct keystone object", exc_info=True) @@ -477,7 +478,8 @@ def glance_client(self): try: self._glance_client = glanceclient.Client( glance_api_version, endpoint, token=token, - session=self.keystone_session) + session=self.keystone_session, + timeout=self.api_timeout) except Exception as e: self.log.debug("glance unknown issue", exc_info=True) raise OpenStackCloudException( @@ -507,7 +509,8 @@ def cinder_client(self): # Make the connection self._cinder_client = cinder_client.Client( session=self.keystone_session, - region_name=self.region_name) + region_name=self.region_name, + timeout=self.api_timeout) if self._cinder_client is None: raise OpenStackCloudException( @@ -542,6 +545,7 @@ def trove_client(self): session=self.keystone_session, region_name=self.region_name, service_type=self.get_service_type('database'), + timeout=self.api_timeout, ) if self._trove_client is None: @@ -557,7 +561,8 @@ def neutron_client(self): self._neutron_client = neutron_client.Client( token=self.auth_token, session=self.keystone_session, - region_name=self.region_name) + region_name=self.region_name, + timeout=self.api_timeout) return self._neutron_client def get_name(self): @@ -1515,7 +1520,8 @@ def ironic_client(self): endpoint = self.get_endpoint(service_type='baremetal') try: self._ironic_client = ironic_client.Client( - '1', endpoint, token=token) + '1', endpoint, token=token, + timeout=self.api_timeout) except Exception as e: self.log.debug("ironic auth failed", exc_info=True) raise OpenStackCloudException( From afd2b107a72a21ae1095ace92598c439a40d7b17 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 29 Mar 2015 12:27:11 -0400 Subject: [PATCH 0135/3836] Disable warnings about old Rackspace certificates Rackspace has 'old' certificates deployed that the urllib3 people don't like. There is nothing a user can do about this - so just disable the warnings. Change-Id: Ib2d34c89dace491dabf6bc402d37babd93488e20 --- shade/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index ad5eee2f5..3af324220 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -37,6 +37,9 @@ import swiftclient.exceptions as swift_exceptions import troveclient.client as trove_client +# Disable the Rackspace warnings about deprecated certificates. We are aware +import warnings +warnings.filterwarnings('ignore', 'Certificate has no `subjectAltName`') from shade import meta From 1bd2a10cc010770c5f378f0a4a765c30d68422c7 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 30 Mar 2015 18:07:46 -0400 Subject: [PATCH 0136/3836] Fix a use of in where it should be equality The pattern for most other comparisons against name_or_id are if name_or_id in (name, id) - but in this case, because we're doing the name exclusion parameter as well, we split them. However, we kept the in, even though it should be testing for equality. This can cause some failures that make people sad. Change-Id: If33bfe5c1e32b36c27c32cfcba4c07dcf01a4e74 --- shade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index ad5eee2f5..5b30c3ca6 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -753,7 +753,7 @@ def get_image(self, name_or_id, exclude=None): if image_id == name_or_id: return image if (image is not None and - name_or_id in image.name and ( + name_or_id == image.name and ( not exclude or exclude not in image.name)): return image return None From 3de1b739c62ff49eea2c3e8a90c2dede2341a390 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 29 Mar 2015 12:10:08 -0400 Subject: [PATCH 0137/3836] Create swift container if it does not exist In the glance v2 image upload workflow, the user is not doing things with swift containers, swift containers are being created by shade. This means that it's shade's responsibility to ensure that the container exists before it attempts to upload something to it. Change-Id: I34cfc2821819aec105947dbe70a578307e583d77 --- shade/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index ad5eee2f5..4443956b1 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1450,6 +1450,9 @@ def create_object( if not filename: filename = name + # On some clouds this is not necessary. On others it is. I'm confused. + self.create_container(container) + if self.is_object_stale(container, name, filename, md5, sha256): with open(filename, 'r') as fileobj: self.log.debug( From f5c647e744773690312874a653f92752d8a7541f Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 31 Mar 2015 18:33:39 +0000 Subject: [PATCH 0138/3836] Let router update to specify external gw net ID One can also update the external gateway network ID on a router. Let's allow that. Change-Id: If1026c897687d285f01bcfd847bfc004fa720887 --- shade/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index 51f0d9649..4fc70772b 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -734,12 +734,14 @@ def create_router(self, name=None, admin_state_up=True): # a dict). return new_router['router'] - def update_router(self, router_id, name=None, admin_state_up=None): + def update_router(self, router_id, name=None, admin_state_up=None, + ext_gateway_net_id=None): """Update an existing logical router. :param router_id: The router UUID. :param name: The router name. :param admin_state_up: The administrative state of the router. + :param ext_gateway_net_id: The network ID for the external gateway. :returns: The router object. :raises: OpenStackCloudException on operation error. @@ -750,6 +752,10 @@ def update_router(self, router_id, name=None, admin_state_up=None): router['name'] = name if admin_state_up: router['admin_state_up'] = admin_state_up + if ext_gateway_net_id: + router['external_gateway_info'] = { + 'network_id': ext_gateway_net_id + } if not router: self.log.debug("No router data to update") From 5ce06cc5e999d26f1d25e90e4384baebed410ff0 Mon Sep 17 00:00:00 2001 From: Timothy Chavez Date: Tue, 3 Mar 2015 19:37:53 -0600 Subject: [PATCH 0139/3836] Add 'rebuild' to shade Adding the ability to rebuild instances via shade. While this is not used by nodepool currently, there is some interest in evaluating whether or not it could be a viable replacement for the current delete / create strategy employed by nodepool. Having rebuild in shade now will make benchmarks a little simpler. Change-Id: I12a00b71d248ce77e038dce1642d229587487886 --- shade/__init__.py | 27 ++++++ shade/tests/unit/test_rebuild_server.py | 105 ++++++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 shade/tests/unit/test_rebuild_server.py diff --git a/shade/__init__.py b/shade/__init__.py index ebe50d273..93543e7a4 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1432,6 +1432,33 @@ def create_server(self, auto_ip=True, ips=None, ip_pool=None, server=meta.obj_to_dict(server))) return server + def rebuild_server(self, server_id, image_id, wait=False, timeout=180): + try: + server = self.nova_client.servers.rebuild(server_id, image_id) + except Exception as e: + self.log.debug("nova instance rebuild failed", exc_info=True) + raise OpenStackCloudException( + "Error in rebuilding instance: {0}".format(e)) + if wait: + for count in _iterate_timeout( + timeout, + "Timeout waiting for server {0} to " + "rebuild.".format(server_id)): + try: + server = self.nova_client.servers.get(server_id) + except Exception: + continue + + if server.status == 'ACTIVE': + break + + if server.status == 'ERROR': + raise OpenStackCloudException( + "Error in rebuilding the server", + extra_data=dict( + server=meta.obj_to_dict(server))) + return server + def delete_server(self, name, wait=False, timeout=180): server_list = self.nova_client.servers.list(True, {'name': name}) if server_list: diff --git a/shade/tests/unit/test_rebuild_server.py b/shade/tests/unit/test_rebuild_server.py new file mode 100644 index 000000000..fad757bbe --- /dev/null +++ b/shade/tests/unit/test_rebuild_server.py @@ -0,0 +1,105 @@ +# -*- 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. + +""" +test_rebuild_server +---------------------------------- + +Tests for the `rebuild_server` command. +""" + +from mock import patch, Mock +from shade import ( + OpenStackCloud, OpenStackCloudException, OpenStackCloudTimeout) +from shade.tests import base + + +class TestRebuild(base.TestCase): + + def setUp(self): + super(TestRebuild, self).setUp() + self.client = OpenStackCloud("cloud", {}) + + def test_server_rebuild_rebuild_exception(self): + """ + Test that an exception in the novaclient rebuild raises an exception in + server_rebuild. + """ + with patch("shade.OpenStackCloud"): + config = { + "servers.rebuild.side_effect": Exception("exception"), + } + OpenStackCloud.nova_client = Mock(**config) + self.assertRaises( + OpenStackCloudException, self.client.rebuild_server, "a", "b") + + def test_server_rebuild_server_error(self): + """ + Test that a server error while waiting for the server to rebuild + raises an exception in server_rebuild. + """ + with patch("shade.OpenStackCloud"): + config = { + "servers.rebuild.return_value": Mock(status="REBUILD"), + "servers.get.return_value": Mock(status="ERROR") + } + OpenStackCloud.nova_client = Mock(**config) + self.assertRaises( + OpenStackCloudException, + self.client.rebuild_server, "a", "b", wait=True) + + def test_server_rebuild_timeout(self): + """ + Test that a timeout while waiting for the server to rebuild raises an + exception in server_rebuild. + """ + with patch("shade.OpenStackCloud"): + config = { + "servers.rebuild.return_value": Mock(status="REBUILD"), + "servers.get.return_value": Mock(status="REBUILD") + } + OpenStackCloud.nova_client = Mock(**config) + self.assertRaises( + OpenStackCloudTimeout, + self.client.rebuild_server, "a", "b", wait=True, timeout=1) + + def test_server_rebuild_no_wait(self): + """ + Test that server_rebuild with no wait and no exception in the + novaclient rebuild call returns the server instance. + """ + with patch("shade.OpenStackCloud"): + mock_server = Mock(status="ACTIVE") + config = { + "servers.rebuild.return_value": mock_server + } + OpenStackCloud.nova_client = Mock(**config) + self.assertEqual( + self.client.rebuild_server("a", "b"), mock_server) + + def test_server_rebuild_wait(self): + """ + Test that server_rebuild with a wait returns the server instance when + its status changes to "ACTIVE". + """ + with patch("shade.OpenStackCloud"): + mock_server = Mock(status="ACTIVE") + config = { + "servers.rebuild.return_value": Mock(status="REBUILD"), + "servers.get.return_value": mock_server + } + OpenStackCloud.nova_client = Mock(**config) + self.assertEqual( + self.client.rebuild_server("a", "b", wait=True), + mock_server) From c713eb3288fc89851df434ba9b3ddbaa84665682 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 6 Mar 2015 08:23:40 -0500 Subject: [PATCH 0140/3836] Allow for passing cache class in as a parameter It's possible you'd want to set the cache class either as a programmatic parameter to the constructor, or in the clouds.yaml config file. Facilitate both. Depends-On: I6c9eab71a88a534de7e52ad2a564450c44aacc1d Change-Id: I9575eae869881bf6784d2d867f2f571ddbab7769 --- shade/__init__.py | 47 +++++++++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 821109446..56def0e89 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -72,22 +72,37 @@ class OpenStackCloudTimeout(OpenStackCloudException): def openstack_clouds(config=None, debug=False): if not config: config = os_client_config.OpenStackConfig() - return [OpenStackCloud(cloud=f.name, debug=debug, **f.config) - for f in config.get_all_clouds()] + return [ + OpenStackCloud( + cloud=f.name, debug=debug, + cache_interval=config.get_cache_max_age(), + cache_class=config.get_cache_class(), + cache_arguments=config.get_cache_arguments(), + **f.config) + for f in config.get_all_clouds() + ] def openstack_cloud(debug=False, **kwargs): - cloud_config = os_client_config.OpenStackConfig().get_one_cloud( - **kwargs) + config = os_client_config.OpenStackConfig() + cloud_config = config.get_one_cloud(**kwargs) return OpenStackCloud( cloud=cloud_config.name, + cache_interval=config.get_cache_max_age(), + cache_class=config.get_cache_class(), + cache_arguments=config.get_cache_arguments(), debug=debug, **cloud_config.config) def operator_cloud(debug=False, **kwargs): - cloud_config = os_client_config.OpenStackConfig().get_one_cloud(**kwargs) + config = os_client_config.OpenStackConfig() + cloud_config = config.get_one_cloud(**kwargs) return OperatorCloud( - cloud_config.name, debug=debug, **cloud_config.config) + cloud_config.name, debug=debug, + cache_interval=config.get_cache_max_age(), + cache_class=config.get_cache_class(), + cache_arguments=config.get_cache_arguments(), + **cloud_config.config) def _ssl_args(verify, cacert, cert, key): @@ -173,6 +188,10 @@ class OpenStackCloud(object): Value will be passed to dogpile.cache. None means do not cache at all. (optional, defaults to None) + :param string cache_class: What dogpile.cache cache class to use. + (optional, defaults to dogpile.cache.null) + :param dict cache_arguments: Additional arguments to pass to the cache + constructor (optional, defaults to None) """ def __init__(self, cloud, auth, @@ -182,7 +201,10 @@ def __init__(self, cloud, auth, private=False, verify=True, cacert=None, cert=None, key=None, api_timeout=None, - debug=False, cache_interval=None, **kwargs): + debug=False, cache_interval=None, + cache_class='dogpile.cache.null', + cache_arguments=None, + **kwargs): self.name = cloud self.auth = auth @@ -199,16 +221,9 @@ def __init__(self, cloud, auth, (self.verify, self.cert) = _ssl_args(verify, cacert, cert, key) - # dogpile.cache.memory does not clear things out of the memory dict, - # so there is a possibility of memory bloat over time. By passing - # None a user will not get any in-memory cache and thus not have - # a large memory leak. - if cache_interval is None: - backend_name = 'dogpile.cache.null' - else: - backend_name = 'dogpile.cache.memory' self._cache = cache.make_region().configure( - backend_name, expiration_time=cache_interval) + cache_class, expiration_time=cache_interval, + arguments=cache_arguments) self._container_cache = dict() self._file_hash_cache = dict() From 1c756b8a570e09db6d0e35da8f4b6387dda84b77 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 28 Mar 2015 14:59:58 -0400 Subject: [PATCH 0141/3836] Namespace caching per cloud In places where we have multiple clouds, such as nodepool, if we want to use shared or persistent caching, we need to include cloud name in the cache so that we don't try to return rackspace flavors to hp clouds. Change-Id: I65a7b1bf6818f6b40b42bd2fa4b560f127c5589b --- shade/__init__.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 56def0e89..b80f09bab 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -221,7 +221,9 @@ def __init__(self, cloud, auth, (self.verify, self.cert) = _ssl_args(verify, cacert, cert, key) - self._cache = cache.make_region().configure( + self._cache = cache.make_region( + function_key_generator=self._make_cache_key + ).configure( cache_class, expiration_time=cache_interval, arguments=cache_arguments) self._container_cache = dict() @@ -247,6 +249,23 @@ def __init__(self, cloud, auth, self.log.setLevel(log_level) self.log.addHandler(logging.StreamHandler()) + def _make_cache_key(self, namespace, fn): + fname = fn.__name__ + if namespace is None: + name_key = self.name + else: + name_key = '%s:%s' % (self.name, namespace) + + def generate_key(*args, **kwargs): + arg_key = ','.join(args) + kwargs_keys = kwargs.keys() + kwargs_keys.sort() + kwargs_key = ','.join( + ['%s:%s' % (k, kwargs[k]) for k in kwargs_keys]) + return "_".join( + [name_key, fname, arg_key, kwargs_key]) + return generate_key + def get_service_type(self, service): return self.service_types.get(service, service) @@ -595,10 +614,10 @@ def get_region(self): @property def flavor_cache(self): @self._cache.cache_on_arguments() - def _flavor_cache(): + def _flavor_cache(cloud): return {flavor.id: flavor for flavor in self.nova_client.flavors.list()} - return _flavor_cache() + return _flavor_cache(self.name) def get_flavor_name(self, flavor_id): flavor = self.flavor_cache.get(flavor_id, None) From fbac3c07a2aaa97454319394435efeca2bc3b07d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 28 Mar 2015 19:16:09 -0400 Subject: [PATCH 0142/3836] Add task management framework Some shade clients, like nodepool, have a need to manage threads that are doing API tasks and to manage rate limiting. That's not really a thing that's appropriate for shade to know about, but because shade does want to know the business logic, we need to provide a mechanism for people to do that. TaskManager is essentially nodepool.TaskManager except with threading features removed and reworked to do instantaneous blocking execution, since that's the behavior that most users will expect. A patch will follow to move API calls to be manged by Tasks and the shade.TaskManager. Once those are there, then nodepool can pass in its TaskManager and the API operations in shade will naturally be managed by the rate-limited operations in nodepool. Co-Authored-By: James E. Blair Change-Id: I60d25271de4009ee3f7f7684c72299fbd5d0f54f --- shade/__init__.py | 11 ++++++ shade/task_manager.py | 81 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 shade/task_manager.py diff --git a/shade/__init__.py b/shade/__init__.py index b80f09bab..7a9b937de 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -42,6 +42,7 @@ warnings.filterwarnings('ignore', 'Certificate has no `subjectAltName`') from shade import meta +from shade import task_manager __version__ = pbr.version.VersionInfo('shade').version_string() OBJECT_MD5_KEY = 'x-object-meta-x-shade-md5' @@ -192,6 +193,10 @@ class OpenStackCloud(object): (optional, defaults to dogpile.cache.null) :param dict cache_arguments: Additional arguments to pass to the cache constructor (optional, defaults to None) + :param TaskManager manager: Optional task manager to use for running + OpenStack API tasks. Unless you're doing + rate limiting client side, you almost + certainly don't need this. (optional) """ def __init__(self, cloud, auth, @@ -204,6 +209,7 @@ def __init__(self, cloud, auth, debug=False, cache_interval=None, cache_class='dogpile.cache.null', cache_arguments=None, + manager=None, **kwargs): self.name = cloud @@ -213,6 +219,11 @@ def __init__(self, cloud, auth, self.endpoint_type = endpoint_type self.private = private self.api_timeout = api_timeout + if manager is not None: + self.manager = manager + else: + self.manager = task_manager.TaskManager( + name=self.name, client=self) self.service_types = _get_service_values(kwargs, 'service_type') self.service_names = _get_service_values(kwargs, 'service_name') diff --git a/shade/task_manager.py b/shade/task_manager.py new file mode 100644 index 000000000..9e410ba3f --- /dev/null +++ b/shade/task_manager.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python + +# Copyright (C) 2011-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 abc +import sys +import logging +import time + +import six + + +@six.add_metaclass(abc.ABCMeta) +class Task(object): + def __init__(self, **kw): + self._exception = None + self._traceback = None + self._result = None + self.args = kw + + @abc.abstractmethod + def main(self, client): + """ Override this method with the actual workload to be performed """ + + def done(self, result): + self._result = result + + def exception(self, e, tb): + self._exception = e + self._traceback = tb + + def wait(self): + if self._exception: + six.reraise(self._exception, None, self._traceback) + return self._result + + def run(self, client): + try: + self.done(self.main(client)) + except Exception as e: + self.exception(e, sys.exc_info()[2]) + + +class TaskManager(object): + log = logging.getLogger("shade.TaskManager") + + def __init__(self, client, name): + self.name = name + self._client = client + + def stop(self): + """ This is a direct action passthrough TaskManager """ + pass + + def run(self): + """ This is a direct action passthrough TaskManager """ + pass + + def submitTask(self, task): + self.log.debug( + "Manager %s running task %s" % (self.name, type(task).__name__)) + start = time.time() + task.run(self._client) + end = time.time() + self.log.debug( + "Manager %s ran task %s in %ss" % (self.name, task, (end - start))) + return task.wait() From d9f4965e6221b059df810347eaf7c12fce5e9d5d Mon Sep 17 00:00:00 2001 From: Timothy Chavez Date: Wed, 1 Apr 2015 23:18:03 -0500 Subject: [PATCH 0143/3836] Fix naming inconsistencies in rebuild_server tests There were a handful of naming inconsistencies in the rebuild_server tests that were corrected. Change-Id: I6d993892b140276294b00c9d92fc107569d8e740 --- shade/tests/unit/test_rebuild_server.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/shade/tests/unit/test_rebuild_server.py b/shade/tests/unit/test_rebuild_server.py index fad757bbe..6cba05f5d 100644 --- a/shade/tests/unit/test_rebuild_server.py +++ b/shade/tests/unit/test_rebuild_server.py @@ -25,16 +25,16 @@ from shade.tests import base -class TestRebuild(base.TestCase): +class TestRebuildServer(base.TestCase): def setUp(self): - super(TestRebuild, self).setUp() + super(TestRebuildServer, self).setUp() self.client = OpenStackCloud("cloud", {}) - def test_server_rebuild_rebuild_exception(self): + def test_rebuild_server_rebuild_exception(self): """ Test that an exception in the novaclient rebuild raises an exception in - server_rebuild. + rebuild_server. """ with patch("shade.OpenStackCloud"): config = { @@ -44,10 +44,10 @@ def test_server_rebuild_rebuild_exception(self): self.assertRaises( OpenStackCloudException, self.client.rebuild_server, "a", "b") - def test_server_rebuild_server_error(self): + def test_rebuild_server_server_error(self): """ Test that a server error while waiting for the server to rebuild - raises an exception in server_rebuild. + raises an exception in rebuild_server. """ with patch("shade.OpenStackCloud"): config = { @@ -59,10 +59,10 @@ def test_server_rebuild_server_error(self): OpenStackCloudException, self.client.rebuild_server, "a", "b", wait=True) - def test_server_rebuild_timeout(self): + def test_rebuild_server_timeout(self): """ Test that a timeout while waiting for the server to rebuild raises an - exception in server_rebuild. + exception in rebuild_server. """ with patch("shade.OpenStackCloud"): config = { @@ -74,9 +74,9 @@ def test_server_rebuild_timeout(self): OpenStackCloudTimeout, self.client.rebuild_server, "a", "b", wait=True, timeout=1) - def test_server_rebuild_no_wait(self): + def test_rebuild_server_no_wait(self): """ - Test that server_rebuild with no wait and no exception in the + Test that rebuild_server with no wait and no exception in the novaclient rebuild call returns the server instance. """ with patch("shade.OpenStackCloud"): @@ -88,9 +88,9 @@ def test_server_rebuild_no_wait(self): self.assertEqual( self.client.rebuild_server("a", "b"), mock_server) - def test_server_rebuild_wait(self): + def test_rebuild_server_wait(self): """ - Test that server_rebuild with a wait returns the server instance when + Test that rebuild_server with a wait returns the server instance when its status changes to "ACTIVE". """ with patch("shade.OpenStackCloud"): From 0a7479d8d997c1e69ba96e30194f4105ad0f8f7f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 28 Mar 2015 19:49:33 -0400 Subject: [PATCH 0144/3836] Migrate API calls to task management Now that we have a task management framework, make sure all of the API calls we make go through it. The net effect on people not taking advantage of the feature should be zero, but should allow for porting of nodepool to shade. Change-Id: I0f2cf912f45e76c9f5194e3b8813278b883b4adc --- shade/__init__.py | 177 +++++++++++++++++++---------------- shade/_tasks.py | 233 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 330 insertions(+), 80 deletions(-) create mode 100644 shade/_tasks.py diff --git a/shade/__init__.py b/shade/__init__.py index 7a9b937de..ea1ca8172 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -29,7 +29,6 @@ from keystoneclient import client as keystone_client from novaclient import client as nova_client from novaclient import exceptions as nova_exceptions -from novaclient.v2 import floating_ips from neutronclient.v2_0 import client as neutron_client import os_client_config import pbr.version @@ -43,6 +42,7 @@ from shade import meta from shade import task_manager +from shade import _tasks __version__ = pbr.version.VersionInfo('shade').version_string() OBJECT_MD5_KEY = 'x-object-meta-x-shade-md5' @@ -443,8 +443,8 @@ def delete_project(self, name_or_id): def user_cache(self): @self._cache.cache_on_arguments() def _user_cache(): - return {user.id: user for user in - self.keystone_client.users.list()} + user_list = self.manager.submitTask(_tasks.UserListTask()) + return {user.id: user for user in user_list} return _user_cache() def _get_user(self, name_or_id): @@ -482,9 +482,9 @@ def create_user( project_id = self._get_project(project).id else: project_id = None - self.keystone_client.users.create( + self.manager.submitTask(_tasks.UserCreate( user_name=name, password=password, email=email, - project=project_id, enabled=enabled) + project=project_id, enabled=enabled)) except Exception as e: self.log.debug("keystone create user issue", exc_info=True) raise OpenStackCloudException( @@ -627,7 +627,7 @@ def flavor_cache(self): @self._cache.cache_on_arguments() def _flavor_cache(cloud): return {flavor.id: flavor for flavor in - self.nova_client.flavors.list()} + self.manager.submitTask(_tasks.FlavorList())} return _flavor_cache(self.name) def get_flavor_name(self, flavor_id): @@ -668,24 +668,25 @@ def get_endpoint(self, service_type): return endpoint def list_servers(self): - return self.nova_client.servers.list() + return self.manager.submitTask(_tasks.ServerList()) def list_server_dicts(self): return [self.get_openstack_vars(server) for server in self.list_servers()] def list_keypairs(self): - return self.nova_client.keypairs.list() + return self.manager.submitTask(_tasks.KeypairList()) def list_keypair_dicts(self): return [meta.obj_to_dict(keypair) for keypair in self.list_keypairs()] def create_keypair(self, name, public_key): - return self.nova_client.keypairs.create(name, public_key) + return self.manager.submitTask(_tasks.KeypairCreate( + name=name, public_key=public_key)) def delete_keypair(self, name): - return self.nova_client.keypairs.delete(name) + return self.manager.submitTask(_tasks.KeypairDelete(key=name)) @property def extension_cache(self): @@ -693,7 +694,8 @@ def extension_cache(self): self._extension_cache = set() try: - resp, body = self.nova_client.client.get('/extensions') + resp, body = self.manager.submitTask( + _tasks.NovaUrlGet(url='/extensions')) if resp.status_code == 200: for x in body['extensions']: self._extension_cache.add(x['alias']) @@ -705,7 +707,7 @@ def has_extension(self, extension_name): return extension_name in self.extension_cache def list_networks(self): - return self.neutron_client.list_networks()['networks'] + return self.manager.submitTask(_tasks.NetworkList())['networks'] def get_network(self, name_or_id): for network in self.list_networks(): @@ -714,7 +716,7 @@ def get_network(self, name_or_id): return None def list_routers(self): - return self.neutron_client.list_routers()['routers'] + return self.manager.submitTask(_tasks.RouterList())['routers'] def get_router(self, name_or_id): for router in self.list_routers(): @@ -734,7 +736,6 @@ def create_network(self, name, shared=False, admin_state_up=True): :returns: The network object. :raises: OpenStackCloudException on operation error. """ - neutron = self.neutron_client network = { 'name': name, @@ -743,7 +744,8 @@ def create_network(self, name, shared=False, admin_state_up=True): } try: - net = neutron.create_network({'network': network}) + net = self.manager.submitTask( + _tasks.NetworkCreate(body=dict({'network': network}))) except Exception as e: self.log.debug("Network creation failed", exc_info=True) raise OpenStackCloudException( @@ -759,10 +761,10 @@ def delete_network(self, name_or_id): :param name_or_id: Name or ID of the network being deleted. :raises: OpenStackCloudException on operation error. """ - neutron = self.neutron_client network = self.get_network(name_or_id) try: - neutron.delete_network(network['id']) + self.manager.submitTask( + _tasks.NetworkDelete(network=network['id'])) except Exception as e: self.log.debug("Network deletion failed", exc_info=True) raise OpenStackCloudException( @@ -777,7 +779,6 @@ def create_router(self, name=None, admin_state_up=True): :returns: The router object. :raises: OpenStackCloudException on operation error. """ - neutron = self.neutron_client router = { 'admin_state_up': admin_state_up } @@ -785,7 +786,8 @@ def create_router(self, name=None, admin_state_up=True): router['name'] = name try: - new_router = neutron.create_router(dict(router=router)) + new_router = self.manager.submitTask( + _tasks.RouterCreate(body=dict(router=router))) except Exception as e: self.log.debug("Router create failed", exc_info=True) raise OpenStackCloudException( @@ -807,7 +809,6 @@ def update_router(self, router_id, name=None, admin_state_up=None, :returns: The router object. :raises: OpenStackCloudException on operation error. """ - neutron = self.neutron_client router = {} if name: router['name'] = name @@ -823,7 +824,9 @@ def update_router(self, router_id, name=None, admin_state_up=None, return try: - new_router = neutron.update_router(router_id, dict(router=router)) + new_router = self.manager.submitTask( + _tasks.RouterUpdate( + router=router_id, body=dict(router=router))) except Exception as e: self.log.debug("Router update failed", exc_info=True) raise OpenStackCloudException( @@ -843,8 +846,6 @@ def delete_router(self, name_or_id): :param name_or_id: Name or ID of the router being deleted. :raises: OpenStackCloudException on operation error. """ - neutron = self.neutron_client - routers = [] for router in self.list_routers(): if name_or_id in (router['id'], router['name']): @@ -859,7 +860,8 @@ def delete_router(self, name_or_id): "More than one router named %s. Use ID." % name_or_id) try: - neutron.delete_router(routers[0]['id']) + self.manager.submitTask( + _tasks.RouterDelete(router=routers[0]['id'])) except Exception as e: self.log.debug("Router delete failed", exc_info=True) raise OpenStackCloudException( @@ -869,14 +871,13 @@ def _get_images_from_cloud(self, filter_deleted): # First, try to actually get images from glance, it's more efficient images = dict() try: - # This can fail both because we don't have glanceclient installed - # and because the cloud may not expose the glance API publically - image_list = self.glance_client.images.list() + # If the cloud does not expose the glance API publically + image_list = self.manager.submitTask(_tasks.GlanceImageList()) except (OpenStackCloudException, glanceclient.exc.HTTPInternalServerError): # We didn't have glance, let's try nova # If this doesn't work - we just let the exception propagate - image_list = self.nova_client.images.list() + image_list = self.manager.submitTask(_tasks.NovaImageList()) for image in image_list: # The cloud might return DELETED for invalid images. # While that's cute and all, that's an implementation detail. @@ -934,8 +935,8 @@ def get_image_dict(self, name_or_id, exclude=None): return meta.obj_to_dict(image) def create_image_snapshot(self, name, **metadata): - image = self.nova_client.servers.create_image( - name, **metadata) + image = self.manager.submitTask(_tasks.ImageSnapshotCreate( + name=name, **metadata)) if image: return meta.obj_to_dict(image) return None @@ -972,8 +973,10 @@ def create_image( return self._upload_image_v1(name, filename, **image_kwargs) def _upload_image_v1(self, name, filename, **image_kwargs): - image = self.glance_client.images.create(name=name, **image_kwargs) - image.update(data=open(filename, 'rb')) + image = self.manager.submitTask(_tasks.ImageCreate( + name=name, **image_kwargs)) + self.manager.submitTask(_tasks.ImageUpdate( + image=image, data=open(filename, 'rb'))) self._cache.invalidate() return self.get_image_dict(image.id) @@ -990,26 +993,26 @@ def _upload_image_v2( # using glance properties to not delete then upload but instead make a # new "good" image and then mark the old one as "bad" # self.glance_client.images.delete(current_image) - task = self.glance_client.tasks.create( + glance_task = self.manager.submitTask(_tasks.ImageTaskCreate( type='import', input=dict( import_from='{container}/{name}'.format( container=container, name=name), - image_properties=dict(name=name))) + image_properties=dict(name=name)))) if wait: for count in _iterate_timeout( timeout, "Timeout waiting for the image to import."): try: - status = self.glance_client.tasks.get(task.id) + status = self.manager.submitTask( + _tasks.ImageTaskGet(task_id=glance_task.id)) except glanceclient.exc.HTTPServiceUnavailable: # Intermittent failure - catch and try again continue if status.status == 'success': self._reset_image_cache() - image = self.get_image(status.result['image_id']) self.update_image_properties( - image=image, + name_or_id=status.result['image_id'], **image_properties) return self.get_image_dict(status.result['image_id']) if status.status == 'failure': @@ -1018,11 +1021,11 @@ def _upload_image_v2( message=status.message), extra_data=status) else: - return meta.warlock_to_dict(task) + return meta.warlock_to_dict(glance_task) def update_image_properties( self, image=None, name_or_id=None, **properties): - if not image: + if image is None: image = self.get_image(name_or_id) img_props = {} @@ -1045,7 +1048,8 @@ def _update_image_properties_v2(self, image, properties): img_props[k] = str(v) if not img_props: return False - self.glance_client.images.update(image.id, **img_props) + self.manager.submitTask(_tasks.ImageUpdate( + image_id=image.id, **img_props)) return True def _update_image_properties_v1(self, image, properties): @@ -1055,10 +1059,11 @@ def _update_image_properties_v1(self, image, properties): img_props[k] = v if not img_props: return False - image.update(properties=img_props) + self.manager.submitTask(_tasks.ImageUpdate( + image=image, properties=img_props)) return True - def create_volume(self, wait=True, timeout=None, **volkwargs): + def create_volume(self, wait=True, timeout=None, **kwargs): """Create a volume. :param wait: If true, waits for volume to be created. @@ -1071,10 +1076,8 @@ def create_volume(self, wait=True, timeout=None, **volkwargs): :raises: OpenStackCloudException on operation error. """ - cinder = self.cinder_client - try: - volume = cinder.volumes.create(**volkwargs) + volume = self.manager.submitTask(_tasks.VolumeCreate(**kwargs)) except Exception as e: self.log.debug("Volume creation failed", exc_info=True) raise OpenStackCloudException( @@ -1111,11 +1114,11 @@ def delete_volume(self, name_or_id=None, wait=True, timeout=None): :raises: OpenStackCloudException on operation error. """ - cinder = self.cinder_client volume = self.get_volume(name_or_id, cache=False) try: - cinder.volumes.delete(volume.id) + volume = self.manager.submitTask( + _tasks.VolumeDelete(volume=volume.id)) except Exception as e: self.log.debug("Volume deletion failed", exc_info=True) raise OpenStackCloudException( @@ -1130,7 +1133,7 @@ def delete_volume(self, name_or_id=None, wait=True, timeout=None): def _get_volumes_from_cloud(self): try: - return self.cinder_client.volumes.list() + return self.manager.submitTask(_tasks.VolumeList()) except Exception: return [] @@ -1194,8 +1197,6 @@ def detach_volume(self, server, volume, wait=True, timeout=None): :raises: OpenStackCloudTimeout if wait time exceeded. :raises: OpenStackCloudException on operation error. """ - nova = self.nova_client - dev = self.get_volume_attach_device(volume, server.id) if not dev: raise OpenStackCloudException( @@ -1204,7 +1205,8 @@ def detach_volume(self, server, volume, wait=True, timeout=None): ) try: - nova.volumes.delete_server_volume(server.id, volume.id) + self.manager.submitTask( + _tasks.VolumeDetach(volume_id=volume, server_id=server.id)) except Exception as e: self.log.debug("nova volume detach failed", exc_info=True) raise OpenStackCloudException( @@ -1254,8 +1256,6 @@ def attach_volume(self, server, volume, device=None, :raises: OpenStackCloudTimeout if wait time exceeded. :raises: OpenStackCloudException on operation error. """ - nova = self.nova_client - dev = self.get_volume_attach_device(volume, server.id) if dev: raise OpenStackCloudException( @@ -1270,7 +1270,9 @@ def attach_volume(self, server, volume, device=None, ) try: - nova.volumes.create_server_volume(server.id, volume.id, device) + self.manager.submitTask( + _tasks.VolumeAttach( + volume_id=volume.id, server_id=server.id, device=device)) except Exception as e: self.log.debug( "nova volume attach of %s failed" % volume.id, exc_info=True) @@ -1332,7 +1334,7 @@ def get_server_meta(self, server): return dict(server_vars=server_vars, groups=groups) def get_security_group(self, name_or_id): - for secgroup in self.nova_client.security_groups.list(): + for secgroup in self.manager.submitTask(_tasks.SecurityGroupList()): if name_or_id in (secgroup.name, secgroup.id): return secgroup return None @@ -1342,16 +1344,13 @@ def get_openstack_vars(self, server): def add_ip_from_pool(self, server, pools): - # instantiate FloatingIPManager object - floating_ip_obj = floating_ips.FloatingIPManager(self.nova_client) - # empty dict and list usable_floating_ips = {} # get the list of all floating IPs. Mileage may # vary according to Nova Compute configuration # per cloud provider - all_floating_ips = floating_ip_obj.list() + all_floating_ips = self.manager.submitTask(_tasks.FloatingIPList()) # iterate through all pools of IP address. Empty # string means all and is the default value @@ -1369,7 +1368,8 @@ def add_ip_from_pool(self, server, pools): # if the list is empty, add for this pool if not pool_ips: try: - new_ip = self.nova_client.floating_ips.create(pool) + new_ip = self.manager.submitTask( + _tasks.FloatingIPCreate(pool=pool)) except Exception: self.log.debug( "nova floating ip create failed", exc_info=True) @@ -1392,7 +1392,8 @@ def add_ip_list(self, server, ips): # add ip(s) to instance for ip in ips: try: - server.add_floating_ip(ip) + self.manager.submitTask( + _tasks.FloatingIPAttach(server=server, address=ip)) except Exception as e: self.log.debug( "nova floating ip add failed", exc_info=True) @@ -1402,7 +1403,7 @@ def add_ip_list(self, server, ips): def add_auto_ip(self, server): try: - new_ip = self.nova_client.floating_ips.create() + new_ip = self.manager.submitTask(_tasks.FloatingIPCreate()) except Exception as e: self.log.debug( "nova floating ip create failed", exc_info=True) @@ -1413,7 +1414,8 @@ def add_auto_ip(self, server): except OpenStackCloudException: # Clean up - we auto-created this ip, and it's not attached # to the server, so the cloud will not know what to do with it - self.nova_client.floating_ips.delete(new_ip) + self.manager.submitTask( + _tasks.FloatingIPDelete(floating_ip=new_ip)) raise def add_ips_to_server(self, server, auto_ip=True, ips=None, ip_pool=None): @@ -1432,7 +1434,7 @@ def add_ips_to_server(self, server, auto_ip=True, ips=None, ip_pool=None): # floating IP, then it needs to be obtained from # a recent server object if the above code path exec'd try: - server = self.nova_client.servers.get(server.id) + server = self.manager.submitTask(_tasks.ServerGet(server=server)) except Exception as e: self.log.debug("nova info failed", exc_info=True) raise OpenStackCloudException( @@ -1454,8 +1456,8 @@ def create_server(self, auto_ip=True, ips=None, ip_pool=None, bootkwargs['block_device_mapping']['vda'] = volume_id try: - server = self.nova_client.servers.create(**bootkwargs) - server = self.nova_client.servers.get(server.id) + server = self.manager.submitTask(_tasks.ServerCreate(**bootkwargs)) + server = self.manager.submitTask(_tasks.ServerGet(server=server)) except Exception as e: self.log.debug("nova instance create failed", exc_info=True) raise OpenStackCloudException( @@ -1468,7 +1470,8 @@ def create_server(self, auto_ip=True, ips=None, ip_pool=None, timeout, "Timeout waiting for the server to come up."): try: - server = self.nova_client.servers.get(server.id) + server = self.manager.submitTask( + _tasks.ServerGet(server=server)) except Exception: continue @@ -1485,7 +1488,8 @@ def create_server(self, auto_ip=True, ips=None, ip_pool=None, def rebuild_server(self, server_id, image_id, wait=False, timeout=180): try: - server = self.nova_client.servers.rebuild(server_id, image_id) + server = self.manager.submitTask(_tasks.ServerRebuild( + server=server_id, image=image_id)) except Exception as e: self.log.debug("nova instance rebuild failed", exc_info=True) raise OpenStackCloudException( @@ -1496,7 +1500,8 @@ def rebuild_server(self, server_id, image_id, wait=False, timeout=180): "Timeout waiting for server {0} to " "rebuild.".format(server_id)): try: - server = self.nova_client.servers.get(server_id) + server = self.manager.submitTask( + _tasks.ServerGet(server=server)) except Exception: continue @@ -1511,23 +1516,28 @@ def rebuild_server(self, server_id, image_id, wait=False, timeout=180): return server def delete_server(self, name, wait=False, timeout=180): - server_list = self.nova_client.servers.list(True, {'name': name}) + # TODO(mordred): Why is this not using self.get_server()? + server_list = self.manager.submitTask(_tasks.ServerList( + detailed=True, search_opts={'name': name})) + # TODO(mordred): Why, after searching for a name, are we filtering + # again? if server_list: server = [x for x in server_list if x.name == name] - self.nova_client.servers.delete(server.pop()) + self.manager.submitTask(_tasks.ServerDelete(server=server.pop())) if not wait: return for count in _iterate_timeout( timeout, "Timed out waiting for server to get deleted."): - server = self.nova_client.servers.list(True, {'name': name}) + server = self.manager.submitTask(_tasks.ServerGet(server=server)) if not server: return def get_container(self, name, skip_cache=False): if skip_cache or name not in self._container_cache: try: - container = self.swift_client.head_container(name) + container = self.manager.submitTask( + _tasks.ContainerGet(container=name)) self._container_cache[name] = container except swift_exceptions.ClientException as e: if e.http_status == 404: @@ -1543,7 +1553,8 @@ def create_container(self, name, public=False): if container: return container try: - self.swift_client.put_container(name) + self.manager.submitTask( + _tasks.ContainerCreate(container=name)) if public: self.set_container_access(name, 'public') return self.get_container(name, skip_cache=True) @@ -1555,7 +1566,8 @@ def create_container(self, name, public=False): def delete_container(self, name): try: - self.swift_client.delete_container(name) + self.manager.submitTask( + _tasks.ContainerDelete(container=name)) except swift_exceptions.ClientException as e: if e.http_status == 404: return @@ -1566,7 +1578,8 @@ def delete_container(self, name): def update_container(self, name, headers): try: - self.swift_client.post_container(name, headers) + self.manager.submitTask( + _tasks.ContainerUpdate(container=name, headers=headers)) except swift_exceptions.ClientException as e: self.log.debug("swift container update failed", exc_info=True) raise OpenStackCloudException( @@ -1646,18 +1659,21 @@ def create_object( self.log.debug( "swift uploading {filename} to {container}/{name}".format( filename=filename, container=container, name=name)) - self.swift_client.put_object(container, name, contents=fileobj) + self.manager.submitTask(_tasks.ObjectCreate( + container=container, obj=name, contents=fileobj)) (md5, sha256) = self._get_file_hashes(filename) headers[OBJECT_MD5_KEY] = md5 headers[OBJECT_SHA256_KEY] = sha256 - self.swift_client.post_object(container, name, headers=headers) + self.manager.submitTask(_tasks.ObjectUpdate( + container=container, obj=name, headers=headers)) def delete_object(self, container, name): if not self.get_object_metadata(container, name): return try: - self.swift_client.delete_object(container, name) + self.manager.submitTask(_tasks.ObjectDelete( + container=container, obj=name)) except swift_exceptions.ClientException as e: raise OpenStackCloudException( "Object deletion failed: %s (%s/%s)" % ( @@ -1665,7 +1681,8 @@ def delete_object(self, container, name): def get_object_metadata(self, container, name): try: - return self.swift_client.head_object(container, name) + return self.manager.submitTask(_tasks.ObjectMetadata( + container=container, obj=name)) except swift_exceptions.ClientException as e: if e.http_status == 404: return None diff --git a/shade/_tasks.py b/shade/_tasks.py new file mode 100644 index 000000000..f20d4063b --- /dev/null +++ b/shade/_tasks.py @@ -0,0 +1,233 @@ +# Copyright (c) 2015 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. + +from shade import task_manager + + +class UserList(task_manager.Task): + def main(self, client): + return client.keystone_client.users.list() + + +class UserCreate(task_manager.Task): + def main(self, client): + return client.keystone_client.users.create(**self.args) + + +class FlavorList(task_manager.Task): + def main(self, client): + return client.nova_client.flavors.list() + + +class ServerList(task_manager.Task): + def main(self, client): + return client.nova_client.servers.list(**self.args) + + +class ServerGet(task_manager.Task): + def main(self, client): + return client.nova_client.servers.get(**self.args) + + +class ServerCreate(task_manager.Task): + def main(self, client): + return client.nova_client.servers.create(**self.args) + + +class ServerDelete(task_manager.Task): + def main(self, client): + return client.nova_client.servers.delete(**self.args) + + +class ServerRebuild(task_manager.Task): + def main(self, client): + return client.nova_client.servers.rebuild(**self.args) + + +class KeypairList(task_manager.Task): + def main(self, client): + return client.nova_client.keypairs.list() + + +class KeypairCreate(task_manager.Task): + def main(self, client): + return client.nova_client.keypairs.create(**self.args) + + +class KeypairDelete(task_manager.Task): + def main(self, client): + return client.nova_client.keypairs.delete(**self.args) + + +class NovaUrlGet(task_manager.Task): + def main(self, client): + return client.nova_client.client.get(**self.args) + + +class NetworkList(task_manager.Task): + def main(self, client): + return client.neutron_client.list_networks() + + +class NetworkCreate(task_manager.Task): + def main(self, client): + return client.neutron_client.create_network(**self.args) + + +class NetworkDelete(task_manager.Task): + def main(self, client): + return client.neutron_client.delete_network(**self.args) + + +class RouterList(task_manager.Task): + def main(self, client): + return client.neutron_client.list_routers() + + +class RouterCreate(task_manager.Task): + def main(self, client): + return client.neutron_client.create_router(**self.args) + + +class RouterUpdate(task_manager.Task): + def main(self, client): + return client.neutron_client.update_router(**self.args) + + +class RouterDelete(task_manager.Task): + def main(self, client): + client.neutron_client.delete_router(**self.args) + + +class GlanceImageList(task_manager.Task): + def main(self, client): + return client.glance_client.images.list() + + +class NovaImageList(task_manager.Task): + def main(self, client): + return client.nova_client.images.list() + + +class ImageSnapshotCreate(task_manager.Task): + def main(self, client): + return client.nova_client.servers.create_image(**self.args) + + +class ImageCreate(task_manager.Task): + def main(self, client): + return client.glance_client.images.create(**self.args) + + +class ImageTaskCreate(task_manager.Task): + def main(self, client): + return client.glance_client.tasks.create(**self.args) + + +class ImageTaskGet(task_manager.Task): + def main(self, client): + return client.glance_client.tasks.get(**self.args) + + +class ImageUpdate(task_manager.Task): + def main(self, client): + client.glance_client.images.update(**self.args) + + +class VolumeCreate(task_manager.Task): + def main(self, client): + return client.cinder_client.volumes.create(**self.args) + + +class VolumeDelete(task_manager.Task): + def main(self, client): + return client.cinder_client.volumes.delete(**self.args) + + +class VolumeList(task_manager.Task): + def main(self, client): + return client.cinder_client.volumes.list() + + +class VolumeDetach(task_manager.Task): + def main(self, client): + client.nova_client.volumes.delete_server_volume(**self.args) + + +class VolumeAttach(task_manager.Task): + def main(self, client): + client.nova_client.volumes.create_server_volume(**self.args) + + +class SecurityGroupList(task_manager.Task): + def main(self, client): + return client.nova_client.security_groups.list() + + +# TODO: Do this with neutron instead of nova if possible +class FloatingIPList(task_manager.Task): + def main(self, client): + return client.nova_client.floating_ips.list() + + +class FloatingIPCreate(task_manager.Task): + def main(self, client): + return client.nova_client.floating_ips.create(**self.args) + + +class FloatingIPDelete(task_manager.Task): + def main(self, client): + return client.nova_client.floating_ips.delete(**self.args) + + +class FloatingIPAttach(task_manager.Task): + def main(self, client): + return client.nova_client.servers.add_floating_ip(**self.args) + + +class ContainerGet(task_manager.Task): + def main(self, client): + return client.swift_client.head_container(**self.args) + + +class ContainerCreate(task_manager.Task): + def main(self, client): + client.swift_client.put_container(**self.args) + + +class ContainerDelete(task_manager.Task): + def main(self, client): + client.swift_client.delete_container(**self.args) + + +class ContainerUpdate(task_manager.Task): + def main(self, client): + client.swift_client.post_container(**self.args) + + +class ObjectCreate(task_manager.Task): + def main(self, client): + client.swift_client.put_object(**self.args) + + +class ObjectUpdate(task_manager.Task): + def main(self, client): + client.swift_client.post_object(**self.args) + + +class ObjectMetadata(task_manager.Task): + def main(self, client): + return client.swift_client.head_object(**self.args) From 4fbc21bc61f4c46cbcdb707b07b107051993c1c6 Mon Sep 17 00:00:00 2001 From: Timothy Chavez Date: Thu, 2 Apr 2015 01:36:00 -0500 Subject: [PATCH 0145/3836] Add some unit test for create_server Begin adding unit tests to other shade methods starting with create_server. Change-Id: Ie68d91ce7bc56063aa8d0001de4ff37139a35b1f --- shade/__init__.py | 2 +- shade/tests/unit/test_create_server.py | 138 +++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 shade/tests/unit/test_create_server.py diff --git a/shade/__init__.py b/shade/__init__.py index 821109446..907eb68b6 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1414,7 +1414,7 @@ def create_server(self, auto_ip=True, ips=None, ip_pool=None, except Exception as e: self.log.debug("nova instance create failed", exc_info=True) raise OpenStackCloudException( - "Error in creating instance: %s" % e.message) + "Error in creating instance: {0}".format(e)) if server.status == 'ERROR': raise OpenStackCloudException( "Error in creating the server.") diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py new file mode 100644 index 000000000..cca1570ff --- /dev/null +++ b/shade/tests/unit/test_create_server.py @@ -0,0 +1,138 @@ +# -*- 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. + +""" +test_create_server +---------------------------------- + +Tests for the `create_server` command. +""" + +from mock import patch, Mock +from shade import ( + OpenStackCloud, OpenStackCloudException, OpenStackCloudTimeout) +from shade.tests import base + + +class TestCreateServer(base.TestCase): + + def setUp(self): + super(TestCreateServer, self).setUp() + self.client = OpenStackCloud("cloud", {}) + + def test_create_server_with_create_exception(self): + """ + Test that an exception in the novaclient create raises an exception in + create_server. + """ + with patch("shade.OpenStackCloud"): + config = { + "servers.create.side_effect": Exception("exception"), + } + OpenStackCloud.nova_client = Mock(**config) + self.assertRaises( + OpenStackCloudException, self.client.create_server) + + def test_create_server_with_get_exception(self): + """ + Test that an exception when attempting to get the server instance via + the novaclient raises an exception in create_server. + """ + with patch("shade.OpenStackCloud"): + config = { + "servers.create.return_value": Mock(status="BUILD"), + "servers.get.side_effect": Exception("exception") + } + OpenStackCloud.nova_client = Mock(**config) + self.assertRaises( + OpenStackCloudException, self.client.create_server) + + def test_create_server_with_server_error(self): + """ + Test that a server error before we return or begin waiting for the + server instance spawn raises an exception in create_server. + """ + with patch("shade.OpenStackCloud"): + config = { + "servers.create.return_value": Mock(status="BUILD"), + "servers.get.return_value": Mock(status="ERROR") + } + OpenStackCloud.nova_client = Mock(**config) + self.assertRaises( + OpenStackCloudException, self.client.create_server) + + def test_create_server_wait_server_error(self): + """ + Test that a server error while waiting for the server to spawn + raises an exception in create_server. + """ + with patch("shade.OpenStackCloud"): + config = { + "servers.create.return_value": Mock(status="BUILD"), + "servers.get.side_effect": [ + Mock(status="BUILD"), Mock(status="ERROR")] + } + OpenStackCloud.nova_client = Mock(**config) + self.assertRaises( + OpenStackCloudException, + self.client.create_server, wait=True) + + def test_create_server_with_timeout(self): + """ + Test that a timeout while waiting for the server to spawn raises an + exception in create_server. + """ + with patch("shade.OpenStackCloud"): + config = { + "servers.create.return_value": Mock(status="BUILD"), + "servers.get.return_value": Mock(status="BUILD") + } + OpenStackCloud.nova_client = Mock(**config) + self.assertRaises( + OpenStackCloudTimeout, + self.client.create_server, wait=True, timeout=1) + + def test_create_server_no_wait(self): + """ + Test that create_server with no wait and no exception in the + novaclient create call returns the server instance. + """ + with patch("shade.OpenStackCloud"): + mock_server = Mock(status="BUILD") + config = { + "servers.create.return_value": mock_server, + "servers.get.return_value": mock_server + } + OpenStackCloud.nova_client = Mock(**config) + self.assertEqual( + self.client.create_server(), mock_server) + + def test_create_server_wait(self): + """ + Test that create_server with a wait returns the server instance when + its status changes to "ACTIVE". + """ + with patch("shade.OpenStackCloud"): + mock_server = Mock(status="ACTIVE") + config = { + "servers.create.return_value": Mock(status="BUILD"), + "servers.get.side_effect": [ + Mock(status="BUILD"), mock_server] + } + OpenStackCloud.nova_client = Mock(**config) + with patch.object(OpenStackCloud, "add_ips_to_server", + return_value=mock_server): + self.assertEqual( + self.client.create_server(wait=True), + mock_server) From a0d0d7139007655f7d3f6efbce7f0d1ecb6ded38 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 1 Apr 2015 11:19:46 -0400 Subject: [PATCH 0146/3836] Skip passing in timeout to glance if it's not set glanceclient wants to do a float type validation, which is laudable. However, it means we can't pass in timeout=None, and instead have to avoid passing in the parameter if we do not have it set. Change-Id: Ie6e144d6e3cedf5bf8846beefcfda0f3252377e6 --- shade/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index 9037a3dda..18cb6f338 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -526,11 +526,14 @@ def glance_client(self): token = self.auth_token endpoint = self._get_glance_endpoint() glance_api_version = self._get_glance_api_version() + kwargs = dict() + if self.api_timeout is not None: + kwargs['timeout'] = self.api_timeout try: self._glance_client = glanceclient.Client( glance_api_version, endpoint, token=token, session=self.keystone_session, - timeout=self.api_timeout) + **kwargs) except Exception as e: self.log.debug("glance unknown issue", exc_info=True) raise OpenStackCloudException( From b03f1f3b59236370349e84c4895ee4dca18e555f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 1 Apr 2015 10:44:14 -0400 Subject: [PATCH 0147/3836] Add delete_image call Lo and behold! Deleting images works differently between glance v1 and v2. Of course it does. Change-Id: I9eb97950507d95076342ef3cb56205ff505f7fae --- shade/__init__.py | 25 +++++++++++++++++++++++++ shade/_tasks.py | 5 +++++ 2 files changed, 30 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 18cb6f338..94e00a2ee 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -944,6 +944,31 @@ def create_image_snapshot(self, name, **metadata): return meta.obj_to_dict(image) return None + def delete_image(self, name_or_id, wait=False, timeout=3600): + image = self.get_image(name_or_id) + try: + # Note that in v1, the param name is image, but in v2, + # it's image_id + glance_api_version = self._get_glance_api_version() + if glance_api_version == '2': + self.manager.submitTask( + _tasks.ImageDelete(image_id=image.id)) + elif glance_api_version == '1': + self.manager.submitTask( + _tasks.ImageDelete(image=image.id)) + except Exception as e: + self.log.debug("Image deletion failed", exc_info=True) + raise OpenStackCloudException( + "Error in deleting image: %s" % e.message) + + if wait: + for count in _iterate_timeout( + timeout, + "Timeout waiting for the image to be deleted."): + self._cache.invalidate() + if self.get_image(image.id) is None: + return + def create_image( self, name, filename, container='images', md5=None, sha256=None, diff --git a/shade/_tasks.py b/shade/_tasks.py index f20d4063b..be4d0c8fd 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -132,6 +132,11 @@ def main(self, client): return client.glance_client.images.create(**self.args) +class ImageDelete(task_manager.Task): + def main(self, client): + return client.glance_client.images.delete(**self.args) + + class ImageTaskCreate(task_manager.Task): def main(self, client): return client.glance_client.tasks.create(**self.args) From 7eb7c4c89a4dcf18f34bd1004b700b45f3cc10f4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 1 Apr 2015 11:17:31 -0400 Subject: [PATCH 0148/3836] Poll on the actual image showing up Turns out just polling on the task completion doesn't mean you actually have the image. You also need to poll on the image becoming a thing in glance. Change-Id: I910ac612e6af1f5888c1820a09ee79fab012bdae --- shade/__init__.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 94e00a2ee..4249cbecc 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1027,20 +1027,30 @@ def _upload_image_v2( container=container, name=name), image_properties=dict(name=name)))) if wait: + image_id = None for count in _iterate_timeout( timeout, "Timeout waiting for the image to import."): try: - status = self.manager.submitTask( - _tasks.ImageTaskGet(task_id=glance_task.id)) + if image_id is None: + status = self.manager.submitTask( + _tasks.ImageTaskGet(task_id=glance_task.id)) except glanceclient.exc.HTTPServiceUnavailable: # Intermittent failure - catch and try again continue if status.status == 'success': + image_id = status.result['image_id'] self._reset_image_cache() + try: + image = self.get_image(image_id) + except glanceclient.exc.HTTPServiceUnavailable: + # Intermittent failure - catch and try again + continue + if image is None: + continue self.update_image_properties( - name_or_id=status.result['image_id'], + image=image, **image_properties) return self.get_image_dict(status.result['image_id']) if status.status == 'failure': From 8c3601474b2c5aa0c52bb1f85f9dc5caa1f048ed Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 2 Apr 2015 08:36:42 -0400 Subject: [PATCH 0149/3836] Fix docs nit - make it clear the arg is a string jeblair made a good point in a previous review, there is a string argument that the docs for made it look like you'd pass in a class. Change-Id: Icc9449f75165a3ef8d9920b68eb23c830f8198f0 --- shade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index 4249cbecc..02e742bf6 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -190,7 +190,7 @@ class OpenStackCloud(object): means do not cache at all. (optional, defaults to None) :param string cache_class: What dogpile.cache cache class to use. - (optional, defaults to dogpile.cache.null) + (optional, defaults to "dogpile.cache.null") :param dict cache_arguments: Additional arguments to pass to the cache constructor (optional, defaults to None) :param TaskManager manager: Optional task manager to use for running From 5ca933aa7f0959f478dafeeebd2d3695d4026d0a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 2 Apr 2015 13:48:10 -0400 Subject: [PATCH 0150/3836] Add API auto-generation based on docstrings We've been adding docstrings, maybe we should put them into our documentation. Change-Id: I11ee33e1b87854403ec8b4e35f338c2c604d7fa4 --- doc/source/conf.py | 2 ++ doc/source/index.rst | 3 ++- doc/source/readme.rst | 1 - doc/source/usage.rst | 5 ++++- test-requirements.txt | 4 ++-- 5 files changed, 10 insertions(+), 5 deletions(-) delete mode 100644 doc/source/readme.rst diff --git a/doc/source/conf.py b/doc/source/conf.py index 97d3c2ff4..bbebf4729 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -3,6 +3,8 @@ sys.path.insert(0, os.path.abspath('../..')) +extensions = ['sphinx.ext.autodoc', 'oslosphinx'] + # The suffix of source filenames. source_suffix = '.rst' diff --git a/doc/source/index.rst b/doc/source/index.rst index e8b236528..15a603f40 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -11,11 +11,12 @@ Contents: .. toctree:: :maxdepth: 2 - readme installation usage contributing +.. include:: ../../README.rst + Indices and tables ================== diff --git a/doc/source/readme.rst b/doc/source/readme.rst deleted file mode 100644 index 6b2b3ec68..000000000 --- a/doc/source/readme.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../README.rst \ No newline at end of file diff --git a/doc/source/usage.rst b/doc/source/usage.rst index 96c1020df..75dfc88ae 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -4,4 +4,7 @@ Usage To use shade in a project:: - import shade \ No newline at end of file + import shade + +.. automodule:: shade + :members: diff --git a/test-requirements.txt b/test-requirements.txt index 6957d91b4..b6a09015e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,8 +5,8 @@ discover fixtures>=0.3.14 mock>=1.0 python-subunit -sphinx>=1.1.2 -oslo.sphinx +oslosphinx>=2.2.0 # Apache-2.0 +sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 testrepository>=0.0.17 testscenarios>=0.4,<0.5 testtools>=0.9.32 From 05119822dd4200ebb16cce84d16005679df6938b Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 3 Apr 2015 14:14:12 +0000 Subject: [PATCH 0151/3836] Fix exception in update_router() Correct variable used for router identification in exception message. The 'name' is optional, so use 'router_id'. Change-Id: I606b397019e73dd5b77695932f907d8ba31ca3dc --- shade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index 02e742bf6..af3f9ee44 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -833,7 +833,7 @@ def update_router(self, router_id, name=None, admin_state_up=None, except Exception as e: self.log.debug("Router update failed", exc_info=True) raise OpenStackCloudException( - "Error updating router %s: %s" % (name, e)) + "Error updating router %s: %s" % (router_id, e)) # Turns out neutron returns an actual dict, so no need for the # use of meta.obj_to_dict() here (which would not work against # a dict). From ff5444e75913d29c828894759bf9407faddc468e Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 7 Apr 2015 14:20:49 -0400 Subject: [PATCH 0152/3836] Fix volume operations The recent change to use Tasks has broken some volume operations. This corrects them. Change-Id: I29c40d978d4a5db0e8ee0daf4f4e2aa2f0ab9f66 --- shade/__init__.py | 5 +++-- shade/_tasks.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 02e742bf6..4f94d189a 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1155,7 +1155,7 @@ def delete_volume(self, name_or_id=None, wait=True, timeout=None): volume = self.get_volume(name_or_id, cache=False) try: - volume = self.manager.submitTask( + self.manager.submitTask( _tasks.VolumeDelete(volume=volume.id)) except Exception as e: self.log.debug("Volume deletion failed", exc_info=True) @@ -1244,7 +1244,8 @@ def detach_volume(self, server, volume, wait=True, timeout=None): try: self.manager.submitTask( - _tasks.VolumeDetach(volume_id=volume, server_id=server.id)) + _tasks.VolumeDetach(attachment_id=volume.id, + server_id=server.id)) except Exception as e: self.log.debug("nova volume detach failed", exc_info=True) raise OpenStackCloudException( diff --git a/shade/_tasks.py b/shade/_tasks.py index be4d0c8fd..d250eb413 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -159,7 +159,7 @@ def main(self, client): class VolumeDelete(task_manager.Task): def main(self, client): - return client.cinder_client.volumes.delete(**self.args) + client.cinder_client.volumes.delete(**self.args) class VolumeList(task_manager.Task): From 854efa48c6d8d739bb0183108187756338b2b429 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 7 Apr 2015 15:14:55 -0400 Subject: [PATCH 0153/3836] Update os-client-config min version The os-client-config library changed its default dogpile backend from memory to null after 0.6.0 was released. Caching is pretty much broken in shade, at the moment, so let's make 0.7.0 the minimum version. Change-Id: I640b0f50b195bd63db185a64d2a5223781f424d1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 95dabd739..54786dcff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ pbr>=0.5.21,<1.0 -os-client-config>=0.5.0 +os-client-config>=0.7.0 six python-novaclient>=2.21.0 From b5bde90d963d4511d24bfd4967572d4c81027e32 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 7 Apr 2015 19:33:55 -0400 Subject: [PATCH 0154/3836] Rename auth_plugin to auth_type This is already auth_type in ansible and in os-client-config. We never noticed here because it defaults to password and we never pass in anything else and we pass it as a positional parameter. BUT - it's broken. Change-Id: I677a63489f818fdb46d179ebeb7f5286fb4fb0b7 --- shade/__init__.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 02e742bf6..fb007f0aa 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -160,12 +160,12 @@ class OpenStackCloud(object): :param string name: The name of the cloud :param dict auth: Dictionary containing authentication information. - Depending on the value of auth_plugin, the contents + Depending on the value of auth_type, the contents of this dict can vary wildly. :param string region_name: The region of the cloud that all operations should be performed against. (optional, default '') - :param string auth_plugin: The name of the keystone auth_plugin to be used + :param string auth_type: The name of the keystone auth_type to be used :param string endpoint_type: The type of endpoint to get for services from the service catalog. Valid types are `public` ,`internal` or `admin`. (optional, @@ -201,7 +201,7 @@ class OpenStackCloud(object): def __init__(self, cloud, auth, region_name='', - auth_plugin='password', + auth_type='password', endpoint_type='public', private=False, verify=True, cacert=None, cert=None, key=None, @@ -215,7 +215,7 @@ def __init__(self, cloud, auth, self.name = cloud self.auth = auth self.region_name = region_name - self.auth_plugin = auth_plugin + self.auth_type = auth_type self.endpoint_type = endpoint_type self.private = private self.api_timeout = api_timeout @@ -318,12 +318,12 @@ def keystone_session(self): keystone_logging.addHandler(logging.NullHandler()) try: - auth_plugin = ksc_auth.get_plugin_class(self.auth_plugin) + auth_plugin = ksc_auth.get_plugin_class(self.auth_type) except Exception as e: self.log.debug("keystone auth plugin failure", exc_info=True) raise OpenStackCloudException( "Could not find auth plugin: {plugin}".format( - plugin=self.auth_plugin)) + plugin=self.auth_type)) try: keystone_auth = auth_plugin(**self.auth) except Exception as e: @@ -331,7 +331,7 @@ def keystone_session(self): "keystone couldn't construct plugin", exc_info=True) raise OpenStackCloudException( "Error constructing auth plugin: {plugin}".format( - plugin=self.auth_plugin)) + plugin=self.auth_type)) try: self._keystone_session = ksc_session.Session( @@ -1737,7 +1737,7 @@ class OperatorCloud(OpenStackCloud): @property def auth_token(self): - if self.auth_plugin in (None, "None", ''): + if self.auth_type in (None, "None", ''): return self._auth_token if not self._auth_token: self._auth_token = self.keystone_session.get_token() @@ -1749,7 +1749,7 @@ def ironic_client(self): ironic_logging = logging.getLogger('ironicclient') ironic_logging.addHandler(logging.NullHandler()) token = self.auth_token - if self.auth_plugin in (None, "None", ''): + if self.auth_type in (None, "None", ''): # TODO: This needs to be improved logic wise, perhaps a list, # or enhancement of the data stuctures with-in the library # to allow for things aside password authentication, or no From 021c75f39ebe232abd06f0033fa3e7ecb1a6bcbe Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Wed, 8 Apr 2015 12:58:57 -0700 Subject: [PATCH 0155/3836] Add tests for caching Currently we don't actually test caching as it is expected to happen. This test added coverage which found a python3 incompatibility in the key generation function, which has been corrected. Change-Id: Ifc7d5318350ef069c2e8f4dcc1ad96b05c2cffa2 --- shade/__init__.py | 3 +- shade/tests/unit/test_caching.py | 71 ++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 shade/tests/unit/test_caching.py diff --git a/shade/__init__.py b/shade/__init__.py index b5edae925..a5a6831e0 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -269,8 +269,7 @@ def _make_cache_key(self, namespace, fn): def generate_key(*args, **kwargs): arg_key = ','.join(args) - kwargs_keys = kwargs.keys() - kwargs_keys.sort() + kwargs_keys = sorted(kwargs.keys()) kwargs_key = ','.join( ['%s:%s' % (k, kwargs[k]) for k in kwargs_keys]) return "_".join( diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py new file mode 100644 index 000000000..6307de98d --- /dev/null +++ b/shade/tests/unit/test_caching.py @@ -0,0 +1,71 @@ +# -*- 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. +import tempfile + +import mock +import os_client_config as occ +import yaml + +import shade +from shade.tests import base + + +class TestMemoryCache(base.TestCase): + + CACHING_CONFIG = { + 'cache': + { + 'max_age': 10, + 'class': 'dogpile.cache.memory', + }, + } + + def setUp(self): + super(TestMemoryCache, self).setUp() + + # Isolate os-client-config from test environment + config = tempfile.NamedTemporaryFile(delete=False) + config.write(bytes(yaml.dump(self.CACHING_CONFIG).encode('utf-8'))) + config.close() + vendor = tempfile.NamedTemporaryFile(delete=False) + vendor.write(b'{}') + vendor.close() + + self.cloud_config = occ.OpenStackConfig(config_files=[config.name], + vendor_files=[vendor.name]) + self.cloud = shade.openstack_cloud(config=self.cloud_config) + + def test_openstack_cloud(self): + self.assertIsInstance(self.cloud, shade.OpenStackCloud) + + @mock.patch('shade.OpenStackCloud.keystone_client') + def test_project_cache(self, keystone_mock): + mock_project = mock.MagicMock() + mock_project.id = 'project_a' + keystone_mock.projects.list.return_value = [mock_project] + self.assertEqual({'project_a': mock_project}, self.cloud.project_cache) + mock_project_b = mock.MagicMock() + mock_project_b.id = 'project_b' + keystone_mock.projects.list.return_value = [mock_project, + mock_project_b] + # Caching should hide this from us until we invalidate + # TODO(clint) fix os-client-config which doesn't actually let us use + # the memory cache. + # TODO(clint) refactor to allow invalidating just the project cache. + #self.assertEqual( + # {'project_a': mock_project}, self.cloud.project_cache) + #self.cloud._cache.invalidate() + #self.assertEqual( + # {'project_a': mock_project, + # 'project_b': mock_project_b}, self.cloud.project_cache) From da1e06f6965c5fb1ed1a510074072df688645ad4 Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Wed, 8 Apr 2015 13:06:32 -0700 Subject: [PATCH 0156/3836] Refactor caching to allow per-method invalidate Previously dogpile caching was hidden in private functions, but this is now how dogpile intends for the methods to be wrapped. Unfortunately, the property decorator also seems to make it impossible for dogpile to add the invalidate method, so properties with caching will need a non-property equivalent method added to the API. Change-Id: I0f10d8f7f1c6847d6122b7631306db292eaa8b8f --- shade/__init__.py | 54 +++++++++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index a5a6831e0..a5e88ed7f 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -148,6 +148,21 @@ def _iterate_timeout(timeout, message): raise OpenStackCloudTimeout(message) +def _cache_on_arguments(func): + def _cache_decorator(obj, *args, **kwargs): + the_method = obj._cache.cache_on_arguments()( + func.__get__(obj, type(obj))) + return the_method(*args, **kwargs) + + def invalidate(obj, *args, **kwargs): + return obj._cache.cache_on_arguments()(func).invalidate( + *args, **kwargs) + + _cache_decorator.invalidate = invalidate + + return _cache_decorator + + class OpenStackCloud(object): """Represent a connection to an OpenStack Cloud. @@ -372,11 +387,12 @@ def auth_token(self): @property def project_cache(self): - @self._cache.cache_on_arguments() - def _project_cache(): - return {project.id: project for project in - self._project_manager.list()} - return _project_cache() + return self.get_project_cache() + + @_cache_on_arguments + def get_project_cache(self): + return {project.id: project for project in + self._project_manager.list()} @property def _project_manager(self): @@ -439,12 +455,10 @@ def delete_project(self, name_or_id): project=name_or_id, message=e.message)) @property + @_cache_on_arguments def user_cache(self): - @self._cache.cache_on_arguments() - def _user_cache(): - user_list = self.manager.submitTask(_tasks.UserListTask()) - return {user.id: user for user in user_list} - return _user_cache() + user_list = self.manager.submitTask(_tasks.UserListTask()) + return {user.id: user for user in user_list} def _get_user(self, name_or_id): """Retrieve a user by name or id.""" @@ -625,12 +639,10 @@ def get_region(self): return self.region_name @property + @_cache_on_arguments def flavor_cache(self): - @self._cache.cache_on_arguments() - def _flavor_cache(cloud): - return {flavor.id: flavor for flavor in - self.manager.submitTask(_tasks.FlavorList())} - return _flavor_cache(self.name) + return {flavor.id: flavor for flavor in + self.manager.submitTask(_tasks.FlavorList())} def get_flavor_name(self, flavor_id): flavor = self.flavor_cache.get(flavor_id, None) @@ -892,16 +904,14 @@ def _get_images_from_cloud(self, filter_deleted): def _reset_image_cache(self): self._image_cache = None + @_cache_on_arguments def list_images(self, filter_deleted=True): """Get available glance images. :param filter_deleted: Control whether deleted images are returned. :returns: A dictionary of glance images indexed by image UUID. """ - @self._cache.cache_on_arguments() - def _list_images(): - return self._get_images_from_cloud(filter_deleted) - return _list_images() + return self._get_images_from_cloud(filter_deleted) def get_image_name(self, image_id, exclude=None): image = self.get_image(image_id, exclude) @@ -1174,11 +1184,9 @@ def _get_volumes_from_cloud(self): except Exception: return [] + @_cache_on_arguments def list_volumes(self, cache=True): - @self._cache.cache_on_arguments() - def _list_volumes(): - return self._get_volumes_from_cloud() - return _list_volumes() + return self._get_volumes_from_cloud() def get_volumes(self, server, cache=True): volumes = [] From 0478c32706d71f4a0e67de3fb84828f30b432363 Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Wed, 8 Apr 2015 15:47:35 -0700 Subject: [PATCH 0157/3836] Allow passing config into shade.openstack_cloud For some reason one cannot pass in a pre-defined configuration into shade.openstack_cloud. This made it very hard to use that function for testing. This also allows us to fully enable the caching test, since the memory backend is now used as configured in the test. Change-Id: I4d88537274d3163b975c6176a91a1f4617485c23 --- shade/__init__.py | 4 +++- shade/tests/unit/test_caching.py | 30 ++++++++++++++++++++---------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index a5e88ed7f..2993a78e2 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -85,7 +85,9 @@ def openstack_clouds(config=None, debug=False): def openstack_cloud(debug=False, **kwargs): - config = os_client_config.OpenStackConfig() + config = kwargs.get('config') + if config is None: + config = os_client_config.OpenStackConfig() cloud_config = config.get_one_cloud(**kwargs) return OpenStackCloud( cloud=cloud_config.name, diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 6307de98d..1bfe6936f 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -29,6 +29,20 @@ class TestMemoryCache(base.TestCase): 'max_age': 10, 'class': 'dogpile.cache.memory', }, + 'clouds': + { + '_cache_test_': + { + 'auth': + { + 'auth_url': 'http://198.51.100.1:35357/v2.0', + 'username': '_test_user_', + 'password': '_test_pass_', + 'project_name': '_test_project_', + }, + 'region_name': '_test_region_', + }, + }, } def setUp(self): @@ -59,13 +73,9 @@ def test_project_cache(self, keystone_mock): mock_project_b.id = 'project_b' keystone_mock.projects.list.return_value = [mock_project, mock_project_b] - # Caching should hide this from us until we invalidate - # TODO(clint) fix os-client-config which doesn't actually let us use - # the memory cache. - # TODO(clint) refactor to allow invalidating just the project cache. - #self.assertEqual( - # {'project_a': mock_project}, self.cloud.project_cache) - #self.cloud._cache.invalidate() - #self.assertEqual( - # {'project_a': mock_project, - # 'project_b': mock_project_b}, self.cloud.project_cache) + self.assertEqual( + {'project_a': mock_project}, self.cloud.project_cache) + self.cloud.get_project_cache.invalidate(self.cloud) + self.assertEqual( + {'project_a': mock_project, + 'project_b': mock_project_b}, self.cloud.project_cache) From bd78c48d1581d7faaec5cee70039260ec3449eb6 Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Fri, 10 Apr 2015 14:53:31 -0700 Subject: [PATCH 0158/3836] Test volume list caching This behavior actually causes problems, but we want to make sure it is covered before changing behaviors. Change-Id: I5c7558a4b036d6b177bb01e8fb0582d936528c61 --- shade/tests/unit/test_caching.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 1bfe6936f..05c2b5bdc 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -79,3 +79,21 @@ def test_project_cache(self, keystone_mock): self.assertEqual( {'project_a': mock_project, 'project_b': mock_project_b}, self.cloud.project_cache) + + @mock.patch('shade.OpenStackCloud.cinder_client') + def test_list_volumes(self, cinder_mock): + mock_volume = mock.MagicMock() + mock_volume.id = 'volume1' + mock_volume.status = 'available' + mock_volume.display_name = 'Volume 1 Display Name' + cinder_mock.volumes.list.return_value = [mock_volume] + self.assertEqual([mock_volume], self.cloud.list_volumes()) + mock_volume2 = mock.MagicMock() + mock_volume2.id = 'volume2' + mock_volume2.status = 'available' + mock_volume2.display_name = 'Volume 2 Display Name' + cinder_mock.volumes.list.return_value = [mock_volume, mock_volume2] + self.assertEqual([mock_volume], self.cloud.list_volumes()) + self.cloud.list_volumes.invalidate(self.cloud) + self.assertEqual([mock_volume, mock_volume2], + self.cloud.list_volumes()) From 557f488a2d16b9a5ea8f242e0dc7d66a7d5ec85f Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Fri, 10 Apr 2015 15:29:39 -0700 Subject: [PATCH 0159/3836] Unsteady state in volume list should prevent cache Volumes that are in states that imply a state change should cause us to not cache. That way we are never spinning on a cache knowing a new state is impending. This is only the first half of the change that is needed. We also need to invalidate the volume list cache if we change anything. Note that the odd structure for the should_cache_fn is imposed on us by dogpile.cache's decorator function, which requires that things be fully defined at parse time. Change-Id: I8a34e3d04edea6c1b8ed636609102d7d0b7c86d8 --- shade/__init__.py | 41 ++++++++++++++++++++------------ shade/tests/unit/test_caching.py | 16 +++++++++++++ 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 2993a78e2..945bf265b 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -150,19 +150,30 @@ def _iterate_timeout(timeout, message): raise OpenStackCloudTimeout(message) -def _cache_on_arguments(func): - def _cache_decorator(obj, *args, **kwargs): - the_method = obj._cache.cache_on_arguments()( - func.__get__(obj, type(obj))) - return the_method(*args, **kwargs) +def _cache_on_arguments(*cache_on_args, **cache_on_kwargs): + def _inner_cache_on_arguments(func): + def _cache_decorator(obj, *args, **kwargs): + the_method = obj._cache.cache_on_arguments( + *cache_on_args, **cache_on_kwargs)( + func.__get__(obj, type(obj))) + return the_method(*args, **kwargs) - def invalidate(obj, *args, **kwargs): - return obj._cache.cache_on_arguments()(func).invalidate( - *args, **kwargs) + def invalidate(obj, *args, **kwargs): + return obj._cache.cache_on_arguments()(func).invalidate( + *args, **kwargs) - _cache_decorator.invalidate = invalidate + _cache_decorator.invalidate = invalidate - return _cache_decorator + return _cache_decorator + return _inner_cache_on_arguments + + +def _no_pending_volumes(volumes): + '''If there are any volumes not in a steady state, don't cache''' + for volume in volumes: + if volume.status not in ('available', 'error'): + return False + return True class OpenStackCloud(object): @@ -391,7 +402,7 @@ def auth_token(self): def project_cache(self): return self.get_project_cache() - @_cache_on_arguments + @_cache_on_arguments() def get_project_cache(self): return {project.id: project for project in self._project_manager.list()} @@ -457,7 +468,7 @@ def delete_project(self, name_or_id): project=name_or_id, message=e.message)) @property - @_cache_on_arguments + @_cache_on_arguments() def user_cache(self): user_list = self.manager.submitTask(_tasks.UserListTask()) return {user.id: user for user in user_list} @@ -641,7 +652,7 @@ def get_region(self): return self.region_name @property - @_cache_on_arguments + @_cache_on_arguments() def flavor_cache(self): return {flavor.id: flavor for flavor in self.manager.submitTask(_tasks.FlavorList())} @@ -906,7 +917,7 @@ def _get_images_from_cloud(self, filter_deleted): def _reset_image_cache(self): self._image_cache = None - @_cache_on_arguments + @_cache_on_arguments() def list_images(self, filter_deleted=True): """Get available glance images. @@ -1186,7 +1197,7 @@ def _get_volumes_from_cloud(self): except Exception: return [] - @_cache_on_arguments + @_cache_on_arguments(should_cache_fn=_no_pending_volumes) def list_volumes(self, cache=True): return self._get_volumes_from_cloud() diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 05c2b5bdc..37734a712 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -97,3 +97,19 @@ def test_list_volumes(self, cinder_mock): self.cloud.list_volumes.invalidate(self.cloud) self.assertEqual([mock_volume, mock_volume2], self.cloud.list_volumes()) + + @mock.patch('shade.OpenStackCloud.cinder_client') + def test_list_volumes_creating_invalidates(self, cinder_mock): + mock_volume = mock.MagicMock() + mock_volume.id = 'volume1' + mock_volume.status = 'creating' + mock_volume.display_name = 'Volume 1 Display Name' + cinder_mock.volumes.list.return_value = [mock_volume] + self.assertEqual([mock_volume], self.cloud.list_volumes()) + mock_volume2 = mock.MagicMock() + mock_volume2.id = 'volume2' + mock_volume2.status = 'available' + mock_volume2.display_name = 'Volume 2 Display Name' + cinder_mock.volumes.list.return_value = [mock_volume, mock_volume2] + self.assertEqual([mock_volume, mock_volume2], + self.cloud.list_volumes()) From 52e676644415a7dd14629b3bdd467d9139850aa6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 11 Apr 2015 08:06:29 -0400 Subject: [PATCH 0160/3836] Actually put some content into our sphinx docs The README is pretty good - get it into the main index page for the sphinx docs so that we can publish them. Partial-Bug: #1440814 Change-Id: Ic72b81964cab1f939f08b957dec3be969c47a32e --- doc/source/conf.py | 4 ++-- doc/source/index.rst | 14 ++------------ doc/source/readme.rst | 1 - doc/source/usage.rst | 7 ------- 4 files changed, 4 insertions(+), 22 deletions(-) delete mode 100644 doc/source/readme.rst delete mode 100644 doc/source/usage.rst diff --git a/doc/source/conf.py b/doc/source/conf.py index 02be16776..221de3c88 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -38,7 +38,7 @@ # General information about the project. project = u'os-client-config' -copyright = u'2013, OpenStack Foundation' +copyright = u'2015, various OpenStack developers' # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True @@ -72,4 +72,4 @@ ] # Example configuration for intersphinx: refer to the Python standard library. -#intersphinx_mapping = {'http://docs.python.org/': None} \ No newline at end of file +#intersphinx_mapping = {'http://docs.python.org/': None} diff --git a/doc/source/index.rst b/doc/source/index.rst index 2c8b52d5e..efde7601a 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -1,20 +1,10 @@ -.. os-client-config documentation master file, created by - sphinx-quickstart on Tue Jul 9 22:26:36 2013. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to os-client-config's documentation! -======================================================== - -Contents: +.. include:: ../../README.rst .. toctree:: :maxdepth: 2 - readme - installation - usage contributing + installation Indices and tables ================== diff --git a/doc/source/readme.rst b/doc/source/readme.rst deleted file mode 100644 index 38ba8043d..000000000 --- a/doc/source/readme.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../../README.rst \ No newline at end of file diff --git a/doc/source/usage.rst b/doc/source/usage.rst deleted file mode 100644 index 910fd0790..000000000 --- a/doc/source/usage.rst +++ /dev/null @@ -1,7 +0,0 @@ -======== -Usage -======== - -To use os-client-config in a project:: - - import os_client_config \ No newline at end of file From 2ac9258563a969283472fbde053850eccbc2fbf2 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 11 Apr 2015 11:32:33 -0400 Subject: [PATCH 0161/3836] Add keystoneclient to test-requirements The auth parameter name validation requires keystoneclient and can't be tested if it's not there. While we're at it - update the current requirements to be inline with global requirements. Change-Id: I6da62476f3851670545143184f9f29479f1caaca --- requirements.txt | 2 +- test-requirements.txt | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 71026aa67..498c5c336 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ # 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. -PyYAML +PyYAML>=3.1.0 diff --git a/test-requirements.txt b/test-requirements.txt index 03e01941e..62f3e8831 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,10 +8,11 @@ coverage>=3.6 extras fixtures>=0.3.14 discover -python-subunit -sphinx>=1.1.2 -oslosphinx -oslotest>=1.1.0.0a1 +python-keystoneclient>=1.1.0 +python-subunit>=0.0.18 +sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 +oslosphinx>=2.5.0,<2.6.0 # Apache-2.0 +oslotest>=1.5.1,<1.6.0 # Apache-2.0 testrepository>=0.0.18 testscenarios>=0.4 -testtools>=0.9.34 +testtools>=0.9.36,!=1.2.0 From 7e682d3bf097a006ec43c16ecc96664bf4b29294 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 11 Apr 2015 08:27:05 -0400 Subject: [PATCH 0162/3836] Put env vars into their own cloud config The semantics around mixing environment variables and config file values are confusing at best and no reasonable usecase has been expressed as to why doing so is desirable. Move the logic around environment variable processing to always provide an "envvars" cloud if any envvars are set. The cloud will only exist in the presence of OS_ env vars. get_one_cloud() will default to returning the envvars cloud if it exists. Change-Id: I6c3a54997c3278feedfdf93cc4d1e74b6235700a Closes-Bug: #1439927 --- README.rst | 15 ++-- os_client_config/cloud_config.py | 2 +- os_client_config/config.py | 31 +++++-- os_client_config/tests/base.py | 80 ++++++++++++++++++- os_client_config/tests/test_config.py | 67 +--------------- os_client_config/tests/test_environ.py | 67 ++++++++++++++++ .../tests/test_os_client_config.py | 28 ------- 7 files changed, 181 insertions(+), 109 deletions(-) create mode 100644 os_client_config/tests/test_environ.py delete mode 100644 os_client_config/tests/test_os_client_config.py diff --git a/README.rst b/README.rst index 4c67acfab..18bf81d77 100644 --- a/README.rst +++ b/README.rst @@ -16,14 +16,13 @@ os-client-config honors all of the normal `OS_*` variables. It does not provide backwards compatibility to service-specific variables such as `NOVA_USERNAME`. -If you have environment variables and no config files, os-client-config -will produce a cloud config object named "openstack" containing your -values from the environment. +If you have OpenStack environment variables seet and no config files, +os-client-config will produce a cloud config object named "envvars" containing +your values from the environment. Service specific settings, like the nova service type, are set with the default service type as a prefix. For instance, to set a special service_type -for trove (because you're using Rackspace) set: -:: +for trove set:: export OS_DATABASE_SERVICE_TYPE=rax:database @@ -40,7 +39,7 @@ locations: The first file found wins. The keys are all of the keys you'd expect from `OS_*` - except lower case -and without the OS prefix. So, username is set with `username`. +and without the OS prefix. So, region name is set with `region_name`. Service specific settings, like the nova service type, are set with the default service type as a prefix. For instance, to set a special service_type @@ -119,6 +118,10 @@ behaviors: * `cache.max_age` and nothing else gets you memory cache. * Otherwise, `cache.class` and `cache.arguments` are passed in +`os-client-config` does not actually cache anything itself, but it collects +and presents the cache information so that your various applications that +are connecting to OpenStack can share a cache should you desire. + :: cache: diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 6f501c692..c17bfc2b7 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -15,7 +15,7 @@ class CloudConfig(object): def __init__(self, name, region, config): - self.name = name or 'openstack' + self.name = name self.region = region self.config = config diff --git a/os_client_config/config.py b/os_client_config/config.py index 84940436e..70326cfc0 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -54,10 +54,12 @@ def get_boolean(value): def _get_os_environ(): ret = dict(defaults._defaults) - for (k, v) in os.environ.items(): - if k.startswith('OS_'): - newkey = k[3:].lower() - ret[newkey] = v + environkeys = [k for k in os.environ.keys() if k.startswith('OS_')] + if not environkeys: + return None + for k in environkeys: + newkey = k[3:].lower() + ret[newkey] = os.environ[k] return ret @@ -80,7 +82,7 @@ def __init__(self, config_files=None, vendor_files=None): self._config_files = config_files or CONFIG_FILES self._vendor_files = vendor_files or VENDOR_FILES - self.defaults = _get_os_environ() + self.defaults = dict(defaults._defaults) # use a config file if it exists where expected self.cloud_config = self._load_config_file() @@ -88,6 +90,14 @@ def __init__(self, config_files=None, vendor_files=None): self.cloud_config = dict( clouds=dict(openstack=dict(self.defaults))) + envvars = _get_os_environ() + if envvars: + if 'envvars' in self.cloud_config['clouds']: + raise exceptions.OpenStackConfigException( + 'clouds.yaml defines a cloud named envvars, and OS_' + ' env vars are set') + self.cloud_config['clouds']['envvars'] = envvars + self._cache_max_age = None self._cache_path = CACHE_PATH self._cache_class = 'dogpile.cache.null' @@ -109,6 +119,7 @@ def _load_config_file(self): if os.path.exists(path): with open(path, 'r') as f: return yaml.safe_load(f) + return dict(clouds=dict()) def _load_vendor_file(self): for path in self._vendor_files: @@ -135,7 +146,7 @@ def _get_regions(self, cloud): # No region configured return '' - def _get_region(self, cloud): + def _get_region(self, cloud=None): return self._get_regions(cloud).split(',')[0] def _get_cloud_sections(self): @@ -152,7 +163,7 @@ def _get_base_cloud_config(self, name): our_cloud = self.cloud_config['clouds'].get(name, dict()) - # Get the defaults (including env vars) first + # Get the defaults cloud.update(self.defaults) # yes, I know the next line looks silly @@ -197,7 +208,8 @@ def _fix_backwards_project(self, cloud): if key in cloud['auth']: target = cloud['auth'][key] del cloud['auth'][key] - cloud['auth'][target_key] = target + if target: + cloud['auth'][target_key] = target return cloud def _fix_backwards_auth_plugin(self, cloud): @@ -321,6 +333,9 @@ def get_one_cloud(self, cloud=None, validate=True, :param kwargs: Additional configuration options """ + if cloud is None and 'envvars' in self._get_cloud_sections(): + cloud = 'envvars' + args = self._fix_args(kwargs, argparse=argparse) if 'region_name' not in args or args['region_name'] is None: diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py index 1c30cdb56..f6e815d9c 100644 --- a/os_client_config/tests/base.py +++ b/os_client_config/tests/base.py @@ -15,9 +15,87 @@ # License for the specific language governing permissions and limitations # under the License. + +import os +import tempfile + +from os_client_config import cloud_config + +import extras +import fixtures from oslotest import base +import yaml -class TestCase(base.BaseTestCase): +VENDOR_CONF = { + 'public-clouds': { + '_test_cloud_in_our_cloud': { + 'auth': { + 'username': 'testotheruser', + 'project_name': 'testproject', + }, + }, + } +} +USER_CONF = { + 'clouds': { + '_test_cloud_': { + 'cloud': '_test_cloud_in_our_cloud', + 'auth': { + 'username': 'testuser', + 'password': 'testpass', + }, + 'region_name': 'test-region', + }, + '_test_cloud_no_vendor': { + 'cloud': '_test_non_existant_cloud', + 'auth': { + 'username': 'testuser', + 'password': 'testpass', + 'project_name': 'testproject', + }, + 'region_name': 'test-region', + }, + }, + 'cache': {'max_age': 1}, +} + + +def _write_yaml(obj): + # Assume NestedTempfile so we don't have to cleanup + with tempfile.NamedTemporaryFile(delete=False) as obj_yaml: + obj_yaml.write(yaml.safe_dump(obj).encode('utf-8')) + return obj_yaml.name + +class TestCase(base.BaseTestCase): """Test case base class for all unit tests.""" + + def setUp(self): + super(TestCase, self).setUp() + + self.useFixture(fixtures.NestedTempfile()) + conf = dict(USER_CONF) + tdir = self.useFixture(fixtures.TempDir()) + conf['cache']['path'] = tdir.path + self.cloud_yaml = _write_yaml(conf) + self.vendor_yaml = _write_yaml(VENDOR_CONF) + + # Isolate the test runs from the environment + # Do this as two loops because you can't modify the dict in a loop + # over the dict in 3.4 + keys_to_isolate = [] + for env in os.environ.keys(): + if env.startswith('OS_'): + keys_to_isolate.append(env) + for env in keys_to_isolate: + self.useFixture(fixtures.EnvironmentVariable(env)) + + def _assert_cloud_details(self, cc): + self.assertIsInstance(cc, cloud_config.CloudConfig) + self.assertTrue(extras.safe_hasattr(cc, 'auth')) + self.assertIsInstance(cc.auth, dict) + self.assertIsNone(cc.cloud) + self.assertIn('username', cc.auth) + self.assertEqual('testuser', cc.auth['username']) + self.assertEqual('testproject', cc.auth['project_name']) diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 5ef3048f0..9decf3efa 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -12,66 +12,12 @@ # License for the specific language governing permissions and limitations # under the License. -import tempfile - -import extras -import fixtures -import testtools -import yaml - from os_client_config import cloud_config from os_client_config import config - -VENDOR_CONF = { - 'public-clouds': { - '_test_cloud_in_our_cloud': { - 'auth': { - 'username': 'testotheruser', - 'project_name': 'testproject', - }, - }, - } -} -USER_CONF = { - 'clouds': { - '_test_cloud_': { - 'cloud': '_test_cloud_in_our_cloud', - 'auth': { - 'username': 'testuser', - 'password': 'testpass', - }, - 'region_name': 'test-region', - }, - '_test_cloud_no_vendor': { - 'cloud': '_test_non_existant_cloud', - 'auth': { - 'username': 'testuser', - 'password': 'testpass', - 'project_name': 'testproject', - }, - 'region_name': 'test-region', - }, - }, - 'cache': {'max_age': 1}, -} +from os_client_config.tests import base -def _write_yaml(obj): - # Assume NestedTempfile so we don't have to cleanup - with tempfile.NamedTemporaryFile(delete=False) as obj_yaml: - obj_yaml.write(yaml.safe_dump(obj).encode('utf-8')) - return obj_yaml.name - - -class TestConfig(testtools.TestCase): - def setUp(self): - super(TestConfig, self).setUp() - self.useFixture(fixtures.NestedTempfile()) - conf = dict(USER_CONF) - tdir = self.useFixture(fixtures.TempDir()) - conf['cache']['path'] = tdir.path - self.cloud_yaml = _write_yaml(conf) - self.vendor_yaml = _write_yaml(VENDOR_CONF) +class TestConfig(base.TestCase): def test_get_one_cloud(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], @@ -90,12 +36,3 @@ def test_get_one_cloud_with_config_files(self): self._assert_cloud_details(cc) cc = c.get_one_cloud('_test_cloud_no_vendor') self._assert_cloud_details(cc) - - def _assert_cloud_details(self, cc): - self.assertIsInstance(cc, cloud_config.CloudConfig) - self.assertTrue(extras.safe_hasattr(cc, 'auth')) - self.assertIsInstance(cc.auth, dict) - self.assertIsNone(cc.cloud) - self.assertIn('username', cc.auth) - self.assertEqual('testuser', cc.auth['username']) - self.assertEqual('testproject', cc.auth['project_name']) diff --git a/os_client_config/tests/test_environ.py b/os_client_config/tests/test_environ.py new file mode 100644 index 000000000..473c41743 --- /dev/null +++ b/os_client_config/tests/test_environ.py @@ -0,0 +1,67 @@ +# Copyright (c) 2014 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. + + +from os_client_config import cloud_config +from os_client_config import config +from os_client_config import exceptions +from os_client_config.tests import base + +import fixtures + + +class TestConfig(base.TestCase): + + def test_get_one_cloud(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + self.assertIsInstance(c.get_one_cloud(), cloud_config.CloudConfig) + + def test_no_environ(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + self.assertRaises( + exceptions.OpenStackConfigException, c.get_one_cloud, 'envvars') + + def test_environ_exists(self): + self.useFixture( + fixtures.EnvironmentVariable('OS_AUTH_URL', 'https://example.com')) + self.useFixture( + fixtures.EnvironmentVariable('OS_USERNAME', 'testuser')) + self.useFixture( + fixtures.EnvironmentVariable('OS_PROJECT_NAME', 'testproject')) + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + cc = c.get_one_cloud('envvars') + self._assert_cloud_details(cc) + self.assertNotIn('auth_url', cc.config) + self.assertIn('auth_url', cc.config['auth']) + self.assertNotIn('auth_url', cc.config) + cc = c.get_one_cloud('_test_cloud_') + self._assert_cloud_details(cc) + cc = c.get_one_cloud('_test_cloud_no_vendor') + self._assert_cloud_details(cc) + + def test_get_one_cloud_with_config_files(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + self.assertIsInstance(c.cloud_config, dict) + self.assertIn('cache', c.cloud_config) + self.assertIsInstance(c.cloud_config['cache'], dict) + self.assertIn('max_age', c.cloud_config['cache']) + self.assertIn('path', c.cloud_config['cache']) + cc = c.get_one_cloud('_test_cloud_') + self._assert_cloud_details(cc) + cc = c.get_one_cloud('_test_cloud_no_vendor') + self._assert_cloud_details(cc) diff --git a/os_client_config/tests/test_os_client_config.py b/os_client_config/tests/test_os_client_config.py deleted file mode 100644 index 7421b6fe4..000000000 --- a/os_client_config/tests/test_os_client_config.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- 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. - -""" -test_os_client_config ----------------------------------- - -Tests for `os_client_config` module. -""" - -from os_client_config.tests import base - - -class TestOs_client_config(base.TestCase): - - def test_something(self): - pass From ffafb52fa7d41e7e0d4d3a44588d94dcc8dfa200 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 11 Apr 2015 11:55:52 -0400 Subject: [PATCH 0163/3836] Allow overriding envvars as the name of the cloud For environment variables created cloud objects, it's possible someone may not want it to be called envvars. I mean, let's be honest, I cannot imagine why this would be important ... but people get emotional about things. Let them name their cloud "bunnyrabbit" because that makes people happy. Change-Id: I0c232de7d93080ea632fb66a82b9e6d3e925c901 --- README.rst | 3 ++- os_client_config/config.py | 21 ++++++++++++++++----- os_client_config/tests/test_config.py | 7 +++++++ os_client_config/tests/test_environ.py | 26 +++++++++++++++----------- 4 files changed, 40 insertions(+), 17 deletions(-) diff --git a/README.rst b/README.rst index 18bf81d77..34a1b02d8 100644 --- a/README.rst +++ b/README.rst @@ -18,7 +18,8 @@ provide backwards compatibility to service-specific variables such as If you have OpenStack environment variables seet and no config files, os-client-config will produce a cloud config object named "envvars" containing -your values from the environment. +your values from the environment. If you don't like the name "envvars", that's +ok, you can override it by setting `OS_CLOUD_NAME`. Service specific settings, like the nova service type, are set with the default service type as a prefix. For instance, to set a special service_type diff --git a/os_client_config/config.py b/os_client_config/config.py index 70326cfc0..a0e7672a1 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -90,13 +90,24 @@ def __init__(self, config_files=None, vendor_files=None): self.cloud_config = dict( clouds=dict(openstack=dict(self.defaults))) + self.envvar_key = os.environ.pop('OS_CLOUD_NAME', None) + if self.envvar_key: + if self.envvar_key in self.cloud_config['clouds']: + raise exceptions.OpenStackConfigException( + 'clouds.yaml defines a cloud named "{0}", but' + ' OS_CLOUD_NAME is also set to "{0}". Please rename' + ' either your environment based cloud, or one of your' + ' file-based clouds.'.format(self.envvar_key)) + else: + self.envvar_key = 'envvars' + envvars = _get_os_environ() if envvars: - if 'envvars' in self.cloud_config['clouds']: + if self.envvar_key in self.cloud_config['clouds']: raise exceptions.OpenStackConfigException( - 'clouds.yaml defines a cloud named envvars, and OS_' + 'clouds.yaml defines a cloud named {0}, and OS_*' ' env vars are set') - self.cloud_config['clouds']['envvars'] = envvars + self.cloud_config['clouds'][self.envvar_key] = envvars self._cache_max_age = None self._cache_path = CACHE_PATH @@ -333,8 +344,8 @@ def get_one_cloud(self, cloud=None, validate=True, :param kwargs: Additional configuration options """ - if cloud is None and 'envvars' in self._get_cloud_sections(): - cloud = 'envvars' + if cloud is None and self.envvar_key in self._get_cloud_sections(): + cloud = self.envvar_key args = self._fix_args(kwargs, argparse=argparse) diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 9decf3efa..c537842b7 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -14,6 +14,7 @@ from os_client_config import cloud_config from os_client_config import config +from os_client_config import exceptions from os_client_config.tests import base @@ -36,3 +37,9 @@ def test_get_one_cloud_with_config_files(self): self._assert_cloud_details(cc) cc = c.get_one_cloud('_test_cloud_no_vendor') self._assert_cloud_details(cc) + + def test_no_environ(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + self.assertRaises( + exceptions.OpenStackConfigException, c.get_one_cloud, 'envvars') diff --git a/os_client_config/tests/test_environ.py b/os_client_config/tests/test_environ.py index 473c41743..363cff1a7 100644 --- a/os_client_config/tests/test_environ.py +++ b/os_client_config/tests/test_environ.py @@ -15,32 +15,36 @@ from os_client_config import cloud_config from os_client_config import config -from os_client_config import exceptions from os_client_config.tests import base import fixtures -class TestConfig(base.TestCase): +class TestEnviron(base.TestCase): + + def setUp(self): + super(TestEnviron, self).setUp() + self.useFixture( + fixtures.EnvironmentVariable('OS_AUTH_URL', 'https://example.com')) + self.useFixture( + fixtures.EnvironmentVariable('OS_USERNAME', 'testuser')) + self.useFixture( + fixtures.EnvironmentVariable('OS_PROJECT_NAME', 'testproject')) def test_get_one_cloud(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) self.assertIsInstance(c.get_one_cloud(), cloud_config.CloudConfig) - def test_no_environ(self): + def test_envvar_name_override(self): + self.useFixture( + fixtures.EnvironmentVariable('OS_CLOUD_NAME', 'openstack')) c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - self.assertRaises( - exceptions.OpenStackConfigException, c.get_one_cloud, 'envvars') + cc = c.get_one_cloud('openstack') + self._assert_cloud_details(cc) def test_environ_exists(self): - self.useFixture( - fixtures.EnvironmentVariable('OS_AUTH_URL', 'https://example.com')) - self.useFixture( - fixtures.EnvironmentVariable('OS_USERNAME', 'testuser')) - self.useFixture( - fixtures.EnvironmentVariable('OS_PROJECT_NAME', 'testproject')) c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) cc = c.get_one_cloud('envvars') From db39e9831ea600c8f7f02a5ddec2e0b8b5924de2 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 11 Apr 2015 09:16:56 -0400 Subject: [PATCH 0164/3836] Add DreamCompute to vendors list Change-Id: I4d81e221c8105d796dcd29fcd7628738486e4b00 --- os_client_config/vendors.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/os_client_config/vendors.py b/os_client_config/vendors.py index eb08bbdbd..3fafe4124 100644 --- a/os_client_config/vendors.py +++ b/os_client_config/vendors.py @@ -21,6 +21,7 @@ region_name='region-b.geo-1', dns_service_type='hpext:dns', image_api_version='1', + image_format='qcow2', ), rackspace=dict( auth=dict( @@ -29,5 +30,14 @@ database_service_type='rax:database', compute_service_name='cloudServersOpenStack', image_api_version='2', - ) + image_format='vhd', + ), + dreamhost=dict( + auth=dict( + auth_url='https://keystone.dream.io/v2.0', + region_name='RegionOne', + ), + image_api_version='2', + image_format='raw', + ), ) From 6ec480162aac7ffe4d905a89836247077c352806 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 13 Apr 2015 10:00:58 -0400 Subject: [PATCH 0165/3836] Add vexxhost vexxhost has a public cloud - we should list them in our vendors file. Change-Id: Icf1276d59dadf50cabca3a7c2540121fb1cf7057 --- os_client_config/vendors.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/os_client_config/vendors.py b/os_client_config/vendors.py index 3fafe4124..fec3159ba 100644 --- a/os_client_config/vendors.py +++ b/os_client_config/vendors.py @@ -40,4 +40,10 @@ image_api_version='2', image_format='raw', ), + vexxhost=dict( + auth=dict( + auth_url='http://auth.api.thenebulacloud.com:5000/v2.0/', + region_name='ca-ymq-1', + ), + ), ) From c5d350c03d062af5fd4b9277accab4f4a0b64504 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 2 Apr 2015 17:39:25 +0000 Subject: [PATCH 0166/3836] Add API method create_subnet() Add a method to create a subnet and associated unit tests. Methods for deleting and updating subnets will be added separately because large code reviews suck. Change-Id: Id2031a129a9204f07698f1912b7e743aff5d69f1 --- shade/__init__.py | 118 +++++++++++++++++++++++++++++++++ shade/_tasks.py | 5 ++ shade/tests/unit/test_shade.py | 35 ++++++++++ 3 files changed, 158 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 4249cbecc..8aa98ea35 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1732,6 +1732,124 @@ def get_object_metadata(self, container, name): "Object metadata fetch failed: %s (%s/%s)" % ( e.http_reason, e.http_host, e.http_path)) + def create_subnet(self, network_name_or_id, cidr, ip_version=4, + enable_dhcp=False, subnet_name=None, tenant_id=None, + allocation_pools=None, gateway_ip=None, + dns_nameservers=None, host_routes=None, + ipv6_ra_mode=None, ipv6_address_mode=None): + """Create a subnet on a specified network. + + :param string network_name_or_id: + The unique name or ID of the attached network. If a non-unique + name is supplied, an exception is raised. + :param string cidr: + The CIDR. + :param int ip_version: + The IP version, which is 4 or 6. + :param bool enable_dhcp: + Set to ``True`` if DHCP is enabled and ``False`` if disabled. + Default is ``False``. + :param string subnet_name: + The name of the subnet. + :param string tenant_id: + The ID of the tenant who owns the network. Only administrative users + can specify a tenant ID other than their own. + :param list allocation_pools: + A list of dictionaries of the start and end addresses for the + allocation pools. For example:: + + [ + { + "start": "192.168.199.2", + "end": "192.168.199.254" + } + ] + + :param string gateway_ip: + The gateway IP address. When you specify both allocation_pools and + gateway_ip, you must ensure that the gateway IP does not overlap + with the specified allocation pools. + :param list dns_nameservers: + A list of DNS name servers for the subnet. For example:: + + [ "8.8.8.7", "8.8.8.8" ] + + :param list host_routes: + A list of host route dictionaries for the subnet. For example:: + + [ + { + "destination": "0.0.0.0/0", + "nexthop": "123.456.78.9" + }, + { + "destination": "192.168.0.0/24", + "nexthop": "192.168.0.1" + } + ] + + :param string ipv6_ra_mode: + IPv6 Router Advertisement mode. Valid values are: 'dhcpv6-stateful', + 'dhcpv6-stateless', or 'slaac'. + :param string ipv6_address_mode: + IPv6 address mode. Valid values are: 'dhcpv6-stateful', + 'dhcpv6-stateless', or 'slaac'. + + :returns: The new subnet object. + :raises: OpenStackCloudException on operation error. + """ + + networks = [] + for network in self.list_networks(): + if network_name_or_id in (network['id'], network['name']): + networks.append(network) + + if not networks: + raise OpenStackCloudException( + "Network %s not found." % network_name_or_id) + + if len(networks) > 1: + raise OpenStackCloudException( + "More than one network named %s. Use ID." % network_name_or_id) + + # The body of the neutron message for the subnet we wish to create. + # This includes attributes that are required or have defaults. + subnet = { + 'network_id': networks[0]['id'], + 'cidr': cidr, + 'ip_version': ip_version, + 'enable_dhcp': enable_dhcp + } + + # Add optional attributes to the message. + if subnet_name: + subnet['name'] = subnet_name + if tenant_id: + subnet['tenant_id'] = tenant_id + if allocation_pools: + subnet['allocation_pools'] = allocation_pools + if gateway_ip: + subnet['gateway_ip'] = gateway_ip + if dns_nameservers: + subnet['dns_nameservers'] = dns_nameservers + if host_routes: + subnet['host_routes'] = host_routes + if ipv6_ra_mode: + subnet['ipv6_ra_mode'] = ipv6_ra_mode + if ipv6_address_mode: + subnet['ipv6_address_mode'] = ipv6_address_mode + + try: + new_subnet = self.manager.submitTask( + _tasks.SubnetCreate(body=dict(subnet=subnet))) + except Exception as e: + self.log.debug("Subnet creation failed", exc_info=True) + raise OpenStackCloudException( + "Error in creating subnet on network %s: %s" + % (network_name_or_id, e)) + + return new_subnet['subnet'] + class OperatorCloud(OpenStackCloud): diff --git a/shade/_tasks.py b/shade/_tasks.py index be4d0c8fd..073e8f491 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -236,3 +236,8 @@ def main(self, client): class ObjectMetadata(task_manager.Task): def main(self, client): return client.swift_client.head_object(**self.args) + + +class SubnetCreate(task_manager.Task): + def main(self, client): + return client.neutron_client.create_subnet(**self.args) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 305ec61ce..2bbc61fe7 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -88,3 +88,38 @@ def test_delete_router_multiple_using_id(self, mock_client, mock_list): mock_list.return_value = [router1, router2] self.cloud.delete_router('123') self.assertTrue(mock_client.delete_router.called) + + @mock.patch.object(shade.OpenStackCloud, 'list_networks') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_subnet(self, mock_client, mock_list): + net1 = dict(id='123', name='donald') + mock_list.return_value = [net1] + pool = [{'start': '192.168.199.2', 'end': '192.168.199.254'}] + dns = ['8.8.8.8'] + routes = [{"destination": "0.0.0.0/0", "nexthop": "123.456.78.9"}] + self.cloud.create_subnet('donald', '192.168.199.0/24', + allocation_pools=pool, + dns_nameservers=dns, + host_routes=routes) + self.assertTrue(mock_client.create_subnet.called) + + @mock.patch.object(shade.OpenStackCloud, 'list_networks') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_subnet_bad_network(self, mock_client, mock_list): + net1 = dict(id='123', name='donald') + mock_list.return_value = [net1] + self.assertRaises(shade.OpenStackCloudException, + self.cloud.create_subnet, + 'duck', '192.168.199.0/24') + self.assertFalse(mock_client.create_subnet.called) + + @mock.patch.object(shade.OpenStackCloud, 'list_networks') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_subnet_non_unique_network(self, mock_client, mock_list): + net1 = dict(id='123', name='donald') + net2 = dict(id='456', name='donald') + mock_list.return_value = [net1, net2] + self.assertRaises(shade.OpenStackCloudException, + self.cloud.create_subnet, + 'donald', '192.168.199.0/24') + self.assertFalse(mock_client.create_subnet.called) From 5ec510440933f2c1014c05b9bda15a5cf03ccca2 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 3 Apr 2015 12:49:02 +0000 Subject: [PATCH 0167/3836] Add API method delete_subnet() Add a method to delete a subnet and unit tests for that. Change-Id: Ia2901ccaf9a295ff61b525c989199ec75a8cb50e --- shade/__init__.py | 34 ++++++++++++++++++++++++++++++ shade/_tasks.py | 10 +++++++++ shade/tests/unit/test_shade.py | 38 ++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 8aa98ea35..1695f1fac 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -727,6 +727,9 @@ def get_router(self, name_or_id): return router return None + def list_subnets(self): + return self.manager.submitTask(_tasks.SubnetList())['subnets'] + # TODO(Shrews): This will eventually need to support tenant ID and # provider networks, which are admin-level params. def create_network(self, name, shared=False, admin_state_up=True): @@ -1850,6 +1853,37 @@ def create_subnet(self, network_name_or_id, cidr, ip_version=4, return new_subnet['subnet'] + def delete_subnet(self, name_or_id): + """Delete a subnet. + + If a name, instead of a unique UUID, is supplied, it is possible + that we could find more than one matching subnet since names are + not required to be unique. An error will be raised in this case. + + :param name_or_id: Name or ID of the subnet being deleted. + :raises: OpenStackCloudException on operation error. + """ + subnets = [] + for subnet in self.list_subnets(): + if name_or_id in (subnet['id'], subnet['name']): + subnets.append(subnet) + + if not subnets: + raise OpenStackCloudException( + "Subnet %s not found." % name_or_id) + + if len(subnets) > 1: + raise OpenStackCloudException( + "More than one subnet named %s. Use ID." % name_or_id) + + try: + self.manager.submitTask( + _tasks.SubnetDelete(subnet=subnets[0]['id'])) + except Exception as e: + self.log.debug("Subnet delete failed", exc_info=True) + raise OpenStackCloudException( + "Error deleting subnet %s: %s" % (name_or_id, e)) + class OperatorCloud(OpenStackCloud): diff --git a/shade/_tasks.py b/shade/_tasks.py index 073e8f491..a1d38cb91 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -241,3 +241,13 @@ def main(self, client): class SubnetCreate(task_manager.Task): def main(self, client): return client.neutron_client.create_subnet(**self.args) + + +class SubnetList(task_manager.Task): + def main(self, client): + return client.neutron_client.list_subnets() + + +class SubnetDelete(task_manager.Task): + def main(self, client): + client.neutron_client.delete_subnet(**self.args) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 2bbc61fe7..669cd2012 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -123,3 +123,41 @@ def test_create_subnet_non_unique_network(self, mock_client, mock_list): self.cloud.create_subnet, 'donald', '192.168.199.0/24') self.assertFalse(mock_client.create_subnet.called) + + @mock.patch.object(shade.OpenStackCloud, 'list_subnets') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_delete_subnet(self, mock_client, mock_list): + subnet1 = dict(id='123', name='mickey') + mock_list.return_value = [subnet1] + self.cloud.delete_subnet('mickey') + self.assertTrue(mock_client.delete_subnet.called) + + @mock.patch.object(shade.OpenStackCloud, 'list_subnets') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_delete_subnet_not_found(self, mock_client, mock_list): + subnet1 = dict(id='123', name='mickey') + mock_list.return_value = [subnet1] + self.assertRaises(shade.OpenStackCloudException, + self.cloud.delete_subnet, + 'goofy') + self.assertFalse(mock_client.delete_subnet.called) + + @mock.patch.object(shade.OpenStackCloud, 'list_subnets') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_delete_subnet_multiple_found(self, mock_client, mock_list): + subnet1 = dict(id='123', name='mickey') + subnet2 = dict(id='456', name='mickey') + mock_list.return_value = [subnet1, subnet2] + self.assertRaises(shade.OpenStackCloudException, + self.cloud.delete_subnet, + 'mickey') + self.assertFalse(mock_client.delete_subnet.called) + + @mock.patch.object(shade.OpenStackCloud, 'list_subnets') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_delete_subnet_multiple_using_id(self, mock_client, mock_list): + subnet1 = dict(id='123', name='mickey') + subnet2 = dict(id='456', name='mickey') + mock_list.return_value = [subnet1, subnet2] + self.cloud.delete_subnet('123') + self.assertTrue(mock_client.delete_subnet.called) From d42de63d227c58279ded786b2e09a62426ab3832 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 3 Apr 2015 14:30:11 +0000 Subject: [PATCH 0168/3836] Add API method update_subnet() Add a method to update a subnet and unit tests for that. Also corrects the message in the exception raised from create_subnet(). Change-Id: I1ad6c3bf7787ef7e26805a8fa6ce7f55f26924d3 --- shade/__init__.py | 77 ++++++++++++++++++++++++++++++++++ shade/_tasks.py | 5 +++ shade/tests/unit/test_shade.py | 5 +++ 3 files changed, 87 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 1695f1fac..e525ac77b 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1884,6 +1884,83 @@ def delete_subnet(self, name_or_id): raise OpenStackCloudException( "Error deleting subnet %s: %s" % (name_or_id, e)) + def update_subnet(self, subnet_id, subnet_name=None, enable_dhcp=None, + gateway_ip=None, allocation_pools=None, + dns_nameservers=None, host_routes=None): + """Update an existing subnet. + + :param string subnet_name: + The name of the subnet. + :param bool enable_dhcp: + Set to ``True`` if DHCP is enabled and ``False`` if disabled. + :param string gateway_ip: + The gateway IP address. When you specify both allocation_pools and + gateway_ip, you must ensure that the gateway IP does not overlap + with the specified allocation pools. + :param list allocation_pools: + A list of dictionaries of the start and end addresses for the + allocation pools. For example:: + + [ + { + "start": "192.168.199.2", + "end": "192.168.199.254" + } + ] + + :param list dns_nameservers: + A list of DNS name servers for the subnet. For example:: + + [ "8.8.8.7", "8.8.8.8" ] + + :param list host_routes: + A list of host route dictionaries for the subnet. For example:: + + [ + { + "destination": "0.0.0.0/0", + "nexthop": "123.456.78.9" + }, + { + "destination": "192.168.0.0/24", + "nexthop": "192.168.0.1" + } + ] + + :returns: The updated subnet object. + :raises: OpenStackCloudException on operation error. + """ + subnet = {} + if subnet_name: + subnet['name'] = subnet_name + if enable_dhcp is not None: + subnet['enable_dhcp'] = enable_dhcp + if gateway_ip: + subnet['gateway_ip'] = gateway_ip + if allocation_pools: + subnet['allocation_pools'] = allocation_pools + if dns_nameservers: + subnet['dns_nameservers'] = dns_nameservers + if host_routes: + subnet['host_routes'] = host_routes + + if not subnet: + self.log.debug("No subnet data to update") + return + + try: + new_subnet = self.manager.submitTask( + _tasks.SubnetUpdate( + subnet=subnet_id, body=dict(subnet=subnet))) + except Exception as e: + self.log.debug("Subnet update failed", exc_info=True) + raise OpenStackCloudException( + "Error updating subnet %s: %s" % (subnet_id, e)) + # Turns out neutron returns an actual dict, so no need for the + # use of meta.obj_to_dict() here (which would not work against + # a dict). + return new_subnet['subnet'] + class OperatorCloud(OpenStackCloud): diff --git a/shade/_tasks.py b/shade/_tasks.py index a1d38cb91..76b8c47a5 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -251,3 +251,8 @@ def main(self, client): class SubnetDelete(task_manager.Task): def main(self, client): client.neutron_client.delete_subnet(**self.args) + + +class SubnetUpdate(task_manager.Task): + def main(self, client): + return client.neutron_client.update_subnet(**self.args) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 669cd2012..d1ad58573 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -161,3 +161,8 @@ def test_delete_subnet_multiple_using_id(self, mock_client, mock_list): mock_list.return_value = [subnet1, subnet2] self.cloud.delete_subnet('123') self.assertTrue(mock_client.delete_subnet.called) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_update_subnet(self, mock_client): + self.cloud.update_subnet(subnet_id=123, subnet_name='goofy') + self.assertTrue(mock_client.update_subnet.called) From 5bc39aea20b30df03f3ceffda697be7ece9355d3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 13 Apr 2015 10:00:58 -0400 Subject: [PATCH 0169/3836] Add image information to vexxhost account Change-Id: I0e39c2a9828fb4fa73403158c3e58fb346ac9a10 --- os_client_config/vendors.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/os_client_config/vendors.py b/os_client_config/vendors.py index fec3159ba..96ecfcfbb 100644 --- a/os_client_config/vendors.py +++ b/os_client_config/vendors.py @@ -45,5 +45,7 @@ auth_url='http://auth.api.thenebulacloud.com:5000/v2.0/', region_name='ca-ymq-1', ), + image_api_version='1', + image_format='qcow2', ), ) From ce6502270fb863e31c64e8e029993e9239c0d46a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 13 Apr 2015 13:04:44 -0400 Subject: [PATCH 0170/3836] Add runabove to vendors Change-Id: I319365aeb3a5a00498b37128c5c9fbaf018d88f4 --- os_client_config/vendors.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/os_client_config/vendors.py b/os_client_config/vendors.py index 96ecfcfbb..90dccc756 100644 --- a/os_client_config/vendors.py +++ b/os_client_config/vendors.py @@ -48,4 +48,12 @@ image_api_version='1', image_format='qcow2', ), + runabove=dict( + auth=dict( + auth_url='https://auth.runabove.io/v2.0', + ), + image_api_version='1', + image_format='qcow2', + ), + ) From 825e5b5e83385184a8472113d0d528aa770c3e95 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 13 Apr 2015 14:02:32 -0400 Subject: [PATCH 0171/3836] Move region_names out of auth dict region_name is not an auth parameter - this is just simply an error. Change-Id: I5cc3847932d7c51288f451b4532b71f95d8c823d --- os_client_config/vendors.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/os_client_config/vendors.py b/os_client_config/vendors.py index 90dccc756..70756aebf 100644 --- a/os_client_config/vendors.py +++ b/os_client_config/vendors.py @@ -35,16 +35,16 @@ dreamhost=dict( auth=dict( auth_url='https://keystone.dream.io/v2.0', - region_name='RegionOne', ), + region_name='RegionOne', image_api_version='2', image_format='raw', ), vexxhost=dict( auth=dict( auth_url='http://auth.api.thenebulacloud.com:5000/v2.0/', - region_name='ca-ymq-1', ), + region_name='ca-ymq-1', image_api_version='1', image_format='qcow2', ), From f2e943e178e9d46ff911f224df8954a33a15a69c Mon Sep 17 00:00:00 2001 From: TerryHowe Date: Mon, 13 Apr 2015 12:45:44 -0600 Subject: [PATCH 0172/3836] add .venv to gitignore Change-Id: I36856a31f7a68280f9b787e5ab9ce5ac3aa0dc60 --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 837459343..218bf6a45 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.py[cod] +.venv # C extensions *.so From 05ba6a5535ef700fa9a020d63922e16c12814bda Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 8 Apr 2015 17:57:10 +0000 Subject: [PATCH 0173/3836] Add get_subnet() method Although not currently used directly within shade, users of the shade library may need to get a subnet by name or ID. Change-Id: I6423bb32fd7e29c1d533fa3d8a6d1ad3fab0616b --- shade/__init__.py | 6 ++++++ shade/tests/unit/test_shade.py | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index e525ac77b..5cc5de572 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -730,6 +730,12 @@ def get_router(self, name_or_id): def list_subnets(self): return self.manager.submitTask(_tasks.SubnetList())['subnets'] + def get_subnet(self, name_or_id): + for subnet in self.list_subnets(): + if name_or_id in (subnet['id'], subnet['name']): + return subnet + return None + # TODO(Shrews): This will eventually need to support tenant ID and # provider networks, which are admin-level params. def create_network(self, name, shared=False, admin_state_up=True): diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index d1ad58573..c566dbc4f 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -27,6 +27,14 @@ def setUp(self): def test_openstack_cloud(self): self.assertIsInstance(self.cloud, shade.OpenStackCloud) + @mock.patch.object(shade.OpenStackCloud, 'list_subnets') + def test_get_subnet(self, mock_list): + subnet = dict(id='123', name='mickey') + mock_list.return_value = [subnet] + r = self.cloud.get_subnet('mickey') + self.assertIsNotNone(r) + self.assertDictEqual(subnet, r) + @mock.patch.object(shade.OpenStackCloud, 'list_routers') def test_get_router(self, mock_list): router1 = dict(id='123', name='mickey') From 0e3e496a01d861fe16e5b50cb9586d9534f19c6e Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Mon, 13 Apr 2015 15:49:23 -0700 Subject: [PATCH 0174/3836] Make cache key generator ignore cache argument This argument pollutes the cache keys with cache:True, which is not actually relevant to the data requested, and makes invalidation complicated. Change-Id: Idd5e20de0fff6d540fadc79a5f30c52ff0e29136 --- shade/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 15594375f..7aa59b940 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -297,11 +297,12 @@ def _make_cache_key(self, namespace, fn): def generate_key(*args, **kwargs): arg_key = ','.join(args) - kwargs_keys = sorted(kwargs.keys()) + kw_keys = sorted(kwargs.keys()) kwargs_key = ','.join( - ['%s:%s' % (k, kwargs[k]) for k in kwargs_keys]) - return "_".join( + ['%s:%s' % (k, kwargs[k]) for k in kw_keys if k != 'cache']) + ans = "_".join( [name_key, fname, arg_key, kwargs_key]) + return ans return generate_key def get_service_type(self, service): From 36348231abb8576511db72fb32ca97592ddaa1db Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Fri, 10 Apr 2015 15:43:56 -0700 Subject: [PATCH 0175/3836] Invalidate volume list cache when creating We will handle other state-changing calls in subsequent patches. Change-Id: Iceff1df4dbb0b5fac87326114db9a13be2ccc267 --- shade/__init__.py | 1 + shade/tests/unit/test_caching.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 7aa59b940..8c5cf9e37 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1143,6 +1143,7 @@ def create_volume(self, wait=True, timeout=None, **kwargs): self.log.debug("Volume creation failed", exc_info=True) raise OpenStackCloudException( "Error in creating volume: %s" % e.message) + self.list_volumes.invalidate(self) if volume.status == 'error': raise OpenStackCloudException("Error in creating volume") diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 37734a712..2b1fc8264 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -113,3 +113,35 @@ def test_list_volumes_creating_invalidates(self, cinder_mock): cinder_mock.volumes.list.return_value = [mock_volume, mock_volume2] self.assertEqual([mock_volume, mock_volume2], self.cloud.list_volumes()) + + @mock.patch.object(shade.OpenStackCloud, 'cinder_client') + def test_create_volume_invalidates(self, cinder_mock): + mock_volb4 = mock.MagicMock() + mock_volb4.id = 'volume1' + mock_volb4.status = 'available' + mock_volb4.display_name = 'Volume 1 Display Name' + cinder_mock.volumes.list.return_value = [mock_volb4] + self.assertEqual([mock_volb4], self.cloud.list_volumes()) + volume = dict(display_name='junk_vol', + size=1, + display_description='test junk volume') + mock_vol = mock.Mock() + mock_vol.status = 'creating' + mock_vol.id = '12345' + cinder_mock.volumes.create.return_value = mock_vol + cinder_mock.volumes.list.return_value = [mock_volb4, mock_vol] + + def creating_available(): + def now_available(): + mock_vol.status = 'available' + return mock.DEFAULT + cinder_mock.volumes.list.side_effect = now_available + return mock.DEFAULT + cinder_mock.volumes.list.side_effect = creating_available + self.cloud.create_volume(wait=True, timeout=None, **volume) + self.assertTrue(cinder_mock.volumes.create.called) + self.assertEqual(3, cinder_mock.volumes.list.call_count) + # If cache was not invalidated, we would not see our own volume here + # because the first volume was available and thus would already be + # cached. + self.assertEqual([mock_volb4, mock_vol], self.cloud.list_volumes()) From a6cd1a9ae32889b3eed3077f28c82ac80b7168dc Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Fri, 10 Apr 2015 16:27:14 -0700 Subject: [PATCH 0176/3836] Deprecate use of cache in list_volumes The more proactive approach is to invalidate rather than use cache=False. In all cases, we are about to update, or have just updated, the item that is in the cache. This will provide more liveness for other cache users with a shared cache, and will provide less skew between our own queries to the cache versus reality. Change-Id: I5a41a3d21fd451f5432daa4e268b57328a9ad90f --- shade/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 8c5cf9e37..33fdf1608 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1153,7 +1153,7 @@ def create_volume(self, wait=True, timeout=None, **kwargs): for count in _iterate_timeout( timeout, "Timeout waiting for the volume to be available."): - volume = self.get_volume(vol_id, cache=False, error=False) + volume = self.get_volume(vol_id, error=False) if not volume: continue @@ -1176,7 +1176,7 @@ def delete_volume(self, name_or_id=None, wait=True, timeout=None): :raises: OpenStackCloudException on operation error. """ - volume = self.get_volume(name_or_id, cache=False) + volume = self.get_volume(name_or_id) try: self.manager.submitTask( @@ -1201,6 +1201,9 @@ def _get_volumes_from_cloud(self): @_cache_on_arguments(should_cache_fn=_no_pending_volumes) def list_volumes(self, cache=True): + if not cache: + warnings.warn('cache argument to list_volumes is deprecated. Use ' + ' invalidate instead.') return self._get_volumes_from_cloud() def get_volumes(self, server, cache=True): @@ -1228,7 +1231,7 @@ def get_volume(self, name_or_id, cache=True, error=True): def volume_exists(self, name_or_id): return self.get_volume( - name_or_id, cache=False, error=False) is not None + name_or_id, error=False) is not None def get_volume_attach_device(self, volume, server_id): """Return the device name a volume is attached to for a server. From ebf2dbaf7cc4ec924a0a7dc1e452f33e16ef8f2c Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Fri, 10 Apr 2015 16:33:00 -0700 Subject: [PATCH 0177/3836] Add test for invalidation after delete We need to invalidate the volume list every time we add or delete. Change-Id: I243548dc3256260ae09017fd0eef4cbe673a4901 --- shade/__init__.py | 2 ++ shade/tests/unit/test_caching.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 33fdf1608..54d52c8d8 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1176,6 +1176,7 @@ def delete_volume(self, name_or_id=None, wait=True, timeout=None): :raises: OpenStackCloudException on operation error. """ + self.list_volumes.invalidate(self) volume = self.get_volume(name_or_id) try: @@ -1186,6 +1187,7 @@ def delete_volume(self, name_or_id=None, wait=True, timeout=None): raise OpenStackCloudException( "Error in deleting volume: %s" % e.message) + self.list_volumes.invalidate(self) if wait: for count in _iterate_timeout( timeout, diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 2b1fc8264..648ef54b8 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -145,3 +145,19 @@ def now_available(): # because the first volume was available and thus would already be # cached. self.assertEqual([mock_volb4, mock_vol], self.cloud.list_volumes()) + + # And now delete and check same thing since list is cached as all + # available + mock_vol.status = 'deleting' + + def deleting_gone(): + def now_gone(): + cinder_mock.volumes.list.return_value = [mock_volb4] + return mock.DEFAULT + cinder_mock.volumes.list.side_effect = now_gone + return mock.DEFAULT + cinder_mock.volumes.list.return_value = [mock_volb4, mock_vol] + cinder_mock.volumes.list.side_effect = deleting_gone + cinder_mock.volumes.delete.return_value = mock_vol + self.cloud.delete_volume('12345') + self.assertEqual([mock_volb4], self.cloud.list_volumes()) From b7f38ff66144320116cb5ddf3aa39700dad42209 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 14 Apr 2015 10:26:14 -0400 Subject: [PATCH 0178/3836] Reset cache default to 0 None breaks the ansible inventory script. Change-Id: Iac30cdcce3a51910e0b373521263b239f7478a15 --- os_client_config/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index a0e7672a1..8d687ef61 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -109,7 +109,7 @@ def __init__(self, config_files=None, vendor_files=None): ' env vars are set') self.cloud_config['clouds'][self.envvar_key] = envvars - self._cache_max_age = None + self._cache_max_age = 0 self._cache_path = CACHE_PATH self._cache_class = 'dogpile.cache.null' self._cache_arguments = {} From b55de0e1637b1e9f6e424b070d13577af42d7fc6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 14 Apr 2015 11:23:49 -0400 Subject: [PATCH 0179/3836] Document vendor support information In the future, I'd like for this doc to be generated from the vendors.py file, but for now, this is great. Change-Id: Ifd0c8da5da46ba156c789f05398abcfa689f4f01 --- doc/source/index.rst | 1 + doc/source/vendor-support.rst | 86 +++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 doc/source/vendor-support.rst diff --git a/doc/source/index.rst b/doc/source/index.rst index efde7601a..0f793a1a3 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -3,6 +3,7 @@ .. toctree:: :maxdepth: 2 + vendor-support contributing installation diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst new file mode 100644 index 000000000..f74038e4b --- /dev/null +++ b/doc/source/vendor-support.rst @@ -0,0 +1,86 @@ +============== +Vendor Support +============== + +OpenStack presents deployers with many options, some of which can expose +differences to end users. `os-client-config` tries its best to collect +information about various things a user would need to know. The following +is a text representation of the vendor related defaults `os-client-config` +knows about. + +HP Cloud +-------- + +https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0 + +============== ================ +Region Name Human Name +============== ================ +region-a.geo-1 US West +region-b.geo-1 US East +============== ================ + +* DNS Service Type is `hpext:dns` +* Image API Version is 1 +* Images must be in `qcow2` format + +Rackspace +--------- + +https://identity.api.rackspacecloud.com/v2.0/ + +============== ================ +Region Name Human Name +============== ================ +DFW Dallas +ORD Chicago +IAD Washington, D.C. +============== ================ + +* Database Service Type is `rax:database` +* Compute Service Name is `cloudServersOpenStack` +* Image API Version is 2 +* Images must be in `vhd` format + +Dreamhost +--------- + +https://keystone.dream.io/v2.0 + +============== ================ +Region Name Human Name +============== ================ +RegionOne Region One +============== ================ + +* Image API Version is 2 +* Images must be in `raw` format + +Vexxhost +-------- + +http://auth.api.thenebulacloud.com:5000/v2.0/ + +============== ================ +Region Name Human Name +============== ================ +ca-ymq-1 Montreal +============== ================ + +* Image API Version is 1 +* Images must be in `qcow2` format + +RunAbove +-------- + +https://auth.runabove.io/v2.0 + +============== ================ +Region Name Human Name +============== ================ +SBG-1 Strassbourg, FR +BHS-1 Beauharnois, QC +============== ================ + +* Image API Version is 1 +* Images must be in `qcow2` format From c28877d490a3e1c516ef3311022e10ff4631c18d Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Tue, 14 Apr 2015 19:21:35 -0400 Subject: [PATCH 0180/3836] Add meta method obj_list_to_dict Turns out some data structures returned are actually lists of objects and we need a method to facilitate the conversion of these data structures. Change-Id: Icaea889051b75034bb48a138a2cb34c86e4a0453 --- shade/meta.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/shade/meta.py b/shade/meta.py index a562eb94a..80bee14e4 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -160,6 +160,19 @@ def obj_to_dict(obj): return instance +def obj_list_to_dict(list): + """Enumerate through lists of objects and return lists of dictonaries. + + Some of the objects returned in OpenStack are actually lists of objects, + and in order to expose the data structures as JSON, we need to facilitate + the conversion to lists of dictonaries. + """ + new_list = [] + for obj in list: + new_list.append(obj_to_dict(obj)) + return new_list + + def warlock_to_dict(obj): # glanceclient v2 uses warlock to construct its objects. Warlock does # deep black magic to attribute look up to support validation things that From fdccd14befbb79b4563dd3f1328c4024fb53f37e Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Fri, 3 Apr 2015 14:07:17 -0400 Subject: [PATCH 0181/3836] Change Ironic node lookups to support names Ironic's API has been changed to support a node being looked up by name or UUID in the form of a general ID. Added a get_machine method and re-mapped the pre-existing get_machine_by_uuid method to it. Incremented the python-ironicclient requirements version as required to support name based ID lookups. Change-Id: I765de8c1b0059a6667f66cd4e0695ddf08d96842 --- requirements.txt | 2 +- shade/__init__.py | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 54786dcff..c0a25f569 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ python-glanceclient python-cinderclient python-neutronclient>=2.3.10 python-troveclient -python-ironicclient +python-ironicclient>=0.5.1 python-swiftclient dogpile.cache>=0.5.3 diff --git a/shade/__init__.py b/shade/__init__.py index 15594375f..fc984726f 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1806,9 +1806,19 @@ def get_nic_by_mac(self, mac): def list_machines(self): return self.ironic_client.node.list() - def get_machine_by_uuid(self, uuid): + def get_machine(self, name_or_id): + """Get Machine by name or uuid + + Search the baremetal host out by utilizing the supplied id value + which can consist of a name or UUID. + + :param name_or_id: A node name or UUID that will be looked up. + + :returns: Dictonary representing the node found or None if no nodes + are found. + """ try: - return self.ironic_client.node.get(uuid) + return meta.obj_to_dict(self.ironic_client.node.get(name_or_id)) except ironic_exceptions.ClientException: return None From 3cfc64cf3c3da42da73e481a44023942880b8f18 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Wed, 15 Apr 2015 09:29:20 -0400 Subject: [PATCH 0182/3836] Basic test for meta method obj_list_to_dict Adding a basic test that exercises obj_list_to_dict and ultimately the obj_to_dict method Change-Id: I06ca3528c4e9f7c2eaccab22d17c8510f5f9f40e --- shade/tests/unit/test_meta.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 72859fee2..644a3841d 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -70,3 +70,16 @@ class Server(object): 'test-region_test-az', 'test-name_test-region_test-az'], meta.get_groups_from_server(Cloud(), Server(), server_vars)) + + def test_obj_list_to_dict(self): + """Test conversion of a list of objects to a list of dictonaries""" + class obj0(object): + value = 0 + + class obj1(object): + value = 1 + + list = [obj0, obj1] + new_list = meta.obj_list_to_dict(list) + self.assertEqual(new_list[0]['value'], 0) + self.assertEqual(new_list[1]['value'], 1) From 55665717dab19c59b358a5f05c95048ba37f6071 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Tue, 14 Apr 2015 17:01:32 -0400 Subject: [PATCH 0183/3836] Wrap ironicclient methods that leak objects Wrap calls to python-ironicclient methods that leak objects instead of dictionaries, so dictionaries are only returned moving forward. Change-Id: I6483ac8c0c316d1a4682e6327d7d5c90498c65f3 --- shade/__init__.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 97b3ef6a8..e16e45f4e 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2029,19 +2029,19 @@ def ironic_client(self): return self._ironic_client def list_nics(self): - return self.ironic_client.port.list() + return meta.obj_list_to_dict(self.ironic_client.port.list()) def list_nics_for_machine(self, uuid): - return self.ironic_client.node.list_ports(uuid) + return meta.obj_list_to_dict(self.ironic_client.node.list_ports(uuid)) def get_nic_by_mac(self, mac): try: - return self.ironic_client.port.get(mac) + return meta.obj_to_dict(self.ironic_client.port.get(mac)) except ironic_exceptions.ClientException: return None def list_machines(self): - return self.ironic_client.node.list() + return meta.obj_list_to_dict(self.ironic_client.node.list()) def get_machine(self, name_or_id): """Get Machine by name or uuid @@ -2062,7 +2062,8 @@ def get_machine(self, name_or_id): def get_machine_by_mac(self, mac): try: port = self.ironic_client.port.get(mac) - return self.ironic_client.node.get(port.node_uuid) + return meta.obj_to_dict( + self.ironic_client.node.get(port.node_uuid)) except ironic_exceptions.ClientException: return None @@ -2088,7 +2089,7 @@ def register_machine(self, nics, **kwargs): self.ironic_client.node.delete(machine.uuid) raise OpenStackCloudException( "Error registering NICs with Ironic: %s" % e.message) - return machine + return meta.obj_to_dict(machine) def unregister_machine(self, nics, uuid): for nic in nics: @@ -2122,10 +2123,12 @@ def validate_node(self, uuid): def node_set_provision_state(self, uuid, state, configdrive=None): try: - self.ironic_client.node.set_provision_state( - uuid, - state, - configdrive + return meta.obj_to_dict( + self.ironic_client.node.set_provision_state( + uuid, + state, + configdrive + ) ) except Exception as e: self.log.debug( @@ -2134,14 +2137,16 @@ def node_set_provision_state(self, uuid, state, configdrive=None): raise OpenStackCloudException(e.message) def activate_node(self, uuid, configdrive=None): - self.node_set_provision_state(uuid, 'active', configdrive) + return meta.obj_to_dict( + self.node_set_provision_state(uuid, 'active', configdrive)) def deactivate_node(self, uuid): self.node_set_provision_state(uuid, 'deleted') def set_node_instance_info(self, uuid, patch): try: - self.ironic_client.node.update(uuid, patch) + return meta.obj_to_dict( + self.ironic_client.node.update(uuid, patch)) except Exception as e: self.log.debug( "Failed to update instance_info", exc_info=True) @@ -2151,7 +2156,8 @@ def purge_node_instance_info(self, uuid): patch = [] patch.append({'op': 'remove', 'path': '/instance_info'}) try: - return self.ironic_client.node.update(uuid, patch) + return meta.obj_to_dict( + self.ironic_client.node.update(uuid, patch)) except Exception as e: self.log.debug( "Failed to delete instance_info", exc_info=True) From 94e392c822ac92ea576ce763fbf36fba677344e2 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Fri, 3 Apr 2015 16:49:41 -0400 Subject: [PATCH 0184/3836] Add patch_machine method and operator unit test substrate Add method to allow machine configuration to be patched via shade in addition to a basic unit test and the requisite substrate for unit testing OperatorCloud methods. Change-Id: I1095be60748186c876d6267768f8353b67200eb1 --- shade/__init__.py | 49 ++++++++++++++++++++++++++++++++++ shade/_tasks.py | 5 ++++ shade/tests/unit/test_shade.py | 18 +++++++++++++ 3 files changed, 72 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index e16e45f4e..61814e4f2 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2108,6 +2108,55 @@ def unregister_machine(self, nics, uuid): raise OpenStackCloudException( "Error unregistering machine from Ironic: %s" % e.message) + def patch_machine(self, name_or_id, patch): + """Patch Machine Information + + This method allows for an interface to manipulate node entries + within Ironic. Specifically, it is a pass-through for the + ironicclient.nodes.update interface which allows the Ironic Node + properties to be updated. + + :param node_id: The server object to attach to. + :param patch: The JSON Patch document is a list of dictonary objects + that comply with RFC 6902 which can be found at + https://tools.ietf.org/html/rfc6902. + + Example patch construction: + + patch=[] + patch.append({ + 'op': 'remove', + 'path': '/instance_info' + }) + patch.append({ + 'op': 'replace', + 'path': '/name', + 'value': 'newname' + }) + patch.append({ + 'op': 'add', + 'path': '/driver_info/username', + 'value': 'administrator' + }) + + :raises: OpenStackCloudException on operation error. + + :returns: Dictonary representing the newly updated node. + """ + + try: + return meta.obj_to_dict( + self.manager.submitTask( + _tasks.MachinePatch(node_id=name_or_id, + patch=patch, + http_method='PATCH'))) + except Exception as e: + self.log.debug( + "Machine patch update failed", exc_info=True) + raise OpenStackCloudException( + "Error updating machine via patch operation. node: %s. " + "%s" % (name_or_id, e)) + def validate_node(self, uuid): try: ifaces = self.ironic_client.node.validate(uuid) diff --git a/shade/_tasks.py b/shade/_tasks.py index 5b78a70e3..202f255d2 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -256,3 +256,8 @@ def main(self, client): class SubnetUpdate(task_manager.Task): def main(self, client): return client.neutron_client.update_subnet(**self.args) + + +class MachinePatch(task_manager.Task): + def main(self, client): + return client.ironic_client.node.update(**self.args) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index c566dbc4f..918d1f1bc 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -174,3 +174,21 @@ def test_delete_subnet_multiple_using_id(self, mock_client, mock_list): def test_update_subnet(self, mock_client): self.cloud.update_subnet(subnet_id=123, subnet_name='goofy') self.assertTrue(mock_client.update_subnet.called) + + +class TestShadeOperator(base.TestCase): + + def setUp(self): + super(TestShadeOperator, self).setUp() + self.cloud = shade.operator_cloud() + + def test_operator_cloud(self): + self.assertIsInstance(self.cloud, shade.OperatorCloud) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_patch_machine(self, mock_client): + node_id = 'node01' + patch = [] + patch.append({'op': 'remove', 'path': '/instance_info'}) + self.cloud.patch_machine(node_id, patch) + self.assertTrue(mock_client.node.update.called) From 0fd99acb5b0ef0461330daab065edef0449122e9 Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Mon, 13 Apr 2015 15:59:59 -0700 Subject: [PATCH 0185/3836] MonkeyPatch time.sleep in unit tests to avoid wait There's no need to wait real world seconds when there are no real world servers at the other end of these API calls. We still do wait a little bit of time, in case there is some reliance on actually having called the real time.sleep, but this should feed up test running in a loop quite a bit. Also lowering timeout on rebuild_server as it unnecessarily sleeps for 1 second. Change-Id: Ic26e90af12aedbedfbe0cc468332b921516a8409 --- shade/tests/base.py | 2 +- shade/tests/unit/base.py | 42 +++++++++++++++++++++++++ shade/tests/unit/test_caching.py | 4 +-- shade/tests/unit/test_rebuild_server.py | 4 +-- shade/tests/unit/test_shade.py | 2 +- 5 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 shade/tests/unit/base.py diff --git a/shade/tests/base.py b/shade/tests/base.py index 0b5b1f446..41d89a04a 100644 --- a/shade/tests/base.py +++ b/shade/tests/base.py @@ -25,7 +25,7 @@ class TestCase(testtools.TestCase): - """Test case base class for all unit tests.""" + """Test case base class for all tests.""" def setUp(self): """Run before each test method to initialize test environment.""" diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py new file mode 100644 index 000000000..2c7e57f43 --- /dev/null +++ b/shade/tests/unit/base.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + +# Copyright 2010-2011 OpenStack Foundation +# 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. + +import time + +import fixtures + +from shade.tests import base + + +class TestCase(base.TestCase): + + """Test case base class for all unit tests.""" + + def setUp(self): + """Run before each test method to initialize test environment.""" + + super(TestCase, self).setUp() + + # Sleeps are for real testing, but unit tests shouldn't need them + realsleep = time.sleep + + def _nosleep(seconds): + return realsleep(seconds * 0.0001) + + self.sleep_fixture = self.useFixture(fixtures.MonkeyPatch( + 'time.sleep', + _nosleep)) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 648ef54b8..35c901a8f 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -18,7 +18,7 @@ import yaml import shade -from shade.tests import base +from shade.tests.unit import base class TestMemoryCache(base.TestCase): @@ -26,7 +26,7 @@ class TestMemoryCache(base.TestCase): CACHING_CONFIG = { 'cache': { - 'max_age': 10, + 'max_age': 90, 'class': 'dogpile.cache.memory', }, 'clouds': diff --git a/shade/tests/unit/test_rebuild_server.py b/shade/tests/unit/test_rebuild_server.py index 6cba05f5d..960840a85 100644 --- a/shade/tests/unit/test_rebuild_server.py +++ b/shade/tests/unit/test_rebuild_server.py @@ -22,7 +22,7 @@ from mock import patch, Mock from shade import ( OpenStackCloud, OpenStackCloudException, OpenStackCloudTimeout) -from shade.tests import base +from shade.tests.unit import base class TestRebuildServer(base.TestCase): @@ -72,7 +72,7 @@ def test_rebuild_server_timeout(self): OpenStackCloud.nova_client = Mock(**config) self.assertRaises( OpenStackCloudTimeout, - self.client.rebuild_server, "a", "b", wait=True, timeout=1) + self.client.rebuild_server, "a", "b", wait=True, timeout=0.001) def test_rebuild_server_no_wait(self): """ diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 918d1f1bc..bfb2d498f 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -15,7 +15,7 @@ import mock import shade -from shade.tests import base +from shade.tests.unit import base class TestShade(base.TestCase): From fbe5fd06749201f0b80ab75fe0f4f71439df6b25 Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Tue, 14 Apr 2015 15:38:15 -0700 Subject: [PATCH 0186/3836] Add test for user_cache Since we intend to invalidate the user_cache, and properties and caching decorators don't play nicely, we split this into the property and a getter which can be called explicitly. Note that this test also uncovered a bug in user_cache, which was that it called a non-existent task. Change-Id: Ica04a82242cf4724155f99de5b8d3d9c356a8ec4 --- shade/__init__.py | 7 +++++-- shade/tests/unit/test_caching.py | 7 +++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index b841c886f..7a1d7a072 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -469,9 +469,12 @@ def delete_project(self, name_or_id): project=name_or_id, message=e.message)) @property - @_cache_on_arguments() def user_cache(self): - user_list = self.manager.submitTask(_tasks.UserListTask()) + return self.get_user_cache() + + @_cache_on_arguments() + def get_user_cache(self): + user_list = self.manager.submitTask(_tasks.UserList()) return {user.id: user for user in user_list} def _get_user(self, name_or_id): diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 35c901a8f..bb3d9a88b 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -161,3 +161,10 @@ def now_gone(): cinder_mock.volumes.delete.return_value = mock_vol self.cloud.delete_volume('12345') self.assertEqual([mock_volb4], self.cloud.list_volumes()) + + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_get_user_cache(self, keystone_mock): + mock_user = mock.MagicMock() + mock_user.id = '999' + keystone_mock.users.list.return_value = [mock_user] + self.assertEqual({'999': mock_user}, self.cloud.get_user_cache()) From 9ac0ef59c20e1f0a526e2fa3a528cb0a1670de4e Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 15 Apr 2015 17:11:38 +0000 Subject: [PATCH 0187/3836] Allow name or ID for update_subnet() Even though the subnet name is an updatable value, we should still allow specifying the name of the subnet to update along with the ID. This implies that we need to error if a name is used and multiple matches are found, since name is not a unique attribute. Change-Id: Ie57900a3a31f470c5e411e0c9c141ede8b95e0b2 --- shade/__init__.py | 23 +++++++++++++++++++---- shade/tests/unit/test_shade.py | 7 +++++-- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 97b3ef6a8..2a538797e 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1913,13 +1913,15 @@ def delete_subnet(self, name_or_id): raise OpenStackCloudException( "Error deleting subnet %s: %s" % (name_or_id, e)) - def update_subnet(self, subnet_id, subnet_name=None, enable_dhcp=None, + def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, gateway_ip=None, allocation_pools=None, dns_nameservers=None, host_routes=None): """Update an existing subnet. + :param string name_or_id: + Name or ID of the subnet to update. :param string subnet_name: - The name of the subnet. + The new name of the subnet. :param bool enable_dhcp: Set to ``True`` if DHCP is enabled and ``False`` if disabled. :param string gateway_ip: @@ -1977,14 +1979,27 @@ def update_subnet(self, subnet_id, subnet_name=None, enable_dhcp=None, self.log.debug("No subnet data to update") return + # Find matching subnets, raising an exception if none are found + # or multiple matches are found. + subnets = [] + for sub in self.list_subnets(): + if name_or_id in (sub['id'], sub['name']): + subnets.append(sub) + if not subnets: + raise OpenStackCloudException( + "Subnet %s not found." % name_or_id) + if len(subnets) > 1: + raise OpenStackCloudException( + "More than one subnet named %s. Use ID." % name_or_id) + try: new_subnet = self.manager.submitTask( _tasks.SubnetUpdate( - subnet=subnet_id, body=dict(subnet=subnet))) + subnet=subnets[0]['id'], body=dict(subnet=subnet))) except Exception as e: self.log.debug("Subnet update failed", exc_info=True) raise OpenStackCloudException( - "Error updating subnet %s: %s" % (subnet_id, e)) + "Error updating subnet %s: %s" % (name_or_id, e)) # Turns out neutron returns an actual dict, so no need for the # use of meta.obj_to_dict() here (which would not work against # a dict). diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index c566dbc4f..559b6a2fc 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -170,7 +170,10 @@ def test_delete_subnet_multiple_using_id(self, mock_client, mock_list): self.cloud.delete_subnet('123') self.assertTrue(mock_client.delete_subnet.called) + @mock.patch.object(shade.OpenStackCloud, 'list_subnets') @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_update_subnet(self, mock_client): - self.cloud.update_subnet(subnet_id=123, subnet_name='goofy') + def test_update_subnet(self, mock_client, mock_list): + subnet1 = dict(id='123', name='mickey') + mock_list.return_value = [subnet1] + self.cloud.update_subnet('123', subnet_name='goofy') self.assertTrue(mock_client.update_subnet.called) From 3f6f25c100b5cad760c72c3c428d39859e6ab415 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 15 Apr 2015 17:27:14 +0000 Subject: [PATCH 0188/3836] Allow name or ID for update_router() Even though the router name is an updatable value, we should still allow specifying the name of the router to update along with the ID. This implies that we need to error if a name is used and multiple matches are found, since name is not a unique attribute. Change-Id: I17535ad363f0ff19fdc0fdde9fa472c7ce263b23 --- shade/__init__.py | 23 ++++++++++++++++++----- shade/tests/unit/test_shade.py | 7 +++++-- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 97b3ef6a8..4b91c27c1 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -834,12 +834,12 @@ def create_router(self, name=None, admin_state_up=True): # a dict). return new_router['router'] - def update_router(self, router_id, name=None, admin_state_up=None, + def update_router(self, name_or_id, name=None, admin_state_up=None, ext_gateway_net_id=None): """Update an existing logical router. - :param router_id: The router UUID. - :param name: The router name. + :param name_or_id: The name or UUID of the router to update. + :param name: The new router name. :param admin_state_up: The administrative state of the router. :param ext_gateway_net_id: The network ID for the external gateway. @@ -860,14 +860,27 @@ def update_router(self, router_id, name=None, admin_state_up=None, self.log.debug("No router data to update") return + # Find matching routers, raising an exception if none are found + # or multiple matches are found. + routers = [] + for r in self.list_routers(): + if name_or_id in (r['id'], r['name']): + routers.append(r) + if not routers: + raise OpenStackCloudException( + "Router %s not found." % name_or_id) + if len(routers) > 1: + raise OpenStackCloudException( + "More than one router named %s. Use ID." % name_or_id) + try: new_router = self.manager.submitTask( _tasks.RouterUpdate( - router=router_id, body=dict(router=router))) + router=routers[0]['id'], body=dict(router=router))) except Exception as e: self.log.debug("Router update failed", exc_info=True) raise OpenStackCloudException( - "Error updating router %s: %s" % (router_id, e)) + "Error updating router %s: %s" % (name_or_id, e)) # Turns out neutron returns an actual dict, so no need for the # use of meta.obj_to_dict() here (which would not work against # a dict). diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index c566dbc4f..c16699c36 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -54,9 +54,12 @@ def test_create_router(self, mock_client): self.cloud.create_router(name='goofy', admin_state_up=True) self.assertTrue(mock_client.create_router.called) + @mock.patch.object(shade.OpenStackCloud, 'list_routers') @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_update_router(self, mock_client): - self.cloud.update_router(router_id=123, name='goofy') + def test_update_router(self, mock_client, mock_list): + router1 = dict(id='123', name='mickey') + mock_list.return_value = [router1] + self.cloud.update_router('123', name='goofy') self.assertTrue(mock_client.update_router.called) @mock.patch.object(shade.OpenStackCloud, 'list_routers') From 8ebba2af62f7b917427fa1233ad81314f4e47102 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Thu, 9 Apr 2015 14:16:08 -0400 Subject: [PATCH 0189/3836] Add test of OperatorCloud auth_type=None Ironic features a noauth mode which is intended for use in isolated trusted environments. As this is not a normal use case for shade, it is an increadibly important item to have a test for. Change-Id: If86b9df238982d912105fb08dcd59c9c85b7de4a --- shade/tests/unit/test_operator_noauth.py | 49 ++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 shade/tests/unit/test_operator_noauth.py diff --git a/shade/tests/unit/test_operator_noauth.py b/shade/tests/unit/test_operator_noauth.py new file mode 100644 index 000000000..f32c6696a --- /dev/null +++ b/shade/tests/unit/test_operator_noauth.py @@ -0,0 +1,49 @@ +# Copyright (c) 2015 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. + +import mock + +import ironicclient +import shade +from shade.tests import base + + +class TestShadeOperatorNoAuth(base.TestCase): + def setUp(self): + """Setup Noauth OperatorCloud tests + + Setup the test to utilize no authentication and an endpoint + URL in the auth data. This is permits testing of the basic + mechanism that enables Ironic noauth mode to be utilized with + Shade. + """ + super(TestShadeOperatorNoAuth, self).setUp() + self.cloud_noauth = shade.operator_cloud( + auth_type='None', + auth=dict(endpoint="http://localhost:6385") + ) + + @mock.patch.object(shade.OperatorCloud, 'get_endpoint') + @mock.patch.object(ironicclient.client, 'Client') + def test_ironic_noauth_selection_using_a_task( + self, mock_client, mock_endpoint): + """Test noauth selection for Ironic in OperatorCloud + + Utilize a task to trigger the client connection attempt + and evaluate if get_endpoint was called while the client + was still called. + """ + self.cloud_noauth.patch_machine('name', {}) + self.assertFalse(mock_endpoint.called) + self.assertTrue(mock_client.called) From 9dfa4b8f946301a5742ba4fb46c5659b509cb7f5 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Tue, 7 Apr 2015 12:59:08 -0400 Subject: [PATCH 0190/3836] Update register_machine to use tasks Updated register_machine to utilize tasks instead of direct calls to the ironicclient library which resulted in additional tasks beyond the initial MachineCreate task. Added MachinePortCreate along with MachinePortDelete and MachineDelete as the register_machine includes rollback functionality if port creation fails. Additionally, as part of this, the failure handling logic has been cleaned up in order to help ensure that all ports and nodes are completely deleted. Basic tests added to validate methods execute as expected. Change-Id: Ide208ce5445bbc50c0a9bd6476bf7fe306658831 --- shade/__init__.py | 57 +++++++++++++++++++++++++++++----- shade/_tasks.py | 20 ++++++++++++ shade/tests/unit/test_shade.py | 27 ++++++++++++++++ 3 files changed, 96 insertions(+), 8 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 61814e4f2..1c2ab70ce 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2068,27 +2068,68 @@ def get_machine_by_mac(self, mac): return None def register_machine(self, nics, **kwargs): + """Register Baremetal with Ironic + + Allows for the registration of Baremetal nodes with Ironic + and population of pertinant node information or configuration + to be passed to the Ironic API for the node. + + This method also creates ports for a list of MAC addresses passed + in to be utilized for boot and potentially network configuration. + + If a failure is detected creating the network ports, any ports + created are deleted, and the node is removed from Ironic. + + :param list nics: An array of MAC addresses that represent the + network interfaces for the node to be created. + + Example: + [ + {'mac': 'aa:bb:cc:dd:ee:01'}, + {'mac': 'aa:bb:cc:dd:ee:02'} + ] + + :param **kwargs: Key value pairs to be passed to the Ironic API, + including uuid, name, chassis_uuid, driver_info, + parameters. + + :raises: OpenStackCloudException on operation error. + + :returns: Returns a dictonary representing the new + baremetal node. + """ try: - machine = self.ironic_client.node.create(**kwargs) + machine = self.manager.submitTask( + _tasks.MachineCreate(**kwargs)) except Exception as e: self.log.debug("ironic machine registration failed", exc_info=True) raise OpenStackCloudException( - "Error registering machine with Ironic: %s" % e.message) + "Error registering machine with Ironic: %s" % e) created_nics = [] try: for row in nics: - nic = self.ironic_client.port.create(address=row['mac'], - node_uuid=machine.uuid) + nic = self.manager.submitTask( + _tasks.MachinePortCreate(address=row['mac'], + node_uuid=machine.uuid)) created_nics.append(nic.uuid) + except Exception as e: self.log.debug("ironic NIC registration failed", exc_info=True) # TODO(mordred) Handle failures here - for uuid in created_nics: - self.ironic_client.port.delete(uuid) - self.ironic_client.node.delete(machine.uuid) + try: + for uuid in created_nics: + try: + self.manager.submitTask( + _tasks.MachinePortDelete( + port_id=uuid)) + except: + pass + finally: + self.manager.submitTask( + _tasks.MachineDelete(node_id=machine.uuid)) raise OpenStackCloudException( - "Error registering NICs with Ironic: %s" % e.message) + "Error registering NICs with baremetal service: %s" % e) return meta.obj_to_dict(machine) def unregister_machine(self, nics, uuid): diff --git a/shade/_tasks.py b/shade/_tasks.py index 202f255d2..07d43cf21 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -258,6 +258,26 @@ def main(self, client): return client.neutron_client.update_subnet(**self.args) +class MachineCreate(task_manager.Task): + def main(self, client): + return client.ironic_client.node.create(**self.args) + + +class MachineDelete(task_manager.Task): + def main(self, client): + return client.ironic_client.node.delete(**self.args) + + class MachinePatch(task_manager.Task): def main(self, client): return client.ironic_client.node.update(**self.args) + + +class MachinePortCreate(task_manager.Task): + def main(self, client): + return client.ironic_client.port.create(**self.args) + + +class MachinePortDelete(task_manager.Task): + def main(self, client): + return client.ironic_client.port.delete(**self.args) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 918d1f1bc..ca2ee140e 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -192,3 +192,30 @@ def test_patch_machine(self, mock_client): patch.append({'op': 'remove', 'path': '/instance_info'}) self.cloud.patch_machine(node_id, patch) self.assertTrue(mock_client.node.update.called) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_register_machine(self, mock_client): + class fake_node: + uuid = "00000000-0000-0000-0000-000000000000" + + expected_return_value = dict( + uuid="00000000-0000-0000-0000-000000000000", + ) + mock_client.node.create.return_value = fake_node + nics = [{'mac': '00:00:00:00:00:00'}] + return_value = self.cloud.register_machine(nics) + self.assertDictEqual(expected_return_value, return_value) + self.assertTrue(mock_client.node.create.called) + self.assertTrue(mock_client.port.create.called) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_register_machine_port_create_failed(self, mock_client): + nics = [{'mac': '00:00:00:00:00:00'}] + mock_client.port.create.side_effect = ( + shade.OpenStackCloudException("Error")) + self.assertRaises(shade.OpenStackCloudException, + self.cloud.register_machine, + nics) + self.assertTrue(mock_client.node.create.called) + self.assertTrue(mock_client.port.create.called) + self.assertTrue(mock_client.node.delete.called) From a591e3e732e9801d72aa1b697ddcfb4771703f16 Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Tue, 14 Apr 2015 15:49:58 -0700 Subject: [PATCH 0191/3836] Invalidate user cache on user create Change-Id: Iadca03e63c97d01d149c1c4940a7abf604c7a293 --- shade/__init__.py | 1 + shade/tests/unit/test_caching.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index a5f08bef5..0b546c102 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -520,6 +520,7 @@ def create_user( raise OpenStackCloudException( "Error in creating user {user}: {message}".format( user=name, message=e.message)) + self.get_user_cache.invalidate(self) def delete_user(self, name_or_id): try: diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index bb3d9a88b..5c09de0ce 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -168,3 +168,17 @@ def test_get_user_cache(self, keystone_mock): mock_user.id = '999' keystone_mock.users.list.return_value = [mock_user] self.assertEqual({'999': mock_user}, self.cloud.get_user_cache()) + + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_modify_user_invalidates_cache(self, keystone_mock): + mock_user = mock.MagicMock() + mock_user.id = 'abc123' + # first cache an empty list + keystone_mock.users.list.return_value = [] + self.assertEqual({}, self.cloud.get_user_cache()) + # now add one + keystone_mock.users.list.return_value = [mock_user] + keystone_mock.users.create.return_value = mock_user + self.cloud.create_user(name='abc123 name') + # Cache should have been invalidated + self.assertEqual({'abc123': mock_user}, self.cloud.get_user_cache()) From c408583570ed9d5d823ff7766d5a2c73701a6f45 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Tue, 7 Apr 2015 14:16:04 -0400 Subject: [PATCH 0192/3836] Update unregister_machine to use tasks Updated the unregister_machine to utilize tasks for the main actions taken by the module. Additionally, added a docstring to the method and a basic unit. Change-Id: I738d8573b0c5f7ae693a73326ffa517d401a8c53 --- shade/__init__.py | 39 +++++++++++++++++++++++++++------- shade/tests/unit/test_shade.py | 9 ++++++++ 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 1c2ab70ce..6bd9cafd7 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2129,25 +2129,48 @@ def register_machine(self, nics, **kwargs): self.manager.submitTask( _tasks.MachineDelete(node_id=machine.uuid)) raise OpenStackCloudException( - "Error registering NICs with baremetal service: %s" % e) + "Error registering NICs with the baremetal service: %s" % e) return meta.obj_to_dict(machine) def unregister_machine(self, nics, uuid): + """Unregister Baremetal from Ironic + + Removes entries for Network Interfaces and baremetal nodes + from an Ironic API + + :param list nics: An array of strings that consist of MAC addresses + to be removed. + :param string uuid: The UUID of the node to be deleted. + + :raises: OpenStackCloudException on operation failure. + """ + + # TODO(TheJulia): Change to lookup the MAC addresses and/or block any + # the action if the node is in an Active state as the API would. for nic in nics: try: - self.ironic_client.port.delete( - self.ironic_client.port.get_by_address(nic['mac'])) + self.manager.submitTask( + _tasks.MachinePortDelete( + port_id=( + self.ironic_client.port.get_by_address(nic['mac']) + ))) + except Exception as e: self.log.debug( - "ironic NIC unregistration failed", exc_info=True) - raise OpenStackCloudException(e.message) + "baremetal NIC unregistration failed", exc_info=True) + raise OpenStackCloudException( + "Error removing NIC '%s' from baremetal API for " + "node '%s'. Error: %s" % (nic, uuid, e)) try: - self.ironic_client.node.delete(uuid) + self.manager.submitTask( + _tasks.MachineDelete(node_id=uuid)) + except Exception as e: self.log.debug( - "ironic machine unregistration failed", exc_info=True) + "baremetal machine unregistration failed", exc_info=True) raise OpenStackCloudException( - "Error unregistering machine from Ironic: %s" % e.message) + "Error unregistering machine %s from the baremetal API. " + "Error: %s" % (uuid, e)) def patch_machine(self, name_or_id, patch): """Patch Machine Information diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index ca2ee140e..34e6d9120 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -219,3 +219,12 @@ def test_register_machine_port_create_failed(self, mock_client): self.assertTrue(mock_client.node.create.called) self.assertTrue(mock_client.port.create.called) self.assertTrue(mock_client.node.delete.called) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_unregister_machine(self, mock_client): + nics = [{'mac': '00:00:00:00:00:00'}] + uuid = "00000000-0000-0000-0000-000000000000" + self.cloud.unregister_machine(nics, uuid) + self.assertTrue(mock_client.node.delete.called) + self.assertTrue(mock_client.port.delete.called) + self.assertTrue(mock_client.port.get_by_address.called) From f351d4f65d1db65959fa4e171979254002fbb501 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 17 Apr 2015 15:00:53 -0400 Subject: [PATCH 0193/3836] Search methods for networks, subnets and routers Add get/list/search methods for network, subnet, and router objects. Change-Id: I3179cb88ec8a238165d629ddd23a619aae112879 --- shade/__init__.py | 133 ++++++++++++++++++++------------- shade/tests/unit/test_shade.py | 94 +++++++++++++---------- 2 files changed, 136 insertions(+), 91 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 0b546c102..859f3f9f3 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -738,32 +738,85 @@ def extension_cache(self): def has_extension(self, extension_name): return extension_name in self.extension_cache + def _filter_list(self, data, name_or_id, filters): + """Filter a list by name/ID and arbitrary meta data. + + :param list data: + The list of dictionary data to filter. It is expected that + each dictionary contains an 'id' and 'name' key if a value + for name_or_id is given. + :param string name_or_id: + The name or ID of the entity being filtered. + :param dict filters: + A dictionary of meta data to use for further filtering. + """ + if name_or_id: + identifier_matches = [] + for e in data: + if name_or_id in (e['id'], e['name']): + identifier_matches.append(e) + data = identifier_matches + + if not filters: + return data + + filtered = [] + for e in data: + filtered.append(e) + for key in filters.keys(): + if key not in e or e[key] != filters[key]: + filtered.pop() + break + return filtered + + def _get_entity(self, func, name_or_id, filters): + """Return a single entity from the list returned by a given method. + + :param callable func: + A function that takes `name_or_id` and `filters` as parameters + and returns a list of entities to filter. + :param string name_or_id: + The name or ID of the entity being filtered. + :param dict filters: + A dictionary of meta data to use for further filtering. + """ + entities = func(name_or_id, filters) + if not entities: + return None + if len(entities) > 1: + raise OpenStackCloudException( + "Multiple matches found for %s" % name_or_id) + return entities[0] + + def search_networks(self, name_or_id=None, filters=None): + networks = self.list_networks() + return self._filter_list(networks, name_or_id, filters) + + def search_routers(self, name_or_id=None, filters=None): + routers = self.list_routers() + return self._filter_list(routers, name_or_id, filters) + + def search_subnets(self, name_or_id=None, filters=None): + subnets = self.list_subnets() + return self._filter_list(subnets, name_or_id, filters) + def list_networks(self): return self.manager.submitTask(_tasks.NetworkList())['networks'] - def get_network(self, name_or_id): - for network in self.list_networks(): - if name_or_id in (network['id'], network['name']): - return network - return None - def list_routers(self): return self.manager.submitTask(_tasks.RouterList())['routers'] - def get_router(self, name_or_id): - for router in self.list_routers(): - if name_or_id in (router['id'], router['name']): - return router - return None - def list_subnets(self): return self.manager.submitTask(_tasks.SubnetList())['subnets'] - def get_subnet(self, name_or_id): - for subnet in self.list_subnets(): - if name_or_id in (subnet['id'], subnet['name']): - return subnet - return None + def get_network(self, name_or_id, filters=None): + return self._get_entity(self.search_networks, name_or_id, filters) + + def get_router(self, name_or_id, filters=None): + return self._get_entity(self.search_routers, name_or_id, filters) + + def get_subnet(self, name_or_id, filters=None): + return self._get_entity(self.search_subnets, name_or_id, filters) # TODO(Shrews): This will eventually need to support tenant ID and # provider networks, which are admin-level params. @@ -803,6 +856,10 @@ def delete_network(self, name_or_id): :raises: OpenStackCloudException on operation error. """ network = self.get_network(name_or_id) + if not network: + raise OpenStackCloudException( + "Network %s not found." % name_or_id) + try: self.manager.submitTask( _tasks.NetworkDelete(network=network['id'])) @@ -900,22 +957,14 @@ def delete_router(self, name_or_id): :param name_or_id: Name or ID of the router being deleted. :raises: OpenStackCloudException on operation error. """ - routers = [] - for router in self.list_routers(): - if name_or_id in (router['id'], router['name']): - routers.append(router) - - if not routers: + router = self.get_router(name_or_id) + if not router: raise OpenStackCloudException( "Router %s not found." % name_or_id) - if len(routers) > 1: - raise OpenStackCloudException( - "More than one router named %s. Use ID." % name_or_id) - try: self.manager.submitTask( - _tasks.RouterDelete(router=routers[0]['id'])) + _tasks.RouterDelete(router=router['id'])) except Exception as e: self.log.debug("Router delete failed", exc_info=True) raise OpenStackCloudException( @@ -1853,23 +1902,15 @@ def create_subnet(self, network_name_or_id, cidr, ip_version=4, :raises: OpenStackCloudException on operation error. """ - networks = [] - for network in self.list_networks(): - if network_name_or_id in (network['id'], network['name']): - networks.append(network) - - if not networks: + network = self.get_network(network_name_or_id) + if not network: raise OpenStackCloudException( "Network %s not found." % network_name_or_id) - if len(networks) > 1: - raise OpenStackCloudException( - "More than one network named %s. Use ID." % network_name_or_id) - # The body of the neutron message for the subnet we wish to create. # This includes attributes that are required or have defaults. subnet = { - 'network_id': networks[0]['id'], + 'network_id': network['id'], 'cidr': cidr, 'ip_version': ip_version, 'enable_dhcp': enable_dhcp @@ -1914,22 +1955,14 @@ def delete_subnet(self, name_or_id): :param name_or_id: Name or ID of the subnet being deleted. :raises: OpenStackCloudException on operation error. """ - subnets = [] - for subnet in self.list_subnets(): - if name_or_id in (subnet['id'], subnet['name']): - subnets.append(subnet) - - if not subnets: + subnet = self.get_subnet(name_or_id) + if not subnet: raise OpenStackCloudException( "Subnet %s not found." % name_or_id) - if len(subnets) > 1: - raise OpenStackCloudException( - "More than one subnet named %s. Use ID." % name_or_id) - try: self.manager.submitTask( - _tasks.SubnetDelete(subnet=subnets[0]['id'])) + _tasks.SubnetDelete(subnet=subnet['id'])) except Exception as e: self.log.debug("Subnet delete failed", exc_info=True) raise OpenStackCloudException( diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 94addf8b1..32d21969b 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -27,25 +27,39 @@ def setUp(self): def test_openstack_cloud(self): self.assertIsInstance(self.cloud, shade.OpenStackCloud) - @mock.patch.object(shade.OpenStackCloud, 'list_subnets') - def test_get_subnet(self, mock_list): + def test__filter_list_name_or_id(self): + el1 = dict(id=100, name='donald') + el2 = dict(id=200, name='pluto') + data = [el1, el2] + ret = self.cloud._filter_list(data, 'donald', None) + self.assertEquals([el1], ret) + + def test__filter_list_filter(self): + el1 = dict(id=100, name='donald', other='duck') + el2 = dict(id=200, name='donald', other='trump') + data = [el1, el2] + ret = self.cloud._filter_list(data, 'donald', {'other': 'duck'}) + self.assertEquals([el1], ret) + + @mock.patch.object(shade.OpenStackCloud, 'search_subnets') + def test_get_subnet(self, mock_search): subnet = dict(id='123', name='mickey') - mock_list.return_value = [subnet] + mock_search.return_value = [subnet] r = self.cloud.get_subnet('mickey') self.assertIsNotNone(r) self.assertDictEqual(subnet, r) - @mock.patch.object(shade.OpenStackCloud, 'list_routers') - def test_get_router(self, mock_list): + @mock.patch.object(shade.OpenStackCloud, 'search_routers') + def test_get_router(self, mock_search): router1 = dict(id='123', name='mickey') - mock_list.return_value = [router1] + mock_search.return_value = [router1] r = self.cloud.get_router('mickey') self.assertIsNotNone(r) self.assertDictEqual(router1, r) - @mock.patch.object(shade.OpenStackCloud, 'list_routers') - def test_get_router_not_found(self, mock_list): - mock_list.return_value = [] + @mock.patch.object(shade.OpenStackCloud, 'search_routers') + def test_get_router_not_found(self, mock_search): + mock_search.return_value = [] r = self.cloud.get_router('goofy') self.assertIsNone(r) @@ -62,49 +76,48 @@ def test_update_router(self, mock_client, mock_list): self.cloud.update_router('123', name='goofy') self.assertTrue(mock_client.update_router.called) - @mock.patch.object(shade.OpenStackCloud, 'list_routers') + @mock.patch.object(shade.OpenStackCloud, 'search_routers') @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_router(self, mock_client, mock_list): + def test_delete_router(self, mock_client, mock_search): router1 = dict(id='123', name='mickey') - mock_list.return_value = [router1] + mock_search.return_value = [router1] self.cloud.delete_router('mickey') self.assertTrue(mock_client.delete_router.called) - @mock.patch.object(shade.OpenStackCloud, 'list_routers') + @mock.patch.object(shade.OpenStackCloud, 'search_routers') @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_router_not_found(self, mock_client, mock_list): - router1 = dict(id='123', name='mickey') - mock_list.return_value = [router1] + def test_delete_router_not_found(self, mock_client, mock_search): + mock_search.return_value = [] self.assertRaises(shade.OpenStackCloudException, self.cloud.delete_router, 'goofy') self.assertFalse(mock_client.delete_router.called) - @mock.patch.object(shade.OpenStackCloud, 'list_routers') @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_router_multiple_found(self, mock_client, mock_list): + def test_delete_router_multiple_found(self, mock_client): router1 = dict(id='123', name='mickey') router2 = dict(id='456', name='mickey') - mock_list.return_value = [router1, router2] + mock_client.list_routers.return_value = dict(routers=[router1, + router2]) self.assertRaises(shade.OpenStackCloudException, self.cloud.delete_router, 'mickey') self.assertFalse(mock_client.delete_router.called) - @mock.patch.object(shade.OpenStackCloud, 'list_routers') @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_router_multiple_using_id(self, mock_client, mock_list): + def test_delete_router_multiple_using_id(self, mock_client): router1 = dict(id='123', name='mickey') router2 = dict(id='456', name='mickey') - mock_list.return_value = [router1, router2] + mock_client.list_routers.return_value = dict(routers=[router1, + router2]) self.cloud.delete_router('123') self.assertTrue(mock_client.delete_router.called) - @mock.patch.object(shade.OpenStackCloud, 'list_networks') + @mock.patch.object(shade.OpenStackCloud, 'search_networks') @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_subnet(self, mock_client, mock_list): + def test_create_subnet(self, mock_client, mock_search): net1 = dict(id='123', name='donald') - mock_list.return_value = [net1] + mock_search.return_value = [net1] pool = [{'start': '192.168.199.2', 'end': '192.168.199.254'}] dns = ['8.8.8.8'] routes = [{"destination": "0.0.0.0/0", "nexthop": "123.456.78.9"}] @@ -124,52 +137,51 @@ def test_create_subnet_bad_network(self, mock_client, mock_list): 'duck', '192.168.199.0/24') self.assertFalse(mock_client.create_subnet.called) - @mock.patch.object(shade.OpenStackCloud, 'list_networks') + @mock.patch.object(shade.OpenStackCloud, 'search_networks') @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_subnet_non_unique_network(self, mock_client, mock_list): + def test_create_subnet_non_unique_network(self, mock_client, mock_search): net1 = dict(id='123', name='donald') net2 = dict(id='456', name='donald') - mock_list.return_value = [net1, net2] + mock_search.return_value = [net1, net2] self.assertRaises(shade.OpenStackCloudException, self.cloud.create_subnet, 'donald', '192.168.199.0/24') self.assertFalse(mock_client.create_subnet.called) - @mock.patch.object(shade.OpenStackCloud, 'list_subnets') + @mock.patch.object(shade.OpenStackCloud, 'search_subnets') @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_subnet(self, mock_client, mock_list): + def test_delete_subnet(self, mock_client, mock_search): subnet1 = dict(id='123', name='mickey') - mock_list.return_value = [subnet1] + mock_search.return_value = [subnet1] self.cloud.delete_subnet('mickey') self.assertTrue(mock_client.delete_subnet.called) - @mock.patch.object(shade.OpenStackCloud, 'list_subnets') + @mock.patch.object(shade.OpenStackCloud, 'search_subnets') @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_subnet_not_found(self, mock_client, mock_list): - subnet1 = dict(id='123', name='mickey') - mock_list.return_value = [subnet1] + def test_delete_subnet_not_found(self, mock_client, mock_search): + mock_search.return_value = [] self.assertRaises(shade.OpenStackCloudException, self.cloud.delete_subnet, 'goofy') self.assertFalse(mock_client.delete_subnet.called) - @mock.patch.object(shade.OpenStackCloud, 'list_subnets') @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_subnet_multiple_found(self, mock_client, mock_list): + def test_delete_subnet_multiple_found(self, mock_client): subnet1 = dict(id='123', name='mickey') subnet2 = dict(id='456', name='mickey') - mock_list.return_value = [subnet1, subnet2] + mock_client.list_subnets.return_value = dict(subnets=[subnet1, + subnet2]) self.assertRaises(shade.OpenStackCloudException, self.cloud.delete_subnet, 'mickey') self.assertFalse(mock_client.delete_subnet.called) - @mock.patch.object(shade.OpenStackCloud, 'list_subnets') @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_subnet_multiple_using_id(self, mock_client, mock_list): + def test_delete_subnet_multiple_using_id(self, mock_client): subnet1 = dict(id='123', name='mickey') subnet2 = dict(id='456', name='mickey') - mock_list.return_value = [subnet1, subnet2] + mock_client.list_subnets.return_value = dict(subnets=[subnet1, + subnet2]) self.cloud.delete_subnet('123') self.assertTrue(mock_client.delete_subnet.called) From c72204f6d1ac701148f3de3cb94d90d732aa2e88 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 22 Apr 2015 11:21:12 -0400 Subject: [PATCH 0194/3836] Update volume API for new getters and dict retval This switches the volume API to use the new get/list/search style. Doing this necessitated that we fix another issue with the volume API and NOT return the volume object as received from cinder, but rather convert it to a dict using meta.obj_to_dict. Change-Id: I81ba97d2b4fc9465e1b3538e97f5ceb9bef4f7e8 --- shade/__init__.py | 123 ++++++++++++++++--------------- shade/tests/unit/test_caching.py | 28 ++++--- 2 files changed, 81 insertions(+), 70 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 859f3f9f3..643994914 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -171,7 +171,7 @@ def invalidate(obj, *args, **kwargs): def _no_pending_volumes(volumes): '''If there are any volumes not in a steady state, don't cache''' for volume in volumes: - if volume.status not in ('available', 'error'): + if volume['status'] not in ('available', 'error'): return False return True @@ -743,8 +743,8 @@ def _filter_list(self, data, name_or_id, filters): :param list data: The list of dictionary data to filter. It is expected that - each dictionary contains an 'id' and 'name' key if a value - for name_or_id is given. + each dictionary contains an 'id', 'name' (or 'display_name') + key if a value for name_or_id is given. :param string name_or_id: The name or ID of the entity being filtered. :param dict filters: @@ -753,7 +753,11 @@ def _filter_list(self, data, name_or_id, filters): if name_or_id: identifier_matches = [] for e in data: - if name_or_id in (e['id'], e['name']): + e_id = e.get('id', None) + e_name = e.get('name', None) + # cinder likes to be different and use display_name + e_display_name = e.get('display_name', None) + if name_or_id in (e_id, e_name, e_display_name): identifier_matches.append(e) data = identifier_matches @@ -800,6 +804,10 @@ def search_subnets(self, name_or_id=None, filters=None): subnets = self.list_subnets() return self._filter_list(subnets, name_or_id, filters) + def search_volumes(self, name_or_id=None, filters=None): + volumes = self.list_volumes() + return self._filter_list(volumes, name_or_id, filters) + def list_networks(self): return self.manager.submitTask(_tasks.NetworkList())['networks'] @@ -809,6 +817,15 @@ def list_routers(self): def list_subnets(self): return self.manager.submitTask(_tasks.SubnetList())['subnets'] + @_cache_on_arguments(should_cache_fn=_no_pending_volumes) + def list_volumes(self, cache=True): + if not cache: + warnings.warn('cache argument to list_volumes is deprecated. Use ' + 'invalidate instead.') + return meta.obj_list_to_dict( + self.manager.submitTask(_tasks.VolumeList()) + ) + def get_network(self, name_or_id, filters=None): return self._get_entity(self.search_networks, name_or_id, filters) @@ -818,6 +835,9 @@ def get_router(self, name_or_id, filters=None): def get_subnet(self, name_or_id, filters=None): return self._get_entity(self.search_subnets, name_or_id, filters) + def get_volume(self, name_or_id, filters=None): + return self._get_entity(self.search_volumes, name_or_id, filters) + # TODO(Shrews): This will eventually need to support tenant ID and # provider networks, which are admin-level params. def create_network(self, name, shared=False, admin_state_up=True): @@ -1220,23 +1240,25 @@ def create_volume(self, wait=True, timeout=None, **kwargs): "Error in creating volume: %s" % e.message) self.list_volumes.invalidate(self) - if volume.status == 'error': + volume = meta.obj_to_dict(volume) + + if volume['status'] == 'error': raise OpenStackCloudException("Error in creating volume") if wait: - vol_id = volume.id + vol_id = volume['id'] for count in _iterate_timeout( timeout, "Timeout waiting for the volume to be available."): - volume = self.get_volume(vol_id, error=False) + volume = self.get_volume(vol_id) if not volume: continue - if volume.status == 'available': + if volume['status'] == 'available': return volume - if volume.status == 'error': + if volume['status'] == 'error': raise OpenStackCloudException( "Error in creating volume, please check logs") @@ -1256,7 +1278,7 @@ def delete_volume(self, name_or_id=None, wait=True, timeout=None): try: self.manager.submitTask( - _tasks.VolumeDelete(volume=volume.id)) + _tasks.VolumeDelete(volume=volume['id'])) except Exception as e: self.log.debug("Volume deletion failed", exc_info=True) raise OpenStackCloudException( @@ -1267,48 +1289,25 @@ def delete_volume(self, name_or_id=None, wait=True, timeout=None): for count in _iterate_timeout( timeout, "Timeout waiting for the volume to be deleted."): - if not self.volume_exists(volume.id): + if not self.volume_exists(volume['id']): return - def _get_volumes_from_cloud(self): - try: - return self.manager.submitTask(_tasks.VolumeList()) - except Exception: - return [] - - @_cache_on_arguments(should_cache_fn=_no_pending_volumes) - def list_volumes(self, cache=True): - if not cache: - warnings.warn('cache argument to list_volumes is deprecated. Use ' - ' invalidate instead.') - return self._get_volumes_from_cloud() - def get_volumes(self, server, cache=True): volumes = [] for volume in self.list_volumes(cache=cache): - for attach in volume.attachments: + for attach in volume['attachments']: if attach['server_id'] == server.id: volumes.append(volume) return volumes def get_volume_id(self, name_or_id): - image = self.get_volume(name_or_id) - if image: - return image.id - return None - - def get_volume(self, name_or_id, cache=True, error=True): - for v in self.list_volumes(cache=cache): - if name_or_id in (v.display_name, v.id): - return v - if error: - raise OpenStackCloudException( - "Error finding volume from %s" % name_or_id) + volume = self.get_volume(name_or_id) + if volume: + return volume['id'] return None def volume_exists(self, name_or_id): - return self.get_volume( - name_or_id, error=False) is not None + return self.get_volume(name_or_id) is not None def get_volume_attach_device(self, volume, server_id): """Return the device name a volume is attached to for a server. @@ -1321,7 +1320,7 @@ def get_volume_attach_device(self, volume, server_id): :returns: Device name if attached, None if volume is not attached. """ - for attach in volume.attachments: + for attach in volume['attachments']: if server_id == attach['server_id']: return attach['device'] return None @@ -1341,38 +1340,38 @@ def detach_volume(self, server, volume, wait=True, timeout=None): if not dev: raise OpenStackCloudException( "Volume %s is not attached to server %s" - % (volume.id, server.id) + % (volume['id'], server.id) ) try: self.manager.submitTask( - _tasks.VolumeDetach(attachment_id=volume.id, + _tasks.VolumeDetach(attachment_id=volume['id'], server_id=server.id)) except Exception as e: self.log.debug("nova volume detach failed", exc_info=True) raise OpenStackCloudException( "Error detaching volume %s from server %s: %s" % - (volume.id, server.id, e) + (volume['id'], server.id, e) ) if wait: for count in _iterate_timeout( timeout, - "Timeout waiting for volume %s to detach." % volume.id): + "Timeout waiting for volume %s to detach." % volume['id']): try: - vol = self.get_volume(volume.id, cache=False) + vol = self.get_volume(volume['id']) except Exception: self.log.debug( - "Error getting volume info %s" % volume.id, + "Error getting volume info %s" % volume['id'], exc_info=True) continue - if vol.status == 'available': + if vol['status'] == 'available': return - if vol.status == 'error': + if vol['status'] == 'error': raise OpenStackCloudException( - "Error in detaching volume %s" % volume.id + "Error in detaching volume %s" % volume['id'] ) def attach_volume(self, server, volume, device=None, @@ -1401,36 +1400,38 @@ def attach_volume(self, server, volume, device=None, if dev: raise OpenStackCloudException( "Volume %s already attached to server %s on device %s" - % (volume.id, server.id, dev) + % (volume['id'], server.id, dev) ) - if volume.status != 'available': + if volume['status'] != 'available': raise OpenStackCloudException( "Volume %s is not available. Status is '%s'" - % (volume.id, volume.status) + % (volume['id'], volume['status']) ) try: self.manager.submitTask( - _tasks.VolumeAttach( - volume_id=volume.id, server_id=server.id, device=device)) + _tasks.VolumeAttach(volume_id=volume['id'], + server_id=server.id, + device=device)) except Exception as e: self.log.debug( - "nova volume attach of %s failed" % volume.id, exc_info=True) + "nova volume attach of %s failed" % volume['id'], + exc_info=True) raise OpenStackCloudException( "Error attaching volume %s to server %s: %s" % - (volume.id, server.id, e) + (volume['id'], server.id, e) ) if wait: for count in _iterate_timeout( timeout, - "Timeout waiting for volume %s to attach." % volume.id): + "Timeout waiting for volume %s to attach." % volume['id']): try: - vol = self.get_volume(volume.id, cache=False) + vol = self.get_volume(volume['id']) except Exception: self.log.debug( - "Error getting volume info %s" % volume.id, + "Error getting volume info %s" % volume['id'], exc_info=True) continue @@ -1440,9 +1441,9 @@ def attach_volume(self, server, volume, device=None, # TODO(Shrews) check to see if a volume can be in error status # and also attached. If so, we should move this # above the get_volume_attach_device call - if vol.status == 'error': + if vol['status'] == 'error': raise OpenStackCloudException( - "Error in attaching volume %s" % volume.id + "Error in attaching volume %s" % volume['id'] ) def get_server_id(self, name_or_id): diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 5c09de0ce..e04878242 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -18,6 +18,7 @@ import yaml import shade +from shade import meta from shade.tests.unit import base @@ -86,16 +87,18 @@ def test_list_volumes(self, cinder_mock): mock_volume.id = 'volume1' mock_volume.status = 'available' mock_volume.display_name = 'Volume 1 Display Name' + mock_volume_dict = meta.obj_to_dict(mock_volume) cinder_mock.volumes.list.return_value = [mock_volume] - self.assertEqual([mock_volume], self.cloud.list_volumes()) + self.assertEqual([mock_volume_dict], self.cloud.list_volumes()) mock_volume2 = mock.MagicMock() mock_volume2.id = 'volume2' mock_volume2.status = 'available' mock_volume2.display_name = 'Volume 2 Display Name' + mock_volume2_dict = meta.obj_to_dict(mock_volume2) cinder_mock.volumes.list.return_value = [mock_volume, mock_volume2] - self.assertEqual([mock_volume], self.cloud.list_volumes()) + self.assertEqual([mock_volume_dict], self.cloud.list_volumes()) self.cloud.list_volumes.invalidate(self.cloud) - self.assertEqual([mock_volume, mock_volume2], + self.assertEqual([mock_volume_dict, mock_volume2_dict], self.cloud.list_volumes()) @mock.patch('shade.OpenStackCloud.cinder_client') @@ -104,14 +107,16 @@ def test_list_volumes_creating_invalidates(self, cinder_mock): mock_volume.id = 'volume1' mock_volume.status = 'creating' mock_volume.display_name = 'Volume 1 Display Name' + mock_volume_dict = meta.obj_to_dict(mock_volume) cinder_mock.volumes.list.return_value = [mock_volume] - self.assertEqual([mock_volume], self.cloud.list_volumes()) + self.assertEqual([mock_volume_dict], self.cloud.list_volumes()) mock_volume2 = mock.MagicMock() mock_volume2.id = 'volume2' mock_volume2.status = 'available' mock_volume2.display_name = 'Volume 2 Display Name' + mock_volume2_dict = meta.obj_to_dict(mock_volume2) cinder_mock.volumes.list.return_value = [mock_volume, mock_volume2] - self.assertEqual([mock_volume, mock_volume2], + self.assertEqual([mock_volume_dict, mock_volume2_dict], self.cloud.list_volumes()) @mock.patch.object(shade.OpenStackCloud, 'cinder_client') @@ -120,20 +125,23 @@ def test_create_volume_invalidates(self, cinder_mock): mock_volb4.id = 'volume1' mock_volb4.status = 'available' mock_volb4.display_name = 'Volume 1 Display Name' + mock_volb4_dict = meta.obj_to_dict(mock_volb4) cinder_mock.volumes.list.return_value = [mock_volb4] - self.assertEqual([mock_volb4], self.cloud.list_volumes()) + self.assertEqual([mock_volb4_dict], self.cloud.list_volumes()) volume = dict(display_name='junk_vol', size=1, display_description='test junk volume') mock_vol = mock.Mock() mock_vol.status = 'creating' mock_vol.id = '12345' + mock_vol_dict = meta.obj_to_dict(mock_vol) cinder_mock.volumes.create.return_value = mock_vol cinder_mock.volumes.list.return_value = [mock_volb4, mock_vol] def creating_available(): def now_available(): mock_vol.status = 'available' + mock_vol_dict['status'] = 'available' return mock.DEFAULT cinder_mock.volumes.list.side_effect = now_available return mock.DEFAULT @@ -144,11 +152,13 @@ def now_available(): # If cache was not invalidated, we would not see our own volume here # because the first volume was available and thus would already be # cached. - self.assertEqual([mock_volb4, mock_vol], self.cloud.list_volumes()) + self.assertEqual([mock_volb4_dict, mock_vol_dict], + self.cloud.list_volumes()) # And now delete and check same thing since list is cached as all # available mock_vol.status = 'deleting' + mock_vol_dict = meta.obj_to_dict(mock_vol) def deleting_gone(): def now_gone(): @@ -158,9 +168,9 @@ def now_gone(): return mock.DEFAULT cinder_mock.volumes.list.return_value = [mock_volb4, mock_vol] cinder_mock.volumes.list.side_effect = deleting_gone - cinder_mock.volumes.delete.return_value = mock_vol + cinder_mock.volumes.delete.return_value = mock_vol_dict self.cloud.delete_volume('12345') - self.assertEqual([mock_volb4], self.cloud.list_volumes()) + self.assertEqual([mock_volb4_dict], self.cloud.list_volumes()) @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_get_user_cache(self, keystone_mock): From cb6673163dbe297c82b2bf2db2d96f4df3306948 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 21 Apr 2015 08:18:16 -0400 Subject: [PATCH 0195/3836] Use new getters in update_subnet and update_router Take advantage of the new style of object getters that will raise an exception on duplicates for us. Change-Id: I89799eeebef60adf92ffbe16b16cf6a798ecac83 --- shade/__init__.py | 28 ++++++---------------------- shade/tests/unit/test_shade.py | 12 ++++++------ 2 files changed, 12 insertions(+), 28 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 859f3f9f3..5d425076f 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -921,23 +921,15 @@ def update_router(self, name_or_id, name=None, admin_state_up=None, self.log.debug("No router data to update") return - # Find matching routers, raising an exception if none are found - # or multiple matches are found. - routers = [] - for r in self.list_routers(): - if name_or_id in (r['id'], r['name']): - routers.append(r) - if not routers: + curr_router = self.get_router(name_or_id) + if not curr_router: raise OpenStackCloudException( "Router %s not found." % name_or_id) - if len(routers) > 1: - raise OpenStackCloudException( - "More than one router named %s. Use ID." % name_or_id) try: new_router = self.manager.submitTask( _tasks.RouterUpdate( - router=routers[0]['id'], body=dict(router=router))) + router=curr_router['id'], body=dict(router=router))) except Exception as e: self.log.debug("Router update failed", exc_info=True) raise OpenStackCloudException( @@ -2034,23 +2026,15 @@ def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, self.log.debug("No subnet data to update") return - # Find matching subnets, raising an exception if none are found - # or multiple matches are found. - subnets = [] - for sub in self.list_subnets(): - if name_or_id in (sub['id'], sub['name']): - subnets.append(sub) - if not subnets: + curr_subnet = self.get_subnet(name_or_id) + if not curr_subnet: raise OpenStackCloudException( "Subnet %s not found." % name_or_id) - if len(subnets) > 1: - raise OpenStackCloudException( - "More than one subnet named %s. Use ID." % name_or_id) try: new_subnet = self.manager.submitTask( _tasks.SubnetUpdate( - subnet=subnets[0]['id'], body=dict(subnet=subnet))) + subnet=curr_subnet['id'], body=dict(subnet=subnet))) except Exception as e: self.log.debug("Subnet update failed", exc_info=True) raise OpenStackCloudException( diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 32d21969b..65d689733 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -68,11 +68,11 @@ def test_create_router(self, mock_client): self.cloud.create_router(name='goofy', admin_state_up=True) self.assertTrue(mock_client.create_router.called) - @mock.patch.object(shade.OpenStackCloud, 'list_routers') + @mock.patch.object(shade.OpenStackCloud, 'get_router') @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_update_router(self, mock_client, mock_list): + def test_update_router(self, mock_client, mock_get): router1 = dict(id='123', name='mickey') - mock_list.return_value = [router1] + mock_get.return_value = router1 self.cloud.update_router('123', name='goofy') self.assertTrue(mock_client.update_router.called) @@ -185,11 +185,11 @@ def test_delete_subnet_multiple_using_id(self, mock_client): self.cloud.delete_subnet('123') self.assertTrue(mock_client.delete_subnet.called) - @mock.patch.object(shade.OpenStackCloud, 'list_subnets') + @mock.patch.object(shade.OpenStackCloud, 'get_subnet') @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_update_subnet(self, mock_client, mock_list): + def test_update_subnet(self, mock_client, mock_get): subnet1 = dict(id='123', name='mickey') - mock_list.return_value = [subnet1] + mock_get.return_value = subnet1 self.cloud.update_subnet('123', subnet_name='goofy') self.assertTrue(mock_client.update_subnet.called) From 457db089ca9857ae995f25c677dcacc75d742f80 Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Tue, 14 Apr 2015 16:12:30 -0700 Subject: [PATCH 0196/3836] Test that deleting user invalidates user cache In writing tests for this, a bug was found that there was no UserDelete task, and the old internal API no longer functions. So that bit of code was refactored to have a better chance at working in the real world. Change-Id: I06fad3133bb0c5f9bd23318e32eb1c538cc27c24 --- shade/__init__.py | 4 +++- shade/_tasks.py | 5 +++++ shade/tests/unit/test_caching.py | 5 +++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index 0b546c102..13aa56b89 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -523,14 +523,16 @@ def create_user( self.get_user_cache.invalidate(self) def delete_user(self, name_or_id): + self.get_user_cache.invalidate(self) try: user = self._get_user(name_or_id) - self._user_manager.delete(user.id) + self.manager.submitTask(_tasks.UserDelete(user=user)) except Exception as e: self.log.debug("keystone delete user issue", exc_info=True) raise OpenStackCloudException( "Error in deleting user {user}: {message}".format( user=name_or_id, message=e.message)) + self.get_user_cache.invalidate(self) def _get_glance_api_version(self): if 'image' in self.api_versions: diff --git a/shade/_tasks.py b/shade/_tasks.py index 202f255d2..ff2b4131d 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -27,6 +27,11 @@ def main(self, client): return client.keystone_client.users.create(**self.args) +class UserDelete(task_manager.Task): + def main(self, client): + return client.keystone_client.users.delete(**self.args) + + class FlavorList(task_manager.Task): def main(self, client): return client.nova_client.flavors.list() diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 5c09de0ce..7c9c8789a 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -182,3 +182,8 @@ def test_modify_user_invalidates_cache(self, keystone_mock): self.cloud.create_user(name='abc123 name') # Cache should have been invalidated self.assertEqual({'abc123': mock_user}, self.cloud.get_user_cache()) + # Now delete and ensure it disappears + keystone_mock.users.list.return_value = [] + self.cloud.delete_user('abc123') + self.assertEqual({}, self.cloud.get_user_cache()) + self.assertTrue(keystone_mock.users.delete.was_called) From 31d07b49f300e385a42ca50c3ea6394eef8f2115 Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Thu, 16 Apr 2015 16:26:39 -0700 Subject: [PATCH 0197/3836] create_user should return the user created Otherwise callers won't know what ID their new user was assigned. Change-Id: I3949b92aed7e1f262be43772846fc493e60f5462 --- shade/__init__.py | 3 ++- shade/tests/unit/test_caching.py | 19 +++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 13aa56b89..c46203e8d 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -512,7 +512,7 @@ def create_user( project_id = self._get_project(project).id else: project_id = None - self.manager.submitTask(_tasks.UserCreate( + user = self.manager.submitTask(_tasks.UserCreate( user_name=name, password=password, email=email, project=project_id, enabled=enabled)) except Exception as e: @@ -521,6 +521,7 @@ def create_user( "Error in creating user {user}: {message}".format( user=name, message=e.message)) self.get_user_cache.invalidate(self) + return meta.obj_to_dict(user) def delete_user(self, name_or_id): self.get_user_cache.invalidate(self) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 7c9c8789a..4ac9b498d 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -171,17 +171,24 @@ def test_get_user_cache(self, keystone_mock): @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_modify_user_invalidates_cache(self, keystone_mock): - mock_user = mock.MagicMock() - mock_user.id = 'abc123' + class User(object): + id = 'abc123' + email = 'abc123@domain.test' + name = 'abc123 name' + fake_user = User() # first cache an empty list keystone_mock.users.list.return_value = [] self.assertEqual({}, self.cloud.get_user_cache()) # now add one - keystone_mock.users.list.return_value = [mock_user] - keystone_mock.users.create.return_value = mock_user - self.cloud.create_user(name='abc123 name') + keystone_mock.users.list.return_value = [fake_user] + keystone_mock.users.create.return_value = fake_user + created = self.cloud.create_user(name='abc123 name', + email='abc123@domain.test') + self.assertEqual({'id': 'abc123', + 'name': 'abc123 name', + 'email': 'abc123@domain.test'}, created) # Cache should have been invalidated - self.assertEqual({'abc123': mock_user}, self.cloud.get_user_cache()) + self.assertEqual({'abc123': fake_user}, self.cloud.get_user_cache()) # Now delete and ensure it disappears keystone_mock.users.list.return_value = [] self.cloud.delete_user('abc123') From 2deadb0c1b7508a8e8a9cefd26cb3847a4c92b45 Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Thu, 16 Apr 2015 22:36:07 -0700 Subject: [PATCH 0198/3836] Fix major update_user issues update_user didn't actually work at all, and needed porting to the tasks system. Also it needs to invalidate the user cache to make sure the latest list is requested. Note that this breaks backward compatibility because defaulting the enabled argument to True would mean we are unable to tell if the user passed True, or left it blank. But the old method was entirely broken anyway so we don't care at all. Change-Id: Ief38f75b9b715cf149ada599c28cea03f99c8174 --- shade/__init__.py | 22 +++++++++++++++++----- shade/_tasks.py | 5 +++++ shade/tests/unit/test_caching.py | 9 +++++++++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index c46203e8d..7a07f877e 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -492,16 +492,28 @@ def get_user(self, name_or_id): return meta.obj_to_dict(user) return None - def update_user(self, name_or_id, email=None, enabled=True): + def update_user(self, name_or_id, email=None, enabled=None): + self.get_user_cache.invalidate(self) + user = self._get_user(name_or_id) + user_args = {} + if email is not None: + user_args['email'] = email + if enabled is not None: + user_args['enabled'] = enabled + if not user_args: + self.log.debug("No user data to update") + return None + user_args['user'] = user + try: - user = self._get_user(name_or_id) - return meta.obj_to_dict( - user.update(email=email, enabled=enabled)) + user = self.manager.submitTask(_tasks.UserUpdate(**user_args)) except Exception as e: self.log.debug("keystone update user issue", exc_info=True) raise OpenStackCloudException( "Error in updating user {user}: {message}".format( - user=name_or_id, message=e.message)) + user=name_or_id, message=str(e))) + self.get_user_cache.invalidate(self) + return meta.obj_to_dict(user) def create_user( self, name, password=None, email=None, project=None, diff --git a/shade/_tasks.py b/shade/_tasks.py index ff2b4131d..fa82ec03e 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -32,6 +32,11 @@ def main(self, client): return client.keystone_client.users.delete(**self.args) +class UserUpdate(task_manager.Task): + def main(self, client): + return client.keystone_client.users.update(**self.args) + + class FlavorList(task_manager.Task): def main(self, client): return client.nova_client.flavors.list() diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 4ac9b498d..5afcea357 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -189,6 +189,15 @@ class User(object): 'email': 'abc123@domain.test'}, created) # Cache should have been invalidated self.assertEqual({'abc123': fake_user}, self.cloud.get_user_cache()) + # Update and check to see if it is updated + fake_user2 = User() + fake_user2.email = 'abc123-changed@domain.test' + keystone_mock.users.update.return_value = fake_user2 + keystone_mock.users.list.return_value = [fake_user2] + self.cloud.update_user('abc123', email='abc123-changed@domain.test') + keystone_mock.users.update.assert_called_with( + user=fake_user2, email='abc123-changed@domain.test') + self.assertEqual({'abc123': fake_user2}, self.cloud.get_user_cache()) # Now delete and ensure it disappears keystone_mock.users.list.return_value = [] self.cloud.delete_user('abc123') From 960537738180c7df482882d35879230ae8e306cf Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Thu, 16 Apr 2015 22:52:22 -0700 Subject: [PATCH 0199/3836] Test flavor cache and add invalidation Adding get_flavor_cache call so that the dogpile.cache decorator can provide a magic invalidate method. Change-Id: If3523430b9a9b816f72ef3aaf9100a2db17f09ad --- shade/__init__.py | 5 ++++- shade/tests/unit/test_caching.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index 7a07f877e..99ffc0091 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -672,8 +672,11 @@ def get_region(self): return self.region_name @property - @_cache_on_arguments() def flavor_cache(self): + return self.get_flavor_cache() + + @_cache_on_arguments() + def get_flavor_cache(self): return {flavor.id: flavor for flavor in self.manager.submitTask(_tasks.FlavorList())} diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 5afcea357..1d7d924e4 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -203,3 +203,16 @@ class User(object): self.cloud.delete_user('abc123') self.assertEqual({}, self.cloud.get_user_cache()) self.assertTrue(keystone_mock.users.delete.was_called) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_get_flavor_cache(self, nova_mock): + nova_mock.flavors.list.return_value = [] + self.assertEqual({}, self.cloud.get_flavor_cache()) + + class Flavor(object): + id = '555' + name = 'vanilla' + fake_flavor = Flavor() + nova_mock.flavors.list.return_value = [fake_flavor] + self.cloud.get_flavor_cache.invalidate(self.cloud) + self.assertEqual({'555': fake_flavor}, self.cloud.get_flavor_cache()) From 10e123f4d56e34e4c187f6dbd24c54a9a05b689d Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Thu, 16 Apr 2015 23:06:12 -0700 Subject: [PATCH 0200/3836] Add test for caching in list_images Change-Id: I7f88854b822ad560bb0a2a6aae41c32a945f5258 --- shade/tests/unit/test_caching.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 1d7d924e4..9f3f88495 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -216,3 +216,17 @@ class Flavor(object): nova_mock.flavors.list.return_value = [fake_flavor] self.cloud.get_flavor_cache.invalidate(self.cloud) self.assertEqual({'555': fake_flavor}, self.cloud.get_flavor_cache()) + + @mock.patch.object(shade.OpenStackCloud, 'glance_client') + def test_list_images(self, glance_mock): + glance_mock.images.list.return_value = [] + self.assertEqual({}, self.cloud.list_images()) + + class Image(object): + id = '22' + name = '22 name' + status = 'success' + fake_image = Image() + glance_mock.images.list.return_value = [fake_image] + self.cloud.list_images.invalidate(self.cloud) + self.assertEqual({'22': fake_image}, self.cloud.list_images()) From 3e3f52528262fa6f29b8922a7bbc4730f5cc6fb7 Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Mon, 20 Apr 2015 20:22:10 -0700 Subject: [PATCH 0201/3836] Explicitly request cloud name in test_caching It's not entirely clear why, but there is some confusing interaction going on with tests that causes tests to fail when run individually without this change. Change-Id: Ifc0b71656c87eacf3b8a37edafda00a7f3978b48 --- shade/tests/unit/test_caching.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 9f3f88495..afc816e17 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -58,7 +58,8 @@ def setUp(self): self.cloud_config = occ.OpenStackConfig(config_files=[config.name], vendor_files=[vendor.name]) - self.cloud = shade.openstack_cloud(config=self.cloud_config) + self.cloud = shade.openstack_cloud(cloud='_cache_test_', + config=self.cloud_config) def test_openstack_cloud(self): self.assertIsInstance(self.cloud, shade.OpenStackCloud) From e351f5090251ae63b771f7949a0455c026448ac7 Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Mon, 20 Apr 2015 20:27:51 -0700 Subject: [PATCH 0202/3836] Add test for create_image with glance v1 The path is different for v1 and v2 and so they will need completely separate tests. Change-Id: If258cb811ebc1a9210e49c97d33a5ebdf15932e1 --- shade/tests/unit/test_caching.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 9b12f7ffe..7d8882829 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -241,3 +241,28 @@ class Image(object): glance_mock.images.list.return_value = [fake_image] self.cloud.list_images.invalidate(self.cloud) self.assertEqual({'22': fake_image}, self.cloud.list_images()) + + @mock.patch.object(shade.OpenStackCloud, 'glance_client') + def test_create_image_v1(self, glance_mock): + self.cloud.api_versions['image'] = '1' + glance_mock.images.list.return_value = [] + self.assertEqual({}, self.cloud.list_images()) + + class Image(object): + id = '42' + name = '42 name' + status = 'success' + fake_image = Image() + glance_mock.images.create.return_value = fake_image + glance_mock.images.list.return_value = [fake_image] + imagefile = tempfile.NamedTemporaryFile(delete=False) + imagefile.write(b'\0') + imagefile.close() + self.cloud.create_image('42 name', imagefile.name) + args = {'name': '42 name', + 'properties': {'owner_specified.shade.md5': mock.ANY, + 'owner_specified.shade.sha256': mock.ANY}} + glance_mock.images.create.assert_called_with(**args) + glance_mock.images.update.assert_called_with(data=mock.ANY, + image=fake_image) + self.assertEqual({'42': fake_image}, self.cloud.list_images()) From 3b72d6f4a7bfa97ed6ed85b54dced5571e6e8518 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 23 Apr 2015 10:47:54 -0400 Subject: [PATCH 0203/3836] Fix get_hostvars_from_server for volume API update The meta.get_hostvars_from_server() method was broken with the latest changes to the volume APIs that now return dict instead of an object. Converting it to a dict is no longer necessary. Change-Id: Ic5d501cc230a16da1ed2659e62da34c66ae37e81 --- shade/meta.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/shade/meta.py b/shade/meta.py index 80bee14e4..95c9861f1 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -123,8 +123,7 @@ def get_hostvars_from_server(cloud, server, mounts=None): server_vars['image']['name'] = image_name volumes = [] - for vol in cloud.get_volumes(server): - volume = obj_to_dict(vol) + for volume in cloud.get_volumes(server): # Make things easier to consume elsewhere volume['device'] = volume['attachments'][0]['device'] volumes.append(volume) From 8908ebd07a18697d9bae83d46e4a2a4600c29237 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 22 Apr 2015 19:39:41 -0400 Subject: [PATCH 0204/3836] Have get_image_name return an image_name Turns out an id is not a name. Change-Id: I5f28a52d2554ed29c9db05debbe5a0a3a7b52567 --- shade/__init__.py | 2 +- shade/tests/functional/test_compute.py | 12 ++++++++++++ shade/tests/unit/test_shade.py | 24 ++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index 6fce7707d..7cceea2e6 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1035,7 +1035,7 @@ def list_images(self, filter_deleted=True): def get_image_name(self, image_id, exclude=None): image = self.get_image(image_id, exclude) if image: - return image.id + return image.name return None def get_image_id(self, image_name, exclude=None): diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index 44f6e4725..5105e0f52 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -57,3 +57,15 @@ def test_delete_server(self): image=self.image, flavor=self.flavor) server_deleted = self.cloud.delete_server('test_delete_server') self.assertIsNone(server_deleted) + + def test_get_image_id(self): + self.assertEqual( + self.image.id, self.cloud.get_image_id(self.image.id)) + self.assertEqual( + self.image.id, self.cloud.get_image_id(self.image.name)) + + def test_get_image_name(self): + self.assertEqual( + self.image.name, self.cloud.get_image_name(self.image.id)) + self.assertEqual( + self.image.name, self.cloud.get_image_name(self.image.name)) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 216aff6d3..98f49b520 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -246,3 +246,27 @@ def test_unregister_machine(self, mock_client): self.assertTrue(mock_client.node.delete.called) self.assertTrue(mock_client.port.delete.called) self.assertTrue(mock_client.port.get_by_address.called) + + @mock.patch.object(shade.OpenStackCloud, 'glance_client') + def test_get_image_name(self, glance_mock): + + class Image(object): + id = '22' + name = '22 name' + status = 'success' + fake_image = Image() + glance_mock.images.list.return_value = [fake_image] + self.assertEqual('22 name', self.cloud.get_image_name('22')) + self.assertEqual('22 name', self.cloud.get_image_name('22 name')) + + @mock.patch.object(shade.OpenStackCloud, 'glance_client') + def test_get_image_id(self, glance_mock): + + class Image(object): + id = '22' + name = '22 name' + status = 'success' + fake_image = Image() + glance_mock.images.list.return_value = [fake_image] + self.assertEqual('22', self.cloud.get_image_id('22')) + self.assertEqual('22', self.cloud.get_image_id('22 name')) From 3f1c99c032ca026dd85d726e78c56c5116ad029e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 21 Apr 2015 10:43:26 -0400 Subject: [PATCH 0205/3836] Remove REST links from inventory metadata In the server_vars / meta / inventory context, the links values have no use, but they to make the data structure more chatty. Remove them so that examining the data is more betterer. Change-Id: Ib84e36a917b3511d59ff2b7aa6ad69712983665a --- shade/meta.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shade/meta.py b/shade/meta.py index 95c9861f1..fafb50837 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -93,6 +93,7 @@ def get_groups_from_server(cloud, server, server_vars): def get_hostvars_from_server(cloud, server, mounts=None): server_vars = obj_to_dict(server) + server_vars.pop('links', None) # Fist, add an IP address server_vars['public_v4'] = get_server_public_ip(server) @@ -111,6 +112,7 @@ def get_hostvars_from_server(cloud, server, mounts=None): flavor_name = cloud.get_flavor_name(flavor_id) if flavor_name: server_vars['flavor']['name'] = flavor_name + server_vars['flavor'].pop('links', None) # OpenStack can return image as a string when you've booted from volume if unicode(server.image) == server.image: @@ -121,6 +123,7 @@ def get_hostvars_from_server(cloud, server, mounts=None): image_name = cloud.get_image_name(image_id) if image_name: server_vars['image']['name'] = image_name + server_vars['image'].pop('links', None) volumes = [] for volume in cloud.get_volumes(server): From cd2a8a6029af2e47d65b747db8f186a58ae28cd3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 24 Apr 2015 09:00:42 -0400 Subject: [PATCH 0206/3836] Add a docstring to the Task class The purpose of the Task class was a bit vague, so let's add a docstring. Change-Id: I74b65a4094f424f262d07c990e2f7ff04471664d --- shade/task_manager.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/shade/task_manager.py b/shade/task_manager.py index 9e410ba3f..879bc154e 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -26,6 +26,20 @@ @six.add_metaclass(abc.ABCMeta) class Task(object): + """Represent a task to be performed on an OpenStack Cloud. + + Some consumers need to inject things like rate-limiting or auditing + around each external REST interaction. Task provides an interface + to encapsulate each such interaction. Also, although shade itself + operates normally in a single-threaded direct action manner, consuming + programs may provide a multi-threaded TaskManager themselves. + + A consumer is expected to overload the main method. + + :param dict kw: Any args that are expected to be passed to something in + the main payload at execution time. + """ + def __init__(self, **kw): self._exception = None self._traceback = None From 746a1a84d750574cbb8dc603ad90cd546bf86a67 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 23 Apr 2015 14:10:35 -0400 Subject: [PATCH 0207/3836] Update flavor API for new get/list/search API The flavor API methods now support the get/list/search style interface. This required changing the format of the flavor list from a list of {flavor_id: flavor_obj} dicts to a list of converted flavor dicts. Change-Id: I593493c16f441470e9f210592c4baf2fa81664b5 --- shade/__init__.py | 53 ++++++++++++++++++-------------- shade/tests/unit/test_caching.py | 9 +++--- shade/tests/unit/test_shade.py | 50 ++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 27 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 6fce7707d..535f804ed 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -671,36 +671,30 @@ def get_name(self): def get_region(self): return self.region_name - @property - def flavor_cache(self): - return self.get_flavor_cache() - - @_cache_on_arguments() - def get_flavor_cache(self): - return {flavor.id: flavor for flavor in - self.manager.submitTask(_tasks.FlavorList())} - def get_flavor_name(self, flavor_id): - flavor = self.flavor_cache.get(flavor_id, None) + flavor = self.get_flavor(flavor_id) if flavor: - return flavor.name - return None - - def get_flavor(self, name_or_id): - for id, flavor in self.flavor_cache.items(): - if name_or_id in (id, flavor.name): - return flavor + return flavor['name'] return None def get_flavor_by_ram(self, ram, include=None): - for flavor in sorted( - self.flavor_cache.values(), - key=operator.attrgetter('ram')): - if (flavor.ram >= ram and - (not include or include in flavor.name)): + """Get a flavor based on amount of RAM available. + + Finds the flavor with the least amount of RAM that is at least + as much as the specified amount. If `include` is given, further + filter based on matching flavor name. + + :param int ram: Minimum amount of RAM. + :param string include: If given, will return a flavor whose name + contains this string as a substring. + """ + flavors = self.list_flavors() + for flavor in sorted(flavors, key=operator.itemgetter('ram')): + if (flavor['ram'] >= ram and + (not include or include in flavor['name'])): return flavor raise OpenStackCloudException( - "Cloud not find a flavor with {ram} and '{include}'".format( + "Could not find a flavor with {ram} and '{include}'".format( ram=ram, include=include)) def get_endpoint(self, service_type): @@ -826,6 +820,10 @@ def search_volumes(self, name_or_id=None, filters=None): volumes = self.list_volumes() return self._filter_list(volumes, name_or_id, filters) + def search_flavors(self, name_or_id=None, filters=None): + flavors = self.list_flavors() + return self._filter_list(flavors, name_or_id, filters) + def list_networks(self): return self.manager.submitTask(_tasks.NetworkList())['networks'] @@ -844,6 +842,12 @@ def list_volumes(self, cache=True): self.manager.submitTask(_tasks.VolumeList()) ) + @_cache_on_arguments() + def list_flavors(self): + return meta.obj_list_to_dict( + self.manager.submitTask(_tasks.FlavorList()) + ) + def get_network(self, name_or_id, filters=None): return self._get_entity(self.search_networks, name_or_id, filters) @@ -856,6 +860,9 @@ def get_subnet(self, name_or_id, filters=None): def get_volume(self, name_or_id, filters=None): return self._get_entity(self.search_volumes, name_or_id, filters) + def get_flavor(self, name_or_id, filters=None): + return self._get_entity(self.search_flavors, name_or_id, filters) + # TODO(Shrews): This will eventually need to support tenant ID and # provider networks, which are admin-level params. def create_network(self, name, shared=False, admin_state_up=True): diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 9b12f7ffe..7f2c1784c 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -216,17 +216,18 @@ class User(object): self.assertTrue(keystone_mock.users.delete.was_called) @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_get_flavor_cache(self, nova_mock): + def test_list_flavors(self, nova_mock): nova_mock.flavors.list.return_value = [] - self.assertEqual({}, self.cloud.get_flavor_cache()) + self.assertEqual([], self.cloud.list_flavors()) class Flavor(object): id = '555' name = 'vanilla' fake_flavor = Flavor() + fake_flavor_dict = meta.obj_to_dict(fake_flavor) nova_mock.flavors.list.return_value = [fake_flavor] - self.cloud.get_flavor_cache.invalidate(self.cloud) - self.assertEqual({'555': fake_flavor}, self.cloud.get_flavor_cache()) + self.cloud.list_flavors.invalidate(self.cloud) + self.assertEqual([fake_flavor_dict], self.cloud.list_flavors()) @mock.patch.object(shade.OpenStackCloud, 'glance_client') def test_list_images(self, glance_mock): diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 216aff6d3..c0516ede2 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -15,6 +15,7 @@ import mock import shade +from shade import meta from shade.tests.unit import base @@ -193,6 +194,55 @@ def test_update_subnet(self, mock_client, mock_get): self.cloud.update_subnet('123', subnet_name='goofy') self.assertTrue(mock_client.update_subnet.called) + @mock.patch.object(shade.OpenStackCloud, 'list_flavors') + def test_get_flavor_by_ram(self, mock_list): + class Flavor1(object): + id = '1' + name = 'vanilla ice cream' + ram = 100 + + class Flavor2(object): + id = '2' + name = 'chocolate ice cream' + ram = 200 + + vanilla = meta.obj_to_dict(Flavor1()) + chocolate = meta.obj_to_dict(Flavor2()) + mock_list.return_value = [vanilla, chocolate] + flavor = self.cloud.get_flavor_by_ram(ram=150) + self.assertEquals(chocolate, flavor) + + @mock.patch.object(shade.OpenStackCloud, 'list_flavors') + def test_get_flavor_by_ram_and_include(self, mock_list): + class Flavor1(object): + id = '1' + name = 'vanilla ice cream' + ram = 100 + + class Flavor2(object): + id = '2' + name = 'chocolate ice cream' + ram = 200 + + class Flavor3(object): + id = '3' + name = 'strawberry ice cream' + ram = 250 + + vanilla = meta.obj_to_dict(Flavor1()) + chocolate = meta.obj_to_dict(Flavor2()) + strawberry = meta.obj_to_dict(Flavor3()) + mock_list.return_value = [vanilla, chocolate, strawberry] + flavor = self.cloud.get_flavor_by_ram(ram=150, include='strawberry') + self.assertEquals(strawberry, flavor) + + @mock.patch.object(shade.OpenStackCloud, 'list_flavors') + def test_get_flavor_by_ram_not_found(self, mock_list): + mock_list.return_value = [] + self.assertRaises(shade.OpenStackCloudException, + self.cloud.get_flavor_by_ram, + ram=100) + class TestShadeOperator(base.TestCase): From 34c1ad9a6a73dacace1a33748037b4f81a04dc54 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 24 Apr 2015 12:08:13 -0400 Subject: [PATCH 0208/3836] Replace e.message with str(e) Exception does not have a message field in python 3. Change-Id: Ic6c6c1dd519cf30db659bac73a3210e5bf0f0933 --- shade/__init__.py | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 7cceea2e6..c0994da81 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -369,7 +369,7 @@ def keystone_session(self): except Exception as e: self.log.debug("keystone unknown issue", exc_info=True) raise OpenStackCloudException( - "Error authenticating to the keystone: %s " % e.message) + "Error authenticating to the keystone: %s " % str(e)) return self._keystone_session @property @@ -385,7 +385,7 @@ def keystone_client(self): self.log.debug( "Couldn't construct keystone object", exc_info=True) raise OpenStackCloudException( - "Error constructing keystone client: %s" % e.message) + "Error constructing keystone client: %s" % str(e)) return self._keystone_client @property @@ -445,7 +445,7 @@ def update_project(self, name_or_id, description=None, enabled=True): self.log.debug("keystone update project issue", exc_info=True) raise OpenStackCloudException( "Error in updating project {project}: {message}".format( - project=name_or_id, message=e.message)) + project=name_or_id, message=str(e))) def create_project(self, name, description=None, enabled=True): """Create a project.""" @@ -456,7 +456,7 @@ def create_project(self, name, description=None, enabled=True): self.log.debug("keystone create project issue", exc_info=True) raise OpenStackCloudException( "Error in creating project {project}: {message}".format( - project=name, message=e.message)) + project=name, message=str(e))) def delete_project(self, name_or_id): try: @@ -466,7 +466,7 @@ def delete_project(self, name_or_id): self.log.debug("keystone delete project issue", exc_info=True) raise OpenStackCloudException( "Error in deleting project {project}: {message}".format( - project=name_or_id, message=e.message)) + project=name_or_id, message=str(e))) @property def user_cache(self): @@ -531,7 +531,7 @@ def create_user( self.log.debug("keystone create user issue", exc_info=True) raise OpenStackCloudException( "Error in creating user {user}: {message}".format( - user=name, message=e.message)) + user=name, message=str(e))) self.get_user_cache.invalidate(self) return meta.obj_to_dict(user) @@ -544,7 +544,7 @@ def delete_user(self, name_or_id): self.log.debug("keystone delete user issue", exc_info=True) raise OpenStackCloudException( "Error in deleting user {user}: {message}".format( - user=name_or_id, message=e.message)) + user=name_or_id, message=str(e))) self.get_user_cache.invalidate(self) def _get_glance_api_version(self): @@ -583,7 +583,7 @@ def glance_client(self): except Exception as e: self.log.debug("glance unknown issue", exc_info=True) raise OpenStackCloudException( - "Error in connecting to glance: %s" % e.message) + "Error in connecting to glance: %s" % str(e)) if not self._glance_client: raise OpenStackCloudException("Error connecting to glance") @@ -714,7 +714,7 @@ def get_endpoint(self, service_type): except Exception as e: self.log.debug("keystone cannot get endpoint", exc_info=True) raise OpenStackCloudException( - "Error getting %s endpoint: %s" % (service_type, e.message)) + "Error getting %s endpoint: %s" % (service_type, str(e))) return endpoint def list_servers(self): @@ -881,7 +881,7 @@ def create_network(self, name, shared=False, admin_state_up=True): except Exception as e: self.log.debug("Network creation failed", exc_info=True) raise OpenStackCloudException( - "Error in creating network %s: %s" % (name, e.message)) + "Error in creating network %s: %s" % (name, str(e))) # Turns out neutron returns an actual dict, so no need for the # use of meta.obj_to_dict() here (which would not work against # a dict). @@ -904,7 +904,7 @@ def delete_network(self, name_or_id): except Exception as e: self.log.debug("Network deletion failed", exc_info=True) raise OpenStackCloudException( - "Error in deleting network %s: %s" % (name_or_id, e.message)) + "Error in deleting network %s: %s" % (name_or_id, str(e))) def create_router(self, name=None, admin_state_up=True): """Create a logical router. @@ -1087,7 +1087,7 @@ def delete_image(self, name_or_id, wait=False, timeout=3600): except Exception as e: self.log.debug("Image deletion failed", exc_info=True) raise OpenStackCloudException( - "Error in deleting image: %s" % e.message) + "Error in deleting image: %s" % str(e)) if wait: for count in _iterate_timeout( @@ -1247,7 +1247,7 @@ def create_volume(self, wait=True, timeout=None, **kwargs): except Exception as e: self.log.debug("Volume creation failed", exc_info=True) raise OpenStackCloudException( - "Error in creating volume: %s" % e.message) + "Error in creating volume: %s" % str(e)) self.list_volumes.invalidate(self) volume = meta.obj_to_dict(volume) @@ -1292,7 +1292,7 @@ def delete_volume(self, name_or_id=None, wait=True, timeout=None): except Exception as e: self.log.debug("Volume deletion failed", exc_info=True) raise OpenStackCloudException( - "Error in deleting volume: %s" % e.message) + "Error in deleting volume: %s" % str(e)) self.list_volumes.invalidate(self) if wait: @@ -1551,7 +1551,7 @@ def add_ip_list(self, server, ips): "nova floating ip add failed", exc_info=True) raise OpenStackCloudException( "Error attaching IP {ip} to instance {id}: {msg} ".format( - ip=ip, id=server.id, msg=e.message)) + ip=ip, id=server.id, msg=str(e))) def add_auto_ip(self, server): try: @@ -1560,7 +1560,7 @@ def add_auto_ip(self, server): self.log.debug( "nova floating ip create failed", exc_info=True) raise OpenStackCloudException( - "Unable to create floating ip: %s" % (e.message)) + "Unable to create floating ip: %s" % (str(e))) try: self.add_ip_list(server, [new_ip]) except OpenStackCloudException: @@ -1590,7 +1590,7 @@ def add_ips_to_server(self, server, auto_ip=True, ips=None, ip_pool=None): except Exception as e: self.log.debug("nova info failed", exc_info=True) raise OpenStackCloudException( - "Error in getting info from instance: %s " % e.message) + "Error in getting info from instance: %s " % str(e)) return server def create_server(self, auto_ip=True, ips=None, ip_pool=None, @@ -2098,7 +2098,7 @@ def ironic_client(self): except Exception as e: self.log.debug("ironic auth failed", exc_info=True) raise OpenStackCloudException( - "Error in connecting to ironic: %s" % e.message) + "Error in connecting to ironic: %s" % str(e)) return self._ironic_client def list_nics(self): @@ -2300,7 +2300,7 @@ def validate_node(self, uuid): except Exception as e: self.log.debug( "ironic node validation call failed", exc_info=True) - raise OpenStackCloudException(e.message) + raise OpenStackCloudException(str(e)) if not ifaces.deploy or not ifaces.power: raise OpenStackCloudException( @@ -2320,7 +2320,7 @@ def node_set_provision_state(self, uuid, state, configdrive=None): self.log.debug( "ironic node failed change provision state to %s" % state, exc_info=True) - raise OpenStackCloudException(e.message) + raise OpenStackCloudException(str(e)) def activate_node(self, uuid, configdrive=None): return meta.obj_to_dict( @@ -2336,7 +2336,7 @@ def set_node_instance_info(self, uuid, patch): except Exception as e: self.log.debug( "Failed to update instance_info", exc_info=True) - raise OpenStackCloudException(e.message) + raise OpenStackCloudException(str(e)) def purge_node_instance_info(self, uuid): patch = [] @@ -2347,4 +2347,4 @@ def purge_node_instance_info(self, uuid): except Exception as e: self.log.debug( "Failed to delete instance_info", exc_info=True) - raise OpenStackCloudException(e.message) + raise OpenStackCloudException(str(e)) From 6ac348dd13760d23b1d099aca6973f3333dd98f2 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Wed, 15 Apr 2015 13:16:30 -0400 Subject: [PATCH 0209/3836] Add update_machine method Additional method which leverages jsonpatch to appropriately update a machine's configuration as-necessary. Change-Id: I401829be21484519eb4880e7108c7693c9501df8 --- requirements.txt | 1 + shade/__init__.py | 95 +++++++++++++ shade/tests/unit/test_shade.py | 244 +++++++++++++++++++++++++++++++++ 3 files changed, 340 insertions(+) diff --git a/requirements.txt b/requirements.txt index c0a25f569..cf25ff199 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ pbr>=0.5.21,<1.0 +jsonpatch os-client-config>=0.7.0 six diff --git a/shade/__init__.py b/shade/__init__.py index 7cceea2e6..070c5cad3 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -24,6 +24,7 @@ import glanceclient.exc from ironicclient import client as ironic_client from ironicclient import exceptions as ironic_exceptions +import jsonpatch from keystoneclient import auth as ksc_auth from keystoneclient import session as ksc_session from keystoneclient import client as keystone_client @@ -2294,6 +2295,100 @@ def patch_machine(self, name_or_id, patch): "Error updating machine via patch operation. node: %s. " "%s" % (name_or_id, e)) + def update_machine(self, name_or_id, chassis_uuid=None, driver=None, + driver_info=None, name=None, instance_info=None, + instance_uuid=None, properties=None): + """Update a machine with new configuration information + + A user-friendly method to perform updates of a machine, in whole or + part. + + :param string name_or_id: A machine name or UUID to be updated. + :param string chassis_uuid: Assign a chassis UUID to the machine. + NOTE: As of the Kilo release, this value + cannot be changed once set. If a user + attempts to change this value, then the + Ironic API, as of Kilo, will reject the + request. + :param string driver: The driver name for controlling the machine. + :param dict driver_info: The dictonary defining the configuration + that the driver will utilize to control + the machine. Permutations of this are + dependent upon the specific driver utilized. + :param string name: A human relatable name to represent the machine. + :param dict instance_info: A dictonary of configuration information + that conveys to the driver how the host + is to be configured when deployed. + be deployed to the machine. + :param string instance_uuid: A UUID value representing the instance + that the deployed machine represents. + :param dict properties: A dictonary defining the properties of a + machine. + + :raises: OpenStackCloudException on operation error. + + :returns: Dictonary containing a machine sub-dictonary consisting + of the updated data returned from the API update operation, + and a list named changes which contains all of the API paths + that received updates. + """ + try: + machine = self.get_machine(name_or_id) + + machine_config = {} + new_config = {} + + if chassis_uuid: + machine_config['chassis_uuid'] = machine['chassis_uuid'] + new_config['chassis_uuid'] = chassis_uuid + + if driver: + machine_config['driver'] = machine['driver'] + new_config['driver'] = driver + + if driver_info: + machine_config['driver_info'] = machine['driver_info'] + new_config['driver_info'] = driver_info + + if name: + machine_config['name'] = machine['name'] + new_config['name'] = name + + if instance_info: + machine_config['instance_info'] = machine['instance_info'] + new_config['instance_info'] = instance_info + + if instance_uuid: + machine_config['instance_uuid'] = machine['instance_uuid'] + new_config['instance_uuid'] = instance_uuid + + if properties: + machine_config['properties'] = machine['properties'] + new_config['properties'] = properties + + patch = jsonpatch.JsonPatch.from_diff(machine_config, new_config) + + if not patch: + return dict( + node=machine, + changes=None + ) + else: + machine = self.patch_machine(machine['uuid'], list(patch)) + change_list = [] + for change in list(patch): + change_list.append(change['path']) + return dict( + node=machine, + changes=change_list + ) + except Exception as e: + self.log.debug( + "Machine update failed", exc_info=True) + raise OpenStackCloudException( + "Error updating machine node %s. " + "%s" % (name_or_id, e)) + def validate_node(self, uuid): try: ifaces = self.ironic_client.node.validate(uuid) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 98f49b520..4d436e617 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -211,6 +211,250 @@ def test_patch_machine(self, mock_client): self.cloud.patch_machine(node_id, patch) self.assertTrue(mock_client.node.update.called) + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_no_action(self, mock_patch, mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + name = 'node01' + + expected_machine = dict( + uuid='00000000-0000-0000-0000-000000000000', + name='node01' + ) + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine('node01') + self.assertIsNone(update_dict['changes']) + self.assertFalse(mock_patch.called) + self.assertDictEqual(expected_machine, update_dict['node']) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_no_action_name(self, mock_patch, + mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + name = 'node01' + + expected_machine = dict( + uuid='00000000-0000-0000-0000-000000000000', + name='node01' + ) + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine('node01', name='node01') + self.assertIsNone(update_dict['changes']) + self.assertFalse(mock_patch.called) + self.assertDictEqual(expected_machine, update_dict['node']) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_action_name(self, mock_patch, + mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + name = 'evil' + + expected_patch = [dict(op='replace', path='/name', value='good')] + + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine('evil', name='good') + self.assertIsNotNone(update_dict['changes']) + self.assertEqual('/name', update_dict['changes'][0]) + self.assertTrue(mock_patch.called) + mock_patch.assert_called_with( + '00000000-0000-0000-0000-000000000000', + expected_patch) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_update_name(self, mock_patch, + mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + name = 'evil' + + expected_patch = [dict(op='replace', path='/name', value='good')] + + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine('evil', name='good') + self.assertIsNotNone(update_dict['changes']) + self.assertEqual('/name', update_dict['changes'][0]) + self.assertTrue(mock_patch.called) + mock_patch.assert_called_with( + '00000000-0000-0000-0000-000000000000', + expected_patch) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_update_chassis_uuid(self, mock_patch, + mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + chassis_uuid = None + + expected_patch = [ + dict( + op='replace', + path='/chassis_uuid', + value='00000000-0000-0000-0000-000000000001' + )] + + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine( + '00000000-0000-0000-0000-000000000000', + chassis_uuid='00000000-0000-0000-0000-000000000001') + self.assertIsNotNone(update_dict['changes']) + self.assertEqual('/chassis_uuid', update_dict['changes'][0]) + self.assertTrue(mock_patch.called) + mock_patch.assert_called_with( + '00000000-0000-0000-0000-000000000000', + expected_patch) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_update_driver(self, mock_patch, + mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + driver = None + + expected_patch = [ + dict( + op='replace', + path='/driver', + value='fake' + )] + + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine( + '00000000-0000-0000-0000-000000000000', + driver='fake' + ) + self.assertIsNotNone(update_dict['changes']) + self.assertEqual('/driver', update_dict['changes'][0]) + self.assertTrue(mock_patch.called) + mock_patch.assert_called_with( + '00000000-0000-0000-0000-000000000000', + expected_patch) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_update_driver_info(self, mock_patch, + mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + driver_info = None + + expected_patch = [ + dict( + op='replace', + path='/driver_info', + value=dict(var='fake') + )] + + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine( + '00000000-0000-0000-0000-000000000000', + driver_info=dict(var="fake") + ) + self.assertIsNotNone(update_dict['changes']) + self.assertEqual('/driver_info', update_dict['changes'][0]) + self.assertTrue(mock_patch.called) + mock_patch.assert_called_with( + '00000000-0000-0000-0000-000000000000', + expected_patch) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_update_instance_info(self, mock_patch, + mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + instance_info = None + + expected_patch = [ + dict( + op='replace', + path='/instance_info', + value=dict(var='fake') + )] + + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine( + '00000000-0000-0000-0000-000000000000', + instance_info=dict(var="fake") + ) + self.assertIsNotNone(update_dict['changes']) + self.assertEqual('/instance_info', update_dict['changes'][0]) + self.assertTrue(mock_patch.called) + mock_patch.assert_called_with( + '00000000-0000-0000-0000-000000000000', + expected_patch) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_update_instance_uuid(self, mock_patch, + mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + instance_uuid = None + + expected_patch = [ + dict( + op='replace', + path='/instance_uuid', + value='00000000-0000-0000-0000-000000000002' + )] + + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine( + '00000000-0000-0000-0000-000000000000', + instance_uuid='00000000-0000-0000-0000-000000000002' + ) + self.assertIsNotNone(update_dict['changes']) + self.assertEqual('/instance_uuid', update_dict['changes'][0]) + self.assertTrue(mock_patch.called) + mock_patch.assert_called_with( + '00000000-0000-0000-0000-000000000000', + expected_patch) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_update_properties(self, mock_patch, + mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + properties = None + + expected_patch = [ + dict( + op='replace', + path='/properties', + value=dict(var='fake') + )] + + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine( + '00000000-0000-0000-0000-000000000000', + properties=dict(var="fake") + ) + self.assertIsNotNone(update_dict['changes']) + self.assertEqual('/properties', update_dict['changes'][0]) + self.assertTrue(mock_patch.called) + mock_patch.assert_called_with( + '00000000-0000-0000-0000-000000000000', + expected_patch) + @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_register_machine(self, mock_client): class fake_node: From 4d4d555e9686f31c741c4cd8b0a3c12a949dad4d Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Thu, 9 Apr 2015 16:50:21 -0400 Subject: [PATCH 0210/3836] Add Ironic maintenance state pass-through Adding methods to Shade to support controlling the node maintenance state in Ironic via new methods set_machine_maintenace_state and remove_machine_from_maintenance. Basic tests added for both methods. Change-Id: I50e7f55199992228c0417c98d18a6e63588cd03d --- shade/__init__.py | 56 ++++++++++++++++++++++++++++++++++ shade/_tasks.py | 5 +++ shade/tests/unit/test_shade.py | 26 ++++++++++++++++ 3 files changed, 87 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 070c5cad3..2404c6a1f 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2417,6 +2417,62 @@ def node_set_provision_state(self, uuid, state, configdrive=None): exc_info=True) raise OpenStackCloudException(e.message) + def set_machine_maintenance_state( + self, + name_or_id, + state=True, + reason=None): + """Set Baremetal Machine Maintenance State + + Sets Baremetal maintenance state and maintenance reason. + + :param string name_or_id: The Name or UUID value representing the + baremetal node. + :param boolean state: The desired state of the node. True being in + maintenance where as False means the machine + is not in maintenance mode. This value + defaults to True if not explicitly set. + :param string reason: An optional freeform string that is supplied to + the baremetal API to allow for notation as to why + the node is in maintenance state. + + :raises: OpenStackCloudException on operation error. + + :returns: Per the API, no value should be returned with a successful + operation. + """ + try: + if state: + return self.manager.submitTask( + _tasks.MachineSetMaintenance(node_id=name_or_id, + state='true', + maint_reason=reason)) + else: + return self.manager.submitTask( + _tasks.MachineSetMaintenance(node_id=name_or_id, + state='false')) + except Exception as e: + self.log.debug( + "failed setting maintenance state on node %s" % name_or_id, + exc_info=True) + raise OpenStackCloudException( + "Error setting machine maintenance on node %s. " + "state: %s" % (name_or_id, e)) + + def remove_machine_from_maintenance(self, name_or_id): + """Remove Baremetal Machine from Maintenance State + + Similarly to set_machine_maintenance_state, this method + removes a machine from maintenance state. It must be noted + that this method simpily calls set_machine_maintenace_state + for the name_or_id requested and sets the state to False. + + :param string name_or_id: The Name or UUID value representing the + baremetal node. + :returns: Dictonary object representing the updated node. + """ + self.set_machine_maintenance_state(name_or_id, False) + def activate_node(self, uuid, configdrive=None): return meta.obj_to_dict( self.node_set_provision_state(uuid, 'active', configdrive)) diff --git a/shade/_tasks.py b/shade/_tasks.py index 546e32285..9232403b0 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -291,3 +291,8 @@ def main(self, client): class MachinePortDelete(task_manager.Task): def main(self, client): return client.ironic_client.port.delete(**self.args) + + +class MachineSetMaintenance(task_manager.Task): + def main(self, client): + return client.ironic_client.node.set_maintenance(**self.args) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 4d436e617..26c0e3ebb 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -491,6 +491,32 @@ def test_unregister_machine(self, mock_client): self.assertTrue(mock_client.port.delete.called) self.assertTrue(mock_client.port.get_by_address.called) + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_set_machine_maintenace_state(self, mock_client): + node_id = 'node01' + reason = 'no reason' + self.cloud.set_machine_maintenance_state(node_id, True, reason=reason) + mock_client.node.set_maintenance.assert_called_with( + node_id='node01', + state='true', + maint_reason='no reason') + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_set_machine_maintenace_state_false(self, mock_client): + node_id = 'node01' + self.cloud.set_machine_maintenance_state(node_id, False) + mock_client.node.set_maintenance.assert_called_with( + node_id='node01', + state='false') + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_remove_machine_from_maintenance(self, mock_client): + node_id = 'node01' + self.cloud.remove_machine_from_maintenance(node_id) + mock_client.node.set_maintenance.assert_called_with( + node_id='node01', + state='false') + @mock.patch.object(shade.OpenStackCloud, 'glance_client') def test_get_image_name(self, glance_mock): From 788dd1be83b0d6622f96f1562934d767db27865a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 24 Apr 2015 12:09:33 -0400 Subject: [PATCH 0211/3836] Call super in OpenStackCloudException Python3 does not have a message attribute. Also, we should pass all of the parameters correctly. Change-Id: I31168bf9e5152d786ce9c0369316e4a23e0f3d0c --- shade/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index c0994da81..3643ec852 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -59,11 +59,17 @@ class OpenStackCloudException(Exception): def __init__(self, message, extra_data=None): - self.message = message + args = [message] + if extra_data: + args.append(extra_data) + super(OpenStackCloudException, self).__init__(*args) self.extra_data = extra_data def __str__(self): - return "%s (Extra: %s)" % (self.message, self.extra_data) + if self.extra_data is not None: + return "%s (Extra: %s)" % ( + Exception.__str__(self), self.extra_data) + return Exception.__str__(self) class OpenStackCloudTimeout(OpenStackCloudException): From 59041cc685f9c2e48a492f9dadaff8ee5e3ed5c9 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 28 Apr 2015 13:21:35 -0400 Subject: [PATCH 0212/3836] Allow complex filtering with embedded dicts Allow filtering of searchable data to be done with multiple levels of dict data, rather than only simple types (string, int, etc). Two tests are added for testing a dict within the filtering dict, and a dict within a dict within the filtering dict (to test the recursive aspect). Change-Id: I17562bfe6bf5cb6a0dc48c4a693fe755200eee08 --- shade/__init__.py | 27 +++++++++++++++++++++++++-- shade/tests/unit/test_shade.py | 26 ++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index b1fee4822..eb4c77ea6 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -773,7 +773,15 @@ def _filter_list(self, data, name_or_id, filters): :param string name_or_id: The name or ID of the entity being filtered. :param dict filters: - A dictionary of meta data to use for further filtering. + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } """ if name_or_id: identifier_matches = [] @@ -789,11 +797,26 @@ def _filter_list(self, data, name_or_id, filters): if not filters: return data + def _dict_filter(f, d): + if not d: + return False + for key in f.keys(): + if isinstance(f[key], dict): + if not _dict_filter(f[key], d.get(key, None)): + return False + elif d.get(key, None) != f[key]: + return False + return True + filtered = [] for e in data: filtered.append(e) for key in filters.keys(): - if key not in e or e[key] != filters[key]: + if isinstance(filters[key], dict): + if not _dict_filter(filters[key], e.get(key, None)): + filtered.pop() + break + elif e.get(key, None) != filters[key]: filtered.pop() break return filtered diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 26c0e3ebb..a585e0967 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -41,6 +41,32 @@ def test__filter_list_filter(self): ret = self.cloud._filter_list(data, 'donald', {'other': 'duck'}) self.assertEquals([el1], ret) + def test__filter_list_dict1(self): + el1 = dict(id=100, name='donald', last='duck', + other=dict(category='duck')) + el2 = dict(id=200, name='donald', last='trump', + other=dict(category='human')) + el3 = dict(id=300, name='donald', last='ronald mac', + other=dict(category='clown')) + data = [el1, el2, el3] + ret = self.cloud._filter_list(data, 'donald', + {'other': {'category': 'clown'}}) + self.assertEquals([el3], ret) + + def test__filter_list_dict2(self): + el1 = dict(id=100, name='donald', last='duck', + other=dict(category='duck', financial=dict(status='poor'))) + el2 = dict(id=200, name='donald', last='trump', + other=dict(category='human', financial=dict(status='rich'))) + el3 = dict(id=300, name='donald', last='ronald mac', + other=dict(category='clown', financial=dict(status='rich'))) + data = [el1, el2, el3] + ret = self.cloud._filter_list(data, 'donald', + {'other': { + 'financial': {'status': 'rich'} + }}) + self.assertEquals([el2, el3], ret) + @mock.patch.object(shade.OpenStackCloud, 'search_subnets') def test_get_subnet(self, mock_search): subnet = dict(id='123', name='mickey') From 41ef6412909516ed5f12b2c8935b5acfebad761c Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Tue, 21 Apr 2015 11:06:20 -0700 Subject: [PATCH 0213/3836] Add tests and invalidation for glance v2 upload This coverage exposed python3 incompatibilities, and also revealed that some more caches needed invalidating both before and after update. Change-Id: I0c9a2a435efae0e1b1ef016c46ec54f7278f97a5 --- shade/__init__.py | 8 ++-- shade/tests/unit/test_caching.py | 71 ++++++++++++++++++++++++++++++-- 2 files changed, 72 insertions(+), 7 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index b1fee4822..6d68d4530 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1177,6 +1177,7 @@ def _upload_image_v2( if status.status == 'success': image_id = status.result['image_id'] self._reset_image_cache() + self.list_images.invalidate(self) try: image = self.get_image(image_id) except glanceclient.exc.HTTPServiceUnavailable: @@ -1187,6 +1188,7 @@ def _upload_image_v2( self.update_image_properties( image=image, **image_properties) + self.list_images.invalidate(self) return self.get_image_dict(status.result['image_id']) if status.status == 'failure': raise OpenStackCloudException( @@ -1202,7 +1204,7 @@ def update_image_properties( image = self.get_image(name_or_id) img_props = {} - for k, v in properties.iteritems(): + for k, v in iter(properties.items()): if v and k in ['ramdisk', 'kernel']: v = self.get_image_id(v) k = '{0}_id'.format(k) @@ -1216,7 +1218,7 @@ def update_image_properties( def _update_image_properties_v2(self, image, properties): img_props = {} - for k, v in properties.iteritems(): + for k, v in iter(properties.items()): if image.get(k, None) != v: img_props[k] = str(v) if not img_props: @@ -1227,7 +1229,7 @@ def _update_image_properties_v2(self, image, properties): def _update_image_properties_v1(self, image, properties): img_props = {} - for k, v in properties.iteritems(): + for k, v in iter(properties.items()): if image.properties.get(k, None) != v: img_props[k] = v if not img_props: diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 7d8882829..29b5f6ac3 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -242,6 +242,13 @@ class Image(object): self.cloud.list_images.invalidate(self.cloud) self.assertEqual({'22': fake_image}, self.cloud.list_images()) + def _call_create_image(self, name, container=None): + imagefile = tempfile.NamedTemporaryFile(delete=False) + imagefile.write(b'\0') + imagefile.close() + self.cloud.create_image( + name, imagefile.name, container=container, wait=True) + @mock.patch.object(shade.OpenStackCloud, 'glance_client') def test_create_image_v1(self, glance_mock): self.cloud.api_versions['image'] = '1' @@ -255,10 +262,7 @@ class Image(object): fake_image = Image() glance_mock.images.create.return_value = fake_image glance_mock.images.list.return_value = [fake_image] - imagefile = tempfile.NamedTemporaryFile(delete=False) - imagefile.write(b'\0') - imagefile.close() - self.cloud.create_image('42 name', imagefile.name) + self._call_create_image('42 name') args = {'name': '42 name', 'properties': {'owner_specified.shade.md5': mock.ANY, 'owner_specified.shade.sha256': mock.ANY}} @@ -266,3 +270,62 @@ class Image(object): glance_mock.images.update.assert_called_with(data=mock.ANY, image=fake_image) self.assertEqual({'42': fake_image}, self.cloud.list_images()) + + @mock.patch.object(shade.OpenStackCloud, 'glance_client') + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + def test_create_image_v2(self, swift_mock, glance_mock): + self.cloud.api_versions['image'] = '2' + + class Container(object): + name = 'image_upload_v2_test_container' + + fake_container = Container() + swift_mock.put_container.return_value = fake_container + swift_mock.head_object.return_value = {} + glance_mock.images.list.return_value = [] + self.assertEqual({}, self.cloud.list_images()) + + # V2's warlock objects just work like dicts + class FakeImage(dict): + status = 'CREATED' + id = '99' + name = '99 name' + + fake_image = FakeImage() + fake_image.update({ + 'id': '99', + 'name': '99 name', + shade.IMAGE_MD5_KEY: '', + shade.IMAGE_SHA256_KEY: '', + }) + glance_mock.images.list.return_value = [fake_image] + + class FakeTask(dict): + status = 'success' + result = {'image_id': '99'} + + fake_task = FakeTask() + fake_task.update({ + 'id': '100', + 'status': 'success', + }) + glance_mock.tasks.get.return_value = fake_task + self._call_create_image(name='99 name', + container='image_upload_v2_test_container') + args = {'headers': {'x-object-meta-x-shade-md5': mock.ANY, + 'x-object-meta-x-shade-sha256': mock.ANY}, + 'obj': '99 name', + 'container': 'image_upload_v2_test_container'} + swift_mock.post_object.assert_called_with(**args) + swift_mock.put_object.assert_called_with( + contents=mock.ANY, + obj='99 name', + container='image_upload_v2_test_container') + glance_mock.tasks.create.assert_called_with(type='import', input={ + 'import_from': 'image_upload_v2_test_container/99 name', + 'image_properties': {'name': '99 name'}}) + args = {'owner_specified.shade.md5': mock.ANY, + 'owner_specified.shade.sha256': mock.ANY, + 'image_id': '99'} + glance_mock.images.update.assert_called_with(**args) + self.assertEqual({'99': fake_image}, self.cloud.list_images()) From 0a3cd476437e08717f3498ff93b905198407a666 Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Fri, 24 Apr 2015 16:47:42 -0700 Subject: [PATCH 0214/3836] Do not cache unsteady state images States that are expected to change should not be cached, as this will just prolong any polling loops to detect the changes. This also makes for less invalidations as we can depend on the fact that the cache will only ever contain steady states. Change-Id: Ie241db6311540725d24dcc6328f6f965820baf1b --- shade/__init__.py | 10 +++++- shade/tests/unit/test_caching.py | 52 ++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index 6d68d4530..da9b2b6d3 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -183,6 +183,14 @@ def _no_pending_volumes(volumes): return True +def _no_pending_images(images): + '''If there are any images not in a steady state, don't cache''' + for image_id, image in iter(images.items()): + if image.status not in ('active', 'deleted', 'killed'): + return False + return True + + class OpenStackCloud(object): """Represent a connection to an OpenStack Cloud. @@ -1030,7 +1038,7 @@ def _get_images_from_cloud(self, filter_deleted): def _reset_image_cache(self): self._image_cache = None - @_cache_on_arguments() + @_cache_on_arguments(should_cache_fn=_no_pending_images) def list_images(self, filter_deleted=True): """Get available glance images. diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 29b5f6ac3..c88a367a5 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -242,6 +242,58 @@ class Image(object): self.cloud.list_images.invalidate(self.cloud) self.assertEqual({'22': fake_image}, self.cloud.list_images()) + @mock.patch.object(shade.OpenStackCloud, 'glance_client') + def test_list_images_ignores_unsteady_status(self, glance_mock): + class Image(object): + id = None + name = None + status = None + steady_image = Image() + steady_image.id = '68' + steady_image.name = 'Jagr' + steady_image.status = 'active' + for status in ('queued', 'saving', 'pending_delete'): + active_image = Image() + active_image.id = self.getUniqueString() + active_image.name = self.getUniqueString() + active_image.status = status + glance_mock.images.list.return_value = [active_image] + self.assertEqual({active_image.id: active_image}, + self.cloud.list_images()) + glance_mock.images.list.return_value = [active_image, steady_image] + # Should expect steady_image to appear if active wasn't cached + self.assertEqual({active_image.id: active_image, + '68': steady_image}, + self.cloud.list_images()) + + @mock.patch.object(shade.OpenStackCloud, 'glance_client') + def test_list_images_caches_steady_status(self, glance_mock): + class Image(object): + id = None + name = None + status = None + steady_image = Image() + steady_image.id = '91' + steady_image.name = 'Federov' + steady_image.status = 'active' + first_image = None + for status in ('active', 'deleted', 'killed'): + active_image = Image() + active_image.id = self.getUniqueString() + active_image.name = self.getUniqueString() + active_image.status = status + if not first_image: + first_image = active_image + glance_mock.images.list.return_value = [active_image] + self.assertEqual({first_image.id: first_image}, + self.cloud.list_images()) + glance_mock.images.list.return_value = [active_image, steady_image] + # because we skipped the create_image code path, no invalidation + # was done, so we _SHOULD_ expect steady state images to cache and + # therefore we should _not_ expect to see the new one here + self.assertEqual({first_image.id: first_image}, + self.cloud.list_images()) + def _call_create_image(self, name, container=None): imagefile = tempfile.NamedTemporaryFile(delete=False) imagefile.write(b'\0') From 930a8b1a7c980183df5469627a734033ca39a444 Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Tue, 28 Apr 2015 15:27:27 -0700 Subject: [PATCH 0215/3836] Add functional tests for create_image Change-Id: Iadb70ca764fbc2c8102a988d6e03cf623b6df48d --- shade/tests/functional/test_image.py | 49 ++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 shade/tests/functional/test_image.py diff --git a/shade/tests/functional/test_image.py b/shade/tests/functional/test_image.py new file mode 100644 index 000000000..cef17607e --- /dev/null +++ b/shade/tests/functional/test_image.py @@ -0,0 +1,49 @@ +# -*- 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. + +""" +test_compute +---------------------------------- + +Functional tests for `shade` image methods. +""" + +import tempfile +import uuid + +from shade import openstack_cloud +from shade.tests import base +from shade.tests.functional.util import pick_image + + +class TestCompute(base.TestCase): + def setUp(self): + super(TestCompute, self).setUp() + # Shell should have OS-* envvars from openrc, typically loaded by job + self.cloud = openstack_cloud() + self.image = pick_image(self.cloud.nova_client.images.list()) + + def test_create_image(self): + test_image = tempfile.NamedTemporaryFile(delete=False) + test_image.write('\0' * 1024 * 1024) + test_image.close() + image_name = 'test-image-%s' % uuid.uuid4() + try: + self.cloud.create_image(name=image_name, + filename=test_image.name, + disk_format='raw', + container_format='bare', + wait=True) + finally: + self.cloud.delete_image(image_name, wait=True) From aa092bafd9c8ab1b19a72bf01a9a5f63ab26ee6b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 26 Apr 2015 09:21:17 -0400 Subject: [PATCH 0216/3836] Fix functional tests to run against live clouds It's possible a developer may have a test account on a live cloud, rather than a devstack. To support this, we should not make assumptions about content put into a cloud by devstack, but should request things we want - such as "the smallest flavor you have". Keep the attempt to use the cirros image to continue to support devstack, but fall back to the first ubuntu image found, since that's the most likely to be cached on compute hosts on public clouds. Change-Id: I6f0336d02cf924b3657fdc42bd151d11d5e3a6be --- shade/tests/functional/test_compute.py | 4 ++-- shade/tests/functional/util.py | 17 +++++++++-------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index 5105e0f52..7053bed73 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -33,10 +33,10 @@ def setUp(self): self.nova = self.cloud.nova_client self.flavor = pick_flavor(self.nova.flavors.list()) if self.flavor is None: - self.addDetail('pick_flavor', 'no sensible flavor available') + self.assertFalse('no sensible flavor available') self.image = pick_image(self.nova.images.list()) if self.image is None: - self.addDetail('pick_image', 'no sensible image available') + self.assertFalse('no sensible image available') def _cleanup_servers(self): for i in self.nova.servers.list(): diff --git a/shade/tests/functional/util.py b/shade/tests/functional/util.py index bddb57f3e..960bec613 100644 --- a/shade/tests/functional/util.py +++ b/shade/tests/functional/util.py @@ -18,20 +18,21 @@ Util methods for functional tests """ +import operator def pick_flavor(flavors): - """Given a flavor list pick a reasonable one.""" - for flavor in flavors: - if flavor.name == 'm1.tiny': - return flavor - - for flavor in flavors: - if flavor.name == 'm1.small': - return flavor + """Given a flavor list pick the smallest one.""" + for flavor in sorted( + flavors, + key=operator.attrgetter('ram')): + return flavor def pick_image(images): for image in images: if image.name.startswith('cirros') and image.name.endswith('-uec'): return image + for image in images: + if image.name.lower().startswith('ubuntu'): + return image From a540167587ccbbea16227a66170ac1d3ad68bfcb Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 29 Apr 2015 15:02:59 -0400 Subject: [PATCH 0217/3836] Update secgroup API for new get/list/search API Security group API now conforms to the new get/list/search interface. Instead of a Nova object being returned, we now return a dict (or list of dicts). Change-Id: I0b84ef6065101948ffb2a0f56235bda7a071ad17 --- shade/__init__.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 535f804ed..0044fba6f 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -824,6 +824,10 @@ def search_flavors(self, name_or_id=None, filters=None): flavors = self.list_flavors() return self._filter_list(flavors, name_or_id, filters) + def search_security_groups(self, name_or_id=None, filters=None): + groups = self.list_security_groups() + return self._filter_list(groups, name_or_id, filters) + def list_networks(self): return self.manager.submitTask(_tasks.NetworkList())['networks'] @@ -848,6 +852,11 @@ def list_flavors(self): self.manager.submitTask(_tasks.FlavorList()) ) + def list_security_groups(self): + return meta.obj_list_to_dict( + self.manager.submitTask(_tasks.SecurityGroupList()) + ) + def get_network(self, name_or_id, filters=None): return self._get_entity(self.search_networks, name_or_id, filters) @@ -863,6 +872,10 @@ def get_volume(self, name_or_id, filters=None): def get_flavor(self, name_or_id, filters=None): return self._get_entity(self.search_flavors, name_or_id, filters) + def get_security_group(self, name_or_id, filters=None): + return self._get_entity(self.search_security_groups, + name_or_id, filters) + # TODO(Shrews): This will eventually need to support tenant ID and # provider networks, which are admin-level params. def create_network(self, name, shared=False, admin_state_up=True): @@ -1492,12 +1505,6 @@ def get_server_meta(self, server): groups = meta.get_groups_from_server(self, server, server_vars) return dict(server_vars=server_vars, groups=groups) - def get_security_group(self, name_or_id): - for secgroup in self.manager.submitTask(_tasks.SecurityGroupList()): - if name_or_id in (secgroup.name, secgroup.id): - return secgroup - return None - def get_openstack_vars(self, server): return meta.get_hostvars_from_server(self, server) From 128a62f2fb43e0a9def19e9d64f20f0d666c16e9 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Thu, 9 Apr 2015 22:19:08 -0400 Subject: [PATCH 0218/3836] Add Ironic machine power state pass-through Adding machine power state passthrough in the form of a _set_machine_power_state method, along with wrapper methods set_machine_power_on, set_machine_power_off and set_machine_power_reboot. Tests to utilize the on, off, and reboot methods have been which all invoke the _set_machine_power_state method. Change-Id: I4e1ce863d5a93d81ec90ae1c244cbfef68912916 --- shade/__init__.py | 87 ++++++++++++++++++++++++++++++++++ shade/_tasks.py | 5 ++ shade/tests/unit/test_shade.py | 40 ++++++++++++++++ 3 files changed, 132 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index b1fee4822..f47de18ca 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2479,6 +2479,93 @@ def remove_machine_from_maintenance(self, name_or_id): """ self.set_machine_maintenance_state(name_or_id, False) + def _set_machine_power_state(self, name_or_id, state): + """Set machine power state to on or off + + This private method allows a user to turn power on or off to + a node via the Baremetal API. + + :params string name_or_id: A string representing the baremetal + node to have power turned to an "on" + state. + :params string state: A value of "on", "off", or "reboot" that is + passed to the baremetal API to be asserted to + the machine. In the case of the "reboot" state, + Ironic will return the host to the "on" state. + + :raises: OpenStackCloudException on operation error or. + + :returns: None + """ + try: + power = self.manager.submitTask( + _tasks.MachineSetPower(node_id=name_or_id, + state=state)) + if power is not None: + self.log.debug( + "Failed setting machine power state on node %s. User " + "requested '%s'.' Received: %s" % ( + name_or_id, state, power)) + raise OpenStackCloudException( + "Failed setting machine power state on node %s. " + "Received: %s" % (name_or_id, power)) + return None + except Exception as e: + self.log.debug( + "Error setting machine power state on node %s. User " + "requested '%s'.'" % (name_or_id, state), + exc_info=True) + raise OpenStackCloudException( + "Error setting machine power state on node %s. " + "Error: %s" % (name_or_id, str(e))) + + def set_machine_power_on(self, name_or_id): + """Activate baremetal machine power + + This is a method that sets the node power state to "on". + + :params string name_or_id: A string representing the baremetal + node to have power turned to an "on" + state. + + :raises: OpenStackCloudException on operation error. + + :returns: None + """ + self._set_machine_power_state(name_or_id, 'on') + + def set_machine_power_off(self, name_or_id): + """De-activate baremetal machine power + + This is a method that sets the node power state to "off". + + :params string name_or_id: A string representing the baremetal + node to have power turned to an "off" + state. + + :raises: OpenStackCloudException on operation error. + + :returns: + """ + self._set_machine_power_state(name_or_id, 'off') + + def set_machine_power_reboot(self, name_or_id): + """De-activate baremetal machine power + + This is a method that sets the node power state to "reboot", which + in essence changes the machine power state to "off", and that back + to "on". + + :params string name_or_id: A string representing the baremetal + node to have power turned to an "off" + state. + + :raises: OpenStackCloudException on operation error. + + :returns: None + """ + self._set_machine_power_state(name_or_id, 'reboot') + def activate_node(self, uuid, configdrive=None): return meta.obj_to_dict( self.node_set_provision_state(uuid, 'active', configdrive)) diff --git a/shade/_tasks.py b/shade/_tasks.py index 9232403b0..d2b21cd07 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -296,3 +296,8 @@ def main(self, client): class MachineSetMaintenance(task_manager.Task): def main(self, client): return client.ironic_client.node.set_maintenance(**self.args) + + +class MachineSetPower(task_manager.Task): + def main(self, client): + return client.ironic_client.node.set_power_state(**self.args) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 26c0e3ebb..42d773ebe 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -517,6 +517,46 @@ def test_remove_machine_from_maintenance(self, mock_client): node_id='node01', state='false') + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_set_machine_power_on(self, mock_client): + mock_client.node.set_power_state.return_value = None + node_id = 'node01' + return_value = self.cloud.set_machine_power_on(node_id) + self.assertEqual(None, return_value) + mock_client.node.set_power_state.assert_called_with( + node_id='node01', + state='on') + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_set_machine_power_off(self, mock_client): + mock_client.node.set_power_state.return_value = None + node_id = 'node01' + return_value = self.cloud.set_machine_power_off(node_id) + self.assertEqual(None, return_value) + mock_client.node.set_power_state.assert_called_with( + node_id='node01', + state='off') + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_set_machine_power_reboot(self, mock_client): + mock_client.node.set_power_state.return_value = None + node_id = 'node01' + return_value = self.cloud.set_machine_power_reboot(node_id) + self.assertEqual(None, return_value) + mock_client.node.set_power_state.assert_called_with( + node_id='node01', + state='reboot') + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_set_machine_power_reboot_failure(self, mock_client): + mock_client.node.set_power_state.return_value = 'failure' + self.assertRaises(shade.OpenStackCloudException, + self.cloud.set_machine_power_reboot, + 'node01') + mock_client.node.set_power_state.assert_called_with( + node_id='node01', + state='reboot') + @mock.patch.object(shade.OpenStackCloud, 'glance_client') def test_get_image_name(self, glance_mock): From 7468d1150c55c8cf277605d6e2cac22854751061 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Wed, 29 Apr 2015 16:19:43 -0400 Subject: [PATCH 0219/3836] Change ironic maintenance method to align with power method Upon obtaining a better understanding of the Ironicclient and Ironic API interaction, along with what is actually returned, which should be None, it makes sense to have set_machine_maintenance_state match set_power_state in behavior. Change-Id: I2899f290c0521e35d010dc9246b590ca9e949214 --- shade/__init__.py | 21 ++++++++++++++++----- shade/tests/unit/test_shade.py | 3 +++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index f47de18ca..a4304c78d 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2444,19 +2444,27 @@ def set_machine_maintenance_state( :raises: OpenStackCloudException on operation error. - :returns: Per the API, no value should be returned with a successful - operation. + :returns: None """ try: if state: - return self.manager.submitTask( + result = self.manager.submitTask( _tasks.MachineSetMaintenance(node_id=name_or_id, state='true', maint_reason=reason)) else: - return self.manager.submitTask( + result = self.manager.submitTask( _tasks.MachineSetMaintenance(node_id=name_or_id, state='false')) + if result is not None: + self.log.debug( + "Failed setting machine maintenance state on node %s. " + "User requested '%s'.' Received: %s" % ( + name_or_id, state, result)) + raise OpenStackCloudException( + "Failed setting machine maintenance state on node %s. " + "Received: %s" % (name_or_id, result)) + return None except Exception as e: self.log.debug( "failed setting maintenance state on node %s" % name_or_id, @@ -2475,7 +2483,10 @@ def remove_machine_from_maintenance(self, name_or_id): :param string name_or_id: The Name or UUID value representing the baremetal node. - :returns: Dictonary object representing the updated node. + + :raises: OpenStackCloudException on operation error. + + :returns: None """ self.set_machine_maintenance_state(name_or_id, False) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 42d773ebe..fbbf28825 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -493,6 +493,7 @@ def test_unregister_machine(self, mock_client): @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_set_machine_maintenace_state(self, mock_client): + mock_client.node.set_maintenance.return_value = None node_id = 'node01' reason = 'no reason' self.cloud.set_machine_maintenance_state(node_id, True, reason=reason) @@ -503,6 +504,7 @@ def test_set_machine_maintenace_state(self, mock_client): @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_set_machine_maintenace_state_false(self, mock_client): + mock_client.node.set_maintenance.return_value = None node_id = 'node01' self.cloud.set_machine_maintenance_state(node_id, False) mock_client.node.set_maintenance.assert_called_with( @@ -511,6 +513,7 @@ def test_set_machine_maintenace_state_false(self, mock_client): @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_remove_machine_from_maintenance(self, mock_client): + mock_client.node.set_maintenance.return_value = None node_id = 'node01' self.cloud.remove_machine_from_maintenance(node_id) mock_client.node.set_maintenance.assert_called_with( From 3f8431a9de68b9e2fa24237f51d5497ca5da0295 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 30 Apr 2015 02:26:34 +0200 Subject: [PATCH 0220/3836] Add flag to trigger task interface for rackspace It turns out that the task interface for upload is not implied by v2 glance api. Add a boolean flag that can be consumed elsewhere. Change-Id: I2cdcfe302a73ebfa7f739399c1eeb3bc9f96ab3c --- doc/source/vendor-support.rst | 1 + os_client_config/vendors.py | 1 + 2 files changed, 2 insertions(+) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index f74038e4b..b9eb769e1 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -41,6 +41,7 @@ IAD Washington, D.C. * Compute Service Name is `cloudServersOpenStack` * Image API Version is 2 * Images must be in `vhd` format +* Images must be uploaded using the Glance Task Interface Dreamhost --------- diff --git a/os_client_config/vendors.py b/os_client_config/vendors.py index 70756aebf..f0a526609 100644 --- a/os_client_config/vendors.py +++ b/os_client_config/vendors.py @@ -30,6 +30,7 @@ database_service_type='rax:database', compute_service_name='cloudServersOpenStack', image_api_version='2', + image_api_use_tasks=True, image_format='vhd', ), dreamhost=dict( From 87000d9f768f5ca95fea2727ea2ca0f32865d923 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 30 Apr 2015 09:36:38 -0400 Subject: [PATCH 0221/3836] Allow for int or string ID comparisons Some IDs are clearly strings (e.g., UUIDs) and some look like integers (e.g., flavor ID of '100'). Let's make it easy for a user by allowing for string or int. For example: flavor = cloud.get_flavor(100) or flavor = cloud.get_flavor('100') The former example using the int doesn't work, but this fixes that. Change-Id: If201352a9917d7c8fba7b386b38fced9949f00a9 --- shade/__init__.py | 4 ++-- shade/tests/unit/test_shade.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 5cfae53c0..0c95df5ea 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -780,11 +780,11 @@ def _filter_list(self, data, name_or_id, filters): if name_or_id: identifier_matches = [] for e in data: - e_id = e.get('id', None) + e_id = str(e.get('id', None)) e_name = e.get('name', None) # cinder likes to be different and use display_name e_display_name = e.get('display_name', None) - if name_or_id in (e_id, e_name, e_display_name): + if str(name_or_id) in (e_id, e_name, e_display_name): identifier_matches.append(e) data = identifier_matches diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 5db0d4293..a2e60ad5f 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -243,6 +243,20 @@ def test_get_flavor_by_ram_not_found(self, mock_list): self.cloud.get_flavor_by_ram, ram=100) + @mock.patch.object(shade.OpenStackCloud, 'list_flavors') + def test_get_flavor_string_and_int(self, mock_list): + class Flavor1(object): + id = '1' + name = 'vanilla ice cream' + ram = 100 + + vanilla = meta.obj_to_dict(Flavor1()) + mock_list.return_value = [vanilla] + flavor1 = self.cloud.get_flavor('1') + self.assertEquals(vanilla, flavor1) + flavor2 = self.cloud.get_flavor(1) + self.assertEquals(vanilla, flavor2) + class TestShadeOperator(base.TestCase): From abdb5ca0e78cf017b1353c5e2c39d76b237e6cf2 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 30 Apr 2015 14:58:57 -0400 Subject: [PATCH 0222/3836] Add minor OperatorCloud documentation The documentation for the OperatorCloud methods with docstrings will not be generated unless the class itself has a docstring. This adds a simple docstring just to get the documentation generated. Also adds a link to the CONTRIBUTING.rst doc pointing to the project documentation. Change-Id: I535289a938f68895c19b5e226d2e2302b566d00d --- CONTRIBUTING.rst | 5 ++++- shade/__init__.py | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index ee1e32b0e..798b5b5af 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -24,6 +24,9 @@ signed OpenStack's contributor's agreement. Project Hosting Details ------------------------- +Project Documentation + http://docs.openstack.org/infra/shade/ + Bug tracker http://storyboard.openstack.org @@ -31,7 +34,7 @@ Mailing list (prefix subjects with ``[shade]`` for faster responses) http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-infra Code Hosting - * https://git.openstack.org/cgit/openstack-infra/shade + https://git.openstack.org/cgit/openstack-infra/shade Code Review https://review.openstack.org/#/q/status:open+project:openstack-infra/shade,n,z diff --git a/shade/__init__.py b/shade/__init__.py index 5cfae53c0..1bb7a99d7 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2096,6 +2096,8 @@ def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, class OperatorCloud(OpenStackCloud): + """Represent a privileged/operator connection to an OpenStack Cloud. + """ @property def auth_token(self): From 3b64ee66e4b56d81ea167afd7bb4babfe2d39c02 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 24 Apr 2015 18:32:15 -0400 Subject: [PATCH 0223/3836] Split exceptions into their own file We were getting to the point where we were making some circular depends. Clear that up by splitting the exceptions into their own file. We're importing * to keep the external interface in the ansible modules. Change-Id: I1d804377c3cd4eebcc94624902f9e5ec6ebf9321 --- shade/__init__.py | 20 +--------------- shade/exc.py | 32 +++++++++++++++++++++++++ shade/tests/unit/test_create_server.py | 4 ++-- shade/tests/unit/test_rebuild_server.py | 4 ++-- shade/tests/unit/test_shade.py | 17 ++++++------- 5 files changed, 46 insertions(+), 31 deletions(-) create mode 100644 shade/exc.py diff --git a/shade/__init__.py b/shade/__init__.py index 5cfae53c0..4a9f377a5 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -41,6 +41,7 @@ import warnings warnings.filterwarnings('ignore', 'Certificate has no `subjectAltName`') +from shade.exc import * # noqa from shade import meta from shade import task_manager from shade import _tasks @@ -58,25 +59,6 @@ } -class OpenStackCloudException(Exception): - def __init__(self, message, extra_data=None): - args = [message] - if extra_data: - args.append(extra_data) - super(OpenStackCloudException, self).__init__(*args) - self.extra_data = extra_data - - def __str__(self): - if self.extra_data is not None: - return "%s (Extra: %s)" % ( - Exception.__str__(self), self.extra_data) - return Exception.__str__(self) - - -class OpenStackCloudTimeout(OpenStackCloudException): - pass - - def openstack_clouds(config=None, debug=False): if not config: config = os_client_config.OpenStackConfig() diff --git a/shade/exc.py b/shade/exc.py new file mode 100644 index 000000000..fed6c4bc3 --- /dev/null +++ b/shade/exc.py @@ -0,0 +1,32 @@ +# Copyright (c) 2015 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. + + +class OpenStackCloudException(Exception): + def __init__(self, message, extra_data=None): + args = [message] + if extra_data: + args.append(extra_data) + super(OpenStackCloudException, self).__init__(*args) + self.extra_data = extra_data + + def __str__(self): + if self.extra_data is not None: + return "%s (Extra: %s)" % ( + Exception.__str__(self), self.extra_data) + return Exception.__str__(self) + + +class OpenStackCloudTimeout(OpenStackCloudException): + pass diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index cca1570ff..e581d0ad2 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -20,8 +20,8 @@ """ from mock import patch, Mock -from shade import ( - OpenStackCloud, OpenStackCloudException, OpenStackCloudTimeout) +from shade import OpenStackCloud +from shade.exc import (OpenStackCloudException, OpenStackCloudTimeout) from shade.tests import base diff --git a/shade/tests/unit/test_rebuild_server.py b/shade/tests/unit/test_rebuild_server.py index 960840a85..c455b246a 100644 --- a/shade/tests/unit/test_rebuild_server.py +++ b/shade/tests/unit/test_rebuild_server.py @@ -20,8 +20,8 @@ """ from mock import patch, Mock -from shade import ( - OpenStackCloud, OpenStackCloudException, OpenStackCloudTimeout) +from shade import OpenStackCloud +from shade.exc import (OpenStackCloudException, OpenStackCloudTimeout) from shade.tests.unit import base diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 5db0d4293..6ddcb7751 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -15,6 +15,7 @@ import mock import shade +from shade import exc from shade import meta from shade.tests.unit import base @@ -89,7 +90,7 @@ def test_delete_router(self, mock_client, mock_search): @mock.patch.object(shade.OpenStackCloud, 'neutron_client') def test_delete_router_not_found(self, mock_client, mock_search): mock_search.return_value = [] - self.assertRaises(shade.OpenStackCloudException, + self.assertRaises(exc.OpenStackCloudException, self.cloud.delete_router, 'goofy') self.assertFalse(mock_client.delete_router.called) @@ -100,7 +101,7 @@ def test_delete_router_multiple_found(self, mock_client): router2 = dict(id='456', name='mickey') mock_client.list_routers.return_value = dict(routers=[router1, router2]) - self.assertRaises(shade.OpenStackCloudException, + self.assertRaises(exc.OpenStackCloudException, self.cloud.delete_router, 'mickey') self.assertFalse(mock_client.delete_router.called) @@ -133,7 +134,7 @@ def test_create_subnet(self, mock_client, mock_search): def test_create_subnet_bad_network(self, mock_client, mock_list): net1 = dict(id='123', name='donald') mock_list.return_value = [net1] - self.assertRaises(shade.OpenStackCloudException, + self.assertRaises(exc.OpenStackCloudException, self.cloud.create_subnet, 'duck', '192.168.199.0/24') self.assertFalse(mock_client.create_subnet.called) @@ -144,7 +145,7 @@ def test_create_subnet_non_unique_network(self, mock_client, mock_search): net1 = dict(id='123', name='donald') net2 = dict(id='456', name='donald') mock_search.return_value = [net1, net2] - self.assertRaises(shade.OpenStackCloudException, + self.assertRaises(exc.OpenStackCloudException, self.cloud.create_subnet, 'donald', '192.168.199.0/24') self.assertFalse(mock_client.create_subnet.called) @@ -161,7 +162,7 @@ def test_delete_subnet(self, mock_client, mock_search): @mock.patch.object(shade.OpenStackCloud, 'neutron_client') def test_delete_subnet_not_found(self, mock_client, mock_search): mock_search.return_value = [] - self.assertRaises(shade.OpenStackCloudException, + self.assertRaises(exc.OpenStackCloudException, self.cloud.delete_subnet, 'goofy') self.assertFalse(mock_client.delete_subnet.called) @@ -172,7 +173,7 @@ def test_delete_subnet_multiple_found(self, mock_client): subnet2 = dict(id='456', name='mickey') mock_client.list_subnets.return_value = dict(subnets=[subnet1, subnet2]) - self.assertRaises(shade.OpenStackCloudException, + self.assertRaises(exc.OpenStackCloudException, self.cloud.delete_subnet, 'mickey') self.assertFalse(mock_client.delete_subnet.called) @@ -524,8 +525,8 @@ class fake_node: def test_register_machine_port_create_failed(self, mock_client): nics = [{'mac': '00:00:00:00:00:00'}] mock_client.port.create.side_effect = ( - shade.OpenStackCloudException("Error")) - self.assertRaises(shade.OpenStackCloudException, + exc.OpenStackCloudException("Error")) + self.assertRaises(exc.OpenStackCloudException, self.cloud.register_machine, nics) self.assertTrue(mock_client.node.create.called) From 2c4d909111f1ffe49456eafd377e5407d0b7cac0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 22 Apr 2015 20:00:05 -0400 Subject: [PATCH 0224/3836] Raise a shade exception on broken volumes Some clouds don't have volumes and bomb out here, which is a bad place to bomb out. A later patch will add an active check for service existence, but we still should catch and throw here so that we're not leaking python-*client exceptions. Change-Id: I1f768dcd6ad6f54fb1b5638e9486653ee07965ef --- shade/__init__.py | 13 ++++++++++--- shade/meta.py | 13 +++++++++---- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 4a9f377a5..7db69bdaa 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -839,9 +839,16 @@ def list_volumes(self, cache=True): if not cache: warnings.warn('cache argument to list_volumes is deprecated. Use ' 'invalidate instead.') - return meta.obj_list_to_dict( - self.manager.submitTask(_tasks.VolumeList()) - ) + try: + return meta.obj_list_to_dict( + self.manager.submitTask(_tasks.VolumeList()) + ) + except Exception as e: + self.log.debug( + "cinder could not list volumes: {message}".format( + message=str(e)), + exc_info=True) + raise OpenStackCloudException("Error fetching volume list") @_cache_on_arguments() def list_flavors(self): diff --git a/shade/meta.py b/shade/meta.py index fafb50837..e51852d6c 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -15,6 +15,8 @@ import six +from shade import exc + NON_CALLABLES = (six.string_types, bool, dict, int, list, type(None)) @@ -126,10 +128,13 @@ def get_hostvars_from_server(cloud, server, mounts=None): server_vars['image'].pop('links', None) volumes = [] - for volume in cloud.get_volumes(server): - # Make things easier to consume elsewhere - volume['device'] = volume['attachments'][0]['device'] - volumes.append(volume) + try: + for volume in cloud.get_volumes(server): + # Make things easier to consume elsewhere + volume['device'] = volume['attachments'][0]['device'] + volumes.append(volume) + except exc.OpenStackCloudException: + pass server_vars['volumes'] = volumes if mounts: for mount in mounts: From 896f844948c06ce26a5291eae1afa8b3c87cd0ca Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 24 Apr 2015 10:31:03 -0400 Subject: [PATCH 0225/3836] Add a method for getting an endpoint There are a couple of different things you need to get the right endpoint. We know them all, but the logic is spread out. Consolidate. Also, put the silly keystone-specific logic there, rather than in keystone_client. Change-Id: Ide66c62fe0babddb1530b4f3472a6aeb9713917a --- shade/__init__.py | 41 +++++++++++++++++----------------- shade/tests/unit/test_shade.py | 31 +++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 21 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 7db69bdaa..a918b3b4f 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -375,8 +375,7 @@ def keystone_client(self): try: self._keystone_client = keystone_client.Client( session=self.keystone_session, - auth_url=self.keystone_session.get_endpoint( - interface=ksc_auth.AUTH_INTERFACE), + auth_url=self.get_endpoint('identity'), timeout=self.api_timeout) except Exception as e: self.log.debug( @@ -549,7 +548,7 @@ def _get_glance_api_version(self): return self.api_versions['image'] # Yay. We get to guess ... # Get rid of trailing '/' if present - endpoint = self._get_glance_endpoint() + endpoint = self.get_endpoint('image') if endpoint.endswith('/'): endpoint = endpoint[:-1] url_bits = endpoint.split('/') @@ -557,17 +556,11 @@ def _get_glance_api_version(self): return url_bits[-1][1] return '1' # Who knows? Let's just try 1 ... - def _get_glance_endpoint(self): - if self._glance_endpoint is None: - self._glance_endpoint = self.get_endpoint( - service_type=self.get_service_type('image')) - return self._glance_endpoint - @property def glance_client(self): if self._glance_client is None: token = self.auth_token - endpoint = self._get_glance_endpoint() + endpoint = self.get_endpoint('image') glance_api_version = self._get_glance_api_version() kwargs = dict() if self.api_timeout is not None: @@ -591,7 +584,7 @@ def swift_client(self): if self._swift_client is None: token = self.auth_token endpoint = self.get_endpoint( - service_type=self.get_service_type('object-store')) + service_key='object-store') self._swift_client = swift_client.Connection( preauthurl=endpoint, preauthtoken=token, @@ -633,7 +626,7 @@ def _get_trove_api_version(self, endpoint): def trove_client(self): if self._trove_client is None: endpoint = self.get_endpoint( - service_type=self.get_service_type('database')) + service_key='database') trove_api_version = self._get_trove_api_version(endpoint) # Make the connection - can't use keystone session until there # is one @@ -694,18 +687,24 @@ def get_flavor_by_ram(self, ram, include=None): "Could not find a flavor with {ram} and '{include}'".format( ram=ram, include=include)) - def get_endpoint(self, service_type): - if service_type in self.endpoints: - return self.endpoints[service_type] + def get_endpoint(self, service_key): + if service_key in self.endpoints: + return self.endpoints[service_key] try: - endpoint = self.keystone_session.get_endpoint( - service_type=service_type, - interface=self.endpoint_type, - region_name=self.region_name) + # keystone is a special case in keystone, because what? + if service_key == 'identity': + endpoint = self.keystone_session.get_endpoint( + interface=ksc_auth.AUTH_INTERFACE) + else: + endpoint = self.keystone_session.get_endpoint( + service_type=self.get_service_type(service_key), + service_name=self.get_service_name(service_key), + interface=self.endpoint_type, + region_name=self.region_name) except Exception as e: self.log.debug("keystone cannot get endpoint", exc_info=True) raise OpenStackCloudException( - "Error getting %s endpoint: %s" % (service_type, str(e))) + "Error getting %s endpoint: %s" % (service_key, str(e))) return endpoint def list_servers(self): @@ -2110,7 +2109,7 @@ def ironic_client(self): # dict as the endpoint. endpoint = self.auth['endpoint'] else: - endpoint = self.get_endpoint(service_type='baremetal') + endpoint = self.get_endpoint(service_key='baremetal') try: self._ironic_client = ironic_client.Client( '1', endpoint, token=token, diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 6ddcb7751..dc5e775fc 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -12,7 +12,10 @@ # License for the specific language governing permissions and limitations # under the License. +from keystoneclient import auth as ksc_auth + import mock +import testtools import shade from shade import exc @@ -591,3 +594,31 @@ class Image(object): glance_mock.images.list.return_value = [fake_image] self.assertEqual('22', self.cloud.get_image_id('22')) self.assertEqual('22', self.cloud.get_image_id('22 name')) + + def test_get_endpoint_provided(self): + self.cloud.endpoints['image'] = 'http://fake.url' + self.assertEqual('http://fake.url', self.cloud.get_endpoint('image')) + + @mock.patch.object(shade.OpenStackCloud, 'keystone_session') + def test_get_endpoint_session(self, session_mock): + session_mock.get_endpoint.return_value = 'http://fake.url' + self.assertEqual('http://fake.url', self.cloud.get_endpoint('image')) + + @mock.patch.object(shade.OpenStackCloud, 'keystone_session') + def test_get_endpoint_exception(self, session_mock): + class FakeException(Exception): + pass + + def side_effect(*args, **kwargs): + raise FakeException("No service") + session_mock.get_endpoint.side_effect = side_effect + with testtools.ExpectedException( + exc.OpenStackCloudException, + "Error getting image endpoint: No service"): + self.cloud.get_endpoint("image") + + @mock.patch.object(shade.OpenStackCloud, 'keystone_session') + def test_get_endpoint_identity(self, session_mock): + self.cloud.get_endpoint('identity') + session_mock.get_endpoint.assert_called_with( + interface=ksc_auth.AUTH_INTERFACE) From be87e99e5daa47d3935c488655fb099c30d5831a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 24 Apr 2015 11:21:09 -0400 Subject: [PATCH 0226/3836] Add early service fail and active check method To make things clearer - do an active check at each client instantiation to make sure that the service exists in the catalog so that we can throw a clear error. Also, add a method that can be used for switching on things like "if neutron: neutron-floating-ips: else: nova-floating-ips" that doesn't have to do a REST roundtrip to find the failure. Change-Id: I43b32fff56e09f31bd6d6845b3bf9b5414632651 --- shade/__init__.py | 17 +++++++++++++++++ shade/exc.py | 4 ++++ shade/tests/unit/test_shade.py | 18 ++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index a918b3b4f..001c4bf13 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -317,6 +317,8 @@ def nova_client(self): # Make the connection try: + # trigger exception on lack of compute. (what?) + self.get_endpoint('compute') self._nova_client = nova_client.Client( self._get_nova_api_version(), session=self.keystone_session, @@ -596,6 +598,8 @@ def swift_client(self): def cinder_client(self): if self._cinder_client is None: + # trigger exception on lack of cinder + self.get_endpoint('volume') # Make the connection self._cinder_client = cinder_client.Client( session=self.keystone_session, @@ -648,6 +652,8 @@ def trove_client(self): @property def neutron_client(self): if self._neutron_client is None: + # trigger exception on lack of neutron + self.get_endpoint('network') self._neutron_client = neutron_client.Client( token=self.auth_token, session=self.keystone_session, @@ -705,8 +711,19 @@ def get_endpoint(self, service_key): self.log.debug("keystone cannot get endpoint", exc_info=True) raise OpenStackCloudException( "Error getting %s endpoint: %s" % (service_key, str(e))) + if endpoint is None: + raise OpenStackCloudUnavailableService( + "Cloud {cloud} does not have a {service} service".format( + cloud=self.name, service=service_key)) return endpoint + def has_service(self, service_key): + try: + self.get_endpoint(service_key) + return True + except OpenStackCloudException: + return False + def list_servers(self): return self.manager.submitTask(_tasks.ServerList()) diff --git a/shade/exc.py b/shade/exc.py index fed6c4bc3..cc2e4c326 100644 --- a/shade/exc.py +++ b/shade/exc.py @@ -30,3 +30,7 @@ def __str__(self): class OpenStackCloudTimeout(OpenStackCloudException): pass + + +class OpenStackCloudUnavailableService(OpenStackCloudException): + pass diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index dc5e775fc..58c4c4f89 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -617,8 +617,26 @@ def side_effect(*args, **kwargs): "Error getting image endpoint: No service"): self.cloud.get_endpoint("image") + @mock.patch.object(shade.OpenStackCloud, 'keystone_session') + def test_get_endpoint_unavailable(self, session_mock): + session_mock.get_endpoint.return_value = None + with testtools.ExpectedException( + exc.OpenStackCloudUnavailableService, + "Cloud.*does not have a image service"): + self.cloud.get_endpoint("image") + @mock.patch.object(shade.OpenStackCloud, 'keystone_session') def test_get_endpoint_identity(self, session_mock): self.cloud.get_endpoint('identity') session_mock.get_endpoint.assert_called_with( interface=ksc_auth.AUTH_INTERFACE) + + @mock.patch.object(shade.OpenStackCloud, 'keystone_session') + def test_has_service_no(self, session_mock): + session_mock.get_endpoint.return_value = None + self.assertFalse(self.cloud.has_service("image")) + + @mock.patch.object(shade.OpenStackCloud, 'keystone_session') + def test_has_service_yes(self, session_mock): + session_mock.get_endpoint.return_value = 'http://fake.url' + self.assertTrue(self.cloud.has_service("image")) From e1c2cd973b08e4448f0711019650eaad7f180ca6 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Wed, 22 Apr 2015 13:03:47 -0700 Subject: [PATCH 0227/3836] Add thread sync points to Task Although the default behavior of Task is to return immediately in shade, the reason it's abstracted at all is to facilitate multi-threaded rate-limited TaskManagers such as the one found in nodepool. In order for those to work, we kind of need the wait conditions to actual do something. Change-Id: Icce9729aa2f8f31931dd9d3723d9d5d758436fa9 --- shade/task_manager.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/shade/task_manager.py b/shade/task_manager.py index 879bc154e..fca5d3bb1 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -17,8 +17,9 @@ # limitations under the License. import abc -import sys import logging +import sys +import threading import time import six @@ -32,7 +33,9 @@ class Task(object): around each external REST interaction. Task provides an interface to encapsulate each such interaction. Also, although shade itself operates normally in a single-threaded direct action manner, consuming - programs may provide a multi-threaded TaskManager themselves. + programs may provide a multi-threaded TaskManager themselves. For that + reason, Task uses threading events to ensure appropriate wait conditions. + These should be a no-op in single-threaded applications. A consumer is expected to overload the main method. @@ -44,6 +47,7 @@ def __init__(self, **kw): self._exception = None self._traceback = None self._result = None + self._finished = threading.Event() self.args = kw @abc.abstractmethod @@ -52,12 +56,15 @@ def main(self, client): def done(self, result): self._result = result + self._finished.set() def exception(self, e, tb): self._exception = e self._traceback = tb + self._finished.set() def wait(self): + self._finished.wait() if self._exception: six.reraise(self._exception, None, self._traceback) return self._result From 59848071e41c1fec76928d8285223b5c30c4ce9a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 26 Apr 2015 10:47:57 -0400 Subject: [PATCH 0228/3836] Add more tests for server metadata processing Make sure that each of the things we do in get_hostvars_for_server is checked as working. In walking through it, found a bug in image ids from boot-from-volume servers. Change-Id: If71d2cd114c5b5406a39dbb04c96b61fc7193399 --- shade/meta.py | 3 +- shade/tests/unit/test_meta.py | 128 +++++++++++++++++++++++++++++----- 2 files changed, 111 insertions(+), 20 deletions(-) diff --git a/shade/meta.py b/shade/meta.py index e51852d6c..3f2567049 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -117,8 +117,9 @@ def get_hostvars_from_server(cloud, server, mounts=None): server_vars['flavor'].pop('links', None) # OpenStack can return image as a string when you've booted from volume - if unicode(server.image) == server.image: + if str(server.image) == server.image: image_id = server.image + server_vars['image'] = dict(id=image_id) else: image_id = server.image.get('id', None) if image_id: diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 644a3841d..3071f3113 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -12,10 +12,43 @@ # See the License for the specific language governing permissions and # limitations under the License. +import mock import testtools +from shade import exc from shade import meta +PRIVATE_V4 = '198.51.100.3' +PUBLIC_V4 = '192.0.2.99' + + +class FakeCloud(object): + region_name = 'test-region' + name = 'test-name' + private = False + + def get_flavor_name(self, id): + return 'test-flavor-name' + + def get_image_name(self, id): + return 'test-image-name' + + def get_volumes(self, server): + return [] + + +class FakeServer(object): + id = 'test-id-0' + metadata = {'group': 'test-group'} + addresses = {'private': [{'OS-EXT-IPS:type': 'fixed', + 'addr': PRIVATE_V4, + 'version': 4}], + 'public': [{'OS-EXT-IPS:type': 'floating', + 'addr': PUBLIC_V4, + 'version': 4}]} + flavor = {'id': '101'} + image = {'id': '471c2475-da2f-47ac-aba5-cb4aa3d546f5'} + class TestMeta(testtools.TestCase): def test_find_nova_addresses_key_name(self): @@ -36,26 +69,11 @@ def test_find_nova_addresses_ext_tag(self): self.assertEqual([], meta.find_nova_addresses(addrs, ext_tag='foo')) def test_get_server_ip(self): - class Server(object): - addresses = {'private': [{'OS-EXT-IPS:type': 'fixed', - 'addr': '198.51.100.3', - 'version': 4}], - 'public': [{'OS-EXT-IPS:type': 'floating', - 'addr': '192.0.2.99', - 'version': 4}]} - srv = Server() - self.assertEqual('198.51.100.3', meta.get_server_private_ip(srv)) - self.assertEqual('192.0.2.99', meta.get_server_public_ip(srv)) + srv = FakeServer() + self.assertEqual(PRIVATE_V4, meta.get_server_private_ip(srv)) + self.assertEqual(PUBLIC_V4, meta.get_server_public_ip(srv)) def test_get_groups_from_server(self): - class Cloud(object): - region_name = 'test-region' - name = 'test-name' - - class Server(object): - id = 'test-id-0' - metadata = {'group': 'test-group'} - server_vars = {'flavor': 'test-flavor', 'image': 'test-image', 'az': 'test-az'} @@ -69,7 +87,8 @@ class Server(object): 'test-az', 'test-region_test-az', 'test-name_test-region_test-az'], - meta.get_groups_from_server(Cloud(), Server(), server_vars)) + meta.get_groups_from_server( + FakeCloud(), FakeServer(), server_vars)) def test_obj_list_to_dict(self): """Test conversion of a list of objects to a list of dictonaries""" @@ -83,3 +102,74 @@ class obj1(object): new_list = meta.obj_list_to_dict(list) self.assertEqual(new_list[0]['value'], 0) self.assertEqual(new_list[1]['value'], 1) + + def test_basic_hostvars(self): + hostvars = meta.get_hostvars_from_server(FakeCloud(), FakeServer()) + self.assertNotIn('links', hostvars) + self.assertEqual(PRIVATE_V4, hostvars['private_v4']) + self.assertEqual(PUBLIC_V4, hostvars['public_v4']) + self.assertEqual(PUBLIC_V4, hostvars['interface_ip']) + self.assertEquals(FakeCloud.region_name, hostvars['region']) + self.assertEquals(FakeCloud.name, hostvars['cloud']) + self.assertEquals("test-image-name", hostvars['image']['name']) + self.assertEquals(FakeServer.image['id'], hostvars['image']['id']) + self.assertNotIn('links', hostvars['image']) + self.assertEquals(FakeServer.flavor['id'], hostvars['flavor']['id']) + self.assertEquals("test-flavor-name", hostvars['flavor']['name']) + self.assertNotIn('links', hostvars['flavor']) + # test having volumes + # test volume exception + self.assertEquals([], hostvars['volumes']) + + def test_private_interface_ip(self): + cloud = FakeCloud() + cloud.private = True + hostvars = meta.get_hostvars_from_server(cloud, FakeServer()) + self.assertEqual(PRIVATE_V4, hostvars['interface_ip']) + + def test_image_string(self): + server = FakeServer() + server.image = 'fake-image-id' + hostvars = meta.get_hostvars_from_server(FakeCloud(), server) + self.assertEquals('fake-image-id', hostvars['image']['id']) + + def test_az(self): + server = FakeServer() + server.__dict__['OS-EXT-AZ:availability_zone'] = 'az1' + hostvars = meta.get_hostvars_from_server(FakeCloud(), server) + self.assertEquals('az1', hostvars['az']) + + def test_has_volume(self): + mock_cloud = mock.MagicMock() + mock_volume = mock.MagicMock() + mock_volume.id = 'volume1' + mock_volume.status = 'available' + mock_volume.display_name = 'Volume 1 Display Name' + mock_volume.attachments = [{'device': '/dev/sda0'}] + mock_volume_dict = meta.obj_to_dict(mock_volume) + mock_cloud.get_volumes.return_value = [mock_volume_dict] + hostvars = meta.get_hostvars_from_server(mock_cloud, FakeServer()) + self.assertEquals('volume1', hostvars['volumes'][0]['id']) + self.assertEquals('/dev/sda0', hostvars['volumes'][0]['device']) + + def test_has_no_volume_service(self): + mock_cloud = mock.MagicMock() + + def side_effect(*args): + raise exc.OpenStackCloudException("No Volumes") + mock_cloud.get_volumes.side_effect = side_effect + hostvars = meta.get_hostvars_from_server(mock_cloud, FakeServer()) + self.assertEquals([], hostvars['volumes']) + + def test_unknown_volume_exception(self): + mock_cloud = mock.MagicMock() + + class FakeException(Exception): + pass + + def side_effect(*args): + raise FakeException("No Volumes") + mock_cloud.get_volumes.side_effect = side_effect + self.assertRaises( + FakeException, + meta.get_hostvars_from_server, mock_cloud, FakeServer()) From 4d80851fae66cb18db36b85007f19f9dbaa0aab7 Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Tue, 28 Apr 2015 10:30:25 +0100 Subject: [PATCH 0229/3836] Fix exception re-raise during task execution for py34 This is the code fragment from six (executed when the interpreter is python 3): def reraise(tp, value, tb=None): if value is None: value = tp() if value.__traceback__ is not tb: raise value.with_traceback(tb) raise value We want `raise value` or `raise value.with_traceback(tb)` to be executed as we are invoking six.reraise with a class instance. Change-Id: Ic0c7e14cecb1cbdcb8cae87047e20514c4d38301 --- shade/task_manager.py | 3 +- shade/tests/unit/test_task_manager.py | 42 +++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 shade/tests/unit/test_task_manager.py diff --git a/shade/task_manager.py b/shade/task_manager.py index fca5d3bb1..d4aaf73e2 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -66,7 +66,8 @@ def exception(self, e, tb): def wait(self): self._finished.wait() if self._exception: - six.reraise(self._exception, None, self._traceback) + six.reraise(type(self._exception), self._exception, + self._traceback) return self._result def run(self, client): diff --git a/shade/tests/unit/test_task_manager.py b/shade/tests/unit/test_task_manager.py new file mode 100644 index 000000000..9616ff0ec --- /dev/null +++ b/shade/tests/unit/test_task_manager.py @@ -0,0 +1,42 @@ +# Copyright (c) 2015 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. + + +from shade import task_manager +from shade.tests.unit import base + + +class TestException(Exception): + pass + + +class TestTask(task_manager.Task): + def main(self, client): + raise TestException("This is a test exception") + + +class TestTaskManager(base.TestCase): + + def setUp(self): + super(TestTaskManager, self).setUp() + self.manager = task_manager.TaskManager(name='test', client=self) + + def test_wait_re_raise(self): + """Test that Exceptions thrown in a Task is reraised correctly + + This test is aimed to six.reraise(), called in Task::wait(). + Specifically, we test if we get the same behaviour with all the + configured interpreters (e.g. py27, p34, pypy, ...) + """ + self.assertRaises(TestException, self.manager.submitTask, TestTask()) From a417f3882f79ade8565890619e72c5f5b6670215 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 26 Apr 2015 12:35:39 -0400 Subject: [PATCH 0230/3836] Make warlock filtering match dict filtering For obj_to_dict, we filter out keys that start with _ and values that are not normal values. Do the same for our warlock conversion. Change-Id: Idbf4303c1e4151494d1ea814be6ca5f86e76b16d --- shade/meta.py | 5 +++-- shade/tests/unit/test_meta.py | 28 ++++++++++++++++++++++++++++ test-requirements.txt | 1 + 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/shade/meta.py b/shade/meta.py index 3f2567049..0cc82bcdd 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -186,6 +186,7 @@ def warlock_to_dict(obj): # deep black magic to attribute look up to support validation things that # means we cannot use normal obj_to_dict obj_dict = {} - for key in obj.keys(): - obj_dict[key] = obj[key] + for (key, value) in obj.items(): + if isinstance(value, NON_CALLABLES) and not key.startswith('_'): + obj_dict[key] = value return obj_dict diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 3071f3113..885c8a77d 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -14,6 +14,7 @@ import mock import testtools +import warlock from shade import exc from shade import meta @@ -26,6 +27,7 @@ class FakeCloud(object): region_name = 'test-region' name = 'test-name' private = False + _unused = "useless" def get_flavor_name(self, id): return 'test-flavor-name' @@ -173,3 +175,29 @@ def side_effect(*args): self.assertRaises( FakeException, meta.get_hostvars_from_server, mock_cloud, FakeServer()) + + def test_obj_to_dict(self): + cloud = FakeCloud() + cloud.server = FakeServer() + cloud_dict = meta.obj_to_dict(cloud) + self.assertEqual(FakeCloud.name, cloud_dict['name']) + self.assertNotIn('_unused', cloud_dict) + self.assertNotIn('get_flavor_name', cloud_dict) + self.assertNotIn('server', cloud_dict) + + def test_warlock_to_dict(self): + schema = { + 'name': 'Test', + 'properties': { + 'id': {'type': 'string'}, + 'name': {'type': 'string'}, + '_unused': {'type': 'string'}, + } + } + test_model = warlock.model_factory(schema) + test_obj = test_model( + id='471c2475-da2f-47ac-aba5-cb4aa3d546f5', + name='test-image') + test_dict = meta.warlock_to_dict(test_obj) + self.assertNotIn('_unused', test_dict) + self.assertEqual('test-image', test_dict['name']) diff --git a/test-requirements.txt b/test-requirements.txt index b6a09015e..0efea2b0a 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,3 +10,4 @@ sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 testrepository>=0.0.17 testscenarios>=0.4,<0.5 testtools>=0.9.32 +warlock>=1.0.1,<2 From ace6d92a77066a04c0ff59cce5ad95ee6906d2d1 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 2 May 2015 18:16:40 -0400 Subject: [PATCH 0231/3836] Update vendor support to reflect v2 non-task Vexxhost and RunAbove both support v2 of glance, but do not require the task interface. Update the erroneous information. Also, there are two additional regions for Rackspace that we did not list. We don't list the Rackspace LON region because it does not work like the others and we do not know how to support it. Change-Id: Ib155d00d1d6bf7b2e5bcf4b868c268561e198611 --- doc/source/vendor-support.rst | 6 ++++-- os_client_config/vendors.py | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index b9eb769e1..c75f0122b 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -35,6 +35,8 @@ Region Name Human Name DFW Dallas ORD Chicago IAD Washington, D.C. +SYD Sydney +HKG Hong Kong ============== ================ * Database Service Type is `rax:database` @@ -68,7 +70,7 @@ Region Name Human Name ca-ymq-1 Montreal ============== ================ -* Image API Version is 1 +* Image API Version is 2 * Images must be in `qcow2` format RunAbove @@ -83,5 +85,5 @@ SBG-1 Strassbourg, FR BHS-1 Beauharnois, QC ============== ================ -* Image API Version is 1 +* Image API Version is 2 * Images must be in `qcow2` format diff --git a/os_client_config/vendors.py b/os_client_config/vendors.py index f0a526609..5953729da 100644 --- a/os_client_config/vendors.py +++ b/os_client_config/vendors.py @@ -46,14 +46,14 @@ auth_url='http://auth.api.thenebulacloud.com:5000/v2.0/', ), region_name='ca-ymq-1', - image_api_version='1', + image_api_version='2', image_format='qcow2', ), runabove=dict( auth=dict( auth_url='https://auth.runabove.io/v2.0', ), - image_api_version='1', + image_api_version='2', image_format='qcow2', ), From 08f330d98cd74871599e1ac172aa3ba22f5e4b58 Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Thu, 30 Apr 2015 11:06:41 +0100 Subject: [PATCH 0232/3836] Rename get_endpoint() to get_session_endpoint() In a subsequent change, we will add Keystone endpoint methods. In that change, get_endpoint() would return an endpoint by id, as done by other get_() methods. This change renames get_endpoint() in OpenStackCloud with a more appropriate name, as it returns the current session endpoints. Change-Id: Ie91f97457fb164255d2adf4e84c1b1f729e797b0 --- shade/__init__.py | 23 +++++++++++------------ shade/tests/unit/test_operator_noauth.py | 4 ++-- shade/tests/unit/test_shade.py | 22 ++++++++++++---------- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index fa6542663..ef4c0bf24 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -318,7 +318,7 @@ def nova_client(self): # Make the connection try: # trigger exception on lack of compute. (what?) - self.get_endpoint('compute') + self.get_session_endpoint('compute') self._nova_client = nova_client.Client( self._get_nova_api_version(), session=self.keystone_session, @@ -377,7 +377,7 @@ def keystone_client(self): try: self._keystone_client = keystone_client.Client( session=self.keystone_session, - auth_url=self.get_endpoint('identity'), + auth_url=self.get_session_endpoint('identity'), timeout=self.api_timeout) except Exception as e: self.log.debug( @@ -550,7 +550,7 @@ def _get_glance_api_version(self): return self.api_versions['image'] # Yay. We get to guess ... # Get rid of trailing '/' if present - endpoint = self.get_endpoint('image') + endpoint = self.get_session_endpoint('image') if endpoint.endswith('/'): endpoint = endpoint[:-1] url_bits = endpoint.split('/') @@ -562,7 +562,7 @@ def _get_glance_api_version(self): def glance_client(self): if self._glance_client is None: token = self.auth_token - endpoint = self.get_endpoint('image') + endpoint = self.get_session_endpoint('image') glance_api_version = self._get_glance_api_version() kwargs = dict() if self.api_timeout is not None: @@ -585,7 +585,7 @@ def glance_client(self): def swift_client(self): if self._swift_client is None: token = self.auth_token - endpoint = self.get_endpoint( + endpoint = self.get_session_endpoint( service_key='object-store') self._swift_client = swift_client.Connection( preauthurl=endpoint, @@ -599,7 +599,7 @@ def cinder_client(self): if self._cinder_client is None: # trigger exception on lack of cinder - self.get_endpoint('volume') + self.get_session_endpoint('volume') # Make the connection self._cinder_client = cinder_client.Client( session=self.keystone_session, @@ -629,8 +629,7 @@ def _get_trove_api_version(self, endpoint): @property def trove_client(self): if self._trove_client is None: - endpoint = self.get_endpoint( - service_key='database') + endpoint = self.get_session_endpoint(service_key='database') trove_api_version = self._get_trove_api_version(endpoint) # Make the connection - can't use keystone session until there # is one @@ -653,7 +652,7 @@ def trove_client(self): def neutron_client(self): if self._neutron_client is None: # trigger exception on lack of neutron - self.get_endpoint('network') + self.get_session_endpoint('network') self._neutron_client = neutron_client.Client( token=self.auth_token, session=self.keystone_session, @@ -693,7 +692,7 @@ def get_flavor_by_ram(self, ram, include=None): "Could not find a flavor with {ram} and '{include}'".format( ram=ram, include=include)) - def get_endpoint(self, service_key): + def get_session_endpoint(self, service_key): if service_key in self.endpoints: return self.endpoints[service_key] try: @@ -719,7 +718,7 @@ def get_endpoint(self, service_key): def has_service(self, service_key): try: - self.get_endpoint(service_key) + self.get_session_endpoint(service_key) return True except OpenStackCloudException: return False @@ -2151,7 +2150,7 @@ def ironic_client(self): # dict as the endpoint. endpoint = self.auth['endpoint'] else: - endpoint = self.get_endpoint(service_key='baremetal') + endpoint = self.get_session_endpoint(service_key='baremetal') try: self._ironic_client = ironic_client.Client( '1', endpoint, token=token, diff --git a/shade/tests/unit/test_operator_noauth.py b/shade/tests/unit/test_operator_noauth.py index f32c6696a..4c5cfed70 100644 --- a/shade/tests/unit/test_operator_noauth.py +++ b/shade/tests/unit/test_operator_noauth.py @@ -34,14 +34,14 @@ def setUp(self): auth=dict(endpoint="http://localhost:6385") ) - @mock.patch.object(shade.OperatorCloud, 'get_endpoint') + @mock.patch.object(shade.OperatorCloud, 'get_session_endpoint') @mock.patch.object(ironicclient.client, 'Client') def test_ironic_noauth_selection_using_a_task( self, mock_client, mock_endpoint): """Test noauth selection for Ironic in OperatorCloud Utilize a task to trigger the client connection attempt - and evaluate if get_endpoint was called while the client + and evaluate if get_session_endpoint was called while the client was still called. """ self.cloud_noauth.patch_machine('name', {}) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index b59f99f42..9d041736e 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -678,17 +678,19 @@ class Image(object): self.assertEqual('22', self.cloud.get_image_id('22')) self.assertEqual('22', self.cloud.get_image_id('22 name')) - def test_get_endpoint_provided(self): + def test_get_session_endpoint_provided(self): self.cloud.endpoints['image'] = 'http://fake.url' - self.assertEqual('http://fake.url', self.cloud.get_endpoint('image')) + self.assertEqual( + 'http://fake.url', self.cloud.get_session_endpoint('image')) @mock.patch.object(shade.OpenStackCloud, 'keystone_session') - def test_get_endpoint_session(self, session_mock): + def test_get_session_endpoint_session(self, session_mock): session_mock.get_endpoint.return_value = 'http://fake.url' - self.assertEqual('http://fake.url', self.cloud.get_endpoint('image')) + self.assertEqual( + 'http://fake.url', self.cloud.get_session_endpoint('image')) @mock.patch.object(shade.OpenStackCloud, 'keystone_session') - def test_get_endpoint_exception(self, session_mock): + def test_get_session_endpoint_exception(self, session_mock): class FakeException(Exception): pass @@ -698,19 +700,19 @@ def side_effect(*args, **kwargs): with testtools.ExpectedException( exc.OpenStackCloudException, "Error getting image endpoint: No service"): - self.cloud.get_endpoint("image") + self.cloud.get_session_endpoint("image") @mock.patch.object(shade.OpenStackCloud, 'keystone_session') - def test_get_endpoint_unavailable(self, session_mock): + def test_get_session_endpoint_unavailable(self, session_mock): session_mock.get_endpoint.return_value = None with testtools.ExpectedException( exc.OpenStackCloudUnavailableService, "Cloud.*does not have a image service"): - self.cloud.get_endpoint("image") + self.cloud.get_session_endpoint("image") @mock.patch.object(shade.OpenStackCloud, 'keystone_session') - def test_get_endpoint_identity(self, session_mock): - self.cloud.get_endpoint('identity') + def test_get_session_endpoint_identity(self, session_mock): + self.cloud.get_session_endpoint('identity') session_mock.get_endpoint.assert_called_with( interface=ksc_auth.AUTH_INTERFACE) From 912af15b522423ee8f91d8b8861c4d4a090ef793 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 2 May 2015 19:27:22 -0400 Subject: [PATCH 0233/3836] Remove crufty lines from README Change-Id: Ibf36a67503aab5f3bcd3ba535da5f9f54ce9c28d --- README.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.rst b/README.rst index 34a1b02d8..5c3dcfaf2 100644 --- a/README.rst +++ b/README.rst @@ -169,6 +169,3 @@ Or, get all of the clouds. cloud_config = os_client_config.OpenStackConfig().get_all_clouds() for cloud in cloud_config: print(cloud.name, cloud.region, cloud.config) - -* Free software: Apache license -* Source: http://git.openstack.org/cgit/stackforge/os-client-config From f62cf08348d27b64a7911ce47b5f709642fa2301 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Mon, 27 Apr 2015 15:58:54 -0400 Subject: [PATCH 0234/3836] Enhance error message in update_machine Updated error messages through the use of try/except blocks in update_machine in order to help a user identify what the possible cause of a failure could be without studying the code. Change-Id: I04db479ce1b25aadd710cde5d90fe71fa528869b --- shade/__init__.py | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index fa6542663..44daea612 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2392,12 +2392,15 @@ def update_machine(self, name_or_id, chassis_uuid=None, driver=None, and a list named changes which contains all of the API paths that received updates. """ - try: - machine = self.get_machine(name_or_id) + machine = self.get_machine(name_or_id) + if not machine: + raise OpenStackCloudException( + "Machine update failed to find Machine: %s. " % name_or_id) - machine_config = {} - new_config = {} + machine_config = {} + new_config = {} + try: if chassis_uuid: machine_config['chassis_uuid'] = machine['chassis_uuid'] new_config['chassis_uuid'] = chassis_uuid @@ -2425,9 +2428,31 @@ def update_machine(self, name_or_id, chassis_uuid=None, driver=None, if properties: machine_config['properties'] = machine['properties'] new_config['properties'] = properties + except KeyError as e: + self.log.debug( + "Unexpected machine response missing key %s [%s]" % ( + e.args[0], name_or_id)) + self.log.debug( + "Machine update failed - update value preparation failed. " + "Potential API failure or change has been encountered", + exc_info=True) + raise OpenStackCloudException( + "Machine update failed - machine [%s] missing key %s. " + "Potential API issue." + % (name_or_id, e.args[0])) + try: patch = jsonpatch.JsonPatch.from_diff(machine_config, new_config) + except Exception as e: + self.log.debug( + "Machine update failed - patch object generation failed", + exc_info=True) + raise OpenStackCloudException( + "Machine update failed - Error generating JSON patch object " + "for submission to the API. Machine: %s Error: %s" + % (name_or_id, str(e))) + try: if not patch: return dict( node=machine, @@ -2444,10 +2469,11 @@ def update_machine(self, name_or_id, chassis_uuid=None, driver=None, ) except Exception as e: self.log.debug( - "Machine update failed", exc_info=True) + "Machine update failed - patch operation failed", + exc_info=True) raise OpenStackCloudException( - "Error updating machine node %s. " - "%s" % (name_or_id, e)) + "Machine update failed - patch operation failed Machine: %s " + "Error: %s" % (name_or_id, str(e))) def validate_node(self, uuid): try: From 545d3f0f36b372acd895b0fcba9b3bbad1f5b6bd Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Mon, 27 Apr 2015 16:30:31 -0400 Subject: [PATCH 0235/3836] Update recent Ironic exceptions Update of the recent changes landed for Ironic so that the returned exception messages are converted to a string like the other returned exception messages in Shade. Change-Id: I774b85692ae9adb5fa4202d1c0e3109dcb2fddf4 --- shade/__init__.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 44daea612..a7a95fd4d 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2238,7 +2238,7 @@ def register_machine(self, nics, **kwargs): except Exception as e: self.log.debug("ironic machine registration failed", exc_info=True) raise OpenStackCloudException( - "Error registering machine with Ironic: %s" % e) + "Error registering machine with Ironic: %s" % str(e)) created_nics = [] try: @@ -2263,7 +2263,8 @@ def register_machine(self, nics, **kwargs): self.manager.submitTask( _tasks.MachineDelete(node_id=machine.uuid)) raise OpenStackCloudException( - "Error registering NICs with the baremetal service: %s" % e) + "Error registering NICs with the baremetal service: %s" + % str(e)) return meta.obj_to_dict(machine) def unregister_machine(self, nics, uuid): @@ -2294,7 +2295,7 @@ def unregister_machine(self, nics, uuid): "baremetal NIC unregistration failed", exc_info=True) raise OpenStackCloudException( "Error removing NIC '%s' from baremetal API for " - "node '%s'. Error: %s" % (nic, uuid, e)) + "node '%s'. Error: %s" % (nic, uuid, str(e))) try: self.manager.submitTask( _tasks.MachineDelete(node_id=uuid)) @@ -2304,7 +2305,7 @@ def unregister_machine(self, nics, uuid): "baremetal machine unregistration failed", exc_info=True) raise OpenStackCloudException( "Error unregistering machine %s from the baremetal API. " - "Error: %s" % (uuid, e)) + "Error: %s" % (uuid, str(e))) def patch_machine(self, name_or_id, patch): """Patch Machine Information @@ -2353,7 +2354,7 @@ def patch_machine(self, name_or_id, patch): "Machine patch update failed", exc_info=True) raise OpenStackCloudException( "Error updating machine via patch operation. node: %s. " - "%s" % (name_or_id, e)) + "%s" % (name_or_id, str(e))) def update_machine(self, name_or_id, chassis_uuid=None, driver=None, driver_info=None, name=None, instance_info=None, @@ -2551,7 +2552,7 @@ def set_machine_maintenance_state( exc_info=True) raise OpenStackCloudException( "Error setting machine maintenance on node %s. " - "state: %s" % (name_or_id, e)) + "state: %s" % (name_or_id, str(e))) def remove_machine_from_maintenance(self, name_or_id): """Remove Baremetal Machine from Maintenance State From f2a86ce4fccdb193117d61bcac973acbe4d8a5ca Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Mon, 27 Apr 2015 18:11:32 -0400 Subject: [PATCH 0236/3836] Convert node_set_provision_state to task Changed node_set_provision_state to utilize a task instead of a direct ironicclient library API call. Additionally added basic unit tests for activate_node, deactivate_node, and node_set_provision_state. Change-Id: I036f1e87310e48727ef4e402e475c77cd014e6b1 --- shade/__init__.py | 39 ++++++++++++++++++++++++++-------- shade/_tasks.py | 5 +++++ shade/tests/unit/test_shade.py | 39 ++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 9 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index a7a95fd4d..e69868c19 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2489,18 +2489,40 @@ def validate_node(self, uuid): "ironic node %s failed to validate. " "(deploy: %s, power: %s)" % (ifaces.deploy, ifaces.power)) - def node_set_provision_state(self, uuid, state, configdrive=None): + def node_set_provision_state(self, name_or_id, state, configdrive=None): + """Set Node Provision State + + Enables a user to provision a Machine and optionally define a + config drive to be utilized. + + :param string name_or_id: The Name or UUID value representing the + baremetal node. + :param string state: The desired provision state for the + baremetal node. + :param string configdrive: An optional URL or file or path + representing the configdrive. In the + case of a directory, the client API + will create a properly formatted + configuration drive file and post the + file contents to the API for + deployment. + + :raises: OpenStackCloudException on operation error. + + :returns: Per the API, no value should be returned with a successful + operation. + """ try: return meta.obj_to_dict( - self.ironic_client.node.set_provision_state( - uuid, - state, - configdrive + self.manager.submitTask( + _tasks.MachineSetProvision(node_uuid=name_or_id, + state=state, + configdrive=configdrive)) ) - ) except Exception as e: self.log.debug( - "ironic node failed change provision state to %s" % state, + "Baremetal machine node failed change provision state to %s" + % state, exc_info=True) raise OpenStackCloudException(str(e)) @@ -2659,8 +2681,7 @@ def set_machine_power_reboot(self, name_or_id): self._set_machine_power_state(name_or_id, 'reboot') def activate_node(self, uuid, configdrive=None): - return meta.obj_to_dict( - self.node_set_provision_state(uuid, 'active', configdrive)) + self.node_set_provision_state(uuid, 'active', configdrive) def deactivate_node(self, uuid): self.node_set_provision_state(uuid, 'deleted') diff --git a/shade/_tasks.py b/shade/_tasks.py index d2b21cd07..bb336fc80 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -301,3 +301,8 @@ def main(self, client): class MachineSetPower(task_manager.Task): def main(self, client): return client.ironic_client.node.set_power_state(**self.args) + + +class MachineSetProvision(task_manager.Task): + def main(self, client): + return client.ironic_client.node.set_provision_state(**self.args) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index b59f99f42..75300b43f 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -654,6 +654,45 @@ def test_set_machine_power_reboot_failure(self, mock_client): node_id='node01', state='reboot') + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_node_set_provision_state(self, mock_client): + mock_client.node.set_provision_state.return_value = None + node_id = 'node01' + return_value = self.cloud.node_set_provision_state( + node_id, + 'active', + configdrive='http://127.0.0.1/file.iso') + self.assertEqual({}, return_value) + mock_client.node.set_provision_state.assert_called_with( + node_uuid='node01', + state='active', + configdrive='http://127.0.0.1/file.iso') + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_activate_node(self, mock_client): + mock_client.node.set_provision_state.return_value = None + node_id = 'node02' + return_value = self.cloud.activate_node( + node_id, + configdrive='http://127.0.0.1/file.iso') + self.assertEqual(None, return_value) + mock_client.node.set_provision_state.assert_called_with( + node_uuid='node02', + state='active', + configdrive='http://127.0.0.1/file.iso') + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_deactivate_node(self, mock_client): + mock_client.node.set_provision_state.return_value = None + node_id = 'node03' + return_value = self.cloud.deactivate_node( + node_id) + self.assertEqual(None, return_value) + mock_client.node.set_provision_state.assert_called_with( + node_uuid='node03', + state='deleted', + configdrive=None) + @mock.patch.object(shade.OpenStackCloud, 'glance_client') def test_get_image_name(self, glance_mock): From f495ebd73f45d8782198484368472ff00d35728a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 3 May 2015 08:40:48 -0400 Subject: [PATCH 0237/3836] Enhance the OperatorCloud constructor Set the default for endpoint_type to 'admin' and expand the docstring a smidge. Change-Id: Iab8bafcc497bf5d4a221581158371712db5b736b --- shade/__init__.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 78619cfdb..f0c935063 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2124,8 +2124,25 @@ def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, class OperatorCloud(OpenStackCloud): """Represent a privileged/operator connection to an OpenStack Cloud. + + `OperatorCloud` is the entry point for all admin operations, regardless + of which OpenStack service those operations are for. + + See the :class:`OpenStackCloud` class for a description of most options. + `OperatorCloud` overrides the default value of `endpoint_type` from + `public` to `admin`. + + :param string endpoint_type: The type of endpoint to get for services + from the service catalog. Valid types are + `public` ,`internal` or `admin`. (optional, + defaults to `admin`) """ + def __init__(self, *args, **kwargs): + super(OperatorCloud, self).__init__(*args, **kwargs) + if 'endpoint_type' not in kwargs: + self.endpoint_type = 'admin' + @property def auth_token(self): if self.auth_type in (None, "None", ''): From 760d996829979e4d2ba5c9d389961e6e375f15e9 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 30 Apr 2015 02:45:28 +0200 Subject: [PATCH 0238/3836] Switch tasks vs put on a boolean config flag Rackspace's requirement for task-create is a choice that is orthogonal to the v2 choice. Make that decision point clear. Also, rename the hidden methods just so that we're clear on what's happening. Update to a more recent version of os-client-config, since that also has support for the task-create flag. Depends-On: I2cdcfe302a73ebfa7f739399c1eeb3bc9f96ab3c Change-Id: I72bb5992eeb116f1338a090d997893b9ae93050c --- requirements.txt | 2 +- shade/__init__.py | 19 ++++++++++++------- shade/tests/unit/test_caching.py | 5 +++-- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/requirements.txt b/requirements.txt index cf25ff199..ec2cb7793 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ pbr>=0.5.21,<1.0 jsonpatch -os-client-config>=0.7.0 +os-client-config>=0.8.1 six python-novaclient>=2.21.0 diff --git a/shade/__init__.py b/shade/__init__.py index da9b2b6d3..52b9e231e 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -240,6 +240,10 @@ class OpenStackCloud(object): OpenStack API tasks. Unless you're doing rate limiting client side, you almost certainly don't need this. (optional) + :param bool image_api_use_tasks: Whether or not this cloud needs to + use the glance task-create interface for + image upload activities instead of direct + calls. (optional, defaults to False) """ def __init__(self, cloud, auth, @@ -253,6 +257,7 @@ def __init__(self, cloud, auth, cache_class='dogpile.cache.null', cache_arguments=None, manager=None, + image_api_use_tasks=False, **kwargs): self.name = cloud @@ -272,6 +277,7 @@ def __init__(self, cloud, auth, self.service_names = _get_service_values(kwargs, 'service_name') self.endpoints = _get_service_values(kwargs, 'endpoint') self.api_versions = _get_service_values(kwargs, 'api_version') + self.image_api_use_tasks = image_api_use_tasks (self.verify, self.cert) = _ssl_args(verify, cacert, cert, key) @@ -1128,22 +1134,21 @@ def create_image( kwargs[IMAGE_MD5_KEY] = md5 kwargs[IMAGE_SHA256_KEY] = sha256 # This makes me want to die inside - glance_api_version = self._get_glance_api_version() - if glance_api_version == '2': - return self._upload_image_v2( + if self.image_api_use_tasks: + return self._upload_image_task( name, filename, container, current_image=current_image, wait=wait, timeout=timeout, **kwargs) - elif glance_api_version == '1': + else: image_kwargs = dict(properties=kwargs) if disk_format: image_kwargs['disk_format'] = disk_format if container_format: image_kwargs['container_format'] = container_format - return self._upload_image_v1(name, filename, **image_kwargs) + return self._upload_image_put(name, filename, **image_kwargs) - def _upload_image_v1(self, name, filename, **image_kwargs): + def _upload_image_put(self, name, filename, **image_kwargs): image = self.manager.submitTask(_tasks.ImageCreate( name=name, **image_kwargs)) self.manager.submitTask(_tasks.ImageUpdate( @@ -1151,7 +1156,7 @@ def _upload_image_v1(self, name, filename, **image_kwargs): self._cache.invalidate() return self.get_image_dict(image.id) - def _upload_image_v2( + def _upload_image_task( self, name, filename, container, current_image=None, wait=True, timeout=None, **image_properties): self.create_object( diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index c88a367a5..ee70a908e 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -302,7 +302,7 @@ def _call_create_image(self, name, container=None): name, imagefile.name, container=container, wait=True) @mock.patch.object(shade.OpenStackCloud, 'glance_client') - def test_create_image_v1(self, glance_mock): + def test_create_image_put(self, glance_mock): self.cloud.api_versions['image'] = '1' glance_mock.images.list.return_value = [] self.assertEqual({}, self.cloud.list_images()) @@ -325,8 +325,9 @@ class Image(object): @mock.patch.object(shade.OpenStackCloud, 'glance_client') @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_create_image_v2(self, swift_mock, glance_mock): + def test_create_image_task(self, swift_mock, glance_mock): self.cloud.api_versions['image'] = '2' + self.cloud.image_api_use_tasks = True class Container(object): name = 'image_upload_v2_test_container' From 2e3498d76a09c7d3873944609e09d34c0a347b02 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 5 May 2015 14:07:12 -0400 Subject: [PATCH 0239/3836] Return Bunch objects instead of plain dicts Bunch objects are dot-accessible dictionaries, so we can say server.name or server['name'] equally. Change-Id: I792c9792116824c4f8f840f961eaddc0b2060e6f --- requirements.txt | 1 + shade/meta.py | 5 +++-- shade/tests/unit/test_meta.py | 4 ++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index ec2cb7793..7cca7eac9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ pbr>=0.5.21,<1.0 +bunch jsonpatch os-client-config>=0.8.1 six diff --git a/shade/meta.py b/shade/meta.py index 0cc82bcdd..c794df6e4 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -13,6 +13,7 @@ # limitations under the License. +import bunch import six from shade import exc @@ -160,7 +161,7 @@ def obj_to_dict(obj): that we can just have a plain dict of all of the values that exist in the nova metadata for a server. """ - instance = {} + instance = bunch.Bunch() for key in dir(obj): value = getattr(obj, key) if isinstance(value, NON_CALLABLES) and not key.startswith('_'): @@ -185,7 +186,7 @@ def warlock_to_dict(obj): # glanceclient v2 uses warlock to construct its objects. Warlock does # deep black magic to attribute look up to support validation things that # means we cannot use normal obj_to_dict - obj_dict = {} + obj_dict = bunch.Bunch() for (key, value) in obj.items(): if isinstance(value, NON_CALLABLES) and not key.startswith('_'): obj_dict[key] = value diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 885c8a77d..a36d9929f 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -184,6 +184,8 @@ def test_obj_to_dict(self): self.assertNotIn('_unused', cloud_dict) self.assertNotIn('get_flavor_name', cloud_dict) self.assertNotIn('server', cloud_dict) + self.assertTrue(hasattr(cloud_dict, 'name')) + self.assertEquals(cloud_dict.name, cloud_dict['name']) def test_warlock_to_dict(self): schema = { @@ -201,3 +203,5 @@ def test_warlock_to_dict(self): test_dict = meta.warlock_to_dict(test_obj) self.assertNotIn('_unused', test_dict) self.assertEqual('test-image', test_dict['name']) + self.assertTrue(hasattr(test_dict, 'name')) + self.assertEquals(test_dict.name, test_dict['name']) From cecbc2a4cf51064c462dd787f30bcc3a17e49769 Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Wed, 6 May 2015 10:24:23 -0700 Subject: [PATCH 0240/3836] Fix delete_server when wait=True Functional tests were running with wait=False, which hid that we are not handling the expected NotFound error after delete. So we refactor delete_server to use get_server and be much simpler. Note that we call the ServerGet task directly since we already have the ID that we want to make sure we deleted, no need for listing at that point. Adding unit test coverage to help avoid these problems in the future as well. Change-Id: Ib78aef75a5ad3f75bc8ee2b81078d30cc2fc76db --- shade/__init__.py | 21 ++++--- shade/tests/functional/test_compute.py | 3 +- shade/tests/unit/test_delete_server.py | 84 ++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 11 deletions(-) create mode 100644 shade/tests/unit/test_delete_server.py diff --git a/shade/__init__.py b/shade/__init__.py index a225ae475..6c3d19bee 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1731,21 +1731,22 @@ def rebuild_server(self, server_id, image_id, wait=False, timeout=180): return server def delete_server(self, name, wait=False, timeout=180): - # TODO(mordred): Why is this not using self.get_server()? - server_list = self.manager.submitTask(_tasks.ServerList( - detailed=True, search_opts={'name': name})) - # TODO(mordred): Why, after searching for a name, are we filtering - # again? - if server_list: - server = [x for x in server_list if x.name == name] - self.manager.submitTask(_tasks.ServerDelete(server=server.pop())) + server = self.get_server(name) + if server: + self.manager.submitTask(_tasks.ServerDelete(server=server)) + else: + return if not wait: return for count in _iterate_timeout( timeout, "Timed out waiting for server to get deleted."): - server = self.manager.submitTask(_tasks.ServerGet(server=server)) - if not server: + try: + server = self.manager.submitTask( + _tasks.ServerGet(server=server)) + if not server: + return + except nova_exceptions.NotFound: return def get_container(self, name, skip_cache=False): diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index 7053bed73..a2edc8815 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -55,7 +55,8 @@ def test_create_server(self): def test_delete_server(self): self.cloud.create_server(name='test_delete_server', image=self.image, flavor=self.flavor) - server_deleted = self.cloud.delete_server('test_delete_server') + server_deleted = self.cloud.delete_server('test_delete_server', + wait=True) self.assertIsNone(server_deleted) def test_get_image_id(self): diff --git a/shade/tests/unit/test_delete_server.py b/shade/tests/unit/test_delete_server.py new file mode 100644 index 000000000..6259d507d --- /dev/null +++ b/shade/tests/unit/test_delete_server.py @@ -0,0 +1,84 @@ +# -*- 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. + +""" +test_delete_server +---------------------------------- + +Tests for the `delete_server` command. +""" + +import mock +from novaclient import exceptions as nova_exc + +from shade import OpenStackCloud +from shade.tests.unit import base + + +class TestDeleteServer(base.TestCase): + + def setUp(self): + super(TestDeleteServer, self).setUp() + self.cloud = OpenStackCloud("cloud", {}) + + @mock.patch('shade.OpenStackCloud.nova_client') + def test_delete_server(self, nova_mock): + """ + Test that novaclient server delete is called when wait=False + """ + server = mock.MagicMock(id='1234', + status='ACTIVE') + server.name = 'daffy' + nova_mock.servers.list.return_value = [server] + self.cloud.delete_server('daffy', wait=False) + nova_mock.servers.delete.assert_called_with(server=server) + + @mock.patch('shade.OpenStackCloud.nova_client') + def test_delete_server_already_gone(self, nova_mock): + """ + Test that we return immediately when server is already gone + """ + nova_mock.servers.list.return_value = [] + self.cloud.delete_server('tweety', wait=False) + self.assertFalse(nova_mock.servers.delete.called) + + @mock.patch('shade.OpenStackCloud.nova_client') + def test_delete_server_already_gone_wait(self, nova_mock): + self.cloud.delete_server('speedy', wait=True) + self.assertFalse(nova_mock.servers.delete.called) + + @mock.patch('shade.OpenStackCloud.nova_client') + def test_delete_server_wait_for_notfound(self, nova_mock): + """ + Test that delete_server waits for NotFound from novaclient + """ + server = mock.MagicMock(id='9999', + status='ACTIVE') + server.name = 'wily' + nova_mock.servers.list.return_value = [server] + + def _delete_wily(*args, **kwargs): + self.assertIn('server', kwargs) + self.assertEqual('9999', kwargs['server'].id) + nova_mock.servers.list.return_value = [] + + def _raise_notfound(*args, **kwargs): + self.assertIn('server', kwargs) + self.assertEqual('9999', kwargs['server'].id) + raise nova_exc.NotFound(code='404') + nova_mock.servers.get.side_effect = _raise_notfound + + nova_mock.servers.delete.side_effect = _delete_wily + self.cloud.delete_server('wily', wait=True) + nova_mock.servers.delete.assert_called_with(server=server) From dc8d21db11da1c80ad13f5626bbea63fdd649628 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 5 May 2015 14:27:40 -0600 Subject: [PATCH 0241/3836] Also accept .yml as a suffix The ansible world uses .yml suffixes, which brings people to try to use that suffix on clouds.yaml files. It's easy enough to accept it as a possibility. Change-Id: Iba05eab9cf4406833afafe8143794461b656b548 --- os_client_config/config.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 8d687ef61..f2ddd48b2 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -31,15 +31,22 @@ os.environ.get('XDG_CONFIG_HOME', os.path.join('~', '.config'))), 'openstack') CONFIG_SEARCH_PATH = [os.getcwd(), CONFIG_HOME, '/etc/openstack'] +YAML_SUFFIXES = ('.yaml', '.yml') CONFIG_FILES = [ - os.path.join(d, 'clouds.yaml') for d in CONFIG_SEARCH_PATH] + os.path.join(d, 'clouds' + s) + for d in CONFIG_SEARCH_PATH + for s in YAML_SUFFIXES +] CACHE_PATH = os.path.join(os.path.expanduser( os.environ.get('XDG_CACHE_PATH', os.path.join('~', '.cache'))), 'openstack') BOOL_KEYS = ('insecure', 'cache') VENDOR_SEARCH_PATH = [os.getcwd(), CONFIG_HOME, '/etc/openstack'] VENDOR_FILES = [ - os.path.join(d, 'clouds-public.yaml') for d in VENDOR_SEARCH_PATH] + os.path.join(d, 'clouds-public' + s) + for d in VENDOR_SEARCH_PATH + for s in YAML_SUFFIXES +] def set_default(key, value): @@ -126,14 +133,13 @@ def __init__(self, config_files=None, vendor_files=None): 'arguments', self._cache_arguments) def _load_config_file(self): - for path in self._config_files: - if os.path.exists(path): - with open(path, 'r') as f: - return yaml.safe_load(f) - return dict(clouds=dict()) + return self._load_yaml_file(self._config_files) def _load_vendor_file(self): - for path in self._vendor_files: + return self._load_yaml_file(self._vendor_files) + + def _load_yaml_file(self, filelist): + for path in filelist: if os.path.exists(path): with open(path, 'r') as f: return yaml.safe_load(f) From f6d08765cd8ceb071e99453d3f17faabb5878431 Mon Sep 17 00:00:00 2001 From: Gregory Haynes Date: Thu, 7 May 2015 17:58:54 +0000 Subject: [PATCH 0242/3836] get_one_cloud should use auth merge Currently, get_one_cloud does not merge the auth dictionary, which means passing any auth value overrides all auth: values in clouds.yaml. Change-Id: I22c33648e32cc7ce8fc163433b7c72912c28beb9 --- os_client_config/config.py | 5 ++++- os_client_config/tests/test_config.py | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 8d687ef61..b05fc44f3 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -357,7 +357,10 @@ def get_one_cloud(self, cloud=None, validate=True, # Can't just do update, because None values take over for (key, val) in iter(args.items()): if val is not None: - config[key] = val + if key == 'auth' and config[key] is not None: + config[key] = _auth_update(config[key], val) + else: + config[key] = val for key in BOOL_KEYS: if key in config: diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index c537842b7..274b188c1 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -43,3 +43,9 @@ def test_no_environ(self): vendor_files=[self.vendor_yaml]) self.assertRaises( exceptions.OpenStackConfigException, c.get_one_cloud, 'envvars') + + def test_get_one_cloud_auth_merge(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml]) + cc = c.get_one_cloud(cloud='_test_cloud_', auth={'username': 'user'}) + self.assertEqual('user', cc.auth['username']) + self.assertEqual('testpass', cc.auth['password']) From 33634dda7219f334c5545cb93673311c8bc99130 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 7 May 2015 18:50:28 -0400 Subject: [PATCH 0243/3836] Add flag to indicate where floating ips come from One of the things that's not possible to discover is where floating ips come from. You'd think that looking for a neutron endpoint would do it, but you'd be oh-so-wrong. Change-Id: I10a0c6f37afb409af0078cede3eac2eaa0ff4f04 --- doc/source/vendor-support.rst | 5 +++++ os_client_config/defaults.py | 2 ++ os_client_config/vendors.py | 3 +++ 3 files changed, 10 insertions(+) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index c75f0122b..1d5aff35f 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -23,6 +23,7 @@ region-b.geo-1 US East * DNS Service Type is `hpext:dns` * Image API Version is 1 * Images must be in `qcow2` format +* Floating IPs are provided by Neutron Rackspace --------- @@ -44,6 +45,7 @@ HKG Hong Kong * Image API Version is 2 * Images must be in `vhd` format * Images must be uploaded using the Glance Task Interface +* Floating IPs are not needed Dreamhost --------- @@ -58,6 +60,7 @@ RegionOne Region One * Image API Version is 2 * Images must be in `raw` format +* Floating IPs are provided by Neutron Vexxhost -------- @@ -72,6 +75,7 @@ ca-ymq-1 Montreal * Image API Version is 2 * Images must be in `qcow2` format +* Floating IPs are not needed RunAbove -------- @@ -87,3 +91,4 @@ BHS-1 Beauharnois, QC * Image API Version is 2 * Images must be in `qcow2` format +* Floating IPs are not needed diff --git a/os_client_config/defaults.py b/os_client_config/defaults.py index 5f70d6ee4..905950201 100644 --- a/os_client_config/defaults.py +++ b/os_client_config/defaults.py @@ -16,8 +16,10 @@ auth_type='password', compute_api_version='2', identity_api_version='2', + image_api_use_tasks=False, image_api_version='1', network_api_version='2', object_api_version='1', volume_api_version='1', + floating_ip_source='neutron', ) diff --git a/os_client_config/vendors.py b/os_client_config/vendors.py index 5953729da..2eecfa38e 100644 --- a/os_client_config/vendors.py +++ b/os_client_config/vendors.py @@ -32,6 +32,7 @@ image_api_version='2', image_api_use_tasks=True, image_format='vhd', + floating_ip_source=None, ), dreamhost=dict( auth=dict( @@ -48,6 +49,7 @@ region_name='ca-ymq-1', image_api_version='2', image_format='qcow2', + floating_ip_source=None, ), runabove=dict( auth=dict( @@ -55,6 +57,7 @@ ), image_api_version='2', image_format='qcow2', + floating_ip_source=None, ), ) From 731cfab0e28fb0e1090708fd0244d13d376c3018 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 5 May 2015 15:49:28 -0400 Subject: [PATCH 0244/3836] Update server API for get/list/search interface Support the new search interface, and transform the server objects to Bunch objects. Change-Id: Ic8ea3e79863e11b4a00f104557966c81e2f06388 --- shade/__init__.py | 30 ++++++++++++++++---------- shade/meta.py | 2 +- shade/tests/unit/test_delete_server.py | 8 +++---- shade/tests/unit/test_meta.py | 22 +++++++++++++------ shade/tests/unit/test_shade.py | 20 +++++++++++++++++ 5 files changed, 59 insertions(+), 23 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 6c3d19bee..532cf7271 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -729,9 +729,6 @@ def has_service(self, service_key): except OpenStackCloudException: return False - def list_servers(self): - return self.manager.submitTask(_tasks.ServerList()) - def list_server_dicts(self): return [self.get_openstack_vars(server) for server in self.list_servers()] @@ -869,6 +866,10 @@ def search_security_groups(self, name_or_id=None, filters=None): groups = self.list_security_groups() return self._filter_list(groups, name_or_id, filters) + def search_servers(self, name_or_id=None, filters=None): + servers = self.list_servers() + return self._filter_list(servers, name_or_id, filters) + def list_networks(self): return self.manager.submitTask(_tasks.NetworkList())['networks'] @@ -905,6 +906,16 @@ def list_security_groups(self): self.manager.submitTask(_tasks.SecurityGroupList()) ) + def list_servers(self): + try: + return meta.obj_list_to_dict( + self.manager.submitTask(_tasks.ServerList()) + ) + except Exception as e: + self.log.debug("server list failed: %s" % e, exc_info=True) + raise OpenStackCloudException( + "Error fetching server list: %s" % e) + def get_network(self, name_or_id, filters=None): return self._get_entity(self.search_networks, name_or_id, filters) @@ -924,6 +935,9 @@ def get_security_group(self, name_or_id, filters=None): return self._get_entity(self.search_security_groups, name_or_id, filters) + def get_server(self, name_or_id, filters=None): + return self._get_entity(self.search_servers, name_or_id, filters) + # TODO(Shrews): This will eventually need to support tenant ID and # provider networks, which are admin-level params. def create_network(self, name, shared=False, admin_state_up=True): @@ -1537,12 +1551,6 @@ def get_server_private_ip(self, server): def get_server_public_ip(self, server): return meta.get_server_public_ip(server) - def get_server(self, name_or_id): - for server in self.list_servers(): - if name_or_id in (server.name, server.id): - return server - return None - def get_server_dict(self, name_or_id): server = self.get_server(name_or_id) if not server: @@ -1733,7 +1741,7 @@ def rebuild_server(self, server_id, image_id, wait=False, timeout=180): def delete_server(self, name, wait=False, timeout=180): server = self.get_server(name) if server: - self.manager.submitTask(_tasks.ServerDelete(server=server)) + self.manager.submitTask(_tasks.ServerDelete(server=server.id)) else: return if not wait: @@ -1743,7 +1751,7 @@ def delete_server(self, name, wait=False, timeout=180): "Timed out waiting for server to get deleted."): try: server = self.manager.submitTask( - _tasks.ServerGet(server=server)) + _tasks.ServerGet(server=server.id)) if not server: return except nova_exceptions.NotFound: diff --git a/shade/meta.py b/shade/meta.py index c794df6e4..6f5718b74 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -95,7 +95,7 @@ def get_groups_from_server(cloud, server, server_vars): def get_hostvars_from_server(cloud, server, mounts=None): - server_vars = obj_to_dict(server) + server_vars = server server_vars.pop('links', None) # Fist, add an IP address diff --git a/shade/tests/unit/test_delete_server.py b/shade/tests/unit/test_delete_server.py index 6259d507d..1e8157722 100644 --- a/shade/tests/unit/test_delete_server.py +++ b/shade/tests/unit/test_delete_server.py @@ -42,7 +42,7 @@ def test_delete_server(self, nova_mock): server.name = 'daffy' nova_mock.servers.list.return_value = [server] self.cloud.delete_server('daffy', wait=False) - nova_mock.servers.delete.assert_called_with(server=server) + nova_mock.servers.delete.assert_called_with(server=server.id) @mock.patch('shade.OpenStackCloud.nova_client') def test_delete_server_already_gone(self, nova_mock): @@ -70,15 +70,15 @@ def test_delete_server_wait_for_notfound(self, nova_mock): def _delete_wily(*args, **kwargs): self.assertIn('server', kwargs) - self.assertEqual('9999', kwargs['server'].id) + self.assertEqual('9999', kwargs['server']) nova_mock.servers.list.return_value = [] def _raise_notfound(*args, **kwargs): self.assertIn('server', kwargs) - self.assertEqual('9999', kwargs['server'].id) + self.assertEqual('9999', kwargs['server']) raise nova_exc.NotFound(code='404') nova_mock.servers.get.side_effect = _raise_notfound nova_mock.servers.delete.side_effect = _delete_wily self.cloud.delete_server('wily', wait=True) - nova_mock.servers.delete.assert_called_with(server=server) + nova_mock.servers.delete.assert_called_with(server=server.id) diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index a36d9929f..da3076892 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -106,7 +106,8 @@ class obj1(object): self.assertEqual(new_list[1]['value'], 1) def test_basic_hostvars(self): - hostvars = meta.get_hostvars_from_server(FakeCloud(), FakeServer()) + hostvars = meta.get_hostvars_from_server( + FakeCloud(), meta.obj_to_dict(FakeServer())) self.assertNotIn('links', hostvars) self.assertEqual(PRIVATE_V4, hostvars['private_v4']) self.assertEqual(PUBLIC_V4, hostvars['public_v4']) @@ -126,19 +127,22 @@ def test_basic_hostvars(self): def test_private_interface_ip(self): cloud = FakeCloud() cloud.private = True - hostvars = meta.get_hostvars_from_server(cloud, FakeServer()) + hostvars = meta.get_hostvars_from_server( + cloud, meta.obj_to_dict(FakeServer())) self.assertEqual(PRIVATE_V4, hostvars['interface_ip']) def test_image_string(self): server = FakeServer() server.image = 'fake-image-id' - hostvars = meta.get_hostvars_from_server(FakeCloud(), server) + hostvars = meta.get_hostvars_from_server( + FakeCloud(), meta.obj_to_dict(server)) self.assertEquals('fake-image-id', hostvars['image']['id']) def test_az(self): server = FakeServer() server.__dict__['OS-EXT-AZ:availability_zone'] = 'az1' - hostvars = meta.get_hostvars_from_server(FakeCloud(), server) + hostvars = meta.get_hostvars_from_server( + FakeCloud(), meta.obj_to_dict(server)) self.assertEquals('az1', hostvars['az']) def test_has_volume(self): @@ -150,7 +154,8 @@ def test_has_volume(self): mock_volume.attachments = [{'device': '/dev/sda0'}] mock_volume_dict = meta.obj_to_dict(mock_volume) mock_cloud.get_volumes.return_value = [mock_volume_dict] - hostvars = meta.get_hostvars_from_server(mock_cloud, FakeServer()) + hostvars = meta.get_hostvars_from_server( + mock_cloud, meta.obj_to_dict(FakeServer())) self.assertEquals('volume1', hostvars['volumes'][0]['id']) self.assertEquals('/dev/sda0', hostvars['volumes'][0]['device']) @@ -160,7 +165,8 @@ def test_has_no_volume_service(self): def side_effect(*args): raise exc.OpenStackCloudException("No Volumes") mock_cloud.get_volumes.side_effect = side_effect - hostvars = meta.get_hostvars_from_server(mock_cloud, FakeServer()) + hostvars = meta.get_hostvars_from_server( + mock_cloud, meta.obj_to_dict(FakeServer())) self.assertEquals([], hostvars['volumes']) def test_unknown_volume_exception(self): @@ -174,7 +180,9 @@ def side_effect(*args): mock_cloud.get_volumes.side_effect = side_effect self.assertRaises( FakeException, - meta.get_hostvars_from_server, mock_cloud, FakeServer()) + meta.get_hostvars_from_server, + mock_cloud, + meta.obj_to_dict(FakeServer())) def test_obj_to_dict(self): cloud = FakeCloud() diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 268e38de7..0109d071b 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -72,6 +72,26 @@ def test__filter_list_dict2(self): }}) self.assertEquals([el2, el3], ret) + @mock.patch.object(shade.OpenStackCloud, 'search_servers') + def test_get_server(self, mock_search): + server1 = dict(id='123', name='mickey') + mock_search.return_value = [server1] + r = self.cloud.get_server('mickey') + self.assertIsNotNone(r) + self.assertDictEqual(server1, r) + + @mock.patch.object(shade.OpenStackCloud, 'search_servers') + def test_get_server_not_found(self, mock_search): + mock_search.return_value = [] + r = self.cloud.get_server('doesNotExist') + self.assertIsNone(r) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_list_servers_exception(self, mock_client): + mock_client.servers.list.side_effect = Exception() + self.assertRaises(exc.OpenStackCloudException, + self.cloud.list_servers) + @mock.patch.object(shade.OpenStackCloud, 'search_subnets') def test_get_subnet(self, mock_search): subnet = dict(id='123', name='mickey') From 1981431c08f4c9fa0d206157baf55eccca17c1be Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Mon, 11 May 2015 16:24:16 +0100 Subject: [PATCH 0245/3836] Rewrite extension checking methods Old implementation didn't work, was using ad-hoc caching and its name was too generic. Moreover it exposed 2 methods that should be private as Shade aims to hide implementation details. This change-set gets rid of extension_cache() and has_extension() methods and create _nova_extensions() and _has_nova_extension() methods. Caching is performed using _cache_on_arguments decorator. Change-Id: I6aa3ed5a306159ba477d15810138b3a4fb5a3649 --- shade/__init__.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 6c3d19bee..e77516ef0 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -750,23 +750,27 @@ def create_keypair(self, name, public_key): def delete_keypair(self, name): return self.manager.submitTask(_tasks.KeypairDelete(key=name)) - @property - def extension_cache(self): - if not self._extension_cache: - self._extension_cache = set() + @_cache_on_arguments() + def _nova_extensions(self): + extensions = set() - try: - resp, body = self.manager.submitTask( - _tasks.NovaUrlGet(url='/extensions')) - if resp.status_code == 200: - for x in body['extensions']: - self._extension_cache.add(x['alias']) - except nova_exceptions.NotFound: - pass - return self._extension_cache + try: + resp, body = self.manager.submitTask( + _tasks.NovaUrlGet(url='/extensions')) + for x in body['extensions']: + extensions.add(x['alias']) + except Exception as e: + self.log.debug( + "nova could not list extensions: {msg}".format( + msg=str(e)), exc_info=True) + raise OpenStackCloudException( + "error fetching extension list for nova: {msg}".format( + msg=str(e))) + + return extensions - def has_extension(self, extension_name): - return extension_name in self.extension_cache + def _has_nova_extension(self, extension_name): + return extension_name in self._nova_extensions() def _filter_list(self, data, name_or_id, filters): """Filter a list by name/ID and arbitrary meta data. From 78df129bd21a1c0cdc9e3b61c9b6d508f29f26ae Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 6 May 2015 12:03:42 -0400 Subject: [PATCH 0246/3836] Update images API for get/list/search interface Images API now conforms to the new search interface. This required us to no longer return a dict of images keyed by image ID, but, rather, just a list if images. The 'exclude' argument to get_image() was not being used by either nodepool or the Ansible modules, so this was simply removed to make that method conform to the standard interface. However, it is still used by the Ansible modules in get_image_id(), so that functionality is kept intact. Change-Id: Icf825fcb0471de4acb08b59b93a4dfcd399b4c69 --- shade/__init__.py | 116 ++++++++++++++++--------------- shade/meta.py | 7 ++ shade/tests/unit/test_caching.py | 31 +++++---- shade/tests/unit/test_shade.py | 14 ++++ 4 files changed, 99 insertions(+), 69 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 532cf7271..30a0b7acf 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -167,7 +167,7 @@ def _no_pending_volumes(volumes): def _no_pending_images(images): '''If there are any images not in a steady state, don't cache''' - for image_id, image in iter(images.items()): + for image in images: if image.status not in ('active', 'deleted', 'killed'): return False return True @@ -870,6 +870,10 @@ def search_servers(self, name_or_id=None, filters=None): servers = self.list_servers() return self._filter_list(servers, name_or_id, filters) + def search_images(self, name_or_id=None, filters=None): + images = self.list_images() + return self._filter_list(images, name_or_id, filters) + def list_networks(self): return self.manager.submitTask(_tasks.NetworkList())['networks'] @@ -916,6 +920,46 @@ def list_servers(self): raise OpenStackCloudException( "Error fetching server list: %s" % e) + @_cache_on_arguments(should_cache_fn=_no_pending_images) + def list_images(self, filter_deleted=True): + """Get available glance images. + + :param filter_deleted: Control whether deleted images are returned. + :returns: A list of glance images. + """ + # First, try to actually get images from glance, it's more efficient + images = [] + try: + # If the cloud does not expose the glance API publically + image_gen = self.manager.submitTask(_tasks.GlanceImageList()) + + # Deal with the generator to make a list + image_list = [image for image in image_gen] + + if image_list: + if getattr(image_list[0], 'validate', None): + # glanceclient returns a "warlock" object if you use v2 + image_list = meta.warlock_list_to_dict(image_list) + else: + # glanceclient returns a normal object if you use v1 + image_list = meta.obj_list_to_dict(image_list) + except (OpenStackCloudException, + glanceclient.exc.HTTPInternalServerError): + # We didn't have glance, let's try nova + # If this doesn't work - we just let the exception propagate + image_list = meta.obj_list_to_dict( + self.manager.submitTask(_tasks.NovaImageList()) + ) + + for image in image_list: + # The cloud might return DELETED for invalid images. + # While that's cute and all, that's an implementation detail. + if not filter_deleted: + images.append(image) + elif image.status != 'DELETED': + images.append(image) + return images + def get_network(self, name_or_id, filters=None): return self._get_entity(self.search_networks, name_or_id, filters) @@ -938,6 +982,9 @@ def get_security_group(self, name_or_id, filters=None): def get_server(self, name_or_id, filters=None): return self._get_entity(self.search_servers, name_or_id, filters) + def get_image(self, name_or_id, filters=None): + return self._get_entity(self.search_images, name_or_id, filters) + # TODO(Shrews): This will eventually need to support tenant ID and # provider networks, which are admin-level params. def create_network(self, name, shared=False, admin_state_up=True): @@ -1082,71 +1129,30 @@ def delete_router(self, name_or_id): raise OpenStackCloudException( "Error deleting router %s: %s" % (name_or_id, e)) - def _get_images_from_cloud(self, filter_deleted): - # First, try to actually get images from glance, it's more efficient - images = dict() - try: - # If the cloud does not expose the glance API publically - image_list = self.manager.submitTask(_tasks.GlanceImageList()) - except (OpenStackCloudException, - glanceclient.exc.HTTPInternalServerError): - # We didn't have glance, let's try nova - # If this doesn't work - we just let the exception propagate - image_list = self.manager.submitTask(_tasks.NovaImageList()) - for image in image_list: - # The cloud might return DELETED for invalid images. - # While that's cute and all, that's an implementation detail. - if not filter_deleted: - images[image.id] = image - elif image.status != 'DELETED': - images[image.id] = image - return images - def _reset_image_cache(self): self._image_cache = None - @_cache_on_arguments(should_cache_fn=_no_pending_images) - def list_images(self, filter_deleted=True): - """Get available glance images. - - :param filter_deleted: Control whether deleted images are returned. - :returns: A dictionary of glance images indexed by image UUID. - """ - return self._get_images_from_cloud(filter_deleted) + def get_image_exclude(self, name_or_id, exclude): + for image in self.search_images(name_or_id): + if exclude: + if exclude not in image.name: + return image + else: + return image + return None def get_image_name(self, image_id, exclude=None): - image = self.get_image(image_id, exclude) + image = self.get_image_exclude(image_id, exclude) if image: return image.name return None def get_image_id(self, image_name, exclude=None): - image = self.get_image(image_name, exclude) + image = self.get_image_exclude(image_name, exclude) if image: return image.id return None - def get_image(self, name_or_id, exclude=None): - for (image_id, image) in self.list_images().items(): - if image_id == name_or_id: - return image - if (image is not None and - name_or_id == image.name and ( - not exclude or exclude not in image.name)): - return image - return None - - def get_image_dict(self, name_or_id, exclude=None): - image = self.get_image(name_or_id, exclude) - if not image: - return image - if getattr(image, 'validate', None): - # glanceclient returns a "warlock" object if you use v2 - return meta.warlock_to_dict(image) - else: - # glanceclient returns a normal object if you use v1 - return meta.obj_to_dict(image) - def create_image_snapshot(self, name, **metadata): image = self.manager.submitTask(_tasks.ImageSnapshotCreate( name=name, **metadata)) @@ -1186,7 +1192,7 @@ def create_image( wait=False, timeout=3600, **kwargs): if not md5 or not sha256: (md5, sha256) = self._get_file_hashes(filename) - current_image = self.get_image_dict(name) + current_image = self.get_image(name) if (current_image and current_image.get(IMAGE_MD5_KEY, '') == md5 and current_image.get(IMAGE_SHA256_KEY, '') == sha256): self.log.debug( @@ -1215,7 +1221,7 @@ def _upload_image_put(self, name, filename, **image_kwargs): self.manager.submitTask(_tasks.ImageUpdate( image=image, data=open(filename, 'rb'))) self._cache.invalidate() - return self.get_image_dict(image.id) + return self.get_image(image.id) def _upload_image_task( self, name, filename, container, current_image=None, @@ -1263,7 +1269,7 @@ def _upload_image_task( image=image, **image_properties) self.list_images.invalidate(self) - return self.get_image_dict(status.result['image_id']) + return self.get_image(status.result['image_id']) if status.status == 'failure': raise OpenStackCloudException( "Image creation failed: {message}".format( diff --git a/shade/meta.py b/shade/meta.py index 6f5718b74..c2539fec9 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -191,3 +191,10 @@ def warlock_to_dict(obj): if isinstance(value, NON_CALLABLES) and not key.startswith('_'): obj_dict[key] = value return obj_dict + + +def warlock_list_to_dict(list): + new_list = [] + for obj in list: + new_list.append(warlock_to_dict(obj)) + return new_list diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index df5dd50d1..6df819899 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -232,16 +232,17 @@ class Flavor(object): @mock.patch.object(shade.OpenStackCloud, 'glance_client') def test_list_images(self, glance_mock): glance_mock.images.list.return_value = [] - self.assertEqual({}, self.cloud.list_images()) + self.assertEqual([], self.cloud.list_images()) class Image(object): id = '22' name = '22 name' status = 'success' fake_image = Image() + fake_image_dict = meta.obj_to_dict(fake_image) glance_mock.images.list.return_value = [fake_image] self.cloud.list_images.invalidate(self.cloud) - self.assertEqual({'22': fake_image}, self.cloud.list_images()) + self.assertEqual([fake_image_dict], self.cloud.list_images()) @mock.patch.object(shade.OpenStackCloud, 'glance_client') def test_list_images_ignores_unsteady_status(self, glance_mock): @@ -253,18 +254,19 @@ class Image(object): steady_image.id = '68' steady_image.name = 'Jagr' steady_image.status = 'active' + steady_image_dict = meta.obj_to_dict(steady_image) for status in ('queued', 'saving', 'pending_delete'): active_image = Image() active_image.id = self.getUniqueString() active_image.name = self.getUniqueString() active_image.status = status glance_mock.images.list.return_value = [active_image] - self.assertEqual({active_image.id: active_image}, + active_image_dict = meta.obj_to_dict(active_image) + self.assertEqual([active_image_dict], self.cloud.list_images()) glance_mock.images.list.return_value = [active_image, steady_image] # Should expect steady_image to appear if active wasn't cached - self.assertEqual({active_image.id: active_image, - '68': steady_image}, + self.assertEqual([active_image_dict, steady_image_dict], self.cloud.list_images()) @mock.patch.object(shade.OpenStackCloud, 'glance_client') @@ -283,17 +285,16 @@ class Image(object): active_image.id = self.getUniqueString() active_image.name = self.getUniqueString() active_image.status = status + active_image_dict = meta.obj_to_dict(active_image) if not first_image: - first_image = active_image + first_image = active_image_dict glance_mock.images.list.return_value = [active_image] - self.assertEqual({first_image.id: first_image}, - self.cloud.list_images()) + self.assertEqual([first_image], self.cloud.list_images()) glance_mock.images.list.return_value = [active_image, steady_image] # because we skipped the create_image code path, no invalidation # was done, so we _SHOULD_ expect steady state images to cache and # therefore we should _not_ expect to see the new one here - self.assertEqual({first_image.id: first_image}, - self.cloud.list_images()) + self.assertEqual([first_image], self.cloud.list_images()) def _call_create_image(self, name, container=None): imagefile = tempfile.NamedTemporaryFile(delete=False) @@ -306,7 +307,7 @@ def _call_create_image(self, name, container=None): def test_create_image_put(self, glance_mock): self.cloud.api_versions['image'] = '1' glance_mock.images.list.return_value = [] - self.assertEqual({}, self.cloud.list_images()) + self.assertEqual([], self.cloud.list_images()) class Image(object): id = '42' @@ -322,7 +323,8 @@ class Image(object): glance_mock.images.create.assert_called_with(**args) glance_mock.images.update.assert_called_with(data=mock.ANY, image=fake_image) - self.assertEqual({'42': fake_image}, self.cloud.list_images()) + fake_image_dict = meta.obj_to_dict(fake_image) + self.assertEqual([fake_image_dict], self.cloud.list_images()) @mock.patch.object(shade.OpenStackCloud, 'glance_client') @mock.patch.object(shade.OpenStackCloud, 'swift_client') @@ -337,7 +339,7 @@ class Container(object): swift_mock.put_container.return_value = fake_container swift_mock.head_object.return_value = {} glance_mock.images.list.return_value = [] - self.assertEqual({}, self.cloud.list_images()) + self.assertEqual([], self.cloud.list_images()) # V2's warlock objects just work like dicts class FakeImage(dict): @@ -382,4 +384,5 @@ class FakeTask(dict): 'owner_specified.shade.sha256': mock.ANY, 'image_id': '99'} glance_mock.images.update.assert_called_with(**args) - self.assertEqual({'99': fake_image}, self.cloud.list_images()) + fake_image_dict = meta.obj_to_dict(fake_image) + self.assertEqual([fake_image_dict], self.cloud.list_images()) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 0109d071b..14f2e0d61 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -72,6 +72,20 @@ def test__filter_list_dict2(self): }}) self.assertEquals([el2, el3], ret) + @mock.patch.object(shade.OpenStackCloud, 'search_images') + def test_get_images(self, mock_search): + image1 = dict(id='123', name='mickey') + mock_search.return_value = [image1] + r = self.cloud.get_image('mickey') + self.assertIsNotNone(r) + self.assertDictEqual(image1, r) + + @mock.patch.object(shade.OpenStackCloud, 'search_images') + def test_get_image_not_found(self, mock_search): + mock_search.return_value = [] + r = self.cloud.get_image('doesNotExist') + self.assertIsNone(r) + @mock.patch.object(shade.OpenStackCloud, 'search_servers') def test_get_server(self, mock_search): server1 = dict(id='123', name='mickey') From 2f1e6c13bee31a7d43ef80847d764648afdb9578 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 9 May 2015 08:49:59 -0400 Subject: [PATCH 0247/3836] Only add fall through cloud as a fall through We only want do define the 'openstack' cloud if there are neither environment varaibles nor config files. We need to define it as a place to put passed-in-parameters in the case of neither, but we don't want it any other time. The behavior can now be described as: - If you have a config file, you will get the clouds listed in it - If you have environment variable, you will get a cloud named 'envvars' - If you have neither, you will get a cloud named 'defaults' Change-Id: I6752c1ccecf1ef979b2603246eeaab7da360c8a4 --- README.rst | 12 ++++++--- os_client_config/config.py | 37 ++++++++++++++------------ os_client_config/tests/base.py | 4 +++ os_client_config/tests/test_config.py | 12 +++++++++ os_client_config/tests/test_environ.py | 11 ++++++-- 5 files changed, 53 insertions(+), 23 deletions(-) diff --git a/README.rst b/README.rst index 5c3dcfaf2..2c27153ec 100644 --- a/README.rst +++ b/README.rst @@ -9,6 +9,10 @@ put in a config file. It will read environment variables and config files, and it also contains some vendor specific default values so that you don't have to know extra info to use OpenStack +* If you have a config file, you will get the clouds listed in it +* If you have environment variables, you will get a cloud named 'envvars' +* If you have neither, you will get a cloud named 'defaults' with base defaults + Environment Variables --------------------- @@ -16,10 +20,10 @@ os-client-config honors all of the normal `OS_*` variables. It does not provide backwards compatibility to service-specific variables such as `NOVA_USERNAME`. -If you have OpenStack environment variables seet and no config files, -os-client-config will produce a cloud config object named "envvars" containing -your values from the environment. If you don't like the name "envvars", that's -ok, you can override it by setting `OS_CLOUD_NAME`. +If you have OpenStack environment variables set, os-client-config will produce +a cloud config object named "envvars" containing your values from the +environment. If you don't like the name "envvars", that's ok, you can override +it by setting `OS_CLOUD_NAME`. Service specific settings, like the nova service type, are set with the default service type as a prefix. For instance, to set a special service_type diff --git a/os_client_config/config.py b/os_client_config/config.py index e926555bc..45770d97b 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -91,31 +91,34 @@ def __init__(self, config_files=None, vendor_files=None): self.defaults = dict(defaults._defaults) - # use a config file if it exists where expected + # First, use a config file if it exists where expected self.cloud_config = self._load_config_file() + if not self.cloud_config: - self.cloud_config = dict( - clouds=dict(openstack=dict(self.defaults))) + self.cloud_config = {'clouds': {}} + if 'clouds' not in self.cloud_config: + self.cloud_config['clouds'] = {} - self.envvar_key = os.environ.pop('OS_CLOUD_NAME', None) - if self.envvar_key: - if self.envvar_key in self.cloud_config['clouds']: - raise exceptions.OpenStackConfigException( - 'clouds.yaml defines a cloud named "{0}", but' - ' OS_CLOUD_NAME is also set to "{0}". Please rename' - ' either your environment based cloud, or one of your' - ' file-based clouds.'.format(self.envvar_key)) - else: - self.envvar_key = 'envvars' + # Next, process environment variables and add them to the mix + self.envvar_key = os.environ.pop('OS_CLOUD_NAME', 'envvars') + if self.envvar_key in self.cloud_config['clouds']: + raise exceptions.OpenStackConfigException( + 'clouds.yaml defines a cloud named "{0}", but' + ' OS_CLOUD_NAME is also set to "{0}". Please rename' + ' either your environment based cloud, or one of your' + ' file-based clouds.'.format(self.envvar_key)) envvars = _get_os_environ() if envvars: - if self.envvar_key in self.cloud_config['clouds']: - raise exceptions.OpenStackConfigException( - 'clouds.yaml defines a cloud named {0}, and OS_*' - ' env vars are set') self.cloud_config['clouds'][self.envvar_key] = envvars + # Finally, fall through and make a cloud that starts with defaults + # because we need somewhere to put arguments, and there are neither + # config files or env vars + if not self.cloud_config['clouds']: + self.cloud_config = dict( + clouds=dict(defaults=dict(self.defaults))) + self._cache_max_age = 0 self._cache_path = CACHE_PATH self._cache_class = 'dogpile.cache.null' diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py index f6e815d9c..1169051f8 100644 --- a/os_client_config/tests/base.py +++ b/os_client_config/tests/base.py @@ -59,6 +59,9 @@ }, 'cache': {'max_age': 1}, } +NO_CONF = { + 'cache': {'max_age': 1}, +} def _write_yaml(obj): @@ -80,6 +83,7 @@ def setUp(self): conf['cache']['path'] = tdir.path self.cloud_yaml = _write_yaml(conf) self.vendor_yaml = _write_yaml(VENDOR_CONF) + self.no_yaml = _write_yaml(NO_CONF) # Isolate the test runs from the environment # Do this as two loops because you can't modify the dict in a loop diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 274b188c1..ae573a638 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -12,6 +12,10 @@ # License for the specific language governing permissions and limitations # under the License. +import os + +import fixtures + from os_client_config import cloud_config from os_client_config import config from os_client_config import exceptions @@ -44,6 +48,14 @@ def test_no_environ(self): self.assertRaises( exceptions.OpenStackConfigException, c.get_one_cloud, 'envvars') + def test_fallthrough(self): + c = config.OpenStackConfig(config_files=[self.no_yaml], + vendor_files=[self.no_yaml]) + for k in os.environ.keys(): + if k.startswith('OS_'): + self.useFixture(fixtures.EnvironmentVariable(k)) + c.get_one_cloud(cloud='defaults') + def test_get_one_cloud_auth_merge(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml]) cc = c.get_one_cloud(cloud='_test_cloud_', auth={'username': 'user'}) diff --git a/os_client_config/tests/test_environ.py b/os_client_config/tests/test_environ.py index 363cff1a7..e60c73b31 100644 --- a/os_client_config/tests/test_environ.py +++ b/os_client_config/tests/test_environ.py @@ -15,6 +15,7 @@ from os_client_config import cloud_config from os_client_config import config +from os_client_config import exceptions from os_client_config.tests import base import fixtures @@ -36,12 +37,18 @@ def test_get_one_cloud(self): vendor_files=[self.vendor_yaml]) self.assertIsInstance(c.get_one_cloud(), cloud_config.CloudConfig) + def test_no_fallthrough(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + self.assertRaises( + exceptions.OpenStackConfigException, c.get_one_cloud, 'openstack') + def test_envvar_name_override(self): self.useFixture( - fixtures.EnvironmentVariable('OS_CLOUD_NAME', 'openstack')) + fixtures.EnvironmentVariable('OS_CLOUD_NAME', 'override')) c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud('openstack') + cc = c.get_one_cloud('override') self._assert_cloud_details(cc) def test_environ_exists(self): From b9cdb7666580d81b956a045c0d10d697f0963164 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 9 May 2015 09:41:36 -0400 Subject: [PATCH 0248/3836] Sort defaults list for less conflicts Change-Id: Ic27c50f745a093cc20e3f22f09698f7ae643bc83 --- os_client_config/defaults.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/os_client_config/defaults.py b/os_client_config/defaults.py index 905950201..bf9a26dcf 100644 --- a/os_client_config/defaults.py +++ b/os_client_config/defaults.py @@ -15,11 +15,11 @@ _defaults = dict( auth_type='password', compute_api_version='2', + floating_ip_source='neutron', identity_api_version='2', image_api_use_tasks=False, image_api_version='1', network_api_version='2', object_api_version='1', volume_api_version='1', - floating_ip_source='neutron', ) From b16c49cfd6a0ee659e4493ef959e0483e93d350a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 9 May 2015 09:50:33 -0400 Subject: [PATCH 0249/3836] Add default versions for trove and ironic Change-Id: Ib7af38664cfbe75c78c70693117f1193c4beb7e6 --- os_client_config/defaults.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/os_client_config/defaults.py b/os_client_config/defaults.py index bf9a26dcf..f0476a60b 100644 --- a/os_client_config/defaults.py +++ b/os_client_config/defaults.py @@ -14,7 +14,9 @@ _defaults = dict( auth_type='password', + baremetal_api_version='1', compute_api_version='2', + database_api_version='1.0', floating_ip_source='neutron', identity_api_version='2', image_api_use_tasks=False, From 508c240f851dbfd5aa1d1223910ecb1df6a33b2a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 9 May 2015 09:54:01 -0400 Subject: [PATCH 0250/3836] Expose function to get defaults dict shade needs sane defaults for some of these too, but it's entirely legitimate to use shade without having first created an os_client_config object. Adding a function to get a copy of the defaults dict allows it to be consumed as a single source or truth for default values. Change-Id: I616d9492c7e0f53c48519cc8dacf3dfbd0082e36 --- os_client_config/config.py | 4 ++-- os_client_config/defaults.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 45770d97b..64afdbdd3 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -60,7 +60,7 @@ def get_boolean(value): def _get_os_environ(): - ret = dict(defaults._defaults) + ret = defaults.get_defaults() environkeys = [k for k in os.environ.keys() if k.startswith('OS_')] if not environkeys: return None @@ -89,7 +89,7 @@ def __init__(self, config_files=None, vendor_files=None): self._config_files = config_files or CONFIG_FILES self._vendor_files = vendor_files or VENDOR_FILES - self.defaults = dict(defaults._defaults) + self.defaults = defaults.get_defaults() # First, use a config file if it exists where expected self.cloud_config = self._load_config_file() diff --git a/os_client_config/defaults.py b/os_client_config/defaults.py index f0476a60b..14b820949 100644 --- a/os_client_config/defaults.py +++ b/os_client_config/defaults.py @@ -25,3 +25,7 @@ object_api_version='1', volume_api_version='1', ) + + +def get_defaults(): + return _defaults.copy() From 9b9e3d0d329b541960c8e28f897718a62a74ddf1 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 9 May 2015 21:10:13 -0400 Subject: [PATCH 0251/3836] Add UnitedStack With Keystone v3 even! Change-Id: If9445d99ccfa5f15ca3760bee4da900f302dc698 --- doc/source/vendor-support.rst | 17 +++++++++++++++++ os_client_config/vendors.py | 10 +++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index 1d5aff35f..dea27d13d 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -92,3 +92,20 @@ BHS-1 Beauharnois, QC * Image API Version is 2 * Images must be in `qcow2` format * Floating IPs are not needed + +UnitedStack +----------- + +https://identity.api.ustack.com/v3 + +============== ================ +Region Name Human Name +============== ================ +bj1 Beijing +gd1 Guangdong +============== ================ + +* Identity API Version is 3 +* Image API Version is 2 +* Images must be in `raw` format +* Floating IPs are not needed diff --git a/os_client_config/vendors.py b/os_client_config/vendors.py index 2eecfa38e..dec63dc2e 100644 --- a/os_client_config/vendors.py +++ b/os_client_config/vendors.py @@ -59,5 +59,13 @@ image_format='qcow2', floating_ip_source=None, ), - + unitedstack=dict( + auth=dict( + auth_url='https://identity.api.ustack.com/v3', + ), + identity_api_version='3', + image_api_version='2', + image_format='raw', + floating_ip_source=None, + ), ) From 4b40133e2199e11ccd5dc48c2ad60ac06d056d0a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 11 May 2015 16:24:28 -0400 Subject: [PATCH 0252/3836] Use appdirs for platform-independent locations Cache, data and config files live rooted in different places across different OS's. Use appdirs to find where. Depends-On: Ic939dea11b7476ec504d2bf65854a0781b1bfb39 Change-Id: I7338ae1d0442e0c5cc1ec4ae4d619fac319a4a28 --- README.rst | 22 ++++++++++++++++++++++ os_client_config/config.py | 29 +++++++++++++++++++---------- requirements.txt | 1 + 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index 2c27153ec..4389b680b 100644 --- a/README.rst +++ b/README.rst @@ -50,6 +50,28 @@ Service specific settings, like the nova service type, are set with the default service type as a prefix. For instance, to set a special service_type for trove (because you're using Rackspace) set: +Site Specific File Locations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In addition to `~/.config/openstack` and `/etc/openstack` - some platforms +have other locations they like to put things. `os-client-config` will also +look in an OS specific config dir + +* `USER_CONFIG_DIR` +* `SITE_CONFIG_DIR` + +`USER_CONFIG_DIR` is different on Linux, OSX and Windows. + +* Linux: `~/.config/openstack` +* OSX: `~/Library/Application Support/openstack` +* Windows: `C:\\Users\\USERNAME\\AppData\\Local\\OpenStack\\openstack` + +`SITE_CONFIG_DIR` is different on Linux, OSX and Windows. + +* Linux: `/etc/openstack` +* OSX: `/Library/Application Support/openstack` +* Windows: `C:\\ProgramData\\OpenStack\\openstack` + :: database_service_type: 'rax:database' diff --git a/os_client_config/config.py b/os_client_config/config.py index 64afdbdd3..dd4f37f2b 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -15,6 +15,7 @@ import os +import appdirs import yaml try: @@ -27,27 +28,35 @@ from os_client_config import exceptions from os_client_config import vendors -CONFIG_HOME = os.path.join(os.path.expanduser( - os.environ.get('XDG_CONFIG_HOME', os.path.join('~', '.config'))), - 'openstack') -CONFIG_SEARCH_PATH = [os.getcwd(), CONFIG_HOME, '/etc/openstack'] +APPDIRS = appdirs.AppDirs('openstack', 'OpenStack', multipath='/etc') +CONFIG_HOME = APPDIRS.user_config_dir +CACHE_PATH = APPDIRS.user_cache_dir + +UNIX_CONFIG_HOME = os.path.join( + os.path.expanduser(os.path.join('~', '.config')), 'openstack') +UNIX_SITE_CONFIG_HOME = '/etc/openstack' + +SITE_CONFIG_HOME = APPDIRS.site_config_dir + +CONFIG_SEARCH_PATH = [ + os.getcwd(), + CONFIG_HOME, UNIX_CONFIG_HOME, + SITE_CONFIG_HOME, UNIX_SITE_CONFIG_HOME +] YAML_SUFFIXES = ('.yaml', '.yml') CONFIG_FILES = [ os.path.join(d, 'clouds' + s) for d in CONFIG_SEARCH_PATH for s in YAML_SUFFIXES ] -CACHE_PATH = os.path.join(os.path.expanduser( - os.environ.get('XDG_CACHE_PATH', os.path.join('~', '.cache'))), - 'openstack') -BOOL_KEYS = ('insecure', 'cache') -VENDOR_SEARCH_PATH = [os.getcwd(), CONFIG_HOME, '/etc/openstack'] VENDOR_FILES = [ os.path.join(d, 'clouds-public' + s) - for d in VENDOR_SEARCH_PATH + for d in CONFIG_SEARCH_PATH for s in YAML_SUFFIXES ] +BOOL_KEYS = ('insecure', 'cache') + def set_default(key, value): defaults._defaults[key] = value diff --git a/requirements.txt b/requirements.txt index 498c5c336..894a70acc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. PyYAML>=3.1.0 +appdirs>=1.3.0 From 3328cc77da66608a1b9437299ed7d97d5edfd618 Mon Sep 17 00:00:00 2001 From: Gregory Haynes Date: Mon, 11 May 2015 22:59:09 +0000 Subject: [PATCH 0253/3836] Use fakes instead of mocks for data objects We hit a nasty infinite recursion issue because we were passing mock's to obj_to_dict which was setting the mock's attributes as attributes of the returned Bunch objects. Really though, we should just not be turning mock objects into Bunch's. Change-Id: I91a69a87082b30e16545c45ef8705ef4e929d5ca --- shade/tests/fakes.py | 59 +++++++++ shade/tests/unit/test_caching.py | 176 +++++++++---------------- shade/tests/unit/test_create_server.py | 21 +-- shade/tests/unit/test_delete_server.py | 9 +- 4 files changed, 138 insertions(+), 127 deletions(-) create mode 100644 shade/tests/fakes.py diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py new file mode 100644 index 000000000..38ea3b69a --- /dev/null +++ b/shade/tests/fakes.py @@ -0,0 +1,59 @@ +# -*- 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.V + +""" +fakes +---------------------------------- + +Fakes used for testing +""" + + +class FakeFlavor(object): + def __init__(self, id, name): + self.id = id + self.name = name + + +class FakeImage(object): + def __init__(self, id, name, status): + self.id = id + self.name = name + self.status = status + + +class FakeProject(object): + def __init__(self, id): + self.id = id + + +class FakeServer(object): + def __init__(self, id, name, status): + self.id = id + self.name = name + self.status = status + + +class FakeUser(object): + def __init__(self, id, email, name): + self.id = id + self.email = email + self.name = name + + +class FakeVolume(object): + def __init__(self, id, status, display_name): + self.id = id + self.status = status + self.display_name = display_name diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 6df819899..0f021c77c 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -19,6 +19,7 @@ import shade from shade import meta +from shade.tests import fakes from shade.tests.unit import base @@ -67,82 +68,68 @@ def test_openstack_cloud(self): @mock.patch('shade.OpenStackCloud.keystone_client') def test_project_cache(self, keystone_mock): - mock_project = mock.MagicMock() - mock_project.id = 'project_a' - keystone_mock.projects.list.return_value = [mock_project] - self.assertEqual({'project_a': mock_project}, self.cloud.project_cache) - mock_project_b = mock.MagicMock() - mock_project_b.id = 'project_b' - keystone_mock.projects.list.return_value = [mock_project, - mock_project_b] + project = fakes.FakeProject('project_a') + keystone_mock.projects.list.return_value = [project] + self.assertEqual({'project_a': project}, self.cloud.project_cache) + project_b = fakes.FakeProject('project_b') + keystone_mock.projects.list.return_value = [project, + project_b] self.assertEqual( - {'project_a': mock_project}, self.cloud.project_cache) + {'project_a': project}, self.cloud.project_cache) self.cloud.get_project_cache.invalidate(self.cloud) self.assertEqual( - {'project_a': mock_project, - 'project_b': mock_project_b}, self.cloud.project_cache) + {'project_a': project, + 'project_b': project_b}, self.cloud.project_cache) @mock.patch('shade.OpenStackCloud.cinder_client') def test_list_volumes(self, cinder_mock): - mock_volume = mock.MagicMock() - mock_volume.id = 'volume1' - mock_volume.status = 'available' - mock_volume.display_name = 'Volume 1 Display Name' - mock_volume_dict = meta.obj_to_dict(mock_volume) - cinder_mock.volumes.list.return_value = [mock_volume] - self.assertEqual([mock_volume_dict], self.cloud.list_volumes()) - mock_volume2 = mock.MagicMock() - mock_volume2.id = 'volume2' - mock_volume2.status = 'available' - mock_volume2.display_name = 'Volume 2 Display Name' - mock_volume2_dict = meta.obj_to_dict(mock_volume2) - cinder_mock.volumes.list.return_value = [mock_volume, mock_volume2] - self.assertEqual([mock_volume_dict], self.cloud.list_volumes()) + fake_volume = fakes.FakeVolume('volume1', 'available', + 'Volume 1 Display Name') + fake_volume_dict = meta.obj_to_dict(fake_volume) + cinder_mock.volumes.list.return_value = [fake_volume] + self.assertEqual([fake_volume_dict], self.cloud.list_volumes()) + fake_volume2 = fakes.FakeVolume('volume2', 'available', + 'Volume 2 Display Name') + fake_volume2_dict = meta.obj_to_dict(fake_volume2) + cinder_mock.volumes.list.return_value = [fake_volume, fake_volume2] + self.assertEqual([fake_volume_dict], self.cloud.list_volumes()) self.cloud.list_volumes.invalidate(self.cloud) - self.assertEqual([mock_volume_dict, mock_volume2_dict], + self.assertEqual([fake_volume_dict, fake_volume2_dict], self.cloud.list_volumes()) @mock.patch('shade.OpenStackCloud.cinder_client') def test_list_volumes_creating_invalidates(self, cinder_mock): - mock_volume = mock.MagicMock() - mock_volume.id = 'volume1' - mock_volume.status = 'creating' - mock_volume.display_name = 'Volume 1 Display Name' - mock_volume_dict = meta.obj_to_dict(mock_volume) - cinder_mock.volumes.list.return_value = [mock_volume] - self.assertEqual([mock_volume_dict], self.cloud.list_volumes()) - mock_volume2 = mock.MagicMock() - mock_volume2.id = 'volume2' - mock_volume2.status = 'available' - mock_volume2.display_name = 'Volume 2 Display Name' - mock_volume2_dict = meta.obj_to_dict(mock_volume2) - cinder_mock.volumes.list.return_value = [mock_volume, mock_volume2] - self.assertEqual([mock_volume_dict, mock_volume2_dict], + fake_volume = fakes.FakeVolume('volume1', 'creating', + 'Volume 1 Display Name') + fake_volume_dict = meta.obj_to_dict(fake_volume) + cinder_mock.volumes.list.return_value = [fake_volume] + self.assertEqual([fake_volume_dict], self.cloud.list_volumes()) + fake_volume2 = fakes.FakeVolume('volume2', 'available', + 'Volume 2 Display Name') + fake_volume2_dict = meta.obj_to_dict(fake_volume2) + cinder_mock.volumes.list.return_value = [fake_volume, fake_volume2] + self.assertEqual([fake_volume_dict, fake_volume2_dict], self.cloud.list_volumes()) @mock.patch.object(shade.OpenStackCloud, 'cinder_client') def test_create_volume_invalidates(self, cinder_mock): - mock_volb4 = mock.MagicMock() - mock_volb4.id = 'volume1' - mock_volb4.status = 'available' - mock_volb4.display_name = 'Volume 1 Display Name' - mock_volb4_dict = meta.obj_to_dict(mock_volb4) - cinder_mock.volumes.list.return_value = [mock_volb4] - self.assertEqual([mock_volb4_dict], self.cloud.list_volumes()) + fake_volb4 = fakes.FakeVolume('volume1', 'available', + 'Volume 1 Display Name') + fake_volb4_dict = meta.obj_to_dict(fake_volb4) + cinder_mock.volumes.list.return_value = [fake_volb4] + self.assertEqual([fake_volb4_dict], self.cloud.list_volumes()) volume = dict(display_name='junk_vol', size=1, display_description='test junk volume') - mock_vol = mock.Mock() - mock_vol.status = 'creating' - mock_vol.id = '12345' - mock_vol_dict = meta.obj_to_dict(mock_vol) - cinder_mock.volumes.create.return_value = mock_vol - cinder_mock.volumes.list.return_value = [mock_volb4, mock_vol] + fake_vol = fakes.FakeVolume('12345', 'creating', '') + fake_vol_dict = meta.obj_to_dict(fake_vol) + cinder_mock.volumes.create.return_value = fake_vol + cinder_mock.volumes.list.return_value = [fake_volb4, fake_vol] def creating_available(): def now_available(): - mock_vol.status = 'available' - mock_vol_dict['status'] = 'available' + fake_vol.status = 'available' + fake_vol_dict['status'] = 'available' return mock.DEFAULT cinder_mock.volumes.list.side_effect = now_available return mock.DEFAULT @@ -153,40 +140,36 @@ def now_available(): # If cache was not invalidated, we would not see our own volume here # because the first volume was available and thus would already be # cached. - self.assertEqual([mock_volb4_dict, mock_vol_dict], + self.assertEqual([fake_volb4_dict, fake_vol_dict], self.cloud.list_volumes()) # And now delete and check same thing since list is cached as all # available - mock_vol.status = 'deleting' - mock_vol_dict = meta.obj_to_dict(mock_vol) + fake_vol.status = 'deleting' + fake_vol_dict = meta.obj_to_dict(fake_vol) def deleting_gone(): def now_gone(): - cinder_mock.volumes.list.return_value = [mock_volb4] + cinder_mock.volumes.list.return_value = [fake_volb4] return mock.DEFAULT cinder_mock.volumes.list.side_effect = now_gone return mock.DEFAULT - cinder_mock.volumes.list.return_value = [mock_volb4, mock_vol] + cinder_mock.volumes.list.return_value = [fake_volb4, fake_vol] cinder_mock.volumes.list.side_effect = deleting_gone - cinder_mock.volumes.delete.return_value = mock_vol_dict + cinder_mock.volumes.delete.return_value = fake_vol_dict self.cloud.delete_volume('12345') - self.assertEqual([mock_volb4_dict], self.cloud.list_volumes()) + self.assertEqual([fake_volb4_dict], self.cloud.list_volumes()) @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_get_user_cache(self, keystone_mock): - mock_user = mock.MagicMock() - mock_user.id = '999' - keystone_mock.users.list.return_value = [mock_user] - self.assertEqual({'999': mock_user}, self.cloud.get_user_cache()) + fake_user = fakes.FakeUser('999', '', '') + keystone_mock.users.list.return_value = [fake_user] + self.assertEqual({'999': fake_user}, self.cloud.get_user_cache()) @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_modify_user_invalidates_cache(self, keystone_mock): - class User(object): - id = 'abc123' - email = 'abc123@domain.test' - name = 'abc123 name' - fake_user = User() + fake_user = fakes.FakeUser('abc123', 'abc123@domain.test', + 'abc123 name') # first cache an empty list keystone_mock.users.list.return_value = [] self.assertEqual({}, self.cloud.get_user_cache()) @@ -201,8 +184,8 @@ class User(object): # Cache should have been invalidated self.assertEqual({'abc123': fake_user}, self.cloud.get_user_cache()) # Update and check to see if it is updated - fake_user2 = User() - fake_user2.email = 'abc123-changed@domain.test' + fake_user2 = fakes.FakeUser('abc123', 'abc123 name', + 'abc123-changed@domain.test') keystone_mock.users.update.return_value = fake_user2 keystone_mock.users.list.return_value = [fake_user2] self.cloud.update_user('abc123', email='abc123-changed@domain.test') @@ -220,10 +203,7 @@ def test_list_flavors(self, nova_mock): nova_mock.flavors.list.return_value = [] self.assertEqual([], self.cloud.list_flavors()) - class Flavor(object): - id = '555' - name = 'vanilla' - fake_flavor = Flavor() + fake_flavor = fakes.FakeFlavor('555', 'vanilla') fake_flavor_dict = meta.obj_to_dict(fake_flavor) nova_mock.flavors.list.return_value = [fake_flavor] self.cloud.list_flavors.invalidate(self.cloud) @@ -234,11 +214,7 @@ def test_list_images(self, glance_mock): glance_mock.images.list.return_value = [] self.assertEqual([], self.cloud.list_images()) - class Image(object): - id = '22' - name = '22 name' - status = 'success' - fake_image = Image() + fake_image = fakes.FakeImage('22', '22 name', 'success') fake_image_dict = meta.obj_to_dict(fake_image) glance_mock.images.list.return_value = [fake_image] self.cloud.list_images.invalidate(self.cloud) @@ -246,20 +222,11 @@ class Image(object): @mock.patch.object(shade.OpenStackCloud, 'glance_client') def test_list_images_ignores_unsteady_status(self, glance_mock): - class Image(object): - id = None - name = None - status = None - steady_image = Image() - steady_image.id = '68' - steady_image.name = 'Jagr' - steady_image.status = 'active' + steady_image = fakes.FakeImage('68', 'Jagr', 'active') steady_image_dict = meta.obj_to_dict(steady_image) for status in ('queued', 'saving', 'pending_delete'): - active_image = Image() - active_image.id = self.getUniqueString() - active_image.name = self.getUniqueString() - active_image.status = status + active_image = fakes.FakeImage(self.getUniqueString(), + self.getUniqueString(), status) glance_mock.images.list.return_value = [active_image] active_image_dict = meta.obj_to_dict(active_image) self.assertEqual([active_image_dict], @@ -271,20 +238,11 @@ class Image(object): @mock.patch.object(shade.OpenStackCloud, 'glance_client') def test_list_images_caches_steady_status(self, glance_mock): - class Image(object): - id = None - name = None - status = None - steady_image = Image() - steady_image.id = '91' - steady_image.name = 'Federov' - steady_image.status = 'active' + steady_image = fakes.FakeImage('91', 'Federov', 'active') first_image = None for status in ('active', 'deleted', 'killed'): - active_image = Image() - active_image.id = self.getUniqueString() - active_image.name = self.getUniqueString() - active_image.status = status + active_image = fakes.FakeImage(self.getUniqueString(), + self.getUniqueString(), status) active_image_dict = meta.obj_to_dict(active_image) if not first_image: first_image = active_image_dict @@ -309,11 +267,7 @@ def test_create_image_put(self, glance_mock): glance_mock.images.list.return_value = [] self.assertEqual([], self.cloud.list_images()) - class Image(object): - id = '42' - name = '42 name' - status = 'success' - fake_image = Image() + fake_image = fakes.FakeImage('42', '42 name', 'success') glance_mock.images.create.return_value = fake_image glance_mock.images.list.return_value = [fake_image] self._call_create_image('42 name') diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index e581d0ad2..53ae52efa 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -22,7 +22,7 @@ from mock import patch, Mock from shade import OpenStackCloud from shade.exc import (OpenStackCloudException, OpenStackCloudTimeout) -from shade.tests import base +from shade.tests import base, fakes class TestCreateServer(base.TestCase): @@ -109,14 +109,14 @@ def test_create_server_no_wait(self): novaclient create call returns the server instance. """ with patch("shade.OpenStackCloud"): - mock_server = Mock(status="BUILD") + fake_server = fakes.FakeServer('', '', 'BUILD') config = { - "servers.create.return_value": mock_server, - "servers.get.return_value": mock_server + "servers.create.return_value": fake_server, + "servers.get.return_value": fake_server } OpenStackCloud.nova_client = Mock(**config) self.assertEqual( - self.client.create_server(), mock_server) + self.client.create_server(), fake_server) def test_create_server_wait(self): """ @@ -124,15 +124,16 @@ def test_create_server_wait(self): its status changes to "ACTIVE". """ with patch("shade.OpenStackCloud"): - mock_server = Mock(status="ACTIVE") + fake_server = fakes.FakeServer('', '', 'ACTIVE') config = { - "servers.create.return_value": Mock(status="BUILD"), + "servers.create.return_value": fakes.FakeServer('', '', + 'ACTIVE'), "servers.get.side_effect": [ - Mock(status="BUILD"), mock_server] + Mock(status="BUILD"), fake_server] } OpenStackCloud.nova_client = Mock(**config) with patch.object(OpenStackCloud, "add_ips_to_server", - return_value=mock_server): + return_value=fake_server): self.assertEqual( self.client.create_server(wait=True), - mock_server) + fake_server) diff --git a/shade/tests/unit/test_delete_server.py b/shade/tests/unit/test_delete_server.py index 1e8157722..58ae0d4b6 100644 --- a/shade/tests/unit/test_delete_server.py +++ b/shade/tests/unit/test_delete_server.py @@ -23,6 +23,7 @@ from novaclient import exceptions as nova_exc from shade import OpenStackCloud +from shade.tests import fakes from shade.tests.unit import base @@ -37,9 +38,7 @@ def test_delete_server(self, nova_mock): """ Test that novaclient server delete is called when wait=False """ - server = mock.MagicMock(id='1234', - status='ACTIVE') - server.name = 'daffy' + server = fakes.FakeServer('1234', 'daffy', 'ACTIVE') nova_mock.servers.list.return_value = [server] self.cloud.delete_server('daffy', wait=False) nova_mock.servers.delete.assert_called_with(server=server.id) @@ -63,9 +62,7 @@ def test_delete_server_wait_for_notfound(self, nova_mock): """ Test that delete_server waits for NotFound from novaclient """ - server = mock.MagicMock(id='9999', - status='ACTIVE') - server.name = 'wily' + server = fakes.FakeServer('9999', 'wily', 'ACTIVE') nova_mock.servers.list.return_value = [server] def _delete_wily(*args, **kwargs): From 948aa01d0c15c1728deb954b2764682705176c9c Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 11 May 2015 17:29:42 -0500 Subject: [PATCH 0254/3836] Add tests for OSC usage OSC uses os-client-config as an intermediate handler in the argparse-to-final-options sequence. * Validate the expected usage of the argparse arguement to get_one_cloud(). This turned out to be a problem with the way set_defaults() was implemented and has been withdrawn until set_defaults is re-worked as a kwarg to OpenStackCloud.__init__() * Validate setting the default auth_type value Change-Id: Idae91962f05d787cecf4a59fac01e9321bc69687 --- os_client_config/tests/test_config.py | 50 +++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index ae573a638..f4bf6dbbe 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. +import argparse import os import fixtures @@ -61,3 +62,52 @@ def test_get_one_cloud_auth_merge(self): cc = c.get_one_cloud(cloud='_test_cloud_', auth={'username': 'user'}) self.assertEqual('user', cc.auth['username']) self.assertEqual('testpass', cc.auth['password']) + + +class TestConfigArgparse(base.TestCase): + + def setUp(self): + super(TestConfigArgparse, self).setUp() + + self.options = argparse.Namespace( + region_name='other-test-region', + snack_type='cookie', + ) + + def test_get_one_cloud_argparse(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + + cc = c.get_one_cloud(cloud='_test_cloud_', argparse=self.options) + self._assert_cloud_details(cc) + self.assertEqual(cc.region_name, 'other-test-region') + self.assertEqual(cc.snack_type, 'cookie') + + def test_get_one_cloud_just_argparse(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + + cc = c.get_one_cloud(cloud='', argparse=self.options) + self.assertIsNone(cc.cloud) + self.assertNotIn('username', cc.auth) + self.assertEqual(cc.region_name, 'other-test-region') + self.assertEqual(cc.snack_type, 'cookie') + + def test_get_one_cloud_no_argparse(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + + cc = c.get_one_cloud(cloud='_test_cloud_', argparse=None) + self._assert_cloud_details(cc) + self.assertEqual(cc.region_name, 'test-region') + self.assertIsNone(cc.snack_type) + + +class TestConfigDefault(base.TestCase): + + def test_set_no_default(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + cc = c.get_one_cloud(cloud='_test_cloud_', argparse=None) + self._assert_cloud_details(cc) + self.assertEqual(cc.auth_type, 'password') From e71bee318c95b05fdf4d5e0102f7da60bde3adb5 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 8 May 2015 15:23:55 -0400 Subject: [PATCH 0255/3836] Stop leaking server objects We should not be returning raw client objects when creating or rebuilding a server. The usage document is updated to indicate that access to resource values via attribute is deprecated, and the examples in the README now reflect dict-style access. Change-Id: Iac38d4c0b29f867cc3cefaccf48c1c3fcd17a3d9 --- README.rst | 4 +-- doc/source/usage.rst | 11 ++++++-- shade/__init__.py | 36 ++++++++++++++----------- shade/tests/functional/test_compute.py | 8 +++--- shade/tests/unit/test_create_server.py | 5 ++-- shade/tests/unit/test_rebuild_server.py | 10 +++---- 6 files changed, 43 insertions(+), 31 deletions(-) diff --git a/README.rst b/README.rst index 4f8766073..dcda824b8 100644 --- a/README.rst +++ b/README.rst @@ -37,9 +37,9 @@ Sometimes an example is nice. # But you can also access the underlying python-*client objects cinder = cloud.cinder_client volumes = cinder.volumes.list() - volume_id = [v for v in volumes if v.status == 'available'][0].id + volume_id = [v for v in volumes if v['status'] == 'available'][0]['id'] nova = cloud.nova_client - print nova.volumes.create_server_volume(s.id, volume_id, None) + print nova.volumes.create_server_volume(s['id'], volume_id, None) attachments = [] print volume_id while not attachments: diff --git a/doc/source/usage.rst b/doc/source/usage.rst index 75dfc88ae..55a263166 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -1,10 +1,17 @@ -======== +===== Usage -======== +===== To use shade in a project:: import shade +.. warning:: + Several of the API methods return a ``dict`` that describe a resource. + It is possible to access keys of the dict as an attribute (e.g., + ``server.id`` instead of ``server['id']``) to maintain some backward + compatibility, but attribute access is deprecated. New code should + assume a normal dictionary and access values via key. + .. automodule:: shade :members: diff --git a/shade/__init__.py b/shade/__init__.py index 9cb0a3f9e..5ff4f72e6 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1677,7 +1677,11 @@ def add_ips_to_server(self, server, auto_ip=True, ips=None, ip_pool=None): def create_server(self, auto_ip=True, ips=None, ip_pool=None, root_volume=None, terminate_volume=False, wait=False, timeout=180, **bootkwargs): + """Create a virtual server instance. + :returns: A dict representing the created server. + :raises: OpenStackCloudException on operation error. + """ if root_volume: if terminate_volume: suffix = ':::1' @@ -1703,21 +1707,22 @@ def create_server(self, auto_ip=True, ips=None, ip_pool=None, timeout, "Timeout waiting for the server to come up."): try: - server = self.manager.submitTask( - _tasks.ServerGet(server=server)) + server = meta.obj_to_dict( + self.manager.submitTask( + _tasks.ServerGet(server=server)) + ) except Exception: continue - if server.status == 'ACTIVE': + if server['status'] == 'ACTIVE': return self.add_ips_to_server( server, auto_ip, ips, ip_pool) - if server.status == 'ERROR': + if server['status'] == 'ERROR': raise OpenStackCloudException( "Error in creating the server", - extra_data=dict( - server=meta.obj_to_dict(server))) - return server + extra_data=dict(server=server)) + return meta.obj_to_dict(server) def rebuild_server(self, server_id, image_id, wait=False, timeout=180): try: @@ -1733,20 +1738,21 @@ def rebuild_server(self, server_id, image_id, wait=False, timeout=180): "Timeout waiting for server {0} to " "rebuild.".format(server_id)): try: - server = self.manager.submitTask( - _tasks.ServerGet(server=server)) + server = meta.obj_to_dict( + self.manager.submitTask( + _tasks.ServerGet(server=server)) + ) except Exception: continue - if server.status == 'ACTIVE': - break + if server['status'] == 'ACTIVE': + return server - if server.status == 'ERROR': + if server['status'] == 'ERROR': raise OpenStackCloudException( "Error in rebuilding the server", - extra_data=dict( - server=meta.obj_to_dict(server))) - return server + extra_data=dict(server=server)) + return meta.obj_to_dict(server) def delete_server(self, name, wait=False, timeout=180): server = self.get_server(name) diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index a2edc8815..6a1551476 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -19,7 +19,6 @@ Functional tests for `shade` compute methods. """ -from novaclient.v2.servers import Server from shade import openstack_cloud from shade.tests import base from shade.tests.functional.util import pick_flavor, pick_image @@ -47,10 +46,9 @@ def test_create_server(self): self.addCleanup(self._cleanup_servers) server = self.cloud.create_server(name='test_create_server', image=self.image, flavor=self.flavor) - self.assertIsInstance(server, Server) - self.assertEquals(server.name, 'test_create_server') - self.assertEquals(server.image['id'], self.image.id) - self.assertEquals(server.flavor['id'], self.flavor.id) + self.assertEquals(server['name'], 'test_create_server') + self.assertEquals(server['image']['id'], self.image.id) + self.assertEquals(server['flavor']['id'], self.flavor.id) def test_delete_server(self): self.cloud.create_server(name='test_delete_server', diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index 53ae52efa..2a631a825 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -20,6 +20,7 @@ """ from mock import patch, Mock +from shade import meta from shade import OpenStackCloud from shade.exc import (OpenStackCloudException, OpenStackCloudTimeout) from shade.tests import base, fakes @@ -115,8 +116,8 @@ def test_create_server_no_wait(self): "servers.get.return_value": fake_server } OpenStackCloud.nova_client = Mock(**config) - self.assertEqual( - self.client.create_server(), fake_server) + self.assertEqual(meta.obj_to_dict(fake_server), + self.client.create_server()) def test_create_server_wait(self): """ diff --git a/shade/tests/unit/test_rebuild_server.py b/shade/tests/unit/test_rebuild_server.py index c455b246a..160848fd7 100644 --- a/shade/tests/unit/test_rebuild_server.py +++ b/shade/tests/unit/test_rebuild_server.py @@ -20,6 +20,7 @@ """ from mock import patch, Mock +from shade import meta from shade import OpenStackCloud from shade.exc import (OpenStackCloudException, OpenStackCloudTimeout) from shade.tests.unit import base @@ -85,8 +86,8 @@ def test_rebuild_server_no_wait(self): "servers.rebuild.return_value": mock_server } OpenStackCloud.nova_client = Mock(**config) - self.assertEqual( - self.client.rebuild_server("a", "b"), mock_server) + self.assertEqual(meta.obj_to_dict(mock_server), + self.client.rebuild_server("a", "b")) def test_rebuild_server_wait(self): """ @@ -100,6 +101,5 @@ def test_rebuild_server_wait(self): "servers.get.return_value": mock_server } OpenStackCloud.nova_client = Mock(**config) - self.assertEqual( - self.client.rebuild_server("a", "b", wait=True), - mock_server) + self.assertEqual(meta.obj_to_dict(mock_server), + self.client.rebuild_server("a", "b", wait=True)) From cbdc7c70801d7a0bd9a6ac51e0b9b6f4dc9e7b8d Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Tue, 12 May 2015 14:26:27 -0500 Subject: [PATCH 0256/3836] Change overriding defaults to kwarg Forcibly changing the global defaults isn't the nicest thing to do and turns out to be a pain during tests. Make overriding defaults into an arg to OpenStackConfig.__init__(). OSC is the only known user of set_default(), once this is released and in global-requirements OSC an be updated then set_default() can be removed. Change-Id: Iddbc66398e89f06f1d665d7b0ef243bc786c8e36 --- os_client_config/config.py | 9 ++++++++- os_client_config/tests/test_config.py | 26 ++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 64afdbdd3..5b6439550 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -49,6 +49,10 @@ ] +# NOTE(dtroyer): This turns out to be not the best idea so let's move +# overriding defaults to a kwarg to OpenStackConfig.__init__() +# Remove this sometime in June 2015 once OSC is comfortably +# changed-over and global-defaults is updated. def set_default(key, value): defaults._defaults[key] = value @@ -85,11 +89,14 @@ def _auth_update(old_dict, new_dict): class OpenStackConfig(object): - def __init__(self, config_files=None, vendor_files=None): + def __init__(self, config_files=None, vendor_files=None, + override_defaults=None): self._config_files = config_files or CONFIG_FILES self._vendor_files = vendor_files or VENDOR_FILES self.defaults = defaults.get_defaults() + if override_defaults: + self.defaults.update(override_defaults) # First, use a config file if it exists where expected self.cloud_config = self._load_config_file() diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index ae573a638..8df62d60a 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -18,6 +18,7 @@ from os_client_config import cloud_config from os_client_config import config +from os_client_config import defaults from os_client_config import exceptions from os_client_config.tests import base @@ -29,6 +30,31 @@ def test_get_one_cloud(self): vendor_files=[self.vendor_yaml]) self.assertIsInstance(c.get_one_cloud(), cloud_config.CloudConfig) + def test_get_one_cloud_auth_defaults(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml]) + cc = c.get_one_cloud(cloud='_test_cloud_', auth={'username': 'user'}) + self.assertEqual('user', cc.auth['username']) + self.assertEqual( + defaults._defaults['auth_type'], + cc.auth_type, + ) + self.assertEqual( + defaults._defaults['identity_api_version'], + cc.identity_api_version, + ) + + def test_get_one_cloud_auth_override_defaults(self): + default_options = {'auth_type': 'token'} + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + override_defaults=default_options) + cc = c.get_one_cloud(cloud='_test_cloud_', auth={'username': 'user'}) + self.assertEqual('user', cc.auth['username']) + self.assertEqual('token', cc.auth_type) + self.assertEqual( + defaults._defaults['identity_api_version'], + cc.identity_api_version, + ) + def test_get_one_cloud_with_config_files(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) From 9bcf25973899238132f1795fede2bb9a8376f5c8 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 12 May 2015 18:48:58 -0400 Subject: [PATCH 0257/3836] Don't error on missing certs We were trying to be too helpful. The underlying libraries do the right thing here. Change-Id: Iba1a87fee10ba5e5f6d7ea1e70632c47fd67275c --- shade/__init__.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 9cb0a3f9e..62d036f42 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -15,7 +15,6 @@ import hashlib import logging import operator -import os import time from cinderclient.v1 import client as cinder_client @@ -99,19 +98,10 @@ def operator_cloud(debug=False, **kwargs): def _ssl_args(verify, cacert, cert, key): if cacert: - if not os.path.exists(cacert): - raise OpenStackCloudException( - "CA Cert {0} does not exist".format(cacert)) verify = cacert if cert: - if not os.path.exists(cert): - raise OpenStackCloudException( - "Client Cert {0} does not exist".format(cert)) if key: - if not os.path.exists(key): - raise OpenStackCloudException( - "Client key {0} does not exist".format(key)) cert = (cert, key) return (verify, cert) From 569fe40caf7054048d4777883b81934056af841f Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Tue, 5 May 2015 18:46:29 +0100 Subject: [PATCH 0258/3836] Add floating IP pool resource methods This change add floating IP pool resource list method. floating IP pool is a Nova API read-only resource. Change-Id: I1c68a696926f45e93050f441404726d92a717900 --- shade/__init__.py | 21 +++++++ shade/_tasks.py | 5 ++ shade/exc.py | 4 ++ shade/tests/fakes.py | 6 ++ .../tests/functional/test_floating_ip_pool.py | 53 ++++++++++++++++ shade/tests/unit/test_floating_ip_pool.py | 60 +++++++++++++++++++ 6 files changed, 149 insertions(+) create mode 100644 shade/tests/functional/test_floating_ip_pool.py create mode 100644 shade/tests/unit/test_floating_ip_pool.py diff --git a/shade/__init__.py b/shade/__init__.py index 9cb0a3f9e..43602c3bc 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -878,6 +878,10 @@ def search_images(self, name_or_id=None, filters=None): images = self.list_images() return self._filter_list(images, name_or_id, filters) + def search_floating_ip_pools(self, name=None, filters=None): + pools = self.list_floating_ip_pools() + return self._filter_list(pools, name, filters) + def list_networks(self): return self.manager.submitTask(_tasks.NetworkList())['networks'] @@ -964,6 +968,23 @@ def list_images(self, filter_deleted=True): images.append(image) return images + def list_floating_ip_pools(self): + if not self._has_nova_extension('os-floating-ip-pools'): + raise OpenStackCloudUnavailableExtension( + 'Floating IP pools extension is not available on target cloud') + + try: + return meta.obj_list_to_dict( + self.manager.submitTask(_tasks.FloatingIPPoolList()) + ) + except Exception as e: + self.log.debug( + "nova could not list floating IP pools: {msg}".format( + msg=str(e)), exc_info=True) + raise OpenStackCloudException( + "error fetching floating IP pool list: {msg}".format( + msg=str(e))) + def get_network(self, name_or_id, filters=None): return self._get_entity(self.search_networks, name_or_id, filters) diff --git a/shade/_tasks.py b/shade/_tasks.py index bb336fc80..4084a1f46 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -213,6 +213,11 @@ def main(self, client): return client.nova_client.servers.add_floating_ip(**self.args) +class FloatingIPPoolList(task_manager.Task): + def main(self, client): + return client.nova_client.floating_ip_pools.list() + + class ContainerGet(task_manager.Task): def main(self, client): return client.swift_client.head_container(**self.args) diff --git a/shade/exc.py b/shade/exc.py index cc2e4c326..26f1acb51 100644 --- a/shade/exc.py +++ b/shade/exc.py @@ -34,3 +34,7 @@ class OpenStackCloudTimeout(OpenStackCloudException): class OpenStackCloudUnavailableService(OpenStackCloudException): pass + + +class OpenStackCloudUnavailableExtension(OpenStackCloudException): + pass diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 38ea3b69a..d7f585b2b 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -26,6 +26,12 @@ def __init__(self, id, name): self.name = name +class FakeFloatingIPPool(object): + def __init__(self, id, name): + self.id = id + self.name = name + + class FakeImage(object): def __init__(self, id, name, status): self.id = id diff --git a/shade/tests/functional/test_floating_ip_pool.py b/shade/tests/functional/test_floating_ip_pool.py new file mode 100644 index 000000000..9b608ec8e --- /dev/null +++ b/shade/tests/functional/test_floating_ip_pool.py @@ -0,0 +1,53 @@ +# Copyright (c) 2015 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. + +""" +test_floating_ip_pool +---------------------------------- + +Functional tests for floating IP pool resource (managed by nova) +""" + +from shade import openstack_cloud +from shade.tests import base + + +# When using nova-network, floating IP pools are created with nova-manage +# command. +# When using Neutron, floating IP pools in Nova are mapped from external +# network names. This only if the floating-ip-pools nova extension is +# available. +# For instance, for current implementation of hpcloud that's not true: +# nova floating-ip-pool-list returns 404. + + +class TestFloatingIPPool(base.TestCase): + def setUp(self): + super(TestFloatingIPPool, self).setUp() + # Shell should have OS-* envvars from openrc, typically loaded by job + self.cloud = openstack_cloud() + + if not self.cloud._has_nova_extension('os-floating-ip-pools'): + # Skipping this test is floating-ip-pool extension is not + # available on the testing cloud + self.skip( + 'Floating IP pools extension is not available') + + def test_list_floating_ip_pools(self): + pools = self.cloud.list_floating_ip_pools() + if not pools: + self.assertFalse('no floating-ip pool available') + + for pool in pools: + self.assertTrue('name' in pool) diff --git a/shade/tests/unit/test_floating_ip_pool.py b/shade/tests/unit/test_floating_ip_pool.py new file mode 100644 index 000000000..507d9f655 --- /dev/null +++ b/shade/tests/unit/test_floating_ip_pool.py @@ -0,0 +1,60 @@ +# Copyright (c) 2015 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. + +""" +test_floating_ip_pool +---------------------------------- + +Test floating IP pool resource (managed by nova) +""" + +from mock import patch +from shade import OpenStackCloud +from shade import OpenStackCloudException +from shade.tests.unit import base +from shade.tests.fakes import FakeFloatingIPPool + + +class TestFloatingIPPool(base.TestCase): + mock_pools = [ + {'id': 'pool1_id', 'name': 'pool1'}, + {'id': 'pool2_id', 'name': 'pool2'}] + + def setUp(self): + super(TestFloatingIPPool, self).setUp() + self.client = OpenStackCloud('cloud', {}) + + @patch.object(OpenStackCloud, '_has_nova_extension') + @patch.object(OpenStackCloud, 'nova_client') + def test_list_floating_ip_pools( + self, mock_nova_client, mock__has_nova_extension): + mock_nova_client.floating_ip_pools.list.return_value = [ + FakeFloatingIPPool(**p) for p in self.mock_pools + ] + mock__has_nova_extension.return_value = True + + floating_ip_pools = self.client.list_floating_ip_pools() + + self.assertItemsEqual(floating_ip_pools, self.mock_pools) + + @patch.object(OpenStackCloud, '_has_nova_extension') + @patch.object(OpenStackCloud, 'nova_client') + def test_list_floating_ip_pools_exception( + self, mock_nova_client, mock__has_nova_extension): + mock_nova_client.floating_ip_pools.list.side_effect = \ + Exception('whatever') + mock__has_nova_extension.return_value = True + + self.assertRaises( + OpenStackCloudException, self.client.list_floating_ip_pools) From a0a36b849af1a8706872bbf598323dc0725c9bdd Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Wed, 13 May 2015 13:59:56 -0700 Subject: [PATCH 0259/3836] Handle novaclient exceptions during delete_server Currently they are allowed to bubble up without being caught. Change-Id: I0c5cf52a0315cee4a84ee4b3ff205778f461af2e --- shade/__init__.py | 9 +++++++- shade/tests/unit/test_delete_server.py | 29 ++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index ef68cd19a..663b06045 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1747,7 +1747,14 @@ def rebuild_server(self, server_id, image_id, wait=False, timeout=180): def delete_server(self, name, wait=False, timeout=180): server = self.get_server(name) if server: - self.manager.submitTask(_tasks.ServerDelete(server=server.id)) + try: + self.manager.submitTask(_tasks.ServerDelete(server=server.id)) + except nova_exceptions.NotFound: + return + except Exception as e: + self.log.debug("nova delete server failed", exc_info=True) + raise OpenStackCloudException( + "Error in deleting server: {0}".format(e)) else: return if not wait: diff --git a/shade/tests/unit/test_delete_server.py b/shade/tests/unit/test_delete_server.py index 58ae0d4b6..0b8c18e3b 100644 --- a/shade/tests/unit/test_delete_server.py +++ b/shade/tests/unit/test_delete_server.py @@ -23,6 +23,7 @@ from novaclient import exceptions as nova_exc from shade import OpenStackCloud +from shade import exc as shade_exc from shade.tests import fakes from shade.tests.unit import base @@ -79,3 +80,31 @@ def _raise_notfound(*args, **kwargs): nova_mock.servers.delete.side_effect = _delete_wily self.cloud.delete_server('wily', wait=True) nova_mock.servers.delete.assert_called_with(server=server.id) + + @mock.patch('shade.OpenStackCloud.nova_client') + def test_delete_server_fails(self, nova_mock): + """ + Test that delete_server wraps novaclient exceptions + """ + nova_mock.servers.list.return_value = [fakes.FakeServer('1212', + 'speedy', + 'ACTIVE')] + for fail in (nova_exc.BadRequest, + nova_exc.Unauthorized, + nova_exc.Forbidden, + nova_exc.MethodNotAllowed, + nova_exc.Conflict, + nova_exc.OverLimit, + nova_exc.RateLimit, + nova_exc.HTTPNotImplemented): + + def _raise_fail(server): + raise fail(code=fail.http_status) + + nova_mock.servers.delete.side_effect = _raise_fail + exc = self.assertRaises(shade_exc.OpenStackCloudException, + self.cloud.delete_server, 'speedy', + wait=False) + # Note that message is deprecated from Exception, but not in + # the novaclient exceptions. + self.assertIn(fail.message, str(exc)) From 5d9ab4498d7fedbb73f4c1a038bce95ad9f3c710 Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Wed, 13 May 2015 17:15:44 -0700 Subject: [PATCH 0260/3836] Improve error message on auth_plugin failure We should include the error always! Change-Id: I018f554018c66967588dd7da4176066d4848d552 --- shade/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index ef68cd19a..d33867a63 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -353,8 +353,8 @@ def keystone_session(self): self.log.debug( "keystone couldn't construct plugin", exc_info=True) raise OpenStackCloudException( - "Error constructing auth plugin: {plugin}".format( - plugin=self.auth_type)) + "Error constructing auth plugin: {plugin} {error}".format( + plugin=self.auth_type, error=str(e))) try: self._keystone_session = ksc_session.Session( From 5af9a9d198e0eeb90b2604fcb4b1c9c17f985c70 Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Thu, 14 May 2015 10:29:51 -0700 Subject: [PATCH 0261/3836] Pass OS_ variables through to functional tests tox 2.0 isolates tests from environment variables by default now. Once devstack emits clouds.yaml we can remove this and consume that. Change-Id: I8e8c769bbdb3f467af0802e1d49f5ebd84663a8f --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 52264275c..d473d9ffa 100644 --- a/tox.ini +++ b/tox.ini @@ -18,6 +18,7 @@ commands = python setup.py testr --slowest --testr-args='{posargs}' [testenv:functional] setenv = OS_TEST_PATH = ./shade/tests/functional +passenv = OS_* commands = python setup.py testr --slowest --testr-args='--concurrency=1 {posargs}' [testenv:pep8] From ef47a50f72f757006d2af86a230fdb54fc9f19e6 Mon Sep 17 00:00:00 2001 From: Jeremy Stanley Date: Thu, 14 May 2015 21:38:19 +0000 Subject: [PATCH 0262/3836] Replace ci.o.o links with docs.o.o/infra The http://ci.openstack.org/ documentation site has been deprecated, replaced by redirects to corresponding paths within http://docs.openstack.org/infra/ where other Project Infrastructure documentation already resides. Change-Id: I764b134046e175905f00fb2158e9078698364029 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 1756a9c85..4e7aebba1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,7 @@ description-file = README.rst author = OpenStack Infrastructure Team author-email = openstack-infra@lists.openstack.org -home-page = http://ci.openstack.org/ +home-page = http://docs.openstack.org/infra/shade/ classifier = Environment :: OpenStack Intended Audience :: Information Technology From 6d654bc7adce911d34216b2038ef0e02cc6dba1c Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 15 May 2015 08:55:48 -0400 Subject: [PATCH 0263/3836] Catch client exceptions during list ops We were not consistently catching the underlying client exceptions from the list operations, and just letting them escape as non-wrapped exceptions. This corrects that. Also, list_images() was trying to catch an OpenStackCloudException that was never going to happen from the code in the try block. Change-Id: Ia863ec2f988c7e90138097be15cd66fc858d1b21 --- shade/__init__.py | 72 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 53 insertions(+), 19 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index c41b19d79..68f4928a2 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -873,13 +873,28 @@ def search_floating_ip_pools(self, name=None, filters=None): return self._filter_list(pools, name, filters) def list_networks(self): - return self.manager.submitTask(_tasks.NetworkList())['networks'] + try: + return self.manager.submitTask(_tasks.NetworkList())['networks'] + except Exception as e: + self.log.debug("network list failed: %s" % e, exc_info=True) + raise OpenStackCloudException( + "Error fetching network list: %s" % e) def list_routers(self): - return self.manager.submitTask(_tasks.RouterList())['routers'] + try: + return self.manager.submitTask(_tasks.RouterList())['routers'] + except Exception as e: + self.log.debug("router list failed: %s" % e, exc_info=True) + raise OpenStackCloudException( + "Error fetching router list: %s" % e) def list_subnets(self): - return self.manager.submitTask(_tasks.SubnetList())['subnets'] + try: + return self.manager.submitTask(_tasks.SubnetList())['subnets'] + except Exception as e: + self.log.debug("subnet list failed: %s" % e, exc_info=True) + raise OpenStackCloudException( + "Error fetching subnet list: %s" % e) @_cache_on_arguments(should_cache_fn=_no_pending_volumes) def list_volumes(self, cache=True): @@ -891,22 +906,31 @@ def list_volumes(self, cache=True): self.manager.submitTask(_tasks.VolumeList()) ) except Exception as e: - self.log.debug( - "cinder could not list volumes: {message}".format( - message=str(e)), - exc_info=True) - raise OpenStackCloudException("Error fetching volume list") + self.log.debug("volume list failed: %s" % e, exc_info=True) + raise OpenStackCloudException( + "Error fetching volume list: %s" % e) @_cache_on_arguments() def list_flavors(self): - return meta.obj_list_to_dict( - self.manager.submitTask(_tasks.FlavorList()) - ) + try: + return meta.obj_list_to_dict( + self.manager.submitTask(_tasks.FlavorList()) + ) + except Exception as e: + self.log.debug("flavor list failed: %s" % e, exc_info=True) + raise OpenStackCloudException( + "Error fetching flavor list: %s" % e) def list_security_groups(self): - return meta.obj_list_to_dict( - self.manager.submitTask(_tasks.SecurityGroupList()) - ) + try: + return meta.obj_list_to_dict( + self.manager.submitTask(_tasks.SecurityGroupList()) + ) + except Exception as e: + self.log.debug( + "security group list failed: %s" % e, exc_info=True) + raise OpenStackCloudException( + "Error fetching security group list: %s" % e) def list_servers(self): try: @@ -941,13 +965,23 @@ def list_images(self, filter_deleted=True): else: # glanceclient returns a normal object if you use v1 image_list = meta.obj_list_to_dict(image_list) - except (OpenStackCloudException, - glanceclient.exc.HTTPInternalServerError): + + except glanceclient.exc.HTTPInternalServerError: # We didn't have glance, let's try nova # If this doesn't work - we just let the exception propagate - image_list = meta.obj_list_to_dict( - self.manager.submitTask(_tasks.NovaImageList()) - ) + try: + image_list = meta.obj_list_to_dict( + self.manager.submitTask(_tasks.NovaImageList()) + ) + except Exception as e: + self.log.debug("nova image list failed: %s" % e, exc_info=True) + raise OpenStackCloudException( + "Error fetching image list: %s" % e) + + except Exception as e: + self.log.debug("glance image list failed: %s" % e, exc_info=True) + raise OpenStackCloudException( + "Error fetching image list: %s" % e) for image in image_list: # The cloud might return DELETED for invalid images. From 88a12a98fbab037b01e95a92bc79f44a9e9cc25d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 9 May 2015 09:43:29 -0400 Subject: [PATCH 0264/3836] Make ironic use the API version system The other services honor settings for API versions. Make Ironic comply. Change-Id: I736785896d6d24e18b680d07c82b9922f9d39447 --- shade/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index 68f4928a2..9521707d2 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -307,6 +307,9 @@ def get_service_name(self, service): def _get_nova_api_version(self): return self.api_versions['compute'] + def _get_ironic_api_version(self): + return self.api_versions.get('baremetal', '1') + @property def nova_client(self): if self._nova_client is None: @@ -2245,7 +2248,7 @@ def ironic_client(self): endpoint = self.get_session_endpoint(service_key='baremetal') try: self._ironic_client = ironic_client.Client( - '1', endpoint, token=token, + self._get_ironic_api_version(), endpoint, token=token, timeout=self.api_timeout) except Exception as e: self.log.debug("ironic auth failed", exc_info=True) From 1cb9b2094a0a2de0afb5c14a44cabf5e2b181cbe Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 10 May 2015 15:05:09 -0400 Subject: [PATCH 0265/3836] Support PUT in Image v2 API Not only is there a difference between put and task uploads, there is also a difference between v1 put and v2 put. Of course there is. Co-Authored-By: David Shrewsbury Change-Id: I63f8dd4279cf242da7e76018580479968bc9aaef --- shade/__init__.py | 29 +++++++++++++++++++++++++++-- shade/_tasks.py | 5 +++++ shade/tests/unit/test_caching.py | 23 ++++++++++++++++++++++- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 68f4928a2..4aca45099 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -15,6 +15,7 @@ import hashlib import logging import operator +import os import time from cinderclient.v1 import client as cinder_client @@ -1264,11 +1265,35 @@ def create_image( return self._upload_image_put(name, filename, **image_kwargs) - def _upload_image_put(self, name, filename, **image_kwargs): + def _upload_image_put_v2(self, name, image_data, **image_kwargs): + if 'properties' in image_kwargs: + img_props = image_kwargs.pop('properties') + for k, v in iter(img_props.items()): + image_kwargs[k] = str(v) + image = self.manager.submitTask(_tasks.ImageCreate( + name=name, **image_kwargs)) + curr = image_data.tell() + image_data.seek(0, os.SEEK_END) + data_size = image_data.tell() + image_data.seek(curr) + self.manager.submitTask(_tasks.ImageUpload( + image_id=image.id, image_data=image_data, image_size=data_size)) + return image + + def _upload_image_put_v1(self, name, image_data, **image_kwargs): image = self.manager.submitTask(_tasks.ImageCreate( name=name, **image_kwargs)) self.manager.submitTask(_tasks.ImageUpdate( - image=image, data=open(filename, 'rb'))) + image=image, data=image_data)) + return image + + def _upload_image_put(self, name, filename, **image_kwargs): + image_data = open(filename, 'rb') + # Because reasons and crying bunnies + if self.api_versions['image'] == '2': + image = self._upload_image_put_v2(name, image_data, **image_kwargs) + else: + image = self._upload_image_put_v1(name, image_data, **image_kwargs) self._cache.invalidate() return self.get_image(image.id) diff --git a/shade/_tasks.py b/shade/_tasks.py index 4084a1f46..49d6f7d67 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -162,6 +162,11 @@ def main(self, client): client.glance_client.images.update(**self.args) +class ImageUpload(task_manager.Task): + def main(self, client): + client.glance_client.images.upload(**self.args) + + class VolumeCreate(task_manager.Task): def main(self, client): return client.cinder_client.volumes.create(**self.args) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 0f021c77c..c652bfb58 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -262,7 +262,7 @@ def _call_create_image(self, name, container=None): name, imagefile.name, container=container, wait=True) @mock.patch.object(shade.OpenStackCloud, 'glance_client') - def test_create_image_put(self, glance_mock): + def test_create_image_put_v1(self, glance_mock): self.cloud.api_versions['image'] = '1' glance_mock.images.list.return_value = [] self.assertEqual([], self.cloud.list_images()) @@ -280,6 +280,27 @@ def test_create_image_put(self, glance_mock): fake_image_dict = meta.obj_to_dict(fake_image) self.assertEqual([fake_image_dict], self.cloud.list_images()) + @mock.patch.object(shade.OpenStackCloud, 'glance_client') + def test_create_image_put_v2(self, glance_mock): + self.cloud.api_versions['image'] = '2' + self.cloud.image_api_use_tasks = False + + glance_mock.images.list.return_value = [] + self.assertEqual([], self.cloud.list_images()) + + fake_image = fakes.FakeImage('42', '42 name', 'success') + glance_mock.images.create.return_value = fake_image + glance_mock.images.list.return_value = [fake_image] + self._call_create_image('42 name') + args = {'name': '42 name', + 'owner_specified.shade.md5': mock.ANY, + 'owner_specified.shade.sha256': mock.ANY} + glance_mock.images.create.assert_called_with(**args) + glance_mock.images.upload.assert_called_with( + image_data=mock.ANY, image_id=fake_image.id, image_size=1) + fake_image_dict = meta.obj_to_dict(fake_image) + self.assertEqual([fake_image_dict], self.cloud.list_images()) + @mock.patch.object(shade.OpenStackCloud, 'glance_client') @mock.patch.object(shade.OpenStackCloud, 'swift_client') def test_create_image_task(self, swift_mock, glance_mock): From 1dc1d25b8e1510e910f62a7ebb981af0298e336c Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Wed, 13 May 2015 14:11:59 -0700 Subject: [PATCH 0266/3836] Handle novaclient exception in delete_server wait When a wait is requested, we poll the nova server with the ID of the deleted server until it disappears. We might get an error on that poll, and we should wrap that exception to avoid leaking client exceptions. Change-Id: I6474bf37835728f70210ad29a3757a4488dfe037 --- shade/__init__.py | 5 ++++ shade/tests/unit/test_delete_server.py | 38 ++++++++++++++++++++------ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 663b06045..7a99debe6 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1769,6 +1769,11 @@ def delete_server(self, name, wait=False, timeout=180): return except nova_exceptions.NotFound: return + except Exception as e: + self.log.debug("nova get server failed when waiting for " + "delete", exc_info=True) + raise OpenStackCloudException( + "Error in deleting server: {0}".format(e)) def get_container(self, name, skip_cache=False): if skip_cache or name not in self._container_cache: diff --git a/shade/tests/unit/test_delete_server.py b/shade/tests/unit/test_delete_server.py index 0b8c18e3b..0ea2b4902 100644 --- a/shade/tests/unit/test_delete_server.py +++ b/shade/tests/unit/test_delete_server.py @@ -29,6 +29,14 @@ class TestDeleteServer(base.TestCase): + novaclient_exceptions = (nova_exc.BadRequest, + nova_exc.Unauthorized, + nova_exc.Forbidden, + nova_exc.MethodNotAllowed, + nova_exc.Conflict, + nova_exc.OverLimit, + nova_exc.RateLimit, + nova_exc.HTTPNotImplemented) def setUp(self): super(TestDeleteServer, self).setUp() @@ -89,14 +97,7 @@ def test_delete_server_fails(self, nova_mock): nova_mock.servers.list.return_value = [fakes.FakeServer('1212', 'speedy', 'ACTIVE')] - for fail in (nova_exc.BadRequest, - nova_exc.Unauthorized, - nova_exc.Forbidden, - nova_exc.MethodNotAllowed, - nova_exc.Conflict, - nova_exc.OverLimit, - nova_exc.RateLimit, - nova_exc.HTTPNotImplemented): + for fail in self.novaclient_exceptions: def _raise_fail(server): raise fail(code=fail.http_status) @@ -108,3 +109,24 @@ def _raise_fail(server): # Note that message is deprecated from Exception, but not in # the novaclient exceptions. self.assertIn(fail.message, str(exc)) + + @mock.patch('shade.OpenStackCloud.nova_client') + def test_delete_server_get_fails(self, nova_mock): + """ + Test that delete_server wraps novaclient exceptions on wait fails + """ + nova_mock.servers.list.return_value = [fakes.FakeServer('2000', + 'yosemite', + 'ACTIVE')] + for fail in self.novaclient_exceptions: + + def _raise_fail(server): + raise fail(code=fail.http_status) + + nova_mock.servers.get.side_effect = _raise_fail + exc = self.assertRaises(shade_exc.OpenStackCloudException, + self.cloud.delete_server, 'yosemite', + wait=True) + # Note that message is deprecated from Exception, but not in + # the novaclient exceptions. + self.assertIn(fail.message, str(exc)) From f57433929a9d51fd7927ed43a639a1d8917695a4 Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Tue, 26 May 2015 11:48:21 -0700 Subject: [PATCH 0267/3836] Make caching work when cloud name is None All of the other keys are coerced to strings through some method but a cloud name of None with no namespace produces a TypeError because sequence 0 is not a string when joined. Change-Id: I78d0e6daedfe3cf1a0db5f606c21af803450f018 --- shade/__init__.py | 2 +- shade/tests/unit/test_caching.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index 4aca45099..5427a6c21 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -295,7 +295,7 @@ def generate_key(*args, **kwargs): kwargs_key = ','.join( ['%s:%s' % (k, kwargs[k]) for k in kw_keys if k != 'cache']) ans = "_".join( - [name_key, fname, arg_key, kwargs_key]) + [str(name_key), fname, arg_key, kwargs_key]) return ans return generate_key diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index c652bfb58..9c65ced13 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -361,3 +361,24 @@ class FakeTask(dict): glance_mock.images.update.assert_called_with(**args) fake_image_dict = meta.obj_to_dict(fake_image) self.assertEqual([fake_image_dict], self.cloud.list_images()) + + @mock.patch.object(shade.OpenStackCloud, 'glance_client') + def test_cache_no_cloud_name(self, glance_mock): + class FakeImage(dict): + id = 1 + status = 'active' + name = 'None Test Image' + fi = FakeImage(id=FakeImage.id, status=FakeImage.status, + name=FakeImage.name) + glance_mock.images.list.return_value = [fi] + self.cloud.name = None + self.assertEqual([fi], [dict(x) for x in self.cloud.list_images()]) + # Now test that the list was cached + fi2 = FakeImage(id=2, status=FakeImage.status, name=FakeImage.name) + fi2.id = 2 + glance_mock.images.list.return_value = [fi, fi2] + self.assertEqual([fi], [dict(x) for x in self.cloud.list_images()]) + # Invalidation too + self.cloud.list_images.invalidate(self.cloud) + self.assertEqual( + [fi, fi2], [dict(x) for x in self.cloud.list_images()]) From 39eefc1989ff4e51db8716dd7106967b14fee12a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 26 May 2015 18:05:57 -0400 Subject: [PATCH 0268/3836] Make sure glance image list actually runs in Tasks images.list() does not actually talk to an API. So putting it in TaskManager actually takes an execution slot that it does not need. On the other hand, the follow up list expansion DOES talk to the API. So put it in the Task, since it's the evil thing. Change-Id: I7f30ba908552fbecaedd364ecf20b19e096b03d9 --- shade/__init__.py | 9 ++++++--- shade/_tasks.py | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 5427a6c21..59fa88611 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -953,11 +953,14 @@ def list_images(self, filter_deleted=True): # First, try to actually get images from glance, it's more efficient images = [] try: - # If the cloud does not expose the glance API publically - image_gen = self.manager.submitTask(_tasks.GlanceImageList()) + # Creates a generator - does not actually talk to the cloud API + # hardcoding page size for now. We'll have to get MUCH smarter + # if we want to deal with page size per unit of rate limiting + image_gen = self.glance_client.images.list(page_size=1000) # Deal with the generator to make a list - image_list = [image for image in image_gen] + image_list = self.manager.submitTask( + _tasks.GlanceImageList(image_gen=image_gen)) if image_list: if getattr(image_list[0], 'validate', None): diff --git a/shade/_tasks.py b/shade/_tasks.py index 49d6f7d67..3dc051c0f 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -124,7 +124,7 @@ def main(self, client): class GlanceImageList(task_manager.Task): def main(self, client): - return client.glance_client.images.list() + return [image for image in self.args['image_gen']] class NovaImageList(task_manager.Task): From be3ddcd09b7713d3651020d87a9d3295a007efd8 Mon Sep 17 00:00:00 2001 From: Clark Boylan Date: Tue, 26 May 2015 16:01:07 -0700 Subject: [PATCH 0269/3836] Don't cache keystone tokens as KSC does it for us We don't need to keep track of what our current valid keystone token is becauase keystone client does it for us. Instead defer to ksc.session.get_token() to either reuse a valid token or make a new one for us if necessary. Change-Id: I23f90b6292f32ce9cef962bdc641c291e4d2fe4d --- shade/__init__.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 5427a6c21..5a8089838 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -263,7 +263,6 @@ def __init__(self, cloud, auth, self._file_hash_cache = dict() self._keystone_session = None - self._auth_token = None self._cinder_client = None self._glance_client = None @@ -390,9 +389,9 @@ def service_catalog(self): @property def auth_token(self): - if not self._auth_token: - self._auth_token = self.keystone_session.get_token() - return self._auth_token + # Keystone's session will reuse a token if it is still valid. + # We don't need to track validity here, just get_token() each time. + return self.keystone_session.get_token() @property def project_cache(self): @@ -2246,10 +2245,13 @@ def __init__(self, *args, **kwargs): @property def auth_token(self): if self.auth_type in (None, "None", ''): - return self._auth_token - if not self._auth_token: - self._auth_token = self.keystone_session.get_token() - return self._auth_token + # Ironic can operate in no keystone mode. Signify this with a + # token of None. + return None + else: + # Keystone's session will reuse a token if it is still valid. + # We don't need to track validity here, just get_token() each time. + return self.keystone_session.get_token() @property def ironic_client(self): From 24385adb7a8829885f0172e0cd3607e5dba9a594 Mon Sep 17 00:00:00 2001 From: Clark Boylan Date: Tue, 26 May 2015 16:05:10 -0700 Subject: [PATCH 0270/3836] Always refresh glanceclient for tokens validity Glanceclient doesn't refresh tokens on its own. Work around this by always making a new glanceclient with a potentialy new token if the existing token is near expiration. This adds some overhead to our use of glance but that is preferable to having glanceclient break because its token has expired. Change-Id: If57531a72eb90ee7bc6e67905ddfd5bda9bb6f1b --- shade/__init__.py | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 5a8089838..64095ef75 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -556,25 +556,29 @@ def _get_glance_api_version(self): @property def glance_client(self): - if self._glance_client is None: - token = self.auth_token - endpoint = self.get_session_endpoint('image') - glance_api_version = self._get_glance_api_version() - kwargs = dict() - if self.api_timeout is not None: - kwargs['timeout'] = self.api_timeout - try: - self._glance_client = glanceclient.Client( - glance_api_version, endpoint, token=token, - session=self.keystone_session, - **kwargs) - except Exception as e: - self.log.debug("glance unknown issue", exc_info=True) - raise OpenStackCloudException( - "Error in connecting to glance: %s" % str(e)) + # Note that glanceclient doesn't use keystoneclient sessions + # which means that it won't make a new token if the old one has + # expired. Work around that by always making a new glanceclient here + # which may create a new token if the current one is close to + # expiration. + token = self.auth_token + endpoint = self.get_session_endpoint('image') + glance_api_version = self._get_glance_api_version() + kwargs = dict() + if self.api_timeout is not None: + kwargs['timeout'] = self.api_timeout + try: + self._glance_client = glanceclient.Client( + glance_api_version, endpoint, token=token, + session=self.keystone_session, + **kwargs) + except Exception as e: + self.log.debug("glance unknown issue", exc_info=True) + raise OpenStackCloudException( + "Error in connecting to glance: %s" % str(e)) - if not self._glance_client: - raise OpenStackCloudException("Error connecting to glance") + if not self._glance_client: + raise OpenStackCloudException("Error connecting to glance") return self._glance_client @property From 434d51aac292609162f738eea4611e2a52df8a0f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 26 May 2015 14:49:56 -0400 Subject: [PATCH 0271/3836] Don't pass None as the cloud name cloud names want to be strings. If there is no cloud name, stringify None. Change-Id: I4fa5bb8ac03baf464cfa00b451627f63ff847fdf --- os_client_config/config.py | 6 +++++- os_client_config/tests/test_config.py | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 88e4b4c2e..eb7b5c07b 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -404,8 +404,12 @@ def get_one_cloud(self, cloud=None, validate=True, if hasattr(value, 'format'): config[key] = value.format(**config) + if cloud is None: + cloud_name = '' + else: + cloud_name = str(cloud) return cloud_config.CloudConfig( - name=cloud, region=config['region_name'], config=config) + name=cloud_name, region=config['region_name'], config=config) if __name__ == '__main__': config = OpenStackConfig().get_all_clouds() diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index d196a8691..bb815293b 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -29,7 +29,9 @@ class TestConfig(base.TestCase): def test_get_one_cloud(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - self.assertIsInstance(c.get_one_cloud(), cloud_config.CloudConfig) + cloud = c.get_one_cloud() + self.assertIsInstance(cloud, cloud_config.CloudConfig) + self.assertEqual(cloud.name, '') def test_get_one_cloud_auth_defaults(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml]) From b51f9f841651710ed088867b9047a7311e39cdea Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 27 May 2015 08:01:40 -0400 Subject: [PATCH 0272/3836] Rename cloud to profile The cloud parameter was confusing people, especially since it was inside of a dict that was named "clouds". profile was suggested as less confusing, which seems fine. Continue processing cloud as a parameter so that we don't break anyway, but change the docs to only mention profile. Change-Id: Idf3d089703985ecc60f23a3c780ddcab914aa678 --- README.rst | 16 ++++++++-------- os_client_config/config.py | 13 +++++++------ os_client_config/tests/base.py | 4 ++-- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/README.rst b/README.rst index 4389b680b..193f0a112 100644 --- a/README.rst +++ b/README.rst @@ -82,7 +82,7 @@ An example config file is probably helpful: clouds: mordred: - cloud: hp + profile: hp auth: username: mordred@inaugust.com password: XXXXXXXXX @@ -99,7 +99,7 @@ An example config file is probably helpful: region_name: region-b.geo-1 dns_service_type: hpext:dns infra: - cloud: rackspace + profile: rackspace auth: username: openstackci password: XXXXXXXX @@ -107,11 +107,11 @@ An example config file is probably helpful: region_name: DFW,ORD,IAD You may note a few things. First, since auth_url settings are silly -and embarrasingly ugly, known cloud vendors are included and may be referrenced -by name. One of the benefits of that is that auth_url isn't the only thing -the vendor defaults contain. For instance, since Rackspace lists -`rax:database` as the service type for trove, os-client-config knows that -so that you don't have to. +and embarrasingly ugly, known cloud vendor profile information is included and +may be referrenced by name. One of the benefits of that is that auth_url +isn't the only thing the vendor defaults contain. For instance, since +Rackspace lists `rax:database` as the service type for trove, os-client-config +knows that so that you don't have to. Also, region_name can be a list of regions. When you call get_all_clouds, you'll get a cloud config object for each cloud/region combo. @@ -159,7 +159,7 @@ are connecting to OpenStack can share a cache should you desire. - 127.0.0.1 clouds: mordred: - cloud: hp + profile: hp auth: username: mordred@inaugust.com password: XXXXXXXXX diff --git a/os_client_config/config.py b/os_client_config/config.py index eb7b5c07b..8fc1644cf 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -202,15 +202,16 @@ def _get_base_cloud_config(self, name): # Get the defaults cloud.update(self.defaults) - # yes, I know the next line looks silly - if 'cloud' in our_cloud: - cloud_name = our_cloud['cloud'] + # Expand a profile if it exists. 'cloud' is an old confusing name + # for this. + profile_name = our_cloud.get('profile', our_cloud.get('cloud', None)) + if profile_name: vendor_file = self._load_vendor_file() - if vendor_file and cloud_name in vendor_file['public-clouds']: - _auth_update(cloud, vendor_file['public-clouds'][cloud_name]) + if vendor_file and profile_name in vendor_file['public-clouds']: + _auth_update(cloud, vendor_file['public-clouds'][profile_name]) else: try: - _auth_update(cloud, vendors.CLOUD_DEFAULTS[cloud_name]) + _auth_update(cloud, vendors.CLOUD_DEFAULTS[profile_name]) except KeyError: # Can't find the requested vendor config, go about business pass diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py index 1169051f8..b6834bf8c 100644 --- a/os_client_config/tests/base.py +++ b/os_client_config/tests/base.py @@ -40,7 +40,7 @@ USER_CONF = { 'clouds': { '_test_cloud_': { - 'cloud': '_test_cloud_in_our_cloud', + 'profile': '_test_cloud_in_our_cloud', 'auth': { 'username': 'testuser', 'password': 'testpass', @@ -48,7 +48,7 @@ 'region_name': 'test-region', }, '_test_cloud_no_vendor': { - 'cloud': '_test_non_existant_cloud', + 'profile': '_test_non_existant_cloud', 'auth': { 'username': 'testuser', 'password': 'testpass', From 353875b55e1c527a19a23e9e52a7514e84f6e9c8 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 27 May 2015 11:48:18 -0400 Subject: [PATCH 0273/3836] Set metadata headers on object create It's a common swift deployment pattern for an update to be a server-side object copy, which is quite expensive. Rather than doing a second update step, just set the headers when we upload the image. Change-Id: Ic4f5ddddfc403f21eba5972b24a3968ae3ce0a58 --- shade/__init__.py | 14 +++++++------- shade/tests/unit/test_caching.py | 6 +----- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 2b6e2c5bc..d5941a77b 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1970,6 +1970,11 @@ def create_object( if not filename: filename = name + if not md5 or not sha256: + (md5, sha256) = self._get_file_hashes(filename) + headers[OBJECT_MD5_KEY] = md5 + headers[OBJECT_SHA256_KEY] = sha256 + # On some clouds this is not necessary. On others it is. I'm confused. self.create_container(container) @@ -1979,13 +1984,8 @@ def create_object( "swift uploading {filename} to {container}/{name}".format( filename=filename, container=container, name=name)) self.manager.submitTask(_tasks.ObjectCreate( - container=container, obj=name, contents=fileobj)) - - (md5, sha256) = self._get_file_hashes(filename) - headers[OBJECT_MD5_KEY] = md5 - headers[OBJECT_SHA256_KEY] = sha256 - self.manager.submitTask(_tasks.ObjectUpdate( - container=container, obj=name, headers=headers)) + container=container, obj=name, contents=fileobj, + headers=headers)) def delete_object(self, container, name): if not self.get_object_metadata(container, name): diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 9c65ced13..b9381e3aa 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -347,11 +347,7 @@ class FakeTask(dict): 'x-object-meta-x-shade-sha256': mock.ANY}, 'obj': '99 name', 'container': 'image_upload_v2_test_container'} - swift_mock.post_object.assert_called_with(**args) - swift_mock.put_object.assert_called_with( - contents=mock.ANY, - obj='99 name', - container='image_upload_v2_test_container') + swift_mock.put_object.assert_called_with(contents=mock.ANY, **args) glance_mock.tasks.create.assert_called_with(type='import', input={ 'import_from': 'image_upload_v2_test_container/99 name', 'image_properties': {'name': '99 name'}}) From a5dd46af2ede29580450ffad46ad1888cca4a9ac Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 27 May 2015 08:04:39 -0400 Subject: [PATCH 0274/3836] Expose method for getting a list of cloud names Knowing what cloud names os-client-config found can be useful for introspection or investigation. Change-Id: I77ab133634236d2a7a59ea805a6d650854165030 --- os_client_config/config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 8fc1644cf..813d0a139 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -185,7 +185,7 @@ def _get_regions(self, cloud): def _get_region(self, cloud=None): return self._get_regions(cloud).split(',')[0] - def _get_cloud_sections(self): + def get_cloud_names(self): return self.cloud_config['clouds'].keys() def _get_base_cloud_config(self, name): @@ -267,7 +267,7 @@ def get_all_clouds(self): clouds = [] - for cloud in self._get_cloud_sections(): + for cloud in self.get_cloud_names(): for region in self._get_regions(cloud).split(','): clouds.append(self.get_one_cloud(cloud, region_name=region)) return clouds @@ -370,7 +370,7 @@ def get_one_cloud(self, cloud=None, validate=True, :param kwargs: Additional configuration options """ - if cloud is None and self.envvar_key in self._get_cloud_sections(): + if cloud is None and self.envvar_key in self.get_cloud_names(): cloud = self.envvar_key args = self._fix_args(kwargs, argparse=argparse) From f3eb3d47bc51c5c4dbf8a609c27e99cf9abaca53 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 27 May 2015 08:14:47 -0400 Subject: [PATCH 0275/3836] Normalize all keys down to _ instead of - It's really common to use the config dict in a **kwargs context. Therefore, normalize every key with a - to use _ instead. Testing for this is done by changing one of the base region_name args to be region-name. Change-Id: Ibd834f7a70cf2285619b5499492858b21635ba57 --- os_client_config/config.py | 16 +++++++++++++--- os_client_config/tests/base.py | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 813d0a139..c5b6d9bb3 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -161,7 +161,17 @@ def _load_yaml_file(self, filelist): for path in filelist: if os.path.exists(path): with open(path, 'r') as f: - return yaml.safe_load(f) + return self._normalize_keys(yaml.safe_load(f)) + + def _normalize_keys(self, config): + new_config = {} + for key, value in config.items(): + key = key.replace('-', '_') + if isinstance(value, dict): + new_config[key] = self._normalize_keys(value) + else: + new_config[key] = value + return new_config def get_cache_max_age(self): return self._cache_max_age @@ -207,8 +217,8 @@ def _get_base_cloud_config(self, name): profile_name = our_cloud.get('profile', our_cloud.get('cloud', None)) if profile_name: vendor_file = self._load_vendor_file() - if vendor_file and profile_name in vendor_file['public-clouds']: - _auth_update(cloud, vendor_file['public-clouds'][profile_name]) + if vendor_file and profile_name in vendor_file['public_clouds']: + _auth_update(cloud, vendor_file['public_clouds'][profile_name]) else: try: _auth_update(cloud, vendors.CLOUD_DEFAULTS[profile_name]) diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py index b6834bf8c..81b499556 100644 --- a/os_client_config/tests/base.py +++ b/os_client_config/tests/base.py @@ -54,7 +54,7 @@ 'password': 'testpass', 'project_name': 'testproject', }, - 'region_name': 'test-region', + 'region-name': 'test-region', }, }, 'cache': {'max_age': 1}, From 5a4f809caf2223121803debab2e6ac52bc943116 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 27 May 2015 08:46:21 -0400 Subject: [PATCH 0276/3836] Capture the filename used for config Consumers who may want to watch for config file changes would find such a thing interesting information. Change-Id: I66c59c6acdbb930f013d4742d5b3cc7e35a922d4 --- os_client_config/config.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index c5b6d9bb3..b9c89c296 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -108,7 +108,7 @@ def __init__(self, config_files=None, vendor_files=None, self.defaults.update(override_defaults) # First, use a config file if it exists where expected - self.cloud_config = self._load_config_file() + self.config_filename, self.cloud_config = self._load_config_file() if not self.cloud_config: self.cloud_config = {'clouds': {}} @@ -161,7 +161,8 @@ def _load_yaml_file(self, filelist): for path in filelist: if os.path.exists(path): with open(path, 'r') as f: - return self._normalize_keys(yaml.safe_load(f)) + return path, self._normalize_keys(yaml.safe_load(f)) + return (None, None) def _normalize_keys(self, config): new_config = {} @@ -216,7 +217,7 @@ def _get_base_cloud_config(self, name): # for this. profile_name = our_cloud.get('profile', our_cloud.get('cloud', None)) if profile_name: - vendor_file = self._load_vendor_file() + vendor_filename, vendor_file = self._load_vendor_file() if vendor_file and profile_name in vendor_file['public_clouds']: _auth_update(cloud, vendor_file['public_clouds'][profile_name]) else: From 5c60aad725b0b98008ee467c5130931339c12d48 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 27 May 2015 08:50:32 -0400 Subject: [PATCH 0277/3836] Add an equality method for CloudConfig In order to track if a config has changed, we need to be able to compare the CloudConfig objects for equality. Change-Id: Icdd9acede81bc5fba60d877194048e24a62c9e5d --- os_client_config/cloud_config.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index c17bfc2b7..d893b0b5f 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -32,3 +32,7 @@ def __getattr__(self, key): def __iter__(self): return self.config.__iter__() + + def __eq__(self, other): + return (self.name == other.name and self.region == other.region + and self.config == other.config) From 9b98ee0e098cc33eb976a9d5e917d10a0ad7968c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 27 May 2015 15:52:54 -0400 Subject: [PATCH 0278/3836] Add inequality method One method does not imply the other. Change-Id: Ifcd39ee188d88d9490f67b5644a941d4d1c6ec38 --- os_client_config/cloud_config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index d893b0b5f..347eb7c48 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -36,3 +36,6 @@ def __iter__(self): def __eq__(self, other): return (self.name == other.name and self.region == other.region and self.config == other.config) + + def __ne__(self, other): + return not self == other From 2580c0aa0112d94606879c8f8864e34602ddaab8 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 27 May 2015 16:09:49 -0400 Subject: [PATCH 0279/3836] Add tests for cloud config comparison Unit tests are good. Change-Id: Iaad73898daf6b00839be5a134558d53b95f0dd65 --- os_client_config/tests/test_cloud_config.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index 36386e50e..1f79b3e71 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -42,3 +42,20 @@ def test_iteration(self): cc = cloud_config.CloudConfig("test1", "region-al", fake_config_dict) self.assertTrue('a' in cc) self.assertFalse('x' in cc) + + def test_equality(self): + cc1 = cloud_config.CloudConfig("test1", "region-al", fake_config_dict) + cc2 = cloud_config.CloudConfig("test1", "region-al", fake_config_dict) + self.assertEqual(cc1, cc2) + + def test_inequality(self): + cc1 = cloud_config.CloudConfig("test1", "region-al", fake_config_dict) + + cc2 = cloud_config.CloudConfig("test2", "region-al", fake_config_dict) + self.assertNotEqual(cc1, cc2) + + cc2 = cloud_config.CloudConfig("test1", "region-xx", fake_config_dict) + self.assertNotEqual(cc1, cc2) + + cc2 = cloud_config.CloudConfig("test1", "region-al", {}) + self.assertNotEqual(cc1, cc2) From 88c5e08599d70bde80abea45404ead57d5a17388 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 27 May 2015 22:44:08 -0400 Subject: [PATCH 0280/3836] Don't normalize too deeply We were normalizing the entire clouds.yaml output - but the keys underneath clouds: are names of clouds. We should not touch them. Instead - normalize the keys of the config dict we return. Change-Id: I792475d701d31201a9be6d54e066227d97b7ce5f --- os_client_config/config.py | 9 +++++---- os_client_config/tests/base.py | 2 +- os_client_config/tests/test_config.py | 14 +++++++------- os_client_config/tests/test_environ.py | 4 ++-- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index b9c89c296..7b52912ed 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -161,7 +161,7 @@ def _load_yaml_file(self, filelist): for path in filelist: if os.path.exists(path): with open(path, 'r') as f: - return path, self._normalize_keys(yaml.safe_load(f)) + return path, yaml.safe_load(f) return (None, None) def _normalize_keys(self, config): @@ -218,8 +218,8 @@ def _get_base_cloud_config(self, name): profile_name = our_cloud.get('profile', our_cloud.get('cloud', None)) if profile_name: vendor_filename, vendor_file = self._load_vendor_file() - if vendor_file and profile_name in vendor_file['public_clouds']: - _auth_update(cloud, vendor_file['public_clouds'][profile_name]) + if vendor_file and profile_name in vendor_file['public-clouds']: + _auth_update(cloud, vendor_file['public-clouds'][profile_name]) else: try: _auth_update(cloud, vendors.CLOUD_DEFAULTS[profile_name]) @@ -421,7 +421,8 @@ def get_one_cloud(self, cloud=None, validate=True, else: cloud_name = str(cloud) return cloud_config.CloudConfig( - name=cloud_name, region=config['region_name'], config=config) + name=cloud_name, region=config['region_name'], + config=self._normalize_keys(config)) if __name__ == '__main__': config = OpenStackConfig().get_all_clouds() diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py index 81b499556..6a412bfd6 100644 --- a/os_client_config/tests/base.py +++ b/os_client_config/tests/base.py @@ -39,7 +39,7 @@ } USER_CONF = { 'clouds': { - '_test_cloud_': { + '_test-cloud_': { 'profile': '_test_cloud_in_our_cloud', 'auth': { 'username': 'testuser', diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index bb815293b..5a7fc1a94 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -35,7 +35,7 @@ def test_get_one_cloud(self): def test_get_one_cloud_auth_defaults(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml]) - cc = c.get_one_cloud(cloud='_test_cloud_', auth={'username': 'user'}) + cc = c.get_one_cloud(cloud='_test-cloud_', auth={'username': 'user'}) self.assertEqual('user', cc.auth['username']) self.assertEqual( defaults._defaults['auth_type'], @@ -50,7 +50,7 @@ def test_get_one_cloud_auth_override_defaults(self): default_options = {'auth_type': 'token'} c = config.OpenStackConfig(config_files=[self.cloud_yaml], override_defaults=default_options) - cc = c.get_one_cloud(cloud='_test_cloud_', auth={'username': 'user'}) + cc = c.get_one_cloud(cloud='_test-cloud_', auth={'username': 'user'}) self.assertEqual('user', cc.auth['username']) self.assertEqual('token', cc.auth_type) self.assertEqual( @@ -66,7 +66,7 @@ def test_get_one_cloud_with_config_files(self): self.assertIsInstance(c.cloud_config['cache'], dict) self.assertIn('max_age', c.cloud_config['cache']) self.assertIn('path', c.cloud_config['cache']) - cc = c.get_one_cloud('_test_cloud_') + cc = c.get_one_cloud('_test-cloud_') self._assert_cloud_details(cc) cc = c.get_one_cloud('_test_cloud_no_vendor') self._assert_cloud_details(cc) @@ -87,7 +87,7 @@ def test_fallthrough(self): def test_get_one_cloud_auth_merge(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml]) - cc = c.get_one_cloud(cloud='_test_cloud_', auth={'username': 'user'}) + cc = c.get_one_cloud(cloud='_test-cloud_', auth={'username': 'user'}) self.assertEqual('user', cc.auth['username']) self.assertEqual('testpass', cc.auth['password']) @@ -106,7 +106,7 @@ def test_get_one_cloud_argparse(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud(cloud='_test_cloud_', argparse=self.options) + cc = c.get_one_cloud(cloud='_test-cloud_', argparse=self.options) self._assert_cloud_details(cc) self.assertEqual(cc.region_name, 'other-test-region') self.assertEqual(cc.snack_type, 'cookie') @@ -125,7 +125,7 @@ def test_get_one_cloud_no_argparse(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud(cloud='_test_cloud_', argparse=None) + cc = c.get_one_cloud(cloud='_test-cloud_', argparse=None) self._assert_cloud_details(cc) self.assertEqual(cc.region_name, 'test-region') self.assertIsNone(cc.snack_type) @@ -136,6 +136,6 @@ class TestConfigDefault(base.TestCase): def test_set_no_default(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud(cloud='_test_cloud_', argparse=None) + cc = c.get_one_cloud(cloud='_test-cloud_', argparse=None) self._assert_cloud_details(cc) self.assertEqual(cc.auth_type, 'password') diff --git a/os_client_config/tests/test_environ.py b/os_client_config/tests/test_environ.py index e60c73b31..365ad6780 100644 --- a/os_client_config/tests/test_environ.py +++ b/os_client_config/tests/test_environ.py @@ -59,7 +59,7 @@ def test_environ_exists(self): self.assertNotIn('auth_url', cc.config) self.assertIn('auth_url', cc.config['auth']) self.assertNotIn('auth_url', cc.config) - cc = c.get_one_cloud('_test_cloud_') + cc = c.get_one_cloud('_test-cloud_') self._assert_cloud_details(cc) cc = c.get_one_cloud('_test_cloud_no_vendor') self._assert_cloud_details(cc) @@ -72,7 +72,7 @@ def test_get_one_cloud_with_config_files(self): self.assertIsInstance(c.cloud_config['cache'], dict) self.assertIn('max_age', c.cloud_config['cache']) self.assertIn('path', c.cloud_config['cache']) - cc = c.get_one_cloud('_test_cloud_') + cc = c.get_one_cloud('_test-cloud_') self._assert_cloud_details(cc) cc = c.get_one_cloud('_test_cloud_no_vendor') self._assert_cloud_details(cc) From 6daed957c3647efb3d47d7e85065a33a74e39047 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 22 May 2015 11:25:37 -0700 Subject: [PATCH 0281/3836] Add flag to indicate handling of security groups Security groups can be handled by either nova, neutron, or just not be supported. This adds a flag for that. Values will be one of 'neutron' (the default), 'nova', or None. None indicates that security groups are not supported. Change-Id: I81a2e10e14e53acc9ffc328771d8ef721e2fd370 --- doc/source/vendor-support.rst | 6 ++++++ os_client_config/defaults.py | 1 + os_client_config/vendors.py | 1 + 3 files changed, 8 insertions(+) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index dea27d13d..02e093729 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -24,6 +24,7 @@ region-b.geo-1 US East * Image API Version is 1 * Images must be in `qcow2` format * Floating IPs are provided by Neutron +* Security groups are provided by Neutron Rackspace --------- @@ -46,6 +47,7 @@ HKG Hong Kong * Images must be in `vhd` format * Images must be uploaded using the Glance Task Interface * Floating IPs are not needed +* Security groups are not supported Dreamhost --------- @@ -61,6 +63,7 @@ RegionOne Region One * Image API Version is 2 * Images must be in `raw` format * Floating IPs are provided by Neutron +* Security groups are provided by Neutron Vexxhost -------- @@ -76,6 +79,7 @@ ca-ymq-1 Montreal * Image API Version is 2 * Images must be in `qcow2` format * Floating IPs are not needed +* Security groups are provided by Neutron RunAbove -------- @@ -92,6 +96,7 @@ BHS-1 Beauharnois, QC * Image API Version is 2 * Images must be in `qcow2` format * Floating IPs are not needed +* Security groups are provided by Neutron UnitedStack ----------- @@ -109,3 +114,4 @@ gd1 Guangdong * Image API Version is 2 * Images must be in `raw` format * Floating IPs are not needed +* Security groups are provided by Neutron diff --git a/os_client_config/defaults.py b/os_client_config/defaults.py index 14b820949..11a94c47a 100644 --- a/os_client_config/defaults.py +++ b/os_client_config/defaults.py @@ -23,6 +23,7 @@ image_api_version='1', network_api_version='2', object_api_version='1', + secgroup_source='neutron', volume_api_version='1', ) diff --git a/os_client_config/vendors.py b/os_client_config/vendors.py index dec63dc2e..ed8d0471e 100644 --- a/os_client_config/vendors.py +++ b/os_client_config/vendors.py @@ -33,6 +33,7 @@ image_api_use_tasks=True, image_format='vhd', floating_ip_source=None, + secgroup_source=None, ), dreamhost=dict( auth=dict( From b16d6d391779cc0a1bb156f0cf4468521f4e5d4e Mon Sep 17 00:00:00 2001 From: TerryHowe Date: Wed, 27 May 2015 14:34:27 -0600 Subject: [PATCH 0282/3836] Add tests for get_cloud_names Change-Id: I21b71657d83fb25628f230a48ddca197570a38a9 --- os_client_config/tests/test_config.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 5a7fc1a94..dddaf8b0f 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -91,6 +91,18 @@ def test_get_one_cloud_auth_merge(self): self.assertEqual('user', cc.auth['username']) self.assertEqual('testpass', cc.auth['password']) + def test_get_cloud_names(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml]) + self.assertEqual(['_test-cloud_', '_test_cloud_no_vendor'], + sorted(c.get_cloud_names())) + c = config.OpenStackConfig(config_files=[self.no_yaml], + vendor_files=[self.no_yaml]) + for k in os.environ.keys(): + if k.startswith('OS_'): + self.useFixture(fixtures.EnvironmentVariable(k)) + c.get_one_cloud(cloud='defaults') + self.assertEqual(['defaults'], sorted(c.get_cloud_names())) + class TestConfigArgparse(base.TestCase): From daab3615e2884100f88a797570c1326c8139983c Mon Sep 17 00:00:00 2001 From: Gregory Haynes Date: Thu, 14 May 2015 20:01:38 +0000 Subject: [PATCH 0283/3836] Add set_one_cloud method It is useful for clients to be able to update configuration as well. By doing this in shade we can perform things like merging and do some testing rather than require clients to do it. Change-Id: Ia185847c29c10f2cc6838adf962defd80894d0db --- os_client_config/config.py | 31 ++++++++++++++++++++++ os_client_config/tests/test_config.py | 37 +++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/os_client_config/config.py b/os_client_config/config.py index 7b52912ed..b323e7028 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -424,6 +424,37 @@ def get_one_cloud(self, cloud=None, validate=True, name=cloud_name, region=config['region_name'], config=self._normalize_keys(config)) + @staticmethod + def set_one_cloud(config_file, cloud, set_config=None): + """Set a single cloud configuration. + + :param string config_file: + The path to the config file to edit. If this file does not exist + it will be created. + :param string cloud: + The name of the configuration to save to clouds.yaml + :param dict set_config: Configuration options to be set + """ + + set_config = set_config or {} + cur_config = {} + try: + with open(config_file) as fh: + cur_config = yaml.safe_load(fh) + except IOError as e: + # Not no such file + if e.errno != 2: + raise + pass + + clouds_config = cur_config.get('clouds', {}) + cloud_config = _auth_update(clouds_config.get(cloud, {}), set_config) + clouds_config[cloud] = cloud_config + cur_config['clouds'] = clouds_config + + with open(config_file, 'w') as fh: + yaml.safe_dump(cur_config, fh, default_flow_style=False) + if __name__ == '__main__': config = OpenStackConfig().get_all_clouds() for cloud in config: diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index dddaf8b0f..25e1cc199 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -13,9 +13,11 @@ # under the License. import argparse +import copy import os import fixtures +import yaml from os_client_config import cloud_config from os_client_config import config @@ -103,6 +105,41 @@ def test_get_cloud_names(self): c.get_one_cloud(cloud='defaults') self.assertEqual(['defaults'], sorted(c.get_cloud_names())) + def test_set_one_cloud_creates_file(self): + config_dir = fixtures.TempDir() + self.useFixture(config_dir) + config_path = os.path.join(config_dir.path, 'clouds.yaml') + config.OpenStackConfig.set_one_cloud(config_path, '_test_cloud_') + self.assertTrue(os.path.isfile(config_path)) + with open(config_path) as fh: + self.assertEqual({'clouds': {'_test_cloud_': {}}}, + yaml.safe_load(fh)) + + def test_set_one_cloud_updates_cloud(self): + new_config = { + 'cloud': 'new_cloud', + 'auth': { + 'password': 'newpass' + } + } + + resulting_cloud_config = { + 'auth': { + 'password': 'newpass', + 'username': 'testuser' + }, + 'cloud': 'new_cloud', + 'profile': '_test_cloud_in_our_cloud', + 'region_name': 'test-region' + } + resulting_config = copy.deepcopy(base.USER_CONF) + resulting_config['clouds']['_test-cloud_'] = resulting_cloud_config + config.OpenStackConfig.set_one_cloud(self.cloud_yaml, '_test-cloud_', + new_config) + with open(self.cloud_yaml) as fh: + written_config = yaml.safe_load(fh) + self.assertEqual(written_config, resulting_config) + class TestConfigArgparse(base.TestCase): From cb54ac54609ec6319e8bfe91adc07a63a4c37d6c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 27 May 2015 23:51:14 -0400 Subject: [PATCH 0284/3836] Update pbr version pins We've released 1.0, so we should use it. Also, you want at least 0.11. Update the hacking pin because we need new flake8 to deal with a pkg_resources issue. Change-Id: I62767ea281df94f67d453a14e9c8500aeb305e97 --- requirements.txt | 2 +- test-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 7cca7eac9..d1b745ad4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -pbr>=0.5.21,<1.0 +pbr>=0.11,<2.0 bunch jsonpatch diff --git a/test-requirements.txt b/test-requirements.txt index 0efea2b0a..d42d7d7f3 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,4 +1,4 @@ -hacking>=0.5.6,<0.8 +hacking>=0.10.0,<0.11 coverage>=3.6 discover From d503f0594fbdc377c07e4d79ce2cb08f6226aa7d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 29 May 2015 07:39:20 -0400 Subject: [PATCH 0285/3836] Add list of image params needed to disable agents Some clouds have an in-instance agent to handle things like online password resets and other such activities. When building and uploading images, it's often advantageous to not install such an agent and instead handle such things via config management... but doing so requires data to be set on the image itself. Change-Id: I5b7c9d72fd2d49890bc466d7dd22a3cb9595f670 --- doc/source/vendor-support.rst | 3 +++ os_client_config/defaults.py | 1 + os_client_config/vendors.py | 4 ++++ 3 files changed, 8 insertions(+) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index 02e093729..602dd2bc0 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -48,6 +48,9 @@ HKG Hong Kong * Images must be uploaded using the Glance Task Interface * Floating IPs are not needed * Security groups are not supported +* Uploaded Images need properties to not use vendor agent +:vm_mode: hvm +:xenapi_use_agent: False Dreamhost --------- diff --git a/os_client_config/defaults.py b/os_client_config/defaults.py index 11a94c47a..e92149223 100644 --- a/os_client_config/defaults.py +++ b/os_client_config/defaults.py @@ -25,6 +25,7 @@ object_api_version='1', secgroup_source='neutron', volume_api_version='1', + disable_vendor_agent={}, ) diff --git a/os_client_config/vendors.py b/os_client_config/vendors.py index ed8d0471e..68a70c544 100644 --- a/os_client_config/vendors.py +++ b/os_client_config/vendors.py @@ -34,6 +34,10 @@ image_format='vhd', floating_ip_source=None, secgroup_source=None, + disable_vendor_agent=dict( + vm_mode='hvm', + xenapi_use_agent=False, + ) ), dreamhost=dict( auth=dict( From f66aad5ad9b3ac3476d591c71546b468ab1407d3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 30 May 2015 13:01:41 -0400 Subject: [PATCH 0286/3836] Add auro to list of known vendors Change-Id: I48d6cffdcc16bdbf4912da775fb29876007fe8db --- doc/source/vendor-support.rst | 17 +++++++++++++++++ os_client_config/vendors.py | 11 +++++++++++ 2 files changed, 28 insertions(+) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index 602dd2bc0..1818e6b5f 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -118,3 +118,20 @@ gd1 Guangdong * Images must be in `raw` format * Floating IPs are not needed * Security groups are provided by Neutron + +Auro +---- + +https://api.auro.io:5000/v2.0 + +============== ================ +Region Name Human Name +============== ================ +RegionOne RegionOne +============== ================ + +* Identity API Version is 2 +* Image API Version is 1 +* Images must be in `qcow2` format +* Floating IPs are provided by Nova +* Security groups are provided by Nova diff --git a/os_client_config/vendors.py b/os_client_config/vendors.py index 68a70c544..ad04c4dc1 100644 --- a/os_client_config/vendors.py +++ b/os_client_config/vendors.py @@ -73,4 +73,15 @@ image_format='raw', floating_ip_source=None, ), + auro=dict( + auth=dict( + auth_url='https://api.auro.io:5000/v2.0', + ), + region_name='RegionOne', + identity_api_version='2', + image_api_version='1', + image_format='qcow2', + secgroup_source='nova', + floating_ip_source='nova', + ), ) From b431669a6b8210668fa37751bd208b55720ba314 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 30 May 2015 13:02:47 -0400 Subject: [PATCH 0287/3836] Change naming in vendor doc to match vendors.py Change-Id: I90da039d8b0551b80ec34975480e650885f59d3a --- doc/source/vendor-support.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index 1818e6b5f..351d66665 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -8,8 +8,8 @@ information about various things a user would need to know. The following is a text representation of the vendor related defaults `os-client-config` knows about. -HP Cloud --------- +hp +-- https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0 @@ -26,7 +26,7 @@ region-b.geo-1 US East * Floating IPs are provided by Neutron * Security groups are provided by Neutron -Rackspace +rackspace --------- https://identity.api.rackspacecloud.com/v2.0/ @@ -52,7 +52,7 @@ HKG Hong Kong :vm_mode: hvm :xenapi_use_agent: False -Dreamhost +dreamhost --------- https://keystone.dream.io/v2.0 @@ -68,7 +68,7 @@ RegionOne Region One * Floating IPs are provided by Neutron * Security groups are provided by Neutron -Vexxhost +vexxhost -------- http://auth.api.thenebulacloud.com:5000/v2.0/ @@ -84,7 +84,7 @@ ca-ymq-1 Montreal * Floating IPs are not needed * Security groups are provided by Neutron -RunAbove +runabove -------- https://auth.runabove.io/v2.0 @@ -101,7 +101,7 @@ BHS-1 Beauharnois, QC * Floating IPs are not needed * Security groups are provided by Neutron -UnitedStack +unitedstack ----------- https://identity.api.ustack.com/v3 @@ -119,7 +119,7 @@ gd1 Guangdong * Floating IPs are not needed * Security groups are provided by Neutron -Auro +auro ---- https://api.auro.io:5000/v2.0 From ba0986f58ce7468bdc91380ecd1b07086fa37964 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 2 Jun 2015 15:55:59 -0500 Subject: [PATCH 0288/3836] Add more defaults to our defaults file It's easier logic when we have default values for things. Change-Id: I2d66dcee68ea95371609640677fd41cca4b0a7cf --- os_client_config/defaults.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/os_client_config/defaults.py b/os_client_config/defaults.py index e92149223..8bf693d8d 100644 --- a/os_client_config/defaults.py +++ b/os_client_config/defaults.py @@ -13,10 +13,12 @@ # under the License. _defaults = dict( + api_timeout=None, auth_type='password', baremetal_api_version='1', compute_api_version='2', database_api_version='1.0', + endpoint_type='public', floating_ip_source='neutron', identity_api_version='2', image_api_use_tasks=False, @@ -26,6 +28,11 @@ secgroup_source='neutron', volume_api_version='1', disable_vendor_agent={}, + # SSL Related args + verify=True, + cacert=None, + cert=None, + key=None, ) From 7e69d826c4366b402eefb209af6d399b9bb95c20 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 2 Jun 2015 16:35:14 -0400 Subject: [PATCH 0289/3836] Cleanup OperatorCloud doc errors/warnings Some of the OperatorCloud method docstrings were generating sphinx warnings, and the output just didn't look good. This cleans up the warnings and makes things pretty. Change-Id: I8be7acc9872d18b7ba899d75ba3dff43dc600731 --- shade/__init__.py | 67 +++++++++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index f9e8d14b1..57da2695f 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2354,18 +2354,20 @@ def register_machine(self, nics, **kwargs): If a failure is detected creating the network ports, any ports created are deleted, and the node is removed from Ironic. - :param list nics: An array of MAC addresses that represent the - network interfaces for the node to be created. + :param list nics: + An array of MAC addresses that represent the + network interfaces for the node to be created. - Example: - [ - {'mac': 'aa:bb:cc:dd:ee:01'}, - {'mac': 'aa:bb:cc:dd:ee:02'} - ] + Example:: - :param **kwargs: Key value pairs to be passed to the Ironic API, - including uuid, name, chassis_uuid, driver_info, - parameters. + [ + {'mac': 'aa:bb:cc:dd:ee:01'}, + {'mac': 'aa:bb:cc:dd:ee:02'} + ] + + :param kwargs: Key value pairs to be passed to the Ironic API, + including uuid, name, chassis_uuid, driver_info, + parameters. :raises: OpenStackCloudException on operation error. @@ -2414,7 +2416,7 @@ def unregister_machine(self, nics, uuid): from an Ironic API :param list nics: An array of strings that consist of MAC addresses - to be removed. + to be removed. :param string uuid: The UUID of the node to be deleted. :raises: OpenStackCloudException on operation failure. @@ -2456,27 +2458,28 @@ def patch_machine(self, name_or_id, patch): properties to be updated. :param node_id: The server object to attach to. - :param patch: The JSON Patch document is a list of dictonary objects - that comply with RFC 6902 which can be found at - https://tools.ietf.org/html/rfc6902. - - Example patch construction: - - patch=[] - patch.append({ - 'op': 'remove', - 'path': '/instance_info' - }) - patch.append({ - 'op': 'replace', - 'path': '/name', - 'value': 'newname' - }) - patch.append({ - 'op': 'add', - 'path': '/driver_info/username', - 'value': 'administrator' - }) + :param patch: + The JSON Patch document is a list of dictonary objects + that comply with RFC 6902 which can be found at + https://tools.ietf.org/html/rfc6902. + + Example patch construction:: + + patch=[] + patch.append({ + 'op': 'remove', + 'path': '/instance_info' + }) + patch.append({ + 'op': 'replace', + 'path': '/name', + 'value': 'newname' + }) + patch.append({ + 'op': 'add', + 'path': '/driver_info/username', + 'value': 'administrator' + }) :raises: OpenStackCloudException on operation error. From e763e3bb74cd62aaa8cfef3e9416d1a0d2b818ae Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 26 Apr 2015 13:17:11 -0400 Subject: [PATCH 0290/3836] Split iterate_timeout into _utils In keeping with the theme of decomposition, move _iterate_timeout into a file. Change-Id: I180177e8d7dc1458ea987da7273ada485abb72d7 --- shade/__init__.py | 38 ++++++++++---------------------------- shade/_utils.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 28 deletions(-) create mode 100644 shade/_utils.py diff --git a/shade/__init__.py b/shade/__init__.py index f9e8d14b1..65ebb3a1d 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -16,7 +16,6 @@ import logging import operator import os -import time from cinderclient.v1 import client as cinder_client from dogpile import cache @@ -45,6 +44,7 @@ from shade import meta from shade import task_manager from shade import _tasks +from shade import _utils __version__ = pbr.version.VersionInfo('shade').version_string() OBJECT_MD5_KEY = 'x-object-meta-x-shade-md5' @@ -112,24 +112,6 @@ def _get_service_values(kwargs, service_key): for k in kwargs.keys() if k.endswith(service_key)} -def _iterate_timeout(timeout, message): - """Iterate and raise an exception on timeout. - - This is a generator that will continually yield and sleep for 2 - seconds, and if the timeout is reached, will raise an exception - with . - - """ - - start = time.time() - count = 0 - while (timeout is None) or (time.time() < start + timeout): - count += 1 - yield count - time.sleep(2) - raise OpenStackCloudTimeout(message) - - def _cache_on_arguments(*cache_on_args, **cache_on_kwargs): def _inner_cache_on_arguments(func): def _cache_decorator(obj, *args, **kwargs): @@ -1237,7 +1219,7 @@ def delete_image(self, name_or_id, wait=False, timeout=3600): "Error in deleting image: %s" % str(e)) if wait: - for count in _iterate_timeout( + for count in _utils._iterate_timeout( timeout, "Timeout waiting for the image to be deleted."): self._cache.invalidate() @@ -1326,7 +1308,7 @@ def _upload_image_task( image_properties=dict(name=name)))) if wait: image_id = None - for count in _iterate_timeout( + for count in _utils._iterate_timeout( timeout, "Timeout waiting for the image to import."): try: @@ -1429,7 +1411,7 @@ def create_volume(self, wait=True, timeout=None, **kwargs): if wait: vol_id = volume['id'] - for count in _iterate_timeout( + for count in _utils._iterate_timeout( timeout, "Timeout waiting for the volume to be available."): volume = self.get_volume(vol_id) @@ -1468,7 +1450,7 @@ def delete_volume(self, name_or_id=None, wait=True, timeout=None): self.list_volumes.invalidate(self) if wait: - for count in _iterate_timeout( + for count in _utils._iterate_timeout( timeout, "Timeout waiting for the volume to be deleted."): if not self.volume_exists(volume['id']): @@ -1537,7 +1519,7 @@ def detach_volume(self, server, volume, wait=True, timeout=None): ) if wait: - for count in _iterate_timeout( + for count in _utils._iterate_timeout( timeout, "Timeout waiting for volume %s to detach." % volume['id']): try: @@ -1606,7 +1588,7 @@ def attach_volume(self, server, volume, device=None, ) if wait: - for count in _iterate_timeout( + for count in _utils._iterate_timeout( timeout, "Timeout waiting for volume %s to attach." % volume['id']): try: @@ -1782,7 +1764,7 @@ def create_server(self, auto_ip=True, ips=None, ip_pool=None, raise OpenStackCloudException( "Error in creating the server.") if wait: - for count in _iterate_timeout( + for count in _utils._iterate_timeout( timeout, "Timeout waiting for the server to come up."): try: @@ -1812,7 +1794,7 @@ def rebuild_server(self, server_id, image_id, wait=False, timeout=180): raise OpenStackCloudException( "Error in rebuilding instance: {0}".format(e)) if wait: - for count in _iterate_timeout( + for count in _utils._iterate_timeout( timeout, "Timeout waiting for server {0} to " "rebuild.".format(server_id)): @@ -1848,7 +1830,7 @@ def delete_server(self, name, wait=False, timeout=180): return if not wait: return - for count in _iterate_timeout( + for count in _utils._iterate_timeout( timeout, "Timed out waiting for server to get deleted."): try: diff --git a/shade/_utils.py b/shade/_utils.py new file mode 100644 index 000000000..162527bfe --- /dev/null +++ b/shade/_utils.py @@ -0,0 +1,35 @@ +# Copyright (c) 2015 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. + +import time + +from shade.exc import OpenStackCloudTimeout + + +def _iterate_timeout(timeout, message): + """Iterate and raise an exception on timeout. + + This is a generator that will continually yield and sleep for 2 + seconds, and if the timeout is reached, will raise an exception + with . + + """ + + start = time.time() + count = 0 + while (timeout is None) or (time.time() < start + timeout): + count += 1 + yield count + time.sleep(2) + raise OpenStackCloudTimeout(message) From 8d9cab2eb9772d1d38ab8a333d8d4f6f2afbb0e3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 2 Jun 2015 18:03:29 -0500 Subject: [PATCH 0291/3836] Cast nova server object to dict after refetch When we assign a floating ip to a server, we re-fetch it from the cloud so that the metadata is all correct. However, we weren't doing obj_to_dict on it like all of the other code paths that return out of create_server - which means that it bombs out on get_hostvars_from_server if subsequently passed to it. Change-Id: I3f69f525b7d57ec91f70b77cdbd62aed9816f17d --- shade/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index 57da2695f..57ff03283 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1746,7 +1746,8 @@ def add_ips_to_server(self, server, auto_ip=True, ips=None, ip_pool=None): # floating IP, then it needs to be obtained from # a recent server object if the above code path exec'd try: - server = self.manager.submitTask(_tasks.ServerGet(server=server)) + server = meta.obj_to_dict( + self.manager.submitTask(_tasks.ServerGet(server=server))) except Exception as e: self.log.debug("nova info failed", exc_info=True) raise OpenStackCloudException( From dc012c3494be300133d8ac267e420d2593acdf1a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 28 May 2015 10:31:06 -0400 Subject: [PATCH 0292/3836] Split list filtering into _utils The inventory module wants to use these. Also, they are not actually methods - they are simple functions. Split them out for sanity. Change-Id: Ib46f788da0f92ed49d350facc0a59cb1697f06ec --- shade/__init__.py | 113 ++++++--------------------------- shade/_utils.py | 83 +++++++++++++++++++++++- shade/tests/unit/test_shade.py | 19 +++--- 3 files changed, 110 insertions(+), 105 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 65ebb3a1d..57c960002 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -748,118 +748,41 @@ def _nova_extensions(self): def _has_nova_extension(self, extension_name): return extension_name in self._nova_extensions() - def _filter_list(self, data, name_or_id, filters): - """Filter a list by name/ID and arbitrary meta data. - - :param list data: - The list of dictionary data to filter. It is expected that - each dictionary contains an 'id', 'name' (or 'display_name') - key if a value for name_or_id is given. - :param string name_or_id: - The name or ID of the entity being filtered. - :param dict filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - """ - if name_or_id: - identifier_matches = [] - for e in data: - e_id = str(e.get('id', None)) - e_name = e.get('name', None) - # cinder likes to be different and use display_name - e_display_name = e.get('display_name', None) - if str(name_or_id) in (e_id, e_name, e_display_name): - identifier_matches.append(e) - data = identifier_matches - - if not filters: - return data - - def _dict_filter(f, d): - if not d: - return False - for key in f.keys(): - if isinstance(f[key], dict): - if not _dict_filter(f[key], d.get(key, None)): - return False - elif d.get(key, None) != f[key]: - return False - return True - - filtered = [] - for e in data: - filtered.append(e) - for key in filters.keys(): - if isinstance(filters[key], dict): - if not _dict_filter(filters[key], e.get(key, None)): - filtered.pop() - break - elif e.get(key, None) != filters[key]: - filtered.pop() - break - return filtered - - def _get_entity(self, func, name_or_id, filters): - """Return a single entity from the list returned by a given method. - - :param callable func: - A function that takes `name_or_id` and `filters` as parameters - and returns a list of entities to filter. - :param string name_or_id: - The name or ID of the entity being filtered. - :param dict filters: - A dictionary of meta data to use for further filtering. - """ - entities = func(name_or_id, filters) - if not entities: - return None - if len(entities) > 1: - raise OpenStackCloudException( - "Multiple matches found for %s" % name_or_id) - return entities[0] - def search_networks(self, name_or_id=None, filters=None): networks = self.list_networks() - return self._filter_list(networks, name_or_id, filters) + return _utils._filter_list(networks, name_or_id, filters) def search_routers(self, name_or_id=None, filters=None): routers = self.list_routers() - return self._filter_list(routers, name_or_id, filters) + return _utils._filter_list(routers, name_or_id, filters) def search_subnets(self, name_or_id=None, filters=None): subnets = self.list_subnets() - return self._filter_list(subnets, name_or_id, filters) + return _utils._filter_list(subnets, name_or_id, filters) def search_volumes(self, name_or_id=None, filters=None): volumes = self.list_volumes() - return self._filter_list(volumes, name_or_id, filters) + return _utils._filter_list(volumes, name_or_id, filters) def search_flavors(self, name_or_id=None, filters=None): flavors = self.list_flavors() - return self._filter_list(flavors, name_or_id, filters) + return _utils._filter_list(flavors, name_or_id, filters) def search_security_groups(self, name_or_id=None, filters=None): groups = self.list_security_groups() - return self._filter_list(groups, name_or_id, filters) + return _utils._filter_list(groups, name_or_id, filters) def search_servers(self, name_or_id=None, filters=None): servers = self.list_servers() - return self._filter_list(servers, name_or_id, filters) + return _utils._filter_list(servers, name_or_id, filters) def search_images(self, name_or_id=None, filters=None): images = self.list_images() - return self._filter_list(images, name_or_id, filters) + return _utils._filter_list(images, name_or_id, filters) def search_floating_ip_pools(self, name=None, filters=None): pools = self.list_floating_ip_pools() - return self._filter_list(pools, name, filters) + return _utils._filter_list(pools, name, filters) def list_networks(self): try: @@ -1002,29 +925,29 @@ def list_floating_ip_pools(self): msg=str(e))) def get_network(self, name_or_id, filters=None): - return self._get_entity(self.search_networks, name_or_id, filters) + return _utils._get_entity(self.search_networks, name_or_id, filters) def get_router(self, name_or_id, filters=None): - return self._get_entity(self.search_routers, name_or_id, filters) + return _utils._get_entity(self.search_routers, name_or_id, filters) def get_subnet(self, name_or_id, filters=None): - return self._get_entity(self.search_subnets, name_or_id, filters) + return _utils._get_entity(self.search_subnets, name_or_id, filters) def get_volume(self, name_or_id, filters=None): - return self._get_entity(self.search_volumes, name_or_id, filters) + return _utils._get_entity(self.search_volumes, name_or_id, filters) def get_flavor(self, name_or_id, filters=None): - return self._get_entity(self.search_flavors, name_or_id, filters) + return _utils._get_entity(self.search_flavors, name_or_id, filters) def get_security_group(self, name_or_id, filters=None): - return self._get_entity(self.search_security_groups, - name_or_id, filters) + return _utils._get_entity( + self.search_security_groups, name_or_id, filters) def get_server(self, name_or_id, filters=None): - return self._get_entity(self.search_servers, name_or_id, filters) + return _utils._get_entity(self.search_servers, name_or_id, filters) def get_image(self, name_or_id, filters=None): - return self._get_entity(self.search_images, name_or_id, filters) + return _utils._get_entity(self.search_images, name_or_id, filters) # TODO(Shrews): This will eventually need to support tenant ID and # provider networks, which are admin-level params. diff --git a/shade/_utils.py b/shade/_utils.py index 162527bfe..10a6647ff 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -14,7 +14,7 @@ import time -from shade.exc import OpenStackCloudTimeout +from shade import exc def _iterate_timeout(timeout, message): @@ -32,4 +32,83 @@ def _iterate_timeout(timeout, message): count += 1 yield count time.sleep(2) - raise OpenStackCloudTimeout(message) + raise exc.OpenStackCloudTimeout(message) + + +def _filter_list(data, name_or_id, filters): + """Filter a list by name/ID and arbitrary meta data. + + :param list data: + The list of dictionary data to filter. It is expected that + each dictionary contains an 'id', 'name' (or 'display_name') + key if a value for name_or_id is given. + :param string name_or_id: + The name or ID of the entity being filtered. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + """ + if name_or_id: + identifier_matches = [] + for e in data: + e_id = str(e.get('id', None)) + e_name = e.get('name', None) + # cinder likes to be different and use display_name + e_display_name = e.get('display_name', None) + if str(name_or_id) in (e_id, e_name, e_display_name): + identifier_matches.append(e) + data = identifier_matches + + if not filters: + return data + + def _dict_filter(f, d): + if not d: + return False + for key in f.keys(): + if isinstance(f[key], dict): + if not _dict_filter(f[key], d.get(key, None)): + return False + elif d.get(key, None) != f[key]: + return False + return True + + filtered = [] + for e in data: + filtered.append(e) + for key in filters.keys(): + if isinstance(filters[key], dict): + if not _dict_filter(filters[key], e.get(key, None)): + filtered.pop() + break + elif e.get(key, None) != filters[key]: + filtered.pop() + break + return filtered + + +def _get_entity(func, name_or_id, filters): + """Return a single entity from the list returned by a given method. + + :param callable func: + A function that takes `name_or_id` and `filters` as parameters + and returns a list of entities to filter. + :param string name_or_id: + The name or ID of the entity being filtered. + :param dict filters: + A dictionary of meta data to use for further filtering. + """ + entities = func(name_or_id, filters) + if not entities: + return None + if len(entities) > 1: + raise exc.OpenStackCloudException( + "Multiple matches found for %s" % name_or_id) + return entities[0] diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 14f2e0d61..ec4cf8218 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -20,6 +20,8 @@ import shade from shade import exc from shade import meta +from shade import _utils + from shade.tests.unit import base @@ -36,14 +38,14 @@ def test__filter_list_name_or_id(self): el1 = dict(id=100, name='donald') el2 = dict(id=200, name='pluto') data = [el1, el2] - ret = self.cloud._filter_list(data, 'donald', None) + ret = _utils._filter_list(data, 'donald', None) self.assertEquals([el1], ret) def test__filter_list_filter(self): el1 = dict(id=100, name='donald', other='duck') el2 = dict(id=200, name='donald', other='trump') data = [el1, el2] - ret = self.cloud._filter_list(data, 'donald', {'other': 'duck'}) + ret = _utils._filter_list(data, 'donald', {'other': 'duck'}) self.assertEquals([el1], ret) def test__filter_list_dict1(self): @@ -54,8 +56,8 @@ def test__filter_list_dict1(self): el3 = dict(id=300, name='donald', last='ronald mac', other=dict(category='clown')) data = [el1, el2, el3] - ret = self.cloud._filter_list(data, 'donald', - {'other': {'category': 'clown'}}) + ret = _utils._filter_list( + data, 'donald', {'other': {'category': 'clown'}}) self.assertEquals([el3], ret) def test__filter_list_dict2(self): @@ -66,10 +68,11 @@ def test__filter_list_dict2(self): el3 = dict(id=300, name='donald', last='ronald mac', other=dict(category='clown', financial=dict(status='rich'))) data = [el1, el2, el3] - ret = self.cloud._filter_list(data, 'donald', - {'other': { - 'financial': {'status': 'rich'} - }}) + ret = _utils._filter_list( + data, 'donald', + {'other': { + 'financial': {'status': 'rich'} + }}) self.assertEquals([el2, el3], ret) @mock.patch.object(shade.OpenStackCloud, 'search_images') From 6c3b1e5f119b5207a11effd4258e0bcd4358d944 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 3 Jun 2015 09:56:03 -0400 Subject: [PATCH 0293/3836] Port ironic client port.list() to a Task This call was not yet a Task. Port it, and add missing tests. Change-Id: Ifc8b81f5e21b661911ba19f12ce8848e3abee809 --- shade/__init__.py | 9 ++++++++- shade/_tasks.py | 5 +++++ shade/tests/fakes.py | 7 +++++++ shade/tests/unit/test_shade.py | 20 ++++++++++++++++++++ 4 files changed, 40 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index 57ff03283..1d26f1740 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2304,7 +2304,14 @@ def ironic_client(self): return self._ironic_client def list_nics(self): - return meta.obj_list_to_dict(self.ironic_client.port.list()) + try: + return meta.obj_list_to_dict( + self.manager.submitTask(_tasks.MachinePortList()) + ) + except Exception as e: + self.log.debug("machine port list failed: %s" % e, exc_info=True) + raise OpenStackCloudException( + "Error fetching machine port list: %s" % e) def list_nics_for_machine(self, uuid): return meta.obj_list_to_dict(self.ironic_client.node.list_ports(uuid)) diff --git a/shade/_tasks.py b/shade/_tasks.py index 3dc051c0f..2f577725f 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -303,6 +303,11 @@ def main(self, client): return client.ironic_client.port.delete(**self.args) +class MachinePortList(task_manager.Task): + def main(self, client): + return client.ironic_client.port.list() + + class MachineSetMaintenance(task_manager.Task): def main(self, client): return client.ironic_client.node.set_maintenance(**self.args) diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index d7f585b2b..54c59be67 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -63,3 +63,10 @@ def __init__(self, id, status, display_name): self.id = id self.status = status self.display_name = display_name + + +class FakeMachinePort(object): + def __init__(self, id, address, node_id): + self.id = id + self.address = address + self.node_id = node_id diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 14f2e0d61..ec5948844 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -21,6 +21,7 @@ from shade import exc from shade import meta from shade.tests.unit import base +from shade.tests import fakes class TestShade(base.TestCase): @@ -331,6 +332,25 @@ def setUp(self): def test_operator_cloud(self): self.assertIsInstance(self.cloud, shade.OperatorCloud) + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_list_nics(self, mock_client): + port1 = fakes.FakeMachinePort(1, "aa:bb:cc:dd", "node1") + port2 = fakes.FakeMachinePort(2, "dd:cc:bb:aa", "node2") + port_list = [port1, port2] + port_dict_list = meta.obj_list_to_dict(port_list) + + mock_client.port.list.return_value = port_list + nics = self.cloud.list_nics() + + self.assertTrue(mock_client.port.list.called) + self.assertEqual(port_dict_list, nics) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_list_nics_failure(self, mock_client): + mock_client.port.list.side_effect = Exception() + self.assertRaises(exc.OpenStackCloudException, + self.cloud.list_nics) + @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_patch_machine(self, mock_client): node_id = 'node01' From eccbd04f812e4a42fa9294df948613dff4591fbd Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 3 Jun 2015 10:38:21 -0400 Subject: [PATCH 0294/3836] Port ironic client node.list_ports() to a Task This call was not yet a Task. Port it, and add missing tests. Change-Id: I7195cd46e20e6653c03b88858995439bc8b3d631 --- shade/__init__.py | 11 ++++++++++- shade/_tasks.py | 5 +++++ shade/tests/unit/test_shade.py | 12 ++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index 1d26f1740..1630f6a5c 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2314,7 +2314,16 @@ def list_nics(self): "Error fetching machine port list: %s" % e) def list_nics_for_machine(self, uuid): - return meta.obj_list_to_dict(self.ironic_client.node.list_ports(uuid)) + try: + return meta.obj_list_to_dict( + self.manager.submitTask( + _tasks.MachineNodePortList(node_id=uuid)) + ) + except Exception as e: + self.log.debug("port list for node %s failed: %s" % (uuid, e), + exc_info=True) + raise OpenStackCloudException( + "Error fetching port list for node %s: %s" % (uuid, e)) def get_nic_by_mac(self, mac): try: diff --git a/shade/_tasks.py b/shade/_tasks.py index 2f577725f..633659dce 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -308,6 +308,11 @@ def main(self, client): return client.ironic_client.port.list() +class MachineNodePortList(task_manager.Task): + def main(self, client): + return client.ironic_client.node.list_ports(**self.args) + + class MachineSetMaintenance(task_manager.Task): def main(self, client): return client.ironic_client.node.set_maintenance(**self.args) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index ec5948844..192d1779c 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -351,6 +351,18 @@ def test_list_nics_failure(self, mock_client): self.assertRaises(exc.OpenStackCloudException, self.cloud.list_nics) + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_list_nics_for_machine(self, mock_client): + mock_client.node.list_ports.return_value = [] + self.cloud.list_nics_for_machine("123") + mock_client.node.list_ports.assert_called_with(node_id="123") + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_list_nics_for_machine_failure(self, mock_client): + mock_client.node.list_ports.side_effect = Exception() + self.assertRaises(exc.OpenStackCloudException, + self.cloud.list_nics_for_machine, None) + @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_patch_machine(self, mock_client): node_id = 'node01' From 2fcf0530a6eba7ace554f5422110e81c9585a841 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 17 Apr 2015 17:01:20 -0400 Subject: [PATCH 0295/3836] Add design for an object interface We are currently butting up against the point where the functional interface is a bit ugly. Write up a general design for an object interface, as well as a few other design thoughts on the library. Change-Id: Iac675860336275ea56026fcbed27338ba80ef886 --- README.rst | 171 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) diff --git a/README.rst b/README.rst index dcda824b8..5159739ce 100644 --- a/README.rst +++ b/README.rst @@ -35,6 +35,8 @@ Sometimes an example is nice. s = cloud.list_servers()[0] # But you can also access the underlying python-*client objects + # This will go away at some point in time and should be considered only + # usable for temporary poking cinder = cloud.cinder_client volumes = cinder.volumes.list() volume_id = [v for v in volumes if v['status'] == 'available'][0]['id'] @@ -47,3 +49,172 @@ Sometimes an example is nice. time.sleep(1) attachments = cinder.volumes.get(volume_id).attachments print attachments + + +Object Design +============= + +Shade is a library for managing resources, not for operating APIs. As such, +it is the resource in question that is the primary object and not the service +that may or may not provide that resource, much as we may feel warm and fuzzy +to one of the services. + +Every resource at minimum has CRUD functions. Additionally, every resource +action should have a "do this task blocking" or "request that the cloud start +this action and give me a way to check its status" The creation and deletion +of Resources will be handled by a ResourceManager that is attached to the Cloud +:: + + class Cloud: + ResourceManager server + servers = server + ResourceManager floating_ip + floating_ips = floating_ip + ResourceManager image + images = image + ResourceManager role + roles = role + ResourceManager volume + volumes = volume + +getting, listing and searching +------------------------------ + +In addition to creating a resource, there are different ways of getting your +hands on a resource. A `get`, a `list` and a `search`. + +`list` has the simplest semantics - it takes no parameters and simply returns +a list of all of the resources that exist. + +`search` takes a set of parameters to match against and returns a list of +resources that match the parameters given. If no resources match, it returns +an empty list. + +`get` takes the same set of parameters that `search` takes, but will only ever +return a single matching resource or None. If multiple resources are matched, +an exception will be raised. + +:: + + class ResourceManager: + def get -> Resource + def list -> List + def search -> List + def create -> Resource + +Cloud and ResourceManager interface +----------------------------------- + +All ResourceManagers should accept a cache object passed in to their constructor +and should additionally pass that cache object to all Resource constructors. +The top-level cloud should create the cache object, then pass it to each of +the ResourceManagers when it creates them. + +Client connection objects should exist and be managed at the Cloud level. A +backreference to the OpenStack cloud should be passed to every resource manager +so that ResourceManagers can get hold of the ones they need. For instance, +an Image ResourceManager would potentially need access to both the glance_client +and the swift_client. + +:: + + class ResourceManager + def __init__(self, cache, cloud) + class ServerManager(ResourceManager) + class OpenStackCloud + def __init__(self): + self.cache = dogpile.cache() + self.server = ServerManager(self.cache, self) + self.servers = self.server + +Any resources that have an association action - such as servers and +floating_ips, should carry reciprocal methods on each resource with absolutely +no difference in behavior. + +:: + + class Server(Resource): + def connect_floating_ip: + class FloatingIp(Resource): + def connect_server: + +Resource objects should have all of the accessor methods you'd expect, as well +as any other interesting rollup methods or actions. For instance, since +a keystone User can be enabled or disabled, one should expect that there +would be an enable() and a disable() method, and that those methods will +immediately operate the necessary REST apis. However, if you need to make 80 +changes to a Resource, 80 REST calls may or may not be silly, so there should +also be a generic update() method which can be used to request the minimal +amount of REST calls needed to update the attributes to the requested values. + +Resource objects should all have a to_dict method which will return a plain +flat dictionary of their attributes. + +:: + + class Resource: + def update(**new_values) -> Resource + def delete -> None, throws on error + +Readiness +--------- + +`create`, `get`, and `attach` can return resources that are not yet ready. Each +method should take a `wait` and a `timeout` parameter, that will cause the +request for the resource to block until it is ready. However, the user may +want to poll themselves. Each resource should have an `is_ready` method which +will return True when the resource is ready. The `wait` method then can +actually be implemented in the base Resource class as an iterate timeout +loop around calls to `is_ready`. Every Resource should also have an +`is_failed` and an `is_deleted` method. + +Optional Behavior +----------------- + +Not all clouds expose all features. For instance, some clouds do not have +floating ips. Additionally, some clouds may have the feature but the user +account does not, which is effectively the same thing. +This should be handled in several ways: + +If the user explicitly requests a resource that they do not have access to, +an error should be raised. For instance, if a user tries to create a floating +ip on a cloud that does not expose that feature to them, shade should throw +a "Your cloud does not let you do that" error. + +If the resource concept can be can be serviced by multiple possible services, +shade should transparently try all of them. The discovery method should use +the dogpile.cache mechanism so that it can be avoided on subsequent tries. For +instance, if the user says "please upload this image", shade should figure +out which sequence of actions need to be performed and should get the job done. + +If the resource isn't present on some clouds, but the overall concept the +resouce represents is, a different resource should present the concept. For +instance, while some clouds do not have floating ips, if what the user wants +is "a server with an IP" - then the fact that one needs to request a floating +ip on some clouds is a detail, and the right thing for that to be is a quality +of a server and managed by the server resource. A floating ip resource should +really only be directly manipulated by the user if they were doing something +very floating-ip specific, such as moving a floating ip from one server to +another. + +In short, it should be considered a MASSIVE bug in shade if the shade user +ever has to have in their own code "if cloud.has_capability("X") do_thing +else do_other_thing" - since that construct conveys some resource that shade +should really be able to model. + +Functional Interface +-------------------- + +shade should also provide a functional mapping to the object interface that +does not expse the object interface at all. For instance, fora resource type +`server`, one could expect the following. + +:: + + class OpenStackCloud: + def create_server + return self.server.create().to_dict() + def get_server + return self.server.get().to_dict() + def update_server + return self.server.get().update().to_dict() From a0df67704ace186b18fd1ecdc220a7e56409bc6f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 2 Jun 2015 15:56:17 -0500 Subject: [PATCH 0296/3836] Provide a helper method to get requests ssl values There is a weird logic around the interaction between how the openstack things all accept cacert and verify/insecure parameters and how requests wants them. Rather than spreading the parameter combining logic across the universe, put it here. Note that this inverts the usual requests logic in that !verify will override the presence of a cacert value and cause verification to NOT occur. This is intended to become the normal mode of operation for OpenStack clients. Change-Id: I3c76d9a10e6e5d60a593ceefc87dafdc6857d9c6 --- os_client_config/cloud_config.py | 13 +++++++++ os_client_config/tests/test_cloud_config.py | 29 +++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 347eb7c48..d8ac85d55 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -39,3 +39,16 @@ def __eq__(self, other): def __ne__(self, other): return not self == other + + def get_requests_verify_args(self): + """Return the verify and cert values for the requests library.""" + if self.config['verify'] and self.config['cacert']: + verify = self.config['cacert'] + else: + verify = self.config['verify'] + + cert = self.config.get('cert', None) + if cert: + if self.config['key']: + cert = (cert, self.config['key']) + return (verify, cert) diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index 1f79b3e71..729bc9974 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import copy from os_client_config import cloud_config from os_client_config.tests import base @@ -59,3 +60,31 @@ def test_inequality(self): cc2 = cloud_config.CloudConfig("test1", "region-al", {}) self.assertNotEqual(cc1, cc2) + + def test_verify(self): + config_dict = copy.deepcopy(fake_config_dict) + config_dict['cacert'] = None + + config_dict['verify'] = False + cc = cloud_config.CloudConfig("test1", "region-xx", config_dict) + (verify, cacert) = cc.get_requests_verify_args() + self.assertFalse(verify) + + config_dict['verify'] = True + cc = cloud_config.CloudConfig("test1", "region-xx", config_dict) + (verify, cacert) = cc.get_requests_verify_args() + self.assertTrue(verify) + + def test_verify_cacert(self): + config_dict = copy.deepcopy(fake_config_dict) + config_dict['cacert'] = "certfile" + + config_dict['verify'] = False + cc = cloud_config.CloudConfig("test1", "region-xx", config_dict) + (verify, cacert) = cc.get_requests_verify_args() + self.assertFalse(verify) + + config_dict['verify'] = True + cc = cloud_config.CloudConfig("test1", "region-xx", config_dict) + (verify, cacert) = cc.get_requests_verify_args() + self.assertEqual("certfile", verify) From d5d539da8141ad5557bb32c89a6bbf4d591ec830 Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Wed, 3 Jun 2015 10:37:17 -0700 Subject: [PATCH 0297/3836] Remove hacking select line Contrary to popular belief, this does not mean "also check this". It means "only check this and don't check anything else". So this was testing _only_ H321. Change-Id: I8ae173e62d57232cec1bf751004fe4213214353b --- shade/__init__.py | 6 +++--- tox.ini | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 7e0e05ae5..37e5b305f 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -331,7 +331,7 @@ def keystone_session(self): self.log.debug("keystone auth plugin failure", exc_info=True) raise OpenStackCloudException( "Could not find auth plugin: {plugin}".format( - plugin=self.auth_type)) + plugin=self.auth_type)) try: keystone_auth = auth_plugin(**self.auth) except Exception as e: @@ -1882,7 +1882,7 @@ def is_object_stale( self.log.debug( "swift object up to date: {container}/{name}".format( - container=container, name=name)) + container=container, name=name)) return False def create_object( @@ -2336,7 +2336,7 @@ def unregister_machine(self, nics, uuid): _tasks.MachinePortDelete( port_id=( self.ironic_client.port.get_by_address(nic['mac']) - ))) + ))) except Exception as e: self.log.debug( diff --git a/tox.ini b/tox.ini index d473d9ffa..a01094d13 100644 --- a/tox.ini +++ b/tox.ini @@ -33,7 +33,6 @@ commands = python setup.py testr --coverage --testr-args='{posargs}' [flake8] # Infra does not follow hacking, nor the broken E12* things ignore = E123,E125,H -select = H231 show-source = True builtins = _ exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build From fd731c1cec9fcb839320e99aa1ad8f23494acae6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 9 May 2015 10:00:53 -0400 Subject: [PATCH 0298/3836] Consume os_client_config defaults as base defaults In 99% of invocations, os_client_config will be passing in default values. However, if someone decides to just construct an OpenStackCloud without going through os_client_config, there will be defaults missing. Grab the defaults from os_client_config and use them, even if they aren't passed in as arguments. Change-Id: I25a1ff3eeba445f91b9bde8cd4e2d68b72b06136 --- requirements.txt | 2 +- shade/__init__.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index d1b745ad4..6323ef608 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ pbr>=0.11,<2.0 bunch jsonpatch -os-client-config>=0.8.1 +os-client-config>=1.0.0 six python-novaclient>=2.21.0 diff --git a/shade/__init__.py b/shade/__init__.py index 7e0e05ae5..be6a25819 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -31,6 +31,7 @@ from novaclient import exceptions as nova_exceptions from neutronclient.v2_0 import client as neutron_client import os_client_config +import os_client_config.defaults import pbr.version import swiftclient.client as swift_client import swiftclient.exceptions as swift_exceptions @@ -108,8 +109,11 @@ def _ssl_args(verify, cacert, cert, key): def _get_service_values(kwargs, service_key): - return {k[:-(len(service_key) + 1)]: kwargs[k] - for k in kwargs.keys() if k.endswith(service_key)} + # get defauts returns a copy of the defaults dict + values = os_client_config.defaults.get_defaults() + values.update(kwargs) + return {k[:-(len(service_key) + 1)]: values[k] + for k in values.keys() if k.endswith(service_key)} def _cache_on_arguments(*cache_on_args, **cache_on_kwargs): From fa0a120b4f8ace0b9b77ef797bc106d5f76c0557 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 9 May 2015 10:11:37 -0400 Subject: [PATCH 0299/3836] Rely on defaults being present There will always be a fleshed out defaults dict now, so don't spend a lot of effort in dealing with one not being there. Change-Id: I93cea42c6e35457de1f2d8d1e37ead2ff41fba3a --- shade/__init__.py | 48 +++++++---------------------------------------- 1 file changed, 7 insertions(+), 41 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index be6a25819..6ce372ccd 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -290,12 +290,6 @@ def get_service_type(self, service): def get_service_name(self, service): return self.service_names.get(service, None) - def _get_nova_api_version(self): - return self.api_versions['compute'] - - def _get_ironic_api_version(self): - return self.api_versions.get('baremetal', '1') - @property def nova_client(self): if self._nova_client is None: @@ -305,7 +299,7 @@ def nova_client(self): # trigger exception on lack of compute. (what?) self.get_session_endpoint('compute') self._nova_client = nova_client.Client( - self._get_nova_api_version(), + self.api_versions['compute'], session=self.keystone_session, service_name=self.get_service_name('compute'), region_name=self.region_name, @@ -530,19 +524,6 @@ def delete_user(self, name_or_id): user=name_or_id, message=str(e))) self.get_user_cache.invalidate(self) - def _get_glance_api_version(self): - if 'image' in self.api_versions: - return self.api_versions['image'] - # Yay. We get to guess ... - # Get rid of trailing '/' if present - endpoint = self.get_session_endpoint('image') - if endpoint.endswith('/'): - endpoint = endpoint[:-1] - url_bits = endpoint.split('/') - if url_bits[-1].startswith('v'): - return url_bits[-1][1] - return '1' # Who knows? Let's just try 1 ... - @property def glance_client(self): # Note that glanceclient doesn't use keystoneclient sessions @@ -552,13 +533,12 @@ def glance_client(self): # expiration. token = self.auth_token endpoint = self.get_session_endpoint('image') - glance_api_version = self._get_glance_api_version() kwargs = dict() if self.api_timeout is not None: kwargs['timeout'] = self.api_timeout try: self._glance_client = glanceclient.Client( - glance_api_version, endpoint, token=token, + self.api_versions['image'], endpoint, token=token, session=self.keystone_session, **kwargs) except Exception as e: @@ -602,28 +582,14 @@ def cinder_client(self): return self._cinder_client - def _get_trove_api_version(self, endpoint): - if 'database' in self.api_versions: - return self.api_versions['database'] - # Yay. We get to guess ... - # Get rid of trailing '/' if present - if endpoint.endswith('/'): - endpoint = endpoint[:-1] - url_bits = endpoint.split('/') - for bit in url_bits: - if bit.startswith('v'): - return bit[1:] - return '1.0' # Who knows? Let's just try 1.0 ... - @property def trove_client(self): if self._trove_client is None: - endpoint = self.get_session_endpoint(service_key='database') - trove_api_version = self._get_trove_api_version(endpoint) + self.get_session_endpoint(service_key='database') # Make the connection - can't use keystone session until there # is one self._trove_client = trove_client.Client( - trove_api_version, + self.api_versions['database'], session=self.keystone_session, region_name=self.region_name, service_type=self.get_service_type('database'), @@ -1133,7 +1099,7 @@ def delete_image(self, name_or_id, wait=False, timeout=3600): try: # Note that in v1, the param name is image, but in v2, # it's image_id - glance_api_version = self._get_glance_api_version() + glance_api_version = self.api_versions['image'] if glance_api_version == '2': self.manager.submitTask( _tasks.ImageDelete(image_id=image.id)) @@ -1283,7 +1249,7 @@ def update_image_properties( img_props[k] = v # This makes me want to die inside - if self._get_glance_api_version() == '2': + if self.api_versions['image'] == '2': return self._update_image_properties_v2(image, img_props) else: return self._update_image_properties_v1(image, img_props) @@ -2204,7 +2170,7 @@ def ironic_client(self): endpoint = self.get_session_endpoint(service_key='baremetal') try: self._ironic_client = ironic_client.Client( - self._get_ironic_api_version(), endpoint, token=token, + self.api_versions['baremetal'], endpoint, token=token, timeout=self.api_timeout) except Exception as e: self.log.debug("ironic auth failed", exc_info=True) From da6929777d2ae49dcebe1bbe730b1661b3153424 Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Mon, 27 Apr 2015 00:03:39 +0100 Subject: [PATCH 0300/3836] Add Keystone service resource methods This change adds keystone service resource methods to OperatorClouds. Only Keystone v2 API is currently supported Change-Id: I5d0b44664a6839502d86fed8d68717b086c34a81 --- shade/__init__.py | 100 ++++++++++++++++++ shade/_tasks.py | 15 +++ shade/tests/fakes.py | 8 ++ shade/tests/functional/test_services.py | 108 +++++++++++++++++++ shade/tests/unit/test_services.py | 132 ++++++++++++++++++++++++ 5 files changed, 363 insertions(+) create mode 100644 shade/tests/functional/test_services.py create mode 100644 shade/tests/unit/test_services.py diff --git a/shade/__init__.py b/shade/__init__.py index cd71efd78..5c9b7f8e5 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2724,3 +2724,103 @@ def purge_node_instance_info(self, uuid): self.log.debug( "Failed to delete instance_info", exc_info=True) raise OpenStackCloudException(str(e)) + + def create_service(self, name, service_type, description=None): + """Create a service. + + :param name: Service name. + :param service_type: Service type. + :param description: Service description (optional). + + :returns: a dict containing the services description, i.e. the + following attributes:: + - id: + - name: + - service_type: + - description: + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call. + + """ + try: + service = self.manager.submitTask(_tasks.ServiceCreate( + name=name, service_type=service_type, + description=description)) + except Exception as e: + self.log.debug( + "Failed to create service {name}".format(name=name), + exc_info=True) + raise OpenStackCloudException(str(e)) + return meta.obj_to_dict(service) + + def list_services(self): + """List all Keystone services. + + :returns: a list of dict containing the services description. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call. + """ + try: + services = self.manager.submitTask(_tasks.ServiceList()) + except Exception as e: + self.log.debug("Failed to list services", exc_info=True) + raise OpenStackCloudException(str(e)) + return meta.obj_list_to_dict(services) + + def search_services(self, name_or_id=None, filters=None): + """Search Keystone services. + + :param name_or_id: Name or id of the desired service. + :param filters: a dict containing additional filters to use. e.g. + {'service_type': 'network'}. + + :returns: a list of dict containing the services description. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call. + """ + services = self.list_services() + return _utils._filter_list(services, name_or_id, filters) + + def get_service(self, name_or_id, filters=None): + """Get exactly one Keystone service. + + :param name_or_id: Name or id of the desired service. + :param filters: a dict containing additional filters to use. e.g. + {'service_type': 'network'} + + :returns: a dict containing the services description, i.e. the + following attributes:: + - id: + - name: + - service_type: + - description: + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call or if multiple matches are found. + """ + return _utils._get_entity(self.search_services, name_or_id, filters) + + def delete_service(self, name_or_id): + """Delete a Keystone service. + + :param name_or_id: Service name or id. + + :returns: None + + :raises: ``OpenStackCloudException`` if something goes wrong during + the openstack API call + """ + service = self.get_service(name_or_id=name_or_id) + if service is None: + return + + try: + self.manager.submitTask(_tasks.ServiceDelete(id=service['id'])) + except Exception as e: + self.log.debug( + "Failed to delete service {id}".format(id=service['id']), + exc_info=True) + raise OpenStackCloudException(str(e)) diff --git a/shade/_tasks.py b/shade/_tasks.py index 3dc051c0f..5033fbae3 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -316,3 +316,18 @@ def main(self, client): class MachineSetProvision(task_manager.Task): def main(self, client): return client.ironic_client.node.set_provision_state(**self.args) + + +class ServiceCreate(task_manager.Task): + def main(self, client): + return client.keystone_client.services.create(**self.args) + + +class ServiceList(task_manager.Task): + def main(self, client): + return client.keystone_client.services.list() + + +class ServiceDelete(task_manager.Task): + def main(self, client): + return client.keystone_client.services.delete(**self.args) diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index d7f585b2b..67f38e111 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -51,6 +51,14 @@ def __init__(self, id, name, status): self.status = status +class FakeService(object): + def __init__(self, id, name, type, description=''): + self.id = id + self.name = name + self.type = type + self.description = description + + class FakeUser(object): def __init__(self, id, email, name): self.id = id diff --git a/shade/tests/functional/test_services.py b/shade/tests/functional/test_services.py new file mode 100644 index 000000000..57137f9aa --- /dev/null +++ b/shade/tests/functional/test_services.py @@ -0,0 +1,108 @@ +# Copyright (c) 2015 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. + +""" +test_services +---------------------------------- + +Functional tests for `shade` service resource. +""" + +import string +import random + +from shade import operator_cloud +from shade.exc import OpenStackCloudException +from shade.tests import base + + +class TestServices(base.TestCase): + + service_attributes = ['id', 'name', 'type', 'description'] + + def setUp(self): + super(TestServices, self).setUp() + # Shell should have OS-* envvars from openrc, typically loaded by job + self.operator_cloud = operator_cloud() + + # Generate a random name for services in this test + self.new_service_name = 'test_' + ''.join( + random.choice(string.ascii_lowercase) for _ in range(5)) + + self.addCleanup(self._cleanup_services) + + def _cleanup_services(self): + exception_list = list() + for s in self.operator_cloud.list_services(): + if s['name'].startswith(self.new_service_name): + try: + self.operator_cloud.delete_service(name_or_id=s['id']) + except Exception as e: + # We were unable to delete a service, let's try with next + exception_list.append(e) + continue + if exception_list: + # Raise an error: we must make users aware that something went + # wrong + raise OpenStackCloudException('\n'.join(exception_list)) + + def test_create_service(self): + service = self.operator_cloud.create_service( + name=self.new_service_name + '_create', service_type='test_type', + description='this is a test description') + self.assertIsNotNone(service.get('id')) + + def test_list_services(self): + service = self.operator_cloud.create_service( + name=self.new_service_name + '_list', service_type='test_type') + observed_services = self.operator_cloud.list_services() + self.assertIsInstance(observed_services, list) + found = False + for s in observed_services: + # Test all attributes are returned + if s['id'] == service['id']: + self.assertEqual(self.new_service_name + '_list', + s.get('name')) + self.assertEqual('test_type', s.get('type')) + found = True + self.assertTrue(found, msg='new service not found in service list!') + + def test_delete_service_by_name(self): + # Test delete by name + service = self.operator_cloud.create_service( + name=self.new_service_name + '_delete_by_name', + service_type='test_type') + self.operator_cloud.delete_service(name_or_id=service['name']) + observed_services = self.operator_cloud.list_services() + found = False + for s in observed_services: + if s['id'] == service['id']: + found = True + break + self.failUnlessEqual(False, found, message='service was not deleted!') + + def test_delete_service_by_id(self): + # Test delete by id + service = self.operator_cloud.create_service( + name=self.new_service_name + '_delete_by_id', + service_type='test_type') + self.operator_cloud.delete_service(name_or_id=service['id']) + observed_services = self.operator_cloud.list_services() + found = False + for s in observed_services: + if s['id'] == service['id']: + found = True + self.failUnlessEqual(False, found, message='service was not deleted!') diff --git a/shade/tests/unit/test_services.py b/shade/tests/unit/test_services.py new file mode 100644 index 000000000..1056d6708 --- /dev/null +++ b/shade/tests/unit/test_services.py @@ -0,0 +1,132 @@ +# Copyright (c) 2015 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. + +""" +test_cloud_services +---------------------------------- + +Tests Keystone services commands. +""" + +from mock import patch +from shade import OpenStackCloudException +from shade import OperatorCloud +from shade.tests.fakes import FakeService +from shade.tests.unit import base + + +class CloudServices(base.TestCase): + mock_services = [ + {'id': 'id1', 'name': 'service1', 'type': 'type1', + 'description': 'desc1'}, + {'id': 'id2', 'name': 'service2', 'type': 'type2', + 'description': 'desc2'}, + {'id': 'id3', 'name': 'service3', 'type': 'type2', + 'description': 'desc3'}, + {'id': 'id4', 'name': 'service4', 'type': 'type3', + 'description': 'desc4'} + ] + + def setUp(self): + super(CloudServices, self).setUp() + self.client = OperatorCloud("op_cloud", {}) + self.mock_ks_services = [FakeService(**kwa) for kwa in + self.mock_services] + + @patch.object(OperatorCloud, 'keystone_client') + def test_create_service(self, mock_keystone_client): + kwargs = { + 'name': 'a service', + 'service_type': 'network', + 'description': 'This is a test service' + } + + self.client.create_service(**kwargs) + mock_keystone_client.services.create.assert_called_with(**kwargs) + + @patch.object(OperatorCloud, 'keystone_client') + def test_list_services(self, mock_keystone_client): + mock_keystone_client.services.list.return_value = \ + self.mock_ks_services + + services = self.client.list_services() + mock_keystone_client.services.list.assert_called_with() + + self.assertItemsEqual(self.mock_services, services) + + @patch.object(OperatorCloud, 'keystone_client') + def test_get_service(self, mock_keystone_client): + mock_keystone_client.services.list.return_value = \ + self.mock_ks_services + + # Search by id + service = self.client.get_service(name_or_id='id4') + # test we are getting exactly 1 element + self.assertEqual(service, self.mock_services[3]) + + # Search by name + service = self.client.get_service(name_or_id='service2') + # test we are getting exactly 1 element + self.assertEqual(service, self.mock_services[1]) + + # Not found + service = self.client.get_service(name_or_id='blah!') + self.assertIs(None, service) + + # Multiple matches + # test we are getting an Exception + self.assertRaises(OpenStackCloudException, self.client.get_service, + name_or_id=None, filters={'type': 'type2'}) + + @patch.object(OperatorCloud, 'keystone_client') + def test_search_services(self, mock_keystone_client): + mock_keystone_client.services.list.return_value = \ + self.mock_ks_services + + # Search by id + services = self.client.search_services(name_or_id='id4') + # test we are getting exactly 1 element + self.assertEqual(1, len(services)) + self.assertEqual(services, [self.mock_services[3]]) + + # Search by name + services = self.client.search_services(name_or_id='service2') + # test we are getting exactly 1 element + self.assertEqual(1, len(services)) + self.assertEqual(services, [self.mock_services[1]]) + + # Not found + services = self.client.search_services(name_or_id='blah!') + self.assertEqual(0, len(services)) + + # Multiple matches + services = self.client.search_services( + filters={'type': 'type2'}) + # test we are getting exactly 2 elements + self.assertEqual(2, len(services)) + self.assertEqual(services, [self.mock_services[1], + self.mock_services[2]]) + + @patch.object(OperatorCloud, 'keystone_client') + def test_delete_service(self, mock_keystone_client): + mock_keystone_client.services.list.return_value = \ + self.mock_ks_services + + # Delete by name + self.client.delete_service(name_or_id='service3') + mock_keystone_client.services.delete.assert_called_with(id='id3') + + # Delete by id + self.client.delete_service('id1') + mock_keystone_client.services.delete.assert_called_with(id='id1') From 988ab2a451a5af53a46fdf94ceffb2a864dbf8b1 Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Mon, 27 Apr 2015 00:03:39 +0100 Subject: [PATCH 0301/3836] Add keystone endpoint resource methods This change adds keystone endpoint resource methods to OperatorClouds instances. Only Keystone v2 API is currently supported. Change-Id: Idf3bd7500f5853ec329945d8c8203bd7b6aed52f --- shade/__init__.py | 112 +++++++++++++++++ shade/_tasks.py | 15 +++ shade/tests/fakes.py | 11 ++ shade/tests/functional/test_endpoints.py | 152 +++++++++++++++++++++++ shade/tests/unit/test_endpoints.py | 132 ++++++++++++++++++++ 5 files changed, 422 insertions(+) create mode 100644 shade/tests/functional/test_endpoints.py create mode 100644 shade/tests/unit/test_endpoints.py diff --git a/shade/__init__.py b/shade/__init__.py index 5c9b7f8e5..9e9ee3997 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2824,3 +2824,115 @@ def delete_service(self, name_or_id): "Failed to delete service {id}".format(id=service['id']), exc_info=True) raise OpenStackCloudException(str(e)) + + def create_endpoint(self, service_name_or_id, public_url, + internal_url=None, admin_url=None, region=None): + """Create a Keystone endpoint. + + :param service_name_or_id: Service name or id for this endpoint. + :param public_url: Endpoint public URL. + :param internal_url: Endpoint internal URL. + :param admin_url: Endpoint admin URL. + :param region: Endpoint region. + + :returns: a dict containing the endpoint description. + + :raise OpenStackCloudException: if the service cannot be found or if + something goes wrong during the openstack API call. + """ + # ToDo: support v3 api (dguerri) + service = self.get_service(name_or_id=service_name_or_id) + if service is None: + raise OpenStackCloudException("service {service} not found".format( + service=service_name_or_id)) + try: + endpoint = self.manager.submitTask(_tasks.EndpointCreate( + service_id=service['id'], + region=region, + publicurl=public_url, + internalurl=internal_url, + adminurl=admin_url + )) + except Exception as e: + self.log.debug( + "Failed to create endpoint for service {service}".format( + service=service['name'], exc_info=True)) + raise OpenStackCloudException(str(e)) + return meta.obj_to_dict(endpoint) + + def list_endpoints(self): + """List Keystone endpoints. + + :returns: a list of dict containing the endpoint description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + # ToDo: support v3 api (dguerri) + try: + endpoints = self.manager.submitTask(_tasks.EndpointList()) + except Exception as e: + self.log.debug("Failed to list endpoints", exc_info=True) + raise OpenStackCloudException(str(e)) + return meta.obj_list_to_dict(endpoints) + + def search_endpoints(self, id=None, filters=None): + """List Keystone endpoints. + + :param id: endpoint id. + :param filters: a dict containing additional filters to use. e.g. + {'region': 'region-a.geo-1'} + + :returns: a list of dict containing the endpoint description. Each dict + contains the following attributes:: + - id: + - region: + - public_url: + - internal_url: (optional) + - admin_url: (optional) + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + endpoints = self.list_endpoints() + return _utils._filter_list(endpoints, id, filters) + + def get_endpoint(self, id, filters=None): + """Get exactly one Keystone endpoint. + + :param id: endpoint id. + :param filters: a dict containing additional filters to use. e.g. + {'region': 'region-a.geo-1'} + + :returns: a dict containing the endpoint description. i.e. a dict + containing the following attributes:: + - id: + - region: + - public_url: + - internal_url: (optional) + - admin_url: (optional) + """ + return _utils._get_entity(self.search_endpoints, id, filters) + + def delete_endpoint(self, id): + """Delete a Keystone endpoint. + + :param id: Id of the endpoint to delete. + + :returns: None + + :raises: ``OpenStackCloudException`` if something goes wrong during + the openstack API call. + """ + # ToDo: support v3 api (dguerri) + endpoint = self.get_endpoint(id=id) + if endpoint is None: + return + + try: + self.manager.submitTask(_tasks.EndpointDelete(id=id)) + except Exception as e: + self.log.debug( + "Failed to delete endpoint {id}".format(id=id), + exc_info=True) + raise OpenStackCloudException(str(e)) diff --git a/shade/_tasks.py b/shade/_tasks.py index 5033fbae3..8e8f0a3f9 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -331,3 +331,18 @@ def main(self, client): class ServiceDelete(task_manager.Task): def main(self, client): return client.keystone_client.services.delete(**self.args) + + +class EndpointCreate(task_manager.Task): + def main(self, client): + return client.keystone_client.endpoints.create(**self.args) + + +class EndpointList(task_manager.Task): + def main(self, client): + return client.keystone_client.endpoints.list() + + +class EndpointDelete(task_manager.Task): + def main(self, client): + return client.keystone_client.endpoints.delete(**self.args) diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 67f38e111..e74895f8b 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -20,6 +20,17 @@ """ +class FakeEndpoint(object): + def __init__(self, id, service_id, region, publicurl, internalurl=None, + adminurl=None): + self.id = id + self.service_id = service_id + self.region = region + self.publicurl = publicurl + self.internalurl = internalurl + self.adminurl = adminurl + + class FakeFlavor(object): def __init__(self, id, name): self.id = id diff --git a/shade/tests/functional/test_endpoints.py b/shade/tests/functional/test_endpoints.py new file mode 100644 index 000000000..93cb52bd3 --- /dev/null +++ b/shade/tests/functional/test_endpoints.py @@ -0,0 +1,152 @@ +# Copyright (c) 2015 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. + +""" +test_endpoint +---------------------------------- + +Functional tests for `shade` endpoint resource. +""" + +import string +import random + +from shade import operator_cloud +from shade.exc import OpenStackCloudException +from shade.tests import base + + +class TestEndpoints(base.TestCase): + + endpoint_attributes = ['id', 'region', 'publicurl', 'internalurl', + 'service_id', 'adminurl'] + + def setUp(self): + super(TestEndpoints, self).setUp() + # Shell should have OS-* envvars from openrc, typically loaded by job + self.operator_cloud = operator_cloud() + + # Generate a random name for services and regions in this test + self.new_item_name = 'test_' + ''.join( + random.choice(string.ascii_lowercase) for _ in range(5)) + + self.addCleanup(self._cleanup_services) + self.addCleanup(self._cleanup_endpoints) + + def _cleanup_endpoints(self): + exception_list = list() + for e in self.operator_cloud.list_endpoints(): + if e.get('region') is not None and \ + e['region'].startswith(self.new_item_name): + try: + self.operator_cloud.delete_endpoint(id=e['id']) + except Exception as e: + # We were unable to delete a service, let's try with next + exception_list.append(e) + continue + if exception_list: + # Raise an error: we must make users aware that something went + # wrong + raise OpenStackCloudException('\n'.join(exception_list)) + + def _cleanup_services(self): + exception_list = list() + for s in self.operator_cloud.list_services(): + if s['name'].startswith(self.new_item_name): + try: + self.operator_cloud.delete_service(name_or_id=s['id']) + except Exception as e: + # We were unable to delete a service, let's try with next + exception_list.append(e) + continue + if exception_list: + # Raise an error: we must make users aware that something went + # wrong + raise OpenStackCloudException('\n'.join(exception_list)) + + def test_create_endpoint(self): + service_name = self.new_item_name + '_create' + + service = self.operator_cloud.create_service( + name=service_name, service_type='test_type', + description='this is a test description') + + endpoint = self.operator_cloud.create_endpoint( + service_name_or_id=service['id'], + public_url='http://public.test/', + internal_url='http://internal.test/', + admin_url='http://admin.url/', + region=service_name) + + self.assertIsNotNone(endpoint.get('id')) + + # Test None parameters + endpoint = self.operator_cloud.create_endpoint( + service_name_or_id=service['id'], + public_url='http://public.test/', + region=service_name) + + self.assertIsNotNone(endpoint.get('id')) + + def test_list_endpoints(self): + service_name = self.new_item_name + '_list' + + service = self.operator_cloud.create_service( + name=service_name, service_type='test_type', + description='this is a test description') + + endpoint = self.operator_cloud.create_endpoint( + service_name_or_id=service['id'], + public_url='http://public.test/', + internal_url='http://internal.test/', + region=service_name) + + observed_endpoints = self.operator_cloud.list_endpoints() + found = False + for e in observed_endpoints: + # Test all attributes are returned + if e['id'] == endpoint['id']: + found = True + self.assertEqual(service['id'], e['service_id']) + self.assertEqual('http://public.test/', e['publicurl']) + self.assertEqual('http://internal.test/', e['internalurl']) + self.assertEqual(service_name, e['region']) + + self.assertTrue(found, msg='new endpoint not found in endpoints list!') + + def test_delete_endpoint(self): + service_name = self.new_item_name + '_delete' + + service = self.operator_cloud.create_service( + name=service_name, service_type='test_type', + description='this is a test description') + + endpoint = self.operator_cloud.create_endpoint( + service_name_or_id=service['id'], + public_url='http://public.test/', + internal_url='http://internal.test/', + region=service_name) + + self.operator_cloud.delete_endpoint(endpoint['id']) + + observed_endpoints = self.operator_cloud.list_endpoints() + found = False + for e in observed_endpoints: + if e['id'] == endpoint['id']: + found = True + break + self.failUnlessEqual( + False, found, message='new endpoint was not deleted!') diff --git a/shade/tests/unit/test_endpoints.py b/shade/tests/unit/test_endpoints.py new file mode 100644 index 000000000..d4ad3bb1b --- /dev/null +++ b/shade/tests/unit/test_endpoints.py @@ -0,0 +1,132 @@ +# Copyright (c) 2015 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. + +""" +test_cloud_endpoints +---------------------------------- + +Tests Keystone endpoints commands. +""" + +from mock import patch +from shade import OperatorCloud +from shade.tests.fakes import FakeEndpoint +from shade.tests.unit import base + + +# ToDo: support v3 api (dguerri) +class TestCloudEndpoints(base.TestCase): + mock_endpoints = [ + {'id': 'id1', 'service_id': 'sid1', 'region': 'region1', + 'publicurl': 'purl1', 'internalurl': None, 'adminurl': None}, + {'id': 'id2', 'service_id': 'sid2', 'region': 'region1', + 'publicurl': 'purl2', 'internalurl': None, 'adminurl': None}, + {'id': 'id3', 'service_id': 'sid3', 'region': 'region2', + 'publicurl': 'purl3', 'internalurl': 'iurl3', 'adminurl': 'aurl3'} + ] + + def setUp(self): + super(TestCloudEndpoints, self).setUp() + self.client = OperatorCloud("op_cloud", {}) + self.mock_ks_endpoints = \ + [FakeEndpoint(**kwa) for kwa in self.mock_endpoints] + + @patch.object(OperatorCloud, 'list_services') + @patch.object(OperatorCloud, 'keystone_client') + def test_create_endpoint(self, mock_keystone_client, mock_list_services): + mock_list_services.return_value = [ + { + 'id': 'service_id1', + 'name': 'service1', + 'type': 'type1', + 'description': 'desc1' + } + ] + mock_keystone_client.endpoints.create.return_value = \ + self.mock_ks_endpoints[0] + + endpoint = self.client.create_endpoint( + service_name_or_id='service1', + region='mock_region', + public_url='mock_public_url', + internal_url='mock_internal_url', + admin_url='mock_admin_url' + ) + + mock_keystone_client.endpoints.create.assert_called_with( + service_id='service_id1', + region='mock_region', + publicurl='mock_public_url', + internalurl='mock_internal_url', + adminurl='mock_admin_url' + ) + + # test keys and values are correct + for k, v in self.mock_endpoints[0].items(): + self.assertEquals(v, endpoint.get(k)) + + @patch.object(OperatorCloud, 'keystone_client') + def test_list_endpoints(self, mock_keystone_client): + mock_keystone_client.endpoints.list.return_value = \ + self.mock_ks_endpoints + + endpoints = self.client.list_endpoints() + mock_keystone_client.endpoints.list.assert_called_with() + + # test we are getting exactly len(self.mock_endpoints) elements + self.assertEqual(len(self.mock_endpoints), len(endpoints)) + + # test keys and values are correct + for mock_endpoint in self.mock_endpoints: + found = False + for e in endpoints: + if e['id'] == mock_endpoint['id']: + found = True + for k, v in mock_endpoint.items(): + self.assertEquals(v, e.get(k)) + break + self.assertTrue( + found, msg="endpoint {id} not found!".format( + id=mock_endpoint['id'])) + + @patch.object(OperatorCloud, 'keystone_client') + def test_search_endpoints(self, mock_keystone_client): + mock_keystone_client.endpoints.list.return_value = \ + self.mock_ks_endpoints + + # Search by id + endpoints = self.client.search_endpoints(id='id3') + # # test we are getting exactly 1 element + self.assertEqual(1, len(endpoints)) + for k, v in self.mock_endpoints[2].items(): + self.assertEquals(v, endpoints[0].get(k)) + + # Not found + endpoints = self.client.search_endpoints(id='blah!') + self.assertEqual(0, len(endpoints)) + + # Multiple matches + endpoints = self.client.search_endpoints( + filters={'region': 'region1'}) + # # test we are getting exactly 2 elements + self.assertEqual(2, len(endpoints)) + + @patch.object(OperatorCloud, 'keystone_client') + def test_delete_endpoint(self, mock_keystone_client): + mock_keystone_client.endpoints.list.return_value = \ + self.mock_ks_endpoints + + # Delete by id + self.client.delete_endpoint(id='id2') + mock_keystone_client.endpoints.delete.assert_called_with(id='id2') From a4e9d5cfc0fb796b9c982a2faf8335d21af85020 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 4 May 2015 17:26:36 -0400 Subject: [PATCH 0302/3836] Split security group list operations Security groups can come from either nova or neutron or not be available by the vendor. This uses a new flag from os-client-config to determine where we should get the list from. Also, a new exception is added for features that are not supported. This required a minimum version bump in os-client-config. Change-Id: I4474341ebc255700a06247846c6350981437e8ae --- requirements.txt | 2 +- shade/__init__.py | 61 +++++++++++++++++++++++++++++----- shade/_tasks.py | 7 +++- shade/exc.py | 4 +++ shade/tests/unit/test_shade.py | 25 ++++++++++++++ 5 files changed, 89 insertions(+), 10 deletions(-) diff --git a/requirements.txt b/requirements.txt index 6323ef608..d2edd2eb1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ pbr>=0.11,<2.0 bunch jsonpatch -os-client-config>=1.0.0 +os-client-config>=1.2.0 six python-novaclient>=2.21.0 diff --git a/shade/__init__.py b/shade/__init__.py index cd71efd78..329fec2d7 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -238,6 +238,8 @@ def __init__(self, cloud, auth, self.api_versions = _get_service_values(kwargs, 'api_version') self.image_api_use_tasks = image_api_use_tasks + self.secgroup_source = kwargs.get('secgroup_source', None) + (self.verify, self.cert) = _ssl_args(verify, cacert, cert, key) self._cache = cache.make_region( @@ -804,15 +806,58 @@ def list_flavors(self): "Error fetching flavor list: %s" % e) def list_security_groups(self): - try: - return meta.obj_list_to_dict( - self.manager.submitTask(_tasks.SecurityGroupList()) + # Handle neutron security groups + if self.secgroup_source == 'neutron': + # Neutron returns dicts, so no need to convert objects here. + try: + groups = self.manager.submitTask( + _tasks.NeutronSecurityGroupList())['security_groups'] + except Exception as e: + self.log.debug( + "neutron could not list security groups: {message}".format( + message=str(e)), + exc_info=True) + raise OpenStackCloudException( + "Error fetching security group list" + ) + return groups + + # Handle nova security groups + elif self.secgroup_source == 'nova': + try: + groups = meta.obj_list_to_dict( + self.manager.submitTask(_tasks.NovaSecurityGroupList()) + ) + except Exception as e: + self.log.debug( + "nova could not list security groups: {message}".format( + message=str(e)), + exc_info=True) + raise OpenStackCloudException( + "Error fetching security group list" + ) + # Make Nova data look like Neutron data. This doesn't make them + # look exactly the same, but pretty close. + return [{'id': g['id'], + 'name': g['name'], + 'description': g['description'], + 'security_group_rules': [{ + 'id': r['id'], + 'direction': 'ingress', + 'ethertype': 'IPv4', + 'port_range_min': r['from_port'], + 'port_range_max': r['to_port'], + 'protocol': r['ip_protocol'], + 'remote_ip_prefix': r['ip_range'].get('cidr', None), + 'security_group_id': r['parent_group_id'], + } for r in g['rules']] + } for g in groups] + + # Security groups not supported + else: + raise OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" ) - except Exception as e: - self.log.debug( - "security group list failed: %s" % e, exc_info=True) - raise OpenStackCloudException( - "Error fetching security group list: %s" % e) def list_servers(self): try: diff --git a/shade/_tasks.py b/shade/_tasks.py index 3dc051c0f..cc2d7ba29 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -192,7 +192,12 @@ def main(self, client): client.nova_client.volumes.create_server_volume(**self.args) -class SecurityGroupList(task_manager.Task): +class NeutronSecurityGroupList(task_manager.Task): + def main(self, client): + return client.neutron_client.list_security_groups() + + +class NovaSecurityGroupList(task_manager.Task): def main(self, client): return client.nova_client.security_groups.list() diff --git a/shade/exc.py b/shade/exc.py index 26f1acb51..946961d78 100644 --- a/shade/exc.py +++ b/shade/exc.py @@ -38,3 +38,7 @@ class OpenStackCloudUnavailableService(OpenStackCloudException): class OpenStackCloudUnavailableExtension(OpenStackCloudException): pass + + +class OpenStackCloudUnavailableFeature(OpenStackCloudException): + pass diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index ec4cf8218..ec3ff4c1c 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -324,6 +324,31 @@ class Flavor1(object): flavor2 = self.cloud.get_flavor(1) self.assertEquals(vanilla, flavor2) + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_list_security_groups_neutron(self, mock_nova, mock_neutron): + self.cloud.secgroup_source = 'neutron' + self.cloud.list_security_groups() + self.assertTrue(mock_neutron.list_security_groups.called) + self.assertFalse(mock_nova.security_groups.list.called) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_list_security_groups_nova(self, mock_nova, mock_neutron): + self.cloud.secgroup_source = 'nova' + self.cloud.list_security_groups() + self.assertFalse(mock_neutron.list_security_groups.called) + self.assertTrue(mock_nova.security_groups.list.called) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_list_security_groups_none(self, mock_nova, mock_neutron): + self.cloud.secgroup_source = None + self.assertRaises(shade.OpenStackCloudUnavailableFeature, + self.cloud.list_security_groups) + self.assertFalse(mock_neutron.list_security_groups.called) + self.assertFalse(mock_nova.security_groups.list.called) + class TestShadeOperator(base.TestCase): From 92040ef08cf8329d6f7f09c1bfa2dbd9fed2a0b4 Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Tue, 5 May 2015 00:25:06 +0100 Subject: [PATCH 0303/3836] Add port resource methods This change adds port CRUD methods to shade. Change-Id: I0f91dab236076511c9d35eac4d32afa31b5b9a4f --- requirements.txt | 1 + shade/__init__.py | 164 +++++++++++++++++ shade/_tasks.py | 20 +++ shade/tests/functional/test_port.py | 136 ++++++++++++++ shade/tests/unit/test_port.py | 268 ++++++++++++++++++++++++++++ 5 files changed, 589 insertions(+) create mode 100644 shade/tests/functional/test_port.py create mode 100644 shade/tests/unit/test_port.py diff --git a/requirements.txt b/requirements.txt index 6323ef608..bb38009bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ pbr>=0.11,<2.0 bunch +decorator jsonpatch os-client-config>=1.0.0 six diff --git a/shade/__init__.py b/shade/__init__.py index cd71efd78..42a904ae4 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -13,11 +13,13 @@ # limitations under the License. import hashlib +import inspect import logging import operator import os from cinderclient.v1 import client as cinder_client +from decorator import decorator from dogpile import cache import glanceclient import glanceclient.exc @@ -60,6 +62,32 @@ } +def valid_kwargs(*valid_args): + # This decorator checks if argument passed as **kwargs to a function are + # present in valid_args. + # + # Typically, valid_kwargs is used when we want to distinguish between + # None and omitted arguments and we still want to validate the argument + # list. + # + # Example usage: + # + # @valid_kwargs('opt_arg1', 'opt_arg2') + # def my_func(self, mandatory_arg1, mandatory_arg2, **kwargs): + # ... + # + @decorator + def func_wrapper(func, *args, **kwargs): + argspec = inspect.getargspec(func) + for k in kwargs: + if k not in argspec.args[1:] and k not in valid_args: + raise TypeError( + "{f}() got an unexpected keyword argument " + "'{arg}'".format(f=inspect.stack()[1][3], arg=k)) + return func(*args, **kwargs) + return func_wrapper + + def openstack_clouds(config=None, debug=False): if not config: config = os_client_config.OpenStackConfig() @@ -730,6 +758,10 @@ def search_subnets(self, name_or_id=None, filters=None): subnets = self.list_subnets() return _utils._filter_list(subnets, name_or_id, filters) + def search_ports(self, name_or_id=None, filters=None): + ports = self.list_ports() + return _utils._filter_list(ports, name_or_id, filters) + def search_volumes(self, name_or_id=None, filters=None): volumes = self.list_volumes() return _utils._filter_list(volumes, name_or_id, filters) @@ -778,6 +810,16 @@ def list_subnets(self): raise OpenStackCloudException( "Error fetching subnet list: %s" % e) + def list_ports(self): + try: + return self.manager.submitTask(_tasks.PortList())['ports'] + except Exception as e: + self.log.debug( + "neutron could not list ports: {msg}".format( + msg=str(e)), exc_info=True) + raise OpenStackCloudException( + "error fetching port list: {msg}".format(msg=str(e))) + @_cache_on_arguments(should_cache_fn=_no_pending_volumes) def list_volumes(self, cache=True): if not cache: @@ -903,6 +945,9 @@ def get_router(self, name_or_id, filters=None): def get_subnet(self, name_or_id, filters=None): return _utils._get_entity(self.search_subnets, name_or_id, filters) + def get_port(self, name_or_id, filters=None): + return _utils._get_entity(self.search_ports, name_or_id, filters) + def get_volume(self, name_or_id, filters=None): return _utils._get_entity(self.search_volumes, name_or_id, filters) @@ -2118,6 +2163,125 @@ def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, # a dict). return new_subnet['subnet'] + @valid_kwargs('name', 'admin_state_up', 'mac_address', 'fixed_ips', + 'subnet_id', 'ip_address', 'security_groups', + 'allowed_address_pairs', 'extra_dhcp_opts', 'device_owner', + 'device_id') + def create_port(self, network_id, **kwargs): + """Create a port + + :param network_id: The ID of the network. (Required) + :param name: A symbolic name for the port. (Optional) + :param admin_state_up: The administrative status of the port, + which is up (true, default) or down (false). (Optional) + :param mac_address: The MAC address. (Optional) + :param fixed_ips: If you specify only a subnet ID, OpenStack Networking + allocates an available IP from that subnet to the port. (Optional) + :param subnet_id: If you specify only a subnet ID, OpenStack Networking + allocates an available IP from that subnet to the port. (Optional) + If you specify both a subnet ID and an IP address, OpenStack + Networking tries to allocate the specified address to the port. + :param ip_address: If you specify both a subnet ID and an IP address, + OpenStack Networking tries to allocate the specified address to + the port. + :param security_groups: List of security group UUIDs. (Optional) + :param allowed_address_pairs: Allowed address pairs list (Optional) + For example:: + + [ + { + "ip_address": "23.23.23.1", + "mac_address": "fa:16:3e:c4:cd:3f" + }, ... + ] + :param extra_dhcp_opts: Extra DHCP options. (Optional). + For example:: + + [ + { + "opt_name": "opt name1", + "opt_value": "value1" + }, ... + ] + :param device_owner: The ID of the entity that uses this port. + For example, a DHCP agent. (Optional) + :param device_id: The ID of the device that uses this port. + For example, a virtual server. (Optional) + + :returns: a dictionary describing the created port. + + :raises: ``OpenStackCloudException`` on operation error. + """ + kwargs['network_id'] = network_id + + try: + return self.manager.submitTask( + _tasks.PortCreate(body={'port': kwargs}))['port'] + except Exception as e: + self.log.debug("failed to create a new port for network" + "'{net}'".format(net=network_id), + exc_info=True) + raise OpenStackCloudException( + "error creating a new port for network " + "'{net}': {msg}".format(net=network_id, msg=str(e))) + + @valid_kwargs('name', 'admin_state_up', 'fixed_ips', 'security_groups') + def update_port(self, name_or_id, **kwargs): + """Update a port + + Note: to unset an attribute use None value. To leave an attribute + untouched just omit it. + + :param name_or_id: name or id of the port to update. (Required) + :param name: A symbolic name for the port. (Optional) + :param admin_state_up: The administrative status of the port, + which is up (true) or down (false). (Optional) + :param fixed_ips: If you specify only a subnet ID, OpenStack Networking + allocates an available IP from that subnet to the port. (Optional) + :param security_groups: List of security group UUIDs. (Optional) + + :returns: a dictionary describing the updated port. + + :raises: OpenStackCloudException on operation error. + """ + port = self.get_port(name_or_id=name_or_id) + if port is None: + raise OpenStackCloudException( + "failed to find port '{port}'".format(port=name_or_id)) + + try: + return self.manager.submitTask( + _tasks.PortUpdate( + port=port['id'], body={'port': kwargs}))['port'] + except Exception as e: + self.log.debug("failed to update port '{port}'".format( + port=name_or_id), exc_info=True) + raise OpenStackCloudException( + "failed to update port '{port}': {msg}".format( + port=name_or_id, msg=str(e))) + + def delete_port(self, name_or_id): + """Delete a port + + :param name_or_id: id or name of the port to delete. + + :returns: None. + + :raises: OpenStackCloudException on operation error. + """ + port = self.get_port(name_or_id=name_or_id) + if port is None: + return + + try: + self.manager.submitTask(_tasks.PortDelete(port=port['id'])) + except Exception as e: + self.log.debug("failed to delete port '{port}'".format( + port=name_or_id), exc_info=True) + raise OpenStackCloudException( + "failed to delete port '{port}': {msg}".format( + port=name_or_id, msg=str(e))) + class OperatorCloud(OpenStackCloud): """Represent a privileged/operator connection to an OpenStack Cloud. diff --git a/shade/_tasks.py b/shade/_tasks.py index 3dc051c0f..c179307c4 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -278,6 +278,26 @@ def main(self, client): return client.neutron_client.update_subnet(**self.args) +class PortList(task_manager.Task): + def main(self, client): + return client.neutron_client.list_ports(**self.args) + + +class PortCreate(task_manager.Task): + def main(self, client): + return client.neutron_client.create_port(**self.args) + + +class PortUpdate(task_manager.Task): + def main(self, client): + return client.neutron_client.update_port(**self.args) + + +class PortDelete(task_manager.Task): + def main(self, client): + return client.neutron_client.delete_port(**self.args) + + class MachineCreate(task_manager.Task): def main(self, client): return client.ironic_client.node.create(**self.args) diff --git a/shade/tests/functional/test_port.py b/shade/tests/functional/test_port.py new file mode 100644 index 000000000..82ca096c6 --- /dev/null +++ b/shade/tests/functional/test_port.py @@ -0,0 +1,136 @@ +# Copyright (c) 2015 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. + +""" +test_port +---------------------------------- + +Functional tests for `shade` port resource. +""" + +import string +import random + +from shade import openstack_cloud +from shade.exc import OpenStackCloudException +from shade.tests import base + + +class TestPort(base.TestCase): + + def setUp(self): + super(TestPort, self).setUp() + # Shell should have OS-* envvars from openrc, typically loaded by job + self.cloud = openstack_cloud() + # Skip Neutron tests if neutron is not present + if not self.cloud.has_service('network'): + self.skipTest('Network service not supported by cloud') + + # Generate a unique port name to allow concurrent tests + self.new_port_name = 'test_' + ''.join( + random.choice(string.ascii_lowercase) for _ in range(5)) + + self.addCleanup(self._cleanup_ports) + + def _cleanup_ports(self): + exception_list = list() + + for p in self.cloud.list_ports(): + if p['name'].startswith(self.new_port_name): + try: + self.cloud.delete_port(name_or_id=p['id']) + except Exception as e: + # We were unable to delete this port, let's try with next + exception_list.append(e) + continue + + if exception_list: + # Raise an error: we must make users aware that something went + # wrong + raise OpenStackCloudException('\n'.join(exception_list)) + + def test_create_port(self): + port_name = self.new_port_name + '_create' + + networks = self.cloud.list_networks() + if not networks: + self.assertFalse('no sensible network available') + + port = self.cloud.create_port( + network_id=networks[0]['id'], name=port_name) + self.assertIsInstance(port, dict) + self.assertTrue('id' in port) + self.assertEqual(port.get('name'), port_name) + + def test_get_port(self): + port_name = self.new_port_name + '_get' + + networks = self.cloud.list_networks() + if not networks: + self.assertFalse('no sensible network available') + + port = self.cloud.create_port( + network_id=networks[0]['id'], name=port_name) + self.assertIsInstance(port, dict) + self.assertTrue('id' in port) + self.assertEqual(port.get('name'), port_name) + + updated_port = self.cloud.get_port(name_or_id=port['id']) + # extra_dhcp_opts is added later by Neutron... + if 'extra_dhcp_opts' in updated_port: + del updated_port['extra_dhcp_opts'] + self.assertEqual(port, updated_port) + + def test_update_port(self): + port_name = self.new_port_name + '_update' + new_port_name = port_name + '_new' + + networks = self.cloud.list_networks() + if not networks: + self.assertFalse('no sensible network available') + + self.cloud.create_port( + network_id=networks[0]['id'], name=port_name) + + port = self.cloud.update_port(name_or_id=port_name, + name=new_port_name) + self.assertIsInstance(port, dict) + self.assertEqual(port.get('name'), new_port_name) + + updated_port = self.cloud.get_port(name_or_id=port['id']) + self.assertEqual(port.get('name'), new_port_name) + self.assertEqual(port, updated_port) + + def test_delete_port(self): + port_name = self.new_port_name + '_delete' + + networks = self.cloud.list_networks() + if not networks: + self.assertFalse('no sensible network available') + + port = self.cloud.create_port( + network_id=networks[0]['id'], name=port_name) + self.assertIsInstance(port, dict) + self.assertTrue('id' in port) + self.assertEqual(port.get('name'), port_name) + + updated_port = self.cloud.get_port(name_or_id=port['id']) + self.assertIsNotNone(updated_port) + + self.cloud.delete_port(name_or_id=port_name) + + updated_port = self.cloud.get_port(name_or_id=port['id']) + self.assertIsNone(updated_port) diff --git a/shade/tests/unit/test_port.py b/shade/tests/unit/test_port.py new file mode 100644 index 000000000..ca3b866c9 --- /dev/null +++ b/shade/tests/unit/test_port.py @@ -0,0 +1,268 @@ +# Copyright (c) 2015 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. + +""" +test_port +---------------------------------- + +Test port resource (managed by neutron) +""" + +from mock import patch +from shade import OpenStackCloud +from shade.exc import OpenStackCloudException +from shade.tests.unit import base + + +class TestPort(base.TestCase): + mock_neutron_port_create_rep = { + 'port': { + 'status': 'DOWN', + 'binding:host_id': '', + 'name': 'test-port-name', + 'allowed_address_pairs': [], + 'admin_state_up': True, + 'network_id': 'test-net-id', + 'tenant_id': 'test-tenant-id', + 'binding:vif_details': {}, + 'binding:vnic_type': 'normal', + 'binding:vif_type': 'unbound', + 'device_owner': '', + 'mac_address': '50:1c:0d:e4:f0:0d', + 'binding:profile': {}, + 'fixed_ips': [ + { + 'subnet_id': 'test-subnet-id', + 'ip_address': '29.29.29.29' + } + ], + 'id': 'test-port-id', + 'security_groups': [], + 'device_id': '' + } + } + + mock_neutron_port_update_rep = { + 'port': { + 'status': 'DOWN', + 'binding:host_id': '', + 'name': 'test-port-name-updated', + 'allowed_address_pairs': [], + 'admin_state_up': True, + 'network_id': 'test-net-id', + 'tenant_id': 'test-tenant-id', + 'binding:vif_details': {}, + 'binding:vnic_type': 'normal', + 'binding:vif_type': 'unbound', + 'device_owner': '', + 'mac_address': '50:1c:0d:e4:f0:0d', + 'binding:profile': {}, + 'fixed_ips': [ + { + 'subnet_id': 'test-subnet-id', + 'ip_address': '29.29.29.29' + } + ], + 'id': 'test-port-id', + 'security_groups': [], + 'device_id': '' + } + } + + mock_neutron_port_list_rep = { + 'ports': [ + { + 'status': 'ACTIVE', + 'binding:host_id': 'devstack', + 'name': 'first-port', + 'allowed_address_pairs': [], + 'admin_state_up': True, + 'network_id': '70c1db1f-b701-45bd-96e0-a313ee3430b3', + 'tenant_id': '', + 'extra_dhcp_opts': [], + 'binding:vif_details': { + 'port_filter': True, + 'ovs_hybrid_plug': True + }, + 'binding:vif_type': 'ovs', + 'device_owner': 'network:router_gateway', + 'mac_address': 'fa:16:3e:58:42:ed', + 'binding:profile': {}, + 'binding:vnic_type': 'normal', + 'fixed_ips': [ + { + 'subnet_id': '008ba151-0b8c-4a67-98b5-0d2b87666062', + 'ip_address': '172.24.4.2' + } + ], + 'id': 'd80b1a3b-4fc1-49f3-952e-1e2ab7081d8b', + 'security_groups': [], + 'device_id': '9ae135f4-b6e0-4dad-9e91-3c223e385824' + }, + { + 'status': 'ACTIVE', + 'binding:host_id': 'devstack', + 'name': '', + 'allowed_address_pairs': [], + 'admin_state_up': True, + 'network_id': 'f27aa545-cbdd-4907-b0c6-c9e8b039dcc2', + 'tenant_id': 'd397de8a63f341818f198abb0966f6f3', + 'extra_dhcp_opts': [], + 'binding:vif_details': { + 'port_filter': True, + 'ovs_hybrid_plug': True + }, + 'binding:vif_type': 'ovs', + 'device_owner': 'network:router_interface', + 'mac_address': 'fa:16:3e:bb:3c:e4', + 'binding:profile': {}, + 'binding:vnic_type': 'normal', + 'fixed_ips': [ + { + 'subnet_id': '288bf4a1-51ba-43b6-9d0a-520e9005db17', + 'ip_address': '10.0.0.1' + } + ], + 'id': 'f71a6703-d6de-4be1-a91a-a570ede1d159', + 'security_groups': [], + 'device_id': '9ae135f4-b6e0-4dad-9e91-3c223e385824' + } + ] + } + + def setUp(self): + super(TestPort, self).setUp() + self.client = OpenStackCloud('cloud', {}) + + @patch.object(OpenStackCloud, 'neutron_client') + def test_create_port(self, mock_neutron_client): + mock_neutron_client.create_port.return_value = \ + self.mock_neutron_port_create_rep + + port = self.client.create_port( + network_id='test-net-id', name='test-port-name', + admin_state_up=True) + + mock_neutron_client.create_port.assert_called_with( + body={'port': dict(network_id='test-net-id', name='test-port-name', + admin_state_up=True)}) + self.assertEqual(self.mock_neutron_port_create_rep['port'], port) + + def test_create_port_parameters(self): + """Test that we detect invalid arguments passed to create_port""" + self.assertRaises( + TypeError, self.client.create_port, + network_id='test-net-id', nome='test-port-name', + stato_amministrativo_porta=True) + + @patch.object(OpenStackCloud, 'neutron_client') + def test_create_port_exception(self, mock_neutron_client): + mock_neutron_client.create_port.side_effect = Exception('blah') + + self.assertRaises( + OpenStackCloudException, self.client.create_port, + network_id='test-net-id', name='test-port-name', + admin_state_up=True) + + @patch.object(OpenStackCloud, 'neutron_client') + def test_update_port(self, mock_neutron_client): + mock_neutron_client.list_ports.return_value = \ + self.mock_neutron_port_list_rep + mock_neutron_client.update_port.return_value = \ + self.mock_neutron_port_update_rep + + port = self.client.update_port( + name_or_id='d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b', + name='test-port-name-updated') + + mock_neutron_client.update_port.assert_called_with( + port='d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b', + body={'port': dict(name='test-port-name-updated')}) + self.assertEqual(self.mock_neutron_port_update_rep['port'], port) + + def test_update_port_parameters(self): + """Test that we detect invalid arguments passed to update_port""" + self.assertRaises( + TypeError, self.client.update_port, + name_or_id='test-port-id', nome='test-port-name-updated') + + @patch.object(OpenStackCloud, 'neutron_client') + def test_update_port_exception(self, mock_neutron_client): + mock_neutron_client.list_ports.return_value = \ + self.mock_neutron_port_list_rep + mock_neutron_client.update_port.side_effect = Exception('blah') + + self.assertRaises( + OpenStackCloudException, self.client.update_port, + name_or_id='d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b', + name='test-port-name-updated') + + @patch.object(OpenStackCloud, 'neutron_client') + def test_list_ports(self, mock_neutron_client): + mock_neutron_client.list_ports.return_value = \ + self.mock_neutron_port_list_rep + + ports = self.client.list_ports() + + mock_neutron_client.list_ports.assert_called_with() + self.assertItemsEqual(self.mock_neutron_port_list_rep['ports'], ports) + + @patch.object(OpenStackCloud, 'neutron_client') + def test_list_ports_exception(self, mock_neutron_client): + mock_neutron_client.list_ports.side_effect = Exception('blah') + + self.assertRaises(OpenStackCloudException, self.client.list_ports) + + @patch.object(OpenStackCloud, 'neutron_client') + def test_search_ports_by_id(self, mock_neutron_client): + mock_neutron_client.list_ports.return_value = \ + self.mock_neutron_port_list_rep + + ports = self.client.search_ports( + name_or_id='f71a6703-d6de-4be1-a91a-a570ede1d159') + + mock_neutron_client.list_ports.assert_called_with() + self.assertEquals(1, len(ports)) + self.assertEquals('fa:16:3e:bb:3c:e4', ports[0]['mac_address']) + + @patch.object(OpenStackCloud, 'neutron_client') + def test_search_ports_by_name(self, mock_neutron_client): + mock_neutron_client.list_ports.return_value = \ + self.mock_neutron_port_list_rep + + ports = self.client.search_ports(name_or_id='first-port') + + mock_neutron_client.list_ports.assert_called_with() + self.assertEquals(1, len(ports)) + self.assertEquals('fa:16:3e:58:42:ed', ports[0]['mac_address']) + + @patch.object(OpenStackCloud, 'neutron_client') + def test_search_ports_not_found(self, mock_neutron_client): + mock_neutron_client.list_ports.return_value = \ + self.mock_neutron_port_list_rep + + ports = self.client.search_ports(name_or_id='non-existent') + + mock_neutron_client.list_ports.assert_called_with() + self.assertEquals(0, len(ports)) + + @patch.object(OpenStackCloud, 'neutron_client') + def test_delete_port(self, mock_neutron_client): + mock_neutron_client.list_ports.return_value = \ + self.mock_neutron_port_list_rep + + self.client.delete_port(name_or_id='first-port') + + mock_neutron_client.delete_port.assert_called_with( + port='d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b') From 038ddd38185ace24e96017b3d638952a397e61c6 Mon Sep 17 00:00:00 2001 From: Ghe Rivero Date: Thu, 4 Jun 2015 09:56:33 +0200 Subject: [PATCH 0304/3836] Add cloud vendor files config in doc Although it is possible to include specific config files for unknown vendors, this wasn't specified in the documentation. Change-Id: Ib27277d480e373a8a083e820161e0bdb985de284 --- README.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 193f0a112..ed232c49e 100644 --- a/README.rst +++ b/README.rst @@ -108,10 +108,12 @@ An example config file is probably helpful: You may note a few things. First, since auth_url settings are silly and embarrasingly ugly, known cloud vendor profile information is included and -may be referrenced by name. One of the benefits of that is that auth_url +may be referenced by name. One of the benefits of that is that auth_url isn't the only thing the vendor defaults contain. For instance, since Rackspace lists `rax:database` as the service type for trove, os-client-config -knows that so that you don't have to. +knows that so that you don't have to. In case the cloud vendor profile is not +available, you can provide one called clouds-public.yaml, following the same +location rules previously mentioned for the config files. Also, region_name can be a list of regions. When you call get_all_clouds, you'll get a cloud config object for each cloud/region combo. From acc2cbdc98a80c71be4e1f55580914b9fdf8d4ab Mon Sep 17 00:00:00 2001 From: Ghe Rivero Date: Thu, 4 Jun 2015 10:24:58 +0200 Subject: [PATCH 0305/3836] Raise a warning when using 'cloud' in config The former use of 'cloud' in the config file, was changed in favor of 'profile' to avoid confusions. Change-Id: Iba08746a06ebb397ee1d0f59d5cda41db2b86053 --- os_client_config/config.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/os_client_config/config.py b/os_client_config/config.py index b323e7028..fa1843382 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -14,6 +14,7 @@ import os +import warnings import appdirs import yaml @@ -217,6 +218,11 @@ def _get_base_cloud_config(self, name): # for this. profile_name = our_cloud.get('profile', our_cloud.get('cloud', None)) if profile_name: + if 'cloud' in our_cloud: + warnings.warn( + "clouds.yaml use the keyword 'cloud' to reference a known " + "vendor profile. This has been deprecated in favor of the " + "'profile' keyword.") vendor_filename, vendor_file = self._load_vendor_file() if vendor_file and profile_name in vendor_file['public-clouds']: _auth_update(cloud, vendor_file['public-clouds'][profile_name]) From f02636ecb9b2bc99a31d1a3c6c5901b90436e186 Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Tue, 2 Jun 2015 14:34:21 -0700 Subject: [PATCH 0306/3836] Add support to get a SwiftService object This adds a property of the cloud, swift_service, for the purpose of enabling uploads via SwiftService's more advanced segmenting capabilities. In writing tests, it became clear that cloud.swift_client was leaking exceptions, which we don't want it to do, which is why we add exception handling to that method. Change-Id: I045266e6d5f0b6bed3100905ada6ab35fbb06987 --- shade/__init__.py | 46 ++++++++++++++++++---- shade/tests/unit/test_object.py | 69 +++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 8 deletions(-) create mode 100644 shade/tests/unit/test_object.py diff --git a/shade/__init__.py b/shade/__init__.py index f9e8d14b1..8e8bd1ba0 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -34,6 +34,7 @@ import os_client_config import pbr.version import swiftclient.client as swift_client +import swiftclient.service as swift_service import swiftclient.exceptions as swift_exceptions import troveclient.client as trove_client @@ -272,6 +273,7 @@ def __init__(self, cloud, auth, self._neutron_client = None self._nova_client = None self._swift_client = None + self._swift_service = None self._trove_client = None self.log = logging.getLogger('shade') @@ -587,16 +589,44 @@ def glance_client(self): @property def swift_client(self): if self._swift_client is None: - token = self.auth_token - endpoint = self.get_session_endpoint( - service_key='object-store') - self._swift_client = swift_client.Connection( - preauthurl=endpoint, - preauthtoken=token, - os_options=dict(region_name=self.region_name), - ) + try: + token = self.auth_token + endpoint = self.get_session_endpoint( + service_key='object-store') + self._swift_client = swift_client.Connection( + preauthurl=endpoint, + preauthtoken=token, + os_options=dict(region_name=self.region_name), + ) + except OpenStackCloudException: + raise + except Exception as e: + self.log.debug( + "error constructing swift client", exc_info=True) + raise OpenStackCloudException( + "Error constructing swift client: %s", str(e)) return self._swift_client + @property + def swift_service(self): + if self._swift_service is None: + try: + endpoint = self.get_session_endpoint( + service_key='object-store') + options = dict(os_auth_token=self.auth_token, + os_storage_url=endpoint, + os_region_name=self.region_name) + self._swift_service = swift_service.SwiftService( + options=options) + except OpenStackCloudException: + raise + except Exception as e: + self.log.debug( + "error constructing swift client", exc_info=True) + raise OpenStackCloudException( + "Error constructing swift client: %s", str(e)) + return self._swift_service + @property def cinder_client(self): diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py new file mode 100644 index 000000000..37a7f4510 --- /dev/null +++ b/shade/tests/unit/test_object.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015 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. + +import mock +from swiftclient import client as swift_client +from swiftclient import service as swift_service + +import shade +from shade import exc +from shade.tests.unit import base + + +class TestShade(base.TestCase): + + def setUp(self): + super(TestShade, self).setUp() + self.cloud = shade.openstack_cloud() + + @mock.patch.object(swift_client, 'Connection') + @mock.patch.object(shade.OpenStackCloud, 'auth_token', + new_callable=mock.PropertyMock) + @mock.patch.object(shade.OpenStackCloud, 'get_session_endpoint') + def test_swift_client(self, endpoint_mock, auth_mock, swift_mock): + endpoint_mock.return_value = 'danzig' + auth_mock.return_value = 'yankee' + self.cloud.swift_client + swift_mock.assert_called_with(preauthurl='danzig', + preauthtoken='yankee', + os_options=mock.ANY) + + @mock.patch.object(shade.OpenStackCloud, 'auth_token', + new_callable=mock.PropertyMock) + @mock.patch.object(shade.OpenStackCloud, 'get_session_endpoint') + def test_swift_client_no_endpoint(self, endpoint_mock, auth_mock): + endpoint_mock.side_effect = KeyError + auth_mock.return_value = 'quebec' + e = self.assertRaises( + exc.OpenStackCloudException, lambda: self.cloud.swift_client) + self.assertIn( + 'Error constructing swift client', str(e)) + + @mock.patch.object(shade.OpenStackCloud, 'auth_token') + @mock.patch.object(shade.OpenStackCloud, 'get_session_endpoint') + def test_swift_service(self, endpoint_mock, auth_mock): + endpoint_mock.return_value = 'slayer' + auth_mock.return_value = 'zulu' + self.assertIsInstance(self.cloud.swift_service, + swift_service.SwiftService) + endpoint_mock.assert_called_with(service_key='object-store') + + @mock.patch.object(shade.OpenStackCloud, 'get_session_endpoint') + def test_swift_service_no_endpoint(self, endpoint_mock): + endpoint_mock.side_effect = KeyError + e = self.assertRaises(exc.OpenStackCloudException, lambda: + self.cloud.swift_service) + self.assertIn( + 'Error constructing swift client', str(e)) From 8c445b52242371d98e566bf002f48b94b7dd00b5 Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Mon, 1 Jun 2015 12:10:06 -0700 Subject: [PATCH 0307/3836] Switch to SwiftService for segmented uploads Swift caps individual file sizes at various levels, so we need to adapt to this and upload in segments. swiftclient includes the SwiftService object to do just that. This adds a functional test to try to upload files to swift. Because sending 5GB of nulls does take a while, we're just going to try a segmented upload and an unsegmented one, and see how that goes. Note that this test coverage found a missing ObjectDelete task which has been added. We also add the ObjectCapabilities task so that we can make sure segment_size is never higher than max_file_size. Change-Id: I43903235576245a316ae1921505237e9298b5200 --- shade/__init__.py | 39 +++++++++++++---- shade/_tasks.py | 12 +++++- shade/tests/functional/test_object.py | 60 +++++++++++++++++++++++++++ shade/tests/unit/test_caching.py | 19 +++++++-- shade/tests/unit/test_object.py | 8 ++++ 5 files changed, 125 insertions(+), 13 deletions(-) create mode 100644 shade/tests/functional/test_object.py diff --git a/shade/__init__.py b/shade/__init__.py index 8e8bd1ba0..b7b8865f5 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -52,6 +52,7 @@ OBJECT_SHA256_KEY = 'x-object-meta-x-shade-sha256' IMAGE_MD5_KEY = 'owner_specified.shade.md5' IMAGE_SHA256_KEY = 'owner_specified.shade.sha256' +DEFAULT_OBJECT_SEGMENT_SIZE = 1073741824 # 1GB OBJECT_CONTAINER_ACLS = { @@ -1980,6 +1981,20 @@ def _get_file_hashes(self, filename): return (self._file_hash_cache[filename]['md5'], self._file_hash_cache[filename]['sha256']) + @_cache_on_arguments() + def get_object_capabilities(self): + return self.manager.submitTask(_tasks.ObjectCapabilities()) + + def get_object_segment_size(self, segment_size): + '''get a segment size that will work given capabilities''' + if segment_size is None: + segment_size = DEFAULT_OBJECT_SEGMENT_SIZE + caps = self.get_object_capabilities() + server_max_file_size = caps.get('swift', {}).get('max_file_size', 0) + if segment_size > server_max_file_size: + return server_max_file_size + return segment_size + def is_object_stale( self, container, name, filename, file_md5=None, file_sha256=None): @@ -2011,10 +2026,13 @@ def is_object_stale( def create_object( self, container, name, filename=None, - md5=None, sha256=None, **headers): + md5=None, sha256=None, segment_size=None, + **headers): if not filename: filename = name + segment_size = self.get_object_segment_size(segment_size) + if not md5 or not sha256: (md5, sha256) = self._get_file_hashes(filename) headers[OBJECT_MD5_KEY] = md5 @@ -2024,13 +2042,18 @@ def create_object( self.create_container(container) if self.is_object_stale(container, name, filename, md5, sha256): - with open(filename, 'r') as fileobj: - self.log.debug( - "swift uploading {filename} to {container}/{name}".format( - filename=filename, container=container, name=name)) - self.manager.submitTask(_tasks.ObjectCreate( - container=container, obj=name, contents=fileobj, - headers=headers)) + self.log.debug( + "swift uploading {filename} to {container}/{name}".format( + filename=filename, container=container, name=name)) + upload = swift_service.SwiftUploadObject(source=filename, + object_name=name) + for r in self.manager.submitTask(_tasks.ObjectCreate( + container=container, objects=[upload], + options=dict(headers=headers, + segment_size=segment_size))): + if not r['success']: + raise OpenStackCloudException( + 'Failed at action ({action}) [{error}]:'.format(**r)) def delete_object(self, container, name): if not self.get_object_metadata(container, name): diff --git a/shade/_tasks.py b/shade/_tasks.py index 3dc051c0f..68bdded3b 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -243,9 +243,19 @@ def main(self, client): client.swift_client.post_container(**self.args) +class ObjectCapabilities(task_manager.Task): + def main(self, client): + return client.swift_client.get_capabilities(**self.args) + + +class ObjectDelete(task_manager.Task): + def main(self, client): + return client.swift_client.delete_object(**self.args) + + class ObjectCreate(task_manager.Task): def main(self, client): - client.swift_client.put_object(**self.args) + return client.swift_service.upload(**self.args) class ObjectUpdate(task_manager.Task): diff --git a/shade/tests/functional/test_object.py b/shade/tests/functional/test_object.py new file mode 100644 index 000000000..6202c79a5 --- /dev/null +++ b/shade/tests/functional/test_object.py @@ -0,0 +1,60 @@ +# -*- 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. + +""" +test_object +---------------------------------- + +Functional tests for `shade` object methods. +""" + +import tempfile +import uuid + +from testtools import content + +from shade import openstack_cloud +from shade.tests import base + + +class TestObject(base.TestCase): + + def setUp(self): + super(TestObject, self).setUp() + # Shell should have OS-* envvars from openrc, typically loaded by job + self.cloud = openstack_cloud() + + def test_create_object(self): + '''Test uploading small and large files.''' + container = str(uuid.uuid4()) + self.addDetail('container', content.text_content(container)) + self.addCleanup(self.cloud.delete_container, container) + self.cloud.create_container(container) + sizes = ( + (64 * 1024, 1), # 64K, one segment + (50 * 1024 ** 2, 5) # 50MB, 5 segments + ) + for size, nseg in sizes: + segment_size = round(size / nseg) + with tempfile.NamedTemporaryFile() as sparse_file: + sparse_file.seek(size) + sparse_file.write("\0") + sparse_file.flush() + name = 'test-%d' % size + self.cloud.create_object(container, name, sparse_file.name, + segment_size=segment_size) + self.assertIsNotNone( + self.cloud.get_object_metadata(container, name)) + self.cloud.delete_object(container, name) + self.cloud.delete_container(container) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index b9381e3aa..ef4b103c1 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -303,7 +303,11 @@ def test_create_image_put_v2(self, glance_mock): @mock.patch.object(shade.OpenStackCloud, 'glance_client') @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_create_image_task(self, swift_mock, glance_mock): + @mock.patch.object(shade.OpenStackCloud, 'swift_service') + def test_create_image_task(self, + swift_service_mock, + swift_mock, + glance_mock): self.cloud.api_versions['image'] = '2' self.cloud.image_api_use_tasks = True @@ -311,6 +315,11 @@ class Container(object): name = 'image_upload_v2_test_container' fake_container = Container() + swift_mock.get_capabilities.return_value = { + 'swift': { + 'max_file_size': 1000 + } + } swift_mock.put_container.return_value = fake_container swift_mock.head_object.return_value = {} glance_mock.images.list.return_value = [] @@ -345,9 +354,11 @@ class FakeTask(dict): container='image_upload_v2_test_container') args = {'headers': {'x-object-meta-x-shade-md5': mock.ANY, 'x-object-meta-x-shade-sha256': mock.ANY}, - 'obj': '99 name', - 'container': 'image_upload_v2_test_container'} - swift_mock.put_object.assert_called_with(contents=mock.ANY, **args) + 'segment_size': 1000} + swift_service_mock.upload.assert_called_with( + container='image_upload_v2_test_container', + objects=mock.ANY, + options=args) glance_mock.tasks.create.assert_called_with(type='import', input={ 'import_from': 'image_upload_v2_test_container/99 name', 'image_properties': {'name': '99 name'}}) diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index 37a7f4510..3d886d92a 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -67,3 +67,11 @@ def test_swift_service_no_endpoint(self, endpoint_mock): self.cloud.swift_service) self.assertIn( 'Error constructing swift client', str(e)) + + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + def test_get_object_segment_size(self, swift_mock): + swift_mock.get_capabilities.return_value = {'swift': + {'max_file_size': 1000}} + self.assertEqual(900, self.cloud.get_object_segment_size(900)) + self.assertEqual(1000, self.cloud.get_object_segment_size(1000)) + self.assertEqual(1000, self.cloud.get_object_segment_size(1100)) From 50975f52edd0ffe656d95b67c85b466c7dce12b9 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 4 Jun 2015 15:11:20 -0400 Subject: [PATCH 0308/3836] Add delete method for security groups Adds the API method to delete a security group, using nova or neutron, and consolidates existing and new security group tests into a new file. Change-Id: Ib85b3ec0de8b2b42f691c3e2127adb16e8b14c99 --- shade/__init__.py | 48 +++++++++ shade/_tasks.py | 10 ++ shade/tests/fakes.py | 8 ++ shade/tests/unit/test_security_groups.py | 127 +++++++++++++++++++++++ shade/tests/unit/test_shade.py | 25 ----- 5 files changed, 193 insertions(+), 25 deletions(-) create mode 100644 shade/tests/unit/test_security_groups.py diff --git a/shade/__init__.py b/shade/__init__.py index ebcbcfb07..41a95697f 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2327,6 +2327,54 @@ def delete_port(self, name_or_id): "failed to delete port '{port}': {msg}".format( port=name_or_id, msg=str(e))) + def delete_security_group(self, name_or_id): + """Delete a security group + + :param string name_or_id: The name or unique ID of the security group. + + :raises: OpenStackCloudException on operation error. + :raises: OpenStackCloudUnavailableFeature if security groups are + not supported on this cloud. + """ + secgroup = self.get_security_group(name_or_id) + if secgroup is None: + self.log.debug('security group %s was not found' % name_or_id) + return + + if self.secgroup_source == 'neutron': + try: + self.manager.submitTask( + _tasks.NeutronSecurityGroupDelete( + security_group=secgroup['id'] + ) + ) + except Exception as e: + self.log.debug( + "neutron failed to delete security group '{group}'".format( + group=name_or_id), exc_info=True) + raise OpenStackCloudException( + "failed to delete security group '{group}': {msg}".format( + group=name_or_id, msg=str(e))) + + elif self.secgroup_source == 'nova': + try: + self.manager.submitTask( + _tasks.NovaSecurityGroupDelete(group=secgroup['id']) + ) + except Exception as e: + self.log.debug( + "nova failed to delete security group '{group}'".format( + group=name_or_id), exc_info=True) + raise OpenStackCloudException( + "failed to delete security group '{group}': {msg}".format( + group=name_or_id, msg=str(e))) + + # Security groups not supported + else: + raise OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + class OperatorCloud(OpenStackCloud): """Represent a privileged/operator connection to an OpenStack Cloud. diff --git a/shade/_tasks.py b/shade/_tasks.py index 9d713e91d..72edba23c 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -197,11 +197,21 @@ def main(self, client): return client.neutron_client.list_security_groups() +class NeutronSecurityGroupDelete(task_manager.Task): + def main(self, client): + return client.neutron_client.delete_security_group(**self.args) + + class NovaSecurityGroupList(task_manager.Task): def main(self, client): return client.nova_client.security_groups.list() +class NovaSecurityGroupDelete(task_manager.Task): + def main(self, client): + return client.nova_client.security_groups.delete(**self.args) + + # TODO: Do this with neutron instead of nova if possible class FloatingIPList(task_manager.Task): def main(self, client): diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 3978fa812..b62865f19 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -89,3 +89,11 @@ def __init__(self, id, address, node_id): self.id = id self.address = address self.node_id = node_id + + +class FakeSecgroup(object): + def __init__(self, id, name, description='', rules=None): + self.id = id + self.name = name + self.description = description + self.rules = rules diff --git a/shade/tests/unit/test_security_groups.py b/shade/tests/unit/test_security_groups.py new file mode 100644 index 000000000..a76b3fdaf --- /dev/null +++ b/shade/tests/unit/test_security_groups.py @@ -0,0 +1,127 @@ +# -*- 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. + + +import mock + +import shade +from shade import meta +from shade.tests.unit import base +from shade.tests import fakes + + +neutron_grp_obj = fakes.FakeSecgroup( + id='1', + name='neutron-sec-group', + description='Test Neutron security group', + rules=[ + dict(id='1', port_range_min=80, port_range_max=81, + protocol='tcp', remote_ip_prefix='0.0.0.0/0') + ] +) + + +nova_grp_obj = fakes.FakeSecgroup( + id='2', + name='nova-sec-group', + description='Test Nova security group #1', + rules=[ + dict(id='2', from_port=8000, to_port=8001, ip_protocol='tcp', + ip_range=dict(cidr='0.0.0.0/0'), parent_group_id=None) + ] +) + + +# Neutron returns dicts instead of objects, so the dict versions should +# be used as expected return values from neutron API methods. +neutron_grp_dict = meta.obj_to_dict(neutron_grp_obj) +nova_grp_dict = meta.obj_to_dict(nova_grp_obj) + + +class TestSecurityGroups(base.TestCase): + + def setUp(self): + super(TestSecurityGroups, self).setUp() + self.cloud = shade.openstack_cloud() + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_list_security_groups_neutron(self, mock_nova, mock_neutron): + self.cloud.secgroup_source = 'neutron' + self.cloud.list_security_groups() + self.assertTrue(mock_neutron.list_security_groups.called) + self.assertFalse(mock_nova.security_groups.list.called) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_list_security_groups_nova(self, mock_nova, mock_neutron): + self.cloud.secgroup_source = 'nova' + self.cloud.list_security_groups() + self.assertFalse(mock_neutron.list_security_groups.called) + self.assertTrue(mock_nova.security_groups.list.called) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_list_security_groups_none(self, mock_nova, mock_neutron): + self.cloud.secgroup_source = None + self.assertRaises(shade.OpenStackCloudUnavailableFeature, + self.cloud.list_security_groups) + self.assertFalse(mock_neutron.list_security_groups.called) + self.assertFalse(mock_nova.security_groups.list.called) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_delete_security_group_neutron(self, mock_neutron): + self.cloud.secgroup_source = 'neutron' + neutron_return = dict(security_groups=[neutron_grp_dict]) + mock_neutron.list_security_groups.return_value = neutron_return + self.cloud.delete_security_group('1') + mock_neutron.delete_security_group.assert_called_once_with( + security_group='1' + ) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_delete_security_group_nova(self, mock_nova): + self.cloud.secgroup_source = 'nova' + nova_return = [nova_grp_obj] + mock_nova.security_groups.list.return_value = nova_return + self.cloud.delete_security_group('2') + mock_nova.security_groups.delete.assert_called_once_with( + group='2' + ) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_delete_security_group_neutron_not_found(self, mock_neutron): + self.cloud.secgroup_source = 'neutron' + neutron_return = dict(security_groups=[neutron_grp_dict]) + mock_neutron.list_security_groups.return_value = neutron_return + self.cloud.delete_security_group('doesNotExist') + self.assertFalse(mock_neutron.delete_security_group.called) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_delete_security_group_nova_not_found(self, mock_nova): + self.cloud.secgroup_source = 'nova' + nova_return = [nova_grp_obj] + mock_nova.security_groups.list.return_value = nova_return + self.cloud.delete_security_group('doesNotExist') + self.assertFalse(mock_nova.security_groups.delete.called) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_delete_security_group_none(self, mock_nova, mock_neutron): + self.cloud.secgroup_source = None + self.assertRaises(shade.OpenStackCloudUnavailableFeature, + self.cloud.delete_security_group, + 'doesNotExist') + self.assertFalse(mock_neutron.delete_security_group.called) + self.assertFalse(mock_nova.security_groups.delete.called) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 8098f990a..a46ec1a3d 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -325,31 +325,6 @@ class Flavor1(object): flavor2 = self.cloud.get_flavor(1) self.assertEquals(vanilla, flavor2) - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_list_security_groups_neutron(self, mock_nova, mock_neutron): - self.cloud.secgroup_source = 'neutron' - self.cloud.list_security_groups() - self.assertTrue(mock_neutron.list_security_groups.called) - self.assertFalse(mock_nova.security_groups.list.called) - - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_list_security_groups_nova(self, mock_nova, mock_neutron): - self.cloud.secgroup_source = 'nova' - self.cloud.list_security_groups() - self.assertFalse(mock_neutron.list_security_groups.called) - self.assertTrue(mock_nova.security_groups.list.called) - - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_list_security_groups_none(self, mock_nova, mock_neutron): - self.cloud.secgroup_source = None - self.assertRaises(shade.OpenStackCloudUnavailableFeature, - self.cloud.list_security_groups) - self.assertFalse(mock_neutron.list_security_groups.called) - self.assertFalse(mock_nova.security_groups.list.called) - class TestShadeOperator(base.TestCase): From cf0bdc420aea3b4d282c01cdda17d14738ff13f7 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 4 Jun 2015 21:22:13 -0400 Subject: [PATCH 0309/3836] Add create method for security groups Adds the API method to create a security group, using nova or neutron. Change-Id: I495c8272220837982b7e1f880646748a30b7d727 --- shade/__init__.py | 47 ++++++++++++++++++++++++ shade/_tasks.py | 10 +++++ shade/tests/unit/test_security_groups.py | 31 ++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 41a95697f..2302a161c 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2327,6 +2327,53 @@ def delete_port(self, name_or_id): "failed to delete port '{port}': {msg}".format( port=name_or_id, msg=str(e))) + def create_security_group(self, name, description): + """Create a new security group + + :param string name: A name for the security group. + :param string description: Describes the security group. + + :raises: OpenStackCloudException on operation error. + :raises: OpenStackCloudUnavailableFeature if security groups are + not supported on this cloud. + """ + if self.secgroup_source == 'neutron': + try: + self.manager.submitTask( + _tasks.NeutronSecurityGroupCreate( + body=dict(security_group=dict(name=name, + description=description)) + ) + ) + except Exception as e: + self.log.debug( + "neutron failed to create security group '{name}'".format( + name=name), exc_info=True) + raise OpenStackCloudException( + "failed to create security group '{name}': {msg}".format( + name=name, msg=str(e))) + + elif self.secgroup_source == 'nova': + try: + self.manager.submitTask( + _tasks.NovaSecurityGroupCreate( + name=name, description=description + ) + ) + except Exception as e: + self.log.debug( + "nova failed to create security group '{name}'".format( + name=name), exc_info=True) + raise OpenStackCloudException( + "failed to create security group '{name}': {msg}".format( + name=name, msg=str(e))) + + # Security groups not supported + else: + raise OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + def delete_security_group(self, name_or_id): """Delete a security group diff --git a/shade/_tasks.py b/shade/_tasks.py index 72edba23c..854ca1fbc 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -197,6 +197,11 @@ def main(self, client): return client.neutron_client.list_security_groups() +class NeutronSecurityGroupCreate(task_manager.Task): + def main(self, client): + return client.neutron_client.create_security_group(**self.args) + + class NeutronSecurityGroupDelete(task_manager.Task): def main(self, client): return client.neutron_client.delete_security_group(**self.args) @@ -207,6 +212,11 @@ def main(self, client): return client.nova_client.security_groups.list() +class NovaSecurityGroupCreate(task_manager.Task): + def main(self, client): + return client.nova_client.security_groups.create(**self.args) + + class NovaSecurityGroupDelete(task_manager.Task): def main(self, client): return client.nova_client.security_groups.delete(**self.args) diff --git a/shade/tests/unit/test_security_groups.py b/shade/tests/unit/test_security_groups.py index a76b3fdaf..88f8c8bb5 100644 --- a/shade/tests/unit/test_security_groups.py +++ b/shade/tests/unit/test_security_groups.py @@ -125,3 +125,34 @@ def test_delete_security_group_none(self, mock_nova, mock_neutron): 'doesNotExist') self.assertFalse(mock_neutron.delete_security_group.called) self.assertFalse(mock_nova.security_groups.delete.called) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_security_group_neutron(self, mock_neutron): + self.cloud.secgroup_source = 'neutron' + group_name = self.getUniqueString() + group_desc = 'security group from test_create_security_group_neutron' + self.cloud.create_security_group(group_name, group_desc) + mock_neutron.create_security_group.assert_called_once_with( + body=dict(security_group=dict(name=group_name, + description=group_desc)) + ) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_create_security_group_nova(self, mock_nova): + self.cloud.secgroup_source = 'nova' + group_name = self.getUniqueString() + group_desc = 'security group from test_create_security_group_neutron' + self.cloud.create_security_group(group_name, group_desc) + mock_nova.security_groups.create.assert_called_once_with( + name=group_name, description=group_desc + ) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_create_security_group_none(self, mock_nova, mock_neutron): + self.cloud.secgroup_source = None + self.assertRaises(shade.OpenStackCloudUnavailableFeature, + self.cloud.create_security_group, + '', '') + self.assertFalse(mock_neutron.create_security_group.called) + self.assertFalse(mock_nova.security_groups.create.called) From b1e562c3e59f64118d184e8655c7bfe343a09cdc Mon Sep 17 00:00:00 2001 From: Ghe Rivero Date: Thu, 4 Jun 2015 11:21:38 +0200 Subject: [PATCH 0310/3836] Change references of "clouds.yaml" for real file The config file clouds.yaml can be located in several places, and even the file extension can be .yml. Replace all user visible messages making reference for that file to show the full path of the config file used. Change-Id: I489d87368b72dfe69b7d4e3c07ba5d5249c45667 --- os_client_config/config.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index fa1843382..dd80e2a72 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -120,10 +120,11 @@ def __init__(self, config_files=None, vendor_files=None, self.envvar_key = os.environ.pop('OS_CLOUD_NAME', 'envvars') if self.envvar_key in self.cloud_config['clouds']: raise exceptions.OpenStackConfigException( - 'clouds.yaml defines a cloud named "{0}", but' - ' OS_CLOUD_NAME is also set to "{0}". Please rename' + '"{0}" defines a cloud named "{1}", but' + ' OS_CLOUD_NAME is also set to "{1}". Please rename' ' either your environment based cloud, or one of your' - ' file-based clouds.'.format(self.envvar_key)) + ' file-based clouds.'.format(self.config_filename, + self.envvar_key)) envvars = _get_os_environ() if envvars: @@ -220,9 +221,9 @@ def _get_base_cloud_config(self, name): if profile_name: if 'cloud' in our_cloud: warnings.warn( - "clouds.yaml use the keyword 'cloud' to reference a known " + "{0} use the keyword 'cloud' to reference a known " "vendor profile. This has been deprecated in favor of the " - "'profile' keyword.") + "'profile' keyword.".format(self.config_filename)) vendor_filename, vendor_file = self._load_vendor_file() if vendor_file and profile_name in vendor_file['public-clouds']: _auth_update(cloud, vendor_file['public-clouds'][profile_name]) From bc253d62b9a61575c3ba2f443b2429b5df904905 Mon Sep 17 00:00:00 2001 From: Ghe Rivero Date: Thu, 4 Jun 2015 13:06:43 +0200 Subject: [PATCH 0311/3836] Raise a warning with conflicting SSL params Setting a cacert to check the cloud cert is useless when changing the default verify flag to False since this will have precedence. Raise a warning to alert the user about this behavior. Change-Id: I099d03fef5e8da0d6eed572613f4693604173ecd --- os_client_config/cloud_config.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index d8ac85d55..018908e92 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -12,6 +12,8 @@ # License for the specific language governing permissions and limitations # under the License. +import warnings + class CloudConfig(object): def __init__(self, name, region, config): @@ -46,6 +48,11 @@ def get_requests_verify_args(self): verify = self.config['cacert'] else: verify = self.config['verify'] + if self.config['cacert']: + warnings.warn( + "You are specifying a cacert for the cloud {0} but " + "also to ignore the host verification. The host SSL cert " + "will not be verified.".format(self.name)) cert = self.config.get('cert', None) if cert: From 95beafeadd99383fc99dd8aff5bfc9ce8ece7030 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 5 Jun 2015 10:40:01 -0400 Subject: [PATCH 0312/3836] Stringify project details There are some clouds that have things like integer project names. That's no fun when they are interperted at int, so stringify them. Stringifying integer project ids is apparently less important, but does not hurt. Change-Id: Ife9ecaa28c552d589dbea9a065da0dfa483592eb --- os_client_config/config.py | 4 ++-- os_client_config/tests/base.py | 8 ++++++++ os_client_config/tests/test_config.py | 14 ++++++++++++-- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index b323e7028..432d1b470 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -251,10 +251,10 @@ def _fix_backwards_project(self, cloud): target = None for key in possible_values: if key in cloud: - target = cloud[key] + target = str(cloud[key]) del cloud[key] if key in cloud['auth']: - target = cloud['auth'][key] + target = str(cloud['auth'][key]) del cloud['auth'][key] if target: cloud['auth'][target_key] = target diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py index 6a412bfd6..967f119dd 100644 --- a/os_client_config/tests/base.py +++ b/os_client_config/tests/base.py @@ -56,6 +56,14 @@ }, 'region-name': 'test-region', }, + '_test-cloud-int-project_': { + 'auth': { + 'username': 'testuser', + 'password': 'testpass', + 'project_id': 12345, + }, + 'region_name': 'test-region', + }, }, 'cache': {'max_age': 1}, } diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 25e1cc199..562e40815 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -73,6 +73,12 @@ def test_get_one_cloud_with_config_files(self): cc = c.get_one_cloud('_test_cloud_no_vendor') self._assert_cloud_details(cc) + def test_get_one_cloud_with_int_project_id(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + cc = c.get_one_cloud('_test-cloud-int-project_') + self.assertEqual('12345', cc.auth['project_name']) + def test_no_environ(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) @@ -95,8 +101,12 @@ def test_get_one_cloud_auth_merge(self): def test_get_cloud_names(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml]) - self.assertEqual(['_test-cloud_', '_test_cloud_no_vendor'], - sorted(c.get_cloud_names())) + self.assertEqual( + ['_test-cloud-int-project_', + '_test-cloud_', + '_test_cloud_no_vendor', + ], + sorted(c.get_cloud_names())) c = config.OpenStackConfig(config_files=[self.no_yaml], vendor_files=[self.no_yaml]) for k in os.environ.keys(): From 2c926e618c17933b29a077d48bfda2dda0131a93 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 5 Jun 2015 10:58:57 -0400 Subject: [PATCH 0313/3836] Extract logging config into a helper function It's an error to have a library class set up logging output for you, as this will prevent a consumer from doing what they need to do with their logging config. However, for simple scripts, the complexity of what needs to be done for "normal" operation is a bit onerous. Create a simple helper function that a user can run to set up the two most common types of logging expected for simple scripts. Change-Id: I81ef597712ff885d95194c6e29a45a1b2e7f86b9 --- shade/__init__.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index ebcbcfb07..836facb39 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -49,6 +49,7 @@ from shade import _tasks from shade import _utils + __version__ = pbr.version.VersionInfo('shade').version_string() OBJECT_MD5_KEY = 'x-object-meta-x-shade-md5' OBJECT_SHA256_KEY = 'x-object-meta-x-shade-sha256' @@ -88,6 +89,16 @@ def func_wrapper(func, *args, **kwargs): return func_wrapper +def simple_logging(debug=False): + if debug: + log_level = logging.DEBUG + else: + log_level = logging.INFO + log = logging.getLogger('shade') + log.addHandler(logging.StreamHandler()) + log.setLevel(log_level) + + def openstack_clouds(config=None, debug=False): if not config: config = os_client_config.OpenStackConfig() @@ -213,8 +224,7 @@ class OpenStackCloud(object): :param string cert: A path to a client certificate to pass to requests. (optional) :param string key: A path to a client key to pass to requests. (optional) - :param bool debug: Enable or disable debug logging (optional, defaults to - False) + :param bool debug: Deprecated and unused parameter. :param int cache_interval: How long to cache items fetched from the cloud. Value will be passed to dogpile.cache. None means do not cache at all. @@ -247,6 +257,8 @@ def __init__(self, cloud, auth, image_api_use_tasks=False, **kwargs): + self.log = logging.getLogger('shade') + self.name = cloud self.auth = auth self.region_name = region_name @@ -290,13 +302,6 @@ def __init__(self, cloud, auth, self._swift_client = None self._trove_client = None - self.log = logging.getLogger('shade') - log_level = logging.INFO - if debug: - log_level = logging.DEBUG - self.log.setLevel(log_level) - self.log.addHandler(logging.StreamHandler()) - def _make_cache_key(self, namespace, fn): fname = fn.__name__ if namespace is None: @@ -348,11 +353,6 @@ def nova_client(self): @property def keystone_session(self): if self._keystone_session is None: - # keystoneclient does crazy things with logging that are - # none of them interesting - keystone_logging = logging.getLogger('keystoneclient') - keystone_logging.addHandler(logging.NullHandler()) - try: auth_plugin = ksc_auth.get_plugin_class(self.auth_type) except Exception as e: @@ -2363,8 +2363,6 @@ def auth_token(self): @property def ironic_client(self): if self._ironic_client is None: - ironic_logging = logging.getLogger('ironicclient') - ironic_logging.addHandler(logging.NullHandler()) token = self.auth_token if self.auth_type in (None, "None", ''): # TODO: This needs to be improved logic wise, perhaps a list, From 92b4cc30d7388917a27931b35f53a2655688b3ef Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 21 Apr 2015 10:17:18 -0400 Subject: [PATCH 0314/3836] Add inventory command to shade The ansible inventory plugin is actually really useful for general purpose debugging of cloud metadata. Also, having this in tree means we can test it and make sure we don't break the interface for people. Change-Id: Ibb1463936fac9a7e959e291a3856c4f8d32898e0 --- setup.cfg | 4 ++ shade/__init__.py | 1 + shade/cmd/__init__.py | 0 shade/cmd/inventory.py | 66 +++++++++++++++++++++ shade/inventory.py | 61 ++++++++++++++++++++ shade/tests/functional/test_inventory.py | 73 ++++++++++++++++++++++++ 6 files changed, 205 insertions(+) create mode 100644 shade/cmd/__init__.py create mode 100755 shade/cmd/inventory.py create mode 100644 shade/inventory.py create mode 100644 shade/tests/functional/test_inventory.py diff --git a/setup.cfg b/setup.cfg index 4e7aebba1..b726f8d52 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,6 +18,10 @@ classifier = Programming Language :: Python :: 3 Programming Language :: Python :: 3.4 +[entry_points] +console_scripts = + shade-inventory = shade.cmd.inventory:main + [build_sphinx] source-dir = doc/source build-dir = doc/build diff --git a/shade/__init__.py b/shade/__init__.py index 836facb39..bbe05bcbd 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1612,6 +1612,7 @@ def get_server_dict(self, name_or_id): return self.get_openstack_vars(server) def get_server_meta(self, server): + # TODO(mordred) remove once ansible has moved to Inventory interface server_vars = meta.get_hostvars_from_server(self, server) groups = meta.get_groups_from_server(self, server, server_vars) return dict(server_vars=server_vars, groups=groups) diff --git a/shade/cmd/__init__.py b/shade/cmd/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/shade/cmd/inventory.py b/shade/cmd/inventory.py new file mode 100755 index 000000000..c4d396f46 --- /dev/null +++ b/shade/cmd/inventory.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python +# Copyright (c) 2015 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. + +import argparse +import json +import sys +import yaml + +import shade +import shade.inventory + + +def output_format_dict(data, use_yaml): + if use_yaml: + return yaml.safe_dump(data, default_flow_style=False) + else: + return json.dumps(data, sort_keys=True, indent=2) + + +def parse_args(): + parser = argparse.ArgumentParser(description='OpenStack Inventory Module') + parser.add_argument('--refresh', action='store_true', + help='Refresh cached information') + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('--list', action='store_true', + help='List active servers') + group.add_argument('--host', help='List details about the specific host') + parser.add_argument('--yaml', action='store_true', default=False, + help='Output data in nicely readable yaml') + parser.add_argument('--debug', action='store_true', default=False, + help='Enable debug output') + return parser.parse_args() + + +def main(): + args = parse_args() + try: + shade.simple_logging(debug=args.debug) + inventory = shade.inventory.OpenStackInventory( + refresh=args.refresh) + if args.list: + output = inventory.list_hosts() + elif args.host: + output = inventory.get_host(args.host) + print(output_format_dict(output, args.yaml)) + except shade.OpenStackCloudException as e: + sys.stderr.write(e.message + '\n') + sys.exit(1) + sys.exit(0) + + +if __name__ == '__main__': + main() diff --git a/shade/inventory.py b/shade/inventory.py new file mode 100644 index 000000000..d66808a84 --- /dev/null +++ b/shade/inventory.py @@ -0,0 +1,61 @@ +# Copyright (c) 2015 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. + +import os_client_config + +import shade +from shade import _utils + + +class OpenStackInventory(object): + + def __init__( + self, config_files=[], refresh=False): + config = os_client_config.config.OpenStackConfig( + config_files=os_client_config.config.CONFIG_FILES + config_files) + + self.clouds = [ + shade.OpenStackCloud( + cloud=f.name, + cache_interval=config.get_cache_max_age(), + cache_class=config.get_cache_class(), + cache_arguments=config.get_cache_arguments(), + **f.config) + for f in config.get_all_clouds() + ] + + # Handle manual invalidation of entire persistent cache + if refresh: + for cloud in self.clouds: + cloud._cache.invalidate() + + def list_hosts(self): + hostvars = [] + + for cloud in self.clouds: + + # Cycle on servers + for server in cloud.list_servers(): + + meta = cloud.get_openstack_vars(server) + hostvars.append(meta) + + return hostvars + + def search_hosts(self, name_or_id=None, filters=None): + hosts = self.list_hosts() + return _utils._filter_list(hosts, name_or_id, filters) + + def get_host(self, name_or_id, filters=None): + return _utils._get_entity(self.search_hosts, name_or_id, filters) diff --git a/shade/tests/functional/test_inventory.py b/shade/tests/functional/test_inventory.py new file mode 100644 index 000000000..3a4bfcdb8 --- /dev/null +++ b/shade/tests/functional/test_inventory.py @@ -0,0 +1,73 @@ +# Copyright (c) 2015 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. + +""" +test_inventory +---------------------------------- + +Functional tests for `shade` inventory methods. +""" + +from shade import openstack_cloud +from shade import inventory + +from shade.tests import base +from shade.tests.functional.util import pick_flavor, pick_image + + +class TestInventory(base.TestCase): + def setUp(self): + super(TestInventory, self).setUp() + # Shell should have OS-* envvars from openrc, typically loaded by job + self.cloud = openstack_cloud() + self.inventory = inventory.OpenStackInventory() + self.server_name = 'test_inventory_server' + self.nova = self.cloud.nova_client + self.flavor = pick_flavor(self.nova.flavors.list()) + if self.flavor is None: + self.assertTrue(False, 'no sensible flavor available') + self.image = pick_image(self.nova.images.list()) + if self.image is None: + self.assertTrue(False, 'no sensible image available') + self.addCleanup(self._cleanup_servers) + self.cloud.create_server( + name=self.server_name, image=self.image, flavor=self.flavor, + wait=True, auto_ip=True) + + def _cleanup_servers(self): + for i in self.nova.servers.list(): + if i.name.startswith(self.server_name): + self.nova.servers.delete(i) + + def _test_host_content(self, host): + self.assertEquals(host['image']['id'], self.image.id) + self.assertNotIn('links', host['image']) + self.assertEquals(host['flavor']['id'], self.flavor.id) + self.assertNotIn('links', host['flavor']) + self.assertNotIn('links', host) + self.assertIsInstance(host['volumes'], list) + self.assertIsInstance(host['metadata'], dict) + self.assertIn('interface_ip', host) + + def test_get_host(self): + host = self.inventory.get_host(self.server_name) + self.assertIsNotNone(host) + self.assertEquals(host['name'], self.server_name) + self._test_host_content(host) + host_found = False + for host in self.inventory.list_hosts(): + if host['name'] == self.server_name: + host_found = True + self._test_host_content(host) + self.assertTrue(host_found) From fae69e63f2196485921fe8b829da73cb9afcdc1e Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 5 Jun 2015 14:06:25 -0400 Subject: [PATCH 0315/3836] Port ironic client port.get() to a Task This call was not yet a Task. Change-Id: I3c28b9dfb03d8eca6936973ae43a01eedd6e6f8b --- shade/__init__.py | 8 ++++++-- shade/_tasks.py | 5 +++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index ebcbcfb07..bab04690f 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2411,7 +2411,10 @@ def list_nics_for_machine(self, uuid): def get_nic_by_mac(self, mac): try: - return meta.obj_to_dict(self.ironic_client.port.get(mac)) + return meta.obj_to_dict( + self.manager.submitTask( + _tasks.MachineNodePortGet(port_id=mac)) + ) except ironic_exceptions.ClientException: return None @@ -2436,7 +2439,8 @@ def get_machine(self, name_or_id): def get_machine_by_mac(self, mac): try: - port = self.ironic_client.port.get(mac) + port = self.manager.submitTask( + _tasks.MachineNodePortGet(port_id=mac)) return meta.obj_to_dict( self.ironic_client.node.get(port.node_uuid)) except ironic_exceptions.ClientException: diff --git a/shade/_tasks.py b/shade/_tasks.py index 9d713e91d..b42375973 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -318,6 +318,11 @@ def main(self, client): return client.ironic_client.node.update(**self.args) +class MachinePortGet(task_manager.Task): + def main(self, client): + return client.ironic_client.port.get(**self.args) + + class MachinePortCreate(task_manager.Task): def main(self, client): return client.ironic_client.port.create(**self.args) From 0b2a5557360c1f3eda37c4cf836ac54139822533 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 5 Jun 2015 14:14:44 -0400 Subject: [PATCH 0316/3836] Port ironic client port.get_by_address() to a Task This call was not yet a Task. Change-Id: Ibc46791240fe2f3671bc5d41f0134704fdb57bf7 --- shade/__init__.py | 7 +++---- shade/_tasks.py | 5 +++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index bab04690f..297b7a423 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2531,11 +2531,10 @@ def unregister_machine(self, nics, uuid): # the action if the node is in an Active state as the API would. for nic in nics: try: + port_id = self.manager.submitTask( + _tasks.MachinePortGetByAddress(address=nic['mac'])) self.manager.submitTask( - _tasks.MachinePortDelete( - port_id=( - self.ironic_client.port.get_by_address(nic['mac']) - ))) + _tasks.MachinePortDelete(port_id=port_id)) except Exception as e: self.log.debug( diff --git a/shade/_tasks.py b/shade/_tasks.py index b42375973..7e745a887 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -323,6 +323,11 @@ def main(self, client): return client.ironic_client.port.get(**self.args) +class MachinePortGetByAddress(task_manager.Task): + def main(self, client): + return client.ironic_client.port.get_by_address(**self.args) + + class MachinePortCreate(task_manager.Task): def main(self, client): return client.ironic_client.port.create(**self.args) From 7e605f963fe88eded0017a4fdf85ebf13b4c52c1 Mon Sep 17 00:00:00 2001 From: Ghe Rivero Date: Thu, 4 Jun 2015 13:39:12 +0200 Subject: [PATCH 0317/3836] Add SSL documentation to README.rst Explain usage and warn avoid behavior with conflicting cacert and verify options. Change-Id: I25b43ba47bd0feb941b649265c6e67723a93e277 --- README.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.rst b/README.rst index ed232c49e..4d3f50cb7 100644 --- a/README.rst +++ b/README.rst @@ -135,6 +135,20 @@ as a result of a chosen plugin need to go into the auth dict. For password auth, this includes `auth_url`, `username` and `password` as well as anything related to domains, projects and trusts. +SSL Settings +------------ + +When the access to a cloud is done via a secure connection, `os-client-config` +will always verify the SSL cert by default. This can be disabled by setting +`verify` to `False`. In case the cert is signed by an unknown CA, a specific +cacert can be provided via `cacert`. **WARNING:** `verify` will always have +precedence over `cacert`, so when setting a CA cert but disabling `verify`, the +cloud cert will never be validated. + +Client certs are also configurable. `cert` will be the client cert file +location. In case the cert key is not included within the client cert file, +its file location needs to be set via `key`. + Cache Settings -------------- From a2f25244d352f3f537f602f6a4b8f0ca785c05b6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 5 Jun 2015 15:48:17 -0400 Subject: [PATCH 0318/3836] Add support for OVH Public Cloud Change-Id: If2b4bc34a159d1ef4180fbd5de9bbedfaa5b3e82 --- doc/source/vendor-support.rst | 17 +++++++++++++++++ os_client_config/vendors.py | 11 +++++++++++ 2 files changed, 28 insertions(+) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index 351d66665..e7d51c01a 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -135,3 +135,20 @@ RegionOne RegionOne * Images must be in `qcow2` format * Floating IPs are provided by Nova * Security groups are provided by Nova + +ovh +--- + +https://auth.cloud.ovh.net/v2.0 + +============== ================ +Region Name Human Name +============== ================ +SBG-1 Strassbourg, FR +============== ================ + +* Identity API Version is 2 +* Image API Version is 1 +* Images must be in `raw` format +* Floating IPs are provided by Neutron +* Security groups are provided by Neutron diff --git a/os_client_config/vendors.py b/os_client_config/vendors.py index ad04c4dc1..e02b32517 100644 --- a/os_client_config/vendors.py +++ b/os_client_config/vendors.py @@ -84,4 +84,15 @@ secgroup_source='nova', floating_ip_source='nova', ), + ovh=dict( + auth=dict( + auth_url='https://auth.cloud.ovh.net/v2.0', + ), + region_name='SBG1', + identity_api_version='2', + image_api_version='1', + image_format='raw', + secgroup_source='neutron', + floating_ip_source='neutron', + ), ) From d710accb3fd95eb7e5bc108a484e3ecc78498af7 Mon Sep 17 00:00:00 2001 From: Ghe Rivero Date: Sat, 6 Jun 2015 13:32:20 +0200 Subject: [PATCH 0319/3836] Some cleanup in the README.rst Change-Id: I9f7c6c727708a9095566bce8d4ff03837be95d07 --- README.rst | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/README.rst b/README.rst index 4d3f50cb7..9cdda45dc 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ os-client-config =============================== -os-client-config is a library for collecting client configuration for +`os-client-config` is a library for collecting client configuration for using an OpenStack cloud in a consistent and comprehensive manner. It will find cloud config for as few as 1 cloud and as many as you want to put in a config file. It will read environment variables and config files, @@ -10,19 +10,19 @@ and it also contains some vendor specific default values so that you don't have to know extra info to use OpenStack * If you have a config file, you will get the clouds listed in it -* If you have environment variables, you will get a cloud named 'envvars' -* If you have neither, you will get a cloud named 'defaults' with base defaults +* If you have environment variables, you will get a cloud named `envvars` +* If you have neither, you will get a cloud named `defaults` with base defaults Environment Variables --------------------- -os-client-config honors all of the normal `OS_*` variables. It does not +`os-client-config` honors all of the normal `OS_*` variables. It does not provide backwards compatibility to service-specific variables such as `NOVA_USERNAME`. -If you have OpenStack environment variables set, os-client-config will produce -a cloud config object named "envvars" containing your values from the -environment. If you don't like the name "envvars", that's ok, you can override +If you have OpenStack environment variables set, `os-client-config` will produce +a cloud config object named `envvars` containing your values from the +environment. If you don't like the name `envvars`, that's ok, you can override it by setting `OS_CLOUD_NAME`. Service specific settings, like the nova service type, are set with the @@ -34,7 +34,7 @@ for trove set:: Config Files ------------ -os-client-config will look for a file called clouds.yaml in the following +`os-client-config` will look for a file called `clouds.yaml` in the following locations: * Current Directory @@ -50,6 +50,11 @@ Service specific settings, like the nova service type, are set with the default service type as a prefix. For instance, to set a special service_type for trove (because you're using Rackspace) set: +:: + + database_service_type: 'rax:database' + + Site Specific File Locations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -72,10 +77,6 @@ look in an OS specific config dir * OSX: `/Library/Application Support/openstack` * Windows: `C:\\ProgramData\\OpenStack\\openstack` -:: - - database_service_type: 'rax:database' - An example config file is probably helpful: :: @@ -106,16 +107,16 @@ An example config file is probably helpful: project_id: 610275 region_name: DFW,ORD,IAD -You may note a few things. First, since auth_url settings are silly +You may note a few things. First, since `auth_url` settings are silly and embarrasingly ugly, known cloud vendor profile information is included and -may be referenced by name. One of the benefits of that is that auth_url +may be referenced by name. One of the benefits of that is that `auth_url` isn't the only thing the vendor defaults contain. For instance, since -Rackspace lists `rax:database` as the service type for trove, os-client-config +Rackspace lists `rax:database` as the service type for trove, `os-client-config` knows that so that you don't have to. In case the cloud vendor profile is not -available, you can provide one called clouds-public.yaml, following the same +available, you can provide one called `clouds-public.yaml`, following the same location rules previously mentioned for the config files. -Also, region_name can be a list of regions. When you call get_all_clouds, +Also, `region_name` can be a list of regions. When you call `get_all_clouds`, you'll get a cloud config object for each cloud/region combo. As seen with `dns_service_type`, any setting that makes sense to be per-service, @@ -153,7 +154,7 @@ Cache Settings -------------- Accessing a cloud is often expensive, so it's quite common to want to do some -client-side caching of those operations. To facilitate that, os-client-config +client-side caching of those operations. To facilitate that, `os-client-config` understands passing through cache settings to dogpile.cache, with the following behaviors: From 9f7a50619a170ec9c9d702862ea617cc1b17e765 Mon Sep 17 00:00:00 2001 From: Ghe Rivero Date: Sat, 6 Jun 2015 12:50:42 +0200 Subject: [PATCH 0320/3836] Raise warning when a vendor profile is missing Change-Id: I12f5c9d824abee4af42403a86db8bf4a8bbcac16 --- os_client_config/config.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index c08fbeea9..bc8ab1e33 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -232,7 +232,9 @@ def _get_base_cloud_config(self, name): _auth_update(cloud, vendors.CLOUD_DEFAULTS[profile_name]) except KeyError: # Can't find the requested vendor config, go about business - pass + warnings.warn("Couldn't find the vendor profile '{0}', for" + " the cloud '{1}'".format(profile_name, + name)) if 'auth' not in cloud: cloud['auth'] = dict() From 843402653d8c05698db481c4146a67846219ae60 Mon Sep 17 00:00:00 2001 From: Ghe Rivero Date: Sat, 6 Jun 2015 13:06:39 +0200 Subject: [PATCH 0321/3836] Use one yaml file per vendor With an increasing numbers of vendors, having all profiles in a single file, and with dicts inside dicts inside dicts... is getting more complicated to have a general view of what is available. Change-Id: I6f386829774365125d585a6ff1b6e22f4c98df2a --- os_client_config/vendors.py | 98 ----------------------- os_client_config/vendors/__init__.py | 25 ++++++ os_client_config/vendors/auro.yaml | 10 +++ os_client_config/vendors/dreamhost.yaml | 7 ++ os_client_config/vendors/hp.yaml | 8 ++ os_client_config/vendors/ovh.yaml | 10 +++ os_client_config/vendors/rackspace.yaml | 14 ++++ os_client_config/vendors/runabove.yaml | 7 ++ os_client_config/vendors/unitedstack.yaml | 8 ++ os_client_config/vendors/vexxhost.yaml | 8 ++ 10 files changed, 97 insertions(+), 98 deletions(-) delete mode 100644 os_client_config/vendors.py create mode 100644 os_client_config/vendors/__init__.py create mode 100644 os_client_config/vendors/auro.yaml create mode 100644 os_client_config/vendors/dreamhost.yaml create mode 100644 os_client_config/vendors/hp.yaml create mode 100644 os_client_config/vendors/ovh.yaml create mode 100644 os_client_config/vendors/rackspace.yaml create mode 100644 os_client_config/vendors/runabove.yaml create mode 100644 os_client_config/vendors/unitedstack.yaml create mode 100644 os_client_config/vendors/vexxhost.yaml diff --git a/os_client_config/vendors.py b/os_client_config/vendors.py deleted file mode 100644 index e02b32517..000000000 --- a/os_client_config/vendors.py +++ /dev/null @@ -1,98 +0,0 @@ -# flake8: noqa -# Copyright (c) 2014 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. - -CLOUD_DEFAULTS = dict( - hp=dict( - auth=dict( - auth_url='https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0', - ), - region_name='region-b.geo-1', - dns_service_type='hpext:dns', - image_api_version='1', - image_format='qcow2', - ), - rackspace=dict( - auth=dict( - auth_url='https://identity.api.rackspacecloud.com/v2.0/', - ), - database_service_type='rax:database', - compute_service_name='cloudServersOpenStack', - image_api_version='2', - image_api_use_tasks=True, - image_format='vhd', - floating_ip_source=None, - secgroup_source=None, - disable_vendor_agent=dict( - vm_mode='hvm', - xenapi_use_agent=False, - ) - ), - dreamhost=dict( - auth=dict( - auth_url='https://keystone.dream.io/v2.0', - ), - region_name='RegionOne', - image_api_version='2', - image_format='raw', - ), - vexxhost=dict( - auth=dict( - auth_url='http://auth.api.thenebulacloud.com:5000/v2.0/', - ), - region_name='ca-ymq-1', - image_api_version='2', - image_format='qcow2', - floating_ip_source=None, - ), - runabove=dict( - auth=dict( - auth_url='https://auth.runabove.io/v2.0', - ), - image_api_version='2', - image_format='qcow2', - floating_ip_source=None, - ), - unitedstack=dict( - auth=dict( - auth_url='https://identity.api.ustack.com/v3', - ), - identity_api_version='3', - image_api_version='2', - image_format='raw', - floating_ip_source=None, - ), - auro=dict( - auth=dict( - auth_url='https://api.auro.io:5000/v2.0', - ), - region_name='RegionOne', - identity_api_version='2', - image_api_version='1', - image_format='qcow2', - secgroup_source='nova', - floating_ip_source='nova', - ), - ovh=dict( - auth=dict( - auth_url='https://auth.cloud.ovh.net/v2.0', - ), - region_name='SBG1', - identity_api_version='2', - image_api_version='1', - image_format='raw', - secgroup_source='neutron', - floating_ip_source='neutron', - ), -) diff --git a/os_client_config/vendors/__init__.py b/os_client_config/vendors/__init__.py new file mode 100644 index 000000000..0ccb04c5d --- /dev/null +++ b/os_client_config/vendors/__init__.py @@ -0,0 +1,25 @@ +# Copyright (c) 2014 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. + +import glob +import os + +import yaml + +vendors_path = os.path.dirname(os.path.realpath(__file__)) + +CLOUD_DEFAULTS = {} +for vendor in glob.glob(os.path.join(vendors_path, '*.yaml')): + with open(vendor, 'r') as f: + CLOUD_DEFAULTS.update(yaml.safe_load(f)) diff --git a/os_client_config/vendors/auro.yaml b/os_client_config/vendors/auro.yaml new file mode 100644 index 000000000..da1491dec --- /dev/null +++ b/os_client_config/vendors/auro.yaml @@ -0,0 +1,10 @@ +--- +auro: + auth: + auth_url: https://api.auro.io:5000/v2.0 + region_name: RegionOne + identity_api_version: 2 + image_api_version: 1 + image_format: qcow2 + secgroup_source: nova + floating_ip_source: nova diff --git a/os_client_config/vendors/dreamhost.yaml b/os_client_config/vendors/dreamhost.yaml new file mode 100644 index 000000000..63f15c31d --- /dev/null +++ b/os_client_config/vendors/dreamhost.yaml @@ -0,0 +1,7 @@ +--- +dreamhost: + auth: + auth_url: https://keystone.dream.io/v2.0 + region_name: RegionOne + image_api_version: 2 + image_format: raw diff --git a/os_client_config/vendors/hp.yaml b/os_client_config/vendors/hp.yaml new file mode 100644 index 000000000..2ac551781 --- /dev/null +++ b/os_client_config/vendors/hp.yaml @@ -0,0 +1,8 @@ +--- +hp: + auth: + auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0 + region_name: region-b.geo-1 + dns_service_type: hpext:dns + image_api_version: 1 + image_format: qcow2 diff --git a/os_client_config/vendors/ovh.yaml b/os_client_config/vendors/ovh.yaml new file mode 100644 index 000000000..5b4426f18 --- /dev/null +++ b/os_client_config/vendors/ovh.yaml @@ -0,0 +1,10 @@ +--- +ovh: + auth: + auth_url: https://auth.cloud.ovh.net/v2.0 + region_name: SBG1 + identity_api_version: 2 + image_api_version: 1 + image_format: raw + secgroup_source: neutron + floating_ip_source: neutron diff --git a/os_client_config/vendors/rackspace.yaml b/os_client_config/vendors/rackspace.yaml new file mode 100644 index 000000000..7af3cab96 --- /dev/null +++ b/os_client_config/vendors/rackspace.yaml @@ -0,0 +1,14 @@ +--- +rackspace: + auth: + auth_url: https://identity.api.rackspacecloud.com/v2.0 + database_service_type: 'rax:database' + compute_service_name: cloudServersOpenStack + image_api_version: 2 + image_api_use_tasks: True + image_format: vhd + floating_ip_source: None + secgroup_source: None + disable_vendor_agent: + vm_mode: hvm + xenapi_use_agent: False diff --git a/os_client_config/vendors/runabove.yaml b/os_client_config/vendors/runabove.yaml new file mode 100644 index 000000000..c9641dd41 --- /dev/null +++ b/os_client_config/vendors/runabove.yaml @@ -0,0 +1,7 @@ +--- +runabove: + auth: + auth_url: https://auth.runabove.io/v2.0 + image_api_version: 2 + image_format: qcow2 + floating_ip_sourc: None diff --git a/os_client_config/vendors/unitedstack.yaml b/os_client_config/vendors/unitedstack.yaml new file mode 100644 index 000000000..43c600a6f --- /dev/null +++ b/os_client_config/vendors/unitedstack.yaml @@ -0,0 +1,8 @@ +--- +unitedstack: + auth: + auth_url: https://identity.api.ustack.com/v3 + identity_api_version: 3 + image_api_version: 2 + image_format: raw + floating_ip_source: None diff --git a/os_client_config/vendors/vexxhost.yaml b/os_client_config/vendors/vexxhost.yaml new file mode 100644 index 000000000..5247ef436 --- /dev/null +++ b/os_client_config/vendors/vexxhost.yaml @@ -0,0 +1,8 @@ +--- +vexxhost: + auth: + auth_url: http://auth.api.thenebulacloud.com:5000/v2.0/ + region_name: ca-ymq-1 + image_api_version: 2 + image_format: qcow2 + floating_ip_source: None From 3a81f596b2c8d08a77f1fdcb84c79e598d673c2e Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 8 Jun 2015 15:36:28 -0400 Subject: [PATCH 0322/3836] Return new secgroup object The create method for a security group had a bug in that it was not returning the newly created security group. This also moves the logic for normalizing the nova data into the _utils module since it is reused. Change-Id: I19a3063769f1fc8c7bfd1d57aae0e5b839b8189c --- shade/__init__.py | 31 +++++++++--------------- shade/_utils.py | 27 +++++++++++++++++++++ shade/tests/unit/test_security_groups.py | 12 +++++++-- 3 files changed, 48 insertions(+), 22 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 431f97da9..64a3a7e32 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -908,22 +908,7 @@ def list_security_groups(self): raise OpenStackCloudException( "Error fetching security group list" ) - # Make Nova data look like Neutron data. This doesn't make them - # look exactly the same, but pretty close. - return [{'id': g['id'], - 'name': g['name'], - 'description': g['description'], - 'security_group_rules': [{ - 'id': r['id'], - 'direction': 'ingress', - 'ethertype': 'IPv4', - 'port_range_min': r['from_port'], - 'port_range_max': r['to_port'], - 'protocol': r['ip_protocol'], - 'remote_ip_prefix': r['ip_range'].get('cidr', None), - 'security_group_id': r['parent_group_id'], - } for r in g['rules']] - } for g in groups] + return _utils.normalize_nova_secgroups(groups) # Security groups not supported else: @@ -2363,13 +2348,15 @@ def create_security_group(self, name, description): :param string name: A name for the security group. :param string description: Describes the security group. + :returns: A dict representing the new security group. + :raises: OpenStackCloudException on operation error. :raises: OpenStackCloudUnavailableFeature if security groups are not supported on this cloud. """ if self.secgroup_source == 'neutron': try: - self.manager.submitTask( + group = self.manager.submitTask( _tasks.NeutronSecurityGroupCreate( body=dict(security_group=dict(name=name, description=description)) @@ -2382,12 +2369,15 @@ def create_security_group(self, name, description): raise OpenStackCloudException( "failed to create security group '{name}': {msg}".format( name=name, msg=str(e))) + return group['security_group'] elif self.secgroup_source == 'nova': try: - self.manager.submitTask( - _tasks.NovaSecurityGroupCreate( - name=name, description=description + group = meta.obj_to_dict( + self.manager.submitTask( + _tasks.NovaSecurityGroupCreate( + name=name, description=description + ) ) ) except Exception as e: @@ -2397,6 +2387,7 @@ def create_security_group(self, name, description): raise OpenStackCloudException( "failed to create security group '{name}': {msg}".format( name=name, msg=str(e))) + return _utils.normalize_nova_secgroups([group])[0] # Security groups not supported else: diff --git a/shade/_utils.py b/shade/_utils.py index 10a6647ff..7bb859d5e 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -112,3 +112,30 @@ def _get_entity(func, name_or_id, filters): raise exc.OpenStackCloudException( "Multiple matches found for %s" % name_or_id) return entities[0] + + +def normalize_nova_secgroups(groups): + """Normalize the structure of nova security groups + + This makes security group dicts, as returned from nova, look like the + security group dicts as returned from neutron. This does not make them + look exactly the same, but it's pretty close. + + :param list groups: A list of security group dicts. + + :returns: A list of normalized dicts. + """ + return [{'id': g['id'], + 'name': g['name'], + 'description': g['description'], + 'security_group_rules': [{ + 'id': r['id'], + 'direction': 'ingress', + 'ethertype': 'IPv4', + 'port_range_min': r['from_port'], + 'port_range_max': r['to_port'], + 'protocol': r['ip_protocol'], + 'remote_ip_prefix': r['ip_range'].get('cidr', None), + 'security_group_id': r['parent_group_id'], + } for r in g['rules']] + } for g in groups] diff --git a/shade/tests/unit/test_security_groups.py b/shade/tests/unit/test_security_groups.py index 88f8c8bb5..a7bc25345 100644 --- a/shade/tests/unit/test_security_groups.py +++ b/shade/tests/unit/test_security_groups.py @@ -139,13 +139,21 @@ def test_create_security_group_neutron(self, mock_neutron): @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_create_security_group_nova(self, mock_nova): - self.cloud.secgroup_source = 'nova' group_name = self.getUniqueString() group_desc = 'security group from test_create_security_group_neutron' - self.cloud.create_security_group(group_name, group_desc) + new_group = fakes.FakeSecgroup(id='2', + name=group_name, + description=group_desc, + rules=[]) + + mock_nova.security_groups.create.return_value = new_group + self.cloud.secgroup_source = 'nova' + r = self.cloud.create_security_group(group_name, group_desc) mock_nova.security_groups.create.assert_called_once_with( name=group_name, description=group_desc ) + self.assertEqual(group_name, r['name']) + self.assertEqual(group_desc, r['description']) @mock.patch.object(shade.OpenStackCloud, 'neutron_client') @mock.patch.object(shade.OpenStackCloud, 'nova_client') From fd16aa7d830cfe0c7f467205919e4a9d3a2fed9c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 5 Jun 2015 13:29:55 -0400 Subject: [PATCH 0323/3836] Don't emit volume tracebacks in inventory debug It's ok if a cloud doesn't have volumes, and it's a question we can ask of the cloud. Now, keystone tells us about it by way of throwing an EndpointNotFound exception. But it's a normal logic exception, not a "this is unexpected" exception. So we can handle it, log a sane error, and move on, rather than logging the full traceback. Change-Id: If6fc1ab0bb8247e0fdbd1920aa0b076ac1707cba --- shade/__init__.py | 7 ++++++- shade/meta.py | 15 ++++++++------- shade/tests/unit/test_meta.py | 14 +++++++------- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index fca07db84..57b2a399e 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -27,8 +27,9 @@ from ironicclient import exceptions as ironic_exceptions import jsonpatch from keystoneclient import auth as ksc_auth -from keystoneclient import session as ksc_session from keystoneclient import client as keystone_client +from keystoneclient import exceptions as keystone_exceptions +from keystoneclient import session as ksc_session from novaclient import client as nova_client from novaclient import exceptions as nova_exceptions from neutronclient.v2_0 import client as neutron_client @@ -721,6 +722,10 @@ def get_session_endpoint(self, service_key): service_name=self.get_service_name(service_key), interface=self.endpoint_type, region_name=self.region_name) + except keystone_exceptions.EndpointNotFound as e: + self.log.debug( + "Endpoint not found in %s cloud: %s", self.name, str(e)) + endpoint = None except Exception as e: self.log.debug("keystone cannot get endpoint", exc_info=True) raise OpenStackCloudException( diff --git a/shade/meta.py b/shade/meta.py index c2539fec9..2f19dc651 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -130,13 +130,14 @@ def get_hostvars_from_server(cloud, server, mounts=None): server_vars['image'].pop('links', None) volumes = [] - try: - for volume in cloud.get_volumes(server): - # Make things easier to consume elsewhere - volume['device'] = volume['attachments'][0]['device'] - volumes.append(volume) - except exc.OpenStackCloudException: - pass + if cloud.has_service('volumes'): + try: + for volume in cloud.get_volumes(server): + # Make things easier to consume elsewhere + volume['device'] = volume['attachments'][0]['device'] + volumes.append(volume) + except exc.OpenStackCloudException: + pass server_vars['volumes'] = volumes if mounts: for mount in mounts: diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index da3076892..f6dd09008 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -16,7 +16,6 @@ import testtools import warlock -from shade import exc from shade import meta PRIVATE_V4 = '198.51.100.3' @@ -27,6 +26,7 @@ class FakeCloud(object): region_name = 'test-region' name = 'test-name' private = False + service_val = True _unused = "useless" def get_flavor_name(self, id): @@ -38,6 +38,9 @@ def get_image_name(self, id): def get_volumes(self, server): return [] + def has_service(self, service_name): + return self.service_val + class FakeServer(object): id = 'test-id-0' @@ -160,13 +163,10 @@ def test_has_volume(self): self.assertEquals('/dev/sda0', hostvars['volumes'][0]['device']) def test_has_no_volume_service(self): - mock_cloud = mock.MagicMock() - - def side_effect(*args): - raise exc.OpenStackCloudException("No Volumes") - mock_cloud.get_volumes.side_effect = side_effect + fake_cloud = FakeCloud() + fake_cloud.service_val = False hostvars = meta.get_hostvars_from_server( - mock_cloud, meta.obj_to_dict(FakeServer())) + fake_cloud, meta.obj_to_dict(FakeServer())) self.assertEquals([], hostvars['volumes']) def test_unknown_volume_exception(self): From de84b798f9eaac831621a63198ce2366effaeabd Mon Sep 17 00:00:00 2001 From: Ghe Rivero Date: Tue, 9 Jun 2015 13:00:16 +0200 Subject: [PATCH 0324/3836] Add test to check cert and key as a tuple When the cert file doesn't include a key within it, a tuple with the cert and key needs to be passed to the requests library. Change-Id: I17534b8e7d07b3ad102cc6a6a839541c83281b8e --- os_client_config/tests/test_cloud_config.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index 729bc9974..5f964dbeb 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -67,12 +67,12 @@ def test_verify(self): config_dict['verify'] = False cc = cloud_config.CloudConfig("test1", "region-xx", config_dict) - (verify, cacert) = cc.get_requests_verify_args() + (verify, cert) = cc.get_requests_verify_args() self.assertFalse(verify) config_dict['verify'] = True cc = cloud_config.CloudConfig("test1", "region-xx", config_dict) - (verify, cacert) = cc.get_requests_verify_args() + (verify, cert) = cc.get_requests_verify_args() self.assertTrue(verify) def test_verify_cacert(self): @@ -81,10 +81,22 @@ def test_verify_cacert(self): config_dict['verify'] = False cc = cloud_config.CloudConfig("test1", "region-xx", config_dict) - (verify, cacert) = cc.get_requests_verify_args() + (verify, cert) = cc.get_requests_verify_args() self.assertFalse(verify) config_dict['verify'] = True cc = cloud_config.CloudConfig("test1", "region-xx", config_dict) - (verify, cacert) = cc.get_requests_verify_args() + (verify, cert) = cc.get_requests_verify_args() self.assertEqual("certfile", verify) + + def test_cert_with_key(self): + config_dict = copy.deepcopy(fake_config_dict) + config_dict['cacert'] = None + config_dict['verify'] = False + + config_dict['cert'] = 'cert' + config_dict['key'] = 'key' + + cc = cloud_config.CloudConfig("test1", "region-xx", config_dict) + (verify, cert) = cc.get_requests_verify_args() + self.assertEqual(("cert", "key"), cert) From dfcccbf30b1f64b345e610e450e75e39de97dbbc Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 3 Jun 2015 18:47:10 -0400 Subject: [PATCH 0325/3836] Add very initial support for passing in occ object We've got a split personality in terms of whether we should use or not use os-client-config... except we have a hard dependency on it. Maybe we should just use it. Change-Id: I398cfb474791fd0ee5e15de4e0ae222c1b25b9c0 --- shade/__init__.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 05d0fa51e..216dba64e 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -110,6 +110,7 @@ def openstack_clouds(config=None, debug=False): cache_interval=config.get_cache_max_age(), cache_class=config.get_cache_class(), cache_arguments=config.get_cache_arguments(), + cloud_config=f, **f.config) for f in config.get_all_clouds() ] @@ -125,6 +126,7 @@ def openstack_cloud(debug=False, **kwargs): cache_interval=config.get_cache_max_age(), cache_class=config.get_cache_class(), cache_arguments=config.get_cache_arguments(), + cloud_config=cloud_config, debug=debug, **cloud_config.config) @@ -136,6 +138,7 @@ def operator_cloud(debug=False, **kwargs): cache_interval=config.get_cache_max_age(), cache_class=config.get_cache_class(), cache_arguments=config.get_cache_arguments(), + cloud_config=cloud_config, **cloud_config.config) @@ -243,6 +246,10 @@ class OpenStackCloud(object): use the glance task-create interface for image upload activities instead of direct calls. (optional, defaults to False) + :param CloudConfig cloud_config: Cloud config object from os-client-config + In the future, this will be the only way + to pass in cloud configuration, but is + being phased in currently. """ def __init__(self, cloud, auth, @@ -257,10 +264,18 @@ def __init__(self, cloud, auth, cache_arguments=None, manager=None, image_api_use_tasks=False, + cloud_config=None, **kwargs): self.log = logging.getLogger('shade') + if cloud_config is None: + config = os_client_config.OpenStackConfig() + if cloud in config.get_cloud_names(): + cloud_config = config.get_one_cloud(cloud) + else: + cloud_config = config.get_one_cloud() + self.name = cloud self.auth = auth self.region_name = region_name From eded4b31970d0762119548ef76e4f60dec2376d1 Mon Sep 17 00:00:00 2001 From: Ghe Rivero Date: Tue, 9 Jun 2015 17:49:30 +0200 Subject: [PATCH 0326/3836] Add missing tests - Test get_all_clouds returns a full list of available clouds - Test env. variables are stripped of initial 'os_' and '-' replaced with '_' Change-Id: If277aade17776d57236cc0e48a46fbb04158e7ed --- os_client_config/tests/test_config.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 562e40815..a6ba2ee83 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -28,6 +28,14 @@ class TestConfig(base.TestCase): + def test_get_all_clouds(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + clouds = c.get_all_clouds() + user_clouds = [cloud for cloud in base.USER_CONF['clouds'].keys()] + configured_clouds = [cloud.name for cloud in clouds] + self.assertItemsEqual(user_clouds, configured_clouds) + def test_get_one_cloud(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) @@ -189,6 +197,15 @@ def test_get_one_cloud_no_argparse(self): self.assertEqual(cc.region_name, 'test-region') self.assertIsNone(cc.snack_type) + def test_fix_env_args(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + + env_args = {'os-compute-api-version': 1} + fixed_args = c._fix_args(env_args) + + self.assertDictEqual({'compute_api_version': 1}, fixed_args) + class TestConfigDefault(base.TestCase): From 78a3b204a7307cab82d8032d13bf915b7370c64f Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 8 Jun 2015 18:06:42 -0400 Subject: [PATCH 0327/3836] Add secgroup update API Add the update API method for security groups and tests. Change-Id: I7ac7b13e1beabec86fd11c62c0e905fd6f4589b5 --- shade/__init__.py | 57 ++++++++++++++++++++++++ shade/_tasks.py | 10 +++++ shade/tests/unit/test_security_groups.py | 36 +++++++++++++++ 3 files changed, 103 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 05d0fa51e..b2eb0d6a9 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2449,6 +2449,63 @@ def delete_security_group(self, name_or_id): "Unavailable feature: security groups" ) + @valid_kwargs('name', 'description') + def update_security_group(self, name_or_id, **kwargs): + """Update a security group + + :param string name_or_id: Name or ID of the security group to update. + :param string name: New name for the security group. + :param string description: New description for the security group. + + :returns: A dictionary describing the updated security group. + + :raises: OpenStackCloudException on operation error. + """ + secgroup = self.get_security_group(name_or_id) + + if secgroup is None: + raise OpenStackCloudException( + "Security group %s not found." % name_or_id) + + if self.secgroup_source == 'neutron': + try: + group = self.manager.submitTask( + _tasks.NeutronSecurityGroupUpdate( + security_group=secgroup['id'], + body={'security_group': kwargs}) + ) + except Exception as e: + self.log.debug( + "neutron failed to update security group '{group}'".format( + group=name_or_id), exc_info=True) + raise OpenStackCloudException( + "failed to update security group '{group}': {msg}".format( + group=name_or_id, msg=str(e))) + return group['security_group'] + + elif self.secgroup_source == 'nova': + try: + group = meta.obj_to_dict( + self.manager.submitTask( + _tasks.NovaSecurityGroupUpdate( + group=secgroup['id'], **kwargs) + ) + ) + except Exception as e: + self.log.debug( + "nova failed to update security group '{group}'".format( + group=name_or_id), exc_info=True) + raise OpenStackCloudException( + "failed to update security group '{group}': {msg}".format( + group=name_or_id, msg=str(e))) + return _utils.normalize_nova_secgroups([group])[0] + + # Security groups not supported + else: + raise OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + class OperatorCloud(OpenStackCloud): """Represent a privileged/operator connection to an OpenStack Cloud. diff --git a/shade/_tasks.py b/shade/_tasks.py index c0ccfb753..130a2b1ef 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -207,6 +207,11 @@ def main(self, client): return client.neutron_client.delete_security_group(**self.args) +class NeutronSecurityGroupUpdate(task_manager.Task): + def main(self, client): + return client.neutron_client.update_security_group(**self.args) + + class NovaSecurityGroupList(task_manager.Task): def main(self, client): return client.nova_client.security_groups.list() @@ -222,6 +227,11 @@ def main(self, client): return client.nova_client.security_groups.delete(**self.args) +class NovaSecurityGroupUpdate(task_manager.Task): + def main(self, client): + return client.nova_client.security_groups.update(**self.args) + + # TODO: Do this with neutron instead of nova if possible class FloatingIPList(task_manager.Task): def main(self, client): diff --git a/shade/tests/unit/test_security_groups.py b/shade/tests/unit/test_security_groups.py index a7bc25345..83c21a021 100644 --- a/shade/tests/unit/test_security_groups.py +++ b/shade/tests/unit/test_security_groups.py @@ -13,6 +13,7 @@ # under the License. +import copy import mock import shade @@ -164,3 +165,38 @@ def test_create_security_group_none(self, mock_nova, mock_neutron): '', '') self.assertFalse(mock_neutron.create_security_group.called) self.assertFalse(mock_nova.security_groups.create.called) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_update_security_group_neutron(self, mock_neutron): + self.cloud.secgroup_source = 'neutron' + neutron_return = dict(security_groups=[neutron_grp_dict]) + mock_neutron.list_security_groups.return_value = neutron_return + self.cloud.update_security_group(neutron_grp_obj.id, name='new_name') + mock_neutron.update_security_group.assert_called_once_with( + security_group=neutron_grp_dict['id'], + body={'security_group': {'name': 'new_name'}} + ) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_update_security_group_nova(self, mock_nova): + new_name = self.getUniqueString() + self.cloud.secgroup_source = 'nova' + nova_return = [nova_grp_obj] + update_return = copy.deepcopy(nova_grp_obj) + update_return.name = new_name + mock_nova.security_groups.list.return_value = nova_return + mock_nova.security_groups.update.return_value = update_return + r = self.cloud.update_security_group(nova_grp_obj.id, name=new_name) + mock_nova.security_groups.update.assert_called_once_with( + group=nova_grp_obj.id, name=new_name + ) + self.assertEqual(r['name'], new_name) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_update_security_group_bad_kwarg(self, mock_nova, mock_neutron): + self.assertRaises(TypeError, + self.cloud.update_security_group, + 'doesNotExist', bad_arg='') + self.assertFalse(mock_neutron.create_security_group.called) + self.assertFalse(mock_nova.security_groups.create.called) From 120d68828965d8f2992fbb4c0a474a85d94b7246 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 11 Jun 2015 09:56:07 -0400 Subject: [PATCH 0328/3836] Move _utils unit testing to separate file As more functions get added to _utils, it will be easier to have their unit tests in their own file. Change-Id: I59ec1b044de7b3777ede5ad62de5bad041d1a6d3 --- shade/tests/unit/test__utils.py | 61 +++++++++++++++++++++++++++++++++ shade/tests/unit/test_shade.py | 45 +----------------------- 2 files changed, 62 insertions(+), 44 deletions(-) create mode 100644 shade/tests/unit/test__utils.py diff --git a/shade/tests/unit/test__utils.py b/shade/tests/unit/test__utils.py new file mode 100644 index 000000000..f3c175835 --- /dev/null +++ b/shade/tests/unit/test__utils.py @@ -0,0 +1,61 @@ +# -*- 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. + + +from shade import _utils +from shade.tests.unit import base + + +class TestUtils(base.TestCase): + + def test__filter_list_name_or_id(self): + el1 = dict(id=100, name='donald') + el2 = dict(id=200, name='pluto') + data = [el1, el2] + ret = _utils._filter_list(data, 'donald', None) + self.assertEquals([el1], ret) + + def test__filter_list_filter(self): + el1 = dict(id=100, name='donald', other='duck') + el2 = dict(id=200, name='donald', other='trump') + data = [el1, el2] + ret = _utils._filter_list(data, 'donald', {'other': 'duck'}) + self.assertEquals([el1], ret) + + def test__filter_list_dict1(self): + el1 = dict(id=100, name='donald', last='duck', + other=dict(category='duck')) + el2 = dict(id=200, name='donald', last='trump', + other=dict(category='human')) + el3 = dict(id=300, name='donald', last='ronald mac', + other=dict(category='clown')) + data = [el1, el2, el3] + ret = _utils._filter_list( + data, 'donald', {'other': {'category': 'clown'}}) + self.assertEquals([el3], ret) + + def test__filter_list_dict2(self): + el1 = dict(id=100, name='donald', last='duck', + other=dict(category='duck', financial=dict(status='poor'))) + el2 = dict(id=200, name='donald', last='trump', + other=dict(category='human', financial=dict(status='rich'))) + el3 = dict(id=300, name='donald', last='ronald mac', + other=dict(category='clown', financial=dict(status='rich'))) + data = [el1, el2, el3] + ret = _utils._filter_list( + data, 'donald', + {'other': { + 'financial': {'status': 'rich'} + }}) + self.assertEquals([el2, el3], ret) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index a46ec1a3d..5f2c00f1c 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -20,10 +20,8 @@ import shade from shade import exc from shade import meta -from shade import _utils - -from shade.tests.unit import base from shade.tests import fakes +from shade.tests.unit import base class TestShade(base.TestCase): @@ -35,47 +33,6 @@ def setUp(self): def test_openstack_cloud(self): self.assertIsInstance(self.cloud, shade.OpenStackCloud) - def test__filter_list_name_or_id(self): - el1 = dict(id=100, name='donald') - el2 = dict(id=200, name='pluto') - data = [el1, el2] - ret = _utils._filter_list(data, 'donald', None) - self.assertEquals([el1], ret) - - def test__filter_list_filter(self): - el1 = dict(id=100, name='donald', other='duck') - el2 = dict(id=200, name='donald', other='trump') - data = [el1, el2] - ret = _utils._filter_list(data, 'donald', {'other': 'duck'}) - self.assertEquals([el1], ret) - - def test__filter_list_dict1(self): - el1 = dict(id=100, name='donald', last='duck', - other=dict(category='duck')) - el2 = dict(id=200, name='donald', last='trump', - other=dict(category='human')) - el3 = dict(id=300, name='donald', last='ronald mac', - other=dict(category='clown')) - data = [el1, el2, el3] - ret = _utils._filter_list( - data, 'donald', {'other': {'category': 'clown'}}) - self.assertEquals([el3], ret) - - def test__filter_list_dict2(self): - el1 = dict(id=100, name='donald', last='duck', - other=dict(category='duck', financial=dict(status='poor'))) - el2 = dict(id=200, name='donald', last='trump', - other=dict(category='human', financial=dict(status='rich'))) - el3 = dict(id=300, name='donald', last='ronald mac', - other=dict(category='clown', financial=dict(status='rich'))) - data = [el1, el2, el3] - ret = _utils._filter_list( - data, 'donald', - {'other': { - 'financial': {'status': 'rich'} - }}) - self.assertEquals([el2, el3], ret) - @mock.patch.object(shade.OpenStackCloud, 'search_images') def test_get_images(self, mock_search): image1 = dict(id='123', name='mickey') From 2f499ab98b03201374b1a18cb0e1669530c7e328 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 11 Jun 2015 10:37:37 -0400 Subject: [PATCH 0329/3836] Coalesce port values in secgroup rules Nova uses -1 for non-specific port values in security group rules. Neutron rules represent these as None. We need to be consistent in the values we return from shade, so we'll pick the Neutron way since we make Nova rules look like Neutron rules anyway. Change-Id: I6b58c0f9d2f9f11860eade0b6ee2d164e67914b8 --- shade/_utils.py | 9 ++++++-- shade/tests/unit/test__utils.py | 40 +++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/shade/_utils.py b/shade/_utils.py index 7bb859d5e..8619f3571 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -121,6 +121,9 @@ def normalize_nova_secgroups(groups): security group dicts as returned from neutron. This does not make them look exactly the same, but it's pretty close. + Note that nova uses -1 for non-specific port values, but neutron + represents these with None. + :param list groups: A list of security group dicts. :returns: A list of normalized dicts. @@ -132,8 +135,10 @@ def normalize_nova_secgroups(groups): 'id': r['id'], 'direction': 'ingress', 'ethertype': 'IPv4', - 'port_range_min': r['from_port'], - 'port_range_max': r['to_port'], + 'port_range_min': + None if r['from_port'] == -1 else r['from_port'], + 'port_range_max': + None if r['to_port'] == -1 else r['to_port'], 'protocol': r['ip_protocol'], 'remote_ip_prefix': r['ip_range'].get('cidr', None), 'security_group_id': r['parent_group_id'], diff --git a/shade/tests/unit/test__utils.py b/shade/tests/unit/test__utils.py index f3c175835..f64b1eb08 100644 --- a/shade/tests/unit/test__utils.py +++ b/shade/tests/unit/test__utils.py @@ -59,3 +59,43 @@ def test__filter_list_dict2(self): 'financial': {'status': 'rich'} }}) self.assertEquals([el2, el3], ret) + + def test_normalize_nova_secgroups(self): + nova_secgroup = dict( + id='abc123', + name='nova_secgroup', + description='A Nova security group', + rules=[ + dict(id='123', from_port=80, to_port=81, ip_protocol='tcp', + ip_range={'cidr': '0.0.0.0/0'}, parent_group_id='xyz123') + ] + ) + + expected = dict( + id='abc123', + name='nova_secgroup', + description='A Nova security group', + security_group_rules=[ + dict(id='123', direction='ingress', ethertype='IPv4', + port_range_min=80, port_range_max=81, protocol='tcp', + remote_ip_prefix='0.0.0.0/0', security_group_id='xyz123') + ] + ) + + retval = _utils.normalize_nova_secgroups([nova_secgroup])[0] + self.assertEqual(expected, retval) + + def test_normalize_nova_secgroups_negone_port(self): + nova_secgroup = dict( + id='abc123', + name='nova_secgroup', + description='A Nova security group with -1 ports', + rules=[ + dict(id='123', from_port=-1, to_port=-1, ip_protocol='icmp', + ip_range={'cidr': '0.0.0.0/0'}, parent_group_id='xyz123') + ] + ) + + retval = _utils.normalize_nova_secgroups([nova_secgroup])[0] + self.assertIsNone(retval['security_group_rules'][0]['port_range_min']) + self.assertIsNone(retval['security_group_rules'][0]['port_range_max']) From 965998c764557f3eff59cf040aa164f8402e9e47 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 11 Jun 2015 15:08:42 -0400 Subject: [PATCH 0330/3836] Add create method for secgroup rule Adds the creation method for security group rules. Rules, as returned from Nova, are normalized to look more like Neutron rule definitions. Change-Id: I4b1fa8b3067997a3a87de1ca0e8c924ae0b69f2a --- shade/__init__.py | 120 +++++++++++++++++++++++ shade/_tasks.py | 10 ++ shade/_utils.py | 39 +++++--- shade/tests/fakes.py | 12 +++ shade/tests/unit/test__utils.py | 13 +++ shade/tests/unit/test_security_groups.py | 55 +++++++++++ 6 files changed, 234 insertions(+), 15 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index f7ee3c138..25bab6814 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2521,6 +2521,126 @@ def update_security_group(self, name_or_id, **kwargs): "Unavailable feature: security groups" ) + def create_security_group_rule(self, + secgroup_name_or_id, + port_range_min=None, + port_range_max=None, + protocol=None, + remote_ip_prefix=None, + remote_group_id=None, + direction='ingress', + ethertype='IPv4'): + """Create a new security group rule + + :param string secgroup_name_or_id: + The security group name or ID to associate with this security + group rule. If a non-unique group name is given, an exception + is raised. + :param int port_range_min: + The minimum port number in the range that is matched by the + security group rule. If the protocol is TCP or UDP, this value + must be less than or equal to the port_range_max attribute value. + If nova is used by the cloud provider for security groups, then + a value of None will be transformed to -1. + :param int port_range_max: + The maximum port number in the range that is matched by the + security group rule. The port_range_min attribute constrains the + port_range_max attribute. If nova is used by the cloud provider + for security groups, then a value of None will be transformed + to -1. + :param string protocol: + The protocol that is matched by the security group rule. Valid + values are None, tcp, udp, and icmp. + :param string remote_ip_prefix: + The remote IP prefix to be associated with this security group + rule. This attribute matches the specified IP prefix as the + source IP address of the IP packet. + :param string remote_group_id: + The remote group ID to be associated with this security group + rule. + :param string direction: + Ingress or egress: The direction in which the security group + rule is applied. For a compute instance, an ingress security + group rule is applied to incoming (ingress) traffic for that + instance. An egress rule is applied to traffic leaving the + instance. + :param string ethertype: + Must be IPv4 or IPv6, and addresses represented in CIDR must + match the ingress or egress rules. + + :returns: A dict representing the new security group rule. + + :raises: OpenStackCloudException on operation error. + """ + + secgroup = self.get_security_group(secgroup_name_or_id) + if not secgroup: + raise OpenStackCloudException( + "Security group %s not found." % secgroup_name_or_id) + + if self.secgroup_source == 'neutron': + # NOTE: Nova accepts -1 port numbers, but Neutron accepts None + # as the equivalent value. + rule_def = { + 'security_group_id': secgroup['id'], + 'port_range_min': + None if port_range_min == -1 else port_range_min, + 'port_range_max': + None if port_range_max == -1 else port_range_max, + 'protocol': protocol, + 'remote_ip_prefix': remote_ip_prefix, + 'remote_group_id': remote_group_id, + 'direction': direction, + 'ethertype': ethertype + } + + try: + rule = self.manager.submitTask( + _tasks.NeutronSecurityGroupRuleCreate( + body={'security_group_rule': rule_def}) + ) + except Exception as e: + self.log.debug("neutron failed to create security group rule", + exc_info=True) + raise OpenStackCloudException( + "failed to create security group rule: {msg}".format( + msg=str(e))) + return rule['security_group_rule'] + + elif self.secgroup_source == 'nova': + # NOTE: Neutron accepts None for ports, but Nova accepts -1 + # as the equivalent value. + if port_range_min is None: + port_range_min = -1 + if port_range_max is None: + port_range_max = -1 + try: + rule = meta.obj_to_dict( + self.manager.submitTask( + _tasks.NovaSecurityGroupRuleCreate( + parent_group_id=secgroup['id'], + ip_protocol=protocol, + from_port=port_range_min, + to_port=port_range_max, + cidr=remote_ip_prefix, + group_id=remote_group_id + ) + ) + ) + except Exception as e: + self.log.debug("nova failed to create security group rule", + exc_info=True) + raise OpenStackCloudException( + "failed to create security group rule: {msg}".format( + msg=str(e))) + return _utils.normalize_nova_secgroup_rules([rule])[0] + + # Security groups not supported + else: + raise OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + class OperatorCloud(OpenStackCloud): """Represent a privileged/operator connection to an OpenStack Cloud. diff --git a/shade/_tasks.py b/shade/_tasks.py index 130a2b1ef..8be7280b6 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -212,6 +212,11 @@ def main(self, client): return client.neutron_client.update_security_group(**self.args) +class NeutronSecurityGroupRuleCreate(task_manager.Task): + def main(self, client): + return client.neutron_client.create_security_group_rule(**self.args) + + class NovaSecurityGroupList(task_manager.Task): def main(self, client): return client.nova_client.security_groups.list() @@ -232,6 +237,11 @@ def main(self, client): return client.nova_client.security_groups.update(**self.args) +class NovaSecurityGroupRuleCreate(task_manager.Task): + def main(self, client): + return client.nova_client.security_group_rules.create(**self.args) + + # TODO: Do this with neutron instead of nova if possible class FloatingIPList(task_manager.Task): def main(self, client): diff --git a/shade/_utils.py b/shade/_utils.py index 8619f3571..6b5ece632 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -121,9 +121,6 @@ def normalize_nova_secgroups(groups): security group dicts as returned from neutron. This does not make them look exactly the same, but it's pretty close. - Note that nova uses -1 for non-specific port values, but neutron - represents these with None. - :param list groups: A list of security group dicts. :returns: A list of normalized dicts. @@ -131,16 +128,28 @@ def normalize_nova_secgroups(groups): return [{'id': g['id'], 'name': g['name'], 'description': g['description'], - 'security_group_rules': [{ - 'id': r['id'], - 'direction': 'ingress', - 'ethertype': 'IPv4', - 'port_range_min': - None if r['from_port'] == -1 else r['from_port'], - 'port_range_max': - None if r['to_port'] == -1 else r['to_port'], - 'protocol': r['ip_protocol'], - 'remote_ip_prefix': r['ip_range'].get('cidr', None), - 'security_group_id': r['parent_group_id'], - } for r in g['rules']] + 'security_group_rules': normalize_nova_secgroup_rules(g['rules']) } for g in groups] + + +def normalize_nova_secgroup_rules(rules): + """Normalize the structure of nova security group rules + + Note that nova uses -1 for non-specific port values, but neutron + represents these with None. + + :param list rules: A list of security group rule dicts. + + :returns: A list of normalized dicts. + """ + return [{'id': r['id'], + 'direction': 'ingress', + 'ethertype': 'IPv4', + 'port_range_min': + None if r['from_port'] == -1 else r['from_port'], + 'port_range_max': + None if r['to_port'] == -1 else r['to_port'], + 'protocol': r['ip_protocol'], + 'remote_ip_prefix': r['ip_range'].get('cidr', None), + 'security_group_id': r['parent_group_id'] + } for r in rules] diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index b62865f19..a6446894b 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -97,3 +97,15 @@ def __init__(self, id, name, description='', rules=None): self.name = name self.description = description self.rules = rules + + +class FakeNovaSecgroupRule(object): + def __init__(self, id, from_port=None, to_port=None, ip_protocol=None, + cidr=None, parent_group_id=None): + self.id = id + self.from_port = from_port + self.to_port = to_port + self.ip_protocol = ip_protocol + if cidr: + self.ip_range = {'cidr': cidr} + self.parent_group_id = parent_group_id diff --git a/shade/tests/unit/test__utils.py b/shade/tests/unit/test__utils.py index f64b1eb08..869fbb3e0 100644 --- a/shade/tests/unit/test__utils.py +++ b/shade/tests/unit/test__utils.py @@ -99,3 +99,16 @@ def test_normalize_nova_secgroups_negone_port(self): retval = _utils.normalize_nova_secgroups([nova_secgroup])[0] self.assertIsNone(retval['security_group_rules'][0]['port_range_min']) self.assertIsNone(retval['security_group_rules'][0]['port_range_max']) + + def test_normalize_nova_secgroup_rules(self): + nova_rules = [ + dict(id='123', from_port=80, to_port=81, ip_protocol='tcp', + ip_range={'cidr': '0.0.0.0/0'}, parent_group_id='xyz123') + ] + expected = [ + dict(id='123', direction='ingress', ethertype='IPv4', + port_range_min=80, port_range_max=81, protocol='tcp', + remote_ip_prefix='0.0.0.0/0', security_group_id='xyz123') + ] + retval = _utils.normalize_nova_secgroup_rules(nova_rules) + self.assertEqual(expected, retval) diff --git a/shade/tests/unit/test_security_groups.py b/shade/tests/unit/test_security_groups.py index 83c21a021..12ae38213 100644 --- a/shade/tests/unit/test_security_groups.py +++ b/shade/tests/unit/test_security_groups.py @@ -200,3 +200,58 @@ def test_update_security_group_bad_kwarg(self, mock_nova, mock_neutron): 'doesNotExist', bad_arg='') self.assertFalse(mock_neutron.create_security_group.called) self.assertFalse(mock_nova.security_groups.create.called) + + @mock.patch.object(shade.OpenStackCloud, 'get_security_group') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_security_group_rule_neutron(self, mock_neutron, mock_get): + self.cloud.secgroup_source = 'neutron' + args = dict( + port_range_min=-1, + port_range_max=40000, + protocol='tcp', + remote_ip_prefix='0.0.0.0/0', + remote_group_id='456', + direction='egress', + ethertype='IPv6' + ) + mock_get.return_value = {'id': 'abc'} + self.cloud.create_security_group_rule(secgroup_name_or_id='abc', + **args) + + # For neutron, -1 port should be converted to None + args['port_range_min'] = None + args['security_group_id'] = 'abc' + + mock_neutron.create_security_group_rule.assert_called_once_with( + body={'security_group_rule': args} + ) + + @mock.patch.object(shade.OpenStackCloud, 'get_security_group') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_create_security_group_rule_nova(self, mock_nova, mock_get): + self.cloud.secgroup_source = 'nova' + + new_rule = fakes.FakeNovaSecgroupRule( + id='xyz', from_port=-1, to_port=2000, ip_protocol='tcp', + cidr='1.2.3.4/32') + mock_nova.security_group_rules.create.return_value = new_rule + mock_get.return_value = {'id': 'abc'} + + self.cloud.create_security_group_rule( + 'abc', port_range_max=2000, protocol='tcp', + remote_ip_prefix='1.2.3.4/32', remote_group_id='123') + + mock_nova.security_group_rules.create.assert_called_once_with( + parent_group_id='abc', ip_protocol='tcp', from_port=-1, + to_port=2000, cidr='1.2.3.4/32', group_id='123' + ) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_create_security_group_rule_none(self, mock_nova, mock_neutron): + self.cloud.secgroup_source = None + self.assertRaises(shade.OpenStackCloudUnavailableFeature, + self.cloud.create_security_group_rule, + '') + self.assertFalse(mock_neutron.create_security_group.called) + self.assertFalse(mock_nova.security_groups.create.called) From 6f5bf9c60cf09ce3b7600695d4ce825285e272f9 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Fri, 12 Jun 2015 09:29:52 -0400 Subject: [PATCH 0331/3836] Correct get_machine_by_mac and add test Corrected get_machine_by_mac so it is functional and added a test in order to help prevent it from being accidently broken at some point down the line. Change-Id: I1c1b40f1827c9fafe8826f7b07942c2ed7c85228 --- shade/__init__.py | 9 ++++++++- shade/tests/unit/test_shade.py | 21 +++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index f7ee3c138..43337b3e1 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2630,9 +2630,16 @@ def get_machine(self, name_or_id): return None def get_machine_by_mac(self, mac): + """Get machine by port MAC address + + :param mac: Port MAC address to query in order to return a node. + + :returns: Dictonary representing the node found or None + if the node is not found. + """ try: port = self.manager.submitTask( - _tasks.MachineNodePortGet(port_id=mac)) + _tasks.MachinePortGetByAddress(address=mac)) return meta.obj_to_dict( self.ironic_client.node.get(port.node_uuid)) except ironic_exceptions.ClientException: diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index a46ec1a3d..e65d9a25e 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -335,6 +335,27 @@ def setUp(self): def test_operator_cloud(self): self.assertIsInstance(self.cloud, shade.OperatorCloud) + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_get_machine_by_mac(self, mock_client): + class port_value: + node_uuid = '00000000-0000-0000-0000-000000000000' + address = '00:00:00:00:00:00' + + class node_value: + uuid = '00000000-0000-0000-0000-000000000000' + + expected_value = dict( + uuid='00000000-0000-0000-0000-000000000000') + + mock_client.port.get_by_address.return_value = port_value + mock_client.node.get.return_value = node_value + machine = self.cloud.get_machine_by_mac('00:00:00:00:00:00') + mock_client.port.get_by_address.assert_called_with( + address='00:00:00:00:00:00') + mock_client.node.get.assert_called_with( + '00000000-0000-0000-0000-000000000000') + self.assertEqual(machine, expected_value) + @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_list_nics(self, mock_client): port1 = fakes.FakeMachinePort(1, "aa:bb:cc:dd", "node1") From 20f86ae4b66571cfb4926d1a1495a4744b0da62f Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Fri, 12 Jun 2015 22:11:45 +0100 Subject: [PATCH 0332/3836] Improve documentation for create_port() Improve documentation for the fixed_ips parameter of create_port() Change-Id: Ie29e388a43a633e2434584b1dc3752cb83e325ae --- shade/__init__.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index f7ee3c138..eb2af5c40 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2256,8 +2256,16 @@ def create_port(self, network_id, **kwargs): :param admin_state_up: The administrative status of the port, which is up (true, default) or down (false). (Optional) :param mac_address: The MAC address. (Optional) - :param fixed_ips: If you specify only a subnet ID, OpenStack Networking - allocates an available IP from that subnet to the port. (Optional) + :param fixed_ips: List of ip_addresses and subnet_ids. See subnet_id + and ip_address. (Optional) + For example:: + + [ + { + "ip_address": "10.29.29.13", + "subnet_id": "a78484c4-c380-4b47-85aa-21c51a2d8cbd" + }, ... + ] :param subnet_id: If you specify only a subnet ID, OpenStack Networking allocates an available IP from that subnet to the port. (Optional) If you specify both a subnet ID and an IP address, OpenStack From b533b09373386ea4615ae17797212ce04f6b78ca Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Fri, 12 Jun 2015 22:15:15 +0100 Subject: [PATCH 0333/3836] Add more parameters to update_port() While writing an Ansible modules for Neutron port (os_port) it turned out almost all the parameters passed to create_port() function can be passed to update_port() as well. Official Neutron API documentation only mentions 'name', 'admin_state_up', 'fixed_ips' and 'security_groups'. This patch add all the parameters that can actually be passed to the Neutron update_port method. Change-Id: I0e274d882295c7435cbeda529c4f48e2cccd9a79 --- shade/__init__.py | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index eb2af5c40..766a64410 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2314,7 +2314,8 @@ def create_port(self, network_id, **kwargs): "error creating a new port for network " "'{net}': {msg}".format(net=network_id, msg=str(e))) - @valid_kwargs('name', 'admin_state_up', 'fixed_ips', 'security_groups') + @valid_kwargs('name', 'admin_state_up', 'fixed_ips', 'security_groups', + 'allowed_address_pairs', 'extra_dhcp_opts', 'device_owner') def update_port(self, name_or_id, **kwargs): """Update a port @@ -2325,9 +2326,40 @@ def update_port(self, name_or_id, **kwargs): :param name: A symbolic name for the port. (Optional) :param admin_state_up: The administrative status of the port, which is up (true) or down (false). (Optional) - :param fixed_ips: If you specify only a subnet ID, OpenStack Networking - allocates an available IP from that subnet to the port. (Optional) + :param fixed_ips: List of ip_addresses and subnet_ids. (Optional) + If you specify only a subnet ID, OpenStack Networking allocates + an available IP from that subnet to the port. + If you specify both a subnet ID and an IP address, OpenStack + Networking tries to allocate the specified address to the port. + For example:: + + [ + { + "ip_address": "10.29.29.13", + "subnet_id": "a78484c4-c380-4b47-85aa-21c51a2d8cbd" + }, ... + ] :param security_groups: List of security group UUIDs. (Optional) + :param allowed_address_pairs: Allowed address pairs list (Optional) + For example:: + + [ + { + "ip_address": "23.23.23.1", + "mac_address": "fa:16:3e:c4:cd:3f" + }, ... + ] + :param extra_dhcp_opts: Extra DHCP options. (Optional). + For example:: + + [ + { + "opt_name": "opt name1", + "opt_value": "value1" + }, ... + ] + :param device_owner: The ID of the entity that uses this port. + For example, a DHCP agent. (Optional) :returns: a dictionary describing the updated port. From 07f72e19f585138994eed2a446aec53ab3b90705 Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Sat, 6 Jun 2015 17:49:02 +0100 Subject: [PATCH 0334/3836] Add get_server_external_ipv4() to meta It turned out we can't rely on get_server_public_ip to get an externally reachable IP of servers because not always floating IPs are used and not always are what the caller is looking for. get_server_external_ipv4() tries hard to provide an IP address of the given server that can be reached from networks that are external to the cloud. Unfortunately from some clouds (or for very particularly configured servers with multiple interfaces) it could not be possible to reliably determine those IP addresses. Those clouds are corner cases that are not supported by get_server_external_ipv4() Co-Authored-By: Monty Taylor Change-Id: I099193437d0f45cfda78923349e500e9b2e0e053 --- shade/_utils.py | 40 ++++++++++++++++ shade/meta.py | 56 ++++++++++++++++++++++ shade/tests/fakes.py | 3 +- shade/tests/unit/test_meta.py | 89 +++++++++++++++++++++++++++++++++++ 4 files changed, 187 insertions(+), 1 deletion(-) diff --git a/shade/_utils.py b/shade/_utils.py index 6b5ece632..4261073e7 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -12,8 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +import re import time +from socket import inet_aton +from struct import unpack + from shade import exc @@ -153,3 +157,39 @@ def normalize_nova_secgroup_rules(rules): 'remote_ip_prefix': r['ip_range'].get('cidr', None), 'security_group_id': r['parent_group_id'] } for r in rules] + + +def is_ipv4(ip): + return re.match( + '^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|' + '[01]?[0-9][0-9]?)$', ip) is not None + + +def is_globally_routable_ipv4(ip): + # Comprehensive list of non-globally routable IPv4 networks + ngr_nets = ( + ["192.168.0.0", "255.255.0.0"], # rfc1918 + ["172.16.0.0", "255.240.0.0"], # rfc1918 + ["10.0.0.0", "255.0.0.0"], # rfc1918 + ["192.0.2.0", "255.255.255.0"], # rfc5737 + ["198.51.100.0", "255.255.255.0"], # rfc5737 + ["203.0.113.0", "255.255.255.0"], # rfc5737 + ["169.254.0.0", "255.255.0.0"], # rfc3927 + ["100.64.0.0", "255.192.0.0"], # rfc6598 + ["192.0.0.0", "255.255.255.0"], # rfc5736 + ["192.88.99.0", "255.255.255.0"], # rfc3068 + ["198.18.0.0", "255.254.0.0"], # rfc2544 + ["224.0.0.0", "240.0.0.0"], # rfc5771 + ["240.0.0.0", "240.0.0.0"], # rfc6890 + ["0.0.0.0", "255.0.0.0"], # rfc1700 + ["255.255.255.255", "0.0.0.0"], # rfc6890 + ["127.0.0.0", "255.0.0.0"], # rfc3330 + ) + + int_ip = unpack('!I', inet_aton(ip))[0] + for net in ngr_nets: + mask = unpack('!I', inet_aton(net[1]))[0] + if (int_ip & mask) == unpack('!I', inet_aton(net[0]))[0]: + return False + + return True diff --git a/shade/meta.py b/shade/meta.py index 2f19dc651..0e5780041 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -14,12 +14,19 @@ import bunch +import logging import six +from neutronclient.common.exceptions import NeutronClientException + from shade import exc +from shade import _utils + NON_CALLABLES = (six.string_types, bool, dict, int, list, type(None)) +log = logging.getLogger(__name__) + def find_nova_addresses(addresses, ext_tag=None, key_name=None, version=4): @@ -52,6 +59,55 @@ def get_server_public_ip(server): return get_server_ip(server, ext_tag='floating', key_name='public') +def get_server_external_ipv4(cloud, server): + if cloud.has_service('network'): + try: + # Search a fixed IP attached to an external net. Unfortunately + # Neutron ports don't have a 'floating_ips' attribute + server_ports = cloud.search_ports( + filters={'device_id': server.id}) + ext_nets = cloud.search_networks(filters={'router:external': True}) + except NeutronClientException as e: + log.debug( + "Something went wrong talking to neutron API: " + "'{msg}'. Trying with Nova.".format(msg=str(e))) + # Fall-through, trying with Nova + else: + for net in ext_nets: + for port in server_ports: + if net['id'] == port['network_id']: + for ip in port['fixed_ips']: + if _utils.is_ipv4(ip['ip_address']): + return ip['ip_address'] + # The server doesn't have an interface on an external network so it + # can either have a floating IP or have no way to be reached from + # outside the cloud. + # Fall-through, trying with Nova + + # The cloud doesn't support Neutron or Neutron can't be contacted. The + # server might have fixed addresses that are reachable from outside the + # cloud (e.g. Rax) or have plain ol' floating IPs + + # Try to get an address from a network named 'public' + ext_ip = get_server_ip(server, key_name='public') + if ext_ip is not None: + return ext_ip + + # Try to find a globally routable IP address + for interfaces in server.addresses.values(): + for interface in interfaces: + if _utils.is_ipv4(interface['addr']) and \ + _utils.is_globally_routable_ipv4(interface['addr']): + return interface['addr'] + + # Last, try to get a floating IP address + ext_ip = get_server_ip(server, ext_tag='floating') + if ext_ip is not None: + return ext_ip + + return None + + def get_groups_from_server(cloud, server, server_vars): groups = [] diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index a6446894b..9a6487a19 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -56,10 +56,11 @@ def __init__(self, id): class FakeServer(object): - def __init__(self, id, name, status): + def __init__(self, id, name, status, addresses=None): self.id = id self.name = name self.status = status + self.addresses = addresses class FakeService(object): diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index f6dd09008..3e1abce3a 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -16,7 +16,12 @@ import testtools import warlock +from neutronclient.common import exceptions as neutron_exceptions + +import shade from shade import meta +from shade import _utils +from shade.tests import fakes PRIVATE_V4 = '198.51.100.3' PUBLIC_V4 = '192.0.2.99' @@ -78,6 +83,90 @@ def test_get_server_ip(self): self.assertEqual(PRIVATE_V4, meta.get_server_private_ip(srv)) self.assertEqual(PUBLIC_V4, meta.get_server_public_ip(srv)) + @mock.patch.object(shade.OpenStackCloud, 'has_service') + @mock.patch.object(shade.OpenStackCloud, 'search_ports') + @mock.patch.object(shade.OpenStackCloud, 'search_networks') + @mock.patch.object(meta, 'get_server_ip') + def test_get_server_external_ipv4_neutron( + self, mock_get_server_ip, mock_search_networks, + mock_search_ports, mock_has_service): + # Testing Clouds with Neutron + mock_has_service.return_value = True + mock_search_ports.return_value = [{ + 'network_id': 'test-net-id', + 'fixed_ips': [{'ip_address': PUBLIC_V4}], + 'device_id': 'test-id' + }] + mock_search_networks.return_value = [{'id': 'test-net-id'}] + + srv = fakes.FakeServer( + id='test-id', name='test-name', status='ACTIVE') + ip = meta.get_server_external_ipv4( + cloud=shade.openstack_cloud(), server=srv) + + self.assertEqual(PUBLIC_V4, ip) + self.assertFalse(mock_get_server_ip.called) + + @mock.patch.object(shade.OpenStackCloud, 'has_service') + @mock.patch.object(shade.OpenStackCloud, 'search_ports') + @mock.patch.object(meta, 'get_server_ip') + def test_get_server_external_ipv4_neutron_exception( + self, mock_get_server_ip, mock_search_ports, mock_has_service): + # Testing Clouds with a non working Neutron + mock_has_service.return_value = True + mock_search_ports.side_effect = neutron_exceptions.NotFound() + mock_get_server_ip.return_value = PUBLIC_V4 + + srv = fakes.FakeServer( + id='test-id', name='test-name', status='ACTIVE') + ip = meta.get_server_external_ipv4( + cloud=shade.openstack_cloud(), server=srv) + + self.assertEqual(PUBLIC_V4, ip) + self.assertTrue(mock_get_server_ip.called) + + @mock.patch.object(shade.OpenStackCloud, 'has_service') + @mock.patch.object(meta, 'get_server_ip') + @mock.patch.object(_utils, 'is_globally_routable_ipv4') + def test_get_server_external_ipv4_nova_public( + self, mock_is_globally_routable_ipv4, + mock_get_server_ip, mock_has_service): + # Testing Clouds w/o Neutron and a network named public + mock_has_service.return_value = False + mock_get_server_ip.return_value = None + mock_is_globally_routable_ipv4.return_value = True + + srv = fakes.FakeServer( + id='test-id', name='test-name', status='ACTIVE', + addresses={'test-net': [{'addr': PUBLIC_V4}]}) + ip = meta.get_server_external_ipv4( + cloud=shade.openstack_cloud(), server=srv) + + self.assertEqual(PUBLIC_V4, ip) + self.assertTrue(mock_get_server_ip.called) + self.assertTrue(mock_is_globally_routable_ipv4.called) + + @mock.patch.object(shade.OpenStackCloud, 'has_service') + @mock.patch.object(meta, 'get_server_ip') + @mock.patch.object(_utils, 'is_globally_routable_ipv4') + def test_get_server_external_ipv4_nova_none( + self, mock_is_globally_routable_ipv4, + mock_get_server_ip, mock_has_service): + # Testing Clouds w/o Neutron and a globally routable IP + mock_has_service.return_value = False + mock_get_server_ip.return_value = None + mock_is_globally_routable_ipv4.return_value = False + + srv = fakes.FakeServer( + id='test-id', name='test-name', status='ACTIVE', + addresses={'test-net': [{'addr': PRIVATE_V4}]}) + ip = meta.get_server_external_ipv4( + cloud=shade.openstack_cloud(), server=srv) + + self.assertIsNone(ip) + self.assertTrue(mock_get_server_ip.called) + self.assertTrue(mock_is_globally_routable_ipv4.called) + def test_get_groups_from_server(self): server_vars = {'flavor': 'test-flavor', 'image': 'test-image', From 1f0171bea3d43635b02d40aaeab7ac969cfb54ee Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Sat, 13 Jun 2015 15:39:04 +0100 Subject: [PATCH 0335/3836] Replace get_server_public_ip() with get_server_external_ipv4() get_server_public_ip() just returns floating IP addresses or addresses from a network named 'public'. get_server_external_ipv4() tries hard to find an IPv4 address reachable from outside the cloud. Change-Id: I8e2c856b8f913424d3c0f20b72f240e08e4b9f62 --- shade/__init__.py | 2 +- shade/meta.py | 2 +- shade/tests/unit/test_meta.py | 20 ++++++++++++++++---- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 8a1df2a4d..f7c36cab6 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1638,7 +1638,7 @@ def get_server_private_ip(self, server): return meta.get_server_private_ip(server) def get_server_public_ip(self, server): - return meta.get_server_public_ip(server) + return meta.get_server_external_ipv4(self, server) def get_server_dict(self, name_or_id): server = self.get_server(name_or_id) diff --git a/shade/meta.py b/shade/meta.py index 0e5780041..d243330cd 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -155,7 +155,7 @@ def get_hostvars_from_server(cloud, server, mounts=None): server_vars.pop('links', None) # Fist, add an IP address - server_vars['public_v4'] = get_server_public_ip(server) + server_vars['public_v4'] = get_server_external_ipv4(cloud, server) server_vars['private_v4'] = get_server_private_ip(server) if cloud.private: interface_ip = server_vars['private_v4'] diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 3e1abce3a..fd668bc05 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -197,7 +197,10 @@ class obj1(object): self.assertEqual(new_list[0]['value'], 0) self.assertEqual(new_list[1]['value'], 1) - def test_basic_hostvars(self): + @mock.patch.object(shade.meta, 'get_server_external_ipv4') + def test_basic_hostvars(self, mock_get_server_external_ipv4): + mock_get_server_external_ipv4.return_value = PUBLIC_V4 + hostvars = meta.get_hostvars_from_server( FakeCloud(), meta.obj_to_dict(FakeServer())) self.assertNotIn('links', hostvars) @@ -216,21 +219,30 @@ def test_basic_hostvars(self): # test volume exception self.assertEquals([], hostvars['volumes']) - def test_private_interface_ip(self): + @mock.patch.object(shade.meta, 'get_server_external_ipv4') + def test_private_interface_ip(self, mock_get_server_external_ipv4): + mock_get_server_external_ipv4.return_value = PUBLIC_V4 + cloud = FakeCloud() cloud.private = True hostvars = meta.get_hostvars_from_server( cloud, meta.obj_to_dict(FakeServer())) self.assertEqual(PRIVATE_V4, hostvars['interface_ip']) - def test_image_string(self): + @mock.patch.object(shade.meta, 'get_server_external_ipv4') + def test_image_string(self, mock_get_server_external_ipv4): + mock_get_server_external_ipv4.return_value = PUBLIC_V4 + server = FakeServer() server.image = 'fake-image-id' hostvars = meta.get_hostvars_from_server( FakeCloud(), meta.obj_to_dict(server)) self.assertEquals('fake-image-id', hostvars['image']['id']) - def test_az(self): + @mock.patch.object(shade.meta, 'get_server_external_ipv4') + def test_az(self, mock_get_server_external_ipv4): + mock_get_server_external_ipv4.return_value = PUBLIC_V4 + server = FakeServer() server.__dict__['OS-EXT-AZ:availability_zone'] = 'az1' hostvars = meta.get_hostvars_from_server( From 669af9aa6083a7de717dde47251e506c99e06295 Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Sun, 14 Jun 2015 00:52:06 +0100 Subject: [PATCH 0336/3836] Refactor find_nova_addresses() Current implementation of find_nova_addresses() doesn't allow to search just IPv4 or IPv6 addresses without specifying key_name or ext_tag too. Moreover, when using key_name it doesn't take into account ext_tag. This patch refactors find_nova_addresses to be more "intuitive" and to allow searching just IPv4 or IPv6 addresses. Change-Id: I58601a8a12ede4519869c6cf2206a934c38553f3 --- shade/meta.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/shade/meta.py b/shade/meta.py index d243330cd..ecf04af68 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -32,15 +32,28 @@ def find_nova_addresses(addresses, ext_tag=None, key_name=None, version=4): ret = [] for (k, v) in iter(addresses.items()): - if key_name and k == key_name: - ret.extend([addrs['addr'] for addrs in v - if addrs['version'] == version]) - else: - for interface_spec in v: - if ('OS-EXT-IPS:type' in interface_spec - and interface_spec['OS-EXT-IPS:type'] == ext_tag - and interface_spec['version'] == version): - ret.append(interface_spec['addr']) + if key_name is not None and k != key_name: + # key_name is specified and it doesn't match the current network. + # Continue with the next one + continue + + for interface_spec in v: + if ext_tag is not None: + if 'OS-EXT-IPS:type' not in interface_spec: + # ext_tag is specified, but this interface has no tag + # We could actually return right away as this means that + # this cloud doesn't support OS-EXT-IPS. Nevertheless, + # it would be better to perform an explicit check. e.g.: + # cloud._has_nova_extension('OS-EXT-IPS') + # But this needs cloud to be passed to this function. + continue + elif interface_spec['OS-EXT-IPS:type'] != ext_tag: + # Type doesn't match, continue with next one + continue + + if interface_spec['version'] == version: + ret.append(interface_spec['addr']) + return ret From 3cee02eb80d048dbc520a933ab78a925c4e7118f Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Sun, 14 Jun 2015 00:52:35 +0100 Subject: [PATCH 0337/3836] Add get_server_external_ipv6() to meta This function assumes that if a server has an IPv6 address, that address is reachable from outside the cloud Change-Id: Ie8949a85d676fc836f90303e61e5abcd1f11cc53 --- shade/meta.py | 15 +++++++++++++++ shade/tests/unit/test_meta.py | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/shade/meta.py b/shade/meta.py index ecf04af68..da22b3119 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -121,6 +121,21 @@ def get_server_external_ipv4(cloud, server): return None +def get_server_external_ipv6(server): + """ Get an IPv6 address reachable from outside the cloud. + + This function assumes that if a server has an IPv6 address, that address + is reachable from outside the cloud. + + :param server: the server from which we want to get an IPv6 address + :return: a string containing the IPv6 address or None + """ + addresses = find_nova_addresses(addresses=server.addresses, version=6) + if addresses: + return addresses[0] + return None + + def get_groups_from_server(cloud, server, server_vars): groups = [] diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index fd668bc05..703f7c202 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -25,6 +25,7 @@ PRIVATE_V4 = '198.51.100.3' PUBLIC_V4 = '192.0.2.99' +PUBLIC_V6 = '2001:0db8:face:0da0:face::0b00:1c' # rfc3849 class FakeCloud(object): @@ -78,6 +79,28 @@ def test_find_nova_addresses_ext_tag(self): ['198.51.100.2'], meta.find_nova_addresses(addrs, ext_tag='fixed')) self.assertEqual([], meta.find_nova_addresses(addrs, ext_tag='foo')) + def test_find_nova_addresses_key_name_and_ext_tag(self): + addrs = {'public': [{'OS-EXT-IPS:type': 'fixed', + 'addr': '198.51.100.2', + 'version': 4}]} + self.assertEqual( + ['198.51.100.2'], meta.find_nova_addresses( + addrs, key_name='public', ext_tag='fixed')) + self.assertEqual([], meta.find_nova_addresses( + addrs, key_name='public', ext_tag='foo')) + self.assertEqual([], meta.find_nova_addresses( + addrs, key_name='bar', ext_tag='fixed')) + + def test_find_nova_addresses_all(self): + addrs = {'public': [{'OS-EXT-IPS:type': 'fixed', + 'addr': '198.51.100.2', + 'version': 4}]} + self.assertEqual( + ['198.51.100.2'], meta.find_nova_addresses( + addrs, key_name='public', ext_tag='fixed', version=4)) + self.assertEqual([], meta.find_nova_addresses( + addrs, key_name='public', ext_tag='fixed', version=6)) + def test_get_server_ip(self): srv = FakeServer() self.assertEqual(PRIVATE_V4, meta.get_server_private_ip(srv)) @@ -167,6 +190,19 @@ def test_get_server_external_ipv4_nova_none( self.assertTrue(mock_get_server_ip.called) self.assertTrue(mock_is_globally_routable_ipv4.called) + def test_get_server_external_ipv6(self): + srv = fakes.FakeServer( + id='test-id', name='test-name', status='ACTIVE', + addresses={ + 'test-net': [ + {'addr': PUBLIC_V4, 'version': 4}, + {'addr': PUBLIC_V6, 'version': 6} + ] + } + ) + ip = meta.get_server_external_ipv6(srv) + self.assertEqual(PUBLIC_V6, ip) + def test_get_groups_from_server(self): server_vars = {'flavor': 'test-flavor', 'image': 'test-image', From f6c0f808fa879024881aa7b714f05fc14175e73d Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 15 Jun 2015 15:51:47 -0400 Subject: [PATCH 0338/3836] Add delete method for security group rules Adds new API method for deleting a security group rule. This conforms to the newly agreed upon pattern of returning True if the resource was deleted, or False if it was not found and could not be deleted. Change-Id: I9c66aee078fa999a85336f600f086b93640ca96e --- shade/__init__.py | 54 ++++++++++++++++++++++++ shade/_tasks.py | 10 +++++ shade/tests/unit/test_security_groups.py | 48 +++++++++++++++++++++ 3 files changed, 112 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 89d7bb189..c44a29a92 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -32,6 +32,7 @@ from keystoneclient import session as ksc_session from novaclient import client as nova_client from novaclient import exceptions as nova_exceptions +from neutronclient.common import exceptions as neutron_exceptions from neutronclient.v2_0 import client as neutron_client import os_client_config import os_client_config.defaults @@ -2704,6 +2705,59 @@ def create_security_group_rule(self, "Unavailable feature: security groups" ) + def delete_security_group_rule(self, rule_id): + """Delete a security group rule + + :param string rule_id: The unique ID of the security group rule. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException on operation error. + :raises: OpenStackCloudUnavailableFeature if security groups are + not supported on this cloud. + """ + + if self.secgroup_source == 'neutron': + try: + self.manager.submitTask( + _tasks.NeutronSecurityGroupRuleDelete( + security_group_rule=rule_id) + ) + except neutron_exceptions.NotFound: + return False + except Exception as e: + self.log.debug( + "neutron failed to delete security group rule {id}".format( + id=rule_id), + exc_info=True) + raise OpenStackCloudException( + "failed to delete security group rule {id}: {msg}".format( + id=rule_id, msg=str(e))) + return True + + elif self.secgroup_source == 'nova': + try: + self.manager.submitTask( + _tasks.NovaSecurityGroupRuleDelete(rule=rule_id) + ) + except nova_exceptions.NotFound: + return False + except Exception as e: + self.log.debug( + "nova failed to delete security group rule {id}".format( + id=rule_id), + exc_info=True) + raise OpenStackCloudException( + "failed to delete security group rule {id}: {msg}".format( + id=rule_id, msg=str(e))) + return True + + # Security groups not supported + else: + raise OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + class OperatorCloud(OpenStackCloud): """Represent a privileged/operator connection to an OpenStack Cloud. diff --git a/shade/_tasks.py b/shade/_tasks.py index 499d229d5..29fa6cfdb 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -217,6 +217,11 @@ def main(self, client): return client.neutron_client.create_security_group_rule(**self.args) +class NeutronSecurityGroupRuleDelete(task_manager.Task): + def main(self, client): + return client.neutron_client.delete_security_group_rule(**self.args) + + class NovaSecurityGroupList(task_manager.Task): def main(self, client): return client.nova_client.security_groups.list() @@ -242,6 +247,11 @@ def main(self, client): return client.nova_client.security_group_rules.create(**self.args) +class NovaSecurityGroupRuleDelete(task_manager.Task): + def main(self, client): + return client.nova_client.security_group_rules.delete(**self.args) + + # TODO: Do this with neutron instead of nova if possible class FloatingIPList(task_manager.Task): def main(self, client): diff --git a/shade/tests/unit/test_security_groups.py b/shade/tests/unit/test_security_groups.py index 12ae38213..33ea7090b 100644 --- a/shade/tests/unit/test_security_groups.py +++ b/shade/tests/unit/test_security_groups.py @@ -16,6 +16,9 @@ import copy import mock +from novaclient import exceptions as nova_exc +from neutronclient.common import exceptions as neutron_exc + import shade from shade import meta from shade.tests.unit import base @@ -255,3 +258,48 @@ def test_create_security_group_rule_none(self, mock_nova, mock_neutron): '') self.assertFalse(mock_neutron.create_security_group.called) self.assertFalse(mock_nova.security_groups.create.called) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_delete_security_group_rule_neutron(self, mock_neutron): + self.cloud.secgroup_source = 'neutron' + r = self.cloud.delete_security_group_rule('xyz') + mock_neutron.delete_security_group_rule.assert_called_once_with( + security_group_rule='xyz') + self.assertTrue(r) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_delete_security_group_rule_nova(self, mock_nova): + self.cloud.secgroup_source = 'nova' + r = self.cloud.delete_security_group_rule('xyz') + mock_nova.security_group_rules.delete.assert_called_once_with( + rule='xyz') + self.assertTrue(r) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_delete_security_group_rule_none(self, mock_nova, mock_neutron): + self.cloud.secgroup_source = None + self.assertRaises(shade.OpenStackCloudUnavailableFeature, + self.cloud.delete_security_group_rule, + '') + self.assertFalse(mock_neutron.create_security_group.called) + self.assertFalse(mock_nova.security_groups.create.called) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_delete_security_group_rule_not_found(self, + mock_nova, + mock_neutron): + self.cloud.secgroup_source = 'neutron' + mock_neutron.delete_security_group_rule.side_effect = ( + neutron_exc.NotFound() + ) + r = self.cloud.delete_security_group('doesNotExist') + self.assertFalse(r) + + self.cloud.secgroup_source = 'nova' + mock_neutron.security_group_rules.delete.side_effect = ( + nova_exc.NotFound("uh oh") + ) + r = self.cloud.delete_security_group('doesNotExist') + self.assertFalse(r) From 1550cfce0a11eb92f2411be9c94136e01c0a185b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 14 Jun 2015 04:18:32 +0300 Subject: [PATCH 0339/3836] Add some accessor methods to CloudConfig There are some questions that people might want to ask without necessarily digging in to the underlying dict (this came up when noodling on openstacksdk factory functions. Change-Id: I3d9554a5e64797794de646d4d0d61936b857f2b4 --- doc/source/vendor-support.rst | 42 +++++++++++++++++--------------- os_client_config/cloud_config.py | 37 ++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 19 deletions(-) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index e7d51c01a..314e36229 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -8,6 +8,22 @@ information about various things a user would need to know. The following is a text representation of the vendor related defaults `os-client-config` knows about. +Default Values +-------------- + +These are the default behaviors unless a cloud is configured differently. + +* Identity uses `password` authentication +* Identity API Version is 2 +* Image API Version is 1 +* Images must be in `qcow2` format +* Images are uploaded using PUT interface +* Public IPv4 is directly routable via DHCP from Neutron +* IPv6 is not provided +* Floating IPs are provided by Neutron +* Security groups are provided by Neutron +* Vendor specific agents are not used + hp -- @@ -21,10 +37,6 @@ region-b.geo-1 US East ============== ================ * DNS Service Type is `hpext:dns` -* Image API Version is 1 -* Images must be in `qcow2` format -* Floating IPs are provided by Neutron -* Security groups are provided by Neutron rackspace --------- @@ -47,6 +59,8 @@ HKG Hong Kong * Images must be in `vhd` format * Images must be uploaded using the Glance Task Interface * Floating IPs are not needed +* Public IPv4 is directly routable via static config by Nova +* IPv6 is provided to every server * Security groups are not supported * Uploaded Images need properties to not use vendor agent :vm_mode: hvm @@ -65,8 +79,8 @@ RegionOne Region One * Image API Version is 2 * Images must be in `raw` format -* Floating IPs are provided by Neutron -* Security groups are provided by Neutron +* Public IPv4 is provided via Floating IP from Neutron +* IPv6 is provided to every server vexxhost -------- @@ -80,9 +94,6 @@ ca-ymq-1 Montreal ============== ================ * Image API Version is 2 -* Images must be in `qcow2` format -* Floating IPs are not needed -* Security groups are provided by Neutron runabove -------- @@ -98,8 +109,7 @@ BHS-1 Beauharnois, QC * Image API Version is 2 * Images must be in `qcow2` format -* Floating IPs are not needed -* Security groups are provided by Neutron +* Floating IPs are not supported unitedstack ----------- @@ -116,8 +126,6 @@ gd1 Guangdong * Identity API Version is 3 * Image API Version is 2 * Images must be in `raw` format -* Floating IPs are not needed -* Security groups are provided by Neutron auro ---- @@ -131,8 +139,7 @@ RegionOne RegionOne ============== ================ * Identity API Version is 2 -* Image API Version is 1 -* Images must be in `qcow2` format +* Public IPv4 is provided via Floating IP from Nova * Floating IPs are provided by Nova * Security groups are provided by Nova @@ -147,8 +154,5 @@ Region Name Human Name SBG-1 Strassbourg, FR ============== ================ -* Identity API Version is 2 -* Image API Version is 1 * Images must be in `raw` format -* Floating IPs are provided by Neutron -* Security groups are provided by Neutron +* Floating IPs are not supported diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 018908e92..19f1bb4ee 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -59,3 +59,40 @@ def get_requests_verify_args(self): if self.config['key']: cert = (cert, self.config['key']) return (verify, cert) + + def get_services(self): + """Return a list of service types we know something about.""" + services = [] + for key, val in self.config.items(): + if (key.endswith('api_version') + or key.endswith('service_type') + or key.endswith('service_name')): + services.append("_".join(key.split('_')[:-2])) + return list(set(services)) + + def get_auth_args(self): + return self.config['auth'] + + def get_endpoint_type(self, service_type=None): + if not service_type: + return self.config['endpoint_type'] + key = '{service_type}_endpoint_type'.format(service_type=service_type) + return self.config.get(key, self.config['endpoint_type']) + + def get_region_name(self, service_type=None): + if not service_type: + return self.region + key = '{service_type}_region_name'.format(service_type=service_type) + return self.config.get(key, self.region) + + def get_api_version(self, service_type): + key = '{service_type}_api_version'.format(service_type=service_type) + return self.config.get(key, None) + + def get_service_type(self, service_type): + key = '{service_type}_service_type'.format(service_type=service_type) + return self.config.get(key, service_type) + + def get_service_name(self, service_type): + key = '{service_type}_service_name'.format(service_type=service_type) + return self.config.get(key, service_type) From 03967b17ff5999242007db8fead832f9754bb55c Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 17 Jun 2015 09:55:32 -0400 Subject: [PATCH 0340/3836] Return True/False for delete methods This changes many (not all) of our delete methods to have consistent return values: True if the resource was deleted, False if it was not found for deletion. The methods being changed here either returned nothing at all, or they raised an exception on resource not found. The ones that raised an exception no longer raise this exception, and instead return False. We also make sure that a debug log message is output for a delete request that could not find the resource. Change-Id: I0ee4654dc6f68b475179d36002fe9db713e26290 --- shade/__init__.py | 51 ++++++++++++++++++++++++---------- shade/tests/unit/test_shade.py | 10 +++---- 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 95394515f..7df14acfc 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1081,12 +1081,15 @@ def delete_network(self, name_or_id): """Delete a network. :param name_or_id: Name or ID of the network being deleted. + + :returns: True if delete succeeded, False otherwise. + :raises: OpenStackCloudException on operation error. """ network = self.get_network(name_or_id) if not network: - raise OpenStackCloudException( - "Network %s not found." % name_or_id) + self.log.debug("Network %s not found for deleting" % name_or_id) + return False try: self.manager.submitTask( @@ -1095,6 +1098,7 @@ def delete_network(self, name_or_id): self.log.debug("Network deletion failed", exc_info=True) raise OpenStackCloudException( "Error in deleting network %s: %s" % (name_or_id, str(e))) + return True def create_router(self, name=None, admin_state_up=True): """Create a logical router. @@ -1175,12 +1179,15 @@ def delete_router(self, name_or_id): not required to be unique. An error will be raised in this case. :param name_or_id: Name or ID of the router being deleted. + + :returns: True if delete succeeded, False otherwise. + :raises: OpenStackCloudException on operation error. """ router = self.get_router(name_or_id) if not router: - raise OpenStackCloudException( - "Router %s not found." % name_or_id) + self.log.debug("Router %s not found for deleting" % name_or_id) + return False try: self.manager.submitTask( @@ -1189,6 +1196,7 @@ def delete_router(self, name_or_id): self.log.debug("Router delete failed", exc_info=True) raise OpenStackCloudException( "Error deleting router %s: %s" % (name_or_id, e)) + return True def _reset_image_cache(self): self._image_cache = None @@ -2169,12 +2177,15 @@ def delete_subnet(self, name_or_id): not required to be unique. An error will be raised in this case. :param name_or_id: Name or ID of the subnet being deleted. + + :returns: True if delete succeeded, False otherwise. + :raises: OpenStackCloudException on operation error. """ subnet = self.get_subnet(name_or_id) if not subnet: - raise OpenStackCloudException( - "Subnet %s not found." % name_or_id) + self.log.debug("Subnet %s not found for deleting" % name_or_id) + return False try: self.manager.submitTask( @@ -2183,6 +2194,7 @@ def delete_subnet(self, name_or_id): self.log.debug("Subnet delete failed", exc_info=True) raise OpenStackCloudException( "Error deleting subnet %s: %s" % (name_or_id, e)) + return True def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, gateway_ip=None, allocation_pools=None, @@ -2410,13 +2422,14 @@ def delete_port(self, name_or_id): :param name_or_id: id or name of the port to delete. - :returns: None. + :returns: True if delete succeeded, False otherwise. :raises: OpenStackCloudException on operation error. """ port = self.get_port(name_or_id=name_or_id) if port is None: - return + self.log.debug("Port %s not found for deleting" % name_or_id) + return False try: self.manager.submitTask(_tasks.PortDelete(port=port['id'])) @@ -2426,6 +2439,7 @@ def delete_port(self, name_or_id): raise OpenStackCloudException( "failed to delete port '{port}': {msg}".format( port=name_or_id, msg=str(e))) + return True def create_security_group(self, name, description): """Create a new security group @@ -2485,14 +2499,17 @@ def delete_security_group(self, name_or_id): :param string name_or_id: The name or unique ID of the security group. + :returns: True if delete succeeded, False otherwise. + :raises: OpenStackCloudException on operation error. :raises: OpenStackCloudUnavailableFeature if security groups are not supported on this cloud. """ secgroup = self.get_security_group(name_or_id) if secgroup is None: - self.log.debug('security group %s was not found' % name_or_id) - return + self.log.debug('Security group %s not found for deleting' % + name_or_id) + return False if self.secgroup_source == 'neutron': try: @@ -2508,6 +2525,7 @@ def delete_security_group(self, name_or_id): raise OpenStackCloudException( "failed to delete security group '{group}': {msg}".format( group=name_or_id, msg=str(e))) + return True elif self.secgroup_source == 'nova': try: @@ -2521,6 +2539,7 @@ def delete_security_group(self, name_or_id): raise OpenStackCloudException( "failed to delete security group '{group}': {msg}".format( group=name_or_id, msg=str(e))) + return True # Security groups not supported else: @@ -3472,14 +3491,15 @@ def delete_service(self, name_or_id): :param name_or_id: Service name or id. - :returns: None + :returns: True if delete succeeded, False otherwise. :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call """ service = self.get_service(name_or_id=name_or_id) if service is None: - return + self.log.debug("Service %s not found for deleting" % name_or_id) + return False try: self.manager.submitTask(_tasks.ServiceDelete(id=service['id'])) @@ -3488,6 +3508,7 @@ def delete_service(self, name_or_id): "Failed to delete service {id}".format(id=service['id']), exc_info=True) raise OpenStackCloudException(str(e)) + return True def create_endpoint(self, service_name_or_id, public_url, internal_url=None, admin_url=None, region=None): @@ -3583,7 +3604,7 @@ def delete_endpoint(self, id): :param id: Id of the endpoint to delete. - :returns: None + :returns: True if delete succeeded, False otherwise. :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call. @@ -3591,7 +3612,8 @@ def delete_endpoint(self, id): # ToDo: support v3 api (dguerri) endpoint = self.get_endpoint(id=id) if endpoint is None: - return + self.log.debug("Endpoint %s not found for deleting" % id) + return False try: self.manager.submitTask(_tasks.EndpointDelete(id=id)) @@ -3600,3 +3622,4 @@ def delete_endpoint(self, id): "Failed to delete endpoint {id}".format(id=id), exc_info=True) raise OpenStackCloudException(str(e)) + return True diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 4c814979b..0ac004767 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -114,9 +114,8 @@ def test_delete_router(self, mock_client, mock_search): @mock.patch.object(shade.OpenStackCloud, 'neutron_client') def test_delete_router_not_found(self, mock_client, mock_search): mock_search.return_value = [] - self.assertRaises(exc.OpenStackCloudException, - self.cloud.delete_router, - 'goofy') + r = self.cloud.delete_router('goofy') + self.assertFalse(r) self.assertFalse(mock_client.delete_router.called) @mock.patch.object(shade.OpenStackCloud, 'neutron_client') @@ -186,9 +185,8 @@ def test_delete_subnet(self, mock_client, mock_search): @mock.patch.object(shade.OpenStackCloud, 'neutron_client') def test_delete_subnet_not_found(self, mock_client, mock_search): mock_search.return_value = [] - self.assertRaises(exc.OpenStackCloudException, - self.cloud.delete_subnet, - 'goofy') + r = self.cloud.delete_subnet('goofy') + self.assertFalse(r) self.assertFalse(mock_client.delete_subnet.called) @mock.patch.object(shade.OpenStackCloud, 'neutron_client') From 208c084a5495af2d11aecda8d58fb0d7c9a48615 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 17 Jun 2015 12:02:03 -0400 Subject: [PATCH 0341/3836] Convert ironicclient node.list() call to Task Changes the node.list() calls to Task calls. Also adds missing test case. Change-Id: I5a7cd1a4193777d1c1f96c87efd1308a29f59b64 --- shade/__init__.py | 4 +++- shade/_tasks.py | 5 +++++ shade/tests/fakes.py | 14 ++++++++++++++ shade/tests/unit/test_shade.py | 8 ++++++++ 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index 95394515f..e39252ee6 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2848,7 +2848,9 @@ def get_nic_by_mac(self, mac): return None def list_machines(self): - return meta.obj_list_to_dict(self.ironic_client.node.list()) + return meta.obj_list_to_dict( + self.manager.submitTask(_tasks.MachineNodeList()) + ) def get_machine(self, name_or_id): """Get Machine by name or uuid diff --git a/shade/_tasks.py b/shade/_tasks.py index 29fa6cfdb..c8f38b1ce 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -403,6 +403,11 @@ def main(self, client): return client.ironic_client.port.list() +class MachineNodeList(task_manager.Task): + def main(self, client): + return client.ironic_client.node.list(**self.args) + + class MachineNodePortList(task_manager.Task): def main(self, client): return client.ironic_client.node.list_ports(**self.args) diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 9a6487a19..56abb4e48 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -85,6 +85,20 @@ def __init__(self, id, status, display_name): self.display_name = display_name +class FakeMachine(object): + def __init__(self, id, name=None, driver=None, driver_info=None, + chassis_uuid=None, instance_info=None, instance_uuid=None, + properties=None): + self.id = id + self.name = name + self.driver = driver + self.driver_info = driver_info + self.chassis_uuid = chassis_uuid + self.instance_info = instance_info + self.instance_uuid = instance_uuid + self.properties = properties + + class FakeMachinePort(object): def __init__(self, id, address, node_id): self.id = id diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 4c814979b..3231a0975 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -313,6 +313,14 @@ class node_value: '00000000-0000-0000-0000-000000000000') self.assertEqual(machine, expected_value) + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_list_machines(self, mock_client): + m1 = fakes.FakeMachine(1, 'fake_machine1') + mock_client.node.list.return_value = [m1] + machines = self.cloud.list_machines() + self.assertTrue(mock_client.node.list.called) + self.assertEqual(meta.obj_to_dict(m1), machines[0]) + @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_list_nics(self, mock_client): port1 = fakes.FakeMachinePort(1, "aa:bb:cc:dd", "node1") From 6098388a490c6e81f51161dd83b23fd205b34977 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 17 Jun 2015 12:17:52 -0400 Subject: [PATCH 0342/3836] Convert ironicclient node.validate() call to Task Changes the node.validate() calls to Task calls. Also adds missing test case. Change-Id: I3db70de3ecd1faf5380f9b3201dbecc37541b994 --- shade/__init__.py | 3 ++- shade/_tasks.py | 5 +++++ shade/tests/unit/test_shade.py | 8 ++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index e39252ee6..c53f4424a 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -3163,7 +3163,8 @@ def update_machine(self, name_or_id, chassis_uuid=None, driver=None, def validate_node(self, uuid): try: - ifaces = self.ironic_client.node.validate(uuid) + ifaces = self.manager.submitTask( + _tasks.MachineNodeValidate(node_uuid=uuid)) except Exception as e: self.log.debug( "ironic node validation call failed", exc_info=True) diff --git a/shade/_tasks.py b/shade/_tasks.py index c8f38b1ce..2c6ea150a 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -413,6 +413,11 @@ def main(self, client): return client.ironic_client.node.list_ports(**self.args) +class MachineNodeValidate(task_manager.Task): + def main(self, client): + return client.ironic_client.node.validate(**self.args) + + class MachineSetMaintenance(task_manager.Task): def main(self, client): return client.ironic_client.node.set_maintenance(**self.args) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 3231a0975..0a6e70756 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -321,6 +321,14 @@ def test_list_machines(self, mock_client): self.assertTrue(mock_client.node.list.called) self.assertEqual(meta.obj_to_dict(m1), machines[0]) + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_validate_node(self, mock_client): + node_uuid = '123' + self.cloud.validate_node(node_uuid) + mock_client.node.validate.assert_called_once_with( + node_uuid=node_uuid + ) + @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_list_nics(self, mock_client): port1 = fakes.FakeMachinePort(1, "aa:bb:cc:dd", "node1") From 11059de528e58d50c1f9c53bf89ba244e327c8f4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 18 Jun 2015 09:01:31 -0400 Subject: [PATCH 0343/3836] Pass token and endpoint to swift os_options get_capabilities runs through a code path in swiftclient where the auth token pieces need to be in os_options. We also need to tell swift to use keystone auth, not swift auth. Change-Id: I3768f40059b6f6591f67e47a3202ed17779d0fe8 --- shade/__init__.py | 6 +++++- shade/tests/unit/test_object.py | 11 ++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 95394515f..d3ce69887 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -610,7 +610,11 @@ def swift_client(self): self._swift_client = swift_client.Connection( preauthurl=endpoint, preauthtoken=token, - os_options=dict(region_name=self.region_name), + auth_version=self.api_versions['identity'], + os_options=dict( + auth_token=token, + object_storage_url=endpoint, + region_name=self.region_name), ) except OpenStackCloudException: raise diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index 3d886d92a..cf9e0254a 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -36,9 +36,14 @@ def test_swift_client(self, endpoint_mock, auth_mock, swift_mock): endpoint_mock.return_value = 'danzig' auth_mock.return_value = 'yankee' self.cloud.swift_client - swift_mock.assert_called_with(preauthurl='danzig', - preauthtoken='yankee', - os_options=mock.ANY) + swift_mock.assert_called_with( + preauthurl='danzig', + preauthtoken='yankee', + auth_version='2', + os_options=dict( + object_storage_url='danzig', + auth_token='yankee', + region_name='')) @mock.patch.object(shade.OpenStackCloud, 'auth_token', new_callable=mock.PropertyMock) From e25936b1a3e9af283705c982e03b2e11b6826795 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 18 Jun 2015 10:46:04 -0400 Subject: [PATCH 0344/3836] Ensure that service values are strings Sometimes ints can sneak in in api version values. We never want that. Change-Id: I730be13d3328bdee1652186021ee1579f8ef4b57 --- shade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index 9a8aec07d..214b16c71 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -158,7 +158,7 @@ def _get_service_values(kwargs, service_key): # get defauts returns a copy of the defaults dict values = os_client_config.defaults.get_defaults() values.update(kwargs) - return {k[:-(len(service_key) + 1)]: values[k] + return {k[:-(len(service_key) + 1)]: str(values[k]) for k in values.keys() if k.endswith(service_key)} From 6367ead33baa35939d591b76ffa9563887d1b25b Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Thu, 18 Jun 2015 09:20:58 -0700 Subject: [PATCH 0345/3836] Fix MD5 headers regression This regression was introduced when we switched to swiftclient.service.SwiftService for uploading because of a documentation bug. The option was listed as 'headers' but it should be 'header'. Change-Id: Id590f624aa084af0747b93ae70b214600147ebb2 --- shade/__init__.py | 3 ++- shade/tests/functional/test_object.py | 2 ++ shade/tests/unit/test_caching.py | 20 +++++++++++++------- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 9a8aec07d..87a1bf57e 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2022,6 +2022,7 @@ def create_object( (md5, sha256) = self._get_file_hashes(filename) headers[OBJECT_MD5_KEY] = md5 headers[OBJECT_SHA256_KEY] = sha256 + header_list = sorted([':'.join([k, v]) for (k, v) in headers.items()]) # On some clouds this is not necessary. On others it is. I'm confused. self.create_container(container) @@ -2034,7 +2035,7 @@ def create_object( object_name=name) for r in self.manager.submitTask(_tasks.ObjectCreate( container=container, objects=[upload], - options=dict(headers=headers, + options=dict(header=header_list, segment_size=segment_size))): if not r['success']: raise OpenStackCloudException( diff --git a/shade/tests/functional/test_object.py b/shade/tests/functional/test_object.py index 6202c79a5..8ca213986 100644 --- a/shade/tests/functional/test_object.py +++ b/shade/tests/functional/test_object.py @@ -54,6 +54,8 @@ def test_create_object(self): name = 'test-%d' % size self.cloud.create_object(container, name, sparse_file.name, segment_size=segment_size) + self.assertFalse(self.cloud.is_object_stale(container, name, + sparse_file.name)) self.assertIsNotNone( self.cloud.get_object_metadata(container, name)) self.cloud.delete_object(container, name) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index ef4b103c1..cd6ca6cbb 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -301,13 +301,15 @@ def test_create_image_put_v2(self, glance_mock): fake_image_dict = meta.obj_to_dict(fake_image) self.assertEqual([fake_image_dict], self.cloud.list_images()) + @mock.patch.object(shade.OpenStackCloud, '_get_file_hashes') @mock.patch.object(shade.OpenStackCloud, 'glance_client') @mock.patch.object(shade.OpenStackCloud, 'swift_client') @mock.patch.object(shade.OpenStackCloud, 'swift_service') def test_create_image_task(self, swift_service_mock, swift_mock, - glance_mock): + glance_mock, + get_file_hashes): self.cloud.api_versions['image'] = '2' self.cloud.image_api_use_tasks = True @@ -325,6 +327,10 @@ class Container(object): glance_mock.images.list.return_value = [] self.assertEqual([], self.cloud.list_images()) + fake_md5 = "fake-md5" + fake_sha256 = "fake-sha256" + get_file_hashes.return_value = (fake_md5, fake_sha256) + # V2's warlock objects just work like dicts class FakeImage(dict): status = 'CREATED' @@ -335,8 +341,8 @@ class FakeImage(dict): fake_image.update({ 'id': '99', 'name': '99 name', - shade.IMAGE_MD5_KEY: '', - shade.IMAGE_SHA256_KEY: '', + shade.IMAGE_MD5_KEY: fake_md5, + shade.IMAGE_SHA256_KEY: fake_sha256, }) glance_mock.images.list.return_value = [fake_image] @@ -352,8 +358,8 @@ class FakeTask(dict): glance_mock.tasks.get.return_value = fake_task self._call_create_image(name='99 name', container='image_upload_v2_test_container') - args = {'headers': {'x-object-meta-x-shade-md5': mock.ANY, - 'x-object-meta-x-shade-sha256': mock.ANY}, + args = {'header': ['x-object-meta-x-shade-md5:fake-md5', + 'x-object-meta-x-shade-sha256:fake-sha256'], 'segment_size': 1000} swift_service_mock.upload.assert_called_with( container='image_upload_v2_test_container', @@ -362,8 +368,8 @@ class FakeTask(dict): glance_mock.tasks.create.assert_called_with(type='import', input={ 'import_from': 'image_upload_v2_test_container/99 name', 'image_properties': {'name': '99 name'}}) - args = {'owner_specified.shade.md5': mock.ANY, - 'owner_specified.shade.sha256': mock.ANY, + args = {'owner_specified.shade.md5': fake_md5, + 'owner_specified.shade.sha256': fake_sha256, 'image_id': '99'} glance_mock.images.update.assert_called_with(**args) fake_image_dict = meta.obj_to_dict(fake_image) From ebfef54bc391bc8075a83e2cfa18c3f625374c1d Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Tue, 16 Jun 2015 13:49:26 +0100 Subject: [PATCH 0346/3836] Centralize exception management for Neutron A common pattern we use is to catch OpenStack clients exceptions and re-raise an OpenStackCloud* exceptions. Moreover, we need to distinguish NeutronClientException with status code 404 from other Neutron exceptions in order to handle Rax broken catalog (i.e. falling-through and using Nova API). This patch centralises all that. Change-Id: Ia14083bee7988e90a9e5ce04305a8ead4511b3d3 --- shade/__init__.py | 214 ++++++++++++--------------------- shade/exc.py | 8 ++ shade/tests/unit/test_shade.py | 50 ++++++++ 3 files changed, 132 insertions(+), 140 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 7947fa57c..9f88dddc9 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import contextlib import hashlib import inspect import logging @@ -322,6 +323,30 @@ def __init__(self, cloud, auth, self._swift_service = None self._trove_client = None + @contextlib.contextmanager + def _neutron_exceptions(self, error_message): + try: + yield + except neutron_exceptions.NotFound as e: + raise OpenStackCloudResourceNotFound( + "{msg}: {exc}".format(msg=error_message, exc=str(e))) + except neutron_exceptions.NeutronClientException as e: + self.log.debug( + "{msg}: {exc}".format(msg=error_message, exc=str(e), + exc_info=True)) + if e.status_code == 404: + raise OpenStackCloudURINotFound( + "{msg}: {exc}".format(msg=error_message, exc=str(e))) + else: + raise OpenStackCloudException( + "{msg}: {exc}".format(msg=error_message, exc=str(e))) + except Exception as e: + self.log.debug( + "{msg}: {exc}".format(msg=error_message, exc=str(e), + exc_info=True)) + raise OpenStackCloudException( + "{msg}: {exc}".format(msg=error_message, exc=str(e))) + def _make_cache_key(self, namespace, fn): fname = fn.__name__ if namespace is None: @@ -845,38 +870,20 @@ def search_floating_ip_pools(self, name=None, filters=None): return _utils._filter_list(pools, name, filters) def list_networks(self): - try: + with self._neutron_exceptions("Error fetching network list"): return self.manager.submitTask(_tasks.NetworkList())['networks'] - except Exception as e: - self.log.debug("network list failed: %s" % e, exc_info=True) - raise OpenStackCloudException( - "Error fetching network list: %s" % e) def list_routers(self): - try: + with self._neutron_exceptions("Error fetching router list"): return self.manager.submitTask(_tasks.RouterList())['routers'] - except Exception as e: - self.log.debug("router list failed: %s" % e, exc_info=True) - raise OpenStackCloudException( - "Error fetching router list: %s" % e) def list_subnets(self): - try: + with self._neutron_exceptions("Error fetching subnet list"): return self.manager.submitTask(_tasks.SubnetList())['subnets'] - except Exception as e: - self.log.debug("subnet list failed: %s" % e, exc_info=True) - raise OpenStackCloudException( - "Error fetching subnet list: %s" % e) def list_ports(self): - try: + with self._neutron_exceptions("Error fetching port list"): return self.manager.submitTask(_tasks.PortList())['ports'] - except Exception as e: - self.log.debug( - "neutron could not list ports: {msg}".format( - msg=str(e)), exc_info=True) - raise OpenStackCloudException( - "error fetching port list: {msg}".format(msg=str(e))) @_cache_on_arguments(should_cache_fn=_no_pending_volumes) def list_volumes(self, cache=True): @@ -907,18 +914,10 @@ def list_security_groups(self): # Handle neutron security groups if self.secgroup_source == 'neutron': # Neutron returns dicts, so no need to convert objects here. - try: - groups = self.manager.submitTask( + with self._neutron_exceptions( + "Error fetching security group list"): + return self.manager.submitTask( _tasks.NeutronSecurityGroupList())['security_groups'] - except Exception as e: - self.log.debug( - "neutron could not list security groups: {message}".format( - message=str(e)), - exc_info=True) - raise OpenStackCloudException( - "Error fetching security group list" - ) - return groups # Handle nova security groups elif self.secgroup_source == 'nova': @@ -1069,13 +1068,10 @@ def create_network(self, name, shared=False, admin_state_up=True): 'admin_state_up': admin_state_up } - try: + with self._neutron_exceptions( + "Error creating network {0}".format(name)): net = self.manager.submitTask( _tasks.NetworkCreate(body=dict({'network': network}))) - except Exception as e: - self.log.debug("Network creation failed", exc_info=True) - raise OpenStackCloudException( - "Error in creating network %s: %s" % (name, str(e))) # Turns out neutron returns an actual dict, so no need for the # use of meta.obj_to_dict() here (which would not work against # a dict). @@ -1095,13 +1091,11 @@ def delete_network(self, name_or_id): self.log.debug("Network %s not found for deleting" % name_or_id) return False - try: + with self._neutron_exceptions( + "Error deleting network {0}".format(name_or_id)): self.manager.submitTask( _tasks.NetworkDelete(network=network['id'])) - except Exception as e: - self.log.debug("Network deletion failed", exc_info=True) - raise OpenStackCloudException( - "Error in deleting network %s: %s" % (name_or_id, str(e))) + return True def create_router(self, name=None, admin_state_up=True): @@ -1119,13 +1113,10 @@ def create_router(self, name=None, admin_state_up=True): if name: router['name'] = name - try: + with self._neutron_exceptions( + "Error creating router {0}".format(name)): new_router = self.manager.submitTask( _tasks.RouterCreate(body=dict(router=router))) - except Exception as e: - self.log.debug("Router create failed", exc_info=True) - raise OpenStackCloudException( - "Error creating router %s: %s" % (name, e)) # Turns out neutron returns an actual dict, so no need for the # use of meta.obj_to_dict() here (which would not work against # a dict). @@ -1162,14 +1153,12 @@ def update_router(self, name_or_id, name=None, admin_state_up=None, raise OpenStackCloudException( "Router %s not found." % name_or_id) - try: + with self._neutron_exceptions( + "Error updating router {0}".format(name_or_id)): new_router = self.manager.submitTask( _tasks.RouterUpdate( router=curr_router['id'], body=dict(router=router))) - except Exception as e: - self.log.debug("Router update failed", exc_info=True) - raise OpenStackCloudException( - "Error updating router %s: %s" % (name_or_id, e)) + # Turns out neutron returns an actual dict, so no need for the # use of meta.obj_to_dict() here (which would not work against # a dict). @@ -1193,13 +1182,11 @@ def delete_router(self, name_or_id): self.log.debug("Router %s not found for deleting" % name_or_id) return False - try: + with self._neutron_exceptions( + "Error deleting router {0}".format(name_or_id)): self.manager.submitTask( _tasks.RouterDelete(router=router['id'])) - except Exception as e: - self.log.debug("Router delete failed", exc_info=True) - raise OpenStackCloudException( - "Error deleting router %s: %s" % (name_or_id, e)) + return True def _reset_image_cache(self): @@ -2163,14 +2150,11 @@ def create_subnet(self, network_name_or_id, cidr, ip_version=4, if ipv6_address_mode: subnet['ipv6_address_mode'] = ipv6_address_mode - try: + with self._neutron_exceptions( + "Error creating subnet on network " + "{0}".format(network_name_or_id)): new_subnet = self.manager.submitTask( _tasks.SubnetCreate(body=dict(subnet=subnet))) - except Exception as e: - self.log.debug("Subnet creation failed", exc_info=True) - raise OpenStackCloudException( - "Error in creating subnet on network %s: %s" - % (network_name_or_id, e)) return new_subnet['subnet'] @@ -2192,13 +2176,10 @@ def delete_subnet(self, name_or_id): self.log.debug("Subnet %s not found for deleting" % name_or_id) return False - try: + with self._neutron_exceptions( + "Error deleting subnet {0}".format(name_or_id)): self.manager.submitTask( _tasks.SubnetDelete(subnet=subnet['id'])) - except Exception as e: - self.log.debug("Subnet delete failed", exc_info=True) - raise OpenStackCloudException( - "Error deleting subnet %s: %s" % (name_or_id, e)) return True def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, @@ -2272,14 +2253,11 @@ def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, raise OpenStackCloudException( "Subnet %s not found." % name_or_id) - try: + with self._neutron_exceptions( + "Error updating subnet {0}".format(name_or_id)): new_subnet = self.manager.submitTask( _tasks.SubnetUpdate( subnet=curr_subnet['id'], body=dict(subnet=subnet))) - except Exception as e: - self.log.debug("Subnet update failed", exc_info=True) - raise OpenStackCloudException( - "Error updating subnet %s: %s" % (name_or_id, e)) # Turns out neutron returns an actual dict, so no need for the # use of meta.obj_to_dict() here (which would not work against # a dict). @@ -2344,16 +2322,10 @@ def create_port(self, network_id, **kwargs): """ kwargs['network_id'] = network_id - try: + with self._neutron_exceptions( + "Error creating port for network {0}".format(network_id)): return self.manager.submitTask( _tasks.PortCreate(body={'port': kwargs}))['port'] - except Exception as e: - self.log.debug("failed to create a new port for network" - "'{net}'".format(net=network_id), - exc_info=True) - raise OpenStackCloudException( - "error creating a new port for network " - "'{net}': {msg}".format(net=network_id, msg=str(e))) @valid_kwargs('name', 'admin_state_up', 'fixed_ips', 'security_groups', 'allowed_address_pairs', 'extra_dhcp_opts', 'device_owner') @@ -2411,16 +2383,11 @@ def update_port(self, name_or_id, **kwargs): raise OpenStackCloudException( "failed to find port '{port}'".format(port=name_or_id)) - try: + with self._neutron_exceptions( + "Error updating port {0}".format(name_or_id)): return self.manager.submitTask( _tasks.PortUpdate( port=port['id'], body={'port': kwargs}))['port'] - except Exception as e: - self.log.debug("failed to update port '{port}'".format( - port=name_or_id), exc_info=True) - raise OpenStackCloudException( - "failed to update port '{port}': {msg}".format( - port=name_or_id, msg=str(e))) def delete_port(self, name_or_id): """Delete a port @@ -2436,14 +2403,9 @@ def delete_port(self, name_or_id): self.log.debug("Port %s not found for deleting" % name_or_id) return False - try: + with self._neutron_exceptions( + "Error deleting port {0}".format(name_or_id)): self.manager.submitTask(_tasks.PortDelete(port=port['id'])) - except Exception as e: - self.log.debug("failed to delete port '{port}'".format( - port=name_or_id), exc_info=True) - raise OpenStackCloudException( - "failed to delete port '{port}': {msg}".format( - port=name_or_id, msg=str(e))) return True def create_security_group(self, name, description): @@ -2459,20 +2421,14 @@ def create_security_group(self, name, description): not supported on this cloud. """ if self.secgroup_source == 'neutron': - try: + with self._neutron_exceptions( + "Error creating security group {0}".format(name)): group = self.manager.submitTask( _tasks.NeutronSecurityGroupCreate( body=dict(security_group=dict(name=name, description=description)) ) ) - except Exception as e: - self.log.debug( - "neutron failed to create security group '{name}'".format( - name=name), exc_info=True) - raise OpenStackCloudException( - "failed to create security group '{name}': {msg}".format( - name=name, msg=str(e))) return group['security_group'] elif self.secgroup_source == 'nova': @@ -2517,19 +2473,13 @@ def delete_security_group(self, name_or_id): return False if self.secgroup_source == 'neutron': - try: + with self._neutron_exceptions( + "Error deleting security group {0}".format(name_or_id)): self.manager.submitTask( _tasks.NeutronSecurityGroupDelete( security_group=secgroup['id'] ) ) - except Exception as e: - self.log.debug( - "neutron failed to delete security group '{group}'".format( - group=name_or_id), exc_info=True) - raise OpenStackCloudException( - "failed to delete security group '{group}': {msg}".format( - group=name_or_id, msg=str(e))) return True elif self.secgroup_source == 'nova': @@ -2571,19 +2521,13 @@ def update_security_group(self, name_or_id, **kwargs): "Security group %s not found." % name_or_id) if self.secgroup_source == 'neutron': - try: + with self._neutron_exceptions( + "Error updating security group {0}".format(name_or_id)): group = self.manager.submitTask( _tasks.NeutronSecurityGroupUpdate( security_group=secgroup['id'], body={'security_group': kwargs}) ) - except Exception as e: - self.log.debug( - "neutron failed to update security group '{group}'".format( - group=name_or_id), exc_info=True) - raise OpenStackCloudException( - "failed to update security group '{group}': {msg}".format( - group=name_or_id, msg=str(e))) return group['security_group'] elif self.secgroup_source == 'nova': @@ -2682,17 +2626,12 @@ def create_security_group_rule(self, 'ethertype': ethertype } - try: + with self._neutron_exceptions( + "Error creating security group rule"): rule = self.manager.submitTask( _tasks.NeutronSecurityGroupRuleCreate( body={'security_group_rule': rule_def}) ) - except Exception as e: - self.log.debug("neutron failed to create security group rule", - exc_info=True) - raise OpenStackCloudException( - "failed to create security group rule: {msg}".format( - msg=str(e))) return rule['security_group_rule'] elif self.secgroup_source == 'nova': @@ -2743,20 +2682,15 @@ def delete_security_group_rule(self, rule_id): if self.secgroup_source == 'neutron': try: - self.manager.submitTask( - _tasks.NeutronSecurityGroupRuleDelete( - security_group_rule=rule_id) - ) - except neutron_exceptions.NotFound: + with self._neutron_exceptions( + "Error deleting security group rule " + "{0}".format(rule_id)): + self.manager.submitTask( + _tasks.NeutronSecurityGroupRuleDelete( + security_group_rule=rule_id) + ) + except OpenStackCloudResourceNotFound: return False - except Exception as e: - self.log.debug( - "neutron failed to delete security group rule {id}".format( - id=rule_id), - exc_info=True) - raise OpenStackCloudException( - "failed to delete security group rule {id}: {msg}".format( - id=rule_id, msg=str(e))) return True elif self.secgroup_source == 'nova': diff --git a/shade/exc.py b/shade/exc.py index 946961d78..997e1caf0 100644 --- a/shade/exc.py +++ b/shade/exc.py @@ -42,3 +42,11 @@ class OpenStackCloudUnavailableExtension(OpenStackCloudException): class OpenStackCloudUnavailableFeature(OpenStackCloudException): pass + + +class OpenStackCloudResourceNotFound(OpenStackCloudException): + pass + + +class OpenStackCloudURINotFound(OpenStackCloudException): + pass diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 3eb61aac7..c4aa22fe5 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -17,6 +17,8 @@ import mock import testtools +from neutronclient.common import exceptions as n_exc + import shade from shade import exc from shade import meta @@ -24,10 +26,25 @@ from shade.tests.unit import base +def mock_me(self): + pass + + +def test_exception(self): + with self._neutron_exceptions("This is actually a test"): + self.mock_me() + + class TestShade(base.TestCase): def setUp(self): super(TestShade, self).setUp() + + # Inject test_exception and mock_me, used to test + # _neutron_exceptions context + shade.OpenStackCloud.mock_me = mock_me + shade.OpenStackCloud.test_exception = test_exception + self.cloud = shade.openstack_cloud() def test_openstack_cloud(self): @@ -825,3 +842,36 @@ def test_has_service_no(self, session_mock): def test_has_service_yes(self, session_mock): session_mock.get_endpoint.return_value = 'http://fake.url' self.assertTrue(self.cloud.has_service("image")) + + @mock.patch.object(shade.OpenStackCloud, 'mock_me') + def test__neutron_exceptions_resource_not_found( + self, mock_mock_me): + mock_mock_me.side_effect = n_exc.NotFound() + + self.assertRaises(exc.OpenStackCloudResourceNotFound, + self.cloud.test_exception) + + @mock.patch.object(shade.OpenStackCloud, 'mock_me') + def test__neutron_exceptions_url_not_found( + self, mock_mock_me): + mock_mock_me.side_effect = n_exc.NeutronClientException( + status_code=404) + + self.assertRaises(exc.OpenStackCloudURINotFound, + self.cloud.test_exception) + + @mock.patch.object(shade.OpenStackCloud, 'mock_me') + def test__neutron_exceptions_neutron_client_generic( + self, mock_mock_me): + mock_mock_me.side_effect = n_exc.NeutronClientException() + + self.assertRaises(exc.OpenStackCloudException, + self.cloud.test_exception) + + @mock.patch.object(shade.OpenStackCloud, 'mock_me') + def test__neutron_exceptions_generic( + self, mock_mock_me): + mock_mock_me.side_effect = Exception() + + self.assertRaises(exc.OpenStackCloudException, + self.cloud.test_exception) From 7cde7c5ad136cda8304e7edb36f8059cd4464360 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 19 Jun 2015 09:25:13 -0400 Subject: [PATCH 0347/3836] Catch all exceptions around port for ip finding If neutron does not work, then it's going to throw inside of list_ports, which is going to send an OpenStackCloudException through search_ports, which is then not going to be caught by the NeutronClientException trap here. Change-Id: Id25ff2688de55f4b39a145185269bc7b7ccb89eb --- shade/meta.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/shade/meta.py b/shade/meta.py index da22b3119..de851cad6 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -17,8 +17,6 @@ import logging import six -from neutronclient.common.exceptions import NeutronClientException - from shade import exc from shade import _utils @@ -80,7 +78,7 @@ def get_server_external_ipv4(cloud, server): server_ports = cloud.search_ports( filters={'device_id': server.id}) ext_nets = cloud.search_networks(filters={'router:external': True}) - except NeutronClientException as e: + except Exception as e: log.debug( "Something went wrong talking to neutron API: " "'{msg}'. Trying with Nova.".format(msg=str(e))) From cb1c0eacb380771ce02f0f156f9f1687022637b7 Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Wed, 13 May 2015 13:02:12 +0100 Subject: [PATCH 0348/3836] Add Neutron/Nova Floating IP list/search/get Some clouds out there are still running nova-network but we want to provide support for neutron specific features and concepts like ports, networks, subnets, etc. This change is part of a set that adds neutron support to existing floating IP-related functions, hiding that behind resource-oriented methods. For instance, at high level, end-users can now request that a public IP is assigned to an instance without worrying about the specific service, procedures and protocols used to provide that feature. Change-Id: I35c3af4b8c43af9988463785cb48c0c41abe6abe --- shade/__init__.py | 54 +++++++- shade/_tasks.py | 8 +- shade/_utils.py | 80 ++++++++++++ shade/tests/fakes.py | 9 ++ shade/tests/unit/test_floating_ip_neutron.py | 115 +++++++++++++++++ shade/tests/unit/test_floating_ip_nova.py | 123 +++++++++++++++++++ 6 files changed, 382 insertions(+), 7 deletions(-) create mode 100644 shade/tests/unit/test_floating_ip_neutron.py create mode 100644 shade/tests/unit/test_floating_ip_nova.py diff --git a/shade/__init__.py b/shade/__init__.py index 9f88dddc9..2e8acdb10 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -869,6 +869,16 @@ def search_floating_ip_pools(self, name=None, filters=None): pools = self.list_floating_ip_pools() return _utils._filter_list(pools, name, filters) + # Note (dguerri): when using Neutron, this can be optimized using + # server-side search. + # There are some cases in which such optimization is not possible (e.g. + # nested attributes or list of objects) so we need to use the client-side + # filtering when we can't do otherwise. + # The same goes for all neutron-related search/get methods! + def search_floating_ips(self, id=None, filters=None): + floating_ips = self.list_floating_ips() + return _utils._filter_list(floating_ips, id, filters) + def list_networks(self): with self._neutron_exceptions("Error fetching network list"): return self.manager.submitTask(_tasks.NetworkList())['networks'] @@ -1021,6 +1031,36 @@ def list_floating_ip_pools(self): "error fetching floating IP pool list: {msg}".format( msg=str(e))) + def list_floating_ips(self): + if self.has_service('network'): + try: + return _utils.normalize_neutron_floating_ips( + self._neutron_list_floating_ips()) + except OpenStackCloudURINotFound as e: + self.log.debug( + "Something went wrong talking to neutron API: " + "'{msg}'. Trying with Nova.".format(msg=str(e))) + # Fall-through, trying with Nova + + floating_ips = self._nova_list_floating_ips() + return _utils.normalize_nova_floating_ips(floating_ips) + + def _neutron_list_floating_ips(self): + with self._neutron_exceptions("error fetching floating IPs list"): + return self.manager.submitTask( + _tasks.NeutronFloatingIPList())['floatingips'] + + def _nova_list_floating_ips(self): + try: + return meta.obj_list_to_dict( + self.manager.submitTask(_tasks.NovaFloatingIPList())) + except Exception as e: + self.log.debug( + "nova could not list floating IPs: {msg}".format( + msg=str(e)), exc_info=True) + raise OpenStackCloudException( + "error fetching floating IPs list: {msg}".format(msg=str(e))) + def get_network(self, name_or_id, filters=None): return _utils._get_entity(self.search_networks, name_or_id, filters) @@ -1049,6 +1089,9 @@ def get_server(self, name_or_id, filters=None): def get_image(self, name_or_id, filters=None): return _utils._get_entity(self.search_images, name_or_id, filters) + def get_floating_ip(self, id, filters=None): + return _utils._get_entity(self.search_floating_ips, id, filters) + # TODO(Shrews): This will eventually need to support tenant ID and # provider networks, which are admin-level params. def create_network(self, name, shared=False, admin_state_up=True): @@ -1664,7 +1707,7 @@ def add_ip_from_pool(self, server, pools): # get the list of all floating IPs. Mileage may # vary according to Nova Compute configuration # per cloud provider - all_floating_ips = self.manager.submitTask(_tasks.FloatingIPList()) + all_floating_ips = self.list_floating_ips() # iterate through all pools of IP address. Empty # string means all and is the default value @@ -1674,8 +1717,9 @@ def add_ip_from_pool(self, server, pools): # loop through all floating IPs for f_ip in all_floating_ips: # if not reserved and the correct pool, add - if f_ip.instance_id is None and (f_ip.pool == pool): - pool_ips.append(f_ip.ip) + if f_ip['fixed_ip_address'] is None and \ + (f_ip['network'] == pool): + pool_ips.append(f_ip['floating_ip_address']) # only need one break @@ -1689,7 +1733,7 @@ def add_ip_from_pool(self, server, pools): "nova floating ip create failed", exc_info=True) raise OpenStackCloudException( "Unable to create floating ip in pool %s" % pool) - pool_ips.append(new_ip.ip) + pool_ips.append(new_ip['floating_ip_address']) # Add to the main list usable_floating_ips[pool] = pool_ips @@ -1713,7 +1757,7 @@ def add_ip_list(self, server, ips): "nova floating ip add failed", exc_info=True) raise OpenStackCloudException( "Error attaching IP {ip} to instance {id}: {msg} ".format( - ip=ip, id=server.id, msg=str(e))) + ip=ip, id=server['id'], msg=str(e))) def add_auto_ip(self, server): try: diff --git a/shade/_tasks.py b/shade/_tasks.py index 2c6ea150a..ab5dff0b1 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -252,8 +252,12 @@ def main(self, client): return client.nova_client.security_group_rules.delete(**self.args) -# TODO: Do this with neutron instead of nova if possible -class FloatingIPList(task_manager.Task): +class NeutronFloatingIPList(task_manager.Task): + def main(self, client): + return client.neutron_client.list_floatingips(**self.args) + + +class NovaFloatingIPList(task_manager.Task): def main(self, client): return client.nova_client.floating_ips.list() diff --git a/shade/_utils.py b/shade/_utils.py index 4261073e7..b7ff2d32e 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -193,3 +193,83 @@ def is_globally_routable_ipv4(ip): return False return True + + +def normalize_nova_floating_ips(ips): + """Normalize the structure of Neutron floating IPs + + Unfortunately, not all the Neutron floating_ip attributes are available + with Nova and not all Nova floating_ip attributes are available with + Neutron. + This function extract attributes that are common to Nova and Neutron + floating IP resource. + If the whole structure is needed inside shade, shade provides private + methods that returns "original" objects (e.g. _nova_allocate_floating_ip) + + :param list ips: A list of Nova floating IPs. + + :returns: + A list of normalized dicts with the following attributes:: + + [ + { + "id": "this-is-a-floating-ip-id", + "fixed_ip_address": "192.0.2.10", + "floating_ip_address": "198.51.100.10", + "network": "this-is-a-net-or-pool-id", + "attached": True, + "status": "ACTIVE" + }, ... + ] + + """ + return [dict( + id=ip['id'], + fixed_ip_address=ip.get('fixed_ip'), + floating_ip_address=ip['ip'], + network=ip['pool'], + attached=(ip.get('instance_id') is not None and + ip.get('instance_id') != ''), + status='ACTIVE' # In neutrons terms, Nova floating IPs are always + # ACTIVE + ) for ip in ips] + + +def normalize_neutron_floating_ips(ips): + """Normalize the structure of Neutron floating IPs + + Unfortunately, not all the Neutron floating_ip attributes are available + with Nova and not all Nova floating_ip attributes are available with + Neutron. + This function extract attributes that are common to Nova and Neutron + floating IP resource. + If the whole structure is needed inside shade, shade provides private + methods that returns "original" objects (e.g. + _neutron_allocate_floating_ip) + + :param list ips: A list of Neutron floating IPs. + + :returns: + A list of normalized dicts with the following attributes:: + + [ + { + "id": "this-is-a-floating-ip-id", + "fixed_ip_address": "192.0.2.10", + "floating_ip_address": "198.51.100.10", + "network": "this-is-a-net-or-pool-id", + "attached": True, + "status": "ACTIVE" + }, ... + ] + + """ + return [dict( + id=ip['id'], + fixed_ip_address=ip.get('fixed_ip_address'), + floating_ip_address=ip['floating_ip_address'], + network=ip['floating_network_id'], + attached=(ip.get('port_id') is not None and + ip.get('port_id') != ''), + status=ip['status'] + ) for ip in ips] diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 56abb4e48..3c60ba4d7 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -37,6 +37,15 @@ def __init__(self, id, name): self.name = name +class FakeFloatingIP(object): + def __init__(self, id, pool, ip, fixed_ip, instance_id): + self.id = id + self.pool = pool + self.ip = ip + self.fixed_ip = fixed_ip + self.instance_id = instance_id + + class FakeFloatingIPPool(object): def __init__(self, id, name): self.id = id diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py new file mode 100644 index 000000000..e907ec570 --- /dev/null +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -0,0 +1,115 @@ +# Copyright (c) 2015 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. + +""" +test_floating_ip_neutron +---------------------------------- + +Tests Floating IP resource methods for Neutron +""" + +from mock import patch +from shade import OpenStackCloud +from shade.tests.unit import base + + +class TestFloatingIP(base.TestCase): + mock_floating_ip_list_rep = { + 'floatingips': [ + { + 'router_id': 'd23abc8d-2991-4a55-ba98-2aaea84cc72f', + 'tenant_id': '4969c491a3c74ee4af974e6d800c62de', + 'floating_network_id': '376da547-b977-4cfe-9cba-275c80debf57', + 'fixed_ip_address': '192.0.2.29', + 'floating_ip_address': '203.0.113.29', + 'port_id': 'ce705c24-c1ef-408a-bda3-7bbd946164ab', + 'id': '2f245a7b-796b-4f26-9cf9-9e82d248fda7', + 'status': 'ACTIVE' + }, + { + 'router_id': None, + 'tenant_id': '4969c491a3c74ee4af974e6d800c62de', + 'floating_network_id': '376da547-b977-4cfe-9cba-275c80debf57', + 'fixed_ip_address': None, + 'floating_ip_address': '203.0.113.30', + 'port_id': None, + 'id': '61cea855-49cb-4846-997d-801b70c71bdd', + 'status': 'DOWN' + } + ] + } + + def assertAreInstances(self, elements, elem_type): + for e in elements: + self.assertIsInstance(e, elem_type) + + def setUp(self): + super(TestFloatingIP, self).setUp() + self.client = OpenStackCloud("cloud", {}) + + @patch.object(OpenStackCloud, 'neutron_client') + @patch.object(OpenStackCloud, 'has_service') + def test_list_floating_ips(self, mock_has_service, mock_neutron_client): + mock_has_service.return_value = True + mock_neutron_client.list_floatingips.return_value = \ + self.mock_floating_ip_list_rep + + floating_ips = self.client.list_floating_ips() + + mock_neutron_client.list_floatingips.assert_called_with() + self.assertIsInstance(floating_ips, list) + self.assertAreInstances(floating_ips, dict) + self.assertEqual(2, len(floating_ips)) + + @patch.object(OpenStackCloud, 'neutron_client') + @patch.object(OpenStackCloud, 'has_service') + def test_search_floating_ips(self, mock_has_service, mock_neutron_client): + mock_has_service.return_value = True + mock_neutron_client.list_floatingips.return_value = \ + self.mock_floating_ip_list_rep + + floating_ips = self.client.search_floating_ips( + filters={'attached': False}) + + mock_neutron_client.list_floatingips.assert_called_with() + self.assertIsInstance(floating_ips, list) + self.assertAreInstances(floating_ips, dict) + self.assertEqual(1, len(floating_ips)) + + @patch.object(OpenStackCloud, 'neutron_client') + @patch.object(OpenStackCloud, 'has_service') + def test_get_floating_ip(self, mock_has_service, mock_neutron_client): + mock_has_service.return_value = True + mock_neutron_client.list_floatingips.return_value = \ + self.mock_floating_ip_list_rep + + floating_ip = self.client.get_floating_ip( + id='2f245a7b-796b-4f26-9cf9-9e82d248fda7') + + mock_neutron_client.list_floatingips.assert_called_with() + self.assertIsInstance(floating_ip, dict) + self.assertEqual('203.0.113.29', floating_ip['floating_ip_address']) + + @patch.object(OpenStackCloud, 'neutron_client') + @patch.object(OpenStackCloud, 'has_service') + def test_get_floating_ip_not_found( + self, mock_has_service, mock_neutron_client): + mock_has_service.return_value = True + mock_neutron_client.list_floatingips.return_value = \ + self.mock_floating_ip_list_rep + + floating_ip = self.client.get_floating_ip( + id='non-existent') + + self.assertIsNone(floating_ip) diff --git a/shade/tests/unit/test_floating_ip_nova.py b/shade/tests/unit/test_floating_ip_nova.py new file mode 100644 index 000000000..9bac9fee5 --- /dev/null +++ b/shade/tests/unit/test_floating_ip_nova.py @@ -0,0 +1,123 @@ +# Copyright (c) 2015 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. + +""" +test_floating_ip_nova +---------------------------------- + +Tests Floating IP resource methods for nova-network +""" + +from mock import patch +from shade import OpenStackCloud +from shade.tests.fakes import FakeFloatingIP +from shade.tests.unit import base + + +def has_service_side_effect(s): + if s == 'network': + return False + return True + + +class TestFloatingIP(base.TestCase): + mock_floating_ip_list_rep = [ + { + 'fixed_ip': None, + 'id': 1, + 'instance_id': None, + 'ip': '203.0.113.1', + 'pool': 'nova' + }, + { + 'fixed_ip': None, + 'id': 2, + 'instance_id': None, + 'ip': '203.0.113.2', + 'pool': 'nova' + }, + { + 'fixed_ip': '192.0.2.3', + 'id': 29, + 'instance_id': 'myself', + 'ip': '198.51.100.29', + 'pool': 'black_hole' + } + ] + + def assertAreInstances(self, elements, elem_type): + for e in elements: + self.assertIsInstance(e, elem_type) + + def setUp(self): + super(TestFloatingIP, self).setUp() + self.client = OpenStackCloud("cloud", {}) + + @patch.object(OpenStackCloud, 'nova_client') + @patch.object(OpenStackCloud, 'has_service') + def test_list_floating_ips(self, mock_has_service, mock_nova_client): + mock_has_service.side_effect = has_service_side_effect + mock_nova_client.floating_ips.list.return_value = [ + FakeFloatingIP(**ip) for ip in self.mock_floating_ip_list_rep + ] + + floating_ips = self.client.list_floating_ips() + + mock_nova_client.floating_ips.list.assert_called_with() + self.assertIsInstance(floating_ips, list) + self.assertEqual(3, len(floating_ips)) + self.assertAreInstances(floating_ips, dict) + + @patch.object(OpenStackCloud, 'nova_client') + @patch.object(OpenStackCloud, 'has_service') + def test_search_floating_ips(self, mock_has_service, mock_nova_client): + mock_has_service.side_effect = has_service_side_effect + mock_nova_client.floating_ips.list.return_value = [ + FakeFloatingIP(**ip) for ip in self.mock_floating_ip_list_rep + ] + + floating_ips = self.client.search_floating_ips( + filters={'attached': False}) + + mock_nova_client.floating_ips.list.assert_called_with() + self.assertIsInstance(floating_ips, list) + self.assertEqual(2, len(floating_ips)) + self.assertAreInstances(floating_ips, dict) + + @patch.object(OpenStackCloud, 'nova_client') + @patch.object(OpenStackCloud, 'has_service') + def test_get_floating_ip(self, mock_has_service, mock_nova_client): + mock_has_service.side_effect = has_service_side_effect + mock_nova_client.floating_ips.list.return_value = [ + FakeFloatingIP(**ip) for ip in self.mock_floating_ip_list_rep + ] + + floating_ip = self.client.get_floating_ip(id='29') + + mock_nova_client.floating_ips.list.assert_called_with() + self.assertIsInstance(floating_ip, dict) + self.assertEqual('198.51.100.29', floating_ip['floating_ip_address']) + + @patch.object(OpenStackCloud, 'nova_client') + @patch.object(OpenStackCloud, 'has_service') + def test_get_floating_ip_not_found( + self, mock_has_service, mock_nova_client): + mock_has_service.side_effect = has_service_side_effect + mock_nova_client.floating_ips.list.return_value = [ + FakeFloatingIP(**ip) for ip in self.mock_floating_ip_list_rep + ] + + floating_ip = self.client.get_floating_ip(id='666') + + self.assertIsNone(floating_ip) From af32bbb474e4de8ba30a247d866502477ead631b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 19 Jun 2015 10:02:47 -0400 Subject: [PATCH 0349/3836] Use accessIPv4 and accessIPv6 if they're there If the cloud has been friendly enough to provide an accessIPv4 or accessIPv6, just use it. Change-Id: Ice62e9cdfe1ce7d1ea8fc0ce84de7febaa4e6c2a --- shade/meta.py | 6 +++++- shade/tests/fakes.py | 6 +++++- shade/tests/unit/test_meta.py | 31 +++++++++++++++++++++---------- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/shade/meta.py b/shade/meta.py index de851cad6..28992793d 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -71,6 +71,8 @@ def get_server_public_ip(server): def get_server_external_ipv4(cloud, server): + if server['accessIPv4']: + return server['accessIPv4'] if cloud.has_service('network'): try: # Search a fixed IP attached to an external net. Unfortunately @@ -128,7 +130,9 @@ def get_server_external_ipv6(server): :param server: the server from which we want to get an IPv6 address :return: a string containing the IPv6 address or None """ - addresses = find_nova_addresses(addresses=server.addresses, version=6) + if server['accessIPv6']: + return server['accessIPv6'] + addresses = find_nova_addresses(addresses=server['addresses'], version=6) if addresses: return addresses[0] return None diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 56abb4e48..ff27096ae 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -56,11 +56,15 @@ def __init__(self, id): class FakeServer(object): - def __init__(self, id, name, status, addresses=None): + def __init__( + self, id, name, status, addresses=None, + accessIPv4='', accessIPv6=''): self.id = id self.name = name self.status = status self.addresses = addresses + self.accessIPv4 = accessIPv4 + self.accessIPv6 = accessIPv6 class FakeService(object): diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 703f7c202..415783b0c 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -59,6 +59,8 @@ class FakeServer(object): 'version': 4}]} flavor = {'id': '101'} image = {'id': '471c2475-da2f-47ac-aba5-cb4aa3d546f5'} + accessIPv4 = '' + accessIPv6 = '' class TestMeta(testtools.TestCase): @@ -122,14 +124,23 @@ def test_get_server_external_ipv4_neutron( }] mock_search_networks.return_value = [{'id': 'test-net-id'}] - srv = fakes.FakeServer( - id='test-id', name='test-name', status='ACTIVE') + srv = meta.obj_to_dict(fakes.FakeServer( + id='test-id', name='test-name', status='ACTIVE')) ip = meta.get_server_external_ipv4( cloud=shade.openstack_cloud(), server=srv) self.assertEqual(PUBLIC_V4, ip) self.assertFalse(mock_get_server_ip.called) + def test_get_server_external_ipv4_neutron_accessIPv4(self): + srv = meta.obj_to_dict(fakes.FakeServer( + id='test-id', name='test-name', status='ACTIVE', + accessIPv4=PUBLIC_V4)) + ip = meta.get_server_external_ipv4( + cloud=shade.openstack_cloud(), server=srv) + + self.assertEqual(PUBLIC_V4, ip) + @mock.patch.object(shade.OpenStackCloud, 'has_service') @mock.patch.object(shade.OpenStackCloud, 'search_ports') @mock.patch.object(meta, 'get_server_ip') @@ -140,8 +151,8 @@ def test_get_server_external_ipv4_neutron_exception( mock_search_ports.side_effect = neutron_exceptions.NotFound() mock_get_server_ip.return_value = PUBLIC_V4 - srv = fakes.FakeServer( - id='test-id', name='test-name', status='ACTIVE') + srv = meta.obj_to_dict(fakes.FakeServer( + id='test-id', name='test-name', status='ACTIVE')) ip = meta.get_server_external_ipv4( cloud=shade.openstack_cloud(), server=srv) @@ -159,9 +170,9 @@ def test_get_server_external_ipv4_nova_public( mock_get_server_ip.return_value = None mock_is_globally_routable_ipv4.return_value = True - srv = fakes.FakeServer( + srv = meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', - addresses={'test-net': [{'addr': PUBLIC_V4}]}) + addresses={'test-net': [{'addr': PUBLIC_V4}]})) ip = meta.get_server_external_ipv4( cloud=shade.openstack_cloud(), server=srv) @@ -180,9 +191,9 @@ def test_get_server_external_ipv4_nova_none( mock_get_server_ip.return_value = None mock_is_globally_routable_ipv4.return_value = False - srv = fakes.FakeServer( + srv = meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', - addresses={'test-net': [{'addr': PRIVATE_V4}]}) + addresses={'test-net': [{'addr': PRIVATE_V4}]})) ip = meta.get_server_external_ipv4( cloud=shade.openstack_cloud(), server=srv) @@ -191,7 +202,7 @@ def test_get_server_external_ipv4_nova_none( self.assertTrue(mock_is_globally_routable_ipv4.called) def test_get_server_external_ipv6(self): - srv = fakes.FakeServer( + srv = meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', addresses={ 'test-net': [ @@ -199,7 +210,7 @@ def test_get_server_external_ipv6(self): {'addr': PUBLIC_V6, 'version': 6} ] } - ) + )) ip = meta.get_server_external_ipv6(srv) self.assertEqual(PUBLIC_V6, ip) From a998ecdb6c2060392d919d578958a693879ddd01 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 19 Jun 2015 10:40:05 -0400 Subject: [PATCH 0350/3836] Add IPv6 to the server information too We have things that like to use it. It would be nice if we could arrange for them to use IPv6 if it's there. Change-Id: I64bba5bece7f00aa600b4108bcf60f6be1d5316d --- shade/meta.py | 15 ++++++++++++--- shade/tests/unit/test_meta.py | 15 ++++++++++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/shade/meta.py b/shade/meta.py index 28992793d..c763b8a53 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -184,9 +184,11 @@ def get_hostvars_from_server(cloud, server, mounts=None): server_vars = server server_vars.pop('links', None) - # Fist, add an IP address - server_vars['public_v4'] = get_server_external_ipv4(cloud, server) - server_vars['private_v4'] = get_server_private_ip(server) + # First, add an IP address. Set it to '' rather than None if it does + # not exist to remain consistent with the pre-existing missing values + server_vars['public_v4'] = get_server_external_ipv4(cloud, server) or '' + server_vars['public_v6'] = get_server_external_ipv6(server) or '' + server_vars['private_v4'] = get_server_private_ip(server) or '' if cloud.private: interface_ip = server_vars['private_v4'] else: @@ -194,6 +196,13 @@ def get_hostvars_from_server(cloud, server, mounts=None): if interface_ip: server_vars['interface_ip'] = interface_ip + # Some clouds do not set these, but they're a regular part of the Nova + # server record. Since we know them, go ahead and set them. In the case + # where they were set previous, we use the values, so this will not break + # clouds that provide the information + server_vars['accessIPv4'] = server_vars['public_v4'] + server_vars['accessIPv6'] = server_vars['public_v6'] + server_vars['region'] = cloud.region_name server_vars['cloud'] = cloud.name diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 415783b0c..13915b784 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -141,6 +141,14 @@ def test_get_server_external_ipv4_neutron_accessIPv4(self): self.assertEqual(PUBLIC_V4, ip) + def test_get_server_external_ipv4_neutron_accessIPv6(self): + srv = meta.obj_to_dict(fakes.FakeServer( + id='test-id', name='test-name', status='ACTIVE', + accessIPv6=PUBLIC_V6)) + ip = meta.get_server_external_ipv6(server=srv) + + self.assertEqual(PUBLIC_V6, ip) + @mock.patch.object(shade.OpenStackCloud, 'has_service') @mock.patch.object(shade.OpenStackCloud, 'search_ports') @mock.patch.object(meta, 'get_server_ip') @@ -244,15 +252,20 @@ class obj1(object): self.assertEqual(new_list[0]['value'], 0) self.assertEqual(new_list[1]['value'], 1) + @mock.patch.object(shade.meta, 'get_server_external_ipv6') @mock.patch.object(shade.meta, 'get_server_external_ipv4') - def test_basic_hostvars(self, mock_get_server_external_ipv4): + def test_basic_hostvars( + self, mock_get_server_external_ipv4, + mock_get_server_external_ipv6): mock_get_server_external_ipv4.return_value = PUBLIC_V4 + mock_get_server_external_ipv6.return_value = PUBLIC_V6 hostvars = meta.get_hostvars_from_server( FakeCloud(), meta.obj_to_dict(FakeServer())) self.assertNotIn('links', hostvars) self.assertEqual(PRIVATE_V4, hostvars['private_v4']) self.assertEqual(PUBLIC_V4, hostvars['public_v4']) + self.assertEqual(PUBLIC_V6, hostvars['public_v6']) self.assertEqual(PUBLIC_V4, hostvars['interface_ip']) self.assertEquals(FakeCloud.region_name, hostvars['region']) self.assertEquals(FakeCloud.name, hostvars['cloud']) From bbcfbc86d299536dc465e32fdba856a74c1e6cce Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 17 Jun 2015 07:55:58 -0400 Subject: [PATCH 0351/3836] Add comment explaining why finding an IP is hard Change-Id: I1bef3397f8e0134079a2fe18a2445c3de0a3ed1d --- shade/meta.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/shade/meta.py b/shade/meta.py index c763b8a53..9d2425f3c 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -71,6 +71,25 @@ def get_server_public_ip(server): def get_server_external_ipv4(cloud, server): + """Find an externally routable IP for the server. + + There are 5 different scenarios we have to account for: + + * Cloud has externally routable IP from neutron but neutron APIs don't + work (only info available is in nova server record) (rackspace) + * Cloud has externally routable IP from neutron (runabove, ovh) + * Cloud has externally routable IP from neutron AND supports optional + private tenant networks (vexxhost, unitedstack) + * Cloud only has private tenant network provided by neutron and requires + floating-ip for external routing (dreamhost, hp) + * Cloud only has private tenant network provided by nova-network and + requires floating-ip for external routing (auro) + + :param cloud: the cloud we're working with + :param server: the server dict from which we want to get an IPv4 address + :return: a string containing the IPv4 address or None + """ + if server['accessIPv4']: return server['accessIPv4'] if cloud.has_service('network'): From b631da86fee360533111eb9993e215d6cb64f522 Mon Sep 17 00:00:00 2001 From: Gregory Haynes Date: Mon, 22 Jun 2015 05:40:40 +0000 Subject: [PATCH 0352/3836] Normalize project_name aliases We arent normalizing keys before we check for project_name aliases, therefore using hyphenated versions of the aliases fail. Change-Id: I3e0aa9dc38bbafc3c3a205f08b65abbd4528e874 --- os_client_config/config.py | 6 ++++-- os_client_config/tests/base.py | 8 ++++++++ os_client_config/tests/test_config.py | 7 +++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index bc8ab1e33..d2bc8166a 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -253,8 +253,10 @@ def _fix_backwards_madness(self, cloud): def _fix_backwards_project(self, cloud): # Do the lists backwards so that project_name is the ultimate winner mappings = { - 'project_name': ('tenant_id', 'project_id', - 'tenant_name', 'project_name'), + 'project_name': ('tenant_id', 'tenant-id', + 'project_id', 'project-id', + 'tenant_name', 'tenant-name', + 'project_name', 'project-name'), } for target_key, possible_values in mappings.items(): target = None diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py index 967f119dd..0a4f456aa 100644 --- a/os_client_config/tests/base.py +++ b/os_client_config/tests/base.py @@ -64,6 +64,14 @@ }, 'region_name': 'test-region', }, + '_test_cloud_hyphenated': { + 'auth': { + 'username': 'testuser', + 'password': 'testpass', + 'project-id': '12345', + }, + 'region_name': 'test-region', + } }, 'cache': {'max_age': 1}, } diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index a6ba2ee83..f23a6b987 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -87,6 +87,12 @@ def test_get_one_cloud_with_int_project_id(self): cc = c.get_one_cloud('_test-cloud-int-project_') self.assertEqual('12345', cc.auth['project_name']) + def test_get_one_cloud_with_hyphenated_project_id(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + cc = c.get_one_cloud('_test_cloud_hyphenated') + self.assertEqual('12345', cc.auth['project_name']) + def test_no_environ(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) @@ -112,6 +118,7 @@ def test_get_cloud_names(self): self.assertEqual( ['_test-cloud-int-project_', '_test-cloud_', + '_test_cloud_hyphenated', '_test_cloud_no_vendor', ], sorted(c.get_cloud_names())) From 29790338f5c0fa14ae1c3b0c87f31dd1e1fc284b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 22 Jun 2015 09:42:24 -0400 Subject: [PATCH 0353/3836] Pin cinderclient cinderclient 1.2.x is broken for all public clouds tested. What's worse is that the break exhibits as hanging until timeout because of errors in version autonegotiation. pin so that people aren't confused and angry. Change-Id: Ia3896f3ac38ded6225447bd4cbd03a086e741ad0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index dc2950d2c..a5e0143f4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ six python-novaclient>=2.21.0 python-keystoneclient>=0.11.0 python-glanceclient -python-cinderclient +python-cinderclient<1.2 python-neutronclient>=2.3.10 python-troveclient python-ironicclient>=0.5.1 From 9db26ef1e2c7a33ad01eaf2a9cd27ff1ed56be97 Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Sat, 20 Jun 2015 23:23:48 +0100 Subject: [PATCH 0354/3836] Fix intermittent error in unit tests After the introduction of unit tests for the _neutron_exceptions context, unit tests fail randomly with the following error: AttributeError: does not have the attribute 'mock_me' This error is due to the wrong location of test__neutron_excetpions tests (i.e. the TestShadeOperator class rather than the TestShade class). Moreover, as pointed out on IRC we can get rid of "fake" methods to test the _neutron_exception context, just using the existing code. Change-Id: I2cfdcfce606272fbdbdf8a463db895d3d5a4a1e3 --- shade/tests/unit/test_shade.py | 76 +++++++++++++--------------------- 1 file changed, 28 insertions(+), 48 deletions(-) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index c4aa22fe5..5d6bb512c 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -26,25 +26,10 @@ from shade.tests.unit import base -def mock_me(self): - pass - - -def test_exception(self): - with self._neutron_exceptions("This is actually a test"): - self.mock_me() - - class TestShade(base.TestCase): def setUp(self): super(TestShade, self).setUp() - - # Inject test_exception and mock_me, used to test - # _neutron_exceptions context - shade.OpenStackCloud.mock_me = mock_me - shade.OpenStackCloud.test_exception = test_exception - self.cloud = shade.openstack_cloud() def test_openstack_cloud(self): @@ -297,6 +282,34 @@ class Flavor1(object): flavor2 = self.cloud.get_flavor(1) self.assertEquals(vanilla, flavor2) + def test__neutron_exceptions_resource_not_found(self): + with mock.patch.object( + shade._tasks, 'NetworkList', + side_effect=n_exc.NotFound()): + self.assertRaises(exc.OpenStackCloudResourceNotFound, + self.cloud.list_networks) + + def test__neutron_exceptions_url_not_found(self): + with mock.patch.object( + shade._tasks, 'NetworkList', + side_effect=n_exc.NeutronClientException(status_code=404)): + self.assertRaises(exc.OpenStackCloudURINotFound, + self.cloud.list_networks) + + def test__neutron_exceptions_neutron_client_generic(self): + with mock.patch.object( + shade._tasks, 'NetworkList', + side_effect=n_exc.NeutronClientException()): + self.assertRaises(exc.OpenStackCloudException, + self.cloud.list_networks) + + def test__neutron_exceptions_generic(self): + with mock.patch.object( + shade._tasks, 'NetworkList', + side_effect=Exception()): + self.assertRaises(exc.OpenStackCloudException, + self.cloud.list_networks) + class TestShadeOperator(base.TestCase): @@ -842,36 +855,3 @@ def test_has_service_no(self, session_mock): def test_has_service_yes(self, session_mock): session_mock.get_endpoint.return_value = 'http://fake.url' self.assertTrue(self.cloud.has_service("image")) - - @mock.patch.object(shade.OpenStackCloud, 'mock_me') - def test__neutron_exceptions_resource_not_found( - self, mock_mock_me): - mock_mock_me.side_effect = n_exc.NotFound() - - self.assertRaises(exc.OpenStackCloudResourceNotFound, - self.cloud.test_exception) - - @mock.patch.object(shade.OpenStackCloud, 'mock_me') - def test__neutron_exceptions_url_not_found( - self, mock_mock_me): - mock_mock_me.side_effect = n_exc.NeutronClientException( - status_code=404) - - self.assertRaises(exc.OpenStackCloudURINotFound, - self.cloud.test_exception) - - @mock.patch.object(shade.OpenStackCloud, 'mock_me') - def test__neutron_exceptions_neutron_client_generic( - self, mock_mock_me): - mock_mock_me.side_effect = n_exc.NeutronClientException() - - self.assertRaises(exc.OpenStackCloudException, - self.cloud.test_exception) - - @mock.patch.object(shade.OpenStackCloud, 'mock_me') - def test__neutron_exceptions_generic( - self, mock_mock_me): - mock_mock_me.side_effect = Exception() - - self.assertRaises(exc.OpenStackCloudException, - self.cloud.test_exception) From fdefa583ae80a62f8b03edb904fd2bdbe406159c Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Mon, 22 Jun 2015 15:48:41 +0100 Subject: [PATCH 0355/3836] Move TestShadeOperator in a separate file This patch separate tests for OpenStackCloud and OperatorCloud classes. Change-Id: I503c31cb6f15258572d85b8b82da03bee67b77f1 --- shade/tests/unit/test_shade.py | 550 ----------------------- shade/tests/unit/test_shade_operator.py | 570 ++++++++++++++++++++++++ 2 files changed, 570 insertions(+), 550 deletions(-) create mode 100644 shade/tests/unit/test_shade_operator.py diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 5d6bb512c..17b23fa93 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -12,17 +12,13 @@ # License for the specific language governing permissions and limitations # under the License. -from keystoneclient import auth as ksc_auth - import mock -import testtools from neutronclient.common import exceptions as n_exc import shade from shade import exc from shade import meta -from shade.tests import fakes from shade.tests.unit import base @@ -309,549 +305,3 @@ def test__neutron_exceptions_generic(self): side_effect=Exception()): self.assertRaises(exc.OpenStackCloudException, self.cloud.list_networks) - - -class TestShadeOperator(base.TestCase): - - def setUp(self): - super(TestShadeOperator, self).setUp() - self.cloud = shade.operator_cloud() - - def test_operator_cloud(self): - self.assertIsInstance(self.cloud, shade.OperatorCloud) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_get_machine_by_mac(self, mock_client): - class port_value: - node_uuid = '00000000-0000-0000-0000-000000000000' - address = '00:00:00:00:00:00' - - class node_value: - uuid = '00000000-0000-0000-0000-000000000000' - - expected_value = dict( - uuid='00000000-0000-0000-0000-000000000000') - - mock_client.port.get_by_address.return_value = port_value - mock_client.node.get.return_value = node_value - machine = self.cloud.get_machine_by_mac('00:00:00:00:00:00') - mock_client.port.get_by_address.assert_called_with( - address='00:00:00:00:00:00') - mock_client.node.get.assert_called_with( - '00000000-0000-0000-0000-000000000000') - self.assertEqual(machine, expected_value) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_list_machines(self, mock_client): - m1 = fakes.FakeMachine(1, 'fake_machine1') - mock_client.node.list.return_value = [m1] - machines = self.cloud.list_machines() - self.assertTrue(mock_client.node.list.called) - self.assertEqual(meta.obj_to_dict(m1), machines[0]) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_validate_node(self, mock_client): - node_uuid = '123' - self.cloud.validate_node(node_uuid) - mock_client.node.validate.assert_called_once_with( - node_uuid=node_uuid - ) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_list_nics(self, mock_client): - port1 = fakes.FakeMachinePort(1, "aa:bb:cc:dd", "node1") - port2 = fakes.FakeMachinePort(2, "dd:cc:bb:aa", "node2") - port_list = [port1, port2] - port_dict_list = meta.obj_list_to_dict(port_list) - - mock_client.port.list.return_value = port_list - nics = self.cloud.list_nics() - - self.assertTrue(mock_client.port.list.called) - self.assertEqual(port_dict_list, nics) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_list_nics_failure(self, mock_client): - mock_client.port.list.side_effect = Exception() - self.assertRaises(exc.OpenStackCloudException, - self.cloud.list_nics) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_list_nics_for_machine(self, mock_client): - mock_client.node.list_ports.return_value = [] - self.cloud.list_nics_for_machine("123") - mock_client.node.list_ports.assert_called_with(node_id="123") - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_list_nics_for_machine_failure(self, mock_client): - mock_client.node.list_ports.side_effect = Exception() - self.assertRaises(exc.OpenStackCloudException, - self.cloud.list_nics_for_machine, None) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_patch_machine(self, mock_client): - node_id = 'node01' - patch = [] - patch.append({'op': 'remove', 'path': '/instance_info'}) - self.cloud.patch_machine(node_id, patch) - self.assertTrue(mock_client.node.update.called) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - @mock.patch.object(shade.OperatorCloud, 'patch_machine') - def test_update_machine_patch_no_action(self, mock_patch, mock_client): - class client_return_value: - uuid = '00000000-0000-0000-0000-000000000000' - name = 'node01' - - expected_machine = dict( - uuid='00000000-0000-0000-0000-000000000000', - name='node01' - ) - mock_client.node.get.return_value = client_return_value - - update_dict = self.cloud.update_machine('node01') - self.assertIsNone(update_dict['changes']) - self.assertFalse(mock_patch.called) - self.assertDictEqual(expected_machine, update_dict['node']) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - @mock.patch.object(shade.OperatorCloud, 'patch_machine') - def test_update_machine_patch_no_action_name(self, mock_patch, - mock_client): - class client_return_value: - uuid = '00000000-0000-0000-0000-000000000000' - name = 'node01' - - expected_machine = dict( - uuid='00000000-0000-0000-0000-000000000000', - name='node01' - ) - mock_client.node.get.return_value = client_return_value - - update_dict = self.cloud.update_machine('node01', name='node01') - self.assertIsNone(update_dict['changes']) - self.assertFalse(mock_patch.called) - self.assertDictEqual(expected_machine, update_dict['node']) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - @mock.patch.object(shade.OperatorCloud, 'patch_machine') - def test_update_machine_patch_action_name(self, mock_patch, - mock_client): - class client_return_value: - uuid = '00000000-0000-0000-0000-000000000000' - name = 'evil' - - expected_patch = [dict(op='replace', path='/name', value='good')] - - mock_client.node.get.return_value = client_return_value - - update_dict = self.cloud.update_machine('evil', name='good') - self.assertIsNotNone(update_dict['changes']) - self.assertEqual('/name', update_dict['changes'][0]) - self.assertTrue(mock_patch.called) - mock_patch.assert_called_with( - '00000000-0000-0000-0000-000000000000', - expected_patch) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - @mock.patch.object(shade.OperatorCloud, 'patch_machine') - def test_update_machine_patch_update_name(self, mock_patch, - mock_client): - class client_return_value: - uuid = '00000000-0000-0000-0000-000000000000' - name = 'evil' - - expected_patch = [dict(op='replace', path='/name', value='good')] - - mock_client.node.get.return_value = client_return_value - - update_dict = self.cloud.update_machine('evil', name='good') - self.assertIsNotNone(update_dict['changes']) - self.assertEqual('/name', update_dict['changes'][0]) - self.assertTrue(mock_patch.called) - mock_patch.assert_called_with( - '00000000-0000-0000-0000-000000000000', - expected_patch) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - @mock.patch.object(shade.OperatorCloud, 'patch_machine') - def test_update_machine_patch_update_chassis_uuid(self, mock_patch, - mock_client): - class client_return_value: - uuid = '00000000-0000-0000-0000-000000000000' - chassis_uuid = None - - expected_patch = [ - dict( - op='replace', - path='/chassis_uuid', - value='00000000-0000-0000-0000-000000000001' - )] - - mock_client.node.get.return_value = client_return_value - - update_dict = self.cloud.update_machine( - '00000000-0000-0000-0000-000000000000', - chassis_uuid='00000000-0000-0000-0000-000000000001') - self.assertIsNotNone(update_dict['changes']) - self.assertEqual('/chassis_uuid', update_dict['changes'][0]) - self.assertTrue(mock_patch.called) - mock_patch.assert_called_with( - '00000000-0000-0000-0000-000000000000', - expected_patch) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - @mock.patch.object(shade.OperatorCloud, 'patch_machine') - def test_update_machine_patch_update_driver(self, mock_patch, - mock_client): - class client_return_value: - uuid = '00000000-0000-0000-0000-000000000000' - driver = None - - expected_patch = [ - dict( - op='replace', - path='/driver', - value='fake' - )] - - mock_client.node.get.return_value = client_return_value - - update_dict = self.cloud.update_machine( - '00000000-0000-0000-0000-000000000000', - driver='fake' - ) - self.assertIsNotNone(update_dict['changes']) - self.assertEqual('/driver', update_dict['changes'][0]) - self.assertTrue(mock_patch.called) - mock_patch.assert_called_with( - '00000000-0000-0000-0000-000000000000', - expected_patch) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - @mock.patch.object(shade.OperatorCloud, 'patch_machine') - def test_update_machine_patch_update_driver_info(self, mock_patch, - mock_client): - class client_return_value: - uuid = '00000000-0000-0000-0000-000000000000' - driver_info = None - - expected_patch = [ - dict( - op='replace', - path='/driver_info', - value=dict(var='fake') - )] - - mock_client.node.get.return_value = client_return_value - - update_dict = self.cloud.update_machine( - '00000000-0000-0000-0000-000000000000', - driver_info=dict(var="fake") - ) - self.assertIsNotNone(update_dict['changes']) - self.assertEqual('/driver_info', update_dict['changes'][0]) - self.assertTrue(mock_patch.called) - mock_patch.assert_called_with( - '00000000-0000-0000-0000-000000000000', - expected_patch) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - @mock.patch.object(shade.OperatorCloud, 'patch_machine') - def test_update_machine_patch_update_instance_info(self, mock_patch, - mock_client): - class client_return_value: - uuid = '00000000-0000-0000-0000-000000000000' - instance_info = None - - expected_patch = [ - dict( - op='replace', - path='/instance_info', - value=dict(var='fake') - )] - - mock_client.node.get.return_value = client_return_value - - update_dict = self.cloud.update_machine( - '00000000-0000-0000-0000-000000000000', - instance_info=dict(var="fake") - ) - self.assertIsNotNone(update_dict['changes']) - self.assertEqual('/instance_info', update_dict['changes'][0]) - self.assertTrue(mock_patch.called) - mock_patch.assert_called_with( - '00000000-0000-0000-0000-000000000000', - expected_patch) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - @mock.patch.object(shade.OperatorCloud, 'patch_machine') - def test_update_machine_patch_update_instance_uuid(self, mock_patch, - mock_client): - class client_return_value: - uuid = '00000000-0000-0000-0000-000000000000' - instance_uuid = None - - expected_patch = [ - dict( - op='replace', - path='/instance_uuid', - value='00000000-0000-0000-0000-000000000002' - )] - - mock_client.node.get.return_value = client_return_value - - update_dict = self.cloud.update_machine( - '00000000-0000-0000-0000-000000000000', - instance_uuid='00000000-0000-0000-0000-000000000002' - ) - self.assertIsNotNone(update_dict['changes']) - self.assertEqual('/instance_uuid', update_dict['changes'][0]) - self.assertTrue(mock_patch.called) - mock_patch.assert_called_with( - '00000000-0000-0000-0000-000000000000', - expected_patch) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - @mock.patch.object(shade.OperatorCloud, 'patch_machine') - def test_update_machine_patch_update_properties(self, mock_patch, - mock_client): - class client_return_value: - uuid = '00000000-0000-0000-0000-000000000000' - properties = None - - expected_patch = [ - dict( - op='replace', - path='/properties', - value=dict(var='fake') - )] - - mock_client.node.get.return_value = client_return_value - - update_dict = self.cloud.update_machine( - '00000000-0000-0000-0000-000000000000', - properties=dict(var="fake") - ) - self.assertIsNotNone(update_dict['changes']) - self.assertEqual('/properties', update_dict['changes'][0]) - self.assertTrue(mock_patch.called) - mock_patch.assert_called_with( - '00000000-0000-0000-0000-000000000000', - expected_patch) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_register_machine(self, mock_client): - class fake_node: - uuid = "00000000-0000-0000-0000-000000000000" - - expected_return_value = dict( - uuid="00000000-0000-0000-0000-000000000000", - ) - mock_client.node.create.return_value = fake_node - nics = [{'mac': '00:00:00:00:00:00'}] - return_value = self.cloud.register_machine(nics) - self.assertDictEqual(expected_return_value, return_value) - self.assertTrue(mock_client.node.create.called) - self.assertTrue(mock_client.port.create.called) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_register_machine_port_create_failed(self, mock_client): - nics = [{'mac': '00:00:00:00:00:00'}] - mock_client.port.create.side_effect = ( - exc.OpenStackCloudException("Error")) - self.assertRaises(exc.OpenStackCloudException, - self.cloud.register_machine, - nics) - self.assertTrue(mock_client.node.create.called) - self.assertTrue(mock_client.port.create.called) - self.assertTrue(mock_client.node.delete.called) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_unregister_machine(self, mock_client): - nics = [{'mac': '00:00:00:00:00:00'}] - uuid = "00000000-0000-0000-0000-000000000000" - self.cloud.unregister_machine(nics, uuid) - self.assertTrue(mock_client.node.delete.called) - self.assertTrue(mock_client.port.delete.called) - self.assertTrue(mock_client.port.get_by_address.called) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_set_machine_maintenace_state(self, mock_client): - mock_client.node.set_maintenance.return_value = None - node_id = 'node01' - reason = 'no reason' - self.cloud.set_machine_maintenance_state(node_id, True, reason=reason) - mock_client.node.set_maintenance.assert_called_with( - node_id='node01', - state='true', - maint_reason='no reason') - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_set_machine_maintenace_state_false(self, mock_client): - mock_client.node.set_maintenance.return_value = None - node_id = 'node01' - self.cloud.set_machine_maintenance_state(node_id, False) - mock_client.node.set_maintenance.assert_called_with( - node_id='node01', - state='false') - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_remove_machine_from_maintenance(self, mock_client): - mock_client.node.set_maintenance.return_value = None - node_id = 'node01' - self.cloud.remove_machine_from_maintenance(node_id) - mock_client.node.set_maintenance.assert_called_with( - node_id='node01', - state='false') - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_set_machine_power_on(self, mock_client): - mock_client.node.set_power_state.return_value = None - node_id = 'node01' - return_value = self.cloud.set_machine_power_on(node_id) - self.assertEqual(None, return_value) - mock_client.node.set_power_state.assert_called_with( - node_id='node01', - state='on') - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_set_machine_power_off(self, mock_client): - mock_client.node.set_power_state.return_value = None - node_id = 'node01' - return_value = self.cloud.set_machine_power_off(node_id) - self.assertEqual(None, return_value) - mock_client.node.set_power_state.assert_called_with( - node_id='node01', - state='off') - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_set_machine_power_reboot(self, mock_client): - mock_client.node.set_power_state.return_value = None - node_id = 'node01' - return_value = self.cloud.set_machine_power_reboot(node_id) - self.assertEqual(None, return_value) - mock_client.node.set_power_state.assert_called_with( - node_id='node01', - state='reboot') - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_set_machine_power_reboot_failure(self, mock_client): - mock_client.node.set_power_state.return_value = 'failure' - self.assertRaises(shade.OpenStackCloudException, - self.cloud.set_machine_power_reboot, - 'node01') - mock_client.node.set_power_state.assert_called_with( - node_id='node01', - state='reboot') - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_node_set_provision_state(self, mock_client): - mock_client.node.set_provision_state.return_value = None - node_id = 'node01' - return_value = self.cloud.node_set_provision_state( - node_id, - 'active', - configdrive='http://127.0.0.1/file.iso') - self.assertEqual({}, return_value) - mock_client.node.set_provision_state.assert_called_with( - node_uuid='node01', - state='active', - configdrive='http://127.0.0.1/file.iso') - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_activate_node(self, mock_client): - mock_client.node.set_provision_state.return_value = None - node_id = 'node02' - return_value = self.cloud.activate_node( - node_id, - configdrive='http://127.0.0.1/file.iso') - self.assertEqual(None, return_value) - mock_client.node.set_provision_state.assert_called_with( - node_uuid='node02', - state='active', - configdrive='http://127.0.0.1/file.iso') - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_deactivate_node(self, mock_client): - mock_client.node.set_provision_state.return_value = None - node_id = 'node03' - return_value = self.cloud.deactivate_node( - node_id) - self.assertEqual(None, return_value) - mock_client.node.set_provision_state.assert_called_with( - node_uuid='node03', - state='deleted', - configdrive=None) - - @mock.patch.object(shade.OpenStackCloud, 'glance_client') - def test_get_image_name(self, glance_mock): - - class Image(object): - id = '22' - name = '22 name' - status = 'success' - fake_image = Image() - glance_mock.images.list.return_value = [fake_image] - self.assertEqual('22 name', self.cloud.get_image_name('22')) - self.assertEqual('22 name', self.cloud.get_image_name('22 name')) - - @mock.patch.object(shade.OpenStackCloud, 'glance_client') - def test_get_image_id(self, glance_mock): - - class Image(object): - id = '22' - name = '22 name' - status = 'success' - fake_image = Image() - glance_mock.images.list.return_value = [fake_image] - self.assertEqual('22', self.cloud.get_image_id('22')) - self.assertEqual('22', self.cloud.get_image_id('22 name')) - - def test_get_session_endpoint_provided(self): - self.cloud.endpoints['image'] = 'http://fake.url' - self.assertEqual( - 'http://fake.url', self.cloud.get_session_endpoint('image')) - - @mock.patch.object(shade.OpenStackCloud, 'keystone_session') - def test_get_session_endpoint_session(self, session_mock): - session_mock.get_endpoint.return_value = 'http://fake.url' - self.assertEqual( - 'http://fake.url', self.cloud.get_session_endpoint('image')) - - @mock.patch.object(shade.OpenStackCloud, 'keystone_session') - def test_get_session_endpoint_exception(self, session_mock): - class FakeException(Exception): - pass - - def side_effect(*args, **kwargs): - raise FakeException("No service") - session_mock.get_endpoint.side_effect = side_effect - with testtools.ExpectedException( - exc.OpenStackCloudException, - "Error getting image endpoint: No service"): - self.cloud.get_session_endpoint("image") - - @mock.patch.object(shade.OpenStackCloud, 'keystone_session') - def test_get_session_endpoint_unavailable(self, session_mock): - session_mock.get_endpoint.return_value = None - with testtools.ExpectedException( - exc.OpenStackCloudUnavailableService, - "Cloud.*does not have a image service"): - self.cloud.get_session_endpoint("image") - - @mock.patch.object(shade.OpenStackCloud, 'keystone_session') - def test_get_session_endpoint_identity(self, session_mock): - self.cloud.get_session_endpoint('identity') - session_mock.get_endpoint.assert_called_with( - interface=ksc_auth.AUTH_INTERFACE) - - @mock.patch.object(shade.OpenStackCloud, 'keystone_session') - def test_has_service_no(self, session_mock): - session_mock.get_endpoint.return_value = None - self.assertFalse(self.cloud.has_service("image")) - - @mock.patch.object(shade.OpenStackCloud, 'keystone_session') - def test_has_service_yes(self, session_mock): - session_mock.get_endpoint.return_value = 'http://fake.url' - self.assertTrue(self.cloud.has_service("image")) diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py new file mode 100644 index 000000000..32c04f3e2 --- /dev/null +++ b/shade/tests/unit/test_shade_operator.py @@ -0,0 +1,570 @@ +# -*- 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. + +from keystoneclient import auth as ksc_auth + +import mock +import testtools + +import shade +from shade import exc +from shade import meta +from shade.tests import fakes +from shade.tests.unit import base + + +class TestShadeOperator(base.TestCase): + + def setUp(self): + super(TestShadeOperator, self).setUp() + self.cloud = shade.operator_cloud() + + def test_operator_cloud(self): + self.assertIsInstance(self.cloud, shade.OperatorCloud) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_get_machine_by_mac(self, mock_client): + class port_value: + node_uuid = '00000000-0000-0000-0000-000000000000' + address = '00:00:00:00:00:00' + + class node_value: + uuid = '00000000-0000-0000-0000-000000000000' + + expected_value = dict( + uuid='00000000-0000-0000-0000-000000000000') + + mock_client.port.get_by_address.return_value = port_value + mock_client.node.get.return_value = node_value + machine = self.cloud.get_machine_by_mac('00:00:00:00:00:00') + mock_client.port.get_by_address.assert_called_with( + address='00:00:00:00:00:00') + mock_client.node.get.assert_called_with( + '00000000-0000-0000-0000-000000000000') + self.assertEqual(machine, expected_value) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_list_machines(self, mock_client): + m1 = fakes.FakeMachine(1, 'fake_machine1') + mock_client.node.list.return_value = [m1] + machines = self.cloud.list_machines() + self.assertTrue(mock_client.node.list.called) + self.assertEqual(meta.obj_to_dict(m1), machines[0]) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_validate_node(self, mock_client): + node_uuid = '123' + self.cloud.validate_node(node_uuid) + mock_client.node.validate.assert_called_once_with( + node_uuid=node_uuid + ) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_list_nics(self, mock_client): + port1 = fakes.FakeMachinePort(1, "aa:bb:cc:dd", "node1") + port2 = fakes.FakeMachinePort(2, "dd:cc:bb:aa", "node2") + port_list = [port1, port2] + port_dict_list = meta.obj_list_to_dict(port_list) + + mock_client.port.list.return_value = port_list + nics = self.cloud.list_nics() + + self.assertTrue(mock_client.port.list.called) + self.assertEqual(port_dict_list, nics) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_list_nics_failure(self, mock_client): + mock_client.port.list.side_effect = Exception() + self.assertRaises(exc.OpenStackCloudException, + self.cloud.list_nics) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_list_nics_for_machine(self, mock_client): + mock_client.node.list_ports.return_value = [] + self.cloud.list_nics_for_machine("123") + mock_client.node.list_ports.assert_called_with(node_id="123") + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_list_nics_for_machine_failure(self, mock_client): + mock_client.node.list_ports.side_effect = Exception() + self.assertRaises(exc.OpenStackCloudException, + self.cloud.list_nics_for_machine, None) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_patch_machine(self, mock_client): + node_id = 'node01' + patch = [] + patch.append({'op': 'remove', 'path': '/instance_info'}) + self.cloud.patch_machine(node_id, patch) + self.assertTrue(mock_client.node.update.called) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_no_action(self, mock_patch, mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + name = 'node01' + + expected_machine = dict( + uuid='00000000-0000-0000-0000-000000000000', + name='node01' + ) + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine('node01') + self.assertIsNone(update_dict['changes']) + self.assertFalse(mock_patch.called) + self.assertDictEqual(expected_machine, update_dict['node']) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_no_action_name(self, mock_patch, + mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + name = 'node01' + + expected_machine = dict( + uuid='00000000-0000-0000-0000-000000000000', + name='node01' + ) + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine('node01', name='node01') + self.assertIsNone(update_dict['changes']) + self.assertFalse(mock_patch.called) + self.assertDictEqual(expected_machine, update_dict['node']) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_action_name(self, mock_patch, + mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + name = 'evil' + + expected_patch = [dict(op='replace', path='/name', value='good')] + + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine('evil', name='good') + self.assertIsNotNone(update_dict['changes']) + self.assertEqual('/name', update_dict['changes'][0]) + self.assertTrue(mock_patch.called) + mock_patch.assert_called_with( + '00000000-0000-0000-0000-000000000000', + expected_patch) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_update_name(self, mock_patch, + mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + name = 'evil' + + expected_patch = [dict(op='replace', path='/name', value='good')] + + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine('evil', name='good') + self.assertIsNotNone(update_dict['changes']) + self.assertEqual('/name', update_dict['changes'][0]) + self.assertTrue(mock_patch.called) + mock_patch.assert_called_with( + '00000000-0000-0000-0000-000000000000', + expected_patch) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_update_chassis_uuid(self, mock_patch, + mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + chassis_uuid = None + + expected_patch = [ + dict( + op='replace', + path='/chassis_uuid', + value='00000000-0000-0000-0000-000000000001' + )] + + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine( + '00000000-0000-0000-0000-000000000000', + chassis_uuid='00000000-0000-0000-0000-000000000001') + self.assertIsNotNone(update_dict['changes']) + self.assertEqual('/chassis_uuid', update_dict['changes'][0]) + self.assertTrue(mock_patch.called) + mock_patch.assert_called_with( + '00000000-0000-0000-0000-000000000000', + expected_patch) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_update_driver(self, mock_patch, + mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + driver = None + + expected_patch = [ + dict( + op='replace', + path='/driver', + value='fake' + )] + + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine( + '00000000-0000-0000-0000-000000000000', + driver='fake' + ) + self.assertIsNotNone(update_dict['changes']) + self.assertEqual('/driver', update_dict['changes'][0]) + self.assertTrue(mock_patch.called) + mock_patch.assert_called_with( + '00000000-0000-0000-0000-000000000000', + expected_patch) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_update_driver_info(self, mock_patch, + mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + driver_info = None + + expected_patch = [ + dict( + op='replace', + path='/driver_info', + value=dict(var='fake') + )] + + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine( + '00000000-0000-0000-0000-000000000000', + driver_info=dict(var="fake") + ) + self.assertIsNotNone(update_dict['changes']) + self.assertEqual('/driver_info', update_dict['changes'][0]) + self.assertTrue(mock_patch.called) + mock_patch.assert_called_with( + '00000000-0000-0000-0000-000000000000', + expected_patch) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_update_instance_info(self, mock_patch, + mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + instance_info = None + + expected_patch = [ + dict( + op='replace', + path='/instance_info', + value=dict(var='fake') + )] + + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine( + '00000000-0000-0000-0000-000000000000', + instance_info=dict(var="fake") + ) + self.assertIsNotNone(update_dict['changes']) + self.assertEqual('/instance_info', update_dict['changes'][0]) + self.assertTrue(mock_patch.called) + mock_patch.assert_called_with( + '00000000-0000-0000-0000-000000000000', + expected_patch) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_update_instance_uuid(self, mock_patch, + mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + instance_uuid = None + + expected_patch = [ + dict( + op='replace', + path='/instance_uuid', + value='00000000-0000-0000-0000-000000000002' + )] + + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine( + '00000000-0000-0000-0000-000000000000', + instance_uuid='00000000-0000-0000-0000-000000000002' + ) + self.assertIsNotNone(update_dict['changes']) + self.assertEqual('/instance_uuid', update_dict['changes'][0]) + self.assertTrue(mock_patch.called) + mock_patch.assert_called_with( + '00000000-0000-0000-0000-000000000000', + expected_patch) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_update_properties(self, mock_patch, + mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + properties = None + + expected_patch = [ + dict( + op='replace', + path='/properties', + value=dict(var='fake') + )] + + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine( + '00000000-0000-0000-0000-000000000000', + properties=dict(var="fake") + ) + self.assertIsNotNone(update_dict['changes']) + self.assertEqual('/properties', update_dict['changes'][0]) + self.assertTrue(mock_patch.called) + mock_patch.assert_called_with( + '00000000-0000-0000-0000-000000000000', + expected_patch) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_register_machine(self, mock_client): + class fake_node: + uuid = "00000000-0000-0000-0000-000000000000" + + expected_return_value = dict( + uuid="00000000-0000-0000-0000-000000000000", + ) + mock_client.node.create.return_value = fake_node + nics = [{'mac': '00:00:00:00:00:00'}] + return_value = self.cloud.register_machine(nics) + self.assertDictEqual(expected_return_value, return_value) + self.assertTrue(mock_client.node.create.called) + self.assertTrue(mock_client.port.create.called) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_register_machine_port_create_failed(self, mock_client): + nics = [{'mac': '00:00:00:00:00:00'}] + mock_client.port.create.side_effect = ( + exc.OpenStackCloudException("Error")) + self.assertRaises(exc.OpenStackCloudException, + self.cloud.register_machine, + nics) + self.assertTrue(mock_client.node.create.called) + self.assertTrue(mock_client.port.create.called) + self.assertTrue(mock_client.node.delete.called) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_unregister_machine(self, mock_client): + nics = [{'mac': '00:00:00:00:00:00'}] + uuid = "00000000-0000-0000-0000-000000000000" + self.cloud.unregister_machine(nics, uuid) + self.assertTrue(mock_client.node.delete.called) + self.assertTrue(mock_client.port.delete.called) + self.assertTrue(mock_client.port.get_by_address.called) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_set_machine_maintenace_state(self, mock_client): + mock_client.node.set_maintenance.return_value = None + node_id = 'node01' + reason = 'no reason' + self.cloud.set_machine_maintenance_state(node_id, True, reason=reason) + mock_client.node.set_maintenance.assert_called_with( + node_id='node01', + state='true', + maint_reason='no reason') + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_set_machine_maintenace_state_false(self, mock_client): + mock_client.node.set_maintenance.return_value = None + node_id = 'node01' + self.cloud.set_machine_maintenance_state(node_id, False) + mock_client.node.set_maintenance.assert_called_with( + node_id='node01', + state='false') + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_remove_machine_from_maintenance(self, mock_client): + mock_client.node.set_maintenance.return_value = None + node_id = 'node01' + self.cloud.remove_machine_from_maintenance(node_id) + mock_client.node.set_maintenance.assert_called_with( + node_id='node01', + state='false') + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_set_machine_power_on(self, mock_client): + mock_client.node.set_power_state.return_value = None + node_id = 'node01' + return_value = self.cloud.set_machine_power_on(node_id) + self.assertEqual(None, return_value) + mock_client.node.set_power_state.assert_called_with( + node_id='node01', + state='on') + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_set_machine_power_off(self, mock_client): + mock_client.node.set_power_state.return_value = None + node_id = 'node01' + return_value = self.cloud.set_machine_power_off(node_id) + self.assertEqual(None, return_value) + mock_client.node.set_power_state.assert_called_with( + node_id='node01', + state='off') + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_set_machine_power_reboot(self, mock_client): + mock_client.node.set_power_state.return_value = None + node_id = 'node01' + return_value = self.cloud.set_machine_power_reboot(node_id) + self.assertEqual(None, return_value) + mock_client.node.set_power_state.assert_called_with( + node_id='node01', + state='reboot') + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_set_machine_power_reboot_failure(self, mock_client): + mock_client.node.set_power_state.return_value = 'failure' + self.assertRaises(shade.OpenStackCloudException, + self.cloud.set_machine_power_reboot, + 'node01') + mock_client.node.set_power_state.assert_called_with( + node_id='node01', + state='reboot') + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_node_set_provision_state(self, mock_client): + mock_client.node.set_provision_state.return_value = None + node_id = 'node01' + return_value = self.cloud.node_set_provision_state( + node_id, + 'active', + configdrive='http://127.0.0.1/file.iso') + self.assertEqual({}, return_value) + mock_client.node.set_provision_state.assert_called_with( + node_uuid='node01', + state='active', + configdrive='http://127.0.0.1/file.iso') + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_activate_node(self, mock_client): + mock_client.node.set_provision_state.return_value = None + node_id = 'node02' + return_value = self.cloud.activate_node( + node_id, + configdrive='http://127.0.0.1/file.iso') + self.assertEqual(None, return_value) + mock_client.node.set_provision_state.assert_called_with( + node_uuid='node02', + state='active', + configdrive='http://127.0.0.1/file.iso') + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_deactivate_node(self, mock_client): + mock_client.node.set_provision_state.return_value = None + node_id = 'node03' + return_value = self.cloud.deactivate_node( + node_id) + self.assertEqual(None, return_value) + mock_client.node.set_provision_state.assert_called_with( + node_uuid='node03', + state='deleted', + configdrive=None) + + @mock.patch.object(shade.OpenStackCloud, 'glance_client') + def test_get_image_name(self, glance_mock): + + class Image(object): + id = '22' + name = '22 name' + status = 'success' + fake_image = Image() + glance_mock.images.list.return_value = [fake_image] + self.assertEqual('22 name', self.cloud.get_image_name('22')) + self.assertEqual('22 name', self.cloud.get_image_name('22 name')) + + @mock.patch.object(shade.OpenStackCloud, 'glance_client') + def test_get_image_id(self, glance_mock): + + class Image(object): + id = '22' + name = '22 name' + status = 'success' + fake_image = Image() + glance_mock.images.list.return_value = [fake_image] + self.assertEqual('22', self.cloud.get_image_id('22')) + self.assertEqual('22', self.cloud.get_image_id('22 name')) + + def test_get_session_endpoint_provided(self): + self.cloud.endpoints['image'] = 'http://fake.url' + self.assertEqual( + 'http://fake.url', self.cloud.get_session_endpoint('image')) + + @mock.patch.object(shade.OpenStackCloud, 'keystone_session') + def test_get_session_endpoint_session(self, session_mock): + session_mock.get_endpoint.return_value = 'http://fake.url' + self.assertEqual( + 'http://fake.url', self.cloud.get_session_endpoint('image')) + + @mock.patch.object(shade.OpenStackCloud, 'keystone_session') + def test_get_session_endpoint_exception(self, session_mock): + class FakeException(Exception): + pass + + def side_effect(*args, **kwargs): + raise FakeException("No service") + session_mock.get_endpoint.side_effect = side_effect + with testtools.ExpectedException( + exc.OpenStackCloudException, + "Error getting image endpoint: No service"): + self.cloud.get_session_endpoint("image") + + @mock.patch.object(shade.OpenStackCloud, 'keystone_session') + def test_get_session_endpoint_unavailable(self, session_mock): + session_mock.get_endpoint.return_value = None + with testtools.ExpectedException( + exc.OpenStackCloudUnavailableService, + "Cloud.*does not have a image service"): + self.cloud.get_session_endpoint("image") + + @mock.patch.object(shade.OpenStackCloud, 'keystone_session') + def test_get_session_endpoint_identity(self, session_mock): + self.cloud.get_session_endpoint('identity') + session_mock.get_endpoint.assert_called_with( + interface=ksc_auth.AUTH_INTERFACE) + + @mock.patch.object(shade.OpenStackCloud, 'keystone_session') + def test_has_service_no(self, session_mock): + session_mock.get_endpoint.return_value = None + self.assertFalse(self.cloud.has_service("image")) + + @mock.patch.object(shade.OpenStackCloud, 'keystone_session') + def test_has_service_yes(self, session_mock): + session_mock.get_endpoint.return_value = 'http://fake.url' + self.assertTrue(self.cloud.has_service("image")) From 5369ffc3dce5cb5789fb654da2829f2de793e21a Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 19 Jun 2015 09:22:12 -0400 Subject: [PATCH 0356/3836] Convert ironicclient node.get() call to Task Changes the node.get() calls to Task calls. Also adds missing test case. Change-Id: I94b2c2bdb4c7c8bb7b96d3aa0e0117ec3585e620 --- shade/__init__.py | 9 +++++++-- shade/_tasks.py | 5 +++++ shade/tests/unit/test_shade_operator.py | 11 ++++++++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 2e8acdb10..f1136ce95 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2866,7 +2866,10 @@ def get_machine(self, name_or_id): are found. """ try: - return meta.obj_to_dict(self.ironic_client.node.get(name_or_id)) + return meta.obj_to_dict( + self.manager.submitTask( + _tasks.MachineNodeGet(node_id=name_or_id)) + ) except ironic_exceptions.ClientException: return None @@ -2882,7 +2885,9 @@ def get_machine_by_mac(self, mac): port = self.manager.submitTask( _tasks.MachinePortGetByAddress(address=mac)) return meta.obj_to_dict( - self.ironic_client.node.get(port.node_uuid)) + self.manager.submitTask( + _tasks.MachineNodeGet(node_id=port.node_uuid)) + ) except ironic_exceptions.ClientException: return None diff --git a/shade/_tasks.py b/shade/_tasks.py index ab5dff0b1..348a6d7c9 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -407,6 +407,11 @@ def main(self, client): return client.ironic_client.port.list() +class MachineNodeGet(task_manager.Task): + def main(self, client): + return client.ironic_client.node.get(**self.args) + + class MachineNodeList(task_manager.Task): def main(self, client): return client.ironic_client.node.list(**self.args) diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index 32c04f3e2..c021229e8 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -33,6 +33,15 @@ def setUp(self): def test_operator_cloud(self): self.assertIsInstance(self.cloud, shade.OperatorCloud) + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_get_machine(self, mock_client): + node = fakes.FakeMachine(id='00000000-0000-0000-0000-000000000000', + name='bigOlFaker') + mock_client.node.get.return_value = node + machine = self.cloud.get_machine('bigOlFaker') + mock_client.node.get.assert_called_with(node_id='bigOlFaker') + self.assertEqual(meta.obj_to_dict(node), machine) + @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_get_machine_by_mac(self, mock_client): class port_value: @@ -51,7 +60,7 @@ class node_value: mock_client.port.get_by_address.assert_called_with( address='00:00:00:00:00:00') mock_client.node.get.assert_called_with( - '00000000-0000-0000-0000-000000000000') + node_id='00000000-0000-0000-0000-000000000000') self.assertEqual(machine, expected_value) @mock.patch.object(shade.OperatorCloud, 'ironic_client') From 735aae777ede16deec7cc206d1d4e9ebdb5b7564 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 19 Jun 2015 09:47:23 -0400 Subject: [PATCH 0357/3836] Convert ironicclient node.update() call to Task Changes the node.update() calls to Task calls. Also adds missing test cases. Change-Id: Iaa97aef01b14c348b8cf9045b928a972151b0614 --- shade/__init__.py | 8 ++++++-- shade/_tasks.py | 5 +++++ shade/tests/unit/test_shade_operator.py | 18 ++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index f1136ce95..b391158b6 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -3382,7 +3382,9 @@ def deactivate_node(self, uuid): def set_node_instance_info(self, uuid, patch): try: return meta.obj_to_dict( - self.ironic_client.node.update(uuid, patch)) + self.manager.submitTask( + _tasks.MachineNodeUpdate(node_id=uuid, patch=patch)) + ) except Exception as e: self.log.debug( "Failed to update instance_info", exc_info=True) @@ -3393,7 +3395,9 @@ def purge_node_instance_info(self, uuid): patch.append({'op': 'remove', 'path': '/instance_info'}) try: return meta.obj_to_dict( - self.ironic_client.node.update(uuid, patch)) + self.manager.submitTask( + _tasks.MachineNodeUpdate(node_id=uuid, patch=patch)) + ) except Exception as e: self.log.debug( "Failed to delete instance_info", exc_info=True) diff --git a/shade/_tasks.py b/shade/_tasks.py index 348a6d7c9..5fbb5d650 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -422,6 +422,11 @@ def main(self, client): return client.ironic_client.node.list_ports(**self.args) +class MachineNodeUpdate(task_manager.Task): + def main(self, client): + return client.ironic_client.node.update(**self.args) + + class MachineNodeValidate(task_manager.Task): def main(self, client): return client.ironic_client.node.validate(**self.args) diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index c021229e8..48159b8cc 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -506,6 +506,24 @@ def test_deactivate_node(self, mock_client): state='deleted', configdrive=None) + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_set_node_instance_info(self, mock_client): + uuid = 'aaa' + patch = [{'op': 'add', 'foo': 'bar'}] + self.cloud.set_node_instance_info(uuid, patch) + mock_client.node.update.assert_called_with( + node_id=uuid, patch=patch + ) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_purge_node_instance_info(self, mock_client): + uuid = 'aaa' + expected_patch = [{'op': 'remove', 'path': '/instance_info'}] + self.cloud.purge_node_instance_info(uuid) + mock_client.node.update.assert_called_with( + node_id=uuid, patch=expected_patch + ) + @mock.patch.object(shade.OpenStackCloud, 'glance_client') def test_get_image_name(self, glance_mock): From 73c07744132ea0244181b8faef3e4df592476889 Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Fri, 15 May 2015 15:59:27 +0100 Subject: [PATCH 0358/3836] Add Neutron/Nova Floating IP create (i.e. allocate to project) Some clouds out there are still running nova-network but we want to provide support for neutron specific features and concepts like ports, networks, subnets, etc. This change is part of a set that adds neutron support to existing floating IP-related functions, hiding that behind resource-oriented methods. For instance, at high level, end-users can now request that a public IP is assigned to an instance without worrying about the specific service, procedures and protocols used to provide that feature in the target cloud. Change-Id: I2196ebf9b0131346675f90ca9fb21c3baf8d177f --- shade/__init__.py | 198 +++++++++++++++++-- shade/_tasks.py | 7 +- shade/tests/unit/test_floating_ip_neutron.py | 86 ++++++++ shade/tests/unit/test_floating_ip_nova.py | 46 +++++ 4 files changed, 319 insertions(+), 18 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index b391158b6..21e0b95d8 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1699,6 +1699,182 @@ def get_server_meta(self, server): def get_openstack_vars(self, server): return meta.get_hostvars_from_server(self, server) + def available_floating_ip(self, network=None): + """Get a floating IP from a network or a pool. + + Return the first available floating IP or allocate a new one. + + :param network: Nova pool name or Neutron network name or id. + + :returns: a (normalized) structure with a floating IP address + description. + """ + if self.has_service('network'): + try: + f_ips = _utils.normalize_neutron_floating_ips( + self._neutron_available_floating_ips( + network=network)) + return f_ips[0] + except OpenStackCloudURINotFound as e: + self.log.debug( + "Something went wrong talking to neutron API: " + "'{msg}'. Trying with Nova.".format(msg=str(e))) + # Fall-through, trying with Nova + + f_ips = _utils.normalize_nova_floating_ips( + self._nova_available_floating_ips(pool=network) + ) + return f_ips[0] + + def _neutron_available_floating_ips(self, network=None): + """Get a floating IP from a Neutron network. + + Return a list of available floating IPs or allocate a new one and + return it in a list of 1 element. + + :param network: Nova pool name or Neutron network name or id. + + :returns: a list of floating IP addresses. + + :raises: ``OpenStackCloudResourceNotFound``, if an external network + that meets the specified criteria cannot be found. + """ + with self._neutron_exceptions("unable to get available floating IPs"): + networks = self.search_networks( + name_or_id=network, + filters={'router:external': True}) + if not networks: + raise OpenStackCloudResourceNotFound( + "unable to find an external network") + + filters = { + 'port_id': None, + 'floating_network_id': networks[0]['id'] + } + floating_ips = self._neutron_list_floating_ips() + available_ips = _utils._filter_list( + floating_ips, name_or_id=None, filters=filters) + if available_ips: + return available_ips + + # No available IP found, allocate a new Floating IP + f_ip = self._neutron_create_floating_ip( + network_name_or_id=networks[0]['id']) + + return [f_ip] + + def _nova_available_floating_ips(self, pool=None): + """Get available floating IPs from a floating IP pool. + + Return a list of available floating IPs or allocate a new one and + return it in a list of 1 element. + + :param pool: Nova floating IP pool name. + + :returns: a list of floating IP addresses. + + :raises: ``OpenStackCloudResourceNotFound``, if a floating IP pool + is not specified and cannot be found. + """ + + try: + if pool is None: + pools = self.list_floating_ip_pools() + if not pools: + raise OpenStackCloudResourceNotFound( + "unable to find a floating ip pool") + pool = pools[0]['name'] + + filters = { + 'instance_id': None, + 'pool': pool + } + + floating_ips = self._nova_list_floating_ips() + available_ips = _utils._filter_list( + floating_ips, name_or_id=None, filters=filters) + if available_ips: + return available_ips + + # No available IP found, allocate a new Floating IP + pools = self.search_floating_ip_pools(name=pool) + f_ip = self._nova_create_floating_ip(pool=pools[0]['name']) + + return [meta.obj_to_dict(f_ip)] + + except Exception as e: + self.log.debug( + "nova floating IP create failed: {msg}".format( + msg=str(e)), exc_info=True) + raise OpenStackCloudException( + "unable to create floating IP in pool {pool}: {msg}".format( + pool=pool, msg=str(e))) + + def create_floating_ip(self, network=None): + """Allocate a new floating IP from a network or a pool. + + :param network: Nova pool name or Neutron network name or id. + + :returns: a floating IP address + + :raises: ``OpenStackCloudException``, on operation error. + """ + if self.has_service('network'): + try: + f_ips = _utils.normalize_neutron_floating_ips( + [self._neutron_create_floating_ip( + network_name_or_id=network)] + ) + return f_ips[0] + except OpenStackCloudURINotFound as e: + self.log.debug( + "Something went wrong talking to neutron API: " + "'{msg}'. Trying with Nova.".format(msg=str(e))) + # Fall-through, trying with Nova + + # Else, we are using Nova network + f_ips = _utils.normalize_nova_floating_ips( + [self._nova_create_floating_ip(pool=network)]) + return f_ips[0] + + def _neutron_create_floating_ip(self, network_name_or_id=None): + with self._neutron_exceptions( + "unable to create floating IP for net " + "{0}".format(network_name_or_id)): + networks = self.search_networks( + name_or_id=network_name_or_id, + filters={'router:external': True}) + if not networks: + raise OpenStackCloudResourceNotFound( + "unable to find an external network with id " + "{0}".format(network_name_or_id or "(no id specified)")) + kwargs = { + 'floating_network_id': networks[0]['id'], + } + return self.manager.submitTask(_tasks.NeutronFloatingIPCreate( + body={'floatingip': kwargs}))['floatingip'] + + def _nova_create_floating_ip(self, pool=None): + try: + if pool is None: + pools = self.list_floating_ip_pools() + if not pools: + raise OpenStackCloudResourceNotFound( + "unable to find a floating ip pool") + pool = pools[0]['name'] + + pool_ip = self.manager.submitTask( + _tasks.NovaFloatingIPCreate(pool=pool)) + return meta.obj_to_dict(pool_ip) + + except Exception as e: + self.log.debug( + "nova floating IP create failed: {msg}".format( + msg=str(e)), exc_info=True) + raise OpenStackCloudException( + "unable to create floating IP in pool {pool}: {msg}".format( + pool=pool, msg=str(e))) + def add_ip_from_pool(self, server, pools): # empty dict and list @@ -1725,14 +1901,7 @@ def add_ip_from_pool(self, server, pools): # if the list is empty, add for this pool if not pool_ips: - try: - new_ip = self.manager.submitTask( - _tasks.FloatingIPCreate(pool=pool)) - except Exception: - self.log.debug( - "nova floating ip create failed", exc_info=True) - raise OpenStackCloudException( - "Unable to create floating ip in pool %s" % pool) + new_ip = self.create_floating_ip(network=pool) pool_ips.append(new_ip['floating_ip_address']) # Add to the main list usable_floating_ips[pool] = pool_ips @@ -1760,20 +1929,15 @@ def add_ip_list(self, server, ips): ip=ip, id=server['id'], msg=str(e))) def add_auto_ip(self, server): + new_ip = self.create_floating_ip() + try: - new_ip = self.manager.submitTask(_tasks.FloatingIPCreate()) - except Exception as e: - self.log.debug( - "nova floating ip create failed", exc_info=True) - raise OpenStackCloudException( - "Unable to create floating ip: %s" % (str(e))) - try: - self.add_ip_list(server, [new_ip]) + self.add_ip_list(server, [new_ip['floating_ip_address']]) except OpenStackCloudException: # Clean up - we auto-created this ip, and it's not attached # to the server, so the cloud will not know what to do with it self.manager.submitTask( - _tasks.FloatingIPDelete(floating_ip=new_ip)) + _tasks.FloatingIPDelete(floating_ip=new_ip['id'])) raise def add_ips_to_server(self, server, auto_ip=True, ips=None, ip_pool=None): diff --git a/shade/_tasks.py b/shade/_tasks.py index 5fbb5d650..d5741f583 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -262,7 +262,12 @@ def main(self, client): return client.nova_client.floating_ips.list() -class FloatingIPCreate(task_manager.Task): +class NeutronFloatingIPCreate(task_manager.Task): + def main(self, client): + return client.neutron_client.create_floatingip(**self.args) + + +class NovaFloatingIPCreate(task_manager.Task): def main(self, client): return client.nova_client.floating_ips.create(**self.args) diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index e907ec570..549a2e75b 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -50,12 +50,42 @@ class TestFloatingIP(base.TestCase): ] } + mock_floating_ip_new_rep = { + 'floatingip': { + 'fixed_ip_address': '10.0.0.4', + 'floating_ip_address': '172.24.4.229', + 'floating_network_id': 'my-network-id', + 'id': '2f245a7b-796b-4f26-9cf9-9e82d248fda8', + 'port_id': None, + 'router_id': None, + 'status': 'ACTIVE', + 'tenant_id': '4969c491a3c74ee4af974e6d800c62df' + } + } + + mock_get_network_rep = { + 'status': 'ACTIVE', + 'subnets': [ + '54d6f61d-db07-451c-9ab3-b9609b6b6f0b' + ], + 'name': 'my-network', + 'provider:physical_network': None, + 'admin_state_up': True, + 'tenant_id': '4fd44f30292945e481c7b8a0c8908869', + 'provider:network_type': 'local', + 'router:external': True, + 'shared': True, + 'id': 'my-network-id', + 'provider:segmentation_id': None + } + def assertAreInstances(self, elements, elem_type): for e in elements: self.assertIsInstance(e, elem_type) def setUp(self): super(TestFloatingIP, self).setUp() + # floating_ip_source='neutron' is default for OpenStackCloud() self.client = OpenStackCloud("cloud", {}) @patch.object(OpenStackCloud, 'neutron_client') @@ -113,3 +143,59 @@ def test_get_floating_ip_not_found( id='non-existent') self.assertIsNone(floating_ip) + + @patch.object(OpenStackCloud, 'neutron_client') + @patch.object(OpenStackCloud, 'search_networks') + @patch.object(OpenStackCloud, 'has_service') + def test_create_floating_ip( + self, mock_has_service, mock_search_networks, mock_neutron_client): + mock_has_service.return_value = True + mock_search_networks.return_value = [self.mock_get_network_rep] + mock_neutron_client.create_floatingip.return_value = \ + self.mock_floating_ip_new_rep + + ip = self.client.create_floating_ip(network='my-network') + + mock_neutron_client.create_floatingip.assert_called_with( + body={'floatingip': {'floating_network_id': 'my-network-id'}} + ) + self.assertEqual( + self.mock_floating_ip_new_rep['floatingip']['floating_ip_address'], + ip['floating_ip_address']) + + @patch.object(OpenStackCloud, '_neutron_list_floating_ips') + @patch.object(OpenStackCloud, 'search_networks') + @patch.object(OpenStackCloud, 'has_service') + def test_available_floating_ip_existing( + self, mock_has_service, mock_search_networks, + mock__neutron_list_floating_ips): + mock_has_service.return_value = True + mock_search_networks.return_value = [self.mock_get_network_rep] + mock__neutron_list_floating_ips.return_value = \ + [self.mock_floating_ip_new_rep['floatingip']] + + ip = self.client.available_floating_ip(network='my-network') + + self.assertEqual( + self.mock_floating_ip_new_rep['floatingip']['floating_ip_address'], + ip['floating_ip_address']) + + @patch.object(OpenStackCloud, '_neutron_create_floating_ip') + @patch.object(OpenStackCloud, '_neutron_list_floating_ips') + @patch.object(OpenStackCloud, 'search_networks') + @patch.object(OpenStackCloud, 'has_service') + def test_available_floating_ip_new( + self, mock_has_service, mock_search_networks, + mock__neutron_list_floating_ips, + mock__neutron_create_floating_ip): + mock_has_service.return_value = True + mock_search_networks.return_value = [self.mock_get_network_rep] + mock__neutron_list_floating_ips.return_value = [] + mock__neutron_create_floating_ip.return_value = \ + self.mock_floating_ip_new_rep['floatingip'] + + ip = self.client.available_floating_ip(network='my-network') + + self.assertEqual( + self.mock_floating_ip_new_rep['floatingip']['floating_ip_address'], + ip['floating_ip_address']) diff --git a/shade/tests/unit/test_floating_ip_nova.py b/shade/tests/unit/test_floating_ip_nova.py index 9bac9fee5..b049a9d9a 100644 --- a/shade/tests/unit/test_floating_ip_nova.py +++ b/shade/tests/unit/test_floating_ip_nova.py @@ -56,6 +56,10 @@ class TestFloatingIP(base.TestCase): } ] + mock_floating_ip_pools = [ + {'id': 'pool1_id', 'name': 'nova'}, + {'id': 'pool2_id', 'name': 'pool2'}] + def assertAreInstances(self, elements, elem_type): for e in elements: self.assertIsInstance(e, elem_type) @@ -121,3 +125,45 @@ def test_get_floating_ip_not_found( floating_ip = self.client.get_floating_ip(id='666') self.assertIsNone(floating_ip) + + @patch.object(OpenStackCloud, 'nova_client') + @patch.object(OpenStackCloud, 'has_service') + def test_create_floating_ip(self, mock_has_service, mock_nova_client): + mock_has_service.side_effect = has_service_side_effect + mock_nova_client.floating_ips.create.return_value = FakeFloatingIP( + **self.mock_floating_ip_list_rep[1]) + + self.client.create_floating_ip(network='nova') + + mock_nova_client.floating_ips.create.assert_called_with(pool='nova') + + @patch.object(OpenStackCloud, '_nova_list_floating_ips') + @patch.object(OpenStackCloud, 'has_service') + def test_available_floating_ip_existing( + self, mock_has_service, mock__nova_list_floating_ips): + mock_has_service.side_effect = has_service_side_effect + mock__nova_list_floating_ips.return_value = \ + self.mock_floating_ip_list_rep[:1] + + ip = self.client.available_floating_ip(network='nova') + + self.assertEqual(self.mock_floating_ip_list_rep[0]['ip'], + ip['floating_ip_address']) + + @patch.object(OpenStackCloud, '_nova_create_floating_ip') + @patch.object(OpenStackCloud, 'list_floating_ip_pools') + @patch.object(OpenStackCloud, '_nova_list_floating_ips') + @patch.object(OpenStackCloud, 'has_service') + def test_available_floating_ip_new( + self, mock_has_service, mock__nova_list_floating_ips, + mock_list_floating_ip_pools, mock__nova_create_floating_ip): + mock_has_service.side_effect = has_service_side_effect + mock__nova_list_floating_ips.return_value = [] + mock_list_floating_ip_pools.return_value = self.mock_floating_ip_pools + mock__nova_create_floating_ip.return_value = \ + FakeFloatingIP(**self.mock_floating_ip_list_rep[0]) + + ip = self.client.available_floating_ip(network='nova') + + self.assertEqual(self.mock_floating_ip_list_rep[0]['ip'], + ip['floating_ip_address']) From f9c72e91a6284df3e6e520a968b554f29fd54cc9 Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Fri, 15 May 2015 19:15:51 +0100 Subject: [PATCH 0359/3836] Add Neutron/Nova Floating IP delete (i.e. deallocate from project) Some clouds out there are still running nova-network but we want to provide support for neutron specific features and concepts like ports, networks, subnets, etc. This change is part of a set that adds neutron support to existing floating IP-related functions, hiding that behind resource-oriented methods. For instance, at high level, end-users can now request that a public IP is assigned to an instance without worrying about the specific service, procedures and protocols used to provide that feature in the target cloud. Change-Id: I699d65ef3f9d917e14b4046d5ee4e6763e2ecc74 --- shade/__init__.py | 51 +++++++++++++++++++- shade/_tasks.py | 7 ++- shade/tests/unit/test_floating_ip_neutron.py | 31 ++++++++++++ shade/tests/unit/test_floating_ip_nova.py | 30 ++++++++++++ 4 files changed, 116 insertions(+), 3 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 21e0b95d8..0bcc52ea2 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1875,6 +1875,54 @@ def _nova_create_floating_ip(self, pool=None): "unable to create floating IP in pool {pool}: {msg}".format( pool=pool, msg=str(e))) + def delete_floating_ip(self, floating_ip_id): + """Deallocate a floating IP from a tenant. + + :param floating_ip_id: a floating IP address id. + + :returns: True if the IP address has been deleted, False if the IP + address was not found. + + :raises: ``OpenStackCloudException``, on operation error. + """ + if self.has_service('network'): + try: + return self._neutron_delete_floating_ip(floating_ip_id) + except OpenStackCloudURINotFound as e: + self.log.debug( + "Something went wrong talking to neutron API: " + "'{msg}'. Trying with Nova.".format(msg=str(e))) + # Fall-through, trying with Nova + + # Else, we are using Nova network + return self._nova_delete_floating_ip(floating_ip_id) + + def _neutron_delete_floating_ip(self, floating_ip_id): + try: + with self._neutron_exceptions("unable to delete floating IP"): + self.manager.submitTask( + _tasks.NeutronFloatingIPDelete(floatingip=floating_ip_id)) + except OpenStackCloudResourceNotFound: + return False + + return True + + def _nova_delete_floating_ip(self, floating_ip_id): + try: + self.manager.submitTask( + _tasks.NovaFloatingIPDelete(floating_ip=floating_ip_id)) + except nova_exceptions.NotFound: + return False + except Exception as e: + self.log.debug( + "nova floating IP delete failed: {msg}".format( + msg=str(e)), exc_info=True) + raise OpenStackCloudException( + "unable to delete floating IP id {fip_id}: {msg}".format( + fip_id=floating_ip_id, msg=str(e))) + + return True + def add_ip_from_pool(self, server, pools): # empty dict and list @@ -1936,8 +1984,7 @@ def add_auto_ip(self, server): except OpenStackCloudException: # Clean up - we auto-created this ip, and it's not attached # to the server, so the cloud will not know what to do with it - self.manager.submitTask( - _tasks.FloatingIPDelete(floating_ip=new_ip['id'])) + self.delete_floating_ip(floating_ip_id=new_ip['id']) raise def add_ips_to_server(self, server, auto_ip=True, ips=None, ip_pool=None): diff --git a/shade/_tasks.py b/shade/_tasks.py index d5741f583..a41409ee9 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -272,7 +272,12 @@ def main(self, client): return client.nova_client.floating_ips.create(**self.args) -class FloatingIPDelete(task_manager.Task): +class NeutronFloatingIPDelete(task_manager.Task): + def main(self, client): + return client.neutron_client.delete_floatingip(**self.args) + + +class NovaFloatingIPDelete(task_manager.Task): def main(self, client): return client.nova_client.floating_ips.delete(**self.args) diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 549a2e75b..702df8620 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -20,6 +20,9 @@ """ from mock import patch + +from neutronclient.common import exceptions as n_exc + from shade import OpenStackCloud from shade.tests.unit import base @@ -199,3 +202,31 @@ def test_available_floating_ip_new( self.assertEqual( self.mock_floating_ip_new_rep['floatingip']['floating_ip_address'], ip['floating_ip_address']) + + @patch.object(OpenStackCloud, 'neutron_client') + @patch.object(OpenStackCloud, 'has_service') + def test_delete_floating_ip_existing( + self, mock_has_service, mock_neutron_client): + mock_has_service.return_value = True + mock_neutron_client.delete_floatingip.return_value = None + + ret = self.client.delete_floating_ip( + floating_ip_id='2f245a7b-796b-4f26-9cf9-9e82d248fda7') + + mock_neutron_client.delete_floatingip.assert_called_with( + floatingip='2f245a7b-796b-4f26-9cf9-9e82d248fda7' + ) + self.assertTrue(ret) + + @patch.object(OpenStackCloud, 'neutron_client') + @patch.object(OpenStackCloud, 'has_service') + def test_delete_floating_ip_not_found( + self, mock_has_service, mock_neutron_client): + mock_has_service.return_value = True + mock_neutron_client.delete_floatingip.side_effect = \ + n_exc.NotFound() + + ret = self.client.delete_floating_ip( + floating_ip_id='a-wild-id-appears') + + self.assertFalse(ret) diff --git a/shade/tests/unit/test_floating_ip_nova.py b/shade/tests/unit/test_floating_ip_nova.py index b049a9d9a..5e68ccf8d 100644 --- a/shade/tests/unit/test_floating_ip_nova.py +++ b/shade/tests/unit/test_floating_ip_nova.py @@ -20,6 +20,9 @@ """ from mock import patch + +from novaclient import exceptions as n_exc + from shade import OpenStackCloud from shade.tests.fakes import FakeFloatingIP from shade.tests.unit import base @@ -167,3 +170,30 @@ def test_available_floating_ip_new( self.assertEqual(self.mock_floating_ip_list_rep[0]['ip'], ip['floating_ip_address']) + + @patch.object(OpenStackCloud, 'nova_client') + @patch.object(OpenStackCloud, 'has_service') + def test_delete_floating_ip_existing( + self, mock_has_service, mock_nova_client): + mock_has_service.side_effect = has_service_side_effect + mock_nova_client.floating_ips.delete.return_value = None + + ret = self.client.delete_floating_ip( + floating_ip_id='a-wild-id-appears') + + mock_nova_client.floating_ips.delete.assert_called_with( + floating_ip='a-wild-id-appears') + self.assertTrue(ret) + + @patch.object(OpenStackCloud, 'nova_client') + @patch.object(OpenStackCloud, 'has_service') + def test_delete_floating_ip_not_found( + self, mock_has_service, mock_nova_client): + mock_has_service.side_effect = has_service_side_effect + mock_nova_client.floating_ips.delete.side_effect = n_exc.NotFound( + code=404) + + ret = self.client.delete_floating_ip( + floating_ip_id='a-wild-id-appears') + + self.assertFalse(ret) From 6f93b3a253128191f35064f3d5e2a9d10086a99c Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 24 Jun 2015 17:07:58 -0400 Subject: [PATCH 0360/3836] Update keypair APIs to latest standards This brings the keypair API interface up to our current standards: - Stop leaking keypair objects. - Adheres to the new get/list/search interface. - Catches client exceptions and rethrows as OpenStackCloudException. - Returns True/False in the delete API method. Change-Id: I1d762898fc03765fc028943c2f792605b1ebcd1c --- shade/__init__.py | 67 ++++++++++++++++++++++++----- shade/tests/fakes.py | 7 ++++ shade/tests/unit/test_keypair.py | 72 ++++++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 10 deletions(-) create mode 100644 shade/tests/unit/test_keypair.py diff --git a/shade/__init__.py b/shade/__init__.py index 21e0b95d8..beda97ac7 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -793,20 +793,10 @@ def list_server_dicts(self): return [self.get_openstack_vars(server) for server in self.list_servers()] - def list_keypairs(self): - return self.manager.submitTask(_tasks.KeypairList()) - def list_keypair_dicts(self): return [meta.obj_to_dict(keypair) for keypair in self.list_keypairs()] - def create_keypair(self, name, public_key): - return self.manager.submitTask(_tasks.KeypairCreate( - name=name, public_key=public_key)) - - def delete_keypair(self, name): - return self.manager.submitTask(_tasks.KeypairDelete(key=name)) - @_cache_on_arguments() def _nova_extensions(self): extensions = set() @@ -829,6 +819,10 @@ def _nova_extensions(self): def _has_nova_extension(self, extension_name): return extension_name in self._nova_extensions() + def search_keypairs(self, name_or_id=None, filters=None): + keypairs = self.list_keypairs() + return _utils._filter_list(keypairs, name_or_id, filters) + def search_networks(self, name_or_id=None, filters=None): networks = self.list_networks() return _utils._filter_list(networks, name_or_id, filters) @@ -879,6 +873,16 @@ def search_floating_ips(self, id=None, filters=None): floating_ips = self.list_floating_ips() return _utils._filter_list(floating_ips, id, filters) + def list_keypairs(self): + try: + return meta.obj_list_to_dict( + self.manager.submitTask(_tasks.KeypairList()) + ) + except Exception as e: + self.log.debug("keypair list failed: %s" % e, exc_info=True) + raise OpenStackCloudException( + "Error fetching keypair list: %s" % e) + def list_networks(self): with self._neutron_exceptions("Error fetching network list"): return self.manager.submitTask(_tasks.NetworkList())['networks'] @@ -1061,6 +1065,9 @@ def _nova_list_floating_ips(self): raise OpenStackCloudException( "error fetching floating IPs list: {msg}".format(msg=str(e))) + def get_keypair(self, name_or_id, filters=None): + return _utils._get_entity(self.search_keypairs, name_or_id, filters) + def get_network(self, name_or_id, filters=None): return _utils._get_entity(self.search_networks, name_or_id, filters) @@ -1092,6 +1099,46 @@ def get_image(self, name_or_id, filters=None): def get_floating_ip(self, id, filters=None): return _utils._get_entity(self.search_floating_ips, id, filters) + def create_keypair(self, name, public_key): + """Create a new keypair. + + :param name: Name of the keypair being created. + :param public_key: Public key for the new keypair. + + :raises: OpenStackCloudException on operation error. + """ + try: + return meta.obj_to_dict( + self.manager.submitTask(_tasks.KeypairCreate( + name=name, public_key=public_key)) + ) + except Exception as e: + self.log.debug("Error creating keypair %s" % name, exc_info=True) + raise OpenStackCloudException( + "Unable to create keypair %s: %s" % (name, e) + ) + + def delete_keypair(self, name): + """Delete a keypair. + + :param name: Name of the keypair to delete. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException on operation error. + """ + try: + self.manager.submitTask(_tasks.KeypairDelete(key=name)) + except nova_exceptions.NotFound: + self.log.debug("Keypair %s not found for deleting" % name) + return False + except Exception as e: + self.log.debug("Error deleting keypair %s" % name, exc_info=True) + raise OpenStackCloudException( + "Unable to delete keypair %s: %s" % (name, e) + ) + return True + # TODO(Shrews): This will eventually need to support tenant ID and # provider networks, which are admin-level params. def create_network(self, name, shared=False, admin_state_up=True): diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 9597c3f84..4573c724b 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -137,3 +137,10 @@ def __init__(self, id, from_port=None, to_port=None, ip_protocol=None, if cidr: self.ip_range = {'cidr': cidr} self.parent_group_id = parent_group_id + + +class FakeKeypair(object): + def __init__(self, id, name, public_key): + self.id = id + self.name = name + self.public_key = public_key diff --git a/shade/tests/unit/test_keypair.py b/shade/tests/unit/test_keypair.py new file mode 100644 index 000000000..2a4f151e9 --- /dev/null +++ b/shade/tests/unit/test_keypair.py @@ -0,0 +1,72 @@ +# +# 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 shade + +from mock import patch +from novaclient import exceptions as nova_exc + +from shade import exc +from shade import meta +from shade.tests import fakes +from shade.tests.unit import base + + +class TestKeypair(base.TestCase): + + def setUp(self): + super(TestKeypair, self).setUp() + self.cloud = shade.openstack_cloud() + + @patch.object(shade.OpenStackCloud, 'nova_client') + def test_create_keypair(self, mock_nova): + keyname = 'my_keyname' + pub_key = 'ssh-rsa BLAH' + key = fakes.FakeKeypair('keyid', keyname, pub_key) + mock_nova.keypairs.create.return_value = key + + new_key = self.cloud.create_keypair(keyname, pub_key) + mock_nova.keypairs.create.assert_called_once_with( + name=keyname, public_key=pub_key + ) + self.assertEqual(meta.obj_to_dict(key), new_key) + + @patch.object(shade.OpenStackCloud, 'nova_client') + def test_create_keypair_exception(self, mock_nova): + mock_nova.keypairs.create.side_effect = Exception() + self.assertRaises(exc.OpenStackCloudException, + self.cloud.create_keypair, '', '') + + @patch.object(shade.OpenStackCloud, 'nova_client') + def test_delete_keypair(self, mock_nova): + self.assertTrue(self.cloud.delete_keypair('mykey')) + mock_nova.keypairs.delete.assert_called_once_with( + key='mykey' + ) + + @patch.object(shade.OpenStackCloud, 'nova_client') + def test_delete_keypair_not_found(self, mock_nova): + mock_nova.keypairs.delete.side_effect = nova_exc.NotFound('') + self.assertFalse(self.cloud.delete_keypair('invalid')) + + @patch.object(shade.OpenStackCloud, 'nova_client') + def test_list_keypairs(self, mock_nova): + self.cloud.list_keypairs() + mock_nova.keypairs.list.assert_called_once_with() + + @patch.object(shade.OpenStackCloud, 'nova_client') + def test_list_keypairs_exception(self, mock_nova): + mock_nova.keypairs.list.side_effect = Exception() + self.assertRaises(exc.OpenStackCloudException, + self.cloud.list_keypairs) From 2b64bbc7d6e8e36c10ca86ae7cf7aed78c55c9d5 Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Thu, 25 Jun 2015 09:39:01 +0100 Subject: [PATCH 0361/3836] Fix AttributeError in keystone functional tests OpenStack allows omitting names for services. This patch adds a check to ensure we do not call startswith on None values. Change-Id: Id6be1182a2226e3e53a10f6e29f674d777eae6c5 --- shade/tests/functional/test_endpoints.py | 3 ++- shade/tests/functional/test_services.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/shade/tests/functional/test_endpoints.py b/shade/tests/functional/test_endpoints.py index 93cb52bd3..fbf06d013 100644 --- a/shade/tests/functional/test_endpoints.py +++ b/shade/tests/functional/test_endpoints.py @@ -65,7 +65,8 @@ def _cleanup_endpoints(self): def _cleanup_services(self): exception_list = list() for s in self.operator_cloud.list_services(): - if s['name'].startswith(self.new_item_name): + if s['name'] is not None and \ + s['name'].startswith(self.new_item_name): try: self.operator_cloud.delete_service(name_or_id=s['id']) except Exception as e: diff --git a/shade/tests/functional/test_services.py b/shade/tests/functional/test_services.py index 57137f9aa..654f86e52 100644 --- a/shade/tests/functional/test_services.py +++ b/shade/tests/functional/test_services.py @@ -47,7 +47,8 @@ def setUp(self): def _cleanup_services(self): exception_list = list() for s in self.operator_cloud.list_services(): - if s['name'].startswith(self.new_service_name): + if s['name'] is not None and \ + s['name'].startswith(self.new_service_name): try: self.operator_cloud.delete_service(name_or_id=s['id']) except Exception as e: From fd36850b24ade1af62aa510850e34d473428e1f5 Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Thu, 25 Jun 2015 09:41:36 +0100 Subject: [PATCH 0362/3836] Skip Swift functional tests if needed Not all clouds have Swift, this patch skips functional tests for it if the cloud used doesn't have the object service in its catalog. A warning message is printed to inform the user object service functional tests have been skipped. Change-Id: Ie05d08c695f4d42d41d23d5e35e9caca03aaebb1 --- shade/tests/functional/test_object.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shade/tests/functional/test_object.py b/shade/tests/functional/test_object.py index 8ca213986..2aee9e9b2 100644 --- a/shade/tests/functional/test_object.py +++ b/shade/tests/functional/test_object.py @@ -34,6 +34,8 @@ def setUp(self): super(TestObject, self).setUp() # Shell should have OS-* envvars from openrc, typically loaded by job self.cloud = openstack_cloud() + if not self.cloud.has_service('object'): + self.skipTest('Object service not supported by cloud') def test_create_object(self): '''Test uploading small and large files.''' From 5d666939ba35d539ab3e512ec0b165df9f9d0ccc Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Thu, 25 Jun 2015 10:03:19 +0100 Subject: [PATCH 0363/3836] Fix available_floating_ip when using Nova network When neutron is not available and there are no floating IP already allocated to the project, available_floating_ip calls meta.obj_to_dict() on a Bunch object. Moreover we can save an API call as we already know the pool name. Change-Id: I997cb0edc4fd9160b6804a29d404190526303720 --- shade/__init__.py | 5 ++--- shade/tests/unit/test_floating_ip_nova.py | 8 +++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 0bcc52ea2..15092a067 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1797,10 +1797,9 @@ def _nova_available_floating_ips(self, pool=None): return available_ips # No available IP found, allocate a new Floating IP - pools = self.search_floating_ip_pools(name=pool) - f_ip = self._nova_create_floating_ip(pool=pools[0]['name']) + f_ip = self._nova_create_floating_ip(pool=pool) - return [meta.obj_to_dict(f_ip)] + return [f_ip] except Exception as e: self.log.debug( diff --git a/shade/tests/unit/test_floating_ip_nova.py b/shade/tests/unit/test_floating_ip_nova.py index 5e68ccf8d..1d8fb6799 100644 --- a/shade/tests/unit/test_floating_ip_nova.py +++ b/shade/tests/unit/test_floating_ip_nova.py @@ -153,17 +153,15 @@ def test_available_floating_ip_existing( self.assertEqual(self.mock_floating_ip_list_rep[0]['ip'], ip['floating_ip_address']) - @patch.object(OpenStackCloud, '_nova_create_floating_ip') - @patch.object(OpenStackCloud, 'list_floating_ip_pools') + @patch.object(OpenStackCloud, 'nova_client') @patch.object(OpenStackCloud, '_nova_list_floating_ips') @patch.object(OpenStackCloud, 'has_service') def test_available_floating_ip_new( self, mock_has_service, mock__nova_list_floating_ips, - mock_list_floating_ip_pools, mock__nova_create_floating_ip): + mock_nova_client): mock_has_service.side_effect = has_service_side_effect mock__nova_list_floating_ips.return_value = [] - mock_list_floating_ip_pools.return_value = self.mock_floating_ip_pools - mock__nova_create_floating_ip.return_value = \ + mock_nova_client.floating_ips.create.return_value = \ FakeFloatingIP(**self.mock_floating_ip_list_rep[0]) ip = self.client.available_floating_ip(network='nova') From 882ad91dffdfac2945932bbbc9071646f07cdcd4 Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Wed, 24 Jun 2015 17:29:11 +0100 Subject: [PATCH 0364/3836] Add Neutron/Nova Floating IP attach/detach Some clouds out there are still running nova-network but we want to provide support for neutron specific features and concepts like ports, networks, subnets, etc. This change is part of a set that adds neutron support to existing floating IP-related functions, hiding that behind resource-oriented methods. For instance, at high level, end-users can now request that a public IP is assigned to an instance without worrying about the specific service, procedures and protocols used to provide that feature in the target cloud. Change-Id: Ibeec7354e4690f5ea69641e8367e7adea7c64ce7 --- shade/__init__.py | 278 +++++++++++++++---- shade/_tasks.py | 12 +- shade/tests/unit/test_floating_ip_neutron.py | 102 +++++++ shade/tests/unit/test_floating_ip_nova.py | 45 +++ 4 files changed, 381 insertions(+), 56 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 15092a067..5a93bb9ef 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1922,69 +1922,237 @@ def _nova_delete_floating_ip(self, floating_ip_id): return True - def add_ip_from_pool(self, server, pools): - - # empty dict and list - usable_floating_ips = {} - - # get the list of all floating IPs. Mileage may - # vary according to Nova Compute configuration - # per cloud provider - all_floating_ips = self.list_floating_ips() - - # iterate through all pools of IP address. Empty - # string means all and is the default value - for pool in pools: - # temporary list per pool - pool_ips = [] - # loop through all floating IPs - for f_ip in all_floating_ips: - # if not reserved and the correct pool, add - if f_ip['fixed_ip_address'] is None and \ - (f_ip['network'] == pool): - pool_ips.append(f_ip['floating_ip_address']) - # only need one - break + def attach_ip_to_server( + self, server_id, floating_ip_id, fixed_address=None, wait=False, + timeout=60): + """Attach a floating IP to a server. + + :param server_id: id of a server. + :param floating_ip_id: id of the floating IP to attach. + :param fixed_address: (optional) fixed address to which attach the + floating IP to. + :param wait: (optional) Wait for the address to appear as assigned + to the server in Nova. Defaults to False. + :param timeout: (optional) Seconds to wait, defaults to 60. + See the ``wait`` parameter. - # if the list is empty, add for this pool - if not pool_ips: - new_ip = self.create_floating_ip(network=pool) - pool_ips.append(new_ip['floating_ip_address']) - # Add to the main list - usable_floating_ips[pool] = pool_ips - - # finally, add ip(s) to instance for each pool - for pool in usable_floating_ips: - for ip in usable_floating_ips[pool]: - self.add_ip_list(server, [ip]) - # We only need to assign one ip - but there is an inherent - # race condition and some other cloud operation may have - # stolen an available floating ip - break + :returns: None - def add_ip_list(self, server, ips): - # add ip(s) to instance - for ip in ips: + :raises: OpenStackCloudException, on operation error. + """ + if self.has_service('network'): try: - self.manager.submitTask( - _tasks.FloatingIPAttach(server=server, address=ip)) - except Exception as e: + self._neutron_attach_ip_to_server( + server_id=server_id, floating_ip_id=floating_ip_id, + fixed_address=fixed_address) + except OpenStackCloudURINotFound as e: self.log.debug( - "nova floating ip add failed", exc_info=True) + "Something went wrong talking to neutron API: " + "'{msg}'. Trying with Nova.".format(msg=str(e))) + # Fall-through, trying with Nova + else: + # Nova network + self._nova_attach_ip_to_server( + server_id=server_id, floating_ip_id=floating_ip_id, + fixed_address=fixed_address) + + if wait: + # Wait for the address to be assigned to the server + f_ip = self.get_floating_ip(id=floating_ip_id) + for _ in _utils._iterate_timeout( + timeout, + "Timeout waiting for the floating IP to be attached."): + server = self.get_server(name_or_id=server_id) + for k, v in server['addresses'].items(): + for interface_spec in v: + if interface_spec['addr'] == \ + f_ip['floating_ip_address']: + return + + def _neutron_attach_ip_to_server(self, server_id, floating_ip_id, + fixed_address=None): + with self._neutron_exceptions( + "unable to bind a floating ip to server " + "{0}".format(server_id)): + # Find an available port + ports = self.search_ports(filters={'device_id': server_id}) + port = None + if ports and fixed_address is None: + port = ports[0] + elif ports: + # unfortunately a port can have more than one fixed IP: + # we can't use the search_ports filtering for fixed_address as + # they are contained in a list. e.g. + # + # "fixed_ips": [ + # { + # "subnet_id": "008ba151-0b8c-4a67-98b5-0d2b87666062", + # "ip_address": "172.24.4.2" + # } + # ] + # + # Search fixed_address + for p in ports: + for fixed_ip in p['fixed_ips']: + if fixed_address == fixed_ip['ip_address']: + port = p + break + else: + continue + break + + if not port: raise OpenStackCloudException( - "Error attaching IP {ip} to instance {id}: {msg} ".format( - ip=ip, id=server['id'], msg=str(e))) + "unable to find a port for server {0}".format(server_id)) - def add_auto_ip(self, server): - new_ip = self.create_floating_ip() + return self.manager.submitTask(_tasks.NeutronFloatingIPUpdate( + floatingip=floating_ip_id, + body={'floatingip': {'port_id': port['id']}} + ))['floatingip'] + def _nova_attach_ip_to_server(self, server_id, floating_ip_id, + fixed_address=None): try: - self.add_ip_list(server, [new_ip['floating_ip_address']]) - except OpenStackCloudException: - # Clean up - we auto-created this ip, and it's not attached - # to the server, so the cloud will not know what to do with it - self.delete_floating_ip(floating_ip_id=new_ip['id']) - raise + f_ip = self.get_floating_ip(id=floating_ip_id) + return self.manager.submitTask(_tasks.NovaFloatingIPAttach( + server=server_id, address=f_ip['floating_ip_address'], + fixed_address=fixed_address)) + except Exception as e: + self.log.debug( + "nova floating IP attach failed: {msg}".format(msg=str(e)), + exc_info=True) + raise OpenStackCloudException( + "error attaching IP {ip} to instance {id}: {msg}".format( + ip=floating_ip_id, id=server_id, msg=str(e))) + + def detach_ip_from_server(self, server_id, floating_ip_id): + """Detach a floating IP from a server. + + :param server_id: id of a server. + :param floating_ip_id: Id of the floating IP to detach. + + :returns: True if the IP has been detached, or False if the IP wasn't + attached to any server. + + :raises: ``OpenStackCloudException``, on operation error. + """ + if self.has_service('network'): + try: + return self._neutron_detach_ip_from_server( + server_id=server_id, floating_ip_id=floating_ip_id) + except OpenStackCloudURINotFound as e: + self.log.debug( + "Something went wrong talking to neutron API: " + "'{msg}'. Trying with Nova.".format(msg=str(e))) + # Fall-through, trying with Nova + + # Nova network + self._nova_detach_ip_from_server( + server_id=server_id, floating_ip_id=floating_ip_id) + + def _neutron_detach_ip_from_server(self, server_id, floating_ip_id): + with self._neutron_exceptions( + "unable to detach a floating ip from server " + "{0}".format(server_id)): + f_ip = self.get_floating_ip(id=floating_ip_id) + if f_ip is None or not f_ip['attached']: + return False + self.manager.submitTask(_tasks.NeutronFloatingIPUpdate( + floatingip=floating_ip_id, + body={'floatingip': {'port_id': None}})) + + return True + + def _nova_detach_ip_from_server(self, server_id, floating_ip_id): + try: + f_ip = self.get_floating_ip(id=floating_ip_id) + if f_ip is None: + raise OpenStackCloudException( + "unable to find floating IP {0}".format(floating_ip_id)) + self.manager.submitTask(_tasks.NovaFloatingIPDetach( + server=server_id, address=f_ip['floating_ip_address'])) + except nova_exceptions.Conflict as e: + self.log.debug( + "nova floating IP detach failed: {msg}".format(msg=str(e)), + exc_info=True) + return False + except Exception as e: + self.log.debug( + "nova floating IP detach failed: {msg}".format(msg=str(e)), + exc_info=True) + raise OpenStackCloudException( + "error detaching IP {ip} from instance {id}: {msg}".format( + ip=floating_ip_id, id=server_id, msg=str(e))) + + return True + + def add_ip_from_pool(self, server_id, network, fixed_address=None): + """Add a floating IP to a sever from a given pool + + This method reuses available IPs, when possible, or allocate new IPs + to the current tenant. + The floating IP is attached to the given fixed address or to the + first server port/fixed address + + :param server_id: Id of a server + :param network: Nova pool name or Neutron network name or id. + :param fixed_address: a fixed address + + :returns: the floating IP assigned + """ + f_ip = self.available_floating_ip(network=network) + + self.attach_ip_to_server( + server_id=server_id, floating_ip_id=f_ip['id'], + fixed_address=fixed_address) + + return f_ip + + def add_ip_list(self, server, ips): + """Attach a list of IPs to a server. + + :param server: a server object + :param ips: list of IP addresses (floating IPs) + + :returns: None + + :raises: ``OpenStackCloudException``, on operation error. + """ + # ToDo(dguerri): this makes no sense as we cannot attach multiple + # floating IPs to a single fixed_address (this is true for both + # neutron and nova). I will leave this here for the moment as we are + # refactoring floating IPs methods. + for ip in ips: + f_ip = self.get_floating_ip( + id=None, filters={'floating_ip_address': ip}) + self.attach_ip_to_server( + server_id=server['id'], floating_ip_id=f_ip['id']) + + def add_auto_ip(self, server, wait=False, timeout=60): + """Add a floating IP to a server. + + This method is intended for basic usage. For advanced network + architecture (e.g. multiple external networks or servers with multiple + interfaces), use other floating IP methods. + + This method reuses available IPs, when possible, or allocate new IPs + to the current tenant. + + :param server: a server dictionary. + :param wait: (optional) Wait for the address to appear as assigned + to the server in Nova. Defaults to False. + :param timeout: (optional) Seconds to wait, defaults to 60. + See the ``wait`` parameter. + + :returns: Floating IP address attached to server. + + """ + f_ip = self.available_floating_ip() + self.attach_ip_to_server( + server_id=server['id'], floating_ip_id=f_ip['id'], wait=wait, + timeout=timeout) + + return self.get_floating_ip(id=f_ip['id']) def add_ips_to_server(self, server, auto_ip=True, ips=None, ip_pool=None): if ip_pool: diff --git a/shade/_tasks.py b/shade/_tasks.py index a41409ee9..50897434f 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -282,11 +282,21 @@ def main(self, client): return client.nova_client.floating_ips.delete(**self.args) -class FloatingIPAttach(task_manager.Task): +class NovaFloatingIPAttach(task_manager.Task): def main(self, client): return client.nova_client.servers.add_floating_ip(**self.args) +class NovaFloatingIPDetach(task_manager.Task): + def main(self, client): + return client.nova_client.servers.remove_floating_ip(**self.args) + + +class NeutronFloatingIPUpdate(task_manager.Task): + def main(self, client): + return client.neutron_client.update_floatingip(**self.args) + + class FloatingIPPoolList(task_manager.Task): def main(self, client): return client.nova_client.floating_ip_pools.list() diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 702df8620..61117aa19 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -23,6 +23,7 @@ from neutronclient.common import exceptions as n_exc +from shade import _utils from shade import OpenStackCloud from shade.tests.unit import base @@ -82,6 +83,37 @@ class TestFloatingIP(base.TestCase): 'provider:segmentation_id': None } + mock_search_ports_rep = [ + { + 'status': 'ACTIVE', + 'binding:host_id': 'devstack', + 'name': 'first-port', + 'allowed_address_pairs': [], + 'admin_state_up': True, + 'network_id': '70c1db1f-b701-45bd-96e0-a313ee3430b3', + 'tenant_id': '', + 'extra_dhcp_opts': [], + 'binding:vif_details': { + 'port_filter': True, + 'ovs_hybrid_plug': True + }, + 'binding:vif_type': 'ovs', + 'device_owner': 'compute:None', + 'mac_address': 'fa:16:3e:58:42:ed', + 'binding:profile': {}, + 'binding:vnic_type': 'normal', + 'fixed_ips': [ + { + 'subnet_id': '008ba151-0b8c-4a67-98b5-0d2b87666062', + 'ip_address': '172.24.4.2' + } + ], + 'id': 'ce705c24-c1ef-408a-bda3-7bbd946164ac', + 'security_groups': [], + 'device_id': 'server_id' + } + ] + def assertAreInstances(self, elements, elem_type): for e in elements: self.assertIsInstance(e, elem_type) @@ -230,3 +262,73 @@ def test_delete_floating_ip_not_found( floating_ip_id='a-wild-id-appears') self.assertFalse(ret) + + @patch.object(OpenStackCloud, '_neutron_list_floating_ips') + @patch.object(OpenStackCloud, 'search_ports') + @patch.object(OpenStackCloud, 'neutron_client') + @patch.object(OpenStackCloud, 'has_service') + def test_attach_ip_to_server( + self, mock_has_service, mock_neutron_client, mock_search_ports, + mock__neutron_list_floating_ips): + mock_has_service.return_value = True + mock__neutron_list_floating_ips.return_value = \ + [self.mock_floating_ip_new_rep['floatingip']] + + mock_search_ports.return_value = self.mock_search_ports_rep + + self.client.attach_ip_to_server( + server_id='server_id', + floating_ip_id='2f245a7b-796b-4f26-9cf9-9e82d248fda8') + + mock_neutron_client.update_floatingip.assert_called_with( + floatingip=self.mock_floating_ip_new_rep['floatingip']['id'], + body={ + 'floatingip': { + 'port_id': self.mock_search_ports_rep[0]['id'] + } + } + ) + + @patch.object(OpenStackCloud, '_neutron_list_floating_ips') + @patch.object(OpenStackCloud, 'neutron_client') + @patch.object(OpenStackCloud, 'has_service') + def test_detach_ip_from_server( + self, mock_has_service, mock_neutron_client, + mock__neutron_list_floating_ips): + mock_has_service.return_value = True + mock__neutron_list_floating_ips.return_value = \ + self.mock_floating_ip_list_rep['floatingips'] + + self.client.detach_ip_from_server( + server_id='server-id', + floating_ip_id='2f245a7b-796b-4f26-9cf9-9e82d248fda7') + + mock_neutron_client.update_floatingip.assert_called_with( + floatingip='2f245a7b-796b-4f26-9cf9-9e82d248fda7', + body={ + 'floatingip': { + 'port_id': None + } + } + ) + + @patch.object(OpenStackCloud, 'attach_ip_to_server') + @patch.object(OpenStackCloud, 'available_floating_ip') + @patch.object(OpenStackCloud, 'has_service') + def test_add_ip_from_pool( + self, mock_has_service, mock_available_floating_ip, + mock_attach_ip_to_server): + mock_has_service.return_value = True + mock_available_floating_ip.return_value = \ + _utils.normalize_neutron_floating_ips([ + self.mock_floating_ip_new_rep['floatingip']])[0] + mock_attach_ip_to_server.return_value = None + + ip = self.client.add_ip_from_pool( + server_id='server-id', + network='network-name', + fixed_address='1.2.3.4') + + self.assertEqual( + self.mock_floating_ip_new_rep['floatingip']['floating_ip_address'], + ip['floating_ip_address']) diff --git a/shade/tests/unit/test_floating_ip_nova.py b/shade/tests/unit/test_floating_ip_nova.py index 1d8fb6799..9a8a5dade 100644 --- a/shade/tests/unit/test_floating_ip_nova.py +++ b/shade/tests/unit/test_floating_ip_nova.py @@ -195,3 +195,48 @@ def test_delete_floating_ip_not_found( floating_ip_id='a-wild-id-appears') self.assertFalse(ret) + + @patch.object(OpenStackCloud, 'nova_client') + @patch.object(OpenStackCloud, 'has_service') + def test_attach_ip_to_server(self, mock_has_service, mock_nova_client): + mock_has_service.side_effect = has_service_side_effect + mock_nova_client.floating_ips.list.return_value = [ + FakeFloatingIP(**ip) for ip in self.mock_floating_ip_list_rep + ] + + self.client.attach_ip_to_server( + server_id='server-id', floating_ip_id=1, + fixed_address='192.0.2.129') + + mock_nova_client.servers.add_floating_ip.assert_called_with( + server='server-id', address='203.0.113.1', + fixed_address='192.0.2.129') + + @patch.object(OpenStackCloud, 'nova_client') + @patch.object(OpenStackCloud, 'has_service') + def test_detach_ip_from_server(self, mock_has_service, mock_nova_client): + mock_has_service.side_effect = has_service_side_effect + mock_nova_client.floating_ips.list.return_value = [ + FakeFloatingIP(**ip) for ip in self.mock_floating_ip_list_rep + ] + + self.client.detach_ip_from_server( + server_id='server-id', floating_ip_id=1) + + mock_nova_client.servers.remove_floating_ip.assert_called_with( + server='server-id', address='203.0.113.1') + + @patch.object(OpenStackCloud, 'nova_client') + @patch.object(OpenStackCloud, 'has_service') + def test_add_ip_from_pool(self, mock_has_service, mock_nova_client): + mock_has_service.side_effect = has_service_side_effect + mock_nova_client.floating_ips.list.return_value = [ + FakeFloatingIP(**ip) for ip in self.mock_floating_ip_list_rep + ] + + ip = self.client.add_ip_from_pool( + server_id='server-id', + network='nova', + fixed_address='192.0.2.129') + + self.assertEqual('203.0.113.1', ip['floating_ip_address']) From 1e447b4e202de54c0026ae9ed2a2fa7f6d36f22e Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Thu, 25 Jun 2015 10:20:04 +0100 Subject: [PATCH 0365/3836] Do not use environment for Swift unit tests Swift unit tests initialize the cloud object using shade::openstack_cloud(). That method uses os-cloud-config to load cloud parameters. As a consequence, this test could use real clouds or just fail because of unexpected values in the cloud object settings. Change-Id: I8006913ed2edf9b5019e83c8b958d48dfdf04cb2 --- shade/tests/unit/test_object.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index cf9e0254a..4dfc04fcb 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -19,6 +19,7 @@ import shade from shade import exc +from shade import OpenStackCloud from shade.tests.unit import base @@ -26,7 +27,7 @@ class TestShade(base.TestCase): def setUp(self): super(TestShade, self).setUp() - self.cloud = shade.openstack_cloud() + self.cloud = OpenStackCloud('cloud', {}) @mock.patch.object(swift_client, 'Connection') @mock.patch.object(shade.OpenStackCloud, 'auth_token', From 0b09988cba58a03cf7d351238621c8d4b37c2ece Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 25 Jun 2015 10:48:47 -0400 Subject: [PATCH 0366/3836] Remove list_keypair_dicts method This is unused and unnecessary now with the update to the keypair API. Change-Id: I04906e0fb35ffcac0d046ba37aa52c2a3cad8b1a --- shade/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index beda97ac7..a6a620784 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -793,10 +793,6 @@ def list_server_dicts(self): return [self.get_openstack_vars(server) for server in self.list_servers()] - def list_keypair_dicts(self): - return [meta.obj_to_dict(keypair) - for keypair in self.list_keypairs()] - @_cache_on_arguments() def _nova_extensions(self): extensions = set() From 65dd84515be415fe9f0c5751ecb8b6c51e3fabcb Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 26 Jun 2015 10:58:37 -0400 Subject: [PATCH 0367/3836] Add support for indicating preference for IPv6 People, such as Infra, would like to use IPv6 when it's there, but don't want to need to write the "if ipv6, awesome, else, ipv4" code all the time. Change-Id: I870955863f1e8851c684dc604584c1ef3e20dd6b --- README.rst | 31 +++++++++++++++++++++ os_client_config/cloud_config.py | 7 ++++- os_client_config/config.py | 15 +++++++++- os_client_config/tests/base.py | 3 ++ os_client_config/tests/test_cloud_config.py | 8 ++++++ os_client_config/tests/test_config.py | 12 ++++++++ os_client_config/tests/test_environ.py | 8 ++++++ 7 files changed, 82 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 9cdda45dc..b265341a1 100644 --- a/README.rst +++ b/README.rst @@ -185,6 +185,37 @@ are connecting to OpenStack can share a cache should you desire. dns_service_type: hpext:dns +IPv6 +---- + +IPv6 may be a thing you would prefer to use not only if the cloud supports it, +but also if your local machine support it. A simple boolean flag is settable +either in an environment variable, `OS_PREFER_IPV6`, or in the client section +of the clouds.yaml. + +:: + client: + prefer_ipv6: true + clouds: + mordred: + profile: hp + auth: + username: mordred@inaugust.com + password: XXXXXXXXX + project_name: mordred@inaugust.com + region_name: region-b.geo-1 + monty: + profile: rax + auth: + username: mordred@inaugust.com + password: XXXXXXXXX + project_name: mordred@inaugust.com + region_name: DFW + +The above snippet will tell client programs to prefer returning an IPv6 +address. This will result in calls to, for instance, `shade`'s `get_public_ip` +to return an IPv4 address on HP, and an IPv6 address on Rackspace. + Usage ----- diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 19f1bb4ee..d0586f218 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -16,10 +16,11 @@ class CloudConfig(object): - def __init__(self, name, region, config): + def __init__(self, name, region, config, prefer_ipv6=False): self.name = name self.region = region self.config = config + self._prefer_ipv6 = prefer_ipv6 def __getattr__(self, key): """Return arbitrary attributes.""" @@ -96,3 +97,7 @@ def get_service_type(self, service_type): def get_service_name(self, service_type): key = '{service_type}_service_name'.format(service_type=service_type) return self.config.get(key, service_type) + + @property + def prefer_ipv6(self): + return self._prefer_ipv6 diff --git a/os_client_config/config.py b/os_client_config/config.py index d2bc8166a..7961e72d2 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -68,6 +68,8 @@ def set_default(key, value): def get_boolean(value): + if type(value) is bool: + return value if value.lower() == 'true': return True return False @@ -116,6 +118,14 @@ def __init__(self, config_files=None, vendor_files=None, if 'clouds' not in self.cloud_config: self.cloud_config['clouds'] = {} + # Grab ipv6 preference settings from env + client_config = self.cloud_config.get('client', {}) + self.prefer_ipv6 = get_boolean( + os.environ.pop( + 'OS_PREFER_IPV6', client_config.get( + 'prefer_ipv6', client_config.get( + 'prefer-ipv6', False)))) + # Next, process environment variables and add them to the mix self.envvar_key = os.environ.pop('OS_CLOUD_NAME', 'envvars') if self.envvar_key in self.cloud_config['clouds']: @@ -427,13 +437,16 @@ def get_one_cloud(self, cloud=None, validate=True, if hasattr(value, 'format'): config[key] = value.format(**config) + prefer_ipv6 = config.pop('prefer_ipv6', self.prefer_ipv6) + if cloud is None: cloud_name = '' else: cloud_name = str(cloud) return cloud_config.CloudConfig( name=cloud_name, region=config['region_name'], - config=self._normalize_keys(config)) + config=self._normalize_keys(config), + prefer_ipv6=prefer_ipv6) @staticmethod def set_one_cloud(config_file, cloud, set_config=None): diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py index 0a4f456aa..8bb9145d2 100644 --- a/os_client_config/tests/base.py +++ b/os_client_config/tests/base.py @@ -38,6 +38,9 @@ } } USER_CONF = { + 'client': { + 'prefer_ipv6': True, + }, 'clouds': { '_test-cloud_': { 'profile': '_test_cloud_in_our_cloud', diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index 5f964dbeb..1a20ebf96 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -39,6 +39,9 @@ def test_arbitrary_attributes(self): # Lookup mystery attribute self.assertIsNone(cc.x) + # Test default ipv6 + self.assertFalse(cc.prefer_ipv6) + def test_iteration(self): cc = cloud_config.CloudConfig("test1", "region-al", fake_config_dict) self.assertTrue('a' in cc) @@ -100,3 +103,8 @@ def test_cert_with_key(self): cc = cloud_config.CloudConfig("test1", "region-xx", config_dict) (verify, cert) = cc.get_requests_verify_args() self.assertEqual(("cert", "key"), cert) + + def test_ipv6(self): + cc = cloud_config.CloudConfig( + "test1", "region-al", fake_config_dict, prefer_ipv6=True) + self.assertTrue(cc.prefer_ipv6) diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index f23a6b987..4c244d2a4 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -107,6 +107,18 @@ def test_fallthrough(self): self.useFixture(fixtures.EnvironmentVariable(k)) c.get_one_cloud(cloud='defaults') + def test_prefer_ipv6_true(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + cc = c.get_one_cloud(cloud='_test-cloud_') + self.assertTrue(cc.prefer_ipv6) + + def test_prefer_ipv6_false(self): + c = config.OpenStackConfig(config_files=[self.no_yaml], + vendor_files=[self.no_yaml]) + cc = c.get_one_cloud(cloud='defaults') + self.assertFalse(cc.prefer_ipv6) + def test_get_one_cloud_auth_merge(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml]) cc = c.get_one_cloud(cloud='_test-cloud_', auth={'username': 'user'}) diff --git a/os_client_config/tests/test_environ.py b/os_client_config/tests/test_environ.py index 365ad6780..ff44afc32 100644 --- a/os_client_config/tests/test_environ.py +++ b/os_client_config/tests/test_environ.py @@ -51,6 +51,14 @@ def test_envvar_name_override(self): cc = c.get_one_cloud('override') self._assert_cloud_details(cc) + def test_envvar_prefer_ipv6_override(self): + self.useFixture( + fixtures.EnvironmentVariable('OS_PREFER_IPV6', 'false')) + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + cc = c.get_one_cloud('_test-cloud_') + self.assertFalse(cc.prefer_ipv6) + def test_environ_exists(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) From 50f05448982b5fafd9d9a7783b639dd145090a0d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 6 Jun 2015 09:40:03 -0400 Subject: [PATCH 0368/3836] Clean up vendor data There are some clear central defaults. Call them out and don't repeat them. Also, ran the yaml files through a flamel conversion so they're all consistently formatted. Change-Id: Id19116c5e8266c109cf015d097cb6cb35f1beae8 --- os_client_config/config.py | 9 ++--- os_client_config/defaults.py | 44 +++++++++++------------ os_client_config/defaults.yaml | 16 +++++++++ os_client_config/vendors/__init__.py | 17 ++++++--- os_client_config/vendors/auro.yaml | 7 ++-- os_client_config/vendors/dreamhost.yaml | 6 ++-- os_client_config/vendors/hp.yaml | 8 ++--- os_client_config/vendors/ovh.yaml | 8 ++--- os_client_config/vendors/rackspace.yaml | 15 ++++---- os_client_config/vendors/runabove.yaml | 9 ++--- os_client_config/vendors/unitedstack.yaml | 9 ++--- os_client_config/vendors/vexxhost.yaml | 7 ++-- 12 files changed, 86 insertions(+), 69 deletions(-) create mode 100644 os_client_config/defaults.yaml diff --git a/os_client_config/config.py b/os_client_config/config.py index d2bc8166a..9eb13b3ba 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -218,7 +218,7 @@ def _get_base_cloud_config(self, name): # Expand a profile if it exists. 'cloud' is an old confusing name # for this. profile_name = our_cloud.get('profile', our_cloud.get('cloud', None)) - if profile_name: + if profile_name and profile_name != self.envvar_key: if 'cloud' in our_cloud: warnings.warn( "{0} use the keyword 'cloud' to reference a known " @@ -228,9 +228,10 @@ def _get_base_cloud_config(self, name): if vendor_file and profile_name in vendor_file['public-clouds']: _auth_update(cloud, vendor_file['public-clouds'][profile_name]) else: - try: - _auth_update(cloud, vendors.CLOUD_DEFAULTS[profile_name]) - except KeyError: + profile_data = vendors.get_profile(profile_name) + if profile_data: + _auth_update(cloud, profile_data) + else: # Can't find the requested vendor config, go about business warnings.warn("Couldn't find the vendor profile '{0}', for" " the cloud '{1}'".format(profile_name, diff --git a/os_client_config/defaults.py b/os_client_config/defaults.py index 8bf693d8d..a274767bd 100644 --- a/os_client_config/defaults.py +++ b/os_client_config/defaults.py @@ -12,29 +12,29 @@ # License for the specific language governing permissions and limitations # under the License. -_defaults = dict( - api_timeout=None, - auth_type='password', - baremetal_api_version='1', - compute_api_version='2', - database_api_version='1.0', - endpoint_type='public', - floating_ip_source='neutron', - identity_api_version='2', - image_api_use_tasks=False, - image_api_version='1', - network_api_version='2', - object_api_version='1', - secgroup_source='neutron', - volume_api_version='1', - disable_vendor_agent={}, - # SSL Related args - verify=True, - cacert=None, - cert=None, - key=None, -) +import os + +import yaml + +_yaml_path = os.path.join( + os.path.dirname(os.path.realpath(__file__)), 'defaults.yaml') +_defaults = None def get_defaults(): + global _defaults + if not _defaults: + # Python language specific defaults + # These are defaults related to use of python libraries, they are + # not qualities of a cloud. + _defaults = dict( + api_timeout=None, + verify=True, + cacert=None, + cert=None, + key=None, + ) + with open(_yaml_path, 'r') as yaml_file: + _defaults.update(yaml.load(yaml_file.read())) + return _defaults.copy() diff --git a/os_client_config/defaults.yaml b/os_client_config/defaults.yaml new file mode 100644 index 000000000..e4c900062 --- /dev/null +++ b/os_client_config/defaults.yaml @@ -0,0 +1,16 @@ +auth_type: password +baremetal_api_version: '1' +compute_api_version: '2' +database_api_version: '1.0' +disable_vendor_agent: {} +dns_api_version: '2' +endpoint_type: public +floating_ip_source: neutron +identity_api_version: '2' +image_api_use_tasks: false +image_api_version: '1' +image_format: qcow2 +network_api_version: '2' +object_api_version: '1' +secgroup_source: neutron +volume_api_version: '1' diff --git a/os_client_config/vendors/__init__.py b/os_client_config/vendors/__init__.py index 0ccb04c5d..367e3182b 100644 --- a/os_client_config/vendors/__init__.py +++ b/os_client_config/vendors/__init__.py @@ -17,9 +17,16 @@ import yaml -vendors_path = os.path.dirname(os.path.realpath(__file__)) +_vendors_path = os.path.dirname(os.path.realpath(__file__)) +_vendor_defaults = None -CLOUD_DEFAULTS = {} -for vendor in glob.glob(os.path.join(vendors_path, '*.yaml')): - with open(vendor, 'r') as f: - CLOUD_DEFAULTS.update(yaml.safe_load(f)) + +def get_profile(profile_name): + global _vendor_defaults + if _vendor_defaults is None: + _vendor_defaults = {} + for vendor in glob.glob(os.path.join(_vendors_path, '*.yaml')): + with open(vendor, 'r') as f: + vendor_data = yaml.load(f) + _vendor_defaults[vendor_data['name']] = vendor_data['profile'] + return _vendor_defaults.get(profile_name) diff --git a/os_client_config/vendors/auro.yaml b/os_client_config/vendors/auro.yaml index da1491dec..987838bbb 100644 --- a/os_client_config/vendors/auro.yaml +++ b/os_client_config/vendors/auro.yaml @@ -1,10 +1,7 @@ ---- -auro: +name: auro +profile: auth: auth_url: https://api.auro.io:5000/v2.0 region_name: RegionOne - identity_api_version: 2 - image_api_version: 1 - image_format: qcow2 secgroup_source: nova floating_ip_source: nova diff --git a/os_client_config/vendors/dreamhost.yaml b/os_client_config/vendors/dreamhost.yaml index 63f15c31d..5e10d1403 100644 --- a/os_client_config/vendors/dreamhost.yaml +++ b/os_client_config/vendors/dreamhost.yaml @@ -1,7 +1,7 @@ ---- -dreamhost: +name: dreamhost +profile: auth: auth_url: https://keystone.dream.io/v2.0 region_name: RegionOne - image_api_version: 2 + image_api_version: '2' image_format: raw diff --git a/os_client_config/vendors/hp.yaml b/os_client_config/vendors/hp.yaml index 2ac551781..277343325 100644 --- a/os_client_config/vendors/hp.yaml +++ b/os_client_config/vendors/hp.yaml @@ -1,8 +1,6 @@ ---- -hp: +name: hp +profile: auth: auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0 - region_name: region-b.geo-1 + region_name: region-a.geo-1,region-b.geo-1 dns_service_type: hpext:dns - image_api_version: 1 - image_format: qcow2 diff --git a/os_client_config/vendors/ovh.yaml b/os_client_config/vendors/ovh.yaml index 5b4426f18..f83437297 100644 --- a/os_client_config/vendors/ovh.yaml +++ b/os_client_config/vendors/ovh.yaml @@ -1,10 +1,6 @@ ---- -ovh: +name: ovh +profile: auth: auth_url: https://auth.cloud.ovh.net/v2.0 region_name: SBG1 - identity_api_version: 2 - image_api_version: 1 image_format: raw - secgroup_source: neutron - floating_ip_source: neutron diff --git a/os_client_config/vendors/rackspace.yaml b/os_client_config/vendors/rackspace.yaml index 7af3cab96..a350584c4 100644 --- a/os_client_config/vendors/rackspace.yaml +++ b/os_client_config/vendors/rackspace.yaml @@ -1,14 +1,15 @@ ---- -rackspace: +name: rackspace +profile: auth: - auth_url: https://identity.api.rackspacecloud.com/v2.0 - database_service_type: 'rax:database' + auth_url: https://identity.api.rackspacecloud.com/v2.0/ + region_name: DFW,ORD,IAD,SYD,HKG + database_service_type: rax:database compute_service_name: cloudServersOpenStack - image_api_version: 2 - image_api_use_tasks: True + image_api_version: '2' + image_api_use_tasks: true image_format: vhd floating_ip_source: None secgroup_source: None disable_vendor_agent: vm_mode: hvm - xenapi_use_agent: False + xenapi_use_agent: false diff --git a/os_client_config/vendors/runabove.yaml b/os_client_config/vendors/runabove.yaml index c9641dd41..381020ab8 100644 --- a/os_client_config/vendors/runabove.yaml +++ b/os_client_config/vendors/runabove.yaml @@ -1,7 +1,8 @@ ---- -runabove: +name: runabove +profile: auth: auth_url: https://auth.runabove.io/v2.0 - image_api_version: 2 + region_name: SBG-1,BHS-1 + image_api_version: '2' image_format: qcow2 - floating_ip_sourc: None + floating_ip_source: None diff --git a/os_client_config/vendors/unitedstack.yaml b/os_client_config/vendors/unitedstack.yaml index 43c600a6f..db9b61239 100644 --- a/os_client_config/vendors/unitedstack.yaml +++ b/os_client_config/vendors/unitedstack.yaml @@ -1,8 +1,9 @@ ---- -unitedstack: +name: unitedstack +profile: auth: auth_url: https://identity.api.ustack.com/v3 - identity_api_version: 3 - image_api_version: 2 + region_name: bj1,gd1 + identity_api_version: '3' + image_api_version: '2' image_format: raw floating_ip_source: None diff --git a/os_client_config/vendors/vexxhost.yaml b/os_client_config/vendors/vexxhost.yaml index 5247ef436..f67c644c6 100644 --- a/os_client_config/vendors/vexxhost.yaml +++ b/os_client_config/vendors/vexxhost.yaml @@ -1,8 +1,7 @@ ---- -vexxhost: +name: vexxhost +profile: auth: auth_url: http://auth.api.thenebulacloud.com:5000/v2.0/ region_name: ca-ymq-1 - image_api_version: 2 - image_format: qcow2 + image_api_version: '2' floating_ip_source: None From b12b3b6e2294b148df0bac01d4ca44057a03f227 Mon Sep 17 00:00:00 2001 From: Steve Leon Date: Thu, 25 Jun 2015 16:43:02 -0700 Subject: [PATCH 0369/3836] Adding SSL arguments to glance client I am using os_server ansible module to boot VMs. This fails in a SSL Openstack env. It fails listing the images because it is verifying the cert. Change-Id: I062c0fd3adb84a758e1a1b3664bc6d225ca1e174 --- shade/__init__.py | 4 ++-- shade/tests/unit/test_shade.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index fe19b33db..9351530d0 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -614,8 +614,8 @@ def glance_client(self): try: self._glance_client = glanceclient.Client( self.api_versions['image'], endpoint, token=token, - session=self.keystone_session, - **kwargs) + session=self.keystone_session, insecure=not self.verify, + cacert=self.cert, **kwargs) except Exception as e: self.log.debug("glance unknown issue", exc_info=True) raise OpenStackCloudException( diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 17b23fa93..baa9ac23d 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -14,6 +14,7 @@ import mock +import glanceclient from neutronclient.common import exceptions as n_exc import shade @@ -65,6 +66,17 @@ def test_list_servers_exception(self, mock_client): self.assertRaises(exc.OpenStackCloudException, self.cloud.list_servers) + @mock.patch.object(shade.OpenStackCloud, 'keystone_session') + @mock.patch.object(glanceclient, 'Client') + def test_glance_ssl_args(self, mock_client, mock_keystone_session): + mock_keystone_session.return_value = None + self.cloud.glance_client + mock_client.assert_called_with( + '1', mock.ANY, token=mock.ANY, session=mock.ANY, + insecure=False, + cacert=None, + ) + @mock.patch.object(shade.OpenStackCloud, 'search_subnets') def test_get_subnet(self, mock_search): subnet = dict(id='123', name='mickey') From 808a50aa4f8d004eaefb51e303f5b6311c491e86 Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Fri, 15 May 2015 21:48:06 +0100 Subject: [PATCH 0370/3836] Add Neutron/Nova Floating IP tests Nova/Neutron floating IP unit and functional tests. Some clouds out there are still running nova-network but we want to provide support for neutron specific features and concepts like ports, networks, subnets, etc. This change is part of a set that adds neutron support to existing floating IP-related functions, hiding that behind resource-oriented methods. For instance, at high level, end-users can now request that a public IP is assigned to an instance without worrying about the specific service, procedures and protocols used to provide that feature in the target cloud. Change-Id: Ib53923246e095283e1b470e50aeac3050b8c296e --- shade/tests/functional/test_floating_ip.py | 234 ++++++++++++++++++++ shade/tests/unit/test_floating_ip_common.py | 104 +++++++++ 2 files changed, 338 insertions(+) create mode 100644 shade/tests/functional/test_floating_ip.py create mode 100644 shade/tests/unit/test_floating_ip_common.py diff --git a/shade/tests/functional/test_floating_ip.py b/shade/tests/functional/test_floating_ip.py new file mode 100644 index 000000000..ce853d5c1 --- /dev/null +++ b/shade/tests/functional/test_floating_ip.py @@ -0,0 +1,234 @@ +# Copyright (c) 2015 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. + +""" +test_floating_ip +---------------------------------- + +Functional tests for floating IP resource. +""" + +import random +import string +import time + +from novaclient import exceptions as nova_exc + +from shade import openstack_cloud +from shade import meta +from shade.exc import OpenStackCloudException +from shade.exc import OpenStackCloudTimeout +from shade.tests import base +from shade.tests.functional.util import pick_flavor, pick_image + + +def _iterate_timeout(timeout, message): + start = time.time() + count = 0 + while (timeout is None) or (time.time() < start + timeout): + count += 1 + yield count + time.sleep(2) + raise OpenStackCloudTimeout(message) + + +class TestFloatingIP(base.TestCase): + timeout = 60 + + # Generate a random name for these tests + new_item_name = 'test_' + ''.join( + random.choice(string.ascii_lowercase) for _ in range(5)) + + def setUp(self): + super(TestFloatingIP, self).setUp() + # Shell should have OS-* envvars from openrc, typically loaded by job + self.cloud = openstack_cloud() + self.nova = self.cloud.nova_client + if self.cloud.has_service('network'): + self.neutron = self.cloud.neutron_client + self.flavor = pick_flavor(self.nova.flavors.list()) + if self.flavor is None: + self.assertFalse('no sensible flavor available') + self.image = pick_image(self.nova.images.list()) + if self.image is None: + self.assertFalse('no sensible image available') + + self.addCleanup(self._cleanup_network) + self.addCleanup(self._cleanup_servers) + + def _cleanup_network(self): + exception_list = list() + + # Delete stale networks as well as networks created for this test + if self.cloud.has_service('network'): + # Delete routers + for r in self.cloud.list_routers(): + try: + if r['name'].startswith(self.new_item_name): + # ToDo: update_router currently won't allow removing + # external_gateway_info + router = { + 'external_gateway_info': None + } + self.neutron.update_router( + router=r['id'], body={'router': router}) + # ToDo: Shade currently doesn't have methods for this + for s in self.cloud.list_subnets(): + if s['name'].startswith(self.new_item_name): + try: + self.neutron.remove_interface_router( + router=r['id'], + body={'subnet_id': s['id']}) + except Exception: + pass + self.cloud.delete_router(name_or_id=r['id']) + except Exception as e: + exception_list.append(e) + continue + # Delete subnets + for s in self.cloud.list_subnets(): + if s['name'].startswith(self.new_item_name): + try: + self.cloud.delete_subnet(name_or_id=s['id']) + except Exception as e: + exception_list.append(e) + continue + # Delete networks + for n in self.cloud.list_networks(): + if n['name'].startswith(self.new_item_name): + try: + self.cloud.delete_network(name_or_id=n['id']) + except Exception as e: + exception_list.append(e) + continue + + if exception_list: + # Raise an error: we must make users aware that something went + # wrong + raise OpenStackCloudException('\n'.join(exception_list)) + + def _cleanup_servers(self): + exception_list = list() + + # Delete stale servers as well as server created for this test + for i in self.nova.servers.list(): + if i.name.startswith(self.new_item_name): + self.nova.servers.delete(i) + for _ in _iterate_timeout( + self.timeout, "Timeout deleting servers"): + try: + self.nova.servers.get(server=i) + except nova_exc.NotFound: + break + except Exception as e: + exception_list.append(e) + continue + + if exception_list: + # Raise an error: we must make users aware that something went + # wrong + raise OpenStackCloudException('\n'.join(exception_list)) + + def _cleanup_ips(self, ips): + exception_list = list() + + for ip in ips: + try: + self.cloud.delete_floating_ip(ip) + except Exception as e: + exception_list.append(e) + continue + + if exception_list: + # Raise an error: we must make users aware that something went + # wrong + raise OpenStackCloudException('\n'.join(exception_list)) + + def _setup_networks(self): + if self.cloud.has_service('network'): + # Create a network + self.test_net = self.cloud.create_network( + name=self.new_item_name + '_net') + # Create a subnet on it + self.test_subnet = self.cloud.create_subnet( + subnet_name=self.new_item_name + '_subnet', + network_name_or_id=self.test_net['id'], + cidr='172.24.4.0/24', + enable_dhcp=True + ) + # Create a router + self.test_router = self.cloud.create_router( + name=self.new_item_name + '_router') + # Attach the router to an external network + ext_nets = self.cloud.search_networks( + filters={'router:external': True}) + self.cloud.update_router( + name_or_id=self.test_router['id'], + ext_gateway_net_id=ext_nets[0]['id']) + # Attach the router to the internal subnet + self.neutron.add_interface_router( + router=self.test_router['id'], + body={'subnet_id': self.test_subnet['id']}) + + # Select the network for creating new servers + self.nic = {'net-id': self.test_net['id']} + else: + # ToDo: remove once we have list/get methods for nova networks + nets = self.cloud.nova_client.networks.list() + self.nic = {'net-id': nets[0].id} + + def test_add_auto_ip(self): + self._setup_networks() + + new_server = self.cloud.create_server( + wait=True, name=self.new_item_name + '_server', + image=self.image, + flavor=self.flavor, nics=[self.nic]) + + # ToDo: remove the following iteration when create_server waits for + # the IP to be attached + ip = None + for _ in _iterate_timeout( + self.timeout, "Timeout waiting for IP address to be attached"): + ip = meta.get_server_external_ipv4(self.cloud, new_server) + if ip is not None: + break + new_server = self.cloud.get_server(new_server.id) + + self.addCleanup(self._cleanup_ips, [ip]) + + def test_detach_ip_from_server(self): + self._setup_networks() + + new_server = self.cloud.create_server( + wait=True, name=self.new_item_name + '_server', + image=self.image, + flavor=self.flavor, nics=[self.nic]) + + # ToDo: remove the following iteration when create_server waits for + # the IP to be attached + ip = None + for _ in _iterate_timeout( + self.timeout, "Timeout waiting for IP address to be attached"): + ip = meta.get_server_external_ipv4(self.cloud, new_server) + if ip is not None: + break + new_server = self.cloud.get_server(new_server.id) + + self.addCleanup(self._cleanup_ips, [ip]) + + f_ip = self.cloud.get_floating_ip( + id=None, filters={'floating_ip_address': ip}) + self.cloud.detach_ip_from_server( + server_id=new_server.id, floating_ip_id=f_ip['id']) diff --git a/shade/tests/unit/test_floating_ip_common.py b/shade/tests/unit/test_floating_ip_common.py new file mode 100644 index 000000000..7c5e193dc --- /dev/null +++ b/shade/tests/unit/test_floating_ip_common.py @@ -0,0 +1,104 @@ +# Copyright (c) 2015 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. + +""" +test_floating_ip_common +---------------------------------- + +Tests floating IP resource methods for Neutron and Nova-network. +""" + +from mock import patch +from shade import meta +from shade import OpenStackCloud +from shade.tests.fakes import FakeServer +from shade.tests.unit import base + + +class TestFloatingIP(base.TestCase): + def setUp(self): + super(TestFloatingIP, self).setUp() + self.client = OpenStackCloud("cloud", {}) + + @patch.object(OpenStackCloud, 'get_floating_ip') + @patch.object(OpenStackCloud, 'attach_ip_to_server') + @patch.object(OpenStackCloud, 'available_floating_ip') + def test_add_auto_ip( + self, mock_available_floating_ip, mock_attach_ip_to_server, + mock_get_floating_ip): + server = FakeServer( + id='server-id', name='test-server', status="ACTIVE", addresses={} + ) + server_dict = meta.obj_to_dict(server) + + mock_available_floating_ip.return_value = { + "id": "this-is-a-floating-ip-id", + "fixed_ip_address": None, + "floating_ip_address": "203.0.113.29", + "network": "this-is-a-net-or-pool-id", + "attached": False, + "status": "ACTIVE" + } + + self.client.add_auto_ip(server=server_dict) + + mock_attach_ip_to_server.assert_called_with( + timeout=60, wait=False, server_id='server-id', + floating_ip_id='this-is-a-floating-ip-id') + + @patch.object(OpenStackCloud, 'nova_client') + @patch.object(OpenStackCloud, 'add_ip_from_pool') + def test_add_ips_to_server_pool( + self, mock_add_ip_from_pool, mock_nova_client): + server = FakeServer( + id='server-id', name='test-server', status="ACTIVE", addresses={} + ) + server_dict = meta.obj_to_dict(server) + pool = 'nova' + + mock_nova_client.servers.get.return_value = server + + self.client.add_ips_to_server(server_dict, ip_pool=pool) + + mock_add_ip_from_pool.assert_called_with(server_dict, pool) + + @patch.object(OpenStackCloud, 'nova_client') + @patch.object(OpenStackCloud, 'add_ip_list') + def test_add_ips_to_server_ip_list( + self, mock_add_ip_list, mock_nova_client): + server = FakeServer( + id='server-id', name='test-server', status="ACTIVE", addresses={} + ) + server_dict = meta.obj_to_dict(server) + ips = ['203.0.113.29', '172.24.4.229'] + mock_nova_client.servers.get.return_value = server + + self.client.add_ips_to_server(server_dict, ips=ips) + + mock_add_ip_list.assert_called_with(server_dict, ips) + + @patch.object(OpenStackCloud, 'nova_client') + @patch.object(OpenStackCloud, 'add_auto_ip') + def test_add_ips_to_server_auto_ip( + self, mock_add_auto_ip, mock_nova_client): + server = FakeServer( + id='server-id', name='test-server', status="ACTIVE", addresses={} + ) + server_dict = meta.obj_to_dict(server) + + mock_nova_client.servers.get.return_value = server + + self.client.add_ips_to_server(server_dict) + + mock_add_auto_ip.assert_called_with(server_dict) From fceb193f53a91fa1857dcf772ff8be5e38b14d4a Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Thu, 25 Jun 2015 10:48:19 -0400 Subject: [PATCH 0371/3836] Locking ironic API microversion Due to changes related to the ironic state machine, and impending breaking change that enroll node state will cause, we will lock the ironic API microversion that shade utilizes to ensure consistent user experience until shade receives sufficent updates to perform the state transitions for the user. Blueprint: enroll-node-state https://blueprints.launchpad.net/ironic/+spec/enroll-node-state Change-Id: I3332fdadcfe0759ef4ebe237a6d0bbdca4b10272 --- shade/__init__.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index 21e0b95d8..510598fd8 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2961,6 +2961,18 @@ def auth_token(self): def ironic_client(self): if self._ironic_client is None: token = self.auth_token + # Set the ironic API microversion to a known-good + # supported/tested with the contents of shade. + # + # Note(TheJulia): Defaulted to version 1.6 as the ironic + # state machine changes which will increment the version + # and break an automatic transition of an enrolled node + # to an available state. Locking the version is intended + # to utilize the original transition until shade supports + # calling for node inspection to allow the transition to + # take place automatically. + ironic_api_microversion = '1.6' + if self.auth_type in (None, "None", ''): # TODO: This needs to be improved logic wise, perhaps a list, # or enhancement of the data stuctures with-in the library @@ -2975,7 +2987,8 @@ def ironic_client(self): try: self._ironic_client = ironic_client.Client( self.api_versions['baremetal'], endpoint, token=token, - timeout=self.api_timeout) + timeout=self.api_timeout, + os_ironic_api_version=ironic_api_microversion) except Exception as e: self.log.debug("ironic auth failed", exc_info=True) raise OpenStackCloudException( From 235dedcf3d3db477acda827d6583214abc3caff8 Mon Sep 17 00:00:00 2001 From: Gregory Haynes Date: Mon, 29 Jun 2015 22:12:35 +0000 Subject: [PATCH 0372/3836] Correctly name the functional TestImage class This seems like a copy-pasta that got overlooked, the TestImage class should not be named TestCompute. Change-Id: I00e9d651936f1aacfece5f461b41c43573830715 --- shade/tests/functional/test_image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shade/tests/functional/test_image.py b/shade/tests/functional/test_image.py index cef17607e..043bd65f0 100644 --- a/shade/tests/functional/test_image.py +++ b/shade/tests/functional/test_image.py @@ -27,9 +27,9 @@ from shade.tests.functional.util import pick_image -class TestCompute(base.TestCase): +class TestImage(base.TestCase): def setUp(self): - super(TestCompute, self).setUp() + super(TestImage, self).setUp() # Shell should have OS-* envvars from openrc, typically loaded by job self.cloud = openstack_cloud() self.image = pick_image(self.cloud.nova_client.images.list()) From 648e4fd08120738cee1553c3fa2d19f48a60852a Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Tue, 30 Jun 2015 00:11:09 +0100 Subject: [PATCH 0373/3836] Make sure we are returning floating IPs in current domain Make sure we are returning floating IPs in current domain when searching for already allocated and available floating IPs. When using a privileged user, Neutron returns all floating IPs allocated in the cloud by default. Nova just return floating IPs in current tenant. This patch adds project_id parameter to _neutron_available_floating_ips() to allow searching floating IPs in an arbitrary tenant. If project_id is None, the current tenant id is used. Change-Id: I4055e73ace8fa6653e154adf049ad423e976ecfe --- shade/__init__.py | 11 +++++++++-- shade/tests/unit/test_floating_ip_neutron.py | 10 ++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 41c4c5d45..85a606336 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1769,7 +1769,7 @@ def available_floating_ip(self, network=None): ) return f_ips[0] - def _neutron_available_floating_ips(self, network=None): + def _neutron_available_floating_ips(self, network=None, project_id=None): """Get a floating IP from a Neutron network. Return a list of available floating IPs or allocate a new one and @@ -1782,6 +1782,11 @@ def _neutron_available_floating_ips(self, network=None): :raises: ``OpenStackCloudResourceNotFound``, if an external network that meets the specified criteria cannot be found. """ + if project_id is None: + # Make sure we are only listing floatingIPs allocated the current + # tenant. This is the default behaviour of Nova + project_id = self.keystone_session.get_project_id() + with self._neutron_exceptions("unable to get available floating IPs"): networks = self.search_networks( name_or_id=network, @@ -1792,7 +1797,9 @@ def _neutron_available_floating_ips(self, network=None): filters = { 'port_id': None, - 'floating_network_id': networks[0]['id'] + 'floating_network_id': networks[0]['id'], + 'tenant_id': project_id + } floating_ips = self._neutron_list_floating_ips() available_ips = _utils._filter_list( diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 61117aa19..c7c77317d 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -198,16 +198,19 @@ def test_create_floating_ip( self.mock_floating_ip_new_rep['floatingip']['floating_ip_address'], ip['floating_ip_address']) + @patch.object(OpenStackCloud, 'keystone_session') @patch.object(OpenStackCloud, '_neutron_list_floating_ips') @patch.object(OpenStackCloud, 'search_networks') @patch.object(OpenStackCloud, 'has_service') def test_available_floating_ip_existing( self, mock_has_service, mock_search_networks, - mock__neutron_list_floating_ips): + mock__neutron_list_floating_ips, mock_keystone_session): mock_has_service.return_value = True mock_search_networks.return_value = [self.mock_get_network_rep] mock__neutron_list_floating_ips.return_value = \ [self.mock_floating_ip_new_rep['floatingip']] + mock_keystone_session.get_project_id.return_value = \ + '4969c491a3c74ee4af974e6d800c62df' ip = self.client.available_floating_ip(network='my-network') @@ -215,6 +218,7 @@ def test_available_floating_ip_existing( self.mock_floating_ip_new_rep['floatingip']['floating_ip_address'], ip['floating_ip_address']) + @patch.object(OpenStackCloud, 'keystone_session') @patch.object(OpenStackCloud, '_neutron_create_floating_ip') @patch.object(OpenStackCloud, '_neutron_list_floating_ips') @patch.object(OpenStackCloud, 'search_networks') @@ -222,12 +226,14 @@ def test_available_floating_ip_existing( def test_available_floating_ip_new( self, mock_has_service, mock_search_networks, mock__neutron_list_floating_ips, - mock__neutron_create_floating_ip): + mock__neutron_create_floating_ip, mock_keystone_session): mock_has_service.return_value = True mock_search_networks.return_value = [self.mock_get_network_rep] mock__neutron_list_floating_ips.return_value = [] mock__neutron_create_floating_ip.return_value = \ self.mock_floating_ip_new_rep['floatingip'] + mock_keystone_session.get_project_id.return_value = \ + '4969c491a3c74ee4af974e6d800c62df' ip = self.client.available_floating_ip(network='my-network') From 01aeceeae988262c2c5c2de7041ff9483fe5a855 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 30 Jun 2015 16:35:55 -0400 Subject: [PATCH 0374/3836] Modify secgroup rule processing It turns out that Neutron can accept None for a rule protocol to represent all protocols. Nova has no similar concept for a single rule, so let's throw an exception. Also, Neutron relaxes the rules on accepting None for both port values to represent the full port range for TCP and UDP. For ICMP, it is the same as -1. Change-Id: Icc408586804e2a538399a2aa3f740b97bb4c5246 --- shade/__init__.py | 28 +++++++++++++++++++----- shade/tests/unit/test_security_groups.py | 27 ++++++++++++++++++++--- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 41c4c5d45..5d4510531 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -3100,12 +3100,28 @@ def create_security_group_rule(self, return rule['security_group_rule'] elif self.secgroup_source == 'nova': - # NOTE: Neutron accepts None for ports, but Nova accepts -1 - # as the equivalent value. - if port_range_min is None: - port_range_min = -1 - if port_range_max is None: - port_range_max = -1 + # NOTE: Neutron accepts None for protocol. Nova does not. + if protocol is None: + raise OpenStackCloudException('Protocol must be specified') + + # NOTE: Neutron accepts None for ports, but Nova requires -1 + # as the equivalent value for ICMP. + # + # For TCP/UDP, if both are None, Neutron allows this and Nova + # represents this as all ports (1-65535). Nova does not accept + # None values, so to hide this difference, we will automatically + # convert to the full port range. If only a single port value is + # specified, it will error as normal. + if protocol == 'icmp': + if port_range_min is None: + port_range_min = -1 + if port_range_max is None: + port_range_max = -1 + elif protocol in ['tcp', 'udp']: + if port_range_min is None and port_range_max is None: + port_range_min = 1 + port_range_max = 65535 + try: rule = meta.obj_to_dict( self.manager.submitTask( diff --git a/shade/tests/unit/test_security_groups.py b/shade/tests/unit/test_security_groups.py index 33ea7090b..37179a6d5 100644 --- a/shade/tests/unit/test_security_groups.py +++ b/shade/tests/unit/test_security_groups.py @@ -235,20 +235,41 @@ def test_create_security_group_rule_nova(self, mock_nova, mock_get): self.cloud.secgroup_source = 'nova' new_rule = fakes.FakeNovaSecgroupRule( - id='xyz', from_port=-1, to_port=2000, ip_protocol='tcp', + id='xyz', from_port=1, to_port=2000, ip_protocol='tcp', cidr='1.2.3.4/32') mock_nova.security_group_rules.create.return_value = new_rule mock_get.return_value = {'id': 'abc'} self.cloud.create_security_group_rule( - 'abc', port_range_max=2000, protocol='tcp', + 'abc', port_range_min=1, port_range_max=2000, protocol='tcp', remote_ip_prefix='1.2.3.4/32', remote_group_id='123') mock_nova.security_group_rules.create.assert_called_once_with( - parent_group_id='abc', ip_protocol='tcp', from_port=-1, + parent_group_id='abc', ip_protocol='tcp', from_port=1, to_port=2000, cidr='1.2.3.4/32', group_id='123' ) + @mock.patch.object(shade.OpenStackCloud, 'get_security_group') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_create_security_group_rule_nova_no_ports(self, + mock_nova, mock_get): + self.cloud.secgroup_source = 'nova' + + new_rule = fakes.FakeNovaSecgroupRule( + id='xyz', from_port=1, to_port=65535, ip_protocol='tcp', + cidr='1.2.3.4/32') + mock_nova.security_group_rules.create.return_value = new_rule + mock_get.return_value = {'id': 'abc'} + + self.cloud.create_security_group_rule( + 'abc', protocol='tcp', + remote_ip_prefix='1.2.3.4/32', remote_group_id='123') + + mock_nova.security_group_rules.create.assert_called_once_with( + parent_group_id='abc', ip_protocol='tcp', from_port=1, + to_port=65535, cidr='1.2.3.4/32', group_id='123' + ) + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_create_security_group_rule_none(self, mock_nova, mock_neutron): From 2ab23109cb74a66b1d3108508a8c162a078e9edf Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 6 Jul 2015 15:39:09 -0400 Subject: [PATCH 0375/3836] Raise exception for nova egress secgroup rule Nova does not support egress security group rules, only Neutron. Trying to add one with the current code base simply ignores the direction and creates an ingress rule. Not ideal. Change-Id: I10f3d67b1f66b8c05eb36ec5cecfb530d93458aa --- shade/__init__.py | 6 ++++++ shade/tests/unit/test_security_groups.py | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 5d4510531..f5bc5db3f 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -3104,6 +3104,12 @@ def create_security_group_rule(self, if protocol is None: raise OpenStackCloudException('Protocol must be specified') + if direction == 'egress': + self.log.debug( + 'Rule creation failed: Nova does not support egress rules' + ) + raise OpenStackCloudException('No support for egress rules') + # NOTE: Neutron accepts None for ports, but Nova requires -1 # as the equivalent value for ICMP. # diff --git a/shade/tests/unit/test_security_groups.py b/shade/tests/unit/test_security_groups.py index 37179a6d5..14e0235e1 100644 --- a/shade/tests/unit/test_security_groups.py +++ b/shade/tests/unit/test_security_groups.py @@ -324,3 +324,12 @@ def test_delete_security_group_rule_not_found(self, ) r = self.cloud.delete_security_group('doesNotExist') self.assertFalse(r) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_nova_egress_security_group_rule(self, mock_nova): + self.cloud.secgroup_source = 'nova' + mock_nova.security_groups.list.return_value = [nova_grp_obj] + self.assertRaises(shade.OpenStackCloudException, + self.cloud.create_security_group_rule, + secgroup_name_or_id='nova-sec-group', + direction='egress') From 604c27fb54708e5d6abc1a462527b86ef4d9cbc3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 11 Jun 2015 00:53:47 +0200 Subject: [PATCH 0376/3836] Add CRUD methods for Keystone domains We're going to call these identity domains. I'm very tempted to call them realms, since that's the name that would be most specific and since Domain is clearly a thing owned by the Domain Name Service, but I'll stop using this commit message as a rant now. Co-Authored-By: Haneef Ali Change-Id: I2dd10aa7081fdb1d0bf21aa266e5a707e965f055 --- shade/__init__.py | 114 ++++++++++++++++++++++++++++++++++++++++++++++ shade/_tasks.py | 24 ++++++++++ 2 files changed, 138 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index ec52e012b..c21eb6e13 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -520,6 +520,60 @@ def delete_project(self, name_or_id): "Error in deleting project {project}: {message}".format( project=name_or_id, message=str(e))) + def list_identity_domains(self): + """List Keystone domains. + + :returns: a list of dicts containing the domain description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + # ToDo: support v3 api (dguerri) + try: + domains = self.manager.submitTask(_tasks.IdentityDomainList()) + except Exception as e: + self.log.debug("Failed to list domains", exc_info=True) + raise OpenStackCloudException(str(e)) + return meta.obj_list_to_dict(domains) + + def search_indentity_domains(self, name_or_id=None, filters=None): + """Seach Keystone domains. + + :param id: domain name or id. + :param filters: a dict containing additional filters to use. e.g. + {'enabled': False} + + :returns: a list of dict containing the domain description. Each dict + contains the following attributes:: + - id: + - name: + - description: + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + domains = self.list_identity_domains() + return _utils._filter_list(domains, name_or_id, filters) + + def get_identity_domain(self, name_or_id, filters=None): + """Get exactly one Keystone domain. + + :param id: domain name or id. + :param filters: a dict containing additional filters to use. e.g. + {'enabled': True} + + :returns: a list of dict containing the domain description. Each dict + contains the following attributes:: + - id: + - name: + - description: + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + return _utils._get_entity( + self.search_identity_domains, name_or_id, filters) + @property def user_cache(self): return self.get_user_cache() @@ -4081,3 +4135,63 @@ def delete_endpoint(self, id): exc_info=True) raise OpenStackCloudException(str(e)) return True + + def create_identity_domain( + self, name, description=None, enabled=True): + """Create a Keystone domain. + + :param name: The name of the domain. + :param description: A description of the domain. + :param enabled: Is the domain enabled or not (default True). + + :returns: a dict containing the domain description + + :raise OpenStackCloudException: if the domain cannot be created + """ + try: + domain = self.manager.submitTask(_tasks.IdentityDomainCreate( + name=name, + description=description, + enabled=enabled)) + except Exception as e: + self.log.debug( + "Failed to create domain {name}".format( + name=name, exc_info=True)) + raise OpenStackCloudException(str(e)) + return meta.obj_to_dict(domain) + + def update_identity_domain( + self, name_or_id, name=None, description=None, enabled=None): + try: + domain = self.get_identity_domain(name_or_id) + return meta.obj_to_dict( + self.manager.submitTask(_tasks.IdentityDomainUpdate( + domain=domain.id, description=description, + enabled=enabled))) + except Exception as e: + self.log.debug("keystone update domain issue", exc_info=True) + raise OpenStackCloudException( + "Error in updating domain {domain}: {message}".format( + domain=name_or_id, message=str(e))) + + def delete_identity_domain(self, name_or_id): + """Delete a Keystone domain. + + :param id: Name or id of the domain to delete. + + :returns: None + + :raises: ``OpenStackCloudException`` if something goes wrong during + the openstack API call. + """ + try: + # Deleting a domain is expensive, so disabling it first increases + # the changes of success + domain = self.update_domain(name_or_id, enabled=False) + self.manager.submitTask(_tasks.IdentityDomainDelete( + domain=domain.id)) + except Exception as e: + self.log.debug( + "Failed to delete domain {id}".format(id=domain.id), + exc_info=True) + raise OpenStackCloudException(str(e)) diff --git a/shade/_tasks.py b/shade/_tasks.py index 50897434f..81b92ad72 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -495,3 +495,27 @@ def main(self, client): class EndpointDelete(task_manager.Task): def main(self, client): return client.keystone_client.endpoints.delete(**self.args) + + +# IdentityDomain and not Domain because Domain is a DNS concept +class IdentityDomainCreate(task_manager.Task): + def main(self, client): + return client.keystone_client.domains.create(**self.args) + + +# IdentityDomain and not Domain because Domain is a DNS concept +class IdentityDomainList(task_manager.Task): + def main(self, client): + return client.keystone_client.domains.list() + + +# IdentityDomain and not Domain because Domain is a DNS concept +class IdentityDomainUpdate(task_manager.Task): + def main(self, client): + return client.keystone_client.domains.update(**self.args) + + +# IdentityDomain and not Domain because Domain is a DNS concept +class IdentityDomainDelete(task_manager.Task): + def main(self, client): + return client.keystone_client.domains.delete(**self.args) From b3197e9fd5680578ce602f98a1cd6e85cbb109a9 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 11 Jun 2015 02:59:32 +0200 Subject: [PATCH 0377/3836] Support project/tenant and domain vs. None Keystone v2 and v3 have a different enough interface that we need some helper methods to make sure we're passing in the right things all the time. Change-Id: Ic2a6cde5746237842287cdd426849562d25195ba --- shade/__init__.py | 60 ++++++++++++++++++----- shade/tests/unit/test_domains.py | 83 ++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 11 deletions(-) create mode 100644 shade/tests/unit/test_domains.py diff --git a/shade/__init__.py b/shade/__init__.py index c21eb6e13..71da75894 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -468,13 +468,50 @@ def _project_manager(self): return getattr( self.keystone_client, 'projects', self.keystone_client.tenants) + def _get_project_param_dict(self, name_or_id): + project_dict = dict() + if name_or_id: + project_id = self._get_project(name_or_id).id + if self.api_versions['identity'] == '3': + project_dict['default_project'] = project_id + else: + project_dict['tenant_id'] = project_id + return project_dict + + def _get_domain_param_dict(self, domain): + """Get a useable domain.""" + + # Keystone v3 requires domains for user and project creation. v2 does + # not. However, keystone v2 does not allow user creation by non-admin + # users, so we can throw an error to the user that does not need to + # mention api versions + if self.api_versions['identity'] == '3': + if not domain: + raise OpenStackCloudException( + "User creation requires an explicit domain argument.") + else: + return {'domain': self.get_identity_domain(domain).id} + else: + return {} + + def _get_identity_params(self, domain=None, project=None): + """Get the domain and project/tenant parameters if needed. + + keystone v2 and v3 are divergent enough that we need to pass or not + pass project or tenant_id or domain or nothing in a sane manner. + """ + ret = {} + ret.update(self._get_domain_param_dict(domain)) + ret.update(self._get_project_param_dict(project)) + return ret + def _get_project(self, name_or_id): """Retrieve a project by name or id.""" # TODO(mordred): This, and other keystone operations, need to have # domain information passed in. When there is no # available domain information, we should default to - # the currently scoped domain which we can requset from + # the currently scoped domain which we can request from # the session. for id, project in self.project_cache.items(): if name_or_id in (id, project.name): @@ -499,11 +536,14 @@ def update_project(self, name_or_id, description=None, enabled=True): "Error in updating project {project}: {message}".format( project=name_or_id, message=str(e))) - def create_project(self, name, description=None, enabled=True): + def create_project( + self, name, description=None, domain=None, enabled=True): """Create a project.""" try: + domain_params = self._get_domain_param_dict(domain) self._project_manager.create( - project_name=name, description=description, enabled=enabled) + project_name=name, description=description, enabled=enabled, + **domain_params) except Exception as e: self.log.debug("keystone create project issue", exc_info=True) raise OpenStackCloudException( @@ -512,7 +552,7 @@ def create_project(self, name, description=None, enabled=True): def delete_project(self, name_or_id): try: - project = self._get_project(name_or_id) + project = self.update_project(name_or_id, enabled=False) self._project_manager.delete(project.id) except Exception as e: self.log.debug("keystone delete project issue", exc_info=True) @@ -622,17 +662,15 @@ def update_user(self, name_or_id, email=None, enabled=None): return meta.obj_to_dict(user) def create_user( - self, name, password=None, email=None, project=None, - enabled=True): + self, name, password=None, email=None, default_project=None, + enabled=True, domain=None): """Create a user.""" try: - if project: - project_id = self._get_project(project).id - else: - project_id = None + identity_params = self._get_identity_params( + domain, default_project) user = self.manager.submitTask(_tasks.UserCreate( user_name=name, password=password, email=email, - project=project_id, enabled=enabled)) + enabled=enabled, **identity_params)) except Exception as e: self.log.debug("keystone create user issue", exc_info=True) raise OpenStackCloudException( diff --git a/shade/tests/unit/test_domains.py b/shade/tests/unit/test_domains.py new file mode 100644 index 000000000..7be44a3d5 --- /dev/null +++ b/shade/tests/unit/test_domains.py @@ -0,0 +1,83 @@ +# -*- 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. + +import mock + +import bunch + +import shade +from shade import exc +from shade.tests.unit import base + + +class TestDomains(base.TestCase): + + def setUp(self): + super(TestDomains, self).setUp() + self.cloud = shade.openstack_cloud() + + @mock.patch.object(shade.OpenStackCloud, 'get_identity_domain') + @mock.patch.object(shade.OpenStackCloud, '_get_project') + def test_identity_params_v3(self, mock_get_project, mock_get_domain): + mock_get_project.return_value = bunch.Bunch(id=1234) + mock_get_domain.return_value = bunch.Bunch(id=5678) + + self.cloud.api_versions = dict(identity='3') + + ret = self.cloud._get_identity_params(domain='foo', project='bar') + self.assertIn('default_project', ret) + self.assertEqual(ret['default_project'], 1234) + self.assertIn('domain', ret) + self.assertEqual(ret['domain'], 5678) + + @mock.patch.object(shade.OpenStackCloud, 'get_identity_domain') + @mock.patch.object(shade.OpenStackCloud, '_get_project') + def test_identity_params_v3_no_domain( + self, mock_get_project, mock_get_domain): + mock_get_project.return_value = bunch.Bunch(id=1234) + mock_get_domain.return_value = bunch.Bunch(id=5678) + + self.cloud.api_versions = dict(identity='3') + + self.assertRaises( + exc.OpenStackCloudException, + self.cloud._get_identity_params, + domain=None, project='bar') + + @mock.patch.object(shade.OpenStackCloud, 'get_identity_domain') + @mock.patch.object(shade.OpenStackCloud, '_get_project') + def test_identity_params_v2(self, mock_get_project, mock_get_domain): + mock_get_project.return_value = bunch.Bunch(id=1234) + mock_get_domain.return_value = bunch.Bunch(id=5678) + + self.cloud.api_versions = dict(identity='2') + + ret = self.cloud._get_identity_params(domain='foo', project='bar') + self.assertIn('tenant_id', ret) + self.assertEqual(ret['tenant_id'], 1234) + self.assertNotIn('domain', ret) + + @mock.patch.object(shade.OpenStackCloud, 'get_identity_domain') + @mock.patch.object(shade.OpenStackCloud, '_get_project') + def test_identity_params_v2_no_domain( + self, mock_get_project, mock_get_domain): + mock_get_project.return_value = bunch.Bunch(id=1234) + mock_get_domain.return_value = bunch.Bunch(id=5678) + + self.cloud.api_versions = dict(identity='2') + + ret = self.cloud._get_identity_params(domain=None, project='bar') + self.assertIn('tenant_id', ret) + self.assertEqual(ret['tenant_id'], 1234) + self.assertNotIn('domain', ret) From bfa1955e57b15d73f9d2e141794d53014de5b1b1 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 8 Jul 2015 11:51:17 -0400 Subject: [PATCH 0378/3836] Add a testing framework for the Ansible modules Change-Id: Ifa23d5b3fd72ba40a832399cdf2956d887e11cfb --- shade/tests/ansible/README.txt | 20 +++ .../ansible/roles/keypair/tasks/main.yml | 38 ++++++ .../tests/ansible/roles/keypair/vars/main.yml | 1 + .../roles/security_group/tasks/main.yml | 123 ++++++++++++++++++ .../roles/security_group/vars/main.yml | 1 + shade/tests/ansible/run.yml | 8 ++ 6 files changed, 191 insertions(+) create mode 100644 shade/tests/ansible/README.txt create mode 100644 shade/tests/ansible/roles/keypair/tasks/main.yml create mode 100644 shade/tests/ansible/roles/keypair/vars/main.yml create mode 100644 shade/tests/ansible/roles/security_group/tasks/main.yml create mode 100644 shade/tests/ansible/roles/security_group/vars/main.yml create mode 100644 shade/tests/ansible/run.yml diff --git a/shade/tests/ansible/README.txt b/shade/tests/ansible/README.txt new file mode 100644 index 000000000..fd255d5bf --- /dev/null +++ b/shade/tests/ansible/README.txt @@ -0,0 +1,20 @@ +This directory contains a testing infrastructure for the Ansible +OpenStack modules. You will need a clouds.yaml file in order to run +the tests. You must provide a value for the `cloud` variable for each +run (using the -e option) as a default is not currently provided. + + +Examples +-------- + +* Run all module tests against a provider: + + ansible-playbook run.yml -e "cloud=hp" + +* Run only the keypair and security_group tests: + + ansible-playbook run.yml -e "cloud=hp" --tags "keypair,security_group" + +* Run all tests except security_group: + + ansible-playbook run.yml -e "cloud=hp" --skip-tags "security_group" diff --git a/shade/tests/ansible/roles/keypair/tasks/main.yml b/shade/tests/ansible/roles/keypair/tasks/main.yml new file mode 100644 index 000000000..cc30d9624 --- /dev/null +++ b/shade/tests/ansible/roles/keypair/tasks/main.yml @@ -0,0 +1,38 @@ +--- +- name: Create keypair (non-existing) + os_keypair: + cloud: "{{ cloud }}" + name: "{{ keypair_name }}" + state: present + +- name: Delete keypair (non-existing) + os_keypair: + cloud: "{{ cloud }}" + name: "{{ keypair_name }}" + state: absent + +- name: Create keypair (file) + os_keypair: + cloud: "{{ cloud }}" + name: "{{ keypair_name }}" + state: present + public_key_file: "{{ ansible_env.HOME }}/.ssh/id_rsa.pub" + +- name: Delete keypair (file) + os_keypair: + cloud: "{{ cloud }}" + name: "{{ keypair_name }}" + state: absent + +- name: Create keypair (key) + os_keypair: + cloud: "{{ cloud }}" + name: "{{ keypair_name }}" + state: present + public_key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}" + +- name: Delete keypair (key) + os_keypair: + cloud: "{{ cloud }}" + name: "{{ keypair_name }}" + state: absent diff --git a/shade/tests/ansible/roles/keypair/vars/main.yml b/shade/tests/ansible/roles/keypair/vars/main.yml new file mode 100644 index 000000000..3956b56a2 --- /dev/null +++ b/shade/tests/ansible/roles/keypair/vars/main.yml @@ -0,0 +1 @@ +keypair_name: shade_keypair diff --git a/shade/tests/ansible/roles/security_group/tasks/main.yml b/shade/tests/ansible/roles/security_group/tasks/main.yml new file mode 100644 index 000000000..ddc7e50cd --- /dev/null +++ b/shade/tests/ansible/roles/security_group/tasks/main.yml @@ -0,0 +1,123 @@ +--- +- name: Create security group + os_security_group: + cloud: "{{ cloud }}" + name: "{{ secgroup_name }}" + state: present + description: Created from Ansible playbook + +- name: Create empty ICMP rule + os_security_group_rule: + cloud: "{{ cloud }}" + security_group: "{{ secgroup_name }}" + state: present + protocol: icmp + remote_ip_prefix: 0.0.0.0/0 + +- name: Create -1 ICMP rule + os_security_group_rule: + cloud: "{{ cloud }}" + security_group: "{{ secgroup_name }}" + state: present + protocol: icmp + port_range_min: -1 + port_range_max: -1 + remote_ip_prefix: 0.0.0.0/0 + +- name: Create empty TCP rule + os_security_group_rule: + cloud: "{{ cloud }}" + security_group: "{{ secgroup_name }}" + state: present + protocol: tcp + remote_ip_prefix: 0.0.0.0/0 + +- name: Create empty UDP rule + os_security_group_rule: + cloud: "{{ cloud }}" + security_group: "{{ secgroup_name }}" + state: present + protocol: udp + remote_ip_prefix: 0.0.0.0/0 + +- name: Create HTTP rule + os_security_group_rule: + cloud: "{{ cloud }}" + security_group: "{{ secgroup_name }}" + state: present + protocol: tcp + port_range_min: 80 + port_range_max: 80 + remote_ip_prefix: 0.0.0.0/0 + +- name: Create egress rule + os_security_group_rule: + cloud: "{{ cloud }}" + security_group: "{{ secgroup_name }}" + state: present + protocol: tcp + port_range_min: 30000 + port_range_max: 30001 + remote_ip_prefix: 0.0.0.0/0 + direction: egress + +- name: Delete empty ICMP rule + os_security_group_rule: + cloud: "{{ cloud }}" + security_group: "{{ secgroup_name }}" + state: absent + protocol: icmp + remote_ip_prefix: 0.0.0.0/0 + +- name: Delete -1 ICMP rule + os_security_group_rule: + cloud: "{{ cloud }}" + security_group: "{{ secgroup_name }}" + state: absent + protocol: icmp + port_range_min: -1 + port_range_max: -1 + remote_ip_prefix: 0.0.0.0/0 + +- name: Delete empty TCP rule + os_security_group_rule: + cloud: "{{ cloud }}" + security_group: "{{ secgroup_name }}" + state: absent + protocol: tcp + remote_ip_prefix: 0.0.0.0/0 + +- name: Delete empty UDP rule + os_security_group_rule: + cloud: "{{ cloud }}" + security_group: "{{ secgroup_name }}" + state: absent + protocol: udp + remote_ip_prefix: 0.0.0.0/0 + +- name: Delete HTTP rule + os_security_group_rule: + cloud: "{{ cloud }}" + security_group: "{{ secgroup_name }}" + state: absent + protocol: tcp + port_range_min: 80 + port_range_max: 80 + remote_ip_prefix: 0.0.0.0/0 + +- name: Delete egress rule + os_security_group_rule: + cloud: "{{ cloud }}" + security_group: "{{ secgroup_name }}" + state: absent + protocol: tcp + port_range_min: 30000 + port_range_max: 30001 + remote_ip_prefix: 0.0.0.0/0 + direction: egress + +- name: Delete security group + os_security_group: + cloud: "{{ cloud }}" + name: "{{ secgroup_name }}" + state: absent diff --git a/shade/tests/ansible/roles/security_group/vars/main.yml b/shade/tests/ansible/roles/security_group/vars/main.yml new file mode 100644 index 000000000..00310dd10 --- /dev/null +++ b/shade/tests/ansible/roles/security_group/vars/main.yml @@ -0,0 +1 @@ +secgroup_name: shade_secgroup diff --git a/shade/tests/ansible/run.yml b/shade/tests/ansible/run.yml new file mode 100644 index 000000000..879d0e705 --- /dev/null +++ b/shade/tests/ansible/run.yml @@ -0,0 +1,8 @@ +--- +- hosts: localhost + connection: local + gather_facts: true + + roles: + - { role: keypair, tags: keypair } + - { role: security_group, tags: security_group } From 9d3cc7969b7022ac850d98321fa038a09a70f939 Mon Sep 17 00:00:00 2001 From: Spencer Krum Date: Wed, 8 Jul 2015 10:25:51 -0700 Subject: [PATCH 0379/3836] Fix rendering issue in Readme Change-Id: If089b0331c6b40e983d81623ee3a6a541f93a45a --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index b265341a1..e2f75cd70 100644 --- a/README.rst +++ b/README.rst @@ -194,6 +194,7 @@ either in an environment variable, `OS_PREFER_IPV6`, or in the client section of the clouds.yaml. :: + client: prefer_ipv6: true clouds: From 8a2db7ba3fe2ec923f706fe6fcbb23c25429f6cd Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 8 Jul 2015 14:38:28 -0400 Subject: [PATCH 0380/3836] Add Ansible module test for networks Change-Id: I4e5a452de5952d8fa48bfa1831ac803642fc6ec5 --- shade/tests/ansible/roles/network/tasks/main.yml | 12 ++++++++++++ shade/tests/ansible/roles/network/vars/main.yml | 1 + shade/tests/ansible/run.yml | 1 + 3 files changed, 14 insertions(+) create mode 100644 shade/tests/ansible/roles/network/tasks/main.yml create mode 100644 shade/tests/ansible/roles/network/vars/main.yml diff --git a/shade/tests/ansible/roles/network/tasks/main.yml b/shade/tests/ansible/roles/network/tasks/main.yml new file mode 100644 index 000000000..fb6ca5726 --- /dev/null +++ b/shade/tests/ansible/roles/network/tasks/main.yml @@ -0,0 +1,12 @@ +--- +- name: Create network + os_network: + cloud: "{{ cloud }}" + name: "{{ network_name }}" + state: present + +- name: Delete network + os_network: + cloud: "{{ cloud }}" + name: "{{ network_name }}" + state: absent diff --git a/shade/tests/ansible/roles/network/vars/main.yml b/shade/tests/ansible/roles/network/vars/main.yml new file mode 100644 index 000000000..4b16af49d --- /dev/null +++ b/shade/tests/ansible/roles/network/vars/main.yml @@ -0,0 +1 @@ +network_name: shade_network diff --git a/shade/tests/ansible/run.yml b/shade/tests/ansible/run.yml index 879d0e705..b03bde01d 100644 --- a/shade/tests/ansible/run.yml +++ b/shade/tests/ansible/run.yml @@ -6,3 +6,4 @@ roles: - { role: keypair, tags: keypair } - { role: security_group, tags: security_group } + - { role: network, tags: network } From df195a30144ed9878aa09ff4d5adec4f02fa9ace Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 8 Jul 2015 15:20:51 -0400 Subject: [PATCH 0381/3836] Add Ansible module test for subnet Change-Id: Ia6303d4012cdbd8be226122cfddd46090c816cc0 --- .../tests/ansible/roles/subnet/tasks/main.yml | 35 +++++++++++++++++++ .../tests/ansible/roles/subnet/vars/main.yml | 1 + shade/tests/ansible/run.yml | 1 + 3 files changed, 37 insertions(+) create mode 100644 shade/tests/ansible/roles/subnet/tasks/main.yml create mode 100644 shade/tests/ansible/roles/subnet/vars/main.yml diff --git a/shade/tests/ansible/roles/subnet/tasks/main.yml b/shade/tests/ansible/roles/subnet/tasks/main.yml new file mode 100644 index 000000000..0affe1605 --- /dev/null +++ b/shade/tests/ansible/roles/subnet/tasks/main.yml @@ -0,0 +1,35 @@ +--- +- include_vars: roles/network/vars/main.yml + +- name: Create network {{ network_name }} + os_network: + cloud: "{{ cloud }}" + name: "{{ network_name }}" + state: present + +- name: Create subnet {{ subnet_name }} on network {{ network_name }} + os_subnet: + cloud: "{{ cloud }}" + network_name: "{{ network_name }}" + name: "{{ subnet_name }}" + state: present + enable_dhcp: false + dns_nameservers: + - 8.8.8.7 + - 8.8.8.8 + cidr: 192.168.0.0/24 + gateway_ip: 192.168.0.1 + allocation_pool_start: 192.168.0.2 + allocation_pool_end: 192.168.0.254 + +- name: Delete subnet {{ subnet_name }} + os_subnet: + cloud: "{{ cloud }}" + name: "{{ subnet_name }}" + state: absent + +- name: Delete network {{ network_name }} + os_network: + cloud: "{{ cloud }}" + name: "{{ network_name }}" + state: absent diff --git a/shade/tests/ansible/roles/subnet/vars/main.yml b/shade/tests/ansible/roles/subnet/vars/main.yml new file mode 100644 index 000000000..b9df9212a --- /dev/null +++ b/shade/tests/ansible/roles/subnet/vars/main.yml @@ -0,0 +1 @@ +subnet_name: shade_subnet diff --git a/shade/tests/ansible/run.yml b/shade/tests/ansible/run.yml index b03bde01d..38e038840 100644 --- a/shade/tests/ansible/run.yml +++ b/shade/tests/ansible/run.yml @@ -7,3 +7,4 @@ - { role: keypair, tags: keypair } - { role: security_group, tags: security_group } - { role: network, tags: network } + - { role: subnet, tags: subnet} From 7ad8db93391f75ee2ecd0361275941fd80237add Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Thu, 9 Jul 2015 03:11:17 +0100 Subject: [PATCH 0382/3836] Fix "Bad floatingip request" when multiple fixed IPs are present This patch fixes the "Bad floatingip request" error raised when there are multiple fixed address assigned to a port. The issue is fixed using the fixed_address provided as a parameter of _neutron_attach_ip_to_server() when updating the neutron floating IP. Change-Id: I6bcc000912ee75c9507c3d7e790ffbe10b3b271a --- shade/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index 71da75894..9aec6599f 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2147,9 +2147,13 @@ def _neutron_attach_ip_to_server(self, server_id, floating_ip_id, raise OpenStackCloudException( "unable to find a port for server {0}".format(server_id)) + floating_ip = {'port_id': port['id']} + if fixed_address is not None: + floating_ip['fixed_ip_address'] = fixed_address + return self.manager.submitTask(_tasks.NeutronFloatingIPUpdate( floatingip=floating_ip_id, - body={'floatingip': {'port_id': port['id']}} + body={'floatingip': floating_ip} ))['floatingip'] def _nova_attach_ip_to_server(self, server_id, floating_ip_id, From 1065ea4dbf91a1416e267223d0db96719490653b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 4 Jul 2015 10:55:25 -0400 Subject: [PATCH 0383/3836] Add support for configuring region lists with yaml yaml supports encoding lists in a manner that is cleaner than comma separated lists. While the old commas-in-region_name will still work, add a 'regions' option that can be used to configure lists of regions. Remove the comma-separated-list from the docs so that people don't try to use it - even though it will work. Change-Id: Ieb0aedb9c03fd26e644e9ba733b935f2c69daaf0 --- README.rst | 7 ++++-- os_client_config/config.py | 24 +++++++++++++++------ os_client_config/tests/base.py | 11 ++++++++++ os_client_config/tests/test_config.py | 26 ++++++++++++++++++++++- os_client_config/vendors/hp.yaml | 4 +++- os_client_config/vendors/rackspace.yaml | 7 +++++- os_client_config/vendors/runabove.yaml | 4 +++- os_client_config/vendors/unitedstack.yaml | 4 +++- 8 files changed, 73 insertions(+), 14 deletions(-) diff --git a/README.rst b/README.rst index b265341a1..19927135c 100644 --- a/README.rst +++ b/README.rst @@ -105,7 +105,10 @@ An example config file is probably helpful: username: openstackci password: XXXXXXXX project_id: 610275 - region_name: DFW,ORD,IAD + regions: + - DFW + - ORD + - IAD You may note a few things. First, since `auth_url` settings are silly and embarrasingly ugly, known cloud vendor profile information is included and @@ -116,7 +119,7 @@ knows that so that you don't have to. In case the cloud vendor profile is not available, you can provide one called `clouds-public.yaml`, following the same location rules previously mentioned for the config files. -Also, `region_name` can be a list of regions. When you call `get_all_clouds`, +`regions` can be a list of regions. When you call `get_all_clouds`, you'll get a cloud config object for each cloud/region combo. As seen with `dns_service_type`, any setting that makes sense to be per-service, diff --git a/os_client_config/config.py b/os_client_config/config.py index 55328cc0e..93d7aa0b8 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -199,14 +199,24 @@ def get_cache_arguments(self): return self._cache_arguments def _get_regions(self, cloud): - try: - return self.cloud_config['clouds'][cloud]['region_name'] - except KeyError: - # No region configured - return '' + if cloud not in self.cloud_config['clouds']: + return [''] + config = self._normalize_keys(self.cloud_config['clouds'][cloud]) + if 'regions' in config: + return config['regions'] + elif 'region_name' in config: + regions = config['region_name'].split(',') + if len(regions) > 1: + warnings.warn( + "Comma separated lists in region_name are deprecated." + " Please use a yaml list in the regions" + " parameter in {0} instead.".format(self.config_filename)) + return regions + else: + return [''] def _get_region(self, cloud=None): - return self._get_regions(cloud).split(',')[0] + return self._get_regions(cloud)[0] def get_cloud_names(self): return self.cloud_config['clouds'].keys() @@ -301,7 +311,7 @@ def get_all_clouds(self): clouds = [] for cloud in self.get_cloud_names(): - for region in self._get_regions(cloud).split(','): + for region in self._get_regions(cloud): clouds.append(self.get_one_cloud(cloud, region_name=region)) return clouds diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py index 8bb9145d2..569b52ea7 100644 --- a/os_client_config/tests/base.py +++ b/os_client_config/tests/base.py @@ -67,6 +67,17 @@ }, 'region_name': 'test-region', }, + '_test_cloud_regions': { + 'auth': { + 'username': 'testuser', + 'password': 'testpass', + 'project-id': 'testproject', + }, + 'regions': [ + 'region1', + 'region2', + ], + }, '_test_cloud_hyphenated': { 'auth': { 'username': 'testuser', diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 4c244d2a4..dde7d83c7 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -32,7 +32,11 @@ def test_get_all_clouds(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) clouds = c.get_all_clouds() - user_clouds = [cloud for cloud in base.USER_CONF['clouds'].keys()] + # We add one by hand because the regions cloud is going to exist + # twice since it has two regions in it + user_clouds = [ + cloud for cloud in base.USER_CONF['clouds'].keys() + ] + ['_test_cloud_regions'] configured_clouds = [cloud.name for cloud in clouds] self.assertItemsEqual(user_clouds, configured_clouds) @@ -132,6 +136,7 @@ def test_get_cloud_names(self): '_test-cloud_', '_test_cloud_hyphenated', '_test_cloud_no_vendor', + '_test_cloud_regions', ], sorted(c.get_cloud_names())) c = config.OpenStackConfig(config_files=[self.no_yaml], @@ -216,6 +221,25 @@ def test_get_one_cloud_no_argparse(self): self.assertEqual(cc.region_name, 'test-region') self.assertIsNone(cc.snack_type) + def test_get_one_cloud_no_argparse_regions(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + + cc = c.get_one_cloud(cloud='_test_cloud_regions', argparse=None) + self._assert_cloud_details(cc) + self.assertEqual(cc.region_name, 'region1') + self.assertIsNone(cc.snack_type) + + def test_get_one_cloud_no_argparse_region2(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + + cc = c.get_one_cloud( + cloud='_test_cloud_regions', region_name='region2', argparse=None) + self._assert_cloud_details(cc) + self.assertEqual(cc.region_name, 'region2') + self.assertIsNone(cc.snack_type) + def test_fix_env_args(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) diff --git a/os_client_config/vendors/hp.yaml b/os_client_config/vendors/hp.yaml index 277343325..4da49568d 100644 --- a/os_client_config/vendors/hp.yaml +++ b/os_client_config/vendors/hp.yaml @@ -2,5 +2,7 @@ name: hp profile: auth: auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0 - region_name: region-a.geo-1,region-b.geo-1 + regions: + - region-a.geo-1 + - region-b.geo-1 dns_service_type: hpext:dns diff --git a/os_client_config/vendors/rackspace.yaml b/os_client_config/vendors/rackspace.yaml index a350584c4..ad7825dec 100644 --- a/os_client_config/vendors/rackspace.yaml +++ b/os_client_config/vendors/rackspace.yaml @@ -2,7 +2,12 @@ name: rackspace profile: auth: auth_url: https://identity.api.rackspacecloud.com/v2.0/ - region_name: DFW,ORD,IAD,SYD,HKG + regions: + - DFW + - HKG + - IAD + - ORD + - SYD database_service_type: rax:database compute_service_name: cloudServersOpenStack image_api_version: '2' diff --git a/os_client_config/vendors/runabove.yaml b/os_client_config/vendors/runabove.yaml index 381020ab8..57e75f37a 100644 --- a/os_client_config/vendors/runabove.yaml +++ b/os_client_config/vendors/runabove.yaml @@ -2,7 +2,9 @@ name: runabove profile: auth: auth_url: https://auth.runabove.io/v2.0 - region_name: SBG-1,BHS-1 + regions: + - BHS-1 + - SBG-1 image_api_version: '2' image_format: qcow2 floating_ip_source: None diff --git a/os_client_config/vendors/unitedstack.yaml b/os_client_config/vendors/unitedstack.yaml index db9b61239..f22bd8abe 100644 --- a/os_client_config/vendors/unitedstack.yaml +++ b/os_client_config/vendors/unitedstack.yaml @@ -2,7 +2,9 @@ name: unitedstack profile: auth: auth_url: https://identity.api.ustack.com/v3 - region_name: bj1,gd1 + regions: + - bj1 + - gd1 identity_api_version: '3' image_api_version: '2' image_format: raw From 6523cf62fa369fe1909a951ac7bce465aa222c06 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 4 Jul 2015 11:03:07 -0400 Subject: [PATCH 0384/3836] Specify the config file with environment variable The fine folks at ansible want to be able to specify a specific location for the config file with an env var which seems like a perfectly reasonable thing to allow. Inject the specified file at the beginning of the list so that it'll be the first one found. Change-Id: Ib1947be1c0ae812e9cb83c7b99168c05dfc6fa6a Co-authored-by: Chris Church --- README.rst | 4 ++++ os_client_config/config.py | 4 ++++ os_client_config/tests/test_environ.py | 9 +++++++++ 3 files changed, 17 insertions(+) diff --git a/README.rst b/README.rst index 19927135c..13e6f2a51 100644 --- a/README.rst +++ b/README.rst @@ -43,6 +43,10 @@ locations: The first file found wins. +You can also set the environment variable `OS_CLIENT_CONFIG_FILE` to an +absolute path of a file to look for and that location will be inserted at the +front of the file search list. + The keys are all of the keys you'd expect from `OS_*` - except lower case and without the OS prefix. So, region name is set with `region_name`. diff --git a/os_client_config/config.py b/os_client_config/config.py index 93d7aa0b8..203772e21 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -106,6 +106,10 @@ def __init__(self, config_files=None, vendor_files=None, self._config_files = config_files or CONFIG_FILES self._vendor_files = vendor_files or VENDOR_FILES + config_file_override = os.environ.pop('OS_CLIENT_CONFIG_FILE', None) + if config_file_override: + self._config_files.insert(0, config_file_override) + self.defaults = defaults.get_defaults() if override_defaults: self.defaults.update(override_defaults) diff --git a/os_client_config/tests/test_environ.py b/os_client_config/tests/test_environ.py index ff44afc32..7f284c5eb 100644 --- a/os_client_config/tests/test_environ.py +++ b/os_client_config/tests/test_environ.py @@ -84,3 +84,12 @@ def test_get_one_cloud_with_config_files(self): self._assert_cloud_details(cc) cc = c.get_one_cloud('_test_cloud_no_vendor') self._assert_cloud_details(cc) + + def test_config_file_override(self): + self.useFixture( + fixtures.EnvironmentVariable( + 'OS_CLIENT_CONFIG_FILE', self.cloud_yaml)) + c = config.OpenStackConfig(config_files=[], + vendor_files=[self.vendor_yaml]) + cc = c.get_one_cloud('_test-cloud_') + self._assert_cloud_details(cc) From 7965fd2a24c4875fdea0843dae6ed33b4ee010d1 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 8 Jul 2015 10:51:38 -0400 Subject: [PATCH 0385/3836] Rework how we get domains Listing domains is very expensive and to be avoided. It's also only a thing that really an admin is going to do. Go directly to the API for specific domain get calls, rather than doing a list and filter because of the cost. Change-Id: Icfcb08fb87a37287491c69c6e2b416ec810b754b --- shade/__init__.py | 140 +++++++++--------- shade/_tasks.py | 6 + shade/tests/fakes.py | 8 + ...{test_domains.py => test_domain_params.py} | 32 ++-- shade/tests/unit/test_identity_domains.py | 48 ++++++ 5 files changed, 146 insertions(+), 88 deletions(-) rename shade/tests/unit/{test_domains.py => test_domain_params.py} (61%) create mode 100644 shade/tests/unit/test_identity_domains.py diff --git a/shade/__init__.py b/shade/__init__.py index 71da75894..9461ee44f 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -478,7 +478,7 @@ def _get_project_param_dict(self, name_or_id): project_dict['tenant_id'] = project_id return project_dict - def _get_domain_param_dict(self, domain): + def _get_domain_param_dict(self, domain_id): """Get a useable domain.""" # Keystone v3 requires domains for user and project creation. v2 does @@ -486,22 +486,22 @@ def _get_domain_param_dict(self, domain): # users, so we can throw an error to the user that does not need to # mention api versions if self.api_versions['identity'] == '3': - if not domain: + if not domain_id: raise OpenStackCloudException( - "User creation requires an explicit domain argument.") + "User creation requires an explicit domain_id argument.") else: - return {'domain': self.get_identity_domain(domain).id} + return {'domain': domain_id} else: return {} - def _get_identity_params(self, domain=None, project=None): + def _get_identity_params(self, domain_id=None, project=None): """Get the domain and project/tenant parameters if needed. keystone v2 and v3 are divergent enough that we need to pass or not pass project or tenant_id or domain or nothing in a sane manner. """ ret = {} - ret.update(self._get_domain_param_dict(domain)) + ret.update(self._get_domain_param_dict(domain_id)) ret.update(self._get_project_param_dict(project)) return ret @@ -537,10 +537,10 @@ def update_project(self, name_or_id, description=None, enabled=True): project=name_or_id, message=str(e))) def create_project( - self, name, description=None, domain=None, enabled=True): + self, name, description=None, domain_id=None, enabled=True): """Create a project.""" try: - domain_params = self._get_domain_param_dict(domain) + domain_params = self._get_domain_param_dict(domain_id) self._project_manager.create( project_name=name, description=description, enabled=enabled, **domain_params) @@ -560,60 +560,6 @@ def delete_project(self, name_or_id): "Error in deleting project {project}: {message}".format( project=name_or_id, message=str(e))) - def list_identity_domains(self): - """List Keystone domains. - - :returns: a list of dicts containing the domain description. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - # ToDo: support v3 api (dguerri) - try: - domains = self.manager.submitTask(_tasks.IdentityDomainList()) - except Exception as e: - self.log.debug("Failed to list domains", exc_info=True) - raise OpenStackCloudException(str(e)) - return meta.obj_list_to_dict(domains) - - def search_indentity_domains(self, name_or_id=None, filters=None): - """Seach Keystone domains. - - :param id: domain name or id. - :param filters: a dict containing additional filters to use. e.g. - {'enabled': False} - - :returns: a list of dict containing the domain description. Each dict - contains the following attributes:: - - id: - - name: - - description: - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - domains = self.list_identity_domains() - return _utils._filter_list(domains, name_or_id, filters) - - def get_identity_domain(self, name_or_id, filters=None): - """Get exactly one Keystone domain. - - :param id: domain name or id. - :param filters: a dict containing additional filters to use. e.g. - {'enabled': True} - - :returns: a list of dict containing the domain description. Each dict - contains the following attributes:: - - id: - - name: - - description: - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - return _utils._get_entity( - self.search_identity_domains, name_or_id, filters) - @property def user_cache(self): return self.get_user_cache() @@ -663,11 +609,11 @@ def update_user(self, name_or_id, email=None, enabled=None): def create_user( self, name, password=None, email=None, default_project=None, - enabled=True, domain=None): + enabled=True, domain_id=None): """Create a user.""" try: identity_params = self._get_identity_params( - domain, default_project) + domain_id, default_project) user = self.manager.submitTask(_tasks.UserCreate( user_name=name, password=password, email=email, enabled=enabled, **identity_params)) @@ -4199,12 +4145,11 @@ def create_identity_domain( return meta.obj_to_dict(domain) def update_identity_domain( - self, name_or_id, name=None, description=None, enabled=None): + self, domain_id, name=None, description=None, enabled=None): try: - domain = self.get_identity_domain(name_or_id) return meta.obj_to_dict( self.manager.submitTask(_tasks.IdentityDomainUpdate( - domain=domain.id, description=description, + domain=domain_id, description=description, enabled=enabled))) except Exception as e: self.log.debug("keystone update domain issue", exc_info=True) @@ -4233,3 +4178,64 @@ def delete_identity_domain(self, name_or_id): "Failed to delete domain {id}".format(id=domain.id), exc_info=True) raise OpenStackCloudException(str(e)) + + def list_identity_domains(self): + """List Keystone domains. + + :returns: a list of dicts containing the domain description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + try: + domains = self.manager.submitTask(_tasks.IdentityDomainList()) + except Exception as e: + self.log.debug("Failed to list domains", exc_info=True) + raise OpenStackCloudException(str(e)) + return meta.obj_list_to_dict(domains) + + def search_identity_domains(self, **filters): + """Seach Keystone domains. + + :param filters: a dict containing additional filters to use. + keys to search on are id, name, enabled and description. + + :returns: a list of dicts containing the domain description. Each dict + contains the following attributes:: + - id: + - name: + - description: + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + try: + domains = self.manager.submitTask( + _tasks.IdentityDomainList(**filters)) + except Exception as e: + self.log.debug("Failed to list domains", exc_info=True) + raise OpenStackCloudException(str(e)) + return meta.obj_list_to_dict(domains) + + def get_identity_domain(self, domain_id): + """Get exactly one Keystone domain. + + :param domain_id: domain id. + + :returns: a dict containing the domain description, or None if not + found. Each dict contains the following attributes:: + - id: + - name: + - description: + + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + try: + domain = self.manager.submitTask( + _tasks.IdentityDomainGet(domain=domain_id)) + except Exception as e: + self.log.debug("Failed to get domain", exc_info=True) + raise OpenStackCloudException(str(e)) + return meta.obj_to_dict(domain) diff --git a/shade/_tasks.py b/shade/_tasks.py index 81b92ad72..45be562ed 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -509,6 +509,12 @@ def main(self, client): return client.keystone_client.domains.list() +# IdentityDomain and not Domain because Domain is a DNS concept +class IdentityDomainGet(task_manager.Task): + def main(self, client): + return client.keystone_client.domains.get(**self.args) + + # IdentityDomain and not Domain because Domain is a DNS concept class IdentityDomainUpdate(task_manager.Task): def main(self, client): diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 4573c724b..7b72dfc92 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -144,3 +144,11 @@ def __init__(self, id, name, public_key): self.id = id self.name = name self.public_key = public_key + + +class FakeIdentityDomain(object): + def __init__(self, id, name, description, enabled): + self.id = id + self.name = name + self.description = description + self.enabled = enabled diff --git a/shade/tests/unit/test_domains.py b/shade/tests/unit/test_domain_params.py similarity index 61% rename from shade/tests/unit/test_domains.py rename to shade/tests/unit/test_domain_params.py index 7be44a3d5..56bbcb9d6 100644 --- a/shade/tests/unit/test_domains.py +++ b/shade/tests/unit/test_domain_params.py @@ -21,63 +21,53 @@ from shade.tests.unit import base -class TestDomains(base.TestCase): +class TestDomainParams(base.TestCase): def setUp(self): - super(TestDomains, self).setUp() + super(TestDomainParams, self).setUp() self.cloud = shade.openstack_cloud() - @mock.patch.object(shade.OpenStackCloud, 'get_identity_domain') @mock.patch.object(shade.OpenStackCloud, '_get_project') - def test_identity_params_v3(self, mock_get_project, mock_get_domain): + def test_identity_params_v3(self, mock_get_project): mock_get_project.return_value = bunch.Bunch(id=1234) - mock_get_domain.return_value = bunch.Bunch(id=5678) self.cloud.api_versions = dict(identity='3') - ret = self.cloud._get_identity_params(domain='foo', project='bar') + ret = self.cloud._get_identity_params(domain_id='5678', project='bar') self.assertIn('default_project', ret) self.assertEqual(ret['default_project'], 1234) self.assertIn('domain', ret) - self.assertEqual(ret['domain'], 5678) + self.assertEqual(ret['domain'], '5678') - @mock.patch.object(shade.OpenStackCloud, 'get_identity_domain') @mock.patch.object(shade.OpenStackCloud, '_get_project') - def test_identity_params_v3_no_domain( - self, mock_get_project, mock_get_domain): + def test_identity_params_v3_no_domain(self, mock_get_project): mock_get_project.return_value = bunch.Bunch(id=1234) - mock_get_domain.return_value = bunch.Bunch(id=5678) self.cloud.api_versions = dict(identity='3') self.assertRaises( exc.OpenStackCloudException, self.cloud._get_identity_params, - domain=None, project='bar') + domain_id=None, project='bar') - @mock.patch.object(shade.OpenStackCloud, 'get_identity_domain') @mock.patch.object(shade.OpenStackCloud, '_get_project') - def test_identity_params_v2(self, mock_get_project, mock_get_domain): + def test_identity_params_v2(self, mock_get_project): mock_get_project.return_value = bunch.Bunch(id=1234) - mock_get_domain.return_value = bunch.Bunch(id=5678) self.cloud.api_versions = dict(identity='2') - ret = self.cloud._get_identity_params(domain='foo', project='bar') + ret = self.cloud._get_identity_params(domain_id='foo', project='bar') self.assertIn('tenant_id', ret) self.assertEqual(ret['tenant_id'], 1234) self.assertNotIn('domain', ret) - @mock.patch.object(shade.OpenStackCloud, 'get_identity_domain') @mock.patch.object(shade.OpenStackCloud, '_get_project') - def test_identity_params_v2_no_domain( - self, mock_get_project, mock_get_domain): + def test_identity_params_v2_no_domain(self, mock_get_project): mock_get_project.return_value = bunch.Bunch(id=1234) - mock_get_domain.return_value = bunch.Bunch(id=5678) self.cloud.api_versions = dict(identity='2') - ret = self.cloud._get_identity_params(domain=None, project='bar') + ret = self.cloud._get_identity_params(domain_id=None, project='bar') self.assertIn('tenant_id', ret) self.assertEqual(ret['tenant_id'], 1234) self.assertNotIn('domain', ret) diff --git a/shade/tests/unit/test_identity_domains.py b/shade/tests/unit/test_identity_domains.py new file mode 100644 index 000000000..83b53a489 --- /dev/null +++ b/shade/tests/unit/test_identity_domains.py @@ -0,0 +1,48 @@ +# Copyright (c) 2015 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. + +import mock + +import shade +from shade.tests.unit import base +from shade.tests import fakes + + +domain_obj = fakes.FakeIdentityDomain( + id='1', + name='a-domain', + description='A wonderful keystone domain', + enabled=True, +) + + +class TestIdentityDomains(base.TestCase): + + def setUp(self): + super(TestIdentityDomains, self).setUp() + self.cloud = shade.operator_cloud() + + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_list_identity_domains(self, mock_keystone): + self.cloud.list_identity_domains() + self.assertTrue(mock_keystone.domains.list.called) + + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_get_identity_domain(self, mock_keystone): + mock_keystone.domains.get.return_value = domain_obj + domain = self.cloud.get_identity_domain(domain_id='1234') + self.assertFalse(mock_keystone.domains.list.called) + self.assertTrue(mock_keystone.domains.get.called) + self.assertEqual(domain['name'], 'a-domain') From ab1c566cb8afbf477492180e2cc8257817a06893 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 10 Jul 2015 08:09:21 -0400 Subject: [PATCH 0386/3836] Update ansible module playbooks - Removed unnecessary variable import from subnet role. - Add client_config role. - Add os_auth role. Change-Id: Ie42a4a26589807ec47cc081d72350ada11b076fe --- shade/tests/ansible/roles/auth/tasks/main.yml | 6 ++++++ shade/tests/ansible/roles/client_config/tasks/main.yml | 7 +++++++ shade/tests/ansible/roles/subnet/tasks/main.yml | 2 -- shade/tests/ansible/run.yml | 4 +++- 4 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 shade/tests/ansible/roles/auth/tasks/main.yml create mode 100644 shade/tests/ansible/roles/client_config/tasks/main.yml diff --git a/shade/tests/ansible/roles/auth/tasks/main.yml b/shade/tests/ansible/roles/auth/tasks/main.yml new file mode 100644 index 000000000..ca894e50a --- /dev/null +++ b/shade/tests/ansible/roles/auth/tasks/main.yml @@ -0,0 +1,6 @@ +--- +- name: Authenticate to the cloud + os_auth: + cloud={{ cloud }} + +- debug: var=service_catalog diff --git a/shade/tests/ansible/roles/client_config/tasks/main.yml b/shade/tests/ansible/roles/client_config/tasks/main.yml new file mode 100644 index 000000000..1506f6d69 --- /dev/null +++ b/shade/tests/ansible/roles/client_config/tasks/main.yml @@ -0,0 +1,7 @@ +--- +- name: List all profiles + os_client_config: + register: list + +# WARNING: This will output sensitive authentication information!!!! +- debug: var=list diff --git a/shade/tests/ansible/roles/subnet/tasks/main.yml b/shade/tests/ansible/roles/subnet/tasks/main.yml index 0affe1605..4d68c57d2 100644 --- a/shade/tests/ansible/roles/subnet/tasks/main.yml +++ b/shade/tests/ansible/roles/subnet/tasks/main.yml @@ -1,6 +1,4 @@ --- -- include_vars: roles/network/vars/main.yml - - name: Create network {{ network_name }} os_network: cloud: "{{ cloud }}" diff --git a/shade/tests/ansible/run.yml b/shade/tests/ansible/run.yml index 38e038840..7e927b2ac 100644 --- a/shade/tests/ansible/run.yml +++ b/shade/tests/ansible/run.yml @@ -4,7 +4,9 @@ gather_facts: true roles: + - { role: auth, tags: auth } + - { role: client_config, tags: client_config } - { role: keypair, tags: keypair } - - { role: security_group, tags: security_group } - { role: network, tags: network } + - { role: security_group, tags: security_group } - { role: subnet, tags: subnet} From 5709ca8f28d67fe648924be8c9a2562b75695134 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 10 Jul 2015 15:10:25 -0400 Subject: [PATCH 0387/3836] Fix identity domain methods Some bugs made it through the review process. The delete_identity_domain() method was calling an invalid update method. It also should only accept a domain ID, not name, since converting from a domain name to ID would require listing the domains (an expensive operation we are trying to avoid). Change-Id: Iccc89f133b4122d88bc77f72d89ae6c12eff1eb9 --- shade/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 3f6afb3a4..c3c898608 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -4159,12 +4159,12 @@ def update_identity_domain( self.log.debug("keystone update domain issue", exc_info=True) raise OpenStackCloudException( "Error in updating domain {domain}: {message}".format( - domain=name_or_id, message=str(e))) + domain=domain_id, message=str(e))) - def delete_identity_domain(self, name_or_id): + def delete_identity_domain(self, domain_id): """Delete a Keystone domain. - :param id: Name or id of the domain to delete. + :param domain_id: ID of the domain to delete. :returns: None @@ -4174,12 +4174,12 @@ def delete_identity_domain(self, name_or_id): try: # Deleting a domain is expensive, so disabling it first increases # the changes of success - domain = self.update_domain(name_or_id, enabled=False) + domain = self.update_identity_domain(domain_id, enabled=False) self.manager.submitTask(_tasks.IdentityDomainDelete( - domain=domain.id)) + domain=domain['id'])) except Exception as e: self.log.debug( - "Failed to delete domain {id}".format(id=domain.id), + "Failed to delete domain {id}".format(id=domain_id), exc_info=True) raise OpenStackCloudException(str(e)) From 9ac1517f287642bd1b2c6bddda2ab613d6b94597 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 7 Jul 2015 14:01:50 -0400 Subject: [PATCH 0388/3836] Allow use of admin tokens in keystone For bootstrapping purposes, an admin uses an "admin token" which is a statically configured secret in the keystone config file. If one has used this, there are things one cannot do, such as query the catalog. That's ok - because what you're doing is bootstrapping the contents of the catalog. In order to enable that, we need to work around the normal "just infer the right thing from the catalog" logic, because there is no catalog. Also - it turns out that direct use of the v2 and v3 clients allows one to just pass in a Session, rather than needing to query the session for the auth_url to pass in. This does not hurt the non-admin cases. Change-Id: I7e15214a589dd3cb270f4835e074dc13cab3e529 --- shade/__init__.py | 37 ++++++++++++++++++++-------- shade/tests/unit/test_shade.py | 44 ++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 10 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 3f6afb3a4..f02c08e04 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -28,7 +28,9 @@ from ironicclient import exceptions as ironic_exceptions import jsonpatch from keystoneclient import auth as ksc_auth -from keystoneclient import client as keystone_client +from keystoneclient.auth import token_endpoint +from keystoneclient.v2_0 import client as k2_client +from keystoneclient.v3 import client as k3_client from keystoneclient import exceptions as keystone_exceptions from keystoneclient import session as ksc_session from novaclient import client as nova_client @@ -395,16 +397,32 @@ def nova_client(self): return self._nova_client + def _get_auth_plugin_class(self): + try: + if self.auth_type == 'token_endpoint': + return token_endpoint.Token + else: + return ksc_auth.get_plugin_class(self.auth_type) + except Exception as e: + self.log.debug("keystone auth plugin failure", exc_info=True) + raise OpenStackCloudException( + "Could not find auth plugin: {plugin} {error}".format( + plugin=self.auth_type, error=str(e))) + + def _get_identity_client_class(self): + if self.api_versions['identity'] == '3': + return k3_client.Client + elif self.api_versions['identity'] in ('2', '2.0'): + return k2_client.Client + raise OpenStackCloudException( + "Unknown identity API version: {version}".format( + version=self.api_versions['identity'])) + @property def keystone_session(self): if self._keystone_session is None: - try: - auth_plugin = ksc_auth.get_plugin_class(self.auth_type) - except Exception as e: - self.log.debug("keystone auth plugin failure", exc_info=True) - raise OpenStackCloudException( - "Could not find auth plugin: {plugin}".format( - plugin=self.auth_type)) + + auth_plugin = self._get_auth_plugin_class() try: keystone_auth = auth_plugin(**self.auth) except Exception as e: @@ -429,9 +447,8 @@ def keystone_session(self): def keystone_client(self): if self._keystone_client is None: try: - self._keystone_client = keystone_client.Client( + self._keystone_client = self._get_identity_client_class()( session=self.keystone_session, - auth_url=self.get_session_endpoint('identity'), timeout=self.api_timeout) except Exception as e: self.log.debug( diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index baa9ac23d..66f6d3ca3 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -15,6 +15,10 @@ import mock import glanceclient +import keystoneclient.auth.identity.generic.password +import keystoneclient.auth.token_endpoint +from keystoneclient.v2_0 import client as k2_client +from keystoneclient.v3 import client as k3_client from neutronclient.common import exceptions as n_exc import shade @@ -32,6 +36,46 @@ def setUp(self): def test_openstack_cloud(self): self.assertIsInstance(self.cloud, shade.OpenStackCloud) + def test_get_auth_token_endpoint(self): + self.cloud.auth_type = 'token_endpoint' + plugin = self.cloud._get_auth_plugin_class() + + self.assertIs(plugin, keystoneclient.auth.token_endpoint.Token) + + def test_get_auth_bogus(self): + self.cloud.auth_type = 'bogus' + self.assertRaises( + exc.OpenStackCloudException, + self.cloud._get_auth_plugin_class) + + def test_get_auth_password(self): + plugin = self.cloud._get_auth_plugin_class() + + self.assertIs( + plugin, + keystoneclient.auth.identity.generic.password.Password) + + def test_get_client_v2(self): + self.cloud.api_versions['identity'] = '2' + + self.assertIs( + self.cloud._get_identity_client_class(), + k2_client.Client) + + def test_get_client_v3(self): + self.cloud.api_versions['identity'] = '3' + + self.assertIs( + self.cloud._get_identity_client_class(), + k3_client.Client) + + def test_get_client_v4(self): + self.cloud.api_versions['identity'] = '4' + + self.assertRaises( + exc.OpenStackCloudException, + self.cloud._get_identity_client_class) + @mock.patch.object(shade.OpenStackCloud, 'search_images') def test_get_images(self, mock_search): image1 = dict(id='123', name='mickey') From b74df460a821d522d78ee76ba699bc4265efa2bc Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 14 Jul 2015 10:54:21 -0400 Subject: [PATCH 0389/3836] Fix set_default() when used before config init Using set_default() before initializing OpenStackConfig would cause an error since the _defaults dict would still be None and not an actual dict. This corrects that by calling get_defaults() to make sure it is initialized properly, and also adds a warning to note that the method is now deprecated. Change-Id: I81803c680b614f9bee47c6f69a4efffa638dcebc --- os_client_config/config.py | 5 +++++ os_client_config/tests/test_config.py | 19 ++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 203772e21..89503e752 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -64,6 +64,11 @@ # Remove this sometime in June 2015 once OSC is comfortably # changed-over and global-defaults is updated. def set_default(key, value): + warnings.warn( + "Use of set_default() is deprecated. Defaults should be set with the " + "`override_defaults` parameter of OpenStackConfig." + ) + defaults.get_defaults() # make sure the dict is initialized defaults._defaults[key] = value diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index dde7d83c7..1444ed557 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -252,9 +252,26 @@ def test_fix_env_args(self): class TestConfigDefault(base.TestCase): + def setUp(self): + super(TestConfigDefault, self).setUp() + + # Reset defaults after each test so that other tests are + # not affected by any changes. + self.addCleanup(self._reset_defaults) + + def _reset_defaults(self): + defaults._defaults = None + def test_set_no_default(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) cc = c.get_one_cloud(cloud='_test-cloud_', argparse=None) self._assert_cloud_details(cc) - self.assertEqual(cc.auth_type, 'password') + self.assertEqual('password', cc.auth_type) + + def test_set_default_before_init(self): + config.set_default('auth_type', 'token') + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + cc = c.get_one_cloud(cloud='_test-cloud_', argparse=None) + self.assertEqual('token', cc.auth_type) From 8f3a48ba815b7d6e69626dcde769331214e8e80d Mon Sep 17 00:00:00 2001 From: Ghe Rivero Date: Wed, 15 Jul 2015 01:53:39 +0200 Subject: [PATCH 0390/3836] Fix small error in README.rst Change-Id: Iba33df0c19d895be98e6f63467f6d3143fae88c0 --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 5159739ce..7001a94c5 100644 --- a/README.rst +++ b/README.rst @@ -28,7 +28,7 @@ Sometimes an example is nice. # Initialize cloud # Cloud configs are read with os-client-config - cloud = openstack_cloud('mordred') + cloud = openstack_cloud(cloud='mordred') # OpenStackCloud object has an interface exposing OpenStack services methods print cloud.list_servers() From 62c1f2a23e644d7fe0140c4062651febb2545d26 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 15 Jul 2015 07:58:14 -0400 Subject: [PATCH 0391/3836] Account for Error 396 on Rackspace Error 396 doesn't have a name, or a description. It's just Error 396. It shouldn't be judged for its failings in being descriptive, nor for the utter lack of necessity of its existence. Instead, we should feel sorry for it and the meaningless life it lives. You see, Error 396 is the error that the asynchronous image importing code returns when an unknown error is encountered and retrying is the appropriate action to take. Why doesn't the importing worker simply retry itself? Only Cthulhu can possibly know the answer to such a dark question, but perhaps it's for the best. Even a meaningless life such as the one Error 396 leads is better than no life at all. Or? Change-Id: I848986c9616d96caaad69806ad8b4f9f10301768 --- shade/__init__.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index c3c898608..8b28d9263 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -59,6 +59,8 @@ OBJECT_SHA256_KEY = 'x-object-meta-x-shade-sha256' IMAGE_MD5_KEY = 'owner_specified.shade.md5' IMAGE_SHA256_KEY = 'owner_specified.shade.sha256' +# Rackspace returns this for intermittent import errors +IMAGE_ERROR_396 = "Image cannot be imported. Error code: '396'" DEFAULT_OBJECT_SEGMENT_SIZE = 1073741824 # 1GB @@ -1444,11 +1446,13 @@ def _upload_image_task( # using glance properties to not delete then upload but instead make a # new "good" image and then mark the old one as "bad" # self.glance_client.images.delete(current_image) - glance_task = self.manager.submitTask(_tasks.ImageTaskCreate( + task_args = dict( type='import', input=dict( import_from='{container}/{name}'.format( container=container, name=name), - image_properties=dict(name=name)))) + image_properties=dict(name=name))) + glance_task = self.manager.submitTask( + _tasks.ImageTaskCreate(**task_args)) if wait: image_id = None for count in _utils._iterate_timeout( @@ -1479,10 +1483,14 @@ def _upload_image_task( self.list_images.invalidate(self) return self.get_image(status.result['image_id']) if status.status == 'failure': - raise OpenStackCloudException( - "Image creation failed: {message}".format( - message=status.message), - extra_data=status) + if status.message == IMAGE_ERROR_396: + glance_task = self.manager.submitTask( + _tasks.ImageTaskCreate(**task_args)) + else: + raise OpenStackCloudException( + "Image creation failed: {message}".format( + message=status.message), + extra_data=status) else: return meta.warlock_to_dict(glance_task) From 608713583ad3553f3cb9d456b5df8a01dcc999e5 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 15 Jul 2015 16:11:13 -0400 Subject: [PATCH 0392/3836] Fix debug logging lines There were several places where the exc_info parameter to the logging.debug() method was included inside of string.format() methods. Change-Id: I696e03c95c101d612174e78604f2753cf41a0d6b --- shade/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 57c0fc544..b8812e438 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -334,8 +334,8 @@ def _neutron_exceptions(self, error_message): "{msg}: {exc}".format(msg=error_message, exc=str(e))) except neutron_exceptions.NeutronClientException as e: self.log.debug( - "{msg}: {exc}".format(msg=error_message, exc=str(e), - exc_info=True)) + "{msg}: {exc}".format(msg=error_message, exc=str(e)), + exc_info=True) if e.status_code == 404: raise OpenStackCloudURINotFound( "{msg}: {exc}".format(msg=error_message, exc=str(e))) @@ -344,8 +344,8 @@ def _neutron_exceptions(self, error_message): "{msg}: {exc}".format(msg=error_message, exc=str(e))) except Exception as e: self.log.debug( - "{msg}: {exc}".format(msg=error_message, exc=str(e), - exc_info=True)) + "{msg}: {exc}".format(msg=error_message, exc=str(e)), + exc_info=True) raise OpenStackCloudException( "{msg}: {exc}".format(msg=error_message, exc=str(e))) @@ -4058,7 +4058,7 @@ def create_endpoint(self, service_name_or_id, public_url, except Exception as e: self.log.debug( "Failed to create endpoint for service {service}".format( - service=service['name'], exc_info=True)) + service=service['name']), exc_info=True) raise OpenStackCloudException(str(e)) return meta.obj_to_dict(endpoint) @@ -4161,7 +4161,7 @@ def create_identity_domain( except Exception as e: self.log.debug( "Failed to create domain {name}".format( - name=name, exc_info=True)) + name=name), exc_info=True) raise OpenStackCloudException(str(e)) return meta.obj_to_dict(domain) From 3891816acb6dd287f89498dd0fcb714b722f9252 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 16 Jul 2015 09:09:35 -0400 Subject: [PATCH 0393/3836] Remove region list from single cloud regions is a list of regions that can be used to create more than one CloudConfig object for each cloud/region combo. The regions field itself makes no sense to set on each of those CloudConfigs, so do not pass it through. Change-Id: I76b3bb3bc4778223d72f86d01d02ce150651b3b9 --- os_client_config/config.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/os_client_config/config.py b/os_client_config/config.py index 93d7aa0b8..6c3abff50 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -423,6 +423,10 @@ def get_one_cloud(self, cloud=None, validate=True, config = self._get_base_cloud_config(cloud) + # Regions is a list that we can use to create a list of cloud/region + # objects. It does not belong in the single-cloud dict + config.pop('regions', None) + # Can't just do update, because None values take over for (key, val) in iter(args.items()): if val is not None: From e7241b4e626e4e24fd2692a9f42520a50d3b3414 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 6 Jul 2015 09:30:27 -0400 Subject: [PATCH 0394/3836] Add flavor admin support Add methods to create and delete flavors to the admin interface. Change-Id: If8a239a4d0c5db63f6fa34048f8a5f44b09fb109 --- shade/__init__.py | 58 ++++++++++++++++++++++++++++++ shade/_tasks.py | 10 ++++++ shade/tests/unit/test_flavors.py | 61 ++++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 shade/tests/unit/test_flavors.py diff --git a/shade/__init__.py b/shade/__init__.py index f02c08e04..dd967de68 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -4260,3 +4260,61 @@ def get_identity_domain(self, domain_id): self.log.debug("Failed to get domain", exc_info=True) raise OpenStackCloudException(str(e)) return meta.obj_to_dict(domain) + + def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", + ephemeral=0, swap=0, rxtx_factor=1.0, is_public=True): + """Create a new flavor. + + :param name: Descriptive name of the flavor + :param ram: Memory in MB for the flavor + :param vcpus: Number of VCPUs for the flavor + :param disk: Size of local disk in GB + :param flavorid: ID for the flavor (optional) + :param ephemeral: Ephemeral space size in GB + :param swap: Swap space in MB + :param rxtx_factor: RX/TX factor + :param is_public: Make flavor accessible to the public + + :returns: A dict describing the new flavor. + + :raises: OpenStackCloudException on operation error. + """ + try: + flavor = self.manager.submitTask( + _tasks.FlavorCreate(name=name, ram=ram, vcpus=vcpus, disk=disk, + flavorid=flavorid, ephemeral=ephemeral, + swap=swap, rxtx_factor=rxtx_factor, + is_public=is_public) + ) + except Exception as e: + self.log.debug( + "Failed to create flavor {0}".format(name), + exc_info=True) + raise OpenStackCloudException(str(e)) + return meta.obj_to_dict(flavor) + + def delete_flavor(self, name_or_id): + """Delete a flavor + + :param name_or_id: ID or name of the flavor to delete. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException on operation error. + """ + flavor = self.get_flavor(name_or_id) + if flavor is None: + self.log.debug( + "Flavor {0} not found for deleting".format(name_or_id)) + return False + + try: + self.manager.submitTask(_tasks.FlavorDelete(flavor=flavor['id'])) + except Exception as e: + self.log.debug("Error deleting flavor {0}".format(name_or_id), + exc_info=True) + raise OpenStackCloudException( + "Unable to delete flavor {0}: {1}".format(name_or_id, e) + ) + + return True diff --git a/shade/_tasks.py b/shade/_tasks.py index 45be562ed..090c14851 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -42,6 +42,16 @@ def main(self, client): return client.nova_client.flavors.list() +class FlavorCreate(task_manager.Task): + def main(self, client): + return client.nova_client.flavors.create(**self.args) + + +class FlavorDelete(task_manager.Task): + def main(self, client): + return client.nova_client.flavors.delete(**self.args) + + class ServerList(task_manager.Task): def main(self, client): return client.nova_client.servers.list(**self.args) diff --git a/shade/tests/unit/test_flavors.py b/shade/tests/unit/test_flavors.py new file mode 100644 index 000000000..6a708f355 --- /dev/null +++ b/shade/tests/unit/test_flavors.py @@ -0,0 +1,61 @@ +# -*- 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. + + +import mock + +import shade +from shade.tests import fakes +from shade.tests.unit import base + + +class TestFlavors(base.TestCase): + + def setUp(self): + super(TestFlavors, self).setUp() + self.op_cloud = shade.operator_cloud() + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_create_flavor(self, mock_nova): + self.op_cloud.create_flavor( + 'vanilla', 12345, 4, 100 + ) + mock_nova.flavors.create.assert_called_once_with( + name='vanilla', ram=12345, vcpus=4, disk=100, + flavorid='auto', ephemeral=0, swap=0, rxtx_factor=1.0, + is_public=True + ) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_delete_flavor(self, mock_nova): + mock_nova.flavors.list.return_value = [ + fakes.FakeFlavor('123', 'lemon') + ] + self.assertTrue(self.op_cloud.delete_flavor('lemon')) + mock_nova.flavors.delete.assert_called_once_with(flavor='123') + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_delete_flavor_not_found(self, mock_nova): + mock_nova.flavors.list.return_value = [] + self.assertFalse(self.op_cloud.delete_flavor('invalid')) + self.assertFalse(mock_nova.flavors.delete.called) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_delete_flavor_exception(self, mock_nova): + mock_nova.flavors.list.return_value = [ + fakes.FakeFlavor('123', 'lemon') + ] + mock_nova.flavors.delete.side_effect = Exception() + self.assertRaises(shade.OpenStackCloudException, + self.op_cloud.delete_flavor, '') From b4145438fd603aca96f196383f3aee8b0973d24c Mon Sep 17 00:00:00 2001 From: TerryHowe Date: Mon, 13 Jul 2015 13:51:11 -0600 Subject: [PATCH 0395/3836] Have service name default to None It seems like having service name default to service_type would not work in a lot of situations. Change-Id: Ia70242fad346c1681fa4abca9d604aea3ae002dd --- os_client_config/cloud_config.py | 2 +- os_client_config/tests/test_cloud_config.py | 29 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index d0586f218..5ac061208 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -96,7 +96,7 @@ def get_service_type(self, service_type): def get_service_name(self, service_type): key = '{service_type}_service_name'.format(service_type=service_type) - return self.config.get(key, service_type) + return self.config.get(key, None) @property def prefer_ipv6(self): diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index 1a20ebf96..2af568fbc 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -17,6 +17,15 @@ fake_config_dict = {'a': 1, 'os_b': 2, 'c': 3, 'os_c': 4} +fake_services_dict = { + 'compute_api_version': 2, + 'compute_region_name': 'region-bl', + 'endpoint_type': 'public', + 'image_service_type': 'mage', + 'identity_endpoint_type': 'admin', + 'identity_service_name': 'locks', + 'auth': {'password': 'hunter2', 'username': 'AzureDiamond'}, +} class TestCloudConfig(base.TestCase): @@ -108,3 +117,23 @@ def test_ipv6(self): cc = cloud_config.CloudConfig( "test1", "region-al", fake_config_dict, prefer_ipv6=True) self.assertTrue(cc.prefer_ipv6) + + def test_getters(self): + cc = cloud_config.CloudConfig("test1", "region-al", fake_services_dict) + + self.assertEqual(['compute', 'identity', 'image'], + sorted(cc.get_services())) + self.assertEqual({'password': 'hunter2', 'username': 'AzureDiamond'}, + cc.get_auth_args()) + self.assertEqual('public', cc.get_endpoint_type()) + self.assertEqual('public', cc.get_endpoint_type('image')) + self.assertEqual('admin', cc.get_endpoint_type('identity')) + self.assertEqual('region-al', cc.get_region_name()) + self.assertEqual('region-al', cc.get_region_name('image')) + self.assertEqual('region-bl', cc.get_region_name('compute')) + self.assertEqual(None, cc.get_api_version('image')) + self.assertEqual(2, cc.get_api_version('compute')) + self.assertEqual('mage', cc.get_service_type('image')) + self.assertEqual('compute', cc.get_service_type('compute')) + self.assertEqual(None, cc.get_service_name('compute')) + self.assertEqual('locks', cc.get_service_name('identity')) From 0b698134db7fef262304f1eb55b374dc36ce93b1 Mon Sep 17 00:00:00 2001 From: TerryHowe Date: Tue, 14 Jul 2015 12:30:16 -0600 Subject: [PATCH 0396/3836] Rename 'endpoint_type' to 'interface' The keystone folks seem to feel that interface is a better name than endpoint type. Change-Id: Ibfc54e725b6dae843c07f7786f51f9fb9141c983 --- os_client_config/cloud_config.py | 9 +++++---- os_client_config/config.py | 8 ++++++++ os_client_config/defaults.yaml | 2 +- os_client_config/tests/test_cloud_config.py | 10 +++++----- os_client_config/tests/test_config.py | 21 +++++++++++++++++++++ 5 files changed, 40 insertions(+), 10 deletions(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 5ac061208..fd35aeacd 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -74,11 +74,12 @@ def get_services(self): def get_auth_args(self): return self.config['auth'] - def get_endpoint_type(self, service_type=None): + def get_interface(self, service_type=None): + interface = self.config.get('interface') if not service_type: - return self.config['endpoint_type'] - key = '{service_type}_endpoint_type'.format(service_type=service_type) - return self.config.get(key, self.config['endpoint_type']) + return interface + key = '{service_type}_interface'.format(service_type=service_type) + return self.config.get(key, interface) def get_region_name(self, service_type=None): if not service_type: diff --git a/os_client_config/config.py b/os_client_config/config.py index 89503e752..25fbf64d4 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -278,6 +278,7 @@ def _get_base_cloud_config(self, name): def _fix_backwards_madness(self, cloud): cloud = self._fix_backwards_project(cloud) cloud = self._fix_backwards_auth_plugin(cloud) + cloud = self._fix_backwards_interface(cloud) return cloud def _fix_backwards_project(self, cloud): @@ -315,6 +316,13 @@ def _fix_backwards_auth_plugin(self, cloud): cloud[target_key] = target return cloud + def _fix_backwards_interface(self, cloud): + for key in cloud.keys(): + if key.endswith('endpoint_type'): + target_key = key.replace('endpoint_type', 'interface') + cloud[target_key] = cloud.pop(key) + return cloud + def get_all_clouds(self): clouds = [] diff --git a/os_client_config/defaults.yaml b/os_client_config/defaults.yaml index e4c900062..9130228f2 100644 --- a/os_client_config/defaults.yaml +++ b/os_client_config/defaults.yaml @@ -4,7 +4,7 @@ compute_api_version: '2' database_api_version: '1.0' disable_vendor_agent: {} dns_api_version: '2' -endpoint_type: public +interface: public floating_ip_source: neutron identity_api_version: '2' image_api_use_tasks: false diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index 2af568fbc..29cbba037 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -20,9 +20,9 @@ fake_services_dict = { 'compute_api_version': 2, 'compute_region_name': 'region-bl', - 'endpoint_type': 'public', + 'interface': 'public', 'image_service_type': 'mage', - 'identity_endpoint_type': 'admin', + 'identity_interface': 'admin', 'identity_service_name': 'locks', 'auth': {'password': 'hunter2', 'username': 'AzureDiamond'}, } @@ -125,9 +125,9 @@ def test_getters(self): sorted(cc.get_services())) self.assertEqual({'password': 'hunter2', 'username': 'AzureDiamond'}, cc.get_auth_args()) - self.assertEqual('public', cc.get_endpoint_type()) - self.assertEqual('public', cc.get_endpoint_type('image')) - self.assertEqual('admin', cc.get_endpoint_type('identity')) + self.assertEqual('public', cc.get_interface()) + self.assertEqual('public', cc.get_interface('compute')) + self.assertEqual('admin', cc.get_interface('identity')) self.assertEqual('region-al', cc.get_region_name()) self.assertEqual('region-al', cc.get_region_name('image')) self.assertEqual('region-bl', cc.get_region_name('compute')) diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 1444ed557..6bb65fce5 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -275,3 +275,24 @@ def test_set_default_before_init(self): vendor_files=[self.vendor_yaml]) cc = c.get_one_cloud(cloud='_test-cloud_', argparse=None) self.assertEqual('token', cc.auth_type) + + +class TestBackwardsCompatibility(base.TestCase): + + def test_set_no_default(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + cloud = { + 'identity_endpoint_type': 'admin', + 'compute_endpoint_type': 'private', + 'endpoint_type': 'public', + 'auth_type': 'v3password', + } + result = c._fix_backwards_interface(cloud) + expected = { + 'identity_interface': 'admin', + 'compute_interface': 'private', + 'interface': 'public', + 'auth_type': 'v3password', + } + self.assertEqual(expected, result) From b292db766ff49a9d34a9b8877440add6d0253335 Mon Sep 17 00:00:00 2001 From: TerryHowe Date: Thu, 16 Jul 2015 14:52:24 -0600 Subject: [PATCH 0397/3836] Remove py26 and py33 from tox.ini Change-Id: I79a9dceffc0432398bda70949db2e523cbbff515 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 860e3378b..af6ede141 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 1.6 -envlist = py33,py34,py26,py27,pypy,pep8 +envlist = py34,py27,pypy,pep8 skipsdist = True [testenv] From db0480e716aebf44dc8bdaadcd83a138e4441543 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 17 Jul 2015 11:17:22 -0400 Subject: [PATCH 0398/3836] Catch leaky exceptions from create_image() The create_image() method calls various other helper private methods that do not attempt to catch the underlying client exceptions and wrap them as OpenStackCloudException exceptions. This change will catch all exceptions, rethrow OpenStackCloudException exceptions, and wrap all others as OpenStackCloudException. Change-Id: I155af0dcb6aeeb33a001a30183496bb2c227f46a --- shade/__init__.py | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index cff6d028d..3340b0035 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1403,20 +1403,29 @@ def create_image( return current_image kwargs[IMAGE_MD5_KEY] = md5 kwargs[IMAGE_SHA256_KEY] = sha256 - # This makes me want to die inside - if self.image_api_use_tasks: - return self._upload_image_task( - name, filename, container, - current_image=current_image, - wait=wait, timeout=timeout, **kwargs) - else: - image_kwargs = dict(properties=kwargs) - if disk_format: - image_kwargs['disk_format'] = disk_format - if container_format: - image_kwargs['container_format'] = container_format - return self._upload_image_put(name, filename, **image_kwargs) + try: + # This makes me want to die inside + if self.image_api_use_tasks: + return self._upload_image_task( + name, filename, container, + current_image=current_image, + wait=wait, timeout=timeout, **kwargs) + else: + image_kwargs = dict(properties=kwargs) + if disk_format: + image_kwargs['disk_format'] = disk_format + if container_format: + image_kwargs['container_format'] = container_format + + return self._upload_image_put(name, filename, **image_kwargs) + except OpenStackCloudException: + self.log.debug("Image creation failed", exc_info=True) + raise + except Exception as e: + self.log.debug("Image creation failed", exc_info=True) + raise OpenStackCloudException( + "Image creation failed: {message}".format(message=str(e))) def _upload_image_put_v2(self, name, image_data, **image_kwargs): if 'properties' in image_kwargs: From d6d2cbe5e1ada67f78e7dd1bedebe8a57fcac8fe Mon Sep 17 00:00:00 2001 From: TerryHowe Date: Fri, 17 Jul 2015 11:26:03 -0600 Subject: [PATCH 0399/3836] Remove requirements.txt from tox.ini From lifeless: pbr reflects the package dependencies from requirements.txt into the sdist that tox builds. Change-Id: Iaa6026a504cc53784aad5731e9afe8b684b3ede5 --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index af6ede141..7a2d3a07a 100644 --- a/tox.ini +++ b/tox.ini @@ -8,8 +8,7 @@ usedevelop = True install_command = pip install -U {opts} {packages} setenv = VIRTUAL_ENV={envdir} -deps = -r{toxinidir}/requirements.txt - -r{toxinidir}/test-requirements.txt +deps = -r{toxinidir}/test-requirements.txt commands = python setup.py testr --slowest --testr-args='{posargs}' [testenv:pep8] From a9cf85b0cb8497757cce010e9d7accc2bf25ce06 Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Fri, 17 Jul 2015 22:10:42 +0200 Subject: [PATCH 0400/3836] Always use a fixed address when attaching a floating IP to a server When using Neutron, if the caller didn't specify a fixed_address when assigning a floating IP to a server, automatically pick the fist IPv4 address assigned to the fist port of the server. This should fix the DreamHost use case: DH uses Neutron and automatically assigns an IPv4 and an IPv6 fixed address. Even if it doesn't make sense to attach a floating IPv4 address to a fixed IPv6 address, OpenStack returns an error if we don't specify a fixed address when attacking a floating IP to a server port when multiple IPs assigned to that port. Change-Id: I4b91bf29366c4d8b5277d0d97cd9571770252961 --- shade/__init__.py | 9 +++++++++ shade/tests/unit/test_floating_ip_neutron.py | 4 +++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index cff6d028d..0feb1ece0 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2092,6 +2092,15 @@ def _neutron_attach_ip_to_server(self, server_id, floating_ip_id, port = None if ports and fixed_address is None: port = ports[0] + # Select the first available IPv4 address + for address in port.get('fixed_ips', list()): + if _utils.is_ipv4(address['ip_address']): + fixed_address = address['ip_address'] + break + if fixed_address is None: + raise OpenStackCloudException( + "unable to find a suitable IPv4 address for server " + "{0}".format(server_id)) elif ports: # unfortunately a port can have more than one fixed IP: # we can't use the search_ports filtering for fixed_address as diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index c7c77317d..9f0dd069b 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -290,7 +290,9 @@ def test_attach_ip_to_server( floatingip=self.mock_floating_ip_new_rep['floatingip']['id'], body={ 'floatingip': { - 'port_id': self.mock_search_ports_rep[0]['id'] + 'port_id': self.mock_search_ports_rep[0]['id'], + 'fixed_ip_address': self.mock_search_ports_rep[0][ + 'fixed_ips'][0]['ip_address'] } } ) From 21e7628882cd473be627863f047622b1502d15f7 Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Mon, 29 Jun 2015 16:35:07 +0200 Subject: [PATCH 0401/3836] Add initial designate read-only operations This change adds list/get operations for Designate domains and records objects. Change-Id: If37d07245281139c91e168c8652368d6f7a7c484 --- requirements.txt | 1 + shade/__init__.py | 54 ++++++++++++++++++++++++++++++++++ shade/_tasks.py | 20 +++++++++++++ shade/tests/unit/test_shade.py | 16 ++++++++++ 4 files changed, 91 insertions(+) diff --git a/requirements.txt b/requirements.txt index a5e0143f4..48f9204bc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,5 +14,6 @@ python-neutronclient>=2.3.10 python-troveclient python-ironicclient>=0.5.1 python-swiftclient +python-designateclient>=1.3.0 dogpile.cache>=0.5.3 diff --git a/shade/__init__.py b/shade/__init__.py index cff6d028d..36f1996ae 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -20,6 +20,7 @@ import os from cinderclient.v1 import client as cinder_client +from designateclient.v1 import Client as designate_client from decorator import decorator from dogpile import cache import glanceclient @@ -317,6 +318,7 @@ def __init__(self, cloud, auth, self._keystone_session = None self._cinder_client = None + self._designate_client = None self._glance_client = None self._glance_endpoint = None self._ironic_client = None @@ -779,6 +781,26 @@ def neutron_client(self): timeout=self.api_timeout) return self._neutron_client + @property + def designate_client(self): + if self._designate_client is None: + # get dns service type if defined in cloud config + dns_service_type = self.get_service_type('dns') + # trigger exception on lack of designate + self.get_session_endpoint(dns_service_type) + + self._designate_client = designate_client( + session=self.keystone_session, + region_name=self.region_name, + service_type=dns_service_type) + + if self._designate_client is None: + raise OpenStackCloudException( + "Failed to instantiate designate client." + " This could mean that your credentials are wrong.") + + return self._designate_client + def get_name(self): return self.name @@ -926,6 +948,14 @@ def search_floating_ips(self, id=None, filters=None): floating_ips = self.list_floating_ips() return _utils._filter_list(floating_ips, id, filters) + def search_domains(self, name_or_id=None, filters=None): + domains = self.list_domains() + return _utils._filter_list(domains, name_or_id, filters) + + def search_records(self, domain_id, name_or_id=None, filters=None): + records = self.list_records(domain_id=domain_id) + return _utils._filter_list(records, name_or_id, filters) + def list_keypairs(self): try: return meta.obj_list_to_dict( @@ -1118,6 +1148,22 @@ def _nova_list_floating_ips(self): raise OpenStackCloudException( "error fetching floating IPs list: {msg}".format(msg=str(e))) + def list_domains(self): + try: + return self.manager.submitTask(_tasks.DomainList()) + except Exception as e: + self.log.debug("domain list failed: %s" % e, exc_info=True) + raise OpenStackCloudException( + "Error fetching domain list: %s" % e) + + def list_records(self, domain_id): + try: + return self.manager.submitTask(_tasks.RecordList(domain=domain_id)) + except Exception as e: + self.log.debug("record list failed: %s" % e, exc_info=True) + raise OpenStackCloudException( + "Error fetching record list: %s" % e) + def get_keypair(self, name_or_id, filters=None): return _utils._get_entity(self.search_keypairs, name_or_id, filters) @@ -1152,6 +1198,14 @@ def get_image(self, name_or_id, filters=None): def get_floating_ip(self, id, filters=None): return _utils._get_entity(self.search_floating_ips, id, filters) + def get_domain(self, name_or_id, filters=None): + return _utils._get_entity(self.search_domains, name_or_id, filters) + + def get_record(self, domain_id, name_or_id, filters=None): + f = lambda name_or_id, filters: self.search_records( + domain_id, name_or_id, filters) + return _utils._get_entity(f, name_or_id, filters) + def create_keypair(self, name, public_key): """Create a new keypair. diff --git a/shade/_tasks.py b/shade/_tasks.py index 090c14851..bdc5d7a57 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -535,3 +535,23 @@ def main(self, client): class IdentityDomainDelete(task_manager.Task): def main(self, client): return client.keystone_client.domains.delete(**self.args) + + +class DomainList(task_manager.Task): + def main(self, client): + return client.designate_client.domains.list() + + +class DomainGet(task_manager.Task): + def main(self, client): + return client.designate_client.domains.get(**self.args) + + +class RecordList(task_manager.Task): + def main(self, client): + return client.designate_client.records.list(**self.args) + + +class RecordGet(task_manager.Task): + def main(self, client): + return client.designate_client.records.get(**self.args) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 66f6d3ca3..e7a5bf6c0 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -361,3 +361,19 @@ def test__neutron_exceptions_generic(self): side_effect=Exception()): self.assertRaises(exc.OpenStackCloudException, self.cloud.list_networks) + + @mock.patch.object(shade.OpenStackCloud, 'list_domains') + def test_get_domain(self, mock_search): + domain1 = dict(id='123', name='mickey') + mock_search.return_value = [domain1] + r = self.cloud.get_domain('mickey') + self.assertIsNotNone(r) + self.assertDictEqual(domain1, r) + + @mock.patch.object(shade.OpenStackCloud, 'list_records') + def test_get_record(self, mock_search): + record1 = dict(id='123', name='mickey', domain_id='mickey.domain') + mock_search.return_value = [record1] + r = self.cloud.get_record('mickey.domain', 'mickey') + self.assertIsNotNone(r) + self.assertDictEqual(record1, r) From 482d6ed82267de2c12e000f415aea38ee4d886db Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Thu, 23 Jul 2015 12:42:57 -0700 Subject: [PATCH 0402/3836] Correctly pass the server ID to add_ip_from_pool The add_ips_to_server method is incorrectly passing the server object to add_ip_from_pool instead of the server ID. Change-Id: I252b585b940fd6598e6a328df4ef3041ee326249 Signed-off-by: Rosario Di Somma --- shade/__init__.py | 2 +- shade/tests/unit/test_floating_ip_common.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index de0110cb7..bb9f85872 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2345,7 +2345,7 @@ def add_auto_ip(self, server, wait=False, timeout=60): def add_ips_to_server(self, server, auto_ip=True, ips=None, ip_pool=None): if ip_pool: - self.add_ip_from_pool(server, ip_pool) + self.add_ip_from_pool(server['id'], ip_pool) elif ips: self.add_ip_list(server, ips) elif auto_ip: diff --git a/shade/tests/unit/test_floating_ip_common.py b/shade/tests/unit/test_floating_ip_common.py index 7c5e193dc..810916b82 100644 --- a/shade/tests/unit/test_floating_ip_common.py +++ b/shade/tests/unit/test_floating_ip_common.py @@ -62,7 +62,7 @@ def test_add_auto_ip( def test_add_ips_to_server_pool( self, mock_add_ip_from_pool, mock_nova_client): server = FakeServer( - id='server-id', name='test-server', status="ACTIVE", addresses={} + id='romeo', name='test-server', status="ACTIVE", addresses={} ) server_dict = meta.obj_to_dict(server) pool = 'nova' @@ -71,7 +71,7 @@ def test_add_ips_to_server_pool( self.client.add_ips_to_server(server_dict, ip_pool=pool) - mock_add_ip_from_pool.assert_called_with(server_dict, pool) + mock_add_ip_from_pool.assert_called_with('romeo', pool) @patch.object(OpenStackCloud, 'nova_client') @patch.object(OpenStackCloud, 'add_ip_list') From 856b298175f970b3226f0cb39d78e82ed31c32ed Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 3 Jun 2015 18:52:57 -0400 Subject: [PATCH 0403/3836] Use os-client-config SSL arg processing This logic was added to occ, so use it from there. Change-Id: Icc224128ee397cdb50ee01fc75568a5207b8dbf7 --- requirements.txt | 2 +- shade/__init__.py | 20 ++++++-------------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/requirements.txt b/requirements.txt index 48f9204bc..ea31f8e94 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ pbr>=0.11,<2.0 bunch decorator jsonpatch -os-client-config>=1.2.0 +os-client-config>=1.3.0 six python-novaclient>=2.21.0 diff --git a/shade/__init__.py b/shade/__init__.py index de0110cb7..d8ece7622 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -150,16 +150,6 @@ def operator_cloud(debug=False, **kwargs): **cloud_config.config) -def _ssl_args(verify, cacert, cert, key): - if cacert: - verify = cacert - - if cert: - if key: - cert = (cert, key) - return (verify, cert) - - def _get_service_values(kwargs, service_key): # get defauts returns a copy of the defaults dict values = os_client_config.defaults.get_defaults() @@ -279,11 +269,13 @@ def __init__(self, cloud, auth, if cloud_config is None: config = os_client_config.OpenStackConfig() + ssl_args = dict( + verify=verify, cacert=cacert, cert=cert, key=key, + ) if cloud in config.get_cloud_names(): - cloud_config = config.get_one_cloud(cloud) + cloud_config = config.get_one_cloud(cloud, **ssl_args) else: - cloud_config = config.get_one_cloud() - + cloud_config = config.get_one_cloud(**ssl_args) self.name = cloud self.auth = auth self.region_name = region_name @@ -305,7 +297,7 @@ def __init__(self, cloud, auth, self.secgroup_source = kwargs.get('secgroup_source', None) - (self.verify, self.cert) = _ssl_args(verify, cacert, cert, key) + (self.verify, self.cert) = cloud_config.get_requests_verify_args() self._cache = cache.make_region( function_key_generator=self._make_cache_key From 5c6aefacc772b6e7d48654297d4512d54b18ba42 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 3 Jun 2015 20:33:44 -0400 Subject: [PATCH 0404/3836] Use disable_vendor_agent flags in create_image By default, if you're creating an image by uploading it with glance, it means you either made it yourself or you downloaded from somewhere like Ubuntu or Fedora. In neither case are you likely to have special vendor-specific agents in your image. If you DO have them, you know what you're doing and can flip a flag. Change-Id: I9cacfc0284f196f2f65494382ceb0d4e130f9fb2 --- shade/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index d8ece7622..d1e5220e4 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -276,6 +276,7 @@ def __init__(self, cloud, auth, cloud_config = config.get_one_cloud(cloud, **ssl_args) else: cloud_config = config.get_one_cloud(**ssl_args) + self._cloud_config = cloud_config self.name = cloud self.auth = auth self.region_name = region_name @@ -1438,6 +1439,7 @@ def create_image( self, name, filename, container='images', md5=None, sha256=None, disk_format=None, container_format=None, + disable_vendor_agent=True, wait=False, timeout=3600, **kwargs): if not md5 or not sha256: (md5, sha256) = self._get_file_hashes(filename) @@ -1450,6 +1452,9 @@ def create_image( kwargs[IMAGE_MD5_KEY] = md5 kwargs[IMAGE_SHA256_KEY] = sha256 + if disable_vendor_agent: + kwargs.update(self._cloud_config.config['disable_vendor_agent']) + try: # This makes me want to die inside if self.image_api_use_tasks: From b2d10751eaea6e1b9fe15a202a18a98d2c25c95f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 27 Jul 2015 11:48:26 -0400 Subject: [PATCH 0405/3836] Add per-service endpoint overrides Make it possible to override a service's endpoint like service_type and service_name. This is already documented as working. Change-Id: I8764ed68f8a38563c4242d4b50e2158e99ed4109 --- os_client_config/cloud_config.py | 4 ++++ os_client_config/tests/test_cloud_config.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index fd35aeacd..f60303b2a 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -99,6 +99,10 @@ def get_service_name(self, service_type): key = '{service_type}_service_name'.format(service_type=service_type) return self.config.get(key, None) + def get_endpoint(self, service_type): + key = '{service_type}_endpoint'.format(service_type=service_type) + return self.config.get(key, None) + @property def prefer_ipv6(self): return self._prefer_ipv6 diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index 29cbba037..4f5260fc1 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -19,6 +19,7 @@ fake_config_dict = {'a': 1, 'os_b': 2, 'c': 3, 'os_c': 4} fake_services_dict = { 'compute_api_version': 2, + 'compute_endpoint': 'http://compute.example.com', 'compute_region_name': 'region-bl', 'interface': 'public', 'image_service_type': 'mage', @@ -135,5 +136,9 @@ def test_getters(self): self.assertEqual(2, cc.get_api_version('compute')) self.assertEqual('mage', cc.get_service_type('image')) self.assertEqual('compute', cc.get_service_type('compute')) + self.assertEqual('http://compute.example.com', + cc.get_endpoint('compute')) + self.assertEqual(None, + cc.get_endpoint('image')) self.assertEqual(None, cc.get_service_name('compute')) self.assertEqual('locks', cc.get_service_name('identity')) From 212ce988609fafc830fcb59f42d31eacefc62565 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 27 Jul 2015 12:15:46 -0400 Subject: [PATCH 0406/3836] Clarify floating ip use for vendors HP was not listed as needing a floating ip. Also, the non-floating IP case mentions direct routing, so amend the floating IP case to mention NAT for completeness and clarity. Change-Id: I220c41f6c822b7b6ffb1ec11038749153ef5a6ee --- doc/source/vendor-support.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index 314e36229..3cf39e03f 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -37,6 +37,7 @@ region-b.geo-1 US East ============== ================ * DNS Service Type is `hpext:dns` +* Public IPv4 is provided via NAT with Neutron Floating IP rackspace --------- @@ -79,7 +80,7 @@ RegionOne Region One * Image API Version is 2 * Images must be in `raw` format -* Public IPv4 is provided via Floating IP from Neutron +* Public IPv4 is provided via NAT with Neutron Floating IP * IPv6 is provided to every server vexxhost @@ -139,7 +140,7 @@ RegionOne RegionOne ============== ================ * Identity API Version is 2 -* Public IPv4 is provided via Floating IP from Nova +* Public IPv4 is provided via NAT with Nova Floating IP * Floating IPs are provided by Nova * Security groups are provided by Nova From 6e9a52a734bfe44cc7971a4a2d8b37425013156d Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 20 Jul 2015 15:34:35 -0400 Subject: [PATCH 0407/3836] Fix for swift servers older than 1.11.0 Discoverable capabilities was not added to swift until 1.11.0. Some providers are running an older version. These will return a swift exception with an HTTP status of 412 because the URL (/info) is bad. This change sets a default that is half the Swift default max file size. The result is also cached as it isn't expected to change. A separate, future change will take advantage of a newer os-client-config which will allow setting the default value per-vendor profile. Change-Id: I80b14cfae42fc160f42b44e4923472d14a4b9057 --- shade/__init__.py | 21 +++++++++++++++++++-- shade/tests/unit/test_object.py | 8 ++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index cff6d028d..fd848b240 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -64,6 +64,8 @@ # Rackspace returns this for intermittent import errors IMAGE_ERROR_396 = "Image cannot be imported. Error code: '396'" DEFAULT_OBJECT_SEGMENT_SIZE = 1073741824 # 1GB +# This halves the current default for Swift +DEFAULT_MAX_FILE_SIZE = (5 * 1024 * 1024 * 1024 + 2) / 2 OBJECT_CONTAINER_ACLS = { @@ -2500,8 +2502,23 @@ def get_object_segment_size(self, segment_size): '''get a segment size that will work given capabilities''' if segment_size is None: segment_size = DEFAULT_OBJECT_SEGMENT_SIZE - caps = self.get_object_capabilities() - server_max_file_size = caps.get('swift', {}).get('max_file_size', 0) + try: + caps = self.get_object_capabilities() + except swift_exceptions.ClientException as e: + if e.http_status == 412: + server_max_file_size = DEFAULT_MAX_FILE_SIZE + self.log.info( + "Swift capabilities not supported. " + "Using default max file size.") + else: + self.log.debug( + "Failed to query swift capabilities", exc_info=True) + raise OpenStackCloudException( + "Could not determine capabilities") + else: + server_max_file_size = caps.get('swift', {}).get('max_file_size', + 0) + if segment_size > server_max_file_size: return server_max_file_size return segment_size diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index 4dfc04fcb..c725af61c 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -16,6 +16,7 @@ import mock from swiftclient import client as swift_client from swiftclient import service as swift_service +from swiftclient import exceptions as swift_exc import shade from shade import exc @@ -81,3 +82,10 @@ def test_get_object_segment_size(self, swift_mock): self.assertEqual(900, self.cloud.get_object_segment_size(900)) self.assertEqual(1000, self.cloud.get_object_segment_size(1000)) self.assertEqual(1000, self.cloud.get_object_segment_size(1100)) + + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + def test_get_object_segment_size_http_412(self, swift_mock): + swift_mock.get_capabilities.side_effect = swift_exc.ClientException( + "Precondition failed", http_status=412) + self.assertEqual(shade.DEFAULT_OBJECT_SEGMENT_SIZE, + self.cloud.get_object_segment_size(None)) From 31eabb9a424c340f77039b26a8a6d3bf08589657 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 20 Jul 2015 15:41:28 -0400 Subject: [PATCH 0408/3836] Fix test_object.py test class name Looks like a copy-paste error as TestShade is in test_shade.py. Rename the test_object class to TestObject. Change-Id: I73df0db1de2f4ca41d801bcca4dce53215faabe6 --- shade/tests/unit/test_object.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index c725af61c..7ec2f735b 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -24,10 +24,10 @@ from shade.tests.unit import base -class TestShade(base.TestCase): +class TestObject(base.TestCase): def setUp(self): - super(TestShade, self).setUp() + super(TestObject, self).setUp() self.cloud = OpenStackCloud('cloud', {}) @mock.patch.object(swift_client, 'Connection') From 494cea2d1cddf117252619735c433fb681e63801 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 28 Jul 2015 09:55:13 -0400 Subject: [PATCH 0409/3836] Update ansible subnet test Add a task for updating a subnet. Change-Id: I55564f2d4a82bc4a0de23501f7d6b334fc54c973 --- shade/tests/ansible/roles/subnet/tasks/main.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/shade/tests/ansible/roles/subnet/tasks/main.yml b/shade/tests/ansible/roles/subnet/tasks/main.yml index 4d68c57d2..8d70cd2b5 100644 --- a/shade/tests/ansible/roles/subnet/tasks/main.yml +++ b/shade/tests/ansible/roles/subnet/tasks/main.yml @@ -20,6 +20,16 @@ allocation_pool_start: 192.168.0.2 allocation_pool_end: 192.168.0.254 +- name: Update subnet + os_subnet: + cloud: "{{ cloud }}" + network_name: "{{ network_name }}" + name: "{{ subnet_name }}" + state: present + dns_nameservers: + - 8.8.8.7 + cidr: 192.168.0.0/24 + - name: Delete subnet {{ subnet_name }} os_subnet: cloud: "{{ cloud }}" From ad59fe3a728cb12d930776aea8463a8982fd0d88 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 27 Jul 2015 11:58:43 -0400 Subject: [PATCH 0410/3836] Process config options via os-client-config Assume that a cloud config object will be passed in. For compat, create one and pass in kwargs if it's not - but based the internal operation on the existence of a cloud_config object. Change-Id: I31b2d851e0636c7742468aa62565295a2c5266e2 Depends-On: I8764ed68f8a38563c4242d4b50e2158e99ed4109 --- requirements.txt | 2 +- shade/__init__.py | 186 ++++++------------- shade/tests/unit/test_caching.py | 16 +- shade/tests/unit/test_create_server.py | 4 +- shade/tests/unit/test_delete_server.py | 4 +- shade/tests/unit/test_domain_params.py | 20 +- shade/tests/unit/test_endpoints.py | 4 +- shade/tests/unit/test_floating_ip_common.py | 4 +- shade/tests/unit/test_floating_ip_neutron.py | 4 +- shade/tests/unit/test_floating_ip_nova.py | 4 +- shade/tests/unit/test_floating_ip_pool.py | 4 +- shade/tests/unit/test_object.py | 4 +- shade/tests/unit/test_port.py | 4 +- shade/tests/unit/test_rebuild_server.py | 4 +- shade/tests/unit/test_services.py | 4 +- shade/tests/unit/test_shade.py | 19 +- shade/tests/unit/test_shade_operator.py | 7 +- 17 files changed, 131 insertions(+), 163 deletions(-) diff --git a/requirements.txt b/requirements.txt index ea31f8e94..94fd45232 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ pbr>=0.11,<2.0 bunch decorator jsonpatch -os-client-config>=1.3.0 +os-client-config>=1.6.0 six python-novaclient>=2.21.0 diff --git a/shade/__init__.py b/shade/__init__.py index 003350d06..723f9efdc 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -126,38 +126,28 @@ def openstack_clouds(config=None, debug=False): ] -def openstack_cloud(debug=False, **kwargs): - config = kwargs.get('config') - if config is None: +def openstack_cloud(config=None, **kwargs): + if not config: config = os_client_config.OpenStackConfig() cloud_config = config.get_one_cloud(**kwargs) return OpenStackCloud( - cloud=cloud_config.name, cache_interval=config.get_cache_max_age(), cache_class=config.get_cache_class(), cache_arguments=config.get_cache_arguments(), - cloud_config=cloud_config, - debug=debug, **cloud_config.config) + cloud_config=cloud_config) -def operator_cloud(debug=False, **kwargs): - config = os_client_config.OpenStackConfig() +def operator_cloud(config=None, **kwargs): + if 'interface' not in kwargs: + kwargs['interface'] = 'admin' + if not config: + config = os_client_config.OpenStackConfig() cloud_config = config.get_one_cloud(**kwargs) return OperatorCloud( - cloud_config.name, debug=debug, cache_interval=config.get_cache_max_age(), cache_class=config.get_cache_class(), cache_arguments=config.get_cache_arguments(), - cloud_config=cloud_config, - **cloud_config.config) - - -def _get_service_values(kwargs, service_key): - # get defauts returns a copy of the defaults dict - values = os_client_config.defaults.get_defaults() - values.update(kwargs) - return {k[:-(len(service_key) + 1)]: str(values[k]) - for k in values.keys() if k.endswith(service_key)} + cloud_config=cloud_config) def _cache_on_arguments(*cache_on_args, **cache_on_kwargs): @@ -204,32 +194,6 @@ class OpenStackCloud(object): and that Floating IP will be actualized either via neutron or via nova depending on how this particular cloud has decided to arrange itself. - :param string name: The name of the cloud - :param dict auth: Dictionary containing authentication information. - Depending on the value of auth_type, the contents - of this dict can vary wildly. - :param string region_name: The region of the cloud that all operations - should be performed against. - (optional, default '') - :param string auth_type: The name of the keystone auth_type to be used - :param string endpoint_type: The type of endpoint to get for services - from the service catalog. Valid types are - `public` ,`internal` or `admin`. (optional, - defaults to `public`) - :param bool private: Whether to return or use private IPs by default for - servers. (optional, defaults to False) - :param float api_timeout: A timeout to pass to REST client constructors - indicating network-level timeouts. (optional) - :param bool verify: The verification arguments to pass to requests. True - tells requests to verify SSL requests, False to not - verify. (optional, defaults to True) - :param string cacert: A path to a CA Cert bundle that can be used as part - of verifying SSL requests. If this is set, verify - is set to True. (optional) - :param string cert: A path to a client certificate to pass to requests. - (optional) - :param string key: A path to a client key to pass to requests. (optional) - :param bool debug: Deprecated and unused parameter. :param int cache_interval: How long to cache items fetched from the cloud. Value will be passed to dogpile.cache. None means do not cache at all. @@ -242,64 +206,41 @@ class OpenStackCloud(object): OpenStack API tasks. Unless you're doing rate limiting client side, you almost certainly don't need this. (optional) - :param bool image_api_use_tasks: Whether or not this cloud needs to - use the glance task-create interface for - image upload activities instead of direct - calls. (optional, defaults to False) :param CloudConfig cloud_config: Cloud config object from os-client-config In the future, this will be the only way to pass in cloud configuration, but is being phased in currently. """ - def __init__(self, cloud, auth, - region_name='', - auth_type='password', - endpoint_type='public', - private=False, - verify=True, cacert=None, cert=None, key=None, - api_timeout=None, - debug=False, cache_interval=None, - cache_class='dogpile.cache.null', - cache_arguments=None, - manager=None, - image_api_use_tasks=False, - cloud_config=None, - **kwargs): + def __init__( + self, + cloud_config=None, + cache_interval=None, + cache_class='dogpile.cache.null', + cache_arguments=None, + manager=None, **kwargs): self.log = logging.getLogger('shade') - - if cloud_config is None: + if not cloud_config: config = os_client_config.OpenStackConfig() - ssl_args = dict( - verify=verify, cacert=cacert, cert=cert, key=key, - ) - if cloud in config.get_cloud_names(): - cloud_config = config.get_one_cloud(cloud, **ssl_args) - else: - cloud_config = config.get_one_cloud(**ssl_args) - self._cloud_config = cloud_config - self.name = cloud - self.auth = auth - self.region_name = region_name - self.auth_type = auth_type - self.endpoint_type = endpoint_type - self.private = private - self.api_timeout = api_timeout + cloud_config = config.get_one_cloud(**kwargs) + + self.name = cloud_config.name + self.auth = cloud_config.get_auth_args() + self.region_name = cloud_config.region_name + self.auth_type = cloud_config.config['auth_type'] + self.default_interface = cloud_config.get_interface() + self.private = cloud_config.config.get('private', False) + self.api_timeout = cloud_config.config['api_timeout'] + self.image_api_use_tasks = cloud_config.config['image_api_use_tasks'] + self.secgroup_source = cloud_config.config['secgroup_source'] + if manager is not None: self.manager = manager else: self.manager = task_manager.TaskManager( name=self.name, client=self) - self.service_types = _get_service_values(kwargs, 'service_type') - self.service_names = _get_service_values(kwargs, 'service_name') - self.endpoints = _get_service_values(kwargs, 'endpoint') - self.api_versions = _get_service_values(kwargs, 'api_version') - self.image_api_use_tasks = image_api_use_tasks - - self.secgroup_source = kwargs.get('secgroup_source', None) - (self.verify, self.cert) = cloud_config.get_requests_verify_args() self._cache = cache.make_region( @@ -324,6 +265,8 @@ def __init__(self, cloud, auth, self._swift_service = None self._trove_client = None + self.cloud_config = cloud_config + @contextlib.contextmanager def _neutron_exceptions(self, error_message): try: @@ -365,12 +308,6 @@ def generate_key(*args, **kwargs): return ans return generate_key - def get_service_type(self, service): - return self.service_types.get(service, service) - - def get_service_name(self, service): - return self.service_names.get(service, None) - @property def nova_client(self): if self._nova_client is None: @@ -380,9 +317,9 @@ def nova_client(self): # trigger exception on lack of compute. (what?) self.get_session_endpoint('compute') self._nova_client = nova_client.Client( - self.api_versions['compute'], + self.cloud_config.get_api_version('compute'), session=self.keystone_session, - service_name=self.get_service_name('compute'), + service_name=self.cloud_config.get_service_name('compute'), region_name=self.region_name, timeout=self.api_timeout) except Exception: @@ -409,13 +346,13 @@ def _get_auth_plugin_class(self): plugin=self.auth_type, error=str(e))) def _get_identity_client_class(self): - if self.api_versions['identity'] == '3': + if self.cloud_config.get_api_version('identity') == '3': return k3_client.Client - elif self.api_versions['identity'] in ('2', '2.0'): + elif self.cloud_config.get_api_version('identity') in ('2', '2.0'): return k2_client.Client raise OpenStackCloudException( "Unknown identity API version: {version}".format( - version=self.api_versions['identity'])) + version=self.cloud_config.get_api_version('identity'))) @property def keystone_session(self): @@ -488,7 +425,7 @@ def _get_project_param_dict(self, name_or_id): project_dict = dict() if name_or_id: project_id = self._get_project(name_or_id).id - if self.api_versions['identity'] == '3': + if self.cloud_config.get_api_version('identity') == '3': project_dict['default_project'] = project_id else: project_dict['tenant_id'] = project_id @@ -501,7 +438,7 @@ def _get_domain_param_dict(self, domain_id): # not. However, keystone v2 does not allow user creation by non-admin # users, so we can throw an error to the user that does not need to # mention api versions - if self.api_versions['identity'] == '3': + if self.cloud_config.get_api_version('identity') == '3': if not domain_id: raise OpenStackCloudException( "User creation requires an explicit domain_id argument.") @@ -667,7 +604,8 @@ def glance_client(self): kwargs['timeout'] = self.api_timeout try: self._glance_client = glanceclient.Client( - self.api_versions['image'], endpoint, token=token, + self.cloud_config.get_api_version('image'), + endpoint, token=token, session=self.keystone_session, insecure=not self.verify, cacert=self.cert, **kwargs) except Exception as e: @@ -689,7 +627,7 @@ def swift_client(self): self._swift_client = swift_client.Connection( preauthurl=endpoint, preauthtoken=token, - auth_version=self.api_versions['identity'], + auth_version=self.cloud_config.get_api_version('identity'), os_options=dict( auth_token=token, object_storage_url=endpoint, @@ -750,10 +688,10 @@ def trove_client(self): # Make the connection - can't use keystone session until there # is one self._trove_client = trove_client.Client( - self.api_versions['database'], + self.cloud_config.get_api_version('database'), session=self.keystone_session, region_name=self.region_name, - service_type=self.get_service_type('database'), + service_type=self.cloud_config.get_service_type('database'), timeout=self.api_timeout, ) @@ -780,7 +718,7 @@ def neutron_client(self): def designate_client(self): if self._designate_client is None: # get dns service type if defined in cloud config - dns_service_type = self.get_service_type('dns') + dns_service_type = self.cloud_config.get_service_type('dns') # trigger exception on lack of designate self.get_session_endpoint(dns_service_type) @@ -829,8 +767,9 @@ def get_flavor_by_ram(self, ram, include=None): ram=ram, include=include)) def get_session_endpoint(self, service_key): - if service_key in self.endpoints: - return self.endpoints[service_key] + override_endpoint = self.cloud_config.get_endpoint(service_key) + if override_endpoint: + return override_endpoint try: # keystone is a special case in keystone, because what? if service_key == 'identity': @@ -838,9 +777,11 @@ def get_session_endpoint(self, service_key): interface=ksc_auth.AUTH_INTERFACE) else: endpoint = self.keystone_session.get_endpoint( - service_type=self.get_service_type(service_key), - service_name=self.get_service_name(service_key), - interface=self.endpoint_type, + service_type=self.cloud_config.get_service_type( + service_key), + service_name=self.cloud_config.get_service_name( + service_key), + interface=self.cloud_config.get_interface(service_key), region_name=self.region_name) except keystone_exceptions.EndpointNotFound as e: self.log.debug( @@ -1417,7 +1358,7 @@ def delete_image(self, name_or_id, wait=False, timeout=3600): try: # Note that in v1, the param name is image, but in v2, # it's image_id - glance_api_version = self.api_versions['image'] + glance_api_version = self.cloud_config.get_api_version('image') if glance_api_version == '2': self.manager.submitTask( _tasks.ImageDelete(image_id=image.id)) @@ -1455,7 +1396,7 @@ def create_image( kwargs[IMAGE_SHA256_KEY] = sha256 if disable_vendor_agent: - kwargs.update(self._cloud_config.config['disable_vendor_agent']) + kwargs.update(self.cloud_config.config['disable_vendor_agent']) try: # This makes me want to die inside @@ -1505,7 +1446,7 @@ def _upload_image_put_v1(self, name, image_data, **image_kwargs): def _upload_image_put(self, name, filename, **image_kwargs): image_data = open(filename, 'rb') # Because reasons and crying bunnies - if self.api_versions['image'] == '2': + if self.cloud_config.get_api_version('image') == '2': image = self._upload_image_put_v2(name, image_data, **image_kwargs) else: image = self._upload_image_put_v1(name, image_data, **image_kwargs) @@ -1586,7 +1527,7 @@ def update_image_properties( img_props[k] = v # This makes me want to die inside - if self.api_versions['image'] == '2': + if self.cloud_config.get_api_version('image') == '2': return self._update_image_properties_v2(image, img_props) else: return self._update_image_properties_v1(image, img_props) @@ -3371,20 +3312,8 @@ class OperatorCloud(OpenStackCloud): of which OpenStack service those operations are for. See the :class:`OpenStackCloud` class for a description of most options. - `OperatorCloud` overrides the default value of `endpoint_type` from - `public` to `admin`. - - :param string endpoint_type: The type of endpoint to get for services - from the service catalog. Valid types are - `public` ,`internal` or `admin`. (optional, - defaults to `admin`) """ - def __init__(self, *args, **kwargs): - super(OperatorCloud, self).__init__(*args, **kwargs) - if 'endpoint_type' not in kwargs: - self.endpoint_type = 'admin' - @property def auth_token(self): if self.auth_type in (None, "None", ''): @@ -3425,7 +3354,8 @@ def ironic_client(self): endpoint = self.get_session_endpoint(service_key='baremetal') try: self._ironic_client = ironic_client.Client( - self.api_versions['baremetal'], endpoint, token=token, + self.cloud_config.get_api_version('baremetal'), + endpoint, token=token, timeout=self.api_timeout, os_ironic_api_version=ironic_api_microversion) except Exception as e: diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index cd6ca6cbb..9c2d662c3 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -261,9 +261,10 @@ def _call_create_image(self, name, container=None): self.cloud.create_image( name, imagefile.name, container=container, wait=True) + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, 'glance_client') - def test_create_image_put_v1(self, glance_mock): - self.cloud.api_versions['image'] = '1' + def test_create_image_put_v1(self, glance_mock, mock_api_version): + mock_api_version.return_value = '1' glance_mock.images.list.return_value = [] self.assertEqual([], self.cloud.list_images()) @@ -280,9 +281,10 @@ def test_create_image_put_v1(self, glance_mock): fake_image_dict = meta.obj_to_dict(fake_image) self.assertEqual([fake_image_dict], self.cloud.list_images()) + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, 'glance_client') - def test_create_image_put_v2(self, glance_mock): - self.cloud.api_versions['image'] = '2' + def test_create_image_put_v2(self, glance_mock, mock_api_version): + mock_api_version.return_value = '2' self.cloud.image_api_use_tasks = False glance_mock.images.list.return_value = [] @@ -301,6 +303,7 @@ def test_create_image_put_v2(self, glance_mock): fake_image_dict = meta.obj_to_dict(fake_image) self.assertEqual([fake_image_dict], self.cloud.list_images()) + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, '_get_file_hashes') @mock.patch.object(shade.OpenStackCloud, 'glance_client') @mock.patch.object(shade.OpenStackCloud, 'swift_client') @@ -309,8 +312,9 @@ def test_create_image_task(self, swift_service_mock, swift_mock, glance_mock, - get_file_hashes): - self.cloud.api_versions['image'] = '2' + get_file_hashes, + mock_api_version): + mock_api_version.return_value = '2' self.cloud.image_api_use_tasks = True class Container(object): diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index 2a631a825..9895b28ed 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -20,6 +20,7 @@ """ from mock import patch, Mock +import os_client_config from shade import meta from shade import OpenStackCloud from shade.exc import (OpenStackCloudException, OpenStackCloudTimeout) @@ -30,7 +31,8 @@ class TestCreateServer(base.TestCase): def setUp(self): super(TestCreateServer, self).setUp() - self.client = OpenStackCloud("cloud", {}) + config = os_client_config.OpenStackConfig() + self.client = OpenStackCloud(cloud_config=config.get_one_cloud()) def test_create_server_with_create_exception(self): """ diff --git a/shade/tests/unit/test_delete_server.py b/shade/tests/unit/test_delete_server.py index 0ea2b4902..f2187481d 100644 --- a/shade/tests/unit/test_delete_server.py +++ b/shade/tests/unit/test_delete_server.py @@ -21,6 +21,7 @@ import mock from novaclient import exceptions as nova_exc +import os_client_config from shade import OpenStackCloud from shade import exc as shade_exc @@ -40,7 +41,8 @@ class TestDeleteServer(base.TestCase): def setUp(self): super(TestDeleteServer, self).setUp() - self.cloud = OpenStackCloud("cloud", {}) + config = os_client_config.OpenStackConfig() + self.cloud = OpenStackCloud(cloud_config=config.get_one_cloud()) @mock.patch('shade.OpenStackCloud.nova_client') def test_delete_server(self, nova_mock): diff --git a/shade/tests/unit/test_domain_params.py b/shade/tests/unit/test_domain_params.py index 56bbcb9d6..79a37764c 100644 --- a/shade/tests/unit/test_domain_params.py +++ b/shade/tests/unit/test_domain_params.py @@ -13,6 +13,7 @@ # under the License. import mock +import os_client_config as occ import bunch @@ -27,11 +28,11 @@ def setUp(self): super(TestDomainParams, self).setUp() self.cloud = shade.openstack_cloud() + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, '_get_project') - def test_identity_params_v3(self, mock_get_project): + def test_identity_params_v3(self, mock_get_project, mock_api_version): mock_get_project.return_value = bunch.Bunch(id=1234) - - self.cloud.api_versions = dict(identity='3') + mock_api_version.return_value = '3' ret = self.cloud._get_identity_params(domain_id='5678', project='bar') self.assertIn('default_project', ret) @@ -39,22 +40,23 @@ def test_identity_params_v3(self, mock_get_project): self.assertIn('domain', ret) self.assertEqual(ret['domain'], '5678') + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, '_get_project') - def test_identity_params_v3_no_domain(self, mock_get_project): + def test_identity_params_v3_no_domain( + self, mock_get_project, mock_api_version): mock_get_project.return_value = bunch.Bunch(id=1234) - - self.cloud.api_versions = dict(identity='3') + mock_api_version.return_value = '3' self.assertRaises( exc.OpenStackCloudException, self.cloud._get_identity_params, domain_id=None, project='bar') + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, '_get_project') - def test_identity_params_v2(self, mock_get_project): + def test_identity_params_v2(self, mock_get_project, mock_api_version): mock_get_project.return_value = bunch.Bunch(id=1234) - - self.cloud.api_versions = dict(identity='2') + mock_api_version.return_value = '2' ret = self.cloud._get_identity_params(domain_id='foo', project='bar') self.assertIn('tenant_id', ret) diff --git a/shade/tests/unit/test_endpoints.py b/shade/tests/unit/test_endpoints.py index d4ad3bb1b..6b03f1e89 100644 --- a/shade/tests/unit/test_endpoints.py +++ b/shade/tests/unit/test_endpoints.py @@ -20,6 +20,7 @@ """ from mock import patch +import os_client_config from shade import OperatorCloud from shade.tests.fakes import FakeEndpoint from shade.tests.unit import base @@ -38,7 +39,8 @@ class TestCloudEndpoints(base.TestCase): def setUp(self): super(TestCloudEndpoints, self).setUp() - self.client = OperatorCloud("op_cloud", {}) + config = os_client_config.OpenStackConfig() + self.client = OperatorCloud(cloud_config=config.get_one_cloud()) self.mock_ks_endpoints = \ [FakeEndpoint(**kwa) for kwa in self.mock_endpoints] diff --git a/shade/tests/unit/test_floating_ip_common.py b/shade/tests/unit/test_floating_ip_common.py index 7c5e193dc..85b879ca3 100644 --- a/shade/tests/unit/test_floating_ip_common.py +++ b/shade/tests/unit/test_floating_ip_common.py @@ -20,6 +20,7 @@ """ from mock import patch +import os_client_config from shade import meta from shade import OpenStackCloud from shade.tests.fakes import FakeServer @@ -29,7 +30,8 @@ class TestFloatingIP(base.TestCase): def setUp(self): super(TestFloatingIP, self).setUp() - self.client = OpenStackCloud("cloud", {}) + config = os_client_config.OpenStackConfig() + self.client = OpenStackCloud(cloud_config=config.get_one_cloud()) @patch.object(OpenStackCloud, 'get_floating_ip') @patch.object(OpenStackCloud, 'attach_ip_to_server') diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 9f0dd069b..115a93caf 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -20,6 +20,7 @@ """ from mock import patch +import os_client_config from neutronclient.common import exceptions as n_exc @@ -121,7 +122,8 @@ def assertAreInstances(self, elements, elem_type): def setUp(self): super(TestFloatingIP, self).setUp() # floating_ip_source='neutron' is default for OpenStackCloud() - self.client = OpenStackCloud("cloud", {}) + config = os_client_config.OpenStackConfig() + self.client = OpenStackCloud(cloud_config=config.get_one_cloud()) @patch.object(OpenStackCloud, 'neutron_client') @patch.object(OpenStackCloud, 'has_service') diff --git a/shade/tests/unit/test_floating_ip_nova.py b/shade/tests/unit/test_floating_ip_nova.py index 9a8a5dade..061bf67c4 100644 --- a/shade/tests/unit/test_floating_ip_nova.py +++ b/shade/tests/unit/test_floating_ip_nova.py @@ -20,6 +20,7 @@ """ from mock import patch +import os_client_config from novaclient import exceptions as n_exc @@ -69,7 +70,8 @@ def assertAreInstances(self, elements, elem_type): def setUp(self): super(TestFloatingIP, self).setUp() - self.client = OpenStackCloud("cloud", {}) + config = os_client_config.OpenStackConfig() + self.client = OpenStackCloud(cloud_config=config.get_one_cloud()) @patch.object(OpenStackCloud, 'nova_client') @patch.object(OpenStackCloud, 'has_service') diff --git a/shade/tests/unit/test_floating_ip_pool.py b/shade/tests/unit/test_floating_ip_pool.py index 507d9f655..7443aa7ad 100644 --- a/shade/tests/unit/test_floating_ip_pool.py +++ b/shade/tests/unit/test_floating_ip_pool.py @@ -20,6 +20,7 @@ """ from mock import patch +import os_client_config from shade import OpenStackCloud from shade import OpenStackCloudException from shade.tests.unit import base @@ -33,7 +34,8 @@ class TestFloatingIPPool(base.TestCase): def setUp(self): super(TestFloatingIPPool, self).setUp() - self.client = OpenStackCloud('cloud', {}) + config = os_client_config.OpenStackConfig() + self.client = OpenStackCloud(cloud_config=config.get_one_cloud()) @patch.object(OpenStackCloud, '_has_nova_extension') @patch.object(OpenStackCloud, 'nova_client') diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index 7ec2f735b..93d5cb352 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -14,6 +14,7 @@ # under the License. import mock +import os_client_config from swiftclient import client as swift_client from swiftclient import service as swift_service from swiftclient import exceptions as swift_exc @@ -28,7 +29,8 @@ class TestObject(base.TestCase): def setUp(self): super(TestObject, self).setUp() - self.cloud = OpenStackCloud('cloud', {}) + config = os_client_config.OpenStackConfig() + self.cloud = OpenStackCloud(cloud_config=config.get_one_cloud()) @mock.patch.object(swift_client, 'Connection') @mock.patch.object(shade.OpenStackCloud, 'auth_token', diff --git a/shade/tests/unit/test_port.py b/shade/tests/unit/test_port.py index ca3b866c9..3e56a6b40 100644 --- a/shade/tests/unit/test_port.py +++ b/shade/tests/unit/test_port.py @@ -20,6 +20,7 @@ """ from mock import patch +import os_client_config from shade import OpenStackCloud from shade.exc import OpenStackCloudException from shade.tests.unit import base @@ -143,7 +144,8 @@ class TestPort(base.TestCase): def setUp(self): super(TestPort, self).setUp() - self.client = OpenStackCloud('cloud', {}) + config = os_client_config.OpenStackConfig() + self.client = OpenStackCloud(cloud_config=config.get_one_cloud()) @patch.object(OpenStackCloud, 'neutron_client') def test_create_port(self, mock_neutron_client): diff --git a/shade/tests/unit/test_rebuild_server.py b/shade/tests/unit/test_rebuild_server.py index 160848fd7..c4e6c7c16 100644 --- a/shade/tests/unit/test_rebuild_server.py +++ b/shade/tests/unit/test_rebuild_server.py @@ -20,6 +20,7 @@ """ from mock import patch, Mock +import os_client_config from shade import meta from shade import OpenStackCloud from shade.exc import (OpenStackCloudException, OpenStackCloudTimeout) @@ -30,7 +31,8 @@ class TestRebuildServer(base.TestCase): def setUp(self): super(TestRebuildServer, self).setUp() - self.client = OpenStackCloud("cloud", {}) + config = os_client_config.OpenStackConfig() + self.client = OpenStackCloud(cloud_config=config.get_one_cloud()) def test_rebuild_server_rebuild_exception(self): """ diff --git a/shade/tests/unit/test_services.py b/shade/tests/unit/test_services.py index 1056d6708..79172efd7 100644 --- a/shade/tests/unit/test_services.py +++ b/shade/tests/unit/test_services.py @@ -20,6 +20,7 @@ """ from mock import patch +import os_client_config from shade import OpenStackCloudException from shade import OperatorCloud from shade.tests.fakes import FakeService @@ -40,7 +41,8 @@ class CloudServices(base.TestCase): def setUp(self): super(CloudServices, self).setUp() - self.client = OperatorCloud("op_cloud", {}) + config = os_client_config.OpenStackConfig() + self.client = OperatorCloud(cloud_config=config.get_one_cloud()) self.mock_ks_services = [FakeService(**kwa) for kwa in self.mock_services] diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index e7a5bf6c0..72ca680ea 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -21,6 +21,7 @@ from keystoneclient.v3 import client as k3_client from neutronclient.common import exceptions as n_exc +import os_client_config.cloud_config import shade from shade import exc from shade import meta @@ -55,22 +56,28 @@ def test_get_auth_password(self): plugin, keystoneclient.auth.identity.generic.password.Password) - def test_get_client_v2(self): - self.cloud.api_versions['identity'] = '2' + @mock.patch.object( + os_client_config.cloud_config.CloudConfig, 'get_api_version') + def test_get_client_v2(self, mock_api_version): + mock_api_version.return_value = '2' self.assertIs( self.cloud._get_identity_client_class(), k2_client.Client) - def test_get_client_v3(self): - self.cloud.api_versions['identity'] = '3' + @mock.patch.object( + os_client_config.cloud_config.CloudConfig, 'get_api_version') + def test_get_client_v3(self, mock_api_version): + mock_api_version.return_value = '3' self.assertIs( self.cloud._get_identity_client_class(), k3_client.Client) - def test_get_client_v4(self): - self.cloud.api_versions['identity'] = '4' + @mock.patch.object( + os_client_config.cloud_config.CloudConfig, 'get_api_version') + def test_get_client_v4(self, mock_api_version): + mock_api_version.return_value = '4' self.assertRaises( exc.OpenStackCloudException, diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index 48159b8cc..47ac46a3a 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -17,6 +17,7 @@ import mock import testtools +import os_client_config.cloud_config import shade from shade import exc from shade import meta @@ -548,8 +549,10 @@ class Image(object): self.assertEqual('22', self.cloud.get_image_id('22')) self.assertEqual('22', self.cloud.get_image_id('22 name')) - def test_get_session_endpoint_provided(self): - self.cloud.endpoints['image'] = 'http://fake.url' + @mock.patch.object( + os_client_config.cloud_config.CloudConfig, 'get_endpoint') + def test_get_session_endpoint_provided(self, fake_get_endpoint): + fake_get_endpoint.return_value = 'http://fake.url' self.assertEqual( 'http://fake.url', self.cloud.get_session_endpoint('image')) From 8218cb261db734be64a2222136fc18fa05174602 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 29 Mar 2015 01:38:00 -0400 Subject: [PATCH 0411/3836] Pass socket timeout to swiftclient Needs a release of swiftclient containing the depended-on patch. We're passing timeout to everyone else now, so for consistency, we should pass it to swiftclient too. Depends-On: I699ebb1e092aa010af678de7ba15712da6ed5315 Change-Id: I864403e0ac32f2645026dd13189a03cb9b0eeb01 --- requirements.txt | 2 +- shade/__init__.py | 1 + shade/tests/unit/test_object.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ea31f8e94..863827c17 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ python-cinderclient<1.2 python-neutronclient>=2.3.10 python-troveclient python-ironicclient>=0.5.1 -python-swiftclient +python-swiftclient>=2.5.0 python-designateclient>=1.3.0 dogpile.cache>=0.5.3 diff --git a/shade/__init__.py b/shade/__init__.py index 3420e7e72..549f8096a 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -694,6 +694,7 @@ def swift_client(self): auth_token=token, object_storage_url=endpoint, region_name=self.region_name), + timeout=self.api_timeout, ) except OpenStackCloudException: raise diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index 7ec2f735b..6c19bb35e 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -42,6 +42,7 @@ def test_swift_client(self, endpoint_mock, auth_mock, swift_mock): preauthurl='danzig', preauthtoken='yankee', auth_version='2', + timeout=None, os_options=dict( object_storage_url='danzig', auth_token='yankee', From 914d82a33a21ea8848cef3be80bd8e3b94641cfc Mon Sep 17 00:00:00 2001 From: Atsushi SAKAI Date: Wed, 29 Jul 2015 14:34:12 +0900 Subject: [PATCH 0412/3836] Fix two typos and one readablity on shade documentation resouce => resource expse => expose virtualenvwrapper => virtualenv wrapper Change-Id: Id4dbcb4852eb2ef5dac4e0379e384c945c93a1ae --- README.rst | 4 ++-- doc/source/installation.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 7001a94c5..b4dc82f04 100644 --- a/README.rst +++ b/README.rst @@ -188,7 +188,7 @@ instance, if the user says "please upload this image", shade should figure out which sequence of actions need to be performed and should get the job done. If the resource isn't present on some clouds, but the overall concept the -resouce represents is, a different resource should present the concept. For +resource represents is, a different resource should present the concept. For instance, while some clouds do not have floating ips, if what the user wants is "a server with an IP" - then the fact that one needs to request a floating ip on some clouds is a detail, and the right thing for that to be is a quality @@ -206,7 +206,7 @@ Functional Interface -------------------- shade should also provide a functional mapping to the object interface that -does not expse the object interface at all. For instance, fora resource type +does not expose the object interface at all. For instance, fora resource type `server`, one could expect the following. :: diff --git a/doc/source/installation.rst b/doc/source/installation.rst index a7b1bc3f4..c47e48758 100644 --- a/doc/source/installation.rst +++ b/doc/source/installation.rst @@ -6,7 +6,7 @@ At the command line:: $ pip install shade -Or, if you have virtualenvwrapper installed:: +Or, if you have virtualenv wrapper installed:: $ mkvirtualenv shade $ pip install shade \ No newline at end of file From b1ae2233881d59a8d1187693446b2f0dd57334a4 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 28 Jul 2015 17:18:54 -0400 Subject: [PATCH 0413/3836] Remove unused server functions These functions are used neither in the Ansible modules, nor in the nodepool code. Their existence is confusing as they serve no purpose other than to add extra attributes to the server dict. They are holdovers from when we still returned server objects, not dicts. Change-Id: I06e5c2b7bfe3ba098016d5348e41e7002b361e6c --- shade/__init__.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 3420e7e72..2f263adf4 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -863,10 +863,6 @@ def has_service(self, service_key): except OpenStackCloudException: return False - def list_server_dicts(self): - return [self.get_openstack_vars(server) - for server in self.list_servers()] - @_cache_on_arguments() def _nova_extensions(self): extensions = set() @@ -1852,12 +1848,6 @@ def get_server_private_ip(self, server): def get_server_public_ip(self, server): return meta.get_server_external_ipv4(self, server) - def get_server_dict(self, name_or_id): - server = self.get_server(name_or_id) - if not server: - return server - return self.get_openstack_vars(server) - def get_server_meta(self, server): # TODO(mordred) remove once ansible has moved to Inventory interface server_vars = meta.get_hostvars_from_server(self, server) From 6f09b546026b95b3034e76d198b05e2d89028694 Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Tue, 28 Jul 2015 22:47:56 -0700 Subject: [PATCH 0414/3836] Document create_object Change-Id: Ia9c4ffd27c1dbef1bff69aaef8100010cfe0d219 --- shade/__init__.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index de0110cb7..f7c1945f3 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2611,6 +2611,26 @@ def create_object( self, container, name, filename=None, md5=None, sha256=None, segment_size=None, **headers): + """Create a file object + + :param container: The name of the container to store the file in. + This container will be created if it does not exist already. + :param name: Name for the object within the container. + :param filename: The path to the local file whose contents will be + uploaded. + :param md5: A hexadecimal md5 of the file. (Optional), if it is known + and can be passed here, it will save repeating the expensive md5 + process. It is assumed to be accurate. + :param sha256: A hexadecimal sha256 of the file. (Optional) See md5. + :param segment_size: Break the uploaded object into segments of this + many bytes. (Optional) Shade will attempt to discover the maximum + value for this from the server if it is not specified, or will use + a reasonable default. + :param headers: These will be passed through to the object creation + API as HTTP Headers. + + :raises: ``OpenStackCloudException`` on operation error. + """ if not filename: filename = name From 264fa42bfdd0af5cc4407dfe398472be74ea42b5 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 29 Jul 2015 13:36:44 -0400 Subject: [PATCH 0415/3836] Remove meta.get_server_public_ip() function This method is unused and outdated. There is a public method in the OpenStackCloud class named get_server_public_ip() that calls the correct function in meta.py. Change-Id: I852844a003f9d09d1579ce3509caa04cbbffd270 --- shade/meta.py | 4 ---- shade/tests/unit/test_meta.py | 7 +++++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/shade/meta.py b/shade/meta.py index 9d2425f3c..2b162616e 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -66,10 +66,6 @@ def get_server_private_ip(server): return get_server_ip(server, ext_tag='fixed', key_name='private') -def get_server_public_ip(server): - return get_server_ip(server, ext_tag='floating', key_name='public') - - def get_server_external_ipv4(cloud, server): """Find an externally routable IP for the server. diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 13915b784..87b5eeaca 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -104,9 +104,12 @@ def test_find_nova_addresses_all(self): addrs, key_name='public', ext_tag='fixed', version=6)) def test_get_server_ip(self): - srv = FakeServer() + srv = meta.obj_to_dict(FakeServer()) self.assertEqual(PRIVATE_V4, meta.get_server_private_ip(srv)) - self.assertEqual(PUBLIC_V4, meta.get_server_public_ip(srv)) + self.assertEqual(PUBLIC_V4, + meta.get_server_external_ipv4(shade.openstack_cloud(), + srv) + ) @mock.patch.object(shade.OpenStackCloud, 'has_service') @mock.patch.object(shade.OpenStackCloud, 'search_ports') From a8e8c809eeec0f09b5d9207c29efb3c6a1f9c088 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 1 Aug 2015 12:45:16 +1000 Subject: [PATCH 0416/3836] Align to generic password auth-type v2password can't handle v3 parameter names. But we align to those for sanity. Ensure that a user gets the v2plugin that can handle the auth instead of the one that can't. Change-Id: Ie693e613fd5d0e20a4837923300502b1de02364b --- os_client_config/config.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/os_client_config/config.py b/os_client_config/config.py index 015a783d6..1840d18dc 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -314,6 +314,12 @@ def _fix_backwards_auth_plugin(self, cloud): target = cloud[key] del cloud[key] cloud[target_key] = target + # Because we force alignment to v3 nouns, we want to force + # use of the auth plugin that can do auto-selection and dealing + # with that based on auth parameters. v2password is basically + # completely broken + if cloud['auth_type'] == 'v2password': + cloud['auth_type'] = 'password' return cloud def _fix_backwards_interface(self, cloud): From 0af29bda2fe3ed9ac6ff7e2237c52471ba413cd3 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 4 Aug 2015 09:08:32 -0400 Subject: [PATCH 0417/3836] Clarify future changes in docs The README was discussing future design decisions as if they were already implemented. This can be confusing for new users. This separates that discussion into a separate doc page and clarifies its intentions. Also, fix sphinx doc build warnings. Change-Id: Ie66b60d972cae25a9805804ad17632aed0932627 --- README.rst | 180 ++---------------------------------------- doc/source/future.rst | 176 +++++++++++++++++++++++++++++++++++++++++ doc/source/index.rst | 3 +- shade/__init__.py | 51 ++++++------ 4 files changed, 209 insertions(+), 201 deletions(-) create mode 100644 doc/source/future.rst diff --git a/README.rst b/README.rst index b4dc82f04..9b1d254ca 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ -shade -===== +Introduction +============ shade is a simple client library for operating OpenStack clouds. The key word here is *simple*. Clouds can do many many many things - but there are @@ -17,18 +17,18 @@ library, and adding logic and features that the OpenStack Infra team had developed to run client applications at scale, it turned out that we'd written nine-tenths of what we'd need to have a standalone library. -example -------- +Example +======= Sometimes an example is nice. :: - from shade import * + import shade import time # Initialize cloud # Cloud configs are read with os-client-config - cloud = openstack_cloud(cloud='mordred') + cloud = shade.openstack_cloud(cloud='mordred') # OpenStackCloud object has an interface exposing OpenStack services methods print cloud.list_servers() @@ -50,171 +50,3 @@ Sometimes an example is nice. attachments = cinder.volumes.get(volume_id).attachments print attachments - -Object Design -============= - -Shade is a library for managing resources, not for operating APIs. As such, -it is the resource in question that is the primary object and not the service -that may or may not provide that resource, much as we may feel warm and fuzzy -to one of the services. - -Every resource at minimum has CRUD functions. Additionally, every resource -action should have a "do this task blocking" or "request that the cloud start -this action and give me a way to check its status" The creation and deletion -of Resources will be handled by a ResourceManager that is attached to the Cloud -:: - - class Cloud: - ResourceManager server - servers = server - ResourceManager floating_ip - floating_ips = floating_ip - ResourceManager image - images = image - ResourceManager role - roles = role - ResourceManager volume - volumes = volume - -getting, listing and searching ------------------------------- - -In addition to creating a resource, there are different ways of getting your -hands on a resource. A `get`, a `list` and a `search`. - -`list` has the simplest semantics - it takes no parameters and simply returns -a list of all of the resources that exist. - -`search` takes a set of parameters to match against and returns a list of -resources that match the parameters given. If no resources match, it returns -an empty list. - -`get` takes the same set of parameters that `search` takes, but will only ever -return a single matching resource or None. If multiple resources are matched, -an exception will be raised. - -:: - - class ResourceManager: - def get -> Resource - def list -> List - def search -> List - def create -> Resource - -Cloud and ResourceManager interface ------------------------------------ - -All ResourceManagers should accept a cache object passed in to their constructor -and should additionally pass that cache object to all Resource constructors. -The top-level cloud should create the cache object, then pass it to each of -the ResourceManagers when it creates them. - -Client connection objects should exist and be managed at the Cloud level. A -backreference to the OpenStack cloud should be passed to every resource manager -so that ResourceManagers can get hold of the ones they need. For instance, -an Image ResourceManager would potentially need access to both the glance_client -and the swift_client. - -:: - - class ResourceManager - def __init__(self, cache, cloud) - class ServerManager(ResourceManager) - class OpenStackCloud - def __init__(self): - self.cache = dogpile.cache() - self.server = ServerManager(self.cache, self) - self.servers = self.server - -Any resources that have an association action - such as servers and -floating_ips, should carry reciprocal methods on each resource with absolutely -no difference in behavior. - -:: - - class Server(Resource): - def connect_floating_ip: - class FloatingIp(Resource): - def connect_server: - -Resource objects should have all of the accessor methods you'd expect, as well -as any other interesting rollup methods or actions. For instance, since -a keystone User can be enabled or disabled, one should expect that there -would be an enable() and a disable() method, and that those methods will -immediately operate the necessary REST apis. However, if you need to make 80 -changes to a Resource, 80 REST calls may or may not be silly, so there should -also be a generic update() method which can be used to request the minimal -amount of REST calls needed to update the attributes to the requested values. - -Resource objects should all have a to_dict method which will return a plain -flat dictionary of their attributes. - -:: - - class Resource: - def update(**new_values) -> Resource - def delete -> None, throws on error - -Readiness ---------- - -`create`, `get`, and `attach` can return resources that are not yet ready. Each -method should take a `wait` and a `timeout` parameter, that will cause the -request for the resource to block until it is ready. However, the user may -want to poll themselves. Each resource should have an `is_ready` method which -will return True when the resource is ready. The `wait` method then can -actually be implemented in the base Resource class as an iterate timeout -loop around calls to `is_ready`. Every Resource should also have an -`is_failed` and an `is_deleted` method. - -Optional Behavior ------------------ - -Not all clouds expose all features. For instance, some clouds do not have -floating ips. Additionally, some clouds may have the feature but the user -account does not, which is effectively the same thing. -This should be handled in several ways: - -If the user explicitly requests a resource that they do not have access to, -an error should be raised. For instance, if a user tries to create a floating -ip on a cloud that does not expose that feature to them, shade should throw -a "Your cloud does not let you do that" error. - -If the resource concept can be can be serviced by multiple possible services, -shade should transparently try all of them. The discovery method should use -the dogpile.cache mechanism so that it can be avoided on subsequent tries. For -instance, if the user says "please upload this image", shade should figure -out which sequence of actions need to be performed and should get the job done. - -If the resource isn't present on some clouds, but the overall concept the -resource represents is, a different resource should present the concept. For -instance, while some clouds do not have floating ips, if what the user wants -is "a server with an IP" - then the fact that one needs to request a floating -ip on some clouds is a detail, and the right thing for that to be is a quality -of a server and managed by the server resource. A floating ip resource should -really only be directly manipulated by the user if they were doing something -very floating-ip specific, such as moving a floating ip from one server to -another. - -In short, it should be considered a MASSIVE bug in shade if the shade user -ever has to have in their own code "if cloud.has_capability("X") do_thing -else do_other_thing" - since that construct conveys some resource that shade -should really be able to model. - -Functional Interface --------------------- - -shade should also provide a functional mapping to the object interface that -does not expose the object interface at all. For instance, fora resource type -`server`, one could expect the following. - -:: - - class OpenStackCloud: - def create_server - return self.server.create().to_dict() - def get_server - return self.server.get().to_dict() - def update_server - return self.server.get().update().to_dict() diff --git a/doc/source/future.rst b/doc/source/future.rst new file mode 100644 index 000000000..61c1a4af8 --- /dev/null +++ b/doc/source/future.rst @@ -0,0 +1,176 @@ +************************ +Future Design Discussion +************************ + +This document discusses a new approach to the Shade library and how +we might wish for it to operate in a future, not-yet-developed version. +It presents a more object oriented approach, and design decisions that +we have learned and decided on while working on the current version. + +Object Design +============= + +Shade is a library for managing resources, not for operating APIs. As such, +it is the resource in question that is the primary object and not the service +that may or may not provide that resource, much as we may feel warm and fuzzy +to one of the services. + +Every resource at minimum has CRUD functions. Additionally, every resource +action should have a "do this task blocking" or "request that the cloud start +this action and give me a way to check its status" The creation and deletion +of Resources will be handled by a ResourceManager that is attached to the Cloud +:: + + class Cloud: + ResourceManager server + servers = server + ResourceManager floating_ip + floating_ips = floating_ip + ResourceManager image + images = image + ResourceManager role + roles = role + ResourceManager volume + volumes = volume + +getting, listing and searching +------------------------------ + +In addition to creating a resource, there are different ways of getting your +hands on a resource. A `get`, a `list` and a `search`. + +`list` has the simplest semantics - it takes no parameters and simply returns +a list of all of the resources that exist. + +`search` takes a set of parameters to match against and returns a list of +resources that match the parameters given. If no resources match, it returns +an empty list. + +`get` takes the same set of parameters that `search` takes, but will only ever +return a single matching resource or None. If multiple resources are matched, +an exception will be raised. + +:: + + class ResourceManager: + def get -> Resource + def list -> List + def search -> List + def create -> Resource + +Cloud and ResourceManager interface +=================================== + +All ResourceManagers should accept a cache object passed in to their constructor +and should additionally pass that cache object to all Resource constructors. +The top-level cloud should create the cache object, then pass it to each of +the ResourceManagers when it creates them. + +Client connection objects should exist and be managed at the Cloud level. A +backreference to the OpenStack cloud should be passed to every resource manager +so that ResourceManagers can get hold of the ones they need. For instance, +an Image ResourceManager would potentially need access to both the glance_client +and the swift_client. + +:: + + class ResourceManager + def __init__(self, cache, cloud) + class ServerManager(ResourceManager) + class OpenStackCloud + def __init__(self): + self.cache = dogpile.cache() + self.server = ServerManager(self.cache, self) + self.servers = self.server + +Any resources that have an association action - such as servers and +floating_ips, should carry reciprocal methods on each resource with absolutely +no difference in behavior. + +:: + + class Server(Resource): + def connect_floating_ip: + class FloatingIp(Resource): + def connect_server: + +Resource objects should have all of the accessor methods you'd expect, as well +as any other interesting rollup methods or actions. For instance, since +a keystone User can be enabled or disabled, one should expect that there +would be an enable() and a disable() method, and that those methods will +immediately operate the necessary REST apis. However, if you need to make 80 +changes to a Resource, 80 REST calls may or may not be silly, so there should +also be a generic update() method which can be used to request the minimal +amount of REST calls needed to update the attributes to the requested values. + +Resource objects should all have a to_dict method which will return a plain +flat dictionary of their attributes. + +:: + + class Resource: + def update(**new_values) -> Resource + def delete -> None, throws on error + +Readiness +--------- + +`create`, `get`, and `attach` can return resources that are not yet ready. Each +method should take a `wait` and a `timeout` parameter, that will cause the +request for the resource to block until it is ready. However, the user may +want to poll themselves. Each resource should have an `is_ready` method which +will return True when the resource is ready. The `wait` method then can +actually be implemented in the base Resource class as an iterate timeout +loop around calls to `is_ready`. Every Resource should also have an +`is_failed` and an `is_deleted` method. + +Optional Behavior +----------------- + +Not all clouds expose all features. For instance, some clouds do not have +floating ips. Additionally, some clouds may have the feature but the user +account does not, which is effectively the same thing. +This should be handled in several ways: + +If the user explicitly requests a resource that they do not have access to, +an error should be raised. For instance, if a user tries to create a floating +ip on a cloud that does not expose that feature to them, shade should throw +a "Your cloud does not let you do that" error. + +If the resource concept can be can be serviced by multiple possible services, +shade should transparently try all of them. The discovery method should use +the dogpile.cache mechanism so that it can be avoided on subsequent tries. For +instance, if the user says "please upload this image", shade should figure +out which sequence of actions need to be performed and should get the job done. + +If the resource isn't present on some clouds, but the overall concept the +resource represents is, a different resource should present the concept. For +instance, while some clouds do not have floating ips, if what the user wants +is "a server with an IP" - then the fact that one needs to request a floating +ip on some clouds is a detail, and the right thing for that to be is a quality +of a server and managed by the server resource. A floating ip resource should +really only be directly manipulated by the user if they were doing something +very floating-ip specific, such as moving a floating ip from one server to +another. + +In short, it should be considered a MASSIVE bug in shade if the shade user +ever has to have in their own code "if cloud.has_capability("X") do_thing +else do_other_thing" - since that construct conveys some resource that shade +should really be able to model. + +Functional Interface +==================== + +shade should also provide a functional mapping to the object interface that +does not expose the object interface at all. For instance, for a resource type +`server`, one could expect the following. + +:: + + class OpenStackCloud: + def create_server + return self.server.create().to_dict() + def get_server + return self.server.get().to_dict() + def update_server + return self.server.get().update().to_dict() diff --git a/doc/source/index.rst b/doc/source/index.rst index 15a603f40..97455f584 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -4,7 +4,7 @@ contain the root `toctree` directive. Welcome to shade's documentation! -======================================================== +================================= Contents: @@ -14,6 +14,7 @@ Contents: installation usage contributing + future .. include:: ../../README.rst diff --git a/shade/__init__.py b/shade/__init__.py index a7b8f4629..81f613ada 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -3969,10 +3969,10 @@ def create_service(self, name, service_type, description=None): :returns: a dict containing the services description, i.e. the following attributes:: - - id: - - name: - - service_type: - - description: + - id: + - name: + - service_type: + - description: :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call. @@ -4028,10 +4028,10 @@ def get_service(self, name_or_id, filters=None): :returns: a dict containing the services description, i.e. the following attributes:: - - id: - - name: - - service_type: - - description: + - id: + - name: + - service_type: + - description: :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call or if multiple matches are found. @@ -4074,7 +4074,7 @@ def create_endpoint(self, service_name_or_id, public_url, :returns: a dict containing the endpoint description. - :raise OpenStackCloudException: if the service cannot be found or if + :raises: OpenStackCloudException if the service cannot be found or if something goes wrong during the openstack API call. """ # ToDo: support v3 api (dguerri) @@ -4122,11 +4122,11 @@ def search_endpoints(self, id=None, filters=None): :returns: a list of dict containing the endpoint description. Each dict contains the following attributes:: - - id: - - region: - - public_url: - - internal_url: (optional) - - admin_url: (optional) + - id: + - region: + - public_url: + - internal_url: (optional) + - admin_url: (optional) :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. @@ -4143,11 +4143,11 @@ def get_endpoint(self, id, filters=None): :returns: a dict containing the endpoint description. i.e. a dict containing the following attributes:: - - id: - - region: - - public_url: - - internal_url: (optional) - - admin_url: (optional) + - id: + - region: + - public_url: + - internal_url: (optional) + - admin_url: (optional) """ return _utils._get_entity(self.search_endpoints, id, filters) @@ -4258,9 +4258,9 @@ def search_identity_domains(self, **filters): :returns: a list of dicts containing the domain description. Each dict contains the following attributes:: - - id: - - name: - - description: + - id: + - name: + - description: :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. @@ -4280,10 +4280,9 @@ def get_identity_domain(self, domain_id): :returns: a dict containing the domain description, or None if not found. Each dict contains the following attributes:: - - id: - - name: - - description: - + - id: + - name: + - description: :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. From 80e1fbcd9072645c040a53cd0324e2596efa567f Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 29 Jul 2015 14:13:58 -0400 Subject: [PATCH 0418/3836] Be smarter finding private IP In a neutron enabled environment, we can be more intelligent about how we find a private IP address by looking for an IP on a network that has `router:external` as False and `shared` as False. Change-Id: I0029a04710ec39c997e051037cd32e671642b198 --- shade/__init__.py | 2 +- shade/meta.py | 45 +++++++++++++++++++++++++++++++---- shade/tests/unit/test_meta.py | 32 +++++++++++++++++++++---- 3 files changed, 70 insertions(+), 9 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 3420e7e72..6ed779a9e 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1847,7 +1847,7 @@ def get_server_id(self, name_or_id): return None def get_server_private_ip(self, server): - return meta.get_server_private_ip(server) + return meta.get_server_private_ip(server, self) def get_server_public_ip(self, server): return meta.get_server_external_ipv4(self, server) diff --git a/shade/meta.py b/shade/meta.py index 2b162616e..bbe1d20a9 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -62,8 +62,45 @@ def get_server_ip(server, **kwargs): return addrs[0] -def get_server_private_ip(server): - return get_server_ip(server, ext_tag='fixed', key_name='private') +def get_server_private_ip(server, cloud=None): + """Find the private IP address + + If Neutron is available, search for a port on a network where + `router:external` is False and `shared` is False. This combination + indicates a private network with private IP addresses. This port should + have the private IP. + + If Neutron is not available, or something goes wrong communicating with it, + as a fallback, try the list of addresses associated with the server dict, + looking for an IP type tagged as 'fixed' in the network named 'private'. + + Last resort, ignore the IP type and just look for an IP on the 'private' + network (e.g., Rackspace). + """ + if cloud and cloud.has_service('network'): + try: + server_ports = cloud.search_ports( + filters={'device_id': server['id']}) + nets = cloud.search_networks( + filters={'router:external': False, 'shared': False}) + except Exception as e: + log.debug( + "Something went wrong talking to neutron API: " + "'{msg}'. Trying with Nova.".format(msg=str(e))) + else: + for net in nets: + for port in server_ports: + if net['id'] == port['network_id']: + for ip in port['fixed_ips']: + if _utils.is_ipv4(ip['ip_address']): + return ip['ip_address'] + + ip = get_server_ip(server, ext_tag='fixed', key_name='private') + if ip: + return ip + + # Last resort, and Rackspace + return get_server_ip(server, key_name='private') def get_server_external_ipv4(cloud, server): @@ -93,7 +130,7 @@ def get_server_external_ipv4(cloud, server): # Search a fixed IP attached to an external net. Unfortunately # Neutron ports don't have a 'floating_ips' attribute server_ports = cloud.search_ports( - filters={'device_id': server.id}) + filters={'device_id': server['id']}) ext_nets = cloud.search_networks(filters={'router:external': True}) except Exception as e: log.debug( @@ -203,7 +240,7 @@ def get_hostvars_from_server(cloud, server, mounts=None): # not exist to remain consistent with the pre-existing missing values server_vars['public_v4'] = get_server_external_ipv4(cloud, server) or '' server_vars['public_v6'] = get_server_external_ipv6(server) or '' - server_vars['private_v4'] = get_server_private_ip(server) or '' + server_vars['private_v4'] = get_server_private_ip(server, cloud) or '' if cloud.private: interface_ip = server_vars['private_v4'] else: diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 87b5eeaca..ecb121d76 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -105,11 +105,35 @@ def test_find_nova_addresses_all(self): def test_get_server_ip(self): srv = meta.obj_to_dict(FakeServer()) + cloud = shade.openstack_cloud() self.assertEqual(PRIVATE_V4, meta.get_server_private_ip(srv)) - self.assertEqual(PUBLIC_V4, - meta.get_server_external_ipv4(shade.openstack_cloud(), - srv) - ) + self.assertEqual(PUBLIC_V4, meta.get_server_external_ipv4(cloud, srv)) + + @mock.patch.object(shade.OpenStackCloud, 'has_service') + @mock.patch.object(shade.OpenStackCloud, 'search_ports') + @mock.patch.object(shade.OpenStackCloud, 'search_networks') + def test_get_server_private_ip(self, mock_search_networks, + mock_search_ports, mock_has_service): + mock_has_service.return_value = True + mock_search_ports.return_value = [{ + 'network_id': 'test-net-id', + 'fixed_ips': [{'ip_address': PRIVATE_V4}], + 'device_id': 'test-id' + }] + mock_search_networks.return_value = [{'id': 'test-net-id'}] + + srv = meta.obj_to_dict(fakes.FakeServer( + id='test-id', name='test-name', status='ACTIVE')) + cloud = shade.openstack_cloud() + + self.assertEqual(PRIVATE_V4, meta.get_server_private_ip(srv, cloud)) + mock_has_service.assert_called_once_with('network') + mock_search_ports.assert_called_once_with( + filters={'device_id': 'test-id'} + ) + mock_search_networks.assert_called_once_with( + filters={'router:external': False, 'shared': False} + ) @mock.patch.object(shade.OpenStackCloud, 'has_service') @mock.patch.object(shade.OpenStackCloud, 'search_ports') From 84e3aae8da39142dc3acc97dd7ff2683400b34c5 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 1 Aug 2015 10:29:23 +1000 Subject: [PATCH 0419/3836] Throw an exception on a server without an IP It's possible for a server to reach ACTIVE without getting an IP. Of course, this server is not very useful. So, treat it as an error. Change-Id: I1f5fd2632c33dcddaa924bf37c0896b08e568753 --- shade/__init__.py | 26 ++++++++++++++++++++++++-- shade/tests/unit/test_create_server.py | 23 ++++++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index ce25ef45d..f9df45657 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2339,6 +2339,25 @@ def create_server(self, auto_ip=True, ips=None, ip_pool=None, continue if server['status'] == 'ACTIVE': + if not server['addresses']: + try: + self._delete_server( + server=server, wait=wait, timeout=timeout) + except Exception as e: + self.log.debug( + "Failed deleting server {server} that booted" + " without an IP address. Manual cleanup is" + " required.".format(server=server['id']), + exc_info=True) + raise OpenStackCloudException( + "Server reached ACTIVE state without being" + " allocated an IP address AND then could not" + " be deleted: {0}".format(e), + extra_data=dict(server=server)) + raise OpenStackCloudException( + 'Server reached ACTIVE state without being' + ' allocated an IP address.', + extra_data=dict(server=server)) return self.add_ips_to_server( server, auto_ip, ips, ip_pool) @@ -2378,8 +2397,11 @@ def rebuild_server(self, server_id, image_id, wait=False, timeout=180): extra_data=dict(server=server)) return meta.obj_to_dict(server) - def delete_server(self, name, wait=False, timeout=180): - server = self.get_server(name) + def delete_server(self, name_or_id, wait=False, timeout=180): + server = self.get_server(name_or_id) + return self._delete_server(server, wait=wait, timeout=timeout) + + def _delete_server(self, server, wait=False, timeout=180): if server: try: self.manager.submitTask(_tasks.ServerDelete(server=server.id)) diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index 9895b28ed..5a1ea3209 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -127,7 +127,8 @@ def test_create_server_wait(self): its status changes to "ACTIVE". """ with patch("shade.OpenStackCloud"): - fake_server = fakes.FakeServer('', '', 'ACTIVE') + fake_server = fakes.FakeServer( + '', '', 'ACTIVE', addresses=dict(public='1.1.1.1')) config = { "servers.create.return_value": fakes.FakeServer('', '', 'ACTIVE'), @@ -140,3 +141,23 @@ def test_create_server_wait(self): self.assertEqual( self.client.create_server(wait=True), fake_server) + + def test_create_server_no_addresses(self): + """ + Test that create_server with a wait throws an exception if the + server doesn't have addresses. + """ + with patch("shade.OpenStackCloud"): + fake_server = fakes.FakeServer('', '', 'ACTIVE') + config = { + "servers.create.return_value": fakes.FakeServer('', '', + 'ACTIVE'), + "servers.get.side_effect": [ + Mock(status="BUILD"), fake_server] + } + OpenStackCloud.nova_client = Mock(**config) + with patch.object(OpenStackCloud, "add_ips_to_server", + return_value=fake_server): + self.assertRaises( + OpenStackCloudException, self.client.create_server, + wait=True) From b55f88bddc211b46fe6513ae11c2b743469a1dd6 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 5 Aug 2015 09:14:01 -0400 Subject: [PATCH 0420/3836] Be consistent with accessing server dict We had several different places where we were treating the server dict as an object. Although it can currently be both because we use Bunch objects for backward compatibility, we should be consistent with treating it as a dict so that we can, hopefully, at some point stop using Bunch objects. And also we don't want to continue to provide bad examples. Change-Id: I27eb467ac2e86454a86566336a55e892e7025fda --- shade/__init__.py | 42 ++++++++++++++++++----------------- shade/meta.py | 20 ++++++++--------- shade/tests/unit/test_meta.py | 2 +- 3 files changed, 33 insertions(+), 31 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index f9df45657..8aa8b3aff 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1628,7 +1628,7 @@ def get_volumes(self, server, cache=True): volumes = [] for volume in self.list_volumes(cache=cache): for attach in volume['attachments']: - if attach['server_id'] == server.id: + if attach['server_id'] == server['id']: volumes.append(volume) return volumes @@ -1647,7 +1647,7 @@ def get_volume_attach_device(self, volume, server_id): This can also be used to verify if a volume is attached to a particular server. - :param volume: Volume object + :param volume: Volume dict :param server_id: ID of server to check :returns: Device name if attached, None if volume is not attached. @@ -1660,30 +1660,30 @@ def get_volume_attach_device(self, volume, server_id): def detach_volume(self, server, volume, wait=True, timeout=None): """Detach a volume from a server. - :param server: The server object to detach from. - :param volume: The volume object to detach. + :param server: The server dict to detach from. + :param volume: The volume dict to detach. :param wait: If true, waits for volume to be detached. :param timeout: Seconds to wait for volume detachment. None is forever. :raises: OpenStackCloudTimeout if wait time exceeded. :raises: OpenStackCloudException on operation error. """ - dev = self.get_volume_attach_device(volume, server.id) + dev = self.get_volume_attach_device(volume, server['id']) if not dev: raise OpenStackCloudException( "Volume %s is not attached to server %s" - % (volume['id'], server.id) + % (volume['id'], server['id']) ) try: self.manager.submitTask( _tasks.VolumeDetach(attachment_id=volume['id'], - server_id=server.id)) + server_id=server['id'])) except Exception as e: self.log.debug("nova volume detach failed", exc_info=True) raise OpenStackCloudException( "Error detaching volume %s from server %s: %s" % - (volume['id'], server.id, e) + (volume['id'], server['id'], e) ) if wait: @@ -1711,16 +1711,16 @@ def attach_volume(self, server, volume, device=None, """Attach a volume to a server. This will attach a volume, described by the passed in volume - object (as returned by get_volume()), to the server described by - the passed in server object (as returned by get_server()) on the + dict (as returned by get_volume()), to the server described by + the passed in server dict (as returned by get_server()) on the named device on the server. If the volume is already attached to the server, or generally not available, then an exception is raised. To re-attach to a server, but under a different device, the user must detach it first. - :param server: The server object to attach to. - :param volume: The volume object to attach. + :param server: The server dict to attach to. + :param volume: The volume dict to attach. :param device: The device name where the volume will attach. :param wait: If true, waits for volume to be attached. :param timeout: Seconds to wait for volume attachment. None is forever. @@ -1728,11 +1728,11 @@ def attach_volume(self, server, volume, device=None, :raises: OpenStackCloudTimeout if wait time exceeded. :raises: OpenStackCloudException on operation error. """ - dev = self.get_volume_attach_device(volume, server.id) + dev = self.get_volume_attach_device(volume, server['id']) if dev: raise OpenStackCloudException( "Volume %s already attached to server %s on device %s" - % (volume['id'], server.id, dev) + % (volume['id'], server['id'], dev) ) if volume['status'] != 'available': @@ -1744,7 +1744,7 @@ def attach_volume(self, server, volume, device=None, try: self.manager.submitTask( _tasks.VolumeAttach(volume_id=volume['id'], - server_id=server.id, + server_id=server['id'], device=device)) except Exception as e: self.log.debug( @@ -1752,7 +1752,7 @@ def attach_volume(self, server, volume, device=None, exc_info=True) raise OpenStackCloudException( "Error attaching volume %s to server %s: %s" % - (volume['id'], server.id, e) + (volume['id'], server['id'], e) ) if wait: @@ -1767,7 +1767,7 @@ def attach_volume(self, server, volume, device=None, exc_info=True) continue - if self.get_volume_attach_device(vol, server.id): + if self.get_volume_attach_device(vol, server['id']): return # TODO(Shrews) check to see if a volume can be in error status @@ -1781,7 +1781,7 @@ def attach_volume(self, server, volume, device=None, def get_server_id(self, name_or_id): server = self.get_server(name_or_id) if server: - return server.id + return server['id'] return None def get_server_private_ip(self, server): @@ -2404,7 +2404,8 @@ def delete_server(self, name_or_id, wait=False, timeout=180): def _delete_server(self, server, wait=False, timeout=180): if server: try: - self.manager.submitTask(_tasks.ServerDelete(server=server.id)) + self.manager.submitTask( + _tasks.ServerDelete(server=server['id'])) except nova_exceptions.NotFound: return except Exception as e: @@ -2420,9 +2421,10 @@ def _delete_server(self, server, wait=False, timeout=180): "Timed out waiting for server to get deleted."): try: server = self.manager.submitTask( - _tasks.ServerGet(server=server.id)) + _tasks.ServerGet(server=server['id'])) if not server: return + server = meta.obj_to_dict(server) except nova_exceptions.NotFound: return except Exception as e: diff --git a/shade/meta.py b/shade/meta.py index bbe1d20a9..51338a1f6 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -56,7 +56,7 @@ def find_nova_addresses(addresses, ext_tag=None, key_name=None, version=4): def get_server_ip(server, **kwargs): - addrs = find_nova_addresses(server.addresses, **kwargs) + addrs = find_nova_addresses(server['addresses'], **kwargs) if not addrs: return None return addrs[0] @@ -159,7 +159,7 @@ def get_server_external_ipv4(cloud, server): return ext_ip # Try to find a globally routable IP address - for interfaces in server.addresses.values(): + for interfaces in server['addresses'].values(): for interface in interfaces: if _utils.is_ipv4(interface['addr']) and \ _utils.is_globally_routable_ipv4(interface['addr']): @@ -206,21 +206,21 @@ def get_groups_from_server(cloud, server, server_vars): groups.append("%s_%s" % (cloud_name, region)) # Check if group metadata key in servers' metadata - group = server.metadata.get('group') + group = server['metadata'].get('group') if group: groups.append(group) - for extra_group in server.metadata.get('groups', '').split(','): + for extra_group in server['metadata'].get('groups', '').split(','): if extra_group: groups.append(extra_group) - groups.append('instance-%s' % server.id) + groups.append('instance-%s' % server['id']) for key in ('flavor', 'image'): if 'name' in server_vars[key]: groups.append('%s-%s' % (key, server_vars[key]['name'])) - for key, value in iter(server.metadata.items()): + for key, value in iter(server['metadata'].items()): groups.append('meta-%s_%s' % (key, value)) az = server_vars.get('az', None) @@ -258,18 +258,18 @@ def get_hostvars_from_server(cloud, server, mounts=None): server_vars['region'] = cloud.region_name server_vars['cloud'] = cloud.name - flavor_id = server.flavor['id'] + flavor_id = server['flavor']['id'] flavor_name = cloud.get_flavor_name(flavor_id) if flavor_name: server_vars['flavor']['name'] = flavor_name server_vars['flavor'].pop('links', None) # OpenStack can return image as a string when you've booted from volume - if str(server.image) == server.image: - image_id = server.image + if str(server['image']) == server['image']: + image_id = server['image'] server_vars['image'] = dict(id=image_id) else: - image_id = server.image.get('id', None) + image_id = server['image'].get('id', None) if image_id: image_name = cloud.get_image_name(image_id) if image_name: diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index ecb121d76..c9d8b7dff 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -264,7 +264,7 @@ def test_get_groups_from_server(self): 'test-region_test-az', 'test-name_test-region_test-az'], meta.get_groups_from_server( - FakeCloud(), FakeServer(), server_vars)) + FakeCloud(), meta.obj_to_dict(FakeServer()), server_vars)) def test_obj_list_to_dict(self): """Test conversion of a list of objects to a list of dictonaries""" From ee34c7d483421842fede1f0ec280e442a9484773 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 6 Aug 2015 16:41:49 +0000 Subject: [PATCH 0421/3836] Listing flavors should pull all flavors There is a bug in list_flavors() in that we were not listing ALL available flavors, only public ones. This fixes that bug. In order to list only public (or private) flavors, search_flavors() should be used with a filter on is_public. Change-Id: I444ddde9a2ac6b66b2c427c860046bd85e216e63 --- shade/__init__.py | 2 +- shade/_tasks.py | 2 +- shade/tests/unit/test_flavors.py | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index ce25ef45d..452e8476a 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -933,7 +933,7 @@ def list_volumes(self, cache=True): def list_flavors(self): try: return meta.obj_list_to_dict( - self.manager.submitTask(_tasks.FlavorList()) + self.manager.submitTask(_tasks.FlavorList(is_public=None)) ) except Exception as e: self.log.debug("flavor list failed: %s" % e, exc_info=True) diff --git a/shade/_tasks.py b/shade/_tasks.py index bdc5d7a57..aa0b5f221 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -39,7 +39,7 @@ def main(self, client): class FlavorList(task_manager.Task): def main(self, client): - return client.nova_client.flavors.list() + return client.nova_client.flavors.list(**self.args) class FlavorCreate(task_manager.Task): diff --git a/shade/tests/unit/test_flavors.py b/shade/tests/unit/test_flavors.py index 6a708f355..0ccc7898b 100644 --- a/shade/tests/unit/test_flavors.py +++ b/shade/tests/unit/test_flavors.py @@ -59,3 +59,8 @@ def test_delete_flavor_exception(self, mock_nova): mock_nova.flavors.delete.side_effect = Exception() self.assertRaises(shade.OpenStackCloudException, self.op_cloud.delete_flavor, '') + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_list_flavors(self, mock_nova): + self.op_cloud.list_flavors() + mock_nova.flavors.list.assert_called_once_with(is_public=None) From 1ead623f8c06ea2adc1a6343df1e2ece27327ed8 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 6 Aug 2015 11:16:41 -0400 Subject: [PATCH 0422/3836] Add methods to set and unset flavor extra specs Extra specs cannot be set on a flavor at create time, so these methods are needed so folks can, you know, set and unset specs. Change-Id: Ifc62bbe6dabe5bda893e299a53c611b6002c00ba --- shade/__init__.py | 65 ++++++++++++++++++++++++++++++++ shade/_tasks.py | 5 +++ shade/tests/unit/test_flavors.py | 18 +++++++++ 3 files changed, 88 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 452e8476a..af6b5e44f 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -4352,3 +4352,68 @@ def delete_flavor(self, name_or_id): ) return True + + def _mod_flavor_specs(self, action, flavor_id, specs): + """Common method for modifying flavor extra specs. + + Nova (very sadly) doesn't expose this with a public API, so we + must get the actual flavor object and make a method call on it. + + Two separate try-except blocks are used because Nova can raise + a NotFound exception if FlavorGet() is given an invalid flavor ID, + or if the unset_keys() method of the flavor object is given an + invalid spec key. We need to be able to differentiate between these + actions, thus the separate blocks. + """ + try: + flavor = self.manager.submitTask( + _tasks.FlavorGet(flavor=flavor_id) + ) + except nova_exceptions.NotFound: + self.log.debug( + "Flavor ID {0} not found. " + "Cannot {1} extra specs.".format(flavor_id, action) + ) + raise OpenStackCloudResourceNotFound( + "Flavor ID {0} not found".format(flavor_id) + ) + except Exception as e: + self.log.debug("Error getting flavor ID {0}".format(flavor_id), + exc_info=True) + raise OpenStackCloudException( + "Error getting flavor ID {0}: {1}".format(flavor_id, e) + ) + + try: + if action == 'set': + flavor.set_keys(specs) + elif action == 'unset': + flavor.unset_keys(specs) + except Exception as e: + self.log.debug("Error during {0} of flavor specs".format(action), + exc_info=True) + raise OpenStackCloudException( + "Unable to {0} flavor specs: {1}".format(action, e) + ) + + def set_flavor_specs(self, flavor_id, extra_specs): + """Add extra specs to a flavor + + :param string flavor_id: ID of the flavor to update. + :param dict extra_specs: Dictionary of key-value pairs. + + :raises: OpenStackCloudException on operation error. + :raises: OpenStackCloudResourceNotFound if flavor ID is not found. + """ + self._mod_flavor_specs('set', flavor_id, extra_specs) + + def unset_flavor_specs(self, flavor_id, keys): + """Delete extra specs from a flavor + + :param string flavor_id: ID of the flavor to update. + :param list keys: List of spec keys to delete. + + :raises: OpenStackCloudException on operation error. + :raises: OpenStackCloudResourceNotFound if flavor ID is not found. + """ + self._mod_flavor_specs('unset', flavor_id, keys) diff --git a/shade/_tasks.py b/shade/_tasks.py index aa0b5f221..75030e308 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -52,6 +52,11 @@ def main(self, client): return client.nova_client.flavors.delete(**self.args) +class FlavorGet(task_manager.Task): + def main(self, client): + return client.nova_client.flavors.get(**self.args) + + class ServerList(task_manager.Task): def main(self, client): return client.nova_client.servers.list(**self.args) diff --git a/shade/tests/unit/test_flavors.py b/shade/tests/unit/test_flavors.py index 0ccc7898b..56695692a 100644 --- a/shade/tests/unit/test_flavors.py +++ b/shade/tests/unit/test_flavors.py @@ -64,3 +64,21 @@ def test_delete_flavor_exception(self, mock_nova): def test_list_flavors(self, mock_nova): self.op_cloud.list_flavors() mock_nova.flavors.list.assert_called_once_with(is_public=None) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_set_flavor_specs(self, mock_nova): + flavor = mock.Mock(id=1, name='orange') + mock_nova.flavors.get.return_value = flavor + extra_specs = dict(key1='value1') + self.op_cloud.set_flavor_specs(1, extra_specs) + mock_nova.flavors.get.assert_called_once_with(flavor=1) + flavor.set_keys.assert_called_once_with(extra_specs) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_unset_flavor_specs(self, mock_nova): + flavor = mock.Mock(id=1, name='orange') + mock_nova.flavors.get.return_value = flavor + keys = ['key1', 'key2'] + self.op_cloud.unset_flavor_specs(1, keys) + mock_nova.flavors.get.assert_called_once_with(flavor=1) + flavor.unset_keys.assert_called_once_with(keys) From 20b3dd11763237b6fc75a637527e7d94ec303032 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 7 Aug 2015 06:40:12 +1000 Subject: [PATCH 0423/3836] Add log message for when IP addresses fail We take the action of deleting something for the user, we should tell them. Change-Id: I1afe1bc53c0ae76967442b93cd25693d4d8b4e89 --- shade/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 8aa8b3aff..c310ae8e4 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2340,6 +2340,11 @@ def create_server(self, auto_ip=True, ips=None, ip_pool=None, if server['status'] == 'ACTIVE': if not server['addresses']: + self.log.debug( + 'Server {server} reached ACTIVE state without' + ' being allocated an IP address.' + ' Deleting server.'.format( + server=server['id'])) try: self._delete_server( server=server, wait=wait, timeout=timeout) From ef756c46b8f558d14d4c3baf989d4a8188233279 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 7 Aug 2015 18:52:02 +0000 Subject: [PATCH 0424/3836] Bug fix for obj_to_dict() Float values were being discarded by obj_to_dict(). Eek. Change-Id: Ia04c74ceda02e408ae4aa5b372a6976d12591589 --- shade/meta.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/meta.py b/shade/meta.py index bbe1d20a9..1c83f1410 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -21,7 +21,7 @@ from shade import _utils -NON_CALLABLES = (six.string_types, bool, dict, int, list, type(None)) +NON_CALLABLES = (six.string_types, bool, dict, int, float, list, type(None)) log = logging.getLogger(__name__) From 575508dba275b47664a98eafa4ba11e430a6d954 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 7 Aug 2015 19:15:01 +0000 Subject: [PATCH 0425/3836] Add flavor functional tests Test for creating and listing flavors. Change-Id: I30a351424515558d5e0e71cb2d391e19bff20fe5 --- shade/tests/functional/test_flavor.py | 98 +++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 shade/tests/functional/test_flavor.py diff --git a/shade/tests/functional/test_flavor.py b/shade/tests/functional/test_flavor.py new file mode 100644 index 000000000..91507a8ec --- /dev/null +++ b/shade/tests/functional/test_flavor.py @@ -0,0 +1,98 @@ +# Copyright (c) 2015 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. + +""" +test_flavor +---------------------------------- + +Functional tests for `shade` flavor resource. +""" + +import string +import random + +import shade +from shade.exc import OpenStackCloudException +from shade.tests import base + + +class TestFlavor(base.TestCase): + + def setUp(self): + super(TestFlavor, self).setUp() + # Shell should have OS-* envvars from openrc, typically loaded by job + self.operator_cloud = shade.operator_cloud() + + # Generate a random name for flavors in this test + self.new_item_name = 'flavor_' + ''.join( + random.choice(string.ascii_lowercase) for _ in range(5)) + + self.addCleanup(self._cleanup_flavors) + + def _cleanup_flavors(self): + exception_list = list() + for f in self.operator_cloud.list_flavors(): + if f['name'].startswith(self.new_item_name): + try: + self.operator_cloud.delete_flavor(f['id']) + except Exception as e: + # We were unable to delete a flavor, let's try with next + exception_list.append(e) + continue + if exception_list: + # Raise an error: we must make users aware that something went + # wrong + raise OpenStackCloudException('\n'.join(exception_list)) + + def test_create_flavor(self): + flavor_name = self.new_item_name + '_create' + flavor_kwargs = dict( + name=flavor_name, ram=1024, vcpus=2, disk=10, ephemeral=5, + swap=100, rxtx_factor=1.5, is_public=True + ) + + flavor = self.operator_cloud.create_flavor(**flavor_kwargs) + + self.assertIsNotNone(flavor['id']) + for key in flavor_kwargs.keys(): + self.assertIn(key, flavor) + for key, value in flavor_kwargs.items(): + self.assertEqual(value, flavor[key]) + + def test_list_flavors(self): + pub_flavor_name = self.new_item_name + '_public' + priv_flavor_name = self.new_item_name + '_private' + public_kwargs = dict( + name=pub_flavor_name, ram=1024, vcpus=2, disk=10, is_public=True + ) + private_kwargs = dict( + name=priv_flavor_name, ram=1024, vcpus=2, disk=10, is_public=True + ) + + # Create a public and private flavor. We expect both to be listed + # for an operator. + self.operator_cloud.create_flavor(**public_kwargs) + self.operator_cloud.create_flavor(**private_kwargs) + + flavors = self.operator_cloud.list_flavors() + + # Flavor list will include the standard devstack flavors. We just want + # to make sure both of the flavors we just created are present. + found = [] + for f in flavors: + if f['name'] in (pub_flavor_name, priv_flavor_name): + found.append(f) + self.assertEqual(2, len(found)) From dfef01a7cbb9d68ad443d182e548ec58143a3a1f Mon Sep 17 00:00:00 2001 From: Davide Guerri Date: Tue, 11 Aug 2015 23:54:39 +0100 Subject: [PATCH 0426/3836] Use the correct auth_plugin for token authentication Use ksc_auth.token_endpoint.Token as Keystone authentication plugin if auth_type is token_endpoint. Change-Id: I1a6d6dfe731527d7040cfbc2404fd5cf86ba5893 --- os_client_config/config.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 1840d18dc..240220ae9 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -379,8 +379,12 @@ def _find_winning_auth_value(self, opt, config): def _validate_auth(self, config): # May throw a keystoneclient.exceptions.NoMatchingPlugin - plugin_options = ksc_auth.get_plugin_class( - config['auth_type']).get_options() + if config['auth_type'] == 'token_endpoint': + auth_plugin = ksc_auth.token_endpoint.Token + else: + auth_plugin = ksc_auth.get_plugin_class(config['auth_type']) + + plugin_options = auth_plugin.get_options() for p_opt in plugin_options: # if it's in config.auth, win, kill it from config dict From 13a04f76b9f13ec89a9d0aa8a7e9b064e8fa98b4 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 12 Aug 2015 19:56:51 +0000 Subject: [PATCH 0427/3836] Ignore infra CI env vars When running tests in the infra CI system, some OS_ environment variables are defined (e.g., OS_TEST_TIMEOUT, OS_TEST_PATH, OS_STDERR_CAPTURE, OS_STDOUT_CAPTURE) and this makes o-c-c think that we are using environment variables for an envvar cloud. In some rare cases, this can cause tests to fail if they expect cloud definitions to come only from clouds.yaml. This change ignores these CI variables. Change-Id: I6458969b45f5534e1172b9f8ba598d7c536a59b5 --- os_client_config/config.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 1840d18dc..7c65b537b 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -82,7 +82,11 @@ def get_boolean(value): def _get_os_environ(): ret = defaults.get_defaults() - environkeys = [k for k in os.environ.keys() if k.startswith('OS_')] + environkeys = [k for k in os.environ.keys() + if k.startswith('OS_') + and not k.startswith('OS_TEST') # infra CI var + and not k.startswith('OS_STD') # infra CI var + ] if not environkeys: return None for k in environkeys: From eeae22fb7f6e179f4371fcbf82367918df091a00 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 10 Aug 2015 09:17:02 -0400 Subject: [PATCH 0428/3836] Add a developer coding standards doc We have coding standards. Let's tell others about them. Change-Id: I36456fa706db96ad1abb3d121f90981845065d8c --- doc/source/coding.rst | 89 +++++++++++++++++++++++++++++++++++++++++++ doc/source/index.rst | 1 + 2 files changed, 90 insertions(+) create mode 100644 doc/source/coding.rst diff --git a/doc/source/coding.rst b/doc/source/coding.rst new file mode 100644 index 000000000..be67398ca --- /dev/null +++ b/doc/source/coding.rst @@ -0,0 +1,89 @@ +******************************** +Shade Developer Coding Standards +******************************** + +In the beginning, there were no guidelines. And it was good. But that +didn't last long. As more and more people added more and more code, +we realized that we needed a set of coding standards to make sure that +the shade API at least *attempted* to display some form of consistency. + +Thus, these coding standards/guidelines were developed. Note that not +all of shade adheres to these standards just yet. Some older code has +not been updated because we need to maintain backward compatibility. +Some of it just hasn't been changed yet. But be clear, all new code +*must* adhere to these guidelines. + +Below are the patterns that we expect Shade developers to follow. + +API Methods +=========== + +- When an API call acts on a resource that has both a unique ID and a + name, that API call should accept either identifier with a name_or_id + parameter. + +- All resources should adhere to the get/list/search interface that + control retrieval of those resources. E.g., `get_image()`, `list_images()`, + `search_images()`. + +- Resources should have `create_RESOURCE()`, `delete_RESOURCE()`, + `update_RESOURCE()` API methods (as it makes sense). + +- For those methods that should behave differently for omitted or None-valued + parameters, use the `valid_kwargs` decorator. Notably: all Neutron + `update_*` functions. + +- Deleting a resource should return True if the delete succeeded, or False + if the resource was not found. + +Exceptions +========== + +All underlying client exceptions must be captured and converted to an +`OpenStackCloudException` or one of its derivatives. + +Client Calls +============ + +All underlying client calls (novaclient, swiftclient, etc.) must be +wrapped by a Task object. + +Returned Resources +================== + +Complex objects returned to the caller must be a dict type. The +methods `obj_to_dict()` or `obj_list_to_dict()` should be used for this. + +As of this writing, those two methods are returning Bunch objects, which help +to maintain backward compatibility with a time when shade returned raw +objects. Bunch allows the returned resource to act as *both* an object +and a dict. Use of Bunch objects will eventually be deprecated in favor +of just pure dicts, so do not depend on the Bunch object functionality. +Expect a pure dict type. + +Nova vs. Neutron +================ + +- Recognize that not all cloud providers support Neutron, so never + assume it will be present. If a task can be handled by either + Neutron or Nova, code it to be handled by either. + +- For methods that accept either a Nova pool or Neutron network, the + parameter should just refer to the network, but documentation of it + should explain about the pool. See: `create_floating_ip()` and + `available_floating_ip()` methods. + +Tests +===== + +- New API methods *must* have unit tests! + +- Functional tests should be added, when possible. + +- In functional tests, always use unique names (for resources that have this + attribute) and use it for clean up (see next point). + +- In functional tests, always define cleanup functions to delete data added + by your test, should something go wrong. Data removal should be wrapped in + a try except block and try to delete as many entries added by the test as + possible. diff --git a/doc/source/index.rst b/doc/source/index.rst index 97455f584..af9eaa3d2 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -14,6 +14,7 @@ Contents: installation usage contributing + coding future .. include:: ../../README.rst From 0e3117cb01d201f403be72cbb1218b9eec69a541 Mon Sep 17 00:00:00 2001 From: lifeless Date: Thu, 13 Aug 2015 07:09:22 +0000 Subject: [PATCH 0429/3836] Revert "Use the correct auth_plugin for token authentication" This reverts commit dfef01a7cbb9d68ad443d182e548ec58143a3a1f. This broke the world. Change-Id: I65d820b0da807a161eae814ea4b5413f245d7bf0 --- os_client_config/config.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 240220ae9..1840d18dc 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -379,12 +379,8 @@ def _find_winning_auth_value(self, opt, config): def _validate_auth(self, config): # May throw a keystoneclient.exceptions.NoMatchingPlugin - if config['auth_type'] == 'token_endpoint': - auth_plugin = ksc_auth.token_endpoint.Token - else: - auth_plugin = ksc_auth.get_plugin_class(config['auth_type']) - - plugin_options = auth_plugin.get_options() + plugin_options = ksc_auth.get_plugin_class( + config['auth_type']).get_options() for p_opt in plugin_options: # if it's in config.auth, win, kill it from config dict From 085074d6023695829b2c530ed2ec3210bb9abe9b Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 12 Aug 2015 15:49:44 +0000 Subject: [PATCH 0430/3836] Change functional testing to use clouds.yaml Using clouds.yaml, instead of sourcing openrc, we can have better functional testing by distinquishing between an admin account and a normal user account. We cannot both source openrc AND use clouds.yaml as this confuses the inventory functional tests, and is not a recommended way of utilizing o-c-c, anyway. In order for these test to work, a bug fix was needed in os-client-config, so the requirements was bumped to 1.6.2 or higher. Depends-On: I6458969b45f5534e1172b9f8ba598d7c536a59b5 Change-Id: If459afba3cf61b3c7f641c79494cd53a682666a4 --- requirements.txt | 2 +- shade/tests/functional/hooks/post_test_hook.sh | 8 +++++--- shade/tests/functional/test_compute.py | 3 +-- shade/tests/functional/test_endpoints.py | 3 +-- shade/tests/functional/test_flavor.py | 3 +-- shade/tests/functional/test_floating_ip.py | 3 +-- shade/tests/functional/test_floating_ip_pool.py | 3 +-- shade/tests/functional/test_image.py | 3 +-- shade/tests/functional/test_inventory.py | 5 +++-- shade/tests/functional/test_object.py | 3 +-- shade/tests/functional/test_port.py | 3 +-- shade/tests/functional/test_services.py | 3 +-- 12 files changed, 18 insertions(+), 24 deletions(-) diff --git a/requirements.txt b/requirements.txt index d7f341c36..890a48acd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ pbr>=0.11,<2.0 bunch decorator jsonpatch -os-client-config>=1.6.0 +os-client-config>=1.6.2 six python-novaclient>=2.21.0 diff --git a/shade/tests/functional/hooks/post_test_hook.sh b/shade/tests/functional/hooks/post_test_hook.sh index ccf477455..8a2f97ee1 100755 --- a/shade/tests/functional/hooks/post_test_hook.sh +++ b/shade/tests/functional/hooks/post_test_hook.sh @@ -14,9 +14,11 @@ export SHADE_DIR="$BASE/new/shade" -cd $BASE/new/devstack -source openrc admin admin -unset OS_CACERT +# The jenkins user needs access to the clouds.yaml file +# for the functional tests. +sudo mkdir -p ~jenkins/.config/openstack +sudo cp $BASE/new/.config/openstack/clouds.yaml ~jenkins/.config/openstack +sudo chown -R jenkins:stack ~jenkins/.config cd $SHADE_DIR sudo chown -R jenkins:stack $SHADE_DIR diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index 6a1551476..58a8662fa 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -27,8 +27,7 @@ class TestCompute(base.TestCase): def setUp(self): super(TestCompute, self).setUp() - # Shell should have OS-* envvars from openrc, typically loaded by job - self.cloud = openstack_cloud() + self.cloud = openstack_cloud(cloud='devstack') self.nova = self.cloud.nova_client self.flavor = pick_flavor(self.nova.flavors.list()) if self.flavor is None: diff --git a/shade/tests/functional/test_endpoints.py b/shade/tests/functional/test_endpoints.py index fbf06d013..f3acf6d86 100644 --- a/shade/tests/functional/test_endpoints.py +++ b/shade/tests/functional/test_endpoints.py @@ -36,8 +36,7 @@ class TestEndpoints(base.TestCase): def setUp(self): super(TestEndpoints, self).setUp() - # Shell should have OS-* envvars from openrc, typically loaded by job - self.operator_cloud = operator_cloud() + self.operator_cloud = operator_cloud(cloud='devstack-admin') # Generate a random name for services and regions in this test self.new_item_name = 'test_' + ''.join( diff --git a/shade/tests/functional/test_flavor.py b/shade/tests/functional/test_flavor.py index 91507a8ec..e5434b6ef 100644 --- a/shade/tests/functional/test_flavor.py +++ b/shade/tests/functional/test_flavor.py @@ -33,8 +33,7 @@ class TestFlavor(base.TestCase): def setUp(self): super(TestFlavor, self).setUp() - # Shell should have OS-* envvars from openrc, typically loaded by job - self.operator_cloud = shade.operator_cloud() + self.operator_cloud = shade.operator_cloud(cloud='devstack-admin') # Generate a random name for flavors in this test self.new_item_name = 'flavor_' + ''.join( diff --git a/shade/tests/functional/test_floating_ip.py b/shade/tests/functional/test_floating_ip.py index ce853d5c1..e72b74e10 100644 --- a/shade/tests/functional/test_floating_ip.py +++ b/shade/tests/functional/test_floating_ip.py @@ -52,8 +52,7 @@ class TestFloatingIP(base.TestCase): def setUp(self): super(TestFloatingIP, self).setUp() - # Shell should have OS-* envvars from openrc, typically loaded by job - self.cloud = openstack_cloud() + self.cloud = openstack_cloud(cloud='devstack') self.nova = self.cloud.nova_client if self.cloud.has_service('network'): self.neutron = self.cloud.neutron_client diff --git a/shade/tests/functional/test_floating_ip_pool.py b/shade/tests/functional/test_floating_ip_pool.py index 9b608ec8e..4edbcaf72 100644 --- a/shade/tests/functional/test_floating_ip_pool.py +++ b/shade/tests/functional/test_floating_ip_pool.py @@ -35,8 +35,7 @@ class TestFloatingIPPool(base.TestCase): def setUp(self): super(TestFloatingIPPool, self).setUp() - # Shell should have OS-* envvars from openrc, typically loaded by job - self.cloud = openstack_cloud() + self.cloud = openstack_cloud(cloud='devstack') if not self.cloud._has_nova_extension('os-floating-ip-pools'): # Skipping this test is floating-ip-pool extension is not diff --git a/shade/tests/functional/test_image.py b/shade/tests/functional/test_image.py index 043bd65f0..89c760a04 100644 --- a/shade/tests/functional/test_image.py +++ b/shade/tests/functional/test_image.py @@ -30,8 +30,7 @@ class TestImage(base.TestCase): def setUp(self): super(TestImage, self).setUp() - # Shell should have OS-* envvars from openrc, typically loaded by job - self.cloud = openstack_cloud() + self.cloud = openstack_cloud(cloud='devstack') self.image = pick_image(self.cloud.nova_client.images.list()) def test_create_image(self): diff --git a/shade/tests/functional/test_inventory.py b/shade/tests/functional/test_inventory.py index 3a4bfcdb8..859fd5a7f 100644 --- a/shade/tests/functional/test_inventory.py +++ b/shade/tests/functional/test_inventory.py @@ -29,8 +29,9 @@ class TestInventory(base.TestCase): def setUp(self): super(TestInventory, self).setUp() - # Shell should have OS-* envvars from openrc, typically loaded by job - self.cloud = openstack_cloud() + # This needs to use an admin account, otherwise a public IP + # is not allocated from devstack. + self.cloud = openstack_cloud(cloud='devstack-admin') self.inventory = inventory.OpenStackInventory() self.server_name = 'test_inventory_server' self.nova = self.cloud.nova_client diff --git a/shade/tests/functional/test_object.py b/shade/tests/functional/test_object.py index 2aee9e9b2..b642ce780 100644 --- a/shade/tests/functional/test_object.py +++ b/shade/tests/functional/test_object.py @@ -32,8 +32,7 @@ class TestObject(base.TestCase): def setUp(self): super(TestObject, self).setUp() - # Shell should have OS-* envvars from openrc, typically loaded by job - self.cloud = openstack_cloud() + self.cloud = openstack_cloud(cloud='devstack') if not self.cloud.has_service('object'): self.skipTest('Object service not supported by cloud') diff --git a/shade/tests/functional/test_port.py b/shade/tests/functional/test_port.py index 82ca096c6..b8416cfd7 100644 --- a/shade/tests/functional/test_port.py +++ b/shade/tests/functional/test_port.py @@ -33,8 +33,7 @@ class TestPort(base.TestCase): def setUp(self): super(TestPort, self).setUp() - # Shell should have OS-* envvars from openrc, typically loaded by job - self.cloud = openstack_cloud() + self.cloud = openstack_cloud(cloud='devstack-admin') # Skip Neutron tests if neutron is not present if not self.cloud.has_service('network'): self.skipTest('Network service not supported by cloud') diff --git a/shade/tests/functional/test_services.py b/shade/tests/functional/test_services.py index 654f86e52..16889f080 100644 --- a/shade/tests/functional/test_services.py +++ b/shade/tests/functional/test_services.py @@ -35,8 +35,7 @@ class TestServices(base.TestCase): def setUp(self): super(TestServices, self).setUp() - # Shell should have OS-* envvars from openrc, typically loaded by job - self.operator_cloud = operator_cloud() + self.operator_cloud = operator_cloud(cloud='devstack-admin') # Generate a random name for services in this test self.new_service_name = 'test_' + ''.join( From e29ed75a4b72ed25c7791dab8a66f3a3c86f962a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 13 Aug 2015 09:14:01 +0000 Subject: [PATCH 0431/3836] Revert "Revert "Use the correct auth_plugin for token authentication"" Name the auth type needed for using an admin token "admin_token". This hack can be removed with keystoneclient and/or keystoneauth make a release with the admin_token entrypoint defined. Naming it admin_token further reduces the conflict with OSC, which should make the patch to work around OSC having different arguments unnecessary. This reverts commit 0e3117cb01d201f403be72cbb1218b9eec69a541. Change-Id: I079c7c61b2637ded73542876cb1378a0731f8631 --- os_client_config/config.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 1840d18dc..99839785e 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -379,8 +379,12 @@ def _find_winning_auth_value(self, opt, config): def _validate_auth(self, config): # May throw a keystoneclient.exceptions.NoMatchingPlugin - plugin_options = ksc_auth.get_plugin_class( - config['auth_type']).get_options() + if config['auth_type'] == 'admin_endpoint': + auth_plugin = ksc_auth.token_endpoint.Token + else: + auth_plugin = ksc_auth.get_plugin_class(config['auth_type']) + + plugin_options = auth_plugin.get_options() for p_opt in plugin_options: # if it's in config.auth, win, kill it from config dict From 40fdf2bac69823498fc1893eef15e897ae69eb1c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 15 Jun 2015 14:25:14 +0300 Subject: [PATCH 0432/3836] Make client constructor calls consistent The previous patch involved copying and pasting a bunch needlessly. Combine the common pattern into a single factory function. Change-Id: Ie80dd13dd4095b23d35362bc8e4ccefc8b346260 --- shade/__init__.py | 80 +++++++++++++++++++---------------------------- 1 file changed, 32 insertions(+), 48 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 0deb56821..cce5f661b 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -308,29 +308,35 @@ def generate_key(*args, **kwargs): return ans return generate_key + def _get_client(self, service_key, client_class): + try: + # trigger exception on lack of service + self.get_session_endpoint(service_key) + client = client_class( + version=self.cloud_config.get_api_version(service_key), + session=self.keystone_session, + service_name=self.cloud_config.get_service_name(service_key), + service_type=self.cloud_config.get_service_type(service_key), + endpoint_type=self.cloud_config.get_interface(service_key), + region_name=self.region_name, + timeout=self.api_timeout) + except Exception: + self.log.debug( + "Couldn't construct {service} object".format( + service=service_key), exc_info=True) + raise + if client is None: + raise OpenStackCloudException( + "Failed to instantiate {service} client." + " This could mean that your credentials are wrong.".format( + service=service_key)) + return client + @property def nova_client(self): if self._nova_client is None: - - # Make the connection - try: - # trigger exception on lack of compute. (what?) - self.get_session_endpoint('compute') - self._nova_client = nova_client.Client( - self.cloud_config.get_api_version('compute'), - session=self.keystone_session, - service_name=self.cloud_config.get_service_name('compute'), - region_name=self.region_name, - timeout=self.api_timeout) - except Exception: - self.log.debug("Couldn't construct nova object", exc_info=True) - raise - - if self._nova_client is None: - raise OpenStackCloudException( - "Failed to instantiate nova client." - " This could mean that your credentials are wrong.") - + self._nova_client = self._get_client( + 'compute', nova_client.Client) return self._nova_client def _get_auth_plugin_class(self): @@ -667,19 +673,8 @@ def swift_service(self): def cinder_client(self): if self._cinder_client is None: - # trigger exception on lack of cinder - self.get_session_endpoint('volume') - # Make the connection - self._cinder_client = cinder_client.Client( - session=self.keystone_session, - region_name=self.region_name, - timeout=self.api_timeout) - - if self._cinder_client is None: - raise OpenStackCloudException( - "Failed to instantiate cinder client." - " This could mean that your credentials are wrong.") - + self._cinder_client = self._get_client( + 'volume', cinder_client.Client) return self._cinder_client @property @@ -701,6 +696,8 @@ def trove_client(self): "Failed to instantiate Trove client." " This could mean that your credentials are wrong.") + self._trove_client = self._get_client( + 'database', trove_client.Client) return self._trove_client @property @@ -718,21 +715,8 @@ def neutron_client(self): @property def designate_client(self): if self._designate_client is None: - # get dns service type if defined in cloud config - dns_service_type = self.cloud_config.get_service_type('dns') - # trigger exception on lack of designate - self.get_session_endpoint(dns_service_type) - - self._designate_client = designate_client( - session=self.keystone_session, - region_name=self.region_name, - service_type=dns_service_type) - - if self._designate_client is None: - raise OpenStackCloudException( - "Failed to instantiate designate client." - " This could mean that your credentials are wrong.") - + self._designate_client = self._get_client( + 'dns', designate_client.Client) return self._designate_client def get_name(self): From a735e12e307acd64e363e0bee57c1b91412c0af3 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 11 Aug 2015 18:39:58 +0000 Subject: [PATCH 0433/3836] Add flavor access API Add methods to control tenant/project access to private flavors. Change-Id: I23fc6f45095ed2b182e17192c00f74ef84afedd5 --- shade/__init__.py | 45 +++++++++++++++++++++++++++ shade/_tasks.py | 14 +++++++++ shade/tests/functional/test_flavor.py | 31 +++++++++++++++++- shade/tests/unit/test_flavors.py | 14 +++++++++ 4 files changed, 103 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index 0deb56821..9d030be9f 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -4446,3 +4446,48 @@ def unset_flavor_specs(self, flavor_id, keys): :raises: OpenStackCloudResourceNotFound if flavor ID is not found. """ self._mod_flavor_specs('unset', flavor_id, keys) + + def _mod_flavor_access(self, action, flavor_id, project_id): + """Common method for adding and removing flavor access + """ + try: + if action == 'add': + self.manager.submitTask( + _tasks.FlavorAddAccess(flavor=flavor_id, + tenant=project_id) + ) + elif action == 'remove': + self.manager.submitTask( + _tasks.FlavorRemoveAccess(flavor=flavor_id, + tenant=project_id) + ) + except Exception as e: + self.log.debug( + "Error trying to {0} access to flavor ID {1}".format( + action, flavor_id), + exc_info=True + ) + raise OpenStackCloudException( + "Error trying to {0} access from flavor ID {1}: {2}".format( + action, flavor_id, e) + ) + + def add_flavor_access(self, flavor_id, project_id): + """Grant access to a private flavor for a project/tenant. + + :param string flavor_id: ID of the private flavor. + :param string project_id: ID of the project/tenant. + + :raises: OpenStackCloudException on operation error. + """ + self._mod_flavor_access('add', flavor_id, project_id) + + def remove_flavor_access(self, flavor_id, project_id): + """Revoke access from a private flavor for a project/tenant. + + :param string flavor_id: ID of the private flavor. + :param string project_id: ID of the project/tenant. + + :raises: OpenStackCloudException on operation error. + """ + self._mod_flavor_access('remove', flavor_id, project_id) diff --git a/shade/_tasks.py b/shade/_tasks.py index 75030e308..01cda0876 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -57,6 +57,20 @@ def main(self, client): return client.nova_client.flavors.get(**self.args) +class FlavorAddAccess(task_manager.Task): + def main(self, client): + return client.nova_client.flavor_access.add_tenant_access( + **self.args + ) + + +class FlavorRemoveAccess(task_manager.Task): + def main(self, client): + return client.nova_client.flavor_access.remove_tenant_access( + **self.args + ) + + class ServerList(task_manager.Task): def main(self, client): return client.nova_client.servers.list(**self.args) diff --git a/shade/tests/functional/test_flavor.py b/shade/tests/functional/test_flavor.py index e5434b6ef..1250374d9 100644 --- a/shade/tests/functional/test_flavor.py +++ b/shade/tests/functional/test_flavor.py @@ -33,6 +33,7 @@ class TestFlavor(base.TestCase): def setUp(self): super(TestFlavor, self).setUp() + self.demo_cloud = shade.openstack_cloud(cloud='devstack') self.operator_cloud = shade.operator_cloud(cloud='devstack-admin') # Generate a random name for flavors in this test @@ -78,7 +79,7 @@ def test_list_flavors(self): name=pub_flavor_name, ram=1024, vcpus=2, disk=10, is_public=True ) private_kwargs = dict( - name=priv_flavor_name, ram=1024, vcpus=2, disk=10, is_public=True + name=priv_flavor_name, ram=1024, vcpus=2, disk=10, is_public=False ) # Create a public and private flavor. We expect both to be listed @@ -95,3 +96,31 @@ def test_list_flavors(self): if f['name'] in (pub_flavor_name, priv_flavor_name): found.append(f) self.assertEqual(2, len(found)) + + def test_flavor_access(self): + priv_flavor_name = self.new_item_name + '_private' + private_kwargs = dict( + name=priv_flavor_name, ram=1024, vcpus=2, disk=10, is_public=False + ) + new_flavor = self.operator_cloud.create_flavor(**private_kwargs) + + # Validate the 'demo' user cannot see the new flavor + flavors = self.demo_cloud.search_flavors(priv_flavor_name) + self.assertEqual(0, len(flavors)) + + # We need the tenant ID for the 'demo' user + project = self.operator_cloud.get_project('demo') + + # Now give 'demo' access + self.operator_cloud.add_flavor_access(new_flavor['id'], project['id']) + + # Now see if the 'demo' user has access to it + flavors = self.demo_cloud.search_flavors(priv_flavor_name) + self.assertEqual(1, len(flavors)) + self.assertEqual(priv_flavor_name, flavors[0]['name']) + + # Now revoke the access and make sure we can't find it + self.operator_cloud.remove_flavor_access(new_flavor['id'], + project['id']) + flavors = self.demo_cloud.search_flavors(priv_flavor_name) + self.assertEqual(0, len(flavors)) diff --git a/shade/tests/unit/test_flavors.py b/shade/tests/unit/test_flavors.py index 56695692a..ef726a139 100644 --- a/shade/tests/unit/test_flavors.py +++ b/shade/tests/unit/test_flavors.py @@ -82,3 +82,17 @@ def test_unset_flavor_specs(self, mock_nova): self.op_cloud.unset_flavor_specs(1, keys) mock_nova.flavors.get.assert_called_once_with(flavor=1) flavor.unset_keys.assert_called_once_with(keys) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_add_flavor_access(self, mock_nova): + self.op_cloud.add_flavor_access('flavor_id', 'tenant_id') + mock_nova.flavor_access.add_tenant_access.assert_called_once_with( + flavor='flavor_id', tenant='tenant_id' + ) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_remove_flavor_access(self, mock_nova): + self.op_cloud.remove_flavor_access('flavor_id', 'tenant_id') + mock_nova.flavor_access.remove_tenant_access.assert_called_once_with( + flavor='flavor_id', tenant='tenant_id' + ) From 2726843ee54a3c9f061b9ed2451d080f459e4f38 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 18 Aug 2015 16:27:08 -0700 Subject: [PATCH 0434/3836] Do not treat project_name and project_id the same There are clouds where doing this is not working. Change-Id: I1d2e71b2a6ad22eb5070b92448779f2e9df71e4a --- os_client_config/config.py | 6 +++--- os_client_config/tests/base.py | 6 +++++- os_client_config/tests/test_config.py | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 1542f2f0a..d6983781f 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -288,9 +288,9 @@ def _fix_backwards_madness(self, cloud): def _fix_backwards_project(self, cloud): # Do the lists backwards so that project_name is the ultimate winner mappings = { - 'project_name': ('tenant_id', 'tenant-id', - 'project_id', 'project-id', - 'tenant_name', 'tenant-name', + 'project_id': ('tenant_id', 'tenant-id', + 'project_id', 'project-id'), + 'project_name': ('tenant_name', 'tenant-name', 'project_name', 'project-name'), } for target_key, possible_values in mappings.items(): diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py index 569b52ea7..89e04c0ae 100644 --- a/os_client_config/tests/base.py +++ b/os_client_config/tests/base.py @@ -132,4 +132,8 @@ def _assert_cloud_details(self, cc): self.assertIsNone(cc.cloud) self.assertIn('username', cc.auth) self.assertEqual('testuser', cc.auth['username']) - self.assertEqual('testproject', cc.auth['project_name']) + self.assertTrue('project_name' in cc.auth or 'project_id' in cc.auth) + if 'project_name' in cc.auth: + self.assertEqual('testproject', cc.auth['project_name']) + elif 'project_id' in cc.auth: + self.assertEqual('testproject', cc.auth['project_id']) diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 6bb65fce5..332e4d323 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -89,13 +89,13 @@ def test_get_one_cloud_with_int_project_id(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) cc = c.get_one_cloud('_test-cloud-int-project_') - self.assertEqual('12345', cc.auth['project_name']) + self.assertEqual('12345', cc.auth['project_id']) def test_get_one_cloud_with_hyphenated_project_id(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) cc = c.get_one_cloud('_test_cloud_hyphenated') - self.assertEqual('12345', cc.auth['project_name']) + self.assertEqual('12345', cc.auth['project_id']) def test_no_environ(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], From de6cb7895001628834c098bbd5b53d71a06bcff7 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Wed, 12 Aug 2015 11:21:05 -0400 Subject: [PATCH 0435/3836] Initial support for ironic enroll state Addition of logic for the ironic enroll state which will ultimately permit a newer client library version. Change-Id: I85fe0f2b2f0e6766928f9fb23cb1a8177779008c --- shade/__init__.py | 92 ++++++++++++++-- shade/tests/unit/test_shade_operator.py | 135 ++++++++++++++++++++++++ 2 files changed, 221 insertions(+), 6 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 0deb56821..6592b2fd7 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -3477,7 +3477,8 @@ def get_machine_by_mac(self, mac): except ironic_exceptions.ClientException: return None - def register_machine(self, nics, **kwargs): + def register_machine(self, nics, wait=False, timeout=3600, + lock_timeout=600, **kwargs): """Register Baremetal with Ironic Allows for the registration of Baremetal nodes with Ironic @@ -3501,6 +3502,18 @@ def register_machine(self, nics, **kwargs): {'mac': 'aa:bb:cc:dd:ee:02'} ] + :param wait: Boolean value, defaulting to false, to wait for the + node to reach the available state where the node can be + provisioned. It must be noted, when set to false, the + method will still wait for locks to clear before sending + the next required command. + + :param timeout: Integer value, defautling to 3600 seconds, for the + wait state to reach completion. + + :param lock_timeout: Integer value, defaulting to 600 seconds, for + locks to clear. + :param kwargs: Key value pairs to be passed to the Ironic API, including uuid, name, chassis_uuid, driver_info, parameters. @@ -3511,8 +3524,9 @@ def register_machine(self, nics, **kwargs): baremetal node. """ try: - machine = self.manager.submitTask( - _tasks.MachineCreate(**kwargs)) + machine = meta.obj_to_dict( + self.manager.submitTask(_tasks.MachineCreate(**kwargs))) + except Exception as e: self.log.debug("ironic machine registration failed", exc_info=True) raise OpenStackCloudException( @@ -3523,7 +3537,7 @@ def register_machine(self, nics, **kwargs): for row in nics: nic = self.manager.submitTask( _tasks.MachinePortCreate(address=row['mac'], - node_uuid=machine.uuid)) + node_uuid=machine['uuid'])) created_nics.append(nic.uuid) except Exception as e: @@ -3539,11 +3553,77 @@ def register_machine(self, nics, **kwargs): pass finally: self.manager.submitTask( - _tasks.MachineDelete(node_id=machine.uuid)) + _tasks.MachineDelete(node_id=machine['uuid'])) raise OpenStackCloudException( "Error registering NICs with the baremetal service: %s" % str(e)) - return meta.obj_to_dict(machine) + + try: + if wait: + for count in _utils._iterate_timeout( + timeout, + "Timeout waiting for node transition to " + "available state"): + + machine = self.get_machine(machine['uuid']) + + # Note(TheJulia): Per the Ironic state code, a node + # that fails returns to enroll state, which means a failed + # node cannot be determined at this point in time. + if machine['provision_state'] in ['enroll']: + self.node_set_provision_state( + machine['uuid'], 'manage') + elif machine['provision_state'] in ['manageable']: + self.node_set_provision_state( + machine['uuid'], 'provide') + elif machine['last_error'] is not None: + raise OpenStackCloudException( + "Machine encountered a failure: %s" + % machine['last_error']) + + # Note(TheJulia): Earlier versions of Ironic default to + # None and later versions default to available up until + # the introduction of enroll state. + # Note(TheJulia): The node will transition through + # cleaning if it is enabled, and we will wait for + # completion. + elif machine['provision_state'] in ['available', None]: + break + + else: + if machine['provision_state'] in ['enroll']: + self.node_set_provision_state(machine['uuid'], 'manage') + # Note(TheJulia): We need to wait for the lock to clear + # before we attempt to set the machine into provide state + # which allows for the transition to available. + for count in _utils._iterate_timeout( + lock_timeout, + "Timeout waiting for reservation to clear " + "before setting provide state"): + machine = self.get_machine(machine['uuid']) + if (machine['reservation'] is None and + machine['provision_state'] is not 'enroll'): + + self.node_set_provision_state( + machine['uuid'], 'provide') + machine = self.get_machine(machine['uuid']) + break + + elif machine['provision_state'] in [ + 'cleaning', + 'available']: + break + + elif machine['last_error'] is not None: + raise OpenStackCloudException( + "Machine encountered a failure: %s" + % machine['last_error']) + + except Exception as e: + raise OpenStackCloudException( + "Error transitioning node to available state: %s" + % e) + return machine def unregister_machine(self, nics, uuid): """Unregister Baremetal from Ironic diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index 47ac46a3a..3d5a727e0 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -367,20 +367,155 @@ class client_return_value: def test_register_machine(self, mock_client): class fake_node: uuid = "00000000-0000-0000-0000-000000000000" + provision_state = "available" + reservation = None + last_error = None expected_return_value = dict( uuid="00000000-0000-0000-0000-000000000000", + provision_state="available", + reservation=None, + last_error=None ) mock_client.node.create.return_value = fake_node + mock_client.node.get.return_value = fake_node nics = [{'mac': '00:00:00:00:00:00'}] return_value = self.cloud.register_machine(nics) self.assertDictEqual(expected_return_value, return_value) self.assertTrue(mock_client.node.create.called) self.assertTrue(mock_client.port.create.called) + self.assertFalse(mock_client.node.get.called) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'node_set_provision_state') + def test_register_machine_enroll( + self, + mock_set_state, + mock_client): + machine_uuid = "00000000-0000-0000-0000-000000000000" + + class fake_node_init_state: + uuid = machine_uuid + provision_state = "enroll" + reservation = None + last_error = None + + class fake_node_post_manage: + uuid = machine_uuid + provision_state = "enroll" + reservation = "do you have a flag?" + last_error = None + + class fake_node_post_manage_done: + uuid = machine_uuid + provision_state = "manage" + reservation = None + last_error = None + + class fake_node_post_provide: + uuid = machine_uuid + provision_state = "available" + reservation = None + last_error = None + + class fake_node_post_enroll_failure: + uuid = machine_uuid + provision_state = "enroll" + reservation = None + last_error = "insufficent lolcats" + + expected_return_value = dict( + uuid=machine_uuid, + provision_state="available", + reservation=None, + last_error=None + ) + + mock_client.node.get.side_effect = iter([ + fake_node_init_state, + fake_node_post_manage, + fake_node_post_manage_done, + fake_node_post_provide]) + mock_client.node.create.return_value = fake_node_init_state + nics = [{'mac': '00:00:00:00:00:00'}] + return_value = self.cloud.register_machine(nics) + self.assertDictEqual(expected_return_value, return_value) + self.assertTrue(mock_client.node.create.called) + self.assertTrue(mock_client.port.create.called) + self.assertTrue(mock_client.node.get.called) + mock_client.reset_mock() + mock_client.node.get.side_effect = iter([ + fake_node_init_state, + fake_node_post_manage, + fake_node_post_manage_done, + fake_node_post_provide]) + return_value = self.cloud.register_machine(nics, wait=True) + self.assertDictEqual(expected_return_value, return_value) + self.assertTrue(mock_client.node.create.called) + self.assertTrue(mock_client.port.create.called) + self.assertTrue(mock_client.node.get.called) + mock_client.reset_mock() + mock_client.node.get.side_effect = iter([ + fake_node_init_state, + fake_node_post_manage, + fake_node_post_enroll_failure]) + self.assertRaises( + shade.OpenStackCloudException, + self.cloud.register_machine, + nics) + self.assertRaises( + shade.OpenStackCloudException, + self.cloud.register_machine, + nics, + wait=True) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'node_set_provision_state') + def test_register_machine_enroll_timeout( + self, + mock_set_state, + mock_client): + machine_uuid = "00000000-0000-0000-0000-000000000000" + + class fake_node_init_state: + uuid = machine_uuid + provision_state = "enroll" + reservation = "do you have a flag?" + last_error = None + + mock_client.node.get.return_value = fake_node_init_state + mock_client.node.create.return_value = fake_node_init_state + nics = [{'mac': '00:00:00:00:00:00'}] + self.assertRaises( + shade.OpenStackCloudException, + self.cloud.register_machine, + nics, + lock_timeout=0.001) + self.assertTrue(mock_client.node.create.called) + self.assertTrue(mock_client.port.create.called) + self.assertTrue(mock_client.node.get.called) + mock_client.node.get.reset_mock() + mock_client.node.create.reset_mock() + self.assertRaises( + shade.OpenStackCloudException, + self.cloud.register_machine, + nics, + wait=True, + timeout=0.001) + self.assertTrue(mock_client.node.create.called) + self.assertTrue(mock_client.port.create.called) + self.assertTrue(mock_client.node.get.called) @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_register_machine_port_create_failed(self, mock_client): + class fake_node: + uuid = "00000000-0000-0000-0000-000000000000" + provision_state = "available" + resevation = None + last_error = None + nics = [{'mac': '00:00:00:00:00:00'}] + mock_client.node.create.return_value = fake_node mock_client.port.create.side_effect = ( exc.OpenStackCloudException("Error")) self.assertRaises(exc.OpenStackCloudException, From 2920aa0e823043cedaacff002f8b73c1c52da75b Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Mon, 17 Aug 2015 18:35:01 -0400 Subject: [PATCH 0436/3836] Addition of shade unregister_machine timeout Adds support for setting wait and timeout values for the removal of machines from Ironic. Change-Id: If08ccd60e18f2f5ceabecc07a2bbc380552a9af1 --- shade/__init__.py | 16 ++++++++++++++-- shade/tests/unit/test_shade_operator.py | 16 ++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 0deb56821..2183ddd2c 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -3545,7 +3545,7 @@ def register_machine(self, nics, **kwargs): % str(e)) return meta.obj_to_dict(machine) - def unregister_machine(self, nics, uuid): + def unregister_machine(self, nics, uuid, wait=False, timeout=600): """Unregister Baremetal from Ironic Removes entries for Network Interfaces and baremetal nodes @@ -3555,6 +3555,13 @@ def unregister_machine(self, nics, uuid): to be removed. :param string uuid: The UUID of the node to be deleted. + :param wait: Boolean value, defaults to false, if to block the method + upon the final step of unregistering the machine. + + :param timeout: Integer value, representing seconds with a default + value of 600, which controls the maximum amount of + time to block the method's completion on. + :raises: OpenStackCloudException on operation failure. """ @@ -3566,7 +3573,6 @@ def unregister_machine(self, nics, uuid): _tasks.MachinePortGetByAddress(address=nic['mac'])) self.manager.submitTask( _tasks.MachinePortDelete(port_id=port_id)) - except Exception as e: self.log.debug( "baremetal NIC unregistration failed", exc_info=True) @@ -3576,6 +3582,12 @@ def unregister_machine(self, nics, uuid): try: self.manager.submitTask( _tasks.MachineDelete(node_id=uuid)) + if wait: + for count in _utils._iterate_timeout( + timeout, + "Timeout waiting for machine to be deleted"): + if not self.get_machine(uuid): + break except Exception as e: self.log.debug( diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index 47ac46a3a..33a002685 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -399,6 +399,22 @@ def test_unregister_machine(self, mock_client): self.assertTrue(mock_client.port.delete.called) self.assertTrue(mock_client.port.get_by_address.called) + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_unregister_machine_timeout(self, mock_client): + nics = [{'mac': '00:00:00:00:00:00'}] + uuid = "00000000-0000-0000-0000-000000000000" + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.unregister_machine, + nics, + uuid, + wait=True, + timeout=0.001) + self.assertTrue(mock_client.node.delete.called) + self.assertTrue(mock_client.port.delete.called) + self.assertTrue(mock_client.port.get_by_address.called) + self.assertTrue(mock_client.node.get.called) + @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_set_machine_maintenace_state(self, mock_client): mock_client.node.set_maintenance.return_value = None From ceb6a1d36ea197d6489d981c112ec187cf29a961 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 31 Aug 2015 15:49:49 -0400 Subject: [PATCH 0437/3836] Move glanceclient to new common interface This is awesome, because it means glance is aligning. Except that timeout and endpoint_type are divergent. We'll add timeout to glanceclient. We need to sweep the other libs and see if they can accept interface. But for now, this works. Change-Id: I755af46a5621983a04c4e07787fd6d10333b7cc4 --- requirements.txt | 2 +- shade/__init__.py | 38 ++++++++++++++++------------------ shade/tests/unit/test_shade.py | 8 +++---- 3 files changed, 23 insertions(+), 25 deletions(-) diff --git a/requirements.txt b/requirements.txt index 890a48acd..a1d7be0a9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ six python-novaclient>=2.21.0 python-keystoneclient>=0.11.0 -python-glanceclient +python-glanceclient>=1.0.0 python-cinderclient<1.2 python-neutronclient>=2.3.10 python-troveclient diff --git a/shade/__init__.py b/shade/__init__.py index 9d20b4265..d54a5baf3 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -598,29 +598,27 @@ def delete_user(self, name_or_id): @property def glance_client(self): - # Note that glanceclient doesn't use keystoneclient sessions - # which means that it won't make a new token if the old one has - # expired. Work around that by always making a new glanceclient here - # which may create a new token if the current one is close to - # expiration. - token = self.auth_token - endpoint = self.get_session_endpoint('image') - kwargs = dict() - if self.api_timeout is not None: - kwargs['timeout'] = self.api_timeout + service_key = 'image' try: + # trigger exception on lack of service + self.get_session_endpoint(service_key) self._glance_client = glanceclient.Client( - self.cloud_config.get_api_version('image'), - endpoint, token=token, - session=self.keystone_session, insecure=not self.verify, - cacert=self.cert, **kwargs) - except Exception as e: - self.log.debug("glance unknown issue", exc_info=True) + version=self.cloud_config.get_api_version(service_key), + session=self.keystone_session, + service_name=self.cloud_config.get_service_name(service_key), + service_type=self.cloud_config.get_service_type(service_key), + interface=self.cloud_config.get_interface(service_key), + region_name=self.region_name) + except Exception: + self.log.debug( + "Couldn't construct {service} object".format( + service=service_key), exc_info=True) + raise + if self._glance_client is None: raise OpenStackCloudException( - "Error in connecting to glance: %s" % str(e)) - - if not self._glance_client: - raise OpenStackCloudException("Error connecting to glance") + "Failed to instantiate {service} client." + " This could mean that your credentials are wrong.".format( + service=service_key)) return self._glance_client @property diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 72ca680ea..515df91c3 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -119,13 +119,13 @@ def test_list_servers_exception(self, mock_client): @mock.patch.object(shade.OpenStackCloud, 'keystone_session') @mock.patch.object(glanceclient, 'Client') - def test_glance_ssl_args(self, mock_client, mock_keystone_session): + def test_glance_args(self, mock_client, mock_keystone_session): mock_keystone_session.return_value = None self.cloud.glance_client mock_client.assert_called_with( - '1', mock.ANY, token=mock.ANY, session=mock.ANY, - insecure=False, - cacert=None, + version='1', region_name='', service_name=None, + interface='public', + service_type='image', session=mock.ANY, ) @mock.patch.object(shade.OpenStackCloud, 'search_subnets') From dcf079e4185b800d7c5a227ea903fa7abcecb3f4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 2 Sep 2015 10:43:10 -0400 Subject: [PATCH 0438/3836] Delete floating ip by ID instead of name Change the cleanup function so that it does not assume that the external address is a floating ip. Further, pass in floating ip id to delete, rather than the IP address. Change-Id: I2062ddd1bfea1787375961c58af284479dc0982f --- shade/tests/functional/test_floating_ip.py | 23 +++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/shade/tests/functional/test_floating_ip.py b/shade/tests/functional/test_floating_ip.py index e72b74e10..357e60af5 100644 --- a/shade/tests/functional/test_floating_ip.py +++ b/shade/tests/functional/test_floating_ip.py @@ -139,15 +139,20 @@ def _cleanup_servers(self): # wrong raise OpenStackCloudException('\n'.join(exception_list)) - def _cleanup_ips(self, ips): + def _cleanup_ips(self, server): + exception_list = list() - for ip in ips: - try: - self.cloud.delete_floating_ip(ip) - except Exception as e: - exception_list.append(e) - continue + fixed_ip = meta.get_server_private_ip(server) + + for ip in self.cloud.list_floating_ips(): + if (ip.get('fixed_ip', None) == fixed_ip + or ip.get('fixed_ip_address', None) == fixed_ip): + try: + self.cloud.delete_floating_ip(ip['id']) + except Exception as e: + exception_list.append(e) + continue if exception_list: # Raise an error: we must make users aware that something went @@ -205,7 +210,7 @@ def test_add_auto_ip(self): break new_server = self.cloud.get_server(new_server.id) - self.addCleanup(self._cleanup_ips, [ip]) + self.addCleanup(self._cleanup_ips, new_server) def test_detach_ip_from_server(self): self._setup_networks() @@ -225,7 +230,7 @@ def test_detach_ip_from_server(self): break new_server = self.cloud.get_server(new_server.id) - self.addCleanup(self._cleanup_ips, [ip]) + self.addCleanup(self._cleanup_ips, new_server) f_ip = self.cloud.get_floating_ip( id=None, filters={'floating_ip_address': ip}) From 16b0720e40ceba532566f9e794527305b8d299f1 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 1 Sep 2015 10:06:43 -0400 Subject: [PATCH 0439/3836] Pass timeout to session, not constructors In Session-based construction, we need to tell the Session about the timeout. Change-Id: Ia18940848431902c82580a6214d6d6b6e5783fb0 --- shade/__init__.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index d54a5baf3..4cf0b0655 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -318,8 +318,7 @@ def _get_client(self, service_key, client_class): service_name=self.cloud_config.get_service_name(service_key), service_type=self.cloud_config.get_service_type(service_key), endpoint_type=self.cloud_config.get_interface(service_key), - region_name=self.region_name, - timeout=self.api_timeout) + region_name=self.region_name) except Exception: self.log.debug( "Couldn't construct {service} object".format( @@ -378,7 +377,8 @@ def keystone_session(self): self._keystone_session = ksc_session.Session( auth=keystone_auth, verify=self.verify, - cert=self.cert) + cert=self.cert, + timeout=self.api_timeout) except Exception as e: self.log.debug("keystone unknown issue", exc_info=True) raise OpenStackCloudException( @@ -390,8 +390,7 @@ def keystone_client(self): if self._keystone_client is None: try: self._keystone_client = self._get_identity_client_class()( - session=self.keystone_session, - timeout=self.api_timeout) + session=self.keystone_session) except Exception as e: self.log.debug( "Couldn't construct keystone object", exc_info=True) @@ -686,7 +685,6 @@ def trove_client(self): session=self.keystone_session, region_name=self.region_name, service_type=self.cloud_config.get_service_type('database'), - timeout=self.api_timeout, ) if self._trove_client is None: @@ -706,8 +704,7 @@ def neutron_client(self): self._neutron_client = neutron_client.Client( token=self.auth_token, session=self.keystone_session, - region_name=self.region_name, - timeout=self.api_timeout) + region_name=self.region_name) return self._neutron_client @property From bd24a6dd8efa9d35014f98b4f69426656f59c068 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 1 Sep 2015 10:15:25 -0400 Subject: [PATCH 0440/3836] Remove last vestige of glanceclient being different There is an ongoing transition across OpenStack from endpoint_type to interface. New glanceclient uses the new form, so we support both and can remove the copy/pasta in the glance_client constructor. Change-Id: I63f70a0102eb7d7982eacf28ff14642550942911 --- shade/__init__.py | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 4cf0b0655..921a0768c 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -308,17 +308,20 @@ def generate_key(*args, **kwargs): return ans return generate_key - def _get_client(self, service_key, client_class): + def _get_client( + self, service_key, client_class, interface_key='endpoint_type'): try: + interface = self.cloud_config.get_interface(service_key) # trigger exception on lack of service self.get_session_endpoint(service_key) - client = client_class( + constructor_args = dict( version=self.cloud_config.get_api_version(service_key), session=self.keystone_session, service_name=self.cloud_config.get_service_name(service_key), service_type=self.cloud_config.get_service_type(service_key), - endpoint_type=self.cloud_config.get_interface(service_key), region_name=self.region_name) + constructor_args[interface_key] = interface + client = client_class(**constructor_args) except Exception: self.log.debug( "Couldn't construct {service} object".format( @@ -597,27 +600,9 @@ def delete_user(self, name_or_id): @property def glance_client(self): - service_key = 'image' - try: - # trigger exception on lack of service - self.get_session_endpoint(service_key) - self._glance_client = glanceclient.Client( - version=self.cloud_config.get_api_version(service_key), - session=self.keystone_session, - service_name=self.cloud_config.get_service_name(service_key), - service_type=self.cloud_config.get_service_type(service_key), - interface=self.cloud_config.get_interface(service_key), - region_name=self.region_name) - except Exception: - self.log.debug( - "Couldn't construct {service} object".format( - service=service_key), exc_info=True) - raise if self._glance_client is None: - raise OpenStackCloudException( - "Failed to instantiate {service} client." - " This could mean that your credentials are wrong.".format( - service=service_key)) + self._glance_client = self._get_client( + 'image', glanceclient.Client, interface_key='interface') return self._glance_client @property From e5895144ec9591d03349dc6e3066148dc4b4543d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 15 Aug 2015 15:56:23 +0700 Subject: [PATCH 0441/3836] Migrate neutron to the common client interface Neutron was causing issues before. So make it its own patch to isloate it. Change-Id: I4621afa67f4f06d3cb12d6226ee00fb6e5611759 --- shade/__init__.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 921a0768c..b7c1cd45f 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -309,18 +309,21 @@ def generate_key(*args, **kwargs): return generate_key def _get_client( - self, service_key, client_class, interface_key='endpoint_type'): + self, service_key, client_class, interface_key='endpoint_type', + pass_version_arg=True): try: interface = self.cloud_config.get_interface(service_key) # trigger exception on lack of service self.get_session_endpoint(service_key) constructor_args = dict( - version=self.cloud_config.get_api_version(service_key), session=self.keystone_session, service_name=self.cloud_config.get_service_name(service_key), service_type=self.cloud_config.get_service_type(service_key), region_name=self.region_name) constructor_args[interface_key] = interface + if pass_version_arg: + version = self.cloud_config.get_api_version(service_key) + constructor_args['version'] = version client = client_class(**constructor_args) except Exception: self.log.debug( @@ -684,12 +687,8 @@ def trove_client(self): @property def neutron_client(self): if self._neutron_client is None: - # trigger exception on lack of neutron - self.get_session_endpoint('network') - self._neutron_client = neutron_client.Client( - token=self.auth_token, - session=self.keystone_session, - region_name=self.region_name) + self._neutron_client = self._get_client( + 'network', neutron_client.Client, pass_version_arg=False) return self._neutron_client @property From 337a0ed8fcb74ce67f6275851c4f512f66037820 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 2 Sep 2015 07:39:44 -0400 Subject: [PATCH 0442/3836] Fix exception lists in functional tests There is a pattern in our functional tests where exceptions are aggregated within a list and then joined within a single string. However, the join expects them as strings, not as exception objects. When this code is hit, it causes an exception: TypeError: sequence item 0: expected string, OpenStackCloudException found This change converts those exception objects to strings. Change-Id: I0a041a61537b79c73615e5136492141da0926fee --- shade/tests/functional/test_endpoints.py | 4 ++-- shade/tests/functional/test_flavor.py | 2 +- shade/tests/functional/test_floating_ip.py | 10 +++++----- shade/tests/functional/test_port.py | 2 +- shade/tests/functional/test_services.py | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/shade/tests/functional/test_endpoints.py b/shade/tests/functional/test_endpoints.py index f3acf6d86..79c925ea1 100644 --- a/shade/tests/functional/test_endpoints.py +++ b/shade/tests/functional/test_endpoints.py @@ -54,7 +54,7 @@ def _cleanup_endpoints(self): self.operator_cloud.delete_endpoint(id=e['id']) except Exception as e: # We were unable to delete a service, let's try with next - exception_list.append(e) + exception_list.append(str(e)) continue if exception_list: # Raise an error: we must make users aware that something went @@ -70,7 +70,7 @@ def _cleanup_services(self): self.operator_cloud.delete_service(name_or_id=s['id']) except Exception as e: # We were unable to delete a service, let's try with next - exception_list.append(e) + exception_list.append(str(e)) continue if exception_list: # Raise an error: we must make users aware that something went diff --git a/shade/tests/functional/test_flavor.py b/shade/tests/functional/test_flavor.py index 1250374d9..573485b52 100644 --- a/shade/tests/functional/test_flavor.py +++ b/shade/tests/functional/test_flavor.py @@ -50,7 +50,7 @@ def _cleanup_flavors(self): self.operator_cloud.delete_flavor(f['id']) except Exception as e: # We were unable to delete a flavor, let's try with next - exception_list.append(e) + exception_list.append(str(e)) continue if exception_list: # Raise an error: we must make users aware that something went diff --git a/shade/tests/functional/test_floating_ip.py b/shade/tests/functional/test_floating_ip.py index 357e60af5..521eb3afa 100644 --- a/shade/tests/functional/test_floating_ip.py +++ b/shade/tests/functional/test_floating_ip.py @@ -93,7 +93,7 @@ def _cleanup_network(self): pass self.cloud.delete_router(name_or_id=r['id']) except Exception as e: - exception_list.append(e) + exception_list.append(str(e)) continue # Delete subnets for s in self.cloud.list_subnets(): @@ -101,7 +101,7 @@ def _cleanup_network(self): try: self.cloud.delete_subnet(name_or_id=s['id']) except Exception as e: - exception_list.append(e) + exception_list.append(str(e)) continue # Delete networks for n in self.cloud.list_networks(): @@ -109,7 +109,7 @@ def _cleanup_network(self): try: self.cloud.delete_network(name_or_id=n['id']) except Exception as e: - exception_list.append(e) + exception_list.append(str(e)) continue if exception_list: @@ -131,7 +131,7 @@ def _cleanup_servers(self): except nova_exc.NotFound: break except Exception as e: - exception_list.append(e) + exception_list.append(str(e)) continue if exception_list: @@ -151,7 +151,7 @@ def _cleanup_ips(self, server): try: self.cloud.delete_floating_ip(ip['id']) except Exception as e: - exception_list.append(e) + exception_list.append(str(e)) continue if exception_list: diff --git a/shade/tests/functional/test_port.py b/shade/tests/functional/test_port.py index b8416cfd7..85f5f887f 100644 --- a/shade/tests/functional/test_port.py +++ b/shade/tests/functional/test_port.py @@ -53,7 +53,7 @@ def _cleanup_ports(self): self.cloud.delete_port(name_or_id=p['id']) except Exception as e: # We were unable to delete this port, let's try with next - exception_list.append(e) + exception_list.append(str(e)) continue if exception_list: diff --git a/shade/tests/functional/test_services.py b/shade/tests/functional/test_services.py index 16889f080..3c6bc589f 100644 --- a/shade/tests/functional/test_services.py +++ b/shade/tests/functional/test_services.py @@ -52,7 +52,7 @@ def _cleanup_services(self): self.operator_cloud.delete_service(name_or_id=s['id']) except Exception as e: # We were unable to delete a service, let's try with next - exception_list.append(e) + exception_list.append(str(e)) continue if exception_list: # Raise an error: we must make users aware that something went From b52d74478127617b4d0583ce63739fdd3c4eebf4 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Tue, 18 Aug 2015 12:47:38 -0400 Subject: [PATCH 0443/3836] unregister_machine blocking logic Additional logic to preemptively prevent NIC removal when a machine is unregistered by pre-validating if a node is can be safely deleted. Change-Id: I55152de9d42143be3b8a4e3da00da0bc0ec88dff --- shade/__init__.py | 9 ++++++-- shade/tests/unit/test_shade_operator.py | 30 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 4cf0b0655..ebc59dbe7 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -3624,8 +3624,13 @@ def unregister_machine(self, nics, uuid, wait=False, timeout=600): :raises: OpenStackCloudException on operation failure. """ - # TODO(TheJulia): Change to lookup the MAC addresses and/or block any - # the action if the node is in an Active state as the API would. + machine = self.get_machine(uuid) + invalid_states = ['active', 'cleaning', 'clean wait', 'clean failed'] + if machine['provision_state'] in invalid_states: + raise OpenStackCloudException( + "Error unregistering node '%s' due to current provision " + "state '%s'" % (uuid, machine['provision_state'])) + for nic in nics: try: port_id = self.manager.submitTask( diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index 6ee15ea07..60d05b9bb 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -527,6 +527,10 @@ class fake_node: @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_unregister_machine(self, mock_client): + class fake_node: + provision_state = 'available' + + mock_client.node.get.return_value = fake_node nics = [{'mac': '00:00:00:00:00:00'}] uuid = "00000000-0000-0000-0000-000000000000" self.cloud.unregister_machine(nics, uuid) @@ -534,8 +538,34 @@ def test_unregister_machine(self, mock_client): self.assertTrue(mock_client.port.delete.called) self.assertTrue(mock_client.port.get_by_address.called) + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_unregister_machine_unavailable(self, mock_client): + invalid_states = ['active', 'cleaning', 'clean wait', 'clean failed'] + nics = [{'mac': '00:00:00:00:00:00'}] + uuid = "00000000-0000-0000-0000-000000000000" + for state in invalid_states: + class fake_node: + provision_state = state + + mock_client.node.get.return_value = fake_node + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.unregister_machine, + nics, + uuid) + self.assertFalse(mock_client.node.delete.called) + self.assertFalse(mock_client.port.delete.called) + self.assertFalse(mock_client.port.get_by_address.called) + self.assertTrue(mock_client.node.get.called) + mock_client.node.reset_mock() + mock_client.node.reset_mock() + @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_unregister_machine_timeout(self, mock_client): + class fake_node: + provision_state = 'available' + + mock_client.node.get.return_value = fake_node nics = [{'mac': '00:00:00:00:00:00'}] uuid = "00000000-0000-0000-0000-000000000000" self.assertRaises( From fb24f1d113c03cf4d542331428e63f58a80eef71 Mon Sep 17 00:00:00 2001 From: Eric Harney Date: Thu, 3 Sep 2015 07:52:37 -0400 Subject: [PATCH 0444/3836] Handle empty defaults.yaml file If defaults.yaml is empty, a TypeError is thrown because the result of yaml.load is not iterable. Change-Id: Ic3283ebaf9dd325e4f430e70bce08c6d716f60bc --- os_client_config/defaults.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/os_client_config/defaults.py b/os_client_config/defaults.py index a274767bd..897cff568 100644 --- a/os_client_config/defaults.py +++ b/os_client_config/defaults.py @@ -35,6 +35,8 @@ def get_defaults(): key=None, ) with open(_yaml_path, 'r') as yaml_file: - _defaults.update(yaml.load(yaml_file.read())) + updates = yaml.load(yaml_file.read()) + if updates is not None: + _defaults.update(updates) return _defaults.copy() From 82aea48d1057c123f68042f2017f46b5df117f23 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 3 Sep 2015 09:53:22 -0400 Subject: [PATCH 0445/3836] Avoid 2.27.0 of novaclient There is an unfortunate set of bugs out in the wild which make 2.27.0 novaclient non-working against many existing public clouds. Fixes are in works for 2.28.0 - for now, express that 2.27.0 should not be used. Change-Id: Ia6cc574c8ec8bf1290f01d5a602dd2b8ee78549d --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a1d7be0a9..9667cceab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ jsonpatch os-client-config>=1.6.2 six -python-novaclient>=2.21.0 +python-novaclient>=2.21.0,!=2.27.0 python-keystoneclient>=0.11.0 python-glanceclient>=1.0.0 python-cinderclient<1.2 From 24bff5dca48365a767d8b6f0c38be75b2d67d6fd Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 4 Sep 2015 09:41:57 -0400 Subject: [PATCH 0446/3836] Update OVH public cloud information OVH has two regions now. Also, be clear that OVH does not use floating ips. Change-Id: Iabd84748cccec7da6e083b647511cba48d57bc20 --- doc/source/vendor-support.rst | 1 + os_client_config/vendors/ovh.yaml | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index 3cf39e03f..2af8f6eb3 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -153,6 +153,7 @@ https://auth.cloud.ovh.net/v2.0 Region Name Human Name ============== ================ SBG-1 Strassbourg, FR +GRA-1 Gravelines, FR ============== ================ * Images must be in `raw` format diff --git a/os_client_config/vendors/ovh.yaml b/os_client_config/vendors/ovh.yaml index f83437297..52b91a466 100644 --- a/os_client_config/vendors/ovh.yaml +++ b/os_client_config/vendors/ovh.yaml @@ -2,5 +2,8 @@ name: ovh profile: auth: auth_url: https://auth.cloud.ovh.net/v2.0 - region_name: SBG1 + regions: + - GRA1 + - SBG1 image_format: raw + floating_ip_source: None From 743543743daa891424336ac08a3fcd02d16645ea Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Fri, 14 Aug 2015 16:22:21 -0400 Subject: [PATCH 0447/3836] Bump the default API version for python-ironicclient Update the default API client version to 1.11 in order to support node enrollment. Additionally updating the minimum version of the python-ironicclient library. Change-Id: Idedbe31fe1b7f0cb9d0980625ebf84d7656f6996 --- requirements.txt | 2 +- shade/__init__.py | 11 +++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/requirements.txt b/requirements.txt index 9667cceab..ee26521a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ python-glanceclient>=1.0.0 python-cinderclient<1.2 python-neutronclient>=2.3.10 python-troveclient -python-ironicclient>=0.5.1 +python-ironicclient>=0.7.0 python-swiftclient>=2.5.0 python-designateclient>=1.3.0 diff --git a/shade/__init__.py b/shade/__init__.py index 18091fd12..07a6d4589 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -3336,14 +3336,9 @@ def ironic_client(self): # Set the ironic API microversion to a known-good # supported/tested with the contents of shade. # - # Note(TheJulia): Defaulted to version 1.6 as the ironic - # state machine changes which will increment the version - # and break an automatic transition of an enrolled node - # to an available state. Locking the version is intended - # to utilize the original transition until shade supports - # calling for node inspection to allow the transition to - # take place automatically. - ironic_api_microversion = '1.6' + # Note(TheJulia): Defaulted to version 1.11 as node enrollment + # steps are navigated by the register_machine method. + ironic_api_microversion = '1.11' if self.auth_type in (None, "None", ''): # TODO: This needs to be improved logic wise, perhaps a list, From a51ab85bff1172dda04ce57021ad87041b754bb5 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 12 Sep 2015 09:54:16 +0200 Subject: [PATCH 0448/3836] Add default version number for heat Heat only has v1. Change-Id: I31b40b7cfec0685105b8066d1cb385befae00f90 --- os_client_config/defaults.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/os_client_config/defaults.yaml b/os_client_config/defaults.yaml index 9130228f2..abc75cacc 100644 --- a/os_client_config/defaults.yaml +++ b/os_client_config/defaults.yaml @@ -12,5 +12,6 @@ image_api_version: '1' image_format: qcow2 network_api_version: '2' object_api_version: '1' +orchestration_api_version: '1' secgroup_source: neutron volume_api_version: '1' From eb6ed09dede71a441d005d773b94411d70f1557d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 15 Sep 2015 02:11:14 +0200 Subject: [PATCH 0449/3836] Remove duplicate lines that are the same as default Change-Id: I007802ac08a8708fbbbfa8ee8c7d79e4e2bb55f7 --- doc/source/vendor-support.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index 2af8f6eb3..e4db27665 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -109,7 +109,6 @@ BHS-1 Beauharnois, QC ============== ================ * Image API Version is 2 -* Images must be in `qcow2` format * Floating IPs are not supported unitedstack @@ -139,7 +138,6 @@ Region Name Human Name RegionOne RegionOne ============== ================ -* Identity API Version is 2 * Public IPv4 is provided via NAT with Nova Floating IP * Floating IPs are provided by Nova * Security groups are provided by Nova From 9a626589a8082a002a02530a3f480088c9dc4fa9 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 15 Aug 2015 15:58:28 +0700 Subject: [PATCH 0450/3836] Move keystone to common identity client interface It's weird that keystone is different from the other things. But let's just let that be life. Change-Id: I3891454b2706db2553e52f1ca3932285260f08bc --- shade/__init__.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 5c03b0979..c8460cecf 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -394,14 +394,8 @@ def keystone_session(self): @property def keystone_client(self): if self._keystone_client is None: - try: - self._keystone_client = self._get_identity_client_class()( - session=self.keystone_session) - except Exception as e: - self.log.debug( - "Couldn't construct keystone object", exc_info=True) - raise OpenStackCloudException( - "Error constructing keystone client: %s" % str(e)) + self._keystone_client = self._get_client( + 'identity', self._get_identity_client_class()) return self._keystone_client @property From 15c6652dc4bfc5b417bb67df0435935962d71d9c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 7 Sep 2015 18:55:24 -0500 Subject: [PATCH 0451/3836] Return keystoneauth plugins based on auth args We know all of the things that we need to know to return an appropriate auth plugin from keystoneauth based on the auth parameters. This also introduces a hard-depend on keystoneauth, which should be fine since keystoneauth itself has a very low dependency count. We'll also use ksa to help validate auth parameters when we're doing the config processing. Change-Id: Ia1a1a4adb4dcefed5d7607082e026ca7361f394d --- os_client_config/cloud_config.py | 16 ++++++++++++++++ requirements.txt | 1 + 2 files changed, 17 insertions(+) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index f60303b2a..c9f9f0756 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -14,6 +14,8 @@ import warnings +from keystoneauth1 import loading + class CloudConfig(object): def __init__(self, name, region, config, prefer_ipv6=False): @@ -106,3 +108,17 @@ def get_endpoint(self, service_type): @property def prefer_ipv6(self): return self._prefer_ipv6 + + def get_auth(self): + """Return a keystoneauth plugin from the auth credentials.""" + # Re-use the admin_token plugin for the "None" plugin + # since it does not look up endpoints or tokens but rather + # does a passthrough. This is useful for things like Ironic + # that have a keystoneless operational mode, but means we're + # still dealing with a keystoneauth Session object, so all the + # _other_ things (SSL arg handling, timeout) all work consistently + if self.config['auth_type'] in (None, "None", ''): + self.config['auth_type'] = 'admin_token' + self.config['auth']['token'] = None + loader = loading.get_plugin_loader(self.config['auth_type']) + return loader.load_from_options(**self.config['auth']) diff --git a/requirements.txt b/requirements.txt index 894a70acc..db0b6354a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ # process, which may cause wedges in the gate later. PyYAML>=3.1.0 appdirs>=1.3.0 +keystoneauth1>=1.0.0 From 53b7b7a6d61243254974ad3c1c256809c86a27bf Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 15 Sep 2015 02:09:12 +0200 Subject: [PATCH 0452/3836] Add citycloud to the vendors list Nice job with keystone v3 support. Change-Id: Ib2b54feef5055b04740a63a3a1d4e0b967018864 --- doc/source/vendor-support.rst | 17 +++++++++++++++++ os_client_config/vendors/citycloud.yaml | 9 +++++++++ 2 files changed, 26 insertions(+) create mode 100644 os_client_config/vendors/citycloud.yaml diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index e4db27665..51e843a96 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -156,3 +156,20 @@ GRA-1 Gravelines, FR * Images must be in `raw` format * Floating IPs are not supported + +citycloud +--------- + +https://identity1.citycloud.com:5000/v3/ + +============== ================ +Region Name Human Name +============== ================ +Lon1 London, UK +Sto2 Stockholm, SE +Kna1 Karlskrona, SE +============== ================ + +* Identity API Version is 3 +* Image API Version is 2 +* Public IPv4 is provided via NAT with Neutron Floating IP diff --git a/os_client_config/vendors/citycloud.yaml b/os_client_config/vendors/citycloud.yaml new file mode 100644 index 000000000..9ee0b9b4f --- /dev/null +++ b/os_client_config/vendors/citycloud.yaml @@ -0,0 +1,9 @@ +name: citycloud +profile: + auth: + auth_url: https://identity1.citycloud.com:5000/v3/ + regions: + - Lon1 + - Sto2 + - Kna1 + identity_api_version: '3' From d11d165e697871b73236025efd5e8698d0b08750 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 15 Sep 2015 02:30:13 +0200 Subject: [PATCH 0453/3836] Update auro auth_url and region information Also, turns out auro is running Glance v2. Change-Id: Iccc2e09f45192ac21001c346dab048c77a0f7813 --- doc/source/vendor-support.rst | 3 ++- os_client_config/vendors/auro.yaml | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index 51e843a96..58207a7dc 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -135,10 +135,11 @@ https://api.auro.io:5000/v2.0 ============== ================ Region Name Human Name ============== ================ -RegionOne RegionOne +van1 Vancouver, BC ============== ================ * Public IPv4 is provided via NAT with Nova Floating IP +* Image API Version is 2 * Floating IPs are provided by Nova * Security groups are provided by Nova diff --git a/os_client_config/vendors/auro.yaml b/os_client_config/vendors/auro.yaml index 987838bbb..c19a6571a 100644 --- a/os_client_config/vendors/auro.yaml +++ b/os_client_config/vendors/auro.yaml @@ -1,7 +1,8 @@ name: auro profile: auth: - auth_url: https://api.auro.io:5000/v2.0 - region_name: RegionOne + auth_url: https://api.van1.auro.io:5000/v2.0 + region_name: van1 + image_api_version: '2' secgroup_source: nova floating_ip_source: nova From 265abb2bf6e050d9c7bf9b3e0a307dca32899c1f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 15 Sep 2015 02:13:06 +0200 Subject: [PATCH 0454/3836] Switch the image default to v2 It turns out it's more common than v1. Also, ovh was inappropriately listing glance v1. Change-Id: Icb534bf98fd3fa1c900ed9e4dd09ea0643176ff9 --- doc/source/vendor-support.rst | 11 ++--------- os_client_config/defaults.yaml | 2 +- os_client_config/vendors/dreamhost.yaml | 1 - os_client_config/vendors/hp.yaml | 1 + os_client_config/vendors/rackspace.yaml | 1 - os_client_config/vendors/runabove.yaml | 1 - os_client_config/vendors/unitedstack.yaml | 1 - os_client_config/vendors/vexxhost.yaml | 1 - 8 files changed, 4 insertions(+), 15 deletions(-) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index 58207a7dc..b8ececbfb 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -15,7 +15,7 @@ These are the default behaviors unless a cloud is configured differently. * Identity uses `password` authentication * Identity API Version is 2 -* Image API Version is 1 +* Image API Version is 2 * Images must be in `qcow2` format * Images are uploaded using PUT interface * Public IPv4 is directly routable via DHCP from Neutron @@ -37,6 +37,7 @@ region-b.geo-1 US East ============== ================ * DNS Service Type is `hpext:dns` +* Image API Version is 1 * Public IPv4 is provided via NAT with Neutron Floating IP rackspace @@ -56,7 +57,6 @@ HKG Hong Kong * Database Service Type is `rax:database` * Compute Service Name is `cloudServersOpenStack` -* Image API Version is 2 * Images must be in `vhd` format * Images must be uploaded using the Glance Task Interface * Floating IPs are not needed @@ -78,7 +78,6 @@ Region Name Human Name RegionOne Region One ============== ================ -* Image API Version is 2 * Images must be in `raw` format * Public IPv4 is provided via NAT with Neutron Floating IP * IPv6 is provided to every server @@ -94,8 +93,6 @@ Region Name Human Name ca-ymq-1 Montreal ============== ================ -* Image API Version is 2 - runabove -------- @@ -108,7 +105,6 @@ SBG-1 Strassbourg, FR BHS-1 Beauharnois, QC ============== ================ -* Image API Version is 2 * Floating IPs are not supported unitedstack @@ -124,7 +120,6 @@ gd1 Guangdong ============== ================ * Identity API Version is 3 -* Image API Version is 2 * Images must be in `raw` format auro @@ -139,7 +134,6 @@ van1 Vancouver, BC ============== ================ * Public IPv4 is provided via NAT with Nova Floating IP -* Image API Version is 2 * Floating IPs are provided by Nova * Security groups are provided by Nova @@ -172,5 +166,4 @@ Kna1 Karlskrona, SE ============== ================ * Identity API Version is 3 -* Image API Version is 2 * Public IPv4 is provided via NAT with Neutron Floating IP diff --git a/os_client_config/defaults.yaml b/os_client_config/defaults.yaml index abc75cacc..e81eb1649 100644 --- a/os_client_config/defaults.yaml +++ b/os_client_config/defaults.yaml @@ -8,7 +8,7 @@ interface: public floating_ip_source: neutron identity_api_version: '2' image_api_use_tasks: false -image_api_version: '1' +image_api_version: '2' image_format: qcow2 network_api_version: '2' object_api_version: '1' diff --git a/os_client_config/vendors/dreamhost.yaml b/os_client_config/vendors/dreamhost.yaml index 5e10d1403..3cd395a17 100644 --- a/os_client_config/vendors/dreamhost.yaml +++ b/os_client_config/vendors/dreamhost.yaml @@ -3,5 +3,4 @@ profile: auth: auth_url: https://keystone.dream.io/v2.0 region_name: RegionOne - image_api_version: '2' image_format: raw diff --git a/os_client_config/vendors/hp.yaml b/os_client_config/vendors/hp.yaml index 4da49568d..a0544df82 100644 --- a/os_client_config/vendors/hp.yaml +++ b/os_client_config/vendors/hp.yaml @@ -6,3 +6,4 @@ profile: - region-a.geo-1 - region-b.geo-1 dns_service_type: hpext:dns + image_api_version: '1' diff --git a/os_client_config/vendors/rackspace.yaml b/os_client_config/vendors/rackspace.yaml index ad7825dec..5cf7a44e6 100644 --- a/os_client_config/vendors/rackspace.yaml +++ b/os_client_config/vendors/rackspace.yaml @@ -10,7 +10,6 @@ profile: - SYD database_service_type: rax:database compute_service_name: cloudServersOpenStack - image_api_version: '2' image_api_use_tasks: true image_format: vhd floating_ip_source: None diff --git a/os_client_config/vendors/runabove.yaml b/os_client_config/vendors/runabove.yaml index 57e75f37a..34528941a 100644 --- a/os_client_config/vendors/runabove.yaml +++ b/os_client_config/vendors/runabove.yaml @@ -5,6 +5,5 @@ profile: regions: - BHS-1 - SBG-1 - image_api_version: '2' image_format: qcow2 floating_ip_source: None diff --git a/os_client_config/vendors/unitedstack.yaml b/os_client_config/vendors/unitedstack.yaml index f22bd8abe..c6d5cc20e 100644 --- a/os_client_config/vendors/unitedstack.yaml +++ b/os_client_config/vendors/unitedstack.yaml @@ -6,6 +6,5 @@ profile: - bj1 - gd1 identity_api_version: '3' - image_api_version: '2' image_format: raw floating_ip_source: None diff --git a/os_client_config/vendors/vexxhost.yaml b/os_client_config/vendors/vexxhost.yaml index f67c644c6..4a0ba271b 100644 --- a/os_client_config/vendors/vexxhost.yaml +++ b/os_client_config/vendors/vexxhost.yaml @@ -3,5 +3,4 @@ profile: auth: auth_url: http://auth.api.thenebulacloud.com:5000/v2.0/ region_name: ca-ymq-1 - image_api_version: '2' floating_ip_source: None From 7bfa633e86e9ccef6cd58472f6994b05f95ad032 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 15 Sep 2015 11:54:22 +0200 Subject: [PATCH 0455/3836] Add elastx to vendor support matrix Change-Id: I67973f89e2da4ef550e46c32ad63a4c8e043b4d0 --- doc/source/vendor-support.rst | 13 +++++++++++++ os_client_config/vendors/elastx.yaml | 5 +++++ 2 files changed, 18 insertions(+) create mode 100644 os_client_config/vendors/elastx.yaml diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index b8ececbfb..f4179e783 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -167,3 +167,16 @@ Kna1 Karlskrona, SE * Identity API Version is 3 * Public IPv4 is provided via NAT with Neutron Floating IP + +elastx +------ + +https://ops.elastx.net:5000/v2.0 + +============== ================ +Region Name Human Name +============== ================ +regionOne Region One +============== ================ + +* Public IPv4 is provided via NAT with Neutron Floating IP diff --git a/os_client_config/vendors/elastx.yaml b/os_client_config/vendors/elastx.yaml new file mode 100644 index 000000000..810e12ede --- /dev/null +++ b/os_client_config/vendors/elastx.yaml @@ -0,0 +1,5 @@ +name: elastx +profile: + auth: + auth_url: https://ops.elastx.net:5000/v2.0 + region_name: regionOne From d835c47ad35a27bb0e90a8db42d5c943a65c6608 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 15 Sep 2015 12:54:49 +0200 Subject: [PATCH 0456/3836] Add Enter Cloud Suite to vendors list Change-Id: I78e9191073e68a9d3f78ba11b47aa0b1ff816430 --- doc/source/vendor-support.rst | 13 +++++++++++++ os_client_config/vendors/entercloudsuite.yaml | 8 ++++++++ 2 files changed, 21 insertions(+) create mode 100644 os_client_config/vendors/entercloudsuite.yaml diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index f4179e783..d987b1cd2 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -180,3 +180,16 @@ regionOne Region One ============== ================ * Public IPv4 is provided via NAT with Neutron Floating IP + +entercloudsuite +--------------- + +https://api.entercloudsuite.com/v2.0 + +============== ================ +Region Name Human Name +============== ================ +nl-ams1 Amsterdam, NL +it-mil1 Milan, IT +de-fra1 Frankfurt, DE +============== ================ diff --git a/os_client_config/vendors/entercloudsuite.yaml b/os_client_config/vendors/entercloudsuite.yaml new file mode 100644 index 000000000..f68bcf674 --- /dev/null +++ b/os_client_config/vendors/entercloudsuite.yaml @@ -0,0 +1,8 @@ +name: entercloudsuite +profile: + auth: + auth_url: https://api.entercloudsuite.com/v2.0 + regions: + - it-mil1 + - nl-ams1 + - de-fra1 From 2762241d7bfccf8c4bad471dfc20528d1135602d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 15 Sep 2015 13:14:32 +0200 Subject: [PATCH 0457/3836] Add ultimum to list of vendors Change-Id: Ic5b5ee307b9d1eb338839a7feeeab50469316cf5 --- doc/source/vendor-support.rst | 11 +++++++++++ os_client_config/vendors/ultimum.yaml | 5 +++++ 2 files changed, 16 insertions(+) create mode 100644 os_client_config/vendors/ultimum.yaml diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index d987b1cd2..c0fc8c596 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -193,3 +193,14 @@ nl-ams1 Amsterdam, NL it-mil1 Milan, IT de-fra1 Frankfurt, DE ============== ================ + +ultimum +------- + +https://console.ultimum-cloud.com:5000/v2.0 + +============== ================ +Region Name Human Name +============== ================ +RegionOne Region One +============== ================ diff --git a/os_client_config/vendors/ultimum.yaml b/os_client_config/vendors/ultimum.yaml new file mode 100644 index 000000000..866117491 --- /dev/null +++ b/os_client_config/vendors/ultimum.yaml @@ -0,0 +1,5 @@ +name: ultimum +profile: + auth: + auth_url: https://console.ultimum-cloud.com:5000/v2.0 + region-name: RegionOne From b0f4161423d683aad2e5006ba2f9d80e858928ed Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 15 Sep 2015 14:00:25 +0200 Subject: [PATCH 0458/3836] Add Datacentred to the vendor list Change-Id: Iac2a51f2351e6b46469508abadd63942829663f0 --- doc/source/vendor-support.rst | 15 ++++++++++++++- os_client_config/vendors/datacentred.yaml | 6 ++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 os_client_config/vendors/datacentred.yaml diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index c0fc8c596..4d253547f 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -202,5 +202,18 @@ https://console.ultimum-cloud.com:5000/v2.0 ============== ================ Region Name Human Name ============== ================ -RegionOne Region One +RegionOne Region One +============== ================ + +datacentred +----------- + +https://compute.datacentred.io:5000/v2.0 + ============== ================ +Region Name Human Name +============== ================ +sal01 Manchester, UK +============== ================ + +* Image API Version is 1 diff --git a/os_client_config/vendors/datacentred.yaml b/os_client_config/vendors/datacentred.yaml new file mode 100644 index 000000000..5c0a5ed1a --- /dev/null +++ b/os_client_config/vendors/datacentred.yaml @@ -0,0 +1,6 @@ +name: datacentred +profile: + auth: + auth_url: https://compute.datacentred.io:5000/v2.0 + region-name: sal01 + image_api_version: '1' From 3f76cc5fa9d288b25b3804966568b8bd900a54c1 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 15 Sep 2015 14:02:01 +0200 Subject: [PATCH 0459/3836] Remove an extra line Change-Id: I3a7f31a205b9a49e8c93f71e2a9dacacee6e2f91 --- os_client_config/vendors/auro.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/os_client_config/vendors/auro.yaml b/os_client_config/vendors/auro.yaml index c19a6571a..ad721e62d 100644 --- a/os_client_config/vendors/auro.yaml +++ b/os_client_config/vendors/auro.yaml @@ -3,6 +3,5 @@ profile: auth: auth_url: https://api.van1.auro.io:5000/v2.0 region_name: van1 - image_api_version: '2' secgroup_source: nova floating_ip_source: nova From 093d7b085a5743d21f74da532eb45a916830022c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 7 Sep 2015 20:26:24 -0500 Subject: [PATCH 0460/3836] Defer plugin validation to keystoneauth keystoneauth plugin loading has parameter validation itself. Rather than us doing it, let ksa do it. This bubbles up a ksa exception- but I think I'm ok with that as an interface. Change-Id: I3e7741a1b623b133f24f321e97539883dc6cd153 --- os_client_config/cloud_config.py | 18 ++------ os_client_config/config.py | 63 +++++++++++++-------------- os_client_config/tests/base.py | 6 +++ os_client_config/tests/test_config.py | 24 +++++----- 4 files changed, 54 insertions(+), 57 deletions(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index c9f9f0756..86b4f5073 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -14,15 +14,15 @@ import warnings -from keystoneauth1 import loading - class CloudConfig(object): - def __init__(self, name, region, config, prefer_ipv6=False): + def __init__(self, name, region, config, + prefer_ipv6=False, auth_plugin=None): self.name = name self.region = region self.config = config self._prefer_ipv6 = prefer_ipv6 + self._auth = auth_plugin def __getattr__(self, key): """Return arbitrary attributes.""" @@ -111,14 +111,4 @@ def prefer_ipv6(self): def get_auth(self): """Return a keystoneauth plugin from the auth credentials.""" - # Re-use the admin_token plugin for the "None" plugin - # since it does not look up endpoints or tokens but rather - # does a passthrough. This is useful for things like Ironic - # that have a keystoneless operational mode, but means we're - # still dealing with a keystoneauth Session object, so all the - # _other_ things (SSL arg handling, timeout) all work consistently - if self.config['auth_type'] in (None, "None", ''): - self.config['auth_type'] = 'admin_token' - self.config['auth']['token'] = None - loader = loading.get_plugin_loader(self.config['auth_type']) - return loader.load_from_options(**self.config['auth']) + return self._auth diff --git a/os_client_config/config.py b/os_client_config/config.py index d6983781f..5da7799dd 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -17,13 +17,9 @@ import warnings import appdirs +from keystoneauth1 import loading import yaml -try: - import keystoneclient.auth as ksc_auth -except ImportError: - ksc_auth = None - from os_client_config import cloud_config from os_client_config import defaults from os_client_config import exceptions @@ -376,19 +372,27 @@ def _find_winning_auth_value(self, opt, config): if opt_name in config: return config[opt_name] else: - for d_opt in opt.deprecated_opts: + for d_opt in opt.deprecated: d_opt_name = d_opt.name.replace('-', '_') if d_opt_name in config: return config[d_opt_name] - def _validate_auth(self, config): + def _get_auth_loader(self, config): + # Re-use the admin_token plugin for the "None" plugin + # since it does not look up endpoints or tokens but rather + # does a passthrough. This is useful for things like Ironic + # that have a keystoneless operational mode, but means we're + # still dealing with a keystoneauth Session object, so all the + # _other_ things (SSL arg handling, timeout) all work consistently + if config['auth_type'] in (None, "None", ''): + config['auth_type'] = 'admin_token' + config['auth']['token'] = None + return loading.get_plugin_loader(config['auth_type']) + + def _validate_auth(self, config, loader): # May throw a keystoneclient.exceptions.NoMatchingPlugin - if config['auth_type'] == 'admin_endpoint': - auth_plugin = ksc_auth.token_endpoint.Token - else: - auth_plugin = ksc_auth.get_plugin_class(config['auth_type']) - plugin_options = auth_plugin.get_options() + plugin_options = loader.get_options() for p_opt in plugin_options: # if it's in config.auth, win, kill it from config dict @@ -400,19 +404,8 @@ def _validate_auth(self, config): if not winning_value: winning_value = self._find_winning_auth_value(p_opt, config) - # 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: - raise exceptions.OpenStackConfigException( - 'Unable to find auth information for cloud' - ' {cloud} in config files {files}' - ' or environment variables. Missing value {auth_key}' - ' required for auth plugin {plugin}'.format( - cloud=cloud, files=','.join(self._config_files), - 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_opts]: + 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) @@ -435,13 +428,16 @@ def get_one_cloud(self, cloud=None, validate=True, :param string cloud: The name of the configuration to load from clouds.yaml :param boolean validate: - Validate that required arguments are present and certain - argument combinations are valid + Validate the config. Setting this to False causes no auth plugin + to be created. It's really only useful for testing. :param Namespace argparse: An argparse Namespace object; allows direct passing in of argparse options to be added to the cloud config. Values of None and '' will be removed. :param kwargs: Additional configuration options + + :raises: keystoneauth1.exceptions.MissingRequiredOptions + on missing required auth parameters """ if cloud is None and self.envvar_key in self.get_cloud_names(): @@ -471,12 +467,12 @@ def get_one_cloud(self, cloud=None, validate=True, if type(config[key]) is not bool: config[key] = get_boolean(config[key]) - if 'auth_type' in config: - if config['auth_type'] in ('', 'None', None): - validate = False - - if validate and ksc_auth: - config = self._validate_auth(config) + loader = self._get_auth_loader(config) + if validate: + config = self._validate_auth(config, loader) + auth_plugin = loader.load_from_options(**config['auth']) + else: + auth_plugin = None # If any of the defaults reference other values, we need to expand for (key, value) in config.items(): @@ -492,7 +488,8 @@ def get_one_cloud(self, cloud=None, validate=True, return cloud_config.CloudConfig( name=cloud_name, region=config['region_name'], config=self._normalize_keys(config), - prefer_ipv6=prefer_ipv6) + prefer_ipv6=prefer_ipv6, + auth_plugin=auth_plugin) @staticmethod def set_one_cloud(config_file, cloud, set_config=None): diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py index 89e04c0ae..9a2923792 100644 --- a/os_client_config/tests/base.py +++ b/os_client_config/tests/base.py @@ -31,6 +31,7 @@ 'public-clouds': { '_test_cloud_in_our_cloud': { 'auth': { + 'auth_url': 'http://example.com/v2', 'username': 'testotheruser', 'project_name': 'testproject', }, @@ -45,6 +46,7 @@ '_test-cloud_': { 'profile': '_test_cloud_in_our_cloud', 'auth': { + 'auth_url': 'http://example.com/v2', 'username': 'testuser', 'password': 'testpass', }, @@ -53,6 +55,7 @@ '_test_cloud_no_vendor': { 'profile': '_test_non_existant_cloud', 'auth': { + 'auth_url': 'http://example.com/v2', 'username': 'testuser', 'password': 'testpass', 'project_name': 'testproject', @@ -64,6 +67,7 @@ 'username': 'testuser', 'password': 'testpass', 'project_id': 12345, + 'auth_url': 'http://example.com/v2', }, 'region_name': 'test-region', }, @@ -72,6 +76,7 @@ 'username': 'testuser', 'password': 'testpass', 'project-id': 'testproject', + 'auth_url': 'http://example.com/v2', }, 'regions': [ 'region1', @@ -83,6 +88,7 @@ 'username': 'testuser', 'password': 'testpass', 'project-id': '12345', + 'auth_url': 'http://example.com/v2', }, 'region_name': 'test-region', } diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 332e4d323..82f2fb9d8 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -43,7 +43,7 @@ def test_get_all_clouds(self): def test_get_one_cloud(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cloud = c.get_one_cloud() + cloud = c.get_one_cloud(validate=False) self.assertIsInstance(cloud, cloud_config.CloudConfig) self.assertEqual(cloud.name, '') @@ -61,12 +61,12 @@ def test_get_one_cloud_auth_defaults(self): ) def test_get_one_cloud_auth_override_defaults(self): - default_options = {'auth_type': 'token'} + default_options = {'compute_api_version': '4'} c = config.OpenStackConfig(config_files=[self.cloud_yaml], override_defaults=default_options) cc = c.get_one_cloud(cloud='_test-cloud_', auth={'username': 'user'}) self.assertEqual('user', cc.auth['username']) - self.assertEqual('token', cc.auth_type) + self.assertEqual('4', cc.compute_api_version) self.assertEqual( defaults._defaults['identity_api_version'], cc.identity_api_version, @@ -109,7 +109,7 @@ def test_fallthrough(self): for k in os.environ.keys(): if k.startswith('OS_'): self.useFixture(fixtures.EnvironmentVariable(k)) - c.get_one_cloud(cloud='defaults') + c.get_one_cloud(cloud='defaults', validate=False) def test_prefer_ipv6_true(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], @@ -120,7 +120,7 @@ def test_prefer_ipv6_true(self): def test_prefer_ipv6_false(self): c = config.OpenStackConfig(config_files=[self.no_yaml], vendor_files=[self.no_yaml]) - cc = c.get_one_cloud(cloud='defaults') + cc = c.get_one_cloud(cloud='defaults', validate=False) self.assertFalse(cc.prefer_ipv6) def test_get_one_cloud_auth_merge(self): @@ -144,7 +144,7 @@ def test_get_cloud_names(self): for k in os.environ.keys(): if k.startswith('OS_'): self.useFixture(fixtures.EnvironmentVariable(k)) - c.get_one_cloud(cloud='defaults') + c.get_one_cloud(cloud='defaults', validate=False) self.assertEqual(['defaults'], sorted(c.get_cloud_names())) def test_set_one_cloud_creates_file(self): @@ -168,7 +168,8 @@ def test_set_one_cloud_updates_cloud(self): resulting_cloud_config = { 'auth': { 'password': 'newpass', - 'username': 'testuser' + 'username': 'testuser', + 'auth_url': 'http://example.com/v2', }, 'cloud': 'new_cloud', 'profile': '_test_cloud_in_our_cloud', @@ -189,6 +190,10 @@ def setUp(self): super(TestConfigArgparse, self).setUp() self.options = argparse.Namespace( + auth_url='http://example.com/v2', + username='user', + password='password', + project_name='project', region_name='other-test-region', snack_type='cookie', ) @@ -208,7 +213,6 @@ def test_get_one_cloud_just_argparse(self): cc = c.get_one_cloud(cloud='', argparse=self.options) self.assertIsNone(cc.cloud) - self.assertNotIn('username', cc.auth) self.assertEqual(cc.region_name, 'other-test-region') self.assertEqual(cc.snack_type, 'cookie') @@ -270,11 +274,11 @@ def test_set_no_default(self): self.assertEqual('password', cc.auth_type) def test_set_default_before_init(self): - config.set_default('auth_type', 'token') + config.set_default('identity_api_version', '4') c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) cc = c.get_one_cloud(cloud='_test-cloud_', argparse=None) - self.assertEqual('token', cc.auth_type) + self.assertEqual('4', cc.identity_api_version) class TestBackwardsCompatibility(base.TestCase): From 2be0553eb0d168ea0dc57e098c8dcc86566b6622 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 16 Sep 2015 21:54:18 +0200 Subject: [PATCH 0461/3836] Fix typo in comment - we use ksa not ksc Change-Id: I9f35c26fc633b07442141443574ea9b7582036be --- os_client_config/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 5da7799dd..ec35548f6 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -390,7 +390,7 @@ def _get_auth_loader(self, config): return loading.get_plugin_loader(config['auth_type']) def _validate_auth(self, config, loader): - # May throw a keystoneclient.exceptions.NoMatchingPlugin + # May throw a keystoneauth1.exceptions.NoMatchingPlugin plugin_options = loader.get_options() From 54afb28565cb1b24973a403f365081487a2484ec Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 16 Sep 2015 21:18:19 +0200 Subject: [PATCH 0462/3836] Add internap to the vendor list Change-Id: I4c98b72f039fd97a2c55ecbfa546fb908d1fe539 --- doc/source/vendor-support.rst | 16 ++++++++++++++++ os_client_config/vendors/internap.yaml | 10 ++++++++++ 2 files changed, 26 insertions(+) create mode 100644 os_client_config/vendors/internap.yaml diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index 4d253547f..c4f5a4fe4 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -217,3 +217,19 @@ sal01 Manchester, UK ============== ================ * Image API Version is 1 + +internap +-------- + +https://identity.api.cloud.iweb.com/v2.0 + +============== ================ +Region Name Human Name +============== ================ +ams01 Amsterdam, NL +da01 Dallas, TX +nyj01 New York, NY +============== ================ + +* Image API Version is 1 +* Floating IPs are not supported diff --git a/os_client_config/vendors/internap.yaml b/os_client_config/vendors/internap.yaml new file mode 100644 index 000000000..48cd960eb --- /dev/null +++ b/os_client_config/vendors/internap.yaml @@ -0,0 +1,10 @@ +name: internap +profile: + auth: + auth_url: https://identity.api.cloud.iweb.com/v2.0 + regions: + - ams01 + - da01 + - nyj01 + image_api_version: '1' + floating_ip_source: None From aef90e7ec82326e63136be3ace1fa0a0590ee325 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 16 Sep 2015 12:30:12 +0200 Subject: [PATCH 0463/3836] Allow configuring domain id once In the most common case, a use has one and only one domain id that they care about, and it's associated with their user and project. Instead of making them set it for both user_domain_id and project_domain_id, allow for domain_{id, name} and then fill in any missing values for {user,project}_domain_{id,name} with the given value. This is mainly because I wind up with config files looking like this: user_domain_id: d0919bd5e8d74e49adf0e145807ffc38 project_domain_id: d0919bd5e8d74e49adf0e145807ffc38 Which offends my tender sensibilities. Change-Id: I12342dfa9f1b539a3fea5dd8874c42d027c59739 --- os_client_config/config.py | 23 +++++++++++++++++++++++ os_client_config/tests/base.py | 11 +++++++++++ os_client_config/tests/test_config.py | 12 +++++++++++- 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index ec35548f6..e8e76a54c 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -279,15 +279,38 @@ def _fix_backwards_madness(self, cloud): cloud = self._fix_backwards_project(cloud) cloud = self._fix_backwards_auth_plugin(cloud) cloud = self._fix_backwards_interface(cloud) + cloud = self._handle_domain_id(cloud) + return cloud + + def _handle_domain_id(self, cloud): + # Allow people to just specify domain once if it's the same + mappings = { + 'domain_id': ('user_domain_id', 'project_domain_id'), + 'domain_name': ('user_domain_name', 'project_domain_name'), + } + for target_key, possible_values in mappings.items(): + for key in possible_values: + if target_key in cloud['auth'] and key not in cloud['auth']: + cloud['auth'][key] = cloud['auth'][target_key] + cloud['auth'].pop(target_key, None) return cloud def _fix_backwards_project(self, cloud): # Do the lists backwards so that project_name is the ultimate winner + # Also handle moving domain names into auth so that domain mapping + # is easier mappings = { 'project_id': ('tenant_id', 'tenant-id', 'project_id', 'project-id'), 'project_name': ('tenant_name', 'tenant-name', 'project_name', 'project-name'), + 'domain_id': ('domain_id', 'domain-id'), + 'domain_name': ('domain_name', 'domain-name'), + 'user_domain_id': ('user_domain_id', 'user-domain-id'), + 'user_domain_name': ('user_domain_name', 'user-domain-name'), + 'project_domain_id': ('project_domain_id', 'project-domain-id'), + 'project_domain_name': ( + 'project_domain_name', 'project-domain-name'), } for target_key, possible_values in mappings.items(): target = None diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py index 9a2923792..cbf58da36 100644 --- a/os_client_config/tests/base.py +++ b/os_client_config/tests/base.py @@ -71,6 +71,17 @@ }, 'region_name': 'test-region', }, + '_test-cloud-domain-id_': { + 'auth': { + 'username': 'testuser', + 'password': 'testpass', + 'project_id': 12345, + 'auth_url': 'http://example.com/v2', + 'domain_id': '6789', + 'project_domain_id': '123456789', + }, + 'region_name': 'test-region', + }, '_test_cloud_regions': { 'auth': { 'username': 'testuser', diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 82f2fb9d8..36fe15de1 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -91,6 +91,15 @@ def test_get_one_cloud_with_int_project_id(self): cc = c.get_one_cloud('_test-cloud-int-project_') self.assertEqual('12345', cc.auth['project_id']) + def test_get_one_cloud_with_domain_id(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + cc = c.get_one_cloud('_test-cloud-domain-id_') + self.assertEqual('6789', cc.auth['user_domain_id']) + self.assertEqual('123456789', cc.auth['project_domain_id']) + self.assertNotIn('domain_id', cc.auth) + self.assertNotIn('domain-id', cc.auth) + def test_get_one_cloud_with_hyphenated_project_id(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) @@ -132,7 +141,8 @@ def test_get_one_cloud_auth_merge(self): def test_get_cloud_names(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml]) self.assertEqual( - ['_test-cloud-int-project_', + ['_test-cloud-domain-id_', + '_test-cloud-int-project_', '_test-cloud_', '_test_cloud_hyphenated', '_test_cloud_no_vendor', From aabf1431a3e23a734431f56df4a8d6ac446509b3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 17 Sep 2015 01:22:24 +0200 Subject: [PATCH 0464/3836] Test kwargs passing not just argparse Change-Id: Ic14365b1daec9a3f51d6a59db38604edb60865d4 --- os_client_config/tests/test_config.py | 31 +++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 36fe15de1..b4320ad6b 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -199,7 +199,7 @@ class TestConfigArgparse(base.TestCase): def setUp(self): super(TestConfigArgparse, self).setUp() - self.options = argparse.Namespace( + self.args = dict( auth_url='http://example.com/v2', username='user', password='password', @@ -207,6 +207,7 @@ def setUp(self): region_name='other-test-region', snack_type='cookie', ) + self.options = argparse.Namespace(**self.args) def test_get_one_cloud_argparse(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], @@ -221,7 +222,33 @@ def test_get_one_cloud_just_argparse(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud(cloud='', argparse=self.options) + cc = c.get_one_cloud(argparse=self.options) + self.assertIsNone(cc.cloud) + self.assertEqual(cc.region_name, 'other-test-region') + self.assertEqual(cc.snack_type, 'cookie') + + def test_get_one_cloud_just_kwargs(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + + cc = c.get_one_cloud(**self.args) + self.assertIsNone(cc.cloud) + self.assertEqual(cc.region_name, 'other-test-region') + self.assertEqual(cc.snack_type, 'cookie') + + def test_get_one_cloud_dash_kwargs(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + + args = { + 'auth-url': 'http://example.com/v2', + 'username': 'user', + 'password': 'password', + 'project_name': 'project', + 'region_name': 'other-test-region', + 'snack_type': 'cookie', + } + cc = c.get_one_cloud(**args) self.assertIsNone(cc.cloud) self.assertEqual(cc.region_name, 'other-test-region') self.assertEqual(cc.snack_type, 'cookie') From 2906d1e61fd6e2e1024996993e1c108966fc425f Mon Sep 17 00:00:00 2001 From: Clark Boylan Date: Wed, 16 Sep 2015 16:41:58 -0700 Subject: [PATCH 0465/3836] Properly handle os- prefixed args in fix_args Prior to this any arg that started with 'os' had the 'os' prefix chomped including the next character. This meant that any arg like 'osmosis' would've been replaced with 'osis' which is clearly wrong. Instead we want to treat the OpenStack prefix of 'os-' or 'os_' as the special thing so check the next character is correct before chomping. Change-Id: Id12a92adf63d896f7aa5c0e391abd299c4ce3331 --- os_client_config/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index e8e76a54c..65312054f 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -383,7 +383,7 @@ def _fix_args(self, args, argparse=None): new_args = dict() for (key, val) in iter(args.items()): key = key.replace('-', '_') - if key.startswith('os'): + if key.startswith('os_'): os_args[key[3:]] = val else: new_args[key] = val From 0339a9a7f8c004b7e6df5c3d9dc49a69fcc3b357 Mon Sep 17 00:00:00 2001 From: Gregory Haynes Date: Wed, 16 Sep 2015 17:22:39 -0700 Subject: [PATCH 0466/3836] Convert auth kwargs '-' to '_' When passing in kwargs to _get_one_cloud we do not dive into the auth dict to convert '-' to '_'. Change-Id: I8ce12370b5fd4444ba17d724e7f8036a7b0d2784 --- os_client_config/config.py | 5 +++++ os_client_config/tests/test_config.py | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/os_client_config/config.py b/os_client_config/config.py index e8e76a54c..100cf739e 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -382,6 +382,11 @@ def _fix_args(self, args, argparse=None): os_args = dict() new_args = dict() for (key, val) in iter(args.items()): + if type(args[key]) == dict: + # dive into the auth dict + new_args[key] = self._fix_args(args[key]) + continue + key = key.replace('-', '_') if key.startswith('os'): os_args[key[3:]] = val diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 36fe15de1..9c9451d36 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -106,6 +106,21 @@ def test_get_one_cloud_with_hyphenated_project_id(self): cc = c.get_one_cloud('_test_cloud_hyphenated') self.assertEqual('12345', cc.auth['project_id']) + def test_get_one_cloud_with_hyphenated_kwargs(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + args = { + 'auth': { + 'username': 'testuser', + 'password': 'testpass', + 'project-id': '12345', + 'auth-url': 'http://example.com/v2', + }, + 'region_name': 'test-region', + } + cc = c.get_one_cloud(**args) + self.assertEqual('http://example.com/v2', cc.auth['auth_url']) + def test_no_environ(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) From 21bb2f347d0d52defa98726b7102c1853b558072 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 18 Sep 2015 09:55:55 +0200 Subject: [PATCH 0467/3836] Move plugin loader creation to try block We only need the plugin loader if we're going to create an auth plugin. Doing this when validate=False is clearly not a workable solution, because we'll wind up validating unknown plugins. Change-Id: Ieed44aa3ef41a14edd7529ca599a01967d517207 --- os_client_config/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 387d01496..5dbe8db77 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -495,8 +495,8 @@ def get_one_cloud(self, cloud=None, validate=True, if type(config[key]) is not bool: config[key] = get_boolean(config[key]) - loader = self._get_auth_loader(config) if validate: + loader = self._get_auth_loader(config) config = self._validate_auth(config, loader) auth_plugin = loader.load_from_options(**config['auth']) else: From 6d25a939bfdfeea14e1038c240092ebaedf33145 Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Fri, 18 Sep 2015 13:29:12 -0700 Subject: [PATCH 0468/3836] Fix typo in ovh region names Change-Id: If8dee92917d1f5c1e349f02eba7661c39dbe7d0b --- doc/source/vendor-support.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index c4f5a4fe4..77d10ebba 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -145,8 +145,8 @@ https://auth.cloud.ovh.net/v2.0 ============== ================ Region Name Human Name ============== ================ -SBG-1 Strassbourg, FR -GRA-1 Gravelines, FR +SBG1 Strassbourg, FR +GRA1 Gravelines, FR ============== ================ * Images must be in `raw` format From aa41f9bfb10b6f75e1a3f7c599c8c06e7a03d389 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 18 Sep 2015 16:54:46 -0400 Subject: [PATCH 0469/3836] Fall back to keystoneclient arg processing For things not on keystoneauth yet, we need to move things into the auth dict using keystoneclient. Change-Id: Ia4500cc270b775f189048ccf667d5bfdc5dfcd14 --- os_client_config/config.py | 80 +++++++++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 6 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 5dbe8db77..96d5fa74c 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -17,7 +17,10 @@ import warnings import appdirs -from keystoneauth1 import loading +try: + from keystoneauth1 import loading +except ImportError: + loading = None import yaml from os_client_config import cloud_config @@ -400,7 +403,9 @@ def _find_winning_auth_value(self, opt, config): if opt_name in config: return config[opt_name] else: - for d_opt in opt.deprecated: + deprecated = getattr(opt, 'deprecated', getattr( + opt, 'deprecated_opts')) + for d_opt in deprecated: d_opt_name = d_opt.name.replace('-', '_') if d_opt_name in config: return config[d_opt_name] @@ -417,6 +422,54 @@ def _get_auth_loader(self, config): config['auth']['token'] = None return loading.get_plugin_loader(config['auth_type']) + def _validate_auth_ksc(self, config): + try: + import keystoneclient.auth as ksc_auth + except ImportError: + return config + + # May throw a keystoneclient.exceptions.NoMatchingPlugin + plugin_options = ksc_auth.get_plugin_class( + config['auth_type']).get_options() + + for p_opt in plugin_options: + # if it's in config.auth, win, kill it from config dict + # if it's in config and not in config.auth, move it + # deprecated loses to current + # provided beats default, deprecated or not + winning_value = self._find_winning_auth_value( + p_opt, config['auth']) + if not winning_value: + winning_value = self._find_winning_auth_value(p_opt, config) + + # 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: + raise exceptions.OpenStackConfigException( + 'Unable to find auth information for cloud' + ' {cloud} in config files {files}' + ' or environment variables. Missing value {auth_key}' + ' required for auth plugin {plugin}'.format( + cloud=cloud, files=','.join(self._config_files), + 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_opts]: + 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 + + return config + def _validate_auth(self, config, loader): # May throw a keystoneauth1.exceptions.NoMatchingPlugin @@ -495,12 +548,27 @@ def get_one_cloud(self, cloud=None, validate=True, if type(config[key]) is not bool: config[key] = get_boolean(config[key]) - if validate: - loader = self._get_auth_loader(config) - config = self._validate_auth(config, loader) - auth_plugin = loader.load_from_options(**config['auth']) + if loading: + if validate: + try: + loader = self._get_auth_loader(config) + config = self._validate_auth(config, loader) + auth_plugin = loader.load_from_options(**config['auth']) + except Exception as e: + # We WANT the ksa exception normally + # but OSC can't handle it right now, so we try deferring + # to ksc. If that ALSO fails, it means there is likely + # a deeper issue, so we assume the ksa error was correct + auth_plugin = None + try: + config = self._validate_auth_ksc(config) + except Exception: + raise e + else: + auth_plugin = None else: auth_plugin = None + config = self._validate_auth_ksc(config) # If any of the defaults reference other values, we need to expand for (key, value) in config.items(): From 8dee656df809ed1b39b2800e35cc5ef67c31e84e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 20 Sep 2015 20:08:39 -0400 Subject: [PATCH 0470/3836] Handle ksa opt with no deprecated field Bad logic fallthrough causes us to die when trying to process an option that does not have a deprecated option. Change-Id: I613466c6146a94b66a0a6d9955cdc4a6556f44ed --- os_client_config/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 96d5fa74c..1b6193e46 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -404,7 +404,7 @@ def _find_winning_auth_value(self, opt, config): return config[opt_name] else: deprecated = getattr(opt, 'deprecated', getattr( - opt, 'deprecated_opts')) + opt, 'deprecated_opts', [])) for d_opt in deprecated: d_opt_name = d_opt.name.replace('-', '_') if d_opt_name in config: From 7f33416020801dfc8a444a80705ef2f491a4d066 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 21 Sep 2015 14:41:32 +0000 Subject: [PATCH 0471/3836] Change ignore-errors to ignore_errors Needed for coverage 4.0 Change-Id: I033aaed4afa9037017190bc0b5aba7216840627d --- .coveragerc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 5f0c7fd8b..3c1292222 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,4 +4,4 @@ source = os_client_config omit = os_client_config/tests/*,os_client_config/openstack/* [report] -ignore-errors = True \ No newline at end of file +ignore_errors = True From d084ebd643fb22899ee789e099db69f4f182bde7 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 14 May 2015 15:04:24 -0400 Subject: [PATCH 0472/3836] Start using keystoneauth for keystone sessions This is being split out from keystoneclient. That's a happymaking. In order to make this work, we also need to fix glance image data upload for v2 put. It seems that the internal interface has changed. Pass the file object and not a size. We could do the work to figure out how to do it in two discreet steps, but I believe that would involved a chunk of engineering on a dead interface and is not worth it. Also, Change ignore-errors to ignore_errors Change-Id: I816752f8f4d29e40d41622bd8a271eb5c8e5a9fb --- .coveragerc | 2 +- requirements.txt | 3 +- shade/__init__.py | 46 +++++++++----------- shade/tests/unit/test_caching.py | 9 +++- shade/tests/unit/test_create_server.py | 3 +- shade/tests/unit/test_delete_server.py | 3 +- shade/tests/unit/test_domain_params.py | 2 +- shade/tests/unit/test_endpoints.py | 3 +- shade/tests/unit/test_flavors.py | 2 +- shade/tests/unit/test_floating_ip_common.py | 3 +- shade/tests/unit/test_floating_ip_neutron.py | 3 +- shade/tests/unit/test_floating_ip_nova.py | 3 +- shade/tests/unit/test_floating_ip_pool.py | 3 +- shade/tests/unit/test_identity_domains.py | 2 +- shade/tests/unit/test_keypair.py | 2 +- shade/tests/unit/test_meta.py | 14 +++--- shade/tests/unit/test_object.py | 3 +- shade/tests/unit/test_operator_noauth.py | 10 +++-- shade/tests/unit/test_port.py | 3 +- shade/tests/unit/test_rebuild_server.py | 3 +- shade/tests/unit/test_security_groups.py | 2 +- shade/tests/unit/test_services.py | 3 +- shade/tests/unit/test_shade.py | 25 +---------- shade/tests/unit/test_shade_operator.py | 6 +-- 24 files changed, 78 insertions(+), 80 deletions(-) diff --git a/.coveragerc b/.coveragerc index 078cf7252..ca361e59b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,4 +4,4 @@ source = shade omit = shade/tests/* [report] -ignore-errors = True +ignore_errors = True diff --git a/requirements.txt b/requirements.txt index ee26521a2..c5fab814e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,9 +3,10 @@ pbr>=0.11,<2.0 bunch decorator jsonpatch -os-client-config>=1.6.2 +os-client-config>=1.7.4 six +keystoneauth1>=1.0.0 python-novaclient>=2.21.0,!=2.27.0 python-keystoneclient>=0.11.0 python-glanceclient>=1.0.0 diff --git a/shade/__init__.py b/shade/__init__.py index c8460cecf..6e0a7c4fe 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -17,7 +17,6 @@ import inspect import logging import operator -import os from cinderclient.v1 import client as cinder_client from designateclient.v1 import Client as designate_client @@ -28,12 +27,13 @@ from ironicclient import client as ironic_client from ironicclient import exceptions as ironic_exceptions import jsonpatch -from keystoneclient import auth as ksc_auth -from keystoneclient.auth import token_endpoint +import keystoneauth1.exceptions +from keystoneauth1 import loading +from keystoneauth1 import plugin as ksc_plugin +from keystoneauth1 import session as ksc_session from keystoneclient.v2_0 import client as k2_client from keystoneclient.v3 import client as k3_client from keystoneclient import exceptions as keystone_exceptions -from keystoneclient import session as ksc_session from novaclient import client as nova_client from novaclient import exceptions as nova_exceptions from neutronclient.common import exceptions as neutron_exceptions @@ -229,6 +229,9 @@ def __init__( self.auth = cloud_config.get_auth_args() self.region_name = cloud_config.region_name self.auth_type = cloud_config.config['auth_type'] + # provide backwards compat to the old name of this plugin + if self.auth_type == 'token_endpoint': + self.auth_type = 'admin_token' self.default_interface = cloud_config.get_interface() self.private = cloud_config.config.get('private', False) self.api_timeout = cloud_config.config['api_timeout'] @@ -344,18 +347,6 @@ def nova_client(self): 'compute', nova_client.Client) return self._nova_client - def _get_auth_plugin_class(self): - try: - if self.auth_type == 'token_endpoint': - return token_endpoint.Token - else: - return ksc_auth.get_plugin_class(self.auth_type) - except Exception as e: - self.log.debug("keystone auth plugin failure", exc_info=True) - raise OpenStackCloudException( - "Could not find auth plugin: {plugin} {error}".format( - plugin=self.auth_type, error=str(e))) - def _get_identity_client_class(self): if self.cloud_config.get_api_version('identity') == '3': return k3_client.Client @@ -369,9 +360,18 @@ def _get_identity_client_class(self): def keystone_session(self): if self._keystone_session is None: - auth_plugin = self._get_auth_plugin_class() try: - keystone_auth = auth_plugin(**self.auth) + loader = loading.get_plugin_loader(self.auth_type) + except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin: + self.log.debug( + "keystoneauth could not find auth plugin {plugin}".format( + plugin=self.auth_type), exc_info=True) + raise OpenStackCloudException( + "No auth plugin named {plugin}".format( + plugin=self.auth_type)) + + try: + keystone_auth = loader.plugin_class(**self.auth) except Exception as e: self.log.debug( "keystone couldn't construct plugin", exc_info=True) @@ -401,7 +401,7 @@ def keystone_client(self): @property def service_catalog(self): return self.keystone_session.auth.get_access( - self.keystone_session).service_catalog.get_data() + self.keystone_session).service_catalog.catalog @property def auth_token(self): @@ -732,7 +732,7 @@ def get_session_endpoint(self, service_key): # keystone is a special case in keystone, because what? if service_key == 'identity': endpoint = self.keystone_session.get_endpoint( - interface=ksc_auth.AUTH_INTERFACE) + interface=ksc_plugin.AUTH_INTERFACE) else: endpoint = self.keystone_session.get_endpoint( service_type=self.cloud_config.get_service_type( @@ -1382,12 +1382,8 @@ def _upload_image_put_v2(self, name, image_data, **image_kwargs): image_kwargs[k] = str(v) image = self.manager.submitTask(_tasks.ImageCreate( name=name, **image_kwargs)) - curr = image_data.tell() - image_data.seek(0, os.SEEK_END) - data_size = image_data.tell() - image_data.seek(curr) self.manager.submitTask(_tasks.ImageUpload( - image_id=image.id, image_data=image_data, image_size=data_size)) + image_id=image.id, image_data=image_data)) return image def _upload_image_put_v1(self, name, image_data, **image_kwargs): diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 9c2d662c3..7d8c33144 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -15,9 +15,11 @@ import mock import os_client_config as occ +import testtools import yaml import shade +from shade import exc from shade import meta from shade.tests import fakes from shade.tests.unit import base @@ -299,7 +301,7 @@ def test_create_image_put_v2(self, glance_mock, mock_api_version): 'owner_specified.shade.sha256': mock.ANY} glance_mock.images.create.assert_called_with(**args) glance_mock.images.upload.assert_called_with( - image_data=mock.ANY, image_id=fake_image.id, image_size=1) + image_data=mock.ANY, image_id=fake_image.id) fake_image_dict = meta.obj_to_dict(fake_image) self.assertEqual([fake_image_dict], self.cloud.list_images()) @@ -399,3 +401,8 @@ class FakeImage(dict): self.cloud.list_images.invalidate(self.cloud) self.assertEqual( [fi, fi2], [dict(x) for x in self.cloud.list_images()]) + + def test_get_auth_bogus(self): + self.cloud.auth_type = 'bogus' + with testtools.ExpectedException(exc.OpenStackCloudException): + self.cloud.keystone_session diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index 5a1ea3209..63a3b4db5 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -32,7 +32,8 @@ class TestCreateServer(base.TestCase): def setUp(self): super(TestCreateServer, self).setUp() config = os_client_config.OpenStackConfig() - self.client = OpenStackCloud(cloud_config=config.get_one_cloud()) + self.client = OpenStackCloud( + cloud_config=config.get_one_cloud(validate=False)) def test_create_server_with_create_exception(self): """ diff --git a/shade/tests/unit/test_delete_server.py b/shade/tests/unit/test_delete_server.py index f2187481d..36efa8fda 100644 --- a/shade/tests/unit/test_delete_server.py +++ b/shade/tests/unit/test_delete_server.py @@ -42,7 +42,8 @@ class TestDeleteServer(base.TestCase): def setUp(self): super(TestDeleteServer, self).setUp() config = os_client_config.OpenStackConfig() - self.cloud = OpenStackCloud(cloud_config=config.get_one_cloud()) + self.cloud = OpenStackCloud( + cloud_config=config.get_one_cloud(validate=False)) @mock.patch('shade.OpenStackCloud.nova_client') def test_delete_server(self, nova_mock): diff --git a/shade/tests/unit/test_domain_params.py b/shade/tests/unit/test_domain_params.py index 79a37764c..ea6d49375 100644 --- a/shade/tests/unit/test_domain_params.py +++ b/shade/tests/unit/test_domain_params.py @@ -26,7 +26,7 @@ class TestDomainParams(base.TestCase): def setUp(self): super(TestDomainParams, self).setUp() - self.cloud = shade.openstack_cloud() + self.cloud = shade.openstack_cloud(validate=False) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, '_get_project') diff --git a/shade/tests/unit/test_endpoints.py b/shade/tests/unit/test_endpoints.py index 6b03f1e89..a7385abc5 100644 --- a/shade/tests/unit/test_endpoints.py +++ b/shade/tests/unit/test_endpoints.py @@ -40,7 +40,8 @@ class TestCloudEndpoints(base.TestCase): def setUp(self): super(TestCloudEndpoints, self).setUp() config = os_client_config.OpenStackConfig() - self.client = OperatorCloud(cloud_config=config.get_one_cloud()) + self.client = OperatorCloud( + cloud_config=config.get_one_cloud(validate=False)) self.mock_ks_endpoints = \ [FakeEndpoint(**kwa) for kwa in self.mock_endpoints] diff --git a/shade/tests/unit/test_flavors.py b/shade/tests/unit/test_flavors.py index ef726a139..c44b29717 100644 --- a/shade/tests/unit/test_flavors.py +++ b/shade/tests/unit/test_flavors.py @@ -24,7 +24,7 @@ class TestFlavors(base.TestCase): def setUp(self): super(TestFlavors, self).setUp() - self.op_cloud = shade.operator_cloud() + self.op_cloud = shade.operator_cloud(validate=False) @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_create_flavor(self, mock_nova): diff --git a/shade/tests/unit/test_floating_ip_common.py b/shade/tests/unit/test_floating_ip_common.py index 4c3693f79..82bb9d5f2 100644 --- a/shade/tests/unit/test_floating_ip_common.py +++ b/shade/tests/unit/test_floating_ip_common.py @@ -31,7 +31,8 @@ class TestFloatingIP(base.TestCase): def setUp(self): super(TestFloatingIP, self).setUp() config = os_client_config.OpenStackConfig() - self.client = OpenStackCloud(cloud_config=config.get_one_cloud()) + self.client = OpenStackCloud( + cloud_config=config.get_one_cloud(validate=False)) @patch.object(OpenStackCloud, 'get_floating_ip') @patch.object(OpenStackCloud, 'attach_ip_to_server') diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 115a93caf..7dfd3dfff 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -123,7 +123,8 @@ def setUp(self): super(TestFloatingIP, self).setUp() # floating_ip_source='neutron' is default for OpenStackCloud() config = os_client_config.OpenStackConfig() - self.client = OpenStackCloud(cloud_config=config.get_one_cloud()) + self.client = OpenStackCloud( + cloud_config=config.get_one_cloud(validate=False)) @patch.object(OpenStackCloud, 'neutron_client') @patch.object(OpenStackCloud, 'has_service') diff --git a/shade/tests/unit/test_floating_ip_nova.py b/shade/tests/unit/test_floating_ip_nova.py index 061bf67c4..d7179156a 100644 --- a/shade/tests/unit/test_floating_ip_nova.py +++ b/shade/tests/unit/test_floating_ip_nova.py @@ -71,7 +71,8 @@ def assertAreInstances(self, elements, elem_type): def setUp(self): super(TestFloatingIP, self).setUp() config = os_client_config.OpenStackConfig() - self.client = OpenStackCloud(cloud_config=config.get_one_cloud()) + self.client = OpenStackCloud( + cloud_config=config.get_one_cloud(validate=False)) @patch.object(OpenStackCloud, 'nova_client') @patch.object(OpenStackCloud, 'has_service') diff --git a/shade/tests/unit/test_floating_ip_pool.py b/shade/tests/unit/test_floating_ip_pool.py index 7443aa7ad..b3ca44f21 100644 --- a/shade/tests/unit/test_floating_ip_pool.py +++ b/shade/tests/unit/test_floating_ip_pool.py @@ -35,7 +35,8 @@ class TestFloatingIPPool(base.TestCase): def setUp(self): super(TestFloatingIPPool, self).setUp() config = os_client_config.OpenStackConfig() - self.client = OpenStackCloud(cloud_config=config.get_one_cloud()) + self.client = OpenStackCloud( + cloud_config=config.get_one_cloud(validate=False)) @patch.object(OpenStackCloud, '_has_nova_extension') @patch.object(OpenStackCloud, 'nova_client') diff --git a/shade/tests/unit/test_identity_domains.py b/shade/tests/unit/test_identity_domains.py index 83b53a489..1ccef8178 100644 --- a/shade/tests/unit/test_identity_domains.py +++ b/shade/tests/unit/test_identity_domains.py @@ -32,7 +32,7 @@ class TestIdentityDomains(base.TestCase): def setUp(self): super(TestIdentityDomains, self).setUp() - self.cloud = shade.operator_cloud() + self.cloud = shade.operator_cloud(validate=False) @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_list_identity_domains(self, mock_keystone): diff --git a/shade/tests/unit/test_keypair.py b/shade/tests/unit/test_keypair.py index 2a4f151e9..86a7d398c 100644 --- a/shade/tests/unit/test_keypair.py +++ b/shade/tests/unit/test_keypair.py @@ -27,7 +27,7 @@ class TestKeypair(base.TestCase): def setUp(self): super(TestKeypair, self).setUp() - self.cloud = shade.openstack_cloud() + self.cloud = shade.openstack_cloud(validate=False) @patch.object(shade.OpenStackCloud, 'nova_client') def test_create_keypair(self, mock_nova): diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index c9d8b7dff..301a5c252 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -105,7 +105,7 @@ def test_find_nova_addresses_all(self): def test_get_server_ip(self): srv = meta.obj_to_dict(FakeServer()) - cloud = shade.openstack_cloud() + cloud = shade.openstack_cloud(validate=False) self.assertEqual(PRIVATE_V4, meta.get_server_private_ip(srv)) self.assertEqual(PUBLIC_V4, meta.get_server_external_ipv4(cloud, srv)) @@ -124,7 +124,7 @@ def test_get_server_private_ip(self, mock_search_networks, srv = meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE')) - cloud = shade.openstack_cloud() + cloud = shade.openstack_cloud(validate=False) self.assertEqual(PRIVATE_V4, meta.get_server_private_ip(srv, cloud)) mock_has_service.assert_called_once_with('network') @@ -154,7 +154,7 @@ def test_get_server_external_ipv4_neutron( srv = meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE')) ip = meta.get_server_external_ipv4( - cloud=shade.openstack_cloud(), server=srv) + cloud=shade.openstack_cloud(validate=False), server=srv) self.assertEqual(PUBLIC_V4, ip) self.assertFalse(mock_get_server_ip.called) @@ -164,7 +164,7 @@ def test_get_server_external_ipv4_neutron_accessIPv4(self): id='test-id', name='test-name', status='ACTIVE', accessIPv4=PUBLIC_V4)) ip = meta.get_server_external_ipv4( - cloud=shade.openstack_cloud(), server=srv) + cloud=shade.openstack_cloud(validate=False), server=srv) self.assertEqual(PUBLIC_V4, ip) @@ -189,7 +189,7 @@ def test_get_server_external_ipv4_neutron_exception( srv = meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE')) ip = meta.get_server_external_ipv4( - cloud=shade.openstack_cloud(), server=srv) + cloud=shade.openstack_cloud(validate=False), server=srv) self.assertEqual(PUBLIC_V4, ip) self.assertTrue(mock_get_server_ip.called) @@ -209,7 +209,7 @@ def test_get_server_external_ipv4_nova_public( id='test-id', name='test-name', status='ACTIVE', addresses={'test-net': [{'addr': PUBLIC_V4}]})) ip = meta.get_server_external_ipv4( - cloud=shade.openstack_cloud(), server=srv) + cloud=shade.openstack_cloud(validate=False), server=srv) self.assertEqual(PUBLIC_V4, ip) self.assertTrue(mock_get_server_ip.called) @@ -230,7 +230,7 @@ def test_get_server_external_ipv4_nova_none( id='test-id', name='test-name', status='ACTIVE', addresses={'test-net': [{'addr': PRIVATE_V4}]})) ip = meta.get_server_external_ipv4( - cloud=shade.openstack_cloud(), server=srv) + cloud=shade.openstack_cloud(validate=False), server=srv) self.assertIsNone(ip) self.assertTrue(mock_get_server_ip.called) diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index 722691703..a7b9a2966 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -30,7 +30,8 @@ class TestObject(base.TestCase): def setUp(self): super(TestObject, self).setUp() config = os_client_config.OpenStackConfig() - self.cloud = OpenStackCloud(cloud_config=config.get_one_cloud()) + self.cloud = OpenStackCloud( + cloud_config=config.get_one_cloud(validate=False)) @mock.patch.object(swift_client, 'Connection') @mock.patch.object(shade.OpenStackCloud, 'auth_token', diff --git a/shade/tests/unit/test_operator_noauth.py b/shade/tests/unit/test_operator_noauth.py index 4c5cfed70..f0d35774b 100644 --- a/shade/tests/unit/test_operator_noauth.py +++ b/shade/tests/unit/test_operator_noauth.py @@ -27,11 +27,15 @@ def setUp(self): URL in the auth data. This is permits testing of the basic mechanism that enables Ironic noauth mode to be utilized with Shade. + + @todo(mordred): remove the token in the next patch - occ handles + this right. """ super(TestShadeOperatorNoAuth, self).setUp() self.cloud_noauth = shade.operator_cloud( - auth_type='None', - auth=dict(endpoint="http://localhost:6385") + auth_type='admin_token', + auth=dict(endpoint="http://localhost:6385", token='foo'), + validate=False, ) @mock.patch.object(shade.OperatorCloud, 'get_session_endpoint') @@ -45,5 +49,5 @@ def test_ironic_noauth_selection_using_a_task( was still called. """ self.cloud_noauth.patch_machine('name', {}) - self.assertFalse(mock_endpoint.called) + self.assertTrue(mock_endpoint.called) self.assertTrue(mock_client.called) diff --git a/shade/tests/unit/test_port.py b/shade/tests/unit/test_port.py index 3e56a6b40..03f931d9a 100644 --- a/shade/tests/unit/test_port.py +++ b/shade/tests/unit/test_port.py @@ -145,7 +145,8 @@ class TestPort(base.TestCase): def setUp(self): super(TestPort, self).setUp() config = os_client_config.OpenStackConfig() - self.client = OpenStackCloud(cloud_config=config.get_one_cloud()) + self.client = OpenStackCloud( + cloud_config=config.get_one_cloud(validate=False)) @patch.object(OpenStackCloud, 'neutron_client') def test_create_port(self, mock_neutron_client): diff --git a/shade/tests/unit/test_rebuild_server.py b/shade/tests/unit/test_rebuild_server.py index c4e6c7c16..c3feb41f3 100644 --- a/shade/tests/unit/test_rebuild_server.py +++ b/shade/tests/unit/test_rebuild_server.py @@ -32,7 +32,8 @@ class TestRebuildServer(base.TestCase): def setUp(self): super(TestRebuildServer, self).setUp() config = os_client_config.OpenStackConfig() - self.client = OpenStackCloud(cloud_config=config.get_one_cloud()) + self.client = OpenStackCloud( + cloud_config=config.get_one_cloud(validate=False)) def test_rebuild_server_rebuild_exception(self): """ diff --git a/shade/tests/unit/test_security_groups.py b/shade/tests/unit/test_security_groups.py index 14e0235e1..d6d7539ab 100644 --- a/shade/tests/unit/test_security_groups.py +++ b/shade/tests/unit/test_security_groups.py @@ -57,7 +57,7 @@ class TestSecurityGroups(base.TestCase): def setUp(self): super(TestSecurityGroups, self).setUp() - self.cloud = shade.openstack_cloud() + self.cloud = shade.openstack_cloud(validate=False) @mock.patch.object(shade.OpenStackCloud, 'neutron_client') @mock.patch.object(shade.OpenStackCloud, 'nova_client') diff --git a/shade/tests/unit/test_services.py b/shade/tests/unit/test_services.py index 79172efd7..39a81732a 100644 --- a/shade/tests/unit/test_services.py +++ b/shade/tests/unit/test_services.py @@ -42,7 +42,8 @@ class CloudServices(base.TestCase): def setUp(self): super(CloudServices, self).setUp() config = os_client_config.OpenStackConfig() - self.client = OperatorCloud(cloud_config=config.get_one_cloud()) + self.client = OperatorCloud(cloud_config=config.get_one_cloud( + validate=False)) self.mock_ks_services = [FakeService(**kwa) for kwa in self.mock_services] diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 515df91c3..0e5beac9c 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -15,8 +15,6 @@ import mock import glanceclient -import keystoneclient.auth.identity.generic.password -import keystoneclient.auth.token_endpoint from keystoneclient.v2_0 import client as k2_client from keystoneclient.v3 import client as k3_client from neutronclient.common import exceptions as n_exc @@ -32,30 +30,11 @@ class TestShade(base.TestCase): def setUp(self): super(TestShade, self).setUp() - self.cloud = shade.openstack_cloud() + self.cloud = shade.openstack_cloud(validate=False) def test_openstack_cloud(self): self.assertIsInstance(self.cloud, shade.OpenStackCloud) - def test_get_auth_token_endpoint(self): - self.cloud.auth_type = 'token_endpoint' - plugin = self.cloud._get_auth_plugin_class() - - self.assertIs(plugin, keystoneclient.auth.token_endpoint.Token) - - def test_get_auth_bogus(self): - self.cloud.auth_type = 'bogus' - self.assertRaises( - exc.OpenStackCloudException, - self.cloud._get_auth_plugin_class) - - def test_get_auth_password(self): - plugin = self.cloud._get_auth_plugin_class() - - self.assertIs( - plugin, - keystoneclient.auth.identity.generic.password.Password) - @mock.patch.object( os_client_config.cloud_config.CloudConfig, 'get_api_version') def test_get_client_v2(self, mock_api_version): @@ -123,7 +102,7 @@ def test_glance_args(self, mock_client, mock_keystone_session): mock_keystone_session.return_value = None self.cloud.glance_client mock_client.assert_called_with( - version='1', region_name='', service_name=None, + version='2', region_name='', service_name=None, interface='public', service_type='image', session=mock.ANY, ) diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index 60d05b9bb..abe41b263 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -from keystoneclient import auth as ksc_auth +from keystoneauth1 import plugin as ksc_plugin import mock import testtools @@ -29,7 +29,7 @@ class TestShadeOperator(base.TestCase): def setUp(self): super(TestShadeOperator, self).setUp() - self.cloud = shade.operator_cloud() + self.cloud = shade.operator_cloud(validate=False) def test_operator_cloud(self): self.assertIsInstance(self.cloud, shade.OperatorCloud) @@ -768,7 +768,7 @@ def test_get_session_endpoint_unavailable(self, session_mock): def test_get_session_endpoint_identity(self, session_mock): self.cloud.get_session_endpoint('identity') session_mock.get_endpoint.assert_called_with( - interface=ksc_auth.AUTH_INTERFACE) + interface=ksc_plugin.AUTH_INTERFACE) @mock.patch.object(shade.OpenStackCloud, 'keystone_session') def test_has_service_no(self, session_mock): From 5d9bb47f0d0015533cbf547d613600f8b4b7d2d7 Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Fri, 18 Sep 2015 15:35:55 -0700 Subject: [PATCH 0473/3836] Store the inner exception when creating an OSCException Masking exceptions in Python is an anti-pattern. Etc. However, since shade uses a base class of OpenStackCloudException for all of its exceptions (and, for the worst offenders of generalized exception masking, simply uses that exception itself), we can store the original exception for clients to access if needed. This does allow client exceptions to leak out of shade to its users, but in a very controlled manner where we can make it clear that this should not be used for flow control, but for informational purposes. Change-Id: I33269743a8f62b863569130aba3cc9b5a8539aa0 --- shade/exc.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shade/exc.py b/shade/exc.py index 997e1caf0..c4847e2e0 100644 --- a/shade/exc.py +++ b/shade/exc.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import sys + class OpenStackCloudException(Exception): def __init__(self, message, extra_data=None): @@ -20,6 +22,7 @@ def __init__(self, message, extra_data=None): args.append(extra_data) super(OpenStackCloudException, self).__init__(*args) self.extra_data = extra_data + self.inner_exception = sys.exc_info() def __str__(self): if self.extra_data is not None: From 02cf7cee76a4c732c6294d14a557363256d6e83a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 18 Sep 2015 19:12:00 -0400 Subject: [PATCH 0474/3836] Just do the error logging in the base exception We want to make an error log every time we throw an exception that is wrapping another exception. So, just do it in the constructor. Change-Id: Id8f1ab97d1c58787ff101ea2410fcf7c789041ed --- shade/exc.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/shade/exc.py b/shade/exc.py index c4847e2e0..69a7651ba 100644 --- a/shade/exc.py +++ b/shade/exc.py @@ -12,10 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging import sys +log = logging.getLogger(__name__) + class OpenStackCloudException(Exception): + def __init__(self, message, extra_data=None): args = [message] if extra_data: @@ -23,12 +27,19 @@ def __init__(self, message, extra_data=None): super(OpenStackCloudException, self).__init__(*args) self.extra_data = extra_data self.inner_exception = sys.exc_info() + if self.inner_exception and self.inner_exception[1]: + log.error(message, exc_info=self.inner_exception) def __str__(self): + message = Exception.__str__(self) if self.extra_data is not None: - return "%s (Extra: %s)" % ( - Exception.__str__(self), self.extra_data) - return Exception.__str__(self) + message = "%s (Extra: %s)" % (message, self.extra_data) + if (self.inner_exception and self.inner_exception[1] + and hasattr(self.inner_exception[1], 'message')): + message = "%s (Inner Exception: %s)" % ( + message, + self.inner_exception[1].message) + return message class OpenStackCloudTimeout(OpenStackCloudException): From 75954fac2f711bfed815fd4015dff48ea26492c0 Mon Sep 17 00:00:00 2001 From: Xav Paice Date: Tue, 22 Sep 2015 09:23:12 +1200 Subject: [PATCH 0475/3836] Add support for Catalyst as vendor This adds Catalyst IT's public cloud to the list of vendors, https://catalyst.net.nz/catalyst-cloud Change-Id: I2d886bfc4fc94c534a55f6bad1120e474654524f --- doc/source/vendor-support.rst | 14 ++++++++++++++ os_client_config/vendors/catalyst.yaml | 10 ++++++++++ 2 files changed, 24 insertions(+) create mode 100644 os_client_config/vendors/catalyst.yaml diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index 77d10ebba..39f488d14 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -233,3 +233,17 @@ nyj01 New York, NY * Image API Version is 1 * Floating IPs are not supported + +catalyst +-------- + +https://api.cloud.catalyst.net.nz:5000/v2.0 + +============== ================ +Region Name Human Name +============== ================ +nz-por-1 Porirua, NZ +nz_wlg_1 Wellington, NZ + +* Image API Version is 1 +* Images must be in `raw` format diff --git a/os_client_config/vendors/catalyst.yaml b/os_client_config/vendors/catalyst.yaml new file mode 100644 index 000000000..348ceafc8 --- /dev/null +++ b/os_client_config/vendors/catalyst.yaml @@ -0,0 +1,10 @@ +name: catalyst +profile: + auth: + auth_url: https://api.cloud.catalyst.net.nz:5000/v2.0 + regions: + - nz-por-1 + - nz_wlg_1 + identity_api_version: '2' + image_api_version: '1' + image_format: raw From 84904f3f38fbc276e19e94e3ad617a5b27166d20 Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Mon, 21 Sep 2015 16:06:00 -0700 Subject: [PATCH 0476/3836] Make inner_exception a private member So we don't mislead anyone into thinking it's part of the public API. Change-Id: Ibc74eeec733bc56ba4c05c27f7e3de3c89b5f31d --- shade/exc.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/shade/exc.py b/shade/exc.py index 69a7651ba..32d9e90ed 100644 --- a/shade/exc.py +++ b/shade/exc.py @@ -26,19 +26,19 @@ def __init__(self, message, extra_data=None): args.append(extra_data) super(OpenStackCloudException, self).__init__(*args) self.extra_data = extra_data - self.inner_exception = sys.exc_info() - if self.inner_exception and self.inner_exception[1]: - log.error(message, exc_info=self.inner_exception) + self._inner_exception = sys.exc_info() + if self._inner_exception and self._inner_exception[1]: + log.error(message, exc_info=self._inner_exception) def __str__(self): message = Exception.__str__(self) if self.extra_data is not None: message = "%s (Extra: %s)" % (message, self.extra_data) - if (self.inner_exception and self.inner_exception[1] - and hasattr(self.inner_exception[1], 'message')): + if (self._inner_exception and self._inner_exception[1] + and hasattr(self._inner_exception[1], 'message')): message = "%s (Inner Exception: %s)" % ( message, - self.inner_exception[1].message) + self._inner_exception[1].message) return message From 0f089647b89817c5347e1f01335fd6aa6f9d313b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 22 Sep 2015 11:29:46 -0500 Subject: [PATCH 0477/3836] Fix a little error with the None auth type Ironic has a mode where it does not use auth, but we still funnel that through the ksa admin_token type for consistency of code. The hack we had to do that went too far and caused validate_auth to strip the fake token we were adding. Change-Id: Id5275ac7db1a6052db02c2286cbf88862cb1ff70 --- os_client_config/config.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 1b6193e46..fcca30af3 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -419,7 +419,9 @@ def _get_auth_loader(self, config): # _other_ things (SSL arg handling, timeout) all work consistently if config['auth_type'] in (None, "None", ''): config['auth_type'] = 'admin_token' - config['auth']['token'] = None + # Set to notused rather than None because validate_auth will + # strip the value if it's actually python None + config['auth']['token'] = 'notused' return loading.get_plugin_loader(config['auth_type']) def _validate_auth_ksc(self, config): From 2e350d034cb115c4d3a3513b566aa6f048c3c07f Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Fri, 18 Sep 2015 16:46:26 -0700 Subject: [PATCH 0478/3836] Remove many redundant debug logs Now that shade stores inner exceptions in the OpenStackCloudException constructor and logs them, there is no need for logging the same exceptions at debug level in the library. This patch removes all such log entries that appear to add no additional information beyond what is in the exception. Many messages remain which add information not in the exception -- their accompanying exceptions should be updated and then the extraneous log lines should be removed. Change-Id: Idcdbfa77c68260d171e78f6ada4ee5a46518061c --- shade/__init__.py | 137 +--------------------------------------------- 1 file changed, 1 insertion(+), 136 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 6e0a7c4fe..69aecc266 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -278,9 +278,6 @@ def _neutron_exceptions(self, error_message): raise OpenStackCloudResourceNotFound( "{msg}: {exc}".format(msg=error_message, exc=str(e))) except neutron_exceptions.NeutronClientException as e: - self.log.debug( - "{msg}: {exc}".format(msg=error_message, exc=str(e)), - exc_info=True) if e.status_code == 404: raise OpenStackCloudURINotFound( "{msg}: {exc}".format(msg=error_message, exc=str(e))) @@ -288,9 +285,6 @@ def _neutron_exceptions(self, error_message): raise OpenStackCloudException( "{msg}: {exc}".format(msg=error_message, exc=str(e))) except Exception as e: - self.log.debug( - "{msg}: {exc}".format(msg=error_message, exc=str(e)), - exc_info=True) raise OpenStackCloudException( "{msg}: {exc}".format(msg=error_message, exc=str(e))) @@ -363,9 +357,6 @@ def keystone_session(self): try: loader = loading.get_plugin_loader(self.auth_type) except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin: - self.log.debug( - "keystoneauth could not find auth plugin {plugin}".format( - plugin=self.auth_type), exc_info=True) raise OpenStackCloudException( "No auth plugin named {plugin}".format( plugin=self.auth_type)) @@ -373,8 +364,6 @@ def keystone_session(self): try: keystone_auth = loader.plugin_class(**self.auth) except Exception as e: - self.log.debug( - "keystone couldn't construct plugin", exc_info=True) raise OpenStackCloudException( "Error constructing auth plugin: {plugin} {error}".format( plugin=self.auth_type, error=str(e))) @@ -386,7 +375,6 @@ def keystone_session(self): cert=self.cert, timeout=self.api_timeout) except Exception as e: - self.log.debug("keystone unknown issue", exc_info=True) raise OpenStackCloudException( "Error authenticating to the keystone: %s " % str(e)) return self._keystone_session @@ -489,7 +477,6 @@ def update_project(self, name_or_id, description=None, enabled=True): return meta.obj_to_dict( project.update(description=description, enabled=enabled)) except Exception as e: - self.log.debug("keystone update project issue", exc_info=True) raise OpenStackCloudException( "Error in updating project {project}: {message}".format( project=name_or_id, message=str(e))) @@ -503,7 +490,6 @@ def create_project( project_name=name, description=description, enabled=enabled, **domain_params) except Exception as e: - self.log.debug("keystone create project issue", exc_info=True) raise OpenStackCloudException( "Error in creating project {project}: {message}".format( project=name, message=str(e))) @@ -513,7 +499,6 @@ def delete_project(self, name_or_id): project = self.update_project(name_or_id, enabled=False) self._project_manager.delete(project.id) except Exception as e: - self.log.debug("keystone delete project issue", exc_info=True) raise OpenStackCloudException( "Error in deleting project {project}: {message}".format( project=name_or_id, message=str(e))) @@ -558,7 +543,6 @@ def update_user(self, name_or_id, email=None, enabled=None): try: user = self.manager.submitTask(_tasks.UserUpdate(**user_args)) except Exception as e: - self.log.debug("keystone update user issue", exc_info=True) raise OpenStackCloudException( "Error in updating user {user}: {message}".format( user=name_or_id, message=str(e))) @@ -576,7 +560,6 @@ def create_user( user_name=name, password=password, email=email, enabled=enabled, **identity_params)) except Exception as e: - self.log.debug("keystone create user issue", exc_info=True) raise OpenStackCloudException( "Error in creating user {user}: {message}".format( user=name, message=str(e))) @@ -589,7 +572,6 @@ def delete_user(self, name_or_id): user = self._get_user(name_or_id) self.manager.submitTask(_tasks.UserDelete(user=user)) except Exception as e: - self.log.debug("keystone delete user issue", exc_info=True) raise OpenStackCloudException( "Error in deleting user {user}: {message}".format( user=name_or_id, message=str(e))) @@ -622,8 +604,6 @@ def swift_client(self): except OpenStackCloudException: raise except Exception as e: - self.log.debug( - "error constructing swift client", exc_info=True) raise OpenStackCloudException( "Error constructing swift client: %s", str(e)) return self._swift_client @@ -642,8 +622,6 @@ def swift_service(self): except OpenStackCloudException: raise except Exception as e: - self.log.debug( - "error constructing swift client", exc_info=True) raise OpenStackCloudException( "Error constructing swift client: %s", str(e)) return self._swift_service @@ -746,7 +724,6 @@ def get_session_endpoint(self, service_key): "Endpoint not found in %s cloud: %s", self.name, str(e)) endpoint = None except Exception as e: - self.log.debug("keystone cannot get endpoint", exc_info=True) raise OpenStackCloudException( "Error getting %s endpoint: %s" % (service_key, str(e))) if endpoint is None: @@ -772,9 +749,6 @@ def _nova_extensions(self): for x in body['extensions']: extensions.add(x['alias']) except Exception as e: - self.log.debug( - "nova could not list extensions: {msg}".format( - msg=str(e)), exc_info=True) raise OpenStackCloudException( "error fetching extension list for nova: {msg}".format( msg=str(e))) @@ -852,7 +826,6 @@ def list_keypairs(self): self.manager.submitTask(_tasks.KeypairList()) ) except Exception as e: - self.log.debug("keypair list failed: %s" % e, exc_info=True) raise OpenStackCloudException( "Error fetching keypair list: %s" % e) @@ -882,7 +855,6 @@ def list_volumes(self, cache=True): self.manager.submitTask(_tasks.VolumeList()) ) except Exception as e: - self.log.debug("volume list failed: %s" % e, exc_info=True) raise OpenStackCloudException( "Error fetching volume list: %s" % e) @@ -893,7 +865,6 @@ def list_flavors(self): self.manager.submitTask(_tasks.FlavorList(is_public=None)) ) except Exception as e: - self.log.debug("flavor list failed: %s" % e, exc_info=True) raise OpenStackCloudException( "Error fetching flavor list: %s" % e) @@ -912,11 +883,7 @@ def list_security_groups(self): groups = meta.obj_list_to_dict( self.manager.submitTask(_tasks.NovaSecurityGroupList()) ) - except Exception as e: - self.log.debug( - "nova could not list security groups: {message}".format( - message=str(e)), - exc_info=True) + except Exception: raise OpenStackCloudException( "Error fetching security group list" ) @@ -934,7 +901,6 @@ def list_servers(self): self.manager.submitTask(_tasks.ServerList()) ) except Exception as e: - self.log.debug("server list failed: %s" % e, exc_info=True) raise OpenStackCloudException( "Error fetching server list: %s" % e) @@ -973,12 +939,10 @@ def list_images(self, filter_deleted=True): self.manager.submitTask(_tasks.NovaImageList()) ) except Exception as e: - self.log.debug("nova image list failed: %s" % e, exc_info=True) raise OpenStackCloudException( "Error fetching image list: %s" % e) except Exception as e: - self.log.debug("glance image list failed: %s" % e, exc_info=True) raise OpenStackCloudException( "Error fetching image list: %s" % e) @@ -1001,9 +965,6 @@ def list_floating_ip_pools(self): self.manager.submitTask(_tasks.FloatingIPPoolList()) ) except Exception as e: - self.log.debug( - "nova could not list floating IP pools: {msg}".format( - msg=str(e)), exc_info=True) raise OpenStackCloudException( "error fetching floating IP pool list: {msg}".format( msg=str(e))) @@ -1032,9 +993,6 @@ def _nova_list_floating_ips(self): return meta.obj_list_to_dict( self.manager.submitTask(_tasks.NovaFloatingIPList())) except Exception as e: - self.log.debug( - "nova could not list floating IPs: {msg}".format( - msg=str(e)), exc_info=True) raise OpenStackCloudException( "error fetching floating IPs list: {msg}".format(msg=str(e))) @@ -1042,7 +1000,6 @@ def list_domains(self): try: return self.manager.submitTask(_tasks.DomainList()) except Exception as e: - self.log.debug("domain list failed: %s" % e, exc_info=True) raise OpenStackCloudException( "Error fetching domain list: %s" % e) @@ -1050,7 +1007,6 @@ def list_records(self, domain_id): try: return self.manager.submitTask(_tasks.RecordList(domain=domain_id)) except Exception as e: - self.log.debug("record list failed: %s" % e, exc_info=True) raise OpenStackCloudException( "Error fetching record list: %s" % e) @@ -1110,7 +1066,6 @@ def create_keypair(self, name, public_key): name=name, public_key=public_key)) ) except Exception as e: - self.log.debug("Error creating keypair %s" % name, exc_info=True) raise OpenStackCloudException( "Unable to create keypair %s: %s" % (name, e) ) @@ -1130,7 +1085,6 @@ def delete_keypair(self, name): self.log.debug("Keypair %s not found for deleting" % name) return False except Exception as e: - self.log.debug("Error deleting keypair %s" % name, exc_info=True) raise OpenStackCloudException( "Unable to delete keypair %s: %s" % (name, e) ) @@ -1320,7 +1274,6 @@ def delete_image(self, name_or_id, wait=False, timeout=3600): self.manager.submitTask( _tasks.ImageDelete(image=image.id)) except Exception as e: - self.log.debug("Image deletion failed", exc_info=True) raise OpenStackCloudException( "Error in deleting image: %s" % str(e)) @@ -1371,7 +1324,6 @@ def create_image( self.log.debug("Image creation failed", exc_info=True) raise except Exception as e: - self.log.debug("Image creation failed", exc_info=True) raise OpenStackCloudException( "Image creation failed: {message}".format(message=str(e))) @@ -1520,7 +1472,6 @@ def create_volume(self, wait=True, timeout=None, **kwargs): try: volume = self.manager.submitTask(_tasks.VolumeCreate(**kwargs)) except Exception as e: - self.log.debug("Volume creation failed", exc_info=True) raise OpenStackCloudException( "Error in creating volume: %s" % str(e)) self.list_volumes.invalidate(self) @@ -1565,7 +1516,6 @@ def delete_volume(self, name_or_id=None, wait=True, timeout=None): self.manager.submitTask( _tasks.VolumeDelete(volume=volume['id'])) except Exception as e: - self.log.debug("Volume deletion failed", exc_info=True) raise OpenStackCloudException( "Error in deleting volume: %s" % str(e)) @@ -1633,7 +1583,6 @@ def detach_volume(self, server, volume, wait=True, timeout=None): _tasks.VolumeDetach(attachment_id=volume['id'], server_id=server['id'])) except Exception as e: - self.log.debug("nova volume detach failed", exc_info=True) raise OpenStackCloudException( "Error detaching volume %s from server %s: %s" % (volume['id'], server['id'], e) @@ -1700,9 +1649,6 @@ def attach_volume(self, server, volume, device=None, server_id=server['id'], device=device)) except Exception as e: - self.log.debug( - "nova volume attach of %s failed" % volume['id'], - exc_info=True) raise OpenStackCloudException( "Error attaching volume %s to server %s: %s" % (volume['id'], server['id'], e) @@ -1862,9 +1808,6 @@ def _nova_available_floating_ips(self, pool=None): return [f_ip] except Exception as e: - self.log.debug( - "nova floating IP create failed: {msg}".format( - msg=str(e)), exc_info=True) raise OpenStackCloudException( "unable to create floating IP in pool {pool}: {msg}".format( pool=pool, msg=str(e))) @@ -1927,9 +1870,6 @@ def _nova_create_floating_ip(self, pool=None): return meta.obj_to_dict(pool_ip) except Exception as e: - self.log.debug( - "nova floating IP create failed: {msg}".format( - msg=str(e)), exc_info=True) raise OpenStackCloudException( "unable to create floating IP in pool {pool}: {msg}".format( pool=pool, msg=str(e))) @@ -1973,9 +1913,6 @@ def _nova_delete_floating_ip(self, floating_ip_id): except nova_exceptions.NotFound: return False except Exception as e: - self.log.debug( - "nova floating IP delete failed: {msg}".format( - msg=str(e)), exc_info=True) raise OpenStackCloudException( "unable to delete floating IP id {fip_id}: {msg}".format( fip_id=floating_ip_id, msg=str(e))) @@ -2091,9 +2028,6 @@ def _nova_attach_ip_to_server(self, server_id, floating_ip_id, server=server_id, address=f_ip['floating_ip_address'], fixed_address=fixed_address)) except Exception as e: - self.log.debug( - "nova floating IP attach failed: {msg}".format(msg=str(e)), - exc_info=True) raise OpenStackCloudException( "error attaching IP {ip} to instance {id}: {msg}".format( ip=floating_ip_id, id=server_id, msg=str(e))) @@ -2150,9 +2084,6 @@ def _nova_detach_ip_from_server(self, server_id, floating_ip_id): exc_info=True) return False except Exception as e: - self.log.debug( - "nova floating IP detach failed: {msg}".format(msg=str(e)), - exc_info=True) raise OpenStackCloudException( "error detaching IP {ip} from instance {id}: {msg}".format( ip=floating_ip_id, id=server_id, msg=str(e))) @@ -2246,7 +2177,6 @@ def add_ips_to_server(self, server, auto_ip=True, ips=None, ip_pool=None): server = meta.obj_to_dict( self.manager.submitTask(_tasks.ServerGet(server=server))) except Exception as e: - self.log.debug("nova info failed", exc_info=True) raise OpenStackCloudException( "Error in getting info from instance: %s " % str(e)) return server @@ -2273,7 +2203,6 @@ def create_server(self, auto_ip=True, ips=None, ip_pool=None, server = self.manager.submitTask(_tasks.ServerCreate(**bootkwargs)) server = self.manager.submitTask(_tasks.ServerGet(server=server)) except Exception as e: - self.log.debug("nova instance create failed", exc_info=True) raise OpenStackCloudException( "Error in creating instance: {0}".format(e)) if server.status == 'ERROR': @@ -2330,7 +2259,6 @@ def rebuild_server(self, server_id, image_id, wait=False, timeout=180): server = self.manager.submitTask(_tasks.ServerRebuild( server=server_id, image=image_id)) except Exception as e: - self.log.debug("nova instance rebuild failed", exc_info=True) raise OpenStackCloudException( "Error in rebuilding instance: {0}".format(e)) if wait: @@ -2367,7 +2295,6 @@ def _delete_server(self, server, wait=False, timeout=180): except nova_exceptions.NotFound: return except Exception as e: - self.log.debug("nova delete server failed", exc_info=True) raise OpenStackCloudException( "Error in deleting server: {0}".format(e)) else: @@ -2386,8 +2313,6 @@ def _delete_server(self, server, wait=False, timeout=180): except nova_exceptions.NotFound: return except Exception as e: - self.log.debug("nova get server failed when waiting for " - "delete", exc_info=True) raise OpenStackCloudException( "Error in deleting server: {0}".format(e)) @@ -2400,7 +2325,6 @@ def get_container(self, name, skip_cache=False): except swift_exceptions.ClientException as e: if e.http_status == 404: return None - self.log.debug("swift container fetch failed", exc_info=True) raise OpenStackCloudException( "Container fetch failed: %s (%s/%s)" % ( e.http_reason, e.http_host, e.http_path)) @@ -2417,7 +2341,6 @@ def create_container(self, name, public=False): self.set_container_access(name, 'public') return self.get_container(name, skip_cache=True) except swift_exceptions.ClientException as e: - self.log.debug("swift container create failed", exc_info=True) raise OpenStackCloudException( "Container creation failed: %s (%s/%s)" % ( e.http_reason, e.http_host, e.http_path)) @@ -2429,7 +2352,6 @@ def delete_container(self, name): except swift_exceptions.ClientException as e: if e.http_status == 404: return - self.log.debug("swift container delete failed", exc_info=True) raise OpenStackCloudException( "Container deletion failed: %s (%s/%s)" % ( e.http_reason, e.http_host, e.http_path)) @@ -2439,7 +2361,6 @@ def update_container(self, name, headers): self.manager.submitTask( _tasks.ContainerUpdate(container=name, headers=headers)) except swift_exceptions.ClientException as e: - self.log.debug("swift container update failed", exc_info=True) raise OpenStackCloudException( "Container update failed: %s (%s/%s)" % ( e.http_reason, e.http_host, e.http_path)) @@ -2494,8 +2415,6 @@ def get_object_segment_size(self, segment_size): "Swift capabilities not supported. " "Using default max file size.") else: - self.log.debug( - "Failed to query swift capabilities", exc_info=True) raise OpenStackCloudException( "Could not determine capabilities") else: @@ -2605,7 +2524,6 @@ def get_object_metadata(self, container, name): except swift_exceptions.ClientException as e: if e.http_status == 404: return None - self.log.debug("swift metadata fetch failed", exc_info=True) raise OpenStackCloudException( "Object metadata fetch failed: %s (%s/%s)" % ( e.http_reason, e.http_host, e.http_path)) @@ -3000,9 +2918,6 @@ def create_security_group(self, name, description): ) ) except Exception as e: - self.log.debug( - "nova failed to create security group '{name}'".format( - name=name), exc_info=True) raise OpenStackCloudException( "failed to create security group '{name}': {msg}".format( name=name, msg=str(e))) @@ -3047,9 +2962,6 @@ def delete_security_group(self, name_or_id): _tasks.NovaSecurityGroupDelete(group=secgroup['id']) ) except Exception as e: - self.log.debug( - "nova failed to delete security group '{group}'".format( - group=name_or_id), exc_info=True) raise OpenStackCloudException( "failed to delete security group '{group}': {msg}".format( group=name_or_id, msg=str(e))) @@ -3098,9 +3010,6 @@ def update_security_group(self, name_or_id, **kwargs): ) ) except Exception as e: - self.log.debug( - "nova failed to update security group '{group}'".format( - group=name_or_id), exc_info=True) raise OpenStackCloudException( "failed to update security group '{group}': {msg}".format( group=name_or_id, msg=str(e))) @@ -3236,8 +3145,6 @@ def create_security_group_rule(self, ) ) except Exception as e: - self.log.debug("nova failed to create security group rule", - exc_info=True) raise OpenStackCloudException( "failed to create security group rule: {msg}".format( msg=str(e))) @@ -3282,10 +3189,6 @@ def delete_security_group_rule(self, rule_id): except nova_exceptions.NotFound: return False except Exception as e: - self.log.debug( - "nova failed to delete security group rule {id}".format( - id=rule_id), - exc_info=True) raise OpenStackCloudException( "failed to delete security group rule {id}: {msg}".format( id=rule_id, msg=str(e))) @@ -3347,7 +3250,6 @@ def ironic_client(self): timeout=self.api_timeout, os_ironic_api_version=ironic_api_microversion) except Exception as e: - self.log.debug("ironic auth failed", exc_info=True) raise OpenStackCloudException( "Error in connecting to ironic: %s" % str(e)) return self._ironic_client @@ -3358,7 +3260,6 @@ def list_nics(self): self.manager.submitTask(_tasks.MachinePortList()) ) except Exception as e: - self.log.debug("machine port list failed: %s" % e, exc_info=True) raise OpenStackCloudException( "Error fetching machine port list: %s" % e) @@ -3369,8 +3270,6 @@ def list_nics_for_machine(self, uuid): _tasks.MachineNodePortList(node_id=uuid)) ) except Exception as e: - self.log.debug("port list for node %s failed: %s" % (uuid, e), - exc_info=True) raise OpenStackCloudException( "Error fetching port list for node %s: %s" % (uuid, e)) @@ -3476,7 +3375,6 @@ def register_machine(self, nics, wait=False, timeout=3600, self.manager.submitTask(_tasks.MachineCreate(**kwargs))) except Exception as e: - self.log.debug("ironic machine registration failed", exc_info=True) raise OpenStackCloudException( "Error registering machine with Ironic: %s" % str(e)) @@ -3607,8 +3505,6 @@ def unregister_machine(self, nics, uuid, wait=False, timeout=600): self.manager.submitTask( _tasks.MachinePortDelete(port_id=port_id)) except Exception as e: - self.log.debug( - "baremetal NIC unregistration failed", exc_info=True) raise OpenStackCloudException( "Error removing NIC '%s' from baremetal API for " "node '%s'. Error: %s" % (nic, uuid, str(e))) @@ -3623,8 +3519,6 @@ def unregister_machine(self, nics, uuid, wait=False, timeout=600): break except Exception as e: - self.log.debug( - "baremetal machine unregistration failed", exc_info=True) raise OpenStackCloudException( "Error unregistering machine %s from the baremetal API. " "Error: %s" % (uuid, str(e))) @@ -3673,8 +3567,6 @@ def patch_machine(self, name_or_id, patch): patch=patch, http_method='PATCH'))) except Exception as e: - self.log.debug( - "Machine patch update failed", exc_info=True) raise OpenStackCloudException( "Error updating machine via patch operation. node: %s. " "%s" % (name_or_id, str(e))) @@ -3756,10 +3648,6 @@ def update_machine(self, name_or_id, chassis_uuid=None, driver=None, self.log.debug( "Unexpected machine response missing key %s [%s]" % ( e.args[0], name_or_id)) - self.log.debug( - "Machine update failed - update value preparation failed. " - "Potential API failure or change has been encountered", - exc_info=True) raise OpenStackCloudException( "Machine update failed - machine [%s] missing key %s. " "Potential API issue." @@ -3768,9 +3656,6 @@ def update_machine(self, name_or_id, chassis_uuid=None, driver=None, try: patch = jsonpatch.JsonPatch.from_diff(machine_config, new_config) except Exception as e: - self.log.debug( - "Machine update failed - patch object generation failed", - exc_info=True) raise OpenStackCloudException( "Machine update failed - Error generating JSON patch object " "for submission to the API. Machine: %s Error: %s" @@ -3792,9 +3677,6 @@ def update_machine(self, name_or_id, chassis_uuid=None, driver=None, changes=change_list ) except Exception as e: - self.log.debug( - "Machine update failed - patch operation failed", - exc_info=True) raise OpenStackCloudException( "Machine update failed - patch operation failed Machine: %s " "Error: %s" % (name_or_id, str(e))) @@ -3804,8 +3686,6 @@ def validate_node(self, uuid): ifaces = self.manager.submitTask( _tasks.MachineNodeValidate(node_uuid=uuid)) except Exception as e: - self.log.debug( - "ironic node validation call failed", exc_info=True) raise OpenStackCloudException(str(e)) if not ifaces.deploy or not ifaces.power: @@ -4017,8 +3897,6 @@ def set_node_instance_info(self, uuid, patch): _tasks.MachineNodeUpdate(node_id=uuid, patch=patch)) ) except Exception as e: - self.log.debug( - "Failed to update instance_info", exc_info=True) raise OpenStackCloudException(str(e)) def purge_node_instance_info(self, uuid): @@ -4030,8 +3908,6 @@ def purge_node_instance_info(self, uuid): _tasks.MachineNodeUpdate(node_id=uuid, patch=patch)) ) except Exception as e: - self.log.debug( - "Failed to delete instance_info", exc_info=True) raise OpenStackCloudException(str(e)) def create_service(self, name, service_type, description=None): @@ -4074,7 +3950,6 @@ def list_services(self): try: services = self.manager.submitTask(_tasks.ServiceList()) except Exception as e: - self.log.debug("Failed to list services", exc_info=True) raise OpenStackCloudException(str(e)) return meta.obj_list_to_dict(services) @@ -4282,7 +4157,6 @@ def update_identity_domain( domain=domain_id, description=description, enabled=enabled))) except Exception as e: - self.log.debug("keystone update domain issue", exc_info=True) raise OpenStackCloudException( "Error in updating domain {domain}: {message}".format( domain=domain_id, message=str(e))) @@ -4452,8 +4326,6 @@ def _mod_flavor_specs(self, action, flavor_id, specs): "Flavor ID {0} not found".format(flavor_id) ) except Exception as e: - self.log.debug("Error getting flavor ID {0}".format(flavor_id), - exc_info=True) raise OpenStackCloudException( "Error getting flavor ID {0}: {1}".format(flavor_id, e) ) @@ -4464,8 +4336,6 @@ def _mod_flavor_specs(self, action, flavor_id, specs): elif action == 'unset': flavor.unset_keys(specs) except Exception as e: - self.log.debug("Error during {0} of flavor specs".format(action), - exc_info=True) raise OpenStackCloudException( "Unable to {0} flavor specs: {1}".format(action, e) ) @@ -4507,11 +4377,6 @@ def _mod_flavor_access(self, action, flavor_id, project_id): tenant=project_id) ) except Exception as e: - self.log.debug( - "Error trying to {0} access to flavor ID {1}".format( - action, flavor_id), - exc_info=True - ) raise OpenStackCloudException( "Error trying to {0} access from flavor ID {1}: {2}".format( action, flavor_id, e) From 03c1556a12aabfc21de60a9fac97aea7871485a3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 22 Sep 2015 16:03:30 -0500 Subject: [PATCH 0479/3836] Add a NullHandler to all of our loggers As a library, it's important to make sure our logging is set up to not be confusing, but also to not to step on the toes of app developers. https://docs.python.org/3.1/library/logging.html#configuring-logging-for-a-library Change-Id: I312871e057ca0f64b9c5514bc94567cbdb34b4c6 --- shade/__init__.py | 5 +++-- shade/_log.py | 28 ++++++++++++++++++++++++++++ shade/exc.py | 5 +++-- shade/meta.py | 4 ++-- shade/task_manager.py | 5 +++-- 5 files changed, 39 insertions(+), 8 deletions(-) create mode 100644 shade/_log.py diff --git a/shade/__init__.py b/shade/__init__.py index 69aecc266..12d9999d4 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -51,6 +51,7 @@ warnings.filterwarnings('ignore', 'Certificate has no `subjectAltName`') from shade.exc import * # noqa +from shade import _log from shade import meta from shade import task_manager from shade import _tasks @@ -106,7 +107,7 @@ def simple_logging(debug=False): log_level = logging.DEBUG else: log_level = logging.INFO - log = logging.getLogger('shade') + log = _log.setup_logging('shade') log.addHandler(logging.StreamHandler()) log.setLevel(log_level) @@ -220,7 +221,7 @@ def __init__( cache_arguments=None, manager=None, **kwargs): - self.log = logging.getLogger('shade') + self.log = _log.setup_logging('shade') if not cloud_config: config = os_client_config.OpenStackConfig() cloud_config = config.get_one_cloud(**kwargs) diff --git a/shade/_log.py b/shade/_log.py new file mode 100644 index 000000000..ff2f2eac7 --- /dev/null +++ b/shade/_log.py @@ -0,0 +1,28 @@ +# Copyright (c) 2015 IBM Corp. +# +# 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 + + +class NullHandler(logging.Handler): + def emit(self, record): + pass + + +def setup_logging(name): + log = logging.getLogger(name) + if len(log.handlers) == 0: + h = NullHandler() + log.addHandler(h) + return log diff --git a/shade/exc.py b/shade/exc.py index 32d9e90ed..6d20992aa 100644 --- a/shade/exc.py +++ b/shade/exc.py @@ -12,10 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import logging import sys -log = logging.getLogger(__name__) +from shade import _log + +log = _log.setup_logging(__name__) class OpenStackCloudException(Exception): diff --git a/shade/meta.py b/shade/meta.py index 5585e9bf3..576a8c26a 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -14,16 +14,16 @@ import bunch -import logging import six from shade import exc +from shade import _log from shade import _utils NON_CALLABLES = (six.string_types, bool, dict, int, float, list, type(None)) -log = logging.getLogger(__name__) +log = _log.setup_logging(__name__) def find_nova_addresses(addresses, ext_tag=None, key_name=None, version=4): diff --git a/shade/task_manager.py b/shade/task_manager.py index d4aaf73e2..f2b9e5704 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -17,13 +17,14 @@ # limitations under the License. import abc -import logging import sys import threading import time import six +from shade import _log + @six.add_metaclass(abc.ABCMeta) class Task(object): @@ -78,7 +79,7 @@ def run(self, client): class TaskManager(object): - log = logging.getLogger("shade.TaskManager") + log = _log.setup_logging("shade.TaskManager") def __init__(self, client, name): self.name = name From 988e305b37e0f840ba09fdde882158641f8a1d05 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 22 Sep 2015 17:19:13 -0400 Subject: [PATCH 0480/3836] update RST for readme so pypi looks pretty Navigating to https://pypi.python.org/pypi/os-client-config results in seeing the raw RST content of the readme file. This is likely caused by minor RST warnings, but pypi gives up and shows it raw. Change-Id: Ia2d6202ade5282d9aeae9bb948175aae2aa264cd --- README.rst | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 4cae7358a..488dd24bb 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,6 @@ -=============================== +================ os-client-config -=============================== +================ `os-client-config` is a library for collecting client configuration for using an OpenStack cloud in a consistent and comprehensive manner. It @@ -27,7 +27,9 @@ it by setting `OS_CLOUD_NAME`. Service specific settings, like the nova service type, are set with the default service type as a prefix. For instance, to set a special service_type -for trove set:: +for trove set + +:: export OS_DATABASE_SERVICE_TYPE=rax:database @@ -228,6 +230,7 @@ Usage ----- The simplest and least useful thing you can do is: + :: python -m os_client_config.config @@ -236,6 +239,7 @@ Which will print out whatever if finds for your config. If you want to use it from python, which is much more likely what you want to do, things like: Get a named cloud. + :: import os_client_config @@ -245,7 +249,9 @@ Get a named cloud. print(cloud_config.name, cloud_config.region, cloud_config.config) Or, get all of the clouds. + :: + import os_client_config cloud_config = os_client_config.OpenStackConfig().get_all_clouds() From 489314a0f5bc225004492455e0e2bb6726ad92e7 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 7 Sep 2015 19:30:32 -0500 Subject: [PATCH 0481/3836] Use the get_auth function from occ os-client-config now knows how to turn auth parameters into auth objects. Use that, so that we're not carrying local logic for it. Also, this lets us remove our ironic-specific hacks around auth and whatnot. Change-Id: I436b7619f09911d131f380dd103138c8ff52c0b4 Depends-On: Ia1a1a4adb4dcefed5d7607082e026ca7361f394d --- shade/__init__.py | 91 +++++++----------------- shade/tests/unit/test_caching.py | 43 +++++++++-- shade/tests/unit/test_object.py | 14 ++-- shade/tests/unit/test_operator_noauth.py | 16 +++-- 4 files changed, 84 insertions(+), 80 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 69aecc266..7adb7f0d2 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -27,10 +27,8 @@ from ironicclient import client as ironic_client from ironicclient import exceptions as ironic_exceptions import jsonpatch -import keystoneauth1.exceptions -from keystoneauth1 import loading -from keystoneauth1 import plugin as ksc_plugin -from keystoneauth1 import session as ksc_session +from keystoneauth1 import plugin as ksa_plugin +from keystoneauth1 import session as ksa_session from keystoneclient.v2_0 import client as k2_client from keystoneclient.v3 import client as k3_client from keystoneclient import exceptions as keystone_exceptions @@ -228,10 +226,6 @@ def __init__( self.name = cloud_config.name self.auth = cloud_config.get_auth_args() self.region_name = cloud_config.region_name - self.auth_type = cloud_config.config['auth_type'] - # provide backwards compat to the old name of this plugin - if self.auth_type == 'token_endpoint': - self.auth_type = 'admin_token' self.default_interface = cloud_config.get_interface() self.private = cloud_config.config.get('private', False) self.api_timeout = cloud_config.config['api_timeout'] @@ -307,7 +301,7 @@ def generate_key(*args, **kwargs): def _get_client( self, service_key, client_class, interface_key='endpoint_type', - pass_version_arg=True): + pass_version_arg=True, **kwargs): try: interface = self.cloud_config.get_interface(service_key) # trigger exception on lack of service @@ -317,6 +311,7 @@ def _get_client( service_name=self.cloud_config.get_service_name(service_key), service_type=self.cloud_config.get_service_type(service_key), region_name=self.region_name) + constructor_args.update(kwargs) constructor_args[interface_key] = interface if pass_version_arg: version = self.cloud_config.get_api_version(service_key) @@ -355,28 +350,18 @@ def keystone_session(self): if self._keystone_session is None: try: - loader = loading.get_plugin_loader(self.auth_type) - except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin: - raise OpenStackCloudException( - "No auth plugin named {plugin}".format( - plugin=self.auth_type)) - - try: - keystone_auth = loader.plugin_class(**self.auth) - except Exception as e: - raise OpenStackCloudException( - "Error constructing auth plugin: {plugin} {error}".format( - plugin=self.auth_type, error=str(e))) - - try: - self._keystone_session = ksc_session.Session( + keystone_auth = self.cloud_config.get_auth() + if not keystone_auth: + raise OpenStackCloudException( + "Problem with auth parameters") + self._keystone_session = ksa_session.Session( auth=keystone_auth, verify=self.verify, cert=self.cert, timeout=self.api_timeout) except Exception as e: raise OpenStackCloudException( - "Error authenticating to the keystone: %s " % str(e)) + "Error authenticating to keystone: %s " % str(e)) return self._keystone_session @property @@ -588,7 +573,7 @@ def glance_client(self): def swift_client(self): if self._swift_client is None: try: - token = self.auth_token + token = self.keystone_session.get_token() endpoint = self.get_session_endpoint( service_key='object-store') self._swift_client = swift_client.Connection( @@ -710,7 +695,7 @@ def get_session_endpoint(self, service_key): # keystone is a special case in keystone, because what? if service_key == 'identity': endpoint = self.keystone_session.get_endpoint( - interface=ksc_plugin.AUTH_INTERFACE) + interface=ksa_plugin.AUTH_INTERFACE) else: endpoint = self.keystone_session.get_endpoint( service_type=self.cloud_config.get_service_type( @@ -3210,48 +3195,24 @@ class OperatorCloud(OpenStackCloud): See the :class:`OpenStackCloud` class for a description of most options. """ - @property - def auth_token(self): - if self.auth_type in (None, "None", ''): - # Ironic can operate in no keystone mode. Signify this with a - # token of None. - return None - else: - # Keystone's session will reuse a token if it is still valid. - # We don't need to track validity here, just get_token() each time. - return self.keystone_session.get_token() + # Set the ironic API microversion to a known-good + # supported/tested with the contents of shade. + # + # Note(TheJulia): Defaulted to version 1.6 as the ironic + # state machine changes which will increment the version + # and break an automatic transition of an enrolled node + # to an available state. Locking the version is intended + # to utilize the original transition until shade supports + # calling for node inspection to allow the transition to + # take place automatically. + ironic_api_microversion = '1.6' @property def ironic_client(self): if self._ironic_client is None: - token = self.auth_token - # Set the ironic API microversion to a known-good - # supported/tested with the contents of shade. - # - # Note(TheJulia): Defaulted to version 1.11 as node enrollment - # steps are navigated by the register_machine method. - ironic_api_microversion = '1.11' - - if self.auth_type in (None, "None", ''): - # TODO: This needs to be improved logic wise, perhaps a list, - # or enhancement of the data stuctures with-in the library - # to allow for things aside password authentication, or no - # authentication if so desired by the user. - # - # Attempt to utilize a pre-stored endpoint in the auth - # dict as the endpoint. - endpoint = self.auth['endpoint'] - else: - endpoint = self.get_session_endpoint(service_key='baremetal') - try: - self._ironic_client = ironic_client.Client( - self.cloud_config.get_api_version('baremetal'), - endpoint, token=token, - timeout=self.api_timeout, - os_ironic_api_version=ironic_api_microversion) - except Exception as e: - raise OpenStackCloudException( - "Error in connecting to ironic: %s" % str(e)) + self._ironic_client = self._get_client( + 'baremetal', ironic_client.Client, + os_ironic_api_version=self.ironic_api_microversion) return self._ironic_client def list_nics(self): diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 7d8c33144..fd55186cc 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -13,13 +13,13 @@ # under the License. import tempfile +import keystoneauth1 import mock import os_client_config as occ import testtools import yaml import shade -from shade import exc from shade import meta from shade.tests import fakes from shade.tests.unit import base @@ -402,7 +402,42 @@ class FakeImage(dict): self.assertEqual( [fi, fi2], [dict(x) for x in self.cloud.list_images()]) + +class TestBogusAuth(base.TestCase): + CONFIG = { + 'clouds': + { + '_bogus_test_': + { + 'auth_type': 'bogus', + 'auth': + { + 'auth_url': 'http://198.51.100.1:35357/v2.0', + 'username': '_test_user_', + 'password': '_test_pass_', + 'project_name': '_test_project_', + }, + 'region_name': '_test_region_', + }, + }, + } + + def setUp(self): + super(TestBogusAuth, self).setUp() + + # Isolate os-client-config from test environment + config = tempfile.NamedTemporaryFile(delete=False) + config.write(bytes(yaml.dump(self.CONFIG).encode('utf-8'))) + config.close() + vendor = tempfile.NamedTemporaryFile(delete=False) + vendor.write(b'{}') + vendor.close() + + self.cloud_config = occ.OpenStackConfig(config_files=[config.name], + vendor_files=[vendor.name]) + def test_get_auth_bogus(self): - self.cloud.auth_type = 'bogus' - with testtools.ExpectedException(exc.OpenStackCloudException): - self.cloud.keystone_session + with testtools.ExpectedException( + keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin): + shade.openstack_cloud( + cloud='_bogus_test_', config=self.cloud_config) diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index a7b9a2966..02b94a364 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -34,12 +34,15 @@ def setUp(self): cloud_config=config.get_one_cloud(validate=False)) @mock.patch.object(swift_client, 'Connection') - @mock.patch.object(shade.OpenStackCloud, 'auth_token', + @mock.patch.object(shade.OpenStackCloud, 'keystone_session', new_callable=mock.PropertyMock) @mock.patch.object(shade.OpenStackCloud, 'get_session_endpoint') - def test_swift_client(self, endpoint_mock, auth_mock, swift_mock): + def test_swift_client(self, endpoint_mock, session_mock, swift_mock): endpoint_mock.return_value = 'danzig' - auth_mock.return_value = 'yankee' + session = mock.MagicMock() + session.get_token = mock.MagicMock() + session.get_token.return_value = 'yankee' + session_mock.return_value = session self.cloud.swift_client swift_mock.assert_called_with( preauthurl='danzig', @@ -51,12 +54,11 @@ def test_swift_client(self, endpoint_mock, auth_mock, swift_mock): auth_token='yankee', region_name='')) - @mock.patch.object(shade.OpenStackCloud, 'auth_token', + @mock.patch.object(shade.OpenStackCloud, 'keystone_session', new_callable=mock.PropertyMock) @mock.patch.object(shade.OpenStackCloud, 'get_session_endpoint') - def test_swift_client_no_endpoint(self, endpoint_mock, auth_mock): + def test_swift_client_no_endpoint(self, endpoint_mock, session_mock): endpoint_mock.side_effect = KeyError - auth_mock.return_value = 'quebec' e = self.assertRaises( exc.OpenStackCloudException, lambda: self.cloud.swift_client) self.assertIn( diff --git a/shade/tests/unit/test_operator_noauth.py b/shade/tests/unit/test_operator_noauth.py index f0d35774b..ca5a34730 100644 --- a/shade/tests/unit/test_operator_noauth.py +++ b/shade/tests/unit/test_operator_noauth.py @@ -27,27 +27,33 @@ def setUp(self): URL in the auth data. This is permits testing of the basic mechanism that enables Ironic noauth mode to be utilized with Shade. - - @todo(mordred): remove the token in the next patch - occ handles - this right. """ super(TestShadeOperatorNoAuth, self).setUp() self.cloud_noauth = shade.operator_cloud( auth_type='admin_token', - auth=dict(endpoint="http://localhost:6385", token='foo'), + auth=dict(endpoint="http://localhost:6385"), validate=False, ) + @mock.patch.object(shade.OpenStackCloud, 'keystone_session', + new_callable=mock.PropertyMock) @mock.patch.object(shade.OperatorCloud, 'get_session_endpoint') @mock.patch.object(ironicclient.client, 'Client') def test_ironic_noauth_selection_using_a_task( - self, mock_client, mock_endpoint): + self, mock_client, mock_endpoint, session_mock): """Test noauth selection for Ironic in OperatorCloud Utilize a task to trigger the client connection attempt and evaluate if get_session_endpoint was called while the client was still called. + + We want session_endpoint to be called because we're storing the + endpoint in a noauth token Session object now. """ + session = mock.MagicMock() + session.get_token = mock.MagicMock() + session.get_token.return_value = 'yankee' + session_mock.return_value = session self.cloud_noauth.patch_machine('name', {}) self.assertTrue(mock_endpoint.called) self.assertTrue(mock_client.called) From 20ac0479a41ae267ca6e901559c60d32ab5c258c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 20 Sep 2015 15:53:37 -0400 Subject: [PATCH 0482/3836] Get defaults for image type from occ occ knows default image types for all the clouds. So rather than making people provide them, let's just consume from OCC. Change-Id: Ifd83c26599a82d9aeabf4b3380c1c5cbfc603285 --- shade/__init__.py | 8 ++++++++ shade/tests/unit/test_caching.py | 2 ++ 2 files changed, 10 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 7adb7f0d2..03827d9da 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1276,6 +1276,14 @@ def create_image( disk_format=None, container_format=None, disable_vendor_agent=True, wait=False, timeout=3600, **kwargs): + + if not disk_format: + disk_format = self.cloud_config.config['image_format'] + if not container_format: + if disk_format == 'vhd': + container_format = 'ovf' + else: + container_format = 'bare' if not md5 or not sha256: (md5, sha256) = self._get_file_hashes(filename) current_image = self.get_image(name) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index fd55186cc..9d6d1d5d6 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -275,6 +275,7 @@ def test_create_image_put_v1(self, glance_mock, mock_api_version): glance_mock.images.list.return_value = [fake_image] self._call_create_image('42 name') args = {'name': '42 name', + 'container_format': 'bare', 'disk_format': 'qcow2', 'properties': {'owner_specified.shade.md5': mock.ANY, 'owner_specified.shade.sha256': mock.ANY}} glance_mock.images.create.assert_called_with(**args) @@ -297,6 +298,7 @@ def test_create_image_put_v2(self, glance_mock, mock_api_version): glance_mock.images.list.return_value = [fake_image] self._call_create_image('42 name') args = {'name': '42 name', + 'container_format': 'bare', 'disk_format': 'qcow2', 'owner_specified.shade.md5': mock.ANY, 'owner_specified.shade.sha256': mock.ANY} glance_mock.images.create.assert_called_with(**args) From 510561304629ca0c750aa5e0ce37510933048066 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 22 Sep 2015 13:45:21 -0500 Subject: [PATCH 0483/3836] Handle glance v1 and v2 difference with is_public Glance v2 changed is_public=bool to visibility=(public|private) Change-Id: I596788dde1a8d01ae8c0908f48d419ea0fc875b5 --- shade/__init__.py | 12 ++++++++++++ shade/tests/unit/test_caching.py | 12 ++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 03827d9da..a971637da 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1298,6 +1298,18 @@ def create_image( if disable_vendor_agent: kwargs.update(self.cloud_config.config['disable_vendor_agent']) + # We can never have nice things. Glance v1 took "is_public" as a + # boolean. Glance v2 takes "visibility". If the user gives us + # is_public, we know what they mean. If they give us visibility, they + # know that they mean. + if self.cloud_config.get_api_version('image') == '2': + if 'is_public' in kwargs: + is_public = kwargs.pop('is_public') + if is_public: + kwargs['visibility'] = 'public' + else: + kwargs['visibility'] = 'private' + try: # This makes me want to die inside if self.image_api_use_tasks: diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 9d6d1d5d6..8b5771762 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -261,7 +261,8 @@ def _call_create_image(self, name, container=None): imagefile.write(b'\0') imagefile.close() self.cloud.create_image( - name, imagefile.name, container=container, wait=True) + name, imagefile.name, container=container, wait=True, + is_public=False) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, 'glance_client') @@ -277,7 +278,8 @@ def test_create_image_put_v1(self, glance_mock, mock_api_version): args = {'name': '42 name', 'container_format': 'bare', 'disk_format': 'qcow2', 'properties': {'owner_specified.shade.md5': mock.ANY, - 'owner_specified.shade.sha256': mock.ANY}} + 'owner_specified.shade.sha256': mock.ANY, + 'is_public': False}} glance_mock.images.create.assert_called_with(**args) glance_mock.images.update.assert_called_with(data=mock.ANY, image=fake_image) @@ -300,7 +302,8 @@ def test_create_image_put_v2(self, glance_mock, mock_api_version): args = {'name': '42 name', 'container_format': 'bare', 'disk_format': 'qcow2', 'owner_specified.shade.md5': mock.ANY, - 'owner_specified.shade.sha256': mock.ANY} + 'owner_specified.shade.sha256': mock.ANY, + 'visibility': 'private'} glance_mock.images.create.assert_called_with(**args) glance_mock.images.upload.assert_called_with( image_data=mock.ANY, image_id=fake_image.id) @@ -378,7 +381,8 @@ class FakeTask(dict): 'image_properties': {'name': '99 name'}}) args = {'owner_specified.shade.md5': fake_md5, 'owner_specified.shade.sha256': fake_sha256, - 'image_id': '99'} + 'image_id': '99', + 'visibility': 'private'} glance_mock.images.update.assert_called_with(**args) fake_image_dict = meta.obj_to_dict(fake_image) self.assertEqual([fake_image_dict], self.cloud.list_images()) From f3c43e826e9069d2b726518f2780f3db6af39425 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 23 Sep 2015 12:01:19 -0400 Subject: [PATCH 0484/3836] Allow creating externally accessible networks Add an option to create_network() that lets the user create a network that is externally accessible. By default, they will NOT be externally accessible, since this may require special privileges. Change-Id: Ic9a3d05ac15399e55fa078b61fdb454ee9f56e69 --- shade/__init__.py | 13 +++-- shade/tests/functional/test_network.py | 74 ++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 shade/tests/functional/test_network.py diff --git a/shade/__init__.py b/shade/__init__.py index 12d9999d4..ddb3fc249 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1093,12 +1093,14 @@ def delete_keypair(self, name): # TODO(Shrews): This will eventually need to support tenant ID and # provider networks, which are admin-level params. - def create_network(self, name, shared=False, admin_state_up=True): + def create_network(self, name, shared=False, admin_state_up=True, + external=False): """Create a network. - :param name: Name of the network being created. - :param shared: Set the network as shared. - :param admin_state_up: Set the network administrative state to up. + :param string name: Name of the network being created. + :param bool shared: Set the network as shared. + :param bool admin_state_up: Set the network administrative state to up. + :param bool external: Whether this network is externally accessible. :returns: The network object. :raises: OpenStackCloudException on operation error. @@ -1107,7 +1109,8 @@ def create_network(self, name, shared=False, admin_state_up=True): network = { 'name': name, 'shared': shared, - 'admin_state_up': admin_state_up + 'admin_state_up': admin_state_up, + 'router:external': external } with self._neutron_exceptions( diff --git a/shade/tests/functional/test_network.py b/shade/tests/functional/test_network.py new file mode 100644 index 000000000..cb0a5da34 --- /dev/null +++ b/shade/tests/functional/test_network.py @@ -0,0 +1,74 @@ +# -*- 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. + +""" +test_network +---------------------------------- + +Functional tests for `shade` network methods. +""" + +import random +import string + +from shade import openstack_cloud +from shade.exc import OpenStackCloudException +from shade.tests import base + + +class TestNetwork(base.TestCase): + def setUp(self): + super(TestNetwork, self).setUp() + self.cloud = openstack_cloud(cloud='devstack-admin') + if not self.cloud.has_service('network'): + self.skipTest('Network service not supported by cloud') + self.network_prefix = 'test_network' + ''.join( + random.choice(string.ascii_lowercase) for _ in range(5)) + self.addCleanup(self._cleanup_networks) + + def _cleanup_networks(self): + exception_list = list() + for network in self.cloud.list_networks(): + if network['name'].startswith(self.network_prefix): + try: + self.cloud.delete_network(network['name']) + except Exception as e: + exception_list.append(str(e)) + continue + + if exception_list: + raise OpenStackCloudException('\n'.join(exception_list)) + + def test_create_network_basic(self): + net1_name = self.network_prefix + '_net1' + net1 = self.cloud.create_network(name=net1_name) + self.assertIn('id', net1) + self.assertEqual(net1_name, net1['name']) + self.assertFalse(net1['shared']) + self.assertFalse(net1['router:external']) + self.assertTrue(net1['admin_state_up']) + + def test_create_network_advanced(self): + net1_name = self.network_prefix + '_net1' + net1 = self.cloud.create_network( + name=net1_name, + shared=True, + external=True, + admin_state_up=False, + ) + self.assertIn('id', net1) + self.assertEqual(net1_name, net1['name']) + self.assertTrue(net1['router:external']) + self.assertTrue(net1['shared']) + self.assertFalse(net1['admin_state_up']) From 7ccb6f89132e5c7629227fa6afc2a57ba70df822 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 23 Sep 2015 15:10:59 -0400 Subject: [PATCH 0485/3836] Allow more complex router creation Turns out, there are a lot of other options when creating a router. Let's allow those. Also, functional tests are a good idea. Change-Id: Ia9382898decec2a1d3485484e643b50785d235cf --- shade/__init__.py | 38 ++++++- shade/tests/functional/test_router.py | 158 ++++++++++++++++++++++++++ 2 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 shade/tests/functional/test_router.py diff --git a/shade/__init__.py b/shade/__init__.py index ddb3fc249..8ccaaa645 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1143,11 +1143,38 @@ def delete_network(self, name_or_id): return True - def create_router(self, name=None, admin_state_up=True): + def _build_external_gateway_info(self, ext_gateway_net_id, enable_snat, + ext_fixed_ips): + info = {} + if ext_gateway_net_id: + info['network_id'] = ext_gateway_net_id + if enable_snat is not None: + info['enable_snat'] = enable_snat + if ext_fixed_ips: + info['external_fixed_ips'] = ext_fixed_ips + if info: + return info + return None + + def create_router(self, name=None, admin_state_up=True, + ext_gateway_net_id=None, enable_snat=None, + ext_fixed_ips=None): """Create a logical router. - :param name: The router name. - :param admin_state_up: The administrative state of the router. + :param string name: The router name. + :param bool admin_state_up: The administrative state of the router. + :param string ext_gateway_net_id: Network ID for the external gateway. + :param bool enable_snat: Enable Source NAT (SNAT) attribute. + :param list ext_fixed_ips: + List of dictionaries of desired IP and/or subnet on the + external network. Example:: + + [ + { + "subnet_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b", + "ip_address": "192.168.10.2" + } + ] :returns: The router object. :raises: OpenStackCloudException on operation error. @@ -1157,6 +1184,11 @@ def create_router(self, name=None, admin_state_up=True): } if name: router['name'] = name + ext_gw_info = self._build_external_gateway_info( + ext_gateway_net_id, enable_snat, ext_fixed_ips + ) + if ext_gw_info: + router['external_gateway_info'] = ext_gw_info with self._neutron_exceptions( "Error creating router {0}".format(name)): diff --git a/shade/tests/functional/test_router.py b/shade/tests/functional/test_router.py new file mode 100644 index 000000000..c0c27ea35 --- /dev/null +++ b/shade/tests/functional/test_router.py @@ -0,0 +1,158 @@ +# -*- 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. + +""" +test_router +---------------------------------- + +Functional tests for `shade` router methods. +""" + +import random +import string + +from shade import openstack_cloud +from shade.exc import OpenStackCloudException +from shade.tests import base + + +EXPECTED_TOPLEVEL_FIELDS = ( + 'id', 'name', 'admin_state_up', 'external_gateway_info', + 'tenant_id', 'routes', 'status' +) + +EXPECTED_GW_INFO_FIELDS = ('network_id', 'enable_snat', 'external_fixed_ips') + + +class TestRouter(base.TestCase): + def setUp(self): + super(TestRouter, self).setUp() + self.cloud = openstack_cloud(cloud='devstack-admin') + if not self.cloud.has_service('network'): + self.skipTest('Network service not supported by cloud') + + self.router_prefix = 'test_router' + ''.join( + random.choice(string.ascii_lowercase) for _ in range(5)) + self.network_prefix = 'test_network' + ''.join( + random.choice(string.ascii_lowercase) for _ in range(5)) + self.subnet_prefix = 'test_subnet' + ''.join( + random.choice(string.ascii_lowercase) for _ in range(5)) + + # NOTE(Shrews): Order matters! + self.addCleanup(self._cleanup_networks) + self.addCleanup(self._cleanup_subnets) + self.addCleanup(self._cleanup_routers) + + def _cleanup_routers(self): + exception_list = list() + for router in self.cloud.list_routers(): + if router['name'].startswith(self.router_prefix): + try: + self.cloud.delete_router(router['name']) + except Exception as e: + exception_list.append(str(e)) + continue + + if exception_list: + raise OpenStackCloudException('\n'.join(exception_list)) + + def _cleanup_networks(self): + exception_list = list() + for network in self.cloud.list_networks(): + if network['name'].startswith(self.network_prefix): + try: + self.cloud.delete_network(network['name']) + except Exception as e: + exception_list.append(str(e)) + continue + + if exception_list: + raise OpenStackCloudException('\n'.join(exception_list)) + + def _cleanup_subnets(self): + exception_list = list() + for subnet in self.cloud.list_subnets(): + if subnet['name'].startswith(self.subnet_prefix): + try: + self.cloud.delete_subnet(subnet['id']) + except Exception as e: + exception_list.append(str(e)) + continue + + if exception_list: + raise OpenStackCloudException('\n'.join(exception_list)) + + def test_create_router_basic(self): + net1_name = self.network_prefix + '_net1' + net1 = self.cloud.create_network(name=net1_name, external=True) + + router_name = self.router_prefix + '_create_basic' + router = self.cloud.create_router( + name=router_name, + admin_state_up=True, + ext_gateway_net_id=net1['id'], + ) + + for field in EXPECTED_TOPLEVEL_FIELDS: + self.assertIn(field, router) + + ext_gw_info = router['external_gateway_info'] + for field in EXPECTED_GW_INFO_FIELDS: + self.assertIn(field, ext_gw_info) + + self.assertEqual(router_name, router['name']) + self.assertEqual('ACTIVE', router['status']) + self.assertEqual(net1['id'], ext_gw_info['network_id']) + self.assertTrue(ext_gw_info['enable_snat']) + + def test_create_router_advanced(self): + net1_name = self.network_prefix + '_net1' + sub1_name = self.subnet_prefix + '_sub1' + net1 = self.cloud.create_network(name=net1_name, external=True) + sub1 = self.cloud.create_subnet( + net1['id'], '10.5.5.0/24', subnet_name=sub1_name, + gateway_ip='10.5.5.1' + ) + + router_name = self.router_prefix + '_create_full' + router = self.cloud.create_router( + name=router_name, + admin_state_up=False, + ext_gateway_net_id=net1['id'], + enable_snat=False, + ext_fixed_ips=[ + {'subnet_id': sub1['id'], 'ip_address': '10.5.5.99'} + ] + ) + + for field in EXPECTED_TOPLEVEL_FIELDS: + self.assertIn(field, router) + + ext_gw_info = router['external_gateway_info'] + for field in EXPECTED_GW_INFO_FIELDS: + self.assertIn(field, ext_gw_info) + + self.assertEqual(router_name, router['name']) + self.assertEqual('ACTIVE', router['status']) + self.assertFalse(router['admin_state_up']) + + self.assertEqual(1, len(ext_gw_info['external_fixed_ips'])) + self.assertEqual( + sub1['id'], + ext_gw_info['external_fixed_ips'][0]['subnet_id'] + ) + self.assertEqual( + '10.5.5.99', + ext_gw_info['external_fixed_ips'][0]['ip_address'] + ) From 14831e613dca4f3760e6bc285ac344f7821aabea Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 23 Sep 2015 17:58:32 -0400 Subject: [PATCH 0486/3836] Allow more complex router updates There are other attributes of a router that can be updated. Change-Id: Ia71301994122524b5d04bf788dab08a5b84ac85f --- shade/__init__.py | 34 +++++++--- shade/tests/functional/test_router.py | 89 ++++++++++++++++++++++++++- 2 files changed, 111 insertions(+), 12 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 8ccaaa645..aea72b637 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1200,13 +1200,26 @@ def create_router(self, name=None, admin_state_up=True, return new_router['router'] def update_router(self, name_or_id, name=None, admin_state_up=None, - ext_gateway_net_id=None): + ext_gateway_net_id=None, enable_snat=None, + ext_fixed_ips=None): """Update an existing logical router. - :param name_or_id: The name or UUID of the router to update. - :param name: The new router name. - :param admin_state_up: The administrative state of the router. - :param ext_gateway_net_id: The network ID for the external gateway. + :param string name_or_id: The name or UUID of the router to update. + :param string name: The new router name. + :param bool admin_state_up: The administrative state of the router. + :param string ext_gateway_net_id: + The network ID for the external gateway. + :param bool enable_snat: Enable Source NAT (SNAT) attribute. + :param list ext_fixed_ips: + List of dictionaries of desired IP and/or subnet on the + external network. Example:: + + [ + { + "subnet_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b", + "ip_address": "192.168.10.2" + } + ] :returns: The router object. :raises: OpenStackCloudException on operation error. @@ -1214,12 +1227,13 @@ def update_router(self, name_or_id, name=None, admin_state_up=None, router = {} if name: router['name'] = name - if admin_state_up: + if admin_state_up is not None: router['admin_state_up'] = admin_state_up - if ext_gateway_net_id: - router['external_gateway_info'] = { - 'network_id': ext_gateway_net_id - } + ext_gw_info = self._build_external_gateway_info( + ext_gateway_net_id, enable_snat, ext_fixed_ips + ) + if ext_gw_info: + router['external_gateway_info'] = ext_gw_info if not router: self.log.debug("No router data to update") diff --git a/shade/tests/functional/test_router.py b/shade/tests/functional/test_router.py index c0c27ea35..22b9e21c7 100644 --- a/shade/tests/functional/test_router.py +++ b/shade/tests/functional/test_router.py @@ -116,7 +116,7 @@ def test_create_router_basic(self): self.assertEqual(net1['id'], ext_gw_info['network_id']) self.assertTrue(ext_gw_info['enable_snat']) - def test_create_router_advanced(self): + def _create_and_verify_advanced_router(self): net1_name = self.network_prefix + '_net1' sub1_name = self.subnet_prefix + '_sub1' net1 = self.cloud.create_network(name=net1_name, external=True) @@ -125,7 +125,7 @@ def test_create_router_advanced(self): gateway_ip='10.5.5.1' ) - router_name = self.router_prefix + '_create_full' + router_name = self.router_prefix + '_create_advanced' router = self.cloud.create_router( name=router_name, admin_state_up=False, @@ -156,3 +156,88 @@ def test_create_router_advanced(self): '10.5.5.99', ext_gw_info['external_fixed_ips'][0]['ip_address'] ) + + return router + + def test_create_router_advanced(self): + self._create_and_verify_advanced_router() + + def test_update_router_name(self): + router = self._create_and_verify_advanced_router() + + new_name = self.router_prefix + '_update_name' + updated = self.cloud.update_router(router['id'], name=new_name) + self.assertIsNotNone(updated) + + for field in EXPECTED_TOPLEVEL_FIELDS: + self.assertIn(field, updated) + + # Name is the only change we expect + self.assertEqual(new_name, updated['name']) + + # Validate nothing else changed + self.assertEqual(router['status'], updated['status']) + self.assertEqual(router['admin_state_up'], updated['admin_state_up']) + self.assertEqual(router['external_gateway_info'], + updated['external_gateway_info']) + + def test_update_router_admin_state(self): + router = self._create_and_verify_advanced_router() + + updated = self.cloud.update_router(router['id'], + admin_state_up=True) + self.assertIsNotNone(updated) + + for field in EXPECTED_TOPLEVEL_FIELDS: + self.assertIn(field, updated) + + # admin_state_up is the only change we expect + self.assertTrue(updated['admin_state_up']) + self.assertNotEqual(router['admin_state_up'], + updated['admin_state_up']) + + # Validate nothing else changed + self.assertEqual(router['status'], updated['status']) + self.assertEqual(router['name'], updated['name']) + self.assertEqual(router['external_gateway_info'], + updated['external_gateway_info']) + + def test_update_router_ext_gw_info(self): + router = self._create_and_verify_advanced_router() + + # create a new subnet + existing_net_id = router['external_gateway_info']['network_id'] + sub_name = self.subnet_prefix + '_update' + sub = self.cloud.create_subnet( + existing_net_id, '10.6.6.0/24', subnet_name=sub_name, + gateway_ip='10.6.6.1' + ) + + updated = self.cloud.update_router( + router['id'], + ext_gateway_net_id=existing_net_id, + ext_fixed_ips=[ + {'subnet_id': sub['id'], 'ip_address': '10.6.6.77'} + ] + ) + self.assertIsNotNone(updated) + + for field in EXPECTED_TOPLEVEL_FIELDS: + self.assertIn(field, updated) + + # external_gateway_info is the only change we expect + ext_gw_info = updated['external_gateway_info'] + self.assertEqual(1, len(ext_gw_info['external_fixed_ips'])) + self.assertEqual( + sub['id'], + ext_gw_info['external_fixed_ips'][0]['subnet_id'] + ) + self.assertEqual( + '10.6.6.77', + ext_gw_info['external_fixed_ips'][0]['ip_address'] + ) + + # Validate nothing else changed + self.assertEqual(router['status'], updated['status']) + self.assertEqual(router['name'], updated['name']) + self.assertEqual(router['admin_state_up'], updated['admin_state_up']) From 4636dd915df1fdcee67ca969b7c17143ddd2ae2c Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Fri, 18 Sep 2015 17:14:43 -0700 Subject: [PATCH 0487/3836] Add more info to some exceptions And remove accompaning redundant debug lines. Change-Id: Ie022a11417c7535c5c9faf43abd96ad07933678a --- shade/__init__.py | 115 ++++++++++++++++++++-------------------------- 1 file changed, 49 insertions(+), 66 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index e35a789f3..905e049bd 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -813,7 +813,7 @@ def list_keypairs(self): ) except Exception as e: raise OpenStackCloudException( - "Error fetching keypair list: %s" % e) + "Error fetching keypair list: %s" % str(e)) def list_networks(self): with self._neutron_exceptions("Error fetching network list"): @@ -3755,11 +3755,10 @@ def node_set_provision_state(self, name_or_id, state, configdrive=None): configdrive=configdrive)) ) except Exception as e: - self.log.debug( - "Baremetal machine node failed change provision state to %s" - % state, - exc_info=True) - raise OpenStackCloudException(str(e)) + raise OpenStackCloudException( + "Baremetal machine node failed change provision" + " state to {state}: {msg}".format(state=state, + msg=str(e))) def set_machine_maintenance_state( self, @@ -3795,21 +3794,15 @@ def set_machine_maintenance_state( _tasks.MachineSetMaintenance(node_id=name_or_id, state='false')) if result is not None: - self.log.debug( - "Failed setting machine maintenance state on node %s. " - "User requested '%s'.' Received: %s" % ( - name_or_id, state, result)) raise OpenStackCloudException( - "Failed setting machine maintenance state on node %s. " - "Received: %s" % (name_or_id, result)) + "Failed setting machine maintenance state to %s " + "on node %s. Received: %s" % ( + state, name_or_id, result)) return None except Exception as e: - self.log.debug( - "failed setting maintenance state on node %s" % name_or_id, - exc_info=True) raise OpenStackCloudException( - "Error setting machine maintenance on node %s. " - "state: %s" % (name_or_id, str(e))) + "Error setting machine maintenance state to %s " + "on node %s: %s" % (state, name_or_id, str(e))) def remove_machine_from_maintenance(self, name_or_id): """Remove Baremetal Machine from Maintenance State @@ -3851,22 +3844,14 @@ def _set_machine_power_state(self, name_or_id, state): _tasks.MachineSetPower(node_id=name_or_id, state=state)) if power is not None: - self.log.debug( - "Failed setting machine power state on node %s. User " - "requested '%s'.' Received: %s" % ( - name_or_id, state, power)) raise OpenStackCloudException( - "Failed setting machine power state on node %s. " - "Received: %s" % (name_or_id, power)) + "Failed setting machine power state %s on node %s. " + "Received: %s" % (state, name_or_id, power)) return None except Exception as e: - self.log.debug( - "Error setting machine power state on node %s. User " - "requested '%s'.'" % (name_or_id, state), - exc_info=True) raise OpenStackCloudException( - "Error setting machine power state on node %s. " - "Error: %s" % (name_or_id, str(e))) + "Error setting machine power state %s on node %s. " + "Error: %s" % (state, name_or_id, str(e))) def set_machine_power_on(self, name_or_id): """Activate baremetal machine power @@ -3964,10 +3949,9 @@ def create_service(self, name, service_type, description=None): name=name, service_type=service_type, description=description)) except Exception as e: - self.log.debug( - "Failed to create service {name}".format(name=name), - exc_info=True) - raise OpenStackCloudException(str(e)) + raise OpenStackCloudException( + "Failed to create service {name}: {msg}".format( + name=name, msg=str(e))) return meta.obj_to_dict(service) def list_services(self): @@ -4036,10 +4020,10 @@ def delete_service(self, name_or_id): try: self.manager.submitTask(_tasks.ServiceDelete(id=service['id'])) except Exception as e: - self.log.debug( - "Failed to delete service {id}".format(id=service['id']), - exc_info=True) - raise OpenStackCloudException(str(e)) + raise OpenStackCloudException( + "Failed to delete service {id}: {msg}".format( + id=service['id'], + msg=str(e))) return True def create_endpoint(self, service_name_or_id, public_url, @@ -4071,10 +4055,10 @@ def create_endpoint(self, service_name_or_id, public_url, adminurl=admin_url )) except Exception as e: - self.log.debug( - "Failed to create endpoint for service {service}".format( - service=service['name']), exc_info=True) - raise OpenStackCloudException(str(e)) + raise OpenStackCloudException( + "Failed to create endpoint for service {service}: " + "{msg}".format(service=service['name'], + msg=str(e))) return meta.obj_to_dict(endpoint) def list_endpoints(self): @@ -4089,8 +4073,8 @@ def list_endpoints(self): try: endpoints = self.manager.submitTask(_tasks.EndpointList()) except Exception as e: - self.log.debug("Failed to list endpoints", exc_info=True) - raise OpenStackCloudException(str(e)) + raise OpenStackCloudException("Failed to list endpoints: {msg}" + .format(msg=str(e))) return meta.obj_list_to_dict(endpoints) def search_endpoints(self, id=None, filters=None): @@ -4150,10 +4134,10 @@ def delete_endpoint(self, id): try: self.manager.submitTask(_tasks.EndpointDelete(id=id)) except Exception as e: - self.log.debug( - "Failed to delete endpoint {id}".format(id=id), - exc_info=True) - raise OpenStackCloudException(str(e)) + raise OpenStackCloudException( + "Failed to delete endpoint {id}: {msg}".format( + id=id, + msg=str(e))) return True def create_identity_domain( @@ -4174,10 +4158,9 @@ def create_identity_domain( description=description, enabled=enabled)) except Exception as e: - self.log.debug( - "Failed to create domain {name}".format( - name=name), exc_info=True) - raise OpenStackCloudException(str(e)) + raise OpenStackCloudException( + "Failed to create domain {name}".format(name=name, + msg=str(e))) return meta.obj_to_dict(domain) def update_identity_domain( @@ -4209,10 +4192,9 @@ def delete_identity_domain(self, domain_id): self.manager.submitTask(_tasks.IdentityDomainDelete( domain=domain['id'])) except Exception as e: - self.log.debug( - "Failed to delete domain {id}".format(id=domain_id), - exc_info=True) - raise OpenStackCloudException(str(e)) + raise OpenStackCloudException( + "Failed to delete domain {id}: {msg}".format(id=domain_id, + msg=str(e))) def list_identity_domains(self): """List Keystone domains. @@ -4225,8 +4207,8 @@ def list_identity_domains(self): try: domains = self.manager.submitTask(_tasks.IdentityDomainList()) except Exception as e: - self.log.debug("Failed to list domains", exc_info=True) - raise OpenStackCloudException(str(e)) + raise OpenStackCloudException("Failed to list domains: {msg}" + .format(msg=str(e))) return meta.obj_list_to_dict(domains) def search_identity_domains(self, **filters): @@ -4248,8 +4230,8 @@ def search_identity_domains(self, **filters): domains = self.manager.submitTask( _tasks.IdentityDomainList(**filters)) except Exception as e: - self.log.debug("Failed to list domains", exc_info=True) - raise OpenStackCloudException(str(e)) + raise OpenStackCloudException("Failed to list domains: {msg}" + .format(msg=str(e))) return meta.obj_list_to_dict(domains) def get_identity_domain(self, domain_id): @@ -4270,7 +4252,10 @@ def get_identity_domain(self, domain_id): domain = self.manager.submitTask( _tasks.IdentityDomainGet(domain=domain_id)) except Exception as e: - self.log.debug("Failed to get domain", exc_info=True) + raise OpenStackCloudException( + "Failed to get domain {domain_id}: {msg}".format( + domain_id=domain_id, + msg=str(e))) raise OpenStackCloudException(str(e)) return meta.obj_to_dict(domain) @@ -4300,10 +4285,10 @@ def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", is_public=is_public) ) except Exception as e: - self.log.debug( - "Failed to create flavor {0}".format(name), - exc_info=True) - raise OpenStackCloudException(str(e)) + raise OpenStackCloudException( + "Failed to create flavor {name}: {msg}".format( + name=name, + msg=str(e))) return meta.obj_to_dict(flavor) def delete_flavor(self, name_or_id): @@ -4324,8 +4309,6 @@ def delete_flavor(self, name_or_id): try: self.manager.submitTask(_tasks.FlavorDelete(flavor=flavor['id'])) except Exception as e: - self.log.debug("Error deleting flavor {0}".format(name_or_id), - exc_info=True) raise OpenStackCloudException( "Unable to delete flavor {0}: {1}".format(name_or_id, e) ) From a0e1ae1b54c78fc0b108ed0e789af7421942c101 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 17 Sep 2015 14:47:22 +0200 Subject: [PATCH 0488/3836] Trap exceptions in helper functions os-client-config can bubble up a keystoneauth1 error now since that's where auth plugin parameter validation happens. However, consumers of the helper functions will not be expecting such a thing as the helper function is hiding the sequence from them. Change-Id: I84ececc71764b3f345b0d507d419260aef720783 --- shade/__init__.py | 37 +++++++++++++++++++++----------- shade/tests/unit/test_caching.py | 5 ++--- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index e35a789f3..f88585e11 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -27,6 +27,7 @@ from ironicclient import client as ironic_client from ironicclient import exceptions as ironic_exceptions import jsonpatch +import keystoneauth1.exceptions from keystoneauth1 import plugin as ksa_plugin from keystoneauth1 import session as ksa_session from keystoneclient.v2_0 import client as k2_client @@ -113,22 +114,30 @@ def simple_logging(debug=False): def openstack_clouds(config=None, debug=False): if not config: config = os_client_config.OpenStackConfig() - return [ - OpenStackCloud( - cloud=f.name, debug=debug, - cache_interval=config.get_cache_max_age(), - cache_class=config.get_cache_class(), - cache_arguments=config.get_cache_arguments(), - cloud_config=f, - **f.config) - for f in config.get_all_clouds() - ] + try: + return [ + OpenStackCloud( + cloud=f.name, debug=debug, + cache_interval=config.get_cache_max_age(), + cache_class=config.get_cache_class(), + cache_arguments=config.get_cache_arguments(), + cloud_config=f, + **f.config) + for f in config.get_all_clouds() + ] + except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin as e: + raise OpenStackCloudException( + "Invalid cloud configuration: {exc}".format(exc=str(e))) def openstack_cloud(config=None, **kwargs): if not config: config = os_client_config.OpenStackConfig() - cloud_config = config.get_one_cloud(**kwargs) + try: + cloud_config = config.get_one_cloud(**kwargs) + except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin as e: + raise OpenStackCloudException( + "Invalid cloud configuration: {exc}".format(exc=str(e))) return OpenStackCloud( cache_interval=config.get_cache_max_age(), cache_class=config.get_cache_class(), @@ -141,7 +150,11 @@ def operator_cloud(config=None, **kwargs): kwargs['interface'] = 'admin' if not config: config = os_client_config.OpenStackConfig() - cloud_config = config.get_one_cloud(**kwargs) + try: + cloud_config = config.get_one_cloud(**kwargs) + except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin as e: + raise OpenStackCloudException( + "Invalid cloud configuration: {exc}".format(exc=str(e))) return OperatorCloud( cache_interval=config.get_cache_max_age(), cache_class=config.get_cache_class(), diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 8b5771762..48c80f51b 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -13,13 +13,13 @@ # under the License. import tempfile -import keystoneauth1 import mock import os_client_config as occ import testtools import yaml import shade +from shade import exc from shade import meta from shade.tests import fakes from shade.tests.unit import base @@ -443,7 +443,6 @@ def setUp(self): vendor_files=[vendor.name]) def test_get_auth_bogus(self): - with testtools.ExpectedException( - keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin): + with testtools.ExpectedException(exc.OpenStackCloudException): shade.openstack_cloud( cloud='_bogus_test_', config=self.cloud_config) From 0ff48d2b946631668645754b9c9820664a085200 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 25 Sep 2015 15:20:43 -0400 Subject: [PATCH 0489/3836] Add router ansible test and update network role Adds a new playbook for testing creating and updating a router. Also updates network playbook for new 'external' parameter. Change-Id: Ia0b11ebbad2d8f5701754a25267aecf1accc69ba --- .../ansible/roles/network/tasks/main.yml | 2 + .../tests/ansible/roles/network/vars/main.yml | 2 + .../tests/ansible/roles/router/tasks/main.yml | 68 +++++++++++++++++++ .../tests/ansible/roles/router/vars/main.yml | 1 + shade/tests/ansible/run.yml | 1 + 5 files changed, 74 insertions(+) create mode 100644 shade/tests/ansible/roles/router/tasks/main.yml create mode 100644 shade/tests/ansible/roles/router/vars/main.yml diff --git a/shade/tests/ansible/roles/network/tasks/main.yml b/shade/tests/ansible/roles/network/tasks/main.yml index fb6ca5726..8a85c25cc 100644 --- a/shade/tests/ansible/roles/network/tasks/main.yml +++ b/shade/tests/ansible/roles/network/tasks/main.yml @@ -4,6 +4,8 @@ cloud: "{{ cloud }}" name: "{{ network_name }}" state: present + shared: "{{ network_shared }}" + external: "{{ network_external }}" - name: Delete network os_network: diff --git a/shade/tests/ansible/roles/network/vars/main.yml b/shade/tests/ansible/roles/network/vars/main.yml index 4b16af49d..d5435ecb1 100644 --- a/shade/tests/ansible/roles/network/vars/main.yml +++ b/shade/tests/ansible/roles/network/vars/main.yml @@ -1 +1,3 @@ network_name: shade_network +network_shared: false +network_external: false diff --git a/shade/tests/ansible/roles/router/tasks/main.yml b/shade/tests/ansible/roles/router/tasks/main.yml new file mode 100644 index 000000000..e4ecba612 --- /dev/null +++ b/shade/tests/ansible/roles/router/tasks/main.yml @@ -0,0 +1,68 @@ +--- +- name: Create network + os_network: + cloud: "{{ cloud }}" + state: present + name: "{{ network_name }}" + external: true + +- name: Create subnet1 + os_subnet: + cloud: "{{ cloud }}" + state: present + network_name: "{{ network_name }}" + name: shade_subnet1 + cidr: 10.6.6.0/24 + +- name: Create subnet2 + os_subnet: + cloud: "{{ cloud }}" + state: present + network_name: "{{ network_name }}" + name: shade_subnet2 + cidr: 10.7.7.0/24 + +- name: Create router + os_router: + cloud: "{{ cloud }}" + state: present + name: "{{ router_name }}" + network: "{{ network_name }}" + interfaces: + - subnet: shade_subnet1 + ip: 10.6.6.99 + +- name: Update router + os_router: + cloud: "{{ cloud }}" + state: present + name: "{{ router_name }}" + network: "{{ network_name }}" + interfaces: + - subnet: shade_subnet1 + - subnet: shade_subnet2 + ip: 10.7.7.99 + +- name: Delete router + os_router: + cloud: "{{ cloud }}" + state: absent + name: "{{ router_name }}" + +- name: Delete subnet1 + os_subnet: + cloud: "{{ cloud }}" + state: absent + name: shade_subnet1 + +- name: Delete subnet2 + os_subnet: + cloud: "{{ cloud }}" + state: absent + name: shade_subnet2 + +- name: Delete network + os_network: + cloud: "{{ cloud }}" + state: absent + name: "{{ network_name }}" diff --git a/shade/tests/ansible/roles/router/vars/main.yml b/shade/tests/ansible/roles/router/vars/main.yml new file mode 100644 index 000000000..0990d1a59 --- /dev/null +++ b/shade/tests/ansible/roles/router/vars/main.yml @@ -0,0 +1 @@ +router_name: ansible_router diff --git a/shade/tests/ansible/run.yml b/shade/tests/ansible/run.yml index 7e927b2ac..e4bdf231d 100644 --- a/shade/tests/ansible/run.yml +++ b/shade/tests/ansible/run.yml @@ -10,3 +10,4 @@ - { role: network, tags: network } - { role: security_group, tags: security_group } - { role: subnet, tags: subnet} + - { role: router, tags: router} From 8822b7ad560fc4d9ecdd7397827f7798698b1338 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Sat, 26 Sep 2015 13:19:19 -0400 Subject: [PATCH 0490/3836] Fix baremetal port deletion Shade's node deletion logic removes ports one at a time in the requested order, however that logic has been silently failing to the API for some time now as we're inadvertently returning the entire port object back to the API as the port we wish to delete instead of the port UUID. Changed to return the port UUID value to the delete call. Added code to unit test to ensure that correct action is taken. Closes-Bug: #1500063 Change-Id: I01c16990dc66988fab29c41ec4b4e6094213b2ca --- shade/__init__.py | 4 ++-- shade/tests/unit/test_shade_operator.py | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 96c614aa8..e616dab37 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -3544,10 +3544,10 @@ def unregister_machine(self, nics, uuid, wait=False, timeout=600): for nic in nics: try: - port_id = self.manager.submitTask( + port = self.manager.submitTask( _tasks.MachinePortGetByAddress(address=nic['mac'])) self.manager.submitTask( - _tasks.MachinePortDelete(port_id=port_id)) + _tasks.MachinePortDelete(port_id=port.uuid)) except Exception as e: raise OpenStackCloudException( "Error removing NIC '%s' from baremetal API for " diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index abe41b263..a7537349b 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -530,13 +530,22 @@ def test_unregister_machine(self, mock_client): class fake_node: provision_state = 'available' + class fake_port: + uuid = '00000000-0000-0000-0000-000000000001' + + mock_client.port.get_by_address.return_value = fake_port mock_client.node.get.return_value = fake_node nics = [{'mac': '00:00:00:00:00:00'}] uuid = "00000000-0000-0000-0000-000000000000" self.cloud.unregister_machine(nics, uuid) self.assertTrue(mock_client.node.delete.called) + self.assertTrue(mock_client.port.get_by_address.called) self.assertTrue(mock_client.port.delete.called) self.assertTrue(mock_client.port.get_by_address.called) + mock_client.port.get_by_address.assert_called_with( + address='00:00:00:00:00:00') + mock_client.port.delete.assert_called_with( + port_id='00000000-0000-0000-0000-000000000001') @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_unregister_machine_unavailable(self, mock_client): From bccce1857b7c57e1f320ef7bd7299b76b18926dd Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 28 Sep 2015 09:56:48 -0500 Subject: [PATCH 0491/3836] Attempt to use glanceclient strip_version If you pass a Session to glanceclient, you must pass a version. But if you pass a version and the endpoint has a version things go south. Because I can't have nice things. Change-Id: Icbdb007dcc88965fe290ef96f61917d1f17703a9 --- shade/__init__.py | 8 +++++++- shade/tests/unit/test_shade.py | 6 +++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 96c614aa8..0131336dc 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -24,6 +24,7 @@ from dogpile import cache import glanceclient import glanceclient.exc +from glanceclient.common import utils as glance_utils from ironicclient import client as ironic_client from ironicclient import exceptions as ironic_exceptions import jsonpatch @@ -579,8 +580,13 @@ def delete_user(self, name_or_id): @property def glance_client(self): if self._glance_client is None: + endpoint, version = glance_utils.strip_version( + self.get_session_endpoint(service_key='image')) + # TODO(mordred): Put check detected vs. configured version + # and warn if they're different. self._glance_client = self._get_client( - 'image', glanceclient.Client, interface_key='interface') + 'image', glanceclient.Client, interface_key='interface', + endpoint=endpoint) return self._glance_client @property diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 0e5beac9c..3219a085c 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -96,12 +96,16 @@ def test_list_servers_exception(self, mock_client): self.assertRaises(exc.OpenStackCloudException, self.cloud.list_servers) + @mock.patch.object(shade.OpenStackCloud, 'get_session_endpoint') @mock.patch.object(shade.OpenStackCloud, 'keystone_session') @mock.patch.object(glanceclient, 'Client') - def test_glance_args(self, mock_client, mock_keystone_session): + def test_glance_args( + self, mock_client, mock_keystone_session, mock_endpoint): mock_keystone_session.return_value = None + mock_endpoint.return_value = 'http://example.com/v2' self.cloud.glance_client mock_client.assert_called_with( + endpoint='http://example.com', version='2', region_name='', service_name=None, interface='public', service_type='image', session=mock.ANY, From 9f0e6a8a2d8b510d219ab2eb25bfb949d25897a2 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 28 Sep 2015 13:04:08 -0500 Subject: [PATCH 0492/3836] Add functional test for private_v4 private_v4 is failing in nodepool, so maybe there is a bug in shade. Let's add a test to make sure. Change-Id: I8b7d69605fcd1482a54c0d4088d7de464c3e8629 --- shade/tests/functional/test_floating_ip.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/shade/tests/functional/test_floating_ip.py b/shade/tests/functional/test_floating_ip.py index 521eb3afa..334802e0d 100644 --- a/shade/tests/functional/test_floating_ip.py +++ b/shade/tests/functional/test_floating_ip.py @@ -19,11 +19,13 @@ Functional tests for floating IP resource. """ +import pprint import random import string import time from novaclient import exceptions as nova_exc +from testtools import content from shade import openstack_cloud from shade import meta @@ -192,6 +194,18 @@ def _setup_networks(self): nets = self.cloud.nova_client.networks.list() self.nic = {'net-id': nets[0].id} + def test_private_ip(self): + self._setup_networks() + + new_server = self.cloud.get_openstack_vars(self.cloud.create_server( + wait=True, name=self.new_item_name + '_server', + image=self.image, + flavor=self.flavor, nics=[self.nic])) + + self.addDetail( + 'server', content.text_content(pprint.pformat(new_server))) + self.assertNotEqual(new_server['private_v4'], '') + def test_add_auto_ip(self): self._setup_networks() From 79637b12cfebb3ab80c781b1217755832ab069c4 Mon Sep 17 00:00:00 2001 From: Simon Leinen Date: Mon, 28 Sep 2015 23:35:17 +0200 Subject: [PATCH 0493/3836] Added SWITCHengines vendor file Signed-off-by: Simon Leinen Change-Id: Iae386787ee2b3feecb78e3f69c7ed4e279e5acce --- doc/source/vendor-support.rst | 14 ++++++++++++++ os_client_config/vendors/switchengines.yaml | 9 +++++++++ 2 files changed, 23 insertions(+) create mode 100644 os_client_config/vendors/switchengines.yaml diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index 39f488d14..598b2d496 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -247,3 +247,17 @@ nz_wlg_1 Wellington, NZ * Image API Version is 1 * Images must be in `raw` format + +switchengines +------------- + +https://keystone.cloud.switch.ch:5000/v2.0 + +============== ================ +Region Name Human Name +============== ================ +LS Lausanne, CH +ZH Zurich, CH + +* Images must be in `raw` format +* Images must be uploaded using the Glance Task Interface diff --git a/os_client_config/vendors/switchengines.yaml b/os_client_config/vendors/switchengines.yaml new file mode 100644 index 000000000..ff6c50517 --- /dev/null +++ b/os_client_config/vendors/switchengines.yaml @@ -0,0 +1,9 @@ +name: switchengines +profile: + auth: + auth_url: https://keystone.cloud.switch.ch:5000/v2.0 + regions: + - LS + - ZH + image_api_use_tasks: true + image_format: raw From 723b893f22fc3db74761dc64e5296360e5739147 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 30 Sep 2015 17:27:22 -0400 Subject: [PATCH 0494/3836] Handle OS_CLOUD and OS_REGION_NAME friendly-like If you're using os-client-config and ansible and python-openstackclient, the envvar override support can get tricky if you've set OS_CLOUD to select a cloud for python-openstackclient, leading you to have an empty cloud called envvars when you were not actually trying to create one. Special-case pull both out, treating OS_CLOUD as a default value for get_one_cloud() and only creating an envvars cloud if OS_REGION_NAME is not the only env var provided. Change-Id: I2a5235c18f9be1e5dfc019888a0c187ef40074bc --- os_client_config/config.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index fcca30af3..2cea7eee1 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -86,7 +86,10 @@ def _get_os_environ(): and not k.startswith('OS_TEST') # infra CI var and not k.startswith('OS_STD') # infra CI var ] - if not environkeys: + # If the only environ key is region name, don't make a cloud, because + # it's being used as a cloud selector + if not environkeys or ( + len(environkeys) == 1 and 'OS_REGION_NAME' in environkeys): return None for k in environkeys: newkey = k[3:].lower() @@ -147,6 +150,9 @@ def __init__(self, config_files=None, vendor_files=None, ' either your environment based cloud, or one of your' ' file-based clouds.'.format(self.config_filename, self.envvar_key)) + # Pull out OS_CLOUD so that if it's the only thing set, do not + # make an envvars cloud + self.default_cloud = os.environ.pop('OS_CLOUD', None) envvars = _get_os_environ() if envvars: @@ -523,6 +529,9 @@ def get_one_cloud(self, cloud=None, validate=True, on missing required auth parameters """ + if cloud is None and self.default_cloud: + cloud = self.default_cloud + if cloud is None and self.envvar_key in self.get_cloud_names(): cloud = self.envvar_key From 137458fc1cbe27cb7191f0c35f846f844322276a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 30 Sep 2015 17:45:49 -0400 Subject: [PATCH 0495/3836] Dont throw exception on missing service We log exceptions to debug log, but checking for the existence of a service is normal flow. Don't throw an exception when we can just return None and have everything work without spamming logs. Change-Id: I35f431b12c16e085c0e256cad514134066e15a39 --- shade/__init__.py | 14 ++++++-------- shade/exc.py | 4 ---- shade/tests/unit/test_shade_operator.py | 6 ++---- 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index c023fdf06..b3fd5b4b8 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -33,7 +33,6 @@ from keystoneauth1 import session as ksa_session from keystoneclient.v2_0 import client as k2_client from keystoneclient.v3 import client as k3_client -from keystoneclient import exceptions as keystone_exceptions from novaclient import client as nova_client from novaclient import exceptions as nova_exceptions from neutronclient.common import exceptions as neutron_exceptions @@ -724,25 +723,24 @@ def get_session_endpoint(self, service_key): service_key), interface=self.cloud_config.get_interface(service_key), region_name=self.region_name) - except keystone_exceptions.EndpointNotFound as e: + except keystoneauth1.exceptions.catalog.EndpointNotFound as e: self.log.debug( "Endpoint not found in %s cloud: %s", self.name, str(e)) endpoint = None except Exception as e: raise OpenStackCloudException( "Error getting %s endpoint: %s" % (service_key, str(e))) - if endpoint is None: - raise OpenStackCloudUnavailableService( - "Cloud {cloud} does not have a {service} service".format( - cloud=self.name, service=service_key)) return endpoint def has_service(self, service_key): try: - self.get_session_endpoint(service_key) - return True + endpoint = self.get_session_endpoint(service_key) except OpenStackCloudException: return False + if endpoint: + return True + else: + return False @_cache_on_arguments() def _nova_extensions(self): diff --git a/shade/exc.py b/shade/exc.py index 6d20992aa..e5f2a3d89 100644 --- a/shade/exc.py +++ b/shade/exc.py @@ -47,10 +47,6 @@ class OpenStackCloudTimeout(OpenStackCloudException): pass -class OpenStackCloudUnavailableService(OpenStackCloudException): - pass - - class OpenStackCloudUnavailableExtension(OpenStackCloudException): pass diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index a7537349b..a456afd2b 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -768,10 +768,8 @@ def side_effect(*args, **kwargs): @mock.patch.object(shade.OpenStackCloud, 'keystone_session') def test_get_session_endpoint_unavailable(self, session_mock): session_mock.get_endpoint.return_value = None - with testtools.ExpectedException( - exc.OpenStackCloudUnavailableService, - "Cloud.*does not have a image service"): - self.cloud.get_session_endpoint("image") + image_endpoint = self.cloud.get_session_endpoint("image") + self.assertIsNone(image_endpoint) @mock.patch.object(shade.OpenStackCloud, 'keystone_session') def test_get_session_endpoint_identity(self, session_mock): From 733c04f6d381f523991c5ccfd56b9ff722111bf0 Mon Sep 17 00:00:00 2001 From: Ghe Rivero Date: Wed, 30 Sep 2015 23:15:35 +0200 Subject: [PATCH 0496/3836] identity version is 2.0 When using the defaults values from outside, we get a not so right API version. Change-Id: I8159dd4a26b65ad242d5ba75c4a5e0dc161704fc --- os_client_config/defaults.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/os_client_config/defaults.yaml b/os_client_config/defaults.yaml index e81eb1649..ddb975cf0 100644 --- a/os_client_config/defaults.yaml +++ b/os_client_config/defaults.yaml @@ -6,7 +6,7 @@ disable_vendor_agent: {} dns_api_version: '2' interface: public floating_ip_source: neutron -identity_api_version: '2' +identity_api_version: '2.0' image_api_use_tasks: false image_api_version: '2' image_format: qcow2 From 7d84f102313e61aba00d7665250ac6f628f8e8f6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 1 Oct 2015 10:50:27 -0400 Subject: [PATCH 0497/3836] Support passing force_ipv4 to the constructor IPv6 support is detectable, so rather than having a user opt-in to it, provide a flag that can be provided to tell it that detected IPv6 support is lying. This should have to be set for far fewer people and should result in transparent opt-in to IPv6 where available. Change-Id: Ib0c4c4e8b3b7b4bcee5fa3414719969274929b9a --- README.rst | 18 ++++++----- os_client_config/cloud_config.py | 10 ++++-- os_client_config/config.py | 36 ++++++++++++++++----- os_client_config/tests/base.py | 2 +- os_client_config/tests/test_cloud_config.py | 6 ++-- os_client_config/tests/test_config.py | 18 +++++++++-- 6 files changed, 64 insertions(+), 26 deletions(-) diff --git a/README.rst b/README.rst index 488dd24bb..21532ebdc 100644 --- a/README.rst +++ b/README.rst @@ -197,15 +197,18 @@ are connecting to OpenStack can share a cache should you desire. IPv6 ---- -IPv6 may be a thing you would prefer to use not only if the cloud supports it, -but also if your local machine support it. A simple boolean flag is settable -either in an environment variable, `OS_PREFER_IPV6`, or in the client section -of the clouds.yaml. +IPv6 is the future, and you should always use if if your cloud supports it and +if your local network supports it. Both of those are eaily detectable and all +friendly software should do the right thing. However, sometimes you might +exist in a location where you have an IPv6 stack, but something evil has +caused it to not actually function. In that case, there is a config option +you can set to unbreak you `force_ipv4`, or `OS_FORCE_IPV4` boolean +environment variable. :: client: - prefer_ipv6: true + force_ipv4: true clouds: mordred: profile: hp @@ -222,9 +225,8 @@ of the clouds.yaml. project_name: mordred@inaugust.com region_name: DFW -The above snippet will tell client programs to prefer returning an IPv6 -address. This will result in calls to, for instance, `shade`'s `get_public_ip` -to return an IPv4 address on HP, and an IPv6 address on Rackspace. +The above snippet will tell client programs to prefer returning an IPv4 +address. Usage ----- diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 86b4f5073..c8b5269ac 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -17,11 +17,11 @@ class CloudConfig(object): def __init__(self, name, region, config, - prefer_ipv6=False, auth_plugin=None): + force_ipv4=False, auth_plugin=None): self.name = name self.region = region self.config = config - self._prefer_ipv6 = prefer_ipv6 + self._force_ipv4 = force_ipv4 self._auth = auth_plugin def __getattr__(self, key): @@ -107,7 +107,11 @@ def get_endpoint(self, service_type): @property def prefer_ipv6(self): - return self._prefer_ipv6 + return not self._force_ipv4 + + @property + def force_ipv4(self): + return self._force_ipv4 def get_auth(self): """Return a keystoneauth plugin from the auth credentials.""" diff --git a/os_client_config/config.py b/os_client_config/config.py index 2cea7eee1..f43fe7458 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -113,7 +113,7 @@ def _auth_update(old_dict, new_dict): class OpenStackConfig(object): def __init__(self, config_files=None, vendor_files=None, - override_defaults=None): + override_defaults=None, force_ipv4=None): self._config_files = config_files or CONFIG_FILES self._vendor_files = vendor_files or VENDOR_FILES @@ -135,11 +135,28 @@ def __init__(self, config_files=None, vendor_files=None, # Grab ipv6 preference settings from env client_config = self.cloud_config.get('client', {}) - self.prefer_ipv6 = get_boolean( - os.environ.pop( - 'OS_PREFER_IPV6', client_config.get( - 'prefer_ipv6', client_config.get( - 'prefer-ipv6', False)))) + + if force_ipv4 is not None: + # If it's passed in to the constructor, honor it. + self.force_ipv4 = force_ipv4 + else: + # Get the backwards compat value + prefer_ipv6 = get_boolean( + os.environ.pop( + 'OS_PREFER_IPV6', client_config.get( + 'prefer_ipv6', client_config.get( + 'prefer-ipv6', True)))) + force_ipv4 = get_boolean( + os.environ.pop( + 'OS_FORCE_IPV4', client_config.get( + 'force_ipv4', client_config.get( + 'broken-ipv6', False)))) + + self.force_ipv4 = force_ipv4 + if not prefer_ipv6: + # this will only be false if someone set it explicitly + # honor their wishes + self.force_ipv4 = True # Next, process environment variables and add them to the mix self.envvar_key = os.environ.pop('OS_CLOUD_NAME', 'envvars') @@ -586,7 +603,10 @@ def get_one_cloud(self, cloud=None, validate=True, if hasattr(value, 'format'): config[key] = value.format(**config) - prefer_ipv6 = config.pop('prefer_ipv6', self.prefer_ipv6) + force_ipv4 = config.pop('force_ipv4', self.force_ipv4) + prefer_ipv6 = config.pop('prefer_ipv6', True) + if not prefer_ipv6: + force_ipv4 = True if cloud is None: cloud_name = '' @@ -595,7 +615,7 @@ def get_one_cloud(self, cloud=None, validate=True, return cloud_config.CloudConfig( name=cloud_name, region=config['region_name'], config=self._normalize_keys(config), - prefer_ipv6=prefer_ipv6, + force_ipv4=force_ipv4, auth_plugin=auth_plugin) @staticmethod diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py index cbf58da36..36c3bfb27 100644 --- a/os_client_config/tests/base.py +++ b/os_client_config/tests/base.py @@ -40,7 +40,7 @@ } USER_CONF = { 'client': { - 'prefer_ipv6': True, + 'force_ipv4': True, }, 'clouds': { '_test-cloud_': { diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index 4f5260fc1..c9317ad00 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -50,7 +50,7 @@ def test_arbitrary_attributes(self): self.assertIsNone(cc.x) # Test default ipv6 - self.assertFalse(cc.prefer_ipv6) + self.assertFalse(cc.force_ipv4) def test_iteration(self): cc = cloud_config.CloudConfig("test1", "region-al", fake_config_dict) @@ -116,8 +116,8 @@ def test_cert_with_key(self): def test_ipv6(self): cc = cloud_config.CloudConfig( - "test1", "region-al", fake_config_dict, prefer_ipv6=True) - self.assertTrue(cc.prefer_ipv6) + "test1", "region-al", fake_config_dict, force_ipv4=True) + self.assertTrue(cc.force_ipv4) def test_getters(self): cc = cloud_config.CloudConfig("test1", "region-al", fake_services_dict) diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 3331b3374..271c40be8 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -136,16 +136,28 @@ def test_fallthrough(self): c.get_one_cloud(cloud='defaults', validate=False) def test_prefer_ipv6_true(self): + c = config.OpenStackConfig(config_files=[self.no_yaml], + vendor_files=[self.no_yaml]) + cc = c.get_one_cloud(cloud='defaults', validate=False) + self.assertTrue(cc.prefer_ipv6) + + def test_prefer_ipv6_false(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) cc = c.get_one_cloud(cloud='_test-cloud_') - self.assertTrue(cc.prefer_ipv6) + self.assertFalse(cc.prefer_ipv6) - def test_prefer_ipv6_false(self): + def test_force_ipv4_true(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + cc = c.get_one_cloud(cloud='_test-cloud_') + self.assertTrue(cc.force_ipv4) + + def test_force_ipv4_false(self): c = config.OpenStackConfig(config_files=[self.no_yaml], vendor_files=[self.no_yaml]) cc = c.get_one_cloud(cloud='defaults', validate=False) - self.assertFalse(cc.prefer_ipv6) + self.assertFalse(cc.force_ipv4) def test_get_one_cloud_auth_merge(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml]) From 21ff307d130e758242a698312d6a14fc0d5b620e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 1 Oct 2015 13:37:13 -0400 Subject: [PATCH 0498/3836] Put in override for Rackspace broken neutron Rackspace puts neutron in the service catalog but it does not work (404s the endpoint) While it's easy enough to try/except around network calls, it's also a known fact and can be communicated clearly. Allow things that want to short-circuit read the value from the config and react accordingly. Note: This is not a value going in to the defaults, because the fact that this is happening at all is highly strange and hopefully something that can be solved by collaborating on future iterations of DefCore. However, communicating clearly that it's a known issue is helpful for a class of users. Change-Id: I506f4dd397608f2feb6fbda48d297d283312a169 --- os_client_config/vendors/rackspace.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/os_client_config/vendors/rackspace.yaml b/os_client_config/vendors/rackspace.yaml index 5cf7a44e6..c37802976 100644 --- a/os_client_config/vendors/rackspace.yaml +++ b/os_client_config/vendors/rackspace.yaml @@ -17,3 +17,4 @@ profile: disable_vendor_agent: vm_mode: hvm xenapi_use_agent: false + has_network: false From d3c82ab42835c2bfe9817d2f8a571490153879e1 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 1 Oct 2015 14:52:18 -0400 Subject: [PATCH 0499/3836] Fix two typos Change-Id: Idf3df94ea039f07fa958a3afadf3388221ffa2ff --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 21532ebdc..7b737b9de 100644 --- a/README.rst +++ b/README.rst @@ -197,8 +197,8 @@ are connecting to OpenStack can share a cache should you desire. IPv6 ---- -IPv6 is the future, and you should always use if if your cloud supports it and -if your local network supports it. Both of those are eaily detectable and all +IPv6 is the future, and you should always use it if your cloud supports it and +if your local network supports it. Both of those are easily detectable and all friendly software should do the right thing. However, sometimes you might exist in a location where you have an IPv6 stack, but something evil has caused it to not actually function. In that case, there is a config option From a9d5827a0929d5de1fc0951412f96bc031eb2409 Mon Sep 17 00:00:00 2001 From: Ghe Rivero Date: Fri, 2 Oct 2015 12:11:50 +0200 Subject: [PATCH 0500/3836] Some cleanup Remove identity api version 2 support (default) and some cleanup in the doc (tables properly formated) Change-Id: I5d90723dd54905c8a67c52c6111bdf089f4346a0 --- doc/source/vendor-support.rst | 2 ++ os_client_config/vendors/catalyst.yaml | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index 598b2d496..b61a05b8c 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -244,6 +244,7 @@ Region Name Human Name ============== ================ nz-por-1 Porirua, NZ nz_wlg_1 Wellington, NZ +============== ================ * Image API Version is 1 * Images must be in `raw` format @@ -258,6 +259,7 @@ Region Name Human Name ============== ================ LS Lausanne, CH ZH Zurich, CH +============== ================ * Images must be in `raw` format * Images must be uploaded using the Glance Task Interface diff --git a/os_client_config/vendors/catalyst.yaml b/os_client_config/vendors/catalyst.yaml index 348ceafc8..14cdf254f 100644 --- a/os_client_config/vendors/catalyst.yaml +++ b/os_client_config/vendors/catalyst.yaml @@ -5,6 +5,5 @@ profile: regions: - nz-por-1 - nz_wlg_1 - identity_api_version: '2' image_api_version: '1' image_format: raw From 332e7907f7fa680631c2d71988b75ea5e17af66f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 2 Oct 2015 10:13:48 -0400 Subject: [PATCH 0501/3836] Update fake to match latest OCC os-client-config was returning an incorrect value which was finding its way through into one of our fakes. Change-Id: I1f1c59313a429be386061a81bd51990204996909 --- shade/tests/unit/test_object.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index 02b94a364..ee3f5479e 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -47,7 +47,7 @@ def test_swift_client(self, endpoint_mock, session_mock, swift_mock): swift_mock.assert_called_with( preauthurl='danzig', preauthtoken='yankee', - auth_version='2', + auth_version=mock.ANY, timeout=None, os_options=dict( object_storage_url='danzig', From 950f22a2c8816ff5e9440ae975b2ee14b3fe053d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 29 Sep 2015 15:26:58 -0500 Subject: [PATCH 0502/3836] Provide short-circuit for finding server networks Doing a search for the networks on every server connect can be expensive due to the port list - even with caching. Instead, allow for a configurably overridable networks to be found once and cached aggressively, then use that network name to match in the nova networks dict. If that can be found, awesome, we all win. Change-Id: Ic3345e7fb7e428227a9465bd07077e67d4f93244 --- shade/__init__.py | 93 +++++++++++++++++++++++++++++++++++ shade/meta.py | 24 +++++++++ shade/tests/unit/test_meta.py | 38 +++++++++++--- 3 files changed, 149 insertions(+), 6 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index c023fdf06..16a973ad6 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -247,6 +247,24 @@ def __init__( self.image_api_use_tasks = cloud_config.config['image_api_use_tasks'] self.secgroup_source = cloud_config.config['secgroup_source'] + self._external_network = None + self._external_network_name_or_id = cloud_config.config.get( + 'external_network', None) + self._use_external_network = cloud_config.config.get( + 'use_external_network', True) + + self._internal_network = None + self._internal_network_name_or_id = cloud_config.config.get( + 'internal_network', None) + self._use_internal_network = cloud_config.config.get( + 'use_internal_network', True) + + # Variables to prevent us from going through the network finding + # logic again if we've done it once. This is different from just + # the cached value, since "None" is a valid value to find. + self._external_network_stamp = False + self._internal_network_stamp = False + if manager is not None: self.manager = manager else: @@ -1015,6 +1033,81 @@ def list_records(self, domain_id): raise OpenStackCloudException( "Error fetching record list: %s" % e) + def use_external_network(self): + return self._use_external_network + + def use_internal_network(self): + return self._use_internal_network + + def _get_network( + self, + name_or_id, + use_network_func, + network_cache, + network_stamp, + filters): + if not use_network_func(): + return None + if network_cache: + return network_cache + if network_stamp: + return None + if not self.has_service('network'): + return None + if name_or_id: + ext_net = self.get_network(name_or_id) + if not ext_net: + raise OpenStackCloudException( + "Network {network} was provided for external" + " access and that network could not be found".format( + network=name_or_id)) + else: + return ext_net + try: + # TODO(mordred): Rackspace exposes neutron but it does not + # work. I think that overriding what the service catalog + # reports should be a thing os-client-config should handle + # in a vendor profile - but for now it does not. That means + # this search_networks can just totally fail. If it does though, + # that's fine, clearly the neutron introspection is not going + # to work. + ext_nets = self.search_networks(filters=filters) + if len(ext_nets) == 1: + return ext_nets[0] + except OpenStackCloudException: + pass + return None + + def get_external_network(self): + """Return the network that is configured to route northbound. + + :returns: A network dict if one is found, None otherwise. + """ + self._external_network = self._get_network( + self._external_network_name_or_id, + self.use_external_network, + self._external_network, + self._external_network_stamp, + filters={'router:external': True}) + self._external_network_stamp = True + + def get_internal_network(self): + """Return the network that is configured to not route northbound. + + :returns: A network dict if one is found, None otherwise. + """ + self._internal_network = self._get_network( + self._internal_network_name_or_id, + self.use_internal_network, + self._internal_network, + self._internal_network_stamp, + filters={ + 'router:external': False, + 'shared': False + }) + self._internal_network_stamp = True + return self._internal_network + def get_keypair(self, name_or_id, filters=None): return _utils._get_entity(self.search_keypairs, name_or_id, filters) diff --git a/shade/meta.py b/shade/meta.py index 576a8c26a..12d9fa898 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -77,6 +77,18 @@ def get_server_private_ip(server, cloud=None): Last resort, ignore the IP type and just look for an IP on the 'private' network (e.g., Rackspace). """ + if cloud and not cloud.use_internal_network(): + return None + + # Short circuit the ports/networks search below with a heavily cached + # and possibly pre-configured network name + if cloud: + int_net = cloud.get_internal_network() + if int_net: + int_ip = get_server_ip(server, key_name=int_net['name']) + if int_ip is not None: + return int_ip + if cloud and cloud.has_service('network'): try: server_ports = cloud.search_ports( @@ -123,8 +135,20 @@ def get_server_external_ipv4(cloud, server): :return: a string containing the IPv4 address or None """ + if not cloud.use_external_network(): + return None + if server['accessIPv4']: return server['accessIPv4'] + + # Short circuit the ports/networks search below with a heavily cached + # and possibly pre-configured network name + ext_net = cloud.get_external_network() + if ext_net: + ext_ip = get_server_ip(server, key_name=ext_net['name']) + if ext_ip is not None: + return ext_ip + if cloud.has_service('network'): try: # Search a fixed IP attached to an external net. Unfortunately diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 301a5c252..1499503c6 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -47,6 +47,18 @@ def get_volumes(self, server): def has_service(self, service_name): return self.service_val + def use_internal_network(self): + return True + + def use_external_network(self): + return True + + def get_internal_network(self): + return None + + def get_external_network(self): + return None + class FakeServer(object): id = 'test-id-0' @@ -106,7 +118,7 @@ def test_find_nova_addresses_all(self): def test_get_server_ip(self): srv = meta.obj_to_dict(FakeServer()) cloud = shade.openstack_cloud(validate=False) - self.assertEqual(PRIVATE_V4, meta.get_server_private_ip(srv)) + self.assertEqual(PRIVATE_V4, meta.get_server_private_ip(srv, cloud)) self.assertEqual(PUBLIC_V4, meta.get_server_external_ipv4(cloud, srv)) @mock.patch.object(shade.OpenStackCloud, 'has_service') @@ -120,18 +132,28 @@ def test_get_server_private_ip(self, mock_search_networks, 'fixed_ips': [{'ip_address': PRIVATE_V4}], 'device_id': 'test-id' }] - mock_search_networks.return_value = [{'id': 'test-net-id'}] + mock_search_networks.return_value = [{ + 'id': 'test-net-id', + 'name': 'test-net-name' + }] srv = meta.obj_to_dict(fakes.FakeServer( - id='test-id', name='test-name', status='ACTIVE')) + id='test-id', name='test-name', status='ACTIVE', + addresses={'private': [{'OS-EXT-IPS:type': 'fixed', + 'addr': PRIVATE_V4, + 'version': 4}], + 'public': [{'OS-EXT-IPS:type': 'floating', + 'addr': PUBLIC_V4, + 'version': 4}]} + )) cloud = shade.openstack_cloud(validate=False) self.assertEqual(PRIVATE_V4, meta.get_server_private_ip(srv, cloud)) - mock_has_service.assert_called_once_with('network') + mock_has_service.assert_called_with('network') mock_search_ports.assert_called_once_with( filters={'device_id': 'test-id'} ) - mock_search_networks.assert_called_once_with( + mock_search_networks.assert_called_with( filters={'router:external': False, 'shared': False} ) @@ -177,12 +199,16 @@ def test_get_server_external_ipv4_neutron_accessIPv6(self): self.assertEqual(PUBLIC_V6, ip) @mock.patch.object(shade.OpenStackCloud, 'has_service') + @mock.patch.object(shade.OpenStackCloud, 'search_networks') @mock.patch.object(shade.OpenStackCloud, 'search_ports') @mock.patch.object(meta, 'get_server_ip') def test_get_server_external_ipv4_neutron_exception( - self, mock_get_server_ip, mock_search_ports, mock_has_service): + self, mock_get_server_ip, mock_search_ports, + mock_search_networks, + mock_has_service): # Testing Clouds with a non working Neutron mock_has_service.return_value = True + mock_search_networks.return_value = [] mock_search_ports.side_effect = neutron_exceptions.NotFound() mock_get_server_ip.return_value = PUBLIC_V4 From 14c7fd560f678efbe1504b3944c0e544bd2ed13a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 1 Oct 2015 13:33:12 -0400 Subject: [PATCH 0503/3836] Provide shortcut around has_service Some clouds have a service in the catalog, but it does not work (Rackspace with neutron) In other case, it exists and works, but you know that you don't use it (Infra Nodepool does not use volumes, so the extended info for servers in server_list does not need to look for volumes) OCC allows arbitrary passthrough values - so look for booleans of the form has_{service_key} If 'has_volume' is false for a cloud, we will not ask keystone if there is a volume service, and will instead just return False. Change-Id: I1d2f2a7611f4463898d73c1a2b70b9a967a9c7b6 --- shade/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 16a973ad6..859162f38 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -756,6 +756,11 @@ def get_session_endpoint(self, service_key): return endpoint def has_service(self, service_key): + if not self.cloud_config.config.get('has_%s' % service_key, True): + self.log.debug( + "Overriding {service_key} entry in catalog per config".format( + service_key=service_key)) + return False try: self.get_session_endpoint(service_key) return True From 758630ed6bd9a662a3ef55b98bca13db69099531 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 1 Oct 2015 14:10:10 -0400 Subject: [PATCH 0504/3836] Fix mis-named has_service entry Change-Id: I44ee303040cccf3432645b5b90fbdf428895d13d --- shade/meta.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/meta.py b/shade/meta.py index 12d9fa898..a92c19f44 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -301,7 +301,7 @@ def get_hostvars_from_server(cloud, server, mounts=None): server_vars['image'].pop('links', None) volumes = [] - if cloud.has_service('volumes'): + if cloud.has_service('volume'): try: for volume in cloud.get_volumes(server): # Make things easier to consume elsewhere From 12662ddbabf89dda039b96ec01134ca3e6d12cfd Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 1 Oct 2015 18:51:48 -0400 Subject: [PATCH 0505/3836] Pass parameters correctly for image snapshots The passthrough parameters here were just wrong. The image name is passed as image_name and the server value is required. Also, metadata is not taken as kwargs, it's taken as a dict. Change-Id: Iab1c443dbbebddaba0002700304a033b8e161b65 --- shade/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 859162f38..2271d3b79 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1406,9 +1406,9 @@ def get_image_id(self, image_name, exclude=None): return image.id return None - def create_image_snapshot(self, name, **metadata): + def create_image_snapshot(self, name, server, **metadata): image = self.manager.submitTask(_tasks.ImageSnapshotCreate( - name=name, **metadata)) + image_name=name, server=server, metadata=metadata)) if image: return meta.obj_to_dict(image) return None From 0a06226863ac767c3a3915f5470646dc4f46fc54 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 1 Oct 2015 18:13:18 -0400 Subject: [PATCH 0506/3836] Plumb wait and timout down to add_auto_ip add_ips_to_server needs to take a wait and timeout parameter so that it can pass them down to add_auto_ip. Change-Id: Iea0d6e2d270e2bc4df0fc649217a2f1da89ae9cc --- shade/__init__.py | 6 ++++-- shade/tests/unit/test_floating_ip_common.py | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 2271d3b79..aff12942b 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2330,7 +2330,9 @@ def add_auto_ip(self, server, wait=False, timeout=60): return self.get_floating_ip(id=f_ip['id']) - def add_ips_to_server(self, server, auto_ip=True, ips=None, ip_pool=None): + def add_ips_to_server( + self, server, auto_ip=True, ips=None, ip_pool=None, + wait=False, timeout=60): if ip_pool: self.add_ip_from_pool(server['id'], ip_pool) elif ips: @@ -2338,7 +2340,7 @@ def add_ips_to_server(self, server, auto_ip=True, ips=None, ip_pool=None): elif auto_ip: if self.get_server_public_ip(server): return server - self.add_auto_ip(server) + self.add_auto_ip(server, wait=wait, timeout=timeout) else: return server diff --git a/shade/tests/unit/test_floating_ip_common.py b/shade/tests/unit/test_floating_ip_common.py index 82bb9d5f2..a0fcbaf3a 100644 --- a/shade/tests/unit/test_floating_ip_common.py +++ b/shade/tests/unit/test_floating_ip_common.py @@ -104,4 +104,5 @@ def test_add_ips_to_server_auto_ip( self.client.add_ips_to_server(server_dict) - mock_add_auto_ip.assert_called_with(server_dict) + mock_add_auto_ip.assert_called_with( + server_dict, wait=False, timeout=60) From 5a420350db878046959006ffd5e8f3fb139bed1a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 1 Oct 2015 09:12:21 -0400 Subject: [PATCH 0507/3836] Return IPv6 address for interface_ip on request If the local host looks like it can route IPv6 and we have an IPv6 address, and the user has not indicated that they must use IPv4 for some reason, return it to them in the interface_ip field. Also, always return something in interface_ip if possible. Change-Id: I3544280cab7abfa6b4154244f4c4588bc65c7347 Depends-On: Ib0c4c4e8b3b7b4bcee5fa3414719969274929b9a Co-Authored-By: James E. Blair --- requirements.txt | 3 ++- shade/__init__.py | 3 +++ shade/_utils.py | 12 ++++++++++++ shade/meta.py | 14 +++++++++++--- shade/tests/unit/test_meta.py | 18 +++++++++++++++++- tox.ini | 2 +- 6 files changed, 46 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index c5fab814e..8e00f5c12 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,10 +3,11 @@ pbr>=0.11,<2.0 bunch decorator jsonpatch -os-client-config>=1.7.4 +os-client-config>=1.8.0 six keystoneauth1>=1.0.0 +netifaces>=0.10.4 python-novaclient>=2.21.0,!=2.27.0 python-keystoneclient>=0.11.0 python-glanceclient>=1.0.0 diff --git a/shade/__init__.py b/shade/__init__.py index aff12942b..377cbbe0e 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -246,6 +246,7 @@ def __init__( self.api_timeout = cloud_config.config['api_timeout'] self.image_api_use_tasks = cloud_config.config['image_api_use_tasks'] self.secgroup_source = cloud_config.config['secgroup_source'] + self.force_ipv4 = cloud_config.force_ipv4 self._external_network = None self._external_network_name_or_id = cloud_config.config.get( @@ -295,6 +296,8 @@ def __init__( self._swift_service = None self._trove_client = None + self._local_ipv6 = _utils.localhost_supports_ipv6() + self.cloud_config = cloud_config @contextlib.contextmanager diff --git a/shade/_utils.py b/shade/_utils.py index b7ff2d32e..c3a542b4c 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -15,6 +15,7 @@ import re import time +import netifaces from socket import inet_aton from struct import unpack @@ -273,3 +274,14 @@ def normalize_neutron_floating_ips(ips): ip.get('port_id') != ''), status=ip['status'] ) for ip in ips] + + +def localhost_supports_ipv6(): + """Determine whether the local host supports IPv6 + + We look for a default route that supports the IPv6 address family, + and assume that if it is present, this host has globally routable + IPv6 connectivity. + """ + + return netifaces.AF_INET6 in netifaces.gateways()['default'] diff --git a/shade/meta.py b/shade/meta.py index a92c19f44..5285abaa4 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -265,10 +265,15 @@ def get_hostvars_from_server(cloud, server, mounts=None): server_vars['public_v4'] = get_server_external_ipv4(cloud, server) or '' server_vars['public_v6'] = get_server_external_ipv6(server) or '' server_vars['private_v4'] = get_server_private_ip(server, cloud) or '' - if cloud.private: + interface_ip = None + if cloud.private and server_vars['private_v4']: interface_ip = server_vars['private_v4'] else: - interface_ip = server_vars['public_v4'] + if (server_vars['public_v6'] and cloud._local_ipv6 + and not cloud.force_ipv4): + interface_ip = server_vars['public_v6'] + else: + interface_ip = server_vars['public_v4'] if interface_ip: server_vars['interface_ip'] = interface_ip @@ -276,7 +281,10 @@ def get_hostvars_from_server(cloud, server, mounts=None): # server record. Since we know them, go ahead and set them. In the case # where they were set previous, we use the values, so this will not break # clouds that provide the information - server_vars['accessIPv4'] = server_vars['public_v4'] + if cloud.private and server_vars['private_v4']: + server_vars['accessIPv4'] = server_vars['private_v4'] + else: + server_vars['accessIPv4'] = server_vars['public_v4'] server_vars['accessIPv6'] = server_vars['public_v6'] server_vars['region'] = cloud.region_name diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 1499503c6..6ea6f83de 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -32,8 +32,10 @@ class FakeCloud(object): region_name = 'test-region' name = 'test-name' private = False + force_ipv4 = False service_val = True _unused = "useless" + _local_ipv6 = True def get_flavor_name(self, id): return 'test-flavor-name' @@ -319,7 +321,7 @@ def test_basic_hostvars( self.assertEqual(PRIVATE_V4, hostvars['private_v4']) self.assertEqual(PUBLIC_V4, hostvars['public_v4']) self.assertEqual(PUBLIC_V6, hostvars['public_v6']) - self.assertEqual(PUBLIC_V4, hostvars['interface_ip']) + self.assertEqual(PUBLIC_V6, hostvars['interface_ip']) self.assertEquals(FakeCloud.region_name, hostvars['region']) self.assertEquals(FakeCloud.name, hostvars['cloud']) self.assertEquals("test-image-name", hostvars['image']['name']) @@ -332,6 +334,20 @@ def test_basic_hostvars( # test volume exception self.assertEquals([], hostvars['volumes']) + @mock.patch.object(shade.meta, 'get_server_external_ipv6') + @mock.patch.object(shade.meta, 'get_server_external_ipv4') + def test_ipv4_hostvars( + self, mock_get_server_external_ipv4, + mock_get_server_external_ipv6): + mock_get_server_external_ipv4.return_value = PUBLIC_V4 + mock_get_server_external_ipv6.return_value = PUBLIC_V6 + + fake_cloud = FakeCloud() + fake_cloud.force_ipv4 = True + hostvars = meta.get_hostvars_from_server( + fake_cloud, meta.obj_to_dict(FakeServer())) + self.assertEqual(PUBLIC_V4, hostvars['interface_ip']) + @mock.patch.object(shade.meta, 'get_server_external_ipv4') def test_private_interface_ip(self, mock_get_server_external_ipv4): mock_get_server_external_ipv4.return_value = PUBLIC_V4 diff --git a/tox.ini b/tox.ini index a01094d13..777e8a88c 100644 --- a/tox.ini +++ b/tox.ini @@ -32,7 +32,7 @@ commands = python setup.py testr --coverage --testr-args='{posargs}' [flake8] # Infra does not follow hacking, nor the broken E12* things -ignore = E123,E125,H +ignore = E123,E125,E129,H show-source = True builtins = _ exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build From 379962f0e8289e2db85a0eeb7b51d10cff70e67d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 2 Oct 2015 11:08:22 -0400 Subject: [PATCH 0508/3836] Add universal=1 to setup.cfg to build python 3 wheels os-client-config gates on python3. Change-Id: I887ab2f2f2436e7423eab8abc23655423ee7b226 --- setup.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.cfg b/setup.cfg index df3434f0f..bc4f128cf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,3 +31,6 @@ all_files = 1 [upload_sphinx] upload-dir = doc/build/html + +[wheel] +universal = 1 From d3b17d14036c109e065cf112e98efea878874ff2 Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Fri, 2 Oct 2015 16:05:06 -0700 Subject: [PATCH 0509/3836] Fix create_image_snapshot This method from novaclient returns an image id, not an object as was apparently assumed. Before returning, perform an image lookup so that we can return a Bunch() object of the image. Note that if the provider does not immediately make an image available for lookups, this will fail and the user will have no record of the image id. Notably, because it worked with the image_id until the image was built, nodepool was not suceptible to this problem. I am unsure whether there are any clouds that might behave in this way. However, this code matches what we do in shade with servers. Change-Id: Icaff5d8e66a2458817fcbced16014efbc5ff33b8 --- shade/__init__.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index b5aab5dcc..3bd77cad3 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1408,11 +1408,9 @@ def get_image_id(self, image_name, exclude=None): return None def create_image_snapshot(self, name, server, **metadata): - image = self.manager.submitTask(_tasks.ImageSnapshotCreate( - image_name=name, server=server, metadata=metadata)) - if image: - return meta.obj_to_dict(image) - return None + image_id = str(self.manager.submitTask(_tasks.ImageSnapshotCreate( + image_name=name, server=server, metadata=metadata))) + return self.get_image(image_id) def delete_image(self, name_or_id, wait=False, timeout=3600): image = self.get_image(name_or_id) From be11a20412ff827e252c7a6f83361f5d179131a1 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 3 Oct 2015 13:31:14 -0400 Subject: [PATCH 0510/3836] Optimize network finding clarkb was right about the port listing code being unneccesary now that we have get_external_network. All of the logic that happens there is also more efficient and easier to override via config. Skip doing a second pass at ports. Also, move floating ip finding earlier - if the server has one, that's the IP it wants - it's the only reason to use one of those. Change-Id: Ie09c0046aded3646a42cc9714f1fb76375e5e099 --- shade/__init__.py | 41 ++++----- shade/meta.py | 62 +++---------- shade/tests/fakes.py | 4 +- shade/tests/functional/test_floating_ip.py | 8 ++ shade/tests/unit/test_meta.py | 101 +++++++++++++-------- 5 files changed, 103 insertions(+), 113 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 3bd77cad3..7c16875f0 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -247,13 +247,13 @@ def __init__( self.secgroup_source = cloud_config.config['secgroup_source'] self.force_ipv4 = cloud_config.force_ipv4 - self._external_network = None + self._external_networks = [] self._external_network_name_or_id = cloud_config.config.get( 'external_network', None) self._use_external_network = cloud_config.config.get( 'use_external_network', True) - self._internal_network = None + self._internal_networks = [] self._internal_network_name_or_id = cloud_config.config.get( 'internal_network', None) self._use_internal_network = cloud_config.config.get( @@ -1053,13 +1053,13 @@ def _get_network( network_stamp, filters): if not use_network_func(): - return None + return [] if network_cache: return network_cache if network_stamp: - return None + return [] if not self.has_service('network'): - return None + return [] if name_or_id: ext_net = self.get_network(name_or_id) if not ext_net: @@ -1068,7 +1068,7 @@ def _get_network( " access and that network could not be found".format( network=name_or_id)) else: - return ext_net + return [] try: # TODO(mordred): Rackspace exposes neutron but it does not # work. I think that overriding what the service catalog @@ -1077,42 +1077,41 @@ def _get_network( # this search_networks can just totally fail. If it does though, # that's fine, clearly the neutron introspection is not going # to work. - ext_nets = self.search_networks(filters=filters) - if len(ext_nets) == 1: - return ext_nets[0] + return self.search_networks(filters=filters) except OpenStackCloudException: pass - return None + return [] - def get_external_network(self): - """Return the network that is configured to route northbound. + def get_external_networks(self): + """Return the networks that are configured to route northbound. - :returns: A network dict if one is found, None otherwise. + :returns: A list of network dicts if one is found """ - self._external_network = self._get_network( + self._external_networks = self._get_network( self._external_network_name_or_id, self.use_external_network, - self._external_network, + self._external_networks, self._external_network_stamp, filters={'router:external': True}) self._external_network_stamp = True + return self._external_networks - def get_internal_network(self): - """Return the network that is configured to not route northbound. + def get_internal_networks(self): + """Return the networks that are configured to not route northbound. - :returns: A network dict if one is found, None otherwise. + :returns: A list of network dicts if one is found """ - self._internal_network = self._get_network( + self._internal_networks = self._get_network( self._internal_network_name_or_id, self.use_internal_network, - self._internal_network, + self._internal_networks, self._internal_network_stamp, filters={ 'router:external': False, 'shared': False }) self._internal_network_stamp = True - return self._internal_network + return self._internal_networks def get_keypair(self, name_or_id, filters=None): return _utils._get_entity(self.search_keypairs, name_or_id, filters) diff --git a/shade/meta.py b/shade/meta.py index 5285abaa4..62526a1a0 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -83,30 +83,12 @@ def get_server_private_ip(server, cloud=None): # Short circuit the ports/networks search below with a heavily cached # and possibly pre-configured network name if cloud: - int_net = cloud.get_internal_network() - if int_net: + int_nets = cloud.get_internal_networks() + for int_net in int_nets: int_ip = get_server_ip(server, key_name=int_net['name']) if int_ip is not None: return int_ip - if cloud and cloud.has_service('network'): - try: - server_ports = cloud.search_ports( - filters={'device_id': server['id']}) - nets = cloud.search_networks( - filters={'router:external': False, 'shared': False}) - except Exception as e: - log.debug( - "Something went wrong talking to neutron API: " - "'{msg}'. Trying with Nova.".format(msg=str(e))) - else: - for net in nets: - for port in server_ports: - if net['id'] == port['network_id']: - for ip in port['fixed_ips']: - if _utils.is_ipv4(ip['ip_address']): - return ip['ip_address'] - ip = get_server_ip(server, ext_tag='fixed', key_name='private') if ip: return ip @@ -143,35 +125,18 @@ def get_server_external_ipv4(cloud, server): # Short circuit the ports/networks search below with a heavily cached # and possibly pre-configured network name - ext_net = cloud.get_external_network() - if ext_net: + ext_nets = cloud.get_external_networks() + for ext_net in ext_nets: ext_ip = get_server_ip(server, key_name=ext_net['name']) if ext_ip is not None: return ext_ip - if cloud.has_service('network'): - try: - # Search a fixed IP attached to an external net. Unfortunately - # Neutron ports don't have a 'floating_ips' attribute - server_ports = cloud.search_ports( - filters={'device_id': server['id']}) - ext_nets = cloud.search_networks(filters={'router:external': True}) - except Exception as e: - log.debug( - "Something went wrong talking to neutron API: " - "'{msg}'. Trying with Nova.".format(msg=str(e))) - # Fall-through, trying with Nova - else: - for net in ext_nets: - for port in server_ports: - if net['id'] == port['network_id']: - for ip in port['fixed_ips']: - if _utils.is_ipv4(ip['ip_address']): - return ip['ip_address'] - # The server doesn't have an interface on an external network so it - # can either have a floating IP or have no way to be reached from - # outside the cloud. - # Fall-through, trying with Nova + # Try to get a floating IP address + # Much as I might find floating IPs annoying, if it has one, that's + # almost certainly the one that wants to be used + ext_ip = get_server_ip(server, ext_tag='floating') + if ext_ip is not None: + return ext_ip # The cloud doesn't support Neutron or Neutron can't be contacted. The # server might have fixed addresses that are reachable from outside the @@ -182,18 +147,13 @@ def get_server_external_ipv4(cloud, server): if ext_ip is not None: return ext_ip - # Try to find a globally routable IP address + # Nothing else works, try to find a globally routable IP address for interfaces in server['addresses'].values(): for interface in interfaces: if _utils.is_ipv4(interface['addr']) and \ _utils.is_globally_routable_ipv4(interface['addr']): return interface['addr'] - # Last, try to get a floating IP address - ext_ip = get_server_ip(server, ext_tag='floating') - if ext_ip is not None: - return ext_ip - return None diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 7b72dfc92..7aacb7fcf 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -67,11 +67,13 @@ def __init__(self, id): class FakeServer(object): def __init__( self, id, name, status, addresses=None, - accessIPv4='', accessIPv6=''): + accessIPv4='', accessIPv6='', flavor=None, image=None): self.id = id self.name = name self.status = status self.addresses = addresses + self.flavor = flavor + self.image = image self.accessIPv4 = accessIPv4 self.accessIPv6 = accessIPv6 diff --git a/shade/tests/functional/test_floating_ip.py b/shade/tests/functional/test_floating_ip.py index 334802e0d..5bfae17a6 100644 --- a/shade/tests/functional/test_floating_ip.py +++ b/shade/tests/functional/test_floating_ip.py @@ -189,9 +189,17 @@ def _setup_networks(self): # Select the network for creating new servers self.nic = {'net-id': self.test_net['id']} + self.addDetail( + 'networks-neutron', + content.text_content(pprint.pformat( + self.cloud.list_networks()))) else: # ToDo: remove once we have list/get methods for nova networks nets = self.cloud.nova_client.networks.list() + self.addDetail( + 'networks-nova', + content.text_content(pprint.pformat( + nets))) self.nic = {'net-id': nets[0].id} def test_private_ip(self): diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 6ea6f83de..a8adbb992 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -20,7 +20,6 @@ import shade from shade import meta -from shade import _utils from shade.tests import fakes PRIVATE_V4 = '198.51.100.3' @@ -55,11 +54,11 @@ def use_internal_network(self): def use_external_network(self): return True - def get_internal_network(self): - return None + def get_internal_networks(self): + return [] - def get_external_network(self): - return None + def get_external_networks(self): + return [] class FakeServer(object): @@ -124,16 +123,10 @@ def test_get_server_ip(self): self.assertEqual(PUBLIC_V4, meta.get_server_external_ipv4(cloud, srv)) @mock.patch.object(shade.OpenStackCloud, 'has_service') - @mock.patch.object(shade.OpenStackCloud, 'search_ports') @mock.patch.object(shade.OpenStackCloud, 'search_networks') - def test_get_server_private_ip(self, mock_search_networks, - mock_search_ports, mock_has_service): + def test_get_server_private_ip( + self, mock_search_networks, mock_has_service): mock_has_service.return_value = True - mock_search_ports.return_value = [{ - 'network_id': 'test-net-id', - 'fixed_ips': [{'ip_address': PRIVATE_V4}], - 'device_id': 'test-id' - }] mock_search_networks.return_value = [{ 'id': 'test-net-id', 'name': 'test-net-name' @@ -152,36 +145,75 @@ def test_get_server_private_ip(self, mock_search_networks, self.assertEqual(PRIVATE_V4, meta.get_server_private_ip(srv, cloud)) mock_has_service.assert_called_with('network') - mock_search_ports.assert_called_once_with( - filters={'device_id': 'test-id'} + mock_search_networks.assert_called_with( + filters={'router:external': False, 'shared': False} ) + + @mock.patch.object(shade.OpenStackCloud, 'get_image_name') + @mock.patch.object(shade.OpenStackCloud, 'get_flavor_name') + @mock.patch.object(shade.OpenStackCloud, 'has_service') + @mock.patch.object(shade.OpenStackCloud, 'search_networks') + def test_get_server_private_ip_devstack( + self, mock_search_networks, mock_has_service, + mock_get_flavor_name, mock_get_image_name): + mock_get_image_name.return_value = 'cirros-0.3.4-x86_64-uec' + mock_get_flavor_name.return_value = 'm1.tiny' + mock_has_service.return_value = True + mock_search_networks.return_value = [ + { + 'id': 'test_pnztt_net', + 'name': 'test_pnztt_net' + }, + { + 'id': 'private', + 'name': 'private', + }, + ] + + cloud = shade.openstack_cloud(validate=False) + srv = cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( + id='test-id', name='test-name', status='ACTIVE', + flavor={u'id': u'1'}, + image={ + 'name': u'cirros-0.3.4-x86_64-uec', + u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1'}, + addresses={u'test_pnztt_net': [{ + u'OS-EXT-IPS:type': u'fixed', + u'addr': PRIVATE_V4, + u'version': 4, + u'OS-EXT-IPS-MAC:mac_addr': + u'fa:16:3e:ae:7d:42' + }]} + ))) + + self.assertEqual(PRIVATE_V4, srv['private_v4']) + mock_has_service.assert_called_with('volume') mock_search_networks.assert_called_with( filters={'router:external': False, 'shared': False} ) @mock.patch.object(shade.OpenStackCloud, 'has_service') - @mock.patch.object(shade.OpenStackCloud, 'search_ports') @mock.patch.object(shade.OpenStackCloud, 'search_networks') - @mock.patch.object(meta, 'get_server_ip') def test_get_server_external_ipv4_neutron( - self, mock_get_server_ip, mock_search_networks, - mock_search_ports, mock_has_service): + self, mock_search_networks, + mock_has_service): # Testing Clouds with Neutron mock_has_service.return_value = True - mock_search_ports.return_value = [{ - 'network_id': 'test-net-id', - 'fixed_ips': [{'ip_address': PUBLIC_V4}], - 'device_id': 'test-id' + mock_search_networks.return_value = [{ + 'id': 'test-net-id', + 'name': 'test-net' }] - mock_search_networks.return_value = [{'id': 'test-net-id'}] srv = meta.obj_to_dict(fakes.FakeServer( - id='test-id', name='test-name', status='ACTIVE')) + id='test-id', name='test-name', status='ACTIVE', + addresses={'test-net': [{ + 'addr': PUBLIC_V4, + 'version': 4}]}, + )) ip = meta.get_server_external_ipv4( cloud=shade.openstack_cloud(validate=False), server=srv) self.assertEqual(PUBLIC_V4, ip) - self.assertFalse(mock_get_server_ip.called) def test_get_server_external_ipv4_neutron_accessIPv4(self): srv = meta.obj_to_dict(fakes.FakeServer( @@ -223,36 +255,26 @@ def test_get_server_external_ipv4_neutron_exception( self.assertTrue(mock_get_server_ip.called) @mock.patch.object(shade.OpenStackCloud, 'has_service') - @mock.patch.object(meta, 'get_server_ip') - @mock.patch.object(_utils, 'is_globally_routable_ipv4') def test_get_server_external_ipv4_nova_public( - self, mock_is_globally_routable_ipv4, - mock_get_server_ip, mock_has_service): + self, mock_has_service): # Testing Clouds w/o Neutron and a network named public mock_has_service.return_value = False - mock_get_server_ip.return_value = None - mock_is_globally_routable_ipv4.return_value = True srv = meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', - addresses={'test-net': [{'addr': PUBLIC_V4}]})) + addresses={'public': [{'addr': PUBLIC_V4, 'version': 4}]})) ip = meta.get_server_external_ipv4( cloud=shade.openstack_cloud(validate=False), server=srv) self.assertEqual(PUBLIC_V4, ip) - self.assertTrue(mock_get_server_ip.called) - self.assertTrue(mock_is_globally_routable_ipv4.called) @mock.patch.object(shade.OpenStackCloud, 'has_service') @mock.patch.object(meta, 'get_server_ip') - @mock.patch.object(_utils, 'is_globally_routable_ipv4') def test_get_server_external_ipv4_nova_none( - self, mock_is_globally_routable_ipv4, - mock_get_server_ip, mock_has_service): + self, mock_get_server_ip, mock_has_service): # Testing Clouds w/o Neutron and a globally routable IP mock_has_service.return_value = False mock_get_server_ip.return_value = None - mock_is_globally_routable_ipv4.return_value = False srv = meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', @@ -262,7 +284,6 @@ def test_get_server_external_ipv4_nova_none( self.assertIsNone(ip) self.assertTrue(mock_get_server_ip.called) - self.assertTrue(mock_is_globally_routable_ipv4.called) def test_get_server_external_ipv6(self): srv = meta.obj_to_dict(fakes.FakeServer( From 10a3bb16ce7e0171c5e91094be32e35d9ad420d6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 3 Oct 2015 13:39:41 -0400 Subject: [PATCH 0511/3836] Use the ipaddress library for ip calculations ipaddress is core in python3, so let's use it instead of regexes. Change-Id: I8050cb9e50c02f47e14584396c3f1815ed71173c --- requirements.txt | 1 + shade/__init__.py | 7 +++- shade/_utils.py | 39 -------------------- shade/meta.py | 15 ++++++-- shade/tests/unit/test_floating_ip_neutron.py | 2 +- 5 files changed, 19 insertions(+), 45 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8e00f5c12..43e8576a0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ pbr>=0.11,<2.0 bunch decorator jsonpatch +ipaddress os-client-config>=1.8.0 six diff --git a/shade/__init__.py b/shade/__init__.py index 7c16875f0..1d87d74b1 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -25,6 +25,7 @@ import glanceclient import glanceclient.exc from glanceclient.common import utils as glance_utils +import ipaddress from ironicclient import client as ironic_client from ironicclient import exceptions as ironic_exceptions import jsonpatch @@ -2148,7 +2149,11 @@ def _neutron_attach_ip_to_server(self, server_id, floating_ip_id, port = ports[0] # Select the first available IPv4 address for address in port.get('fixed_ips', list()): - if _utils.is_ipv4(address['ip_address']): + try: + ip = ipaddress.ip_address(address['ip_address']) + except Exception: + continue + if ip.version == 4: fixed_address = address['ip_address'] break if fixed_address is None: diff --git a/shade/_utils.py b/shade/_utils.py index c3a542b4c..d0a5d4b03 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -12,12 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -import re import time import netifaces -from socket import inet_aton -from struct import unpack from shade import exc @@ -160,42 +157,6 @@ def normalize_nova_secgroup_rules(rules): } for r in rules] -def is_ipv4(ip): - return re.match( - '^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|' - '[01]?[0-9][0-9]?)$', ip) is not None - - -def is_globally_routable_ipv4(ip): - # Comprehensive list of non-globally routable IPv4 networks - ngr_nets = ( - ["192.168.0.0", "255.255.0.0"], # rfc1918 - ["172.16.0.0", "255.240.0.0"], # rfc1918 - ["10.0.0.0", "255.0.0.0"], # rfc1918 - ["192.0.2.0", "255.255.255.0"], # rfc5737 - ["198.51.100.0", "255.255.255.0"], # rfc5737 - ["203.0.113.0", "255.255.255.0"], # rfc5737 - ["169.254.0.0", "255.255.0.0"], # rfc3927 - ["100.64.0.0", "255.192.0.0"], # rfc6598 - ["192.0.0.0", "255.255.255.0"], # rfc5736 - ["192.88.99.0", "255.255.255.0"], # rfc3068 - ["198.18.0.0", "255.254.0.0"], # rfc2544 - ["224.0.0.0", "240.0.0.0"], # rfc5771 - ["240.0.0.0", "240.0.0.0"], # rfc6890 - ["0.0.0.0", "255.0.0.0"], # rfc1700 - ["255.255.255.255", "0.0.0.0"], # rfc6890 - ["127.0.0.0", "255.0.0.0"], # rfc3330 - ) - - int_ip = unpack('!I', inet_aton(ip))[0] - for net in ngr_nets: - mask = unpack('!I', inet_aton(net[1]))[0] - if (int_ip & mask) == unpack('!I', inet_aton(net[0]))[0]: - return False - - return True - - def normalize_nova_floating_ips(ips): """Normalize the structure of Neutron floating IPs diff --git a/shade/meta.py b/shade/meta.py index 62526a1a0..a45d7f3b2 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -14,11 +14,11 @@ import bunch +import ipaddress import six from shade import exc from shade import _log -from shade import _utils NON_CALLABLES = (six.string_types, bool, dict, int, float, list, type(None)) @@ -150,9 +150,16 @@ def get_server_external_ipv4(cloud, server): # Nothing else works, try to find a globally routable IP address for interfaces in server['addresses'].values(): for interface in interfaces: - if _utils.is_ipv4(interface['addr']) and \ - _utils.is_globally_routable_ipv4(interface['addr']): - return interface['addr'] + try: + ip = ipaddress.ip_address(interface['addr']) + except Exception: + # Skip any error, we're looking for a working ip - if the + # cloud returns garbage, it wouldn't be the first weird thing + # but it still doesn't meet the requirement of "be a working + # ip address" + continue + if ip.version == 4 and not ip.is_private: + return str(ip) return None diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 7dfd3dfff..d26a12b98 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -106,7 +106,7 @@ class TestFloatingIP(base.TestCase): 'fixed_ips': [ { 'subnet_id': '008ba151-0b8c-4a67-98b5-0d2b87666062', - 'ip_address': '172.24.4.2' + 'ip_address': u'172.24.4.2' } ], 'id': 'ce705c24-c1ef-408a-bda3-7bbd946164ac', From eb9ce61915df8dbe1b5ced172d0f82c7c4cac856 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 3 Oct 2015 15:37:34 -0400 Subject: [PATCH 0512/3836] Invalidate image cache everytime we make a change The image cache is already set up to only cache a steady-state list. If we simply poke the cache after every time we make an upload or image change, the existing cache code will cause the cache to stay unfilled until the new change hits steady state again. This should make turning on cache do the correct thing. We want to follow this up with a get bypass of the list, but this should keep our API counts to a reasonable level. Change-Id: I47cdfb5af1ba5c56c4005289b9d6b1ecde936978 --- shade/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 1d87d74b1..e35b7047d 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1383,9 +1383,6 @@ def delete_router(self, name_or_id): return True - def _reset_image_cache(self): - self._image_cache = None - def get_image_exclude(self, name_or_id, exclude): for image in self.search_images(name_or_id): if exclude: @@ -1410,6 +1407,7 @@ def get_image_id(self, image_name, exclude=None): def create_image_snapshot(self, name, server, **metadata): image_id = str(self.manager.submitTask(_tasks.ImageSnapshotCreate( image_name=name, server=server, metadata=metadata))) + self.list_images.invalidate(self) return self.get_image(image_id) def delete_image(self, name_or_id, wait=False, timeout=3600): @@ -1424,6 +1422,7 @@ def delete_image(self, name_or_id, wait=False, timeout=3600): elif glance_api_version == '1': self.manager.submitTask( _tasks.ImageDelete(image=image.id)) + self.list_images.invalidate(self) except Exception as e: raise OpenStackCloudException( "Error in deleting image: %s" % str(e)) @@ -1546,6 +1545,7 @@ def _upload_image_task( image_properties=dict(name=name))) glance_task = self.manager.submitTask( _tasks.ImageTaskCreate(**task_args)) + self.list_images.invalidate(self) if wait: image_id = None for count in _utils._iterate_timeout( @@ -1561,8 +1561,6 @@ def _upload_image_task( if status.status == 'success': image_id = status.result['image_id'] - self._reset_image_cache() - self.list_images.invalidate(self) try: image = self.get_image(image_id) except glanceclient.exc.HTTPServiceUnavailable: @@ -1573,12 +1571,12 @@ def _upload_image_task( self.update_image_properties( image=image, **image_properties) - self.list_images.invalidate(self) return self.get_image(status.result['image_id']) if status.status == 'failure': if status.message == IMAGE_ERROR_396: glance_task = self.manager.submitTask( _tasks.ImageTaskCreate(**task_args)) + self.list_images.invalidate(self) else: raise OpenStackCloudException( "Image creation failed: {message}".format( @@ -1614,6 +1612,7 @@ def _update_image_properties_v2(self, image, properties): return False self.manager.submitTask(_tasks.ImageUpdate( image_id=image.id, **img_props)) + self.list_images.invalidate(self) return True def _update_image_properties_v1(self, image, properties): @@ -1625,6 +1624,7 @@ def _update_image_properties_v1(self, image, properties): return False self.manager.submitTask(_tasks.ImageUpdate( image=image, properties=img_props)) + self.list_images.invalidate(self) return True def create_volume(self, wait=True, timeout=None, **kwargs): From cf4effb5f2e268787d3a9134e698633bb9b58c0f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 5 Oct 2015 15:40:30 +0100 Subject: [PATCH 0513/3836] Split get_hostvars_from_server into two There are really two different things going on here. One is figuring out essential information "what IP do I use to talk to this server" The other is fleshing out some additional information that is useful for ansible inventories. The ansible inventory info is expensive when that's not what you're doing and when you're not using shade-level caching. A follow-on patch will make the create/get/list server calls use the new function always. Change-Id: Ia1de78d26c708ce6fe3205a9484cb16a92360890 --- shade/meta.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/shade/meta.py b/shade/meta.py index a45d7f3b2..ca52af24f 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -223,7 +223,8 @@ def get_groups_from_server(cloud, server, server_vars): return groups -def get_hostvars_from_server(cloud, server, mounts=None): +def expand_server_vars(cloud, server): + """Add clean up the server dict with information that is essential.""" server_vars = server server_vars.pop('links', None) @@ -257,6 +258,16 @@ def get_hostvars_from_server(cloud, server, mounts=None): server_vars['region'] = cloud.region_name server_vars['cloud'] = cloud.name + az = server_vars.get('OS-EXT-AZ:availability_zone', None) + if az: + server_vars['az'] = az + return server_vars + + +def get_hostvars_from_server(cloud, server, mounts=None): + """Expand additional server information useful for ansible inventory.""" + server_vars = expand_server_vars(cloud, server) + flavor_id = server['flavor']['id'] flavor_name = cloud.get_flavor_name(flavor_id) if flavor_name: @@ -292,10 +303,6 @@ def get_hostvars_from_server(cloud, server, mounts=None): if 'mount' in mount: vol['mount'] = mount['mount'] - az = server_vars.get('OS-EXT-AZ:availability_zone', None) - if az: - server_vars['az'] = az - return server_vars From fc5f41e6271085d274e02dbfd45b27522b7b484d Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Mon, 5 Oct 2015 12:02:04 -0400 Subject: [PATCH 0514/3836] Update required ironicclient version Update the required version of ironicclient to account for use of keystone session tokens, as the bug was not fixed in ironiclient until 0.9.0. Change-Id: I8b63531ea2494bcf63107477dea24a36268b8c62 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8e00f5c12..e484c9659 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ python-glanceclient>=1.0.0 python-cinderclient<1.2 python-neutronclient>=2.3.10 python-troveclient -python-ironicclient>=0.7.0 +python-ironicclient>=0.9.0 python-swiftclient>=2.5.0 python-designateclient>=1.3.0 From 4f65ac7c905ddc2893b970855cf1cc2b1f1ccbbf Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Mon, 5 Oct 2015 13:38:27 -0700 Subject: [PATCH 0515/3836] Add a private method for nodepool server vars A previous patch split the work done by get_openstack_vars into costly and less-costly operations. Nodepool only needs the less-costly ones. We will put that information in the shade API in some form, but it has not been decided what form yet. In the mean time, allow Nodepool to easily call the method to perform the less-costly operations for now. Change-Id: I99bec7e4a709ef6b7623414dad5f1453070380f8 --- shade/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index e35b7047d..ec0cce650 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1869,6 +1869,12 @@ def get_server_meta(self, server): def get_openstack_vars(self, server): return meta.get_hostvars_from_server(self, server) + def _expand_server_vars(self, server): + # Used by nodepool + # TODO(mordred) remove after these make it into what we + # actually want the API to be. + return meta.expand_server_vars(self, server) + def available_floating_ip(self, network=None): """Get a floating IP from a network or a pool. From 8f943e1afa6d532da342a9fe5e941a4461e04293 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Tue, 6 Oct 2015 13:15:15 -0400 Subject: [PATCH 0516/3836] Update python-troveclient requirement Without a version defined, it is possible that an older trove client may become installed, which may still attempt to utilize the oslo.utils namespace. Update the version of the troveclient library to a version after the oslo namespace change. Change-Id: I1e1f6043a7c82674312016aea662674675404dd8 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e484c9659..5f1e4ac98 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ python-keystoneclient>=0.11.0 python-glanceclient>=1.0.0 python-cinderclient<1.2 python-neutronclient>=2.3.10 -python-troveclient +python-troveclient>=1.2.0 python-ironicclient>=0.9.0 python-swiftclient>=2.5.0 python-designateclient>=1.3.0 From a52c61a7b407b5f63dca6b779c93f06e3c830327 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 6 Oct 2015 13:07:26 +0100 Subject: [PATCH 0517/3836] Provide option to delete floating IP with server Although the only real reason you should need a floating IP are for the cases where you want an IP with a lifespan independent from the server, there are clouds that are insane and require a floating IP to be created with every server. When a person is deleting that server, we should ensure that the floating IP is delete. However, it's impossible to infer intent about whether the floating IP wants to stick around, so it has to be a flag. We will default to False because deleting things that the user did not expect to be deleted is surprising in a bad way. Change-Id: Id90bdc819ad8f1b04acc1b35788786d23f4e7666 --- shade/__init__.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index ec0cce650..ac5849f0a 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2466,12 +2466,29 @@ def rebuild_server(self, server_id, image_id, wait=False, timeout=180): extra_data=dict(server=server)) return meta.obj_to_dict(server) - def delete_server(self, name_or_id, wait=False, timeout=180): + def delete_server( + self, name_or_id, wait=False, timeout=180, delete_ips=False): server = self.get_server(name_or_id) - return self._delete_server(server, wait=wait, timeout=timeout) + return self._delete_server( + server, wait=wait, timeout=timeout, delete_ips=delete_ips) - def _delete_server(self, server, wait=False, timeout=180): + def _delete_server( + self, server, wait=False, timeout=180, delete_ips=False): if server: + if delete_ips: + floating_ip = meta.get_server_ip(server, ext_tag='floating') + if floating_ip: + ips = self.search_floating_ips(filters={ + 'floating_ip_address': floating_ip}) + if len(ips) != 1: + raise OpenStackException( + "Tried to delete floating ip {floating_ip}" + " associated with server {id} but there was" + " an error finding it. Something is exceptionally" + " broken.".format( + floating_ip=floating_ip, + id=server['id'])) + self.delete_floating_ip(ips[0]['id']) try: self.manager.submitTask( _tasks.ServerDelete(server=server['id'])) From 3609cd59078873bc44ebbe7641889423c9f2b8cd Mon Sep 17 00:00:00 2001 From: Anita Kuno Date: Tue, 6 Oct 2015 17:18:54 -0400 Subject: [PATCH 0518/3836] Adds some lines to complete table formatting Currently the vendor support list is mis-formatted for the last two entries on the page. It appears that the tables don't have lines underneath them completing the formatting. This patch adds lines under the last two vendor entries fixing the formatting of the tables for those entries. Change-Id: I6327eef99059291fcce5c9493a622bec33da3824 --- doc/source/vendor-support.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index 598b2d496..b61a05b8c 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -244,6 +244,7 @@ Region Name Human Name ============== ================ nz-por-1 Porirua, NZ nz_wlg_1 Wellington, NZ +============== ================ * Image API Version is 1 * Images must be in `raw` format @@ -258,6 +259,7 @@ Region Name Human Name ============== ================ LS Lausanne, CH ZH Zurich, CH +============== ================ * Images must be in `raw` format * Images must be uploaded using the Glance Task Interface From 310db3848db9c54391a518b458a07ed443086bd3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 6 Oct 2015 17:22:18 +0100 Subject: [PATCH 0519/3836] Add option to floating ip creation to not reuse Although reusing available floating ips is potentially a more efficient use of resources in the cloud, it's a more expensive amount of API calls. Additionally, if the user knows that they are deleting floating ips with servers, then attempting to reuse is just going to be a waste of effort. Co-Authored-By: James E. Blair Change-Id: Ibfb7583232b03bcf29df229862f6da97170aa845 --- shade/__init__.py | 48 +++++++++++++------- shade/tests/unit/test_floating_ip_common.py | 4 +- shade/tests/unit/test_floating_ip_neutron.py | 44 +++++++++++++++--- shade/tests/unit/test_floating_ip_nova.py | 9 ++-- 4 files changed, 74 insertions(+), 31 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index ac5849f0a..8272530c8 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1921,9 +1921,7 @@ def _neutron_available_floating_ips(self, network=None, project_id=None): project_id = self.keystone_session.get_project_id() with self._neutron_exceptions("unable to get available floating IPs"): - networks = self.search_networks( - name_or_id=network, - filters={'router:external': True}) + networks = self.get_external_networks() if not networks: raise OpenStackCloudResourceNotFound( "unable to find an external network") @@ -1940,7 +1938,8 @@ def _neutron_available_floating_ips(self, network=None, project_id=None): if available_ips: return available_ips - # No available IP found, allocate a new Floating IP + # No available IP found or we didn't try + # allocate a new Floating IP f_ip = self._neutron_create_floating_ip( network_name_or_id=networks[0]['id']) @@ -1979,7 +1978,8 @@ def _nova_available_floating_ips(self, pool=None): if available_ips: return available_ips - # No available IP found, allocate a new Floating IP + # No available IP found or we did not try. + # Allocate a new Floating IP f_ip = self._nova_create_floating_ip(pool=pool) return [f_ip] @@ -2271,7 +2271,8 @@ def _nova_detach_ip_from_server(self, server_id, floating_ip_id): return True - def add_ip_from_pool(self, server_id, network, fixed_address=None): + def add_ip_from_pool( + self, server_id, network, fixed_address=None, reuse=True): """Add a floating IP to a sever from a given pool This method reuses available IPs, when possible, or allocate new IPs @@ -2282,10 +2283,14 @@ def add_ip_from_pool(self, server_id, network, fixed_address=None): :param server_id: Id of a server :param network: Nova pool name or Neutron network name or id. :param fixed_address: a fixed address + :param reuse: Try to reuse existing ips. Defaults to True. :returns: the floating IP assigned """ - f_ip = self.available_floating_ip(network=network) + if reuse: + f_ip = self.available_floating_ip(network=network) + else: + f_ip = self.create_floating_ip(network=network) self.attach_ip_to_server( server_id=server_id, floating_ip_id=f_ip['id'], @@ -2313,43 +2318,51 @@ def add_ip_list(self, server, ips): self.attach_ip_to_server( server_id=server['id'], floating_ip_id=f_ip['id']) - def add_auto_ip(self, server, wait=False, timeout=60): + def add_auto_ip(self, server, wait=False, timeout=60, reuse=True): """Add a floating IP to a server. This method is intended for basic usage. For advanced network architecture (e.g. multiple external networks or servers with multiple interfaces), use other floating IP methods. - This method reuses available IPs, when possible, or allocate new IPs - to the current tenant. + This method can reuse available IPs, or allocate new IPs to the current + project. :param server: a server dictionary. + :param reuse: Whether or not to attempt to reuse IPs, defaults + to True. :param wait: (optional) Wait for the address to appear as assigned to the server in Nova. Defaults to False. :param timeout: (optional) Seconds to wait, defaults to 60. See the ``wait`` parameter. + :param reuse: Try to reuse existing ips. Defaults to True. :returns: Floating IP address attached to server. """ - f_ip = self.available_floating_ip() + if reuse: + f_ip = self.available_floating_ip() + else: + f_ip = self.create_floating_ip() + self.attach_ip_to_server( server_id=server['id'], floating_ip_id=f_ip['id'], wait=wait, timeout=timeout) - return self.get_floating_ip(id=f_ip['id']) + return f_ip def add_ips_to_server( self, server, auto_ip=True, ips=None, ip_pool=None, - wait=False, timeout=60): + wait=False, timeout=60, reuse=True): if ip_pool: - self.add_ip_from_pool(server['id'], ip_pool) + self.add_ip_from_pool(server['id'], ip_pool, reuse=reuse) elif ips: self.add_ip_list(server, ips) elif auto_ip: if self.get_server_public_ip(server): return server - self.add_auto_ip(server, wait=wait, timeout=timeout) + self.add_auto_ip( + server, wait=wait, timeout=timeout, reuse=reuse) else: return server @@ -2366,7 +2379,8 @@ def add_ips_to_server( def create_server(self, auto_ip=True, ips=None, ip_pool=None, root_volume=None, terminate_volume=False, - wait=False, timeout=180, **bootkwargs): + wait=False, timeout=180, reuse_ips=True, + **bootkwargs): """Create a virtual server instance. :returns: A dict representing the created server. @@ -2429,7 +2443,7 @@ def create_server(self, auto_ip=True, ips=None, ip_pool=None, ' allocated an IP address.', extra_data=dict(server=server)) return self.add_ips_to_server( - server, auto_ip, ips, ip_pool) + server, auto_ip, ips, ip_pool, reuse=reuse_ips) if server['status'] == 'ERROR': raise OpenStackCloudException( diff --git a/shade/tests/unit/test_floating_ip_common.py b/shade/tests/unit/test_floating_ip_common.py index a0fcbaf3a..3e74fe5bb 100644 --- a/shade/tests/unit/test_floating_ip_common.py +++ b/shade/tests/unit/test_floating_ip_common.py @@ -74,7 +74,7 @@ def test_add_ips_to_server_pool( self.client.add_ips_to_server(server_dict, ip_pool=pool) - mock_add_ip_from_pool.assert_called_with('romeo', pool) + mock_add_ip_from_pool.assert_called_with('romeo', pool, reuse=True) @patch.object(OpenStackCloud, 'nova_client') @patch.object(OpenStackCloud, 'add_ip_list') @@ -105,4 +105,4 @@ def test_add_ips_to_server_auto_ip( self.client.add_ips_to_server(server_dict) mock_add_auto_ip.assert_called_with( - server_dict, wait=False, timeout=60) + server_dict, wait=False, timeout=60, reuse=True) diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index d26a12b98..cd73aa4c5 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -177,8 +177,7 @@ def test_get_floating_ip_not_found( mock_neutron_client.list_floatingips.return_value = \ self.mock_floating_ip_list_rep - floating_ip = self.client.get_floating_ip( - id='non-existent') + floating_ip = self.client.get_floating_ip(id='non-existent') self.assertIsNone(floating_ip) @@ -221,6 +220,32 @@ def test_available_floating_ip_existing( self.mock_floating_ip_new_rep['floatingip']['floating_ip_address'], ip['floating_ip_address']) + @patch.object(OpenStackCloud, 'nova_client') + @patch.object(OpenStackCloud, 'keystone_session') + @patch.object(OpenStackCloud, '_neutron_create_floating_ip') + @patch.object(OpenStackCloud, 'attach_ip_to_server') + @patch.object(OpenStackCloud, 'has_service') + def test_auto_ip_pool_no_reuse( + self, mock_has_service, + mock_attach_ip_to_server, + mock__neutron_create_floating_ip, + mock_keystone_session, + mock_nova_client): + mock_has_service.return_value = True + mock__neutron_create_floating_ip.return_value = \ + self.mock_floating_ip_new_rep['floatingip'] + mock_keystone_session.get_project_id.return_value = \ + '4969c491a3c74ee4af974e6d800c62df' + + self.client.add_ips_to_server( + dict(id='1234'), ip_pool='my-network', reuse=False) + + mock__neutron_create_floating_ip.assert_called_once_with( + network_name_or_id='my-network') + mock_attach_ip_to_server.assert_called_once_with( + server_id='1234', fixed_address=None, + floating_ip_id=self.mock_floating_ip_new_rep['floatingip']['id']) + @patch.object(OpenStackCloud, 'keystone_session') @patch.object(OpenStackCloud, '_neutron_create_floating_ip') @patch.object(OpenStackCloud, '_neutron_list_floating_ips') @@ -244,11 +269,15 @@ def test_available_floating_ip_new( self.mock_floating_ip_new_rep['floatingip']['floating_ip_address'], ip['floating_ip_address']) + @patch.object(OpenStackCloud, 'get_floating_ip') @patch.object(OpenStackCloud, 'neutron_client') @patch.object(OpenStackCloud, 'has_service') def test_delete_floating_ip_existing( - self, mock_has_service, mock_neutron_client): + self, mock_has_service, mock_neutron_client, mock_get_floating_ip): mock_has_service.return_value = True + mock_get_floating_ip.return_value = { + 'id': '2f245a7b-796b-4f26-9cf9-9e82d248fda7', + } mock_neutron_client.delete_floatingip.return_value = None ret = self.client.delete_floating_ip( @@ -300,15 +329,16 @@ def test_attach_ip_to_server( } ) - @patch.object(OpenStackCloud, '_neutron_list_floating_ips') + @patch.object(OpenStackCloud, 'get_floating_ip') @patch.object(OpenStackCloud, 'neutron_client') @patch.object(OpenStackCloud, 'has_service') def test_detach_ip_from_server( self, mock_has_service, mock_neutron_client, - mock__neutron_list_floating_ips): + mock_get_floating_ip): mock_has_service.return_value = True - mock__neutron_list_floating_ips.return_value = \ - self.mock_floating_ip_list_rep['floatingips'] + mock_get_floating_ip.return_value = \ + _utils.normalize_neutron_floating_ips( + self.mock_floating_ip_list_rep['floatingips'])[0] self.client.detach_ip_from_server( server_id='server-id', diff --git a/shade/tests/unit/test_floating_ip_nova.py b/shade/tests/unit/test_floating_ip_nova.py index d7179156a..f290e3a0f 100644 --- a/shade/tests/unit/test_floating_ip_nova.py +++ b/shade/tests/unit/test_floating_ip_nova.py @@ -20,9 +20,8 @@ """ from mock import patch -import os_client_config - from novaclient import exceptions as n_exc +import os_client_config from shade import OpenStackCloud from shade.tests.fakes import FakeFloatingIP @@ -187,10 +186,10 @@ def test_delete_floating_ip_existing( self.assertTrue(ret) @patch.object(OpenStackCloud, 'nova_client') - @patch.object(OpenStackCloud, 'has_service') + @patch.object(OpenStackCloud, 'get_floating_ip') def test_delete_floating_ip_not_found( - self, mock_has_service, mock_nova_client): - mock_has_service.side_effect = has_service_side_effect + self, mock_get_floating_ip, mock_nova_client): + mock_get_floating_ip.return_value = None mock_nova_client.floating_ips.delete.side_effect = n_exc.NotFound( code=404) From 5dc91cfdf94470d420d457388494227ac09a6a95 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 6 Oct 2015 13:13:58 -0400 Subject: [PATCH 0520/3836] Add get_server_by_id optmization There are times when you know you have a server id, so doing a list/search/filter is inefficient. Provide a get_server_by_id method so that in those situations you can bypass searching server lists. Also, use the method in the other places. Change-Id: I97bd52dff0854a3b4d41b5572e7490fa4df18e1e --- shade/__init__.py | 26 +++++++++------------ shade/meta.py | 3 +++ shade/tests/unit/test_create_server.py | 31 ++++++++++++++------------ 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 8272530c8..b7bc436ad 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1139,9 +1139,13 @@ def get_security_group(self, name_or_id, filters=None): return _utils._get_entity( self.search_security_groups, name_or_id, filters) - def get_server(self, name_or_id, filters=None): + def get_server(self, name_or_id=None, filters=None): return _utils._get_entity(self.search_servers, name_or_id, filters) + def get_server_by_id(self, id): + return meta.obj_to_dict( + self.manager.submitTask(_tasks.ServerGet(server=id))) + def get_image(self, name_or_id, filters=None): return _utils._get_entity(self.search_images, name_or_id, filters) @@ -2136,7 +2140,7 @@ def attach_ip_to_server( for _ in _utils._iterate_timeout( timeout, "Timeout waiting for the floating IP to be attached."): - server = self.get_server(name_or_id=server_id) + server = self.get_server_by_id(server_id) for k, v in server['addresses'].items(): for interface_spec in v: if interface_spec['addr'] == \ @@ -2370,8 +2374,7 @@ def add_ips_to_server( # floating IP, then it needs to be obtained from # a recent server object if the above code path exec'd try: - server = meta.obj_to_dict( - self.manager.submitTask(_tasks.ServerGet(server=server))) + server = self.get_server_by_id(server['id']) except Exception as e: raise OpenStackCloudException( "Error in getting info from instance: %s " % str(e)) @@ -2398,7 +2401,7 @@ def create_server(self, auto_ip=True, ips=None, ip_pool=None, try: server = self.manager.submitTask(_tasks.ServerCreate(**bootkwargs)) - server = self.manager.submitTask(_tasks.ServerGet(server=server)) + server = self.get_server_by_id(server.id) except Exception as e: raise OpenStackCloudException( "Error in creating instance: {0}".format(e)) @@ -2410,10 +2413,7 @@ def create_server(self, auto_ip=True, ips=None, ip_pool=None, timeout, "Timeout waiting for the server to come up."): try: - server = meta.obj_to_dict( - self.manager.submitTask( - _tasks.ServerGet(server=server)) - ) + server = self.get_server_by_id(server.id) except Exception: continue @@ -2464,10 +2464,7 @@ def rebuild_server(self, server_id, image_id, wait=False, timeout=180): "Timeout waiting for server {0} to " "rebuild.".format(server_id)): try: - server = meta.obj_to_dict( - self.manager.submitTask( - _tasks.ServerGet(server=server)) - ) + server = self.get_server_by_id(server_id) except Exception: continue @@ -2519,8 +2516,7 @@ def _delete_server( timeout, "Timed out waiting for server to get deleted."): try: - server = self.manager.submitTask( - _tasks.ServerGet(server=server['id'])) + server = self.get_server_by_id(server['id']) if not server: return server = meta.obj_to_dict(server) diff --git a/shade/meta.py b/shade/meta.py index ca52af24f..d002f5d9b 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -315,6 +315,9 @@ def obj_to_dict(obj): that we can just have a plain dict of all of the values that exist in the nova metadata for a server. """ + # If we obj_to_dict twice, don't fail, just return the bunch + if type(obj) == bunch.Bunch: + return obj instance = bunch.Bunch() for key in dir(obj): value = getattr(obj, key) diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index 63a3b4db5..d8b71d4f4 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -82,10 +82,13 @@ def test_create_server_wait_server_error(self): raises an exception in create_server. """ with patch("shade.OpenStackCloud"): + fake_server = fakes.FakeServer('1234', '', 'BUILD') + error_server = fakes.FakeServer('1234', '', 'ERROR') config = { - "servers.create.return_value": Mock(status="BUILD"), + "servers.create.return_value": fake_server, "servers.get.side_effect": [ - Mock(status="BUILD"), Mock(status="ERROR")] + fake_server, error_server + ] } OpenStackCloud.nova_client = Mock(**config) self.assertRaises( @@ -98,9 +101,10 @@ def test_create_server_with_timeout(self): exception in create_server. """ with patch("shade.OpenStackCloud"): + fake_server = fakes.FakeServer('1234', '', 'BUILD') config = { - "servers.create.return_value": Mock(status="BUILD"), - "servers.get.return_value": Mock(status="BUILD") + "servers.create.return_value": fake_server, + "servers.get.return_value": fake_server } OpenStackCloud.nova_client = Mock(**config) self.assertRaises( @@ -113,7 +117,7 @@ def test_create_server_no_wait(self): novaclient create call returns the server instance. """ with patch("shade.OpenStackCloud"): - fake_server = fakes.FakeServer('', '', 'BUILD') + fake_server = fakes.FakeServer('1234', '', 'BUILD') config = { "servers.create.return_value": fake_server, "servers.get.return_value": fake_server @@ -128,13 +132,13 @@ def test_create_server_wait(self): its status changes to "ACTIVE". """ with patch("shade.OpenStackCloud"): + building_server = fakes.FakeServer( + '1234', '', 'ACTIVE', addresses=dict(public='1.1.1.1')) fake_server = fakes.FakeServer( - '', '', 'ACTIVE', addresses=dict(public='1.1.1.1')) + '1234', '', 'ACTIVE', addresses=dict(public='1.1.1.1')) config = { - "servers.create.return_value": fakes.FakeServer('', '', - 'ACTIVE'), - "servers.get.side_effect": [ - Mock(status="BUILD"), fake_server] + "servers.create.return_value": building_server, + "servers.get.return_value": fake_server, } OpenStackCloud.nova_client = Mock(**config) with patch.object(OpenStackCloud, "add_ips_to_server", @@ -149,12 +153,11 @@ def test_create_server_no_addresses(self): server doesn't have addresses. """ with patch("shade.OpenStackCloud"): - fake_server = fakes.FakeServer('', '', 'ACTIVE') + fake_server = fakes.FakeServer('1234', '', 'ACTIVE') config = { - "servers.create.return_value": fakes.FakeServer('', '', - 'ACTIVE'), + "servers.create.return_value": fake_server, "servers.get.side_effect": [ - Mock(status="BUILD"), fake_server] + fakes.FakeServer('1234', '', 'BUILD'), fake_server] } OpenStackCloud.nova_client = Mock(**config) with patch.object(OpenStackCloud, "add_ips_to_server", From 1a719d3e45dea29680c866ee07624617616a8cd5 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 7 Oct 2015 17:34:56 -0400 Subject: [PATCH 0521/3836] Add methods to update internal router interfaces Adding internal interfaces to a router is a useful thing. This adds methods that will allow a user to add and remove them as separate steps. A possible next step will be to allow this to be done during router creation. Change-Id: Ieb007de3d68d5b8a972df5178c032add8e59377a --- shade/__init__.py | 56 +++++++++++++++++++++++++++ shade/_tasks.py | 12 +++++- shade/tests/functional/test_router.py | 23 +++++++++++ shade/tests/unit/test_shade.py | 14 +++++++ 4 files changed, 104 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index ac5849f0a..200c26aa3 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1259,6 +1259,62 @@ def _build_external_gateway_info(self, ext_gateway_net_id, enable_snat, return info return None + def add_router_interface(self, router, subnet_id=None, port_id=None): + """Attach a subnet to an internal router interface. + + Either a subnet ID or port ID must be specified for the internal + interface. Supplying both will result in an error. + + :param dict router: The dict object of the router being changed + :param string subnet_id: The ID of the subnet to use for the interface + :param string port_id: The ID of the port to use for the interface + + :returns: A dict with the router id (id), subnet ID (subnet_id), + port ID (port_id) and tenant ID (tenant_id). + + :raises: OpenStackCloudException on operation error. + """ + body = {} + if subnet_id: + body['subnet_id'] = subnet_id + if port_id: + body['port_id'] = port_id + + with self._neutron_exceptions( + "Error attaching interface to router {0}".format(router['id']) + ): + return self.manager.submitTask( + _tasks.RouterAddInterface(router=router['id'], body=body) + ) + + def remove_router_interface(self, router, subnet_id=None, port_id=None): + """Detach a subnet from an internal router interface. + + If you specify both subnet and port ID, the subnet ID must + correspond to the subnet ID of the first IP address on the port + specified by the port ID. Otherwise an error occurs. + + :param dict router: The dict object of the router being changed + :param string subnet_id: The ID of the subnet to use for the interface + :param string port_id: The ID of the port to use for the interface + + :returns: None on success + + :raises: OpenStackCloudException on operation error. + """ + body = {} + if subnet_id: + body['subnet_id'] = subnet_id + if port_id: + body['port_id'] = port_id + + with self._neutron_exceptions( + "Error detaching interface from router {0}".format(router['id']) + ): + return self.manager.submitTask( + _tasks.RouterRemoveInterface(router=router['id'], body=body) + ) + def create_router(self, name=None, admin_state_up=True, ext_gateway_net_id=None, enable_snat=None, ext_fixed_ips=None): diff --git a/shade/_tasks.py b/shade/_tasks.py index 01cda0876..dda19df7c 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -148,7 +148,17 @@ def main(self, client): class RouterDelete(task_manager.Task): def main(self, client): - client.neutron_client.delete_router(**self.args) + return client.neutron_client.delete_router(**self.args) + + +class RouterAddInterface(task_manager.Task): + def main(self, client): + return client.neutron_client.add_interface_router(**self.args) + + +class RouterRemoveInterface(task_manager.Task): + def main(self, client): + client.neutron_client.remove_interface_router(**self.args) class GlanceImageList(task_manager.Task): diff --git a/shade/tests/functional/test_router.py b/shade/tests/functional/test_router.py index 22b9e21c7..5ddb8f9f1 100644 --- a/shade/tests/functional/test_router.py +++ b/shade/tests/functional/test_router.py @@ -162,6 +162,29 @@ def _create_and_verify_advanced_router(self): def test_create_router_advanced(self): self._create_and_verify_advanced_router() + def test_add_remove_router_interface(self): + router = self._create_and_verify_advanced_router() + net_name = self.network_prefix + '_intnet1' + sub_name = self.subnet_prefix + '_intsub1' + net = self.cloud.create_network(name=net_name) + sub = self.cloud.create_subnet( + net['id'], '10.4.4.0/24', subnet_name=sub_name, + gateway_ip='10.4.4.1' + ) + + iface = self.cloud.add_router_interface(router, subnet_id=sub['id']) + self.assertIsNone( + self.cloud.remove_router_interface(router, subnet_id=sub['id']) + ) + + # Test return values *after* the interface is detached so the + # resources we've created can be cleaned up if these asserts fail. + self.assertIsNotNone(iface) + for key in ('id', 'subnet_id', 'port_id', 'tenant_id'): + self.assertIn(key, iface) + self.assertEqual(router['id'], iface['id']) + self.assertEqual(sub['id'], iface['subnet_id']) + def test_update_router_name(self): router = self._create_and_verify_advanced_router() diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 3219a085c..375aec4cc 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -138,6 +138,20 @@ def test_create_router(self, mock_client): self.cloud.create_router(name='goofy', admin_state_up=True) self.assertTrue(mock_client.create_router.called) + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_add_router_interface(self, mock_client): + self.cloud.add_router_interface({'id': '123'}, subnet_id='abc') + mock_client.add_interface_router.assert_called_once_with( + router='123', body={'subnet_id': 'abc'} + ) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_remove_router_interface(self, mock_client): + self.cloud.remove_router_interface({'id': '123'}, subnet_id='abc') + mock_client.remove_interface_router.assert_called_once_with( + router='123', body={'subnet_id': 'abc'} + ) + @mock.patch.object(shade.OpenStackCloud, 'get_router') @mock.patch.object(shade.OpenStackCloud, 'neutron_client') def test_update_router(self, mock_client, mock_get): From 9b03198c8f95a4de888b2ad4e90404a96829da86 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 6 Oct 2015 13:28:49 -0400 Subject: [PATCH 0522/3836] Add get/list/search methods for identity roles This partially replaces: Ifd74cbcef9dd0f3531912995da8ad7b75dd19d44 Adds read methods for identity roles. Note that the original code (referenced above) puts these methods in OpenStackCloud, but this puts these methods in OperatorCloud because these methods require admin-level privileges in keystone's policy file. Change-Id: I896972eb08a804b5dc6cd9c87a754598a90933a5 Co-Authored-By: Haneef Ali Co-Authored-By: Monty Taylor --- shade/__init__.py | 51 +++++++++++++++++++++++++ shade/_tasks.py | 5 +++ shade/tests/fakes.py | 6 +++ shade/tests/functional/test_identity.py | 47 +++++++++++++++++++++++ shade/tests/unit/test_identity_roles.py | 42 ++++++++++++++++++++ 5 files changed, 151 insertions(+) create mode 100644 shade/tests/functional/test_identity.py create mode 100644 shade/tests/unit/test_identity_roles.py diff --git a/shade/__init__.py b/shade/__init__.py index ac5849f0a..29f08df77 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -4404,6 +4404,57 @@ def get_identity_domain(self, domain_id): raise OpenStackCloudException(str(e)) return meta.obj_to_dict(domain) + def list_roles(self): + """List Keystone roles. + + :returns: a list of dicts containing the role description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + try: + roles = self.manager.submitTask(_tasks.RoleList()) + except Exception as e: + raise OpenStackCloudException(str(e)) + return meta.obj_list_to_dict(roles) + + def search_roles(self, name_or_id=None, filters=None): + """Seach Keystone roles. + + :param string name: role name or id. + :param dict filters: a dict containing additional filters to use. + + :returns: a list of dict containing the role description. Each dict + contains the following attributes:: + + - id: + - name: + - description: + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + roles = self.list_roles() + return _utils._filter_list(roles, name_or_id, filters) + + def get_role(self, name_or_id, filters=None): + """Get exactly one Keystone role. + + :param id: role name or id. + :param filters: a dict containing additional filters to use. + + :returns: a list of dict containing the role description. Each dict + contains the following attributes:: + + - id: + - name: + - description: + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + return _utils._get_entity(self.search_roles, name_or_id, filters) + def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", ephemeral=0, swap=0, rxtx_factor=1.0, is_public=True): """Create a new flavor. diff --git a/shade/_tasks.py b/shade/_tasks.py index 01cda0876..8a653eccd 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -574,3 +574,8 @@ def main(self, client): class RecordGet(task_manager.Task): def main(self, client): return client.designate_client.records.get(**self.args) + + +class RoleList(task_manager.Task): + def main(self, client): + return client.keystone_client.roles.list() diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 7aacb7fcf..362c38ccf 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -154,3 +154,9 @@ def __init__(self, id, name, description, enabled): self.name = name self.description = description self.enabled = enabled + + +class FakeRole(object): + def __init__(self, id, name): + self.id = id + self.name = name diff --git a/shade/tests/functional/test_identity.py b/shade/tests/functional/test_identity.py new file mode 100644 index 000000000..adeacc23d --- /dev/null +++ b/shade/tests/functional/test_identity.py @@ -0,0 +1,47 @@ +# -*- 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. + +""" +test_identity +---------------------------------- + +Functional tests for `shade` identity methods. +""" + +from shade import operator_cloud +from shade.tests import base + + +class TestIdentity(base.TestCase): + def setUp(self): + super(TestIdentity, self).setUp() + self.cloud = operator_cloud(cloud='devstack-admin') + + def test_list_roles(self): + roles = self.cloud.list_roles() + self.assertIsNotNone(roles) + self.assertNotEqual([], roles) + + def test_get_role(self): + role = self.cloud.get_role('admin') + self.assertIsNotNone(role) + self.assertIn('id', role) + self.assertIn('name', role) + self.assertEqual('admin', role['name']) + + def test_search_roles(self): + roles = self.cloud.search_roles(filters={'name': 'admin'}) + self.assertIsNotNone(roles) + self.assertEqual(1, len(roles)) + self.assertEqual('admin', roles[0]['name']) diff --git a/shade/tests/unit/test_identity_roles.py b/shade/tests/unit/test_identity_roles.py new file mode 100644 index 000000000..d3ab3309f --- /dev/null +++ b/shade/tests/unit/test_identity_roles.py @@ -0,0 +1,42 @@ +# 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 shade +from shade.tests.unit import base +from shade.tests import fakes + + +class TestIdentityRoles(base.TestCase): + + def setUp(self): + super(TestIdentityRoles, self).setUp() + self.cloud = shade.operator_cloud(validate=False) + + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_list_roles(self, mock_keystone): + self.cloud.list_roles() + self.assertTrue(mock_keystone.roles.list.called) + + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_get_role(self, mock_keystone): + role_obj = fakes.FakeRole(id='1234', name='fake_role') + mock_keystone.roles.list.return_value = [role_obj] + + role = self.cloud.get_role('fake_role') + + self.assertTrue(mock_keystone.roles.list.called) + self.assertIsNotNone(role) + self.assertEqual('1234', role['id']) + self.assertEqual('fake_role', role['name']) From 801d09a8dae6a976ddf1603a0b4ceb1bd4166731 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 9 Oct 2015 15:18:27 -0500 Subject: [PATCH 0523/3836] Add auth hook for OpenStackClient Since os_client_config started pulling in ksa plugins we have some duplicate behaviour with OSC. If o-c-c is going to be loading auth plugins OSC needs a way to hook into the notch between setting config values and actually loading the plugins in order to maintain existing behaviours for compatibility. It may also be desirable at some point to defer the plugin loading, OSC did that orignally due to ksc's poor load times, ksa shouldn't have the same problem. Change-Id: I1e8a50e5467fd4f9151a64aa7ae140d2654f3186 --- os_client_config/config.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/os_client_config/config.py b/os_client_config/config.py index f43fe7458..c87870d2a 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -433,6 +433,14 @@ def _find_winning_auth_value(self, opt, config): if d_opt_name in config: return config[d_opt_name] + 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. + """ + return config + def _get_auth_loader(self, config): # Re-use the admin_token plugin for the "None" plugin # since it does not look up endpoints or tokens but rather @@ -576,6 +584,11 @@ def get_one_cloud(self, cloud=None, validate=True, if type(config[key]) is not bool: config[key] = get_boolean(config[key]) + # NOTE(dtroyer): OSC needs a hook into the auth args before the + # plugin is loaded in order to maintain backward- + # compatible behaviour + config = self.auth_config_hook(config) + if loading: if validate: try: From 7a347bf7a3fec7c8bd0a76abc32756d4535ac5ee Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 8 Oct 2015 09:30:14 -0400 Subject: [PATCH 0524/3836] Use keystone v3 service type argument In v3 this changed to be just type (completely ignoring, btw, the fact that it's a python word) Also, recognize keystone version in post_test_hook Check the environment variable set in the keystone v2 job and modify the clouds.yaml file appropriately to use v2. Also, the parameter to delete changed from id in v2 to service in v3. Co-Authored-By: David Shrewsbury Change-Id: I988848b734fa0f83d149c902850db2ad119d959a --- shade/__init__.py | 123 +++++++++++++----- .../tests/functional/hooks/post_test_hook.sh | 12 ++ shade/tests/functional/test_endpoints.py | 52 +++++--- shade/tests/functional/test_services.py | 8 +- shade/tests/unit/test_caching.py | 5 +- shade/tests/unit/test_endpoints.py | 8 +- shade/tests/unit/test_services.py | 3 +- 7 files changed, 148 insertions(+), 63 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index ac5849f0a..196a4e732 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -432,8 +432,9 @@ def _project_manager(self): # Keystone v2 calls this attribute tenants # Keystone v3 calls it projects # Yay for usable APIs! - return getattr( - self.keystone_client, 'projects', self.keystone_client.tenants) + if self.cloud_config.get_api_version('identity').startswith('2'): + return self.keystone_client.tenants + return self.keystone_client.projects def _get_project_param_dict(self, name_or_id): project_dict = dict() @@ -4071,18 +4072,18 @@ def purge_node_instance_info(self, uuid): except Exception as e: raise OpenStackCloudException(str(e)) - def create_service(self, name, service_type, description=None): + def create_service(self, name, type, description=None): """Create a service. :param name: Service name. - :param service_type: Service type. + :param type: Service type. :param description: Service description (optional). :returns: a dict containing the services description, i.e. the following attributes:: - id: - name: - - service_type: + - type: - description: :raises: ``OpenStackCloudException`` if something goes wrong during the @@ -4090,9 +4091,13 @@ def create_service(self, name, service_type, description=None): """ try: + if self.cloud_config.get_api_version('identity').startswith('2'): + service_kwargs = {'service_type': type} + else: + service_kwargs = {'type': type} + service = self.manager.submitTask(_tasks.ServiceCreate( - name=name, service_type=service_type, - description=description)) + name=name, description=description, **service_kwargs)) except Exception as e: raise OpenStackCloudException( "Failed to create service {name}: {msg}".format( @@ -4118,7 +4123,7 @@ def search_services(self, name_or_id=None, filters=None): :param name_or_id: Name or id of the desired service. :param filters: a dict containing additional filters to use. e.g. - {'service_type': 'network'}. + {'type': 'network'}. :returns: a list of dict containing the services description. @@ -4133,13 +4138,13 @@ def get_service(self, name_or_id, filters=None): :param name_or_id: Name or id of the desired service. :param filters: a dict containing additional filters to use. e.g. - {'service_type': 'network'} + {'type': 'network'} :returns: a dict containing the services description, i.e. the following attributes:: - id: - name: - - service_type: + - type: - description: :raises: ``OpenStackCloudException`` if something goes wrong during the @@ -4162,8 +4167,12 @@ def delete_service(self, name_or_id): self.log.debug("Service %s not found for deleting" % name_or_id) return False + if self.cloud_config.get_api_version('identity').startswith('2'): + service_kwargs = {'id': service['id']} + else: + service_kwargs = {'service': service['id']} try: - self.manager.submitTask(_tasks.ServiceDelete(id=service['id'])) + self.manager.submitTask(_tasks.ServiceDelete(**service_kwargs)) except Exception as e: raise OpenStackCloudException( "Failed to delete service {id}: {msg}".format( @@ -4171,40 +4180,90 @@ def delete_service(self, name_or_id): msg=str(e))) return True - def create_endpoint(self, service_name_or_id, public_url, - internal_url=None, admin_url=None, region=None): + @valid_kwargs('public_url', 'internal_url', 'admin_url') + def create_endpoint(self, service_name_or_id, url=None, interface=None, + region=None, enabled=True, **kwargs): """Create a Keystone endpoint. :param service_name_or_id: Service name or id for this endpoint. + :param url: URL of the endpoint + :param interface: Interface type of the endpoint :param public_url: Endpoint public URL. :param internal_url: Endpoint internal URL. :param admin_url: Endpoint admin URL. :param region: Endpoint region. + :param enabled: Whether the endpoint is enabled - :returns: a dict containing the endpoint description. + NOTE: Both v2 (public_url, internal_url, admin_url) and v3 + (url, interface) calling semantics are supported. But + you can only use one of them at a time. + + :returns: a list of dicts containing the endpoint description. :raises: OpenStackCloudException if the service cannot be found or if something goes wrong during the openstack API call. """ - # ToDo: support v3 api (dguerri) + if url and kwargs: + raise OpenStackCloudException( + "create_endpoint takes either url and interace OR" + " public_url, internal_url, admin_url") + service = self.get_service(name_or_id=service_name_or_id) if service is None: raise OpenStackCloudException("service {service} not found".format( service=service_name_or_id)) - try: - endpoint = self.manager.submitTask(_tasks.EndpointCreate( - service_id=service['id'], - region=region, - publicurl=public_url, - internalurl=internal_url, - adminurl=admin_url - )) - except Exception as e: - raise OpenStackCloudException( - "Failed to create endpoint for service {service}: " - "{msg}".format(service=service['name'], - msg=str(e))) - return meta.obj_to_dict(endpoint) + + endpoints = [] + endpoint_args = [] + if url: + urlkwargs = {} + if self.cloud_config.get_api_version('identity').startswith('2'): + if interface != 'public': + raise OpenStackCloudException( + "Error adding endpoint for service {service}." + " On a v2 cloud the url/interface API may only be" + " used for public url. Try using the public_url," + " internal_url, admin_url parameters instead of" + " url and interface".format( + service=service_name_or_id)) + urlkwargs['%url' % interface] = url + urlkwargs['service_id'] = service['id'] + else: + urlkwargs['url'] = url + urlkwargs['interface'] = interface + urlkwargs['enabled'] = enabled + urlkwargs['service'] = service['id'] + endpoint_args.append(urlkwargs) + else: + if self.cloud_config.get_api_version( + 'identity').startswith('2'): + urlkwargs = {} + for arg_key, arg_val in kwargs.items(): + urlkwargs[arg_key.replace('_', '')] = arg_val + urlkwargs['service_id'] = service['id'] + endpoint_args.append(urlkwargs) + else: + for arg_key, arg_val in kwargs.items(): + urlkwargs = {} + urlkwargs['url'] = arg_val + urlkwargs['interface'] = arg_key.split('_')[0] + urlkwargs['enabled'] = enabled + urlkwargs['service'] = service['id'] + endpoint_args.append(urlkwargs) + + for args in endpoint_args: + try: + endpoint = self.manager.submitTask(_tasks.EndpointCreate( + region=region, + **args + )) + except Exception as e: + raise OpenStackCloudException( + "Failed to create endpoint for service {service}: " + "{msg}".format(service=service['name'], + msg=str(e))) + endpoints.append(endpoint) + return meta.obj_list_to_dict(endpoints) def list_endpoints(self): """List Keystone endpoints. @@ -4276,8 +4335,12 @@ def delete_endpoint(self, id): self.log.debug("Endpoint %s not found for deleting" % id) return False + if self.cloud_config.get_api_version('identity').startswith('2'): + endpoint_kwargs = {'id': endpoint['id']} + else: + endpoint_kwargs = {'endpoint': endpoint['id']} try: - self.manager.submitTask(_tasks.EndpointDelete(id=id)) + self.manager.submitTask(_tasks.EndpointDelete(**endpoint_kwargs)) except Exception as e: raise OpenStackCloudException( "Failed to delete endpoint {id}: {msg}".format( diff --git a/shade/tests/functional/hooks/post_test_hook.sh b/shade/tests/functional/hooks/post_test_hook.sh index 8a2f97ee1..128e4a893 100755 --- a/shade/tests/functional/hooks/post_test_hook.sh +++ b/shade/tests/functional/hooks/post_test_hook.sh @@ -22,6 +22,18 @@ sudo chown -R jenkins:stack ~jenkins/.config cd $SHADE_DIR sudo chown -R jenkins:stack $SHADE_DIR + +CLOUDS_YAML=~jenkins/.config/openstack/clouds.yaml + +# Devstack runs both keystone v2 and v3. An environment variable is set +# within the shade keystone v2 job that tells us which version we should +# test against. +if [ ${SHADE_USE_KEYSTONE_V2:-0} -eq 1 ] +then + sed -ie "s/identity_api_version: '3'/identity_api_version: '2.0'/g" $CLOUDS_YAML + sed -ie '/^.*domain_id.*$/d' $CLOUDS_YAML +fi + echo "Running shade functional test suite" set +e sudo -E -H -u jenkins tox -efunctional diff --git a/shade/tests/functional/test_endpoints.py b/shade/tests/functional/test_endpoints.py index 79c925ea1..03ac0eb12 100644 --- a/shade/tests/functional/test_endpoints.py +++ b/shade/tests/functional/test_endpoints.py @@ -81,34 +81,36 @@ def test_create_endpoint(self): service_name = self.new_item_name + '_create' service = self.operator_cloud.create_service( - name=service_name, service_type='test_type', + name=service_name, type='test_type', description='this is a test description') - endpoint = self.operator_cloud.create_endpoint( + endpoints = self.operator_cloud.create_endpoint( service_name_or_id=service['id'], public_url='http://public.test/', internal_url='http://internal.test/', admin_url='http://admin.url/', region=service_name) - self.assertIsNotNone(endpoint.get('id')) + self.assertNotEqual([], endpoints) + self.assertIsNotNone(endpoints[0].get('id')) # Test None parameters - endpoint = self.operator_cloud.create_endpoint( + endpoints = self.operator_cloud.create_endpoint( service_name_or_id=service['id'], public_url='http://public.test/', region=service_name) - self.assertIsNotNone(endpoint.get('id')) + self.assertNotEqual([], endpoints) + self.assertIsNotNone(endpoints[0].get('id')) def test_list_endpoints(self): service_name = self.new_item_name + '_list' service = self.operator_cloud.create_service( - name=service_name, service_type='test_type', + name=service_name, type='test_type', description='this is a test description') - endpoint = self.operator_cloud.create_endpoint( + endpoints = self.operator_cloud.create_endpoint( service_name_or_id=service['id'], public_url='http://public.test/', internal_url='http://internal.test/', @@ -118,12 +120,21 @@ def test_list_endpoints(self): found = False for e in observed_endpoints: # Test all attributes are returned - if e['id'] == endpoint['id']: - found = True - self.assertEqual(service['id'], e['service_id']) - self.assertEqual('http://public.test/', e['publicurl']) - self.assertEqual('http://internal.test/', e['internalurl']) - self.assertEqual(service_name, e['region']) + for endpoint in endpoints: + if e['id'] == endpoint['id']: + found = True + self.assertEqual(service['id'], e['service_id']) + if 'interface' in e: + if 'interface' == 'internal': + self.assertEqual('http://internal.test/', e['url']) + elif 'interface' == 'public': + self.assertEqual('http://public.test/', e['url']) + else: + self.assertEqual('http://public.test/', + e['publicurl']) + self.assertEqual('http://internal.test/', + e['internalurl']) + self.assertEqual(service_name, e['region']) self.assertTrue(found, msg='new endpoint not found in endpoints list!') @@ -131,22 +142,25 @@ def test_delete_endpoint(self): service_name = self.new_item_name + '_delete' service = self.operator_cloud.create_service( - name=service_name, service_type='test_type', + name=service_name, type='test_type', description='this is a test description') - endpoint = self.operator_cloud.create_endpoint( + endpoints = self.operator_cloud.create_endpoint( service_name_or_id=service['id'], public_url='http://public.test/', internal_url='http://internal.test/', region=service_name) - self.operator_cloud.delete_endpoint(endpoint['id']) + self.assertNotEqual([], endpoints) + for endpoint in endpoints: + self.operator_cloud.delete_endpoint(endpoint['id']) observed_endpoints = self.operator_cloud.list_endpoints() found = False for e in observed_endpoints: - if e['id'] == endpoint['id']: - found = True - break + for endpoint in endpoints: + if e['id'] == endpoint['id']: + found = True + break self.failUnlessEqual( False, found, message='new endpoint was not deleted!') diff --git a/shade/tests/functional/test_services.py b/shade/tests/functional/test_services.py index 3c6bc589f..48faf10d3 100644 --- a/shade/tests/functional/test_services.py +++ b/shade/tests/functional/test_services.py @@ -61,13 +61,13 @@ def _cleanup_services(self): def test_create_service(self): service = self.operator_cloud.create_service( - name=self.new_service_name + '_create', service_type='test_type', + name=self.new_service_name + '_create', type='test_type', description='this is a test description') self.assertIsNotNone(service.get('id')) def test_list_services(self): service = self.operator_cloud.create_service( - name=self.new_service_name + '_list', service_type='test_type') + name=self.new_service_name + '_list', type='test_type') observed_services = self.operator_cloud.list_services() self.assertIsInstance(observed_services, list) found = False @@ -84,7 +84,7 @@ def test_delete_service_by_name(self): # Test delete by name service = self.operator_cloud.create_service( name=self.new_service_name + '_delete_by_name', - service_type='test_type') + type='test_type') self.operator_cloud.delete_service(name_or_id=service['name']) observed_services = self.operator_cloud.list_services() found = False @@ -98,7 +98,7 @@ def test_delete_service_by_id(self): # Test delete by id service = self.operator_cloud.create_service( name=self.new_service_name + '_delete_by_id', - service_type='test_type') + type='test_type') self.operator_cloud.delete_service(name_or_id=service['id']) observed_services = self.operator_cloud.list_services() found = False diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 48c80f51b..ecda08bb3 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -71,11 +71,10 @@ def test_openstack_cloud(self): @mock.patch('shade.OpenStackCloud.keystone_client') def test_project_cache(self, keystone_mock): project = fakes.FakeProject('project_a') - keystone_mock.projects.list.return_value = [project] + keystone_mock.tenants.list.return_value = [project] self.assertEqual({'project_a': project}, self.cloud.project_cache) project_b = fakes.FakeProject('project_b') - keystone_mock.projects.list.return_value = [project, - project_b] + keystone_mock.tenants.list.return_value = [project, project_b] self.assertEqual( {'project_a': project}, self.cloud.project_cache) self.cloud.get_project_cache.invalidate(self.cloud) diff --git a/shade/tests/unit/test_endpoints.py b/shade/tests/unit/test_endpoints.py index a7385abc5..060dabcce 100644 --- a/shade/tests/unit/test_endpoints.py +++ b/shade/tests/unit/test_endpoints.py @@ -59,25 +59,21 @@ def test_create_endpoint(self, mock_keystone_client, mock_list_services): mock_keystone_client.endpoints.create.return_value = \ self.mock_ks_endpoints[0] - endpoint = self.client.create_endpoint( + endpoints = self.client.create_endpoint( service_name_or_id='service1', region='mock_region', public_url='mock_public_url', - internal_url='mock_internal_url', - admin_url='mock_admin_url' ) mock_keystone_client.endpoints.create.assert_called_with( service_id='service_id1', region='mock_region', publicurl='mock_public_url', - internalurl='mock_internal_url', - adminurl='mock_admin_url' ) # test keys and values are correct for k, v in self.mock_endpoints[0].items(): - self.assertEquals(v, endpoint.get(k)) + self.assertEquals(v, endpoints[0].get(k)) @patch.object(OperatorCloud, 'keystone_client') def test_list_endpoints(self, mock_keystone_client): diff --git a/shade/tests/unit/test_services.py b/shade/tests/unit/test_services.py index 39a81732a..31722d5c5 100644 --- a/shade/tests/unit/test_services.py +++ b/shade/tests/unit/test_services.py @@ -51,11 +51,12 @@ def setUp(self): def test_create_service(self, mock_keystone_client): kwargs = { 'name': 'a service', - 'service_type': 'network', + 'type': 'network', 'description': 'This is a test service' } self.client.create_service(**kwargs) + kwargs['service_type'] = kwargs.pop('type') mock_keystone_client.services.create.assert_called_with(**kwargs) @patch.object(OperatorCloud, 'keystone_client') From 539c145d3ec45704a35befb9f738e2f36de61f58 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 9 Oct 2015 10:37:28 -0400 Subject: [PATCH 0525/3836] Accept and emit union of keystone v2/v3 service Keystone v3 changed the service_type field to type. Accept and emit both, because programmers should not have to keep up with meaningless trivia like that. Change-Id: I56fc0191e1058e1d37cee9ee71d606624a0133a7 --- shade/__init__.py | 16 +++++++++++----- shade/_utils.py | 24 ++++++++++++++++++++++++ shade/tests/fakes.py | 3 ++- shade/tests/unit/test_services.py | 8 ++++---- 4 files changed, 41 insertions(+), 10 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 196a4e732..a6b4c30c2 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -4072,11 +4072,13 @@ def purge_node_instance_info(self, uuid): except Exception as e: raise OpenStackCloudException(str(e)) - def create_service(self, name, type, description=None): + @valid_kwargs('type', 'service_type', 'description') + def create_service(self, name, **kwargs): """Create a service. :param name: Service name. - :param type: Service type. + :param type: Service type. (type or service_type required.) + :param service_type: Service type. (type or service_type required.) :param description: Service description (optional). :returns: a dict containing the services description, i.e. the @@ -4084,17 +4086,20 @@ def create_service(self, name, type, description=None): - id: - name: - type: + - service_type: - description: :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call. """ + service_type = kwargs.get('type', kwargs.get('service_type')) + description = kwargs.get('description', None) try: if self.cloud_config.get_api_version('identity').startswith('2'): - service_kwargs = {'service_type': type} + service_kwargs = {'service_type': service_type} else: - service_kwargs = {'type': type} + service_kwargs = {'type': service_type} service = self.manager.submitTask(_tasks.ServiceCreate( name=name, description=description, **service_kwargs)) @@ -4116,7 +4121,8 @@ def list_services(self): services = self.manager.submitTask(_tasks.ServiceList()) except Exception as e: raise OpenStackCloudException(str(e)) - return meta.obj_list_to_dict(services) + return _utils.normalize_keystone_services( + meta.obj_list_to_dict(services)) def search_services(self, name_or_id=None, filters=None): """Search Keystone services. diff --git a/shade/_utils.py b/shade/_utils.py index d0a5d4b03..c6d9aa939 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -116,6 +116,30 @@ def _get_entity(func, name_or_id, filters): return entities[0] +def normalize_keystone_services(services): + """Normalize the structure of keystone services + + In keystone v2, there is a field called "service_type". In v3, it's + "type". Just make the returned dict have both. + + :param list services: A list of keystone service dicts + + :returns: A list of normalized dicts. + """ + ret = [] + for service in services: + service_type = service.get('type', service.get('service_type')) + new_service = { + 'id': service['id'], + 'name': service['name'], + 'description': service.get('description', None), + 'type': service_type, + 'service_type': service_type, + } + ret.append(new_service) + return ret + + def normalize_nova_secgroups(groups): """Normalize the structure of nova security groups diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 7aacb7fcf..80a197aa2 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -79,10 +79,11 @@ def __init__( class FakeService(object): - def __init__(self, id, name, type, description=''): + def __init__(self, id, name, type, service_type, description=''): self.id = id self.name = name self.type = type + self.service_type = service_type self.description = description diff --git a/shade/tests/unit/test_services.py b/shade/tests/unit/test_services.py index 31722d5c5..ed5014fc2 100644 --- a/shade/tests/unit/test_services.py +++ b/shade/tests/unit/test_services.py @@ -30,13 +30,13 @@ class CloudServices(base.TestCase): mock_services = [ {'id': 'id1', 'name': 'service1', 'type': 'type1', - 'description': 'desc1'}, + 'service_type': 'type1', 'description': 'desc1'}, {'id': 'id2', 'name': 'service2', 'type': 'type2', - 'description': 'desc2'}, + 'service_type': 'type2', 'description': 'desc2'}, {'id': 'id3', 'name': 'service3', 'type': 'type2', - 'description': 'desc3'}, + 'service_type': 'type2', 'description': 'desc3'}, {'id': 'id4', 'name': 'service4', 'type': 'type3', - 'description': 'desc4'} + 'service_type': 'type3', 'description': 'desc4'} ] def setUp(self): From baa36a8abb79d2c3a52d2cb7082c81f9af7fa109 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 6 Oct 2015 22:14:54 -0400 Subject: [PATCH 0526/3836] Add create/delete for keystone roles This completes the replacement of: Ifd74cbcef9dd0f3531912995da8ad7b75dd19d44 Add create and delete methods for roles. Note that there is no update method (the review we are replacing had an update method). This is because keystone v2 does NOT have support for this, only v3. Change-Id: Ib037484c38673f10bd2b2fe2df5ca87056d06e6d --- shade/__init__.py | 42 +++++++++++++++++++++++++ shade/_tasks.py | 10 ++++++ shade/tests/functional/test_identity.py | 34 ++++++++++++++++++++ shade/tests/unit/test_identity_roles.py | 23 ++++++++++++++ 4 files changed, 109 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 29f08df77..180c0233d 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -4611,3 +4611,45 @@ def remove_flavor_access(self, flavor_id, project_id): :raises: OpenStackCloudException on operation error. """ self._mod_flavor_access('remove', flavor_id, project_id) + + def create_role(self, name): + """Create a Keystone role. + + :param string name: The name of the role. + + :returns: a dict containing the role description + + :raise OpenStackCloudException: if the role cannot be created + """ + try: + role = self.manager.submitTask( + _tasks.RoleCreate(name=name) + ) + except Exception as e: + raise OpenStackCloudException(str(e)) + return meta.obj_to_dict(role) + + def delete_role(self, name_or_id): + """Delete a Keystone role. + + :param string id: Name or id of the role to delete. + + :returns: True if delete succeeded, False otherwise. + + :raises: ``OpenStackCloudException`` if something goes wrong during + the openstack API call. + """ + role = self.get_role(name_or_id) + if role is None: + self.log.debug( + "Role {0} not found for deleting".format(name_or_id)) + return False + + try: + self.manager.submitTask(_tasks.RoleDelete(role=role['id'])) + except Exception as e: + raise OpenStackCloudException( + "Unable to delete role {0}: {1}".format(name_or_id, e) + ) + + return True diff --git a/shade/_tasks.py b/shade/_tasks.py index 8a653eccd..c250accd8 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -579,3 +579,13 @@ def main(self, client): class RoleList(task_manager.Task): def main(self, client): return client.keystone_client.roles.list() + + +class RoleCreate(task_manager.Task): + def main(self, client): + return client.keystone_client.roles.create(**self.args) + + +class RoleDelete(task_manager.Task): + def main(self, client): + return client.keystone_client.roles.delete(**self.args) diff --git a/shade/tests/functional/test_identity.py b/shade/tests/functional/test_identity.py index adeacc23d..b3b31b272 100644 --- a/shade/tests/functional/test_identity.py +++ b/shade/tests/functional/test_identity.py @@ -19,7 +19,11 @@ Functional tests for `shade` identity methods. """ +import random +import string + from shade import operator_cloud +from shade import OpenStackCloudException from shade.tests import base @@ -27,6 +31,22 @@ class TestIdentity(base.TestCase): def setUp(self): super(TestIdentity, self).setUp() self.cloud = operator_cloud(cloud='devstack-admin') + self.role_prefix = 'test_role' + ''.join( + random.choice(string.ascii_lowercase) for _ in range(5)) + self.addCleanup(self._cleanup_roles) + + def _cleanup_roles(self): + exception_list = list() + for role in self.cloud.list_roles(): + if role['name'].startswith(self.role_prefix): + try: + self.cloud.delete_role(role['name']) + except Exception as e: + exception_list.append(str(e)) + continue + + if exception_list: + raise OpenStackCloudException('\n'.join(exception_list)) def test_list_roles(self): roles = self.cloud.list_roles() @@ -45,3 +65,17 @@ def test_search_roles(self): self.assertIsNotNone(roles) self.assertEqual(1, len(roles)) self.assertEqual('admin', roles[0]['name']) + + def test_create_role(self): + role_name = self.role_prefix + '_create_role' + role = self.cloud.create_role(role_name) + self.assertIsNotNone(role) + self.assertIn('id', role) + self.assertIn('name', role) + self.assertEqual(role_name, role['name']) + + def test_delete_role(self): + role_name = self.role_prefix + '_delete_role' + role = self.cloud.create_role(role_name) + self.assertIsNotNone(role) + self.assertTrue(self.cloud.delete_role(role_name)) diff --git a/shade/tests/unit/test_identity_roles.py b/shade/tests/unit/test_identity_roles.py index d3ab3309f..4490d6e51 100644 --- a/shade/tests/unit/test_identity_roles.py +++ b/shade/tests/unit/test_identity_roles.py @@ -14,6 +14,7 @@ import mock import shade +from shade import meta from shade.tests.unit import base from shade.tests import fakes @@ -40,3 +41,25 @@ def test_get_role(self, mock_keystone): self.assertIsNotNone(role) self.assertEqual('1234', role['id']) self.assertEqual('fake_role', role['name']) + + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_create_role(self, mock_keystone): + role_name = 'tootsie_roll' + role_obj = fakes.FakeRole(id='1234', name=role_name) + mock_keystone.roles.create.return_value = role_obj + + role = self.cloud.create_role(role_name) + + mock_keystone.roles.create.assert_called_once_with( + name=role_name + ) + self.assertIsNotNone(role) + self.assertEqual(role_name, role['name']) + + @mock.patch.object(shade.OperatorCloud, 'get_role') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_delete_role(self, mock_keystone, mock_get): + role_obj = fakes.FakeRole(id='1234', name='aaa') + mock_get.return_value = meta.obj_to_dict(role_obj) + self.assertTrue(self.cloud.delete_role('1234')) + self.assertTrue(mock_keystone.roles.delete.called) From 3962a1c39aebdbde187ad6fba3ef59618ce51d6d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 12 Oct 2015 09:45:24 -0400 Subject: [PATCH 0527/3836] Add script to document deleting private networks In order to test clouds with no private networks, we need to delete the ones made by devstack. This is how that works. Change-Id: I6c9d4d62ea34372d53399aa2b8b27b762d53339b --- extras/delete-network.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 extras/delete-network.sh diff --git a/extras/delete-network.sh b/extras/delete-network.sh new file mode 100644 index 000000000..1d02959ea --- /dev/null +++ b/extras/delete-network.sh @@ -0,0 +1,14 @@ +neutron router-gateway-clear router1 +neutron router-interface-delete router1 +for subnet in private-subnet ipv6-private-subnet ; do + neutron router-interface-delete router1 $subnet + subnet_id=$(neutron subnet-show $subnet -f value -c id) + neutron port-list | grep $subnet_id | awk '{print $2}' | xargs -n1 neutron port-delete + neutron subnet-delete $subnet +done +neutron router-delete router1 +neutron net-delete private + +# Make the public network directly consumable +neutron subnet-update public-subnet --enable-dhcp=True +neutron net-update public --shared=True From 151193568439b24798ccbecf108fd55d8092435d Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 7 Oct 2015 11:22:18 -0400 Subject: [PATCH 0528/3836] Align users with list/search/get interface This replaces: Ibca7240efe705c35bf7a17898caba5a4531bc858 Changes include: - Support for the list/search/get methods. - Fixes a bug in user create. - Allow updating user name and some keystone v3 attributes. - delete_user() method is aligned to return True if the delete succeeded, False if the user was not found. - Test all the things. Co-Authored-By: Monty Taylor Change-Id: I2f1296a0247ad33e5cb6b2d67559165a55592fd3 --- shade/__init__.py | 111 +++++++++++++++++--------- shade/tests/functional/test_users.py | 113 +++++++++++++++++++++++++++ shade/tests/unit/test_caching.py | 27 ++++--- 3 files changed, 203 insertions(+), 48 deletions(-) create mode 100644 shade/tests/functional/test_users.py diff --git a/shade/__init__.py b/shade/__init__.py index 180c0233d..5600f571b 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -524,50 +524,77 @@ def delete_project(self, name_or_id): "Error in deleting project {project}: {message}".format( project=name_or_id, message=str(e))) - @property - def user_cache(self): - return self.get_user_cache() - @_cache_on_arguments() - def get_user_cache(self): - user_list = self.manager.submitTask(_tasks.UserList()) - return {user.id: user for user in user_list} + def list_users(self): + """List Keystone Users. - def _get_user(self, name_or_id): - """Retrieve a user by name or id.""" + :returns: a list of dicts containing the user description. - for id, user in self.user_cache.items(): - if name_or_id in (id, user.name): - return user - return None + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + try: + users = self.manager.submitTask(_tasks.UserList()) + except Exception as e: + raise OpenStackCloudException( + "Failed to list users: {0}".format(str(e)) + ) + return meta.obj_list_to_dict(users) - def get_user(self, name_or_id): - """Retrieve a user by name or id.""" - user = self._get_user(name_or_id) - if user: - return meta.obj_to_dict(user) - return None + def search_users(self, name_or_id=None, filters=None): + """Seach Keystone users. - def update_user(self, name_or_id, email=None, enabled=None): - self.get_user_cache.invalidate(self) - user = self._get_user(name_or_id) - user_args = {} - if email is not None: - user_args['email'] = email - if enabled is not None: - user_args['enabled'] = enabled - if not user_args: - self.log.debug("No user data to update") - return None - user_args['user'] = user + :param string name: user name or id. + :param dict filters: a dict containing additional filters to use. + + :returns: a list of dict containing the role description + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + users = self.list_users() + return _utils._filter_list(users, name_or_id, filters) + + def get_user(self, name_or_id, filters=None): + """Get exactly one Keystone user. + + :param string id: user name or id. + :param dict filters: a dict containing additional filters to use. + + :returns: a single dict containing the user description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + return _utils._get_entity(self.search_users, name_or_id, filters) + + # NOTE(Shrews): Keystone v2 supports updating only name, email and enabled. + @valid_kwargs('name', 'email', 'enabled', 'domain_id', 'password', + 'description', 'default_project') + def update_user(self, name_or_id, **kwargs): + self.list_users.invalidate(self) + user = self.get_user(name_or_id) + kwargs['user'] = user + + if self.cloud_config.get_api_version('identity') != '3': + # Do not pass v3 args to a v2 keystone. + kwargs.pop('domain_id', None) + kwargs.pop('password', None) + kwargs.pop('description', None) + kwargs.pop('default_project', None) + elif 'domain_id' in kwargs: + # The incoming parameter is domain_id in order to match the + # parameter name in create_user(), but UserUpdate() needs it + # to be domain. + kwargs['domain'] = kwargs.pop('domain_id') try: - user = self.manager.submitTask(_tasks.UserUpdate(**user_args)) + user = self.manager.submitTask(_tasks.UserUpdate(**kwargs)) except Exception as e: raise OpenStackCloudException( "Error in updating user {user}: {message}".format( user=name_or_id, message=str(e))) - self.get_user_cache.invalidate(self) + self.list_users.invalidate(self) return meta.obj_to_dict(user) def create_user( @@ -578,25 +605,31 @@ def create_user( identity_params = self._get_identity_params( domain_id, default_project) user = self.manager.submitTask(_tasks.UserCreate( - user_name=name, password=password, email=email, + name=name, password=password, email=email, enabled=enabled, **identity_params)) except Exception as e: raise OpenStackCloudException( "Error in creating user {user}: {message}".format( user=name, message=str(e))) - self.get_user_cache.invalidate(self) + self.list_users.invalidate(self) return meta.obj_to_dict(user) def delete_user(self, name_or_id): - self.get_user_cache.invalidate(self) + self.list_users.invalidate(self) + user = self.get_user(name_or_id) + if not user: + self.log.debug( + "User {0} not found for deleting".format(name_or_id)) + return False + try: - user = self._get_user(name_or_id) self.manager.submitTask(_tasks.UserDelete(user=user)) except Exception as e: raise OpenStackCloudException( "Error in deleting user {user}: {message}".format( user=name_or_id, message=str(e))) - self.get_user_cache.invalidate(self) + self.list_users.invalidate(self) + return True @property def glance_client(self): @@ -4443,7 +4476,7 @@ def get_role(self, name_or_id, filters=None): :param id: role name or id. :param filters: a dict containing additional filters to use. - :returns: a list of dict containing the role description. Each dict + :returns: a single dict containing the role description. Each dict contains the following attributes:: - id: diff --git a/shade/tests/functional/test_users.py b/shade/tests/functional/test_users.py new file mode 100644 index 000000000..3cdfae488 --- /dev/null +++ b/shade/tests/functional/test_users.py @@ -0,0 +1,113 @@ +# -*- 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. + +""" +test_users +---------------------------------- + +Functional tests for `shade` user methods. +""" + +import random +import string + +from shade import operator_cloud +from shade import OpenStackCloudException +from shade.tests import base + + +class TestUsers(base.TestCase): + def setUp(self): + super(TestUsers, self).setUp() + self.cloud = operator_cloud(cloud='devstack-admin') + self.user_prefix = 'test_user' + ''.join( + random.choice(string.ascii_lowercase) for _ in range(5)) + self.addCleanup(self._cleanup_users) + + def _cleanup_users(self): + exception_list = list() + for user in self.cloud.list_users(): + if user['name'].startswith(self.user_prefix): + try: + self.cloud.delete_user(user['id']) + except Exception as e: + exception_list.append(str(e)) + continue + + if exception_list: + raise OpenStackCloudException('\n'.join(exception_list)) + + def _create_user(self, **kwargs): + domain_id = None + identity_version = self.cloud.cloud_config.get_api_version('identity') + if identity_version not in ('2', '2.0'): + domain = self.cloud.get_identity_domain('default') + domain_id = domain['id'] + return self.cloud.create_user(domain_id=domain_id, **kwargs) + + def test_list_users(self): + users = self.cloud.list_users() + self.assertIsNotNone(users) + self.assertNotEqual([], users) + + def test_get_user(self): + user = self.cloud.get_user('admin') + self.assertIsNotNone(user) + self.assertIn('id', user) + self.assertIn('name', user) + self.assertEqual('admin', user['name']) + + def test_search_users(self): + users = self.cloud.search_users(filters={'enabled': True}) + self.assertIsNotNone(users) + + def test_create_user(self): + user_name = self.user_prefix + '_create' + user_email = 'nobody@nowhere.com' + user = self._create_user(name=user_name, email=user_email) + self.assertIsNotNone(user) + self.assertEqual(user_name, user['name']) + self.assertEqual(user_email, user['email']) + self.assertTrue(user['enabled']) + + def test_delete_user(self): + user_name = self.user_prefix + '_delete' + user_email = 'nobody@nowhere.com' + user = self._create_user(name=user_name, email=user_email) + self.assertIsNotNone(user) + self.assertTrue(self.cloud.delete_user(user['id'])) + + def test_delete_user_not_found(self): + self.assertFalse(self.cloud.delete_user('does_not_exist')) + + def test_update_user(self): + user_name = self.user_prefix + '_updatev3' + user_email = 'nobody@nowhere.com' + user = self._create_user(name=user_name, email=user_email) + self.assertIsNotNone(user) + self.assertTrue(user['enabled']) + + # Pass some keystone v3 params. This should work no matter which + # version of keystone we are testing against. + new_user = self.cloud.update_user(user['id'], + name=user_name + '2', + email='somebody@nowhere.com', + enabled=False, + password='secret', + description='') + self.assertIsNotNone(new_user) + self.assertEqual(user['id'], new_user['id']) + self.assertEqual(user_name + '2', new_user['name']) + self.assertEqual('somebody@nowhere.com', new_user['email']) + self.assertFalse(new_user['enabled']) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 48c80f51b..87f21015a 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -163,10 +163,11 @@ def now_gone(): self.assertEqual([fake_volb4_dict], self.cloud.list_volumes()) @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_get_user_cache(self, keystone_mock): + def test_list_users(self, keystone_mock): fake_user = fakes.FakeUser('999', '', '') keystone_mock.users.list.return_value = [fake_user] - self.assertEqual({'999': fake_user}, self.cloud.get_user_cache()) + self.assertEqual([{'id': '999', 'name': '', 'email': ''}], + self.cloud.list_users()) @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_modify_user_invalidates_cache(self, keystone_mock): @@ -174,7 +175,7 @@ def test_modify_user_invalidates_cache(self, keystone_mock): 'abc123 name') # first cache an empty list keystone_mock.users.list.return_value = [] - self.assertEqual({}, self.cloud.get_user_cache()) + self.assertEqual([], self.cloud.list_users()) # now add one keystone_mock.users.list.return_value = [fake_user] keystone_mock.users.create.return_value = fake_user @@ -184,20 +185,28 @@ def test_modify_user_invalidates_cache(self, keystone_mock): 'name': 'abc123 name', 'email': 'abc123@domain.test'}, created) # Cache should have been invalidated - self.assertEqual({'abc123': fake_user}, self.cloud.get_user_cache()) + self.assertEqual([{'id': 'abc123', + 'name': 'abc123 name', + 'email': 'abc123@domain.test'}], + self.cloud.list_users()) # Update and check to see if it is updated - fake_user2 = fakes.FakeUser('abc123', 'abc123 name', - 'abc123-changed@domain.test') + fake_user2 = fakes.FakeUser('abc123', + 'abc123-changed@domain.test', + 'abc123 name') + fake_user2_dict = meta.obj_to_dict(fake_user2) keystone_mock.users.update.return_value = fake_user2 keystone_mock.users.list.return_value = [fake_user2] self.cloud.update_user('abc123', email='abc123-changed@domain.test') keystone_mock.users.update.assert_called_with( - user=fake_user2, email='abc123-changed@domain.test') - self.assertEqual({'abc123': fake_user2}, self.cloud.get_user_cache()) + user=fake_user2_dict, email='abc123-changed@domain.test') + self.assertEqual([{'id': 'abc123', + 'name': 'abc123 name', + 'email': 'abc123-changed@domain.test'}], + self.cloud.list_users()) # Now delete and ensure it disappears keystone_mock.users.list.return_value = [] self.cloud.delete_user('abc123') - self.assertEqual({}, self.cloud.get_user_cache()) + self.assertEqual([], self.cloud.list_users()) self.assertTrue(keystone_mock.users.delete.was_called) @mock.patch.object(shade.OpenStackCloud, 'nova_client') From 05adc0bd725c34a7f967d9afc196bcef7481b462 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 12 Oct 2015 12:54:39 -0400 Subject: [PATCH 0529/3836] Tell git to ignore .eggs directory Change-Id: I1d1ca9c7f5ddf29969f0bbc5314973c8941a4d1f --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ed8833456..09132e5f7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ *.so # Packages +.eggs *.egg *.egg-info dist From 8569b81501d2e9738b39caef0142154ab0c64e23 Mon Sep 17 00:00:00 2001 From: Caleb Boylan Date: Thu, 8 Oct 2015 17:48:21 -0700 Subject: [PATCH 0530/3836] Make attach_instance return updated volume object Change-Id: I0ad25017fe8be8c341bbc139f60161349a5502e2 --- shade/__init__.py | 7 +++++-- shade/_tasks.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index ac5849f0a..21b2aa587 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1815,10 +1815,12 @@ def attach_volume(self, server, volume, device=None, ) try: - self.manager.submitTask( + vol = self.manager.submitTask( _tasks.VolumeAttach(volume_id=volume['id'], server_id=server['id'], device=device)) + vol = meta.obj_to_dict(vol) + except Exception as e: raise OpenStackCloudException( "Error attaching volume %s to server %s: %s" % @@ -1838,7 +1840,7 @@ def attach_volume(self, server, volume, device=None, continue if self.get_volume_attach_device(vol, server['id']): - return + break # TODO(Shrews) check to see if a volume can be in error status # and also attached. If so, we should move this @@ -1847,6 +1849,7 @@ def attach_volume(self, server, volume, device=None, raise OpenStackCloudException( "Error in attaching volume %s" % volume['id'] ) + return vol def get_server_id(self, name_or_id): server = self.get_server(name_or_id) diff --git a/shade/_tasks.py b/shade/_tasks.py index 01cda0876..40d8fb8c9 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -218,7 +218,7 @@ def main(self, client): class VolumeAttach(task_manager.Task): def main(self, client): - client.nova_client.volumes.create_server_volume(**self.args) + return client.nova_client.volumes.create_server_volume(**self.args) class NeutronSecurityGroupList(task_manager.Task): From c22cc1f97286fb442d31318f97b4baec6f032e49 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 14 Oct 2015 09:32:05 -0400 Subject: [PATCH 0531/3836] Remove shared=False from get_internal_network It turns out internal networks can be shared (as in BlueBox) Removing the shared=False from the internal network search produces correct results. Change-Id: I40ba45d8fc78a0d10a44503ed5b7daf91924e441 --- shade/__init__.py | 1 - shade/tests/unit/test_meta.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 0e5acee6d..4dcc28cf8 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1143,7 +1143,6 @@ def get_internal_networks(self): self._internal_network_stamp, filters={ 'router:external': False, - 'shared': False }) self._internal_network_stamp = True return self._internal_networks diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index a8adbb992..fabaecccc 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -146,7 +146,7 @@ def test_get_server_private_ip( self.assertEqual(PRIVATE_V4, meta.get_server_private_ip(srv, cloud)) mock_has_service.assert_called_with('network') mock_search_networks.assert_called_with( - filters={'router:external': False, 'shared': False} + filters={'router:external': False} ) @mock.patch.object(shade.OpenStackCloud, 'get_image_name') @@ -189,7 +189,7 @@ def test_get_server_private_ip_devstack( self.assertEqual(PRIVATE_V4, srv['private_v4']) mock_has_service.assert_called_with('volume') mock_search_networks.assert_called_with( - filters={'router:external': False, 'shared': False} + filters={'router:external': False} ) @mock.patch.object(shade.OpenStackCloud, 'has_service') From 6113d037f6240666e3c958db6e83a242e7c8531d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 14 Oct 2015 10:18:59 -0400 Subject: [PATCH 0532/3836] Pass OpenStackConfig in to CloudConfig for caches The unit of consumption is the CloudConfig object, but the cache settings are found only globally in the OpenStackConfig object. Pass the OpenStackConfig object in so that a user consuming settings from a CloudConfig can find out what the cache settings are. Change-Id: I633e35b1d7f295581d7abed9c3957e802690614e --- os_client_config/cloud_config.py | 20 +++++++++++++++++++- os_client_config/config.py | 7 ++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index c8b5269ac..f0c414387 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -17,12 +17,14 @@ class CloudConfig(object): def __init__(self, name, region, config, - force_ipv4=False, auth_plugin=None): + force_ipv4=False, auth_plugin=None, + openstack_config=None): self.name = name self.region = region self.config = config self._force_ipv4 = force_ipv4 self._auth = auth_plugin + self._openstack_config = openstack_config def __getattr__(self, key): """Return arbitrary attributes.""" @@ -116,3 +118,19 @@ def force_ipv4(self): def get_auth(self): """Return a keystoneauth plugin from the auth credentials.""" return self._auth + + def get_cache_interval(self): + if self._openstack_config: + return self._openstack_config.get_cache_interval() + + def get_cache_path(self): + if self._openstack_config: + return self._openstack_config.get_cache_path() + + def get_cache_class(self): + if self._openstack_config: + return self._openstack_config.get_cache_class() + + def get_cache_arguments(self): + if self._openstack_config: + return self._openstack_config.get_cache_arguments() diff --git a/os_client_config/config.py b/os_client_config/config.py index c87870d2a..6d37835ed 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -221,6 +221,9 @@ def _normalize_keys(self, config): new_config[key] = value return new_config + def get_cache_interval(self): + return self._cache_max_age + def get_cache_max_age(self): return self._cache_max_age @@ -629,7 +632,9 @@ def get_one_cloud(self, cloud=None, validate=True, name=cloud_name, region=config['region_name'], config=self._normalize_keys(config), force_ipv4=force_ipv4, - auth_plugin=auth_plugin) + auth_plugin=auth_plugin, + openstack_config=self + ) @staticmethod def set_one_cloud(config_file, cloud, set_config=None): From ec4ebbdd277c2125734640f2b3cd1184c4c6bf86 Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Wed, 14 Oct 2015 09:27:21 -0700 Subject: [PATCH 0533/3836] Add an API reference to the docs Change-Id: I2c40965378dedd67808875eb9453ab8664f0fee8 --- doc/source/api-reference.rst | 10 ++++++++++ doc/source/index.rst | 1 + 2 files changed, 11 insertions(+) create mode 100644 doc/source/api-reference.rst diff --git a/doc/source/api-reference.rst b/doc/source/api-reference.rst new file mode 100644 index 000000000..dfa7f31cb --- /dev/null +++ b/doc/source/api-reference.rst @@ -0,0 +1,10 @@ +============= +API Reference +============= + +.. module:: os_client_config + :synopsis: OpenStack client configuration + +.. autoclass:: os_client_config.OpenStackConfig + :members: + :inherited-members: diff --git a/doc/source/index.rst b/doc/source/index.rst index 0f793a1a3..cc5dbf470 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -6,6 +6,7 @@ vendor-support contributing installation + api-reference Indices and tables ================== From f6681a83192386fcf27479c820998691a43e8b79 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 14 Oct 2015 12:32:20 -0400 Subject: [PATCH 0534/3836] Fix documentation around regions It turns out region_name is an important parameter - it's not just another kwarg that will get passed through. Change-Id: I5cca8d324a1dcd1355991df793fe29eedfa15dc0 --- README.rst | 2 +- os_client_config/config.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 7b737b9de..b0466f797 100644 --- a/README.rst +++ b/README.rst @@ -247,7 +247,7 @@ Get a named cloud. import os_client_config cloud_config = os_client_config.OpenStackConfig().get_one_cloud( - 'hp', 'region-b.geo-1') + 'hp', region_name='region-b.geo-1') print(cloud_config.name, cloud_config.region, cloud_config.config) Or, get all of the clouds. diff --git a/os_client_config/config.py b/os_client_config/config.py index 6d37835ed..bff2f5cce 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -551,6 +551,7 @@ def get_one_cloud(self, cloud=None, validate=True, An argparse Namespace object; allows direct passing in of argparse options to be added to the cloud config. Values of None and '' will be removed. + :param region_name: Name of the region of the cloud. :param kwargs: Additional configuration options :raises: keystoneauth1.exceptions.MissingRequiredOptions From 512ca01715683f7c46985677e7b9be8e3d7a191c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 14 Oct 2015 12:40:50 -0400 Subject: [PATCH 0535/3836] Validate requested region against region list We have lists of valid regions for clouds, but specifying a region incorrectly is a common mistake (specifying dfw instead of DFW, for instance) Throw an error early with a helpful error message. Change-Id: I55edf20c6dddde4a4d71dad33a41d4e10448ddac --- os_client_config/config.py | 10 +++++++++- os_client_config/tests/test_config.py | 17 +++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index bff2f5cce..b2c164936 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -573,7 +573,15 @@ def get_one_cloud(self, cloud=None, validate=True, # Regions is a list that we can use to create a list of cloud/region # objects. It does not belong in the single-cloud dict - config.pop('regions', None) + regions = config.pop('regions', None) + if regions and args['region_name'] not in regions: + raise exceptions.OpenStackConfigException( + 'Region {region_name} is not a valid region name for cloud' + ' {cloud}. Valid choices are {region_list}. Please note that' + ' region names are case sensitive.'.format( + region_name=args['region_name'], + region_list=','.join(regions), + cloud=cloud)) # Can't just do update, because None values take over for (key, val) in iter(args.items()): diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 271c40be8..01307488a 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -298,6 +298,23 @@ def test_get_one_cloud_no_argparse_regions(self): self.assertEqual(cc.region_name, 'region1') self.assertIsNone(cc.snack_type) + def test_get_one_cloud_bad_region(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + + self.assertRaises( + exceptions.OpenStackConfigException, + c.get_one_cloud, + cloud='_test_cloud_regions', region_name='bad') + + def test_get_one_cloud_bad_region_no_regions(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + + cc = c.get_one_cloud(cloud='_test-cloud_', region_name='bad_region') + self._assert_cloud_details(cc) + self.assertEqual(cc.region_name, 'bad_region') + def test_get_one_cloud_no_argparse_region2(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) From ee1e88b117119056e9cd29cc072f7123891df5ef Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Wed, 14 Oct 2015 09:52:30 -0700 Subject: [PATCH 0536/3836] Add some more docstrings Most simple get/list methods were undocumented. Document them so they appear in the API reference. There are still more undocumented methods. Change-Id: Idde33874656f7b8c1dab90e7d5df67a232c69749 --- shade/__init__.py | 154 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 4dcc28cf8..45da7f1b1 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -885,6 +885,11 @@ def search_records(self, domain_id, name_or_id=None, filters=None): return _utils._filter_list(records, name_or_id, filters) def list_keypairs(self): + """List all available keypairs. + + :returns: A list of keypair dicts. + + """ try: return meta.obj_list_to_dict( self.manager.submitTask(_tasks.KeypairList()) @@ -894,23 +899,48 @@ def list_keypairs(self): "Error fetching keypair list: %s" % str(e)) def list_networks(self): + """List all available networks. + + :returns: A list of network dicts. + + """ with self._neutron_exceptions("Error fetching network list"): return self.manager.submitTask(_tasks.NetworkList())['networks'] def list_routers(self): + """List all available routers. + + :returns: A list of router dicts. + + """ with self._neutron_exceptions("Error fetching router list"): return self.manager.submitTask(_tasks.RouterList())['routers'] def list_subnets(self): + """List all available subnets. + + :returns: A list of subnet dicts. + + """ with self._neutron_exceptions("Error fetching subnet list"): return self.manager.submitTask(_tasks.SubnetList())['subnets'] def list_ports(self): + """List all available ports. + + :returns: A list of port dicts. + + """ with self._neutron_exceptions("Error fetching port list"): return self.manager.submitTask(_tasks.PortList())['ports'] @_cache_on_arguments(should_cache_fn=_no_pending_volumes) def list_volumes(self, cache=True): + """List all available volumes. + + :returns: A list of volume dicts. + + """ if not cache: warnings.warn('cache argument to list_volumes is deprecated. Use ' 'invalidate instead.') @@ -924,6 +954,11 @@ def list_volumes(self, cache=True): @_cache_on_arguments() def list_flavors(self): + """List all available flavors. + + :returns: A list of flavor dicts. + + """ try: return meta.obj_list_to_dict( self.manager.submitTask(_tasks.FlavorList(is_public=None)) @@ -933,6 +968,11 @@ def list_flavors(self): "Error fetching flavor list: %s" % e) def list_security_groups(self): + """List all available security groups. + + :returns: A list of security group dicts. + + """ # Handle neutron security groups if self.secgroup_source == 'neutron': # Neutron returns dicts, so no need to convert objects here. @@ -960,6 +1000,11 @@ def list_security_groups(self): ) def list_servers(self): + """List all available servers. + + :returns: A list of server dicts. + + """ try: return meta.obj_list_to_dict( self.manager.submitTask(_tasks.ServerList()) @@ -1020,6 +1065,11 @@ def list_images(self, filter_deleted=True): return images def list_floating_ip_pools(self): + """List all available floating IP pools. + + :returns: A list of floating IP pool dicts. + + """ if not self._has_nova_extension('os-floating-ip-pools'): raise OpenStackCloudUnavailableExtension( 'Floating IP pools extension is not available on target cloud') @@ -1034,6 +1084,11 @@ def list_floating_ip_pools(self): msg=str(e))) def list_floating_ips(self): + """List all available floating IPs. + + :returns: A list of floating IP dicts. + + """ if self.has_service('network'): try: return _utils.normalize_neutron_floating_ips( @@ -1061,6 +1116,11 @@ def _nova_list_floating_ips(self): "error fetching floating IPs list: {msg}".format(msg=str(e))) def list_domains(self): + """List all available DNS domains. + + :returns: A list of domain dicts. + + """ try: return self.manager.submitTask(_tasks.DomainList()) except Exception as e: @@ -1148,31 +1208,102 @@ def get_internal_networks(self): return self._internal_networks def get_keypair(self, name_or_id, filters=None): + """Get a keypair by name or ID. + + :param name_or_id: Name or ID of the keypair. + + :returns: A keypair dict or None if no matching keypair is + found. + + """ return _utils._get_entity(self.search_keypairs, name_or_id, filters) def get_network(self, name_or_id, filters=None): + """Get a network by name or ID. + + :param name_or_id: Name or ID of the network. + + :returns: A network dict or None if no matching network is + found. + + """ return _utils._get_entity(self.search_networks, name_or_id, filters) def get_router(self, name_or_id, filters=None): + """Get a router by name or ID. + + :param name_or_id: Name or ID of the router. + + :returns: A router dict or None if no matching router is + found. + + """ return _utils._get_entity(self.search_routers, name_or_id, filters) def get_subnet(self, name_or_id, filters=None): + """Get a subnet by name or ID. + + :param name_or_id: Name or ID of the subnet. + + :returns: A subnet dict or None if no matching subnet is + found. + + """ return _utils._get_entity(self.search_subnets, name_or_id, filters) def get_port(self, name_or_id, filters=None): + """Get a port by name or ID. + + :param name_or_id: Name or ID of the port. + + :returns: A port dict or None if no matching port is found. + + """ return _utils._get_entity(self.search_ports, name_or_id, filters) def get_volume(self, name_or_id, filters=None): + """Get a volume by name or ID. + + :param name_or_id: Name or ID of the volume. + + :returns: A volume dict or None if no matching volume is + found. + + """ return _utils._get_entity(self.search_volumes, name_or_id, filters) def get_flavor(self, name_or_id, filters=None): + """Get a flavor by name or ID. + + :param name_or_id: Name or ID of the flavor. + + :returns: A flavor dict or None if no matching flavor is + found. + + """ return _utils._get_entity(self.search_flavors, name_or_id, filters) def get_security_group(self, name_or_id, filters=None): + """Get a security group by name or ID. + + :param name_or_id: Name or ID of the security group. + + :returns: A security group dict or None if no matching + security group is found. + + """ return _utils._get_entity( self.search_security_groups, name_or_id, filters) def get_server(self, name_or_id=None, filters=None): + """Get a server by name or ID. + + :param name_or_id: Name or ID of the server. + + :returns: A server dict or None if no matching server is + found. + + """ return _utils._get_entity(self.search_servers, name_or_id, filters) def get_server_by_id(self, id): @@ -1180,12 +1311,35 @@ def get_server_by_id(self, id): self.manager.submitTask(_tasks.ServerGet(server=id))) def get_image(self, name_or_id, filters=None): + """Get an image by name or ID. + + :param name_or_id: Name or ID of the image. + + :returns: An image dict or None if no matching image is found. + + """ return _utils._get_entity(self.search_images, name_or_id, filters) def get_floating_ip(self, id, filters=None): + """Get a floating IP by ID + + :param id: ID of the floating IP. + + :returns: A floating IP dict or None if no matching floating + IP is found. + + """ return _utils._get_entity(self.search_floating_ips, id, filters) def get_domain(self, name_or_id, filters=None): + """Get a DNS domain by name or ID. + + :param name_or_id: Name or ID of the DNS domain. + + :returns: A domain dict or None if no matching DNS domain is + found. + + """ return _utils._get_entity(self.search_domains, name_or_id, filters) def get_record(self, domain_id, name_or_id, filters=None): From 688ca1f3ce72eafa58c1f7a519355bcf8249728d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 14 Oct 2015 13:00:24 -0400 Subject: [PATCH 0537/3836] Document filters for get methods Add documentation for the filter parameter. Change-Id: I862760181b353906dbda8785eb05cde216cc17c0 --- shade/__init__.py | 120 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 45da7f1b1..a9144182c 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1211,6 +1211,16 @@ def get_keypair(self, name_or_id, filters=None): """Get a keypair by name or ID. :param name_or_id: Name or ID of the keypair. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } :returns: A keypair dict or None if no matching keypair is found. @@ -1222,6 +1232,16 @@ def get_network(self, name_or_id, filters=None): """Get a network by name or ID. :param name_or_id: Name or ID of the network. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } :returns: A network dict or None if no matching network is found. @@ -1233,6 +1253,16 @@ def get_router(self, name_or_id, filters=None): """Get a router by name or ID. :param name_or_id: Name or ID of the router. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } :returns: A router dict or None if no matching router is found. @@ -1244,6 +1274,16 @@ def get_subnet(self, name_or_id, filters=None): """Get a subnet by name or ID. :param name_or_id: Name or ID of the subnet. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } :returns: A subnet dict or None if no matching subnet is found. @@ -1255,6 +1295,16 @@ def get_port(self, name_or_id, filters=None): """Get a port by name or ID. :param name_or_id: Name or ID of the port. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } :returns: A port dict or None if no matching port is found. @@ -1265,6 +1315,16 @@ def get_volume(self, name_or_id, filters=None): """Get a volume by name or ID. :param name_or_id: Name or ID of the volume. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } :returns: A volume dict or None if no matching volume is found. @@ -1276,6 +1336,16 @@ def get_flavor(self, name_or_id, filters=None): """Get a flavor by name or ID. :param name_or_id: Name or ID of the flavor. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } :returns: A flavor dict or None if no matching flavor is found. @@ -1287,6 +1357,16 @@ def get_security_group(self, name_or_id, filters=None): """Get a security group by name or ID. :param name_or_id: Name or ID of the security group. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } :returns: A security group dict or None if no matching security group is found. @@ -1299,6 +1379,16 @@ def get_server(self, name_or_id=None, filters=None): """Get a server by name or ID. :param name_or_id: Name or ID of the server. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } :returns: A server dict or None if no matching server is found. @@ -1314,6 +1404,16 @@ def get_image(self, name_or_id, filters=None): """Get an image by name or ID. :param name_or_id: Name or ID of the image. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } :returns: An image dict or None if no matching image is found. @@ -1324,6 +1424,16 @@ def get_floating_ip(self, id, filters=None): """Get a floating IP by ID :param id: ID of the floating IP. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } :returns: A floating IP dict or None if no matching floating IP is found. @@ -1335,6 +1445,16 @@ def get_domain(self, name_or_id, filters=None): """Get a DNS domain by name or ID. :param name_or_id: Name or ID of the DNS domain. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } :returns: A domain dict or None if no matching DNS domain is found. From bcd507a779bf78ddd4737a07e6813695da781f2a Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Wed, 14 Oct 2015 10:27:05 -0700 Subject: [PATCH 0538/3836] Fix floating ip removal on delete server A conditional block was not sufficiently indented. Change-Id: Ia4ab58cc476505570163f7f855fc5a679c7b8d57 --- shade/__init__.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index a9144182c..360474e68 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2854,15 +2854,15 @@ def _delete_server( if floating_ip: ips = self.search_floating_ips(filters={ 'floating_ip_address': floating_ip}) - if len(ips) != 1: - raise OpenStackException( - "Tried to delete floating ip {floating_ip}" - " associated with server {id} but there was" - " an error finding it. Something is exceptionally" - " broken.".format( - floating_ip=floating_ip, - id=server['id'])) - self.delete_floating_ip(ips[0]['id']) + if len(ips) != 1: + raise OpenStackException( + "Tried to delete floating ip {floating_ip}" + " associated with server {id} but there was" + " an error finding it. Something is exceptionally" + " broken.".format( + floating_ip=floating_ip, + id=server['id'])) + self.delete_floating_ip(ips[0]['id']) try: self.manager.submitTask( _tasks.ServerDelete(server=server['id'])) From ed9c516e559316442d23514d719aabe906a71ce3 Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Wed, 14 Oct 2015 13:40:24 -0700 Subject: [PATCH 0539/3836] Pass wait to add_ips_to_server If we are creating a server with auto_ip and waiting for it, we should also wait for the ip, as well as the server. Monty is working on a patch to obsolete this, but in the interim, this works. Change-Id: I26bc94408441edf067493b7ffd50eebd9dd95e75 --- shade/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index 360474e68..e1d74b499 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2806,7 +2806,8 @@ def create_server(self, auto_ip=True, ips=None, ip_pool=None, ' allocated an IP address.', extra_data=dict(server=server)) return self.add_ips_to_server( - server, auto_ip, ips, ip_pool, reuse=reuse_ips) + server, auto_ip, ips, ip_pool, wait=wait, + reuse=reuse_ips) if server['status'] == 'ERROR': raise OpenStackCloudException( From 7d711f21c2dc875cd78e35cacd53bb2c83340936 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 10 Oct 2015 11:22:35 -0400 Subject: [PATCH 0540/3836] Split the nova server active check out If someone, like nodepool, wants to split the logic of requesting the launch of a server and checking its validity, that's totally fair. Make the "is this server ready, if so, please make sure it has network" into its own method. Change-Id: I6347173001af31c53a1b9210528ccd33d3c14ce4 --- shade/__init__.py | 74 +++++++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index b022002f4..6ab04aed9 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2775,48 +2775,54 @@ def create_server(self, auto_ip=True, ips=None, ip_pool=None, raise OpenStackCloudException( "Error in creating the server.") if wait: + server_id = server['id'] for count in _utils._iterate_timeout( timeout, "Timeout waiting for the server to come up."): try: - server = self.get_server_by_id(server.id) + server = self.get_server_by_id(server_id) except Exception: continue - if server['status'] == 'ACTIVE': - if not server['addresses']: - self.log.debug( - 'Server {server} reached ACTIVE state without' - ' being allocated an IP address.' - ' Deleting server.'.format( - server=server['id'])) - try: - self._delete_server( - server=server, wait=wait, timeout=timeout) - except Exception as e: - self.log.debug( - "Failed deleting server {server} that booted" - " without an IP address. Manual cleanup is" - " required.".format(server=server['id']), - exc_info=True) - raise OpenStackCloudException( - "Server reached ACTIVE state without being" - " allocated an IP address AND then could not" - " be deleted: {0}".format(e), - extra_data=dict(server=server)) - raise OpenStackCloudException( - 'Server reached ACTIVE state without being' - ' allocated an IP address.', - extra_data=dict(server=server)) - return self.add_ips_to_server( - server, auto_ip, ips, ip_pool, wait=wait, - reuse=reuse_ips) + server = self.get_active_server( + server=server, reuse=reuse_ips, + auto_ip=auto_ip, ips=ips, ip_pool=ip_pool, + wait=wait, timeout=timeout) + if server: + return server + return server - if server['status'] == 'ERROR': - raise OpenStackCloudException( - "Error in creating the server", - extra_data=dict(server=server)) - return meta.obj_to_dict(server) + def get_active_server( + self, server, auto_ip=True, ips=None, ip_pool=None, + reuse=True, wait=False, timeout=180): + + if server['status'] == 'ERROR': + raise OpenStackCloudException( + "Error in creating the server", extra_data=dict(server=server)) + + if server['status'] == 'ACTIVE': + if 'addresses' in server and server['addresses']: + return self.add_ips_to_server( + server, auto_ip, ips, ip_pool, reuse=reuse, wait=wait) + + self.log.debug( + 'Server {server} reached ACTIVE state without' + ' being allocated an IP address.' + ' Deleting server.'.format(server=server['id'])) + try: + self._delete_server( + server=server, wait=wait, timeout=timeout) + except Exception as e: + raise OpenStackCloudException( + 'Server reached ACTIVE state without being' + ' allocated an IP address AND then could not' + ' be deleted: {0}'.format(e), + extra_data=dict(server=server)) + raise OpenStackCloudException( + 'Server reached ACTIVE state without being' + ' allocated an IP address.', + extra_data=dict(server=server)) + return None def rebuild_server(self, server_id, image_id, wait=False, timeout=180): try: From 46525cf1665fa505735a533311fa8d85c51ed1c8 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 12 Oct 2015 09:13:52 -0400 Subject: [PATCH 0541/3836] Handle list_servers caching more directly Some of the operations in shade are more expensive than others and need a different approach to caching. For instance, nodepool is ALWAYS building and deleting nodes, which means that any sort of active-invalidation scheme renders the cache unusable, while a time-based scheme that would make sense for flavors would be massively too long. This is the first stab but is really only going to be step one - and is essentially the nodepool list_servers caching approach. list_servers is cached by hand into a local mutex-protected list and is only updated every five seconds. Moving forward, the cache length needs to be configurable, but that wants to be systematic rather than specific. Also, a local thread safe dictionary dogpile cache should be written and we should use it by default so that shared caches like redis or consul could be used in a distributed system. Change-Id: I430b4beaf3b05cb255c1cf5d180af450fd6429b2 --- shade/__init__.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 6ab04aed9..6975fb2fa 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -17,6 +17,8 @@ import inspect import logging import operator +import threading +import time from cinderclient.v1 import client as cinder_client from designateclient.v1 import Client as designate_client @@ -224,6 +226,7 @@ class OpenStackCloud(object): to pass in cloud configuration, but is being phased in currently. """ + _SERVER_LIST_AGE = 5 # TODO(mordred) Make this configurable def __init__( self, @@ -279,6 +282,11 @@ def __init__( ).configure( cache_class, expiration_time=cache_interval, arguments=cache_arguments) + + self._servers = [] + self._servers_time = 0 + self._servers_lock = threading.Lock() + self._container_cache = dict() self._file_hash_cache = dict() @@ -1005,6 +1013,24 @@ def list_servers(self): :returns: A list of server dicts. """ + if (time.time() - self._servers_time) >= self._SERVER_LIST_AGE: + # Since we're using cached data anyway, we don't need to + # have more than one thread actually submit the list + # servers task. Let the first one submit it while holding + # a lock, and the non-blocking acquire method will cause + # subsequent threads to just skip this and use the old + # data until it succeeds. + # For the first time, when there is no data, make the call + # blocking. + if self._servers_lock.acquire(len(self._servers) == 0): + try: + self._servers = self._list_servers() + self._servers_time = time.time() + finally: + self._servers_lock.release() + return self._servers + + def _list_servers(self): try: return meta.obj_list_to_dict( self.manager.submitTask(_tasks.ServerList()) From 1ad1e9a688a0386052143d0a0f07d03f9f844441 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 15 Oct 2015 19:59:54 -0400 Subject: [PATCH 0542/3836] Add API method to list router interfaces Sometimes you need to list the port interfaces on a router. This is the method for doing so. Change-Id: Ia216b87e4b9cd7a3dd98a16148723f7dd92f2513 --- shade/__init__.py | 34 +++++++++++++++++++++++++++ shade/tests/functional/test_router.py | 32 +++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index b022002f4..387da9f48 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1626,6 +1626,40 @@ def remove_router_interface(self, router, subnet_id=None, port_id=None): _tasks.RouterRemoveInterface(router=router['id'], body=body) ) + def list_router_interfaces(self, router, interface_type=None): + """List all interfaces for a router. + + :param dict router: A router dict object. + :param string interface_type: One of None, "internal", or "external". + Controls whether all, internal interfaces or external interfaces + are returned. + + :returns: A list of port dict objects. + """ + ports = self.search_ports(filters={'device_id': router['id']}) + + if interface_type: + filtered_ports = [] + ext_fixed = router['external_gateway_info']['external_fixed_ips'] + + # Compare the subnets (subnet_id, ip_address) on the ports with + # the subnets making up the router external gateway. Those ports + # that match are the external interfaces, and those that don't + # are internal. + for port in ports: + matched_ext = False + for port_subnet in port['fixed_ips']: + for router_external_subnet in ext_fixed: + if port_subnet == router_external_subnet: + matched_ext = True + if interface_type == 'internal' and not matched_ext: + filtered_ports.append(port) + elif interface_type == 'external' and matched_ext: + filtered_ports.append(port) + return filtered_ports + + return ports + def create_router(self, name=None, admin_state_up=True, ext_gateway_net_id=None, enable_snat=None, ext_fixed_ips=None): diff --git a/shade/tests/functional/test_router.py b/shade/tests/functional/test_router.py index 5ddb8f9f1..e8fa62b19 100644 --- a/shade/tests/functional/test_router.py +++ b/shade/tests/functional/test_router.py @@ -185,6 +185,38 @@ def test_add_remove_router_interface(self): self.assertEqual(router['id'], iface['id']) self.assertEqual(sub['id'], iface['subnet_id']) + def test_list_router_interfaces(self): + router = self._create_and_verify_advanced_router() + net_name = self.network_prefix + '_intnet1' + sub_name = self.subnet_prefix + '_intsub1' + net = self.cloud.create_network(name=net_name) + sub = self.cloud.create_subnet( + net['id'], '10.4.4.0/24', subnet_name=sub_name, + gateway_ip='10.4.4.1' + ) + + iface = self.cloud.add_router_interface(router, subnet_id=sub['id']) + all_ifaces = self.cloud.list_router_interfaces(router) + int_ifaces = self.cloud.list_router_interfaces( + router, interface_type='internal') + ext_ifaces = self.cloud.list_router_interfaces( + router, interface_type='external') + self.assertIsNone( + self.cloud.remove_router_interface(router, subnet_id=sub['id']) + ) + + # Test return values *after* the interface is detached so the + # resources we've created can be cleaned up if these asserts fail. + self.assertIsNotNone(iface) + self.assertEqual(2, len(all_ifaces)) + self.assertEqual(1, len(int_ifaces)) + self.assertEqual(1, len(ext_ifaces)) + + ext_fixed_ips = router['external_gateway_info']['external_fixed_ips'] + self.assertEqual(ext_fixed_ips[0]['subnet_id'], + ext_ifaces[0]['fixed_ips'][0]['subnet_id']) + self.assertEqual(sub['id'], int_ifaces[0]['fixed_ips'][0]['subnet_id']) + def test_update_router_name(self): router = self._create_and_verify_advanced_router() From b06adf563e42798d02ee9153ccb9096fda3bac92 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 16 Oct 2015 11:13:13 -0400 Subject: [PATCH 0543/3836] Add Rackspace LON region Change-Id: I01df6e7f0cf09d52a50e1af14cdc76db90b44320 --- doc/source/vendor-support.rst | 5 +++-- os_client_config/vendors/rackspace.yaml | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index b61a05b8c..5519928a4 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -49,10 +49,11 @@ https://identity.api.rackspacecloud.com/v2.0/ Region Name Human Name ============== ================ DFW Dallas -ORD Chicago +HKG Hong Kong IAD Washington, D.C. +LON London +ORD Chicago SYD Sydney -HKG Hong Kong ============== ================ * Database Service Type is `rax:database` diff --git a/os_client_config/vendors/rackspace.yaml b/os_client_config/vendors/rackspace.yaml index c37802976..a28d49366 100644 --- a/os_client_config/vendors/rackspace.yaml +++ b/os_client_config/vendors/rackspace.yaml @@ -8,6 +8,7 @@ profile: - IAD - ORD - SYD + - LON database_service_type: rax:database compute_service_name: cloudServersOpenStack image_api_use_tasks: true From 08f29824551e864795dca2ac84aaacd49276f333 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 12 Oct 2015 09:42:58 -0400 Subject: [PATCH 0544/3836] Tweak create_server to use list_servers cache The wait logic wasn't using get_server, which means it was always going to be making direct calls, which obviates the benefit of the list_servers cache. Change-Id: I3883e75469a821eaacde040e66f02cd16e2752e5 --- shade/__init__.py | 15 ++++++++--- shade/_utils.py | 8 +++--- shade/tests/unit/test_create_server.py | 35 +++++++++++++++----------- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 62f432abf..2f506fb1b 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2827,7 +2827,11 @@ def create_server(self, auto_ip=True, ips=None, ip_pool=None, try: server = self.manager.submitTask(_tasks.ServerCreate(**bootkwargs)) + # This is a direct get task call to skip the list_servers + # cache which has absolutely no chance of containing the + # new server server = self.get_server_by_id(server.id) + server_id = server['id'] except Exception as e: raise OpenStackCloudException( "Error in creating instance: {0}".format(e)) @@ -2835,14 +2839,19 @@ def create_server(self, auto_ip=True, ips=None, ip_pool=None, raise OpenStackCloudException( "Error in creating the server.") if wait: - server_id = server['id'] + # There is no point in iterating faster than the list_servers cache for count in _utils._iterate_timeout( timeout, - "Timeout waiting for the server to come up."): + "Timeout waiting for the server to come up.", + wait=self._SERVER_LIST_AGE): try: - server = self.get_server_by_id(server_id) + # Use the get_server call so that the list_servers + # cache can be leveraged + server = self.get_server(server_id) except Exception: continue + if not server: + continue server = self.get_active_server( server=server, reuse=reuse_ips, diff --git a/shade/_utils.py b/shade/_utils.py index c6d9aa939..0fcce4790 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -19,11 +19,11 @@ from shade import exc -def _iterate_timeout(timeout, message): +def _iterate_timeout(timeout, message, wait=2): """Iterate and raise an exception on timeout. - This is a generator that will continually yield and sleep for 2 - seconds, and if the timeout is reached, will raise an exception + This is a generator that will continually yield and sleep for + wait seconds, and if the timeout is reached, will raise an exception with . """ @@ -33,7 +33,7 @@ def _iterate_timeout(timeout, message): while (timeout is None) or (time.time() < start + timeout): count += 1 yield count - time.sleep(2) + time.sleep(wait) raise exc.OpenStackCloudTimeout(message) diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index d8b71d4f4..773831e71 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -67,10 +67,12 @@ def test_create_server_with_server_error(self): Test that a server error before we return or begin waiting for the server instance spawn raises an exception in create_server. """ + build_server = fakes.FakeServer('1234', '', 'BUILD') + error_server = fakes.FakeServer('1234', '', 'ERROR') with patch("shade.OpenStackCloud"): config = { - "servers.create.return_value": Mock(status="BUILD"), - "servers.get.return_value": Mock(status="ERROR") + "servers.create.return_value": build_server, + "servers.get.return_value": error_server, } OpenStackCloud.nova_client = Mock(**config) self.assertRaises( @@ -82,13 +84,13 @@ def test_create_server_wait_server_error(self): raises an exception in create_server. """ with patch("shade.OpenStackCloud"): - fake_server = fakes.FakeServer('1234', '', 'BUILD') + build_server = fakes.FakeServer('1234', '', 'BUILD') error_server = fakes.FakeServer('1234', '', 'ERROR') config = { - "servers.create.return_value": fake_server, - "servers.get.side_effect": [ - fake_server, error_server - ] + "servers.create.return_value": build_server, + "servers.get.return_value": build_server, + "servers.list.side_effect": [ + [build_server], [error_server]] } OpenStackCloud.nova_client = Mock(**config) self.assertRaises( @@ -104,7 +106,8 @@ def test_create_server_with_timeout(self): fake_server = fakes.FakeServer('1234', '', 'BUILD') config = { "servers.create.return_value": fake_server, - "servers.get.return_value": fake_server + "servers.get.return_value": fake_server, + "servers.list.return_value": [fake_server], } OpenStackCloud.nova_client = Mock(**config) self.assertRaises( @@ -132,13 +135,15 @@ def test_create_server_wait(self): its status changes to "ACTIVE". """ with patch("shade.OpenStackCloud"): - building_server = fakes.FakeServer( + build_server = fakes.FakeServer( '1234', '', 'ACTIVE', addresses=dict(public='1.1.1.1')) fake_server = fakes.FakeServer( '1234', '', 'ACTIVE', addresses=dict(public='1.1.1.1')) config = { - "servers.create.return_value": building_server, - "servers.get.return_value": fake_server, + "servers.create.return_value": build_server, + "servers.get.return_value": build_server, + "servers.list.side_effect": [ + [build_server], [fake_server]] } OpenStackCloud.nova_client = Mock(**config) with patch.object(OpenStackCloud, "add_ips_to_server", @@ -153,11 +158,13 @@ def test_create_server_no_addresses(self): server doesn't have addresses. """ with patch("shade.OpenStackCloud"): + build_server = fakes.FakeServer('1234', '', 'BUILD') fake_server = fakes.FakeServer('1234', '', 'ACTIVE') config = { - "servers.create.return_value": fake_server, - "servers.get.side_effect": [ - fakes.FakeServer('1234', '', 'BUILD'), fake_server] + "servers.create.return_value": build_server, + "servers.get.return_value": build_server, + "servers.list.side_effect": [ + [build_server], [fake_server]] } OpenStackCloud.nova_client = Mock(**config) with patch.object(OpenStackCloud, "add_ips_to_server", From ae5e5e03a53e139a138c4d8570f3d25397113e25 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 7 Oct 2015 13:29:04 -0500 Subject: [PATCH 0545/3836] Undecorate cache decorated methods on null cache If we're not actually using the cache, strip the cache decorators from the methods because they make the call stack very difficult to debug. Change-Id: Icc666a4e053dc13e8fbad2c62fd7dc5d8c0ad382 --- shade/__init__.py | 43 +++++++++++++++++++++++++++++----- shade/tests/unit/test_shade.py | 26 ++++++++++---------- 2 files changed, 51 insertions(+), 18 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 2f506fb1b..2b2838c0a 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -13,6 +13,7 @@ # limitations under the License. import contextlib +import functools import hashlib import inspect import logging @@ -165,6 +166,9 @@ def operator_cloud(config=None, **kwargs): cloud_config=cloud_config) +_decorated_methods = [] + + def _cache_on_arguments(*cache_on_args, **cache_on_kwargs): def _inner_cache_on_arguments(func): def _cache_decorator(obj, *args, **kwargs): @@ -178,6 +182,8 @@ def invalidate(obj, *args, **kwargs): *args, **kwargs) _cache_decorator.invalidate = invalidate + _cache_decorator.func = func + _decorated_methods.append(func.__name__) return _cache_decorator return _inner_cache_on_arguments @@ -277,16 +283,41 @@ def __init__( (self.verify, self.cert) = cloud_config.get_requests_verify_args() - self._cache = cache.make_region( - function_key_generator=self._make_cache_key - ).configure( - cache_class, expiration_time=cache_interval, - arguments=cache_arguments) - self._servers = [] self._servers_time = 0 self._servers_lock = threading.Lock() + if cache_class != 'dogpile.cache.null': + self._cache = cache.make_region( + function_key_generator=self._make_cache_key + ).configure( + cache_class, expiration_time=cache_interval, + arguments=cache_arguments) + else: + def _fake_invalidate(unused): + pass + + class _FakeCache(object): + def invalidate(self): + pass + + # Don't cache list_servers if we're not caching things. + # Replace this with a more specific cache configuration + # soon. + self._SERVER_LIST_AGE = 0 + self._cache = _FakeCache() + # Undecorate cache decorated methods. Otherwise the call stacks + # wind up being stupidly long and hard to debug + for method in _decorated_methods: + meth_obj = getattr(self, method, None) + if not meth_obj: + continue + if (hasattr(meth_obj, 'invalidate') + and hasattr(meth_obj, 'func')): + new_func = functools.partial(meth_obj.func, self) + new_func.invalidate = _fake_invalidate + setattr(self, method, new_func) + self._container_cache = dict() self._file_hash_cache = dict() diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 375aec4cc..0a0caf710 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -275,8 +275,9 @@ def test_update_subnet(self, mock_client, mock_get): self.cloud.update_subnet('123', subnet_name='goofy') self.assertTrue(mock_client.update_subnet.called) - @mock.patch.object(shade.OpenStackCloud, 'list_flavors') - def test_get_flavor_by_ram(self, mock_list): + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_get_flavor_by_ram(self, mock_nova_client): + class Flavor1(object): id = '1' name = 'vanilla ice cream' @@ -289,12 +290,12 @@ class Flavor2(object): vanilla = meta.obj_to_dict(Flavor1()) chocolate = meta.obj_to_dict(Flavor2()) - mock_list.return_value = [vanilla, chocolate] + mock_nova_client.flavors.list.return_value = [vanilla, chocolate] flavor = self.cloud.get_flavor_by_ram(ram=150) self.assertEquals(chocolate, flavor) - @mock.patch.object(shade.OpenStackCloud, 'list_flavors') - def test_get_flavor_by_ram_and_include(self, mock_list): + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_get_flavor_by_ram_and_include(self, mock_nova_client): class Flavor1(object): id = '1' name = 'vanilla ice cream' @@ -313,26 +314,27 @@ class Flavor3(object): vanilla = meta.obj_to_dict(Flavor1()) chocolate = meta.obj_to_dict(Flavor2()) strawberry = meta.obj_to_dict(Flavor3()) - mock_list.return_value = [vanilla, chocolate, strawberry] + mock_nova_client.flavors.list.return_value = [ + vanilla, chocolate, strawberry] flavor = self.cloud.get_flavor_by_ram(ram=150, include='strawberry') self.assertEquals(strawberry, flavor) - @mock.patch.object(shade.OpenStackCloud, 'list_flavors') - def test_get_flavor_by_ram_not_found(self, mock_list): - mock_list.return_value = [] + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_get_flavor_by_ram_not_found(self, mock_nova_client): + mock_nova_client.flavors.list.return_value = [] self.assertRaises(shade.OpenStackCloudException, self.cloud.get_flavor_by_ram, ram=100) - @mock.patch.object(shade.OpenStackCloud, 'list_flavors') - def test_get_flavor_string_and_int(self, mock_list): + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_get_flavor_string_and_int(self, mock_nova_client): class Flavor1(object): id = '1' name = 'vanilla ice cream' ram = 100 vanilla = meta.obj_to_dict(Flavor1()) - mock_list.return_value = [vanilla] + mock_nova_client.flavors.list.return_value = [vanilla] flavor1 = self.cloud.get_flavor('1') self.assertEquals(vanilla, flavor1) flavor2 = self.cloud.get_flavor(1) From 65fdaccb29d39eb6394429cb1afdd1c5c327a4a2 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 14 Oct 2015 16:27:50 -0400 Subject: [PATCH 0546/3836] Create neutron floating ips with server info When we create a neutron floating IP with server port information, then we don't need to do a separate attach step. Change-Id: I7e72545465b5de3962d9721339cfe857a37f7d37 --- shade/__init__.py | 221 +++++++++++-------- shade/tests/unit/test_floating_ip_common.py | 11 +- shade/tests/unit/test_floating_ip_neutron.py | 41 ++-- shade/tests/unit/test_floating_ip_nova.py | 56 ++--- 4 files changed, 194 insertions(+), 135 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 2b2838c0a..8452e070e 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2336,12 +2336,13 @@ def _expand_server_vars(self, server): # actually want the API to be. return meta.expand_server_vars(self, server) - def available_floating_ip(self, network=None): + def available_floating_ip(self, network=None, server=None): """Get a floating IP from a network or a pool. Return the first available floating IP or allocate a new one. :param network: Nova pool name or Neutron network name or id. + :param server: Server the IP is for if known :returns: a (normalized) structure with a floating IP address description. @@ -2350,7 +2351,7 @@ def available_floating_ip(self, network=None): try: f_ips = _utils.normalize_neutron_floating_ips( self._neutron_available_floating_ips( - network=network)) + network=network, server=server)) return f_ips[0] except OpenStackCloudURINotFound as e: self.log.debug( @@ -2363,13 +2364,15 @@ def available_floating_ip(self, network=None): ) return f_ips[0] - def _neutron_available_floating_ips(self, network=None, project_id=None): + def _neutron_available_floating_ips( + self, network=None, project_id=None, server=None): """Get a floating IP from a Neutron network. Return a list of available floating IPs or allocate a new one and return it in a list of 1 element. :param network: Nova pool name or Neutron network name or id. + :param server: (server) Server the Floating IP is for :returns: a list of floating IP addresses. @@ -2402,7 +2405,7 @@ def _neutron_available_floating_ips(self, network=None, project_id=None): # No available IP found or we didn't try # allocate a new Floating IP f_ip = self._neutron_create_floating_ip( - network_name_or_id=networks[0]['id']) + network_name_or_id=networks[0]['id'], server=server) return [f_ip] @@ -2450,10 +2453,12 @@ def _nova_available_floating_ips(self, pool=None): "unable to create floating IP in pool {pool}: {msg}".format( pool=pool, msg=str(e))) - def create_floating_ip(self, network=None): + def create_floating_ip(self, network=None, server=None): """Allocate a new floating IP from a network or a pool. :param network: Nova pool name or Neutron network name or id. + :param server: (optional) Server dict for the server to create + the IP for and to which it should be attached :returns: a floating IP address @@ -2463,7 +2468,7 @@ def create_floating_ip(self, network=None): try: f_ips = _utils.normalize_neutron_floating_ips( [self._neutron_create_floating_ip( - network_name_or_id=network)] + network_name_or_id=network, server=server)] ) return f_ips[0] except OpenStackCloudURINotFound as e: @@ -2477,20 +2482,31 @@ def create_floating_ip(self, network=None): [self._nova_create_floating_ip(pool=network)]) return f_ips[0] - def _neutron_create_floating_ip(self, network_name_or_id=None): + def _neutron_create_floating_ip( + self, network_name_or_id=None, server=None): with self._neutron_exceptions( "unable to create floating IP for net " "{0}".format(network_name_or_id)): - networks = self.search_networks( - name_or_id=network_name_or_id, - filters={'router:external': True}) - if not networks: - raise OpenStackCloudResourceNotFound( - "unable to find an external network with id " - "{0}".format(network_name_or_id or "(no id specified)")) + if network_name_or_id: + networks = [self.get_network(network_name_or_id)] + if not networks: + raise OpenStackCloudResourceNotFound( + "unable to find network for floating ips with id " + "{0}".format(network_name_or_id)) + else: + networks = self.get_external_networks() + if not networks: + raise OpenStackCloudResourceNotFound( + "Unable to find an external network in this cloud" + " which makes getting a floating IP impossible") kwargs = { 'floating_network_id': networks[0]['id'], } + if server: + (port, fixed_address) = self._get_free_fixed_port(server) + if port: + kwargs['port_id'] = port['id'] + kwargs['fixed_ip_address'] = fixed_address return self.manager.submitTask(_tasks.NeutronFloatingIPCreate( body={'floatingip': kwargs}))['floatingip'] @@ -2558,108 +2574,127 @@ def _nova_delete_floating_ip(self, floating_ip_id): return True def attach_ip_to_server( - self, server_id, floating_ip_id, fixed_address=None, wait=False, - timeout=60): + self, server, floating_ip, + fixed_address=None, wait=False, + timeout=60, skip_attach=False): """Attach a floating IP to a server. - :param server_id: id of a server. - :param floating_ip_id: id of the floating IP to attach. + :param server: Server dict + :param floating_ip: Floating IP dict to attach :param fixed_address: (optional) fixed address to which attach the floating IP to. :param wait: (optional) Wait for the address to appear as assigned to the server in Nova. Defaults to False. :param timeout: (optional) Seconds to wait, defaults to 60. See the ``wait`` parameter. + :param skip_attach: (optional) Skip the actual attach and just do + the wait. Defaults to False. :returns: None :raises: OpenStackCloudException, on operation error. """ + # Short circuit if we're asking to attach an IP that's already + # attached + ext_ip = meta.get_server_ip(server, ext_tag='floating') + if ext_ip == floating_ip['floating_ip_address']: + return + if self.has_service('network'): - try: - self._neutron_attach_ip_to_server( - server_id=server_id, floating_ip_id=floating_ip_id, - fixed_address=fixed_address) - except OpenStackCloudURINotFound as e: - self.log.debug( - "Something went wrong talking to neutron API: " - "'{msg}'. Trying with Nova.".format(msg=str(e))) - # Fall-through, trying with Nova + if not skip_attach: + try: + self._neutron_attach_ip_to_server( + server=server, floating_ip=floating_ip, + fixed_address=fixed_address) + except OpenStackCloudURINotFound as e: + self.log.debug( + "Something went wrong talking to neutron API: " + "'{msg}'. Trying with Nova.".format(msg=str(e))) + # Fall-through, trying with Nova else: # Nova network self._nova_attach_ip_to_server( - server_id=server_id, floating_ip_id=floating_ip_id, + server_id=server['id'], floating_ip_id=floating_ip['id'], fixed_address=fixed_address) if wait: # Wait for the address to be assigned to the server - f_ip = self.get_floating_ip(id=floating_ip_id) + server_id = server['id'] for _ in _utils._iterate_timeout( timeout, "Timeout waiting for the floating IP to be attached."): server = self.get_server_by_id(server_id) - for k, v in server['addresses'].items(): - for interface_spec in v: - if interface_spec['addr'] == \ - f_ip['floating_ip_address']: - return - - def _neutron_attach_ip_to_server(self, server_id, floating_ip_id, - fixed_address=None): + ext_ip = meta.get_server_ip(server, ext_tag='floating') + if ext_ip == floating_ip['floating_ip_address']: + return + + def _get_free_fixed_port(self, server, fixed_address=None): + ports = self.search_ports(filters={'device_id': server['id']}) + if not ports: + return (None, None) + port = None + if not fixed_address: + if len(ports) > 1: + raise OpenStackCloudException( + "More than one port was found for server {server}" + " and no fixed_address was specified. It is not" + " possible to infer correct behavior. Please specify" + " a fixed_address - or file a bug in shade describing" + " how you think this should work.") + # We're assuming one, because we have no idea what to do with + # more than one. + port = ports[0] + # Select the first available IPv4 address + for address in port.get('fixed_ips', list()): + try: + ip = ipaddress.ip_address(address['ip_address']) + except Exception: + continue + if ip.version == 4: + fixed_address = address['ip_address'] + return port, fixed_address + raise OpenStackCloudException( + "unable to find a free fixed IPv4 address for server " + "{0}".format(server_id)) + # unfortunately a port can have more than one fixed IP: + # we can't use the search_ports filtering for fixed_address as + # they are contained in a list. e.g. + # + # "fixed_ips": [ + # { + # "subnet_id": "008ba151-0b8c-4a67-98b5-0d2b87666062", + # "ip_address": "172.24.4.2" + # } + # ] + # + # Search fixed_address + for p in ports: + for fixed_ip in p['fixed_ips']: + if fixed_address == fixed_ip['ip_address']: + return (p, fixed_address) + return (None, None) + + def _neutron_attach_ip_to_server( + self, server, floating_ip, fixed_address=None): with self._neutron_exceptions( "unable to bind a floating ip to server " - "{0}".format(server_id)): - # Find an available port - ports = self.search_ports(filters={'device_id': server_id}) - port = None - if ports and fixed_address is None: - port = ports[0] - # Select the first available IPv4 address - for address in port.get('fixed_ips', list()): - try: - ip = ipaddress.ip_address(address['ip_address']) - except Exception: - continue - if ip.version == 4: - fixed_address = address['ip_address'] - break - if fixed_address is None: - raise OpenStackCloudException( - "unable to find a suitable IPv4 address for server " - "{0}".format(server_id)) - elif ports: - # unfortunately a port can have more than one fixed IP: - # we can't use the search_ports filtering for fixed_address as - # they are contained in a list. e.g. - # - # "fixed_ips": [ - # { - # "subnet_id": "008ba151-0b8c-4a67-98b5-0d2b87666062", - # "ip_address": "172.24.4.2" - # } - # ] - # - # Search fixed_address - for p in ports: - for fixed_ip in p['fixed_ips']: - if fixed_address == fixed_ip['ip_address']: - port = p - break - else: - continue - break + "{0}".format(server['id'])): + # Find an available port + (port, fixed_address) = self._get_free_fixed_port( + server, fixed_address=fixed_address) if not port: raise OpenStackCloudException( - "unable to find a port for server {0}".format(server_id)) + "unable to find a port for server {0}".format( + server['id'])) - floating_ip = {'port_id': port['id']} + floating_ip_args = {'port_id': port['id']} if fixed_address is not None: - floating_ip['fixed_ip_address'] = fixed_address + floating_ip_args['fixed_ip_address'] = fixed_address return self.manager.submitTask(_tasks.NeutronFloatingIPUpdate( - floatingip=floating_ip_id, - body={'floatingip': floating_ip} + floatingip=floating_ip['id'], + body={'floatingip': floating_ip_args} ))['floatingip'] def _nova_attach_ip_to_server(self, server_id, floating_ip_id, @@ -2733,7 +2768,7 @@ def _nova_detach_ip_from_server(self, server_id, floating_ip_id): return True def add_ip_from_pool( - self, server_id, network, fixed_address=None, reuse=True): + self, server, network, fixed_address=None, reuse=True): """Add a floating IP to a sever from a given pool This method reuses available IPs, when possible, or allocate new IPs @@ -2741,7 +2776,7 @@ def add_ip_from_pool( The floating IP is attached to the given fixed address or to the first server port/fixed address - :param server_id: Id of a server + :param server: Server dict :param network: Nova pool name or Neutron network name or id. :param fixed_address: a fixed address :param reuse: Try to reuse existing ips. Defaults to True. @@ -2754,8 +2789,7 @@ def add_ip_from_pool( f_ip = self.create_floating_ip(network=network) self.attach_ip_to_server( - server_id=server_id, floating_ip_id=f_ip['id'], - fixed_address=fixed_address) + server=server, floating_ip=f_ip, fixed_address=fixed_address) return f_ip @@ -2777,7 +2811,7 @@ def add_ip_list(self, server, ips): f_ip = self.get_floating_ip( id=None, filters={'floating_ip_address': ip}) self.attach_ip_to_server( - server_id=server['id'], floating_ip_id=f_ip['id']) + server=server, floating_ip=f_ip) def add_auto_ip(self, server, wait=False, timeout=60, reuse=True): """Add a floating IP to a server. @@ -2801,14 +2835,19 @@ def add_auto_ip(self, server, wait=False, timeout=60, reuse=True): :returns: Floating IP address attached to server. """ + skip_attach = False if reuse: f_ip = self.available_floating_ip() else: - f_ip = self.create_floating_ip() + f_ip = self.create_floating_ip(server=server) + if server: + # This gets passed in for both nova and neutron + # but is only meaninful for the neutron logic branch + skip_attach = True self.attach_ip_to_server( - server_id=server['id'], floating_ip_id=f_ip['id'], wait=wait, - timeout=timeout) + server=server, floating_ip=f_ip, wait=wait, timeout=timeout, + skip_attach=skip_attach) return f_ip @@ -2816,7 +2855,7 @@ def add_ips_to_server( self, server, auto_ip=True, ips=None, ip_pool=None, wait=False, timeout=60, reuse=True): if ip_pool: - self.add_ip_from_pool(server['id'], ip_pool, reuse=reuse) + self.add_ip_from_pool(server, ip_pool, reuse=reuse) elif ips: self.add_ip_list(server, ips) elif auto_ip: diff --git a/shade/tests/unit/test_floating_ip_common.py b/shade/tests/unit/test_floating_ip_common.py index 3e74fe5bb..de95015b6 100644 --- a/shade/tests/unit/test_floating_ip_common.py +++ b/shade/tests/unit/test_floating_ip_common.py @@ -44,8 +44,7 @@ def test_add_auto_ip( id='server-id', name='test-server', status="ACTIVE", addresses={} ) server_dict = meta.obj_to_dict(server) - - mock_available_floating_ip.return_value = { + floating_ip_dict = { "id": "this-is-a-floating-ip-id", "fixed_ip_address": None, "floating_ip_address": "203.0.113.29", @@ -54,11 +53,13 @@ def test_add_auto_ip( "status": "ACTIVE" } + mock_available_floating_ip.return_value = floating_ip_dict + self.client.add_auto_ip(server=server_dict) mock_attach_ip_to_server.assert_called_with( - timeout=60, wait=False, server_id='server-id', - floating_ip_id='this-is-a-floating-ip-id') + timeout=60, wait=False, server=server_dict, + floating_ip=floating_ip_dict, skip_attach=False) @patch.object(OpenStackCloud, 'nova_client') @patch.object(OpenStackCloud, 'add_ip_from_pool') @@ -74,7 +75,7 @@ def test_add_ips_to_server_pool( self.client.add_ips_to_server(server_dict, ip_pool=pool) - mock_add_ip_from_pool.assert_called_with('romeo', pool, reuse=True) + mock_add_ip_from_pool.assert_called_with(server_dict, pool, reuse=True) @patch.object(OpenStackCloud, 'nova_client') @patch.object(OpenStackCloud, 'add_ip_list') diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index cd73aa4c5..7ec6e5565 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -25,7 +25,9 @@ from neutronclient.common import exceptions as n_exc from shade import _utils +from shade import meta from shade import OpenStackCloud +from shade.tests import fakes from shade.tests.unit import base @@ -126,6 +128,18 @@ def setUp(self): self.client = OpenStackCloud( cloud_config=config.get_one_cloud(validate=False)) + self.fake_server = meta.obj_to_dict( + fakes.FakeServer( + 'server-id', '', 'ACTIVE', + addresses={u'test_pnztt_net': [{ + u'OS-EXT-IPS:type': u'fixed', + u'addr': '192.0.2.129', + u'version': 4, + u'OS-EXT-IPS-MAC:mac_addr': + u'fa:16:3e:ae:7d:42'}]})) + self.floating_ip = _utils.normalize_neutron_floating_ips( + self.mock_floating_ip_list_rep['floatingips'])[0] + @patch.object(OpenStackCloud, 'neutron_client') @patch.object(OpenStackCloud, 'has_service') def test_list_floating_ips(self, mock_has_service, mock_neutron_client): @@ -233,7 +247,7 @@ def test_auto_ip_pool_no_reuse( mock_nova_client): mock_has_service.return_value = True mock__neutron_create_floating_ip.return_value = \ - self.mock_floating_ip_new_rep['floatingip'] + self.mock_floating_ip_list_rep['floatingips'][0] mock_keystone_session.get_project_id.return_value = \ '4969c491a3c74ee4af974e6d800c62df' @@ -241,10 +255,10 @@ def test_auto_ip_pool_no_reuse( dict(id='1234'), ip_pool='my-network', reuse=False) mock__neutron_create_floating_ip.assert_called_once_with( - network_name_or_id='my-network') + network_name_or_id='my-network', server=None) mock_attach_ip_to_server.assert_called_once_with( - server_id='1234', fixed_address=None, - floating_ip_id=self.mock_floating_ip_new_rep['floatingip']['id']) + server={'id': '1234'}, fixed_address=None, + floating_ip=self.floating_ip) @patch.object(OpenStackCloud, 'keystone_session') @patch.object(OpenStackCloud, '_neutron_create_floating_ip') @@ -301,25 +315,24 @@ def test_delete_floating_ip_not_found( self.assertFalse(ret) - @patch.object(OpenStackCloud, '_neutron_list_floating_ips') @patch.object(OpenStackCloud, 'search_ports') @patch.object(OpenStackCloud, 'neutron_client') @patch.object(OpenStackCloud, 'has_service') def test_attach_ip_to_server( - self, mock_has_service, mock_neutron_client, mock_search_ports, - mock__neutron_list_floating_ips): + self, mock_has_service, mock_neutron_client, mock_search_ports): mock_has_service.return_value = True - mock__neutron_list_floating_ips.return_value = \ - [self.mock_floating_ip_new_rep['floatingip']] mock_search_ports.return_value = self.mock_search_ports_rep + mock_neutron_client.list_floatingips.return_value = \ + self.mock_floating_ip_list_rep + self.client.attach_ip_to_server( - server_id='server_id', - floating_ip_id='2f245a7b-796b-4f26-9cf9-9e82d248fda8') + server=self.fake_server, + floating_ip=self.floating_ip) mock_neutron_client.update_floatingip.assert_called_with( - floatingip=self.mock_floating_ip_new_rep['floatingip']['id'], + floatingip=self.mock_floating_ip_list_rep['floatingips'][0]['id'], body={ 'floatingip': { 'port_id': self.mock_search_ports_rep[0]['id'], @@ -366,9 +379,9 @@ def test_add_ip_from_pool( mock_attach_ip_to_server.return_value = None ip = self.client.add_ip_from_pool( - server_id='server-id', + server=self.fake_server, network='network-name', - fixed_address='1.2.3.4') + fixed_address='192.0.2.129') self.assertEqual( self.mock_floating_ip_new_rep['floatingip']['floating_ip_address'], diff --git a/shade/tests/unit/test_floating_ip_nova.py b/shade/tests/unit/test_floating_ip_nova.py index f290e3a0f..97d950b08 100644 --- a/shade/tests/unit/test_floating_ip_nova.py +++ b/shade/tests/unit/test_floating_ip_nova.py @@ -23,8 +23,10 @@ from novaclient import exceptions as n_exc import os_client_config +from shade import _utils +from shade import meta from shade import OpenStackCloud -from shade.tests.fakes import FakeFloatingIP +from shade.tests import fakes from shade.tests.unit import base @@ -72,14 +74,28 @@ def setUp(self): config = os_client_config.OpenStackConfig() self.client = OpenStackCloud( cloud_config=config.get_one_cloud(validate=False)) + self.floating_ips = [ + fakes.FakeFloatingIP(**ip) for ip in self.mock_floating_ip_list_rep + ] + + self.fake_server = meta.obj_to_dict( + fakes.FakeServer( + 'server-id', '', 'ACTIVE', + addresses={u'test_pnztt_net': [{ + u'OS-EXT-IPS:type': u'fixed', + u'addr': '192.0.2.129', + u'version': 4, + u'OS-EXT-IPS-MAC:mac_addr': + u'fa:16:3e:ae:7d:42'}]})) + + self.floating_ip = _utils.normalize_nova_floating_ips( + meta.obj_list_to_dict(self.floating_ips))[0] @patch.object(OpenStackCloud, 'nova_client') @patch.object(OpenStackCloud, 'has_service') def test_list_floating_ips(self, mock_has_service, mock_nova_client): mock_has_service.side_effect = has_service_side_effect - mock_nova_client.floating_ips.list.return_value = [ - FakeFloatingIP(**ip) for ip in self.mock_floating_ip_list_rep - ] + mock_nova_client.floating_ips.list.return_value = self.floating_ips floating_ips = self.client.list_floating_ips() @@ -92,9 +108,7 @@ def test_list_floating_ips(self, mock_has_service, mock_nova_client): @patch.object(OpenStackCloud, 'has_service') def test_search_floating_ips(self, mock_has_service, mock_nova_client): mock_has_service.side_effect = has_service_side_effect - mock_nova_client.floating_ips.list.return_value = [ - FakeFloatingIP(**ip) for ip in self.mock_floating_ip_list_rep - ] + mock_nova_client.floating_ips.list.return_value = self.floating_ips floating_ips = self.client.search_floating_ips( filters={'attached': False}) @@ -108,9 +122,7 @@ def test_search_floating_ips(self, mock_has_service, mock_nova_client): @patch.object(OpenStackCloud, 'has_service') def test_get_floating_ip(self, mock_has_service, mock_nova_client): mock_has_service.side_effect = has_service_side_effect - mock_nova_client.floating_ips.list.return_value = [ - FakeFloatingIP(**ip) for ip in self.mock_floating_ip_list_rep - ] + mock_nova_client.floating_ips.list.return_value = self.floating_ips floating_ip = self.client.get_floating_ip(id='29') @@ -123,9 +135,7 @@ def test_get_floating_ip(self, mock_has_service, mock_nova_client): def test_get_floating_ip_not_found( self, mock_has_service, mock_nova_client): mock_has_service.side_effect = has_service_side_effect - mock_nova_client.floating_ips.list.return_value = [ - FakeFloatingIP(**ip) for ip in self.mock_floating_ip_list_rep - ] + mock_nova_client.floating_ips.list.return_value = self.floating_ips floating_ip = self.client.get_floating_ip(id='666') @@ -135,8 +145,8 @@ def test_get_floating_ip_not_found( @patch.object(OpenStackCloud, 'has_service') def test_create_floating_ip(self, mock_has_service, mock_nova_client): mock_has_service.side_effect = has_service_side_effect - mock_nova_client.floating_ips.create.return_value = FakeFloatingIP( - **self.mock_floating_ip_list_rep[1]) + mock_nova_client.floating_ips.create.return_value =\ + fakes.FakeFloatingIP(**self.mock_floating_ip_list_rep[1]) self.client.create_floating_ip(network='nova') @@ -164,7 +174,7 @@ def test_available_floating_ip_new( mock_has_service.side_effect = has_service_side_effect mock__nova_list_floating_ips.return_value = [] mock_nova_client.floating_ips.create.return_value = \ - FakeFloatingIP(**self.mock_floating_ip_list_rep[0]) + fakes.FakeFloatingIP(**self.mock_floating_ip_list_rep[0]) ip = self.client.available_floating_ip(network='nova') @@ -202,12 +212,10 @@ def test_delete_floating_ip_not_found( @patch.object(OpenStackCloud, 'has_service') def test_attach_ip_to_server(self, mock_has_service, mock_nova_client): mock_has_service.side_effect = has_service_side_effect - mock_nova_client.floating_ips.list.return_value = [ - FakeFloatingIP(**ip) for ip in self.mock_floating_ip_list_rep - ] + mock_nova_client.floating_ips.list.return_value = self.floating_ips self.client.attach_ip_to_server( - server_id='server-id', floating_ip_id=1, + server=self.fake_server, floating_ip=self.floating_ip, fixed_address='192.0.2.129') mock_nova_client.servers.add_floating_ip.assert_called_with( @@ -219,7 +227,7 @@ def test_attach_ip_to_server(self, mock_has_service, mock_nova_client): def test_detach_ip_from_server(self, mock_has_service, mock_nova_client): mock_has_service.side_effect = has_service_side_effect mock_nova_client.floating_ips.list.return_value = [ - FakeFloatingIP(**ip) for ip in self.mock_floating_ip_list_rep + fakes.FakeFloatingIP(**ip) for ip in self.mock_floating_ip_list_rep ] self.client.detach_ip_from_server( @@ -232,12 +240,10 @@ def test_detach_ip_from_server(self, mock_has_service, mock_nova_client): @patch.object(OpenStackCloud, 'has_service') def test_add_ip_from_pool(self, mock_has_service, mock_nova_client): mock_has_service.side_effect = has_service_side_effect - mock_nova_client.floating_ips.list.return_value = [ - FakeFloatingIP(**ip) for ip in self.mock_floating_ip_list_rep - ] + mock_nova_client.floating_ips.list.return_value = self.floating_ips ip = self.client.add_ip_from_pool( - server_id='server-id', + server=self.fake_server, network='nova', fixed_address='192.0.2.129') From 16061f90d9a54ca348f20218ec65ba032858b259 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 16 Oct 2015 15:23:34 -0400 Subject: [PATCH 0547/3836] Make router func tests less racey Some tests were sharing the same code that created a subnet. This could cause problems if the tests were to run concurrently. Avoid that by making each test specify the subnet CIDR it wants. Change-Id: I0a4f8913af8f8069fc86f168cda35495553aee09 --- shade/tests/functional/test_router.py | 59 +++++++++++++++------------ 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/shade/tests/functional/test_router.py b/shade/tests/functional/test_router.py index e8fa62b19..ca2c7fa3d 100644 --- a/shade/tests/functional/test_router.py +++ b/shade/tests/functional/test_router.py @@ -19,8 +19,7 @@ Functional tests for `shade` router methods. """ -import random -import string +import ipaddress from shade import openstack_cloud from shade.exc import OpenStackCloudException @@ -42,12 +41,9 @@ def setUp(self): if not self.cloud.has_service('network'): self.skipTest('Network service not supported by cloud') - self.router_prefix = 'test_router' + ''.join( - random.choice(string.ascii_lowercase) for _ in range(5)) - self.network_prefix = 'test_network' + ''.join( - random.choice(string.ascii_lowercase) for _ in range(5)) - self.subnet_prefix = 'test_subnet' + ''.join( - random.choice(string.ascii_lowercase) for _ in range(5)) + self.router_prefix = self.getUniqueString('router') + self.network_prefix = self.getUniqueString('network') + self.subnet_prefix = self.getUniqueString('subnet') # NOTE(Shrews): Order matters! self.addCleanup(self._cleanup_networks) @@ -116,15 +112,23 @@ def test_create_router_basic(self): self.assertEqual(net1['id'], ext_gw_info['network_id']) self.assertTrue(ext_gw_info['enable_snat']) - def _create_and_verify_advanced_router(self): + def _create_and_verify_advanced_router(self, + external_cidr, + external_gateway_ip=None): + # NOTE(Shrews): The arguments are needed because these tests + # will run in parallel and we want to make sure that each test + # is using different resources to prevent race conditions. net1_name = self.network_prefix + '_net1' sub1_name = self.subnet_prefix + '_sub1' net1 = self.cloud.create_network(name=net1_name, external=True) sub1 = self.cloud.create_subnet( - net1['id'], '10.5.5.0/24', subnet_name=sub1_name, - gateway_ip='10.5.5.1' + net1['id'], external_cidr, subnet_name=sub1_name, + gateway_ip=external_gateway_ip ) + ip_net = ipaddress.IPv4Network(unicode(external_cidr)) + last_ip = str(list(ip_net.hosts())[-1]) + router_name = self.router_prefix + '_create_advanced' router = self.cloud.create_router( name=router_name, @@ -132,7 +136,7 @@ def _create_and_verify_advanced_router(self): ext_gateway_net_id=net1['id'], enable_snat=False, ext_fixed_ips=[ - {'subnet_id': sub1['id'], 'ip_address': '10.5.5.99'} + {'subnet_id': sub1['id'], 'ip_address': last_ip} ] ) @@ -153,17 +157,18 @@ def _create_and_verify_advanced_router(self): ext_gw_info['external_fixed_ips'][0]['subnet_id'] ) self.assertEqual( - '10.5.5.99', + last_ip, ext_gw_info['external_fixed_ips'][0]['ip_address'] ) return router def test_create_router_advanced(self): - self._create_and_verify_advanced_router() + self._create_and_verify_advanced_router(external_cidr='10.2.2.0/24') def test_add_remove_router_interface(self): - router = self._create_and_verify_advanced_router() + router = self._create_and_verify_advanced_router( + external_cidr='10.3.3.0/24') net_name = self.network_prefix + '_intnet1' sub_name = self.subnet_prefix + '_intsub1' net = self.cloud.create_network(name=net_name) @@ -186,13 +191,14 @@ def test_add_remove_router_interface(self): self.assertEqual(sub['id'], iface['subnet_id']) def test_list_router_interfaces(self): - router = self._create_and_verify_advanced_router() + router = self._create_and_verify_advanced_router( + external_cidr='10.5.5.0/24') net_name = self.network_prefix + '_intnet1' sub_name = self.subnet_prefix + '_intsub1' net = self.cloud.create_network(name=net_name) sub = self.cloud.create_subnet( - net['id'], '10.4.4.0/24', subnet_name=sub_name, - gateway_ip='10.4.4.1' + net['id'], '10.6.6.0/24', subnet_name=sub_name, + gateway_ip='10.6.6.1' ) iface = self.cloud.add_router_interface(router, subnet_id=sub['id']) @@ -218,7 +224,8 @@ def test_list_router_interfaces(self): self.assertEqual(sub['id'], int_ifaces[0]['fixed_ips'][0]['subnet_id']) def test_update_router_name(self): - router = self._create_and_verify_advanced_router() + router = self._create_and_verify_advanced_router( + external_cidr='10.7.7.0/24') new_name = self.router_prefix + '_update_name' updated = self.cloud.update_router(router['id'], name=new_name) @@ -237,7 +244,8 @@ def test_update_router_name(self): updated['external_gateway_info']) def test_update_router_admin_state(self): - router = self._create_and_verify_advanced_router() + router = self._create_and_verify_advanced_router( + external_cidr='10.8.8.0/24') updated = self.cloud.update_router(router['id'], admin_state_up=True) @@ -258,21 +266,22 @@ def test_update_router_admin_state(self): updated['external_gateway_info']) def test_update_router_ext_gw_info(self): - router = self._create_and_verify_advanced_router() + router = self._create_and_verify_advanced_router( + external_cidr='10.9.9.0/24') # create a new subnet existing_net_id = router['external_gateway_info']['network_id'] sub_name = self.subnet_prefix + '_update' sub = self.cloud.create_subnet( - existing_net_id, '10.6.6.0/24', subnet_name=sub_name, - gateway_ip='10.6.6.1' + existing_net_id, '10.10.10.0/24', subnet_name=sub_name, + gateway_ip='10.10.10.1' ) updated = self.cloud.update_router( router['id'], ext_gateway_net_id=existing_net_id, ext_fixed_ips=[ - {'subnet_id': sub['id'], 'ip_address': '10.6.6.77'} + {'subnet_id': sub['id'], 'ip_address': '10.10.10.77'} ] ) self.assertIsNotNone(updated) @@ -288,7 +297,7 @@ def test_update_router_ext_gw_info(self): ext_gw_info['external_fixed_ips'][0]['subnet_id'] ) self.assertEqual( - '10.6.6.77', + '10.10.10.77', ext_gw_info['external_fixed_ips'][0]['ip_address'] ) From 48f352d3883bbaa1aecba3f109859f7277514b34 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 16 Oct 2015 17:06:18 -0400 Subject: [PATCH 0548/3836] Make floating IP func tests less racey The resource string should not be shared across tests. This could cause bad things. Also, change a CIDR used in a test to something less used. Change-Id: I47a32b5b10ebc9096a816d2430387234fbe97cef --- shade/tests/functional/test_floating_ip.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/shade/tests/functional/test_floating_ip.py b/shade/tests/functional/test_floating_ip.py index 5bfae17a6..36a68c2a5 100644 --- a/shade/tests/functional/test_floating_ip.py +++ b/shade/tests/functional/test_floating_ip.py @@ -20,8 +20,6 @@ """ import pprint -import random -import string import time from novaclient import exceptions as nova_exc @@ -48,10 +46,6 @@ def _iterate_timeout(timeout, message): class TestFloatingIP(base.TestCase): timeout = 60 - # Generate a random name for these tests - new_item_name = 'test_' + ''.join( - random.choice(string.ascii_lowercase) for _ in range(5)) - def setUp(self): super(TestFloatingIP, self).setUp() self.cloud = openstack_cloud(cloud='devstack') @@ -65,6 +59,9 @@ def setUp(self): if self.image is None: self.assertFalse('no sensible image available') + # Generate a random name for these tests + self.new_item_name = self.getUniqueString() + self.addCleanup(self._cleanup_network) self.addCleanup(self._cleanup_servers) @@ -170,7 +167,7 @@ def _setup_networks(self): self.test_subnet = self.cloud.create_subnet( subnet_name=self.new_item_name + '_subnet', network_name_or_id=self.test_net['id'], - cidr='172.24.4.0/24', + cidr='10.24.4.0/24', enable_dhcp=True ) # Create a router From 790fac98542ba303274180831cbd9a03604a84e8 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 16 Oct 2015 16:04:21 -0400 Subject: [PATCH 0549/3836] Clean up cache interface, add support for services We just added an unreleased interface method to the CloudConfig object - but maybe that should have been more aligned with dogpile words. SO - change the docs to reference the dogpile words and add support for that, while keeping backwards compat support for people using max_age. Also, do the -/_ transform on the cache config like elsewhere. Then, while we're in there, add support for per-service cache timings. We need this in nodepool and shade is adding support, so ability to configure it will be important. Change-Id: I31190a31ab0b79fc080db3611c0cd584076387d4 --- README.rst | 13 +++++++++-- os_client_config/cloud_config.py | 21 +++++++++++++++-- os_client_config/config.py | 39 ++++++++++++++++++++++++-------- 3 files changed, 59 insertions(+), 14 deletions(-) diff --git a/README.rst b/README.rst index b0466f797..f16bbc091 100644 --- a/README.rst +++ b/README.rst @@ -168,9 +168,15 @@ understands passing through cache settings to dogpile.cache, with the following behaviors: * Listing no config settings means you get a null cache. -* `cache.max_age` and nothing else gets you memory cache. +* `cache.expiration_time` and nothing else gets you memory cache. * Otherwise, `cache.class` and `cache.arguments` are passed in +Different cloud behaviors are also differently expensive to deal with. If you +want to get really crazy and tweak stuff, you can specify different expiration +times on a per-resource basis by passing values, in seconds to an expiration +mapping keyed on the singular name of the resource. A value of `-1` indicates +that the resource should never expire. + `os-client-config` does not actually cache anything itself, but it collects and presents the cache information so that your various applications that are connecting to OpenStack can share a cache should you desire. @@ -179,10 +185,13 @@ are connecting to OpenStack can share a cache should you desire. cache: class: dogpile.cache.pylibmc - max_age: 3600 + expiration_time: 3600 arguments: url: - 127.0.0.1 + expiration: + server: 5 + flavor: -1 clouds: mordred: profile: hp diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index f0c414387..63ff8d29b 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -119,9 +119,9 @@ def get_auth(self): """Return a keystoneauth plugin from the auth credentials.""" return self._auth - def get_cache_interval(self): + def get_cache_expiration_time(self): if self._openstack_config: - return self._openstack_config.get_cache_interval() + return self._openstack_config.get_cache_expiration_time() def get_cache_path(self): if self._openstack_config: @@ -134,3 +134,20 @@ def get_cache_class(self): def get_cache_arguments(self): if self._openstack_config: return self._openstack_config.get_cache_arguments() + + def get_cache_expiration(self): + if self._openstack_config: + return self._openstack_config.get_cache_expiration() + + def get_cache_resource_expiration(self, resource): + """Get expiration time for a resource + + :param resource: Name of the resource type + + :returns: Expiration time for the resource type or None + """ + if self._openstack_config: + expiration = self._openstack_config.get_cache_expiration() + if resource not in expiration: + return None + return expiration[resource] diff --git a/os_client_config/config.py b/os_client_config/config.py index b2c164936..2e3644bd0 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -182,21 +182,34 @@ def __init__(self, config_files=None, vendor_files=None, self.cloud_config = dict( clouds=dict(defaults=dict(self.defaults))) - self._cache_max_age = 0 + self._cache_expiration_time = 0 self._cache_path = CACHE_PATH self._cache_class = 'dogpile.cache.null' self._cache_arguments = {} + self._cache_expiration = {} if 'cache' in self.cloud_config: - self._cache_max_age = self.cloud_config['cache'].get( - 'max_age', self._cache_max_age) - if self._cache_max_age: + cache_settings = self._normalize_keys(self.cloud_config['cache']) + + # expiration_time used to be 'max_age' but the dogpile setting + # is expiration_time. Support max_age for backwards compat. + self._cache_expiration_time = cache_settings.get( + 'expiration_time', cache_settings.get( + 'max_age', self._cache_expiration_time)) + + # If cache class is given, use that. If not, but if cache time + # is given, default to memory. Otherwise, default to nothing. + # to memory. + if self._cache_expiration_time: self._cache_class = 'dogpile.cache.memory' - self._cache_path = os.path.expanduser( - self.cloud_config['cache'].get('path', self._cache_path)) self._cache_class = self.cloud_config['cache'].get( 'class', self._cache_class) - self._cache_arguments = self.cloud_config['cache'].get( + + self._cache_path = os.path.expanduser( + cache_settings.get('path', self._cache_path)) + self._cache_arguments = cache_settings.get( 'arguments', self._cache_arguments) + self._cache_expiration = cache_settings.get( + 'expiration', self._cache_expiration) def _load_config_file(self): return self._load_yaml_file(self._config_files) @@ -221,11 +234,14 @@ def _normalize_keys(self, config): new_config[key] = value return new_config + def get_cache_expiration_time(self): + return self._cache_expiration_time + def get_cache_interval(self): - return self._cache_max_age + return self._cache_expiration_time def get_cache_max_age(self): - return self._cache_max_age + return self._cache_expiration_time def get_cache_path(self): return self._cache_path @@ -234,7 +250,10 @@ def get_cache_class(self): return self._cache_class def get_cache_arguments(self): - return self._cache_arguments + return self._cache_arguments.copy() + + def get_cache_expiration(self): + return self._cache_expiration.copy() def _get_regions(self, cloud): if cloud not in self.cloud_config['clouds']: From 05f3316b74780f5d1746ae57ecfcb34e0dd65365 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 15 Oct 2015 11:22:56 -0400 Subject: [PATCH 0550/3836] Push filtering down into neutron All of the neutron list calls support passing filters to the cloud for server-side filtering. Make use of that so that for things like port lists in nodepool we don't wind up listing a bazillion ports all the time. This should reduce stress on the cloud. The number of API calls we make will be the same, but payloads smaller. Change-Id: Ib134c307c135aef5614f8032d965e61ca7242323 --- shade/__init__.py | 87 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 12 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index b022002f4..c7421a37b 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -827,19 +827,63 @@ def search_keypairs(self, name_or_id=None, filters=None): return _utils._filter_list(keypairs, name_or_id, filters) def search_networks(self, name_or_id=None, filters=None): - networks = self.list_networks() + """Search OpenStack networks + + :param name_or_id: Name or id of the desired network. + :param filters: a dict containing additional filters to use. e.g. + {'router:external': True} + + :returns: a list of dicts containing the network description. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call. + """ + networks = self.list_networks(filters) return _utils._filter_list(networks, name_or_id, filters) def search_routers(self, name_or_id=None, filters=None): - routers = self.list_routers() + """Search OpenStack routers + + :param name_or_id: Name or id of the desired router. + :param filters: a dict containing additional filters to use. e.g. + {'admin_state_up': True} + + :returns: a list of dicts containing the router description. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call. + """ + routers = self.list_routers(filters) return _utils._filter_list(routers, name_or_id, filters) def search_subnets(self, name_or_id=None, filters=None): - subnets = self.list_subnets() + """Search OpenStack subnets + + :param name_or_id: Name or id of the desired subnet. + :param filters: a dict containing additional filters to use. e.g. + {'enable_dhcp': True} + + :returns: a list of dicts containing the subnet description. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call. + """ + subnets = self.list_subnets(filters) return _utils._filter_list(subnets, name_or_id, filters) def search_ports(self, name_or_id=None, filters=None): - ports = self.list_ports() + """Search OpenStack ports + + :param name_or_id: Name or id of the desired port. + :param filters: a dict containing additional filters to use. e.g. + {'device_id': '2711c67a-b4a7-43dd-ace7-6187b791c3f0'} + + :returns: a list of dicts containing the port description. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call. + """ + ports = self.list_ports(filters) return _utils._filter_list(ports, name_or_id, filters) def search_volumes(self, name_or_id=None, filters=None): @@ -898,41 +942,60 @@ def list_keypairs(self): raise OpenStackCloudException( "Error fetching keypair list: %s" % str(e)) - def list_networks(self): + def list_networks(self, filters=None): """List all available networks. + :param filters: (optional) dict of filter conditions to push down :returns: A list of network dicts. """ + # Translate None from search interface to empty {} for kwargs below + if not filters: + filters = {} with self._neutron_exceptions("Error fetching network list"): - return self.manager.submitTask(_tasks.NetworkList())['networks'] + return self.manager.submitTask( + _tasks.NetworkList(**filters))['networks'] - def list_routers(self): + def list_routers(self, filters=None): """List all available routers. + :param filters: (optional) dict of filter conditions to push down :returns: A list of router dicts. """ + # Translate None from search interface to empty {} for kwargs below + if not filters: + filters = {} with self._neutron_exceptions("Error fetching router list"): - return self.manager.submitTask(_tasks.RouterList())['routers'] + return self.manager.submitTask( + _tasks.RouterList(**filters))['routers'] - def list_subnets(self): + def list_subnets(self, filters=None): """List all available subnets. + :param filters: (optional) dict of filter conditions to push down :returns: A list of subnet dicts. """ + # Translate None from search interface to empty {} for kwargs below + if not filters: + filters = {} with self._neutron_exceptions("Error fetching subnet list"): - return self.manager.submitTask(_tasks.SubnetList())['subnets'] + return self.manager.submitTask( + _tasks.SubnetList(**filters))['subnets'] - def list_ports(self): + def list_ports(self, filters=None): """List all available ports. + :param filters: (optional) dict of filter conditions to push down :returns: A list of port dicts. """ + # Translate None from search interface to empty {} for kwargs below + if not filters: + filters = {} with self._neutron_exceptions("Error fetching port list"): - return self.manager.submitTask(_tasks.PortList())['ports'] + return self.manager.submitTask(_tasks.PortList(**filters))['ports'] @_cache_on_arguments(should_cache_fn=_no_pending_volumes) def list_volumes(self, cache=True): From 862a71a6a30b91a6ae2778c0e38e6dd276eec56a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 19 Oct 2015 10:40:31 -0400 Subject: [PATCH 0551/3836] Make a few IP methods private We're getting close to a 1.0 release, and these methods have terrible interfaces. They're also not used anywhere. So let's make them private for now, and if we find a time in the future where we need to expose them, we can do that. Change-Id: I357dbc0c2e61a5b9f62bfbd45595992d7eb21909 --- shade/__init__.py | 12 ++++++------ shade/tests/unit/test_floating_ip_common.py | 4 ++-- shade/tests/unit/test_floating_ip_neutron.py | 8 ++++---- shade/tests/unit/test_floating_ip_nova.py | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 8452e070e..c274b9bd0 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2573,7 +2573,7 @@ def _nova_delete_floating_ip(self, floating_ip_id): return True - def attach_ip_to_server( + def _attach_ip_to_server( self, server, floating_ip, fixed_address=None, wait=False, timeout=60, skip_attach=False): @@ -2767,7 +2767,7 @@ def _nova_detach_ip_from_server(self, server_id, floating_ip_id): return True - def add_ip_from_pool( + def _add_ip_from_pool( self, server, network, fixed_address=None, reuse=True): """Add a floating IP to a sever from a given pool @@ -2788,7 +2788,7 @@ def add_ip_from_pool( else: f_ip = self.create_floating_ip(network=network) - self.attach_ip_to_server( + self._attach_ip_to_server( server=server, floating_ip=f_ip, fixed_address=fixed_address) return f_ip @@ -2810,7 +2810,7 @@ def add_ip_list(self, server, ips): for ip in ips: f_ip = self.get_floating_ip( id=None, filters={'floating_ip_address': ip}) - self.attach_ip_to_server( + self._attach_ip_to_server( server=server, floating_ip=f_ip) def add_auto_ip(self, server, wait=False, timeout=60, reuse=True): @@ -2845,7 +2845,7 @@ def add_auto_ip(self, server, wait=False, timeout=60, reuse=True): # but is only meaninful for the neutron logic branch skip_attach = True - self.attach_ip_to_server( + self._attach_ip_to_server( server=server, floating_ip=f_ip, wait=wait, timeout=timeout, skip_attach=skip_attach) @@ -2855,7 +2855,7 @@ def add_ips_to_server( self, server, auto_ip=True, ips=None, ip_pool=None, wait=False, timeout=60, reuse=True): if ip_pool: - self.add_ip_from_pool(server, ip_pool, reuse=reuse) + self._add_ip_from_pool(server, ip_pool, reuse=reuse) elif ips: self.add_ip_list(server, ips) elif auto_ip: diff --git a/shade/tests/unit/test_floating_ip_common.py b/shade/tests/unit/test_floating_ip_common.py index de95015b6..3b46d0ed5 100644 --- a/shade/tests/unit/test_floating_ip_common.py +++ b/shade/tests/unit/test_floating_ip_common.py @@ -35,7 +35,7 @@ def setUp(self): cloud_config=config.get_one_cloud(validate=False)) @patch.object(OpenStackCloud, 'get_floating_ip') - @patch.object(OpenStackCloud, 'attach_ip_to_server') + @patch.object(OpenStackCloud, '_attach_ip_to_server') @patch.object(OpenStackCloud, 'available_floating_ip') def test_add_auto_ip( self, mock_available_floating_ip, mock_attach_ip_to_server, @@ -62,7 +62,7 @@ def test_add_auto_ip( floating_ip=floating_ip_dict, skip_attach=False) @patch.object(OpenStackCloud, 'nova_client') - @patch.object(OpenStackCloud, 'add_ip_from_pool') + @patch.object(OpenStackCloud, '_add_ip_from_pool') def test_add_ips_to_server_pool( self, mock_add_ip_from_pool, mock_nova_client): server = FakeServer( diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 7ec6e5565..0b5e69523 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -237,7 +237,7 @@ def test_available_floating_ip_existing( @patch.object(OpenStackCloud, 'nova_client') @patch.object(OpenStackCloud, 'keystone_session') @patch.object(OpenStackCloud, '_neutron_create_floating_ip') - @patch.object(OpenStackCloud, 'attach_ip_to_server') + @patch.object(OpenStackCloud, '_attach_ip_to_server') @patch.object(OpenStackCloud, 'has_service') def test_auto_ip_pool_no_reuse( self, mock_has_service, @@ -327,7 +327,7 @@ def test_attach_ip_to_server( mock_neutron_client.list_floatingips.return_value = \ self.mock_floating_ip_list_rep - self.client.attach_ip_to_server( + self.client._attach_ip_to_server( server=self.fake_server, floating_ip=self.floating_ip) @@ -366,7 +366,7 @@ def test_detach_ip_from_server( } ) - @patch.object(OpenStackCloud, 'attach_ip_to_server') + @patch.object(OpenStackCloud, '_attach_ip_to_server') @patch.object(OpenStackCloud, 'available_floating_ip') @patch.object(OpenStackCloud, 'has_service') def test_add_ip_from_pool( @@ -378,7 +378,7 @@ def test_add_ip_from_pool( self.mock_floating_ip_new_rep['floatingip']])[0] mock_attach_ip_to_server.return_value = None - ip = self.client.add_ip_from_pool( + ip = self.client._add_ip_from_pool( server=self.fake_server, network='network-name', fixed_address='192.0.2.129') diff --git a/shade/tests/unit/test_floating_ip_nova.py b/shade/tests/unit/test_floating_ip_nova.py index 97d950b08..03c834cb7 100644 --- a/shade/tests/unit/test_floating_ip_nova.py +++ b/shade/tests/unit/test_floating_ip_nova.py @@ -214,7 +214,7 @@ def test_attach_ip_to_server(self, mock_has_service, mock_nova_client): mock_has_service.side_effect = has_service_side_effect mock_nova_client.floating_ips.list.return_value = self.floating_ips - self.client.attach_ip_to_server( + self.client._attach_ip_to_server( server=self.fake_server, floating_ip=self.floating_ip, fixed_address='192.0.2.129') @@ -242,7 +242,7 @@ def test_add_ip_from_pool(self, mock_has_service, mock_nova_client): mock_has_service.side_effect = has_service_side_effect mock_nova_client.floating_ips.list.return_value = self.floating_ips - ip = self.client.add_ip_from_pool( + ip = self.client._add_ip_from_pool( server=self.fake_server, network='nova', fixed_address='192.0.2.129') From a398485525326c908c98ef9c93685721492f69ec Mon Sep 17 00:00:00 2001 From: Haikel Guemar Date: Mon, 19 Oct 2015 18:22:43 +0200 Subject: [PATCH 0552/3836] Replace Bunch with compatible fork Munch Bunch has dead upstream, so I suggest that we replace it with Munch. Munch is a compatible fork of Bunch with active upstream. Change-Id: I2d272085643ec113d55bc06fa7cc4f40a9f826ed --- requirements.txt | 2 +- shade/meta.py | 10 +++++----- shade/tests/unit/test_domain_params.py | 10 +++++----- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/requirements.txt b/requirements.txt index 238241358..b11566a48 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ pbr>=0.11,<2.0 -bunch +munch decorator jsonpatch ipaddress diff --git a/shade/meta.py b/shade/meta.py index d002f5d9b..2523e2876 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -13,7 +13,7 @@ # limitations under the License. -import bunch +import munch import ipaddress import six @@ -315,10 +315,10 @@ def obj_to_dict(obj): that we can just have a plain dict of all of the values that exist in the nova metadata for a server. """ - # If we obj_to_dict twice, don't fail, just return the bunch - if type(obj) == bunch.Bunch: + # If we obj_to_dict twice, don't fail, just return the munch + if type(obj) == munch.Munch: return obj - instance = bunch.Bunch() + instance = munch.Munch() for key in dir(obj): value = getattr(obj, key) if isinstance(value, NON_CALLABLES) and not key.startswith('_'): @@ -343,7 +343,7 @@ def warlock_to_dict(obj): # glanceclient v2 uses warlock to construct its objects. Warlock does # deep black magic to attribute look up to support validation things that # means we cannot use normal obj_to_dict - obj_dict = bunch.Bunch() + obj_dict = munch.Munch() for (key, value) in obj.items(): if isinstance(value, NON_CALLABLES) and not key.startswith('_'): obj_dict[key] = value diff --git a/shade/tests/unit/test_domain_params.py b/shade/tests/unit/test_domain_params.py index ea6d49375..fe1c7da17 100644 --- a/shade/tests/unit/test_domain_params.py +++ b/shade/tests/unit/test_domain_params.py @@ -15,7 +15,7 @@ import mock import os_client_config as occ -import bunch +import munch import shade from shade import exc @@ -31,7 +31,7 @@ def setUp(self): @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, '_get_project') def test_identity_params_v3(self, mock_get_project, mock_api_version): - mock_get_project.return_value = bunch.Bunch(id=1234) + mock_get_project.return_value = munch.Munch(id=1234) mock_api_version.return_value = '3' ret = self.cloud._get_identity_params(domain_id='5678', project='bar') @@ -44,7 +44,7 @@ def test_identity_params_v3(self, mock_get_project, mock_api_version): @mock.patch.object(shade.OpenStackCloud, '_get_project') def test_identity_params_v3_no_domain( self, mock_get_project, mock_api_version): - mock_get_project.return_value = bunch.Bunch(id=1234) + mock_get_project.return_value = munch.Munch(id=1234) mock_api_version.return_value = '3' self.assertRaises( @@ -55,7 +55,7 @@ def test_identity_params_v3_no_domain( @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, '_get_project') def test_identity_params_v2(self, mock_get_project, mock_api_version): - mock_get_project.return_value = bunch.Bunch(id=1234) + mock_get_project.return_value = munch.Munch(id=1234) mock_api_version.return_value = '2' ret = self.cloud._get_identity_params(domain_id='foo', project='bar') @@ -65,7 +65,7 @@ def test_identity_params_v2(self, mock_get_project, mock_api_version): @mock.patch.object(shade.OpenStackCloud, '_get_project') def test_identity_params_v2_no_domain(self, mock_get_project): - mock_get_project.return_value = bunch.Bunch(id=1234) + mock_get_project.return_value = munch.Munch(id=1234) self.cloud.api_versions = dict(identity='2') From 6afb7ad42f159400ebb8ad0ba77d39f18785e577 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 19 Oct 2015 15:39:33 -0400 Subject: [PATCH 0553/3836] Rename designate domains to zones In anticipation of the Big Rename, rename domains to zones. This helps be less conflicty with keystone. Change-Id: I6e9ad746b0fdafae81ce76f77fa12c7ad8907bd4 Blueprint: the-big-rename --- shade/__init__.py | 39 +++++++++++++++++----------------- shade/_tasks.py | 4 ++-- shade/tests/unit/test_shade.py | 12 +++++------ 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 04c89e01b..38b566ae0 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -959,12 +959,12 @@ def search_floating_ips(self, id=None, filters=None): floating_ips = self.list_floating_ips() return _utils._filter_list(floating_ips, id, filters) - def search_domains(self, name_or_id=None, filters=None): - domains = self.list_domains() - return _utils._filter_list(domains, name_or_id, filters) + def search_zones(self, name_or_id=None, filters=None): + zones = self.list_zones() + return _utils._filter_list(zones, name_or_id, filters) - def search_records(self, domain_id, name_or_id=None, filters=None): - records = self.list_records(domain_id=domain_id) + def search_records(self, zone_id, name_or_id=None, filters=None): + records = self.list_records(zone_id=zone_id) return _utils._filter_list(records, name_or_id, filters) def list_keypairs(self): @@ -1235,21 +1235,22 @@ def _nova_list_floating_ips(self): raise OpenStackCloudException( "error fetching floating IPs list: {msg}".format(msg=str(e))) - def list_domains(self): - """List all available DNS domains. + def list_zones(self): + """List all available DNS zones. - :returns: A list of domain dicts. + :returns: A list of zone dicts. """ try: - return self.manager.submitTask(_tasks.DomainList()) + return self.manager.submitTask(_tasks.ZoneList()) except Exception as e: raise OpenStackCloudException( - "Error fetching domain list: %s" % e) + "Error fetching zone list: %s" % e) - def list_records(self, domain_id): + def list_records(self, zone_id): + # TODO(mordred) switch domain= to zone= after the Big Rename try: - return self.manager.submitTask(_tasks.RecordList(domain=domain_id)) + return self.manager.submitTask(_tasks.RecordList(domain=zone_id)) except Exception as e: raise OpenStackCloudException( "Error fetching record list: %s" % e) @@ -1561,10 +1562,10 @@ def get_floating_ip(self, id, filters=None): """ return _utils._get_entity(self.search_floating_ips, id, filters) - def get_domain(self, name_or_id, filters=None): - """Get a DNS domain by name or ID. + def get_zone(self, name_or_id, filters=None): + """Get a DNS zone by name or ID. - :param name_or_id: Name or ID of the DNS domain. + :param name_or_id: Name or ID of the DNS zone. :param dict filters: A dictionary of meta data to use for further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: @@ -1576,15 +1577,15 @@ def get_domain(self, name_or_id, filters=None): } } - :returns: A domain dict or None if no matching DNS domain is + :returns: A zone dict or None if no matching DNS zone is found. """ - return _utils._get_entity(self.search_domains, name_or_id, filters) + return _utils._get_entity(self.search_zones, name_or_id, filters) - def get_record(self, domain_id, name_or_id, filters=None): + def get_record(self, zone_id, name_or_id, filters=None): f = lambda name_or_id, filters: self.search_records( - domain_id, name_or_id, filters) + zone_id, name_or_id, filters) return _utils._get_entity(f, name_or_id, filters) def create_keypair(self, name, public_key): diff --git a/shade/_tasks.py b/shade/_tasks.py index e8c0f459a..1b325e388 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -566,12 +566,12 @@ def main(self, client): return client.keystone_client.domains.delete(**self.args) -class DomainList(task_manager.Task): +class ZoneList(task_manager.Task): def main(self, client): return client.designate_client.domains.list() -class DomainGet(task_manager.Task): +class ZoneGet(task_manager.Task): def main(self, client): return client.designate_client.domains.get(**self.args) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 0a0caf710..1a262b1dc 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -368,13 +368,13 @@ def test__neutron_exceptions_generic(self): self.assertRaises(exc.OpenStackCloudException, self.cloud.list_networks) - @mock.patch.object(shade.OpenStackCloud, 'list_domains') - def test_get_domain(self, mock_search): - domain1 = dict(id='123', name='mickey') - mock_search.return_value = [domain1] - r = self.cloud.get_domain('mickey') + @mock.patch.object(shade.OpenStackCloud, 'list_zones') + def test_get_zone(self, mock_search): + zone1 = dict(id='123', name='mickey') + mock_search.return_value = [zone1] + r = self.cloud.get_zone('mickey') self.assertIsNotNone(r) - self.assertDictEqual(domain1, r) + self.assertDictEqual(zone1, r) @mock.patch.object(shade.OpenStackCloud, 'list_records') def test_get_record(self, mock_search): From f7afb98dfeb7c70b0ccaea673932a5f8002406a1 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 19 Oct 2015 15:44:24 -0400 Subject: [PATCH 0554/3836] Rename identity_domain to domain Since designate renamed domains to zones (or is going to), we can just call keystone domains "domain" without a prefix of identity. Change-Id: I2d9b56ac53c8bf0745ada16e4419c19d82353a7a --- shade/__init__.py | 26 +++++++++---------- shade/_tasks.py | 15 ++++------- shade/tests/fakes.py | 2 +- shade/tests/functional/test_users.py | 2 +- ...st_identity_domains.py => test_domains.py} | 14 +++++----- 5 files changed, 27 insertions(+), 32 deletions(-) rename shade/tests/unit/{test_identity_domains.py => test_domains.py} (78%) diff --git a/shade/__init__.py b/shade/__init__.py index 38b566ae0..f76ca24c1 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -4900,7 +4900,7 @@ def delete_endpoint(self, id): msg=str(e))) return True - def create_identity_domain( + def create_domain( self, name, description=None, enabled=True): """Create a Keystone domain. @@ -4913,7 +4913,7 @@ def create_identity_domain( :raise OpenStackCloudException: if the domain cannot be created """ try: - domain = self.manager.submitTask(_tasks.IdentityDomainCreate( + domain = self.manager.submitTask(_tasks.DomainCreate( name=name, description=description, enabled=enabled)) @@ -4923,11 +4923,11 @@ def create_identity_domain( msg=str(e))) return meta.obj_to_dict(domain) - def update_identity_domain( + def update_domain( self, domain_id, name=None, description=None, enabled=None): try: return meta.obj_to_dict( - self.manager.submitTask(_tasks.IdentityDomainUpdate( + self.manager.submitTask(_tasks.DomainUpdate( domain=domain_id, description=description, enabled=enabled))) except Exception as e: @@ -4935,7 +4935,7 @@ def update_identity_domain( "Error in updating domain {domain}: {message}".format( domain=domain_id, message=str(e))) - def delete_identity_domain(self, domain_id): + def delete_domain(self, domain_id): """Delete a Keystone domain. :param domain_id: ID of the domain to delete. @@ -4948,15 +4948,15 @@ def delete_identity_domain(self, domain_id): try: # Deleting a domain is expensive, so disabling it first increases # the changes of success - domain = self.update_identity_domain(domain_id, enabled=False) - self.manager.submitTask(_tasks.IdentityDomainDelete( + domain = self.update_domain(domain_id, enabled=False) + self.manager.submitTask(_tasks.DomainDelete( domain=domain['id'])) except Exception as e: raise OpenStackCloudException( "Failed to delete domain {id}: {msg}".format(id=domain_id, msg=str(e))) - def list_identity_domains(self): + def list_domains(self): """List Keystone domains. :returns: a list of dicts containing the domain description. @@ -4965,13 +4965,13 @@ def list_identity_domains(self): the openstack API call. """ try: - domains = self.manager.submitTask(_tasks.IdentityDomainList()) + domains = self.manager.submitTask(_tasks.DomainList()) except Exception as e: raise OpenStackCloudException("Failed to list domains: {msg}" .format(msg=str(e))) return meta.obj_list_to_dict(domains) - def search_identity_domains(self, **filters): + def search_domains(self, **filters): """Seach Keystone domains. :param filters: a dict containing additional filters to use. @@ -4988,13 +4988,13 @@ def search_identity_domains(self, **filters): """ try: domains = self.manager.submitTask( - _tasks.IdentityDomainList(**filters)) + _tasks.DomainList(**filters)) except Exception as e: raise OpenStackCloudException("Failed to list domains: {msg}" .format(msg=str(e))) return meta.obj_list_to_dict(domains) - def get_identity_domain(self, domain_id): + def get_domain(self, domain_id): """Get exactly one Keystone domain. :param domain_id: domain id. @@ -5010,7 +5010,7 @@ def get_identity_domain(self, domain_id): """ try: domain = self.manager.submitTask( - _tasks.IdentityDomainGet(domain=domain_id)) + _tasks.DomainGet(domain=domain_id)) except Exception as e: raise OpenStackCloudException( "Failed to get domain {domain_id}: {msg}".format( diff --git a/shade/_tasks.py b/shade/_tasks.py index 1b325e388..578c0bbe8 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -536,32 +536,27 @@ def main(self, client): return client.keystone_client.endpoints.delete(**self.args) -# IdentityDomain and not Domain because Domain is a DNS concept -class IdentityDomainCreate(task_manager.Task): +class DomainCreate(task_manager.Task): def main(self, client): return client.keystone_client.domains.create(**self.args) -# IdentityDomain and not Domain because Domain is a DNS concept -class IdentityDomainList(task_manager.Task): +class DomainList(task_manager.Task): def main(self, client): return client.keystone_client.domains.list() -# IdentityDomain and not Domain because Domain is a DNS concept -class IdentityDomainGet(task_manager.Task): +class DomainGet(task_manager.Task): def main(self, client): return client.keystone_client.domains.get(**self.args) -# IdentityDomain and not Domain because Domain is a DNS concept -class IdentityDomainUpdate(task_manager.Task): +class DomainUpdate(task_manager.Task): def main(self, client): return client.keystone_client.domains.update(**self.args) -# IdentityDomain and not Domain because Domain is a DNS concept -class IdentityDomainDelete(task_manager.Task): +class DomainDelete(task_manager.Task): def main(self, client): return client.keystone_client.domains.delete(**self.args) diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index ed373c3c5..726d22a6f 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -149,7 +149,7 @@ def __init__(self, id, name, public_key): self.public_key = public_key -class FakeIdentityDomain(object): +class FakeDomain(object): def __init__(self, id, name, description, enabled): self.id = id self.name = name diff --git a/shade/tests/functional/test_users.py b/shade/tests/functional/test_users.py index 3cdfae488..4b83ec875 100644 --- a/shade/tests/functional/test_users.py +++ b/shade/tests/functional/test_users.py @@ -52,7 +52,7 @@ def _create_user(self, **kwargs): domain_id = None identity_version = self.cloud.cloud_config.get_api_version('identity') if identity_version not in ('2', '2.0'): - domain = self.cloud.get_identity_domain('default') + domain = self.cloud.get_domain('default') domain_id = domain['id'] return self.cloud.create_user(domain_id=domain_id, **kwargs) diff --git a/shade/tests/unit/test_identity_domains.py b/shade/tests/unit/test_domains.py similarity index 78% rename from shade/tests/unit/test_identity_domains.py rename to shade/tests/unit/test_domains.py index 1ccef8178..a016d0732 100644 --- a/shade/tests/unit/test_identity_domains.py +++ b/shade/tests/unit/test_domains.py @@ -20,7 +20,7 @@ from shade.tests import fakes -domain_obj = fakes.FakeIdentityDomain( +domain_obj = fakes.FakeDomain( id='1', name='a-domain', description='A wonderful keystone domain', @@ -28,21 +28,21 @@ ) -class TestIdentityDomains(base.TestCase): +class TestDomains(base.TestCase): def setUp(self): - super(TestIdentityDomains, self).setUp() + super(TestDomains, self).setUp() self.cloud = shade.operator_cloud(validate=False) @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_list_identity_domains(self, mock_keystone): - self.cloud.list_identity_domains() + def test_list_domains(self, mock_keystone): + self.cloud.list_domains() self.assertTrue(mock_keystone.domains.list.called) @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_get_identity_domain(self, mock_keystone): + def test_get_domain(self, mock_keystone): mock_keystone.domains.get.return_value = domain_obj - domain = self.cloud.get_identity_domain(domain_id='1234') + domain = self.cloud.get_domain(domain_id='1234') self.assertFalse(mock_keystone.domains.list.called) self.assertTrue(mock_keystone.domains.get.called) self.assertEqual(domain['name'], 'a-domain') From e081dcbb0d11e322a33d12c0ddd30703101f5f59 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 18 Oct 2015 21:41:34 -0400 Subject: [PATCH 0555/3836] Rely on devstack for clouds.yaml devstack makes clouds.yaml in /etc now so each project doesn't need to copy it. Depends-On: I21d3c2ad7a020a5ab02dc1ab532feae70b718892 Change-Id: I5010486f288f781611f8e838cf1c4089ad67ed53 --- shade/tests/functional/hooks/post_test_hook.sh | 6 ------ 1 file changed, 6 deletions(-) diff --git a/shade/tests/functional/hooks/post_test_hook.sh b/shade/tests/functional/hooks/post_test_hook.sh index 128e4a893..df9557451 100755 --- a/shade/tests/functional/hooks/post_test_hook.sh +++ b/shade/tests/functional/hooks/post_test_hook.sh @@ -14,12 +14,6 @@ export SHADE_DIR="$BASE/new/shade" -# The jenkins user needs access to the clouds.yaml file -# for the functional tests. -sudo mkdir -p ~jenkins/.config/openstack -sudo cp $BASE/new/.config/openstack/clouds.yaml ~jenkins/.config/openstack -sudo chown -R jenkins:stack ~jenkins/.config - cd $SHADE_DIR sudo chown -R jenkins:stack $SHADE_DIR From 0d22456cba63d5915873faa593a0c05a8b47ce11 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 21 Oct 2015 01:50:40 +0900 Subject: [PATCH 0556/3836] Make designate record methods private for now The API for designate is not something we can commit to right now. It turns out that v1 has Domains and Records and v2 has Zones, RecordSets and Records. Figuring out an API design that can handle both is not going to happen before 1.0, so mark the current Record API as private so that we can add something we can commit to later. Change-Id: Ia2611f19d04a2d7a51d85ac99f19ed4054972085 --- shade/__init__.py | 10 +++++----- shade/tests/unit/test_shade.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index f76ca24c1..55744dea6 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -963,8 +963,8 @@ def search_zones(self, name_or_id=None, filters=None): zones = self.list_zones() return _utils._filter_list(zones, name_or_id, filters) - def search_records(self, zone_id, name_or_id=None, filters=None): - records = self.list_records(zone_id=zone_id) + def _search_records(self, zone_id, name_or_id=None, filters=None): + records = self._list_records(zone_id=zone_id) return _utils._filter_list(records, name_or_id, filters) def list_keypairs(self): @@ -1247,7 +1247,7 @@ def list_zones(self): raise OpenStackCloudException( "Error fetching zone list: %s" % e) - def list_records(self, zone_id): + def _list_records(self, zone_id): # TODO(mordred) switch domain= to zone= after the Big Rename try: return self.manager.submitTask(_tasks.RecordList(domain=zone_id)) @@ -1583,8 +1583,8 @@ def get_zone(self, name_or_id, filters=None): """ return _utils._get_entity(self.search_zones, name_or_id, filters) - def get_record(self, zone_id, name_or_id, filters=None): - f = lambda name_or_id, filters: self.search_records( + def _get_record(self, zone_id, name_or_id, filters=None): + f = lambda name_or_id, filters: self._search_records( zone_id, name_or_id, filters) return _utils._get_entity(f, name_or_id, filters) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 1a262b1dc..767179ae3 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -376,10 +376,10 @@ def test_get_zone(self, mock_search): self.assertIsNotNone(r) self.assertDictEqual(zone1, r) - @mock.patch.object(shade.OpenStackCloud, 'list_records') + @mock.patch.object(shade.OpenStackCloud, '_list_records') def test_get_record(self, mock_search): record1 = dict(id='123', name='mickey', domain_id='mickey.domain') mock_search.return_value = [record1] - r = self.cloud.get_record('mickey.domain', 'mickey') + r = self.cloud._get_record('mickey.domain', 'mickey') self.assertIsNotNone(r) self.assertDictEqual(record1, r) From cbc32990c9e95d89d06a9fada58fe02fc817d487 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 19 Oct 2015 11:29:32 -0400 Subject: [PATCH 0557/3836] Set cache information from clouds.yaml With 1.9.0 of os-client-config, there are appropriate methods on the cloud_config object which allow access to cache config settings. Additionally, there is a documented mechanism for passing in per-resource cache timing, for things like list_servers. Depends-On: I352f827715821db8a69983eae0ee3e9c8c99f512 Change-Id: Iba9af1cdddf0844c885ff203f67a522dc451a3ed --- requirements.txt | 2 +- shade/__init__.py | 39 ++++++++++++++------------------------- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/requirements.txt b/requirements.txt index 238241358..560168d44 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ bunch decorator jsonpatch ipaddress -os-client-config>=1.8.0 +os-client-config>=1.9.0 six keystoneauth1>=1.0.0 diff --git a/shade/__init__.py b/shade/__init__.py index c274b9bd0..efb2778e9 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -122,9 +122,6 @@ def openstack_clouds(config=None, debug=False): return [ OpenStackCloud( cloud=f.name, debug=debug, - cache_interval=config.get_cache_max_age(), - cache_class=config.get_cache_class(), - cache_arguments=config.get_cache_arguments(), cloud_config=f, **f.config) for f in config.get_all_clouds() @@ -142,11 +139,7 @@ def openstack_cloud(config=None, **kwargs): except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin as e: raise OpenStackCloudException( "Invalid cloud configuration: {exc}".format(exc=str(e))) - return OpenStackCloud( - cache_interval=config.get_cache_max_age(), - cache_class=config.get_cache_class(), - cache_arguments=config.get_cache_arguments(), - cloud_config=cloud_config) + return OpenStackCloud(cloud_config=cloud_config) def operator_cloud(config=None, **kwargs): @@ -159,11 +152,7 @@ def operator_cloud(config=None, **kwargs): except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin as e: raise OpenStackCloudException( "Invalid cloud configuration: {exc}".format(exc=str(e))) - return OperatorCloud( - cache_interval=config.get_cache_max_age(), - cache_class=config.get_cache_class(), - cache_arguments=config.get_cache_arguments(), - cloud_config=cloud_config) + return OperatorCloud(cloud_config=cloud_config) _decorated_methods = [] @@ -215,14 +204,6 @@ class OpenStackCloud(object): and that Floating IP will be actualized either via neutron or via nova depending on how this particular cloud has decided to arrange itself. - :param int cache_interval: How long to cache items fetched from the cloud. - Value will be passed to dogpile.cache. None - means do not cache at all. - (optional, defaults to None) - :param string cache_class: What dogpile.cache cache class to use. - (optional, defaults to "dogpile.cache.null") - :param dict cache_arguments: Additional arguments to pass to the cache - constructor (optional, defaults to None) :param TaskManager manager: Optional task manager to use for running OpenStack API tasks. Unless you're doing rate limiting client side, you almost @@ -237,9 +218,6 @@ class OpenStackCloud(object): def __init__( self, cloud_config=None, - cache_interval=None, - cache_class='dogpile.cache.null', - cache_arguments=None, manager=None, **kwargs): self.log = _log.setup_logging('shade') @@ -287,11 +265,17 @@ def __init__( self._servers_time = 0 self._servers_lock = threading.Lock() + cache_expiration_time = cloud_config.get_cache_expiration_time() + cache_class = cloud_config.get_cache_class() + cache_arguments = cloud_config.get_cache_arguments() + cache_expiration = cloud_config.get_cache_expiration() + if cache_class != 'dogpile.cache.null': self._cache = cache.make_region( function_key_generator=self._make_cache_key ).configure( - cache_class, expiration_time=cache_interval, + cache_class, + expiration_time=cache_expiration_time, arguments=cache_arguments) else: def _fake_invalidate(unused): @@ -318,6 +302,11 @@ def invalidate(self): new_func.invalidate = _fake_invalidate setattr(self, method, new_func) + # If server expiration time is set explicitly, use that. Otherwise + # fall back to whatever it was before + self._SERVER_LIST_AGE = cache_expiration.get( + 'server', self._SERVER_LIST_AGE) + self._container_cache = dict() self._file_hash_cache = dict() From 76b081efe45f1f2ab8e09a1fd5a23d9fc54361bc Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 20 Oct 2015 16:48:00 -0400 Subject: [PATCH 0558/3836] Normalize user information We need to be consistent with the information returned about the user across keystone versions. Change-Id: I73890078e4c321db250c0245cad6b7c455c87ffc --- shade/__init__.py | 33 ++++++++++++++++++++++++++----- shade/_tasks.py | 5 +++++ shade/_utils.py | 15 ++++++++++++++ shade/tests/unit/test_caching.py | 34 ++++++++++++++++++++------------ 4 files changed, 69 insertions(+), 18 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index f76ca24c1..d574b9f83 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -579,7 +579,7 @@ def list_users(self): raise OpenStackCloudException( "Failed to list users: {0}".format(str(e)) ) - return meta.obj_list_to_dict(users) + return _utils.normalize_users(meta.obj_list_to_dict(users)) def search_users(self, name_or_id=None, filters=None): """Seach Keystone users. @@ -598,7 +598,7 @@ def search_users(self, name_or_id=None, filters=None): def get_user(self, name_or_id, filters=None): """Get exactly one Keystone user. - :param string id: user name or id. + :param string name_or_id: user name or id. :param dict filters: a dict containing additional filters to use. :returns: a single dict containing the user description. @@ -608,13 +608,34 @@ def get_user(self, name_or_id, filters=None): """ return _utils._get_entity(self.search_users, name_or_id, filters) + def get_user_by_id(self, user_id, normalize=True): + """Get a Keystone user by ID. + + :param string user_id: user ID + :param bool normalize: Flag to control dict normalization + + :returns: a single dict containing the user description + """ + try: + user = meta.obj_to_dict( + self.manager.submitTask(_tasks.UserGet(user=user_id)) + ) + except Exception as e: + raise OpenStackCloudException( + "Error getting user with ID {user_id}: {message}".format( + user_id=user_id, message=str(e))) + if user and normalize: + return _utils.normalize_users([user])[0] + return user + # NOTE(Shrews): Keystone v2 supports updating only name, email and enabled. @valid_kwargs('name', 'email', 'enabled', 'domain_id', 'password', 'description', 'default_project') def update_user(self, name_or_id, **kwargs): self.list_users.invalidate(self) user = self.get_user(name_or_id) - kwargs['user'] = user + # normalized dict won't work + kwargs['user'] = self.get_user_by_id(user['id'], normalize=False) if self.cloud_config.get_api_version('identity') != '3': # Do not pass v3 args to a v2 keystone. @@ -635,7 +656,7 @@ def update_user(self, name_or_id, **kwargs): "Error in updating user {user}: {message}".format( user=name_or_id, message=str(e))) self.list_users.invalidate(self) - return meta.obj_to_dict(user) + return _utils.normalize_users([meta.obj_to_dict(user)])[0] def create_user( self, name, password=None, email=None, default_project=None, @@ -652,7 +673,7 @@ def create_user( "Error in creating user {user}: {message}".format( user=name, message=str(e))) self.list_users.invalidate(self) - return meta.obj_to_dict(user) + return _utils.normalize_users([meta.obj_to_dict(user)])[0] def delete_user(self, name_or_id): self.list_users.invalidate(self) @@ -662,6 +683,8 @@ def delete_user(self, name_or_id): "User {0} not found for deleting".format(name_or_id)) return False + # normalized dict won't work + user = self.get_user_by_id(user['id'], normalize=False) try: self.manager.submitTask(_tasks.UserDelete(user=user)) except Exception as e: diff --git a/shade/_tasks.py b/shade/_tasks.py index 578c0bbe8..74c2c1b51 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -37,6 +37,11 @@ def main(self, client): return client.keystone_client.users.update(**self.args) +class UserGet(task_manager.Task): + def main(self, client): + return client.keystone_client.users.get(**self.args) + + class FlavorList(task_manager.Task): def main(self, client): return client.nova_client.flavors.list(**self.args) diff --git a/shade/_utils.py b/shade/_utils.py index 0fcce4790..5d9a31ae9 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -270,3 +270,18 @@ def localhost_supports_ipv6(): """ return netifaces.AF_INET6 in netifaces.gateways()['default'] + + +def normalize_users(users): + return [ + dict( + id=user.get('id'), + email=user.get('email'), + name=user.get('name'), + username=user.get('username'), + default_project_id=user.get('default_project_id', + user.get('tenantId')), + domain_id=user.get('domain_id'), + enabled=user.get('enabled'), + ) for user in users + ] diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index fc2d70de6..c78e41560 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -165,8 +165,11 @@ def now_gone(): def test_list_users(self, keystone_mock): fake_user = fakes.FakeUser('999', '', '') keystone_mock.users.list.return_value = [fake_user] - self.assertEqual([{'id': '999', 'name': '', 'email': ''}], - self.cloud.list_users()) + users = self.cloud.list_users() + self.assertEqual(1, len(users)) + self.assertEqual('999', users[0]['id']) + self.assertEqual('', users[0]['name']) + self.assertEqual('', users[0]['email']) @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_modify_user_invalidates_cache(self, keystone_mock): @@ -180,14 +183,17 @@ def test_modify_user_invalidates_cache(self, keystone_mock): keystone_mock.users.create.return_value = fake_user created = self.cloud.create_user(name='abc123 name', email='abc123@domain.test') - self.assertEqual({'id': 'abc123', - 'name': 'abc123 name', - 'email': 'abc123@domain.test'}, created) + self.assertEqual('abc123', created['id']) + self.assertEqual('abc123 name', created['name']) + self.assertEqual('abc123@domain.test', created['email']) + # Cache should have been invalidated - self.assertEqual([{'id': 'abc123', - 'name': 'abc123 name', - 'email': 'abc123@domain.test'}], - self.cloud.list_users()) + users = self.cloud.list_users() + self.assertEqual(1, len(users)) + self.assertEqual('abc123', users[0]['id']) + self.assertEqual('abc123 name', users[0]['name']) + self.assertEqual('abc123@domain.test', users[0]['email']) + # Update and check to see if it is updated fake_user2 = fakes.FakeUser('abc123', 'abc123-changed@domain.test', @@ -195,13 +201,15 @@ def test_modify_user_invalidates_cache(self, keystone_mock): fake_user2_dict = meta.obj_to_dict(fake_user2) keystone_mock.users.update.return_value = fake_user2 keystone_mock.users.list.return_value = [fake_user2] + keystone_mock.users.get.return_value = fake_user2_dict self.cloud.update_user('abc123', email='abc123-changed@domain.test') keystone_mock.users.update.assert_called_with( user=fake_user2_dict, email='abc123-changed@domain.test') - self.assertEqual([{'id': 'abc123', - 'name': 'abc123 name', - 'email': 'abc123-changed@domain.test'}], - self.cloud.list_users()) + users = self.cloud.list_users() + self.assertEqual(1, len(users)) + self.assertEqual('abc123', users[0]['id']) + self.assertEqual('abc123 name', users[0]['name']) + self.assertEqual('abc123-changed@domain.test', users[0]['email']) # Now delete and ensure it disappears keystone_mock.users.list.return_value = [] self.cloud.delete_user('abc123') From 0a1c7c84a3a7ce63a531861129beef0207c3931c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 21 Oct 2015 06:02:27 +0900 Subject: [PATCH 0559/3836] Support private address override in inventory ansible inventory supports a command line switch --private which overrides the clouds.yaml setting. Support passing it in. Change-Id: I74bf98be26b605a5e9ac74ab478f421c39616d74 --- shade/cmd/inventory.py | 4 +++- shade/inventory.py | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/shade/cmd/inventory.py b/shade/cmd/inventory.py index c4d396f46..4f9538fff 100755 --- a/shade/cmd/inventory.py +++ b/shade/cmd/inventory.py @@ -38,6 +38,8 @@ def parse_args(): group.add_argument('--list', action='store_true', help='List active servers') group.add_argument('--host', help='List details about the specific host') + parser.add_argument('--private', action='store_true', default=False, + help='Use private IPs for interface_ip') parser.add_argument('--yaml', action='store_true', default=False, help='Output data in nicely readable yaml') parser.add_argument('--debug', action='store_true', default=False, @@ -50,7 +52,7 @@ def main(): try: shade.simple_logging(debug=args.debug) inventory = shade.inventory.OpenStackInventory( - refresh=args.refresh) + refresh=args.refresh, private=args.private) if args.list: output = inventory.list_hosts() elif args.host: diff --git a/shade/inventory.py b/shade/inventory.py index d66808a84..2c15d7fc5 100644 --- a/shade/inventory.py +++ b/shade/inventory.py @@ -21,7 +21,7 @@ class OpenStackInventory(object): def __init__( - self, config_files=[], refresh=False): + self, config_files=[], refresh=False, private=False): config = os_client_config.config.OpenStackConfig( config_files=os_client_config.config.CONFIG_FILES + config_files) @@ -34,6 +34,9 @@ def __init__( **f.config) for f in config.get_all_clouds() ] + if private: + for cloud in self.clouds: + cloud.private = True # Handle manual invalidation of entire persistent cache if refresh: From 26aff17bd3aa0c3d42947ee45bdcf46bf75832ef Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 21 Oct 2015 08:38:59 +0900 Subject: [PATCH 0560/3836] novaclient 2.32.0 does not work against rackspace shade should be clear that you should not install it. Change-Id: I05c2b8f4b1e1403a78c0a72e7613f8697cb70218 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 238241358..eb04cf71d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ six keystoneauth1>=1.0.0 netifaces>=0.10.4 -python-novaclient>=2.21.0,!=2.27.0 +python-novaclient>=2.21.0,!=2.27.0,!=2.32.0 python-keystoneclient>=0.11.0 python-glanceclient>=1.0.0 python-cinderclient<1.2 From da2bfd7a919d8888e0d79999005c210c3e9c5118 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 21 Oct 2015 06:11:59 +0900 Subject: [PATCH 0561/3836] Use OCC to create clouds in inventory The inventory module has lagged behind just a smidge. Change-Id: I9207128c82b0f5f48b012231178cc835906426d5 --- shade/inventory.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/shade/inventory.py b/shade/inventory.py index 2c15d7fc5..2ea9995c8 100644 --- a/shade/inventory.py +++ b/shade/inventory.py @@ -26,14 +26,10 @@ def __init__( config_files=os_client_config.config.CONFIG_FILES + config_files) self.clouds = [ - shade.OpenStackCloud( - cloud=f.name, - cache_interval=config.get_cache_max_age(), - cache_class=config.get_cache_class(), - cache_arguments=config.get_cache_arguments(), - **f.config) - for f in config.get_all_clouds() - ] + shade.OpenStackCloud(cloud_config=cloud_config) + for cloud_config in config.get_all_clouds() + ] + if private: for cloud in self.clouds: cloud.private = True From 7aa043ec8781d9ccdb07b90fe694ae482198c022 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 12 Sep 2015 10:07:24 +0200 Subject: [PATCH 0562/3836] Add heatclient support In support of https://github.com/ansible/ansible-modules-core/pull/2036 add at least a heat client constructor. Also, check it out - heatclient doesn't support Session auth. Until it does, we're not going to support advanced things like overriding SSL args. Change-Id: I488fde4abe5abc1c58515cf77d08644b49eb5ba6 --- requirements.txt | 1 + shade/__init__.py | 21 +++++++++++++++++++++ shade/tests/unit/test_shade.py | 15 +++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/requirements.txt b/requirements.txt index 3a7c965af..3bd8985ad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,5 +18,6 @@ python-troveclient>=1.2.0 python-ironicclient>=0.9.0 python-swiftclient>=2.5.0 python-designateclient>=1.3.0 +python-heatclient>=0.3.0 dogpile.cache>=0.5.3 diff --git a/shade/__init__.py b/shade/__init__.py index 3a0001fd8..b56ec3ea2 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -29,6 +29,8 @@ import glanceclient.exc from glanceclient.common import utils as glance_utils import ipaddress +from heatclient import client as heat_client +from heatclient.common import template_utils from ironicclient import client as ironic_client from ironicclient import exceptions as ironic_exceptions import jsonpatch @@ -316,6 +318,7 @@ def invalidate(self): self._designate_client = None self._glance_client = None self._glance_endpoint = None + self._heat_client = None self._ironic_client = None self._keystone_client = None self._neutron_client = None @@ -695,6 +698,24 @@ def glance_client(self): endpoint=endpoint) return self._glance_client + @property + def heat_client(self): + if self._heat_client is None: + self._heat_client = self._get_client( + 'orchestration', heat_client.Client) + return self._heat_client + + def get_template_contents( + self, template_file=None, template_url=None, + template_object=None, files=None): + try: + return template_utils.get_template_contents( + template_file=template_file, template_url=template_url, + template_object=template_object, files=files) + except Exception as e: + raise OpenStackCloudException( + "Error in processing template files: %s" % str(e)) + @property def swift_client(self): if self._swift_client is None: diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 767179ae3..87bcae552 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -15,6 +15,7 @@ import mock import glanceclient +from heatclient import client as heat_client from keystoneclient.v2_0 import client as k2_client from keystoneclient.v3 import client as k3_client from neutronclient.common import exceptions as n_exc @@ -111,6 +112,20 @@ def test_glance_args( service_type='image', session=mock.ANY, ) + @mock.patch.object(shade.OpenStackCloud, 'keystone_session') + @mock.patch.object(heat_client, 'Client') + def test_heat_args(self, mock_client, mock_keystone_session): + mock_keystone_session.return_value = None + self.cloud.heat_client + mock_client.assert_called_with( + endpoint_type='public', + region_name='', + service_name=None, + service_type='orchestration', + session=mock.ANY, + version='1' + ) + @mock.patch.object(shade.OpenStackCloud, 'search_subnets') def test_get_subnet(self, mock_search): subnet = dict(id='123', name='mickey') From 0bb5192f0c980413cfe916208789eff767103eb2 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 13 Sep 2015 20:35:08 +0200 Subject: [PATCH 0563/3836] Abstract out the name of the name key Heat and Cinder each have different key names for their "name" concept. Rather than keeping a list of them, make an argument so we can override it in the instances we need to. Change-Id: I6ba0b69129c74e976ea6762728a940b4670f3873 --- shade/__init__.py | 3 ++- shade/_utils.py | 16 +++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index b56ec3ea2..d5cd3e70b 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -960,7 +960,8 @@ def search_ports(self, name_or_id=None, filters=None): def search_volumes(self, name_or_id=None, filters=None): volumes = self.list_volumes() - return _utils._filter_list(volumes, name_or_id, filters) + return _utils._filter_list( + volumes, name_or_id, filters, name_key='display_name') def search_flavors(self, name_or_id=None, filters=None): flavors = self.list_flavors() diff --git a/shade/_utils.py b/shade/_utils.py index 5d9a31ae9..91c09006a 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -37,13 +37,14 @@ def _iterate_timeout(timeout, message, wait=2): raise exc.OpenStackCloudTimeout(message) -def _filter_list(data, name_or_id, filters): +def _filter_list(data, name_or_id, filters, name_key='name'): """Filter a list by name/ID and arbitrary meta data. :param list data: The list of dictionary data to filter. It is expected that - each dictionary contains an 'id', 'name' (or 'display_name') - key if a value for name_or_id is given. + each dictionary contains an 'id' and 'name' + key if a value for name_or_id is given. The 'name' key can be + overridden with the name_key parameter. :param string name_or_id: The name or ID of the entity being filtered. :param dict filters: @@ -56,15 +57,16 @@ def _filter_list(data, name_or_id, filters): 'gender': 'Female' } } + :param string name_key: + The name of the name key. Cinder wants display_name. Heat wants + stack_name. Defaults to 'name' """ if name_or_id: identifier_matches = [] for e in data: e_id = str(e.get('id', None)) - e_name = e.get('name', None) - # cinder likes to be different and use display_name - e_display_name = e.get('display_name', None) - if str(name_or_id) in (e_id, e_name, e_display_name): + e_name = e.get(name_key, None) + if str(name_or_id) in (e_id, e_name): identifier_matches.append(e) data = identifier_matches From 3d66d1f9fbd5a87d5155e73d599734a03dbe7766 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 31 Jan 2015 16:46:08 -0500 Subject: [PATCH 0564/3836] Add heat support A lovely human upstream in ansible submitted a heat module - which means that it needs to be rewritten to use shade - which means we need heat support in shade. Add it. Change-Id: Idb0fe452698d74180a0ae96963c3d8cbca4d47e5 --- shade/__init__.py | 111 ++++++++++++++++++++++++++++++++++++++++++++++ shade/_tasks.py | 15 +++++++ 2 files changed, 126 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index d5cd3e70b..e64a4de39 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -196,6 +196,15 @@ def _no_pending_images(images): return True +def _no_pending_stacks(stacks): + '''If there are any stacks not in a steady state, don't cache''' + for stack in stacks: + status = stack['status'] + if '_COMPLETE' not in status and '_FAILED' not in status: + return False + return True + + class OpenStackCloud(object): """Represent a connection to an OpenStack Cloud. @@ -802,6 +811,62 @@ def designate_client(self): 'dns', designate_client.Client) return self._designate_client + def create_stack( + self, name, + template_file=None, template_url=None, + template_object=None, files=None, + rollback=True, + wait=False, timeout=180, + **parameters): + tpl_files, template = template_utils.get_template_contents( + template_file=template_file, + template_url=template_url, + template_object=template_object, + object_request=object_request, + files=files) + params = dict( + stack_name=name, + disable_rollback=not rollback, + parameters=parameters, + template=template, + files=tpl_files, + ) + try: + stack = self.manager.submitTask(_tasks.StackCreate(**params)) + except Exception as e: + raise OpenStackCloudException( + "Error creating stack {name}: {message}".format( + name=name, message=e.message)) + if not wait: + return stack + for count in _iterate_timeout( + timeout, + "Timed out waiting for heat stack to finish"): + if self.get_stack(name, cache=False): + return stack + + def delete_stack(self, name_or_id): + """Delete a Heat Stack + + :param name_or_id: Stack name or id. + + :returns: True if delete succeeded, False otherwise. + + :raises: ``OpenStackCloudException`` if something goes wrong during + the openstack API call + """ + stack = self.get_stack(name_or_id=name_or_id) + if stack is None: + self.log.debug("Stack %s not found for deleting" % name_or_id) + return False + + try: + self.manager.submitTask(_tasks.StackDelete(id=stack['id'])) + except Exception: + raise OpenStackCloudException( + "Failed to delete stack {id}".format(id=stack['id'])) + return True + def get_name(self): return self.name @@ -1001,6 +1066,22 @@ def _search_records(self, zone_id, name_or_id=None, filters=None): records = self._list_records(zone_id=zone_id) return _utils._filter_list(records, name_or_id, filters) + def search_stacks(self, name_or_id=None, filters=None): + """Search Heat stacks. + + :param name_or_id: Name or id of the desired stack. + :param filters: a dict containing additional filters to use. e.g. + {'stack_status': 'CREATE_COMPLETE'} + + :returns: a list of dict containing the stack description. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call. + """ + stacks = self.list_stacks() + return _utils._filter_list( + stacks, name_or_id, filters, name_name='stack_name') + def list_keypairs(self): """List all available keypairs. @@ -1103,6 +1184,21 @@ def list_flavors(self): raise OpenStackCloudException( "Error fetching flavor list: %s" % e) + @_cache_on_arguments(should_cache_fn=_no_pending_stacks) + def list_stacks(self): + """List all Heat stacks. + + :returns: a list of dict containing the stack description. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call. + """ + try: + stacks = self.manager.submitTask(_tasks.StackList()) + except Exception as e: + raise OpenStackCloudException(str(e)) + return meta.obj_list_to_dict(stacks) + def list_security_groups(self): """List all available security groups. @@ -1622,6 +1718,21 @@ def _get_record(self, zone_id, name_or_id, filters=None): zone_id, name_or_id, filters) return _utils._get_entity(f, name_or_id, filters) + def get_stack(self, name_or_id, filters=None): + """Get exactly one Heat stack. + + :param name_or_id: Name or id of the desired stack. + :param filters: a dict containing additional filters to use. e.g. + {'stack_status': 'CREATE_COMPLETE'} + + :returns: a dict containing the stack description + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call or if multiple matches are found. + """ + return _utils._get_entity( + self.search_stacks, name_or_id, filters) + def create_keypair(self, name, public_key): """Create a new keypair. diff --git a/shade/_tasks.py b/shade/_tasks.py index 74c2c1b51..1873064c4 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -599,3 +599,18 @@ def main(self, client): class RoleDelete(task_manager.Task): def main(self, client): return client.keystone_client.roles.delete(**self.args) + + +class StackList(task_manager.Task): + def main(self, client): + return client.heat_client.stacks.list() + + +class StackCreate(task_manager.Task): + def main(self, client): + return client.heat_client.stacks.create(**self.args) + + +class StackDelete(task_manager.Task): + def main(self, client): + return client.heat_client.stacks.delete(**self.args) From 0ab8b82c6bdc764cb972e8d47660491f7aca5378 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 22 Oct 2015 09:16:57 +0900 Subject: [PATCH 0565/3836] Move valid_kwargs decorator to _utils It's not really a public symbol. Change-Id: Iab016b93697f1e44fd299d6bfbfc80677030102d --- doc/source/coding.rst | 2 +- shade/__init__.py | 51 ++++++++++--------------------------------- shade/_utils.py | 28 ++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 40 deletions(-) diff --git a/doc/source/coding.rst b/doc/source/coding.rst index be67398ca..40a49e7c9 100644 --- a/doc/source/coding.rst +++ b/doc/source/coding.rst @@ -30,7 +30,7 @@ API Methods `update_RESOURCE()` API methods (as it makes sense). - For those methods that should behave differently for omitted or None-valued - parameters, use the `valid_kwargs` decorator. Notably: all Neutron + parameters, use the `_utils.valid_kwargs` decorator. Notably: all Neutron `update_*` functions. - Deleting a resource should return True if the delete succeeded, or False diff --git a/shade/__init__.py b/shade/__init__.py index e64a4de39..8c5821dca 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -15,7 +15,6 @@ import contextlib import functools import hashlib -import inspect import logging import operator import threading @@ -23,7 +22,6 @@ from cinderclient.v1 import client as cinder_client from designateclient.v1 import Client as designate_client -from decorator import decorator from dogpile import cache import glanceclient import glanceclient.exc @@ -81,32 +79,6 @@ } -def valid_kwargs(*valid_args): - # This decorator checks if argument passed as **kwargs to a function are - # present in valid_args. - # - # Typically, valid_kwargs is used when we want to distinguish between - # None and omitted arguments and we still want to validate the argument - # list. - # - # Example usage: - # - # @valid_kwargs('opt_arg1', 'opt_arg2') - # def my_func(self, mandatory_arg1, mandatory_arg2, **kwargs): - # ... - # - @decorator - def func_wrapper(func, *args, **kwargs): - argspec = inspect.getargspec(func) - for k in kwargs: - if k not in argspec.args[1:] and k not in valid_args: - raise TypeError( - "{f}() got an unexpected keyword argument " - "'{arg}'".format(f=inspect.stack()[1][3], arg=k)) - return func(*args, **kwargs) - return func_wrapper - - def simple_logging(debug=False): if debug: log_level = logging.DEBUG @@ -630,8 +602,8 @@ def get_user_by_id(self, user_id, normalize=True): return user # NOTE(Shrews): Keystone v2 supports updating only name, email and enabled. - @valid_kwargs('name', 'email', 'enabled', 'domain_id', 'password', - 'description', 'default_project') + @_utils.valid_kwargs('name', 'email', 'enabled', 'domain_id', 'password', + 'description', 'default_project') def update_user(self, name_or_id, **kwargs): self.list_users.invalidate(self) user = self.get_user(name_or_id) @@ -3671,10 +3643,10 @@ def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, # a dict). return new_subnet['subnet'] - @valid_kwargs('name', 'admin_state_up', 'mac_address', 'fixed_ips', - 'subnet_id', 'ip_address', 'security_groups', - 'allowed_address_pairs', 'extra_dhcp_opts', 'device_owner', - 'device_id') + @_utils.valid_kwargs('name', 'admin_state_up', 'mac_address', 'fixed_ips', + 'subnet_id', 'ip_address', 'security_groups', + 'allowed_address_pairs', 'extra_dhcp_opts', + 'device_owner', 'device_id') def create_port(self, network_id, **kwargs): """Create a port @@ -3735,8 +3707,9 @@ def create_port(self, network_id, **kwargs): return self.manager.submitTask( _tasks.PortCreate(body={'port': kwargs}))['port'] - @valid_kwargs('name', 'admin_state_up', 'fixed_ips', 'security_groups', - 'allowed_address_pairs', 'extra_dhcp_opts', 'device_owner') + @_utils.valid_kwargs('name', 'admin_state_up', 'fixed_ips', + 'security_groups', 'allowed_address_pairs', + 'extra_dhcp_opts', 'device_owner') def update_port(self, name_or_id, **kwargs): """Update a port @@ -3904,7 +3877,7 @@ def delete_security_group(self, name_or_id): "Unavailable feature: security groups" ) - @valid_kwargs('name', 'description') + @_utils.valid_kwargs('name', 'description') def update_security_group(self, name_or_id, **kwargs): """Update a security group @@ -4802,7 +4775,7 @@ def purge_node_instance_info(self, uuid): except Exception as e: raise OpenStackCloudException(str(e)) - @valid_kwargs('type', 'service_type', 'description') + @_utils.valid_kwargs('type', 'service_type', 'description') def create_service(self, name, **kwargs): """Create a service. @@ -4916,7 +4889,7 @@ def delete_service(self, name_or_id): msg=str(e))) return True - @valid_kwargs('public_url', 'internal_url', 'admin_url') + @_utils.valid_kwargs('public_url', 'internal_url', 'admin_url') def create_endpoint(self, service_name_or_id, url=None, interface=None, region=None, enabled=True, **kwargs): """Create a Keystone endpoint. diff --git a/shade/_utils.py b/shade/_utils.py index 91c09006a..39ef2bf26 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from decorator import decorator +import inspect import time import netifaces @@ -287,3 +289,29 @@ def normalize_users(users): enabled=user.get('enabled'), ) for user in users ] + + +def valid_kwargs(*valid_args): + # This decorator checks if argument passed as **kwargs to a function are + # present in valid_args. + # + # Typically, valid_kwargs is used when we want to distinguish between + # None and omitted arguments and we still want to validate the argument + # list. + # + # Example usage: + # + # @valid_kwargs('opt_arg1', 'opt_arg2') + # def my_func(self, mandatory_arg1, mandatory_arg2, **kwargs): + # ... + # + @decorator + def func_wrapper(func, *args, **kwargs): + argspec = inspect.getargspec(func) + for k in kwargs: + if k not in argspec.args[1:] and k not in valid_args: + raise TypeError( + "{f}() got an unexpected keyword argument " + "'{arg}'".format(f=inspect.stack()[1][3], arg=k)) + return func(*args, **kwargs) + return func_wrapper From dc45027fec3a932c2063cf1b17d835a4dc7c3fa6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 22 Oct 2015 09:21:29 +0900 Subject: [PATCH 0566/3836] Remove unused parameter from create_stack We cleaned this up a little while ago, but left a bogus param. Change-Id: I4a1031630ba839b802493f8355010b87c1bb4f69 --- shade/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index 8c5821dca..d498a3537 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -794,7 +794,6 @@ def create_stack( template_file=template_file, template_url=template_url, template_object=template_object, - object_request=object_request, files=files) params = dict( stack_name=name, From 8cd3b51728af18e195fdefaca1245d4ff27fca17 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 14 Jun 2015 01:12:47 +0300 Subject: [PATCH 0567/3836] Fix projects list/search/get interface Similar to users, projects was off in neverland. Bring it back in to the fold so that the ansible modules can work sanely. Change-Id: I4c3715f2de91db73b3f06c483de20f108a7d62fe --- shade/__init__.py | 116 +++++++++++++++++-------- shade/_tasks.py | 20 +++++ shade/tests/functional/test_flavor.py | 1 + shade/tests/unit/test_caching.py | 30 +++++-- shade/tests/unit/test_domain_params.py | 8 +- 5 files changed, 128 insertions(+), 47 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index e64a4de39..2b4f36bb8 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -458,15 +458,6 @@ def auth_token(self): # We don't need to track validity here, just get_token() each time. return self.keystone_session.get_token() - @property - def project_cache(self): - return self.get_project_cache() - - @_cache_on_arguments() - def get_project_cache(self): - return {project.id: project for project in - self._project_manager.list()} - @property def _project_manager(self): # Keystone v2 calls this attribute tenants @@ -479,11 +470,13 @@ def _project_manager(self): def _get_project_param_dict(self, name_or_id): project_dict = dict() if name_or_id: - project_id = self._get_project(name_or_id).id + project = self.get_project(name_or_id) + if not project: + return project_dict if self.cloud_config.get_api_version('identity') == '3': - project_dict['default_project'] = project_id + project_dict['default_project'] = project['id'] else: - project_dict['tenant_id'] = project_id + project_dict['tenant_id'] = project['id'] return project_dict def _get_domain_param_dict(self, domain_id): @@ -513,53 +506,102 @@ def _get_identity_params(self, domain_id=None, project=None): ret.update(self._get_project_param_dict(project)) return ret - def _get_project(self, name_or_id): - """Retrieve a project by name or id.""" - - # TODO(mordred): This, and other keystone operations, need to have - # domain information passed in. When there is no - # available domain information, we should default to - # the currently scoped domain which we can request from - # the session. - for id, project in self.project_cache.items(): - if name_or_id in (id, project.name): - return project - return None + @_cache_on_arguments() + def list_projects(self): + """List Keystone Projects. - def get_project(self, name_or_id): - """Retrieve a project by name or id.""" - project = self._get_project(name_or_id) - if project: - return meta.obj_to_dict(project) - return None + :returns: a list of dicts containing the project description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + try: + projects = self.manager.submitTask(_tasks.ProjectList()) + except Exception as e: + self.log.debug("Failed to list projects", exc_info=True) + raise OpenStackCloudException(str(e)) + return meta.obj_list_to_dict(projects) + + def search_projects(self, name_or_id=None, filters=None): + """Seach Keystone projects. + + :param name: project name or id. + :param filters: a dict containing additional filters to use. + + :returns: a list of dict containing the role description + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + projects = self.list_projects() + return _utils._filter_list(projects, name_or_id, filters) + + def get_project(self, name_or_id, filters=None): + """Get exactly one Keystone project. + + :param id: project name or id. + :param filters: a dict containing additional filters to use. + + :returns: a list of dicts containing the project description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + return _utils._get_entity(self.search_projects, name_or_id, filters) def update_project(self, name_or_id, description=None, enabled=True): try: - project = self._get_project(name_or_id) - return meta.obj_to_dict( - project.update(description=description, enabled=enabled)) + proj = self.get_project(name_or_id) + if not proj: + raise OpenStackCloudException( + "Project %s not found." % name_or_id) + + params = {} + if self.api_versions['identity'] == '3': + params['project'] = proj['id'] + else: + params['tenant_id'] = proj['id'] + + project = self.manager.submitTask(_tasks.ProjectUpdate( + description=description, + enabled=enabled, + **params)) except Exception as e: raise OpenStackCloudException( "Error in updating project {project}: {message}".format( project=name_or_id, message=str(e))) + self.list_projects.invalidate() + return meta.obj_to_dict(project) def create_project( self, name, description=None, domain_id=None, enabled=True): """Create a project.""" try: - domain_params = self._get_domain_param_dict(domain_id) - self._project_manager.create( + params = self._get_domain_param_dict(domain) + if self.api_versions['identity'] == '3': + params['name'] = name + else: + params['tenant_name'] = name + + project = self.manager.submitTask(_tasks.ProjectCreate( project_name=name, description=description, enabled=enabled, - **domain_params) + **params)) except Exception as e: raise OpenStackCloudException( "Error in creating project {project}: {message}".format( project=name, message=str(e))) + self.list_projects.invalidate() + return meta.obj_to_dict(project) def delete_project(self, name_or_id): try: project = self.update_project(name_or_id, enabled=False) - self._project_manager.delete(project.id) + params = {} + if self.api_versions['identity'] == '3': + params['project'] = project['id'] + else: + params['tenant'] = project['id'] + self.manager.submitTask(_tasks.ProjectDelete(**params)) except Exception as e: raise OpenStackCloudException( "Error in deleting project {project}: {message}".format( diff --git a/shade/_tasks.py b/shade/_tasks.py index 1873064c4..470183015 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -42,6 +42,26 @@ def main(self, client): return client.keystone_client.users.get(**self.args) +class ProjectList(task_manager.Task): + def main(self, client): + return client._project_manager.list() + + +class ProjectCreate(task_manager.Task): + def main(self, client): + return client._project_manager.create(**self.args) + + +class ProjectDelete(task_manager.Task): + def main(self, client): + return client._project_manager.delete(**self.args) + + +class ProjectUpdate(task_manager.Task): + def main(self, client): + return client._project_manager.update(**self.args) + + class FlavorList(task_manager.Task): def main(self, client): return client.nova_client.flavors.list(**self.args) diff --git a/shade/tests/functional/test_flavor.py b/shade/tests/functional/test_flavor.py index 573485b52..123b3c850 100644 --- a/shade/tests/functional/test_flavor.py +++ b/shade/tests/functional/test_flavor.py @@ -110,6 +110,7 @@ def test_flavor_access(self): # We need the tenant ID for the 'demo' user project = self.operator_cloud.get_project('demo') + self.assertIsNotNone(project) # Now give 'demo' access self.operator_cloud.add_flavor_access(new_flavor['id'], project['id']) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index c78e41560..90773d09e 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -69,18 +69,36 @@ def test_openstack_cloud(self): self.assertIsInstance(self.cloud, shade.OpenStackCloud) @mock.patch('shade.OpenStackCloud.keystone_client') - def test_project_cache(self, keystone_mock): + def test_list_projects_v3(self, keystone_mock): + project = fakes.FakeProject('project_a') + keystone_mock.projects.list.return_value = [project] + self.cloud.cloud_config.config['identity_api_version'] = '3' + self.assertEqual( + meta.obj_list_to_dict([project]), self.cloud.list_projects()) + project_b = fakes.FakeProject('project_b') + keystone_mock.projects.list.return_value = [project, project_b] + self.assertEqual( + meta.obj_list_to_dict([project]), self.cloud.list_projects()) + self.cloud.list_projects.invalidate(self.cloud) + self.assertEqual( + meta.obj_list_to_dict([project, project_b]), + self.cloud.list_projects()) + + @mock.patch('shade.OpenStackCloud.keystone_client') + def test_list_projects_v2(self, keystone_mock): project = fakes.FakeProject('project_a') keystone_mock.tenants.list.return_value = [project] - self.assertEqual({'project_a': project}, self.cloud.project_cache) + self.cloud.cloud_config.config['identity_api_version'] = '2' + self.assertEqual( + meta.obj_list_to_dict([project]), self.cloud.list_projects()) project_b = fakes.FakeProject('project_b') keystone_mock.tenants.list.return_value = [project, project_b] self.assertEqual( - {'project_a': project}, self.cloud.project_cache) - self.cloud.get_project_cache.invalidate(self.cloud) + meta.obj_list_to_dict([project]), self.cloud.list_projects()) + self.cloud.list_projects.invalidate(self.cloud) self.assertEqual( - {'project_a': project, - 'project_b': project_b}, self.cloud.project_cache) + meta.obj_list_to_dict([project, project_b]), + self.cloud.list_projects()) @mock.patch('shade.OpenStackCloud.cinder_client') def test_list_volumes(self, cinder_mock): diff --git a/shade/tests/unit/test_domain_params.py b/shade/tests/unit/test_domain_params.py index fe1c7da17..83e5960c3 100644 --- a/shade/tests/unit/test_domain_params.py +++ b/shade/tests/unit/test_domain_params.py @@ -29,7 +29,7 @@ def setUp(self): self.cloud = shade.openstack_cloud(validate=False) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, '_get_project') + @mock.patch.object(shade.OpenStackCloud, 'get_project') def test_identity_params_v3(self, mock_get_project, mock_api_version): mock_get_project.return_value = munch.Munch(id=1234) mock_api_version.return_value = '3' @@ -41,7 +41,7 @@ def test_identity_params_v3(self, mock_get_project, mock_api_version): self.assertEqual(ret['domain'], '5678') @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, '_get_project') + @mock.patch.object(shade.OpenStackCloud, 'get_project') def test_identity_params_v3_no_domain( self, mock_get_project, mock_api_version): mock_get_project.return_value = munch.Munch(id=1234) @@ -53,7 +53,7 @@ def test_identity_params_v3_no_domain( domain_id=None, project='bar') @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, '_get_project') + @mock.patch.object(shade.OpenStackCloud, 'get_project') def test_identity_params_v2(self, mock_get_project, mock_api_version): mock_get_project.return_value = munch.Munch(id=1234) mock_api_version.return_value = '2' @@ -63,7 +63,7 @@ def test_identity_params_v2(self, mock_get_project, mock_api_version): self.assertEqual(ret['tenant_id'], 1234) self.assertNotIn('domain', ret) - @mock.patch.object(shade.OpenStackCloud, '_get_project') + @mock.patch.object(shade.OpenStackCloud, 'get_project') def test_identity_params_v2_no_domain(self, mock_get_project): mock_get_project.return_value = munch.Munch(id=1234) From bf792d7ff40ed976df3e0c3012bbaa2f28b9cf8c Mon Sep 17 00:00:00 2001 From: Lars Kellogg-Stedman Date: Thu, 22 Oct 2015 10:54:52 -0400 Subject: [PATCH 0568/3836] handle routers without an external gateway in list_router_interfaces list_router_interfaces would fail if called on a router with no external gateway: File "/home/lars/.ansible/tmp/ansible-tmp-1445525437.55-149961620412652/os_router", line 343, in main ports = cloud.list_router_interfaces(router, 'internal') File "/usr/lib/python2.7/site-packages/shade/__init__.py", line 1922, in list_router_interfaces ext_fixed = router['external_gateway_info']['external_fixed_ips'] TypeError: 'NoneType' object has no attribute '__getitem__' This commit defaults `ext_fixed` to an empty list if there is no external gateway. Change-Id: Idbcd5f005ebef47e11377e83a60d2adea7c3380b --- shade/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index b2d69e2dd..4f4b47252 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1919,7 +1919,9 @@ def list_router_interfaces(self, router, interface_type=None): if interface_type: filtered_ports = [] - ext_fixed = router['external_gateway_info']['external_fixed_ips'] + ext_fixed = (router['external_gateway_info']['external_fixed_ips'] + if router['external_gateway_info'] + else []) # Compare the subnets (subnet_id, ip_address) on the ports with # the subnets making up the router external gateway. Those ports From fa8d0186608747e36f37552084240ad726653635 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 23 Oct 2015 09:51:05 +0900 Subject: [PATCH 0569/3836] Add entry for James Blair to .mailmap Jim shows up twice in the AUTHORS file and that's silly. I mean, he's clearly worth two people - but maybe not two AUTHORS file entries yet. Change-Id: I4abe95250501391b0d549ae7c0b05dd5ab0d5f76 --- .mailmap | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.mailmap b/.mailmap index cc92f17b8..4d8361ef0 100644 --- a/.mailmap +++ b/.mailmap @@ -1,3 +1,4 @@ # Format is: # -# \ No newline at end of file +# + From aed20265f8bd65cb011bb1c1232e09f14ee215ed Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 23 Oct 2015 13:08:41 +0900 Subject: [PATCH 0570/3836] Change the fallback on server wait to 2 seconds The original iterate_timeout default was 2 seconds - but the introduction of per-service cache timing support falls back to 0 - which is pretty extreme. Set the fallback to 2. Change-Id: I0163e3a3397e5a7bca22bd4bbaaca63509a072b6 --- shade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index 4f4b47252..896a34217 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -271,7 +271,7 @@ def invalidate(self): # Don't cache list_servers if we're not caching things. # Replace this with a more specific cache configuration # soon. - self._SERVER_LIST_AGE = 0 + self._SERVER_LIST_AGE = 2 self._cache = _FakeCache() # Undecorate cache decorated methods. Otherwise the call stacks # wind up being stupidly long and hard to debug From c0f0f36fd854d14c42878879b128675c25dcb256 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 23 Oct 2015 13:09:31 +0900 Subject: [PATCH 0571/3836] Add debug logging to iterate timeout Output a debug message in each iterate timeout loop that we're waiting. It's entirely possible that this will become noise, but it's also silencable by setting a null handler for shade._utils. Change-Id: I908040e4bfaaa0abd3eeb254af3bfea2fc40626c --- shade/_utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/shade/_utils.py b/shade/_utils.py index 39ef2bf26..8f053aeb9 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -18,8 +18,11 @@ import netifaces +from shade import _log from shade import exc +log = _log.setup_logging(__name__) + def _iterate_timeout(timeout, message, wait=2): """Iterate and raise an exception on timeout. @@ -35,6 +38,7 @@ def _iterate_timeout(timeout, message, wait=2): while (timeout is None) or (time.time() < start + timeout): count += 1 yield count + log.debug('Waiting {wait} seconds'.format(wait=wait)) time.sleep(wait) raise exc.OpenStackCloudTimeout(message) From 139a2b521c8cc2de57c0d95308e389a2e17bd898 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 24 Oct 2015 03:59:46 +0900 Subject: [PATCH 0572/3836] Translate task name in log message always We translated it for 'running' but not for 'ran' which made output very strange. Change-Id: Idfef7fdf65798b010a52669ed5d6759878b089ff --- shade/task_manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shade/task_manager.py b/shade/task_manager.py index f2b9e5704..f26739eda 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -100,5 +100,6 @@ def submitTask(self, task): task.run(self._client) end = time.time() self.log.debug( - "Manager %s ran task %s in %ss" % (self.name, task, (end - start))) + "Manager %s ran task %s in %ss" % ( + self.name, type(task).__name__, (end - start))) return task.wait() From 7f5ae9518850f57d2bd803f78d9e8a5f3bd7b83f Mon Sep 17 00:00:00 2001 From: Caleb Boylan Date: Fri, 23 Oct 2015 11:39:27 -0700 Subject: [PATCH 0573/3836] Add swift object and container list functionality Change-Id: Id8b5296f6a3277360011874b5292817b63acfbac --- shade/__init__.py | 18 ++++++++++++++++++ shade/_tasks.py | 10 ++++++++++ shade/tests/functional/test_object.py | 3 +++ 3 files changed, 31 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 896a34217..b7afabebb 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -3262,6 +3262,15 @@ def _delete_server( raise OpenStackCloudException( "Error in deleting server: {0}".format(e)) + def list_containers(self): + try: + return meta.obj_to_dict(self.manager.submitTask( + _tasks.ContainerList())) + except swift_exceptions.ClientException as e: + raise OpenStackCloudException( + "Container list failed: %s (%s/%s)" % ( + e.http_reason, e.http_host, e.http_path)) + def get_container(self, name, skip_cache=False): if skip_cache or name not in self._container_cache: try: @@ -3452,6 +3461,15 @@ def create_object( raise OpenStackCloudException( 'Failed at action ({action}) [{error}]:'.format(**r)) + def list_objects(self, container): + try: + return meta.obj_to_dict(self.manager.submitTask( + _tasks.ObjectList(container))) + except swift_exceptions.ClientException as e: + raise OpenStackCloudException( + "Object list failed: %s (%s/%s)" % ( + e.http_reason, e.http_host, e.http_path)) + def delete_object(self, container, name): if not self.get_object_metadata(container, name): return diff --git a/shade/_tasks.py b/shade/_tasks.py index 470183015..044e22acb 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -386,6 +386,11 @@ def main(self, client): client.swift_client.post_container(**self.args) +class ContainerList(task_manager.Task): + def main(self, client): + return client.swift_client.list(**self.args) + + class ObjectCapabilities(task_manager.Task): def main(self, client): return client.swift_client.get_capabilities(**self.args) @@ -406,6 +411,11 @@ def main(self, client): client.swift_client.post_object(**self.args) +class ObjectList(task_manager.Task): + def main(self, client): + return client.swift_client.list(**self.args) + + class ObjectMetadata(task_manager.Task): def main(self, client): return client.swift_client.head_object(**self.args) diff --git a/shade/tests/functional/test_object.py b/shade/tests/functional/test_object.py index b642ce780..de83e09e9 100644 --- a/shade/tests/functional/test_object.py +++ b/shade/tests/functional/test_object.py @@ -59,5 +59,8 @@ def test_create_object(self): sparse_file.name)) self.assertIsNotNone( self.cloud.get_object_metadata(container, name)) + self.assertEqual([name], self.cloud.list_objects(container)) self.cloud.delete_object(container, name) + self.assertEmpty(self.cloud.list_objects(container)) + self.assertEqual([container], self.cloud.list_containers()) self.cloud.delete_container(container) From 8f93623e45d3b988bcd54ec53fef148c18cc34c3 Mon Sep 17 00:00:00 2001 From: Lars Kellogg-Stedman Date: Fri, 23 Oct 2015 22:03:14 -0400 Subject: [PATCH 0574/3836] add list_server_security_groups method this adds a new `list_server_security_groups` method that returns a list of security groups associated with a particular server. Internally, it calls `nova_client.servers.list_security_group(server)`. >>> import shade >>> c = shade.openstack_cloud() >>> server = c.get_server(name='cirros') >>> c.list_server_security_groups(server) Change-Id: I63228bed4e1fb65a7a15627fbe1da31cfff2aa0e --- shade/__init__.py | 12 ++++++++++++ shade/_tasks.py | 5 +++++ 2 files changed, 17 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 896a34217..e0b09f16e 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1212,6 +1212,18 @@ def list_stacks(self): raise OpenStackCloudException(str(e)) return meta.obj_list_to_dict(stacks) + def list_server_security_groups(self, server): + """List all security groups associated with the given server. + + :returns: A list of security group dicts. + """ + + groups = meta.obj_list_to_dict( + self.manager.submitTask( + _tasks.ServerListSecurityGroups(server=server['id']))) + + return _utils.normalize_nova_secgroups(groups) + def list_security_groups(self): """List all available security groups. diff --git a/shade/_tasks.py b/shade/_tasks.py index 470183015..43efd8d06 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -101,6 +101,11 @@ def main(self, client): return client.nova_client.servers.list(**self.args) +class ServerListSecurityGroups(task_manager.Task): + def main(self, client): + return client.nova_client.servers.list_security_group(**self.args) + + class ServerGet(task_manager.Task): def main(self, client): return client.nova_client.servers.get(**self.args) From f39cb4b9330307eb363b2e1707f87c9d4d5e8539 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 26 Oct 2015 13:36:13 +0900 Subject: [PATCH 0575/3836] Always pull regions from vendor profiles Regions are special, since they're a driver for creating a cloud config object in the first place. We weren't syncing them from the vendor profile, so if the region wasn't set in the clouds.yaml, region name validation (and get_all_clouds) would fail. Change-Id: I5b8d6807a86b87a7f69f523f2fee2784389fab0a --- os_client_config/config.py | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 2e3644bd0..aa161e474 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -270,7 +270,17 @@ def _get_regions(self, cloud): " parameter in {0} instead.".format(self.config_filename)) return regions else: - return [''] + # crappit. we don't have a region defined. + new_cloud = dict() + our_cloud = self.cloud_config['clouds'].get(cloud, dict()) + self._expand_vendor_profile(cloud, new_cloud, our_cloud) + if 'regions' in new_cloud and new_cloud['regions']: + return new_cloud['regions'] + elif 'region_name' in new_cloud and new_cloud['region_name']: + return [new_cloud['region_name']] + else: + # Wow. We really tried + return [''] def _get_region(self, cloud=None): return self._get_regions(cloud)[0] @@ -291,7 +301,18 @@ def _get_base_cloud_config(self, name): # Get the defaults cloud.update(self.defaults) + self._expand_vendor_profile(name, cloud, our_cloud) + + if 'auth' not in cloud: + cloud['auth'] = dict() + + _auth_update(cloud, our_cloud) + if 'cloud' in cloud: + del cloud['cloud'] + + return self._fix_backwards_madness(cloud) + def _expand_vendor_profile(self, name, cloud, our_cloud): # Expand a profile if it exists. 'cloud' is an old confusing name # for this. profile_name = our_cloud.get('profile', our_cloud.get('cloud', None)) @@ -314,15 +335,6 @@ def _get_base_cloud_config(self, name): " the cloud '{1}'".format(profile_name, name)) - if 'auth' not in cloud: - cloud['auth'] = dict() - - _auth_update(cloud, our_cloud) - if 'cloud' in cloud: - del cloud['cloud'] - - return self._fix_backwards_madness(cloud) - def _fix_backwards_madness(self, cloud): cloud = self._fix_backwards_project(cloud) cloud = self._fix_backwards_auth_plugin(cloud) From 631a8e53e90dcdb5fcc9131d72c3501b0b06a8d8 Mon Sep 17 00:00:00 2001 From: Lars Kellogg-Stedman Date: Fri, 23 Oct 2015 22:09:02 -0400 Subject: [PATCH 0576/3836] expand security groups in get_hostvars_from_server Add a call to `list_server_security_groups` to `meta.get_hostvars_from_server`. This replaces the minimal list of security group names with detailed information that includes security group ids. Change-Id: I5a01b37efd5520205c6ce94f0fdc4c09f75053c2 --- shade/meta.py | 8 ++++++++ shade/tests/unit/test_meta.py | 24 +++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/shade/meta.py b/shade/meta.py index 2523e2876..9b8805653 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -264,6 +264,12 @@ def expand_server_vars(cloud, server): return server_vars +def expand_server_security_groups(cloud, server): + groups = cloud.list_server_security_groups(server) + + server['security_groups'] = groups + + def get_hostvars_from_server(cloud, server, mounts=None): """Expand additional server information useful for ansible inventory.""" server_vars = expand_server_vars(cloud, server) @@ -274,6 +280,8 @@ def get_hostvars_from_server(cloud, server, mounts=None): server_vars['flavor']['name'] = flavor_name server_vars['flavor'].pop('links', None) + expand_server_security_groups(cloud, server) + # OpenStack can return image as a string when you've booted from volume if str(server['image']) == server['image']: image_id = server['image'] diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index fabaecccc..f1c2bf1f6 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -60,6 +60,9 @@ def get_internal_networks(self): def get_external_networks(self): return [] + def list_server_security_groups(self, server): + return [] + class FakeServer(object): id = 'test-id-0' @@ -149,13 +152,15 @@ def test_get_server_private_ip( filters={'router:external': False} ) + @mock.patch.object(shade.OpenStackCloud, 'list_server_security_groups') @mock.patch.object(shade.OpenStackCloud, 'get_image_name') @mock.patch.object(shade.OpenStackCloud, 'get_flavor_name') @mock.patch.object(shade.OpenStackCloud, 'has_service') @mock.patch.object(shade.OpenStackCloud, 'search_networks') def test_get_server_private_ip_devstack( self, mock_search_networks, mock_has_service, - mock_get_flavor_name, mock_get_image_name): + mock_get_flavor_name, mock_get_image_name, + mock_list_server_security_groups): mock_get_image_name.return_value = 'cirros-0.3.4-x86_64-uec' mock_get_flavor_name.return_value = 'm1.tiny' mock_has_service.return_value = True @@ -328,6 +333,23 @@ class obj1(object): self.assertEqual(new_list[0]['value'], 0) self.assertEqual(new_list[1]['value'], 1) + @mock.patch.object(FakeCloud, 'list_server_security_groups') + def test_get_security_groups(self, + mock_list_server_security_groups): + '''This test verifies that calling get_hostvars_froms_server + ultimately calls list_server_security_groups, and that the return + value from list_server_security_groups ends up in + server['security_groups'].''' + mock_list_server_security_groups.return_value = [ + {'name': 'testgroup', 'id': '1'}] + + server = meta.obj_to_dict(FakeServer()) + hostvars = meta.get_hostvars_from_server(FakeCloud(), server) + + mock_list_server_security_groups.assert_called_once_with(server) + self.assertEqual('testgroup', + hostvars['security_groups'][0]['name']) + @mock.patch.object(shade.meta, 'get_server_external_ipv6') @mock.patch.object(shade.meta, 'get_server_external_ipv4') def test_basic_hostvars( From e941434e4ff25153c9661919852316438f8499a8 Mon Sep 17 00:00:00 2001 From: Lars Kellogg-Stedman Date: Fri, 23 Oct 2015 22:11:31 -0400 Subject: [PATCH 0577/3836] return additional detail about servers Introduce a new parameter `detailed` to `list_servers`, `search_servers`, and `get_server`. When True, `list_servers` will call `meta.get_hostvars_from_server` for each server. This does not modify the default behavior (`detailed=False`) of these methods. Change-Id: I919fd2ec20515c2b13eee02a75623c6ce6d7d273 --- shade/__init__.py | 29 ++++++++++++++++++---------- shade/tests/unit/test_shade.py | 35 ++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index bc863d76e..36929ba84 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1049,8 +1049,8 @@ def search_security_groups(self, name_or_id=None, filters=None): groups = self.list_security_groups() return _utils._filter_list(groups, name_or_id, filters) - def search_servers(self, name_or_id=None, filters=None): - servers = self.list_servers() + def search_servers(self, name_or_id=None, filters=None, detailed=False): + servers = self.list_servers(detailed=detailed) return _utils._filter_list(servers, name_or_id, filters) def search_images(self, name_or_id=None, filters=None): @@ -1256,7 +1256,7 @@ def list_security_groups(self): "Unavailable feature: security groups" ) - def list_servers(self): + def list_servers(self, detailed=False): """List all available servers. :returns: A list of server dicts. @@ -1273,17 +1273,24 @@ def list_servers(self): # blocking. if self._servers_lock.acquire(len(self._servers) == 0): try: - self._servers = self._list_servers() + self._servers = self._list_servers(detailed=detailed) self._servers_time = time.time() finally: self._servers_lock.release() return self._servers - def _list_servers(self): + def _list_servers(self, detailed=False): try: - return meta.obj_list_to_dict( - self.manager.submitTask(_tasks.ServerList()) - ) + servers = meta.obj_list_to_dict( + self.manager.submitTask(_tasks.ServerList())) + + if detailed: + return [ + meta.get_hostvars_from_server(self, server) + for server in servers + ] + else: + return servers except Exception as e: raise OpenStackCloudException( "Error fetching server list: %s" % e) @@ -1651,7 +1658,7 @@ def get_security_group(self, name_or_id, filters=None): return _utils._get_entity( self.search_security_groups, name_or_id, filters) - def get_server(self, name_or_id=None, filters=None): + def get_server(self, name_or_id=None, filters=None, detailed=False): """Get a server by name or ID. :param name_or_id: Name or ID of the server. @@ -1670,7 +1677,9 @@ def get_server(self, name_or_id=None, filters=None): found. """ - return _utils._get_entity(self.search_servers, name_or_id, filters) + searchfunc = functools.partial(self.search_servers, + detailed=detailed) + return _utils._get_entity(searchfunc, name_or_id, filters) def get_server_by_id(self, id): return meta.obj_to_dict( diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 87bcae552..3cd61cc91 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -13,6 +13,7 @@ # under the License. import mock +import munch import glanceclient from heatclient import client as heat_client @@ -398,3 +399,37 @@ def test_get_record(self, mock_search): r = self.cloud._get_record('mickey.domain', 'mickey') self.assertIsNotNone(r) self.assertDictEqual(record1, r) + + @mock.patch.object(shade._tasks.ServerList, 'main') + def test_list_servers(self, mock_serverlist): + '''This test verifies that calling list_servers results in a call + to the ServerList task.''' + mock_serverlist.return_value = [ + munch.Munch({'name': 'testserver', + 'id': '1'}) + ] + + r = self.cloud.list_servers() + self.assertEquals(1, len(r)) + self.assertEquals('testserver', r[0]['name']) + + @mock.patch.object(shade._tasks.ServerList, 'main') + @mock.patch('shade.meta.get_hostvars_from_server') + def test_list_servers_detailed(self, + mock_get_hostvars_from_server, + mock_serverlist): + '''This test verifies that when list_servers is called with + `detailed=True` that it calls `get_hostvars_from_server` for each + server in the list.''' + mock_serverlist.return_value = ['server1', 'server2'] + mock_get_hostvars_from_server.side_effect = [ + {'name': 'server1', 'id': '1'}, + {'name': 'server2', 'id': '2'}, + ] + + r = self.cloud.list_servers(detailed=True) + + self.assertEquals(2, len(r)) + self.assertEquals(len(r), mock_get_hostvars_from_server.call_count) + self.assertEquals('server1', r[0]['name']) + self.assertEquals('server2', r[1]['name']) From f8804bd4f5f9ab5a1e83ec5decfaa8474f1258b6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 27 Oct 2015 07:27:09 +0900 Subject: [PATCH 0578/3836] Use assertDictEqual to test dict equality Just using equals is not a consistent test, because dict ordering can change. assertDictEqual tests that the contents of the two dicts are the same. Change-Id: I1b084a3fee91d73d57a6b9e6a2d0ab9c186e5572 --- os_client_config/tests/test_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 01307488a..d225b7c17 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -380,4 +380,4 @@ def test_set_no_default(self): 'interface': 'public', 'auth_type': 'v3password', } - self.assertEqual(expected, result) + self.assertDictEqual(expected, result) From 4ae04265ffc015259a2c3709b03edcfe86953833 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 26 Oct 2015 14:11:55 +0900 Subject: [PATCH 0579/3836] Allow for templated variables in auth_url Some clouds have auth_urls per region, which means putting a static auth_url in the config file isn't going to work. Allow {} substitutions in the auth_url so that other elements of the auth dict can be injected. This will not solve _everything, but it will solve all of the currently identified issues, and should be upwardly open if we need something more complex later. Change-Id: I6107d0669734647cfa0bef118bdf7739949991ad --- os_client_config/config.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/os_client_config/config.py b/os_client_config/config.py index 2e3644bd0..578e5cd8f 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -615,6 +615,13 @@ def get_one_cloud(self, cloud=None, validate=True, if type(config[key]) is not bool: config[key] = get_boolean(config[key]) + # TODO(mordred): Special casing auth_url here. We should + # come back to this betterer later so that it's + # more generalized + if 'auth' in config and 'auth_url' in config['auth']: + config['auth']['auth_url'] = config['auth']['auth_url'].format( + **config) + # NOTE(dtroyer): OSC needs a hook into the auth args before the # plugin is loaded in order to maintain backward- # compatible behaviour From c71ba514a3827f09312e873899a39d4974bd1e4c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 26 Oct 2015 14:16:53 +0900 Subject: [PATCH 0580/3836] Add conoha public cloud Change-Id: I05a716bc54c98390a7cbeb352338a7c6cd7e86c3 --- doc/source/vendor-support.rst | 15 +++++++++++++++ os_client_config/vendors/conoha.yaml | 8 ++++++++ 2 files changed, 23 insertions(+) create mode 100644 os_client_config/vendors/conoha.yaml diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index 5519928a4..21995315e 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -264,3 +264,18 @@ ZH Zurich, CH * Images must be in `raw` format * Images must be uploaded using the Glance Task Interface + +conoha +------ + +https://identity.%(region_name)s.conoha.io/v2.0 + +============== ================ +Region Name Human Name +============== ================ +tyo1 Tokyo, JP +sin1 Singapore +lon1 London, UK +============== ================ + +* Images cannot be uploaded diff --git a/os_client_config/vendors/conoha.yaml b/os_client_config/vendors/conoha.yaml new file mode 100644 index 000000000..1ed4063ec --- /dev/null +++ b/os_client_config/vendors/conoha.yaml @@ -0,0 +1,8 @@ +name: conoha +profile: + auth: + auth_url: https://identity.{region_name}.conoha.io/v2.0 + regions: + - sin1 + - lon1 + - tyo1 From 2aaba84c7c4dd71420b7881358f89ec1e061c11c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 27 Oct 2015 07:08:49 +0900 Subject: [PATCH 0581/3836] Sort vendor list We've got enough of them that the arbitrary sort order was a bit weird. Sort alphabetically. Change-Id: I6d4ec563f03d1b25bad7d8337db90007a562e97c --- doc/source/vendor-support.rst | 240 +++++++++++++++++----------------- 1 file changed, 120 insertions(+), 120 deletions(-) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index 21995315e..d082ba04f 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -24,150 +24,94 @@ These are the default behaviors unless a cloud is configured differently. * Security groups are provided by Neutron * Vendor specific agents are not used -hp --- +auro +---- -https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0 +https://api.auro.io:5000/v2.0 ============== ================ Region Name Human Name ============== ================ -region-a.geo-1 US West -region-b.geo-1 US East +van1 Vancouver, BC ============== ================ -* DNS Service Type is `hpext:dns` -* Image API Version is 1 -* Public IPv4 is provided via NAT with Neutron Floating IP +* Public IPv4 is provided via NAT with Nova Floating IP +* Floating IPs are provided by Nova +* Security groups are provided by Nova -rackspace ---------- +catalyst +-------- -https://identity.api.rackspacecloud.com/v2.0/ +https://api.cloud.catalyst.net.nz:5000/v2.0 ============== ================ Region Name Human Name ============== ================ -DFW Dallas -HKG Hong Kong -IAD Washington, D.C. -LON London -ORD Chicago -SYD Sydney +nz-por-1 Porirua, NZ +nz_wlg_1 Wellington, NZ ============== ================ -* Database Service Type is `rax:database` -* Compute Service Name is `cloudServersOpenStack` -* Images must be in `vhd` format -* Images must be uploaded using the Glance Task Interface -* Floating IPs are not needed -* Public IPv4 is directly routable via static config by Nova -* IPv6 is provided to every server -* Security groups are not supported -* Uploaded Images need properties to not use vendor agent -:vm_mode: hvm -:xenapi_use_agent: False +* Image API Version is 1 +* Images must be in `raw` format -dreamhost +citycloud --------- -https://keystone.dream.io/v2.0 +https://identity1.citycloud.com:5000/v3/ ============== ================ Region Name Human Name ============== ================ -RegionOne Region One +Lon1 London, UK +Sto2 Stockholm, SE +Kna1 Karlskrona, SE ============== ================ -* Images must be in `raw` format +* Identity API Version is 3 * Public IPv4 is provided via NAT with Neutron Floating IP -* IPv6 is provided to every server - -vexxhost --------- - -http://auth.api.thenebulacloud.com:5000/v2.0/ - -============== ================ -Region Name Human Name -============== ================ -ca-ymq-1 Montreal -============== ================ -runabove --------- +conoha +------ -https://auth.runabove.io/v2.0 +https://identity.%(region_name)s.conoha.io/v2.0 ============== ================ Region Name Human Name ============== ================ -SBG-1 Strassbourg, FR -BHS-1 Beauharnois, QC +tyo1 Tokyo, JP +sin1 Singapore +lon1 London, UK ============== ================ -* Floating IPs are not supported +* Images cannot be uploaded -unitedstack +datacentred ----------- -https://identity.api.ustack.com/v3 - -============== ================ -Region Name Human Name -============== ================ -bj1 Beijing -gd1 Guangdong -============== ================ - -* Identity API Version is 3 -* Images must be in `raw` format - -auro ----- - -https://api.auro.io:5000/v2.0 - -============== ================ -Region Name Human Name -============== ================ -van1 Vancouver, BC -============== ================ - -* Public IPv4 is provided via NAT with Nova Floating IP -* Floating IPs are provided by Nova -* Security groups are provided by Nova - -ovh ---- - -https://auth.cloud.ovh.net/v2.0 +https://compute.datacentred.io:5000/v2.0 ============== ================ Region Name Human Name ============== ================ -SBG1 Strassbourg, FR -GRA1 Gravelines, FR +sal01 Manchester, UK ============== ================ -* Images must be in `raw` format -* Floating IPs are not supported +* Image API Version is 1 -citycloud +dreamhost --------- -https://identity1.citycloud.com:5000/v3/ +https://keystone.dream.io/v2.0 ============== ================ Region Name Human Name ============== ================ -Lon1 London, UK -Sto2 Stockholm, SE -Kna1 Karlskrona, SE +RegionOne Region One ============== ================ -* Identity API Version is 3 +* Images must be in `raw` format * Public IPv4 is provided via NAT with Neutron Floating IP +* IPv6 is provided to every server elastx ------ @@ -195,29 +139,21 @@ it-mil1 Milan, IT de-fra1 Frankfurt, DE ============== ================ -ultimum -------- - -https://console.ultimum-cloud.com:5000/v2.0 - -============== ================ -Region Name Human Name -============== ================ -RegionOne Region One -============== ================ - -datacentred ------------ +hp +-- -https://compute.datacentred.io:5000/v2.0 +https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0 ============== ================ Region Name Human Name ============== ================ -sal01 Manchester, UK +region-a.geo-1 US West +region-b.geo-1 US East ============== ================ +* DNS Service Type is `hpext:dns` * Image API Version is 1 +* Public IPv4 is provided via NAT with Neutron Floating IP internap -------- @@ -235,20 +171,62 @@ nyj01 New York, NY * Image API Version is 1 * Floating IPs are not supported -catalyst --------- +ovh +--- -https://api.cloud.catalyst.net.nz:5000/v2.0 +https://auth.cloud.ovh.net/v2.0 ============== ================ Region Name Human Name ============== ================ -nz-por-1 Porirua, NZ -nz_wlg_1 Wellington, NZ +SBG1 Strassbourg, FR +GRA1 Gravelines, FR ============== ================ -* Image API Version is 1 * Images must be in `raw` format +* Floating IPs are not supported + +rackspace +--------- + +https://identity.api.rackspacecloud.com/v2.0/ + +============== ================ +Region Name Human Name +============== ================ +DFW Dallas +HKG Hong Kong +IAD Washington, D.C. +LON London +ORD Chicago +SYD Sydney +============== ================ + +* Database Service Type is `rax:database` +* Compute Service Name is `cloudServersOpenStack` +* Images must be in `vhd` format +* Images must be uploaded using the Glance Task Interface +* Floating IPs are not needed +* Public IPv4 is directly routable via static config by Nova +* IPv6 is provided to every server +* Security groups are not supported +* Uploaded Images need properties to not use vendor agent +:vm_mode: hvm +:xenapi_use_agent: False + +runabove +-------- + +https://auth.runabove.io/v2.0 + +============== ================ +Region Name Human Name +============== ================ +SBG-1 Strassbourg, FR +BHS-1 Beauharnois, QC +============== ================ + +* Floating IPs are not supported switchengines ------------- @@ -265,17 +243,39 @@ ZH Zurich, CH * Images must be in `raw` format * Images must be uploaded using the Glance Task Interface -conoha ------- +ultimum +------- -https://identity.%(region_name)s.conoha.io/v2.0 +https://console.ultimum-cloud.com:5000/v2.0 ============== ================ Region Name Human Name ============== ================ -tyo1 Tokyo, JP -sin1 Singapore -lon1 London, UK +RegionOne Region One ============== ================ -* Images cannot be uploaded +unitedstack +----------- + +https://identity.api.ustack.com/v3 + +============== ================ +Region Name Human Name +============== ================ +bj1 Beijing +gd1 Guangdong +============== ================ + +* Identity API Version is 3 +* Images must be in `raw` format + +vexxhost +-------- + +http://auth.api.thenebulacloud.com:5000/v2.0/ + +============== ================ +Region Name Human Name +============== ================ +ca-ymq-1 Montreal +============== ================ From 0cee2505032bbbb5d976d00c3fc096ec1eb6c679 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 27 Oct 2015 07:11:50 +0900 Subject: [PATCH 0582/3836] Aligned a few words in the docs We say "not supported" when the cloud doens't do something, but there were two places where we used other words. Fix them. Also, make the image properties a blockquote. Change-Id: Id933f7d38d706c6fb3ed9f59ad10c2e1e2d73c43 --- doc/source/vendor-support.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index d082ba04f..3b4a37078 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -83,7 +83,7 @@ sin1 Singapore lon1 London, UK ============== ================ -* Images cannot be uploaded +* Image upload is not supported datacentred ----------- @@ -206,13 +206,13 @@ SYD Sydney * Compute Service Name is `cloudServersOpenStack` * Images must be in `vhd` format * Images must be uploaded using the Glance Task Interface -* Floating IPs are not needed +* Floating IPs are not supported * Public IPv4 is directly routable via static config by Nova * IPv6 is provided to every server * Security groups are not supported -* Uploaded Images need properties to not use vendor agent -:vm_mode: hvm -:xenapi_use_agent: False +* Uploaded Images need properties to not use vendor agent:: + :vm_mode: hvm + :xenapi_use_agent: False runabove -------- From faea2f8abbcba0745087a4555a691439771a0ce8 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 27 Oct 2015 07:37:10 +0900 Subject: [PATCH 0583/3836] Suppress Rackspace SAN warnings again The old warning suppression stopped working because the string from urllib changed. Instead of using that, use the actual warning class in the suppression. Change-Id: Ib06516ffb5b2cc272d597e7eac959dd39e27c3a4 --- shade/__init__.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index bc863d76e..8c2622648 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -51,7 +51,16 @@ # Disable the Rackspace warnings about deprecated certificates. We are aware import warnings -warnings.filterwarnings('ignore', 'Certificate has no `subjectAltName`') +try: + from requests.packages.urllib3.exceptions import SubjectAltNameWarning +except ImportError: + try: + from urllib3.exceptions import SubjectAltNameWarning + except ImportError: + SubjectAltNameWarning = None + +if SubjectAltNameWarning: + warnings.filterwarnings('ignore', category=SubjectAltNameWarning) from shade.exc import * # noqa from shade import _log From 2c63d6e7177a4e15ed9d7fd0a251f39df6f545ac Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 27 Oct 2015 07:39:45 +0900 Subject: [PATCH 0584/3836] Add warning suppression for keystoneauth loggers In our simple_logging helper function, add a null logger for keystoneauth base auth plugin to suppress the "no handler found" warning. Change-Id: Ie210e76142a6c3dc731e937e82f12572932f07e0 --- shade/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 8c2622648..60dca2140 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -96,6 +96,8 @@ def simple_logging(debug=False): log = _log.setup_logging('shade') log.addHandler(logging.StreamHandler()) log.setLevel(log_level) + # Suppress warning about keystoneauth loggers + log = _log.setup_logging('keystoneauth.identity.base') def openstack_clouds(config=None, debug=False): From 80f2a21bc05f141ca413ae30bae9f2faad919a6b Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Sun, 23 Aug 2015 19:53:15 -0400 Subject: [PATCH 0585/3836] node_set_provision_state wait/timeout support Addition of wait/timeout support for node_set_provision_state to block the method until the machine has entered the desired state. Combined with this, node_set_provision_state will now return the state of the machine as opposed to just passing through the default from the ironicclient node set provision state call. Change-Id: I3e0bfe9e4612291dc77bd683479f7f99aef50d0a --- shade/__init__.py | 43 ++++++++++--- shade/tests/unit/test_shade_operator.py | 80 ++++++++++++++++++++++++- 2 files changed, 113 insertions(+), 10 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index b3fd5b4b8..e80b2bfdc 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -3741,7 +3741,12 @@ def validate_node(self, uuid): "ironic node %s failed to validate. " "(deploy: %s, power: %s)" % (ifaces.deploy, ifaces.power)) - def node_set_provision_state(self, name_or_id, state, configdrive=None): + def node_set_provision_state(self, + name_or_id, + state, + configdrive=None, + wait=False, + timeout=3600): """Set Node Provision State Enables a user to provision a Machine and optionally define a @@ -3758,19 +3763,39 @@ def node_set_provision_state(self, name_or_id, state, configdrive=None): configuration drive file and post the file contents to the API for deployment. + :param boolean wait: A boolean value, defaulted to false, to control + if the method will wait for the desire end state + to be reached before returning. + :param integer timeout: Integer value, defaulting to 3600 seconds, + representing the amount of time to wait for + the desire end state to be reached. :raises: OpenStackCloudException on operation error. - :returns: Per the API, no value should be returned with a successful - operation. + :returns: Dictonary representing the current state of the machine + upon exit of the method. """ try: - return meta.obj_to_dict( - self.manager.submitTask( - _tasks.MachineSetProvision(node_uuid=name_or_id, - state=state, - configdrive=configdrive)) - ) + machine = self.manager.submitTask( + _tasks.MachineSetProvision(node_uuid=name_or_id, + state=state, + configdrive=configdrive)) + + if wait: + for count in _utils._iterate_timeout( + timeout, + "Timeout waiting for node transition to " + "target state of '%s'" % state): + machine = self.get_machine(name_or_id) + if state in machine['provision_state']: + break + if ("available" in machine['provision_state'] and + "provide" in state): + break + else: + machine = self.get_machine(name_or_id) + return machine + except Exception as e: raise OpenStackCloudException( "Baremetal machine node failed change provision" diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index a456afd2b..077aa266b 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -660,18 +660,96 @@ def test_set_machine_power_reboot_failure(self, mock_client): @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_node_set_provision_state(self, mock_client): + + class active_node_state: + provision_state = "active" + + active_return_value = dict( + provision_state="active") + + mock_client.node.set_provision_state.return_value = None + mock_client.node.get.return_value = active_node_state + node_id = 'node01' + return_value = self.cloud.node_set_provision_state( + node_id, + 'active', + configdrive='http://127.0.0.1/file.iso') + self.assertEqual(active_return_value, return_value) + mock_client.node.set_provision_state.assert_called_with( + node_uuid='node01', + state='active', + configdrive='http://127.0.0.1/file.iso') + self.assertTrue(mock_client.node.get.called) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_node_set_provision_state_wait_timeout(self, mock_client): + class deploying_node_state: + provision_state = "deploying" + + class active_node_state: + provision_state = "active" + + class managable_node_state: + provision_state = "managable" + + class available_node_state: + provision_state = "available" + + active_return_value = dict( + provision_state="active") + mock_client.node.get.return_value = active_node_state mock_client.node.set_provision_state.return_value = None node_id = 'node01' return_value = self.cloud.node_set_provision_state( node_id, 'active', + configdrive='http://127.0.0.1/file.iso', + wait=True) + + self.assertEqual(active_return_value, return_value) + mock_client.node.set_provision_state.assert_called_with( + node_uuid='node01', + state='active', configdrive='http://127.0.0.1/file.iso') - self.assertEqual({}, return_value) + self.assertTrue(mock_client.node.get.called) + mock_client.mock_reset() + mock_client.node.get.return_value = deploying_node_state + self.assertRaises( + shade.OpenStackCloudException, + self.cloud.node_set_provision_state, + node_id, + 'active', + configdrive='http://127.0.0.1/file.iso', + wait=True, + timeout=0.001) + self.assertTrue(mock_client.node.get.called) mock_client.node.set_provision_state.assert_called_with( node_uuid='node01', state='active', configdrive='http://127.0.0.1/file.iso') + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_node_set_provision_state_wait_provide(self, mock_client): + + class managable_node_state: + provision_state = "managable" + + class available_node_state: + provision_state = "available" + + node_provide_return_value = dict( + provision_state="available") + + mock_client.node.get.side_effect = iter([ + managable_node_state, + available_node_state]) + return_value = self.cloud.node_set_provision_state( + 'test_node', + 'provide', + wait=True) + self.assertEqual(mock_client.node.get.call_count, 2) + self.assertDictEqual(node_provide_return_value, return_value) + @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_activate_node(self, mock_client): mock_client.node.set_provision_state.return_value = None From 12e78681bc7d488228ba84b19982f27a9627aea3 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Fri, 2 Oct 2015 14:59:19 -0400 Subject: [PATCH 0586/3836] Add logic to support baremetal inspection Ironic supports an inspect verb which directs ironic to boot the node into a special firmware image in order to collect properties from the remote node. Change-Id: I4ba3f0cf4ad6f5466e52f5d034bb217f06eec7dd --- shade/__init__.py | 68 +++++++++ shade/tests/unit/test_shade_operator.py | 174 ++++++++++++++++++++++++ 2 files changed, 242 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index e80b2bfdc..4ab8e7c3b 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -3372,6 +3372,74 @@ def get_machine_by_mac(self, mac): except ironic_exceptions.ClientException: return None + def inspect_machine(self, name_or_id, wait=False, timeout=3600): + """Inspect a Barmetal machine + + Engages the Ironic node inspection behavior in order to collect + metadata about the baremetal machine. + + :param name_or_id: String representing machine name or UUID value in + order to identify the machine. + + :param wait: Boolean value controlling if the method is to wait for + the desired state to be reached or a failure to occur. + + :param timeout: Integer value, defautling to 3600 seconds, for the$ + wait state to reach completion. + + :returns: Dictonary representing the current state of the machine + upon exit of the method. + """ + + return_to_available = False + + machine = self.get_machine(name_or_id) + if not machine: + raise OpenStackCloudException( + "Machine inspection failed to find: %s." % name_or_id) + + # NOTE(TheJulia): If in available state, we can do this, however + # We need to to move the host back to m + if "available" in machine['provision_state']: + return_to_available = True + # NOTE(TheJulia): Changing available machine to managedable state + # and due to state transitions we need to until that transition has + # completd. + self.node_set_provision_state(machine['uuid'], 'manage', + wait=True, timeout=timeout) + elif ("manage" not in machine['provision_state'] and + "inspect failed" not in machine['provision_state']): + raise OpenStackCloudException( + "Machine must be in 'manage' or 'available' state to " + "engage inspection: Machine: %s State: %s" + % (machine['uuid'], machine['provision_state'])) + try: + machine = self.node_set_provision_state(machine['uuid'], 'inspect') + if wait: + for count in _utils._iterate_timeout( + timeout, + "Timeout waiting for node transition to " + "target state of 'inpection'"): + machine = self.get_machine(name_or_id) + + if "inspect failed" in machine['provision_state']: + raise OpenStackCloudException( + "Inspection of node %s failed, last error: %s" + % (machine['uuid'], machine['last_error'])) + + if "manageable" in machine['provision_state']: + break + + if return_to_available: + machine = self.node_set_provision_state( + machine['uuid'], 'provide', wait=wait, timeout=timeout) + + return(machine) + + except Exception as e: + raise OpenStackCloudException( + "Error inspecting machine: %s" % e) + def register_machine(self, nics, wait=False, timeout=3600, lock_timeout=600, **kwargs): """Register Baremetal with Ironic diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index 077aa266b..1aeef011d 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -363,6 +363,180 @@ class client_return_value: '00000000-0000-0000-0000-000000000000', expected_patch) + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_inspect_machine_fail_active(self, mock_client): + + machine_uuid = '00000000-0000-0000-0000-000000000000' + + class active_machine: + uuid = machine_uuid + provision_state = "active" + + mock_client.node.get.return_value = active_machine + self.assertRaises( + shade.OpenStackCloudException, + self.cloud.inspect_machine, + machine_uuid, + wait=True, + timeout=0.001) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_inspect_machine_failed(self, mock_client): + + machine_uuid = '00000000-0000-0000-0000-000000000000' + + class inspect_failed_machine: + uuid = machine_uuid + provision_state = "inspect failed" + last_error = "kaboom" + + mock_client.node.get.return_value = inspect_failed_machine + self.cloud.inspect_machine(machine_uuid) + self.assertTrue(mock_client.node.set_provision_state.called) + self.assertEqual( + mock_client.node.set_provision_state.call_count, 1) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_inspect_machine_managable(self, mock_client): + + machine_uuid = '00000000-0000-0000-0000-000000000000' + + class manageable_machine: + uuid = machine_uuid + provision_state = "manageable" + + mock_client.node.get.return_value = manageable_machine + self.cloud.inspect_machine(machine_uuid) + self.assertEqual( + mock_client.node.set_provision_state.call_count, 1) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_inspect_machine_available(self, mock_client): + + machine_uuid = '00000000-0000-0000-0000-000000000000' + + class available_machine: + uuid = machine_uuid + provision_state = "available" + + class manageable_machine: + uuid = machine_uuid + provision_state = "manageable" + + class inspecting_machine: + uuid = machine_uuid + provision_state = "inspecting" + + mock_client.node.get.side_effect = iter([ + available_machine, + available_machine, + manageable_machine, + manageable_machine, + inspecting_machine]) + self.cloud.inspect_machine(machine_uuid) + self.assertTrue(mock_client.node.set_provision_state.called) + self.assertEqual( + mock_client.node.set_provision_state.call_count, 3) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_inspect_machine_available_wait(self, mock_client): + + machine_uuid = '00000000-0000-0000-0000-000000000000' + + class available_machine: + uuid = machine_uuid + provision_state = "available" + + class manageable_machine: + uuid = machine_uuid + provision_state = "manageable" + + class inspecting_machine: + uuid = machine_uuid + provision_state = "inspecting" + + mock_client.node.get.side_effect = iter([ + available_machine, + available_machine, + manageable_machine, + inspecting_machine, + manageable_machine, + available_machine, + available_machine]) + expected_return_value = dict( + uuid=machine_uuid, + provision_state="available" + ) + + return_value = self.cloud.inspect_machine( + machine_uuid, wait=True, timeout=0.001) + self.assertTrue(mock_client.node.set_provision_state.called) + self.assertEqual( + mock_client.node.set_provision_state.call_count, 3) + self.assertDictEqual(expected_return_value, return_value) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_inspect_machine_wait(self, mock_client): + + machine_uuid = '00000000-0000-0000-0000-000000000000' + + class manageable_machine: + uuid = machine_uuid + provision_state = "manageable" + + class inspecting_machine: + uuid = machine_uuid + provision_state = "inspecting" + + expected_return_value = dict( + uuid=machine_uuid, + provision_state="manageable" + ) + mock_client.node.get.side_effect = iter([ + manageable_machine, + inspecting_machine, + inspecting_machine, + manageable_machine, + manageable_machine]) + + return_value = self.cloud.inspect_machine( + machine_uuid, wait=True, timeout=0.001) + self.assertDictEqual(expected_return_value, return_value) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_inspect_machine_inspect_failed(self, mock_client): + + machine_uuid = '00000000-0000-0000-0000-000000000000' + + class manageable_machine: + uuid = machine_uuid + provision_state = "manageable" + last_error = None + + class inspecting_machine: + uuid = machine_uuid + provision_state = "inspecting" + last_error = None + + class inspect_failed_machine: + uuid = machine_uuid + provision_state = "inspect failed" + last_error = "kaboom" + + mock_client.node.get.side_effect = iter([ + manageable_machine, + inspecting_machine, + inspect_failed_machine]) + self.assertRaises( + shade.OpenStackCloudException, + self.cloud.inspect_machine, + machine_uuid, + wait=True, + timeout=0.001) + self.assertEqual( + mock_client.node.set_provision_state.call_count, 1) + self.assertEqual(mock_client.node.get.call_count, 3) + @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_register_machine(self, mock_client): class fake_node: From f826d93ad0e9f7a92e37afaba1feb93d6721125b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 30 Oct 2015 11:38:26 +0900 Subject: [PATCH 0587/3836] Consume /etc/openstack/clouds.yaml devstack write out clouds.yaml to /etc. Use that, rather than ~jenkins. Also, remove the jenkins one since we edit the file. Change-Id: Ie89fb49e760855c5eec46e44ea2f947c4852b949 --- shade/tests/functional/hooks/post_test_hook.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/shade/tests/functional/hooks/post_test_hook.sh b/shade/tests/functional/hooks/post_test_hook.sh index df9557451..a0e064ebb 100755 --- a/shade/tests/functional/hooks/post_test_hook.sh +++ b/shade/tests/functional/hooks/post_test_hook.sh @@ -17,15 +17,16 @@ export SHADE_DIR="$BASE/new/shade" cd $SHADE_DIR sudo chown -R jenkins:stack $SHADE_DIR -CLOUDS_YAML=~jenkins/.config/openstack/clouds.yaml +sudo rm -f ~jenkins/.config/openstack/clouds.yaml +CLOUDS_YAML=/etc/openstack/clouds.yaml # Devstack runs both keystone v2 and v3. An environment variable is set # within the shade keystone v2 job that tells us which version we should # test against. if [ ${SHADE_USE_KEYSTONE_V2:-0} -eq 1 ] then - sed -ie "s/identity_api_version: '3'/identity_api_version: '2.0'/g" $CLOUDS_YAML - sed -ie '/^.*domain_id.*$/d' $CLOUDS_YAML + sudo sed -ie "s/identity_api_version: '3'/identity_api_version: '2.0'/g" $CLOUDS_YAML + sudo sed -ie '/^.*domain_id.*$/d' $CLOUDS_YAML fi echo "Running shade functional test suite" From 96839a5d5067a153bb1289852c5c933ea31230a9 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 30 Oct 2015 11:41:26 +0900 Subject: [PATCH 0588/3836] Remove removal of jenkins clouds.yaml Once devstack stops writing out a jenkins clouds.yaml, we can stop deleting it. Change-Id: Ifab8af4ca08851be68bfaa00525938e15e6b5bfb Depends-On: I036704734785958c95d2234917d7b40bd797a375 --- shade/tests/functional/hooks/post_test_hook.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/shade/tests/functional/hooks/post_test_hook.sh b/shade/tests/functional/hooks/post_test_hook.sh index a0e064ebb..35c1c06a7 100755 --- a/shade/tests/functional/hooks/post_test_hook.sh +++ b/shade/tests/functional/hooks/post_test_hook.sh @@ -17,7 +17,6 @@ export SHADE_DIR="$BASE/new/shade" cd $SHADE_DIR sudo chown -R jenkins:stack $SHADE_DIR -sudo rm -f ~jenkins/.config/openstack/clouds.yaml CLOUDS_YAML=/etc/openstack/clouds.yaml # Devstack runs both keystone v2 and v3. An environment variable is set From fedb0357c44921fc3b0141264ab852d8bedbc3cf Mon Sep 17 00:00:00 2001 From: Caleb Boylan Date: Thu, 29 Oct 2015 10:33:56 -0700 Subject: [PATCH 0589/3836] Fix the return values of create and delete volume also fixes the delete function so it uses get_volume() instead of the volume_exists() function Change-Id: I13e27921e105ac752e91f31f0622d6508bd110a2 --- shade/__init__.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 2c32e2802..2fc5332fd 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -2377,12 +2377,14 @@ def create_volume(self, wait=True, timeout=None, **kwargs): continue if volume['status'] == 'available': - return volume + break if volume['status'] == 'error': raise OpenStackCloudException( "Error in creating volume, please check logs") + return volume + def delete_volume(self, name_or_id=None, wait=True, timeout=None): """Delete a volume. @@ -2397,6 +2399,13 @@ def delete_volume(self, name_or_id=None, wait=True, timeout=None): self.list_volumes.invalidate(self) volume = self.get_volume(name_or_id) + if not volume: + self.log.debug( + "Volume {name_or_id} does not exist".format( + name_or_id=name_or_id), + exc_info=True) + return False + try: self.manager.submitTask( _tasks.VolumeDelete(volume=volume['id'])) @@ -2409,8 +2418,11 @@ def delete_volume(self, name_or_id=None, wait=True, timeout=None): for count in _utils._iterate_timeout( timeout, "Timeout waiting for the volume to be deleted."): - if not self.volume_exists(volume['id']): - return + + if not self.get_volume(volume['id']): + break + + return True def get_volumes(self, server, cache=True): volumes = [] From fff354e64feed6ebd10a2684d944b143608a8645 Mon Sep 17 00:00:00 2001 From: Caleb Boylan Date: Sat, 24 Oct 2015 07:05:04 -0700 Subject: [PATCH 0590/3836] Adds volume snapshot functionality to shade Adds volume snapshot create, delete, get, and list functions Change-Id: Ibe540f3b1932f1a703c98e580f98d0c7091c9622 --- shade/__init__.py | 159 ++++++++++++++++++ shade/_tasks.py | 20 +++ shade/tests/fakes.py | 8 + shade/tests/functional/test_volume.py | 66 ++++++++ .../tests/unit/test_create_volume_snapshot.py | 120 +++++++++++++ .../tests/unit/test_delete_volume_snapshot.py | 94 +++++++++++ 6 files changed, 467 insertions(+) create mode 100644 shade/tests/functional/test_volume.py create mode 100644 shade/tests/unit/test_create_volume_snapshot.py create mode 100644 shade/tests/unit/test_delete_volume_snapshot.py diff --git a/shade/__init__.py b/shade/__init__.py index bc863d76e..9c38f4fce 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1041,6 +1041,11 @@ def search_volumes(self, name_or_id=None, filters=None): return _utils._filter_list( volumes, name_or_id, filters, name_key='display_name') + def search_volume_snapshots(self, name_or_id=None, filters=None): + volumesnapshots = self.list_volume_snapshots() + return _utils._filter_list( + volumesnapshots, name_or_id, filters, name_key='display_name') + def search_flavors(self, name_or_id=None, filters=None): flavors = self.list_flavors() return _utils._filter_list(flavors, name_or_id, filters) @@ -2545,6 +2550,160 @@ def attach_volume(self, server, volume, device=None, ) return vol + def create_volume_snapshot(self, volume_id, force=False, + display_name=None, display_description=None, + wait=True, timeout=None): + """Create a volume. + + :param volume_id: the id of the volume to snapshot. + :param force: If set to True the snapshot will be created even if the + volume is attached to an instance, if False it will not + :param display_name: name of the snapshot, one will be generated if + one is not provided + :param display_description: description of the snapshot, one will be + one is not provided + :param wait: If true, waits for volume snapshot to be created. + :param timeout: Seconds to wait for volume snapshot creation. None is + forever. + + :returns: The created volume object. + + :raises: OpenStackCloudTimeout if wait time exceeded. + :raises: OpenStackCloudException on operation error. + """ + try: + snapshot = self.manager.submitTask( + _tasks.VolumeSnapshotCreate( + volume_id=volume_id, force=force, + display_name=display_name, + display_description=display_description) + ) + + except Exception as e: + raise OpenStackCloudException( + "Error creating snapshot of volume %s: %s" % (volume_id, e) + ) + + snapshot = meta.obj_to_dict(snapshot) + + if wait: + snapshot_id = snapshot['id'] + for count in _utils._iterate_timeout( + timeout, + "Timeout waiting for the volume snapshot to be available." + ): + snapshot = self.get_volume_snapshot_by_id(snapshot_id) + + if snapshot['status'] == 'available': + break + + if snapshot['status'] == 'error': + raise OpenStackCloudException( + "Error in creating volume, please check logs") + + return snapshot + + def get_volume_snapshot_by_id(self, snapshot_id): + """Takes a snapshot_id and gets a dict of the snapshot + that maches that id. + + Note: This is more efficient than get_volume_snapshot. + + param: snapshot_id: ID of the volume snapshot. + + """ + try: + snapshot = self.manager.submitTask( + _tasks.VolumeSnapshotGet( + snapshot_id=snapshot_id + ) + ) + + except Exception as e: + raise OpenStackCloudException( + "Error getting snapshot %s: %s" % (snapshot_id, e) + ) + + return meta.obj_to_dict(snapshot) + + def get_volume_snapshot(self, name_or_id, filters=None): + """Get a volume by name or ID. + + :param name_or_id: Name or ID of the volume snapshot. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + :returns: A volume dict or None if no matching volume is + found. + + """ + return _utils._get_entity(self.search_volume_snapshots, name_or_id, + filters) + + def list_volume_snapshots(self, detailed=True, search_opts=None): + """List all volume snapshots. + + :returns: A list of volume snapshots dicts. + + """ + try: + return meta.obj_list_to_dict( + self.manager.submitTask( + _tasks.VolumeSnapshotList(detailed=detailed, + search_opts=search_opts) + ) + ) + + except Exception as e: + raise OpenStackCloudException( + "Error getting a list of snapshots: %s" % e + ) + + def delete_volume_snapshot(self, name_or_id=None, wait=False, + timeout=None): + """Delete a volume snapshot. + + :param name_or_id: Name or unique ID of the volume snapshot. + :param wait: If true, waits for volume snapshot to be deleted. + :param timeout: Seconds to wait for volume snapshot deletion. None is + forever. + + :raises: OpenStackCloudTimeout if wait time exceeded. + :raises: OpenStackCloudException on operation error. + """ + + volumesnapshot = self.get_volume_snapshot(name_or_id) + + if not volumesnapshot: + return False + + try: + self.manager.submitTask( + _tasks.VolumeSnapshotDelete( + snapshot=volumesnapshot['id'] + ) + ) + except Exception as e: + raise OpenStackCloudException( + "Error in deleting volume snapshot: %s" % str(e)) + + if wait: + for count in _utils._iterate_timeout( + timeout, + "Timeout waiting for the volume snapshot to be deleted."): + if not self.get_volume_snapshot(volumesnapshot['id']): + break + + return True + def get_server_id(self, name_or_id): server = self.get_server(name_or_id) if server: diff --git a/shade/_tasks.py b/shade/_tasks.py index a25ab9c1f..77ac1ac56 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -261,6 +261,26 @@ def main(self, client): return client.nova_client.volumes.create_server_volume(**self.args) +class VolumeSnapshotCreate(task_manager.Task): + def main(self, client): + return client.cinder_client.volume_snapshots.create(**self.args) + + +class VolumeSnapshotGet(task_manager.Task): + def main(self, client): + return client.cinder_client.volume_snapshots.get(**self.args) + + +class VolumeSnapshotList(task_manager.Task): + def main(self, client): + return client.cinder_client.volume_snapshots.list(**self.args) + + +class VolumeSnapshotDelete(task_manager.Task): + def main(self, client): + return client.cinder_client.volume_snapshots.delete(**self.args) + + class NeutronSecurityGroupList(task_manager.Task): def main(self, client): return client.neutron_client.list_security_groups() diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 726d22a6f..46b92142c 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -101,6 +101,14 @@ def __init__(self, id, status, display_name): self.display_name = display_name +class FakeVolumeSnapshot(object): + def __init__(self, id, status, display_name, display_description): + self.id = id + self.status = status + self.display_name = display_name + self.display_description = display_description + + class FakeMachine(object): def __init__(self, id, name=None, driver=None, driver_info=None, chassis_uuid=None, instance_info=None, instance_uuid=None, diff --git a/shade/tests/functional/test_volume.py b/shade/tests/functional/test_volume.py new file mode 100644 index 000000000..eff06041f --- /dev/null +++ b/shade/tests/functional/test_volume.py @@ -0,0 +1,66 @@ +# -*- 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. + +""" +test_volume +---------------------------------- + +Functional tests for `shade` block storage methods. +""" + +from testtools import content + +from shade import openstack_cloud +from shade.tests import base + + +class TestVolume(base.TestCase): + + def setUp(self): + super(TestVolume, self).setUp() + self.cloud = openstack_cloud(cloud='devstack') + if not self.cloud.has_service('volume'): + self.skipTest('volume service not supported by cloud') + + def test_volumes(self): + '''Test volume and snapshot functionality''' + volume_name = self.getUniqueString() + snapshot_name = self.getUniqueString() + self.addDetail('volume', content.text_content(volume_name)) + self.addCleanup(self.cleanup, volume_name, snapshot_name) + volume = self.cloud.create_volume(display_name=volume_name, size=1) + snapshot = self.cloud.create_volume_snapshot( + volume['id'], + display_name=snapshot_name + ) + + self.assertEqual([volume], self.cloud.list_volumes()) + self.assertEqual([snapshot], self.cloud.list_volume_snapshots()) + self.assertEqual(snapshot, + self.cloud.get_volume_snapshot( + snapshot['display_name'])) + self.assertEqual(snapshot, + self.cloud.get_volume_snapshot_by_id(snapshot['id'])) + + self.cloud.delete_volume_snapshot(snapshot_name, wait=True) + self.cloud.delete_volume(volume_name) + + def cleanup(self, volume_name, snapshot_name): + volume = self.cloud.get_volume(volume_name) + snapshot = self.cloud.get_volume_snapshot(snapshot_name) + if volume: + self.cloud.delete_volume(volume_name) + + if snapshot: + self.cloud.delete_volume_snapshot(snapshot_name) diff --git a/shade/tests/unit/test_create_volume_snapshot.py b/shade/tests/unit/test_create_volume_snapshot.py new file mode 100644 index 000000000..8791f7533 --- /dev/null +++ b/shade/tests/unit/test_create_volume_snapshot.py @@ -0,0 +1,120 @@ +# -*- 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. + +""" +test_create_volume_snapshot +---------------------------------- + +Tests for the `create_volume_snapshot` command. +""" + +from mock import patch +import os_client_config +from shade import meta +from shade import OpenStackCloud +from shade.tests import base, fakes +from shade.exc import (OpenStackCloudException, OpenStackCloudTimeout) + + +class TestCreateVolumeSnapshot(base.TestCase): + + def setUp(self): + super(TestCreateVolumeSnapshot, self).setUp() + config = os_client_config.OpenStackConfig() + self.client = OpenStackCloud( + cloud_config=config.get_one_cloud(validate=False)) + + @patch.object(OpenStackCloud, 'cinder_client') + def test_create_volume_snapshot_wait(self, mock_cinder): + """ + Test that create_volume_snapshot with a wait returns the volume + snapshot when its status changes to "available". + """ + build_snapshot = fakes.FakeVolumeSnapshot('1234', 'creating', + 'foo', 'derpysnapshot') + fake_snapshot = fakes.FakeVolumeSnapshot('1234', 'available', + 'foo', 'derpysnapshot') + + mock_cinder.volume_snapshots.create.return_value = build_snapshot + mock_cinder.volume_snapshots.get.return_value = fake_snapshot + mock_cinder.volume_snapshots.list.return_value = [ + build_snapshot, fake_snapshot] + + self.assertEqual( + meta.obj_to_dict(fake_snapshot), + self.client.create_volume_snapshot(volume_id='1234', + wait=True) + ) + + mock_cinder.volume_snapshots.create.assert_called_with( + display_description=None, display_name=None, force=False, + volume_id='1234' + ) + mock_cinder.volume_snapshots.get.assert_called_with( + snapshot_id=meta.obj_to_dict(build_snapshot)['id'] + ) + + @patch.object(OpenStackCloud, 'cinder_client') + def test_create_volume_snapshot_with_timeout(self, mock_cinder): + """ + Test that a timeout while waiting for the volume snapshot to create + raises an exception in create_volume_snapshot. + """ + build_snapshot = fakes.FakeVolumeSnapshot('1234', 'creating', + 'foo', 'derpysnapshot') + + mock_cinder.volume_snapshots.create.return_value = build_snapshot + mock_cinder.volume_snapshots.get.return_value = build_snapshot + mock_cinder.volume_snapshots.list.return_value = [build_snapshot] + + self.assertRaises( + OpenStackCloudTimeout, + self.client.create_volume_snapshot, volume_id='1234', + wait=True, timeout=1) + + mock_cinder.volume_snapshots.create.assert_called_with( + display_description=None, display_name=None, force=False, + volume_id='1234' + ) + mock_cinder.volume_snapshots.get.assert_called_with( + snapshot_id=meta.obj_to_dict(build_snapshot)['id'] + ) + + @patch.object(OpenStackCloud, 'cinder_client') + def test_create_volume_snapshot_with_error(self, mock_cinder): + """ + Test that a error status while waiting for the volume snapshot to + create raises an exception in create_volume_snapshot. + """ + build_snapshot = fakes.FakeVolumeSnapshot('1234', 'creating', + 'bar', 'derpysnapshot') + error_snapshot = fakes.FakeVolumeSnapshot('1234', 'error', + 'blah', 'derpysnapshot') + + mock_cinder.volume_snapshots.create.return_value = build_snapshot + mock_cinder.volume_snapshots.get.return_value = error_snapshot + mock_cinder.volume_snapshots.list.return_value = [error_snapshot] + + self.assertRaises( + OpenStackCloudException, + self.client.create_volume_snapshot, volume_id='1234', + wait=True, timeout=5) + + mock_cinder.volume_snapshots.create.assert_called_with( + display_description=None, display_name=None, force=False, + volume_id='1234' + ) + mock_cinder.volume_snapshots.get.assert_called_with( + snapshot_id=meta.obj_to_dict(build_snapshot)['id'] + ) diff --git a/shade/tests/unit/test_delete_volume_snapshot.py b/shade/tests/unit/test_delete_volume_snapshot.py new file mode 100644 index 000000000..14918d409 --- /dev/null +++ b/shade/tests/unit/test_delete_volume_snapshot.py @@ -0,0 +1,94 @@ +# -*- 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. + +""" +test_delete_volume_snapshot +---------------------------------- + +Tests for the `delete_volume_snapshot` command. +""" + +from mock import patch +import os_client_config +from shade import OpenStackCloud +from shade.tests import base, fakes +from shade.exc import (OpenStackCloudException, OpenStackCloudTimeout) + + +class TestDeleteVolumeSnapshot(base.TestCase): + + def setUp(self): + super(TestDeleteVolumeSnapshot, self).setUp() + config = os_client_config.OpenStackConfig() + self.client = OpenStackCloud( + cloud_config=config.get_one_cloud(validate=False)) + + @patch.object(OpenStackCloud, 'cinder_client') + def test_delete_volume_snapshot(self, mock_cinder): + """ + Test that delete_volume_snapshot without a wait returns True instance + when the volume snapshot deletes. + """ + fake_snapshot = fakes.FakeVolumeSnapshot('1234', 'available', + 'foo', 'derpysnapshot') + + mock_cinder.volume_snapshots.list.return_value = [fake_snapshot] + + self.assertEqual( + True, + self.client.delete_volume_snapshot(name_or_id='1234', wait=False) + ) + + mock_cinder.volume_snapshots.list.assert_called_with(detailed=True, + search_opts=None) + + @patch.object(OpenStackCloud, 'cinder_client') + def test_delete_volume_snapshot_with_error(self, mock_cinder): + """ + Test that a exception while deleting a volume snapshot will cause an + OpenStackCloudException. + """ + fake_snapshot = fakes.FakeVolumeSnapshot('1234', 'available', + 'foo', 'derpysnapshot') + + mock_cinder.volume_snapshots.delete.side_effect = Exception( + "exception") + mock_cinder.volume_snapshots.list.return_value = [fake_snapshot] + + self.assertRaises( + OpenStackCloudException, + self.client.delete_volume_snapshot, name_or_id='1234', + wait=True, timeout=1) + + mock_cinder.volume_snapshots.delete.assert_called_with( + snapshot='1234') + + @patch.object(OpenStackCloud, 'cinder_client') + def test_delete_volume_snapshot_with_timeout(self, mock_cinder): + """ + Test that a timeout while waiting for the volume snapshot to delete + raises an exception in delete_volume_snapshot. + """ + fake_snapshot = fakes.FakeVolumeSnapshot('1234', 'available', + 'foo', 'derpysnapshot') + + mock_cinder.volume_snapshots.list.return_value = [fake_snapshot] + + self.assertRaises( + OpenStackCloudTimeout, + self.client.delete_volume_snapshot, name_or_id='1234', + wait=True, timeout=1) + + mock_cinder.volume_snapshots.list.assert_called_with(detailed=True, + search_opts=None) From 0f6203f303701dee8cdda0fd4df8e3ed810de301 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Sat, 31 Oct 2015 14:25:50 -0400 Subject: [PATCH 0591/3836] Split out OpenStackCloud and OperatorCloud classes Change-Id: If8a09f52313c07d12c7fe0da66f6599de3120979 --- doc/source/usage.rst | 5 +- shade/__init__.py | 5696 +----------------------------- shade/_utils.py | 23 + shade/openstackcloud.py | 4274 ++++++++++++++++++++++ shade/operatorcloud.py | 1438 ++++++++ shade/tests/unit/test_caching.py | 5 +- shade/tests/unit/test_object.py | 3 +- 7 files changed, 5746 insertions(+), 5698 deletions(-) create mode 100644 shade/openstackcloud.py create mode 100644 shade/operatorcloud.py diff --git a/doc/source/usage.rst b/doc/source/usage.rst index 55a263166..43bc0d957 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -13,5 +13,8 @@ To use shade in a project:: compatibility, but attribute access is deprecated. New code should assume a normal dictionary and access values via key. -.. automodule:: shade +.. autoclass:: shade.OpenStackCloud + :members: + +.. autoclass:: shade.OperatorCloud :members: diff --git a/shade/__init__.py b/shade/__init__.py index a6356989e..3632de0d5 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -12,42 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import contextlib -import functools -import hashlib import logging -import operator -import threading -import time -from cinderclient.v1 import client as cinder_client -from designateclient.v1 import Client as designate_client -from dogpile import cache -import glanceclient -import glanceclient.exc -from glanceclient.common import utils as glance_utils -import ipaddress -from heatclient import client as heat_client -from heatclient.common import template_utils -from ironicclient import client as ironic_client -from ironicclient import exceptions as ironic_exceptions -import jsonpatch import keystoneauth1.exceptions -from keystoneauth1 import plugin as ksa_plugin -from keystoneauth1 import session as ksa_session -from keystoneclient.v2_0 import client as k2_client -from keystoneclient.v3 import client as k3_client -from novaclient import client as nova_client -from novaclient import exceptions as nova_exceptions -from neutronclient.common import exceptions as neutron_exceptions -from neutronclient.v2_0 import client as neutron_client import os_client_config -import os_client_config.defaults import pbr.version -import swiftclient.client as swift_client -import swiftclient.service as swift_service -import swiftclient.exceptions as swift_exceptions -import troveclient.client as trove_client # Disable the Rackspace warnings about deprecated certificates. We are aware import warnings @@ -63,29 +32,12 @@ warnings.filterwarnings('ignore', category=SubjectAltNameWarning) from shade.exc import * # noqa +from shade.openstackcloud import OpenStackCloud +from shade.operatorcloud import OperatorCloud from shade import _log -from shade import meta -from shade import task_manager -from shade import _tasks -from shade import _utils __version__ = pbr.version.VersionInfo('shade').version_string() -OBJECT_MD5_KEY = 'x-object-meta-x-shade-md5' -OBJECT_SHA256_KEY = 'x-object-meta-x-shade-sha256' -IMAGE_MD5_KEY = 'owner_specified.shade.md5' -IMAGE_SHA256_KEY = 'owner_specified.shade.sha256' -# Rackspace returns this for intermittent import errors -IMAGE_ERROR_396 = "Image cannot be imported. Error code: '396'" -DEFAULT_OBJECT_SEGMENT_SIZE = 1073741824 # 1GB -# This halves the current default for Swift -DEFAULT_MAX_FILE_SIZE = (5 * 1024 * 1024 * 1024 + 2) / 2 - - -OBJECT_CONTAINER_ACLS = { - 'public': ".r:*,.rlistings", - 'private': '', -} def simple_logging(debug=False): @@ -138,5647 +90,3 @@ def operator_cloud(config=None, **kwargs): raise OpenStackCloudException( "Invalid cloud configuration: {exc}".format(exc=str(e))) return OperatorCloud(cloud_config=cloud_config) - - -_decorated_methods = [] - - -def _cache_on_arguments(*cache_on_args, **cache_on_kwargs): - def _inner_cache_on_arguments(func): - def _cache_decorator(obj, *args, **kwargs): - the_method = obj._cache.cache_on_arguments( - *cache_on_args, **cache_on_kwargs)( - func.__get__(obj, type(obj))) - return the_method(*args, **kwargs) - - def invalidate(obj, *args, **kwargs): - return obj._cache.cache_on_arguments()(func).invalidate( - *args, **kwargs) - - _cache_decorator.invalidate = invalidate - _cache_decorator.func = func - _decorated_methods.append(func.__name__) - - return _cache_decorator - return _inner_cache_on_arguments - - -def _no_pending_volumes(volumes): - '''If there are any volumes not in a steady state, don't cache''' - for volume in volumes: - if volume['status'] not in ('available', 'error'): - return False - return True - - -def _no_pending_images(images): - '''If there are any images not in a steady state, don't cache''' - for image in images: - if image.status not in ('active', 'deleted', 'killed'): - return False - return True - - -def _no_pending_stacks(stacks): - '''If there are any stacks not in a steady state, don't cache''' - for stack in stacks: - status = stack['status'] - if '_COMPLETE' not in status and '_FAILED' not in status: - return False - return True - - -class OpenStackCloud(object): - """Represent a connection to an OpenStack Cloud. - - OpenStackCloud is the entry point for all cloud operations, regardless - of which OpenStack service those operations may ultimately come from. - The operations on an OpenStackCloud are resource oriented rather than - REST API operation oriented. For instance, one will request a Floating IP - and that Floating IP will be actualized either via neutron or via nova - depending on how this particular cloud has decided to arrange itself. - - :param TaskManager manager: Optional task manager to use for running - OpenStack API tasks. Unless you're doing - rate limiting client side, you almost - certainly don't need this. (optional) - :param CloudConfig cloud_config: Cloud config object from os-client-config - In the future, this will be the only way - to pass in cloud configuration, but is - being phased in currently. - """ - _SERVER_LIST_AGE = 5 # TODO(mordred) Make this configurable - - def __init__( - self, - cloud_config=None, - manager=None, **kwargs): - - self.log = _log.setup_logging('shade') - if not cloud_config: - config = os_client_config.OpenStackConfig() - cloud_config = config.get_one_cloud(**kwargs) - - self.name = cloud_config.name - self.auth = cloud_config.get_auth_args() - self.region_name = cloud_config.region_name - self.default_interface = cloud_config.get_interface() - self.private = cloud_config.config.get('private', False) - self.api_timeout = cloud_config.config['api_timeout'] - self.image_api_use_tasks = cloud_config.config['image_api_use_tasks'] - self.secgroup_source = cloud_config.config['secgroup_source'] - self.force_ipv4 = cloud_config.force_ipv4 - - self._external_networks = [] - self._external_network_name_or_id = cloud_config.config.get( - 'external_network', None) - self._use_external_network = cloud_config.config.get( - 'use_external_network', True) - - self._internal_networks = [] - self._internal_network_name_or_id = cloud_config.config.get( - 'internal_network', None) - self._use_internal_network = cloud_config.config.get( - 'use_internal_network', True) - - # Variables to prevent us from going through the network finding - # logic again if we've done it once. This is different from just - # the cached value, since "None" is a valid value to find. - self._external_network_stamp = False - self._internal_network_stamp = False - - if manager is not None: - self.manager = manager - else: - self.manager = task_manager.TaskManager( - name=self.name, client=self) - - (self.verify, self.cert) = cloud_config.get_requests_verify_args() - - self._servers = [] - self._servers_time = 0 - self._servers_lock = threading.Lock() - - cache_expiration_time = cloud_config.get_cache_expiration_time() - cache_class = cloud_config.get_cache_class() - cache_arguments = cloud_config.get_cache_arguments() - cache_expiration = cloud_config.get_cache_expiration() - - if cache_class != 'dogpile.cache.null': - self._cache = cache.make_region( - function_key_generator=self._make_cache_key - ).configure( - cache_class, - expiration_time=cache_expiration_time, - arguments=cache_arguments) - else: - def _fake_invalidate(unused): - pass - - class _FakeCache(object): - def invalidate(self): - pass - - # Don't cache list_servers if we're not caching things. - # Replace this with a more specific cache configuration - # soon. - self._SERVER_LIST_AGE = 2 - self._cache = _FakeCache() - # Undecorate cache decorated methods. Otherwise the call stacks - # wind up being stupidly long and hard to debug - for method in _decorated_methods: - meth_obj = getattr(self, method, None) - if not meth_obj: - continue - if (hasattr(meth_obj, 'invalidate') - and hasattr(meth_obj, 'func')): - new_func = functools.partial(meth_obj.func, self) - new_func.invalidate = _fake_invalidate - setattr(self, method, new_func) - - # If server expiration time is set explicitly, use that. Otherwise - # fall back to whatever it was before - self._SERVER_LIST_AGE = cache_expiration.get( - 'server', self._SERVER_LIST_AGE) - - self._container_cache = dict() - self._file_hash_cache = dict() - - self._keystone_session = None - - self._cinder_client = None - self._designate_client = None - self._glance_client = None - self._glance_endpoint = None - self._heat_client = None - self._ironic_client = None - self._keystone_client = None - self._neutron_client = None - self._nova_client = None - self._swift_client = None - self._swift_service = None - self._trove_client = None - - self._local_ipv6 = _utils.localhost_supports_ipv6() - - self.cloud_config = cloud_config - - @contextlib.contextmanager - def _neutron_exceptions(self, error_message): - try: - yield - except neutron_exceptions.NotFound as e: - raise OpenStackCloudResourceNotFound( - "{msg}: {exc}".format(msg=error_message, exc=str(e))) - except neutron_exceptions.NeutronClientException as e: - if e.status_code == 404: - raise OpenStackCloudURINotFound( - "{msg}: {exc}".format(msg=error_message, exc=str(e))) - else: - raise OpenStackCloudException( - "{msg}: {exc}".format(msg=error_message, exc=str(e))) - except Exception as e: - raise OpenStackCloudException( - "{msg}: {exc}".format(msg=error_message, exc=str(e))) - - def _make_cache_key(self, namespace, fn): - fname = fn.__name__ - if namespace is None: - name_key = self.name - else: - name_key = '%s:%s' % (self.name, namespace) - - def generate_key(*args, **kwargs): - arg_key = ','.join(args) - kw_keys = sorted(kwargs.keys()) - kwargs_key = ','.join( - ['%s:%s' % (k, kwargs[k]) for k in kw_keys if k != 'cache']) - ans = "_".join( - [str(name_key), fname, arg_key, kwargs_key]) - return ans - return generate_key - - def _get_client( - self, service_key, client_class, interface_key='endpoint_type', - pass_version_arg=True, **kwargs): - try: - interface = self.cloud_config.get_interface(service_key) - # trigger exception on lack of service - self.get_session_endpoint(service_key) - constructor_args = dict( - session=self.keystone_session, - service_name=self.cloud_config.get_service_name(service_key), - service_type=self.cloud_config.get_service_type(service_key), - region_name=self.region_name) - constructor_args.update(kwargs) - constructor_args[interface_key] = interface - if pass_version_arg: - version = self.cloud_config.get_api_version(service_key) - constructor_args['version'] = version - client = client_class(**constructor_args) - except Exception: - self.log.debug( - "Couldn't construct {service} object".format( - service=service_key), exc_info=True) - raise - if client is None: - raise OpenStackCloudException( - "Failed to instantiate {service} client." - " This could mean that your credentials are wrong.".format( - service=service_key)) - return client - - @property - def nova_client(self): - if self._nova_client is None: - self._nova_client = self._get_client( - 'compute', nova_client.Client) - return self._nova_client - - def _get_identity_client_class(self): - if self.cloud_config.get_api_version('identity') == '3': - return k3_client.Client - elif self.cloud_config.get_api_version('identity') in ('2', '2.0'): - return k2_client.Client - raise OpenStackCloudException( - "Unknown identity API version: {version}".format( - version=self.cloud_config.get_api_version('identity'))) - - @property - def keystone_session(self): - if self._keystone_session is None: - - try: - keystone_auth = self.cloud_config.get_auth() - if not keystone_auth: - raise OpenStackCloudException( - "Problem with auth parameters") - self._keystone_session = ksa_session.Session( - auth=keystone_auth, - verify=self.verify, - cert=self.cert, - timeout=self.api_timeout) - except Exception as e: - raise OpenStackCloudException( - "Error authenticating to keystone: %s " % str(e)) - return self._keystone_session - - @property - def keystone_client(self): - if self._keystone_client is None: - self._keystone_client = self._get_client( - 'identity', self._get_identity_client_class()) - return self._keystone_client - - @property - def service_catalog(self): - return self.keystone_session.auth.get_access( - self.keystone_session).service_catalog.catalog - - @property - def auth_token(self): - # Keystone's session will reuse a token if it is still valid. - # We don't need to track validity here, just get_token() each time. - return self.keystone_session.get_token() - - @property - def _project_manager(self): - # Keystone v2 calls this attribute tenants - # Keystone v3 calls it projects - # Yay for usable APIs! - if self.cloud_config.get_api_version('identity').startswith('2'): - return self.keystone_client.tenants - return self.keystone_client.projects - - def _get_project_param_dict(self, name_or_id): - project_dict = dict() - if name_or_id: - project = self.get_project(name_or_id) - if not project: - return project_dict - if self.cloud_config.get_api_version('identity') == '3': - project_dict['default_project'] = project['id'] - else: - project_dict['tenant_id'] = project['id'] - return project_dict - - def _get_domain_param_dict(self, domain_id): - """Get a useable domain.""" - - # Keystone v3 requires domains for user and project creation. v2 does - # not. However, keystone v2 does not allow user creation by non-admin - # users, so we can throw an error to the user that does not need to - # mention api versions - if self.cloud_config.get_api_version('identity') == '3': - if not domain_id: - raise OpenStackCloudException( - "User creation requires an explicit domain_id argument.") - else: - return {'domain': domain_id} - else: - return {} - - def _get_identity_params(self, domain_id=None, project=None): - """Get the domain and project/tenant parameters if needed. - - keystone v2 and v3 are divergent enough that we need to pass or not - pass project or tenant_id or domain or nothing in a sane manner. - """ - ret = {} - ret.update(self._get_domain_param_dict(domain_id)) - ret.update(self._get_project_param_dict(project)) - return ret - - @_cache_on_arguments() - def list_projects(self): - """List Keystone Projects. - - :returns: a list of dicts containing the project description. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - try: - projects = self.manager.submitTask(_tasks.ProjectList()) - except Exception as e: - self.log.debug("Failed to list projects", exc_info=True) - raise OpenStackCloudException(str(e)) - return meta.obj_list_to_dict(projects) - - def search_projects(self, name_or_id=None, filters=None): - """Seach Keystone projects. - - :param name: project name or id. - :param filters: a dict containing additional filters to use. - - :returns: a list of dict containing the role description - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - projects = self.list_projects() - return _utils._filter_list(projects, name_or_id, filters) - - def get_project(self, name_or_id, filters=None): - """Get exactly one Keystone project. - - :param id: project name or id. - :param filters: a dict containing additional filters to use. - - :returns: a list of dicts containing the project description. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - return _utils._get_entity(self.search_projects, name_or_id, filters) - - def update_project(self, name_or_id, description=None, enabled=True): - try: - proj = self.get_project(name_or_id) - if not proj: - raise OpenStackCloudException( - "Project %s not found." % name_or_id) - - params = {} - if self.api_versions['identity'] == '3': - params['project'] = proj['id'] - else: - params['tenant_id'] = proj['id'] - - project = self.manager.submitTask(_tasks.ProjectUpdate( - description=description, - enabled=enabled, - **params)) - except Exception as e: - raise OpenStackCloudException( - "Error in updating project {project}: {message}".format( - project=name_or_id, message=str(e))) - self.list_projects.invalidate() - return meta.obj_to_dict(project) - - def create_project( - self, name, description=None, domain_id=None, enabled=True): - """Create a project.""" - try: - params = self._get_domain_param_dict(domain) - if self.api_versions['identity'] == '3': - params['name'] = name - else: - params['tenant_name'] = name - - project = self.manager.submitTask(_tasks.ProjectCreate( - project_name=name, description=description, enabled=enabled, - **params)) - except Exception as e: - raise OpenStackCloudException( - "Error in creating project {project}: {message}".format( - project=name, message=str(e))) - self.list_projects.invalidate() - return meta.obj_to_dict(project) - - def delete_project(self, name_or_id): - try: - project = self.update_project(name_or_id, enabled=False) - params = {} - if self.api_versions['identity'] == '3': - params['project'] = project['id'] - else: - params['tenant'] = project['id'] - self.manager.submitTask(_tasks.ProjectDelete(**params)) - except Exception as e: - raise OpenStackCloudException( - "Error in deleting project {project}: {message}".format( - project=name_or_id, message=str(e))) - - @_cache_on_arguments() - def list_users(self): - """List Keystone Users. - - :returns: a list of dicts containing the user description. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - try: - users = self.manager.submitTask(_tasks.UserList()) - except Exception as e: - raise OpenStackCloudException( - "Failed to list users: {0}".format(str(e)) - ) - return _utils.normalize_users(meta.obj_list_to_dict(users)) - - def search_users(self, name_or_id=None, filters=None): - """Seach Keystone users. - - :param string name: user name or id. - :param dict filters: a dict containing additional filters to use. - - :returns: a list of dict containing the role description - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - users = self.list_users() - return _utils._filter_list(users, name_or_id, filters) - - def get_user(self, name_or_id, filters=None): - """Get exactly one Keystone user. - - :param string name_or_id: user name or id. - :param dict filters: a dict containing additional filters to use. - - :returns: a single dict containing the user description. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - return _utils._get_entity(self.search_users, name_or_id, filters) - - def get_user_by_id(self, user_id, normalize=True): - """Get a Keystone user by ID. - - :param string user_id: user ID - :param bool normalize: Flag to control dict normalization - - :returns: a single dict containing the user description - """ - try: - user = meta.obj_to_dict( - self.manager.submitTask(_tasks.UserGet(user=user_id)) - ) - except Exception as e: - raise OpenStackCloudException( - "Error getting user with ID {user_id}: {message}".format( - user_id=user_id, message=str(e))) - if user and normalize: - return _utils.normalize_users([user])[0] - return user - - # NOTE(Shrews): Keystone v2 supports updating only name, email and enabled. - @_utils.valid_kwargs('name', 'email', 'enabled', 'domain_id', 'password', - 'description', 'default_project') - def update_user(self, name_or_id, **kwargs): - self.list_users.invalidate(self) - user = self.get_user(name_or_id) - # normalized dict won't work - kwargs['user'] = self.get_user_by_id(user['id'], normalize=False) - - if self.cloud_config.get_api_version('identity') != '3': - # Do not pass v3 args to a v2 keystone. - kwargs.pop('domain_id', None) - kwargs.pop('password', None) - kwargs.pop('description', None) - kwargs.pop('default_project', None) - elif 'domain_id' in kwargs: - # The incoming parameter is domain_id in order to match the - # parameter name in create_user(), but UserUpdate() needs it - # to be domain. - kwargs['domain'] = kwargs.pop('domain_id') - - try: - user = self.manager.submitTask(_tasks.UserUpdate(**kwargs)) - except Exception as e: - raise OpenStackCloudException( - "Error in updating user {user}: {message}".format( - user=name_or_id, message=str(e))) - self.list_users.invalidate(self) - return _utils.normalize_users([meta.obj_to_dict(user)])[0] - - def create_user( - self, name, password=None, email=None, default_project=None, - enabled=True, domain_id=None): - """Create a user.""" - try: - identity_params = self._get_identity_params( - domain_id, default_project) - user = self.manager.submitTask(_tasks.UserCreate( - name=name, password=password, email=email, - enabled=enabled, **identity_params)) - except Exception as e: - raise OpenStackCloudException( - "Error in creating user {user}: {message}".format( - user=name, message=str(e))) - self.list_users.invalidate(self) - return _utils.normalize_users([meta.obj_to_dict(user)])[0] - - def delete_user(self, name_or_id): - self.list_users.invalidate(self) - user = self.get_user(name_or_id) - if not user: - self.log.debug( - "User {0} not found for deleting".format(name_or_id)) - return False - - # normalized dict won't work - user = self.get_user_by_id(user['id'], normalize=False) - try: - self.manager.submitTask(_tasks.UserDelete(user=user)) - except Exception as e: - raise OpenStackCloudException( - "Error in deleting user {user}: {message}".format( - user=name_or_id, message=str(e))) - self.list_users.invalidate(self) - return True - - @property - def glance_client(self): - if self._glance_client is None: - endpoint, version = glance_utils.strip_version( - self.get_session_endpoint(service_key='image')) - # TODO(mordred): Put check detected vs. configured version - # and warn if they're different. - self._glance_client = self._get_client( - 'image', glanceclient.Client, interface_key='interface', - endpoint=endpoint) - return self._glance_client - - @property - def heat_client(self): - if self._heat_client is None: - self._heat_client = self._get_client( - 'orchestration', heat_client.Client) - return self._heat_client - - def get_template_contents( - self, template_file=None, template_url=None, - template_object=None, files=None): - try: - return template_utils.get_template_contents( - template_file=template_file, template_url=template_url, - template_object=template_object, files=files) - except Exception as e: - raise OpenStackCloudException( - "Error in processing template files: %s" % str(e)) - - @property - def swift_client(self): - if self._swift_client is None: - try: - token = self.keystone_session.get_token() - endpoint = self.get_session_endpoint( - service_key='object-store') - self._swift_client = swift_client.Connection( - preauthurl=endpoint, - preauthtoken=token, - auth_version=self.cloud_config.get_api_version('identity'), - os_options=dict( - auth_token=token, - object_storage_url=endpoint, - region_name=self.region_name), - timeout=self.api_timeout, - ) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Error constructing swift client: %s", str(e)) - return self._swift_client - - @property - def swift_service(self): - if self._swift_service is None: - try: - endpoint = self.get_session_endpoint( - service_key='object-store') - options = dict(os_auth_token=self.auth_token, - os_storage_url=endpoint, - os_region_name=self.region_name) - self._swift_service = swift_service.SwiftService( - options=options) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Error constructing swift client: %s", str(e)) - return self._swift_service - - @property - def cinder_client(self): - - if self._cinder_client is None: - self._cinder_client = self._get_client( - 'volume', cinder_client.Client) - return self._cinder_client - - @property - def trove_client(self): - if self._trove_client is None: - self.get_session_endpoint(service_key='database') - # Make the connection - can't use keystone session until there - # is one - self._trove_client = trove_client.Client( - self.cloud_config.get_api_version('database'), - session=self.keystone_session, - region_name=self.region_name, - service_type=self.cloud_config.get_service_type('database'), - ) - - if self._trove_client is None: - raise OpenStackCloudException( - "Failed to instantiate Trove client." - " This could mean that your credentials are wrong.") - - self._trove_client = self._get_client( - 'database', trove_client.Client) - return self._trove_client - - @property - def neutron_client(self): - if self._neutron_client is None: - self._neutron_client = self._get_client( - 'network', neutron_client.Client, pass_version_arg=False) - return self._neutron_client - - @property - def designate_client(self): - if self._designate_client is None: - self._designate_client = self._get_client( - 'dns', designate_client.Client) - return self._designate_client - - def create_stack( - self, name, - template_file=None, template_url=None, - template_object=None, files=None, - rollback=True, - wait=False, timeout=180, - **parameters): - tpl_files, template = template_utils.get_template_contents( - template_file=template_file, - template_url=template_url, - template_object=template_object, - files=files) - params = dict( - stack_name=name, - disable_rollback=not rollback, - parameters=parameters, - template=template, - files=tpl_files, - ) - try: - stack = self.manager.submitTask(_tasks.StackCreate(**params)) - except Exception as e: - raise OpenStackCloudException( - "Error creating stack {name}: {message}".format( - name=name, message=e.message)) - if not wait: - return stack - for count in _iterate_timeout( - timeout, - "Timed out waiting for heat stack to finish"): - if self.get_stack(name, cache=False): - return stack - - def delete_stack(self, name_or_id): - """Delete a Heat Stack - - :param name_or_id: Stack name or id. - - :returns: True if delete succeeded, False otherwise. - - :raises: ``OpenStackCloudException`` if something goes wrong during - the openstack API call - """ - stack = self.get_stack(name_or_id=name_or_id) - if stack is None: - self.log.debug("Stack %s not found for deleting" % name_or_id) - return False - - try: - self.manager.submitTask(_tasks.StackDelete(id=stack['id'])) - except Exception: - raise OpenStackCloudException( - "Failed to delete stack {id}".format(id=stack['id'])) - return True - - def get_name(self): - return self.name - - def get_region(self): - return self.region_name - - def get_flavor_name(self, flavor_id): - flavor = self.get_flavor(flavor_id) - if flavor: - return flavor['name'] - return None - - def get_flavor_by_ram(self, ram, include=None): - """Get a flavor based on amount of RAM available. - - Finds the flavor with the least amount of RAM that is at least - as much as the specified amount. If `include` is given, further - filter based on matching flavor name. - - :param int ram: Minimum amount of RAM. - :param string include: If given, will return a flavor whose name - contains this string as a substring. - """ - flavors = self.list_flavors() - for flavor in sorted(flavors, key=operator.itemgetter('ram')): - if (flavor['ram'] >= ram and - (not include or include in flavor['name'])): - return flavor - raise OpenStackCloudException( - "Could not find a flavor with {ram} and '{include}'".format( - ram=ram, include=include)) - - def get_session_endpoint(self, service_key): - override_endpoint = self.cloud_config.get_endpoint(service_key) - if override_endpoint: - return override_endpoint - try: - # keystone is a special case in keystone, because what? - if service_key == 'identity': - endpoint = self.keystone_session.get_endpoint( - interface=ksa_plugin.AUTH_INTERFACE) - else: - endpoint = self.keystone_session.get_endpoint( - service_type=self.cloud_config.get_service_type( - service_key), - service_name=self.cloud_config.get_service_name( - service_key), - interface=self.cloud_config.get_interface(service_key), - region_name=self.region_name) - except keystoneauth1.exceptions.catalog.EndpointNotFound as e: - self.log.debug( - "Endpoint not found in %s cloud: %s", self.name, str(e)) - endpoint = None - except Exception as e: - raise OpenStackCloudException( - "Error getting %s endpoint: %s" % (service_key, str(e))) - return endpoint - - def has_service(self, service_key): - if not self.cloud_config.config.get('has_%s' % service_key, True): - self.log.debug( - "Overriding {service_key} entry in catalog per config".format( - service_key=service_key)) - return False - try: - endpoint = self.get_session_endpoint(service_key) - except OpenStackCloudException: - return False - if endpoint: - return True - else: - return False - - @_cache_on_arguments() - def _nova_extensions(self): - extensions = set() - - try: - resp, body = self.manager.submitTask( - _tasks.NovaUrlGet(url='/extensions')) - for x in body['extensions']: - extensions.add(x['alias']) - except Exception as e: - raise OpenStackCloudException( - "error fetching extension list for nova: {msg}".format( - msg=str(e))) - - return extensions - - def _has_nova_extension(self, extension_name): - return extension_name in self._nova_extensions() - - def search_keypairs(self, name_or_id=None, filters=None): - keypairs = self.list_keypairs() - return _utils._filter_list(keypairs, name_or_id, filters) - - def search_networks(self, name_or_id=None, filters=None): - """Search OpenStack networks - - :param name_or_id: Name or id of the desired network. - :param filters: a dict containing additional filters to use. e.g. - {'router:external': True} - - :returns: a list of dicts containing the network description. - - :raises: ``OpenStackCloudException`` if something goes wrong during the - openstack API call. - """ - networks = self.list_networks(filters) - return _utils._filter_list(networks, name_or_id, filters) - - def search_routers(self, name_or_id=None, filters=None): - """Search OpenStack routers - - :param name_or_id: Name or id of the desired router. - :param filters: a dict containing additional filters to use. e.g. - {'admin_state_up': True} - - :returns: a list of dicts containing the router description. - - :raises: ``OpenStackCloudException`` if something goes wrong during the - openstack API call. - """ - routers = self.list_routers(filters) - return _utils._filter_list(routers, name_or_id, filters) - - def search_subnets(self, name_or_id=None, filters=None): - """Search OpenStack subnets - - :param name_or_id: Name or id of the desired subnet. - :param filters: a dict containing additional filters to use. e.g. - {'enable_dhcp': True} - - :returns: a list of dicts containing the subnet description. - - :raises: ``OpenStackCloudException`` if something goes wrong during the - openstack API call. - """ - subnets = self.list_subnets(filters) - return _utils._filter_list(subnets, name_or_id, filters) - - def search_ports(self, name_or_id=None, filters=None): - """Search OpenStack ports - - :param name_or_id: Name or id of the desired port. - :param filters: a dict containing additional filters to use. e.g. - {'device_id': '2711c67a-b4a7-43dd-ace7-6187b791c3f0'} - - :returns: a list of dicts containing the port description. - - :raises: ``OpenStackCloudException`` if something goes wrong during the - openstack API call. - """ - ports = self.list_ports(filters) - return _utils._filter_list(ports, name_or_id, filters) - - def search_volumes(self, name_or_id=None, filters=None): - volumes = self.list_volumes() - return _utils._filter_list( - volumes, name_or_id, filters, name_key='display_name') - - def search_volume_snapshots(self, name_or_id=None, filters=None): - volumesnapshots = self.list_volume_snapshots() - return _utils._filter_list( - volumesnapshots, name_or_id, filters, name_key='display_name') - - def search_flavors(self, name_or_id=None, filters=None): - flavors = self.list_flavors() - return _utils._filter_list(flavors, name_or_id, filters) - - def search_security_groups(self, name_or_id=None, filters=None): - groups = self.list_security_groups() - return _utils._filter_list(groups, name_or_id, filters) - - def search_servers(self, name_or_id=None, filters=None, detailed=False): - servers = self.list_servers(detailed=detailed) - return _utils._filter_list(servers, name_or_id, filters) - - def search_images(self, name_or_id=None, filters=None): - images = self.list_images() - return _utils._filter_list(images, name_or_id, filters) - - def search_floating_ip_pools(self, name=None, filters=None): - pools = self.list_floating_ip_pools() - return _utils._filter_list(pools, name, filters) - - # Note (dguerri): when using Neutron, this can be optimized using - # server-side search. - # There are some cases in which such optimization is not possible (e.g. - # nested attributes or list of objects) so we need to use the client-side - # filtering when we can't do otherwise. - # The same goes for all neutron-related search/get methods! - def search_floating_ips(self, id=None, filters=None): - floating_ips = self.list_floating_ips() - return _utils._filter_list(floating_ips, id, filters) - - def search_zones(self, name_or_id=None, filters=None): - zones = self.list_zones() - return _utils._filter_list(zones, name_or_id, filters) - - def _search_records(self, zone_id, name_or_id=None, filters=None): - records = self._list_records(zone_id=zone_id) - return _utils._filter_list(records, name_or_id, filters) - - def search_stacks(self, name_or_id=None, filters=None): - """Search Heat stacks. - - :param name_or_id: Name or id of the desired stack. - :param filters: a dict containing additional filters to use. e.g. - {'stack_status': 'CREATE_COMPLETE'} - - :returns: a list of dict containing the stack description. - - :raises: ``OpenStackCloudException`` if something goes wrong during the - openstack API call. - """ - stacks = self.list_stacks() - return _utils._filter_list( - stacks, name_or_id, filters, name_name='stack_name') - - def list_keypairs(self): - """List all available keypairs. - - :returns: A list of keypair dicts. - - """ - try: - return meta.obj_list_to_dict( - self.manager.submitTask(_tasks.KeypairList()) - ) - except Exception as e: - raise OpenStackCloudException( - "Error fetching keypair list: %s" % str(e)) - - def list_networks(self, filters=None): - """List all available networks. - - :param filters: (optional) dict of filter conditions to push down - :returns: A list of network dicts. - - """ - # Translate None from search interface to empty {} for kwargs below - if not filters: - filters = {} - with self._neutron_exceptions("Error fetching network list"): - return self.manager.submitTask( - _tasks.NetworkList(**filters))['networks'] - - def list_routers(self, filters=None): - """List all available routers. - - :param filters: (optional) dict of filter conditions to push down - :returns: A list of router dicts. - - """ - # Translate None from search interface to empty {} for kwargs below - if not filters: - filters = {} - with self._neutron_exceptions("Error fetching router list"): - return self.manager.submitTask( - _tasks.RouterList(**filters))['routers'] - - def list_subnets(self, filters=None): - """List all available subnets. - - :param filters: (optional) dict of filter conditions to push down - :returns: A list of subnet dicts. - - """ - # Translate None from search interface to empty {} for kwargs below - if not filters: - filters = {} - with self._neutron_exceptions("Error fetching subnet list"): - return self.manager.submitTask( - _tasks.SubnetList(**filters))['subnets'] - - def list_ports(self, filters=None): - """List all available ports. - - :param filters: (optional) dict of filter conditions to push down - :returns: A list of port dicts. - - """ - # Translate None from search interface to empty {} for kwargs below - if not filters: - filters = {} - with self._neutron_exceptions("Error fetching port list"): - return self.manager.submitTask(_tasks.PortList(**filters))['ports'] - - @_cache_on_arguments(should_cache_fn=_no_pending_volumes) - def list_volumes(self, cache=True): - """List all available volumes. - - :returns: A list of volume dicts. - - """ - if not cache: - warnings.warn('cache argument to list_volumes is deprecated. Use ' - 'invalidate instead.') - try: - return meta.obj_list_to_dict( - self.manager.submitTask(_tasks.VolumeList()) - ) - except Exception as e: - raise OpenStackCloudException( - "Error fetching volume list: %s" % e) - - @_cache_on_arguments() - def list_flavors(self): - """List all available flavors. - - :returns: A list of flavor dicts. - - """ - try: - return meta.obj_list_to_dict( - self.manager.submitTask(_tasks.FlavorList(is_public=None)) - ) - except Exception as e: - raise OpenStackCloudException( - "Error fetching flavor list: %s" % e) - - @_cache_on_arguments(should_cache_fn=_no_pending_stacks) - def list_stacks(self): - """List all Heat stacks. - - :returns: a list of dict containing the stack description. - - :raises: ``OpenStackCloudException`` if something goes wrong during the - openstack API call. - """ - try: - stacks = self.manager.submitTask(_tasks.StackList()) - except Exception as e: - raise OpenStackCloudException(str(e)) - return meta.obj_list_to_dict(stacks) - - def list_server_security_groups(self, server): - """List all security groups associated with the given server. - - :returns: A list of security group dicts. - """ - - groups = meta.obj_list_to_dict( - self.manager.submitTask( - _tasks.ServerListSecurityGroups(server=server['id']))) - - return _utils.normalize_nova_secgroups(groups) - - def list_security_groups(self): - """List all available security groups. - - :returns: A list of security group dicts. - - """ - # Handle neutron security groups - if self.secgroup_source == 'neutron': - # Neutron returns dicts, so no need to convert objects here. - with self._neutron_exceptions( - "Error fetching security group list"): - return self.manager.submitTask( - _tasks.NeutronSecurityGroupList())['security_groups'] - - # Handle nova security groups - elif self.secgroup_source == 'nova': - try: - groups = meta.obj_list_to_dict( - self.manager.submitTask(_tasks.NovaSecurityGroupList()) - ) - except Exception: - raise OpenStackCloudException( - "Error fetching security group list" - ) - return _utils.normalize_nova_secgroups(groups) - - # Security groups not supported - else: - raise OpenStackCloudUnavailableFeature( - "Unavailable feature: security groups" - ) - - def list_servers(self, detailed=False): - """List all available servers. - - :returns: A list of server dicts. - - """ - if (time.time() - self._servers_time) >= self._SERVER_LIST_AGE: - # Since we're using cached data anyway, we don't need to - # have more than one thread actually submit the list - # servers task. Let the first one submit it while holding - # a lock, and the non-blocking acquire method will cause - # subsequent threads to just skip this and use the old - # data until it succeeds. - # For the first time, when there is no data, make the call - # blocking. - if self._servers_lock.acquire(len(self._servers) == 0): - try: - self._servers = self._list_servers(detailed=detailed) - self._servers_time = time.time() - finally: - self._servers_lock.release() - return self._servers - - def _list_servers(self, detailed=False): - try: - servers = meta.obj_list_to_dict( - self.manager.submitTask(_tasks.ServerList())) - - if detailed: - return [ - meta.get_hostvars_from_server(self, server) - for server in servers - ] - else: - return servers - except Exception as e: - raise OpenStackCloudException( - "Error fetching server list: %s" % e) - - @_cache_on_arguments(should_cache_fn=_no_pending_images) - def list_images(self, filter_deleted=True): - """Get available glance images. - - :param filter_deleted: Control whether deleted images are returned. - :returns: A list of glance images. - """ - # First, try to actually get images from glance, it's more efficient - images = [] - try: - - # Creates a generator - does not actually talk to the cloud API - # hardcoding page size for now. We'll have to get MUCH smarter - # if we want to deal with page size per unit of rate limiting - image_gen = self.glance_client.images.list(page_size=1000) - # Deal with the generator to make a list - image_list = self.manager.submitTask( - _tasks.GlanceImageList(image_gen=image_gen)) - - if image_list: - if getattr(image_list[0], 'validate', None): - # glanceclient returns a "warlock" object if you use v2 - image_list = meta.warlock_list_to_dict(image_list) - else: - # glanceclient returns a normal object if you use v1 - image_list = meta.obj_list_to_dict(image_list) - - except glanceclient.exc.HTTPInternalServerError: - # We didn't have glance, let's try nova - # If this doesn't work - we just let the exception propagate - try: - image_list = meta.obj_list_to_dict( - self.manager.submitTask(_tasks.NovaImageList()) - ) - except Exception as e: - raise OpenStackCloudException( - "Error fetching image list: %s" % e) - - except Exception as e: - raise OpenStackCloudException( - "Error fetching image list: %s" % e) - - for image in image_list: - # The cloud might return DELETED for invalid images. - # While that's cute and all, that's an implementation detail. - if not filter_deleted: - images.append(image) - elif image.status != 'DELETED': - images.append(image) - return images - - def list_floating_ip_pools(self): - """List all available floating IP pools. - - :returns: A list of floating IP pool dicts. - - """ - if not self._has_nova_extension('os-floating-ip-pools'): - raise OpenStackCloudUnavailableExtension( - 'Floating IP pools extension is not available on target cloud') - - try: - return meta.obj_list_to_dict( - self.manager.submitTask(_tasks.FloatingIPPoolList()) - ) - except Exception as e: - raise OpenStackCloudException( - "error fetching floating IP pool list: {msg}".format( - msg=str(e))) - - def list_floating_ips(self): - """List all available floating IPs. - - :returns: A list of floating IP dicts. - - """ - if self.has_service('network'): - try: - return _utils.normalize_neutron_floating_ips( - self._neutron_list_floating_ips()) - except OpenStackCloudURINotFound as e: - self.log.debug( - "Something went wrong talking to neutron API: " - "'{msg}'. Trying with Nova.".format(msg=str(e))) - # Fall-through, trying with Nova - - floating_ips = self._nova_list_floating_ips() - return _utils.normalize_nova_floating_ips(floating_ips) - - def _neutron_list_floating_ips(self): - with self._neutron_exceptions("error fetching floating IPs list"): - return self.manager.submitTask( - _tasks.NeutronFloatingIPList())['floatingips'] - - def _nova_list_floating_ips(self): - try: - return meta.obj_list_to_dict( - self.manager.submitTask(_tasks.NovaFloatingIPList())) - except Exception as e: - raise OpenStackCloudException( - "error fetching floating IPs list: {msg}".format(msg=str(e))) - - def list_zones(self): - """List all available DNS zones. - - :returns: A list of zone dicts. - - """ - try: - return self.manager.submitTask(_tasks.ZoneList()) - except Exception as e: - raise OpenStackCloudException( - "Error fetching zone list: %s" % e) - - def _list_records(self, zone_id): - # TODO(mordred) switch domain= to zone= after the Big Rename - try: - return self.manager.submitTask(_tasks.RecordList(domain=zone_id)) - except Exception as e: - raise OpenStackCloudException( - "Error fetching record list: %s" % e) - - def use_external_network(self): - return self._use_external_network - - def use_internal_network(self): - return self._use_internal_network - - def _get_network( - self, - name_or_id, - use_network_func, - network_cache, - network_stamp, - filters): - if not use_network_func(): - return [] - if network_cache: - return network_cache - if network_stamp: - return [] - if not self.has_service('network'): - return [] - if name_or_id: - ext_net = self.get_network(name_or_id) - if not ext_net: - raise OpenStackCloudException( - "Network {network} was provided for external" - " access and that network could not be found".format( - network=name_or_id)) - else: - return [] - try: - # TODO(mordred): Rackspace exposes neutron but it does not - # work. I think that overriding what the service catalog - # reports should be a thing os-client-config should handle - # in a vendor profile - but for now it does not. That means - # this search_networks can just totally fail. If it does though, - # that's fine, clearly the neutron introspection is not going - # to work. - return self.search_networks(filters=filters) - except OpenStackCloudException: - pass - return [] - - def get_external_networks(self): - """Return the networks that are configured to route northbound. - - :returns: A list of network dicts if one is found - """ - self._external_networks = self._get_network( - self._external_network_name_or_id, - self.use_external_network, - self._external_networks, - self._external_network_stamp, - filters={'router:external': True}) - self._external_network_stamp = True - return self._external_networks - - def get_internal_networks(self): - """Return the networks that are configured to not route northbound. - - :returns: A list of network dicts if one is found - """ - self._internal_networks = self._get_network( - self._internal_network_name_or_id, - self.use_internal_network, - self._internal_networks, - self._internal_network_stamp, - filters={ - 'router:external': False, - }) - self._internal_network_stamp = True - return self._internal_networks - - def get_keypair(self, name_or_id, filters=None): - """Get a keypair by name or ID. - - :param name_or_id: Name or ID of the keypair. - :param dict filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - :returns: A keypair dict or None if no matching keypair is - found. - - """ - return _utils._get_entity(self.search_keypairs, name_or_id, filters) - - def get_network(self, name_or_id, filters=None): - """Get a network by name or ID. - - :param name_or_id: Name or ID of the network. - :param dict filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - :returns: A network dict or None if no matching network is - found. - - """ - return _utils._get_entity(self.search_networks, name_or_id, filters) - - def get_router(self, name_or_id, filters=None): - """Get a router by name or ID. - - :param name_or_id: Name or ID of the router. - :param dict filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - :returns: A router dict or None if no matching router is - found. - - """ - return _utils._get_entity(self.search_routers, name_or_id, filters) - - def get_subnet(self, name_or_id, filters=None): - """Get a subnet by name or ID. - - :param name_or_id: Name or ID of the subnet. - :param dict filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - :returns: A subnet dict or None if no matching subnet is - found. - - """ - return _utils._get_entity(self.search_subnets, name_or_id, filters) - - def get_port(self, name_or_id, filters=None): - """Get a port by name or ID. - - :param name_or_id: Name or ID of the port. - :param dict filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - :returns: A port dict or None if no matching port is found. - - """ - return _utils._get_entity(self.search_ports, name_or_id, filters) - - def get_volume(self, name_or_id, filters=None): - """Get a volume by name or ID. - - :param name_or_id: Name or ID of the volume. - :param dict filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - :returns: A volume dict or None if no matching volume is - found. - - """ - return _utils._get_entity(self.search_volumes, name_or_id, filters) - - def get_flavor(self, name_or_id, filters=None): - """Get a flavor by name or ID. - - :param name_or_id: Name or ID of the flavor. - :param dict filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - :returns: A flavor dict or None if no matching flavor is - found. - - """ - return _utils._get_entity(self.search_flavors, name_or_id, filters) - - def get_security_group(self, name_or_id, filters=None): - """Get a security group by name or ID. - - :param name_or_id: Name or ID of the security group. - :param dict filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - :returns: A security group dict or None if no matching - security group is found. - - """ - return _utils._get_entity( - self.search_security_groups, name_or_id, filters) - - def get_server(self, name_or_id=None, filters=None, detailed=False): - """Get a server by name or ID. - - :param name_or_id: Name or ID of the server. - :param dict filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - :returns: A server dict or None if no matching server is - found. - - """ - searchfunc = functools.partial(self.search_servers, - detailed=detailed) - return _utils._get_entity(searchfunc, name_or_id, filters) - - def get_server_by_id(self, id): - return meta.obj_to_dict( - self.manager.submitTask(_tasks.ServerGet(server=id))) - - def get_image(self, name_or_id, filters=None): - """Get an image by name or ID. - - :param name_or_id: Name or ID of the image. - :param dict filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - :returns: An image dict or None if no matching image is found. - - """ - return _utils._get_entity(self.search_images, name_or_id, filters) - - def get_floating_ip(self, id, filters=None): - """Get a floating IP by ID - - :param id: ID of the floating IP. - :param dict filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - :returns: A floating IP dict or None if no matching floating - IP is found. - - """ - return _utils._get_entity(self.search_floating_ips, id, filters) - - def get_zone(self, name_or_id, filters=None): - """Get a DNS zone by name or ID. - - :param name_or_id: Name or ID of the DNS zone. - :param dict filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - :returns: A zone dict or None if no matching DNS zone is - found. - - """ - return _utils._get_entity(self.search_zones, name_or_id, filters) - - def _get_record(self, zone_id, name_or_id, filters=None): - f = lambda name_or_id, filters: self._search_records( - zone_id, name_or_id, filters) - return _utils._get_entity(f, name_or_id, filters) - - def get_stack(self, name_or_id, filters=None): - """Get exactly one Heat stack. - - :param name_or_id: Name or id of the desired stack. - :param filters: a dict containing additional filters to use. e.g. - {'stack_status': 'CREATE_COMPLETE'} - - :returns: a dict containing the stack description - - :raises: ``OpenStackCloudException`` if something goes wrong during the - openstack API call or if multiple matches are found. - """ - return _utils._get_entity( - self.search_stacks, name_or_id, filters) - - def create_keypair(self, name, public_key): - """Create a new keypair. - - :param name: Name of the keypair being created. - :param public_key: Public key for the new keypair. - - :raises: OpenStackCloudException on operation error. - """ - try: - return meta.obj_to_dict( - self.manager.submitTask(_tasks.KeypairCreate( - name=name, public_key=public_key)) - ) - except Exception as e: - raise OpenStackCloudException( - "Unable to create keypair %s: %s" % (name, e) - ) - - def delete_keypair(self, name): - """Delete a keypair. - - :param name: Name of the keypair to delete. - - :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. - """ - try: - self.manager.submitTask(_tasks.KeypairDelete(key=name)) - except nova_exceptions.NotFound: - self.log.debug("Keypair %s not found for deleting" % name) - return False - except Exception as e: - raise OpenStackCloudException( - "Unable to delete keypair %s: %s" % (name, e) - ) - return True - - # TODO(Shrews): This will eventually need to support tenant ID and - # provider networks, which are admin-level params. - def create_network(self, name, shared=False, admin_state_up=True, - external=False): - """Create a network. - - :param string name: Name of the network being created. - :param bool shared: Set the network as shared. - :param bool admin_state_up: Set the network administrative state to up. - :param bool external: Whether this network is externally accessible. - - :returns: The network object. - :raises: OpenStackCloudException on operation error. - """ - - network = { - 'name': name, - 'shared': shared, - 'admin_state_up': admin_state_up, - 'router:external': external - } - - with self._neutron_exceptions( - "Error creating network {0}".format(name)): - net = self.manager.submitTask( - _tasks.NetworkCreate(body=dict({'network': network}))) - # Turns out neutron returns an actual dict, so no need for the - # use of meta.obj_to_dict() here (which would not work against - # a dict). - return net['network'] - - def delete_network(self, name_or_id): - """Delete a network. - - :param name_or_id: Name or ID of the network being deleted. - - :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. - """ - network = self.get_network(name_or_id) - if not network: - self.log.debug("Network %s not found for deleting" % name_or_id) - return False - - with self._neutron_exceptions( - "Error deleting network {0}".format(name_or_id)): - self.manager.submitTask( - _tasks.NetworkDelete(network=network['id'])) - - return True - - def _build_external_gateway_info(self, ext_gateway_net_id, enable_snat, - ext_fixed_ips): - info = {} - if ext_gateway_net_id: - info['network_id'] = ext_gateway_net_id - if enable_snat is not None: - info['enable_snat'] = enable_snat - if ext_fixed_ips: - info['external_fixed_ips'] = ext_fixed_ips - if info: - return info - return None - - def add_router_interface(self, router, subnet_id=None, port_id=None): - """Attach a subnet to an internal router interface. - - Either a subnet ID or port ID must be specified for the internal - interface. Supplying both will result in an error. - - :param dict router: The dict object of the router being changed - :param string subnet_id: The ID of the subnet to use for the interface - :param string port_id: The ID of the port to use for the interface - - :returns: A dict with the router id (id), subnet ID (subnet_id), - port ID (port_id) and tenant ID (tenant_id). - - :raises: OpenStackCloudException on operation error. - """ - body = {} - if subnet_id: - body['subnet_id'] = subnet_id - if port_id: - body['port_id'] = port_id - - with self._neutron_exceptions( - "Error attaching interface to router {0}".format(router['id']) - ): - return self.manager.submitTask( - _tasks.RouterAddInterface(router=router['id'], body=body) - ) - - def remove_router_interface(self, router, subnet_id=None, port_id=None): - """Detach a subnet from an internal router interface. - - If you specify both subnet and port ID, the subnet ID must - correspond to the subnet ID of the first IP address on the port - specified by the port ID. Otherwise an error occurs. - - :param dict router: The dict object of the router being changed - :param string subnet_id: The ID of the subnet to use for the interface - :param string port_id: The ID of the port to use for the interface - - :returns: None on success - - :raises: OpenStackCloudException on operation error. - """ - body = {} - if subnet_id: - body['subnet_id'] = subnet_id - if port_id: - body['port_id'] = port_id - - with self._neutron_exceptions( - "Error detaching interface from router {0}".format(router['id']) - ): - return self.manager.submitTask( - _tasks.RouterRemoveInterface(router=router['id'], body=body) - ) - - def list_router_interfaces(self, router, interface_type=None): - """List all interfaces for a router. - - :param dict router: A router dict object. - :param string interface_type: One of None, "internal", or "external". - Controls whether all, internal interfaces or external interfaces - are returned. - - :returns: A list of port dict objects. - """ - ports = self.search_ports(filters={'device_id': router['id']}) - - if interface_type: - filtered_ports = [] - ext_fixed = (router['external_gateway_info']['external_fixed_ips'] - if router['external_gateway_info'] - else []) - - # Compare the subnets (subnet_id, ip_address) on the ports with - # the subnets making up the router external gateway. Those ports - # that match are the external interfaces, and those that don't - # are internal. - for port in ports: - matched_ext = False - for port_subnet in port['fixed_ips']: - for router_external_subnet in ext_fixed: - if port_subnet == router_external_subnet: - matched_ext = True - if interface_type == 'internal' and not matched_ext: - filtered_ports.append(port) - elif interface_type == 'external' and matched_ext: - filtered_ports.append(port) - return filtered_ports - - return ports - - def create_router(self, name=None, admin_state_up=True, - ext_gateway_net_id=None, enable_snat=None, - ext_fixed_ips=None): - """Create a logical router. - - :param string name: The router name. - :param bool admin_state_up: The administrative state of the router. - :param string ext_gateway_net_id: Network ID for the external gateway. - :param bool enable_snat: Enable Source NAT (SNAT) attribute. - :param list ext_fixed_ips: - List of dictionaries of desired IP and/or subnet on the - external network. Example:: - - [ - { - "subnet_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b", - "ip_address": "192.168.10.2" - } - ] - - :returns: The router object. - :raises: OpenStackCloudException on operation error. - """ - router = { - 'admin_state_up': admin_state_up - } - if name: - router['name'] = name - ext_gw_info = self._build_external_gateway_info( - ext_gateway_net_id, enable_snat, ext_fixed_ips - ) - if ext_gw_info: - router['external_gateway_info'] = ext_gw_info - - with self._neutron_exceptions( - "Error creating router {0}".format(name)): - new_router = self.manager.submitTask( - _tasks.RouterCreate(body=dict(router=router))) - # Turns out neutron returns an actual dict, so no need for the - # use of meta.obj_to_dict() here (which would not work against - # a dict). - return new_router['router'] - - def update_router(self, name_or_id, name=None, admin_state_up=None, - ext_gateway_net_id=None, enable_snat=None, - ext_fixed_ips=None): - """Update an existing logical router. - - :param string name_or_id: The name or UUID of the router to update. - :param string name: The new router name. - :param bool admin_state_up: The administrative state of the router. - :param string ext_gateway_net_id: - The network ID for the external gateway. - :param bool enable_snat: Enable Source NAT (SNAT) attribute. - :param list ext_fixed_ips: - List of dictionaries of desired IP and/or subnet on the - external network. Example:: - - [ - { - "subnet_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b", - "ip_address": "192.168.10.2" - } - ] - - :returns: The router object. - :raises: OpenStackCloudException on operation error. - """ - router = {} - if name: - router['name'] = name - if admin_state_up is not None: - router['admin_state_up'] = admin_state_up - ext_gw_info = self._build_external_gateway_info( - ext_gateway_net_id, enable_snat, ext_fixed_ips - ) - if ext_gw_info: - router['external_gateway_info'] = ext_gw_info - - if not router: - self.log.debug("No router data to update") - return - - curr_router = self.get_router(name_or_id) - if not curr_router: - raise OpenStackCloudException( - "Router %s not found." % name_or_id) - - with self._neutron_exceptions( - "Error updating router {0}".format(name_or_id)): - new_router = self.manager.submitTask( - _tasks.RouterUpdate( - router=curr_router['id'], body=dict(router=router))) - - # Turns out neutron returns an actual dict, so no need for the - # use of meta.obj_to_dict() here (which would not work against - # a dict). - return new_router['router'] - - def delete_router(self, name_or_id): - """Delete a logical router. - - If a name, instead of a unique UUID, is supplied, it is possible - that we could find more than one matching router since names are - not required to be unique. An error will be raised in this case. - - :param name_or_id: Name or ID of the router being deleted. - - :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. - """ - router = self.get_router(name_or_id) - if not router: - self.log.debug("Router %s not found for deleting" % name_or_id) - return False - - with self._neutron_exceptions( - "Error deleting router {0}".format(name_or_id)): - self.manager.submitTask( - _tasks.RouterDelete(router=router['id'])) - - return True - - def get_image_exclude(self, name_or_id, exclude): - for image in self.search_images(name_or_id): - if exclude: - if exclude not in image.name: - return image - else: - return image - return None - - def get_image_name(self, image_id, exclude=None): - image = self.get_image_exclude(image_id, exclude) - if image: - return image.name - return None - - def get_image_id(self, image_name, exclude=None): - image = self.get_image_exclude(image_name, exclude) - if image: - return image.id - return None - - def create_image_snapshot(self, name, server, **metadata): - image_id = str(self.manager.submitTask(_tasks.ImageSnapshotCreate( - image_name=name, server=server, metadata=metadata))) - self.list_images.invalidate(self) - return self.get_image(image_id) - - def delete_image(self, name_or_id, wait=False, timeout=3600): - image = self.get_image(name_or_id) - try: - # Note that in v1, the param name is image, but in v2, - # it's image_id - glance_api_version = self.cloud_config.get_api_version('image') - if glance_api_version == '2': - self.manager.submitTask( - _tasks.ImageDelete(image_id=image.id)) - elif glance_api_version == '1': - self.manager.submitTask( - _tasks.ImageDelete(image=image.id)) - self.list_images.invalidate(self) - except Exception as e: - raise OpenStackCloudException( - "Error in deleting image: %s" % str(e)) - - if wait: - for count in _utils._iterate_timeout( - timeout, - "Timeout waiting for the image to be deleted."): - self._cache.invalidate() - if self.get_image(image.id) is None: - return - - def create_image( - self, name, filename, container='images', - md5=None, sha256=None, - disk_format=None, container_format=None, - disable_vendor_agent=True, - wait=False, timeout=3600, **kwargs): - - if not disk_format: - disk_format = self.cloud_config.config['image_format'] - if not container_format: - if disk_format == 'vhd': - container_format = 'ovf' - else: - container_format = 'bare' - if not md5 or not sha256: - (md5, sha256) = self._get_file_hashes(filename) - current_image = self.get_image(name) - if (current_image and current_image.get(IMAGE_MD5_KEY, '') == md5 - and current_image.get(IMAGE_SHA256_KEY, '') == sha256): - self.log.debug( - "image {name} exists and is up to date".format(name=name)) - return current_image - kwargs[IMAGE_MD5_KEY] = md5 - kwargs[IMAGE_SHA256_KEY] = sha256 - - if disable_vendor_agent: - kwargs.update(self.cloud_config.config['disable_vendor_agent']) - - # We can never have nice things. Glance v1 took "is_public" as a - # boolean. Glance v2 takes "visibility". If the user gives us - # is_public, we know what they mean. If they give us visibility, they - # know that they mean. - if self.cloud_config.get_api_version('image') == '2': - if 'is_public' in kwargs: - is_public = kwargs.pop('is_public') - if is_public: - kwargs['visibility'] = 'public' - else: - kwargs['visibility'] = 'private' - - try: - # This makes me want to die inside - if self.image_api_use_tasks: - return self._upload_image_task( - name, filename, container, - current_image=current_image, - wait=wait, timeout=timeout, **kwargs) - else: - image_kwargs = dict(properties=kwargs) - if disk_format: - image_kwargs['disk_format'] = disk_format - if container_format: - image_kwargs['container_format'] = container_format - - return self._upload_image_put(name, filename, **image_kwargs) - except OpenStackCloudException: - self.log.debug("Image creation failed", exc_info=True) - raise - except Exception as e: - raise OpenStackCloudException( - "Image creation failed: {message}".format(message=str(e))) - - def _upload_image_put_v2(self, name, image_data, **image_kwargs): - if 'properties' in image_kwargs: - img_props = image_kwargs.pop('properties') - for k, v in iter(img_props.items()): - image_kwargs[k] = str(v) - image = self.manager.submitTask(_tasks.ImageCreate( - name=name, **image_kwargs)) - self.manager.submitTask(_tasks.ImageUpload( - image_id=image.id, image_data=image_data)) - return image - - def _upload_image_put_v1(self, name, image_data, **image_kwargs): - image = self.manager.submitTask(_tasks.ImageCreate( - name=name, **image_kwargs)) - self.manager.submitTask(_tasks.ImageUpdate( - image=image, data=image_data)) - return image - - def _upload_image_put(self, name, filename, **image_kwargs): - image_data = open(filename, 'rb') - # Because reasons and crying bunnies - if self.cloud_config.get_api_version('image') == '2': - image = self._upload_image_put_v2(name, image_data, **image_kwargs) - else: - image = self._upload_image_put_v1(name, image_data, **image_kwargs) - self._cache.invalidate() - return self.get_image(image.id) - - def _upload_image_task( - self, name, filename, container, current_image=None, - wait=True, timeout=None, **image_properties): - self.create_object( - container, name, filename, - md5=image_properties.get('md5', None), - sha256=image_properties.get('sha256', None)) - if not current_image: - current_image = self.get_image(name) - # TODO(mordred): Can we do something similar to what nodepool does - # using glance properties to not delete then upload but instead make a - # new "good" image and then mark the old one as "bad" - # self.glance_client.images.delete(current_image) - task_args = dict( - type='import', input=dict( - import_from='{container}/{name}'.format( - container=container, name=name), - image_properties=dict(name=name))) - glance_task = self.manager.submitTask( - _tasks.ImageTaskCreate(**task_args)) - self.list_images.invalidate(self) - if wait: - image_id = None - for count in _utils._iterate_timeout( - timeout, - "Timeout waiting for the image to import."): - try: - if image_id is None: - status = self.manager.submitTask( - _tasks.ImageTaskGet(task_id=glance_task.id)) - except glanceclient.exc.HTTPServiceUnavailable: - # Intermittent failure - catch and try again - continue - - if status.status == 'success': - image_id = status.result['image_id'] - try: - image = self.get_image(image_id) - except glanceclient.exc.HTTPServiceUnavailable: - # Intermittent failure - catch and try again - continue - if image is None: - continue - self.update_image_properties( - image=image, - **image_properties) - return self.get_image(status.result['image_id']) - if status.status == 'failure': - if status.message == IMAGE_ERROR_396: - glance_task = self.manager.submitTask( - _tasks.ImageTaskCreate(**task_args)) - self.list_images.invalidate(self) - else: - raise OpenStackCloudException( - "Image creation failed: {message}".format( - message=status.message), - extra_data=status) - else: - return meta.warlock_to_dict(glance_task) - - def update_image_properties( - self, image=None, name_or_id=None, **properties): - if image is None: - image = self.get_image(name_or_id) - - img_props = {} - for k, v in iter(properties.items()): - if v and k in ['ramdisk', 'kernel']: - v = self.get_image_id(v) - k = '{0}_id'.format(k) - img_props[k] = v - - # This makes me want to die inside - if self.cloud_config.get_api_version('image') == '2': - return self._update_image_properties_v2(image, img_props) - else: - return self._update_image_properties_v1(image, img_props) - - def _update_image_properties_v2(self, image, properties): - img_props = {} - for k, v in iter(properties.items()): - if image.get(k, None) != v: - img_props[k] = str(v) - if not img_props: - return False - self.manager.submitTask(_tasks.ImageUpdate( - image_id=image.id, **img_props)) - self.list_images.invalidate(self) - return True - - def _update_image_properties_v1(self, image, properties): - img_props = {} - for k, v in iter(properties.items()): - if image.properties.get(k, None) != v: - img_props[k] = v - if not img_props: - return False - self.manager.submitTask(_tasks.ImageUpdate( - image=image, properties=img_props)) - self.list_images.invalidate(self) - return True - - def create_volume(self, wait=True, timeout=None, **kwargs): - """Create a volume. - - :param wait: If true, waits for volume to be created. - :param timeout: Seconds to wait for volume creation. None is forever. - :param volkwargs: Keyword arguments as expected for cinder client. - - :returns: The created volume object. - - :raises: OpenStackCloudTimeout if wait time exceeded. - :raises: OpenStackCloudException on operation error. - """ - - try: - volume = self.manager.submitTask(_tasks.VolumeCreate(**kwargs)) - except Exception as e: - raise OpenStackCloudException( - "Error in creating volume: %s" % str(e)) - self.list_volumes.invalidate(self) - - volume = meta.obj_to_dict(volume) - - if volume['status'] == 'error': - raise OpenStackCloudException("Error in creating volume") - - if wait: - vol_id = volume['id'] - for count in _utils._iterate_timeout( - timeout, - "Timeout waiting for the volume to be available."): - volume = self.get_volume(vol_id) - - if not volume: - continue - - if volume['status'] == 'available': - break - - if volume['status'] == 'error': - raise OpenStackCloudException( - "Error in creating volume, please check logs") - - return volume - - def delete_volume(self, name_or_id=None, wait=True, timeout=None): - """Delete a volume. - - :param name_or_id: Name or unique ID of the volume. - :param wait: If true, waits for volume to be deleted. - :param timeout: Seconds to wait for volume deletion. None is forever. - - :raises: OpenStackCloudTimeout if wait time exceeded. - :raises: OpenStackCloudException on operation error. - """ - - self.list_volumes.invalidate(self) - volume = self.get_volume(name_or_id) - - if not volume: - self.log.debug( - "Volume {name_or_id} does not exist".format( - name_or_id=name_or_id), - exc_info=True) - return False - - try: - self.manager.submitTask( - _tasks.VolumeDelete(volume=volume['id'])) - except Exception as e: - raise OpenStackCloudException( - "Error in deleting volume: %s" % str(e)) - - self.list_volumes.invalidate(self) - if wait: - for count in _utils._iterate_timeout( - timeout, - "Timeout waiting for the volume to be deleted."): - - if not self.get_volume(volume['id']): - break - - return True - - def get_volumes(self, server, cache=True): - volumes = [] - for volume in self.list_volumes(cache=cache): - for attach in volume['attachments']: - if attach['server_id'] == server['id']: - volumes.append(volume) - return volumes - - def get_volume_id(self, name_or_id): - volume = self.get_volume(name_or_id) - if volume: - return volume['id'] - return None - - def volume_exists(self, name_or_id): - return self.get_volume(name_or_id) is not None - - def get_volume_attach_device(self, volume, server_id): - """Return the device name a volume is attached to for a server. - - This can also be used to verify if a volume is attached to - a particular server. - - :param volume: Volume dict - :param server_id: ID of server to check - - :returns: Device name if attached, None if volume is not attached. - """ - for attach in volume['attachments']: - if server_id == attach['server_id']: - return attach['device'] - return None - - def detach_volume(self, server, volume, wait=True, timeout=None): - """Detach a volume from a server. - - :param server: The server dict to detach from. - :param volume: The volume dict to detach. - :param wait: If true, waits for volume to be detached. - :param timeout: Seconds to wait for volume detachment. None is forever. - - :raises: OpenStackCloudTimeout if wait time exceeded. - :raises: OpenStackCloudException on operation error. - """ - dev = self.get_volume_attach_device(volume, server['id']) - if not dev: - raise OpenStackCloudException( - "Volume %s is not attached to server %s" - % (volume['id'], server['id']) - ) - - try: - self.manager.submitTask( - _tasks.VolumeDetach(attachment_id=volume['id'], - server_id=server['id'])) - except Exception as e: - raise OpenStackCloudException( - "Error detaching volume %s from server %s: %s" % - (volume['id'], server['id'], e) - ) - - if wait: - for count in _utils._iterate_timeout( - timeout, - "Timeout waiting for volume %s to detach." % volume['id']): - try: - vol = self.get_volume(volume['id']) - except Exception: - self.log.debug( - "Error getting volume info %s" % volume['id'], - exc_info=True) - continue - - if vol['status'] == 'available': - return - - if vol['status'] == 'error': - raise OpenStackCloudException( - "Error in detaching volume %s" % volume['id'] - ) - - def attach_volume(self, server, volume, device=None, - wait=True, timeout=None): - """Attach a volume to a server. - - This will attach a volume, described by the passed in volume - dict (as returned by get_volume()), to the server described by - the passed in server dict (as returned by get_server()) on the - named device on the server. - - If the volume is already attached to the server, or generally not - available, then an exception is raised. To re-attach to a server, - but under a different device, the user must detach it first. - - :param server: The server dict to attach to. - :param volume: The volume dict to attach. - :param device: The device name where the volume will attach. - :param wait: If true, waits for volume to be attached. - :param timeout: Seconds to wait for volume attachment. None is forever. - - :raises: OpenStackCloudTimeout if wait time exceeded. - :raises: OpenStackCloudException on operation error. - """ - dev = self.get_volume_attach_device(volume, server['id']) - if dev: - raise OpenStackCloudException( - "Volume %s already attached to server %s on device %s" - % (volume['id'], server['id'], dev) - ) - - if volume['status'] != 'available': - raise OpenStackCloudException( - "Volume %s is not available. Status is '%s'" - % (volume['id'], volume['status']) - ) - - try: - vol = self.manager.submitTask( - _tasks.VolumeAttach(volume_id=volume['id'], - server_id=server['id'], - device=device)) - vol = meta.obj_to_dict(vol) - - except Exception as e: - raise OpenStackCloudException( - "Error attaching volume %s to server %s: %s" % - (volume['id'], server['id'], e) - ) - - if wait: - for count in _utils._iterate_timeout( - timeout, - "Timeout waiting for volume %s to attach." % volume['id']): - try: - vol = self.get_volume(volume['id']) - except Exception: - self.log.debug( - "Error getting volume info %s" % volume['id'], - exc_info=True) - continue - - if self.get_volume_attach_device(vol, server['id']): - break - - # TODO(Shrews) check to see if a volume can be in error status - # and also attached. If so, we should move this - # above the get_volume_attach_device call - if vol['status'] == 'error': - raise OpenStackCloudException( - "Error in attaching volume %s" % volume['id'] - ) - return vol - - def create_volume_snapshot(self, volume_id, force=False, - display_name=None, display_description=None, - wait=True, timeout=None): - """Create a volume. - - :param volume_id: the id of the volume to snapshot. - :param force: If set to True the snapshot will be created even if the - volume is attached to an instance, if False it will not - :param display_name: name of the snapshot, one will be generated if - one is not provided - :param display_description: description of the snapshot, one will be - one is not provided - :param wait: If true, waits for volume snapshot to be created. - :param timeout: Seconds to wait for volume snapshot creation. None is - forever. - - :returns: The created volume object. - - :raises: OpenStackCloudTimeout if wait time exceeded. - :raises: OpenStackCloudException on operation error. - """ - try: - snapshot = self.manager.submitTask( - _tasks.VolumeSnapshotCreate( - volume_id=volume_id, force=force, - display_name=display_name, - display_description=display_description) - ) - - except Exception as e: - raise OpenStackCloudException( - "Error creating snapshot of volume %s: %s" % (volume_id, e) - ) - - snapshot = meta.obj_to_dict(snapshot) - - if wait: - snapshot_id = snapshot['id'] - for count in _utils._iterate_timeout( - timeout, - "Timeout waiting for the volume snapshot to be available." - ): - snapshot = self.get_volume_snapshot_by_id(snapshot_id) - - if snapshot['status'] == 'available': - break - - if snapshot['status'] == 'error': - raise OpenStackCloudException( - "Error in creating volume, please check logs") - - return snapshot - - def get_volume_snapshot_by_id(self, snapshot_id): - """Takes a snapshot_id and gets a dict of the snapshot - that maches that id. - - Note: This is more efficient than get_volume_snapshot. - - param: snapshot_id: ID of the volume snapshot. - - """ - try: - snapshot = self.manager.submitTask( - _tasks.VolumeSnapshotGet( - snapshot_id=snapshot_id - ) - ) - - except Exception as e: - raise OpenStackCloudException( - "Error getting snapshot %s: %s" % (snapshot_id, e) - ) - - return meta.obj_to_dict(snapshot) - - def get_volume_snapshot(self, name_or_id, filters=None): - """Get a volume by name or ID. - - :param name_or_id: Name or ID of the volume snapshot. - :param dict filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - :returns: A volume dict or None if no matching volume is - found. - - """ - return _utils._get_entity(self.search_volume_snapshots, name_or_id, - filters) - - def list_volume_snapshots(self, detailed=True, search_opts=None): - """List all volume snapshots. - - :returns: A list of volume snapshots dicts. - - """ - try: - return meta.obj_list_to_dict( - self.manager.submitTask( - _tasks.VolumeSnapshotList(detailed=detailed, - search_opts=search_opts) - ) - ) - - except Exception as e: - raise OpenStackCloudException( - "Error getting a list of snapshots: %s" % e - ) - - def delete_volume_snapshot(self, name_or_id=None, wait=False, - timeout=None): - """Delete a volume snapshot. - - :param name_or_id: Name or unique ID of the volume snapshot. - :param wait: If true, waits for volume snapshot to be deleted. - :param timeout: Seconds to wait for volume snapshot deletion. None is - forever. - - :raises: OpenStackCloudTimeout if wait time exceeded. - :raises: OpenStackCloudException on operation error. - """ - - volumesnapshot = self.get_volume_snapshot(name_or_id) - - if not volumesnapshot: - return False - - try: - self.manager.submitTask( - _tasks.VolumeSnapshotDelete( - snapshot=volumesnapshot['id'] - ) - ) - except Exception as e: - raise OpenStackCloudException( - "Error in deleting volume snapshot: %s" % str(e)) - - if wait: - for count in _utils._iterate_timeout( - timeout, - "Timeout waiting for the volume snapshot to be deleted."): - if not self.get_volume_snapshot(volumesnapshot['id']): - break - - return True - - def get_server_id(self, name_or_id): - server = self.get_server(name_or_id) - if server: - return server['id'] - return None - - def get_server_private_ip(self, server): - return meta.get_server_private_ip(server, self) - - def get_server_public_ip(self, server): - return meta.get_server_external_ipv4(self, server) - - def get_server_meta(self, server): - # TODO(mordred) remove once ansible has moved to Inventory interface - server_vars = meta.get_hostvars_from_server(self, server) - groups = meta.get_groups_from_server(self, server, server_vars) - return dict(server_vars=server_vars, groups=groups) - - def get_openstack_vars(self, server): - return meta.get_hostvars_from_server(self, server) - - def _expand_server_vars(self, server): - # Used by nodepool - # TODO(mordred) remove after these make it into what we - # actually want the API to be. - return meta.expand_server_vars(self, server) - - def available_floating_ip(self, network=None, server=None): - """Get a floating IP from a network or a pool. - - Return the first available floating IP or allocate a new one. - - :param network: Nova pool name or Neutron network name or id. - :param server: Server the IP is for if known - - :returns: a (normalized) structure with a floating IP address - description. - """ - if self.has_service('network'): - try: - f_ips = _utils.normalize_neutron_floating_ips( - self._neutron_available_floating_ips( - network=network, server=server)) - return f_ips[0] - except OpenStackCloudURINotFound as e: - self.log.debug( - "Something went wrong talking to neutron API: " - "'{msg}'. Trying with Nova.".format(msg=str(e))) - # Fall-through, trying with Nova - - f_ips = _utils.normalize_nova_floating_ips( - self._nova_available_floating_ips(pool=network) - ) - return f_ips[0] - - def _neutron_available_floating_ips( - self, network=None, project_id=None, server=None): - """Get a floating IP from a Neutron network. - - Return a list of available floating IPs or allocate a new one and - return it in a list of 1 element. - - :param network: Nova pool name or Neutron network name or id. - :param server: (server) Server the Floating IP is for - - :returns: a list of floating IP addresses. - - :raises: ``OpenStackCloudResourceNotFound``, if an external network - that meets the specified criteria cannot be found. - """ - if project_id is None: - # Make sure we are only listing floatingIPs allocated the current - # tenant. This is the default behaviour of Nova - project_id = self.keystone_session.get_project_id() - - with self._neutron_exceptions("unable to get available floating IPs"): - networks = self.get_external_networks() - if not networks: - raise OpenStackCloudResourceNotFound( - "unable to find an external network") - - filters = { - 'port_id': None, - 'floating_network_id': networks[0]['id'], - 'tenant_id': project_id - - } - floating_ips = self._neutron_list_floating_ips() - available_ips = _utils._filter_list( - floating_ips, name_or_id=None, filters=filters) - if available_ips: - return available_ips - - # No available IP found or we didn't try - # allocate a new Floating IP - f_ip = self._neutron_create_floating_ip( - network_name_or_id=networks[0]['id'], server=server) - - return [f_ip] - - def _nova_available_floating_ips(self, pool=None): - """Get available floating IPs from a floating IP pool. - - Return a list of available floating IPs or allocate a new one and - return it in a list of 1 element. - - :param pool: Nova floating IP pool name. - - :returns: a list of floating IP addresses. - - :raises: ``OpenStackCloudResourceNotFound``, if a floating IP pool - is not specified and cannot be found. - """ - - try: - if pool is None: - pools = self.list_floating_ip_pools() - if not pools: - raise OpenStackCloudResourceNotFound( - "unable to find a floating ip pool") - pool = pools[0]['name'] - - filters = { - 'instance_id': None, - 'pool': pool - } - - floating_ips = self._nova_list_floating_ips() - available_ips = _utils._filter_list( - floating_ips, name_or_id=None, filters=filters) - if available_ips: - return available_ips - - # No available IP found or we did not try. - # Allocate a new Floating IP - f_ip = self._nova_create_floating_ip(pool=pool) - - return [f_ip] - - except Exception as e: - raise OpenStackCloudException( - "unable to create floating IP in pool {pool}: {msg}".format( - pool=pool, msg=str(e))) - - def create_floating_ip(self, network=None, server=None): - """Allocate a new floating IP from a network or a pool. - - :param network: Nova pool name or Neutron network name or id. - :param server: (optional) Server dict for the server to create - the IP for and to which it should be attached - - :returns: a floating IP address - - :raises: ``OpenStackCloudException``, on operation error. - """ - if self.has_service('network'): - try: - f_ips = _utils.normalize_neutron_floating_ips( - [self._neutron_create_floating_ip( - network_name_or_id=network, server=server)] - ) - return f_ips[0] - except OpenStackCloudURINotFound as e: - self.log.debug( - "Something went wrong talking to neutron API: " - "'{msg}'. Trying with Nova.".format(msg=str(e))) - # Fall-through, trying with Nova - - # Else, we are using Nova network - f_ips = _utils.normalize_nova_floating_ips( - [self._nova_create_floating_ip(pool=network)]) - return f_ips[0] - - def _neutron_create_floating_ip( - self, network_name_or_id=None, server=None): - with self._neutron_exceptions( - "unable to create floating IP for net " - "{0}".format(network_name_or_id)): - if network_name_or_id: - networks = [self.get_network(network_name_or_id)] - if not networks: - raise OpenStackCloudResourceNotFound( - "unable to find network for floating ips with id " - "{0}".format(network_name_or_id)) - else: - networks = self.get_external_networks() - if not networks: - raise OpenStackCloudResourceNotFound( - "Unable to find an external network in this cloud" - " which makes getting a floating IP impossible") - kwargs = { - 'floating_network_id': networks[0]['id'], - } - if server: - (port, fixed_address) = self._get_free_fixed_port(server) - if port: - kwargs['port_id'] = port['id'] - kwargs['fixed_ip_address'] = fixed_address - return self.manager.submitTask(_tasks.NeutronFloatingIPCreate( - body={'floatingip': kwargs}))['floatingip'] - - def _nova_create_floating_ip(self, pool=None): - try: - if pool is None: - pools = self.list_floating_ip_pools() - if not pools: - raise OpenStackCloudResourceNotFound( - "unable to find a floating ip pool") - pool = pools[0]['name'] - - pool_ip = self.manager.submitTask( - _tasks.NovaFloatingIPCreate(pool=pool)) - return meta.obj_to_dict(pool_ip) - - except Exception as e: - raise OpenStackCloudException( - "unable to create floating IP in pool {pool}: {msg}".format( - pool=pool, msg=str(e))) - - def delete_floating_ip(self, floating_ip_id): - """Deallocate a floating IP from a tenant. - - :param floating_ip_id: a floating IP address id. - - :returns: True if the IP address has been deleted, False if the IP - address was not found. - - :raises: ``OpenStackCloudException``, on operation error. - """ - if self.has_service('network'): - try: - return self._neutron_delete_floating_ip(floating_ip_id) - except OpenStackCloudURINotFound as e: - self.log.debug( - "Something went wrong talking to neutron API: " - "'{msg}'. Trying with Nova.".format(msg=str(e))) - # Fall-through, trying with Nova - - # Else, we are using Nova network - return self._nova_delete_floating_ip(floating_ip_id) - - def _neutron_delete_floating_ip(self, floating_ip_id): - try: - with self._neutron_exceptions("unable to delete floating IP"): - self.manager.submitTask( - _tasks.NeutronFloatingIPDelete(floatingip=floating_ip_id)) - except OpenStackCloudResourceNotFound: - return False - - return True - - def _nova_delete_floating_ip(self, floating_ip_id): - try: - self.manager.submitTask( - _tasks.NovaFloatingIPDelete(floating_ip=floating_ip_id)) - except nova_exceptions.NotFound: - return False - except Exception as e: - raise OpenStackCloudException( - "unable to delete floating IP id {fip_id}: {msg}".format( - fip_id=floating_ip_id, msg=str(e))) - - return True - - def _attach_ip_to_server( - self, server, floating_ip, - fixed_address=None, wait=False, - timeout=60, skip_attach=False): - """Attach a floating IP to a server. - - :param server: Server dict - :param floating_ip: Floating IP dict to attach - :param fixed_address: (optional) fixed address to which attach the - floating IP to. - :param wait: (optional) Wait for the address to appear as assigned - to the server in Nova. Defaults to False. - :param timeout: (optional) Seconds to wait, defaults to 60. - See the ``wait`` parameter. - :param skip_attach: (optional) Skip the actual attach and just do - the wait. Defaults to False. - - :returns: None - - :raises: OpenStackCloudException, on operation error. - """ - # Short circuit if we're asking to attach an IP that's already - # attached - ext_ip = meta.get_server_ip(server, ext_tag='floating') - if ext_ip == floating_ip['floating_ip_address']: - return - - if self.has_service('network'): - if not skip_attach: - try: - self._neutron_attach_ip_to_server( - server=server, floating_ip=floating_ip, - fixed_address=fixed_address) - except OpenStackCloudURINotFound as e: - self.log.debug( - "Something went wrong talking to neutron API: " - "'{msg}'. Trying with Nova.".format(msg=str(e))) - # Fall-through, trying with Nova - else: - # Nova network - self._nova_attach_ip_to_server( - server_id=server['id'], floating_ip_id=floating_ip['id'], - fixed_address=fixed_address) - - if wait: - # Wait for the address to be assigned to the server - server_id = server['id'] - for _ in _utils._iterate_timeout( - timeout, - "Timeout waiting for the floating IP to be attached."): - server = self.get_server_by_id(server_id) - ext_ip = meta.get_server_ip(server, ext_tag='floating') - if ext_ip == floating_ip['floating_ip_address']: - return - - def _get_free_fixed_port(self, server, fixed_address=None): - ports = self.search_ports(filters={'device_id': server['id']}) - if not ports: - return (None, None) - port = None - if not fixed_address: - if len(ports) > 1: - raise OpenStackCloudException( - "More than one port was found for server {server}" - " and no fixed_address was specified. It is not" - " possible to infer correct behavior. Please specify" - " a fixed_address - or file a bug in shade describing" - " how you think this should work.") - # We're assuming one, because we have no idea what to do with - # more than one. - port = ports[0] - # Select the first available IPv4 address - for address in port.get('fixed_ips', list()): - try: - ip = ipaddress.ip_address(address['ip_address']) - except Exception: - continue - if ip.version == 4: - fixed_address = address['ip_address'] - return port, fixed_address - raise OpenStackCloudException( - "unable to find a free fixed IPv4 address for server " - "{0}".format(server_id)) - # unfortunately a port can have more than one fixed IP: - # we can't use the search_ports filtering for fixed_address as - # they are contained in a list. e.g. - # - # "fixed_ips": [ - # { - # "subnet_id": "008ba151-0b8c-4a67-98b5-0d2b87666062", - # "ip_address": "172.24.4.2" - # } - # ] - # - # Search fixed_address - for p in ports: - for fixed_ip in p['fixed_ips']: - if fixed_address == fixed_ip['ip_address']: - return (p, fixed_address) - return (None, None) - - def _neutron_attach_ip_to_server( - self, server, floating_ip, fixed_address=None): - with self._neutron_exceptions( - "unable to bind a floating ip to server " - "{0}".format(server['id'])): - - # Find an available port - (port, fixed_address) = self._get_free_fixed_port( - server, fixed_address=fixed_address) - if not port: - raise OpenStackCloudException( - "unable to find a port for server {0}".format( - server['id'])) - - floating_ip_args = {'port_id': port['id']} - if fixed_address is not None: - floating_ip_args['fixed_ip_address'] = fixed_address - - return self.manager.submitTask(_tasks.NeutronFloatingIPUpdate( - floatingip=floating_ip['id'], - body={'floatingip': floating_ip_args} - ))['floatingip'] - - def _nova_attach_ip_to_server(self, server_id, floating_ip_id, - fixed_address=None): - try: - f_ip = self.get_floating_ip(id=floating_ip_id) - return self.manager.submitTask(_tasks.NovaFloatingIPAttach( - server=server_id, address=f_ip['floating_ip_address'], - fixed_address=fixed_address)) - except Exception as e: - raise OpenStackCloudException( - "error attaching IP {ip} to instance {id}: {msg}".format( - ip=floating_ip_id, id=server_id, msg=str(e))) - - def detach_ip_from_server(self, server_id, floating_ip_id): - """Detach a floating IP from a server. - - :param server_id: id of a server. - :param floating_ip_id: Id of the floating IP to detach. - - :returns: True if the IP has been detached, or False if the IP wasn't - attached to any server. - - :raises: ``OpenStackCloudException``, on operation error. - """ - if self.has_service('network'): - try: - return self._neutron_detach_ip_from_server( - server_id=server_id, floating_ip_id=floating_ip_id) - except OpenStackCloudURINotFound as e: - self.log.debug( - "Something went wrong talking to neutron API: " - "'{msg}'. Trying with Nova.".format(msg=str(e))) - # Fall-through, trying with Nova - - # Nova network - self._nova_detach_ip_from_server( - server_id=server_id, floating_ip_id=floating_ip_id) - - def _neutron_detach_ip_from_server(self, server_id, floating_ip_id): - with self._neutron_exceptions( - "unable to detach a floating ip from server " - "{0}".format(server_id)): - f_ip = self.get_floating_ip(id=floating_ip_id) - if f_ip is None or not f_ip['attached']: - return False - self.manager.submitTask(_tasks.NeutronFloatingIPUpdate( - floatingip=floating_ip_id, - body={'floatingip': {'port_id': None}})) - - return True - - def _nova_detach_ip_from_server(self, server_id, floating_ip_id): - try: - f_ip = self.get_floating_ip(id=floating_ip_id) - if f_ip is None: - raise OpenStackCloudException( - "unable to find floating IP {0}".format(floating_ip_id)) - self.manager.submitTask(_tasks.NovaFloatingIPDetach( - server=server_id, address=f_ip['floating_ip_address'])) - except nova_exceptions.Conflict as e: - self.log.debug( - "nova floating IP detach failed: {msg}".format(msg=str(e)), - exc_info=True) - return False - except Exception as e: - raise OpenStackCloudException( - "error detaching IP {ip} from instance {id}: {msg}".format( - ip=floating_ip_id, id=server_id, msg=str(e))) - - return True - - def _add_ip_from_pool( - self, server, network, fixed_address=None, reuse=True): - """Add a floating IP to a sever from a given pool - - This method reuses available IPs, when possible, or allocate new IPs - to the current tenant. - The floating IP is attached to the given fixed address or to the - first server port/fixed address - - :param server: Server dict - :param network: Nova pool name or Neutron network name or id. - :param fixed_address: a fixed address - :param reuse: Try to reuse existing ips. Defaults to True. - - :returns: the floating IP assigned - """ - if reuse: - f_ip = self.available_floating_ip(network=network) - else: - f_ip = self.create_floating_ip(network=network) - - self._attach_ip_to_server( - server=server, floating_ip=f_ip, fixed_address=fixed_address) - - return f_ip - - def add_ip_list(self, server, ips): - """Attach a list of IPs to a server. - - :param server: a server object - :param ips: list of IP addresses (floating IPs) - - :returns: None - - :raises: ``OpenStackCloudException``, on operation error. - """ - # ToDo(dguerri): this makes no sense as we cannot attach multiple - # floating IPs to a single fixed_address (this is true for both - # neutron and nova). I will leave this here for the moment as we are - # refactoring floating IPs methods. - for ip in ips: - f_ip = self.get_floating_ip( - id=None, filters={'floating_ip_address': ip}) - self._attach_ip_to_server( - server=server, floating_ip=f_ip) - - def add_auto_ip(self, server, wait=False, timeout=60, reuse=True): - """Add a floating IP to a server. - - This method is intended for basic usage. For advanced network - architecture (e.g. multiple external networks or servers with multiple - interfaces), use other floating IP methods. - - This method can reuse available IPs, or allocate new IPs to the current - project. - - :param server: a server dictionary. - :param reuse: Whether or not to attempt to reuse IPs, defaults - to True. - :param wait: (optional) Wait for the address to appear as assigned - to the server in Nova. Defaults to False. - :param timeout: (optional) Seconds to wait, defaults to 60. - See the ``wait`` parameter. - :param reuse: Try to reuse existing ips. Defaults to True. - - :returns: Floating IP address attached to server. - - """ - skip_attach = False - if reuse: - f_ip = self.available_floating_ip() - else: - f_ip = self.create_floating_ip(server=server) - if server: - # This gets passed in for both nova and neutron - # but is only meaninful for the neutron logic branch - skip_attach = True - - self._attach_ip_to_server( - server=server, floating_ip=f_ip, wait=wait, timeout=timeout, - skip_attach=skip_attach) - - return f_ip - - def add_ips_to_server( - self, server, auto_ip=True, ips=None, ip_pool=None, - wait=False, timeout=60, reuse=True): - if ip_pool: - self._add_ip_from_pool(server, ip_pool, reuse=reuse) - elif ips: - self.add_ip_list(server, ips) - elif auto_ip: - if self.get_server_public_ip(server): - return server - self.add_auto_ip( - server, wait=wait, timeout=timeout, reuse=reuse) - else: - return server - - # this may look redundant, but if there is now a - # floating IP, then it needs to be obtained from - # a recent server object if the above code path exec'd - try: - server = self.get_server_by_id(server['id']) - except Exception as e: - raise OpenStackCloudException( - "Error in getting info from instance: %s " % str(e)) - return server - - def create_server(self, auto_ip=True, ips=None, ip_pool=None, - root_volume=None, terminate_volume=False, - wait=False, timeout=180, reuse_ips=True, - **bootkwargs): - """Create a virtual server instance. - - :returns: A dict representing the created server. - :raises: OpenStackCloudException on operation error. - """ - if root_volume: - if terminate_volume: - suffix = ':::1' - else: - suffix = ':::0' - volume_id = self.get_volume_id(root_volume) + suffix - if 'block_device_mapping' not in bootkwargs: - bootkwargs['block_device_mapping'] = dict() - bootkwargs['block_device_mapping']['vda'] = volume_id - - try: - server = self.manager.submitTask(_tasks.ServerCreate(**bootkwargs)) - # This is a direct get task call to skip the list_servers - # cache which has absolutely no chance of containing the - # new server - server = self.get_server_by_id(server.id) - server_id = server['id'] - except Exception as e: - raise OpenStackCloudException( - "Error in creating instance: {0}".format(e)) - if server.status == 'ERROR': - raise OpenStackCloudException( - "Error in creating the server.") - if wait: - # There is no point in iterating faster than the list_servers cache - for count in _utils._iterate_timeout( - timeout, - "Timeout waiting for the server to come up.", - wait=self._SERVER_LIST_AGE): - try: - # Use the get_server call so that the list_servers - # cache can be leveraged - server = self.get_server(server_id) - except Exception: - continue - if not server: - continue - - server = self.get_active_server( - server=server, reuse=reuse_ips, - auto_ip=auto_ip, ips=ips, ip_pool=ip_pool, - wait=wait, timeout=timeout) - if server: - return server - return server - - def get_active_server( - self, server, auto_ip=True, ips=None, ip_pool=None, - reuse=True, wait=False, timeout=180): - - if server['status'] == 'ERROR': - raise OpenStackCloudException( - "Error in creating the server", extra_data=dict(server=server)) - - if server['status'] == 'ACTIVE': - if 'addresses' in server and server['addresses']: - return self.add_ips_to_server( - server, auto_ip, ips, ip_pool, reuse=reuse, wait=wait) - - self.log.debug( - 'Server {server} reached ACTIVE state without' - ' being allocated an IP address.' - ' Deleting server.'.format(server=server['id'])) - try: - self._delete_server( - server=server, wait=wait, timeout=timeout) - except Exception as e: - raise OpenStackCloudException( - 'Server reached ACTIVE state without being' - ' allocated an IP address AND then could not' - ' be deleted: {0}'.format(e), - extra_data=dict(server=server)) - raise OpenStackCloudException( - 'Server reached ACTIVE state without being' - ' allocated an IP address.', - extra_data=dict(server=server)) - return None - - def rebuild_server(self, server_id, image_id, wait=False, timeout=180): - try: - server = self.manager.submitTask(_tasks.ServerRebuild( - server=server_id, image=image_id)) - except Exception as e: - raise OpenStackCloudException( - "Error in rebuilding instance: {0}".format(e)) - if wait: - for count in _utils._iterate_timeout( - timeout, - "Timeout waiting for server {0} to " - "rebuild.".format(server_id)): - try: - server = self.get_server_by_id(server_id) - except Exception: - continue - - if server['status'] == 'ACTIVE': - return server - - if server['status'] == 'ERROR': - raise OpenStackCloudException( - "Error in rebuilding the server", - extra_data=dict(server=server)) - return meta.obj_to_dict(server) - - def delete_server( - self, name_or_id, wait=False, timeout=180, delete_ips=False): - server = self.get_server(name_or_id) - return self._delete_server( - server, wait=wait, timeout=timeout, delete_ips=delete_ips) - - def _delete_server( - self, server, wait=False, timeout=180, delete_ips=False): - if server: - if delete_ips: - floating_ip = meta.get_server_ip(server, ext_tag='floating') - if floating_ip: - ips = self.search_floating_ips(filters={ - 'floating_ip_address': floating_ip}) - if len(ips) != 1: - raise OpenStackException( - "Tried to delete floating ip {floating_ip}" - " associated with server {id} but there was" - " an error finding it. Something is exceptionally" - " broken.".format( - floating_ip=floating_ip, - id=server['id'])) - self.delete_floating_ip(ips[0]['id']) - try: - self.manager.submitTask( - _tasks.ServerDelete(server=server['id'])) - except nova_exceptions.NotFound: - return - except Exception as e: - raise OpenStackCloudException( - "Error in deleting server: {0}".format(e)) - else: - return - if not wait: - return - for count in _utils._iterate_timeout( - timeout, - "Timed out waiting for server to get deleted."): - try: - server = self.get_server_by_id(server['id']) - if not server: - return - server = meta.obj_to_dict(server) - except nova_exceptions.NotFound: - return - except Exception as e: - raise OpenStackCloudException( - "Error in deleting server: {0}".format(e)) - - def list_containers(self): - try: - return meta.obj_to_dict(self.manager.submitTask( - _tasks.ContainerList())) - except swift_exceptions.ClientException as e: - raise OpenStackCloudException( - "Container list failed: %s (%s/%s)" % ( - e.http_reason, e.http_host, e.http_path)) - - def get_container(self, name, skip_cache=False): - if skip_cache or name not in self._container_cache: - try: - container = self.manager.submitTask( - _tasks.ContainerGet(container=name)) - self._container_cache[name] = container - except swift_exceptions.ClientException as e: - if e.http_status == 404: - return None - raise OpenStackCloudException( - "Container fetch failed: %s (%s/%s)" % ( - e.http_reason, e.http_host, e.http_path)) - return self._container_cache[name] - - def create_container(self, name, public=False): - container = self.get_container(name) - if container: - return container - try: - self.manager.submitTask( - _tasks.ContainerCreate(container=name)) - if public: - self.set_container_access(name, 'public') - return self.get_container(name, skip_cache=True) - except swift_exceptions.ClientException as e: - raise OpenStackCloudException( - "Container creation failed: %s (%s/%s)" % ( - e.http_reason, e.http_host, e.http_path)) - - def delete_container(self, name): - try: - self.manager.submitTask( - _tasks.ContainerDelete(container=name)) - except swift_exceptions.ClientException as e: - if e.http_status == 404: - return - raise OpenStackCloudException( - "Container deletion failed: %s (%s/%s)" % ( - e.http_reason, e.http_host, e.http_path)) - - def update_container(self, name, headers): - try: - self.manager.submitTask( - _tasks.ContainerUpdate(container=name, headers=headers)) - except swift_exceptions.ClientException as e: - raise OpenStackCloudException( - "Container update failed: %s (%s/%s)" % ( - e.http_reason, e.http_host, e.http_path)) - - def set_container_access(self, name, access): - if access not in OBJECT_CONTAINER_ACLS: - raise OpenStackCloudException( - "Invalid container access specified: %s. Must be one of %s" - % (access, list(OBJECT_CONTAINER_ACLS.keys()))) - header = {'x-container-read': OBJECT_CONTAINER_ACLS[access]} - self.update_container(name, header) - - def get_container_access(self, name): - container = self.get_container(name, skip_cache=True) - if not container: - raise OpenStackCloudException("Container not found: %s" % name) - acl = container.get('x-container-read', '') - try: - return [p for p, a in OBJECT_CONTAINER_ACLS.items() - if acl == a].pop() - except IndexError: - raise OpenStackCloudException( - "Could not determine container access for ACL: %s." % acl) - - def _get_file_hashes(self, filename): - if filename not in self._file_hash_cache: - md5 = hashlib.md5() - sha256 = hashlib.sha256() - with open(filename, 'rb') as file_obj: - for chunk in iter(lambda: file_obj.read(8192), b''): - md5.update(chunk) - sha256.update(chunk) - self._file_hash_cache[filename] = dict( - md5=md5.hexdigest(), sha256=sha256.hexdigest()) - return (self._file_hash_cache[filename]['md5'], - self._file_hash_cache[filename]['sha256']) - - @_cache_on_arguments() - def get_object_capabilities(self): - return self.manager.submitTask(_tasks.ObjectCapabilities()) - - def get_object_segment_size(self, segment_size): - '''get a segment size that will work given capabilities''' - if segment_size is None: - segment_size = DEFAULT_OBJECT_SEGMENT_SIZE - try: - caps = self.get_object_capabilities() - except swift_exceptions.ClientException as e: - if e.http_status == 412: - server_max_file_size = DEFAULT_MAX_FILE_SIZE - self.log.info( - "Swift capabilities not supported. " - "Using default max file size.") - else: - raise OpenStackCloudException( - "Could not determine capabilities") - else: - server_max_file_size = caps.get('swift', {}).get('max_file_size', - 0) - - if segment_size > server_max_file_size: - return server_max_file_size - return segment_size - - def is_object_stale( - self, container, name, filename, file_md5=None, file_sha256=None): - - metadata = self.get_object_metadata(container, name) - if not metadata: - self.log.debug( - "swift stale check, no object: {container}/{name}".format( - container=container, name=name)) - return True - - if file_md5 is None or file_sha256 is None: - (file_md5, file_sha256) = self._get_file_hashes(filename) - - if metadata.get(OBJECT_MD5_KEY, '') != file_md5: - self.log.debug( - "swift md5 mismatch: {filename}!={container}/{name}".format( - filename=filename, container=container, name=name)) - return True - if metadata.get(OBJECT_SHA256_KEY, '') != file_sha256: - self.log.debug( - "swift sha256 mismatch: {filename}!={container}/{name}".format( - filename=filename, container=container, name=name)) - return True - - self.log.debug( - "swift object up to date: {container}/{name}".format( - container=container, name=name)) - return False - - def create_object( - self, container, name, filename=None, - md5=None, sha256=None, segment_size=None, - **headers): - """Create a file object - - :param container: The name of the container to store the file in. - This container will be created if it does not exist already. - :param name: Name for the object within the container. - :param filename: The path to the local file whose contents will be - uploaded. - :param md5: A hexadecimal md5 of the file. (Optional), if it is known - and can be passed here, it will save repeating the expensive md5 - process. It is assumed to be accurate. - :param sha256: A hexadecimal sha256 of the file. (Optional) See md5. - :param segment_size: Break the uploaded object into segments of this - many bytes. (Optional) Shade will attempt to discover the maximum - value for this from the server if it is not specified, or will use - a reasonable default. - :param headers: These will be passed through to the object creation - API as HTTP Headers. - - :raises: ``OpenStackCloudException`` on operation error. - """ - if not filename: - filename = name - - segment_size = self.get_object_segment_size(segment_size) - - if not md5 or not sha256: - (md5, sha256) = self._get_file_hashes(filename) - headers[OBJECT_MD5_KEY] = md5 - headers[OBJECT_SHA256_KEY] = sha256 - header_list = sorted([':'.join([k, v]) for (k, v) in headers.items()]) - - # On some clouds this is not necessary. On others it is. I'm confused. - self.create_container(container) - - if self.is_object_stale(container, name, filename, md5, sha256): - self.log.debug( - "swift uploading {filename} to {container}/{name}".format( - filename=filename, container=container, name=name)) - upload = swift_service.SwiftUploadObject(source=filename, - object_name=name) - for r in self.manager.submitTask(_tasks.ObjectCreate( - container=container, objects=[upload], - options=dict(header=header_list, - segment_size=segment_size))): - if not r['success']: - raise OpenStackCloudException( - 'Failed at action ({action}) [{error}]:'.format(**r)) - - def list_objects(self, container): - try: - return meta.obj_to_dict(self.manager.submitTask( - _tasks.ObjectList(container))) - except swift_exceptions.ClientException as e: - raise OpenStackCloudException( - "Object list failed: %s (%s/%s)" % ( - e.http_reason, e.http_host, e.http_path)) - - def delete_object(self, container, name): - if not self.get_object_metadata(container, name): - return - try: - self.manager.submitTask(_tasks.ObjectDelete( - container=container, obj=name)) - except swift_exceptions.ClientException as e: - raise OpenStackCloudException( - "Object deletion failed: %s (%s/%s)" % ( - e.http_reason, e.http_host, e.http_path)) - - def get_object_metadata(self, container, name): - try: - return self.manager.submitTask(_tasks.ObjectMetadata( - container=container, obj=name)) - except swift_exceptions.ClientException as e: - if e.http_status == 404: - return None - raise OpenStackCloudException( - "Object metadata fetch failed: %s (%s/%s)" % ( - e.http_reason, e.http_host, e.http_path)) - - def create_subnet(self, network_name_or_id, cidr, ip_version=4, - enable_dhcp=False, subnet_name=None, tenant_id=None, - allocation_pools=None, gateway_ip=None, - dns_nameservers=None, host_routes=None, - ipv6_ra_mode=None, ipv6_address_mode=None): - """Create a subnet on a specified network. - - :param string network_name_or_id: - The unique name or ID of the attached network. If a non-unique - name is supplied, an exception is raised. - :param string cidr: - The CIDR. - :param int ip_version: - The IP version, which is 4 or 6. - :param bool enable_dhcp: - Set to ``True`` if DHCP is enabled and ``False`` if disabled. - Default is ``False``. - :param string subnet_name: - The name of the subnet. - :param string tenant_id: - The ID of the tenant who owns the network. Only administrative users - can specify a tenant ID other than their own. - :param list allocation_pools: - A list of dictionaries of the start and end addresses for the - allocation pools. For example:: - - [ - { - "start": "192.168.199.2", - "end": "192.168.199.254" - } - ] - - :param string gateway_ip: - The gateway IP address. When you specify both allocation_pools and - gateway_ip, you must ensure that the gateway IP does not overlap - with the specified allocation pools. - :param list dns_nameservers: - A list of DNS name servers for the subnet. For example:: - - [ "8.8.8.7", "8.8.8.8" ] - - :param list host_routes: - A list of host route dictionaries for the subnet. For example:: - - [ - { - "destination": "0.0.0.0/0", - "nexthop": "123.456.78.9" - }, - { - "destination": "192.168.0.0/24", - "nexthop": "192.168.0.1" - } - ] - - :param string ipv6_ra_mode: - IPv6 Router Advertisement mode. Valid values are: 'dhcpv6-stateful', - 'dhcpv6-stateless', or 'slaac'. - :param string ipv6_address_mode: - IPv6 address mode. Valid values are: 'dhcpv6-stateful', - 'dhcpv6-stateless', or 'slaac'. - - :returns: The new subnet object. - :raises: OpenStackCloudException on operation error. - """ - - network = self.get_network(network_name_or_id) - if not network: - raise OpenStackCloudException( - "Network %s not found." % network_name_or_id) - - # The body of the neutron message for the subnet we wish to create. - # This includes attributes that are required or have defaults. - subnet = { - 'network_id': network['id'], - 'cidr': cidr, - 'ip_version': ip_version, - 'enable_dhcp': enable_dhcp - } - - # Add optional attributes to the message. - if subnet_name: - subnet['name'] = subnet_name - if tenant_id: - subnet['tenant_id'] = tenant_id - if allocation_pools: - subnet['allocation_pools'] = allocation_pools - if gateway_ip: - subnet['gateway_ip'] = gateway_ip - if dns_nameservers: - subnet['dns_nameservers'] = dns_nameservers - if host_routes: - subnet['host_routes'] = host_routes - if ipv6_ra_mode: - subnet['ipv6_ra_mode'] = ipv6_ra_mode - if ipv6_address_mode: - subnet['ipv6_address_mode'] = ipv6_address_mode - - with self._neutron_exceptions( - "Error creating subnet on network " - "{0}".format(network_name_or_id)): - new_subnet = self.manager.submitTask( - _tasks.SubnetCreate(body=dict(subnet=subnet))) - - return new_subnet['subnet'] - - def delete_subnet(self, name_or_id): - """Delete a subnet. - - If a name, instead of a unique UUID, is supplied, it is possible - that we could find more than one matching subnet since names are - not required to be unique. An error will be raised in this case. - - :param name_or_id: Name or ID of the subnet being deleted. - - :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. - """ - subnet = self.get_subnet(name_or_id) - if not subnet: - self.log.debug("Subnet %s not found for deleting" % name_or_id) - return False - - with self._neutron_exceptions( - "Error deleting subnet {0}".format(name_or_id)): - self.manager.submitTask( - _tasks.SubnetDelete(subnet=subnet['id'])) - return True - - def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, - gateway_ip=None, allocation_pools=None, - dns_nameservers=None, host_routes=None): - """Update an existing subnet. - - :param string name_or_id: - Name or ID of the subnet to update. - :param string subnet_name: - The new name of the subnet. - :param bool enable_dhcp: - Set to ``True`` if DHCP is enabled and ``False`` if disabled. - :param string gateway_ip: - The gateway IP address. When you specify both allocation_pools and - gateway_ip, you must ensure that the gateway IP does not overlap - with the specified allocation pools. - :param list allocation_pools: - A list of dictionaries of the start and end addresses for the - allocation pools. For example:: - - [ - { - "start": "192.168.199.2", - "end": "192.168.199.254" - } - ] - - :param list dns_nameservers: - A list of DNS name servers for the subnet. For example:: - - [ "8.8.8.7", "8.8.8.8" ] - - :param list host_routes: - A list of host route dictionaries for the subnet. For example:: - - [ - { - "destination": "0.0.0.0/0", - "nexthop": "123.456.78.9" - }, - { - "destination": "192.168.0.0/24", - "nexthop": "192.168.0.1" - } - ] - - :returns: The updated subnet object. - :raises: OpenStackCloudException on operation error. - """ - subnet = {} - if subnet_name: - subnet['name'] = subnet_name - if enable_dhcp is not None: - subnet['enable_dhcp'] = enable_dhcp - if gateway_ip: - subnet['gateway_ip'] = gateway_ip - if allocation_pools: - subnet['allocation_pools'] = allocation_pools - if dns_nameservers: - subnet['dns_nameservers'] = dns_nameservers - if host_routes: - subnet['host_routes'] = host_routes - - if not subnet: - self.log.debug("No subnet data to update") - return - - curr_subnet = self.get_subnet(name_or_id) - if not curr_subnet: - raise OpenStackCloudException( - "Subnet %s not found." % name_or_id) - - with self._neutron_exceptions( - "Error updating subnet {0}".format(name_or_id)): - new_subnet = self.manager.submitTask( - _tasks.SubnetUpdate( - subnet=curr_subnet['id'], body=dict(subnet=subnet))) - # Turns out neutron returns an actual dict, so no need for the - # use of meta.obj_to_dict() here (which would not work against - # a dict). - return new_subnet['subnet'] - - @_utils.valid_kwargs('name', 'admin_state_up', 'mac_address', 'fixed_ips', - 'subnet_id', 'ip_address', 'security_groups', - 'allowed_address_pairs', 'extra_dhcp_opts', - 'device_owner', 'device_id') - def create_port(self, network_id, **kwargs): - """Create a port - - :param network_id: The ID of the network. (Required) - :param name: A symbolic name for the port. (Optional) - :param admin_state_up: The administrative status of the port, - which is up (true, default) or down (false). (Optional) - :param mac_address: The MAC address. (Optional) - :param fixed_ips: List of ip_addresses and subnet_ids. See subnet_id - and ip_address. (Optional) - For example:: - - [ - { - "ip_address": "10.29.29.13", - "subnet_id": "a78484c4-c380-4b47-85aa-21c51a2d8cbd" - }, ... - ] - :param subnet_id: If you specify only a subnet ID, OpenStack Networking - allocates an available IP from that subnet to the port. (Optional) - If you specify both a subnet ID and an IP address, OpenStack - Networking tries to allocate the specified address to the port. - :param ip_address: If you specify both a subnet ID and an IP address, - OpenStack Networking tries to allocate the specified address to - the port. - :param security_groups: List of security group UUIDs. (Optional) - :param allowed_address_pairs: Allowed address pairs list (Optional) - For example:: - - [ - { - "ip_address": "23.23.23.1", - "mac_address": "fa:16:3e:c4:cd:3f" - }, ... - ] - :param extra_dhcp_opts: Extra DHCP options. (Optional). - For example:: - - [ - { - "opt_name": "opt name1", - "opt_value": "value1" - }, ... - ] - :param device_owner: The ID of the entity that uses this port. - For example, a DHCP agent. (Optional) - :param device_id: The ID of the device that uses this port. - For example, a virtual server. (Optional) - - :returns: a dictionary describing the created port. - - :raises: ``OpenStackCloudException`` on operation error. - """ - kwargs['network_id'] = network_id - - with self._neutron_exceptions( - "Error creating port for network {0}".format(network_id)): - return self.manager.submitTask( - _tasks.PortCreate(body={'port': kwargs}))['port'] - - @_utils.valid_kwargs('name', 'admin_state_up', 'fixed_ips', - 'security_groups', 'allowed_address_pairs', - 'extra_dhcp_opts', 'device_owner') - def update_port(self, name_or_id, **kwargs): - """Update a port - - Note: to unset an attribute use None value. To leave an attribute - untouched just omit it. - - :param name_or_id: name or id of the port to update. (Required) - :param name: A symbolic name for the port. (Optional) - :param admin_state_up: The administrative status of the port, - which is up (true) or down (false). (Optional) - :param fixed_ips: List of ip_addresses and subnet_ids. (Optional) - If you specify only a subnet ID, OpenStack Networking allocates - an available IP from that subnet to the port. - If you specify both a subnet ID and an IP address, OpenStack - Networking tries to allocate the specified address to the port. - For example:: - - [ - { - "ip_address": "10.29.29.13", - "subnet_id": "a78484c4-c380-4b47-85aa-21c51a2d8cbd" - }, ... - ] - :param security_groups: List of security group UUIDs. (Optional) - :param allowed_address_pairs: Allowed address pairs list (Optional) - For example:: - - [ - { - "ip_address": "23.23.23.1", - "mac_address": "fa:16:3e:c4:cd:3f" - }, ... - ] - :param extra_dhcp_opts: Extra DHCP options. (Optional). - For example:: - - [ - { - "opt_name": "opt name1", - "opt_value": "value1" - }, ... - ] - :param device_owner: The ID of the entity that uses this port. - For example, a DHCP agent. (Optional) - - :returns: a dictionary describing the updated port. - - :raises: OpenStackCloudException on operation error. - """ - port = self.get_port(name_or_id=name_or_id) - if port is None: - raise OpenStackCloudException( - "failed to find port '{port}'".format(port=name_or_id)) - - with self._neutron_exceptions( - "Error updating port {0}".format(name_or_id)): - return self.manager.submitTask( - _tasks.PortUpdate( - port=port['id'], body={'port': kwargs}))['port'] - - def delete_port(self, name_or_id): - """Delete a port - - :param name_or_id: id or name of the port to delete. - - :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. - """ - port = self.get_port(name_or_id=name_or_id) - if port is None: - self.log.debug("Port %s not found for deleting" % name_or_id) - return False - - with self._neutron_exceptions( - "Error deleting port {0}".format(name_or_id)): - self.manager.submitTask(_tasks.PortDelete(port=port['id'])) - return True - - def create_security_group(self, name, description): - """Create a new security group - - :param string name: A name for the security group. - :param string description: Describes the security group. - - :returns: A dict representing the new security group. - - :raises: OpenStackCloudException on operation error. - :raises: OpenStackCloudUnavailableFeature if security groups are - not supported on this cloud. - """ - if self.secgroup_source == 'neutron': - with self._neutron_exceptions( - "Error creating security group {0}".format(name)): - group = self.manager.submitTask( - _tasks.NeutronSecurityGroupCreate( - body=dict(security_group=dict(name=name, - description=description)) - ) - ) - return group['security_group'] - - elif self.secgroup_source == 'nova': - try: - group = meta.obj_to_dict( - self.manager.submitTask( - _tasks.NovaSecurityGroupCreate( - name=name, description=description - ) - ) - ) - except Exception as e: - raise OpenStackCloudException( - "failed to create security group '{name}': {msg}".format( - name=name, msg=str(e))) - return _utils.normalize_nova_secgroups([group])[0] - - # Security groups not supported - else: - raise OpenStackCloudUnavailableFeature( - "Unavailable feature: security groups" - ) - - def delete_security_group(self, name_or_id): - """Delete a security group - - :param string name_or_id: The name or unique ID of the security group. - - :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. - :raises: OpenStackCloudUnavailableFeature if security groups are - not supported on this cloud. - """ - secgroup = self.get_security_group(name_or_id) - if secgroup is None: - self.log.debug('Security group %s not found for deleting' % - name_or_id) - return False - - if self.secgroup_source == 'neutron': - with self._neutron_exceptions( - "Error deleting security group {0}".format(name_or_id)): - self.manager.submitTask( - _tasks.NeutronSecurityGroupDelete( - security_group=secgroup['id'] - ) - ) - return True - - elif self.secgroup_source == 'nova': - try: - self.manager.submitTask( - _tasks.NovaSecurityGroupDelete(group=secgroup['id']) - ) - except Exception as e: - raise OpenStackCloudException( - "failed to delete security group '{group}': {msg}".format( - group=name_or_id, msg=str(e))) - return True - - # Security groups not supported - else: - raise OpenStackCloudUnavailableFeature( - "Unavailable feature: security groups" - ) - - @_utils.valid_kwargs('name', 'description') - def update_security_group(self, name_or_id, **kwargs): - """Update a security group - - :param string name_or_id: Name or ID of the security group to update. - :param string name: New name for the security group. - :param string description: New description for the security group. - - :returns: A dictionary describing the updated security group. - - :raises: OpenStackCloudException on operation error. - """ - secgroup = self.get_security_group(name_or_id) - - if secgroup is None: - raise OpenStackCloudException( - "Security group %s not found." % name_or_id) - - if self.secgroup_source == 'neutron': - with self._neutron_exceptions( - "Error updating security group {0}".format(name_or_id)): - group = self.manager.submitTask( - _tasks.NeutronSecurityGroupUpdate( - security_group=secgroup['id'], - body={'security_group': kwargs}) - ) - return group['security_group'] - - elif self.secgroup_source == 'nova': - try: - group = meta.obj_to_dict( - self.manager.submitTask( - _tasks.NovaSecurityGroupUpdate( - group=secgroup['id'], **kwargs) - ) - ) - except Exception as e: - raise OpenStackCloudException( - "failed to update security group '{group}': {msg}".format( - group=name_or_id, msg=str(e))) - return _utils.normalize_nova_secgroups([group])[0] - - # Security groups not supported - else: - raise OpenStackCloudUnavailableFeature( - "Unavailable feature: security groups" - ) - - def create_security_group_rule(self, - secgroup_name_or_id, - port_range_min=None, - port_range_max=None, - protocol=None, - remote_ip_prefix=None, - remote_group_id=None, - direction='ingress', - ethertype='IPv4'): - """Create a new security group rule - - :param string secgroup_name_or_id: - The security group name or ID to associate with this security - group rule. If a non-unique group name is given, an exception - is raised. - :param int port_range_min: - The minimum port number in the range that is matched by the - security group rule. If the protocol is TCP or UDP, this value - must be less than or equal to the port_range_max attribute value. - If nova is used by the cloud provider for security groups, then - a value of None will be transformed to -1. - :param int port_range_max: - The maximum port number in the range that is matched by the - security group rule. The port_range_min attribute constrains the - port_range_max attribute. If nova is used by the cloud provider - for security groups, then a value of None will be transformed - to -1. - :param string protocol: - The protocol that is matched by the security group rule. Valid - values are None, tcp, udp, and icmp. - :param string remote_ip_prefix: - The remote IP prefix to be associated with this security group - rule. This attribute matches the specified IP prefix as the - source IP address of the IP packet. - :param string remote_group_id: - The remote group ID to be associated with this security group - rule. - :param string direction: - Ingress or egress: The direction in which the security group - rule is applied. For a compute instance, an ingress security - group rule is applied to incoming (ingress) traffic for that - instance. An egress rule is applied to traffic leaving the - instance. - :param string ethertype: - Must be IPv4 or IPv6, and addresses represented in CIDR must - match the ingress or egress rules. - - :returns: A dict representing the new security group rule. - - :raises: OpenStackCloudException on operation error. - """ - - secgroup = self.get_security_group(secgroup_name_or_id) - if not secgroup: - raise OpenStackCloudException( - "Security group %s not found." % secgroup_name_or_id) - - if self.secgroup_source == 'neutron': - # NOTE: Nova accepts -1 port numbers, but Neutron accepts None - # as the equivalent value. - rule_def = { - 'security_group_id': secgroup['id'], - 'port_range_min': - None if port_range_min == -1 else port_range_min, - 'port_range_max': - None if port_range_max == -1 else port_range_max, - 'protocol': protocol, - 'remote_ip_prefix': remote_ip_prefix, - 'remote_group_id': remote_group_id, - 'direction': direction, - 'ethertype': ethertype - } - - with self._neutron_exceptions( - "Error creating security group rule"): - rule = self.manager.submitTask( - _tasks.NeutronSecurityGroupRuleCreate( - body={'security_group_rule': rule_def}) - ) - return rule['security_group_rule'] - - elif self.secgroup_source == 'nova': - # NOTE: Neutron accepts None for protocol. Nova does not. - if protocol is None: - raise OpenStackCloudException('Protocol must be specified') - - if direction == 'egress': - self.log.debug( - 'Rule creation failed: Nova does not support egress rules' - ) - raise OpenStackCloudException('No support for egress rules') - - # NOTE: Neutron accepts None for ports, but Nova requires -1 - # as the equivalent value for ICMP. - # - # For TCP/UDP, if both are None, Neutron allows this and Nova - # represents this as all ports (1-65535). Nova does not accept - # None values, so to hide this difference, we will automatically - # convert to the full port range. If only a single port value is - # specified, it will error as normal. - if protocol == 'icmp': - if port_range_min is None: - port_range_min = -1 - if port_range_max is None: - port_range_max = -1 - elif protocol in ['tcp', 'udp']: - if port_range_min is None and port_range_max is None: - port_range_min = 1 - port_range_max = 65535 - - try: - rule = meta.obj_to_dict( - self.manager.submitTask( - _tasks.NovaSecurityGroupRuleCreate( - parent_group_id=secgroup['id'], - ip_protocol=protocol, - from_port=port_range_min, - to_port=port_range_max, - cidr=remote_ip_prefix, - group_id=remote_group_id - ) - ) - ) - except Exception as e: - raise OpenStackCloudException( - "failed to create security group rule: {msg}".format( - msg=str(e))) - return _utils.normalize_nova_secgroup_rules([rule])[0] - - # Security groups not supported - else: - raise OpenStackCloudUnavailableFeature( - "Unavailable feature: security groups" - ) - - def delete_security_group_rule(self, rule_id): - """Delete a security group rule - - :param string rule_id: The unique ID of the security group rule. - - :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. - :raises: OpenStackCloudUnavailableFeature if security groups are - not supported on this cloud. - """ - - if self.secgroup_source == 'neutron': - try: - with self._neutron_exceptions( - "Error deleting security group rule " - "{0}".format(rule_id)): - self.manager.submitTask( - _tasks.NeutronSecurityGroupRuleDelete( - security_group_rule=rule_id) - ) - except OpenStackCloudResourceNotFound: - return False - return True - - elif self.secgroup_source == 'nova': - try: - self.manager.submitTask( - _tasks.NovaSecurityGroupRuleDelete(rule=rule_id) - ) - except nova_exceptions.NotFound: - return False - except Exception as e: - raise OpenStackCloudException( - "failed to delete security group rule {id}: {msg}".format( - id=rule_id, msg=str(e))) - return True - - # Security groups not supported - else: - raise OpenStackCloudUnavailableFeature( - "Unavailable feature: security groups" - ) - - -class OperatorCloud(OpenStackCloud): - """Represent a privileged/operator connection to an OpenStack Cloud. - - `OperatorCloud` is the entry point for all admin operations, regardless - of which OpenStack service those operations are for. - - See the :class:`OpenStackCloud` class for a description of most options. - """ - - # Set the ironic API microversion to a known-good - # supported/tested with the contents of shade. - # - # Note(TheJulia): Defaulted to version 1.6 as the ironic - # state machine changes which will increment the version - # and break an automatic transition of an enrolled node - # to an available state. Locking the version is intended - # to utilize the original transition until shade supports - # calling for node inspection to allow the transition to - # take place automatically. - ironic_api_microversion = '1.6' - - @property - def ironic_client(self): - if self._ironic_client is None: - self._ironic_client = self._get_client( - 'baremetal', ironic_client.Client, - os_ironic_api_version=self.ironic_api_microversion) - return self._ironic_client - - def list_nics(self): - try: - return meta.obj_list_to_dict( - self.manager.submitTask(_tasks.MachinePortList()) - ) - except Exception as e: - raise OpenStackCloudException( - "Error fetching machine port list: %s" % e) - - def list_nics_for_machine(self, uuid): - try: - return meta.obj_list_to_dict( - self.manager.submitTask( - _tasks.MachineNodePortList(node_id=uuid)) - ) - except Exception as e: - raise OpenStackCloudException( - "Error fetching port list for node %s: %s" % (uuid, e)) - - def get_nic_by_mac(self, mac): - try: - return meta.obj_to_dict( - self.manager.submitTask( - _tasks.MachineNodePortGet(port_id=mac)) - ) - except ironic_exceptions.ClientException: - return None - - def list_machines(self): - return meta.obj_list_to_dict( - self.manager.submitTask(_tasks.MachineNodeList()) - ) - - def get_machine(self, name_or_id): - """Get Machine by name or uuid - - Search the baremetal host out by utilizing the supplied id value - which can consist of a name or UUID. - - :param name_or_id: A node name or UUID that will be looked up. - - :returns: Dictonary representing the node found or None if no nodes - are found. - """ - try: - return meta.obj_to_dict( - self.manager.submitTask( - _tasks.MachineNodeGet(node_id=name_or_id)) - ) - except ironic_exceptions.ClientException: - return None - - def get_machine_by_mac(self, mac): - """Get machine by port MAC address - - :param mac: Port MAC address to query in order to return a node. - - :returns: Dictonary representing the node found or None - if the node is not found. - """ - try: - port = self.manager.submitTask( - _tasks.MachinePortGetByAddress(address=mac)) - return meta.obj_to_dict( - self.manager.submitTask( - _tasks.MachineNodeGet(node_id=port.node_uuid)) - ) - except ironic_exceptions.ClientException: - return None - - def inspect_machine(self, name_or_id, wait=False, timeout=3600): - """Inspect a Barmetal machine - - Engages the Ironic node inspection behavior in order to collect - metadata about the baremetal machine. - - :param name_or_id: String representing machine name or UUID value in - order to identify the machine. - - :param wait: Boolean value controlling if the method is to wait for - the desired state to be reached or a failure to occur. - - :param timeout: Integer value, defautling to 3600 seconds, for the$ - wait state to reach completion. - - :returns: Dictonary representing the current state of the machine - upon exit of the method. - """ - - return_to_available = False - - machine = self.get_machine(name_or_id) - if not machine: - raise OpenStackCloudException( - "Machine inspection failed to find: %s." % name_or_id) - - # NOTE(TheJulia): If in available state, we can do this, however - # We need to to move the host back to m - if "available" in machine['provision_state']: - return_to_available = True - # NOTE(TheJulia): Changing available machine to managedable state - # and due to state transitions we need to until that transition has - # completd. - self.node_set_provision_state(machine['uuid'], 'manage', - wait=True, timeout=timeout) - elif ("manage" not in machine['provision_state'] and - "inspect failed" not in machine['provision_state']): - raise OpenStackCloudException( - "Machine must be in 'manage' or 'available' state to " - "engage inspection: Machine: %s State: %s" - % (machine['uuid'], machine['provision_state'])) - try: - machine = self.node_set_provision_state(machine['uuid'], 'inspect') - if wait: - for count in _utils._iterate_timeout( - timeout, - "Timeout waiting for node transition to " - "target state of 'inpection'"): - machine = self.get_machine(name_or_id) - - if "inspect failed" in machine['provision_state']: - raise OpenStackCloudException( - "Inspection of node %s failed, last error: %s" - % (machine['uuid'], machine['last_error'])) - - if "manageable" in machine['provision_state']: - break - - if return_to_available: - machine = self.node_set_provision_state( - machine['uuid'], 'provide', wait=wait, timeout=timeout) - - return(machine) - - except Exception as e: - raise OpenStackCloudException( - "Error inspecting machine: %s" % e) - - def register_machine(self, nics, wait=False, timeout=3600, - lock_timeout=600, **kwargs): - """Register Baremetal with Ironic - - Allows for the registration of Baremetal nodes with Ironic - and population of pertinant node information or configuration - to be passed to the Ironic API for the node. - - This method also creates ports for a list of MAC addresses passed - in to be utilized for boot and potentially network configuration. - - If a failure is detected creating the network ports, any ports - created are deleted, and the node is removed from Ironic. - - :param list nics: - An array of MAC addresses that represent the - network interfaces for the node to be created. - - Example:: - - [ - {'mac': 'aa:bb:cc:dd:ee:01'}, - {'mac': 'aa:bb:cc:dd:ee:02'} - ] - - :param wait: Boolean value, defaulting to false, to wait for the - node to reach the available state where the node can be - provisioned. It must be noted, when set to false, the - method will still wait for locks to clear before sending - the next required command. - - :param timeout: Integer value, defautling to 3600 seconds, for the - wait state to reach completion. - - :param lock_timeout: Integer value, defaulting to 600 seconds, for - locks to clear. - - :param kwargs: Key value pairs to be passed to the Ironic API, - including uuid, name, chassis_uuid, driver_info, - parameters. - - :raises: OpenStackCloudException on operation error. - - :returns: Returns a dictonary representing the new - baremetal node. - """ - try: - machine = meta.obj_to_dict( - self.manager.submitTask(_tasks.MachineCreate(**kwargs))) - - except Exception as e: - raise OpenStackCloudException( - "Error registering machine with Ironic: %s" % str(e)) - - created_nics = [] - try: - for row in nics: - nic = self.manager.submitTask( - _tasks.MachinePortCreate(address=row['mac'], - node_uuid=machine['uuid'])) - created_nics.append(nic.uuid) - - except Exception as e: - self.log.debug("ironic NIC registration failed", exc_info=True) - # TODO(mordred) Handle failures here - try: - for uuid in created_nics: - try: - self.manager.submitTask( - _tasks.MachinePortDelete( - port_id=uuid)) - except: - pass - finally: - self.manager.submitTask( - _tasks.MachineDelete(node_id=machine['uuid'])) - raise OpenStackCloudException( - "Error registering NICs with the baremetal service: %s" - % str(e)) - - try: - if wait: - for count in _utils._iterate_timeout( - timeout, - "Timeout waiting for node transition to " - "available state"): - - machine = self.get_machine(machine['uuid']) - - # Note(TheJulia): Per the Ironic state code, a node - # that fails returns to enroll state, which means a failed - # node cannot be determined at this point in time. - if machine['provision_state'] in ['enroll']: - self.node_set_provision_state( - machine['uuid'], 'manage') - elif machine['provision_state'] in ['manageable']: - self.node_set_provision_state( - machine['uuid'], 'provide') - elif machine['last_error'] is not None: - raise OpenStackCloudException( - "Machine encountered a failure: %s" - % machine['last_error']) - - # Note(TheJulia): Earlier versions of Ironic default to - # None and later versions default to available up until - # the introduction of enroll state. - # Note(TheJulia): The node will transition through - # cleaning if it is enabled, and we will wait for - # completion. - elif machine['provision_state'] in ['available', None]: - break - - else: - if machine['provision_state'] in ['enroll']: - self.node_set_provision_state(machine['uuid'], 'manage') - # Note(TheJulia): We need to wait for the lock to clear - # before we attempt to set the machine into provide state - # which allows for the transition to available. - for count in _utils._iterate_timeout( - lock_timeout, - "Timeout waiting for reservation to clear " - "before setting provide state"): - machine = self.get_machine(machine['uuid']) - if (machine['reservation'] is None and - machine['provision_state'] is not 'enroll'): - - self.node_set_provision_state( - machine['uuid'], 'provide') - machine = self.get_machine(machine['uuid']) - break - - elif machine['provision_state'] in [ - 'cleaning', - 'available']: - break - - elif machine['last_error'] is not None: - raise OpenStackCloudException( - "Machine encountered a failure: %s" - % machine['last_error']) - - except Exception as e: - raise OpenStackCloudException( - "Error transitioning node to available state: %s" - % e) - return machine - - def unregister_machine(self, nics, uuid, wait=False, timeout=600): - """Unregister Baremetal from Ironic - - Removes entries for Network Interfaces and baremetal nodes - from an Ironic API - - :param list nics: An array of strings that consist of MAC addresses - to be removed. - :param string uuid: The UUID of the node to be deleted. - - :param wait: Boolean value, defaults to false, if to block the method - upon the final step of unregistering the machine. - - :param timeout: Integer value, representing seconds with a default - value of 600, which controls the maximum amount of - time to block the method's completion on. - - :raises: OpenStackCloudException on operation failure. - """ - - machine = self.get_machine(uuid) - invalid_states = ['active', 'cleaning', 'clean wait', 'clean failed'] - if machine['provision_state'] in invalid_states: - raise OpenStackCloudException( - "Error unregistering node '%s' due to current provision " - "state '%s'" % (uuid, machine['provision_state'])) - - for nic in nics: - try: - port = self.manager.submitTask( - _tasks.MachinePortGetByAddress(address=nic['mac'])) - self.manager.submitTask( - _tasks.MachinePortDelete(port_id=port.uuid)) - except Exception as e: - raise OpenStackCloudException( - "Error removing NIC '%s' from baremetal API for " - "node '%s'. Error: %s" % (nic, uuid, str(e))) - try: - self.manager.submitTask( - _tasks.MachineDelete(node_id=uuid)) - if wait: - for count in _utils._iterate_timeout( - timeout, - "Timeout waiting for machine to be deleted"): - if not self.get_machine(uuid): - break - - except Exception as e: - raise OpenStackCloudException( - "Error unregistering machine %s from the baremetal API. " - "Error: %s" % (uuid, str(e))) - - def patch_machine(self, name_or_id, patch): - """Patch Machine Information - - This method allows for an interface to manipulate node entries - within Ironic. Specifically, it is a pass-through for the - ironicclient.nodes.update interface which allows the Ironic Node - properties to be updated. - - :param node_id: The server object to attach to. - :param patch: - The JSON Patch document is a list of dictonary objects - that comply with RFC 6902 which can be found at - https://tools.ietf.org/html/rfc6902. - - Example patch construction:: - - patch=[] - patch.append({ - 'op': 'remove', - 'path': '/instance_info' - }) - patch.append({ - 'op': 'replace', - 'path': '/name', - 'value': 'newname' - }) - patch.append({ - 'op': 'add', - 'path': '/driver_info/username', - 'value': 'administrator' - }) - - :raises: OpenStackCloudException on operation error. - - :returns: Dictonary representing the newly updated node. - """ - - try: - return meta.obj_to_dict( - self.manager.submitTask( - _tasks.MachinePatch(node_id=name_or_id, - patch=patch, - http_method='PATCH'))) - except Exception as e: - raise OpenStackCloudException( - "Error updating machine via patch operation. node: %s. " - "%s" % (name_or_id, str(e))) - - def update_machine(self, name_or_id, chassis_uuid=None, driver=None, - driver_info=None, name=None, instance_info=None, - instance_uuid=None, properties=None): - """Update a machine with new configuration information - - A user-friendly method to perform updates of a machine, in whole or - part. - - :param string name_or_id: A machine name or UUID to be updated. - :param string chassis_uuid: Assign a chassis UUID to the machine. - NOTE: As of the Kilo release, this value - cannot be changed once set. If a user - attempts to change this value, then the - Ironic API, as of Kilo, will reject the - request. - :param string driver: The driver name for controlling the machine. - :param dict driver_info: The dictonary defining the configuration - that the driver will utilize to control - the machine. Permutations of this are - dependent upon the specific driver utilized. - :param string name: A human relatable name to represent the machine. - :param dict instance_info: A dictonary of configuration information - that conveys to the driver how the host - is to be configured when deployed. - be deployed to the machine. - :param string instance_uuid: A UUID value representing the instance - that the deployed machine represents. - :param dict properties: A dictonary defining the properties of a - machine. - - :raises: OpenStackCloudException on operation error. - - :returns: Dictonary containing a machine sub-dictonary consisting - of the updated data returned from the API update operation, - and a list named changes which contains all of the API paths - that received updates. - """ - machine = self.get_machine(name_or_id) - if not machine: - raise OpenStackCloudException( - "Machine update failed to find Machine: %s. " % name_or_id) - - machine_config = {} - new_config = {} - - try: - if chassis_uuid: - machine_config['chassis_uuid'] = machine['chassis_uuid'] - new_config['chassis_uuid'] = chassis_uuid - - if driver: - machine_config['driver'] = machine['driver'] - new_config['driver'] = driver - - if driver_info: - machine_config['driver_info'] = machine['driver_info'] - new_config['driver_info'] = driver_info - - if name: - machine_config['name'] = machine['name'] - new_config['name'] = name - - if instance_info: - machine_config['instance_info'] = machine['instance_info'] - new_config['instance_info'] = instance_info - - if instance_uuid: - machine_config['instance_uuid'] = machine['instance_uuid'] - new_config['instance_uuid'] = instance_uuid - - if properties: - machine_config['properties'] = machine['properties'] - new_config['properties'] = properties - except KeyError as e: - self.log.debug( - "Unexpected machine response missing key %s [%s]" % ( - e.args[0], name_or_id)) - raise OpenStackCloudException( - "Machine update failed - machine [%s] missing key %s. " - "Potential API issue." - % (name_or_id, e.args[0])) - - try: - patch = jsonpatch.JsonPatch.from_diff(machine_config, new_config) - except Exception as e: - raise OpenStackCloudException( - "Machine update failed - Error generating JSON patch object " - "for submission to the API. Machine: %s Error: %s" - % (name_or_id, str(e))) - - try: - if not patch: - return dict( - node=machine, - changes=None - ) - else: - machine = self.patch_machine(machine['uuid'], list(patch)) - change_list = [] - for change in list(patch): - change_list.append(change['path']) - return dict( - node=machine, - changes=change_list - ) - except Exception as e: - raise OpenStackCloudException( - "Machine update failed - patch operation failed Machine: %s " - "Error: %s" % (name_or_id, str(e))) - - def validate_node(self, uuid): - try: - ifaces = self.manager.submitTask( - _tasks.MachineNodeValidate(node_uuid=uuid)) - except Exception as e: - raise OpenStackCloudException(str(e)) - - if not ifaces.deploy or not ifaces.power: - raise OpenStackCloudException( - "ironic node %s failed to validate. " - "(deploy: %s, power: %s)" % (ifaces.deploy, ifaces.power)) - - def node_set_provision_state(self, - name_or_id, - state, - configdrive=None, - wait=False, - timeout=3600): - """Set Node Provision State - - Enables a user to provision a Machine and optionally define a - config drive to be utilized. - - :param string name_or_id: The Name or UUID value representing the - baremetal node. - :param string state: The desired provision state for the - baremetal node. - :param string configdrive: An optional URL or file or path - representing the configdrive. In the - case of a directory, the client API - will create a properly formatted - configuration drive file and post the - file contents to the API for - deployment. - :param boolean wait: A boolean value, defaulted to false, to control - if the method will wait for the desire end state - to be reached before returning. - :param integer timeout: Integer value, defaulting to 3600 seconds, - representing the amount of time to wait for - the desire end state to be reached. - - :raises: OpenStackCloudException on operation error. - - :returns: Dictonary representing the current state of the machine - upon exit of the method. - """ - try: - machine = self.manager.submitTask( - _tasks.MachineSetProvision(node_uuid=name_or_id, - state=state, - configdrive=configdrive)) - - if wait: - for count in _utils._iterate_timeout( - timeout, - "Timeout waiting for node transition to " - "target state of '%s'" % state): - machine = self.get_machine(name_or_id) - if state in machine['provision_state']: - break - if ("available" in machine['provision_state'] and - "provide" in state): - break - else: - machine = self.get_machine(name_or_id) - return machine - - except Exception as e: - raise OpenStackCloudException( - "Baremetal machine node failed change provision" - " state to {state}: {msg}".format(state=state, - msg=str(e))) - - def set_machine_maintenance_state( - self, - name_or_id, - state=True, - reason=None): - """Set Baremetal Machine Maintenance State - - Sets Baremetal maintenance state and maintenance reason. - - :param string name_or_id: The Name or UUID value representing the - baremetal node. - :param boolean state: The desired state of the node. True being in - maintenance where as False means the machine - is not in maintenance mode. This value - defaults to True if not explicitly set. - :param string reason: An optional freeform string that is supplied to - the baremetal API to allow for notation as to why - the node is in maintenance state. - - :raises: OpenStackCloudException on operation error. - - :returns: None - """ - try: - if state: - result = self.manager.submitTask( - _tasks.MachineSetMaintenance(node_id=name_or_id, - state='true', - maint_reason=reason)) - else: - result = self.manager.submitTask( - _tasks.MachineSetMaintenance(node_id=name_or_id, - state='false')) - if result is not None: - raise OpenStackCloudException( - "Failed setting machine maintenance state to %s " - "on node %s. Received: %s" % ( - state, name_or_id, result)) - return None - except Exception as e: - raise OpenStackCloudException( - "Error setting machine maintenance state to %s " - "on node %s: %s" % (state, name_or_id, str(e))) - - def remove_machine_from_maintenance(self, name_or_id): - """Remove Baremetal Machine from Maintenance State - - Similarly to set_machine_maintenance_state, this method - removes a machine from maintenance state. It must be noted - that this method simpily calls set_machine_maintenace_state - for the name_or_id requested and sets the state to False. - - :param string name_or_id: The Name or UUID value representing the - baremetal node. - - :raises: OpenStackCloudException on operation error. - - :returns: None - """ - self.set_machine_maintenance_state(name_or_id, False) - - def _set_machine_power_state(self, name_or_id, state): - """Set machine power state to on or off - - This private method allows a user to turn power on or off to - a node via the Baremetal API. - - :params string name_or_id: A string representing the baremetal - node to have power turned to an "on" - state. - :params string state: A value of "on", "off", or "reboot" that is - passed to the baremetal API to be asserted to - the machine. In the case of the "reboot" state, - Ironic will return the host to the "on" state. - - :raises: OpenStackCloudException on operation error or. - - :returns: None - """ - try: - power = self.manager.submitTask( - _tasks.MachineSetPower(node_id=name_or_id, - state=state)) - if power is not None: - raise OpenStackCloudException( - "Failed setting machine power state %s on node %s. " - "Received: %s" % (state, name_or_id, power)) - return None - except Exception as e: - raise OpenStackCloudException( - "Error setting machine power state %s on node %s. " - "Error: %s" % (state, name_or_id, str(e))) - - def set_machine_power_on(self, name_or_id): - """Activate baremetal machine power - - This is a method that sets the node power state to "on". - - :params string name_or_id: A string representing the baremetal - node to have power turned to an "on" - state. - - :raises: OpenStackCloudException on operation error. - - :returns: None - """ - self._set_machine_power_state(name_or_id, 'on') - - def set_machine_power_off(self, name_or_id): - """De-activate baremetal machine power - - This is a method that sets the node power state to "off". - - :params string name_or_id: A string representing the baremetal - node to have power turned to an "off" - state. - - :raises: OpenStackCloudException on operation error. - - :returns: - """ - self._set_machine_power_state(name_or_id, 'off') - - def set_machine_power_reboot(self, name_or_id): - """De-activate baremetal machine power - - This is a method that sets the node power state to "reboot", which - in essence changes the machine power state to "off", and that back - to "on". - - :params string name_or_id: A string representing the baremetal - node to have power turned to an "off" - state. - - :raises: OpenStackCloudException on operation error. - - :returns: None - """ - self._set_machine_power_state(name_or_id, 'reboot') - - def activate_node(self, uuid, configdrive=None): - self.node_set_provision_state(uuid, 'active', configdrive) - - def deactivate_node(self, uuid): - self.node_set_provision_state(uuid, 'deleted') - - def set_node_instance_info(self, uuid, patch): - try: - return meta.obj_to_dict( - self.manager.submitTask( - _tasks.MachineNodeUpdate(node_id=uuid, patch=patch)) - ) - except Exception as e: - raise OpenStackCloudException(str(e)) - - def purge_node_instance_info(self, uuid): - patch = [] - patch.append({'op': 'remove', 'path': '/instance_info'}) - try: - return meta.obj_to_dict( - self.manager.submitTask( - _tasks.MachineNodeUpdate(node_id=uuid, patch=patch)) - ) - except Exception as e: - raise OpenStackCloudException(str(e)) - - @_utils.valid_kwargs('type', 'service_type', 'description') - def create_service(self, name, **kwargs): - """Create a service. - - :param name: Service name. - :param type: Service type. (type or service_type required.) - :param service_type: Service type. (type or service_type required.) - :param description: Service description (optional). - - :returns: a dict containing the services description, i.e. the - following attributes:: - - id: - - name: - - type: - - service_type: - - description: - - :raises: ``OpenStackCloudException`` if something goes wrong during the - openstack API call. - - """ - service_type = kwargs.get('type', kwargs.get('service_type')) - description = kwargs.get('description', None) - try: - if self.cloud_config.get_api_version('identity').startswith('2'): - service_kwargs = {'service_type': service_type} - else: - service_kwargs = {'type': service_type} - - service = self.manager.submitTask(_tasks.ServiceCreate( - name=name, description=description, **service_kwargs)) - except Exception as e: - raise OpenStackCloudException( - "Failed to create service {name}: {msg}".format( - name=name, msg=str(e))) - return meta.obj_to_dict(service) - - def list_services(self): - """List all Keystone services. - - :returns: a list of dict containing the services description. - - :raises: ``OpenStackCloudException`` if something goes wrong during the - openstack API call. - """ - try: - services = self.manager.submitTask(_tasks.ServiceList()) - except Exception as e: - raise OpenStackCloudException(str(e)) - return _utils.normalize_keystone_services( - meta.obj_list_to_dict(services)) - - def search_services(self, name_or_id=None, filters=None): - """Search Keystone services. - - :param name_or_id: Name or id of the desired service. - :param filters: a dict containing additional filters to use. e.g. - {'type': 'network'}. - - :returns: a list of dict containing the services description. - - :raises: ``OpenStackCloudException`` if something goes wrong during the - openstack API call. - """ - services = self.list_services() - return _utils._filter_list(services, name_or_id, filters) - - def get_service(self, name_or_id, filters=None): - """Get exactly one Keystone service. - - :param name_or_id: Name or id of the desired service. - :param filters: a dict containing additional filters to use. e.g. - {'type': 'network'} - - :returns: a dict containing the services description, i.e. the - following attributes:: - - id: - - name: - - type: - - description: - - :raises: ``OpenStackCloudException`` if something goes wrong during the - openstack API call or if multiple matches are found. - """ - return _utils._get_entity(self.search_services, name_or_id, filters) - - def delete_service(self, name_or_id): - """Delete a Keystone service. - - :param name_or_id: Service name or id. - - :returns: True if delete succeeded, False otherwise. - - :raises: ``OpenStackCloudException`` if something goes wrong during - the openstack API call - """ - service = self.get_service(name_or_id=name_or_id) - if service is None: - self.log.debug("Service %s not found for deleting" % name_or_id) - return False - - if self.cloud_config.get_api_version('identity').startswith('2'): - service_kwargs = {'id': service['id']} - else: - service_kwargs = {'service': service['id']} - try: - self.manager.submitTask(_tasks.ServiceDelete(**service_kwargs)) - except Exception as e: - raise OpenStackCloudException( - "Failed to delete service {id}: {msg}".format( - id=service['id'], - msg=str(e))) - return True - - @_utils.valid_kwargs('public_url', 'internal_url', 'admin_url') - def create_endpoint(self, service_name_or_id, url=None, interface=None, - region=None, enabled=True, **kwargs): - """Create a Keystone endpoint. - - :param service_name_or_id: Service name or id for this endpoint. - :param url: URL of the endpoint - :param interface: Interface type of the endpoint - :param public_url: Endpoint public URL. - :param internal_url: Endpoint internal URL. - :param admin_url: Endpoint admin URL. - :param region: Endpoint region. - :param enabled: Whether the endpoint is enabled - - NOTE: Both v2 (public_url, internal_url, admin_url) and v3 - (url, interface) calling semantics are supported. But - you can only use one of them at a time. - - :returns: a list of dicts containing the endpoint description. - - :raises: OpenStackCloudException if the service cannot be found or if - something goes wrong during the openstack API call. - """ - if url and kwargs: - raise OpenStackCloudException( - "create_endpoint takes either url and interace OR" - " public_url, internal_url, admin_url") - - service = self.get_service(name_or_id=service_name_or_id) - if service is None: - raise OpenStackCloudException("service {service} not found".format( - service=service_name_or_id)) - - endpoints = [] - endpoint_args = [] - if url: - urlkwargs = {} - if self.cloud_config.get_api_version('identity').startswith('2'): - if interface != 'public': - raise OpenStackCloudException( - "Error adding endpoint for service {service}." - " On a v2 cloud the url/interface API may only be" - " used for public url. Try using the public_url," - " internal_url, admin_url parameters instead of" - " url and interface".format( - service=service_name_or_id)) - urlkwargs['%url' % interface] = url - urlkwargs['service_id'] = service['id'] - else: - urlkwargs['url'] = url - urlkwargs['interface'] = interface - urlkwargs['enabled'] = enabled - urlkwargs['service'] = service['id'] - endpoint_args.append(urlkwargs) - else: - if self.cloud_config.get_api_version( - 'identity').startswith('2'): - urlkwargs = {} - for arg_key, arg_val in kwargs.items(): - urlkwargs[arg_key.replace('_', '')] = arg_val - urlkwargs['service_id'] = service['id'] - endpoint_args.append(urlkwargs) - else: - for arg_key, arg_val in kwargs.items(): - urlkwargs = {} - urlkwargs['url'] = arg_val - urlkwargs['interface'] = arg_key.split('_')[0] - urlkwargs['enabled'] = enabled - urlkwargs['service'] = service['id'] - endpoint_args.append(urlkwargs) - - for args in endpoint_args: - try: - endpoint = self.manager.submitTask(_tasks.EndpointCreate( - region=region, - **args - )) - except Exception as e: - raise OpenStackCloudException( - "Failed to create endpoint for service {service}: " - "{msg}".format(service=service['name'], - msg=str(e))) - endpoints.append(endpoint) - return meta.obj_list_to_dict(endpoints) - - def list_endpoints(self): - """List Keystone endpoints. - - :returns: a list of dict containing the endpoint description. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - # ToDo: support v3 api (dguerri) - try: - endpoints = self.manager.submitTask(_tasks.EndpointList()) - except Exception as e: - raise OpenStackCloudException("Failed to list endpoints: {msg}" - .format(msg=str(e))) - return meta.obj_list_to_dict(endpoints) - - def search_endpoints(self, id=None, filters=None): - """List Keystone endpoints. - - :param id: endpoint id. - :param filters: a dict containing additional filters to use. e.g. - {'region': 'region-a.geo-1'} - - :returns: a list of dict containing the endpoint description. Each dict - contains the following attributes:: - - id: - - region: - - public_url: - - internal_url: (optional) - - admin_url: (optional) - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - endpoints = self.list_endpoints() - return _utils._filter_list(endpoints, id, filters) - - def get_endpoint(self, id, filters=None): - """Get exactly one Keystone endpoint. - - :param id: endpoint id. - :param filters: a dict containing additional filters to use. e.g. - {'region': 'region-a.geo-1'} - - :returns: a dict containing the endpoint description. i.e. a dict - containing the following attributes:: - - id: - - region: - - public_url: - - internal_url: (optional) - - admin_url: (optional) - """ - return _utils._get_entity(self.search_endpoints, id, filters) - - def delete_endpoint(self, id): - """Delete a Keystone endpoint. - - :param id: Id of the endpoint to delete. - - :returns: True if delete succeeded, False otherwise. - - :raises: ``OpenStackCloudException`` if something goes wrong during - the openstack API call. - """ - # ToDo: support v3 api (dguerri) - endpoint = self.get_endpoint(id=id) - if endpoint is None: - self.log.debug("Endpoint %s not found for deleting" % id) - return False - - if self.cloud_config.get_api_version('identity').startswith('2'): - endpoint_kwargs = {'id': endpoint['id']} - else: - endpoint_kwargs = {'endpoint': endpoint['id']} - try: - self.manager.submitTask(_tasks.EndpointDelete(**endpoint_kwargs)) - except Exception as e: - raise OpenStackCloudException( - "Failed to delete endpoint {id}: {msg}".format( - id=id, - msg=str(e))) - return True - - def create_domain( - self, name, description=None, enabled=True): - """Create a Keystone domain. - - :param name: The name of the domain. - :param description: A description of the domain. - :param enabled: Is the domain enabled or not (default True). - - :returns: a dict containing the domain description - - :raise OpenStackCloudException: if the domain cannot be created - """ - try: - domain = self.manager.submitTask(_tasks.DomainCreate( - name=name, - description=description, - enabled=enabled)) - except Exception as e: - raise OpenStackCloudException( - "Failed to create domain {name}".format(name=name, - msg=str(e))) - return meta.obj_to_dict(domain) - - def update_domain( - self, domain_id, name=None, description=None, enabled=None): - try: - return meta.obj_to_dict( - self.manager.submitTask(_tasks.DomainUpdate( - domain=domain_id, description=description, - enabled=enabled))) - except Exception as e: - raise OpenStackCloudException( - "Error in updating domain {domain}: {message}".format( - domain=domain_id, message=str(e))) - - def delete_domain(self, domain_id): - """Delete a Keystone domain. - - :param domain_id: ID of the domain to delete. - - :returns: None - - :raises: ``OpenStackCloudException`` if something goes wrong during - the openstack API call. - """ - try: - # Deleting a domain is expensive, so disabling it first increases - # the changes of success - domain = self.update_domain(domain_id, enabled=False) - self.manager.submitTask(_tasks.DomainDelete( - domain=domain['id'])) - except Exception as e: - raise OpenStackCloudException( - "Failed to delete domain {id}: {msg}".format(id=domain_id, - msg=str(e))) - - def list_domains(self): - """List Keystone domains. - - :returns: a list of dicts containing the domain description. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - try: - domains = self.manager.submitTask(_tasks.DomainList()) - except Exception as e: - raise OpenStackCloudException("Failed to list domains: {msg}" - .format(msg=str(e))) - return meta.obj_list_to_dict(domains) - - def search_domains(self, **filters): - """Seach Keystone domains. - - :param filters: a dict containing additional filters to use. - keys to search on are id, name, enabled and description. - - :returns: a list of dicts containing the domain description. Each dict - contains the following attributes:: - - id: - - name: - - description: - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - try: - domains = self.manager.submitTask( - _tasks.DomainList(**filters)) - except Exception as e: - raise OpenStackCloudException("Failed to list domains: {msg}" - .format(msg=str(e))) - return meta.obj_list_to_dict(domains) - - def get_domain(self, domain_id): - """Get exactly one Keystone domain. - - :param domain_id: domain id. - - :returns: a dict containing the domain description, or None if not - found. Each dict contains the following attributes:: - - id: - - name: - - description: - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - try: - domain = self.manager.submitTask( - _tasks.DomainGet(domain=domain_id)) - except Exception as e: - raise OpenStackCloudException( - "Failed to get domain {domain_id}: {msg}".format( - domain_id=domain_id, - msg=str(e))) - raise OpenStackCloudException(str(e)) - return meta.obj_to_dict(domain) - - def list_roles(self): - """List Keystone roles. - - :returns: a list of dicts containing the role description. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - try: - roles = self.manager.submitTask(_tasks.RoleList()) - except Exception as e: - raise OpenStackCloudException(str(e)) - return meta.obj_list_to_dict(roles) - - def search_roles(self, name_or_id=None, filters=None): - """Seach Keystone roles. - - :param string name: role name or id. - :param dict filters: a dict containing additional filters to use. - - :returns: a list of dict containing the role description. Each dict - contains the following attributes:: - - - id: - - name: - - description: - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - roles = self.list_roles() - return _utils._filter_list(roles, name_or_id, filters) - - def get_role(self, name_or_id, filters=None): - """Get exactly one Keystone role. - - :param id: role name or id. - :param filters: a dict containing additional filters to use. - - :returns: a single dict containing the role description. Each dict - contains the following attributes:: - - - id: - - name: - - description: - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - return _utils._get_entity(self.search_roles, name_or_id, filters) - - def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", - ephemeral=0, swap=0, rxtx_factor=1.0, is_public=True): - """Create a new flavor. - - :param name: Descriptive name of the flavor - :param ram: Memory in MB for the flavor - :param vcpus: Number of VCPUs for the flavor - :param disk: Size of local disk in GB - :param flavorid: ID for the flavor (optional) - :param ephemeral: Ephemeral space size in GB - :param swap: Swap space in MB - :param rxtx_factor: RX/TX factor - :param is_public: Make flavor accessible to the public - - :returns: A dict describing the new flavor. - - :raises: OpenStackCloudException on operation error. - """ - try: - flavor = self.manager.submitTask( - _tasks.FlavorCreate(name=name, ram=ram, vcpus=vcpus, disk=disk, - flavorid=flavorid, ephemeral=ephemeral, - swap=swap, rxtx_factor=rxtx_factor, - is_public=is_public) - ) - except Exception as e: - raise OpenStackCloudException( - "Failed to create flavor {name}: {msg}".format( - name=name, - msg=str(e))) - return meta.obj_to_dict(flavor) - - def delete_flavor(self, name_or_id): - """Delete a flavor - - :param name_or_id: ID or name of the flavor to delete. - - :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. - """ - flavor = self.get_flavor(name_or_id) - if flavor is None: - self.log.debug( - "Flavor {0} not found for deleting".format(name_or_id)) - return False - - try: - self.manager.submitTask(_tasks.FlavorDelete(flavor=flavor['id'])) - except Exception as e: - raise OpenStackCloudException( - "Unable to delete flavor {0}: {1}".format(name_or_id, e) - ) - - return True - - def _mod_flavor_specs(self, action, flavor_id, specs): - """Common method for modifying flavor extra specs. - - Nova (very sadly) doesn't expose this with a public API, so we - must get the actual flavor object and make a method call on it. - - Two separate try-except blocks are used because Nova can raise - a NotFound exception if FlavorGet() is given an invalid flavor ID, - or if the unset_keys() method of the flavor object is given an - invalid spec key. We need to be able to differentiate between these - actions, thus the separate blocks. - """ - try: - flavor = self.manager.submitTask( - _tasks.FlavorGet(flavor=flavor_id) - ) - except nova_exceptions.NotFound: - self.log.debug( - "Flavor ID {0} not found. " - "Cannot {1} extra specs.".format(flavor_id, action) - ) - raise OpenStackCloudResourceNotFound( - "Flavor ID {0} not found".format(flavor_id) - ) - except Exception as e: - raise OpenStackCloudException( - "Error getting flavor ID {0}: {1}".format(flavor_id, e) - ) - - try: - if action == 'set': - flavor.set_keys(specs) - elif action == 'unset': - flavor.unset_keys(specs) - except Exception as e: - raise OpenStackCloudException( - "Unable to {0} flavor specs: {1}".format(action, e) - ) - - def set_flavor_specs(self, flavor_id, extra_specs): - """Add extra specs to a flavor - - :param string flavor_id: ID of the flavor to update. - :param dict extra_specs: Dictionary of key-value pairs. - - :raises: OpenStackCloudException on operation error. - :raises: OpenStackCloudResourceNotFound if flavor ID is not found. - """ - self._mod_flavor_specs('set', flavor_id, extra_specs) - - def unset_flavor_specs(self, flavor_id, keys): - """Delete extra specs from a flavor - - :param string flavor_id: ID of the flavor to update. - :param list keys: List of spec keys to delete. - - :raises: OpenStackCloudException on operation error. - :raises: OpenStackCloudResourceNotFound if flavor ID is not found. - """ - self._mod_flavor_specs('unset', flavor_id, keys) - - def _mod_flavor_access(self, action, flavor_id, project_id): - """Common method for adding and removing flavor access - """ - try: - if action == 'add': - self.manager.submitTask( - _tasks.FlavorAddAccess(flavor=flavor_id, - tenant=project_id) - ) - elif action == 'remove': - self.manager.submitTask( - _tasks.FlavorRemoveAccess(flavor=flavor_id, - tenant=project_id) - ) - except Exception as e: - raise OpenStackCloudException( - "Error trying to {0} access from flavor ID {1}: {2}".format( - action, flavor_id, e) - ) - - def add_flavor_access(self, flavor_id, project_id): - """Grant access to a private flavor for a project/tenant. - - :param string flavor_id: ID of the private flavor. - :param string project_id: ID of the project/tenant. - - :raises: OpenStackCloudException on operation error. - """ - self._mod_flavor_access('add', flavor_id, project_id) - - def remove_flavor_access(self, flavor_id, project_id): - """Revoke access from a private flavor for a project/tenant. - - :param string flavor_id: ID of the private flavor. - :param string project_id: ID of the project/tenant. - - :raises: OpenStackCloudException on operation error. - """ - self._mod_flavor_access('remove', flavor_id, project_id) - - def create_role(self, name): - """Create a Keystone role. - - :param string name: The name of the role. - - :returns: a dict containing the role description - - :raise OpenStackCloudException: if the role cannot be created - """ - try: - role = self.manager.submitTask( - _tasks.RoleCreate(name=name) - ) - except Exception as e: - raise OpenStackCloudException(str(e)) - return meta.obj_to_dict(role) - - def delete_role(self, name_or_id): - """Delete a Keystone role. - - :param string id: Name or id of the role to delete. - - :returns: True if delete succeeded, False otherwise. - - :raises: ``OpenStackCloudException`` if something goes wrong during - the openstack API call. - """ - role = self.get_role(name_or_id) - if role is None: - self.log.debug( - "Role {0} not found for deleting".format(name_or_id)) - return False - - try: - self.manager.submitTask(_tasks.RoleDelete(role=role['id'])) - except Exception as e: - raise OpenStackCloudException( - "Unable to delete role {0}: {1}".format(name_or_id, e) - ) - - return True diff --git a/shade/_utils.py b/shade/_utils.py index 8f053aeb9..0df687fc0 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -24,6 +24,9 @@ log = _log.setup_logging(__name__) +_decorated_methods = [] + + def _iterate_timeout(timeout, message, wait=2): """Iterate and raise an exception on timeout. @@ -319,3 +322,23 @@ def func_wrapper(func, *args, **kwargs): "'{arg}'".format(f=inspect.stack()[1][3], arg=k)) return func(*args, **kwargs) return func_wrapper + + +def cache_on_arguments(*cache_on_args, **cache_on_kwargs): + def _inner_cache_on_arguments(func): + def _cache_decorator(obj, *args, **kwargs): + the_method = obj._cache.cache_on_arguments( + *cache_on_args, **cache_on_kwargs)( + func.__get__(obj, type(obj))) + return the_method(*args, **kwargs) + + def invalidate(obj, *args, **kwargs): + return obj._cache.cache_on_arguments()(func).invalidate( + *args, **kwargs) + + _cache_decorator.invalidate = invalidate + _cache_decorator.func = func + _decorated_methods.append(func.__name__) + + return _cache_decorator + return _inner_cache_on_arguments diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py new file mode 100644 index 000000000..16e2baf99 --- /dev/null +++ b/shade/openstackcloud.py @@ -0,0 +1,4274 @@ +# 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 functools +import hashlib +import ipaddress +import operator +import os_client_config +import os_client_config.defaults +import threading +import time + +from dogpile import cache + +from cinderclient.v1 import client as cinder_client +from designateclient.v1 import Client as designate_client +import glanceclient +import glanceclient.exc +from glanceclient.common import utils as glance_utils +from heatclient import client as heat_client +from heatclient.common import template_utils +import keystoneauth1.exceptions +from keystoneauth1 import plugin as ksa_plugin +from keystoneauth1 import session as ksa_session +from keystoneclient.v2_0 import client as k2_client +from keystoneclient.v3 import client as k3_client +from neutronclient.common import exceptions as neutron_exceptions +from neutronclient.v2_0 import client as neutron_client +from novaclient import client as nova_client +from novaclient import exceptions as nova_exceptions +import swiftclient.client as swift_client +import swiftclient.service as swift_service +import swiftclient.exceptions as swift_exceptions +import troveclient.client as trove_client + +from shade.exc import * # noqa +from shade import _log +from shade import meta +from shade import task_manager +from shade import _tasks +from shade import _utils + + +OBJECT_MD5_KEY = 'x-object-meta-x-shade-md5' +OBJECT_SHA256_KEY = 'x-object-meta-x-shade-sha256' +IMAGE_MD5_KEY = 'owner_specified.shade.md5' +IMAGE_SHA256_KEY = 'owner_specified.shade.sha256' +# Rackspace returns this for intermittent import errors +IMAGE_ERROR_396 = "Image cannot be imported. Error code: '396'" +DEFAULT_OBJECT_SEGMENT_SIZE = 1073741824 # 1GB +# This halves the current default for Swift +DEFAULT_MAX_FILE_SIZE = (5 * 1024 * 1024 * 1024 + 2) / 2 + + +OBJECT_CONTAINER_ACLS = { + 'public': ".r:*,.rlistings", + 'private': '', +} + + +def _no_pending_volumes(volumes): + '''If there are any volumes not in a steady state, don't cache''' + for volume in volumes: + if volume['status'] not in ('available', 'error'): + return False + return True + + +def _no_pending_images(images): + '''If there are any images not in a steady state, don't cache''' + for image in images: + if image.status not in ('active', 'deleted', 'killed'): + return False + return True + + +def _no_pending_stacks(stacks): + '''If there are any stacks not in a steady state, don't cache''' + for stack in stacks: + status = stack['status'] + if '_COMPLETE' not in status and '_FAILED' not in status: + return False + return True + + +class OpenStackCloud(object): + """Represent a connection to an OpenStack Cloud. + + OpenStackCloud is the entry point for all cloud operations, regardless + of which OpenStack service those operations may ultimately come from. + The operations on an OpenStackCloud are resource oriented rather than + REST API operation oriented. For instance, one will request a Floating IP + and that Floating IP will be actualized either via neutron or via nova + depending on how this particular cloud has decided to arrange itself. + + :param TaskManager manager: Optional task manager to use for running + OpenStack API tasks. Unless you're doing + rate limiting client side, you almost + certainly don't need this. (optional) + :param CloudConfig cloud_config: Cloud config object from os-client-config + In the future, this will be the only way + to pass in cloud configuration, but is + being phased in currently. + """ + _SERVER_LIST_AGE = 5 # TODO(mordred) Make this configurable + + def __init__( + self, + cloud_config=None, + manager=None, **kwargs): + + self.log = _log.setup_logging('shade') + if not cloud_config: + config = os_client_config.OpenStackConfig() + cloud_config = config.get_one_cloud(**kwargs) + + self.name = cloud_config.name + self.auth = cloud_config.get_auth_args() + self.region_name = cloud_config.region_name + self.default_interface = cloud_config.get_interface() + self.private = cloud_config.config.get('private', False) + self.api_timeout = cloud_config.config['api_timeout'] + self.image_api_use_tasks = cloud_config.config['image_api_use_tasks'] + self.secgroup_source = cloud_config.config['secgroup_source'] + self.force_ipv4 = cloud_config.force_ipv4 + + self._external_networks = [] + self._external_network_name_or_id = cloud_config.config.get( + 'external_network', None) + self._use_external_network = cloud_config.config.get( + 'use_external_network', True) + + self._internal_networks = [] + self._internal_network_name_or_id = cloud_config.config.get( + 'internal_network', None) + self._use_internal_network = cloud_config.config.get( + 'use_internal_network', True) + + # Variables to prevent us from going through the network finding + # logic again if we've done it once. This is different from just + # the cached value, since "None" is a valid value to find. + self._external_network_stamp = False + self._internal_network_stamp = False + + if manager is not None: + self.manager = manager + else: + self.manager = task_manager.TaskManager( + name=self.name, client=self) + + (self.verify, self.cert) = cloud_config.get_requests_verify_args() + + self._servers = [] + self._servers_time = 0 + self._servers_lock = threading.Lock() + + cache_expiration_time = cloud_config.get_cache_expiration_time() + cache_class = cloud_config.get_cache_class() + cache_arguments = cloud_config.get_cache_arguments() + cache_expiration = cloud_config.get_cache_expiration() + + if cache_class != 'dogpile.cache.null': + self._cache = cache.make_region( + function_key_generator=self._make_cache_key + ).configure( + cache_class, + expiration_time=cache_expiration_time, + arguments=cache_arguments) + else: + def _fake_invalidate(unused): + pass + + class _FakeCache(object): + def invalidate(self): + pass + + # Don't cache list_servers if we're not caching things. + # Replace this with a more specific cache configuration + # soon. + self._SERVER_LIST_AGE = 2 + self._cache = _FakeCache() + # Undecorate cache decorated methods. Otherwise the call stacks + # wind up being stupidly long and hard to debug + for method in _utils._decorated_methods: + meth_obj = getattr(self, method, None) + if not meth_obj: + continue + if (hasattr(meth_obj, 'invalidate') + and hasattr(meth_obj, 'func')): + new_func = functools.partial(meth_obj.func, self) + new_func.invalidate = _fake_invalidate + setattr(self, method, new_func) + + # If server expiration time is set explicitly, use that. Otherwise + # fall back to whatever it was before + self._SERVER_LIST_AGE = cache_expiration.get( + 'server', self._SERVER_LIST_AGE) + + self._container_cache = dict() + self._file_hash_cache = dict() + + self._keystone_session = None + + self._cinder_client = None + self._designate_client = None + self._glance_client = None + self._glance_endpoint = None + self._heat_client = None + self._ironic_client = None + self._keystone_client = None + self._neutron_client = None + self._nova_client = None + self._swift_client = None + self._swift_service = None + self._trove_client = None + + self._local_ipv6 = _utils.localhost_supports_ipv6() + + self.cloud_config = cloud_config + + @contextlib.contextmanager + def _neutron_exceptions(self, error_message): + try: + yield + except neutron_exceptions.NotFound as e: + raise OpenStackCloudResourceNotFound( + "{msg}: {exc}".format(msg=error_message, exc=str(e))) + except neutron_exceptions.NeutronClientException as e: + if e.status_code == 404: + raise OpenStackCloudURINotFound( + "{msg}: {exc}".format(msg=error_message, exc=str(e))) + else: + raise OpenStackCloudException( + "{msg}: {exc}".format(msg=error_message, exc=str(e))) + except Exception as e: + raise OpenStackCloudException( + "{msg}: {exc}".format(msg=error_message, exc=str(e))) + + def _make_cache_key(self, namespace, fn): + fname = fn.__name__ + if namespace is None: + name_key = self.name + else: + name_key = '%s:%s' % (self.name, namespace) + + def generate_key(*args, **kwargs): + arg_key = ','.join(args) + kw_keys = sorted(kwargs.keys()) + kwargs_key = ','.join( + ['%s:%s' % (k, kwargs[k]) for k in kw_keys if k != 'cache']) + ans = "_".join( + [str(name_key), fname, arg_key, kwargs_key]) + return ans + return generate_key + + def _get_client( + self, service_key, client_class, interface_key='endpoint_type', + pass_version_arg=True, **kwargs): + try: + interface = self.cloud_config.get_interface(service_key) + # trigger exception on lack of service + self.get_session_endpoint(service_key) + constructor_args = dict( + session=self.keystone_session, + service_name=self.cloud_config.get_service_name(service_key), + service_type=self.cloud_config.get_service_type(service_key), + region_name=self.region_name) + constructor_args.update(kwargs) + constructor_args[interface_key] = interface + if pass_version_arg: + version = self.cloud_config.get_api_version(service_key) + constructor_args['version'] = version + client = client_class(**constructor_args) + except Exception: + self.log.debug( + "Couldn't construct {service} object".format( + service=service_key), exc_info=True) + raise + if client is None: + raise OpenStackCloudException( + "Failed to instantiate {service} client." + " This could mean that your credentials are wrong.".format( + service=service_key)) + return client + + @property + def nova_client(self): + if self._nova_client is None: + self._nova_client = self._get_client( + 'compute', nova_client.Client) + return self._nova_client + + def _get_identity_client_class(self): + if self.cloud_config.get_api_version('identity') == '3': + return k3_client.Client + elif self.cloud_config.get_api_version('identity') in ('2', '2.0'): + return k2_client.Client + raise OpenStackCloudException( + "Unknown identity API version: {version}".format( + version=self.cloud_config.get_api_version('identity'))) + + @property + def keystone_session(self): + if self._keystone_session is None: + + try: + keystone_auth = self.cloud_config.get_auth() + if not keystone_auth: + raise OpenStackCloudException( + "Problem with auth parameters") + self._keystone_session = ksa_session.Session( + auth=keystone_auth, + verify=self.verify, + cert=self.cert, + timeout=self.api_timeout) + except Exception as e: + raise OpenStackCloudException( + "Error authenticating to keystone: %s " % str(e)) + return self._keystone_session + + @property + def keystone_client(self): + if self._keystone_client is None: + self._keystone_client = self._get_client( + 'identity', self._get_identity_client_class()) + return self._keystone_client + + @property + def service_catalog(self): + return self.keystone_session.auth.get_access( + self.keystone_session).service_catalog.catalog + + @property + def auth_token(self): + # Keystone's session will reuse a token if it is still valid. + # We don't need to track validity here, just get_token() each time. + return self.keystone_session.get_token() + + @property + def _project_manager(self): + # Keystone v2 calls this attribute tenants + # Keystone v3 calls it projects + # Yay for usable APIs! + if self.cloud_config.get_api_version('identity').startswith('2'): + return self.keystone_client.tenants + return self.keystone_client.projects + + def _get_project_param_dict(self, name_or_id): + project_dict = dict() + if name_or_id: + project = self.get_project(name_or_id) + if not project: + return project_dict + if self.cloud_config.get_api_version('identity') == '3': + project_dict['default_project'] = project['id'] + else: + project_dict['tenant_id'] = project['id'] + return project_dict + + def _get_domain_param_dict(self, domain_id): + """Get a useable domain.""" + + # Keystone v3 requires domains for user and project creation. v2 does + # not. However, keystone v2 does not allow user creation by non-admin + # users, so we can throw an error to the user that does not need to + # mention api versions + if self.cloud_config.get_api_version('identity') == '3': + if not domain_id: + raise OpenStackCloudException( + "User creation requires an explicit domain_id argument.") + else: + return {'domain': domain_id} + else: + return {} + + def _get_identity_params(self, domain_id=None, project=None): + """Get the domain and project/tenant parameters if needed. + + keystone v2 and v3 are divergent enough that we need to pass or not + pass project or tenant_id or domain or nothing in a sane manner. + """ + ret = {} + ret.update(self._get_domain_param_dict(domain_id)) + ret.update(self._get_project_param_dict(project)) + return ret + + @_utils.cache_on_arguments() + def list_projects(self): + """List Keystone Projects. + + :returns: a list of dicts containing the project description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + try: + projects = self.manager.submitTask(_tasks.ProjectList()) + except Exception as e: + self.log.debug("Failed to list projects", exc_info=True) + raise OpenStackCloudException(str(e)) + return meta.obj_list_to_dict(projects) + + def search_projects(self, name_or_id=None, filters=None): + """Seach Keystone projects. + + :param name: project name or id. + :param filters: a dict containing additional filters to use. + + :returns: a list of dict containing the role description + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + projects = self.list_projects() + return _utils._filter_list(projects, name_or_id, filters) + + def get_project(self, name_or_id, filters=None): + """Get exactly one Keystone project. + + :param id: project name or id. + :param filters: a dict containing additional filters to use. + + :returns: a list of dicts containing the project description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + return _utils._get_entity(self.search_projects, name_or_id, filters) + + def update_project(self, name_or_id, description=None, enabled=True): + try: + proj = self.get_project(name_or_id) + if not proj: + raise OpenStackCloudException( + "Project %s not found." % name_or_id) + + params = {} + if self.api_versions['identity'] == '3': + params['project'] = proj['id'] + else: + params['tenant_id'] = proj['id'] + + project = self.manager.submitTask(_tasks.ProjectUpdate( + description=description, + enabled=enabled, + **params)) + except Exception as e: + raise OpenStackCloudException( + "Error in updating project {project}: {message}".format( + project=name_or_id, message=str(e))) + self.list_projects.invalidate() + return meta.obj_to_dict(project) + + def create_project( + self, name, description=None, domain_id=None, enabled=True): + """Create a project.""" + try: + params = self._get_domain_param_dict(domain) + if self.api_versions['identity'] == '3': + params['name'] = name + else: + params['tenant_name'] = name + + project = self.manager.submitTask(_tasks.ProjectCreate( + project_name=name, description=description, enabled=enabled, + **params)) + except Exception as e: + raise OpenStackCloudException( + "Error in creating project {project}: {message}".format( + project=name, message=str(e))) + self.list_projects.invalidate() + return meta.obj_to_dict(project) + + def delete_project(self, name_or_id): + try: + project = self.update_project(name_or_id, enabled=False) + params = {} + if self.api_versions['identity'] == '3': + params['project'] = project['id'] + else: + params['tenant'] = project['id'] + self.manager.submitTask(_tasks.ProjectDelete(**params)) + except Exception as e: + raise OpenStackCloudException( + "Error in deleting project {project}: {message}".format( + project=name_or_id, message=str(e))) + + @_utils.cache_on_arguments() + def list_users(self): + """List Keystone Users. + + :returns: a list of dicts containing the user description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + try: + users = self.manager.submitTask(_tasks.UserList()) + except Exception as e: + raise OpenStackCloudException( + "Failed to list users: {0}".format(str(e)) + ) + return _utils.normalize_users(meta.obj_list_to_dict(users)) + + def search_users(self, name_or_id=None, filters=None): + """Seach Keystone users. + + :param string name: user name or id. + :param dict filters: a dict containing additional filters to use. + + :returns: a list of dict containing the role description + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + users = self.list_users() + return _utils._filter_list(users, name_or_id, filters) + + def get_user(self, name_or_id, filters=None): + """Get exactly one Keystone user. + + :param string name_or_id: user name or id. + :param dict filters: a dict containing additional filters to use. + + :returns: a single dict containing the user description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + return _utils._get_entity(self.search_users, name_or_id, filters) + + def get_user_by_id(self, user_id, normalize=True): + """Get a Keystone user by ID. + + :param string user_id: user ID + :param bool normalize: Flag to control dict normalization + + :returns: a single dict containing the user description + """ + try: + user = meta.obj_to_dict( + self.manager.submitTask(_tasks.UserGet(user=user_id)) + ) + except Exception as e: + raise OpenStackCloudException( + "Error getting user with ID {user_id}: {message}".format( + user_id=user_id, message=str(e))) + if user and normalize: + return _utils.normalize_users([user])[0] + return user + + # NOTE(Shrews): Keystone v2 supports updating only name, email and enabled. + @_utils.valid_kwargs('name', 'email', 'enabled', 'domain_id', 'password', + 'description', 'default_project') + def update_user(self, name_or_id, **kwargs): + self.list_users.invalidate(self) + user = self.get_user(name_or_id) + # normalized dict won't work + kwargs['user'] = self.get_user_by_id(user['id'], normalize=False) + + if self.cloud_config.get_api_version('identity') != '3': + # Do not pass v3 args to a v2 keystone. + kwargs.pop('domain_id', None) + kwargs.pop('password', None) + kwargs.pop('description', None) + kwargs.pop('default_project', None) + elif 'domain_id' in kwargs: + # The incoming parameter is domain_id in order to match the + # parameter name in create_user(), but UserUpdate() needs it + # to be domain. + kwargs['domain'] = kwargs.pop('domain_id') + + try: + user = self.manager.submitTask(_tasks.UserUpdate(**kwargs)) + except Exception as e: + raise OpenStackCloudException( + "Error in updating user {user}: {message}".format( + user=name_or_id, message=str(e))) + self.list_users.invalidate(self) + return _utils.normalize_users([meta.obj_to_dict(user)])[0] + + def create_user( + self, name, password=None, email=None, default_project=None, + enabled=True, domain_id=None): + """Create a user.""" + try: + identity_params = self._get_identity_params( + domain_id, default_project) + user = self.manager.submitTask(_tasks.UserCreate( + name=name, password=password, email=email, + enabled=enabled, **identity_params)) + except Exception as e: + raise OpenStackCloudException( + "Error in creating user {user}: {message}".format( + user=name, message=str(e))) + self.list_users.invalidate(self) + return _utils.normalize_users([meta.obj_to_dict(user)])[0] + + def delete_user(self, name_or_id): + self.list_users.invalidate(self) + user = self.get_user(name_or_id) + if not user: + self.log.debug( + "User {0} not found for deleting".format(name_or_id)) + return False + + # normalized dict won't work + user = self.get_user_by_id(user['id'], normalize=False) + try: + self.manager.submitTask(_tasks.UserDelete(user=user)) + except Exception as e: + raise OpenStackCloudException( + "Error in deleting user {user}: {message}".format( + user=name_or_id, message=str(e))) + self.list_users.invalidate(self) + return True + + @property + def glance_client(self): + if self._glance_client is None: + endpoint, version = glance_utils.strip_version( + self.get_session_endpoint(service_key='image')) + # TODO(mordred): Put check detected vs. configured version + # and warn if they're different. + self._glance_client = self._get_client( + 'image', glanceclient.Client, interface_key='interface', + endpoint=endpoint) + return self._glance_client + + @property + def heat_client(self): + if self._heat_client is None: + self._heat_client = self._get_client( + 'orchestration', heat_client.Client) + return self._heat_client + + def get_template_contents( + self, template_file=None, template_url=None, + template_object=None, files=None): + try: + return template_utils.get_template_contents( + template_file=template_file, template_url=template_url, + template_object=template_object, files=files) + except Exception as e: + raise OpenStackCloudException( + "Error in processing template files: %s" % str(e)) + + @property + def swift_client(self): + if self._swift_client is None: + try: + token = self.keystone_session.get_token() + endpoint = self.get_session_endpoint( + service_key='object-store') + self._swift_client = swift_client.Connection( + preauthurl=endpoint, + preauthtoken=token, + auth_version=self.cloud_config.get_api_version('identity'), + os_options=dict( + auth_token=token, + object_storage_url=endpoint, + region_name=self.region_name), + timeout=self.api_timeout, + ) + except OpenStackCloudException: + raise + except Exception as e: + raise OpenStackCloudException( + "Error constructing swift client: %s", str(e)) + return self._swift_client + + @property + def swift_service(self): + if self._swift_service is None: + try: + endpoint = self.get_session_endpoint( + service_key='object-store') + options = dict(os_auth_token=self.auth_token, + os_storage_url=endpoint, + os_region_name=self.region_name) + self._swift_service = swift_service.SwiftService( + options=options) + except OpenStackCloudException: + raise + except Exception as e: + raise OpenStackCloudException( + "Error constructing swift client: %s", str(e)) + return self._swift_service + + @property + def cinder_client(self): + + if self._cinder_client is None: + self._cinder_client = self._get_client( + 'volume', cinder_client.Client) + return self._cinder_client + + @property + def trove_client(self): + if self._trove_client is None: + self.get_session_endpoint(service_key='database') + # Make the connection - can't use keystone session until there + # is one + self._trove_client = trove_client.Client( + self.cloud_config.get_api_version('database'), + session=self.keystone_session, + region_name=self.region_name, + service_type=self.cloud_config.get_service_type('database'), + ) + + if self._trove_client is None: + raise OpenStackCloudException( + "Failed to instantiate Trove client." + " This could mean that your credentials are wrong.") + + self._trove_client = self._get_client( + 'database', trove_client.Client) + return self._trove_client + + @property + def neutron_client(self): + if self._neutron_client is None: + self._neutron_client = self._get_client( + 'network', neutron_client.Client, pass_version_arg=False) + return self._neutron_client + + @property + def designate_client(self): + if self._designate_client is None: + self._designate_client = self._get_client( + 'dns', designate_client.Client) + return self._designate_client + + def create_stack( + self, name, + template_file=None, template_url=None, + template_object=None, files=None, + rollback=True, + wait=False, timeout=180, + **parameters): + tpl_files, template = template_utils.get_template_contents( + template_file=template_file, + template_url=template_url, + template_object=template_object, + files=files) + params = dict( + stack_name=name, + disable_rollback=not rollback, + parameters=parameters, + template=template, + files=tpl_files, + ) + try: + stack = self.manager.submitTask(_tasks.StackCreate(**params)) + except Exception as e: + raise OpenStackCloudException( + "Error creating stack {name}: {message}".format( + name=name, message=e.message)) + if not wait: + return stack + for count in _iterate_timeout( + timeout, + "Timed out waiting for heat stack to finish"): + if self.get_stack(name, cache=False): + return stack + + def delete_stack(self, name_or_id): + """Delete a Heat Stack + + :param name_or_id: Stack name or id. + + :returns: True if delete succeeded, False otherwise. + + :raises: ``OpenStackCloudException`` if something goes wrong during + the openstack API call + """ + stack = self.get_stack(name_or_id=name_or_id) + if stack is None: + self.log.debug("Stack %s not found for deleting" % name_or_id) + return False + + try: + self.manager.submitTask(_tasks.StackDelete(id=stack['id'])) + except Exception: + raise OpenStackCloudException( + "Failed to delete stack {id}".format(id=stack['id'])) + return True + + def get_name(self): + return self.name + + def get_region(self): + return self.region_name + + def get_flavor_name(self, flavor_id): + flavor = self.get_flavor(flavor_id) + if flavor: + return flavor['name'] + return None + + def get_flavor_by_ram(self, ram, include=None): + """Get a flavor based on amount of RAM available. + + Finds the flavor with the least amount of RAM that is at least + as much as the specified amount. If `include` is given, further + filter based on matching flavor name. + + :param int ram: Minimum amount of RAM. + :param string include: If given, will return a flavor whose name + contains this string as a substring. + """ + flavors = self.list_flavors() + for flavor in sorted(flavors, key=operator.itemgetter('ram')): + if (flavor['ram'] >= ram and + (not include or include in flavor['name'])): + return flavor + raise OpenStackCloudException( + "Could not find a flavor with {ram} and '{include}'".format( + ram=ram, include=include)) + + def get_session_endpoint(self, service_key): + override_endpoint = self.cloud_config.get_endpoint(service_key) + if override_endpoint: + return override_endpoint + try: + # keystone is a special case in keystone, because what? + if service_key == 'identity': + endpoint = self.keystone_session.get_endpoint( + interface=ksa_plugin.AUTH_INTERFACE) + else: + endpoint = self.keystone_session.get_endpoint( + service_type=self.cloud_config.get_service_type( + service_key), + service_name=self.cloud_config.get_service_name( + service_key), + interface=self.cloud_config.get_interface(service_key), + region_name=self.region_name) + except keystoneauth1.exceptions.catalog.EndpointNotFound as e: + self.log.debug( + "Endpoint not found in %s cloud: %s", self.name, str(e)) + endpoint = None + except Exception as e: + raise OpenStackCloudException( + "Error getting %s endpoint: %s" % (service_key, str(e))) + return endpoint + + def has_service(self, service_key): + if not self.cloud_config.config.get('has_%s' % service_key, True): + self.log.debug( + "Overriding {service_key} entry in catalog per config".format( + service_key=service_key)) + return False + try: + endpoint = self.get_session_endpoint(service_key) + except OpenStackCloudException: + return False + if endpoint: + return True + else: + return False + + @_utils.cache_on_arguments() + def _nova_extensions(self): + extensions = set() + + try: + resp, body = self.manager.submitTask( + _tasks.NovaUrlGet(url='/extensions')) + for x in body['extensions']: + extensions.add(x['alias']) + except Exception as e: + raise OpenStackCloudException( + "error fetching extension list for nova: {msg}".format( + msg=str(e))) + + return extensions + + def _has_nova_extension(self, extension_name): + return extension_name in self._nova_extensions() + + def search_keypairs(self, name_or_id=None, filters=None): + keypairs = self.list_keypairs() + return _utils._filter_list(keypairs, name_or_id, filters) + + def search_networks(self, name_or_id=None, filters=None): + """Search OpenStack networks + + :param name_or_id: Name or id of the desired network. + :param filters: a dict containing additional filters to use. e.g. + {'router:external': True} + + :returns: a list of dicts containing the network description. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call. + """ + networks = self.list_networks(filters) + return _utils._filter_list(networks, name_or_id, filters) + + def search_routers(self, name_or_id=None, filters=None): + """Search OpenStack routers + + :param name_or_id: Name or id of the desired router. + :param filters: a dict containing additional filters to use. e.g. + {'admin_state_up': True} + + :returns: a list of dicts containing the router description. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call. + """ + routers = self.list_routers(filters) + return _utils._filter_list(routers, name_or_id, filters) + + def search_subnets(self, name_or_id=None, filters=None): + """Search OpenStack subnets + + :param name_or_id: Name or id of the desired subnet. + :param filters: a dict containing additional filters to use. e.g. + {'enable_dhcp': True} + + :returns: a list of dicts containing the subnet description. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call. + """ + subnets = self.list_subnets(filters) + return _utils._filter_list(subnets, name_or_id, filters) + + def search_ports(self, name_or_id=None, filters=None): + """Search OpenStack ports + + :param name_or_id: Name or id of the desired port. + :param filters: a dict containing additional filters to use. e.g. + {'device_id': '2711c67a-b4a7-43dd-ace7-6187b791c3f0'} + + :returns: a list of dicts containing the port description. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call. + """ + ports = self.list_ports(filters) + return _utils._filter_list(ports, name_or_id, filters) + + def search_volumes(self, name_or_id=None, filters=None): + volumes = self.list_volumes() + return _utils._filter_list( + volumes, name_or_id, filters, name_key='display_name') + + def search_volume_snapshots(self, name_or_id=None, filters=None): + volumesnapshots = self.list_volume_snapshots() + return _utils._filter_list( + volumesnapshots, name_or_id, filters, name_key='display_name') + + def search_flavors(self, name_or_id=None, filters=None): + flavors = self.list_flavors() + return _utils._filter_list(flavors, name_or_id, filters) + + def search_security_groups(self, name_or_id=None, filters=None): + groups = self.list_security_groups() + return _utils._filter_list(groups, name_or_id, filters) + + def search_servers(self, name_or_id=None, filters=None, detailed=False): + servers = self.list_servers(detailed=detailed) + return _utils._filter_list(servers, name_or_id, filters) + + def search_images(self, name_or_id=None, filters=None): + images = self.list_images() + return _utils._filter_list(images, name_or_id, filters) + + def search_floating_ip_pools(self, name=None, filters=None): + pools = self.list_floating_ip_pools() + return _utils._filter_list(pools, name, filters) + + # Note (dguerri): when using Neutron, this can be optimized using + # server-side search. + # There are some cases in which such optimization is not possible (e.g. + # nested attributes or list of objects) so we need to use the client-side + # filtering when we can't do otherwise. + # The same goes for all neutron-related search/get methods! + def search_floating_ips(self, id=None, filters=None): + floating_ips = self.list_floating_ips() + return _utils._filter_list(floating_ips, id, filters) + + def search_zones(self, name_or_id=None, filters=None): + zones = self.list_zones() + return _utils._filter_list(zones, name_or_id, filters) + + def _search_records(self, zone_id, name_or_id=None, filters=None): + records = self._list_records(zone_id=zone_id) + return _utils._filter_list(records, name_or_id, filters) + + def search_stacks(self, name_or_id=None, filters=None): + """Search Heat stacks. + + :param name_or_id: Name or id of the desired stack. + :param filters: a dict containing additional filters to use. e.g. + {'stack_status': 'CREATE_COMPLETE'} + + :returns: a list of dict containing the stack description. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call. + """ + stacks = self.list_stacks() + return _utils._filter_list( + stacks, name_or_id, filters, name_name='stack_name') + + def list_keypairs(self): + """List all available keypairs. + + :returns: A list of keypair dicts. + + """ + try: + return meta.obj_list_to_dict( + self.manager.submitTask(_tasks.KeypairList()) + ) + except Exception as e: + raise OpenStackCloudException( + "Error fetching keypair list: %s" % str(e)) + + def list_networks(self, filters=None): + """List all available networks. + + :param filters: (optional) dict of filter conditions to push down + :returns: A list of network dicts. + + """ + # Translate None from search interface to empty {} for kwargs below + if not filters: + filters = {} + with self._neutron_exceptions("Error fetching network list"): + return self.manager.submitTask( + _tasks.NetworkList(**filters))['networks'] + + def list_routers(self, filters=None): + """List all available routers. + + :param filters: (optional) dict of filter conditions to push down + :returns: A list of router dicts. + + """ + # Translate None from search interface to empty {} for kwargs below + if not filters: + filters = {} + with self._neutron_exceptions("Error fetching router list"): + return self.manager.submitTask( + _tasks.RouterList(**filters))['routers'] + + def list_subnets(self, filters=None): + """List all available subnets. + + :param filters: (optional) dict of filter conditions to push down + :returns: A list of subnet dicts. + + """ + # Translate None from search interface to empty {} for kwargs below + if not filters: + filters = {} + with self._neutron_exceptions("Error fetching subnet list"): + return self.manager.submitTask( + _tasks.SubnetList(**filters))['subnets'] + + def list_ports(self, filters=None): + """List all available ports. + + :param filters: (optional) dict of filter conditions to push down + :returns: A list of port dicts. + + """ + # Translate None from search interface to empty {} for kwargs below + if not filters: + filters = {} + with self._neutron_exceptions("Error fetching port list"): + return self.manager.submitTask(_tasks.PortList(**filters))['ports'] + + @_utils.cache_on_arguments(should_cache_fn=_no_pending_volumes) + def list_volumes(self, cache=True): + """List all available volumes. + + :returns: A list of volume dicts. + + """ + if not cache: + warnings.warn('cache argument to list_volumes is deprecated. Use ' + 'invalidate instead.') + try: + return meta.obj_list_to_dict( + self.manager.submitTask(_tasks.VolumeList()) + ) + except Exception as e: + raise OpenStackCloudException( + "Error fetching volume list: %s" % e) + + @_utils.cache_on_arguments() + def list_flavors(self): + """List all available flavors. + + :returns: A list of flavor dicts. + + """ + try: + return meta.obj_list_to_dict( + self.manager.submitTask(_tasks.FlavorList(is_public=None)) + ) + except Exception as e: + raise OpenStackCloudException( + "Error fetching flavor list: %s" % e) + + @_utils.cache_on_arguments(should_cache_fn=_no_pending_stacks) + def list_stacks(self): + """List all Heat stacks. + + :returns: a list of dict containing the stack description. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call. + """ + try: + stacks = self.manager.submitTask(_tasks.StackList()) + except Exception as e: + raise OpenStackCloudException(str(e)) + return meta.obj_list_to_dict(stacks) + + def list_server_security_groups(self, server): + """List all security groups associated with the given server. + + :returns: A list of security group dicts. + """ + + groups = meta.obj_list_to_dict( + self.manager.submitTask( + _tasks.ServerListSecurityGroups(server=server['id']))) + + return _utils.normalize_nova_secgroups(groups) + + def list_security_groups(self): + """List all available security groups. + + :returns: A list of security group dicts. + + """ + # Handle neutron security groups + if self.secgroup_source == 'neutron': + # Neutron returns dicts, so no need to convert objects here. + with self._neutron_exceptions( + "Error fetching security group list"): + return self.manager.submitTask( + _tasks.NeutronSecurityGroupList())['security_groups'] + + # Handle nova security groups + elif self.secgroup_source == 'nova': + try: + groups = meta.obj_list_to_dict( + self.manager.submitTask(_tasks.NovaSecurityGroupList()) + ) + except Exception: + raise OpenStackCloudException( + "Error fetching security group list" + ) + return _utils.normalize_nova_secgroups(groups) + + # Security groups not supported + else: + raise OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + + def list_servers(self, detailed=False): + """List all available servers. + + :returns: A list of server dicts. + + """ + if (time.time() - self._servers_time) >= self._SERVER_LIST_AGE: + # Since we're using cached data anyway, we don't need to + # have more than one thread actually submit the list + # servers task. Let the first one submit it while holding + # a lock, and the non-blocking acquire method will cause + # subsequent threads to just skip this and use the old + # data until it succeeds. + # For the first time, when there is no data, make the call + # blocking. + if self._servers_lock.acquire(len(self._servers) == 0): + try: + self._servers = self._list_servers(detailed=detailed) + self._servers_time = time.time() + finally: + self._servers_lock.release() + return self._servers + + def _list_servers(self, detailed=False): + try: + servers = meta.obj_list_to_dict( + self.manager.submitTask(_tasks.ServerList())) + + if detailed: + return [ + meta.get_hostvars_from_server(self, server) + for server in servers + ] + else: + return servers + except Exception as e: + raise OpenStackCloudException( + "Error fetching server list: %s" % e) + + @_utils.cache_on_arguments(should_cache_fn=_no_pending_images) + def list_images(self, filter_deleted=True): + """Get available glance images. + + :param filter_deleted: Control whether deleted images are returned. + :returns: A list of glance images. + """ + # First, try to actually get images from glance, it's more efficient + images = [] + try: + + # Creates a generator - does not actually talk to the cloud API + # hardcoding page size for now. We'll have to get MUCH smarter + # if we want to deal with page size per unit of rate limiting + image_gen = self.glance_client.images.list(page_size=1000) + # Deal with the generator to make a list + image_list = self.manager.submitTask( + _tasks.GlanceImageList(image_gen=image_gen)) + + if image_list: + if getattr(image_list[0], 'validate', None): + # glanceclient returns a "warlock" object if you use v2 + image_list = meta.warlock_list_to_dict(image_list) + else: + # glanceclient returns a normal object if you use v1 + image_list = meta.obj_list_to_dict(image_list) + + except glanceclient.exc.HTTPInternalServerError: + # We didn't have glance, let's try nova + # If this doesn't work - we just let the exception propagate + try: + image_list = meta.obj_list_to_dict( + self.manager.submitTask(_tasks.NovaImageList()) + ) + except Exception as e: + raise OpenStackCloudException( + "Error fetching image list: %s" % e) + + except Exception as e: + raise OpenStackCloudException( + "Error fetching image list: %s" % e) + + for image in image_list: + # The cloud might return DELETED for invalid images. + # While that's cute and all, that's an implementation detail. + if not filter_deleted: + images.append(image) + elif image.status != 'DELETED': + images.append(image) + return images + + def list_floating_ip_pools(self): + """List all available floating IP pools. + + :returns: A list of floating IP pool dicts. + + """ + if not self._has_nova_extension('os-floating-ip-pools'): + raise OpenStackCloudUnavailableExtension( + 'Floating IP pools extension is not available on target cloud') + + try: + return meta.obj_list_to_dict( + self.manager.submitTask(_tasks.FloatingIPPoolList()) + ) + except Exception as e: + raise OpenStackCloudException( + "error fetching floating IP pool list: {msg}".format( + msg=str(e))) + + def list_floating_ips(self): + """List all available floating IPs. + + :returns: A list of floating IP dicts. + + """ + if self.has_service('network'): + try: + return _utils.normalize_neutron_floating_ips( + self._neutron_list_floating_ips()) + except OpenStackCloudURINotFound as e: + self.log.debug( + "Something went wrong talking to neutron API: " + "'{msg}'. Trying with Nova.".format(msg=str(e))) + # Fall-through, trying with Nova + + floating_ips = self._nova_list_floating_ips() + return _utils.normalize_nova_floating_ips(floating_ips) + + def _neutron_list_floating_ips(self): + with self._neutron_exceptions("error fetching floating IPs list"): + return self.manager.submitTask( + _tasks.NeutronFloatingIPList())['floatingips'] + + def _nova_list_floating_ips(self): + try: + return meta.obj_list_to_dict( + self.manager.submitTask(_tasks.NovaFloatingIPList())) + except Exception as e: + raise OpenStackCloudException( + "error fetching floating IPs list: {msg}".format(msg=str(e))) + + def list_zones(self): + """List all available DNS zones. + + :returns: A list of zone dicts. + + """ + try: + return self.manager.submitTask(_tasks.ZoneList()) + except Exception as e: + raise OpenStackCloudException( + "Error fetching zone list: %s" % e) + + def _list_records(self, zone_id): + # TODO(mordred) switch domain= to zone= after the Big Rename + try: + return self.manager.submitTask(_tasks.RecordList(domain=zone_id)) + except Exception as e: + raise OpenStackCloudException( + "Error fetching record list: %s" % e) + + def use_external_network(self): + return self._use_external_network + + def use_internal_network(self): + return self._use_internal_network + + def _get_network( + self, + name_or_id, + use_network_func, + network_cache, + network_stamp, + filters): + if not use_network_func(): + return [] + if network_cache: + return network_cache + if network_stamp: + return [] + if not self.has_service('network'): + return [] + if name_or_id: + ext_net = self.get_network(name_or_id) + if not ext_net: + raise OpenStackCloudException( + "Network {network} was provided for external" + " access and that network could not be found".format( + network=name_or_id)) + else: + return [] + try: + # TODO(mordred): Rackspace exposes neutron but it does not + # work. I think that overriding what the service catalog + # reports should be a thing os-client-config should handle + # in a vendor profile - but for now it does not. That means + # this search_networks can just totally fail. If it does though, + # that's fine, clearly the neutron introspection is not going + # to work. + return self.search_networks(filters=filters) + except OpenStackCloudException: + pass + return [] + + def get_external_networks(self): + """Return the networks that are configured to route northbound. + + :returns: A list of network dicts if one is found + """ + self._external_networks = self._get_network( + self._external_network_name_or_id, + self.use_external_network, + self._external_networks, + self._external_network_stamp, + filters={'router:external': True}) + self._external_network_stamp = True + return self._external_networks + + def get_internal_networks(self): + """Return the networks that are configured to not route northbound. + + :returns: A list of network dicts if one is found + """ + self._internal_networks = self._get_network( + self._internal_network_name_or_id, + self.use_internal_network, + self._internal_networks, + self._internal_network_stamp, + filters={ + 'router:external': False, + }) + self._internal_network_stamp = True + return self._internal_networks + + def get_keypair(self, name_or_id, filters=None): + """Get a keypair by name or ID. + + :param name_or_id: Name or ID of the keypair. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + :returns: A keypair dict or None if no matching keypair is + found. + + """ + return _utils._get_entity(self.search_keypairs, name_or_id, filters) + + def get_network(self, name_or_id, filters=None): + """Get a network by name or ID. + + :param name_or_id: Name or ID of the network. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + :returns: A network dict or None if no matching network is + found. + + """ + return _utils._get_entity(self.search_networks, name_or_id, filters) + + def get_router(self, name_or_id, filters=None): + """Get a router by name or ID. + + :param name_or_id: Name or ID of the router. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + :returns: A router dict or None if no matching router is + found. + + """ + return _utils._get_entity(self.search_routers, name_or_id, filters) + + def get_subnet(self, name_or_id, filters=None): + """Get a subnet by name or ID. + + :param name_or_id: Name or ID of the subnet. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + :returns: A subnet dict or None if no matching subnet is + found. + + """ + return _utils._get_entity(self.search_subnets, name_or_id, filters) + + def get_port(self, name_or_id, filters=None): + """Get a port by name or ID. + + :param name_or_id: Name or ID of the port. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + :returns: A port dict or None if no matching port is found. + + """ + return _utils._get_entity(self.search_ports, name_or_id, filters) + + def get_volume(self, name_or_id, filters=None): + """Get a volume by name or ID. + + :param name_or_id: Name or ID of the volume. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + :returns: A volume dict or None if no matching volume is + found. + + """ + return _utils._get_entity(self.search_volumes, name_or_id, filters) + + def get_flavor(self, name_or_id, filters=None): + """Get a flavor by name or ID. + + :param name_or_id: Name or ID of the flavor. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + :returns: A flavor dict or None if no matching flavor is + found. + + """ + return _utils._get_entity(self.search_flavors, name_or_id, filters) + + def get_security_group(self, name_or_id, filters=None): + """Get a security group by name or ID. + + :param name_or_id: Name or ID of the security group. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + :returns: A security group dict or None if no matching + security group is found. + + """ + return _utils._get_entity( + self.search_security_groups, name_or_id, filters) + + def get_server(self, name_or_id=None, filters=None, detailed=False): + """Get a server by name or ID. + + :param name_or_id: Name or ID of the server. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + :returns: A server dict or None if no matching server is + found. + + """ + searchfunc = functools.partial(self.search_servers, + detailed=detailed) + return _utils._get_entity(searchfunc, name_or_id, filters) + + def get_server_by_id(self, id): + return meta.obj_to_dict( + self.manager.submitTask(_tasks.ServerGet(server=id))) + + def get_image(self, name_or_id, filters=None): + """Get an image by name or ID. + + :param name_or_id: Name or ID of the image. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + :returns: An image dict or None if no matching image is found. + + """ + return _utils._get_entity(self.search_images, name_or_id, filters) + + def get_floating_ip(self, id, filters=None): + """Get a floating IP by ID + + :param id: ID of the floating IP. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + :returns: A floating IP dict or None if no matching floating + IP is found. + + """ + return _utils._get_entity(self.search_floating_ips, id, filters) + + def get_zone(self, name_or_id, filters=None): + """Get a DNS zone by name or ID. + + :param name_or_id: Name or ID of the DNS zone. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + :returns: A zone dict or None if no matching DNS zone is + found. + + """ + return _utils._get_entity(self.search_zones, name_or_id, filters) + + def _get_record(self, zone_id, name_or_id, filters=None): + f = lambda name_or_id, filters: self._search_records( + zone_id, name_or_id, filters) + return _utils._get_entity(f, name_or_id, filters) + + def get_stack(self, name_or_id, filters=None): + """Get exactly one Heat stack. + + :param name_or_id: Name or id of the desired stack. + :param filters: a dict containing additional filters to use. e.g. + {'stack_status': 'CREATE_COMPLETE'} + + :returns: a dict containing the stack description + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call or if multiple matches are found. + """ + return _utils._get_entity( + self.search_stacks, name_or_id, filters) + + def create_keypair(self, name, public_key): + """Create a new keypair. + + :param name: Name of the keypair being created. + :param public_key: Public key for the new keypair. + + :raises: OpenStackCloudException on operation error. + """ + try: + return meta.obj_to_dict( + self.manager.submitTask(_tasks.KeypairCreate( + name=name, public_key=public_key)) + ) + except Exception as e: + raise OpenStackCloudException( + "Unable to create keypair %s: %s" % (name, e) + ) + + def delete_keypair(self, name): + """Delete a keypair. + + :param name: Name of the keypair to delete. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException on operation error. + """ + try: + self.manager.submitTask(_tasks.KeypairDelete(key=name)) + except nova_exceptions.NotFound: + self.log.debug("Keypair %s not found for deleting" % name) + return False + except Exception as e: + raise OpenStackCloudException( + "Unable to delete keypair %s: %s" % (name, e) + ) + return True + + # TODO(Shrews): This will eventually need to support tenant ID and + # provider networks, which are admin-level params. + def create_network(self, name, shared=False, admin_state_up=True, + external=False): + """Create a network. + + :param string name: Name of the network being created. + :param bool shared: Set the network as shared. + :param bool admin_state_up: Set the network administrative state to up. + :param bool external: Whether this network is externally accessible. + + :returns: The network object. + :raises: OpenStackCloudException on operation error. + """ + + network = { + 'name': name, + 'shared': shared, + 'admin_state_up': admin_state_up, + 'router:external': external + } + + with self._neutron_exceptions( + "Error creating network {0}".format(name)): + net = self.manager.submitTask( + _tasks.NetworkCreate(body=dict({'network': network}))) + # Turns out neutron returns an actual dict, so no need for the + # use of meta.obj_to_dict() here (which would not work against + # a dict). + return net['network'] + + def delete_network(self, name_or_id): + """Delete a network. + + :param name_or_id: Name or ID of the network being deleted. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException on operation error. + """ + network = self.get_network(name_or_id) + if not network: + self.log.debug("Network %s not found for deleting" % name_or_id) + return False + + with self._neutron_exceptions( + "Error deleting network {0}".format(name_or_id)): + self.manager.submitTask( + _tasks.NetworkDelete(network=network['id'])) + + return True + + def _build_external_gateway_info(self, ext_gateway_net_id, enable_snat, + ext_fixed_ips): + info = {} + if ext_gateway_net_id: + info['network_id'] = ext_gateway_net_id + if enable_snat is not None: + info['enable_snat'] = enable_snat + if ext_fixed_ips: + info['external_fixed_ips'] = ext_fixed_ips + if info: + return info + return None + + def add_router_interface(self, router, subnet_id=None, port_id=None): + """Attach a subnet to an internal router interface. + + Either a subnet ID or port ID must be specified for the internal + interface. Supplying both will result in an error. + + :param dict router: The dict object of the router being changed + :param string subnet_id: The ID of the subnet to use for the interface + :param string port_id: The ID of the port to use for the interface + + :returns: A dict with the router id (id), subnet ID (subnet_id), + port ID (port_id) and tenant ID (tenant_id). + + :raises: OpenStackCloudException on operation error. + """ + body = {} + if subnet_id: + body['subnet_id'] = subnet_id + if port_id: + body['port_id'] = port_id + + with self._neutron_exceptions( + "Error attaching interface to router {0}".format(router['id']) + ): + return self.manager.submitTask( + _tasks.RouterAddInterface(router=router['id'], body=body) + ) + + def remove_router_interface(self, router, subnet_id=None, port_id=None): + """Detach a subnet from an internal router interface. + + If you specify both subnet and port ID, the subnet ID must + correspond to the subnet ID of the first IP address on the port + specified by the port ID. Otherwise an error occurs. + + :param dict router: The dict object of the router being changed + :param string subnet_id: The ID of the subnet to use for the interface + :param string port_id: The ID of the port to use for the interface + + :returns: None on success + + :raises: OpenStackCloudException on operation error. + """ + body = {} + if subnet_id: + body['subnet_id'] = subnet_id + if port_id: + body['port_id'] = port_id + + with self._neutron_exceptions( + "Error detaching interface from router {0}".format(router['id']) + ): + return self.manager.submitTask( + _tasks.RouterRemoveInterface(router=router['id'], body=body) + ) + + def list_router_interfaces(self, router, interface_type=None): + """List all interfaces for a router. + + :param dict router: A router dict object. + :param string interface_type: One of None, "internal", or "external". + Controls whether all, internal interfaces or external interfaces + are returned. + + :returns: A list of port dict objects. + """ + ports = self.search_ports(filters={'device_id': router['id']}) + + if interface_type: + filtered_ports = [] + ext_fixed = (router['external_gateway_info']['external_fixed_ips'] + if router['external_gateway_info'] + else []) + + # Compare the subnets (subnet_id, ip_address) on the ports with + # the subnets making up the router external gateway. Those ports + # that match are the external interfaces, and those that don't + # are internal. + for port in ports: + matched_ext = False + for port_subnet in port['fixed_ips']: + for router_external_subnet in ext_fixed: + if port_subnet == router_external_subnet: + matched_ext = True + if interface_type == 'internal' and not matched_ext: + filtered_ports.append(port) + elif interface_type == 'external' and matched_ext: + filtered_ports.append(port) + return filtered_ports + + return ports + + def create_router(self, name=None, admin_state_up=True, + ext_gateway_net_id=None, enable_snat=None, + ext_fixed_ips=None): + """Create a logical router. + + :param string name: The router name. + :param bool admin_state_up: The administrative state of the router. + :param string ext_gateway_net_id: Network ID for the external gateway. + :param bool enable_snat: Enable Source NAT (SNAT) attribute. + :param list ext_fixed_ips: + List of dictionaries of desired IP and/or subnet on the + external network. Example:: + + [ + { + "subnet_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b", + "ip_address": "192.168.10.2" + } + ] + + :returns: The router object. + :raises: OpenStackCloudException on operation error. + """ + router = { + 'admin_state_up': admin_state_up + } + if name: + router['name'] = name + ext_gw_info = self._build_external_gateway_info( + ext_gateway_net_id, enable_snat, ext_fixed_ips + ) + if ext_gw_info: + router['external_gateway_info'] = ext_gw_info + + with self._neutron_exceptions( + "Error creating router {0}".format(name)): + new_router = self.manager.submitTask( + _tasks.RouterCreate(body=dict(router=router))) + # Turns out neutron returns an actual dict, so no need for the + # use of meta.obj_to_dict() here (which would not work against + # a dict). + return new_router['router'] + + def update_router(self, name_or_id, name=None, admin_state_up=None, + ext_gateway_net_id=None, enable_snat=None, + ext_fixed_ips=None): + """Update an existing logical router. + + :param string name_or_id: The name or UUID of the router to update. + :param string name: The new router name. + :param bool admin_state_up: The administrative state of the router. + :param string ext_gateway_net_id: + The network ID for the external gateway. + :param bool enable_snat: Enable Source NAT (SNAT) attribute. + :param list ext_fixed_ips: + List of dictionaries of desired IP and/or subnet on the + external network. Example:: + + [ + { + "subnet_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b", + "ip_address": "192.168.10.2" + } + ] + + :returns: The router object. + :raises: OpenStackCloudException on operation error. + """ + router = {} + if name: + router['name'] = name + if admin_state_up is not None: + router['admin_state_up'] = admin_state_up + ext_gw_info = self._build_external_gateway_info( + ext_gateway_net_id, enable_snat, ext_fixed_ips + ) + if ext_gw_info: + router['external_gateway_info'] = ext_gw_info + + if not router: + self.log.debug("No router data to update") + return + + curr_router = self.get_router(name_or_id) + if not curr_router: + raise OpenStackCloudException( + "Router %s not found." % name_or_id) + + with self._neutron_exceptions( + "Error updating router {0}".format(name_or_id)): + new_router = self.manager.submitTask( + _tasks.RouterUpdate( + router=curr_router['id'], body=dict(router=router))) + + # Turns out neutron returns an actual dict, so no need for the + # use of meta.obj_to_dict() here (which would not work against + # a dict). + return new_router['router'] + + def delete_router(self, name_or_id): + """Delete a logical router. + + If a name, instead of a unique UUID, is supplied, it is possible + that we could find more than one matching router since names are + not required to be unique. An error will be raised in this case. + + :param name_or_id: Name or ID of the router being deleted. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException on operation error. + """ + router = self.get_router(name_or_id) + if not router: + self.log.debug("Router %s not found for deleting" % name_or_id) + return False + + with self._neutron_exceptions( + "Error deleting router {0}".format(name_or_id)): + self.manager.submitTask( + _tasks.RouterDelete(router=router['id'])) + + return True + + def get_image_exclude(self, name_or_id, exclude): + for image in self.search_images(name_or_id): + if exclude: + if exclude not in image.name: + return image + else: + return image + return None + + def get_image_name(self, image_id, exclude=None): + image = self.get_image_exclude(image_id, exclude) + if image: + return image.name + return None + + def get_image_id(self, image_name, exclude=None): + image = self.get_image_exclude(image_name, exclude) + if image: + return image.id + return None + + def create_image_snapshot(self, name, server, **metadata): + image_id = str(self.manager.submitTask(_tasks.ImageSnapshotCreate( + image_name=name, server=server, metadata=metadata))) + self.list_images.invalidate(self) + return self.get_image(image_id) + + def delete_image(self, name_or_id, wait=False, timeout=3600): + image = self.get_image(name_or_id) + try: + # Note that in v1, the param name is image, but in v2, + # it's image_id + glance_api_version = self.cloud_config.get_api_version('image') + if glance_api_version == '2': + self.manager.submitTask( + _tasks.ImageDelete(image_id=image.id)) + elif glance_api_version == '1': + self.manager.submitTask( + _tasks.ImageDelete(image=image.id)) + self.list_images.invalidate(self) + except Exception as e: + raise OpenStackCloudException( + "Error in deleting image: %s" % str(e)) + + if wait: + for count in _utils._iterate_timeout( + timeout, + "Timeout waiting for the image to be deleted."): + self._cache.invalidate() + if self.get_image(image.id) is None: + return + + def create_image( + self, name, filename, container='images', + md5=None, sha256=None, + disk_format=None, container_format=None, + disable_vendor_agent=True, + wait=False, timeout=3600, **kwargs): + + if not disk_format: + disk_format = self.cloud_config.config['image_format'] + if not container_format: + if disk_format == 'vhd': + container_format = 'ovf' + else: + container_format = 'bare' + if not md5 or not sha256: + (md5, sha256) = self._get_file_hashes(filename) + current_image = self.get_image(name) + if (current_image and current_image.get(IMAGE_MD5_KEY, '') == md5 + and current_image.get(IMAGE_SHA256_KEY, '') == sha256): + self.log.debug( + "image {name} exists and is up to date".format(name=name)) + return current_image + kwargs[IMAGE_MD5_KEY] = md5 + kwargs[IMAGE_SHA256_KEY] = sha256 + + if disable_vendor_agent: + kwargs.update(self.cloud_config.config['disable_vendor_agent']) + + # We can never have nice things. Glance v1 took "is_public" as a + # boolean. Glance v2 takes "visibility". If the user gives us + # is_public, we know what they mean. If they give us visibility, they + # know that they mean. + if self.cloud_config.get_api_version('image') == '2': + if 'is_public' in kwargs: + is_public = kwargs.pop('is_public') + if is_public: + kwargs['visibility'] = 'public' + else: + kwargs['visibility'] = 'private' + + try: + # This makes me want to die inside + if self.image_api_use_tasks: + return self._upload_image_task( + name, filename, container, + current_image=current_image, + wait=wait, timeout=timeout, **kwargs) + else: + image_kwargs = dict(properties=kwargs) + if disk_format: + image_kwargs['disk_format'] = disk_format + if container_format: + image_kwargs['container_format'] = container_format + + return self._upload_image_put(name, filename, **image_kwargs) + except OpenStackCloudException: + self.log.debug("Image creation failed", exc_info=True) + raise + except Exception as e: + raise OpenStackCloudException( + "Image creation failed: {message}".format(message=str(e))) + + def _upload_image_put_v2(self, name, image_data, **image_kwargs): + if 'properties' in image_kwargs: + img_props = image_kwargs.pop('properties') + for k, v in iter(img_props.items()): + image_kwargs[k] = str(v) + image = self.manager.submitTask(_tasks.ImageCreate( + name=name, **image_kwargs)) + self.manager.submitTask(_tasks.ImageUpload( + image_id=image.id, image_data=image_data)) + return image + + def _upload_image_put_v1(self, name, image_data, **image_kwargs): + image = self.manager.submitTask(_tasks.ImageCreate( + name=name, **image_kwargs)) + self.manager.submitTask(_tasks.ImageUpdate( + image=image, data=image_data)) + return image + + def _upload_image_put(self, name, filename, **image_kwargs): + image_data = open(filename, 'rb') + # Because reasons and crying bunnies + if self.cloud_config.get_api_version('image') == '2': + image = self._upload_image_put_v2(name, image_data, **image_kwargs) + else: + image = self._upload_image_put_v1(name, image_data, **image_kwargs) + self._cache.invalidate() + return self.get_image(image.id) + + def _upload_image_task( + self, name, filename, container, current_image=None, + wait=True, timeout=None, **image_properties): + self.create_object( + container, name, filename, + md5=image_properties.get('md5', None), + sha256=image_properties.get('sha256', None)) + if not current_image: + current_image = self.get_image(name) + # TODO(mordred): Can we do something similar to what nodepool does + # using glance properties to not delete then upload but instead make a + # new "good" image and then mark the old one as "bad" + # self.glance_client.images.delete(current_image) + task_args = dict( + type='import', input=dict( + import_from='{container}/{name}'.format( + container=container, name=name), + image_properties=dict(name=name))) + glance_task = self.manager.submitTask( + _tasks.ImageTaskCreate(**task_args)) + self.list_images.invalidate(self) + if wait: + image_id = None + for count in _utils._iterate_timeout( + timeout, + "Timeout waiting for the image to import."): + try: + if image_id is None: + status = self.manager.submitTask( + _tasks.ImageTaskGet(task_id=glance_task.id)) + except glanceclient.exc.HTTPServiceUnavailable: + # Intermittent failure - catch and try again + continue + + if status.status == 'success': + image_id = status.result['image_id'] + try: + image = self.get_image(image_id) + except glanceclient.exc.HTTPServiceUnavailable: + # Intermittent failure - catch and try again + continue + if image is None: + continue + self.update_image_properties( + image=image, + **image_properties) + return self.get_image(status.result['image_id']) + if status.status == 'failure': + if status.message == IMAGE_ERROR_396: + glance_task = self.manager.submitTask( + _tasks.ImageTaskCreate(**task_args)) + self.list_images.invalidate(self) + else: + raise OpenStackCloudException( + "Image creation failed: {message}".format( + message=status.message), + extra_data=status) + else: + return meta.warlock_to_dict(glance_task) + + def update_image_properties( + self, image=None, name_or_id=None, **properties): + if image is None: + image = self.get_image(name_or_id) + + img_props = {} + for k, v in iter(properties.items()): + if v and k in ['ramdisk', 'kernel']: + v = self.get_image_id(v) + k = '{0}_id'.format(k) + img_props[k] = v + + # This makes me want to die inside + if self.cloud_config.get_api_version('image') == '2': + return self._update_image_properties_v2(image, img_props) + else: + return self._update_image_properties_v1(image, img_props) + + def _update_image_properties_v2(self, image, properties): + img_props = {} + for k, v in iter(properties.items()): + if image.get(k, None) != v: + img_props[k] = str(v) + if not img_props: + return False + self.manager.submitTask(_tasks.ImageUpdate( + image_id=image.id, **img_props)) + self.list_images.invalidate(self) + return True + + def _update_image_properties_v1(self, image, properties): + img_props = {} + for k, v in iter(properties.items()): + if image.properties.get(k, None) != v: + img_props[k] = v + if not img_props: + return False + self.manager.submitTask(_tasks.ImageUpdate( + image=image, properties=img_props)) + self.list_images.invalidate(self) + return True + + def create_volume(self, wait=True, timeout=None, **kwargs): + """Create a volume. + + :param wait: If true, waits for volume to be created. + :param timeout: Seconds to wait for volume creation. None is forever. + :param volkwargs: Keyword arguments as expected for cinder client. + + :returns: The created volume object. + + :raises: OpenStackCloudTimeout if wait time exceeded. + :raises: OpenStackCloudException on operation error. + """ + + try: + volume = self.manager.submitTask(_tasks.VolumeCreate(**kwargs)) + except Exception as e: + raise OpenStackCloudException( + "Error in creating volume: %s" % str(e)) + self.list_volumes.invalidate(self) + + volume = meta.obj_to_dict(volume) + + if volume['status'] == 'error': + raise OpenStackCloudException("Error in creating volume") + + if wait: + vol_id = volume['id'] + for count in _utils._iterate_timeout( + timeout, + "Timeout waiting for the volume to be available."): + volume = self.get_volume(vol_id) + + if not volume: + continue + + if volume['status'] == 'available': + break + + if volume['status'] == 'error': + raise OpenStackCloudException( + "Error in creating volume, please check logs") + + return volume + + def delete_volume(self, name_or_id=None, wait=True, timeout=None): + """Delete a volume. + + :param name_or_id: Name or unique ID of the volume. + :param wait: If true, waits for volume to be deleted. + :param timeout: Seconds to wait for volume deletion. None is forever. + + :raises: OpenStackCloudTimeout if wait time exceeded. + :raises: OpenStackCloudException on operation error. + """ + + self.list_volumes.invalidate(self) + volume = self.get_volume(name_or_id) + + if not volume: + self.log.debug( + "Volume {name_or_id} does not exist".format( + name_or_id=name_or_id), + exc_info=True) + return False + + try: + self.manager.submitTask( + _tasks.VolumeDelete(volume=volume['id'])) + except Exception as e: + raise OpenStackCloudException( + "Error in deleting volume: %s" % str(e)) + + self.list_volumes.invalidate(self) + if wait: + for count in _utils._iterate_timeout( + timeout, + "Timeout waiting for the volume to be deleted."): + + if not self.get_volume(volume['id']): + break + + return True + + def get_volumes(self, server, cache=True): + volumes = [] + for volume in self.list_volumes(cache=cache): + for attach in volume['attachments']: + if attach['server_id'] == server['id']: + volumes.append(volume) + return volumes + + def get_volume_id(self, name_or_id): + volume = self.get_volume(name_or_id) + if volume: + return volume['id'] + return None + + def volume_exists(self, name_or_id): + return self.get_volume(name_or_id) is not None + + def get_volume_attach_device(self, volume, server_id): + """Return the device name a volume is attached to for a server. + + This can also be used to verify if a volume is attached to + a particular server. + + :param volume: Volume dict + :param server_id: ID of server to check + + :returns: Device name if attached, None if volume is not attached. + """ + for attach in volume['attachments']: + if server_id == attach['server_id']: + return attach['device'] + return None + + def detach_volume(self, server, volume, wait=True, timeout=None): + """Detach a volume from a server. + + :param server: The server dict to detach from. + :param volume: The volume dict to detach. + :param wait: If true, waits for volume to be detached. + :param timeout: Seconds to wait for volume detachment. None is forever. + + :raises: OpenStackCloudTimeout if wait time exceeded. + :raises: OpenStackCloudException on operation error. + """ + dev = self.get_volume_attach_device(volume, server['id']) + if not dev: + raise OpenStackCloudException( + "Volume %s is not attached to server %s" + % (volume['id'], server['id']) + ) + + try: + self.manager.submitTask( + _tasks.VolumeDetach(attachment_id=volume['id'], + server_id=server['id'])) + except Exception as e: + raise OpenStackCloudException( + "Error detaching volume %s from server %s: %s" % + (volume['id'], server['id'], e) + ) + + if wait: + for count in _utils._iterate_timeout( + timeout, + "Timeout waiting for volume %s to detach." % volume['id']): + try: + vol = self.get_volume(volume['id']) + except Exception: + self.log.debug( + "Error getting volume info %s" % volume['id'], + exc_info=True) + continue + + if vol['status'] == 'available': + return + + if vol['status'] == 'error': + raise OpenStackCloudException( + "Error in detaching volume %s" % volume['id'] + ) + + def attach_volume(self, server, volume, device=None, + wait=True, timeout=None): + """Attach a volume to a server. + + This will attach a volume, described by the passed in volume + dict (as returned by get_volume()), to the server described by + the passed in server dict (as returned by get_server()) on the + named device on the server. + + If the volume is already attached to the server, or generally not + available, then an exception is raised. To re-attach to a server, + but under a different device, the user must detach it first. + + :param server: The server dict to attach to. + :param volume: The volume dict to attach. + :param device: The device name where the volume will attach. + :param wait: If true, waits for volume to be attached. + :param timeout: Seconds to wait for volume attachment. None is forever. + + :raises: OpenStackCloudTimeout if wait time exceeded. + :raises: OpenStackCloudException on operation error. + """ + dev = self.get_volume_attach_device(volume, server['id']) + if dev: + raise OpenStackCloudException( + "Volume %s already attached to server %s on device %s" + % (volume['id'], server['id'], dev) + ) + + if volume['status'] != 'available': + raise OpenStackCloudException( + "Volume %s is not available. Status is '%s'" + % (volume['id'], volume['status']) + ) + + try: + vol = self.manager.submitTask( + _tasks.VolumeAttach(volume_id=volume['id'], + server_id=server['id'], + device=device)) + vol = meta.obj_to_dict(vol) + + except Exception as e: + raise OpenStackCloudException( + "Error attaching volume %s to server %s: %s" % + (volume['id'], server['id'], e) + ) + + if wait: + for count in _utils._iterate_timeout( + timeout, + "Timeout waiting for volume %s to attach." % volume['id']): + try: + vol = self.get_volume(volume['id']) + except Exception: + self.log.debug( + "Error getting volume info %s" % volume['id'], + exc_info=True) + continue + + if self.get_volume_attach_device(vol, server['id']): + break + + # TODO(Shrews) check to see if a volume can be in error status + # and also attached. If so, we should move this + # above the get_volume_attach_device call + if vol['status'] == 'error': + raise OpenStackCloudException( + "Error in attaching volume %s" % volume['id'] + ) + return vol + + def create_volume_snapshot(self, volume_id, force=False, + display_name=None, display_description=None, + wait=True, timeout=None): + """Create a volume. + + :param volume_id: the id of the volume to snapshot. + :param force: If set to True the snapshot will be created even if the + volume is attached to an instance, if False it will not + :param display_name: name of the snapshot, one will be generated if + one is not provided + :param display_description: description of the snapshot, one will be + one is not provided + :param wait: If true, waits for volume snapshot to be created. + :param timeout: Seconds to wait for volume snapshot creation. None is + forever. + + :returns: The created volume object. + + :raises: OpenStackCloudTimeout if wait time exceeded. + :raises: OpenStackCloudException on operation error. + """ + try: + snapshot = self.manager.submitTask( + _tasks.VolumeSnapshotCreate( + volume_id=volume_id, force=force, + display_name=display_name, + display_description=display_description) + ) + + except Exception as e: + raise OpenStackCloudException( + "Error creating snapshot of volume %s: %s" % (volume_id, e) + ) + + snapshot = meta.obj_to_dict(snapshot) + + if wait: + snapshot_id = snapshot['id'] + for count in _utils._iterate_timeout( + timeout, + "Timeout waiting for the volume snapshot to be available." + ): + snapshot = self.get_volume_snapshot_by_id(snapshot_id) + + if snapshot['status'] == 'available': + break + + if snapshot['status'] == 'error': + raise OpenStackCloudException( + "Error in creating volume, please check logs") + + return snapshot + + def get_volume_snapshot_by_id(self, snapshot_id): + """Takes a snapshot_id and gets a dict of the snapshot + that maches that id. + + Note: This is more efficient than get_volume_snapshot. + + param: snapshot_id: ID of the volume snapshot. + + """ + try: + snapshot = self.manager.submitTask( + _tasks.VolumeSnapshotGet( + snapshot_id=snapshot_id + ) + ) + + except Exception as e: + raise OpenStackCloudException( + "Error getting snapshot %s: %s" % (snapshot_id, e) + ) + + return meta.obj_to_dict(snapshot) + + def get_volume_snapshot(self, name_or_id, filters=None): + """Get a volume by name or ID. + + :param name_or_id: Name or ID of the volume snapshot. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + :returns: A volume dict or None if no matching volume is + found. + + """ + return _utils._get_entity(self.search_volume_snapshots, name_or_id, + filters) + + def list_volume_snapshots(self, detailed=True, search_opts=None): + """List all volume snapshots. + + :returns: A list of volume snapshots dicts. + + """ + try: + return meta.obj_list_to_dict( + self.manager.submitTask( + _tasks.VolumeSnapshotList(detailed=detailed, + search_opts=search_opts) + ) + ) + + except Exception as e: + raise OpenStackCloudException( + "Error getting a list of snapshots: %s" % e + ) + + def delete_volume_snapshot(self, name_or_id=None, wait=False, + timeout=None): + """Delete a volume snapshot. + + :param name_or_id: Name or unique ID of the volume snapshot. + :param wait: If true, waits for volume snapshot to be deleted. + :param timeout: Seconds to wait for volume snapshot deletion. None is + forever. + + :raises: OpenStackCloudTimeout if wait time exceeded. + :raises: OpenStackCloudException on operation error. + """ + + volumesnapshot = self.get_volume_snapshot(name_or_id) + + if not volumesnapshot: + return False + + try: + self.manager.submitTask( + _tasks.VolumeSnapshotDelete( + snapshot=volumesnapshot['id'] + ) + ) + except Exception as e: + raise OpenStackCloudException( + "Error in deleting volume snapshot: %s" % str(e)) + + if wait: + for count in _utils._iterate_timeout( + timeout, + "Timeout waiting for the volume snapshot to be deleted."): + if not self.get_volume_snapshot(volumesnapshot['id']): + break + + return True + + def get_server_id(self, name_or_id): + server = self.get_server(name_or_id) + if server: + return server['id'] + return None + + def get_server_private_ip(self, server): + return meta.get_server_private_ip(server, self) + + def get_server_public_ip(self, server): + return meta.get_server_external_ipv4(self, server) + + def get_server_meta(self, server): + # TODO(mordred) remove once ansible has moved to Inventory interface + server_vars = meta.get_hostvars_from_server(self, server) + groups = meta.get_groups_from_server(self, server, server_vars) + return dict(server_vars=server_vars, groups=groups) + + def get_openstack_vars(self, server): + return meta.get_hostvars_from_server(self, server) + + def _expand_server_vars(self, server): + # Used by nodepool + # TODO(mordred) remove after these make it into what we + # actually want the API to be. + return meta.expand_server_vars(self, server) + + def available_floating_ip(self, network=None, server=None): + """Get a floating IP from a network or a pool. + + Return the first available floating IP or allocate a new one. + + :param network: Nova pool name or Neutron network name or id. + :param server: Server the IP is for if known + + :returns: a (normalized) structure with a floating IP address + description. + """ + if self.has_service('network'): + try: + f_ips = _utils.normalize_neutron_floating_ips( + self._neutron_available_floating_ips( + network=network, server=server)) + return f_ips[0] + except OpenStackCloudURINotFound as e: + self.log.debug( + "Something went wrong talking to neutron API: " + "'{msg}'. Trying with Nova.".format(msg=str(e))) + # Fall-through, trying with Nova + + f_ips = _utils.normalize_nova_floating_ips( + self._nova_available_floating_ips(pool=network) + ) + return f_ips[0] + + def _neutron_available_floating_ips( + self, network=None, project_id=None, server=None): + """Get a floating IP from a Neutron network. + + Return a list of available floating IPs or allocate a new one and + return it in a list of 1 element. + + :param network: Nova pool name or Neutron network name or id. + :param server: (server) Server the Floating IP is for + + :returns: a list of floating IP addresses. + + :raises: ``OpenStackCloudResourceNotFound``, if an external network + that meets the specified criteria cannot be found. + """ + if project_id is None: + # Make sure we are only listing floatingIPs allocated the current + # tenant. This is the default behaviour of Nova + project_id = self.keystone_session.get_project_id() + + with self._neutron_exceptions("unable to get available floating IPs"): + networks = self.get_external_networks() + if not networks: + raise OpenStackCloudResourceNotFound( + "unable to find an external network") + + filters = { + 'port_id': None, + 'floating_network_id': networks[0]['id'], + 'tenant_id': project_id + + } + floating_ips = self._neutron_list_floating_ips() + available_ips = _utils._filter_list( + floating_ips, name_or_id=None, filters=filters) + if available_ips: + return available_ips + + # No available IP found or we didn't try + # allocate a new Floating IP + f_ip = self._neutron_create_floating_ip( + network_name_or_id=networks[0]['id'], server=server) + + return [f_ip] + + def _nova_available_floating_ips(self, pool=None): + """Get available floating IPs from a floating IP pool. + + Return a list of available floating IPs or allocate a new one and + return it in a list of 1 element. + + :param pool: Nova floating IP pool name. + + :returns: a list of floating IP addresses. + + :raises: ``OpenStackCloudResourceNotFound``, if a floating IP pool + is not specified and cannot be found. + """ + + try: + if pool is None: + pools = self.list_floating_ip_pools() + if not pools: + raise OpenStackCloudResourceNotFound( + "unable to find a floating ip pool") + pool = pools[0]['name'] + + filters = { + 'instance_id': None, + 'pool': pool + } + + floating_ips = self._nova_list_floating_ips() + available_ips = _utils._filter_list( + floating_ips, name_or_id=None, filters=filters) + if available_ips: + return available_ips + + # No available IP found or we did not try. + # Allocate a new Floating IP + f_ip = self._nova_create_floating_ip(pool=pool) + + return [f_ip] + + except Exception as e: + raise OpenStackCloudException( + "unable to create floating IP in pool {pool}: {msg}".format( + pool=pool, msg=str(e))) + + def create_floating_ip(self, network=None, server=None): + """Allocate a new floating IP from a network or a pool. + + :param network: Nova pool name or Neutron network name or id. + :param server: (optional) Server dict for the server to create + the IP for and to which it should be attached + + :returns: a floating IP address + + :raises: ``OpenStackCloudException``, on operation error. + """ + if self.has_service('network'): + try: + f_ips = _utils.normalize_neutron_floating_ips( + [self._neutron_create_floating_ip( + network_name_or_id=network, server=server)] + ) + return f_ips[0] + except OpenStackCloudURINotFound as e: + self.log.debug( + "Something went wrong talking to neutron API: " + "'{msg}'. Trying with Nova.".format(msg=str(e))) + # Fall-through, trying with Nova + + # Else, we are using Nova network + f_ips = _utils.normalize_nova_floating_ips( + [self._nova_create_floating_ip(pool=network)]) + return f_ips[0] + + def _neutron_create_floating_ip( + self, network_name_or_id=None, server=None): + with self._neutron_exceptions( + "unable to create floating IP for net " + "{0}".format(network_name_or_id)): + if network_name_or_id: + networks = [self.get_network(network_name_or_id)] + if not networks: + raise OpenStackCloudResourceNotFound( + "unable to find network for floating ips with id " + "{0}".format(network_name_or_id)) + else: + networks = self.get_external_networks() + if not networks: + raise OpenStackCloudResourceNotFound( + "Unable to find an external network in this cloud" + " which makes getting a floating IP impossible") + kwargs = { + 'floating_network_id': networks[0]['id'], + } + if server: + (port, fixed_address) = self._get_free_fixed_port(server) + if port: + kwargs['port_id'] = port['id'] + kwargs['fixed_ip_address'] = fixed_address + return self.manager.submitTask(_tasks.NeutronFloatingIPCreate( + body={'floatingip': kwargs}))['floatingip'] + + def _nova_create_floating_ip(self, pool=None): + try: + if pool is None: + pools = self.list_floating_ip_pools() + if not pools: + raise OpenStackCloudResourceNotFound( + "unable to find a floating ip pool") + pool = pools[0]['name'] + + pool_ip = self.manager.submitTask( + _tasks.NovaFloatingIPCreate(pool=pool)) + return meta.obj_to_dict(pool_ip) + + except Exception as e: + raise OpenStackCloudException( + "unable to create floating IP in pool {pool}: {msg}".format( + pool=pool, msg=str(e))) + + def delete_floating_ip(self, floating_ip_id): + """Deallocate a floating IP from a tenant. + + :param floating_ip_id: a floating IP address id. + + :returns: True if the IP address has been deleted, False if the IP + address was not found. + + :raises: ``OpenStackCloudException``, on operation error. + """ + if self.has_service('network'): + try: + return self._neutron_delete_floating_ip(floating_ip_id) + except OpenStackCloudURINotFound as e: + self.log.debug( + "Something went wrong talking to neutron API: " + "'{msg}'. Trying with Nova.".format(msg=str(e))) + # Fall-through, trying with Nova + + # Else, we are using Nova network + return self._nova_delete_floating_ip(floating_ip_id) + + def _neutron_delete_floating_ip(self, floating_ip_id): + try: + with self._neutron_exceptions("unable to delete floating IP"): + self.manager.submitTask( + _tasks.NeutronFloatingIPDelete(floatingip=floating_ip_id)) + except OpenStackCloudResourceNotFound: + return False + + return True + + def _nova_delete_floating_ip(self, floating_ip_id): + try: + self.manager.submitTask( + _tasks.NovaFloatingIPDelete(floating_ip=floating_ip_id)) + except nova_exceptions.NotFound: + return False + except Exception as e: + raise OpenStackCloudException( + "unable to delete floating IP id {fip_id}: {msg}".format( + fip_id=floating_ip_id, msg=str(e))) + + return True + + def _attach_ip_to_server( + self, server, floating_ip, + fixed_address=None, wait=False, + timeout=60, skip_attach=False): + """Attach a floating IP to a server. + + :param server: Server dict + :param floating_ip: Floating IP dict to attach + :param fixed_address: (optional) fixed address to which attach the + floating IP to. + :param wait: (optional) Wait for the address to appear as assigned + to the server in Nova. Defaults to False. + :param timeout: (optional) Seconds to wait, defaults to 60. + See the ``wait`` parameter. + :param skip_attach: (optional) Skip the actual attach and just do + the wait. Defaults to False. + + :returns: None + + :raises: OpenStackCloudException, on operation error. + """ + # Short circuit if we're asking to attach an IP that's already + # attached + ext_ip = meta.get_server_ip(server, ext_tag='floating') + if ext_ip == floating_ip['floating_ip_address']: + return + + if self.has_service('network'): + if not skip_attach: + try: + self._neutron_attach_ip_to_server( + server=server, floating_ip=floating_ip, + fixed_address=fixed_address) + except OpenStackCloudURINotFound as e: + self.log.debug( + "Something went wrong talking to neutron API: " + "'{msg}'. Trying with Nova.".format(msg=str(e))) + # Fall-through, trying with Nova + else: + # Nova network + self._nova_attach_ip_to_server( + server_id=server['id'], floating_ip_id=floating_ip['id'], + fixed_address=fixed_address) + + if wait: + # Wait for the address to be assigned to the server + server_id = server['id'] + for _ in _utils._iterate_timeout( + timeout, + "Timeout waiting for the floating IP to be attached."): + server = self.get_server_by_id(server_id) + ext_ip = meta.get_server_ip(server, ext_tag='floating') + if ext_ip == floating_ip['floating_ip_address']: + return + + def _get_free_fixed_port(self, server, fixed_address=None): + ports = self.search_ports(filters={'device_id': server['id']}) + if not ports: + return (None, None) + port = None + if not fixed_address: + if len(ports) > 1: + raise OpenStackCloudException( + "More than one port was found for server {server}" + " and no fixed_address was specified. It is not" + " possible to infer correct behavior. Please specify" + " a fixed_address - or file a bug in shade describing" + " how you think this should work.") + # We're assuming one, because we have no idea what to do with + # more than one. + port = ports[0] + # Select the first available IPv4 address + for address in port.get('fixed_ips', list()): + try: + ip = ipaddress.ip_address(address['ip_address']) + except Exception: + continue + if ip.version == 4: + fixed_address = address['ip_address'] + return port, fixed_address + raise OpenStackCloudException( + "unable to find a free fixed IPv4 address for server " + "{0}".format(server_id)) + # unfortunately a port can have more than one fixed IP: + # we can't use the search_ports filtering for fixed_address as + # they are contained in a list. e.g. + # + # "fixed_ips": [ + # { + # "subnet_id": "008ba151-0b8c-4a67-98b5-0d2b87666062", + # "ip_address": "172.24.4.2" + # } + # ] + # + # Search fixed_address + for p in ports: + for fixed_ip in p['fixed_ips']: + if fixed_address == fixed_ip['ip_address']: + return (p, fixed_address) + return (None, None) + + def _neutron_attach_ip_to_server( + self, server, floating_ip, fixed_address=None): + with self._neutron_exceptions( + "unable to bind a floating ip to server " + "{0}".format(server['id'])): + + # Find an available port + (port, fixed_address) = self._get_free_fixed_port( + server, fixed_address=fixed_address) + if not port: + raise OpenStackCloudException( + "unable to find a port for server {0}".format( + server['id'])) + + floating_ip_args = {'port_id': port['id']} + if fixed_address is not None: + floating_ip_args['fixed_ip_address'] = fixed_address + + return self.manager.submitTask(_tasks.NeutronFloatingIPUpdate( + floatingip=floating_ip['id'], + body={'floatingip': floating_ip_args} + ))['floatingip'] + + def _nova_attach_ip_to_server(self, server_id, floating_ip_id, + fixed_address=None): + try: + f_ip = self.get_floating_ip(id=floating_ip_id) + return self.manager.submitTask(_tasks.NovaFloatingIPAttach( + server=server_id, address=f_ip['floating_ip_address'], + fixed_address=fixed_address)) + except Exception as e: + raise OpenStackCloudException( + "error attaching IP {ip} to instance {id}: {msg}".format( + ip=floating_ip_id, id=server_id, msg=str(e))) + + def detach_ip_from_server(self, server_id, floating_ip_id): + """Detach a floating IP from a server. + + :param server_id: id of a server. + :param floating_ip_id: Id of the floating IP to detach. + + :returns: True if the IP has been detached, or False if the IP wasn't + attached to any server. + + :raises: ``OpenStackCloudException``, on operation error. + """ + if self.has_service('network'): + try: + return self._neutron_detach_ip_from_server( + server_id=server_id, floating_ip_id=floating_ip_id) + except OpenStackCloudURINotFound as e: + self.log.debug( + "Something went wrong talking to neutron API: " + "'{msg}'. Trying with Nova.".format(msg=str(e))) + # Fall-through, trying with Nova + + # Nova network + self._nova_detach_ip_from_server( + server_id=server_id, floating_ip_id=floating_ip_id) + + def _neutron_detach_ip_from_server(self, server_id, floating_ip_id): + with self._neutron_exceptions( + "unable to detach a floating ip from server " + "{0}".format(server_id)): + f_ip = self.get_floating_ip(id=floating_ip_id) + if f_ip is None or not f_ip['attached']: + return False + self.manager.submitTask(_tasks.NeutronFloatingIPUpdate( + floatingip=floating_ip_id, + body={'floatingip': {'port_id': None}})) + + return True + + def _nova_detach_ip_from_server(self, server_id, floating_ip_id): + try: + f_ip = self.get_floating_ip(id=floating_ip_id) + if f_ip is None: + raise OpenStackCloudException( + "unable to find floating IP {0}".format(floating_ip_id)) + self.manager.submitTask(_tasks.NovaFloatingIPDetach( + server=server_id, address=f_ip['floating_ip_address'])) + except nova_exceptions.Conflict as e: + self.log.debug( + "nova floating IP detach failed: {msg}".format(msg=str(e)), + exc_info=True) + return False + except Exception as e: + raise OpenStackCloudException( + "error detaching IP {ip} from instance {id}: {msg}".format( + ip=floating_ip_id, id=server_id, msg=str(e))) + + return True + + def _add_ip_from_pool( + self, server, network, fixed_address=None, reuse=True): + """Add a floating IP to a sever from a given pool + + This method reuses available IPs, when possible, or allocate new IPs + to the current tenant. + The floating IP is attached to the given fixed address or to the + first server port/fixed address + + :param server: Server dict + :param network: Nova pool name or Neutron network name or id. + :param fixed_address: a fixed address + :param reuse: Try to reuse existing ips. Defaults to True. + + :returns: the floating IP assigned + """ + if reuse: + f_ip = self.available_floating_ip(network=network) + else: + f_ip = self.create_floating_ip(network=network) + + self._attach_ip_to_server( + server=server, floating_ip=f_ip, fixed_address=fixed_address) + + return f_ip + + def add_ip_list(self, server, ips): + """Attach a list of IPs to a server. + + :param server: a server object + :param ips: list of IP addresses (floating IPs) + + :returns: None + + :raises: ``OpenStackCloudException``, on operation error. + """ + # ToDo(dguerri): this makes no sense as we cannot attach multiple + # floating IPs to a single fixed_address (this is true for both + # neutron and nova). I will leave this here for the moment as we are + # refactoring floating IPs methods. + for ip in ips: + f_ip = self.get_floating_ip( + id=None, filters={'floating_ip_address': ip}) + self._attach_ip_to_server( + server=server, floating_ip=f_ip) + + def add_auto_ip(self, server, wait=False, timeout=60, reuse=True): + """Add a floating IP to a server. + + This method is intended for basic usage. For advanced network + architecture (e.g. multiple external networks or servers with multiple + interfaces), use other floating IP methods. + + This method can reuse available IPs, or allocate new IPs to the current + project. + + :param server: a server dictionary. + :param reuse: Whether or not to attempt to reuse IPs, defaults + to True. + :param wait: (optional) Wait for the address to appear as assigned + to the server in Nova. Defaults to False. + :param timeout: (optional) Seconds to wait, defaults to 60. + See the ``wait`` parameter. + :param reuse: Try to reuse existing ips. Defaults to True. + + :returns: Floating IP address attached to server. + + """ + skip_attach = False + if reuse: + f_ip = self.available_floating_ip() + else: + f_ip = self.create_floating_ip(server=server) + if server: + # This gets passed in for both nova and neutron + # but is only meaninful for the neutron logic branch + skip_attach = True + + self._attach_ip_to_server( + server=server, floating_ip=f_ip, wait=wait, timeout=timeout, + skip_attach=skip_attach) + + return f_ip + + def add_ips_to_server( + self, server, auto_ip=True, ips=None, ip_pool=None, + wait=False, timeout=60, reuse=True): + if ip_pool: + self._add_ip_from_pool(server, ip_pool, reuse=reuse) + elif ips: + self.add_ip_list(server, ips) + elif auto_ip: + if self.get_server_public_ip(server): + return server + self.add_auto_ip( + server, wait=wait, timeout=timeout, reuse=reuse) + else: + return server + + # this may look redundant, but if there is now a + # floating IP, then it needs to be obtained from + # a recent server object if the above code path exec'd + try: + server = self.get_server_by_id(server['id']) + except Exception as e: + raise OpenStackCloudException( + "Error in getting info from instance: %s " % str(e)) + return server + + def create_server(self, auto_ip=True, ips=None, ip_pool=None, + root_volume=None, terminate_volume=False, + wait=False, timeout=180, reuse_ips=True, + **bootkwargs): + """Create a virtual server instance. + + :returns: A dict representing the created server. + :raises: OpenStackCloudException on operation error. + """ + if root_volume: + if terminate_volume: + suffix = ':::1' + else: + suffix = ':::0' + volume_id = self.get_volume_id(root_volume) + suffix + if 'block_device_mapping' not in bootkwargs: + bootkwargs['block_device_mapping'] = dict() + bootkwargs['block_device_mapping']['vda'] = volume_id + + try: + server = self.manager.submitTask(_tasks.ServerCreate(**bootkwargs)) + # This is a direct get task call to skip the list_servers + # cache which has absolutely no chance of containing the + # new server + server = self.get_server_by_id(server.id) + server_id = server['id'] + except Exception as e: + raise OpenStackCloudException( + "Error in creating instance: {0}".format(e)) + if server.status == 'ERROR': + raise OpenStackCloudException( + "Error in creating the server.") + if wait: + # There is no point in iterating faster than the list_servers cache + for count in _utils._iterate_timeout( + timeout, + "Timeout waiting for the server to come up.", + wait=self._SERVER_LIST_AGE): + try: + # Use the get_server call so that the list_servers + # cache can be leveraged + server = self.get_server(server_id) + except Exception: + continue + if not server: + continue + + server = self.get_active_server( + server=server, reuse=reuse_ips, + auto_ip=auto_ip, ips=ips, ip_pool=ip_pool, + wait=wait, timeout=timeout) + if server: + return server + return server + + def get_active_server( + self, server, auto_ip=True, ips=None, ip_pool=None, + reuse=True, wait=False, timeout=180): + + if server['status'] == 'ERROR': + raise OpenStackCloudException( + "Error in creating the server", extra_data=dict(server=server)) + + if server['status'] == 'ACTIVE': + if 'addresses' in server and server['addresses']: + return self.add_ips_to_server( + server, auto_ip, ips, ip_pool, reuse=reuse, wait=wait) + + self.log.debug( + 'Server {server} reached ACTIVE state without' + ' being allocated an IP address.' + ' Deleting server.'.format(server=server['id'])) + try: + self._delete_server( + server=server, wait=wait, timeout=timeout) + except Exception as e: + raise OpenStackCloudException( + 'Server reached ACTIVE state without being' + ' allocated an IP address AND then could not' + ' be deleted: {0}'.format(e), + extra_data=dict(server=server)) + raise OpenStackCloudException( + 'Server reached ACTIVE state without being' + ' allocated an IP address.', + extra_data=dict(server=server)) + return None + + def rebuild_server(self, server_id, image_id, wait=False, timeout=180): + try: + server = self.manager.submitTask(_tasks.ServerRebuild( + server=server_id, image=image_id)) + except Exception as e: + raise OpenStackCloudException( + "Error in rebuilding instance: {0}".format(e)) + if wait: + for count in _utils._iterate_timeout( + timeout, + "Timeout waiting for server {0} to " + "rebuild.".format(server_id)): + try: + server = self.get_server_by_id(server_id) + except Exception: + continue + + if server['status'] == 'ACTIVE': + return server + + if server['status'] == 'ERROR': + raise OpenStackCloudException( + "Error in rebuilding the server", + extra_data=dict(server=server)) + return meta.obj_to_dict(server) + + def delete_server( + self, name_or_id, wait=False, timeout=180, delete_ips=False): + server = self.get_server(name_or_id) + return self._delete_server( + server, wait=wait, timeout=timeout, delete_ips=delete_ips) + + def _delete_server( + self, server, wait=False, timeout=180, delete_ips=False): + if server: + if delete_ips: + floating_ip = meta.get_server_ip(server, ext_tag='floating') + if floating_ip: + ips = self.search_floating_ips(filters={ + 'floating_ip_address': floating_ip}) + if len(ips) != 1: + raise OpenStackException( + "Tried to delete floating ip {floating_ip}" + " associated with server {id} but there was" + " an error finding it. Something is exceptionally" + " broken.".format( + floating_ip=floating_ip, + id=server['id'])) + self.delete_floating_ip(ips[0]['id']) + try: + self.manager.submitTask( + _tasks.ServerDelete(server=server['id'])) + except nova_exceptions.NotFound: + return + except Exception as e: + raise OpenStackCloudException( + "Error in deleting server: {0}".format(e)) + else: + return + if not wait: + return + for count in _utils._iterate_timeout( + timeout, + "Timed out waiting for server to get deleted."): + try: + server = self.get_server_by_id(server['id']) + if not server: + return + server = meta.obj_to_dict(server) + except nova_exceptions.NotFound: + return + except Exception as e: + raise OpenStackCloudException( + "Error in deleting server: {0}".format(e)) + + def list_containers(self): + try: + return meta.obj_to_dict(self.manager.submitTask( + _tasks.ContainerList())) + except swift_exceptions.ClientException as e: + raise OpenStackCloudException( + "Container list failed: %s (%s/%s)" % ( + e.http_reason, e.http_host, e.http_path)) + + def get_container(self, name, skip_cache=False): + if skip_cache or name not in self._container_cache: + try: + container = self.manager.submitTask( + _tasks.ContainerGet(container=name)) + self._container_cache[name] = container + except swift_exceptions.ClientException as e: + if e.http_status == 404: + return None + raise OpenStackCloudException( + "Container fetch failed: %s (%s/%s)" % ( + e.http_reason, e.http_host, e.http_path)) + return self._container_cache[name] + + def create_container(self, name, public=False): + container = self.get_container(name) + if container: + return container + try: + self.manager.submitTask( + _tasks.ContainerCreate(container=name)) + if public: + self.set_container_access(name, 'public') + return self.get_container(name, skip_cache=True) + except swift_exceptions.ClientException as e: + raise OpenStackCloudException( + "Container creation failed: %s (%s/%s)" % ( + e.http_reason, e.http_host, e.http_path)) + + def delete_container(self, name): + try: + self.manager.submitTask( + _tasks.ContainerDelete(container=name)) + except swift_exceptions.ClientException as e: + if e.http_status == 404: + return + raise OpenStackCloudException( + "Container deletion failed: %s (%s/%s)" % ( + e.http_reason, e.http_host, e.http_path)) + + def update_container(self, name, headers): + try: + self.manager.submitTask( + _tasks.ContainerUpdate(container=name, headers=headers)) + except swift_exceptions.ClientException as e: + raise OpenStackCloudException( + "Container update failed: %s (%s/%s)" % ( + e.http_reason, e.http_host, e.http_path)) + + def set_container_access(self, name, access): + if access not in OBJECT_CONTAINER_ACLS: + raise OpenStackCloudException( + "Invalid container access specified: %s. Must be one of %s" + % (access, list(OBJECT_CONTAINER_ACLS.keys()))) + header = {'x-container-read': OBJECT_CONTAINER_ACLS[access]} + self.update_container(name, header) + + def get_container_access(self, name): + container = self.get_container(name, skip_cache=True) + if not container: + raise OpenStackCloudException("Container not found: %s" % name) + acl = container.get('x-container-read', '') + try: + return [p for p, a in OBJECT_CONTAINER_ACLS.items() + if acl == a].pop() + except IndexError: + raise OpenStackCloudException( + "Could not determine container access for ACL: %s." % acl) + + def _get_file_hashes(self, filename): + if filename not in self._file_hash_cache: + md5 = hashlib.md5() + sha256 = hashlib.sha256() + with open(filename, 'rb') as file_obj: + for chunk in iter(lambda: file_obj.read(8192), b''): + md5.update(chunk) + sha256.update(chunk) + self._file_hash_cache[filename] = dict( + md5=md5.hexdigest(), sha256=sha256.hexdigest()) + return (self._file_hash_cache[filename]['md5'], + self._file_hash_cache[filename]['sha256']) + + @_utils.cache_on_arguments() + def get_object_capabilities(self): + return self.manager.submitTask(_tasks.ObjectCapabilities()) + + def get_object_segment_size(self, segment_size): + '''get a segment size that will work given capabilities''' + if segment_size is None: + segment_size = DEFAULT_OBJECT_SEGMENT_SIZE + try: + caps = self.get_object_capabilities() + except swift_exceptions.ClientException as e: + if e.http_status == 412: + server_max_file_size = DEFAULT_MAX_FILE_SIZE + self.log.info( + "Swift capabilities not supported. " + "Using default max file size.") + else: + raise OpenStackCloudException( + "Could not determine capabilities") + else: + server_max_file_size = caps.get('swift', {}).get('max_file_size', + 0) + + if segment_size > server_max_file_size: + return server_max_file_size + return segment_size + + def is_object_stale( + self, container, name, filename, file_md5=None, file_sha256=None): + + metadata = self.get_object_metadata(container, name) + if not metadata: + self.log.debug( + "swift stale check, no object: {container}/{name}".format( + container=container, name=name)) + return True + + if file_md5 is None or file_sha256 is None: + (file_md5, file_sha256) = self._get_file_hashes(filename) + + if metadata.get(OBJECT_MD5_KEY, '') != file_md5: + self.log.debug( + "swift md5 mismatch: {filename}!={container}/{name}".format( + filename=filename, container=container, name=name)) + return True + if metadata.get(OBJECT_SHA256_KEY, '') != file_sha256: + self.log.debug( + "swift sha256 mismatch: {filename}!={container}/{name}".format( + filename=filename, container=container, name=name)) + return True + + self.log.debug( + "swift object up to date: {container}/{name}".format( + container=container, name=name)) + return False + + def create_object( + self, container, name, filename=None, + md5=None, sha256=None, segment_size=None, + **headers): + """Create a file object + + :param container: The name of the container to store the file in. + This container will be created if it does not exist already. + :param name: Name for the object within the container. + :param filename: The path to the local file whose contents will be + uploaded. + :param md5: A hexadecimal md5 of the file. (Optional), if it is known + and can be passed here, it will save repeating the expensive md5 + process. It is assumed to be accurate. + :param sha256: A hexadecimal sha256 of the file. (Optional) See md5. + :param segment_size: Break the uploaded object into segments of this + many bytes. (Optional) Shade will attempt to discover the maximum + value for this from the server if it is not specified, or will use + a reasonable default. + :param headers: These will be passed through to the object creation + API as HTTP Headers. + + :raises: ``OpenStackCloudException`` on operation error. + """ + if not filename: + filename = name + + segment_size = self.get_object_segment_size(segment_size) + + if not md5 or not sha256: + (md5, sha256) = self._get_file_hashes(filename) + headers[OBJECT_MD5_KEY] = md5 + headers[OBJECT_SHA256_KEY] = sha256 + header_list = sorted([':'.join([k, v]) for (k, v) in headers.items()]) + + # On some clouds this is not necessary. On others it is. I'm confused. + self.create_container(container) + + if self.is_object_stale(container, name, filename, md5, sha256): + self.log.debug( + "swift uploading {filename} to {container}/{name}".format( + filename=filename, container=container, name=name)) + upload = swift_service.SwiftUploadObject(source=filename, + object_name=name) + for r in self.manager.submitTask(_tasks.ObjectCreate( + container=container, objects=[upload], + options=dict(header=header_list, + segment_size=segment_size))): + if not r['success']: + raise OpenStackCloudException( + 'Failed at action ({action}) [{error}]:'.format(**r)) + + def list_objects(self, container): + try: + return meta.obj_to_dict(self.manager.submitTask( + _tasks.ObjectList(container))) + except swift_exceptions.ClientException as e: + raise OpenStackCloudException( + "Object list failed: %s (%s/%s)" % ( + e.http_reason, e.http_host, e.http_path)) + + def delete_object(self, container, name): + if not self.get_object_metadata(container, name): + return + try: + self.manager.submitTask(_tasks.ObjectDelete( + container=container, obj=name)) + except swift_exceptions.ClientException as e: + raise OpenStackCloudException( + "Object deletion failed: %s (%s/%s)" % ( + e.http_reason, e.http_host, e.http_path)) + + def get_object_metadata(self, container, name): + try: + return self.manager.submitTask(_tasks.ObjectMetadata( + container=container, obj=name)) + except swift_exceptions.ClientException as e: + if e.http_status == 404: + return None + raise OpenStackCloudException( + "Object metadata fetch failed: %s (%s/%s)" % ( + e.http_reason, e.http_host, e.http_path)) + + def create_subnet(self, network_name_or_id, cidr, ip_version=4, + enable_dhcp=False, subnet_name=None, tenant_id=None, + allocation_pools=None, gateway_ip=None, + dns_nameservers=None, host_routes=None, + ipv6_ra_mode=None, ipv6_address_mode=None): + """Create a subnet on a specified network. + + :param string network_name_or_id: + The unique name or ID of the attached network. If a non-unique + name is supplied, an exception is raised. + :param string cidr: + The CIDR. + :param int ip_version: + The IP version, which is 4 or 6. + :param bool enable_dhcp: + Set to ``True`` if DHCP is enabled and ``False`` if disabled. + Default is ``False``. + :param string subnet_name: + The name of the subnet. + :param string tenant_id: + The ID of the tenant who owns the network. Only administrative users + can specify a tenant ID other than their own. + :param list allocation_pools: + A list of dictionaries of the start and end addresses for the + allocation pools. For example:: + + [ + { + "start": "192.168.199.2", + "end": "192.168.199.254" + } + ] + + :param string gateway_ip: + The gateway IP address. When you specify both allocation_pools and + gateway_ip, you must ensure that the gateway IP does not overlap + with the specified allocation pools. + :param list dns_nameservers: + A list of DNS name servers for the subnet. For example:: + + [ "8.8.8.7", "8.8.8.8" ] + + :param list host_routes: + A list of host route dictionaries for the subnet. For example:: + + [ + { + "destination": "0.0.0.0/0", + "nexthop": "123.456.78.9" + }, + { + "destination": "192.168.0.0/24", + "nexthop": "192.168.0.1" + } + ] + + :param string ipv6_ra_mode: + IPv6 Router Advertisement mode. Valid values are: 'dhcpv6-stateful', + 'dhcpv6-stateless', or 'slaac'. + :param string ipv6_address_mode: + IPv6 address mode. Valid values are: 'dhcpv6-stateful', + 'dhcpv6-stateless', or 'slaac'. + + :returns: The new subnet object. + :raises: OpenStackCloudException on operation error. + """ + + network = self.get_network(network_name_or_id) + if not network: + raise OpenStackCloudException( + "Network %s not found." % network_name_or_id) + + # The body of the neutron message for the subnet we wish to create. + # This includes attributes that are required or have defaults. + subnet = { + 'network_id': network['id'], + 'cidr': cidr, + 'ip_version': ip_version, + 'enable_dhcp': enable_dhcp + } + + # Add optional attributes to the message. + if subnet_name: + subnet['name'] = subnet_name + if tenant_id: + subnet['tenant_id'] = tenant_id + if allocation_pools: + subnet['allocation_pools'] = allocation_pools + if gateway_ip: + subnet['gateway_ip'] = gateway_ip + if dns_nameservers: + subnet['dns_nameservers'] = dns_nameservers + if host_routes: + subnet['host_routes'] = host_routes + if ipv6_ra_mode: + subnet['ipv6_ra_mode'] = ipv6_ra_mode + if ipv6_address_mode: + subnet['ipv6_address_mode'] = ipv6_address_mode + + with self._neutron_exceptions( + "Error creating subnet on network " + "{0}".format(network_name_or_id)): + new_subnet = self.manager.submitTask( + _tasks.SubnetCreate(body=dict(subnet=subnet))) + + return new_subnet['subnet'] + + def delete_subnet(self, name_or_id): + """Delete a subnet. + + If a name, instead of a unique UUID, is supplied, it is possible + that we could find more than one matching subnet since names are + not required to be unique. An error will be raised in this case. + + :param name_or_id: Name or ID of the subnet being deleted. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException on operation error. + """ + subnet = self.get_subnet(name_or_id) + if not subnet: + self.log.debug("Subnet %s not found for deleting" % name_or_id) + return False + + with self._neutron_exceptions( + "Error deleting subnet {0}".format(name_or_id)): + self.manager.submitTask( + _tasks.SubnetDelete(subnet=subnet['id'])) + return True + + def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, + gateway_ip=None, allocation_pools=None, + dns_nameservers=None, host_routes=None): + """Update an existing subnet. + + :param string name_or_id: + Name or ID of the subnet to update. + :param string subnet_name: + The new name of the subnet. + :param bool enable_dhcp: + Set to ``True`` if DHCP is enabled and ``False`` if disabled. + :param string gateway_ip: + The gateway IP address. When you specify both allocation_pools and + gateway_ip, you must ensure that the gateway IP does not overlap + with the specified allocation pools. + :param list allocation_pools: + A list of dictionaries of the start and end addresses for the + allocation pools. For example:: + + [ + { + "start": "192.168.199.2", + "end": "192.168.199.254" + } + ] + + :param list dns_nameservers: + A list of DNS name servers for the subnet. For example:: + + [ "8.8.8.7", "8.8.8.8" ] + + :param list host_routes: + A list of host route dictionaries for the subnet. For example:: + + [ + { + "destination": "0.0.0.0/0", + "nexthop": "123.456.78.9" + }, + { + "destination": "192.168.0.0/24", + "nexthop": "192.168.0.1" + } + ] + + :returns: The updated subnet object. + :raises: OpenStackCloudException on operation error. + """ + subnet = {} + if subnet_name: + subnet['name'] = subnet_name + if enable_dhcp is not None: + subnet['enable_dhcp'] = enable_dhcp + if gateway_ip: + subnet['gateway_ip'] = gateway_ip + if allocation_pools: + subnet['allocation_pools'] = allocation_pools + if dns_nameservers: + subnet['dns_nameservers'] = dns_nameservers + if host_routes: + subnet['host_routes'] = host_routes + + if not subnet: + self.log.debug("No subnet data to update") + return + + curr_subnet = self.get_subnet(name_or_id) + if not curr_subnet: + raise OpenStackCloudException( + "Subnet %s not found." % name_or_id) + + with self._neutron_exceptions( + "Error updating subnet {0}".format(name_or_id)): + new_subnet = self.manager.submitTask( + _tasks.SubnetUpdate( + subnet=curr_subnet['id'], body=dict(subnet=subnet))) + # Turns out neutron returns an actual dict, so no need for the + # use of meta.obj_to_dict() here (which would not work against + # a dict). + return new_subnet['subnet'] + + @_utils.valid_kwargs('name', 'admin_state_up', 'mac_address', 'fixed_ips', + 'subnet_id', 'ip_address', 'security_groups', + 'allowed_address_pairs', 'extra_dhcp_opts', + 'device_owner', 'device_id') + def create_port(self, network_id, **kwargs): + """Create a port + + :param network_id: The ID of the network. (Required) + :param name: A symbolic name for the port. (Optional) + :param admin_state_up: The administrative status of the port, + which is up (true, default) or down (false). (Optional) + :param mac_address: The MAC address. (Optional) + :param fixed_ips: List of ip_addresses and subnet_ids. See subnet_id + and ip_address. (Optional) + For example:: + + [ + { + "ip_address": "10.29.29.13", + "subnet_id": "a78484c4-c380-4b47-85aa-21c51a2d8cbd" + }, ... + ] + :param subnet_id: If you specify only a subnet ID, OpenStack Networking + allocates an available IP from that subnet to the port. (Optional) + If you specify both a subnet ID and an IP address, OpenStack + Networking tries to allocate the specified address to the port. + :param ip_address: If you specify both a subnet ID and an IP address, + OpenStack Networking tries to allocate the specified address to + the port. + :param security_groups: List of security group UUIDs. (Optional) + :param allowed_address_pairs: Allowed address pairs list (Optional) + For example:: + + [ + { + "ip_address": "23.23.23.1", + "mac_address": "fa:16:3e:c4:cd:3f" + }, ... + ] + :param extra_dhcp_opts: Extra DHCP options. (Optional). + For example:: + + [ + { + "opt_name": "opt name1", + "opt_value": "value1" + }, ... + ] + :param device_owner: The ID of the entity that uses this port. + For example, a DHCP agent. (Optional) + :param device_id: The ID of the device that uses this port. + For example, a virtual server. (Optional) + + :returns: a dictionary describing the created port. + + :raises: ``OpenStackCloudException`` on operation error. + """ + kwargs['network_id'] = network_id + + with self._neutron_exceptions( + "Error creating port for network {0}".format(network_id)): + return self.manager.submitTask( + _tasks.PortCreate(body={'port': kwargs}))['port'] + + @_utils.valid_kwargs('name', 'admin_state_up', 'fixed_ips', + 'security_groups', 'allowed_address_pairs', + 'extra_dhcp_opts', 'device_owner') + def update_port(self, name_or_id, **kwargs): + """Update a port + + Note: to unset an attribute use None value. To leave an attribute + untouched just omit it. + + :param name_or_id: name or id of the port to update. (Required) + :param name: A symbolic name for the port. (Optional) + :param admin_state_up: The administrative status of the port, + which is up (true) or down (false). (Optional) + :param fixed_ips: List of ip_addresses and subnet_ids. (Optional) + If you specify only a subnet ID, OpenStack Networking allocates + an available IP from that subnet to the port. + If you specify both a subnet ID and an IP address, OpenStack + Networking tries to allocate the specified address to the port. + For example:: + + [ + { + "ip_address": "10.29.29.13", + "subnet_id": "a78484c4-c380-4b47-85aa-21c51a2d8cbd" + }, ... + ] + :param security_groups: List of security group UUIDs. (Optional) + :param allowed_address_pairs: Allowed address pairs list (Optional) + For example:: + + [ + { + "ip_address": "23.23.23.1", + "mac_address": "fa:16:3e:c4:cd:3f" + }, ... + ] + :param extra_dhcp_opts: Extra DHCP options. (Optional). + For example:: + + [ + { + "opt_name": "opt name1", + "opt_value": "value1" + }, ... + ] + :param device_owner: The ID of the entity that uses this port. + For example, a DHCP agent. (Optional) + + :returns: a dictionary describing the updated port. + + :raises: OpenStackCloudException on operation error. + """ + port = self.get_port(name_or_id=name_or_id) + if port is None: + raise OpenStackCloudException( + "failed to find port '{port}'".format(port=name_or_id)) + + with self._neutron_exceptions( + "Error updating port {0}".format(name_or_id)): + return self.manager.submitTask( + _tasks.PortUpdate( + port=port['id'], body={'port': kwargs}))['port'] + + def delete_port(self, name_or_id): + """Delete a port + + :param name_or_id: id or name of the port to delete. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException on operation error. + """ + port = self.get_port(name_or_id=name_or_id) + if port is None: + self.log.debug("Port %s not found for deleting" % name_or_id) + return False + + with self._neutron_exceptions( + "Error deleting port {0}".format(name_or_id)): + self.manager.submitTask(_tasks.PortDelete(port=port['id'])) + return True + + def create_security_group(self, name, description): + """Create a new security group + + :param string name: A name for the security group. + :param string description: Describes the security group. + + :returns: A dict representing the new security group. + + :raises: OpenStackCloudException on operation error. + :raises: OpenStackCloudUnavailableFeature if security groups are + not supported on this cloud. + """ + if self.secgroup_source == 'neutron': + with self._neutron_exceptions( + "Error creating security group {0}".format(name)): + group = self.manager.submitTask( + _tasks.NeutronSecurityGroupCreate( + body=dict(security_group=dict(name=name, + description=description)) + ) + ) + return group['security_group'] + + elif self.secgroup_source == 'nova': + try: + group = meta.obj_to_dict( + self.manager.submitTask( + _tasks.NovaSecurityGroupCreate( + name=name, description=description + ) + ) + ) + except Exception as e: + raise OpenStackCloudException( + "failed to create security group '{name}': {msg}".format( + name=name, msg=str(e))) + return _utils.normalize_nova_secgroups([group])[0] + + # Security groups not supported + else: + raise OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + + def delete_security_group(self, name_or_id): + """Delete a security group + + :param string name_or_id: The name or unique ID of the security group. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException on operation error. + :raises: OpenStackCloudUnavailableFeature if security groups are + not supported on this cloud. + """ + secgroup = self.get_security_group(name_or_id) + if secgroup is None: + self.log.debug('Security group %s not found for deleting' % + name_or_id) + return False + + if self.secgroup_source == 'neutron': + with self._neutron_exceptions( + "Error deleting security group {0}".format(name_or_id)): + self.manager.submitTask( + _tasks.NeutronSecurityGroupDelete( + security_group=secgroup['id'] + ) + ) + return True + + elif self.secgroup_source == 'nova': + try: + self.manager.submitTask( + _tasks.NovaSecurityGroupDelete(group=secgroup['id']) + ) + except Exception as e: + raise OpenStackCloudException( + "failed to delete security group '{group}': {msg}".format( + group=name_or_id, msg=str(e))) + return True + + # Security groups not supported + else: + raise OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + + @_utils.valid_kwargs('name', 'description') + def update_security_group(self, name_or_id, **kwargs): + """Update a security group + + :param string name_or_id: Name or ID of the security group to update. + :param string name: New name for the security group. + :param string description: New description for the security group. + + :returns: A dictionary describing the updated security group. + + :raises: OpenStackCloudException on operation error. + """ + secgroup = self.get_security_group(name_or_id) + + if secgroup is None: + raise OpenStackCloudException( + "Security group %s not found." % name_or_id) + + if self.secgroup_source == 'neutron': + with self._neutron_exceptions( + "Error updating security group {0}".format(name_or_id)): + group = self.manager.submitTask( + _tasks.NeutronSecurityGroupUpdate( + security_group=secgroup['id'], + body={'security_group': kwargs}) + ) + return group['security_group'] + + elif self.secgroup_source == 'nova': + try: + group = meta.obj_to_dict( + self.manager.submitTask( + _tasks.NovaSecurityGroupUpdate( + group=secgroup['id'], **kwargs) + ) + ) + except Exception as e: + raise OpenStackCloudException( + "failed to update security group '{group}': {msg}".format( + group=name_or_id, msg=str(e))) + return _utils.normalize_nova_secgroups([group])[0] + + # Security groups not supported + else: + raise OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + + def create_security_group_rule(self, + secgroup_name_or_id, + port_range_min=None, + port_range_max=None, + protocol=None, + remote_ip_prefix=None, + remote_group_id=None, + direction='ingress', + ethertype='IPv4'): + """Create a new security group rule + + :param string secgroup_name_or_id: + The security group name or ID to associate with this security + group rule. If a non-unique group name is given, an exception + is raised. + :param int port_range_min: + The minimum port number in the range that is matched by the + security group rule. If the protocol is TCP or UDP, this value + must be less than or equal to the port_range_max attribute value. + If nova is used by the cloud provider for security groups, then + a value of None will be transformed to -1. + :param int port_range_max: + The maximum port number in the range that is matched by the + security group rule. The port_range_min attribute constrains the + port_range_max attribute. If nova is used by the cloud provider + for security groups, then a value of None will be transformed + to -1. + :param string protocol: + The protocol that is matched by the security group rule. Valid + values are None, tcp, udp, and icmp. + :param string remote_ip_prefix: + The remote IP prefix to be associated with this security group + rule. This attribute matches the specified IP prefix as the + source IP address of the IP packet. + :param string remote_group_id: + The remote group ID to be associated with this security group + rule. + :param string direction: + Ingress or egress: The direction in which the security group + rule is applied. For a compute instance, an ingress security + group rule is applied to incoming (ingress) traffic for that + instance. An egress rule is applied to traffic leaving the + instance. + :param string ethertype: + Must be IPv4 or IPv6, and addresses represented in CIDR must + match the ingress or egress rules. + + :returns: A dict representing the new security group rule. + + :raises: OpenStackCloudException on operation error. + """ + + secgroup = self.get_security_group(secgroup_name_or_id) + if not secgroup: + raise OpenStackCloudException( + "Security group %s not found." % secgroup_name_or_id) + + if self.secgroup_source == 'neutron': + # NOTE: Nova accepts -1 port numbers, but Neutron accepts None + # as the equivalent value. + rule_def = { + 'security_group_id': secgroup['id'], + 'port_range_min': + None if port_range_min == -1 else port_range_min, + 'port_range_max': + None if port_range_max == -1 else port_range_max, + 'protocol': protocol, + 'remote_ip_prefix': remote_ip_prefix, + 'remote_group_id': remote_group_id, + 'direction': direction, + 'ethertype': ethertype + } + + with self._neutron_exceptions( + "Error creating security group rule"): + rule = self.manager.submitTask( + _tasks.NeutronSecurityGroupRuleCreate( + body={'security_group_rule': rule_def}) + ) + return rule['security_group_rule'] + + elif self.secgroup_source == 'nova': + # NOTE: Neutron accepts None for protocol. Nova does not. + if protocol is None: + raise OpenStackCloudException('Protocol must be specified') + + if direction == 'egress': + self.log.debug( + 'Rule creation failed: Nova does not support egress rules' + ) + raise OpenStackCloudException('No support for egress rules') + + # NOTE: Neutron accepts None for ports, but Nova requires -1 + # as the equivalent value for ICMP. + # + # For TCP/UDP, if both are None, Neutron allows this and Nova + # represents this as all ports (1-65535). Nova does not accept + # None values, so to hide this difference, we will automatically + # convert to the full port range. If only a single port value is + # specified, it will error as normal. + if protocol == 'icmp': + if port_range_min is None: + port_range_min = -1 + if port_range_max is None: + port_range_max = -1 + elif protocol in ['tcp', 'udp']: + if port_range_min is None and port_range_max is None: + port_range_min = 1 + port_range_max = 65535 + + try: + rule = meta.obj_to_dict( + self.manager.submitTask( + _tasks.NovaSecurityGroupRuleCreate( + parent_group_id=secgroup['id'], + ip_protocol=protocol, + from_port=port_range_min, + to_port=port_range_max, + cidr=remote_ip_prefix, + group_id=remote_group_id + ) + ) + ) + except Exception as e: + raise OpenStackCloudException( + "failed to create security group rule: {msg}".format( + msg=str(e))) + return _utils.normalize_nova_secgroup_rules([rule])[0] + + # Security groups not supported + else: + raise OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + + def delete_security_group_rule(self, rule_id): + """Delete a security group rule + + :param string rule_id: The unique ID of the security group rule. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException on operation error. + :raises: OpenStackCloudUnavailableFeature if security groups are + not supported on this cloud. + """ + + if self.secgroup_source == 'neutron': + try: + with self._neutron_exceptions( + "Error deleting security group rule " + "{0}".format(rule_id)): + self.manager.submitTask( + _tasks.NeutronSecurityGroupRuleDelete( + security_group_rule=rule_id) + ) + except OpenStackCloudResourceNotFound: + return False + return True + + elif self.secgroup_source == 'nova': + try: + self.manager.submitTask( + _tasks.NovaSecurityGroupRuleDelete(rule=rule_id) + ) + except nova_exceptions.NotFound: + return False + except Exception as e: + raise OpenStackCloudException( + "failed to delete security group rule {id}: {msg}".format( + id=rule_id, msg=str(e))) + return True + + # Security groups not supported + else: + raise OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py new file mode 100644 index 000000000..f850509e3 --- /dev/null +++ b/shade/operatorcloud.py @@ -0,0 +1,1438 @@ +# 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 jsonpatch + +from ironicclient import client as ironic_client +from ironicclient import exceptions as ironic_exceptions +from novaclient import exceptions as nova_exceptions + +from shade.exc import * # noqa +from shade import meta +from shade import openstackcloud +from shade import _tasks +from shade import _utils + + +class OperatorCloud(openstackcloud.OpenStackCloud): + """Represent a privileged/operator connection to an OpenStack Cloud. + + `OperatorCloud` is the entry point for all admin operations, regardless + of which OpenStack service those operations are for. + + See the :class:`OpenStackCloud` class for a description of most options. + """ + + # Set the ironic API microversion to a known-good + # supported/tested with the contents of shade. + # + # Note(TheJulia): Defaulted to version 1.6 as the ironic + # state machine changes which will increment the version + # and break an automatic transition of an enrolled node + # to an available state. Locking the version is intended + # to utilize the original transition until shade supports + # calling for node inspection to allow the transition to + # take place automatically. + ironic_api_microversion = '1.6' + + @property + def ironic_client(self): + if self._ironic_client is None: + self._ironic_client = self._get_client( + 'baremetal', ironic_client.Client, + os_ironic_api_version=self.ironic_api_microversion) + return self._ironic_client + + def list_nics(self): + try: + return meta.obj_list_to_dict( + self.manager.submitTask(_tasks.MachinePortList()) + ) + except Exception as e: + raise OpenStackCloudException( + "Error fetching machine port list: %s" % e) + + def list_nics_for_machine(self, uuid): + try: + return meta.obj_list_to_dict( + self.manager.submitTask( + _tasks.MachineNodePortList(node_id=uuid)) + ) + except Exception as e: + raise OpenStackCloudException( + "Error fetching port list for node %s: %s" % (uuid, e)) + + def get_nic_by_mac(self, mac): + try: + return meta.obj_to_dict( + self.manager.submitTask( + _tasks.MachineNodePortGet(port_id=mac)) + ) + except ironic_exceptions.ClientException: + return None + + def list_machines(self): + return meta.obj_list_to_dict( + self.manager.submitTask(_tasks.MachineNodeList()) + ) + + def get_machine(self, name_or_id): + """Get Machine by name or uuid + + Search the baremetal host out by utilizing the supplied id value + which can consist of a name or UUID. + + :param name_or_id: A node name or UUID that will be looked up. + + :returns: Dictonary representing the node found or None if no nodes + are found. + """ + try: + return meta.obj_to_dict( + self.manager.submitTask( + _tasks.MachineNodeGet(node_id=name_or_id)) + ) + except ironic_exceptions.ClientException: + return None + + def get_machine_by_mac(self, mac): + """Get machine by port MAC address + + :param mac: Port MAC address to query in order to return a node. + + :returns: Dictonary representing the node found or None + if the node is not found. + """ + try: + port = self.manager.submitTask( + _tasks.MachinePortGetByAddress(address=mac)) + return meta.obj_to_dict( + self.manager.submitTask( + _tasks.MachineNodeGet(node_id=port.node_uuid)) + ) + except ironic_exceptions.ClientException: + return None + + def inspect_machine(self, name_or_id, wait=False, timeout=3600): + """Inspect a Barmetal machine + + Engages the Ironic node inspection behavior in order to collect + metadata about the baremetal machine. + + :param name_or_id: String representing machine name or UUID value in + order to identify the machine. + + :param wait: Boolean value controlling if the method is to wait for + the desired state to be reached or a failure to occur. + + :param timeout: Integer value, defautling to 3600 seconds, for the$ + wait state to reach completion. + + :returns: Dictonary representing the current state of the machine + upon exit of the method. + """ + + return_to_available = False + + machine = self.get_machine(name_or_id) + if not machine: + raise OpenStackCloudException( + "Machine inspection failed to find: %s." % name_or_id) + + # NOTE(TheJulia): If in available state, we can do this, however + # We need to to move the host back to m + if "available" in machine['provision_state']: + return_to_available = True + # NOTE(TheJulia): Changing available machine to managedable state + # and due to state transitions we need to until that transition has + # completd. + self.node_set_provision_state(machine['uuid'], 'manage', + wait=True, timeout=timeout) + elif ("manage" not in machine['provision_state'] and + "inspect failed" not in machine['provision_state']): + raise OpenStackCloudException( + "Machine must be in 'manage' or 'available' state to " + "engage inspection: Machine: %s State: %s" + % (machine['uuid'], machine['provision_state'])) + try: + machine = self.node_set_provision_state(machine['uuid'], 'inspect') + if wait: + for count in _utils._iterate_timeout( + timeout, + "Timeout waiting for node transition to " + "target state of 'inpection'"): + machine = self.get_machine(name_or_id) + + if "inspect failed" in machine['provision_state']: + raise OpenStackCloudException( + "Inspection of node %s failed, last error: %s" + % (machine['uuid'], machine['last_error'])) + + if "manageable" in machine['provision_state']: + break + + if return_to_available: + machine = self.node_set_provision_state( + machine['uuid'], 'provide', wait=wait, timeout=timeout) + + return(machine) + + except Exception as e: + raise OpenStackCloudException( + "Error inspecting machine: %s" % e) + + def register_machine(self, nics, wait=False, timeout=3600, + lock_timeout=600, **kwargs): + """Register Baremetal with Ironic + + Allows for the registration of Baremetal nodes with Ironic + and population of pertinant node information or configuration + to be passed to the Ironic API for the node. + + This method also creates ports for a list of MAC addresses passed + in to be utilized for boot and potentially network configuration. + + If a failure is detected creating the network ports, any ports + created are deleted, and the node is removed from Ironic. + + :param list nics: + An array of MAC addresses that represent the + network interfaces for the node to be created. + + Example:: + + [ + {'mac': 'aa:bb:cc:dd:ee:01'}, + {'mac': 'aa:bb:cc:dd:ee:02'} + ] + + :param wait: Boolean value, defaulting to false, to wait for the + node to reach the available state where the node can be + provisioned. It must be noted, when set to false, the + method will still wait for locks to clear before sending + the next required command. + + :param timeout: Integer value, defautling to 3600 seconds, for the + wait state to reach completion. + + :param lock_timeout: Integer value, defaulting to 600 seconds, for + locks to clear. + + :param kwargs: Key value pairs to be passed to the Ironic API, + including uuid, name, chassis_uuid, driver_info, + parameters. + + :raises: OpenStackCloudException on operation error. + + :returns: Returns a dictonary representing the new + baremetal node. + """ + try: + machine = meta.obj_to_dict( + self.manager.submitTask(_tasks.MachineCreate(**kwargs))) + + except Exception as e: + raise OpenStackCloudException( + "Error registering machine with Ironic: %s" % str(e)) + + created_nics = [] + try: + for row in nics: + nic = self.manager.submitTask( + _tasks.MachinePortCreate(address=row['mac'], + node_uuid=machine['uuid'])) + created_nics.append(nic.uuid) + + except Exception as e: + self.log.debug("ironic NIC registration failed", exc_info=True) + # TODO(mordred) Handle failures here + try: + for uuid in created_nics: + try: + self.manager.submitTask( + _tasks.MachinePortDelete( + port_id=uuid)) + except: + pass + finally: + self.manager.submitTask( + _tasks.MachineDelete(node_id=machine['uuid'])) + raise OpenStackCloudException( + "Error registering NICs with the baremetal service: %s" + % str(e)) + + try: + if wait: + for count in _utils._iterate_timeout( + timeout, + "Timeout waiting for node transition to " + "available state"): + + machine = self.get_machine(machine['uuid']) + + # Note(TheJulia): Per the Ironic state code, a node + # that fails returns to enroll state, which means a failed + # node cannot be determined at this point in time. + if machine['provision_state'] in ['enroll']: + self.node_set_provision_state( + machine['uuid'], 'manage') + elif machine['provision_state'] in ['manageable']: + self.node_set_provision_state( + machine['uuid'], 'provide') + elif machine['last_error'] is not None: + raise OpenStackCloudException( + "Machine encountered a failure: %s" + % machine['last_error']) + + # Note(TheJulia): Earlier versions of Ironic default to + # None and later versions default to available up until + # the introduction of enroll state. + # Note(TheJulia): The node will transition through + # cleaning if it is enabled, and we will wait for + # completion. + elif machine['provision_state'] in ['available', None]: + break + + else: + if machine['provision_state'] in ['enroll']: + self.node_set_provision_state(machine['uuid'], 'manage') + # Note(TheJulia): We need to wait for the lock to clear + # before we attempt to set the machine into provide state + # which allows for the transition to available. + for count in _utils._iterate_timeout( + lock_timeout, + "Timeout waiting for reservation to clear " + "before setting provide state"): + machine = self.get_machine(machine['uuid']) + if (machine['reservation'] is None and + machine['provision_state'] is not 'enroll'): + + self.node_set_provision_state( + machine['uuid'], 'provide') + machine = self.get_machine(machine['uuid']) + break + + elif machine['provision_state'] in [ + 'cleaning', + 'available']: + break + + elif machine['last_error'] is not None: + raise OpenStackCloudException( + "Machine encountered a failure: %s" + % machine['last_error']) + + except Exception as e: + raise OpenStackCloudException( + "Error transitioning node to available state: %s" + % e) + return machine + + def unregister_machine(self, nics, uuid, wait=False, timeout=600): + """Unregister Baremetal from Ironic + + Removes entries for Network Interfaces and baremetal nodes + from an Ironic API + + :param list nics: An array of strings that consist of MAC addresses + to be removed. + :param string uuid: The UUID of the node to be deleted. + + :param wait: Boolean value, defaults to false, if to block the method + upon the final step of unregistering the machine. + + :param timeout: Integer value, representing seconds with a default + value of 600, which controls the maximum amount of + time to block the method's completion on. + + :raises: OpenStackCloudException on operation failure. + """ + + machine = self.get_machine(uuid) + invalid_states = ['active', 'cleaning', 'clean wait', 'clean failed'] + if machine['provision_state'] in invalid_states: + raise OpenStackCloudException( + "Error unregistering node '%s' due to current provision " + "state '%s'" % (uuid, machine['provision_state'])) + + for nic in nics: + try: + port = self.manager.submitTask( + _tasks.MachinePortGetByAddress(address=nic['mac'])) + self.manager.submitTask( + _tasks.MachinePortDelete(port_id=port.uuid)) + except Exception as e: + raise OpenStackCloudException( + "Error removing NIC '%s' from baremetal API for " + "node '%s'. Error: %s" % (nic, uuid, str(e))) + try: + self.manager.submitTask( + _tasks.MachineDelete(node_id=uuid)) + if wait: + for count in _utils._iterate_timeout( + timeout, + "Timeout waiting for machine to be deleted"): + if not self.get_machine(uuid): + break + + except Exception as e: + raise OpenStackCloudException( + "Error unregistering machine %s from the baremetal API. " + "Error: %s" % (uuid, str(e))) + + def patch_machine(self, name_or_id, patch): + """Patch Machine Information + + This method allows for an interface to manipulate node entries + within Ironic. Specifically, it is a pass-through for the + ironicclient.nodes.update interface which allows the Ironic Node + properties to be updated. + + :param node_id: The server object to attach to. + :param patch: + The JSON Patch document is a list of dictonary objects + that comply with RFC 6902 which can be found at + https://tools.ietf.org/html/rfc6902. + + Example patch construction:: + + patch=[] + patch.append({ + 'op': 'remove', + 'path': '/instance_info' + }) + patch.append({ + 'op': 'replace', + 'path': '/name', + 'value': 'newname' + }) + patch.append({ + 'op': 'add', + 'path': '/driver_info/username', + 'value': 'administrator' + }) + + :raises: OpenStackCloudException on operation error. + + :returns: Dictonary representing the newly updated node. + """ + + try: + return meta.obj_to_dict( + self.manager.submitTask( + _tasks.MachinePatch(node_id=name_or_id, + patch=patch, + http_method='PATCH'))) + except Exception as e: + raise OpenStackCloudException( + "Error updating machine via patch operation. node: %s. " + "%s" % (name_or_id, str(e))) + + def update_machine(self, name_or_id, chassis_uuid=None, driver=None, + driver_info=None, name=None, instance_info=None, + instance_uuid=None, properties=None): + """Update a machine with new configuration information + + A user-friendly method to perform updates of a machine, in whole or + part. + + :param string name_or_id: A machine name or UUID to be updated. + :param string chassis_uuid: Assign a chassis UUID to the machine. + NOTE: As of the Kilo release, this value + cannot be changed once set. If a user + attempts to change this value, then the + Ironic API, as of Kilo, will reject the + request. + :param string driver: The driver name for controlling the machine. + :param dict driver_info: The dictonary defining the configuration + that the driver will utilize to control + the machine. Permutations of this are + dependent upon the specific driver utilized. + :param string name: A human relatable name to represent the machine. + :param dict instance_info: A dictonary of configuration information + that conveys to the driver how the host + is to be configured when deployed. + be deployed to the machine. + :param string instance_uuid: A UUID value representing the instance + that the deployed machine represents. + :param dict properties: A dictonary defining the properties of a + machine. + + :raises: OpenStackCloudException on operation error. + + :returns: Dictonary containing a machine sub-dictonary consisting + of the updated data returned from the API update operation, + and a list named changes which contains all of the API paths + that received updates. + """ + machine = self.get_machine(name_or_id) + if not machine: + raise OpenStackCloudException( + "Machine update failed to find Machine: %s. " % name_or_id) + + machine_config = {} + new_config = {} + + try: + if chassis_uuid: + machine_config['chassis_uuid'] = machine['chassis_uuid'] + new_config['chassis_uuid'] = chassis_uuid + + if driver: + machine_config['driver'] = machine['driver'] + new_config['driver'] = driver + + if driver_info: + machine_config['driver_info'] = machine['driver_info'] + new_config['driver_info'] = driver_info + + if name: + machine_config['name'] = machine['name'] + new_config['name'] = name + + if instance_info: + machine_config['instance_info'] = machine['instance_info'] + new_config['instance_info'] = instance_info + + if instance_uuid: + machine_config['instance_uuid'] = machine['instance_uuid'] + new_config['instance_uuid'] = instance_uuid + + if properties: + machine_config['properties'] = machine['properties'] + new_config['properties'] = properties + except KeyError as e: + self.log.debug( + "Unexpected machine response missing key %s [%s]" % ( + e.args[0], name_or_id)) + raise OpenStackCloudException( + "Machine update failed - machine [%s] missing key %s. " + "Potential API issue." + % (name_or_id, e.args[0])) + + try: + patch = jsonpatch.JsonPatch.from_diff(machine_config, new_config) + except Exception as e: + raise OpenStackCloudException( + "Machine update failed - Error generating JSON patch object " + "for submission to the API. Machine: %s Error: %s" + % (name_or_id, str(e))) + + try: + if not patch: + return dict( + node=machine, + changes=None + ) + else: + machine = self.patch_machine(machine['uuid'], list(patch)) + change_list = [] + for change in list(patch): + change_list.append(change['path']) + return dict( + node=machine, + changes=change_list + ) + except Exception as e: + raise OpenStackCloudException( + "Machine update failed - patch operation failed Machine: %s " + "Error: %s" % (name_or_id, str(e))) + + def validate_node(self, uuid): + try: + ifaces = self.manager.submitTask( + _tasks.MachineNodeValidate(node_uuid=uuid)) + except Exception as e: + raise OpenStackCloudException(str(e)) + + if not ifaces.deploy or not ifaces.power: + raise OpenStackCloudException( + "ironic node %s failed to validate. " + "(deploy: %s, power: %s)" % (ifaces.deploy, ifaces.power)) + + def node_set_provision_state(self, + name_or_id, + state, + configdrive=None, + wait=False, + timeout=3600): + """Set Node Provision State + + Enables a user to provision a Machine and optionally define a + config drive to be utilized. + + :param string name_or_id: The Name or UUID value representing the + baremetal node. + :param string state: The desired provision state for the + baremetal node. + :param string configdrive: An optional URL or file or path + representing the configdrive. In the + case of a directory, the client API + will create a properly formatted + configuration drive file and post the + file contents to the API for + deployment. + :param boolean wait: A boolean value, defaulted to false, to control + if the method will wait for the desire end state + to be reached before returning. + :param integer timeout: Integer value, defaulting to 3600 seconds, + representing the amount of time to wait for + the desire end state to be reached. + + :raises: OpenStackCloudException on operation error. + + :returns: Dictonary representing the current state of the machine + upon exit of the method. + """ + try: + machine = self.manager.submitTask( + _tasks.MachineSetProvision(node_uuid=name_or_id, + state=state, + configdrive=configdrive)) + + if wait: + for count in _utils._iterate_timeout( + timeout, + "Timeout waiting for node transition to " + "target state of '%s'" % state): + machine = self.get_machine(name_or_id) + if state in machine['provision_state']: + break + if ("available" in machine['provision_state'] and + "provide" in state): + break + else: + machine = self.get_machine(name_or_id) + return machine + + except Exception as e: + raise OpenStackCloudException( + "Baremetal machine node failed change provision" + " state to {state}: {msg}".format(state=state, + msg=str(e))) + + def set_machine_maintenance_state( + self, + name_or_id, + state=True, + reason=None): + """Set Baremetal Machine Maintenance State + + Sets Baremetal maintenance state and maintenance reason. + + :param string name_or_id: The Name or UUID value representing the + baremetal node. + :param boolean state: The desired state of the node. True being in + maintenance where as False means the machine + is not in maintenance mode. This value + defaults to True if not explicitly set. + :param string reason: An optional freeform string that is supplied to + the baremetal API to allow for notation as to why + the node is in maintenance state. + + :raises: OpenStackCloudException on operation error. + + :returns: None + """ + try: + if state: + result = self.manager.submitTask( + _tasks.MachineSetMaintenance(node_id=name_or_id, + state='true', + maint_reason=reason)) + else: + result = self.manager.submitTask( + _tasks.MachineSetMaintenance(node_id=name_or_id, + state='false')) + if result is not None: + raise OpenStackCloudException( + "Failed setting machine maintenance state to %s " + "on node %s. Received: %s" % ( + state, name_or_id, result)) + return None + except Exception as e: + raise OpenStackCloudException( + "Error setting machine maintenance state to %s " + "on node %s: %s" % (state, name_or_id, str(e))) + + def remove_machine_from_maintenance(self, name_or_id): + """Remove Baremetal Machine from Maintenance State + + Similarly to set_machine_maintenance_state, this method + removes a machine from maintenance state. It must be noted + that this method simpily calls set_machine_maintenace_state + for the name_or_id requested and sets the state to False. + + :param string name_or_id: The Name or UUID value representing the + baremetal node. + + :raises: OpenStackCloudException on operation error. + + :returns: None + """ + self.set_machine_maintenance_state(name_or_id, False) + + def _set_machine_power_state(self, name_or_id, state): + """Set machine power state to on or off + + This private method allows a user to turn power on or off to + a node via the Baremetal API. + + :params string name_or_id: A string representing the baremetal + node to have power turned to an "on" + state. + :params string state: A value of "on", "off", or "reboot" that is + passed to the baremetal API to be asserted to + the machine. In the case of the "reboot" state, + Ironic will return the host to the "on" state. + + :raises: OpenStackCloudException on operation error or. + + :returns: None + """ + try: + power = self.manager.submitTask( + _tasks.MachineSetPower(node_id=name_or_id, + state=state)) + if power is not None: + raise OpenStackCloudException( + "Failed setting machine power state %s on node %s. " + "Received: %s" % (state, name_or_id, power)) + return None + except Exception as e: + raise OpenStackCloudException( + "Error setting machine power state %s on node %s. " + "Error: %s" % (state, name_or_id, str(e))) + + def set_machine_power_on(self, name_or_id): + """Activate baremetal machine power + + This is a method that sets the node power state to "on". + + :params string name_or_id: A string representing the baremetal + node to have power turned to an "on" + state. + + :raises: OpenStackCloudException on operation error. + + :returns: None + """ + self._set_machine_power_state(name_or_id, 'on') + + def set_machine_power_off(self, name_or_id): + """De-activate baremetal machine power + + This is a method that sets the node power state to "off". + + :params string name_or_id: A string representing the baremetal + node to have power turned to an "off" + state. + + :raises: OpenStackCloudException on operation error. + + :returns: + """ + self._set_machine_power_state(name_or_id, 'off') + + def set_machine_power_reboot(self, name_or_id): + """De-activate baremetal machine power + + This is a method that sets the node power state to "reboot", which + in essence changes the machine power state to "off", and that back + to "on". + + :params string name_or_id: A string representing the baremetal + node to have power turned to an "off" + state. + + :raises: OpenStackCloudException on operation error. + + :returns: None + """ + self._set_machine_power_state(name_or_id, 'reboot') + + def activate_node(self, uuid, configdrive=None): + self.node_set_provision_state(uuid, 'active', configdrive) + + def deactivate_node(self, uuid): + self.node_set_provision_state(uuid, 'deleted') + + def set_node_instance_info(self, uuid, patch): + try: + return meta.obj_to_dict( + self.manager.submitTask( + _tasks.MachineNodeUpdate(node_id=uuid, patch=patch)) + ) + except Exception as e: + raise OpenStackCloudException(str(e)) + + def purge_node_instance_info(self, uuid): + patch = [] + patch.append({'op': 'remove', 'path': '/instance_info'}) + try: + return meta.obj_to_dict( + self.manager.submitTask( + _tasks.MachineNodeUpdate(node_id=uuid, patch=patch)) + ) + except Exception as e: + raise OpenStackCloudException(str(e)) + + @_utils.valid_kwargs('type', 'service_type', 'description') + def create_service(self, name, **kwargs): + """Create a service. + + :param name: Service name. + :param type: Service type. (type or service_type required.) + :param service_type: Service type. (type or service_type required.) + :param description: Service description (optional). + + :returns: a dict containing the services description, i.e. the + following attributes:: + - id: + - name: + - type: + - service_type: + - description: + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call. + + """ + service_type = kwargs.get('type', kwargs.get('service_type')) + description = kwargs.get('description', None) + try: + if self.cloud_config.get_api_version('identity').startswith('2'): + service_kwargs = {'service_type': service_type} + else: + service_kwargs = {'type': service_type} + + service = self.manager.submitTask(_tasks.ServiceCreate( + name=name, description=description, **service_kwargs)) + except Exception as e: + raise OpenStackCloudException( + "Failed to create service {name}: {msg}".format( + name=name, msg=str(e))) + return meta.obj_to_dict(service) + + def list_services(self): + """List all Keystone services. + + :returns: a list of dict containing the services description. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call. + """ + try: + services = self.manager.submitTask(_tasks.ServiceList()) + except Exception as e: + raise OpenStackCloudException(str(e)) + return _utils.normalize_keystone_services( + meta.obj_list_to_dict(services)) + + def search_services(self, name_or_id=None, filters=None): + """Search Keystone services. + + :param name_or_id: Name or id of the desired service. + :param filters: a dict containing additional filters to use. e.g. + {'type': 'network'}. + + :returns: a list of dict containing the services description. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call. + """ + services = self.list_services() + return _utils._filter_list(services, name_or_id, filters) + + def get_service(self, name_or_id, filters=None): + """Get exactly one Keystone service. + + :param name_or_id: Name or id of the desired service. + :param filters: a dict containing additional filters to use. e.g. + {'type': 'network'} + + :returns: a dict containing the services description, i.e. the + following attributes:: + - id: + - name: + - type: + - description: + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call or if multiple matches are found. + """ + return _utils._get_entity(self.search_services, name_or_id, filters) + + def delete_service(self, name_or_id): + """Delete a Keystone service. + + :param name_or_id: Service name or id. + + :returns: True if delete succeeded, False otherwise. + + :raises: ``OpenStackCloudException`` if something goes wrong during + the openstack API call + """ + service = self.get_service(name_or_id=name_or_id) + if service is None: + self.log.debug("Service %s not found for deleting" % name_or_id) + return False + + if self.cloud_config.get_api_version('identity').startswith('2'): + service_kwargs = {'id': service['id']} + else: + service_kwargs = {'service': service['id']} + try: + self.manager.submitTask(_tasks.ServiceDelete(**service_kwargs)) + except Exception as e: + raise OpenStackCloudException( + "Failed to delete service {id}: {msg}".format( + id=service['id'], + msg=str(e))) + return True + + @_utils.valid_kwargs('public_url', 'internal_url', 'admin_url') + def create_endpoint(self, service_name_or_id, url=None, interface=None, + region=None, enabled=True, **kwargs): + """Create a Keystone endpoint. + + :param service_name_or_id: Service name or id for this endpoint. + :param url: URL of the endpoint + :param interface: Interface type of the endpoint + :param public_url: Endpoint public URL. + :param internal_url: Endpoint internal URL. + :param admin_url: Endpoint admin URL. + :param region: Endpoint region. + :param enabled: Whether the endpoint is enabled + + NOTE: Both v2 (public_url, internal_url, admin_url) and v3 + (url, interface) calling semantics are supported. But + you can only use one of them at a time. + + :returns: a list of dicts containing the endpoint description. + + :raises: OpenStackCloudException if the service cannot be found or if + something goes wrong during the openstack API call. + """ + if url and kwargs: + raise OpenStackCloudException( + "create_endpoint takes either url and interace OR" + " public_url, internal_url, admin_url") + + service = self.get_service(name_or_id=service_name_or_id) + if service is None: + raise OpenStackCloudException("service {service} not found".format( + service=service_name_or_id)) + + endpoints = [] + endpoint_args = [] + if url: + urlkwargs = {} + if self.cloud_config.get_api_version('identity').startswith('2'): + if interface != 'public': + raise OpenStackCloudException( + "Error adding endpoint for service {service}." + " On a v2 cloud the url/interface API may only be" + " used for public url. Try using the public_url," + " internal_url, admin_url parameters instead of" + " url and interface".format( + service=service_name_or_id)) + urlkwargs['%url' % interface] = url + urlkwargs['service_id'] = service['id'] + else: + urlkwargs['url'] = url + urlkwargs['interface'] = interface + urlkwargs['enabled'] = enabled + urlkwargs['service'] = service['id'] + endpoint_args.append(urlkwargs) + else: + if self.cloud_config.get_api_version( + 'identity').startswith('2'): + urlkwargs = {} + for arg_key, arg_val in kwargs.items(): + urlkwargs[arg_key.replace('_', '')] = arg_val + urlkwargs['service_id'] = service['id'] + endpoint_args.append(urlkwargs) + else: + for arg_key, arg_val in kwargs.items(): + urlkwargs = {} + urlkwargs['url'] = arg_val + urlkwargs['interface'] = arg_key.split('_')[0] + urlkwargs['enabled'] = enabled + urlkwargs['service'] = service['id'] + endpoint_args.append(urlkwargs) + + for args in endpoint_args: + try: + endpoint = self.manager.submitTask(_tasks.EndpointCreate( + region=region, + **args + )) + except Exception as e: + raise OpenStackCloudException( + "Failed to create endpoint for service {service}: " + "{msg}".format(service=service['name'], + msg=str(e))) + endpoints.append(endpoint) + return meta.obj_list_to_dict(endpoints) + + def list_endpoints(self): + """List Keystone endpoints. + + :returns: a list of dict containing the endpoint description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + # ToDo: support v3 api (dguerri) + try: + endpoints = self.manager.submitTask(_tasks.EndpointList()) + except Exception as e: + raise OpenStackCloudException("Failed to list endpoints: {msg}" + .format(msg=str(e))) + return meta.obj_list_to_dict(endpoints) + + def search_endpoints(self, id=None, filters=None): + """List Keystone endpoints. + + :param id: endpoint id. + :param filters: a dict containing additional filters to use. e.g. + {'region': 'region-a.geo-1'} + + :returns: a list of dict containing the endpoint description. Each dict + contains the following attributes:: + - id: + - region: + - public_url: + - internal_url: (optional) + - admin_url: (optional) + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + endpoints = self.list_endpoints() + return _utils._filter_list(endpoints, id, filters) + + def get_endpoint(self, id, filters=None): + """Get exactly one Keystone endpoint. + + :param id: endpoint id. + :param filters: a dict containing additional filters to use. e.g. + {'region': 'region-a.geo-1'} + + :returns: a dict containing the endpoint description. i.e. a dict + containing the following attributes:: + - id: + - region: + - public_url: + - internal_url: (optional) + - admin_url: (optional) + """ + return _utils._get_entity(self.search_endpoints, id, filters) + + def delete_endpoint(self, id): + """Delete a Keystone endpoint. + + :param id: Id of the endpoint to delete. + + :returns: True if delete succeeded, False otherwise. + + :raises: ``OpenStackCloudException`` if something goes wrong during + the openstack API call. + """ + # ToDo: support v3 api (dguerri) + endpoint = self.get_endpoint(id=id) + if endpoint is None: + self.log.debug("Endpoint %s not found for deleting" % id) + return False + + if self.cloud_config.get_api_version('identity').startswith('2'): + endpoint_kwargs = {'id': endpoint['id']} + else: + endpoint_kwargs = {'endpoint': endpoint['id']} + try: + self.manager.submitTask(_tasks.EndpointDelete(**endpoint_kwargs)) + except Exception as e: + raise OpenStackCloudException( + "Failed to delete endpoint {id}: {msg}".format( + id=id, + msg=str(e))) + return True + + def create_domain( + self, name, description=None, enabled=True): + """Create a Keystone domain. + + :param name: The name of the domain. + :param description: A description of the domain. + :param enabled: Is the domain enabled or not (default True). + + :returns: a dict containing the domain description + + :raise OpenStackCloudException: if the domain cannot be created + """ + try: + domain = self.manager.submitTask(_tasks.DomainCreate( + name=name, + description=description, + enabled=enabled)) + except Exception as e: + raise OpenStackCloudException( + "Failed to create domain {name}".format(name=name, + msg=str(e))) + return meta.obj_to_dict(domain) + + def update_domain( + self, domain_id, name=None, description=None, enabled=None): + try: + return meta.obj_to_dict( + self.manager.submitTask(_tasks.DomainUpdate( + domain=domain_id, description=description, + enabled=enabled))) + except Exception as e: + raise OpenStackCloudException( + "Error in updating domain {domain}: {message}".format( + domain=domain_id, message=str(e))) + + def delete_domain(self, domain_id): + """Delete a Keystone domain. + + :param domain_id: ID of the domain to delete. + + :returns: None + + :raises: ``OpenStackCloudException`` if something goes wrong during + the openstack API call. + """ + try: + # Deleting a domain is expensive, so disabling it first increases + # the changes of success + domain = self.update_domain(domain_id, enabled=False) + self.manager.submitTask(_tasks.DomainDelete( + domain=domain['id'])) + except Exception as e: + raise OpenStackCloudException( + "Failed to delete domain {id}: {msg}".format(id=domain_id, + msg=str(e))) + + def list_domains(self): + """List Keystone domains. + + :returns: a list of dicts containing the domain description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + try: + domains = self.manager.submitTask(_tasks.DomainList()) + except Exception as e: + raise OpenStackCloudException("Failed to list domains: {msg}" + .format(msg=str(e))) + return meta.obj_list_to_dict(domains) + + def search_domains(self, **filters): + """Seach Keystone domains. + + :param filters: a dict containing additional filters to use. + keys to search on are id, name, enabled and description. + + :returns: a list of dicts containing the domain description. Each dict + contains the following attributes:: + - id: + - name: + - description: + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + try: + domains = self.manager.submitTask( + _tasks.DomainList(**filters)) + except Exception as e: + raise OpenStackCloudException("Failed to list domains: {msg}" + .format(msg=str(e))) + return meta.obj_list_to_dict(domains) + + def get_domain(self, domain_id): + """Get exactly one Keystone domain. + + :param domain_id: domain id. + + :returns: a dict containing the domain description, or None if not + found. Each dict contains the following attributes:: + - id: + - name: + - description: + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + try: + domain = self.manager.submitTask( + _tasks.DomainGet(domain=domain_id)) + except Exception as e: + raise OpenStackCloudException( + "Failed to get domain {domain_id}: {msg}".format( + domain_id=domain_id, + msg=str(e))) + raise OpenStackCloudException(str(e)) + return meta.obj_to_dict(domain) + + def list_roles(self): + """List Keystone roles. + + :returns: a list of dicts containing the role description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + try: + roles = self.manager.submitTask(_tasks.RoleList()) + except Exception as e: + raise OpenStackCloudException(str(e)) + return meta.obj_list_to_dict(roles) + + def search_roles(self, name_or_id=None, filters=None): + """Seach Keystone roles. + + :param string name: role name or id. + :param dict filters: a dict containing additional filters to use. + + :returns: a list of dict containing the role description. Each dict + contains the following attributes:: + + - id: + - name: + - description: + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + roles = self.list_roles() + return _utils._filter_list(roles, name_or_id, filters) + + def get_role(self, name_or_id, filters=None): + """Get exactly one Keystone role. + + :param id: role name or id. + :param filters: a dict containing additional filters to use. + + :returns: a single dict containing the role description. Each dict + contains the following attributes:: + + - id: + - name: + - description: + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + return _utils._get_entity(self.search_roles, name_or_id, filters) + + def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", + ephemeral=0, swap=0, rxtx_factor=1.0, is_public=True): + """Create a new flavor. + + :param name: Descriptive name of the flavor + :param ram: Memory in MB for the flavor + :param vcpus: Number of VCPUs for the flavor + :param disk: Size of local disk in GB + :param flavorid: ID for the flavor (optional) + :param ephemeral: Ephemeral space size in GB + :param swap: Swap space in MB + :param rxtx_factor: RX/TX factor + :param is_public: Make flavor accessible to the public + + :returns: A dict describing the new flavor. + + :raises: OpenStackCloudException on operation error. + """ + try: + flavor = self.manager.submitTask( + _tasks.FlavorCreate(name=name, ram=ram, vcpus=vcpus, disk=disk, + flavorid=flavorid, ephemeral=ephemeral, + swap=swap, rxtx_factor=rxtx_factor, + is_public=is_public) + ) + except Exception as e: + raise OpenStackCloudException( + "Failed to create flavor {name}: {msg}".format( + name=name, + msg=str(e))) + return meta.obj_to_dict(flavor) + + def delete_flavor(self, name_or_id): + """Delete a flavor + + :param name_or_id: ID or name of the flavor to delete. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException on operation error. + """ + flavor = self.get_flavor(name_or_id) + if flavor is None: + self.log.debug( + "Flavor {0} not found for deleting".format(name_or_id)) + return False + + try: + self.manager.submitTask(_tasks.FlavorDelete(flavor=flavor['id'])) + except Exception as e: + raise OpenStackCloudException( + "Unable to delete flavor {0}: {1}".format(name_or_id, e) + ) + + return True + + def _mod_flavor_specs(self, action, flavor_id, specs): + """Common method for modifying flavor extra specs. + + Nova (very sadly) doesn't expose this with a public API, so we + must get the actual flavor object and make a method call on it. + + Two separate try-except blocks are used because Nova can raise + a NotFound exception if FlavorGet() is given an invalid flavor ID, + or if the unset_keys() method of the flavor object is given an + invalid spec key. We need to be able to differentiate between these + actions, thus the separate blocks. + """ + try: + flavor = self.manager.submitTask( + _tasks.FlavorGet(flavor=flavor_id) + ) + except nova_exceptions.NotFound: + self.log.debug( + "Flavor ID {0} not found. " + "Cannot {1} extra specs.".format(flavor_id, action) + ) + raise OpenStackCloudResourceNotFound( + "Flavor ID {0} not found".format(flavor_id) + ) + except Exception as e: + raise OpenStackCloudException( + "Error getting flavor ID {0}: {1}".format(flavor_id, e) + ) + + try: + if action == 'set': + flavor.set_keys(specs) + elif action == 'unset': + flavor.unset_keys(specs) + except Exception as e: + raise OpenStackCloudException( + "Unable to {0} flavor specs: {1}".format(action, e) + ) + + def set_flavor_specs(self, flavor_id, extra_specs): + """Add extra specs to a flavor + + :param string flavor_id: ID of the flavor to update. + :param dict extra_specs: Dictionary of key-value pairs. + + :raises: OpenStackCloudException on operation error. + :raises: OpenStackCloudResourceNotFound if flavor ID is not found. + """ + self._mod_flavor_specs('set', flavor_id, extra_specs) + + def unset_flavor_specs(self, flavor_id, keys): + """Delete extra specs from a flavor + + :param string flavor_id: ID of the flavor to update. + :param list keys: List of spec keys to delete. + + :raises: OpenStackCloudException on operation error. + :raises: OpenStackCloudResourceNotFound if flavor ID is not found. + """ + self._mod_flavor_specs('unset', flavor_id, keys) + + def _mod_flavor_access(self, action, flavor_id, project_id): + """Common method for adding and removing flavor access + """ + try: + if action == 'add': + self.manager.submitTask( + _tasks.FlavorAddAccess(flavor=flavor_id, + tenant=project_id) + ) + elif action == 'remove': + self.manager.submitTask( + _tasks.FlavorRemoveAccess(flavor=flavor_id, + tenant=project_id) + ) + except Exception as e: + raise OpenStackCloudException( + "Error trying to {0} access from flavor ID {1}: {2}".format( + action, flavor_id, e) + ) + + def add_flavor_access(self, flavor_id, project_id): + """Grant access to a private flavor for a project/tenant. + + :param string flavor_id: ID of the private flavor. + :param string project_id: ID of the project/tenant. + + :raises: OpenStackCloudException on operation error. + """ + self._mod_flavor_access('add', flavor_id, project_id) + + def remove_flavor_access(self, flavor_id, project_id): + """Revoke access from a private flavor for a project/tenant. + + :param string flavor_id: ID of the private flavor. + :param string project_id: ID of the project/tenant. + + :raises: OpenStackCloudException on operation error. + """ + self._mod_flavor_access('remove', flavor_id, project_id) + + def create_role(self, name): + """Create a Keystone role. + + :param string name: The name of the role. + + :returns: a dict containing the role description + + :raise OpenStackCloudException: if the role cannot be created + """ + try: + role = self.manager.submitTask( + _tasks.RoleCreate(name=name) + ) + except Exception as e: + raise OpenStackCloudException(str(e)) + return meta.obj_to_dict(role) + + def delete_role(self, name_or_id): + """Delete a Keystone role. + + :param string id: Name or id of the role to delete. + + :returns: True if delete succeeded, False otherwise. + + :raises: ``OpenStackCloudException`` if something goes wrong during + the openstack API call. + """ + role = self.get_role(name_or_id) + if role is None: + self.log.debug( + "Role {0} not found for deleting".format(name_or_id)) + return False + + try: + self.manager.submitTask(_tasks.RoleDelete(role=role['id'])) + except Exception as e: + raise OpenStackCloudException( + "Unable to delete role {0}: {1}".format(name_or_id, e) + ) + + return True diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 90773d09e..a224ba523 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -19,6 +19,7 @@ import yaml import shade +import shade.openstackcloud from shade import exc from shade import meta from shade.tests import fakes @@ -386,8 +387,8 @@ class FakeImage(dict): fake_image.update({ 'id': '99', 'name': '99 name', - shade.IMAGE_MD5_KEY: fake_md5, - shade.IMAGE_SHA256_KEY: fake_sha256, + shade.openstackcloud.IMAGE_MD5_KEY: fake_md5, + shade.openstackcloud.IMAGE_SHA256_KEY: fake_sha256, }) glance_mock.images.list.return_value = [fake_image] diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index ee3f5479e..a31252897 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -20,6 +20,7 @@ from swiftclient import exceptions as swift_exc import shade +import shade.openstackcloud from shade import exc from shade import OpenStackCloud from shade.tests.unit import base @@ -93,5 +94,5 @@ def test_get_object_segment_size(self, swift_mock): def test_get_object_segment_size_http_412(self, swift_mock): swift_mock.get_capabilities.side_effect = swift_exc.ClientException( "Precondition failed", http_status=412) - self.assertEqual(shade.DEFAULT_OBJECT_SEGMENT_SIZE, + self.assertEqual(shade.openstackcloud.DEFAULT_OBJECT_SEGMENT_SIZE, self.cloud.get_object_segment_size(None)) From e2f0253547214c7f351735cce979edc475f59748 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Sun, 1 Nov 2015 13:31:09 -0500 Subject: [PATCH 0592/3836] Timeout too aggressive for inspection tests The new ironic inspection tests has a timeout that appears may be too aggressively low, which may be causing the iterator to give up before yielding even once, on a random basis. Change-Id: I18d447e092c74e21edcc7f90f36112d8c87ffee7 --- shade/tests/unit/test_shade_operator.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index 1aeef011d..78a4b8acf 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -378,7 +378,7 @@ class active_machine: self.cloud.inspect_machine, machine_uuid, wait=True, - timeout=0.001) + timeout=1) @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_inspect_machine_failed(self, mock_client): @@ -469,7 +469,7 @@ class inspecting_machine: ) return_value = self.cloud.inspect_machine( - machine_uuid, wait=True, timeout=0.001) + machine_uuid, wait=True, timeout=1) self.assertTrue(mock_client.node.set_provision_state.called) self.assertEqual( mock_client.node.set_provision_state.call_count, 3) @@ -500,7 +500,7 @@ class inspecting_machine: manageable_machine]) return_value = self.cloud.inspect_machine( - machine_uuid, wait=True, timeout=0.001) + machine_uuid, wait=True, timeout=1) self.assertDictEqual(expected_return_value, return_value) @mock.patch.object(shade.OperatorCloud, 'ironic_client') @@ -532,7 +532,7 @@ class inspect_failed_machine: self.cloud.inspect_machine, machine_uuid, wait=True, - timeout=0.001) + timeout=1) self.assertEqual( mock_client.node.set_provision_state.call_count, 1) self.assertEqual(mock_client.node.get.call_count, 3) From f31b20623efa653f579f137b4d4abcc0b8818e50 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Sun, 1 Nov 2015 13:33:06 -0500 Subject: [PATCH 0593/3836] Fix misspelling of ironic state name. Change-Id: I27f7dd4b3b07e18b94baf15079d4bf78ea2b225a --- shade/operatorcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index f850509e3..ca2099bc2 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -169,7 +169,7 @@ def inspect_machine(self, name_or_id, wait=False, timeout=3600): for count in _utils._iterate_timeout( timeout, "Timeout waiting for node transition to " - "target state of 'inpection'"): + "target state of 'inspect'"): machine = self.get_machine(name_or_id) if "inspect failed" in machine['provision_state']: From 62578215183982e493dbe490faf80a50bfb77281 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Sun, 1 Nov 2015 14:52:33 -0500 Subject: [PATCH 0594/3836] Move _neutron_exceptions context manager to _utils Continue with our work of moving utility methods out of OpenStackCloud and into the _utils module. Change-Id: I898249a7dd1a93f3a9d798008a840b44bea35927 --- shade/_utils.py | 26 ++++++++++++-- shade/openstackcloud.py | 78 +++++++++++++++-------------------------- 2 files changed, 53 insertions(+), 51 deletions(-) diff --git a/shade/_utils.py b/shade/_utils.py index 0df687fc0..6d1806be4 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -12,15 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -from decorator import decorator +import contextlib import inspect +import netifaces import time -import netifaces +from decorator import decorator +from neutronclient.common import exceptions as neutron_exc from shade import _log from shade import exc + log = _log.setup_logging(__name__) @@ -342,3 +345,22 @@ def invalidate(obj, *args, **kwargs): return _cache_decorator return _inner_cache_on_arguments + + +@contextlib.contextmanager +def neutron_exceptions(error_message): + try: + yield + except neutron_exc.NotFound as e: + raise exc.OpenStackCloudResourceNotFound( + "{msg}: {exc}".format(msg=error_message, exc=str(e))) + except neutron_exc.NeutronClientException as e: + if e.status_code == 404: + raise exc.OpenStackCloudURINotFound( + "{msg}: {exc}".format(msg=error_message, exc=str(e))) + else: + raise exc.OpenStackCloudException( + "{msg}: {exc}".format(msg=error_message, exc=str(e))) + except Exception as e: + raise exc.OpenStackCloudException( + "{msg}: {exc}".format(msg=error_message, exc=str(e))) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 16e2baf99..95531fe1d 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -10,7 +10,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import contextlib import functools import hashlib import ipaddress @@ -34,7 +33,6 @@ from keystoneauth1 import session as ksa_session from keystoneclient.v2_0 import client as k2_client from keystoneclient.v3 import client as k3_client -from neutronclient.common import exceptions as neutron_exceptions from neutronclient.v2_0 import client as neutron_client from novaclient import client as nova_client from novaclient import exceptions as nova_exceptions @@ -228,24 +226,6 @@ def invalidate(self): self.cloud_config = cloud_config - @contextlib.contextmanager - def _neutron_exceptions(self, error_message): - try: - yield - except neutron_exceptions.NotFound as e: - raise OpenStackCloudResourceNotFound( - "{msg}: {exc}".format(msg=error_message, exc=str(e))) - except neutron_exceptions.NeutronClientException as e: - if e.status_code == 404: - raise OpenStackCloudURINotFound( - "{msg}: {exc}".format(msg=error_message, exc=str(e))) - else: - raise OpenStackCloudException( - "{msg}: {exc}".format(msg=error_message, exc=str(e))) - except Exception as e: - raise OpenStackCloudException( - "{msg}: {exc}".format(msg=error_message, exc=str(e))) - def _make_cache_key(self, namespace, fn): fname = fn.__name__ if namespace is None: @@ -1040,7 +1020,7 @@ def list_networks(self, filters=None): # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} - with self._neutron_exceptions("Error fetching network list"): + with _utils.neutron_exceptions("Error fetching network list"): return self.manager.submitTask( _tasks.NetworkList(**filters))['networks'] @@ -1054,7 +1034,7 @@ def list_routers(self, filters=None): # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} - with self._neutron_exceptions("Error fetching router list"): + with _utils.neutron_exceptions("Error fetching router list"): return self.manager.submitTask( _tasks.RouterList(**filters))['routers'] @@ -1068,7 +1048,7 @@ def list_subnets(self, filters=None): # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} - with self._neutron_exceptions("Error fetching subnet list"): + with _utils.neutron_exceptions("Error fetching subnet list"): return self.manager.submitTask( _tasks.SubnetList(**filters))['subnets'] @@ -1082,7 +1062,7 @@ def list_ports(self, filters=None): # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} - with self._neutron_exceptions("Error fetching port list"): + with _utils.neutron_exceptions("Error fetching port list"): return self.manager.submitTask(_tasks.PortList(**filters))['ports'] @_utils.cache_on_arguments(should_cache_fn=_no_pending_volumes) @@ -1154,7 +1134,7 @@ def list_security_groups(self): # Handle neutron security groups if self.secgroup_source == 'neutron': # Neutron returns dicts, so no need to convert objects here. - with self._neutron_exceptions( + with _utils.neutron_exceptions( "Error fetching security group list"): return self.manager.submitTask( _tasks.NeutronSecurityGroupList())['security_groups'] @@ -1306,7 +1286,7 @@ def list_floating_ips(self): return _utils.normalize_nova_floating_ips(floating_ips) def _neutron_list_floating_ips(self): - with self._neutron_exceptions("error fetching floating IPs list"): + with _utils.neutron_exceptions("error fetching floating IPs list"): return self.manager.submitTask( _tasks.NeutronFloatingIPList())['floatingips'] @@ -1748,7 +1728,7 @@ def create_network(self, name, shared=False, admin_state_up=True, 'router:external': external } - with self._neutron_exceptions( + with _utils.neutron_exceptions( "Error creating network {0}".format(name)): net = self.manager.submitTask( _tasks.NetworkCreate(body=dict({'network': network}))) @@ -1771,7 +1751,7 @@ def delete_network(self, name_or_id): self.log.debug("Network %s not found for deleting" % name_or_id) return False - with self._neutron_exceptions( + with _utils.neutron_exceptions( "Error deleting network {0}".format(name_or_id)): self.manager.submitTask( _tasks.NetworkDelete(network=network['id'])) @@ -1812,7 +1792,7 @@ def add_router_interface(self, router, subnet_id=None, port_id=None): if port_id: body['port_id'] = port_id - with self._neutron_exceptions( + with _utils.neutron_exceptions( "Error attaching interface to router {0}".format(router['id']) ): return self.manager.submitTask( @@ -1840,7 +1820,7 @@ def remove_router_interface(self, router, subnet_id=None, port_id=None): if port_id: body['port_id'] = port_id - with self._neutron_exceptions( + with _utils.neutron_exceptions( "Error detaching interface from router {0}".format(router['id']) ): return self.manager.submitTask( @@ -1917,7 +1897,7 @@ def create_router(self, name=None, admin_state_up=True, if ext_gw_info: router['external_gateway_info'] = ext_gw_info - with self._neutron_exceptions( + with _utils.neutron_exceptions( "Error creating router {0}".format(name)): new_router = self.manager.submitTask( _tasks.RouterCreate(body=dict(router=router))) @@ -1971,7 +1951,7 @@ def update_router(self, name_or_id, name=None, admin_state_up=None, raise OpenStackCloudException( "Router %s not found." % name_or_id) - with self._neutron_exceptions( + with _utils.neutron_exceptions( "Error updating router {0}".format(name_or_id)): new_router = self.manager.submitTask( _tasks.RouterUpdate( @@ -2000,7 +1980,7 @@ def delete_router(self, name_or_id): self.log.debug("Router %s not found for deleting" % name_or_id) return False - with self._neutron_exceptions( + with _utils.neutron_exceptions( "Error deleting router {0}".format(name_or_id)): self.manager.submitTask( _tasks.RouterDelete(router=router['id'])) @@ -2716,7 +2696,7 @@ def _neutron_available_floating_ips( # tenant. This is the default behaviour of Nova project_id = self.keystone_session.get_project_id() - with self._neutron_exceptions("unable to get available floating IPs"): + with _utils.neutron_exceptions("unable to get available floating IPs"): networks = self.get_external_networks() if not networks: raise OpenStackCloudResourceNotFound( @@ -2816,7 +2796,7 @@ def create_floating_ip(self, network=None, server=None): def _neutron_create_floating_ip( self, network_name_or_id=None, server=None): - with self._neutron_exceptions( + with _utils.neutron_exceptions( "unable to create floating IP for net " "{0}".format(network_name_or_id)): if network_name_or_id: @@ -2884,7 +2864,7 @@ def delete_floating_ip(self, floating_ip_id): def _neutron_delete_floating_ip(self, floating_ip_id): try: - with self._neutron_exceptions("unable to delete floating IP"): + with _utils.neutron_exceptions("unable to delete floating IP"): self.manager.submitTask( _tasks.NeutronFloatingIPDelete(floatingip=floating_ip_id)) except OpenStackCloudResourceNotFound: @@ -3008,7 +2988,7 @@ def _get_free_fixed_port(self, server, fixed_address=None): def _neutron_attach_ip_to_server( self, server, floating_ip, fixed_address=None): - with self._neutron_exceptions( + with _utils.neutron_exceptions( "unable to bind a floating ip to server " "{0}".format(server['id'])): @@ -3067,7 +3047,7 @@ def detach_ip_from_server(self, server_id, floating_ip_id): server_id=server_id, floating_ip_id=floating_ip_id) def _neutron_detach_ip_from_server(self, server_id, floating_ip_id): - with self._neutron_exceptions( + with _utils.neutron_exceptions( "unable to detach a floating ip from server " "{0}".format(server_id)): f_ip = self.get_floating_ip(id=floating_ip_id) @@ -3699,7 +3679,7 @@ def create_subnet(self, network_name_or_id, cidr, ip_version=4, if ipv6_address_mode: subnet['ipv6_address_mode'] = ipv6_address_mode - with self._neutron_exceptions( + with _utils.neutron_exceptions( "Error creating subnet on network " "{0}".format(network_name_or_id)): new_subnet = self.manager.submitTask( @@ -3725,7 +3705,7 @@ def delete_subnet(self, name_or_id): self.log.debug("Subnet %s not found for deleting" % name_or_id) return False - with self._neutron_exceptions( + with _utils.neutron_exceptions( "Error deleting subnet {0}".format(name_or_id)): self.manager.submitTask( _tasks.SubnetDelete(subnet=subnet['id'])) @@ -3802,7 +3782,7 @@ def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, raise OpenStackCloudException( "Subnet %s not found." % name_or_id) - with self._neutron_exceptions( + with _utils.neutron_exceptions( "Error updating subnet {0}".format(name_or_id)): new_subnet = self.manager.submitTask( _tasks.SubnetUpdate( @@ -3871,7 +3851,7 @@ def create_port(self, network_id, **kwargs): """ kwargs['network_id'] = network_id - with self._neutron_exceptions( + with _utils.neutron_exceptions( "Error creating port for network {0}".format(network_id)): return self.manager.submitTask( _tasks.PortCreate(body={'port': kwargs}))['port'] @@ -3933,7 +3913,7 @@ def update_port(self, name_or_id, **kwargs): raise OpenStackCloudException( "failed to find port '{port}'".format(port=name_or_id)) - with self._neutron_exceptions( + with _utils.neutron_exceptions( "Error updating port {0}".format(name_or_id)): return self.manager.submitTask( _tasks.PortUpdate( @@ -3953,7 +3933,7 @@ def delete_port(self, name_or_id): self.log.debug("Port %s not found for deleting" % name_or_id) return False - with self._neutron_exceptions( + with _utils.neutron_exceptions( "Error deleting port {0}".format(name_or_id)): self.manager.submitTask(_tasks.PortDelete(port=port['id'])) return True @@ -3971,7 +3951,7 @@ def create_security_group(self, name, description): not supported on this cloud. """ if self.secgroup_source == 'neutron': - with self._neutron_exceptions( + with _utils.neutron_exceptions( "Error creating security group {0}".format(name)): group = self.manager.submitTask( _tasks.NeutronSecurityGroupCreate( @@ -4020,7 +4000,7 @@ def delete_security_group(self, name_or_id): return False if self.secgroup_source == 'neutron': - with self._neutron_exceptions( + with _utils.neutron_exceptions( "Error deleting security group {0}".format(name_or_id)): self.manager.submitTask( _tasks.NeutronSecurityGroupDelete( @@ -4065,7 +4045,7 @@ def update_security_group(self, name_or_id, **kwargs): "Security group %s not found." % name_or_id) if self.secgroup_source == 'neutron': - with self._neutron_exceptions( + with _utils.neutron_exceptions( "Error updating security group {0}".format(name_or_id)): group = self.manager.submitTask( _tasks.NeutronSecurityGroupUpdate( @@ -4167,7 +4147,7 @@ def create_security_group_rule(self, 'ethertype': ethertype } - with self._neutron_exceptions( + with _utils.neutron_exceptions( "Error creating security group rule"): rule = self.manager.submitTask( _tasks.NeutronSecurityGroupRuleCreate( @@ -4243,7 +4223,7 @@ def delete_security_group_rule(self, rule_id): if self.secgroup_source == 'neutron': try: - with self._neutron_exceptions( + with _utils.neutron_exceptions( "Error deleting security group rule " "{0}".format(rule_id)): self.manager.submitTask( From 91e01482c6c2eb772cd8c4c7bb9ec470dad3ca29 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Sun, 1 Nov 2015 09:26:25 -0500 Subject: [PATCH 0595/3836] Move ironic client attribute to correct class The _ironic_client attribute is useful only for the OperatorCloud class, so having it defined in OpenStackCloud is bad programming style. Move it to where it is used. Change-Id: I8141e1758e28a8120f0e78c9e407bb416a499166 --- shade/openstackcloud.py | 1 - shade/operatorcloud.py | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 16e2baf99..420817431 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -216,7 +216,6 @@ def invalidate(self): self._glance_client = None self._glance_endpoint = None self._heat_client = None - self._ironic_client = None self._keystone_client = None self._neutron_client = None self._nova_client = None diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index ca2099bc2..16c0baae8 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -32,6 +32,10 @@ class OperatorCloud(openstackcloud.OpenStackCloud): See the :class:`OpenStackCloud` class for a description of most options. """ + def __init__(self, *args, **kwargs): + super(OperatorCloud, self).__init__(*args, **kwargs) + self._ironic_client = None + # Set the ironic API microversion to a known-good # supported/tested with the contents of shade. # From edc5fc2d98b28e540de54cad313b4ce054188274 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 1 Nov 2015 11:38:48 +0900 Subject: [PATCH 0596/3836] Update README to not reference client passthrough It's there and we support it, but we don't have to _talk_ about it. Change-Id: Ia3419a72fa13d42a17ddc0243d7800e462a827b2 --- README.rst | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/README.rst b/README.rst index 9b1d254ca..33eda5c98 100644 --- a/README.rst +++ b/README.rst @@ -24,29 +24,22 @@ Sometimes an example is nice. :: import shade - import time + + # Initialize and turn on debug loggin + shade.simple_logging(debug=True) # Initialize cloud # Cloud configs are read with os-client-config cloud = shade.openstack_cloud(cloud='mordred') - # OpenStackCloud object has an interface exposing OpenStack services methods - print cloud.list_servers() - s = cloud.list_servers()[0] - - # But you can also access the underlying python-*client objects - # This will go away at some point in time and should be considered only - # usable for temporary poking - cinder = cloud.cinder_client - volumes = cinder.volumes.list() - volume_id = [v for v in volumes if v['status'] == 'available'][0]['id'] - nova = cloud.nova_client - print nova.volumes.create_server_volume(s['id'], volume_id, None) - attachments = [] - print volume_id - while not attachments: - print "Waiting for attach to finish" - time.sleep(1) - attachments = cinder.volumes.get(volume_id).attachments - print attachments + # Upload an image to the cloud + image = cloud.create_image( + 'ubuntu-trusty', filename='ubuntu-trusty.qcow2', wait=True) + + # Find a flavor with at least 512M of RAM + flavor = cloud.get_flavor_by_ram(512) + # Boot a server, wait for it to boot, and then do whatever is needed + # to get a public ip for it. + cloud.create_server( + 'my-server', image=image, flavor=flavor, wait=True, auto_ip=True) From 684b8b69290d5df1f494a1d2979f06566800ce5f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 1 Nov 2015 11:01:52 +0900 Subject: [PATCH 0597/3836] Add docs for create_server create_server does a lot of passthrough to nova create_server, which is great, but means it's tough to know what you need to pass in to shade when reading the docs. List and document the parameters that shade is supporting. Continue to support **kwargs pass through for others that could be used but which shade doesn't know about. Change-Id: I97cbb50f027e58766015b768a5da9c853b8c29fe --- shade/openstackcloud.py | 81 +++++++++++++++++++++++--- shade/tests/unit/test_create_server.py | 23 +++++--- 2 files changed, 89 insertions(+), 15 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 420817431..acf65fdbe 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3207,12 +3207,76 @@ def add_ips_to_server( "Error in getting info from instance: %s " % str(e)) return server - def create_server(self, auto_ip=True, ips=None, ip_pool=None, - root_volume=None, terminate_volume=False, - wait=False, timeout=180, reuse_ips=True, - **bootkwargs): + @_utils.valid_kwargs( + 'meta', 'files', 'userdata', + 'reservation_id', 'return_raw', 'min_count', + 'max_count', 'security_groups', 'key_name', + 'availability_zone', 'block_device_mapping', + 'block_device_mapping_v2', 'nics', 'scheduler_hints', + 'config_drive', 'admin_pass', 'disk_config') + def create_server( + self, name, image, flavor, + auto_ip=True, ips=None, ip_pool=None, + root_volume=None, terminate_volume=False, + wait=False, timeout=180, reuse_ips=True, + **kwargs): """Create a virtual server instance. + :param name: Something to name the server. + :param image: Image dict or id to boot with. + :param flavor: Flavor dict or id to boot onto. + :param auto_ip: Whether to take actions to find a routable IP for + the server. (defaults to True) + :param ips: List of IPs to attach to the server (defaults to None) + :param ip_pool: Name of the network or floating IP pool to get an + address from. (defaults to None) + :param root_volume: Name or id of a volume to boot from + (defaults to None) + :param terminate_volume: If booting from a volume, whether it should + be deleted when the server is destroyed. + (defaults to False) + :param meta: (optional) A dict of arbitrary key/value metadata to + store for this server. Both keys and values must be + <=255 characters. + :param files: (optional, deprecated) A dict of files to overwrite + on the server upon boot. Keys are file names (i.e. + ``/etc/passwd``) and values + are the file contents (either as a string or as a + file-like object). A maximum of five entries is allowed, + and each file must be 10k or less. + :param reservation_id: a UUID for the set of servers being requested. + :param min_count: (optional extension) The minimum number of + servers to launch. + :param max_count: (optional extension) The maximum number of + servers to launch. + :param security_groups: A list of security group names + :param userdata: user data to pass to be exposed by the metadata + server this can be a file type object as well or a + string. + :param key_name: (optional extension) name of previously created + keypair to inject into the instance. + :param availability_zone: Name of the availability zone for instance + placement. + :param block_device_mapping: (optional) A dict of block + device mappings for this server. + :param block_device_mapping_v2: (optional) A dict of block + device mappings for this server. + :param nics: (optional extension) an ordered list of nics to be + added to this server, with information about + connected networks, fixed IPs, port etc. + :param scheduler_hints: (optional extension) arbitrary key-value pairs + specified by the client to help boot an instance + :param config_drive: (optional extension) value for config drive + either boolean, or volume-id + :param disk_config: (optional extension) control how the disk is + partitioned when the server is created. possible + values are 'AUTO' or 'MANUAL'. + :param admin_pass: (optional extension) add a user supplied admin + password. + :param wait: (optional) Wait for the address to appear as assigned + to the server in Nova. Defaults to False. + :param timeout: (optional) Seconds to wait, defaults to 60. + See the ``wait`` parameter. :returns: A dict representing the created server. :raises: OpenStackCloudException on operation error. """ @@ -3222,12 +3286,13 @@ def create_server(self, auto_ip=True, ips=None, ip_pool=None, else: suffix = ':::0' volume_id = self.get_volume_id(root_volume) + suffix - if 'block_device_mapping' not in bootkwargs: - bootkwargs['block_device_mapping'] = dict() - bootkwargs['block_device_mapping']['vda'] = volume_id + if 'block_device_mapping' not in kwargs: + kwargs['block_device_mapping'] = dict() + kwargs['block_device_mapping']['vda'] = volume_id try: - server = self.manager.submitTask(_tasks.ServerCreate(**bootkwargs)) + server = self.manager.submitTask(_tasks.ServerCreate( + name=name, image=image, flavor=flavor, **kwargs)) # This is a direct get task call to skip the list_servers # cache which has absolutely no chance of containing the # new server diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index 773831e71..2ed172a65 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -46,7 +46,8 @@ def test_create_server_with_create_exception(self): } OpenStackCloud.nova_client = Mock(**config) self.assertRaises( - OpenStackCloudException, self.client.create_server) + OpenStackCloudException, self.client.create_server, + 'server-name', 'image-id', 'flavor-id') def test_create_server_with_get_exception(self): """ @@ -60,7 +61,8 @@ def test_create_server_with_get_exception(self): } OpenStackCloud.nova_client = Mock(**config) self.assertRaises( - OpenStackCloudException, self.client.create_server) + OpenStackCloudException, self.client.create_server, + 'server-name', 'image-id', 'flavor-id') def test_create_server_with_server_error(self): """ @@ -76,7 +78,8 @@ def test_create_server_with_server_error(self): } OpenStackCloud.nova_client = Mock(**config) self.assertRaises( - OpenStackCloudException, self.client.create_server) + OpenStackCloudException, self.client.create_server, + 'server-name', 'image-id', 'flavor-id') def test_create_server_wait_server_error(self): """ @@ -95,7 +98,8 @@ def test_create_server_wait_server_error(self): OpenStackCloud.nova_client = Mock(**config) self.assertRaises( OpenStackCloudException, - self.client.create_server, wait=True) + self.client.create_server, + 'server-name', 'image-id', 'flavor-id', wait=True) def test_create_server_with_timeout(self): """ @@ -112,7 +116,8 @@ def test_create_server_with_timeout(self): OpenStackCloud.nova_client = Mock(**config) self.assertRaises( OpenStackCloudTimeout, - self.client.create_server, wait=True, timeout=1) + self.client.create_server, + 'server-name', 'image-id', 'flavor-id', wait=True, timeout=1) def test_create_server_no_wait(self): """ @@ -127,7 +132,9 @@ def test_create_server_no_wait(self): } OpenStackCloud.nova_client = Mock(**config) self.assertEqual(meta.obj_to_dict(fake_server), - self.client.create_server()) + self.client.create_server( + name='server-name', image='image=id', + flavor='flavor-id')) def test_create_server_wait(self): """ @@ -149,7 +156,8 @@ def test_create_server_wait(self): with patch.object(OpenStackCloud, "add_ips_to_server", return_value=fake_server): self.assertEqual( - self.client.create_server(wait=True), + self.client.create_server( + 'server-name', 'image-id', 'flavor-id', wait=True), fake_server) def test_create_server_no_addresses(self): @@ -171,4 +179,5 @@ def test_create_server_no_addresses(self): return_value=fake_server): self.assertRaises( OpenStackCloudException, self.client.create_server, + 'server-name', 'image-id', 'flavor-id', wait=True) From b6e918089cbd561b6f9961f832ea4f4d925bfcd0 Mon Sep 17 00:00:00 2001 From: matthew wagoner Date: Mon, 2 Nov 2015 10:36:31 -0500 Subject: [PATCH 0598/3836] Don't wrap wrapped exceptions in operatorcloud.py Similar to change Ia1e45f4971f4b51f28538260d64f778aecaa6f3d If we've wrapped the exception because of Auth things, we don't need to re-wrap it - it's just confusing. Change-Id: Iecbe4137acc2b27848b09e415df35d486f92c951 --- shade/operatorcloud.py | 68 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 16c0baae8..d94f2e3a2 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -61,6 +61,8 @@ def list_nics(self): return meta.obj_list_to_dict( self.manager.submitTask(_tasks.MachinePortList()) ) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Error fetching machine port list: %s" % e) @@ -71,6 +73,8 @@ def list_nics_for_machine(self, uuid): self.manager.submitTask( _tasks.MachineNodePortList(node_id=uuid)) ) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Error fetching port list for node %s: %s" % (uuid, e)) @@ -190,6 +194,8 @@ def inspect_machine(self, name_or_id, wait=False, timeout=3600): return(machine) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Error inspecting machine: %s" % e) @@ -244,6 +250,8 @@ def register_machine(self, nics, wait=False, timeout=3600, machine = meta.obj_to_dict( self.manager.submitTask(_tasks.MachineCreate(**kwargs))) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Error registering machine with Ironic: %s" % str(e)) @@ -335,6 +343,8 @@ def register_machine(self, nics, wait=False, timeout=3600, "Machine encountered a failure: %s" % machine['last_error']) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Error transitioning node to available state: %s" @@ -374,6 +384,8 @@ def unregister_machine(self, nics, uuid, wait=False, timeout=600): _tasks.MachinePortGetByAddress(address=nic['mac'])) self.manager.submitTask( _tasks.MachinePortDelete(port_id=port.uuid)) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Error removing NIC '%s' from baremetal API for " @@ -388,6 +400,8 @@ def unregister_machine(self, nics, uuid, wait=False, timeout=600): if not self.get_machine(uuid): break + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Error unregistering machine %s from the baremetal API. " @@ -436,6 +450,8 @@ def patch_machine(self, name_or_id, patch): _tasks.MachinePatch(node_id=name_or_id, patch=patch, http_method='PATCH'))) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Error updating machine via patch operation. node: %s. " @@ -546,6 +562,8 @@ def update_machine(self, name_or_id, chassis_uuid=None, driver=None, node=machine, changes=change_list ) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Machine update failed - patch operation failed Machine: %s " @@ -555,6 +573,8 @@ def validate_node(self, uuid): try: ifaces = self.manager.submitTask( _tasks.MachineNodeValidate(node_uuid=uuid)) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException(str(e)) @@ -618,6 +638,8 @@ def node_set_provision_state(self, machine = self.get_machine(name_or_id) return machine + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Baremetal machine node failed change provision" @@ -663,6 +685,8 @@ def set_machine_maintenance_state( "on node %s. Received: %s" % ( state, name_or_id, result)) return None + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Error setting machine maintenance state to %s " @@ -712,6 +736,8 @@ def _set_machine_power_state(self, name_or_id, state): "Failed setting machine power state %s on node %s. " "Received: %s" % (state, name_or_id, power)) return None + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Error setting machine power state %s on node %s. " @@ -776,6 +802,8 @@ def set_node_instance_info(self, uuid, patch): self.manager.submitTask( _tasks.MachineNodeUpdate(node_id=uuid, patch=patch)) ) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException(str(e)) @@ -787,6 +815,8 @@ def purge_node_instance_info(self, uuid): self.manager.submitTask( _tasks.MachineNodeUpdate(node_id=uuid, patch=patch)) ) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException(str(e)) @@ -821,6 +851,8 @@ def create_service(self, name, **kwargs): service = self.manager.submitTask(_tasks.ServiceCreate( name=name, description=description, **service_kwargs)) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Failed to create service {name}: {msg}".format( @@ -837,6 +869,8 @@ def list_services(self): """ try: services = self.manager.submitTask(_tasks.ServiceList()) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException(str(e)) return _utils.normalize_keystone_services( @@ -897,6 +931,8 @@ def delete_service(self, name_or_id): service_kwargs = {'service': service['id']} try: self.manager.submitTask(_tasks.ServiceDelete(**service_kwargs)) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Failed to delete service {id}: {msg}".format( @@ -981,6 +1017,8 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, region=region, **args )) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Failed to create endpoint for service {service}: " @@ -1000,6 +1038,8 @@ def list_endpoints(self): # ToDo: support v3 api (dguerri) try: endpoints = self.manager.submitTask(_tasks.EndpointList()) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException("Failed to list endpoints: {msg}" .format(msg=str(e))) @@ -1065,6 +1105,8 @@ def delete_endpoint(self, id): endpoint_kwargs = {'endpoint': endpoint['id']} try: self.manager.submitTask(_tasks.EndpointDelete(**endpoint_kwargs)) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Failed to delete endpoint {id}: {msg}".format( @@ -1089,6 +1131,8 @@ def create_domain( name=name, description=description, enabled=enabled)) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Failed to create domain {name}".format(name=name, @@ -1102,6 +1146,8 @@ def update_domain( self.manager.submitTask(_tasks.DomainUpdate( domain=domain_id, description=description, enabled=enabled))) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Error in updating domain {domain}: {message}".format( @@ -1123,6 +1169,8 @@ def delete_domain(self, domain_id): domain = self.update_domain(domain_id, enabled=False) self.manager.submitTask(_tasks.DomainDelete( domain=domain['id'])) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Failed to delete domain {id}: {msg}".format(id=domain_id, @@ -1138,6 +1186,8 @@ def list_domains(self): """ try: domains = self.manager.submitTask(_tasks.DomainList()) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException("Failed to list domains: {msg}" .format(msg=str(e))) @@ -1161,6 +1211,8 @@ def search_domains(self, **filters): try: domains = self.manager.submitTask( _tasks.DomainList(**filters)) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException("Failed to list domains: {msg}" .format(msg=str(e))) @@ -1183,6 +1235,8 @@ def get_domain(self, domain_id): try: domain = self.manager.submitTask( _tasks.DomainGet(domain=domain_id)) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Failed to get domain {domain_id}: {msg}".format( @@ -1201,6 +1255,8 @@ def list_roles(self): """ try: roles = self.manager.submitTask(_tasks.RoleList()) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException(str(e)) return meta.obj_list_to_dict(roles) @@ -1267,6 +1323,8 @@ def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", swap=swap, rxtx_factor=rxtx_factor, is_public=is_public) ) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Failed to create flavor {name}: {msg}".format( @@ -1291,6 +1349,8 @@ def delete_flavor(self, name_or_id): try: self.manager.submitTask(_tasks.FlavorDelete(flavor=flavor['id'])) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Unable to delete flavor {0}: {1}".format(name_or_id, e) @@ -1322,6 +1382,8 @@ def _mod_flavor_specs(self, action, flavor_id, specs): raise OpenStackCloudResourceNotFound( "Flavor ID {0} not found".format(flavor_id) ) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Error getting flavor ID {0}: {1}".format(flavor_id, e) @@ -1373,6 +1435,8 @@ def _mod_flavor_access(self, action, flavor_id, project_id): _tasks.FlavorRemoveAccess(flavor=flavor_id, tenant=project_id) ) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Error trying to {0} access from flavor ID {1}: {2}".format( @@ -1412,6 +1476,8 @@ def create_role(self, name): role = self.manager.submitTask( _tasks.RoleCreate(name=name) ) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException(str(e)) return meta.obj_to_dict(role) @@ -1434,6 +1500,8 @@ def delete_role(self, name_or_id): try: self.manager.submitTask(_tasks.RoleDelete(role=role['id'])) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Unable to delete role {0}: {1}".format(name_or_id, e) From eba0221a26f49f7b4c807f4732846ef2d42db169 Mon Sep 17 00:00:00 2001 From: Xav Paice Date: Tue, 3 Nov 2015 12:39:57 +1300 Subject: [PATCH 0599/3836] Fix typo in Catalyst region configs Small typo making one region not useful for customers (my bad!). Change-Id: If2680b521f92789f2621aa1249b304666fcde95b --- doc/source/vendor-support.rst | 2 +- os_client_config/vendors/catalyst.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index 3b4a37078..34cfbe937 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -48,7 +48,7 @@ https://api.cloud.catalyst.net.nz:5000/v2.0 Region Name Human Name ============== ================ nz-por-1 Porirua, NZ -nz_wlg_1 Wellington, NZ +nz_wlg_2 Wellington, NZ ============== ================ * Image API Version is 1 diff --git a/os_client_config/vendors/catalyst.yaml b/os_client_config/vendors/catalyst.yaml index 14cdf254f..d6251930d 100644 --- a/os_client_config/vendors/catalyst.yaml +++ b/os_client_config/vendors/catalyst.yaml @@ -4,6 +4,6 @@ profile: auth_url: https://api.cloud.catalyst.net.nz:5000/v2.0 regions: - nz-por-1 - - nz_wlg_1 + - nz_wlg_2 image_api_version: '1' image_format: raw From 0085effd4024fc8f48f7aa4e41853bcbb1675b4a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 1 Nov 2015 11:03:17 +0900 Subject: [PATCH 0600/3836] Skip an extra unneeded server get When we're attaching a floating ip, the wait loop gets the most up to date server dict as part of waiting. In that case, there is no need to do an additional get. If we're NOT waiting, it's also pointless to do a single get, as it's not guaranteed to have the updated info. Change-Id: I84bc54ce32a7d5bf10078027fdb29fab5e84bd39 --- shade/openstackcloud.py | 82 ++++++++++---------- shade/tests/unit/test_floating_ip_common.py | 8 +- shade/tests/unit/test_floating_ip_neutron.py | 10 +-- shade/tests/unit/test_floating_ip_nova.py | 4 +- 4 files changed, 52 insertions(+), 52 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 9c334cc02..3b9f2955c 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2901,7 +2901,7 @@ def _attach_ip_to_server( :param skip_attach: (optional) Skip the actual attach and just do the wait. Defaults to False. - :returns: None + :returns: The server dict :raises: OpenStackCloudException, on operation error. """ @@ -2909,7 +2909,7 @@ def _attach_ip_to_server( # attached ext_ip = meta.get_server_ip(server, ext_tag='floating') if ext_ip == floating_ip['floating_ip_address']: - return + return server if self.has_service('network'): if not skip_attach: @@ -2937,7 +2937,8 @@ def _attach_ip_to_server( server = self.get_server_by_id(server_id) ext_ip = meta.get_server_ip(server, ext_tag='floating') if ext_ip == floating_ip['floating_ip_address']: - return + return server + return server def _get_free_fixed_port(self, server, fixed_address=None): ports = self.search_ports(filters={'device_id': server['id']}) @@ -3079,7 +3080,8 @@ def _nova_detach_ip_from_server(self, server_id, floating_ip_id): return True def _add_ip_from_pool( - self, server, network, fixed_address=None, reuse=True): + self, server, network, fixed_address=None, reuse=True, + wait=False, timeout=60): """Add a floating IP to a sever from a given pool This method reuses available IPs, when possible, or allocate new IPs @@ -3091,38 +3093,44 @@ def _add_ip_from_pool( :param network: Nova pool name or Neutron network name or id. :param fixed_address: a fixed address :param reuse: Try to reuse existing ips. Defaults to True. + :param wait: (optional) Wait for the address to appear as assigned + to the server in Nova. Defaults to False. + :param timeout: (optional) Seconds to wait, defaults to 60. + See the ``wait`` parameter. - :returns: the floating IP assigned + :returns: the update server dict """ if reuse: f_ip = self.available_floating_ip(network=network) else: f_ip = self.create_floating_ip(network=network) - self._attach_ip_to_server( - server=server, floating_ip=f_ip, fixed_address=fixed_address) + return self._attach_ip_to_server( + server=server, floating_ip=f_ip, fixed_address=fixed_address, + wait=wait, timeout=timeout) - return f_ip - - def add_ip_list(self, server, ips): + def add_ip_list(self, server, ips, wait=False, timeout=60): """Attach a list of IPs to a server. :param server: a server object - :param ips: list of IP addresses (floating IPs) + :param ips: list of floating IP addresses or a single address + :param wait: (optional) Wait for the address to appear as assigned + to the server in Nova. Defaults to False. + :param timeout: (optional) Seconds to wait, defaults to 60. + See the ``wait`` parameter. - :returns: None + :returns: The updated server dict :raises: ``OpenStackCloudException``, on operation error. """ - # ToDo(dguerri): this makes no sense as we cannot attach multiple - # floating IPs to a single fixed_address (this is true for both - # neutron and nova). I will leave this here for the moment as we are - # refactoring floating IPs methods. - for ip in ips: - f_ip = self.get_floating_ip( - id=None, filters={'floating_ip_address': ip}) - self._attach_ip_to_server( - server=server, floating_ip=f_ip) + if type(ips) == list: + ip = ips[0] + else: + ip = ips + f_ip = self.get_floating_ip( + id=None, filters={'floating_ip_address': ip}) + return self._attach_ip_to_server( + server=server, floating_ip=f_ip, wait=wait, timeout=timeout) def add_auto_ip(self, server, wait=False, timeout=60, reuse=True): """Add a floating IP to a server. @@ -3146,6 +3154,11 @@ def add_auto_ip(self, server, wait=False, timeout=60, reuse=True): :returns: Floating IP address attached to server. """ + server = self._add_auto_ip( + server, wait=wait, timeout=timeout, reuse=reuse) + return self.get_server_public_ip(server) + + def _add_auto_ip(self, server, wait=False, timeout=60, reuse=True): skip_attach = False if reuse: f_ip = self.available_floating_ip() @@ -3156,35 +3169,22 @@ def add_auto_ip(self, server, wait=False, timeout=60, reuse=True): # but is only meaninful for the neutron logic branch skip_attach = True - self._attach_ip_to_server( + return self._attach_ip_to_server( server=server, floating_ip=f_ip, wait=wait, timeout=timeout, skip_attach=skip_attach) - return f_ip - def add_ips_to_server( self, server, auto_ip=True, ips=None, ip_pool=None, wait=False, timeout=60, reuse=True): if ip_pool: - self._add_ip_from_pool(server, ip_pool, reuse=reuse) + server = self._add_ip_from_pool( + server, ip_pool, reuse=reuse, wait=wait, timeout=timeout) elif ips: - self.add_ip_list(server, ips) + server = self.add_ip_list(server, ips, wait=wait, timeout=timeout) elif auto_ip: - if self.get_server_public_ip(server): - return server - self.add_auto_ip( - server, wait=wait, timeout=timeout, reuse=reuse) - else: - return server - - # this may look redundant, but if there is now a - # floating IP, then it needs to be obtained from - # a recent server object if the above code path exec'd - try: - server = self.get_server_by_id(server['id']) - except Exception as e: - raise OpenStackCloudException( - "Error in getting info from instance: %s " % str(e)) + if not self.get_server_public_ip(server): + server = self._add_auto_ip( + server, wait=wait, timeout=timeout, reuse=reuse) return server @_utils.valid_kwargs( diff --git a/shade/tests/unit/test_floating_ip_common.py b/shade/tests/unit/test_floating_ip_common.py index 3b46d0ed5..4a49b5caa 100644 --- a/shade/tests/unit/test_floating_ip_common.py +++ b/shade/tests/unit/test_floating_ip_common.py @@ -75,7 +75,8 @@ def test_add_ips_to_server_pool( self.client.add_ips_to_server(server_dict, ip_pool=pool) - mock_add_ip_from_pool.assert_called_with(server_dict, pool, reuse=True) + mock_add_ip_from_pool.assert_called_with( + server_dict, pool, reuse=True, wait=False, timeout=60) @patch.object(OpenStackCloud, 'nova_client') @patch.object(OpenStackCloud, 'add_ip_list') @@ -90,10 +91,11 @@ def test_add_ips_to_server_ip_list( self.client.add_ips_to_server(server_dict, ips=ips) - mock_add_ip_list.assert_called_with(server_dict, ips) + mock_add_ip_list.assert_called_with( + server_dict, ips, wait=False, timeout=60) @patch.object(OpenStackCloud, 'nova_client') - @patch.object(OpenStackCloud, 'add_auto_ip') + @patch.object(OpenStackCloud, '_add_auto_ip') def test_add_ips_to_server_auto_ip( self, mock_add_auto_ip, mock_nova_client): server = FakeServer( diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 0b5e69523..b2b8a9add 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -258,7 +258,7 @@ def test_auto_ip_pool_no_reuse( network_name_or_id='my-network', server=None) mock_attach_ip_to_server.assert_called_once_with( server={'id': '1234'}, fixed_address=None, - floating_ip=self.floating_ip) + floating_ip=self.floating_ip, wait=False, timeout=60) @patch.object(OpenStackCloud, 'keystone_session') @patch.object(OpenStackCloud, '_neutron_create_floating_ip') @@ -376,13 +376,11 @@ def test_add_ip_from_pool( mock_available_floating_ip.return_value = \ _utils.normalize_neutron_floating_ips([ self.mock_floating_ip_new_rep['floatingip']])[0] - mock_attach_ip_to_server.return_value = None + mock_attach_ip_to_server.return_value = self.fake_server - ip = self.client._add_ip_from_pool( + server = self.client._add_ip_from_pool( server=self.fake_server, network='network-name', fixed_address='192.0.2.129') - self.assertEqual( - self.mock_floating_ip_new_rep['floatingip']['floating_ip_address'], - ip['floating_ip_address']) + self.assertEqual(server, self.fake_server) diff --git a/shade/tests/unit/test_floating_ip_nova.py b/shade/tests/unit/test_floating_ip_nova.py index 03c834cb7..2ad83b894 100644 --- a/shade/tests/unit/test_floating_ip_nova.py +++ b/shade/tests/unit/test_floating_ip_nova.py @@ -242,9 +242,9 @@ def test_add_ip_from_pool(self, mock_has_service, mock_nova_client): mock_has_service.side_effect = has_service_side_effect mock_nova_client.floating_ips.list.return_value = self.floating_ips - ip = self.client._add_ip_from_pool( + server = self.client._add_ip_from_pool( server=self.fake_server, network='nova', fixed_address='192.0.2.129') - self.assertEqual('203.0.113.1', ip['floating_ip_address']) + self.assertEqual(server, self.fake_server) From 7c5087d17bce239c10f6827c26ecb7b149f3d908 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 1 Nov 2015 11:20:16 +0900 Subject: [PATCH 0601/3836] Don't wrap wrapped exception in create_server If we've wrapped the exception because of Auth things, we don't need to re-wrap it - it's just confusing. Change-Id: Ia1e45f4971f4b51f28538260d64f778aecaa6f3d --- shade/openstackcloud.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 3b9f2955c..18326d373 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -302,6 +302,8 @@ def keystone_session(self): verify=self.verify, cert=self.cert, timeout=self.api_timeout) + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Error authenticating to keystone: %s " % str(e)) @@ -828,6 +830,8 @@ def get_session_endpoint(self, service_key): self.log.debug( "Endpoint not found in %s cloud: %s", self.name, str(e)) endpoint = None + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Error getting %s endpoint: %s" % (service_key, str(e))) @@ -3278,6 +3282,8 @@ def create_server( # new server server = self.get_server_by_id(server.id) server_id = server['id'] + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Error in creating instance: {0}".format(e)) From d68e819fd7395f87fafb6a52842d43559ae43425 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 2 Nov 2015 12:08:16 -0500 Subject: [PATCH 0602/3836] Remove another extraneous get for create_server The pattern of doing a get after a create is one we've been doing in Infra for quite a while. However, if we're going to spin off a wait loop, there is no need for this get, as the only thing we need in the loop is the server id - which we already have. OTOH, if we're not going to wait, this is still a useful get to do. Lucky for us, python has a feature know as "if" which makes expressing this easy. Change-Id: Ida354fb8fc772325a2fcf8e4ef1dcf3841176e2b --- shade/openstackcloud.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 18326d373..ad3f2d116 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3277,19 +3277,25 @@ def create_server( try: server = self.manager.submitTask(_tasks.ServerCreate( name=name, image=image, flavor=flavor, **kwargs)) - # This is a direct get task call to skip the list_servers - # cache which has absolutely no chance of containing the - # new server - server = self.get_server_by_id(server.id) - server_id = server['id'] + server_id = server.id + if not wait: + # This is a direct get task call to skip the list_servers + # cache which has absolutely no chance of containing the + # new server + # Only do this if we're not going to wait for the server + # to complete booting, because the only reason we do it + # is to get a server record that is the return value from + # get/list rather than the return value of create. If we're + # going to do the wait loop below, this is a waste of a call + server = self.get_server_by_id(server.id) + if server.status == 'ERROR': + raise OpenStackCloudException( + "Error in creating the server.") except OpenStackCloudException: raise except Exception as e: raise OpenStackCloudException( "Error in creating instance: {0}".format(e)) - if server.status == 'ERROR': - raise OpenStackCloudException( - "Error in creating the server.") if wait: # There is no point in iterating faster than the list_servers cache for count in _utils._iterate_timeout( From 5f2b64bd313b2d6f5578d824537836c45763e7a5 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 2 Nov 2015 21:44:06 -0500 Subject: [PATCH 0603/3836] Copy values in backwards_interface differently There is the world's weirdest race condition in the unit test for this on python 3.4. Change-Id: Idee8320f03651964600ba0822edfff04f880ea4a --- os_client_config/config.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 483b214c8..92eb15d64 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -406,11 +406,14 @@ def _fix_backwards_auth_plugin(self, cloud): return cloud def _fix_backwards_interface(self, cloud): + new_cloud = {} for key in cloud.keys(): if key.endswith('endpoint_type'): target_key = key.replace('endpoint_type', 'interface') - cloud[target_key] = cloud.pop(key) - return cloud + else: + target_key = key + new_cloud[target_key] = cloud[key] + return new_cloud def get_all_clouds(self): From 48c0668b155232137cbdcf6537caca83cb870630 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 2 Nov 2015 12:35:11 -0500 Subject: [PATCH 0604/3836] Update auro to indicate move to neutron Auro runs neutron now. Update the config and the docs to reflect that. Change-Id: I4d2fd12b6f3d93f9e1d201b687145f1fe022e593 --- doc/source/vendor-support.rst | 4 +--- os_client_config/vendors/auro.yaml | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index 34cfbe937..c3d4b3eed 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -35,9 +35,7 @@ Region Name Human Name van1 Vancouver, BC ============== ================ -* Public IPv4 is provided via NAT with Nova Floating IP -* Floating IPs are provided by Nova -* Security groups are provided by Nova +* Public IPv4 is provided via NAT with Neutron Floating IP catalyst -------- diff --git a/os_client_config/vendors/auro.yaml b/os_client_config/vendors/auro.yaml index ad721e62d..094ab7479 100644 --- a/os_client_config/vendors/auro.yaml +++ b/os_client_config/vendors/auro.yaml @@ -3,5 +3,3 @@ profile: auth: auth_url: https://api.van1.auro.io:5000/v2.0 region_name: van1 - secgroup_source: nova - floating_ip_source: nova From b7ad03eadc52c3c234005b09295af8dff33eda8e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 3 Nov 2015 08:47:52 -0500 Subject: [PATCH 0605/3836] Minor logging improvements While testing running an inventory on a clouds.yaml with 17 clouds, errors popped up that were opaque. These are mainly just the things I noticed doing that. Change-Id: If3f0dc85b7a8d5a510f9f7b561f3c49edd817908 --- shade/__init__.py | 1 + shade/openstackcloud.py | 13 +++++++++++-- shade/tests/unit/test_shade_operator.py | 5 ++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 3632de0d5..320503498 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -50,6 +50,7 @@ def simple_logging(debug=False): log.setLevel(log_level) # Suppress warning about keystoneauth loggers log = _log.setup_logging('keystoneauth.identity.base') + log = _log.setup_logging('keystoneauth.identity.generic.base') def openstack_clouds(config=None, debug=False): diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 9c334cc02..273aced10 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -830,7 +830,12 @@ def get_session_endpoint(self, service_key): endpoint = None except Exception as e: raise OpenStackCloudException( - "Error getting %s endpoint: %s" % (service_key, str(e))) + "Error getting {service} endpoint on {cloud}:{region}:" + " {error}".format( + service=service_key, + cloud=self.name, + region=self.region_name, + error=str(e))) return endpoint def has_service(self, service_key): @@ -1193,7 +1198,11 @@ def _list_servers(self, detailed=False): return servers except Exception as e: raise OpenStackCloudException( - "Error fetching server list: %s" % e) + "Error fetching server list on {cloud}:{region}:" + " {error}".format( + cloud=self.name, + region=self.region_name, + error=str(e))) @_utils.cache_on_arguments(should_cache_fn=_no_pending_images) def list_images(self, filter_deleted=True): diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index 78a4b8acf..2d15df621 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -1012,9 +1012,12 @@ class FakeException(Exception): def side_effect(*args, **kwargs): raise FakeException("No service") session_mock.get_endpoint.side_effect = side_effect + self.cloud.name = 'testcloud' + self.cloud.region_name = 'testregion' with testtools.ExpectedException( exc.OpenStackCloudException, - "Error getting image endpoint: No service"): + "Error getting image endpoint on testcloud:testregion:" + " No service"): self.cloud.get_session_endpoint("image") @mock.patch.object(shade.OpenStackCloud, 'keystone_session') From b5b3fbaafa3556b55a63e2b45600f43f538defff Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 2 Nov 2015 06:36:14 -0500 Subject: [PATCH 0606/3836] Make raw-requests calls behave like client calls NovaGetUrl returns a tuple of response/body, but everything else returns the actual object. If we're going to make raw calls, we should handle them in such a way that the rest of the code doesn't konw the difference. This also means that we have the opportunity to look at response codes and throw exceptions if they aren't 200. Change-Id: I0bab5056b5ba234f5509b56a716ce426e6a2c604 --- shade/_tasks.py | 1 + shade/openstackcloud.py | 2 +- shade/task_manager.py | 10 +++++++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 77ac1ac56..e291658bb 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -143,6 +143,7 @@ def main(self, client): class NovaUrlGet(task_manager.Task): def main(self, client): + self.requests = True return client.nova_client.client.get(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 9c334cc02..035bc928c 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -853,7 +853,7 @@ def _nova_extensions(self): extensions = set() try: - resp, body = self.manager.submitTask( + body = self.manager.submitTask( _tasks.NovaUrlGet(url='/extensions')) for x in body['extensions']: extensions.add(x['alias']) diff --git a/shade/task_manager.py b/shade/task_manager.py index f26739eda..f44c8ef23 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -48,15 +48,20 @@ def __init__(self, **kw): self._exception = None self._traceback = None self._result = None + self._response = None self._finished = threading.Event() self.args = kw + self.requests = False @abc.abstractmethod def main(self, client): """ Override this method with the actual workload to be performed """ def done(self, result): - self._result = result + if self.requests: + self._response, self._result = result + else: + self._result = result self._finished.set() def exception(self, e, tb): @@ -66,6 +71,9 @@ def exception(self, e, tb): def wait(self): self._finished.wait() + # TODO(mordred): We store the raw requests response if there is + # one now. So we should probably do an error handler to throw + # some exceptions if it's not 200 if self._exception: six.reraise(type(self._exception), self._exception, self._traceback) From 586dbbb0a4d5d6b5a87e8ceea4cf4497f9452a64 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 24 Oct 2015 03:59:01 +0900 Subject: [PATCH 0607/3836] Always return a munch from Tasks This should let us remove a ton of calls to obj_to_dict everywhere. Change-Id: I627b0c05a43e313cc4ff045835a7eda50bab440a --- shade/meta.py | 20 ++++++++++-------- shade/task_manager.py | 6 +++++- shade/tests/fakes.py | 3 ++- shade/tests/unit/test_caching.py | 6 +++--- shade/tests/unit/test_meta.py | 15 ++++++------- shade/tests/unit/test_rebuild_server.py | 28 ++++++++++++++----------- 6 files changed, 45 insertions(+), 33 deletions(-) diff --git a/shade/meta.py b/shade/meta.py index 9b8805653..9d4c6f47a 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -323,9 +323,18 @@ def obj_to_dict(obj): that we can just have a plain dict of all of the values that exist in the nova metadata for a server. """ - # If we obj_to_dict twice, don't fail, just return the munch - if type(obj) == munch.Munch: + if obj is None: + return None + elif type(obj) == munch.Munch or hasattr(obj, 'mock_add_spec'): + # If we obj_to_dict twice, don't fail, just return the munch + # Also, don't try to modify Mock objects - that way lies madness return obj + elif type(obj) == dict: + return munch.Munch(obj) + elif hasattr(obj, 'schema') and hasattr(obj, 'validate'): + # It's a warlock + return warlock_to_dict(obj) + instance = munch.Munch() for key in dir(obj): value = getattr(obj, key) @@ -356,10 +365,3 @@ def warlock_to_dict(obj): if isinstance(value, NON_CALLABLES) and not key.startswith('_'): obj_dict[key] = value return obj_dict - - -def warlock_list_to_dict(list): - new_list = [] - for obj in list: - new_list.append(warlock_to_dict(obj)) - return new_list diff --git a/shade/task_manager.py b/shade/task_manager.py index f44c8ef23..6e60278b4 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -24,6 +24,7 @@ import six from shade import _log +from shade import meta @six.add_metaclass(abc.ABCMeta) @@ -77,7 +78,10 @@ def wait(self): if self._exception: six.reraise(type(self._exception), self._exception, self._traceback) - return self._result + if type(self._result) == list: + return meta.obj_list_to_dict(self._result) + else: + return meta.obj_to_dict(self._result) def run(self, client): try: diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 46b92142c..676ff0348 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -95,10 +95,11 @@ def __init__(self, id, email, name): class FakeVolume(object): - def __init__(self, id, status, display_name): + def __init__(self, id, status, display_name, attachments=[]): self.id = id self.status = status self.display_name = display_name + self.attachments = attachments class FakeVolumeSnapshot(object): diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index a224ba523..5715b7132 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -315,10 +315,10 @@ def test_create_image_put_v1(self, glance_mock, mock_api_version): 'properties': {'owner_specified.shade.md5': mock.ANY, 'owner_specified.shade.sha256': mock.ANY, 'is_public': False}} - glance_mock.images.create.assert_called_with(**args) - glance_mock.images.update.assert_called_with(data=mock.ANY, - image=fake_image) fake_image_dict = meta.obj_to_dict(fake_image) + glance_mock.images.create.assert_called_with(**args) + glance_mock.images.update.assert_called_with( + data=mock.ANY, image=fake_image_dict) self.assertEqual([fake_image_dict], self.cloud.list_images()) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index f1c2bf1f6..631893308 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -423,13 +423,14 @@ def test_az(self, mock_get_server_external_ipv4): def test_has_volume(self): mock_cloud = mock.MagicMock() - mock_volume = mock.MagicMock() - mock_volume.id = 'volume1' - mock_volume.status = 'available' - mock_volume.display_name = 'Volume 1 Display Name' - mock_volume.attachments = [{'device': '/dev/sda0'}] - mock_volume_dict = meta.obj_to_dict(mock_volume) - mock_cloud.get_volumes.return_value = [mock_volume_dict] + + fake_volume = fakes.FakeVolume( + id='volume1', + status='available', + display_name='Volume 1 Display Name', + attachments=[{'device': '/dev/sda0'}]) + fake_volume_dict = meta.obj_to_dict(fake_volume) + mock_cloud.get_volumes.return_value = [fake_volume_dict] hostvars = meta.get_hostvars_from_server( mock_cloud, meta.obj_to_dict(FakeServer())) self.assertEquals('volume1', hostvars['volumes'][0]['id']) diff --git a/shade/tests/unit/test_rebuild_server.py b/shade/tests/unit/test_rebuild_server.py index c3feb41f3..436824e0d 100644 --- a/shade/tests/unit/test_rebuild_server.py +++ b/shade/tests/unit/test_rebuild_server.py @@ -24,7 +24,7 @@ from shade import meta from shade import OpenStackCloud from shade.exc import (OpenStackCloudException, OpenStackCloudTimeout) -from shade.tests.unit import base +from shade.tests import base, fakes class TestRebuildServer(base.TestCase): @@ -53,10 +53,12 @@ def test_rebuild_server_server_error(self): Test that a server error while waiting for the server to rebuild raises an exception in rebuild_server. """ + rebuild_server = fakes.FakeServer('1234', '', 'REBUILD') + error_server = fakes.FakeServer('1234', '', 'ERROR') with patch("shade.OpenStackCloud"): config = { - "servers.rebuild.return_value": Mock(status="REBUILD"), - "servers.get.return_value": Mock(status="ERROR") + "servers.rebuild.return_value": rebuild_server, + "servers.get.return_value": error_server, } OpenStackCloud.nova_client = Mock(**config) self.assertRaises( @@ -68,10 +70,11 @@ def test_rebuild_server_timeout(self): Test that a timeout while waiting for the server to rebuild raises an exception in rebuild_server. """ + rebuild_server = fakes.FakeServer('1234', '', 'REBUILD') with patch("shade.OpenStackCloud"): config = { - "servers.rebuild.return_value": Mock(status="REBUILD"), - "servers.get.return_value": Mock(status="REBUILD") + "servers.rebuild.return_value": rebuild_server, + "servers.get.return_value": rebuild_server, } OpenStackCloud.nova_client = Mock(**config) self.assertRaises( @@ -84,12 +87,12 @@ def test_rebuild_server_no_wait(self): novaclient rebuild call returns the server instance. """ with patch("shade.OpenStackCloud"): - mock_server = Mock(status="ACTIVE") + rebuild_server = fakes.FakeServer('1234', '', 'REBUILD') config = { - "servers.rebuild.return_value": mock_server + "servers.rebuild.return_value": rebuild_server } OpenStackCloud.nova_client = Mock(**config) - self.assertEqual(meta.obj_to_dict(mock_server), + self.assertEqual(meta.obj_to_dict(rebuild_server), self.client.rebuild_server("a", "b")) def test_rebuild_server_wait(self): @@ -98,11 +101,12 @@ def test_rebuild_server_wait(self): its status changes to "ACTIVE". """ with patch("shade.OpenStackCloud"): - mock_server = Mock(status="ACTIVE") + rebuild_server = fakes.FakeServer('1234', '', 'REBUILD') + active_server = fakes.FakeServer('1234', '', 'ACTIVE') config = { - "servers.rebuild.return_value": Mock(status="REBUILD"), - "servers.get.return_value": mock_server + "servers.rebuild.return_value": rebuild_server, + "servers.get.return_value": active_server } OpenStackCloud.nova_client = Mock(**config) - self.assertEqual(meta.obj_to_dict(mock_server), + self.assertEqual(meta.obj_to_dict(active_server), self.client.rebuild_server("a", "b", wait=True)) From 5dca987851d7ccb2e42b25e768be88be1797beaa Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 1 Nov 2015 13:02:09 +0900 Subject: [PATCH 0608/3836] Stop calling obj_to_dict everwhere Now that Task objects just return the right thing, we can strip a ton of calls. Change-Id: I3aac80f83ba259a358534f1c4357c4631dbd3a9f --- shade/openstackcloud.py | 146 +++++++++++----------------------- shade/operatorcloud.py | 86 ++++++++------------ shade/tests/unit/test_meta.py | 2 +- 3 files changed, 78 insertions(+), 156 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 035bc928c..360d8ba22 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -387,7 +387,7 @@ def list_projects(self): except Exception as e: self.log.debug("Failed to list projects", exc_info=True) raise OpenStackCloudException(str(e)) - return meta.obj_list_to_dict(projects) + return projects def search_projects(self, name_or_id=None, filters=None): """Seach Keystone projects. @@ -438,7 +438,7 @@ def update_project(self, name_or_id, description=None, enabled=True): "Error in updating project {project}: {message}".format( project=name_or_id, message=str(e))) self.list_projects.invalidate() - return meta.obj_to_dict(project) + return project def create_project( self, name, description=None, domain_id=None, enabled=True): @@ -458,7 +458,7 @@ def create_project( "Error in creating project {project}: {message}".format( project=name, message=str(e))) self.list_projects.invalidate() - return meta.obj_to_dict(project) + return project def delete_project(self, name_or_id): try: @@ -489,7 +489,7 @@ def list_users(self): raise OpenStackCloudException( "Failed to list users: {0}".format(str(e)) ) - return _utils.normalize_users(meta.obj_list_to_dict(users)) + return _utils.normalize_users(users) def search_users(self, name_or_id=None, filters=None): """Seach Keystone users. @@ -527,9 +527,7 @@ def get_user_by_id(self, user_id, normalize=True): :returns: a single dict containing the user description """ try: - user = meta.obj_to_dict( - self.manager.submitTask(_tasks.UserGet(user=user_id)) - ) + user = self.manager.submitTask(_tasks.UserGet(user=user_id)) except Exception as e: raise OpenStackCloudException( "Error getting user with ID {user_id}: {message}".format( @@ -566,7 +564,7 @@ def update_user(self, name_or_id, **kwargs): "Error in updating user {user}: {message}".format( user=name_or_id, message=str(e))) self.list_users.invalidate(self) - return _utils.normalize_users([meta.obj_to_dict(user)])[0] + return _utils.normalize_users([user])[0] def create_user( self, name, password=None, email=None, default_project=None, @@ -583,7 +581,7 @@ def create_user( "Error in creating user {user}: {message}".format( user=name, message=str(e))) self.list_users.invalidate(self) - return _utils.normalize_users([meta.obj_to_dict(user)])[0] + return _utils.normalize_users([user])[0] def delete_user(self, name_or_id): self.list_users.invalidate(self) @@ -1002,9 +1000,7 @@ def list_keypairs(self): """ try: - return meta.obj_list_to_dict( - self.manager.submitTask(_tasks.KeypairList()) - ) + return self.manager.submitTask(_tasks.KeypairList()) except Exception as e: raise OpenStackCloudException( "Error fetching keypair list: %s" % str(e)) @@ -1075,9 +1071,7 @@ def list_volumes(self, cache=True): warnings.warn('cache argument to list_volumes is deprecated. Use ' 'invalidate instead.') try: - return meta.obj_list_to_dict( - self.manager.submitTask(_tasks.VolumeList()) - ) + return self.manager.submitTask(_tasks.VolumeList()) except Exception as e: raise OpenStackCloudException( "Error fetching volume list: %s" % e) @@ -1090,9 +1084,7 @@ def list_flavors(self): """ try: - return meta.obj_list_to_dict( - self.manager.submitTask(_tasks.FlavorList(is_public=None)) - ) + return self.manager.submitTask(_tasks.FlavorList(is_public=None)) except Exception as e: raise OpenStackCloudException( "Error fetching flavor list: %s" % e) @@ -1110,7 +1102,7 @@ def list_stacks(self): stacks = self.manager.submitTask(_tasks.StackList()) except Exception as e: raise OpenStackCloudException(str(e)) - return meta.obj_list_to_dict(stacks) + return stacks def list_server_security_groups(self, server): """List all security groups associated with the given server. @@ -1118,9 +1110,8 @@ def list_server_security_groups(self, server): :returns: A list of security group dicts. """ - groups = meta.obj_list_to_dict( - self.manager.submitTask( - _tasks.ServerListSecurityGroups(server=server['id']))) + groups = self.manager.submitTask( + _tasks.ServerListSecurityGroups(server=server['id'])) return _utils.normalize_nova_secgroups(groups) @@ -1141,9 +1132,8 @@ def list_security_groups(self): # Handle nova security groups elif self.secgroup_source == 'nova': try: - groups = meta.obj_list_to_dict( - self.manager.submitTask(_tasks.NovaSecurityGroupList()) - ) + groups = self.manager.submitTask( + _tasks.NovaSecurityGroupList()) except Exception: raise OpenStackCloudException( "Error fetching security group list" @@ -1181,8 +1171,7 @@ def list_servers(self, detailed=False): def _list_servers(self, detailed=False): try: - servers = meta.obj_list_to_dict( - self.manager.submitTask(_tasks.ServerList())) + servers = self.manager.submitTask(_tasks.ServerList()) if detailed: return [ @@ -1214,21 +1203,11 @@ def list_images(self, filter_deleted=True): image_list = self.manager.submitTask( _tasks.GlanceImageList(image_gen=image_gen)) - if image_list: - if getattr(image_list[0], 'validate', None): - # glanceclient returns a "warlock" object if you use v2 - image_list = meta.warlock_list_to_dict(image_list) - else: - # glanceclient returns a normal object if you use v1 - image_list = meta.obj_list_to_dict(image_list) - except glanceclient.exc.HTTPInternalServerError: # We didn't have glance, let's try nova # If this doesn't work - we just let the exception propagate try: - image_list = meta.obj_list_to_dict( - self.manager.submitTask(_tasks.NovaImageList()) - ) + image_list = self.manager.submitTask(_tasks.NovaImageList()) except Exception as e: raise OpenStackCloudException( "Error fetching image list: %s" % e) @@ -1257,9 +1236,7 @@ def list_floating_ip_pools(self): 'Floating IP pools extension is not available on target cloud') try: - return meta.obj_list_to_dict( - self.manager.submitTask(_tasks.FloatingIPPoolList()) - ) + return self.manager.submitTask(_tasks.FloatingIPPoolList()) except Exception as e: raise OpenStackCloudException( "error fetching floating IP pool list: {msg}".format( @@ -1291,8 +1268,7 @@ def _neutron_list_floating_ips(self): def _nova_list_floating_ips(self): try: - return meta.obj_list_to_dict( - self.manager.submitTask(_tasks.NovaFloatingIPList())) + return self.manager.submitTask(_tasks.NovaFloatingIPList()) except Exception as e: raise OpenStackCloudException( "error fetching floating IPs list: {msg}".format(msg=str(e))) @@ -1582,8 +1558,7 @@ def get_server(self, name_or_id=None, filters=None, detailed=False): return _utils._get_entity(searchfunc, name_or_id, filters) def get_server_by_id(self, id): - return meta.obj_to_dict( - self.manager.submitTask(_tasks.ServerGet(server=id))) + return self.manager.submitTask(_tasks.ServerGet(server=id)) def get_image(self, name_or_id, filters=None): """Get an image by name or ID. @@ -1676,10 +1651,8 @@ def create_keypair(self, name, public_key): :raises: OpenStackCloudException on operation error. """ try: - return meta.obj_to_dict( - self.manager.submitTask(_tasks.KeypairCreate( - name=name, public_key=public_key)) - ) + return self.manager.submitTask(_tasks.KeypairCreate( + name=name, public_key=public_key)) except Exception as e: raise OpenStackCloudException( "Unable to create keypair %s: %s" % (name, e) @@ -1731,9 +1704,6 @@ def create_network(self, name, shared=False, admin_state_up=True, "Error creating network {0}".format(name)): net = self.manager.submitTask( _tasks.NetworkCreate(body=dict({'network': network}))) - # Turns out neutron returns an actual dict, so no need for the - # use of meta.obj_to_dict() here (which would not work against - # a dict). return net['network'] def delete_network(self, name_or_id): @@ -1900,9 +1870,6 @@ def create_router(self, name=None, admin_state_up=True, "Error creating router {0}".format(name)): new_router = self.manager.submitTask( _tasks.RouterCreate(body=dict(router=router))) - # Turns out neutron returns an actual dict, so no need for the - # use of meta.obj_to_dict() here (which would not work against - # a dict). return new_router['router'] def update_router(self, name_or_id, name=None, admin_state_up=None, @@ -1956,9 +1923,6 @@ def update_router(self, name_or_id, name=None, admin_state_up=None, _tasks.RouterUpdate( router=curr_router['id'], body=dict(router=router))) - # Turns out neutron returns an actual dict, so no need for the - # use of meta.obj_to_dict() here (which would not work against - # a dict). return new_router['router'] def delete_router(self, name_or_id): @@ -2186,7 +2150,7 @@ def _upload_image_task( message=status.message), extra_data=status) else: - return meta.warlock_to_dict(glance_task) + return glance_task def update_image_properties( self, image=None, name_or_id=None, **properties): @@ -2250,8 +2214,6 @@ def create_volume(self, wait=True, timeout=None, **kwargs): "Error in creating volume: %s" % str(e)) self.list_volumes.invalidate(self) - volume = meta.obj_to_dict(volume) - if volume['status'] == 'error': raise OpenStackCloudException("Error in creating volume") @@ -2434,8 +2396,6 @@ def attach_volume(self, server, volume, device=None, _tasks.VolumeAttach(volume_id=volume['id'], server_id=server['id'], device=device)) - vol = meta.obj_to_dict(vol) - except Exception as e: raise OpenStackCloudException( "Error attaching volume %s to server %s: %s" % @@ -2500,8 +2460,6 @@ def create_volume_snapshot(self, volume_id, force=False, "Error creating snapshot of volume %s: %s" % (volume_id, e) ) - snapshot = meta.obj_to_dict(snapshot) - if wait: snapshot_id = snapshot['id'] for count in _utils._iterate_timeout( @@ -2540,7 +2498,7 @@ def get_volume_snapshot_by_id(self, snapshot_id): "Error getting snapshot %s: %s" % (snapshot_id, e) ) - return meta.obj_to_dict(snapshot) + return snapshot def get_volume_snapshot(self, name_or_id, filters=None): """Get a volume by name or ID. @@ -2571,11 +2529,9 @@ def list_volume_snapshots(self, detailed=True, search_opts=None): """ try: - return meta.obj_list_to_dict( - self.manager.submitTask( - _tasks.VolumeSnapshotList(detailed=detailed, - search_opts=search_opts) - ) + return self.manager.submitTask( + _tasks.VolumeSnapshotList(detailed=detailed, + search_opts=search_opts) ) except Exception as e: @@ -2832,7 +2788,7 @@ def _nova_create_floating_ip(self, pool=None): pool_ip = self.manager.submitTask( _tasks.NovaFloatingIPCreate(pool=pool)) - return meta.obj_to_dict(pool_ip) + return pool_ip except Exception as e: raise OpenStackCloudException( @@ -3363,7 +3319,7 @@ def rebuild_server(self, server_id, image_id, wait=False, timeout=180): raise OpenStackCloudException( "Error in rebuilding the server", extra_data=dict(server=server)) - return meta.obj_to_dict(server) + return server def delete_server( self, name_or_id, wait=False, timeout=180, delete_ips=False): @@ -3407,7 +3363,6 @@ def _delete_server( server = self.get_server_by_id(server['id']) if not server: return - server = meta.obj_to_dict(server) except nova_exceptions.NotFound: return except Exception as e: @@ -3416,8 +3371,7 @@ def _delete_server( def list_containers(self): try: - return meta.obj_to_dict(self.manager.submitTask( - _tasks.ContainerList())) + return manager.submitTask(_tasks.ContainerList()) except swift_exceptions.ClientException as e: raise OpenStackCloudException( "Container list failed: %s (%s/%s)" % ( @@ -3615,8 +3569,7 @@ def create_object( def list_objects(self, container): try: - return meta.obj_to_dict(self.manager.submitTask( - _tasks.ObjectList(container))) + return self.manager.submitTask(_tasks.ObjectList(container)) except swift_exceptions.ClientException as e: raise OpenStackCloudException( "Object list failed: %s (%s/%s)" % ( @@ -3851,9 +3804,6 @@ def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, new_subnet = self.manager.submitTask( _tasks.SubnetUpdate( subnet=curr_subnet['id'], body=dict(subnet=subnet))) - # Turns out neutron returns an actual dict, so no need for the - # use of meta.obj_to_dict() here (which would not work against - # a dict). return new_subnet['subnet'] @_utils.valid_kwargs('name', 'admin_state_up', 'mac_address', 'fixed_ips', @@ -4027,11 +3977,9 @@ def create_security_group(self, name, description): elif self.secgroup_source == 'nova': try: - group = meta.obj_to_dict( - self.manager.submitTask( - _tasks.NovaSecurityGroupCreate( - name=name, description=description - ) + group = self.manager.submitTask( + _tasks.NovaSecurityGroupCreate( + name=name, description=description ) ) except Exception as e: @@ -4120,11 +4068,9 @@ def update_security_group(self, name_or_id, **kwargs): elif self.secgroup_source == 'nova': try: - group = meta.obj_to_dict( - self.manager.submitTask( - _tasks.NovaSecurityGroupUpdate( - group=secgroup['id'], **kwargs) - ) + group = self.manager.submitTask( + _tasks.NovaSecurityGroupUpdate( + group=secgroup['id'], **kwargs) ) except Exception as e: raise OpenStackCloudException( @@ -4249,16 +4195,14 @@ def create_security_group_rule(self, port_range_max = 65535 try: - rule = meta.obj_to_dict( - self.manager.submitTask( - _tasks.NovaSecurityGroupRuleCreate( - parent_group_id=secgroup['id'], - ip_protocol=protocol, - from_port=port_range_min, - to_port=port_range_max, - cidr=remote_ip_prefix, - group_id=remote_group_id - ) + rule = self.manager.submitTask( + _tasks.NovaSecurityGroupRuleCreate( + parent_group_id=secgroup['id'], + ip_protocol=protocol, + from_port=port_range_min, + to_port=port_range_max, + cidr=remote_ip_prefix, + group_id=remote_group_id ) ) except Exception as e: diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index d94f2e3a2..65b51af31 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -17,7 +17,6 @@ from novaclient import exceptions as nova_exceptions from shade.exc import * # noqa -from shade import meta from shade import openstackcloud from shade import _tasks from shade import _utils @@ -58,9 +57,7 @@ def ironic_client(self): def list_nics(self): try: - return meta.obj_list_to_dict( - self.manager.submitTask(_tasks.MachinePortList()) - ) + return self.manager.submitTask(_tasks.MachinePortList()) except OpenStackCloudException: raise except Exception as e: @@ -69,10 +66,8 @@ def list_nics(self): def list_nics_for_machine(self, uuid): try: - return meta.obj_list_to_dict( - self.manager.submitTask( - _tasks.MachineNodePortList(node_id=uuid)) - ) + return self.manager.submitTask( + _tasks.MachineNodePortList(node_id=uuid)) except OpenStackCloudException: raise except Exception as e: @@ -81,17 +76,13 @@ def list_nics_for_machine(self, uuid): def get_nic_by_mac(self, mac): try: - return meta.obj_to_dict( - self.manager.submitTask( - _tasks.MachineNodePortGet(port_id=mac)) - ) + return self.manager.submitTask( + _tasks.MachineNodePortGet(port_id=mac)) except ironic_exceptions.ClientException: return None def list_machines(self): - return meta.obj_list_to_dict( - self.manager.submitTask(_tasks.MachineNodeList()) - ) + return self.manager.submitTask(_tasks.MachineNodeList()) def get_machine(self, name_or_id): """Get Machine by name or uuid @@ -105,10 +96,8 @@ def get_machine(self, name_or_id): are found. """ try: - return meta.obj_to_dict( - self.manager.submitTask( - _tasks.MachineNodeGet(node_id=name_or_id)) - ) + return self.manager.submitTask( + _tasks.MachineNodeGet(node_id=name_or_id)) except ironic_exceptions.ClientException: return None @@ -123,10 +112,8 @@ def get_machine_by_mac(self, mac): try: port = self.manager.submitTask( _tasks.MachinePortGetByAddress(address=mac)) - return meta.obj_to_dict( - self.manager.submitTask( - _tasks.MachineNodeGet(node_id=port.node_uuid)) - ) + return self.manager.submitTask( + _tasks.MachineNodeGet(node_id=port.node_uuid)) except ironic_exceptions.ClientException: return None @@ -247,8 +234,7 @@ def register_machine(self, nics, wait=False, timeout=3600, baremetal node. """ try: - machine = meta.obj_to_dict( - self.manager.submitTask(_tasks.MachineCreate(**kwargs))) + machine = self.manager.submitTask(_tasks.MachineCreate(**kwargs)) except OpenStackCloudException: raise @@ -445,11 +431,10 @@ def patch_machine(self, name_or_id, patch): """ try: - return meta.obj_to_dict( - self.manager.submitTask( - _tasks.MachinePatch(node_id=name_or_id, - patch=patch, - http_method='PATCH'))) + return self.manager.submitTask( + _tasks.MachinePatch(node_id=name_or_id, + patch=patch, + http_method='PATCH')) except OpenStackCloudException: raise except Exception as e: @@ -798,10 +783,8 @@ def deactivate_node(self, uuid): def set_node_instance_info(self, uuid, patch): try: - return meta.obj_to_dict( - self.manager.submitTask( - _tasks.MachineNodeUpdate(node_id=uuid, patch=patch)) - ) + return self.manager.submitTask( + _tasks.MachineNodeUpdate(node_id=uuid, patch=patch)) except OpenStackCloudException: raise except Exception as e: @@ -811,10 +794,8 @@ def purge_node_instance_info(self, uuid): patch = [] patch.append({'op': 'remove', 'path': '/instance_info'}) try: - return meta.obj_to_dict( - self.manager.submitTask( - _tasks.MachineNodeUpdate(node_id=uuid, patch=patch)) - ) + return self.manager.submitTask( + _tasks.MachineNodeUpdate(node_id=uuid, patch=patch)) except OpenStackCloudException: raise except Exception as e: @@ -857,7 +838,7 @@ def create_service(self, name, **kwargs): raise OpenStackCloudException( "Failed to create service {name}: {msg}".format( name=name, msg=str(e))) - return meta.obj_to_dict(service) + return service def list_services(self): """List all Keystone services. @@ -873,8 +854,7 @@ def list_services(self): raise except Exception as e: raise OpenStackCloudException(str(e)) - return _utils.normalize_keystone_services( - meta.obj_list_to_dict(services)) + return _utils.normalize_keystone_services(services) def search_services(self, name_or_id=None, filters=None): """Search Keystone services. @@ -1025,7 +1005,7 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, "{msg}".format(service=service['name'], msg=str(e))) endpoints.append(endpoint) - return meta.obj_list_to_dict(endpoints) + return endpoints def list_endpoints(self): """List Keystone endpoints. @@ -1043,7 +1023,7 @@ def list_endpoints(self): except Exception as e: raise OpenStackCloudException("Failed to list endpoints: {msg}" .format(msg=str(e))) - return meta.obj_list_to_dict(endpoints) + return endpoints def search_endpoints(self, id=None, filters=None): """List Keystone endpoints. @@ -1137,15 +1117,13 @@ def create_domain( raise OpenStackCloudException( "Failed to create domain {name}".format(name=name, msg=str(e))) - return meta.obj_to_dict(domain) + return domain def update_domain( self, domain_id, name=None, description=None, enabled=None): try: - return meta.obj_to_dict( - self.manager.submitTask(_tasks.DomainUpdate( - domain=domain_id, description=description, - enabled=enabled))) + return self.manager.submitTask(_tasks.DomainUpdate( + domain=domain_id, description=description, enabled=enabled)) except OpenStackCloudException: raise except Exception as e: @@ -1191,7 +1169,7 @@ def list_domains(self): except Exception as e: raise OpenStackCloudException("Failed to list domains: {msg}" .format(msg=str(e))) - return meta.obj_list_to_dict(domains) + return domains def search_domains(self, **filters): """Seach Keystone domains. @@ -1216,7 +1194,7 @@ def search_domains(self, **filters): except Exception as e: raise OpenStackCloudException("Failed to list domains: {msg}" .format(msg=str(e))) - return meta.obj_list_to_dict(domains) + return domains def get_domain(self, domain_id): """Get exactly one Keystone domain. @@ -1243,7 +1221,7 @@ def get_domain(self, domain_id): domain_id=domain_id, msg=str(e))) raise OpenStackCloudException(str(e)) - return meta.obj_to_dict(domain) + return domain def list_roles(self): """List Keystone roles. @@ -1259,7 +1237,7 @@ def list_roles(self): raise except Exception as e: raise OpenStackCloudException(str(e)) - return meta.obj_list_to_dict(roles) + return roles def search_roles(self, name_or_id=None, filters=None): """Seach Keystone roles. @@ -1330,7 +1308,7 @@ def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", "Failed to create flavor {name}: {msg}".format( name=name, msg=str(e))) - return meta.obj_to_dict(flavor) + return flavor def delete_flavor(self, name_or_id): """Delete a flavor @@ -1480,7 +1458,7 @@ def create_role(self, name): raise except Exception as e: raise OpenStackCloudException(str(e)) - return meta.obj_to_dict(role) + return role def delete_role(self, name_or_id): """Delete a Keystone role. diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 631893308..fc9d255dc 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -482,7 +482,7 @@ def test_warlock_to_dict(self): test_obj = test_model( id='471c2475-da2f-47ac-aba5-cb4aa3d546f5', name='test-image') - test_dict = meta.warlock_to_dict(test_obj) + test_dict = meta.obj_to_dict(test_obj) self.assertNotIn('_unused', test_dict) self.assertEqual('test-image', test_dict['name']) self.assertTrue(hasattr(test_dict, 'name')) From 796bfad22dae30e39f678c38fc9fba4f7f96032b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 6 Jun 2015 09:40:03 -0400 Subject: [PATCH 0609/3836] Use json for in-tree cloud data In preparation for sharing the default and vendor data with other projects, potentially even non-python ones, move the data into json format, which is slighly less exciting to read, but has more widespread standard library support. The user-facing config file will still be in yaml format, because that's easier on the eyes and it's expected to be read and edited by humans. Continue to accept yaml everywhere, because an end user may have dropped a yaml config file into a dir somewhere, and that's fine. Change-Id: I269d31e61da433ac20abb39acdde0f9f9fe12837 --- os_client_config/config.py | 17 +- os_client_config/defaults.json | 19 ++ os_client_config/defaults.py | 11 +- os_client_config/defaults.yaml | 17 -- os_client_config/schema.json | 121 ++++++++++ os_client_config/tests/test_json.py | 62 ++++++ os_client_config/vendor-schema.json | 206 ++++++++++++++++++ os_client_config/vendors/__init__.py | 7 +- os_client_config/vendors/auro.json | 9 + os_client_config/vendors/auro.yaml | 5 - os_client_config/vendors/bluebox.json | 6 + os_client_config/vendors/catalyst.json | 14 ++ os_client_config/vendors/catalyst.yaml | 9 - os_client_config/vendors/citycloud.json | 14 ++ os_client_config/vendors/citycloud.yaml | 9 - os_client_config/vendors/conoha.json | 13 ++ os_client_config/vendors/conoha.yaml | 8 - os_client_config/vendors/datacentred.json | 10 + os_client_config/vendors/datacentred.yaml | 6 - os_client_config/vendors/dreamhost.json | 10 + os_client_config/vendors/dreamhost.yaml | 6 - os_client_config/vendors/elastx.json | 9 + os_client_config/vendors/elastx.yaml | 5 - os_client_config/vendors/entercloudsuite.json | 13 ++ os_client_config/vendors/entercloudsuite.yaml | 8 - os_client_config/vendors/hp.json | 14 ++ os_client_config/vendors/hp.yaml | 9 - os_client_config/vendors/internap.json | 15 ++ os_client_config/vendors/internap.yaml | 10 - os_client_config/vendors/ovh.json | 14 ++ os_client_config/vendors/ovh.yaml | 9 - os_client_config/vendors/rackspace.json | 27 +++ os_client_config/vendors/rackspace.yaml | 21 -- os_client_config/vendors/runabove.json | 14 ++ os_client_config/vendors/runabove.yaml | 9 - os_client_config/vendors/switchengines.json | 14 ++ os_client_config/vendors/switchengines.yaml | 9 - os_client_config/vendors/ultimum.json | 9 + os_client_config/vendors/ultimum.yaml | 5 - os_client_config/vendors/unitedstack.json | 15 ++ os_client_config/vendors/unitedstack.yaml | 10 - os_client_config/vendors/vexxhost.json | 10 + os_client_config/vendors/vexxhost.yaml | 6 - test-requirements.txt | 1 + 44 files changed, 661 insertions(+), 174 deletions(-) create mode 100644 os_client_config/defaults.json delete mode 100644 os_client_config/defaults.yaml create mode 100644 os_client_config/schema.json create mode 100644 os_client_config/tests/test_json.py create mode 100644 os_client_config/vendor-schema.json create mode 100644 os_client_config/vendors/auro.json delete mode 100644 os_client_config/vendors/auro.yaml create mode 100644 os_client_config/vendors/bluebox.json create mode 100644 os_client_config/vendors/catalyst.json delete mode 100644 os_client_config/vendors/catalyst.yaml create mode 100644 os_client_config/vendors/citycloud.json delete mode 100644 os_client_config/vendors/citycloud.yaml create mode 100644 os_client_config/vendors/conoha.json delete mode 100644 os_client_config/vendors/conoha.yaml create mode 100644 os_client_config/vendors/datacentred.json delete mode 100644 os_client_config/vendors/datacentred.yaml create mode 100644 os_client_config/vendors/dreamhost.json delete mode 100644 os_client_config/vendors/dreamhost.yaml create mode 100644 os_client_config/vendors/elastx.json delete mode 100644 os_client_config/vendors/elastx.yaml create mode 100644 os_client_config/vendors/entercloudsuite.json delete mode 100644 os_client_config/vendors/entercloudsuite.yaml create mode 100644 os_client_config/vendors/hp.json delete mode 100644 os_client_config/vendors/hp.yaml create mode 100644 os_client_config/vendors/internap.json delete mode 100644 os_client_config/vendors/internap.yaml create mode 100644 os_client_config/vendors/ovh.json delete mode 100644 os_client_config/vendors/ovh.yaml create mode 100644 os_client_config/vendors/rackspace.json delete mode 100644 os_client_config/vendors/rackspace.yaml create mode 100644 os_client_config/vendors/runabove.json delete mode 100644 os_client_config/vendors/runabove.yaml create mode 100644 os_client_config/vendors/switchengines.json delete mode 100644 os_client_config/vendors/switchengines.yaml create mode 100644 os_client_config/vendors/ultimum.json delete mode 100644 os_client_config/vendors/ultimum.yaml create mode 100644 os_client_config/vendors/unitedstack.json delete mode 100644 os_client_config/vendors/unitedstack.yaml create mode 100644 os_client_config/vendors/vexxhost.json delete mode 100644 os_client_config/vendors/vexxhost.yaml diff --git a/os_client_config/config.py b/os_client_config/config.py index 92eb15d64..af9dc3b48 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -13,6 +13,7 @@ # under the License. +import json import os import warnings @@ -44,15 +45,16 @@ SITE_CONFIG_HOME, UNIX_SITE_CONFIG_HOME ] YAML_SUFFIXES = ('.yaml', '.yml') +JSON_SUFFIXES = ('.json',) CONFIG_FILES = [ os.path.join(d, 'clouds' + s) for d in CONFIG_SEARCH_PATH - for s in YAML_SUFFIXES + for s in YAML_SUFFIXES + JSON_SUFFIXES ] VENDOR_FILES = [ os.path.join(d, 'clouds-public' + s) for d in CONFIG_SEARCH_PATH - for s in YAML_SUFFIXES + for s in YAML_SUFFIXES + JSON_SUFFIXES ] BOOL_KEYS = ('insecure', 'cache') @@ -212,16 +214,19 @@ def __init__(self, config_files=None, vendor_files=None, 'expiration', self._cache_expiration) def _load_config_file(self): - return self._load_yaml_file(self._config_files) + return self._load_yaml_json_file(self._config_files) def _load_vendor_file(self): - return self._load_yaml_file(self._vendor_files) + return self._load_yaml_json_file(self._vendor_files) - def _load_yaml_file(self, filelist): + def _load_yaml_json_file(self, filelist): for path in filelist: if os.path.exists(path): with open(path, 'r') as f: - return path, yaml.safe_load(f) + if path.endswith('json'): + return path, json.load(f) + else: + return path, yaml.safe_load(f) return (None, None) def _normalize_keys(self, config): diff --git a/os_client_config/defaults.json b/os_client_config/defaults.json new file mode 100644 index 000000000..9239d0fe8 --- /dev/null +++ b/os_client_config/defaults.json @@ -0,0 +1,19 @@ +{ + "auth_type": "password", + "baremetal_api_version": "1", + "compute_api_version": "2", + "database_api_version": "1.0", + "disable_vendor_agent": {}, + "dns_api_version": "2", + "interface": "public", + "floating_ip_source": "neutron", + "identity_api_version": "2.0", + "image_api_use_tasks": false, + "image_api_version": "2", + "image_format": "qcow2", + "network_api_version": "2", + "object_api_version": "1", + "orchestration_api_version": "1", + "secgroup_source": "neutron", + "volume_api_version": "1" +} diff --git a/os_client_config/defaults.py b/os_client_config/defaults.py index 897cff568..c10358a10 100644 --- a/os_client_config/defaults.py +++ b/os_client_config/defaults.py @@ -12,12 +12,11 @@ # License for the specific language governing permissions and limitations # under the License. +import json import os -import yaml - -_yaml_path = os.path.join( - os.path.dirname(os.path.realpath(__file__)), 'defaults.yaml') +_json_path = os.path.join( + os.path.dirname(os.path.realpath(__file__)), 'defaults.json') _defaults = None @@ -34,8 +33,8 @@ def get_defaults(): cert=None, key=None, ) - with open(_yaml_path, 'r') as yaml_file: - updates = yaml.load(yaml_file.read()) + with open(_json_path, 'r') as json_file: + updates = json.load(json_file) if updates is not None: _defaults.update(updates) diff --git a/os_client_config/defaults.yaml b/os_client_config/defaults.yaml deleted file mode 100644 index ddb975cf0..000000000 --- a/os_client_config/defaults.yaml +++ /dev/null @@ -1,17 +0,0 @@ -auth_type: password -baremetal_api_version: '1' -compute_api_version: '2' -database_api_version: '1.0' -disable_vendor_agent: {} -dns_api_version: '2' -interface: public -floating_ip_source: neutron -identity_api_version: '2.0' -image_api_use_tasks: false -image_api_version: '2' -image_format: qcow2 -network_api_version: '2' -object_api_version: '1' -orchestration_api_version: '1' -secgroup_source: neutron -volume_api_version: '1' diff --git a/os_client_config/schema.json b/os_client_config/schema.json new file mode 100644 index 000000000..dfd1f4a9b --- /dev/null +++ b/os_client_config/schema.json @@ -0,0 +1,121 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "https://git.openstack.org/cgit/openstack/cloud-data/plain/schema.json#", + "type": "object", + "properties": { + "auth_type": { + "name": "Auth Type", + "description": "Name of authentication plugin to be used", + "default": "password", + "type": "string" + }, + "disable_vendor_agent": { + "name": "Disable Vendor Agent Properties", + "description": "Image properties required to disable vendor agent", + "type": "object", + "properties": {} + }, + "floating_ip_source": { + "name": "Floating IP Source", + "description": "Which service provides Floating IPs", + "enum": [ "neutron", "nova", "None" ], + "default": "neutron" + }, + "image_api_use_tasks": { + "name": "Image Task API", + "description": "Does the cloud require the Image Task API", + "default": false, + "type": "boolean" + }, + "image_format": { + "name": "Image Format", + "description": "Format for uploaded Images", + "default": "qcow2", + "type": "string" + }, + "interface": { + "name": "API Interface", + "description": "Which API Interface should connections hit", + "default": "public", + "enum": [ "public", "internal", "admin" ] + }, + "secgroup_source": { + "name": "Security Group Source", + "description": "Which service provides security groups", + "default": "neutron", + "enum": [ "neutron", "nova", "None" ] + }, + "baremetal_api_version": { + "name": "Baremetal API Service Type", + "description": "Baremetal API Service Type", + "default": "1", + "type": "string" + }, + "compute_api_version": { + "name": "Compute API Version", + "description": "Compute API Version", + "default": "2", + "type": "string" + }, + "database_api_version": { + "name": "Database API Version", + "description": "Database API Version", + "default": "1.0", + "type": "string" + }, + "dns_api_version": { + "name": "DNS API Version", + "description": "DNS API Version", + "default": "2", + "type": "string" + }, + "identity_api_version": { + "name": "Identity API Version", + "description": "Identity API Version", + "default": "2", + "type": "string" + }, + "image_api_version": { + "name": "Image API Version", + "description": "Image API Version", + "default": "1", + "type": "string" + }, + "network_api_version": { + "name": "Network API Version", + "description": "Network API Version", + "default": "2", + "type": "string" + }, + "object_api_version": { + "name": "Object Storage API Version", + "description": "Object Storage API Version", + "default": "1", + "type": "string" + }, + "volume_api_version": { + "name": "Volume API Version", + "description": "Volume API Version", + "default": "2", + "type": "string" + } + }, + "required": [ + "auth_type", + "baremetal_api_version", + "compute_api_version", + "database_api_version", + "disable_vendor_agent", + "dns_api_version", + "floating_ip_source", + "identity_api_version", + "image_api_use_tasks", + "image_api_version", + "image_format", + "interface", + "network_api_version", + "object_api_version", + "secgroup_source", + "volume_api_version" + ] +} diff --git a/os_client_config/tests/test_json.py b/os_client_config/tests/test_json.py new file mode 100644 index 000000000..f618f3b29 --- /dev/null +++ b/os_client_config/tests/test_json.py @@ -0,0 +1,62 @@ +# Copyright (c) 2015 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. + +import glob +import json +import os + +import jsonschema +from testtools import content + +from os_client_config import defaults +from os_client_config.tests import base + + +class TestConfig(base.TestCase): + + def json_diagnostics(self, exc_info): + self.addDetail('filename', content.text_content(self.filename)) + for error in sorted(self.validator.iter_errors(self.json_data)): + self.addDetail('jsonschema', content.text_content(str(error))) + + def test_defaults_valid_json(self): + _schema_path = os.path.join( + os.path.dirname(os.path.realpath(defaults.__file__)), + 'schema.json') + schema = json.load(open(_schema_path, 'r')) + self.validator = jsonschema.Draft4Validator(schema) + self.addOnException(self.json_diagnostics) + + self.filename = os.path.join( + os.path.dirname(os.path.realpath(defaults.__file__)), + 'defaults.json') + self.json_data = json.load(open(self.filename, 'r')) + + self.assertTrue(self.validator.is_valid(self.json_data)) + + def test_vendors_valid_json(self): + _schema_path = os.path.join( + os.path.dirname(os.path.realpath(defaults.__file__)), + 'vendor-schema.json') + schema = json.load(open(_schema_path, 'r')) + self.validator = jsonschema.Draft4Validator(schema) + self.addOnException(self.json_diagnostics) + + _vendors_path = os.path.join( + os.path.dirname(os.path.realpath(defaults.__file__)), + 'vendors') + for self.filename in glob.glob(os.path.join(_vendors_path, '*.json')): + self.json_data = json.load(open(self.filename, 'r')) + + self.assertTrue(self.validator.is_valid(self.json_data)) diff --git a/os_client_config/vendor-schema.json b/os_client_config/vendor-schema.json new file mode 100644 index 000000000..e3fb57359 --- /dev/null +++ b/os_client_config/vendor-schema.json @@ -0,0 +1,206 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "https://git.openstack.org/cgit/openstack/cloud-data/plain/vendor-schema.json#", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "profile": { + "type": "object", + "properties": { + "auth": { + "type": "object", + "properties": { + "auth_url": { + "name": "Auth URL", + "description": "URL of the primary Keystone endpoint", + "type": "string" + } + } + }, + "auth_type": { + "name": "Auth Type", + "description": "Name of authentication plugin to be used", + "default": "password", + "type": "string" + }, + "disable_vendor_agent": { + "name": "Disable Vendor Agent Properties", + "description": "Image properties required to disable vendor agent", + "type": "object", + "properties": {} + }, + "floating_ip_source": { + "name": "Floating IP Source", + "description": "Which service provides Floating IPs", + "enum": [ "neutron", "nova", "None" ], + "default": "neutron" + }, + "image_api_use_tasks": { + "name": "Image Task API", + "description": "Does the cloud require the Image Task API", + "default": false, + "type": "boolean" + }, + "image_format": { + "name": "Image Format", + "description": "Format for uploaded Images", + "default": "qcow2", + "type": "string" + }, + "interface": { + "name": "API Interface", + "description": "Which API Interface should connections hit", + "default": "public", + "enum": [ "public", "internal", "admin" ] + }, + "secgroup_source": { + "name": "Security Group Source", + "description": "Which service provides security groups", + "enum": [ "neutron", "nova", "None" ], + "default": "neutron" + }, + "compute_api_service_name": { + "name": "Compute API Service Name", + "description": "Compute API Service Name", + "type": "string" + }, + "database_api_service_name": { + "name": "Database API Service Name", + "description": "Database API Service Name", + "type": "string" + }, + "dns_api_service_name": { + "name": "DNS API Service Name", + "description": "DNS API Service Name", + "type": "string" + }, + "identity_api_service_name": { + "name": "Identity API Service Name", + "description": "Identity API Service Name", + "type": "string" + }, + "image_api_service_name": { + "name": "Image API Service Name", + "description": "Image API Service Name", + "type": "string" + }, + "volume_api_service_name": { + "name": "Volume API Service Name", + "description": "Volume API Service Name", + "type": "string" + }, + "network_api_service_name": { + "name": "Network API Service Name", + "description": "Network API Service Name", + "type": "string" + }, + "object_api_service_name": { + "name": "Object Storage API Service Name", + "description": "Object Storage API Service Name", + "type": "string" + }, + "baremetal_api_service_name": { + "name": "Baremetal API Service Name", + "description": "Baremetal API Service Name", + "type": "string" + }, + "compute_api_service_type": { + "name": "Compute API Service Type", + "description": "Compute API Service Type", + "type": "string" + }, + "database_api_service_type": { + "name": "Database API Service Type", + "description": "Database API Service Type", + "type": "string" + }, + "dns_api_service_type": { + "name": "DNS API Service Type", + "description": "DNS API Service Type", + "type": "string" + }, + "identity_api_service_type": { + "name": "Identity API Service Type", + "description": "Identity API Service Type", + "type": "string" + }, + "image_api_service_type": { + "name": "Image API Service Type", + "description": "Image API Service Type", + "type": "string" + }, + "volume_api_service_type": { + "name": "Volume API Service Type", + "description": "Volume API Service Type", + "type": "string" + }, + "network_api_service_type": { + "name": "Network API Service Type", + "description": "Network API Service Type", + "type": "string" + }, + "object_api_service_type": { + "name": "Object Storage API Service Type", + "description": "Object Storage API Service Type", + "type": "string" + }, + "baremetal_api_version": { + "name": "Baremetal API Service Type", + "description": "Baremetal API Service Type", + "type": "string" + }, + "compute_api_version": { + "name": "Compute API Version", + "description": "Compute API Version", + "type": "string" + }, + "database_api_version": { + "name": "Database API Version", + "description": "Database API Version", + "type": "string" + }, + "dns_api_version": { + "name": "DNS API Version", + "description": "DNS API Version", + "type": "string" + }, + "identity_api_version": { + "name": "Identity API Version", + "description": "Identity API Version", + "type": "string" + }, + "image_api_version": { + "name": "Image API Version", + "description": "Image API Version", + "type": "string" + }, + "volume_api_version": { + "name": "Volume API Version", + "description": "Volume API Version", + "type": "string" + }, + "network_api_version": { + "name": "Network API Version", + "description": "Network API Version", + "type": "string" + }, + "object_api_version": { + "name": "Object Storage API Version", + "description": "Object Storage API Version", + "type": "string" + }, + "baremetal_api_version": { + "name": "Baremetal API Version", + "description": "Baremetal API Version", + "type": "string" + } + } + } + }, + "required": [ + "name", + "profile" + ] +} diff --git a/os_client_config/vendors/__init__.py b/os_client_config/vendors/__init__.py index 367e3182b..3e1d20a5a 100644 --- a/os_client_config/vendors/__init__.py +++ b/os_client_config/vendors/__init__.py @@ -13,6 +13,7 @@ # under the License. import glob +import json import os import yaml @@ -27,6 +28,10 @@ def get_profile(profile_name): _vendor_defaults = {} for vendor in glob.glob(os.path.join(_vendors_path, '*.yaml')): with open(vendor, 'r') as f: - vendor_data = yaml.load(f) + vendor_data = yaml.safe_load(f) + _vendor_defaults[vendor_data['name']] = vendor_data['profile'] + for vendor in glob.glob(os.path.join(_vendors_path, '*.json')): + with open(vendor, 'r') as f: + vendor_data = json.load(f) _vendor_defaults[vendor_data['name']] = vendor_data['profile'] return _vendor_defaults.get(profile_name) diff --git a/os_client_config/vendors/auro.json b/os_client_config/vendors/auro.json new file mode 100644 index 000000000..1e59f01d4 --- /dev/null +++ b/os_client_config/vendors/auro.json @@ -0,0 +1,9 @@ +{ + "name": "auro", + "profile": { + "auth": { + "auth_url": "https://api.van1.auro.io:5000/v2.0" + }, + "region_name": "van1" + } +} diff --git a/os_client_config/vendors/auro.yaml b/os_client_config/vendors/auro.yaml deleted file mode 100644 index 094ab7479..000000000 --- a/os_client_config/vendors/auro.yaml +++ /dev/null @@ -1,5 +0,0 @@ -name: auro -profile: - auth: - auth_url: https://api.van1.auro.io:5000/v2.0 - region_name: van1 diff --git a/os_client_config/vendors/bluebox.json b/os_client_config/vendors/bluebox.json new file mode 100644 index 000000000..2227aac81 --- /dev/null +++ b/os_client_config/vendors/bluebox.json @@ -0,0 +1,6 @@ +{ + "name": "bluebox", + "profile": { + "region_name": "RegionOne" + } +} diff --git a/os_client_config/vendors/catalyst.json b/os_client_config/vendors/catalyst.json new file mode 100644 index 000000000..ddde83825 --- /dev/null +++ b/os_client_config/vendors/catalyst.json @@ -0,0 +1,14 @@ +{ + "name": "catalyst", + "profile": { + "auth": { + "auth_url": "https://api.cloud.catalyst.net.nz:5000/v2.0" + }, + "regions": [ + "nz-por-1", + "nz_wlg_2" + ], + "image_api_version": "1", + "image_format": "raw" + } +} diff --git a/os_client_config/vendors/catalyst.yaml b/os_client_config/vendors/catalyst.yaml deleted file mode 100644 index d6251930d..000000000 --- a/os_client_config/vendors/catalyst.yaml +++ /dev/null @@ -1,9 +0,0 @@ -name: catalyst -profile: - auth: - auth_url: https://api.cloud.catalyst.net.nz:5000/v2.0 - regions: - - nz-por-1 - - nz_wlg_2 - image_api_version: '1' - image_format: raw diff --git a/os_client_config/vendors/citycloud.json b/os_client_config/vendors/citycloud.json new file mode 100644 index 000000000..f6c57c7b5 --- /dev/null +++ b/os_client_config/vendors/citycloud.json @@ -0,0 +1,14 @@ +{ + "name": "citycloud", + "profile": { + "auth": { + "auth_url": "https://identity1.citycloud.com:5000/v3/" + }, + "regions": [ + "Lon1", + "Sto2", + "Kna1" + ], + "identity_api_version": "3" + } +} diff --git a/os_client_config/vendors/citycloud.yaml b/os_client_config/vendors/citycloud.yaml deleted file mode 100644 index 9ee0b9b4f..000000000 --- a/os_client_config/vendors/citycloud.yaml +++ /dev/null @@ -1,9 +0,0 @@ -name: citycloud -profile: - auth: - auth_url: https://identity1.citycloud.com:5000/v3/ - regions: - - Lon1 - - Sto2 - - Kna1 - identity_api_version: '3' diff --git a/os_client_config/vendors/conoha.json b/os_client_config/vendors/conoha.json new file mode 100644 index 000000000..28f1b2754 --- /dev/null +++ b/os_client_config/vendors/conoha.json @@ -0,0 +1,13 @@ +{ + "name": "conoha", + "profile": { + "auth": { + "auth_url": "https://identity.{region_name}.conoha.io/v2.0" + }, + "regions": [ + "sin1", + "lon1", + "tyo1" + ] + } +} diff --git a/os_client_config/vendors/conoha.yaml b/os_client_config/vendors/conoha.yaml deleted file mode 100644 index 1ed4063ec..000000000 --- a/os_client_config/vendors/conoha.yaml +++ /dev/null @@ -1,8 +0,0 @@ -name: conoha -profile: - auth: - auth_url: https://identity.{region_name}.conoha.io/v2.0 - regions: - - sin1 - - lon1 - - tyo1 diff --git a/os_client_config/vendors/datacentred.json b/os_client_config/vendors/datacentred.json new file mode 100644 index 000000000..1fb4dbb09 --- /dev/null +++ b/os_client_config/vendors/datacentred.json @@ -0,0 +1,10 @@ +{ + "name": "datacentred", + "profile": { + "auth": { + "auth_url": "https://compute.datacentred.io:5000/v2.0" + }, + "region-name": "sal01", + "image_api_version": "1" + } +} diff --git a/os_client_config/vendors/datacentred.yaml b/os_client_config/vendors/datacentred.yaml deleted file mode 100644 index 5c0a5ed1a..000000000 --- a/os_client_config/vendors/datacentred.yaml +++ /dev/null @@ -1,6 +0,0 @@ -name: datacentred -profile: - auth: - auth_url: https://compute.datacentred.io:5000/v2.0 - region-name: sal01 - image_api_version: '1' diff --git a/os_client_config/vendors/dreamhost.json b/os_client_config/vendors/dreamhost.json new file mode 100644 index 000000000..8580826e1 --- /dev/null +++ b/os_client_config/vendors/dreamhost.json @@ -0,0 +1,10 @@ +{ + "name": "dreamhost", + "profile": { + "auth": { + "auth_url": "https://keystone.dream.io/v2.0" + }, + "region_name": "RegionOne", + "image_format": "raw" + } +} diff --git a/os_client_config/vendors/dreamhost.yaml b/os_client_config/vendors/dreamhost.yaml deleted file mode 100644 index 3cd395a17..000000000 --- a/os_client_config/vendors/dreamhost.yaml +++ /dev/null @@ -1,6 +0,0 @@ -name: dreamhost -profile: - auth: - auth_url: https://keystone.dream.io/v2.0 - region_name: RegionOne - image_format: raw diff --git a/os_client_config/vendors/elastx.json b/os_client_config/vendors/elastx.json new file mode 100644 index 000000000..cac755e8f --- /dev/null +++ b/os_client_config/vendors/elastx.json @@ -0,0 +1,9 @@ +{ + "name": "elastx", + "profile": { + "auth": { + "auth_url": "https://ops.elastx.net:5000/v2.0" + }, + "region_name": "regionOne" + } +} diff --git a/os_client_config/vendors/elastx.yaml b/os_client_config/vendors/elastx.yaml deleted file mode 100644 index 810e12ede..000000000 --- a/os_client_config/vendors/elastx.yaml +++ /dev/null @@ -1,5 +0,0 @@ -name: elastx -profile: - auth: - auth_url: https://ops.elastx.net:5000/v2.0 - region_name: regionOne diff --git a/os_client_config/vendors/entercloudsuite.json b/os_client_config/vendors/entercloudsuite.json new file mode 100644 index 000000000..826c25f7f --- /dev/null +++ b/os_client_config/vendors/entercloudsuite.json @@ -0,0 +1,13 @@ +{ + "name": "entercloudsuite", + "profile": { + "auth": { + "auth_url": "https://api.entercloudsuite.com/v2.0" + }, + "regions": [ + "it-mil1", + "nl-ams1", + "de-fra1" + ] + } +} diff --git a/os_client_config/vendors/entercloudsuite.yaml b/os_client_config/vendors/entercloudsuite.yaml deleted file mode 100644 index f68bcf674..000000000 --- a/os_client_config/vendors/entercloudsuite.yaml +++ /dev/null @@ -1,8 +0,0 @@ -name: entercloudsuite -profile: - auth: - auth_url: https://api.entercloudsuite.com/v2.0 - regions: - - it-mil1 - - nl-ams1 - - de-fra1 diff --git a/os_client_config/vendors/hp.json b/os_client_config/vendors/hp.json new file mode 100644 index 000000000..10789a91b --- /dev/null +++ b/os_client_config/vendors/hp.json @@ -0,0 +1,14 @@ +{ + "name": "hp", + "profile": { + "auth": { + "auth_url": "https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0" + }, + "regions": [ + "region-a.geo-1", + "region-b.geo-1" + ], + "dns_service_type": "hpext:dns", + "image_api_version": "1" + } +} diff --git a/os_client_config/vendors/hp.yaml b/os_client_config/vendors/hp.yaml deleted file mode 100644 index a0544df82..000000000 --- a/os_client_config/vendors/hp.yaml +++ /dev/null @@ -1,9 +0,0 @@ -name: hp -profile: - auth: - auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0 - regions: - - region-a.geo-1 - - region-b.geo-1 - dns_service_type: hpext:dns - image_api_version: '1' diff --git a/os_client_config/vendors/internap.json b/os_client_config/vendors/internap.json new file mode 100644 index 000000000..9b27536d5 --- /dev/null +++ b/os_client_config/vendors/internap.json @@ -0,0 +1,15 @@ +{ + "name": "internap", + "profile": { + "auth": { + "auth_url": "https://identity.api.cloud.iweb.com/v2.0" + }, + "regions": [ + "ams01", + "da01", + "nyj01" + ], + "image_api_version": "1", + "floating_ip_source": "None" + } +} diff --git a/os_client_config/vendors/internap.yaml b/os_client_config/vendors/internap.yaml deleted file mode 100644 index 48cd960eb..000000000 --- a/os_client_config/vendors/internap.yaml +++ /dev/null @@ -1,10 +0,0 @@ -name: internap -profile: - auth: - auth_url: https://identity.api.cloud.iweb.com/v2.0 - regions: - - ams01 - - da01 - - nyj01 - image_api_version: '1' - floating_ip_source: None diff --git a/os_client_config/vendors/ovh.json b/os_client_config/vendors/ovh.json new file mode 100644 index 000000000..cfd234b64 --- /dev/null +++ b/os_client_config/vendors/ovh.json @@ -0,0 +1,14 @@ +{ + "name": "ovh", + "profile": { + "auth": { + "auth_url": "https://auth.cloud.ovh.net/v2.0" + }, + "regions": [ + "GRA1", + "SBG1" + ], + "image_format": "raw", + "floating_ip_source": "None" + } +} diff --git a/os_client_config/vendors/ovh.yaml b/os_client_config/vendors/ovh.yaml deleted file mode 100644 index 52b91a466..000000000 --- a/os_client_config/vendors/ovh.yaml +++ /dev/null @@ -1,9 +0,0 @@ -name: ovh -profile: - auth: - auth_url: https://auth.cloud.ovh.net/v2.0 - regions: - - GRA1 - - SBG1 - image_format: raw - floating_ip_source: None diff --git a/os_client_config/vendors/rackspace.json b/os_client_config/vendors/rackspace.json new file mode 100644 index 000000000..582e1225c --- /dev/null +++ b/os_client_config/vendors/rackspace.json @@ -0,0 +1,27 @@ +{ + "name": "rackspace", + "profile": { + "auth": { + "auth_url": "https://identity.api.rackspacecloud.com/v2.0/" + }, + "regions": [ + "DFW", + "HKG", + "IAD", + "ORD", + "SYD", + "LON" + ], + "database_service_type": "rax:database", + "compute_service_name": "cloudServersOpenStack", + "image_api_use_tasks": true, + "image_format": "vhd", + "floating_ip_source": "None", + "secgroup_source": "None", + "disable_vendor_agent": { + "vm_mode": "hvm", + "xenapi_use_agent": false + }, + "has_network": false + } +} diff --git a/os_client_config/vendors/rackspace.yaml b/os_client_config/vendors/rackspace.yaml deleted file mode 100644 index a28d49366..000000000 --- a/os_client_config/vendors/rackspace.yaml +++ /dev/null @@ -1,21 +0,0 @@ -name: rackspace -profile: - auth: - auth_url: https://identity.api.rackspacecloud.com/v2.0/ - regions: - - DFW - - HKG - - IAD - - ORD - - SYD - - LON - database_service_type: rax:database - compute_service_name: cloudServersOpenStack - image_api_use_tasks: true - image_format: vhd - floating_ip_source: None - secgroup_source: None - disable_vendor_agent: - vm_mode: hvm - xenapi_use_agent: false - has_network: false diff --git a/os_client_config/vendors/runabove.json b/os_client_config/vendors/runabove.json new file mode 100644 index 000000000..56dd9453c --- /dev/null +++ b/os_client_config/vendors/runabove.json @@ -0,0 +1,14 @@ +{ + "name": "runabove", + "profile": { + "auth": { + "auth_url": "https://auth.runabove.io/v2.0" + }, + "regions": [ + "BHS-1", + "SBG-1" + ], + "image_format": "qcow2", + "floating_ip_source": "None" + } +} diff --git a/os_client_config/vendors/runabove.yaml b/os_client_config/vendors/runabove.yaml deleted file mode 100644 index 34528941a..000000000 --- a/os_client_config/vendors/runabove.yaml +++ /dev/null @@ -1,9 +0,0 @@ -name: runabove -profile: - auth: - auth_url: https://auth.runabove.io/v2.0 - regions: - - BHS-1 - - SBG-1 - image_format: qcow2 - floating_ip_source: None diff --git a/os_client_config/vendors/switchengines.json b/os_client_config/vendors/switchengines.json new file mode 100644 index 000000000..8a7c566b8 --- /dev/null +++ b/os_client_config/vendors/switchengines.json @@ -0,0 +1,14 @@ +{ + "name": "switchengines", + "profile": { + "auth": { + "auth_url": "https://keystone.cloud.switch.ch:5000/v2.0" + }, + "regions": [ + "LS", + "ZH" + ], + "image_api_use_tasks": true, + "image_format": "raw" + } +} diff --git a/os_client_config/vendors/switchengines.yaml b/os_client_config/vendors/switchengines.yaml deleted file mode 100644 index ff6c50517..000000000 --- a/os_client_config/vendors/switchengines.yaml +++ /dev/null @@ -1,9 +0,0 @@ -name: switchengines -profile: - auth: - auth_url: https://keystone.cloud.switch.ch:5000/v2.0 - regions: - - LS - - ZH - image_api_use_tasks: true - image_format: raw diff --git a/os_client_config/vendors/ultimum.json b/os_client_config/vendors/ultimum.json new file mode 100644 index 000000000..ada6e3de7 --- /dev/null +++ b/os_client_config/vendors/ultimum.json @@ -0,0 +1,9 @@ +{ + "name": "ultimum", + "profile": { + "auth": { + "auth_url": "https://console.ultimum-cloud.com:5000/v2.0" + }, + "region-name": "RegionOne" + } +} diff --git a/os_client_config/vendors/ultimum.yaml b/os_client_config/vendors/ultimum.yaml deleted file mode 100644 index 866117491..000000000 --- a/os_client_config/vendors/ultimum.yaml +++ /dev/null @@ -1,5 +0,0 @@ -name: ultimum -profile: - auth: - auth_url: https://console.ultimum-cloud.com:5000/v2.0 - region-name: RegionOne diff --git a/os_client_config/vendors/unitedstack.json b/os_client_config/vendors/unitedstack.json new file mode 100644 index 000000000..41f45851a --- /dev/null +++ b/os_client_config/vendors/unitedstack.json @@ -0,0 +1,15 @@ +{ + "name": "unitedstack", + "profile": { + "auth": { + "auth_url": "https://identity.api.ustack.com/v3" + }, + "regions": [ + "bj1", + "gd1" + ], + "identity_api_version": "3", + "image_format": "raw", + "floating_ip_source": "None" + } +} diff --git a/os_client_config/vendors/unitedstack.yaml b/os_client_config/vendors/unitedstack.yaml deleted file mode 100644 index c6d5cc20e..000000000 --- a/os_client_config/vendors/unitedstack.yaml +++ /dev/null @@ -1,10 +0,0 @@ -name: unitedstack -profile: - auth: - auth_url: https://identity.api.ustack.com/v3 - regions: - - bj1 - - gd1 - identity_api_version: '3' - image_format: raw - floating_ip_source: None diff --git a/os_client_config/vendors/vexxhost.json b/os_client_config/vendors/vexxhost.json new file mode 100644 index 000000000..25911cae1 --- /dev/null +++ b/os_client_config/vendors/vexxhost.json @@ -0,0 +1,10 @@ +{ + "name": "vexxhost", + "profile": { + "auth": { + "auth_url": "http://auth.api.thenebulacloud.com:5000/v2.0/" + }, + "region_name": "ca-ymq-1", + "floating_ip_source": "None" + } +} diff --git a/os_client_config/vendors/vexxhost.yaml b/os_client_config/vendors/vexxhost.yaml deleted file mode 100644 index 4a0ba271b..000000000 --- a/os_client_config/vendors/vexxhost.yaml +++ /dev/null @@ -1,6 +0,0 @@ -name: vexxhost -profile: - auth: - auth_url: http://auth.api.thenebulacloud.com:5000/v2.0/ - region_name: ca-ymq-1 - floating_ip_source: None diff --git a/test-requirements.txt b/test-requirements.txt index 62f3e8831..9a02b042c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,6 +8,7 @@ coverage>=3.6 extras fixtures>=0.3.14 discover +jsonschema>=2.0.0,<3.0.0,!=2.5.0 python-keystoneclient>=1.1.0 python-subunit>=0.0.18 sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 From 27678e5d948296986c31705c8405f569d2c06553 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 3 Nov 2015 08:11:53 -0500 Subject: [PATCH 0610/3836] Update conoha's vendor profile to include SJC Turns out the lon region is not really a thing. Change-Id: Ib315c301d5f8b589006a61d2a255a6b295b1b9a5 --- doc/source/vendor-support.rst | 2 +- os_client_config/vendors/conoha.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index c3d4b3eed..a9dd5ef8c 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -78,7 +78,7 @@ Region Name Human Name ============== ================ tyo1 Tokyo, JP sin1 Singapore -lon1 London, UK +sjc1 San Jose, CA ============== ================ * Image upload is not supported diff --git a/os_client_config/vendors/conoha.json b/os_client_config/vendors/conoha.json index 28f1b2754..8e33ca41b 100644 --- a/os_client_config/vendors/conoha.json +++ b/os_client_config/vendors/conoha.json @@ -6,7 +6,7 @@ }, "regions": [ "sin1", - "lon1", + "sjc1", "tyo1" ] } From a494b31b85cf320c891260e837a69867348eca78 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 26 Oct 2015 10:31:15 +0900 Subject: [PATCH 0611/3836] Add methods for getting Session and Client objects These come originally from the shade library, but are helpful for things like the client libs themselves. Once one has a CloudConfig, there is really one and only one correct way to get both a session and a Client. Change-Id: I1b4d4321828864fddab85a127fbf63f4c8384ab9 --- os_client_config/cloud_config.py | 139 ++++++++++++++++++++ os_client_config/tests/test_cloud_config.py | 135 +++++++++++++++++++ test-requirements.txt | 2 + 3 files changed, 276 insertions(+) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 63ff8d29b..6085a6496 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -14,6 +14,11 @@ import warnings +from keystoneauth1 import plugin +from keystoneauth1 import session + +from os_client_config import exceptions + class CloudConfig(object): def __init__(self, name, region, config, @@ -25,6 +30,7 @@ def __init__(self, name, region, config, self._force_ipv4 = force_ipv4 self._auth = auth_plugin self._openstack_config = openstack_config + self._keystone_session = None def __getattr__(self, key): """Return arbitrary attributes.""" @@ -119,6 +125,139 @@ def get_auth(self): """Return a keystoneauth plugin from the auth credentials.""" return self._auth + def get_session(self): + """Return a keystoneauth session based on the auth credentials.""" + if self._keystone_session is None: + if not self._auth: + raise exceptions.OpenStackConfigException( + "Problem with auth parameters") + (verify, cert) = self.get_requests_verify_args() + self._keystone_session = session.Session( + auth=self._auth, + verify=verify, + cert=cert, + timeout=self.config['api_timeout']) + return self._keystone_session + + def get_session_endpoint(self, service_key): + """Return the endpoint from config or the catalog. + + If a configuration lists an explicit endpoint for a service, + return that. Otherwise, fetch the service catalog from the + keystone session and return the appropriate endpoint. + + :param service_key: Generic key for service, such as 'compute' or + 'network' + + :returns: Endpoint for the service, or None if not found + """ + + override_endpoint = self.get_endpoint(service_key) + if override_endpoint: + return override_endpoint + # keystone is a special case in keystone, because what? + session = self.get_session() + if service_key == 'identity': + endpoint = session.get_endpoint(interface=plugin.AUTH_INTERFACE) + else: + endpoint = session.get_endpoint( + service_type=self.get_service_type(service_key), + service_name=self.get_service_name(service_key), + interface=self.get_interface(service_key), + region_name=self.region) + return endpoint + + def get_legacy_client( + self, service_key, client_class, interface_key=None, + pass_version_arg=True, **kwargs): + """Return a legacy OpenStack client object for the given config. + + Most of the OpenStack python-*client libraries have the same + interface for their client constructors, but there are several + parameters one wants to pass given a :class:`CloudConfig` object. + + In the future, OpenStack API consumption should be done through + the OpenStack SDK, but that's not ready yet. This is for getting + Client objects from python-*client only. + + :param service_key: Generic key for service, such as 'compute' or + 'network' + :param client_class: Class of the client to be instantiated. This + should be the unversioned version if there + is one, such as novaclient.client.Client, or + the versioned one, such as + neutronclient.v2_0.client.Client if there isn't + :param interface_key: (optional) Some clients, such as glanceclient + only accept the parameter 'interface' instead + of 'endpoint_type' - this is a get-out-of-jail + parameter for those until they can be aligned. + os-client-config understands this to be the + case if service_key is image, so this is really + only for use with other unknown broken clients. + :param pass_version_arg: (optional) If a versioned Client constructor + was passed to client_class, set this to + False, which will tell get_client to not + pass a version parameter. os-client-config + already understand that this is the + case for network, so it can be omitted in + that case. + :param kwargs: (optional) keyword args are passed through to the + Client constructor, so this is in case anything + additional needs to be passed in. + """ + # Because of course swift is different + if service_key == 'object-store': + return self._get_swift_client(client_class=client_class, **kwargs) + interface = self.get_interface(service_key) + # trigger exception on lack of service + endpoint = self.get_session_endpoint(service_key) + + if not interface_key: + if service_key == 'image': + interface_key = 'interface' + else: + interface_key = 'endpoint_type' + if service_key == 'network': + pass_version_arg = False + + constructor_args = dict( + session=self.get_session(), + service_name=self.get_service_name(service_key), + service_type=self.get_service_type(service_key), + region_name=self.region) + + if service_key == 'image': + # os-client-config does not depend on glanceclient, but if + # the user passed in glanceclient.client.Client, which they + # would need to do if they were requesting 'image' - then + # they necessarily have glanceclient installed + from glanceclient.common import utils as glance_utils + endpoint, version = glance_utils.strip_version(endpoint) + constructor_args['endpoint'] = endpoint + constructor_args.update(kwargs) + constructor_args[interface_key] = interface + if pass_version_arg: + version = self.get_api_version(service_key) + constructor_args['version'] = version + return client_class(**constructor_args) + + def _get_swift_client(self, client_class, **kwargs): + session = self.get_session() + token = session.get_token() + endpoint = self.get_session_endpoint(service_key='object-store') + if not endpoint: + return None + return client_class( + preauthurl=endpoint, + preauthtoken=token, + auth_version=self.get_api_version('identity'), + os_options=dict( + auth_token=token, + object_storage_url=endpoint, + region_name=self.get_region_name()), + timeout=self.api_timeout, + ) + def get_cache_expiration_time(self): if self._openstack_config: return self._openstack_config.get_cache_expiration_time() diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index c9317ad00..f5ceac7b7 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -12,7 +12,13 @@ import copy +from keystoneauth1 import plugin as ksa_plugin +from keystoneauth1 import session as ksa_session +import mock + from os_client_config import cloud_config +from os_client_config import defaults +from os_client_config import exceptions from os_client_config.tests import base @@ -142,3 +148,132 @@ def test_getters(self): cc.get_endpoint('image')) self.assertEqual(None, cc.get_service_name('compute')) self.assertEqual('locks', cc.get_service_name('identity')) + + def test_get_session_no_auth(self): + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + cc = cloud_config.CloudConfig("test1", "region-al", config_dict) + self.assertRaises( + exceptions.OpenStackConfigException, + cc.get_session) + + @mock.patch.object(ksa_session, 'Session') + def test_get_session(self, mock_session): + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + cc = cloud_config.CloudConfig( + "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + cc.get_session() + mock_session.assert_called_with( + auth=mock.ANY, + verify=True, cert=None, timeout=None) + + @mock.patch.object(ksa_session, 'Session') + def test_override_session_endpoint(self, mock_session): + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + cc = cloud_config.CloudConfig( + "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + self.assertEqual( + cc.get_session_endpoint('compute'), + fake_services_dict['compute_endpoint']) + + @mock.patch.object(cloud_config.CloudConfig, 'get_session') + def test_session_endpoint_identity(self, mock_get_session): + mock_session = mock.Mock() + mock_get_session.return_value = mock_session + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + cc = cloud_config.CloudConfig( + "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + cc.get_session_endpoint('identity') + mock_session.get_endpoint.assert_called_with( + interface=ksa_plugin.AUTH_INTERFACE) + + @mock.patch.object(cloud_config.CloudConfig, 'get_session') + def test_session_endpoint(self, mock_get_session): + mock_session = mock.Mock() + mock_get_session.return_value = mock_session + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + cc = cloud_config.CloudConfig( + "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + cc.get_session_endpoint('orchestration') + mock_session.get_endpoint.assert_called_with( + interface='public', + service_name=None, + region_name='region-al', + service_type='orchestration') + + @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + def test_legacy_client_object_store(self, mock_get_session_endpoint): + mock_client = mock.Mock() + mock_get_session_endpoint.return_value = 'http://example.com/v2' + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + cc = cloud_config.CloudConfig( + "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + cc.get_legacy_client('object-store', mock_client) + mock_client.assert_called_with( + preauthtoken=mock.ANY, + os_options={ + 'auth_token': mock.ANY, + 'region_name': 'region-al', + 'object_storage_url': 'http://example.com/v2' + }, + preauthurl='http://example.com/v2', + auth_version='2.0', + timeout=None) + + @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + def test_legacy_client_image(self, mock_get_session_endpoint): + mock_client = mock.Mock() + mock_get_session_endpoint.return_value = 'http://example.com/v2' + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + cc = cloud_config.CloudConfig( + "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + cc.get_legacy_client('image', mock_client) + mock_client.assert_called_with( + version='2', + service_name=None, + endpoint='http://example.com', + region_name='region-al', + interface='public', + session=mock.ANY, + # Not a typo - the config dict above overrides this + service_type='mage' + ) + + @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + def test_legacy_client_network(self, mock_get_session_endpoint): + mock_client = mock.Mock() + mock_get_session_endpoint.return_value = 'http://example.com/v2' + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + cc = cloud_config.CloudConfig( + "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + cc.get_legacy_client('network', mock_client) + mock_client.assert_called_with( + endpoint_type='public', + region_name='region-al', + service_type='network', + session=mock.ANY, + service_name=None) + + @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + def test_legacy_client_compute(self, mock_get_session_endpoint): + mock_client = mock.Mock() + mock_get_session_endpoint.return_value = 'http://example.com/v2' + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + cc = cloud_config.CloudConfig( + "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + cc.get_legacy_client('compute', mock_client) + mock_client.assert_called_with( + version=2, + endpoint_type='public', + region_name='region-al', + service_type='compute', + session=mock.ANY, + service_name=None) diff --git a/test-requirements.txt b/test-requirements.txt index 9a02b042c..70530517e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,6 +9,8 @@ extras fixtures>=0.3.14 discover jsonschema>=2.0.0,<3.0.0,!=2.5.0 +mock>=1.2 +python-glanceclient>=0.18.0 python-keystoneclient>=1.1.0 python-subunit>=0.0.18 sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 From 335ed4a6944ad32759ad810aa6a4da599bc358fd Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 3 Nov 2015 10:45:20 -0500 Subject: [PATCH 0612/3836] Add logging module support The _log.py module is from shade and is just basic logging support that does not emit warnings if the consumer does not have logging enabled. Change-Id: Id4639763cf488eb7cd0c27904be055a7843e287f --- os_client_config/_log.py | 28 ++++++++++++++++++++++++++++ os_client_config/cloud_config.py | 2 ++ 2 files changed, 30 insertions(+) create mode 100644 os_client_config/_log.py diff --git a/os_client_config/_log.py b/os_client_config/_log.py new file mode 100644 index 000000000..ff2f2eac7 --- /dev/null +++ b/os_client_config/_log.py @@ -0,0 +1,28 @@ +# Copyright (c) 2015 IBM Corp. +# +# 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 + + +class NullHandler(logging.Handler): + def emit(self, record): + pass + + +def setup_logging(name): + log = logging.getLogger(name) + if len(log.handlers) == 0: + h = NullHandler() + log.addHandler(h) + return log diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 6085a6496..fe5c3021d 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -17,6 +17,7 @@ from keystoneauth1 import plugin from keystoneauth1 import session +from os_client_config import _log from os_client_config import exceptions @@ -27,6 +28,7 @@ def __init__(self, name, region, config, self.name = name self.region = region self.config = config + self.log = _log.setup_logging(__name__) self._force_ipv4 = force_ipv4 self._auth = auth_plugin self._openstack_config = openstack_config From 6d957b01e55637ad4e8ce69c31cf3b3f64881c57 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 3 Nov 2015 10:42:53 -0500 Subject: [PATCH 0613/3836] Disable spurious urllib warnings Since we can return a keystoneauth1 Session correctly, disable the stupid warnings that are evil and never should be emitted. Change-Id: I128e4587ab07a20c7c5da745b12e4f5d0a3ee5a7 --- os_client_config/cloud_config.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index fe5c3021d..17941c970 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -20,6 +20,15 @@ from os_client_config import _log from os_client_config import exceptions +# Importing these for later but not disabling for now +try: + from requests.packages.urllib3 import exceptions as urllib_exc +except ImportError: + try: + from urllib3 import exceptions as urllib_exc + except ImportError: + urllib_exc = None + class CloudConfig(object): def __init__(self, name, region, config, @@ -134,6 +143,16 @@ def get_session(self): raise exceptions.OpenStackConfigException( "Problem with auth parameters") (verify, cert) = self.get_requests_verify_args() + # Turn off urllib3 warnings about insecure certs if we have + # explicitly configured requests to tell it we do not want + # cert verification + if not verify and urllib_exc is not None: + self.log.debug( + "Turning off SSL warnings for {cloud}:{region}" + " since verify=False".format( + cloud=self.name, region=self.region)) + warnings.filterwarnings( + 'ignore', category=urllib_exc.InsecureRequestWarning) self._keystone_session = session.Session( auth=self._auth, verify=verify, From 409134d4d013503fc2fdfd16cda8896e4499771e Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 3 Nov 2015 15:12:50 -0500 Subject: [PATCH 0614/3836] Update ansible router playbook Change-Id: Iae2cb060f8600476fd7af2ff77e99263edd67362 --- .../tests/ansible/roles/router/tasks/main.yml | 24 ++++++++++--------- .../tests/ansible/roles/router/vars/main.yml | 1 + 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/shade/tests/ansible/roles/router/tasks/main.yml b/shade/tests/ansible/roles/router/tasks/main.yml index e4ecba612..4e98987bf 100644 --- a/shade/tests/ansible/roles/router/tasks/main.yml +++ b/shade/tests/ansible/roles/router/tasks/main.yml @@ -1,16 +1,23 @@ --- -- name: Create network +- name: Create external network os_network: cloud: "{{ cloud }}" state: present - name: "{{ network_name }}" + name: "{{ external_network_name }}" external: true +- name: Create internal network + os_network: + cloud: "{{ cloud }}" + state: present + name: "{{ network_name }}" + external: false + - name: Create subnet1 os_subnet: cloud: "{{ cloud }}" state: present - network_name: "{{ network_name }}" + network_name: "{{ external_network_name }}" name: shade_subnet1 cidr: 10.6.6.0/24 @@ -27,21 +34,16 @@ cloud: "{{ cloud }}" state: present name: "{{ router_name }}" - network: "{{ network_name }}" - interfaces: - - subnet: shade_subnet1 - ip: 10.6.6.99 + network: "{{ external_network_name }}" - name: Update router os_router: cloud: "{{ cloud }}" state: present name: "{{ router_name }}" - network: "{{ network_name }}" + network: "{{ external_network_name }}" interfaces: - - subnet: shade_subnet1 - - subnet: shade_subnet2 - ip: 10.7.7.99 + - shade_subnet2 - name: Delete router os_router: diff --git a/shade/tests/ansible/roles/router/vars/main.yml b/shade/tests/ansible/roles/router/vars/main.yml index 0990d1a59..df5cbeb55 100644 --- a/shade/tests/ansible/roles/router/vars/main.yml +++ b/shade/tests/ansible/roles/router/vars/main.yml @@ -1 +1,2 @@ +external_network_name: ansible_external_net router_name: ansible_router From 234a2bf0685754a08d27e09187cdc54fdef04558 Mon Sep 17 00:00:00 2001 From: Lars Kellogg-Stedman Date: Fri, 30 Oct 2015 19:33:32 -0400 Subject: [PATCH 0615/3836] teach shade how to list_hypervisors adds a list_hypervisors method to shade, along with a corresponding unit test. Change-Id: Ia7d8f7249356f28fdbad6c513adee4176615142f --- shade/_tasks.py | 5 +++++ shade/operatorcloud.py | 14 ++++++++++++++ shade/tests/unit/test_shade_operator.py | 16 ++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/shade/_tasks.py b/shade/_tasks.py index e291658bb..d87967321 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -126,6 +126,11 @@ def main(self, client): return client.nova_client.servers.rebuild(**self.args) +class HypervisorList(task_manager.Task): + def main(self, client): + return client.nova_client.hypervisors.list(**self.args) + + class KeypairList(task_manager.Task): def main(self, client): return client.nova_client.keypairs.list() diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 65b51af31..45cf06291 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1486,3 +1486,17 @@ def delete_role(self, name_or_id): ) return True + + def list_hypervisors(self): + """List all hypervisors + + :returns: A list of hypervisor dicts. + """ + + try: + return self.manager.submitTask(_tasks.HypervisorList()) + except OpenStackCloudException: + raise + except Exception as e: + raise OpenStackCloudException( + "Error fetching hypervisor list: %s" % str(e)) diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index 78a4b8acf..beae25aae 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -19,6 +19,7 @@ import os_client_config.cloud_config import shade +import munch from shade import exc from shade import meta from shade.tests import fakes @@ -1038,3 +1039,18 @@ def test_has_service_no(self, session_mock): def test_has_service_yes(self, session_mock): session_mock.get_endpoint.return_value = 'http://fake.url' self.assertTrue(self.cloud.has_service("image")) + + @mock.patch.object(shade._tasks.HypervisorList, 'main') + def test_list_hypervisors(self, mock_hypervisorlist): + '''This test verifies that calling list_hypervisors results in a call + to the HypervisorList task.''' + mock_hypervisorlist.return_value = [ + munch.Munch({'hypervisor_hostname': 'testserver1', + 'id': '1'}), + munch.Munch({'hypervisor_hostname': 'testserver2', + 'id': '2'}) + ] + + r = self.cloud.list_hypervisors() + self.assertEquals(2, len(r)) + self.assertEquals('testserver1', r[0]['hypervisor_hostname']) From 1d3128d73b52559de9ca28fcb4ccfcea1609a962 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 3 Nov 2015 17:11:37 -0500 Subject: [PATCH 0616/3836] Add new context manager for shade exceptions Shade has developed a common pattern within many of the API methods where we try to avoid re-wrapping OpenStackClientException exceptions by catching them and simply re-raising. We can eliminate this duplicate exception handling with a context manager. Change-Id: Ic39b6e3efc33b7a30f298a3daca6d6c16ab4ca89 --- shade/_utils.py | 27 +++++++++++++++++++++++++++ shade/operatorcloud.py | 28 +++++++++------------------- 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/shade/_utils.py b/shade/_utils.py index 6d1806be4..0d1f03f9c 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -364,3 +364,30 @@ def neutron_exceptions(error_message): except Exception as e: raise exc.OpenStackCloudException( "{msg}: {exc}".format(msg=error_message, exc=str(e))) + + +@contextlib.contextmanager +def shade_exceptions(error_message=None): + """Context manager for dealing with shade exceptions. + + :param string error_message: String to use for the exception message + content on non-OpenStackCloudExceptions. + + Useful for avoiding wrapping shade OpenStackCloudException exceptions + within themselves. Code called from within the context may throw such + exceptions without having to catch and reraise them. + + Non-OpenStackCloudException exceptions thrown within the context will + be wrapped and the exception message will be appended to the given error + message. + """ + try: + yield + except exc.OpenStackCloudException: + raise + except Exception as e: + if error_message is not None: + message = "{msg}: {exc}".format(msg=error_message, exc=str(e)) + else: + message = str(e) + raise exc.OpenStackCloudException(message) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 65b51af31..e3153b7d1 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -654,7 +654,10 @@ def set_machine_maintenance_state( :returns: None """ - try: + with _utils.shade_exceptions( + "Error setting machine maintenance state to {state} on node " + "{node}".format(state=state, node=name_or_id) + ): if state: result = self.manager.submitTask( _tasks.MachineSetMaintenance(node_id=name_or_id, @@ -670,12 +673,6 @@ def set_machine_maintenance_state( "on node %s. Received: %s" % ( state, name_or_id, result)) return None - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Error setting machine maintenance state to %s " - "on node %s: %s" % (state, name_or_id, str(e))) def remove_machine_from_maintenance(self, name_or_id): """Remove Baremetal Machine from Maintenance State @@ -712,7 +709,10 @@ def _set_machine_power_state(self, name_or_id, state): :returns: None """ - try: + with _utils.shade_exceptions( + "Error setting machine power state to {state} on node " + "{node}".format(state=state, node=name_or_id) + ): power = self.manager.submitTask( _tasks.MachineSetPower(node_id=name_or_id, state=state)) @@ -721,12 +721,6 @@ def _set_machine_power_state(self, name_or_id, state): "Failed setting machine power state %s on node %s. " "Received: %s" % (state, name_or_id, power)) return None - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Error setting machine power state %s on node %s. " - "Error: %s" % (state, name_or_id, str(e))) def set_machine_power_on(self, name_or_id): """Activate baremetal machine power @@ -782,13 +776,9 @@ def deactivate_node(self, uuid): self.node_set_provision_state(uuid, 'deleted') def set_node_instance_info(self, uuid, patch): - try: + with _utils.shade_exceptions(): return self.manager.submitTask( _tasks.MachineNodeUpdate(node_id=uuid, patch=patch)) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException(str(e)) def purge_node_instance_info(self, uuid): patch = [] From ac51f4459106c10b0564747f36635779d40fadac Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 4 Nov 2015 13:33:41 -0500 Subject: [PATCH 0617/3836] Normalize int config values to string We have no things that expect actual int values. It's easy to write ints in yaml though - so normalize them to strings. Change-Id: I58cfdda697da9b7e030f4165ef8e60f4e9d00b66 --- os_client_config/config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/os_client_config/config.py b/os_client_config/config.py index af9dc3b48..eac4db610 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -235,6 +235,8 @@ def _normalize_keys(self, config): key = key.replace('-', '_') if isinstance(value, dict): new_config[key] = self._normalize_keys(value) + elif isinstance(value, int): + new_config[key] = str(value) else: new_config[key] = value return new_config From b4e7985d6a3a11e92cb51e463a547236bef59367 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 4 Nov 2015 12:36:26 -0500 Subject: [PATCH 0618/3836] Fix keystone domain searching Domain searches is a bit different in that it pushes down the filtering to the keystone client. This is fine, except that it didn't work as the filters were not actually passed. This change passes the filters, and changes the search_domains() API to accept a 'filters' parameter to be consistent with our other search methods. It previously accepted only keyword parameters rather than just a filter dict. Change-Id: I61c8591493c5d10de38a6541a9859be2950d60a6 --- shade/_tasks.py | 2 +- shade/operatorcloud.py | 8 ++-- shade/tests/functional/test_domain.py | 65 +++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 shade/tests/functional/test_domain.py diff --git a/shade/_tasks.py b/shade/_tasks.py index d87967321..ad56f9ef4 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -609,7 +609,7 @@ def main(self, client): class DomainList(task_manager.Task): def main(self, client): - return client.keystone_client.domains.list() + return client.keystone_client.domains.list(**self.args) class DomainGet(task_manager.Task): diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 4cf4ab03f..6e644cb90 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1161,11 +1161,11 @@ def list_domains(self): .format(msg=str(e))) return domains - def search_domains(self, **filters): - """Seach Keystone domains. + def search_domains(self, filters=None): + """Search Keystone domains. - :param filters: a dict containing additional filters to use. - keys to search on are id, name, enabled and description. + :param dict filters: A dict containing additional filters to use. + Keys to search on are id, name, enabled and description. :returns: a list of dicts containing the domain description. Each dict contains the following attributes:: diff --git a/shade/tests/functional/test_domain.py b/shade/tests/functional/test_domain.py new file mode 100644 index 000000000..954348a89 --- /dev/null +++ b/shade/tests/functional/test_domain.py @@ -0,0 +1,65 @@ +# 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_domain +---------------------------------- + +Functional tests for `shade` keystone domain resource. +""" + +import shade +from shade.tests import base + + +class TestDomain(base.TestCase): + + def setUp(self): + super(TestDomain, self).setUp() + self.cloud = shade.operator_cloud(cloud='devstack-admin') + if self.cloud.cloud_config.get_api_version('identity') in ('2', '2.0'): + self.skipTest('Identity service does not support domains') + self.domain_prefix = self.getUniqueString('domain') + self.addCleanup(self._cleanup_domains) + + def _cleanup_domains(self): + exception_list = list() + for domain in self.cloud.list_domains(): + if domain['name'].startswith(self.domain_prefix): + try: + self.cloud.delete_domain(domain['id']) + except Exception as e: + exception_list.append(str(e)) + continue + + if exception_list: + # Raise an error: we must make users aware that something went + # wrong + raise shade.OpenStackCloudException('\n'.join(exception_list)) + + def test_search_domains(self): + domain_name = self.domain_prefix + '_search' + + # Shouldn't find any domain with this name yet + results = self.cloud.search_domains(filters=dict(name=domain_name)) + self.assertEqual(0, len(results)) + + # Now create a new domain + domain = self.cloud.create_domain(domain_name) + self.assertEqual(domain_name, domain['name']) + + # Now we should find only the new domain + results = self.cloud.search_domains(filters=dict(name=domain_name)) + self.assertEqual(1, len(results)) + self.assertEqual(domain_name, results[0]['name']) From 5e031a592ceecf0207ebb88e637e5dcb2faab275 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 4 Nov 2015 14:52:19 -0500 Subject: [PATCH 0619/3836] Normalization methods should return Munch For the 1.0 release, we settled on returning Munch objects rather than plain dicts. However, we didn't arrange for that in the place where we normalize the returned data. Change-Id: I9eca96635ddfa012d6c5cff64162de1379f355ee --- shade/_utils.py | 46 ++++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/shade/_utils.py b/shade/_utils.py index 0d1f03f9c..d0e5ae097 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -22,6 +22,7 @@ from shade import _log from shade import exc +from shade import meta log = _log.setup_logging(__name__) @@ -151,7 +152,7 @@ def normalize_keystone_services(services): 'service_type': service_type, } ret.append(new_service) - return ret + return meta.obj_list_to_dict(ret) def normalize_nova_secgroups(groups): @@ -165,11 +166,12 @@ def normalize_nova_secgroups(groups): :returns: A list of normalized dicts. """ - return [{'id': g['id'], - 'name': g['name'], - 'description': g['description'], - 'security_group_rules': normalize_nova_secgroup_rules(g['rules']) - } for g in groups] + ret = [{'id': g['id'], + 'name': g['name'], + 'description': g['description'], + 'security_group_rules': normalize_nova_secgroup_rules(g['rules']) + } for g in groups] + return meta.obj_list_to_dict(ret) def normalize_nova_secgroup_rules(rules): @@ -182,17 +184,18 @@ def normalize_nova_secgroup_rules(rules): :returns: A list of normalized dicts. """ - return [{'id': r['id'], - 'direction': 'ingress', - 'ethertype': 'IPv4', - 'port_range_min': - None if r['from_port'] == -1 else r['from_port'], - 'port_range_max': - None if r['to_port'] == -1 else r['to_port'], - 'protocol': r['ip_protocol'], - 'remote_ip_prefix': r['ip_range'].get('cidr', None), - 'security_group_id': r['parent_group_id'] - } for r in rules] + ret = [{'id': r['id'], + 'direction': 'ingress', + 'ethertype': 'IPv4', + 'port_range_min': + None if r['from_port'] == -1 else r['from_port'], + 'port_range_max': + None if r['to_port'] == -1 else r['to_port'], + 'protocol': r['ip_protocol'], + 'remote_ip_prefix': r['ip_range'].get('cidr', None), + 'security_group_id': r['parent_group_id'] + } for r in rules] + return meta.obj_list_to_dict(ret) def normalize_nova_floating_ips(ips): @@ -223,7 +226,7 @@ def normalize_nova_floating_ips(ips): ] """ - return [dict( + ret = [dict( id=ip['id'], fixed_ip_address=ip.get('fixed_ip'), floating_ip_address=ip['ip'], @@ -233,6 +236,7 @@ def normalize_nova_floating_ips(ips): status='ACTIVE' # In neutrons terms, Nova floating IPs are always # ACTIVE ) for ip in ips] + return meta.obj_list_to_dict(ret) def normalize_neutron_floating_ips(ips): @@ -264,7 +268,7 @@ def normalize_neutron_floating_ips(ips): ] """ - return [dict( + ret = [dict( id=ip['id'], fixed_ip_address=ip.get('fixed_ip_address'), floating_ip_address=ip['floating_ip_address'], @@ -273,6 +277,7 @@ def normalize_neutron_floating_ips(ips): ip.get('port_id') != ''), status=ip['status'] ) for ip in ips] + return meta.obj_list_to_dict(ret) def localhost_supports_ipv6(): @@ -287,7 +292,7 @@ def localhost_supports_ipv6(): def normalize_users(users): - return [ + ret = [ dict( id=user.get('id'), email=user.get('email'), @@ -299,6 +304,7 @@ def normalize_users(users): enabled=user.get('enabled'), ) for user in users ] + return meta.obj_list_to_dict(ret) def valid_kwargs(*valid_args): From ba3b20610f52b7c3f0e0c087f03caf5a23839500 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 4 Nov 2015 15:06:12 -0500 Subject: [PATCH 0620/3836] Normalize domain data Keystone will not return a description attribute if no description was given when creating a domain. So let's normalize the data so all the things are there that we may expect. Change-Id: I0fd14d512554853d5a5a052e0517f47a509a35b2 --- shade/_utils.py | 12 ++++++++++++ shade/operatorcloud.py | 11 ++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/shade/_utils.py b/shade/_utils.py index d0e5ae097..a827c9b26 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -307,6 +307,18 @@ def normalize_users(users): return meta.obj_list_to_dict(ret) +def normalize_domains(domains): + ret = [ + dict( + id=domain.get('id'), + name=domain.get('name'), + description=domain.get('description'), + enabled=domain.get('enabled'), + ) for domain in domains + ] + return meta.obj_list_to_dict(ret) + + def valid_kwargs(*valid_args): # This decorator checks if argument passed as **kwargs to a function are # present in valid_args. diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 6e644cb90..41c56deba 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1107,12 +1107,12 @@ def create_domain( raise OpenStackCloudException( "Failed to create domain {name}".format(name=name, msg=str(e))) - return domain + return _utils.normalize_domains([domain])[0] def update_domain( self, domain_id, name=None, description=None, enabled=None): try: - return self.manager.submitTask(_tasks.DomainUpdate( + domain = self.manager.submitTask(_tasks.DomainUpdate( domain=domain_id, description=description, enabled=enabled)) except OpenStackCloudException: raise @@ -1120,6 +1120,7 @@ def update_domain( raise OpenStackCloudException( "Error in updating domain {domain}: {message}".format( domain=domain_id, message=str(e))) + return _utils.normalize_domains([domain])[0] def delete_domain(self, domain_id): """Delete a Keystone domain. @@ -1159,7 +1160,7 @@ def list_domains(self): except Exception as e: raise OpenStackCloudException("Failed to list domains: {msg}" .format(msg=str(e))) - return domains + return _utils.normalize_domains(domains) def search_domains(self, filters=None): """Search Keystone domains. @@ -1184,7 +1185,7 @@ def search_domains(self, filters=None): except Exception as e: raise OpenStackCloudException("Failed to list domains: {msg}" .format(msg=str(e))) - return domains + return _utils.normalize_domains(domains) def get_domain(self, domain_id): """Get exactly one Keystone domain. @@ -1211,7 +1212,7 @@ def get_domain(self, domain_id): domain_id=domain_id, msg=str(e))) raise OpenStackCloudException(str(e)) - return domain + return _utils.normalize_domains([domain])[0] def list_roles(self): """List Keystone roles. From 93d8b79900435e2f3b1755963e4a465cb6d4cdbf Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 3 Nov 2015 09:13:44 -0500 Subject: [PATCH 0621/3836] Don't warn on configured insecure certs If we have to connect to bogus certs and we know this and we've configured 'verfiy=False' meaning that we've explicitly indicated that we want to turn off cert verification, then spamming our logs with warnings that the certs are invalid is a bit lame. We know. We're not happy about it - but at least one of our providers has bad certs. We wish we had the power to change it, but we don't. We already feel bad. STOP YELLING AT US. Change-Id: I08d816bcc1685fc9ed3bfcf2e3e8300859059903 --- shade/openstackcloud.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index d1177e4c8..d79e7e110 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -18,6 +18,7 @@ import os_client_config.defaults import threading import time +import warnings from dogpile import cache @@ -48,6 +49,14 @@ from shade import _tasks from shade import _utils +# Importing these for later but not disabling for now +try: + from requests.packages.urllib3.exceptions import InsecureRequestWarning +except ImportError: + try: + from urllib3.exceptions import InsecureRequestWarning + except ImportError: + InsecureRequestWarning = None OBJECT_MD5_KEY = 'x-object-meta-x-shade-md5' OBJECT_SHA256_KEY = 'x-object-meta-x-shade-sha256' @@ -157,6 +166,13 @@ def __init__( name=self.name, client=self) (self.verify, self.cert) = cloud_config.get_requests_verify_args() + # Turn off urllib3 warnings about insecure certs if we have + # explicitly configured requests to tell it we do not want + # cert verification + if not self.verify: + self.log.debug( + "Turning off Insecure SSL warnings since verify=False") + warnings.filterwarnings('ignore', category=InsecureRequestWarning) self._servers = [] self._servers_time = 0 From 2b4542814381144dc6ac2f1afc932420c2c6c8d6 Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Tue, 3 Nov 2015 09:33:40 -0800 Subject: [PATCH 0622/3836] Use the requestsexceptions library Change-Id: Idf277388a006ab0d0c6071f77bce3ca6879ee562 --- requirements.txt | 1 + shade/__init__.py | 20 ++++++-------------- shade/openstackcloud.py | 13 +++---------- 3 files changed, 10 insertions(+), 24 deletions(-) diff --git a/requirements.txt b/requirements.txt index 3bd8985ad..70b82e2c2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ decorator jsonpatch ipaddress os-client-config>=1.9.0 +requestsexceptions>=1.1.1 six keystoneauth1>=1.0.0 diff --git a/shade/__init__.py b/shade/__init__.py index 320503498..c2820c353 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -13,32 +13,24 @@ # limitations under the License. import logging +import warnings import keystoneauth1.exceptions import os_client_config import pbr.version - -# Disable the Rackspace warnings about deprecated certificates. We are aware -import warnings -try: - from requests.packages.urllib3.exceptions import SubjectAltNameWarning -except ImportError: - try: - from urllib3.exceptions import SubjectAltNameWarning - except ImportError: - SubjectAltNameWarning = None - -if SubjectAltNameWarning: - warnings.filterwarnings('ignore', category=SubjectAltNameWarning) +import requestsexceptions from shade.exc import * # noqa from shade.openstackcloud import OpenStackCloud from shade.operatorcloud import OperatorCloud from shade import _log - __version__ = pbr.version.VersionInfo('shade').version_string() +if requestsexceptions.SubjectAltNameWarning: + warnings.filterwarnings( + 'ignore', category=requestsexceptions.SubjectAltNameWarning) + def simple_logging(debug=False): if debug: diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index d79e7e110..732c232b3 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -21,6 +21,7 @@ import warnings from dogpile import cache +import requestsexceptions from cinderclient.v1 import client as cinder_client from designateclient.v1 import Client as designate_client @@ -49,15 +50,6 @@ from shade import _tasks from shade import _utils -# Importing these for later but not disabling for now -try: - from requests.packages.urllib3.exceptions import InsecureRequestWarning -except ImportError: - try: - from urllib3.exceptions import InsecureRequestWarning - except ImportError: - InsecureRequestWarning = None - OBJECT_MD5_KEY = 'x-object-meta-x-shade-md5' OBJECT_SHA256_KEY = 'x-object-meta-x-shade-sha256' IMAGE_MD5_KEY = 'owner_specified.shade.md5' @@ -172,7 +164,8 @@ def __init__( if not self.verify: self.log.debug( "Turning off Insecure SSL warnings since verify=False") - warnings.filterwarnings('ignore', category=InsecureRequestWarning) + category = requestsexceptions.InsecureRequestWarning + warnings.filterwarnings('ignore', category) self._servers = [] self._servers_time = 0 From 1bf09410d191b872b975634f8cf3f8847865b546 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 4 Nov 2015 18:28:41 -0500 Subject: [PATCH 0623/3836] Use requestsexceptions for urllib squelching The code to deal with this properly is quite sharable and we should not care. Use requestsexceptions from the Infra team to handle it. Change-Id: Ie20a3e1b2d8d18a4a76b34219cf12510cb1cda98 Depends-On: I52249b6d2fe04c49a9f4ed139d7625c890309ca8 --- os_client_config/cloud_config.py | 15 +++------------ requirements.txt | 1 + 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 17941c970..5e8d49293 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -16,19 +16,11 @@ from keystoneauth1 import plugin from keystoneauth1 import session +import requestsexceptions from os_client_config import _log from os_client_config import exceptions -# Importing these for later but not disabling for now -try: - from requests.packages.urllib3 import exceptions as urllib_exc -except ImportError: - try: - from urllib3 import exceptions as urllib_exc - except ImportError: - urllib_exc = None - class CloudConfig(object): def __init__(self, name, region, config, @@ -146,13 +138,12 @@ def get_session(self): # Turn off urllib3 warnings about insecure certs if we have # explicitly configured requests to tell it we do not want # cert verification - if not verify and urllib_exc is not None: + if not verify: self.log.debug( "Turning off SSL warnings for {cloud}:{region}" " since verify=False".format( cloud=self.name, region=self.region)) - warnings.filterwarnings( - 'ignore', category=urllib_exc.InsecureRequestWarning) + requestsexceptions.squelch_warnings(insecure_requests=not verify) self._keystone_session = session.Session( auth=self._auth, verify=verify, diff --git a/requirements.txt b/requirements.txt index db0b6354a..3c32ced99 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ PyYAML>=3.1.0 appdirs>=1.3.0 keystoneauth1>=1.0.0 +requestsexceptions>=1.1.1 # Apache-2.0 From 588be0126307a6b6dd0582ea546c73f05f23b919 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 5 Nov 2015 02:55:50 -0500 Subject: [PATCH 0624/3836] Dont turn bools into strings It turns out that requests does not like that. Change-Id: I206be8107f5cfaaa7dc7f34ab0b0764e0dc3fb0d --- os_client_config/config.py | 2 ++ os_client_config/tests/base.py | 1 + 2 files changed, 3 insertions(+) diff --git a/os_client_config/config.py b/os_client_config/config.py index eac4db610..1bb7999f4 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -235,6 +235,8 @@ def _normalize_keys(self, config): key = key.replace('-', '_') if isinstance(value, dict): new_config[key] = self._normalize_keys(value) + elif isinstance(value, bool): + new_config[key] = value elif isinstance(value, int): new_config[key] = str(value) else: diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py index 36c3bfb27..67c80f254 100644 --- a/os_client_config/tests/base.py +++ b/os_client_config/tests/base.py @@ -149,6 +149,7 @@ def _assert_cloud_details(self, cc): self.assertIsNone(cc.cloud) self.assertIn('username', cc.auth) self.assertEqual('testuser', cc.auth['username']) + self.assertFalse(cc.config['image_api_use_tasks']) self.assertTrue('project_name' in cc.auth or 'project_id' in cc.auth) if 'project_name' in cc.auth: self.assertEqual('testproject', cc.auth['project_name']) From 059d314ba44fd52f16436bf104b1a476df77bbdb Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 5 Nov 2015 02:11:23 -0500 Subject: [PATCH 0625/3836] Update network api version in defaults.json neutronclient expects 2.0, not 2 - but we'd not noticed because all of the consumption of client constructors were bypasing the version arg for neutron. In a follow-up patch we'll fix that, but the value in defaults.json is just straight wrong. Change-Id: I684c4ef063193355c6cf936d4f18576db919762b --- os_client_config/defaults.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/os_client_config/defaults.json b/os_client_config/defaults.json index 9239d0fe8..d88ad0488 100644 --- a/os_client_config/defaults.json +++ b/os_client_config/defaults.json @@ -11,7 +11,7 @@ "image_api_use_tasks": false, "image_api_version": "2", "image_format": "qcow2", - "network_api_version": "2", + "network_api_version": "2.0", "object_api_version": "1", "orchestration_api_version": "1", "secgroup_source": "neutron", From d373969b98cd1db9188085dc2cd0b7754c687ff6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 5 Nov 2015 02:12:40 -0500 Subject: [PATCH 0626/3836] Don't assume pass_version_arg=False for network Excitingly, one of the places where we hard-code a workaround for a differently behaving client is not needed. The Client in neutron that takes a version arg is in neutronclient.neutron.client. Also, pass the version arg as positional when we pass it, as some of the clients (neutron) require it, and all accept it. Change-Id: Ifcbaab782173b95a678af0b2792a1194b198b687 --- os_client_config/cloud_config.py | 16 ++++++++-------- os_client_config/tests/test_cloud_config.py | 9 +++++---- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 17941c970..8e0b96336 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -238,10 +238,8 @@ def get_legacy_client( interface_key = 'interface' else: interface_key = 'endpoint_type' - if service_key == 'network': - pass_version_arg = False - constructor_args = dict( + constructor_kwargs = dict( session=self.get_session(), service_name=self.get_service_name(service_key), service_type=self.get_service_type(service_key), @@ -254,13 +252,15 @@ def get_legacy_client( # they necessarily have glanceclient installed from glanceclient.common import utils as glance_utils endpoint, version = glance_utils.strip_version(endpoint) - constructor_args['endpoint'] = endpoint - constructor_args.update(kwargs) - constructor_args[interface_key] = interface + constructor_kwargs['endpoint'] = endpoint + constructor_kwargs.update(kwargs) + constructor_kwargs[interface_key] = interface + constructor_args = [] if pass_version_arg: version = self.get_api_version(service_key) - constructor_args['version'] = version - return client_class(**constructor_args) + constructor_args.append(version) + + return client_class(*constructor_args, **constructor_kwargs) def _get_swift_client(self, client_class, **kwargs): session = self.get_session() diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index f5ceac7b7..080f1001d 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -24,7 +24,7 @@ fake_config_dict = {'a': 1, 'os_b': 2, 'c': 3, 'os_c': 4} fake_services_dict = { - 'compute_api_version': 2, + 'compute_api_version': '2', 'compute_endpoint': 'http://compute.example.com', 'compute_region_name': 'region-bl', 'interface': 'public', @@ -139,7 +139,7 @@ def test_getters(self): self.assertEqual('region-al', cc.get_region_name('image')) self.assertEqual('region-bl', cc.get_region_name('compute')) self.assertEqual(None, cc.get_api_version('image')) - self.assertEqual(2, cc.get_api_version('compute')) + self.assertEqual('2', cc.get_api_version('compute')) self.assertEqual('mage', cc.get_service_type('image')) self.assertEqual('compute', cc.get_service_type('compute')) self.assertEqual('http://compute.example.com', @@ -235,7 +235,7 @@ def test_legacy_client_image(self, mock_get_session_endpoint): "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('image', mock_client) mock_client.assert_called_with( - version='2', + '2', service_name=None, endpoint='http://example.com', region_name='region-al', @@ -255,6 +255,7 @@ def test_legacy_client_network(self, mock_get_session_endpoint): "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('network', mock_client) mock_client.assert_called_with( + '2.0', endpoint_type='public', region_name='region-al', service_type='network', @@ -271,7 +272,7 @@ def test_legacy_client_compute(self, mock_get_session_endpoint): "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('compute', mock_client) mock_client.assert_called_with( - version=2, + '2', endpoint_type='public', region_name='region-al', service_type='compute', From 10836aeb85b36d3ab02794762b7089a3ab6a4928 Mon Sep 17 00:00:00 2001 From: matthew wagoner Date: Wed, 4 Nov 2015 11:01:41 -0500 Subject: [PATCH 0627/3836] Add new context manager for shade exceptions, cont. This commit continues the work of change Ic39b6e3efc33b7a30f298a3daca6d6c16ab4ca89 Shade has developed a common pattern within many of the API methods where we try to avoid re-wrapping OpenStackClientException exceptions by catching them and simply re-raising. We can eliminate this duplicate exception handling with a context manager. Change-Id: Ifd97181b33ed72bb9ad5b3da1e2b92a660e12a65 --- shade/operatorcloud.py | 230 +++++++++++------------------------------ 1 file changed, 58 insertions(+), 172 deletions(-) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 41c56deba..1c3cd2e87 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -56,13 +56,8 @@ def ironic_client(self): return self._ironic_client def list_nics(self): - try: + with _utils.shade_exceptions("Error fetching machine port list"): return self.manager.submitTask(_tasks.MachinePortList()) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Error fetching machine port list: %s" % e) def list_nics_for_machine(self, uuid): try: @@ -158,7 +153,7 @@ def inspect_machine(self, name_or_id, wait=False, timeout=3600): "Machine must be in 'manage' or 'available' state to " "engage inspection: Machine: %s State: %s" % (machine['uuid'], machine['provision_state'])) - try: + with _utils.shade_exceptions("Error inspecting machine"): machine = self.node_set_provision_state(machine['uuid'], 'inspect') if wait: for count in _utils._iterate_timeout( @@ -181,12 +176,6 @@ def inspect_machine(self, name_or_id, wait=False, timeout=3600): return(machine) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Error inspecting machine: %s" % e) - def register_machine(self, nics, wait=False, timeout=3600, lock_timeout=600, **kwargs): """Register Baremetal with Ironic @@ -233,15 +222,9 @@ def register_machine(self, nics, wait=False, timeout=3600, :returns: Returns a dictonary representing the new baremetal node. """ - try: + with _utils.shade_exceptions("Error registering machine with Ironic"): machine = self.manager.submitTask(_tasks.MachineCreate(**kwargs)) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Error registering machine with Ironic: %s" % str(e)) - created_nics = [] try: for row in nics: @@ -268,7 +251,8 @@ def register_machine(self, nics, wait=False, timeout=3600, "Error registering NICs with the baremetal service: %s" % str(e)) - try: + with _utils.shade_exceptions( + "Error transitioning node to available state"): if wait: for count in _utils._iterate_timeout( timeout, @@ -329,12 +313,6 @@ def register_machine(self, nics, wait=False, timeout=3600, "Machine encountered a failure: %s" % machine['last_error']) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Error transitioning node to available state: %s" - % e) return machine def unregister_machine(self, nics, uuid, wait=False, timeout=600): @@ -430,17 +408,14 @@ def patch_machine(self, name_or_id, patch): :returns: Dictonary representing the newly updated node. """ - try: + with _utils.shade_exceptions( + "Error updating machine via patch operation on node " + "{node}".format(node=name_or_id) + ): return self.manager.submitTask( _tasks.MachinePatch(node_id=name_or_id, patch=patch, http_method='PATCH')) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Error updating machine via patch operation. node: %s. " - "%s" % (name_or_id, str(e))) def update_machine(self, name_or_id, chassis_uuid=None, driver=None, driver_info=None, name=None, instance_info=None, @@ -532,7 +507,10 @@ def update_machine(self, name_or_id, chassis_uuid=None, driver=None, "for submission to the API. Machine: %s Error: %s" % (name_or_id, str(e))) - try: + with _utils.shade_exceptions( + "Machine update failed - patch operation failed on Machine " + "{node}".format(node=name_or_id) + ): if not patch: return dict( node=machine, @@ -547,21 +525,11 @@ def update_machine(self, name_or_id, chassis_uuid=None, driver=None, node=machine, changes=change_list ) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Machine update failed - patch operation failed Machine: %s " - "Error: %s" % (name_or_id, str(e))) def validate_node(self, uuid): - try: + with _utils.shade_exceptions(): ifaces = self.manager.submitTask( _tasks.MachineNodeValidate(node_uuid=uuid)) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException(str(e)) if not ifaces.deploy or not ifaces.power: raise OpenStackCloudException( @@ -602,7 +570,10 @@ def node_set_provision_state(self, :returns: Dictonary representing the current state of the machine upon exit of the method. """ - try: + with _utils.shade_exceptions( + "Baremetal machine node failed change provision state to " + "{state}".format(state=state) + ): machine = self.manager.submitTask( _tasks.MachineSetProvision(node_uuid=name_or_id, state=state, @@ -623,14 +594,6 @@ def node_set_provision_state(self, machine = self.get_machine(name_or_id) return machine - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Baremetal machine node failed change provision" - " state to {state}: {msg}".format(state=state, - msg=str(e))) - def set_machine_maintenance_state( self, name_or_id, @@ -783,13 +746,9 @@ def set_node_instance_info(self, uuid, patch): def purge_node_instance_info(self, uuid): patch = [] patch.append({'op': 'remove', 'path': '/instance_info'}) - try: + with _utils.shade_exceptions(): return self.manager.submitTask( _tasks.MachineNodeUpdate(node_id=uuid, patch=patch)) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException(str(e)) @_utils.valid_kwargs('type', 'service_type', 'description') def create_service(self, name, **kwargs): @@ -814,7 +773,8 @@ def create_service(self, name, **kwargs): """ service_type = kwargs.get('type', kwargs.get('service_type')) description = kwargs.get('description', None) - try: + with _utils.shade_exceptions("Failed to create service {name}".format( + name=name)): if self.cloud_config.get_api_version('identity').startswith('2'): service_kwargs = {'service_type': service_type} else: @@ -822,12 +782,7 @@ def create_service(self, name, **kwargs): service = self.manager.submitTask(_tasks.ServiceCreate( name=name, description=description, **service_kwargs)) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Failed to create service {name}: {msg}".format( - name=name, msg=str(e))) + return service def list_services(self): @@ -838,12 +793,8 @@ def list_services(self): :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call. """ - try: + with _utils.shade_exceptions(): services = self.manager.submitTask(_tasks.ServiceList()) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException(str(e)) return _utils.normalize_keystone_services(services) def search_services(self, name_or_id=None, filters=None): @@ -899,15 +850,10 @@ def delete_service(self, name_or_id): service_kwargs = {'id': service['id']} else: service_kwargs = {'service': service['id']} - try: + with _utils.shade_exceptions("Failed to delete service {id}".format( + id=service['id'])): self.manager.submitTask(_tasks.ServiceDelete(**service_kwargs)) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Failed to delete service {id}: {msg}".format( - id=service['id'], - msg=str(e))) + return True @_utils.valid_kwargs('public_url', 'internal_url', 'admin_url') @@ -981,21 +927,17 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, urlkwargs['service'] = service['id'] endpoint_args.append(urlkwargs) - for args in endpoint_args: - try: + with _utils.shade_exceptions( + "Failed to create endpoint for service " + "{service}".format(service=service['name']) + ): + for args in endpoint_args: endpoint = self.manager.submitTask(_tasks.EndpointCreate( region=region, **args )) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Failed to create endpoint for service {service}: " - "{msg}".format(service=service['name'], - msg=str(e))) - endpoints.append(endpoint) - return endpoints + endpoints.append(endpoint) + return endpoints def list_endpoints(self): """List Keystone endpoints. @@ -1006,13 +948,9 @@ def list_endpoints(self): the openstack API call. """ # ToDo: support v3 api (dguerri) - try: + with _utils.shade_exceptions("Failed to list endpoints"): endpoints = self.manager.submitTask(_tasks.EndpointList()) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException("Failed to list endpoints: {msg}" - .format(msg=str(e))) + return endpoints def search_endpoints(self, id=None, filters=None): @@ -1073,15 +1011,10 @@ def delete_endpoint(self, id): endpoint_kwargs = {'id': endpoint['id']} else: endpoint_kwargs = {'endpoint': endpoint['id']} - try: + with _utils.shade_exceptions("Failed to delete endpoint {id}".format( + id=id)): self.manager.submitTask(_tasks.EndpointDelete(**endpoint_kwargs)) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Failed to delete endpoint {id}: {msg}".format( - id=id, - msg=str(e))) + return True def create_domain( @@ -1096,30 +1029,20 @@ def create_domain( :raise OpenStackCloudException: if the domain cannot be created """ - try: + with _utils.shade_exceptions("Failed to create domain {name}".format( + name=name)): domain = self.manager.submitTask(_tasks.DomainCreate( name=name, description=description, enabled=enabled)) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Failed to create domain {name}".format(name=name, - msg=str(e))) return _utils.normalize_domains([domain])[0] def update_domain( self, domain_id, name=None, description=None, enabled=None): - try: + with _utils.shade_exceptions( + "Error in updating domain {domain}".format(domain=domain_id)): domain = self.manager.submitTask(_tasks.DomainUpdate( domain=domain_id, description=description, enabled=enabled)) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Error in updating domain {domain}: {message}".format( - domain=domain_id, message=str(e))) return _utils.normalize_domains([domain])[0] def delete_domain(self, domain_id): @@ -1132,18 +1055,13 @@ def delete_domain(self, domain_id): :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call. """ - try: + with _utils.shade_exceptions("Failed to delete domain {id}".format( + id=domain_id)): # Deleting a domain is expensive, so disabling it first increases # the changes of success domain = self.update_domain(domain_id, enabled=False) self.manager.submitTask(_tasks.DomainDelete( domain=domain['id'])) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Failed to delete domain {id}: {msg}".format(id=domain_id, - msg=str(e))) def list_domains(self): """List Keystone domains. @@ -1153,13 +1071,8 @@ def list_domains(self): :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ - try: + with _utils.shade_exceptions("Failed to list domains"): domains = self.manager.submitTask(_tasks.DomainList()) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException("Failed to list domains: {msg}" - .format(msg=str(e))) return _utils.normalize_domains(domains) def search_domains(self, filters=None): @@ -1177,14 +1090,9 @@ def search_domains(self, filters=None): :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ - try: + with _utils.shade_exceptions("Failed to list domains"): domains = self.manager.submitTask( _tasks.DomainList(**filters)) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException("Failed to list domains: {msg}" - .format(msg=str(e))) return _utils.normalize_domains(domains) def get_domain(self, domain_id): @@ -1201,17 +1109,12 @@ def get_domain(self, domain_id): :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ - try: + with _utils.shade_exceptions( + "Failed to get domain " + "{domain_id}".format(domain_id=domain_id) + ): domain = self.manager.submitTask( _tasks.DomainGet(domain=domain_id)) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Failed to get domain {domain_id}: {msg}".format( - domain_id=domain_id, - msg=str(e))) - raise OpenStackCloudException(str(e)) return _utils.normalize_domains([domain])[0] def list_roles(self): @@ -1222,12 +1125,9 @@ def list_roles(self): :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ - try: + with _utils.shade_exceptions(): roles = self.manager.submitTask(_tasks.RoleList()) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException(str(e)) + return roles def search_roles(self, name_or_id=None, filters=None): @@ -1285,20 +1185,15 @@ def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", :raises: OpenStackCloudException on operation error. """ - try: + with _utils.shade_exceptions("Failed to create flavor {name}".format( + name=name)): flavor = self.manager.submitTask( _tasks.FlavorCreate(name=name, ram=ram, vcpus=vcpus, disk=disk, flavorid=flavorid, ephemeral=ephemeral, swap=swap, rxtx_factor=rxtx_factor, is_public=is_public) ) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Failed to create flavor {name}: {msg}".format( - name=name, - msg=str(e))) + return flavor def delete_flavor(self, name_or_id): @@ -1316,14 +1211,9 @@ def delete_flavor(self, name_or_id): "Flavor {0} not found for deleting".format(name_or_id)) return False - try: + with _utils.shade_exceptions("Unable to delete flavor {name}".format( + name=name_or_id)): self.manager.submitTask(_tasks.FlavorDelete(flavor=flavor['id'])) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Unable to delete flavor {0}: {1}".format(name_or_id, e) - ) return True @@ -1441,14 +1331,10 @@ def create_role(self, name): :raise OpenStackCloudException: if the role cannot be created """ - try: + with _utils.shade_exceptions(): role = self.manager.submitTask( _tasks.RoleCreate(name=name) ) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException(str(e)) return role def delete_role(self, name_or_id): From b8874ffc98488500fd4b573f7fa6196d3997e84e Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 5 Nov 2015 10:26:10 -0500 Subject: [PATCH 0628/3836] Convert floats to string We do it for int values, so floats should be covered, too. Change-Id: I1b98f7887ad421596b3bfdf2f6ad871a7d7f6bb2 --- os_client_config/config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/os_client_config/config.py b/os_client_config/config.py index 1bb7999f4..fd28c6910 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -239,6 +239,8 @@ def _normalize_keys(self, config): new_config[key] = value elif isinstance(value, int): new_config[key] = str(value) + elif isinstance(value, float): + new_config[key] = str(value) else: new_config[key] = value return new_config From 3b43304c73f09d7e4f19b57de7e346ec1f3a171b Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 5 Nov 2015 15:44:34 -0500 Subject: [PATCH 0629/3836] Make sure cache expiration time is an int The latest os-client-config release, 1.10.0, makes sure that all numerical values are strings. But we need to pass an int to the cache configure stuff. Change-Id: I5d5e42440afd58111daeab6a5f42aa9ae7ceddf4 --- shade/openstackcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 732c232b3..980caf169 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -171,7 +171,7 @@ def __init__( self._servers_time = 0 self._servers_lock = threading.Lock() - cache_expiration_time = cloud_config.get_cache_expiration_time() + cache_expiration_time = int(cloud_config.get_cache_expiration_time()) cache_class = cloud_config.get_cache_class() cache_arguments = cloud_config.get_cache_arguments() cache_expiration = cloud_config.get_cache_expiration() From 26c8ebd4b49acb8d12db2498d12f591e9955d528 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 31 Oct 2015 10:24:10 +0900 Subject: [PATCH 0630/3836] Bump ironicclient depend ironicclient 0.10.0 drops the lxml requirement. Let's set the min in shade to that so that we make sure people don't try to install lxml. Change-Id: I942de581e15017de9706cabd2eb759eec578565e Depends-On: I6f0e5d7f3d97a5fb6a948d653633b067dfe258f7 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 70b82e2c2..cd8f14de8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ python-glanceclient>=1.0.0 python-cinderclient<1.2 python-neutronclient>=2.3.10 python-troveclient>=1.2.0 -python-ironicclient>=0.9.0 +python-ironicclient>=0.10.0 python-swiftclient>=2.5.0 python-designateclient>=1.3.0 python-heatclient>=0.3.0 From 2339243e66b676325c84693e7f8199d476199203 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 5 Nov 2015 17:00:33 -0500 Subject: [PATCH 0631/3836] Add method to get a mounted session from config Getting a session is great, but sometimes you need a thing called an "adapter" which takes 5 parameters which are all already contained in the config that you used to get the session. Change-Id: Id4e418cd04ae81540d9898f7b2e959b974f355d2 --- os_client_config/__init__.py | 17 +++++++++++++++++ os_client_config/cloud_config.py | 23 +++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/os_client_config/__init__.py b/os_client_config/__init__.py index d5fd36cb6..00e6ff514 100644 --- a/os_client_config/__init__.py +++ b/os_client_config/__init__.py @@ -13,3 +13,20 @@ # under the License. from os_client_config.config import OpenStackConfig # noqa + + +def simple_client(service_key, cloud=None, region_name=None): + """Simple wrapper function. It has almost no features. + + This will get you a raw requests Session Adapter that is mounted + on the given service from the keystone service catalog. If you leave + off cloud and region_name, it will assume that you've got env vars + set, but if you give them, it'll use clouds.yaml as you'd expect. + + This function is deliberately simple. It has no flexibility. If you + want flexibility, you can make a cloud config object and call + get_session_client on it. This function is to make it easy to poke + at OpenStack REST APIs with a properly configured keystone session. + """ + return OpenStackConfig().get_one_cloud( + cloud=cloud, region_name=region_name).get_session_client('compute') diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 8e0b96336..7cab1acd4 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -14,6 +14,7 @@ import warnings +from keystoneauth1 import adapter from keystoneauth1 import plugin from keystoneauth1 import session @@ -160,6 +161,28 @@ def get_session(self): timeout=self.config['api_timeout']) return self._keystone_session + def get_session_client(self, service_key): + """Return a prepped requests adapter for a given service. + + This is useful for making direct requests calls against a + 'mounted' endpoint. That is, if you do: + + client = get_session_client('compute') + + then you can do: + + client.get('/flavors') + + and it will work like you think. + """ + + return adapter.Adapter( + session=self.get_session(), + service_type=self.get_service_type(service_key), + service_name=self.get_service_name(service_key), + interface=self.get_interface(service_key), + region_name=self.region) + def get_session_endpoint(self, service_key): """Return the endpoint from config or the catalog. From 5b993208b97a459429bcb5f6fb852372c576cdaf Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 6 Nov 2015 06:39:34 -0500 Subject: [PATCH 0632/3836] Return cache settings as numbers not strings While we're at it, let's also put in some tests to ensure that we're processing data types properly. Change-Id: I0442d234e8422a58738612b2da114f61cc9afc5c --- os_client_config/cloud_config.py | 10 ++++++---- os_client_config/config.py | 6 +++--- os_client_config/tests/base.py | 11 ++++++++++- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 8e0b96336..79a6ebff1 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -299,15 +299,17 @@ def get_cache_expiration(self): if self._openstack_config: return self._openstack_config.get_cache_expiration() - def get_cache_resource_expiration(self, resource): + def get_cache_resource_expiration(self, resource, default=None): """Get expiration time for a resource :param resource: Name of the resource type + :param default: Default value to return if not found (optional, + defaults to None) - :returns: Expiration time for the resource type or None + :returns: Expiration time for the resource type as float or default """ if self._openstack_config: expiration = self._openstack_config.get_cache_expiration() if resource not in expiration: - return None - return expiration[resource] + return default + return float(expiration[resource]) diff --git a/os_client_config/config.py b/os_client_config/config.py index fd28c6910..8d2a2ee76 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -246,13 +246,13 @@ def _normalize_keys(self, config): return new_config def get_cache_expiration_time(self): - return self._cache_expiration_time + return int(self._cache_expiration_time) def get_cache_interval(self): - return self._cache_expiration_time + return self.get_cache_expiration_time() def get_cache_max_age(self): - return self._cache_expiration_time + return self.get_cache_expiration_time() def get_cache_path(self): return self._cache_path diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py index 67c80f254..3d94e2581 100644 --- a/os_client_config/tests/base.py +++ b/os_client_config/tests/base.py @@ -39,6 +39,13 @@ } } USER_CONF = { + 'cache': { + 'max_age': '1', + 'expiration': { + 'server': 5, + 'image': '7', + }, + }, 'client': { 'force_ipv4': True, }, @@ -104,7 +111,6 @@ 'region_name': 'test-region', } }, - 'cache': {'max_age': 1}, } NO_CONF = { 'cache': {'max_age': 1}, @@ -155,3 +161,6 @@ def _assert_cloud_details(self, cc): self.assertEqual('testproject', cc.auth['project_name']) elif 'project_id' in cc.auth: self.assertEqual('testproject', cc.auth['project_id']) + self.assertEqual(cc.get_cache_expiration_time(), 1) + self.assertEqual(cc.get_cache_resource_expiration('server'), 5.0) + self.assertEqual(cc.get_cache_resource_expiration('image'), 7.0) From 13b6fbabeb27a346c91a9fb3a1fb86c7adbd2779 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 6 Nov 2015 09:04:12 -0500 Subject: [PATCH 0633/3836] Work around a bug in keystoneclient constructor keystoneclient bug #1513839 means that even when you proerly pass a Session (like we do) to the discovery constructor (what you'd be calling if you were passing "pass_version_arg = True") ksc fails because you didn't send a URL. We _HAVE_ an appropriate URL that we can pull from the Session. So until ksc learns how to pull the URL from the Session itself, do it for them. Change-Id: I38eb4cfa750fab5196b86989c2cd498d41bf37ac --- os_client_config/cloud_config.py | 11 +++++- os_client_config/tests/test_cloud_config.py | 37 +++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 79a6ebff1..37029f1bb 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -258,7 +258,16 @@ def get_legacy_client( constructor_args = [] if pass_version_arg: version = self.get_api_version(service_key) - constructor_args.append(version) + if service_key == 'identity': + # keystoneclient takes version as a tuple. + version = tuple(str(float(version)).split('.')) + constructor_kwargs['version'] = version + # Workaround for bug#1513839 + if 'endpoint' not in constructor_kwargs: + endpoint = self.get_session_endpoint('identity') + constructor_kwargs['endpoint'] = endpoint + else: + constructor_args.append(version) return client_class(*constructor_args, **constructor_kwargs) diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index 080f1001d..deaec4d35 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -278,3 +278,40 @@ def test_legacy_client_compute(self, mock_get_session_endpoint): service_type='compute', session=mock.ANY, service_name=None) + + @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + def test_legacy_client_identity(self, mock_get_session_endpoint): + mock_client = mock.Mock() + mock_get_session_endpoint.return_value = 'http://example.com/v2' + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + cc = cloud_config.CloudConfig( + "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + cc.get_legacy_client('identity', mock_client) + mock_client.assert_called_with( + version=('2', '0'), + endpoint='http://example.com/v2', + endpoint_type='admin', + region_name='region-al', + service_type='identity', + session=mock.ANY, + service_name='locks') + + @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + def test_legacy_client_identity_v3(self, mock_get_session_endpoint): + mock_client = mock.Mock() + mock_get_session_endpoint.return_value = 'http://example.com' + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + config_dict['identity_api_version'] = '3' + cc = cloud_config.CloudConfig( + "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + cc.get_legacy_client('identity', mock_client) + mock_client.assert_called_with( + version=('3', '0'), + endpoint='http://example.com', + endpoint_type='admin', + region_name='region-al', + service_type='identity', + session=mock.ANY, + service_name='locks') From 89dd35301c22c46f3cc3ba4f4f07f6f047383429 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 6 Nov 2015 16:10:27 -0500 Subject: [PATCH 0634/3836] Add CRUD methods for keystone groups This replaces review Ia25b76c38c3463c963a5ac2ae1c81fcd33dc591b. Change-Id: I9a6a28994d076f032fe27624cdb3b0fbe248acef Co-Authored-By: Monty Taylor --- shade/_tasks.py | 20 +++++ shade/_utils.py | 13 +++ shade/operatorcloud.py | 122 ++++++++++++++++++++++++++ shade/tests/fakes.py | 8 ++ shade/tests/functional/test_groups.py | 105 ++++++++++++++++++++++ shade/tests/unit/test_groups.py | 64 ++++++++++++++ 6 files changed, 332 insertions(+) create mode 100644 shade/tests/functional/test_groups.py create mode 100644 shade/tests/unit/test_groups.py diff --git a/shade/_tasks.py b/shade/_tasks.py index ad56f9ef4..55b71af9a 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -627,6 +627,26 @@ def main(self, client): return client.keystone_client.domains.delete(**self.args) +class GroupList(task_manager.Task): + def main(self, client): + return client.keystone_client.groups.list() + + +class GroupCreate(task_manager.Task): + def main(self, client): + return client.keystone_client.groups.create(**self.args) + + +class GroupDelete(task_manager.Task): + def main(self, client): + return client.keystone_client.groups.delete(**self.args) + + +class GroupUpdate(task_manager.Task): + def main(self, client): + return client.keystone_client.groups.update(**self.args) + + class ZoneList(task_manager.Task): def main(self, client): return client.designate_client.domains.list() diff --git a/shade/_utils.py b/shade/_utils.py index a827c9b26..865ecaaca 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -319,6 +319,19 @@ def normalize_domains(domains): return meta.obj_list_to_dict(ret) +def normalize_groups(domains): + """Normalize Identity groups.""" + ret = [ + dict( + id=domain.get('id'), + name=domain.get('name'), + description=domain.get('description'), + domain_id=domain.get('domain_id'), + ) for domain in domains + ] + return meta.obj_list_to_dict(ret) + + def valid_kwargs(*valid_args): # This decorator checks if argument passed as **kwargs to a function are # present in valid_args. diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 1c3cd2e87..cb40f4b61 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1117,6 +1117,128 @@ def get_domain(self, domain_id): _tasks.DomainGet(domain=domain_id)) return _utils.normalize_domains([domain])[0] + @_utils.cache_on_arguments() + def list_groups(self): + """List Keystone Groups. + + :returns: A list of dicts containing the group description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + with _utils.shade_exceptions("Failed to list groups"): + groups = self.manager.submitTask(_tasks.GroupList()) + return _utils.normalize_groups(groups) + + def search_groups(self, name_or_id=None, filters=None): + """Search Keystone groups. + + :param name: Group name or id. + :param filters: A dict containing additional filters to use. + + :returns: A list of dict containing the group description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + groups = self.list_groups() + return _utils._filter_list(groups, name_or_id, filters) + + def get_group(self, name_or_id, filters=None): + """Get exactly one Keystone group. + + :param id: Group name or id. + :param filters: A dict containing additional filters to use. + + :returns: A dict containing the group description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + return _utils._get_entity(self.search_groups, name_or_id, filters) + + def create_group(self, name, description, domain=None): + """Create a group. + + :param string name: Group name. + :param string description: Group description. + :param string domain: Domain name or ID for the group. + + :returns: A dict containing the group description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + with _utils.shade_exceptions( + "Error creating group {group}".format(group=name) + ): + domain_id = None + if domain: + dom = self.get_domain(domain) + if not dom: + raise OpenStackCloudException( + "Creating group {group} failed: Invalid domain " + "{domain}".format(group=name, domain=domain) + ) + domain_id = dom['id'] + + group = self.manager.submitTask(_tasks.GroupCreate( + name=name, description=description, domain=domain_id) + ) + self.list_groups.invalidate(self) + return _utils.normalize_groups([group])[0] + + def update_group(self, name_or_id, name=None, description=None): + """Update an existing group + + :param string name: New group name. + :param string description: New group description. + + :returns: A dict containing the group description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + self.list_groups.invalidate(self) + group = self.get_group(name_or_id) + if group is None: + raise OpenStackCloudException( + "Group {0} not found for updating".format(name_or_id) + ) + + with _utils.shade_exceptions( + "Unable to update group {name}".format(name=name_or_id) + ): + group = self.manager.submitTask(_tasks.GroupUpdate( + group=group['id'], name=name, description=description)) + + self.list_groups.invalidate(self) + return _utils.normalize_groups([group])[0] + + def delete_group(self, name_or_id): + """Delete a group + + :param name_or_id: ID or name of the group to delete. + + :returns: True if delete succeeded, False otherwise. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + group = self.get_group(name_or_id) + if group is None: + self.log.debug( + "Group {0} not found for deleting".format(name_or_id)) + return False + + with _utils.shade_exceptions( + "Unable to delete group {name}".format(name=name_or_id) + ): + self.manager.submitTask(_tasks.GroupDelete(group=group['id'])) + + self.list_groups.invalidate(self) + return True + def list_roles(self): """List Keystone roles. diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 676ff0348..7931e7a73 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -170,3 +170,11 @@ class FakeRole(object): def __init__(self, id, name): self.id = id self.name = name + + +class FakeGroup(object): + def __init__(self, id, name, description, domain=None): + self.id = id + self.name = name + self.description = description + self.domain = domain diff --git a/shade/tests/functional/test_groups.py b/shade/tests/functional/test_groups.py new file mode 100644 index 000000000..1a45dd844 --- /dev/null +++ b/shade/tests/functional/test_groups.py @@ -0,0 +1,105 @@ +# 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_groups +---------------------------------- + +Functional tests for `shade` keystone group resource. +""" + +import shade +from shade.tests import base + + +class TestGroup(base.TestCase): + + def setUp(self): + super(TestGroup, self).setUp() + self.cloud = shade.operator_cloud(cloud='devstack-admin') + if self.cloud.cloud_config.get_api_version('identity') in ('2', '2.0'): + self.skipTest('Identity service does not support groups') + self.group_prefix = self.getUniqueString('group') + self.addCleanup(self._cleanup_groups) + + def _cleanup_groups(self): + exception_list = list() + for group in self.cloud.list_groups(): + if group['name'].startswith(self.group_prefix): + try: + self.cloud.delete_group(group['id']) + except Exception as e: + exception_list.append(str(e)) + continue + + if exception_list: + # Raise an error: we must make users aware that something went + # wrong + raise shade.OpenStackCloudException('\n'.join(exception_list)) + + def test_create_group(self): + group_name = self.group_prefix + '_create' + group = self.cloud.create_group(group_name, 'test group') + + for key in ('id', 'name', 'description', 'domain_id'): + self.assertIn(key, group) + self.assertEqual(group_name, group['name']) + self.assertEqual('test group', group['description']) + + def test_delete_group(self): + group_name = self.group_prefix + '_delete' + + group = self.cloud.create_group(group_name, 'test group') + self.assertIsNotNone(group) + + self.assertTrue(self.cloud.delete_group(group_name)) + + results = self.cloud.search_groups(filters=dict(name=group_name)) + self.assertEqual(0, len(results)) + + def test_delete_group_not_exists(self): + self.assertFalse(self.cloud.delete_group('xInvalidGroupx')) + + def test_search_groups(self): + group_name = self.group_prefix + '_search' + + # Shouldn't find any group with this name yet + results = self.cloud.search_groups(filters=dict(name=group_name)) + self.assertEqual(0, len(results)) + + # Now create a new group + group = self.cloud.create_group(group_name, 'test group') + self.assertEqual(group_name, group['name']) + + # Now we should find only the new group + results = self.cloud.search_groups(filters=dict(name=group_name)) + self.assertEqual(1, len(results)) + self.assertEqual(group_name, results[0]['name']) + + def test_update_group(self): + group_name = self.group_prefix + '_update' + group_desc = 'test group' + + group = self.cloud.create_group(group_name, group_desc) + self.assertEqual(group_name, group['name']) + self.assertEqual(group_desc, group['description']) + + updated_group_name = group_name + '_xyz' + updated_group_desc = group_desc + ' updated' + updated_group = self.cloud.update_group( + group_name, + name=updated_group_name, + description=updated_group_desc) + self.assertEqual(updated_group_name, updated_group['name']) + self.assertEqual(updated_group_desc, updated_group['description']) diff --git a/shade/tests/unit/test_groups.py b/shade/tests/unit/test_groups.py new file mode 100644 index 000000000..e5973b6ea --- /dev/null +++ b/shade/tests/unit/test_groups.py @@ -0,0 +1,64 @@ +# 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 shade +from shade.tests.unit import base +from shade.tests import fakes + + +class TestGroups(base.TestCase): + + def setUp(self): + super(TestGroups, self).setUp() + self.cloud = shade.operator_cloud(validate=False) + + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_list_groups(self, mock_keystone): + self.cloud.list_groups() + mock_keystone.groups.list.assert_called_once_with() + + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_get_group(self, mock_keystone): + self.cloud.get_group('1234') + mock_keystone.groups.list.assert_called_once_with() + + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_delete_group(self, mock_keystone): + mock_keystone.groups.list.return_value = [ + fakes.FakeGroup('1234', 'name', 'desc') + ] + self.assertTrue(self.cloud.delete_group('1234')) + mock_keystone.groups.list.assert_called_once_with() + mock_keystone.groups.delete.assert_called_once_with( + group='1234' + ) + + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_create_group(self, mock_keystone): + self.cloud.create_group('test-group', 'test desc') + mock_keystone.groups.create.assert_called_once_with( + name='test-group', description='test desc', domain=None + ) + + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_update_group(self, mock_keystone): + mock_keystone.groups.list.return_value = [ + fakes.FakeGroup('1234', 'name', 'desc') + ] + self.cloud.update_group('1234', 'test-group', 'test desc') + mock_keystone.groups.list.assert_called_once_with() + mock_keystone.groups.update.assert_called_once_with( + group='1234', name='test-group', description='test desc' + ) From 4a061caf5d0ef655bbcde33d317a9fe14a806c50 Mon Sep 17 00:00:00 2001 From: Alberto Gireud Date: Fri, 6 Nov 2015 16:40:17 -0600 Subject: [PATCH 0635/3836] Fix incorrect variable name Change-Id: I16c4b7394b943fa988c51c315f5c203fea938788 --- shade/openstackcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index d98442b6e..157603ae7 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -455,7 +455,7 @@ def create_project( self, name, description=None, domain_id=None, enabled=True): """Create a project.""" try: - params = self._get_domain_param_dict(domain) + params = self._get_domain_param_dict(domain_id) if self.api_versions['identity'] == '3': params['name'] = name else: From e557b6e4dbc0cf43610dc2a99a55cd80e8d8a3a4 Mon Sep 17 00:00:00 2001 From: Alberto Gireud Date: Sat, 7 Nov 2015 09:05:07 -0600 Subject: [PATCH 0636/3836] Update dated project methods Change-Id: Ifccf7b04168ca45a91a40ace7f33346da893b8d9 --- shade/openstackcloud.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 157603ae7..a05b2f984 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -435,7 +435,7 @@ def update_project(self, name_or_id, description=None, enabled=True): "Project %s not found." % name_or_id) params = {} - if self.api_versions['identity'] == '3': + if self.cloud_config.get_api_version('identity') == '3': params['project'] = proj['id'] else: params['tenant_id'] = proj['id'] @@ -448,7 +448,7 @@ def update_project(self, name_or_id, description=None, enabled=True): raise OpenStackCloudException( "Error in updating project {project}: {message}".format( project=name_or_id, message=str(e))) - self.list_projects.invalidate() + self.list_projects.invalidate(self) return project def create_project( @@ -456,7 +456,7 @@ def create_project( """Create a project.""" try: params = self._get_domain_param_dict(domain_id) - if self.api_versions['identity'] == '3': + if self.cloud_config.get_api_version('identity') == '3': params['name'] = name else: params['tenant_name'] = name @@ -468,14 +468,14 @@ def create_project( raise OpenStackCloudException( "Error in creating project {project}: {message}".format( project=name, message=str(e))) - self.list_projects.invalidate() + self.list_projects.invalidate(self) return project def delete_project(self, name_or_id): try: project = self.update_project(name_or_id, enabled=False) params = {} - if self.api_versions['identity'] == '3': + if self.cloud_config.get_api_version('identity') == '3': params['project'] = project['id'] else: params['tenant'] = project['id'] From ae2bdeda71970b1478ad2aa9eee48987b2cf7199 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Sat, 7 Nov 2015 12:00:51 -0500 Subject: [PATCH 0637/3836] Remove test reference to api_versions API version information comes from occ now. Update the test to get the version from the correct location. This removes the last reference to the old api_versions attribute after change Ifccf7b04168ca45a91a40ace7f33346da893b8d9 merges. This also makes sure that the version calls actually happen with the correct 'identity' parameter. Change-Id: I9b918fa402579fc60319644b1434841fd71ffbcf --- shade/tests/unit/test_domain_params.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/shade/tests/unit/test_domain_params.py b/shade/tests/unit/test_domain_params.py index 83e5960c3..f91483b22 100644 --- a/shade/tests/unit/test_domain_params.py +++ b/shade/tests/unit/test_domain_params.py @@ -63,13 +63,16 @@ def test_identity_params_v2(self, mock_get_project, mock_api_version): self.assertEqual(ret['tenant_id'], 1234) self.assertNotIn('domain', ret) + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, 'get_project') - def test_identity_params_v2_no_domain(self, mock_get_project): + def test_identity_params_v2_no_domain(self, mock_get_project, + mock_api_version): mock_get_project.return_value = munch.Munch(id=1234) - - self.cloud.api_versions = dict(identity='2') + mock_api_version.return_value = '2' ret = self.cloud._get_identity_params(domain_id=None, project='bar') + api_calls = [mock.call('identity'), mock.call('identity')] + mock_api_version.assert_has_calls(api_calls) self.assertIn('tenant_id', ret) self.assertEqual(ret['tenant_id'], 1234) self.assertNotIn('domain', ret) From 9df40b00dcd8f9f99e9ada3ab2156c471fb59b26 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 5 Nov 2015 09:46:50 -0500 Subject: [PATCH 0638/3836] Remove designate support It was broken, thus it was impossible that anyone was actually using it. We do not have a plan for v1/v2 compat support yet, and it's a hard one. SO - rather than leaving a broken thing in the tree which was not possible to use, or fixing it and then having a half-baked thing which people might actually start using, pull it out pending someone stepping up and caring about it. Change-Id: I390494fee8c6c7fb5ddde2ce47e7eb598ad5e08b --- requirements.txt | 1 - shade/_tasks.py | 20 ----------- shade/openstackcloud.py | 63 ---------------------------------- shade/tests/unit/test_shade.py | 16 --------- 4 files changed, 100 deletions(-) diff --git a/requirements.txt b/requirements.txt index cd8f14de8..c69d12e97 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,6 @@ python-neutronclient>=2.3.10 python-troveclient>=1.2.0 python-ironicclient>=0.10.0 python-swiftclient>=2.5.0 -python-designateclient>=1.3.0 python-heatclient>=0.3.0 dogpile.cache>=0.5.3 diff --git a/shade/_tasks.py b/shade/_tasks.py index 55b71af9a..54be6d677 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -647,26 +647,6 @@ def main(self, client): return client.keystone_client.groups.update(**self.args) -class ZoneList(task_manager.Task): - def main(self, client): - return client.designate_client.domains.list() - - -class ZoneGet(task_manager.Task): - def main(self, client): - return client.designate_client.domains.get(**self.args) - - -class RecordList(task_manager.Task): - def main(self, client): - return client.designate_client.records.list(**self.args) - - -class RecordGet(task_manager.Task): - def main(self, client): - return client.designate_client.records.get(**self.args) - - class RoleList(task_manager.Task): def main(self, client): return client.keystone_client.roles.list() diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index a05b2f984..4eb810fee 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -24,7 +24,6 @@ import requestsexceptions from cinderclient.v1 import client as cinder_client -from designateclient.v1 import Client as designate_client import glanceclient import glanceclient.exc from glanceclient.common import utils as glance_utils @@ -219,7 +218,6 @@ def invalidate(self): self._keystone_session = None self._cinder_client = None - self._designate_client = None self._glance_client = None self._glance_endpoint = None self._heat_client = None @@ -722,13 +720,6 @@ def neutron_client(self): 'network', neutron_client.Client, pass_version_arg=False) return self._neutron_client - @property - def designate_client(self): - if self._designate_client is None: - self._designate_client = self._get_client( - 'dns', designate_client.Client) - return self._designate_client - def create_stack( self, name, template_file=None, template_url=None, @@ -987,14 +978,6 @@ def search_floating_ips(self, id=None, filters=None): floating_ips = self.list_floating_ips() return _utils._filter_list(floating_ips, id, filters) - def search_zones(self, name_or_id=None, filters=None): - zones = self.list_zones() - return _utils._filter_list(zones, name_or_id, filters) - - def _search_records(self, zone_id, name_or_id=None, filters=None): - records = self._list_records(zone_id=zone_id) - return _utils._filter_list(records, name_or_id, filters) - def search_stacks(self, name_or_id=None, filters=None): """Search Heat stacks. @@ -1295,26 +1278,6 @@ def _nova_list_floating_ips(self): raise OpenStackCloudException( "error fetching floating IPs list: {msg}".format(msg=str(e))) - def list_zones(self): - """List all available DNS zones. - - :returns: A list of zone dicts. - - """ - try: - return self.manager.submitTask(_tasks.ZoneList()) - except Exception as e: - raise OpenStackCloudException( - "Error fetching zone list: %s" % e) - - def _list_records(self, zone_id): - # TODO(mordred) switch domain= to zone= after the Big Rename - try: - return self.manager.submitTask(_tasks.RecordList(domain=zone_id)) - except Exception as e: - raise OpenStackCloudException( - "Error fetching record list: %s" % e) - def use_external_network(self): return self._use_external_network @@ -1623,32 +1586,6 @@ def get_floating_ip(self, id, filters=None): """ return _utils._get_entity(self.search_floating_ips, id, filters) - def get_zone(self, name_or_id, filters=None): - """Get a DNS zone by name or ID. - - :param name_or_id: Name or ID of the DNS zone. - :param dict filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - :returns: A zone dict or None if no matching DNS zone is - found. - - """ - return _utils._get_entity(self.search_zones, name_or_id, filters) - - def _get_record(self, zone_id, name_or_id, filters=None): - f = lambda name_or_id, filters: self._search_records( - zone_id, name_or_id, filters) - return _utils._get_entity(f, name_or_id, filters) - def get_stack(self, name_or_id, filters=None): """Get exactly one Heat stack. diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 3cd61cc91..afe1a59bb 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -384,22 +384,6 @@ def test__neutron_exceptions_generic(self): self.assertRaises(exc.OpenStackCloudException, self.cloud.list_networks) - @mock.patch.object(shade.OpenStackCloud, 'list_zones') - def test_get_zone(self, mock_search): - zone1 = dict(id='123', name='mickey') - mock_search.return_value = [zone1] - r = self.cloud.get_zone('mickey') - self.assertIsNotNone(r) - self.assertDictEqual(zone1, r) - - @mock.patch.object(shade.OpenStackCloud, '_list_records') - def test_get_record(self, mock_search): - record1 = dict(id='123', name='mickey', domain_id='mickey.domain') - mock_search.return_value = [record1] - r = self.cloud._get_record('mickey.domain', 'mickey') - self.assertIsNotNone(r) - self.assertDictEqual(record1, r) - @mock.patch.object(shade._tasks.ServerList, 'main') def test_list_servers(self, mock_serverlist): '''This test verifies that calling list_servers results in a call From f7ca875777e5f33387b84934cd5951c221e24b95 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 1 Nov 2015 19:30:41 -0500 Subject: [PATCH 0639/3836] Let os-client-config handle session creation More things than shade need consistent Session creation, so we moved a copy of the shade code into os-client-config to allow python-*client to consume the same logic. Now that it's there, consume it in shade and remove the need for shade to know/duplicate the logic. Depends-On: Ide4c613cc143a0b8a3f36130989b57e808b2530f Change-Id: I86cdb3cdd2710ef302520184ccfcb1605384f706 --- requirements.txt | 2 +- shade/openstackcloud.py | 121 ++++------------------- shade/tests/unit/test_object.py | 29 +++--- shade/tests/unit/test_operator_noauth.py | 18 ++-- shade/tests/unit/test_shade.py | 53 +++------- shade/tests/unit/test_shade_operator.py | 45 +++++---- 6 files changed, 84 insertions(+), 184 deletions(-) diff --git a/requirements.txt b/requirements.txt index c69d12e97..53190d185 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ munch decorator jsonpatch ipaddress -os-client-config>=1.9.0 +os-client-config>=1.10.1 requestsexceptions>=1.1.1 six diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 4eb810fee..1b95e12c2 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -26,15 +26,11 @@ from cinderclient.v1 import client as cinder_client import glanceclient import glanceclient.exc -from glanceclient.common import utils as glance_utils from heatclient import client as heat_client from heatclient.common import template_utils import keystoneauth1.exceptions -from keystoneauth1 import plugin as ksa_plugin -from keystoneauth1 import session as ksa_session -from keystoneclient.v2_0 import client as k2_client -from keystoneclient.v3 import client as k3_client -from neutronclient.v2_0 import client as neutron_client +from keystoneclient import client as keystone_client +from neutronclient.neutron import client as neutron_client from novaclient import client as nova_client from novaclient import exceptions as nova_exceptions import swiftclient.client as swift_client @@ -209,8 +205,10 @@ def invalidate(self): # If server expiration time is set explicitly, use that. Otherwise # fall back to whatever it was before - self._SERVER_LIST_AGE = cache_expiration.get( - 'server', self._SERVER_LIST_AGE) + # TODO(mordred) replace with get_cache_resource_expiration once + # it has a release with default value + self._SERVER_LIST_AGE = int(cache_expiration.get( + 'server', self._SERVER_LIST_AGE)) self._container_cache = dict() self._file_hash_cache = dict() @@ -250,23 +248,13 @@ def generate_key(*args, **kwargs): return generate_key def _get_client( - self, service_key, client_class, interface_key='endpoint_type', + self, service_key, client_class, interface_key=None, pass_version_arg=True, **kwargs): try: - interface = self.cloud_config.get_interface(service_key) - # trigger exception on lack of service - self.get_session_endpoint(service_key) - constructor_args = dict( - session=self.keystone_session, - service_name=self.cloud_config.get_service_name(service_key), - service_type=self.cloud_config.get_service_type(service_key), - region_name=self.region_name) - constructor_args.update(kwargs) - constructor_args[interface_key] = interface - if pass_version_arg: - version = self.cloud_config.get_api_version(service_key) - constructor_args['version'] = version - client = client_class(**constructor_args) + client = self.cloud_config.get_legacy_client( + service_key=service_key, client_class=client_class, + interface_key=interface_key, pass_version_arg=pass_version_arg, + **kwargs) except Exception: self.log.debug( "Couldn't construct {service} object".format( @@ -286,31 +274,11 @@ def nova_client(self): 'compute', nova_client.Client) return self._nova_client - def _get_identity_client_class(self): - if self.cloud_config.get_api_version('identity') == '3': - return k3_client.Client - elif self.cloud_config.get_api_version('identity') in ('2', '2.0'): - return k2_client.Client - raise OpenStackCloudException( - "Unknown identity API version: {version}".format( - version=self.cloud_config.get_api_version('identity'))) - @property def keystone_session(self): if self._keystone_session is None: - try: - keystone_auth = self.cloud_config.get_auth() - if not keystone_auth: - raise OpenStackCloudException( - "Problem with auth parameters") - self._keystone_session = ksa_session.Session( - auth=keystone_auth, - verify=self.verify, - cert=self.cert, - timeout=self.api_timeout) - except OpenStackCloudException: - raise + self._keystone_session = self.cloud_config.get_session() except Exception as e: raise OpenStackCloudException( "Error authenticating to keystone: %s " % str(e)) @@ -320,7 +288,7 @@ def keystone_session(self): def keystone_client(self): if self._keystone_client is None: self._keystone_client = self._get_client( - 'identity', self._get_identity_client_class()) + 'identity', keystone_client.Client) return self._keystone_client @property @@ -614,13 +582,8 @@ def delete_user(self, name_or_id): @property def glance_client(self): if self._glance_client is None: - endpoint, version = glance_utils.strip_version( - self.get_session_endpoint(service_key='image')) - # TODO(mordred): Put check detected vs. configured version - # and warn if they're different. self._glance_client = self._get_client( - 'image', glanceclient.Client, interface_key='interface', - endpoint=endpoint) + 'image', glanceclient.Client) return self._glance_client @property @@ -644,25 +607,8 @@ def get_template_contents( @property def swift_client(self): if self._swift_client is None: - try: - token = self.keystone_session.get_token() - endpoint = self.get_session_endpoint( - service_key='object-store') - self._swift_client = swift_client.Connection( - preauthurl=endpoint, - preauthtoken=token, - auth_version=self.cloud_config.get_api_version('identity'), - os_options=dict( - auth_token=token, - object_storage_url=endpoint, - region_name=self.region_name), - timeout=self.api_timeout, - ) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Error constructing swift client: %s", str(e)) + self._swift_client = self._get_client( + 'object-store', swift_client.Connection) return self._swift_client @property @@ -694,21 +640,6 @@ def cinder_client(self): @property def trove_client(self): if self._trove_client is None: - self.get_session_endpoint(service_key='database') - # Make the connection - can't use keystone session until there - # is one - self._trove_client = trove_client.Client( - self.cloud_config.get_api_version('database'), - session=self.keystone_session, - region_name=self.region_name, - service_type=self.cloud_config.get_service_type('database'), - ) - - if self._trove_client is None: - raise OpenStackCloudException( - "Failed to instantiate Trove client." - " This could mean that your credentials are wrong.") - self._trove_client = self._get_client( 'database', trove_client.Client) return self._trove_client @@ -717,7 +648,7 @@ def trove_client(self): def neutron_client(self): if self._neutron_client is None: self._neutron_client = self._get_client( - 'network', neutron_client.Client, pass_version_arg=False) + 'network', neutron_client.Client) return self._neutron_client def create_stack( @@ -808,22 +739,8 @@ def get_flavor_by_ram(self, ram, include=None): ram=ram, include=include)) def get_session_endpoint(self, service_key): - override_endpoint = self.cloud_config.get_endpoint(service_key) - if override_endpoint: - return override_endpoint try: - # keystone is a special case in keystone, because what? - if service_key == 'identity': - endpoint = self.keystone_session.get_endpoint( - interface=ksa_plugin.AUTH_INTERFACE) - else: - endpoint = self.keystone_session.get_endpoint( - service_type=self.cloud_config.get_service_type( - service_key), - service_name=self.cloud_config.get_service_name( - service_key), - interface=self.cloud_config.get_interface(service_key), - region_name=self.region_name) + return self.cloud_config.get_session_endpoint(service_key) except keystoneauth1.exceptions.catalog.EndpointNotFound as e: self.log.debug( "Endpoint not found in %s cloud: %s", self.name, str(e)) @@ -843,7 +760,7 @@ def get_session_endpoint(self, service_key): def has_service(self, service_key): if not self.cloud_config.config.get('has_%s' % service_key, True): self.log.debug( - "Overriding {service_key} entry in catalog per config".format( + "Disabling {service_key} entry in catalog per config".format( service_key=service_key)) return False try: diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index a31252897..a3749be51 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -15,6 +15,7 @@ import mock import os_client_config +from os_client_config import cloud_config from swiftclient import client as swift_client from swiftclient import service as swift_service from swiftclient import exceptions as swift_exc @@ -35,15 +36,13 @@ def setUp(self): cloud_config=config.get_one_cloud(validate=False)) @mock.patch.object(swift_client, 'Connection') - @mock.patch.object(shade.OpenStackCloud, 'keystone_session', - new_callable=mock.PropertyMock) - @mock.patch.object(shade.OpenStackCloud, 'get_session_endpoint') - def test_swift_client(self, endpoint_mock, session_mock, swift_mock): - endpoint_mock.return_value = 'danzig' - session = mock.MagicMock() - session.get_token = mock.MagicMock() - session.get_token.return_value = 'yankee' - session_mock.return_value = session + @mock.patch.object(cloud_config.CloudConfig, 'get_session') + def test_swift_client(self, get_session_mock, swift_mock): + session_mock = mock.Mock() + session_mock.get_endpoint.return_value = 'danzig' + session_mock.get_token.return_value = 'yankee' + get_session_mock.return_value = session_mock + self.cloud.swift_client swift_mock.assert_called_with( preauthurl='danzig', @@ -55,15 +54,15 @@ def test_swift_client(self, endpoint_mock, session_mock, swift_mock): auth_token='yankee', region_name='')) - @mock.patch.object(shade.OpenStackCloud, 'keystone_session', - new_callable=mock.PropertyMock) - @mock.patch.object(shade.OpenStackCloud, 'get_session_endpoint') - def test_swift_client_no_endpoint(self, endpoint_mock, session_mock): - endpoint_mock.side_effect = KeyError + @mock.patch.object(cloud_config.CloudConfig, 'get_session') + def test_swift_client_no_endpoint(self, get_session_mock): + session_mock = mock.Mock() + session_mock.get_endpoint.return_value = None + get_session_mock.return_value = session_mock e = self.assertRaises( exc.OpenStackCloudException, lambda: self.cloud.swift_client) self.assertIn( - 'Error constructing swift client', str(e)) + 'Failed to instantiate object-store client.', str(e)) @mock.patch.object(shade.OpenStackCloud, 'auth_token') @mock.patch.object(shade.OpenStackCloud, 'get_session_endpoint') diff --git a/shade/tests/unit/test_operator_noauth.py b/shade/tests/unit/test_operator_noauth.py index ca5a34730..37b760f3f 100644 --- a/shade/tests/unit/test_operator_noauth.py +++ b/shade/tests/unit/test_operator_noauth.py @@ -15,6 +15,7 @@ import mock import ironicclient +from os_client_config import cloud_config import shade from shade.tests import base @@ -35,12 +36,10 @@ def setUp(self): validate=False, ) - @mock.patch.object(shade.OpenStackCloud, 'keystone_session', - new_callable=mock.PropertyMock) - @mock.patch.object(shade.OperatorCloud, 'get_session_endpoint') + @mock.patch.object(cloud_config.CloudConfig, 'get_session') @mock.patch.object(ironicclient.client, 'Client') def test_ironic_noauth_selection_using_a_task( - self, mock_client, mock_endpoint, session_mock): + self, mock_client, get_session_mock): """Test noauth selection for Ironic in OperatorCloud Utilize a task to trigger the client connection attempt @@ -50,10 +49,11 @@ def test_ironic_noauth_selection_using_a_task( We want session_endpoint to be called because we're storing the endpoint in a noauth token Session object now. """ - session = mock.MagicMock() - session.get_token = mock.MagicMock() - session.get_token.return_value = 'yankee' - session_mock.return_value = session + session_mock = mock.Mock() + session_mock.get_endpoint.return_value = None + session_mock.get_token.return_value = 'yankee' + get_session_mock.return_value = session_mock + self.cloud_noauth.patch_machine('name', {}) - self.assertTrue(mock_endpoint.called) + self.assertTrue(get_session_mock.called) self.assertTrue(mock_client.called) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index afe1a59bb..f184dd839 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -17,11 +17,9 @@ import glanceclient from heatclient import client as heat_client -from keystoneclient.v2_0 import client as k2_client -from keystoneclient.v3 import client as k3_client from neutronclient.common import exceptions as n_exc -import os_client_config.cloud_config +from os_client_config import cloud_config import shade from shade import exc from shade import meta @@ -37,33 +35,6 @@ def setUp(self): def test_openstack_cloud(self): self.assertIsInstance(self.cloud, shade.OpenStackCloud) - @mock.patch.object( - os_client_config.cloud_config.CloudConfig, 'get_api_version') - def test_get_client_v2(self, mock_api_version): - mock_api_version.return_value = '2' - - self.assertIs( - self.cloud._get_identity_client_class(), - k2_client.Client) - - @mock.patch.object( - os_client_config.cloud_config.CloudConfig, 'get_api_version') - def test_get_client_v3(self, mock_api_version): - mock_api_version.return_value = '3' - - self.assertIs( - self.cloud._get_identity_client_class(), - k3_client.Client) - - @mock.patch.object( - os_client_config.cloud_config.CloudConfig, 'get_api_version') - def test_get_client_v4(self, mock_api_version): - mock_api_version.return_value = '4' - - self.assertRaises( - exc.OpenStackCloudException, - self.cloud._get_identity_client_class) - @mock.patch.object(shade.OpenStackCloud, 'search_images') def test_get_images(self, mock_search): image1 = dict(id='123', name='mickey') @@ -98,33 +69,35 @@ def test_list_servers_exception(self, mock_client): self.assertRaises(exc.OpenStackCloudException, self.cloud.list_servers) - @mock.patch.object(shade.OpenStackCloud, 'get_session_endpoint') - @mock.patch.object(shade.OpenStackCloud, 'keystone_session') + @mock.patch.object(cloud_config.CloudConfig, 'get_session') @mock.patch.object(glanceclient, 'Client') def test_glance_args( - self, mock_client, mock_keystone_session, mock_endpoint): - mock_keystone_session.return_value = None - mock_endpoint.return_value = 'http://example.com/v2' + self, mock_client, get_session_mock): + session_mock = mock.Mock() + session_mock.get_endpoint.return_value = 'http://example.com/v2' + get_session_mock.return_value = session_mock self.cloud.glance_client mock_client.assert_called_with( + '2', endpoint='http://example.com', - version='2', region_name='', service_name=None, + region_name='', service_name=None, interface='public', service_type='image', session=mock.ANY, ) - @mock.patch.object(shade.OpenStackCloud, 'keystone_session') + @mock.patch.object(cloud_config.CloudConfig, 'get_session') @mock.patch.object(heat_client, 'Client') - def test_heat_args(self, mock_client, mock_keystone_session): - mock_keystone_session.return_value = None + def test_heat_args(self, mock_client, get_session_mock): + session_mock = mock.Mock() + get_session_mock.return_value = session_mock self.cloud.heat_client mock_client.assert_called_with( + '1', endpoint_type='public', region_name='', service_name=None, service_type='orchestration', session=mock.ANY, - version='1' ) @mock.patch.object(shade.OpenStackCloud, 'search_subnets') diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index 4a6382822..a7fe3ee9d 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -12,12 +12,12 @@ # License for the specific language governing permissions and limitations # under the License. -from keystoneauth1 import plugin as ksc_plugin +from keystoneauth1 import plugin as ksa_plugin import mock import testtools -import os_client_config.cloud_config +from os_client_config import cloud_config import shade import munch from shade import exc @@ -992,27 +992,30 @@ class Image(object): self.assertEqual('22', self.cloud.get_image_id('22')) self.assertEqual('22', self.cloud.get_image_id('22 name')) - @mock.patch.object( - os_client_config.cloud_config.CloudConfig, 'get_endpoint') + @mock.patch.object(cloud_config.CloudConfig, 'get_endpoint') def test_get_session_endpoint_provided(self, fake_get_endpoint): fake_get_endpoint.return_value = 'http://fake.url' self.assertEqual( 'http://fake.url', self.cloud.get_session_endpoint('image')) - @mock.patch.object(shade.OpenStackCloud, 'keystone_session') - def test_get_session_endpoint_session(self, session_mock): + @mock.patch.object(cloud_config.CloudConfig, 'get_session') + def test_get_session_endpoint_session(self, get_session_mock): + session_mock = mock.Mock() session_mock.get_endpoint.return_value = 'http://fake.url' + get_session_mock.return_value = session_mock self.assertEqual( 'http://fake.url', self.cloud.get_session_endpoint('image')) - @mock.patch.object(shade.OpenStackCloud, 'keystone_session') - def test_get_session_endpoint_exception(self, session_mock): + @mock.patch.object(cloud_config.CloudConfig, 'get_session') + def test_get_session_endpoint_exception(self, get_session_mock): class FakeException(Exception): pass def side_effect(*args, **kwargs): raise FakeException("No service") + session_mock = mock.Mock() session_mock.get_endpoint.side_effect = side_effect + get_session_mock.return_value = session_mock self.cloud.name = 'testcloud' self.cloud.region_name = 'testregion' with testtools.ExpectedException( @@ -1021,26 +1024,34 @@ def side_effect(*args, **kwargs): " No service"): self.cloud.get_session_endpoint("image") - @mock.patch.object(shade.OpenStackCloud, 'keystone_session') - def test_get_session_endpoint_unavailable(self, session_mock): + @mock.patch.object(cloud_config.CloudConfig, 'get_session') + def test_get_session_endpoint_unavailable(self, get_session_mock): + session_mock = mock.Mock() session_mock.get_endpoint.return_value = None + get_session_mock.return_value = session_mock image_endpoint = self.cloud.get_session_endpoint("image") self.assertIsNone(image_endpoint) - @mock.patch.object(shade.OpenStackCloud, 'keystone_session') - def test_get_session_endpoint_identity(self, session_mock): + @mock.patch.object(cloud_config.CloudConfig, 'get_session') + def test_get_session_endpoint_identity(self, get_session_mock): + session_mock = mock.Mock() + get_session_mock.return_value = session_mock self.cloud.get_session_endpoint('identity') session_mock.get_endpoint.assert_called_with( - interface=ksc_plugin.AUTH_INTERFACE) + interface=ksa_plugin.AUTH_INTERFACE) - @mock.patch.object(shade.OpenStackCloud, 'keystone_session') - def test_has_service_no(self, session_mock): + @mock.patch.object(cloud_config.CloudConfig, 'get_session') + def test_has_service_no(self, get_session_mock): + session_mock = mock.Mock() session_mock.get_endpoint.return_value = None + get_session_mock.return_value = session_mock self.assertFalse(self.cloud.has_service("image")) - @mock.patch.object(shade.OpenStackCloud, 'keystone_session') - def test_has_service_yes(self, session_mock): + @mock.patch.object(cloud_config.CloudConfig, 'get_session') + def test_has_service_yes(self, get_session_mock): + session_mock = mock.Mock() session_mock.get_endpoint.return_value = 'http://fake.url' + get_session_mock.return_value = session_mock self.assertTrue(self.cloud.has_service("image")) @mock.patch.object(shade._tasks.HypervisorList, 'main') From 59ad0ed00bfa485b5f0d24f5edb4e5b20cc7603a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 8 Nov 2015 18:21:46 -0500 Subject: [PATCH 0640/3836] Add default API version for magnum service shade is adding magnum support, so we should probably grow a default value for the container_api_version. Change-Id: I2551f2921f10ba109ccb52301cc8ad23c5f81c46 --- os_client_config/defaults.json | 1 + 1 file changed, 1 insertion(+) diff --git a/os_client_config/defaults.json b/os_client_config/defaults.json index d88ad0488..d1b66a318 100644 --- a/os_client_config/defaults.json +++ b/os_client_config/defaults.json @@ -1,6 +1,7 @@ { "auth_type": "password", "baremetal_api_version": "1", + "container_api_version": "1", "compute_api_version": "2", "database_api_version": "1.0", "disable_vendor_agent": {}, From ce7d716ffc5867d6cb7f73cbca6bbc4470499fb9 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 8 Nov 2015 18:24:24 -0500 Subject: [PATCH 0641/3836] Remove unneeded workaround for ksc It turns out keystoneclient can take string input and does the same transform we're doing here, it's just not documented. Remove the workaround on our side as it's unneccesary. Change-Id: Ie46945f7d96e3d65004cd19823b3be989e1d18a7 --- os_client_config/cloud_config.py | 6 +----- os_client_config/tests/test_cloud_config.py | 4 ++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 37029f1bb..30dc27f6a 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -259,15 +259,11 @@ def get_legacy_client( if pass_version_arg: version = self.get_api_version(service_key) if service_key == 'identity': - # keystoneclient takes version as a tuple. - version = tuple(str(float(version)).split('.')) - constructor_kwargs['version'] = version # Workaround for bug#1513839 if 'endpoint' not in constructor_kwargs: endpoint = self.get_session_endpoint('identity') constructor_kwargs['endpoint'] = endpoint - else: - constructor_args.append(version) + constructor_args.append(version) return client_class(*constructor_args, **constructor_kwargs) diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index deaec4d35..1b98b8aee 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -289,7 +289,7 @@ def test_legacy_client_identity(self, mock_get_session_endpoint): "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('identity', mock_client) mock_client.assert_called_with( - version=('2', '0'), + '2.0', endpoint='http://example.com/v2', endpoint_type='admin', region_name='region-al', @@ -308,7 +308,7 @@ def test_legacy_client_identity_v3(self, mock_get_session_endpoint): "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('identity', mock_client) mock_client.assert_called_with( - version=('3', '0'), + '3', endpoint='http://example.com', endpoint_type='admin', region_name='region-al', From c90de1f691a27d4f434d948f1b77db02c23162a4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 9 Nov 2015 09:07:06 -0500 Subject: [PATCH 0642/3836] Workaround for int value with verbose_level python-openstackclient uses an int value for verbose level which the stringification patch broke. As a quick fix, special case verbose_level, as fixing it properly might take more than 5 minutes. Change-Id: Ie12a40d3d3e400b3ec2103d7a58c4902fb10fc2d Closes-Bug: #1513919 --- os_client_config/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 8d2a2ee76..f439d5f90 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -237,7 +237,7 @@ def _normalize_keys(self, config): new_config[key] = self._normalize_keys(value) elif isinstance(value, bool): new_config[key] = value - elif isinstance(value, int): + elif isinstance(value, int) and key != 'verbose_level': new_config[key] = str(value) elif isinstance(value, float): new_config[key] = str(value) From 397da54db11b5a5506c24dc9af60a34665bbd953 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 9 Nov 2015 09:43:09 -0500 Subject: [PATCH 0643/3836] Workaround a dispute between osc and neutronclient python-openstackclient wants network_api_version to be 2. neutronclient wants it to be 2.0. There is a patch to OSC to make it understand both, but in the mean time, let's unbreak people. Change-Id: I4d8f187d1302c5bcfa246e017e6c6d8af9c3f733 --- os_client_config/cloud_config.py | 4 ++++ os_client_config/defaults.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 37029f1bb..bace53ef7 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -258,6 +258,10 @@ def get_legacy_client( constructor_args = [] if pass_version_arg: version = self.get_api_version(service_key) + # Temporary workaround while we wait for python-openstackclient + # to be able to handle 2.0 which is what neutronclient expects + if service_key == 'network' and version == '2': + version = '2.0' if service_key == 'identity': # keystoneclient takes version as a tuple. version = tuple(str(float(version)).split('.')) diff --git a/os_client_config/defaults.json b/os_client_config/defaults.json index d88ad0488..9239d0fe8 100644 --- a/os_client_config/defaults.json +++ b/os_client_config/defaults.json @@ -11,7 +11,7 @@ "image_api_use_tasks": false, "image_api_version": "2", "image_format": "qcow2", - "network_api_version": "2.0", + "network_api_version": "2", "object_api_version": "1", "orchestration_api_version": "1", "secgroup_source": "neutron", From a71db08bc71308bb54ba7a715048767242434d0f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 9 Nov 2015 10:36:31 -0500 Subject: [PATCH 0644/3836] Plumb fixed_address through add_ips_to_server Specifying a fixed_address if you know it is somethig that the lower level functions understand, but was left out of the argument list for add_ips_to_server. Plumb it through. Change-Id: Iba3456f0f45e5b0230c752a50fc3d3b29e8ade54 --- shade/openstackcloud.py | 18 +++++++++++++----- shade/tests/unit/test_floating_ip_common.py | 5 +++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 4eb810fee..0dc62435b 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3024,7 +3024,9 @@ def _add_ip_from_pool( server=server, floating_ip=f_ip, fixed_address=fixed_address, wait=wait, timeout=timeout) - def add_ip_list(self, server, ips, wait=False, timeout=60): + def add_ip_list( + self, server, ips, wait=False, timeout=60, + fixed_address=None): """Attach a list of IPs to a server. :param server: a server object @@ -3033,6 +3035,8 @@ def add_ip_list(self, server, ips, wait=False, timeout=60): to the server in Nova. Defaults to False. :param timeout: (optional) Seconds to wait, defaults to 60. See the ``wait`` parameter. + :param fixed_address: (optional) Fixed address of the server to + attach the IP to :returns: The updated server dict @@ -3045,7 +3049,8 @@ def add_ip_list(self, server, ips, wait=False, timeout=60): f_ip = self.get_floating_ip( id=None, filters={'floating_ip_address': ip}) return self._attach_ip_to_server( - server=server, floating_ip=f_ip, wait=wait, timeout=timeout) + server=server, floating_ip=f_ip, wait=wait, timeout=timeout, + fixed_address=fixed_address) def add_auto_ip(self, server, wait=False, timeout=60, reuse=True): """Add a floating IP to a server. @@ -3090,12 +3095,15 @@ def _add_auto_ip(self, server, wait=False, timeout=60, reuse=True): def add_ips_to_server( self, server, auto_ip=True, ips=None, ip_pool=None, - wait=False, timeout=60, reuse=True): + wait=False, timeout=60, reuse=True, fixed_address=None): if ip_pool: server = self._add_ip_from_pool( - server, ip_pool, reuse=reuse, wait=wait, timeout=timeout) + server, ip_pool, reuse=reuse, wait=wait, timeout=timeout, + fixed_address=fixed_address) elif ips: - server = self.add_ip_list(server, ips, wait=wait, timeout=timeout) + server = self.add_ip_list( + server, ips, wait=wait, timeout=timeout, + fixed_address=fixed_address) elif auto_ip: if not self.get_server_public_ip(server): server = self._add_auto_ip( diff --git a/shade/tests/unit/test_floating_ip_common.py b/shade/tests/unit/test_floating_ip_common.py index 4a49b5caa..f54493171 100644 --- a/shade/tests/unit/test_floating_ip_common.py +++ b/shade/tests/unit/test_floating_ip_common.py @@ -76,7 +76,8 @@ def test_add_ips_to_server_pool( self.client.add_ips_to_server(server_dict, ip_pool=pool) mock_add_ip_from_pool.assert_called_with( - server_dict, pool, reuse=True, wait=False, timeout=60) + server_dict, pool, reuse=True, wait=False, timeout=60, + fixed_address=None) @patch.object(OpenStackCloud, 'nova_client') @patch.object(OpenStackCloud, 'add_ip_list') @@ -92,7 +93,7 @@ def test_add_ips_to_server_ip_list( self.client.add_ips_to_server(server_dict, ips=ips) mock_add_ip_list.assert_called_with( - server_dict, ips, wait=False, timeout=60) + server_dict, ips, wait=False, timeout=60, fixed_address=None) @patch.object(OpenStackCloud, 'nova_client') @patch.object(OpenStackCloud, '_add_auto_ip') From ef0ccaaef4e31a1fbbfef60a17ff81d24865edbb Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 9 Nov 2015 15:50:28 -0500 Subject: [PATCH 0645/3836] Pull server list cache setting via API To ensure that we get numbers and not strings from OCC for cache settings, we need to use the actual API for that, not direct data introspection. Change-Id: I457c624ebfcda6578417fb977ea3742a63507344 --- shade/openstackcloud.py | 7 ++----- shade/tests/unit/test_caching.py | 3 +++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 0fbbec516..063d5d6df 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -169,7 +169,6 @@ def __init__( cache_expiration_time = int(cloud_config.get_cache_expiration_time()) cache_class = cloud_config.get_cache_class() cache_arguments = cloud_config.get_cache_arguments() - cache_expiration = cloud_config.get_cache_expiration() if cache_class != 'dogpile.cache.null': self._cache = cache.make_region( @@ -205,10 +204,8 @@ def invalidate(self): # If server expiration time is set explicitly, use that. Otherwise # fall back to whatever it was before - # TODO(mordred) replace with get_cache_resource_expiration once - # it has a release with default value - self._SERVER_LIST_AGE = int(cache_expiration.get( - 'server', self._SERVER_LIST_AGE)) + self._SERVER_LIST_AGE = cloud_config.get_cache_resource_expiration( + 'server', self._SERVER_LIST_AGE) self._container_cache = dict() self._file_hash_cache = dict() diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 5715b7132..a637c7994 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -33,6 +33,9 @@ class TestMemoryCache(base.TestCase): { 'max_age': 90, 'class': 'dogpile.cache.memory', + 'expiration': { + 'server': 1, + }, }, 'clouds': { From 105ba778a2cacc3e46ae2fb929ac9450623ae5ec Mon Sep 17 00:00:00 2001 From: matthew wagoner Date: Mon, 9 Nov 2015 11:55:13 -0500 Subject: [PATCH 0646/3836] Add new context manager for shade exceptions, cont. again This commit continues the work of change Ic39b6e3efc33b7a30f298a3daca6d6c16ab4ca89 Shade has developed a common pattern within many of the API methods where we try to avoid re-wrapping OpenStackClientException exceptions by catching them and simply re-raising. We can eliminate this duplicate exception handling with a context manager. Change-Id: Ic714673e842847c987166fe4a7bf43965dd4d8a9 --- shade/openstackcloud.py | 301 +++++++++++++--------------------------- 1 file changed, 96 insertions(+), 205 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 0fbbec516..cd78a0e72 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -394,7 +394,9 @@ def get_project(self, name_or_id, filters=None): return _utils._get_entity(self.search_projects, name_or_id, filters) def update_project(self, name_or_id, description=None, enabled=True): - try: + with _utils.shade.exceptions( + "Error in updating project {project}".format( + project=name_or_id)): proj = self.get_project(name_or_id) if not proj: raise OpenStackCloudException( @@ -410,17 +412,14 @@ def update_project(self, name_or_id, description=None, enabled=True): description=description, enabled=enabled, **params)) - except Exception as e: - raise OpenStackCloudException( - "Error in updating project {project}: {message}".format( - project=name_or_id, message=str(e))) self.list_projects.invalidate(self) return project def create_project( self, name, description=None, domain_id=None, enabled=True): """Create a project.""" - try: + with _utils.shade_exceptions( + "Error in creating project {project}".format(project=name)): params = self._get_domain_param_dict(domain_id) if self.cloud_config.get_api_version('identity') == '3': params['name'] = name @@ -430,15 +429,13 @@ def create_project( project = self.manager.submitTask(_tasks.ProjectCreate( project_name=name, description=description, enabled=enabled, **params)) - except Exception as e: - raise OpenStackCloudException( - "Error in creating project {project}: {message}".format( - project=name, message=str(e))) self.list_projects.invalidate(self) return project def delete_project(self, name_or_id): - try: + with _utils.shade_exceptions( + "Error in deleting project {project}".format( + project=name_or_id)): project = self.update_project(name_or_id, enabled=False) params = {} if self.cloud_config.get_api_version('identity') == '3': @@ -446,10 +443,6 @@ def delete_project(self, name_or_id): else: params['tenant'] = project['id'] self.manager.submitTask(_tasks.ProjectDelete(**params)) - except Exception as e: - raise OpenStackCloudException( - "Error in deleting project {project}: {message}".format( - project=name_or_id, message=str(e))) @_utils.cache_on_arguments() def list_users(self): @@ -460,12 +453,8 @@ def list_users(self): :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ - try: + with _utils.shade_exceptions("Failed to list users"): users = self.manager.submitTask(_tasks.UserList()) - except Exception as e: - raise OpenStackCloudException( - "Failed to list users: {0}".format(str(e)) - ) return _utils.normalize_users(users) def search_users(self, name_or_id=None, filters=None): @@ -503,12 +492,10 @@ def get_user_by_id(self, user_id, normalize=True): :returns: a single dict containing the user description """ - try: + with _utils.shade_exceptions( + "Error getting user with ID {user_id}".format( + user_id=user_id)): user = self.manager.submitTask(_tasks.UserGet(user=user_id)) - except Exception as e: - raise OpenStackCloudException( - "Error getting user with ID {user_id}: {message}".format( - user_id=user_id, message=str(e))) if user and normalize: return _utils.normalize_users([user])[0] return user @@ -534,12 +521,9 @@ def update_user(self, name_or_id, **kwargs): # to be domain. kwargs['domain'] = kwargs.pop('domain_id') - try: + with _utils.shade_exceptions("Error in updating user {user}".format( + user=name_or_id)): user = self.manager.submitTask(_tasks.UserUpdate(**kwargs)) - except Exception as e: - raise OpenStackCloudException( - "Error in updating user {user}: {message}".format( - user=name_or_id, message=str(e))) self.list_users.invalidate(self) return _utils.normalize_users([user])[0] @@ -547,16 +531,13 @@ def create_user( self, name, password=None, email=None, default_project=None, enabled=True, domain_id=None): """Create a user.""" - try: + with _utils.shade_exceptions("Error in creating user {user}".format( + user=name)): identity_params = self._get_identity_params( domain_id, default_project) user = self.manager.submitTask(_tasks.UserCreate( name=name, password=password, email=email, enabled=enabled, **identity_params)) - except Exception as e: - raise OpenStackCloudException( - "Error in creating user {user}: {message}".format( - user=name, message=str(e))) self.list_users.invalidate(self) return _utils.normalize_users([user])[0] @@ -570,12 +551,9 @@ def delete_user(self, name_or_id): # normalized dict won't work user = self.get_user_by_id(user['id'], normalize=False) - try: + with _utils.shade_exceptions("Error in deleting user {user}".format( + user=name_or_id)): self.manager.submitTask(_tasks.UserDelete(user=user)) - except Exception as e: - raise OpenStackCloudException( - "Error in deleting user {user}: {message}".format( - user=name_or_id, message=str(e))) self.list_users.invalidate(self) return True @@ -614,7 +592,7 @@ def swift_client(self): @property def swift_service(self): if self._swift_service is None: - try: + with _utils.shade_exceptions("Error constructing swift client"): endpoint = self.get_session_endpoint( service_key='object-store') options = dict(os_auth_token=self.auth_token, @@ -622,11 +600,6 @@ def swift_service(self): os_region_name=self.region_name) self._swift_service = swift_service.SwiftService( options=options) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Error constructing swift client: %s", str(e)) return self._swift_service @property @@ -670,12 +643,9 @@ def create_stack( template=template, files=tpl_files, ) - try: + with _utils.shade_exceptions("Error creating stack {name}".format( + name=name)): stack = self.manager.submitTask(_tasks.StackCreate(**params)) - except Exception as e: - raise OpenStackCloudException( - "Error creating stack {name}: {message}".format( - name=name, message=e.message)) if not wait: return stack for count in _iterate_timeout( @@ -699,11 +669,9 @@ def delete_stack(self, name_or_id): self.log.debug("Stack %s not found for deleting" % name_or_id) return False - try: + with _utils.shade_exceptions("Failed to delete stack {id}".format( + id=stack['id'])): self.manager.submitTask(_tasks.StackDelete(id=stack['id'])) - except Exception: - raise OpenStackCloudException( - "Failed to delete stack {id}".format(id=stack['id'])) return True def get_name(self): @@ -776,15 +744,11 @@ def has_service(self, service_key): def _nova_extensions(self): extensions = set() - try: + with _utils.shade_exceptions("Error fetching extension list for nova"): body = self.manager.submitTask( _tasks.NovaUrlGet(url='/extensions')) for x in body['extensions']: extensions.add(x['alias']) - except Exception as e: - raise OpenStackCloudException( - "error fetching extension list for nova: {msg}".format( - msg=str(e))) return extensions @@ -917,11 +881,8 @@ def list_keypairs(self): :returns: A list of keypair dicts. """ - try: + with _utils.shade_exceptions("Error fetching keypair list"): return self.manager.submitTask(_tasks.KeypairList()) - except Exception as e: - raise OpenStackCloudException( - "Error fetching keypair list: %s" % str(e)) def list_networks(self, filters=None): """List all available networks. @@ -988,11 +949,8 @@ def list_volumes(self, cache=True): if not cache: warnings.warn('cache argument to list_volumes is deprecated. Use ' 'invalidate instead.') - try: + with _utils.shade_exceptions("Error fetching volume list"): return self.manager.submitTask(_tasks.VolumeList()) - except Exception as e: - raise OpenStackCloudException( - "Error fetching volume list: %s" % e) @_utils.cache_on_arguments() def list_flavors(self): @@ -1001,11 +959,8 @@ def list_flavors(self): :returns: A list of flavor dicts. """ - try: + with _utils.shade_exceptions("Error fetching flavor list"): return self.manager.submitTask(_tasks.FlavorList(is_public=None)) - except Exception as e: - raise OpenStackCloudException( - "Error fetching flavor list: %s" % e) @_utils.cache_on_arguments(should_cache_fn=_no_pending_stacks) def list_stacks(self): @@ -1016,10 +971,8 @@ def list_stacks(self): :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call. """ - try: + with _utils.shade_exceptions(): stacks = self.manager.submitTask(_tasks.StackList()) - except Exception as e: - raise OpenStackCloudException(str(e)) return stacks def list_server_security_groups(self, server): @@ -1049,13 +1002,9 @@ def list_security_groups(self): # Handle nova security groups elif self.secgroup_source == 'nova': - try: + with _utils.shade_exceptions("Error fetching security group list"): groups = self.manager.submitTask( _tasks.NovaSecurityGroupList()) - except Exception: - raise OpenStackCloudException( - "Error fetching security group list" - ) return _utils.normalize_nova_secgroups(groups) # Security groups not supported @@ -1088,7 +1037,10 @@ def list_servers(self, detailed=False): return self._servers def _list_servers(self, detailed=False): - try: + with _utils.shade_exceptions( + "Error fetching server list on {cloud}:{region}:".format( + cloud=self.name, + region=self.region_name)): servers = self.manager.submitTask(_tasks.ServerList()) if detailed: @@ -1098,13 +1050,6 @@ def _list_servers(self, detailed=False): ] else: return servers - except Exception as e: - raise OpenStackCloudException( - "Error fetching server list on {cloud}:{region}:" - " {error}".format( - cloud=self.name, - region=self.region_name, - error=str(e))) @_utils.cache_on_arguments(should_cache_fn=_no_pending_images) def list_images(self, filter_deleted=True): @@ -1128,12 +1073,10 @@ def list_images(self, filter_deleted=True): except glanceclient.exc.HTTPInternalServerError: # We didn't have glance, let's try nova # If this doesn't work - we just let the exception propagate - try: + with _utils.shade_exceptions("Error fetching image list"): image_list = self.manager.submitTask(_tasks.NovaImageList()) - except Exception as e: - raise OpenStackCloudException( - "Error fetching image list: %s" % e) - + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Error fetching image list: %s" % e) @@ -1157,12 +1100,8 @@ def list_floating_ip_pools(self): raise OpenStackCloudUnavailableExtension( 'Floating IP pools extension is not available on target cloud') - try: + with _utils.shade_exceptions("Error fetching floating IP pool list"): return self.manager.submitTask(_tasks.FloatingIPPoolList()) - except Exception as e: - raise OpenStackCloudException( - "error fetching floating IP pool list: {msg}".format( - msg=str(e))) def list_floating_ips(self): """List all available floating IPs. @@ -1189,11 +1128,8 @@ def _neutron_list_floating_ips(self): _tasks.NeutronFloatingIPList())['floatingips'] def _nova_list_floating_ips(self): - try: + with _utils.shade_exceptions("Error fetching floating IPs list"): return self.manager.submitTask(_tasks.NovaFloatingIPList()) - except Exception as e: - raise OpenStackCloudException( - "error fetching floating IPs list: {msg}".format(msg=str(e))) def use_external_network(self): return self._use_external_network @@ -1526,13 +1462,10 @@ def create_keypair(self, name, public_key): :raises: OpenStackCloudException on operation error. """ - try: + with _utils.shade_exceptions("Unable to create keypair {name}".format( + name=name)): return self.manager.submitTask(_tasks.KeypairCreate( name=name, public_key=public_key)) - except Exception as e: - raise OpenStackCloudException( - "Unable to create keypair %s: %s" % (name, e) - ) def delete_keypair(self, name): """Delete a keypair. @@ -1548,10 +1481,11 @@ def delete_keypair(self, name): except nova_exceptions.NotFound: self.log.debug("Keypair %s not found for deleting" % name) return False + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( - "Unable to delete keypair %s: %s" % (name, e) - ) + "Unable to delete keypair %s: %s" % (name, e)) return True # TODO(Shrews): This will eventually need to support tenant ID and @@ -1855,7 +1789,7 @@ def create_image_snapshot(self, name, server, **metadata): def delete_image(self, name_or_id, wait=False, timeout=3600): image = self.get_image(name_or_id) - try: + with _utils.shade_exceptions("Error in deleting image"): # Note that in v1, the param name is image, but in v2, # it's image_id glance_api_version = self.cloud_config.get_api_version('image') @@ -1866,9 +1800,6 @@ def delete_image(self, name_or_id, wait=False, timeout=3600): self.manager.submitTask( _tasks.ImageDelete(image=image.id)) self.list_images.invalidate(self) - except Exception as e: - raise OpenStackCloudException( - "Error in deleting image: %s" % str(e)) if wait: for count in _utils._iterate_timeout( @@ -2083,11 +2014,8 @@ def create_volume(self, wait=True, timeout=None, **kwargs): :raises: OpenStackCloudException on operation error. """ - try: + with _utils.shade_exceptions("Error in creating volume"): volume = self.manager.submitTask(_tasks.VolumeCreate(**kwargs)) - except Exception as e: - raise OpenStackCloudException( - "Error in creating volume: %s" % str(e)) self.list_volumes.invalidate(self) if volume['status'] == 'error': @@ -2133,12 +2061,9 @@ def delete_volume(self, name_or_id=None, wait=True, timeout=None): exc_info=True) return False - try: + with _utils.shade_exceptions("Error in deleting volume"): self.manager.submitTask( _tasks.VolumeDelete(volume=volume['id'])) - except Exception as e: - raise OpenStackCloudException( - "Error in deleting volume: %s" % str(e)) self.list_volumes.invalidate(self) if wait: @@ -2202,15 +2127,12 @@ def detach_volume(self, server, volume, wait=True, timeout=None): % (volume['id'], server['id']) ) - try: + with _utils.shade_exceptions( + "Error detaching volume {volume} from server {server}".format( + volume=volume['id'], server=server['id'])): self.manager.submitTask( _tasks.VolumeDetach(attachment_id=volume['id'], server_id=server['id'])) - except Exception as e: - raise OpenStackCloudException( - "Error detaching volume %s from server %s: %s" % - (volume['id'], server['id'], e) - ) if wait: for count in _utils._iterate_timeout( @@ -2267,16 +2189,14 @@ def attach_volume(self, server, volume, device=None, % (volume['id'], volume['status']) ) - try: + with _utils.shade_exceptions( + "Error attaching volume {volume_id} to server " + "{server_id}".format(volume_id=volume['id'], + server_id=server['id'])): vol = self.manager.submitTask( _tasks.VolumeAttach(volume_id=volume['id'], server_id=server['id'], device=device)) - except Exception as e: - raise OpenStackCloudException( - "Error attaching volume %s to server %s: %s" % - (volume['id'], server['id'], e) - ) if wait: for count in _utils._iterate_timeout( @@ -2323,7 +2243,9 @@ def create_volume_snapshot(self, volume_id, force=False, :raises: OpenStackCloudTimeout if wait time exceeded. :raises: OpenStackCloudException on operation error. """ - try: + with _utils.shade_exceptions( + "Error creating snapshot of volume {volume_id}".format( + volume_id=volume_id)): snapshot = self.manager.submitTask( _tasks.VolumeSnapshotCreate( volume_id=volume_id, force=force, @@ -2331,11 +2253,6 @@ def create_volume_snapshot(self, volume_id, force=False, display_description=display_description) ) - except Exception as e: - raise OpenStackCloudException( - "Error creating snapshot of volume %s: %s" % (volume_id, e) - ) - if wait: snapshot_id = snapshot['id'] for count in _utils._iterate_timeout( @@ -2362,18 +2279,15 @@ def get_volume_snapshot_by_id(self, snapshot_id): param: snapshot_id: ID of the volume snapshot. """ - try: + with _utils.shade_exceptions( + "Error getting snapshot {snapshot_id}".format( + snapshot_id=snapshot_id)): snapshot = self.manager.submitTask( _tasks.VolumeSnapshotGet( snapshot_id=snapshot_id ) ) - except Exception as e: - raise OpenStackCloudException( - "Error getting snapshot %s: %s" % (snapshot_id, e) - ) - return snapshot def get_volume_snapshot(self, name_or_id, filters=None): @@ -2404,17 +2318,12 @@ def list_volume_snapshots(self, detailed=True, search_opts=None): :returns: A list of volume snapshots dicts. """ - try: + with _utils.shade_exceptions("Error getting a list of snapshots"): return self.manager.submitTask( _tasks.VolumeSnapshotList(detailed=detailed, search_opts=search_opts) ) - except Exception as e: - raise OpenStackCloudException( - "Error getting a list of snapshots: %s" % e - ) - def delete_volume_snapshot(self, name_or_id=None, wait=False, timeout=None): """Delete a volume snapshot. @@ -2433,15 +2342,12 @@ def delete_volume_snapshot(self, name_or_id=None, wait=False, if not volumesnapshot: return False - try: + with _utils.shade_exceptions("Error in deleting volume snapshot"): self.manager.submitTask( _tasks.VolumeSnapshotDelete( snapshot=volumesnapshot['id'] ) ) - except Exception as e: - raise OpenStackCloudException( - "Error in deleting volume snapshot: %s" % str(e)) if wait: for count in _utils._iterate_timeout( @@ -2566,7 +2472,9 @@ def _nova_available_floating_ips(self, pool=None): is not specified and cannot be found. """ - try: + with _utils.shade_exceptions( + "Unable to create floating IP in pool {pool}".format( + pool=pool)): if pool is None: pools = self.list_floating_ip_pools() if not pools: @@ -2591,11 +2499,6 @@ def _nova_available_floating_ips(self, pool=None): return [f_ip] - except Exception as e: - raise OpenStackCloudException( - "unable to create floating IP in pool {pool}: {msg}".format( - pool=pool, msg=str(e))) - def create_floating_ip(self, network=None, server=None): """Allocate a new floating IP from a network or a pool. @@ -2654,7 +2557,9 @@ def _neutron_create_floating_ip( body={'floatingip': kwargs}))['floatingip'] def _nova_create_floating_ip(self, pool=None): - try: + with _utils.shade_exceptions( + "Unable to create floating IP in pool {pool}".format( + pool=pool)): if pool is None: pools = self.list_floating_ip_pools() if not pools: @@ -2666,11 +2571,6 @@ def _nova_create_floating_ip(self, pool=None): _tasks.NovaFloatingIPCreate(pool=pool)) return pool_ip - except Exception as e: - raise OpenStackCloudException( - "unable to create floating IP in pool {pool}: {msg}".format( - pool=pool, msg=str(e))) - def delete_floating_ip(self, floating_ip_id): """Deallocate a floating IP from a tenant. @@ -2709,9 +2609,11 @@ def _nova_delete_floating_ip(self, floating_ip_id): _tasks.NovaFloatingIPDelete(floating_ip=floating_ip_id)) except nova_exceptions.NotFound: return False + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( - "unable to delete floating IP id {fip_id}: {msg}".format( + "Unable to delete floating IP id {fip_id}: {msg}".format( fip_id=floating_ip_id, msg=str(e))) return True @@ -2843,15 +2745,13 @@ def _neutron_attach_ip_to_server( def _nova_attach_ip_to_server(self, server_id, floating_ip_id, fixed_address=None): - try: + with _utils.shade_exceptions( + "Error attaching IP {ip} to instance {id}".format( + ip=floating_ip_id, id=server_id)): f_ip = self.get_floating_ip(id=floating_ip_id) return self.manager.submitTask(_tasks.NovaFloatingIPAttach( server=server_id, address=f_ip['floating_ip_address'], fixed_address=fixed_address)) - except Exception as e: - raise OpenStackCloudException( - "error attaching IP {ip} to instance {id}: {msg}".format( - ip=floating_ip_id, id=server_id, msg=str(e))) def detach_ip_from_server(self, server_id, floating_ip_id): """Detach a floating IP from a server. @@ -2904,9 +2804,11 @@ def _nova_detach_ip_from_server(self, server_id, floating_ip_id): "nova floating IP detach failed: {msg}".format(msg=str(e)), exc_info=True) return False + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( - "error detaching IP {ip} from instance {id}: {msg}".format( + "Error detaching IP {ip} from instance {id}: {msg}".format( ip=floating_ip_id, id=server_id, msg=str(e))) return True @@ -3110,7 +3012,7 @@ def create_server( kwargs['block_device_mapping'] = dict() kwargs['block_device_mapping']['vda'] = volume_id - try: + with _utils.shade_exceptions("Error in creating instance"): server = self.manager.submitTask(_tasks.ServerCreate( name=name, image=image, flavor=flavor, **kwargs)) server_id = server.id @@ -3127,11 +3029,6 @@ def create_server( if server.status == 'ERROR': raise OpenStackCloudException( "Error in creating the server.") - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Error in creating instance: {0}".format(e)) if wait: # There is no point in iterating faster than the list_servers cache for count in _utils._iterate_timeout( @@ -3188,12 +3085,9 @@ def get_active_server( return None def rebuild_server(self, server_id, image_id, wait=False, timeout=180): - try: + with _utils.shade_exceptions("Error in rebuilding instance"): server = self.manager.submitTask(_tasks.ServerRebuild( server=server_id, image=image_id)) - except Exception as e: - raise OpenStackCloudException( - "Error in rebuilding instance: {0}".format(e)) if wait: for count in _utils._iterate_timeout( timeout, @@ -3241,6 +3135,8 @@ def _delete_server( _tasks.ServerDelete(server=server['id'])) except nova_exceptions.NotFound: return + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Error in deleting server: {0}".format(e)) @@ -3257,6 +3153,8 @@ def _delete_server( return except nova_exceptions.NotFound: return + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( "Error in deleting server: {0}".format(e)) @@ -3868,16 +3766,14 @@ def create_security_group(self, name, description): return group['security_group'] elif self.secgroup_source == 'nova': - try: + with _utils.shade_exceptions( + "Failed to create security group '{name}'".format( + name=name)): group = self.manager.submitTask( _tasks.NovaSecurityGroupCreate( name=name, description=description ) ) - except Exception as e: - raise OpenStackCloudException( - "failed to create security group '{name}': {msg}".format( - name=name, msg=str(e))) return _utils.normalize_nova_secgroups([group])[0] # Security groups not supported @@ -3914,14 +3810,12 @@ def delete_security_group(self, name_or_id): return True elif self.secgroup_source == 'nova': - try: + with _utils.shade_exceptions( + "Failed to delete security group '{group}'".format( + group=name_or_id)): self.manager.submitTask( _tasks.NovaSecurityGroupDelete(group=secgroup['id']) ) - except Exception as e: - raise OpenStackCloudException( - "failed to delete security group '{group}': {msg}".format( - group=name_or_id, msg=str(e))) return True # Security groups not supported @@ -3959,15 +3853,13 @@ def update_security_group(self, name_or_id, **kwargs): return group['security_group'] elif self.secgroup_source == 'nova': - try: + with _utils.shade_exceptions( + "Failed to update security group '{group}'".format( + group=name_or_id)): group = self.manager.submitTask( _tasks.NovaSecurityGroupUpdate( group=secgroup['id'], **kwargs) ) - except Exception as e: - raise OpenStackCloudException( - "failed to update security group '{group}': {msg}".format( - group=name_or_id, msg=str(e))) return _utils.normalize_nova_secgroups([group])[0] # Security groups not supported @@ -4086,7 +3978,8 @@ def create_security_group_rule(self, port_range_min = 1 port_range_max = 65535 - try: + with _utils.shade_exceptions( + "Failed to create security group rule"): rule = self.manager.submitTask( _tasks.NovaSecurityGroupRuleCreate( parent_group_id=secgroup['id'], @@ -4097,10 +3990,6 @@ def create_security_group_rule(self, group_id=remote_group_id ) ) - except Exception as e: - raise OpenStackCloudException( - "failed to create security group rule: {msg}".format( - msg=str(e))) return _utils.normalize_nova_secgroup_rules([rule])[0] # Security groups not supported @@ -4141,9 +4030,11 @@ def delete_security_group_rule(self, rule_id): ) except nova_exceptions.NotFound: return False + except OpenStackCloudException: + raise except Exception as e: raise OpenStackCloudException( - "failed to delete security group rule {id}: {msg}".format( + "Failed to delete security group rule {id}: {msg}".format( id=rule_id, msg=str(e))) return True From 506d6e8ffefdda4d31e1b33686008e9e610beca2 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 10 Nov 2015 17:16:02 -0500 Subject: [PATCH 0647/3836] Fix JSON schema We incorrectly refered to things with _api_ in the name. Change-Id: I9130ae0f72484957d0a8943eceabcd6a6bc21c91 --- os_client_config/vendor-schema.json | 34 ++++++++++++++--------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/os_client_config/vendor-schema.json b/os_client_config/vendor-schema.json index e3fb57359..6c57ba4bc 100644 --- a/os_client_config/vendor-schema.json +++ b/os_client_config/vendor-schema.json @@ -61,87 +61,87 @@ "enum": [ "neutron", "nova", "None" ], "default": "neutron" }, - "compute_api_service_name": { + "compute_service_name": { "name": "Compute API Service Name", "description": "Compute API Service Name", "type": "string" }, - "database_api_service_name": { + "database_service_name": { "name": "Database API Service Name", "description": "Database API Service Name", "type": "string" }, - "dns_api_service_name": { + "dns_service_name": { "name": "DNS API Service Name", "description": "DNS API Service Name", "type": "string" }, - "identity_api_service_name": { + "identity_service_name": { "name": "Identity API Service Name", "description": "Identity API Service Name", "type": "string" }, - "image_api_service_name": { + "image_service_name": { "name": "Image API Service Name", "description": "Image API Service Name", "type": "string" }, - "volume_api_service_name": { + "volume_service_name": { "name": "Volume API Service Name", "description": "Volume API Service Name", "type": "string" }, - "network_api_service_name": { + "network_service_name": { "name": "Network API Service Name", "description": "Network API Service Name", "type": "string" }, - "object_api_service_name": { + "object_service_name": { "name": "Object Storage API Service Name", "description": "Object Storage API Service Name", "type": "string" }, - "baremetal_api_service_name": { + "baremetal_service_name": { "name": "Baremetal API Service Name", "description": "Baremetal API Service Name", "type": "string" }, - "compute_api_service_type": { + "compute_service_type": { "name": "Compute API Service Type", "description": "Compute API Service Type", "type": "string" }, - "database_api_service_type": { + "database_service_type": { "name": "Database API Service Type", "description": "Database API Service Type", "type": "string" }, - "dns_api_service_type": { + "dns_service_type": { "name": "DNS API Service Type", "description": "DNS API Service Type", "type": "string" }, - "identity_api_service_type": { + "identity_service_type": { "name": "Identity API Service Type", "description": "Identity API Service Type", "type": "string" }, - "image_api_service_type": { + "image_service_type": { "name": "Image API Service Type", "description": "Image API Service Type", "type": "string" }, - "volume_api_service_type": { + "volume_service_type": { "name": "Volume API Service Type", "description": "Volume API Service Type", "type": "string" }, - "network_api_service_type": { + "network_service_type": { "name": "Network API Service Type", "description": "Network API Service Type", "type": "string" }, - "object_api_service_type": { + "object_service_type": { "name": "Object Storage API Service Type", "description": "Object Storage API Service Type", "type": "string" From cb0e9130f5acf3c703070d4c33cf159de17020fe Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 9 Nov 2015 11:24:53 -0500 Subject: [PATCH 0648/3836] Add Ansible testing infrastructure The keypair test needed adjusting so that it would work in the gating environment, and anywhere an SSH key may not be available. Change-Id: Ic9bf41c2b4041911d05217ad3748e097326bac3a --- extras/run-ansible-tests.sh | 35 +++++++++++++++++++ shade/tests/ansible/hooks/post_test_hook.sh | 26 ++++++++++++++ .../ansible/roles/keypair/tasks/main.yml | 20 +++++++++-- tox.ini | 5 +++ 4 files changed, 84 insertions(+), 2 deletions(-) create mode 100755 extras/run-ansible-tests.sh create mode 100755 shade/tests/ansible/hooks/post_test_hook.sh diff --git a/extras/run-ansible-tests.sh b/extras/run-ansible-tests.sh new file mode 100755 index 000000000..bfd1ada36 --- /dev/null +++ b/extras/run-ansible-tests.sh @@ -0,0 +1,35 @@ +#!/bin/bash +############################################################################# +# run-ansible-tests.sh +# +# Script used to setup a tox environment for running Ansible. This is meant +# to be called by tox (via tox.ini). To run the Ansible tests, use: +# +# tox -e ansible +# +# USAGE: +# run-ansible-tests.sh +# +# PARAMETERS: +# Directory of the tox environment to use for testing. +############################################################################# + +ENVDIR=$1 + +if [ -d ${ENVDIR}/ansible ] +then + echo "Using existing Ansible install" +else + echo "Installing Ansible at $ENVDIR" + git clone --recursive git://github.com/ansible/ansible.git ${ENVDIR}/ansible +fi + +# We need to source the current tox environment so that Ansible will +# be setup for the correct python environment. +source $ENVDIR/bin/activate + +# Setup Ansible +source $ENVDIR/ansible/hacking/env-setup + +# Run the shade Ansible tests +ansible-playbook -vvv ./shade/tests/ansible/run.yml -e "cloud=devstack-admin" diff --git a/shade/tests/ansible/hooks/post_test_hook.sh b/shade/tests/ansible/hooks/post_test_hook.sh new file mode 100755 index 000000000..d79a9427f --- /dev/null +++ b/shade/tests/ansible/hooks/post_test_hook.sh @@ -0,0 +1,26 @@ +# -*- 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. + +export SHADE_DIR="$BASE/new/shade" + +cd $SHADE_DIR +sudo chown -R jenkins:stack $SHADE_DIR + +echo "Running shade Ansible test suite" +set +e +sudo -E -H -u jenkins tox -eansible +EXIT_CODE=$? +set -e + +exit $EXIT_CODE diff --git a/shade/tests/ansible/roles/keypair/tasks/main.yml b/shade/tests/ansible/roles/keypair/tasks/main.yml index cc30d9624..53a856e2f 100644 --- a/shade/tests/ansible/roles/keypair/tasks/main.yml +++ b/shade/tests/ansible/roles/keypair/tasks/main.yml @@ -11,12 +11,18 @@ name: "{{ keypair_name }}" state: absent +- name: Generate test key file + user: + name: "{{ ansible_env.USER }}" + generate_ssh_key: yes + ssh_key_file: .ssh/shade_id_rsa + - name: Create keypair (file) os_keypair: cloud: "{{ cloud }}" name: "{{ keypair_name }}" state: present - public_key_file: "{{ ansible_env.HOME }}/.ssh/id_rsa.pub" + public_key_file: "{{ ansible_env.HOME }}/.ssh/shade_id_rsa.pub" - name: Delete keypair (file) os_keypair: @@ -29,10 +35,20 @@ cloud: "{{ cloud }}" name: "{{ keypair_name }}" state: present - public_key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}" + public_key: "{{ lookup('file', '~/.ssh/shade_id_rsa.pub') }}" - name: Delete keypair (key) os_keypair: cloud: "{{ cloud }}" name: "{{ keypair_name }}" state: absent + +- name: Delete test key pub file + file: + name: "{{ ansible_env.HOME }}/.ssh/shade_id_rsa.pub" + state: absent + +- name: Delete test key pvt file + file: + name: "{{ ansible_env.HOME }}/.ssh/shade_id_rsa" + state: absent diff --git a/tox.ini b/tox.ini index 777e8a88c..fb2415f7e 100644 --- a/tox.ini +++ b/tox.ini @@ -30,6 +30,11 @@ commands = {posargs} [testenv:cover] commands = python setup.py testr --coverage --testr-args='{posargs}' +[testenv:ansible] +# Need to pass some env vars for the Ansible playbooks +passenv = HOME USER +commands = {toxinidir}/extras/run-ansible-tests.sh {envdir} + [flake8] # Infra does not follow hacking, nor the broken E12* things ignore = E123,E125,E129,H From 1ac8530c25e439ae97f088930b1cea1b5f260f36 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 12 Nov 2015 15:59:45 -0500 Subject: [PATCH 0649/3836] Add ability to selectively run ansible tests Now that the Ansible module tests can be run via tox, it might be nice to allow developers the ability to selectively run individual module tests (accomplished via Ansible tags) from tox. Change-Id: I38a3040a7157a9bcf1cda76e26ab11577e9f80a6 --- extras/run-ansible-tests.sh | 22 +++++++++++++++++++--- shade/tests/ansible/README.txt | 20 +++++++++++++------- tox.ini | 2 +- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/extras/run-ansible-tests.sh b/extras/run-ansible-tests.sh index bfd1ada36..7797cc349 100755 --- a/extras/run-ansible-tests.sh +++ b/extras/run-ansible-tests.sh @@ -5,16 +5,27 @@ # Script used to setup a tox environment for running Ansible. This is meant # to be called by tox (via tox.ini). To run the Ansible tests, use: # -# tox -e ansible +# tox -e ansible [TAG] # # USAGE: -# run-ansible-tests.sh +# run-ansible-tests.sh [TAG] # # PARAMETERS: # Directory of the tox environment to use for testing. +# [TAG] Optional list of space-separated tags to control which +# modules are tested. +# +# EXAMPLES: +# # Run all Ansible tests +# run-ansible-tests.sh ansible +# +# # Run auth, keypair, and network tests +# run-ansible-tests.sh ansible auth keypair network ############################################################################# ENVDIR=$1 +shift +TAGS=$( echo "$@" | tr ' ' , ) if [ -d ${ENVDIR}/ansible ] then @@ -32,4 +43,9 @@ source $ENVDIR/bin/activate source $ENVDIR/ansible/hacking/env-setup # Run the shade Ansible tests -ansible-playbook -vvv ./shade/tests/ansible/run.yml -e "cloud=devstack-admin" +tag_opt="" +if [ "${TAGS}" != "" ] +then + tag_opt="--tags ${TAGS}" +fi +ansible-playbook -vvv ./shade/tests/ansible/run.yml -e "cloud=devstack-admin" ${tag_opt} diff --git a/shade/tests/ansible/README.txt b/shade/tests/ansible/README.txt index fd255d5bf..3931b4af9 100644 --- a/shade/tests/ansible/README.txt +++ b/shade/tests/ansible/README.txt @@ -3,18 +3,24 @@ OpenStack modules. You will need a clouds.yaml file in order to run the tests. You must provide a value for the `cloud` variable for each run (using the -e option) as a default is not currently provided. +If you want to run these tests against devstack, it is easiest to use +the tox target. This assumes you have a devstack-admin cloud defined +in your clouds.yaml file that points to devstack. Some examples of +using tox: -Examples --------- + tox -e ansible -* Run all module tests against a provider: + tox -e ansible keypair security_group - ansible-playbook run.yml -e "cloud=hp" +If you want to run these tests directly, or against different clouds, +then you'll need to use the ansible-playbook command that comes with +the Ansible distribution and feed it the run.yml playbook. Some examples: -* Run only the keypair and security_group tests: + # Run all module tests against a provider + ansible-playbook run.yml -e "cloud=hp" + # Run only the keypair and security_group tests ansible-playbook run.yml -e "cloud=hp" --tags "keypair,security_group" -* Run all tests except security_group: - + # Run all tests except security_group ansible-playbook run.yml -e "cloud=hp" --skip-tags "security_group" diff --git a/tox.ini b/tox.ini index fb2415f7e..586ede98a 100644 --- a/tox.ini +++ b/tox.ini @@ -33,7 +33,7 @@ commands = python setup.py testr --coverage --testr-args='{posargs}' [testenv:ansible] # Need to pass some env vars for the Ansible playbooks passenv = HOME USER -commands = {toxinidir}/extras/run-ansible-tests.sh {envdir} +commands = {toxinidir}/extras/run-ansible-tests.sh {envdir} {posargs} [flake8] # Infra does not follow hacking, nor the broken E12* things From 633441fbc7fe7d041695c02e60077fcd631824e9 Mon Sep 17 00:00:00 2001 From: matthew wagoner Date: Thu, 12 Nov 2015 12:18:30 -0500 Subject: [PATCH 0650/3836] Add new context manager for shade exceptions, final. This commit completes the work of change Ic39b6e3efc33b7a30f298a3daca6d6c16ab4ca89 Shade has developed a common pattern within many of the API methods where we try to avoid re-wrapping OpenStackClientException exceptions by catching them and simply re-raising. We can eliminate this duplicate exception handling with a context manager. Change-Id: If236656146ecfa5a2b3eb3379efc6869829bdaa7 --- shade/operatorcloud.py | 57 +++++++++++------------------------------- 1 file changed, 15 insertions(+), 42 deletions(-) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index cb40f4b61..0ee183a2a 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -60,14 +60,11 @@ def list_nics(self): return self.manager.submitTask(_tasks.MachinePortList()) def list_nics_for_machine(self, uuid): - try: + with _utils.shade_exceptions( + "Error fetching port list for node {node_id}".format( + node_id=uuid)): return self.manager.submitTask( _tasks.MachineNodePortList(node_id=uuid)) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Error fetching port list for node %s: %s" % (uuid, e)) def get_nic_by_mac(self, mac): try: @@ -343,18 +340,16 @@ def unregister_machine(self, nics, uuid, wait=False, timeout=600): "state '%s'" % (uuid, machine['provision_state'])) for nic in nics: - try: + with _utils.shade_exceptions( + "Error removing NIC {nic} from baremetal API for node " + "{uuid}".format(nic=nic, uuid=uuid)): port = self.manager.submitTask( _tasks.MachinePortGetByAddress(address=nic['mac'])) self.manager.submitTask( _tasks.MachinePortDelete(port_id=port.uuid)) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Error removing NIC '%s' from baremetal API for " - "node '%s'. Error: %s" % (nic, uuid, str(e))) - try: + with _utils.shade_exceptions( + "Error unregistering machine {node_id} from the baremetal " + "API".format(node_id=uuid)): self.manager.submitTask( _tasks.MachineDelete(node_id=uuid)) if wait: @@ -364,13 +359,6 @@ def unregister_machine(self, nics, uuid, wait=False, timeout=600): if not self.get_machine(uuid): break - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Error unregistering machine %s from the baremetal API. " - "Error: %s" % (uuid, str(e))) - def patch_machine(self, name_or_id, patch): """Patch Machine Information @@ -1405,7 +1393,9 @@ def unset_flavor_specs(self, flavor_id, keys): def _mod_flavor_access(self, action, flavor_id, project_id): """Common method for adding and removing flavor access """ - try: + with _utils.shade_exceptions("Error trying to {action} access from " + "flavor ID {flavor}".format( + action=action, flavor=flavor_id)): if action == 'add': self.manager.submitTask( _tasks.FlavorAddAccess(flavor=flavor_id, @@ -1416,13 +1406,6 @@ def _mod_flavor_access(self, action, flavor_id, project_id): _tasks.FlavorRemoveAccess(flavor=flavor_id, tenant=project_id) ) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Error trying to {0} access from flavor ID {1}: {2}".format( - action, flavor_id, e) - ) def add_flavor_access(self, flavor_id, project_id): """Grant access to a private flavor for a project/tenant. @@ -1475,14 +1458,9 @@ def delete_role(self, name_or_id): "Role {0} not found for deleting".format(name_or_id)) return False - try: + with _utils.shade_exceptions("Unable to delete role {name}".format( + name=name_or_id)): self.manager.submitTask(_tasks.RoleDelete(role=role['id'])) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Unable to delete role {0}: {1}".format(name_or_id, e) - ) return True @@ -1492,10 +1470,5 @@ def list_hypervisors(self): :returns: A list of hypervisor dicts. """ - try: + with _utils.shade_exceptions("Error fetching hypervisor list"): return self.manager.submitTask(_tasks.HypervisorList()) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Error fetching hypervisor list: %s" % str(e)) From b2bc8c55ff004c202e1ac252d44dd1c8e7ffad0c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 10 Nov 2015 10:12:55 -0500 Subject: [PATCH 0651/3836] Four minor fixes that make debugging better While running a script against all of the existing clouds, there were a few issues that popped up. Most notably: - get_openstack_vars would bomb out if your cloud didn't have security groups (rax) - nova boot has a "this is why your boot failed" field that we did not pass on to the user - TaskManager debug output showed the cloud but not the region, which is confusing when you have clouds with more than one region - The novaclient error message for when you pass in the wrong datatype for nics is "str has no attribute get" which does not tell you that what you did was pass in a dict when you were supposed to have passed in a list of dicts. The problem is easy to trap for, and in more extreme cases to provide a better error message for. Change-Id: Ie4f7c68d0ff95e020042189937ce452e596802cf --- shade/meta.py | 6 ++++-- shade/openstackcloud.py | 25 ++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/shade/meta.py b/shade/meta.py index 9d4c6f47a..b952b3660 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -265,8 +265,10 @@ def expand_server_vars(cloud, server): def expand_server_security_groups(cloud, server): - groups = cloud.list_server_security_groups(server) - + try: + groups = cloud.list_server_security_groups(server) + except exc.OpenStackCloudException: + groups = [] server['security_groups'] = groups diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index dd3311c90..61ceb800b 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -150,7 +150,7 @@ def __init__( self.manager = manager else: self.manager = task_manager.TaskManager( - name=self.name, client=self) + name=':'.join([self.name, self.region_name]), client=self) (self.verify, self.cert) = cloud_config.get_requests_verify_args() # Turn off urllib3 warnings about insecure certs if we have @@ -978,8 +978,13 @@ def list_server_security_groups(self, server): :returns: A list of security group dicts. """ - groups = self.manager.submitTask( - _tasks.ServerListSecurityGroups(server=server['id'])) + # Don't even try if we're a cloud that doesn't have them + if self.secgroup_source not in ('nova', 'neutron'): + return [] + + with _utils.shade_exceptions(): + groups = self.manager.submitTask( + _tasks.ServerListSecurityGroups(server=server['id'])) return _utils.normalize_nova_secgroups(groups) @@ -2999,6 +3004,14 @@ def create_server( :returns: A dict representing the created server. :raises: OpenStackCloudException on operation error. """ + if 'nics' in kwargs and not isinstance(kwargs['nics'], list): + if isinstance(kwargs['nics'], dict): + # Be nice and help the user out + kwargs['nics'] = [kwargs['nics']] + else: + raise OpenStackCloudException( + 'nics parameter to create_server takes a list of dicts.' + ' Got: {nics}'.format(nics=kwargs['nics'])) if root_volume: if terminate_volume: suffix = ':::1' @@ -3054,6 +3067,12 @@ def get_active_server( reuse=True, wait=False, timeout=180): if server['status'] == 'ERROR': + if 'fault' in server and 'message' in server['fault']: + raise OpenStackCloudException( + "Error in creating the server: {reason}".format( + reason=server['fault']['message']), + extra_data=dict(server=server)) + raise OpenStackCloudException( "Error in creating the server", extra_data=dict(server=server)) From f5e1b859c8547865deb0ef8241c51ea4a00a7f97 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 12 Nov 2015 09:43:05 -0500 Subject: [PATCH 0652/3836] Add support for legacy envvar prefixes In trying to move the legacy clients to do their config via os-client-config, many of them had prefixes like NOVA_ or GLANCE_ in their shells. It's pretty easy to support optionally passing those in so that the transition to OCC is less of a change and we can treat the deprecation cycle of those features as a different topic. Change-Id: Ic819c0790989fa034db03d83535ee7b70aaacc3a --- os_client_config/config.py | 20 ++++++++++++-------- os_client_config/tests/test_environ.py | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index f439d5f90..8d92c8fe7 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -81,21 +81,24 @@ def get_boolean(value): return False -def _get_os_environ(): +def _get_os_environ(envvar_prefix=None): ret = defaults.get_defaults() + if not envvar_prefix: + # This makes the or below be OS_ or OS_ which is a no-op + envvar_prefix = 'OS_' environkeys = [k for k in os.environ.keys() - if k.startswith('OS_') + if k.startswith('OS_') or k.startswith(envvar_prefix) and not k.startswith('OS_TEST') # infra CI var and not k.startswith('OS_STD') # infra CI var ] + for k in environkeys: + newkey = k.split('_', 1)[-1].lower() + ret[newkey] = os.environ[k] # If the only environ key is region name, don't make a cloud, because # it's being used as a cloud selector if not environkeys or ( - len(environkeys) == 1 and 'OS_REGION_NAME' in environkeys): + len(environkeys) == 1 and 'region_name' in ret): return None - for k in environkeys: - newkey = k[3:].lower() - ret[newkey] = os.environ[k] return ret @@ -115,7 +118,8 @@ def _auth_update(old_dict, new_dict): class OpenStackConfig(object): def __init__(self, config_files=None, vendor_files=None, - override_defaults=None, force_ipv4=None): + override_defaults=None, force_ipv4=None, + envvar_prefix=None): self._config_files = config_files or CONFIG_FILES self._vendor_files = vendor_files or VENDOR_FILES @@ -173,7 +177,7 @@ def __init__(self, config_files=None, vendor_files=None, # make an envvars cloud self.default_cloud = os.environ.pop('OS_CLOUD', None) - envvars = _get_os_environ() + envvars = _get_os_environ(envvar_prefix=envvar_prefix) if envvars: self.cloud_config['clouds'][self.envvar_key] = envvars diff --git a/os_client_config/tests/test_environ.py b/os_client_config/tests/test_environ.py index 7f284c5eb..1e804fc3c 100644 --- a/os_client_config/tests/test_environ.py +++ b/os_client_config/tests/test_environ.py @@ -31,6 +31,8 @@ def setUp(self): fixtures.EnvironmentVariable('OS_USERNAME', 'testuser')) self.useFixture( fixtures.EnvironmentVariable('OS_PROJECT_NAME', 'testproject')) + self.useFixture( + fixtures.EnvironmentVariable('NOVA_PROJECT_ID', 'testnova')) def test_get_one_cloud(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], @@ -66,6 +68,22 @@ def test_environ_exists(self): self._assert_cloud_details(cc) self.assertNotIn('auth_url', cc.config) self.assertIn('auth_url', cc.config['auth']) + self.assertNotIn('project_id', cc.config['auth']) + self.assertNotIn('auth_url', cc.config) + cc = c.get_one_cloud('_test-cloud_') + self._assert_cloud_details(cc) + cc = c.get_one_cloud('_test_cloud_no_vendor') + self._assert_cloud_details(cc) + + def test_environ_prefix(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + envvar_prefix='NOVA_') + cc = c.get_one_cloud('envvars') + self._assert_cloud_details(cc) + self.assertNotIn('auth_url', cc.config) + self.assertIn('auth_url', cc.config['auth']) + self.assertIn('project_id', cc.config['auth']) self.assertNotIn('auth_url', cc.config) cc = c.get_one_cloud('_test-cloud_') self._assert_cloud_details(cc) From 74477b6d51f263a1b8966e82f1a51e2ec8c948d1 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 13 Nov 2015 16:00:22 -0500 Subject: [PATCH 0653/3836] Fix for create_object The swiftclient upload() API call returns a generator. We were trashing this generator by trying to Munch-ify it. This caused create_object() to not be happy. Change-Id: I8d391f361778328cea9920e923584988c8048946 --- shade/task_manager.py | 5 ++++- shade/tests/unit/test_task_manager.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/shade/task_manager.py b/shade/task_manager.py index 6e60278b4..258f7fcb2 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -20,6 +20,7 @@ import sys import threading import time +import types import six @@ -80,8 +81,10 @@ def wait(self): self._traceback) if type(self._result) == list: return meta.obj_list_to_dict(self._result) - else: + elif not isinstance(self._result, types.GeneratorType): return meta.obj_to_dict(self._result) + else: + return self._result def run(self, client): try: diff --git a/shade/tests/unit/test_task_manager.py b/shade/tests/unit/test_task_manager.py index 9616ff0ec..8c9074a85 100644 --- a/shade/tests/unit/test_task_manager.py +++ b/shade/tests/unit/test_task_manager.py @@ -13,6 +13,10 @@ # limitations under the License. +import mock +import types + +import shade from shade import task_manager from shade.tests.unit import base @@ -26,6 +30,11 @@ def main(self, client): raise TestException("This is a test exception") +class TestTaskGenerator(task_manager.Task): + def main(self, client): + yield 1 + + class TestTaskManager(base.TestCase): def setUp(self): @@ -40,3 +49,11 @@ def test_wait_re_raise(self): configured interpreters (e.g. py27, p34, pypy, ...) """ self.assertRaises(TestException, self.manager.submitTask, TestTask()) + + @mock.patch.object(shade.meta, 'obj_to_dict') + @mock.patch.object(shade.meta, 'obj_list_to_dict') + def test_dont_munchify_generators(self, mock_ol2d, mock_o2d): + ret = self.manager.submitTask(TestTaskGenerator()) + self.assertEqual(types.GeneratorType, type(ret)) + self.assertFalse(mock_o2d.called) + self.assertFalse(mock_ol2d.called) From e5715de5f09b54da8ca181cf86d57454591fcae9 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 13 Nov 2015 16:43:23 -0500 Subject: [PATCH 0654/3836] Add Ansible object role Add an Ansible role to test os_object. Change-Id: I86d451a9c78014aa4a0f29f7edbb9b9c52b19f4e --- .../tests/ansible/roles/object/tasks/main.yml | 37 +++++++++++++++++++ shade/tests/ansible/run.yml | 1 + 2 files changed, 38 insertions(+) create mode 100644 shade/tests/ansible/roles/object/tasks/main.yml diff --git a/shade/tests/ansible/roles/object/tasks/main.yml b/shade/tests/ansible/roles/object/tasks/main.yml new file mode 100644 index 000000000..ae54b6ba2 --- /dev/null +++ b/shade/tests/ansible/roles/object/tasks/main.yml @@ -0,0 +1,37 @@ +--- +- name: Create a test object file + shell: mktemp + register: tmp_file + +- name: Create container + os_object: + cloud: "{{ cloud }}" + state: present + container: ansible_container + container_access: private + +- name: Put object + os_object: + cloud: "{{ cloud }}" + state: present + name: ansible_object + filename: "{{ tmp_file.stdout }}" + container: ansible_container + +- name: Delete object + os_object: + cloud: "{{ cloud }}" + state: absent + name: ansible_object + container: ansible_container + +- name: Delete container + os_object: + cloud: "{{ cloud }}" + state: absent + container: ansible_container + +- name: Delete test object file + file: + name: "{{ tmp_file.stdout }}" + state: absent diff --git a/shade/tests/ansible/run.yml b/shade/tests/ansible/run.yml index e4bdf231d..a75d87bae 100644 --- a/shade/tests/ansible/run.yml +++ b/shade/tests/ansible/run.yml @@ -11,3 +11,4 @@ - { role: security_group, tags: security_group } - { role: subnet, tags: subnet} - { role: router, tags: router} + - { role: object, tags: object} From 3e76af913ae4a29ba1e4dd9fb4cf3b5ca109f79c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 14 Nov 2015 12:10:09 -0500 Subject: [PATCH 0655/3836] Refactor per-service key making The key for swift is object-store, but the key in the config dict would be object_store_api_version, so the key concatenation would not work. In fixing that, refactor out the creation of the keys so that the concatenation and transformation needed always happens. Change-Id: Ic095912bfc84f13ef8b11f312303a517289e0441 --- os_client_config/cloud_config.py | 22 +++++++++++++-------- os_client_config/tests/test_cloud_config.py | 19 ++++++++++++++++++ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 3f46c6ecd..32fd3b6d5 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -23,6 +23,14 @@ from os_client_config import exceptions +def _make_key(key, service_type): + if not service_type: + return key + else: + service_type = service_type.lower().replace('-', '_') + return "_".join([service_type, key]) + + class CloudConfig(object): def __init__(self, name, region, config, force_ipv4=False, auth_plugin=None, @@ -89,32 +97,30 @@ def get_auth_args(self): return self.config['auth'] def get_interface(self, service_type=None): + key = _make_key('interface', service_type) interface = self.config.get('interface') - if not service_type: - return interface - key = '{service_type}_interface'.format(service_type=service_type) return self.config.get(key, interface) def get_region_name(self, service_type=None): if not service_type: return self.region - key = '{service_type}_region_name'.format(service_type=service_type) + key = _make_key('region_name', service_type) return self.config.get(key, self.region) def get_api_version(self, service_type): - key = '{service_type}_api_version'.format(service_type=service_type) + key = _make_key('api_version', service_type) return self.config.get(key, None) def get_service_type(self, service_type): - key = '{service_type}_service_type'.format(service_type=service_type) + key = _make_key('service_type', service_type) return self.config.get(key, service_type) def get_service_name(self, service_type): - key = '{service_type}_service_name'.format(service_type=service_type) + key = _make_key('service_name', service_type) return self.config.get(key, None) def get_endpoint(self, service_type): - key = '{service_type}_endpoint'.format(service_type=service_type) + key = _make_key('endpoint', service_type) return self.config.get(key, None) @property diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index 1b98b8aee..be2b8fe4a 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -225,6 +225,25 @@ def test_legacy_client_object_store(self, mock_get_session_endpoint): auth_version='2.0', timeout=None) + def test_legacy_client_object_store_endpoint(self): + mock_client = mock.Mock() + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + config_dict['object_store_endpoint'] = 'http://example.com/v2' + cc = cloud_config.CloudConfig( + "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + cc.get_legacy_client('object-store', mock_client) + mock_client.assert_called_with( + preauthtoken=mock.ANY, + os_options={ + 'auth_token': mock.ANY, + 'region_name': 'region-al', + 'object_storage_url': 'http://example.com/v2' + }, + preauthurl='http://example.com/v2', + auth_version='2.0', + timeout=None) + @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') def test_legacy_client_image(self, mock_get_session_endpoint): mock_client = mock.Mock() From 0aefe152af40436f532e3b37ff8ab602a0af6e6f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 14 Nov 2015 12:12:39 -0500 Subject: [PATCH 0656/3836] Fix name of the object-store api key There is only one value for this, and it's not consumed in the process of making a swift client, so the chances that anyone set it to something else are pretty much nil. However, for completeness we should make it the right name, as "object-store" is the service key name for swift, not "object". Change-Id: I395c1c44a2f50996b61dff22e07149b8dd13eda9 --- os_client_config/defaults.json | 2 +- os_client_config/schema.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/os_client_config/defaults.json b/os_client_config/defaults.json index 208641aff..eb8162e4e 100644 --- a/os_client_config/defaults.json +++ b/os_client_config/defaults.json @@ -13,7 +13,7 @@ "image_api_version": "2", "image_format": "qcow2", "network_api_version": "2", - "object_api_version": "1", + "object_store_api_version": "1", "orchestration_api_version": "1", "secgroup_source": "neutron", "volume_api_version": "1" diff --git a/os_client_config/schema.json b/os_client_config/schema.json index dfd1f4a9b..8110d58e9 100644 --- a/os_client_config/schema.json +++ b/os_client_config/schema.json @@ -87,7 +87,7 @@ "default": "2", "type": "string" }, - "object_api_version": { + "object_store_api_version": { "name": "Object Storage API Version", "description": "Object Storage API Version", "default": "1", @@ -114,7 +114,7 @@ "image_format", "interface", "network_api_version", - "object_api_version", + "object_store_api_version", "secgroup_source", "volume_api_version" ] From 26412cbe83fa84c756ce635c6660282984c4fe81 Mon Sep 17 00:00:00 2001 From: Caleb Boylan Date: Tue, 10 Nov 2015 14:12:10 -0800 Subject: [PATCH 0657/3836] Make functional object tests actually run Object tests were being skipped over because we were checking for the wrong service name. Also fixes list_containers() method. Change-Id: Ia0b71cefb327eaaf5f641067d0c80b91c93e6c45 --- shade/_tasks.py | 4 +-- shade/openstackcloud.py | 10 +++++--- shade/tests/functional/test_object.py | 37 +++++++++++++++++---------- 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 54be6d677..35151db42 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -419,7 +419,7 @@ def main(self, client): class ContainerList(task_manager.Task): def main(self, client): - return client.swift_client.list(**self.args) + return client.swift_client.get_account(**self.args)[1] class ObjectCapabilities(task_manager.Task): @@ -444,7 +444,7 @@ def main(self, client): class ObjectList(task_manager.Task): def main(self, client): - return client.swift_client.list(**self.args) + return client.swift_client.get_container(**self.args)[1] class ObjectMetadata(task_manager.Task): diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 063d5d6df..541bbbbbe 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3258,9 +3258,10 @@ def _delete_server( raise OpenStackCloudException( "Error in deleting server: {0}".format(e)) - def list_containers(self): + def list_containers(self, full_listing=True): try: - return manager.submitTask(_tasks.ContainerList()) + return self.manager.submitTask(_tasks.ContainerList( + full_listing=full_listing)) except swift_exceptions.ClientException as e: raise OpenStackCloudException( "Container list failed: %s (%s/%s)" % ( @@ -3456,9 +3457,10 @@ def create_object( raise OpenStackCloudException( 'Failed at action ({action}) [{error}]:'.format(**r)) - def list_objects(self, container): + def list_objects(self, container, full_listing=True): try: - return self.manager.submitTask(_tasks.ObjectList(container)) + return self.manager.submitTask(_tasks.ObjectList( + container=container, full_listing=full_listing)) except swift_exceptions.ClientException as e: raise OpenStackCloudException( "Object list failed: %s (%s/%s)" % ( diff --git a/shade/tests/functional/test_object.py b/shade/tests/functional/test_object.py index de83e09e9..736d69135 100644 --- a/shade/tests/functional/test_object.py +++ b/shade/tests/functional/test_object.py @@ -33,15 +33,17 @@ class TestObject(base.TestCase): def setUp(self): super(TestObject, self).setUp() self.cloud = openstack_cloud(cloud='devstack') - if not self.cloud.has_service('object'): + if not self.cloud.has_service('object-store'): self.skipTest('Object service not supported by cloud') def test_create_object(self): '''Test uploading small and large files.''' - container = str(uuid.uuid4()) - self.addDetail('container', content.text_content(container)) - self.addCleanup(self.cloud.delete_container, container) - self.cloud.create_container(container) + container_name = str(uuid.uuid4()) + self.addDetail('container', content.text_content(container_name)) + self.addCleanup(self.cloud.delete_container, container_name) + self.cloud.create_container(container_name) + self.assertEqual(container_name, + self.cloud.list_containers()[0]['name']) sizes = ( (64 * 1024, 1), # 64K, one segment (50 * 1024 ** 2, 5) # 50MB, 5 segments @@ -53,14 +55,21 @@ def test_create_object(self): sparse_file.write("\0") sparse_file.flush() name = 'test-%d' % size - self.cloud.create_object(container, name, sparse_file.name, + self.cloud.create_object(container_name, name, + sparse_file.name, segment_size=segment_size) - self.assertFalse(self.cloud.is_object_stale(container, name, - sparse_file.name)) + self.assertFalse(self.cloud.is_object_stale( + container_name, name, + sparse_file.name + ) + ) self.assertIsNotNone( - self.cloud.get_object_metadata(container, name)) - self.assertEqual([name], self.cloud.list_objects(container)) - self.cloud.delete_object(container, name) - self.assertEmpty(self.cloud.list_objects(container)) - self.assertEqual([container], self.cloud.list_containers()) - self.cloud.delete_container(container) + self.cloud.get_object_metadata(container_name, name)) + self.assertEqual( + name, + self.cloud.list_objects(container_name)[0]['name']) + self.cloud.delete_object(container_name, name) + self.assertEqual([], self.cloud.list_objects(container_name)) + self.assertEqual(container_name, + self.cloud.list_containers()[0]['name']) + self.cloud.delete_container(container_name) From 21438f412f686aa511193db511f68ef4983940c9 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 16 Nov 2015 10:14:03 -0500 Subject: [PATCH 0658/3836] Stop using uuid in functional tests Some functional tests were using the uuid library for unique names. The testing framework provides us a method, getUniqueString(), for just such a thing, so we should use that instead. Change-Id: I08b8ef91b9cd467ade9fac1a73a5bd8976e207ed --- shade/tests/functional/test_image.py | 3 +-- shade/tests/functional/test_object.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/shade/tests/functional/test_image.py b/shade/tests/functional/test_image.py index 89c760a04..8c630a4e7 100644 --- a/shade/tests/functional/test_image.py +++ b/shade/tests/functional/test_image.py @@ -20,7 +20,6 @@ """ import tempfile -import uuid from shade import openstack_cloud from shade.tests import base @@ -37,7 +36,7 @@ def test_create_image(self): test_image = tempfile.NamedTemporaryFile(delete=False) test_image.write('\0' * 1024 * 1024) test_image.close() - image_name = 'test-image-%s' % uuid.uuid4() + image_name = self.getUniqueString('image') try: self.cloud.create_image(name=image_name, filename=test_image.name, diff --git a/shade/tests/functional/test_object.py b/shade/tests/functional/test_object.py index 736d69135..e2818238e 100644 --- a/shade/tests/functional/test_object.py +++ b/shade/tests/functional/test_object.py @@ -20,7 +20,6 @@ """ import tempfile -import uuid from testtools import content @@ -38,7 +37,7 @@ def setUp(self): def test_create_object(self): '''Test uploading small and large files.''' - container_name = str(uuid.uuid4()) + container_name = self.getUniqueString('container') self.addDetail('container', content.text_content(container_name)) self.addCleanup(self.cloud.delete_container, container_name) self.cloud.create_container(container_name) From 4834756fd14f68cab89d630e0b9fbaa277123459 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 16 Nov 2015 10:30:26 -0500 Subject: [PATCH 0659/3836] Add test for os_nova_flavor Ansible module Change-Id: I23603d664cc4885f1900d2a36bc1647ff96f0cfa --- .../ansible/roles/nova_flavor/tasks/main.yml | 53 +++++++++++++++++++ shade/tests/ansible/run.yml | 1 + 2 files changed, 54 insertions(+) create mode 100644 shade/tests/ansible/roles/nova_flavor/tasks/main.yml diff --git a/shade/tests/ansible/roles/nova_flavor/tasks/main.yml b/shade/tests/ansible/roles/nova_flavor/tasks/main.yml new file mode 100644 index 000000000..c034bfc70 --- /dev/null +++ b/shade/tests/ansible/roles/nova_flavor/tasks/main.yml @@ -0,0 +1,53 @@ +--- +- name: Create public flavor + os_nova_flavor: + cloud: "{{ cloud }}" + state: present + name: ansible_public_flavor + is_public: True + ram: 1024 + vcpus: 1 + disk: 10 + ephemeral: 10 + swap: 1 + flavorid: 12345 + +- name: Delete public flavor + os_nova_flavor: + cloud: "{{ cloud }}" + state: absent + name: ansible_public_flavor + +- name: Create private flavor + os_nova_flavor: + cloud: "{{ cloud }}" + state: present + name: ansible_private_flavor + is_public: False + ram: 1024 + vcpus: 1 + disk: 10 + ephemeral: 10 + swap: 1 + flavorid: 12345 + +- name: Delete private flavor + os_nova_flavor: + cloud: "{{ cloud }}" + state: absent + name: ansible_private_flavor + +- name: Create flavor (defaults) + os_nova_flavor: + cloud: "{{ cloud }}" + state: present + name: ansible_defaults_flavor + ram: 1024 + vcpus: 1 + disk: 10 + +- name: Delete flavor (defaults) + os_nova_flavor: + cloud: "{{ cloud }}" + state: absent + name: ansible_defaults_flavor diff --git a/shade/tests/ansible/run.yml b/shade/tests/ansible/run.yml index a75d87bae..a582253ad 100644 --- a/shade/tests/ansible/run.yml +++ b/shade/tests/ansible/run.yml @@ -12,3 +12,4 @@ - { role: subnet, tags: subnet} - { role: router, tags: router} - { role: object, tags: object} + - { role: nova_flavor, tags: nova_flavor} From 0189dd248a1f1f1672a3f37dd518533372fe4fc9 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 16 Nov 2015 11:40:10 -0500 Subject: [PATCH 0660/3836] Add test for os_user Ansible module This also re-orders the role names in run.yml to be in alphabetical order. Change-Id: Ia6c3a9b5f1cb2e9394f5a1070f6716da0e478e61 --- shade/tests/ansible/roles/user/tasks/main.yml | 30 +++++++++++++++++++ shade/tests/ansible/run.yml | 9 +++--- 2 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 shade/tests/ansible/roles/user/tasks/main.yml diff --git a/shade/tests/ansible/roles/user/tasks/main.yml b/shade/tests/ansible/roles/user/tasks/main.yml new file mode 100644 index 000000000..6585ca582 --- /dev/null +++ b/shade/tests/ansible/roles/user/tasks/main.yml @@ -0,0 +1,30 @@ +--- +- name: Create user + os_user: + cloud: "{{ cloud }}" + state: present + name: ansible_user + password: secret + email: ansible.user@nowhere.net + domain: default + default_project: demo + register: user + +- debug: var=user + +- name: Update user + os_user: + cloud: "{{ cloud }}" + state: present + name: ansible_user + password: secret + email: updated.ansible.user@nowhere.net + register: updateduser + +- debug: var=updateduser + +- name: Delete user + os_user: + cloud: "{{ cloud }}" + state: absent + name: ansible_user diff --git a/shade/tests/ansible/run.yml b/shade/tests/ansible/run.yml index a582253ad..e582a3b97 100644 --- a/shade/tests/ansible/run.yml +++ b/shade/tests/ansible/run.yml @@ -8,8 +8,9 @@ - { role: client_config, tags: client_config } - { role: keypair, tags: keypair } - { role: network, tags: network } + - { role: nova_flavor, tags: nova_flavor } + - { role: object, tags: object } + - { role: router, tags: router } - { role: security_group, tags: security_group } - - { role: subnet, tags: subnet} - - { role: router, tags: router} - - { role: object, tags: object} - - { role: nova_flavor, tags: nova_flavor} + - { role: subnet, tags: subnet } + - { role: user, tags: user } From 72190ec19110e35f834b0a226d0c2ac872eccb79 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 18 Nov 2015 09:45:38 -0500 Subject: [PATCH 0661/3836] Add user group assignment API Add methods for adding and removing users to/from groups and checking for membership. This also updates the task_manager code to look for more types that we don't want to munchify because one of the new keystone API methods being called returned a bool value. This replaces Ida3cff3acdc1406c5e6d61500766a292565191fc Change-Id: Ib34c116010312ed26b042621fcf2e7b5b774424f --- shade/_tasks.py | 15 +++++ shade/openstackcloud.py | 80 +++++++++++++++++++++++++++ shade/task_manager.py | 4 +- shade/tests/functional/test_users.py | 31 +++++++++-- shade/tests/unit/test_task_manager.py | 55 +++++++++++++++--- shade/tests/unit/test_users.py | 62 +++++++++++++++++++++ 6 files changed, 233 insertions(+), 14 deletions(-) create mode 100644 shade/tests/unit/test_users.py diff --git a/shade/_tasks.py b/shade/_tasks.py index 35151db42..5f5320ef9 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -42,6 +42,21 @@ def main(self, client): return client.keystone_client.users.get(**self.args) +class UserAddToGroup(task_manager.Task): + def main(self, client): + return client.keystone_client.users.add_to_group(**self.args) + + +class UserCheckInGroup(task_manager.Task): + def main(self, client): + return client.keystone_client.users.check_in_group(**self.args) + + +class UserRemoveFromGroup(task_manager.Task): + def main(self, client): + return client.keystone_client.users.remove_from_group(**self.args) + + class ProjectList(task_manager.Task): def main(self, client): return client._project_manager.list() diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index c6dba5cd5..01664f9b7 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -554,6 +554,86 @@ def delete_user(self, name_or_id): self.list_users.invalidate(self) return True + def _get_user_and_group(self, user_name_or_id, group_name_or_id): + user = self.get_user(user_name_or_id) + if not user: + raise OpenStackCloudException( + 'User {user} not found'.format(user=user_name_or_id)) + + group = self.get_group(group_name_or_id) + if not group: + raise OpenStackCloudException( + 'Group {user} not found'.format(user=group_name_or_id)) + + return (user, group) + + def add_user_to_group(self, name_or_id, group_name_or_id): + """Add a user to a group. + + :param string name_or_id: User name or ID + :param string group_name_or_id: Group name or ID + + :raises: ``OpenStackCloudException`` if something goes wrong during + the openstack API call + """ + user, group = self._get_user_and_group(name_or_id, group_name_or_id) + + with _utils.shade_exceptions( + "Error adding user {user} to group {group}".format( + user=name_or_id, group=group_name_or_id) + ): + self.manager.submitTask( + _tasks.UserAddToGroup(user=user['id'], group=group['id']) + ) + + def is_user_in_group(self, name_or_id, group_name_or_id): + """Check to see if a user is in a group. + + :param string name_or_id: User name or ID + :param string group_name_or_id: Group name or ID + + :returns: True if user is in the group, False otherwise + + :raises: ``OpenStackCloudException`` if something goes wrong during + the openstack API call + """ + user, group = self._get_user_and_group(name_or_id, group_name_or_id) + + try: + return self.manager.submitTask( + _tasks.UserCheckInGroup(user=user['id'], group=group['id']) + ) + except keystoneauth1.exceptions.http.NotFound: + # Because the keystone API returns either True or raises an + # exception, which is awesome. + return False + except OpenStackCloudException: + raise + except Exception as e: + raise OpenStackCloudException( + "Error adding user {user} to group {group}: {err}".format( + user=name_or_id, group=group_name_or_id, err=str(e)) + ) + + def remove_user_from_group(self, name_or_id, group_name_or_id): + """Remove a user from a group. + + :param string name_or_id: User name or ID + :param string group_name_or_id: Group name or ID + + :raises: ``OpenStackCloudException`` if something goes wrong during + the openstack API call + """ + user, group = self._get_user_and_group(name_or_id, group_name_or_id) + + with _utils.shade_exceptions( + "Error removing user {user} from group {group}".format( + user=name_or_id, group=group_name_or_id) + ): + self.manager.submitTask( + _tasks.UserRemoveFromGroup(user=user['id'], group=group['id']) + ) + @property def glance_client(self): if self._glance_client is None: diff --git a/shade/task_manager.py b/shade/task_manager.py index 258f7fcb2..c96e86617 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -79,9 +79,11 @@ def wait(self): if self._exception: six.reraise(type(self._exception), self._exception, self._traceback) + if type(self._result) == list: return meta.obj_list_to_dict(self._result) - elif not isinstance(self._result, types.GeneratorType): + elif type(self._result) not in (bool, int, float, str, set, + types.GeneratorType): return meta.obj_to_dict(self._result) else: return self._result diff --git a/shade/tests/functional/test_users.py b/shade/tests/functional/test_users.py index 4b83ec875..2167121c0 100644 --- a/shade/tests/functional/test_users.py +++ b/shade/tests/functional/test_users.py @@ -19,9 +19,6 @@ Functional tests for `shade` user methods. """ -import random -import string - from shade import operator_cloud from shade import OpenStackCloudException from shade.tests import base @@ -31,8 +28,7 @@ class TestUsers(base.TestCase): def setUp(self): super(TestUsers, self).setUp() self.cloud = operator_cloud(cloud='devstack-admin') - self.user_prefix = 'test_user' + ''.join( - random.choice(string.ascii_lowercase) for _ in range(5)) + self.user_prefix = self.getUniqueString('user') self.addCleanup(self._cleanup_users) def _cleanup_users(self): @@ -111,3 +107,28 @@ def test_update_user(self): self.assertEqual(user_name + '2', new_user['name']) self.assertEqual('somebody@nowhere.com', new_user['email']) self.assertFalse(new_user['enabled']) + + def test_users_and_groups(self): + if self.cloud.cloud_config.get_api_version('identity') in ('2', '2.0'): + self.skipTest('Identity service does not support groups') + + group_name = self.getUniqueString('group') + self.addCleanup(self.cloud.delete_group, group_name) + + # Create a group + group = self.cloud.create_group(group_name, 'test group') + self.assertIsNotNone(group) + + # Create a user + user_name = self.user_prefix + '_ug' + user_email = 'nobody@nowhere.com' + user = self._create_user(name=user_name, email=user_email) + self.assertIsNotNone(user) + + # Add the user to the group + self.cloud.add_user_to_group(user_name, group_name) + self.assertTrue(self.cloud.is_user_in_group(user_name, group_name)) + + # Remove them from the group + self.cloud.remove_user_from_group(user_name, group_name) + self.assertFalse(self.cloud.is_user_in_group(user_name, group_name)) diff --git a/shade/tests/unit/test_task_manager.py b/shade/tests/unit/test_task_manager.py index 8c9074a85..e45db7282 100644 --- a/shade/tests/unit/test_task_manager.py +++ b/shade/tests/unit/test_task_manager.py @@ -13,10 +13,8 @@ # limitations under the License. -import mock import types -import shade from shade import task_manager from shade.tests.unit import base @@ -35,6 +33,31 @@ def main(self, client): yield 1 +class TestTaskInt(task_manager.Task): + def main(self, client): + return int(1) + + +class TestTaskFloat(task_manager.Task): + def main(self, client): + return float(2.0) + + +class TestTaskStr(task_manager.Task): + def main(self, client): + return "test" + + +class TestTaskBool(task_manager.Task): + def main(self, client): + return True + + +class TestTaskSet(task_manager.Task): + def main(self, client): + return set([1, 2]) + + class TestTaskManager(base.TestCase): def setUp(self): @@ -50,10 +73,26 @@ def test_wait_re_raise(self): """ self.assertRaises(TestException, self.manager.submitTask, TestTask()) - @mock.patch.object(shade.meta, 'obj_to_dict') - @mock.patch.object(shade.meta, 'obj_list_to_dict') - def test_dont_munchify_generators(self, mock_ol2d, mock_o2d): + def test_dont_munchify_generators(self): ret = self.manager.submitTask(TestTaskGenerator()) - self.assertEqual(types.GeneratorType, type(ret)) - self.assertFalse(mock_o2d.called) - self.assertFalse(mock_ol2d.called) + self.assertIsInstance(ret, types.GeneratorType) + + def test_dont_munchify_int(self): + ret = self.manager.submitTask(TestTaskInt()) + self.assertIsInstance(ret, int) + + def test_dont_munchify_float(self): + ret = self.manager.submitTask(TestTaskFloat()) + self.assertIsInstance(ret, float) + + def test_dont_munchify_str(self): + ret = self.manager.submitTask(TestTaskStr()) + self.assertIsInstance(ret, str) + + def test_dont_munchify_bool(self): + ret = self.manager.submitTask(TestTaskBool()) + self.assertIsInstance(ret, bool) + + def test_dont_munchify_set(self): + ret = self.manager.submitTask(TestTaskSet()) + self.assertIsInstance(ret, set) diff --git a/shade/tests/unit/test_users.py b/shade/tests/unit/test_users.py new file mode 100644 index 000000000..ed1ee1428 --- /dev/null +++ b/shade/tests/unit/test_users.py @@ -0,0 +1,62 @@ +# -*- 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. + + +import mock +import munch + +import shade +from shade.tests.unit import base + + +class TestUsers(base.TestCase): + + def setUp(self): + super(TestUsers, self).setUp() + self.cloud = shade.operator_cloud(validate=False) + + @mock.patch.object(shade.OpenStackCloud, 'get_user') + @mock.patch.object(shade.OperatorCloud, 'get_group') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_add_user_to_group(self, mock_keystone, mock_group, mock_user): + mock_user.return_value = munch.Munch(dict(id=1)) + mock_group.return_value = munch.Munch(dict(id=2)) + self.cloud.add_user_to_group("user", "group") + mock_keystone.users.add_to_group.assert_called_once_with( + user=1, group=2 + ) + + @mock.patch.object(shade.OpenStackCloud, 'get_user') + @mock.patch.object(shade.OperatorCloud, 'get_group') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_is_user_in_group(self, mock_keystone, mock_group, mock_user): + mock_user.return_value = munch.Munch(dict(id=1)) + mock_group.return_value = munch.Munch(dict(id=2)) + mock_keystone.users.check_in_group.return_value = True + self.assertTrue(self.cloud.is_user_in_group("user", "group")) + mock_keystone.users.check_in_group.assert_called_once_with( + user=1, group=2 + ) + + @mock.patch.object(shade.OpenStackCloud, 'get_user') + @mock.patch.object(shade.OperatorCloud, 'get_group') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_remove_user_from_group(self, mock_keystone, mock_group, + mock_user): + mock_user.return_value = munch.Munch(dict(id=1)) + mock_group.return_value = munch.Munch(dict(id=2)) + self.cloud.remove_user_from_group("user", "group") + mock_keystone.users.remove_from_group.assert_called_once_with( + user=1, group=2 + ) From ef80430afc83b89b560dbcf4b48ac3a19eacd86a Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 18 Nov 2015 15:10:37 -0500 Subject: [PATCH 0662/3836] Add test for os_user_group Ansible module NOTE: This won't work yet until this PR is merged: https://github.com/ansible/ansible-modules-core/pull/2492 Change-Id: I4e346e5da958c7bada78901405ca8d2887bdc023 --- .../ansible/roles/user_group/tasks/main.yml | 31 +++++++++++++++++++ shade/tests/ansible/run.yml | 1 + 2 files changed, 32 insertions(+) create mode 100644 shade/tests/ansible/roles/user_group/tasks/main.yml diff --git a/shade/tests/ansible/roles/user_group/tasks/main.yml b/shade/tests/ansible/roles/user_group/tasks/main.yml new file mode 100644 index 000000000..a0074e2dc --- /dev/null +++ b/shade/tests/ansible/roles/user_group/tasks/main.yml @@ -0,0 +1,31 @@ +--- +- name: Create user + os_user: + cloud: "{{ cloud }}" + state: present + name: ansible_user + password: secret + email: ansible.user@nowhere.net + domain: default + default_project: demo + register: user + +- name: Assign user to nonadmins group + os_user_group: + cloud: "{{ cloud }}" + state: present + user: ansible_user + group: nonadmins + +- name: Remove user from nonadmins group + os_user_group: + cloud: "{{ cloud }}" + state: absent + user: ansible_user + group: nonadmins + +- name: Delete user + os_user: + cloud: "{{ cloud }}" + state: absent + name: ansible_user diff --git a/shade/tests/ansible/run.yml b/shade/tests/ansible/run.yml index e582a3b97..0f26abe5f 100644 --- a/shade/tests/ansible/run.yml +++ b/shade/tests/ansible/run.yml @@ -14,3 +14,4 @@ - { role: security_group, tags: security_group } - { role: subnet, tags: subnet } - { role: user, tags: user } + - { role: user_group, tags: user_group } From 10e96bcb7bed2ca8eaf0185b46dbb882bd8b2b7c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 19 Nov 2015 17:15:08 -0500 Subject: [PATCH 0663/3836] Only pass timeout to swift if we have a value swiftclient does not have the built-in None detection that keystoneauth has, so we have to do it ourselves. Change-Id: I3fcf60dd2f3350045b824899ac48b02809452a4b --- os_client_config/cloud_config.py | 6 ++-- os_client_config/tests/test_cloud_config.py | 38 +++++++++++++++++++-- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 32fd3b6d5..3174f60c6 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -297,7 +297,7 @@ def _get_swift_client(self, client_class, **kwargs): endpoint = self.get_session_endpoint(service_key='object-store') if not endpoint: return None - return client_class( + swift_kwargs = dict( preauthurl=endpoint, preauthtoken=token, auth_version=self.get_api_version('identity'), @@ -305,8 +305,10 @@ def _get_swift_client(self, client_class, **kwargs): auth_token=token, object_storage_url=endpoint, region_name=self.get_region_name()), - timeout=self.api_timeout, ) + if self.config['api_timeout'] is not None: + swift_kwargs['timeout'] = float(self.config['api_timeout']) + return client_class(**swift_kwargs) def get_cache_expiration_time(self): if self._openstack_config: diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index be2b8fe4a..45d262f30 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -168,6 +168,18 @@ def test_get_session(self, mock_session): auth=mock.ANY, verify=True, cert=None, timeout=None) + @mock.patch.object(ksa_session, 'Session') + def test_get_session_with_timeout(self, mock_session): + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + config_dict['api_timeout'] = 9 + cc = cloud_config.CloudConfig( + "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + cc.get_session() + mock_session.assert_called_with( + auth=mock.ANY, + verify=True, cert=None, timeout=9) + @mock.patch.object(ksa_session, 'Session') def test_override_session_endpoint(self, mock_session): config_dict = defaults.get_defaults() @@ -214,6 +226,27 @@ def test_legacy_client_object_store(self, mock_get_session_endpoint): cc = cloud_config.CloudConfig( "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('object-store', mock_client) + mock_client.assert_called_with( + preauthtoken=mock.ANY, + os_options={ + 'auth_token': mock.ANY, + 'region_name': 'region-al', + 'object_storage_url': 'http://example.com/v2' + }, + preauthurl='http://example.com/v2', + auth_version='2.0') + + @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + def test_legacy_client_object_store_timeout( + self, mock_get_session_endpoint): + mock_client = mock.Mock() + mock_get_session_endpoint.return_value = 'http://example.com/v2' + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + config_dict['api_timeout'] = 9 + cc = cloud_config.CloudConfig( + "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + cc.get_legacy_client('object-store', mock_client) mock_client.assert_called_with( preauthtoken=mock.ANY, os_options={ @@ -223,7 +256,7 @@ def test_legacy_client_object_store(self, mock_get_session_endpoint): }, preauthurl='http://example.com/v2', auth_version='2.0', - timeout=None) + timeout=9.0) def test_legacy_client_object_store_endpoint(self): mock_client = mock.Mock() @@ -241,8 +274,7 @@ def test_legacy_client_object_store_endpoint(self): 'object_storage_url': 'http://example.com/v2' }, preauthurl='http://example.com/v2', - auth_version='2.0', - timeout=None) + auth_version='2.0') @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') def test_legacy_client_image(self, mock_get_session_endpoint): From 9c59002116e755d9ac7f14b11c3774c6848d4a3c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 20 Nov 2015 12:10:16 -0500 Subject: [PATCH 0664/3836] Fix lack of parenthesis around boolean logic The legacy envvar prefix support broke a workaround for use of OS_ envvars in test cases. This is only currently a problem for shade functional tests, but it IS a bug. Change-Id: Ia0cbb4e2ea7ce6eeeea36533e057bd53a830d44c --- os_client_config/config.py | 2 +- os_client_config/tests/test_environ.py | 40 ++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 8d92c8fe7..5f7c402ae 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -87,7 +87,7 @@ def _get_os_environ(envvar_prefix=None): # This makes the or below be OS_ or OS_ which is a no-op envvar_prefix = 'OS_' environkeys = [k for k in os.environ.keys() - if k.startswith('OS_') or k.startswith(envvar_prefix) + if (k.startswith('OS_') or k.startswith(envvar_prefix)) and not k.startswith('OS_TEST') # infra CI var and not k.startswith('OS_STD') # infra CI var ] diff --git a/os_client_config/tests/test_environ.py b/os_client_config/tests/test_environ.py index 1e804fc3c..0ff800fd4 100644 --- a/os_client_config/tests/test_environ.py +++ b/os_client_config/tests/test_environ.py @@ -111,3 +111,43 @@ def test_config_file_override(self): vendor_files=[self.vendor_yaml]) cc = c.get_one_cloud('_test-cloud_') self._assert_cloud_details(cc) + + +class TestEnvvars(base.TestCase): + + def test_no_envvars(self): + self.useFixture( + fixtures.EnvironmentVariable('NOVA_USERNAME', 'nova')) + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + self.assertRaises( + exceptions.OpenStackConfigException, c.get_one_cloud, 'envvars') + + def test_test_envvars(self): + self.useFixture( + fixtures.EnvironmentVariable('NOVA_USERNAME', 'nova')) + self.useFixture( + fixtures.EnvironmentVariable('OS_STDERR_CAPTURE', 'True')) + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + self.assertRaises( + exceptions.OpenStackConfigException, c.get_one_cloud, 'envvars') + + def test_have_envvars(self): + self.useFixture( + fixtures.EnvironmentVariable('NOVA_USERNAME', 'nova')) + self.useFixture( + fixtures.EnvironmentVariable('OS_USERNAME', 'user')) + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + cc = c.get_one_cloud('envvars') + self.assertEqual(cc.config['auth']['username'], 'user') + + def test_old_envvars(self): + self.useFixture( + fixtures.EnvironmentVariable('NOVA_USERNAME', 'nova')) + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + envvar_prefix='NOVA_') + cc = c.get_one_cloud('envvars') + self.assertEqual(cc.config['auth']['username'], 'nova') From c84179154d90247c784ddaf76cbd84eeaeaf3cca Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 20 Nov 2015 08:54:53 -0500 Subject: [PATCH 0665/3836] Bump os-client-config requirement The 1.11.0 release of os-client-config contains a bugfix for the swift api timeout paramter issue. Be explicit that this version of os-client-config is required for proper operation of shade. The 1.11.1 release contains a bugfix for the 1.11.0 release. Change-Id: Iabcb62593143473a7459ad01122a734d4230ab77 --- requirements.txt | 2 +- shade/tests/unit/test_object.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 53190d185..c41f168f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ munch decorator jsonpatch ipaddress -os-client-config>=1.10.1 +os-client-config>=1.11.1 requestsexceptions>=1.1.1 six diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index a3749be51..3daedf91e 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -48,7 +48,6 @@ def test_swift_client(self, get_session_mock, swift_mock): preauthurl='danzig', preauthtoken='yankee', auth_version=mock.ANY, - timeout=None, os_options=dict( object_storage_url='danzig', auth_token='yankee', From e9d4223cf9a15695c890bd3981237173977d986b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 19 Nov 2015 17:03:02 -0500 Subject: [PATCH 0666/3836] Remove default values from innner method There are node codepaths where we want to not pass in values to _upload_image_task - so remove the default values because they're confusing. Change-Id: I3575e9d7f31548aa99ef58bca15dfc8042155855 --- shade/openstackcloud.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 01664f9b7..0e3720121 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1982,8 +1982,8 @@ def _upload_image_put(self, name, filename, **image_kwargs): return self.get_image(image.id) def _upload_image_task( - self, name, filename, container, current_image=None, - wait=True, timeout=None, **image_properties): + self, name, filename, container, current_image, + wait, timeout, **image_properties): self.create_object( container, name, filename, md5=image_properties.get('md5', None), From 23b49f840df9c19b6ae48118187af2b3c13519d9 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 19 Nov 2015 16:43:22 -0500 Subject: [PATCH 0667/3836] Make sure timeouts are floats timeouts passed to iterate_timeout need to be int or float. If something goes south and a timeout gets converted to a string, this blows up. Luckily, python is good at converting string floats to real floats. Change-Id: I3489532f0e43e1c289ecb50c77dd80e47ac65ab2 --- shade/_utils.py | 7 +++++ shade/tests/functional/test_floating_ip.py | 19 +++--------- shade/tests/unit/test_shade.py | 36 ++++++++++++++++++++++ 3 files changed, 47 insertions(+), 15 deletions(-) diff --git a/shade/_utils.py b/shade/_utils.py index 865ecaaca..1f6cf2d7a 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -40,6 +40,13 @@ def _iterate_timeout(timeout, message, wait=2): """ + try: + wait = float(wait) + except ValueError: + raise exc.OpenStackCloudException( + "Wait value must be an int or float value. {wait} given" + " instead".format(wait=wait)) + start = time.time() count = 0 while (timeout is None) or (time.time() < start + timeout): diff --git a/shade/tests/functional/test_floating_ip.py b/shade/tests/functional/test_floating_ip.py index 36a68c2a5..9a978bc7b 100644 --- a/shade/tests/functional/test_floating_ip.py +++ b/shade/tests/functional/test_floating_ip.py @@ -20,29 +20,18 @@ """ import pprint -import time from novaclient import exceptions as nova_exc from testtools import content +from shade import _utils from shade import openstack_cloud from shade import meta from shade.exc import OpenStackCloudException -from shade.exc import OpenStackCloudTimeout from shade.tests import base from shade.tests.functional.util import pick_flavor, pick_image -def _iterate_timeout(timeout, message): - start = time.time() - count = 0 - while (timeout is None) or (time.time() < start + timeout): - count += 1 - yield count - time.sleep(2) - raise OpenStackCloudTimeout(message) - - class TestFloatingIP(base.TestCase): timeout = 60 @@ -123,7 +112,7 @@ def _cleanup_servers(self): for i in self.nova.servers.list(): if i.name.startswith(self.new_item_name): self.nova.servers.delete(i) - for _ in _iterate_timeout( + for _ in _utils._iterate_timeout( self.timeout, "Timeout deleting servers"): try: self.nova.servers.get(server=i) @@ -222,7 +211,7 @@ def test_add_auto_ip(self): # ToDo: remove the following iteration when create_server waits for # the IP to be attached ip = None - for _ in _iterate_timeout( + for _ in _utils._iterate_timeout( self.timeout, "Timeout waiting for IP address to be attached"): ip = meta.get_server_external_ipv4(self.cloud, new_server) if ip is not None: @@ -242,7 +231,7 @@ def test_detach_ip_from_server(self): # ToDo: remove the following iteration when create_server waits for # the IP to be attached ip = None - for _ in _iterate_timeout( + for _ in _utils._iterate_timeout( self.timeout, "Timeout waiting for IP address to be attached"): ip = meta.get_server_external_ipv4(self.cloud, new_server) if ip is not None: diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index f184dd839..e4c463895 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -18,9 +18,11 @@ import glanceclient from heatclient import client as heat_client from neutronclient.common import exceptions as n_exc +import testtools from os_client_config import cloud_config import shade +from shade import _utils from shade import exc from shade import meta from shade.tests.unit import base @@ -390,3 +392,37 @@ def test_list_servers_detailed(self, self.assertEquals(len(r), mock_get_hostvars_from_server.call_count) self.assertEquals('server1', r[0]['name']) self.assertEquals('server2', r[1]['name']) + + def test_iterate_timeout_bad_wait(self): + with testtools.ExpectedException( + exc.OpenStackCloudException, + "Wait value must be an int or float value."): + for count in _utils._iterate_timeout( + 1, "test_iterate_timeout_bad_wait", wait="timeishard"): + pass + + @mock.patch('time.sleep') + def test_iterate_timeout_str_wait(self, mock_sleep): + iter = _utils._iterate_timeout( + 10, "test_iterate_timeout_str_wait", wait="1.6") + next(iter) + next(iter) + mock_sleep.assert_called_with(1.6) + + @mock.patch('time.sleep') + def test_iterate_timeout_int_wait(self, mock_sleep): + iter = _utils._iterate_timeout( + 10, "test_iterate_timeout_int_wait", wait=1) + next(iter) + next(iter) + mock_sleep.assert_called_with(1.0) + + @mock.patch('time.sleep') + def test_iterate_timeout_timeout(self, mock_sleep): + message = "timeout test" + with testtools.ExpectedException( + exc.OpenStackCloudTimeout, + message): + for count in _utils._iterate_timeout(0.1, message, wait=1): + pass + mock_sleep.assert_called_with(1.0) From 27154a9c14ea7c3fc03147043f8a3093b14a1a1f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 20 Nov 2015 09:31:04 -0500 Subject: [PATCH 0668/3836] Fix a 60 second unit test The no-addresses test was waiting for delete timeout and was therefore sticking around for 60 seconds for a sideeffect we did not care about. Add a second get return value so that the get in delete returns without a wait-poll loop. Also, set the list server interval to 0 so that we done have a bunch of tests with a single 6 second wait. Change-Id: Ia62ace386a183892f9446aee4e9d4cbbee528dec --- shade/tests/unit/test_create_server.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index 2ed172a65..8abf6cc4d 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -34,6 +34,7 @@ def setUp(self): config = os_client_config.OpenStackConfig() self.client = OpenStackCloud( cloud_config=config.get_one_cloud(validate=False)) + self.client._SERVER_LIST_AGE = 0 def test_create_server_with_create_exception(self): """ @@ -160,7 +161,8 @@ def test_create_server_wait(self): 'server-name', 'image-id', 'flavor-id', wait=True), fake_server) - def test_create_server_no_addresses(self): + @patch('time.sleep') + def test_create_server_no_addresses(self, mock_sleep): """ Test that create_server with a wait throws an exception if the server doesn't have addresses. @@ -170,11 +172,13 @@ def test_create_server_no_addresses(self): fake_server = fakes.FakeServer('1234', '', 'ACTIVE') config = { "servers.create.return_value": build_server, - "servers.get.return_value": build_server, + "servers.get.return_value": [build_server, None], "servers.list.side_effect": [ - [build_server], [fake_server]] + [build_server], [fake_server]], + "servers.delete.return_value": None, } OpenStackCloud.nova_client = Mock(**config) + self.client._SERVER_LIST_AGE = 0 with patch.object(OpenStackCloud, "add_ips_to_server", return_value=fake_server): self.assertRaises( From 9d7229f65b9862e27c58490921b04d7d4c388e02 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 20 Nov 2015 10:10:43 -0500 Subject: [PATCH 0669/3836] Allow specifying cloud name to ansible tests It would be *really* cool and useful and stuff to be able to run the Ansible playbooks against more than just devstack. This change allows that. Now you can send the -c option to the tox command line: tox -e ansible -- -c coolcloud Or, specific tests: tox -e ansible -- -c coolcloud auth network Going against devstack is still: tox -e ansible tox -e ansible auth network Change-Id: I666a09aee3d283865c3813d67edfb75123057c22 --- extras/run-ansible-tests.sh | 50 +++++++++++++++++++++++++++---------- tox.ini | 2 +- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/extras/run-ansible-tests.sh b/extras/run-ansible-tests.sh index 7797cc349..816eb53fe 100755 --- a/extras/run-ansible-tests.sh +++ b/extras/run-ansible-tests.sh @@ -5,27 +5,50 @@ # Script used to setup a tox environment for running Ansible. This is meant # to be called by tox (via tox.ini). To run the Ansible tests, use: # -# tox -e ansible [TAG] +# tox -e ansible [TAG ...] +# or +# tox -e ansible -- -c cloudX [TAG ...] # # USAGE: -# run-ansible-tests.sh [TAG] +# run-ansible-tests.sh -e ENVDIR [-c CLOUD] [TAG ...] # # PARAMETERS: -# Directory of the tox environment to use for testing. -# [TAG] Optional list of space-separated tags to control which -# modules are tested. +# -e ENVDIR Directory of the tox environment to use for testing. +# -c CLOUD Name of the cloud to use for testing. +# Defaults to "devstack-admin". +# [TAG ...] Optional list of space-separated tags to control which +# modules are tested. # # EXAMPLES: # # Run all Ansible tests -# run-ansible-tests.sh ansible +# run-ansible-tests.sh -e ansible # -# # Run auth, keypair, and network tests -# run-ansible-tests.sh ansible auth keypair network +# # Run auth, keypair, and network tests against cloudX +# run-ansible-tests.sh -e ansible -c cloudX auth keypair network ############################################################################# -ENVDIR=$1 -shift -TAGS=$( echo "$@" | tr ' ' , ) + +CLOUD="devstack-admin" +ENVDIR= + +while getopts "c:e:" opt +do + case $opt in + c) CLOUD=${OPTARG} ;; + e) ENVDIR=${OPTARG} ;; + ?) echo "Invalid option: -${OPTARG}" + exit 1;; + esac +done + +if [ -z ${ENVDIR} ] +then + echo "Option -e is required" + exit 1 +fi + +shift $((OPTIND-1)) +TAGS=$( echo "$*" | tr ' ' , ) if [ -d ${ENVDIR}/ansible ] then @@ -44,8 +67,9 @@ source $ENVDIR/ansible/hacking/env-setup # Run the shade Ansible tests tag_opt="" -if [ "${TAGS}" != "" ] +if [ ! -z ${TAGS} ] then tag_opt="--tags ${TAGS}" fi -ansible-playbook -vvv ./shade/tests/ansible/run.yml -e "cloud=devstack-admin" ${tag_opt} + +ansible-playbook -vvv ./shade/tests/ansible/run.yml -e "cloud=${CLOUD}" ${tag_opt} diff --git a/tox.ini b/tox.ini index 586ede98a..2ca4fefd5 100644 --- a/tox.ini +++ b/tox.ini @@ -33,7 +33,7 @@ commands = python setup.py testr --coverage --testr-args='{posargs}' [testenv:ansible] # Need to pass some env vars for the Ansible playbooks passenv = HOME USER -commands = {toxinidir}/extras/run-ansible-tests.sh {envdir} {posargs} +commands = {toxinidir}/extras/run-ansible-tests.sh -e {envdir} {posargs} [flake8] # Infra does not follow hacking, nor the broken E12* things From c6c1b7326b492f20e758e9f5f4df58bd820d9339 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 19 Nov 2015 15:36:27 -0500 Subject: [PATCH 0670/3836] Add test for os_port Ansible module Change-Id: I0ae6ada61fe7d409b6bb45f559cfee4146585efc --- shade/tests/ansible/roles/port/tasks/main.yml | 79 +++++++++++++++++++ shade/tests/ansible/roles/port/vars/main.yml | 4 + shade/tests/ansible/run.yml | 1 + 3 files changed, 84 insertions(+) create mode 100644 shade/tests/ansible/roles/port/tasks/main.yml create mode 100644 shade/tests/ansible/roles/port/vars/main.yml diff --git a/shade/tests/ansible/roles/port/tasks/main.yml b/shade/tests/ansible/roles/port/tasks/main.yml new file mode 100644 index 000000000..5f3040be9 --- /dev/null +++ b/shade/tests/ansible/roles/port/tasks/main.yml @@ -0,0 +1,79 @@ +--- +- name: Create network + os_network: + cloud: "{{ cloud }}" + state: present + name: "{{ network_name }}" + external: True + +- name: Create subnet + os_subnet: + cloud: "{{ cloud }}" + state: present + name: "{{ subnet_name }}" + network_name: "{{ network_name }}" + cidr: 10.5.5.0/24 + +- name: Create port (no security group) + os_port: + cloud: "{{ cloud }}" + state: present + name: "{{ port_name }}" + network: "{{ network_name }}" + no_security_groups: True + fixed_ips: + - ip_address: 10.5.5.69 + register: port + +- debug: var=port + +- name: Delete port (no security group) + os_port: + cloud: "{{ cloud }}" + state: absent + name: "{{ port_name }}" + +- name: Create security group + os_security_group: + cloud: "{{ cloud }}" + state: present + name: "{{ secgroup_name }}" + description: Test group + +- name: Create port (with security group) + os_port: + cloud: "{{ cloud }}" + state: present + name: "{{ port_name }}" + network: "{{ network_name }}" + fixed_ips: + - ip_address: 10.5.5.69 + security_groups: + - "{{ secgroup_name }}" + register: port + +- debug: var=port + +- name: Delete port (with security group) + os_port: + cloud: "{{ cloud }}" + state: absent + name: "{{ port_name }}" + +- name: Delete security group + os_security_group: + cloud: "{{ cloud }}" + state: absent + name: "{{ secgroup_name }}" + +- name: Delete subnet + os_subnet: + cloud: "{{ cloud }}" + state: absent + name: "{{ subnet_name }}" + +- name: Delete network + os_network: + cloud: "{{ cloud }}" + state: absent + name: "{{ network_name }}" diff --git a/shade/tests/ansible/roles/port/vars/main.yml b/shade/tests/ansible/roles/port/vars/main.yml new file mode 100644 index 000000000..a81f6a2ea --- /dev/null +++ b/shade/tests/ansible/roles/port/vars/main.yml @@ -0,0 +1,4 @@ +network_name: ansible_port_network +subnet_name: ansible_port_subnet +port_name: ansible_port +secgroup_name: ansible_port_secgroup diff --git a/shade/tests/ansible/run.yml b/shade/tests/ansible/run.yml index 0f26abe5f..32d8cdad5 100644 --- a/shade/tests/ansible/run.yml +++ b/shade/tests/ansible/run.yml @@ -10,6 +10,7 @@ - { role: network, tags: network } - { role: nova_flavor, tags: nova_flavor } - { role: object, tags: object } + - { role: port, tags: port } - { role: router, tags: router } - { role: security_group, tags: security_group } - { role: subnet, tags: subnet } From 2cc62575e7c23c581eab7a228a7beb0abd4a79ad Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 19 Nov 2015 12:40:28 -0500 Subject: [PATCH 0671/3836] Do not send 'router:external' unless it is set There are some setups where sending 'router:external' can produce a Forbidden error. For an example, see: https://github.com/ansible/ansible-modules-core/issues/2435 Let's not send it unless it is explicitly set to True. Change-Id: I59c43bf3eab09ed620cd1e8e5e445037767eb8bb --- shade/openstackcloud.py | 7 ++++- shade/tests/unit/test_network.py | 50 ++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 shade/tests/unit/test_network.py diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 01664f9b7..7f4ad334b 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1589,9 +1589,14 @@ def create_network(self, name, shared=False, admin_state_up=True, 'name': name, 'shared': shared, 'admin_state_up': admin_state_up, - 'router:external': external } + # Do not send 'router:external' unless it is explicitly + # set since sending it *might* cause "Forbidden" errors in + # some situations. It defaults to False in the client, anyway. + if external: + network['router:external'] = True + with _utils.neutron_exceptions( "Error creating network {0}".format(name)): net = self.manager.submitTask( diff --git a/shade/tests/unit/test_network.py b/shade/tests/unit/test_network.py new file mode 100644 index 000000000..02b41f7f7 --- /dev/null +++ b/shade/tests/unit/test_network.py @@ -0,0 +1,50 @@ +# 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 shade +from shade.tests.unit import base + + +class TestNetwork(base.TestCase): + + def setUp(self): + super(TestNetwork, self).setUp() + self.cloud = shade.openstack_cloud(validate=False) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_network(self, mock_neutron): + self.cloud.create_network("netname") + mock_neutron.create_network.assert_called_with( + body=dict( + network=dict( + name='netname', + shared=False, + admin_state_up=True + ) + ) + ) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_network_external(self, mock_neutron): + self.cloud.create_network("netname", external=True) + mock_neutron.create_network.assert_called_with( + body=dict( + network={ + 'name': 'netname', + 'shared': False, + 'admin_state_up': True, + 'router:external': True + } + ) + ) From 630ce6704befd0caa5e1f25f69ceb5a8224c117c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 18 Nov 2015 17:10:28 -0500 Subject: [PATCH 0672/3836] boot-from-volume and network params for server create Two of the more complex arguments for server creation are booting from volume and attaching to specific networks. It's possible to add some syntactic sugage to the constructor to make this work in a less insane way. Change-Id: Icdfd15c0c6ada79cee0fb15a31e1cb727869d215 --- shade/openstackcloud.py | 116 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 106 insertions(+), 10 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 01664f9b7..c8bc0e970 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3011,6 +3011,73 @@ def add_ips_to_server( server, wait=wait, timeout=timeout, reuse=reuse) return server + def _get_boot_from_volume_kwargs( + self, image, boot_from_volume, boot_volume, volume_size, + terminate_volume, volumes, kwargs): + if boot_volume or boot_from_volume or volumes: + kwargs.setdefault('block_device_mapping_v2', []) + else: + return kwargs + + # If we have boot_from_volume but no root volume, then we're + # booting an image from volume + if boot_volume: + volume = self.get_volume(boot_volume) + if not volume: + raise OpenStackCloudException( + 'Volume {boot_volume} is not a valid volume' + ' in {cloud}:{region}'.format( + boot_volume=boot_volume, + cloud=self.name, region=self.region_name)) + block_mapping = { + 'boot_index': '0', + 'delete_on_termination': terminate_volume, + 'destination_type': 'volume', + 'uuid': volume['id'], + 'source_type': 'volume', + } + kwargs['block_device_mapping'].append(block_mapping) + elif boot_from_volume: + + if not hasattr(image, 'id'): + image_obj = self.get_image(image) + if not image_obj: + raise OpenStackCloudException( + 'Image {image} is not a valid image in' + ' {cloud}:{region}'.format( + image=image, + cloud=self.name, region=self.region_name)) + else: + image = image_obj + + block_mapping = { + 'boot_index': '0', + 'delete_on_termination': terminate_volume, + 'destination_type': 'volume', + 'uuid': image['id'], + 'source_type': 'image', + 'volume_size': volume_size, + } + del kwargs['image'] + kwargs['block_device_mapping_v2'].append(block_mapping) + for volume in volumes: + volume_obj = self.get_volume(volume) + if not volume_obj: + raise OpenStackCloudException( + 'Volume {volume} is not a valid volume' + ' in {cloud}:{region}'.format( + volume=volume, + cloud=self.name, region=self.region_name)) + block_mapping = { + 'boot_index': None, + 'delete_on_termination': False, + 'destination_type': 'volume', + 'uuid': volume['id'], + 'source_type': 'volume', + } + kwargs['block_device_mapping_v2'].append(block_mapping) + return kwargs + @_utils.valid_kwargs( 'meta', 'files', 'userdata', 'reservation_id', 'return_raw', 'min_count', @@ -3023,6 +3090,8 @@ def create_server( auto_ip=True, ips=None, ip_pool=None, root_volume=None, terminate_volume=False, wait=False, timeout=180, reuse_ips=True, + network=None, boot_from_volume=False, volume_size='50', + boot_volume=None, volumes=[], **kwargs): """Create a virtual server instance. @@ -3035,10 +3104,13 @@ def create_server( :param ip_pool: Name of the network or floating IP pool to get an address from. (defaults to None) :param root_volume: Name or id of a volume to boot from + (defaults to None - deprecated, use boot_volume) + :param boot_volume: Name or id of a volume to boot from (defaults to None) :param terminate_volume: If booting from a volume, whether it should be deleted when the server is destroyed. (defaults to False) + :param volumes: (optional) A list of volumes to attach to the server :param meta: (optional) A dict of arbitrary key/value metadata to store for this server. Both keys and values must be <=255 characters. @@ -3081,9 +3153,24 @@ def create_server( to the server in Nova. Defaults to False. :param timeout: (optional) Seconds to wait, defaults to 60. See the ``wait`` parameter. + :param reuse_ips: (optional) Whether to attempt to reuse pre-existing + floating ips should a floating IP be + needed (defaults to True) + :param network: (optional) Network name or id to attach the server to. + Mutually exclusive with the nics parameter. + :param boot_from_volume: Whether to boot from volume. 'boot_volume' + implies True, but boot_from_volume=True with + no boot_volume is valid and will create a + volume from the image and use that. + :param volume_size: When booting an image from volume, how big should + the created volume be? Defaults to 50. :returns: A dict representing the created server. :raises: OpenStackCloudException on operation error. """ + # nova cli calls this boot_volume. Let's be the same + if root_volume and not boot_volume: + boot_volume = root_volume + if 'nics' in kwargs and not isinstance(kwargs['nics'], list): if isinstance(kwargs['nics'], dict): # Be nice and help the user out @@ -3092,19 +3179,28 @@ def create_server( raise OpenStackCloudException( 'nics parameter to create_server takes a list of dicts.' ' Got: {nics}'.format(nics=kwargs['nics'])) - if root_volume: - if terminate_volume: - suffix = ':::1' - else: - suffix = ':::0' - volume_id = self.get_volume_id(root_volume) + suffix - if 'block_device_mapping' not in kwargs: - kwargs['block_device_mapping'] = dict() - kwargs['block_device_mapping']['vda'] = volume_id + if network and 'nics' not in kwargs: + network_obj = self.get_network(name_or_id=network) + if not network_obj: + raise OpenStackCloudException( + 'Network {network} is not a valid network in' + ' {cloud}:{region}'.format( + network=network, + cloud=self.name, region=self.region_name)) + + kwargs['nics'] = [{'net-id': network_obj['id']}] + + kwargs['image'] = image + + kwargs = self._get_boot_from_volume_kwargs( + image=image, boot_from_volume=boot_from_volume, + boot_volume=boot_volume, volume_size=str(volume_size), + terminate_volume=terminate_volume, + volumes=volumes, kwargs=kwargs) with _utils.shade_exceptions("Error in creating instance"): server = self.manager.submitTask(_tasks.ServerCreate( - name=name, image=image, flavor=flavor, **kwargs)) + name=name, flavor=flavor, **kwargs)) server_id = server.id if not wait: # This is a direct get task call to skip the list_servers From 05bd92c8a180de7404a145e9ddd88c1bdb05d40f Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Sat, 21 Nov 2015 16:30:22 -0500 Subject: [PATCH 0673/3836] Fix warnings.filterwarnings call We need to use a keyword argument for the category since it is not the second parameter to filterwarnings(). Change-Id: I0f393a742468c150e0debbc3453cf38ebc2fb58e --- shade/openstackcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index e0981dc13..a87334e4d 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -160,7 +160,7 @@ def __init__( self.log.debug( "Turning off Insecure SSL warnings since verify=False") category = requestsexceptions.InsecureRequestWarning - warnings.filterwarnings('ignore', category) + warnings.filterwarnings('ignore', category=category) self._servers = [] self._servers_time = 0 From b17bbcdef9acfaf6e2b47671c9982d3378045961 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 22 Nov 2015 13:16:44 -0500 Subject: [PATCH 0674/3836] Add support for secure.yaml file for auth info Almost nothing in clouds.yaml is secret, but the file has to be treated as if it were because of the passwords or other secrets contained in it. This makes it difficult to put clouds.yaml into a public or broadly accessible config repository. Add support for having a second optional file, secure.yaml, which can contain any value you can put in clouds.yaml and which will be overlayed on top of clouds.yaml values. Most people probably do not need this, but for folks with complex cloud configs with teams of people working on them, this reduces the amount of things that have to be managed by the privileged system. Change-Id: I631d826588b0a0b1f36244caa7982dd42d9eb498 --- README.rst | 28 ++++++++++++++++++++++ os_client_config/config.py | 33 +++++++++++++++++++++++++- os_client_config/tests/base.py | 12 +++++++++- os_client_config/tests/test_config.py | 21 ++++++++++------ os_client_config/tests/test_environ.py | 14 +++++++---- 5 files changed, 95 insertions(+), 13 deletions(-) diff --git a/README.rst b/README.rst index f16bbc091..0bdc18290 100644 --- a/README.rst +++ b/README.rst @@ -145,6 +145,34 @@ as a result of a chosen plugin need to go into the auth dict. For password auth, this includes `auth_url`, `username` and `password` as well as anything related to domains, projects and trusts. +Splitting Secrets +----------------- + +In some scenarios, such as configuragtion managment controlled environments, +it might be eaiser to have secrets in one file and non-secrets in another. +This is fully supported via an optional file `secure.yaml` which follows all +the same location rules as `clouds.yaml`. It can contain anything you put +in `clouds.yaml` and will take precedence over anything in the `clouds.yaml` +file. + +:: + + # clouds.yaml + clouds: + internap: + profile: internap + auth: + username: api-55f9a00fb2619 + project_name: inap-17037 + regions: + - ams01 + - nyj01 + # secure.yaml + clouds: + internap: + auth: + password: XXXXXXXXXXXXXXXXX + SSL Settings ------------ diff --git a/os_client_config/config.py b/os_client_config/config.py index 5f7c402ae..c12b25a40 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -51,6 +51,11 @@ for d in CONFIG_SEARCH_PATH for s in YAML_SUFFIXES + JSON_SUFFIXES ] +SECURE_FILES = [ + os.path.join(d, 'secure' + s) + for d in CONFIG_SEARCH_PATH + for s in YAML_SUFFIXES + JSON_SUFFIXES +] VENDOR_FILES = [ os.path.join(d, 'clouds-public' + s) for d in CONFIG_SEARCH_PATH @@ -102,6 +107,20 @@ def _get_os_environ(envvar_prefix=None): return ret +def _merge_clouds(old_dict, new_dict): + """Like dict.update, except handling nested dicts.""" + ret = old_dict.copy() + for (k, v) in new_dict.items(): + if isinstance(v, dict): + if k in ret: + ret[k] = _merge_clouds(ret[k], v) + else: + ret[k] = v.copy() + else: + ret[k] = v + return ret + + def _auth_update(old_dict, new_dict): """Like dict.update, except handling the nested dict called auth.""" for (k, v) in new_dict.items(): @@ -119,20 +138,29 @@ class OpenStackConfig(object): def __init__(self, config_files=None, vendor_files=None, override_defaults=None, force_ipv4=None, - envvar_prefix=None): + envvar_prefix=None, secure_files=None): self._config_files = config_files or CONFIG_FILES + self._secure_files = secure_files or SECURE_FILES self._vendor_files = vendor_files or VENDOR_FILES config_file_override = os.environ.pop('OS_CLIENT_CONFIG_FILE', None) if config_file_override: self._config_files.insert(0, config_file_override) + secure_file_override = os.environ.pop('OS_CLIENT_SECURE_FILE', None) + if secure_file_override: + self._secure_files.insert(0, secure_file_override) + self.defaults = defaults.get_defaults() if override_defaults: self.defaults.update(override_defaults) # First, use a config file if it exists where expected self.config_filename, self.cloud_config = self._load_config_file() + _, secure_config = self._load_secure_file() + if secure_config: + self.cloud_config = _merge_clouds( + self.cloud_config, secure_config) if not self.cloud_config: self.cloud_config = {'clouds': {}} @@ -220,6 +248,9 @@ def __init__(self, config_files=None, vendor_files=None, def _load_config_file(self): return self._load_yaml_json_file(self._config_files) + def _load_secure_file(self): + return self._load_yaml_json_file(self._secure_files) + def _load_vendor_file(self): return self._load_yaml_json_file(self._vendor_files) diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py index 3d94e2581..33a868d33 100644 --- a/os_client_config/tests/base.py +++ b/os_client_config/tests/base.py @@ -64,7 +64,6 @@ 'auth': { 'auth_url': 'http://example.com/v2', 'username': 'testuser', - 'password': 'testpass', 'project_name': 'testproject', }, 'region-name': 'test-region', @@ -112,6 +111,15 @@ } }, } +SECURE_CONF = { + 'clouds': { + '_test_cloud_no_vendor': { + 'auth': { + 'password': 'testpass', + }, + } + } +} NO_CONF = { 'cache': {'max_age': 1}, } @@ -135,6 +143,7 @@ def setUp(self): tdir = self.useFixture(fixtures.TempDir()) conf['cache']['path'] = tdir.path self.cloud_yaml = _write_yaml(conf) + self.secure_yaml = _write_yaml(SECURE_CONF) self.vendor_yaml = _write_yaml(VENDOR_CONF) self.no_yaml = _write_yaml(NO_CONF) @@ -155,6 +164,7 @@ def _assert_cloud_details(self, cc): self.assertIsNone(cc.cloud) self.assertIn('username', cc.auth) self.assertEqual('testuser', cc.auth['username']) + self.assertEqual('testpass', cc.auth['password']) self.assertFalse(cc.config['image_api_use_tasks']) self.assertTrue('project_name' in cc.auth or 'project_id' in cc.auth) if 'project_name' in cc.auth: diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index d225b7c17..aff8c6d25 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -30,7 +30,8 @@ class TestConfig(base.TestCase): def test_get_all_clouds(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + vendor_files=[self.vendor_yaml], + secure_files=[self.no_yaml]) clouds = c.get_all_clouds() # We add one by hand because the regions cloud is going to exist # twice since it has two regions in it @@ -74,7 +75,8 @@ def test_get_one_cloud_auth_override_defaults(self): def test_get_one_cloud_with_config_files(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + vendor_files=[self.vendor_yaml], + secure_files=[self.secure_yaml]) self.assertIsInstance(c.cloud_config, dict) self.assertIn('cache', c.cloud_config) self.assertIsInstance(c.cloud_config['cache'], dict) @@ -129,7 +131,8 @@ def test_no_environ(self): def test_fallthrough(self): c = config.OpenStackConfig(config_files=[self.no_yaml], - vendor_files=[self.no_yaml]) + vendor_files=[self.no_yaml], + secure_files=[self.no_yaml]) for k in os.environ.keys(): if k.startswith('OS_'): self.useFixture(fixtures.EnvironmentVariable(k)) @@ -137,7 +140,8 @@ def test_fallthrough(self): def test_prefer_ipv6_true(self): c = config.OpenStackConfig(config_files=[self.no_yaml], - vendor_files=[self.no_yaml]) + vendor_files=[self.no_yaml], + secure_files=[self.no_yaml]) cc = c.get_one_cloud(cloud='defaults', validate=False) self.assertTrue(cc.prefer_ipv6) @@ -155,7 +159,8 @@ def test_force_ipv4_true(self): def test_force_ipv4_false(self): c = config.OpenStackConfig(config_files=[self.no_yaml], - vendor_files=[self.no_yaml]) + vendor_files=[self.no_yaml], + secure_files=[self.no_yaml]) cc = c.get_one_cloud(cloud='defaults', validate=False) self.assertFalse(cc.force_ipv4) @@ -166,7 +171,8 @@ def test_get_one_cloud_auth_merge(self): self.assertEqual('testpass', cc.auth['password']) def test_get_cloud_names(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml]) + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + secure_files=[self.no_yaml]) self.assertEqual( ['_test-cloud-domain-id_', '_test-cloud-int-project_', @@ -177,7 +183,8 @@ def test_get_cloud_names(self): ], sorted(c.get_cloud_names())) c = config.OpenStackConfig(config_files=[self.no_yaml], - vendor_files=[self.no_yaml]) + vendor_files=[self.no_yaml], + secure_files=[self.no_yaml]) for k in os.environ.keys(): if k.startswith('OS_'): self.useFixture(fixtures.EnvironmentVariable(k)) diff --git a/os_client_config/tests/test_environ.py b/os_client_config/tests/test_environ.py index 0ff800fd4..b75db1c61 100644 --- a/os_client_config/tests/test_environ.py +++ b/os_client_config/tests/test_environ.py @@ -29,6 +29,8 @@ def setUp(self): fixtures.EnvironmentVariable('OS_AUTH_URL', 'https://example.com')) self.useFixture( fixtures.EnvironmentVariable('OS_USERNAME', 'testuser')) + self.useFixture( + fixtures.EnvironmentVariable('OS_PASSWORD', 'testpass')) self.useFixture( fixtures.EnvironmentVariable('OS_PROJECT_NAME', 'testproject')) self.useFixture( @@ -57,13 +59,15 @@ def test_envvar_prefer_ipv6_override(self): self.useFixture( fixtures.EnvironmentVariable('OS_PREFER_IPV6', 'false')) c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + vendor_files=[self.vendor_yaml], + secure_files=[self.secure_yaml]) cc = c.get_one_cloud('_test-cloud_') self.assertFalse(cc.prefer_ipv6) def test_environ_exists(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + vendor_files=[self.vendor_yaml], + secure_files=[self.secure_yaml]) cc = c.get_one_cloud('envvars') self._assert_cloud_details(cc) self.assertNotIn('auth_url', cc.config) @@ -78,7 +82,8 @@ def test_environ_exists(self): def test_environ_prefix(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml], - envvar_prefix='NOVA_') + envvar_prefix='NOVA_', + secure_files=[self.secure_yaml]) cc = c.get_one_cloud('envvars') self._assert_cloud_details(cc) self.assertNotIn('auth_url', cc.config) @@ -92,7 +97,8 @@ def test_environ_prefix(self): def test_get_one_cloud_with_config_files(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + vendor_files=[self.vendor_yaml], + secure_files=[self.secure_yaml]) self.assertIsInstance(c.cloud_config, dict) self.assertIn('cache', c.cloud_config) self.assertIsInstance(c.cloud_config['cache'], dict) From 4c7583b33a64bb9fcc0794b4ff83f57486c7f374 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 23 Nov 2015 09:33:39 -0500 Subject: [PATCH 0675/3836] Add test for os_image Ansible module Change-Id: I80c983e41a08df43f39376868290cf78e0085771 --- .../tests/ansible/roles/image/tasks/main.yml | 54 +++++++++++++++++++ shade/tests/ansible/roles/image/vars/main.yml | 1 + shade/tests/ansible/run.yml | 1 + 3 files changed, 56 insertions(+) create mode 100644 shade/tests/ansible/roles/image/tasks/main.yml create mode 100644 shade/tests/ansible/roles/image/vars/main.yml diff --git a/shade/tests/ansible/roles/image/tasks/main.yml b/shade/tests/ansible/roles/image/tasks/main.yml new file mode 100644 index 000000000..587e887b8 --- /dev/null +++ b/shade/tests/ansible/roles/image/tasks/main.yml @@ -0,0 +1,54 @@ +--- +- name: Create a test image file + shell: mktemp + register: tmp_file + +- name: Fill test image file to 1MB + shell: truncate -s 1048576 {{ tmp_file.stdout }} + +- name: Create raw image (defaults) + os_image: + cloud: "{{ cloud }}" + state: present + name: "{{ image_name }}" + filename: "{{ tmp_file.stdout }}" + disk_format: raw + register: image + +- debug: var=image + +- name: Delete raw image (defaults) + os_image: + cloud: "{{ cloud }}" + state: absent + name: "{{ image_name }}" + +- name: Create raw image (complex) + os_image: + cloud: "{{ cloud }}" + state: present + name: "{{ image_name }}" + filename: "{{ tmp_file.stdout }}" + disk_format: raw + is_public: True + min_disk: 10 + min_ram: 1024 + kernel: cirros-vmlinuz + ramdisk: cirros-initrd + properties: + cpu_arch: x86_64 + distro: ubuntu + register: image + +- debug: var=image + +- name: Delete raw image (complex) + os_image: + cloud: "{{ cloud }}" + state: absent + name: "{{ image_name }}" + +- name: Delete test image file + file: + name: "{{ tmp_file.stdout }}" + state: absent diff --git a/shade/tests/ansible/roles/image/vars/main.yml b/shade/tests/ansible/roles/image/vars/main.yml new file mode 100644 index 000000000..13efe7144 --- /dev/null +++ b/shade/tests/ansible/roles/image/vars/main.yml @@ -0,0 +1 @@ +image_name: ansible_image diff --git a/shade/tests/ansible/run.yml b/shade/tests/ansible/run.yml index 32d8cdad5..39556c023 100644 --- a/shade/tests/ansible/run.yml +++ b/shade/tests/ansible/run.yml @@ -6,6 +6,7 @@ roles: - { role: auth, tags: auth } - { role: client_config, tags: client_config } + - { role: image, tags: image } - { role: keypair, tags: keypair } - { role: network, tags: network } - { role: nova_flavor, tags: nova_flavor } From 31cdee11dd35040b051283f783e607f9ea51d710 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 23 Nov 2015 11:14:24 -0500 Subject: [PATCH 0676/3836] Fix for min_disk/min_ram in create_image API If min_disk or min_ram are passed in as keyword arguments, the values must be integers or glance hates us even more than usual. Change-Id: I1ed04174796e12258055840e9184aa9f8bcf3ea8 --- shade/openstackcloud.py | 4 ++++ shade/tests/functional/test_image.py | 2 ++ shade/tests/unit/test_caching.py | 9 +++++---- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index a87334e4d..9647ac9f8 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1963,6 +1963,10 @@ def _upload_image_put_v2(self, name, image_data, **image_kwargs): img_props = image_kwargs.pop('properties') for k, v in iter(img_props.items()): image_kwargs[k] = str(v) + # some MUST be integer + for k in ('min_disk', 'min_ram'): + if k in image_kwargs: + image_kwargs[k] = int(image_kwargs[k]) image = self.manager.submitTask(_tasks.ImageCreate( name=name, **image_kwargs)) self.manager.submitTask(_tasks.ImageUpload( diff --git a/shade/tests/functional/test_image.py b/shade/tests/functional/test_image.py index 8c630a4e7..e603acee9 100644 --- a/shade/tests/functional/test_image.py +++ b/shade/tests/functional/test_image.py @@ -42,6 +42,8 @@ def test_create_image(self): filename=test_image.name, disk_format='raw', container_format='bare', + min_disk=10, + min_ram=1024, wait=True) finally: self.cloud.delete_image(image_name, wait=True) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index a637c7994..82f95a699 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -294,13 +294,13 @@ def test_list_images_caches_steady_status(self, glance_mock): # therefore we should _not_ expect to see the new one here self.assertEqual([first_image], self.cloud.list_images()) - def _call_create_image(self, name, container=None): + def _call_create_image(self, name, container=None, **kwargs): imagefile = tempfile.NamedTemporaryFile(delete=False) imagefile.write(b'\0') imagefile.close() self.cloud.create_image( name, imagefile.name, container=container, wait=True, - is_public=False) + is_public=False, **kwargs) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, 'glance_client') @@ -336,12 +336,13 @@ def test_create_image_put_v2(self, glance_mock, mock_api_version): fake_image = fakes.FakeImage('42', '42 name', 'success') glance_mock.images.create.return_value = fake_image glance_mock.images.list.return_value = [fake_image] - self._call_create_image('42 name') + self._call_create_image('42 name', min_disk=0, min_ram=0) args = {'name': '42 name', 'container_format': 'bare', 'disk_format': 'qcow2', 'owner_specified.shade.md5': mock.ANY, 'owner_specified.shade.sha256': mock.ANY, - 'visibility': 'private'} + 'visibility': 'private', + 'min_disk': 0, 'min_ram': 0} glance_mock.images.create.assert_called_with(**args) glance_mock.images.upload.assert_called_with( image_data=mock.ANY, image_id=fake_image.id) From 7698842adbb31d5c5baab97d109768347c66aa5d Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 23 Nov 2015 16:30:54 -0500 Subject: [PATCH 0677/3836] Add test for os_volume Ansible module Change-Id: I2c0b537535f86d1057ec84b7d887ed0d9b4ecfc0 --- shade/tests/ansible/roles/volume/tasks/main.yml | 17 +++++++++++++++++ shade/tests/ansible/run.yml | 1 + 2 files changed, 18 insertions(+) create mode 100644 shade/tests/ansible/roles/volume/tasks/main.yml diff --git a/shade/tests/ansible/roles/volume/tasks/main.yml b/shade/tests/ansible/roles/volume/tasks/main.yml new file mode 100644 index 000000000..1479a0030 --- /dev/null +++ b/shade/tests/ansible/roles/volume/tasks/main.yml @@ -0,0 +1,17 @@ +--- +- name: Create volume + os_volume: + cloud: "{{ cloud }}" + state: present + size: 1 + display_name: ansible_volume + display_description: Test volume + register: vol + +- debug: var=vol + +- name: Delete volume + os_volume: + cloud: "{{ cloud }}" + state: absent + display_name: ansible_volume diff --git a/shade/tests/ansible/run.yml b/shade/tests/ansible/run.yml index 39556c023..c3a973a29 100644 --- a/shade/tests/ansible/run.yml +++ b/shade/tests/ansible/run.yml @@ -17,3 +17,4 @@ - { role: subnet, tags: subnet } - { role: user, tags: user } - { role: user_group, tags: user_group } + - { role: volume, tags: volume } From 9f23d6ef6718e77bbded25fe7f84b0a167adc5fd Mon Sep 17 00:00:00 2001 From: Alberto Gireud Date: Mon, 23 Nov 2015 16:25:33 -0600 Subject: [PATCH 0678/3836] Fix call to shade_exceptions in update_project Change-Id: Ib92b68f719895c32a464f05a97ddd48ac267a46a --- shade/openstackcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 9647ac9f8..2f66b3c94 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -391,7 +391,7 @@ def get_project(self, name_or_id, filters=None): return _utils._get_entity(self.search_projects, name_or_id, filters) def update_project(self, name_or_id, description=None, enabled=True): - with _utils.shade.exceptions( + with _utils.shade_exceptions( "Error in updating project {project}".format( project=name_or_id)): proj = self.get_project(name_or_id) From 4c1418f7b415beac59a2d9d170a5ed420f81be06 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 24 Nov 2015 11:15:39 -0500 Subject: [PATCH 0679/3836] Retry API calls if they get a Retryable failure keystoneauth1 Session throws exceptions based on RetriableConnectionFailure when the operation can be sanely retried. Since we have commuication encapsulated, there is no reason to not just retry. We should maybe in the future allow for configuration of number of times to retry - but let's start with one and go from there. Change-Id: Iacc6ac3d7eecbccfa7602fefb238602cc8a66cc4 --- shade/task_manager.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/shade/task_manager.py b/shade/task_manager.py index c96e86617..8de4b3add 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -22,6 +22,7 @@ import time import types +import keystoneauth1.exceptions import six from shade import _log @@ -90,7 +91,16 @@ def wait(self): def run(self, client): try: - self.done(self.main(client)) + # Retry one time if we get a retriable connection failure + try: + self.done(self.main(client)) + except keystoneauth1.exceptions.RetriableConnectionFailure: + client.log.debug( + "Connection failure for {name}, retrying".format( + name=type(self).__name__)) + self.done(self.main(client)) + except Exception: + raise except Exception as e: self.exception(e, sys.exc_info()[2]) From 014db1730c26d367dc0dc13ad75c4076ff0eb571 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 24 Nov 2015 16:33:35 -0500 Subject: [PATCH 0680/3836] Stop using nova client in test_compute The test_compute functional test code was using nova client directly instead of the shade API. Let's avoid that. Change-Id: Iac5650a036f006cf3d30ff44086d348cba7c303f --- shade/tests/functional/test_compute.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index 58a8662fa..afde4f9da 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -28,18 +28,17 @@ class TestCompute(base.TestCase): def setUp(self): super(TestCompute, self).setUp() self.cloud = openstack_cloud(cloud='devstack') - self.nova = self.cloud.nova_client - self.flavor = pick_flavor(self.nova.flavors.list()) + self.flavor = pick_flavor(self.cloud.list_flavors()) if self.flavor is None: self.assertFalse('no sensible flavor available') - self.image = pick_image(self.nova.images.list()) + self.image = pick_image(self.cloud.list_images()) if self.image is None: self.assertFalse('no sensible image available') def _cleanup_servers(self): - for i in self.nova.servers.list(): + for i in self.cloud.list_servers(): if i.name.startswith('test_create'): - self.nova.servers.delete(i) + self.cloud.delete_server(i) def test_create_server(self): self.addCleanup(self._cleanup_servers) From 0ea32e7dc011373c61e4d79d710a09e4ae404a9e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 15 Nov 2015 11:34:41 -0500 Subject: [PATCH 0681/3836] Handle cinder v2 It turns out that cinder v2 has a service_type of volumev2 because nobody thought of the children. But that's ok - we actually care about user experience around here. SO - take the sane approach and return service_type = volumev2 if service_type == volume and volume_api_version == 2. This way user code can all safely say "please give me the endpoint for the volume service" and can use the api_version parameter to specify which version they want. We should all possess righteous indignation about this patch. Change-Id: I15fc5ddd92345d78b6928f11a8d77cecd0427f7d --- os_client_config/cloud_config.py | 8 ++++++++ os_client_config/tests/test_cloud_config.py | 10 +++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 3174f60c6..18ea4c1eb 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -113,6 +113,14 @@ def get_api_version(self, service_type): def get_service_type(self, service_type): key = _make_key('service_type', service_type) + # Cinder did an evil thing where they defined a second service + # type in the catalog. Of course, that's insane, so let's hide this + # atrocity from the as-yet-unsullied eyes of our users. + # Of course, if the user requests a volumev2, that structure should + # still work. + if (service_type == 'volume' and + self.get_api_version(service_type).startswith('2')): + service_type = 'volumev2' return self.config.get(key, service_type) def get_service_name(self, service_type): diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index 45d262f30..9e683d1da 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -31,6 +31,7 @@ 'image_service_type': 'mage', 'identity_interface': 'admin', 'identity_service_name': 'locks', + 'volume_api_version': '1', 'auth': {'password': 'hunter2', 'username': 'AzureDiamond'}, } @@ -128,7 +129,7 @@ def test_ipv6(self): def test_getters(self): cc = cloud_config.CloudConfig("test1", "region-al", fake_services_dict) - self.assertEqual(['compute', 'identity', 'image'], + self.assertEqual(['compute', 'identity', 'image', 'volume'], sorted(cc.get_services())) self.assertEqual({'password': 'hunter2', 'username': 'AzureDiamond'}, cc.get_auth_args()) @@ -142,6 +143,8 @@ def test_getters(self): self.assertEqual('2', cc.get_api_version('compute')) self.assertEqual('mage', cc.get_service_type('image')) self.assertEqual('compute', cc.get_service_type('compute')) + self.assertEqual('1', cc.get_api_version('volume')) + self.assertEqual('volume', cc.get_service_type('volume')) self.assertEqual('http://compute.example.com', cc.get_endpoint('compute')) self.assertEqual(None, @@ -149,6 +152,11 @@ def test_getters(self): self.assertEqual(None, cc.get_service_name('compute')) self.assertEqual('locks', cc.get_service_name('identity')) + def test_volume_override(self): + cc = cloud_config.CloudConfig("test1", "region-al", fake_services_dict) + cc.config['volume_api_version'] = '2' + self.assertEqual('volumev2', cc.get_service_type('volume')) + def test_get_session_no_auth(self): config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) From 596a5d8b60bd0a853a6c42cf314efc7411a0e414 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 24 Nov 2015 16:44:34 -0500 Subject: [PATCH 0682/3836] Clean up compute functional tests Although the server names being created were actually unique across tests, this change will make the test_compute.py code more consistent with other functional test code by creating a server name prefix string with getUniqueString(). This also opens the door for use across new tests. It also changes the server cleanup code to just call delete_server() with the name of the instance for the test. Since tests can run in parallel, we don't want to delete an instance with a similar name out from under some other test. Change-Id: I1b0dc3f2759f77b7d420e45b3a7a2730868e6eb0 --- shade/tests/functional/test_compute.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index afde4f9da..b2352fd44 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -34,24 +34,23 @@ def setUp(self): self.image = pick_image(self.cloud.list_images()) if self.image is None: self.assertFalse('no sensible image available') - - def _cleanup_servers(self): - for i in self.cloud.list_servers(): - if i.name.startswith('test_create'): - self.cloud.delete_server(i) + self.server_prefix = self.getUniqueString('server') def test_create_server(self): - self.addCleanup(self._cleanup_servers) - server = self.cloud.create_server(name='test_create_server', + server_name = self.server_prefix + '_create_server' + self.addCleanup(self.cloud.delete_server, server_name) + server = self.cloud.create_server(name=server_name, image=self.image, flavor=self.flavor) - self.assertEquals(server['name'], 'test_create_server') + self.assertEquals(server['name'], server_name) self.assertEquals(server['image']['id'], self.image.id) self.assertEquals(server['flavor']['id'], self.flavor.id) def test_delete_server(self): - self.cloud.create_server(name='test_delete_server', - image=self.image, flavor=self.flavor) - server_deleted = self.cloud.delete_server('test_delete_server', + server_name = self.server_prefix + '_delete_server' + self.cloud.create_server(name=server_name, + image=self.image, + flavor=self.flavor) + server_deleted = self.cloud.delete_server(server_name, wait=True) self.assertIsNone(server_deleted) From 9f43f8a793652bf4a4e7e62f06eb3c8103173778 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 24 Nov 2015 17:07:45 -0500 Subject: [PATCH 0683/3836] Fix resource leak in test_compute Depending on the time between the create_server() and delete_server() calls, it's possible the delete does not actually find the server we want to delete, so we leave instances hanging around after our tests. Fix this by waiting for the servers with wait=True to create_server(). Change-Id: I908fdb94dcf5e410597a7cd3ecc6c962f2bfbce3 --- shade/tests/functional/test_compute.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index b2352fd44..106ba1143 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -40,7 +40,9 @@ def test_create_server(self): server_name = self.server_prefix + '_create_server' self.addCleanup(self.cloud.delete_server, server_name) server = self.cloud.create_server(name=server_name, - image=self.image, flavor=self.flavor) + image=self.image, + flavor=self.flavor, + wait=True) self.assertEquals(server['name'], server_name) self.assertEquals(server['image']['id'], self.image.id) self.assertEquals(server['flavor']['id'], self.flavor.id) @@ -49,7 +51,8 @@ def test_delete_server(self): server_name = self.server_prefix + '_delete_server' self.cloud.create_server(name=server_name, image=self.image, - flavor=self.flavor) + flavor=self.flavor, + wait=True) server_deleted = self.cloud.delete_server(server_name, wait=True) self.assertIsNone(server_deleted) From c01913dd75d8b6fa551cee8f7cdc913a89a11536 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 25 Nov 2015 10:53:45 -0500 Subject: [PATCH 0684/3836] Only log errors in exceptions on demand Rather than error logging all of them, allow the user a mechanism to error log the ones they care about. Also make inner_exception public so that people can make their own choices. Change-Id: I15973f52d3d68c24ac8ef7643a53ff0118e892cf --- shade/exc.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/shade/exc.py b/shade/exc.py index e5f2a3d89..efdccab8c 100644 --- a/shade/exc.py +++ b/shade/exc.py @@ -27,19 +27,22 @@ def __init__(self, message, extra_data=None): args.append(extra_data) super(OpenStackCloudException, self).__init__(*args) self.extra_data = extra_data - self._inner_exception = sys.exc_info() - if self._inner_exception and self._inner_exception[1]: - log.error(message, exc_info=self._inner_exception) + self.inner_exception = sys.exc_info() + self.orig_message = message + + def log_error(self, logger=log): + if self.inner_exception and self.inner_exception[1]: + logger.error(self.orig_message, exc_info=self.inner_exception) def __str__(self): message = Exception.__str__(self) if self.extra_data is not None: message = "%s (Extra: %s)" % (message, self.extra_data) - if (self._inner_exception and self._inner_exception[1] - and hasattr(self._inner_exception[1], 'message')): + if (self.inner_exception and self.inner_exception[1] + and hasattr(self.inner_exception[1], 'message')): message = "%s (Inner Exception: %s)" % ( message, - self._inner_exception[1].message) + self.inner_exception[1].message) return message From e1646ac1fd6663e8dd9c0d5d0c429cf932c899e8 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 25 Nov 2015 12:21:17 -0500 Subject: [PATCH 0685/3836] Adjust conditions when enable_snat is specified Some clouds barf about policy if the enable_snat attribute is sent during router creation. Only send it if it is different from the default (i.e., only when False). This is a fix for: https://github.com/ansible/ansible-modules-core/issues/2474 Change-Id: Id656029b9cf593fcb8e0771fa19c567126c565ee --- shade/openstackcloud.py | 7 +++++-- shade/tests/unit/test_shade.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 8c740b434..399b3ead0 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1629,8 +1629,11 @@ def _build_external_gateway_info(self, ext_gateway_net_id, enable_snat, info = {} if ext_gateway_net_id: info['network_id'] = ext_gateway_net_id - if enable_snat is not None: - info['enable_snat'] = enable_snat + # Only send enable_snat if it is different from the Neutron + # default of True. Sending it can cause a policy violation error + # on some clouds. + if enable_snat is not None and not enable_snat: + info['enable_snat'] = False if ext_fixed_ips: info['external_fixed_ips'] = ext_fixed_ips if info: diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index e4c463895..6a0fb7fa8 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -129,6 +129,37 @@ def test_create_router(self, mock_client): self.cloud.create_router(name='goofy', admin_state_up=True) self.assertTrue(mock_client.create_router.called) + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_router_with_enable_snat_True(self, mock_client): + """Do not send enable_snat when same as neutron default.""" + self.cloud.create_router(name='goofy', admin_state_up=True, + enable_snat=True) + mock_client.create_router.assert_called_once_with( + body=dict( + router=dict( + name='goofy', + admin_state_up=True, + ) + ) + ) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_router_with_enable_snat_False(self, mock_client): + """Send enable_snat when it is False.""" + self.cloud.create_router(name='goofy', admin_state_up=True, + enable_snat=False) + mock_client.create_router.assert_called_once_with( + body=dict( + router=dict( + name='goofy', + admin_state_up=True, + external_gateway_info=dict( + enable_snat=False + ) + ) + ) + ) + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') def test_add_router_interface(self, mock_client): self.cloud.add_router_interface({'id': '123'}, subnet_id='abc') From cc92c92870be48d6eae57869c4989125fe2eb8e4 Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Wed, 25 Nov 2015 09:37:45 -0800 Subject: [PATCH 0686/3836] Add BHS1 to OVH Change-Id: I0ef175edccbbc3e24803d02ab6809cfe1a68e0e8 --- doc/source/vendor-support.rst | 1 + os_client_config/vendors/ovh.json | 1 + 2 files changed, 2 insertions(+) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index a9dd5ef8c..90fd31fad 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -177,6 +177,7 @@ https://auth.cloud.ovh.net/v2.0 ============== ================ Region Name Human Name ============== ================ +BHS1 Beauharnois, QC SBG1 Strassbourg, FR GRA1 Gravelines, FR ============== ================ diff --git a/os_client_config/vendors/ovh.json b/os_client_config/vendors/ovh.json index cfd234b64..032741f83 100644 --- a/os_client_config/vendors/ovh.json +++ b/os_client_config/vendors/ovh.json @@ -5,6 +5,7 @@ "auth_url": "https://auth.cloud.ovh.net/v2.0" }, "regions": [ + "BHS1", "GRA1", "SBG1" ], From 02ac37fe8f58b6a969b16eb9b1b3039512ebbe41 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 25 Nov 2015 11:30:15 -0500 Subject: [PATCH 0687/3836] Make delete_server() return True/False To be consistent with our other delete API methods, make delete_server() return True if the delete succeeded, or False if the server was not deleted because it was not found. This also combines the functional testing of create_server() and delete_server() so that tests are more efficient by eliminating an extra instance creation. This is in anticipation of adding more tests that will create server instances and there are only so many resources available in the test environment. Change-Id: If1b1a3886df60fb064682caa23113f91b78a4090 --- shade/openstackcloud.py | 82 ++++++++++++++++---------- shade/tests/functional/test_compute.py | 16 +---- shade/tests/unit/test_delete_server.py | 8 +-- 3 files changed, 59 insertions(+), 47 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index c8bc0e970..24ffd3b13 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3301,55 +3301,77 @@ def rebuild_server(self, server_id, image_id, wait=False, timeout=180): def delete_server( self, name_or_id, wait=False, timeout=180, delete_ips=False): + """Delete a server instance. + + :param bool wait: If true, waits for server to be deleted. + :param int timeout: Seconds to wait for server deletion. + :param bool delete_ips: If true, deletes any floating IPs + associated with the instance. + + :returns: True if delete succeeded, False otherwise if the + server does not exist. + + :raises: OpenStackCloudException on operation error. + """ server = self.get_server(name_or_id) + if not server: + return False + + # This portion of the code is intentionally left as a separate + # private method in order to avoid an unnecessary API call to get + # a server we already have. return self._delete_server( server, wait=wait, timeout=timeout, delete_ips=delete_ips) def _delete_server( self, server, wait=False, timeout=180, delete_ips=False): - if server: - if delete_ips: - floating_ip = meta.get_server_ip(server, ext_tag='floating') - if floating_ip: - ips = self.search_floating_ips(filters={ - 'floating_ip_address': floating_ip}) - if len(ips) != 1: - raise OpenStackException( - "Tried to delete floating ip {floating_ip}" - " associated with server {id} but there was" - " an error finding it. Something is exceptionally" - " broken.".format( - floating_ip=floating_ip, - id=server['id'])) - self.delete_floating_ip(ips[0]['id']) - try: - self.manager.submitTask( - _tasks.ServerDelete(server=server['id'])) - except nova_exceptions.NotFound: - return - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Error in deleting server: {0}".format(e)) - else: - return + if not server: + return False + + if delete_ips: + floating_ip = meta.get_server_ip(server, ext_tag='floating') + if floating_ip: + ips = self.search_floating_ips(filters={ + 'floating_ip_address': floating_ip}) + if len(ips) != 1: + raise OpenStackException( + "Tried to delete floating ip {floating_ip}" + " associated with server {id} but there was" + " an error finding it. Something is exceptionally" + " broken.".format( + floating_ip=floating_ip, + id=server['id'])) + self.delete_floating_ip(ips[0]['id']) + + try: + self.manager.submitTask( + _tasks.ServerDelete(server=server['id'])) + except nova_exceptions.NotFound: + return False + except OpenStackCloudException: + raise + except Exception as e: + raise OpenStackCloudException( + "Error in deleting server: {0}".format(e)) + if not wait: - return + return True + for count in _utils._iterate_timeout( timeout, "Timed out waiting for server to get deleted."): try: server = self.get_server_by_id(server['id']) if not server: - return + break except nova_exceptions.NotFound: - return + break except OpenStackCloudException: raise except Exception as e: raise OpenStackCloudException( "Error in deleting server: {0}".format(e)) + return True def list_containers(self, full_listing=True): try: diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index 106ba1143..637261f9e 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -36,9 +36,8 @@ def setUp(self): self.assertFalse('no sensible image available') self.server_prefix = self.getUniqueString('server') - def test_create_server(self): - server_name = self.server_prefix + '_create_server' - self.addCleanup(self.cloud.delete_server, server_name) + def test_create_and_delete_server(self): + server_name = self.server_prefix + '_create_and_delete' server = self.cloud.create_server(name=server_name, image=self.image, flavor=self.flavor, @@ -46,16 +45,7 @@ def test_create_server(self): self.assertEquals(server['name'], server_name) self.assertEquals(server['image']['id'], self.image.id) self.assertEquals(server['flavor']['id'], self.flavor.id) - - def test_delete_server(self): - server_name = self.server_prefix + '_delete_server' - self.cloud.create_server(name=server_name, - image=self.image, - flavor=self.flavor, - wait=True) - server_deleted = self.cloud.delete_server(server_name, - wait=True) - self.assertIsNone(server_deleted) + self.assertTrue(self.cloud.delete_server(server_name, wait=True)) def test_get_image_id(self): self.assertEqual( diff --git a/shade/tests/unit/test_delete_server.py b/shade/tests/unit/test_delete_server.py index 36efa8fda..13d2d3877 100644 --- a/shade/tests/unit/test_delete_server.py +++ b/shade/tests/unit/test_delete_server.py @@ -52,7 +52,7 @@ def test_delete_server(self, nova_mock): """ server = fakes.FakeServer('1234', 'daffy', 'ACTIVE') nova_mock.servers.list.return_value = [server] - self.cloud.delete_server('daffy', wait=False) + self.assertTrue(self.cloud.delete_server('daffy', wait=False)) nova_mock.servers.delete.assert_called_with(server=server.id) @mock.patch('shade.OpenStackCloud.nova_client') @@ -61,12 +61,12 @@ def test_delete_server_already_gone(self, nova_mock): Test that we return immediately when server is already gone """ nova_mock.servers.list.return_value = [] - self.cloud.delete_server('tweety', wait=False) + self.assertFalse(self.cloud.delete_server('tweety', wait=False)) self.assertFalse(nova_mock.servers.delete.called) @mock.patch('shade.OpenStackCloud.nova_client') def test_delete_server_already_gone_wait(self, nova_mock): - self.cloud.delete_server('speedy', wait=True) + self.assertFalse(self.cloud.delete_server('speedy', wait=True)) self.assertFalse(nova_mock.servers.delete.called) @mock.patch('shade.OpenStackCloud.nova_client') @@ -89,7 +89,7 @@ def _raise_notfound(*args, **kwargs): nova_mock.servers.get.side_effect = _raise_notfound nova_mock.servers.delete.side_effect = _delete_wily - self.cloud.delete_server('wily', wait=True) + self.assertTrue(self.cloud.delete_server('wily', wait=True)) nova_mock.servers.delete.assert_called_with(server=server.id) @mock.patch('shade.OpenStackCloud.nova_client') From 87309adcfc6c950c08146fe209d5903571913120 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 28 Nov 2015 10:18:18 -0500 Subject: [PATCH 0688/3836] Fix argument sequences for boot from volume When booting from volume, image always needs to equal None. Additionally, we updated the argument structure to be block_device_mapping_v2 but were still passing as just block_device_mapping in one place. Change-Id: Ia22792920f392d1accb5a96cf2b36f22aee75785 --- shade/openstackcloud.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 6f698b9a9..280243123 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3048,7 +3048,8 @@ def _get_boot_from_volume_kwargs( 'uuid': volume['id'], 'source_type': 'volume', } - kwargs['block_device_mapping'].append(block_mapping) + kwargs['block_device_mapping_v2'].append(block_mapping) + kwargs['image'] = None elif boot_from_volume: if not hasattr(image, 'id'): @@ -3070,7 +3071,7 @@ def _get_boot_from_volume_kwargs( 'source_type': 'image', 'volume_size': volume_size, } - del kwargs['image'] + kwargs['image'] = None kwargs['block_device_mapping_v2'].append(block_mapping) for volume in volumes: volume_obj = self.get_volume(volume) @@ -3084,7 +3085,7 @@ def _get_boot_from_volume_kwargs( 'boot_index': None, 'delete_on_termination': False, 'destination_type': 'volume', - 'uuid': volume['id'], + 'uuid': volume_obj['id'], 'source_type': 'volume', } kwargs['block_device_mapping_v2'].append(block_mapping) From bd2f1cbc6ff1195e08c84e22fe3c209d349c5649 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 28 Nov 2015 12:20:09 -0500 Subject: [PATCH 0689/3836] Normalize volume objects Cinder v1 uses "display_name" and "display_description". Cinder v2 users "name" and "description". Normalize the volume objects so that both values work as both input and output. Having done this, the name_key param to _filter_list is no longer needed, since it was there to work around cinder wanting display name. In the future, if we have another such thing (like heat) we should likely just normalize the dict to include a name param rather than making _filter_list carry the weight - since humans will want to get the name of the objects they work with. Co-Authored-By: David Shrewsbury Change-Id: Ia43815fd3d25ff7308e91489645bd3c459072359 --- shade/_utils.py | 31 +++++-- shade/openstackcloud.py | 83 +++++++++++++------ shade/tests/fakes.py | 25 ++++-- shade/tests/unit/test_caching.py | 19 +++-- .../tests/unit/test_create_volume_snapshot.py | 13 ++- shade/tests/unit/test_meta.py | 2 +- 6 files changed, 123 insertions(+), 50 deletions(-) diff --git a/shade/_utils.py b/shade/_utils.py index 1f6cf2d7a..d034206fb 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -57,14 +57,13 @@ def _iterate_timeout(timeout, message, wait=2): raise exc.OpenStackCloudTimeout(message) -def _filter_list(data, name_or_id, filters, name_key='name'): +def _filter_list(data, name_or_id, filters): """Filter a list by name/ID and arbitrary meta data. :param list data: The list of dictionary data to filter. It is expected that each dictionary contains an 'id' and 'name' - key if a value for name_or_id is given. The 'name' key can be - overridden with the name_key parameter. + key if a value for name_or_id is given. :param string name_or_id: The name or ID of the entity being filtered. :param dict filters: @@ -77,15 +76,12 @@ def _filter_list(data, name_or_id, filters, name_key='name'): 'gender': 'Female' } } - :param string name_key: - The name of the name key. Cinder wants display_name. Heat wants - stack_name. Defaults to 'name' """ if name_or_id: identifier_matches = [] for e in data: e_id = str(e.get('id', None)) - e_name = e.get(name_key, None) + e_name = e.get('name', None) if str(name_or_id) in (e_id, e_name): identifier_matches.append(e) data = identifier_matches @@ -314,6 +310,27 @@ def normalize_users(users): return meta.obj_list_to_dict(ret) +def normalize_volumes(volumes): + ret = [] + for vol in volumes: + new_vol = vol.copy() + name = vol.get('name', vol.get('display_name')) + description = vol.get('description', vol.get('display_description')) + new_vol['name'] = name + new_vol['display_name'] = name + new_vol['description'] = description + new_vol['display_description'] = description + # For some reason, cinder uses strings for bools for these fields + for field in ('bootable', 'multiattach'): + if field in new_vol and new_vol[field] is not None: + if new_vol[field].lower() == 'true': + new_vol[field] = True + elif new_vol[field].lower() == 'false': + new_vol[field] = False + ret.append(new_vol) + return meta.obj_list_to_dict(ret) + + def normalize_domains(domains): ret = [ dict( diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 280243123..b4b7ae6ce 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -899,12 +899,12 @@ def search_ports(self, name_or_id=None, filters=None): def search_volumes(self, name_or_id=None, filters=None): volumes = self.list_volumes() return _utils._filter_list( - volumes, name_or_id, filters, name_key='display_name') + volumes, name_or_id, filters) def search_volume_snapshots(self, name_or_id=None, filters=None): volumesnapshots = self.list_volume_snapshots() return _utils._filter_list( - volumesnapshots, name_or_id, filters, name_key='display_name') + volumesnapshots, name_or_id, filters) def search_flavors(self, name_or_id=None, filters=None): flavors = self.list_flavors() @@ -1027,7 +1027,8 @@ def list_volumes(self, cache=True): warnings.warn('cache argument to list_volumes is deprecated. Use ' 'invalidate instead.') with _utils.shade_exceptions("Error fetching volume list"): - return self.manager.submitTask(_tasks.VolumeList()) + return _utils.normalize_volumes( + self.manager.submitTask(_tasks.VolumeList())) @_utils.cache_on_arguments() def list_flavors(self): @@ -2095,12 +2096,19 @@ def _update_image_properties_v1(self, image, properties): self.list_images.invalidate(self) return True - def create_volume(self, wait=True, timeout=None, **kwargs): + def create_volume( + self, size, + wait=True, timeout=None, image=None, **kwargs): """Create a volume. + :param size: Size, in GB of the volume to create. + :param name: (optional) Name for the volume. + :param description: (optional) Name for the volume. :param wait: If true, waits for volume to be created. :param timeout: Seconds to wait for volume creation. None is forever. - :param volkwargs: Keyword arguments as expected for cinder client. + :param image: (optional) Image name, id or object from which to create + the volume + :param kwargs: Keyword arguments as expected for cinder client. :returns: The created volume object. @@ -2108,8 +2116,18 @@ def create_volume(self, wait=True, timeout=None, **kwargs): :raises: OpenStackCloudException on operation error. """ + if image: + image_obj = self.get_image(image) + if not image_obj: + raise OpenStackCloudException( + "Image {image} was requested as the basis for a new" + " volume, but was not found on the cloud".format( + image=image)) + kwargs['imageRef'] = image_obj['id'] + kwargs = self._get_volume_kwargs(kwargs) with _utils.shade_exceptions("Error in creating volume"): - volume = self.manager.submitTask(_tasks.VolumeCreate(**kwargs)) + volume = self.manager.submitTask(_tasks.VolumeCreate( + size=size, **kwargs)) self.list_volumes.invalidate(self) if volume['status'] == 'error': @@ -2126,13 +2144,13 @@ def create_volume(self, wait=True, timeout=None, **kwargs): continue if volume['status'] == 'available': - break + return volume if volume['status'] == 'error': raise OpenStackCloudException( "Error in creating volume, please check logs") - return volume + return _utils.normalize_volumes([volume])[0] def delete_volume(self, name_or_id=None, wait=True, timeout=None): """Delete a volume. @@ -2316,18 +2334,35 @@ def attach_volume(self, server, volume, device=None, ) return vol + def _get_volume_kwargs(self, kwargs): + name = kwargs.pop('name', kwargs.pop('display_name', None)) + description = kwargs.pop('description', + kwargs.pop('display_description', None)) + if name: + if self.cloud_config.get_api_version('volume').startswith('2'): + kwargs['name'] = name + else: + kwargs['display_name'] = name + if description: + if self.cloud_config.get_api_version('volume').startswith('2'): + kwargs['description'] = description + else: + kwargs['display_description'] = description + return kwargs + + @_utils.valid_kwargs('name', 'display_name', + 'description', 'display_description') def create_volume_snapshot(self, volume_id, force=False, - display_name=None, display_description=None, - wait=True, timeout=None): + wait=True, timeout=None, **kwargs): """Create a volume. :param volume_id: the id of the volume to snapshot. :param force: If set to True the snapshot will be created even if the volume is attached to an instance, if False it will not - :param display_name: name of the snapshot, one will be generated if - one is not provided - :param display_description: description of the snapshot, one will be - one is not provided + :param name: name of the snapshot, one will be generated if one is + not provided + :param description: description of the snapshot, one will be generated + if one is not provided :param wait: If true, waits for volume snapshot to be created. :param timeout: Seconds to wait for volume snapshot creation. None is forever. @@ -2337,15 +2372,15 @@ def create_volume_snapshot(self, volume_id, force=False, :raises: OpenStackCloudTimeout if wait time exceeded. :raises: OpenStackCloudException on operation error. """ + + kwargs = self._get_volume_kwargs(kwargs) with _utils.shade_exceptions( "Error creating snapshot of volume {volume_id}".format( volume_id=volume_id)): snapshot = self.manager.submitTask( _tasks.VolumeSnapshotCreate( volume_id=volume_id, force=force, - display_name=display_name, - display_description=display_description) - ) + **kwargs)) if wait: snapshot_id = snapshot['id'] @@ -2360,9 +2395,9 @@ def create_volume_snapshot(self, volume_id, force=False, if snapshot['status'] == 'error': raise OpenStackCloudException( - "Error in creating volume, please check logs") + "Error in creating volume snapshot, please check logs") - return snapshot + return _utils.normalize_volumes([snapshot])[0] def get_volume_snapshot_by_id(self, snapshot_id): """Takes a snapshot_id and gets a dict of the snapshot @@ -2382,7 +2417,7 @@ def get_volume_snapshot_by_id(self, snapshot_id): ) ) - return snapshot + return _utils.normalize_volumes([snapshot])[0] def get_volume_snapshot(self, name_or_id, filters=None): """Get a volume by name or ID. @@ -2413,10 +2448,10 @@ def list_volume_snapshots(self, detailed=True, search_opts=None): """ with _utils.shade_exceptions("Error getting a list of snapshots"): - return self.manager.submitTask( - _tasks.VolumeSnapshotList(detailed=detailed, - search_opts=search_opts) - ) + return _utils.normalize_volumes( + self.manager.submitTask( + _tasks.VolumeSnapshotList( + detailed=detailed, search_opts=search_opts))) def delete_volume_snapshot(self, name_or_id=None, wait=False, timeout=None): diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 7931e7a73..fa6a75b9c 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -95,19 +95,34 @@ def __init__(self, id, email, name): class FakeVolume(object): - def __init__(self, id, status, display_name, attachments=[]): + def __init__( + self, id, status, name, attachments=[], + size=75): self.id = id self.status = status - self.display_name = display_name + self.name = name self.attachments = attachments + self.size = size + self.snapshot_id = 'id:snapshot' + self.description = 'description' + self.volume_type = 'type:volume' + self.availability_zone = 'az1' + self.created_at = '1900-01-01 12:34:56' + self.source_volid = '12345' + self.metadata = {} class FakeVolumeSnapshot(object): - def __init__(self, id, status, display_name, display_description): + def __init__( + self, id, status, name, description, size=75): self.id = id self.status = status - self.display_name = display_name - self.display_description = display_description + self.name = name + self.description = description + self.size = size + self.created_at = '1900-01-01 12:34:56' + self.volume_id = '12345' + self.metadata = {} class FakeMachine(object): diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 82f95a699..2a080ad71 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -18,8 +18,8 @@ import testtools import yaml -import shade import shade.openstackcloud +from shade import _utils from shade import exc from shade import meta from shade.tests import fakes @@ -108,12 +108,14 @@ def test_list_projects_v2(self, keystone_mock): def test_list_volumes(self, cinder_mock): fake_volume = fakes.FakeVolume('volume1', 'available', 'Volume 1 Display Name') - fake_volume_dict = meta.obj_to_dict(fake_volume) + fake_volume_dict = _utils.normalize_volumes( + [meta.obj_to_dict(fake_volume)])[0] cinder_mock.volumes.list.return_value = [fake_volume] self.assertEqual([fake_volume_dict], self.cloud.list_volumes()) fake_volume2 = fakes.FakeVolume('volume2', 'available', 'Volume 2 Display Name') - fake_volume2_dict = meta.obj_to_dict(fake_volume2) + fake_volume2_dict = _utils.normalize_volumes( + [meta.obj_to_dict(fake_volume2)])[0] cinder_mock.volumes.list.return_value = [fake_volume, fake_volume2] self.assertEqual([fake_volume_dict], self.cloud.list_volumes()) self.cloud.list_volumes.invalidate(self.cloud) @@ -124,12 +126,14 @@ def test_list_volumes(self, cinder_mock): def test_list_volumes_creating_invalidates(self, cinder_mock): fake_volume = fakes.FakeVolume('volume1', 'creating', 'Volume 1 Display Name') - fake_volume_dict = meta.obj_to_dict(fake_volume) + fake_volume_dict = _utils.normalize_volumes( + [meta.obj_to_dict(fake_volume)])[0] cinder_mock.volumes.list.return_value = [fake_volume] self.assertEqual([fake_volume_dict], self.cloud.list_volumes()) fake_volume2 = fakes.FakeVolume('volume2', 'available', 'Volume 2 Display Name') - fake_volume2_dict = meta.obj_to_dict(fake_volume2) + fake_volume2_dict = _utils.normalize_volumes( + [meta.obj_to_dict(fake_volume2)])[0] cinder_mock.volumes.list.return_value = [fake_volume, fake_volume2] self.assertEqual([fake_volume_dict, fake_volume2_dict], self.cloud.list_volumes()) @@ -138,7 +142,8 @@ def test_list_volumes_creating_invalidates(self, cinder_mock): def test_create_volume_invalidates(self, cinder_mock): fake_volb4 = fakes.FakeVolume('volume1', 'available', 'Volume 1 Display Name') - fake_volb4_dict = meta.obj_to_dict(fake_volb4) + fake_volb4_dict = _utils.normalize_volumes( + [meta.obj_to_dict(fake_volb4)])[0] cinder_mock.volumes.list.return_value = [fake_volb4] self.assertEqual([fake_volb4_dict], self.cloud.list_volumes()) volume = dict(display_name='junk_vol', @@ -146,6 +151,8 @@ def test_create_volume_invalidates(self, cinder_mock): display_description='test junk volume') fake_vol = fakes.FakeVolume('12345', 'creating', '') fake_vol_dict = meta.obj_to_dict(fake_vol) + fake_vol_dict = _utils.normalize_volumes( + [meta.obj_to_dict(fake_vol)])[0] cinder_mock.volumes.create.return_value = fake_vol cinder_mock.volumes.list.return_value = [fake_volb4, fake_vol] diff --git a/shade/tests/unit/test_create_volume_snapshot.py b/shade/tests/unit/test_create_volume_snapshot.py index 8791f7533..bf6f6c458 100644 --- a/shade/tests/unit/test_create_volume_snapshot.py +++ b/shade/tests/unit/test_create_volume_snapshot.py @@ -21,6 +21,7 @@ from mock import patch import os_client_config +from shade import _utils from shade import meta from shade import OpenStackCloud from shade.tests import base, fakes @@ -52,14 +53,14 @@ def test_create_volume_snapshot_wait(self, mock_cinder): build_snapshot, fake_snapshot] self.assertEqual( - meta.obj_to_dict(fake_snapshot), + _utils.normalize_volumes( + [meta.obj_to_dict(fake_snapshot)])[0], self.client.create_volume_snapshot(volume_id='1234', wait=True) ) mock_cinder.volume_snapshots.create.assert_called_with( - display_description=None, display_name=None, force=False, - volume_id='1234' + force=False, volume_id='1234' ) mock_cinder.volume_snapshots.get.assert_called_with( snapshot_id=meta.obj_to_dict(build_snapshot)['id'] @@ -84,8 +85,7 @@ def test_create_volume_snapshot_with_timeout(self, mock_cinder): wait=True, timeout=1) mock_cinder.volume_snapshots.create.assert_called_with( - display_description=None, display_name=None, force=False, - volume_id='1234' + force=False, volume_id='1234' ) mock_cinder.volume_snapshots.get.assert_called_with( snapshot_id=meta.obj_to_dict(build_snapshot)['id'] @@ -112,8 +112,7 @@ def test_create_volume_snapshot_with_error(self, mock_cinder): wait=True, timeout=5) mock_cinder.volume_snapshots.create.assert_called_with( - display_description=None, display_name=None, force=False, - volume_id='1234' + force=False, volume_id='1234' ) mock_cinder.volume_snapshots.get.assert_called_with( snapshot_id=meta.obj_to_dict(build_snapshot)['id'] diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index fc9d255dc..dece29784 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -427,7 +427,7 @@ def test_has_volume(self): fake_volume = fakes.FakeVolume( id='volume1', status='available', - display_name='Volume 1 Display Name', + name='Volume 1 Display Name', attachments=[{'device': '/dev/sda0'}]) fake_volume_dict = meta.obj_to_dict(fake_volume) mock_cloud.get_volumes.return_value = [fake_volume_dict] From 0d5e7b561ea4e0404157cabbcd1e6b56d633c079 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 28 Nov 2015 10:55:01 -0500 Subject: [PATCH 0690/3836] Accept objects in name_or_id parameter Let's not tell anybody about it, but if someone passes us an object in the name_or_id slot, let's roll with it. Change-Id: I70ea91c948c0f82185861baa22830a4954359b6f --- shade/_utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shade/_utils.py b/shade/_utils.py index d034206fb..ecab3b34f 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -125,6 +125,9 @@ def _get_entity(func, name_or_id, filters): :param dict filters: A dictionary of meta data to use for further filtering. """ + # We've been passed a dict/object already, return it + if hasattr(name_or_id, 'id'): + return name_or_id entities = func(name_or_id, filters) if not entities: return None From 2c753699ac9c3c4209d5f070855f4315ea468efa Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 28 Nov 2015 11:18:29 -0500 Subject: [PATCH 0691/3836] Don't double-print exception subjects We're creating an error message string that contains the shade message and the underlying message AND we're passing the underlying message in so that we append it as part of the Inner Exception string. So we wind up with something like: Error in creating volume: Invalid input received: 'size' parameter must be between 75 and 1024 (HTTP 400) (Request-ID: req-ae3f3489-997b-4038-8c9f-efbe7dd73a70) (Inner Exception: Invalid input received: 'size' parameter must be between 75 and 1024) Which is a bit insane. With this change, the above becomes: Error in creating volume (Inner Exception: Invalid input received: 'size' parameter must be between 75 and 1024 (HTTP 400) (Request-ID: req-d4da66b2-41eb-44c8-85f1-064454af5a1c)) And if the shade_exceptions context manager is invoked with no message, the output will be: Invalid input received: 'size' parameter must be between 75 and 1024 (HTTP 400) (Request-ID: req-d4da66b2-41eb-44c8-85f1-064454af5a1c)) Change-Id: I6bd70b70585722d2266ef08496b6860aeeab1824 --- shade/_utils.py | 8 +++----- shade/exc.py | 5 +++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/shade/_utils.py b/shade/_utils.py index ecab3b34f..3a720efd4 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -444,8 +444,6 @@ def shade_exceptions(error_message=None): except exc.OpenStackCloudException: raise except Exception as e: - if error_message is not None: - message = "{msg}: {exc}".format(msg=error_message, exc=str(e)) - else: - message = str(e) - raise exc.OpenStackCloudException(message) + if error_message is None: + error_message = str(e) + raise exc.OpenStackCloudException(error_message) diff --git a/shade/exc.py b/shade/exc.py index efdccab8c..ed102dd5d 100644 --- a/shade/exc.py +++ b/shade/exc.py @@ -39,10 +39,11 @@ def __str__(self): if self.extra_data is not None: message = "%s (Extra: %s)" % (message, self.extra_data) if (self.inner_exception and self.inner_exception[1] - and hasattr(self.inner_exception[1], 'message')): + and not self.orig_message.endswith( + str(self.inner_exception[1]))): message = "%s (Inner Exception: %s)" % ( message, - self.inner_exception[1].message) + str(self.inner_exception[1])) return message From 372edf44efd7e028890e4623a950052a606bb123 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 29 Nov 2015 10:03:11 -0500 Subject: [PATCH 0692/3836] Enable running tests against RAX and IBM Rackspace requires performance flavors be used for boot from volume. IBM does not have Ubuntu or Cirros images in the cloud. Change-Id: I95c15d92072311eb4aa0a4b7f551a95c4dc6e082 --- shade/tests/functional/util.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/shade/tests/functional/util.py b/shade/tests/functional/util.py index 960bec613..07a9a5031 100644 --- a/shade/tests/functional/util.py +++ b/shade/tests/functional/util.py @@ -23,6 +23,13 @@ def pick_flavor(flavors): """Given a flavor list pick the smallest one.""" + # Enable running functional tests against rax - which requires + # performance flavors be used for boot from volume + for flavor in sorted( + flavors, + key=operator.attrgetter('ram')): + if 'performance' in flavor.name: + return flavor for flavor in sorted( flavors, key=operator.attrgetter('ram')): @@ -36,3 +43,6 @@ def pick_image(images): for image in images: if image.name.lower().startswith('ubuntu'): return image + for image in images: + if image.name.lower().startswith('centos'): + return image From 026a17c9eb9d8ebad8c56f8d1b7946bd4694519e Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Wed, 2 Dec 2015 11:05:29 +0800 Subject: [PATCH 0693/3836] Remove optional keystoneauth1 imports keystoneauth1 is now a hard dependency of os-client-config so there is no way that this should not be importable. Change-Id: I20901623e8b29f50d7ab1ed956472a4b1eda51bf --- os_client_config/config.py | 37 +++++++++++++++---------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 5f7c402ae..19cfa3598 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -18,10 +18,7 @@ import warnings import appdirs -try: - from keystoneauth1 import loading -except ImportError: - loading = None +from keystoneauth1 import loading import yaml from os_client_config import cloud_config @@ -657,27 +654,23 @@ def get_one_cloud(self, cloud=None, validate=True, # compatible behaviour config = self.auth_config_hook(config) - if loading: - if validate: - try: - loader = self._get_auth_loader(config) - config = self._validate_auth(config, loader) - auth_plugin = loader.load_from_options(**config['auth']) - except Exception as e: - # We WANT the ksa exception normally - # but OSC can't handle it right now, so we try deferring - # to ksc. If that ALSO fails, it means there is likely - # a deeper issue, so we assume the ksa error was correct - auth_plugin = None - try: - config = self._validate_auth_ksc(config) - except Exception: - raise e - else: + if validate: + try: + loader = self._get_auth_loader(config) + config = self._validate_auth(config, loader) + auth_plugin = loader.load_from_options(**config['auth']) + except Exception as e: + # We WANT the ksa exception normally + # but OSC can't handle it right now, so we try deferring + # to ksc. If that ALSO fails, it means there is likely + # a deeper issue, so we assume the ksa error was correct auth_plugin = None + try: + config = self._validate_auth_ksc(config) + except Exception: + raise e else: auth_plugin = None - config = self._validate_auth_ksc(config) # If any of the defaults reference other values, we need to expand for (key, value) in config.items(): From 07ac77ca765df2f9cf46b0210d40ef2f96fe5586 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 29 Nov 2015 10:41:30 -0500 Subject: [PATCH 0694/3836] Add functional tests for boot from volume Test both that we can boot from volume from a pre-existing image with and without auto-termination. Also that manually making the volume from the image and then booting from it also works. Also, add a cache invalidation that testing showed to be an issue. Co-Authored-By: David Shrewsbury Change-Id: Ia0f5801c17edacf8cce67c7594657e7ae41e0f18 --- shade/openstackcloud.py | 17 ++- shade/tests/functional/test_compute.py | 138 +++++++++++++++++++++++-- shade/tests/functional/test_volume.py | 6 +- 3 files changed, 147 insertions(+), 14 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index b4b7ae6ce..08a269986 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3087,7 +3087,9 @@ def _get_boot_from_volume_kwargs( kwargs['image'] = None elif boot_from_volume: - if not hasattr(image, 'id'): + if hasattr(image, 'id'): + image_obj = image + else: image_obj = self.get_image(image) if not image_obj: raise OpenStackCloudException( @@ -3095,14 +3097,12 @@ def _get_boot_from_volume_kwargs( ' {cloud}:{region}'.format( image=image, cloud=self.name, region=self.region_name)) - else: - image = image_obj block_mapping = { 'boot_index': '0', 'delete_on_termination': terminate_volume, 'destination_type': 'volume', - 'uuid': image['id'], + 'uuid': image_obj['id'], 'source_type': 'image', 'volume_size': volume_size, } @@ -3124,6 +3124,8 @@ def _get_boot_from_volume_kwargs( 'source_type': 'volume', } kwargs['block_device_mapping_v2'].append(block_mapping) + if boot_volume or boot_from_volume or volumes: + self.list_volumes.invalidate(self) return kwargs @_utils.valid_kwargs( @@ -3419,6 +3421,13 @@ def _delete_server( except Exception as e: raise OpenStackCloudException( "Error in deleting server: {0}".format(e)) + + # If the server has volume attachments, or if it has booted + # from volume, deleting it will change volume state + if (not server['image'] or not server['image']['id'] + or self.get_volumes(server)): + self.list_volumes.invalidate(self) + return True def list_containers(self, full_listing=True): diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index 637261f9e..1c62dcc59 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -34,18 +34,36 @@ def setUp(self): self.image = pick_image(self.cloud.list_images()) if self.image is None: self.assertFalse('no sensible image available') - self.server_prefix = self.getUniqueString('server') + self.server_name = self.getUniqueString() + + def _cleanup_servers_and_volumes(self, server_name): + """Delete the named server and any attached volumes. + + Adding separate cleanup calls for servers and volumes can be tricky + since they need to be done in the proper order. And sometimes deleting + a server can start the process of deleting a volume if it is booted + from that volume. This encapsulates that logic. + """ + server = self.cloud.get_server(server_name) + if not server: + return + volumes = self.cloud.get_volumes(server) + self.cloud.delete_server(server.name, wait=True) + for volume in volumes: + if volume.status != 'deleting': + self.cloud.delete_volume(volume.id, wait=True) def test_create_and_delete_server(self): - server_name = self.server_prefix + '_create_and_delete' - server = self.cloud.create_server(name=server_name, + self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) + server = self.cloud.create_server(name=self.server_name, image=self.image, flavor=self.flavor, wait=True) - self.assertEquals(server['name'], server_name) - self.assertEquals(server['image']['id'], self.image.id) - self.assertEquals(server['flavor']['id'], self.flavor.id) - self.assertTrue(self.cloud.delete_server(server_name, wait=True)) + self.assertEqual(self.server_name, server['name']) + self.assertEqual(self.image.id, server['image']['id']) + self.assertEqual(self.flavor.id, server['flavor']['id']) + self.assertTrue(self.cloud.delete_server(self.server_name, wait=True)) + self.assertIsNone(self.cloud.get_server(self.server_name)) def test_get_image_id(self): self.assertEqual( @@ -58,3 +76,109 @@ def test_get_image_name(self): self.image.name, self.cloud.get_image_name(self.image.id)) self.assertEqual( self.image.name, self.cloud.get_image_name(self.image.name)) + + def _assert_volume_attach(self, server, volume_id=None): + self.assertEqual(self.server_name, server['name']) + self.assertEqual('', server['image']) + self.assertEqual(self.flavor.id, server['flavor']['id']) + volumes = self.cloud.get_volumes(server) + self.assertEqual(1, len(volumes)) + volume = volumes[0] + if volume_id: + self.assertEqual(volume_id, volume['id']) + else: + volume_id = volume['id'] + self.assertEqual(1, len(volume['attachments']), 1) + self.assertEqual(server['id'], volume['attachments'][0]['server_id']) + return volume_id + + def test_create_boot_from_volume_image(self): + if not self.cloud.has_service('volume'): + self.skipTest('volume service not supported by cloud') + self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) + server = self.cloud.create_server( + name=self.server_name, + image=self.image, + flavor=self.flavor, + boot_from_volume=True, + volume_size=1, + wait=True) + volume_id = self._assert_volume_attach(server) + volume = self.cloud.get_volume(volume_id) + self.assertIsNotNone(volume) + self.assertEqual(volume['name'], volume['display_name']) + self.assertEqual(True, volume['bootable']) + self.assertEqual(server['id'], volume['attachments'][0]['server_id']) + self.assertTrue(self.cloud.delete_server(server.id, wait=True)) + self.assertTrue(self.cloud.delete_volume(volume.id, wait=True)) + self.assertIsNone(self.cloud.get_server(server.id)) + self.assertIsNone(self.cloud.get_volume(volume.id)) + + def test_create_terminate_volume_image(self): + if not self.cloud.has_service('volume'): + self.skipTest('volume service not supported by cloud') + self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) + server = self.cloud.create_server( + name=self.server_name, + image=self.image, + flavor=self.flavor, + boot_from_volume=True, + terminate_volume=True, + volume_size=1, + wait=True) + volume_id = self._assert_volume_attach(server) + self.assertTrue(self.cloud.delete_server(self.server_name, wait=True)) + volume = self.cloud.get_volume(volume_id) + # We can either get None (if the volume delete was quick), or a volume + # that is in the process of being deleted. + if volume: + self.assertEquals('deleting', volume.status) + self.assertIsNone(self.cloud.get_server(self.server_name)) + + def test_create_boot_from_volume_preexisting(self): + if not self.cloud.has_service('volume'): + self.skipTest('volume service not supported by cloud') + self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) + volume = self.cloud.create_volume( + size=1, name=self.server_name, image=self.image, wait=True) + server = self.cloud.create_server( + name=self.server_name, + image=None, + flavor=self.flavor, + boot_volume=volume, + volume_size=1, + wait=True) + volume_id = self._assert_volume_attach(server, volume_id=volume['id']) + self.assertTrue(self.cloud.delete_server(self.server_name, wait=True)) + self.addCleanup(self.cloud.delete_volume, volume_id) + volume = self.cloud.get_volume(volume_id) + self.assertIsNotNone(volume) + self.assertEqual(volume['name'], volume['display_name']) + self.assertEqual(True, volume['bootable']) + self.assertEqual([], volume['attachments']) + self.assertTrue(self.cloud.delete_volume(volume_id)) + self.assertIsNone(self.cloud.get_server(self.server_name)) + self.assertIsNone(self.cloud.get_volume(volume_id)) + + def test_create_boot_from_volume_preexisting_terminate(self): + if not self.cloud.has_service('volume'): + self.skipTest('volume service not supported by cloud') + self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) + volume = self.cloud.create_volume( + size=1, name=self.server_name, image=self.image, wait=True) + server = self.cloud.create_server( + name=self.server_name, + image=None, + flavor=self.flavor, + boot_volume=volume, + terminate_volume=True, + volume_size=1, + wait=True) + volume_id = self._assert_volume_attach(server, volume_id=volume['id']) + self.assertTrue(self.cloud.delete_server(self.server_name, wait=True)) + volume = self.cloud.get_volume(volume_id) + # We can either get None (if the volume delete was quick), or a volume + # that is in the process of being deleted. + if volume: + self.assertEquals('deleting', volume.status) + self.assertIsNone(self.cloud.get_server(self.server_name)) diff --git a/shade/tests/functional/test_volume.py b/shade/tests/functional/test_volume.py index eff06041f..11b4f4ece 100644 --- a/shade/tests/functional/test_volume.py +++ b/shade/tests/functional/test_volume.py @@ -45,8 +45,8 @@ def test_volumes(self): display_name=snapshot_name ) - self.assertEqual([volume], self.cloud.list_volumes()) - self.assertEqual([snapshot], self.cloud.list_volume_snapshots()) + self.assertIn(volume, self.cloud.list_volumes()) + self.assertIn(snapshot, self.cloud.list_volume_snapshots()) self.assertEqual(snapshot, self.cloud.get_volume_snapshot( snapshot['display_name'])) @@ -54,7 +54,7 @@ def test_volumes(self): self.cloud.get_volume_snapshot_by_id(snapshot['id'])) self.cloud.delete_volume_snapshot(snapshot_name, wait=True) - self.cloud.delete_volume(volume_name) + self.cloud.delete_volume(volume_name, wait=True) def cleanup(self, volume_name, snapshot_name): volume = self.cloud.get_volume(volume_name) From 1700e197c1ee0a16a1b78365e621b71a101d17d8 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 1 Dec 2015 17:53:44 -0600 Subject: [PATCH 0695/3836] Remove cinderclient version pin We had put this in because of the v2 version negotiation breaking public clouds. That has since been fixed, and the pin here causes conflicts with python-openstackclient. Change-Id: I9ee2d9bfad36160ba41f65dcd657cbbca9f26050 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c41f168f5..8d6eaf752 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ netifaces>=0.10.4 python-novaclient>=2.21.0,!=2.27.0,!=2.32.0 python-keystoneclient>=0.11.0 python-glanceclient>=1.0.0 -python-cinderclient<1.2 +python-cinderclient>=1.3.1 python-neutronclient>=2.3.10 python-troveclient>=1.2.0 python-ironicclient>=0.10.0 From dcdb20d0f5ec0e9a3409c0d6f14d138f042129c9 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 2 Dec 2015 06:07:38 -0800 Subject: [PATCH 0696/3836] Provide a better comment for the object short-circuit A previous commit was landed over a -1 from a core reviewer that was concerned about the terse and flippant commit message that accompanied the commit. The substance of that objection was that there was no good explanation for why the change was made. Indeed, that why is likely useful for people looking at the code too, so add a more complete comment on the code in question. Change-Id: I1ff777393502edf4ef3ed595e47bfcf73be0e5e8 --- shade/_utils.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/shade/_utils.py b/shade/_utils.py index 3a720efd4..ef436630b 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -121,11 +121,15 @@ def _get_entity(func, name_or_id, filters): A function that takes `name_or_id` and `filters` as parameters and returns a list of entities to filter. :param string name_or_id: - The name or ID of the entity being filtered. + The name or ID of the entity being filtered or a dict :param dict filters: A dictionary of meta data to use for further filtering. """ - # We've been passed a dict/object already, return it + # Sometimes in the control flow of shade, we already have an object + # fetched. Rather than then needing to pull the name or id out of that + # object, pass it in here and rely on caching to prevent us from making + # an additional call, it's simple enough to test to see if we got an + # object and just short-circuit return it. if hasattr(name_or_id, 'id'): return name_or_id entities = func(name_or_id, filters) From 0e6085fe66a5e529d64f670ec98b024cb649c5f5 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 2 Dec 2015 12:55:40 -0500 Subject: [PATCH 0697/3836] Improve test coverage: user API Add tests for creating users (keystone v2 and v3) and deleting users. Change-Id: I20dc1ab329797c49a8144ee0b8130c03fe749d29 --- shade/tests/unit/test_users.py | 72 ++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/shade/tests/unit/test_users.py b/shade/tests/unit/test_users.py index ed1ee1428..65d3378d6 100644 --- a/shade/tests/unit/test_users.py +++ b/shade/tests/unit/test_users.py @@ -14,9 +14,13 @@ import mock + import munch +import os_client_config as occ +import testtools import shade +from shade.tests import fakes from shade.tests.unit import base @@ -26,6 +30,74 @@ def setUp(self): super(TestUsers, self).setUp() self.cloud = shade.operator_cloud(validate=False) + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_create_user_v2(self, mock_keystone, mock_api_version): + mock_api_version.return_value = '2' + name = 'Mickey Mouse' + email = 'mickey@disney.com' + password = 'mice-rule' + fake_user = fakes.FakeUser('1', email, name) + mock_keystone.users.create.return_value = fake_user + user = self.cloud.create_user(name=name, email=email, + password=password) + mock_keystone.users.create.assert_called_once_with( + name=name, password=password, email=email, enabled=True, + ) + self.assertEqual(name, user.name) + self.assertEqual(email, user.email) + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_create_user_v3(self, mock_keystone, mock_api_version): + mock_api_version.return_value = '3' + name = 'Mickey Mouse' + email = 'mickey@disney.com' + password = 'mice-rule' + domain_id = '456' + fake_user = fakes.FakeUser('1', email, name) + mock_keystone.users.create.return_value = fake_user + user = self.cloud.create_user(name=name, email=email, + password=password, + domain_id=domain_id) + mock_keystone.users.create.assert_called_once_with( + name=name, password=password, email=email, enabled=True, + domain=domain_id + ) + self.assertEqual(name, user.name) + self.assertEqual(email, user.email) + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_create_user_v3_no_domain(self, mock_keystone, mock_api_version): + mock_api_version.return_value = '3' + name = 'Mickey Mouse' + email = 'mickey@disney.com' + password = 'mice-rule' + with testtools.ExpectedException( + shade.OpenStackCloudException, + "User creation requires an explicit domain_id argument." + ): + self.cloud.create_user(name=name, email=email, password=password) + + @mock.patch.object(shade.OpenStackCloud, 'get_user_by_id') + @mock.patch.object(shade.OpenStackCloud, 'get_user') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_delete_user(self, mock_keystone, mock_get_user, mock_get_by_id): + mock_get_user.return_value = dict(id='123') + fake_user = fakes.FakeUser('123', 'email', 'name') + mock_get_by_id.return_value = fake_user + self.assertTrue(self.cloud.delete_user('name')) + mock_get_by_id.assert_called_once_with('123', normalize=False) + mock_keystone.users.delete.assert_called_once_with(user=fake_user) + + @mock.patch.object(shade.OpenStackCloud, 'get_user') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_delete_user_not_found(self, mock_keystone, mock_get_user): + mock_get_user.return_value = None + self.assertFalse(self.cloud.delete_user('name')) + self.assertFalse(mock_keystone.users.delete.called) + @mock.patch.object(shade.OpenStackCloud, 'get_user') @mock.patch.object(shade.OperatorCloud, 'get_group') @mock.patch.object(shade.OpenStackCloud, 'keystone_client') From f16ec0527ac1ebc8fc96486029fee7eaaf45db46 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 2 Dec 2015 14:08:41 -0500 Subject: [PATCH 0698/3836] Improve test coverage: project API Add tests for creating/deleting/updating projects in keystone version 2 and 3. Change-Id: I0ce7e122f40dfdcdd4b885e3c1be02ad9306e8b1 --- shade/tests/unit/test_project.py | 120 +++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 shade/tests/unit/test_project.py diff --git a/shade/tests/unit/test_project.py b/shade/tests/unit/test_project.py new file mode 100644 index 000000000..55533607c --- /dev/null +++ b/shade/tests/unit/test_project.py @@ -0,0 +1,120 @@ +# -*- 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. + + +import mock + +import munch +import os_client_config as occ +import testtools + +import shade +from shade.tests.unit import base + + +class TestProject(base.TestCase): + + def setUp(self): + super(TestProject, self).setUp() + self.cloud = shade.operator_cloud(validate=False) + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_create_project_v2(self, mock_keystone, mock_api_version): + mock_api_version.return_value = '2' + name = 'project_name' + description = 'Project description' + self.cloud.create_project(name=name, description=description) + mock_keystone.tenants.create.assert_called_once_with( + project_name=name, description=description, enabled=True, + tenant_name=name + ) + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_create_project_v3(self, mock_keystone, mock_api_version): + mock_api_version.return_value = '3' + name = 'project_name' + description = 'Project description' + domain_id = '123' + self.cloud.create_project(name=name, description=description, + domain_id=domain_id) + mock_keystone.projects.create.assert_called_once_with( + project_name=name, description=description, enabled=True, + name=name, domain=domain_id + ) + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_create_project_v3_no_domain(self, mock_keystone, + mock_api_version): + mock_api_version.return_value = '3' + with testtools.ExpectedException( + shade.OpenStackCloudException, + "User creation requires an explicit domain_id argument." + ): + self.cloud.create_project(name='foo', description='bar') + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, 'update_project') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_delete_project_v2(self, mock_keystone, mock_update, + mock_api_version): + mock_api_version.return_value = '2' + mock_update.return_value = dict(id='123') + self.cloud.delete_project('123') + mock_update.assert_called_once_with('123', enabled=False) + mock_keystone.tenants.delete.assert_called_once_with(tenant='123') + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, 'update_project') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_delete_project_v3(self, mock_keystone, mock_update, + mock_api_version): + mock_api_version.return_value = '3' + mock_update.return_value = dict(id='123') + self.cloud.delete_project('123') + mock_update.assert_called_once_with('123', enabled=False) + mock_keystone.projects.delete.assert_called_once_with(project='123') + + @mock.patch.object(shade.OpenStackCloud, 'get_project') + def test_update_project_not_found(self, mock_get_project): + mock_get_project.return_value = None + with testtools.ExpectedException( + shade.OpenStackCloudException, + "Project ABC not found." + ): + self.cloud.update_project('ABC') + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, 'get_project') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_update_project_v2(self, mock_keystone, mock_get_project, + mock_api_version): + mock_api_version.return_value = '2' + mock_get_project.return_value = munch.Munch(dict(id='123')) + self.cloud.update_project('123', description='new', enabled=False) + mock_keystone.tenants.update.assert_called_once_with( + description='new', enabled=False, tenant_id='123') + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, 'get_project') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_update_project_v3(self, mock_keystone, mock_get_project, + mock_api_version): + mock_api_version.return_value = '3' + mock_get_project.return_value = munch.Munch(dict(id='123')) + self.cloud.update_project('123', description='new', enabled=False) + mock_keystone.projects.update.assert_called_once_with( + description='new', enabled=False, project='123') From eea460d5917a1536025d66ce3e2b3f9094d46e7c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 3 Dec 2015 07:34:23 -0800 Subject: [PATCH 0699/3836] Make sure that cloud always has a name If we don't ask for a cloud, and we fall through to the envvars cloud or the defaults cloud, the cloud that is returned winds up not being named - even though we know what cloud it is. Set the name of the cloud we're working with. This is important for the next patch, where we need to peek at the config to get some default values, but in a fallthrough case we do not know which cloud to request. Change-Id: Ie56e490d4384f2d680450bc956e4b7b5b8099f0e --- os_client_config/config.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 19cfa3598..ea3f6a1c3 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -177,6 +177,8 @@ def __init__(self, config_files=None, vendor_files=None, envvars = _get_os_environ(envvar_prefix=envvar_prefix) if envvars: self.cloud_config['clouds'][self.envvar_key] = envvars + if not self.default_cloud: + self.default_cloud = self.envvar_key # Finally, fall through and make a cloud that starts with defaults # because we need somewhere to put arguments, and there are neither @@ -184,6 +186,7 @@ def __init__(self, config_files=None, vendor_files=None, if not self.cloud_config['clouds']: self.cloud_config = dict( clouds=dict(defaults=dict(self.defaults))) + self.default_cloud = 'defaults' self._cache_expiration_time = 0 self._cache_path = CACHE_PATH @@ -604,14 +607,14 @@ def get_one_cloud(self, cloud=None, validate=True, on missing required auth parameters """ - if cloud is None and self.default_cloud: - cloud = self.default_cloud - - if cloud is None and self.envvar_key in self.get_cloud_names(): - cloud = self.envvar_key - args = self._fix_args(kwargs, argparse=argparse) + if cloud is None: + if 'cloud' in args: + cloud = args['cloud'] + else: + cloud = self.default_cloud + if 'region_name' not in args or args['region_name'] is None: args['region_name'] = self._get_region(cloud) From a5f1850cbecb62d0c26940aa49c82ccd7409a087 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 3 Dec 2015 14:27:06 -0500 Subject: [PATCH 0700/3836] Improve test coverage: container API Add missing tests for creating, deleting, and updating swift containers, and getting/setting access information. Change-Id: I80fd73d7ba5171715d148ce7a1693f94c5b3b40a --- shade/tests/unit/test_object.py | 147 ++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index 3daedf91e..9c9c1cac5 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -19,6 +19,7 @@ from swiftclient import client as swift_client from swiftclient import service as swift_service from swiftclient import exceptions as swift_exc +import testtools import shade import shade.openstackcloud @@ -94,3 +95,149 @@ def test_get_object_segment_size_http_412(self, swift_mock): "Precondition failed", http_status=412) self.assertEqual(shade.openstackcloud.DEFAULT_OBJECT_SEGMENT_SIZE, self.cloud.get_object_segment_size(None)) + + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + def test_create_container(self, mock_swift): + """Test creating a (private) container""" + name = 'test_container' + mock_swift.head_container.return_value = None + + self.cloud.create_container(name) + + expected_head_container_calls = [ + # once for exist test + mock.call(container=name), + # once for the final return + mock.call(container=name, skip_cache=True) + ] + self.assertTrue(expected_head_container_calls, + mock_swift.head_container.call_args_list) + mock_swift.put_container.assert_called_once_with(container=name) + # Because the default is 'private', we shouldn't be calling update + self.assertFalse(mock_swift.post_container.called) + + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + def test_create_container_public(self, mock_swift): + """Test creating a public container""" + name = 'test_container' + mock_swift.head_container.return_value = None + + self.cloud.create_container(name, public=True) + + expected_head_container_calls = [ + # once for exist test + mock.call(container=name), + # once for the final return + mock.call(container=name, skip_cache=True) + ] + self.assertTrue(expected_head_container_calls, + mock_swift.head_container.call_args_list) + mock_swift.put_container.assert_called_once_with(container=name) + mock_swift.post_container.assert_called_once_with( + container=name, + headers={'x-container-read': + shade.openstackcloud.OBJECT_CONTAINER_ACLS['public']} + ) + + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + def test_create_container_exists(self, mock_swift): + """Test creating a container that already exists""" + name = 'test_container' + fake_container = dict(id='1', name='name') + mock_swift.head_container.return_value = fake_container + container = self.cloud.create_container(name) + mock_swift.head_container.assert_called_once_with(container=name) + self.assertEqual(fake_container, container) + + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + def test_delete_container(self, mock_swift): + name = 'test_container' + self.cloud.delete_container(name) + mock_swift.delete_container.assert_called_once_with(container=name) + + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + def test_delete_container_404(self, mock_swift): + """No exception when deleting a container that does not exist""" + name = 'test_container' + mock_swift.delete_container.side_effect = swift_exc.ClientException( + 'ERROR', http_status=404) + self.cloud.delete_container(name) + mock_swift.delete_container.assert_called_once_with(container=name) + + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + def test_delete_container_error(self, mock_swift): + """Non-404 swift error re-raised as OSCE""" + mock_swift.delete_container.side_effect = swift_exc.ClientException( + 'ERROR') + self.assertRaises(shade.OpenStackCloudException, + self.cloud.delete_container, '') + + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + def test_update_container(self, mock_swift): + name = 'test_container' + headers = {'x-container-read': + shade.openstackcloud.OBJECT_CONTAINER_ACLS['public']} + self.cloud.update_container(name, headers) + mock_swift.post_container.assert_called_once_with( + container=name, headers=headers) + + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + def test_update_container_error(self, mock_swift): + """Swift error re-raised as OSCE""" + mock_swift.post_container.side_effect = swift_exc.ClientException( + 'ERROR') + self.assertRaises(shade.OpenStackCloudException, + self.cloud.update_container, '', '') + + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + def test_set_container_access_public(self, mock_swift): + name = 'test_container' + self.cloud.set_container_access(name, 'public') + mock_swift.post_container.assert_called_once_with( + container=name, + headers={'x-container-read': + shade.openstackcloud.OBJECT_CONTAINER_ACLS['public']}) + + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + def test_set_container_access_private(self, mock_swift): + name = 'test_container' + self.cloud.set_container_access(name, 'private') + mock_swift.post_container.assert_called_once_with( + container=name, + headers={'x-container-read': + shade.openstackcloud.OBJECT_CONTAINER_ACLS['private']}) + + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + def test_set_container_access_invalid(self, mock_swift): + self.assertRaises(shade.OpenStackCloudException, + self.cloud.set_container_access, '', 'invalid') + + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + def test_get_container(self, mock_swift): + fake_container = { + 'x-container-read': + shade.openstackcloud.OBJECT_CONTAINER_ACLS['public'] + } + mock_swift.head_container.return_value = fake_container + access = self.cloud.get_container_access('foo') + self.assertEqual('public', access) + + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + def test_get_container_invalid(self, mock_swift): + fake_container = {'x-container-read': 'invalid'} + mock_swift.head_container.return_value = fake_container + with testtools.ExpectedException( + exc.OpenStackCloudException, + "Could not determine container access for ACL: invalid" + ): + self.cloud.get_container_access('foo') + + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + def test_get_container_access_not_found(self, mock_swift): + name = 'invalid_container' + mock_swift.head_container.return_value = None + with testtools.ExpectedException( + exc.OpenStackCloudException, + "Container not found: %s" % name + ): + self.cloud.get_container_access(name) From ca1409effa18bebf3a1754d3603139f4ec6ce24c Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 4 Dec 2015 09:21:25 -0500 Subject: [PATCH 0701/3836] Improve test coverage: server secgroup API Change-Id: I4556128548dec0bfed141db4a22e35e954a46f45 --- shade/tests/unit/test_security_groups.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/shade/tests/unit/test_security_groups.py b/shade/tests/unit/test_security_groups.py index d6d7539ab..1c1ab6b9d 100644 --- a/shade/tests/unit/test_security_groups.py +++ b/shade/tests/unit/test_security_groups.py @@ -333,3 +333,21 @@ def test_nova_egress_security_group_rule(self, mock_nova): self.cloud.create_security_group_rule, secgroup_name_or_id='nova-sec-group', direction='egress') + + @mock.patch.object(shade._utils, 'normalize_nova_secgroups') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_list_server_security_groups(self, mock_nova, mock_norm): + server = dict(id='server_id') + self.cloud.list_server_security_groups(server) + mock_nova.servers.list_security_group.assert_called_once_with( + server='server_id' + ) + self.assertTrue(mock_norm.called) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_list_server_security_groups_bad_source(self, mock_nova): + self.cloud.secgroup_source = 'invalid' + server = dict(id='server_id') + ret = self.cloud.list_server_security_groups(server) + self.assertEqual([], ret) + self.assertFalse(mock_nova.servers.list_security_group.called) From 833f54929b95e482bc10bf6e7f0936c8323e6c8c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 3 Dec 2015 18:00:33 -0500 Subject: [PATCH 0702/3836] Use non-versioned cinderclient constructor For some reason, we use the v1 constructor class rather than the one that passes an arg. Change-Id: I387255e09a6e5643022d7e6a9075bc24c8d61cac --- shade/openstackcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 08a269986..91d74de68 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -23,7 +23,7 @@ from dogpile import cache import requestsexceptions -from cinderclient.v1 import client as cinder_client +import cinderclient.client as cinder_client import glanceclient import glanceclient.exc from heatclient import client as heat_client From 9922cfbb3bdab398b444ae9ebbdd2892508ef4df Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 3 Dec 2015 18:12:32 -0500 Subject: [PATCH 0703/3836] Change the client imports to stop shadowing Because sometimes the easter bunny wants to kill you, we have been importing the client library contructor modules with a name that is identical to the proprety we set on the class to provide the client object. That is bad practice in any circumstance. Since we only use those things in one and only one place, don't bother coming up with a better alias for them - just import the module path itself directly. Explcit good. Change-Id: Ib0d7e616df1304277b4b9936a64793bcba8787e3 --- shade/openstackcloud.py | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 91d74de68..dc8471696 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -23,20 +23,20 @@ from dogpile import cache import requestsexceptions -import cinderclient.client as cinder_client +import cinderclient.client import glanceclient import glanceclient.exc -from heatclient import client as heat_client +import heatclient.client from heatclient.common import template_utils import keystoneauth1.exceptions -from keystoneclient import client as keystone_client -from neutronclient.neutron import client as neutron_client -from novaclient import client as nova_client -from novaclient import exceptions as nova_exceptions -import swiftclient.client as swift_client -import swiftclient.service as swift_service +import keystoneclient.client +import neutronclient.neutron.client +import novaclient.client +import novaclient.exceptions as nova_exceptions +import swiftclient.client +import swiftclient.service import swiftclient.exceptions as swift_exceptions -import troveclient.client as trove_client +import troveclient.client from shade.exc import * # noqa from shade import _log @@ -268,7 +268,7 @@ def _get_client( def nova_client(self): if self._nova_client is None: self._nova_client = self._get_client( - 'compute', nova_client.Client) + 'compute', novaclient.client.Client) return self._nova_client @property @@ -285,7 +285,7 @@ def keystone_session(self): def keystone_client(self): if self._keystone_client is None: self._keystone_client = self._get_client( - 'identity', keystone_client.Client) + 'identity', keystoneclient.client.Client) return self._keystone_client @property @@ -645,7 +645,7 @@ def glance_client(self): def heat_client(self): if self._heat_client is None: self._heat_client = self._get_client( - 'orchestration', heat_client.Client) + 'orchestration', heatclient.client.Client) return self._heat_client def get_template_contents( @@ -663,7 +663,7 @@ def get_template_contents( def swift_client(self): if self._swift_client is None: self._swift_client = self._get_client( - 'object-store', swift_client.Connection) + 'object-store', swiftclient.client.Connection) return self._swift_client @property @@ -675,7 +675,7 @@ def swift_service(self): options = dict(os_auth_token=self.auth_token, os_storage_url=endpoint, os_region_name=self.region_name) - self._swift_service = swift_service.SwiftService( + self._swift_service = swiftclient.service.SwiftService( options=options) return self._swift_service @@ -684,21 +684,21 @@ def cinder_client(self): if self._cinder_client is None: self._cinder_client = self._get_client( - 'volume', cinder_client.Client) + 'volume', cinderclient.client.Client) return self._cinder_client @property def trove_client(self): if self._trove_client is None: self._trove_client = self._get_client( - 'database', trove_client.Client) + 'database', troveclient.client.Client) return self._trove_client @property def neutron_client(self): if self._neutron_client is None: self._neutron_client = self._get_client( - 'network', neutron_client.Client) + 'network', neutronclient.neutron.client.Client) return self._neutron_client def create_stack( @@ -3619,8 +3619,8 @@ def create_object( self.log.debug( "swift uploading {filename} to {container}/{name}".format( filename=filename, container=container, name=name)) - upload = swift_service.SwiftUploadObject(source=filename, - object_name=name) + upload = swiftclient.service.SwiftUploadObject( + source=filename, object_name=name) for r in self.manager.submitTask(_tasks.ObjectCreate( container=container, objects=[upload], options=dict(header=header_list, From 99602b51835b6077aaba1601cc90660a0eda1fc1 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 4 Dec 2015 12:06:46 -0500 Subject: [PATCH 0704/3836] Improve test coverage: list_router_interfaces API Change-Id: I559e28d550a617ac759d1c7a108d3f09a4c6f75b --- shade/tests/unit/test_shade.py | 77 ++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 6a0fb7fa8..8c647d430 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -218,6 +218,83 @@ def test_delete_router_multiple_using_id(self, mock_client): self.cloud.delete_router('123') self.assertTrue(mock_client.delete_router.called) + @mock.patch.object(shade.OpenStackCloud, 'search_ports') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_list_router_interfaces_all(self, mock_client, mock_search): + internal_port = {'id': 'internal_port_id', + 'fixed_ips': [ + ('internal_subnet_id', 'ip_address'), + ]} + external_port = {'id': 'external_port_id', + 'fixed_ips': [ + ('external_subnet_id', 'ip_address'), + ]} + port_list = [internal_port, external_port] + router = { + 'id': 'router_id', + 'external_gateway_info': { + 'external_fixed_ips': [('external_subnet_id', 'ip_address')] + } + } + mock_search.return_value = port_list + ret = self.cloud.list_router_interfaces(router) + mock_search.assert_called_once_with( + filters={'device_id': router['id']} + ) + self.assertEqual(port_list, ret) + + @mock.patch.object(shade.OpenStackCloud, 'search_ports') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_list_router_interfaces_internal(self, mock_client, mock_search): + internal_port = {'id': 'internal_port_id', + 'fixed_ips': [ + ('internal_subnet_id', 'ip_address'), + ]} + external_port = {'id': 'external_port_id', + 'fixed_ips': [ + ('external_subnet_id', 'ip_address'), + ]} + port_list = [internal_port, external_port] + router = { + 'id': 'router_id', + 'external_gateway_info': { + 'external_fixed_ips': [('external_subnet_id', 'ip_address')] + } + } + mock_search.return_value = port_list + ret = self.cloud.list_router_interfaces(router, + interface_type='internal') + mock_search.assert_called_once_with( + filters={'device_id': router['id']} + ) + self.assertEqual([internal_port], ret) + + @mock.patch.object(shade.OpenStackCloud, 'search_ports') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_list_router_interfaces_external(self, mock_client, mock_search): + internal_port = {'id': 'internal_port_id', + 'fixed_ips': [ + ('internal_subnet_id', 'ip_address'), + ]} + external_port = {'id': 'external_port_id', + 'fixed_ips': [ + ('external_subnet_id', 'ip_address'), + ]} + port_list = [internal_port, external_port] + router = { + 'id': 'router_id', + 'external_gateway_info': { + 'external_fixed_ips': [('external_subnet_id', 'ip_address')] + } + } + mock_search.return_value = port_list + ret = self.cloud.list_router_interfaces(router, + interface_type='external') + mock_search.assert_called_once_with( + filters={'device_id': router['id']} + ) + self.assertEqual([external_port], ret) + @mock.patch.object(shade.OpenStackCloud, 'search_networks') @mock.patch.object(shade.OpenStackCloud, 'neutron_client') def test_create_subnet(self, mock_client, mock_search): From 6aecb87e7f2cb2c031bf9c89564ea0b46d387c79 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 6 Dec 2015 10:35:08 -0500 Subject: [PATCH 0705/3836] Update vexxhost to Identity v3 There is a discovery URL for vexxhost for keystone v3. Also, there is a new vexxhost domain for it. Also, vexxhost has DNS running designate v1. And make the region list a list of one region, because there is a second region coming soon. Change-Id: Ie72c19976646f41c713124659e69725df59e1580 --- doc/source/vendor-support.rst | 5 ++++- os_client_config/vendors/vexxhost.json | 8 ++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index 90fd31fad..d7af6b913 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -271,10 +271,13 @@ gd1 Guangdong vexxhost -------- -http://auth.api.thenebulacloud.com:5000/v2.0/ +http://auth.vexxhost.net ============== ================ Region Name Human Name ============== ================ ca-ymq-1 Montreal ============== ================ + +* DNS API Version is 1 +* Identity API Version is 3 diff --git a/os_client_config/vendors/vexxhost.json b/os_client_config/vendors/vexxhost.json index 25911cae1..dd683be86 100644 --- a/os_client_config/vendors/vexxhost.json +++ b/os_client_config/vendors/vexxhost.json @@ -2,9 +2,13 @@ "name": "vexxhost", "profile": { "auth": { - "auth_url": "http://auth.api.thenebulacloud.com:5000/v2.0/" + "auth_url": "http://auth.vexxhost.net" }, - "region_name": "ca-ymq-1", + "regions": [ + "ca-ymq-1" + ], + "dns_api_version": "1", + "identity_api_version": "3", "floating_ip_source": "None" } } From ed2f34b06a7d581fb5fdd9811e3f8a7f748a2ce4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 4 Nov 2015 09:22:17 -0500 Subject: [PATCH 0706/3836] Add method for registering argparse options keystoneauth knows about a bunch of argparse options that users from a command line will want. We do a good job of processing them once they've been collected, but an os-client-config user doesn't have a great way to make sure that they register all of the options, especially when once considers that you really want to peek at the args to see which auth plugin has been selected so that the right arguments can be registered and displayed. Depends-On: Ifea90b981044009c3642b268dd639a703df1ef05 Change-Id: Ic196f65f89b3ccf92ebec39564f5eaefe8a4ae4b --- README.rst | 23 +++++ os_client_config/config.py | 124 ++++++++++++++++++++++++++ os_client_config/tests/test_config.py | 115 ++++++++++++++++++++++++ requirements.txt | 2 +- 4 files changed, 263 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 0bdc18290..d8fde5924 100644 --- a/README.rst +++ b/README.rst @@ -296,3 +296,26 @@ Or, get all of the clouds. cloud_config = os_client_config.OpenStackConfig().get_all_clouds() for cloud in cloud_config: print(cloud.name, cloud.region, cloud.config) + +argparse +-------- + +If you're using os-client-config from a program that wants to process +command line options, there is a registration function to register the +arguments that both os-client-config and keystoneauth know how to deal +with - as well as a consumption argument. + +:: + + import argparse + import sys + + import os_client_config + + cloud_config = os_client_config.OpenStackConfig() + parser = argparse.ArgumentParser() + cloud_config.register_argparse_arguments(parser, sys.argv) + + options = parser.parse_args() + + cloud = cloud_config.get_one_cloud(argparse=options) diff --git a/os_client_config/config.py b/os_client_config/config.py index dff637a10..48aa0e2d0 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -13,11 +13,14 @@ # under the License. +# alias because we already had an option named argparse +import argparse as argparse_mod import json import os import warnings import appdirs +from keystoneauth1 import adapter from keystoneauth1 import loading import yaml @@ -245,6 +248,9 @@ def __init__(self, config_files=None, vendor_files=None, self._cache_expiration = cache_settings.get( 'expiration', self._cache_expiration) + # Flag location to hold the peeked value of an argparse timeout value + self._argv_timeout = False + def _load_config_file(self): return self._load_yaml_json_file(self._config_files) @@ -451,6 +457,94 @@ def _fix_backwards_auth_plugin(self, cloud): cloud['auth_type'] = 'password' return cloud + def register_argparse_arguments(self, parser, argv, service_keys=[]): + """Register all of the common argparse options needed. + + Given an argparse parser, register the keystoneauth Session arguments, + the keystoneauth Auth Plugin Options and os-cloud. Also, peek in the + argv to see if all of the auth plugin options should be registered + or merely the ones already configured. + :param argparse.ArgumentParser: parser to attach argparse options to + :param list argv: the arguments provided to the application + :param string service_keys: Service or list of services this argparse + should be specialized for, if known. + The first item in the list will be used + as the default value for service_type + (optional) + + :raises exceptions.OpenStackConfigException if an invalid auth-type + is requested + """ + + local_parser = argparse_mod.ArgumentParser(add_help=False) + + for p in (parser, local_parser): + p.add_argument( + '--os-cloud', + metavar='', + default=os.environ.get('OS_CLOUD', None), + help='Named cloud to connect to') + + # we need to peek to see if timeout was actually passed, since + # the keystoneauth declaration of it has a default, which means + # we have no clue if the value we get is from the ksa default + # for from the user passing it explicitly. We'll stash it for later + local_parser.add_argument('--timeout', metavar='') + + # Peek into the future and see if we have an auth-type set in + # config AND a cloud set, so that we know which command line + # arguments to register and show to the user (the user may want + # to say something like: + # openstack --os-cloud=foo --os-oidctoken=bar + # although I think that user is the cause of my personal pain + options, _args = local_parser.parse_known_args(argv) + if options.timeout: + self._argv_timeout = True + + # validate = False because we're not _actually_ loading here + # we're only peeking, so it's the wrong time to assert that + # the rest of the arguments given are invalid for the plugin + # chosen (for instance, --help may be requested, so that the + # user can see what options he may want to give + cloud = self.get_one_cloud(argparse=options, validate=False) + default_auth_type = cloud.config['auth_type'] + + try: + loading.register_auth_argparse_arguments( + parser, argv, default=default_auth_type) + except Exception: + # Hidiing the keystoneauth exception because we're not actually + # loading the auth plugin at this point, so the error message + # from it doesn't actually make sense to os-client-config users + options, _args = parser.parse_known_args(argv) + plugin_names = loading.get_available_plugin_names() + raise exceptions.OpenStackConfigException( + "An invalid auth-type was specified: {auth_type}." + " Valid choices are: {plugin_names}.".format( + auth_type=options.os_auth_type, + plugin_names=",".join(plugin_names))) + + if service_keys: + primary_service = service_keys[0] + else: + primary_service = None + loading.register_session_argparse_arguments(parser) + adapter.register_adapter_argparse_arguments( + parser, service_type=primary_service) + for service_key in service_keys: + # legacy clients have un-prefixed api-version options + parser.add_argument( + '--{service_key}-api-version'.format( + service_key=service_key.replace('_', '-'), + help=argparse_mod.SUPPRESS)) + adapter.register_service_adapter_argparse_arguments( + parser, service_type=service_key) + + # Backwards compat options for legacy clients + parser.add_argument('--http-timeout', help=argparse_mod.SUPPRESS) + parser.add_argument('--os-endpoint-type', help=argparse_mod.SUPPRESS) + parser.add_argument('--endpoint-type', help=argparse_mod.SUPPRESS) + def _fix_backwards_interface(self, cloud): new_cloud = {} for key in cloud.keys(): @@ -461,6 +555,30 @@ def _fix_backwards_interface(self, cloud): new_cloud[target_key] = cloud[key] return new_cloud + def _fix_backwards_api_timeout(self, cloud): + new_cloud = {} + # requests can only have one timeout, which means that in a single + # cloud there is no point in different timeout values. However, + # for some reason many of the legacy clients decided to shove their + # service name in to the arg name for reasons surpassin sanity. If + # we find any values that are not api_timeout, overwrite api_timeout + # with the value + service_timeout = None + for key in cloud.keys(): + if key.endswith('timeout') and not ( + key == 'timeout' or key == 'api_timeout'): + service_timeout = cloud[key] + else: + new_cloud[key] = cloud[key] + if service_timeout is not None: + new_cloud['api_timeout'] = service_timeout + # The common argparse arg from keystoneauth is called timeout, but + # os-client-config expects it to be called api_timeout + if self._argv_timeout: + if 'timeout' in new_cloud and new_cloud['timeout']: + new_cloud['api_timeout'] = new_cloud.pop('timeout') + return new_cloud + def get_all_clouds(self): clouds = [] @@ -671,6 +789,12 @@ def get_one_cloud(self, cloud=None, validate=True, else: config[key] = val + # These backwards compat values are only set via argparse. If it's + # there, it's because it was passed in explicitly, and should win + config = self._fix_backwards_api_timeout(config) + if 'endpoint_type' in config: + config['interface'] = config.pop('endpoint_type') + for key in BOOL_KEYS: if key in config: if type(config[key]) is not bool: diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index aff8c6d25..c9318fcd9 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -17,6 +17,7 @@ import os import fixtures +import testtools import yaml from os_client_config import cloud_config @@ -341,6 +342,120 @@ def test_fix_env_args(self): self.assertDictEqual({'compute_api_version': 1}, fixed_args) + def test_register_argparse_cloud(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + parser = argparse.ArgumentParser() + c.register_argparse_arguments(parser, []) + opts, _remain = parser.parse_known_args(['--os-cloud', 'foo']) + self.assertEqual(opts.os_cloud, 'foo') + + def test_register_argparse_bad_plugin(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + parser = argparse.ArgumentParser() + self.assertRaises( + exceptions.OpenStackConfigException, + c.register_argparse_arguments, + parser, ['--os-auth-type', 'foo']) + + def test_register_argparse_not_password(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + parser = argparse.ArgumentParser() + args = [ + '--os-auth-type', 'v3token', + '--os-token', 'some-secret', + ] + c.register_argparse_arguments(parser, args) + opts, _remain = parser.parse_known_args(args) + self.assertEqual(opts.os_token, 'some-secret') + + def test_register_argparse_password(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + parser = argparse.ArgumentParser() + args = [ + '--os-password', 'some-secret', + ] + c.register_argparse_arguments(parser, args) + opts, _remain = parser.parse_known_args(args) + self.assertEqual(opts.os_password, 'some-secret') + with testtools.ExpectedException(AttributeError): + opts.os_token + + def test_register_argparse_service_type(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + parser = argparse.ArgumentParser() + args = [ + '--os-service-type', 'network', + '--os-endpoint-type', 'admin', + '--http-timeout', '20', + ] + c.register_argparse_arguments(parser, args) + opts, _remain = parser.parse_known_args(args) + self.assertEqual(opts.os_service_type, 'network') + self.assertEqual(opts.os_endpoint_type, 'admin') + self.assertEqual(opts.http_timeout, '20') + with testtools.ExpectedException(AttributeError): + opts.os_network_service_type + cloud = c.get_one_cloud(argparse=opts, verify=False) + self.assertEqual(cloud.config['service_type'], 'network') + self.assertEqual(cloud.config['interface'], 'admin') + self.assertEqual(cloud.config['api_timeout'], '20') + self.assertNotIn('http_timeout', cloud.config) + + def test_register_argparse_network_service_type(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + parser = argparse.ArgumentParser() + args = [ + '--os-endpoint-type', 'admin', + '--network-api-version', '4', + ] + c.register_argparse_arguments(parser, args, ['network']) + opts, _remain = parser.parse_known_args(args) + self.assertEqual(opts.os_service_type, 'network') + self.assertEqual(opts.os_endpoint_type, 'admin') + self.assertEqual(opts.os_network_service_type, None) + self.assertEqual(opts.os_network_api_version, None) + self.assertEqual(opts.network_api_version, '4') + cloud = c.get_one_cloud(argparse=opts, verify=False) + self.assertEqual(cloud.config['service_type'], 'network') + self.assertEqual(cloud.config['interface'], 'admin') + self.assertEqual(cloud.config['network_api_version'], '4') + self.assertNotIn('http_timeout', cloud.config) + + def test_register_argparse_network_service_types(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + parser = argparse.ArgumentParser() + args = [ + '--os-compute-service-name', 'cloudServers', + '--os-network-service-type', 'badtype', + '--os-endpoint-type', 'admin', + '--network-api-version', '4', + ] + c.register_argparse_arguments( + parser, args, ['compute', 'network', 'volume']) + opts, _remain = parser.parse_known_args(args) + self.assertEqual(opts.os_network_service_type, 'badtype') + self.assertEqual(opts.os_compute_service_type, None) + self.assertEqual(opts.os_volume_service_type, None) + self.assertEqual(opts.os_service_type, 'compute') + self.assertEqual(opts.os_compute_service_name, 'cloudServers') + self.assertEqual(opts.os_endpoint_type, 'admin') + self.assertEqual(opts.os_network_api_version, None) + self.assertEqual(opts.network_api_version, '4') + cloud = c.get_one_cloud(argparse=opts, verify=False) + self.assertEqual(cloud.config['service_type'], 'compute') + self.assertEqual(cloud.config['network_service_type'], 'badtype') + self.assertEqual(cloud.config['interface'], 'admin') + self.assertEqual(cloud.config['network_api_version'], '4') + self.assertNotIn('volume_service_type', cloud.config) + self.assertNotIn('http_timeout', cloud.config) + class TestConfigDefault(base.TestCase): diff --git a/requirements.txt b/requirements.txt index 3c32ced99..1531be808 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,5 @@ # process, which may cause wedges in the gate later. PyYAML>=3.1.0 appdirs>=1.3.0 -keystoneauth1>=1.0.0 +keystoneauth1>=2.1.0 requestsexceptions>=1.1.1 # Apache-2.0 From 5beaeef2c3140f84b2e5a57a789460d4db9ff766 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 3 Dec 2015 11:26:12 -0600 Subject: [PATCH 0707/3836] Add simple helper function for client construction Often times you don't want to take advantage of all the flexibility, you simple want the basic works-like-it-should thing. Add a warpper around get_legacy_client to do tht one thing. Change-Id: I086dc4a8e762d4e8e56e01cabe2386577f2ceec8 --- README.rst | 31 +++++++++++++++++++++++++++++++ os_client_config/__init__.py | 23 +++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/README.rst b/README.rst index d8fde5924..156c7607a 100644 --- a/README.rst +++ b/README.rst @@ -319,3 +319,34 @@ with - as well as a consumption argument. options = parser.parse_args() cloud = cloud_config.get_one_cloud(argparse=options) + +Constructing OpenStack Client objects +------------------------------------- + +If all you want to do is get a Client object from a python-*client library, +and you want it to do all the normal things related to clouds.yaml, `OS_` +environment variables, a hepler function is provided. + +:: + + import argparse + + from novaclient import client + import os_client_config + + nova = os_client_config.make_client('compute', client.Client) + +If you want to do the same thing but also support command line parsing. + +:: + + import argparse + + from novaclient import client + import os_client_config + + nova = os_client_config.make_client( + 'compute', client.Client, options=argparse.ArgumentParser()) + +If you want to get fancier than that in your python, then the rest of the +API is avaiable to you. But often times, you just want to do the one thing. diff --git a/os_client_config/__init__.py b/os_client_config/__init__.py index 00e6ff514..ac585f248 100644 --- a/os_client_config/__init__.py +++ b/os_client_config/__init__.py @@ -12,6 +12,8 @@ # License for the specific language governing permissions and limitations # under the License. +import sys + from os_client_config.config import OpenStackConfig # noqa @@ -30,3 +32,24 @@ def simple_client(service_key, cloud=None, region_name=None): """ return OpenStackConfig().get_one_cloud( cloud=cloud, region_name=region_name).get_session_client('compute') + + +def make_client(service_key, constructor, options=None, **kwargs): + """Simple wrapper for getting a client instance from a client lib. + + OpenStack Client Libraries all have a fairly consistent constructor + interface which os-client-config supports. In the simple case, there + is one and only one right way to construct a client object. If as a user + you don't want to do fancy things, just use this. It honors OS_ environment + variables and clouds.yaml - and takes as **kwargs anything you'd expect + to pass in. + """ + config = OpenStackConfig() + if options: + config.register_argparse_options(options, sys.argv, service_key) + parsed_options = options.parse_args(sys.argv) + else: + parsed_options = None + + cloud_config = config.get_one_cloud(options=parsed_options, **kwargs) + return cloud_config.get_legacy_client(service_key, constructor) From b90f53bbf45c67fd2139b3f75c8f25bff0d3cfeb Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 6 Dec 2015 21:49:29 -0500 Subject: [PATCH 0708/3836] Updated README to clarify legacy client usage Also, update it to use code-block - which makes things look much nicer. Change-Id: I930ab63a5d159cf4cea27b4e2c4d6fd933de04fc --- README.rst | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/README.rst b/README.rst index 156c7607a..585dda9b9 100644 --- a/README.rst +++ b/README.rst @@ -29,7 +29,7 @@ Service specific settings, like the nova service type, are set with the default service type as a prefix. For instance, to set a special service_type for trove set -:: +.. code-block:: bash export OS_DATABASE_SERVICE_TYPE=rax:database @@ -56,7 +56,7 @@ Service specific settings, like the nova service type, are set with the default service type as a prefix. For instance, to set a special service_type for trove (because you're using Rackspace) set: -:: +.. code-block:: yaml database_service_type: 'rax:database' @@ -85,7 +85,7 @@ look in an OS specific config dir An example config file is probably helpful: -:: +.. code-block:: yaml clouds: mordred: @@ -155,7 +155,7 @@ the same location rules as `clouds.yaml`. It can contain anything you put in `clouds.yaml` and will take precedence over anything in the `clouds.yaml` file. -:: +.. code-block:: yaml # clouds.yaml clouds: @@ -209,7 +209,7 @@ that the resource should never expire. and presents the cache information so that your various applications that are connecting to OpenStack can share a cache should you desire. -:: +.. code-block:: yaml cache: class: dogpile.cache.pylibmc @@ -242,7 +242,7 @@ caused it to not actually function. In that case, there is a config option you can set to unbreak you `force_ipv4`, or `OS_FORCE_IPV4` boolean environment variable. -:: +.. code-block:: yaml client: force_ipv4: true @@ -270,7 +270,7 @@ Usage The simplest and least useful thing you can do is: -:: +.. code-block:: python python -m os_client_config.config @@ -279,7 +279,7 @@ it from python, which is much more likely what you want to do, things like: Get a named cloud. -:: +.. code-block:: python import os_client_config @@ -289,7 +289,7 @@ Get a named cloud. Or, get all of the clouds. -:: +.. code-block:: python import os_client_config @@ -305,7 +305,7 @@ command line options, there is a registration function to register the arguments that both os-client-config and keystoneauth know how to deal with - as well as a consumption argument. -:: +.. code-block:: python import argparse import sys @@ -320,14 +320,14 @@ with - as well as a consumption argument. cloud = cloud_config.get_one_cloud(argparse=options) -Constructing OpenStack Client objects -------------------------------------- +Constructing Legacy Client objects +---------------------------------- If all you want to do is get a Client object from a python-*client library, and you want it to do all the normal things related to clouds.yaml, `OS_` environment variables, a hepler function is provided. -:: +.. code-block:: python import argparse @@ -338,7 +338,7 @@ environment variables, a hepler function is provided. If you want to do the same thing but also support command line parsing. -:: +.. code-block:: python import argparse From 8eced67abe20160fc3f20a7c76f01baae2dd1956 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 3 Dec 2015 13:28:00 -0600 Subject: [PATCH 0709/3836] Make client constructor optional Turns out we know the mapping of service name to constsructor, so we can try the import for the user without actually importing. Keep the argument though, because this method should be usable by just about any random openstack client lib. Also, because backwards compat. Change-Id: I7e9672e3bf61b8b7b92d55903f4596382f18b515 --- README.rst | 9 +++---- os_client_config/cloud_config.py | 41 ++++++++++++++++++++++++++++++- os_client_config/constructors.py | 28 +++++++++++++++++++++ os_client_config/constructos.json | 10 ++++++++ 4 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 os_client_config/constructors.py create mode 100644 os_client_config/constructos.json diff --git a/README.rst b/README.rst index 156c7607a..7541b9335 100644 --- a/README.rst +++ b/README.rst @@ -325,16 +325,16 @@ Constructing OpenStack Client objects If all you want to do is get a Client object from a python-*client library, and you want it to do all the normal things related to clouds.yaml, `OS_` -environment variables, a hepler function is provided. +environment variables, a hepler function is provided. The following +will get you a fully configured `novaclient` instance. :: import argparse - from novaclient import client import os_client_config - nova = os_client_config.make_client('compute', client.Client) + nova = os_client_config.make_client('compute') If you want to do the same thing but also support command line parsing. @@ -342,11 +342,10 @@ If you want to do the same thing but also support command line parsing. import argparse - from novaclient import client import os_client_config nova = os_client_config.make_client( - 'compute', client.Client, options=argparse.ArgumentParser()) + 'compute', options=argparse.ArgumentParser()) If you want to get fancier than that in your python, then the rest of the API is avaiable to you. But often times, you just want to do the one thing. diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 18ea4c1eb..6b3b5d9a0 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. +import importlib import warnings from keystoneauth1 import adapter @@ -20,9 +21,44 @@ import requestsexceptions from os_client_config import _log +from os_client_config import constructors from os_client_config import exceptions +def _get_client(service_key): + class_mapping = constructors.get_constructor_mapping() + if service_key not in class_mapping: + raise exceptions.OpenStackConfigException( + "Service {service_key} is unkown. Please pass in a client" + " constructor or submit a patch to os-client-config".format( + service_key=service_key)) + mod_name, ctr_name = class_mapping[service_key].rsplit('.', 1) + lib_name = mod_name.split('.')[0] + try: + mod = importlib.import_module(mod_name) + except ImportError: + raise exceptions.OpenStackConfigException( + "Client for '{service_key}' was requested, but" + " {mod_name} was unable to be imported. Either import" + " the module yourself and pass the constructor in as an argument," + " or perhaps you do not have python-{lib_name} installed.".format( + service_key=service_key, + mod_name=mod_name, + lib_name=lib_name)) + try: + ctr = getattr(mod, ctr_name) + except AttributeError: + raise exceptions.OpenStackConfigException( + "Client for '{service_key}' was requested, but although" + " {mod_name} imported fine, the constructor at {fullname}" + " as not found. Please check your installation, we have no" + " clue what is wrong with your computer.".format( + service_key=service_key, + mod_name=mod_name, + fullname=class_mapping[service_key])) + return ctr + + def _make_key(key, service_type): if not service_type: return key @@ -217,7 +253,7 @@ def get_session_endpoint(self, service_key): return endpoint def get_legacy_client( - self, service_key, client_class, interface_key=None, + self, service_key, client_class=None, interface_key=None, pass_version_arg=True, **kwargs): """Return a legacy OpenStack client object for the given config. @@ -254,6 +290,9 @@ def get_legacy_client( Client constructor, so this is in case anything additional needs to be passed in. """ + if not client_class: + client_class = _get_client(service_key) + # Because of course swift is different if service_key == 'object-store': return self._get_swift_client(client_class=client_class, **kwargs) diff --git a/os_client_config/constructors.py b/os_client_config/constructors.py new file mode 100644 index 000000000..e88ac92d6 --- /dev/null +++ b/os_client_config/constructors.py @@ -0,0 +1,28 @@ +# Copyright (c) 2014 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. + +import json +import os + +_json_path = os.path.join( + os.path.dirname(os.path.realpath(__file__)), 'constructors.json') +_class_mapping = None + + +def get_constructor_mapping(): + global _class_mapping + if not _class_mapping: + with open(_json_path, 'r') as json_file: + _class_mapping = json.load(json_file) + return _class_mapping diff --git a/os_client_config/constructos.json b/os_client_config/constructos.json new file mode 100644 index 000000000..d9ebf2c97 --- /dev/null +++ b/os_client_config/constructos.json @@ -0,0 +1,10 @@ +{ + "compute": "novaclient.client.Client", + "database": "troveclient.client.Client", + "identity": "keystoneclient.client.Client", + "image": "glanceclient.Client", + "network": "neutronclient.neutron.client.Client", + "object-store": "swiftclient.client.Connection", + "orchestration": "heatclient.client.Client", + "volume": "cinderclient.client.Client" +} From 1221ea7fca67c22c455b4aeae1e09c9ad7e928a7 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 4 Dec 2015 13:19:19 -0500 Subject: [PATCH 0710/3836] Fix a README typo - hepler is not actually a thing Change-Id: Ie8c267e75171b88bd3a1a3a684e85869e75843d7 --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 7541b9335..4edff9e14 100644 --- a/README.rst +++ b/README.rst @@ -325,7 +325,7 @@ Constructing OpenStack Client objects If all you want to do is get a Client object from a python-*client library, and you want it to do all the normal things related to clouds.yaml, `OS_` -environment variables, a hepler function is provided. The following +environment variables, a helper function is provided. The following will get you a fully configured `novaclient` instance. :: From d0c70cc96279d2bf24f30a501b9bf572e40f8e7a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 22 Nov 2015 10:55:46 -0500 Subject: [PATCH 0711/3836] Add support for generalized per-region settings Internap creates a public and a private network for each customer for each region on region activation. This means there is a per-region external network that the user may want to specify. Also, conoha has per-region auth-urls. Per-region config is still overridden by argparse or kwargs values. Change-Id: Ie2f3d2ca3ccbe7e3dd674983136b42c323544997 --- README.rst | 32 ++++++++++ os_client_config/config.py | 90 ++++++++++++++++++--------- os_client_config/tests/base.py | 17 ++++- os_client_config/tests/test_config.py | 50 ++++++++++++--- 4 files changed, 146 insertions(+), 43 deletions(-) diff --git a/README.rst b/README.rst index 9850c05ac..ced3b1821 100644 --- a/README.rst +++ b/README.rst @@ -265,6 +265,38 @@ environment variable. The above snippet will tell client programs to prefer returning an IPv4 address. +Per-region settings +------------------- + +Sometimes you have a cloud provider that has config that is common to the +cloud, but also with some things you might want to express on a per-region +basis. For instance, Internap provides a public and private network specific +to the user in each region, and putting the values of those networks into +config can make consuming programs more efficient. + +To support this, the region list can actually be a list of dicts, and any +setting that can be set at the cloud level can be overridden for that +region. + +:: + + clouds: + internap: + profile: internap + auth: + password: XXXXXXXXXXXXXXXXX + username: api-55f9a00fb2619 + project_name: inap-17037 + regions: + - name: ams01 + values: + external_network: inap-17037-WAN1654 + internal_network: inap-17037-LAN4820 + - name: nyj01 + values: + external_network: inap-17037-WAN7752 + internal_network: inap-17037-LAN6745 + Usage ----- diff --git a/os_client_config/config.py b/os_client_config/config.py index 48aa0e2d0..70989bfd7 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -15,6 +15,7 @@ # alias because we already had an option named argparse import argparse as argparse_mod +import copy import json import os import warnings @@ -121,8 +122,9 @@ def _merge_clouds(old_dict, new_dict): return ret -def _auth_update(old_dict, new_dict): +def _auth_update(old_dict, new_dict_source): """Like dict.update, except handling the nested dict called auth.""" + new_dict = copy.deepcopy(new_dict_source) for (k, v) in new_dict.items(): if k == 'auth': if k in old_dict: @@ -302,17 +304,29 @@ def get_cache_class(self): return self._cache_class def get_cache_arguments(self): - return self._cache_arguments.copy() + return copy.deepcopy(self._cache_arguments) def get_cache_expiration(self): - return self._cache_expiration.copy() + return copy.deepcopy(self._cache_expiration) + + def _expand_region_name(self, region_name): + return {'name': region_name, 'values': {}} + + def _expand_regions(self, regions): + ret = [] + for region in regions: + if isinstance(region, dict): + ret.append(copy.deepcopy(region)) + else: + ret.append(self._expand_region_name(region)) + return ret def _get_regions(self, cloud): if cloud not in self.cloud_config['clouds']: - return [''] + return [self._expand_region_name('')] config = self._normalize_keys(self.cloud_config['clouds'][cloud]) if 'regions' in config: - return config['regions'] + return self._expand_regions(config['regions']) elif 'region_name' in config: regions = config['region_name'].split(',') if len(regions) > 1: @@ -320,22 +334,39 @@ def _get_regions(self, cloud): "Comma separated lists in region_name are deprecated." " Please use a yaml list in the regions" " parameter in {0} instead.".format(self.config_filename)) - return regions + return self._expand_regions(regions) else: # crappit. we don't have a region defined. new_cloud = dict() our_cloud = self.cloud_config['clouds'].get(cloud, dict()) self._expand_vendor_profile(cloud, new_cloud, our_cloud) if 'regions' in new_cloud and new_cloud['regions']: - return new_cloud['regions'] + return self._expand_regions(new_cloud['regions']) elif 'region_name' in new_cloud and new_cloud['region_name']: - return [new_cloud['region_name']] + return [self._expand_region_name(new_cloud['region_name'])] else: # Wow. We really tried - return [''] + return [self._expand_region_name('')] + + def _get_region(self, cloud=None, region_name=''): + if not cloud: + return self._expand_region_name(region_name) + + regions = self._get_regions(cloud) + if not region_name: + return regions[0] + + for region in regions: + if region['name'] == region_name: + return region - def _get_region(self, cloud=None): - return self._get_regions(cloud)[0] + raise exceptions.OpenStackConfigException( + 'Region {region_name} is not a valid region name for cloud' + ' {cloud}. Valid choices are {region_list}. Please note that' + ' region names are case sensitive.'.format( + region_name=region_name, + region_list=','.join([r['name'] for r in regions]), + cloud=cloud)) def get_cloud_names(self): return self.cloud_config['clouds'].keys() @@ -585,7 +616,9 @@ def get_all_clouds(self): for cloud in self.get_cloud_names(): for region in self._get_regions(cloud): - clouds.append(self.get_one_cloud(cloud, region_name=region)) + if region: + clouds.append(self.get_one_cloud( + cloud, region_name=region['name'])) return clouds def _fix_args(self, args, argparse=None): @@ -764,30 +797,27 @@ def get_one_cloud(self, cloud=None, validate=True, else: cloud = self.default_cloud - if 'region_name' not in args or args['region_name'] is None: - args['region_name'] = self._get_region(cloud) - config = self._get_base_cloud_config(cloud) + # Get region specific settings + if 'region_name' not in args: + args['region_name'] = '' + region = self._get_region(cloud=cloud, region_name=args['region_name']) + args['region_name'] = region['name'] + region_args = copy.deepcopy(region['values']) + # Regions is a list that we can use to create a list of cloud/region # objects. It does not belong in the single-cloud dict - regions = config.pop('regions', None) - if regions and args['region_name'] not in regions: - raise exceptions.OpenStackConfigException( - 'Region {region_name} is not a valid region name for cloud' - ' {cloud}. Valid choices are {region_list}. Please note that' - ' region names are case sensitive.'.format( - region_name=args['region_name'], - region_list=','.join(regions), - cloud=cloud)) + config.pop('regions', None) # Can't just do update, because None values take over - for (key, val) in iter(args.items()): - if val is not None: - if key == 'auth' and config[key] is not None: - config[key] = _auth_update(config[key], val) - else: - config[key] = val + for arg_list in region_args, args: + for (key, val) in iter(arg_list.items()): + if val is not None: + if key == 'auth' and config[key] is not None: + config[key] = _auth_update(config[key], val) + else: + config[key] = val # These backwards compat values are only set via argparse. If it's # there, it's because it was passed in explicitly, and should win diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py index 33a868d33..6d9e093d3 100644 --- a/os_client_config/tests/base.py +++ b/os_client_config/tests/base.py @@ -16,6 +16,7 @@ # under the License. +import copy import os import tempfile @@ -96,8 +97,18 @@ 'auth_url': 'http://example.com/v2', }, 'regions': [ - 'region1', - 'region2', + { + 'name': 'region1', + 'values': { + 'external_network': 'region1-network', + } + }, + { + 'name': 'region2', + 'values': { + 'external_network': 'my-network', + } + } ], }, '_test_cloud_hyphenated': { @@ -139,7 +150,7 @@ def setUp(self): super(TestCase, self).setUp() self.useFixture(fixtures.NestedTempfile()) - conf = dict(USER_CONF) + conf = copy.deepcopy(USER_CONF) tdir = self.useFixture(fixtures.TempDir()) conf['cache']['path'] = tdir.path self.cloud_yaml = _write_yaml(conf) diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index c9318fcd9..a6a35ada9 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -226,6 +226,8 @@ def test_set_one_cloud_updates_cloud(self): new_config) with open(self.cloud_yaml) as fh: written_config = yaml.safe_load(fh) + # We write a cache config for testing + written_config['cache'].pop('path', None) self.assertEqual(written_config, resulting_config) @@ -239,18 +241,26 @@ def setUp(self): username='user', password='password', project_name='project', - region_name='other-test-region', + region_name='region2', snack_type='cookie', ) self.options = argparse.Namespace(**self.args) + def test_get_one_cloud_bad_region_argparse(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + + self.assertRaises( + exceptions.OpenStackConfigException, c.get_one_cloud, + cloud='_test-cloud_', argparse=self.options) + def test_get_one_cloud_argparse(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud(cloud='_test-cloud_', argparse=self.options) - self._assert_cloud_details(cc) - self.assertEqual(cc.region_name, 'other-test-region') + cc = c.get_one_cloud( + cloud='_test_cloud_regions', argparse=self.options) + self.assertEqual(cc.region_name, 'region2') self.assertEqual(cc.snack_type, 'cookie') def test_get_one_cloud_just_argparse(self): @@ -259,7 +269,7 @@ def test_get_one_cloud_just_argparse(self): cc = c.get_one_cloud(argparse=self.options) self.assertIsNone(cc.cloud) - self.assertEqual(cc.region_name, 'other-test-region') + self.assertEqual(cc.region_name, 'region2') self.assertEqual(cc.snack_type, 'cookie') def test_get_one_cloud_just_kwargs(self): @@ -268,7 +278,7 @@ def test_get_one_cloud_just_kwargs(self): cc = c.get_one_cloud(**self.args) self.assertIsNone(cc.cloud) - self.assertEqual(cc.region_name, 'other-test-region') + self.assertEqual(cc.region_name, 'region2') self.assertEqual(cc.snack_type, 'cookie') def test_get_one_cloud_dash_kwargs(self): @@ -318,10 +328,10 @@ def test_get_one_cloud_bad_region(self): def test_get_one_cloud_bad_region_no_regions(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - - cc = c.get_one_cloud(cloud='_test-cloud_', region_name='bad_region') - self._assert_cloud_details(cc) - self.assertEqual(cc.region_name, 'bad_region') + self.assertRaises( + exceptions.OpenStackConfigException, + c.get_one_cloud, + cloud='_test-cloud_', region_name='bad_region') def test_get_one_cloud_no_argparse_region2(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], @@ -333,6 +343,26 @@ def test_get_one_cloud_no_argparse_region2(self): self.assertEqual(cc.region_name, 'region2') self.assertIsNone(cc.snack_type) + def test_get_one_cloud_network(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + + cc = c.get_one_cloud( + cloud='_test_cloud_regions', region_name='region1', argparse=None) + self._assert_cloud_details(cc) + self.assertEqual(cc.region_name, 'region1') + self.assertEqual('region1-network', cc.config['external_network']) + + def test_get_one_cloud_per_region_network(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + + cc = c.get_one_cloud( + cloud='_test_cloud_regions', region_name='region2', argparse=None) + self._assert_cloud_details(cc) + self.assertEqual(cc.region_name, 'region2') + self.assertEqual('my-network', cc.config['external_network']) + def test_fix_env_args(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) From 83159ae486aa8a526655d68ef36b16d033de9089 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 4 Dec 2015 11:43:07 -0500 Subject: [PATCH 0712/3836] Use reno for release notes The OpenStack Release team has created a great release notes management tool that integrates with Sphinx. Start using it. For reference on how to use it, see http://docs.openstack.org/developer/reno/ Change-Id: I57692d720174fedb68ab2f52d5a4c496a6d993b2 --- doc/source/conf.py | 6 +++++- doc/source/index.rst | 1 + doc/source/installation.rst | 2 +- doc/source/releasenotes.rst | 5 +++++ releasenotes/notes/started-using-reno-242e2b0cd27f9480.yaml | 3 +++ test-requirements.txt | 1 + 6 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 doc/source/releasenotes.rst create mode 100644 releasenotes/notes/started-using-reno-242e2b0cd27f9480.yaml diff --git a/doc/source/conf.py b/doc/source/conf.py index bbebf4729..e55e85b19 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -3,7 +3,11 @@ sys.path.insert(0, os.path.abspath('../..')) -extensions = ['sphinx.ext.autodoc', 'oslosphinx'] +extensions = [ + 'sphinx.ext.autodoc', + 'oslosphinx', + 'reno.sphinxext' +] # The suffix of source filenames. source_suffix = '.rst' diff --git a/doc/source/index.rst b/doc/source/index.rst index af9eaa3d2..2aa8bd35e 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -16,6 +16,7 @@ Contents: contributing coding future + releasenotes .. include:: ../../README.rst diff --git a/doc/source/installation.rst b/doc/source/installation.rst index c47e48758..9699e779e 100644 --- a/doc/source/installation.rst +++ b/doc/source/installation.rst @@ -9,4 +9,4 @@ At the command line:: Or, if you have virtualenv wrapper installed:: $ mkvirtualenv shade - $ pip install shade \ No newline at end of file + $ pip install shade diff --git a/doc/source/releasenotes.rst b/doc/source/releasenotes.rst new file mode 100644 index 000000000..2a4bceb4e --- /dev/null +++ b/doc/source/releasenotes.rst @@ -0,0 +1,5 @@ +============= +Release Notes +============= + +.. release-notes:: diff --git a/releasenotes/notes/started-using-reno-242e2b0cd27f9480.yaml b/releasenotes/notes/started-using-reno-242e2b0cd27f9480.yaml new file mode 100644 index 000000000..d7cfb5145 --- /dev/null +++ b/releasenotes/notes/started-using-reno-242e2b0cd27f9480.yaml @@ -0,0 +1,3 @@ +--- +other: +- Started using reno for release notes. diff --git a/test-requirements.txt b/test-requirements.txt index d42d7d7f3..d3b6fb988 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -11,3 +11,4 @@ testrepository>=0.0.17 testscenarios>=0.4,<0.5 testtools>=0.9.32 warlock>=1.0.1,<2 +reno From f0440f80b766b9da74ef619a8c26757e511a6ad6 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 8 Dec 2015 10:35:41 -0500 Subject: [PATCH 0713/3836] Improve test coverage: hypervisor list The existing test for list_hypervisors() did not go deep enough into the call stack to test the actual underlying nova client call. Change-Id: I1d4cd9bc9424dfed6554794fa7a80db74cdac8a3 --- shade/tests/fakes.py | 6 ++++++ shade/tests/unit/test_shade_operator.py | 17 ++++++++--------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index fa6a75b9c..fadf5c1e0 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -193,3 +193,9 @@ def __init__(self, id, name, description, domain=None): self.name = name self.description = description self.domain = domain + + +class FakeHypervisor(object): + def __init__(self, id, hostname): + self.id = id + self.hypervisor_hostname = hostname diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index a7fe3ee9d..c5e9774aa 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -19,7 +19,6 @@ from os_client_config import cloud_config import shade -import munch from shade import exc from shade import meta from shade.tests import fakes @@ -1054,17 +1053,17 @@ def test_has_service_yes(self, get_session_mock): get_session_mock.return_value = session_mock self.assertTrue(self.cloud.has_service("image")) - @mock.patch.object(shade._tasks.HypervisorList, 'main') - def test_list_hypervisors(self, mock_hypervisorlist): + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_list_hypervisors(self, mock_nova): '''This test verifies that calling list_hypervisors results in a call - to the HypervisorList task.''' - mock_hypervisorlist.return_value = [ - munch.Munch({'hypervisor_hostname': 'testserver1', - 'id': '1'}), - munch.Munch({'hypervisor_hostname': 'testserver2', - 'id': '2'}) + to nova client.''' + mock_nova.hypervisors.list.return_value = [ + fakes.FakeHypervisor('1', 'testserver1'), + fakes.FakeHypervisor('2', 'testserver2'), ] r = self.cloud.list_hypervisors() + mock_nova.hypervisors.list.assert_called_once_with() self.assertEquals(2, len(r)) self.assertEquals('testserver1', r[0]['hypervisor_hostname']) + self.assertEquals('testserver2', r[1]['hypervisor_hostname']) From 8696fbdd765cf5a9767bfb9267499c159f30b853 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 8 Dec 2015 11:07:48 -0500 Subject: [PATCH 0714/3836] Improve test coverage: private extension API Add tests for querying for Nova extensions with our private API methods. Change-Id: If7ab721222c0643a320a067ee4d7f8b512b83eff --- shade/tests/unit/test_shade.py | 62 ++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 8c647d430..50f1a6b22 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -534,3 +534,65 @@ def test_iterate_timeout_timeout(self, mock_sleep): for count in _utils._iterate_timeout(0.1, message, wait=1): pass mock_sleep.assert_called_with(1.0) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test__nova_extensions(self, mock_nova): + body = { + 'extensions': [ + { + "updated": "2014-12-03T00:00:00Z", + "name": "Multinic", + "links": [], + "namespace": "http://openstack.org/compute/ext/fake_xml", + "alias": "NMN", + "description": "Multiple network support." + }, + { + "updated": "2014-12-03T00:00:00Z", + "name": "DiskConfig", + "links": [], + "namespace": "http://openstack.org/compute/ext/fake_xml", + "alias": "OS-DCF", + "description": "Disk Management Extension." + }, + ] + } + mock_nova.client.get.return_value = ('200', body) + extensions = self.cloud._nova_extensions() + mock_nova.client.get.assert_called_once_with(url='/extensions') + self.assertEqual(set(['NMN', 'OS-DCF']), extensions) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test__nova_extensions_fails(self, mock_nova): + mock_nova.client.get.side_effect = Exception() + with testtools.ExpectedException( + exc.OpenStackCloudException, + "Error fetching extension list for nova" + ): + self.cloud._nova_extensions() + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test__has_nova_extension(self, mock_nova): + body = { + 'extensions': [ + { + "updated": "2014-12-03T00:00:00Z", + "name": "Multinic", + "links": [], + "namespace": "http://openstack.org/compute/ext/fake_xml", + "alias": "NMN", + "description": "Multiple network support." + }, + { + "updated": "2014-12-03T00:00:00Z", + "name": "DiskConfig", + "links": [], + "namespace": "http://openstack.org/compute/ext/fake_xml", + "alias": "OS-DCF", + "description": "Disk Management Extension." + }, + ] + } + mock_nova.client.get.return_value = ('200', body) + self.assertTrue(self.cloud._has_nova_extension('NMN')) + self.assertFalse(self.cloud._has_nova_extension('invalid')) From c47ec154578e505eba9c81a8c90ef82703c697af Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 8 Dec 2015 13:06:56 -0500 Subject: [PATCH 0715/3836] Consider 'in-use' a non-pending volume for caching We don't cache volume list if one of the volumes is in pending state, as otherwise we'd miss the cloud-side cache invalidation. However, active volumes that are attached are in state "in-use" - so basically if you had one active volume, the cache would never cache. Change-Id: I03cbc1b814e4a5829936a22751ee81d52b83fb2e --- releasenotes/notes/cache-in-use-volumes-c7fa8bb378106fe3.yaml | 4 ++++ shade/openstackcloud.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/cache-in-use-volumes-c7fa8bb378106fe3.yaml diff --git a/releasenotes/notes/cache-in-use-volumes-c7fa8bb378106fe3.yaml b/releasenotes/notes/cache-in-use-volumes-c7fa8bb378106fe3.yaml new file mode 100644 index 000000000..4ac0b61af --- /dev/null +++ b/releasenotes/notes/cache-in-use-volumes-c7fa8bb378106fe3.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - Fixed caching the volume list when volumes are in + use. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index dc8471696..35708a19d 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -65,7 +65,7 @@ def _no_pending_volumes(volumes): '''If there are any volumes not in a steady state, don't cache''' for volume in volumes: - if volume['status'] not in ('available', 'error'): + if volume['status'] not in ('available', 'error', 'in-use'): return False return True From d7e616780b3768d8674875f0e40e09dc91b62951 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 8 Dec 2015 12:25:06 -0500 Subject: [PATCH 0716/3836] Bug fix: Fix pass thru filtering in list_networks The filters for list_networks() was not being passed through to the neutron client. Actually pass the arguments and add missing unit and functional tests to verify the behavior. Change-Id: I653ed4c4fcbab6f36bf03cc68ffe862b6bfcd6eb --- .../fix-list-networks-a592725df64c306e.yaml | 3 +++ shade/_tasks.py | 2 +- shade/tests/functional/test_network.py | 27 ++++++++++--------- shade/tests/unit/test_shade.py | 25 +++++++++++++++++ 4 files changed, 44 insertions(+), 13 deletions(-) create mode 100644 releasenotes/notes/fix-list-networks-a592725df64c306e.yaml diff --git a/releasenotes/notes/fix-list-networks-a592725df64c306e.yaml b/releasenotes/notes/fix-list-networks-a592725df64c306e.yaml new file mode 100644 index 000000000..eecc255e6 --- /dev/null +++ b/releasenotes/notes/fix-list-networks-a592725df64c306e.yaml @@ -0,0 +1,3 @@ +--- +fixes: + - Fix for list_networks() ignoring any filters. diff --git a/shade/_tasks.py b/shade/_tasks.py index 5f5320ef9..121605747 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -169,7 +169,7 @@ def main(self, client): class NetworkList(task_manager.Task): def main(self, client): - return client.neutron_client.list_networks() + return client.neutron_client.list_networks(**self.args) class NetworkCreate(task_manager.Task): diff --git a/shade/tests/functional/test_network.py b/shade/tests/functional/test_network.py index cb0a5da34..432c63061 100644 --- a/shade/tests/functional/test_network.py +++ b/shade/tests/functional/test_network.py @@ -19,9 +19,6 @@ Functional tests for `shade` network methods. """ -import random -import string - from shade import openstack_cloud from shade.exc import OpenStackCloudException from shade.tests import base @@ -33,14 +30,13 @@ def setUp(self): self.cloud = openstack_cloud(cloud='devstack-admin') if not self.cloud.has_service('network'): self.skipTest('Network service not supported by cloud') - self.network_prefix = 'test_network' + ''.join( - random.choice(string.ascii_lowercase) for _ in range(5)) + self.network_name = self.getUniqueString('network') self.addCleanup(self._cleanup_networks) def _cleanup_networks(self): exception_list = list() for network in self.cloud.list_networks(): - if network['name'].startswith(self.network_prefix): + if network['name'].startswith(self.network_name): try: self.cloud.delete_network(network['name']) except Exception as e: @@ -51,24 +47,31 @@ def _cleanup_networks(self): raise OpenStackCloudException('\n'.join(exception_list)) def test_create_network_basic(self): - net1_name = self.network_prefix + '_net1' - net1 = self.cloud.create_network(name=net1_name) + net1 = self.cloud.create_network(name=self.network_name) self.assertIn('id', net1) - self.assertEqual(net1_name, net1['name']) + self.assertEqual(self.network_name, net1['name']) self.assertFalse(net1['shared']) self.assertFalse(net1['router:external']) self.assertTrue(net1['admin_state_up']) def test_create_network_advanced(self): - net1_name = self.network_prefix + '_net1' net1 = self.cloud.create_network( - name=net1_name, + name=self.network_name, shared=True, external=True, admin_state_up=False, ) self.assertIn('id', net1) - self.assertEqual(net1_name, net1['name']) + self.assertEqual(self.network_name, net1['name']) self.assertTrue(net1['router:external']) self.assertTrue(net1['shared']) self.assertFalse(net1['admin_state_up']) + + def test_list_networks_filtered(self): + net1 = self.cloud.create_network(name=self.network_name) + self.assertIsNotNone(net1) + net2 = self.cloud.create_network(name=self.network_name + 'other') + self.assertIsNotNone(net2) + match = self.cloud.list_networks(filters=dict(name=self.network_name)) + self.assertEqual(1, len(match)) + self.assertEqual(net1['name'], match[0]['name']) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 50f1a6b22..a6c8bdb8f 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -102,6 +102,31 @@ def test_heat_args(self, mock_client, get_session_mock): session=mock.ANY, ) + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_list_networks(self, mock_neutron): + net1 = {'id': '1', 'name': 'net1'} + net2 = {'id': '2', 'name': 'net2'} + mock_neutron.list_networks.return_value = { + 'networks': [net1, net2] + } + nets = self.cloud.list_networks() + mock_neutron.list_networks.assert_called_once_with() + self.assertEqual([net1, net2], nets) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_list_networks_filtered(self, mock_neutron): + self.cloud.list_networks(filters={'name': 'test'}) + mock_neutron.list_networks.assert_called_once_with(name='test') + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_list_networks_exception(self, mock_neutron): + mock_neutron.list_networks.side_effect = Exception() + with testtools.ExpectedException( + exc.OpenStackCloudException, + "Error fetching network list" + ): + self.cloud.list_networks() + @mock.patch.object(shade.OpenStackCloud, 'search_subnets') def test_get_subnet(self, mock_search): subnet = dict(id='123', name='mickey') From 4c8cfe4b18bb17704d4e49af95a80d5ed04d5958 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 8 Dec 2015 14:32:54 -0500 Subject: [PATCH 0717/3836] Improve test coverage: network delete API Add missing unit tests for delete_network() API method. Change-Id: I75ab31ddeb731d192a0266712bfb95c5f21c8acb --- shade/tests/unit/test_network.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/shade/tests/unit/test_network.py b/shade/tests/unit/test_network.py index 02b41f7f7..11b39609e 100644 --- a/shade/tests/unit/test_network.py +++ b/shade/tests/unit/test_network.py @@ -11,6 +11,7 @@ # limitations under the License. import mock +import testtools import shade from shade.tests.unit import base @@ -48,3 +49,30 @@ def test_create_network_external(self, mock_neutron): } ) ) + + @mock.patch.object(shade.OpenStackCloud, 'get_network') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_delete_network(self, mock_neutron, mock_get): + mock_get.return_value = dict(id='net-id', name='test-net') + self.assertTrue(self.cloud.delete_network('test-net')) + mock_get.assert_called_once_with('test-net') + mock_neutron.delete_network.assert_called_once_with(network='net-id') + + @mock.patch.object(shade.OpenStackCloud, 'get_network') + def test_delete_network_not_found(self, mock_get): + mock_get.return_value = None + self.assertFalse(self.cloud.delete_network('test-net')) + mock_get.assert_called_once_with('test-net') + + @mock.patch.object(shade.OpenStackCloud, 'get_network') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_delete_network_exception(self, mock_neutron, mock_get): + mock_get.return_value = dict(id='net-id', name='test-net') + mock_neutron.delete_network.side_effect = Exception() + with testtools.ExpectedException( + shade.OpenStackCloudException, + "Error deleting network test-net" + ): + self.cloud.delete_network('test-net') + mock_get.assert_called_once_with('test-net') + mock_neutron.delete_network.assert_called_once_with(network='net-id') From d38af94bed473595155852f499c6aa56c5a33649 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 8 Dec 2015 15:18:27 -0500 Subject: [PATCH 0718/3836] Bug fix: Allow name update for domains We were not passing the 'name' argument through to the client for updating a domain. Fix that and add tests to verify it is passed. Also add missing domain API unit tests. Change-Id: I62a7d6de0a4890ca9f58aaa3de5090e395baf850 --- .../fix-update-domain-af47b066ac52eb7f.yaml | 3 + shade/operatorcloud.py | 3 +- shade/tests/functional/test_domain.py | 12 ++++ shade/tests/unit/test_domains.py | 64 +++++++++++++++++++ 4 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/fix-update-domain-af47b066ac52eb7f.yaml diff --git a/releasenotes/notes/fix-update-domain-af47b066ac52eb7f.yaml b/releasenotes/notes/fix-update-domain-af47b066ac52eb7f.yaml new file mode 100644 index 000000000..060461d09 --- /dev/null +++ b/releasenotes/notes/fix-update-domain-af47b066ac52eb7f.yaml @@ -0,0 +1,3 @@ +--- +fixes: + - Fix for update_domain() where 'name' was not updatable. diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 0ee183a2a..2d80d1153 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1030,7 +1030,8 @@ def update_domain( with _utils.shade_exceptions( "Error in updating domain {domain}".format(domain=domain_id)): domain = self.manager.submitTask(_tasks.DomainUpdate( - domain=domain_id, description=description, enabled=enabled)) + domain=domain_id, name=name, description=description, + enabled=enabled)) return _utils.normalize_domains([domain])[0] def delete_domain(self, domain_id): diff --git a/shade/tests/functional/test_domain.py b/shade/tests/functional/test_domain.py index 954348a89..dcb496bdb 100644 --- a/shade/tests/functional/test_domain.py +++ b/shade/tests/functional/test_domain.py @@ -63,3 +63,15 @@ def test_search_domains(self): results = self.cloud.search_domains(filters=dict(name=domain_name)) self.assertEqual(1, len(results)) self.assertEqual(domain_name, results[0]['name']) + + def test_update_domain(self): + domain = self.cloud.create_domain(self.domain_prefix, 'description') + self.assertEqual(self.domain_prefix, domain['name']) + self.assertEqual('description', domain['description']) + self.assertTrue(domain['enabled']) + updated = self.cloud.update_domain(domain['id'], name='updated name', + description='updated description', + enabled=False) + self.assertEqual('updated name', updated['name']) + self.assertEqual('updated description', updated['description']) + self.assertFalse(updated['enabled']) diff --git a/shade/tests/unit/test_domains.py b/shade/tests/unit/test_domains.py index a016d0732..fe72a0183 100644 --- a/shade/tests/unit/test_domains.py +++ b/shade/tests/unit/test_domains.py @@ -14,8 +14,10 @@ # limitations under the License. import mock +import testtools import shade +from shade import meta from shade.tests.unit import base from shade.tests import fakes @@ -46,3 +48,65 @@ def test_get_domain(self, mock_keystone): self.assertFalse(mock_keystone.domains.list.called) self.assertTrue(mock_keystone.domains.get.called) self.assertEqual(domain['name'], 'a-domain') + + @mock.patch.object(shade._utils, 'normalize_domains') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_create_domain(self, mock_keystone, mock_normalize): + mock_keystone.domains.create.return_value = domain_obj + self.cloud.create_domain(domain_obj.name, + domain_obj.description) + mock_keystone.domains.create.assert_called_once_with( + name=domain_obj.name, description=domain_obj.description, + enabled=True) + mock_normalize.assert_called_once_with([meta.obj_to_dict(domain_obj)]) + + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_create_domain_exception(self, mock_keystone): + mock_keystone.domains.create.side_effect = Exception() + with testtools.ExpectedException( + shade.OpenStackCloudException, + "Failed to create domain domain_name" + ): + self.cloud.create_domain('domain_name') + + @mock.patch.object(shade.OperatorCloud, 'update_domain') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_delete_domain(self, mock_keystone, mock_update): + mock_update.return_value = dict(id='update_domain_id') + self.cloud.delete_domain('domain_id') + mock_update.assert_called_once_with('domain_id', enabled=False) + mock_keystone.domains.delete.assert_called_once_with( + domain='update_domain_id') + + @mock.patch.object(shade.OperatorCloud, 'update_domain') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_delete_domain_exception(self, mock_keystone, mock_update): + mock_keystone.domains.delete.side_effect = Exception() + with testtools.ExpectedException( + shade.OpenStackCloudException, + "Failed to delete domain domain_id" + ): + self.cloud.delete_domain('domain_id') + + @mock.patch.object(shade._utils, 'normalize_domains') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_update_domain(self, mock_keystone, mock_normalize): + mock_keystone.domains.update.return_value = domain_obj + self.cloud.update_domain('domain_id', + name='new name', + description='new description', + enabled=False) + mock_keystone.domains.update.assert_called_once_with( + domain='domain_id', name='new name', + description='new description', enabled=False) + mock_normalize.assert_called_once_with( + [meta.obj_to_dict(domain_obj)]) + + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_update_domain_exception(self, mock_keystone): + mock_keystone.domains.update.side_effect = Exception() + with testtools.ExpectedException( + shade.OpenStackCloudException, + "Error in updating domain domain_id" + ): + self.cloud.delete_domain('domain_id') From f4237a809cccbbffce2233b1f283b36a9ebb75c1 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 9 Dec 2015 15:42:20 -0500 Subject: [PATCH 0719/3836] Add ceilometer constructor to known constructors In porting ospurge to use get_legacy_client, it became clear that the ceilometer client constructor was missing. Add it. Change-Id: I1102105b78574378c4f11064e21245b08513247b --- os_client_config/{constructos.json => constructors.json} | 1 + os_client_config/defaults.json | 1 + 2 files changed, 2 insertions(+) rename os_client_config/{constructos.json => constructors.json} (88%) diff --git a/os_client_config/constructos.json b/os_client_config/constructors.json similarity index 88% rename from os_client_config/constructos.json rename to os_client_config/constructors.json index d9ebf2c97..be4433920 100644 --- a/os_client_config/constructos.json +++ b/os_client_config/constructors.json @@ -3,6 +3,7 @@ "database": "troveclient.client.Client", "identity": "keystoneclient.client.Client", "image": "glanceclient.Client", + "metering": "ceilometerclient.client.Client", "network": "neutronclient.neutron.client.Client", "object-store": "swiftclient.client.Connection", "orchestration": "heatclient.client.Client", diff --git a/os_client_config/defaults.json b/os_client_config/defaults.json index eb8162e4e..6735b5535 100644 --- a/os_client_config/defaults.json +++ b/os_client_config/defaults.json @@ -12,6 +12,7 @@ "image_api_use_tasks": false, "image_api_version": "2", "image_format": "qcow2", + "metering_api_version": "2", "network_api_version": "2", "object_store_api_version": "1", "orchestration_api_version": "1", From 47d2472df849d16c6e6c998e4f7dbee35a99ab76 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 9 Dec 2015 16:29:19 -0500 Subject: [PATCH 0720/3836] Improve test coverage: volume attach/detach API Add missing tests for attaching and detaching volumes to and from servers. Change-Id: I0fa9f9b0190cbd31d4c9b0cc6422c2f876510f77 --- shade/tests/unit/test_volume.py | 194 ++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 shade/tests/unit/test_volume.py diff --git a/shade/tests/unit/test_volume.py b/shade/tests/unit/test_volume.py new file mode 100644 index 000000000..f53f284c3 --- /dev/null +++ b/shade/tests/unit/test_volume.py @@ -0,0 +1,194 @@ +# -*- 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. + + +import mock +import testtools + +import shade +from shade.tests.unit import base + + +class TestVolume(base.TestCase): + + def setUp(self): + super(TestVolume, self).setUp() + self.cloud = shade.openstack_cloud(validate=False) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_attach_volume(self, mock_nova): + server = dict(id='server001') + volume = dict(id='volume001', status='available', attachments=[]) + rvol = dict(id='volume001', status='attached', + attachments=[ + {'server_id': server['id'], 'device': 'device001'} + ]) + mock_nova.volumes.create_server_volume.return_value = rvol + + ret = self.cloud.attach_volume(server, volume, wait=False) + + self.assertEqual(rvol, ret) + mock_nova.volumes.create_server_volume.assert_called_once_with( + volume_id=volume['id'], server_id=server['id'], device=None + ) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_attach_volume_exception(self, mock_nova): + server = dict(id='server001') + volume = dict(id='volume001', status='available', attachments=[]) + mock_nova.volumes.create_server_volume.side_effect = Exception() + + with testtools.ExpectedException( + shade.OpenStackCloudException, + "Error attaching volume %s to server %s" % ( + volume['id'], server['id']) + ): + self.cloud.attach_volume(server, volume, wait=False) + + @mock.patch.object(shade.OpenStackCloud, 'get_volume') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_attach_volume_wait(self, mock_nova, mock_get): + server = dict(id='server001') + volume = dict(id='volume001', status='available', attachments=[]) + attached_volume = dict( + id=volume['id'], status='attached', + attachments=[{'server_id': server['id'], 'device': 'device001'}] + ) + mock_get.side_effect = iter([volume, attached_volume]) + + # defaults to wait=True + ret = self.cloud.attach_volume(server, volume) + + mock_nova.volumes.create_server_volume.assert_called_once_with( + volume_id=volume['id'], server_id=server['id'], device=None + ) + self.assertEqual(2, mock_get.call_count) + self.assertEqual(attached_volume, ret) + + @mock.patch.object(shade.OpenStackCloud, 'get_volume') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_attach_volume_wait_error(self, mock_nova, mock_get): + server = dict(id='server001') + volume = dict(id='volume001', status='available', attachments=[]) + errored_volume = dict(id=volume['id'], status='error', attachments=[]) + mock_get.side_effect = iter([volume, errored_volume]) + + with testtools.ExpectedException( + shade.OpenStackCloudException, + "Error in attaching volume %s" % errored_volume['id'] + ): + self.cloud.attach_volume(server, volume) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_attach_volume_not_available(self, mock_nova): + server = dict(id='server001') + volume = dict(id='volume001', status='error', attachments=[]) + + with testtools.ExpectedException( + shade.OpenStackCloudException, + "Volume %s is not available. Status is '%s'" % ( + volume['id'], volume['status']) + ): + self.cloud.attach_volume(server, volume) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_attach_volume_already_attached(self, mock_nova): + device_id = 'device001' + server = dict(id='server001') + volume = dict(id='volume001', + attachments=[ + {'server_id': 'server001', 'device': device_id} + ]) + + with testtools.ExpectedException( + shade.OpenStackCloudException, + "Volume %s already attached to server %s on device %s" % ( + volume['id'], server['id'], device_id) + ): + self.cloud.attach_volume(server, volume) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_detach_volume(self, mock_nova): + server = dict(id='server001') + volume = dict(id='volume001', + attachments=[ + {'server_id': 'server001', 'device': 'device001'} + ]) + self.cloud.detach_volume(server, volume, wait=False) + mock_nova.volumes.delete_server_volume.assert_called_once_with( + attachment_id=volume['id'], server_id=server['id'] + ) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_detach_volume_exception(self, mock_nova): + server = dict(id='server001') + volume = dict(id='volume001', + attachments=[ + {'server_id': 'server001', 'device': 'device001'} + ]) + mock_nova.volumes.delete_server_volume.side_effect = Exception() + with testtools.ExpectedException( + shade.OpenStackCloudException, + "Error detaching volume %s from server %s" % ( + volume['id'], server['id']) + ): + self.cloud.detach_volume(server, volume, wait=False) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_detach_volume_not_attached(self, mock_nova): + server = dict(id='server001') + volume = dict(id='volume001', + attachments=[ + {'server_id': 'server999', 'device': 'device001'} + ]) + with testtools.ExpectedException( + shade.OpenStackCloudException, + "Volume %s is not attached to server %s" % ( + volume['id'], server['id']) + ): + self.cloud.detach_volume(server, volume, wait=False) + + @mock.patch.object(shade.OpenStackCloud, 'get_volume') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_detach_volume_wait(self, mock_nova, mock_get): + server = dict(id='server001') + volume = dict(id='volume001', status='attached', + attachments=[ + {'server_id': 'server001', 'device': 'device001'} + ]) + avail_volume = dict(id=volume['id'], status='available', + attachments=[]) + mock_get.side_effect = iter([volume, avail_volume]) + self.cloud.detach_volume(server, volume) + mock_nova.volumes.delete_server_volume.assert_called_once_with( + attachment_id=volume['id'], server_id=server['id'] + ) + self.assertEqual(2, mock_get.call_count) + + @mock.patch.object(shade.OpenStackCloud, 'get_volume') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_detach_volume_wait_error(self, mock_nova, mock_get): + server = dict(id='server001') + volume = dict(id='volume001', status='attached', + attachments=[ + {'server_id': 'server001', 'device': 'device001'} + ]) + errored_volume = dict(id=volume['id'], status='error', attachments=[]) + mock_get.side_effect = iter([volume, errored_volume]) + + with testtools.ExpectedException( + shade.OpenStackCloudException, + "Error in detaching volume %s" % errored_volume['id'] + ): + self.cloud.detach_volume(server, volume) From 44e796a3dd48273236b586992fc870b4f81b26ed Mon Sep 17 00:00:00 2001 From: Clark Boylan Date: Wed, 9 Dec 2015 17:16:41 -0800 Subject: [PATCH 0721/3836] Make a new swift client prior to each image upload Since swift clients do not support keystone sessions any client that exists for a period of time runs the risk of having an expired token and the OpenStackCloud object will never recreate it to reauth. Work around this by making a new client whenever we attempt image uploads to swift. There are other potential ways to address this. We could pass the auth info into swift_client.get_capabilities() so that swift will reauth on its own if it needs to. We could just always make a new swiftclient for every swiftclient operation. We could add keystone session support to swiftclient. This workaround is simple and uses existing APIs with shade and os-c-c so starting with it. Change-Id: Ib35dd49627bc2209060848e719e3cec40dbb4f2a --- shade/openstackcloud.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 35708a19d..1ffa483f2 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -220,6 +220,10 @@ def invalidate(self): self._neutron_client = None self._nova_client = None self._swift_client = None + # Lock used to reset client as swift client does not + # support keystone sessions meaning that we have to make + # a new client in order to get new auth prior to operations. + self._swift_client_lock = threading.Lock() self._swift_service = None self._trove_client = None @@ -661,10 +665,11 @@ def get_template_contents( @property def swift_client(self): - if self._swift_client is None: - self._swift_client = self._get_client( - 'object-store', swiftclient.client.Connection) - return self._swift_client + with self._swift_client_lock: + if self._swift_client is None: + self._swift_client = self._get_client( + 'object-store', swiftclient.client.Connection) + return self._swift_client @property def swift_service(self): @@ -1997,6 +2002,8 @@ def _upload_image_put(self, name, filename, **image_kwargs): def _upload_image_task( self, name, filename, container, current_image, wait, timeout, **image_properties): + with self._swift_client_lock: + self._swift_client = None self.create_object( container, name, filename, md5=image_properties.get('md5', None), From 8d868cb27c13d73267f7097459b7b3e05ccf3eb0 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 10 Dec 2015 10:08:43 -0500 Subject: [PATCH 0722/3836] Improve test coverage: container/object list API Add missing tests for listing containers and objects. Change-Id: Iacbd5060f33a9f7273d7208dd400b4868602a3e4 --- shade/tests/unit/test_object.py | 47 +++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index 9c9c1cac5..15c72b33c 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -241,3 +241,50 @@ def test_get_container_access_not_found(self, mock_swift): "Container not found: %s" % name ): self.cloud.get_container_access(name) + + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + def test_list_containers(self, mock_swift): + containers = [dict(id='1', name='containter1')] + mock_swift.get_account.return_value = ('response_headers', containers) + ret = self.cloud.list_containers() + mock_swift.get_account.assert_called_once_with(full_listing=True) + self.assertEqual(containers, ret) + + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + def test_list_containers_not_full(self, mock_swift): + containers = [dict(id='1', name='containter1')] + mock_swift.get_account.return_value = ('response_headers', containers) + ret = self.cloud.list_containers(full_listing=False) + mock_swift.get_account.assert_called_once_with(full_listing=False) + self.assertEqual(containers, ret) + + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + def test_list_containers_exception(self, mock_swift): + mock_swift.get_account.side_effect = swift_exc.ClientException("ERROR") + self.assertRaises(exc.OpenStackCloudException, + self.cloud.list_containers) + + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + def test_list_objects(self, mock_swift): + objects = [dict(id='1', name='object1')] + mock_swift.get_container.return_value = ('response_headers', objects) + ret = self.cloud.list_objects('container_name') + mock_swift.get_container.assert_called_once_with( + container='container_name', full_listing=True) + self.assertEqual(objects, ret) + + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + def test_list_objects_not_full(self, mock_swift): + objects = [dict(id='1', name='object1')] + mock_swift.get_container.return_value = ('response_headers', objects) + ret = self.cloud.list_objects('container_name', full_listing=False) + mock_swift.get_container.assert_called_once_with( + container='container_name', full_listing=False) + self.assertEqual(objects, ret) + + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + def test_list_objects_exception(self, mock_swift): + mock_swift.get_container.side_effect = swift_exc.ClientException( + "ERROR") + self.assertRaises(exc.OpenStackCloudException, + self.cloud.list_objects, 'container_name') From 3b50573f639af48cf3b524cb12e8380b4464e274 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Sun, 6 Dec 2015 17:12:10 -0500 Subject: [PATCH 0723/3836] Add wait support for ironic node [de]activate Original shade support for ironic node activation and deactivation was without support for a wait option being passed into the helper methods. So the os_ironic_node module can be updated to support wait=True, we need to add support in the activate_node and deactivate_node methods. Change-Id: I69eee2d254cde2fffcf0c1ac7679a623fa7f97a5 --- shade/operatorcloud.py | 18 ++++++-- shade/tests/unit/test_shade_operator.py | 61 +++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 8 deletions(-) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 0ee183a2a..375932bc9 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -573,10 +573,14 @@ def node_set_provision_state(self, "Timeout waiting for node transition to " "target state of '%s'" % state): machine = self.get_machine(name_or_id) + # NOTE(TheJulia): This performs matching if the requested + # end state matches the state the node has reached. if state in machine['provision_state']: break + # NOTE(TheJulia): This performs matching for cases where + # the reqeusted state action ends in available state. if ("available" in machine['provision_state'] and - "provide" in state): + state in ["provide", "deleted"]): break else: machine = self.get_machine(name_or_id) @@ -720,11 +724,15 @@ def set_machine_power_reboot(self, name_or_id): """ self._set_machine_power_state(name_or_id, 'reboot') - def activate_node(self, uuid, configdrive=None): - self.node_set_provision_state(uuid, 'active', configdrive) + def activate_node(self, uuid, configdrive=None, + wait=False, timeout=1200): + self.node_set_provision_state( + uuid, 'active', configdrive, wait=wait, timeout=timeout) - def deactivate_node(self, uuid): - self.node_set_provision_state(uuid, 'deleted') + def deactivate_node(self, uuid, wait=False, + timeout=1200): + self.node_set_provision_state( + uuid, 'deleted', wait=wait, timeout=timeout) def set_node_instance_info(self, uuid, patch): with _utils.shade_exceptions(): diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index a7fe3ee9d..8209e197a 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -926,7 +926,8 @@ class available_node_state: self.assertDictEqual(node_provide_return_value, return_value) @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_activate_node(self, mock_client): + @mock.patch.object(shade._utils, '_iterate_timeout') + def test_activate_node(self, mock_timeout, mock_client): mock_client.node.set_provision_state.return_value = None node_id = 'node02' return_value = self.cloud.activate_node( @@ -937,18 +938,72 @@ def test_activate_node(self, mock_client): node_uuid='node02', state='active', configdrive='http://127.0.0.1/file.iso') + self.assertFalse(mock_timeout.called) @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_deactivate_node(self, mock_client): + def test_activate_node_timeout(self, mock_client): + + class active_node_state: + provision_state = 'active' + + class available_node_state: + provision_state = 'available' + + mock_client.node.get.side_effect = iter([ + available_node_state, + active_node_state]) + + mock_client.node.set_provision_state.return_value = None + node_id = 'node04' + return_value = self.cloud.activate_node( + node_id, + configdrive='http://127.0.0.1/file.iso', + wait=True, + timeout=2) + self.assertEqual(None, return_value) + mock_client.node.set_provision_state.assert_called_with( + node_uuid='node04', + state='active', + configdrive='http://127.0.0.1/file.iso') + self.assertEqual(mock_client.node.get.call_count, 2) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade._utils, '_iterate_timeout') + def test_deactivate_node(self, mock_timeout, mock_client): mock_client.node.set_provision_state.return_value = None node_id = 'node03' return_value = self.cloud.deactivate_node( - node_id) + node_id, wait=False) self.assertEqual(None, return_value) mock_client.node.set_provision_state.assert_called_with( node_uuid='node03', state='deleted', configdrive=None) + self.assertFalse(mock_timeout.called) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_deactivate_node_timeout(self, mock_client): + + class active_node_state: + provision_state = 'active' + + class deactivated_node_state: + provision_state = 'available' + + mock_client.node.get.side_effect = iter([ + active_node_state, + deactivated_node_state]) + + mock_client.node.set_provision_state.return_value = None + node_id = 'node03' + return_value = self.cloud.deactivate_node( + node_id, wait=True, timeout=2) + self.assertEqual(None, return_value) + mock_client.node.set_provision_state.assert_called_with( + node_uuid='node03', + state='deleted', + configdrive=None) + self.assertEqual(mock_client.node.get.call_count, 2) @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_set_node_instance_info(self, mock_client): From 8d5abfbf56a5620d1326d44dee8ffdc9dfba1c62 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 11 Dec 2015 15:45:42 -0500 Subject: [PATCH 0724/3836] Bug fix: delete_object() returns True/False Our delete APIs return True if the delete succeeded, or False if the thing being deleted was not found. delete_object() was not doing this, so this makes it consistent with the other delete API calls. Also adds missing unit tests for this method. Change-Id: I0951765193459300f08b0ab804e6ca327c6fa57d --- .../delete-obj-return-a3ecf0415b7a2989.yaml | 5 +++ shade/openstackcloud.py | 12 ++++++- shade/tests/unit/test_object.py | 34 +++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/delete-obj-return-a3ecf0415b7a2989.yaml diff --git a/releasenotes/notes/delete-obj-return-a3ecf0415b7a2989.yaml b/releasenotes/notes/delete-obj-return-a3ecf0415b7a2989.yaml new file mode 100644 index 000000000..381bcb99a --- /dev/null +++ b/releasenotes/notes/delete-obj-return-a3ecf0415b7a2989.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - The delete_object() method was not returning True/False, + similar to other delete methods. It is now consistent with + the other delete APIs. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 1ffa483f2..930b7597e 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3646,8 +3646,17 @@ def list_objects(self, container, full_listing=True): e.http_reason, e.http_host, e.http_path)) def delete_object(self, container, name): + """Delete an object from a container. + + :param string container: Name of the container holding the object. + :param string name: Name of the object to delete. + + :returns: True if delete succeeded, False if the object was not found. + + :raises: OpenStackCloudException on operation error. + """ if not self.get_object_metadata(container, name): - return + return False try: self.manager.submitTask(_tasks.ObjectDelete( container=container, obj=name)) @@ -3655,6 +3664,7 @@ def delete_object(self, container, name): raise OpenStackCloudException( "Object deletion failed: %s (%s/%s)" % ( e.http_reason, e.http_host, e.http_path)) + return True def get_object_metadata(self, container, name): try: diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index 15c72b33c..b27edb32e 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -288,3 +288,37 @@ def test_list_objects_exception(self, mock_swift): "ERROR") self.assertRaises(exc.OpenStackCloudException, self.cloud.list_objects, 'container_name') + + @mock.patch.object(shade.OpenStackCloud, 'get_object_metadata') + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + def test_delete_object(self, mock_swift, mock_get_meta): + container_name = 'container_name' + object_name = 'object_name' + mock_get_meta.return_value = {'object': object_name} + self.assertTrue(self.cloud.delete_object(container_name, object_name)) + mock_get_meta.assert_called_once_with(container_name, object_name) + mock_swift.delete_object.assert_called_once_with( + container=container_name, obj=object_name + ) + + @mock.patch.object(shade.OpenStackCloud, 'get_object_metadata') + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + def test_delete_object_not_found(self, mock_swift, mock_get_meta): + container_name = 'container_name' + object_name = 'object_name' + mock_get_meta.return_value = None + self.assertFalse(self.cloud.delete_object(container_name, object_name)) + mock_get_meta.assert_called_once_with(container_name, object_name) + self.assertFalse(mock_swift.delete_object.called) + + @mock.patch.object(shade.OpenStackCloud, 'get_object_metadata') + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + def test_delete_object_exception(self, mock_swift, mock_get_meta): + container_name = 'container_name' + object_name = 'object_name' + mock_get_meta.return_value = {'object': object_name} + mock_swift.delete_object.side_effect = swift_exc.ClientException( + "ERROR") + self.assertRaises(shade.OpenStackCloudException, + self.cloud.delete_object, + container_name, object_name) From fb8ea73f27f683519dcb27ed9e83cd6e4e423379 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 11 Dec 2015 16:29:32 -0500 Subject: [PATCH 0725/3836] Stack API improvements The exception that would be raised from list_stacks() if it failed would not have any type of error message (except the inner exception). This could be confusing, so add some text that indicates where the exception is being thrown. Unit tests for this method are added. Also improve some comments in delete_stack() and add unit tests for this method. Change-Id: I979a2948938fc73b708c26d81599061bac7681d4 --- shade/openstackcloud.py | 8 ++-- shade/tests/fakes.py | 8 ++++ shade/tests/unit/test_stack.py | 78 ++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 shade/tests/unit/test_stack.py diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 930b7597e..912b6ff27 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -739,14 +739,14 @@ def create_stack( def delete_stack(self, name_or_id): """Delete a Heat Stack - :param name_or_id: Stack name or id. + :param string name_or_id: Stack name or id. - :returns: True if delete succeeded, False otherwise. + :returns: True if delete succeeded, False if the stack was not found. :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call """ - stack = self.get_stack(name_or_id=name_or_id) + stack = self.get_stack(name_or_id) if stack is None: self.log.debug("Stack %s not found for deleting" % name_or_id) return False @@ -1054,7 +1054,7 @@ def list_stacks(self): :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call. """ - with _utils.shade_exceptions(): + with _utils.shade_exceptions("Error fetching stack list"): stacks = self.manager.submitTask(_tasks.StackList()) return stacks diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index fadf5c1e0..94a5e45f1 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -199,3 +199,11 @@ class FakeHypervisor(object): def __init__(self, id, hostname): self.id = id self.hypervisor_hostname = hostname + + +class FakeStack(object): + def __init__(self, id, name, description=None, status=None): + self.id = id + self.stack_name = name + self.stack_description = description + self.stack_status = status diff --git a/shade/tests/unit/test_stack.py b/shade/tests/unit/test_stack.py new file mode 100644 index 000000000..6cf493724 --- /dev/null +++ b/shade/tests/unit/test_stack.py @@ -0,0 +1,78 @@ +# -*- 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. + + +import mock +import testtools + +import shade +from shade import meta +from shade.tests import fakes +from shade.tests.unit import base + + +class TestStack(base.TestCase): + + def setUp(self): + super(TestStack, self).setUp() + self.cloud = shade.openstack_cloud(validate=False) + + @mock.patch.object(shade.OpenStackCloud, 'heat_client') + def test_list_stacks(self, mock_heat): + fake_stacks = [ + fakes.FakeStack('001', 'stack1'), + fakes.FakeStack('002', 'stack2'), + ] + mock_heat.stacks.list.return_value = fake_stacks + stacks = self.cloud.list_stacks() + mock_heat.stacks.list.assert_called_once_with() + self.assertEqual(meta.obj_list_to_dict(fake_stacks), stacks) + + @mock.patch.object(shade.OpenStackCloud, 'heat_client') + def test_list_stacks_exception(self, mock_heat): + mock_heat.stacks.list.side_effect = Exception() + with testtools.ExpectedException( + shade.OpenStackCloudException, + "Error fetching stack list" + ): + self.cloud.list_stacks() + + @mock.patch.object(shade.OpenStackCloud, 'get_stack') + @mock.patch.object(shade.OpenStackCloud, 'heat_client') + def test_delete_stack(self, mock_heat, mock_get): + stack = {'id': 'stack_id', 'name': 'stack_name'} + mock_get.return_value = stack + self.assertTrue(self.cloud.delete_stack('stack_name')) + mock_get.assert_called_once_with('stack_name') + mock_heat.stacks.delete.assert_called_once_with(id=stack['id']) + + @mock.patch.object(shade.OpenStackCloud, 'get_stack') + @mock.patch.object(shade.OpenStackCloud, 'heat_client') + def test_delete_stack_not_found(self, mock_heat, mock_get): + mock_get.return_value = None + self.assertFalse(self.cloud.delete_stack('stack_name')) + mock_get.assert_called_once_with('stack_name') + self.assertFalse(mock_heat.stacks.delete.called) + + @mock.patch.object(shade.OpenStackCloud, 'get_stack') + @mock.patch.object(shade.OpenStackCloud, 'heat_client') + def test_delete_stack_exception(self, mock_heat, mock_get): + stack = {'id': 'stack_id', 'name': 'stack_name'} + mock_get.return_value = stack + mock_heat.stacks.delete.side_effect = Exception() + with testtools.ExpectedException( + shade.OpenStackCloudException, + "Failed to delete stack %s" % stack['id'] + ): + self.cloud.delete_stack('stack_name') From 451e51340d8be78d66a27876cd966005b5d71dcc Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 14 Dec 2015 09:06:03 -0500 Subject: [PATCH 0726/3836] Bug fix: create_stack() fails when waiting The create_stack() call had two bugs: If wait was True, it attempted to call an iterate method that had been moved to _utils; it also did not return the stack from the get_stack() calls. Change-Id: I2588e3a84729a8f1b3bfcb6d401c7d51fb16832b --- .../create-stack-fix-12dbb59a48ac7442.yaml | 4 +++ shade/openstackcloud.py | 5 +-- shade/tests/unit/test_stack.py | 35 +++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/create-stack-fix-12dbb59a48ac7442.yaml diff --git a/releasenotes/notes/create-stack-fix-12dbb59a48ac7442.yaml b/releasenotes/notes/create-stack-fix-12dbb59a48ac7442.yaml new file mode 100644 index 000000000..35bb8c0b6 --- /dev/null +++ b/releasenotes/notes/create-stack-fix-12dbb59a48ac7442.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - The create_stack() call was fixed to call the correct iterator + method and to return the updated stack object when waiting. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 912b6ff27..699f8488d 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -730,10 +730,11 @@ def create_stack( stack = self.manager.submitTask(_tasks.StackCreate(**params)) if not wait: return stack - for count in _iterate_timeout( + for count in _utils._iterate_timeout( timeout, "Timed out waiting for heat stack to finish"): - if self.get_stack(name, cache=False): + stack = self.get_stack(name, cache=False) + if stack: return stack def delete_stack(self, name_or_id): diff --git a/shade/tests/unit/test_stack.py b/shade/tests/unit/test_stack.py index 6cf493724..b044c5577 100644 --- a/shade/tests/unit/test_stack.py +++ b/shade/tests/unit/test_stack.py @@ -16,6 +16,8 @@ import mock import testtools +from heatclient.common import template_utils + import shade from shade import meta from shade.tests import fakes @@ -76,3 +78,36 @@ def test_delete_stack_exception(self, mock_heat, mock_get): "Failed to delete stack %s" % stack['id'] ): self.cloud.delete_stack('stack_name') + + @mock.patch.object(template_utils, 'get_template_contents') + @mock.patch.object(shade.OpenStackCloud, 'heat_client') + def test_create_stack(self, mock_heat, mock_template): + mock_template.return_value = ({}, {}) + self.cloud.create_stack('stack_name') + self.assertTrue(mock_template.called) + mock_heat.stacks.create.assert_called_once_with( + stack_name='stack_name', + disable_rollback=False, + parameters={}, + template={}, + files={} + ) + + @mock.patch.object(template_utils, 'get_template_contents') + @mock.patch.object(shade.OpenStackCloud, 'get_stack') + @mock.patch.object(shade.OpenStackCloud, 'heat_client') + def test_create_stack_wait(self, mock_heat, mock_get, mock_template): + stack = {'id': 'stack_id', 'name': 'stack_name'} + mock_template.return_value = ({}, {}) + mock_get.side_effect = iter([None, stack]) + ret = self.cloud.create_stack('stack_name', wait=True) + self.assertTrue(mock_template.called) + mock_heat.stacks.create.assert_called_once_with( + stack_name='stack_name', + disable_rollback=False, + parameters={}, + template={}, + files={} + ) + self.assertEqual(2, mock_get.call_count) + self.assertEqual(stack, ret) From f4971b50a6abfe1ebdca20d1da82611538d69d76 Mon Sep 17 00:00:00 2001 From: Kyle Mestery Date: Mon, 14 Dec 2015 09:44:30 -0600 Subject: [PATCH 0727/3836] Pedantic spelling correction Change-Id: Id1d395485c0223847194abde6a0aa658ac27741d Signed-off-by: Kyle Mestery --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 33eda5c98..2b46ebdc4 100644 --- a/README.rst +++ b/README.rst @@ -25,7 +25,7 @@ Sometimes an example is nice. import shade - # Initialize and turn on debug loggin + # Initialize and turn on debug logging shade.simple_logging(debug=True) # Initialize cloud From add21aa9810ac1522d564791f7c4a932b79235a1 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sat, 12 Dec 2015 21:15:14 -0500 Subject: [PATCH 0728/3836] Fix server deletes when cinder isn't available This commit fixes an issue when trying to delete a server and cinder isn't available. Cinder isn't actually a required component of a cloud and some public clouds (like vexxhost) don't deploy it, so we shouldn't unconditionally attempt to talk to cinder without being able to handle this case. Change-Id: Ia1e19aad711c43fec21e2c1e5ad64b69acb561d3 --- shade/openstackcloud.py | 11 ++++++----- shade/tests/unit/test_delete_server.py | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 930b7597e..b4931e777 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3429,11 +3429,12 @@ def _delete_server( raise OpenStackCloudException( "Error in deleting server: {0}".format(e)) - # If the server has volume attachments, or if it has booted - # from volume, deleting it will change volume state - if (not server['image'] or not server['image']['id'] - or self.get_volumes(server)): - self.list_volumes.invalidate(self) + if self.has_service('volume'): + # If the server has volume attachments, or if it has booted + # from volume, deleting it will change volume state + if (not server['image'] or not server['image']['id'] + or self.get_volume(server)): + self.list_volumes.invalidate(self) return True diff --git a/shade/tests/unit/test_delete_server.py b/shade/tests/unit/test_delete_server.py index 13d2d3877..f9e8a9d1c 100644 --- a/shade/tests/unit/test_delete_server.py +++ b/shade/tests/unit/test_delete_server.py @@ -133,3 +133,17 @@ def _raise_fail(server): # Note that message is deprecated from Exception, but not in # the novaclient exceptions. self.assertIn(fail.message, str(exc)) + + @mock.patch('shade.OpenStackCloud.get_volume') + @mock.patch('shade.OpenStackCloud.nova_client') + def test_delete_server_no_cinder(self, nova_mock, cinder_mock): + """ + Test that novaclient server delete is called when wait=False + """ + server = fakes.FakeServer('1234', 'porky', 'ACTIVE') + nova_mock.servers.list.return_value = [server] + with mock.patch('shade.OpenStackCloud.has_service', + return_value=False): + self.assertTrue(self.cloud.delete_server('porky', wait=False)) + nova_mock.servers.delete.assert_called_with(server=server.id) + self.assertFalse(cinder_mock.called) From 837ca712288ceffea5b54ceaeb349d6577f38360 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 17 Nov 2015 15:17:55 -0500 Subject: [PATCH 0729/3836] Allow arbitrary client-specific options There are occasionally some client-specific things that would be handy to be able to configure about behaviors. For instance, the only config file that ansible's openstack inventory has is clouds.yaml. Rather than teaching os-client-config about such things, allow a pass-through config section. Apply key normalization to _'s like other configs, and merge the clouds and secure files so that the sections behave like other OCC config sections. Change-Id: If307e95006abf6e1efbbd77cfc99e5fdfed6c80a --- os_client_config/config.py | 13 +++++++++++++ os_client_config/tests/base.py | 4 ++++ os_client_config/tests/test_config.py | 21 +++++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/os_client_config/config.py b/os_client_config/config.py index 70989bfd7..ab3a003c6 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -253,6 +253,19 @@ def __init__(self, config_files=None, vendor_files=None, # Flag location to hold the peeked value of an argparse timeout value self._argv_timeout = False + def get_extra_config(self, key, defaults=None): + """Fetch an arbitrary extra chunk of config, laying in defaults. + + :param string key: name of the config section to fetch + :param dict defaults: (optional) default values to merge under the + found config + """ + if not defaults: + defaults = {} + return _merge_clouds( + self._normalize_keys(defaults), + self._normalize_keys(self.cloud_config.get(key, {}))) + def _load_config_file(self): return self._load_yaml_json_file(self._config_files) diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py index 6d9e093d3..fdc50cd0b 100644 --- a/os_client_config/tests/base.py +++ b/os_client_config/tests/base.py @@ -121,6 +121,10 @@ 'region_name': 'test-region', } }, + 'ansible': { + 'expand-hostvars': False, + 'use_hostnames': True, + }, } SECURE_CONF = { 'clouds': { diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index a6a35ada9..98aaf79fc 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -372,6 +372,27 @@ def test_fix_env_args(self): self.assertDictEqual({'compute_api_version': 1}, fixed_args) + def test_extra_config(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + + defaults = {'use_hostnames': False, 'other-value': 'something'} + ansible_options = c.get_extra_config('ansible', defaults) + + # This should show that the default for use_hostnames above is + # overridden by the value in the config file defined in base.py + # It should also show that other-value key is normalized and passed + # through even though there is no corresponding value in the config + # file, and that expand-hostvars key is normalized and the value + # from the config comes through even though there is no default. + self.assertDictEqual( + { + 'expand_hostvars': False, + 'use_hostnames': True, + 'other_value': 'something', + }, + ansible_options) + def test_register_argparse_cloud(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) From d9c283c37f350c8b1629e671f38ef447ef1bbd1f Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 15 Dec 2015 17:17:05 -0500 Subject: [PATCH 0730/3836] Add inventory unit tests Add some basic unit tests of the inventory code base. Change-Id: I368c1c36e7b49fec1c4c06ebc82c54e9103b951c --- shade/tests/unit/test_inventory.py | 88 ++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 shade/tests/unit/test_inventory.py diff --git a/shade/tests/unit/test_inventory.py b/shade/tests/unit/test_inventory.py new file mode 100644 index 000000000..751a456d1 --- /dev/null +++ b/shade/tests/unit/test_inventory.py @@ -0,0 +1,88 @@ +# -*- 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. + + +import mock +import os_client_config + +from shade import inventory +from shade.tests.unit import base + + +@mock.patch("os_client_config.config.OpenStackConfig") +class TestInventory(base.TestCase): + + def setUp(self): + super(TestInventory, self).setUp() + + @mock.patch("shade.OpenStackCloud") + def test__init(self, mock_cloud, mock_config): + mock_config.return_value.get_all_clouds.return_value = [{}] + + inv = inventory.OpenStackInventory() + + mock_config.assert_called_once_with( + config_files=os_client_config.config.CONFIG_FILES + ) + self.assertIsInstance(inv.clouds, list) + self.assertEqual(1, len(inv.clouds)) + self.assertTrue(mock_config.return_value.get_all_clouds.called) + + @mock.patch("shade.OpenStackCloud") + def test_list_hosts(self, mock_cloud, mock_config): + mock_config.return_value.get_all_clouds.return_value = [{}] + + inv = inventory.OpenStackInventory() + + server = dict(id='server_id', name='server_name') + self.assertIsInstance(inv.clouds, list) + self.assertEqual(1, len(inv.clouds)) + inv.clouds[0].list_servers.return_value = [server] + inv.clouds[0].get_openstack_vars.return_value = server + + ret = inv.list_hosts() + + inv.clouds[0].list_servers.assert_called_once_with() + inv.clouds[0].get_openstack_vars.assert_called_once_with(server) + self.assertEqual([server], ret) + + @mock.patch("shade.OpenStackCloud") + def test_search_hosts(self, mock_cloud, mock_config): + mock_config.return_value.get_all_clouds.return_value = [{}] + + inv = inventory.OpenStackInventory() + + server = dict(id='server_id', name='server_name') + self.assertIsInstance(inv.clouds, list) + self.assertEqual(1, len(inv.clouds)) + inv.clouds[0].list_servers.return_value = [server] + inv.clouds[0].get_openstack_vars.return_value = server + + ret = inv.search_hosts('server_id') + self.assertEqual([server], ret) + + @mock.patch("shade.OpenStackCloud") + def test_get_host(self, mock_cloud, mock_config): + mock_config.return_value.get_all_clouds.return_value = [{}] + + inv = inventory.OpenStackInventory() + + server = dict(id='server_id', name='server_name') + self.assertIsInstance(inv.clouds, list) + self.assertEqual(1, len(inv.clouds)) + inv.clouds[0].list_servers.return_value = [server] + inv.clouds[0].get_openstack_vars.return_value = server + + ret = inv.get_host('server_id') + self.assertEqual(server, ret) From 88b7e643b9637864252d9c6b715da0dced606352 Mon Sep 17 00:00:00 2001 From: Shuquan Huang Date: Thu, 17 Dec 2015 13:58:10 +0800 Subject: [PATCH 0731/3836] Replace assertEqual(None, *) with assertIsNone in tests Replace assertEqual(None, *) with assertIsNone in tests to have more clear messages in case of failure. Change-Id: Ia1af9f64f4f0a66c1429d81313b2c27a7c67cdd7 Closes-bug: #1280522 --- os_client_config/tests/test_cloud_config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index 9e683d1da..322973ad7 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -47,7 +47,7 @@ def test_arbitrary_attributes(self): self.assertEqual(1, cc.a) # Look up prefixed attribute, fail - returns None - self.assertEqual(None, cc.os_b) + self.assertIsNone(cc.os_b) # Look up straight value, then prefixed value self.assertEqual(3, cc.c) @@ -139,7 +139,7 @@ def test_getters(self): self.assertEqual('region-al', cc.get_region_name()) self.assertEqual('region-al', cc.get_region_name('image')) self.assertEqual('region-bl', cc.get_region_name('compute')) - self.assertEqual(None, cc.get_api_version('image')) + self.assertIsNone(cc.get_api_version('image')) self.assertEqual('2', cc.get_api_version('compute')) self.assertEqual('mage', cc.get_service_type('image')) self.assertEqual('compute', cc.get_service_type('compute')) @@ -149,7 +149,7 @@ def test_getters(self): cc.get_endpoint('compute')) self.assertEqual(None, cc.get_endpoint('image')) - self.assertEqual(None, cc.get_service_name('compute')) + self.assertIsNone(cc.get_service_name('compute')) self.assertEqual('locks', cc.get_service_name('identity')) def test_volume_override(self): From 86814859d0dd8a90f8a8d537b3d74b234a0bc639 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 17 Dec 2015 13:37:34 -0500 Subject: [PATCH 0732/3836] Add support for querying role assignments. When you are granting or revoking roles, you need to know what the role assignments are. Also, the return dict for role_assignments is exceptionally hard to query, so turn it in to a structure that makes some sanity. Note that role assignments don't lend themselves to use our list/search/get standard very well. Assignments are not unique (an assignment has neither a name nor an ID). Searching is supported via the filter push-down to the keystone client itself. This replaces: I963b7a07cb711d3f790715a1fe55b16d0200073d Change-Id: I5fb96f32a2f1a5f0408938105750afcd7c5c93af Co-Authored-By: Monty Taylor --- shade/_tasks.py | 5 +++ shade/_utils.py | 50 +++++++++++++++++++++++++ shade/operatorcloud.py | 37 ++++++++++++++++++ shade/tests/functional/test_identity.py | 10 +++++ shade/tests/unit/test_identity_roles.py | 43 +++++++++++++++++++++ 5 files changed, 145 insertions(+) diff --git a/shade/_tasks.py b/shade/_tasks.py index 121605747..dffda0042 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -677,6 +677,11 @@ def main(self, client): return client.keystone_client.roles.delete(**self.args) +class RoleAssignmentList(task_manager.Task): + def main(self, client): + return client.keystone_client.role_assignments.list(**self.args) + + class StackList(task_manager.Task): def main(self, client): return client.heat_client.stacks.list() diff --git a/shade/_utils.py b/shade/_utils.py index ef436630b..f8fa5a58c 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -363,6 +363,56 @@ def normalize_groups(domains): return meta.obj_list_to_dict(ret) +def normalize_role_assignments(assignments): + """Put role_assignments into a form that works with search/get interface. + + Role assignments have the structure:: + + [ + { + "role": { + "id": "--role-id--" + }, + "scope": { + "domain": { + "id": "--domain-id--" + } + }, + "user": { + "id": "--user-id--" + } + }, + ] + + Which is hard to work with in the rest of our interface. Map this to be:: + + [ + { + "id": "--role-id--", + "domain": "--domain-id--", + "user": "--user-id--", + } + ] + + Scope can be "domain" or "project" and "user" can also be "group". + + :param list assignments: A list of dictionaries of role assignments. + + :returns: A list of flattened/normalized role assignment dicts. + """ + new_assignments = [] + for assignment in assignments: + new_val = {'id': assignment['role']['id']} + for scope in ('project', 'domain'): + if scope in assignment['scope']: + new_val[scope] = assignment['scope'][scope]['id'] + for assignee in ('user', 'group'): + if assignee in assignment: + new_val[assignee] = assignment[assignee]['id'] + new_assignments.append(new_val) + return new_assignments + + def valid_kwargs(*valid_args): # This decorator checks if argument passed as **kwargs to a function are # present in valid_args. diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 5c9c7d483..c50202424 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1286,6 +1286,43 @@ def get_role(self, name_or_id, filters=None): """ return _utils._get_entity(self.search_roles, name_or_id, filters) + def list_role_assignments(self, filters=None): + """List Keystone role assignments + + :param dict filters: Dict of filter conditions. Acceptable keys are:: + + - 'user' (string) - User ID to be used as query filter. + - 'group' (string) - Group ID to be used as query filter. + - 'project' (string) - Project ID to be used as query filter. + - 'domain' (string) - Domain ID to be used as query filter. + - 'role' (string) - Role ID to be used as query filter. + - 'os_inherit_extension_inherited_to' (string) - Return inherited + role assignments for either 'projects' or 'domains' + - 'effective' (boolean) - Return effective role assignments. + - 'include_subtree' (boolean) - Include subtree + + 'user' and 'group' are mutually exclusive, as are 'domain' and + 'project'. + + :returns: a list of dicts containing the role assignment description. + Contains the following attributes:: + + - id: + - user|group: + - project|domain: + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + if not filters: + filters = {} + + with _utils.shade_exceptions("Failed to list role assignments"): + assignments = self.manager.submitTask( + _tasks.RoleAssignmentList(**filters) + ) + return _utils.normalize_role_assignments(assignments) + def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", ephemeral=0, swap=0, rxtx_factor=1.0, is_public=True): """Create a new flavor. diff --git a/shade/tests/functional/test_identity.py b/shade/tests/functional/test_identity.py index b3b31b272..d50be33af 100644 --- a/shade/tests/functional/test_identity.py +++ b/shade/tests/functional/test_identity.py @@ -79,3 +79,13 @@ def test_delete_role(self): role = self.cloud.create_role(role_name) self.assertIsNotNone(role) self.assertTrue(self.cloud.delete_role(role_name)) + + # TODO(Shrews): Once we can support assigning roles within shade, we + # need to make this test a little more specific, and add more for testing + # filtering functionality. + def test_list_role_assignments(self): + if self.cloud.cloud_config.get_api_version('identity') in ('2', '2.0'): + self.skipTest("Identity service does not support role assignments") + assignments = self.cloud.list_role_assignments() + self.assertIsInstance(assignments, list) + self.assertTrue(len(assignments) > 0) diff --git a/shade/tests/unit/test_identity_roles.py b/shade/tests/unit/test_identity_roles.py index 4490d6e51..dad7a1678 100644 --- a/shade/tests/unit/test_identity_roles.py +++ b/shade/tests/unit/test_identity_roles.py @@ -12,13 +12,31 @@ # limitations under the License. import mock +import testtools import shade from shade import meta +from shade import _utils from shade.tests.unit import base from shade.tests import fakes +RAW_ROLE_ASSIGNMENTS = [ + { + "links": {"assignment": "http://example"}, + "role": {"id": "123456"}, + "scope": {"domain": {"id": "161718"}}, + "user": {"id": "313233"} + }, + { + "links": {"assignment": "http://example"}, + "group": {"id": "101112"}, + "role": {"id": "123456"}, + "scope": {"project": {"id": "456789"}} + } +] + + class TestIdentityRoles(base.TestCase): def setUp(self): @@ -63,3 +81,28 @@ def test_delete_role(self, mock_keystone, mock_get): mock_get.return_value = meta.obj_to_dict(role_obj) self.assertTrue(self.cloud.delete_role('1234')) self.assertTrue(mock_keystone.roles.delete.called) + + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_list_role_assignments(self, mock_keystone): + mock_keystone.role_assignments.list.return_value = RAW_ROLE_ASSIGNMENTS + ret = self.cloud.list_role_assignments() + mock_keystone.role_assignments.list.assert_called_once_with() + normalized_assignments = _utils.normalize_role_assignments( + RAW_ROLE_ASSIGNMENTS + ) + self.assertEqual(normalized_assignments, ret) + + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_list_role_assignments_filters(self, mock_keystone): + params = dict(user='123', domain='456', effective=True) + self.cloud.list_role_assignments(filters=params) + mock_keystone.role_assignments.list.assert_called_once_with(**params) + + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_list_role_assignments_exception(self, mock_keystone): + mock_keystone.role_assignments.list.side_effect = Exception() + with testtools.ExpectedException( + shade.OpenStackCloudException, + "Failed to list role assignments" + ): + self.cloud.list_role_assignments() From 22d740b7007e1182c99370cb2629322384b17a14 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 12 Dec 2015 10:53:53 -0500 Subject: [PATCH 0733/3836] Add backwards compat mapping for auth-token novaclient accepted an auth-token argument, which also triggered a token not password based workflow. That's fine - let's map that to token, and if we find it, change auth_type's default from password to token. Change-Id: Ie9acece5cb3c68560ae975bfb0fb2393381b6fba --- os_client_config/config.py | 15 ++++++++++++++ os_client_config/tests/test_config.py | 30 +++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/os_client_config/config.py b/os_client_config/config.py index ab3a003c6..48bcb0f31 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -467,6 +467,7 @@ def _fix_backwards_project(self, cloud): 'project_domain_id': ('project_domain_id', 'project-domain-id'), 'project_domain_name': ( 'project_domain_name', 'project-domain-name'), + 'token': ('auth-token', 'auth_token', 'token'), } for target_key, possible_values in mappings.items(): target = None @@ -535,6 +536,13 @@ def register_argparse_arguments(self, parser, argv, service_keys=[]): # for from the user passing it explicitly. We'll stash it for later local_parser.add_argument('--timeout', metavar='') + # We need for get_one_cloud to be able to peek at whether a token + # was passed so that we can swap the default from password to + # token if it was. And we need to also peek for --os-auth-token + # for novaclient backwards compat + local_parser.add_argument('--os-token') + local_parser.add_argument('--os-auth-token') + # Peek into the future and see if we have an auth-type set in # config AND a cloud set, so that we know which command line # arguments to register and show to the user (the user may want @@ -832,6 +840,13 @@ def get_one_cloud(self, cloud=None, validate=True, else: config[key] = val + # Infer token plugin if a token was given + if (('auth' in config and 'token' in config['auth']) or + ('auth_token' in config and config['auth_token']) or + ('token' in config and config['token'])): + config['auth_type'] = 'token' + config.setdefault('token', config.pop('auth_token', None)) + # These backwards compat values are only set via argparse. If it's # there, it's because it was passed in explicitly, and should win config = self._fix_backwards_api_timeout(config) diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 98aaf79fc..bb4569306 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -243,7 +243,9 @@ def setUp(self): project_name='project', region_name='region2', snack_type='cookie', + os_auth_token='no-good-things', ) + self.options = argparse.Namespace(**self.args) def test_get_one_cloud_bad_region_argparse(self): @@ -401,6 +403,34 @@ def test_register_argparse_cloud(self): opts, _remain = parser.parse_known_args(['--os-cloud', 'foo']) self.assertEqual(opts.os_cloud, 'foo') + def test_argparse_default_no_token(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + + parser = argparse.ArgumentParser() + c.register_argparse_arguments(parser, []) + # novaclient will add this + parser.add_argument('--os-auth-token') + opts, _remain = parser.parse_known_args() + cc = c.get_one_cloud( + cloud='_test_cloud_regions', argparse=opts) + self.assertEqual(cc.config['auth_type'], 'password') + self.assertNotIn('token', cc.config['auth']) + + def test_argparse_token(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + + parser = argparse.ArgumentParser() + c.register_argparse_arguments(parser, []) + # novaclient will add this + parser.add_argument('--os-auth-token') + opts, _remain = parser.parse_known_args( + ['--os-auth-token', 'very-bad-things']) + cc = c.get_one_cloud(argparse=opts) + self.assertEqual(cc.config['auth_type'], 'token') + self.assertEqual(cc.config['auth']['token'], 'very-bad-things') + def test_register_argparse_bad_plugin(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) From 3a34378f712abee2d525973815a99188c598d726 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 12 Dec 2015 13:03:07 -0500 Subject: [PATCH 0734/3836] Support backwards compat for _ args Instead of putting tons of hidden options to allow for variations of argparse options with _ in them, just manipulate the argv when it's passed in to translate to - instead. (why the heck does argparse not already do this?) Change-Id: I5f0bd9d9a333781ad13d531b3667fff5fdac9eac --- os_client_config/config.py | 32 +++++++++++++++++++++++++++ os_client_config/tests/test_config.py | 31 ++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/os_client_config/config.py b/os_client_config/config.py index 48bcb0f31..077c109fe 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -15,6 +15,7 @@ # alias because we already had an option named argparse import argparse as argparse_mod +import collections import copy import json import os @@ -136,6 +137,34 @@ def _auth_update(old_dict, new_dict_source): return old_dict +def _fix_argv(argv): + # Transform any _ characters in arg names to - so that we don't + # have to throw billions of compat argparse arguments around all + # over the place. + processed = collections.defaultdict(list) + for index in range(0, len(argv)): + if argv[index].startswith('--'): + split_args = argv[index].split('=') + orig = split_args[0] + new = orig.replace('_', '-') + if orig != new: + split_args[0] = new + argv[index] = "=".join(split_args) + # Save both for later so we can throw an error about dupes + processed[new].append(orig) + overlap = [] + for new, old in processed.items(): + if len(old) > 1: + overlap.extend(old) + if overlap: + raise exceptions.OpenStackConfigException( + "The following options were given: '{options}' which contain" + " duplicates except that one has _ and one has -. There is" + " no sane way for us to know what you're doing. Remove the" + " duplicate option and try again".format( + options=','.join(overlap))) + + class OpenStackConfig(object): def __init__(self, config_files=None, vendor_files=None, @@ -521,6 +550,9 @@ def register_argparse_arguments(self, parser, argv, service_keys=[]): is requested """ + # Fix argv in place - mapping any keys with embedded _ in them to - + _fix_argv(argv) + local_parser = argparse_mod.ArgumentParser(add_help=False) for p in (parser, local_parser): diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index bb4569306..30ed73188 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -431,6 +431,37 @@ def test_argparse_token(self): self.assertEqual(cc.config['auth_type'], 'token') self.assertEqual(cc.config['auth']['token'], 'very-bad-things') + def test_argparse_underscores(self): + c = config.OpenStackConfig(config_files=[self.no_yaml], + vendor_files=[self.no_yaml], + secure_files=[self.no_yaml]) + parser = argparse.ArgumentParser() + parser.add_argument('--os_username') + argv = [ + '--os_username', 'user', '--os_password', 'pass', + '--os-auth-url', 'auth-url', '--os-project-name', 'project'] + c.register_argparse_arguments(parser, argv=argv) + opts, _remain = parser.parse_known_args(argv) + cc = c.get_one_cloud(argparse=opts) + self.assertEqual(cc.config['auth']['username'], 'user') + self.assertEqual(cc.config['auth']['password'], 'pass') + self.assertEqual(cc.config['auth']['auth_url'], 'auth-url') + + def test_argparse_underscores_duplicate(self): + c = config.OpenStackConfig(config_files=[self.no_yaml], + vendor_files=[self.no_yaml], + secure_files=[self.no_yaml]) + parser = argparse.ArgumentParser() + parser.add_argument('--os_username') + argv = [ + '--os_username', 'user', '--os_password', 'pass', + '--os-username', 'user1', '--os-password', 'pass1', + '--os-auth-url', 'auth-url', '--os-project-name', 'project'] + self.assertRaises( + exceptions.OpenStackConfigException, + c.register_argparse_arguments, + parser=parser, argv=argv) + def test_register_argparse_bad_plugin(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) From 16166c03c27fe73896a1717ad9a145a466bc0afd Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 12 Dec 2015 13:03:41 -0500 Subject: [PATCH 0735/3836] Pass endpoint override to constructors Also, the variable name from keystoneauth is "*-endpoint-override" ... so we need to respond to that. Respond to the old -endpoint for compat reasons. Then let's actually pass in the value. Change-Id: I6f413b02e0d2b167a4ee30494b2c91c67124b219 --- os_client_config/cloud_config.py | 6 ++++-- os_client_config/tests/test_cloud_config.py | 22 ++++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 6b3b5d9a0..2f3c94ab2 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -164,8 +164,9 @@ def get_service_name(self, service_type): return self.config.get(key, None) def get_endpoint(self, service_type): - key = _make_key('endpoint', service_type) - return self.config.get(key, None) + key = _make_key('endpoint_override', service_type) + old_key = _make_key('endpoint', service_type) + return self.config.get(key, self.config.get(old_key, None)) @property def prefer_ipv6(self): @@ -310,6 +311,7 @@ def get_legacy_client( session=self.get_session(), service_name=self.get_service_name(service_key), service_type=self.get_service_type(service_key), + endpoint_override=self.get_endpoint(service_key), region_name=self.region) if service_key == 'image': diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index 322973ad7..341225482 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -25,8 +25,9 @@ fake_config_dict = {'a': 1, 'os_b': 2, 'c': 3, 'os_c': 4} fake_services_dict = { 'compute_api_version': '2', - 'compute_endpoint': 'http://compute.example.com', + 'compute_endpoint_override': 'http://compute.example.com', 'compute_region_name': 'region-bl', + 'telemetry_endpoint': 'http://telemetry.example.com', 'interface': 'public', 'image_service_type': 'mage', 'identity_interface': 'admin', @@ -189,14 +190,24 @@ def test_get_session_with_timeout(self, mock_session): verify=True, cert=None, timeout=9) @mock.patch.object(ksa_session, 'Session') - def test_override_session_endpoint(self, mock_session): + def test_override_session_endpoint_override(self, mock_session): config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) cc = cloud_config.CloudConfig( "test1", "region-al", config_dict, auth_plugin=mock.Mock()) self.assertEqual( cc.get_session_endpoint('compute'), - fake_services_dict['compute_endpoint']) + fake_services_dict['compute_endpoint_override']) + + @mock.patch.object(ksa_session, 'Session') + def test_override_session_endpoint(self, mock_session): + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + cc = cloud_config.CloudConfig( + "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + self.assertEqual( + cc.get_session_endpoint('telemetry'), + fake_services_dict['telemetry_endpoint']) @mock.patch.object(cloud_config.CloudConfig, 'get_session') def test_session_endpoint_identity(self, mock_get_session): @@ -297,6 +308,7 @@ def test_legacy_client_image(self, mock_get_session_endpoint): '2', service_name=None, endpoint='http://example.com', + endpoint_override=None, region_name='region-al', interface='public', session=mock.ANY, @@ -316,6 +328,7 @@ def test_legacy_client_network(self, mock_get_session_endpoint): mock_client.assert_called_with( '2.0', endpoint_type='public', + endpoint_override=None, region_name='region-al', service_type='network', session=mock.ANY, @@ -333,6 +346,7 @@ def test_legacy_client_compute(self, mock_get_session_endpoint): mock_client.assert_called_with( '2', endpoint_type='public', + endpoint_override='http://compute.example.com', region_name='region-al', service_type='compute', session=mock.ANY, @@ -351,6 +365,7 @@ def test_legacy_client_identity(self, mock_get_session_endpoint): '2.0', endpoint='http://example.com/v2', endpoint_type='admin', + endpoint_override=None, region_name='region-al', service_type='identity', session=mock.ANY, @@ -370,6 +385,7 @@ def test_legacy_client_identity_v3(self, mock_get_session_endpoint): '3', endpoint='http://example.com', endpoint_type='admin', + endpoint_override=None, region_name='region-al', service_type='identity', session=mock.ANY, From 0a25cb5c5059fc8f850e27a984613515ae76ee11 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 12 Dec 2015 17:26:09 -0500 Subject: [PATCH 0736/3836] Allow passing in explicit version for legacy_client Nova (and indeed other clients with microversions, need a user to be able to request an explicit version. Change-Id: I5f67b7fc007b7d6123f621c5943345f88db1f84b --- os_client_config/cloud_config.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 2f3c94ab2..0233eff81 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -255,7 +255,7 @@ def get_session_endpoint(self, service_key): def get_legacy_client( self, service_key, client_class=None, interface_key=None, - pass_version_arg=True, **kwargs): + pass_version_arg=True, version=None, **kwargs): """Return a legacy OpenStack client object for the given config. Most of the OpenStack python-*client libraries have the same @@ -287,6 +287,8 @@ def get_legacy_client( already understand that this is the case for network, so it can be omitted in that case. + :param version: (optional) Version string to override the configured + version string. :param kwargs: (optional) keyword args are passed through to the Client constructor, so this is in case anything additional needs to be passed in. @@ -320,13 +322,14 @@ def get_legacy_client( # would need to do if they were requesting 'image' - then # they necessarily have glanceclient installed from glanceclient.common import utils as glance_utils - endpoint, version = glance_utils.strip_version(endpoint) + endpoint, _ = glance_utils.strip_version(endpoint) constructor_kwargs['endpoint'] = endpoint constructor_kwargs.update(kwargs) constructor_kwargs[interface_key] = interface constructor_args = [] if pass_version_arg: - version = self.get_api_version(service_key) + if not version: + version = self.get_api_version(service_key) # Temporary workaround while we wait for python-openstackclient # to be able to handle 2.0 which is what neutronclient expects if service_key == 'network' and version == '2': From 939862e55e42c5fafee9c2fec42b5f5fde8fc205 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 21 Dec 2015 11:35:56 -0600 Subject: [PATCH 0737/3836] Fix glance endpoints with endpoint_override Now that we properly pass endpoint_override all the time, we broke glance. The reason for this is that we calculate the glance url via glance url stripping in all cases, so the case where we did not have a configured endpoint override was passing the wrong information to the constructor, causing double version addition. Change-Id: I5699b0581d0cb68fed68800c29c8a847e2606ec9 --- os_client_config/cloud_config.py | 15 +++- os_client_config/tests/test_cloud_config.py | 92 ++++++++++++++++++++- 2 files changed, 101 insertions(+), 6 deletions(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 0233eff81..f73da04ed 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -302,6 +302,7 @@ def get_legacy_client( interface = self.get_interface(service_key) # trigger exception on lack of service endpoint = self.get_session_endpoint(service_key) + endpoint_override = self.get_endpoint(service_key) if not interface_key: if service_key == 'image': @@ -313,7 +314,7 @@ def get_legacy_client( session=self.get_session(), service_name=self.get_service_name(service_key), service_type=self.get_service_type(service_key), - endpoint_override=self.get_endpoint(service_key), + endpoint_override=endpoint_override, region_name=self.region) if service_key == 'image': @@ -322,8 +323,16 @@ def get_legacy_client( # would need to do if they were requesting 'image' - then # they necessarily have glanceclient installed from glanceclient.common import utils as glance_utils - endpoint, _ = glance_utils.strip_version(endpoint) - constructor_kwargs['endpoint'] = endpoint + endpoint, detected_version = glance_utils.strip_version(endpoint) + # If the user has passed in a version, that's explicit, use it + if not version: + version = detected_version + # If the user has passed in or configured an override, use it. + # Otherwise, ALWAYS pass in an endpoint_override becuase + # we've already done version stripping, so we don't want version + # reconstruction to happen twice + if not endpoint_override: + constructor_kwargs['endpoint_override'] = endpoint constructor_kwargs.update(kwargs) constructor_kwargs[interface_key] = interface constructor_args = [] diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index 341225482..01581b1fb 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -305,10 +305,96 @@ def test_legacy_client_image(self, mock_get_session_endpoint): "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('image', mock_client) mock_client.assert_called_with( - '2', + 2.0, service_name=None, - endpoint='http://example.com', - endpoint_override=None, + endpoint_override='http://example.com', + region_name='region-al', + interface='public', + session=mock.ANY, + # Not a typo - the config dict above overrides this + service_type='mage' + ) + + @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + def test_legacy_client_image_override(self, mock_get_session_endpoint): + mock_client = mock.Mock() + mock_get_session_endpoint.return_value = 'http://example.com/v2' + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + config_dict['image_endpoint_override'] = 'http://example.com/override' + cc = cloud_config.CloudConfig( + "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + cc.get_legacy_client('image', mock_client) + mock_client.assert_called_with( + 2.0, + service_name=None, + endpoint_override='http://example.com/override', + region_name='region-al', + interface='public', + session=mock.ANY, + # Not a typo - the config dict above overrides this + service_type='mage' + ) + + @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + def test_legacy_client_image_versioned(self, mock_get_session_endpoint): + mock_client = mock.Mock() + mock_get_session_endpoint.return_value = 'http://example.com/v2' + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + # v2 endpoint was passed, 1 requested in config, endpoint wins + config_dict['image_api_version'] = '1' + cc = cloud_config.CloudConfig( + "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + cc.get_legacy_client('image', mock_client) + mock_client.assert_called_with( + 2.0, + service_name=None, + endpoint_override='http://example.com', + region_name='region-al', + interface='public', + session=mock.ANY, + # Not a typo - the config dict above overrides this + service_type='mage' + ) + + @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + def test_legacy_client_image_unversioned(self, mock_get_session_endpoint): + mock_client = mock.Mock() + mock_get_session_endpoint.return_value = 'http://example.com/' + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + # Versionless endpoint, config wins + config_dict['image_api_version'] = '1' + cc = cloud_config.CloudConfig( + "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + cc.get_legacy_client('image', mock_client) + mock_client.assert_called_with( + '1', + service_name=None, + endpoint_override='http://example.com', + region_name='region-al', + interface='public', + session=mock.ANY, + # Not a typo - the config dict above overrides this + service_type='mage' + ) + + @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + def test_legacy_client_image_argument(self, mock_get_session_endpoint): + mock_client = mock.Mock() + mock_get_session_endpoint.return_value = 'http://example.com/v3' + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + # Versionless endpoint, config wins + config_dict['image_api_version'] = '6' + cc = cloud_config.CloudConfig( + "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + cc.get_legacy_client('image', mock_client, version='beef') + mock_client.assert_called_with( + 'beef', + service_name=None, + endpoint_override='http://example.com', region_name='region-al', interface='public', session=mock.ANY, From ea859622fa34c6f8e79d83aaa42d7dfb731034a7 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 21 Dec 2015 11:58:12 -0600 Subject: [PATCH 0738/3836] Add option to enable HTTP tracing During development, it's useful sometimes to see an HTTP trace log. That's controlled by keystoneauth in all cases, so it's easy to turn it on. Provide a flag for people using simple_logging. Change-Id: I2f44d6f7fd1268028eeb455341198a704edcaad4 --- shade/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index c2820c353..385cd73ac 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -32,11 +32,18 @@ 'ignore', category=requestsexceptions.SubjectAltNameWarning) -def simple_logging(debug=False): +def simple_logging(debug=False, http_debug=False): + if http_debug: + debug = True if debug: log_level = logging.DEBUG else: log_level = logging.INFO + if http_debug: + # Enable HTTP level tracing + log = _log.setup_logging('keystoneauth') + log.addHandler(logging.StreamHandler()) + log.setLevel(log_level) log = _log.setup_logging('shade') log.addHandler(logging.StreamHandler()) log.setLevel(log_level) From 95a7be3cb6448e51d9b0995a26f55df66819d9a9 Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Tue, 22 Dec 2015 10:40:01 -0800 Subject: [PATCH 0739/3836] No Mutable Defaults Do not have mutable defaults in functions/constructors/etc. This is bad news since they can be changed for the entire run of the program (and subsequent calls). Change-Id: I756629a3edca72a4c7fbe748e2f6964a7e827e72 --- shade/inventory.py | 4 +++- shade/openstackcloud.py | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/shade/inventory.py b/shade/inventory.py index 2ea9995c8..40251f9c3 100644 --- a/shade/inventory.py +++ b/shade/inventory.py @@ -21,7 +21,9 @@ class OpenStackInventory(object): def __init__( - self, config_files=[], refresh=False, private=False): + self, config_files=None, refresh=False, private=False): + if config_files is None: + config_files = [] config = os_client_config.config.OpenStackConfig( config_files=os_client_config.config.CONFIG_FILES + config_files) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index dccda532d..9b4415ab7 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3149,7 +3149,7 @@ def create_server( root_volume=None, terminate_volume=False, wait=False, timeout=180, reuse_ips=True, network=None, boot_from_volume=False, volume_size='50', - boot_volume=None, volumes=[], + boot_volume=None, volumes=None, **kwargs): """Create a virtual server instance. @@ -3226,6 +3226,10 @@ def create_server( :raises: OpenStackCloudException on operation error. """ # nova cli calls this boot_volume. Let's be the same + + if volumes is None: + volumes = [] + if root_volume and not boot_volume: boot_volume = root_volume From 8fa55700b90e335e54cd459ea8a60578e8d27fc7 Mon Sep 17 00:00:00 2001 From: Clark Boylan Date: Tue, 22 Dec 2015 12:28:20 -0800 Subject: [PATCH 0740/3836] If cloud doesn't list regions expand passed name Don't fail on a cloud not having regions when a region name is passed. Instead just use the name that is given and expand it properly. This adds test coverage for the paths through the OpenStackConfig._get_region() method to avoid problems like this in the future. In order for this work to be done cleanly a small refactor of get_regions() is done to split it into two methods, one that gets all regions with a sane fallback default (for backward compat) and another that returns only regions that are known in the config and None otherwise. This allows us to switch on whether or not there are known regions. Change-Id: I62736ea82f365badaea5016a23d37a9f1c760927 --- os_client_config/config.py | 15 +++++-- os_client_config/tests/base.py | 10 ++++- os_client_config/tests/test_config.py | 56 +++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 5 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 077c109fe..89015cc90 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -366,6 +366,13 @@ def _expand_regions(self, regions): def _get_regions(self, cloud): if cloud not in self.cloud_config['clouds']: return [self._expand_region_name('')] + regions = self._get_known_regions(cloud) + if not regions: + # We don't know of any regions use a workable default. + regions = [self._expand_region_name('')] + return regions + + def _get_known_regions(self, cloud): config = self._normalize_keys(self.cloud_config['clouds'][cloud]) if 'regions' in config: return self._expand_regions(config['regions']) @@ -386,15 +393,15 @@ def _get_regions(self, cloud): return self._expand_regions(new_cloud['regions']) elif 'region_name' in new_cloud and new_cloud['region_name']: return [self._expand_region_name(new_cloud['region_name'])] - else: - # Wow. We really tried - return [self._expand_region_name('')] def _get_region(self, cloud=None, region_name=''): if not cloud: return self._expand_region_name(region_name) - regions = self._get_regions(cloud) + regions = self._get_known_regions(cloud) + if not regions: + return self._expand_region_name(region_name) + if not region_name: return regions[0] diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py index fdc50cd0b..3f00f6d82 100644 --- a/os_client_config/tests/base.py +++ b/os_client_config/tests/base.py @@ -119,7 +119,15 @@ 'auth_url': 'http://example.com/v2', }, 'region_name': 'test-region', - } + }, + '_test-cloud_no_region': { + 'profile': '_test_cloud_in_our_cloud', + 'auth': { + 'auth_url': 'http://example.com/v2', + 'username': 'testuser', + 'password': 'testpass', + }, + }, }, 'ansible': { 'expand-hostvars': False, diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 30ed73188..3ea6690e2 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -178,6 +178,7 @@ def test_get_cloud_names(self): ['_test-cloud-domain-id_', '_test-cloud-int-project_', '_test-cloud_', + '_test-cloud_no_region', '_test_cloud_hyphenated', '_test_cloud_no_vendor', '_test_cloud_regions', @@ -230,6 +231,61 @@ def test_set_one_cloud_updates_cloud(self): written_config['cache'].pop('path', None) self.assertEqual(written_config, resulting_config) + def test_get_region_no_region_default(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + secure_files=[self.no_yaml]) + region = c._get_region(cloud='_test-cloud_no_region') + self.assertEqual(region, {'name': '', 'values': {}}) + + def test_get_region_no_region(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + secure_files=[self.no_yaml]) + region = c._get_region(cloud='_test-cloud_no_region', + region_name='override-region') + self.assertEqual(region, {'name': 'override-region', 'values': {}}) + + def test_get_region_region_set(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + secure_files=[self.no_yaml]) + region = c._get_region(cloud='_test-cloud_', region_name='test-region') + self.assertEqual(region, {'name': 'test-region', 'values': {}}) + + def test_get_region_many_regions_default(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + secure_files=[self.no_yaml]) + region = c._get_region(cloud='_test_cloud_regions', + region_name='') + self.assertEqual(region, {'name': 'region1', 'values': + {'external_network': 'region1-network'}}) + + def test_get_region_many_regions(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + secure_files=[self.no_yaml]) + region = c._get_region(cloud='_test_cloud_regions', + region_name='region2') + self.assertEqual(region, {'name': 'region2', 'values': + {'external_network': 'my-network'}}) + + def test_get_region_invalid_region(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + secure_files=[self.no_yaml]) + self.assertRaises( + exceptions.OpenStackConfigException, c._get_region, + cloud='_test_cloud_regions', region_name='invalid-region') + + def test_get_region_no_cloud(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + secure_files=[self.no_yaml]) + region = c._get_region(region_name='no-cloud-region') + self.assertEqual(region, {'name': 'no-cloud-region', 'values': {}}) + class TestConfigArgparse(base.TestCase): From c10566cc0ab0a9415d650a93941deb9b62ecdaf7 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 22 Dec 2015 17:12:30 -0500 Subject: [PATCH 0741/3836] Fix shade tests with OCC 1.13.0 The latest os-client-config, 1.13.0, breaks the shade tests as the client instantiations are slightly different. Since I'm not sure we can modify our tests to use against different OCC versions (there doesn't appear to be a version attribute anywhere), I upped the minimum requirements.txt value as well as fixed the tests for the latest version. Change-Id: I18292ca39789c41caea74901754d66d3301bb420 --- requirements.txt | 2 +- shade/tests/unit/test_shade.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8d6eaf752..47db714ea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ munch decorator jsonpatch ipaddress -os-client-config>=1.11.1 +os-client-config>=1.13.0 requestsexceptions>=1.1.1 six diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index a6c8bdb8f..402cefb24 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -80,8 +80,8 @@ def test_glance_args( get_session_mock.return_value = session_mock self.cloud.glance_client mock_client.assert_called_with( - '2', - endpoint='http://example.com', + 2.0, + endpoint_override='http://example.com', region_name='', service_name=None, interface='public', service_type='image', session=mock.ANY, @@ -95,6 +95,7 @@ def test_heat_args(self, mock_client, get_session_mock): self.cloud.heat_client mock_client.assert_called_with( '1', + endpoint_override=None, endpoint_type='public', region_name='', service_name=None, From f765a16c6c84d8b4a116585676f46087e458987b Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Wed, 23 Dec 2015 01:31:13 +0000 Subject: [PATCH 0742/3836] remove python 2.6 os-client-config classifier OpenStack projects are no longer being tested under Python 2.6, so remove the classifier implying that this project supports 2.6. Change-Id: Ic24f93d5f7e7ffb1eaf91617c09cc897163e88df --- setup.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index bc4f128cf..89df35c11 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,7 +15,6 @@ classifier = Programming Language :: Python Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 - Programming Language :: Python :: 2.6 Programming Language :: Python :: 3 Programming Language :: Python :: 3.3 Programming Language :: Python :: 3.4 From 77c0365ce2d42adce9352cf238fbdbc7c282222f Mon Sep 17 00:00:00 2001 From: Javier Pena Date: Wed, 23 Dec 2015 11:38:40 +0100 Subject: [PATCH 0743/3836] Fix token_endpoint usage Commit 22d740b7007e1182c99370cb2629322384b17a14 broke token_endpoint authentication for openstackclient, by unconditionally setting auth_type to 'token' whenever a token was passed in the command line. This change reverts the portion that always overrides the auth plugin if there is a token passed via arguments. Change-Id: I835c3716dd08eaca10f56682c22fdc6ac700e0fe --- os_client_config/config.py | 1 - os_client_config/tests/test_config.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 89015cc90..51019837e 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -883,7 +883,6 @@ def get_one_cloud(self, cloud=None, validate=True, if (('auth' in config and 'token' in config['auth']) or ('auth_token' in config and config['auth_token']) or ('token' in config and config['token'])): - config['auth_type'] = 'token' config.setdefault('token', config.pop('auth_token', None)) # These backwards compat values are only set via argparse. If it's diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 3ea6690e2..aef97371a 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -482,7 +482,8 @@ def test_argparse_token(self): # novaclient will add this parser.add_argument('--os-auth-token') opts, _remain = parser.parse_known_args( - ['--os-auth-token', 'very-bad-things']) + ['--os-auth-token', 'very-bad-things', + '--os-auth-type', 'token']) cc = c.get_one_cloud(argparse=opts) self.assertEqual(cc.config['auth_type'], 'token') self.assertEqual(cc.config['auth']['token'], 'very-bad-things') From 7be6db82d35a7c3402172e742bb19dfa5a95a472 Mon Sep 17 00:00:00 2001 From: Tim Burke Date: Mon, 28 Dec 2015 16:43:26 -0800 Subject: [PATCH 0744/3836] Fix some README typos Change-Id: I3ebec661d1b02da0c940cde63ab862871dca11c5 --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index ced3b1821..811c2d3e6 100644 --- a/README.rst +++ b/README.rst @@ -117,7 +117,7 @@ An example config file is probably helpful: - IAD You may note a few things. First, since `auth_url` settings are silly -and embarrasingly ugly, known cloud vendor profile information is included and +and embarrassingly ugly, known cloud vendor profile information is included and may be referenced by name. One of the benefits of that is that `auth_url` isn't the only thing the vendor defaults contain. For instance, since Rackspace lists `rax:database` as the service type for trove, `os-client-config` @@ -148,8 +148,8 @@ related to domains, projects and trusts. Splitting Secrets ----------------- -In some scenarios, such as configuragtion managment controlled environments, -it might be eaiser to have secrets in one file and non-secrets in another. +In some scenarios, such as configuration management controlled environments, +it might be easier to have secrets in one file and non-secrets in another. This is fully supported via an optional file `secure.yaml` which follows all the same location rules as `clouds.yaml`. It can contain anything you put in `clouds.yaml` and will take precedence over anything in the `clouds.yaml` @@ -380,4 +380,4 @@ If you want to do the same thing but also support command line parsing. 'compute', options=argparse.ArgumentParser()) If you want to get fancier than that in your python, then the rest of the -API is avaiable to you. But often times, you just want to do the one thing. +API is available to you. But often times, you just want to do the one thing. From 17e019a08e6e8fed7da6d0de403e5525d997095b Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Tue, 29 Dec 2015 15:22:56 -0800 Subject: [PATCH 0745/3836] Munge region_name to '' if set to None The openstack ansible module defaults to setting region_name to None[1]. With region_name explicitly set, _get_region won't use '' as a default and therefor has unexpected behavior if the user does not set the region explicitly. This is apparent in bifrost[2] which does not use any cloud config file and does not set the region explicitly. This patch checks whether None was passed in as the region name and sets it to '' so that it can continue processing it as though it was not set. [1] https://github.com/ansible/ansible/blob/devel/lib/ansible/module_utils/openstack.py#L41 [2] http://paste.openstack.org/show/482831/ Change-Id: I22cce104930f74dd479e704cc1a941dc945b75de --- os_client_config/config.py | 2 ++ os_client_config/tests/test_config.py | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/os_client_config/config.py b/os_client_config/config.py index 89015cc90..d5b1ab552 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -395,6 +395,8 @@ def _get_known_regions(self, cloud): return [self._expand_region_name(new_cloud['region_name'])] def _get_region(self, cloud=None, region_name=''): + if region_name is None: + region_name = '' if not cloud: return self._expand_region_name(region_name) diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 3ea6690e2..b2ee9bba8 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -246,6 +246,13 @@ def test_get_region_no_region(self): region_name='override-region') self.assertEqual(region, {'name': 'override-region', 'values': {}}) + def test_get_region_region_is_none(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + secure_files=[self.no_yaml]) + region = c._get_region(cloud='_test-cloud_no_region', region_name=None) + self.assertEqual(region, {'name': '', 'values': {}}) + def test_get_region_region_set(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml], From 7ee7156254381dc5c06405105c7de42c180c779f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 30 Dec 2015 09:46:21 -0600 Subject: [PATCH 0746/3836] Allow filtering clouds on command line Add a very basic filtering to the test command line function to allow only printing one cloud or one cloud/region worth of config. Change-Id: I0d09717430f41b4229f7743f8531f871b962969e --- os_client_config/config.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 077c109fe..b970728e0 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -19,6 +19,7 @@ import copy import json import os +import sys import warnings import appdirs @@ -976,4 +977,15 @@ def set_one_cloud(config_file, cloud, set_config=None): if __name__ == '__main__': config = OpenStackConfig().get_all_clouds() for cloud in config: - print(cloud.name, cloud.region, cloud.config) + print_cloud = False + if len(sys.argv) == 1: + print_cloud = True + elif len(sys.argv) == 3 and ( + sys.argv[1] == cloud.name and sys.argv[2] == cloud.region): + print_cloud = True + elif len(sys.argv) == 2 and ( + sys.argv[1] == cloud.name): + print_cloud = True + + if print_cloud: + print(cloud.name, cloud.region, cloud.config) From a39fca8e53e7de0c4ffdeafce40902e0e6fc7073 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 25 Dec 2015 07:56:03 -0600 Subject: [PATCH 0747/3836] Fix unittest stack status If you run unittests with a clouds.yaml available that has caching enbaled, the heat test fails because the fake has an invalid value for stack_status (it'll always be a string) and also the pending function looks for the wrong name. We should isolate unittests from calling context clouds.yaml better, but this is a bug anyway. Change-Id: I791df68163b9ce850df6c74eacadd91ff76348ed --- shade/openstackcloud.py | 2 +- shade/tests/fakes.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 9b4415ab7..aaee1b180 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -81,7 +81,7 @@ def _no_pending_images(images): def _no_pending_stacks(stacks): '''If there are any stacks not in a steady state, don't cache''' for stack in stacks: - status = stack['status'] + status = stack['stack_status'] if '_COMPLETE' not in status and '_FAILED' not in status: return False return True diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 94a5e45f1..d20d19e50 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -202,7 +202,7 @@ def __init__(self, id, hostname): class FakeStack(object): - def __init__(self, id, name, description=None, status=None): + def __init__(self, id, name, description=None, status='CREATE_COMPLETE'): self.id = id self.stack_name = name self.stack_description = description From 9929eb3523c964f53a70592136627560923d2990 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 14 Dec 2015 10:16:08 -0500 Subject: [PATCH 0748/3836] Have inventory use os-client-config extra_config There are some additional inventory behaviors that need to be configured in clouds.yaml, which means we need to allow them to be requested by the inventory class. Change-Id: I6f09d73422163e54776d03ead1d83322b6fdccf5 --- shade/inventory.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/shade/inventory.py b/shade/inventory.py index 40251f9c3..52dcde175 100644 --- a/shade/inventory.py +++ b/shade/inventory.py @@ -20,12 +20,18 @@ class OpenStackInventory(object): + # Put this here so the capability can be detected with hasattr on the class + extra_config = None + def __init__( - self, config_files=None, refresh=False, private=False): + self, config_files=None, refresh=False, private=False, + config_key=None, config_defaults=None): if config_files is None: config_files = [] config = os_client_config.config.OpenStackConfig( config_files=os_client_config.config.CONFIG_FILES + config_files) + self.extra_config = config.get_extra_config( + config_key, config_defaults) self.clouds = [ shade.OpenStackCloud(cloud_config=cloud_config) From f3678f03deac0230e1265a8a516a8eea11d301cf Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Wed, 30 Dec 2015 19:06:38 +0000 Subject: [PATCH 0749/3836] add URLs for release announcement tools The release announcement scripts expects to find URLs for the bug tracker, documentation, etc. by looking for patterns in the README.rst file. This change adds the URLs in a format consistent with other OpenStack projects and that works with the release announcement generator. Change-Id: I88151008cca91da3fed7e4c0ec6dfb641a0062b6 --- README.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.rst b/README.rst index 811c2d3e6..5cbc118e5 100644 --- a/README.rst +++ b/README.rst @@ -381,3 +381,11 @@ If you want to do the same thing but also support command line parsing. If you want to get fancier than that in your python, then the rest of the API is available to you. But often times, you just want to do the one thing. + +Source +------ + +* Free software: Apache license +* Documentation: http://docs.openstack.org/developer/os-client-config +* Source: http://git.openstack.org/cgit/openstack/os-client-config +* Bugs: http://bugs.launchpad.net/os-client-config From 594e31a4c262c9ae3fe14e2e4c0fdb71a0df0747 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 30 Dec 2015 13:10:47 -0600 Subject: [PATCH 0750/3836] Use reno for release notes The OpenStack Release team has created a great release notes management tool that integrates with Sphinx. Start using it. For reference on how to use it, see http://docs.openstack.org/developer/reno/ Change-Id: I8153ec7861b508297a28a1916771776dee2deafe --- doc/source/conf.py | 3 ++- doc/source/index.rst | 1 + doc/source/releasenotes.rst | 5 +++++ releasenotes/notes/started-using-reno-242e2b0cd27f9480.yaml | 3 +++ test-requirements.txt | 1 + 5 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 doc/source/releasenotes.rst create mode 100644 releasenotes/notes/started-using-reno-242e2b0cd27f9480.yaml diff --git a/doc/source/conf.py b/doc/source/conf.py index 221de3c88..208517c86 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -23,7 +23,8 @@ extensions = [ 'sphinx.ext.autodoc', #'sphinx.ext.intersphinx', - 'oslosphinx' + 'oslosphinx', + 'reno.sphinxext' ] # autodoc generation is a bit aggressive and a nuisance when doing heavy diff --git a/doc/source/index.rst b/doc/source/index.rst index cc5dbf470..bf667b786 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -7,6 +7,7 @@ contributing installation api-reference + releasenotes Indices and tables ================== diff --git a/doc/source/releasenotes.rst b/doc/source/releasenotes.rst new file mode 100644 index 000000000..2a4bceb4e --- /dev/null +++ b/doc/source/releasenotes.rst @@ -0,0 +1,5 @@ +============= +Release Notes +============= + +.. release-notes:: diff --git a/releasenotes/notes/started-using-reno-242e2b0cd27f9480.yaml b/releasenotes/notes/started-using-reno-242e2b0cd27f9480.yaml new file mode 100644 index 000000000..d7cfb5145 --- /dev/null +++ b/releasenotes/notes/started-using-reno-242e2b0cd27f9480.yaml @@ -0,0 +1,3 @@ +--- +other: +- Started using reno for release notes. diff --git a/test-requirements.txt b/test-requirements.txt index 70530517e..a50a202e3 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -16,6 +16,7 @@ python-subunit>=0.0.18 sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 oslosphinx>=2.5.0,<2.6.0 # Apache-2.0 oslotest>=1.5.1,<1.6.0 # Apache-2.0 +reno>=0.1.1 # Apache2 testrepository>=0.0.18 testscenarios>=0.4 testtools>=0.9.36,!=1.2.0 From ecb537ddad9723226e198d36551a04a8247a4977 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 14 Dec 2015 10:18:18 -0500 Subject: [PATCH 0751/3836] Make server variable expansion optional Sometimes people don't want lists of volumes attached and whatnot, and with lots of hosts it can be expensive to calculate. Skip it if it's configured to not ask for it. Change-Id: Id739fb3bae35cd7a8a24ddd6b58cc9c050ddd5d5 --- shade/inventory.py | 26 ++++++++++++++------ shade/meta.py | 25 ++++++++++++++++---- shade/tests/functional/test_inventory.py | 24 +++++++++++++++++++ shade/tests/unit/test_inventory.py | 30 +++++++++++++++++++++++- 4 files changed, 93 insertions(+), 12 deletions(-) diff --git a/shade/inventory.py b/shade/inventory.py index 52dcde175..64a9cce06 100644 --- a/shade/inventory.py +++ b/shade/inventory.py @@ -12,10 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +import functools + import os_client_config import shade from shade import _utils +from shade import meta class OpenStackInventory(object): @@ -47,7 +50,7 @@ def __init__( for cloud in self.clouds: cloud._cache.invalidate() - def list_hosts(self): + def list_hosts(self, expand=True): hostvars = [] for cloud in self.clouds: @@ -55,14 +58,23 @@ def list_hosts(self): # Cycle on servers for server in cloud.list_servers(): - meta = cloud.get_openstack_vars(server) - hostvars.append(meta) + if expand: + server_vars = cloud.get_openstack_vars(server) + else: + # expand_server_vars gets renamed in a follow on + # patch which should make this a bit clearer. + server_vars = meta.expand_server_vars(cloud, server) + hostvars.append(server_vars) return hostvars - def search_hosts(self, name_or_id=None, filters=None): - hosts = self.list_hosts() + def search_hosts(self, name_or_id=None, filters=None, expand=True): + hosts = self.list_hosts(expand=expand) return _utils._filter_list(hosts, name_or_id, filters) - def get_host(self, name_or_id, filters=None): - return _utils._get_entity(self.search_hosts, name_or_id, filters) + def get_host(self, name_or_id, filters=None, expand=True): + if expand: + func = self.search_hosts + else: + func = functools.partial(self.search_hosts, expand=False) + return _utils._get_entity(func, name_or_id, filters) diff --git a/shade/meta.py b/shade/meta.py index b952b3660..ac10bc4cf 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -224,9 +224,20 @@ def get_groups_from_server(cloud, server, server_vars): def expand_server_vars(cloud, server): - """Add clean up the server dict with information that is essential.""" + """Add and clean up the server dict with information that is essential. + + This function should not make additional calls to the cloud with one + exception - which is getting external/internal IPs, which have to do + some calls to neutron in some cases to find out which IP is routable. This + is essential info as you can't otherwise connect to the server without + it. + """ server_vars = server server_vars.pop('links', None) + server_vars['flavor'].pop('links', None) + # OpenStack can return image as a string when you've booted from volume + if str(server['image']) != server['image']: + server_vars['image'].pop('links', None) # First, add an IP address. Set it to '' rather than None if it does # not exist to remain consistent with the pre-existing missing values @@ -261,6 +272,8 @@ def expand_server_vars(cloud, server): az = server_vars.get('OS-EXT-AZ:availability_zone', None) if az: server_vars['az'] = az + # Ensure volumes is always in the server dict, even if empty + server_vars['volumes'] = [] return server_vars @@ -273,14 +286,19 @@ def expand_server_security_groups(cloud, server): def get_hostvars_from_server(cloud, server, mounts=None): - """Expand additional server information useful for ansible inventory.""" + """Expand additional server information useful for ansible inventory. + + Variables in this function may make additional cloud queries to flesh out + possibly interesting info, making it more expensive to call than + expand_server_vars if caching is not set up. If caching is set up, + the extra cost should be minimal. + """ server_vars = expand_server_vars(cloud, server) flavor_id = server['flavor']['id'] flavor_name = cloud.get_flavor_name(flavor_id) if flavor_name: server_vars['flavor']['name'] = flavor_name - server_vars['flavor'].pop('links', None) expand_server_security_groups(cloud, server) @@ -294,7 +312,6 @@ def get_hostvars_from_server(cloud, server, mounts=None): image_name = cloud.get_image_name(image_id) if image_name: server_vars['image']['name'] = image_name - server_vars['image'].pop('links', None) volumes = [] if cloud.has_service('volume'): diff --git a/shade/tests/functional/test_inventory.py b/shade/tests/functional/test_inventory.py index 859fd5a7f..764613e5d 100644 --- a/shade/tests/functional/test_inventory.py +++ b/shade/tests/functional/test_inventory.py @@ -61,14 +61,38 @@ def _test_host_content(self, host): self.assertIsInstance(host['metadata'], dict) self.assertIn('interface_ip', host) + def _test_expanded_host_content(self, host): + self.assertEquals(host['image']['name'], self.image.name) + self.assertEquals(host['flavor']['name'], self.flavor.name) + def test_get_host(self): host = self.inventory.get_host(self.server_name) self.assertIsNotNone(host) self.assertEquals(host['name'], self.server_name) self._test_host_content(host) + self._test_expanded_host_content(host) host_found = False for host in self.inventory.list_hosts(): if host['name'] == self.server_name: host_found = True self._test_host_content(host) self.assertTrue(host_found) + + def test_get_host_no_detail(self): + host = self.inventory.get_host(self.server_name, expand=False) + self.assertIsNotNone(host) + self.assertEquals(host['name'], self.server_name) + + self.assertEquals(host['image']['id'], self.image.id) + self.assertNotIn('links', host['image']) + self.assertNotIn('name', host['name']) + self.assertEquals(host['flavor']['id'], self.flavor.id) + self.assertNotIn('links', host['flavor']) + self.assertNotIn('name', host['flavor']) + + host_found = False + for host in self.inventory.list_hosts(expand=False): + if host['name'] == self.server_name: + host_found = True + self._test_host_content(host) + self.assertTrue(host_found) diff --git a/shade/tests/unit/test_inventory.py b/shade/tests/unit/test_inventory.py index 751a456d1..9c0a6a441 100644 --- a/shade/tests/unit/test_inventory.py +++ b/shade/tests/unit/test_inventory.py @@ -20,12 +20,12 @@ from shade.tests.unit import base -@mock.patch("os_client_config.config.OpenStackConfig") class TestInventory(base.TestCase): def setUp(self): super(TestInventory, self).setUp() + @mock.patch("os_client_config.config.OpenStackConfig") @mock.patch("shade.OpenStackCloud") def test__init(self, mock_cloud, mock_config): mock_config.return_value.get_all_clouds.return_value = [{}] @@ -39,6 +39,7 @@ def test__init(self, mock_cloud, mock_config): self.assertEqual(1, len(inv.clouds)) self.assertTrue(mock_config.return_value.get_all_clouds.called) + @mock.patch("os_client_config.config.OpenStackConfig") @mock.patch("shade.OpenStackCloud") def test_list_hosts(self, mock_cloud, mock_config): mock_config.return_value.get_all_clouds.return_value = [{}] @@ -57,6 +58,26 @@ def test_list_hosts(self, mock_cloud, mock_config): inv.clouds[0].get_openstack_vars.assert_called_once_with(server) self.assertEqual([server], ret) + @mock.patch("os_client_config.config.OpenStackConfig") + @mock.patch("shade.meta.expand_server_vars") + @mock.patch("shade.OpenStackCloud") + def test_list_hosts_no_detail(self, mock_cloud, mock_expand, mock_config): + mock_config.return_value.get_all_clouds.return_value = [{}] + + inv = inventory.OpenStackInventory() + + server = dict(id='server_id', name='server_name') + self.assertIsInstance(inv.clouds, list) + self.assertEqual(1, len(inv.clouds)) + inv.clouds[0].list_servers.return_value = [server] + + inv.list_hosts(expand=False) + + inv.clouds[0].list_servers.assert_called_once_with() + self.assertFalse(inv.clouds[0].get_openstack_vars.called) + mock_expand.assert_called_once_with(inv.clouds[0], server) + + @mock.patch("os_client_config.config.OpenStackConfig") @mock.patch("shade.OpenStackCloud") def test_search_hosts(self, mock_cloud, mock_config): mock_config.return_value.get_all_clouds.return_value = [{}] @@ -72,6 +93,7 @@ def test_search_hosts(self, mock_cloud, mock_config): ret = inv.search_hosts('server_id') self.assertEqual([server], ret) + @mock.patch("os_client_config.config.OpenStackConfig") @mock.patch("shade.OpenStackCloud") def test_get_host(self, mock_cloud, mock_config): mock_config.return_value.get_all_clouds.return_value = [{}] @@ -86,3 +108,9 @@ def test_get_host(self, mock_cloud, mock_config): ret = inv.get_host('server_id') self.assertEqual(server, ret) + + @mock.patch("shade.inventory.OpenStackInventory.search_hosts") + def test_get_host_no_detail(self, mock_search): + inv = inventory.OpenStackInventory() + inv.get_host('server_id', expand=False) + mock_search.assert_called_once_with('server_id', None, expand=False) From 9688f8ebd1ace0f338a1eabb77e1bee249e5630b Mon Sep 17 00:00:00 2001 From: Yuriy Taraday Date: Thu, 31 Dec 2015 15:40:58 +0300 Subject: [PATCH 0752/3836] Fix README.rst, add a check for it to fit PyPI rules README.rst doesn't appear right on PyPI currently. This commit fixes the issue and expands "docs" environment in tox.ini to use readme tool [0] to verify that README.rst is good for PyPI. [0] https://github.com/pypa/readme Change-Id: I6025bb6c661d8a4a7cd9802a1298928662278f2d --- README.rst | 2 +- tox.ini | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 5cbc118e5..f078f3cee 100644 --- a/README.rst +++ b/README.rst @@ -355,7 +355,7 @@ with - as well as a consumption argument. Constructing Legacy Client objects ---------------------------------- -If all you want to do is get a Client object from a python-*client library, +If all you want to do is get a Client object from a python-\*client library, and you want it to do all the normal things related to clouds.yaml, `OS_` environment variables, a helper function is provided. The following will get you a fully configured `novaclient` instance. diff --git a/tox.ini b/tox.ini index 7a2d3a07a..95dff6ba4 100644 --- a/tox.ini +++ b/tox.ini @@ -21,7 +21,12 @@ commands = {posargs} commands = python setup.py test --coverage --coverage-package-name=os_client_config --testr-args='{posargs}' [testenv:docs] -commands = python setup.py build_sphinx +deps = + {[testenv]deps} + readme +commands = + python setup.py build_sphinx + python setup.py check -r -s [flake8] # H803 skipped on purpose per list discussion. From c514b855d1faed8947ace885bb4656da541d4d2b Mon Sep 17 00:00:00 2001 From: Doug Wiegley Date: Thu, 31 Dec 2015 12:32:37 -0700 Subject: [PATCH 0753/3836] Debug log a deferred keystone exception, else we mask some useful diag Change-Id: Ib1921698bb61f44193034065749b4e246a6258db --- os_client_config/config.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/os_client_config/config.py b/os_client_config/config.py index b57264572..d490006b4 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -26,6 +26,7 @@ from keystoneauth1 import loading import yaml +from os_client_config import _log from os_client_config import cloud_config from os_client_config import defaults from os_client_config import exceptions @@ -170,6 +171,8 @@ class OpenStackConfig(object): def __init__(self, config_files=None, vendor_files=None, override_defaults=None, force_ipv4=None, envvar_prefix=None, secure_files=None): + self.log = _log.setup_logging(__name__) + self._config_files = config_files or CONFIG_FILES self._secure_files = secure_files or SECURE_FILES self._vendor_files = vendor_files or VENDOR_FILES @@ -920,6 +923,7 @@ def get_one_cloud(self, cloud=None, validate=True, # but OSC can't handle it right now, so we try deferring # to ksc. If that ALSO fails, it means there is likely # a deeper issue, so we assume the ksa error was correct + self.log.debug("Deferring keystone exception: {e}".format(e=e)) auth_plugin = None try: config = self._validate_auth_ksc(config) From 1cd3e5bb7fd7cd72a481f5ae8bbcd0b2ab114680 Mon Sep 17 00:00:00 2001 From: Yaguang Tang Date: Sun, 27 Dec 2015 10:59:08 +0800 Subject: [PATCH 0754/3836] Update volume API default version from v1 to v2 Cinder has deprecated API version v1 since Juno release, and there is a blueprint to remove v1 API support which is in progress. We should default to v2 API when it's there. Closes-Bug: 1467589 Change-Id: I83aef4c681cbe342c445f02436fcd40cf1222f23 --- doc/source/vendor-support.rst | 11 +++++++++++ os_client_config/defaults.json | 2 +- os_client_config/vendors/bluebox.json | 1 + os_client_config/vendors/catalyst.json | 1 + os_client_config/vendors/citycloud.json | 1 + os_client_config/vendors/entercloudsuite.json | 1 + os_client_config/vendors/hp.json | 1 + os_client_config/vendors/rackspace.json | 1 + os_client_config/vendors/switchengines.json | 1 + os_client_config/vendors/ultimum.json | 1 + os_client_config/vendors/unitedstack.json | 1 + 11 files changed, 21 insertions(+), 1 deletion(-) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index d7af6b913..8ae2f6194 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -16,6 +16,7 @@ These are the default behaviors unless a cloud is configured differently. * Identity uses `password` authentication * Identity API Version is 2 * Image API Version is 2 +* Volume API Version is 2 * Images must be in `qcow2` format * Images are uploaded using PUT interface * Public IPv4 is directly routable via DHCP from Neutron @@ -51,6 +52,7 @@ nz_wlg_2 Wellington, NZ * Image API Version is 1 * Images must be in `raw` format +* Volume API Version is 1 citycloud --------- @@ -67,6 +69,7 @@ Kna1 Karlskrona, SE * Identity API Version is 3 * Public IPv4 is provided via NAT with Neutron Floating IP +* Volume API Version is 1 conoha ------ @@ -137,6 +140,8 @@ it-mil1 Milan, IT de-fra1 Frankfurt, DE ============== ================ +* Volume API Version is 1 + hp -- @@ -152,6 +157,7 @@ region-b.geo-1 US East * DNS Service Type is `hpext:dns` * Image API Version is 1 * Public IPv4 is provided via NAT with Neutron Floating IP +* Volume API Version is 1 internap -------- @@ -212,6 +218,7 @@ SYD Sydney * Uploaded Images need properties to not use vendor agent:: :vm_mode: hvm :xenapi_use_agent: False +* Volume API Version is 1 runabove -------- @@ -241,6 +248,7 @@ ZH Zurich, CH * Images must be in `raw` format * Images must be uploaded using the Glance Task Interface +* Volume API Version is 1 ultimum ------- @@ -253,6 +261,8 @@ Region Name Human Name RegionOne Region One ============== ================ +* Volume API Version is 1 + unitedstack ----------- @@ -267,6 +277,7 @@ gd1 Guangdong * Identity API Version is 3 * Images must be in `raw` format +* Volume API Version is 1 vexxhost -------- diff --git a/os_client_config/defaults.json b/os_client_config/defaults.json index 6735b5535..2ffb67262 100644 --- a/os_client_config/defaults.json +++ b/os_client_config/defaults.json @@ -17,5 +17,5 @@ "object_store_api_version": "1", "orchestration_api_version": "1", "secgroup_source": "neutron", - "volume_api_version": "1" + "volume_api_version": "2" } diff --git a/os_client_config/vendors/bluebox.json b/os_client_config/vendors/bluebox.json index 2227aac81..647c8429f 100644 --- a/os_client_config/vendors/bluebox.json +++ b/os_client_config/vendors/bluebox.json @@ -1,6 +1,7 @@ { "name": "bluebox", "profile": { + "volume_api_version": "1", "region_name": "RegionOne" } } diff --git a/os_client_config/vendors/catalyst.json b/os_client_config/vendors/catalyst.json index ddde83825..3ad75075b 100644 --- a/os_client_config/vendors/catalyst.json +++ b/os_client_config/vendors/catalyst.json @@ -9,6 +9,7 @@ "nz_wlg_2" ], "image_api_version": "1", + "volume_api_version": "1", "image_format": "raw" } } diff --git a/os_client_config/vendors/citycloud.json b/os_client_config/vendors/citycloud.json index f6c57c7b5..64cadce9a 100644 --- a/os_client_config/vendors/citycloud.json +++ b/os_client_config/vendors/citycloud.json @@ -9,6 +9,7 @@ "Sto2", "Kna1" ], + "volume_api_version": "1", "identity_api_version": "3" } } diff --git a/os_client_config/vendors/entercloudsuite.json b/os_client_config/vendors/entercloudsuite.json index 826c25f7f..5a425b4c1 100644 --- a/os_client_config/vendors/entercloudsuite.json +++ b/os_client_config/vendors/entercloudsuite.json @@ -4,6 +4,7 @@ "auth": { "auth_url": "https://api.entercloudsuite.com/v2.0" }, + "volume_api_version": "1", "regions": [ "it-mil1", "nl-ams1", diff --git a/os_client_config/vendors/hp.json b/os_client_config/vendors/hp.json index 10789a91b..ac280f2d1 100644 --- a/os_client_config/vendors/hp.json +++ b/os_client_config/vendors/hp.json @@ -9,6 +9,7 @@ "region-b.geo-1" ], "dns_service_type": "hpext:dns", + "volume_api_version": "1", "image_api_version": "1" } } diff --git a/os_client_config/vendors/rackspace.json b/os_client_config/vendors/rackspace.json index 582e1225c..3fbbacd90 100644 --- a/os_client_config/vendors/rackspace.json +++ b/os_client_config/vendors/rackspace.json @@ -18,6 +18,7 @@ "image_format": "vhd", "floating_ip_source": "None", "secgroup_source": "None", + "volume_api_version": "1", "disable_vendor_agent": { "vm_mode": "hvm", "xenapi_use_agent": false diff --git a/os_client_config/vendors/switchengines.json b/os_client_config/vendors/switchengines.json index 8a7c566b8..46f632515 100644 --- a/os_client_config/vendors/switchengines.json +++ b/os_client_config/vendors/switchengines.json @@ -8,6 +8,7 @@ "LS", "ZH" ], + "volume_api_version": "1", "image_api_use_tasks": true, "image_format": "raw" } diff --git a/os_client_config/vendors/ultimum.json b/os_client_config/vendors/ultimum.json index ada6e3de7..0b38d71db 100644 --- a/os_client_config/vendors/ultimum.json +++ b/os_client_config/vendors/ultimum.json @@ -4,6 +4,7 @@ "auth": { "auth_url": "https://console.ultimum-cloud.com:5000/v2.0" }, + "volume_api_version": "1", "region-name": "RegionOne" } } diff --git a/os_client_config/vendors/unitedstack.json b/os_client_config/vendors/unitedstack.json index 41f45851a..ac8be117f 100644 --- a/os_client_config/vendors/unitedstack.json +++ b/os_client_config/vendors/unitedstack.json @@ -8,6 +8,7 @@ "bj1", "gd1" ], + "volume_api_version": "1", "identity_api_version": "3", "image_format": "raw", "floating_ip_source": "None" From 59d9135bab823cbeda5c0b2b8b317a009427b34a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 30 Dec 2015 17:10:00 -0600 Subject: [PATCH 0755/3836] Normalize server objects There are a set of things we always want to do to server dicts because they are no-cost. This is in line with how we manage other resource dicts, so make a normalize function. However, because we need to add cloud and region info from the cloud object, make it a member method rather than an friend function. Then, ensure it's always called on server related calls. Change-Id: Ied869efcb7d83c4f1a753962ca3127ce52172a24 --- shade/_utils.py | 30 ++++++++++++ shade/inventory.py | 4 +- shade/meta.py | 62 +++++++++++-------------- shade/openstackcloud.py | 8 +++- shade/tests/fakes.py | 4 ++ shade/tests/unit/test_create_server.py | 13 ++++-- shade/tests/unit/test_inventory.py | 14 ++++-- shade/tests/unit/test_meta.py | 21 +++++---- shade/tests/unit/test_rebuild_server.py | 9 +++- shade/tests/unit/test_shade.py | 10 +++- 10 files changed, 113 insertions(+), 62 deletions(-) diff --git a/shade/_utils.py b/shade/_utils.py index f8fa5a58c..9d8782e44 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -141,6 +141,36 @@ def _get_entity(func, name_or_id, filters): return entities[0] +def normalize_servers(servers, cloud_name, region_name): + # Here instead of _utils because we need access to region and cloud + # name from the cloud object + ret = [] + for server in servers: + ret.append(normalize_server(server, cloud_name, region_name)) + return ret + + +def normalize_server(server, cloud_name, region_name): + server.pop('links', None) + server['flavor'].pop('links', None) + # OpenStack can return image as a string when you've booted + # from volume + if str(server['image']) != server['image']: + server['image'].pop('links', None) + + server['region'] = region_name + server['cloud'] = cloud_name + + az = server.get('OS-EXT-AZ:availability_zone', None) + if az: + server['az'] = az + + # Ensure volumes is always in the server dict, even if empty + server['volumes'] = [] + + return server + + def normalize_keystone_services(services): """Normalize the structure of keystone services diff --git a/shade/inventory.py b/shade/inventory.py index 64a9cce06..98065971c 100644 --- a/shade/inventory.py +++ b/shade/inventory.py @@ -61,9 +61,7 @@ def list_hosts(self, expand=True): if expand: server_vars = cloud.get_openstack_vars(server) else: - # expand_server_vars gets renamed in a follow on - # patch which should make this a bit clearer. - server_vars = meta.expand_server_vars(cloud, server) + server_vars = meta.add_server_interfaces(cloud, server) hostvars.append(server_vars) return hostvars diff --git a/shade/meta.py b/shade/meta.py index ac10bc4cf..c2d492bf2 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -224,57 +224,47 @@ def get_groups_from_server(cloud, server, server_vars): def expand_server_vars(cloud, server): - """Add and clean up the server dict with information that is essential. + """Backwards compatibility function.""" + return add_server_interfaces(cloud, server) - This function should not make additional calls to the cloud with one - exception - which is getting external/internal IPs, which have to do - some calls to neutron in some cases to find out which IP is routable. This - is essential info as you can't otherwise connect to the server without - it. - """ - server_vars = server - server_vars.pop('links', None) - server_vars['flavor'].pop('links', None) - # OpenStack can return image as a string when you've booted from volume - if str(server['image']) != server['image']: - server_vars['image'].pop('links', None) +def add_server_interfaces(cloud, server): + """Add network interface information to server. + + Query the cloud as necessary to add information to the server record + about the network information needed to interface with the server. + + Ensures that public_v4, public_v6, private_v4, private_v6, interface_ip, + accessIPv4 and accessIPv6 are always set. + """ # First, add an IP address. Set it to '' rather than None if it does # not exist to remain consistent with the pre-existing missing values - server_vars['public_v4'] = get_server_external_ipv4(cloud, server) or '' - server_vars['public_v6'] = get_server_external_ipv6(server) or '' - server_vars['private_v4'] = get_server_private_ip(server, cloud) or '' + server['public_v4'] = get_server_external_ipv4(cloud, server) or '' + server['public_v6'] = get_server_external_ipv6(server) or '' + server['private_v4'] = get_server_private_ip(server, cloud) or '' interface_ip = None - if cloud.private and server_vars['private_v4']: - interface_ip = server_vars['private_v4'] + if cloud.private and server['private_v4']: + interface_ip = server['private_v4'] else: - if (server_vars['public_v6'] and cloud._local_ipv6 + if (server['public_v6'] and cloud._local_ipv6 and not cloud.force_ipv4): - interface_ip = server_vars['public_v6'] + interface_ip = server['public_v6'] else: - interface_ip = server_vars['public_v4'] + interface_ip = server['public_v4'] if interface_ip: - server_vars['interface_ip'] = interface_ip + server['interface_ip'] = interface_ip # Some clouds do not set these, but they're a regular part of the Nova # server record. Since we know them, go ahead and set them. In the case # where they were set previous, we use the values, so this will not break # clouds that provide the information - if cloud.private and server_vars['private_v4']: - server_vars['accessIPv4'] = server_vars['private_v4'] + if cloud.private and server['private_v4']: + server['accessIPv4'] = server['private_v4'] else: - server_vars['accessIPv4'] = server_vars['public_v4'] - server_vars['accessIPv6'] = server_vars['public_v6'] - - server_vars['region'] = cloud.region_name - server_vars['cloud'] = cloud.name + server['accessIPv4'] = server['public_v4'] + server['accessIPv6'] = server['public_v6'] - az = server_vars.get('OS-EXT-AZ:availability_zone', None) - if az: - server_vars['az'] = az - # Ensure volumes is always in the server dict, even if empty - server_vars['volumes'] = [] - return server_vars + return server def expand_server_security_groups(cloud, server): @@ -293,7 +283,7 @@ def get_hostvars_from_server(cloud, server, mounts=None): expand_server_vars if caching is not set up. If caching is set up, the extra cost should be minimal. """ - server_vars = expand_server_vars(cloud, server) + server_vars = add_server_interfaces(cloud, server) flavor_id = server['flavor']['id'] flavor_name = cloud.get_flavor_name(flavor_id) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index aaee1b180..01c004e95 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1130,7 +1130,9 @@ def _list_servers(self, detailed=False): "Error fetching server list on {cloud}:{region}:".format( cloud=self.name, region=self.region_name)): - servers = self.manager.submitTask(_tasks.ServerList()) + servers = _utils.normalize_servers( + self.manager.submitTask(_tasks.ServerList()), + cloud_name=self.name, region_name=self.region_name) if detailed: return [ @@ -1485,7 +1487,9 @@ def get_server(self, name_or_id=None, filters=None, detailed=False): return _utils._get_entity(searchfunc, name_or_id, filters) def get_server_by_id(self, id): - return self.manager.submitTask(_tasks.ServerGet(server=id)) + return _utils.normalize_server( + self.manager.submitTask(_tasks.ServerGet(server=id)), + cloud_name=self.name, region_name=self.region_name) def get_image(self, name_or_id, filters=None): """Get an image by name or ID. diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index d20d19e50..c3bf28e31 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -72,7 +72,11 @@ def __init__( self.name = name self.status = status self.addresses = addresses + if not flavor: + flavor = {} self.flavor = flavor + if not image: + image = {} self.image = image self.accessIPv4 = accessIPv4 self.accessIPv6 = accessIPv6 diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index 8abf6cc4d..42ed3c1ea 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -21,6 +21,7 @@ from mock import patch, Mock import os_client_config +from shade import _utils from shade import meta from shade import OpenStackCloud from shade.exc import (OpenStackCloudException, OpenStackCloudTimeout) @@ -132,10 +133,14 @@ def test_create_server_no_wait(self): "servers.get.return_value": fake_server } OpenStackCloud.nova_client = Mock(**config) - self.assertEqual(meta.obj_to_dict(fake_server), - self.client.create_server( - name='server-name', image='image=id', - flavor='flavor-id')) + self.assertEqual( + _utils.normalize_server( + meta.obj_to_dict(fake_server), + cloud_name=self.client.name, + region_name=self.client.region_name), + self.client.create_server( + name='server-name', image='image=id', + flavor='flavor-id')) def test_create_server_wait(self): """ diff --git a/shade/tests/unit/test_inventory.py b/shade/tests/unit/test_inventory.py index 9c0a6a441..88a2d12e2 100644 --- a/shade/tests/unit/test_inventory.py +++ b/shade/tests/unit/test_inventory.py @@ -16,7 +16,10 @@ import mock import os_client_config +from shade import _utils from shade import inventory +from shade import meta +from shade.tests import fakes from shade.tests.unit import base @@ -59,14 +62,17 @@ def test_list_hosts(self, mock_cloud, mock_config): self.assertEqual([server], ret) @mock.patch("os_client_config.config.OpenStackConfig") - @mock.patch("shade.meta.expand_server_vars") + @mock.patch("shade.meta.add_server_interfaces") @mock.patch("shade.OpenStackCloud") - def test_list_hosts_no_detail(self, mock_cloud, mock_expand, mock_config): + def test_list_hosts_no_detail(self, mock_cloud, mock_add, mock_config): mock_config.return_value.get_all_clouds.return_value = [{}] inv = inventory.OpenStackInventory() - server = dict(id='server_id', name='server_name') + server = _utils.normalize_server( + meta.obj_to_dict(fakes.FakeServer( + '1234', 'test', 'ACTIVE', addresses={})), + region_name='', cloud_name='') self.assertIsInstance(inv.clouds, list) self.assertEqual(1, len(inv.clouds)) inv.clouds[0].list_servers.return_value = [server] @@ -75,7 +81,7 @@ def test_list_hosts_no_detail(self, mock_cloud, mock_expand, mock_config): inv.clouds[0].list_servers.assert_called_once_with() self.assertFalse(inv.clouds[0].get_openstack_vars.called) - mock_expand.assert_called_once_with(inv.clouds[0], server) + mock_add.assert_called_once_with(inv.clouds[0], server) @mock.patch("os_client_config.config.OpenStackConfig") @mock.patch("shade.OpenStackCloud") diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index dece29784..77688adfa 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -19,6 +19,7 @@ from neutronclient.common import exceptions as neutron_exceptions import shade +from shade import _utils from shade import meta from shade.tests import fakes @@ -359,14 +360,17 @@ def test_basic_hostvars( mock_get_server_external_ipv6.return_value = PUBLIC_V6 hostvars = meta.get_hostvars_from_server( - FakeCloud(), meta.obj_to_dict(FakeServer())) + FakeCloud(), _utils.normalize_server( + meta.obj_to_dict(FakeServer()), + cloud_name='CLOUD_NAME', + region_name='REGION_NAME')) self.assertNotIn('links', hostvars) self.assertEqual(PRIVATE_V4, hostvars['private_v4']) self.assertEqual(PUBLIC_V4, hostvars['public_v4']) self.assertEqual(PUBLIC_V6, hostvars['public_v6']) self.assertEqual(PUBLIC_V6, hostvars['interface_ip']) - self.assertEquals(FakeCloud.region_name, hostvars['region']) - self.assertEquals(FakeCloud.name, hostvars['cloud']) + self.assertEquals('REGION_NAME', hostvars['region']) + self.assertEquals('CLOUD_NAME', hostvars['cloud']) self.assertEquals("test-image-name", hostvars['image']['name']) self.assertEquals(FakeServer.image['id'], hostvars['image']['id']) self.assertNotIn('links', hostvars['image']) @@ -411,14 +415,13 @@ def test_image_string(self, mock_get_server_external_ipv4): FakeCloud(), meta.obj_to_dict(server)) self.assertEquals('fake-image-id', hostvars['image']['id']) - @mock.patch.object(shade.meta, 'get_server_external_ipv4') - def test_az(self, mock_get_server_external_ipv4): - mock_get_server_external_ipv4.return_value = PUBLIC_V4 - + def test_az(self): server = FakeServer() server.__dict__['OS-EXT-AZ:availability_zone'] = 'az1' - hostvars = meta.get_hostvars_from_server( - FakeCloud(), meta.obj_to_dict(server)) + + hostvars = _utils.normalize_server( + meta.obj_to_dict(server), + cloud_name='', region_name='') self.assertEquals('az1', hostvars['az']) def test_has_volume(self): diff --git a/shade/tests/unit/test_rebuild_server.py b/shade/tests/unit/test_rebuild_server.py index 436824e0d..a76ce188f 100644 --- a/shade/tests/unit/test_rebuild_server.py +++ b/shade/tests/unit/test_rebuild_server.py @@ -21,6 +21,7 @@ from mock import patch, Mock import os_client_config +from shade import _utils from shade import meta from shade import OpenStackCloud from shade.exc import (OpenStackCloudException, OpenStackCloudTimeout) @@ -108,5 +109,9 @@ def test_rebuild_server_wait(self): "servers.get.return_value": active_server } OpenStackCloud.nova_client = Mock(**config) - self.assertEqual(meta.obj_to_dict(active_server), - self.client.rebuild_server("a", "b", wait=True)) + self.client.name = 'cloud-name' + self.assertEqual( + _utils.normalize_server( + meta.obj_to_dict(active_server), + cloud_name='cloud-name', region_name=''), + self.client.rebuild_server("a", "b", wait=True)) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 402cefb24..83349308f 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -25,6 +25,7 @@ from shade import _utils from shade import exc from shade import meta +from shade.tests import fakes from shade.tests.unit import base @@ -499,7 +500,9 @@ def test_list_servers(self, mock_serverlist): to the ServerList task.''' mock_serverlist.return_value = [ munch.Munch({'name': 'testserver', - 'id': '1'}) + 'id': '1', + 'flavor': {}, + 'image': ''}) ] r = self.cloud.list_servers() @@ -514,7 +517,10 @@ def test_list_servers_detailed(self, '''This test verifies that when list_servers is called with `detailed=True` that it calls `get_hostvars_from_server` for each server in the list.''' - mock_serverlist.return_value = ['server1', 'server2'] + mock_serverlist.return_value = [ + fakes.FakeServer('server1', '', 'ACTIVE'), + fakes.FakeServer('server2', '', 'ACTIVE'), + ] mock_get_hostvars_from_server.side_effect = [ {'name': 'server1', 'id': '1'}, {'name': 'server2', 'id': '2'}, From a269d53ed3fdb7bbcffbd1d9e89c6233a9a719d3 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 5 Jan 2016 10:08:13 -0500 Subject: [PATCH 0756/3836] Bug fix: Cinder v2 returns bools now Cinder v1 used to return strings for some boolean values in the volume dict. Cinder v2 uses bools. Fix the volume normalization function to recognize either, and add unit tests for it. Change-Id: Ia8600f660d0608622d0bface25f0d1418e6972cb --- .../cinderv2-norm-fix-037189c60b43089f.yaml | 3 ++ shade/_utils.py | 16 +++++---- shade/tests/unit/test__utils.py | 36 +++++++++++++++++++ 3 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/cinderv2-norm-fix-037189c60b43089f.yaml diff --git a/releasenotes/notes/cinderv2-norm-fix-037189c60b43089f.yaml b/releasenotes/notes/cinderv2-norm-fix-037189c60b43089f.yaml new file mode 100644 index 000000000..0847ee667 --- /dev/null +++ b/releasenotes/notes/cinderv2-norm-fix-037189c60b43089f.yaml @@ -0,0 +1,3 @@ +--- +fixes: + - Fixed the volume normalization function when used with cinder v2. diff --git a/shade/_utils.py b/shade/_utils.py index 9d8782e44..ae2065496 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -15,6 +15,7 @@ import contextlib import inspect import netifaces +import six import time from decorator import decorator @@ -357,13 +358,16 @@ def normalize_volumes(volumes): new_vol['display_name'] = name new_vol['description'] = description new_vol['display_description'] = description - # For some reason, cinder uses strings for bools for these fields + # For some reason, cinder v1 uses strings for bools for these fields. + # Cinder v2 uses booleans. for field in ('bootable', 'multiattach'): - if field in new_vol and new_vol[field] is not None: - if new_vol[field].lower() == 'true': - new_vol[field] = True - elif new_vol[field].lower() == 'false': - new_vol[field] = False + if field in new_vol and isinstance(new_vol[field], + six.string_types): + if new_vol[field] is not None: + if new_vol[field].lower() == 'true': + new_vol[field] = True + elif new_vol[field].lower() == 'false': + new_vol[field] = False ret.append(new_vol) return meta.obj_list_to_dict(ret) diff --git a/shade/tests/unit/test__utils.py b/shade/tests/unit/test__utils.py index 869fbb3e0..afd93010e 100644 --- a/shade/tests/unit/test__utils.py +++ b/shade/tests/unit/test__utils.py @@ -112,3 +112,39 @@ def test_normalize_nova_secgroup_rules(self): ] retval = _utils.normalize_nova_secgroup_rules(nova_rules) self.assertEqual(expected, retval) + + def test_normalize_volumes_v1(self): + vol = dict( + display_name='test', + display_description='description', + bootable=u'false', # unicode type + multiattach='true', # str type + ) + expected = dict( + name=vol['display_name'], + display_name=vol['display_name'], + description=vol['display_description'], + display_description=vol['display_description'], + bootable=False, + multiattach=True, + ) + retval = _utils.normalize_volumes([vol]) + self.assertEqual([expected], retval) + + def test_normalize_volumes_v2(self): + vol = dict( + display_name='test', + display_description='description', + bootable=False, + multiattach=True, + ) + expected = dict( + name=vol['display_name'], + display_name=vol['display_name'], + description=vol['display_description'], + display_description=vol['display_description'], + bootable=False, + multiattach=True, + ) + retval = _utils.normalize_volumes([vol]) + self.assertEqual([expected], retval) From e19c47e26eece425d110fdbf06407837a8893cf9 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 5 Jan 2016 08:03:02 -0500 Subject: [PATCH 0757/3836] Fix filtering in search_stacks() The search_stacks() call was calling _utils._filter_list() with a parameter that was removed in a previous commit. Change-Id: I82c3b36ec0d9885533a7be13c530ecb46285c45e --- shade/openstackcloud.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 01c004e95..3804cce87 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -955,8 +955,7 @@ def search_stacks(self, name_or_id=None, filters=None): openstack API call. """ stacks = self.list_stacks() - return _utils._filter_list( - stacks, name_or_id, filters, name_name='stack_name') + return _utils._filter_list(stacks, name_or_id, filters) def list_keypairs(self): """List all available keypairs. From c53ed12c323676111a959f666bb32dec301379c1 Mon Sep 17 00:00:00 2001 From: Clayton O'Neill Date: Sat, 19 Dec 2015 21:22:03 -0500 Subject: [PATCH 0758/3836] Add tests for stack search API The search_stacks method had a bug before because it wasn't tested, so hopefully this will help prevent that in the future. Change-Id: I4b4506b2120eb633893db0155c8ef626480dbaa9 --- shade/tests/unit/test_stack.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/shade/tests/unit/test_stack.py b/shade/tests/unit/test_stack.py index b044c5577..0cd32d5d8 100644 --- a/shade/tests/unit/test_stack.py +++ b/shade/tests/unit/test_stack.py @@ -50,6 +50,38 @@ def test_list_stacks_exception(self, mock_heat): ): self.cloud.list_stacks() + @mock.patch.object(shade.OpenStackCloud, 'heat_client') + def test_search_stacks(self, mock_heat): + fake_stacks = [ + fakes.FakeStack('001', 'stack1'), + fakes.FakeStack('002', 'stack2'), + ] + mock_heat.stacks.list.return_value = fake_stacks + stacks = self.cloud.search_stacks() + mock_heat.stacks.list.assert_called_once_with() + self.assertEqual(meta.obj_list_to_dict(fake_stacks), stacks) + + @mock.patch.object(shade.OpenStackCloud, 'heat_client') + def test_search_stacks_filters(self, mock_heat): + fake_stacks = [ + fakes.FakeStack('001', 'stack1', status='GOOD'), + fakes.FakeStack('002', 'stack2', status='BAD'), + ] + mock_heat.stacks.list.return_value = fake_stacks + filters = {'stack_status': 'GOOD'} + stacks = self.cloud.search_stacks(filters=filters) + mock_heat.stacks.list.assert_called_once_with() + self.assertEqual(meta.obj_list_to_dict(fake_stacks[:1]), stacks) + + @mock.patch.object(shade.OpenStackCloud, 'heat_client') + def test_search_stacks_exception(self, mock_heat): + mock_heat.stacks.list.side_effect = Exception() + with testtools.ExpectedException( + shade.OpenStackCloudException, + "Error fetching stack list" + ): + self.cloud.search_stacks() + @mock.patch.object(shade.OpenStackCloud, 'get_stack') @mock.patch.object(shade.OpenStackCloud, 'heat_client') def test_delete_stack(self, mock_heat, mock_get): From 84f465614063145a5c6c9bb53053a20ae46c8e07 Mon Sep 17 00:00:00 2001 From: Lars Kellogg-Stedman Date: Mon, 4 Jan 2016 20:55:44 -0500 Subject: [PATCH 0759/3836] correct rpmlint errors these are all tremendously minor changes, but they were necessary to stop rpmlint from throwing errors (which prevents official fedora package builds). Change-Id: I1f08753887583a90e26435b5ad0d8f86d2b30992 --- shade/cmd/inventory.py | 1 - shade/task_manager.py | 2 -- shade/tests/ansible/hooks/post_test_hook.sh | 1 + shade/tests/functional/hooks/post_test_hook.sh | 1 + 4 files changed, 2 insertions(+), 3 deletions(-) diff --git a/shade/cmd/inventory.py b/shade/cmd/inventory.py index 4f9538fff..47d38979c 100755 --- a/shade/cmd/inventory.py +++ b/shade/cmd/inventory.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright (c) 2015 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/shade/task_manager.py b/shade/task_manager.py index 8de4b3add..4bcc85d05 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python - # Copyright (C) 2011-2013 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/shade/tests/ansible/hooks/post_test_hook.sh b/shade/tests/ansible/hooks/post_test_hook.sh index d79a9427f..bf4444ead 100755 --- a/shade/tests/ansible/hooks/post_test_hook.sh +++ b/shade/tests/ansible/hooks/post_test_hook.sh @@ -1,3 +1,4 @@ +#!/bin/sh # -*- coding: utf-8 -*- # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/shade/tests/functional/hooks/post_test_hook.sh b/shade/tests/functional/hooks/post_test_hook.sh index 35c1c06a7..ad823009a 100755 --- a/shade/tests/functional/hooks/post_test_hook.sh +++ b/shade/tests/functional/hooks/post_test_hook.sh @@ -1,3 +1,4 @@ +#!/bin/sh # -*- coding: utf-8 -*- # Licensed under the Apache License, Version 2.0 (the "License"); you may From 0bc9e33c9f978a8262453d7364143e8a02d3eded Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 6 Jan 2016 08:43:19 -0600 Subject: [PATCH 0760/3836] Stop hardcoding compute in simple_client There's a debug leftover oops where we just passed 'compute' rather than the service_key requested. Change-Id: Id8c82e43ba34859426b1fdc93dcf3ab2bbde4966 --- os_client_config/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/os_client_config/__init__.py b/os_client_config/__init__.py index ac585f248..ece155983 100644 --- a/os_client_config/__init__.py +++ b/os_client_config/__init__.py @@ -31,7 +31,7 @@ def simple_client(service_key, cloud=None, region_name=None): at OpenStack REST APIs with a properly configured keystone session. """ return OpenStackConfig().get_one_cloud( - cloud=cloud, region_name=region_name).get_session_client('compute') + cloud=cloud, region_name=region_name).get_session_client(service_key) def make_client(service_key, constructor, options=None, **kwargs): From 3b5673ce4c8c9d54568028056300eae053828ee0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 5 Jan 2016 10:30:28 -0600 Subject: [PATCH 0761/3836] Update auth urls and identity API versions Most of the clouds, it turns out, support unversioned auth_url as well as keystone v3. Change-Id: I088d008cd2732f137c8a1bbbd9c0a43f7d382f92 --- doc/source/vendor-support.rst | 4 ++-- os_client_config/vendors/auro.json | 1 + os_client_config/vendors/conoha.json | 5 +++-- os_client_config/vendors/datacentred.json | 3 ++- os_client_config/vendors/dreamhost.json | 3 ++- os_client_config/vendors/elastx.json | 3 ++- os_client_config/vendors/entercloudsuite.json | 3 ++- os_client_config/vendors/hp.json | 3 ++- os_client_config/vendors/internap.json | 3 ++- os_client_config/vendors/ovh.json | 3 ++- os_client_config/vendors/runabove.json | 3 ++- os_client_config/vendors/ultimum.json | 3 ++- 12 files changed, 24 insertions(+), 13 deletions(-) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index 8ae2f6194..a215822da 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -74,7 +74,7 @@ Kna1 Karlskrona, SE conoha ------ -https://identity.%(region_name)s.conoha.io/v2.0 +https://identity.%(region_name)s.conoha.io ============== ================ Region Name Human Name @@ -89,7 +89,7 @@ sjc1 San Jose, CA datacentred ----------- -https://compute.datacentred.io:5000/v2.0 +https://compute.datacentred.io:5000 ============== ================ Region Name Human Name diff --git a/os_client_config/vendors/auro.json b/os_client_config/vendors/auro.json index 1e59f01d4..a9e709bea 100644 --- a/os_client_config/vendors/auro.json +++ b/os_client_config/vendors/auro.json @@ -4,6 +4,7 @@ "auth": { "auth_url": "https://api.van1.auro.io:5000/v2.0" }, + "identity_api_version": "2", "region_name": "van1" } } diff --git a/os_client_config/vendors/conoha.json b/os_client_config/vendors/conoha.json index 8e33ca41b..5636f0955 100644 --- a/os_client_config/vendors/conoha.json +++ b/os_client_config/vendors/conoha.json @@ -2,12 +2,13 @@ "name": "conoha", "profile": { "auth": { - "auth_url": "https://identity.{region_name}.conoha.io/v2.0" + "auth_url": "https://identity.{region_name}.conoha.io" }, "regions": [ "sin1", "sjc1", "tyo1" - ] + ], + "identity_api_version": "2" } } diff --git a/os_client_config/vendors/datacentred.json b/os_client_config/vendors/datacentred.json index 1fb4dbb09..2be4a5863 100644 --- a/os_client_config/vendors/datacentred.json +++ b/os_client_config/vendors/datacentred.json @@ -2,9 +2,10 @@ "name": "datacentred", "profile": { "auth": { - "auth_url": "https://compute.datacentred.io:5000/v2.0" + "auth_url": "https://compute.datacentred.io:5000" }, "region-name": "sal01", + "identity_api_version": "2", "image_api_version": "1" } } diff --git a/os_client_config/vendors/dreamhost.json b/os_client_config/vendors/dreamhost.json index 8580826e1..6fc2ccf8a 100644 --- a/os_client_config/vendors/dreamhost.json +++ b/os_client_config/vendors/dreamhost.json @@ -2,8 +2,9 @@ "name": "dreamhost", "profile": { "auth": { - "auth_url": "https://keystone.dream.io/v2.0" + "auth_url": "https://keystone.dream.io" }, + "identity_api_version": "3", "region_name": "RegionOne", "image_format": "raw" } diff --git a/os_client_config/vendors/elastx.json b/os_client_config/vendors/elastx.json index cac755e8f..1e7248213 100644 --- a/os_client_config/vendors/elastx.json +++ b/os_client_config/vendors/elastx.json @@ -2,8 +2,9 @@ "name": "elastx", "profile": { "auth": { - "auth_url": "https://ops.elastx.net:5000/v2.0" + "auth_url": "https://ops.elastx.net:5000" }, + "identity_api_version": "3", "region_name": "regionOne" } } diff --git a/os_client_config/vendors/entercloudsuite.json b/os_client_config/vendors/entercloudsuite.json index 5a425b4c1..6d2fc129e 100644 --- a/os_client_config/vendors/entercloudsuite.json +++ b/os_client_config/vendors/entercloudsuite.json @@ -2,8 +2,9 @@ "name": "entercloudsuite", "profile": { "auth": { - "auth_url": "https://api.entercloudsuite.com/v2.0" + "auth_url": "https://api.entercloudsuite.com/" }, + "identity_api_version": "3", "volume_api_version": "1", "regions": [ "it-mil1", diff --git a/os_client_config/vendors/hp.json b/os_client_config/vendors/hp.json index ac280f2d1..b06b90ad5 100644 --- a/os_client_config/vendors/hp.json +++ b/os_client_config/vendors/hp.json @@ -2,12 +2,13 @@ "name": "hp", "profile": { "auth": { - "auth_url": "https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0" + "auth_url": "https://region-b.geo-1.identity.hpcloudsvc.com:35357" }, "regions": [ "region-a.geo-1", "region-b.geo-1" ], + "identity_api_version": "3", "dns_service_type": "hpext:dns", "volume_api_version": "1", "image_api_version": "1" diff --git a/os_client_config/vendors/internap.json b/os_client_config/vendors/internap.json index 9b27536d5..d5ad49f6d 100644 --- a/os_client_config/vendors/internap.json +++ b/os_client_config/vendors/internap.json @@ -2,13 +2,14 @@ "name": "internap", "profile": { "auth": { - "auth_url": "https://identity.api.cloud.iweb.com/v2.0" + "auth_url": "https://identity.api.cloud.iweb.com" }, "regions": [ "ams01", "da01", "nyj01" ], + "identity_api_version": "3", "image_api_version": "1", "floating_ip_source": "None" } diff --git a/os_client_config/vendors/ovh.json b/os_client_config/vendors/ovh.json index 032741f83..664f1617f 100644 --- a/os_client_config/vendors/ovh.json +++ b/os_client_config/vendors/ovh.json @@ -2,13 +2,14 @@ "name": "ovh", "profile": { "auth": { - "auth_url": "https://auth.cloud.ovh.net/v2.0" + "auth_url": "https://auth.cloud.ovh.net/" }, "regions": [ "BHS1", "GRA1", "SBG1" ], + "identity_api_version": "3", "image_format": "raw", "floating_ip_source": "None" } diff --git a/os_client_config/vendors/runabove.json b/os_client_config/vendors/runabove.json index 56dd9453c..abf111633 100644 --- a/os_client_config/vendors/runabove.json +++ b/os_client_config/vendors/runabove.json @@ -2,12 +2,13 @@ "name": "runabove", "profile": { "auth": { - "auth_url": "https://auth.runabove.io/v2.0" + "auth_url": "https://auth.runabove.io/" }, "regions": [ "BHS-1", "SBG-1" ], + "identity_api_version": "3", "image_format": "qcow2", "floating_ip_source": "None" } diff --git a/os_client_config/vendors/ultimum.json b/os_client_config/vendors/ultimum.json index 0b38d71db..4bfd088cd 100644 --- a/os_client_config/vendors/ultimum.json +++ b/os_client_config/vendors/ultimum.json @@ -2,8 +2,9 @@ "name": "ultimum", "profile": { "auth": { - "auth_url": "https://console.ultimum-cloud.com:5000/v2.0" + "auth_url": "https://console.ultimum-cloud.com:5000/" }, + "identity_api_version": "3", "volume_api_version": "1", "region-name": "RegionOne" } From 0b270f0bc9f6dd31d9c17bcc4d49d15630ee999b Mon Sep 17 00:00:00 2001 From: LiuNanke Date: Wed, 6 Jan 2016 22:49:52 +0800 Subject: [PATCH 0762/3836] Replace assertEqual(None, *) with assertIsNone in tests Replace assertEqual(None, *) with assertIsNone in tests to have more clear messages in case of failure. There have one more place should be modified. Change-Id: I53a8f129db0108892b8377edce2dbf19b0b95f5d Closes-bug: #1280522 --- os_client_config/tests/test_cloud_config.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index 01581b1fb..9d6011120 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -148,8 +148,7 @@ def test_getters(self): self.assertEqual('volume', cc.get_service_type('volume')) self.assertEqual('http://compute.example.com', cc.get_endpoint('compute')) - self.assertEqual(None, - cc.get_endpoint('image')) + self.assertIsNone(cc.get_endpoint('image')) self.assertIsNone(cc.get_service_name('compute')) self.assertEqual('locks', cc.get_service_name('identity')) From cab0469ec4471a5fe924d6049cbfcdf2ac0cdba4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 5 Jan 2016 09:08:05 -0600 Subject: [PATCH 0763/3836] Add IBM Public Cloud IBM Cloud has a public Openstack Cloud. We should support it. Change-Id: If0bc29c41869494b2a4da944f7792cbe0f217f0e --- doc/source/vendor-support.rst | 13 +++++++++++++ os_client_config/vendors/ibmcloud.json | 13 +++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 os_client_config/vendors/ibmcloud.json diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index a215822da..46c95d8c4 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -159,6 +159,19 @@ region-b.geo-1 US East * Public IPv4 is provided via NAT with Neutron Floating IP * Volume API Version is 1 +ibmcloud +-------- + +https://identity.open.softlayer.com + +============== ================ +Region Name Human Name +============== ================ +london London, UK +============== ================ + +* Public IPv4 is provided via NAT with Neutron Floating IP + internap -------- diff --git a/os_client_config/vendors/ibmcloud.json b/os_client_config/vendors/ibmcloud.json new file mode 100644 index 000000000..90962c60e --- /dev/null +++ b/os_client_config/vendors/ibmcloud.json @@ -0,0 +1,13 @@ +{ + "name": "ibmcloud", + "profile": { + "auth": { + "auth_url": "https://identity.open.softlayer.com" + }, + "volume_api_version": "2", + "identity_api_version": "3", + "regions": [ + "london" + ] + } +} From caae8ad43487d5060d113d294c8d8862c7d3f788 Mon Sep 17 00:00:00 2001 From: LiuNanke Date: Thu, 7 Jan 2016 15:21:31 +0800 Subject: [PATCH 0764/3836] Remove openstack-common.conf We don't sync from oslo-incubator, so don't need this file any more. Change-Id: Ia4acc67fe38c4a27a098c4da263265ed3742b7e7 --- openstack-common.conf | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 openstack-common.conf diff --git a/openstack-common.conf b/openstack-common.conf deleted file mode 100644 index e8eb2aa2c..000000000 --- a/openstack-common.conf +++ /dev/null @@ -1,6 +0,0 @@ -[DEFAULT] - -# The list of modules to copy from oslo-incubator.git - -# The base module to hold the copy of openstack.common -base=os_client_config \ No newline at end of file From cf439dddd80161fa65e5699ae0fbb291838483b4 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 7 Jan 2016 12:02:27 -0500 Subject: [PATCH 0765/3836] Add range search functionality It can be useful to have more advanced search capability across a data set than just exact value matching. For instance, when searching for flavors, one may want to just say "give me the flavor(s) with the least amount of ram" or "give me the flavor(s) with 2 or more virtual CPUs, but less than 8GB of ram". We can accomplish this with range searching. Example: flavors = self.range_search(self.list_flavors(), {"vcpus": ">=2", "ram": "<8192"}) Besides the normal range operators (<, >, <=, >=), one can also use the values "MIN" or "MAX" for minimum and maximum values: flavors = self.range_search(self.list_flavors(), {"ram": "MIN"}) Change-Id: I706e4eee62a969888db3ea70f7052d3fb00c544e --- shade/_utils.py | 165 +++++++++++++++++++ shade/openstackcloud.py | 47 ++++++ shade/tests/functional/test_range_search.py | 129 +++++++++++++++ shade/tests/unit/test__utils.py | 166 ++++++++++++++++++++ shade/tests/unit/test_shade.py | 43 +++++ 5 files changed, 550 insertions(+) create mode 100644 shade/tests/functional/test_range_search.py diff --git a/shade/_utils.py b/shade/_utils.py index ae2065496..b5811d8fb 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -15,6 +15,7 @@ import contextlib import inspect import netifaces +import re import six import time @@ -535,3 +536,167 @@ def shade_exceptions(error_message=None): if error_message is None: error_message = str(e) raise exc.OpenStackCloudException(error_message) + + +def safe_dict_min(key, data): + """Safely find the minimum for a given key in a list of dict objects. + + This will find the minimum integer value for specific dictionary key + across a list of dictionaries. The values for the given key MUST be + integers, or string representations of an integer. + + The dictionary key does not have to be present in all (or any) + of the elements/dicts within the data set. + + :param string key: The dictionary key to search for the minimum value. + :param list data: List of dicts to use for the data set. + + :returns: None if the field was not not found in any elements, or + the minimum value for the field otherwise. + """ + min_value = None + for d in data: + if (key in d) and (d[key] is not None): + try: + val = int(d[key]) + except ValueError: + raise exc.OpenStackCloudException( + "Search for minimum value failed. " + "Value for {key} is not an integer: {value}".format( + key=key, value=d[key]) + ) + if (min_value is None) or (val < min_value): + min_value = val + return min_value + + +def safe_dict_max(key, data): + """Safely find the maximum for a given key in a list of dict objects. + + This will find the maximum integer value for specific dictionary key + across a list of dictionaries. The values for the given key MUST be + integers, or string representations of an integer. + + The dictionary key does not have to be present in all (or any) + of the elements/dicts within the data set. + + :param string key: The dictionary key to search for the maximum value. + :param list data: List of dicts to use for the data set. + + :returns: None if the field was not not found in any elements, or + the maximum value for the field otherwise. + """ + max_value = None + for d in data: + if (key in d) and (d[key] is not None): + try: + val = int(d[key]) + except ValueError: + raise exc.OpenStackCloudException( + "Search for maximum value failed. " + "Value for {key} is not an integer: {value}".format( + key=key, value=d[key]) + ) + if (max_value is None) or (val > max_value): + max_value = val + return max_value + + +def parse_range(value): + """Parse a numerical range string. + + Breakdown a range expression into its operater and numerical parts. + This expression must be a string. Valid values must be an integer string, + optionally preceeded by one of the following operators:: + + - "<" : Less than + - ">" : Greater than + - "<=" : Less than or equal to + - ">=" : Greater than or equal to + + Some examples of valid values and function return values:: + + - "1024" : returns (None, 1024) + - "<5" : returns ("<", 5) + - ">=100" : returns (">=", 100) + + :param string value: The range expression to be parsed. + + :returns: A tuple with the operator string (or None if no operator + was given) and the integer value. None is returned if parsing failed. + """ + if value is None: + return None + + range_exp = re.match('(<|>|<=|>=){0,1}(\d+)$', value) + if range_exp is None: + return None + + op = range_exp.group(1) + num = int(range_exp.group(2)) + return (op, num) + + +def range_filter(data, key, range_exp): + """Filter a list by a single range expression. + + :param list data: List of dictionaries to be searched. + :param string key: Key name to search within the data set. + :param string range_exp: The expression describing the range of values. + + :returns: A list subset of the original data set. + :raises: OpenStackCloudException on invalid range expressions. + """ + filtered = [] + range_exp = str(range_exp).upper() + + if range_exp == "MIN": + key_min = safe_dict_min(key, data) + if key_min is None: + return [] + for d in data: + if int(d[key]) == key_min: + filtered.append(d) + return filtered + elif range_exp == "MAX": + key_max = safe_dict_max(key, data) + if key_max is None: + return [] + for d in data: + if int(d[key]) == key_max: + filtered.append(d) + return filtered + + # Not looking for a min or max, so a range or exact value must + # have been supplied. + val_range = parse_range(range_exp) + + # If parsing the range fails, it must be a bad value. + if val_range is None: + raise exc.OpenStackCloudException( + "Invalid range value: {value}".format(value=range_exp)) + + op = val_range[0] + if op: + # Range matching + for d in data: + d_val = int(d[key]) + if op == '<': + if d_val < val_range[1]: + filtered.append(d) + elif op == '>': + if d_val > val_range[1]: + filtered.append(d) + elif op == '<=': + if d_val <= val_range[1]: + filtered.append(d) + elif op == '>=': + if d_val >= val_range[1]: + filtered.append(d) + return filtered + else: + # Exact number match + for d in data: + if int(d[key]) == val_range[1]: + filtered.append(d) + return filtered diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 3804cce87..032e409e3 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -351,6 +351,53 @@ def _get_identity_params(self, domain_id=None, project=None): ret.update(self._get_project_param_dict(project)) return ret + def range_search(self, data, filters): + """Perform integer range searches across a list of dictionaries. + + Given a list of dictionaries, search across the list using the given + dictionary keys and a range of integer values for each key. Only + dictionaries that match ALL search filters across the entire original + data set will be returned. + + It is not a requirement that each dictionary contain the key used + for searching. Those without the key will be considered non-matching. + + The range values must be string values and is either a set of digits + representing an integer for matching, or a range operator followed by + a set of digits representing an integer for matching. If a range + operator is not given, exact value matching will be used. Valid + operators are one of: <,>,<=,>= + + :param list data: List of dictionaries to be searched. + :param dict filters: Dict describing the one or more range searches to + perform. If more than one search is given, the result will be the + members of the original data set that match ALL searches. An + example of filtering by multiple ranges:: + + {"vcpus": "<=5", "ram": "<=2048", "disk": "1"} + + :returns: A list subset of the original data set. + :raises: OpenStackCloudException on invalid range expressions. + """ + filtered = [] + + for key, range_value in filters.items(): + # We always want to operate on the full data set so that + # calculations for minimum and maximum are correct. + results = _utils.range_filter(data, key, range_value) + + if not filtered: + # First set of results + filtered = results + else: + # The combination of all searches should be the intersection of + # all result sets from each search. So adjust the current set + # of filtered data by computing its intersection with the + # latest result set. + filtered = [r for r in results for f in filtered if r == f] + + return filtered + @_utils.cache_on_arguments() def list_projects(self): """List Keystone Projects. diff --git a/shade/tests/functional/test_range_search.py b/shade/tests/functional/test_range_search.py new file mode 100644 index 000000000..eb31b7984 --- /dev/null +++ b/shade/tests/functional/test_range_search.py @@ -0,0 +1,129 @@ +# Copyright (c) 2016 IBM +# +# 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 shade +from shade import exc +from shade.tests import base + + +class TestRangeSearch(base.TestCase): + + def setUp(self): + super(TestRangeSearch, self).setUp() + self.cloud = shade.openstack_cloud(cloud='devstack') + + def test_range_search_bad_range(self): + flavors = self.cloud.list_flavors() + self.assertRaises(exc.OpenStackCloudException, + self.cloud.range_search, flavors, {"ram": "<1a0"}) + + def test_range_search_exact(self): + flavors = self.cloud.list_flavors() + result = self.cloud.range_search(flavors, {"ram": "4096"}) + self.assertIsInstance(result, list) + self.assertEqual(1, len(result)) + self.assertEqual("m1.medium", result[0]['name']) + + def test_range_search_min(self): + flavors = self.cloud.list_flavors() + result = self.cloud.range_search(flavors, {"ram": "MIN"}) + self.assertIsInstance(result, list) + self.assertEqual(1, len(result)) + self.assertEqual("m1.tiny", result[0]['name']) + + def test_range_search_max(self): + flavors = self.cloud.list_flavors() + result = self.cloud.range_search(flavors, {"ram": "MAX"}) + self.assertIsInstance(result, list) + self.assertEqual(1, len(result)) + self.assertEqual("m1.xlarge", result[0]['name']) + + def test_range_search_lt(self): + flavors = self.cloud.list_flavors() + result = self.cloud.range_search(flavors, {"ram": "<4096"}) + self.assertIsInstance(result, list) + self.assertEqual(2, len(result)) + flavor_names = [r['name'] for r in result] + self.assertIn("m1.tiny", flavor_names) + self.assertIn("m1.small", flavor_names) + + def test_range_search_gt(self): + flavors = self.cloud.list_flavors() + result = self.cloud.range_search(flavors, {"ram": ">4096"}) + self.assertIsInstance(result, list) + self.assertEqual(2, len(result)) + flavor_names = [r['name'] for r in result] + self.assertIn("m1.large", flavor_names) + self.assertIn("m1.xlarge", flavor_names) + + def test_range_search_le(self): + flavors = self.cloud.list_flavors() + result = self.cloud.range_search(flavors, {"ram": "<=4096"}) + self.assertIsInstance(result, list) + self.assertEqual(3, len(result)) + flavor_names = [r['name'] for r in result] + self.assertIn("m1.tiny", flavor_names) + self.assertIn("m1.small", flavor_names) + self.assertIn("m1.medium", flavor_names) + + def test_range_search_ge(self): + flavors = self.cloud.list_flavors() + result = self.cloud.range_search(flavors, {"ram": ">=4096"}) + self.assertIsInstance(result, list) + self.assertEqual(3, len(result)) + flavor_names = [r['name'] for r in result] + self.assertIn("m1.medium", flavor_names) + self.assertIn("m1.large", flavor_names) + self.assertIn("m1.xlarge", flavor_names) + + def test_range_search_multi_1(self): + flavors = self.cloud.list_flavors() + result = self.cloud.range_search(flavors, + {"ram": "MIN", "vcpus": "MIN"}) + self.assertIsInstance(result, list) + self.assertEqual(1, len(result)) + self.assertEqual("m1.tiny", result[0]['name']) + + def test_range_search_multi_2(self): + flavors = self.cloud.list_flavors() + result = self.cloud.range_search(flavors, + {"ram": "<8192", "vcpus": "MIN"}) + self.assertIsInstance(result, list) + self.assertEqual(2, len(result)) + flavor_names = [r['name'] for r in result] + # All of these should have 1 vcpu + self.assertIn("m1.tiny", flavor_names) + self.assertIn("m1.small", flavor_names) + + def test_range_search_multi_3(self): + flavors = self.cloud.list_flavors() + result = self.cloud.range_search(flavors, + {"ram": ">=4096", "vcpus": "<6"}) + self.assertIsInstance(result, list) + self.assertEqual(2, len(result)) + flavor_names = [r['name'] for r in result] + self.assertIn("m1.medium", flavor_names) + self.assertIn("m1.large", flavor_names) + + def test_range_search_multi_4(self): + flavors = self.cloud.list_flavors() + result = self.cloud.range_search(flavors, + {"ram": ">=4096", "vcpus": "MAX"}) + self.assertIsInstance(result, list) + self.assertEqual(1, len(result)) + # This is the only result that should have max vcpu + self.assertEqual("m1.xlarge", result[0]['name']) diff --git a/shade/tests/unit/test__utils.py b/shade/tests/unit/test__utils.py index afd93010e..dff7956e9 100644 --- a/shade/tests/unit/test__utils.py +++ b/shade/tests/unit/test__utils.py @@ -12,11 +12,23 @@ # License for the specific language governing permissions and limitations # under the License. +import testtools from shade import _utils +from shade import exc from shade.tests.unit import base +RANGE_DATA = [ + dict(id=1, key1=1, key2=5), + dict(id=2, key1=1, key2=20), + dict(id=3, key1=2, key2=10), + dict(id=4, key1=2, key2=30), + dict(id=5, key1=3, key2=40), + dict(id=6, key1=3, key2=40), +] + + class TestUtils(base.TestCase): def test__filter_list_name_or_id(self): @@ -148,3 +160,157 @@ def test_normalize_volumes_v2(self): ) retval = _utils.normalize_volumes([vol]) self.assertEqual([expected], retval) + + def test_safe_dict_min_ints(self): + """Test integer comparison""" + data = [{'f1': 3}, {'f1': 2}, {'f1': 1}] + retval = _utils.safe_dict_min('f1', data) + self.assertEqual(1, retval) + + def test_safe_dict_min_strs(self): + """Test integer as strings comparison""" + data = [{'f1': '3'}, {'f1': '2'}, {'f1': '1'}] + retval = _utils.safe_dict_min('f1', data) + self.assertEqual(1, retval) + + def test_safe_dict_min_None(self): + """Test None values""" + data = [{'f1': 3}, {'f1': None}, {'f1': 1}] + retval = _utils.safe_dict_min('f1', data) + self.assertEqual(1, retval) + + def test_safe_dict_min_key_missing(self): + """Test missing key for an entry still works""" + data = [{'f1': 3}, {'x': 2}, {'f1': 1}] + retval = _utils.safe_dict_min('f1', data) + self.assertEqual(1, retval) + + def test_safe_dict_min_key_not_found(self): + """Test key not found in any elements returns None""" + data = [{'f1': 3}, {'f1': 2}, {'f1': 1}] + retval = _utils.safe_dict_min('doesnotexist', data) + self.assertIsNone(retval) + + def test_safe_dict_min_not_int(self): + """Test non-integer key value raises OSCE""" + data = [{'f1': 3}, {'f1': "aaa"}, {'f1': 1}] + with testtools.ExpectedException( + exc.OpenStackCloudException, + "Search for minimum value failed. " + "Value for f1 is not an integer: aaa" + ): + _utils.safe_dict_min('f1', data) + + def test_safe_dict_max_ints(self): + """Test integer comparison""" + data = [{'f1': 3}, {'f1': 2}, {'f1': 1}] + retval = _utils.safe_dict_max('f1', data) + self.assertEqual(3, retval) + + def test_safe_dict_max_strs(self): + """Test integer as strings comparison""" + data = [{'f1': '3'}, {'f1': '2'}, {'f1': '1'}] + retval = _utils.safe_dict_max('f1', data) + self.assertEqual(3, retval) + + def test_safe_dict_max_None(self): + """Test None values""" + data = [{'f1': 3}, {'f1': None}, {'f1': 1}] + retval = _utils.safe_dict_max('f1', data) + self.assertEqual(3, retval) + + def test_safe_dict_max_key_missing(self): + """Test missing key for an entry still works""" + data = [{'f1': 3}, {'x': 2}, {'f1': 1}] + retval = _utils.safe_dict_max('f1', data) + self.assertEqual(3, retval) + + def test_safe_dict_max_key_not_found(self): + """Test key not found in any elements returns None""" + data = [{'f1': 3}, {'f1': 2}, {'f1': 1}] + retval = _utils.safe_dict_max('doesnotexist', data) + self.assertIsNone(retval) + + def test_safe_dict_max_not_int(self): + """Test non-integer key value raises OSCE""" + data = [{'f1': 3}, {'f1': "aaa"}, {'f1': 1}] + with testtools.ExpectedException( + exc.OpenStackCloudException, + "Search for maximum value failed. " + "Value for f1 is not an integer: aaa" + ): + _utils.safe_dict_max('f1', data) + + def test_parse_range_None(self): + self.assertIsNone(_utils.parse_range(None)) + + def test_parse_range_invalid(self): + self.assertIsNone(_utils.parse_range("1024") + self.assertIsInstance(retval, tuple) + self.assertEqual(">", retval[0]) + self.assertEqual(1024, retval[1]) + + def test_parse_range_le(self): + retval = _utils.parse_range("<=1024") + self.assertIsInstance(retval, tuple) + self.assertEqual("<=", retval[0]) + self.assertEqual(1024, retval[1]) + + def test_parse_range_ge(self): + retval = _utils.parse_range(">=1024") + self.assertIsInstance(retval, tuple) + self.assertEqual(">=", retval[0]) + self.assertEqual(1024, retval[1]) + + def test_range_filter_min(self): + retval = _utils.range_filter(RANGE_DATA, "key1", "min") + self.assertIsInstance(retval, list) + self.assertEqual(2, len(retval)) + self.assertEqual(RANGE_DATA[:2], retval) + + def test_range_filter_max(self): + retval = _utils.range_filter(RANGE_DATA, "key1", "max") + self.assertIsInstance(retval, list) + self.assertEqual(2, len(retval)) + self.assertEqual(RANGE_DATA[-2:], retval) + + def test_range_filter_range(self): + retval = _utils.range_filter(RANGE_DATA, "key1", "<3") + self.assertIsInstance(retval, list) + self.assertEqual(4, len(retval)) + self.assertEqual(RANGE_DATA[:4], retval) + + def test_range_filter_exact(self): + retval = _utils.range_filter(RANGE_DATA, "key1", "2") + self.assertIsInstance(retval, list) + self.assertEqual(2, len(retval)) + self.assertEqual(RANGE_DATA[2:4], retval) + + def test_range_filter_invalid_int(self): + with testtools.ExpectedException( + exc.OpenStackCloudException, + "Invalid range value: <1A0" + ): + _utils.range_filter(RANGE_DATA, "key1", "<1A0") + + def test_range_filter_invalid_op(self): + with testtools.ExpectedException( + exc.OpenStackCloudException, + "Invalid range value: <>100" + ): + _utils.range_filter(RANGE_DATA, "key1", "<>100") diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 83349308f..bde1db790 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -29,6 +29,16 @@ from shade.tests.unit import base +RANGE_DATA = [ + dict(id=1, key1=1, key2=5), + dict(id=2, key1=1, key2=20), + dict(id=3, key1=2, key2=10), + dict(id=4, key1=2, key2=30), + dict(id=5, key1=3, key2=40), + dict(id=6, key1=3, key2=40), +] + + class TestShade(base.TestCase): def setUp(self): @@ -628,3 +638,36 @@ def test__has_nova_extension(self, mock_nova): mock_nova.client.get.return_value = ('200', body) self.assertTrue(self.cloud._has_nova_extension('NMN')) self.assertFalse(self.cloud._has_nova_extension('invalid')) + + def test_range_search(self): + filters = {"key1": "min", "key2": "20"} + retval = self.cloud.range_search(RANGE_DATA, filters) + self.assertIsInstance(retval, list) + self.assertEqual(1, len(retval)) + self.assertEqual([RANGE_DATA[1]], retval) + + def test_range_search_2(self): + filters = {"key1": "<=2", "key2": ">10"} + retval = self.cloud.range_search(RANGE_DATA, filters) + self.assertIsInstance(retval, list) + self.assertEqual(2, len(retval)) + self.assertEqual([RANGE_DATA[1], RANGE_DATA[3]], retval) + + def test_range_search_3(self): + filters = {"key1": "2", "key2": "min"} + retval = self.cloud.range_search(RANGE_DATA, filters) + self.assertIsInstance(retval, list) + self.assertEqual(0, len(retval)) + + def test_range_search_4(self): + filters = {"key1": "max", "key2": "min"} + retval = self.cloud.range_search(RANGE_DATA, filters) + self.assertIsInstance(retval, list) + self.assertEqual(0, len(retval)) + + def test_range_search_5(self): + filters = {"key1": "min", "key2": "min"} + retval = self.cloud.range_search(RANGE_DATA, filters) + self.assertIsInstance(retval, list) + self.assertEqual(1, len(retval)) + self.assertEqual([RANGE_DATA[0]], retval) From 9835daf9f684556c5aed4834dc086e932788f9bc Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 8 Jan 2016 20:24:17 -0500 Subject: [PATCH 0766/3836] Add barbicanclient support barbicanclient is a lovely client library, so we should add support for make_legacy_client to doing the right things constructing a Client object. Change-Id: Idf015b1119ef76b951c195a6498cbb7a928d6e44 --- os_client_config/cloud_config.py | 7 +++++-- os_client_config/constructors.json | 1 + os_client_config/defaults.json | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index f73da04ed..3b9bee98f 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -305,7 +305,7 @@ def get_legacy_client( endpoint_override = self.get_endpoint(service_key) if not interface_key: - if service_key == 'image': + if service_key in ('image', 'key-manager'): interface_key = 'interface' else: interface_key = 'endpoint_type' @@ -348,7 +348,10 @@ def get_legacy_client( if 'endpoint' not in constructor_kwargs: endpoint = self.get_session_endpoint('identity') constructor_kwargs['endpoint'] = endpoint - constructor_args.append(version) + if service_key == 'key-manager': + constructor_kwargs['version'] = version + else: + constructor_args.append(version) return client_class(*constructor_args, **constructor_kwargs) diff --git a/os_client_config/constructors.json b/os_client_config/constructors.json index be4433920..89c844c55 100644 --- a/os_client_config/constructors.json +++ b/os_client_config/constructors.json @@ -3,6 +3,7 @@ "database": "troveclient.client.Client", "identity": "keystoneclient.client.Client", "image": "glanceclient.Client", + "key-manager": "barbicanclient.client.Client", "metering": "ceilometerclient.client.Client", "network": "neutronclient.neutron.client.Client", "object-store": "swiftclient.client.Connection", diff --git a/os_client_config/defaults.json b/os_client_config/defaults.json index 2ffb67262..f501862d5 100644 --- a/os_client_config/defaults.json +++ b/os_client_config/defaults.json @@ -12,6 +12,7 @@ "image_api_use_tasks": false, "image_api_version": "2", "image_format": "qcow2", + "key_manager_api_version": "v1", "metering_api_version": "2", "network_api_version": "2", "object_store_api_version": "1", From f61a487fa13c8292b9fd3ac103e1133ac05dbd26 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 8 Jan 2016 20:38:35 -0500 Subject: [PATCH 0767/3836] Use _get_client in make_client helper function We have a capability to know what constructor is needed for make_client, but we didn't plumb it in. Make sure that the only thing needed is: os_client_config.make_client('compute') Change-Id: I02aa1c46fa7cdfdb1409f8e1232e364b5ba48cd2 --- os_client_config/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/os_client_config/__init__.py b/os_client_config/__init__.py index ece155983..52fcb8511 100644 --- a/os_client_config/__init__.py +++ b/os_client_config/__init__.py @@ -14,6 +14,7 @@ import sys +from os_client_config import cloud_config from os_client_config.config import OpenStackConfig # noqa @@ -34,7 +35,7 @@ def simple_client(service_key, cloud=None, region_name=None): cloud=cloud, region_name=region_name).get_session_client(service_key) -def make_client(service_key, constructor, options=None, **kwargs): +def make_client(service_key, constructor=None, options=None, **kwargs): """Simple wrapper for getting a client instance from a client lib. OpenStack Client Libraries all have a fairly consistent constructor @@ -44,6 +45,8 @@ def make_client(service_key, constructor, options=None, **kwargs): variables and clouds.yaml - and takes as **kwargs anything you'd expect to pass in. """ + if not constructor: + constructor = cloud_config._get_client(service_key) config = OpenStackConfig() if options: config.register_argparse_options(options, sys.argv, service_key) @@ -51,5 +54,5 @@ def make_client(service_key, constructor, options=None, **kwargs): else: parsed_options = None - cloud_config = config.get_one_cloud(options=parsed_options, **kwargs) - return cloud_config.get_legacy_client(service_key, constructor) + cloud = config.get_one_cloud(options=parsed_options, **kwargs) + return cloud.get_legacy_client(service_key, constructor) From 53187f2a53993b7bec47cdb972e86357ef302995 Mon Sep 17 00:00:00 2001 From: Spencer Krum Date: Mon, 4 Jan 2016 17:28:46 -0800 Subject: [PATCH 0768/3836] Allow inventory filtering by cloud name In some cases only one cloud is in scope for the ansible inventory, this patch enables that workflow. Co-Authored-By: David Shrewsbury Change-Id: Ie2a1fc91878c282daaa75e4fc8aeee19f87c3020 --- shade/cmd/inventory.py | 5 ++++- shade/inventory.py | 19 +++++++++++----- shade/tests/unit/test_inventory.py | 35 ++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/shade/cmd/inventory.py b/shade/cmd/inventory.py index 4f9538fff..4e031955f 100755 --- a/shade/cmd/inventory.py +++ b/shade/cmd/inventory.py @@ -40,6 +40,8 @@ def parse_args(): group.add_argument('--host', help='List details about the specific host') parser.add_argument('--private', action='store_true', default=False, help='Use private IPs for interface_ip') + parser.add_argument('--cloud', default=None, + help='Return data for one cloud only') parser.add_argument('--yaml', action='store_true', default=False, help='Output data in nicely readable yaml') parser.add_argument('--debug', action='store_true', default=False, @@ -52,7 +54,8 @@ def main(): try: shade.simple_logging(debug=args.debug) inventory = shade.inventory.OpenStackInventory( - refresh=args.refresh, private=args.private) + refresh=args.refresh, private=args.private, + cloud=args.cloud) if args.list: output = inventory.list_hosts() elif args.host: diff --git a/shade/inventory.py b/shade/inventory.py index 98065971c..42863f070 100644 --- a/shade/inventory.py +++ b/shade/inventory.py @@ -28,7 +28,7 @@ class OpenStackInventory(object): def __init__( self, config_files=None, refresh=False, private=False, - config_key=None, config_defaults=None): + config_key=None, config_defaults=None, cloud=None): if config_files is None: config_files = [] config = os_client_config.config.OpenStackConfig( @@ -36,10 +36,19 @@ def __init__( self.extra_config = config.get_extra_config( config_key, config_defaults) - self.clouds = [ - shade.OpenStackCloud(cloud_config=cloud_config) - for cloud_config in config.get_all_clouds() - ] + if cloud is None: + self.clouds = [ + shade.OpenStackCloud(cloud_config=cloud_config) + for cloud_config in config.get_all_clouds() + ] + else: + try: + self.clouds = [ + shade.OpenStackCloud( + cloud_config=config.get_one_cloud(cloud)) + ] + except os_client_config.exceptions.OpenStackConfigException as e: + raise shade.OpenStackCloudException(e) if private: for cloud in self.clouds: diff --git a/shade/tests/unit/test_inventory.py b/shade/tests/unit/test_inventory.py index 88a2d12e2..205d6721a 100644 --- a/shade/tests/unit/test_inventory.py +++ b/shade/tests/unit/test_inventory.py @@ -16,7 +16,10 @@ import mock import os_client_config +from os_client_config import exceptions as occ_exc + from shade import _utils +from shade import exc from shade import inventory from shade import meta from shade.tests import fakes @@ -42,6 +45,38 @@ def test__init(self, mock_cloud, mock_config): self.assertEqual(1, len(inv.clouds)) self.assertTrue(mock_config.return_value.get_all_clouds.called) + @mock.patch("os_client_config.config.OpenStackConfig") + @mock.patch("shade.OpenStackCloud") + def test__init_one_cloud(self, mock_cloud, mock_config): + mock_config.return_value.get_one_cloud.return_value = [{}] + + inv = inventory.OpenStackInventory(cloud='supercloud') + + mock_config.assert_called_once_with( + config_files=os_client_config.config.CONFIG_FILES + ) + self.assertIsInstance(inv.clouds, list) + self.assertEqual(1, len(inv.clouds)) + self.assertFalse(mock_config.return_value.get_all_clouds.called) + mock_config.return_value.get_one_cloud.assert_called_once_with( + 'supercloud') + + @mock.patch("os_client_config.config.OpenStackConfig") + @mock.patch("shade.OpenStackCloud") + def test__raise_exception_on_no_cloud(self, mock_cloud, mock_config): + """ + Test that when os-client-config can't find a named cloud, a + shade exception is emitted. + """ + mock_config.return_value.get_one_cloud.side_effect = ( + occ_exc.OpenStackConfigException() + ) + self.assertRaises(exc.OpenStackCloudException, + inventory.OpenStackInventory, + cloud='supercloud') + mock_config.return_value.get_one_cloud.assert_called_once_with( + 'supercloud') + @mock.patch("os_client_config.config.OpenStackConfig") @mock.patch("shade.OpenStackCloud") def test_list_hosts(self, mock_cloud, mock_config): From cd5f16cc4d78fde5a812e2715ee9db430760972f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 8 Jan 2016 20:50:35 -0500 Subject: [PATCH 0769/3836] Pass version arg by name not position Everyone except neutron has a first parameter called "version" - so we can pass it by name. For neutron, add a workaround, becuase YAY people being different. Change-Id: Icfd92e5e31763ffccc1ff673298f89d1888941fe --- os_client_config/cloud_config.py | 9 ++++----- os_client_config/tests/test_cloud_config.py | 18 +++++++++--------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 3b9bee98f..85c6f2a0b 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -335,7 +335,6 @@ def get_legacy_client( constructor_kwargs['endpoint_override'] = endpoint constructor_kwargs.update(kwargs) constructor_kwargs[interface_key] = interface - constructor_args = [] if pass_version_arg: if not version: version = self.get_api_version(service_key) @@ -348,12 +347,12 @@ def get_legacy_client( if 'endpoint' not in constructor_kwargs: endpoint = self.get_session_endpoint('identity') constructor_kwargs['endpoint'] = endpoint - if service_key == 'key-manager': - constructor_kwargs['version'] = version + if service_key == 'network': + constructor_kwargs['api_version'] = version else: - constructor_args.append(version) + constructor_kwargs['version'] = version - return client_class(*constructor_args, **constructor_kwargs) + return client_class(**constructor_kwargs) def _get_swift_client(self, client_class, **kwargs): session = self.get_session() diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index 9d6011120..a01d0e1b5 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -304,7 +304,7 @@ def test_legacy_client_image(self, mock_get_session_endpoint): "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('image', mock_client) mock_client.assert_called_with( - 2.0, + version=2.0, service_name=None, endpoint_override='http://example.com', region_name='region-al', @@ -325,7 +325,7 @@ def test_legacy_client_image_override(self, mock_get_session_endpoint): "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('image', mock_client) mock_client.assert_called_with( - 2.0, + version=2.0, service_name=None, endpoint_override='http://example.com/override', region_name='region-al', @@ -347,7 +347,7 @@ def test_legacy_client_image_versioned(self, mock_get_session_endpoint): "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('image', mock_client) mock_client.assert_called_with( - 2.0, + version=2.0, service_name=None, endpoint_override='http://example.com', region_name='region-al', @@ -369,7 +369,7 @@ def test_legacy_client_image_unversioned(self, mock_get_session_endpoint): "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('image', mock_client) mock_client.assert_called_with( - '1', + version='1', service_name=None, endpoint_override='http://example.com', region_name='region-al', @@ -391,7 +391,7 @@ def test_legacy_client_image_argument(self, mock_get_session_endpoint): "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('image', mock_client, version='beef') mock_client.assert_called_with( - 'beef', + version='beef', service_name=None, endpoint_override='http://example.com', region_name='region-al', @@ -411,7 +411,7 @@ def test_legacy_client_network(self, mock_get_session_endpoint): "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('network', mock_client) mock_client.assert_called_with( - '2.0', + api_version='2.0', endpoint_type='public', endpoint_override=None, region_name='region-al', @@ -429,7 +429,7 @@ def test_legacy_client_compute(self, mock_get_session_endpoint): "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('compute', mock_client) mock_client.assert_called_with( - '2', + version='2', endpoint_type='public', endpoint_override='http://compute.example.com', region_name='region-al', @@ -447,7 +447,7 @@ def test_legacy_client_identity(self, mock_get_session_endpoint): "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('identity', mock_client) mock_client.assert_called_with( - '2.0', + version='2.0', endpoint='http://example.com/v2', endpoint_type='admin', endpoint_override=None, @@ -467,7 +467,7 @@ def test_legacy_client_identity_v3(self, mock_get_session_endpoint): "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('identity', mock_client) mock_client.assert_called_with( - '3', + version='3', endpoint='http://example.com', endpoint_type='admin', endpoint_override=None, From 7e5496763522475bb07a377359d69454f1942e1b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 4 Jan 2016 12:56:28 -0600 Subject: [PATCH 0770/3836] Return empty dict instead of None for lack of file We return None for the file content for non-existent files as a fallback. This is normally fine, but in the case of a person having _only_ a secure.conf file, this means that the dictionary merge fails. Change-Id: I61cc0a8c709ea3510428fc3dfce63dc254c07c83 --- os_client_config/config.py | 2 +- os_client_config/tests/test_config.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index d490006b4..d3663077c 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -315,7 +315,7 @@ def _load_yaml_json_file(self, filelist): return path, json.load(f) else: return path, yaml.safe_load(f) - return (None, None) + return (None, {}) def _normalize_keys(self, config): new_config = {} diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 4440ac8e1..dce436af6 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -171,6 +171,13 @@ def test_get_one_cloud_auth_merge(self): self.assertEqual('user', cc.auth['username']) self.assertEqual('testpass', cc.auth['password']) + def test_only_secure_yaml(self): + c = config.OpenStackConfig(config_files=['nonexistent'], + vendor_files=['nonexistent'], + secure_files=[self.secure_yaml]) + cc = c.get_one_cloud(cloud='_test_cloud_no_vendor') + self.assertEqual('testpass', cc.auth['password']) + def test_get_cloud_names(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], secure_files=[self.no_yaml]) From a8532f6c8d221628b697ddb0d134e2a000ef61d6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 13 Jan 2016 13:37:14 -0500 Subject: [PATCH 0771/3836] Fix a precedence problem with auth arguments With the current code, OS_TENANT_NAME will take precednece over --os-project-name beause OS_TENANT_NAME gets early-moved to config['auth']['project_name'], then when the argparse value gets put into config['project_name'] the auth fixing sees auth['project_name'] and thinks it should win. Change-Id: I97084ea221eb963f14d98cf550a04bbd5c7d954c --- os_client_config/config.py | 4 +++- os_client_config/tests/test_config.py | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index d3663077c..7e56f61b8 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -447,7 +447,7 @@ def _get_base_cloud_config(self, name): if 'cloud' in cloud: del cloud['cloud'] - return self._fix_backwards_madness(cloud) + return cloud def _expand_vendor_profile(self, name, cloud, our_cloud): # Expand a profile if it exists. 'cloud' is an old confusing name @@ -896,6 +896,8 @@ def get_one_cloud(self, cloud=None, validate=True, if 'endpoint_type' in config: config['interface'] = config.pop('endpoint_type') + config = self._fix_backwards_madness(config) + for key in BOOL_KEYS: if key in config: if type(config[key]) is not bool: diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index dce436af6..1a16bd8ce 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -473,6 +473,16 @@ def test_register_argparse_cloud(self): opts, _remain = parser.parse_known_args(['--os-cloud', 'foo']) self.assertEqual(opts.os_cloud, 'foo') + def test_env_argparse_precedence(self): + self.useFixture(fixtures.EnvironmentVariable( + 'OS_TENANT_NAME', 'tenants-are-bad')) + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + + cc = c.get_one_cloud( + cloud='envvars', argparse=self.options) + self.assertEqual(cc.auth['project_name'], 'project') + def test_argparse_default_no_token(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) From 25061db5ae6ea9dc98fc26f49cdb378d94b6e190 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 14 Jan 2016 15:28:58 -0500 Subject: [PATCH 0772/3836] Fix unit tests that validate client call arguments Some of our unit tests are trying to validate how the underlying client is called. That is os-client-config's responsibility. Let's just validate how we are calling occ's API so that we don't break every time occ decides to change client call arguments. Change-Id: I2ceab4b09466e3f7d3e8e6dc75587279f5eb80e4 --- shade/tests/unit/test_shade.py | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 83349308f..30b10ed96 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -73,35 +73,30 @@ def test_list_servers_exception(self, mock_client): self.cloud.list_servers) @mock.patch.object(cloud_config.CloudConfig, 'get_session') - @mock.patch.object(glanceclient, 'Client') - def test_glance_args( - self, mock_client, get_session_mock): + @mock.patch.object(cloud_config.CloudConfig, 'get_legacy_client') + def test_glance_args(self, get_legacy_client_mock, get_session_mock): session_mock = mock.Mock() session_mock.get_endpoint.return_value = 'http://example.com/v2' get_session_mock.return_value = session_mock self.cloud.glance_client - mock_client.assert_called_with( - 2.0, - endpoint_override='http://example.com', - region_name='', service_name=None, - interface='public', - service_type='image', session=mock.ANY, + get_legacy_client_mock.assert_called_once_with( + service_key='image', + client_class=glanceclient.Client, + interface_key=None, + pass_version_arg=True, ) @mock.patch.object(cloud_config.CloudConfig, 'get_session') - @mock.patch.object(heat_client, 'Client') - def test_heat_args(self, mock_client, get_session_mock): + @mock.patch.object(cloud_config.CloudConfig, 'get_legacy_client') + def test_heat_args(self, get_legacy_client_mock, get_session_mock): session_mock = mock.Mock() get_session_mock.return_value = session_mock self.cloud.heat_client - mock_client.assert_called_with( - '1', - endpoint_override=None, - endpoint_type='public', - region_name='', - service_name=None, - service_type='orchestration', - session=mock.ANY, + get_legacy_client_mock.assert_called_once_with( + service_key='orchestration', + client_class=heat_client.Client, + interface_key=None, + pass_version_arg=True, ) @mock.patch.object(shade.OpenStackCloud, 'neutron_client') From 4cefd93ca531d5eb2eb3783126d5185c067d5126 Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Tue, 12 Jan 2016 13:03:08 -0600 Subject: [PATCH 0773/3836] Save the adminPass if returned on server create I am using the or, because if the server returns a None in the adminPass field, but the admin_pass is set in kwargs, we should still want that password, but a default on the .get in python wouldn't return it cause the key is actually set to None. Add admin_pass to rebuild_server Change-Id: Iff3242ef916180018c3d878942a7af14988165b7 --- shade/openstackcloud.py | 11 ++++- shade/tests/fakes.py | 4 +- shade/tests/functional/test_compute.py | 15 +++++++ shade/tests/unit/test_create_server.py | 60 +++++++++++++++++++++++++ shade/tests/unit/test_rebuild_server.py | 39 ++++++++++++++++ 5 files changed, 126 insertions(+), 3 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 3804cce87..4e00ac947 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3267,6 +3267,7 @@ def create_server( server = self.manager.submitTask(_tasks.ServerCreate( name=name, flavor=flavor, **kwargs)) server_id = server.id + admin_pass = server.get('adminPass') or kwargs.get('admin_pass') if not wait: # This is a direct get task call to skip the list_servers # cache which has absolutely no chance of containing the @@ -3300,7 +3301,10 @@ def create_server( auto_ip=auto_ip, ips=ips, ip_pool=ip_pool, wait=wait, timeout=timeout) if server: + server.adminPass = admin_pass return server + + server.adminPass = admin_pass return server def get_active_server( @@ -3341,11 +3345,13 @@ def get_active_server( extra_data=dict(server=server)) return None - def rebuild_server(self, server_id, image_id, wait=False, timeout=180): + def rebuild_server(self, server_id, image_id, admin_pass=None, + wait=False, timeout=180): with _utils.shade_exceptions("Error in rebuilding instance"): server = self.manager.submitTask(_tasks.ServerRebuild( - server=server_id, image=image_id)) + server=server_id, image=image_id, password=admin_pass)) if wait: + admin_pass = server.get('adminPass') or admin_pass for count in _utils._iterate_timeout( timeout, "Timeout waiting for server {0} to " @@ -3356,6 +3362,7 @@ def rebuild_server(self, server_id, image_id, wait=False, timeout=180): continue if server['status'] == 'ACTIVE': + server.adminPass = admin_pass return server if server['status'] == 'ERROR': diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index c3bf28e31..45229d962 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -67,7 +67,8 @@ def __init__(self, id): class FakeServer(object): def __init__( self, id, name, status, addresses=None, - accessIPv4='', accessIPv6='', flavor=None, image=None): + accessIPv4='', accessIPv6='', flavor=None, image=None, + adminPass=None): self.id = id self.name = name self.status = status @@ -80,6 +81,7 @@ def __init__( self.image = image self.accessIPv4 = accessIPv4 self.accessIPv6 = accessIPv6 + self.adminPass = adminPass class FakeService(object): diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index 1c62dcc59..e666b4078 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -62,6 +62,21 @@ def test_create_and_delete_server(self): self.assertEqual(self.server_name, server['name']) self.assertEqual(self.image.id, server['image']['id']) self.assertEqual(self.flavor.id, server['flavor']['id']) + self.assertIsNotNone(server['adminPass']) + self.assertTrue(self.cloud.delete_server(self.server_name, wait=True)) + self.assertIsNone(self.cloud.get_server(self.server_name)) + + def test_create_and_delete_server_with_admin_pass(self): + self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) + server = self.cloud.create_server(name=self.server_name, + image=self.image, + flavor=self.flavor, + admin_pass='sheiqu9loegahSh', + wait=True) + self.assertEqual(self.server_name, server['name']) + self.assertEqual(self.image.id, server['image']['id']) + self.assertEqual(self.flavor.id, server['flavor']['id']) + self.assertEqual(server['adminPass'], 'sheiqu9loegahSh') self.assertTrue(self.cloud.delete_server(self.server_name, wait=True)) self.assertIsNone(self.cloud.get_server(self.server_name)) diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index 42ed3c1ea..c0570f373 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -142,6 +142,66 @@ def test_create_server_no_wait(self): name='server-name', image='image=id', flavor='flavor-id')) + def test_create_server_with_admin_pass_no_wait(self): + """ + Test that a server with an admin_pass passed returns the password + """ + with patch("shade.OpenStackCloud"): + fake_server = fakes.FakeServer('1234', '', 'BUILD') + fake_create_server = fakes.FakeServer('1234', '', 'BUILD', + adminPass='ooBootheiX0edoh') + config = { + "servers.create.return_value": fake_create_server, + "servers.get.return_value": fake_server + } + OpenStackCloud.nova_client = Mock(**config) + self.assertEqual( + _utils.normalize_server( + meta.obj_to_dict(fake_create_server), + cloud_name=self.client.name, + region_name=self.client.region_name), + self.client.create_server( + name='server-name', image='image=id', + flavor='flavor-id', admin_pass='ooBootheiX0edoh')) + + def test_create_server_with_admin_pass_wait(self): + """ + Test that a server with an admin_pass passed returns the password + """ + with patch("shade.OpenStackCloud"): + build_server = fakes.FakeServer( + '1234', '', 'BUILD', addresses=dict(public='1.1.1.1'), + adminPass='ooBootheiX0edoh') + next_server = fakes.FakeServer( + '1234', '', 'BUILD', addresses=dict(public='1.1.1.1')) + fake_server = fakes.FakeServer( + '1234', '', 'ACTIVE', addresses=dict(public='1.1.1.1')) + ret_fake_server = fakes.FakeServer( + '1234', '', 'ACTIVE', addresses=dict(public='1.1.1.1'), + adminPass='ooBootheiX0edoh') + config = { + "servers.create.return_value": build_server, + "servers.get.return_value": next_server, + "servers.list.side_effect": [ + [next_server], [fake_server]] + } + OpenStackCloud.nova_client = Mock(**config) + with patch.object(OpenStackCloud, "add_ips_to_server", + return_value=fake_server): + self.assertEqual( + _utils.normalize_server( + meta.obj_to_dict(ret_fake_server), + cloud_name=self.client.name, + region_name=self.client.region_name), + _utils.normalize_server( + meta.obj_to_dict( + self.client.create_server( + 'server-name', 'image-id', 'flavor-id', + wait=True, admin_pass='ooBootheiX0edoh')), + cloud_name=self.client.name, + region_name=self.client.region_name) + ) + def test_create_server_wait(self): """ Test that create_server with a wait returns the server instance when diff --git a/shade/tests/unit/test_rebuild_server.py b/shade/tests/unit/test_rebuild_server.py index a76ce188f..b87a4da75 100644 --- a/shade/tests/unit/test_rebuild_server.py +++ b/shade/tests/unit/test_rebuild_server.py @@ -96,6 +96,45 @@ def test_rebuild_server_no_wait(self): self.assertEqual(meta.obj_to_dict(rebuild_server), self.client.rebuild_server("a", "b")) + def test_rebuild_server_with_admin_pass_no_wait(self): + """ + Test that a server with an admin_pass passed returns the password + """ + with patch("shade.OpenStackCloud"): + rebuild_server = fakes.FakeServer('1234', '', 'REBUILD', + adminPass='ooBootheiX0edoh') + config = { + "servers.rebuild.return_value": rebuild_server, + } + OpenStackCloud.nova_client = Mock(**config) + self.assertEqual( + meta.obj_to_dict(rebuild_server), + self.client.rebuild_server('a', 'b', + admin_pass='ooBootheiX0edoh')) + + def test_rebuild_server_with_admin_pass_wait(self): + """ + Test that a server with an admin_pass passed returns the password + """ + with patch("shade.OpenStackCloud"): + rebuild_server = fakes.FakeServer('1234', '', 'REBUILD', + adminPass='ooBootheiX0edoh') + active_server = fakes.FakeServer('1234', '', 'ACTIVE') + ret_active_server = fakes.FakeServer('1234', '', 'ACTIVE', + adminPass='ooBootheiX0edoh') + config = { + "servers.rebuild.return_value": rebuild_server, + "servers.get.return_value": active_server, + } + OpenStackCloud.nova_client = Mock(**config) + self.client.name = 'cloud-name' + self.assertEqual( + _utils.normalize_server( + meta.obj_to_dict(ret_active_server), + cloud_name='cloud-name', region_name=''), + self.client.rebuild_server("a", "b", wait=True, + admin_pass='ooBootheiX0edoh')) + def test_rebuild_server_wait(self): """ Test that rebuild_server with a wait returns the server instance when From 2f1d184a8e6bed99d027b5ec9f8ee475e527cbdc Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Thu, 14 Jan 2016 17:10:21 +0000 Subject: [PATCH 0774/3836] set up release notes build Add release notes build files and tox environment so the existing release notes job has something to build from. Change-Id: I717d4e7af438cbc94eecf32472f6d1f8213761b8 --- releasenotes/source/_static/.placeholder | 0 releasenotes/source/_templates/.placeholder | 0 releasenotes/source/conf.py | 261 ++++++++++++++++++++ releasenotes/source/index.rst | 17 ++ releasenotes/source/unreleased.rst | 5 + tox.ini | 5 +- 6 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 releasenotes/source/_static/.placeholder create mode 100644 releasenotes/source/_templates/.placeholder create mode 100644 releasenotes/source/conf.py create mode 100644 releasenotes/source/index.rst create mode 100644 releasenotes/source/unreleased.rst diff --git a/releasenotes/source/_static/.placeholder b/releasenotes/source/_static/.placeholder new file mode 100644 index 000000000..e69de29bb diff --git a/releasenotes/source/_templates/.placeholder b/releasenotes/source/_templates/.placeholder new file mode 100644 index 000000000..e69de29bb diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py new file mode 100644 index 000000000..282dbd784 --- /dev/null +++ b/releasenotes/source/conf.py @@ -0,0 +1,261 @@ +# -*- coding: utf-8 -*- +# +# Os-Client-Config Release Notes documentation build configuration file, created by +# sphinx-quickstart on Thu Nov 5 11:50:32 2015. +# +# 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. +#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 = [ + 'oslosphinx', + 'reno.sphinxext', +] + +# 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'os-client-config Release Notes' +copyright = u'2015, os-client-config developers' + +# 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. +# +import pbr.version +occ_version = pbr.version.VersionInfo('os-client-config') +# The short X.Y version. +version = occ_version.canonical_version_string() +# The full version, including alpha/beta/rc tags. +release = occ_version.version_string_with_vcs() + +# 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 = [] + +# 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 = 'default' + +# 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 = [] + +# 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 '', 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 = 'OCCReleaseNotesdoc' + + +# -- 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', 'OCCReleaseNotes.tex', u'os-client-config Release Notes Documentation', + u'os-client-config 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', 'occreleasenotes', u'os-client-config Release Notes Documentation', + [u'os-client-config 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', 'OCCReleaseNotes', u'os-client-config Release Notes Documentation', + u'os-client-config developers', 'OCCReleaseNotes', + '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' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst new file mode 100644 index 000000000..386434ef8 --- /dev/null +++ b/releasenotes/source/index.rst @@ -0,0 +1,17 @@ +Welcome to Nova Release Notes documentation! +============================================== + +Contents +======== + +.. toctree:: + :maxdepth: 2 + + unreleased + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`search` diff --git a/releasenotes/source/unreleased.rst b/releasenotes/source/unreleased.rst new file mode 100644 index 000000000..875030f9d --- /dev/null +++ b/releasenotes/source/unreleased.rst @@ -0,0 +1,5 @@ +============================ +Current Series Release Notes +============================ + +.. release-notes:: diff --git a/tox.ini b/tox.ini index 95dff6ba4..617831dfe 100644 --- a/tox.ini +++ b/tox.ini @@ -28,6 +28,9 @@ commands = python setup.py build_sphinx python setup.py check -r -s +[testenv:releasenotes] +commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html + [flake8] # H803 skipped on purpose per list discussion. # E123, E125 skipped as they are invalid PEP-8. @@ -35,4 +38,4 @@ commands = show-source = True ignore = E123,E125,H803 builtins = _ -exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build +exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,releasenotes/source/conf.py From fde5cc0749fb2062c9aa5dc60d4d051e26ab3478 Mon Sep 17 00:00:00 2001 From: Hideki Saito Date: Thu, 14 Jan 2016 17:38:41 +0900 Subject: [PATCH 0775/3836] Support neutron subnets without gateway IPs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit create_subnet() and update_subnet() allows subnets without gateway IP. A new argument “disable_gateway_ip” controls it. Change-Id: I527af06e09bc2e654e2af4777126cad29f8670fe --- shade/openstackcloud.py | 28 ++++++++++++-- shade/tests/unit/test_shade.py | 67 ++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 3 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 4cd51526a..c6d053fcd 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3742,7 +3742,8 @@ def get_object_metadata(self, container, name): def create_subnet(self, network_name_or_id, cidr, ip_version=4, enable_dhcp=False, subnet_name=None, tenant_id=None, - allocation_pools=None, gateway_ip=None, + allocation_pools=None, + gateway_ip=None, disable_gateway_ip=False, dns_nameservers=None, host_routes=None, ipv6_ra_mode=None, ipv6_address_mode=None): """Create a subnet on a specified network. @@ -3777,6 +3778,10 @@ def create_subnet(self, network_name_or_id, cidr, ip_version=4, The gateway IP address. When you specify both allocation_pools and gateway_ip, you must ensure that the gateway IP does not overlap with the specified allocation pools. + :param bool disable_gateway_ip: + Set to ``True`` if gateway IP address is disabled and ``False`` if + enabled. It is not allowed with gateway_ip. + Default is ``False``. :param list dns_nameservers: A list of DNS name servers for the subnet. For example:: @@ -3812,6 +3817,10 @@ def create_subnet(self, network_name_or_id, cidr, ip_version=4, raise OpenStackCloudException( "Network %s not found." % network_name_or_id) + if disable_gateway_ip and gateway_ip: + raise OpenStackCloudException( + 'arg:disable_gateway_ip is not allowed with arg:gateway_ip') + # The body of the neutron message for the subnet we wish to create. # This includes attributes that are required or have defaults. subnet = { @@ -3830,6 +3839,8 @@ def create_subnet(self, network_name_or_id, cidr, ip_version=4, subnet['allocation_pools'] = allocation_pools if gateway_ip: subnet['gateway_ip'] = gateway_ip + if disable_gateway_ip: + subnet['gateway_ip'] = None if dns_nameservers: subnet['dns_nameservers'] = dns_nameservers if host_routes: @@ -3872,8 +3883,9 @@ def delete_subnet(self, name_or_id): return True def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, - gateway_ip=None, allocation_pools=None, - dns_nameservers=None, host_routes=None): + gateway_ip=None, disable_gateway_ip=None, + allocation_pools=None, dns_nameservers=None, + host_routes=None): """Update an existing subnet. :param string name_or_id: @@ -3886,6 +3898,10 @@ def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, The gateway IP address. When you specify both allocation_pools and gateway_ip, you must ensure that the gateway IP does not overlap with the specified allocation pools. + :param bool disable_gateway_ip: + Set to ``True`` if gateway IP address is disabled and ``False`` if + enabled. It is not allowed with gateway_ip. + Default is ``False``. :param list allocation_pools: A list of dictionaries of the start and end addresses for the allocation pools. For example:: @@ -3926,6 +3942,8 @@ def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, subnet['enable_dhcp'] = enable_dhcp if gateway_ip: subnet['gateway_ip'] = gateway_ip + if disable_gateway_ip: + subnet['gateway_ip'] = None if allocation_pools: subnet['allocation_pools'] = allocation_pools if dns_nameservers: @@ -3937,6 +3955,10 @@ def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, self.log.debug("No subnet data to update") return + if disable_gateway_ip and gateway_ip: + raise OpenStackCloudException( + 'arg:disable_gateway_ip is not allowed with arg:gateway_ip') + curr_subnet = self.get_subnet(name_or_id) if not curr_subnet: raise OpenStackCloudException( diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 8602da98b..eeff62e27 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -341,6 +341,44 @@ def test_create_subnet(self, mock_client, mock_search): host_routes=routes) self.assertTrue(mock_client.create_subnet.called) + @mock.patch.object(shade.OpenStackCloud, 'search_networks') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_subnet_without_gateway_ip(self, mock_client, mock_search): + net1 = dict(id='123', name='donald') + mock_search.return_value = [net1] + pool = [{'start': '192.168.200.2', 'end': '192.168.200.254'}] + dns = ['8.8.8.8'] + self.cloud.create_subnet('kooky', '192.168.200.0/24', + allocation_pools=pool, + dns_nameservers=dns, + disable_gateway_ip=True) + self.assertTrue(mock_client.create_subnet.called) + + @mock.patch.object(shade.OpenStackCloud, 'search_networks') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_subnet_with_gateway_ip(self, mock_client, mock_search): + net1 = dict(id='123', name='donald') + mock_search.return_value = [net1] + pool = [{'start': '192.168.200.8', 'end': '192.168.200.254'}] + dns = ['8.8.8.8'] + gateway = '192.168.200.2' + self.cloud.create_subnet('kooky', '192.168.200.0/24', + allocation_pools=pool, + dns_nameservers=dns, + gateway_ip=gateway) + self.assertTrue(mock_client.create_subnet.called) + + @mock.patch.object(shade.OpenStackCloud, 'search_networks') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_subnet_conflict_gw_ops(self, mock_client, mock_search): + net1 = dict(id='123', name='donald') + mock_search.return_value = [net1] + gateway = '192.168.200.3' + self.assertRaises(exc.OpenStackCloudException, + self.cloud.create_subnet, 'kooky', + '192.168.200.0/24', gateway_ip=gateway, + disable_gateway_ip=True) + @mock.patch.object(shade.OpenStackCloud, 'list_networks') @mock.patch.object(shade.OpenStackCloud, 'neutron_client') def test_create_subnet_bad_network(self, mock_client, mock_list): @@ -406,6 +444,35 @@ def test_update_subnet(self, mock_client, mock_get): self.cloud.update_subnet('123', subnet_name='goofy') self.assertTrue(mock_client.update_subnet.called) + @mock.patch.object(shade.OpenStackCloud, 'get_subnet') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_update_subnet_gateway_ip(self, mock_client, mock_get): + subnet1 = dict(id='456', name='kooky') + mock_get.return_value = subnet1 + gateway = '192.168.200.3' + self.cloud.update_subnet( + '456', gateway_ip=gateway) + self.assertTrue(mock_client.update_subnet.called) + + @mock.patch.object(shade.OpenStackCloud, 'get_subnet') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_update_subnet_disable_gateway_ip(self, mock_client, mock_get): + subnet1 = dict(id='456', name='kooky') + mock_get.return_value = subnet1 + self.cloud.update_subnet( + '456', disable_gateway_ip=True) + self.assertTrue(mock_client.update_subnet.called) + + @mock.patch.object(shade.OpenStackCloud, 'get_subnet') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_update_subnet_conflict_gw_ops(self, mock_client, mock_get): + subnet1 = dict(id='456', name='kooky') + mock_get.return_value = subnet1 + gateway = '192.168.200.3' + self.assertRaises(exc.OpenStackCloudException, + self.cloud.update_subnet, + '456', gateway_ip=gateway, disable_gateway_ip=True) + @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_get_flavor_by_ram(self, mock_nova_client): From 72a3d64c1872987e0a0f78346345f66b24652b69 Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Fri, 15 Jan 2016 16:50:08 -0600 Subject: [PATCH 0776/3836] allow for updating passwords in keystone v2 Perform a users.update_password before throwing away the password from kwargs. Change-Id: Ie478b541a7958a888274eedcd0d2a135a08132c6 --- shade/_tasks.py | 5 +++++ shade/openstackcloud.py | 8 +++++++- shade/tests/functional/test_users.py | 21 +++++++++++++++++++++ shade/tests/unit/test_users.py | 25 +++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 1 deletion(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index dffda0042..6f72bdd2a 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -37,6 +37,11 @@ def main(self, client): return client.keystone_client.users.update(**self.args) +class UserPasswordUpdate(task_manager.Task): + def main(self, client): + return client.keystone_client.users.update_password(**self.args) + + class UserGet(task_manager.Task): def main(self, client): return client.keystone_client.users.get(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 3804cce87..8f7bea3fd 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -513,9 +513,15 @@ def update_user(self, name_or_id, **kwargs): if self.cloud_config.get_api_version('identity') != '3': # Do not pass v3 args to a v2 keystone. kwargs.pop('domain_id', None) - kwargs.pop('password', None) kwargs.pop('description', None) kwargs.pop('default_project', None) + password = kwargs.pop('password', None) + if password is not None: + with _utils.shade_exceptions( + "Error updating password for {user}".format( + user=name_or_id)): + user = self.manager.submitTask(_tasks.UserPasswordUpdate( + user=kwargs['user'], password=password)) elif 'domain_id' in kwargs: # The incoming parameter is domain_id in order to match the # parameter name in create_user(), but UserUpdate() needs it diff --git a/shade/tests/functional/test_users.py b/shade/tests/functional/test_users.py index 2167121c0..72cb38e11 100644 --- a/shade/tests/functional/test_users.py +++ b/shade/tests/functional/test_users.py @@ -108,6 +108,27 @@ def test_update_user(self): self.assertEqual('somebody@nowhere.com', new_user['email']) self.assertFalse(new_user['enabled']) + def test_update_user_password(self): + user_name = self.user_prefix + '_password' + user_email = 'nobody@nowhere.com' + user = self._create_user(name=user_name, + email=user_email, + password='old_secret') + self.assertIsNotNone(user) + self.assertTrue(user['enabled']) + + # This should work for both v2 and v3 + new_user = self.cloud.update_user(user['id'], + password='new_secret') + self.assertIsNotNone(new_user) + self.assertEqual(user['id'], new_user['id']) + self.assertEqual(user_name, new_user['name']) + self.assertEqual(user_email, new_user['email']) + self.assertTrue(new_user['enabled']) + self.assertIsNotNone(operator_cloud( + username=user_name, password='new_secret', + auth_url=self.cloud.auth['auth_url']).keystone_client) + def test_users_and_groups(self): if self.cloud.cloud_config.get_api_version('identity') in ('2', '2.0'): self.skipTest('Identity service does not support groups') diff --git a/shade/tests/unit/test_users.py b/shade/tests/unit/test_users.py index 65d3378d6..b75eb8c94 100644 --- a/shade/tests/unit/test_users.py +++ b/shade/tests/unit/test_users.py @@ -67,6 +67,31 @@ def test_create_user_v3(self, mock_keystone, mock_api_version): self.assertEqual(name, user.name) self.assertEqual(email, user.email) + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_update_user_password_v2(self, mock_keystone, mock_api_version): + mock_api_version.return_value = '2' + name = 'Mickey Mouse' + email = 'mickey@disney.com' + password = 'mice-rule' + domain_id = '1' + user = {'id': '1', 'name': name, 'email': email} + fake_user = fakes.FakeUser(**user) + munch_fake_user = munch.Munch(user) + mock_keystone.users.list.return_value = [fake_user] + mock_keystone.users.get.return_value = fake_user + mock_keystone.users.update.return_value = fake_user + mock_keystone.users.update_password.return_value = fake_user + user = self.cloud.update_user(name, name=name, email=email, + password=password, + domain_id=domain_id) + mock_keystone.users.update.assert_called_once_with( + user=munch_fake_user, name=name, email=email) + mock_keystone.users.update_password.assert_called_once_with( + user=munch_fake_user, password=password) + self.assertEqual(name, user.name) + self.assertEqual(email, user.email) + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_create_user_v3_no_domain(self, mock_keystone, mock_api_version): From cfd29196fedf41dcd61d0df6b0109dc8e43abfc8 Mon Sep 17 00:00:00 2001 From: LiuNanke Date: Thu, 14 Jan 2016 16:45:59 +0800 Subject: [PATCH 0777/3836] Clean up removed hacking rule from [flake8] ignore lists We bump hacking>=0.10.2, and hacking removed some rules, for the full list of rules please see [1]. So don't need them any more. Hacking related commits: Remove H904 in commit b1fe19ebebe47a36b905d709467f5e82521bbd96 Remove H803 in commit f01ce4fd822546cbd52a0aedc49184bddbfe1b10 Remove H307 in commit ec4833b206c23b0b6f9c6b101c70ab925a5e9c67 Remove H305 in commit 8f1fcbdb9aa4fc61349e5e879153c722195b1233 [1]https://github.com/openstack-dev/hacking/blob/master/setup.cfg#L30 Change-Id: I24b82c1913d3d42cc5228b1db700b787623fcdc5 --- test-requirements.txt | 2 +- tox.ini | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index a50a202e3..5e4c30446 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,7 +2,7 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. -hacking>=0.9.2,<0.10 +hacking>=0.10.2,<0.11 # Apache-2.0 coverage>=3.6 extras diff --git a/tox.ini b/tox.ini index 617831dfe..c3f42d6ac 100644 --- a/tox.ini +++ b/tox.ini @@ -32,10 +32,10 @@ commands = commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [flake8] -# H803 skipped on purpose per list discussion. # E123, E125 skipped as they are invalid PEP-8. show-source = True -ignore = E123,E125,H803 +ignore = E123,E125 builtins = _ exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,releasenotes/source/conf.py + From 05aacf133e8b13d825352fddd8930e9d2d3ba754 Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Tue, 19 Jan 2016 17:31:07 -0600 Subject: [PATCH 0778/3836] add the ability to get an object back from swift We allow swift to be used as a fileserver backend for saltstack, for the salt states and pillars, so we need to be able to get the objects out of the containers. Change-Id: I8fc9777807f111397bc942b02421b599f6c6a358 --- shade/_tasks.py | 5 +++++ shade/openstackcloud.py | 25 +++++++++++++++++++++++ shade/task_manager.py | 2 +- shade/tests/functional/test_object.py | 2 ++ shade/tests/unit/test_object.py | 29 +++++++++++++++++++++++++++ 5 files changed, 62 insertions(+), 1 deletion(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index dffda0042..8a68d7188 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -467,6 +467,11 @@ def main(self, client): return client.swift_client.head_object(**self.args) +class ObjectGet(task_manager.Task): + def main(self, client): + return client.swift_client.get_object(**self.args) + + class SubnetCreate(task_manager.Task): def main(self, client): return client.neutron_client.create_subnet(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 3804cce87..fdbf66c91 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3686,6 +3686,31 @@ def get_object_metadata(self, container, name): "Object metadata fetch failed: %s (%s/%s)" % ( e.http_reason, e.http_host, e.http_path)) + def get_object(self, container, obj, query_string=None, + resp_chunk_size=None): + """Get the headers and body of an object from swift + + :param string container: name of the container. + :param string obj: name of the object. + :param string query_string: query args for uri. + (delimiter, prefix, etc.) + :param int resp_chunk_size: chunk size of data to read. + + :returns: Tuple (headers, body) of the object, or None if the object + is not found (404) + :raises: OpenStackCloudException on operation error. + """ + try: + return self.manager.submitTask(_tasks.ObjectGet( + container=container, obj=obj, query_string=query_string, + resp_chunk_size=resp_chunk_size)) + except swift_exceptions.ClientException as e: + if e.http_status == 404: + return None + raise OpenStackCloudException( + "Object fetch failed: %s (%s/%s)" % ( + e.http_reason, e.http_host, e.http_path)) + def create_subnet(self, network_name_or_id, cidr, ip_version=4, enable_dhcp=False, subnet_name=None, tenant_id=None, allocation_pools=None, gateway_ip=None, diff --git a/shade/task_manager.py b/shade/task_manager.py index 4bcc85d05..e949e0f6f 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -82,7 +82,7 @@ def wait(self): if type(self._result) == list: return meta.obj_list_to_dict(self._result) elif type(self._result) not in (bool, int, float, str, set, - types.GeneratorType): + tuple, types.GeneratorType): return meta.obj_to_dict(self._result) else: return self._result diff --git a/shade/tests/functional/test_object.py b/shade/tests/functional/test_object.py index e2818238e..6413f46fd 100644 --- a/shade/tests/functional/test_object.py +++ b/shade/tests/functional/test_object.py @@ -64,6 +64,8 @@ def test_create_object(self): ) self.assertIsNotNone( self.cloud.get_object_metadata(container_name, name)) + self.assertIsNotNone( + self.cloud.get_object(container_name, name)) self.assertEqual( name, self.cloud.list_objects(container_name)[0]['name']) diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index b27edb32e..2132ad1a7 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -322,3 +322,32 @@ def test_delete_object_exception(self, mock_swift, mock_get_meta): self.assertRaises(shade.OpenStackCloudException, self.cloud.delete_object, container_name, object_name) + + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + def test_get_object(self, mock_swift): + fake_resp = ({'headers': 'yup'}, 'test body') + mock_swift.get_object.return_value = fake_resp + container_name = 'container_name' + object_name = 'object_name' + resp = self.cloud.get_object(container_name, object_name) + self.assertEqual(fake_resp, resp) + + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + def test_get_object_not_found(self, mock_swift): + mock_swift.get_object.side_effect = swift_exc.ClientException( + 'ERROR', http_status=404) + container_name = 'container_name' + object_name = 'object_name' + self.assertIsNone(self.cloud.get_object(container_name, object_name)) + mock_swift.get_object.assert_called_once_with( + container=container_name, obj=object_name, + query_string=None, resp_chunk_size=None) + + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + def test_get_object_exception(self, mock_swift): + mock_swift.get_object.side_effect = swift_exc.ClientException("ERROR") + container_name = 'container_name' + object_name = 'object_name' + self.assertRaises(shade.OpenStackCloudException, + self.cloud.get_object, + container_name, object_name) From 003f09412034d176c943d193ed94fc6d580d59ef Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 18 Jan 2016 09:50:04 -0500 Subject: [PATCH 0779/3836] Remove a done todo list item The TODO comment on "make this configurable" is confusing because it is, in fact, configurable. Remove the comment. Also, while doing that, put the default value somewhere sane, and change the variable name to match the config source better. Change-Id: I52c456f29395bfa84d9504e2a59d8390ffbd451a --- shade/openstackcloud.py | 19 ++++++++++++------- shade/tests/unit/test_create_server.py | 4 ++-- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 4cd51526a..242f2be2e 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -54,6 +54,7 @@ DEFAULT_OBJECT_SEGMENT_SIZE = 1073741824 # 1GB # This halves the current default for Swift DEFAULT_MAX_FILE_SIZE = (5 * 1024 * 1024 * 1024 + 2) / 2 +DEFAULT_SERVER_AGE = 5 OBJECT_CONTAINER_ACLS = { @@ -106,7 +107,6 @@ class OpenStackCloud(object): to pass in cloud configuration, but is being phased in currently. """ - _SERVER_LIST_AGE = 5 # TODO(mordred) Make this configurable def __init__( self, @@ -177,6 +177,7 @@ def __init__( cache_class, expiration_time=cache_expiration_time, arguments=cache_arguments) + self._SERVER_AGE = DEFAULT_SERVER_AGE else: def _fake_invalidate(unused): pass @@ -188,7 +189,7 @@ def invalidate(self): # Don't cache list_servers if we're not caching things. # Replace this with a more specific cache configuration # soon. - self._SERVER_LIST_AGE = 2 + self._SERVER_AGE = 0 self._cache = _FakeCache() # Undecorate cache decorated methods. Otherwise the call stacks # wind up being stupidly long and hard to debug @@ -204,8 +205,8 @@ def invalidate(self): # If server expiration time is set explicitly, use that. Otherwise # fall back to whatever it was before - self._SERVER_LIST_AGE = cloud_config.get_cache_resource_expiration( - 'server', self._SERVER_LIST_AGE) + self._SERVER_AGE = cloud_config.get_cache_resource_expiration( + 'server', self._SERVER_AGE) self._container_cache = dict() self._file_hash_cache = dict() @@ -1154,7 +1155,7 @@ def list_servers(self, detailed=False): :returns: A list of server dicts. """ - if (time.time() - self._servers_time) >= self._SERVER_LIST_AGE: + if (time.time() - self._servers_time) >= self._SERVER_AGE: # Since we're using cached data anyway, we don't need to # have more than one thread actually submit the list # servers task. Let the first one submit it while holding @@ -3333,7 +3334,7 @@ def create_server( for count in _utils._iterate_timeout( timeout, "Timeout waiting for the server to come up.", - wait=self._SERVER_LIST_AGE): + wait=self._SERVER_AGE): try: # Use the get_server call so that the list_servers # cache can be leveraged @@ -3478,7 +3479,8 @@ def _delete_server( for count in _utils._iterate_timeout( timeout, - "Timed out waiting for server to get deleted."): + "Timed out waiting for server to get deleted.", + wait=self._SERVER_AGE): try: server = self.get_server_by_id(server['id']) if not server: @@ -3498,6 +3500,9 @@ def _delete_server( or self.get_volume(server)): self.list_volumes.invalidate(self) + # Reset the list servers cache time so that the next list server + # call gets a new list + self._servers_time = self._servers_time - self._SERVER_AGE return True def list_containers(self, full_listing=True): diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index c0570f373..211ac977b 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -35,7 +35,7 @@ def setUp(self): config = os_client_config.OpenStackConfig() self.client = OpenStackCloud( cloud_config=config.get_one_cloud(validate=False)) - self.client._SERVER_LIST_AGE = 0 + self.client._SERVER_AGE = 0 def test_create_server_with_create_exception(self): """ @@ -243,7 +243,7 @@ def test_create_server_no_addresses(self, mock_sleep): "servers.delete.return_value": None, } OpenStackCloud.nova_client = Mock(**config) - self.client._SERVER_LIST_AGE = 0 + self.client._SERVER_AGE = 0 with patch.object(OpenStackCloud, "add_ips_to_server", return_value=fake_server): self.assertRaises( From a2db877b41fad494fe9daa09b5c77914638ac605 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 19 Jan 2016 10:12:02 -0500 Subject: [PATCH 0780/3836] Don't set project_domain if not project scoped The code to expand domain_{name,id} to {user,project}_domain_{name,id} is flawed in that it sets a project_domain_{name,id} even if a project_{name,id} is not set. There is a valid use case for not having a project_{name,id} - specifically getting a domain-scoped token. In the case where we do not set a project, check for that and don't make further assumptions that the domain input needs to be "fixed". Closes-Bug: #1535676 Change-Id: I825fe4bc375687208bb176bb5990c23fe87c8f9d --- os_client_config/config.py | 9 +++++++++ os_client_config/tests/base.py | 9 +++++++++ os_client_config/tests/test_config.py | 17 +++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/os_client_config/config.py b/os_client_config/config.py index 378cd3b60..0444316ed 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -480,6 +480,11 @@ def _fix_backwards_madness(self, cloud): cloud = self._handle_domain_id(cloud) return cloud + def _project_scoped(self, cloud): + return ('project_id' in cloud or 'project_name' in cloud + or 'project_id' in cloud['auth'] + or 'project_name' in cloud['auth']) + def _handle_domain_id(self, cloud): # Allow people to just specify domain once if it's the same mappings = { @@ -487,6 +492,10 @@ def _handle_domain_id(self, cloud): 'domain_name': ('user_domain_name', 'project_domain_name'), } for target_key, possible_values in mappings.items(): + if not self._project_scoped(cloud): + if target_key in cloud and target_key not in cloud['auth']: + cloud['auth'][target_key] = cloud.pop(target_key) + continue for key in possible_values: if target_key in cloud['auth'] and key not in cloud['auth']: cloud['auth'][key] = cloud['auth'][target_key] diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py index 3f00f6d82..9b784b157 100644 --- a/os_client_config/tests/base.py +++ b/os_client_config/tests/base.py @@ -73,6 +73,7 @@ 'auth': { 'username': 'testuser', 'password': 'testpass', + 'domain_id': 'awesome-domain', 'project_id': 12345, 'auth_url': 'http://example.com/v2', }, @@ -128,6 +129,14 @@ 'password': 'testpass', }, }, + '_test-cloud-domain-scoped_': { + 'auth': { + 'auth_url': 'http://example.com/v2', + 'username': 'testuser', + 'password': 'testpass', + 'domain-id': '12345', + }, + }, }, 'ansible': { 'expand-hostvars': False, diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 1a16bd8ce..73be114a5 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -103,6 +103,22 @@ def test_get_one_cloud_with_domain_id(self): self.assertNotIn('domain_id', cc.auth) self.assertNotIn('domain-id', cc.auth) + def test_get_one_cloud_domain_scoped(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + cc = c.get_one_cloud('_test-cloud-domain-scoped_') + self.assertEqual('12345', cc.auth['domain_id']) + self.assertNotIn('user_domain_id', cc.auth) + self.assertNotIn('project_domain_id', cc.auth) + + def test_get_one_cloud_infer_user_domain(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + cc = c.get_one_cloud('_test-cloud-int-project_') + self.assertEqual('awesome-domain', cc.auth['user_domain_id']) + self.assertEqual('awesome-domain', cc.auth['project_domain_id']) + self.assertNotIn('domain_id', cc.auth) + def test_get_one_cloud_with_hyphenated_project_id(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) @@ -183,6 +199,7 @@ def test_get_cloud_names(self): secure_files=[self.no_yaml]) self.assertEqual( ['_test-cloud-domain-id_', + '_test-cloud-domain-scoped_', '_test-cloud-int-project_', '_test-cloud_', '_test-cloud_no_region', From d7ffabca74872941ef244281000d5356a2626796 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 21 Jan 2016 17:31:15 -0500 Subject: [PATCH 0781/3836] Fix normalize_role_assignments() return value This function was returning plain dicts, not Munch objects, so it was acting differently from all other normalization funtions. Change-Id: Iaf9325d509f9c2c015f9c3fbd2d4ec6efa974429 --- .../notes/norm_role_assignments-a13f41768e62d40c.yaml | 4 ++++ shade/_utils.py | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/norm_role_assignments-a13f41768e62d40c.yaml diff --git a/releasenotes/notes/norm_role_assignments-a13f41768e62d40c.yaml b/releasenotes/notes/norm_role_assignments-a13f41768e62d40c.yaml new file mode 100644 index 000000000..39ee2765d --- /dev/null +++ b/releasenotes/notes/norm_role_assignments-a13f41768e62d40c.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - Role assignments were being returned as plain dicts instead of Munch objects. + This has been corrected. diff --git a/shade/_utils.py b/shade/_utils.py index b5811d8fb..7424862e9 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -14,6 +14,7 @@ import contextlib import inspect +import munch import netifaces import re import six @@ -437,7 +438,7 @@ def normalize_role_assignments(assignments): """ new_assignments = [] for assignment in assignments: - new_val = {'id': assignment['role']['id']} + new_val = munch.Munch({'id': assignment['role']['id']}) for scope in ('project', 'domain'): if scope in assignment['scope']: new_val[scope] = assignment['scope'][scope]['id'] From f2ef884a8107608ae109f1b072bd6d1d0f23aa6e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 20 Jan 2016 08:47:30 -0500 Subject: [PATCH 0782/3836] Pass timeout through to floating ip creation When we do create server, we have a timeout from the user for the action. However, in auto_ip, the action can also mean creating a floating ip which can timeout of its own accord. Pass in the remaining time. Change-Id: Ifffdb1d9c34235f5a1fa3b382b9820a579d5c37a --- shade/openstackcloud.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 242f2be2e..58cd7ff84 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3330,10 +3330,12 @@ def create_server( raise OpenStackCloudException( "Error in creating the server.") if wait: + timeout_message = "Timeout waiting for the server to come up." + start_time = time.time() # There is no point in iterating faster than the list_servers cache for count in _utils._iterate_timeout( timeout, - "Timeout waiting for the server to come up.", + timeout_message, wait=self._SERVER_AGE): try: # Use the get_server call so that the list_servers @@ -3344,10 +3346,17 @@ def create_server( if not server: continue + # We have more work to do, but the details of that are + # hidden from the user. So, calculate remaining timeout + # and pass it down into the IP stack. + remaining_timeout = timeout - int(time.time() - start_time) + if remaining_timeout <= 0: + raise OpenStackCloudTimeout(timeout_message) + server = self.get_active_server( server=server, reuse=reuse_ips, auto_ip=auto_ip, ips=ips, ip_pool=ip_pool, - wait=wait, timeout=timeout) + wait=wait, timeout=remaining_timeout) if server: server.adminPass = admin_pass return server @@ -3372,7 +3381,8 @@ def get_active_server( if server['status'] == 'ACTIVE': if 'addresses' in server and server['addresses']: return self.add_ips_to_server( - server, auto_ip, ips, ip_pool, reuse=reuse, wait=wait) + server, auto_ip, ips, ip_pool, reuse=reuse, + wait=wait, timeout=timeout) self.log.debug( 'Server {server} reached ACTIVE state without' From c2058d5fa88b33ddfd14df24d6b1c8561762c2a1 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 22 Jan 2016 09:56:06 -0500 Subject: [PATCH 0783/3836] Add release note for new get_object() API call Change-Id: Ib5ed7a8f1e47756f5c9c0ba60fae9f095e55181a --- releasenotes/notes/get_object_api-968483adb016bce1.yaml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 releasenotes/notes/get_object_api-968483adb016bce1.yaml diff --git a/releasenotes/notes/get_object_api-968483adb016bce1.yaml b/releasenotes/notes/get_object_api-968483adb016bce1.yaml new file mode 100644 index 000000000..bc830d57c --- /dev/null +++ b/releasenotes/notes/get_object_api-968483adb016bce1.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added a new API call, OpenStackCloud.get_object(), to download objects from swift. From b2deeefc8b6b328f5a796b7860d894efea6648bf Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Thu, 21 Jan 2016 13:26:00 -0600 Subject: [PATCH 0784/3836] include keystonev2 role assignments This will require that user is set with project possibly being None, otherwise we would have to query over all combinations of users and projects to find all the assignments, which I would say is unreasonable. project is not required by keystoneclient, but based on what I was told by someone in #openstack-keystone, it is highly discouraged to create roles and users and assignments without the project being involved, so we won't be allowing it. Change-Id: Id5b7b9fb44a9dbecb7488eb8f0ef30773efed6d2 --- ...ignments-keystone-v2-b127b12b4860f50c.yaml | 3 + shade/_tasks.py | 5 ++ shade/operatorcloud.py | 43 ++++++++++-- shade/tests/functional/test_identity.py | 8 +++ shade/tests/unit/test_identity_roles.py | 66 ++++++++++++++++++- 5 files changed, 118 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/list-role-assignments-keystone-v2-b127b12b4860f50c.yaml diff --git a/releasenotes/notes/list-role-assignments-keystone-v2-b127b12b4860f50c.yaml b/releasenotes/notes/list-role-assignments-keystone-v2-b127b12b4860f50c.yaml new file mode 100644 index 000000000..df0d96b3d --- /dev/null +++ b/releasenotes/notes/list-role-assignments-keystone-v2-b127b12b4860f50c.yaml @@ -0,0 +1,3 @@ +--- +features: + - Implement list_role_assignments for keystone v2, using roles_for_user. diff --git a/shade/_tasks.py b/shade/_tasks.py index 6f72bdd2a..66ef37028 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -687,6 +687,11 @@ def main(self, client): return client.keystone_client.role_assignments.list(**self.args) +class RolesForUser(task_manager.Task): + def main(self, client): + return client.keystone_client.roles.roles_for_user(**self.args) + + class StackList(task_manager.Task): def main(self, client): return client.heat_client.stacks.list() diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index c50202424..d83430837 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1286,6 +1286,31 @@ def get_role(self, name_or_id, filters=None): """ return _utils._get_entity(self.search_roles, name_or_id, filters) + def _keystone_v2_role_assignments(self, user, project=None, + role=None, **kwargs): + with _utils.shade_exceptions("Failed to list role assignments"): + roles = self.manager.submitTask( + _tasks.RolesForUser(user=user, tenant=project) + ) + ret = [] + for tmprole in roles: + if role is not None and role != tmprole.id: + continue + ret.append({ + 'role': { + 'id': tmprole.id + }, + 'scope': { + 'project': { + 'id': project, + } + }, + 'user': { + 'id': user, + } + }) + return ret + def list_role_assignments(self, filters=None): """List Keystone role assignments @@ -1304,6 +1329,9 @@ def list_role_assignments(self, filters=None): 'user' and 'group' are mutually exclusive, as are 'domain' and 'project'. + NOTE: For keystone v2, only user, project, and role are used. + Project and user are both required in filters. + :returns: a list of dicts containing the role assignment description. Contains the following attributes:: @@ -1317,10 +1345,17 @@ def list_role_assignments(self, filters=None): if not filters: filters = {} - with _utils.shade_exceptions("Failed to list role assignments"): - assignments = self.manager.submitTask( - _tasks.RoleAssignmentList(**filters) - ) + if self.cloud_config.get_api_version('identity').startswith('2'): + if filters.get('project') is None or filters.get('user') is None: + raise OpenStackCloudException( + "Must provide project and user for keystone v2" + ) + assignments = self._keystone_v2_role_assignments(**filters) + else: + with _utils.shade_exceptions("Failed to list role assignments"): + assignments = self.manager.submitTask( + _tasks.RoleAssignmentList(**filters) + ) return _utils.normalize_role_assignments(assignments) def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", diff --git a/shade/tests/functional/test_identity.py b/shade/tests/functional/test_identity.py index d50be33af..1ad582540 100644 --- a/shade/tests/functional/test_identity.py +++ b/shade/tests/functional/test_identity.py @@ -89,3 +89,11 @@ def test_list_role_assignments(self): assignments = self.cloud.list_role_assignments() self.assertIsInstance(assignments, list) self.assertTrue(len(assignments) > 0) + + def test_list_role_assignments_v2(self): + user = self.cloud.get_user('demo') + project = self.cloud.get_project('demo') + assignments = self.cloud.list_role_assignments( + filters={'user': user.id, 'project': project.id}) + self.assertIsInstance(assignments, list) + self.assertTrue(len(assignments) > 0) diff --git a/shade/tests/unit/test_identity_roles.py b/shade/tests/unit/test_identity_roles.py index dad7a1678..7a7af3363 100644 --- a/shade/tests/unit/test_identity_roles.py +++ b/shade/tests/unit/test_identity_roles.py @@ -14,6 +14,7 @@ import mock import testtools +import os_client_config as occ import shade from shade import meta from shade import _utils @@ -82,8 +83,10 @@ def test_delete_role(self, mock_keystone, mock_get): self.assertTrue(self.cloud.delete_role('1234')) self.assertTrue(mock_keystone.roles.delete.called) + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_list_role_assignments(self, mock_keystone): + def test_list_role_assignments(self, mock_keystone, mock_api_version): + mock_api_version.return_value = '3' mock_keystone.role_assignments.list.return_value = RAW_ROLE_ASSIGNMENTS ret = self.cloud.list_role_assignments() mock_keystone.role_assignments.list.assert_called_once_with() @@ -92,17 +95,74 @@ def test_list_role_assignments(self, mock_keystone): ) self.assertEqual(normalized_assignments, ret) + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_list_role_assignments_filters(self, mock_keystone): + def test_list_role_assignments_filters(self, mock_keystone, + mock_api_version): + mock_api_version.return_value = '3' params = dict(user='123', domain='456', effective=True) self.cloud.list_role_assignments(filters=params) mock_keystone.role_assignments.list.assert_called_once_with(**params) + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_list_role_assignments_exception(self, mock_keystone): + def test_list_role_assignments_exception(self, mock_keystone, + mock_api_version): + mock_api_version.return_value = '3' mock_keystone.role_assignments.list.side_effect = Exception() with testtools.ExpectedException( shade.OpenStackCloudException, "Failed to list role assignments" ): self.cloud.list_role_assignments() + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_list_role_assignments_keystone_v2(self, mock_keystone, + mock_api_version): + fake_role = fakes.FakeRole(id='1234', name='fake_role') + mock_api_version.return_value = '2.0' + mock_keystone.roles.roles_for_user.return_value = [fake_role] + ret = self.cloud.list_role_assignments(filters={'user': '2222', + 'project': '3333'}) + self.assertEqual(ret, [{'id': fake_role.id, + 'project': '3333', + 'user': '2222'}]) + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_list_role_assignments_keystone_v2_with_role(self, mock_keystone, + mock_api_version): + fake_role1 = fakes.FakeRole(id='1234', name='fake_role') + fake_role2 = fakes.FakeRole(id='4321', name='fake_role') + mock_api_version.return_value = '2.0' + mock_keystone.roles.roles_for_user.return_value = [fake_role1, + fake_role2] + ret = self.cloud.list_role_assignments(filters={'role': fake_role1.id, + 'user': '2222', + 'project': '3333'}) + self.assertEqual(ret, [{'id': fake_role1.id, + 'project': '3333', + 'user': '2222'}]) + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_list_role_assignments_exception_v2(self, mock_keystone, + mock_api_version): + mock_api_version.return_value = '2.0' + with testtools.ExpectedException( + shade.OpenStackCloudException, + "Must provide project and user for keystone v2" + ): + self.cloud.list_role_assignments() + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_list_role_assignments_exception_v2_no_project(self, mock_keystone, + mock_api_version): + mock_api_version.return_value = '2.0' + with testtools.ExpectedException( + shade.OpenStackCloudException, + "Must provide project and user for keystone v2" + ): + self.cloud.list_role_assignments(filters={'user': '12345'}) From ae8f4b65e3ac460b7764f2cdf9dfdcfe41ee0d22 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 17 Jan 2016 09:08:27 -0500 Subject: [PATCH 0785/3836] Go ahead and remove final excludes os-client-config is clean on E125 and E123 is ignored in recent pep8 by default. Also, even though they are not 'valid' pep8 rules, they are actually both nice styles and consistent with how we code os-client-config anyway. Change-Id: I7764e1511ed580d37b9a0a8be6743a5fa50441e5 --- tox.ini | 3 --- 1 file changed, 3 deletions(-) diff --git a/tox.ini b/tox.ini index c3f42d6ac..3df64e24c 100644 --- a/tox.ini +++ b/tox.ini @@ -32,10 +32,7 @@ commands = commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [flake8] -# E123, E125 skipped as they are invalid PEP-8. - show-source = True -ignore = E123,E125 builtins = _ exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,releasenotes/source/conf.py From cd9d0f3ab51662432ef21c3c2929b9ee0aa33669 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 25 Jan 2016 10:48:02 -0500 Subject: [PATCH 0786/3836] Add release note for FIP timeout fix. Change-Id: I28913650828084720b83e36371c0439ffb39402b --- releasenotes/notes/fip_timeout-035c4bb3ff92fa1f.yaml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 releasenotes/notes/fip_timeout-035c4bb3ff92fa1f.yaml diff --git a/releasenotes/notes/fip_timeout-035c4bb3ff92fa1f.yaml b/releasenotes/notes/fip_timeout-035c4bb3ff92fa1f.yaml new file mode 100644 index 000000000..2f98ebbda --- /dev/null +++ b/releasenotes/notes/fip_timeout-035c4bb3ff92fa1f.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - When creating a new server, the timeout was not being passed through to + floating IP creation, which could also timeout. From 42727a5e182eade19f4007195ee9058e56ba27bc Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 23 Jan 2016 13:03:55 -0500 Subject: [PATCH 0787/3836] Stop ignoring v2password plugin We have no codepaths that currently set v2password plugin by default. However, there are some cases, such as old clouds, where a user needs to explicitly set v2password as the auth_type to avoid version discovery because their cloud is old enough to not support it. If the user sets v2password, keep it and align the auth parameters the other direction to set tenant_name and tenant_id. Co-Authored-By: David Shrewsbury Change-Id: Ib9eb3ae163b79b67737d01868868187b6dee1756 --- os_client_config/config.py | 21 +++++++++----- os_client_config/tests/test_config.py | 40 +++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 0444316ed..1c47ed72a 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -474,8 +474,8 @@ def _expand_vendor_profile(self, name, cloud, our_cloud): name)) def _fix_backwards_madness(self, cloud): - cloud = self._fix_backwards_project(cloud) cloud = self._fix_backwards_auth_plugin(cloud) + cloud = self._fix_backwards_project(cloud) cloud = self._fix_backwards_interface(cloud) cloud = self._handle_domain_id(cloud) return cloud @@ -507,10 +507,6 @@ def _fix_backwards_project(self, cloud): # Also handle moving domain names into auth so that domain mapping # is easier mappings = { - 'project_id': ('tenant_id', 'tenant-id', - 'project_id', 'project-id'), - 'project_name': ('tenant_name', 'tenant-name', - 'project_name', 'project-name'), 'domain_id': ('domain_id', 'domain-id'), 'domain_name': ('domain_name', 'domain-name'), 'user_domain_id': ('user_domain_id', 'user-domain-id'), @@ -520,6 +516,19 @@ def _fix_backwards_project(self, cloud): 'project_domain_name', 'project-domain-name'), 'token': ('auth-token', 'auth_token', 'token'), } + if cloud.get('auth_type', None) == 'v2password': + # If v2password is explcitly requested, this is to deal with old + # clouds. That's fine - we need to map settings in the opposite + # direction + mappings['tenant_id'] = ( + 'project_id', 'project-id', 'tenant_id', 'tenant-id') + mappings['tenant_name'] = ( + 'project_name', 'project-name', 'tenant_name', 'tenant-name') + else: + mappings['project_id'] = ( + 'tenant_id', 'tenant-id', 'project_id', 'project-id') + mappings['project_name'] = ( + 'tenant_name', 'tenant-name', 'project_name', 'project-name') for target_key, possible_values in mappings.items(): target = None for key in possible_values: @@ -549,8 +558,6 @@ def _fix_backwards_auth_plugin(self, cloud): # use of the auth plugin that can do auto-selection and dealing # with that based on auth parameters. v2password is basically # completely broken - if cloud['auth_type'] == 'v2password': - cloud['auth_type'] = 'password' return cloud def register_argparse_arguments(self, parser, argv, service_keys=[]): diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 73be114a5..10a3d7b31 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -713,3 +713,43 @@ def test_set_no_default(self): 'auth_type': 'v3password', } self.assertDictEqual(expected, result) + + def test_project_v2password(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + cloud = { + 'auth_type': 'v2password', + 'auth': { + 'project-name': 'my_project_name', + 'project-id': 'my_project_id' + } + } + result = c._fix_backwards_project(cloud) + expected = { + 'auth_type': 'v2password', + 'auth': { + 'tenant_name': 'my_project_name', + 'tenant_id': 'my_project_id' + } + } + self.assertEqual(expected, result) + + def test_project_password(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + cloud = { + 'auth_type': 'password', + 'auth': { + 'project-name': 'my_project_name', + 'project-id': 'my_project_id' + } + } + result = c._fix_backwards_project(cloud) + expected = { + 'auth_type': 'password', + 'auth': { + 'project_name': 'my_project_name', + 'project_id': 'my_project_id' + } + } + self.assertEqual(expected, result) From fe2558a2d5b6a2fa8c2f3f3c5472b79a7e01ba4a Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Mon, 25 Jan 2016 20:37:22 -0500 Subject: [PATCH 0788/3836] Add support for zetta.io zetta has an openstack cloud, let's add support for it. Change-Id: I86cda3e42fff468786b2809bb367ad59241bb397 Closes-Bug: 1537959 --- doc/source/vendor-support.rst | 14 ++++++++++++++ os_client_config/vendors/zetta.json | 13 +++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 os_client_config/vendors/zetta.json diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index 46c95d8c4..e007b70f8 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -305,3 +305,17 @@ ca-ymq-1 Montreal * DNS API Version is 1 * Identity API Version is 3 + +zetta +----- + +https://identity.api.zetta.io/v3 + +============== ================ +Region Name Human Name +============== ================ +no-osl1 Oslo +============== ================ + +* DNS API Version is 2 +* Identity API Version is 3 diff --git a/os_client_config/vendors/zetta.json b/os_client_config/vendors/zetta.json new file mode 100644 index 000000000..44e9711ff --- /dev/null +++ b/os_client_config/vendors/zetta.json @@ -0,0 +1,13 @@ +{ + "name": "zetta", + "profile": { + "auth": { + "auth_url": "https://identity.api.zetta.io/v3" + }, + "regions": [ + "no-osl1" + ], + "identity_api_version": "3", + "dns_api_version": "2" + } +} From e38eea3d8cf8e440832893548c77f9feec1bf7d2 Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Fri, 15 Jan 2016 19:36:18 -0600 Subject: [PATCH 0789/3836] granting and revoking privs to users and groups The domain and groups are dropped for keystone v2. project is required for keystone v2 add a test that makes sure domains that don't exist raise an error Change-Id: I3313690c0f0bbf0fcd9fe1db2e46dcd3fb6dd3d0 --- ...t-revoke-assignments-231d3f9596a1ae75.yaml | 3 + shade/_tasks.py | 20 + shade/_utils.py | 11 + shade/operatorcloud.py | 155 ++++ shade/tests/fakes.py | 11 +- shade/tests/functional/test_identity.py | 159 +++- shade/tests/unit/test_role_assignment.py | 859 ++++++++++++++++++ 7 files changed, 1212 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/grant-revoke-assignments-231d3f9596a1ae75.yaml create mode 100644 shade/tests/unit/test_role_assignment.py diff --git a/releasenotes/notes/grant-revoke-assignments-231d3f9596a1ae75.yaml b/releasenotes/notes/grant-revoke-assignments-231d3f9596a1ae75.yaml new file mode 100644 index 000000000..9776030ca --- /dev/null +++ b/releasenotes/notes/grant-revoke-assignments-231d3f9596a1ae75.yaml @@ -0,0 +1,3 @@ +--- +features: + - add granting and revoking of roles from groups and users diff --git a/shade/_tasks.py b/shade/_tasks.py index 66ef37028..40d0463c0 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -682,6 +682,26 @@ def main(self, client): return client.keystone_client.roles.delete(**self.args) +class RoleAddUser(task_manager.Task): + def main(self, client): + return client.keystone_client.roles.add_user_role(**self.args) + + +class RoleGrantUser(task_manager.Task): + def main(self, client): + return client.keystone_client.roles.grant(**self.args) + + +class RoleRemoveUser(task_manager.Task): + def main(self, client): + return client.keystone_client.roles.remove_user_role(**self.args) + + +class RoleRevokeUser(task_manager.Task): + def main(self, client): + return client.keystone_client.roles.revoke(**self.args) + + class RoleAssignmentList(task_manager.Task): def main(self, client): return client.keystone_client.role_assignments.list(**self.args) diff --git a/shade/_utils.py b/shade/_utils.py index b5811d8fb..3f630e261 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -448,6 +448,17 @@ def normalize_role_assignments(assignments): return new_assignments +def normalize_roles(roles): + """Normalize Identity roles.""" + ret = [ + dict( + id=role.get('id'), + name=role.get('name'), + ) for role in roles + ] + return meta.obj_list_to_dict(ret) + + def valid_kwargs(*valid_args): # This decorator checks if argument passed as **kwargs to a function are # present in valid_args. diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index d83430837..9c30cf9c9 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1545,6 +1545,161 @@ def delete_role(self, name_or_id): return True + def _get_grant_revoke_params(self, role, user=None, group=None, + project=None, domain=None): + role = self.get_role(role) + if role is None: + return {} + data = {'role': role.id} + + # domain and group not available in keystone v2.0 + keystone_version = self.cloud_config.get_api_version('identity') + is_keystone_v2 = keystone_version.startswith('2') + + filters = {} + if not is_keystone_v2 and domain: + filters['domain_id'] = data['domain'] = \ + self.get_domain(domain)['id'] + + if user: + data['user'] = self.get_user(user, filters=filters) + + if project: + # drop domain in favor of project + data.pop('domain', None) + data['project'] = self.get_project(project, filters=filters) + + if not is_keystone_v2 and group: + data['group'] = self.get_group(group, filters=filters) + + return data + + def grant_role(self, name_or_id, user=None, group=None, + project=None, domain=None, wait=False, timeout=60): + """Grant a role to a user. + + :param string name_or_id: The name or id of the role. + :param string user: The name or id of the user. + :param string group: The name or id of the group. (v3) + :param string project: The name or id of the project. + :param string domain: The name or id of the domain. (v3) + :param bool wait: Wait for role to be granted + :param int timeout: Timeout to wait for role to be granted + + NOTE: for wait and timeout, sometimes granting roles is not + instantaneous for granting roles. + + NOTE: project is required for keystone v2 + + :returns: True if the role is assigned, otherwise False + + :raise OpenStackCloudException: if the role cannot be granted + """ + data = self._get_grant_revoke_params(name_or_id, user, group, + project, domain) + filters = data.copy() + if not data: + raise OpenStackCloudException( + 'Role {0} not found.'.format(name_or_id)) + + if data.get('user') is not None and data.get('group') is not None: + raise OpenStackCloudException( + 'Specify either a group or a user, not both') + if data.get('user') is None and data.get('group') is None: + raise OpenStackCloudException( + 'Must specify either a user or a group') + if self.cloud_config.get_api_version('identity').startswith('2') and \ + data.get('project') is None: + raise OpenStackCloudException( + 'Must specify project for keystone v2') + + if self.list_role_assignments(filters=filters): + self.log.debug('Assignment already exists') + return False + + with _utils.shade_exceptions( + "Error granting access to role: {0}".format( + data)): + if self.cloud_config.get_api_version('identity').startswith('2'): + data['tenant'] = data.pop('project') + self.manager.submitTask(_tasks.RoleAddUser(**data)) + else: + if data.get('project') is None and data.get('domain') is None: + raise OpenStackCloudException( + 'Must specify either a domain or project') + self.manager.submitTask(_tasks.RoleGrantUser(**data)) + if wait: + for count in _utils._iterate_timeout( + timeout, + "Timeout waiting for role to be granted"): + if self.list_role_assignments(filters=filters): + break + return True + + def revoke_role(self, name_or_id, user=None, group=None, + project=None, domain=None, wait=False, timeout=60): + """Revoke a role from a user. + + :param string name_or_id: The name or id of the role. + :param string user: The name or id of the user. + :param string group: The name or id of the group. (v3) + :param string project: The name or id of the project. + :param string domain: The id of the domain. (v3) + :param bool wait: Wait for role to be revoked + :param int timeout: Timeout to wait for role to be revoked + + NOTE: for wait and timeout, sometimes revoking roles is not + instantaneous for revoking roles. + + NOTE: project is required for keystone v2 + + :returns: True if the role is revoke, otherwise False + + :raise OpenStackCloudException: if the role cannot be removed + """ + data = self._get_grant_revoke_params(name_or_id, user, group, + project, domain) + filters = data.copy() + + if not data: + raise OpenStackCloudException( + 'Role {0} not found.'.format(name_or_id)) + + if data.get('user') is not None and data.get('group') is not None: + raise OpenStackCloudException( + 'Specify either a group or a user, not both') + if data.get('user') is None and data.get('group') is None: + raise OpenStackCloudException( + 'Must specify either a user or a group') + if self.cloud_config.get_api_version('identity').startswith('2') and \ + data.get('project') is None: + raise OpenStackCloudException( + 'Must specify project for keystone v2') + + if not self.list_role_assignments(filters=filters): + self.log.debug('Assignment does not exist') + return False + + with _utils.shade_exceptions( + "Error revoking access to role: {0}".format( + data)): + if self.cloud_config.get_api_version('identity').startswith('2'): + data['tenant'] = data.pop('project') + self.manager.submitTask(_tasks.RoleRemoveUser(**data)) + else: + if data.get('project') is None \ + and data.get('domain') is None: + raise OpenStackCloudException( + 'Must specify either a domain or project') + self.manager.submitTask(_tasks.RoleRevokeUser(**data)) + if wait: + for count in _utils._iterate_timeout( + timeout, + "Timeout waiting for role to be revoked"): + if not self.list_role_assignments(filters=filters): + break + return True + def list_hypervisors(self): """List all hypervisors diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 45229d962..b9ee2557e 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -60,8 +60,9 @@ def __init__(self, id, name, status): class FakeProject(object): - def __init__(self, id): + def __init__(self, id, domain_id=None): self.id = id + self.domain_id = domain_id or 'default' class FakeServer(object): @@ -94,10 +95,12 @@ def __init__(self, id, name, type, service_type, description=''): class FakeUser(object): - def __init__(self, id, email, name): + def __init__(self, id, email, name, domain_id=None): self.id = id self.email = email self.name = name + if domain_id is not None: + self.domain_id = domain_id class FakeVolume(object): @@ -194,11 +197,11 @@ def __init__(self, id, name): class FakeGroup(object): - def __init__(self, id, name, description, domain=None): + def __init__(self, id, name, description, domain_id=None): self.id = id self.name = name self.description = description - self.domain = domain + self.domain_id = domain_id or 'default' class FakeHypervisor(object): diff --git a/shade/tests/functional/test_identity.py b/shade/tests/functional/test_identity.py index 1ad582540..40dbd0b2b 100644 --- a/shade/tests/functional/test_identity.py +++ b/shade/tests/functional/test_identity.py @@ -33,8 +33,44 @@ def setUp(self): self.cloud = operator_cloud(cloud='devstack-admin') self.role_prefix = 'test_role' + ''.join( random.choice(string.ascii_lowercase) for _ in range(5)) + self.user_prefix = self.getUniqueString('user') + self.group_prefix = self.getUniqueString('group') + + self.addCleanup(self._cleanup_users) + self.identity_version = \ + self.cloud.cloud_config.get_api_version('identity') + if self.identity_version not in ('2', '2.0'): + self.addCleanup(self._cleanup_groups) self.addCleanup(self._cleanup_roles) + def _cleanup_groups(self): + exception_list = list() + for group in self.cloud.list_groups(): + if group['name'].startswith(self.group_prefix): + try: + self.cloud.delete_group(group['id']) + except Exception as e: + exception_list.append(str(e)) + continue + + if exception_list: + # Raise an error: we must make users aware that something went + # wrong + raise OpenStackCloudException('\n'.join(exception_list)) + + def _cleanup_users(self): + exception_list = list() + for user in self.cloud.list_users(): + if user['name'].startswith(self.user_prefix): + try: + self.cloud.delete_user(user['id']) + except Exception as e: + exception_list.append(str(e)) + continue + + if exception_list: + raise OpenStackCloudException('\n'.join(exception_list)) + def _cleanup_roles(self): exception_list = list() for role in self.cloud.list_roles(): @@ -48,6 +84,13 @@ def _cleanup_roles(self): if exception_list: raise OpenStackCloudException('\n'.join(exception_list)) + def _create_user(self, **kwargs): + domain_id = None + if self.identity_version not in ('2', '2.0'): + domain = self.cloud.get_domain('default') + domain_id = domain['id'] + return self.cloud.create_user(domain_id=domain_id, **kwargs) + def test_list_roles(self): roles = self.cloud.list_roles() self.assertIsNotNone(roles) @@ -84,7 +127,7 @@ def test_delete_role(self): # need to make this test a little more specific, and add more for testing # filtering functionality. def test_list_role_assignments(self): - if self.cloud.cloud_config.get_api_version('identity') in ('2', '2.0'): + if self.identity_version in ('2', '2.0'): self.skipTest("Identity service does not support role assignments") assignments = self.cloud.list_role_assignments() self.assertIsInstance(assignments, list) @@ -94,6 +137,118 @@ def test_list_role_assignments_v2(self): user = self.cloud.get_user('demo') project = self.cloud.get_project('demo') assignments = self.cloud.list_role_assignments( - filters={'user': user.id, 'project': project.id}) + filters={'user': user['id'], 'project': project['id']}) self.assertIsInstance(assignments, list) self.assertTrue(len(assignments) > 0) + + def test_grant_revoke_role_user_project(self): + user_name = self.user_prefix + '_user_project' + user_email = 'nobody@nowhere.com' + role_name = self.role_prefix + '_grant_user_project' + role = self.cloud.create_role(role_name) + user = self._create_user(name=user_name, + email=user_email, + default_project='demo') + self.assertTrue(self.cloud.grant_role( + role_name, user=user['id'], project='demo', wait=True)) + assignments = self.cloud.list_role_assignments({ + 'role': role['id'], + 'user': user['id'], + 'project': self.cloud.get_project('demo')['id'] + }) + self.assertIsInstance(assignments, list) + self.assertTrue(len(assignments) == 1) + self.assertTrue(self.cloud.revoke_role( + role_name, user=user['id'], project='demo', wait=True)) + assignments = self.cloud.list_role_assignments({ + 'role': role['id'], + 'user': user['id'], + 'project': self.cloud.get_project('demo')['id'] + }) + self.assertIsInstance(assignments, list) + self.assertTrue(len(assignments) == 0) + + def test_grant_revoke_role_group_project(self): + if self.identity_version in ('2', '2.0'): + self.skipTest("Identity service does not support group") + role_name = self.role_prefix + '_grant_group_project' + role = self.cloud.create_role(role_name) + group_name = self.group_prefix + '_group_project' + group = self.cloud.create_group(name=group_name, + description='test group', + domain='default') + self.assertTrue(self.cloud.grant_role( + role_name, group=group['id'], project='demo')) + assignments = self.cloud.list_role_assignments({ + 'role': role['id'], + 'group': group['id'], + 'project': self.cloud.get_project('demo')['id'] + }) + self.assertIsInstance(assignments, list) + self.assertTrue(len(assignments) == 1) + self.assertTrue(self.cloud.revoke_role( + role_name, group=group['id'], project='demo')) + assignments = self.cloud.list_role_assignments({ + 'role': role['id'], + 'group': group['id'], + 'project': self.cloud.get_project('demo')['id'] + }) + self.assertIsInstance(assignments, list) + self.assertTrue(len(assignments) == 0) + + def test_grant_revoke_role_user_domain(self): + if self.identity_version in ('2', '2.0'): + self.skipTest("Identity service does not support domain") + role_name = self.role_prefix + '_grant_user_domain' + role = self.cloud.create_role(role_name) + user_name = self.user_prefix + '_user_domain' + user_email = 'nobody@nowhere.com' + user = self._create_user(name=user_name, + email=user_email, + default_project='demo') + self.assertTrue(self.cloud.grant_role( + role_name, user=user['id'], domain='default')) + assignments = self.cloud.list_role_assignments({ + 'role': role['id'], + 'user': user['id'], + 'domain': self.cloud.get_domain('default')['id'] + }) + self.assertIsInstance(assignments, list) + self.assertTrue(len(assignments) == 1) + self.assertTrue(self.cloud.revoke_role( + role_name, user=user['id'], domain='default')) + assignments = self.cloud.list_role_assignments({ + 'role': role['id'], + 'user': user['id'], + 'domain': self.cloud.get_domain('default')['id'] + }) + self.assertIsInstance(assignments, list) + self.assertTrue(len(assignments) == 0) + + def test_grant_revoke_role_group_domain(self): + if self.identity_version in ('2', '2.0'): + self.skipTest("Identity service does not support domain or group") + role_name = self.role_prefix + '_grant_group_domain' + role = self.cloud.create_role(role_name) + group_name = self.group_prefix + '_group_domain' + group = self.cloud.create_group(name=group_name, + description='test group', + domain='default') + self.assertTrue(self.cloud.grant_role( + role_name, group=group['id'], domain='default')) + assignments = self.cloud.list_role_assignments({ + 'role': role['id'], + 'group': group['id'], + 'domain': self.cloud.get_domain('default')['id'] + }) + self.assertIsInstance(assignments, list) + self.assertTrue(len(assignments) == 1) + self.assertTrue(self.cloud.revoke_role( + role_name, group=group['id'], domain='default')) + assignments = self.cloud.list_role_assignments({ + 'role': role['id'], + 'group': group['id'], + 'domain': self.cloud.get_domain('default')['id'] + }) + self.assertIsInstance(assignments, list) + self.assertTrue(len(assignments) == 0) diff --git a/shade/tests/unit/test_role_assignment.py b/shade/tests/unit/test_role_assignment.py new file mode 100644 index 000000000..5263633dc --- /dev/null +++ b/shade/tests/unit/test_role_assignment.py @@ -0,0 +1,859 @@ +# 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 mock import patch +import os_client_config as occ +from shade import OperatorCloud, operator_cloud +from shade.exc import OpenStackCloudException, OpenStackCloudTimeout +from shade.meta import obj_to_dict +from shade.tests import base, fakes +import testtools + + +class TestRoleAssignment(base.TestCase): + + def setUp(self): + super(TestRoleAssignment, self).setUp() + self.cloud = operator_cloud(validate=False) + self.fake_role = obj_to_dict(fakes.FakeRole('12345', 'test')) + self.fake_user = obj_to_dict(fakes.FakeUser('12345', + 'test@nobody.org', + 'test', + domain_id='test-domain')) + self.fake_group = obj_to_dict(fakes.FakeGroup('12345', + 'test', + 'test group', + domain_id='test-domain')) + self.fake_project = obj_to_dict( + fakes.FakeProject('12345', domain_id='test-domain')) + self.fake_domain = obj_to_dict(fakes.FakeDomain('test-domain', + 'test', + 'test domain', + enabled=True)) + self.user_project_assignment = obj_to_dict({ + 'role': { + 'id': self.fake_role['id'] + }, + 'scope': { + 'project': { + 'id': self.fake_project['id'] + } + }, + 'user': { + 'id': self.fake_user['id'] + } + }) + self.group_project_assignment = obj_to_dict({ + 'role': { + 'id': self.fake_role['id'] + }, + 'scope': { + 'project': { + 'id': self.fake_project['id'] + } + }, + 'group': { + 'id': self.fake_group['id'] + } + }) + self.user_domain_assignment = obj_to_dict({ + 'role': { + 'id': self.fake_role['id'] + }, + 'scope': { + 'domain': { + 'id': self.fake_domain['id'] + } + }, + 'user': { + 'id': self.fake_user['id'] + } + }) + self.group_domain_assignment = obj_to_dict({ + 'role': { + 'id': self.fake_role['id'] + }, + 'scope': { + 'domain': { + 'id': self.fake_domain['id'] + } + }, + 'group': { + 'id': self.fake_group['id'] + } + }) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_grant_role_user_v2(self, mock_keystone, mock_api_version): + mock_api_version.return_value = '2.0' + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.users.list.return_value = [self.fake_user] + mock_keystone.tenants.list.return_value = [self.fake_project] + mock_keystone.roles.roles_for_user.return_value = [] + mock_keystone.roles.add_user_role.return_value = self.fake_role + self.assertTrue(self.cloud.grant_role(self.fake_role['name'], + user=self.fake_user['name'], + project=self.fake_project['id'])) + self.assertTrue(self.cloud.grant_role(self.fake_role['name'], + user=self.fake_user['id'], + project=self.fake_project['id'])) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_grant_role_user_project_v2(self, mock_keystone, mock_api_version): + mock_api_version.return_value = '2.0' + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.tenants.list.return_value = [self.fake_project] + mock_keystone.users.list.return_value = [self.fake_user] + mock_keystone.roles.roles_for_user.return_value = [] + mock_keystone.roles.add_user_role.return_value = self.fake_role + self.assertTrue(self.cloud.grant_role(self.fake_role['name'], + user=self.fake_user['name'], + project=self.fake_project['id'])) + self.assertTrue(self.cloud.grant_role(self.fake_role['name'], + user=self.fake_user['id'], + project=self.fake_project['id'])) + self.assertTrue(self.cloud.grant_role(self.fake_role['id'], + user=self.fake_user['name'], + project=self.fake_project['id'])) + self.assertTrue(self.cloud.grant_role(self.fake_role['id'], + user=self.fake_user['id'], + project=self.fake_project['id'])) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_grant_role_user_project_v2_exists(self, + mock_keystone, + mock_api_version): + mock_api_version.return_value = '2.0' + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.tenants.list.return_value = [self.fake_project] + mock_keystone.users.list.return_value = [self.fake_user] + mock_keystone.roles.roles_for_user.return_value = [self.fake_role] + self.assertFalse(self.cloud.grant_role( + self.fake_role['name'], + user=self.fake_user['name'], + project=self.fake_project['id'])) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_grant_role_user_project(self, mock_keystone, mock_api_version): + mock_api_version.return_value = '3' + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.projects.list.return_value = [self.fake_project] + mock_keystone.users.list.return_value = [self.fake_user] + mock_keystone.role_assignments.list.return_value = [] + self.assertTrue(self.cloud.grant_role(self.fake_role['name'], + user=self.fake_user['name'], + project=self.fake_project['id'])) + self.assertTrue(self.cloud.grant_role(self.fake_role['name'], + user=self.fake_user['id'], + project=self.fake_project['id'])) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_grant_role_user_project_exists(self, + mock_keystone, + mock_api_version): + mock_api_version.return_value = '3' + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.projects.list.return_value = [self.fake_project] + mock_keystone.users.list.return_value = [self.fake_user] + mock_keystone.role_assignments.list.return_value = \ + [self.user_project_assignment] + self.assertFalse(self.cloud.grant_role( + self.fake_role['name'], + user=self.fake_user['name'], + project=self.fake_project['id'])) + self.assertFalse(self.cloud.grant_role( + self.fake_role['id'], + user=self.fake_user['id'], + project=self.fake_project['id'])) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_grant_role_group_project(self, mock_keystone, mock_api_version): + mock_api_version.return_value = '3' + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.projects.list.return_value = [self.fake_project] + mock_keystone.groups.list.return_value = [self.fake_group] + mock_keystone.role_assignments.list.return_value = [] + self.assertTrue(self.cloud.grant_role( + self.fake_role['name'], + group=self.fake_group['name'], + project=self.fake_project['id'])) + self.assertTrue(self.cloud.grant_role( + self.fake_role['name'], + group=self.fake_group['id'], + project=self.fake_project['id'])) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_grant_role_group_project_exists(self, + mock_keystone, + mock_api_version): + mock_api_version.return_value = '3' + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.projects.list.return_value = [self.fake_project] + mock_keystone.groups.list.return_value = [self.fake_group] + mock_keystone.role_assignments.list.return_value = \ + [self.group_project_assignment] + self.assertFalse(self.cloud.grant_role( + self.fake_role['name'], + group=self.fake_group['name'], + project=self.fake_project['id'])) + self.assertFalse(self.cloud.grant_role( + self.fake_role['name'], + group=self.fake_group['id'], + project=self.fake_project['id'])) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_grant_role_user_domain(self, mock_keystone, mock_api_version): + mock_api_version.return_value = '3' + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.users.list.return_value = [self.fake_user] + mock_keystone.domains.get.return_value = self.fake_domain + mock_keystone.role_assignments.list.return_value = [] + self.assertTrue(self.cloud.grant_role( + self.fake_role['name'], + user=self.fake_user['name'], + domain=self.fake_domain['id'])) + self.assertTrue(self.cloud.grant_role( + self.fake_role['name'], + user=self.fake_user['id'], + domain=self.fake_domain['id'])) + self.assertTrue(self.cloud.grant_role( + self.fake_role['name'], + user=self.fake_user['name'], + domain=self.fake_domain['name'])) + self.assertTrue(self.cloud.grant_role( + self.fake_role['name'], + user=self.fake_user['id'], + domain=self.fake_domain['name'])) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_grant_role_user_domain_exists(self, + mock_keystone, + mock_api_version): + mock_api_version.return_value = '3' + mock_keystone.users.list.return_value = [self.fake_user] + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.domains.get.return_value = self.fake_domain + mock_keystone.role_assignments.list.return_value = \ + [self.user_domain_assignment] + self.assertFalse(self.cloud.grant_role( + self.fake_role['name'], + user=self.fake_user['name'], + domain=self.fake_domain['name'])) + self.assertFalse(self.cloud.grant_role( + self.fake_role['name'], + user=self.fake_user['id'], + domain=self.fake_domain['name'])) + self.assertFalse(self.cloud.grant_role( + self.fake_role['name'], + user=self.fake_user['name'], + domain=self.fake_domain['id'])) + self.assertFalse(self.cloud.grant_role( + self.fake_role['name'], + user=self.fake_user['id'], + domain=self.fake_domain['id'])) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_grant_role_group_domain(self, mock_keystone, mock_api_version): + mock_api_version.return_value = '3' + mock_keystone.groups.list.return_value = [self.fake_group] + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.domains.get.return_value = self.fake_domain + mock_keystone.role_assignments.list.return_value = [] + self.assertTrue(self.cloud.grant_role( + self.fake_role['name'], + group=self.fake_group['name'], + domain=self.fake_domain['name'])) + self.assertTrue(self.cloud.grant_role( + self.fake_role['name'], + group=self.fake_group['id'], + domain=self.fake_domain['name'])) + self.assertTrue(self.cloud.grant_role( + self.fake_role['name'], + group=self.fake_group['name'], + domain=self.fake_domain['id'])) + self.assertTrue(self.cloud.grant_role( + self.fake_role['name'], + group=self.fake_group['id'], + domain=self.fake_domain['id'])) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_grant_role_group_domain_exists(self, + mock_keystone, + mock_api_version): + mock_api_version.return_value = '3' + mock_keystone.groups.list.return_value = [self.fake_group] + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.domains.get.return_value = self.fake_domain + mock_keystone.role_assignments.list.return_value = \ + [self.group_domain_assignment] + self.assertFalse(self.cloud.grant_role( + self.fake_role['name'], + group=self.fake_group['name'], + domain=self.fake_domain['name'])) + self.assertFalse(self.cloud.grant_role( + self.fake_role['name'], + group=self.fake_group['id'], + domain=self.fake_domain['name'])) + self.assertFalse(self.cloud.grant_role( + self.fake_role['name'], + group=self.fake_group['name'], + domain=self.fake_domain['id'])) + self.assertFalse(self.cloud.grant_role( + self.fake_role['name'], + group=self.fake_group['id'], + domain=self.fake_domain['id'])) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_revoke_role_user_v2(self, mock_keystone, mock_api_version): + mock_api_version.return_value = '2.0' + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.users.list.return_value = [self.fake_user] + mock_keystone.tenants.list.return_value = [self.fake_project] + mock_keystone.roles.roles_for_user.return_value = [self.fake_role] + mock_keystone.roles.remove_user_role.return_value = self.fake_role + self.assertTrue(self.cloud.revoke_role( + self.fake_role['name'], + user=self.fake_user['name'], + project=self.fake_project['id'])) + self.assertTrue(self.cloud.revoke_role( + self.fake_role['name'], + user=self.fake_user['id'], + project=self.fake_project['id'])) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_revoke_role_user_project_v2(self, + mock_keystone, + mock_api_version): + mock_api_version.return_value = '2.0' + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.tenants.list.return_value = [self.fake_project] + mock_keystone.users.list.return_value = [self.fake_user] + mock_keystone.roles.roles_for_user.return_value = [] + self.assertFalse(self.cloud.revoke_role( + self.fake_role['name'], + user=self.fake_user['name'], + project=self.fake_project['id'])) + self.assertFalse(self.cloud.revoke_role( + self.fake_role['name'], + user=self.fake_user['id'], + project=self.fake_project['id'])) + self.assertFalse(self.cloud.revoke_role( + self.fake_role['id'], + user=self.fake_user['name'], + project=self.fake_project['id'])) + self.assertFalse(self.cloud.revoke_role( + self.fake_role['id'], + user=self.fake_user['id'], + project=self.fake_project['id'])) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_revoke_role_user_project_v2_exists(self, + mock_keystone, + mock_api_version): + mock_api_version.return_value = '2.0' + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.tenants.list.return_value = [self.fake_project] + mock_keystone.users.list.return_value = [self.fake_user] + mock_keystone.roles.roles_for_user.return_value = [self.fake_role] + mock_keystone.roles.remove_user_role.return_value = self.fake_role + self.assertTrue(self.cloud.revoke_role( + self.fake_role['name'], + user=self.fake_user['name'], + project=self.fake_project['id'])) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_revoke_role_user_project(self, mock_keystone, mock_api_version): + mock_api_version.return_value = '3' + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.projects.list.return_value = [self.fake_project] + mock_keystone.users.list.return_value = [self.fake_user] + mock_keystone.role_assignments.list.return_value = [] + self.assertFalse(self.cloud.revoke_role( + self.fake_role['name'], + user=self.fake_user['name'], + project=self.fake_project['id'])) + self.assertFalse(self.cloud.revoke_role( + self.fake_role['name'], + user=self.fake_user['id'], + project=self.fake_project['id'])) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_revoke_role_user_project_exists(self, + mock_keystone, + mock_api_version): + mock_api_version.return_value = '3' + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.projects.list.return_value = [self.fake_project] + mock_keystone.users.list.return_value = [self.fake_user] + mock_keystone.role_assignments.list.return_value = \ + [self.user_project_assignment] + self.assertTrue(self.cloud.revoke_role( + self.fake_role['name'], + user=self.fake_user['name'], + project=self.fake_project['id'])) + self.assertTrue(self.cloud.revoke_role( + self.fake_role['id'], + user=self.fake_user['id'], + project=self.fake_project['id'])) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_revoke_role_group_project(self, mock_keystone, mock_api_version): + mock_api_version.return_value = '3' + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.projects.list.return_value = [self.fake_project] + mock_keystone.groups.list.return_value = [self.fake_group] + mock_keystone.role_assignments.list.return_value = [] + self.assertFalse(self.cloud.revoke_role( + self.fake_role['name'], + group=self.fake_group['name'], + project=self.fake_project['id'])) + self.assertFalse(self.cloud.revoke_role( + self.fake_role['name'], + group=self.fake_group['id'], + project=self.fake_project['id'])) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_revoke_role_group_project_exists(self, + mock_keystone, + mock_api_version): + mock_api_version.return_value = '3' + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.projects.list.return_value = [self.fake_project] + mock_keystone.groups.list.return_value = [self.fake_group] + mock_keystone.role_assignments.list.return_value = \ + [self.group_project_assignment] + self.assertTrue(self.cloud.revoke_role( + self.fake_role['name'], + group=self.fake_group['name'], + project=self.fake_project['id'])) + self.assertTrue(self.cloud.revoke_role( + self.fake_role['name'], + group=self.fake_group['id'], + project=self.fake_project['id'])) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_revoke_role_user_domain(self, mock_keystone, mock_api_version): + mock_api_version.return_value = '3' + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.users.list.return_value = [self.fake_user] + mock_keystone.domains.get.return_value = self.fake_domain + mock_keystone.role_assignments.list.return_value = [] + self.assertFalse(self.cloud.revoke_role( + self.fake_role['name'], + user=self.fake_user['name'], + domain=self.fake_domain['id'])) + self.assertFalse(self.cloud.revoke_role( + self.fake_role['name'], + user=self.fake_user['id'], + domain=self.fake_domain['id'])) + self.assertFalse(self.cloud.revoke_role( + self.fake_role['name'], + user=self.fake_user['name'], + domain=self.fake_domain['name'])) + self.assertFalse(self.cloud.revoke_role( + self.fake_role['name'], + user=self.fake_user['id'], + domain=self.fake_domain['name'])) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_revoke_role_user_domain_exists(self, + mock_keystone, + mock_api_version): + mock_api_version.return_value = '3' + mock_keystone.users.list.return_value = [self.fake_user] + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.domains.get.return_value = self.fake_domain + mock_keystone.role_assignments.list.return_value = \ + [self.user_domain_assignment] + self.assertTrue(self.cloud.revoke_role( + self.fake_role['name'], + user=self.fake_user['name'], + domain=self.fake_domain['name'])) + self.assertTrue(self.cloud.revoke_role( + self.fake_role['name'], + user=self.fake_user['id'], + domain=self.fake_domain['name'])) + self.assertTrue(self.cloud.revoke_role( + self.fake_role['name'], + user=self.fake_user['name'], + domain=self.fake_domain['id'])) + self.assertTrue(self.cloud.revoke_role( + self.fake_role['name'], + user=self.fake_user['id'], + domain=self.fake_domain['id'])) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_revoke_role_group_domain(self, mock_keystone, mock_api_version): + mock_api_version.return_value = '3' + mock_keystone.groups.list.return_value = [self.fake_group] + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.domains.get.return_value = self.fake_domain + mock_keystone.role_assignments.list.return_value = [] + self.assertFalse(self.cloud.revoke_role( + self.fake_role['name'], + group=self.fake_group['name'], + domain=self.fake_domain['name'])) + self.assertFalse(self.cloud.revoke_role( + self.fake_role['name'], + group=self.fake_group['id'], + domain=self.fake_domain['name'])) + self.assertFalse(self.cloud.revoke_role( + self.fake_role['name'], + group=self.fake_group['name'], + domain=self.fake_domain['id'])) + self.assertFalse(self.cloud.revoke_role( + self.fake_role['name'], + group=self.fake_group['id'], + domain=self.fake_domain['id'])) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_revoke_role_group_domain_exists(self, + mock_keystone, + mock_api_version): + mock_api_version.return_value = '3' + mock_keystone.groups.list.return_value = [self.fake_group] + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.domains.get.return_value = self.fake_domain + mock_keystone.role_assignments.list.return_value = \ + [self.group_domain_assignment] + self.assertTrue(self.cloud.revoke_role( + self.fake_role['name'], + group=self.fake_group['name'], + domain=self.fake_domain['name'])) + self.assertTrue(self.cloud.revoke_role( + self.fake_role['name'], + group=self.fake_group['id'], + domain=self.fake_domain['name'])) + self.assertTrue(self.cloud.revoke_role( + self.fake_role['name'], + group=self.fake_group['name'], + domain=self.fake_domain['id'])) + self.assertTrue(self.cloud.revoke_role( + self.fake_role['name'], + group=self.fake_group['id'], + domain=self.fake_domain['id'])) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_grant_no_role(self, mock_keystone, mock_api_version): + mock_api_version.return_value = '3' + mock_keystone.roles.list.return_value = [] + + with testtools.ExpectedException( + OpenStackCloudException, + 'Role {0} not found'.format(self.fake_role['name']) + ): + self.cloud.grant_role(self.fake_role['name'], + group=self.fake_group['name'], + domain=self.fake_domain['name']) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_revoke_no_role(self, mock_keystone, mock_api_version): + mock_api_version.return_value = '3' + mock_keystone.roles.list.return_value = [] + with testtools.ExpectedException( + OpenStackCloudException, + 'Role {0} not found'.format(self.fake_role['name']) + ): + self.cloud.revoke_role(self.fake_role['name'], + group=self.fake_group['name'], + domain=self.fake_domain['name']) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_grant_no_user_or_group_specified(self, + mock_keystone, + mock_api_version): + mock_api_version.return_value = '3' + mock_keystone.roles.list.return_value = [self.fake_role] + with testtools.ExpectedException( + OpenStackCloudException, + 'Must specify either a user or a group' + ): + self.cloud.grant_role(self.fake_role['name']) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_revoke_no_user_or_group_specified(self, + mock_keystone, + mock_api_version): + mock_api_version.return_value = '3' + mock_keystone.roles.list.return_value = [self.fake_role] + with testtools.ExpectedException( + OpenStackCloudException, + 'Must specify either a user or a group' + ): + self.cloud.revoke_role(self.fake_role['name']) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_grant_no_user_or_group(self, mock_keystone, mock_api_version): + mock_api_version.return_value = '3' + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.users.list.return_value = [] + with testtools.ExpectedException( + OpenStackCloudException, + 'Must specify either a user or a group' + ): + self.cloud.grant_role(self.fake_role['name'], + user=self.fake_user['name']) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_revoke_no_user_or_group(self, mock_keystone, mock_api_version): + mock_api_version.return_value = '3' + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.users.list.return_value = [] + with testtools.ExpectedException( + OpenStackCloudException, + 'Must specify either a user or a group' + ): + self.cloud.revoke_role(self.fake_role['name'], + user=self.fake_user['name']) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_grant_both_user_and_group(self, mock_keystone, mock_api_version): + mock_api_version.return_value = '3' + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.users.list.return_value = [self.fake_user] + mock_keystone.groups.list.return_value = [self.fake_group] + with testtools.ExpectedException( + OpenStackCloudException, + 'Specify either a group or a user, not both' + ): + self.cloud.grant_role(self.fake_role['name'], + user=self.fake_user['name'], + group=self.fake_group['name']) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_revoke_both_user_and_group(self, mock_keystone, mock_api_version): + mock_api_version.return_value = '3' + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.users.list.return_value = [self.fake_user] + mock_keystone.groups.list.return_value = [self.fake_group] + with testtools.ExpectedException( + OpenStackCloudException, + 'Specify either a group or a user, not both' + ): + self.cloud.revoke_role(self.fake_role['name'], + user=self.fake_user['name'], + group=self.fake_group['name']) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_grant_both_project_and_domain(self, + mock_keystone, + mock_api_version): + mock_api_version.return_value = '3' + fake_user2 = fakes.FakeUser('12345', + 'test@nobody.org', + 'test', + domain_id='default') + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.users.list.return_value = [self.fake_user, fake_user2] + mock_keystone.projects.list.return_value = [self.fake_project] + mock_keystone.domains.get.return_value = self.fake_domain + self.assertTrue(self.cloud.grant_role(self.fake_role['name'], + user=self.fake_user['name'], + project=self.fake_project['id'], + domain=self.fake_domain['name'])) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_revoke_both_project_and_domain(self, + mock_keystone, + mock_api_version): + mock_api_version.return_value = '3' + fake_user2 = fakes.FakeUser('12345', + 'test@nobody.org', + 'test', + domain_id='default') + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.users.list.return_value = [self.fake_user, fake_user2] + mock_keystone.projects.list.return_value = [self.fake_project] + mock_keystone.domains.get.return_value = self.fake_domain + mock_keystone.role_assignments.list.return_value = \ + [self.user_project_assignment] + self.assertTrue(self.cloud.revoke_role( + self.fake_role['name'], + user=self.fake_user['name'], + project=self.fake_project['id'], + domain=self.fake_domain['name'])) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_grant_no_project_or_domain(self, mock_keystone, mock_api_version): + mock_api_version.return_value = '3' + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.users.list.return_value = [self.fake_user] + mock_keystone.projects.list.return_value = [] + mock_keystone.domains.get.return_value = None + with testtools.ExpectedException( + OpenStackCloudException, + 'Must specify either a domain or project' + ): + self.cloud.grant_role(self.fake_role['name'], + user=self.fake_user['name']) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_revoke_no_project_or_domain(self, + mock_keystone, + mock_api_version): + mock_api_version.return_value = '3' + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.users.list.return_value = [self.fake_user] + mock_keystone.projects.list.return_value = [] + mock_keystone.domains.get.return_value = None + mock_keystone.role_assignments.list.return_value = \ + [self.user_project_assignment] + with testtools.ExpectedException( + OpenStackCloudException, + 'Must specify either a domain or project' + ): + self.cloud.revoke_role(self.fake_role['name'], + user=self.fake_user['name']) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_grant_bad_domain_exception(self, + mock_keystone, + mock_api_version): + mock_api_version.return_value = '3' + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.domains.get.side_effect = Exception('test') + with testtools.ExpectedException( + OpenStackCloudException, + 'Failed to get domain baddomain \(Inner Exception: test\)' + ): + self.cloud.grant_role(self.fake_role['name'], + user=self.fake_user['name'], + domain='baddomain') + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_revoke_bad_domain_exception(self, + mock_keystone, + mock_api_version): + mock_api_version.return_value = '3' + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.domains.get.side_effect = Exception('test') + with testtools.ExpectedException( + OpenStackCloudException, + 'Failed to get domain baddomain \(Inner Exception: test\)' + ): + self.cloud.revoke_role(self.fake_role['name'], + user=self.fake_user['name'], + domain='baddomain') + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_grant_role_user_project_v2_wait(self, + mock_keystone, + mock_api_version): + mock_api_version.return_value = '2.0' + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.tenants.list.return_value = [self.fake_project] + mock_keystone.users.list.return_value = [self.fake_user] + mock_keystone.roles.roles_for_user.side_effect = [ + [], [], [self.fake_role]] + mock_keystone.roles.add_user_role.return_value = self.fake_role + self.assertTrue(self.cloud.grant_role(self.fake_role['name'], + user=self.fake_user['name'], + project=self.fake_project['id'], + wait=True)) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_grant_role_user_project_v2_wait_exception(self, + mock_keystone, + mock_api_version): + mock_api_version.return_value = '2.0' + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.tenants.list.return_value = [self.fake_project] + mock_keystone.users.list.return_value = [self.fake_user] + mock_keystone.roles.roles_for_user.side_effect = [ + [], [], [self.fake_role]] + mock_keystone.roles.add_user_role.return_value = self.fake_role + + with testtools.ExpectedException( + OpenStackCloudTimeout, + 'Timeout waiting for role to be granted' + ): + self.assertTrue(self.cloud.grant_role( + self.fake_role['name'], user=self.fake_user['name'], + project=self.fake_project['id'], wait=True, timeout=1)) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_revoke_role_user_project_v2_wait(self, + mock_keystone, + mock_api_version): + mock_api_version.return_value = '2.0' + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.tenants.list.return_value = [self.fake_project] + mock_keystone.users.list.return_value = [self.fake_user] + mock_keystone.roles.roles_for_user.side_effect = [ + [self.fake_role], [self.fake_role], + []] + mock_keystone.roles.remove_user_role.return_value = self.fake_role + self.assertTrue(self.cloud.revoke_role(self.fake_role['name'], + user=self.fake_user['name'], + project=self.fake_project['id'], + wait=True)) + + @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @patch.object(OperatorCloud, 'keystone_client') + def test_revoke_role_user_project_v2_wait_exception(self, + mock_keystone, + mock_api_version): + mock_api_version.return_value = '2.0' + mock_keystone.roles.list.return_value = [self.fake_role] + mock_keystone.tenants.list.return_value = [self.fake_project] + mock_keystone.users.list.return_value = [self.fake_user] + mock_keystone.roles.roles_for_user.side_effect = [ + [self.fake_role], [self.fake_role], + []] + mock_keystone.roles.remove_user_role.return_value = self.fake_role + with testtools.ExpectedException( + OpenStackCloudTimeout, + 'Timeout waiting for role to be revoked' + ): + self.assertTrue(self.cloud.revoke_role( + self.fake_role['name'], user=self.fake_user['name'], + project=self.fake_project['id'], wait=True, timeout=1)) From 9199c263c9b085f6bbacb3c71a466c021f2c7871 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 8 Feb 2016 10:55:22 -0500 Subject: [PATCH 0790/3836] Fix for stable/liberty job Now that the keystone v2 job runs against stable liberty, we have to look in a different location for the clouds.yaml file since it was still being written to the home directory at that point. Change-Id: I3afd99ef0d460b541d4000740ec045879c507358 --- shade/tests/functional/hooks/post_test_hook.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/shade/tests/functional/hooks/post_test_hook.sh b/shade/tests/functional/hooks/post_test_hook.sh index ad823009a..10a59b16b 100755 --- a/shade/tests/functional/hooks/post_test_hook.sh +++ b/shade/tests/functional/hooks/post_test_hook.sh @@ -20,6 +20,14 @@ sudo chown -R jenkins:stack $SHADE_DIR CLOUDS_YAML=/etc/openstack/clouds.yaml +if [ ! -e ${CLOUDS_YAML} ] +then + # stable/liberty had clouds.yaml in the home/base directory + sudo mkdir -p /etc/openstack + sudo cp $BASE/new/.config/openstack/clouds.yaml ${CLOUDS_YAML} + sudo chown -R jenkins:stack /etc/openstack +fi + # Devstack runs both keystone v2 and v3. An environment variable is set # within the shade keystone v2 job that tells us which version we should # test against. From 8264e09c69bd6c017c1716a70cec21c28919e6d1 Mon Sep 17 00:00:00 2001 From: Mohammed Naser Date: Wed, 10 Feb 2016 11:51:37 -0500 Subject: [PATCH 0791/3836] Added SSL support for VEXXHOST VEXXHOST cloud uses SSL for Keystone and all other services, change the auth URL to the SSL endpoint. Change-Id: If80c76603de44d005d6af1726f34d924384bf747 --- os_client_config/vendors/vexxhost.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/os_client_config/vendors/vexxhost.json b/os_client_config/vendors/vexxhost.json index dd683be86..aa2cedc68 100644 --- a/os_client_config/vendors/vexxhost.json +++ b/os_client_config/vendors/vexxhost.json @@ -2,7 +2,7 @@ "name": "vexxhost", "profile": { "auth": { - "auth_url": "http://auth.vexxhost.net" + "auth_url": "https://auth.vexxhost.net" }, "regions": [ "ca-ymq-1" From 10a9369062504cce1c4c79a1b112e619082cf6ab Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 10 Feb 2016 11:24:10 -0600 Subject: [PATCH 0792/3836] Remove HP and RunAbove from vendor profiles HP has already shut down its public cloud. RunAbove is shutting down 17th February as part of the migration to OVH.com. Neither are therefore valid vendors any longer. Change-Id: I8d305ca2b1cbaf67e6711eedaa1a4c5668a42be7 --- doc/source/vendor-support.rst | 31 -------------------------- os_client_config/vendors/hp.json | 16 ------------- os_client_config/vendors/runabove.json | 15 ------------- 3 files changed, 62 deletions(-) delete mode 100644 os_client_config/vendors/hp.json delete mode 100644 os_client_config/vendors/runabove.json diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index e007b70f8..d30c104e7 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -142,23 +142,6 @@ de-fra1 Frankfurt, DE * Volume API Version is 1 -hp --- - -https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0 - -============== ================ -Region Name Human Name -============== ================ -region-a.geo-1 US West -region-b.geo-1 US East -============== ================ - -* DNS Service Type is `hpext:dns` -* Image API Version is 1 -* Public IPv4 is provided via NAT with Neutron Floating IP -* Volume API Version is 1 - ibmcloud -------- @@ -233,20 +216,6 @@ SYD Sydney :xenapi_use_agent: False * Volume API Version is 1 -runabove --------- - -https://auth.runabove.io/v2.0 - -============== ================ -Region Name Human Name -============== ================ -SBG-1 Strassbourg, FR -BHS-1 Beauharnois, QC -============== ================ - -* Floating IPs are not supported - switchengines ------------- diff --git a/os_client_config/vendors/hp.json b/os_client_config/vendors/hp.json deleted file mode 100644 index b06b90ad5..000000000 --- a/os_client_config/vendors/hp.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "hp", - "profile": { - "auth": { - "auth_url": "https://region-b.geo-1.identity.hpcloudsvc.com:35357" - }, - "regions": [ - "region-a.geo-1", - "region-b.geo-1" - ], - "identity_api_version": "3", - "dns_service_type": "hpext:dns", - "volume_api_version": "1", - "image_api_version": "1" - } -} diff --git a/os_client_config/vendors/runabove.json b/os_client_config/vendors/runabove.json deleted file mode 100644 index abf111633..000000000 --- a/os_client_config/vendors/runabove.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "runabove", - "profile": { - "auth": { - "auth_url": "https://auth.runabove.io/" - }, - "regions": [ - "BHS-1", - "SBG-1" - ], - "identity_api_version": "3", - "image_format": "qcow2", - "floating_ip_source": "None" - } -} From ef5a1b20a130f052d7d272bfffe7a9def88d6c4b Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 10 Feb 2016 10:23:30 -0500 Subject: [PATCH 0793/3836] Modify test workaround for extra_dhcp_opts The neutron create API now returns extra_dhcp_opts. In older versions it did not so it had to be removed when comparing the port definitions between the "create" and the "get". Now we only remove it if it is not present in the "create". This keeps it working for our keystonev2 test job which runs against stable/liberty now. Change-Id: I4d6a57b0f15f1868c5ba4e62a2bf248662bd8896 --- shade/tests/functional/test_port.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/tests/functional/test_port.py b/shade/tests/functional/test_port.py index 85f5f887f..c8f3840d1 100644 --- a/shade/tests/functional/test_port.py +++ b/shade/tests/functional/test_port.py @@ -89,7 +89,7 @@ def test_get_port(self): updated_port = self.cloud.get_port(name_or_id=port['id']) # extra_dhcp_opts is added later by Neutron... - if 'extra_dhcp_opts' in updated_port: + if 'extra_dhcp_opts' in updated_port and 'extra_dhcp_opts' not in port: del updated_port['extra_dhcp_opts'] self.assertEqual(port, updated_port) From 661927392c245607f35ac111e7c5060d41cb4484 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 11 Feb 2016 08:33:50 -0500 Subject: [PATCH 0794/3836] Use release version of Ansible for testing We were using the unreleased (from git) version of Ansible for our Ansible test job because at the time, 2.0 had not been released. Now that it is released, use pip to install it so that we are less likely to get broken by development changes to Ansible. Change-Id: Ie714f42d26569044b6a3ee761bd5fb7451833d96 --- extras/run-ansible-tests.sh | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/extras/run-ansible-tests.sh b/extras/run-ansible-tests.sh index 816eb53fe..d9bf662be 100755 --- a/extras/run-ansible-tests.sh +++ b/extras/run-ansible-tests.sh @@ -50,20 +50,12 @@ fi shift $((OPTIND-1)) TAGS=$( echo "$*" | tr ' ' , ) -if [ -d ${ENVDIR}/ansible ] -then - echo "Using existing Ansible install" -else - echo "Installing Ansible at $ENVDIR" - git clone --recursive git://github.com/ansible/ansible.git ${ENVDIR}/ansible -fi - # We need to source the current tox environment so that Ansible will # be setup for the correct python environment. source $ENVDIR/bin/activate -# Setup Ansible -source $ENVDIR/ansible/hacking/env-setup +echo "Installing Ansible" +pip install ansible # Run the shade Ansible tests tag_opt="" From d052121d6dd66ffd2b0d6ed56c20ed1a1c55afd8 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 11 Feb 2016 15:09:31 -0500 Subject: [PATCH 0795/3836] Prepare functional test subunit stream for collection This commit fixes an issue with the functional test jobs where the subunit stream from the test run isn't archived. This prevents both the testr_results.html file from containing test results, and also the eventual collection of the test results into the subunit2sql db (when it's actually enabled for non-tempest jobs) Change-Id: Ie16206349c59f780d786bffe5db1992cfa6a002e --- shade/tests/functional/hooks/post_test_hook.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/shade/tests/functional/hooks/post_test_hook.sh b/shade/tests/functional/hooks/post_test_hook.sh index 10a59b16b..956dbdaeb 100755 --- a/shade/tests/functional/hooks/post_test_hook.sh +++ b/shade/tests/functional/hooks/post_test_hook.sh @@ -41,6 +41,7 @@ echo "Running shade functional test suite" set +e sudo -E -H -u jenkins tox -efunctional EXIT_CODE=$? +sudo testr last --subunit > $WORKSPACE/tempest.subunit set -e exit $EXIT_CODE From 9035ade760591cf194feaa643f5278049d90bee5 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 29 Jan 2016 09:45:22 -0500 Subject: [PATCH 0796/3836] create_service() should normalize return value The new service data being returned from create_service() was not going through the normalization function. Change-Id: I5e80e7a74f71a61d83653595b9a176d2aa9039ec --- .../notes/create_service_norm-319a97433d68fa6a.yaml | 3 +++ shade/operatorcloud.py | 2 +- shade/tests/unit/test_services.py | 7 ++++--- 3 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/create_service_norm-319a97433d68fa6a.yaml diff --git a/releasenotes/notes/create_service_norm-319a97433d68fa6a.yaml b/releasenotes/notes/create_service_norm-319a97433d68fa6a.yaml new file mode 100644 index 000000000..2f6d018a6 --- /dev/null +++ b/releasenotes/notes/create_service_norm-319a97433d68fa6a.yaml @@ -0,0 +1,3 @@ +--- +fixes: + - The returned data from a create_service() call was not being normalized. diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index d83430837..269bce942 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -779,7 +779,7 @@ def create_service(self, name, **kwargs): service = self.manager.submitTask(_tasks.ServiceCreate( name=name, description=description, **service_kwargs)) - return service + return _utils.normalize_keystone_services([service])[0] def list_services(self): """List all Keystone services. diff --git a/shade/tests/unit/test_services.py b/shade/tests/unit/test_services.py index ed5014fc2..5ac8b5a44 100644 --- a/shade/tests/unit/test_services.py +++ b/shade/tests/unit/test_services.py @@ -21,6 +21,7 @@ from mock import patch import os_client_config +from shade import _utils from shade import OpenStackCloudException from shade import OperatorCloud from shade.tests.fakes import FakeService @@ -47,8 +48,9 @@ def setUp(self): self.mock_ks_services = [FakeService(**kwa) for kwa in self.mock_services] + @patch.object(_utils, 'normalize_keystone_services') @patch.object(OperatorCloud, 'keystone_client') - def test_create_service(self, mock_keystone_client): + def test_create_service(self, mock_keystone_client, mock_norm): kwargs = { 'name': 'a service', 'type': 'network', @@ -58,15 +60,14 @@ def test_create_service(self, mock_keystone_client): self.client.create_service(**kwargs) kwargs['service_type'] = kwargs.pop('type') mock_keystone_client.services.create.assert_called_with(**kwargs) + self.assertTrue(mock_norm.called) @patch.object(OperatorCloud, 'keystone_client') def test_list_services(self, mock_keystone_client): mock_keystone_client.services.list.return_value = \ self.mock_ks_services - services = self.client.list_services() mock_keystone_client.services.list.assert_called_with() - self.assertItemsEqual(self.mock_services, services) @patch.object(OperatorCloud, 'keystone_client') From df379be704fc2c1bb870dc2ab01a1e19626c754c Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 5 Feb 2016 13:51:58 -0500 Subject: [PATCH 0797/3836] Add docs tox target Add a tox target to easily build the shade documentation. Change-Id: I38f3a39de1a34ae8709ce3b83a8fb51dd75299d3 --- tox.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tox.ini b/tox.ini index 2ca4fefd5..c1da17bc8 100644 --- a/tox.ini +++ b/tox.ini @@ -35,6 +35,9 @@ commands = python setup.py testr --coverage --testr-args='{posargs}' passenv = HOME USER commands = {toxinidir}/extras/run-ansible-tests.sh -e {envdir} {posargs} +[testenv:docs] +commands = python setup.py build_sphinx + [flake8] # Infra does not follow hacking, nor the broken E12* things ignore = E123,E125,E129,H From ab8a1969075c80ba7b1cea9b9994d8eeedfd5f49 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 5 Feb 2016 14:11:09 -0500 Subject: [PATCH 0798/3836] Clarify Munch object usage in documentation We have pretty much settled on sticking with Munch objects for the 1.x series of shade releases. Change the documentation to note this. Change-Id: I8b2838c3a5a40b0c74e31081bfb1ed4b0d39280f --- doc/source/usage.rst | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/doc/source/usage.rst b/doc/source/usage.rst index 43bc0d957..87269de5f 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -6,12 +6,18 @@ To use shade in a project:: import shade -.. warning:: - Several of the API methods return a ``dict`` that describe a resource. - It is possible to access keys of the dict as an attribute (e.g., - ``server.id`` instead of ``server['id']``) to maintain some backward - compatibility, but attribute access is deprecated. New code should - assume a normal dictionary and access values via key. +.. note:: + API methods that return a description of an OpenStack resource (e.g., + server instance, image, volume, etc.) do so using a dictionary of values + (e.g., ``server['id']``, ``image['name']``). This is the standard, and + **recommended**, way to access these resource values. + + For backward compatibility, resource values can be accessed using object + attribute access (e.g., ``server.id``, ``image.name``). Shade uses the + `Munch library `_ to provide this + behavior. This is **NOT** the recommended way to access resource values. + We keep this behavior for developer convenience in the 1.x series of shade + releases. This will likely not be the case in future, major releases of shade. .. autoclass:: shade.OpenStackCloud :members: From 8cb0eb37721985a58665c6e3e1b63b17c23c4cee Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 29 Jan 2016 10:28:41 -0500 Subject: [PATCH 0799/3836] Add enabled flag to keystone service data Services can be enabled or disabled. Add this flag to the service data structure. Change-Id: I879811e95829176a64dc34cf7bef09c25421c062 --- .../notes/service_enabled_flag-c917b305d3f2e8fd.yaml | 5 +++++ shade/_utils.py | 1 + shade/tests/fakes.py | 4 +++- shade/tests/unit/test_services.py | 8 ++++---- 4 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/service_enabled_flag-c917b305d3f2e8fd.yaml diff --git a/releasenotes/notes/service_enabled_flag-c917b305d3f2e8fd.yaml b/releasenotes/notes/service_enabled_flag-c917b305d3f2e8fd.yaml new file mode 100644 index 000000000..089d297c9 --- /dev/null +++ b/releasenotes/notes/service_enabled_flag-c917b305d3f2e8fd.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - Keystone service descriptions were missing an attribute describing whether + or not the service was enabled. A new 'enabled' boolean attribute has been + added to the service data. diff --git a/shade/_utils.py b/shade/_utils.py index 7424862e9..f846f9179 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -193,6 +193,7 @@ def normalize_keystone_services(services): 'description': service.get('description', None), 'type': service_type, 'service_type': service_type, + 'enabled': service['enabled'] } ret.append(new_service) return meta.obj_list_to_dict(ret) diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 45229d962..f3c07224c 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -85,12 +85,14 @@ def __init__( class FakeService(object): - def __init__(self, id, name, type, service_type, description=''): + def __init__(self, id, name, type, service_type, description='', + enabled=True): self.id = id self.name = name self.type = type self.service_type = service_type self.description = description + self.enabled = enabled class FakeUser(object): diff --git a/shade/tests/unit/test_services.py b/shade/tests/unit/test_services.py index 5ac8b5a44..f5fd9d965 100644 --- a/shade/tests/unit/test_services.py +++ b/shade/tests/unit/test_services.py @@ -31,13 +31,13 @@ class CloudServices(base.TestCase): mock_services = [ {'id': 'id1', 'name': 'service1', 'type': 'type1', - 'service_type': 'type1', 'description': 'desc1'}, + 'service_type': 'type1', 'description': 'desc1', 'enabled': True}, {'id': 'id2', 'name': 'service2', 'type': 'type2', - 'service_type': 'type2', 'description': 'desc2'}, + 'service_type': 'type2', 'description': 'desc2', 'enabled': True}, {'id': 'id3', 'name': 'service3', 'type': 'type2', - 'service_type': 'type2', 'description': 'desc3'}, + 'service_type': 'type2', 'description': 'desc3', 'enabled': True}, {'id': 'id4', 'name': 'service4', 'type': 'type3', - 'service_type': 'type3', 'description': 'desc4'} + 'service_type': 'type3', 'description': 'desc4', 'enabled': True} ] def setUp(self): From 02b4b4c43fbed744ba375f62ffc5d323e363cfb6 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 12 Feb 2016 11:24:52 -0500 Subject: [PATCH 0800/3836] Add test option to use Ansible source repo This restores the ability to run the Ansible tests using Ansible from the source repo rather than the production version from pip. Using production will be the default, but this sets us up to add a new job to test against the latest dev version, if we want. Change-Id: I93cdec653dd672acfbc03576d100d19ab8595f2e --- extras/run-ansible-tests.sh | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/extras/run-ansible-tests.sh b/extras/run-ansible-tests.sh index d9bf662be..146f5436b 100755 --- a/extras/run-ansible-tests.sh +++ b/extras/run-ansible-tests.sh @@ -8,11 +8,14 @@ # tox -e ansible [TAG ...] # or # tox -e ansible -- -c cloudX [TAG ...] +# or to use the development version of Ansible: +# tox -e ansible -- -d -c cloudX [TAG ...] # # USAGE: -# run-ansible-tests.sh -e ENVDIR [-c CLOUD] [TAG ...] +# run-ansible-tests.sh -e ENVDIR [-d] [-c CLOUD] [TAG ...] # # PARAMETERS: +# -d Use Ansible source repo development branch. # -e ENVDIR Directory of the tox environment to use for testing. # -c CLOUD Name of the cloud to use for testing. # Defaults to "devstack-admin". @@ -30,10 +33,12 @@ CLOUD="devstack-admin" ENVDIR= +USE_DEV=0 -while getopts "c:e:" opt +while getopts "c:de:" opt do case $opt in + d) USE_DEV=1 ;; c) CLOUD=${OPTARG} ;; e) ENVDIR=${OPTARG} ;; ?) echo "Invalid option: -${OPTARG}" @@ -54,8 +59,20 @@ TAGS=$( echo "$*" | tr ' ' , ) # be setup for the correct python environment. source $ENVDIR/bin/activate -echo "Installing Ansible" -pip install ansible +if [ ${USE_DEV} -eq 1 ] +then + if [ -d ${ENVDIR}/ansible ] + then + echo "Using existing Ansible source repo" + else + echo "Installing Ansible source repo at $ENVDIR" + git clone --recursive git://github.com/ansible/ansible.git ${ENVDIR}/ansible + fi + source $ENVDIR/ansible/hacking/env-setup +else + echo "Installing Ansible from pip" + pip install ansible +fi # Run the shade Ansible tests tag_opt="" From dd1f03c597daf1dc422608ab8e2b0b6b78168a3f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 10 Feb 2016 17:37:54 -0600 Subject: [PATCH 0801/3836] Send swiftclient username/password and token For longer-lived operations, tokens can timeout and we need to get new ones. While in theory we should be keystoneauth aware and passing around sessions, swiftclient does not yet support this. So, instead of passing in just a preauthtoken, also pass in credentials if we have them. However, for plugin types that swift does not know about directly, only preauthtoken will be used as before. Change-Id: If724fdcd0649d9fa3b3ee7b127e49a3f77e3b767 --- os_client_config/cloud_config.py | 37 ++++- os_client_config/tests/test_cloud_config.py | 151 ++++++++++++++++++-- 2 files changed, 173 insertions(+), 15 deletions(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 85c6f2a0b..b19607e03 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -355,22 +355,53 @@ def get_legacy_client( return client_class(**constructor_kwargs) def _get_swift_client(self, client_class, **kwargs): + auth_args = self.get_auth_args() + auth_version = self.get_api_version('identity') session = self.get_session() token = session.get_token() endpoint = self.get_session_endpoint(service_key='object-store') if not endpoint: return None + # If we have a username/password, we want to pass them to + # swift - because otherwise it will not re-up tokens appropriately + # However, if we only have non-password auth, then get a token + # and pass it in swift_kwargs = dict( + auth_version=auth_version, preauthurl=endpoint, preauthtoken=token, - auth_version=self.get_api_version('identity'), os_options=dict( + region_name=self.get_region_name(), auth_token=token, object_storage_url=endpoint, - region_name=self.get_region_name()), - ) + service_type=self.get_service_type('object-store'), + endpoint_type=self.get_interface('object-store'), + + )) if self.config['api_timeout'] is not None: swift_kwargs['timeout'] = float(self.config['api_timeout']) + + # create with password + swift_kwargs['user'] = auth_args.get('username') + swift_kwargs['key'] = auth_args.get('password') + swift_kwargs['authurl'] = auth_args.get('auth_url') + os_options = {} + if auth_version == '2.0': + os_options['tenant_name'] = auth_args.get('project_name') + os_options['tenant_id'] = auth_args.get('project_id') + else: + os_options['project_name'] = auth_args.get('project_name') + os_options['project_id'] = auth_args.get('project_id') + + for key in ( + 'user_id', + 'project_domain_id', + 'project_domain_name', + 'user_domain_id', + 'user_domain_name'): + os_options[key] = auth_args.get(key) + swift_kwargs['os_options'].update(os_options) + return client_class(**swift_kwargs) def get_cache_expiration_time(self): diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index a01d0e1b5..7a8b77aed 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -235,10 +235,96 @@ def test_session_endpoint(self, mock_get_session): region_name='region-al', service_type='orchestration') + @mock.patch.object(cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(cloud_config.CloudConfig, 'get_auth_args') @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') - def test_legacy_client_object_store(self, mock_get_session_endpoint): + def test_legacy_client_object_store_password( + self, + mock_get_session_endpoint, + mock_get_auth_args, + mock_get_api_version): + mock_client = mock.Mock() + mock_get_session_endpoint.return_value = 'http://swift.example.com' + mock_get_api_version.return_value = '3' + mock_get_auth_args.return_value = dict( + username='testuser', + password='testpassword', + project_name='testproject', + auth_url='http://example.com', + ) + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + cc = cloud_config.CloudConfig( + "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + cc.get_legacy_client('object-store', mock_client) + mock_client.assert_called_with( + preauthtoken=mock.ANY, + auth_version=u'3', + authurl='http://example.com', + key='testpassword', + os_options={ + 'auth_token': mock.ANY, + 'region_name': 'region-al', + 'object_storage_url': 'http://swift.example.com', + 'user_id': None, + 'user_domain_name': None, + 'project_name': 'testproject', + 'project_domain_name': None, + 'project_domain_id': None, + 'project_id': None, + 'service_type': 'object-store', + 'endpoint_type': 'public', + 'user_domain_id': None + }, + preauthurl='http://swift.example.com', + user='testuser') + + @mock.patch.object(cloud_config.CloudConfig, 'get_auth_args') + @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + def test_legacy_client_object_store_password_v2( + self, mock_get_session_endpoint, mock_get_auth_args): + mock_client = mock.Mock() + mock_get_session_endpoint.return_value = 'http://swift.example.com' + mock_get_auth_args.return_value = dict( + username='testuser', + password='testpassword', + project_name='testproject', + auth_url='http://example.com', + ) + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + cc = cloud_config.CloudConfig( + "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + cc.get_legacy_client('object-store', mock_client) + mock_client.assert_called_with( + preauthtoken=mock.ANY, + auth_version=u'2.0', + authurl='http://example.com', + key='testpassword', + os_options={ + 'auth_token': mock.ANY, + 'region_name': 'region-al', + 'object_storage_url': 'http://swift.example.com', + 'user_id': None, + 'user_domain_name': None, + 'tenant_name': 'testproject', + 'project_domain_name': None, + 'project_domain_id': None, + 'tenant_id': None, + 'service_type': 'object-store', + 'endpoint_type': 'public', + 'user_domain_id': None + }, + preauthurl='http://swift.example.com', + user='testuser') + + @mock.patch.object(cloud_config.CloudConfig, 'get_auth_args') + @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + def test_legacy_client_object_store( + self, mock_get_session_endpoint, mock_get_auth_args): mock_client = mock.Mock() mock_get_session_endpoint.return_value = 'http://example.com/v2' + mock_get_auth_args.return_value = {} config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) cc = cloud_config.CloudConfig( @@ -246,19 +332,33 @@ def test_legacy_client_object_store(self, mock_get_session_endpoint): cc.get_legacy_client('object-store', mock_client) mock_client.assert_called_with( preauthtoken=mock.ANY, + auth_version=u'2.0', + authurl=None, + key=None, os_options={ 'auth_token': mock.ANY, 'region_name': 'region-al', - 'object_storage_url': 'http://example.com/v2' + 'object_storage_url': 'http://example.com/v2', + 'user_id': None, + 'user_domain_name': None, + 'tenant_name': None, + 'project_domain_name': None, + 'project_domain_id': None, + 'tenant_id': None, + 'service_type': 'object-store', + 'endpoint_type': 'public', + 'user_domain_id': None }, preauthurl='http://example.com/v2', - auth_version='2.0') + user=None) + @mock.patch.object(cloud_config.CloudConfig, 'get_auth_args') @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') def test_legacy_client_object_store_timeout( - self, mock_get_session_endpoint): + self, mock_get_session_endpoint, mock_get_auth_args): mock_client = mock.Mock() mock_get_session_endpoint.return_value = 'http://example.com/v2' + mock_get_auth_args.return_value = {} config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) config_dict['api_timeout'] = 9 @@ -267,32 +367,59 @@ def test_legacy_client_object_store_timeout( cc.get_legacy_client('object-store', mock_client) mock_client.assert_called_with( preauthtoken=mock.ANY, + auth_version=u'2.0', + authurl=None, + key=None, os_options={ 'auth_token': mock.ANY, 'region_name': 'region-al', - 'object_storage_url': 'http://example.com/v2' + 'object_storage_url': 'http://example.com/v2', + 'user_id': None, + 'user_domain_name': None, + 'tenant_name': None, + 'project_domain_name': None, + 'project_domain_id': None, + 'tenant_id': None, + 'service_type': 'object-store', + 'endpoint_type': 'public', + 'user_domain_id': None }, preauthurl='http://example.com/v2', - auth_version='2.0', - timeout=9.0) + timeout=9.0, + user=None) - def test_legacy_client_object_store_endpoint(self): + @mock.patch.object(cloud_config.CloudConfig, 'get_auth_args') + def test_legacy_client_object_store_endpoint( + self, mock_get_auth_args): mock_client = mock.Mock() + mock_get_auth_args.return_value = {} config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) - config_dict['object_store_endpoint'] = 'http://example.com/v2' + config_dict['object_store_endpoint'] = 'http://example.com/swift' cc = cloud_config.CloudConfig( "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('object-store', mock_client) mock_client.assert_called_with( preauthtoken=mock.ANY, + auth_version=u'2.0', + authurl=None, + key=None, os_options={ 'auth_token': mock.ANY, 'region_name': 'region-al', - 'object_storage_url': 'http://example.com/v2' + 'object_storage_url': 'http://example.com/swift', + 'user_id': None, + 'user_domain_name': None, + 'tenant_name': None, + 'project_domain_name': None, + 'project_domain_id': None, + 'tenant_id': None, + 'service_type': 'object-store', + 'endpoint_type': 'public', + 'user_domain_id': None }, - preauthurl='http://example.com/v2', - auth_version='2.0') + preauthurl='http://example.com/swift', + user=None) @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') def test_legacy_client_image(self, mock_get_session_endpoint): From dcc9aad9c0c374f62c6152bb77d6db59dea59ea5 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 10 Feb 2016 19:48:48 -0500 Subject: [PATCH 0802/3836] Add a method to download an image from glance This commit adds the missing function to download image data from glance. The get_image() call returns the metadata about an image but there was no method to get the actual data. Change-Id: I8797f90ea4152dfed90b3311ceca098b2807ef7e --- shade/openstackcloud.py | 40 +++++++++++ shade/tests/functional/test_image.py | 22 +++++++ shade/tests/unit/test_image.py | 99 ++++++++++++++++++++++++++++ 3 files changed, 161 insertions(+) create mode 100644 shade/tests/unit/test_image.py diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index bc3fec3b8..9a791ee95 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1564,6 +1564,46 @@ def get_image(self, name_or_id, filters=None): """ return _utils._get_entity(self.search_images, name_or_id, filters) + def download_image(self, name_or_id, output_path=None, output_file=None): + """Download an image from glance by name or ID + + :param str name_or_id: Name or ID of the image. + :param output_path: the output path to write the image to. Either this + or output_file must be specified + :param output_file: a file object (or file-like object) to write the + image data to. Only write() will be called on this object. Either + this or output_path must be specified + + :raises: OpenStackCloudException in the event download_image is called + without exactly one of either output_path or output_file + :raises: OpenStackCloudResourceNotFound if no images are found matching + the name or id provided + """ + if output_path is None and output_file is None: + raise OpenStackCloudException('No output specified, an output path' + ' or file object is necessary to ' + 'write the image data to') + elif output_path is not None and output_file is not None: + raise OpenStackCloudException('Both an output path and file object' + ' were provided, however only one ' + 'can be used at once') + + image = self.search_images(name_or_id) + if len(image) == 0: + raise OpenStackCloudResourceNotFound( + "No images with name or id %s were found" % name_or_id) + image_contents = self.glance_client.images.data(image[0]['id']) + with _utils.shade_exceptions("Unable to download image"): + if output_path: + with open(output_path, 'wb') as fd: + for chunk in image_contents: + fd.write(chunk) + return + elif output_file: + for chunk in image_contents: + output_file.write(chunk) + return + def get_floating_ip(self, id, filters=None): """Get a floating IP by ID diff --git a/shade/tests/functional/test_image.py b/shade/tests/functional/test_image.py index e603acee9..1dc241694 100644 --- a/shade/tests/functional/test_image.py +++ b/shade/tests/functional/test_image.py @@ -19,6 +19,8 @@ Functional tests for `shade` image methods. """ +import filecmp +import os import tempfile from shade import openstack_cloud @@ -47,3 +49,23 @@ def test_create_image(self): wait=True) finally: self.cloud.delete_image(image_name, wait=True) + + def test_download_image(self): + test_image = tempfile.NamedTemporaryFile(delete=False) + self.addCleanup(os.remove, test_image.name) + test_image.write('\0' * 1024 * 1024) + test_image.close() + image_name = self.getUniqueString('image') + self.cloud.create_image(name=image_name, + filename=test_image.name, + disk_format='raw', + container_format='bare', + min_disk=10, + min_ram=1024, + wait=True) + self.addCleanup(self.cloud.delete_image, image_name, wait=True) + output = os.path.join(tempfile.gettempdir(), self.getUniqueString()) + self.cloud.download_image(image_name, output) + self.addCleanup(os.remove, output) + self.assertTrue(filecmp.cmp(test_image.name, output), + "Downloaded contents don't match created image") diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py new file mode 100644 index 000000000..6d35070d8 --- /dev/null +++ b/shade/tests/unit/test_image.py @@ -0,0 +1,99 @@ +# Copyright 2016 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. + +import tempfile +import uuid + +import mock +import six + +import shade +from shade import exc +from shade.tests import base + + +class TestImage(base.TestCase): + + def setUp(self): + super(TestImage, self).setUp() + self.cloud = shade.openstack_cloud(validate=False) + self.image_id = str(uuid.uuid4()) + self.fake_search_return = [{ + u'image_state': u'available', + u'container_format': u'bare', + u'min_ram': 0, + u'ramdisk_id': None, + u'updated_at': u'2016-02-10T05:05:02Z', + u'file': '/v2/images/' + self.image_id + '/file', + u'size': 3402170368, + u'image_type': u'snapshot', + u'disk_format': u'qcow2', + u'id': self.image_id, + u'schema': u'/v2/schemas/image', + u'status': u'active', + u'tags': [], + u'visibility': u'private', + u'locations': [{ + u'url': u'http://127.0.0.1/images/' + self.image_id, + u'metadata': {}}], + u'min_disk': 40, + u'virtual_size': None, + u'name': u'fake_image', + u'checksum': u'ee36e35a297980dee1b514de9803ec6d', + u'created_at': u'2016-02-10T05:03:11Z', + u'protected': False}] + self.output = six.BytesIO() + self.output.write(uuid.uuid4().bytes) + self.output.seek(0) + + def test_download_image_no_output(self): + self.assertRaises(exc.OpenStackCloudException, + self.cloud.download_image, 'fake_image') + + def test_download_image_two_outputs(self): + fake_fd = six.BytesIO() + self.assertRaises(exc.OpenStackCloudException, + self.cloud.download_image, 'fake_image', + output_path='fake_path', output_file=fake_fd) + + @mock.patch.object(shade.OpenStackCloud, 'search_images', return_value=[]) + def test_download_image_no_images_found(self, mock_search): + self.assertRaises(exc.OpenStackCloudResourceNotFound, + self.cloud.download_image, 'fake_image', + output_path='fake_path') + + @mock.patch.object(shade.OpenStackCloud, 'glance_client') + @mock.patch.object(shade.OpenStackCloud, 'search_images') + def test_download_image_with_fd(self, mock_search, mock_glance): + output_file = six.BytesIO() + mock_glance.images.data.return_value = self.output + mock_search.return_value = self.fake_search_return + self.cloud.download_image('fake_image', output_file=output_file) + mock_glance.images.data.assert_called_once_with(self.image_id) + output_file.seek(0) + self.output.seek(0) + self.assertEqual(output_file.read(), self.output.read()) + + @mock.patch.object(shade.OpenStackCloud, 'glance_client') + @mock.patch.object(shade.OpenStackCloud, 'search_images') + def test_download_image_with_path(self, mock_search, mock_glance): + output_file = tempfile.NamedTemporaryFile() + mock_glance.images.data.return_value = self.output + mock_search.return_value = self.fake_search_return + self.cloud.download_image('fake_image', + output_path=output_file.name) + mock_glance.images.data.assert_called_once_with(self.image_id) + output_file.seek(0) + self.output.seek(0) + self.assertEqual(output_file.read(), self.output.read()) From 7865abc22b7289b2679f6848395d4850d544d1f0 Mon Sep 17 00:00:00 2001 From: Clark Boylan Date: Wed, 17 Feb 2016 11:46:57 -0800 Subject: [PATCH 0803/3836] Add release notes Catch up the release notes from the previous release to current state. This does not catch up from the beginning of oscc's history. Change-Id: Ic981fdfbb79cd7fc70167091bdfed281c11eff03 --- ...tch-up-release-notes-e385fad34e9f3d6e.yaml | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 releasenotes/notes/catch-up-release-notes-e385fad34e9f3d6e.yaml diff --git a/releasenotes/notes/catch-up-release-notes-e385fad34e9f3d6e.yaml b/releasenotes/notes/catch-up-release-notes-e385fad34e9f3d6e.yaml new file mode 100644 index 000000000..e7b98afe3 --- /dev/null +++ b/releasenotes/notes/catch-up-release-notes-e385fad34e9f3d6e.yaml @@ -0,0 +1,22 @@ +--- +prelude: > + Swiftclient instantiation now provides authentication + information so that long lived swiftclient objects can + reauthenticate if necessary. This should be a temporary + situation until swiftclient supports keystoneauth + sessions at which point os-client-config will instantiate + swiftclient with a keystoneauth session. +features: + - Swiftclient instantiation now provides authentication + information so that long lived swiftclient objects can + reauthenticate if necessary. + - Add support for explicit v2password auth type. + - Add SSL support to VEXXHOST vendor profile. + - Add zetta.io cloud vendor profile. +fixes: + - Fix bug where project_domain_{name,id} was set even + if project_{name,id} was not set. +other: + - HPCloud vendor profile removed due to cloud shutdown. + - RunAbove vendor profile removed due to migration to + OVH. From 35ece66b4cc263816b9b28239901a5cbad61c8eb Mon Sep 17 00:00:00 2001 From: Arie Bregman Date: Thu, 18 Feb 2016 14:23:12 +0200 Subject: [PATCH 0804/3836] Fix formulation Fixed the formulation for the message that appears when cloud config couldn't be found. Change-Id: I1a4a83fe598d4eab52713061471fab8d1c46ec91 --- os_client_config/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 1c47ed72a..2f6adeb2b 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -432,7 +432,7 @@ def _get_base_cloud_config(self, name): # Only validate cloud name if one was given if name and name not in self.cloud_config['clouds']: raise exceptions.OpenStackConfigException( - "Named cloud {name} requested that was not found.".format( + "Cloud {name} was not found.".format( name=name)) our_cloud = self.cloud_config['clouds'].get(name, dict()) From ef263d6a239ebdcdbf3317a8176702052017df3a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 19 Feb 2016 13:39:44 +0000 Subject: [PATCH 0805/3836] Remove mock testing of os-client-config for swift This test does not test shade - it tests implementation details of os-client-config. We test those in os-client-config. Change-Id: I1a24e1718995b7ac508aec77c7dd351ef22cba74 --- shade/tests/unit/test_object.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index 2132ad1a7..84c543c93 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -16,7 +16,6 @@ import mock import os_client_config from os_client_config import cloud_config -from swiftclient import client as swift_client from swiftclient import service as swift_service from swiftclient import exceptions as swift_exc import testtools @@ -36,24 +35,6 @@ def setUp(self): self.cloud = OpenStackCloud( cloud_config=config.get_one_cloud(validate=False)) - @mock.patch.object(swift_client, 'Connection') - @mock.patch.object(cloud_config.CloudConfig, 'get_session') - def test_swift_client(self, get_session_mock, swift_mock): - session_mock = mock.Mock() - session_mock.get_endpoint.return_value = 'danzig' - session_mock.get_token.return_value = 'yankee' - get_session_mock.return_value = session_mock - - self.cloud.swift_client - swift_mock.assert_called_with( - preauthurl='danzig', - preauthtoken='yankee', - auth_version=mock.ANY, - os_options=dict( - object_storage_url='danzig', - auth_token='yankee', - region_name='')) - @mock.patch.object(cloud_config.CloudConfig, 'get_session') def test_swift_client_no_endpoint(self, get_session_mock): session_mock = mock.Mock() From 11a8527558fed6943b583b14dffc1c24818ce9a4 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 15 Feb 2016 16:38:12 -0500 Subject: [PATCH 0806/3836] Add support for provider network options This has been asked for by the Ansible community. Change-Id: Ib11beb42901cdf6b5c9a5d8f8e3a2a0f2fbf382d --- .../notes/net_provider-dd64b697476b7094.yaml | 3 ++ shade/openstackcloud.py | 18 +++++-- shade/tests/functional/test_network.py | 15 ++++++ shade/tests/unit/test_network.py | 53 +++++++++++++++++++ 4 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/net_provider-dd64b697476b7094.yaml diff --git a/releasenotes/notes/net_provider-dd64b697476b7094.yaml b/releasenotes/notes/net_provider-dd64b697476b7094.yaml new file mode 100644 index 000000000..65a007302 --- /dev/null +++ b/releasenotes/notes/net_provider-dd64b697476b7094.yaml @@ -0,0 +1,3 @@ +--- +features: + - Network provider options are now accepted in create_network(). diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index bc3fec3b8..aeec649a9 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1634,16 +1634,17 @@ def delete_keypair(self, name): "Unable to delete keypair %s: %s" % (name, e)) return True - # TODO(Shrews): This will eventually need to support tenant ID and - # provider networks, which are admin-level params. def create_network(self, name, shared=False, admin_state_up=True, - external=False): + external=False, provider=None): """Create a network. :param string name: Name of the network being created. :param bool shared: Set the network as shared. :param bool admin_state_up: Set the network administrative state to up. :param bool external: Whether this network is externally accessible. + :param dict provider: A dict of network provider options. Example:: + + { 'network_type': 'vlan', 'segmentation_id': 'vlan1' } :returns: The network object. :raises: OpenStackCloudException on operation error. @@ -1655,6 +1656,17 @@ def create_network(self, name, shared=False, admin_state_up=True, 'admin_state_up': admin_state_up, } + if provider: + if not isinstance(provider, dict): + raise OpenStackCloudException( + "Parameter 'provider' must be a dict") + # Only pass what we know + for attr in ('physical_network', 'network_type', + 'segmentation_id'): + if attr in provider: + arg = "provider:" + attr + network[arg] = provider[attr] + # Do not send 'router:external' unless it is explicitly # set since sending it *might* cause "Forbidden" errors in # some situations. It defaults to False in the client, anyway. diff --git a/shade/tests/functional/test_network.py b/shade/tests/functional/test_network.py index 432c63061..26d4264d1 100644 --- a/shade/tests/functional/test_network.py +++ b/shade/tests/functional/test_network.py @@ -67,6 +67,21 @@ def test_create_network_advanced(self): self.assertTrue(net1['shared']) self.assertFalse(net1['admin_state_up']) + def test_create_network_provider_flat(self): + net1 = self.cloud.create_network( + name=self.network_name, + shared=True, + provider={ + 'physical_network': 'private', + 'network_type': 'flat', + } + ) + self.assertIn('id', net1) + self.assertEqual(self.network_name, net1['name']) + self.assertEqual('flat', net1['provider:network_type']) + self.assertEqual('private', net1['provider:physical_network']) + self.assertIsNone(net1['provider:segmentation_id']) + def test_list_networks_filtered(self): net1 = self.cloud.create_network(name=self.network_name) self.assertIsNotNone(net1) diff --git a/shade/tests/unit/test_network.py b/shade/tests/unit/test_network.py index 11b39609e..e62c6e920 100644 --- a/shade/tests/unit/test_network.py +++ b/shade/tests/unit/test_network.py @@ -50,6 +50,59 @@ def test_create_network_external(self, mock_neutron): ) ) + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_network_provider(self, mock_neutron): + provider_opts = {'physical_network': 'mynet', + 'network_type': 'vlan', + 'segmentation_id': 'vlan1'} + self.cloud.create_network("netname", provider=provider_opts) + mock_neutron.create_network.assert_called_once_with( + body=dict( + network={ + 'name': 'netname', + 'shared': False, + 'admin_state_up': True, + 'provider:physical_network': + provider_opts['physical_network'], + 'provider:network_type': + provider_opts['network_type'], + 'provider:segmentation_id': + provider_opts['segmentation_id'], + } + ) + ) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_network_provider_ignored_value(self, mock_neutron): + provider_opts = {'physical_network': 'mynet', + 'network_type': 'vlan', + 'segmentation_id': 'vlan1', + 'should_not_be_passed': 1} + self.cloud.create_network("netname", provider=provider_opts) + mock_neutron.create_network.assert_called_once_with( + body=dict( + network={ + 'name': 'netname', + 'shared': False, + 'admin_state_up': True, + 'provider:physical_network': + provider_opts['physical_network'], + 'provider:network_type': + provider_opts['network_type'], + 'provider:segmentation_id': + provider_opts['segmentation_id'], + } + ) + ) + + def test_create_network_provider_wrong_type(self): + provider_opts = "invalid" + with testtools.ExpectedException( + shade.OpenStackCloudException, + "Parameter 'provider' must be a dict" + ): + self.cloud.create_network("netname", provider=provider_opts) + @mock.patch.object(shade.OpenStackCloud, 'get_network') @mock.patch.object(shade.OpenStackCloud, 'neutron_client') def test_delete_network(self, mock_neutron, mock_get): From f6bbd31dbfeb5ddfd968b8a9cdcb093a2b60d459 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 21 Feb 2016 13:49:12 -0800 Subject: [PATCH 0807/3836] Add ability to pass just filename to create_image There is a common use case of wanting to create a disk image and then upload it to the cloud where the name of the diskimage minus the file extension is the desired name of the image in the cloud. Support that easily by checking to see if the user passed in a filename as the first paramater and, if so, use it as the filename and strip the path and extension to use as the name. Also, document the method while we're in there. Change-Id: Iade5f071544775864534660b544a6a01b15a2adb --- shade/openstackcloud.py | 66 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 9a791ee95..251e6ed00 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2003,12 +2003,76 @@ def delete_image(self, name_or_id, wait=False, timeout=3600): if self.get_image(image.id) is None: return + def _get_name_and_filename(self, name): + # See if name points to an existing file + if os.path.exists(name): + # Neat. Easy enough + return (os.path.splitext(os.path.basename(name))[0], name) + + # Try appending the disk format + name_with_ext = '.'.join(( + name, self.cloud_config.config['image_format'])) + if os.path.exists(name_with_ext): + return (os.path.basename(name), name_with_ext) + + raise OpenStackCloudException( + 'No filename parameter was given to create_image,' + ' and {name} was not the path to an existing file.' + ' Please provide either a path to an existing file' + ' or a name and a filename'.format(name=name)) + def create_image( - self, name, filename, container='images', + self, name, filename=None, container='images', md5=None, sha256=None, disk_format=None, container_format=None, disable_vendor_agent=True, wait=False, timeout=3600, **kwargs): + """Upload an image to Glance. + + :param str name: Name of the image to create. If it is a pathname + of an image, the name will be constructed from the + extensionless basename of the path. + :param str filename: The path to the file to upload, if needed. + (optional, defaults to None) + :param str container: Name of the container in swift where images + should be uploaded for import if the cloud + requires such a thing. (optiona, defaults to + 'images') + :param str md5: md5 sum of the image file. If not given, an md5 will + be calculated. + :param str sha256: sha256 sum of the image file. If not given, an md5 + will be calculated. + :param str disk_format: The disk format the image is in. (optional, + defaults to the os-client-config config value + for this cloud) + :param str container_format: The container format the image is in. + (optional, defaults to the + os-client-config config value for this + cloud) + :param bool disable_vendor_agent: Whether or not to append metadata + flags to the image to inform the + cloud in question to not expect a + vendor agent to be runing. + (optional, defaults to True) + :param bool wait: If true, waits for image to be created. Defaults to + true - however, be aware that one of the upload + methods is always synchronous. + :param timeout: Seconds to wait for image creation. None is forever. + + Additional kwargs will be passed to the image creation as additional + metadata for the image. + + :returns: A ``munch.Munch`` of the Image object + + :raises: OpenStackCloudException if there are problems uploading + """ + + if not disk_format: + disk_format = self.cloud_config.config['image_format'] + + # If there is no filename, see if name is actually the filename + if not filename: + name, filename = self._get_name_and_filename(name) if not disk_format: disk_format = self.cloud_config.config['image_format'] From 7a4993da4190d92cb2f023d84e0e9311db38f0bf Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 22 Feb 2016 06:30:31 -0800 Subject: [PATCH 0808/3836] Allow session_client to take the same args as make_client make_client is a great, simple yet flexible way to get a fully featured Client object. simple_client is similar for Session objects, but lacks the argparse and arbitrary kwargs that make_client - plus it has a weird name. Since adding those two features to make_client did not make it too confusing - do the same for simple_client. Also, rename it to session_client (with a backwards-compat alias) and add it to the README docs. In the process of doing this, extract the "get me a cloud config" functinality into an additional helper function - get_config. Change-Id: Iadd24dfa021f870b3e5858bab8cd91fc96a373c2 --- README.rst | 17 +++++++++-- os_client_config/__init__.py | 28 +++++++++++-------- .../session-client-b581a6e5d18c8f04.yaml | 6 ++++ 3 files changed, 38 insertions(+), 13 deletions(-) create mode 100644 releasenotes/notes/session-client-b581a6e5d18c8f04.yaml diff --git a/README.rst b/README.rst index f078f3cee..54a5631ed 100644 --- a/README.rst +++ b/README.rst @@ -362,8 +362,6 @@ will get you a fully configured `novaclient` instance. .. code-block:: python - import argparse - import os_client_config nova = os_client_config.make_client('compute') @@ -382,6 +380,21 @@ If you want to do the same thing but also support command line parsing. If you want to get fancier than that in your python, then the rest of the API is available to you. But often times, you just want to do the one thing. +Constructing Mounted Session Objects +------------------------------------ + +What if you want to make direct REST calls via a Session interface? You're +in luck. The same interface for `make_client` is supported for `session_client` +and will return you a keystoneauth Session object that is mounted on the +endpoint for the service you're looking for. + + import os_client_config + + session = os_client_config.session_client('compute', cloud='vexxhost') + + response = session.get('/servers') + server_list = response.json()['servers'] + Source ------ diff --git a/os_client_config/__init__.py b/os_client_config/__init__.py index 52fcb8511..6f78d2fca 100644 --- a/os_client_config/__init__.py +++ b/os_client_config/__init__.py @@ -18,7 +18,18 @@ from os_client_config.config import OpenStackConfig # noqa -def simple_client(service_key, cloud=None, region_name=None): +def get_config(service_key=None, options=None, **kwargs): + config = OpenStackConfig() + if options: + config.register_argparse_options(options, sys.argv, service_key) + parsed_options = options.parse_known_args(sys.argv) + else: + parsed_options = None + + return config.get_one_cloud(options=parsed_options, **kwargs) + + +def session_client(service_key, options=None, **kwargs): """Simple wrapper function. It has almost no features. This will get you a raw requests Session Adapter that is mounted @@ -31,8 +42,10 @@ def simple_client(service_key, cloud=None, region_name=None): get_session_client on it. This function is to make it easy to poke at OpenStack REST APIs with a properly configured keystone session. """ - return OpenStackConfig().get_one_cloud( - cloud=cloud, region_name=region_name).get_session_client(service_key) + cloud = get_config(service_key=service_key, options=options, **kwargs) + return cloud.get_session_client(service_key) +# Backwards compat - simple_client was a terrible name +simple_client = session_client def make_client(service_key, constructor=None, options=None, **kwargs): @@ -45,14 +58,7 @@ def make_client(service_key, constructor=None, options=None, **kwargs): variables and clouds.yaml - and takes as **kwargs anything you'd expect to pass in. """ + cloud = get_config(service_key=service_key, options=options, **kwargs) if not constructor: constructor = cloud_config._get_client(service_key) - config = OpenStackConfig() - if options: - config.register_argparse_options(options, sys.argv, service_key) - parsed_options = options.parse_args(sys.argv) - else: - parsed_options = None - - cloud = config.get_one_cloud(options=parsed_options, **kwargs) return cloud.get_legacy_client(service_key, constructor) diff --git a/releasenotes/notes/session-client-b581a6e5d18c8f04.yaml b/releasenotes/notes/session-client-b581a6e5d18c8f04.yaml new file mode 100644 index 000000000..11219016b --- /dev/null +++ b/releasenotes/notes/session-client-b581a6e5d18c8f04.yaml @@ -0,0 +1,6 @@ +--- +features: + - Added kwargs and argparse processing for session_client. +deprecations: + - Renamed simple_client to session_client. simple_client + will remain as an alias for backwards compat. From 03d5659d8b45b0450a86956147cc2d1f27be66ac Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 22 Feb 2016 06:37:13 -0800 Subject: [PATCH 0809/3836] Update the README a bit Cleaned up example references to now-not-existing HP Public Cloud. Also added a named-cloud entry to the make_client section. Change-Id: I398c438e22eb84d6079a5c45f068753c3bcaa216 --- README.rst | 47 +++++++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/README.rst b/README.rst index 54a5631ed..2e584bd53 100644 --- a/README.rst +++ b/README.rst @@ -88,23 +88,21 @@ An example config file is probably helpful: .. code-block:: yaml clouds: - mordred: - profile: hp + mtvexx: + profile: vexxhost auth: username: mordred@inaugust.com password: XXXXXXXXX project_name: mordred@inaugust.com - region_name: region-b.geo-1 - dns_service_type: hpext:dns - compute_api_version: 1.1 - monty: + region_name: ca-ymq-1 + dns_api_version: 1 + mordred: + region_name: RegionOne auth: - auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0 - username: monty.taylor@hp.com - password: XXXXXXXX - project_name: monty.taylor@hp.com-default-tenant - region_name: region-b.geo-1 - dns_service_type: hpext:dns + username: 'mordred' + password: XXXXXXX + project_name: 'shade' + auth_url: 'https://montytaylor-sjc.openstack.blueboxgrid.com:5001/v2.0' infra: profile: rackspace auth: @@ -221,14 +219,14 @@ are connecting to OpenStack can share a cache should you desire. server: 5 flavor: -1 clouds: - mordred: - profile: hp + mtvexx: + profile: vexxhost auth: username: mordred@inaugust.com password: XXXXXXXXX project_name: mordred@inaugust.com - region_name: region-b.geo-1 - dns_service_type: hpext:dns + region_name: ca-ymq-1 + dns_api_version: 1 IPv6 @@ -247,13 +245,14 @@ environment variable. client: force_ipv4: true clouds: - mordred: - profile: hp + mtvexx: + profile: vexxhost auth: username: mordred@inaugust.com password: XXXXXXXXX project_name: mordred@inaugust.com - region_name: region-b.geo-1 + region_name: ca-ymq-1 + dns_api_version: 1 monty: profile: rax auth: @@ -316,7 +315,7 @@ Get a named cloud. import os_client_config cloud_config = os_client_config.OpenStackConfig().get_one_cloud( - 'hp', region_name='region-b.geo-1') + 'internap', region_name='ams01') print(cloud_config.name, cloud_config.region, cloud_config.config) Or, get all of the clouds. @@ -366,6 +365,14 @@ will get you a fully configured `novaclient` instance. nova = os_client_config.make_client('compute') +If you want to do the same thing but on a named cloud. + +.. code-block:: python + + import os_client_config + + nova = os_client_config.make_client('compute', cloud='mtvexx') + If you want to do the same thing but also support command line parsing. .. code-block:: python From d0979597275d514b0b1a1266f98513b99fa1639e Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 26 Feb 2016 16:49:15 -0500 Subject: [PATCH 0810/3836] Recognize subclasses of list types Change-Id: I8b46d11368bf33ad073ed3b28eb2282ee70ffbe3 --- shade/task_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/task_manager.py b/shade/task_manager.py index e949e0f6f..47b9e0dbf 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -79,7 +79,7 @@ def wait(self): six.reraise(type(self._exception), self._exception, self._traceback) - if type(self._result) == list: + if isinstance(self._result, list): return meta.obj_list_to_dict(self._result) elif type(self._result) not in (bool, int, float, str, set, tuple, types.GeneratorType): From a7fe2520aea62372834092362b7cd024982d8b69 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 26 Feb 2016 12:25:39 -0500 Subject: [PATCH 0811/3836] Allow testing against Ansible dev branch Adds support for a new environment variable that will trigger testing against the upstream, development version of Ansible. This will help us catch breakages in Ansible that would affect the OpenStack modules. A new, non-voting job is expected to set this variable. Change-Id: I7ac84487dd323ef95191fab966244da586b37cd3 --- shade/tests/ansible/hooks/post_test_hook.sh | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/shade/tests/ansible/hooks/post_test_hook.sh b/shade/tests/ansible/hooks/post_test_hook.sh index bf4444ead..986d76775 100755 --- a/shade/tests/ansible/hooks/post_test_hook.sh +++ b/shade/tests/ansible/hooks/post_test_hook.sh @@ -19,9 +19,21 @@ cd $SHADE_DIR sudo chown -R jenkins:stack $SHADE_DIR echo "Running shade Ansible test suite" -set +e -sudo -E -H -u jenkins tox -eansible -EXIT_CODE=$? -set -e + +if [ ${SHADE_ANSIBLE_DEV:-0} -eq 1 ] +then + # Use the upstream development version of Ansible + set +e + sudo -E -H -u jenkins tox -eansible -- -d + EXIT_CODE=$? + set -e +else + # Use the release version of Ansible + set +e + sudo -E -H -u jenkins tox -eansible + EXIT_CODE=$? + set -e +fi + exit $EXIT_CODE From 11b834a2d80d30cad2c5cc08a325a5a330f348bc Mon Sep 17 00:00:00 2001 From: Joshua Hesketh Date: Mon, 29 Feb 2016 15:17:30 +1100 Subject: [PATCH 0812/3836] Catch failures with particular clouds If one cloud is unavailable list_hosts will fail and return no results even if another cloud had a listing. Add a flag to allow users to pull results from whichever clouds they can. This is to allow infra to continue working across whichever clouds are up should one fail. Change-Id: Ic2bb2a1642dedead73d7c2e4e60ec59ae8299ef0 --- shade/inventory.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/shade/inventory.py b/shade/inventory.py index 42863f070..3fcb0f031 100644 --- a/shade/inventory.py +++ b/shade/inventory.py @@ -59,19 +59,23 @@ def __init__( for cloud in self.clouds: cloud._cache.invalidate() - def list_hosts(self, expand=True): + def list_hosts(self, expand=True, fail_on_cloud_config=True): hostvars = [] for cloud in self.clouds: - - # Cycle on servers - for server in cloud.list_servers(): - - if expand: - server_vars = cloud.get_openstack_vars(server) - else: - server_vars = meta.add_server_interfaces(cloud, server) - hostvars.append(server_vars) + try: + # Cycle on servers + for server in cloud.list_servers(): + + if expand: + server_vars = cloud.get_openstack_vars(server) + else: + server_vars = meta.add_server_interfaces(cloud, server) + hostvars.append(server_vars) + except shade.OpenStackCloudException: + # Don't fail on one particular cloud as others may work + if fail_on_cloud_config: + raise return hostvars From 795750bfde6a4ab3be5f7f1e34fe7ea6f91c6f2b Mon Sep 17 00:00:00 2001 From: Tristan Cacqueray Date: Thu, 4 Feb 2016 00:26:00 -0500 Subject: [PATCH 0813/3836] Fix heat create_stack and delete_stack This change attempts to fix the create_stack and delete_stack method: * get_stack doesn't have a cache parameter * stacks.delete only take one parameter, the stack id Change-Id: Idc6a4c5a3a90808db7204c7187d1a8779803e4a7 --- shade/_tasks.py | 2 +- shade/openstackcloud.py | 2 +- shade/tests/unit/test_stack.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index d4b23c4da..c50b3fb2b 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -729,4 +729,4 @@ def main(self, client): class StackDelete(task_manager.Task): def main(self, client): - return client.heat_client.stacks.delete(**self.args) + return client.heat_client.stacks.delete(self.args['id']) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 80998df46..e7c1a23b9 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -787,7 +787,7 @@ def create_stack( for count in _utils._iterate_timeout( timeout, "Timed out waiting for heat stack to finish"): - stack = self.get_stack(name, cache=False) + stack = self.get_stack(name) if stack: return stack diff --git a/shade/tests/unit/test_stack.py b/shade/tests/unit/test_stack.py index 0cd32d5d8..54f5ec243 100644 --- a/shade/tests/unit/test_stack.py +++ b/shade/tests/unit/test_stack.py @@ -89,7 +89,7 @@ def test_delete_stack(self, mock_heat, mock_get): mock_get.return_value = stack self.assertTrue(self.cloud.delete_stack('stack_name')) mock_get.assert_called_once_with('stack_name') - mock_heat.stacks.delete.assert_called_once_with(id=stack['id']) + mock_heat.stacks.delete.assert_called_once_with(stack['id']) @mock.patch.object(shade.OpenStackCloud, 'get_stack') @mock.patch.object(shade.OpenStackCloud, 'heat_client') From 80e7c428530a73402b99223a478f9e8f1c39ac64 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 1 Mar 2016 13:49:13 -0500 Subject: [PATCH 0814/3836] os_router playbook cleanup The playbook testing os_router was not cleaning up the external network it created. Change-Id: I49abe439de6ed9a8d844a505e8552c779f29da03 --- shade/tests/ansible/roles/router/tasks/main.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/shade/tests/ansible/roles/router/tasks/main.yml b/shade/tests/ansible/roles/router/tasks/main.yml index 4e98987bf..9987f4c9b 100644 --- a/shade/tests/ansible/roles/router/tasks/main.yml +++ b/shade/tests/ansible/roles/router/tasks/main.yml @@ -63,8 +63,14 @@ state: absent name: shade_subnet2 -- name: Delete network +- name: Delete internal network os_network: cloud: "{{ cloud }}" state: absent name: "{{ network_name }}" + +- name: Delete external network + os_network: + cloud: "{{ cloud }}" + state: absent + name: "{{ external_network_name }}" From f236869b77cb61e09b4be11aff4d88c83927e38e Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 1 Mar 2016 14:18:04 -0500 Subject: [PATCH 0815/3836] Fix create_server() with a named network If 'network' was supplied to create_server() along with an empty 'nics' list, we would never attempt to find the network. The os_server Ansible module uses the API this way. This treats an empty list the same as if the 'nics' parameter were never supplied. Change-Id: Idc844fab2c4c08f158c892104d065e5554ed90f3 --- ...e_server_network_fix-c4a56b31d2850a4b.yaml | 6 +++++ shade/openstackcloud.py | 2 +- shade/tests/unit/test_create_server.py | 25 +++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/create_server_network_fix-c4a56b31d2850a4b.yaml diff --git a/releasenotes/notes/create_server_network_fix-c4a56b31d2850a4b.yaml b/releasenotes/notes/create_server_network_fix-c4a56b31d2850a4b.yaml new file mode 100644 index 000000000..9f9bd5474 --- /dev/null +++ b/releasenotes/notes/create_server_network_fix-c4a56b31d2850a4b.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - The create_server() API call would not use the supplied 'network' + parameter if the 'nics' parameter was also supplied, even though it would + be an empty list. It now uses 'network' if 'nics' is not supplied or if + it is an empty list. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 80998df46..6caeffacc 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3350,7 +3350,7 @@ def create_server( raise OpenStackCloudException( 'nics parameter to create_server takes a list of dicts.' ' Got: {nics}'.format(nics=kwargs['nics'])) - if network and 'nics' not in kwargs: + if network and ('nics' not in kwargs or not kwargs['nics']): network_obj = self.get_network(name_or_id=network) if not network_obj: raise OpenStackCloudException( diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index 211ac977b..5ff0b1d7e 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -250,3 +250,28 @@ def test_create_server_no_addresses(self, mock_sleep): OpenStackCloudException, self.client.create_server, 'server-name', 'image-id', 'flavor-id', wait=True) + + @patch('shade.OpenStackCloud.nova_client') + @patch('shade.OpenStackCloud.get_network') + def test_create_server_network_with_no_nics(self, mock_get_network, + mock_nova): + """ + Verify that if 'network' is supplied, and 'nics' is not, that we + attempt to get the network for the server. + """ + self.client.create_server('server-name', 'image-id', 'flavor-id', + network='network-name') + mock_get_network.assert_called_once_with(name_or_id='network-name') + + @patch('shade.OpenStackCloud.nova_client') + @patch('shade.OpenStackCloud.get_network') + def test_create_server_network_with_empty_nics(self, + mock_get_network, + mock_nova): + """ + Verify that if 'network' is supplied, along with an empty 'nics' list, + it's treated the same as if 'nics' were not included. + """ + self.client.create_server('server-name', 'image-id', 'flavor-id', + network='network-name', nics=[]) + mock_get_network.assert_called_once_with(name_or_id='network-name') From 34799e9f9ebdc8b8b49e8980388b1fc377ffac43 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 26 Feb 2016 11:46:20 -0500 Subject: [PATCH 0816/3836] Add test for os_server Ansible module We haven't been able to test os_server because we haven't had a way to reliably determine the image name (which changes from time to time based on version) from the playbook. A stupidly clever hack is to just grep out the name from a shell call to the openstack client and pass that in to the playbook from the command line. The playbook is simple for now for the initial commit. I expect new tasks to be added to it later. Change-Id: I3caedfd4c805126c8da1fc8bab06ccda3fd96528 --- extras/run-ansible-tests.sh | 12 ++++- .../tests/ansible/roles/server/tasks/main.yml | 46 +++++++++++++++++++ .../tests/ansible/roles/server/vars/main.yaml | 3 ++ shade/tests/ansible/run.yml | 1 + test-requirements.txt | 1 + 5 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 shade/tests/ansible/roles/server/tasks/main.yml create mode 100644 shade/tests/ansible/roles/server/vars/main.yaml diff --git a/extras/run-ansible-tests.sh b/extras/run-ansible-tests.sh index 146f5436b..a794c454b 100755 --- a/extras/run-ansible-tests.sh +++ b/extras/run-ansible-tests.sh @@ -81,4 +81,14 @@ then tag_opt="--tags ${TAGS}" fi -ansible-playbook -vvv ./shade/tests/ansible/run.yml -e "cloud=${CLOUD}" ${tag_opt} +# Until we have a module that lets us determine the image we want from +# within a playbook, we have to find the image here and pass it in. +# We use the openstack client instead of nova client since it can use clouds.yaml. +IMAGE=`openstack --os-cloud=${CLOUD} image list -f value -c Name | grep -v -e ramdisk -e kernel` +if [ $? -ne 0 ] +then + echo "Failed to find Cirros image" + exit 1 +fi + +ansible-playbook -vvv ./shade/tests/ansible/run.yml -e "cloud=${CLOUD} image=${IMAGE}" ${tag_opt} diff --git a/shade/tests/ansible/roles/server/tasks/main.yml b/shade/tests/ansible/roles/server/tasks/main.yml new file mode 100644 index 000000000..50be33f6d --- /dev/null +++ b/shade/tests/ansible/roles/server/tasks/main.yml @@ -0,0 +1,46 @@ +--- +- name: Create server with meta as CSV + os_server: + cloud: "{{ cloud }}" + state: present + name: "{{ server_name }}" + image: "{{ image }}" + flavor: "{{ flavor }}" + network: "{{ server_network }}" + auto_floating_ip: false + meta: "key1=value1,key2=value2" + wait: true + register: server + +- debug: var=server + +- name: Delete server with meta as CSV + os_server: + cloud: "{{ cloud }}" + state: absent + name: "{{ server_name }}" + wait: true + +- name: Create server with meta as dict + os_server: + cloud: "{{ cloud }}" + state: present + name: "{{ server_name }}" + image: "{{ image }}" + flavor: "{{ flavor }}" + auto_floating_ip: false + network: "{{ server_network }}" + meta: + key1: value1 + key2: value2 + wait: true + register: server + +- debug: var=server + +- name: Delete server with meta as dict + os_server: + cloud: "{{ cloud }}" + state: absent + name: "{{ server_name }}" + wait: true diff --git a/shade/tests/ansible/roles/server/vars/main.yaml b/shade/tests/ansible/roles/server/vars/main.yaml new file mode 100644 index 000000000..3db7edf8a --- /dev/null +++ b/shade/tests/ansible/roles/server/vars/main.yaml @@ -0,0 +1,3 @@ +server_network: private +server_name: ansible_server +flavor: m1.tiny diff --git a/shade/tests/ansible/run.yml b/shade/tests/ansible/run.yml index c3a973a29..e935886d3 100644 --- a/shade/tests/ansible/run.yml +++ b/shade/tests/ansible/run.yml @@ -14,6 +14,7 @@ - { role: port, tags: port } - { role: router, tags: router } - { role: security_group, tags: security_group } + - { role: server, tags: server } - { role: subnet, tags: subnet } - { role: user, tags: user } - { role: user_group, tags: user_group } diff --git a/test-requirements.txt b/test-requirements.txt index d3b6fb988..ee73a8c15 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,6 +4,7 @@ coverage>=3.6 discover fixtures>=0.3.14 mock>=1.0 +python-openstackclient>=2.1.0 python-subunit oslosphinx>=2.2.0 # Apache-2.0 sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 From a9d155fd53f19be497c3becda62a3fbb84d9c8aa Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 1 Mar 2016 15:00:58 -0500 Subject: [PATCH 0817/3836] Use isinstance() for result type checking Shade has been broken in the past by the underlying clients deciding to change their API and return a subclass of the thing they used to return. For this reason, let's use isinstance() instead of type(). Change-Id: I1e50d5314fb2cd7d7f8fcf77a3714704ca738672 --- shade/task_manager.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/shade/task_manager.py b/shade/task_manager.py index 47b9e0dbf..53ee6d2b1 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -79,10 +79,17 @@ def wait(self): six.reraise(type(self._exception), self._exception, self._traceback) + # NOTE(Shrews): Since the client API might decide to subclass one + # of these result types, we use isinstance() here instead of type(). if isinstance(self._result, list): return meta.obj_list_to_dict(self._result) - elif type(self._result) not in (bool, int, float, str, set, - tuple, types.GeneratorType): + elif (not isinstance(self._result, bool) and + not isinstance(self._result, int) and + not isinstance(self._result, float) and + not isinstance(self._result, str) and + not isinstance(self._result, set) and + not isinstance(self._result, tuple) and + not isinstance(self._result, types.GeneratorType)): return meta.obj_to_dict(self._result) else: return self._result From 53adfb46ea8c785a1ecfb0808fb91cf60773dc1e Mon Sep 17 00:00:00 2001 From: Clark Boylan Date: Tue, 1 Mar 2016 20:26:15 -0800 Subject: [PATCH 0818/3836] Invalidate volume cache when waiting for attach We need to invalidate the volume cache when waiting for volume attach to complete otherwise we never see the attachment complete (the cached data is perpetually stale). Change-Id: If875997b7fb6682ef4752852c58ea1b4cf52de91 --- shade/openstackcloud.py | 1 + 1 file changed, 1 insertion(+) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index e7c1a23b9..ccca8bfe1 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2432,6 +2432,7 @@ def attach_volume(self, server, volume, device=None, timeout, "Timeout waiting for volume %s to attach." % volume['id']): try: + self.list_volumes.invalidate(self) vol = self.get_volume(volume['id']) except Exception: self.log.debug( From b4411cbadf0035cb59846d95940391d42b089e56 Mon Sep 17 00:00:00 2001 From: Ian Wienand Date: Wed, 2 Mar 2016 14:39:26 +1100 Subject: [PATCH 0819/3836] Also reset swift service object at upload time When you follow through swiftclient.service.SwiftService() it ends up creating a swift swiftclient.connection.Connection() object within it [1]. As noted in change Ib35dd49627bc2209060848e719e3cec40dbb4f2a ; the swift client object does not support keystone sessions, so does not handle having an expired token (tokens for RAX, the cloud we see this issue with in nodepool, expire after 24 hours). This leads to a very confusing exception about "No tenant specified" [2] which I believe is actually somewhat of a red-herring; what has happened is that the authentication failed, the client inside the SwiftService object doing the upload hits its retry path and we end an authentication failure path [3] that outputs this message. The split between what happens in a swiftclient Connection() object versus a SwiftService() object further suggests this is the failure point -- you use the client object to do things list list/create/delete containers, but the SwiftService object provides the wrappers that handle the actual upload of the objects. This explains why we see the exception raised in the upload image path, but not the creation of the containers to hold the image. As with the swift client issue, the simplest way around this is to always request a new SwiftService object when uploading a new image. [1] http://git.openstack.org/cgit/openstack/python-swiftclient/tree/swiftclient/service.py#n226 [2] http://paste.openstack.org/show/488685/ [3] http://git.openstack.org/cgit/openstack/python-swiftclient/tree/swiftclient/client.py#n1544 Change-Id: I1c9d71ec828eb7d67045fb6bef468dc20e67292b --- shade/openstackcloud.py | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index e7c1a23b9..31c84ae3e 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -221,11 +221,13 @@ def invalidate(self): self._neutron_client = None self._nova_client = None self._swift_client = None - # Lock used to reset client as swift client does not - # support keystone sessions meaning that we have to make - # a new client in order to get new auth prior to operations. - self._swift_client_lock = threading.Lock() self._swift_service = None + # Lock used to reset swift client. Since swift client does not + # support keystone sessions, we we have to make a new client + # in order to get new auth prior to operations, otherwise + # long-running sessions will fail. + self._swift_client_lock = threading.Lock() + self._swift_service_lock = threading.Lock() self._trove_client = None self._local_ipv6 = _utils.localhost_supports_ipv6() @@ -727,16 +729,18 @@ def swift_client(self): @property def swift_service(self): - if self._swift_service is None: - with _utils.shade_exceptions("Error constructing swift client"): - endpoint = self.get_session_endpoint( - service_key='object-store') - options = dict(os_auth_token=self.auth_token, - os_storage_url=endpoint, - os_region_name=self.region_name) - self._swift_service = swiftclient.service.SwiftService( - options=options) - return self._swift_service + with self._swift_service_lock: + if self._swift_service is None: + with _utils.shade_exceptions("Error constructing " + "swift client"): + endpoint = self.get_session_endpoint( + service_key='object-store') + options = dict(os_auth_token=self.auth_token, + os_storage_url=endpoint, + os_region_name=self.region_name) + self._swift_service = swiftclient.service.SwiftService( + options=options) + return self._swift_service @property def cinder_client(self): @@ -2112,8 +2116,13 @@ def _upload_image_put(self, name, filename, **image_kwargs): def _upload_image_task( self, name, filename, container, current_image, wait, timeout, **image_properties): + + # get new client sessions with self._swift_client_lock: self._swift_client = None + with self._swift_service_lock: + self._swift_service_lock = None + self.create_object( container, name, filename, md5=image_properties.get('md5', None), From c12d5020b3fb4cf7d55daad6ef0729d1d2c9778f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 2 Mar 2016 08:23:42 -0600 Subject: [PATCH 0820/3836] Pass username/password to SwiftService Similar to the change we made for swift Connection in os-client-config, pass any username/password info we have into the swift SwiftService object so that if it needs to re-auth it has the information. Change-Id: Ie850b8831d0192420289960b210b7a520c8c3322 --- shade/openstackcloud.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 31c84ae3e..9cd411c9a 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -727,6 +727,29 @@ def swift_client(self): 'object-store', swiftclient.client.Connection) return self._swift_client + def _get_swift_kwargs(self): + auth_version = self.cloud_config.get_api_version('identity') + auth_args = self.cloud_config.config.get('auth', {}) + os_options = {'auth_version': auth_version} + if auth_version == '2.0': + os_options['os_tenant_name'] = auth_args.get('project_name') + os_options['os_tenant_id'] = auth_args.get('project_id') + else: + os_options['os_project_name'] = auth_args.get('project_name') + os_options['os_project_id'] = auth_args.get('project_id') + + for key in ( + 'username', + 'password', + 'auth_url', + 'user_id', + 'project_domain_id', + 'project_domain_name', + 'user_domain_id', + 'user_domain_name'): + os_options['os_{key}'.format(key=key)] = auth_args.get(key) + return os_options + @property def swift_service(self): with self._swift_service_lock: @@ -738,6 +761,7 @@ def swift_service(self): options = dict(os_auth_token=self.auth_token, os_storage_url=endpoint, os_region_name=self.region_name) + options.update(self._get_swift_kwargs()) self._swift_service = swiftclient.service.SwiftService( options=options) return self._swift_service From 0f23cb8fa2605acfde4b0c6429fcc1d7e6f221fd Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 2 Mar 2016 09:55:40 -0600 Subject: [PATCH 0821/3836] Add debug message about file hash calculation When debugging image uploads, it can seem like shade has hung when it's in fact calculating file hashes. Emit a debug message to indicate this has been done. Change-Id: Ib0fde6ad96de6b6ba31e5c0b1b2f1bb54aad6f1e --- shade/openstackcloud.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 9cd411c9a..4d6ed4a51 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3686,6 +3686,8 @@ def get_container_access(self, name): def _get_file_hashes(self, filename): if filename not in self._file_hash_cache: + self.log.debug( + 'Calculating hashes for {filename}'.format(filename=filename)) md5 = hashlib.md5() sha256 = hashlib.sha256() with open(filename, 'rb') as file_obj: @@ -3694,6 +3696,11 @@ def _get_file_hashes(self, filename): sha256.update(chunk) self._file_hash_cache[filename] = dict( md5=md5.hexdigest(), sha256=sha256.hexdigest()) + self.log.debug( + "Image file {filename} md5:{md5} sha256:{sha256}".format( + filename=filename, + md5=self._file_hash_cache[filename]['md5'], + sha256=self._file_hash_cache[filename]['sha256'])) return (self._file_hash_cache[filename]['md5'], self._file_hash_cache[filename]['sha256']) From 3850774d8f3f766487462a9bb05e68eb9c8ffe91 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 3 Mar 2016 15:19:07 -0500 Subject: [PATCH 0822/3836] Fixes for latest cinder and neutron clients In obj_to_dict, if we get a dict-like-object, we should convert that with Munch. Otherwise, we just convert its properties and not values. When comparing cinder things, do not compare the entire structure since what we get from the create call can be different than what we get from the list call. Just compare IDs. Change-Id: Iada5ff76cf9192a2c10e117536546989633c290e Co-Authored-By: Monty Taylor Co-Authored-By: Sam Yaple --- shade/meta.py | 19 +++++++++++++++---- shade/tests/functional/test_volume.py | 21 +++++++++++---------- shade/tests/unit/test_caching.py | 10 ++++++++++ shade/tests/unit/test_meta.py | 10 ++++++++++ 4 files changed, 46 insertions(+), 14 deletions(-) diff --git a/shade/meta.py b/shade/meta.py index c2d492bf2..aac20f113 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -334,17 +334,28 @@ def obj_to_dict(obj): """ if obj is None: return None - elif type(obj) == munch.Munch or hasattr(obj, 'mock_add_spec'): + elif isinstance(obj, munch.Munch) or hasattr(obj, 'mock_add_spec'): # If we obj_to_dict twice, don't fail, just return the munch # Also, don't try to modify Mock objects - that way lies madness return obj - elif type(obj) == dict: - return munch.Munch(obj) + elif hasattr(obj, '_shadeunittest'): + # Hook for unittesting + instance = munch.Munch() elif hasattr(obj, 'schema') and hasattr(obj, 'validate'): # It's a warlock return warlock_to_dict(obj) + elif isinstance(obj, dict): + # The new request-id tracking spec: + # https://specs.openstack.org/openstack/nova-specs/specs/juno/approved/log-request-id-mappings.html + # adds a request-ids attribute to returned objects. It does this even + # with dicts, which now become dict subclasses. So we want to convert + # the dict we get, but we also want it to fall through to object + # attribute processing so that we can also get the request_ids + # data into our resulting object. + instance = munch.Munch(obj) + else: + instance = munch.Munch() - instance = munch.Munch() for key in dir(obj): value = getattr(obj, key) if isinstance(value, NON_CALLABLES) and not key.startswith('_'): diff --git a/shade/tests/functional/test_volume.py b/shade/tests/functional/test_volume.py index 11b4f4ece..bcf2dfc04 100644 --- a/shade/tests/functional/test_volume.py +++ b/shade/tests/functional/test_volume.py @@ -45,13 +45,14 @@ def test_volumes(self): display_name=snapshot_name ) - self.assertIn(volume, self.cloud.list_volumes()) - self.assertIn(snapshot, self.cloud.list_volume_snapshots()) - self.assertEqual(snapshot, - self.cloud.get_volume_snapshot( - snapshot['display_name'])) - self.assertEqual(snapshot, - self.cloud.get_volume_snapshot_by_id(snapshot['id'])) + volume_ids = [v['id'] for v in self.cloud.list_volumes()] + self.assertIn(volume['id'], volume_ids) + + snapshot_ids = [s['id'] for s in self.cloud.list_volume_snapshots()] + self.assertIn(snapshot['id'], snapshot_ids) + + ret_snapshot = self.cloud.get_volume_snapshot_by_id(snapshot['id']) + self.assertEqual(snapshot['id'], ret_snapshot['id']) self.cloud.delete_volume_snapshot(snapshot_name, wait=True) self.cloud.delete_volume(volume_name, wait=True) @@ -59,8 +60,8 @@ def test_volumes(self): def cleanup(self, volume_name, snapshot_name): volume = self.cloud.get_volume(volume_name) snapshot = self.cloud.get_volume_snapshot(snapshot_name) - if volume: - self.cloud.delete_volume(volume_name) - + # Need to delete snapshots before volumes if snapshot: self.cloud.delete_volume_snapshot(snapshot_name) + if volume: + self.cloud.delete_volume(volume_name) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 2a080ad71..5abe40638 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -394,6 +394,9 @@ class FakeImage(dict): id = '99' name = '99 name' + def _shadeunittest(self): + pass + fake_image = FakeImage() fake_image.update({ 'id': '99', @@ -407,6 +410,9 @@ class FakeTask(dict): status = 'success' result = {'image_id': '99'} + def _shadeunittest(self): + pass + fake_task = FakeTask() fake_task.update({ 'id': '100', @@ -439,6 +445,10 @@ class FakeImage(dict): id = 1 status = 'active' name = 'None Test Image' + + def _shadeunittest(self): + pass + fi = FakeImage(id=FakeImage.id, status=FakeImage.status, name=FakeImage.name) glance_mock.images.list.return_value = [fi] diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 77688adfa..faf06e462 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -472,6 +472,16 @@ def test_obj_to_dict(self): self.assertTrue(hasattr(cloud_dict, 'name')) self.assertEquals(cloud_dict.name, cloud_dict['name']) + def test_obj_to_dict_subclass(self): + class FakeObjDict(dict): + additional = 1 + obj = FakeObjDict(foo='bar') + obj_dict = meta.obj_to_dict(obj) + self.assertIn('additional', obj_dict) + self.assertIn('foo', obj_dict) + self.assertEquals(obj_dict['additional'], 1) + self.assertEquals(obj_dict['foo'], 'bar') + def test_warlock_to_dict(self): schema = { 'name': 'Test', From d72262e207ec172a2cc04247101959297410f5c3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 4 Mar 2016 08:23:50 -0600 Subject: [PATCH 0823/3836] Use warlock in the glance v2 tests We were mocking what should have been a warlock object with a dict like object. Instead of doing that, actually pull the model from glanceclient and construct a legit warlock object in the mock so that we can make sure our warlock morphing does the right thing. Also, warlock triggers 'smarts' about which parameters to update, so update the test to mock out the right things. Sadly we have to copy the task schema in, because the only place it exists in API form is in the glance source tree. Change-Id: I9a63bfb7a85e69e66d32cf28d0e7fe207996e1b4 --- shade/tests/unit/test_caching.py | 119 ++++++++++++++++++++++--------- 1 file changed, 87 insertions(+), 32 deletions(-) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 5abe40638..9ec3a7e8b 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -13,9 +13,11 @@ # under the License. import tempfile +from glanceclient.v2 import shell import mock import os_client_config as occ import testtools +import warlock import yaml import shade.openstackcloud @@ -26,6 +28,74 @@ from shade.tests.unit import base +# Mock out the gettext function so that the task schema can be copypasta +def _(msg): + return msg + + +_TASK_PROPERTIES = { + "id": { + "description": _("An identifier for the task"), + "pattern": _('^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}' + '-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$'), + "type": "string" + }, + "type": { + "description": _("The type of task represented by this content"), + "enum": [ + "import", + ], + "type": "string" + }, + "status": { + "description": _("The current status of this task"), + "enum": [ + "pending", + "processing", + "success", + "failure" + ], + "type": "string" + }, + "input": { + "description": _("The parameters required by task, JSON blob"), + "type": ["null", "object"], + }, + "result": { + "description": _("The result of current task, JSON blob"), + "type": ["null", "object"], + }, + "owner": { + "description": _("An identifier for the owner of this task"), + "type": "string" + }, + "message": { + "description": _("Human-readable informative message only included" + " when appropriate (usually on failure)"), + "type": "string", + }, + "expires_at": { + "description": _("Datetime when this resource would be" + " subject to removal"), + "type": ["null", "string"] + }, + "created_at": { + "description": _("Datetime when this resource was created"), + "type": "string" + }, + "updated_at": { + "description": _("Datetime when this resource was updated"), + "type": "string" + }, + 'self': {'type': 'string'}, + 'schema': {'type': 'string'} +} +_TASK_SCHEMA = dict( + name='Task', properties=_TASK_PROPERTIES, + additionalProperties=False, +) + + class TestMemoryCache(base.TestCase): CACHING_CONFIG = { @@ -388,38 +458,24 @@ class Container(object): fake_sha256 = "fake-sha256" get_file_hashes.return_value = (fake_md5, fake_sha256) - # V2's warlock objects just work like dicts - class FakeImage(dict): - status = 'CREATED' - id = '99' - name = '99 name' - - def _shadeunittest(self): - pass - - fake_image = FakeImage() - fake_image.update({ - 'id': '99', - 'name': '99 name', - shade.openstackcloud.IMAGE_MD5_KEY: fake_md5, - shade.openstackcloud.IMAGE_SHA256_KEY: fake_sha256, - }) + FakeImage = warlock.model_factory(shell.get_image_schema()) + fake_image = FakeImage( + id='a35e8afc-cae9-4e38-8441-2cd465f79f7b', name='name-99', + status='active', visibility='private') glance_mock.images.list.return_value = [fake_image] - class FakeTask(dict): - status = 'success' - result = {'image_id': '99'} - - def _shadeunittest(self): - pass - - fake_task = FakeTask() - fake_task.update({ - 'id': '100', + FakeTask = warlock.model_factory(_TASK_SCHEMA) + args = { + 'id': '21FBD9A7-85EC-4E07-9D58-72F1ACF7CB1F', 'status': 'success', - }) + 'type': 'import', + 'result': { + 'image_id': 'a35e8afc-cae9-4e38-8441-2cd465f79f7b', + }, + } + fake_task = FakeTask(**args) glance_mock.tasks.get.return_value = fake_task - self._call_create_image(name='99 name', + self._call_create_image(name='name-99', container='image_upload_v2_test_container') args = {'header': ['x-object-meta-x-shade-md5:fake-md5', 'x-object-meta-x-shade-sha256:fake-sha256'], @@ -429,12 +485,11 @@ def _shadeunittest(self): objects=mock.ANY, options=args) glance_mock.tasks.create.assert_called_with(type='import', input={ - 'import_from': 'image_upload_v2_test_container/99 name', - 'image_properties': {'name': '99 name'}}) + 'import_from': 'image_upload_v2_test_container/name-99', + 'image_properties': {'name': 'name-99'}}) args = {'owner_specified.shade.md5': fake_md5, 'owner_specified.shade.sha256': fake_sha256, - 'image_id': '99', - 'visibility': 'private'} + 'image_id': 'a35e8afc-cae9-4e38-8441-2cd465f79f7b'} glance_mock.images.update.assert_called_with(**args) fake_image_dict = meta.obj_to_dict(fake_image) self.assertEqual([fake_image_dict], self.cloud.list_images()) From cf43b98e335e5bbb409e6209dada5e3c86bb8ceb Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 4 Mar 2016 08:31:41 -0600 Subject: [PATCH 0824/3836] Mock glance v1 image with object not dict glance v1 objects are not dict like. They are just regular objects. Making them dictlike triggers an unreal path. This also allows us to remove the _shadeunittest logic line in obj_to_dict. Change-Id: Iae2d926a7d8b899ef842b8cb1e898a38ed17adf7 --- shade/meta.py | 3 --- shade/tests/unit/test_caching.py | 24 +++++++++++++----------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/shade/meta.py b/shade/meta.py index aac20f113..11c77fdba 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -338,9 +338,6 @@ def obj_to_dict(obj): # If we obj_to_dict twice, don't fail, just return the munch # Also, don't try to modify Mock objects - that way lies madness return obj - elif hasattr(obj, '_shadeunittest'): - # Hook for unittesting - instance = munch.Munch() elif hasattr(obj, 'schema') and hasattr(obj, 'validate'): # It's a warlock return warlock_to_dict(obj) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 9ec3a7e8b..85585f456 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -496,28 +496,30 @@ class Container(object): @mock.patch.object(shade.OpenStackCloud, 'glance_client') def test_cache_no_cloud_name(self, glance_mock): - class FakeImage(dict): - id = 1 + class FakeImage(object): status = 'active' name = 'None Test Image' - def _shadeunittest(self): - pass + def __init__(self, id): + self.id = id - fi = FakeImage(id=FakeImage.id, status=FakeImage.status, - name=FakeImage.name) + fi = FakeImage(id=1) glance_mock.images.list.return_value = [fi] self.cloud.name = None - self.assertEqual([fi], [dict(x) for x in self.cloud.list_images()]) + self.assertEqual( + meta.obj_list_to_dict([fi]), + self.cloud.list_images()) # Now test that the list was cached - fi2 = FakeImage(id=2, status=FakeImage.status, name=FakeImage.name) - fi2.id = 2 + fi2 = FakeImage(id=2) glance_mock.images.list.return_value = [fi, fi2] - self.assertEqual([fi], [dict(x) for x in self.cloud.list_images()]) + self.assertEqual( + meta.obj_list_to_dict([fi]), + self.cloud.list_images()) # Invalidation too self.cloud.list_images.invalidate(self.cloud) self.assertEqual( - [fi, fi2], [dict(x) for x in self.cloud.list_images()]) + meta.obj_list_to_dict([fi, fi2]), + self.cloud.list_images()) class TestBogusAuth(base.TestCase): From ff82154b90a3235e9063e2513df37dca4ed2849b Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 7 Mar 2016 14:48:20 -0500 Subject: [PATCH 0825/3836] Bug fix: Do not fail on routers with no ext gw Although I've not been able to reproduce it, some user have reported an exception from shade in the list_router_interfaces() call when trying to access the external_gateway_info of a router that does not have this key set. Let's just be safe and and a check to make sure that the key exists. Change-Id: I949b76b2b306e5161e7ee77d6c588a77ac4c7d87 --- .../notes/router_ext_gw-b86582317bca8b39.yaml | 4 ++++ shade/openstackcloud.py | 9 +++++--- shade/tests/unit/test_shade.py | 22 +++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/router_ext_gw-b86582317bca8b39.yaml diff --git a/releasenotes/notes/router_ext_gw-b86582317bca8b39.yaml b/releasenotes/notes/router_ext_gw-b86582317bca8b39.yaml new file mode 100644 index 000000000..84d9a1ac0 --- /dev/null +++ b/releasenotes/notes/router_ext_gw-b86582317bca8b39.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - No longer fail in list_router_interfaces() if a router does + not have the external_gateway_info key. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index eabc9f35f..6deb6b388 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1854,9 +1854,12 @@ def list_router_interfaces(self, router, interface_type=None): if interface_type: filtered_ports = [] - ext_fixed = (router['external_gateway_info']['external_fixed_ips'] - if router['external_gateway_info'] - else []) + if ('external_gateway_info' in router and + 'external_fixed_ips' in router['external_gateway_info']): + ext_fixed = \ + router['external_gateway_info']['external_fixed_ips'] + else: + ext_fixed = [] # Compare the subnets (subnet_id, ip_address) on the ports with # the subnets making up the router external gateway. Those ports diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index eeff62e27..f66b82976 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -250,6 +250,28 @@ def test_delete_router_multiple_using_id(self, mock_client): self.cloud.delete_router('123') self.assertTrue(mock_client.delete_router.called) + @mock.patch.object(shade.OpenStackCloud, 'search_ports') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_list_router_interfaces_no_gw(self, mock_client, mock_search): + """ + If a router does not have external_gateway_info, do not fail. + """ + external_port = {'id': 'external_port_id', + 'fixed_ips': [ + ('external_subnet_id', 'ip_address'), + ]} + port_list = [external_port] + router = { + 'id': 'router_id', + } + mock_search.return_value = port_list + ret = self.cloud.list_router_interfaces(router, + interface_type='external') + mock_search.assert_called_once_with( + filters={'device_id': router['id']} + ) + self.assertEqual([], ret) + @mock.patch.object(shade.OpenStackCloud, 'search_ports') @mock.patch.object(shade.OpenStackCloud, 'neutron_client') def test_list_router_interfaces_all(self, mock_client, mock_search): From 543954dd056527a4ff5164d0aec3e791c89b0550 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 7 Mar 2016 23:03:12 -0500 Subject: [PATCH 0826/3836] In the service lock, reset the service, not the lock It turns out that when you're in a context manager that has gotten the lock, attempting to set that lock to None will fail. It's also not the thing we actually want to do. What we WANT to do is reset the _service_ so that we'll ensure a new service object gets created. Change-Id: Ic39ef0418dcd5c0cbbe710ac1c31e58b744f56b5 --- releasenotes/notes/swift-upload-lock-d18f3d42b3a0719a.yaml | 5 +++++ shade/openstackcloud.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/swift-upload-lock-d18f3d42b3a0719a.yaml diff --git a/releasenotes/notes/swift-upload-lock-d18f3d42b3a0719a.yaml b/releasenotes/notes/swift-upload-lock-d18f3d42b3a0719a.yaml new file mode 100644 index 000000000..27848a5d2 --- /dev/null +++ b/releasenotes/notes/swift-upload-lock-d18f3d42b3a0719a.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - Fixed an issue where a section of code that was supposed to be resetting + the SwiftService object was instead resetting the protective mutex around + the SwiftService object leading to an exception of "__exit__" diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index eabc9f35f..ac4fc73d2 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2209,7 +2209,7 @@ def _upload_image_task( with self._swift_client_lock: self._swift_client = None with self._swift_service_lock: - self._swift_service_lock = None + self._swift_service = None self.create_object( container, name, filename, From d0709ead25dc5d5e19445f451e2380db004eb37f Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Thu, 3 Mar 2016 22:45:52 +0000 Subject: [PATCH 0827/3836] Allow passing project_id to create_network The neutron net-create verb allows passing a tenant_id param, allowing a cloud admin to create networks on any projects is granted to. This change allows passing this param, which is an admin-only option. Change-Id: I76285ad1f7106f9a5900f118cdc7a5012786869d --- shade/openstackcloud.py | 8 ++++++-- shade/tests/unit/test_network.py | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index e7c1a23b9..af7a0c0eb 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1675,7 +1675,7 @@ def delete_keypair(self, name): return True def create_network(self, name, shared=False, admin_state_up=True, - external=False, provider=None): + external=False, provider=None, project_id=None): """Create a network. :param string name: Name of the network being created. @@ -1685,17 +1685,21 @@ def create_network(self, name, shared=False, admin_state_up=True, :param dict provider: A dict of network provider options. Example:: { 'network_type': 'vlan', 'segmentation_id': 'vlan1' } + :param string project_id: Specify the project ID this network + will be created on (admin-only). :returns: The network object. :raises: OpenStackCloudException on operation error. """ - network = { 'name': name, 'shared': shared, 'admin_state_up': admin_state_up, } + if project_id is not None: + network['tenant_id'] = project_id + if provider: if not isinstance(provider, dict): raise OpenStackCloudException( diff --git a/shade/tests/unit/test_network.py b/shade/tests/unit/test_network.py index e62c6e920..4f20396b6 100644 --- a/shade/tests/unit/test_network.py +++ b/shade/tests/unit/test_network.py @@ -36,6 +36,20 @@ def test_create_network(self, mock_neutron): ) ) + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_network_specific_tenant(self, mock_neutron): + self.cloud.create_network("netname", project_id="project_id_value") + mock_neutron.create_network.assert_called_with( + body=dict( + network=dict( + name='netname', + shared=False, + admin_state_up=True, + tenant_id="project_id_value", + ) + ) + ) + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') def test_create_network_external(self, mock_neutron): self.cloud.create_network("netname", external=True) From 8fda803d170623a0ce91f2f7c3aea2490351a821 Mon Sep 17 00:00:00 2001 From: Stefan Andres Date: Mon, 15 Feb 2016 11:56:17 +0100 Subject: [PATCH 0828/3836] Use network in neutron_available_floating_ips The old behavior takes a list of network names or id, ignores it and picks the first external network it can find This fix implements the actual logic of using the parameter: * Uses the given external network, if supplied. This changes to using only a single network instead of a list because no where is a list supplied to the call. * If no network is given, it takes the first external network. (Shrews) The unit tests are also changed because they were suck. The test for available_floating_ip() is changed to test only it's calls and not the lower level functionality. New tests are added for the _neutron_available_floating_ips() call, which implements the lower level functionality and which is where this change is actually tested. Co-Authored-By: David Shrewsbury Change-Id: I0b27b50b2a6a8e5199bbeed5786b71851bb3ad3e --- shade/openstackcloud.py | 32 +++-- shade/tests/unit/test_floating_ip_neutron.py | 124 ++++++++++++++++--- 2 files changed, 133 insertions(+), 23 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index bc3fec3b8..d0ae09fd1 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2614,7 +2614,7 @@ def _neutron_available_floating_ips( Return a list of available floating IPs or allocate a new one and return it in a list of 1 element. - :param network: Nova pool name or Neutron network name or id. + :param str network: A Neutron network name or id. :param server: (server) Server the Floating IP is for :returns: a list of floating IP addresses. @@ -2628,17 +2628,33 @@ def _neutron_available_floating_ips( project_id = self.keystone_session.get_project_id() with _utils.neutron_exceptions("unable to get available floating IPs"): - networks = self.get_external_networks() - if not networks: - raise OpenStackCloudResourceNotFound( - "unable to find an external network") + if network: + # Use given list to get first matching external network + floating_network_id = None + for ext_net in self.get_external_networks(): + if network in (ext_net['name'], ext_net['id']): + floating_network_id = ext_net['id'] + break + + if floating_network_id is None: + raise OpenStackCloudResourceNotFound( + "unable to find external network {net}".format( + net=network) + ) + else: + # Get first existing external network + networks = self.get_external_networks() + if not networks: + raise OpenStackCloudResourceNotFound( + "unable to find an external network") + floating_network_id = networks[0]['id'] filters = { 'port_id': None, - 'floating_network_id': networks[0]['id'], + 'floating_network_id': floating_network_id, 'tenant_id': project_id - } + floating_ips = self._neutron_list_floating_ips() available_ips = _utils._filter_list( floating_ips, name_or_id=None, filters=filters) @@ -2648,7 +2664,7 @@ def _neutron_available_floating_ips( # No available IP found or we didn't try # allocate a new Floating IP f_ip = self._neutron_create_floating_ip( - network_name_or_id=networks[0]['id'], server=server) + network_name_or_id=floating_network_id, server=server) return [f_ip] diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index b2b8a9add..30abb28ec 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -25,6 +25,7 @@ from neutronclient.common import exceptions as n_exc from shade import _utils +from shade import exc from shade import meta from shade import OpenStackCloud from shade.tests import fakes @@ -214,25 +215,118 @@ def test_create_floating_ip( self.mock_floating_ip_new_rep['floatingip']['floating_ip_address'], ip['floating_ip_address']) - @patch.object(OpenStackCloud, 'keystone_session') - @patch.object(OpenStackCloud, '_neutron_list_floating_ips') - @patch.object(OpenStackCloud, 'search_networks') + @patch.object(_utils, 'normalize_neutron_floating_ips') + @patch.object(OpenStackCloud, '_neutron_available_floating_ips') @patch.object(OpenStackCloud, 'has_service') - def test_available_floating_ip_existing( - self, mock_has_service, mock_search_networks, - mock__neutron_list_floating_ips, mock_keystone_session): + @patch.object(OpenStackCloud, 'keystone_session') + def test_available_floating_ip_neutron(self, + mock_keystone, + mock_has_service, + mock__neutron_call, + mock_normalize): + """ + Test the correct path is taken when using neutron. + """ + # force neutron path mock_has_service.return_value = True - mock_search_networks.return_value = [self.mock_get_network_rep] - mock__neutron_list_floating_ips.return_value = \ - [self.mock_floating_ip_new_rep['floatingip']] - mock_keystone_session.get_project_id.return_value = \ - '4969c491a3c74ee4af974e6d800c62df' + mock__neutron_call.return_value = [] - ip = self.client.available_floating_ip(network='my-network') + self.client.available_floating_ip(network='netname') - self.assertEqual( - self.mock_floating_ip_new_rep['floatingip']['floating_ip_address'], - ip['floating_ip_address']) + mock_has_service.assert_called_once_with('network') + mock__neutron_call.assert_called_once_with(network='netname', + server=None) + mock_normalize.assert_called_once_with([]) + + @patch.object(_utils, '_filter_list') + @patch.object(OpenStackCloud, '_neutron_create_floating_ip') + @patch.object(OpenStackCloud, '_neutron_list_floating_ips') + @patch.object(OpenStackCloud, 'get_external_networks') + @patch.object(OpenStackCloud, 'keystone_session') + def test__neutron_available_floating_ips( + self, + mock_keystone_session, + mock_get_ext_nets, + mock__neutron_list_fips, + mock__neutron_create_fip, + mock__filter_list): + """ + Test without specifying a network name. + """ + mock_keystone_session.get_project_id.return_value = 'proj-id' + mock_get_ext_nets.return_value = [self.mock_get_network_rep] + mock__neutron_list_fips.return_value = [] + mock__filter_list.return_value = [] + + # Test if first network is selected if no network is given + self.client._neutron_available_floating_ips() + + mock_keystone_session.get_project_id.assert_called_once_with() + mock_get_ext_nets.assert_called_once_with() + mock__neutron_list_fips.assert_called_once_with() + mock__filter_list.assert_called_once_with( + [], name_or_id=None, + filters={'port_id': None, + 'floating_network_id': self.mock_get_network_rep['id'], + 'tenant_id': 'proj-id'} + ) + mock__neutron_create_fip.assert_called_once_with( + network_name_or_id=self.mock_get_network_rep['id'], + server=None + ) + + @patch.object(_utils, '_filter_list') + @patch.object(OpenStackCloud, '_neutron_create_floating_ip') + @patch.object(OpenStackCloud, '_neutron_list_floating_ips') + @patch.object(OpenStackCloud, 'get_external_networks') + @patch.object(OpenStackCloud, 'keystone_session') + def test__neutron_available_floating_ips_network( + self, + mock_keystone_session, + mock_get_ext_nets, + mock__neutron_list_fips, + mock__neutron_create_fip, + mock__filter_list): + """ + Test with specifying a network name. + """ + mock_keystone_session.get_project_id.return_value = 'proj-id' + mock_get_ext_nets.return_value = [self.mock_get_network_rep] + mock__neutron_list_fips.return_value = [] + mock__filter_list.return_value = [] + + self.client._neutron_available_floating_ips( + network=self.mock_get_network_rep['name'] + ) + + mock_keystone_session.get_project_id.assert_called_once_with() + mock_get_ext_nets.assert_called_once_with() + mock__neutron_list_fips.assert_called_once_with() + mock__filter_list.assert_called_once_with( + [], name_or_id=None, + filters={'port_id': None, + 'floating_network_id': self.mock_get_network_rep['id'], + 'tenant_id': 'proj-id'} + ) + mock__neutron_create_fip.assert_called_once_with( + network_name_or_id=self.mock_get_network_rep['id'], + server=None + ) + + @patch.object(OpenStackCloud, 'get_external_networks') + @patch.object(OpenStackCloud, 'keystone_session') + def test__neutron_available_floating_ips_invalid_network( + self, + mock_keystone_session, + mock_get_ext_nets): + """ + Test with an invalid network name. + """ + mock_keystone_session.get_project_id.return_value = 'proj-id' + mock_get_ext_nets.return_value = [] + self.assertRaises(exc.OpenStackCloudException, + self.client._neutron_available_floating_ips, + network='INVALID') @patch.object(OpenStackCloud, 'nova_client') @patch.object(OpenStackCloud, 'keystone_session') From f10f704bfa30e32ab8394093b44d1358b1e90af8 Mon Sep 17 00:00:00 2001 From: SamYaple Date: Wed, 2 Mar 2016 22:44:26 +0000 Subject: [PATCH 0829/3836] Add update_service() Services can be updated, we can even update them! Keystone v2.0 did not allow updates, Keystone v3 does Improve unit tests to test v3 items for service functions Change-Id: I36b9f92f7b551451973e3681dd8814ed90614b9c --- .../add_update_service-28e590a7a7524053.yaml | 6 ++ shade/_tasks.py | 5 ++ shade/operatorcloud.py | 53 ++++++++++++++---- shade/tests/functional/test_services.py | 25 +++++++++ shade/tests/unit/test_services.py | 56 ++++++++++++++++++- 5 files changed, 133 insertions(+), 12 deletions(-) create mode 100644 releasenotes/notes/add_update_service-28e590a7a7524053.yaml diff --git a/releasenotes/notes/add_update_service-28e590a7a7524053.yaml b/releasenotes/notes/add_update_service-28e590a7a7524053.yaml new file mode 100644 index 000000000..ff3e7befa --- /dev/null +++ b/releasenotes/notes/add_update_service-28e590a7a7524053.yaml @@ -0,0 +1,6 @@ +--- +features: + - Add the ability to update a keystone service information. This feature is + not available on keystone v2.0. The new function, update_service(), allows + the user to update description, name of service, service type, and enabled + status. diff --git a/shade/_tasks.py b/shade/_tasks.py index c50b3fb2b..014cc5c69 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -607,6 +607,11 @@ def main(self, client): return client.keystone_client.services.list() +class ServiceUpdate(task_manager.Task): + def main(self, client): + return client.keystone_client.services.update(**self.args) + + class ServiceDelete(task_manager.Task): def main(self, client): return client.keystone_client.services.delete(**self.args) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 53114a144..31f5d87af 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -747,13 +747,14 @@ def purge_node_instance_info(self, uuid): _tasks.MachineNodeUpdate(node_id=uuid, patch=patch)) @_utils.valid_kwargs('type', 'service_type', 'description') - def create_service(self, name, **kwargs): + def create_service(self, name, enabled=True, **kwargs): """Create a service. :param name: Service name. :param type: Service type. (type or service_type required.) :param service_type: Service type. (type or service_type required.) :param description: Service description (optional). + :param enabled: Whether the service is enabled (v3 only) :returns: a dict containing the services description, i.e. the following attributes:: @@ -767,17 +768,47 @@ def create_service(self, name, **kwargs): openstack API call. """ - service_type = kwargs.get('type', kwargs.get('service_type')) - description = kwargs.get('description', None) - with _utils.shade_exceptions("Failed to create service {name}".format( - name=name)): - if self.cloud_config.get_api_version('identity').startswith('2'): - service_kwargs = {'service_type': service_type} - else: - service_kwargs = {'type': service_type} + type_ = kwargs.pop('type', None) + service_type = kwargs.pop('service_type', None) + + if self.cloud_config.get_api_version('identity').startswith('2'): + kwargs['service_type'] = type_ or service_type + else: + kwargs['type'] = type_ or service_type + kwargs['enabled'] = enabled + + with _utils.shade_exceptions( + "Failed to create service {name}".format(name=name) + ): + service = self.manager.submitTask( + _tasks.ServiceCreate(name=name, **kwargs) + ) - service = self.manager.submitTask(_tasks.ServiceCreate( - name=name, description=description, **service_kwargs)) + return _utils.normalize_keystone_services([service])[0] + + @_utils.valid_kwargs('name', 'enabled', 'type', 'service_type', + 'description') + def update_service(self, name_or_id, **kwargs): + # NOTE(SamYaple): Service updates are only available on v3 api + if self.cloud_config.get_api_version('identity').startswith('2'): + raise OpenStackCloudUnavailableFeature( + 'Unavailable Feature: Service update requires Identity v3' + ) + + # NOTE(SamYaple): Keystone v3 only accepts 'type' but shade accepts + # both 'type' and 'service_type' with a preference + # towards 'type' + type_ = kwargs.pop('type', None) + service_type = kwargs.pop('service_type', None) + if type_ or service_type: + kwargs['type'] = type_ or service_type + + with _utils.shade_exceptions( + "Error in updating service {service}".format(service=name_or_id) + ): + service = self.manager.submitTask( + _tasks.ServiceUpdate(service=name_or_id, **kwargs) + ) return _utils.normalize_keystone_services([service])[0] diff --git a/shade/tests/functional/test_services.py b/shade/tests/functional/test_services.py index 48faf10d3..87d050293 100644 --- a/shade/tests/functional/test_services.py +++ b/shade/tests/functional/test_services.py @@ -26,6 +26,7 @@ from shade import operator_cloud from shade.exc import OpenStackCloudException +from shade.exc import OpenStackCloudUnavailableFeature from shade.tests import base @@ -65,6 +66,30 @@ def test_create_service(self): description='this is a test description') self.assertIsNotNone(service.get('id')) + def test_update_service(self): + if self.operator_cloud.cloud_config.get_api_version( + 'identity').startswith('2'): + # NOTE(SamYaple): Update service only works with v3 api + self.assertRaises(OpenStackCloudUnavailableFeature, + self.operator_cloud.update_service, + 'service_id', name='new name') + else: + service = self.operator_cloud.create_service( + name=self.new_service_name + '_create', type='test_type', + description='this is a test description', enabled=True) + new_service = self.operator_cloud.update_service( + service.id, + name=self.new_service_name + '_update', + description='this is an updated description', + enabled=False + ) + self.assertEqual(new_service.name, + self.new_service_name + '_update') + self.assertEqual(new_service.description, + 'this is an updated description') + self.assertFalse(new_service.enabled) + self.assertEqual(service.id, new_service.id) + def test_list_services(self): service = self.operator_cloud.create_service( name=self.new_service_name + '_list', type='test_type') diff --git a/shade/tests/unit/test_services.py b/shade/tests/unit/test_services.py index f5fd9d965..0c4dd0087 100644 --- a/shade/tests/unit/test_services.py +++ b/shade/tests/unit/test_services.py @@ -22,7 +22,9 @@ from mock import patch import os_client_config from shade import _utils +from shade import meta from shade import OpenStackCloudException +from shade.exc import OpenStackCloudUnavailableFeature from shade import OperatorCloud from shade.tests.fakes import FakeService from shade.tests.unit import base @@ -50,7 +52,10 @@ def setUp(self): @patch.object(_utils, 'normalize_keystone_services') @patch.object(OperatorCloud, 'keystone_client') - def test_create_service(self, mock_keystone_client, mock_norm): + @patch.object(os_client_config.cloud_config.CloudConfig, 'get_api_version') + def test_create_service_v2(self, mock_api_version, mock_keystone_client, + mock_norm): + mock_api_version.return_value = '2.0' kwargs = { 'name': 'a service', 'type': 'network', @@ -62,6 +67,55 @@ def test_create_service(self, mock_keystone_client, mock_norm): mock_keystone_client.services.create.assert_called_with(**kwargs) self.assertTrue(mock_norm.called) + @patch.object(_utils, 'normalize_keystone_services') + @patch.object(OperatorCloud, 'keystone_client') + @patch.object(os_client_config.cloud_config.CloudConfig, 'get_api_version') + def test_create_service_v3(self, mock_api_version, mock_keystone_client, + mock_norm): + mock_api_version.return_value = '3' + kwargs = { + 'name': 'a v3 service', + 'type': 'cinderv2', + 'description': 'This is a test service', + 'enabled': False + } + + self.client.create_service(**kwargs) + mock_keystone_client.services.create.assert_called_with(**kwargs) + self.assertTrue(mock_norm.called) + + @patch.object(os_client_config.cloud_config.CloudConfig, 'get_api_version') + def test_update_service_v2(self, mock_api_version): + mock_api_version.return_value = '2.0' + # NOTE(SamYaple): Update service only works with v3 api + self.assertRaises(OpenStackCloudUnavailableFeature, + self.client.update_service, + 'service_id', name='new name') + + @patch.object(_utils, 'normalize_keystone_services') + @patch.object(OperatorCloud, 'keystone_client') + @patch.object(os_client_config.cloud_config.CloudConfig, 'get_api_version') + def test_update_service_v3(self, mock_api_version, mock_keystone_client, + mock_norm): + mock_api_version.return_value = '3' + kwargs = { + 'name': 'updated_name', + 'type': 'updated_type', + 'service_type': 'updated_type', + 'description': 'updated_name', + 'enabled': False + } + + service_obj = FakeService(id='id1', **kwargs) + mock_keystone_client.services.update.return_value = service_obj + + self.client.update_service('id1', **kwargs) + del kwargs['service_type'] + mock_keystone_client.services.update.assert_called_once_with( + service='id1', **kwargs + ) + mock_norm.assert_called_once_with([meta.obj_to_dict(service_obj)]) + @patch.object(OperatorCloud, 'keystone_client') def test_list_services(self, mock_keystone_client): mock_keystone_client.services.list.return_value = \ From 82697db9e6cb71a776b6ecfc898346901e323d4b Mon Sep 17 00:00:00 2001 From: SamYaple Date: Sun, 6 Mar 2016 19:32:07 +0000 Subject: [PATCH 0830/3836] Test v3 params on v2.0 endpoint; Add v3 unit Part of the code was not being excericed resulting an invalid line of code slipping through. "'%url' % interface" evaluates %u thinking it is about to put in a number. This is not the case. Use .format() instead. Add unit test to check that section of code. Additionally, add tests to validate v3 endpoint code Change-Id: I116fff73ce102232c2a14acc7ee55bd660dd8be7 --- shade/operatorcloud.py | 2 +- shade/tests/fakes.py | 9 +++ shade/tests/unit/test_endpoints.py | 105 +++++++++++++++++++++++++++-- 3 files changed, 110 insertions(+), 6 deletions(-) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 53114a144..2205d2329 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -898,7 +898,7 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, " internal_url, admin_url parameters instead of" " url and interface".format( service=service_name_or_id)) - urlkwargs['%url' % interface] = url + urlkwargs['{}url'.format(interface)] = url urlkwargs['service_id'] = service['id'] else: urlkwargs['url'] = url diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index f2bf77ba3..8b5326ea9 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -31,6 +31,15 @@ def __init__(self, id, service_id, region, publicurl, internalurl=None, self.adminurl = adminurl +class FakeEndpointv3(object): + def __init__(self, id, service_id, region, url, interface=None): + self.id = id + self.service_id = service_id + self.region = region + self.url = url + self.interface = interface + + class FakeFlavor(object): def __init__(self, id, name): self.id = id diff --git a/shade/tests/unit/test_endpoints.py b/shade/tests/unit/test_endpoints.py index 060dabcce..a0c596903 100644 --- a/shade/tests/unit/test_endpoints.py +++ b/shade/tests/unit/test_endpoints.py @@ -22,11 +22,12 @@ from mock import patch import os_client_config from shade import OperatorCloud +from shade.exc import OpenStackCloudException from shade.tests.fakes import FakeEndpoint +from shade.tests.fakes import FakeEndpointv3 from shade.tests.unit import base -# ToDo: support v3 api (dguerri) class TestCloudEndpoints(base.TestCase): mock_endpoints = [ {'id': 'id1', 'service_id': 'sid1', 'region': 'region1', @@ -36,18 +37,31 @@ class TestCloudEndpoints(base.TestCase): {'id': 'id3', 'service_id': 'sid3', 'region': 'region2', 'publicurl': 'purl3', 'internalurl': 'iurl3', 'adminurl': 'aurl3'} ] + mock_endpoints_v3 = [ + {'id': 'id1_v3', 'service_id': 'sid1', 'region': 'region1', + 'url': 'url1', 'interface': 'public'}, + {'id': 'id2_v3', 'service_id': 'sid1', 'region': 'region1', + 'url': 'url2', 'interface': 'admin'}, + {'id': 'id3_v3', 'service_id': 'sid1', 'region': 'region1', + 'url': 'url3', 'interface': 'internal'} + ] def setUp(self): super(TestCloudEndpoints, self).setUp() config = os_client_config.OpenStackConfig() - self.client = OperatorCloud( - cloud_config=config.get_one_cloud(validate=False)) + self.client = OperatorCloud(cloud_config=config.get_one_cloud( + validate=False)) self.mock_ks_endpoints = \ [FakeEndpoint(**kwa) for kwa in self.mock_endpoints] + self.mock_ks_endpoints_v3 = \ + [FakeEndpointv3(**kwa) for kwa in self.mock_endpoints_v3] @patch.object(OperatorCloud, 'list_services') @patch.object(OperatorCloud, 'keystone_client') - def test_create_endpoint(self, mock_keystone_client, mock_list_services): + @patch.object(os_client_config.cloud_config.CloudConfig, 'get_api_version') + def test_create_endpoint_v2(self, mock_api_version, mock_keystone_client, + mock_list_services): + mock_api_version.return_value = '2.0' mock_list_services.return_value = [ { 'id': 'service_id1', @@ -57,24 +71,105 @@ def test_create_endpoint(self, mock_keystone_client, mock_list_services): } ] mock_keystone_client.endpoints.create.return_value = \ - self.mock_ks_endpoints[0] + self.mock_ks_endpoints[2] endpoints = self.client.create_endpoint( service_name_or_id='service1', region='mock_region', public_url='mock_public_url', + internal_url='mock_internal_url', + admin_url='mock_admin_url' ) mock_keystone_client.endpoints.create.assert_called_with( service_id='service_id1', region='mock_region', publicurl='mock_public_url', + internalurl='mock_internal_url', + adminurl='mock_admin_url', + ) + + # test keys and values are correct + for k, v in self.mock_endpoints[2].items(): + self.assertEquals(v, endpoints[0].get(k)) + + # test v3 semantics on v2.0 endpoint + mock_keystone_client.endpoints.create.return_value = \ + self.mock_ks_endpoints[0] + + self.assertRaises(OpenStackCloudException, + self.client.create_endpoint, + service_name_or_id='service1', + interface='mock_admin_url', + url='admin') + + endpoints_3on2 = self.client.create_endpoint( + service_name_or_id='service1', + region='mock_region', + interface='public', + url='mock_public_url' ) # test keys and values are correct for k, v in self.mock_endpoints[0].items(): + self.assertEquals(v, endpoints_3on2[0].get(k)) + + @patch.object(OperatorCloud, 'list_services') + @patch.object(OperatorCloud, 'keystone_client') + @patch.object(os_client_config.cloud_config.CloudConfig, 'get_api_version') + def test_create_endpoint_v3(self, mock_api_version, mock_keystone_client, + mock_list_services): + mock_api_version.return_value = '3' + mock_list_services.return_value = [ + { + 'id': 'service_id1', + 'name': 'service1', + 'type': 'type1', + 'description': 'desc1' + } + ] + mock_keystone_client.endpoints.create.return_value = \ + self.mock_ks_endpoints_v3[0] + + endpoints = self.client.create_endpoint( + service_name_or_id='service1', + region='mock_region', + url='mock_url', + interface='mock_interface', + enabled=False + ) + mock_keystone_client.endpoints.create.assert_called_with( + service='service_id1', + region='mock_region', + url='mock_url', + interface='mock_interface', + enabled=False + ) + + # test keys and values are correct + for k, v in self.mock_endpoints_v3[0].items(): self.assertEquals(v, endpoints[0].get(k)) + # test v2.0 semantics on v3 endpoint + mock_keystone_client.endpoints.create.side_effect = \ + self.mock_ks_endpoints_v3 + + endpoints_2on3 = self.client.create_endpoint( + service_name_or_id='service1', + region='mock_region', + public_url='mock_public_url', + internal_url='mock_internal_url', + admin_url='mock_admin_url', + ) + + # Three endpoints should be returned, public, internal, and admin + self.assertEquals(len(endpoints_2on3), 3) + + # test keys and values are correct + for count in range(len(endpoints_2on3)): + for k, v in self.mock_endpoints_v3[count].items(): + self.assertEquals(v, endpoints_2on3[count].get(k)) + @patch.object(OperatorCloud, 'keystone_client') def test_list_endpoints(self, mock_keystone_client): mock_keystone_client.endpoints.list.return_value = \ From cbab18b0fa87cb9ddbcd8e37aa4bfffd9b582b34 Mon Sep 17 00:00:00 2001 From: Clark Boylan Date: Wed, 10 Feb 2016 16:05:15 -0800 Subject: [PATCH 0831/3836] Add osic vendor profile The new osic cloud is a thing. Add a vendor profile here to simplify using it. Change-Id: Iecd473c93cd1e1d8e2bf9a785f257a47df10351e --- doc/source/vendor-support.rst | 13 +++++++++++++ os_client_config/vendors/osic.json | 11 +++++++++++ 2 files changed, 24 insertions(+) create mode 100644 os_client_config/vendors/osic.json diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index e007b70f8..7b1cceb47 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -188,6 +188,19 @@ nyj01 New York, NY * Image API Version is 1 * Floating IPs are not supported +osic +---- + +https://cloud1.osic.org:5000 + +============== ================= +Region Name Human Name +============== ================= +RegionOne RegionOne +============== ================= + +* Public IPv4 is provided via NAT with Neutron Floating IP + ovh --- diff --git a/os_client_config/vendors/osic.json b/os_client_config/vendors/osic.json new file mode 100644 index 000000000..484d7111b --- /dev/null +++ b/os_client_config/vendors/osic.json @@ -0,0 +1,11 @@ +{ + "name": "osic", + "profile": { + "auth": { + "auth_url": "https://cloud1.osic.org:5000" + }, + "regions": [ + "RegionOne" + ] + } +} From a71511468ee7395040e8a0246b9cbba9b6de069f Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Thu, 10 Mar 2016 16:17:08 -0500 Subject: [PATCH 0832/3836] Update reno for stable/mitaka Fix a few page titles at the same time Change-Id: I68d082f1cad51bbe58deed6a7e4b0de122c22fc7 --- releasenotes/source/index.rst | 7 ++++--- releasenotes/source/mitaka.rst | 6 ++++++ 2 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 releasenotes/source/mitaka.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 386434ef8..2f4234a88 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -1,5 +1,6 @@ -Welcome to Nova Release Notes documentation! -============================================== +================================ + os-client-config Release Notes +================================ Contents ======== @@ -8,7 +9,7 @@ Contents :maxdepth: 2 unreleased - + mitaka Indices and tables ================== diff --git a/releasenotes/source/mitaka.rst b/releasenotes/source/mitaka.rst new file mode 100644 index 000000000..e54560965 --- /dev/null +++ b/releasenotes/source/mitaka.rst @@ -0,0 +1,6 @@ +=================================== + Mitaka Series Release Notes +=================================== + +.. release-notes:: + :branch: origin/stable/mitaka From b7b6de094b302f536f26dfe6ad4e92d62dc91066 Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Fri, 11 Mar 2016 23:09:47 +0000 Subject: [PATCH 0833/3836] Make delete_project to call get_project delete_project called update_project, which in turn calls get_project, to gather the project object. This round-trip is unnecessary, delete_project can and should just call get_project. Change-Id: I6d6052b924411d52ce0f3ac0cfcd4d5d62563ae3 --- shade/openstackcloud.py | 2 +- shade/tests/unit/test_project.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 769a2c028..1abc56737 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -487,7 +487,7 @@ def delete_project(self, name_or_id): with _utils.shade_exceptions( "Error in deleting project {project}".format( project=name_or_id)): - project = self.update_project(name_or_id, enabled=False) + project = self.get_project(name_or_id) params = {} if self.cloud_config.get_api_version('identity') == '3': params['project'] = project['id'] diff --git a/shade/tests/unit/test_project.py b/shade/tests/unit/test_project.py index 55533607c..47d493734 100644 --- a/shade/tests/unit/test_project.py +++ b/shade/tests/unit/test_project.py @@ -67,25 +67,25 @@ def test_create_project_v3_no_domain(self, mock_keystone, self.cloud.create_project(name='foo', description='bar') @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, 'update_project') + @mock.patch.object(shade.OpenStackCloud, 'get_project') @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_delete_project_v2(self, mock_keystone, mock_update, + def test_delete_project_v2(self, mock_keystone, mock_get, mock_api_version): mock_api_version.return_value = '2' - mock_update.return_value = dict(id='123') + mock_get.return_value = dict(id='123') self.cloud.delete_project('123') - mock_update.assert_called_once_with('123', enabled=False) + mock_get.assert_called_once_with('123') mock_keystone.tenants.delete.assert_called_once_with(tenant='123') @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, 'update_project') + @mock.patch.object(shade.OpenStackCloud, 'get_project') @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_delete_project_v3(self, mock_keystone, mock_update, + def test_delete_project_v3(self, mock_keystone, mock_get, mock_api_version): mock_api_version.return_value = '3' - mock_update.return_value = dict(id='123') + mock_get.return_value = dict(id='123') self.cloud.delete_project('123') - mock_update.assert_called_once_with('123', enabled=False) + mock_get.assert_called_once_with('123') mock_keystone.projects.delete.assert_called_once_with(project='123') @mock.patch.object(shade.OpenStackCloud, 'get_project') From cb60c7eae8f37eed89240966022bc4268e697f31 Mon Sep 17 00:00:00 2001 From: SamYaple Date: Sun, 6 Mar 2016 16:47:15 +0000 Subject: [PATCH 0834/3836] Update create_endpoint() Using kwargs as it is used in create_endpoint() does not work if other kwargs are needed besides the three *_url ones. This means if any new kwargs come along this will have to be refactored anyway. More immediately, this code cannot be used with update_endpoint() in the next patchset leading to an inconsistency in code. This refactor allows both create_endpoint and update_endpoint to be consitent with eachother and, dare I say, makes this.... futureproof? Change-Id: I79273e3dc409cf49b03cc644ea50fe143bcd53aa --- shade/operatorcloud.py | 59 ++++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 2205d2329..60b4b775d 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -875,9 +875,13 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, :raises: OpenStackCloudException if the service cannot be found or if something goes wrong during the openstack API call. """ - if url and kwargs: + public_url = kwargs.pop('public_url', None) + internal_url = kwargs.pop('internal_url', None) + admin_url = kwargs.pop('admin_url', None) + + if (url or interface) and (public_url or internal_url or admin_url): raise OpenStackCloudException( - "create_endpoint takes either url and interace OR" + "create_endpoint takes either url and interface OR" " public_url, internal_url, admin_url") service = self.get_service(name_or_id=service_name_or_id) @@ -899,39 +903,48 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, " url and interface".format( service=service_name_or_id)) urlkwargs['{}url'.format(interface)] = url - urlkwargs['service_id'] = service['id'] else: urlkwargs['url'] = url urlkwargs['interface'] = interface - urlkwargs['enabled'] = enabled - urlkwargs['service'] = service['id'] endpoint_args.append(urlkwargs) else: - if self.cloud_config.get_api_version( - 'identity').startswith('2'): + expected_endpoints = {'public': public_url, + 'internal': internal_url, + 'admin': admin_url} + if self.cloud_config.get_api_version('identity').startswith('2'): urlkwargs = {} - for arg_key, arg_val in kwargs.items(): - urlkwargs[arg_key.replace('_', '')] = arg_val - urlkwargs['service_id'] = service['id'] + for interface, url in expected_endpoints.items(): + if url: + urlkwargs['{}url'.format(interface)] = url endpoint_args.append(urlkwargs) else: - for arg_key, arg_val in kwargs.items(): - urlkwargs = {} - urlkwargs['url'] = arg_val - urlkwargs['interface'] = arg_key.split('_')[0] - urlkwargs['enabled'] = enabled - urlkwargs['service'] = service['id'] - endpoint_args.append(urlkwargs) + for interface, url in expected_endpoints.items(): + if url: + urlkwargs = {} + urlkwargs['url'] = url + urlkwargs['interface'] = interface + endpoint_args.append(urlkwargs) + + if self.cloud_config.get_api_version('identity').startswith('2'): + kwargs['service_id'] = service['id'] + # Keystone v2 requires 'region' arg even if it is None + kwargs['region'] = region + else: + kwargs['service'] = service['id'] + kwargs['enabled'] = enabled + if region is not None: + kwargs['region'] = region with _utils.shade_exceptions( - "Failed to create endpoint for service " - "{service}".format(service=service['name']) + "Failed to create endpoint for service" + " {service}".format(service=service['name']) ): for args in endpoint_args: - endpoint = self.manager.submitTask(_tasks.EndpointCreate( - region=region, - **args - )) + # NOTE(SamYaple): Add shared kwargs to endpoint args + args.update(kwargs) + endpoint = self.manager.submitTask( + _tasks.EndpointCreate(**args) + ) endpoints.append(endpoint) return endpoints From cfd62e4bbd44795cd3fafa623d43afc43b2311ce Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 14 Mar 2016 18:39:57 -0400 Subject: [PATCH 0835/3836] Add wait_for_server API call. We need to be able to wait for a server to reach active status. This was the intended usage of get_active_server(), but it doesn't do any waiting, which is a bummer. Change-Id: I5be0b1cf6f1b910b6d861d81fc1083e4793b9e5f --- .../wait_for_server-8dc8446b7c673d36.yaml | 3 + shade/openstackcloud.py | 75 ++++++----- shade/tests/unit/test_create_server.py | 126 ++++++++++-------- 3 files changed, 119 insertions(+), 85 deletions(-) create mode 100644 releasenotes/notes/wait_for_server-8dc8446b7c673d36.yaml diff --git a/releasenotes/notes/wait_for_server-8dc8446b7c673d36.yaml b/releasenotes/notes/wait_for_server-8dc8446b7c673d36.yaml new file mode 100644 index 000000000..58bc54c5c --- /dev/null +++ b/releasenotes/notes/wait_for_server-8dc8446b7c673d36.yaml @@ -0,0 +1,3 @@ +--- +features: + - New wait_for_server() API call to wait for a server to reach ACTIVE status. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 769a2c028..55dba1d9f 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3474,7 +3474,6 @@ def create_server( with _utils.shade_exceptions("Error in creating instance"): server = self.manager.submitTask(_tasks.ServerCreate( name=name, flavor=flavor, **kwargs)) - server_id = server.id admin_pass = server.get('adminPass') or kwargs.get('admin_pass') if not wait: # This is a direct get task call to skip the list_servers @@ -3489,41 +3488,55 @@ def create_server( if server.status == 'ERROR': raise OpenStackCloudException( "Error in creating the server.") - if wait: - timeout_message = "Timeout waiting for the server to come up." - start_time = time.time() - # There is no point in iterating faster than the list_servers cache - for count in _utils._iterate_timeout( - timeout, - timeout_message, - wait=self._SERVER_AGE): - try: - # Use the get_server call so that the list_servers - # cache can be leveraged - server = self.get_server(server_id) - except Exception: - continue - if not server: - continue - # We have more work to do, but the details of that are - # hidden from the user. So, calculate remaining timeout - # and pass it down into the IP stack. - remaining_timeout = timeout - int(time.time() - start_time) - if remaining_timeout <= 0: - raise OpenStackCloudTimeout(timeout_message) - - server = self.get_active_server( - server=server, reuse=reuse_ips, - auto_ip=auto_ip, ips=ips, ip_pool=ip_pool, - wait=wait, timeout=remaining_timeout) - if server: - server.adminPass = admin_pass - return server + if wait: + server = self.wait_for_server( + server, auto_ip=auto_ip, ips=ips, ip_pool=ip_pool, + reuse=reuse_ips, timeout=timeout + ) server.adminPass = admin_pass return server + def wait_for_server( + self, server, auto_ip=True, ips=None, ip_pool=None, + reuse=True, timeout=180): + """ + Wait for a server to reach ACTIVE status. + """ + server_id = server['id'] + timeout_message = "Timeout waiting for the server to come up." + start_time = time.time() + + # There is no point in iterating faster than the list_servers cache + for count in _utils._iterate_timeout( + timeout, + timeout_message, + wait=self._SERVER_AGE): + try: + # Use the get_server call so that the list_servers + # cache can be leveraged + server = self.get_server(server_id) + except Exception: + continue + if not server: + continue + + # We have more work to do, but the details of that are + # hidden from the user. So, calculate remaining timeout + # and pass it down into the IP stack. + remaining_timeout = timeout - int(time.time() - start_time) + if remaining_timeout <= 0: + raise OpenStackCloudTimeout(timeout_message) + + server = self.get_active_server( + server=server, reuse=reuse, + auto_ip=auto_ip, ips=ips, ip_pool=ip_pool, + wait=True, timeout=remaining_timeout) + + if server is not None and server['status'] == 'ACTIVE': + return server + def get_active_server( self, server, auto_ip=True, ips=None, ip_pool=None, reuse=True, wait=False, timeout=180): diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index 5ff0b1d7e..66e415425 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -20,6 +20,7 @@ """ from mock import patch, Mock +import mock import os_client_config from shade import _utils from shade import meta @@ -164,67 +165,84 @@ def test_create_server_with_admin_pass_no_wait(self): name='server-name', image='image=id', flavor='flavor-id', admin_pass='ooBootheiX0edoh')) - def test_create_server_with_admin_pass_wait(self): + @patch.object(OpenStackCloud, "wait_for_server") + @patch.object(OpenStackCloud, "nova_client") + def test_create_server_with_admin_pass_wait(self, mock_nova, mock_wait): """ Test that a server with an admin_pass passed returns the password """ - with patch("shade.OpenStackCloud"): - build_server = fakes.FakeServer( - '1234', '', 'BUILD', addresses=dict(public='1.1.1.1'), - adminPass='ooBootheiX0edoh') - next_server = fakes.FakeServer( - '1234', '', 'BUILD', addresses=dict(public='1.1.1.1')) - fake_server = fakes.FakeServer( - '1234', '', 'ACTIVE', addresses=dict(public='1.1.1.1')) - ret_fake_server = fakes.FakeServer( - '1234', '', 'ACTIVE', addresses=dict(public='1.1.1.1'), - adminPass='ooBootheiX0edoh') - config = { - "servers.create.return_value": build_server, - "servers.get.return_value": next_server, - "servers.list.side_effect": [ - [next_server], [fake_server]] - } - OpenStackCloud.nova_client = Mock(**config) - with patch.object(OpenStackCloud, "add_ips_to_server", - return_value=fake_server): - self.assertEqual( - _utils.normalize_server( - meta.obj_to_dict(ret_fake_server), - cloud_name=self.client.name, - region_name=self.client.region_name), - _utils.normalize_server( - meta.obj_to_dict( - self.client.create_server( - 'server-name', 'image-id', 'flavor-id', - wait=True, admin_pass='ooBootheiX0edoh')), - cloud_name=self.client.name, - region_name=self.client.region_name) - ) + fake_server = fakes.FakeServer('1234', '', 'BUILD') + fake_server_with_pass = fakes.FakeServer('1234', '', 'BUILD', + adminPass='ooBootheiX0edoh') - def test_create_server_wait(self): + mock_nova.servers.create.return_value = fake_server + mock_nova.servers.get.return_value = fake_server + # The wait returns non-password server + mock_wait.return_value = _utils.normalize_server( + meta.obj_to_dict(fake_server), None, None) + + server = self.client.create_server( + name='server-name', image='image-id', + flavor='flavor-id', admin_pass='ooBootheiX0edoh', wait=True) + + # Assert that we did wait + self.assertTrue(mock_wait.called) + + # Even with the wait, we should still get back a passworded server + self.assertEqual( + server, + _utils.normalize_server(meta.obj_to_dict(fake_server_with_pass), + None, None) + ) + + @patch.object(OpenStackCloud, "get_active_server") + @patch.object(OpenStackCloud, "get_server") + def test_wait_for_server(self, mock_get_server, mock_get_active_server): """ - Test that create_server with a wait returns the server instance when + Test that waiting for a server returns the server instance when its status changes to "ACTIVE". """ - with patch("shade.OpenStackCloud"): - build_server = fakes.FakeServer( - '1234', '', 'ACTIVE', addresses=dict(public='1.1.1.1')) - fake_server = fakes.FakeServer( - '1234', '', 'ACTIVE', addresses=dict(public='1.1.1.1')) - config = { - "servers.create.return_value": build_server, - "servers.get.return_value": build_server, - "servers.list.side_effect": [ - [build_server], [fake_server]] - } - OpenStackCloud.nova_client = Mock(**config) - with patch.object(OpenStackCloud, "add_ips_to_server", - return_value=fake_server): - self.assertEqual( - self.client.create_server( - 'server-name', 'image-id', 'flavor-id', wait=True), - fake_server) + building_server = {'id': 'fake_server_id', 'status': 'BUILDING'} + active_server = {'id': 'fake_server_id', 'status': 'ACTIVE'} + + mock_get_server.side_effect = iter([building_server, active_server]) + mock_get_active_server.side_effect = iter([ + building_server, active_server]) + + server = self.client.wait_for_server(building_server) + + self.assertEqual(2, mock_get_server.call_count) + mock_get_server.assert_has_calls([ + mock.call(building_server['id']), + mock.call(active_server['id']), + ]) + + self.assertEqual(2, mock_get_active_server.call_count) + mock_get_active_server.assert_has_calls([ + mock.call(server=building_server, reuse=True, auto_ip=True, + ips=None, ip_pool=None, wait=True, timeout=mock.ANY), + mock.call(server=active_server, reuse=True, auto_ip=True, + ips=None, ip_pool=None, wait=True, timeout=mock.ANY), + ]) + + self.assertEqual('ACTIVE', server['status']) + + @patch.object(OpenStackCloud, 'wait_for_server') + @patch.object(OpenStackCloud, 'nova_client') + def test_create_server_wait(self, mock_nova, mock_wait): + """ + Test that create_server with a wait actually does the wait. + """ + fake_server = {'id': 'fake_server_id', 'status': 'BUILDING'} + mock_nova.servers.create.return_value = fake_server + + self.client.create_server( + 'server-name', 'image-id', 'flavor-id', wait=True), + + mock_wait.assert_called_once_with( + fake_server, auto_ip=True, ips=None, + ip_pool=None, reuse=True, timeout=180 + ) @patch('time.sleep') def test_create_server_no_addresses(self, mock_sleep): From 1e738dbc91dd29ccac0aaa65364d353a9266c52a Mon Sep 17 00:00:00 2001 From: Mathieu Bultel Date: Thu, 4 Feb 2016 14:16:17 -0500 Subject: [PATCH 0836/3836] Add normalize stack function for heat stack_list With the stack object the _filter_list was not able to do the: e.get('id') e.get('name') Also the stack dict don't have the 'name' key, so need to set name -> stack_name Unit test add for get_stack and list_stacks function Change-Id: Ia1f3c949cc01906166175a9dcd3b844bc6947e28 --- shade/_utils.py | 7 +++++++ shade/openstackcloud.py | 2 +- shade/task_manager.py | 3 ++- shade/tests/fakes.py | 1 + shade/tests/unit/test_stack.py | 10 ++++++++++ shade/tests/unit/test_task_manager.py | 6 ------ 6 files changed, 21 insertions(+), 8 deletions(-) diff --git a/shade/_utils.py b/shade/_utils.py index 93d0bc938..f0833a726 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -461,6 +461,13 @@ def normalize_roles(roles): return meta.obj_list_to_dict(ret) +def normalize_stacks(stacks): + """ Normalize Stack Object """ + for stack in stacks: + stack['name'] = stack['stack_name'] + return stacks + + def valid_kwargs(*valid_args): # This decorator checks if argument passed as **kwargs to a function are # present in valid_args. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 0d3970a45..abe9f7a68 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1138,7 +1138,7 @@ def list_stacks(self): """ with _utils.shade_exceptions("Error fetching stack list"): stacks = self.manager.submitTask(_tasks.StackList()) - return stacks + return _utils.normalize_stacks(stacks) def list_server_security_groups(self, server): """List all security groups associated with the given server. diff --git a/shade/task_manager.py b/shade/task_manager.py index 53ee6d2b1..620347b15 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -81,7 +81,8 @@ def wait(self): # NOTE(Shrews): Since the client API might decide to subclass one # of these result types, we use isinstance() here instead of type(). - if isinstance(self._result, list): + if (isinstance(self._result, list) or + isinstance(self._result, types.GeneratorType)): return meta.obj_list_to_dict(self._result) elif (not isinstance(self._result, bool) and not isinstance(self._result, int) and diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 8b5326ea9..f14314f75 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -224,6 +224,7 @@ def __init__(self, id, hostname): class FakeStack(object): def __init__(self, id, name, description=None, status='CREATE_COMPLETE'): self.id = id + self.name = name self.stack_name = name self.stack_description = description self.stack_status = status diff --git a/shade/tests/unit/test_stack.py b/shade/tests/unit/test_stack.py index 54f5ec243..28820429f 100644 --- a/shade/tests/unit/test_stack.py +++ b/shade/tests/unit/test_stack.py @@ -143,3 +143,13 @@ def test_create_stack_wait(self, mock_heat, mock_get, mock_template): ) self.assertEqual(2, mock_get.call_count) self.assertEqual(stack, ret) + + @mock.patch.object(shade.OpenStackCloud, 'heat_client') + def test_get_stack(self, mock_heat): + stack = fakes.FakeStack('azerty', 'stack',) + mock_heat.stacks.list.return_value = [stack] + res = self.cloud.get_stack('stack') + self.assertIsNotNone(res) + self.assertEqual(stack.stack_name, res['stack_name']) + self.assertEqual(stack.stack_name, res['name']) + self.assertEqual(stack.stack_status, res['stack_status']) diff --git a/shade/tests/unit/test_task_manager.py b/shade/tests/unit/test_task_manager.py index e45db7282..9918eb2c9 100644 --- a/shade/tests/unit/test_task_manager.py +++ b/shade/tests/unit/test_task_manager.py @@ -13,8 +13,6 @@ # limitations under the License. -import types - from shade import task_manager from shade.tests.unit import base @@ -73,10 +71,6 @@ def test_wait_re_raise(self): """ self.assertRaises(TestException, self.manager.submitTask, TestTask()) - def test_dont_munchify_generators(self): - ret = self.manager.submitTask(TestTaskGenerator()) - self.assertIsInstance(ret, types.GeneratorType) - def test_dont_munchify_int(self): ret = self.manager.submitTask(TestTaskInt()) self.assertIsInstance(ret, int) From ee88b231131731a15799930d8d5bd152ce55995b Mon Sep 17 00:00:00 2001 From: Tristan Cacqueray Date: Mon, 8 Feb 2016 15:56:41 -0500 Subject: [PATCH 0837/3836] Add environment_files to stack_create This change adds support for Heat Stack environment files. Change-Id: Ie6145aa5fde71d93f1df802f16ae4969df5de929 --- shade/openstackcloud.py | 6 +++++- shade/tests/unit/test_stack.py | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index abe9f7a68..bf644ffcc 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -794,7 +794,10 @@ def create_stack( template_object=None, files=None, rollback=True, wait=False, timeout=180, + environment_files=None, **parameters): + envfiles, env = template_utils.process_multiple_environments_and_files( + env_paths=environment_files) tpl_files, template = template_utils.get_template_contents( template_file=template_file, template_url=template_url, @@ -805,7 +808,8 @@ def create_stack( disable_rollback=not rollback, parameters=parameters, template=template, - files=tpl_files, + files=dict(list(tpl_files.items()) + list(envfiles.items())), + environment=env, ) with _utils.shade_exceptions("Error creating stack {name}".format( name=name)): diff --git a/shade/tests/unit/test_stack.py b/shade/tests/unit/test_stack.py index 28820429f..f40ab72d0 100644 --- a/shade/tests/unit/test_stack.py +++ b/shade/tests/unit/test_stack.py @@ -120,6 +120,7 @@ def test_create_stack(self, mock_heat, mock_template): mock_heat.stacks.create.assert_called_once_with( stack_name='stack_name', disable_rollback=False, + environment={}, parameters={}, template={}, files={} @@ -137,6 +138,7 @@ def test_create_stack_wait(self, mock_heat, mock_get, mock_template): mock_heat.stacks.create.assert_called_once_with( stack_name='stack_name', disable_rollback=False, + environment={}, parameters={}, template={}, files={} From ebec66853033e0e953055e0a4e8fb906e7092e66 Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Mon, 14 Mar 2016 11:20:57 +1300 Subject: [PATCH 0838/3836] Make get_stack fetch a single full stack There is a convention in shade for get_ calls to do a list_ with no server filtering then do a client-side filter to return a single result. This approach is not appropriate for Heat since the stacks.list call returns significantly less detail than the stacks.get call. A user calling get_stack will almost certainly be wanting the extra detail provided by the proper stacks.get (for example, the stack outputs). This change switches to using stacks.get for the get_stack implementation. It replaces the search_stacks call with a local function that returns a 'list' of one full stack object. Change-Id: I5d326d489f806709252a22360f3dbd8011fdb9c7 --- shade/_tasks.py | 5 +++++ shade/openstackcloud.py | 12 +++++++++++- shade/tests/unit/test_stack.py | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 014cc5c69..9f72b57cc 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -735,3 +735,8 @@ def main(self, client): class StackDelete(task_manager.Task): def main(self, client): return client.heat_client.stacks.delete(self.args['id']) + + +class StackGet(task_manager.Task): + def main(self, client): + return client.heat_client.stacks.get(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index bf644ffcc..8c8582aeb 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1669,8 +1669,18 @@ def get_stack(self, name_or_id, filters=None): :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call or if multiple matches are found. """ + + def search_one_stack(name_or_id=None, filters=None): + # stack names are mandatory and enforced unique in the project + # so a StackGet can always be used for name or ID. + with _utils.shade_exceptions("Error fetching stack"): + stacks = [self.manager.submitTask( + _tasks.StackGet(stack_id=name_or_id))] + nstacks = _utils.normalize_stacks(stacks) + return _utils._filter_list(nstacks, name_or_id, filters) + return _utils._get_entity( - self.search_stacks, name_or_id, filters) + search_one_stack, name_or_id, filters) def create_keypair(self, name, public_key): """Create a new keypair. diff --git a/shade/tests/unit/test_stack.py b/shade/tests/unit/test_stack.py index f40ab72d0..e2e1a013c 100644 --- a/shade/tests/unit/test_stack.py +++ b/shade/tests/unit/test_stack.py @@ -149,7 +149,7 @@ def test_create_stack_wait(self, mock_heat, mock_get, mock_template): @mock.patch.object(shade.OpenStackCloud, 'heat_client') def test_get_stack(self, mock_heat): stack = fakes.FakeStack('azerty', 'stack',) - mock_heat.stacks.list.return_value = [stack] + mock_heat.stacks.get.return_value = stack res = self.cloud.get_stack('stack') self.assertIsNotNone(res) self.assertEqual(stack.stack_name, res['stack_name']) From c7f8a88886ccbfa7b726babce0e8e92405ca9a57 Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Fri, 18 Mar 2016 21:04:31 +0000 Subject: [PATCH 0839/3836] Pass specific cloud to openstack_clouds function The openstack_clouds function gets all the clouds defined in the clouds.yaml. For clouds with multiple regions, it will return an OpenStackCloud object for each one of the regions tied to the cloud. This change allows to pass an specific cloud in case the user wants to pull a single clouds with multiple regions from clouds.yaml. Change-Id: I41512825cac99fae80c61b3a61248442dcd637c7 --- shade/__init__.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 385cd73ac..645f3a596 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -52,17 +52,27 @@ def simple_logging(debug=False, http_debug=False): log = _log.setup_logging('keystoneauth.identity.generic.base') -def openstack_clouds(config=None, debug=False): +def openstack_clouds(config=None, debug=False, cloud=None): if not config: config = os_client_config.OpenStackConfig() try: - return [ - OpenStackCloud( - cloud=f.name, debug=debug, - cloud_config=f, - **f.config) - for f in config.get_all_clouds() - ] + if cloud is None: + return [ + OpenStackCloud( + cloud=f.name, debug=debug, + cloud_config=f, + **f.config) + for f in config.get_all_clouds() + ] + else: + return [ + OpenStackCloud( + cloud=f.name, debug=debug, + cloud_config=f, + **f.config) + for f in config.get_all_clouds() + if f.name == cloud + ] except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin as e: raise OpenStackCloudException( "Invalid cloud configuration: {exc}".format(exc=str(e))) From 4151feb65c5f05771ac1e78d187b7304acd59695 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 19 Mar 2016 06:52:51 -0500 Subject: [PATCH 0840/3836] Log inner_exception in test runs Just like in nodepool, it's useful to get the actual traceback in the output for inner exceptions when you hit an exception in a test case. Change-Id: I1d5723390772011a1ba290596c2b0d13e9431fcc --- shade/exc.py | 4 ++++ shade/tests/unit/base.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/shade/exc.py b/shade/exc.py index ed102dd5d..39e742f61 100644 --- a/shade/exc.py +++ b/shade/exc.py @@ -21,6 +21,8 @@ class OpenStackCloudException(Exception): + log_inner_exceptions = False + def __init__(self, message, extra_data=None): args = [message] if extra_data: @@ -44,6 +46,8 @@ def __str__(self): message = "%s (Inner Exception: %s)" % ( message, str(self.inner_exception[1])) + if self.log_inner_exceptions: + self.log_error() return message diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 2c7e57f43..0461a62ce 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -40,3 +40,7 @@ def _nosleep(seconds): self.sleep_fixture = self.useFixture(fixtures.MonkeyPatch( 'time.sleep', _nosleep)) + # Getting the inner exceptions in the test log is super useful + self.useFixture(fixtures.MonkeyPatch( + 'shade.exc.OpenStackCloudException.log_inner_exceptions', + True)) From d725978313feed9aeba8250946321d0790c57f42 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 19 Mar 2016 08:12:45 -0500 Subject: [PATCH 0841/3836] Add constructor param to turn on inner logging Monkeypatching isn't the best interface for setting a behavior flag. Make it more accessible by presenting it as a constructor option. Change-Id: Icc6b87c205fff551351cad81b9d83c27fc67d2c9 --- shade/openstackcloud.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index abe9f7a68..647c698cd 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -102,6 +102,15 @@ class OpenStackCloud(object): OpenStack API tasks. Unless you're doing rate limiting client side, you almost certainly don't need this. (optional) + :param bool log_inner_exceptions: Send wrapped exceptions to the error log. + Defaults to false, because there are a + number of wrapped exceptions that are + noise for normal usage. It's possible + that for a user that has python logging + configured properly, it's desirable to + have all of the wrapped exceptions be + emitted to the error log. This flag + will enable that behavior. :param CloudConfig cloud_config: Cloud config object from os-client-config In the future, this will be the only way to pass in cloud configuration, but is @@ -111,7 +120,10 @@ class OpenStackCloud(object): def __init__( self, cloud_config=None, - manager=None, **kwargs): + manager=None, log_inner_exceptions=False, **kwargs): + + if log_inner_exceptions: + OpenStackCloudException.log_inner_exceptions = True self.log = _log.setup_logging('shade') if not cloud_config: From e6b60d99c8ab381f2abe0ed66af4510eff3c910f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 19 Mar 2016 08:40:17 -0500 Subject: [PATCH 0842/3836] Refactor unit tests to construct cloud in base There is no need to construct a cloud individually in each unit test. Further more, we can now just use the enable-inner-exception interface introduced a few patches ago. Change-Id: Ia45a47ec243c917ab05b5a2f95c449b9e8d1da68 --- shade/tests/unit/base.py | 41 +++++++++++++++++++-- shade/tests/unit/test_caching.py | 47 ++++++++---------------- shade/tests/unit/test_domain_params.py | 4 -- shade/tests/unit/test_image.py | 3 +- shade/tests/unit/test_keypair.py | 4 -- shade/tests/unit/test_meta.py | 33 +++++++---------- shade/tests/unit/test_network.py | 4 -- shade/tests/unit/test_security_groups.py | 4 -- shade/tests/unit/test_shade.py | 4 -- shade/tests/unit/test_stack.py | 4 -- shade/tests/unit/test_volume.py | 4 -- 11 files changed, 67 insertions(+), 85 deletions(-) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 0461a62ce..d9d6012c4 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -18,7 +18,11 @@ import time import fixtures +import os_client_config as occ +import tempfile +import yaml +import shade.openstackcloud from shade.tests import base @@ -26,6 +30,23 @@ class TestCase(base.TestCase): """Test case base class for all unit tests.""" + CLOUD_CONFIG = { + 'clouds': + { + '_test_cloud_': + { + 'auth': + { + 'auth_url': 'http://198.51.100.1:35357/v2.0', + 'username': '_test_user_', + 'password': '_test_pass_', + 'project_name': '_test_project_', + }, + 'region_name': '_test_region_', + }, + }, + } + def setUp(self): """Run before each test method to initialize test environment.""" @@ -40,7 +61,19 @@ def _nosleep(seconds): self.sleep_fixture = self.useFixture(fixtures.MonkeyPatch( 'time.sleep', _nosleep)) - # Getting the inner exceptions in the test log is super useful - self.useFixture(fixtures.MonkeyPatch( - 'shade.exc.OpenStackCloudException.log_inner_exceptions', - True)) + + # Isolate os-client-config from test environment + config = tempfile.NamedTemporaryFile(delete=False) + config.write(bytes(yaml.dump(self.CLOUD_CONFIG).encode('utf-8'))) + config.close() + vendor = tempfile.NamedTemporaryFile(delete=False) + vendor.write(b'{}') + vendor.close() + + self.config = occ.OpenStackConfig( + config_files=[config.name], + vendor_files=[vendor.name]) + self.cloud_config = self.config.get_one_cloud(cloud='_test_cloud_') + self.cloud = shade.OpenStackCloud( + cloud_config=self.cloud_config, + log_inner_exceptions=True) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 85585f456..76ae9cf22 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -18,7 +18,6 @@ import os_client_config as occ import testtools import warlock -import yaml import shade.openstackcloud from shade import _utils @@ -98,7 +97,7 @@ def _(msg): class TestMemoryCache(base.TestCase): - CACHING_CONFIG = { + CLOUD_CONFIG = { 'cache': { 'max_age': 90, @@ -109,7 +108,7 @@ class TestMemoryCache(base.TestCase): }, 'clouds': { - '_cache_test_': + '_test_cloud_': { 'auth': { @@ -123,22 +122,6 @@ class TestMemoryCache(base.TestCase): }, } - def setUp(self): - super(TestMemoryCache, self).setUp() - - # Isolate os-client-config from test environment - config = tempfile.NamedTemporaryFile(delete=False) - config.write(bytes(yaml.dump(self.CACHING_CONFIG).encode('utf-8'))) - config.close() - vendor = tempfile.NamedTemporaryFile(delete=False) - vendor.write(b'{}') - vendor.close() - - self.cloud_config = occ.OpenStackConfig(config_files=[config.name], - vendor_files=[vendor.name]) - self.cloud = shade.openstack_cloud(cloud='_cache_test_', - config=self.cloud_config) - def test_openstack_cloud(self): self.assertIsInstance(self.cloud, shade.OpenStackCloud) @@ -523,9 +506,20 @@ def __init__(self, id): class TestBogusAuth(base.TestCase): - CONFIG = { + CLOUD_CONFIG = { 'clouds': { + '_test_cloud_': + { + 'auth': + { + 'auth_url': 'http://198.51.100.1:35357/v2.0', + 'username': '_test_user_', + 'password': '_test_pass_', + 'project_name': '_test_project_', + }, + 'region_name': '_test_region_', + }, '_bogus_test_': { 'auth_type': 'bogus', @@ -544,18 +538,7 @@ class TestBogusAuth(base.TestCase): def setUp(self): super(TestBogusAuth, self).setUp() - # Isolate os-client-config from test environment - config = tempfile.NamedTemporaryFile(delete=False) - config.write(bytes(yaml.dump(self.CONFIG).encode('utf-8'))) - config.close() - vendor = tempfile.NamedTemporaryFile(delete=False) - vendor.write(b'{}') - vendor.close() - - self.cloud_config = occ.OpenStackConfig(config_files=[config.name], - vendor_files=[vendor.name]) - def test_get_auth_bogus(self): with testtools.ExpectedException(exc.OpenStackCloudException): shade.openstack_cloud( - cloud='_bogus_test_', config=self.cloud_config) + cloud='_bogus_test_', config=self.config) diff --git a/shade/tests/unit/test_domain_params.py b/shade/tests/unit/test_domain_params.py index f91483b22..628fe9f3c 100644 --- a/shade/tests/unit/test_domain_params.py +++ b/shade/tests/unit/test_domain_params.py @@ -24,10 +24,6 @@ class TestDomainParams(base.TestCase): - def setUp(self): - super(TestDomainParams, self).setUp() - self.cloud = shade.openstack_cloud(validate=False) - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, 'get_project') def test_identity_params_v3(self, mock_get_project, mock_api_version): diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index 6d35070d8..f93ad76ed 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -20,14 +20,13 @@ import shade from shade import exc -from shade.tests import base +from shade.tests.unit import base class TestImage(base.TestCase): def setUp(self): super(TestImage, self).setUp() - self.cloud = shade.openstack_cloud(validate=False) self.image_id = str(uuid.uuid4()) self.fake_search_return = [{ u'image_state': u'available', diff --git a/shade/tests/unit/test_keypair.py b/shade/tests/unit/test_keypair.py index 86a7d398c..2d8698554 100644 --- a/shade/tests/unit/test_keypair.py +++ b/shade/tests/unit/test_keypair.py @@ -25,10 +25,6 @@ class TestKeypair(base.TestCase): - def setUp(self): - super(TestKeypair, self).setUp() - self.cloud = shade.openstack_cloud(validate=False) - @patch.object(shade.OpenStackCloud, 'nova_client') def test_create_keypair(self, mock_nova): keyname = 'my_keyname' diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index faf06e462..adf5aad8d 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -13,7 +13,6 @@ # limitations under the License. import mock -import testtools import warlock from neutronclient.common import exceptions as neutron_exceptions @@ -22,6 +21,7 @@ from shade import _utils from shade import meta from shade.tests import fakes +from shade.tests.unit import base PRIVATE_V4 = '198.51.100.3' PUBLIC_V4 = '192.0.2.99' @@ -80,7 +80,7 @@ class FakeServer(object): accessIPv6 = '' -class TestMeta(testtools.TestCase): +class TestMeta(base.TestCase): def test_find_nova_addresses_key_name(self): # Note 198.51.100.0/24 is TEST-NET-2 from rfc5737 addrs = {'public': [{'addr': '198.51.100.1', 'version': 4}], @@ -122,9 +122,10 @@ def test_find_nova_addresses_all(self): def test_get_server_ip(self): srv = meta.obj_to_dict(FakeServer()) - cloud = shade.openstack_cloud(validate=False) - self.assertEqual(PRIVATE_V4, meta.get_server_private_ip(srv, cloud)) - self.assertEqual(PUBLIC_V4, meta.get_server_external_ipv4(cloud, srv)) + self.assertEqual( + PRIVATE_V4, meta.get_server_private_ip(srv, self.cloud)) + self.assertEqual( + PUBLIC_V4, meta.get_server_external_ipv4(self.cloud, srv)) @mock.patch.object(shade.OpenStackCloud, 'has_service') @mock.patch.object(shade.OpenStackCloud, 'search_networks') @@ -145,9 +146,9 @@ def test_get_server_private_ip( 'addr': PUBLIC_V4, 'version': 4}]} )) - cloud = shade.openstack_cloud(validate=False) - self.assertEqual(PRIVATE_V4, meta.get_server_private_ip(srv, cloud)) + self.assertEqual( + PRIVATE_V4, meta.get_server_private_ip(srv, self.cloud)) mock_has_service.assert_called_with('network') mock_search_networks.assert_called_with( filters={'router:external': False} @@ -176,8 +177,7 @@ def test_get_server_private_ip_devstack( }, ] - cloud = shade.openstack_cloud(validate=False) - srv = cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( + srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', flavor={u'id': u'1'}, image={ @@ -216,8 +216,7 @@ def test_get_server_external_ipv4_neutron( 'addr': PUBLIC_V4, 'version': 4}]}, )) - ip = meta.get_server_external_ipv4( - cloud=shade.openstack_cloud(validate=False), server=srv) + ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) self.assertEqual(PUBLIC_V4, ip) @@ -225,8 +224,7 @@ def test_get_server_external_ipv4_neutron_accessIPv4(self): srv = meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', accessIPv4=PUBLIC_V4)) - ip = meta.get_server_external_ipv4( - cloud=shade.openstack_cloud(validate=False), server=srv) + ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) self.assertEqual(PUBLIC_V4, ip) @@ -254,8 +252,7 @@ def test_get_server_external_ipv4_neutron_exception( srv = meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE')) - ip = meta.get_server_external_ipv4( - cloud=shade.openstack_cloud(validate=False), server=srv) + ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) self.assertEqual(PUBLIC_V4, ip) self.assertTrue(mock_get_server_ip.called) @@ -269,8 +266,7 @@ def test_get_server_external_ipv4_nova_public( srv = meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', addresses={'public': [{'addr': PUBLIC_V4, 'version': 4}]})) - ip = meta.get_server_external_ipv4( - cloud=shade.openstack_cloud(validate=False), server=srv) + ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) self.assertEqual(PUBLIC_V4, ip) @@ -285,8 +281,7 @@ def test_get_server_external_ipv4_nova_none( srv = meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', addresses={'test-net': [{'addr': PRIVATE_V4}]})) - ip = meta.get_server_external_ipv4( - cloud=shade.openstack_cloud(validate=False), server=srv) + ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) self.assertIsNone(ip) self.assertTrue(mock_get_server_ip.called) diff --git a/shade/tests/unit/test_network.py b/shade/tests/unit/test_network.py index 4f20396b6..ee007c91f 100644 --- a/shade/tests/unit/test_network.py +++ b/shade/tests/unit/test_network.py @@ -19,10 +19,6 @@ class TestNetwork(base.TestCase): - def setUp(self): - super(TestNetwork, self).setUp() - self.cloud = shade.openstack_cloud(validate=False) - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') def test_create_network(self, mock_neutron): self.cloud.create_network("netname") diff --git a/shade/tests/unit/test_security_groups.py b/shade/tests/unit/test_security_groups.py index 1c1ab6b9d..4012c3206 100644 --- a/shade/tests/unit/test_security_groups.py +++ b/shade/tests/unit/test_security_groups.py @@ -55,10 +55,6 @@ class TestSecurityGroups(base.TestCase): - def setUp(self): - super(TestSecurityGroups, self).setUp() - self.cloud = shade.openstack_cloud(validate=False) - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_list_security_groups_neutron(self, mock_nova, mock_neutron): diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index f66b82976..0ed6ba9f1 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -41,10 +41,6 @@ class TestShade(base.TestCase): - def setUp(self): - super(TestShade, self).setUp() - self.cloud = shade.openstack_cloud(validate=False) - def test_openstack_cloud(self): self.assertIsInstance(self.cloud, shade.OpenStackCloud) diff --git a/shade/tests/unit/test_stack.py b/shade/tests/unit/test_stack.py index 28820429f..b2b40bc0c 100644 --- a/shade/tests/unit/test_stack.py +++ b/shade/tests/unit/test_stack.py @@ -26,10 +26,6 @@ class TestStack(base.TestCase): - def setUp(self): - super(TestStack, self).setUp() - self.cloud = shade.openstack_cloud(validate=False) - @mock.patch.object(shade.OpenStackCloud, 'heat_client') def test_list_stacks(self, mock_heat): fake_stacks = [ diff --git a/shade/tests/unit/test_volume.py b/shade/tests/unit/test_volume.py index f53f284c3..01cb0c316 100644 --- a/shade/tests/unit/test_volume.py +++ b/shade/tests/unit/test_volume.py @@ -22,10 +22,6 @@ class TestVolume(base.TestCase): - def setUp(self): - super(TestVolume, self).setUp() - self.cloud = shade.openstack_cloud(validate=False) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_attach_volume(self, mock_nova): server = dict(id='server001') From 55db0a565cebcc09c4277e44b356a2e2a7bf2fc1 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 18 Mar 2016 14:02:01 -0400 Subject: [PATCH 0843/3836] Bug fix: Make set/unset of flavor specs work again The 1.2.0 release broke the API methods to set and unset flavor extra specs because we need the raw object, as returned from the nova client, to make method calls to change those values. As a result, a new 'raw' parameter was added to the submitTask() method of TaskManager to allow us to get these raw objects back. Additionally, we were never displaying the 'extra_specs' for a flavor. This, too, requires a raw object method call. Flavors are now normalized to remove client cruft and make sure that 'extra_specs' is always an attribute. As if that weren't enough, we now do functional tests for these things! What more could one ask for??? Change-Id: Ie5c132317392cf26df2c8f43e9f07d040119eca0 --- .../notes/flavor_fix-a53c6b326dc34a2c.yaml | 7 +++ shade/_utils.py | 12 +++++ shade/openstackcloud.py | 9 +++- shade/operatorcloud.py | 4 +- shade/task_manager.py | 16 ++++-- shade/tests/fakes.py | 6 ++- shade/tests/functional/test_flavor.py | 43 +++++++++++++-- shade/tests/unit/test_caching.py | 6 ++- shade/tests/unit/test_flavors.py | 4 +- shade/tests/unit/test_shade.py | 52 ++++--------------- 10 files changed, 101 insertions(+), 58 deletions(-) create mode 100644 releasenotes/notes/flavor_fix-a53c6b326dc34a2c.yaml diff --git a/releasenotes/notes/flavor_fix-a53c6b326dc34a2c.yaml b/releasenotes/notes/flavor_fix-a53c6b326dc34a2c.yaml new file mode 100644 index 000000000..9a7ba7de1 --- /dev/null +++ b/releasenotes/notes/flavor_fix-a53c6b326dc34a2c.yaml @@ -0,0 +1,7 @@ +--- +features: + - Flavors will always contain an 'extra_specs' attribute. Client cruft, + such as 'links', 'HUMAN_ID', etc. has been removed. +fixes: + - Setting and unsetting flavor extra specs now works. This had + been broken since the 1.2.0 release. diff --git a/shade/_utils.py b/shade/_utils.py index f0833a726..7c6eaaf02 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -468,6 +468,18 @@ def normalize_stacks(stacks): return stacks +def normalize_flavors(flavors): + """ Normalize a list of flavor objects """ + for flavor in flavors: + flavor.pop('links', None) + flavor.pop('NAME_ATTR', None) + flavor.pop('HUMAN_ID', None) + flavor.pop('human_id', None) + if 'extra_specs' not in flavor: + flavor['extra_specs'] = {} + return flavors + + def valid_kwargs(*valid_args): # This decorator checks if argument passed as **kwargs to a function are # present in valid_args. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 647c698cd..b89b8a816 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1137,7 +1137,14 @@ def list_flavors(self): """ with _utils.shade_exceptions("Error fetching flavor list"): - return self.manager.submitTask(_tasks.FlavorList(is_public=None)) + raw_flavors = self.manager.submitTask( + _tasks.FlavorList(is_public=None), raw=True) + for flavor in raw_flavors: + flavor.extra_specs = flavor.get_keys() + + return _utils.normalize_flavors( + meta.obj_list_to_dict(raw_flavors) + ) @_utils.cache_on_arguments(should_cache_fn=_no_pending_stacks) def list_stacks(self): diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index ea5342abc..66da61494 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1429,7 +1429,7 @@ def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", is_public=is_public) ) - return flavor + return _utils.normalize_flavors([flavor])[0] def delete_flavor(self, name_or_id): """Delete a flavor @@ -1466,7 +1466,7 @@ def _mod_flavor_specs(self, action, flavor_id, specs): """ try: flavor = self.manager.submitTask( - _tasks.FlavorGet(flavor=flavor_id) + _tasks.FlavorGet(flavor=flavor_id), raw=True ) except nova_exceptions.NotFound: self.log.debug( diff --git a/shade/task_manager.py b/shade/task_manager.py index 620347b15..3b9d0c9a1 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -70,7 +70,7 @@ def exception(self, e, tb): self._traceback = tb self._finished.set() - def wait(self): + def wait(self, raw): self._finished.wait() # TODO(mordred): We store the raw requests response if there is # one now. So we should probably do an error handler to throw @@ -79,6 +79,10 @@ def wait(self): six.reraise(type(self._exception), self._exception, self._traceback) + if raw: + # Do NOT convert the result. + return self._result + # NOTE(Shrews): Since the client API might decide to subclass one # of these result types, we use isinstance() here instead of type(). if (isinstance(self._result, list) or @@ -126,7 +130,13 @@ def run(self): """ This is a direct action passthrough TaskManager """ pass - def submitTask(self, task): + def submitTask(self, task, raw=False): + """Submit and execute the given task. + + :param task: The task to execute. + :param bool raw: If True, return the raw result as received from the + underlying client call. + """ self.log.debug( "Manager %s running task %s" % (self.name, type(task).__name__)) start = time.time() @@ -135,4 +145,4 @@ def submitTask(self, task): self.log.debug( "Manager %s ran task %s in %ss" % ( self.name, type(task).__name__, (end - start))) - return task.wait() + return task.wait(raw) diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index f14314f75..1f3444aaf 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -41,9 +41,13 @@ def __init__(self, id, service_id, region, url, interface=None): class FakeFlavor(object): - def __init__(self, id, name): + def __init__(self, id, name, ram): self.id = id self.name = name + self.ram = ram + + def get_keys(self): + return {} class FakeFloatingIP(object): diff --git a/shade/tests/functional/test_flavor.py b/shade/tests/functional/test_flavor.py index 123b3c850..aefc00452 100644 --- a/shade/tests/functional/test_flavor.py +++ b/shade/tests/functional/test_flavor.py @@ -21,9 +21,6 @@ Functional tests for `shade` flavor resource. """ -import string -import random - import shade from shade.exc import OpenStackCloudException from shade.tests import base @@ -37,8 +34,7 @@ def setUp(self): self.operator_cloud = shade.operator_cloud(cloud='devstack-admin') # Generate a random name for flavors in this test - self.new_item_name = 'flavor_' + ''.join( - random.choice(string.ascii_lowercase) for _ in range(5)) + self.new_item_name = self.getUniqueString('flavor') self.addCleanup(self._cleanup_flavors) @@ -67,6 +63,12 @@ def test_create_flavor(self): flavor = self.operator_cloud.create_flavor(**flavor_kwargs) self.assertIsNotNone(flavor['id']) + + # When properly normalized, we should always get an extra_specs + # and expect empty dict on create. + self.assertIn('extra_specs', flavor) + self.assertEqual({}, flavor['extra_specs']) + for key in flavor_kwargs.keys(): self.assertIn(key, flavor) for key, value in flavor_kwargs.items(): @@ -93,6 +95,8 @@ def test_list_flavors(self): # to make sure both of the flavors we just created are present. found = [] for f in flavors: + # extra_specs should be added within list_flavors() + self.assertIn('extra_specs', f) if f['name'] in (pub_flavor_name, priv_flavor_name): found.append(f) self.assertEqual(2, len(found)) @@ -125,3 +129,32 @@ def test_flavor_access(self): project['id']) flavors = self.demo_cloud.search_flavors(priv_flavor_name) self.assertEqual(0, len(flavors)) + + def test_set_unset_flavor_specs(self): + """ + Test setting and unsetting flavor extra specs + """ + flavor_name = self.new_item_name + '_spec_test' + kwargs = dict( + name=flavor_name, ram=1024, vcpus=2, disk=10 + ) + new_flavor = self.operator_cloud.create_flavor(**kwargs) + + # Expect no extra_specs + self.assertEqual({}, new_flavor['extra_specs']) + + # Now set them + extra_specs = {'foo': 'aaa', 'bar': 'bbb'} + self.operator_cloud.set_flavor_specs(new_flavor['id'], extra_specs) + mod_flavor = self.operator_cloud.get_flavor(new_flavor['id']) + + # Verify extra_specs were set + self.assertIn('extra_specs', mod_flavor) + self.assertEqual(extra_specs, mod_flavor['extra_specs']) + + # Unset the 'foo' value + self.operator_cloud.unset_flavor_specs(mod_flavor['id'], ['foo']) + mod_flavor = self.operator_cloud.get_flavor(new_flavor['id']) + + # Verify 'foo' is unset and 'bar' is still set + self.assertEqual({'bar': 'bbb'}, mod_flavor['extra_specs']) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 76ae9cf22..3b30cdfd3 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -303,8 +303,10 @@ def test_list_flavors(self, nova_mock): nova_mock.flavors.list.return_value = [] self.assertEqual([], self.cloud.list_flavors()) - fake_flavor = fakes.FakeFlavor('555', 'vanilla') - fake_flavor_dict = meta.obj_to_dict(fake_flavor) + fake_flavor = fakes.FakeFlavor('555', 'vanilla', 100) + fake_flavor_dict = _utils.normalize_flavors( + [meta.obj_to_dict(fake_flavor)] + )[0] nova_mock.flavors.list.return_value = [fake_flavor] self.cloud.list_flavors.invalidate(self.cloud) self.assertEqual([fake_flavor_dict], self.cloud.list_flavors()) diff --git a/shade/tests/unit/test_flavors.py b/shade/tests/unit/test_flavors.py index c44b29717..9342ab55f 100644 --- a/shade/tests/unit/test_flavors.py +++ b/shade/tests/unit/test_flavors.py @@ -40,7 +40,7 @@ def test_create_flavor(self, mock_nova): @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_delete_flavor(self, mock_nova): mock_nova.flavors.list.return_value = [ - fakes.FakeFlavor('123', 'lemon') + fakes.FakeFlavor('123', 'lemon', 100) ] self.assertTrue(self.op_cloud.delete_flavor('lemon')) mock_nova.flavors.delete.assert_called_once_with(flavor='123') @@ -54,7 +54,7 @@ def test_delete_flavor_not_found(self, mock_nova): @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_delete_flavor_exception(self, mock_nova): mock_nova.flavors.list.return_value = [ - fakes.FakeFlavor('123', 'lemon') + fakes.FakeFlavor('123', 'lemon', 100) ] mock_nova.flavors.delete.side_effect = Exception() self.assertRaises(shade.OpenStackCloudException, diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 0ed6ba9f1..9cffa1fde 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -24,7 +24,6 @@ import shade from shade import _utils from shade import exc -from shade import meta from shade.tests import fakes from shade.tests.unit import base @@ -493,47 +492,21 @@ def test_update_subnet_conflict_gw_ops(self, mock_client, mock_get): @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_get_flavor_by_ram(self, mock_nova_client): - - class Flavor1(object): - id = '1' - name = 'vanilla ice cream' - ram = 100 - - class Flavor2(object): - id = '2' - name = 'chocolate ice cream' - ram = 200 - - vanilla = meta.obj_to_dict(Flavor1()) - chocolate = meta.obj_to_dict(Flavor2()) + vanilla = fakes.FakeFlavor('1', 'vanilla ice cream', 100) + chocolate = fakes.FakeFlavor('1', 'chocolate ice cream', 200) mock_nova_client.flavors.list.return_value = [vanilla, chocolate] flavor = self.cloud.get_flavor_by_ram(ram=150) - self.assertEquals(chocolate, flavor) + self.assertEquals(chocolate.id, flavor['id']) @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_get_flavor_by_ram_and_include(self, mock_nova_client): - class Flavor1(object): - id = '1' - name = 'vanilla ice cream' - ram = 100 - - class Flavor2(object): - id = '2' - name = 'chocolate ice cream' - ram = 200 - - class Flavor3(object): - id = '3' - name = 'strawberry ice cream' - ram = 250 - - vanilla = meta.obj_to_dict(Flavor1()) - chocolate = meta.obj_to_dict(Flavor2()) - strawberry = meta.obj_to_dict(Flavor3()) + vanilla = fakes.FakeFlavor('1', 'vanilla ice cream', 100) + chocolate = fakes.FakeFlavor('2', 'chocoliate ice cream', 200) + strawberry = fakes.FakeFlavor('3', 'strawberry ice cream', 250) mock_nova_client.flavors.list.return_value = [ vanilla, chocolate, strawberry] flavor = self.cloud.get_flavor_by_ram(ram=150, include='strawberry') - self.assertEquals(strawberry, flavor) + self.assertEquals(strawberry.id, flavor['id']) @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_get_flavor_by_ram_not_found(self, mock_nova_client): @@ -544,17 +517,12 @@ def test_get_flavor_by_ram_not_found(self, mock_nova_client): @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_get_flavor_string_and_int(self, mock_nova_client): - class Flavor1(object): - id = '1' - name = 'vanilla ice cream' - ram = 100 - - vanilla = meta.obj_to_dict(Flavor1()) + vanilla = fakes.FakeFlavor('1', 'vanilla ice cream', 100) mock_nova_client.flavors.list.return_value = [vanilla] flavor1 = self.cloud.get_flavor('1') - self.assertEquals(vanilla, flavor1) + self.assertEquals(vanilla.id, flavor1['id']) flavor2 = self.cloud.get_flavor(1) - self.assertEquals(vanilla, flavor2) + self.assertEquals(vanilla.id, flavor2['id']) def test__neutron_exceptions_resource_not_found(self): with mock.patch.object( From e1f6f1c9eea2d6b0bf5b32a7c16630e233ca6cdb Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 19 Mar 2016 04:47:56 -0500 Subject: [PATCH 0844/3836] Run extra specs through TaskManager and use requests The previous fix for flavors and extra_specs made API calls that did not get wrapped in the TaskManager. This is really novaclient's fault for having a weird interface. Change it to call in through the api.client interface on the flavor manager in novaclient so we can Task it. Some clouds return the extra_specs dict in a key called OS-FLV-WITH-EXT-SPECS:extra_specs. If that's present, just add it to the return dict instead of making the extra API call. There are two instances of making raw REST calls to nova via the client.api property of novaclient. However, we have access to a proper keystoneauth Session object we can mount properly on the compute service, so instead of using weird internal novaclient API - let's just use requests. In order to support this, we need to do the request_id logic ourselves. Luckily for us it's easier logic than in python-*client because we don't have to deal with appending request ids since we're never in the middle of a request chain. Change-Id: Ia8e40d542f98f0ccc55131ca277b862cffeecb12 --- shade/_tasks.py | 20 ++++++++++++--- shade/meta.py | 18 ++++++++----- shade/openstackcloud.py | 41 +++++++++++++++++++---------- shade/task_manager.py | 24 +++++++++++++---- shade/tests/fakes.py | 6 ++++- shade/tests/unit/test_caching.py | 12 +++++++-- shade/tests/unit/test_flavors.py | 6 ++++- shade/tests/unit/test_meta.py | 3 +++ shade/tests/unit/test_shade.py | 44 ++++++++++++++++++++++---------- 9 files changed, 130 insertions(+), 44 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 014cc5c69..c4c58053c 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -87,6 +87,14 @@ def main(self, client): return client.nova_client.flavors.list(**self.args) +class FlavorGetExtraSpecs(task_manager.RequestTask): + result_key = 'extra_specs' + + def main(self, client): + return client._compute_client.get( + "/flavors/{id}/os-extra_specs".format(**self.args)) + + class FlavorCreate(task_manager.Task): def main(self, client): return client.nova_client.flavors.create(**self.args) @@ -166,10 +174,16 @@ def main(self, client): return client.nova_client.keypairs.delete(**self.args) -class NovaUrlGet(task_manager.Task): +class NovaListExtensions(task_manager.RequestTask): + result_key = 'extensions' + + def main(self, client): + return client._compute_client.get('/extensions') + + +class NovaUrlGet(task_manager.RequestTask): def main(self, client): - self.requests = True - return client.nova_client.client.get(**self.args) + return client._compute_client.get(**self.args) class NetworkList(task_manager.Task): diff --git a/shade/meta.py b/shade/meta.py index 11c77fdba..7707c0378 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -323,7 +323,13 @@ def get_hostvars_from_server(cloud, server, mounts=None): return server_vars -def obj_to_dict(obj): +def _add_request_id(obj, request_id): + if request_id is not None: + obj['x_openstack_request_ids'] = [request_id] + return obj + + +def obj_to_dict(obj, request_id=None): """ Turn an object with attributes into a dict suitable for serializing. Some of the things that are returned in OpenStack are objects with @@ -340,7 +346,7 @@ def obj_to_dict(obj): return obj elif hasattr(obj, 'schema') and hasattr(obj, 'validate'): # It's a warlock - return warlock_to_dict(obj) + return _add_request_id(warlock_to_dict(obj), request_id) elif isinstance(obj, dict): # The new request-id tracking spec: # https://specs.openstack.org/openstack/nova-specs/specs/juno/approved/log-request-id-mappings.html @@ -357,10 +363,10 @@ def obj_to_dict(obj): value = getattr(obj, key) if isinstance(value, NON_CALLABLES) and not key.startswith('_'): instance[key] = value - return instance + return _add_request_id(instance, request_id) -def obj_list_to_dict(list): +def obj_list_to_dict(obj_list, request_id=None): """Enumerate through lists of objects and return lists of dictonaries. Some of the objects returned in OpenStack are actually lists of objects, @@ -368,8 +374,8 @@ def obj_list_to_dict(list): the conversion to lists of dictonaries. """ new_list = [] - for obj in list: - new_list.append(obj_to_dict(obj)) + for obj in obj_list: + new_list.append(obj_to_dict(obj, request_id=request_id)) return new_list diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index b89b8a816..a7f94c5cd 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -242,6 +242,8 @@ def invalidate(self): self._swift_service_lock = threading.Lock() self._trove_client = None + self._raw_clients = {} + self._local_ipv6 = _utils.localhost_supports_ipv6() self.cloud_config = cloud_config @@ -283,6 +285,15 @@ def _get_client( service=service_key)) return client + def _get_raw_client(self, service_key): + return self.cloud_config.get_session_client(service_key) + + @property + def _compute_client(self): + if 'compute' not in self._raw_clients: + self._raw_clients['compute'] = self._get_raw_client('compute') + return self._raw_clients['compute'] + @property def nova_client(self): if self._nova_client is None: @@ -922,10 +933,9 @@ def _nova_extensions(self): extensions = set() with _utils.shade_exceptions("Error fetching extension list for nova"): - body = self.manager.submitTask( - _tasks.NovaUrlGet(url='/extensions')) - for x in body['extensions']: - extensions.add(x['alias']) + for extension in self.manager.submitTask( + _tasks.NovaListExtensions()): + extensions.add(extension['alias']) return extensions @@ -1130,21 +1140,26 @@ def list_volumes(self, cache=True): self.manager.submitTask(_tasks.VolumeList())) @_utils.cache_on_arguments() - def list_flavors(self): + def list_flavors(self, get_extra=True): """List all available flavors. :returns: A list of flavor dicts. """ with _utils.shade_exceptions("Error fetching flavor list"): - raw_flavors = self.manager.submitTask( - _tasks.FlavorList(is_public=None), raw=True) - for flavor in raw_flavors: - flavor.extra_specs = flavor.get_keys() - - return _utils.normalize_flavors( - meta.obj_list_to_dict(raw_flavors) - ) + flavors = self.manager.submitTask( + _tasks.FlavorList(is_public=None)) + + with _utils.shade_exceptions("Error fetching flavor extra specs"): + for flavor in flavors: + if 'OS-FLV-WITH-EXT-SPECS:extra_specs' in flavor: + flavor.extra_specs = flavor.get( + 'OS-FLV-WITH-EXT-SPECS:extra_specs') + elif get_extra: + flavor.extra_specs = self.manager.submitTask( + _tasks.FlavorGetExtraSpecs(id=flavor.id)) + + return _utils.normalize_flavors(flavors) @_utils.cache_on_arguments(should_cache_fn=_no_pending_stacks) def list_stacks(self): diff --git a/shade/task_manager.py b/shade/task_manager.py index 3b9d0c9a1..4cd03a6bd 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -53,6 +53,7 @@ def __init__(self, **kw): self._finished = threading.Event() self.args = kw self.requests = False + self._request_id = None @abc.abstractmethod def main(self, client): @@ -72,9 +73,7 @@ def exception(self, e, tb): def wait(self, raw): self._finished.wait() - # TODO(mordred): We store the raw requests response if there is - # one now. So we should probably do an error handler to throw - # some exceptions if it's not 200 + if self._exception: six.reraise(type(self._exception), self._exception, self._traceback) @@ -87,7 +86,8 @@ def wait(self, raw): # of these result types, we use isinstance() here instead of type(). if (isinstance(self._result, list) or isinstance(self._result, types.GeneratorType)): - return meta.obj_list_to_dict(self._result) + return meta.obj_list_to_dict( + self._result, request_id=self._request_id) elif (not isinstance(self._result, bool) and not isinstance(self._result, int) and not isinstance(self._result, float) and @@ -95,7 +95,7 @@ def wait(self, raw): not isinstance(self._result, set) and not isinstance(self._result, tuple) and not isinstance(self._result, types.GeneratorType)): - return meta.obj_to_dict(self._result) + return meta.obj_to_dict(self._result, request_id=self._request_id) else: return self._result @@ -115,6 +115,20 @@ def run(self, client): self.exception(e, sys.exc_info()[2]) +class RequestTask(Task): + + # keystoneauth1 throws keystoneauth1.exceptions.http.HttpError on !200 + def done(self, result): + self._response = result + result_json = self._response.json() + if self.result_key: + self._result = result_json[self.result_key] + else: + self._result = result_json + self._request_id = self._response.headers.get('x-openstack-request-id') + self._finished.set() + + class TaskManager(object): log = _log.setup_logging("shade.TaskManager") diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 1f3444aaf..1f50c3584 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -41,10 +41,14 @@ def __init__(self, id, service_id, region, url, interface=None): class FakeFlavor(object): - def __init__(self, id, name, ram): + def __init__(self, id, name, ram, extra_specs=None): self.id = id self.name = name self.ram = ram + # Leave it unset if we don't pass it in to test that normalize_ works + # but we also have to be able to pass one in to deal with mocks + if extra_specs: + self.extra_specs = extra_specs def get_keys(self): return {} diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 3b30cdfd3..284f1619c 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -298,12 +298,20 @@ def test_modify_user_invalidates_cache(self, keystone_mock): self.assertEqual([], self.cloud.list_users()) self.assertTrue(keystone_mock.users.delete.was_called) + @mock.patch.object(shade.OpenStackCloud, '_compute_client') @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_list_flavors(self, nova_mock): + def test_list_flavors(self, nova_mock, mock_compute): nova_mock.flavors.list.return_value = [] + nova_mock.flavors.api.client.get.return_value = {} + mock_response = mock.Mock() + mock_response.json.return_value = dict(extra_specs={}) + mock_response.headers.get.return_value = 'request-id' + mock_compute.get.return_value = mock_response self.assertEqual([], self.cloud.list_flavors()) - fake_flavor = fakes.FakeFlavor('555', 'vanilla', 100) + fake_flavor = fakes.FakeFlavor( + '555', 'vanilla', 100, dict( + x_openstack_request_ids=['request-id'])) fake_flavor_dict = _utils.normalize_flavors( [meta.obj_to_dict(fake_flavor)] )[0] diff --git a/shade/tests/unit/test_flavors.py b/shade/tests/unit/test_flavors.py index 9342ab55f..820a7a055 100644 --- a/shade/tests/unit/test_flavors.py +++ b/shade/tests/unit/test_flavors.py @@ -37,8 +37,12 @@ def test_create_flavor(self, mock_nova): is_public=True ) + @mock.patch.object(shade.OpenStackCloud, '_compute_client') @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_delete_flavor(self, mock_nova): + def test_delete_flavor(self, mock_nova, mock_compute): + mock_response = mock.Mock() + mock_response.json.return_value = dict(extra_specs=[]) + mock_compute.get.return_value = mock_response mock_nova.flavors.list.return_value = [ fakes.FakeFlavor('123', 'lemon', 100) ] diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index adf5aad8d..9cf6232e6 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -155,6 +155,7 @@ def test_get_server_private_ip( ) @mock.patch.object(shade.OpenStackCloud, 'list_server_security_groups') + @mock.patch.object(shade.OpenStackCloud, 'get_volumes') @mock.patch.object(shade.OpenStackCloud, 'get_image_name') @mock.patch.object(shade.OpenStackCloud, 'get_flavor_name') @mock.patch.object(shade.OpenStackCloud, 'has_service') @@ -162,10 +163,12 @@ def test_get_server_private_ip( def test_get_server_private_ip_devstack( self, mock_search_networks, mock_has_service, mock_get_flavor_name, mock_get_image_name, + mock_get_volumes, mock_list_server_security_groups): mock_get_image_name.return_value = 'cirros-0.3.4-x86_64-uec' mock_get_flavor_name.return_value = 'm1.tiny' mock_has_service.return_value = True + mock_get_volumes.return_value = [] mock_search_networks.return_value = [ { 'id': 'test_pnztt_net', diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 9cffa1fde..2a915a59b 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -490,19 +490,28 @@ def test_update_subnet_conflict_gw_ops(self, mock_client, mock_get): self.cloud.update_subnet, '456', gateway_ip=gateway, disable_gateway_ip=True) + @mock.patch.object(shade.OpenStackCloud, '_compute_client') @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_get_flavor_by_ram(self, mock_nova_client): + def test_get_flavor_by_ram(self, mock_nova_client, mock_compute): vanilla = fakes.FakeFlavor('1', 'vanilla ice cream', 100) chocolate = fakes.FakeFlavor('1', 'chocolate ice cream', 200) mock_nova_client.flavors.list.return_value = [vanilla, chocolate] + mock_response = mock.Mock() + mock_response.json.return_value = dict(extra_specs=[]) + mock_compute.get.return_value = mock_response flavor = self.cloud.get_flavor_by_ram(ram=150) self.assertEquals(chocolate.id, flavor['id']) + @mock.patch.object(shade.OpenStackCloud, '_compute_client') @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_get_flavor_by_ram_and_include(self, mock_nova_client): + def test_get_flavor_by_ram_and_include( + self, mock_nova_client, mock_compute): vanilla = fakes.FakeFlavor('1', 'vanilla ice cream', 100) chocolate = fakes.FakeFlavor('2', 'chocoliate ice cream', 200) strawberry = fakes.FakeFlavor('3', 'strawberry ice cream', 250) + mock_response = mock.Mock() + mock_response.json.return_value = dict(extra_specs=[]) + mock_compute.get.return_value = mock_response mock_nova_client.flavors.list.return_value = [ vanilla, chocolate, strawberry] flavor = self.cloud.get_flavor_by_ram(ram=150, include='strawberry') @@ -515,10 +524,15 @@ def test_get_flavor_by_ram_not_found(self, mock_nova_client): self.cloud.get_flavor_by_ram, ram=100) + @mock.patch.object(shade.OpenStackCloud, '_compute_client') @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_get_flavor_string_and_int(self, mock_nova_client): + def test_get_flavor_string_and_int( + self, mock_nova_client, mock_compute): vanilla = fakes.FakeFlavor('1', 'vanilla ice cream', 100) mock_nova_client.flavors.list.return_value = [vanilla] + mock_response = mock.Mock() + mock_response.json.return_value = dict(extra_specs=[]) + mock_compute.get.return_value = mock_response flavor1 = self.cloud.get_flavor('1') self.assertEquals(vanilla.id, flavor1['id']) flavor2 = self.cloud.get_flavor(1) @@ -625,8 +639,8 @@ def test_iterate_timeout_timeout(self, mock_sleep): pass mock_sleep.assert_called_with(1.0) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test__nova_extensions(self, mock_nova): + @mock.patch.object(shade.OpenStackCloud, '_compute_client') + def test__nova_extensions(self, mock_compute): body = { 'extensions': [ { @@ -647,22 +661,24 @@ def test__nova_extensions(self, mock_nova): }, ] } - mock_nova.client.get.return_value = ('200', body) + mock_response = mock.Mock() + mock_response.json.return_value = body + mock_compute.get.return_value = mock_response extensions = self.cloud._nova_extensions() - mock_nova.client.get.assert_called_once_with(url='/extensions') + mock_compute.get.assert_called_once_with('/extensions') self.assertEqual(set(['NMN', 'OS-DCF']), extensions) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test__nova_extensions_fails(self, mock_nova): - mock_nova.client.get.side_effect = Exception() + @mock.patch.object(shade.OpenStackCloud, '_compute_client') + def test__nova_extensions_fails(self, mock_compute): + mock_compute.get.side_effect = Exception() with testtools.ExpectedException( exc.OpenStackCloudException, "Error fetching extension list for nova" ): self.cloud._nova_extensions() - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test__has_nova_extension(self, mock_nova): + @mock.patch.object(shade.OpenStackCloud, '_compute_client') + def test__has_nova_extension(self, mock_compute): body = { 'extensions': [ { @@ -683,7 +699,9 @@ def test__has_nova_extension(self, mock_nova): }, ] } - mock_nova.client.get.return_value = ('200', body) + mock_response = mock.Mock() + mock_response.json.return_value = body + mock_compute.get.return_value = mock_response self.assertTrue(self.cloud._has_nova_extension('NMN')) self.assertFalse(self.cloud._has_nova_extension('invalid')) From d133f36b2c20cd7bfcdb2454abc276323148d0a8 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 21 Mar 2016 08:56:11 -0500 Subject: [PATCH 0845/3836] Create clouds in Functional Test base class We need to use the long-form of the OpenStackCloud constructor to enable the internal_exception logging. In order for that not to be crazy, we should do it in a base TestCase class. Also, that way we've got consistently named clouds in all of the tests. Change-Id: I42da312dbcaa8926d28e97e37d13370a544cf4bd --- shade/tests/functional/base.py | 33 +++++ shade/tests/functional/test_compute.py | 115 +++++++++--------- shade/tests/functional/test_domain.py | 29 +++-- shade/tests/functional/test_endpoints.py | 6 +- shade/tests/functional/test_flavor.py | 7 +- shade/tests/functional/test_floating_ip.py | 73 ++++++----- .../tests/functional/test_floating_ip_pool.py | 10 +- shade/tests/functional/test_groups.py | 35 +++--- shade/tests/functional/test_identity.py | 4 +- shade/tests/functional/test_image.py | 44 +++---- shade/tests/functional/test_inventory.py | 10 +- shade/tests/functional/test_network.py | 26 ++-- shade/tests/functional/test_object.py | 37 +++--- shade/tests/functional/test_port.py | 42 +++---- shade/tests/functional/test_range_search.py | 68 +++++------ shade/tests/functional/test_router.py | 69 ++++++----- shade/tests/functional/test_services.py | 6 +- shade/tests/functional/test_users.py | 63 +++++----- shade/tests/functional/test_volume.py | 33 ++--- 19 files changed, 372 insertions(+), 338 deletions(-) create mode 100644 shade/tests/functional/base.py diff --git a/shade/tests/functional/base.py b/shade/tests/functional/base.py new file mode 100644 index 000000000..14839d123 --- /dev/null +++ b/shade/tests/functional/base.py @@ -0,0 +1,33 @@ +# -*- 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. + +import os_client_config as occ + +import shade +from shade.tests import base + + +class BaseFunctionalTestCase(base.TestCase): + def setUp(self): + super(BaseFunctionalTestCase, self).setUp() + + self.config = occ.OpenStackConfig() + demo_config = self.config.get_one_cloud(cloud='devstack') + self.demo_cloud = shade.OpenStackCloud( + cloud_config=demo_config, + log_inner_exceptions=True) + operator_config = self.config.get_one_cloud(cloud='devstack-admin') + self.operator_cloud = shade.OperatorCloud( + cloud_config=operator_config, + log_inner_exceptions=True) diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index e666b4078..44afed760 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -19,19 +19,17 @@ Functional tests for `shade` compute methods. """ -from shade import openstack_cloud -from shade.tests import base +from shade.tests.functional import base from shade.tests.functional.util import pick_flavor, pick_image -class TestCompute(base.TestCase): +class TestCompute(base.BaseFunctionalTestCase): def setUp(self): super(TestCompute, self).setUp() - self.cloud = openstack_cloud(cloud='devstack') - self.flavor = pick_flavor(self.cloud.list_flavors()) + self.flavor = pick_flavor(self.demo_cloud.list_flavors()) if self.flavor is None: self.assertFalse('no sensible flavor available') - self.image = pick_image(self.cloud.list_images()) + self.image = pick_image(self.demo_cloud.list_images()) if self.image is None: self.assertFalse('no sensible image available') self.server_name = self.getUniqueString() @@ -44,59 +42,63 @@ def _cleanup_servers_and_volumes(self, server_name): a server can start the process of deleting a volume if it is booted from that volume. This encapsulates that logic. """ - server = self.cloud.get_server(server_name) + server = self.demo_cloud.get_server(server_name) if not server: return - volumes = self.cloud.get_volumes(server) - self.cloud.delete_server(server.name, wait=True) + volumes = self.demo_cloud.get_volumes(server) + self.demo_cloud.delete_server(server.name, wait=True) for volume in volumes: if volume.status != 'deleting': - self.cloud.delete_volume(volume.id, wait=True) + self.demo_cloud.delete_volume(volume.id, wait=True) def test_create_and_delete_server(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) - server = self.cloud.create_server(name=self.server_name, - image=self.image, - flavor=self.flavor, - wait=True) + server = self.demo_cloud.create_server( + name=self.server_name, + image=self.image, + flavor=self.flavor, + wait=True) self.assertEqual(self.server_name, server['name']) self.assertEqual(self.image.id, server['image']['id']) self.assertEqual(self.flavor.id, server['flavor']['id']) self.assertIsNotNone(server['adminPass']) - self.assertTrue(self.cloud.delete_server(self.server_name, wait=True)) - self.assertIsNone(self.cloud.get_server(self.server_name)) + self.assertTrue( + self.demo_cloud.delete_server(self.server_name, wait=True)) + self.assertIsNone(self.demo_cloud.get_server(self.server_name)) def test_create_and_delete_server_with_admin_pass(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) - server = self.cloud.create_server(name=self.server_name, - image=self.image, - flavor=self.flavor, - admin_pass='sheiqu9loegahSh', - wait=True) + server = self.demo_cloud.create_server( + name=self.server_name, + image=self.image, + flavor=self.flavor, + admin_pass='sheiqu9loegahSh', + wait=True) self.assertEqual(self.server_name, server['name']) self.assertEqual(self.image.id, server['image']['id']) self.assertEqual(self.flavor.id, server['flavor']['id']) self.assertEqual(server['adminPass'], 'sheiqu9loegahSh') - self.assertTrue(self.cloud.delete_server(self.server_name, wait=True)) - self.assertIsNone(self.cloud.get_server(self.server_name)) + self.assertTrue( + self.demo_cloud.delete_server(self.server_name, wait=True)) + self.assertIsNone(self.demo_cloud.get_server(self.server_name)) def test_get_image_id(self): self.assertEqual( - self.image.id, self.cloud.get_image_id(self.image.id)) + self.image.id, self.demo_cloud.get_image_id(self.image.id)) self.assertEqual( - self.image.id, self.cloud.get_image_id(self.image.name)) + self.image.id, self.demo_cloud.get_image_id(self.image.name)) def test_get_image_name(self): self.assertEqual( - self.image.name, self.cloud.get_image_name(self.image.id)) + self.image.name, self.demo_cloud.get_image_name(self.image.id)) self.assertEqual( - self.image.name, self.cloud.get_image_name(self.image.name)) + self.image.name, self.demo_cloud.get_image_name(self.image.name)) def _assert_volume_attach(self, server, volume_id=None): self.assertEqual(self.server_name, server['name']) self.assertEqual('', server['image']) self.assertEqual(self.flavor.id, server['flavor']['id']) - volumes = self.cloud.get_volumes(server) + volumes = self.demo_cloud.get_volumes(server) self.assertEqual(1, len(volumes)) volume = volumes[0] if volume_id: @@ -108,10 +110,10 @@ def _assert_volume_attach(self, server, volume_id=None): return volume_id def test_create_boot_from_volume_image(self): - if not self.cloud.has_service('volume'): + if not self.demo_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) - server = self.cloud.create_server( + server = self.demo_cloud.create_server( name=self.server_name, image=self.image, flavor=self.flavor, @@ -119,21 +121,21 @@ def test_create_boot_from_volume_image(self): volume_size=1, wait=True) volume_id = self._assert_volume_attach(server) - volume = self.cloud.get_volume(volume_id) + volume = self.demo_cloud.get_volume(volume_id) self.assertIsNotNone(volume) self.assertEqual(volume['name'], volume['display_name']) self.assertEqual(True, volume['bootable']) self.assertEqual(server['id'], volume['attachments'][0]['server_id']) - self.assertTrue(self.cloud.delete_server(server.id, wait=True)) - self.assertTrue(self.cloud.delete_volume(volume.id, wait=True)) - self.assertIsNone(self.cloud.get_server(server.id)) - self.assertIsNone(self.cloud.get_volume(volume.id)) + self.assertTrue(self.demo_cloud.delete_server(server.id, wait=True)) + self.assertTrue(self.demo_cloud.delete_volume(volume.id, wait=True)) + self.assertIsNone(self.demo_cloud.get_server(server.id)) + self.assertIsNone(self.demo_cloud.get_volume(volume.id)) def test_create_terminate_volume_image(self): - if not self.cloud.has_service('volume'): + if not self.demo_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) - server = self.cloud.create_server( + server = self.demo_cloud.create_server( name=self.server_name, image=self.image, flavor=self.flavor, @@ -142,21 +144,22 @@ def test_create_terminate_volume_image(self): volume_size=1, wait=True) volume_id = self._assert_volume_attach(server) - self.assertTrue(self.cloud.delete_server(self.server_name, wait=True)) - volume = self.cloud.get_volume(volume_id) + self.assertTrue( + self.demo_cloud.delete_server(self.server_name, wait=True)) + volume = self.demo_cloud.get_volume(volume_id) # We can either get None (if the volume delete was quick), or a volume # that is in the process of being deleted. if volume: self.assertEquals('deleting', volume.status) - self.assertIsNone(self.cloud.get_server(self.server_name)) + self.assertIsNone(self.demo_cloud.get_server(self.server_name)) def test_create_boot_from_volume_preexisting(self): - if not self.cloud.has_service('volume'): + if not self.demo_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) - volume = self.cloud.create_volume( + volume = self.demo_cloud.create_volume( size=1, name=self.server_name, image=self.image, wait=True) - server = self.cloud.create_server( + server = self.demo_cloud.create_server( name=self.server_name, image=None, flavor=self.flavor, @@ -164,24 +167,25 @@ def test_create_boot_from_volume_preexisting(self): volume_size=1, wait=True) volume_id = self._assert_volume_attach(server, volume_id=volume['id']) - self.assertTrue(self.cloud.delete_server(self.server_name, wait=True)) - self.addCleanup(self.cloud.delete_volume, volume_id) - volume = self.cloud.get_volume(volume_id) + self.assertTrue( + self.demo_cloud.delete_server(self.server_name, wait=True)) + self.addCleanup(self.demo_cloud.delete_volume, volume_id) + volume = self.demo_cloud.get_volume(volume_id) self.assertIsNotNone(volume) self.assertEqual(volume['name'], volume['display_name']) self.assertEqual(True, volume['bootable']) self.assertEqual([], volume['attachments']) - self.assertTrue(self.cloud.delete_volume(volume_id)) - self.assertIsNone(self.cloud.get_server(self.server_name)) - self.assertIsNone(self.cloud.get_volume(volume_id)) + self.assertTrue(self.demo_cloud.delete_volume(volume_id)) + self.assertIsNone(self.demo_cloud.get_server(self.server_name)) + self.assertIsNone(self.demo_cloud.get_volume(volume_id)) def test_create_boot_from_volume_preexisting_terminate(self): - if not self.cloud.has_service('volume'): + if not self.demo_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) - volume = self.cloud.create_volume( + volume = self.demo_cloud.create_volume( size=1, name=self.server_name, image=self.image, wait=True) - server = self.cloud.create_server( + server = self.demo_cloud.create_server( name=self.server_name, image=None, flavor=self.flavor, @@ -190,10 +194,11 @@ def test_create_boot_from_volume_preexisting_terminate(self): volume_size=1, wait=True) volume_id = self._assert_volume_attach(server, volume_id=volume['id']) - self.assertTrue(self.cloud.delete_server(self.server_name, wait=True)) - volume = self.cloud.get_volume(volume_id) + self.assertTrue( + self.demo_cloud.delete_server(self.server_name, wait=True)) + volume = self.demo_cloud.get_volume(volume_id) # We can either get None (if the volume delete was quick), or a volume # that is in the process of being deleted. if volume: self.assertEquals('deleting', volume.status) - self.assertIsNone(self.cloud.get_server(self.server_name)) + self.assertIsNone(self.demo_cloud.get_server(self.server_name)) diff --git a/shade/tests/functional/test_domain.py b/shade/tests/functional/test_domain.py index dcb496bdb..fff7544cf 100644 --- a/shade/tests/functional/test_domain.py +++ b/shade/tests/functional/test_domain.py @@ -20,25 +20,25 @@ """ import shade -from shade.tests import base +from shade.tests.functional import base -class TestDomain(base.TestCase): +class TestDomain(base.BaseFunctionalTestCase): def setUp(self): super(TestDomain, self).setUp() - self.cloud = shade.operator_cloud(cloud='devstack-admin') - if self.cloud.cloud_config.get_api_version('identity') in ('2', '2.0'): + i_ver = self.operator_cloud.cloud_config.get_api_version('identity') + if i_ver in ('2', '2.0'): self.skipTest('Identity service does not support domains') self.domain_prefix = self.getUniqueString('domain') self.addCleanup(self._cleanup_domains) def _cleanup_domains(self): exception_list = list() - for domain in self.cloud.list_domains(): + for domain in self.operator_cloud.list_domains(): if domain['name'].startswith(self.domain_prefix): try: - self.cloud.delete_domain(domain['id']) + self.operator_cloud.delete_domain(domain['id']) except Exception as e: exception_list.append(str(e)) continue @@ -52,26 +52,29 @@ def test_search_domains(self): domain_name = self.domain_prefix + '_search' # Shouldn't find any domain with this name yet - results = self.cloud.search_domains(filters=dict(name=domain_name)) + results = self.operator_cloud.search_domains( + filters=dict(name=domain_name)) self.assertEqual(0, len(results)) # Now create a new domain - domain = self.cloud.create_domain(domain_name) + domain = self.operator_cloud.create_domain(domain_name) self.assertEqual(domain_name, domain['name']) # Now we should find only the new domain - results = self.cloud.search_domains(filters=dict(name=domain_name)) + results = self.operator_cloud.search_domains( + filters=dict(name=domain_name)) self.assertEqual(1, len(results)) self.assertEqual(domain_name, results[0]['name']) def test_update_domain(self): - domain = self.cloud.create_domain(self.domain_prefix, 'description') + domain = self.operator_cloud.create_domain( + self.domain_prefix, 'description') self.assertEqual(self.domain_prefix, domain['name']) self.assertEqual('description', domain['description']) self.assertTrue(domain['enabled']) - updated = self.cloud.update_domain(domain['id'], name='updated name', - description='updated description', - enabled=False) + updated = self.operator_cloud.update_domain( + domain['id'], name='updated name', + description='updated description', enabled=False) self.assertEqual('updated name', updated['name']) self.assertEqual('updated description', updated['description']) self.assertFalse(updated['enabled']) diff --git a/shade/tests/functional/test_endpoints.py b/shade/tests/functional/test_endpoints.py index 03ac0eb12..ca79f855f 100644 --- a/shade/tests/functional/test_endpoints.py +++ b/shade/tests/functional/test_endpoints.py @@ -24,19 +24,17 @@ import string import random -from shade import operator_cloud from shade.exc import OpenStackCloudException -from shade.tests import base +from shade.tests.functional import base -class TestEndpoints(base.TestCase): +class TestEndpoints(base.BaseFunctionalTestCase): endpoint_attributes = ['id', 'region', 'publicurl', 'internalurl', 'service_id', 'adminurl'] def setUp(self): super(TestEndpoints, self).setUp() - self.operator_cloud = operator_cloud(cloud='devstack-admin') # Generate a random name for services and regions in this test self.new_item_name = 'test_' + ''.join( diff --git a/shade/tests/functional/test_flavor.py b/shade/tests/functional/test_flavor.py index aefc00452..96ec22d57 100644 --- a/shade/tests/functional/test_flavor.py +++ b/shade/tests/functional/test_flavor.py @@ -21,17 +21,14 @@ Functional tests for `shade` flavor resource. """ -import shade from shade.exc import OpenStackCloudException -from shade.tests import base +from shade.tests.functional import base -class TestFlavor(base.TestCase): +class TestFlavor(base.BaseFunctionalTestCase): def setUp(self): super(TestFlavor, self).setUp() - self.demo_cloud = shade.openstack_cloud(cloud='devstack') - self.operator_cloud = shade.operator_cloud(cloud='devstack-admin') # Generate a random name for flavors in this test self.new_item_name = self.getUniqueString('flavor') diff --git a/shade/tests/functional/test_floating_ip.py b/shade/tests/functional/test_floating_ip.py index 9a978bc7b..17e70d5a6 100644 --- a/shade/tests/functional/test_floating_ip.py +++ b/shade/tests/functional/test_floating_ip.py @@ -25,22 +25,20 @@ from testtools import content from shade import _utils -from shade import openstack_cloud from shade import meta from shade.exc import OpenStackCloudException -from shade.tests import base +from shade.tests.functional import base from shade.tests.functional.util import pick_flavor, pick_image -class TestFloatingIP(base.TestCase): +class TestFloatingIP(base.BaseFunctionalTestCase): timeout = 60 def setUp(self): super(TestFloatingIP, self).setUp() - self.cloud = openstack_cloud(cloud='devstack') - self.nova = self.cloud.nova_client - if self.cloud.has_service('network'): - self.neutron = self.cloud.neutron_client + self.nova = self.demo_cloud.nova_client + if self.demo_cloud.has_service('network'): + self.neutron = self.demo_cloud.neutron_client self.flavor = pick_flavor(self.nova.flavors.list()) if self.flavor is None: self.assertFalse('no sensible flavor available') @@ -58,9 +56,9 @@ def _cleanup_network(self): exception_list = list() # Delete stale networks as well as networks created for this test - if self.cloud.has_service('network'): + if self.demo_cloud.has_service('network'): # Delete routers - for r in self.cloud.list_routers(): + for r in self.demo_cloud.list_routers(): try: if r['name'].startswith(self.new_item_name): # ToDo: update_router currently won't allow removing @@ -71,7 +69,7 @@ def _cleanup_network(self): self.neutron.update_router( router=r['id'], body={'router': router}) # ToDo: Shade currently doesn't have methods for this - for s in self.cloud.list_subnets(): + for s in self.demo_cloud.list_subnets(): if s['name'].startswith(self.new_item_name): try: self.neutron.remove_interface_router( @@ -79,23 +77,23 @@ def _cleanup_network(self): body={'subnet_id': s['id']}) except Exception: pass - self.cloud.delete_router(name_or_id=r['id']) + self.demo_cloud.delete_router(name_or_id=r['id']) except Exception as e: exception_list.append(str(e)) continue # Delete subnets - for s in self.cloud.list_subnets(): + for s in self.demo_cloud.list_subnets(): if s['name'].startswith(self.new_item_name): try: - self.cloud.delete_subnet(name_or_id=s['id']) + self.demo_cloud.delete_subnet(name_or_id=s['id']) except Exception as e: exception_list.append(str(e)) continue # Delete networks - for n in self.cloud.list_networks(): + for n in self.demo_cloud.list_networks(): if n['name'].startswith(self.new_item_name): try: - self.cloud.delete_network(name_or_id=n['id']) + self.demo_cloud.delete_network(name_or_id=n['id']) except Exception as e: exception_list.append(str(e)) continue @@ -133,11 +131,11 @@ def _cleanup_ips(self, server): fixed_ip = meta.get_server_private_ip(server) - for ip in self.cloud.list_floating_ips(): + for ip in self.demo_cloud.list_floating_ips(): if (ip.get('fixed_ip', None) == fixed_ip or ip.get('fixed_ip_address', None) == fixed_ip): try: - self.cloud.delete_floating_ip(ip['id']) + self.demo_cloud.delete_floating_ip(ip['id']) except Exception as e: exception_list.append(str(e)) continue @@ -148,24 +146,24 @@ def _cleanup_ips(self, server): raise OpenStackCloudException('\n'.join(exception_list)) def _setup_networks(self): - if self.cloud.has_service('network'): + if self.demo_cloud.has_service('network'): # Create a network - self.test_net = self.cloud.create_network( + self.test_net = self.demo_cloud.create_network( name=self.new_item_name + '_net') # Create a subnet on it - self.test_subnet = self.cloud.create_subnet( + self.test_subnet = self.demo_cloud.create_subnet( subnet_name=self.new_item_name + '_subnet', network_name_or_id=self.test_net['id'], cidr='10.24.4.0/24', enable_dhcp=True ) # Create a router - self.test_router = self.cloud.create_router( + self.test_router = self.demo_cloud.create_router( name=self.new_item_name + '_router') # Attach the router to an external network - ext_nets = self.cloud.search_networks( + ext_nets = self.demo_cloud.search_networks( filters={'router:external': True}) - self.cloud.update_router( + self.demo_cloud.update_router( name_or_id=self.test_router['id'], ext_gateway_net_id=ext_nets[0]['id']) # Attach the router to the internal subnet @@ -178,10 +176,10 @@ def _setup_networks(self): self.addDetail( 'networks-neutron', content.text_content(pprint.pformat( - self.cloud.list_networks()))) + self.demo_cloud.list_networks()))) else: # ToDo: remove once we have list/get methods for nova networks - nets = self.cloud.nova_client.networks.list() + nets = self.demo_cloud.nova_client.networks.list() self.addDetail( 'networks-nova', content.text_content(pprint.pformat( @@ -191,10 +189,11 @@ def _setup_networks(self): def test_private_ip(self): self._setup_networks() - new_server = self.cloud.get_openstack_vars(self.cloud.create_server( - wait=True, name=self.new_item_name + '_server', - image=self.image, - flavor=self.flavor, nics=[self.nic])) + new_server = self.demo_cloud.get_openstack_vars( + self.demo_cloud.create_server( + wait=True, name=self.new_item_name + '_server', + image=self.image, + flavor=self.flavor, nics=[self.nic])) self.addDetail( 'server', content.text_content(pprint.pformat(new_server))) @@ -203,7 +202,7 @@ def test_private_ip(self): def test_add_auto_ip(self): self._setup_networks() - new_server = self.cloud.create_server( + new_server = self.demo_cloud.create_server( wait=True, name=self.new_item_name + '_server', image=self.image, flavor=self.flavor, nics=[self.nic]) @@ -213,17 +212,17 @@ def test_add_auto_ip(self): ip = None for _ in _utils._iterate_timeout( self.timeout, "Timeout waiting for IP address to be attached"): - ip = meta.get_server_external_ipv4(self.cloud, new_server) + ip = meta.get_server_external_ipv4(self.demo_cloud, new_server) if ip is not None: break - new_server = self.cloud.get_server(new_server.id) + new_server = self.demo_cloud.get_server(new_server.id) self.addCleanup(self._cleanup_ips, new_server) def test_detach_ip_from_server(self): self._setup_networks() - new_server = self.cloud.create_server( + new_server = self.demo_cloud.create_server( wait=True, name=self.new_item_name + '_server', image=self.image, flavor=self.flavor, nics=[self.nic]) @@ -233,14 +232,14 @@ def test_detach_ip_from_server(self): ip = None for _ in _utils._iterate_timeout( self.timeout, "Timeout waiting for IP address to be attached"): - ip = meta.get_server_external_ipv4(self.cloud, new_server) + ip = meta.get_server_external_ipv4(self.demo_cloud, new_server) if ip is not None: break - new_server = self.cloud.get_server(new_server.id) + new_server = self.demo_cloud.get_server(new_server.id) self.addCleanup(self._cleanup_ips, new_server) - f_ip = self.cloud.get_floating_ip( + f_ip = self.demo_cloud.get_floating_ip( id=None, filters={'floating_ip_address': ip}) - self.cloud.detach_ip_from_server( + self.demo_cloud.detach_ip_from_server( server_id=new_server.id, floating_ip_id=f_ip['id']) diff --git a/shade/tests/functional/test_floating_ip_pool.py b/shade/tests/functional/test_floating_ip_pool.py index 4edbcaf72..d22c52648 100644 --- a/shade/tests/functional/test_floating_ip_pool.py +++ b/shade/tests/functional/test_floating_ip_pool.py @@ -19,8 +19,7 @@ Functional tests for floating IP pool resource (managed by nova) """ -from shade import openstack_cloud -from shade.tests import base +from shade.tests.functional import base # When using nova-network, floating IP pools are created with nova-manage @@ -32,19 +31,18 @@ # nova floating-ip-pool-list returns 404. -class TestFloatingIPPool(base.TestCase): +class TestFloatingIPPool(base.BaseFunctionalTestCase): def setUp(self): super(TestFloatingIPPool, self).setUp() - self.cloud = openstack_cloud(cloud='devstack') - if not self.cloud._has_nova_extension('os-floating-ip-pools'): + if not self.demo_cloud._has_nova_extension('os-floating-ip-pools'): # Skipping this test is floating-ip-pool extension is not # available on the testing cloud self.skip( 'Floating IP pools extension is not available') def test_list_floating_ip_pools(self): - pools = self.cloud.list_floating_ip_pools() + pools = self.demo_cloud.list_floating_ip_pools() if not pools: self.assertFalse('no floating-ip pool available') diff --git a/shade/tests/functional/test_groups.py b/shade/tests/functional/test_groups.py index 1a45dd844..cabfeb52a 100644 --- a/shade/tests/functional/test_groups.py +++ b/shade/tests/functional/test_groups.py @@ -20,25 +20,25 @@ """ import shade -from shade.tests import base +from shade.tests.functional import base -class TestGroup(base.TestCase): +class TestGroup(base.BaseFunctionalTestCase): def setUp(self): super(TestGroup, self).setUp() - self.cloud = shade.operator_cloud(cloud='devstack-admin') - if self.cloud.cloud_config.get_api_version('identity') in ('2', '2.0'): + i_ver = self.operator_cloud.cloud_config.get_api_version('identity') + if i_ver in ('2', '2.0'): self.skipTest('Identity service does not support groups') self.group_prefix = self.getUniqueString('group') self.addCleanup(self._cleanup_groups) def _cleanup_groups(self): exception_list = list() - for group in self.cloud.list_groups(): + for group in self.operator_cloud.list_groups(): if group['name'].startswith(self.group_prefix): try: - self.cloud.delete_group(group['id']) + self.operator_cloud.delete_group(group['id']) except Exception as e: exception_list.append(str(e)) continue @@ -50,7 +50,7 @@ def _cleanup_groups(self): def test_create_group(self): group_name = self.group_prefix + '_create' - group = self.cloud.create_group(group_name, 'test group') + group = self.operator_cloud.create_group(group_name, 'test group') for key in ('id', 'name', 'description', 'domain_id'): self.assertIn(key, group) @@ -60,30 +60,33 @@ def test_create_group(self): def test_delete_group(self): group_name = self.group_prefix + '_delete' - group = self.cloud.create_group(group_name, 'test group') + group = self.operator_cloud.create_group(group_name, 'test group') self.assertIsNotNone(group) - self.assertTrue(self.cloud.delete_group(group_name)) + self.assertTrue(self.operator_cloud.delete_group(group_name)) - results = self.cloud.search_groups(filters=dict(name=group_name)) + results = self.operator_cloud.search_groups( + filters=dict(name=group_name)) self.assertEqual(0, len(results)) def test_delete_group_not_exists(self): - self.assertFalse(self.cloud.delete_group('xInvalidGroupx')) + self.assertFalse(self.operator_cloud.delete_group('xInvalidGroupx')) def test_search_groups(self): group_name = self.group_prefix + '_search' # Shouldn't find any group with this name yet - results = self.cloud.search_groups(filters=dict(name=group_name)) + results = self.operator_cloud.search_groups( + filters=dict(name=group_name)) self.assertEqual(0, len(results)) # Now create a new group - group = self.cloud.create_group(group_name, 'test group') + group = self.operator_cloud.create_group(group_name, 'test group') self.assertEqual(group_name, group['name']) # Now we should find only the new group - results = self.cloud.search_groups(filters=dict(name=group_name)) + results = self.operator_cloud.search_groups( + filters=dict(name=group_name)) self.assertEqual(1, len(results)) self.assertEqual(group_name, results[0]['name']) @@ -91,13 +94,13 @@ def test_update_group(self): group_name = self.group_prefix + '_update' group_desc = 'test group' - group = self.cloud.create_group(group_name, group_desc) + group = self.operator_cloud.create_group(group_name, group_desc) self.assertEqual(group_name, group['name']) self.assertEqual(group_desc, group['description']) updated_group_name = group_name + '_xyz' updated_group_desc = group_desc + ' updated' - updated_group = self.cloud.update_group( + updated_group = self.operator_cloud.update_group( group_name, name=updated_group_name, description=updated_group_desc) diff --git a/shade/tests/functional/test_identity.py b/shade/tests/functional/test_identity.py index 40dbd0b2b..cc6b727e4 100644 --- a/shade/tests/functional/test_identity.py +++ b/shade/tests/functional/test_identity.py @@ -24,10 +24,10 @@ from shade import operator_cloud from shade import OpenStackCloudException -from shade.tests import base +from shade.tests.functional import base -class TestIdentity(base.TestCase): +class TestIdentity(base.BaseFunctionalTestCase): def setUp(self): super(TestIdentity, self).setUp() self.cloud = operator_cloud(cloud='devstack-admin') diff --git a/shade/tests/functional/test_image.py b/shade/tests/functional/test_image.py index 1dc241694..8a08abbf4 100644 --- a/shade/tests/functional/test_image.py +++ b/shade/tests/functional/test_image.py @@ -23,16 +23,14 @@ import os import tempfile -from shade import openstack_cloud -from shade.tests import base +from shade.tests.functional import base from shade.tests.functional.util import pick_image -class TestImage(base.TestCase): +class TestImage(base.BaseFunctionalTestCase): def setUp(self): super(TestImage, self).setUp() - self.cloud = openstack_cloud(cloud='devstack') - self.image = pick_image(self.cloud.nova_client.images.list()) + self.image = pick_image(self.demo_cloud.nova_client.images.list()) def test_create_image(self): test_image = tempfile.NamedTemporaryFile(delete=False) @@ -40,15 +38,16 @@ def test_create_image(self): test_image.close() image_name = self.getUniqueString('image') try: - self.cloud.create_image(name=image_name, - filename=test_image.name, - disk_format='raw', - container_format='bare', - min_disk=10, - min_ram=1024, - wait=True) + self.demo_cloud.create_image( + name=image_name, + filename=test_image.name, + disk_format='raw', + container_format='bare', + min_disk=10, + min_ram=1024, + wait=True) finally: - self.cloud.delete_image(image_name, wait=True) + self.demo_cloud.delete_image(image_name, wait=True) def test_download_image(self): test_image = tempfile.NamedTemporaryFile(delete=False) @@ -56,16 +55,17 @@ def test_download_image(self): test_image.write('\0' * 1024 * 1024) test_image.close() image_name = self.getUniqueString('image') - self.cloud.create_image(name=image_name, - filename=test_image.name, - disk_format='raw', - container_format='bare', - min_disk=10, - min_ram=1024, - wait=True) - self.addCleanup(self.cloud.delete_image, image_name, wait=True) + self.demo_cloud.create_image( + name=image_name, + filename=test_image.name, + disk_format='raw', + container_format='bare', + min_disk=10, + min_ram=1024, + wait=True) + self.addCleanup(self.demo_cloud.delete_image, image_name, wait=True) output = os.path.join(tempfile.gettempdir(), self.getUniqueString()) - self.cloud.download_image(image_name, output) + self.demo_cloud.download_image(image_name, output) self.addCleanup(os.remove, output) self.assertTrue(filecmp.cmp(test_image.name, output), "Downloaded contents don't match created image") diff --git a/shade/tests/functional/test_inventory.py b/shade/tests/functional/test_inventory.py index 764613e5d..13c5a2d42 100644 --- a/shade/tests/functional/test_inventory.py +++ b/shade/tests/functional/test_inventory.py @@ -19,22 +19,20 @@ Functional tests for `shade` inventory methods. """ -from shade import openstack_cloud from shade import inventory -from shade.tests import base +from shade.tests.functional import base from shade.tests.functional.util import pick_flavor, pick_image -class TestInventory(base.TestCase): +class TestInventory(base.BaseFunctionalTestCase): def setUp(self): super(TestInventory, self).setUp() # This needs to use an admin account, otherwise a public IP # is not allocated from devstack. - self.cloud = openstack_cloud(cloud='devstack-admin') self.inventory = inventory.OpenStackInventory() self.server_name = 'test_inventory_server' - self.nova = self.cloud.nova_client + self.nova = self.operator_cloud.nova_client self.flavor = pick_flavor(self.nova.flavors.list()) if self.flavor is None: self.assertTrue(False, 'no sensible flavor available') @@ -42,7 +40,7 @@ def setUp(self): if self.image is None: self.assertTrue(False, 'no sensible image available') self.addCleanup(self._cleanup_servers) - self.cloud.create_server( + self.operator_cloud.create_server( name=self.server_name, image=self.image, flavor=self.flavor, wait=True, auto_ip=True) diff --git a/shade/tests/functional/test_network.py b/shade/tests/functional/test_network.py index 26d4264d1..51cf9057a 100644 --- a/shade/tests/functional/test_network.py +++ b/shade/tests/functional/test_network.py @@ -19,26 +19,24 @@ Functional tests for `shade` network methods. """ -from shade import openstack_cloud from shade.exc import OpenStackCloudException -from shade.tests import base +from shade.tests.functional import base -class TestNetwork(base.TestCase): +class TestNetwork(base.BaseFunctionalTestCase): def setUp(self): super(TestNetwork, self).setUp() - self.cloud = openstack_cloud(cloud='devstack-admin') - if not self.cloud.has_service('network'): + if not self.operator_cloud.has_service('network'): self.skipTest('Network service not supported by cloud') self.network_name = self.getUniqueString('network') self.addCleanup(self._cleanup_networks) def _cleanup_networks(self): exception_list = list() - for network in self.cloud.list_networks(): + for network in self.operator_cloud.list_networks(): if network['name'].startswith(self.network_name): try: - self.cloud.delete_network(network['name']) + self.operator_cloud.delete_network(network['name']) except Exception as e: exception_list.append(str(e)) continue @@ -47,7 +45,7 @@ def _cleanup_networks(self): raise OpenStackCloudException('\n'.join(exception_list)) def test_create_network_basic(self): - net1 = self.cloud.create_network(name=self.network_name) + net1 = self.operator_cloud.create_network(name=self.network_name) self.assertIn('id', net1) self.assertEqual(self.network_name, net1['name']) self.assertFalse(net1['shared']) @@ -55,7 +53,7 @@ def test_create_network_basic(self): self.assertTrue(net1['admin_state_up']) def test_create_network_advanced(self): - net1 = self.cloud.create_network( + net1 = self.operator_cloud.create_network( name=self.network_name, shared=True, external=True, @@ -68,7 +66,7 @@ def test_create_network_advanced(self): self.assertFalse(net1['admin_state_up']) def test_create_network_provider_flat(self): - net1 = self.cloud.create_network( + net1 = self.operator_cloud.create_network( name=self.network_name, shared=True, provider={ @@ -83,10 +81,12 @@ def test_create_network_provider_flat(self): self.assertIsNone(net1['provider:segmentation_id']) def test_list_networks_filtered(self): - net1 = self.cloud.create_network(name=self.network_name) + net1 = self.operator_cloud.create_network(name=self.network_name) self.assertIsNotNone(net1) - net2 = self.cloud.create_network(name=self.network_name + 'other') + net2 = self.operator_cloud.create_network( + name=self.network_name + 'other') self.assertIsNotNone(net2) - match = self.cloud.list_networks(filters=dict(name=self.network_name)) + match = self.operator_cloud.list_networks( + filters=dict(name=self.network_name)) self.assertEqual(1, len(match)) self.assertEqual(net1['name'], match[0]['name']) diff --git a/shade/tests/functional/test_object.py b/shade/tests/functional/test_object.py index 6413f46fd..c773dcbb6 100644 --- a/shade/tests/functional/test_object.py +++ b/shade/tests/functional/test_object.py @@ -23,26 +23,24 @@ from testtools import content -from shade import openstack_cloud -from shade.tests import base +from shade.tests.functional import base -class TestObject(base.TestCase): +class TestObject(base.BaseFunctionalTestCase): def setUp(self): super(TestObject, self).setUp() - self.cloud = openstack_cloud(cloud='devstack') - if not self.cloud.has_service('object-store'): + if not self.demo_cloud.has_service('object-store'): self.skipTest('Object service not supported by cloud') def test_create_object(self): '''Test uploading small and large files.''' container_name = self.getUniqueString('container') self.addDetail('container', content.text_content(container_name)) - self.addCleanup(self.cloud.delete_container, container_name) - self.cloud.create_container(container_name) + self.addCleanup(self.demo_cloud.delete_container, container_name) + self.demo_cloud.create_container(container_name) self.assertEqual(container_name, - self.cloud.list_containers()[0]['name']) + self.demo_cloud.list_containers()[0]['name']) sizes = ( (64 * 1024, 1), # 64K, one segment (50 * 1024 ** 2, 5) # 50MB, 5 segments @@ -54,23 +52,24 @@ def test_create_object(self): sparse_file.write("\0") sparse_file.flush() name = 'test-%d' % size - self.cloud.create_object(container_name, name, - sparse_file.name, - segment_size=segment_size) - self.assertFalse(self.cloud.is_object_stale( + self.demo_cloud.create_object( + container_name, name, + sparse_file.name, + segment_size=segment_size) + self.assertFalse(self.demo_cloud.is_object_stale( container_name, name, sparse_file.name ) ) self.assertIsNotNone( - self.cloud.get_object_metadata(container_name, name)) + self.demo_cloud.get_object_metadata(container_name, name)) self.assertIsNotNone( - self.cloud.get_object(container_name, name)) + self.demo_cloud.get_object(container_name, name)) self.assertEqual( name, - self.cloud.list_objects(container_name)[0]['name']) - self.cloud.delete_object(container_name, name) - self.assertEqual([], self.cloud.list_objects(container_name)) + self.demo_cloud.list_objects(container_name)[0]['name']) + self.demo_cloud.delete_object(container_name, name) + self.assertEqual([], self.demo_cloud.list_objects(container_name)) self.assertEqual(container_name, - self.cloud.list_containers()[0]['name']) - self.cloud.delete_container(container_name) + self.demo_cloud.list_containers()[0]['name']) + self.demo_cloud.delete_container(container_name) diff --git a/shade/tests/functional/test_port.py b/shade/tests/functional/test_port.py index c8f3840d1..31eedfdf9 100644 --- a/shade/tests/functional/test_port.py +++ b/shade/tests/functional/test_port.py @@ -24,18 +24,16 @@ import string import random -from shade import openstack_cloud from shade.exc import OpenStackCloudException -from shade.tests import base +from shade.tests.functional import base -class TestPort(base.TestCase): +class TestPort(base.BaseFunctionalTestCase): def setUp(self): super(TestPort, self).setUp() - self.cloud = openstack_cloud(cloud='devstack-admin') # Skip Neutron tests if neutron is not present - if not self.cloud.has_service('network'): + if not self.operator_cloud.has_service('network'): self.skipTest('Network service not supported by cloud') # Generate a unique port name to allow concurrent tests @@ -47,10 +45,10 @@ def setUp(self): def _cleanup_ports(self): exception_list = list() - for p in self.cloud.list_ports(): + for p in self.operator_cloud.list_ports(): if p['name'].startswith(self.new_port_name): try: - self.cloud.delete_port(name_or_id=p['id']) + self.operator_cloud.delete_port(name_or_id=p['id']) except Exception as e: # We were unable to delete this port, let's try with next exception_list.append(str(e)) @@ -64,11 +62,11 @@ def _cleanup_ports(self): def test_create_port(self): port_name = self.new_port_name + '_create' - networks = self.cloud.list_networks() + networks = self.operator_cloud.list_networks() if not networks: self.assertFalse('no sensible network available') - port = self.cloud.create_port( + port = self.operator_cloud.create_port( network_id=networks[0]['id'], name=port_name) self.assertIsInstance(port, dict) self.assertTrue('id' in port) @@ -77,17 +75,17 @@ def test_create_port(self): def test_get_port(self): port_name = self.new_port_name + '_get' - networks = self.cloud.list_networks() + networks = self.operator_cloud.list_networks() if not networks: self.assertFalse('no sensible network available') - port = self.cloud.create_port( + port = self.operator_cloud.create_port( network_id=networks[0]['id'], name=port_name) self.assertIsInstance(port, dict) self.assertTrue('id' in port) self.assertEqual(port.get('name'), port_name) - updated_port = self.cloud.get_port(name_or_id=port['id']) + updated_port = self.operator_cloud.get_port(name_or_id=port['id']) # extra_dhcp_opts is added later by Neutron... if 'extra_dhcp_opts' in updated_port and 'extra_dhcp_opts' not in port: del updated_port['extra_dhcp_opts'] @@ -97,39 +95,39 @@ def test_update_port(self): port_name = self.new_port_name + '_update' new_port_name = port_name + '_new' - networks = self.cloud.list_networks() + networks = self.operator_cloud.list_networks() if not networks: self.assertFalse('no sensible network available') - self.cloud.create_port( + self.operator_cloud.create_port( network_id=networks[0]['id'], name=port_name) - port = self.cloud.update_port(name_or_id=port_name, - name=new_port_name) + port = self.operator_cloud.update_port( + name_or_id=port_name, name=new_port_name) self.assertIsInstance(port, dict) self.assertEqual(port.get('name'), new_port_name) - updated_port = self.cloud.get_port(name_or_id=port['id']) + updated_port = self.operator_cloud.get_port(name_or_id=port['id']) self.assertEqual(port.get('name'), new_port_name) self.assertEqual(port, updated_port) def test_delete_port(self): port_name = self.new_port_name + '_delete' - networks = self.cloud.list_networks() + networks = self.operator_cloud.list_networks() if not networks: self.assertFalse('no sensible network available') - port = self.cloud.create_port( + port = self.operator_cloud.create_port( network_id=networks[0]['id'], name=port_name) self.assertIsInstance(port, dict) self.assertTrue('id' in port) self.assertEqual(port.get('name'), port_name) - updated_port = self.cloud.get_port(name_or_id=port['id']) + updated_port = self.operator_cloud.get_port(name_or_id=port['id']) self.assertIsNotNone(updated_port) - self.cloud.delete_port(name_or_id=port_name) + self.operator_cloud.delete_port(name_or_id=port_name) - updated_port = self.cloud.get_port(name_or_id=port['id']) + updated_port = self.operator_cloud.get_port(name_or_id=port['id']) self.assertIsNone(updated_port) diff --git a/shade/tests/functional/test_range_search.py b/shade/tests/functional/test_range_search.py index eb31b7984..032671714 100644 --- a/shade/tests/functional/test_range_search.py +++ b/shade/tests/functional/test_range_search.py @@ -15,46 +15,42 @@ # limitations under the License. -import shade from shade import exc -from shade.tests import base +from shade.tests.functional import base -class TestRangeSearch(base.TestCase): - - def setUp(self): - super(TestRangeSearch, self).setUp() - self.cloud = shade.openstack_cloud(cloud='devstack') +class TestRangeSearch(base.BaseFunctionalTestCase): def test_range_search_bad_range(self): - flavors = self.cloud.list_flavors() - self.assertRaises(exc.OpenStackCloudException, - self.cloud.range_search, flavors, {"ram": "<1a0"}) + flavors = self.demo_cloud.list_flavors() + self.assertRaises( + exc.OpenStackCloudException, + self.demo_cloud.range_search, flavors, {"ram": "<1a0"}) def test_range_search_exact(self): - flavors = self.cloud.list_flavors() - result = self.cloud.range_search(flavors, {"ram": "4096"}) + flavors = self.demo_cloud.list_flavors() + result = self.demo_cloud.range_search(flavors, {"ram": "4096"}) self.assertIsInstance(result, list) self.assertEqual(1, len(result)) self.assertEqual("m1.medium", result[0]['name']) def test_range_search_min(self): - flavors = self.cloud.list_flavors() - result = self.cloud.range_search(flavors, {"ram": "MIN"}) + flavors = self.demo_cloud.list_flavors() + result = self.demo_cloud.range_search(flavors, {"ram": "MIN"}) self.assertIsInstance(result, list) self.assertEqual(1, len(result)) self.assertEqual("m1.tiny", result[0]['name']) def test_range_search_max(self): - flavors = self.cloud.list_flavors() - result = self.cloud.range_search(flavors, {"ram": "MAX"}) + flavors = self.demo_cloud.list_flavors() + result = self.demo_cloud.range_search(flavors, {"ram": "MAX"}) self.assertIsInstance(result, list) self.assertEqual(1, len(result)) self.assertEqual("m1.xlarge", result[0]['name']) def test_range_search_lt(self): - flavors = self.cloud.list_flavors() - result = self.cloud.range_search(flavors, {"ram": "<4096"}) + flavors = self.demo_cloud.list_flavors() + result = self.demo_cloud.range_search(flavors, {"ram": "<4096"}) self.assertIsInstance(result, list) self.assertEqual(2, len(result)) flavor_names = [r['name'] for r in result] @@ -62,8 +58,8 @@ def test_range_search_lt(self): self.assertIn("m1.small", flavor_names) def test_range_search_gt(self): - flavors = self.cloud.list_flavors() - result = self.cloud.range_search(flavors, {"ram": ">4096"}) + flavors = self.demo_cloud.list_flavors() + result = self.demo_cloud.range_search(flavors, {"ram": ">4096"}) self.assertIsInstance(result, list) self.assertEqual(2, len(result)) flavor_names = [r['name'] for r in result] @@ -71,8 +67,8 @@ def test_range_search_gt(self): self.assertIn("m1.xlarge", flavor_names) def test_range_search_le(self): - flavors = self.cloud.list_flavors() - result = self.cloud.range_search(flavors, {"ram": "<=4096"}) + flavors = self.demo_cloud.list_flavors() + result = self.demo_cloud.range_search(flavors, {"ram": "<=4096"}) self.assertIsInstance(result, list) self.assertEqual(3, len(result)) flavor_names = [r['name'] for r in result] @@ -81,8 +77,8 @@ def test_range_search_le(self): self.assertIn("m1.medium", flavor_names) def test_range_search_ge(self): - flavors = self.cloud.list_flavors() - result = self.cloud.range_search(flavors, {"ram": ">=4096"}) + flavors = self.demo_cloud.list_flavors() + result = self.demo_cloud.range_search(flavors, {"ram": ">=4096"}) self.assertIsInstance(result, list) self.assertEqual(3, len(result)) flavor_names = [r['name'] for r in result] @@ -91,17 +87,17 @@ def test_range_search_ge(self): self.assertIn("m1.xlarge", flavor_names) def test_range_search_multi_1(self): - flavors = self.cloud.list_flavors() - result = self.cloud.range_search(flavors, - {"ram": "MIN", "vcpus": "MIN"}) + flavors = self.demo_cloud.list_flavors() + result = self.demo_cloud.range_search( + flavors, {"ram": "MIN", "vcpus": "MIN"}) self.assertIsInstance(result, list) self.assertEqual(1, len(result)) self.assertEqual("m1.tiny", result[0]['name']) def test_range_search_multi_2(self): - flavors = self.cloud.list_flavors() - result = self.cloud.range_search(flavors, - {"ram": "<8192", "vcpus": "MIN"}) + flavors = self.demo_cloud.list_flavors() + result = self.demo_cloud.range_search( + flavors, {"ram": "<8192", "vcpus": "MIN"}) self.assertIsInstance(result, list) self.assertEqual(2, len(result)) flavor_names = [r['name'] for r in result] @@ -110,9 +106,9 @@ def test_range_search_multi_2(self): self.assertIn("m1.small", flavor_names) def test_range_search_multi_3(self): - flavors = self.cloud.list_flavors() - result = self.cloud.range_search(flavors, - {"ram": ">=4096", "vcpus": "<6"}) + flavors = self.demo_cloud.list_flavors() + result = self.demo_cloud.range_search( + flavors, {"ram": ">=4096", "vcpus": "<6"}) self.assertIsInstance(result, list) self.assertEqual(2, len(result)) flavor_names = [r['name'] for r in result] @@ -120,9 +116,9 @@ def test_range_search_multi_3(self): self.assertIn("m1.large", flavor_names) def test_range_search_multi_4(self): - flavors = self.cloud.list_flavors() - result = self.cloud.range_search(flavors, - {"ram": ">=4096", "vcpus": "MAX"}) + flavors = self.demo_cloud.list_flavors() + result = self.demo_cloud.range_search( + flavors, {"ram": ">=4096", "vcpus": "MAX"}) self.assertIsInstance(result, list) self.assertEqual(1, len(result)) # This is the only result that should have max vcpu diff --git a/shade/tests/functional/test_router.py b/shade/tests/functional/test_router.py index ca2c7fa3d..49aad9a74 100644 --- a/shade/tests/functional/test_router.py +++ b/shade/tests/functional/test_router.py @@ -21,9 +21,8 @@ import ipaddress -from shade import openstack_cloud from shade.exc import OpenStackCloudException -from shade.tests import base +from shade.tests.functional import base EXPECTED_TOPLEVEL_FIELDS = ( @@ -34,11 +33,10 @@ EXPECTED_GW_INFO_FIELDS = ('network_id', 'enable_snat', 'external_fixed_ips') -class TestRouter(base.TestCase): +class TestRouter(base.BaseFunctionalTestCase): def setUp(self): super(TestRouter, self).setUp() - self.cloud = openstack_cloud(cloud='devstack-admin') - if not self.cloud.has_service('network'): + if not self.operator_cloud.has_service('network'): self.skipTest('Network service not supported by cloud') self.router_prefix = self.getUniqueString('router') @@ -52,10 +50,10 @@ def setUp(self): def _cleanup_routers(self): exception_list = list() - for router in self.cloud.list_routers(): + for router in self.operator_cloud.list_routers(): if router['name'].startswith(self.router_prefix): try: - self.cloud.delete_router(router['name']) + self.operator_cloud.delete_router(router['name']) except Exception as e: exception_list.append(str(e)) continue @@ -65,10 +63,10 @@ def _cleanup_routers(self): def _cleanup_networks(self): exception_list = list() - for network in self.cloud.list_networks(): + for network in self.operator_cloud.list_networks(): if network['name'].startswith(self.network_prefix): try: - self.cloud.delete_network(network['name']) + self.operator_cloud.delete_network(network['name']) except Exception as e: exception_list.append(str(e)) continue @@ -78,10 +76,10 @@ def _cleanup_networks(self): def _cleanup_subnets(self): exception_list = list() - for subnet in self.cloud.list_subnets(): + for subnet in self.operator_cloud.list_subnets(): if subnet['name'].startswith(self.subnet_prefix): try: - self.cloud.delete_subnet(subnet['id']) + self.operator_cloud.delete_subnet(subnet['id']) except Exception as e: exception_list.append(str(e)) continue @@ -91,10 +89,11 @@ def _cleanup_subnets(self): def test_create_router_basic(self): net1_name = self.network_prefix + '_net1' - net1 = self.cloud.create_network(name=net1_name, external=True) + net1 = self.operator_cloud.create_network( + name=net1_name, external=True) router_name = self.router_prefix + '_create_basic' - router = self.cloud.create_router( + router = self.operator_cloud.create_router( name=router_name, admin_state_up=True, ext_gateway_net_id=net1['id'], @@ -120,8 +119,9 @@ def _create_and_verify_advanced_router(self, # is using different resources to prevent race conditions. net1_name = self.network_prefix + '_net1' sub1_name = self.subnet_prefix + '_sub1' - net1 = self.cloud.create_network(name=net1_name, external=True) - sub1 = self.cloud.create_subnet( + net1 = self.operator_cloud.create_network( + name=net1_name, external=True) + sub1 = self.operator_cloud.create_subnet( net1['id'], external_cidr, subnet_name=sub1_name, gateway_ip=external_gateway_ip ) @@ -130,7 +130,7 @@ def _create_and_verify_advanced_router(self, last_ip = str(list(ip_net.hosts())[-1]) router_name = self.router_prefix + '_create_advanced' - router = self.cloud.create_router( + router = self.operator_cloud.create_router( name=router_name, admin_state_up=False, ext_gateway_net_id=net1['id'], @@ -171,15 +171,17 @@ def test_add_remove_router_interface(self): external_cidr='10.3.3.0/24') net_name = self.network_prefix + '_intnet1' sub_name = self.subnet_prefix + '_intsub1' - net = self.cloud.create_network(name=net_name) - sub = self.cloud.create_subnet( + net = self.operator_cloud.create_network(name=net_name) + sub = self.operator_cloud.create_subnet( net['id'], '10.4.4.0/24', subnet_name=sub_name, gateway_ip='10.4.4.1' ) - iface = self.cloud.add_router_interface(router, subnet_id=sub['id']) + iface = self.operator_cloud.add_router_interface( + router, subnet_id=sub['id']) self.assertIsNone( - self.cloud.remove_router_interface(router, subnet_id=sub['id']) + self.operator_cloud.remove_router_interface( + router, subnet_id=sub['id']) ) # Test return values *after* the interface is detached so the @@ -195,20 +197,22 @@ def test_list_router_interfaces(self): external_cidr='10.5.5.0/24') net_name = self.network_prefix + '_intnet1' sub_name = self.subnet_prefix + '_intsub1' - net = self.cloud.create_network(name=net_name) - sub = self.cloud.create_subnet( + net = self.operator_cloud.create_network(name=net_name) + sub = self.operator_cloud.create_subnet( net['id'], '10.6.6.0/24', subnet_name=sub_name, gateway_ip='10.6.6.1' ) - iface = self.cloud.add_router_interface(router, subnet_id=sub['id']) - all_ifaces = self.cloud.list_router_interfaces(router) - int_ifaces = self.cloud.list_router_interfaces( + iface = self.operator_cloud.add_router_interface( + router, subnet_id=sub['id']) + all_ifaces = self.operator_cloud.list_router_interfaces(router) + int_ifaces = self.operator_cloud.list_router_interfaces( router, interface_type='internal') - ext_ifaces = self.cloud.list_router_interfaces( + ext_ifaces = self.operator_cloud.list_router_interfaces( router, interface_type='external') self.assertIsNone( - self.cloud.remove_router_interface(router, subnet_id=sub['id']) + self.operator_cloud.remove_router_interface( + router, subnet_id=sub['id']) ) # Test return values *after* the interface is detached so the @@ -228,7 +232,8 @@ def test_update_router_name(self): external_cidr='10.7.7.0/24') new_name = self.router_prefix + '_update_name' - updated = self.cloud.update_router(router['id'], name=new_name) + updated = self.operator_cloud.update_router( + router['id'], name=new_name) self.assertIsNotNone(updated) for field in EXPECTED_TOPLEVEL_FIELDS: @@ -247,8 +252,8 @@ def test_update_router_admin_state(self): router = self._create_and_verify_advanced_router( external_cidr='10.8.8.0/24') - updated = self.cloud.update_router(router['id'], - admin_state_up=True) + updated = self.operator_cloud.update_router( + router['id'], admin_state_up=True) self.assertIsNotNone(updated) for field in EXPECTED_TOPLEVEL_FIELDS: @@ -272,12 +277,12 @@ def test_update_router_ext_gw_info(self): # create a new subnet existing_net_id = router['external_gateway_info']['network_id'] sub_name = self.subnet_prefix + '_update' - sub = self.cloud.create_subnet( + sub = self.operator_cloud.create_subnet( existing_net_id, '10.10.10.0/24', subnet_name=sub_name, gateway_ip='10.10.10.1' ) - updated = self.cloud.update_router( + updated = self.operator_cloud.update_router( router['id'], ext_gateway_net_id=existing_net_id, ext_fixed_ips=[ diff --git a/shade/tests/functional/test_services.py b/shade/tests/functional/test_services.py index 87d050293..bd43deb67 100644 --- a/shade/tests/functional/test_services.py +++ b/shade/tests/functional/test_services.py @@ -24,19 +24,17 @@ import string import random -from shade import operator_cloud from shade.exc import OpenStackCloudException from shade.exc import OpenStackCloudUnavailableFeature -from shade.tests import base +from shade.tests.functional import base -class TestServices(base.TestCase): +class TestServices(base.BaseFunctionalTestCase): service_attributes = ['id', 'name', 'type', 'description'] def setUp(self): super(TestServices, self).setUp() - self.operator_cloud = operator_cloud(cloud='devstack-admin') # Generate a random name for services in this test self.new_service_name = 'test_' + ''.join( diff --git a/shade/tests/functional/test_users.py b/shade/tests/functional/test_users.py index 72cb38e11..47ea68930 100644 --- a/shade/tests/functional/test_users.py +++ b/shade/tests/functional/test_users.py @@ -21,22 +21,21 @@ from shade import operator_cloud from shade import OpenStackCloudException -from shade.tests import base +from shade.tests.functional import base -class TestUsers(base.TestCase): +class TestUsers(base.BaseFunctionalTestCase): def setUp(self): super(TestUsers, self).setUp() - self.cloud = operator_cloud(cloud='devstack-admin') self.user_prefix = self.getUniqueString('user') self.addCleanup(self._cleanup_users) def _cleanup_users(self): exception_list = list() - for user in self.cloud.list_users(): + for user in self.operator_cloud.list_users(): if user['name'].startswith(self.user_prefix): try: - self.cloud.delete_user(user['id']) + self.operator_cloud.delete_user(user['id']) except Exception as e: exception_list.append(str(e)) continue @@ -46,26 +45,26 @@ def _cleanup_users(self): def _create_user(self, **kwargs): domain_id = None - identity_version = self.cloud.cloud_config.get_api_version('identity') - if identity_version not in ('2', '2.0'): - domain = self.cloud.get_domain('default') + i_ver = self.operator_cloud.cloud_config.get_api_version('identity') + if i_ver not in ('2', '2.0'): + domain = self.operator_cloud.get_domain('default') domain_id = domain['id'] - return self.cloud.create_user(domain_id=domain_id, **kwargs) + return self.operator_cloud.create_user(domain_id=domain_id, **kwargs) def test_list_users(self): - users = self.cloud.list_users() + users = self.operator_cloud.list_users() self.assertIsNotNone(users) self.assertNotEqual([], users) def test_get_user(self): - user = self.cloud.get_user('admin') + user = self.operator_cloud.get_user('admin') self.assertIsNotNone(user) self.assertIn('id', user) self.assertIn('name', user) self.assertEqual('admin', user['name']) def test_search_users(self): - users = self.cloud.search_users(filters={'enabled': True}) + users = self.operator_cloud.search_users(filters={'enabled': True}) self.assertIsNotNone(users) def test_create_user(self): @@ -82,10 +81,10 @@ def test_delete_user(self): user_email = 'nobody@nowhere.com' user = self._create_user(name=user_name, email=user_email) self.assertIsNotNone(user) - self.assertTrue(self.cloud.delete_user(user['id'])) + self.assertTrue(self.operator_cloud.delete_user(user['id'])) def test_delete_user_not_found(self): - self.assertFalse(self.cloud.delete_user('does_not_exist')) + self.assertFalse(self.operator_cloud.delete_user('does_not_exist')) def test_update_user(self): user_name = self.user_prefix + '_updatev3' @@ -96,12 +95,13 @@ def test_update_user(self): # Pass some keystone v3 params. This should work no matter which # version of keystone we are testing against. - new_user = self.cloud.update_user(user['id'], - name=user_name + '2', - email='somebody@nowhere.com', - enabled=False, - password='secret', - description='') + new_user = self.operator_cloud.update_user( + user['id'], + name=user_name + '2', + email='somebody@nowhere.com', + enabled=False, + password='secret', + description='') self.assertIsNotNone(new_user) self.assertEqual(user['id'], new_user['id']) self.assertEqual(user_name + '2', new_user['name']) @@ -118,8 +118,8 @@ def test_update_user_password(self): self.assertTrue(user['enabled']) # This should work for both v2 and v3 - new_user = self.cloud.update_user(user['id'], - password='new_secret') + new_user = self.operator_cloud.update_user( + user['id'], password='new_secret') self.assertIsNotNone(new_user) self.assertEqual(user['id'], new_user['id']) self.assertEqual(user_name, new_user['name']) @@ -127,17 +127,18 @@ def test_update_user_password(self): self.assertTrue(new_user['enabled']) self.assertIsNotNone(operator_cloud( username=user_name, password='new_secret', - auth_url=self.cloud.auth['auth_url']).keystone_client) + auth_url=self.operator_cloud.auth['auth_url']).keystone_client) def test_users_and_groups(self): - if self.cloud.cloud_config.get_api_version('identity') in ('2', '2.0'): + i_ver = self.operator_cloud.cloud_config.get_api_version('identity') + if i_ver in ('2', '2.0'): self.skipTest('Identity service does not support groups') group_name = self.getUniqueString('group') - self.addCleanup(self.cloud.delete_group, group_name) + self.addCleanup(self.operator_cloud.delete_group, group_name) # Create a group - group = self.cloud.create_group(group_name, 'test group') + group = self.operator_cloud.create_group(group_name, 'test group') self.assertIsNotNone(group) # Create a user @@ -147,9 +148,11 @@ def test_users_and_groups(self): self.assertIsNotNone(user) # Add the user to the group - self.cloud.add_user_to_group(user_name, group_name) - self.assertTrue(self.cloud.is_user_in_group(user_name, group_name)) + self.operator_cloud.add_user_to_group(user_name, group_name) + self.assertTrue( + self.operator_cloud.is_user_in_group(user_name, group_name)) # Remove them from the group - self.cloud.remove_user_from_group(user_name, group_name) - self.assertFalse(self.cloud.is_user_in_group(user_name, group_name)) + self.operator_cloud.remove_user_from_group(user_name, group_name) + self.assertFalse( + self.operator_cloud.is_user_in_group(user_name, group_name)) diff --git a/shade/tests/functional/test_volume.py b/shade/tests/functional/test_volume.py index bcf2dfc04..d040bb720 100644 --- a/shade/tests/functional/test_volume.py +++ b/shade/tests/functional/test_volume.py @@ -21,16 +21,14 @@ from testtools import content -from shade import openstack_cloud -from shade.tests import base +from shade.tests.functional import base -class TestVolume(base.TestCase): +class TestVolume(base.BaseFunctionalTestCase): def setUp(self): super(TestVolume, self).setUp() - self.cloud = openstack_cloud(cloud='devstack') - if not self.cloud.has_service('volume'): + if not self.demo_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') def test_volumes(self): @@ -39,29 +37,32 @@ def test_volumes(self): snapshot_name = self.getUniqueString() self.addDetail('volume', content.text_content(volume_name)) self.addCleanup(self.cleanup, volume_name, snapshot_name) - volume = self.cloud.create_volume(display_name=volume_name, size=1) - snapshot = self.cloud.create_volume_snapshot( + volume = self.demo_cloud.create_volume( + display_name=volume_name, size=1) + snapshot = self.demo_cloud.create_volume_snapshot( volume['id'], display_name=snapshot_name ) - volume_ids = [v['id'] for v in self.cloud.list_volumes()] + volume_ids = [v['id'] for v in self.demo_cloud.list_volumes()] self.assertIn(volume['id'], volume_ids) - snapshot_ids = [s['id'] for s in self.cloud.list_volume_snapshots()] + snapshot_list = self.demo_cloud.list_volume_snapshots() + snapshot_ids = [s['id'] for s in snapshot_list] self.assertIn(snapshot['id'], snapshot_ids) - ret_snapshot = self.cloud.get_volume_snapshot_by_id(snapshot['id']) + ret_snapshot = self.demo_cloud.get_volume_snapshot_by_id( + snapshot['id']) self.assertEqual(snapshot['id'], ret_snapshot['id']) - self.cloud.delete_volume_snapshot(snapshot_name, wait=True) - self.cloud.delete_volume(volume_name, wait=True) + self.demo_cloud.delete_volume_snapshot(snapshot_name, wait=True) + self.demo_cloud.delete_volume(volume_name, wait=True) def cleanup(self, volume_name, snapshot_name): - volume = self.cloud.get_volume(volume_name) - snapshot = self.cloud.get_volume_snapshot(snapshot_name) + volume = self.demo_cloud.get_volume(volume_name) + snapshot = self.demo_cloud.get_volume_snapshot(snapshot_name) # Need to delete snapshots before volumes if snapshot: - self.cloud.delete_volume_snapshot(snapshot_name) + self.demo_cloud.delete_volume_snapshot(snapshot_name) if volume: - self.cloud.delete_volume(volume_name) + self.demo_cloud.delete_volume(volume_name) From e4ba956a6b9956d566c249687a68cb97999e49ec Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 21 Mar 2016 10:23:47 -0500 Subject: [PATCH 0846/3836] Deal with is_public and ephemeral in normalize_flavors There are two attributes that are provided by extensions that we should deal with in our normalization function. novaclient provides helper methods to make the names less suck. We should also include the suck versions of the names though, as they are currently public parts of the API and people might be confused if they're missing. Change-Id: Ia2f1baf1307ec97fb0166cbb4a633852b461770b --- shade/_utils.py | 8 ++++++++ shade/tests/functional/test_flavor.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/shade/_utils.py b/shade/_utils.py index 7c6eaaf02..66f3138cc 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -477,6 +477,14 @@ def normalize_flavors(flavors): flavor.pop('human_id', None) if 'extra_specs' not in flavor: flavor['extra_specs'] = {} + ephemeral = flavor.pop('OS-FLV-EXT-DATA:ephemeral', 0) + is_public = flavor.pop('os-flavor-access:is_public', True) + # Make sure both the extension version and a sane version are present + flavor['OS-FLV-EXT-DATA:ephemeral'] = ephemeral + flavor['ephemeral'] = ephemeral + flavor['os-flavor-access:is_public'] = is_public + flavor['is_public'] = is_public + return flavors diff --git a/shade/tests/functional/test_flavor.py b/shade/tests/functional/test_flavor.py index 96ec22d57..90a21ab96 100644 --- a/shade/tests/functional/test_flavor.py +++ b/shade/tests/functional/test_flavor.py @@ -66,6 +66,14 @@ def test_create_flavor(self): self.assertIn('extra_specs', flavor) self.assertEqual({}, flavor['extra_specs']) + # We should also always have ephemeral and public attributes + self.assertIn('ephemeral', flavor) + self.assertIn('OS-FLV-EXT-DATA:ephemeral', flavor) + self.assertEqual(5, flavor['ephemeral']) + self.assertIn('is_public', flavor) + self.assertIn('os-flavor-access:is_public', flavor) + self.assertEqual(True, flavor['is_public']) + for key in flavor_kwargs.keys(): self.assertIn(key, flavor) for key, value in flavor_kwargs.items(): From e41dabb10ce09fca4d34cc22487f9f2dbe305177 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 22 Mar 2016 08:33:30 -0400 Subject: [PATCH 0847/3836] Add new tasks to os_port playbook The allowed_address_pairs and extra_dhcp_opts parameters were broken in Ansible. We should make sure those are tested. NOTE: This will not pass tests until this merges and gets released: https://github.com/ansible/ansible-modules-core/pull/3303 Change-Id: I51ceb5305109f6f1c4f1890903f117a9ed250d56 --- shade/tests/ansible/roles/port/tasks/main.yml | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/shade/tests/ansible/roles/port/tasks/main.yml b/shade/tests/ansible/roles/port/tasks/main.yml index 5f3040be9..05ce1e20f 100644 --- a/shade/tests/ansible/roles/port/tasks/main.yml +++ b/shade/tests/ansible/roles/port/tasks/main.yml @@ -60,6 +60,28 @@ state: absent name: "{{ port_name }}" +- name: Create port (with allowed_address_pairs and extra_dhcp_opts) + os_port: + cloud: "{{ cloud }}" + state: present + name: "{{ port_name }}" + network: "{{ network_name }}" + no_security_groups: True + allowed_address_pairs: + - ip_address: 10.6.7.0/24 + extra_dhcp_opts: + - opt_name: "bootfile-name" + opt_value: "testfile.1" + register: port + +- debug: var=port + +- name: Delete port (with allowed_address_pairs and extra_dhcp_opts) + os_port: + cloud: "{{ cloud }}" + state: absent + name: "{{ port_name }}" + - name: Delete security group os_security_group: cloud: "{{ cloud }}" From abe2505f70e5ce65f37e488a68498af04967c701 Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Tue, 22 Mar 2016 13:34:09 +0000 Subject: [PATCH 0848/3836] Fix search_users docstring The method would return a list of users, not roles Change-Id: Ia9dd22baa2b6fc765345ce97e144a606ac450950 --- shade/openstackcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 647c698cd..534460bc6 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -526,7 +526,7 @@ def search_users(self, name_or_id=None, filters=None): :param string name: user name or id. :param dict filters: a dict containing additional filters to use. - :returns: a list of dict containing the role description + :returns: a list of dict containing the users :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. From c6f02582e0102241c595e94521c4b5319289dc27 Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Tue, 22 Mar 2016 13:36:21 +0000 Subject: [PATCH 0849/3836] Fix search_projects docstring The method returns a list of projects, not roles Change-Id: I8ce4c8a5ab5fa6adbba4dd3f925e0b0528dae37d --- shade/openstackcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 647c698cd..6d59df908 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -435,7 +435,7 @@ def search_projects(self, name_or_id=None, filters=None): :param name: project name or id. :param filters: a dict containing additional filters to use. - :returns: a list of dict containing the role description + :returns: a list of dict containing the projects :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. From 8a7c609352643487aa5820df052bc66043e07808 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 21 Mar 2016 11:25:02 -0500 Subject: [PATCH 0850/3836] Use direct requests for flavor extra_specs set/unset Same with the get - we need to resort to direct REST to do set/unset. Change-Id: Iec5b66b05bbd63071c5558782de7a67253ae209c --- shade/_tasks.py | 18 ++++++++++ shade/operatorcloud.py | 60 +++++++++----------------------- shade/task_manager.py | 15 +++++++- shade/tests/unit/test_flavors.py | 26 +++++++------- 4 files changed, 62 insertions(+), 57 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index c4c58053c..f775e95db 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -95,6 +95,24 @@ def main(self, client): "/flavors/{id}/os-extra_specs".format(**self.args)) +class FlavorSetExtraSpecs(task_manager.RequestTask): + result_key = 'extra_specs' + + def main(self, client): + return client._compute_client.post( + "/flavors/{id}/os-extra_specs".format(**self.args), + json=self.args['json'] + ) + + +class FlavorUnsetExtraSpecs(task_manager.RequestTask): + + def main(self, client): + return client._compute_client.delete( + "/flavors/{id}/os-extra_specs/{key}".format(**self.args), + ) + + class FlavorCreate(task_manager.Task): def main(self, client): return client.nova_client.flavors.create(**self.args) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 66da61494..c8a090431 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -14,7 +14,6 @@ from ironicclient import client as ironic_client from ironicclient import exceptions as ironic_exceptions -from novaclient import exceptions as nova_exceptions from shade.exc import * # noqa from shade import openstackcloud @@ -1452,47 +1451,6 @@ def delete_flavor(self, name_or_id): return True - def _mod_flavor_specs(self, action, flavor_id, specs): - """Common method for modifying flavor extra specs. - - Nova (very sadly) doesn't expose this with a public API, so we - must get the actual flavor object and make a method call on it. - - Two separate try-except blocks are used because Nova can raise - a NotFound exception if FlavorGet() is given an invalid flavor ID, - or if the unset_keys() method of the flavor object is given an - invalid spec key. We need to be able to differentiate between these - actions, thus the separate blocks. - """ - try: - flavor = self.manager.submitTask( - _tasks.FlavorGet(flavor=flavor_id), raw=True - ) - except nova_exceptions.NotFound: - self.log.debug( - "Flavor ID {0} not found. " - "Cannot {1} extra specs.".format(flavor_id, action) - ) - raise OpenStackCloudResourceNotFound( - "Flavor ID {0} not found".format(flavor_id) - ) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Error getting flavor ID {0}: {1}".format(flavor_id, e) - ) - - try: - if action == 'set': - flavor.set_keys(specs) - elif action == 'unset': - flavor.unset_keys(specs) - except Exception as e: - raise OpenStackCloudException( - "Unable to {0} flavor specs: {1}".format(action, e) - ) - def set_flavor_specs(self, flavor_id, extra_specs): """Add extra specs to a flavor @@ -1502,7 +1460,14 @@ def set_flavor_specs(self, flavor_id, extra_specs): :raises: OpenStackCloudException on operation error. :raises: OpenStackCloudResourceNotFound if flavor ID is not found. """ - self._mod_flavor_specs('set', flavor_id, extra_specs) + try: + self.manager.submitTask( + _tasks.FlavorSetExtraSpecs( + id=flavor_id, json=dict(extra_specs=extra_specs))) + except Exception as e: + raise OpenStackCloudException( + "Unable to set flavor specs: {0}".format(str(e)) + ) def unset_flavor_specs(self, flavor_id, keys): """Delete extra specs from a flavor @@ -1513,7 +1478,14 @@ def unset_flavor_specs(self, flavor_id, keys): :raises: OpenStackCloudException on operation error. :raises: OpenStackCloudResourceNotFound if flavor ID is not found. """ - self._mod_flavor_specs('unset', flavor_id, keys) + for key in keys: + try: + self.manager.submitTask( + _tasks.FlavorUnsetExtraSpecs(id=flavor_id, key=key)) + except Exception as e: + raise OpenStackCloudException( + "Unable to delete flavor spec {0}: {0}".format( + key, str(e))) def _mod_flavor_access(self, action, flavor_id, project_id): """Common method for adding and removing flavor access diff --git a/shade/task_manager.py b/shade/task_manager.py index 4cd03a6bd..e903f6738 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -21,6 +21,7 @@ import types import keystoneauth1.exceptions +import simplejson import six from shade import _log @@ -100,6 +101,7 @@ def wait(self, raw): return self._result def run(self, client): + self._client = client try: # Retry one time if we get a retriable connection failure try: @@ -117,10 +119,21 @@ def run(self, client): class RequestTask(Task): + # It's totally legit for calls to not return things + result_key = None + # keystoneauth1 throws keystoneauth1.exceptions.http.HttpError on !200 def done(self, result): self._response = result - result_json = self._response.json() + + try: + result_json = self._response.json() + except (simplejson.scanner.JSONDecodeError, ValueError) as e: + result_json = self._response.text + self._client.log.debug( + 'Could not decode json in response: {e}'.format(e=str(e))) + self._client.log.debug(result_json) + if self.result_key: self._result = result_json[self.result_key] else: diff --git a/shade/tests/unit/test_flavors.py b/shade/tests/unit/test_flavors.py index 820a7a055..f16e0e1c3 100644 --- a/shade/tests/unit/test_flavors.py +++ b/shade/tests/unit/test_flavors.py @@ -69,23 +69,25 @@ def test_list_flavors(self, mock_nova): self.op_cloud.list_flavors() mock_nova.flavors.list.assert_called_once_with(is_public=None) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_set_flavor_specs(self, mock_nova): - flavor = mock.Mock(id=1, name='orange') - mock_nova.flavors.get.return_value = flavor + @mock.patch.object(shade.OpenStackCloud, '_compute_client') + def test_set_flavor_specs(self, mock_compute): extra_specs = dict(key1='value1') self.op_cloud.set_flavor_specs(1, extra_specs) - mock_nova.flavors.get.assert_called_once_with(flavor=1) - flavor.set_keys.assert_called_once_with(extra_specs) + mock_compute.post.assert_called_once_with( + '/flavors/{id}/os-extra_specs'.format(id=1), + json=dict(extra_specs=extra_specs)) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_unset_flavor_specs(self, mock_nova): - flavor = mock.Mock(id=1, name='orange') - mock_nova.flavors.get.return_value = flavor + @mock.patch.object(shade.OpenStackCloud, '_compute_client') + def test_unset_flavor_specs(self, mock_compute): keys = ['key1', 'key2'] self.op_cloud.unset_flavor_specs(1, keys) - mock_nova.flavors.get.assert_called_once_with(flavor=1) - flavor.unset_keys.assert_called_once_with(keys) + api_spec = '/flavors/{id}/os-extra_specs/{key}' + self.assertEqual( + mock_compute.delete.call_args_list[0], + mock.call(api_spec.format(id=1, key='key1'))) + self.assertEqual( + mock_compute.delete.call_args_list[1], + mock.call(api_spec.format(id=1, key='key2'))) @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_add_flavor_access(self, mock_nova): From 0870ffe9491f3580bc38ae79578275f50c829ffd Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 22 Mar 2016 08:25:43 -0500 Subject: [PATCH 0851/3836] Fix race condition in deleting volumes We check at the top of the delete_volume call for existence of the volume, but then time passes and it's possible the volume does not, in fact, exist. Catch the 404 and move on with our life. Change-Id: I0873a238e519b08c09c2a17dba331401f7fc4bfb --- shade/openstackcloud.py | 11 +++++++++-- shade/tests/unit/test_volume.py | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index a7f94c5cd..9dbcd7ba4 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -24,6 +24,7 @@ import requestsexceptions import cinderclient.client +import cinderclient.exceptions as cinder_exceptions import glanceclient import glanceclient.exc import heatclient.client @@ -2429,8 +2430,14 @@ def delete_volume(self, name_or_id=None, wait=True, timeout=None): return False with _utils.shade_exceptions("Error in deleting volume"): - self.manager.submitTask( - _tasks.VolumeDelete(volume=volume['id'])) + try: + self.manager.submitTask( + _tasks.VolumeDelete(volume=volume['id'])) + except cinder_exceptions.NotFound: + self.log.debug( + "Volume {id} not found when deleting. Ignoring.".format( + id=volume['id'])) + return False self.list_volumes.invalidate(self) if wait: diff --git a/shade/tests/unit/test_volume.py b/shade/tests/unit/test_volume.py index 01cb0c316..6d6383bee 100644 --- a/shade/tests/unit/test_volume.py +++ b/shade/tests/unit/test_volume.py @@ -13,6 +13,7 @@ # under the License. +import cinderclient.exceptions as cinder_exc import mock import testtools @@ -188,3 +189,20 @@ def test_detach_volume_wait_error(self, mock_nova, mock_get): "Error in detaching volume %s" % errored_volume['id'] ): self.cloud.detach_volume(server, volume) + + @mock.patch.object(shade.OpenStackCloud, 'get_volume') + @mock.patch.object(shade.OpenStackCloud, 'cinder_client') + def test_delete_volume_deletes(self, mock_cinder, mock_get): + volume = dict(id='volume001', status='attached') + mock_get.side_effect = iter([volume, None]) + + self.assertTrue(self.cloud.delete_volume(volume['id'])) + + @mock.patch.object(shade.OpenStackCloud, 'get_volume') + @mock.patch.object(shade.OpenStackCloud, 'cinder_client') + def test_delete_volume_gone_away(self, mock_cinder, mock_get): + volume = dict(id='volume001', status='attached') + mock_get.side_effect = iter([volume]) + mock_cinder.volumes.delete.side_effect = cinder_exc.NotFound('N/A') + + self.assertFalse(self.cloud.delete_volume(volume['id'])) From 35e6af88904508c6e4e33aab4af514c41e420c07 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 23 Mar 2016 08:52:43 -0500 Subject: [PATCH 0852/3836] Always do network interface introspection We split the function that figures out what your IP is from the more API heavy functions so that we could always apply it - and then we forgot to always apply it. Change-Id: Ic670a05ed5165be144912642f5ecae4ca0bc94c2 --- shade/inventory.py | 10 ++-------- shade/openstackcloud.py | 5 ++++- shade/tests/fakes.py | 5 ++++- shade/tests/unit/test_inventory.py | 10 ++++------ shade/tests/unit/test_shade.py | 3 +++ 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/shade/inventory.py b/shade/inventory.py index 3fcb0f031..101682a18 100644 --- a/shade/inventory.py +++ b/shade/inventory.py @@ -18,7 +18,6 @@ import shade from shade import _utils -from shade import meta class OpenStackInventory(object): @@ -65,13 +64,8 @@ def list_hosts(self, expand=True, fail_on_cloud_config=True): for cloud in self.clouds: try: # Cycle on servers - for server in cloud.list_servers(): - - if expand: - server_vars = cloud.get_openstack_vars(server) - else: - server_vars = meta.add_server_interfaces(cloud, server) - hostvars.append(server_vars) + for server in cloud.list_servers(detailed=expand): + hostvars.append(server) except shade.OpenStackCloudException: # Don't fail on one particular cloud as others may work if fail_on_cloud_config: diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index d0a1521e6..6f7611185 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1256,7 +1256,10 @@ def _list_servers(self, detailed=False): for server in servers ] else: - return servers + return [ + meta.add_server_interfaces(self, server) + for server in servers + ] @_utils.cache_on_arguments(should_cache_fn=_no_pending_images) def list_images(self, filter_deleted=True): diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 1f50c3584..19ff69f40 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -90,7 +90,10 @@ def __init__( self.id = id self.name = name self.status = status - self.addresses = addresses + if not addresses: + self.addresses = {} + else: + self.addresses = addresses if not flavor: flavor = {} self.flavor = flavor diff --git a/shade/tests/unit/test_inventory.py b/shade/tests/unit/test_inventory.py index 205d6721a..02e0228ca 100644 --- a/shade/tests/unit/test_inventory.py +++ b/shade/tests/unit/test_inventory.py @@ -92,14 +92,13 @@ def test_list_hosts(self, mock_cloud, mock_config): ret = inv.list_hosts() - inv.clouds[0].list_servers.assert_called_once_with() - inv.clouds[0].get_openstack_vars.assert_called_once_with(server) + inv.clouds[0].list_servers.assert_called_once_with(detailed=True) + self.assertFalse(inv.clouds[0].get_openstack_vars.called) self.assertEqual([server], ret) @mock.patch("os_client_config.config.OpenStackConfig") - @mock.patch("shade.meta.add_server_interfaces") @mock.patch("shade.OpenStackCloud") - def test_list_hosts_no_detail(self, mock_cloud, mock_add, mock_config): + def test_list_hosts_no_detail(self, mock_cloud, mock_config): mock_config.return_value.get_all_clouds.return_value = [{}] inv = inventory.OpenStackInventory() @@ -114,9 +113,8 @@ def test_list_hosts_no_detail(self, mock_cloud, mock_add, mock_config): inv.list_hosts(expand=False) - inv.clouds[0].list_servers.assert_called_once_with() + inv.clouds[0].list_servers.assert_called_once_with(detailed=False) self.assertFalse(inv.clouds[0].get_openstack_vars.called) - mock_add.assert_called_once_with(inv.clouds[0], server) @mock.patch("os_client_config.config.OpenStackConfig") @mock.patch("shade.OpenStackCloud") diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 2a915a59b..137798df3 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -574,6 +574,9 @@ def test_list_servers(self, mock_serverlist): munch.Munch({'name': 'testserver', 'id': '1', 'flavor': {}, + 'addresses': {}, + 'accessIPv4': '', + 'accessIPv6': '', 'image': ''}) ] From b5e3cd3307073d7a87514c9ce3de1692c5307bc3 Mon Sep 17 00:00:00 2001 From: Jon Schlueter Date: Wed, 23 Mar 2016 15:59:40 -0400 Subject: [PATCH 0853/3836] Use OpenStackCloudException when _delete_server() raises OpenStackException does not exist Change-Id: I9df905d6630dc7f0d1b3aec6e83c3c2b4d32dc7f --- shade/openstackcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 1073a0150..a9732d050 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3704,7 +3704,7 @@ def _delete_server( ips = self.search_floating_ips(filters={ 'floating_ip_address': floating_ip}) if len(ips) != 1: - raise OpenStackException( + raise OpenStackCloudException( "Tried to delete floating ip {floating_ip}" " associated with server {id} but there was" " an error finding it. Something is exceptionally" From bd8be72f4ef3baa23c1950c8a2667e2e0b7ce883 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 24 Mar 2016 12:12:53 -0500 Subject: [PATCH 0854/3836] Add default value to wait parameter Adding the wait param without a default broke the api for things calling it the old way. Change-Id: I75bbec19f06a0f85bc9cb1fbe4d4c6560dadba49 --- shade/task_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/task_manager.py b/shade/task_manager.py index e903f6738..d2dce731d 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -72,7 +72,7 @@ def exception(self, e, tb): self._traceback = tb self._finished.set() - def wait(self, raw): + def wait(self, raw=False): self._finished.wait() if self._exception: From 20451c32d602a5984c618bfa7967a191f63074ab Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 25 Mar 2016 14:44:58 -0400 Subject: [PATCH 0855/3836] Fix grant_role docstring We do not support finding domains by name, only ID. Change-Id: I2abf889abe1f8186e1f0deb7cd433cbc9e6f0ab1 --- shade/operatorcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index c8a090431..b4ab7675b 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1598,7 +1598,7 @@ def grant_role(self, name_or_id, user=None, group=None, :param string user: The name or id of the user. :param string group: The name or id of the group. (v3) :param string project: The name or id of the project. - :param string domain: The name or id of the domain. (v3) + :param string domain: The id of the domain. (v3) :param bool wait: Wait for role to be granted :param int timeout: Timeout to wait for role to be granted From a9f4130ad03f8e6421ba37cf84ea13c94285dd2f Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Fri, 25 Mar 2016 18:50:46 +0000 Subject: [PATCH 0856/3836] Import os module as it is referenced in line 2097 Just got failure 'os i is not defined' on method _get_name_and_filename on an os_image Ansible play Change-Id: I25d07860b12773217b18cc54a2396b64119cdec1 --- shade/openstackcloud.py | 1 + 1 file changed, 1 insertion(+) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index a9732d050..564154b52 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -14,6 +14,7 @@ import hashlib import ipaddress import operator +import os import os_client_config import os_client_config.defaults import threading From 060ce2e055cfceabd22a97801f4cf33c227aa6a2 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 25 Mar 2016 09:09:38 -0500 Subject: [PATCH 0857/3836] Also add server interfaces for server get In a previous patch, we always add the network info for servers via the server list - but we missed the output of server get. Change-Id: I557694b3931d81a3524c781ab5dabfb5995557f5 --- shade/openstackcloud.py | 4 ++-- shade/tests/fakes.py | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index a9732d050..274290dab 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1610,9 +1610,9 @@ def get_server(self, name_or_id=None, filters=None, detailed=False): return _utils._get_entity(searchfunc, name_or_id, filters) def get_server_by_id(self, id): - return _utils.normalize_server( + return meta.add_server_interfaces(self, _utils.normalize_server( self.manager.submitTask(_tasks.ServerGet(server=id)), - cloud_name=self.name, region_name=self.region_name) + cloud_name=self.name, region_name=self.region_name)) def get_image(self, name_or_id, filters=None): """Get an image by name or ID. diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 19ff69f40..cc65a0a5f 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -85,8 +85,9 @@ def __init__(self, id, domain_id=None): class FakeServer(object): def __init__( self, id, name, status, addresses=None, - accessIPv4='', accessIPv6='', flavor=None, image=None, - adminPass=None): + accessIPv4='', accessIPv6='', private_v4='', + private_v6='', public_v4='', public_v6='', + flavor=None, image=None, adminPass=None): self.id = id self.name = name self.status = status @@ -102,6 +103,10 @@ def __init__( self.image = image self.accessIPv4 = accessIPv4 self.accessIPv6 = accessIPv6 + self.private_v4 = private_v4 + self.public_v4 = public_v4 + self.private_v6 = private_v6 + self.public_v6 = public_v6 self.adminPass = adminPass From d9867f4922891c4cad08b7f96a96059b6667f5f2 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 26 Feb 2016 22:24:49 -0500 Subject: [PATCH 0858/3836] Add wait support to create_image_snapshot() When creating a snapshot from a server sometimes it is desired to make this a synchronous operation. This is the same reasons we have a wait on the normal image create. This commit adds the necessary args and logic to the create_image_snapshot() to do a poll loop after the image snapshot is issued to wait until the image becomes available for use. Change-Id: Ief8a3f6c32b56230f0509e658b14795149647ec3 --- shade/openstackcloud.py | 13 +++++- shade/tests/functional/test_compute.py | 13 ++++++ shade/tests/unit/test_image_snapshot.py | 54 +++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 shade/tests/unit/test_image_snapshot.py diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index a9732d050..755eac520 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2064,11 +2064,20 @@ def get_image_id(self, image_name, exclude=None): return image.id return None - def create_image_snapshot(self, name, server, **metadata): + def create_image_snapshot(self, name, server, wait=False, timeout=3600, + **metadata): image_id = str(self.manager.submitTask(_tasks.ImageSnapshotCreate( image_name=name, server=server, metadata=metadata))) self.list_images.invalidate(self) - return self.get_image(image_id) + if not wait: + return self.get_image(image_id) + for count in _utils._iterate_timeout(timeout, + "Timeout waiting for image to " + "snapshot"): + self.list_images.invalidate(self) + image = self.get_image(image_id) + if image['status'] == 'active': + return image def delete_image(self, name_or_id, wait=False, timeout=3600): image = self.get_image(name_or_id) diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index 44afed760..78ed58c91 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -202,3 +202,16 @@ def test_create_boot_from_volume_preexisting_terminate(self): if volume: self.assertEquals('deleting', volume.status) self.assertIsNone(self.demo_cloud.get_server(self.server_name)) + + def test_create_image_snapshot_wait_active(self): + self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) + server = self.demo_cloud.create_server( + name=self.server_name, + image=self.image, + flavor=self.flavor, + admin_pass='sheiqu9loegahSh', + wait=True) + image = self.demo_cloud.create_image_snapshot('test-snapshot', server, + wait=True) + self.addCleanup(self.demo_cloud.delete_image, image['id']) + self.assertEqual('active', image['status']) diff --git a/shade/tests/unit/test_image_snapshot.py b/shade/tests/unit/test_image_snapshot.py new file mode 100644 index 000000000..3fdb5b683 --- /dev/null +++ b/shade/tests/unit/test_image_snapshot.py @@ -0,0 +1,54 @@ +# Copyright 2016 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. + +import uuid + +import mock + +import shade +from shade import exc +from shade.tests.unit import base + + +class TestImageSnapshot(base.TestCase): + + def setUp(self): + super(TestImageSnapshot, self).setUp() + self.image_id = str(uuid.uuid4()) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + @mock.patch.object(shade.OpenStackCloud, 'get_image') + def test_create_image_snapshot_wait_until_active_never_active(self, + mock_get, + mock_nova): + mock_nova.servers.create_image.return_value = { + 'status': 'queued', + 'id': self.image_id, + } + mock_get.return_value = {'status': 'saving', 'id': self.image_id} + self.assertRaises(exc.OpenStackCloudTimeout, + self.cloud.create_image_snapshot, + 'test-snapshot', 'fake-server', wait=True, timeout=2) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + @mock.patch.object(shade.OpenStackCloud, 'get_image') + def test_create_image_snapshot_wait_active(self, mock_get, mock_nova): + mock_nova.servers.create_image.return_value = { + 'status': 'queued', + 'id': self.image_id, + } + mock_get.return_value = {'status': 'active', 'id': self.image_id} + image = self.cloud.create_image_snapshot( + 'test-snapshot', 'fake-server', wait=True, timeout=2) + self.assertEqual(image['id'], self.image_id) From de364270bcacddc40b4afbe0f8402aee4c211480 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 25 Mar 2016 14:37:02 -0500 Subject: [PATCH 0859/3836] Split waiting for images into its own method Nodepool got bit by incorrectly waiting for images. So let's copy the wait_for_server pattern and make a wait_for_image method that knows how to properly wait for an image to become ready. Change-Id: Icac2606ae4d51c6fde5066e767b53a6baa14dd50 --- shade/openstackcloud.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 755eac520..12f923bc7 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2064,20 +2064,30 @@ def get_image_id(self, image_name, exclude=None): return image.id return None - def create_image_snapshot(self, name, server, wait=False, timeout=3600, - **metadata): + def create_image_snapshot( + self, name, server, wait=False, timeout=3600, **metadata): image_id = str(self.manager.submitTask(_tasks.ImageSnapshotCreate( image_name=name, server=server, metadata=metadata))) self.list_images.invalidate(self) + image = self.get_image(image_id) + if not wait: - return self.get_image(image_id) - for count in _utils._iterate_timeout(timeout, - "Timeout waiting for image to " - "snapshot"): + return image + return self.wait_for_image(image, timeout=timeout) + + def wait_for_image(self, image, timeout=3600): + image_id = image['id'] + for count in _utils._iterate_timeout( + timeout, "Timeout waiting for image to snapshot"): self.list_images.invalidate(self) image = self.get_image(image_id) + if not image: + continue if image['status'] == 'active': return image + elif image['status'] == 'error': + raise OpenStackCloudException( + 'Image {image} hit error state'.format(image=image_id)) def delete_image(self, name_or_id, wait=False, timeout=3600): image = self.get_image(name_or_id) From 5c739ce26ea02f1c8d2ca198d98fe72626c9d3be Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 28 Mar 2016 10:18:40 -0400 Subject: [PATCH 0860/3836] Add release notes for new create_image_snapshot() args This commit adds a new release note to describe the new options added to create_image_snapshot(). Change-Id: Ic69d604c52a669af68cef1c06d5c455b27cef276 --- .../notes/wait-on-image-snapshot-27cd2eacab2fabd8.yaml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 releasenotes/notes/wait-on-image-snapshot-27cd2eacab2fabd8.yaml diff --git a/releasenotes/notes/wait-on-image-snapshot-27cd2eacab2fabd8.yaml b/releasenotes/notes/wait-on-image-snapshot-27cd2eacab2fabd8.yaml new file mode 100644 index 000000000..ae434e28b --- /dev/null +++ b/releasenotes/notes/wait-on-image-snapshot-27cd2eacab2fabd8.yaml @@ -0,0 +1,7 @@ +--- +features: + - Adds a new pair of options to create_image_snapshot(), wait and timeout, + to have the function wait until the image snapshot being created goes + into an active state. + - Adds a new function wait_for_image() which will wait for an image to go + into an active state. From 18e8bfbb3e92045e3ad8f90b9d12b86b7e8c350a Mon Sep 17 00:00:00 2001 From: Cedric Brandily Date: Fri, 25 Mar 2016 18:50:28 +0100 Subject: [PATCH 0861/3836] Support InsecureRequestWarning == None According to requestsexceptions implementation, InsecureRequestWarning can reference a class or None. When InsecureRequestWarning is None, OpenStackCloud.__init__ crashs because warnings.filterwarnings expects a class not None as category. This change updates shade code to support this case. Change-Id: Iad757400e15ed6b87db267bdc522aabce9aee8c9 --- shade/openstackcloud.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index a9732d050..bb5b664a6 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -173,7 +173,9 @@ def __init__( self.log.debug( "Turning off Insecure SSL warnings since verify=False") category = requestsexceptions.InsecureRequestWarning - warnings.filterwarnings('ignore', category=category) + if category: + # InsecureRequestWarning references a Warning class or is None + warnings.filterwarnings('ignore', category=category) self._servers = [] self._servers_time = 0 From c7ebbbfdd3074f250baeae700929050b03e78b00 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 29 Mar 2016 10:37:58 -0400 Subject: [PATCH 0862/3836] Re-allow list of networks for FIP assignment Turns out the os_server module will send a list of network or pool names, so we broke it with this change ID: I0b27b50b2a6a8e5199bbeed5786b71851bb3ad3e Change-Id: I7d957d6d94102f2657e6ee87804f86a91563bb45 --- shade/openstackcloud.py | 15 +++++++++---- .../tests/ansible/roles/server/tasks/main.yml | 22 +++++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index f59b4745b..0f38b7a3b 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -17,6 +17,7 @@ import os import os_client_config import os_client_config.defaults +import six import threading import time import warnings @@ -2841,7 +2842,7 @@ def _neutron_available_floating_ips( Return a list of available floating IPs or allocate a new one and return it in a list of 1 element. - :param str network: A Neutron network name or id. + :param network: A single Neutron network name or id, or a list of them. :param server: (server) Server the Floating IP is for :returns: a list of floating IP addresses. @@ -2856,11 +2857,17 @@ def _neutron_available_floating_ips( with _utils.neutron_exceptions("unable to get available floating IPs"): if network: + if isinstance(network, six.string_types): + network = [network] + # Use given list to get first matching external network floating_network_id = None - for ext_net in self.get_external_networks(): - if network in (ext_net['name'], ext_net['id']): - floating_network_id = ext_net['id'] + for net in network: + for ext_net in self.get_external_networks(): + if net in (ext_net['name'], ext_net['id']): + floating_network_id = ext_net['id'] + break + if floating_network_id: break if floating_network_id is None: diff --git a/shade/tests/ansible/roles/server/tasks/main.yml b/shade/tests/ansible/roles/server/tasks/main.yml index 50be33f6d..64a2c111f 100644 --- a/shade/tests/ansible/roles/server/tasks/main.yml +++ b/shade/tests/ansible/roles/server/tasks/main.yml @@ -44,3 +44,25 @@ state: absent name: "{{ server_name }}" wait: true + +- name: Create server (FIP from pool/network) + os_server: + cloud: "{{ cloud }}" + state: present + name: "{{ server_name }}" + image: "{{ image }}" + flavor: "{{ flavor }}" + network: "{{ server_network }}" + floating_ip_pools: + - public + wait: true + register: server + +- debug: var=server + +- name: Delete server (FIP from pool/network) + os_server: + cloud: "{{ cloud }}" + state: absent + name: "{{ server_name }}" + wait: true From 1853a5400132949c3e264326385500b2d9f42667 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 29 Mar 2016 10:24:32 -0700 Subject: [PATCH 0863/3836] Support provider networks in public network detection The provider networks don't set router:external, but can also be determined to be a thing that is the public network. We also need to check that a thing with router:external=False doesn't have provider network parameters set on it. Change-Id: Ie8ee1329419bd1eee2c196f4a8624670aca1bc20 --- shade/openstackcloud.py | 22 +++++++++++++--- shade/tests/unit/test_meta.py | 49 ++++++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index f59b4745b..143505428 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1396,12 +1396,21 @@ def get_external_networks(self): :returns: A list of network dicts if one is found """ - self._external_networks = self._get_network( + _all_networks = self._get_network( self._external_network_name_or_id, self.use_external_network, self._external_networks, self._external_network_stamp, - filters={'router:external': True}) + filters=None) + # Filter locally because we have an or condition + _external_networks = [] + for network in _all_networks: + if (('router:external' in network + and network['router:external']) or + 'provider:network_type' in network): + _external_networks.append(network) + # TODO(mordred): This needs to be mutex protected + self._external_networks = _external_networks self._external_network_stamp = True return self._external_networks @@ -1410,7 +1419,8 @@ def get_internal_networks(self): :returns: A list of network dicts if one is found """ - self._internal_networks = self._get_network( + # Just router:external False is not enough. + _all_networks = self._get_network( self._internal_network_name_or_id, self.use_internal_network, self._internal_networks, @@ -1418,6 +1428,12 @@ def get_internal_networks(self): filters={ 'router:external': False, }) + _internal_networks = [] + for network in _all_networks: + if 'provider:network_type' not in network: + _internal_networks.append(network) + # TODO(mordred): This needs to be mutex protected + self._internal_networks = _internal_networks self._internal_network_stamp = True return self._internal_networks diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 9cf6232e6..fad5ff692 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -210,7 +210,8 @@ def test_get_server_external_ipv4_neutron( mock_has_service.return_value = True mock_search_networks.return_value = [{ 'id': 'test-net-id', - 'name': 'test-net' + 'name': 'test-net', + 'router:external': True, }] srv = meta.obj_to_dict(fakes.FakeServer( @@ -223,6 +224,52 @@ def test_get_server_external_ipv4_neutron( self.assertEqual(PUBLIC_V4, ip) + @mock.patch.object(shade.OpenStackCloud, 'has_service') + @mock.patch.object(shade.OpenStackCloud, 'search_networks') + def test_get_server_external_provider_ipv4_neutron( + self, mock_search_networks, + mock_has_service): + # Testing Clouds with Neutron + mock_has_service.return_value = True + mock_search_networks.return_value = [{ + 'id': 'test-net-id', + 'name': 'test-net', + 'provider:network_type': 'vlan', + }] + + srv = meta.obj_to_dict(fakes.FakeServer( + id='test-id', name='test-name', status='ACTIVE', + addresses={'test-net': [{ + 'addr': PUBLIC_V4, + 'version': 4}]}, + )) + ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) + + self.assertEqual(PUBLIC_V4, ip) + + @mock.patch.object(shade.OpenStackCloud, 'has_service') + @mock.patch.object(shade.OpenStackCloud, 'search_networks') + def test_get_server_external_none_ipv4_neutron( + self, mock_search_networks, + mock_has_service): + # Testing Clouds with Neutron + mock_has_service.return_value = True + mock_search_networks.return_value = [{ + 'id': 'test-net-id', + 'name': 'test-net', + 'router:external': False, + }] + + srv = meta.obj_to_dict(fakes.FakeServer( + id='test-id', name='test-name', status='ACTIVE', + addresses={'test-net': [{ + 'addr': PUBLIC_V4, + 'version': 4}]}, + )) + ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) + + self.assertEqual(None, ip) + def test_get_server_external_ipv4_neutron_accessIPv4(self): srv = meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', From 75ee8f967df00a8595c8169a1958295839aae387 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 29 Mar 2016 11:31:35 -0700 Subject: [PATCH 0864/3836] Mutex protect internal/external network detection We have stamp variables to prevent double processing the events as a mechanism of caching, but they're not mutex protected, so we can also have a race-condition thundering herd potential in multi-threaded code like nodepool. Add mutex protection around it - but just use one mutex for the two, because we can further refactor this to make a single pass through the network list and sort the networks from that. Change-Id: I74d8b279105e8d9e1a2c138da90e31485e123744 --- shade/openstackcloud.py | 70 +++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 143505428..fcf631b3c 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -182,6 +182,8 @@ def __init__( self._servers_time = 0 self._servers_lock = threading.Lock() + self._networks_lock = threading.Lock() + cache_expiration_time = int(cloud_config.get_cache_expiration_time()) cache_class = cloud_config.get_cache_class() cache_arguments = cloud_config.get_cache_arguments() @@ -1396,22 +1398,25 @@ def get_external_networks(self): :returns: A list of network dicts if one is found """ - _all_networks = self._get_network( - self._external_network_name_or_id, - self.use_external_network, - self._external_networks, - self._external_network_stamp, - filters=None) - # Filter locally because we have an or condition - _external_networks = [] - for network in _all_networks: - if (('router:external' in network - and network['router:external']) or - 'provider:network_type' in network): - _external_networks.append(network) - # TODO(mordred): This needs to be mutex protected - self._external_networks = _external_networks - self._external_network_stamp = True + if self._networks_lock.acquire(): + try: + _all_networks = self._get_network( + self._external_network_name_or_id, + self.use_external_network, + self._external_networks, + self._external_network_stamp, + filters=None) + # Filter locally because we have an or condition + _external_networks = [] + for network in _all_networks: + if (('router:external' in network + and network['router:external']) or + 'provider:network_type' in network): + _external_networks.append(network) + self._external_networks = _external_networks + self._external_network_stamp = True + finally: + self._networks_lock.release() return self._external_networks def get_internal_networks(self): @@ -1420,21 +1425,24 @@ def get_internal_networks(self): :returns: A list of network dicts if one is found """ # Just router:external False is not enough. - _all_networks = self._get_network( - self._internal_network_name_or_id, - self.use_internal_network, - self._internal_networks, - self._internal_network_stamp, - filters={ - 'router:external': False, - }) - _internal_networks = [] - for network in _all_networks: - if 'provider:network_type' not in network: - _internal_networks.append(network) - # TODO(mordred): This needs to be mutex protected - self._internal_networks = _internal_networks - self._internal_network_stamp = True + if self._networks_lock.acquire(): + try: + _all_networks = self._get_network( + self._internal_network_name_or_id, + self.use_internal_network, + self._internal_networks, + self._internal_network_stamp, + filters={ + 'router:external': False, + }) + _internal_networks = [] + for network in _all_networks: + if 'provider:network_type' not in network: + _internal_networks.append(network) + self._internal_networks = _internal_networks + self._internal_network_stamp = True + finally: + self._networks_lock.release() return self._internal_networks def get_keypair(self, name_or_id, filters=None): From 454d30b5cfda3d8effdc2a559c78ed8c6cf18ec4 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 30 Mar 2016 12:02:06 -0400 Subject: [PATCH 0865/3836] Remove duplicate FakeServer class from unit tests For some reason, we had a second FakeServer class in unit/test_meta.py, instead of using the real fake (because real fakes are better than fake fakes). Use the real one. Change-Id: I32200bea83043c3495cc54aa6625df062bcb5b1c --- shade/tests/fakes.py | 4 ++- shade/tests/unit/test_meta.py | 63 ++++++++++++++++++++--------------- 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index cc65a0a5f..fd08c2424 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -87,7 +87,8 @@ def __init__( self, id, name, status, addresses=None, accessIPv4='', accessIPv6='', private_v4='', private_v6='', public_v4='', public_v6='', - flavor=None, image=None, adminPass=None): + flavor=None, image=None, adminPass=None, + metadata=None): self.id = id self.name = name self.status = status @@ -108,6 +109,7 @@ def __init__( self.private_v6 = private_v6 self.public_v6 = public_v6 self.adminPass = adminPass + self.metadata = metadata class FakeService(object): diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 9cf6232e6..94a5f44b5 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -65,19 +65,22 @@ def list_server_security_groups(self, server): return [] -class FakeServer(object): - id = 'test-id-0' - metadata = {'group': 'test-group'} - addresses = {'private': [{'OS-EXT-IPS:type': 'fixed', - 'addr': PRIVATE_V4, - 'version': 4}], - 'public': [{'OS-EXT-IPS:type': 'floating', - 'addr': PUBLIC_V4, - 'version': 4}]} - flavor = {'id': '101'} - image = {'id': '471c2475-da2f-47ac-aba5-cb4aa3d546f5'} - accessIPv4 = '' - accessIPv6 = '' +standard_fake_server = fakes.FakeServer( + id='test-id-0', + name='test-id-0', + status='ACTIVE', + metadata={'group': 'test-group'}, + addresses={'private': [{'OS-EXT-IPS:type': 'fixed', + 'addr': PRIVATE_V4, + 'version': 4}], + 'public': [{'OS-EXT-IPS:type': 'floating', + 'addr': PUBLIC_V4, + 'version': 4}]}, + flavor={'id': '101'}, + image={'id': '471c2475-da2f-47ac-aba5-cb4aa3d546f5'}, + accessIPv4='', + accessIPv6='', +) class TestMeta(base.TestCase): @@ -121,7 +124,7 @@ def test_find_nova_addresses_all(self): addrs, key_name='public', ext_tag='fixed', version=6)) def test_get_server_ip(self): - srv = meta.obj_to_dict(FakeServer()) + srv = meta.obj_to_dict(standard_fake_server) self.assertEqual( PRIVATE_V4, meta.get_server_private_ip(srv, self.cloud)) self.assertEqual( @@ -317,7 +320,11 @@ def test_get_groups_from_server(self): 'test-region_test-az', 'test-name_test-region_test-az'], meta.get_groups_from_server( - FakeCloud(), meta.obj_to_dict(FakeServer()), server_vars)) + FakeCloud(), + meta.obj_to_dict(standard_fake_server), + server_vars + ) + ) def test_obj_list_to_dict(self): """Test conversion of a list of objects to a list of dictonaries""" @@ -342,7 +349,7 @@ def test_get_security_groups(self, mock_list_server_security_groups.return_value = [ {'name': 'testgroup', 'id': '1'}] - server = meta.obj_to_dict(FakeServer()) + server = meta.obj_to_dict(standard_fake_server) hostvars = meta.get_hostvars_from_server(FakeCloud(), server) mock_list_server_security_groups.assert_called_once_with(server) @@ -359,7 +366,7 @@ def test_basic_hostvars( hostvars = meta.get_hostvars_from_server( FakeCloud(), _utils.normalize_server( - meta.obj_to_dict(FakeServer()), + meta.obj_to_dict(standard_fake_server), cloud_name='CLOUD_NAME', region_name='REGION_NAME')) self.assertNotIn('links', hostvars) @@ -370,9 +377,11 @@ def test_basic_hostvars( self.assertEquals('REGION_NAME', hostvars['region']) self.assertEquals('CLOUD_NAME', hostvars['cloud']) self.assertEquals("test-image-name", hostvars['image']['name']) - self.assertEquals(FakeServer.image['id'], hostvars['image']['id']) + self.assertEquals(standard_fake_server.image['id'], + hostvars['image']['id']) self.assertNotIn('links', hostvars['image']) - self.assertEquals(FakeServer.flavor['id'], hostvars['flavor']['id']) + self.assertEquals(standard_fake_server.flavor['id'], + hostvars['flavor']['id']) self.assertEquals("test-flavor-name", hostvars['flavor']['name']) self.assertNotIn('links', hostvars['flavor']) # test having volumes @@ -390,7 +399,7 @@ def test_ipv4_hostvars( fake_cloud = FakeCloud() fake_cloud.force_ipv4 = True hostvars = meta.get_hostvars_from_server( - fake_cloud, meta.obj_to_dict(FakeServer())) + fake_cloud, meta.obj_to_dict(standard_fake_server)) self.assertEqual(PUBLIC_V4, hostvars['interface_ip']) @mock.patch.object(shade.meta, 'get_server_external_ipv4') @@ -400,21 +409,21 @@ def test_private_interface_ip(self, mock_get_server_external_ipv4): cloud = FakeCloud() cloud.private = True hostvars = meta.get_hostvars_from_server( - cloud, meta.obj_to_dict(FakeServer())) + cloud, meta.obj_to_dict(standard_fake_server)) self.assertEqual(PRIVATE_V4, hostvars['interface_ip']) @mock.patch.object(shade.meta, 'get_server_external_ipv4') def test_image_string(self, mock_get_server_external_ipv4): mock_get_server_external_ipv4.return_value = PUBLIC_V4 - server = FakeServer() + server = standard_fake_server server.image = 'fake-image-id' hostvars = meta.get_hostvars_from_server( FakeCloud(), meta.obj_to_dict(server)) self.assertEquals('fake-image-id', hostvars['image']['id']) def test_az(self): - server = FakeServer() + server = standard_fake_server server.__dict__['OS-EXT-AZ:availability_zone'] = 'az1' hostvars = _utils.normalize_server( @@ -433,7 +442,7 @@ def test_has_volume(self): fake_volume_dict = meta.obj_to_dict(fake_volume) mock_cloud.get_volumes.return_value = [fake_volume_dict] hostvars = meta.get_hostvars_from_server( - mock_cloud, meta.obj_to_dict(FakeServer())) + mock_cloud, meta.obj_to_dict(standard_fake_server)) self.assertEquals('volume1', hostvars['volumes'][0]['id']) self.assertEquals('/dev/sda0', hostvars['volumes'][0]['device']) @@ -441,7 +450,7 @@ def test_has_no_volume_service(self): fake_cloud = FakeCloud() fake_cloud.service_val = False hostvars = meta.get_hostvars_from_server( - fake_cloud, meta.obj_to_dict(FakeServer())) + fake_cloud, meta.obj_to_dict(standard_fake_server)) self.assertEquals([], hostvars['volumes']) def test_unknown_volume_exception(self): @@ -457,11 +466,11 @@ def side_effect(*args): FakeException, meta.get_hostvars_from_server, mock_cloud, - meta.obj_to_dict(FakeServer())) + meta.obj_to_dict(standard_fake_server)) def test_obj_to_dict(self): cloud = FakeCloud() - cloud.server = FakeServer() + cloud.server = standard_fake_server cloud_dict = meta.obj_to_dict(cloud) self.assertEqual(FakeCloud.name, cloud_dict['name']) self.assertNotIn('_unused', cloud_dict) From 32c4fff83828f814df8b5af17bc3e7353b41f4b9 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 30 Mar 2016 12:17:15 -0400 Subject: [PATCH 0866/3836] Fix test_get_server_ip unit test It turns out this unit test wasn't actually testing get_server_ip(), and the things it wanted to test were already being tested by other unit tests. And because it didn't do mocking properly, unlike the other tests, it was taking a long time to run. Rework the test to be more betterer and do correct things. Change-Id: Ife0fa7673081c72ca766e8f78ad2d44b1d0035f8 --- shade/tests/unit/test_meta.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 94a5f44b5..bc2aacbe4 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -126,9 +126,9 @@ def test_find_nova_addresses_all(self): def test_get_server_ip(self): srv = meta.obj_to_dict(standard_fake_server) self.assertEqual( - PRIVATE_V4, meta.get_server_private_ip(srv, self.cloud)) + PRIVATE_V4, meta.get_server_ip(srv, ext_tag='fixed')) self.assertEqual( - PUBLIC_V4, meta.get_server_external_ipv4(self.cloud, srv)) + PUBLIC_V4, meta.get_server_ip(srv, ext_tag='floating')) @mock.patch.object(shade.OpenStackCloud, 'has_service') @mock.patch.object(shade.OpenStackCloud, 'search_networks') From 186d57e5f5d3f06ff612bfd2341f7bc67544cb1d Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 30 Mar 2016 12:35:16 -0400 Subject: [PATCH 0867/3836] Fix test_list_servers unit test This test was taking abnormally long due to the fact that it was not mocking all the things. Change-Id: I565e8568d3815bcf1ab7614081145f5b99cb8182 --- shade/tests/unit/test_shade.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 137798df3..114219cb4 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -567,21 +567,24 @@ def test__neutron_exceptions_generic(self): self.cloud.list_networks) @mock.patch.object(shade._tasks.ServerList, 'main') - def test_list_servers(self, mock_serverlist): + @mock.patch('shade.meta.add_server_interfaces') + def test_list_servers(self, mock_add_srv_int, mock_serverlist): '''This test verifies that calling list_servers results in a call to the ServerList task.''' - mock_serverlist.return_value = [ - munch.Munch({'name': 'testserver', - 'id': '1', - 'flavor': {}, - 'addresses': {}, - 'accessIPv4': '', - 'accessIPv6': '', - 'image': ''}) - ] + server_obj = munch.Munch({'name': 'testserver', + 'id': '1', + 'flavor': {}, + 'addresses': {}, + 'accessIPv4': '', + 'accessIPv6': '', + 'image': ''}) + mock_serverlist.return_value = [server_obj] + mock_add_srv_int.side_effect = [server_obj] r = self.cloud.list_servers() + self.assertEquals(1, len(r)) + self.assertEquals(1, mock_add_srv_int.call_count) self.assertEquals('testserver', r[0]['name']) @mock.patch.object(shade._tasks.ServerList, 'main') From b99eb3f3524f2cbefbdd3c7091f79758387b1c32 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 30 Mar 2016 09:57:36 -0400 Subject: [PATCH 0868/3836] Reset network caches after network create/delete Because we always cache the internal and external networks (because they are queried a lot during inventory ops), we need to make sure to reset the cache when a network is added or deleted. Otherwise the cache will not be valid. Change-Id: Id0ef15cd469f3b42fabcb85245853adefcdfa54f --- shade/openstackcloud.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index fcf631b3c..f731ba26d 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -142,24 +142,16 @@ def __init__( self.secgroup_source = cloud_config.config['secgroup_source'] self.force_ipv4 = cloud_config.force_ipv4 - self._external_networks = [] self._external_network_name_or_id = cloud_config.config.get( 'external_network', None) self._use_external_network = cloud_config.config.get( 'use_external_network', True) - self._internal_networks = [] self._internal_network_name_or_id = cloud_config.config.get( 'internal_network', None) self._use_internal_network = cloud_config.config.get( 'use_internal_network', True) - # Variables to prevent us from going through the network finding - # logic again if we've done it once. This is different from just - # the cached value, since "None" is a valid value to find. - self._external_network_stamp = False - self._internal_network_stamp = False - if manager is not None: self.manager = manager else: @@ -183,6 +175,7 @@ def __init__( self._servers_lock = threading.Lock() self._networks_lock = threading.Lock() + self._reset_network_caches() cache_expiration_time = int(cloud_config.get_cache_expiration_time()) cache_class = cloud_config.get_cache_class() @@ -1356,6 +1349,16 @@ def use_external_network(self): def use_internal_network(self): return self._use_internal_network + def _reset_network_caches(self): + # Variables to prevent us from going through the network finding + # logic again if we've done it once. This is different from just + # the cached value, since "None" is a valid value to find. + with self._networks_lock: + self._external_networks = [] + self._internal_networks = [] + self._external_network_stamp = False + self._internal_network_stamp = False + def _get_network( self, name_or_id, @@ -1818,6 +1821,10 @@ def create_network(self, name, shared=False, admin_state_up=True, "Error creating network {0}".format(name)): net = self.manager.submitTask( _tasks.NetworkCreate(body=dict({'network': network}))) + + # Reset cache so the new network is picked up + self._reset_network_caches() + return net['network'] def delete_network(self, name_or_id): @@ -1839,6 +1846,9 @@ def delete_network(self, name_or_id): self.manager.submitTask( _tasks.NetworkDelete(network=network['id'])) + # Reset cache so the deleted network is removed + self._reset_network_caches() + return True def _build_external_gateway_info(self, ext_gateway_net_id, enable_snat, From e43625d30025a11762e1cdd9ff6973ac3ec28db6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 30 Mar 2016 15:18:35 -0700 Subject: [PATCH 0869/3836] Workaround multiple private network ports In the case where there are multiple private networks, don't actively throw an error. Instead, just grab the first port and attach NAT to it. That's not completely correct, so a follow up patch is forthcoming that will add support to os-client-config for such a configuration value. However, in the mean time, just grab one, because why not. Change-Id: Ib3a49c0efc72a6de0bdd84c3fe1f34cc5f553a8c --- shade/openstackcloud.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index fcf631b3c..6cfebd4c1 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3141,15 +3141,10 @@ def _get_free_fixed_port(self, server, fixed_address=None): return (None, None) port = None if not fixed_address: - if len(ports) > 1: - raise OpenStackCloudException( - "More than one port was found for server {server}" - " and no fixed_address was specified. It is not" - " possible to infer correct behavior. Please specify" - " a fixed_address - or file a bug in shade describing" - " how you think this should work.") # We're assuming one, because we have no idea what to do with # more than one. + # TODO(mordred) Fix this for real by allowing a configurable + # NAT destination setting port = ports[0] # Select the first available IPv4 address for address in port.get('fixed_ips', list()): From 278a761df68d1e7d4d93ee2c6fb91f1a0e82e78a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 30 Mar 2016 16:10:04 -0700 Subject: [PATCH 0870/3836] Change network info indication to a generic list Networks can have more information than just internal or external. Notably, if you have two private networks and you're trying to assign floating ips, you need to know which network should be the recipient. This should be backwards compatible with existing external_network and internal_network options. Change-Id: I0d469339ba00486683fcd3ce2995002fa0a576d1 --- README.rst | 12 ++++--- os_client_config/config.py | 24 ++++++++++++++ os_client_config/tests/test_config.py | 31 +++++++++++++++++++ .../notes/network-list-e6e9dafdd8446263.yaml | 11 +++++++ 4 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/network-list-e6e9dafdd8446263.yaml diff --git a/README.rst b/README.rst index 2e584bd53..15f4bf09a 100644 --- a/README.rst +++ b/README.rst @@ -289,12 +289,16 @@ region. regions: - name: ams01 values: - external_network: inap-17037-WAN1654 - internal_network: inap-17037-LAN4820 + networks: + - name: inap-17037-WAN1654 + routes_externally: true + - name: inap-17037-LAN6745 - name: nyj01 values: - external_network: inap-17037-WAN7752 - internal_network: inap-17037-LAN6745 + networks: + - name: inap-17037-WAN1654 + routes_externally: true + - name: inap-17037-LAN6745 Usage ----- diff --git a/os_client_config/config.py b/os_client_config/config.py index 2f6adeb2b..98870f629 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -477,6 +477,7 @@ def _fix_backwards_madness(self, cloud): cloud = self._fix_backwards_auth_plugin(cloud) cloud = self._fix_backwards_project(cloud) cloud = self._fix_backwards_interface(cloud) + cloud = self._fix_backwards_networks(cloud) cloud = self._handle_domain_id(cloud) return cloud @@ -485,6 +486,29 @@ def _project_scoped(self, cloud): or 'project_id' in cloud['auth'] or 'project_name' in cloud['auth']) + def _fix_backwards_networks(self, cloud): + # Leave the external_network and internal_network keys in the + # dict because consuming code might be expecting them. + networks = cloud.get('networks', []) + for key in ('external_network', 'internal_network'): + external = key.startswith('external') + if key in cloud and 'networks' in cloud: + raise exceptions.OpenStackConfigException( + "Both {key} and networks were specified in the config." + " Please remove {key} from the config and use the network" + " list to configure network behavior.".format(key=key)) + if key in cloud: + warnings.warn( + "{key} is deprecated. Please replace with an entry in" + " a dict inside of the networks list with name: {name}" + " and routes_externally: {external}".format( + key=key, name=cloud[key], external=external)) + networks.append(dict( + name=cloud[key], + routes_externally=external)) + cloud['networks'] = networks + return cloud + def _handle_domain_id(self, cloud): # Allow people to just specify domain once if it's the same mappings = { diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 10a3d7b31..0db1457a9 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -753,3 +753,34 @@ def test_project_password(self): } } self.assertEqual(expected, result) + + def test_backwards_network_fail(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + cloud = { + 'external_network': 'public', + 'networks': [ + {'name': 'private', 'routes_externally': False}, + ] + } + self.assertRaises( + exceptions.OpenStackConfigException, + c._fix_backwards_networks, cloud) + + def test_backwards_network(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + cloud = { + 'external_network': 'public', + 'internal_network': 'private', + } + result = c._fix_backwards_networks(cloud) + expected = { + 'external_network': 'public', + 'internal_network': 'private', + 'networks': [ + {'name': 'public', 'routes_externally': True}, + {'name': 'private', 'routes_externally': False}, + ] + } + self.assertEqual(expected, result) diff --git a/releasenotes/notes/network-list-e6e9dafdd8446263.yaml b/releasenotes/notes/network-list-e6e9dafdd8446263.yaml new file mode 100644 index 000000000..83754883a --- /dev/null +++ b/releasenotes/notes/network-list-e6e9dafdd8446263.yaml @@ -0,0 +1,11 @@ +--- +features: + - Support added for configuring metadata about networks + for a cloud in a list of dicts, rather than in the + external_network and internal_network entries. The dicts + support a name and a routes_externally field, as well as + any other arbitrary metadata needed by consuming + applications. +deprecations: + - external_network and internal_network are deprecated and + should be replaced with the list of network dicts. From acf9ffd4dcfa29c3112be527301b2e402191672b Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Wed, 30 Mar 2016 16:04:00 -0700 Subject: [PATCH 0871/3836] Cache ports like servers This implements nearly the same caching logic for ports as servers so that when nodepool has a lot of servers that need floating ips, we can potentially list all of the ports and get the ports for many servers with one API call. If a filter is being pushed down to neutron, we will just perform the API call rather than caching (but in the common case of searching for a port for a server, we will no longer push down the filter in order to allow the caching to happen). Since we could have a caching delay, we also wait a little bit in case we don't get a port when we expect one. Change-Id: I66566f7a84d50b562f1303ed0e645cb41b11367e --- shade/openstackcloud.py | 62 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index f731ba26d..bc7887914 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -57,6 +57,7 @@ # This halves the current default for Swift DEFAULT_MAX_FILE_SIZE = (5 * 1024 * 1024 * 1024 + 2) / 2 DEFAULT_SERVER_AGE = 5 +DEFAULT_PORT_AGE = 5 OBJECT_CONTAINER_ACLS = { @@ -174,6 +175,10 @@ def __init__( self._servers_time = 0 self._servers_lock = threading.Lock() + self._ports = [] + self._ports_time = 0 + self._ports_lock = threading.Lock() + self._networks_lock = threading.Lock() self._reset_network_caches() @@ -189,6 +194,7 @@ def __init__( expiration_time=cache_expiration_time, arguments=cache_arguments) self._SERVER_AGE = DEFAULT_SERVER_AGE + self._PORT_AGE = DEFAULT_PORT_AGE else: def _fake_invalidate(unused): pass @@ -201,6 +207,7 @@ def invalidate(self): # Replace this with a more specific cache configuration # soon. self._SERVER_AGE = 0 + self._PORT_AGE = 0 self._cache = _FakeCache() # Undecorate cache decorated methods. Otherwise the call stacks # wind up being stupidly long and hard to debug @@ -218,6 +225,8 @@ def invalidate(self): # fall back to whatever it was before self._SERVER_AGE = cloud_config.get_cache_resource_expiration( 'server', self._SERVER_AGE) + self._PORT_AGE = cloud_config.get_cache_resource_expiration( + 'port', self._PORT_AGE) self._container_cache = dict() self._file_hash_cache = dict() @@ -1006,7 +1015,14 @@ def search_ports(self, name_or_id=None, filters=None): :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call. """ - ports = self.list_ports(filters) + # If port caching is enabled, do not push the filter down to + # neutron; get all the ports (potentially from the cache) and + # filter locally. + if self._PORT_AGE: + pushdown_filters = None + else: + pushdown_filters = filters + ports = self.list_ports(pushdown_filters) return _utils._filter_list(ports, name_or_id, filters) def search_volumes(self, name_or_id=None, filters=None): @@ -1122,11 +1138,32 @@ def list_ports(self, filters=None): :returns: A list of port dicts. """ + # If pushdown filters are specified, bypass local caching. + if filters: + return self._list_ports(filters) # Translate None from search interface to empty {} for kwargs below - if not filters: - filters = {} + filters = {} + if (time.time() - self._ports_time) >= self._PORT_AGE: + # Since we're using cached data anyway, we don't need to + # have more than one thread actually submit the list + # ports task. Let the first one submit it while holding + # a lock, and the non-blocking acquire method will cause + # subsequent threads to just skip this and use the old + # data until it succeeds. + # For the first time, when there is no data, make the call + # blocking. + if self._ports_lock.acquire(len(self._ports) == 0): + try: + self._ports = self._list_ports(filters) + self._ports_time = time.time() + finally: + self._ports_lock.release() + return self._ports + + def _list_ports(self, filters): with _utils.neutron_exceptions("Error fetching port list"): - return self.manager.submitTask(_tasks.PortList(**filters))['ports'] + return self.manager.submitTask( + _tasks.PortList(**filters))['ports'] @_utils.cache_on_arguments(should_cache_fn=_no_pending_volumes) def list_volumes(self, cache=True): @@ -3146,7 +3183,22 @@ def _attach_ip_to_server( return server def _get_free_fixed_port(self, server, fixed_address=None): - ports = self.search_ports(filters={'device_id': server['id']}) + # If we are caching port lists, we may not find the port for + # our server if the list is old. Try for at least 2 cache + # periods if that is the case. + if self._PORT_AGE: + timeout = self._PORT_AGE * 2 + else: + timeout = None + for count in _utils._iterate_timeout( + timeout, + "Timeout waiting for port to show up in list", + wait=self._PORT_AGE): + try: + ports = self.search_ports(filters={'device_id': server['id']}) + break + except OpenStackCloudTimeout: + ports = None if not ports: return (None, None) port = None From 74ea5ca44eb7ae92cdf1ff46e87e47d1824cee3c Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Wed, 30 Mar 2016 16:19:41 -0700 Subject: [PATCH 0872/3836] Remove conditional blocking on server list When the server list cache proceduce was imported from nodepool, it grew this internal conditional which caused all server list calls to block on aquiring the server list cache update lock iff the server list cache is empty. This means that on startup, we could have a thundering herd of threads which all insisted on waiting for the lock because there was no cached server list. This does not seem to be necessary, and the original nodepool logic of always performing non-blocking acquisitions of the lock and returning the old (possibly empty) list of servers for any thread that did not get the lock (but will try again a few seconds later and should receive updated data at that time) should be sufficient. Change-Id: I5640f60da2b7789a98bea033e16695389c6062e0 --- shade/openstackcloud.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index bc7887914..68467c1fa 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1150,9 +1150,7 @@ def list_ports(self, filters=None): # a lock, and the non-blocking acquire method will cause # subsequent threads to just skip this and use the old # data until it succeeds. - # For the first time, when there is no data, make the call - # blocking. - if self._ports_lock.acquire(len(self._ports) == 0): + if self._ports_lock.acquire(False): try: self._ports = self._list_ports(filters) self._ports_time = time.time() @@ -1270,9 +1268,7 @@ def list_servers(self, detailed=False): # a lock, and the non-blocking acquire method will cause # subsequent threads to just skip this and use the old # data until it succeeds. - # For the first time, when there is no data, make the call - # blocking. - if self._servers_lock.acquire(len(self._servers) == 0): + if self._servers_lock.acquire(False): try: self._servers = self._list_servers(detailed=detailed) self._servers_time = time.time() From 9cbe9ae83df33e7fc67e772da5b35637e4f141c6 Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Fri, 4 Mar 2016 20:04:57 +0000 Subject: [PATCH 0873/3836] Follow name_or_id pattern on domain operations On pretty much all resources you can pass either a name or an ID on CRUD operations. This change does exactly that for domains. Change-Id: I88da81a6a62839f4eedc362fd15cb38149b6efd8 --- ...perations_name_or_id-baba4cac5b67234d.yaml | 4 + shade/operatorcloud.py | 78 ++++++++++++++----- shade/tests/functional/test_domain.py | 44 +++++++++++ shade/tests/unit/test_domains.py | 25 ++++++ 4 files changed, 131 insertions(+), 20 deletions(-) create mode 100644 releasenotes/notes/domain_operations_name_or_id-baba4cac5b67234d.yaml diff --git a/releasenotes/notes/domain_operations_name_or_id-baba4cac5b67234d.yaml b/releasenotes/notes/domain_operations_name_or_id-baba4cac5b67234d.yaml new file mode 100644 index 000000000..6d58e43c1 --- /dev/null +++ b/releasenotes/notes/domain_operations_name_or_id-baba4cac5b67234d.yaml @@ -0,0 +1,4 @@ +--- +features: + - Added name_or_id parameter to domain operations, allowing + an admin to update/delete/get by domain name. diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index b4ab7675b..fef5967a4 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1077,7 +1077,20 @@ def create_domain( return _utils.normalize_domains([domain])[0] def update_domain( - self, domain_id, name=None, description=None, enabled=None): + self, domain_id=None, name=None, description=None, + enabled=None, name_or_id=None): + if domain_id is None: + if name_or_id is None: + raise OpenStackCloudException( + "You must pass either domain_id or name_or_id value" + ) + dom = self.get_domain(None, name_or_id) + if dom is None: + raise OpenStackCloudException( + "Domain {0} not found for updating".format(name_or_id) + ) + domain_id = dom['id'] + with _utils.shade_exceptions( "Error in updating domain {domain}".format(domain=domain_id)): domain = self.manager.submitTask(_tasks.DomainUpdate( @@ -1085,23 +1098,37 @@ def update_domain( enabled=enabled)) return _utils.normalize_domains([domain])[0] - def delete_domain(self, domain_id): + def delete_domain(self, domain_id=None, name_or_id=None): """Delete a Keystone domain. :param domain_id: ID of the domain to delete. + :param name_or_id: Name or ID of the domain to delete. - :returns: None + :returns: True if delete succeeded, False otherwise. :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call. """ - with _utils.shade_exceptions("Failed to delete domain {id}".format( - id=domain_id)): + if domain_id is None: + if name_or_id is None: + raise OpenStackCloudException( + "You must pass either domain_id or name_or_id value" + ) + dom = self.get_domain(None, name_or_id) + if dom is None: + self.log.debug( + "Domain {0} not found for deleting".format(name_or_id)) + return False + domain_id = dom['id'] + + with _utils.shade_exceptions( + "Failed to delete domain {id}".format(id=domain_id)): # Deleting a domain is expensive, so disabling it first increases # the changes of success domain = self.update_domain(domain_id, enabled=False) - self.manager.submitTask(_tasks.DomainDelete( - domain=domain['id'])) + self.manager.submitTask(_tasks.DomainDelete(domain=domain['id'])) + + return True def list_domains(self): """List Keystone domains. @@ -1115,9 +1142,10 @@ def list_domains(self): domains = self.manager.submitTask(_tasks.DomainList()) return _utils.normalize_domains(domains) - def search_domains(self, filters=None): + def search_domains(self, filters=None, name_or_id=None): """Search Keystone domains. + :param name_or_id: domain name or id :param dict filters: A dict containing additional filters to use. Keys to search on are id, name, enabled and description. @@ -1130,15 +1158,22 @@ def search_domains(self, filters=None): :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ - with _utils.shade_exceptions("Failed to list domains"): - domains = self.manager.submitTask( - _tasks.DomainList(**filters)) - return _utils.normalize_domains(domains) + if name_or_id is not None: + domains = self.list_domains() + return _utils._filter_list(domains, name_or_id, filters) + else: + with _utils.shade_exceptions("Failed to list domains"): + domains = self.manager.submitTask( + _tasks.DomainList(**filters)) + return _utils.normalize_domains(domains) - def get_domain(self, domain_id): + def get_domain(self, domain_id=None, name_or_id=None, filters=None): """Get exactly one Keystone domain. :param domain_id: domain id. + :param name_or_id: domain name or id. + :param dict filters: A dict containing additional filters to use. + Keys to search on are id, name, enabled and description. :returns: a dict containing the domain description, or None if not found. Each dict contains the following attributes:: @@ -1149,13 +1184,16 @@ def get_domain(self, domain_id): :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ - with _utils.shade_exceptions( - "Failed to get domain " - "{domain_id}".format(domain_id=domain_id) - ): - domain = self.manager.submitTask( - _tasks.DomainGet(domain=domain_id)) - return _utils.normalize_domains([domain])[0] + if domain_id is None: + return _utils._get_entity(self.search_domains, filters, name_or_id) + else: + with _utils.shade_exceptions( + "Failed to get domain " + "{domain_id}".format(domain_id=domain_id) + ): + domain = self.manager.submitTask( + _tasks.DomainGet(domain=domain_id)) + return _utils.normalize_domains([domain])[0] @_utils.cache_on_arguments() def list_groups(self): diff --git a/shade/tests/functional/test_domain.py b/shade/tests/functional/test_domain.py index fff7544cf..4d5ca94e5 100644 --- a/shade/tests/functional/test_domain.py +++ b/shade/tests/functional/test_domain.py @@ -66,6 +66,11 @@ def test_search_domains(self): self.assertEqual(1, len(results)) self.assertEqual(domain_name, results[0]['name']) + # Now we search by name with name_or_id, should find only new domain + results = self.operator_cloud.search_domains(name_or_id=domain_name) + self.assertEqual(1, len(results)) + self.assertEqual(domain_name, results[0]['name']) + def test_update_domain(self): domain = self.operator_cloud.create_domain( self.domain_prefix, 'description') @@ -78,3 +83,42 @@ def test_update_domain(self): self.assertEqual('updated name', updated['name']) self.assertEqual('updated description', updated['description']) self.assertFalse(updated['enabled']) + + # Now we update domain by name with name_or_id + updated = self.operator_cloud.update_domain( + None, + name_or_id='updated name', + name='updated name 2', + description='updated description 2', + enabled=True) + self.assertEqual('updated name 2', updated['name']) + self.assertEqual('updated description 2', updated['description']) + self.assertTrue(updated['enabled']) + + def test_delete_domain(self): + domain = self.operator_cloud.create_domain(self.domain_prefix, + 'description') + self.assertEqual(self.domain_prefix, domain['name']) + self.assertEqual('description', domain['description']) + self.assertTrue(domain['enabled']) + deleted = self.operator_cloud.delete_domain(domain['id']) + self.assertTrue(deleted) + + # Now we delete domain by name with name_or_id + domain = self.operator_cloud.create_domain( + self.domain_prefix, 'description') + self.assertEqual(self.domain_prefix, domain['name']) + self.assertEqual('description', domain['description']) + self.assertTrue(domain['enabled']) + deleted = self.operator_cloud.delete_domain(None, domain['name']) + self.assertTrue(deleted) + + # Finally, we assert we get False from delete_domain if domain does + # not exist + domain = self.operator_cloud.create_domain( + self.domain_prefix, 'description') + self.assertEqual(self.domain_prefix, domain['name']) + self.assertEqual('description', domain['description']) + self.assertTrue(domain['enabled']) + deleted = self.operator_cloud.delete_domain(None, 'bogus_domain') + self.assertFalse(deleted) diff --git a/shade/tests/unit/test_domains.py b/shade/tests/unit/test_domains.py index fe72a0183..86f1f77f8 100644 --- a/shade/tests/unit/test_domains.py +++ b/shade/tests/unit/test_domains.py @@ -49,6 +49,13 @@ def test_get_domain(self, mock_keystone): self.assertTrue(mock_keystone.domains.get.called) self.assertEqual(domain['name'], 'a-domain') + @mock.patch.object(shade._utils, '_get_entity') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_get_domain_with_name_or_id(self, mock_keystone, mock_get): + self.cloud.get_domain(name_or_id='1234') + mock_get.assert_called_once_with(mock.ANY, + None, '1234') + @mock.patch.object(shade._utils, 'normalize_domains') @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_create_domain(self, mock_keystone, mock_normalize): @@ -78,6 +85,15 @@ def test_delete_domain(self, mock_keystone, mock_update): mock_keystone.domains.delete.assert_called_once_with( domain='update_domain_id') + @mock.patch.object(shade.OperatorCloud, 'get_domain') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_delete_domain_name_or_id(self, mock_keystone, mock_get): + self.cloud.update_domain(name_or_id='a-domain', + name='new name', + description='new description', + enabled=False) + mock_get.assert_called_once_with(None, 'a-domain') + @mock.patch.object(shade.OperatorCloud, 'update_domain') @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_delete_domain_exception(self, mock_keystone, mock_update): @@ -102,6 +118,15 @@ def test_update_domain(self, mock_keystone, mock_normalize): mock_normalize.assert_called_once_with( [meta.obj_to_dict(domain_obj)]) + @mock.patch.object(shade.OperatorCloud, 'get_domain') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_update_domain_name_or_id(self, mock_keystone, mock_get): + self.cloud.update_domain(name_or_id='a-domain', + name='new name', + description='new description', + enabled=False) + mock_get.assert_called_once_with(None, 'a-domain') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_update_domain_exception(self, mock_keystone): mock_keystone.domains.update.side_effect = Exception() From 7c439073f39010ad3ac937b8c9726da0f27976b7 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 2 Apr 2016 09:09:54 -0500 Subject: [PATCH 0874/3836] Flesh out netowrk config list Add support for indicating default_interface. Also, add some validation and normalization code, some interface methods and, shockingly, documentation. Change-Id: Ib45b68894585ac02821d5d2376510fd7a8e8ee40 --- README.rst | 2 +- doc/source/index.rst | 1 + doc/source/network-config.rst | 47 +++++++++++++++++++ os_client_config/cloud_config.py | 24 ++++++++++ os_client_config/config.py | 40 +++++++++++++++- os_client_config/tests/test_config.py | 36 +++++++++++++- .../notes/network-list-e6e9dafdd8446263.yaml | 5 +- 7 files changed, 147 insertions(+), 8 deletions(-) create mode 100644 doc/source/network-config.rst diff --git a/README.rst b/README.rst index 15f4bf09a..97a158429 100644 --- a/README.rst +++ b/README.rst @@ -277,7 +277,7 @@ To support this, the region list can actually be a list of dicts, and any setting that can be set at the cloud level can be overridden for that region. -:: +.. code-block:: yaml clouds: internap: diff --git a/doc/source/index.rst b/doc/source/index.rst index bf667b786..f7263c9c3 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -6,6 +6,7 @@ vendor-support contributing installation + network-config api-reference releasenotes diff --git a/doc/source/network-config.rst b/doc/source/network-config.rst new file mode 100644 index 000000000..9bbbf9d7e --- /dev/null +++ b/doc/source/network-config.rst @@ -0,0 +1,47 @@ +============== +Network Config +============== + +There are several different qualities that networks in OpenStack might have +that might not be able to be automatically inferred from the available +metadata. To help users navigate more complex setups, `os-client-config` +allows configuring a list of network metadata. + +.. code-block:: yaml + + clouds: + amazing: + networks: + - name: blue + routes_externally: true + - name: purple + routes_externally: true + default_interface: true + - name: green + routes_externally: false + - name: purple + routes_externally: false + nat_destination: true + +Every entry must have a name field, which can hold either the name or the id +of the network. + +`routes_externally` is a boolean field that labels the network as handling +north/south traffic off of the cloud. In a public cloud this might be thought +of as the "public" network, but in private clouds it's possible it might +be an RFC1918 address. In either case, it's provides IPs to servers that +things not on the cloud can use. This value defaults to `false`, which +indicates only servers on the same network can talk to it. + +`default_interface` is a boolean field that indicates that the network is the +one that programs should use. It defaults to false. An example of needing to +use this value is a cloud with two private networks, and where a user is +running ansible in one of the servers to talk to other servers on the private +network. Because both networks are private, there would otherwise be no way +to determine which one should be used for the traffic. + +`nat_destination` is a boolean field that indicates which network floating +ips should be attached to. It defaults to false. Normally this can be inferred +by looking for a network that has subnets that have a gateway_ip. But it's +possible to have more than one network that satisfies that condition, so the +user might want to tell programs which one to pick. diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index b19607e03..e63bd127a 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -438,3 +438,27 @@ def get_cache_resource_expiration(self, resource, default=None): if resource not in expiration: return default return float(expiration[resource]) + + def get_external_networks(self): + """Get list of network names for external networks.""" + return [ + net['name'] for net in self._openstack_config['networks'] + if net['routes_externally']] + + def get_internal_networks(self): + """Get list of network names for internal networks.""" + return [ + net['name'] for net in self._openstack_config['networks'] + if not net['routes_externally']] + + def get_default_network(self): + """Get network used for default interactions.""" + for net in self._openstack_config['networks']: + if net['default_interface']: + return net + + def get_nat_destination(self): + """Get network used for NAT destination.""" + for net in self._openstack_config['networks']: + if net['nat_destination']: + return net diff --git a/os_client_config/config.py b/os_client_config/config.py index 98870f629..2dd49c3d1 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -83,6 +83,8 @@ def set_default(key, value): def get_boolean(value): + if value is None: + return False if type(value) is bool: return value if value.lower() == 'true': @@ -486,10 +488,37 @@ def _project_scoped(self, cloud): or 'project_id' in cloud['auth'] or 'project_name' in cloud['auth']) + def _validate_networks(self, networks, key): + value = None + for net in networks: + if value and net[key]: + raise exceptions.OpenStackConfigException( + "Duplicate network entries for {key}: {net1} and {net2}." + " Only one network can be flagged with {key}".format( + key=key, + net1=value['name'], + net2=net['name'])) + if not value and net[key]: + value = net + def _fix_backwards_networks(self, cloud): # Leave the external_network and internal_network keys in the # dict because consuming code might be expecting them. - networks = cloud.get('networks', []) + networks = [] + # Normalize existing network entries + for net in cloud.get('networks', []): + name = net.get('name') + if not name: + raise exceptions.OpenStackConfigException( + 'Entry in network list is missing required field "name".') + network = dict( + name=name, + routes_externally=get_boolean(net.get('routes_externally')), + nat_destination=get_boolean(net.get('nat_destination')), + default_interface=get_boolean(net.get('default_interface')), + ) + networks.append(network) + for key in ('external_network', 'internal_network'): external = key.startswith('external') if key in cloud and 'networks' in cloud: @@ -505,7 +534,14 @@ def _fix_backwards_networks(self, cloud): key=key, name=cloud[key], external=external)) networks.append(dict( name=cloud[key], - routes_externally=external)) + routes_externally=external, + nat_destination=not external, + default_interface=external)) + + # Validate that we don't have duplicates + self._validate_networks(networks, 'nat_destination') + self._validate_networks(networks, 'default_interface') + cloud['networks'] = networks return cloud diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 0db1457a9..7c2bec0f5 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -779,8 +779,40 @@ def test_backwards_network(self): 'external_network': 'public', 'internal_network': 'private', 'networks': [ - {'name': 'public', 'routes_externally': True}, - {'name': 'private', 'routes_externally': False}, + {'name': 'public', 'routes_externally': True, + 'nat_destination': False, 'default_interface': True}, + {'name': 'private', 'routes_externally': False, + 'nat_destination': True, 'default_interface': False}, + ] + } + self.assertEqual(expected, result) + + def test_normalize_network(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + cloud = { + 'networks': [ + {'name': 'private'} + ] + } + result = c._fix_backwards_networks(cloud) + expected = { + 'networks': [ + {'name': 'private', 'routes_externally': False, + 'nat_destination': False, 'default_interface': False}, ] } self.assertEqual(expected, result) + + def test_single_default_interface(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + cloud = { + 'networks': [ + {'name': 'blue', 'default_interface': True}, + {'name': 'purple', 'default_interface': True}, + ] + } + self.assertRaises( + exceptions.OpenStackConfigException, + c._fix_backwards_networks, cloud) diff --git a/releasenotes/notes/network-list-e6e9dafdd8446263.yaml b/releasenotes/notes/network-list-e6e9dafdd8446263.yaml index 83754883a..8f793c2bc 100644 --- a/releasenotes/notes/network-list-e6e9dafdd8446263.yaml +++ b/releasenotes/notes/network-list-e6e9dafdd8446263.yaml @@ -3,9 +3,8 @@ features: - Support added for configuring metadata about networks for a cloud in a list of dicts, rather than in the external_network and internal_network entries. The dicts - support a name and a routes_externally field, as well as - any other arbitrary metadata needed by consuming - applications. + support a name, a routes_externally field, a nat_destination + field and a default_interface field. deprecations: - external_network and internal_network are deprecated and should be replaced with the list of network dicts. From fdb80ad04f5611ebc62b4c64f896448c9213a75c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 2 Apr 2016 09:47:52 -0500 Subject: [PATCH 0875/3836] Clarify one-per-cloud network values Make it clear in the docs that default_interface and nat_destination can each be set only once per cloud. Change-Id: Ic862b9f4dc31580c4e192f13f100428bbec7faa2 --- doc/source/network-config.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/source/network-config.rst b/doc/source/network-config.rst index 9bbbf9d7e..5a27b8e0e 100644 --- a/doc/source/network-config.rst +++ b/doc/source/network-config.rst @@ -38,10 +38,12 @@ one that programs should use. It defaults to false. An example of needing to use this value is a cloud with two private networks, and where a user is running ansible in one of the servers to talk to other servers on the private network. Because both networks are private, there would otherwise be no way -to determine which one should be used for the traffic. +to determine which one should be used for the traffic. There can only be one +`default_interface` per cloud. `nat_destination` is a boolean field that indicates which network floating ips should be attached to. It defaults to false. Normally this can be inferred by looking for a network that has subnets that have a gateway_ip. But it's possible to have more than one network that satisfies that condition, so the -user might want to tell programs which one to pick. +user might want to tell programs which one to pick. There can be only one +`nat_destination` per cloud. From 2ed2254879c2a704c091ead551d5d12a1cd7bda7 Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Mon, 14 Mar 2016 10:38:28 +1300 Subject: [PATCH 0876/3836] Use event_utils.poll_for_events for stack polling Calling get_stack to poll for stack state transitions causes unnecessary high load on heat servers so should be avoided if possible (this is true whether get_stack lists all stacks or a fetches a single full stack). Heatclient has a utility function which instead polls for stack events (with a fallback to fetching the stack when events are not forthcoming). This function is used extensively by heat client and the openstackclient stack commands - it would be appropriate to use it here too. The timeout is passed to the stack create call, meaning that the stack will go to CREATE_FAILED if the timeout is exceeded. The default timeout_mins is usually 60 minutes, so the client-side timeout would never be reached anyway. Also, the current polling approach was not filtering for CREATE_COMPLETE so it wasn't actually waiting for anything. This change adds functional tests which cover get_stack, create_stack and list_stacks. test_stack_nested exercises the stack_create environment_files file composition. Change-Id: Ia14d47f0f51e1f8825b6de6d8dc5a12335913f55 --- shade/openstackcloud.py | 18 ++-- shade/tests/functional/test_stack.py | 137 +++++++++++++++++++++++++++ shade/tests/unit/test_stack.py | 16 +++- 3 files changed, 156 insertions(+), 15 deletions(-) create mode 100644 shade/tests/functional/test_stack.py diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 0a4433191..c494057a7 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -30,6 +30,7 @@ import glanceclient import glanceclient.exc import heatclient.client +from heatclient.common import event_utils from heatclient.common import template_utils import keystoneauth1.exceptions import keystoneclient.client @@ -825,7 +826,7 @@ def create_stack( template_file=None, template_url=None, template_object=None, files=None, rollback=True, - wait=False, timeout=180, + wait=False, timeout=3600, environment_files=None, **parameters): envfiles, env = template_utils.process_multiple_environments_and_files( @@ -842,18 +843,15 @@ def create_stack( template=template, files=dict(list(tpl_files.items()) + list(envfiles.items())), environment=env, + timeout_mins=timeout // 60, ) with _utils.shade_exceptions("Error creating stack {name}".format( name=name)): - stack = self.manager.submitTask(_tasks.StackCreate(**params)) - if not wait: - return stack - for count in _utils._iterate_timeout( - timeout, - "Timed out waiting for heat stack to finish"): - stack = self.get_stack(name) - if stack: - return stack + self.manager.submitTask(_tasks.StackCreate(**params)) + if wait: + event_utils.poll_for_events(self.heat_client, stack_name=name, + action='CREATE') + return self.get_stack(name) def delete_stack(self, name_or_id): """Delete a Heat Stack diff --git a/shade/tests/functional/test_stack.py b/shade/tests/functional/test_stack.py new file mode 100644 index 000000000..b4d5a09f4 --- /dev/null +++ b/shade/tests/functional/test_stack.py @@ -0,0 +1,137 @@ +# -*- 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. + +""" +test_stack +---------------------------------- + +Functional tests for `shade` stack methods. +""" + +import tempfile + +from shade import openstack_cloud +from shade.tests import base + +simple_template = '''heat_template_version: 2014-10-16 +parameters: + length: + type: number + default: 10 + +resources: + my_rand: + type: OS::Heat::RandomString + properties: + length: {get_param: length} +outputs: + rand: + value: + get_attr: [my_rand, value] +''' + +root_template = '''heat_template_version: 2014-10-16 +parameters: + length: + type: number + default: 10 + count: + type: number + default: 5 + +resources: + my_rands: + type: OS::Heat::ResourceGroup + properties: + count: {get_param: count} + resource_def: + type: My::Simple::Template + properties: + length: {get_param: length} +outputs: + rands: + value: + get_attr: [my_rands, attributes, rand] +''' + +environment = ''' +resource_registry: + My::Simple::Template: %s +''' + + +class TestStack(base.TestCase): + + def setUp(self): + super(TestStack, self).setUp() + self.cloud = openstack_cloud(cloud='devstack') + if not self.cloud.has_service('orchestration'): + self.skipTest('Orchestration service not supported by cloud') + + def _cleanup_stack(self): + self.cloud.delete_stack(self.stack_name) + + def test_stack_simple(self): + test_template = tempfile.NamedTemporaryFile(delete=False) + test_template.write(simple_template) + test_template.close() + self.stack_name = self.getUniqueString('simple_stack') + self.addCleanup(self._cleanup_stack) + stack = self.cloud.create_stack(name=self.stack_name, + template_file=test_template.name, + wait=True) + + # assert expected values in stack + self.assertEqual('CREATE_COMPLETE', stack['stack_status']) + rand = stack['outputs'][0]['output_value'] + self.assertEqual(10, len(rand)) + + # assert get_stack matches returned create_stack + stack = self.cloud.get_stack(self.stack_name) + self.assertEqual('CREATE_COMPLETE', stack['stack_status']) + self.assertEqual(rand, stack['outputs'][0]['output_value']) + + # assert stack is in list_stacks + stacks = self.cloud.list_stacks() + stack_ids = [s['id'] for s in stacks] + self.assertIn(stack['id'], stack_ids) + + def test_stack_nested(self): + + test_template = tempfile.NamedTemporaryFile( + suffix='.yaml', delete=False) + test_template.write(root_template) + test_template.close() + + simple_tmpl = tempfile.NamedTemporaryFile(suffix='.yaml', delete=False) + simple_tmpl.write(simple_template) + simple_tmpl.close() + + env = tempfile.NamedTemporaryFile(suffix='.yaml', delete=False) + env.write(environment % simple_tmpl.name) + env.close() + + self.stack_name = self.getUniqueString('nested_stack') + self.addCleanup(self._cleanup_stack) + stack = self.cloud.create_stack(name=self.stack_name, + template_file=test_template.name, + environment_files=[env.name], + wait=True) + + # assert expected values in stack + self.assertEqual('CREATE_COMPLETE', stack['stack_status']) + rands = stack['outputs'][0]['output_value'] + self.assertEqual(['0', '1', '2', '3', '4'], sorted(rands.keys())) + for rand in rands.values(): + self.assertEqual(10, len(rand)) diff --git a/shade/tests/unit/test_stack.py b/shade/tests/unit/test_stack.py index 157a48ec2..383523bff 100644 --- a/shade/tests/unit/test_stack.py +++ b/shade/tests/unit/test_stack.py @@ -16,6 +16,7 @@ import mock import testtools +from heatclient.common import event_utils from heatclient.common import template_utils import shade @@ -119,16 +120,19 @@ def test_create_stack(self, mock_heat, mock_template): environment={}, parameters={}, template={}, - files={} + files={}, + timeout_mins=60, ) + @mock.patch.object(event_utils, 'poll_for_events') @mock.patch.object(template_utils, 'get_template_contents') @mock.patch.object(shade.OpenStackCloud, 'get_stack') @mock.patch.object(shade.OpenStackCloud, 'heat_client') - def test_create_stack_wait(self, mock_heat, mock_get, mock_template): + def test_create_stack_wait(self, mock_heat, mock_get, mock_template, + mock_poll): stack = {'id': 'stack_id', 'name': 'stack_name'} mock_template.return_value = ({}, {}) - mock_get.side_effect = iter([None, stack]) + mock_get.return_value = stack ret = self.cloud.create_stack('stack_name', wait=True) self.assertTrue(mock_template.called) mock_heat.stacks.create.assert_called_once_with( @@ -137,9 +141,11 @@ def test_create_stack_wait(self, mock_heat, mock_get, mock_template): environment={}, parameters={}, template={}, - files={} + files={}, + timeout_mins=60, ) - self.assertEqual(2, mock_get.call_count) + self.assertEqual(1, mock_get.call_count) + self.assertEqual(1, mock_poll.call_count) self.assertEqual(stack, ret) @mock.patch.object(shade.OpenStackCloud, 'heat_client') From bceedbce3f85698ac4b485b6f87f6e0e3a23474f Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Mon, 4 Apr 2016 09:00:36 +1200 Subject: [PATCH 0877/3836] Wrap stack operations in a heat_exceptions Heat exceptions give strings useful to the user (such as validation errors for their templates) so we need a custom exceptions wrapper to include those in the raised exception. Also the message for delete_stack exceptions now includes the name_or_id that the user passed in, which should have more meaning for them than the stack ID. Change-Id: I0bf0b9b249a311f76457115a5a7d2392244343f8 --- shade/_utils.py | 13 +++++++++++++ shade/openstackcloud.py | 14 +++++++++----- shade/tests/functional/test_stack.py | 13 +++++++++++++ shade/tests/unit/test_stack.py | 4 ++-- 4 files changed, 37 insertions(+), 7 deletions(-) diff --git a/shade/_utils.py b/shade/_utils.py index 66f3138cc..ccc13b5f1 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -21,6 +21,7 @@ import time from decorator import decorator +from heatclient import exc as heat_exc from neutronclient.common import exceptions as neutron_exc from shade import _log @@ -534,6 +535,18 @@ def invalidate(obj, *args, **kwargs): return _inner_cache_on_arguments +@contextlib.contextmanager +def heat_exceptions(error_message): + try: + yield + except heat_exc.NotFound as e: + raise exc.OpenStackCloudResourceNotFound( + "{msg}: {exc}".format(msg=error_message, exc=str(e))) + except Exception as e: + raise exc.OpenStackCloudException( + "{msg}: {exc}".format(msg=error_message, exc=str(e))) + + @contextlib.contextmanager def neutron_exceptions(error_message): try: diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index c494057a7..c8900384d 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -32,6 +32,7 @@ import heatclient.client from heatclient.common import event_utils from heatclient.common import template_utils +from heatclient import exc as heat_exceptions import keystoneauth1.exceptions import keystoneclient.client import neutronclient.neutron.client @@ -845,7 +846,7 @@ def create_stack( environment=env, timeout_mins=timeout // 60, ) - with _utils.shade_exceptions("Error creating stack {name}".format( + with _utils.heat_exceptions("Error creating stack {name}".format( name=name)): self.manager.submitTask(_tasks.StackCreate(**params)) if wait: @@ -868,8 +869,8 @@ def delete_stack(self, name_or_id): self.log.debug("Stack %s not found for deleting" % name_or_id) return False - with _utils.shade_exceptions("Failed to delete stack {id}".format( - id=stack['id'])): + with _utils.heat_exceptions("Failed to delete stack {id}".format( + id=name_or_id)): self.manager.submitTask(_tasks.StackDelete(id=stack['id'])) return True @@ -1774,8 +1775,11 @@ def search_one_stack(name_or_id=None, filters=None): # stack names are mandatory and enforced unique in the project # so a StackGet can always be used for name or ID. with _utils.shade_exceptions("Error fetching stack"): - stacks = [self.manager.submitTask( - _tasks.StackGet(stack_id=name_or_id))] + try: + stacks = [self.manager.submitTask( + _tasks.StackGet(stack_id=name_or_id))] + except heat_exceptions.NotFound: + return [] nstacks = _utils.normalize_stacks(stacks) return _utils._filter_list(nstacks, name_or_id, filters) diff --git a/shade/tests/functional/test_stack.py b/shade/tests/functional/test_stack.py index b4d5a09f4..2d08e3970 100644 --- a/shade/tests/functional/test_stack.py +++ b/shade/tests/functional/test_stack.py @@ -21,6 +21,7 @@ import tempfile +from shade import exc from shade import openstack_cloud from shade.tests import base @@ -70,6 +71,8 @@ My::Simple::Template: %s ''' +validate_template = '''heat_template_version: asdf-no-such-version ''' + class TestStack(base.TestCase): @@ -82,6 +85,16 @@ def setUp(self): def _cleanup_stack(self): self.cloud.delete_stack(self.stack_name) + def test_stack_validation(self): + test_template = tempfile.NamedTemporaryFile(delete=False) + test_template.write(validate_template) + test_template.close() + stack_name = self.getUniqueString('validate_template') + self.assertRaises(exc.OpenStackCloudException, + self.cloud.create_stack, + name=stack_name, + template_file=test_template.name) + def test_stack_simple(self): test_template = tempfile.NamedTemporaryFile(delete=False) test_template.write(simple_template) diff --git a/shade/tests/unit/test_stack.py b/shade/tests/unit/test_stack.py index 383523bff..d8b8250a0 100644 --- a/shade/tests/unit/test_stack.py +++ b/shade/tests/unit/test_stack.py @@ -101,10 +101,10 @@ def test_delete_stack_not_found(self, mock_heat, mock_get): def test_delete_stack_exception(self, mock_heat, mock_get): stack = {'id': 'stack_id', 'name': 'stack_name'} mock_get.return_value = stack - mock_heat.stacks.delete.side_effect = Exception() + mock_heat.stacks.delete.side_effect = Exception('ouch') with testtools.ExpectedException( shade.OpenStackCloudException, - "Failed to delete stack %s" % stack['id'] + "Failed to delete stack stack_name: ouch" ): self.cloud.delete_stack('stack_name') From aae0b15d2c5dfc400c9034a5c4cac61eaf0fc96d Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Wed, 6 Apr 2016 09:08:56 +0000 Subject: [PATCH 0878/3836] Fix search_domains when not passing filters I was getting exception "Failed to list domains (Inner Exception: ABCMeta object argument after ** must be a mapping, not NoneType) by just doing cloud.search_domains. Change-Id: Icd10ea3c9af8adb33ab5468193f3de713a521eaa --- shade/operatorcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index fef5967a4..212b21a59 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1142,7 +1142,7 @@ def list_domains(self): domains = self.manager.submitTask(_tasks.DomainList()) return _utils.normalize_domains(domains) - def search_domains(self, filters=None, name_or_id=None): + def search_domains(self, filters={}, name_or_id=None): """Search Keystone domains. :param name_or_id: domain name or id From 5605034fc38862b19e6d2d96aa222dafc7762060 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 5 Apr 2016 10:38:46 -0400 Subject: [PATCH 0879/3836] Pull the network settings from the actual dict Turns out self._openstack_config is not the config dict. self.config is. Also, return names. Change-Id: Ib2013e737b506b3a2acd7aa7b7884240c25384c5 --- os_client_config/cloud_config.py | 14 ++++++++------ os_client_config/tests/base.py | 26 ++++++++++++++++++++++++++ os_client_config/tests/test_config.py | 21 +++++++++++++++++++++ 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index e63bd127a..5dfbba929 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -442,23 +442,25 @@ def get_cache_resource_expiration(self, resource, default=None): def get_external_networks(self): """Get list of network names for external networks.""" return [ - net['name'] for net in self._openstack_config['networks'] + net['name'] for net in self.config['networks'] if net['routes_externally']] def get_internal_networks(self): """Get list of network names for internal networks.""" return [ - net['name'] for net in self._openstack_config['networks'] + net['name'] for net in self.config['networks'] if not net['routes_externally']] def get_default_network(self): """Get network used for default interactions.""" - for net in self._openstack_config['networks']: + for net in self.config['networks']: if net['default_interface']: - return net + return net['name'] + return None def get_nat_destination(self): """Get network used for NAT destination.""" - for net in self._openstack_config['networks']: + for net in self.config['networks']: if net['nat_destination']: - return net + return net['name'] + return None diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py index 9b784b157..d046a941f 100644 --- a/os_client_config/tests/base.py +++ b/os_client_config/tests/base.py @@ -90,6 +90,32 @@ }, 'region_name': 'test-region', }, + '_test-cloud-networks_': { + 'auth': { + 'username': 'testuser', + 'password': 'testpass', + 'project_id': 12345, + 'auth_url': 'http://example.com/v2', + 'domain_id': '6789', + 'project_domain_id': '123456789', + }, + 'networks': [{ + 'name': 'a-public', + 'routes_externally': True, + }, { + 'name': 'another-public', + 'routes_externally': True, + 'default_interface': True, + }, { + 'name': 'a-private', + 'routes_externally': False, + }, { + 'name': 'another-private', + 'routes_externally': False, + 'nat_destination': True, + }], + 'region_name': 'test-region', + }, '_test_cloud_regions': { 'auth': { 'username': 'testuser', diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 7c2bec0f5..a0978f059 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -187,6 +187,26 @@ def test_get_one_cloud_auth_merge(self): self.assertEqual('user', cc.auth['username']) self.assertEqual('testpass', cc.auth['password']) + def test_get_one_cloud_networks(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + cc = c.get_one_cloud('_test-cloud-networks_') + self.assertEqual( + ['a-public', 'another-public'], cc.get_external_networks()) + self.assertEqual( + ['a-private', 'another-private'], cc.get_internal_networks()) + self.assertEqual('another-private', cc.get_nat_destination()) + self.assertEqual('another-public', cc.get_default_network()) + + def test_get_one_cloud_no_networks(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + cc = c.get_one_cloud('_test-cloud-domain-scoped_') + self.assertEqual([], cc.get_external_networks()) + self.assertEqual([], cc.get_internal_networks()) + self.assertIsNone(cc.get_nat_destination()) + self.assertIsNone(cc.get_default_network()) + def test_only_secure_yaml(self): c = config.OpenStackConfig(config_files=['nonexistent'], vendor_files=['nonexistent'], @@ -201,6 +221,7 @@ def test_get_cloud_names(self): ['_test-cloud-domain-id_', '_test-cloud-domain-scoped_', '_test-cloud-int-project_', + '_test-cloud-networks_', '_test-cloud_', '_test-cloud_no_region', '_test_cloud_hyphenated', From 8601dd68f7e4949e94f9e6074657b0e36a146855 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 7 Apr 2016 09:40:06 -0400 Subject: [PATCH 0880/3836] Update func tests for latest devstack flavors The most recent devstack adds new flavors to the existing set, so we need to adjust some functional tests that are testing range searches across flavors. Note that we have a gate test that tests against stable/liberty and that devstack still has the original m1 flavors only. So we need to account for that in the functional tests. Change-Id: I98ce866686f3243ef2d2800b872a9840761fbe1a --- shade/tests/functional/test_range_search.py | 40 +++++++++++++++------ 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/shade/tests/functional/test_range_search.py b/shade/tests/functional/test_range_search.py index 032671714..f988c6b6b 100644 --- a/shade/tests/functional/test_range_search.py +++ b/shade/tests/functional/test_range_search.py @@ -21,6 +21,14 @@ class TestRangeSearch(base.BaseFunctionalTestCase): + def _filter_m1_flavors(self, results): + """The m1 flavors are the original devstack flavors""" + new_results = [] + for flavor in results: + if flavor['name'].startswith("m1."): + new_results.append(flavor) + return new_results + def test_range_search_bad_range(self): flavors = self.demo_cloud.list_flavors() self.assertRaises( @@ -31,6 +39,8 @@ def test_range_search_exact(self): flavors = self.demo_cloud.list_flavors() result = self.demo_cloud.range_search(flavors, {"ram": "4096"}) self.assertIsInstance(result, list) + # should only be 1 m1 flavor with 4096 ram + result = self._filter_m1_flavors(result) self.assertEqual(1, len(result)) self.assertEqual("m1.medium", result[0]['name']) @@ -39,7 +49,8 @@ def test_range_search_min(self): result = self.demo_cloud.range_search(flavors, {"ram": "MIN"}) self.assertIsInstance(result, list) self.assertEqual(1, len(result)) - self.assertEqual("m1.tiny", result[0]['name']) + # older devstack does not have cirros256 + self.assertTrue(result[0]['name'] in ('cirros256', 'm1.tiny')) def test_range_search_max(self): flavors = self.demo_cloud.list_flavors() @@ -50,17 +61,19 @@ def test_range_search_max(self): def test_range_search_lt(self): flavors = self.demo_cloud.list_flavors() - result = self.demo_cloud.range_search(flavors, {"ram": "<4096"}) + result = self.demo_cloud.range_search(flavors, {"ram": "<1024"}) self.assertIsInstance(result, list) - self.assertEqual(2, len(result)) - flavor_names = [r['name'] for r in result] - self.assertIn("m1.tiny", flavor_names) - self.assertIn("m1.small", flavor_names) + # should only be 1 m1 flavor with <1024 ram + result = self._filter_m1_flavors(result) + self.assertEqual(1, len(result)) + self.assertEqual("m1.tiny", result[0]['name']) def test_range_search_gt(self): flavors = self.demo_cloud.list_flavors() result = self.demo_cloud.range_search(flavors, {"ram": ">4096"}) self.assertIsInstance(result, list) + # should only be 2 m1 flavors with >4096 ram + result = self._filter_m1_flavors(result) self.assertEqual(2, len(result)) flavor_names = [r['name'] for r in result] self.assertIn("m1.large", flavor_names) @@ -70,6 +83,8 @@ def test_range_search_le(self): flavors = self.demo_cloud.list_flavors() result = self.demo_cloud.range_search(flavors, {"ram": "<=4096"}) self.assertIsInstance(result, list) + # should only be 3 m1 flavors with <=4096 ram + result = self._filter_m1_flavors(result) self.assertEqual(3, len(result)) flavor_names = [r['name'] for r in result] self.assertIn("m1.tiny", flavor_names) @@ -80,6 +95,8 @@ def test_range_search_ge(self): flavors = self.demo_cloud.list_flavors() result = self.demo_cloud.range_search(flavors, {"ram": ">=4096"}) self.assertIsInstance(result, list) + # should only be 3 m1 flavors with >=4096 ram + result = self._filter_m1_flavors(result) self.assertEqual(3, len(result)) flavor_names = [r['name'] for r in result] self.assertIn("m1.medium", flavor_names) @@ -92,24 +109,25 @@ def test_range_search_multi_1(self): flavors, {"ram": "MIN", "vcpus": "MIN"}) self.assertIsInstance(result, list) self.assertEqual(1, len(result)) - self.assertEqual("m1.tiny", result[0]['name']) + # older devstack does not have cirros256 + self.assertTrue(result[0]['name'] in ('cirros256', 'm1.tiny')) def test_range_search_multi_2(self): flavors = self.demo_cloud.list_flavors() result = self.demo_cloud.range_search( - flavors, {"ram": "<8192", "vcpus": "MIN"}) + flavors, {"ram": "<1024", "vcpus": "MIN"}) self.assertIsInstance(result, list) - self.assertEqual(2, len(result)) + result = self._filter_m1_flavors(result) + self.assertEqual(1, len(result)) flavor_names = [r['name'] for r in result] - # All of these should have 1 vcpu self.assertIn("m1.tiny", flavor_names) - self.assertIn("m1.small", flavor_names) def test_range_search_multi_3(self): flavors = self.demo_cloud.list_flavors() result = self.demo_cloud.range_search( flavors, {"ram": ">=4096", "vcpus": "<6"}) self.assertIsInstance(result, list) + result = self._filter_m1_flavors(result) self.assertEqual(2, len(result)) flavor_names = [r['name'] for r in result] self.assertIn("m1.medium", flavor_names) From d8966d556765331926af62870dac9a8816bf35e1 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 6 Apr 2016 09:53:23 -0400 Subject: [PATCH 0881/3836] Don't use singleton dicts unwittingly filters={} is dangerous because it creates a singleton re-usable dict that will collect data over time. The correct python pattern is to pass None as the parameter and then do if None: set to dict. Change-Id: I90b233dc2b5dc17cb8180aa5786865c0d98358f5 --- shade/operatorcloud.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 212b21a59..5395aafc2 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1142,7 +1142,7 @@ def list_domains(self): domains = self.manager.submitTask(_tasks.DomainList()) return _utils.normalize_domains(domains) - def search_domains(self, filters={}, name_or_id=None): + def search_domains(self, filters=None, name_or_id=None): """Search Keystone domains. :param name_or_id: domain name or id @@ -1158,6 +1158,8 @@ def search_domains(self, filters={}, name_or_id=None): :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ + if filters is None: + filters = {} if name_or_id is not None: domains = self.list_domains() return _utils._filter_list(domains, name_or_id, filters) From 19bbb7bbf46b52ac993eb408c36be1847309fd79 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 8 Apr 2016 14:20:52 -0500 Subject: [PATCH 0882/3836] Set min_segment_size from the swift capabilities Older swifts have a minimum limit on how small a segment can be in an Large Object. This is no longer needed in current OpenStack, but is important in old OpenStack. Change-Id: Iff83506d276b236c2ce3b078b3fbd9533c912b07 --- shade/openstackcloud.py | 4 ++++ shade/tests/unit/test_object.py | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 0a4433191..da27b8ce3 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3977,6 +3977,7 @@ def get_object_segment_size(self, segment_size): '''get a segment size that will work given capabilities''' if segment_size is None: segment_size = DEFAULT_OBJECT_SEGMENT_SIZE + min_segment_size = 0 try: caps = self.get_object_capabilities() except swift_exceptions.ClientException as e: @@ -3991,9 +3992,12 @@ def get_object_segment_size(self, segment_size): else: server_max_file_size = caps.get('swift', {}).get('max_file_size', 0) + min_segment_size = caps.get('slo', {}).get('min_segment_size', 0) if segment_size > server_max_file_size: return server_max_file_size + if segment_size < min_segment_size: + return min_segment_size return segment_size def is_object_stale( diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index 84c543c93..d5f7b22bb 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -64,8 +64,10 @@ def test_swift_service_no_endpoint(self, endpoint_mock): @mock.patch.object(shade.OpenStackCloud, 'swift_client') def test_get_object_segment_size(self, swift_mock): - swift_mock.get_capabilities.return_value = {'swift': - {'max_file_size': 1000}} + swift_mock.get_capabilities.return_value = { + 'swift': {'max_file_size': 1000}, + 'slo': {'min_segment_size': 500}} + self.assertEqual(500, self.cloud.get_object_segment_size(400)) self.assertEqual(900, self.cloud.get_object_segment_size(900)) self.assertEqual(1000, self.cloud.get_object_segment_size(1000)) self.assertEqual(1000, self.cloud.get_object_segment_size(1100)) From 719c064c6dc662c2c7b18fe15c7b74c40552c662 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 7 Apr 2016 20:09:03 -0400 Subject: [PATCH 0883/3836] Upload large objects as SLOs Swift has two type of Large Objects, Dynamic and Static. Static Objects are properly deleted when the manifest object is deleted ... so it's really the thing we want. Change-Id: Ifdc0c2699fd179f3b53a0e9905364bba201f6495 --- shade/openstackcloud.py | 8 +++++--- shade/tests/unit/test_caching.py | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index da27b8ce3..935b87030 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4074,9 +4074,11 @@ def create_object( upload = swiftclient.service.SwiftUploadObject( source=filename, object_name=name) for r in self.manager.submitTask(_tasks.ObjectCreate( - container=container, objects=[upload], - options=dict(header=header_list, - segment_size=segment_size))): + container=container, objects=[upload], + options=dict( + header=header_list, + segment_size=segment_size, + use_slo=True))): if not r['success']: raise OpenStackCloudException( 'Failed at action ({action}) [{error}]:'.format(**r)) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 284f1619c..32b30a289 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -472,11 +472,13 @@ class Container(object): container='image_upload_v2_test_container') args = {'header': ['x-object-meta-x-shade-md5:fake-md5', 'x-object-meta-x-shade-sha256:fake-sha256'], - 'segment_size': 1000} + 'segment_size': 1000, + 'use_slo': True} swift_service_mock.upload.assert_called_with( container='image_upload_v2_test_container', objects=mock.ANY, options=args) + glance_mock.tasks.create.assert_called_with(type='import', input={ 'import_from': 'image_upload_v2_test_container/name-99', 'image_properties': {'name': 'name-99'}}) From e0ad4ece18541db9fa2c9e43afc83eb41044b469 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 7 Apr 2016 20:38:37 -0400 Subject: [PATCH 0884/3836] Add option to control whether SLO or DLO is used It's possible someone wants to upload an object as a Dynamic Large Object, but the previous patch to switch to Static does so unconditionally. Add an option. Change-Id: I6571bff4123d2bafef4d2a6f46198665bb9c21ce --- shade/openstackcloud.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 935b87030..e66b23f11 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4032,6 +4032,7 @@ def is_object_stale( def create_object( self, container, name, filename=None, md5=None, sha256=None, segment_size=None, + use_slo=True, **headers): """Create a file object @@ -4050,6 +4051,10 @@ def create_object( a reasonable default. :param headers: These will be passed through to the object creation API as HTTP Headers. + :param use_slo: If the object is large enough to need to be a Large + Object, use a static rather than dyanmic object. Static Objects + will delete segment objects when the manifest object is deleted. + (optional, defaults to True) :raises: ``OpenStackCloudException`` on operation error. """ @@ -4078,7 +4083,7 @@ def create_object( options=dict( header=header_list, segment_size=segment_size, - use_slo=True))): + use_slo=use_slo))): if not r['success']: raise OpenStackCloudException( 'Failed at action ({action}) [{error}]:'.format(**r)) From 502a4bba86ad4e3b18bf81eff65a26d7233bed0c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 7 Apr 2016 21:04:50 -0400 Subject: [PATCH 0885/3836] Delete uploaded swift objects on image delete If shade auto-uploads swift objects for a glance upload, then shade should auto-delete them when it deletes the image. To that end, also add metadata to the image that records what swift container/object it uploads as. Change-Id: If422d33dafec58c805a0d2bb5b8746727c8761d9 --- shade/openstackcloud.py | 11 ++++++++++- shade/tests/unit/test_caching.py | 16 ++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index e66b23f11..a638db3af 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -52,6 +52,7 @@ OBJECT_SHA256_KEY = 'x-object-meta-x-shade-sha256' IMAGE_MD5_KEY = 'owner_specified.shade.md5' IMAGE_SHA256_KEY = 'owner_specified.shade.sha256' +IMAGE_OBJECT_KEY = 'owner_specified.shade.object' # Rackspace returns this for intermittent import errors IMAGE_ERROR_396 = "Image cannot be imported. Error code: '396'" DEFAULT_OBJECT_SEGMENT_SIZE = 1073741824 # 1GB @@ -2170,7 +2171,9 @@ def wait_for_image(self, image, timeout=3600): raise OpenStackCloudException( 'Image {image} hit error state'.format(image=image_id)) - def delete_image(self, name_or_id, wait=False, timeout=3600): + def delete_image( + self, name_or_id, wait=False, timeout=3600, + delete_objects=True): image = self.get_image(name_or_id) with _utils.shade_exceptions("Error in deleting image"): # Note that in v1, the param name is image, but in v2, @@ -2184,6 +2187,11 @@ def delete_image(self, name_or_id, wait=False, timeout=3600): _tasks.ImageDelete(image=image.id)) self.list_images.invalidate(self) + # Task API means an image was uploaded to swift + if self.image_api_use_tasks and IMAGE_OBJECT_KEY in image: + (container, objname) = image[IMAGE_OBJECT_KEY].split('/', 1) + self.delete_object(container=container, name=name) + if wait: for count in _utils._iterate_timeout( timeout, @@ -2280,6 +2288,7 @@ def create_image( return current_image kwargs[IMAGE_MD5_KEY] = md5 kwargs[IMAGE_SHA256_KEY] = sha256 + kwargs[IMAGE_OBJECT_KEY] = '/'.join([container, name]) if disable_vendor_agent: kwargs.update(self.cloud_config.config['disable_vendor_agent']) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 32b30a289..8626bd9fa 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -364,13 +364,12 @@ def test_list_images_caches_steady_status(self, glance_mock): # therefore we should _not_ expect to see the new one here self.assertEqual([first_image], self.cloud.list_images()) - def _call_create_image(self, name, container=None, **kwargs): + def _call_create_image(self, name, **kwargs): imagefile = tempfile.NamedTemporaryFile(delete=False) imagefile.write(b'\0') imagefile.close() self.cloud.create_image( - name, imagefile.name, container=container, wait=True, - is_public=False, **kwargs) + name, imagefile.name, wait=True, is_public=False, **kwargs) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, 'glance_client') @@ -385,9 +384,11 @@ def test_create_image_put_v1(self, glance_mock, mock_api_version): self._call_create_image('42 name') args = {'name': '42 name', 'container_format': 'bare', 'disk_format': 'qcow2', - 'properties': {'owner_specified.shade.md5': mock.ANY, - 'owner_specified.shade.sha256': mock.ANY, - 'is_public': False}} + 'properties': { + 'owner_specified.shade.md5': mock.ANY, + 'owner_specified.shade.sha256': mock.ANY, + 'owner_specified.shade.object': 'images/42 name', + 'is_public': False}} fake_image_dict = meta.obj_to_dict(fake_image) glance_mock.images.create.assert_called_with(**args) glance_mock.images.update.assert_called_with( @@ -411,6 +412,7 @@ def test_create_image_put_v2(self, glance_mock, mock_api_version): 'container_format': 'bare', 'disk_format': 'qcow2', 'owner_specified.shade.md5': mock.ANY, 'owner_specified.shade.sha256': mock.ANY, + 'owner_specified.shade.object': 'images/42 name', 'visibility': 'private', 'min_disk': 0, 'min_ram': 0} glance_mock.images.create.assert_called_with(**args) @@ -482,8 +484,10 @@ class Container(object): glance_mock.tasks.create.assert_called_with(type='import', input={ 'import_from': 'image_upload_v2_test_container/name-99', 'image_properties': {'name': 'name-99'}}) + object_path = 'image_upload_v2_test_container/name-99' args = {'owner_specified.shade.md5': fake_md5, 'owner_specified.shade.sha256': fake_sha256, + 'owner_specified.shade.object': object_path, 'image_id': 'a35e8afc-cae9-4e38-8441-2cd465f79f7b'} glance_mock.images.update.assert_called_with(**args) fake_image_dict = meta.obj_to_dict(fake_image) From d5e0745fc24eba031d185b18c41e2b0eabe2b716 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 7 Apr 2016 22:34:35 -0500 Subject: [PATCH 0886/3836] Delete image objects after failed upload In glance with PUT uploads, if the PUT fails, you're left with a non-working image shell. There is no point to that. Change-Id: Id47a10eab76f851202538c8b353288cb859656b8 --- shade/openstackcloud.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index a638db3af..96ef44734 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2338,15 +2338,28 @@ def _upload_image_put_v2(self, name, image_data, **image_kwargs): image_kwargs[k] = int(image_kwargs[k]) image = self.manager.submitTask(_tasks.ImageCreate( name=name, **image_kwargs)) - self.manager.submitTask(_tasks.ImageUpload( - image_id=image.id, image_data=image_data)) + try: + self.manager.submitTask(_tasks.ImageUpload( + image_id=image.id, image_data=image_data)) + except Exception: + self.log.debug("Deleting failed upload of image {name}".format( + image=image['name'])) + self.manager.submitTask(_tasks.ImageDelete(image_id=image.id)) + raise + return image def _upload_image_put_v1(self, name, image_data, **image_kwargs): image = self.manager.submitTask(_tasks.ImageCreate( name=name, **image_kwargs)) - self.manager.submitTask(_tasks.ImageUpdate( - image=image, data=image_data)) + try: + self.manager.submitTask(_tasks.ImageUpdate( + image=image, data=image_data)) + except Exception: + self.log.debug("Deleting failed upload of image {name}".format( + image=image['name'])) + self.manager.submitTask(_tasks.ImageDelete(image_id=image.id)) + raise return image def _upload_image_put(self, name, filename, **image_kwargs): From b4720972d16932d22e86239575d30915ea23e04b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 9 Apr 2016 09:28:50 -0500 Subject: [PATCH 0887/3836] Add release note about the swift Large Object changes We landed a few changes to swift support worthy of release notes, but forgot to add release notes. Ooops. Change-Id: I496f35cb01c3ee98fe102d1c63c2eaac306cfbd9 --- .../delete-image-objects-9d4b4e0fff36a23f.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 releasenotes/notes/delete-image-objects-9d4b4e0fff36a23f.yaml diff --git a/releasenotes/notes/delete-image-objects-9d4b4e0fff36a23f.yaml b/releasenotes/notes/delete-image-objects-9d4b4e0fff36a23f.yaml new file mode 100644 index 000000000..00ce4998d --- /dev/null +++ b/releasenotes/notes/delete-image-objects-9d4b4e0fff36a23f.yaml @@ -0,0 +1,18 @@ +--- +fixes: + - Delete swift objects uploaded in service of uploading images + at the time that the corresponding image is deleted. On some clouds, + image uploads are accomplished by uploading the image to swift and + then running a task-import. As shade does this action on behalf of the + user, it is not reasonable to assume that the user would then be aware + of or manage the swift objects shade created, which led to an ongoing + leak of swift objects. + - Upload swift Large Objects as Static Large Objects by default. Shade + automatically uploads objects as Large Objects when they are over a + segment_size threshold. It had been doing this as Dynamic Large Objects, + which sound great, but which have the downside of not deleting their + sub-segments when the primary object is deleted. Since nothing in the + shade interface exposes that the object was segmented, the user would not + know they would also need to find and delete the segments. Instead, we + now upload as Static Large Objects which behave as expected and delete + segments when the object is deleted. From d9f9c05bfb377f02b6150ca50030e088d2c19df5 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Mon, 11 Apr 2016 16:05:27 +1000 Subject: [PATCH 0888/3836] Add version string Use PBR to add an __version__ string to os-client-config. Change-Id: I2293b2bd0dbbe0108e805be8ba02fbba9a5ab064 --- os_client_config/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/os_client_config/__init__.py b/os_client_config/__init__.py index 6f78d2fca..be232fb68 100644 --- a/os_client_config/__init__.py +++ b/os_client_config/__init__.py @@ -14,10 +14,15 @@ import sys +import pbr.version + from os_client_config import cloud_config from os_client_config.config import OpenStackConfig # noqa +__version__ = pbr.version.VersionInfo('os_client_config').version_string() + + def get_config(service_key=None, options=None, **kwargs): config = OpenStackConfig() if options: From 1028f5ad7e21520f172c9c163e6b0ef66511bc13 Mon Sep 17 00:00:00 2001 From: Thomas Bechtold Date: Mon, 11 Apr 2016 10:30:24 +0200 Subject: [PATCH 0889/3836] Remove discover from test-requirements.txt discover is only needed for python 2.6 which is no longer supported. Change-Id: I8faeb05def94ac4adb2fe870fe141678dbd412ae --- test-requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 5e4c30446..0138f13f6 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,7 +7,6 @@ hacking>=0.10.2,<0.11 # Apache-2.0 coverage>=3.6 extras fixtures>=0.3.14 -discover jsonschema>=2.0.0,<3.0.0,!=2.5.0 mock>=1.2 python-glanceclient>=0.18.0 From 0f2c7fb89a46e564b23685fdcb26c704e6cfb0ba Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 11 Apr 2016 09:31:47 -0400 Subject: [PATCH 0890/3836] Correct error message when domain is required The domain is required with keystone v3 when creating either a user OR a project. The error message presented mentions only a user, so this can be confusing when attempting to create a project. Change-Id: I8d88d894f00b301d72494a80db1d97ee1e9c0856 --- shade/openstackcloud.py | 3 ++- shade/tests/unit/test_project.py | 3 ++- shade/tests/unit/test_users.py | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 8c2695566..7e9aa2394 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -372,7 +372,8 @@ def _get_domain_param_dict(self, domain_id): if self.cloud_config.get_api_version('identity') == '3': if not domain_id: raise OpenStackCloudException( - "User creation requires an explicit domain_id argument.") + "User or project creation requires an explicit" + " domain_id argument.") else: return {'domain': domain_id} else: diff --git a/shade/tests/unit/test_project.py b/shade/tests/unit/test_project.py index 55533607c..be350088d 100644 --- a/shade/tests/unit/test_project.py +++ b/shade/tests/unit/test_project.py @@ -62,7 +62,8 @@ def test_create_project_v3_no_domain(self, mock_keystone, mock_api_version.return_value = '3' with testtools.ExpectedException( shade.OpenStackCloudException, - "User creation requires an explicit domain_id argument." + "User or project creation requires an explicit" + " domain_id argument." ): self.cloud.create_project(name='foo', description='bar') diff --git a/shade/tests/unit/test_users.py b/shade/tests/unit/test_users.py index b75eb8c94..0304ed41f 100644 --- a/shade/tests/unit/test_users.py +++ b/shade/tests/unit/test_users.py @@ -101,7 +101,8 @@ def test_create_user_v3_no_domain(self, mock_keystone, mock_api_version): password = 'mice-rule' with testtools.ExpectedException( shade.OpenStackCloudException, - "User creation requires an explicit domain_id argument." + "User or project creation requires an explicit" + " domain_id argument." ): self.cloud.create_user(name=name, email=email, password=password) From 92dc7a20890db0cee0a17943274a12115d00852d Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 11 Apr 2016 10:15:05 -0400 Subject: [PATCH 0891/3836] Return boolean from delete_project Make delete_project conform to our standard delete APIs and return True when the delete succeeds, False when the project was not found for deleting. It would previously raise an exception during the attempt to delete. Also, add some missing functional tests for projects. Change-Id: Ie1773944b573c743a55fec202454968f1d813ec1 --- .../delete_project-399f9b3107014dde.yaml | 5 ++ shade/openstackcloud.py | 17 ++++ shade/tests/functional/test_project.py | 77 +++++++++++++++++++ shade/tests/unit/test_project.py | 4 +- 4 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/delete_project-399f9b3107014dde.yaml create mode 100644 shade/tests/functional/test_project.py diff --git a/releasenotes/notes/delete_project-399f9b3107014dde.yaml b/releasenotes/notes/delete_project-399f9b3107014dde.yaml new file mode 100644 index 000000000..e4cf39fb9 --- /dev/null +++ b/releasenotes/notes/delete_project-399f9b3107014dde.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - The delete_project() API now conforms to our standard of returning True + when the delete succeeds, or False when the project was not found. It + would previously raise an expection if the project was not found. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 76b3484f1..0d1818d3f 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -519,10 +519,25 @@ def create_project( return project def delete_project(self, name_or_id): + """Delete a project + + :param string name_or_id: Project name or id. + + :returns: True if delete succeeded, False if the project was not found. + + :raises: ``OpenStackCloudException`` if something goes wrong during + the openstack API call + """ + with _utils.shade_exceptions( "Error in deleting project {project}".format( project=name_or_id)): project = self.get_project(name_or_id) + if project is None: + self.log.debug( + "Project {0} not found for deleting".format(name_or_id)) + return False + params = {} if self.cloud_config.get_api_version('identity') == '3': params['project'] = project['id'] @@ -530,6 +545,8 @@ def delete_project(self, name_or_id): params['tenant'] = project['id'] self.manager.submitTask(_tasks.ProjectDelete(**params)) + return True + @_utils.cache_on_arguments() def list_users(self): """List Keystone Users. diff --git a/shade/tests/functional/test_project.py b/shade/tests/functional/test_project.py new file mode 100644 index 000000000..029b14895 --- /dev/null +++ b/shade/tests/functional/test_project.py @@ -0,0 +1,77 @@ +# Copyright (c) 2016 IBM +# +# 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_project +---------------------------------- + +Functional tests for `shade` project resource. +""" + +from shade.exc import OpenStackCloudException +from shade.tests.functional import base + + +class TestProject(base.BaseFunctionalTestCase): + + def setUp(self): + super(TestProject, self).setUp() + self.new_project_name = self.getUniqueString('project') + self.identity_version = \ + self.operator_cloud.cloud_config.get_api_version('identity') + self.addCleanup(self._cleanup_projects) + + def _cleanup_projects(self): + exception_list = list() + for p in self.operator_cloud.list_projects(): + if p['name'].startswith(self.new_project_name): + try: + self.operator_cloud.delete_project(p['id']) + except Exception as e: + exception_list.append(str(e)) + continue + if exception_list: + raise OpenStackCloudException('\n'.join(exception_list)) + + def test_create_project(self): + project_name = self.new_project_name + '_create' + + params = { + 'name': project_name, + 'description': 'test_create_project', + } + if self.identity_version == '3': + params['domain_id'] = \ + self.operator_cloud.get_domain('default')['id'] + + project = self.operator_cloud.create_project(**params) + + self.assertIsNotNone(project) + self.assertEqual(project_name, project['name']) + self.assertEqual('test_create_project', project['description']) + + def test_delete_project(self): + project_name = self.new_project_name + '_delete' + params = {'name': project_name} + if self.identity_version == '3': + params['domain_id'] = \ + self.operator_cloud.get_domain('default')['id'] + project = self.operator_cloud.create_project(**params) + self.assertIsNotNone(project) + self.assertTrue(self.operator_cloud.delete_project(project['id'])) + + def test_delete_project_not_found(self): + self.assertFalse(self.operator_cloud.delete_project('doesNotExist')) diff --git a/shade/tests/unit/test_project.py b/shade/tests/unit/test_project.py index 47d493734..dc1dd7935 100644 --- a/shade/tests/unit/test_project.py +++ b/shade/tests/unit/test_project.py @@ -73,7 +73,7 @@ def test_delete_project_v2(self, mock_keystone, mock_get, mock_api_version): mock_api_version.return_value = '2' mock_get.return_value = dict(id='123') - self.cloud.delete_project('123') + self.assertTrue(self.cloud.delete_project('123')) mock_get.assert_called_once_with('123') mock_keystone.tenants.delete.assert_called_once_with(tenant='123') @@ -84,7 +84,7 @@ def test_delete_project_v3(self, mock_keystone, mock_get, mock_api_version): mock_api_version.return_value = '3' mock_get.return_value = dict(id='123') - self.cloud.delete_project('123') + self.assertTrue(self.cloud.delete_project('123')) mock_get.assert_called_once_with('123') mock_keystone.projects.delete.assert_called_once_with(project='123') From 3f79e8db5387ce47dd02effb3ada4a51d465cf08 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 31 Mar 2016 16:12:15 -0500 Subject: [PATCH 0892/3836] Consume config values for NAT destination In a cloud that has multiple private networks and that requires floating IPs for public access, it's not possible to know which private network the floating IP should NAT to without the user providing some configuration. os-client-config added support for expressing such a thing in 1.17.0, so start consuming that. Change-Id: Iee634bcdc933a4c8d793ab9b25b27d9ecd77cdcc --- requirements.txt | 2 +- shade/openstackcloud.py | 252 ++++++++++++------- shade/tests/unit/test_floating_ip_neutron.py | 6 +- shade/tests/unit/test_meta.py | 41 ++- 4 files changed, 190 insertions(+), 111 deletions(-) diff --git a/requirements.txt b/requirements.txt index 47db714ea..bbe0083b1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ munch decorator jsonpatch ipaddress -os-client-config>=1.13.0 +os-client-config>=1.17.0 requestsexceptions>=1.1.1 six diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 8c2695566..aee062bdd 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -147,13 +147,13 @@ def __init__( self.secgroup_source = cloud_config.config['secgroup_source'] self.force_ipv4 = cloud_config.force_ipv4 - self._external_network_name_or_id = cloud_config.config.get( - 'external_network', None) + self._external_network_names = cloud_config.get_external_networks() + self._internal_network_names = cloud_config.get_internal_networks() + self._nat_destination = cloud_config.get_nat_destination() + self._default_network = cloud_config.get_default_network() + self._use_external_network = cloud_config.config.get( 'use_external_network', True) - - self._internal_network_name_or_id = cloud_config.config.get( - 'internal_network', None) self._use_internal_network = cloud_config.config.get( 'use_internal_network', True) @@ -1390,70 +1390,150 @@ def _reset_network_caches(self): with self._networks_lock: self._external_networks = [] self._internal_networks = [] - self._external_network_stamp = False - self._internal_network_stamp = False - - def _get_network( - self, - name_or_id, - use_network_func, - network_cache, - network_stamp, - filters): - if not use_network_func(): - return [] - if network_cache: - return network_cache - if network_stamp: - return [] - if not self.has_service('network'): - return [] - if name_or_id: - ext_net = self.get_network(name_or_id) - if not ext_net: - raise OpenStackCloudException( - "Network {network} was provided for external" - " access and that network could not be found".format( - network=name_or_id)) - else: - return [] - try: - # TODO(mordred): Rackspace exposes neutron but it does not - # work. I think that overriding what the service catalog - # reports should be a thing os-client-config should handle - # in a vendor profile - but for now it does not. That means - # this search_networks can just totally fail. If it does though, - # that's fine, clearly the neutron introspection is not going - # to work. - return self.search_networks(filters=filters) - except OpenStackCloudException: - pass - return [] + self._nat_destination_network = None + self._default_network_network = None + self._network_list_stamp = False - def get_external_networks(self): - """Return the networks that are configured to route northbound. - - :returns: A list of network dicts if one is found - """ + def _find_interesting_networks(self): if self._networks_lock.acquire(): try: - _all_networks = self._get_network( - self._external_network_name_or_id, - self.use_external_network, - self._external_networks, - self._external_network_stamp, - filters=None) + if self._network_list_stamp: + return + if (not self._use_external_network + and not self._use_internal_network): + # Both have been flagged as skip - don't do a list + return + if not self.has_service('network'): + return + + external_networks = [] + internal_networks = [] + nat_destination = None + default_network = None + # Filter locally because we have an or condition - _external_networks = [] - for network in _all_networks: - if (('router:external' in network + try: + # TODO(mordred): Rackspace exposes neutron but it does not + # work. I think that overriding what the service catalog + # reports should be a thing os-client-config should handle + # in a vendor profile - but for now it does not. That means + # this search_networks can just totally fail. If it does + # though, that's fine, clearly the neutron introspection is + # not going to work. + all_networks = self.list_networks() + except OpenStackCloudException: + self._network_list_stamp = True + return + + for network in all_networks: + # External networks + if (network['name'] in self._external_network_names + or network['id'] in self._external_network_names): + external_networks.append(network) + elif (('router:external' in network and network['router:external']) or 'provider:network_type' in network): - _external_networks.append(network) - self._external_networks = _external_networks - self._external_network_stamp = True + external_networks.append(network) + + # Internal networks + if (network['name'] in self._internal_network_names + or network['id'] in self._internal_network_names): + internal_networks.append(network) + elif (('router:external' in network + and not network['router:external']) and + 'provider:network_type' not in network): + internal_networks.append(network) + + # NAT Destination + if self._nat_destination in ( + network['name'], network['id']): + if nat_destination: + raise OpenStackCloudException( + 'Multiple networks were found matching' + ' {nat_net} which is the network configured' + ' to be the NAT destination. Please check your' + ' cloud resources. It is probably a good idea' + ' to configure this network by id rather than' + ' by name.'.format( + nat_net=self._nat_destination)) + nat_destination = network + + # Default network + if self._default_network in ( + network['name'], network['id']): + if default_network: + raise OpenStackCloudException( + 'Multiple networks were found matching' + ' {default_net} which is the network' + ' configured to be the default interface' + ' network. Please check your cloud resources.' + ' It is probably a good idea' + ' to configure this network by id rather than' + ' by name.'.format( + default_net=self._default_network)) + default_network = network + + if (self._external_network_names + and len(self._external_network_names) + != len(external_networks)): + raise OpenStackCloudException( + "Networks: {network} were provided for external" + " access and those networks could not be found".format( + network=",".join(self._external_network_names))) + + if (self._internal_network_names + and len(self._internal_network_names) + != len(internal_networks)): + raise OpenStackCloudException( + "Networks: {network} were provided for internal" + " access and those networks could not be found".format( + network=",".join(self._internal_network_names))) + + if self._nat_destination and not nat_destination: + raise OpenStackCloudException( + 'Network {network} was configured to be the' + ' destination for inbound NAT but it could not be' + ' found'.format( + network=self._nat_destination)) + + if self._default_network and not default_network: + raise OpenStackCloudException( + 'Network {network} was configured to be the' + ' default network interface but it could not be' + ' found'.format( + network=self._default_interface)) + + self._external_networks = external_networks + self._internal_networks = internal_networks + self._nat_destination_network = nat_destination + self._default_network_network = default_network + + self._network_list_stamp = True finally: self._networks_lock.release() + + def get_nat_destination(self): + """Return the network that is configured to be the NAT destination. + + :returns: A network dict if one is found + """ + self._find_interesting_networks() + return self._nat_destination_network + + def get_default_network(self): + """Return the network that is configured to be the default interface. + + :returns: A network dict if one is found + """ + self._find_interesting_networks() + return self._default_network_network + + def get_external_networks(self): + """Return the networks that are configured to route northbound. + + :returns: A list of network dicts if one is found + """ + self._find_interesting_networks() return self._external_networks def get_internal_networks(self): @@ -1461,25 +1541,7 @@ def get_internal_networks(self): :returns: A list of network dicts if one is found """ - # Just router:external False is not enough. - if self._networks_lock.acquire(): - try: - _all_networks = self._get_network( - self._internal_network_name_or_id, - self.use_internal_network, - self._internal_networks, - self._internal_network_stamp, - filters={ - 'router:external': False, - }) - _internal_networks = [] - for network in _all_networks: - if 'provider:network_type' not in network: - _internal_networks.append(network) - self._internal_networks = _internal_networks - self._internal_network_stamp = True - finally: - self._networks_lock.release() + self._find_interesting_networks() return self._internal_networks def get_keypair(self, name_or_id, filters=None): @@ -3240,11 +3302,31 @@ def _get_free_fixed_port(self, server, fixed_address=None): return (None, None) port = None if not fixed_address: - # We're assuming one, because we have no idea what to do with - # more than one. - # TODO(mordred) Fix this for real by allowing a configurable - # NAT destination setting - port = ports[0] + nat_network = self.get_nat_destination() + if len(ports) == 1 or not self._nat_destination: + port = ports[0] + if nat_network is None: + warnings.warn( + 'During Floating IP creation, multiple private' + ' networks were found. {net} is being selected at' + ' random to be the destination of the NAT. If that' + ' is not what you want, please configure the' + ' nat_destination property of the networks list in' + ' your clouds.yaml file. If you do not have a' + ' clouds.yaml file, please make one - your setup' + ' is complicated.'.format(net=port['network_id'])) + else: + for maybe_port in ports: + if maybe_port['network_id'] == nat_network['id']: + port = maybe_port + break + if not port: + raise OpenStackCloudException( + 'No port on server {server} was found matching the' + ' network configured as the NAT destination {dest}.' + ' Please check your config'.format( + server=server['id'], dest=nat_network['name'])) + # Select the first available IPv4 address for address in port.get('fixed_ips', list()): try: diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 30abb28ec..5b7ca7c29 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -357,14 +357,14 @@ def test_auto_ip_pool_no_reuse( @patch.object(OpenStackCloud, 'keystone_session') @patch.object(OpenStackCloud, '_neutron_create_floating_ip') @patch.object(OpenStackCloud, '_neutron_list_floating_ips') - @patch.object(OpenStackCloud, 'search_networks') + @patch.object(OpenStackCloud, 'list_networks') @patch.object(OpenStackCloud, 'has_service') def test_available_floating_ip_new( - self, mock_has_service, mock_search_networks, + self, mock_has_service, mock_list_networks, mock__neutron_list_floating_ips, mock__neutron_create_floating_ip, mock_keystone_session): mock_has_service.return_value = True - mock_search_networks.return_value = [self.mock_get_network_rep] + mock_list_networks.return_value = [self.mock_get_network_rep] mock__neutron_list_floating_ips.return_value = [] mock__neutron_create_floating_ip.return_value = \ self.mock_floating_ip_new_rep['floatingip'] diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 8f44d5b52..9397fe0ef 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -131,11 +131,11 @@ def test_get_server_ip(self): PUBLIC_V4, meta.get_server_ip(srv, ext_tag='floating')) @mock.patch.object(shade.OpenStackCloud, 'has_service') - @mock.patch.object(shade.OpenStackCloud, 'search_networks') + @mock.patch.object(shade.OpenStackCloud, 'list_networks') def test_get_server_private_ip( - self, mock_search_networks, mock_has_service): + self, mock_list_networks, mock_has_service): mock_has_service.return_value = True - mock_search_networks.return_value = [{ + mock_list_networks.return_value = [{ 'id': 'test-net-id', 'name': 'test-net-name' }] @@ -153,18 +153,16 @@ def test_get_server_private_ip( self.assertEqual( PRIVATE_V4, meta.get_server_private_ip(srv, self.cloud)) mock_has_service.assert_called_with('network') - mock_search_networks.assert_called_with( - filters={'router:external': False} - ) + mock_list_networks.assert_called_once_with() @mock.patch.object(shade.OpenStackCloud, 'list_server_security_groups') @mock.patch.object(shade.OpenStackCloud, 'get_volumes') @mock.patch.object(shade.OpenStackCloud, 'get_image_name') @mock.patch.object(shade.OpenStackCloud, 'get_flavor_name') @mock.patch.object(shade.OpenStackCloud, 'has_service') - @mock.patch.object(shade.OpenStackCloud, 'search_networks') + @mock.patch.object(shade.OpenStackCloud, 'list_networks') def test_get_server_private_ip_devstack( - self, mock_search_networks, mock_has_service, + self, mock_list_networks, mock_has_service, mock_get_flavor_name, mock_get_image_name, mock_get_volumes, mock_list_server_security_groups): @@ -172,10 +170,11 @@ def test_get_server_private_ip_devstack( mock_get_flavor_name.return_value = 'm1.tiny' mock_has_service.return_value = True mock_get_volumes.return_value = [] - mock_search_networks.return_value = [ + mock_list_networks.return_value = [ { 'id': 'test_pnztt_net', - 'name': 'test_pnztt_net' + 'name': 'test_pnztt_net', + 'router:external': False, }, { 'id': 'private', @@ -200,18 +199,16 @@ def test_get_server_private_ip_devstack( self.assertEqual(PRIVATE_V4, srv['private_v4']) mock_has_service.assert_called_with('volume') - mock_search_networks.assert_called_with( - filters={'router:external': False} - ) + mock_list_networks.assert_called_once_with() @mock.patch.object(shade.OpenStackCloud, 'has_service') - @mock.patch.object(shade.OpenStackCloud, 'search_networks') + @mock.patch.object(shade.OpenStackCloud, 'list_networks') def test_get_server_external_ipv4_neutron( - self, mock_search_networks, + self, mock_list_networks, mock_has_service): # Testing Clouds with Neutron mock_has_service.return_value = True - mock_search_networks.return_value = [{ + mock_list_networks.return_value = [{ 'id': 'test-net-id', 'name': 'test-net', 'router:external': True, @@ -228,13 +225,13 @@ def test_get_server_external_ipv4_neutron( self.assertEqual(PUBLIC_V4, ip) @mock.patch.object(shade.OpenStackCloud, 'has_service') - @mock.patch.object(shade.OpenStackCloud, 'search_networks') + @mock.patch.object(shade.OpenStackCloud, 'list_networks') def test_get_server_external_provider_ipv4_neutron( - self, mock_search_networks, + self, mock_list_networks, mock_has_service): # Testing Clouds with Neutron mock_has_service.return_value = True - mock_search_networks.return_value = [{ + mock_list_networks.return_value = [{ 'id': 'test-net-id', 'name': 'test-net', 'provider:network_type': 'vlan', @@ -251,13 +248,13 @@ def test_get_server_external_provider_ipv4_neutron( self.assertEqual(PUBLIC_V4, ip) @mock.patch.object(shade.OpenStackCloud, 'has_service') - @mock.patch.object(shade.OpenStackCloud, 'search_networks') + @mock.patch.object(shade.OpenStackCloud, 'list_networks') def test_get_server_external_none_ipv4_neutron( - self, mock_search_networks, + self, mock_list_networks, mock_has_service): # Testing Clouds with Neutron mock_has_service.return_value = True - mock_search_networks.return_value = [{ + mock_list_networks.return_value = [{ 'id': 'test-net-id', 'name': 'test-net', 'router:external': False, From 1368b7dc61be5534d27e6e2830f16352beb7a5fb Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 1 Apr 2016 13:04:24 -0500 Subject: [PATCH 0893/3836] Search subnets for gateway_ip to discover NAT dest The network that should be the target of the NAT of a floatingip should have a subnet that has a gateway_ip. Ignore for a moment the monumental insanity which is that data model ... that a network can have one or more subnets, and a network_id is what a port has and is how you describe what you want to attach to, but even with all of that the gateway_ip is the cogent property and it's needs to be on all of the subnets in the network. Just ignore that ... if you can. Since that is the world we live in, we can discover which network is the netowrk that has one or more subnets that has the gateway_ip set. We'll still fail if there are more than one network that has a subnet with gateway_ip set. But in that case, the config will take care of it. Change-Id: Ib947081a6b7839e1a6f00dcbfd924cc99f19d0fd --- shade/openstackcloud.py | 21 ++++++++++++++ shade/tests/unit/test_meta.py | 52 +++++++++++++++++++++++++++++------ 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index aee062bdd..0490fdd4a 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1411,6 +1411,8 @@ def _find_interesting_networks(self): nat_destination = None default_network = None + all_subnets = None + # Filter locally because we have an or condition try: # TODO(mordred): Rackspace exposes neutron but it does not @@ -1457,6 +1459,25 @@ def _find_interesting_networks(self): ' by name.'.format( nat_net=self._nat_destination)) nat_destination = network + elif self._nat_destination is None: + # TODO(mordred) need a config value for floating + # ips for this cloud so that we can skip this + # No configured nat destination, we have to figured + # it out. + if all_subnets is None: + try: + all_subnets = self.list_subnets() + except OpenStackCloudException: + # Thanks Rackspace broken neutron + all_subnets = [] + + for subnet in all_subnets: + # TODO(mordred) trap for detecting more than + # one network with a gateway_ip without a config + if ('gateway_ip' in subnet and subnet['gateway_ip'] + and network['id'] == subnet['network_id']): + nat_destination = network + break # Default network if self._default_network in ( diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 9397fe0ef..f8fd02e7b 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -82,6 +82,29 @@ def list_server_security_groups(self, server): accessIPv6='', ) +SUBNETS_WITH_NAT = [ + { + u'name': u'', + u'enable_dhcp': True, + u'network_id': u'5ef0358f-9403-4f7b-9151-376ca112abf7', + u'tenant_id': u'29c79f394b2946f1a0f8446d715dc301', + u'dns_nameservers': [], + u'ipv6_ra_mode': None, + u'allocation_pools': [ + { + u'start': u'10.10.10.2', + u'end': u'10.10.10.254' + } + ], + u'gateway_ip': u'10.10.10.1', + u'ipv6_address_mode': None, + u'ip_version': 4, + u'host_routes': [], + u'cidr': u'10.10.10.0/24', + u'id': u'14025a85-436e-4418-b0ee-f5b12a50f9b4' + }, +] + class TestMeta(base.TestCase): def test_find_nova_addresses_key_name(self): @@ -131,10 +154,12 @@ def test_get_server_ip(self): PUBLIC_V4, meta.get_server_ip(srv, ext_tag='floating')) @mock.patch.object(shade.OpenStackCloud, 'has_service') + @mock.patch.object(shade.OpenStackCloud, 'list_subnets') @mock.patch.object(shade.OpenStackCloud, 'list_networks') def test_get_server_private_ip( - self, mock_list_networks, mock_has_service): + self, mock_list_networks, mock_list_subnets, mock_has_service): mock_has_service.return_value = True + mock_list_subnets.return_value = SUBNETS_WITH_NAT mock_list_networks.return_value = [{ 'id': 'test-net-id', 'name': 'test-net-name' @@ -155,6 +180,7 @@ def test_get_server_private_ip( mock_has_service.assert_called_with('network') mock_list_networks.assert_called_once_with() + @mock.patch.object(shade.OpenStackCloud, 'list_subnets') @mock.patch.object(shade.OpenStackCloud, 'list_server_security_groups') @mock.patch.object(shade.OpenStackCloud, 'get_volumes') @mock.patch.object(shade.OpenStackCloud, 'get_image_name') @@ -165,11 +191,13 @@ def test_get_server_private_ip_devstack( self, mock_list_networks, mock_has_service, mock_get_flavor_name, mock_get_image_name, mock_get_volumes, - mock_list_server_security_groups): + mock_list_server_security_groups, + mock_list_subnets): mock_get_image_name.return_value = 'cirros-0.3.4-x86_64-uec' mock_get_flavor_name.return_value = 'm1.tiny' mock_has_service.return_value = True mock_get_volumes.return_value = [] + mock_list_subnets.return_value = SUBNETS_WITH_NAT mock_list_networks.return_value = [ { 'id': 'test_pnztt_net', @@ -202,12 +230,14 @@ def test_get_server_private_ip_devstack( mock_list_networks.assert_called_once_with() @mock.patch.object(shade.OpenStackCloud, 'has_service') + @mock.patch.object(shade.OpenStackCloud, 'list_subnets') @mock.patch.object(shade.OpenStackCloud, 'list_networks') def test_get_server_external_ipv4_neutron( - self, mock_list_networks, + self, mock_list_networks, mock_list_subnets, mock_has_service): # Testing Clouds with Neutron mock_has_service.return_value = True + mock_list_subnets.return_value = [] mock_list_networks.return_value = [{ 'id': 'test-net-id', 'name': 'test-net', @@ -225,12 +255,14 @@ def test_get_server_external_ipv4_neutron( self.assertEqual(PUBLIC_V4, ip) @mock.patch.object(shade.OpenStackCloud, 'has_service') + @mock.patch.object(shade.OpenStackCloud, 'list_subnets') @mock.patch.object(shade.OpenStackCloud, 'list_networks') def test_get_server_external_provider_ipv4_neutron( - self, mock_list_networks, + self, mock_list_networks, mock_list_subnets, mock_has_service): # Testing Clouds with Neutron mock_has_service.return_value = True + mock_list_subnets.return_value = SUBNETS_WITH_NAT mock_list_networks.return_value = [{ 'id': 'test-net-id', 'name': 'test-net', @@ -248,12 +280,14 @@ def test_get_server_external_provider_ipv4_neutron( self.assertEqual(PUBLIC_V4, ip) @mock.patch.object(shade.OpenStackCloud, 'has_service') + @mock.patch.object(shade.OpenStackCloud, 'list_subnets') @mock.patch.object(shade.OpenStackCloud, 'list_networks') def test_get_server_external_none_ipv4_neutron( - self, mock_list_networks, + self, mock_list_networks, mock_list_subnets, mock_has_service): # Testing Clouds with Neutron mock_has_service.return_value = True + mock_list_subnets.return_value = SUBNETS_WITH_NAT mock_list_networks.return_value = [{ 'id': 'test-net-id', 'name': 'test-net', @@ -287,16 +321,18 @@ def test_get_server_external_ipv4_neutron_accessIPv6(self): self.assertEqual(PUBLIC_V6, ip) @mock.patch.object(shade.OpenStackCloud, 'has_service') - @mock.patch.object(shade.OpenStackCloud, 'search_networks') + @mock.patch.object(shade.OpenStackCloud, 'list_subnets') + @mock.patch.object(shade.OpenStackCloud, 'list_networks') @mock.patch.object(shade.OpenStackCloud, 'search_ports') @mock.patch.object(meta, 'get_server_ip') def test_get_server_external_ipv4_neutron_exception( self, mock_get_server_ip, mock_search_ports, - mock_search_networks, + mock_list_networks, mock_list_subnets, mock_has_service): # Testing Clouds with a non working Neutron mock_has_service.return_value = True - mock_search_networks.return_value = [] + mock_list_subnets.return_value = [] + mock_list_networks.return_value = [] mock_search_ports.side_effect = neutron_exceptions.NotFound() mock_get_server_ip.return_value = PUBLIC_V4 From 5d42c845096f364e9a3357a4585d17228a18d72d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 4 Apr 2016 19:54:52 -0400 Subject: [PATCH 0894/3836] Refactor guts of _find_interesting_networks _find_interesting_networks is very sadly WAY too long and ugly. A symptom of that was the code being all squished against the 80 column line - but honestly the problem is that the code is so deep inside of conditionals that figuring out what's going on is a bit madenning. Split it out into another method so that the logic of whether we need to do things can be isolated from the logic of doing things. Change-Id: I4d5a30e3a5abd2b2f71e49dd18eab3eeb8f02f1d --- shade/openstackcloud.py | 249 ++++++++++++++++++++-------------------- 1 file changed, 125 insertions(+), 124 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 0490fdd4a..038ac5143 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1394,6 +1394,130 @@ def _reset_network_caches(self): self._default_network_network = None self._network_list_stamp = False + def _set_interesting_networks(self): + external_networks = [] + internal_networks = [] + nat_destination = None + default_network = None + + all_subnets = None + + # Filter locally because we have an or condition + try: + # TODO(mordred): Rackspace exposes neutron but it does not + # work. I think that overriding what the service catalog + # reports should be a thing os-client-config should handle + # in a vendor profile - but for now it does not. That means + # this search_networks can just totally fail. If it does + # though, that's fine, clearly the neutron introspection is + # not going to work. + all_networks = self.list_networks() + except OpenStackCloudException: + self._network_list_stamp = True + return + + for network in all_networks: + # External networks + if (network['name'] in self._external_network_names + or network['id'] in self._external_network_names): + external_networks.append(network) + elif (('router:external' in network + and network['router:external']) or + 'provider:network_type' in network): + external_networks.append(network) + + # Internal networks + if (network['name'] in self._internal_network_names + or network['id'] in self._internal_network_names): + internal_networks.append(network) + elif (('router:external' in network + and not network['router:external']) and + 'provider:network_type' not in network): + internal_networks.append(network) + + # NAT Destination + if self._nat_destination in ( + network['name'], network['id']): + if nat_destination: + raise OpenStackCloudException( + 'Multiple networks were found matching' + ' {nat_net} which is the network configured' + ' to be the NAT destination. Please check your' + ' cloud resources. It is probably a good idea' + ' to configure this network by id rather than' + ' by name.'.format( + nat_net=self._nat_destination)) + nat_destination = network + elif self._nat_destination is None: + # TODO(mordred) need a config value for floating + # ips for this cloud so that we can skip this + # No configured nat destination, we have to figured + # it out. + if all_subnets is None: + try: + all_subnets = self.list_subnets() + except OpenStackCloudException: + # Thanks Rackspace broken neutron + all_subnets = [] + + for subnet in all_subnets: + # TODO(mordred) trap for detecting more than + # one network with a gateway_ip without a config + if ('gateway_ip' in subnet and subnet['gateway_ip'] + and network['id'] == subnet['network_id']): + nat_destination = network + break + + # Default network + if self._default_network in ( + network['name'], network['id']): + if default_network: + raise OpenStackCloudException( + 'Multiple networks were found matching' + ' {default_net} which is the network' + ' configured to be the default interface' + ' network. Please check your cloud resources.' + ' It is probably a good idea' + ' to configure this network by id rather than' + ' by name.'.format( + default_net=self._default_network)) + default_network = network + + if (self._external_network_names + and len(self._external_network_names) + != len(external_networks)): + raise OpenStackCloudException( + "Networks: {network} were provided for external" + " access and those networks could not be found".format( + network=",".join(self._external_network_names))) + + if (self._internal_network_names + and len(self._internal_network_names) + != len(internal_networks)): + raise OpenStackCloudException( + "Networks: {network} were provided for internal" + " access and those networks could not be found".format( + network=",".join(self._internal_network_names))) + + if self._nat_destination and not nat_destination: + raise OpenStackCloudException( + 'Network {network} was configured to be the' + ' destination for inbound NAT but it could not be' + ' found'.format( + network=self._nat_destination)) + + if self._default_network and not default_network: + raise OpenStackCloudException( + 'Network {network} was configured to be the' + ' default network interface but it could not be' + ' found'.format( + network=self._default_interface)) + + self._external_networks = external_networks + self._internal_networks = internal_networks + self._nat_destination_network = nat_destination + self._default_network_network = default_network + def _find_interesting_networks(self): if self._networks_lock.acquire(): try: @@ -1405,130 +1529,7 @@ def _find_interesting_networks(self): return if not self.has_service('network'): return - - external_networks = [] - internal_networks = [] - nat_destination = None - default_network = None - - all_subnets = None - - # Filter locally because we have an or condition - try: - # TODO(mordred): Rackspace exposes neutron but it does not - # work. I think that overriding what the service catalog - # reports should be a thing os-client-config should handle - # in a vendor profile - but for now it does not. That means - # this search_networks can just totally fail. If it does - # though, that's fine, clearly the neutron introspection is - # not going to work. - all_networks = self.list_networks() - except OpenStackCloudException: - self._network_list_stamp = True - return - - for network in all_networks: - # External networks - if (network['name'] in self._external_network_names - or network['id'] in self._external_network_names): - external_networks.append(network) - elif (('router:external' in network - and network['router:external']) or - 'provider:network_type' in network): - external_networks.append(network) - - # Internal networks - if (network['name'] in self._internal_network_names - or network['id'] in self._internal_network_names): - internal_networks.append(network) - elif (('router:external' in network - and not network['router:external']) and - 'provider:network_type' not in network): - internal_networks.append(network) - - # NAT Destination - if self._nat_destination in ( - network['name'], network['id']): - if nat_destination: - raise OpenStackCloudException( - 'Multiple networks were found matching' - ' {nat_net} which is the network configured' - ' to be the NAT destination. Please check your' - ' cloud resources. It is probably a good idea' - ' to configure this network by id rather than' - ' by name.'.format( - nat_net=self._nat_destination)) - nat_destination = network - elif self._nat_destination is None: - # TODO(mordred) need a config value for floating - # ips for this cloud so that we can skip this - # No configured nat destination, we have to figured - # it out. - if all_subnets is None: - try: - all_subnets = self.list_subnets() - except OpenStackCloudException: - # Thanks Rackspace broken neutron - all_subnets = [] - - for subnet in all_subnets: - # TODO(mordred) trap for detecting more than - # one network with a gateway_ip without a config - if ('gateway_ip' in subnet and subnet['gateway_ip'] - and network['id'] == subnet['network_id']): - nat_destination = network - break - - # Default network - if self._default_network in ( - network['name'], network['id']): - if default_network: - raise OpenStackCloudException( - 'Multiple networks were found matching' - ' {default_net} which is the network' - ' configured to be the default interface' - ' network. Please check your cloud resources.' - ' It is probably a good idea' - ' to configure this network by id rather than' - ' by name.'.format( - default_net=self._default_network)) - default_network = network - - if (self._external_network_names - and len(self._external_network_names) - != len(external_networks)): - raise OpenStackCloudException( - "Networks: {network} were provided for external" - " access and those networks could not be found".format( - network=",".join(self._external_network_names))) - - if (self._internal_network_names - and len(self._internal_network_names) - != len(internal_networks)): - raise OpenStackCloudException( - "Networks: {network} were provided for internal" - " access and those networks could not be found".format( - network=",".join(self._internal_network_names))) - - if self._nat_destination and not nat_destination: - raise OpenStackCloudException( - 'Network {network} was configured to be the' - ' destination for inbound NAT but it could not be' - ' found'.format( - network=self._nat_destination)) - - if self._default_network and not default_network: - raise OpenStackCloudException( - 'Network {network} was configured to be the' - ' default network interface but it could not be' - ' found'.format( - network=self._default_interface)) - - self._external_networks = external_networks - self._internal_networks = internal_networks - self._nat_destination_network = nat_destination - self._default_network_network = default_network - + self._set_interesting_networks() self._network_list_stamp = True finally: self._networks_lock.release() From d9fc340c61aa7439057340249535e934fc6106e8 Mon Sep 17 00:00:00 2001 From: Arie Bregman Date: Wed, 6 Apr 2016 11:50:50 +0300 Subject: [PATCH 0895/3836] Add nat_destination filter to floating IP creation In Ansible 1.9.x 'quantum_floating_ip' module you have the option to specify 'internal_network_name'. This allows you to set the name of the network of the port to associate with the floating ip. In Ansible 2.x 'os_floating_ip' you can't do that. This change should bring the 'internal_network_name' option to be supported for Ansible 2.x version. Change-Id: I2c04a1feb5268a3621e35ed41a6af16622509325 --- shade/openstackcloud.py | 65 +++++++++++++++----- shade/tests/unit/test_floating_ip_common.py | 3 +- shade/tests/unit/test_floating_ip_neutron.py | 3 +- 3 files changed, 53 insertions(+), 18 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 038ac5143..03661c0f3 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3128,12 +3128,19 @@ def _nova_available_floating_ips(self, pool=None): return [f_ip] - def create_floating_ip(self, network=None, server=None): + def create_floating_ip(self, network=None, server=None, + fixed_address=None, nat_destination=None): """Allocate a new floating IP from a network or a pool. - :param network: Nova pool name or Neutron network name or id. + :param network: Nova pool name or Neutron network name or id + that the floating IP should come from. :param server: (optional) Server dict for the server to create - the IP for and to which it should be attached + the IP for and to which it should be attached. + :param fixed_address: (optional) Fixed IP to attach the floating + ip to. + :param nat_destination: (optional) Name or id of the network + that the fixed IP to attach the floating + IP to should be on. :returns: a floating IP address @@ -3143,7 +3150,9 @@ def create_floating_ip(self, network=None, server=None): try: f_ips = _utils.normalize_neutron_floating_ips( [self._neutron_create_floating_ip( - network_name_or_id=network, server=server)] + network_name_or_id=network, server=server, + fixed_address=fixed_address, + nat_destination=nat_destination)] ) return f_ips[0] except OpenStackCloudURINotFound as e: @@ -3158,7 +3167,8 @@ def create_floating_ip(self, network=None, server=None): return f_ips[0] def _neutron_create_floating_ip( - self, network_name_or_id=None, server=None): + self, network_name_or_id=None, server=None, + fixed_address=None, nat_destination=None): with _utils.neutron_exceptions( "unable to create floating IP for net " "{0}".format(network_name_or_id)): @@ -3178,10 +3188,12 @@ def _neutron_create_floating_ip( 'floating_network_id': networks[0]['id'], } if server: - (port, fixed_address) = self._get_free_fixed_port(server) + (port, fixed_ip_address) = self._get_free_fixed_port( + server, fixed_address=fixed_address, + nat_destination=nat_destination) if port: kwargs['port_id'] = port['id'] - kwargs['fixed_ip_address'] = fixed_address + kwargs['fixed_ip_address'] = fixed_ip_address return self.manager.submitTask(_tasks.NeutronFloatingIPCreate( body={'floatingip': kwargs}))['floatingip'] @@ -3303,7 +3315,14 @@ def _attach_ip_to_server( return server return server - def _get_free_fixed_port(self, server, fixed_address=None): + def _get_free_fixed_port(self, server, fixed_address=None, + nat_destination=None): + """Returns server's port. + + :param server: Server dict. + :param fixed_address: Fixed ip address of the port + :param nat_destination: Name or ID of the network of the port. + """ # If we are caching port lists, we may not find the port for # our server if the list is old. Try for at least 2 cache # periods if that is the case. @@ -3316,7 +3335,8 @@ def _get_free_fixed_port(self, server, fixed_address=None): "Timeout waiting for port to show up in list", wait=self._PORT_AGE): try: - ports = self.search_ports(filters={'device_id': server['id']}) + port_filter = {'device_id': server['id']} + ports = self.search_ports(filters=port_filter) break except OpenStackCloudTimeout: ports = None @@ -3324,10 +3344,19 @@ def _get_free_fixed_port(self, server, fixed_address=None): return (None, None) port = None if not fixed_address: - nat_network = self.get_nat_destination() - if len(ports) == 1 or not self._nat_destination: + if nat_destination: + nat_network = self.get_network(nat_destination) + if not nat_network: + raise OpenStackCloudException( + 'NAT Destination {nat_destination} was configured' + ' but not found on the cloud. Please check your' + ' config and your cloud and try again.'.format( + nat_destination=nat_destination)) + else: + nat_network = self.get_nat_destination() + if len(ports) == 1 or not nat_network: port = ports[0] - if nat_network is None: + if len(ports) > 1 and not nat_network: warnings.warn( 'During Floating IP creation, multiple private' ' networks were found. {net} is being selected at' @@ -3474,7 +3503,7 @@ def _nova_detach_ip_from_server(self, server_id, floating_ip_id): def _add_ip_from_pool( self, server, network, fixed_address=None, reuse=True, - wait=False, timeout=60): + wait=False, timeout=60, nat_destination=None): """Add a floating IP to a sever from a given pool This method reuses available IPs, when possible, or allocate new IPs @@ -3490,13 +3519,16 @@ def _add_ip_from_pool( to the server in Nova. Defaults to False. :param timeout: (optional) Seconds to wait, defaults to 60. See the ``wait`` parameter. + :param nat_destination: (optional) the name of the network of the + port to associate with the floating ip. :returns: the update server dict """ if reuse: f_ip = self.available_floating_ip(network=network) else: - f_ip = self.create_floating_ip(network=network) + f_ip = self.create_floating_ip( + network=network, nat_destination=nat_destination) return self._attach_ip_to_server( server=server, floating_ip=f_ip, fixed_address=fixed_address, @@ -3573,11 +3605,12 @@ def _add_auto_ip(self, server, wait=False, timeout=60, reuse=True): def add_ips_to_server( self, server, auto_ip=True, ips=None, ip_pool=None, - wait=False, timeout=60, reuse=True, fixed_address=None): + wait=False, timeout=60, reuse=True, fixed_address=None, + nat_destination=None): if ip_pool: server = self._add_ip_from_pool( server, ip_pool, reuse=reuse, wait=wait, timeout=timeout, - fixed_address=fixed_address) + fixed_address=fixed_address, nat_destination=nat_destination) elif ips: server = self.add_ip_list( server, ips, wait=wait, timeout=timeout, diff --git a/shade/tests/unit/test_floating_ip_common.py b/shade/tests/unit/test_floating_ip_common.py index f54493171..e6e60ca5b 100644 --- a/shade/tests/unit/test_floating_ip_common.py +++ b/shade/tests/unit/test_floating_ip_common.py @@ -47,6 +47,7 @@ def test_add_auto_ip( floating_ip_dict = { "id": "this-is-a-floating-ip-id", "fixed_ip_address": None, + "internal_network": None, "floating_ip_address": "203.0.113.29", "network": "this-is-a-net-or-pool-id", "attached": False, @@ -77,7 +78,7 @@ def test_add_ips_to_server_pool( mock_add_ip_from_pool.assert_called_with( server_dict, pool, reuse=True, wait=False, timeout=60, - fixed_address=None) + fixed_address=None, nat_destination=None) @patch.object(OpenStackCloud, 'nova_client') @patch.object(OpenStackCloud, 'add_ip_list') diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 5b7ca7c29..9a6690908 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -349,7 +349,8 @@ def test_auto_ip_pool_no_reuse( dict(id='1234'), ip_pool='my-network', reuse=False) mock__neutron_create_floating_ip.assert_called_once_with( - network_name_or_id='my-network', server=None) + network_name_or_id='my-network', server=None, + fixed_address=None, nat_destination=None) mock_attach_ip_to_server.assert_called_once_with( server={'id': '1234'}, fixed_address=None, floating_ip=self.floating_ip, wait=False, timeout=60) From e734774d1c20a0c13998a9ab3d19a7a832b7c717 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 10 Apr 2016 13:40:15 -0500 Subject: [PATCH 0896/3836] Allow passing nat_destination to get_active_server One of the code paths in which you might want to declare the nat_destination is if you're calling get_active_server. Change-Id: Idefc9aae6108c2dec926909bbf8f5d72baac13de --- shade/openstackcloud.py | 17 ++++++++++++----- shade/tests/unit/test_create_server.py | 9 ++++++--- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 03661c0f3..fb8174d05 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3704,7 +3704,7 @@ def create_server( root_volume=None, terminate_volume=False, wait=False, timeout=180, reuse_ips=True, network=None, boot_from_volume=False, volume_size='50', - boot_volume=None, volumes=None, + boot_volume=None, volumes=None, nat_destination=None, **kwargs): """Create a virtual server instance. @@ -3777,6 +3777,10 @@ def create_server( volume from the image and use that. :param volume_size: When booting an image from volume, how big should the created volume be? Defaults to 50. + :param nat_destination: Which network should a created floating IP + be attached to, if it's not possible to + infer from the cloud's configuration. + (Optional, defaults to None) :returns: A dict representing the created server. :raises: OpenStackCloudException on operation error. """ @@ -3836,7 +3840,8 @@ def create_server( if wait: server = self.wait_for_server( server, auto_ip=auto_ip, ips=ips, ip_pool=ip_pool, - reuse=reuse_ips, timeout=timeout + reuse=reuse_ips, timeout=timeout, + nat_destination=nat_destination, ) server.adminPass = admin_pass @@ -3844,7 +3849,7 @@ def create_server( def wait_for_server( self, server, auto_ip=True, ips=None, ip_pool=None, - reuse=True, timeout=180): + reuse=True, timeout=180, nat_destination=None): """ Wait for a server to reach ACTIVE status. """ @@ -3876,14 +3881,15 @@ def wait_for_server( server = self.get_active_server( server=server, reuse=reuse, auto_ip=auto_ip, ips=ips, ip_pool=ip_pool, - wait=True, timeout=remaining_timeout) + wait=True, timeout=remaining_timeout, + nat_destination=nat_destination) if server is not None and server['status'] == 'ACTIVE': return server def get_active_server( self, server, auto_ip=True, ips=None, ip_pool=None, - reuse=True, wait=False, timeout=180): + reuse=True, wait=False, timeout=180, nat_destination=None): if server['status'] == 'ERROR': if 'fault' in server and 'message' in server['fault']: @@ -3899,6 +3905,7 @@ def get_active_server( if 'addresses' in server and server['addresses']: return self.add_ips_to_server( server, auto_ip, ips, ip_pool, reuse=reuse, + nat_destination=nat_destination, wait=wait, timeout=timeout) self.log.debug( diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index 66e415425..c03382f91 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -220,9 +220,11 @@ def test_wait_for_server(self, mock_get_server, mock_get_active_server): self.assertEqual(2, mock_get_active_server.call_count) mock_get_active_server.assert_has_calls([ mock.call(server=building_server, reuse=True, auto_ip=True, - ips=None, ip_pool=None, wait=True, timeout=mock.ANY), + ips=None, ip_pool=None, wait=True, timeout=mock.ANY, + nat_destination=None), mock.call(server=active_server, reuse=True, auto_ip=True, - ips=None, ip_pool=None, wait=True, timeout=mock.ANY), + ips=None, ip_pool=None, wait=True, timeout=mock.ANY, + nat_destination=None), ]) self.assertEqual('ACTIVE', server['status']) @@ -241,7 +243,8 @@ def test_create_server_wait(self, mock_nova, mock_wait): mock_wait.assert_called_once_with( fake_server, auto_ip=True, ips=None, - ip_pool=None, reuse=True, timeout=180 + ip_pool=None, reuse=True, timeout=180, + nat_destination=None, ) @patch('time.sleep') From af47ff174d1415b513387bc90359fea9797756a9 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 12 Apr 2016 17:17:47 -0500 Subject: [PATCH 0897/3836] Refactor the port search logic Trying to combine len(ports) and nat_network logic led to weird debugging. Split the two so that we don't try to search for a network to attach the fip to if there is only one port on the server. Change-Id: I1bc83e34a6f66cc5a8eb7ffa66f0288919210d4d --- shade/openstackcloud.py | 48 +++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index fb8174d05..05ea5acf8 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3344,19 +3344,32 @@ def _get_free_fixed_port(self, server, fixed_address=None, return (None, None) port = None if not fixed_address: - if nat_destination: - nat_network = self.get_network(nat_destination) - if not nat_network: - raise OpenStackCloudException( - 'NAT Destination {nat_destination} was configured' - ' but not found on the cloud. Please check your' - ' config and your cloud and try again.'.format( - nat_destination=nat_destination)) - else: - nat_network = self.get_nat_destination() - if len(ports) == 1 or not nat_network: + if len(ports) == 1: port = ports[0] - if len(ports) > 1 and not nat_network: + else: + if nat_destination: + nat_network = self.get_network(nat_destination) + if not nat_network: + raise OpenStackCloudException( + 'NAT Destination {nat_destination} was configured' + ' but not found on the cloud. Please check your' + ' config and your cloud and try again.'.format( + nat_destination=nat_destination)) + else: + nat_network = self.get_nat_destination() + + if nat_network: + for maybe_port in ports: + if maybe_port['network_id'] == nat_network['id']: + port = maybe_port + if not port: + raise OpenStackCloudException( + 'No port on server {server} was found matching' + ' the network configured as the NAT destination' + ' {dest}. Please check your config'.format( + server=server['id'], dest=nat_network['name'])) + else: + port = ports[0] warnings.warn( 'During Floating IP creation, multiple private' ' networks were found. {net} is being selected at' @@ -3366,17 +3379,6 @@ def _get_free_fixed_port(self, server, fixed_address=None, ' your clouds.yaml file. If you do not have a' ' clouds.yaml file, please make one - your setup' ' is complicated.'.format(net=port['network_id'])) - else: - for maybe_port in ports: - if maybe_port['network_id'] == nat_network['id']: - port = maybe_port - break - if not port: - raise OpenStackCloudException( - 'No port on server {server} was found matching the' - ' network configured as the NAT destination {dest}.' - ' Please check your config'.format( - server=server['id'], dest=nat_network['name'])) # Select the first available IPv4 address for address in port.get('fixed_ips', list()): From c6017bdc80b6a0efdcff8202dd7d37e4155f3733 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 12 Apr 2016 07:28:14 -0500 Subject: [PATCH 0898/3836] Honor default_network for interface_ip When we generate the interface_ip (which is used, amongst other things, by ansible dynamic inventory) we do our best to figure out based on config which of the IPs a server has should be the one that the user wants to be "the" interface. In the previous patches, we added support for the user configuring a "default_network" for one of their networks. If the user has done this, and the server has an IP on that network, we should use that network for interface_ip. Change-Id: I440916622f9dfe12f8865bb1841dd0932d3cd7d0 --- shade/meta.py | 61 ++++++++++++++++++++++++++++------- shade/tests/fakes.py | 2 ++ shade/tests/unit/test_meta.py | 2 ++ 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/shade/meta.py b/shade/meta.py index 7707c0378..a3bb663d5 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -181,6 +181,55 @@ def get_server_external_ipv6(server): return None +def get_server_default_ip(cloud, server): + """ Get the configured 'default' address + + It is possible in clouds.yaml to configure for a cloud a network that + is the 'default_interface'. This is the network that should be used + to talk to instances on the network. + + :param cloud: the cloud we're working with + :param server: the server dict from which we want to get the default + IPv4 address + :return: a string containing the IPv4 address or None + """ + ext_net = cloud.get_default_network() + if ext_net: + if (cloud._local_ipv6 and not cloud.force_ipv4): + # try 6 first, fall back to four + versions = [6, 4] + else: + versions = [4] + for version in versions: + ext_ip = get_server_ip( + server, key_name=ext_net['name'], version=version) + if ext_ip is not None: + return ext_ip + return None + + +def _get_interface_ip(cloud, server): + """ Get the interface IP for the server + + Interface IP is the IP that should be used for communicating with the + server. It is: + - the IP on the configured default_interface network + - if cloud.private, the private ip if it exists + - if the server has a public ip, the public ip + """ + default_ip = get_server_default_ip(cloud, server) + if default_ip: + return default_ip + + if cloud.private and server['private_v4']: + return server['private_v4'] + + if (server['public_v6'] and cloud._local_ipv6 and not cloud.force_ipv4): + return server['public_v6'] + else: + return server['public_v4'] + + def get_groups_from_server(cloud, server, server_vars): groups = [] @@ -242,17 +291,7 @@ def add_server_interfaces(cloud, server): server['public_v4'] = get_server_external_ipv4(cloud, server) or '' server['public_v6'] = get_server_external_ipv6(server) or '' server['private_v4'] = get_server_private_ip(server, cloud) or '' - interface_ip = None - if cloud.private and server['private_v4']: - interface_ip = server['private_v4'] - else: - if (server['public_v6'] and cloud._local_ipv6 - and not cloud.force_ipv4): - interface_ip = server['public_v6'] - else: - interface_ip = server['public_v4'] - if interface_ip: - server['interface_ip'] = interface_ip + server['interface_ip'] = _get_interface_ip(cloud, server) or '' # Some clouds do not set these, but they're a regular part of the Nova # server record. Since we know them, go ahead and set them. In the case diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index fd08c2424..ea196c624 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -87,6 +87,7 @@ def __init__( self, id, name, status, addresses=None, accessIPv4='', accessIPv6='', private_v4='', private_v6='', public_v4='', public_v6='', + interface_ip='', flavor=None, image=None, adminPass=None, metadata=None): self.id = id @@ -110,6 +111,7 @@ def __init__( self.public_v6 = public_v6 self.adminPass = adminPass self.metadata = metadata + self.interface_ip = interface_ip class FakeService(object): diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index f8fd02e7b..196461508 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -64,6 +64,8 @@ def get_external_networks(self): def list_server_security_groups(self, server): return [] + def get_default_network(self): + return None standard_fake_server = fakes.FakeServer( id='test-id-0', From f4aa49c171b9513ce87f6f3b7481e197daadcad7 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 12 Apr 2016 07:35:21 -0500 Subject: [PATCH 0899/3836] Consume floating_ip_source config value There are some cases where the cloud needs to use nova and not neutron to deal with floating ips. We already have a config value for it in OCC - maybe we should just use it. Change-Id: Icab91c5dd3fb024875ee110b3d0168b97dc8a5d0 --- shade/openstackcloud.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 05ea5acf8..c2ce3cb5a 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -152,6 +152,8 @@ def __init__( self._nat_destination = cloud_config.get_nat_destination() self._default_network = cloud_config.get_default_network() + self._floating_ip_source = cloud_config.config.get( + 'floating_ip_source').lower() self._use_external_network = cloud_config.config.get( 'use_external_network', True) self._use_internal_network = cloud_config.config.get( @@ -1566,6 +1568,9 @@ def get_internal_networks(self): self._find_interesting_networks() return self._internal_networks + def _use_nova_floating(self): + return self._floating_ip_source == 'nova' + def get_keypair(self, name_or_id, filters=None): """Get a keypair by name or ID. @@ -3003,7 +3008,7 @@ def available_floating_ip(self, network=None, server=None): :returns: a (normalized) structure with a floating IP address description. """ - if self.has_service('network'): + if self.has_service('network') and not self._use_nova_floating(): try: f_ips = _utils.normalize_neutron_floating_ips( self._neutron_available_floating_ips( @@ -3286,7 +3291,7 @@ def _attach_ip_to_server( if ext_ip == floating_ip['floating_ip_address']: return server - if self.has_service('network'): + if self.has_service('network') and not self._use_nova_floating(): if not skip_attach: try: self._neutron_attach_ip_to_server( From 362986ed2e4b4ed8523b421a34e3d52c8a577499 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 10 Apr 2016 13:43:17 -0500 Subject: [PATCH 0900/3836] Start stamping the has_service debug messages We don't need to report this every time the code hits it. Once is fine. Change-Id: I5eaf7abf0608b8491a9d29980bd2de1d2f7c5b84 --- shade/openstackcloud.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index c2ce3cb5a..284540af5 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -177,6 +177,8 @@ def __init__( # InsecureRequestWarning references a Warning class or is None warnings.filterwarnings('ignore', category=category) + self._disable_warnings = {} + self._servers = [] self._servers_time = 0 self._servers_lock = threading.Lock() @@ -930,9 +932,13 @@ def get_session_endpoint(self, service_key): def has_service(self, service_key): if not self.cloud_config.config.get('has_%s' % service_key, True): - self.log.debug( - "Disabling {service_key} entry in catalog per config".format( - service_key=service_key)) + # TODO(mordred) add a stamp here so that we only report this once + if not (service_key in self._disable_warnings + and self._disable_warnings[service_key]): + self.log.debug( + "Disabling {service_key} entry in catalog" + " per config".format(service_key=service_key)) + self._disable_warnings[service_key] = True return False try: endpoint = self.get_session_endpoint(service_key) From 590e6dda649c523bd20f36a867affe40a23b1d87 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 14 Apr 2016 19:01:10 -0500 Subject: [PATCH 0901/3836] Use configured overrides for internal/external Some clouds (hi internap) have very difficult to discover internal and external networks. They defy all of our other logic, but we have configuation for this purpose. However, we were lettting discovery override configuration. Boo on us. Change-Id: I62e34b1f0f32c19acbeccd11bc2ea38a2b119951 --- shade/openstackcloud.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 380e9ab16..45f563b48 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1447,9 +1447,11 @@ def _set_interesting_networks(self): if (network['name'] in self._external_network_names or network['id'] in self._external_network_names): external_networks.append(network) - elif (('router:external' in network + elif ((('router:external' in network and network['router:external']) or - 'provider:network_type' in network): + 'provider:network_type' in network) and + network['name'] not in self._internal_network_names and + network['id'] not in self._internal_network_names): external_networks.append(network) # Internal networks @@ -1458,7 +1460,8 @@ def _set_interesting_networks(self): internal_networks.append(network) elif (('router:external' in network and not network['router:external']) and - 'provider:network_type' not in network): + network['name'] not in self._external_network_names and + network['id'] not in self._external_network_names): internal_networks.append(network) # NAT Destination From 7c32cf607fa442549e4eb573a0fe62f541ed5316 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 16 Apr 2016 10:29:44 -0500 Subject: [PATCH 0902/3836] Honor floating_ip_source: nova everywhere We already added support for honoring the floating_ip_source flag, but we did not add it everywhere. This led to a situation where neutron would do the port attach and then nova would try to attach the IP. Whoops. Change-Id: I19adbd54e828753d9e1d5c810bc7f8ed7294c084 --- shade/openstackcloud.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 45f563b48..9f43fa2ef 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1381,7 +1381,7 @@ def list_floating_ips(self): :returns: A list of floating IP dicts. """ - if self.has_service('network'): + if self.has_service('network') and not self._use_nova_floating(): try: return _utils.normalize_neutron_floating_ips( self._neutron_list_floating_ips()) @@ -1540,7 +1540,7 @@ def _set_interesting_networks(self): 'Network {network} was configured to be the' ' default network interface but it could not be' ' found'.format( - network=self._default_interface)) + network=self._default_network)) self._external_networks = external_networks self._internal_networks = internal_networks @@ -1723,7 +1723,7 @@ def get_volume(self, name_or_id, filters=None): """ return _utils._get_entity(self.search_volumes, name_or_id, filters) - def get_flavor(self, name_or_id, filters=None): + def get_flavor(self, name_or_id, filters=None, get_extra=False): """Get a flavor by name or ID. :param name_or_id: Name or ID of the flavor. @@ -3178,7 +3178,7 @@ def create_floating_ip(self, network=None, server=None, :raises: ``OpenStackCloudException``, on operation error. """ - if self.has_service('network'): + if self.has_service('network') and not self._use_nova_floating(): try: f_ips = _utils.normalize_neutron_floating_ips( [self._neutron_create_floating_ip( @@ -3254,7 +3254,7 @@ def delete_floating_ip(self, floating_ip_id): :raises: ``OpenStackCloudException``, on operation error. """ - if self.has_service('network'): + if self.has_service('network') and not self._use_nova_floating(): try: return self._neutron_delete_floating_ip(floating_ip_id) except OpenStackCloudURINotFound as e: @@ -3486,7 +3486,7 @@ def detach_ip_from_server(self, server_id, floating_ip_id): :raises: ``OpenStackCloudException``, on operation error. """ - if self.has_service('network'): + if self.has_service('network') and not self._use_nova_floating(): try: return self._neutron_detach_ip_from_server( server_id=server_id, floating_ip_id=floating_ip_id) From 59a2afd188f12ed36133bc1d3d49b0a67a39047e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 16 Apr 2016 16:44:42 -0500 Subject: [PATCH 0903/3836] Remove get_extra parameter from get_flavor It snuck in in a previous patch and is not really a thing. Change-Id: I25f9d625f2491986769dfcb27aeecf94452b47b8 --- shade/openstackcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 9f43fa2ef..d0a27aff8 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1723,7 +1723,7 @@ def get_volume(self, name_or_id, filters=None): """ return _utils._get_entity(self.search_volumes, name_or_id, filters) - def get_flavor(self, name_or_id, filters=None, get_extra=False): + def get_flavor(self, name_or_id, filters=None): """Get a flavor by name or ID. :param name_or_id: Name or ID of the flavor. From e255657cd57b1ae74933cb7e3b4481bf451a8995 Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Fri, 11 Mar 2016 12:46:58 +0000 Subject: [PATCH 0904/3836] Add domain_id param to project operations A domain admin should be able to make project operations on the domain it manages, but for that it needs to specify the domain id. Change-Id: I3fdc72b7819206cd5effce26bda08cfe1d42d4e0 --- shade/_tasks.py | 2 +- shade/_utils.py | 4 ++-- shade/openstackcloud.py | 33 ++++++++++++++++++-------- shade/tests/functional/test_project.py | 19 +++++++++++++++ shade/tests/unit/test_project.py | 12 ++++++++-- 5 files changed, 55 insertions(+), 15 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 1ada75a21..304a1bb7c 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -64,7 +64,7 @@ def main(self, client): class ProjectList(task_manager.Task): def main(self, client): - return client._project_manager.list() + return client._project_manager.list(**self.args) class ProjectCreate(task_manager.Task): diff --git a/shade/_utils.py b/shade/_utils.py index ccc13b5f1..f65ca8819 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -118,7 +118,7 @@ def _dict_filter(f, d): return filtered -def _get_entity(func, name_or_id, filters): +def _get_entity(func, name_or_id, filters, **kwargs): """Return a single entity from the list returned by a given method. :param callable func: @@ -136,7 +136,7 @@ def _get_entity(func, name_or_id, filters): # object and just short-circuit return it. if hasattr(name_or_id, 'id'): return name_or_id - entities = func(name_or_id, filters) + entities = func(name_or_id, filters, **kwargs) if not entities: return None if len(entities) > 1: diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 380e9ab16..cd95a1839 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -442,53 +442,64 @@ def range_search(self, data, filters): return filtered @_utils.cache_on_arguments() - def list_projects(self): + def list_projects(self, domain_id=None): """List Keystone Projects. + :param string domain_id: domain id to scope the listed projects. + :returns: a list of dicts containing the project description. :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ try: - projects = self.manager.submitTask(_tasks.ProjectList()) + if self.cloud_config.get_api_version('identity') == '3': + projects = self.manager.submitTask( + _tasks.ProjectList(domain=domain_id)) + else: + projects = self.manager.submitTask( + _tasks.ProjectList()) except Exception as e: self.log.debug("Failed to list projects", exc_info=True) raise OpenStackCloudException(str(e)) return projects - def search_projects(self, name_or_id=None, filters=None): + def search_projects(self, name_or_id=None, filters=None, domain_id=None): """Seach Keystone projects. :param name: project name or id. :param filters: a dict containing additional filters to use. + :param domain_id: domain id to scope the searched projects. :returns: a list of dict containing the projects :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ - projects = self.list_projects() + projects = self.list_projects(domain_id=domain_id) return _utils._filter_list(projects, name_or_id, filters) - def get_project(self, name_or_id, filters=None): + def get_project(self, name_or_id, filters=None, domain_id=None): """Get exactly one Keystone project. :param id: project name or id. :param filters: a dict containing additional filters to use. + :param domain_id: domain id (keystone v3 only) :returns: a list of dicts containing the project description. :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ - return _utils._get_entity(self.search_projects, name_or_id, filters) + return _utils._get_entity(self.search_projects, name_or_id, filters, + domain_id=domain_id) - def update_project(self, name_or_id, description=None, enabled=True): + def update_project(self, name_or_id, description=None, enabled=True, + domain_id=None): with _utils.shade_exceptions( "Error in updating project {project}".format( project=name_or_id)): - proj = self.get_project(name_or_id) + proj = self.get_project(name_or_id, domain_id=domain_id) if not proj: raise OpenStackCloudException( "Project %s not found." % name_or_id) @@ -523,10 +534,12 @@ def create_project( self.list_projects.invalidate(self) return project - def delete_project(self, name_or_id): + def delete_project(self, name_or_id, domain_id=None): """Delete a project :param string name_or_id: Project name or id. + :param string domain_id: Domain id containing the project (keystone + v3 only). :returns: True if delete succeeded, False if the project was not found. @@ -537,7 +550,7 @@ def delete_project(self, name_or_id): with _utils.shade_exceptions( "Error in deleting project {project}".format( project=name_or_id)): - project = self.get_project(name_or_id) + project = self.get_project(name_or_id, domain_id=domain_id) if project is None: self.log.debug( "Project {0} not found for deleting".format(name_or_id)) diff --git a/shade/tests/functional/test_project.py b/shade/tests/functional/test_project.py index 029b14895..4e1e26d48 100644 --- a/shade/tests/functional/test_project.py +++ b/shade/tests/functional/test_project.py @@ -63,6 +63,25 @@ def test_create_project(self): self.assertEqual(project_name, project['name']) self.assertEqual('test_create_project', project['description']) + def test_update_project(self): + project_name = self.new_project_name + '_update' + + params = { + 'name': project_name, + 'description': 'test_update_project', + } + if self.identity_version == '3': + params['domain_id'] = \ + self.operator_cloud.get_domain('default')['id'] + + project = self.operator_cloud.create_project(**params) + updated_project = self.operator_cloud.update_project(project_name, + description='new') + self.assertIsNotNone(updated_project) + self.assertEqual(project['id'], updated_project['id']) + self.assertEqual(project['name'], updated_project['name']) + self.assertEqual(updated_project['description'], 'new') + def test_delete_project(self): project_name = self.new_project_name + '_delete' params = {'name': project_name} diff --git a/shade/tests/unit/test_project.py b/shade/tests/unit/test_project.py index a5507bc5e..6b8b1ad62 100644 --- a/shade/tests/unit/test_project.py +++ b/shade/tests/unit/test_project.py @@ -75,7 +75,7 @@ def test_delete_project_v2(self, mock_keystone, mock_get, mock_api_version.return_value = '2' mock_get.return_value = dict(id='123') self.assertTrue(self.cloud.delete_project('123')) - mock_get.assert_called_once_with('123') + mock_get.assert_called_once_with('123', domain_id=None) mock_keystone.tenants.delete.assert_called_once_with(tenant='123') @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @@ -86,7 +86,7 @@ def test_delete_project_v3(self, mock_keystone, mock_get, mock_api_version.return_value = '3' mock_get.return_value = dict(id='123') self.assertTrue(self.cloud.delete_project('123')) - mock_get.assert_called_once_with('123') + mock_get.assert_called_once_with('123', domain_id=None) mock_keystone.projects.delete.assert_called_once_with(project='123') @mock.patch.object(shade.OpenStackCloud, 'get_project') @@ -119,3 +119,11 @@ def test_update_project_v3(self, mock_keystone, mock_get_project, self.cloud.update_project('123', description='new', enabled=False) mock_keystone.projects.update.assert_called_once_with( description='new', enabled=False, project='123') + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_list_projects_v3(self, mock_keystone, mock_api_version): + mock_api_version.return_value = '3' + self.cloud.list_projects('123') + mock_keystone.projects.list.assert_called_once_with( + domain='123') From fba72070e040f8bce14a90566be33c4f5b0fca96 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 18 Apr 2016 16:11:59 -0400 Subject: [PATCH 0905/3836] Fix string formatting Some strings were being formatted incorrectly with incorrect argument names or numbers. Change-Id: Ic893a02e587334c8838f74d34ff383470b6149c8 --- shade/openstackcloud.py | 4 ++-- shade/operatorcloud.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index d0a27aff8..8c9eb86be 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2460,7 +2460,7 @@ def _upload_image_put_v2(self, name, image_data, **image_kwargs): self.manager.submitTask(_tasks.ImageUpload( image_id=image.id, image_data=image_data)) except Exception: - self.log.debug("Deleting failed upload of image {name}".format( + self.log.debug("Deleting failed upload of image {image}".format( image=image['name'])) self.manager.submitTask(_tasks.ImageDelete(image_id=image.id)) raise @@ -2474,7 +2474,7 @@ def _upload_image_put_v1(self, name, image_data, **image_kwargs): self.manager.submitTask(_tasks.ImageUpdate( image=image, data=image_data)) except Exception: - self.log.debug("Deleting failed upload of image {name}".format( + self.log.debug("Deleting failed upload of image {image}".format( image=image['name'])) self.manager.submitTask(_tasks.ImageDelete(image_id=image.id)) raise diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 5395aafc2..017ffd437 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1524,7 +1524,7 @@ def unset_flavor_specs(self, flavor_id, keys): _tasks.FlavorUnsetExtraSpecs(id=flavor_id, key=key)) except Exception as e: raise OpenStackCloudException( - "Unable to delete flavor spec {0}: {0}".format( + "Unable to delete flavor spec {0}: {1}".format( key, str(e))) def _mod_flavor_access(self, action, flavor_id, project_id): From 40b30a6178c0b2ea2cef84afc3a104adfa2f24a0 Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Mon, 4 Apr 2016 09:04:27 +1200 Subject: [PATCH 0906/3836] Implement update_stack This would definitely be desirable for shade users. Specifically an ansible module should to a update_stack if the stack already exists. If there are no changes the operation would be idempotent. Change-Id: I833027883a4eed11f4568760d44ffec87889021f --- .../notes/stack-update-5886e91fd6e423bf.yaml | 4 ++ shade/_tasks.py | 5 ++ shade/openstackcloud.py | 65 +++++++++++++++++++ shade/tests/functional/test_stack.py | 23 +++++++ shade/tests/unit/test_stack.py | 40 ++++++++++++ 5 files changed, 137 insertions(+) create mode 100644 releasenotes/notes/stack-update-5886e91fd6e423bf.yaml diff --git a/releasenotes/notes/stack-update-5886e91fd6e423bf.yaml b/releasenotes/notes/stack-update-5886e91fd6e423bf.yaml new file mode 100644 index 000000000..29a155236 --- /dev/null +++ b/releasenotes/notes/stack-update-5886e91fd6e423bf.yaml @@ -0,0 +1,4 @@ +--- +features: + - Implement update_stack to perform the update action on existing + orchestration stacks. diff --git a/shade/_tasks.py b/shade/_tasks.py index 1ada75a21..bd487ba02 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -764,6 +764,11 @@ def main(self, client): return client.heat_client.stacks.create(**self.args) +class StackUpdate(task_manager.Task): + def main(self, client): + return client.heat_client.stacks.update(**self.args) + + class StackDelete(task_manager.Task): def main(self, client): return client.heat_client.stacks.delete(self.args['id']) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index d0a27aff8..9ebc1a220 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -877,6 +877,71 @@ def create_stack( action='CREATE') return self.get_stack(name) + def update_stack( + self, name_or_id, + template_file=None, template_url=None, + template_object=None, files=None, + rollback=True, + wait=False, timeout=3600, + environment_files=None, + **parameters): + """Update a Heat Stack. + + :param string name_or_id: Name or id of the stack to update. + :param string template_file: Path to the template. + :param string template_url: URL of template. + :param string template_object: URL to retrieve template object. + :param dict files: dict of additional file content to include. + :param boolean rollback: Enable rollback on update failure. + :param boolean wait: Whether to wait for the delete to finish. + :param int timeout: Stack update timeout in seconds. + :param list environment_files: Paths to environment files to apply. + + Other arguments will be passed as stack parameters which will take + precedence over any parameters specified in the environments. + + Only one of template_file, template_url, template_object should be + specified. + + :returns: a dict containing the stack description + + :raises: ``OpenStackCloudException`` if something goes wrong during + the openstack API calls + """ + envfiles, env = template_utils.process_multiple_environments_and_files( + env_paths=environment_files) + tpl_files, template = template_utils.get_template_contents( + template_file=template_file, + template_url=template_url, + template_object=template_object, + files=files) + params = dict( + stack_id=name_or_id, + disable_rollback=not rollback, + parameters=parameters, + template=template, + files=dict(list(tpl_files.items()) + list(envfiles.items())), + environment=env, + timeout_mins=timeout // 60, + ) + if wait: + # find the last event to use as the marker + events = event_utils.get_events(self.heat_client, + name_or_id, + event_args={'sort_dir': 'desc', + 'limit': 1}) + marker = events[0].id if events else None + + with _utils.heat_exceptions("Error updating stack {name}".format( + name=name_or_id)): + self.manager.submitTask(_tasks.StackUpdate(**params)) + if wait: + event_utils.poll_for_events(self.heat_client, + name_or_id, + action='UPDATE', + marker=marker) + return self.get_stack(name_or_id) + def delete_stack(self, name_or_id): """Delete a Heat Stack diff --git a/shade/tests/functional/test_stack.py b/shade/tests/functional/test_stack.py index 2d08e3970..8f073b87d 100644 --- a/shade/tests/functional/test_stack.py +++ b/shade/tests/functional/test_stack.py @@ -120,6 +120,29 @@ def test_stack_simple(self): stack_ids = [s['id'] for s in stacks] self.assertIn(stack['id'], stack_ids) + # update with no changes + stack = self.cloud.update_stack(self.stack_name, + template_file=test_template.name, + wait=True) + + # assert no change in updated stack + self.assertEqual('UPDATE_COMPLETE', stack['stack_status']) + rand = stack['outputs'][0]['output_value'] + self.assertEqual(rand, stack['outputs'][0]['output_value']) + + # update with changes + stack = self.cloud.update_stack(self.stack_name, + template_file=test_template.name, + wait=True, + length=12) + + # assert changed output in updated stack + stack = self.cloud.get_stack(self.stack_name) + self.assertEqual('UPDATE_COMPLETE', stack['stack_status']) + new_rand = stack['outputs'][0]['output_value'] + self.assertNotEqual(rand, new_rand) + self.assertEqual(12, len(new_rand)) + def test_stack_nested(self): test_template = tempfile.NamedTemporaryFile( diff --git a/shade/tests/unit/test_stack.py b/shade/tests/unit/test_stack.py index d8b8250a0..e796e46eb 100644 --- a/shade/tests/unit/test_stack.py +++ b/shade/tests/unit/test_stack.py @@ -148,6 +148,46 @@ def test_create_stack_wait(self, mock_heat, mock_get, mock_template, self.assertEqual(1, mock_poll.call_count) self.assertEqual(stack, ret) + @mock.patch.object(template_utils, 'get_template_contents') + @mock.patch.object(shade.OpenStackCloud, 'heat_client') + def test_update_stack(self, mock_heat, mock_template): + mock_template.return_value = ({}, {}) + self.cloud.update_stack('stack_name') + self.assertTrue(mock_template.called) + mock_heat.stacks.update.assert_called_once_with( + stack_id='stack_name', + disable_rollback=False, + environment={}, + parameters={}, + template={}, + files={}, + timeout_mins=60, + ) + + @mock.patch.object(event_utils, 'poll_for_events') + @mock.patch.object(template_utils, 'get_template_contents') + @mock.patch.object(shade.OpenStackCloud, 'get_stack') + @mock.patch.object(shade.OpenStackCloud, 'heat_client') + def test_update_stack_wait(self, mock_heat, mock_get, mock_template, + mock_poll): + stack = {'id': 'stack_id', 'name': 'stack_name'} + mock_template.return_value = ({}, {}) + mock_get.return_value = stack + ret = self.cloud.update_stack('stack_name', wait=True) + self.assertTrue(mock_template.called) + mock_heat.stacks.update.assert_called_once_with( + stack_id='stack_name', + disable_rollback=False, + environment={}, + parameters={}, + template={}, + files={}, + timeout_mins=60, + ) + self.assertEqual(1, mock_get.call_count) + self.assertEqual(1, mock_poll.call_count) + self.assertEqual(stack, ret) + @mock.patch.object(shade.OpenStackCloud, 'heat_client') def test_get_stack(self, mock_heat): stack = fakes.FakeStack('azerty', 'stack',) From 7de80f972fce8e2c2db14972cab2d69b6c150efd Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Mon, 4 Apr 2016 09:09:23 +1200 Subject: [PATCH 0907/3836] delete_stack add wait argument This is useful especially to ensure that the delete completed without failures. Change-Id: I64ce8510b9cfdd7f3dd4e67cefa5e6aeda8f67ea --- shade/openstackcloud.py | 27 +++++++++++++++++++++++++- shade/tests/functional/test_stack.py | 3 ++- shade/tests/unit/test_stack.py | 29 ++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 9ebc1a220..02c0ecff4 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -942,10 +942,11 @@ def update_stack( marker=marker) return self.get_stack(name_or_id) - def delete_stack(self, name_or_id): + def delete_stack(self, name_or_id, wait=False): """Delete a Heat Stack :param string name_or_id: Stack name or id. + :param boolean wait: Whether to wait for the delete to finish :returns: True if delete succeeded, False if the stack was not found. @@ -957,9 +958,33 @@ def delete_stack(self, name_or_id): self.log.debug("Stack %s not found for deleting" % name_or_id) return False + if wait: + # find the last event to use as the marker + events = event_utils.get_events(self.heat_client, + name_or_id, + event_args={'sort_dir': 'desc', + 'limit': 1}) + marker = events[0].id if events else None + with _utils.heat_exceptions("Failed to delete stack {id}".format( id=name_or_id)): self.manager.submitTask(_tasks.StackDelete(id=stack['id'])) + if wait: + try: + event_utils.poll_for_events(self.heat_client, + stack_name=name_or_id, + action='DELETE', + marker=marker) + except (heat_exceptions.NotFound, heat_exceptions.CommandError): + # heatclient might raise NotFound or CommandError on + # not found during poll_for_events + pass + stack = self.get_stack(name_or_id) + if stack and stack['stack_status'] == 'DELETE_FAILED': + raise OpenStackCloudException( + "Failed to delete stack {id}: {reason}".format( + id=name_or_id, reason=stack['stack_status_reason'])) + return True def get_name(self): diff --git a/shade/tests/functional/test_stack.py b/shade/tests/functional/test_stack.py index 8f073b87d..a0e649b86 100644 --- a/shade/tests/functional/test_stack.py +++ b/shade/tests/functional/test_stack.py @@ -83,7 +83,8 @@ def setUp(self): self.skipTest('Orchestration service not supported by cloud') def _cleanup_stack(self): - self.cloud.delete_stack(self.stack_name) + self.cloud.delete_stack(self.stack_name, wait=True) + self.assertIsNone(self.cloud.get_stack(self.stack_name)) def test_stack_validation(self): test_template = tempfile.NamedTemporaryFile(delete=False) diff --git a/shade/tests/unit/test_stack.py b/shade/tests/unit/test_stack.py index e796e46eb..5e1280a28 100644 --- a/shade/tests/unit/test_stack.py +++ b/shade/tests/unit/test_stack.py @@ -108,6 +108,35 @@ def test_delete_stack_exception(self, mock_heat, mock_get): ): self.cloud.delete_stack('stack_name') + @mock.patch.object(event_utils, 'poll_for_events') + @mock.patch.object(shade.OpenStackCloud, 'get_stack') + @mock.patch.object(shade.OpenStackCloud, 'heat_client') + def test_delete_stack_wait(self, mock_heat, mock_get, mock_poll): + stack = {'id': 'stack_id', 'name': 'stack_name'} + mock_get.side_effect = (stack, None) + self.assertTrue(self.cloud.delete_stack('stack_name', wait=True)) + mock_heat.stacks.delete.assert_called_once_with(stack['id']) + self.assertEqual(2, mock_get.call_count) + self.assertEqual(1, mock_poll.call_count) + + @mock.patch.object(event_utils, 'poll_for_events') + @mock.patch.object(shade.OpenStackCloud, 'get_stack') + @mock.patch.object(shade.OpenStackCloud, 'heat_client') + def test_delete_stack_wait_failed(self, mock_heat, mock_get, mock_poll): + stack = {'id': 'stack_id', 'name': 'stack_name'} + stack_failed = {'id': 'stack_id', 'name': 'stack_name', + 'stack_status': 'DELETE_FAILED', + 'stack_status_reason': 'ouch'} + mock_get.side_effect = (stack, stack_failed) + with testtools.ExpectedException( + shade.OpenStackCloudException, + "Failed to delete stack stack_name: ouch" + ): + self.cloud.delete_stack('stack_name', wait=True) + mock_heat.stacks.delete.assert_called_once_with(stack['id']) + self.assertEqual(2, mock_get.call_count) + self.assertEqual(1, mock_poll.call_count) + @mock.patch.object(template_utils, 'get_template_contents') @mock.patch.object(shade.OpenStackCloud, 'heat_client') def test_create_stack(self, mock_heat, mock_template): From cb579cd71aac79ea7bfa7a2cd4f9d76ae43b1f15 Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Wed, 20 Apr 2016 09:15:18 +1200 Subject: [PATCH 0908/3836] Document create_stack Change-Id: Ie79f513cb53be60130ce4dabff73e0463b4e1ebb --- shade/openstackcloud.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 02c0ecff4..d7f6e6e76 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -853,6 +853,29 @@ def create_stack( wait=False, timeout=3600, environment_files=None, **parameters): + """Create a Heat Stack. + + :param string name: Name of the stack. + :param string template_file: Path to the template. + :param string template_url: URL of template. + :param string template_object: URL to retrieve template object. + :param dict files: dict of additional file content to include. + :param boolean rollback: Enable rollback on create failure. + :param boolean wait: Whether to wait for the delete to finish. + :param int timeout: Stack create timeout in seconds. + :param list environment_files: Paths to environment files to apply. + + Other arguments will be passed as stack parameters which will take + precedence over any parameters specified in the environments. + + Only one of template_file, template_url, template_object should be + specified. + + :returns: a dict containing the stack description + + :raises: ``OpenStackCloudException`` if something goes wrong during + the openstack API call + """ envfiles, env = template_utils.process_multiple_environments_and_files( env_paths=environment_files) tpl_files, template = template_utils.get_template_contents( From 04fd7dbd73319ab70dfb99db5a60a0c03e6852f6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 19 Apr 2016 10:42:20 -0500 Subject: [PATCH 0909/3836] Have delete_server use the timed server list cache Waiting for server deletion is just as costly as waiting for creation. Make sure that delete honors the 5-second cache if it's in use by using get_server() instead of get_server_by_id(). Using the get_server() call identified a bug in the volume cache invalidation that is done in the code immediately after the call, so we fix that here too, and add a new cache_enabled attribute to allow us to avoid an unnecessary API call. Co-Authored-By: David Shrewsbury Change-Id: I70ccfffe6cbb1f46049b3525bba35c612a572ef0 --- shade/openstackcloud.py | 34 ++++++++++++++------------ shade/tests/unit/test_delete_server.py | 4 +-- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index d0a27aff8..953bb9279 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -195,6 +195,7 @@ def __init__( cache_arguments = cloud_config.get_cache_arguments() if cache_class != 'dogpile.cache.null': + self.cache_enabled = True self._cache = cache.make_region( function_key_generator=self._make_cache_key ).configure( @@ -204,6 +205,8 @@ def __init__( self._SERVER_AGE = DEFAULT_SERVER_AGE self._PORT_AGE = DEFAULT_PORT_AGE else: + self.cache_enabled = False + def _fake_invalidate(unused): pass @@ -4045,28 +4048,27 @@ def _delete_server( if not wait: return True + # If the server has volume attachments, or if it has booted + # from volume, deleting it will change volume state so we will + # need to invalidate the cache. Avoid the extra API call if + # caching is not enabled. + reset_volume_cache = False + if (self.cache_enabled + and self.has_service('volume') + and self.get_volumes(server)): + reset_volume_cache = True + for count in _utils._iterate_timeout( timeout, "Timed out waiting for server to get deleted.", wait=self._SERVER_AGE): - try: - server = self.get_server_by_id(server['id']) + with _utils.shade_exceptions("Error in deleting server"): + server = self.get_server(server['id']) if not server: break - except nova_exceptions.NotFound: - break - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Error in deleting server: {0}".format(e)) - - if self.has_service('volume'): - # If the server has volume attachments, or if it has booted - # from volume, deleting it will change volume state - if (not server['image'] or not server['image']['id'] - or self.get_volume(server)): - self.list_volumes.invalidate(self) + + if reset_volume_cache: + self.list_volumes.invalidate(self) # Reset the list servers cache time so that the next list server # call gets a new list diff --git a/shade/tests/unit/test_delete_server.py b/shade/tests/unit/test_delete_server.py index f9e8a9d1c..6901bc75e 100644 --- a/shade/tests/unit/test_delete_server.py +++ b/shade/tests/unit/test_delete_server.py @@ -123,10 +123,10 @@ def test_delete_server_get_fails(self, nova_mock): 'ACTIVE')] for fail in self.novaclient_exceptions: - def _raise_fail(server): + def _raise_fail(): raise fail(code=fail.http_status) - nova_mock.servers.get.side_effect = _raise_fail + nova_mock.servers.list.side_effect = _raise_fail exc = self.assertRaises(shade_exc.OpenStackCloudException, self.cloud.delete_server, 'yosemite', wait=True) From e91245eaa15ce35c1dde7d68a1e08834e57c158f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 18 Apr 2016 14:36:46 -0500 Subject: [PATCH 0910/3836] Retry floating ip deletion before deleting server If we're waiting for a server to be deleted, and we've asked shade to delete the ips associated with the server, it stands to reason that the action should not be considered complete until both things have been accomplished. Unfortunately, what's going on here is that deleting a floating ip is a synchronous event that just sometimes doesn't work. It is, however, a quick and not very costly event, and we've been doing it before the delete call regardless of user choice on "wait" anyway. So just add a configurable retry. Change-Id: Id7b81417a59c00d3daeb49bd11df0e9fe7dc3b42 --- shade/openstackcloud.py | 60 ++++++++++++++++---- shade/tests/unit/test_floating_ip_neutron.py | 34 ++++++++++- 2 files changed, 81 insertions(+), 13 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 953bb9279..eda4ee9d2 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3247,16 +3247,42 @@ def _nova_create_floating_ip(self, pool=None): _tasks.NovaFloatingIPCreate(pool=pool)) return pool_ip - def delete_floating_ip(self, floating_ip_id): - """Deallocate a floating IP from a tenant. + def delete_floating_ip(self, floating_ip_id, retry=1): + """Deallocate a floating IP from a project. :param floating_ip_id: a floating IP address id. + :param retry: number of times to retry. Optional, defaults to 1, + which is in addition to the initial delete call. + A value of 0 will also cause no checking of results to + occur. :returns: True if the IP address has been deleted, False if the IP address was not found. :raises: ``OpenStackCloudException``, on operation error. """ + for count in range(0, max(0, retry) + 1): + result = self._delete_floating_ip(floating_ip_id) + + if (retry == 0) or not result: + return result + + # neutron sometimes returns success when deleating a floating + # ip. That's awesome. SO - verify that the delete actually + # worked. + f_ip = self.get_floating_ip(id=floating_ip_id) + if not f_ip: + return True + + raise OpenStackCloudException( + "Attempted to delete Floating IP {ip} with id {id} a total of" + " {retry} times. Although the cloud did not indicate any errors" + " the floating ip is still in existence. Aborting further" + " operations.".format( + id=floating_ip_id, ip=f_ip['floating_ip_address'], + retry=retry + 1)) + + def _delete_floating_ip(self, floating_ip_id): if self.has_service('network') and not self._use_nova_floating(): try: return self._neutron_delete_floating_ip(floating_ip_id) @@ -3264,9 +3290,6 @@ def delete_floating_ip(self, floating_ip_id): self.log.debug( "Something went wrong talking to neutron API: " "'{msg}'. Trying with Nova.".format(msg=str(e))) - # Fall-through, trying with Nova - - # Else, we are using Nova network return self._nova_delete_floating_ip(floating_ip_id) def _neutron_delete_floating_ip(self, floating_ip_id): @@ -3276,7 +3299,10 @@ def _neutron_delete_floating_ip(self, floating_ip_id): _tasks.NeutronFloatingIPDelete(floatingip=floating_ip_id)) except OpenStackCloudResourceNotFound: return False - + except Exception as e: + raise OpenStackCloudException( + "Unable to delete floating IP id {fip_id}: {msg}".format( + fip_id=floating_ip_id, msg=str(e))) return True def _nova_delete_floating_ip(self, floating_ip_id): @@ -3291,7 +3317,6 @@ def _nova_delete_floating_ip(self, floating_ip_id): raise OpenStackCloudException( "Unable to delete floating IP id {fip_id}: {msg}".format( fip_id=floating_ip_id, msg=str(e))) - return True def _attach_ip_to_server( @@ -3991,13 +4016,16 @@ def rebuild_server(self, server_id, image_id, admin_pass=None, return server def delete_server( - self, name_or_id, wait=False, timeout=180, delete_ips=False): + self, name_or_id, wait=False, timeout=180, delete_ips=False, + delete_ip_retry=1): """Delete a server instance. :param bool wait: If true, waits for server to be deleted. :param int timeout: Seconds to wait for server deletion. :param bool delete_ips: If true, deletes any floating IPs associated with the instance. + :param int delete_ip_retry: Number of times to retry deleting + any floating ips, should the first try be unsuccessful. :returns: True if delete succeeded, False otherwise if the server does not exist. @@ -4012,10 +4040,12 @@ def delete_server( # private method in order to avoid an unnecessary API call to get # a server we already have. return self._delete_server( - server, wait=wait, timeout=timeout, delete_ips=delete_ips) + server, wait=wait, timeout=timeout, delete_ips=delete_ips, + delete_ip_retry=delete_ip_retry) def _delete_server( - self, server, wait=False, timeout=180, delete_ips=False): + self, server, wait=False, timeout=180, delete_ips=False, + delete_ip_retry=1): if not server: return False @@ -4032,7 +4062,15 @@ def _delete_server( " broken.".format( floating_ip=floating_ip, id=server['id'])) - self.delete_floating_ip(ips[0]['id']) + deleted = self.delete_floating_ip( + ips[0]['id'], retry=delete_ip_retry) + if not deleted: + raise OpenStackCloudException( + "Tried to delete floating ip {floating_ip}" + " associated with server {id} but there was" + " an error deleting it. Not deleting server.".format( + floating_ip=floating_ip, + id=server['id'])) try: self.manager.submitTask( diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 9a6690908..af2f9bc50 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -384,19 +384,49 @@ def test_available_floating_ip_new( def test_delete_floating_ip_existing( self, mock_has_service, mock_neutron_client, mock_get_floating_ip): mock_has_service.return_value = True - mock_get_floating_ip.return_value = { + fake_fip = { 'id': '2f245a7b-796b-4f26-9cf9-9e82d248fda7', + 'floating_ip_address': '172.99.106.167', } + + mock_get_floating_ip.side_effect = [fake_fip, fake_fip, None] mock_neutron_client.delete_floatingip.return_value = None ret = self.client.delete_floating_ip( - floating_ip_id='2f245a7b-796b-4f26-9cf9-9e82d248fda7') + floating_ip_id='2f245a7b-796b-4f26-9cf9-9e82d248fda7', + retry=2) mock_neutron_client.delete_floatingip.assert_called_with( floatingip='2f245a7b-796b-4f26-9cf9-9e82d248fda7' ) + self.assertEqual(mock_get_floating_ip.call_count, 3) self.assertTrue(ret) + @patch.object(OpenStackCloud, 'get_floating_ip') + @patch.object(OpenStackCloud, 'neutron_client') + @patch.object(OpenStackCloud, 'has_service') + def test_delete_floating_ip_existing_no_delete( + self, mock_has_service, mock_neutron_client, mock_get_floating_ip): + mock_has_service.return_value = True + fake_fip = { + 'id': '2f245a7b-796b-4f26-9cf9-9e82d248fda7', + 'floating_ip_address': '172.99.106.167', + } + + mock_get_floating_ip.side_effect = [fake_fip, fake_fip, fake_fip] + mock_neutron_client.delete_floatingip.return_value = None + + self.assertRaises( + exc.OpenStackCloudException, + self.client.delete_floating_ip, + floating_ip_id='2f245a7b-796b-4f26-9cf9-9e82d248fda7', + retry=2) + + mock_neutron_client.delete_floatingip.assert_called_with( + floatingip='2f245a7b-796b-4f26-9cf9-9e82d248fda7' + ) + self.assertEqual(mock_get_floating_ip.call_count, 3) + @patch.object(OpenStackCloud, 'neutron_client') @patch.object(OpenStackCloud, 'has_service') def test_delete_floating_ip_not_found( From 63566eaca7cf1ddc4a0460b9284b17fd9072551d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 19 Apr 2016 15:36:25 -0500 Subject: [PATCH 0911/3836] Delete floating IP on nova refresh failure When we're in the middle of a "create an IP for this port of this server" flow and that times out, not deleting the IP on the failure causes a leak because there is no record given back to the user of the floating IP. Change-Id: I00f8a0f19e4a14eb364c760152b151a1043e7828 --- shade/openstackcloud.py | 26 +++++++++++++++++--- shade/tests/unit/test_floating_ip_neutron.py | 22 +++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index eda4ee9d2..f0316b527 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3652,6 +3652,7 @@ def add_auto_ip(self, server, wait=False, timeout=60, reuse=True): def _add_auto_ip(self, server, wait=False, timeout=60, reuse=True): skip_attach = False + created = False if reuse: f_ip = self.available_floating_ip() else: @@ -3660,10 +3661,29 @@ def _add_auto_ip(self, server, wait=False, timeout=60, reuse=True): # This gets passed in for both nova and neutron # but is only meaninful for the neutron logic branch skip_attach = True + created = True - return self._attach_ip_to_server( - server=server, floating_ip=f_ip, wait=wait, timeout=timeout, - skip_attach=skip_attach) + try: + return self._attach_ip_to_server( + server=server, floating_ip=f_ip, wait=wait, timeout=timeout, + skip_attach=skip_attach) + except OpenStackCloudTimeout: + if (self.has_service('network') + and not self._use_nova_floating() + and created): + # We are here because we created an IP on the port + # It failed. Delete so as not to leak an unmanaged + # resource + self.log.error( + "Timeout waiting for floating IP to become" + " active. Floating IP {ip}:{id} was created for" + " server {server} but is being deleted due to" + " activation failure.".format( + ip=f_ip['floating_ip_address'], + id=f_ip['id'], + server=server['id'])) + self.delete_floating_ip(f_ip['id']) + raise def add_ips_to_server( self, server, auto_ip=True, ips=None, ip_pool=None, diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index af2f9bc50..d0ebad059 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -467,6 +467,28 @@ def test_attach_ip_to_server( } ) + @patch.object(OpenStackCloud, 'delete_floating_ip') + @patch.object(OpenStackCloud, 'get_server_by_id') + @patch.object(OpenStackCloud, 'create_floating_ip') + @patch.object(OpenStackCloud, 'has_service') + def test_add_ip_refresh_timeout( + self, mock_has_service, mock_create_floating_ip, + mock_get_server_by_id, mock_delete_floating_ip): + mock_has_service.return_value = True + + mock_create_floating_ip.return_value = self.floating_ip + mock_get_server_by_id.return_value = self.fake_server + + self.assertRaises( + exc.OpenStackCloudTimeout, + self.client._add_auto_ip, + server=self.fake_server, + wait=True, timeout=2, + reuse=False) + + mock_delete_floating_ip.assert_called_once_with( + self.floating_ip['id']) + @patch.object(OpenStackCloud, 'get_floating_ip') @patch.object(OpenStackCloud, 'neutron_client') @patch.object(OpenStackCloud, 'has_service') From 94dab0101c357e2c3b2c3cd8f417c2105a580996 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 20 Apr 2016 10:12:37 -0500 Subject: [PATCH 0912/3836] Rework floating ip use test to be neutron based We do the if condition in a lot of places. It should be a helper method. Also, the helper method should return the thing we test for, not the negation of it. Change-Id: I47b3dc79c3bfb12b57265f7008b1e7144611c1ef --- shade/openstackcloud.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index f0316b527..76ff9ce99 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1384,7 +1384,7 @@ def list_floating_ips(self): :returns: A list of floating IP dicts. """ - if self.has_service('network') and not self._use_nova_floating(): + if self._use_neutron_floating(): try: return _utils.normalize_neutron_floating_ips( self._neutron_list_floating_ips()) @@ -1598,8 +1598,9 @@ def get_internal_networks(self): self._find_interesting_networks() return self._internal_networks - def _use_nova_floating(self): - return self._floating_ip_source == 'nova' + def _use_neutron_floating(self): + return (self.has_service('network') + and self._floating_ip_source == 'neutron') def get_keypair(self, name_or_id, filters=None): """Get a keypair by name or ID. @@ -3038,7 +3039,7 @@ def available_floating_ip(self, network=None, server=None): :returns: a (normalized) structure with a floating IP address description. """ - if self.has_service('network') and not self._use_nova_floating(): + if self._use_neutron_floating(): try: f_ips = _utils.normalize_neutron_floating_ips( self._neutron_available_floating_ips( @@ -3181,7 +3182,7 @@ def create_floating_ip(self, network=None, server=None, :raises: ``OpenStackCloudException``, on operation error. """ - if self.has_service('network') and not self._use_nova_floating(): + if self._use_neutron_floating(): try: f_ips = _utils.normalize_neutron_floating_ips( [self._neutron_create_floating_ip( @@ -3283,7 +3284,7 @@ def delete_floating_ip(self, floating_ip_id, retry=1): retry=retry + 1)) def _delete_floating_ip(self, floating_ip_id): - if self.has_service('network') and not self._use_nova_floating(): + if self._use_neutron_floating(): try: return self._neutron_delete_floating_ip(floating_ip_id) except OpenStackCloudURINotFound as e: @@ -3346,7 +3347,7 @@ def _attach_ip_to_server( if ext_ip == floating_ip['floating_ip_address']: return server - if self.has_service('network') and not self._use_nova_floating(): + if self._use_neutron_floating(): if not skip_attach: try: self._neutron_attach_ip_to_server( @@ -3514,7 +3515,7 @@ def detach_ip_from_server(self, server_id, floating_ip_id): :raises: ``OpenStackCloudException``, on operation error. """ - if self.has_service('network') and not self._use_nova_floating(): + if self._use_neutron_floating(): try: return self._neutron_detach_ip_from_server( server_id=server_id, floating_ip_id=floating_ip_id) @@ -3668,9 +3669,7 @@ def _add_auto_ip(self, server, wait=False, timeout=60, reuse=True): server=server, floating_ip=f_ip, wait=wait, timeout=timeout, skip_attach=skip_attach) except OpenStackCloudTimeout: - if (self.has_service('network') - and not self._use_nova_floating() - and created): + if self._use_neutron_floating() and created: # We are here because we created an IP on the port # It failed. Delete so as not to leak an unmanaged # resource From 1872af0884531d4bd7d9943a348f91e286551c7c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 20 Apr 2016 10:13:14 -0500 Subject: [PATCH 0913/3836] Add public helper method for cleaning floating ips Some things, like nodepool or utility scripts, need to be able to safely clean floating ips, but doing so requires knowledge of whether the cloud in question is using nova or neutron for floating ips, which shade otherwise hides from the user. Make a helper method that allows the user to do it on the clouds where it is safe to do automatically. Change-Id: I93b0c7d0b0eefdfe0fb1cd4a66cdbba9baabeb09 --- shade/openstackcloud.py | 28 +++++++++++++++++ shade/tests/unit/test_floating_ip_neutron.py | 33 ++++++++++++++++++++ shade/tests/unit/test_floating_ip_nova.py | 13 ++++++++ 3 files changed, 74 insertions(+) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 76ff9ce99..c5c3785f8 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3320,6 +3320,34 @@ def _nova_delete_floating_ip(self, floating_ip_id): fip_id=floating_ip_id, msg=str(e))) return True + def delete_unattached_floating_ips(self, wait=False, timeout=60): + """Safely delete unattached floating ips. + + If the cloud can safely purge any unattached floating ips without + race conditions, do so. + + Safely here means a specific thing. It means that you are not running + this while another process that might do a two step create/attach + is running. You can safely run this method while another process + is creating servers and attaching floating IPs to them if either that + process is using add_auto_ip from shade, or is creating the floating + IPs by passing in a server to the create_floating_ip call. + + :param wait: Whether to wait for each IP to be deleted + :param timeout: How long to wait for each IP + + :returns: True if Floating IPs have been deleted, False if not + + :raises: ``OpenStackCloudException``, on operation error. + """ + processed = [] + if self._use_neutron_floating(): + for ip in self.list_floating_ips(): + if not ip['attached']: + processed.append(self.delete_floating_ip( + id=ip['id'], wait=wait, timeout=timeout)) + return all(processed) if processed else False + def _attach_ip_to_server( self, server, floating_ip, fixed_address=None, wait=False, diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index d0ebad059..85fb977ea 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -531,3 +531,36 @@ def test_add_ip_from_pool( fixed_address='192.0.2.129') self.assertEqual(server, self.fake_server) + + @patch.object(OpenStackCloud, 'delete_floating_ip') + @patch.object(OpenStackCloud, 'list_floating_ips') + @patch.object(OpenStackCloud, '_use_neutron_floating') + def test_cleanup_floating_ips( + self, mock_use_neutron_floating, mock_list_floating_ips, + mock_delete_floating_ip): + mock_use_neutron_floating.return_value = True + floating_ips = [{ + "id": "this-is-a-floating-ip-id", + "fixed_ip_address": None, + "internal_network": None, + "floating_ip_address": "203.0.113.29", + "network": "this-is-a-net-or-pool-id", + "attached": False, + "status": "ACTIVE" + }, { + "id": "this-is-an-attached-floating-ip-id", + "fixed_ip_address": None, + "internal_network": None, + "floating_ip_address": "203.0.113.29", + "network": "this-is-a-net-or-pool-id", + "attached": True, + "status": "ACTIVE" + }] + + mock_list_floating_ips.return_value = floating_ips + + self.client.delete_unattached_floating_ips() + + mock_delete_floating_ip.assert_called_once_with( + id="this-is-a-floating-ip-id", + timeout=60, wait=False) diff --git a/shade/tests/unit/test_floating_ip_nova.py b/shade/tests/unit/test_floating_ip_nova.py index 2ad83b894..34bee06c3 100644 --- a/shade/tests/unit/test_floating_ip_nova.py +++ b/shade/tests/unit/test_floating_ip_nova.py @@ -248,3 +248,16 @@ def test_add_ip_from_pool(self, mock_has_service, mock_nova_client): fixed_address='192.0.2.129') self.assertEqual(server, self.fake_server) + + @patch.object(OpenStackCloud, 'delete_floating_ip') + @patch.object(OpenStackCloud, 'list_floating_ips') + @patch.object(OpenStackCloud, '_use_neutron_floating') + def test_cleanup_floating_ips( + self, mock_use_neutron_floating, mock_list_floating_ips, + mock_delete_floating_ip): + mock_use_neutron_floating.return_value = False + + self.client.delete_unattached_floating_ips() + + mock_delete_floating_ip.assert_not_called() + mock_list_floating_ips.assert_not_called() From d9455f6d8e09d4a80e2f3f03596985c598d19aa9 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 21 Apr 2016 10:12:14 -0500 Subject: [PATCH 0914/3836] Fail if FIP doens't have the requested port_id When we create a FIP on a port, it should return with the port in question. Also, it turns out we can wait for the floating IP to reach ACTIVE status. Finally, our normalize method for fips was producing inconsistent values depending on create or list - so fix that. Change-Id: I56b4136a77dd61b6ef1832759b8169dd53aa49da --- shade/_utils.py | 24 +++--- shade/openstackcloud.py | 85 ++++++++++++++++---- shade/tests/unit/test_floating_ip_neutron.py | 31 +++++-- 3 files changed, 111 insertions(+), 29 deletions(-) diff --git a/shade/_utils.py b/shade/_utils.py index ccc13b5f1..25285d255 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -313,15 +313,21 @@ def normalize_neutron_floating_ips(ips): ] """ - ret = [dict( - id=ip['id'], - fixed_ip_address=ip.get('fixed_ip_address'), - floating_ip_address=ip['floating_ip_address'], - network=ip['floating_network_id'], - attached=(ip.get('port_id') is not None and - ip.get('port_id') != ''), - status=ip['status'] - ) for ip in ips] + ret = [] + for ip in ips: + network_id = ip.get('floating_network_id', ip.get('network')) + ret.append(dict( + id=ip['id'], + fixed_ip_address=ip.get('fixed_ip_address'), + floating_ip_address=ip['floating_ip_address'], + network=network_id, + floating_network_id=network_id, + port_id=ip.get('port_id'), + router_id=ip.get('router_id'), + attached=(ip.get('port_id') is not None and + ip.get('port_id') != ''), + status=ip['status'], + )) return meta.obj_list_to_dict(ret) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index c5c3785f8..4dc9e63b7 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3165,7 +3165,8 @@ def _nova_available_floating_ips(self, pool=None): return [f_ip] def create_floating_ip(self, network=None, server=None, - fixed_address=None, nat_destination=None): + fixed_address=None, nat_destination=None, + wait=False, timeout=60): """Allocate a new floating IP from a network or a pool. :param network: Nova pool name or Neutron network name or id @@ -3177,6 +3178,12 @@ def create_floating_ip(self, network=None, server=None, :param nat_destination: (optional) Name or id of the network that the fixed IP to attach the floating IP to should be on. + :param wait: (optional) Whether to wait for the IP to be active. + Defaults to False. Only applies if a server is + provided. + :param timeout: (optional) How long to wait for the IP to be active. + Defaults to 60. Only applies if a server is + provided. :returns: a floating IP address @@ -3184,13 +3191,11 @@ def create_floating_ip(self, network=None, server=None, """ if self._use_neutron_floating(): try: - f_ips = _utils.normalize_neutron_floating_ips( - [self._neutron_create_floating_ip( - network_name_or_id=network, server=server, - fixed_address=fixed_address, - nat_destination=nat_destination)] - ) - return f_ips[0] + return self._neutron_create_floating_ip( + network_name_or_id=network, server=server, + fixed_address=fixed_address, + nat_destination=nat_destination, + wait=wait, timeout=timeout) except OpenStackCloudURINotFound as e: self.log.debug( "Something went wrong talking to neutron API: " @@ -3202,9 +3207,15 @@ def create_floating_ip(self, network=None, server=None, [self._nova_create_floating_ip(pool=network)]) return f_ips[0] + def _submit_create_fip(self, kwargs): + # Split into a method to aid in test mocking + return _utils.normalize_neutron_floating_ips( + [self.manager.submitTask(_tasks.NeutronFloatingIPCreate( + body={'floatingip': kwargs}))['floatingip']])[0] + def _neutron_create_floating_ip( self, network_name_or_id=None, server=None, - fixed_address=None, nat_destination=None): + fixed_address=None, nat_destination=None, wait=False, timeout=60): with _utils.neutron_exceptions( "unable to create floating IP for net " "{0}".format(network_name_or_id)): @@ -3223,6 +3234,7 @@ def _neutron_create_floating_ip( kwargs = { 'floating_network_id': networks[0]['id'], } + port = None if server: (port, fixed_ip_address) = self._get_free_fixed_port( server, fixed_address=fixed_address, @@ -3230,8 +3242,37 @@ def _neutron_create_floating_ip( if port: kwargs['port_id'] = port['id'] kwargs['fixed_ip_address'] = fixed_ip_address - return self.manager.submitTask(_tasks.NeutronFloatingIPCreate( - body={'floatingip': kwargs}))['floatingip'] + fip = self._submit_create_fip(kwargs) + fip_id = fip['id'] + + if port: + if fip['port_id'] != port['id']: + raise OpenStackCloudException( + "Attempted to create FIP on port {port} for server" + " {server} but something went wrong".format( + port=port['id'], server=server['id'])) + # The FIP is only going to become active in this context + # when we've attached it to something, which only occurs + # if we've provided a port as a parameter + if wait: + try: + for count in _utils._iterate_timeout( + timeout, + "Timeout waiting for the floating IP" + " to be ACTIVE"): + fip = self.get_floating_ip(fip_id) + if fip['status'] == 'ACTIVE': + break + except OpenStackCloudTimeout: + self.log.error( + "Timed out on floating ip {fip} becoming active." + " Deleting".format(fip=fip_id)) + try: + self.delete_floating_ip(fip_id) + except Exception: + pass + raise + return fip def _nova_create_floating_ip(self, pool=None): with _utils.shade_exceptions( @@ -3618,9 +3659,17 @@ def _add_ip_from_pool( if reuse: f_ip = self.available_floating_ip(network=network) else: + start_time = time.time() f_ip = self.create_floating_ip( - network=network, nat_destination=nat_destination) - + network=network, nat_destination=nat_destination, + wait=wait, timeout=timeout) + timeout = timeout - (time.time() - start_time) + + # We run attach as a second call rather than in the create call + # because there are code flows where we will not have an attached + # FIP yet. However, even if it was attached in the create, we run + # the attach function below to get back the server dict refreshed + # with the FIP information. return self._attach_ip_to_server( server=server, floating_ip=f_ip, fixed_address=fixed_address, wait=wait, timeout=timeout) @@ -3685,7 +3734,10 @@ def _add_auto_ip(self, server, wait=False, timeout=60, reuse=True): if reuse: f_ip = self.available_floating_ip() else: - f_ip = self.create_floating_ip(server=server) + start_time = time.time() + f_ip = self.create_floating_ip( + server=server, wait=wait, timeout=timeout) + timeout = timeout - (time.time() - start_time) if server: # This gets passed in for both nova and neutron # but is only meaninful for the neutron logic branch @@ -3693,6 +3745,11 @@ def _add_auto_ip(self, server, wait=False, timeout=60, reuse=True): created = True try: + # We run attach as a second call rather than in the create call + # because there are code flows where we will not have an attached + # FIP yet. However, even if it was attached in the create, we run + # the attach function below to get back the server dict refreshed + # with the FIP information. return self._attach_ip_to_server( server=server, floating_ip=f_ip, wait=wait, timeout=timeout, skip_attach=skip_attach) diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 85fb977ea..80dbe9536 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -19,6 +19,7 @@ Tests Floating IP resource methods for Neutron """ +import mock from mock import patch import os_client_config @@ -340,20 +341,22 @@ def test_auto_ip_pool_no_reuse( mock_keystone_session, mock_nova_client): mock_has_service.return_value = True - mock__neutron_create_floating_ip.return_value = \ - self.mock_floating_ip_list_rep['floatingips'][0] + fip = _utils.normalize_neutron_floating_ips( + self.mock_floating_ip_list_rep['floatingips'])[0] + mock__neutron_create_floating_ip.return_value = fip mock_keystone_session.get_project_id.return_value = \ '4969c491a3c74ee4af974e6d800c62df' + fake_server = meta.obj_to_dict(fakes.FakeServer('1234', '', 'ACTIVE')) self.client.add_ips_to_server( - dict(id='1234'), ip_pool='my-network', reuse=False) + fake_server, ip_pool='my-network', reuse=False) mock__neutron_create_floating_ip.assert_called_once_with( network_name_or_id='my-network', server=None, - fixed_address=None, nat_destination=None) + fixed_address=None, nat_destination=None, wait=False, timeout=60) mock_attach_ip_to_server.assert_called_once_with( - server={'id': '1234'}, fixed_address=None, - floating_ip=self.floating_ip, wait=False, timeout=60) + server=fake_server, fixed_address=None, + floating_ip=fip, wait=False, timeout=mock.ANY) @patch.object(OpenStackCloud, 'keystone_session') @patch.object(OpenStackCloud, '_neutron_create_floating_ip') @@ -564,3 +567,19 @@ def test_cleanup_floating_ips( mock_delete_floating_ip.assert_called_once_with( id="this-is-a-floating-ip-id", timeout=60, wait=False) + + @patch.object(OpenStackCloud, '_submit_create_fip') + @patch.object(OpenStackCloud, '_get_free_fixed_port') + @patch.object(OpenStackCloud, 'get_external_networks') + def test_create_floating_ip_no_port( + self, mock_get_ext_nets, mock_get_free_fixed_port, + mock_submit_create_fip): + fake_port = dict(id='port-id') + mock_get_ext_nets.return_value = [self.mock_get_network_rep] + mock_get_free_fixed_port.return_value = (fake_port, '10.0.0.2') + mock_submit_create_fip.return_value = dict(port_id=None) + + self.assertRaises( + exc.OpenStackCloudException, + self.client._neutron_create_floating_ip, + server=dict(id='some-server')) From 40a50918bdf194991f1d68c2a60df2f512a64dd3 Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Wed, 6 Apr 2016 15:42:19 +0200 Subject: [PATCH 0915/3836] Add support for Designate zones This is the first commit to add initial support for Designate. Starting with zones objects, more to come. Depends-On: Ieaddeb4a0b317f85a2161e67bc5c202cc1b01464 Change-Id: I1109f89075ed663620ecb11d18507e8a5d7351b4 --- ...ignate_zones_support-35fa9b8b09995b43.yaml | 4 + requirements.txt | 1 + shade/_tasks.py | 20 +++ shade/openstackcloud.py | 119 ++++++++++++++++++ shade/tests/fakes.py | 12 ++ shade/tests/functional/test_zone.py | 86 +++++++++++++ shade/tests/unit/test_zone.py | 97 ++++++++++++++ 7 files changed, 339 insertions(+) create mode 100644 releasenotes/notes/add_designate_zones_support-35fa9b8b09995b43.yaml create mode 100644 shade/tests/functional/test_zone.py create mode 100644 shade/tests/unit/test_zone.py diff --git a/releasenotes/notes/add_designate_zones_support-35fa9b8b09995b43.yaml b/releasenotes/notes/add_designate_zones_support-35fa9b8b09995b43.yaml new file mode 100644 index 000000000..f5253af0f --- /dev/null +++ b/releasenotes/notes/add_designate_zones_support-35fa9b8b09995b43.yaml @@ -0,0 +1,4 @@ +--- +features: + - Add support for Designate zones resources, with the + usual methods (search/list/get/create/update/delete). diff --git a/requirements.txt b/requirements.txt index 47db714ea..d8ec26d3a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,5 +19,6 @@ python-troveclient>=1.2.0 python-ironicclient>=0.10.0 python-swiftclient>=2.5.0 python-heatclient>=0.3.0 +python-designateclient>=2.1.0 dogpile.cache>=0.5.3 diff --git a/shade/_tasks.py b/shade/_tasks.py index 1ada75a21..a018612b5 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -772,3 +772,23 @@ def main(self, client): class StackGet(task_manager.Task): def main(self, client): return client.heat_client.stacks.get(**self.args) + + +class ZoneList(task_manager.Task): + def main(self, client): + return client.designate_client.zones.list() + + +class ZoneCreate(task_manager.Task): + def main(self, client): + return client.designate_client.zones.create(**self.args) + + +class ZoneUpdate(task_manager.Task): + def main(self, client): + return client.designate_client.zones.update(**self.args) + + +class ZoneDelete(task_manager.Task): + def main(self, client): + return client.designate_client.zones.delete(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 0a4433191..c6139db02 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -40,6 +40,7 @@ import swiftclient.service import swiftclient.exceptions as swift_exceptions import troveclient.client +import designateclient.client from shade.exc import * # noqa from shade import _log @@ -250,6 +251,7 @@ def invalidate(self): self._swift_client_lock = threading.Lock() self._swift_service_lock = threading.Lock() self._trove_client = None + self._designate_client = None self._raw_clients = {} @@ -820,6 +822,13 @@ def neutron_client(self): 'network', neutronclient.neutron.client.Client) return self._neutron_client + @property + def designate_client(self): + if self._designate_client is None: + self._designate_client = self._get_client( + 'dns', designateclient.client.Client) + return self._designate_client + def create_stack( self, name, template_file=None, template_url=None, @@ -4821,3 +4830,113 @@ def delete_security_group_rule(self, rule_id): raise OpenStackCloudUnavailableFeature( "Unavailable feature: security groups" ) + + def list_zones(self): + """List all available zones. + + :returns: A list of zones dicts. + + """ + with _utils.shade_exceptions("Error fetching zones list"): + return self.manager.submitTask(_tasks.ZoneList()) + + def get_zone(self, name_or_id, filters=None): + """Get a zone by name or ID. + + :param name_or_id: Name or ID of the zone + :param dict filters: + A dictionary of meta data to use for further filtering + + :returns: A zone dict or None if no matching zone is + found. + + """ + return _utils._get_entity(self.search_zones, name_or_id, filters) + + def search_zones(self, name_or_id=None, filters=None): + zones = self.list_zones() + return _utils._filter_list(zones, name_or_id, filters) + + def create_zone(self, name, zone_type=None, email=None, description=None, + ttl=None, masters=None): + """Create a new zone. + + :param name: Name of the zone being created. + :param zone_type: Type of the zone (primary/secondary) + :param email: Email of the zone owner (only + applies if zone_type is primary) + :param description: Description of the zone + :param ttl: TTL (Time to live) value in seconds + :param masters: Master nameservers (only applies + if zone_type is secondary) + + :returns: a dict representing the created zone. + + :raises: OpenStackCloudException on operation error. + """ + + # We capitalize in case the user passes time in lowercase, as + # designate call expects PRIMARY/SECONDARY + if zone_type is not None: + zone_type = zone_type.upper() + if zone_type not in ('PRIMARY', 'SECONDARY'): + raise OpenStackCloudException( + "Invalid type %s, valid choices are PRIMARY or SECONDARY" % + zone_type) + + with _utils.shade_exceptions("Unable to create zone {name}".format( + name=name)): + return self.manager.submitTask(_tasks.ZoneCreate( + name=name, type_=zone_type, email=email, + description=description, ttl=ttl, masters=masters)) + + @_utils.valid_kwargs('email', 'description', 'ttl', 'masters') + def update_zone(self, name_or_id, **kwargs): + """Update a zone. + + :param name_or_id: Name or ID of the zone being updated. + :param email: Email of the zone owner (only + applies if zone_type is primary) + :param description: Description of the zone + :param ttl: TTL (Time to live) value in seconds + :param masters: Master nameservers (only applies + if zone_type is secondary) + + :returns: a dict representing the updated zone. + + :raises: OpenStackCloudException on operation error. + """ + zone = self.get_zone(name_or_id) + if not zone: + raise OpenStackCloudException( + "Zone %s not found." % name_or_id) + + with _utils.shade_exceptions( + "Error updating zone {0}".format(name_or_id)): + new_zone = self.manager.submitTask( + _tasks.ZoneUpdate( + zone=zone['id'], values=kwargs)) + + return new_zone + + def delete_zone(self, name_or_id): + """Delete a zone. + + :param name_or_id: Name or ID of the zone being deleted. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException on operation error. + """ + + zone = self.get_zone(name_or_id) + if zone is None: + self.log.debug("Zone %s not found for deleting" % name_or_id) + return False + + with _utils.shade_exceptions( + "Error deleting zone {0}".format(name_or_id)): + self.manager.submitTask( + _tasks.ZoneDelete(zone=zone['id'])) + + return True diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index fd08c2424..2c4012307 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -246,3 +246,15 @@ def __init__(self, id, name, description=None, status='CREATE_COMPLETE'): self.stack_name = name self.stack_description = description self.stack_status = status + + +class FakeZone(object): + def __init__(self, id, name, type_, email, description, + ttl, masters): + self.id = id + self.name = name + self.type_ = type_ + self.email = email + self.description = description + self.ttl = ttl + self.masters = masters diff --git a/shade/tests/functional/test_zone.py b/shade/tests/functional/test_zone.py new file mode 100644 index 000000000..62e2d7ce2 --- /dev/null +++ b/shade/tests/functional/test_zone.py @@ -0,0 +1,86 @@ +# -*- 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. + +""" +test_zone +---------------------------------- + +Functional tests for `shade` zone methods. +""" + +from testtools import content + +from shade.tests.functional import base + + +class TestZone(base.BaseFunctionalTestCase): + + def setUp(self): + super(TestZone, self).setUp() + if not self.demo_cloud.has_service('dns'): + self.skipTest('dns service not supported by cloud') + + def test_zones(self): + '''Test DNS zones functionality''' + name = 'example.net.' + zone_type = 'primary' + email = 'test@example.net' + description = 'Test zone' + ttl = 3600 + masters = None + + self.addDetail('zone', content.text_content(name)) + self.addCleanup(self.cleanup, name) + + # Test we can create a zone and we get it returned + zone = self.demo_cloud.create_zone( + name=name, zone_type=zone_type, email=email, + description=description, ttl=ttl, + masters=masters) + self.assertEquals(zone['name'], name) + self.assertEquals(zone['type'], zone_type.upper()) + self.assertEquals(zone['email'], email) + self.assertEquals(zone['description'], description) + self.assertEquals(zone['ttl'], ttl) + self.assertEquals(zone['masters'], []) + + # Test that we can list zones + zones = self.demo_cloud.list_zones() + self.assertIsNotNone(zones) + + # Test we get the same zone with the get_zone method + zone_get = self.demo_cloud.get_zone(zone['id']) + self.assertEquals(zone_get['id'], zone['id']) + + # Test the get method also works by name + zone_get = self.demo_cloud.get_zone(name) + self.assertEquals(zone_get['name'], zone['name']) + + # Test we can update a field on the zone and only that field + # is updated + zone_update = self.demo_cloud.update_zone(zone['id'], ttl=7200) + self.assertEquals(zone_update['id'], zone['id']) + self.assertEquals(zone_update['name'], zone['name']) + self.assertEquals(zone_update['type'], zone['type']) + self.assertEquals(zone_update['email'], zone['email']) + self.assertEquals(zone_update['description'], zone['description']) + self.assertEquals(zone_update['ttl'], 7200) + self.assertEquals(zone_update['masters'], zone['masters']) + + # Test we can delete and get True returned + zone_delete = self.demo_cloud.delete_zone(zone['id']) + self.assertTrue(zone_delete) + + def cleanup(self, name): + self.demo_cloud.delete_zone(name) diff --git a/shade/tests/unit/test_zone.py b/shade/tests/unit/test_zone.py new file mode 100644 index 000000000..26315cc0d --- /dev/null +++ b/shade/tests/unit/test_zone.py @@ -0,0 +1,97 @@ +# -*- 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. + + +import mock +import testtools + +import shade +from shade.tests.unit import base +from shade.tests import fakes + + +zone_obj = fakes.FakeZone( + id='1', + name='example.net.', + type_='PRIMARY', + email='test@example.net', + description='Example zone', + ttl=3600, + masters=None +) + + +class TestZone(base.TestCase): + + def setUp(self): + super(TestZone, self).setUp() + self.cloud = shade.openstack_cloud(validate=False) + + @mock.patch.object(shade.OpenStackCloud, 'designate_client') + def test_create_zone(self, mock_designate): + self.cloud.create_zone(name=zone_obj.name, zone_type=zone_obj.type_, + email=zone_obj.email, + description=zone_obj.description, + ttl=zone_obj.ttl, masters=zone_obj.masters) + mock_designate.zones.create.assert_called_once_with( + name=zone_obj.name, type_=zone_obj.type_.upper(), + email=zone_obj.email, description=zone_obj.description, + ttl=zone_obj.ttl, masters=zone_obj.masters + ) + + @mock.patch.object(shade.OpenStackCloud, 'designate_client') + def test_create_zone_exception(self, mock_designate): + mock_designate.zones.create.side_effect = Exception() + with testtools.ExpectedException( + shade.OpenStackCloudException, + "Unable to create zone example.net." + ): + self.cloud.create_zone('example.net.') + + @mock.patch.object(shade.OpenStackCloud, 'designate_client') + def test_update_zone(self, mock_designate): + new_ttl = 7200 + mock_designate.zones.list.return_value = [zone_obj] + self.cloud.update_zone('1', ttl=new_ttl) + mock_designate.zones.update.assert_called_once_with( + zone='1', values={'ttl': new_ttl} + ) + + @mock.patch.object(shade.OpenStackCloud, 'designate_client') + def test_delete_zone(self, mock_designate): + mock_designate.zones.list.return_value = [zone_obj] + self.cloud.delete_zone('1') + mock_designate.zones.delete.assert_called_once_with( + zone='1' + ) + + @mock.patch.object(shade.OpenStackCloud, 'designate_client') + def test_get_zone_by_id(self, mock_designate): + mock_designate.zones.list.return_value = [zone_obj] + zone = self.cloud.get_zone('1') + self.assertTrue(mock_designate.zones.list.called) + self.assertEqual(zone['id'], '1') + + @mock.patch.object(shade.OpenStackCloud, 'designate_client') + def test_get_zone_by_name(self, mock_designate): + mock_designate.zones.list.return_value = [zone_obj] + zone = self.cloud.get_zone('example.net.') + self.assertTrue(mock_designate.zones.list.called) + self.assertEqual(zone['name'], 'example.net.') + + @mock.patch.object(shade.OpenStackCloud, 'designate_client') + def test_get_zone_not_found_returns_false(self, mock_designate): + mock_designate.zones.list.return_value = [] + zone = self.cloud.get_zone('nonexistingzone.net.') + self.assertFalse(zone) From 700ab6f28220dbd09c2bf1c917245dd212c0fb75 Mon Sep 17 00:00:00 2001 From: Ilya Shakhat Date: Fri, 22 Apr 2016 18:00:53 +0300 Subject: [PATCH 0916/3836] Fix formatting in readme file Change-Id: Ifa37d38b3c7689f703c7129459b15a367e2aafff --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 97a158429..ff95c07a8 100644 --- a/README.rst +++ b/README.rst @@ -399,6 +399,8 @@ in luck. The same interface for `make_client` is supported for `session_client` and will return you a keystoneauth Session object that is mounted on the endpoint for the service you're looking for. +.. code-block:: python + import os_client_config session = os_client_config.session_client('compute', cloud='vexxhost') From 31ac451e129b00173966fda4e0e2aa100198a731 Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Fri, 15 Apr 2016 17:18:21 +0200 Subject: [PATCH 0917/3836] Add Designate recordsets support Change-Id: Ica8531c402cb10cee5aae38690ff95ebd80b21f2 --- ...e_recordsets_support-69af0a6b317073e7.yaml | 4 + shade/_tasks.py | 25 ++++ shade/openstackcloud.py | 121 ++++++++++++++++++ shade/tests/fakes.py | 12 ++ shade/tests/functional/test_recordset.py | 95 ++++++++++++++ shade/tests/unit/test_recordset.py | 114 +++++++++++++++++ 6 files changed, 371 insertions(+) create mode 100644 releasenotes/notes/add_designate_recordsets_support-69af0a6b317073e7.yaml create mode 100644 shade/tests/functional/test_recordset.py create mode 100644 shade/tests/unit/test_recordset.py diff --git a/releasenotes/notes/add_designate_recordsets_support-69af0a6b317073e7.yaml b/releasenotes/notes/add_designate_recordsets_support-69af0a6b317073e7.yaml new file mode 100644 index 000000000..0d464961b --- /dev/null +++ b/releasenotes/notes/add_designate_recordsets_support-69af0a6b317073e7.yaml @@ -0,0 +1,4 @@ +--- +features: + - Add support for Designate recordsets resources, with the + usual methods (search/list/get/create/update/delete). diff --git a/shade/_tasks.py b/shade/_tasks.py index 449a27d34..8c2e10821 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -797,3 +797,28 @@ def main(self, client): class ZoneDelete(task_manager.Task): def main(self, client): return client.designate_client.zones.delete(**self.args) + + +class RecordSetList(task_manager.Task): + def main(self, client): + return client.designate_client.recordsets.list(**self.args) + + +class RecordSetGet(task_manager.Task): + def main(self, client): + return client.designate_client.recordsets.get(**self.args) + + +class RecordSetCreate(task_manager.Task): + def main(self, client): + return client.designate_client.recordsets.create(**self.args) + + +class RecordSetUpdate(task_manager.Task): + def main(self, client): + return client.designate_client.recordsets.update(**self.args) + + +class RecordSetDelete(task_manager.Task): + def main(self, client): + return client.designate_client.recordsets.delete(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 2bead1348..f86d4d141 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5423,3 +5423,124 @@ def delete_zone(self, name_or_id): _tasks.ZoneDelete(zone=zone['id'])) return True + + def list_recordsets(self, zone): + """List all available recordsets. + + :param zone: Name or id of the zone managing the recordset + + :returns: A list of recordsets. + + """ + with _utils.shade_exceptions("Error fetching recordsets list"): + return self.manager.submitTask(_tasks.RecordSetList(zone=zone)) + + def get_recordset(self, zone, name_or_id): + """Get a recordset by name or ID. + + :param zone: Name or ID of the zone managing the recordset + :param name_or_id: Name or ID of the recordset + + :returns: A recordset dict or None if no matching recordset is + found. + + """ + try: + return self.manager.submitTask(_tasks.RecordSetGet( + zone=zone, + recordset=name_or_id)) + except: + return None + + def search_recordsets(self, zone, name_or_id=None, filters=None): + recordsets = self.list_recordsets(zone=zone) + return _utils._filter_list(recordsets, name_or_id, filters) + + def create_recordset(self, zone, name, recordset_type, records, + description=None, ttl=None): + """Create a recordset. + + :param zone: Name or ID of the zone managing the recordset + :param name: Name of the recordset + :param recordset_type: Type of the recordset + :param records: List of the recordset definitions + :param description: Description of the recordset + :param ttl: TTL value of the recordset + + :returns: a dict representing the created recordset. + + :raises: OpenStackCloudException on operation error. + + """ + if self.get_zone(zone) is None: + raise OpenStackCloudException( + "Zone %s not found." % zone) + + # We capitalize the type in case the user sends in lowercase + recordset_type = recordset_type.upper() + + with _utils.shade_exceptions( + "Unable to create recordset {name}".format(name=name)): + return self.manager.submitTask(_tasks.RecordSetCreate( + zone=zone, name=name, type_=recordset_type, records=records, + description=description, ttl=ttl)) + + @_utils.valid_kwargs('email', 'description', 'ttl', 'masters') + def update_recordset(self, zone, name_or_id, **kwargs): + """Update a recordset. + + :param zone: Name or id of the zone managing the recordset + :param name_or_id: Name or ID of the recordset being updated. + :param records: List of the recordset definitions + :param description: Description of the recordset + :param ttl: TTL (Time to live) value in seconds of the recordset + + :returns: a dict representing the updated recordset. + + :raises: OpenStackCloudException on operation error. + """ + zone_obj = self.get_zone(zone) + if zone_obj is None: + raise OpenStackCloudException( + "Zone %s not found." % zone) + + recordset_obj = self.get_recordset(zone, name_or_id) + if recordset_obj is None: + raise OpenStackCloudException( + "Recordset %s not found." % name_or_id) + + with _utils.shade_exceptions( + "Error updating recordset {0}".format(name_or_id)): + new_recordset = self.manager.submitTask( + _tasks.RecordSetUpdate( + zone=zone, recordset=name_or_id, values=kwargs)) + + return new_recordset + + def delete_recordset(self, zone, name_or_id): + """Delete a recordset. + + :param zone: Name or ID of the zone managing the recordset. + :param name_or_id: Name or ID of the recordset being deleted. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException on operation error. + """ + + zone = self.get_zone(zone) + if zone is None: + self.log.debug("Zone %s not found for deleting" % zone) + return False + + recordset = self.get_recordset(zone['id'], name_or_id) + if recordset is None: + self.log.debug("Recordset %s not found for deleting" % name_or_id) + return False + + with _utils.shade_exceptions( + "Error deleting recordset {0}".format(name_or_id)): + self.manager.submitTask( + _tasks.RecordSetDelete(zone=zone['id'], recordset=name_or_id)) + + return True diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 6bf017697..01cb822fb 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -260,3 +260,15 @@ def __init__(self, id, name, type_, email, description, self.description = description self.ttl = ttl self.masters = masters + + +class FakeRecordset(object): + def __init__(self, zone, id, name, type_, description, + ttl, records): + self.zone = zone + self.id = id + self.name = name + self.type_ = type_ + self.description = description + self.ttl = ttl + self.records = records diff --git a/shade/tests/functional/test_recordset.py b/shade/tests/functional/test_recordset.py new file mode 100644 index 000000000..73d9dc72c --- /dev/null +++ b/shade/tests/functional/test_recordset.py @@ -0,0 +1,95 @@ +# -*- 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. + +""" +test_recordset +---------------------------------- + +Functional tests for `shade` recordset methods. +""" + +from testtools import content + +from shade.tests.functional import base + + +class TestRecordset(base.BaseFunctionalTestCase): + + def setUp(self): + super(TestRecordset, self).setUp() + if not self.demo_cloud.has_service('dns'): + self.skipTest('dns service not supported by cloud') + + def test_recordsets(self): + '''Test DNS recordsets functionality''' + zone = 'example2.net.' + email = 'test@example2.net' + name = 'www' + type_ = 'a' + description = 'Test recordset' + ttl = 3600 + records = ['192.168.1.1'] + + self.addDetail('zone', content.text_content(zone)) + self.addDetail('recordset', content.text_content(name)) + self.addCleanup(self.cleanup, zone, name) + + # Create a zone to hold the tested recordset + zone_obj = self.demo_cloud.create_zone(name=zone, email=email) + + # Test we can create a recordset and we get it returned + created_recordset = self.demo_cloud.create_recordset(zone, name, type_, + records, + description, ttl) + self.assertEquals(created_recordset['zone_id'], zone_obj['id']) + self.assertEquals(created_recordset['name'], name + '.' + zone) + self.assertEquals(created_recordset['type'], type_.upper()) + self.assertEquals(created_recordset['records'], records) + self.assertEquals(created_recordset['description'], description) + self.assertEquals(created_recordset['ttl'], ttl) + + # Test that we can list recordsets + recordsets = self.demo_cloud.list_recordsets(zone) + self.assertIsNotNone(recordsets) + + # Test we get the same recordset with the get_recordset method + get_recordset = self.demo_cloud.get_recordset(zone, + created_recordset['id']) + self.assertEquals(get_recordset['id'], created_recordset['id']) + + # Test the get method also works by name + get_recordset = self.demo_cloud.get_recordset(zone, name + '.' + zone) + self.assertEquals(get_recordset['id'], created_recordset['id']) + + # Test we can update a field on the recordset and only that field + # is updated + updated_recordset = self.demo_cloud.update_recordset(zone_obj['id'], + name + '.' + zone, + ttl=7200) + self.assertEquals(updated_recordset['id'], created_recordset['id']) + self.assertEquals(updated_recordset['name'], name + '.' + zone) + self.assertEquals(updated_recordset['type'], type_.upper()) + self.assertEquals(updated_recordset['records'], records) + self.assertEquals(updated_recordset['description'], description) + self.assertEquals(updated_recordset['ttl'], 7200) + + # Test we can delete and get True returned + deleted_recordset = self.demo_cloud.delete_recordset( + zone, name + '.' + zone) + self.assertTrue(deleted_recordset) + + def cleanup(self, zone_name, recordset_name): + self.demo_cloud.delete_recordset( + zone_name, recordset_name + '.' + zone_name) + self.demo_cloud.delete_zone(zone_name) diff --git a/shade/tests/unit/test_recordset.py b/shade/tests/unit/test_recordset.py new file mode 100644 index 000000000..7a5b37b39 --- /dev/null +++ b/shade/tests/unit/test_recordset.py @@ -0,0 +1,114 @@ +# -*- 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. + + +import mock +import testtools + +import shade +from shade.tests.unit import base +from shade.tests import fakes + +zone_obj = fakes.FakeZone( + id='1', + name='example.net.', + type_='PRIMARY', + email='test@example.net', + description='Example zone', + ttl=3600, + masters=None +) + +recordset_obj = fakes.FakeRecordset( + zone='1', + id='1', + name='www.example.net.', + type_='A', + description='Example zone', + ttl=3600, + records=['192.168.1.1'] +) + + +class TestRecordset(base.TestCase): + + def setUp(self): + super(TestRecordset, self).setUp() + + @mock.patch.object(shade.OpenStackCloud, 'designate_client') + def test_create_recordset(self, mock_designate): + mock_designate.zones.list.return_value = [zone_obj] + self.cloud.create_recordset(zone=recordset_obj.zone, + name=recordset_obj.name, + recordset_type=recordset_obj.type_, + records=recordset_obj.records, + description=recordset_obj.description, + ttl=recordset_obj.ttl) + mock_designate.recordsets.create.assert_called_once_with( + zone=recordset_obj.zone, name=recordset_obj.name, + type_=recordset_obj.type_.upper(), + records=recordset_obj.records, + description=recordset_obj.description, + ttl=recordset_obj.ttl + ) + + @mock.patch.object(shade.OpenStackCloud, 'designate_client') + def test_create_recordset_exception(self, mock_designate): + mock_designate.zones.list.return_value = [zone_obj] + mock_designate.recordsets.create.side_effect = Exception() + with testtools.ExpectedException( + shade.OpenStackCloudException, + "Unable to create recordset www2.example.net." + ): + self.cloud.create_recordset('1', 'www2.example.net.', + 'a', ['192.168.1.2']) + + @mock.patch.object(shade.OpenStackCloud, 'designate_client') + def test_update_recordset(self, mock_designate): + new_ttl = 7200 + mock_designate.zones.list.return_value = [zone_obj] + mock_designate.recordsets.list.return_value = [recordset_obj] + self.cloud.update_recordset('1', '1', ttl=new_ttl) + mock_designate.recordsets.update.assert_called_once_with( + zone='1', recordset='1', values={'ttl': new_ttl} + ) + + @mock.patch.object(shade.OpenStackCloud, 'designate_client') + def test_delete_recordset(self, mock_designate): + mock_designate.zones.list.return_value = [zone_obj] + mock_designate.recordsets.list.return_value = [recordset_obj] + self.cloud.delete_recordset('1', '1') + mock_designate.recordsets.delete.assert_called_once_with( + zone='1', recordset='1' + ) + + @mock.patch.object(shade.OpenStackCloud, 'designate_client') + def test_get_recordset_by_id(self, mock_designate): + mock_designate.recordsets.get.return_value = recordset_obj + recordset = self.cloud.get_recordset('1', '1') + self.assertTrue(mock_designate.recordsets.get.called) + self.assertEqual(recordset['id'], '1') + + @mock.patch.object(shade.OpenStackCloud, 'designate_client') + def test_get_recordset_by_name(self, mock_designate): + mock_designate.recordsets.get.return_value = recordset_obj + recordset = self.cloud.get_recordset('1', 'www.example.net.') + self.assertTrue(mock_designate.recordsets.get.called) + self.assertEqual(recordset['name'], 'www.example.net.') + + @mock.patch.object(shade.OpenStackCloud, 'designate_client') + def test_get_recordset_not_found_returns_false(self, mock_designate): + mock_designate.recordsets.get.return_value = None + recordset = self.cloud.get_recordset('1', 'www.nonexistingrecord.net.') + self.assertFalse(recordset) From 04774d8f805047ea61be878c1c0a85d0f059cd99 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 4 May 2016 09:54:26 -0400 Subject: [PATCH 0918/3836] Add release note doc to dev guide Add a section to our Coding Standards documentation that describes the use of reno for shade release notes. Change-Id: I87973c5b3f68727aa0cb1ccb9a82c90a8a32ebd1 --- doc/source/coding.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doc/source/coding.rst b/doc/source/coding.rst index 40a49e7c9..eca626440 100644 --- a/doc/source/coding.rst +++ b/doc/source/coding.rst @@ -15,6 +15,20 @@ Some of it just hasn't been changed yet. But be clear, all new code Below are the patterns that we expect Shade developers to follow. +Release Notes +============= + +Shade uses `reno `_ for +managing its release notes. A new release note should be added to +your contribution anytime you add new API calls, fix significant bugs, +add new functionality or parameters to existing API calls, or make any +other significant changes to the code base that we should draw attention +to for the user base. + +It is *not* necessary to add release notes for minor fixes, such as +correction of documentation typos, minor code cleanup or reorganization, +or any other change that a user would not notice through normal usage. + API Methods =========== From 068028cbe72cf9081925bf394d3289787a08b7f7 Mon Sep 17 00:00:00 2001 From: Tim Laszlo Date: Tue, 3 May 2016 15:20:02 -0500 Subject: [PATCH 0919/3836] Add support for server groups This adds support to create and delete server groups. Change-Id: Iecf0ffabe50801ec9bd2bb9ea2dbb17ead4d7cd2 --- ...server_group_support-dfa472e3dae7d34d.yaml | 3 + shade/_tasks.py | 20 +++++ shade/openstackcloud.py | 80 +++++++++++++++++++ shade/tests/fakes.py | 7 ++ shade/tests/functional/test_server_group.py | 45 +++++++++++ shade/tests/unit/test_server_group.py | 44 ++++++++++ 6 files changed, 199 insertions(+) create mode 100644 releasenotes/notes/add_server_group_support-dfa472e3dae7d34d.yaml create mode 100644 shade/tests/functional/test_server_group.py create mode 100644 shade/tests/unit/test_server_group.py diff --git a/releasenotes/notes/add_server_group_support-dfa472e3dae7d34d.yaml b/releasenotes/notes/add_server_group_support-dfa472e3dae7d34d.yaml new file mode 100644 index 000000000..e90384134 --- /dev/null +++ b/releasenotes/notes/add_server_group_support-dfa472e3dae7d34d.yaml @@ -0,0 +1,3 @@ +--- +features: + - Adds support to create and delete server groups. diff --git a/shade/_tasks.py b/shade/_tasks.py index 8c2e10821..6265dff69 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -172,6 +172,26 @@ def main(self, client): return client.nova_client.servers.rebuild(**self.args) +class ServerGroupList(task_manager.Task): + def main(self, client): + return client.nova_client.server_groups.list(**self.args) + + +class ServerGroupGet(task_manager.Task): + def main(self, client): + return client.nova_client.server_groups.get(**self.args) + + +class ServerGroupCreate(task_manager.Task): + def main(self, client): + return client.nova_client.server_groups.create(**self.args) + + +class ServerGroupDelete(task_manager.Task): + def main(self, client): + return client.nova_client.server_groups.delete(**self.args) + + class HypervisorList(task_manager.Task): def main(self, client): return client.nova_client.hypervisors.list(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index f86d4d141..075dd63a6 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1212,6 +1212,20 @@ def search_servers(self, name_or_id=None, filters=None, detailed=False): servers = self.list_servers(detailed=detailed) return _utils._filter_list(servers, name_or_id, filters) + def search_server_groups(self, name_or_id=None, filters=None): + """Seach server groups. + + :param name: server group name or id. + :param filters: a dict containing additional filters to use. + + :returns: a list of dicts containing the server groups + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + server_groups = self.list_server_groups() + return _utils._filter_list(server_groups, name_or_id, filters) + def search_images(self, name_or_id=None, filters=None): images = self.list_images() return _utils._filter_list(images, name_or_id, filters) @@ -1461,6 +1475,15 @@ def _list_servers(self, detailed=False): for server in servers ] + def list_server_groups(self): + """List all available server groups. + + :returns: A list of server group dicts. + + """ + with _utils.shade_exceptions("Error fetching server group list"): + return self.manager.submitTask(_tasks.ServerGroupList()) + @_utils.cache_on_arguments(should_cache_fn=_no_pending_images) def list_images(self, filter_deleted=True): """Get available glance images. @@ -1933,6 +1956,25 @@ def get_server_by_id(self, id): self.manager.submitTask(_tasks.ServerGet(server=id)), cloud_name=self.name, region_name=self.region_name)) + def get_server_group(self, name_or_id=None, filters=None): + """Get a server group by name or ID. + + :param name_or_id: Name or ID of the server group. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'policy': 'affinity', + } + + :returns: A server groups dict or None if no matching server group + is found. + + """ + return _utils._get_entity(self.search_server_groups, name_or_id, + filters) + def get_image(self, name_or_id, filters=None): """Get an image by name or ID. @@ -4352,6 +4394,44 @@ def _delete_server( self._servers_time = self._servers_time - self._SERVER_AGE return True + def create_server_group(self, name, policies): + """Create a new server group. + + :param name: Name of the server group being created + :param policies: List of policies for the server group. + + :returns: a dict representing the new server group. + + :raises: OpenStackCloudException on operation error. + """ + with _utils.shade_exceptions( + "Unable to create server group {name}".format( + name=name)): + return self.manager.submitTask(_tasks.ServerGroupCreate( + name=name, policies=policies)) + + def delete_server_group(self, name_or_id): + """Delete a server group. + + :param name_or_id: Name or id of the server group to delete + + :returns: True if delete succeeded, False otherwise + + :raises: OpenStackCloudException on operation error. + """ + server_group = self.get_server_group(name_or_id) + if not server_group: + self.log.debug("Server group %s not found for deleting" % + name_or_id) + return False + + with _utils.shade_exceptions( + "Error deleting server group {name}".format(name=name_or_id)): + self.manager.submitTask( + _tasks.ServerGroupDelete(id=server_group['id'])) + + return True + def list_containers(self, full_listing=True): try: return self.manager.submitTask(_tasks.ContainerList( diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 01cb822fb..580face58 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -114,6 +114,13 @@ def __init__( self.interface_ip = interface_ip +class FakeServerGroup(object): + def __init__(self, id, name, policies): + self.id = id + self.name = name + self.policies = policies + + class FakeService(object): def __init__(self, id, name, type, service_type, description='', enabled=True): diff --git a/shade/tests/functional/test_server_group.py b/shade/tests/functional/test_server_group.py new file mode 100644 index 000000000..e300e331a --- /dev/null +++ b/shade/tests/functional/test_server_group.py @@ -0,0 +1,45 @@ +# -*- 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. + +""" +test_server_group +---------------------------------- + +Functional tests for `shade` server_group resource. +""" + +from shade.tests.functional import base + + +class TestServerGroup(base.BaseFunctionalTestCase): + + def setUp(self): + super(TestServerGroup, self).setUp() + + def test_server_group(self): + server_group_name = self.getUniqueString() + self.addCleanup(self.cleanup, server_group_name) + server_group = self.demo_cloud.create_server_group( + server_group_name, ['affinity']) + + server_group_ids = [v['id'] + for v in self.demo_cloud.list_server_groups()] + self.assertIn(server_group['id'], server_group_ids) + + self.demo_cloud.delete_server_group(server_group_name) + + def cleanup(self, server_group_name): + server_group = self.demo_cloud.get_server_group(server_group_name) + if server_group: + self.demo_cloud.delete_server_group(server_group['id']) diff --git a/shade/tests/unit/test_server_group.py b/shade/tests/unit/test_server_group.py new file mode 100644 index 000000000..88fe2bb49 --- /dev/null +++ b/shade/tests/unit/test_server_group.py @@ -0,0 +1,44 @@ +# -*- 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. + + +import mock + +import shade +from shade.tests.unit import base +from shade.tests import fakes + + +class TestServerGroup(base.TestCase): + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_create_server_group(self, mock_nova): + server_group_name = 'my-server-group' + self.cloud.create_server_group(name=server_group_name, + policies=['affinity']) + + mock_nova.server_groups.create.assert_called_once_with( + name=server_group_name, policies=['affinity'] + ) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_delete_server_group(self, mock_nova): + mock_nova.server_groups.list.return_value = [ + fakes.FakeServerGroup('1234', 'name', ['affinity']) + ] + self.assertTrue(self.cloud.delete_server_group('1234')) + mock_nova.server_groups.list.assert_called_once_with() + mock_nova.server_groups.delete.assert_called_once_with( + id='1234' + ) From e6891082b3e58fe7a8940a6a77408210bdf95637 Mon Sep 17 00:00:00 2001 From: Tim Laszlo Date: Thu, 28 Apr 2016 16:46:25 -0500 Subject: [PATCH 0920/3836] Add support for host aggregates This adds support to manage host aggregates and host aggregate membership. Change-Id: Iec84164c535171116dd3f97f30c5dc249bf09f0d --- ...st_aggregate_support-471623faf45ec3c3.yaml | 4 + shade/_tasks.py | 35 ++++ shade/operatorcloud.py | 166 ++++++++++++++++++ shade/tests/fakes.py | 14 ++ shade/tests/functional/test_aggregate.py | 63 +++++++ shade/tests/unit/test_aggregate.py | 116 ++++++++++++ 6 files changed, 398 insertions(+) create mode 100644 releasenotes/notes/add_host_aggregate_support-471623faf45ec3c3.yaml create mode 100644 shade/tests/functional/test_aggregate.py create mode 100644 shade/tests/unit/test_aggregate.py diff --git a/releasenotes/notes/add_host_aggregate_support-471623faf45ec3c3.yaml b/releasenotes/notes/add_host_aggregate_support-471623faf45ec3c3.yaml new file mode 100644 index 000000000..6a6ff37a1 --- /dev/null +++ b/releasenotes/notes/add_host_aggregate_support-471623faf45ec3c3.yaml @@ -0,0 +1,4 @@ +--- +features: + - Add support for host aggregates and host aggregate + membership. diff --git a/shade/_tasks.py b/shade/_tasks.py index 8c2e10821..10fbacb52 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -177,6 +177,41 @@ def main(self, client): return client.nova_client.hypervisors.list(**self.args) +class AggregateList(task_manager.Task): + def main(self, client): + return client.nova_client.aggregates.list(**self.args) + + +class AggregateCreate(task_manager.Task): + def main(self, client): + return client.nova_client.aggregates.create(**self.args) + + +class AggregateUpdate(task_manager.Task): + def main(self, client): + return client.nova_client.aggregates.update(**self.args) + + +class AggregateDelete(task_manager.Task): + def main(self, client): + return client.nova_client.aggregates.delete(**self.args) + + +class AggregateAddHost(task_manager.Task): + def main(self, client): + return client.nova_client.aggregates.add_host(**self.args) + + +class AggregateRemoveHost(task_manager.Task): + def main(self, client): + return client.nova_client.aggregates.remove_host(**self.args) + + +class AggregateSetMetadata(task_manager.Task): + def main(self, client): + return client.nova_client.aggregates.set_metadata(**self.args) + + class KeypairList(task_manager.Task): def main(self, client): return client.nova_client.keypairs.list() diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 017ffd437..7fb79c293 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1764,3 +1764,169 @@ def list_hypervisors(self): with _utils.shade_exceptions("Error fetching hypervisor list"): return self.manager.submitTask(_tasks.HypervisorList()) + + def search_aggregates(self, name_or_id=None, filters=None): + """Seach host aggregates. + + :param name: aggregate name or id. + :param filters: a dict containing additional filters to use. + + :returns: a list of dicts containing the aggregates + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + aggregates = self.list_aggregates() + return _utils._filter_list(aggregates, name_or_id, filters) + + def list_aggregates(self): + """List all available host aggregates. + + :returns: A list of aggregate dicts. + + """ + with _utils.shade_exceptions("Error fetching aggregate list"): + return self.manager.submitTask(_tasks.AggregateList()) + + def get_aggregate(self, name_or_id, filters=None): + """Get an aggregate by name or ID. + + :param name_or_id: Name or ID of the aggregate. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'availability_zone': 'nova', + 'metadata': { + 'cpu_allocation_ratio': '1.0' + } + } + + :returns: An aggregate dict or None if no matching aggregate is + found. + + """ + return _utils._get_entity(self.search_aggregates, name_or_id, filters) + + def create_aggregate(self, name, availability_zone=None): + """Create a new host aggregate. + + :param name: Name of the host aggregate being created + :param availability_zone: Availability zone to assign hosts + + :returns: a dict representing the new host aggregate. + + :raises: OpenStackCloudException on operation error. + """ + with _utils.shade_exceptions( + "Unable to create host aggregate {name}".format( + name=name)): + return self.manager.submitTask(_tasks.AggregateCreate( + name=name, availability_zone=availability_zone)) + + @_utils.valid_kwargs('name', 'availability_zone') + def update_aggregate(self, name_or_id, **kwargs): + """Update a host aggregate. + + :param name_or_id: Name or ID of the aggregate being updated. + :param name: New aggregate name + :param availability_zone: Availability zone to assign to hosts + + :returns: a dict representing the updated host aggregate. + + :raises: OpenStackCloudException on operation error. + """ + aggregate = self.get_aggregate(name_or_id) + if not aggregate: + raise OpenStackCloudException( + "Host aggregate %s not found." % name_or_id) + + with _utils.shade_exceptions( + "Error updating aggregate {name}".format(name=name_or_id)): + new_aggregate = self.manager.submitTask( + _tasks.AggregateUpdate( + aggregate=aggregate['id'], values=kwargs)) + + return new_aggregate + + def delete_aggregate(self, name_or_id): + """Delete a host aggregate. + + :param name_or_id: Name or ID of the host aggregate to delete. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException on operation error. + """ + aggregate = self.get_aggregate(name_or_id) + if not aggregate: + self.log.debug("Aggregate %s not found for deleting" % name_or_id) + return False + + with _utils.shade_exceptions( + "Error deleting aggregate {name}".format(name=name_or_id)): + self.manager.submitTask( + _tasks.AggregateDelete(aggregate=aggregate['id'])) + + return True + + def set_aggregate_metadata(self, name_or_id, metadata): + """Set aggregate metadata, replacing the existing metadata. + + :param name_or_id: Name of the host aggregate to update + :param metadata: Dict containing metadata to replace (Use + {'key': None} to remove a key) + + :returns: a dict representing the new host aggregate. + + :raises: OpenStackCloudException on operation error. + """ + aggregate = self.get_aggregate(name_or_id) + if not aggregate: + raise OpenStackCloudException( + "Host aggregate %s not found." % name_or_id) + + with _utils.shade_exceptions( + "Unable to set metadata for host aggregate {name}".format( + name=name_or_id)): + return self.manager.submitTask(_tasks.AggregateSetMetadata( + aggregate=aggregate['id'], metadata=metadata)) + + def add_host_to_aggregate(self, name_or_id, host_name): + """Add a host to an aggregate. + + :param name_or_id: Name or ID of the host aggregate. + :param host_name: Host to add. + + :raises: OpenStackCloudException on operation error. + """ + aggregate = self.get_aggregate(name_or_id) + if not aggregate: + raise OpenStackCloudException( + "Host aggregate %s not found." % name_or_id) + + with _utils.shade_exceptions( + "Unable to add host {host} to aggregate {name}".format( + name=name_or_id, host=host_name)): + return self.manager.submitTask(_tasks.AggregateAddHost( + aggregate=aggregate['id'], host=host_name)) + + def remove_host_from_aggregate(self, name_or_id, host_name): + """Remove a host from an aggregate. + + :param name_or_id: Name or ID of the host aggregate. + :param host_name: Host to remove. + + :raises: OpenStackCloudException on operation error. + """ + aggregate = self.get_aggregate(name_or_id) + if not aggregate: + raise OpenStackCloudException( + "Host aggregate %s not found." % name_or_id) + + with _utils.shade_exceptions( + "Unable to remove host {host} from aggregate {name}".format( + name=name_or_id, host=host_name)): + return self.manager.submitTask(_tasks.AggregateRemoveHost( + aggregate=aggregate['id'], host=host_name)) diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 01cb822fb..53e068590 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -272,3 +272,17 @@ def __init__(self, zone, id, name, type_, description, self.description = description self.ttl = ttl self.records = records + + +class FakeAggregate(object): + def __init__(self, id, name, availability_zone=None, metadata=None, + hosts=None): + self.id = id + self.name = name + self.availability_zone = availability_zone + if not metadata: + metadata = {} + self.metadata = metadata + if not hosts: + hosts = [] + self.hosts = hosts diff --git a/shade/tests/functional/test_aggregate.py b/shade/tests/functional/test_aggregate.py new file mode 100644 index 000000000..7e734e43c --- /dev/null +++ b/shade/tests/functional/test_aggregate.py @@ -0,0 +1,63 @@ +# -*- 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. + +""" +test_aggregate +---------------------------------- + +Functional tests for `shade` aggregate resource. +""" + +from shade.tests.functional import base + + +class TestAggregate(base.BaseFunctionalTestCase): + + def setUp(self): + super(TestAggregate, self).setUp() + + def test_aggregates(self): + aggregate_name = self.getUniqueString() + availability_zone = self.getUniqueString() + self.addCleanup(self.cleanup, aggregate_name) + aggregate = self.operator_cloud.create_aggregate(aggregate_name) + + aggregate_ids = [v['id'] + for v in self.operator_cloud.list_aggregates()] + self.assertIn(aggregate['id'], aggregate_ids) + + aggregate = self.operator_cloud.update_aggregate( + aggregate_name, + availability_zone=availability_zone + ) + self.assertEqual(availability_zone, aggregate['availability_zone']) + + aggregate = self.operator_cloud.set_aggregate_metadata( + aggregate_name, + {'key': 'value'} + ) + self.assertIn('key', aggregate['metadata']) + + aggregate = self.operator_cloud.set_aggregate_metadata( + aggregate_name, + {'key': None} + ) + self.assertNotIn('key', aggregate['metadata']) + + self.operator_cloud.delete_aggregate(aggregate_name) + + def cleanup(self, aggregate_name): + aggregate = self.operator_cloud.get_aggregate(aggregate_name) + if aggregate: + self.operator_cloud.delete_aggregate(aggregate_name) diff --git a/shade/tests/unit/test_aggregate.py b/shade/tests/unit/test_aggregate.py new file mode 100644 index 000000000..02f8859df --- /dev/null +++ b/shade/tests/unit/test_aggregate.py @@ -0,0 +1,116 @@ +# -*- 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. + + +import mock + +import shade +from shade.tests.unit import base +from shade.tests import fakes + + +class TestAggregate(base.TestCase): + + def setUp(self): + super(TestAggregate, self).setUp() + self.cloud = shade.operator_cloud(validate=False) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_create_aggregate(self, mock_nova): + aggregate_name = 'aggr1' + self.cloud.create_aggregate(name=aggregate_name) + + mock_nova.aggregates.create.assert_called_once_with( + name=aggregate_name, availability_zone=None + ) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_create_aggregate_with_az(self, mock_nova): + aggregate_name = 'aggr1' + availability_zone = 'az1' + self.cloud.create_aggregate(name=aggregate_name, + availability_zone=availability_zone) + + mock_nova.aggregates.create.assert_called_once_with( + name=aggregate_name, availability_zone=availability_zone + ) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_delete_aggregate(self, mock_nova): + mock_nova.aggregates.list.return_value = [ + fakes.FakeAggregate('1234', 'name') + ] + self.assertTrue(self.cloud.delete_aggregate('1234')) + mock_nova.aggregates.list.assert_called_once_with() + mock_nova.aggregates.delete.assert_called_once_with( + aggregate='1234' + ) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_update_aggregate_set_az(self, mock_nova): + mock_nova.aggregates.list.return_value = [ + fakes.FakeAggregate('1234', 'name') + ] + self.cloud.update_aggregate('1234', availability_zone='az') + mock_nova.aggregates.update.assert_called_once_with( + aggregate='1234', + values={'availability_zone': 'az'}, + ) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_update_aggregate_unset_az(self, mock_nova): + mock_nova.aggregates.list.return_value = [ + fakes.FakeAggregate('1234', 'name', availability_zone='az') + ] + self.cloud.update_aggregate('1234', availability_zone=None) + mock_nova.aggregates.update.assert_called_once_with( + aggregate='1234', + values={'availability_zone': None}, + ) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_set_aggregate_metadata(self, mock_nova): + metadata = {'key', 'value'} + mock_nova.aggregates.list.return_value = [ + fakes.FakeAggregate('1234', 'name') + ] + self.cloud.set_aggregate_metadata('1234', metadata) + mock_nova.aggregates.set_metadata.assert_called_once_with( + aggregate='1234', + metadata=metadata + ) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_add_host_to_aggregate(self, mock_nova): + hostname = 'host1' + mock_nova.aggregates.list.return_value = [ + fakes.FakeAggregate('1234', 'name') + ] + self.cloud.add_host_to_aggregate('1234', hostname) + mock_nova.aggregates.add_host.assert_called_once_with( + aggregate='1234', + host=hostname + ) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_remove_host_from_aggregate(self, mock_nova): + hostname = 'host1' + mock_nova.aggregates.list.return_value = [ + fakes.FakeAggregate('1234', 'name', hosts=[hostname]) + ] + self.cloud.remove_host_from_aggregate('1234', hostname) + mock_nova.aggregates.remove_host.assert_called_once_with( + aggregate='1234', + host=hostname + ) From a53db2c68094887f132f67de001d0e382e2a1b1b Mon Sep 17 00:00:00 2001 From: Yolanda Robla Date: Wed, 4 May 2016 16:50:42 +0200 Subject: [PATCH 0921/3836] Move cloud fixtures to independent yaml files Instead of hardcoding the cloud data inside the code, move them to independent fixtures, so they are easily reusable, and we have freedom to add more cloud fixtures in the future. Change-Id: I57e460684ccc203b2eb5c70019e64b81ed5f2396 --- shade/tests/unit/base.py | 30 +++------- shade/tests/unit/fixtures/clouds/clouds.yaml | 17 ++++++ .../unit/fixtures/clouds/clouds_cache.yaml | 22 +++++++ shade/tests/unit/test_caching.py | 58 ++----------------- 4 files changed, 52 insertions(+), 75 deletions(-) create mode 100644 shade/tests/unit/fixtures/clouds/clouds.yaml create mode 100644 shade/tests/unit/fixtures/clouds/clouds_cache.yaml diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index d9d6012c4..8768ebb7a 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -20,7 +20,6 @@ import fixtures import os_client_config as occ import tempfile -import yaml import shade.openstackcloud from shade.tests import base @@ -28,26 +27,7 @@ class TestCase(base.TestCase): - """Test case base class for all unit tests.""" - - CLOUD_CONFIG = { - 'clouds': - { - '_test_cloud_': - { - 'auth': - { - 'auth_url': 'http://198.51.100.1:35357/v2.0', - 'username': '_test_user_', - 'password': '_test_pass_', - 'project_name': '_test_project_', - }, - 'region_name': '_test_region_', - }, - }, - } - - def setUp(self): + def setUp(self, cloud_config_fixture='clouds.yaml'): """Run before each test method to initialize test environment.""" super(TestCase, self).setUp() @@ -61,11 +41,17 @@ def _nosleep(seconds): self.sleep_fixture = self.useFixture(fixtures.MonkeyPatch( 'time.sleep', _nosleep)) + self.fixtures_directory = 'shade/tests/unit/fixtures' # Isolate os-client-config from test environment config = tempfile.NamedTemporaryFile(delete=False) - config.write(bytes(yaml.dump(self.CLOUD_CONFIG).encode('utf-8'))) + cloud_path = '%s/clouds/%s' % (self.fixtures_directory, + cloud_config_fixture) + with open(cloud_path, 'rb') as f: + content = f.read() + config.write(content) config.close() + vendor = tempfile.NamedTemporaryFile(delete=False) vendor.write(b'{}') vendor.close() diff --git a/shade/tests/unit/fixtures/clouds/clouds.yaml b/shade/tests/unit/fixtures/clouds/clouds.yaml new file mode 100644 index 000000000..6be237471 --- /dev/null +++ b/shade/tests/unit/fixtures/clouds/clouds.yaml @@ -0,0 +1,17 @@ +clouds: + _test_cloud_: + auth: + auth_url: http://192.168.0.19:35357 + password: password + project_name: admin + username: admin + identity_api_version: '2.0' + region_name: RegionOne + _bogus_test_: + auth_type: bogus + auth: + auth_url: http://198.51.100.1:35357/v2.0 + username: _test_user_ + password: _test_pass_ + project_name: _test_project_ + region_name: _test_region_ diff --git a/shade/tests/unit/fixtures/clouds/clouds_cache.yaml b/shade/tests/unit/fixtures/clouds/clouds_cache.yaml new file mode 100644 index 000000000..bc39142e9 --- /dev/null +++ b/shade/tests/unit/fixtures/clouds/clouds_cache.yaml @@ -0,0 +1,22 @@ +cache: + max_age: 90 + class: dogpile.cache.memory + expiration: + server: 1 +clouds: + _test_cloud_: + auth: + auth_url: http://192.168.0.19:35357 + password: password + project_name: admin + username: admin + identity_api_version: '2.0' + region_name: RegionOne + _bogus_test_: + auth_type: bogus + auth: + auth_url: http://198.51.100.1:35357/v2.0 + username: _test_user_ + password: _test_pass_ + project_name: _test_project_ + region_name: _test_region_ diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 8626bd9fa..477cd9906 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -97,30 +97,9 @@ def _(msg): class TestMemoryCache(base.TestCase): - CLOUD_CONFIG = { - 'cache': - { - 'max_age': 90, - 'class': 'dogpile.cache.memory', - 'expiration': { - 'server': 1, - }, - }, - 'clouds': - { - '_test_cloud_': - { - 'auth': - { - 'auth_url': 'http://198.51.100.1:35357/v2.0', - 'username': '_test_user_', - 'password': '_test_pass_', - 'project_name': '_test_project_', - }, - 'region_name': '_test_region_', - }, - }, - } + def setUp(self): + super(TestMemoryCache, self).setUp( + cloud_config_fixture='clouds_cache.yaml') def test_openstack_cloud(self): self.assertIsInstance(self.cloud, shade.OpenStackCloud) @@ -522,37 +501,10 @@ def __init__(self, id): class TestBogusAuth(base.TestCase): - CLOUD_CONFIG = { - 'clouds': - { - '_test_cloud_': - { - 'auth': - { - 'auth_url': 'http://198.51.100.1:35357/v2.0', - 'username': '_test_user_', - 'password': '_test_pass_', - 'project_name': '_test_project_', - }, - 'region_name': '_test_region_', - }, - '_bogus_test_': - { - 'auth_type': 'bogus', - 'auth': - { - 'auth_url': 'http://198.51.100.1:35357/v2.0', - 'username': '_test_user_', - 'password': '_test_pass_', - 'project_name': '_test_project_', - }, - 'region_name': '_test_region_', - }, - }, - } def setUp(self): - super(TestBogusAuth, self).setUp() + super(TestBogusAuth, self).setUp( + cloud_config_fixture='clouds_cache.yaml') def test_get_auth_bogus(self): with testtools.ExpectedException(exc.OpenStackCloudException): From f728df506cb0f79c8c97d1e4cda983aac0478e00 Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Fri, 6 May 2016 13:43:55 +0200 Subject: [PATCH 0922/3836] Amend the valid fields to update on recordsets Recordsets fields that can be modified are ttl, description and records. This was probably a copy-pasta from update_zone (sorry!). Change-Id: I1b46c5634ceedf3d0f258a8625f0adb2f1a3584a --- shade/openstackcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index f86d4d141..e3f683841 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5485,7 +5485,7 @@ def create_recordset(self, zone, name, recordset_type, records, zone=zone, name=name, type_=recordset_type, records=records, description=description, ttl=ttl)) - @_utils.valid_kwargs('email', 'description', 'ttl', 'masters') + @_utils.valid_kwargs('description', 'ttl', 'records') def update_recordset(self, zone, name_or_id, **kwargs): """Update a recordset. From b0fa4383b06af6cef748e4a4f1fdcc39265915b9 Mon Sep 17 00:00:00 2001 From: "ChangBo Guo(gcb)" Date: Sat, 7 May 2016 16:56:32 +0800 Subject: [PATCH 0923/3836] drop python3.3 support in classifier We don't run python 3.3 CI jobs anymore, so just drop it from classifier. Change-Id: I871269ae54067aae40f5dc06affbd4354104ee91 --- setup.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 89df35c11..07ac625ac 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,7 +16,6 @@ classifier = Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 - Programming Language :: Python :: 3.3 Programming Language :: Python :: 3.4 [files] From 189a60475424e7efd41fe522d5a190f480ad1317 Mon Sep 17 00:00:00 2001 From: "ChangBo Guo(gcb)" Date: Sat, 7 May 2016 16:59:20 +0800 Subject: [PATCH 0924/3836] Trivial: remove openstack/common from flake8 exclude list openstack/common was used to kepp copied files from oslo-incubator, we don't use oslo-incubator stuff, so remove it. Change-Id: Id426c41e6ae277ed1f829820771d5ffc31a81166 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 3df64e24c..d2aac05bd 100644 --- a/tox.ini +++ b/tox.ini @@ -34,5 +34,5 @@ commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasen [flake8] show-source = True builtins = _ -exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,releasenotes/source/conf.py +exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,releasenotes/source/conf.py From 44efe9c955c0e74d47b374085d45130ac78e6a4c Mon Sep 17 00:00:00 2001 From: "ChangBo Guo(gcb)" Date: Sat, 7 May 2016 17:02:37 +0800 Subject: [PATCH 0925/3836] Trivial: Remove 'MANIFEST.in' Everything in this file is automatically generated by pbr. There appears to be no good reason to keep it around. Change-Id: I73eb120dedbdb6685862d26493fc178e6dee1353 --- MANIFEST.in | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 90f8a7aef..000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,6 +0,0 @@ -include AUTHORS -include ChangeLog -exclude .gitignore -exclude .gitreview - -global-exclude *.pyc \ No newline at end of file From 090a265669940c98b84e610bbbdd10c29c3d3d02 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 9 May 2016 04:59:13 -0500 Subject: [PATCH 0926/3836] Workaround bad required params in troveclient troveclient requires username and password as parameters to the Client object, but if a Session is passed (like we do) that's not needed. A patch has been submitted to troveclient, but until that has been released, simply send None to both parameters. Change-Id: Ie130a4e83cceb7cab69bfbeb559493d195ef35e1 --- os_client_config/cloud_config.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 5dfbba929..08c739a4d 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -351,6 +351,13 @@ def get_legacy_client( constructor_kwargs['api_version'] = version else: constructor_kwargs['version'] = version + if service_key == 'database': + # TODO(mordred) Remove when https://review.openstack.org/314032 + # has landed and released. We're passing in a Session, but the + # trove Client object has username and password as required + # args + constructor_kwargs['username'] = None + constructor_kwargs['password'] = None return client_class(**constructor_kwargs) From 02e4d1dc85ff91c37af23a2660c23c95113f159c Mon Sep 17 00:00:00 2001 From: Yolanda Robla Date: Mon, 9 Nov 2015 18:56:19 +0100 Subject: [PATCH 0927/3836] Add initial setup for magnum in shade Start by adding the client, following same structure as the other ones. Change-Id: I499f9fdda08743f7b67afcc1c2f9b07df471a041 --- requirements.txt | 1 + shade/openstackcloud.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/requirements.txt b/requirements.txt index 23ead37c4..8dfe53211 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,5 +20,6 @@ python-ironicclient>=0.10.0 python-swiftclient>=2.5.0 python-heatclient>=0.3.0 python-designateclient>=2.1.0 +python-magnumclient>=2.1.0 dogpile.cache>=0.5.3 diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index f86d4d141..0d2f2ff45 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -35,6 +35,7 @@ from heatclient import exc as heat_exceptions import keystoneauth1.exceptions import keystoneclient.client +import magnumclient.client import neutronclient.neutron.client import novaclient.client import novaclient.exceptions as nova_exceptions @@ -262,6 +263,7 @@ def invalidate(self): self._swift_service_lock = threading.Lock() self._trove_client = None self._designate_client = None + self._magnum_client = None self._raw_clients = {} @@ -856,6 +858,13 @@ def trove_client(self): 'database', troveclient.client.Client) return self._trove_client + @property + def magnum_client(self): + if self._magnum_client is None: + self._magnum_client = self._get_client( + 'container', magnumclient.client.Client) + return self._magnum_client + @property def neutron_client(self): if self._neutron_client is None: From fb357bd7edec2ef72e6d655acb5f8b7e8e786a14 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 10 May 2016 11:40:35 -0400 Subject: [PATCH 0928/3836] Don't fail getting flavors if extra_specs is off Clouds can turn off extra specs. They're extra - we should not fail on getting flavors if they're not there. Change-Id: I6352fa2d64d3e92f823004a5b9f4cbfe61f11403 --- shade/openstackcloud.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index f86d4d141..877d8ed8a 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1359,8 +1359,14 @@ def list_flavors(self, get_extra=True): flavor.extra_specs = flavor.get( 'OS-FLV-WITH-EXT-SPECS:extra_specs') elif get_extra: - flavor.extra_specs = self.manager.submitTask( - _tasks.FlavorGetExtraSpecs(id=flavor.id)) + try: + flavor.extra_specs = self.manager.submitTask( + _tasks.FlavorGetExtraSpecs(id=flavor.id)) + except keystoneauth1.exceptions.http.HttpError as e: + flavor.extra_specs = [] + self.log.debug( + 'Fetching extra specs for flavor failed:' + ' {msg}'.format(msg=str(e))) return _utils.normalize_flavors(flavors) From a1ab68cd2b27a70eda965ce6ae7d3b3e56a8afce Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 13 May 2016 09:49:20 -0400 Subject: [PATCH 0929/3836] Make sure Ansible tests only use cirros images It's possible enabling OpenStack things can cause other images other than cirros to be made available (looking at you Magnum). We should make sure our Ansible tests only look for Cirros. Change-Id: Iefc01f8033629937552861366b9ddaac8058b249 --- extras/run-ansible-tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extras/run-ansible-tests.sh b/extras/run-ansible-tests.sh index a794c454b..37573b29d 100755 --- a/extras/run-ansible-tests.sh +++ b/extras/run-ansible-tests.sh @@ -84,7 +84,7 @@ fi # Until we have a module that lets us determine the image we want from # within a playbook, we have to find the image here and pass it in. # We use the openstack client instead of nova client since it can use clouds.yaml. -IMAGE=`openstack --os-cloud=${CLOUD} image list -f value -c Name | grep -v -e ramdisk -e kernel` +IMAGE=`openstack --os-cloud=${CLOUD} image list -f value -c Name | grep cirros | grep -v -e ramdisk -e kernel` if [ $? -ne 0 ] then echo "Failed to find Cirros image" From 58faf12cc2092653685d08082f888cfa295678ba Mon Sep 17 00:00:00 2001 From: Paul Belanger Date: Wed, 18 May 2016 19:10:45 -0400 Subject: [PATCH 0930/3836] Rework delete_unattached_floating_ips function The current version of delete_unattached_floating_ips was actually broken. We were passing the incorrect paramaters. All 3 actually didn't exist. So we are dropping timeout, since delete_floating_ip() doesn't support it and changing timeout to retries. Change-Id: If6adc4b1a020ec1edec41d996ace87634303ea02 Signed-off-by: Paul Belanger --- shade/openstackcloud.py | 10 ++++++---- shade/tests/unit/test_floating_ip_neutron.py | 3 +-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 24fc10653..8c2057a06 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3544,7 +3544,7 @@ def _nova_delete_floating_ip(self, floating_ip_id): fip_id=floating_ip_id, msg=str(e))) return True - def delete_unattached_floating_ips(self, wait=False, timeout=60): + def delete_unattached_floating_ips(self, retry=1): """Safely delete unattached floating ips. If the cloud can safely purge any unattached floating ips without @@ -3557,8 +3557,10 @@ def delete_unattached_floating_ips(self, wait=False, timeout=60): process is using add_auto_ip from shade, or is creating the floating IPs by passing in a server to the create_floating_ip call. - :param wait: Whether to wait for each IP to be deleted - :param timeout: How long to wait for each IP + :param retry: number of times to retry. Optional, defaults to 1, + which is in addition to the initial delete call. + A value of 0 will also cause no checking of results to + occur. :returns: True if Floating IPs have been deleted, False if not @@ -3569,7 +3571,7 @@ def delete_unattached_floating_ips(self, wait=False, timeout=60): for ip in self.list_floating_ips(): if not ip['attached']: processed.append(self.delete_floating_ip( - id=ip['id'], wait=wait, timeout=timeout)) + floating_ip_id=ip['id'], retry=retry)) return all(processed) if processed else False def _attach_ip_to_server( diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 80dbe9536..8e605496a 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -565,8 +565,7 @@ def test_cleanup_floating_ips( self.client.delete_unattached_floating_ips() mock_delete_floating_ip.assert_called_once_with( - id="this-is-a-floating-ip-id", - timeout=60, wait=False) + floating_ip_id='this-is-a-floating-ip-id', retry=1) @patch.object(OpenStackCloud, '_submit_create_fip') @patch.object(OpenStackCloud, '_get_free_fixed_port') From 74ac5dbf31d315875f2fc25ac3b3a6827a3c00dd Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 19 May 2016 14:02:37 -0500 Subject: [PATCH 0931/3836] Be more precise in our detection of provider networks A provider network with a physical type of None is a private network, not an externally routable network. Change-Id: I3856147b84137d6f76d1bb24cfe86be8513e9038 --- shade/openstackcloud.py | 6 +++--- shade/tests/unit/test_meta.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 24fc10653..2b9849829 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1616,7 +1616,7 @@ def _set_interesting_networks(self): external_networks.append(network) elif ((('router:external' in network and network['router:external']) or - 'provider:network_type' in network) and + network.get('provider:physical_network')) and network['name'] not in self._internal_network_names and network['id'] not in self._internal_network_names): external_networks.append(network) @@ -1625,8 +1625,8 @@ def _set_interesting_networks(self): if (network['name'] in self._internal_network_names or network['id'] in self._internal_network_names): internal_networks.append(network) - elif (('router:external' in network - and not network['router:external']) and + elif (not network.get('router:external', False) and + not network.get('provider:physical_network') and network['name'] not in self._external_network_names and network['id'] not in self._external_network_names): internal_networks.append(network) diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 196461508..66f64f85a 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -269,6 +269,7 @@ def test_get_server_external_provider_ipv4_neutron( 'id': 'test-net-id', 'name': 'test-net', 'provider:network_type': 'vlan', + 'provider:physical_network': 'vlan', }] srv = meta.obj_to_dict(fakes.FakeServer( @@ -281,6 +282,35 @@ def test_get_server_external_provider_ipv4_neutron( self.assertEqual(PUBLIC_V4, ip) + @mock.patch.object(shade.OpenStackCloud, 'has_service') + @mock.patch.object(shade.OpenStackCloud, 'list_subnets') + @mock.patch.object(shade.OpenStackCloud, 'list_networks') + def test_get_server_internal_provider_ipv4_neutron( + self, mock_list_networks, mock_list_subnets, + mock_has_service): + # Testing Clouds with Neutron + mock_has_service.return_value = True + mock_list_subnets.return_value = SUBNETS_WITH_NAT + mock_list_networks.return_value = [{ + 'id': 'test-net-id', + 'name': 'test-net', + 'router:external': False, + 'provider:network_type': 'vxlan', + 'provider:physical_network': None, + }] + + srv = meta.obj_to_dict(fakes.FakeServer( + id='test-id', name='test-name', status='ACTIVE', + addresses={'test-net': [{ + 'addr': PRIVATE_V4, + 'version': 4}]}, + )) + self.assertIsNone( + meta.get_server_external_ipv4(cloud=self.cloud, server=srv)) + int_ip = meta.get_server_private_ip(cloud=self.cloud, server=srv) + + self.assertEqual(PRIVATE_V4, int_ip) + @mock.patch.object(shade.OpenStackCloud, 'has_service') @mock.patch.object(shade.OpenStackCloud, 'list_subnets') @mock.patch.object(shade.OpenStackCloud, 'list_networks') From b1343ac6b65ac9b0423ad78c432f59cb1273f79f Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 20 May 2016 16:06:16 -0400 Subject: [PATCH 0932/3836] Add error logging around FIP delete To help catch leaking floating IPs, let's add some logging that might help us find out what's happening. We've been silently throwing away exceptions from the delete API that might help us narrow down the cause. Change-Id: Ia54a19015725b1d10d3d36301e0aab7bae3e3f60 --- shade/openstackcloud.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 24fc10653..cd6709725 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3452,8 +3452,12 @@ def _neutron_create_floating_ip( " Deleting".format(fip=fip_id)) try: self.delete_floating_ip(fip_id) - except Exception: - pass + except Exception as e: + self.log.error( + "FIP LEAK: Attempted to delete floating ip " + "{fip} but received {exc} exception: " + "{err}".format(fip=fip_id, exc=e.__class__, + err=str(e))) raise return fip @@ -3949,7 +3953,14 @@ def _add_auto_ip(self, server, wait=False, timeout=60, reuse=True): ip=f_ip['floating_ip_address'], id=f_ip['id'], server=server['id'])) - self.delete_floating_ip(f_ip['id']) + try: + self.delete_floating_ip(f_ip['id']) + except Exception as e: + self.log.error( + "FIP LEAK: Attempted to delete floating ip " + "{fip} but received {exc} exception: {err}".format( + fip=f_ip['id'], exc=e.__class__, err=str(e))) + raise e raise def add_ips_to_server( From fbe1b382dfeab55795341fd8c0709e9a5e8517bc Mon Sep 17 00:00:00 2001 From: Alvaro Lopez Garcia Date: Mon, 23 May 2016 11:29:23 +0200 Subject: [PATCH 0933/3836] Add missing "cloud" argument to _validate_auth_ksc The function _validate_auth_ksc was missing a "cloud" parameter so the exception formatting failed as it could not find that variable. Change-Id: Ia1caaa29fcb14d6ce7c16de1f78bbcae6c24adb0 --- os_client_config/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 2dd49c3d1..f1df7977b 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -831,7 +831,7 @@ def _get_auth_loader(self, config): config['auth']['token'] = 'notused' return loading.get_plugin_loader(config['auth_type']) - def _validate_auth_ksc(self, config): + def _validate_auth_ksc(self, config, cloud): try: import keystoneclient.auth as ksc_auth except ImportError: @@ -1005,7 +1005,7 @@ def get_one_cloud(self, cloud=None, validate=True, self.log.debug("Deferring keystone exception: {e}".format(e=e)) auth_plugin = None try: - config = self._validate_auth_ksc(config) + config = self._validate_auth_ksc(config, cloud) except Exception: raise e else: From 7f47eb2a6407830fb5febd5c7475a271d8004411 Mon Sep 17 00:00:00 2001 From: Ghe Rivero Date: Thu, 25 Feb 2016 22:04:51 -0700 Subject: [PATCH 0934/3836] Add quotas support Add the capability to get, update and reset to default quotas in compute service. Change-Id: Id814eaf25547c0272fddc43ae1d89ae613690c57 --- .../compute-quotas-b07a0f24dfac8444.yaml | 3 + shade/_tasks.py | 15 +++++ shade/meta.py | 8 ++- shade/operatorcloud.py | 66 +++++++++++++++++++ shade/tests/functional/test_quotas.py | 42 ++++++++++++ shade/tests/unit/test_quotas.py | 55 ++++++++++++++++ 6 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/compute-quotas-b07a0f24dfac8444.yaml create mode 100644 shade/tests/functional/test_quotas.py create mode 100644 shade/tests/unit/test_quotas.py diff --git a/releasenotes/notes/compute-quotas-b07a0f24dfac8444.yaml b/releasenotes/notes/compute-quotas-b07a0f24dfac8444.yaml new file mode 100644 index 000000000..6e170359c --- /dev/null +++ b/releasenotes/notes/compute-quotas-b07a0f24dfac8444.yaml @@ -0,0 +1,3 @@ +--- +features: + - Add new APIs, OperatorCloud.get_compute_quotas(), OperatorCloud.set_compute_quotas() and OperatorCloud.delete_compute_quotas() to manage nova quotas for projects and users \ No newline at end of file diff --git a/shade/_tasks.py b/shade/_tasks.py index 0f4502969..03faec340 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -877,3 +877,18 @@ def main(self, client): class RecordSetDelete(task_manager.Task): def main(self, client): return client.designate_client.recordsets.delete(**self.args) + + +class NovaQuotasSet(task_manager.Task): + def main(self, client): + return client.nova_client.quotas.update(**self.args) + + +class NovaQuotasGet(task_manager.Task): + def main(self, client): + return client.nova_client.quotas.get(**self.args) + + +class NovaQuotasDelete(task_manager.Task): + def main(self, client): + return client.nova_client.quotas.delete(**self.args) diff --git a/shade/meta.py b/shade/meta.py index a3bb663d5..cf978ccf1 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -399,7 +399,13 @@ def obj_to_dict(obj, request_id=None): instance = munch.Munch() for key in dir(obj): - value = getattr(obj, key) + try: + value = getattr(obj, key) + # some attributes can be defined as a @propierty, so we can't assure + # to have a valid value + # e.g. id in python-novaclient/tree/novaclient/v2/quotas.py + except AttributeError: + continue if isinstance(value, NON_CALLABLES) and not key.startswith('_'): instance[key] = value return _add_request_id(instance, request_id) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 7fb79c293..91df905c1 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -14,6 +14,7 @@ from ironicclient import client as ironic_client from ironicclient import exceptions as ironic_exceptions +from novaclient import exceptions as nova_exceptions from shade.exc import * # noqa from shade import openstackcloud @@ -1930,3 +1931,68 @@ def remove_host_from_aggregate(self, name_or_id, host_name): name=name_or_id, host=host_name)): return self.manager.submitTask(_tasks.AggregateRemoveHost( aggregate=aggregate['id'], host=host_name)) + + def set_compute_quotas(self, name_or_id, **kwargs): + """ Set a quota in a project + + :param name_or_id: project name or id + :param kwargs: key/value pairs of quota name and quota value + + :raises: OpenStackCloudException if the resource to set the + quota does not exist. + """ + + proj = self.get_project(name_or_id) + if not proj: + raise OpenStackCloudException("project does not exist") + + # compute_quotas = {key: val for key, val in kwargs.items() + # if key in quota.COMPUTE_QUOTAS} + # TODO(ghe): Manage volume and network quotas + # network_quotas = {key: val for key, val in kwargs.items() + # if key in quota.NETWORK_QUOTAS} + # volume_quotas = {key: val for key, val in kwargs.items() + # if key in quota.VOLUME_QUOTAS} + + try: + self.manager.submitTask( + _tasks.NovaQuotasSet(tenant_id=proj.id, + force=True, + **kwargs)) + except novaclient.exceptions.BadRequest: + raise OpenStackCloudException("No valid quota or resource") + + def get_compute_quotas(self, name_or_id): + """ Get quota for a project + + :param name_or_id: project name or id + :raises: OpenStackCloudException if it's not a valid project + + :returns: Munch object with the quotas + """ + proj = self.get_project(name_or_id) + if not proj: + raise OpenStackCloudException("project does not exist") + try: + return self.manager.submitTask( + _tasks.NovaQuotasGet(tenant_id=proj.id)) + except nova_exceptions.BadRequest: + raise OpenStackCloudException("nova client call failed") + + def delete_compute_quotas(self, name_or_id): + """ Delete quota for a project + + :param name_or_id: project name or id + :raises: OpenStackCloudException if it's not a valid project or the + nova client call failed + + :returns: dict with the quotas + """ + proj = self.get_project(name_or_id) + if not proj: + raise OpenStackCloudException("project does not exist") + try: + return self.manager.submitTask( + _tasks.NovaQuotasDelete(tenant_id=proj.id)) + except novaclient.exceptions.BadRequest: + raise OpenStackCloudException("nova client call failed") diff --git a/shade/tests/functional/test_quotas.py b/shade/tests/functional/test_quotas.py new file mode 100644 index 000000000..0609ed296 --- /dev/null +++ b/shade/tests/functional/test_quotas.py @@ -0,0 +1,42 @@ +# -*- 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. + +""" +test_quotas +---------------------------------- + +Functional tests for `shade` quotas methods. +""" + +from shade import operator_cloud +from shade.tests import base + + +class TestComputeQuotas(base.TestCase): + + def setUp(self): + super(TestComputeQuotas, self).setUp() + self.cloud = operator_cloud(cloud='devstack-admin') + if not self.cloud.has_service('compute'): + self.skipTest('compute service not supported by cloud') + + def test_quotas(self): + '''Test quotas functionality''' + quotas = self.cloud.get_compute_quotas('demo') + cores = quotas['cores'] + self.cloud.set_compute_quotas('demo', cores=cores + 1) + self.assertEqual(cores + 1, + self.cloud.get_compute_quotas('demo')['cores']) + self.cloud.delete_compute_quotas('demo') + self.assertEqual(cores, self.cloud.get_compute_quotas('demo')['cores']) diff --git a/shade/tests/unit/test_quotas.py b/shade/tests/unit/test_quotas.py new file mode 100644 index 000000000..be43f2fad --- /dev/null +++ b/shade/tests/unit/test_quotas.py @@ -0,0 +1,55 @@ +# -*- 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. + + +import mock + +import shade +from shade.tests.unit import base +from shade.tests import fakes + + +class TestQuotas(base.TestCase): + + def setUp(self): + super(TestQuotas, self).setUp() + self.cloud = shade.operator_cloud(validate=False) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_update_quotas(self, mock_keystone, mock_nova): + project = fakes.FakeProject('project_a') + mock_keystone.tenants.list.return_value = [project] + self.cloud.set_compute_quotas(project, cores=1) + + mock_nova.quotas.update.assert_called_once_with( + cores=1, force=True, tenant_id='project_a') + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_get_quotas(self, mock_keystone, mock_nova): + project = fakes.FakeProject('project_a') + mock_keystone.tenants.list.return_value = [project] + self.cloud.get_compute_quotas(project) + + mock_nova.quotas.get.assert_called_once_with(tenant_id='project_a') + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_delete_quotas(self, mock_keystone, mock_nova): + project = fakes.FakeProject('project_a') + mock_keystone.tenants.list.return_value = [project] + self.cloud.delete_compute_quotas(project) + + mock_nova.quotas.delete.assert_called_once_with(tenant_id='project_a') From 49ca65001565133aa20ebf525e2d05f5df99d7f4 Mon Sep 17 00:00:00 2001 From: Ghe Rivero Date: Fri, 20 May 2016 09:28:03 +0200 Subject: [PATCH 0935/3836] Add volume quotas support Add the capability to get, update and reset to default quotas in volume service. Change-Id: Ib5a996b90bb41c7ad01ac496a89478ea71a6bf2f --- .../notes/volume-quotas-5b674ee8c1f71eb6.yaml | 3 + shade/_tasks.py | 15 +++++ shade/operatorcloud.py | 57 +++++++++++++++++++ shade/tests/functional/test_quotas.py | 20 +++++++ shade/tests/unit/test_quotas.py | 29 ++++++++++ 5 files changed, 124 insertions(+) create mode 100644 releasenotes/notes/volume-quotas-5b674ee8c1f71eb6.yaml diff --git a/releasenotes/notes/volume-quotas-5b674ee8c1f71eb6.yaml b/releasenotes/notes/volume-quotas-5b674ee8c1f71eb6.yaml new file mode 100644 index 000000000..dfb3b1cd6 --- /dev/null +++ b/releasenotes/notes/volume-quotas-5b674ee8c1f71eb6.yaml @@ -0,0 +1,3 @@ +--- +features: + - Add new APIs, OperatorCloud.get_volume_quotas(), OperatorCloud.set_volume_quotas() and OperatorCloud.delete_volume_quotas() to manage cinder quotas for projects and users \ No newline at end of file diff --git a/shade/_tasks.py b/shade/_tasks.py index 03faec340..6ddb8f57a 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -892,3 +892,18 @@ def main(self, client): class NovaQuotasDelete(task_manager.Task): def main(self, client): return client.nova_client.quotas.delete(**self.args) + + +class CinderQuotasSet(task_manager.Task): + def main(self, client): + return client.cinder_client.quotas.update(**self.args) + + +class CinderQuotasGet(task_manager.Task): + def main(self, client): + return client.cinder_client.quotas.get(**self.args) + + +class CinderQuotasDelete(task_manager.Task): + def main(self, client): + return client.cinder_client.quotas.delete(**self.args) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 91df905c1..babadb98e 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -15,6 +15,7 @@ from ironicclient import client as ironic_client from ironicclient import exceptions as ironic_exceptions from novaclient import exceptions as nova_exceptions +from cinderclient import exceptions as cinder_exceptions from shade.exc import * # noqa from shade import openstackcloud @@ -1996,3 +1997,59 @@ def delete_compute_quotas(self, name_or_id): _tasks.NovaQuotasDelete(tenant_id=proj.id)) except novaclient.exceptions.BadRequest: raise OpenStackCloudException("nova client call failed") + + def set_volume_quotas(self, name_or_id, **kwargs): + """ Set a volume quota in a project + + :param name_or_id: project name or id + :param kwargs: key/value pairs of quota name and quota value + + :raises: OpenStackCloudException if the resource to set the + quota does not exist. + """ + + proj = self.get_project(name_or_id) + if not proj: + raise OpenStackCloudException("project does not exist") + + try: + self.manager.submitTask( + _tasks.CinderQuotasSet(tenant_id=proj.id, + **kwargs)) + except cinder_exceptions.BadRequest: + raise OpenStackCloudException("No valid quota or resource") + + def get_volume_quotas(self, name_or_id): + """ Get volume quotas for a project + + :param name_or_id: project name or id + :raises: OpenStackCloudException if it's not a valid project + + :returns: Munch object with the quotas + """ + proj = self.get_project(name_or_id) + if not proj: + raise OpenStackCloudException("project does not exist") + try: + return self.manager.submitTask( + _tasks.CinderQuotasGet(tenant_id=proj.id)) + except cinder_exceptions.BadRequest: + raise OpenStackCloudException("cinder client call failed") + + def delete_volume_quotas(self, name_or_id): + """ Delete volume quotas for a project + + :param name_or_id: project name or id + :raises: OpenStackCloudException if it's not a valid project or the + cinder client call failed + + :returns: dict with the quotas + """ + proj = self.get_project(name_or_id) + if not proj: + raise OpenStackCloudException("project does not exist") + try: + return self.manager.submitTask( + _tasks.CinderQuotasDelete(tenant_id=proj.id)) + except cinder_exceptions.BadRequest: + raise OpenStackCloudException("cinder client call failed") diff --git a/shade/tests/functional/test_quotas.py b/shade/tests/functional/test_quotas.py index 0609ed296..aaf732dc1 100644 --- a/shade/tests/functional/test_quotas.py +++ b/shade/tests/functional/test_quotas.py @@ -40,3 +40,23 @@ def test_quotas(self): self.cloud.get_compute_quotas('demo')['cores']) self.cloud.delete_compute_quotas('demo') self.assertEqual(cores, self.cloud.get_compute_quotas('demo')['cores']) + + +class TestVolumeQuotas(base.TestCase): + + def setUp(self): + super(TestVolumeQuotas, self).setUp() + self.cloud = operator_cloud(cloud='devstack-admin') + if not self.cloud.has_service('volume'): + self.skipTest('volume service not supported by cloud') + + def test_quotas(self): + '''Test quotas functionality''' + quotas = self.cloud.get_volume_quotas('demo') + volumes = quotas['volumes'] + self.cloud.set_volume_quotas('demo', volumes=volumes + 1) + self.assertEqual(volumes + 1, + self.cloud.get_volume_quotas('demo')['volumes']) + self.cloud.delete_volume_quotas('demo') + self.assertEqual(volumes, + self.cloud.get_volume_quotas('demo')['volumes']) diff --git a/shade/tests/unit/test_quotas.py b/shade/tests/unit/test_quotas.py index be43f2fad..b07a18d73 100644 --- a/shade/tests/unit/test_quotas.py +++ b/shade/tests/unit/test_quotas.py @@ -53,3 +53,32 @@ def test_delete_quotas(self, mock_keystone, mock_nova): self.cloud.delete_compute_quotas(project) mock_nova.quotas.delete.assert_called_once_with(tenant_id='project_a') + + @mock.patch.object(shade.OpenStackCloud, 'cinder_client') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_cinder_update_quotas(self, mock_keystone, mock_cinder): + project = fakes.FakeProject('project_a') + mock_keystone.tenants.list.return_value = [project] + self.cloud.set_volume_quotas(project, volumes=1) + + mock_cinder.quotas.update.assert_called_once_with( + volumes=1, tenant_id='project_a') + + @mock.patch.object(shade.OpenStackCloud, 'cinder_client') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_cinder_get_quotas(self, mock_keystone, mock_cinder): + project = fakes.FakeProject('project_a') + mock_keystone.tenants.list.return_value = [project] + self.cloud.get_volume_quotas(project) + + mock_cinder.quotas.get.assert_called_once_with(tenant_id='project_a') + + @mock.patch.object(shade.OpenStackCloud, 'cinder_client') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_cinder_delete_quotas(self, mock_keystone, mock_cinder): + project = fakes.FakeProject('project_a') + mock_keystone.tenants.list.return_value = [project] + self.cloud.delete_volume_quotas(project) + + mock_cinder.quotas.delete.assert_called_once_with( + tenant_id='project_a') From 7128a3e692effa4c2f742e983f73b9a64540cb8d Mon Sep 17 00:00:00 2001 From: Caleb Boylan Date: Thu, 26 May 2016 13:01:14 -0700 Subject: [PATCH 0936/3836] Make it easier to give swift objects metadata Change-Id: Ifcc11841a7742a8418fb270974209e0a9ef783a2 --- shade/openstackcloud.py | 9 ++++++++- shade/tests/functional/test_object.py | 9 ++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 8612dec91..cd95e562f 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4621,7 +4621,7 @@ def is_object_stale( def create_object( self, container, name, filename=None, md5=None, sha256=None, segment_size=None, - use_slo=True, + use_slo=True, metadata=None, **headers): """Create a file object @@ -4644,9 +4644,14 @@ def create_object( Object, use a static rather than dyanmic object. Static Objects will delete segment objects when the manifest object is deleted. (optional, defaults to True) + :param metadata: This dict will get changed into headers that set + metadata of the object :raises: ``OpenStackCloudException`` on operation error. """ + if not metadata: + metadata = {} + if not filename: filename = name @@ -4657,6 +4662,8 @@ def create_object( headers[OBJECT_MD5_KEY] = md5 headers[OBJECT_SHA256_KEY] = sha256 header_list = sorted([':'.join([k, v]) for (k, v) in headers.items()]) + for (k, v) in metadata.items(): + header_list.append(':'.join(['x-object-meta-' + k, v])) # On some clouds this is not necessary. On others it is. I'm confused. self.create_container(container) diff --git a/shade/tests/functional/test_object.py b/shade/tests/functional/test_object.py index c773dcbb6..94d5c9932 100644 --- a/shade/tests/functional/test_object.py +++ b/shade/tests/functional/test_object.py @@ -55,14 +55,17 @@ def test_create_object(self): self.demo_cloud.create_object( container_name, name, sparse_file.name, - segment_size=segment_size) + segment_size=segment_size, + metadata={'foo': 'bar'}) self.assertFalse(self.demo_cloud.is_object_stale( container_name, name, sparse_file.name ) ) - self.assertIsNotNone( - self.demo_cloud.get_object_metadata(container_name, name)) + self.assertEqual( + 'bar', self.demo_cloud.get_object_metadata( + container_name, name)['x-object-meta-foo'] + ) self.assertIsNotNone( self.demo_cloud.get_object(container_name, name)) self.assertEqual( From 8732fa7ec30d71d21f519b63c3ad687e24bcaf3c Mon Sep 17 00:00:00 2001 From: Yolanda Robla Date: Tue, 19 Apr 2016 21:33:21 +0200 Subject: [PATCH 0937/3836] Add magnum baymodel calls to shade. Change-Id: Icba6929d32ae2cc47ccb776e24bc8beac6b717d2 --- ...num_baymodel_support-e35e5aab0b14ff75.yaml | 4 + shade/_tasks.py | 21 +++ shade/_utils.py | 25 +++ shade/openstackcloud.py | 156 ++++++++++++++++++ shade/tests/functional/test_baymodels.py | 113 +++++++++++++ shade/tests/unit/test_baymodels.py | 155 +++++++++++++++++ 6 files changed, 474 insertions(+) create mode 100644 releasenotes/notes/add_magnum_baymodel_support-e35e5aab0b14ff75.yaml create mode 100644 shade/tests/functional/test_baymodels.py create mode 100644 shade/tests/unit/test_baymodels.py diff --git a/releasenotes/notes/add_magnum_baymodel_support-e35e5aab0b14ff75.yaml b/releasenotes/notes/add_magnum_baymodel_support-e35e5aab0b14ff75.yaml new file mode 100644 index 000000000..9c4f9b015 --- /dev/null +++ b/releasenotes/notes/add_magnum_baymodel_support-e35e5aab0b14ff75.yaml @@ -0,0 +1,4 @@ +--- +features: + - Add support for Magnum baymodels, with the + usual methods (search/list/get/create/update/delete). diff --git a/shade/_tasks.py b/shade/_tasks.py index 6ddb8f57a..4783c04c0 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -907,3 +907,24 @@ def main(self, client): class CinderQuotasDelete(task_manager.Task): def main(self, client): return client.cinder_client.quotas.delete(**self.args) + + +class BaymodelList(task_manager.Task): + def main(self, client): + return client.magnum_client.baymodels.list(**self.args) + + +class BaymodelCreate(task_manager.Task): + def main(self, client): + return client.magnum_client.baymodels.create(**self.args) + + +class BaymodelDelete(task_manager.Task): + def main(self, client): + return client.magnum_client.baymodels.delete(self.args['id']) + + +class BaymodelUpdate(task_manager.Task): + def main(self, client): + return client.magnum_client.baymodels.update( + self.args['id'], self.args['patch']) diff --git a/shade/_utils.py b/shade/_utils.py index 851598f46..0e1f622a9 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -495,6 +495,13 @@ def normalize_flavors(flavors): return flavors +def normalize_baymodels(baymodels): + """Normalize Magnum baymodels.""" + for baymodel in baymodels: + baymodel['id'] = baymodel['uuid'] + return baymodels + + def valid_kwargs(*valid_args): # This decorator checks if argument passed as **kwargs to a function are # present in valid_args. @@ -759,3 +766,21 @@ def range_filter(data, key, range_exp): if int(d[key]) == val_range[1]: filtered.append(d) return filtered + + +def generate_patches_from_kwargs(operation, **kwargs): + """Given a set of parameters, returns a list with the + valid patch values. + + :param string operation: The operation to perform. + :param list kwargs: Dict of parameters. + + :returns: A list with the right patch values. + """ + patches = [] + for k, v in kwargs.items(): + patch = {'op': operation, + 'value': v, + 'path': '/%s' % k} + patches.append(patch) + return patches diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 8612dec91..01e5cb419 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -30,6 +30,7 @@ import glanceclient import glanceclient.exc import heatclient.client +import magnumclient.exceptions as magnum_exceptions from heatclient.common import event_utils from heatclient.common import template_utils from heatclient import exc as heat_exceptions @@ -5652,3 +5653,158 @@ def delete_recordset(self, zone, name_or_id): _tasks.RecordSetDelete(zone=zone['id'], recordset=name_or_id)) return True + + @_utils.cache_on_arguments() + def list_baymodels(self, detail=False): + """List Magnum baymodels. + + :param bool detail. Flag to control if we need summarized or + detailed output. + + :returns: a list of dicts containing the baymodel details. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + with _utils.shade_exceptions("Error fetching baymodel list"): + baymodels = self.manager.submitTask( + _tasks.BaymodelList(detail=detail)) + return _utils.normalize_baymodels(baymodels) + + def search_baymodels(self, name_or_id=None, filters=None, detail=False): + """Search Magnum baymodels. + + :param name_or_id: baymodel name or ID. + :param filters: a dict containing additional filters to use. + :param detail: a boolean to control if we need summarized or + detailed output. + + :returns: a list of dict containing the baymodels + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + baymodels = self.list_baymodels(detail=detail) + return _utils._filter_list( + baymodels, name_or_id, filters) + + def get_baymodel(self, name_or_id, filters=None, detail=False): + """Get a baymodel by name or ID. + + :param name_or_id: Name or ID of the baymodel. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + :returns: A baymodel dict or None if no matching baymodel is + found. + + """ + return _utils._get_entity(self.search_baymodels, name_or_id, + filters=filters, detail=detail) + + def create_baymodel(self, name, image_id=None, keypair_id=None, + coe=None, **kwargs): + """Create a Magnum baymodel. + + :param string name: Name of the baymodel. + :param string image_id: Name or ID of the image to use. + :param string keypair_id: Name or ID of the keypair to use. + :param string coe: Name of the coe for the baymodel. + + Other arguments will be passed in kwargs. + + :returns: a dict containing the baymodel description + + :raises: ``OpenStackCloudException`` if something goes wrong during + the openstack API call + """ + with _utils.shade_exceptions( + "Error creating baymodel of name {baymodel_name}".format( + baymodel_name=name)): + baymodel = self.manager.submitTask( + _tasks.BaymodelCreate( + name=name, image_id=image_id, + keypair_id=keypair_id, coe=coe, **kwargs)) + + self.list_baymodels.invalidate(self) + return baymodel + + def delete_baymodel(self, name_or_id): + """Delete a baymodel. + + :param name_or_id: Name or unique ID of the baymodel. + :returns: True if the delete succeeded, False if the + baymodel was not found. + + :raises: OpenStackCloudException on operation error. + """ + + self.list_baymodels.invalidate(self) + baymodel = self.get_baymodel(name_or_id) + + if not baymodel: + self.log.debug( + "Baymodel {name_or_id} does not exist".format( + name_or_id=name_or_id), + exc_info=True) + return False + + with _utils.shade_exceptions("Error in deleting baymodel"): + try: + self.manager.submitTask( + _tasks.BaymodelDelete(id=baymodel['id'])) + except magnum_exceptions.NotFound: + self.log.debug( + "Baymodel {id} not found when deleting. Ignoring.".format( + id=baymodel['id'])) + return False + + self.list_baymodels.invalidate(self) + return True + + @_utils.valid_kwargs('name', 'image_id', 'flavor_id', 'master_flavor_id', + 'keypair_id', 'external_network_id', 'fixed_network', + 'dns_nameserver', 'docker_volume_size', 'labels', + 'coe', 'http_proxy', 'https_proxy', 'no_proxy', + 'network_driver', 'tls_disabled', 'public', + 'registry_enabled', 'volume_driver') + def update_baymodel(self, name_or_id, operation, **kwargs): + """Update a Magnum baymodel. + + :param name_or_id: Name or ID of the baymodel being updated. + :param operation: Operation to perform - add, remove, replace. + + Other arguments will be passed with kwargs. + + :returns: a dict representing the updated baymodel. + + :raises: OpenStackCloudException on operation error. + """ + self.list_baymodels.invalidate(self) + baymodel = self.get_baymodel(name_or_id) + if not baymodel: + raise OpenStackCloudException( + "Baymodel %s not found." % name_or_id) + + if operation not in ['add', 'replace', 'remove']: + raise TypeError( + "%s operation not in 'add', 'replace', 'remove'" % operation) + + patches = _utils.generate_patches_from_kwargs(operation, **kwargs) + + with _utils.shade_exceptions( + "Error updating baymodel {0}".format(name_or_id)): + self.manager.submitTask( + _tasks.BaymodelUpdate( + id=baymodel['id'], patch=patches)) + + new_baymodel = self.get_baymodel(name_or_id) + return new_baymodel diff --git a/shade/tests/functional/test_baymodels.py b/shade/tests/functional/test_baymodels.py new file mode 100644 index 000000000..c3d7e0283 --- /dev/null +++ b/shade/tests/functional/test_baymodels.py @@ -0,0 +1,113 @@ +# -*- 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. + +""" +test_baymodels +---------------------------------- + +Functional tests for `shade` baymodel methods. +""" + +from testtools import content + +from shade.tests.functional import base + +import os +import subprocess + + +class TestBaymodel(base.BaseFunctionalTestCase): + + def setUp(self): + super(TestBaymodel, self).setUp() + if not self.demo_cloud.has_service('container'): + self.skipTest('Container service not supported by cloud') + self.baymodel = None + + def test_baymodels(self): + '''Test baymodels functionality''' + name = 'fake-baymodel' + server_type = 'vm' + public = False + image_id = 'fedora-atomic-f23-dib' + tls_disabled = False + registry_enabled = False + coe = 'kubernetes' + keypair_id = 'testkey' + + self.addDetail('baymodel', content.text_content(name)) + self.addCleanup(self.cleanup, name) + + # generate a keypair to add to nova + ssh_directory = '/tmp/.ssh' + if not os.path.isdir(ssh_directory): + os.mkdir(ssh_directory) + subprocess.call( + ['ssh-keygen', '-t', 'rsa', '-N', '', '-f', + '%s/id_rsa_shade' % ssh_directory]) + + # add keypair to nova + with open('%s/id_rsa_shade.pub' % ssh_directory) as f: + key_content = f.read() + self.demo_cloud.create_keypair('testkey', key_content) + + # Test we can create a baymodel and we get it returned + self.baymodel = self.demo_cloud.create_baymodel( + name=name, image_id=image_id, + keypair_id=keypair_id, coe=coe) + self.assertEquals(self.baymodel['name'], name) + self.assertEquals(self.baymodel['image_id'], image_id) + self.assertEquals(self.baymodel['keypair_id'], keypair_id) + self.assertEquals(self.baymodel['coe'], coe) + self.assertEquals(self.baymodel['registry_enabled'], registry_enabled) + self.assertEquals(self.baymodel['tls_disabled'], tls_disabled) + self.assertEquals(self.baymodel['public'], public) + self.assertEquals(self.baymodel['server_type'], server_type) + + # Test that we can list baymodels + baymodels = self.demo_cloud.list_baymodels() + self.assertIsNotNone(baymodels) + + # Test we get the same baymodel with the get_baymodel method + baymodel_get = self.demo_cloud.get_baymodel(self.baymodel['uuid']) + self.assertEquals(baymodel_get['uuid'], self.baymodel['uuid']) + + # Test the get method also works by name + baymodel_get = self.demo_cloud.get_baymodel(name) + self.assertEquals(baymodel_get['name'], self.baymodel['name']) + + # Test we can update a field on the baymodel and only that field + # is updated + baymodel_update = self.demo_cloud.update_baymodel( + self.baymodel['uuid'], 'replace', tls_disabled=True) + self.assertEquals(baymodel_update['uuid'], + self.baymodel['uuid']) + self.assertEquals(baymodel_update['tls_disabled'], True) + + # Test we can delete and get True returned + baymodel_delete = self.demo_cloud.delete_baymodel( + self.baymodel['uuid']) + self.assertTrue(baymodel_delete) + + def cleanup(self, name): + if self.baymodel: + try: + self.demo_cloud.delete_baymodel(self.baymodel['name']) + except: + pass + + # delete keypair + self.demo_cloud.delete_keypair('testkey') + os.unlink('/tmp/.ssh/id_rsa_shade') + os.unlink('/tmp/.ssh/id_rsa_shade.pub') diff --git a/shade/tests/unit/test_baymodels.py b/shade/tests/unit/test_baymodels.py new file mode 100644 index 000000000..04d9c8fc8 --- /dev/null +++ b/shade/tests/unit/test_baymodels.py @@ -0,0 +1,155 @@ +# -*- 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. + + +import mock +import munch + +import shade +import testtools +from shade.tests.unit import base + + +baymodel_obj = munch.Munch( + apiserver_port=None, + uuid='fake-uuid', + human_id=None, + name='fake-baymodel', + server_type='vm', + public=False, + image_id='fake-image', + tls_disabled=False, + registry_enabled=False, + coe='fake-coe', + keypair_id='fake-key', +) + +baymodel_detail_obj = munch.Munch( + links={}, + labels={}, + apiserver_port=None, + uuid='fake-uuid', + human_id=None, + name='fake-baymodel', + server_type='vm', + public=False, + image_id='fake-image', + tls_disabled=False, + registry_enabled=False, + coe='fake-coe', + created_at='fake-date', + updated_at=None, + master_flavor_id=None, + no_proxy=None, + https_proxy=None, + keypair_id='fake-key', + docker_volume_size=1, + external_network_id='public', + cluster_distro='fake-distro', + volume_driver=None, + network_driver='fake-driver', + fixed_network=None, + flavor_id='fake-flavor', + dns_nameserver='8.8.8.8', +) + + +class TestBaymodels(base.TestCase): + + def setUp(self): + super(TestBaymodels, self).setUp() + self.cloud = shade.openstack_cloud(validate=False) + + @mock.patch.object(shade.OpenStackCloud, 'magnum_client') + def test_list_baymodels_without_detail(self, mock_magnum): + mock_magnum.baymodels.list.return_value = [baymodel_obj, ] + baymodels_list = self.cloud.list_baymodels() + mock_magnum.baymodels.list.assert_called_with(detail=False) + self.assertEqual(baymodels_list[0], baymodel_obj) + + @mock.patch.object(shade.OpenStackCloud, 'magnum_client') + def test_list_baymodels_with_detail(self, mock_magnum): + mock_magnum.baymodels.list.return_value = [baymodel_detail_obj, ] + baymodels_list = self.cloud.list_baymodels(detail=True) + mock_magnum.baymodels.list.assert_called_with(detail=True) + self.assertEqual(baymodels_list[0], baymodel_detail_obj) + + @mock.patch.object(shade.OpenStackCloud, 'magnum_client') + def test_search_baymodels_by_name(self, mock_magnum): + mock_magnum.baymodels.list.return_value = [baymodel_obj, ] + + baymodels = self.cloud.search_baymodels(name_or_id='fake-baymodel') + mock_magnum.baymodels.list.assert_called_with(detail=False) + + self.assertEquals(1, len(baymodels)) + self.assertEquals('fake-uuid', baymodels[0]['uuid']) + + @mock.patch.object(shade.OpenStackCloud, 'magnum_client') + def test_search_baymodels_not_found(self, mock_magnum): + mock_magnum.baymodels.list.return_value = [baymodel_obj, ] + + baymodels = self.cloud.search_baymodels(name_or_id='non-existent') + + mock_magnum.baymodels.list.assert_called_with(detail=False) + self.assertEquals(0, len(baymodels)) + + @mock.patch.object(shade.OpenStackCloud, 'search_baymodels') + def test_get_baymodel(self, mock_search): + mock_search.return_value = [baymodel_obj, ] + r = self.cloud.get_baymodel('fake-baymodel') + self.assertIsNotNone(r) + self.assertDictEqual(baymodel_obj, r) + + @mock.patch.object(shade.OpenStackCloud, 'search_baymodels') + def test_get_baymodel_not_found(self, mock_search): + mock_search.return_value = [] + r = self.cloud.get_baymodel('doesNotExist') + self.assertIsNone(r) + + @mock.patch.object(shade.OpenStackCloud, 'magnum_client') + def test_create_baymodel(self, mock_magnum): + self.cloud.create_baymodel( + name=baymodel_obj.name, image_id=baymodel_obj.image_id, + keypair_id=baymodel_obj.keypair_id, coe=baymodel_obj.coe) + mock_magnum.baymodels.create.assert_called_once_with( + name=baymodel_obj.name, image_id=baymodel_obj.image_id, + keypair_id=baymodel_obj.keypair_id, coe=baymodel_obj.coe + ) + + @mock.patch.object(shade.OpenStackCloud, 'magnum_client') + def test_create_baymodel_exception(self, mock_magnum): + mock_magnum.baymodels.create.side_effect = Exception() + with testtools.ExpectedException( + shade.OpenStackCloudException, + "Error creating baymodel of name fake-baymodel" + ): + self.cloud.create_baymodel('fake-baymodel') + + @mock.patch.object(shade.OpenStackCloud, 'magnum_client') + def test_delete_baymodel(self, mock_magnum): + mock_magnum.baymodels.list.return_value = [baymodel_obj] + self.cloud.delete_baymodel('fake-uuid') + mock_magnum.baymodels.delete.assert_called_once_with( + 'fake-uuid' + ) + + @mock.patch.object(shade.OpenStackCloud, 'magnum_client') + def test_update_baymodel(self, mock_magnum): + new_name = 'new-baymodel' + mock_magnum.baymodels.list.return_value = [baymodel_obj] + self.cloud.update_baymodel('fake-uuid', 'replace', name=new_name) + mock_magnum.baymodels.update.assert_called_once_with( + 'fake-uuid', [{'path': '/name', 'op': 'replace', + 'value': 'new-baymodel'}] + ) From c56d8e0f3858304f09469ebf8c728966ed0e810c Mon Sep 17 00:00:00 2001 From: mariojmdavid Date: Mon, 16 May 2016 17:42:53 +0100 Subject: [PATCH 0938/3836] incorporate unit test in test_shade.py, remove test_router.py fix tenant_id in router add functional test test_create_router_project to functional/test_router.py add unit/test_router.py add project_id to create_router Change-Id: Ie6775a99a84aa32b7b93bd399856972b7212d5c0 --- shade/openstackcloud.py | 5 ++++- shade/tests/functional/test_router.py | 29 +++++++++++++++++++++++++++ shade/tests/unit/test_shade.py | 13 ++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 24fc10653..8aed13dc6 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2314,7 +2314,7 @@ def list_router_interfaces(self, router, interface_type=None): def create_router(self, name=None, admin_state_up=True, ext_gateway_net_id=None, enable_snat=None, - ext_fixed_ips=None): + ext_fixed_ips=None, project_id=None): """Create a logical router. :param string name: The router name. @@ -2331,6 +2331,7 @@ def create_router(self, name=None, admin_state_up=True, "ip_address": "192.168.10.2" } ] + :param string project_id: Project ID for the router. :returns: The router object. :raises: OpenStackCloudException on operation error. @@ -2338,6 +2339,8 @@ def create_router(self, name=None, admin_state_up=True, router = { 'admin_state_up': admin_state_up } + if project_id is not None: + router['tenant_id'] = project_id if name: router['name'] = name ext_gw_info = self._build_external_gateway_info( diff --git a/shade/tests/functional/test_router.py b/shade/tests/functional/test_router.py index 49aad9a74..986650744 100644 --- a/shade/tests/functional/test_router.py +++ b/shade/tests/functional/test_router.py @@ -111,6 +111,35 @@ def test_create_router_basic(self): self.assertEqual(net1['id'], ext_gw_info['network_id']) self.assertTrue(ext_gw_info['enable_snat']) + def test_create_router_project(self): + project = self.operator_cloud.get_project('demo') + self.assertIsNotNone(project) + proj_id = project['id'] + net1_name = self.network_prefix + '_net1' + net1 = self.operator_cloud.create_network( + name=net1_name, external=True, project_id=proj_id) + + router_name = self.router_prefix + '_create_project' + router = self.operator_cloud.create_router( + name=router_name, + admin_state_up=True, + ext_gateway_net_id=net1['id'], + project_id=proj_id + ) + + for field in EXPECTED_TOPLEVEL_FIELDS: + self.assertIn(field, router) + + ext_gw_info = router['external_gateway_info'] + for field in EXPECTED_GW_INFO_FIELDS: + self.assertIn(field, ext_gw_info) + + self.assertEqual(router_name, router['name']) + self.assertEqual('ACTIVE', router['status']) + self.assertEqual(proj_id, router['tenant_id']) + self.assertEqual(net1['id'], ext_gw_info['network_id']) + self.assertTrue(ext_gw_info['enable_snat']) + def _create_and_verify_advanced_router(self, external_cidr, external_gateway_ip=None): diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 114219cb4..54bceac47 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -156,6 +156,19 @@ def test_create_router(self, mock_client): self.cloud.create_router(name='goofy', admin_state_up=True) self.assertTrue(mock_client.create_router.called) + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_router_specific_tenant(self, mock_client): + self.cloud.create_router("goofy", project_id="project_id_value") + mock_client.create_router.assert_called_once_with( + body=dict( + router=dict( + name='goofy', + admin_state_up=True, + tenant_id="project_id_value", + ) + ) + ) + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') def test_create_router_with_enable_snat_True(self, mock_client): """Do not send enable_snat when same as neutron default.""" From 92a3c9ea177a84a062873e31618ff6504f484430 Mon Sep 17 00:00:00 2001 From: Caleb Boylan Date: Thu, 26 May 2016 15:25:20 -0700 Subject: [PATCH 0939/3836] Add function to update object metadata Change-Id: Ibeff2aa7cafd62b932f81fa17449657d9d83b19e --- shade/_tasks.py | 2 +- shade/openstackcloud.py | 31 +++++++++++++++++++++++++++ shade/tests/functional/test_object.py | 6 ++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 6ddb8f57a..b5a767444 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -546,7 +546,7 @@ def main(self, client): class ObjectUpdate(task_manager.Task): def main(self, client): - client.swift_client.post_object(**self.args) + return client.swift_client.post_object(**self.args) class ObjectList(task_manager.Task): diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index cd95e562f..3213a246b 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4684,6 +4684,37 @@ def create_object( raise OpenStackCloudException( 'Failed at action ({action}) [{error}]:'.format(**r)) + def update_object(self, container, name, metadata=None, **headers): + """Update the metadtata of an object + + :param container: The name of the container the object is in + :param name: Name for the object within the container. + :param headers: These will be passed through to the object update + API as HTTP Headers. + :param metadata: This dict will get changed into headers that set + metadata of the object + + :raises: ``OpenStackCloudException`` on operation error. + """ + if not metadata: + metadata = {} + + metadata_headers = {} + + for (k, v) in metadata.items(): + metadata_headers['x-object-meta-' + k] = v + + headers = dict(headers, **metadata_headers) + + try: + return self.manager.submitTask( + _tasks.ObjectUpdate(container=container, obj=name, + headers=headers)) + except swift_exceptions.ClientException as e: + raise OpenStackCloudException( + "Object update failed: %s (%s/%s)" % ( + e.http_reason, e.http_host, e.http_path)) + def list_objects(self, container, full_listing=True): try: return self.manager.submitTask(_tasks.ObjectList( diff --git a/shade/tests/functional/test_object.py b/shade/tests/functional/test_object.py index 94d5c9932..202e7063c 100644 --- a/shade/tests/functional/test_object.py +++ b/shade/tests/functional/test_object.py @@ -66,6 +66,12 @@ def test_create_object(self): 'bar', self.demo_cloud.get_object_metadata( container_name, name)['x-object-meta-foo'] ) + self.demo_cloud.update_object(container=container_name, name=name, + metadata={'testk': 'testv'}) + self.assertEqual( + 'testv', self.demo_cloud.get_object_metadata( + container_name, name)['x-object-meta-testk'] + ) self.assertIsNotNone( self.demo_cloud.get_object(container_name, name)) self.assertEqual( From 41ac1562b5a10f7dcbdd4131b56784763f40eb69 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 30 May 2016 18:33:38 -0400 Subject: [PATCH 0940/3836] Add helper method for OpenStack SDK constructor openstacksdk already has a helper method for dealing with occ, but if a user is already using the occ helper methods, there is no reason we should not provide them an easy path to using the SDK. Change-Id: I1040efb94385fdac0aa02ac960ba95089b954377 --- README.rst | 37 +++++++++++++++++++ os_client_config/__init__.py | 13 +++++++ .../notes/sdk-helper-41f8d815cfbcfb00.yaml | 4 ++ 3 files changed, 54 insertions(+) create mode 100644 releasenotes/notes/sdk-helper-41f8d815cfbcfb00.yaml diff --git a/README.rst b/README.rst index ff95c07a8..223fcfa20 100644 --- a/README.rst +++ b/README.rst @@ -355,6 +355,43 @@ with - as well as a consumption argument. cloud = cloud_config.get_one_cloud(argparse=options) +Constructing OpenStack SDK object +--------------------------------- + +If what you want to do is get an OpenStack SDK Connection and you want it to +do all the normal things related to clouds.yaml, `OS_` environment variables, +a helper function is provided. The following will get you a fully configured +`openstacksdk` instance. + +.. code-block:: python + + import os_client_config + + sdk = os_client_config.make_sdk() + +If you want to do the same thing but on a named cloud. + +.. code-block:: python + + import os_client_config + + sdk = os_client_config.make_sdk(cloud='mtvexx') + +If you want to do the same thing but also support command line parsing. + +.. code-block:: python + + import argparse + + import os_client_config + + sdk = os_client_config.make_sdk(options=argparse.ArgumentParser()) + +It should be noted that OpenStack SDK has ways to construct itself that allow +for additional flexibility. If the helper function here does not meet your +needs, you should see the `from_config` method of +`openstack.connection.Connection `_ + Constructing Legacy Client objects ---------------------------------- diff --git a/os_client_config/__init__.py b/os_client_config/__init__.py index be232fb68..c88ccb2c6 100644 --- a/os_client_config/__init__.py +++ b/os_client_config/__init__.py @@ -67,3 +67,16 @@ def make_client(service_key, constructor=None, options=None, **kwargs): if not constructor: constructor = cloud_config._get_client(service_key) return cloud.get_legacy_client(service_key, constructor) + + +def make_sdk(options=None, **kwargs): + """Simple wrapper for getting an OpenStack SDK Connection. + + For completeness, provide a mechanism that matches make_client and + session_client. The heavy lifting here is done in openstacksdk. + + :rtype: :class:`~openstack.connection.Connection` + """ + from openstack import connection + cloud = get_config(options=options, **kwargs) + return connection.from_config(cloud_config=cloud, options=options) diff --git a/releasenotes/notes/sdk-helper-41f8d815cfbcfb00.yaml b/releasenotes/notes/sdk-helper-41f8d815cfbcfb00.yaml new file mode 100644 index 000000000..a18b57dc3 --- /dev/null +++ b/releasenotes/notes/sdk-helper-41f8d815cfbcfb00.yaml @@ -0,0 +1,4 @@ +--- +features: + - Added helper method for constructing OpenStack SDK + Connection objects. From cb2e59c06ace19c4a875cf4410972dfaf9f2c189 Mon Sep 17 00:00:00 2001 From: Yolanda Robla Date: Fri, 6 May 2016 16:42:11 +0200 Subject: [PATCH 0941/3836] Add magnum services call to shade Change-Id: I7a50be5464b4962ece8e2f13e3e93f8939e26655 --- ...num_services_support-3d95f9dcc60b5573.yaml | 3 ++ shade/_tasks.py | 5 +++ shade/openstackcloud.py | 10 +++++ .../tests/functional/test_magnum_services.py | 42 +++++++++++++++++ shade/tests/unit/test_magnum_services.py | 45 +++++++++++++++++++ 5 files changed, 105 insertions(+) create mode 100644 releasenotes/notes/add_magnum_services_support-3d95f9dcc60b5573.yaml create mode 100644 shade/tests/functional/test_magnum_services.py create mode 100644 shade/tests/unit/test_magnum_services.py diff --git a/releasenotes/notes/add_magnum_services_support-3d95f9dcc60b5573.yaml b/releasenotes/notes/add_magnum_services_support-3d95f9dcc60b5573.yaml new file mode 100644 index 000000000..3a32e3dde --- /dev/null +++ b/releasenotes/notes/add_magnum_services_support-3d95f9dcc60b5573.yaml @@ -0,0 +1,3 @@ +--- +features: + - Add support for listing Magnum services. diff --git a/shade/_tasks.py b/shade/_tasks.py index 4783c04c0..e7f0667dd 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -928,3 +928,8 @@ class BaymodelUpdate(task_manager.Task): def main(self, client): return client.magnum_client.baymodels.update( self.args['id'], self.args['patch']) + + +class MagnumServicesList(task_manager.Task): + def main(self, client): + return client.magnum_client.mservices.list(detail=False) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 23f32c08b..7cf764dc4 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5818,3 +5818,13 @@ def update_baymodel(self, name_or_id, operation, **kwargs): new_baymodel = self.get_baymodel(name_or_id) return new_baymodel + + def list_magnum_services(self): + """List all Magnum services. + :returns: a list of dicts containing the service details. + + :raises: OpenStackCloudException on operation error. + """ + with _utils.shade_exceptions("Error fetching Magnum services list"): + return self.manager.submitTask( + _tasks.MagnumServicesList()) diff --git a/shade/tests/functional/test_magnum_services.py b/shade/tests/functional/test_magnum_services.py new file mode 100644 index 000000000..17c622c95 --- /dev/null +++ b/shade/tests/functional/test_magnum_services.py @@ -0,0 +1,42 @@ +# -*- 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. + +""" +test_magnum_services +-------------------- + +Functional tests for `shade` services method. +""" + +from shade.tests.functional import base + + +class TestMagnumServices(base.BaseFunctionalTestCase): + + def setUp(self): + super(TestMagnumServices, self).setUp() + if not self.operator_cloud.has_service('container'): + self.skipTest('Container service not supported by cloud') + + def test_magnum_services(self): + '''Test magnum services functionality''' + + # Test that we can list services + services = self.operator_cloud.list_magnum_services() + + self.assertEqual(1, len(services)) + self.assertEqual(services[0]['id'], 1) + self.assertEqual('up', services[0]['state']) + self.assertEqual('magnum-conductor', services[0]['binary']) + self.assertGreater(services[0]['report_count'], 0) diff --git a/shade/tests/unit/test_magnum_services.py b/shade/tests/unit/test_magnum_services.py new file mode 100644 index 000000000..d63bb6694 --- /dev/null +++ b/shade/tests/unit/test_magnum_services.py @@ -0,0 +1,45 @@ +# -*- 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. + + +import mock +import munch + +import shade +from shade.tests.unit import base + + +magnum_service_obj = munch.Munch( + binary='fake-service', + state='up', + report_count=1, + human_id=None, + host='fake-host', + id=1, + disabled_reason=None +) + + +class TestMagnumServices(base.TestCase): + + def setUp(self): + super(TestMagnumServices, self).setUp() + self.cloud = shade.openstack_cloud(validate=False) + + @mock.patch.object(shade.OpenStackCloud, 'magnum_client') + def test_list_magnum_services(self, mock_magnum): + mock_magnum.mservices.list.return_value = [magnum_service_obj, ] + mservices_list = self.cloud.list_magnum_services() + mock_magnum.mservices.list.assert_called_with(detail=False) + self.assertEqual(mservices_list[0], magnum_service_obj) From 6a834063a25852f7f6cd45d6f7331aa0f77ff4c5 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 1 Jun 2016 10:28:51 +0300 Subject: [PATCH 0942/3836] Rename session_client to make_rest_client While writing some docs, it became clear that session_client was just a horrible horrible name and that I'm a bad person. Rename it so that we can make docs that make humans happy. Also, move the REST client section of the README up a bit. Change-Id: I1a27853e3031489da5916308a76f19bc72185d24 --- README.rst | 34 +++++++++---------- os_client_config/__init__.py | 8 +++-- .../make-rest-client-dd3d365632a26fa0.yaml | 4 +++ 3 files changed, 26 insertions(+), 20 deletions(-) create mode 100644 releasenotes/notes/make-rest-client-dd3d365632a26fa0.yaml diff --git a/README.rst b/README.rst index 223fcfa20..99ed287b9 100644 --- a/README.rst +++ b/README.rst @@ -392,6 +392,23 @@ for additional flexibility. If the helper function here does not meet your needs, you should see the `from_config` method of `openstack.connection.Connection `_ +Constructing REST API Clients +----------------------------- + +What if you want to make direct REST calls via a Session interface? You're +in luck. The same interface for `make_sdk` is supported for +`make_rest_client` and will return you a keystoneauth Session object that is +mounted on the endpoint for the service you're looking for. + +.. code-block:: python + + import os_client_config + + session = os_client_config.make_rest_client('compute', cloud='vexxhost') + + response = session.get('/servers') + server_list = response.json()['servers'] + Constructing Legacy Client objects ---------------------------------- @@ -428,23 +445,6 @@ If you want to do the same thing but also support command line parsing. If you want to get fancier than that in your python, then the rest of the API is available to you. But often times, you just want to do the one thing. -Constructing Mounted Session Objects ------------------------------------- - -What if you want to make direct REST calls via a Session interface? You're -in luck. The same interface for `make_client` is supported for `session_client` -and will return you a keystoneauth Session object that is mounted on the -endpoint for the service you're looking for. - -.. code-block:: python - - import os_client_config - - session = os_client_config.session_client('compute', cloud='vexxhost') - - response = session.get('/servers') - server_list = response.json()['servers'] - Source ------ diff --git a/os_client_config/__init__.py b/os_client_config/__init__.py index c88ccb2c6..6142853ef 100644 --- a/os_client_config/__init__.py +++ b/os_client_config/__init__.py @@ -34,7 +34,7 @@ def get_config(service_key=None, options=None, **kwargs): return config.get_one_cloud(options=parsed_options, **kwargs) -def session_client(service_key, options=None, **kwargs): +def make_rest_client(service_key, options=None, **kwargs): """Simple wrapper function. It has almost no features. This will get you a raw requests Session Adapter that is mounted @@ -50,7 +50,9 @@ def session_client(service_key, options=None, **kwargs): cloud = get_config(service_key=service_key, options=options, **kwargs) return cloud.get_session_client(service_key) # Backwards compat - simple_client was a terrible name -simple_client = session_client +simple_client = make_rest_client +# Backwards compat - session_client was a terrible name +session_client = make_rest_client def make_client(service_key, constructor=None, options=None, **kwargs): @@ -73,7 +75,7 @@ def make_sdk(options=None, **kwargs): """Simple wrapper for getting an OpenStack SDK Connection. For completeness, provide a mechanism that matches make_client and - session_client. The heavy lifting here is done in openstacksdk. + make_rest_client. The heavy lifting here is done in openstacksdk. :rtype: :class:`~openstack.connection.Connection` """ diff --git a/releasenotes/notes/make-rest-client-dd3d365632a26fa0.yaml b/releasenotes/notes/make-rest-client-dd3d365632a26fa0.yaml new file mode 100644 index 000000000..8e34e5198 --- /dev/null +++ b/releasenotes/notes/make-rest-client-dd3d365632a26fa0.yaml @@ -0,0 +1,4 @@ +--- +deprecations: + - Renamed session_client to make_rest_client. session_client + will continue to be supported for backwards compatability. From d0ce33abfa154cdf0a5f706d7c483cbd2e917a85 Mon Sep 17 00:00:00 2001 From: Caleb Boylan Date: Tue, 31 May 2016 12:51:05 -0700 Subject: [PATCH 0943/3836] Add reno note for create_object and update_object Change-Id: I4dfa51cad077e615cc24550b83a5d15862434c92 --- .../make_object_metadata_easier.yaml-e9751723e002e06f.yaml | 5 +++++ shade/openstackcloud.py | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/make_object_metadata_easier.yaml-e9751723e002e06f.yaml diff --git a/releasenotes/notes/make_object_metadata_easier.yaml-e9751723e002e06f.yaml b/releasenotes/notes/make_object_metadata_easier.yaml-e9751723e002e06f.yaml new file mode 100644 index 000000000..eaa718307 --- /dev/null +++ b/releasenotes/notes/make_object_metadata_easier.yaml-e9751723e002e06f.yaml @@ -0,0 +1,5 @@ +--- +features: + - create_object() now has a "metadata" parameter that can be used to create + an object with metadata of each key and value pair in that dictionary + - Add an update_object() function that updates the metadata of a swift object diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 2cbd86f45..a078d8424 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4689,14 +4689,14 @@ def create_object( 'Failed at action ({action}) [{error}]:'.format(**r)) def update_object(self, container, name, metadata=None, **headers): - """Update the metadtata of an object + """Update the metadata of an object :param container: The name of the container the object is in :param name: Name for the object within the container. - :param headers: These will be passed through to the object update - API as HTTP Headers. :param metadata: This dict will get changed into headers that set metadata of the object + :param headers: These will be passed through to the object update + API as HTTP Headers. :raises: ``OpenStackCloudException`` on operation error. """ From 7d63f12eddb250542587cab8e4a2ca9088ec0fbb Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 1 Jun 2016 10:37:58 +0300 Subject: [PATCH 0944/3836] Add shade constructor helper method We have helper factory methods for REST Client, legacy client and OpenStack SDK all with the same interface ... we might as well have one for shade too. It makes documenting and talking about the simple case of all of them easy. Change-Id: I046da85ae4a3e2a6333223921d5ae9ce3673121d --- README.rst | 33 +++++++++++++++++++ os_client_config/__init__.py | 12 +++++++ .../notes/shade-helper-568f8cb372eef6d9.yaml | 4 +++ 3 files changed, 49 insertions(+) create mode 100644 releasenotes/notes/shade-helper-568f8cb372eef6d9.yaml diff --git a/README.rst b/README.rst index 99ed287b9..3d97476b1 100644 --- a/README.rst +++ b/README.rst @@ -392,6 +392,39 @@ for additional flexibility. If the helper function here does not meet your needs, you should see the `from_config` method of `openstack.connection.Connection `_ +Constructing shade objects +-------------------------- + +If what you want to do is get a +`shade `_ OpenStackCloud object, a +helper function that honors clouds.yaml and `OS_` environment variables is +provided. The following will get you a fully configured `OpenStackCloud` +instance. + +.. code-block:: python + + import os_client_config + + cloud = os_client_config.make_shade() + +If you want to do the same thing but on a named cloud. + +.. code-block:: python + + import os_client_config + + cloud = os_client_config.make_shade(cloud='mtvexx') + +If you want to do the same thing but also support command line parsing. + +.. code-block:: python + + import argparse + + import os_client_config + + cloud = os_client_config.make_shade(options=argparse.ArgumentParser()) + Constructing REST API Clients ----------------------------- diff --git a/os_client_config/__init__.py b/os_client_config/__init__.py index 6142853ef..09d74423b 100644 --- a/os_client_config/__init__.py +++ b/os_client_config/__init__.py @@ -82,3 +82,15 @@ def make_sdk(options=None, **kwargs): from openstack import connection cloud = get_config(options=options, **kwargs) return connection.from_config(cloud_config=cloud, options=options) + + +def make_shade(options=None, **kwargs): + """Simple wrapper for getting a Shade OpenStackCloud object + + A mechanism that matches make_sdk, make_client and make_rest_client. + + :rtype: :class:`~shade.OpenStackCloud` + """ + import shade + cloud = get_config(options=options, **kwargs) + return shade.OpenStackCloud(cloud_config=cloud, **kwargs) diff --git a/releasenotes/notes/shade-helper-568f8cb372eef6d9.yaml b/releasenotes/notes/shade-helper-568f8cb372eef6d9.yaml new file mode 100644 index 000000000..70aab0a13 --- /dev/null +++ b/releasenotes/notes/shade-helper-568f8cb372eef6d9.yaml @@ -0,0 +1,4 @@ +--- +features: + - Added helper method for constructing shade + OpenStackCloud objects. From 4f36eca18f30953352e3fea42776e2e49d1db5fe Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 1 Jun 2016 10:51:44 +0300 Subject: [PATCH 0945/3836] Reword the entries in the README a bit Wanted to make each section a little better, but also to start to indicate that legacy clients should really not be your first choice. Change-Id: I26e08d037c7b28ced22a2a0126693d7e3e779f58 --- README.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 3d97476b1..a47e98bed 100644 --- a/README.rst +++ b/README.rst @@ -429,9 +429,10 @@ Constructing REST API Clients ----------------------------- What if you want to make direct REST calls via a Session interface? You're -in luck. The same interface for `make_sdk` is supported for -`make_rest_client` and will return you a keystoneauth Session object that is -mounted on the endpoint for the service you're looking for. +in luck. A similar interface is available as with `openstacksdk` and `shade`. +The main difference is that you need to specify which service you want to +talk to and `make_rest_client` will return you a keystoneauth Session object +that is mounted on the endpoint for the service you're looking for. .. code-block:: python @@ -445,9 +446,9 @@ mounted on the endpoint for the service you're looking for. Constructing Legacy Client objects ---------------------------------- -If all you want to do is get a Client object from a python-\*client library, +If you want get an old-style Client object from a python-\*client library, and you want it to do all the normal things related to clouds.yaml, `OS_` -environment variables, a helper function is provided. The following +environment variables, a helper function is also provided. The following will get you a fully configured `novaclient` instance. .. code-block:: python From 2d20407729454bcbfea1e34beb946bcc1a4ad348 Mon Sep 17 00:00:00 2001 From: Ghe Rivero Date: Fri, 20 May 2016 11:05:01 +0200 Subject: [PATCH 0946/3836] Add network quotas support Add the capability to get, update and reset to default quotas in network service. Change-Id: I3396e5ecac1379af927f0a99a3b0c4c995dcd283 --- .../network-quotas-b98cce9ffeffdbf4.yaml | 3 ++ shade/_tasks.py | 15 ++++++ shade/operatorcloud.py | 51 +++++++++++++++++++ shade/tests/functional/test_quotas.py | 22 ++++++++ shade/tests/unit/test_quotas.py | 30 +++++++++++ 5 files changed, 121 insertions(+) create mode 100644 releasenotes/notes/network-quotas-b98cce9ffeffdbf4.yaml diff --git a/releasenotes/notes/network-quotas-b98cce9ffeffdbf4.yaml b/releasenotes/notes/network-quotas-b98cce9ffeffdbf4.yaml new file mode 100644 index 000000000..a58cbeab4 --- /dev/null +++ b/releasenotes/notes/network-quotas-b98cce9ffeffdbf4.yaml @@ -0,0 +1,3 @@ +--- +features: + - Add new APIs, OperatorCloud.get_network_quotas(), OperatorCloud.set_network_quotas() and OperatorCloud.delete_network_quotas() to manage neutron quotas for projects and users \ No newline at end of file diff --git a/shade/_tasks.py b/shade/_tasks.py index 4783c04c0..632f55331 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -909,6 +909,21 @@ def main(self, client): return client.cinder_client.quotas.delete(**self.args) +class NeutronQuotasSet(task_manager.Task): + def main(self, client): + return client.neutron_client.update_quota(**self.args) + + +class NeutronQuotasGet(task_manager.Task): + def main(self, client): + return client.neutron_client.show_quota(**self.args)['quota'] + + +class NeutronQuotasDelete(task_manager.Task): + def main(self, client): + return client.neutron_client.delete_quota(**self.args) + + class BaymodelList(task_manager.Task): def main(self, client): return client.magnum_client.baymodels.list(**self.args) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index babadb98e..8607a4b23 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -2053,3 +2053,54 @@ def delete_volume_quotas(self, name_or_id): _tasks.CinderQuotasDelete(tenant_id=proj.id)) except cinder_exceptions.BadRequest: raise OpenStackCloudException("cinder client call failed") + + def set_network_quotas(self, name_or_id, **kwargs): + """ Set a network quota in a project + + :param name_or_id: project name or id + :param kwargs: key/value pairs of quota name and quota value + + :raises: OpenStackCloudException if the resource to set the + quota does not exist. + """ + + proj = self.get_project(name_or_id) + if not proj: + raise OpenStackCloudException("project does not exist") + + body = {'quota': kwargs} + with _utils.neutron_exceptions("network client call failed"): + self.manager.submitTask( + _tasks.NeutronQuotasSet(tenant_id=proj.id, + body=body)) + + def get_network_quotas(self, name_or_id): + """ Get network quotas for a project + + :param name_or_id: project name or id + :raises: OpenStackCloudException if it's not a valid project + + :returns: Munch object with the quotas + """ + proj = self.get_project(name_or_id) + if not proj: + raise OpenStackCloudException("project does not exist") + with _utils.neutron_exceptions("network client call failed"): + return self.manager.submitTask( + _tasks.NeutronQuotasGet(tenant_id=proj.id)) + + def delete_network_quotas(self, name_or_id): + """ Delete network quotas for a project + + :param name_or_id: project name or id + :raises: OpenStackCloudException if it's not a valid project or the + network client call failed + + :returns: dict with the quotas + """ + proj = self.get_project(name_or_id) + if not proj: + raise OpenStackCloudException("project does not exist") + with _utils.neutron_exceptions("network client call failed"): + return self.manager.submitTask( + _tasks.NeutronQuotasDelete(tenant_id=proj.id)) diff --git a/shade/tests/functional/test_quotas.py b/shade/tests/functional/test_quotas.py index aaf732dc1..b99e73ea2 100644 --- a/shade/tests/functional/test_quotas.py +++ b/shade/tests/functional/test_quotas.py @@ -60,3 +60,25 @@ def test_quotas(self): self.cloud.delete_volume_quotas('demo') self.assertEqual(volumes, self.cloud.get_volume_quotas('demo')['volumes']) + + +class TestNetworkQuotas(base.TestCase): + + def setUp(self): + super(TestNetworkQuotas, self).setUp() + self.cloud = operator_cloud(cloud='devstack-admin') + if not self.cloud.has_service('network'): + self.skipTest('network service not supported by cloud') + + def test_quotas(self): + '''Test quotas functionality''' + quotas = self.cloud.get_network_quotas('demo') + network = quotas['network'] + self.cloud.set_network_quotas('demo', network=network + 1) + self.assertEqual(network + 1, + self.cloud.get_network_quotas('demo')['network'] + ) + self.cloud.delete_network_quotas('demo') + self.assertEqual(network, + self.cloud.get_network_quotas('demo')['network'] + ) diff --git a/shade/tests/unit/test_quotas.py b/shade/tests/unit/test_quotas.py index b07a18d73..01160e37e 100644 --- a/shade/tests/unit/test_quotas.py +++ b/shade/tests/unit/test_quotas.py @@ -82,3 +82,33 @@ def test_cinder_delete_quotas(self, mock_keystone, mock_cinder): mock_cinder.quotas.delete.assert_called_once_with( tenant_id='project_a') + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_neutron_update_quotas(self, mock_keystone, mock_neutron): + project = fakes.FakeProject('project_a') + mock_keystone.tenants.list.return_value = [project] + self.cloud.set_network_quotas(project, network=1) + + mock_neutron.update_quota.assert_called_once_with( + body={'quota': {'network': 1}}, tenant_id='project_a') + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_neutron_get_quotas(self, mock_keystone, mock_neutron): + project = fakes.FakeProject('project_a') + mock_keystone.tenants.list.return_value = [project] + self.cloud.get_network_quotas(project) + + mock_neutron.show_quota.assert_called_once_with( + tenant_id='project_a') + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_neutron_delete_quotas(self, mock_keystone, mock_neutron): + project = fakes.FakeProject('project_a') + mock_keystone.tenants.list.return_value = [project] + self.cloud.delete_network_quotas(project) + + mock_neutron.delete_quota.assert_called_once_with( + tenant_id='project_a') From 5f5c925631d5f88d5f4a5d025e75c5d288850b4e Mon Sep 17 00:00:00 2001 From: Yolanda Robla Date: Tue, 29 Mar 2016 12:30:07 +0200 Subject: [PATCH 0947/3836] Use keystoneauth.betamax for shade mocks Instead of mocking the clients, use keystoneauth1.betamax fixture to intercept keystoneauth.construct_session, and provide our own recorded fixtures. Change-Id: I7b2973b0f89b66c19d6bf10571c3c93692107aa3 --- requirements.txt | 2 +- shade/tests/unit/base.py | 14 +++++++++++++- shade/tests/unit/test_flavors.py | 1 - test-requirements.txt | 1 + tox.ini | 14 ++++++++++++++ 5 files changed, 29 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 23ead37c4..c997766dc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ os-client-config>=1.17.0 requestsexceptions>=1.1.1 six -keystoneauth1>=1.0.0 +keystoneauth1>=2.8.0 netifaces>=0.10.4 python-novaclient>=2.21.0,!=2.27.0,!=2.32.0 python-keystoneclient>=0.11.0 diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 8768ebb7a..4d8661922 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -18,6 +18,7 @@ import time import fixtures +import os import os_client_config as occ import tempfile @@ -56,10 +57,21 @@ def _nosleep(seconds): vendor.write(b'{}') vendor.close() + # set record mode depending on environment + record_mode = os.environ.get('BETAMAX_RECORD_FIXTURES', False) + if record_mode: + self.record_fixtures = 'new_episodes' + else: + self.record_fixtures = None + + test_cloud = os.environ.get('SHADE_OS_CLOUD', '_test_cloud_') self.config = occ.OpenStackConfig( config_files=[config.name], vendor_files=[vendor.name]) - self.cloud_config = self.config.get_one_cloud(cloud='_test_cloud_') + self.cloud_config = self.config.get_one_cloud(cloud=test_cloud) self.cloud = shade.OpenStackCloud( cloud_config=self.cloud_config, log_inner_exceptions=True) + self.op_cloud = shade.OperatorCloud( + cloud_config=self.cloud_config, + log_inner_exceptions=True) diff --git a/shade/tests/unit/test_flavors.py b/shade/tests/unit/test_flavors.py index f16e0e1c3..99f6ebdb8 100644 --- a/shade/tests/unit/test_flavors.py +++ b/shade/tests/unit/test_flavors.py @@ -24,7 +24,6 @@ class TestFlavors(base.TestCase): def setUp(self): super(TestFlavors, self).setUp() - self.op_cloud = shade.operator_cloud(validate=False) @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_create_flavor(self, mock_nova): diff --git a/test-requirements.txt b/test-requirements.txt index ee73a8c15..58bfbe812 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,5 +1,6 @@ hacking>=0.10.0,<0.11 +betamax-serializers>=0.1.1 coverage>=3.6 discover fixtures>=0.3.14 diff --git a/tox.ini b/tox.ini index c1da17bc8..5caa2b097 100644 --- a/tox.ini +++ b/tox.ini @@ -15,6 +15,20 @@ deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = python setup.py testr --slowest --testr-args='{posargs}' +[testenv:record] +usedevelop = True +install_command = pip install -U {opts} {packages} +setenv = + VIRTUAL_ENV={envdir} + LANG=en_US.UTF-8 + LANGUAGE=en_US:en + LC_ALL=C + BETAMAX_RECORD_FIXTURES=1 +passenv = SHADE_OS_CLOUD +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt +commands = python setup.py testr --slowest --testr-args='--concurrency=1 {posargs}' + [testenv:functional] setenv = OS_TEST_PATH = ./shade/tests/functional From 891fa1c60fc948aac158ea939c5a10bd1cab04d9 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Sat, 18 Jun 2016 08:17:03 -0500 Subject: [PATCH 0948/3836] Refactor fix magic in get_one_cloud() Extract the magic argument fixes from get_ne_cloud() so they can be extended or modified. Needed by OSC in order to maintain compatibility due to the much earlier loading of auth plugins than before. Have I mentioned that really fouled things up for OSC? Change-Id: I22cd890f9cbd605dcd615f82b3e65c58f52ff114 --- os_client_config/config.py | 71 ++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index f1df7977b..dc9b73172 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -475,14 +475,6 @@ def _expand_vendor_profile(self, name, cloud, our_cloud): " the cloud '{1}'".format(profile_name, name)) - def _fix_backwards_madness(self, cloud): - cloud = self._fix_backwards_auth_plugin(cloud) - cloud = self._fix_backwards_project(cloud) - cloud = self._fix_backwards_interface(cloud) - cloud = self._fix_backwards_networks(cloud) - cloud = self._handle_domain_id(cloud) - return cloud - def _project_scoped(self, cloud): return ('project_id' in cloud or 'project_name' in cloud or 'project_id' in cloud['auth'] @@ -812,7 +804,7 @@ def _find_winning_auth_value(self, opt, config): def auth_config_hook(self, config): """Allow examination of config values before loading auth plugin - OpenStackClient will override this to perform additional chacks + OpenStackClient will override this to perform additional checks on auth_type. """ return config @@ -911,6 +903,41 @@ def _validate_auth(self, config, loader): return config + def magic_fixes(self, config): + """Perform the set of magic argument fixups""" + + # Infer token plugin if a token was given + if (('auth' in config and 'token' in config['auth']) or + ('auth_token' in config and config['auth_token']) or + ('token' in config and config['token'])): + config.setdefault('token', config.pop('auth_token', None)) + + # These backwards compat values are only set via argparse. If it's + # there, it's because it was passed in explicitly, and should win + config = self._fix_backwards_api_timeout(config) + if 'endpoint_type' in config: + config['interface'] = config.pop('endpoint_type') + + config = self._fix_backwards_auth_plugin(config) + config = self._fix_backwards_project(config) + config = self._fix_backwards_interface(config) + config = self._fix_backwards_networks(config) + config = self._handle_domain_id(config) + + for key in BOOL_KEYS: + if key in config: + if type(config[key]) is not bool: + config[key] = get_boolean(config[key]) + + # TODO(mordred): Special casing auth_url here. We should + # come back to this betterer later so that it's + # more generalized + if 'auth' in config and 'auth_url' in config['auth']: + config['auth']['auth_url'] = config['auth']['auth_url'].format( + **config) + + return config + def get_one_cloud(self, cloud=None, validate=True, argparse=None, **kwargs): """Retrieve a single cloud configuration and merge additional options @@ -961,31 +988,7 @@ def get_one_cloud(self, cloud=None, validate=True, else: config[key] = val - # Infer token plugin if a token was given - if (('auth' in config and 'token' in config['auth']) or - ('auth_token' in config and config['auth_token']) or - ('token' in config and config['token'])): - config.setdefault('token', config.pop('auth_token', None)) - - # These backwards compat values are only set via argparse. If it's - # there, it's because it was passed in explicitly, and should win - config = self._fix_backwards_api_timeout(config) - if 'endpoint_type' in config: - config['interface'] = config.pop('endpoint_type') - - config = self._fix_backwards_madness(config) - - for key in BOOL_KEYS: - if key in config: - if type(config[key]) is not bool: - config[key] = get_boolean(config[key]) - - # TODO(mordred): Special casing auth_url here. We should - # come back to this betterer later so that it's - # more generalized - if 'auth' in config and 'auth_url' in config['auth']: - config['auth']['auth_url'] = config['auth']['auth_url'].format( - **config) + config = self.magic_fixes(config) # NOTE(dtroyer): OSC needs a hook into the auth args before the # plugin is loaded in order to maintain backward- From e67b306c10be6358399d0e8a71350422394477e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rio=20Santos?= Date: Fri, 20 May 2016 16:56:20 +0100 Subject: [PATCH 0949/3836] Add support for changing metadata of compute instances Change-Id: I2301e0ab2024c51e7482ce0f05b706e8a395d676 --- ...ture-server-metadata-50caf18cec532160.yaml | 3 + shade/_tasks.py | 10 +++ shade/openstackcloud.py | 41 ++++++++++++ shade/tests/functional/test_compute.py | 35 ++++++++++ .../tests/unit/test_server_delete_metadata.py | 66 ++++++++++++++++++ shade/tests/unit/test_server_set_metadata.py | 67 +++++++++++++++++++ 6 files changed, 222 insertions(+) create mode 100644 releasenotes/notes/feature-server-metadata-50caf18cec532160.yaml create mode 100644 shade/tests/unit/test_server_delete_metadata.py create mode 100644 shade/tests/unit/test_server_set_metadata.py diff --git a/releasenotes/notes/feature-server-metadata-50caf18cec532160.yaml b/releasenotes/notes/feature-server-metadata-50caf18cec532160.yaml new file mode 100644 index 000000000..e0a3f6c83 --- /dev/null +++ b/releasenotes/notes/feature-server-metadata-50caf18cec532160.yaml @@ -0,0 +1,3 @@ +--- +features: + - Add new APIs, OpenStackCloud.set_server_metadata() and OpenStackCloud.delete_server_metadata() to manage metadata of existing nova compute instances diff --git a/shade/_tasks.py b/shade/_tasks.py index 0f4502969..4cc07e203 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -172,6 +172,16 @@ def main(self, client): return client.nova_client.servers.rebuild(**self.args) +class ServerSetMetadata(task_manager.Task): + def main(self, client): + return client.nova_client.servers.set_meta(**self.args) + + +class ServerDeleteMetadata(task_manager.Task): + def main(self, client): + return client.nova_client.servers.delete_meta(**self.args) + + class ServerGroupList(task_manager.Task): def main(self, client): return client.nova_client.server_groups.list(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 24fc10653..e546533c6 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4302,6 +4302,47 @@ def rebuild_server(self, server_id, image_id, admin_pass=None, extra_data=dict(server=server)) return server + def set_server_metadata(self, name_or_id, metadata): + """Set metadata in a server instance. + + :param str name_or_id: The name or id of the server instance + to update. + :param dict metadata: A dictionary with the key=value pairs + to set in the server instance. It only updates the key=value + pairs provided. Existing ones will remain untouched. + + :raises: OpenStackCloudException on operation error. + """ + try: + self.manager.submitTask( + _tasks.ServerSetMetadata(server=self.get_server(name_or_id), + metadata=metadata)) + except OpenStackCloudException: + raise + except Exception as e: + raise OpenStackCloudException( + "Error updating metadata: {0}".format(e)) + + def delete_server_metadata(self, name_or_id, metadata_keys): + """Delete metadata from a server instance. + + :param str name_or_id: The name or id of the server instance + to update. + :param list metadata_keys: A list with the keys to be deleted + from the server instance. + + :raises: OpenStackCloudException on operation error. + """ + try: + self.manager.submitTask( + _tasks.ServerDeleteMetadata(server=self.get_server(name_or_id), + keys=metadata_keys)) + except OpenStackCloudException: + raise + except Exception as e: + raise OpenStackCloudException( + "Error deleting metadata: {0}".format(e)) + def delete_server( self, name_or_id, wait=False, timeout=180, delete_ips=False, delete_ip_retry=1): diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index 78ed58c91..287806685 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -19,6 +19,7 @@ Functional tests for `shade` compute methods. """ +from shade import exc from shade.tests.functional import base from shade.tests.functional.util import pick_flavor, pick_image @@ -215,3 +216,37 @@ def test_create_image_snapshot_wait_active(self): wait=True) self.addCleanup(self.demo_cloud.delete_image, image['id']) self.assertEqual('active', image['status']) + + def test_set_and_delete_metadata(self): + self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) + self.demo_cloud.create_server( + name=self.server_name, + image=self.image, + flavor=self.flavor, + wait=True) + self.demo_cloud.set_server_metadata(self.server_name, + {'key1': 'value1', + 'key2': 'value2'}) + updated_server = self.demo_cloud.get_server(self.server_name) + self.assertEqual(set(updated_server.metadata.items()), + set({'key1': 'value1', 'key2': 'value2'}.items())) + + self.demo_cloud.set_server_metadata(self.server_name, + {'key2': 'value3'}) + updated_server = self.demo_cloud.get_server(self.server_name) + self.assertEqual(set(updated_server.metadata.items()), + set({'key1': 'value1', 'key2': 'value3'}.items())) + + self.demo_cloud.delete_server_metadata(self.server_name, ['key2']) + updated_server = self.demo_cloud.get_server(self.server_name) + self.assertEqual(set(updated_server.metadata.items()), + set({'key1': 'value1'}.items())) + + self.demo_cloud.delete_server_metadata(self.server_name, ['key1']) + updated_server = self.demo_cloud.get_server(self.server_name) + self.assertEqual(set(updated_server.metadata.items()), set([])) + + self.assertRaises( + exc.OpenStackCloudException, + self.demo_cloud.delete_server_metadata, + self.server_name, ['key1']) diff --git a/shade/tests/unit/test_server_delete_metadata.py b/shade/tests/unit/test_server_delete_metadata.py new file mode 100644 index 000000000..d734d7114 --- /dev/null +++ b/shade/tests/unit/test_server_delete_metadata.py @@ -0,0 +1,66 @@ +# -*- 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. + +""" +test_server_delete_metadata +---------------------------------- + +Tests for the `delete_server_metadata` command. +""" + +from mock import patch, Mock +import os_client_config +from shade import OpenStackCloud +from shade.exc import OpenStackCloudException +from shade.tests import base + + +class TestServerDeleteMetadata(base.TestCase): + def setUp(self): + super(TestServerDeleteMetadata, self).setUp() + config = os_client_config.OpenStackConfig() + self.client = OpenStackCloud( + cloud_config=config.get_one_cloud(validate=False)) + self.client._SERVER_AGE = 0 + + def test_server_delete_metadata_with_delete_meta_exception(self): + """ + Test that a generic exception in the novaclient delete_meta raises + an exception in delete_server_metadata. + """ + with patch("shade.OpenStackCloud"): + config = { + "servers.delete_meta.side_effect": Exception("exception"), + } + OpenStackCloud.nova_client = Mock(**config) + + self.assertRaises( + OpenStackCloudException, self.client.delete_server_metadata, + {'id': 'server-id'}, ['key']) + + def test_server_delete_metadata_with_exception_reraise(self): + """ + Test that an OpenStackCloudException exception gets re-raised + in delete_server_metadata. + """ + with patch("shade.OpenStackCloud"): + config = { + "servers.delete_meta.side_effect": + OpenStackCloudException("exception"), + } + OpenStackCloud.nova_client = Mock(**config) + + self.assertRaises( + OpenStackCloudException, self.client.delete_server_metadata, + 'server-id', ['key']) diff --git a/shade/tests/unit/test_server_set_metadata.py b/shade/tests/unit/test_server_set_metadata.py new file mode 100644 index 000000000..b0c0a2d88 --- /dev/null +++ b/shade/tests/unit/test_server_set_metadata.py @@ -0,0 +1,67 @@ +# -*- 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. + +""" +test_server_set_metadata +---------------------------------- + +Tests for the `set_server_metadata` command. +""" + +from mock import patch, Mock +import os_client_config +from shade import OpenStackCloud +from shade.exc import OpenStackCloudException +from shade.tests import base + + +class TestServerSetMetadata(base.TestCase): + + def setUp(self): + super(TestServerSetMetadata, self).setUp() + config = os_client_config.OpenStackConfig() + self.client = OpenStackCloud( + cloud_config=config.get_one_cloud(validate=False)) + self.client._SERVER_AGE = 0 + + def test_server_set_metadata_with_set_meta_exception(self): + """ + Test that a generic exception in the novaclient set_meta raises + an exception in set_server_metadata. + """ + with patch("shade.OpenStackCloud"): + config = { + "servers.set_meta.side_effect": Exception("exception"), + } + OpenStackCloud.nova_client = Mock(**config) + + self.assertRaises( + OpenStackCloudException, self.client.set_server_metadata, + {'id': 'server-id'}, {'meta': 'data'}) + + def test_server_set_metadata_with_exception_reraise(self): + """ + Test that an OpenStackCloudException exception gets re-raised + in set_server_metadata. + """ + with patch("shade.OpenStackCloud"): + config = { + "servers.set_meta.side_effect": + OpenStackCloudException("exception"), + } + OpenStackCloud.nova_client = Mock(**config) + + self.assertRaises( + OpenStackCloudException, self.client.set_server_metadata, + 'server-id', {'meta': 'data'}) From 8481c6baa2becaa709e2d11b1c739cd9792851df Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Thu, 23 Jun 2016 09:03:31 +1200 Subject: [PATCH 0950/3836] Treat DELETE_COMPLETE stacks as NotFound When a stack has just been deleted, fetching it may return a DELETE_COMPLETE stack or raise a NotFound error. Recent changes have resulted in the former happening more frequently causing shade gate failures [1]. This commit changes get_stack to treat a DELETE_COMPLETE stack the same as a NotFound stack. [1] http://paste.openstack.org/show/521286/ Change-Id: I3ca0b55c1c10cf229987551c09fcce853faa3584 --- shade/openstackcloud.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 7d329683f..b289fe796 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2090,8 +2090,12 @@ def search_one_stack(name_or_id=None, filters=None): # so a StackGet can always be used for name or ID. with _utils.shade_exceptions("Error fetching stack"): try: - stacks = [self.manager.submitTask( - _tasks.StackGet(stack_id=name_or_id))] + stack = self.manager.submitTask( + _tasks.StackGet(stack_id=name_or_id)) + # Treat DELETE_COMPLETE stacks as a NotFound + if stack['stack_status'] == 'DELETE_COMPLETE': + return [] + stacks = [stack] except heat_exceptions.NotFound: return [] nstacks = _utils.normalize_stacks(stacks) From 481be16b8b385e1fcccd34607da8a8a3f5bde69f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 9 Jul 2016 17:34:11 +0900 Subject: [PATCH 0951/3836] Add support for deprecating cloud profiles We've had HP shut down their public cloud, and DreamHost has recent spun up a new cloud with a different operational profile and auth_url that is essentially a replacement cloud. That means people using the old information need to do things, so we need to be able to communicate that to them. Add support for adding a deprecated status to a vendor profile, as well as a verbose message explaining what the user may do to remediate. Change-Id: I19b67d7cd71fba2d9da0e3a6adb2d229ead65396 --- os_client_config/config.py | 6 ++++++ os_client_config/defaults.json | 2 ++ os_client_config/vendor-schema.json | 11 +++++++++++ 3 files changed, 19 insertions(+) diff --git a/os_client_config/config.py b/os_client_config/config.py index f1df7977b..84476b2ba 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -468,6 +468,12 @@ def _expand_vendor_profile(self, name, cloud, our_cloud): else: profile_data = vendors.get_profile(profile_name) if profile_data: + status = profile_data.pop('status', 'active') + message = profile_data.pop('message', '') + if status == 'deprecated': + warnings.warn( + "{profile_name} is deprecated: {message}".format( + profile_name=profile_name, message=message)) _auth_update(cloud, profile_data) else: # Can't find the requested vendor config, go about business diff --git a/os_client_config/defaults.json b/os_client_config/defaults.json index f501862d5..ba8bf3923 100644 --- a/os_client_config/defaults.json +++ b/os_client_config/defaults.json @@ -13,10 +13,12 @@ "image_api_version": "2", "image_format": "qcow2", "key_manager_api_version": "v1", + "message": "", "metering_api_version": "2", "network_api_version": "2", "object_store_api_version": "1", "orchestration_api_version": "1", "secgroup_source": "neutron", + "status": "active", "volume_api_version": "2" } diff --git a/os_client_config/vendor-schema.json b/os_client_config/vendor-schema.json index 6c57ba4bc..a5bee27f6 100644 --- a/os_client_config/vendor-schema.json +++ b/os_client_config/vendor-schema.json @@ -55,12 +55,23 @@ "default": "public", "enum": [ "public", "internal", "admin" ] }, + "message": { + "name": "Status message", + "description": "Optional message with information related to status", + "type": "string" + }, "secgroup_source": { "name": "Security Group Source", "description": "Which service provides security groups", "enum": [ "neutron", "nova", "None" ], "default": "neutron" }, + "status": { + "name": "Vendor status", + "description": "Status of the vendor's cloud", + "enum": [ "active", "deprecated"], + "default": "active" + }, "compute_service_name": { "name": "Compute API Service Name", "description": "Compute API Service Name", From 7dd138c8f4c8b8761b0b50d3ce9c7c4f770f8c61 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 12 May 2016 13:28:25 -0500 Subject: [PATCH 0952/3836] Add floating IPs to server dict ourselves The nova/neutron interaction as it relates to updating the network info cache in nova apparently does not believe in an object reality and instead prefers to think of things as unrealized quantum superpositions. While those are great for particle physics, they make getting the address of the server hard. It therefore follows that we should undertake the burden of providing consistent and objective information about the floating ip of the server ourselves, regardless of the sadness this adds to the world. Change-Id: I75ba0318c9a5a011b5a530d86ace275f19511f1f --- shade/meta.py | 27 +++++++++++++++++++++++++ shade/tests/unit/test_create_server.py | 22 +++++++++++++++++--- shade/tests/unit/test_meta.py | 6 +++++- shade/tests/unit/test_rebuild_server.py | 14 ++++++++++++- 4 files changed, 64 insertions(+), 5 deletions(-) diff --git a/shade/meta.py b/shade/meta.py index cf978ccf1..158750bb2 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -277,6 +277,32 @@ def expand_server_vars(cloud, server): return add_server_interfaces(cloud, server) +def _make_address_dict(fip): + address = dict(version=4, addr=fip['floating_ip_address']) + address['OS-EXT-IPS:type'] = 'floating' + # MAC address comes from the port, not the FIP. It also doesn't matter + # to anyone at the moment, so just make a fake one + address['OS-EXT-IPS-MAC:mac_addr'] = 'de:ad:be:ef:be:ef' + return address + + +def _get_suplemental_addresses(cloud, server): + fixed_ip_mapping = {} + for name, network in server['addresses'].items(): + for address in network: + if address['version'] == 6: + continue + if address['OS-EXT-IPS:type'] == 'floating': + # We have a floating IP that nova knows about, do nothing + return server['addresses'] + fixed_ip_mapping[address['addr']] = name + for fip in cloud.list_floating_ips(): + if fip['fixed_ip_address'] in fixed_ip_mapping: + fixed_net = fixed_ip_mapping[fip['fixed_ip_address']] + server['addresses'][fixed_net].append(_make_address_dict(fip)) + return server['addresses'] + + def add_server_interfaces(cloud, server): """Add network interface information to server. @@ -288,6 +314,7 @@ def add_server_interfaces(cloud, server): """ # First, add an IP address. Set it to '' rather than None if it does # not exist to remain consistent with the pre-existing missing values + server['addresses'] = _get_suplemental_addresses(cloud, server) server['public_v4'] = get_server_external_ipv4(cloud, server) or '' server['public_v6'] = get_server_external_ipv6(server) or '' server['private_v4'] = get_server_private_ip(server, cloud) or '' diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index c03382f91..5c8a79bd2 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -92,11 +92,15 @@ def test_create_server_wait_server_error(self): with patch("shade.OpenStackCloud"): build_server = fakes.FakeServer('1234', '', 'BUILD') error_server = fakes.FakeServer('1234', '', 'ERROR') + fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', + '1.1.1.1', '2.2.2.2', + '5678') config = { "servers.create.return_value": build_server, "servers.get.return_value": build_server, "servers.list.side_effect": [ - [build_server], [error_server]] + [build_server], [error_server]], + "floating_ips.list.return_value": [fake_floating_ip] } OpenStackCloud.nova_client = Mock(**config) self.assertRaises( @@ -129,9 +133,13 @@ def test_create_server_no_wait(self): """ with patch("shade.OpenStackCloud"): fake_server = fakes.FakeServer('1234', '', 'BUILD') + fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', + '1.1.1.1', '2.2.2.2', + '5678') config = { "servers.create.return_value": fake_server, - "servers.get.return_value": fake_server + "servers.get.return_value": fake_server, + "floating_ips.list.return_value": [fake_floating_ip] } OpenStackCloud.nova_client = Mock(**config) self.assertEqual( @@ -151,9 +159,13 @@ def test_create_server_with_admin_pass_no_wait(self): fake_server = fakes.FakeServer('1234', '', 'BUILD') fake_create_server = fakes.FakeServer('1234', '', 'BUILD', adminPass='ooBootheiX0edoh') + fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', + '1.1.1.1', '2.2.2.2', + '5678') config = { "servers.create.return_value": fake_create_server, - "servers.get.return_value": fake_server + "servers.get.return_value": fake_server, + "floating_ips.list.return_value": [fake_floating_ip] } OpenStackCloud.nova_client = Mock(**config) self.assertEqual( @@ -256,12 +268,16 @@ def test_create_server_no_addresses(self, mock_sleep): with patch("shade.OpenStackCloud"): build_server = fakes.FakeServer('1234', '', 'BUILD') fake_server = fakes.FakeServer('1234', '', 'ACTIVE') + fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', + '1.1.1.1', '2.2.2.2', + '5678') config = { "servers.create.return_value": build_server, "servers.get.return_value": [build_server, None], "servers.list.side_effect": [ [build_server], [fake_server]], "servers.delete.return_value": None, + "floating_ips.list.return_value": [fake_floating_ip] } OpenStackCloud.nova_client = Mock(**config) self.client._SERVER_AGE = 0 diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 66f64f85a..406903bf5 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -182,6 +182,7 @@ def test_get_server_private_ip( mock_has_service.assert_called_with('network') mock_list_networks.assert_called_once_with() + @mock.patch.object(shade.OpenStackCloud, 'list_floating_ips') @mock.patch.object(shade.OpenStackCloud, 'list_subnets') @mock.patch.object(shade.OpenStackCloud, 'list_server_security_groups') @mock.patch.object(shade.OpenStackCloud, 'get_volumes') @@ -194,7 +195,8 @@ def test_get_server_private_ip_devstack( mock_get_flavor_name, mock_get_image_name, mock_get_volumes, mock_list_server_security_groups, - mock_list_subnets): + mock_list_subnets, + mock_list_floating_ips): mock_get_image_name.return_value = 'cirros-0.3.4-x86_64-uec' mock_get_flavor_name.return_value = 'm1.tiny' mock_has_service.return_value = True @@ -211,6 +213,7 @@ def test_get_server_private_ip_devstack( 'name': 'private', }, ] + mock_list_floating_ips.return_value = [] srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', @@ -230,6 +233,7 @@ def test_get_server_private_ip_devstack( self.assertEqual(PRIVATE_V4, srv['private_v4']) mock_has_service.assert_called_with('volume') mock_list_networks.assert_called_once_with() + mock_list_floating_ips.assert_called_once_with() @mock.patch.object(shade.OpenStackCloud, 'has_service') @mock.patch.object(shade.OpenStackCloud, 'list_subnets') diff --git a/shade/tests/unit/test_rebuild_server.py b/shade/tests/unit/test_rebuild_server.py index b87a4da75..8156e57e4 100644 --- a/shade/tests/unit/test_rebuild_server.py +++ b/shade/tests/unit/test_rebuild_server.py @@ -56,10 +56,14 @@ def test_rebuild_server_server_error(self): """ rebuild_server = fakes.FakeServer('1234', '', 'REBUILD') error_server = fakes.FakeServer('1234', '', 'ERROR') + fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', + '1.1.1.1', '2.2.2.2', + '5678') with patch("shade.OpenStackCloud"): config = { "servers.rebuild.return_value": rebuild_server, "servers.get.return_value": error_server, + "floating_ips.list.return_value": [fake_floating_ip] } OpenStackCloud.nova_client = Mock(**config) self.assertRaises( @@ -122,9 +126,13 @@ def test_rebuild_server_with_admin_pass_wait(self): active_server = fakes.FakeServer('1234', '', 'ACTIVE') ret_active_server = fakes.FakeServer('1234', '', 'ACTIVE', adminPass='ooBootheiX0edoh') + fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', + '1.1.1.1', '2.2.2.2', + '5678') config = { "servers.rebuild.return_value": rebuild_server, "servers.get.return_value": active_server, + "floating_ips.list.return_value": [fake_floating_ip] } OpenStackCloud.nova_client = Mock(**config) self.client.name = 'cloud-name' @@ -143,9 +151,13 @@ def test_rebuild_server_wait(self): with patch("shade.OpenStackCloud"): rebuild_server = fakes.FakeServer('1234', '', 'REBUILD') active_server = fakes.FakeServer('1234', '', 'ACTIVE') + fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', + '1.1.1.1', '2.2.2.2', + '5678') config = { "servers.rebuild.return_value": rebuild_server, - "servers.get.return_value": active_server + "servers.get.return_value": active_server, + "floating_ips.list.return_value": [fake_floating_ip] } OpenStackCloud.nova_client = Mock(**config) self.client.name = 'cloud-name' From 9f4805bedbcd6bf0df4b30a9c496bb2d54a93513 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 14 Jul 2016 11:05:03 +0800 Subject: [PATCH 0953/3836] Change operating to interacting with in README Just noticed that the README says "for operating OpenStack". The word operating has very specific connotations in OpenStack world, and that is decidedly _not_ the primary use case of shade. Change-Id: Ic0205c82abc3b5bfd6223ea7a6ee1bd6bfb365dd --- README.rst | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 2b46ebdc4..f375f8670 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ Introduction ============ -shade is a simple client library for operating OpenStack clouds. The +shade is a simple client library for interacting with OpenStack clouds. The key word here is *simple*. Clouds can do many many many things - but there are probably only about 10 of them that most people care about with any regularity. If you want to do complicated things, you should probably use diff --git a/setup.cfg b/setup.cfg index b726f8d52..7e7b2c9a5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = shade -summary = Client library for operating OpenStack clouds +summary = Simple client library for interacting with OpenStack clouds description-file = README.rst author = OpenStack Infrastructure Team From d9e9bb791ba169e828d6068534ee2f430f4668eb Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 14 Jul 2016 10:30:49 +0800 Subject: [PATCH 0954/3836] Add support for listing a cloud as shut down We've had one vendor cloud go away in the past, and there is one existing deprecated cloud currently. Add support for providing the user with an informative error message in the case where they attempt to use such a cloud. Change-Id: I894e0c0a4786e60fce1238bb2883828e89d44b01 --- os_client_config/config.py | 5 +++++ os_client_config/vendor-schema.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 84476b2ba..f9b2de560 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -474,6 +474,11 @@ def _expand_vendor_profile(self, name, cloud, our_cloud): warnings.warn( "{profile_name} is deprecated: {message}".format( profile_name=profile_name, message=message)) + elif status == 'shutdown': + raise exceptions.OpenStackConfigException( + "{profile_name} references a cloud that no longer" + " exists: {message}".format( + profile_name=profile_name, message=message)) _auth_update(cloud, profile_data) else: # Can't find the requested vendor config, go about business diff --git a/os_client_config/vendor-schema.json b/os_client_config/vendor-schema.json index a5bee27f6..6a6f4561e 100644 --- a/os_client_config/vendor-schema.json +++ b/os_client_config/vendor-schema.json @@ -69,7 +69,7 @@ "status": { "name": "Vendor status", "description": "Status of the vendor's cloud", - "enum": [ "active", "deprecated"], + "enum": [ "active", "deprecated", "shutdown"], "default": "active" }, "compute_service_name": { From 51a5eb575ecbbd8cd51d1953118d9f91f73f43da Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 18 Jul 2016 14:23:41 -0500 Subject: [PATCH 0955/3836] Update hacking version There is an issue with old hacking which is causing it to fail open. Change-Id: I744fc414bffe984a63374d6431d77e6be5a28f40 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 58bfbe812..c0bd3406d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,4 +1,4 @@ -hacking>=0.10.0,<0.11 +hacking>=0.11.0,<0.12 # Apache-2.0 betamax-serializers>=0.1.1 coverage>=3.6 From 354f38ab2c42c05fc43704ddb6eb9f2ad03f8c0e Mon Sep 17 00:00:00 2001 From: "Swapnil Kulkarni (coolsvap)" Date: Fri, 22 Jul 2016 05:02:03 +0000 Subject: [PATCH 0956/3836] Remove discover from test-requirements It's only needed for python < 2.7 which is not supported Change-Id: I9511e1e08af8c0a5740e744252103a94e80eab43 --- test-requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index c0bd3406d..6c5b8c5ff 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,7 +2,6 @@ hacking>=0.11.0,<0.12 # Apache-2.0 betamax-serializers>=0.1.1 coverage>=3.6 -discover fixtures>=0.3.14 mock>=1.0 python-openstackclient>=2.1.0 From 1f7ecbc3ff482de3ae8323fa5a62a359510e13d9 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 9 Jul 2016 18:23:02 +0900 Subject: [PATCH 0957/3836] Update citycloud to list new regions Frankfurt, Buffalo and Los Angeles - oh my! Change-Id: I17d6f46de2a9af82f221b971a359d53eb471f8fa --- doc/source/vendor-support.rst | 5 ++++- os_client_config/vendors/citycloud.json | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index f97c9f6ff..1053b3d4b 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -62,9 +62,12 @@ https://identity1.citycloud.com:5000/v3/ ============== ================ Region Name Human Name ============== ================ +Buf1 Buffalo, NY +Fra1 Frankfurt, DE +Kna1 Karlskrona, SE +La1 Los Angeles, CA Lon1 London, UK Sto2 Stockholm, SE -Kna1 Karlskrona, SE ============== ================ * Identity API Version is 3 diff --git a/os_client_config/vendors/citycloud.json b/os_client_config/vendors/citycloud.json index 64cadce9a..097ddfdb3 100644 --- a/os_client_config/vendors/citycloud.json +++ b/os_client_config/vendors/citycloud.json @@ -5,6 +5,9 @@ "auth_url": "https://identity1.citycloud.com:5000/v3/" }, "regions": [ + "Buf1", + "La1", + "Fra1", "Lon1", "Sto2", "Kna1" From 05b3c933b34e9cec9eb859a15392862918b3eb5f Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Tue, 2 Aug 2016 21:15:49 -0500 Subject: [PATCH 0958/3836] Fix precedence for pass-in options The passed-in argparse Namespace is flat and does not have an 'auth' dict, all of the auth options are in the top level. If the loaded dict from clouds.yaml as an 'auth' dict, that used to totally override any passed-in options. This is backwards. Make the passed-in auth options always win over clouds.yaml as this is the only way to change/override those values from the command line. Change-Id: Ic2752a396d45deeda19a16389437679829e0844d --- os_client_config/config.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index f1df7977b..0f19dd187 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -885,14 +885,14 @@ def _validate_auth(self, config, loader): plugin_options = loader.get_options() for p_opt in plugin_options: - # if it's in config.auth, win, kill it from config dict - # if it's in config and not in config.auth, move it + # 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['auth']) + winning_value = self._find_winning_auth_value(p_opt, config) if not winning_value: - winning_value = self._find_winning_auth_value(p_opt, config) + winning_value = self._find_winning_auth_value( + p_opt, config['auth']) # Clean up after ourselves for opt in [p_opt.name] + [o.name for o in p_opt.deprecated]: From cdea9eb82770b91fe9fcdb1507b3254da928131e Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Thu, 21 Jul 2016 17:55:43 +0000 Subject: [PATCH 0959/3836] Add update_server method Currently, we have no method exposing the ability from novaclient to update a name or a description. Change-Id: Ic372f730c0781cd12f792bd49e073bf1681dd1e2 --- .../add_update_server-8761059d6de7e68b.yaml | 3 + shade/_tasks.py | 5 ++ shade/openstackcloud.py | 24 +++++++ shade/tests/functional/test_compute.py | 13 ++++ shade/tests/unit/test_update_server.py | 72 +++++++++++++++++++ 5 files changed, 117 insertions(+) create mode 100644 releasenotes/notes/add_update_server-8761059d6de7e68b.yaml create mode 100644 shade/tests/unit/test_update_server.py diff --git a/releasenotes/notes/add_update_server-8761059d6de7e68b.yaml b/releasenotes/notes/add_update_server-8761059d6de7e68b.yaml new file mode 100644 index 000000000..5bbe898d4 --- /dev/null +++ b/releasenotes/notes/add_update_server-8761059d6de7e68b.yaml @@ -0,0 +1,3 @@ +--- +features: + - Add update_server method to update name or description of a server. diff --git a/shade/_tasks.py b/shade/_tasks.py index eaa7c007a..6947cb0e1 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -167,6 +167,11 @@ def main(self, client): return client.nova_client.servers.delete(**self.args) +class ServerUpdate(task_manager.Task): + def main(self, client): + return client.nova_client.servers.update(**self.args) + + class ServerRebuild(task_manager.Task): def main(self, client): return client.nova_client.servers.rebuild(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 401f4df39..6b965b47b 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4471,6 +4471,30 @@ def _delete_server( self._servers_time = self._servers_time - self._SERVER_AGE return True + @_utils.valid_kwargs( + 'name', 'description') + def update_server(self, name_or_id, **kwargs): + """Update a server. + + :param name_or_id: Name of the server to be updated. + :name: New name for the server + :description: New description for the server + + :returns: a dictionary representing the updated server. + + :raises: OpenStackCloudException on operation error. + """ + server = self.get_server(name_or_id=name_or_id) + if server is None: + raise OpenStackCloudException( + "failed to find server '{server}'".format(server=name_or_id)) + + with _utils.shade_exceptions( + "Error updating server {0}".format(name_or_id)): + return self.manager.submitTask( + _tasks.ServerUpdate( + server=server['id'], **kwargs)) + def create_server_group(self, name, policies): """Create a new server group. diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index 287806685..d17ebfaa1 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -250,3 +250,16 @@ def test_set_and_delete_metadata(self): exc.OpenStackCloudException, self.demo_cloud.delete_server_metadata, self.server_name, ['key1']) + + def test_update_server(self): + self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) + self.demo_cloud.create_server( + name=self.server_name, + image=self.image, + flavor=self.flavor, + wait=True) + server_updated = self.demo_cloud.update_server( + self.server_name, + name='new_name' + ) + self.assertEqual('new_name', server_updated['name']) diff --git a/shade/tests/unit/test_update_server.py b/shade/tests/unit/test_update_server.py new file mode 100644 index 000000000..b18f2cb77 --- /dev/null +++ b/shade/tests/unit/test_update_server.py @@ -0,0 +1,72 @@ +# -*- 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. + +""" +test_update_server +---------------------------------- + +Tests for the `update_server` command. +""" + +from mock import patch, Mock +import os_client_config +from shade import OpenStackCloud +from shade.exc import OpenStackCloudException +from shade.tests import base, fakes + + +class TestUpdateServer(base.TestCase): + + def setUp(self): + super(TestUpdateServer, self).setUp() + config = os_client_config.OpenStackConfig() + self.client = OpenStackCloud( + cloud_config=config.get_one_cloud(validate=False)) + self.client._SERVER_AGE = 0 + + def test_update_server_with_update_exception(self): + """ + Test that an exception in the novaclient update raises an exception in + update_server. + """ + with patch("shade.OpenStackCloud"): + config = { + "servers.update.side_effect": Exception("exception"), + } + OpenStackCloud.nova_client = Mock(**config) + self.assertRaises( + OpenStackCloudException, self.client.update_server, + 'server-name') + + def test_update_server_name(self): + """ + Test that update_server updates the name without raising any exception + """ + with patch("shade.OpenStackCloud"): + fake_server = fakes.FakeServer('1234', 'server-name', 'ACTIVE') + fake_update_server = fakes.FakeServer('1234', 'server-name2', + 'ACTIVE') + fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', + '1.1.1.1', '2.2.2.2', + '5678') + config = { + "servers.list.return_value": [fake_server], + "servers.update.return_value": fake_update_server, + "floating_ips.list.return_value": [fake_floating_ip] + } + OpenStackCloud.nova_client = Mock(**config) + self.assertEqual( + 'server-name2', + self.client.update_server( + 'server-name', name='server-name2')['name']) From 1a25bb249e290bcdb5dce07cd3985fb9e31e8d7d Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Wed, 3 Aug 2016 15:07:17 +1200 Subject: [PATCH 0960/3836] Depend on python-heatclient>=1.0.0 The event_utils.poll_for_events function is only available from 1.0.0. There have been reports of the ansible os_stack module failing due to a too-old python-heatclient. Depends-On: Ie10099430481ffa76f5a19557e3693189544df6b Change-Id: I0843c377409b71d808a7f096f2daa28c655add3c --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2b246196a..fd29caa6b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ python-neutronclient>=2.3.10 python-troveclient>=1.2.0 python-ironicclient>=0.10.0 python-swiftclient>=2.5.0 -python-heatclient>=0.3.0 +python-heatclient>=1.0.0 python-designateclient>=2.1.0 python-magnumclient>=2.1.0 From 9cdd967550456020d911a6806cf0fdced493787c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 21 Feb 2016 13:30:43 -0800 Subject: [PATCH 0961/3836] Go ahead and admit that we return Munch objects There was a point in time in the past where we were gung ho on using Munch as a halfway house to get us to pure dicts. However, we released 1.0 before we could do that, so we're pretty stuck with them for the forseeable future. That's not terrible - they're pretty low overhead and work pretty well. Depends-On: Ie10099430481ffa76f5a19557e3693189544df6b Change-Id: I8b7dd2c4038db999280ec0c2c9c43fb9499e6d22 --- doc/source/coding.rst | 8 ++- doc/source/usage.rst | 14 ++---- shade/openstackcloud.py | 107 ++++++++++++++++++++-------------------- shade/operatorcloud.py | 83 ++++++++++++++++--------------- 4 files changed, 103 insertions(+), 109 deletions(-) diff --git a/doc/source/coding.rst b/doc/source/coding.rst index eca626440..160aceb04 100644 --- a/doc/source/coding.rst +++ b/doc/source/coding.rst @@ -68,12 +68,10 @@ Returned Resources Complex objects returned to the caller must be a dict type. The methods `obj_to_dict()` or `obj_list_to_dict()` should be used for this. -As of this writing, those two methods are returning Bunch objects, which help +As of this writing, those two methods are returning Munch objects, which help to maintain backward compatibility with a time when shade returned raw -objects. Bunch allows the returned resource to act as *both* an object -and a dict. Use of Bunch objects will eventually be deprecated in favor -of just pure dicts, so do not depend on the Bunch object functionality. -Expect a pure dict type. +objects. Munch allows the returned resource to act as *both* an object +and a dict. Nova vs. Neutron ================ diff --git a/doc/source/usage.rst b/doc/source/usage.rst index 87269de5f..d5348c389 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -8,16 +8,10 @@ To use shade in a project:: .. note:: API methods that return a description of an OpenStack resource (e.g., - server instance, image, volume, etc.) do so using a dictionary of values - (e.g., ``server['id']``, ``image['name']``). This is the standard, and - **recommended**, way to access these resource values. - - For backward compatibility, resource values can be accessed using object - attribute access (e.g., ``server.id``, ``image.name``). Shade uses the - `Munch library `_ to provide this - behavior. This is **NOT** the recommended way to access resource values. - We keep this behavior for developer convenience in the 1.x series of shade - releases. This will likely not be the case in future, major releases of shade. + server instance, image, volume, etc.) do so using a `munch.Munch` object + from the `Munch library `_. `Munch` + objects can be accessed using either dictionary or object notation + (e.g., ``server.id``, ``image.name`` and ``server['id']``, ``image['name']``) .. autoclass:: shade.OpenStackCloud :members: diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 401f4df39..28737975e 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -455,7 +455,7 @@ def list_projects(self, domain_id=None): :param string domain_id: domain id to scope the listed projects. - :returns: a list of dicts containing the project description. + :returns: a list of ``munch.Munch`` containing the project description. :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. @@ -479,7 +479,7 @@ def search_projects(self, name_or_id=None, filters=None, domain_id=None): :param filters: a dict containing additional filters to use. :param domain_id: domain id to scope the searched projects. - :returns: a list of dict containing the projects + :returns: a list of ``munch.Munch`` containing the projects :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. @@ -494,7 +494,7 @@ def get_project(self, name_or_id, filters=None, domain_id=None): :param filters: a dict containing additional filters to use. :param domain_id: domain id (keystone v3 only) - :returns: a list of dicts containing the project description. + :returns: a list of ``munch.Munch`` containing the project description. :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. @@ -577,7 +577,7 @@ def delete_project(self, name_or_id, domain_id=None): def list_users(self): """List Keystone Users. - :returns: a list of dicts containing the user description. + :returns: a list of ``munch.Munch`` containing the user description. :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. @@ -592,7 +592,7 @@ def search_users(self, name_or_id=None, filters=None): :param string name: user name or id. :param dict filters: a dict containing additional filters to use. - :returns: a list of dict containing the users + :returns: a list of ``munch.Munch`` containing the users :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. @@ -606,7 +606,7 @@ def get_user(self, name_or_id, filters=None): :param string name_or_id: user name or id. :param dict filters: a dict containing additional filters to use. - :returns: a single dict containing the user description. + :returns: a single ``munch.Munch`` containing the user description. :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. @@ -619,7 +619,7 @@ def get_user_by_id(self, user_id, normalize=True): :param string user_id: user ID :param bool normalize: Flag to control dict normalization - :returns: a single dict containing the user description + :returns: a single ``munch.Munch`` containing the user description """ with _utils.shade_exceptions( "Error getting user with ID {user_id}".format( @@ -1140,7 +1140,7 @@ def search_networks(self, name_or_id=None, filters=None): :param filters: a dict containing additional filters to use. e.g. {'router:external': True} - :returns: a list of dicts containing the network description. + :returns: a list of ``munch.Munch`` containing the network description. :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call. @@ -1155,7 +1155,7 @@ def search_routers(self, name_or_id=None, filters=None): :param filters: a dict containing additional filters to use. e.g. {'admin_state_up': True} - :returns: a list of dicts containing the router description. + :returns: a list of ``munch.Munch`` containing the router description. :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call. @@ -1170,7 +1170,7 @@ def search_subnets(self, name_or_id=None, filters=None): :param filters: a dict containing additional filters to use. e.g. {'enable_dhcp': True} - :returns: a list of dicts containing the subnet description. + :returns: a list of ``munch.Munch`` containing the subnet description. :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call. @@ -1185,7 +1185,7 @@ def search_ports(self, name_or_id=None, filters=None): :param filters: a dict containing additional filters to use. e.g. {'device_id': '2711c67a-b4a7-43dd-ace7-6187b791c3f0'} - :returns: a list of dicts containing the port description. + :returns: a list of ``munch.Munch`` containing the port description. :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call. @@ -1261,7 +1261,7 @@ def search_stacks(self, name_or_id=None, filters=None): :param filters: a dict containing additional filters to use. e.g. {'stack_status': 'CREATE_COMPLETE'} - :returns: a list of dict containing the stack description. + :returns: a list of ``munch.Munch`` containing the stack description. :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call. @@ -1272,7 +1272,7 @@ def search_stacks(self, name_or_id=None, filters=None): def list_keypairs(self): """List all available keypairs. - :returns: A list of keypair dicts. + :returns: A list of ``munch.Munch`` containing keypair info. """ with _utils.shade_exceptions("Error fetching keypair list"): @@ -1282,7 +1282,7 @@ def list_networks(self, filters=None): """List all available networks. :param filters: (optional) dict of filter conditions to push down - :returns: A list of network dicts. + :returns: A list of ``munch.Munc`` containing network info. """ # Translate None from search interface to empty {} for kwargs below @@ -1296,7 +1296,7 @@ def list_routers(self, filters=None): """List all available routers. :param filters: (optional) dict of filter conditions to push down - :returns: A list of router dicts. + :returns: A list of router ``munch.Munch``. """ # Translate None from search interface to empty {} for kwargs below @@ -1310,7 +1310,7 @@ def list_subnets(self, filters=None): """List all available subnets. :param filters: (optional) dict of filter conditions to push down - :returns: A list of subnet dicts. + :returns: A list of subnet ``munch.Munch``. """ # Translate None from search interface to empty {} for kwargs below @@ -1324,7 +1324,7 @@ def list_ports(self, filters=None): """List all available ports. :param filters: (optional) dict of filter conditions to push down - :returns: A list of port dicts. + :returns: A list of port ``munch.Munch``. """ # If pushdown filters are specified, bypass local caching. @@ -1356,7 +1356,7 @@ def _list_ports(self, filters): def list_volumes(self, cache=True): """List all available volumes. - :returns: A list of volume dicts. + :returns: A list of volume ``munch.Munch``. """ if not cache: @@ -1370,7 +1370,7 @@ def list_volumes(self, cache=True): def list_flavors(self, get_extra=True): """List all available flavors. - :returns: A list of flavor dicts. + :returns: A list of flavor ``munch.Munch``. """ with _utils.shade_exceptions("Error fetching flavor list"): @@ -1398,7 +1398,7 @@ def list_flavors(self, get_extra=True): def list_stacks(self): """List all Heat stacks. - :returns: a list of dict containing the stack description. + :returns: a list of ``munch.Munch`` containing the stack description. :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call. @@ -1410,7 +1410,7 @@ def list_stacks(self): def list_server_security_groups(self, server): """List all security groups associated with the given server. - :returns: A list of security group dicts. + :returns: A list of security group ``munch.Munch``. """ # Don't even try if we're a cloud that doesn't have them @@ -1426,7 +1426,7 @@ def list_server_security_groups(self, server): def list_security_groups(self): """List all available security groups. - :returns: A list of security group dicts. + :returns: A list of security group ``munch.Munch``. """ # Handle neutron security groups @@ -1453,7 +1453,7 @@ def list_security_groups(self): def list_servers(self, detailed=False): """List all available servers. - :returns: A list of server dicts. + :returns: A list of server ``munch.Munch``. """ if (time.time() - self._servers_time) >= self._SERVER_AGE: @@ -1542,7 +1542,7 @@ def list_images(self, filter_deleted=True): def list_floating_ip_pools(self): """List all available floating IP pools. - :returns: A list of floating IP pool dicts. + :returns: A list of floating IP pool ``munch.Munch``. """ if not self._has_nova_extension('os-floating-ip-pools'): @@ -1555,7 +1555,7 @@ def list_floating_ip_pools(self): def list_floating_ips(self): """List all available floating IPs. - :returns: A list of floating IP dicts. + :returns: A list of floating IP ``munch.Munch``. """ if self._use_neutron_floating(): @@ -1759,7 +1759,7 @@ def get_default_network(self): def get_external_networks(self): """Return the networks that are configured to route northbound. - :returns: A list of network dicts if one is found + :returns: A list of network ``munch.Munch`` if one is found """ self._find_interesting_networks() return self._external_networks @@ -1767,7 +1767,7 @@ def get_external_networks(self): def get_internal_networks(self): """Return the networks that are configured to not route northbound. - :returns: A list of network dicts if one is found + :returns: A list of network ``munch.Munch`` if one is found """ self._find_interesting_networks() return self._internal_networks @@ -1791,7 +1791,7 @@ def get_keypair(self, name_or_id, filters=None): } } - :returns: A keypair dict or None if no matching keypair is + :returns: A keypair ``munch.Munch`` or None if no matching keypair is found. """ @@ -1812,7 +1812,7 @@ def get_network(self, name_or_id, filters=None): } } - :returns: A network dict or None if no matching network is + :returns: A network ``munch.Munch`` or None if no matching network is found. """ @@ -1833,7 +1833,7 @@ def get_router(self, name_or_id, filters=None): } } - :returns: A router dict or None if no matching router is + :returns: A router ``munch.Munch`` or None if no matching router is found. """ @@ -1854,7 +1854,7 @@ def get_subnet(self, name_or_id, filters=None): } } - :returns: A subnet dict or None if no matching subnet is + :returns: A subnet ``munch.Munch`` or None if no matching subnet is found. """ @@ -1875,7 +1875,7 @@ def get_port(self, name_or_id, filters=None): } } - :returns: A port dict or None if no matching port is found. + :returns: A port ``munch.Munch`` or None if no matching port is found. """ return _utils._get_entity(self.search_ports, name_or_id, filters) @@ -1895,7 +1895,7 @@ def get_volume(self, name_or_id, filters=None): } } - :returns: A volume dict or None if no matching volume is + :returns: A volume ``munch.Munch`` or None if no matching volume is found. """ @@ -1916,7 +1916,7 @@ def get_flavor(self, name_or_id, filters=None): } } - :returns: A flavor dict or None if no matching flavor is + :returns: A flavor ``munch.Munch`` or None if no matching flavor is found. """ @@ -1937,7 +1937,7 @@ def get_security_group(self, name_or_id, filters=None): } } - :returns: A security group dict or None if no matching + :returns: A security group ``munch.Munch`` or None if no matching security group is found. """ @@ -1959,7 +1959,7 @@ def get_server(self, name_or_id=None, filters=None, detailed=False): } } - :returns: A server dict or None if no matching server is + :returns: A server ``munch.Munch`` or None if no matching server is found. """ @@ -2006,7 +2006,8 @@ def get_image(self, name_or_id, filters=None): } } - :returns: An image dict or None if no matching image is found. + :returns: An image ``munch.Munch`` or None if no matching image + is found """ return _utils._get_entity(self.search_images, name_or_id, filters) @@ -2066,7 +2067,7 @@ def get_floating_ip(self, id, filters=None): } } - :returns: A floating IP dict or None if no matching floating + :returns: A floating IP ``munch.Munch`` or None if no matching floating IP is found. """ @@ -2079,7 +2080,7 @@ def get_stack(self, name_or_id, filters=None): :param filters: a dict containing additional filters to use. e.g. {'stack_status': 'CREATE_COMPLETE'} - :returns: a dict containing the stack description + :returns: a ``munch.Munch`` containing the stack description :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call or if multiple matches are found. @@ -2241,8 +2242,8 @@ def add_router_interface(self, router, subnet_id=None, port_id=None): :param string subnet_id: The ID of the subnet to use for the interface :param string port_id: The ID of the port to use for the interface - :returns: A dict with the router id (id), subnet ID (subnet_id), - port ID (port_id) and tenant ID (tenant_id). + :returns: A ``munch.Munch`` with the router id (id), + subnet ID (subnet_id), port ID (port_id) and tenant ID (tenant_id). :raises: OpenStackCloudException on operation error. """ @@ -2295,7 +2296,7 @@ def list_router_interfaces(self, router, interface_type=None): Controls whether all, internal interfaces or external interfaces are returned. - :returns: A list of port dict objects. + :returns: A list of port ``munch.Munch`` objects. """ ports = self.search_ports(filters={'device_id': router['id']}) @@ -3148,7 +3149,7 @@ def get_volume_snapshot(self, name_or_id, filters=None): } } - :returns: A volume dict or None if no matching volume is + :returns: A volume ``munch.Munch`` or None if no matching volume is found. """ @@ -3158,7 +3159,7 @@ def get_volume_snapshot(self, name_or_id, filters=None): def list_volume_snapshots(self, detailed=True, search_opts=None): """List all volume snapshots. - :returns: A list of volume snapshots dicts. + :returns: A list of volume snapshots ``munch.Munch``. """ with _utils.shade_exceptions("Error getting a list of snapshots"): @@ -3612,7 +3613,7 @@ def _attach_ip_to_server( :param skip_attach: (optional) Skip the actual attach and just do the wait. Defaults to False. - :returns: The server dict + :returns: The server ``munch.Munch`` :raises: OpenStackCloudException, on operation error. """ @@ -3860,7 +3861,7 @@ def _add_ip_from_pool( :param nat_destination: (optional) the name of the network of the port to associate with the floating ip. - :returns: the update server dict + :returns: the updated server ``munch.Munch`` """ if reuse: f_ip = self.available_floating_ip(network=network) @@ -3894,7 +3895,7 @@ def add_ip_list( :param fixed_address: (optional) Fixed address of the server to attach the IP to - :returns: The updated server dict + :returns: The updated server ``munch.Munch`` :raises: ``OpenStackCloudException``, on operation error. """ @@ -4160,7 +4161,7 @@ def create_server( be attached to, if it's not possible to infer from the cloud's configuration. (Optional, defaults to None) - :returns: A dict representing the created server. + :returns: A ``munch.Munch`` representing the created server. :raises: OpenStackCloudException on operation error. """ # nova cli calls this boot_volume. Let's be the same @@ -5114,7 +5115,7 @@ def create_port(self, network_id, **kwargs): :param device_id: The ID of the device that uses this port. For example, a virtual server. (Optional) - :returns: a dictionary describing the created port. + :returns: a ``munch.Munch`` describing the created port. :raises: ``OpenStackCloudException`` on operation error. """ @@ -5173,7 +5174,7 @@ def update_port(self, name_or_id, **kwargs): :param device_owner: The ID of the entity that uses this port. For example, a DHCP agent. (Optional) - :returns: a dictionary describing the updated port. + :returns: a ``munch.Munch`` describing the updated port. :raises: OpenStackCloudException on operation error. """ @@ -5213,7 +5214,7 @@ def create_security_group(self, name, description): :param string name: A name for the security group. :param string description: Describes the security group. - :returns: A dict representing the new security group. + :returns: A ``munch.Munch`` representing the new security group. :raises: OpenStackCloudException on operation error. :raises: OpenStackCloudUnavailableFeature if security groups are @@ -5297,7 +5298,7 @@ def update_security_group(self, name_or_id, **kwargs): :param string name: New name for the security group. :param string description: New description for the security group. - :returns: A dictionary describing the updated security group. + :returns: A ``munch.Munch`` describing the updated security group. :raises: OpenStackCloudException on operation error. """ @@ -5380,7 +5381,7 @@ def create_security_group_rule(self, Must be IPv4 or IPv6, and addresses represented in CIDR must match the ingress or egress rules. - :returns: A dict representing the new security group rule. + :returns: A ``munch.Munch`` representing the new security group rule. :raises: OpenStackCloudException on operation error. """ diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 8607a4b23..a91213372 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -85,8 +85,8 @@ def get_machine(self, name_or_id): :param name_or_id: A node name or UUID that will be looked up. - :returns: Dictonary representing the node found or None if no nodes - are found. + :returns: ``munch.Munch`` representing the node found or None if no + nodes are found. """ try: return self.manager.submitTask( @@ -99,7 +99,7 @@ def get_machine_by_mac(self, mac): :param mac: Port MAC address to query in order to return a node. - :returns: Dictonary representing the node found or None + :returns: ``munch.Munch`` representing the node found or None if the node is not found. """ try: @@ -125,7 +125,7 @@ def inspect_machine(self, name_or_id, wait=False, timeout=3600): :param timeout: Integer value, defautling to 3600 seconds, for the$ wait state to reach completion. - :returns: Dictonary representing the current state of the machine + :returns: ``munch.Munch`` representing the current state of the machine upon exit of the method. """ @@ -217,7 +217,7 @@ def register_machine(self, nics, wait=False, timeout=3600, :raises: OpenStackCloudException on operation error. - :returns: Returns a dictonary representing the new + :returns: Returns a ``munch.Munch`` representing the new baremetal node. """ with _utils.shade_exceptions("Error registering machine with Ironic"): @@ -394,7 +394,7 @@ def patch_machine(self, name_or_id, patch): :raises: OpenStackCloudException on operation error. - :returns: Dictonary representing the newly updated node. + :returns: ``munch.Munch`` representing the newly updated node. """ with _utils.shade_exceptions( @@ -438,7 +438,7 @@ def update_machine(self, name_or_id, chassis_uuid=None, driver=None, :raises: OpenStackCloudException on operation error. - :returns: Dictonary containing a machine sub-dictonary consisting + :returns: ``munch.Munch`` containing a machine sub-dictonary consisting of the updated data returned from the API update operation, and a list named changes which contains all of the API paths that received updates. @@ -556,7 +556,7 @@ def node_set_provision_state(self, :raises: OpenStackCloudException on operation error. - :returns: Dictonary representing the current state of the machine + :returns: ``munch.Munch`` representing the current state of the machine upon exit of the method. """ with _utils.shade_exceptions( @@ -757,8 +757,8 @@ def create_service(self, name, enabled=True, **kwargs): :param description: Service description (optional). :param enabled: Whether the service is enabled (v3 only) - :returns: a dict containing the services description, i.e. the - following attributes:: + :returns: a ``munch.Munch`` containing the services description, + i.e. the following attributes:: - id: - name: - type: @@ -816,7 +816,7 @@ def update_service(self, name_or_id, **kwargs): def list_services(self): """List all Keystone services. - :returns: a list of dict containing the services description. + :returns: a list of ``munch.Munch`` containing the services description :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call. @@ -832,7 +832,7 @@ def search_services(self, name_or_id=None, filters=None): :param filters: a dict containing additional filters to use. e.g. {'type': 'network'}. - :returns: a list of dict containing the services description. + :returns: a list of ``munch.Munch`` containing the services description :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call. @@ -847,8 +847,8 @@ def get_service(self, name_or_id, filters=None): :param filters: a dict containing additional filters to use. e.g. {'type': 'network'} - :returns: a dict containing the services description, i.e. the - following attributes:: + :returns: a ``munch.Munch`` containing the services description, + i.e. the following attributes:: - id: - name: - type: @@ -902,7 +902,7 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, (url, interface) calling semantics are supported. But you can only use one of them at a time. - :returns: a list of dicts containing the endpoint description. + :returns: a list of ``munch.Munch`` containing the endpoint description :raises: OpenStackCloudException if the service cannot be found or if something goes wrong during the openstack API call. @@ -983,7 +983,7 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, def list_endpoints(self): """List Keystone endpoints. - :returns: a list of dict containing the endpoint description. + :returns: a list of ``munch.Munch`` containing the endpoint description :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. @@ -1001,8 +1001,8 @@ def search_endpoints(self, id=None, filters=None): :param filters: a dict containing additional filters to use. e.g. {'region': 'region-a.geo-1'} - :returns: a list of dict containing the endpoint description. Each dict - contains the following attributes:: + :returns: a list of ``munch.Munch`` containing the endpoint + description. Each dict contains the following attributes:: - id: - region: - public_url: @@ -1022,8 +1022,8 @@ def get_endpoint(self, id, filters=None): :param filters: a dict containing additional filters to use. e.g. {'region': 'region-a.geo-1'} - :returns: a dict containing the endpoint description. i.e. a dict - containing the following attributes:: + :returns: a ``munch.Munch`` containing the endpoint description. + i.e. a ``munch.Munch`` containing the following attributes:: - id: - region: - public_url: @@ -1066,7 +1066,7 @@ def create_domain( :param description: A description of the domain. :param enabled: Is the domain enabled or not (default True). - :returns: a dict containing the domain description + :returns: a ``munch.Munch`` containing the domain description :raise OpenStackCloudException: if the domain cannot be created """ @@ -1135,7 +1135,7 @@ def delete_domain(self, domain_id=None, name_or_id=None): def list_domains(self): """List Keystone domains. - :returns: a list of dicts containing the domain description. + :returns: a list of ``munch.Munch`` containing the domain description. :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. @@ -1151,8 +1151,8 @@ def search_domains(self, filters=None, name_or_id=None): :param dict filters: A dict containing additional filters to use. Keys to search on are id, name, enabled and description. - :returns: a list of dicts containing the domain description. Each dict - contains the following attributes:: + :returns: a list of ``munch.Munch`` containing the domain description. + Each ``munch.Munch`` contains the following attributes:: - id: - name: - description: @@ -1179,8 +1179,9 @@ def get_domain(self, domain_id=None, name_or_id=None, filters=None): :param dict filters: A dict containing additional filters to use. Keys to search on are id, name, enabled and description. - :returns: a dict containing the domain description, or None if not - found. Each dict contains the following attributes:: + :returns: a ``munch.Munch`` containing the domain description, or None + if not found. Each ``munch.Munch`` contains the following + attributes:: - id: - name: - description: @@ -1203,7 +1204,7 @@ def get_domain(self, domain_id=None, name_or_id=None, filters=None): def list_groups(self): """List Keystone Groups. - :returns: A list of dicts containing the group description. + :returns: A list of ``munch.Munch`` containing the group description. :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. @@ -1218,7 +1219,7 @@ def search_groups(self, name_or_id=None, filters=None): :param name: Group name or id. :param filters: A dict containing additional filters to use. - :returns: A list of dict containing the group description. + :returns: A list of ``munch.Munch`` containing the group description. :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. @@ -1232,7 +1233,7 @@ def get_group(self, name_or_id, filters=None): :param id: Group name or id. :param filters: A dict containing additional filters to use. - :returns: A dict containing the group description. + :returns: A ``munch.Munch`` containing the group description. :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. @@ -1246,7 +1247,7 @@ def create_group(self, name, description, domain=None): :param string description: Group description. :param string domain: Domain name or ID for the group. - :returns: A dict containing the group description. + :returns: A ``munch.Munch`` containing the group description. :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. @@ -1276,7 +1277,7 @@ def update_group(self, name_or_id, name=None, description=None): :param string name: New group name. :param string description: New group description. - :returns: A dict containing the group description. + :returns: A ``munch.Munch`` containing the group description. :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. @@ -1324,7 +1325,7 @@ def delete_group(self, name_or_id): def list_roles(self): """List Keystone roles. - :returns: a list of dicts containing the role description. + :returns: a list of ``munch.Munch`` containing the role description. :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. @@ -1340,8 +1341,8 @@ def search_roles(self, name_or_id=None, filters=None): :param string name: role name or id. :param dict filters: a dict containing additional filters to use. - :returns: a list of dict containing the role description. Each dict - contains the following attributes:: + :returns: a list of ``munch.Munch`` containing the role description. + Each ``munch.Munch`` contains the following attributes:: - id: - name: @@ -1359,8 +1360,8 @@ def get_role(self, name_or_id, filters=None): :param id: role name or id. :param filters: a dict containing additional filters to use. - :returns: a single dict containing the role description. Each dict - contains the following attributes:: + :returns: a single ``munch.Munch`` containing the role description. + Each ``munch.Munch`` contains the following attributes:: - id: - name: @@ -1417,8 +1418,8 @@ def list_role_assignments(self, filters=None): NOTE: For keystone v2, only user, project, and role are used. Project and user are both required in filters. - :returns: a list of dicts containing the role assignment description. - Contains the following attributes:: + :returns: a list of ``munch.Munch`` containing the role assignment + description. Contains the following attributes:: - id: - user|group: @@ -1457,7 +1458,7 @@ def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", :param rxtx_factor: RX/TX factor :param is_public: Make flavor accessible to the public - :returns: A dict describing the new flavor. + :returns: A ``munch.Munch`` describing the new flavor. :raises: OpenStackCloudException on operation error. """ @@ -1571,7 +1572,7 @@ def create_role(self, name): :param string name: The name of the role. - :returns: a dict containing the role description + :returns: a ``munch.Munch`` containing the role description :raise OpenStackCloudException: if the role cannot be created """ @@ -1761,7 +1762,7 @@ def revoke_role(self, name_or_id, user=None, group=None, def list_hypervisors(self): """List all hypervisors - :returns: A list of hypervisor dicts. + :returns: A list of hypervisor ``munch.Munch``. """ with _utils.shade_exceptions("Error fetching hypervisor list"): From 9c699ed3a649d7d6d1f3c9d8a13eda483f948512 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 9 Jul 2016 17:36:48 +0900 Subject: [PATCH 0962/3836] Add the new DreamCompute cloud It does things the happy way - direct routing of IPv4 and IPv6. However, it's a respin, so we're naming it dreamcompute rather than dreamhost so that users don't get broken by the shift. Remove dreamhost from the docs - people looking at documentation should be using the new region. Change-Id: I92eb38635c4389d2e9326fab038137a673497fa8 --- doc/source/vendor-support.rst | 17 +++++++++++++++++ os_client_config/vendors/dreamcompute.json | 11 +++++++++++ os_client_config/vendors/dreamhost.json | 2 ++ 3 files changed, 30 insertions(+) create mode 100644 os_client_config/vendors/dreamcompute.json diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index f97c9f6ff..0297b7d24 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -99,9 +99,26 @@ sal01 Manchester, UK * Image API Version is 1 +dreamcompute +------------ + +https://iad2.dream.io:5000 + +============== ================ +Region Name Human Name +============== ================ +RegionOne Region One +============== ================ + +* Identity API Version is 3 +* Images must be in `raw` format +* IPv6 is provided to every server + dreamhost --------- +Deprecated, please use dreamcompute + https://keystone.dream.io/v2.0 ============== ================ diff --git a/os_client_config/vendors/dreamcompute.json b/os_client_config/vendors/dreamcompute.json new file mode 100644 index 000000000..8244cf77c --- /dev/null +++ b/os_client_config/vendors/dreamcompute.json @@ -0,0 +1,11 @@ +{ + "name": "dreamcompute", + "profile": { + "auth": { + "auth_url": "https://iad2.dream.io:5000" + }, + "identity_api_version": "3", + "region_name": "RegionOne", + "image_format": "raw" + } +} diff --git a/os_client_config/vendors/dreamhost.json b/os_client_config/vendors/dreamhost.json index 6fc2ccf8a..ea2ebac1e 100644 --- a/os_client_config/vendors/dreamhost.json +++ b/os_client_config/vendors/dreamhost.json @@ -1,6 +1,8 @@ { "name": "dreamhost", "profile": { + "status": "deprecated", + "message": "The dreamhost profile is deprecated. Please use the dreamcompute profile instead", "auth": { "auth_url": "https://keystone.dream.io" }, From 37dcc7e8d874a2e5325d7e473d883c373bf694ca Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 3 Aug 2016 10:43:15 -0500 Subject: [PATCH 0963/3836] Add release notes for 1.19.0 release Change-Id: I92ffcf611d31f7a4f11e5228022ea64864823389 --- .../notes/cloud-profile-status-e0d29b5e2f10e95c.yaml | 6 ++++++ releasenotes/notes/magic-fixes-dca4ae4dac2441a8.yaml | 6 ++++++ releasenotes/notes/option-precedence-1fecab21fdfb2c33.yaml | 7 +++++++ releasenotes/notes/vendor-updates-f11184ba56bb27cf.yaml | 4 ++++ 4 files changed, 23 insertions(+) create mode 100644 releasenotes/notes/cloud-profile-status-e0d29b5e2f10e95c.yaml create mode 100644 releasenotes/notes/magic-fixes-dca4ae4dac2441a8.yaml create mode 100644 releasenotes/notes/option-precedence-1fecab21fdfb2c33.yaml create mode 100644 releasenotes/notes/vendor-updates-f11184ba56bb27cf.yaml diff --git a/releasenotes/notes/cloud-profile-status-e0d29b5e2f10e95c.yaml b/releasenotes/notes/cloud-profile-status-e0d29b5e2f10e95c.yaml new file mode 100644 index 000000000..b447ed0a4 --- /dev/null +++ b/releasenotes/notes/cloud-profile-status-e0d29b5e2f10e95c.yaml @@ -0,0 +1,6 @@ +--- +features: + - Add a field to vendor cloud profiles to indicate + active, deprecated and shutdown status. A message to + the user is triggered when attempting to use cloud + with either deprecated or shutdown status. diff --git a/releasenotes/notes/magic-fixes-dca4ae4dac2441a8.yaml b/releasenotes/notes/magic-fixes-dca4ae4dac2441a8.yaml new file mode 100644 index 000000000..570e4dcca --- /dev/null +++ b/releasenotes/notes/magic-fixes-dca4ae4dac2441a8.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - Refactor ``OpenStackConfig._fix_backward_madness()`` into + ``OpenStackConfig.magic_fixes()`` that allows subclasses + to inject more fixup magic into the flow during + ``get_one_cloud()`` processing. diff --git a/releasenotes/notes/option-precedence-1fecab21fdfb2c33.yaml b/releasenotes/notes/option-precedence-1fecab21fdfb2c33.yaml new file mode 100644 index 000000000..06e6bd2f6 --- /dev/null +++ b/releasenotes/notes/option-precedence-1fecab21fdfb2c33.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - Reverse the order of option selction in + ``OpenStackConfig._validate_auth()`` to prefer auth options + passed in (from argparse) over those found in clouds.yaml. + This allows the application to override config profile + auth settings. diff --git a/releasenotes/notes/vendor-updates-f11184ba56bb27cf.yaml b/releasenotes/notes/vendor-updates-f11184ba56bb27cf.yaml new file mode 100644 index 000000000..e1d6d41a2 --- /dev/null +++ b/releasenotes/notes/vendor-updates-f11184ba56bb27cf.yaml @@ -0,0 +1,4 @@ +--- +other: + - Add citycloud regions for Buffalo, Frankfurt, Karlskrona and Los Angles + - Add new DreamCompute cloud and deprecate DreamHost cloud From d71a90220397f1baa80e943a37854a5be2c8726c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 4 Aug 2016 20:45:49 +0000 Subject: [PATCH 0964/3836] Revert "Fix precedence for pass-in options" This reverts commit 05b3c933b34e9cec9eb859a15392862918b3eb5f. Change-Id: I07dd701ca911cd12701519d2e6d624c69baa0848 --- os_client_config/config.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 0f19dd187..f1df7977b 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -885,14 +885,14 @@ def _validate_auth(self, config, loader): plugin_options = loader.get_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 + # if it's in config.auth, win, kill it from config dict + # if it's in config and not in config.auth, move it # deprecated loses to current # provided beats default, deprecated or not - winning_value = self._find_winning_auth_value(p_opt, config) + winning_value = self._find_winning_auth_value( + p_opt, config['auth']) if not winning_value: - winning_value = self._find_winning_auth_value( - p_opt, config['auth']) + winning_value = self._find_winning_auth_value(p_opt, config) # Clean up after ourselves for opt in [p_opt.name] + [o.name for o in p_opt.deprecated]: From ddfed7f2fbabd1eba3b6ac700cee065c070433a7 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 4 Aug 2016 16:18:42 -0500 Subject: [PATCH 0965/3836] Pass the argparse data into to validate_auth We have two contradictory precedence needs that are impossible to satisfy because we're losing knowledge of the source of data as we build the ultimate dict of data. If we carry the argparse data along in a separate bucket for longer, we can check to see if it's there so that it can win, but so that in situations where kwargs is complex and contains both a top-level and an auth dict we don't assume that the values that are not in the auth dict came from argparse. By doing this, we can establish that precedence for auth args is: - argparse - auth dict - top level kwargs Change-Id: I9eca5937077f5873f7896b6745951fb8d8c4747c --- os_client_config/config.py | 60 +++++++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index f1df7977b..2aa053034 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -763,7 +763,7 @@ def get_all_clouds(self): cloud, region_name=region['name'])) return clouds - def _fix_args(self, args, argparse=None): + def _fix_args(self, args=None, argparse=None): """Massage the passed-in options Replace - with _ and strip os_ prefixes. @@ -771,6 +771,8 @@ def _fix_args(self, args, argparse=None): Convert an argparse Namespace object to a dict, removing values that are either None or ''. """ + if not args: + args = {} if argparse: # Convert the passed-in Namespace @@ -831,7 +833,7 @@ def _get_auth_loader(self, config): config['auth']['token'] = 'notused' return loading.get_plugin_loader(config['auth_type']) - def _validate_auth_ksc(self, config, cloud): + def _validate_auth_ksc(self, config, cloud, fixed_argparse): try: import keystoneclient.auth as ksc_auth except ImportError: @@ -842,14 +844,22 @@ def _validate_auth_ksc(self, config, cloud): config['auth_type']).get_options() for p_opt in plugin_options: + # if it's in argparse, it was passed on the command line and wins # if it's in config.auth, win, kill it from config dict # if it's in config and not in config.auth, move it # deprecated loses to current # provided beats default, deprecated or not winning_value = self._find_winning_auth_value( - p_opt, config['auth']) - if not winning_value: - winning_value = self._find_winning_auth_value(p_opt, config) + p_opt, fixed_argparse) + if winning_value: + found_in_argparse = True + else: + found_in_argparse = False + winning_value = self._find_winning_auth_value( + p_opt, config['auth']) + if not winning_value: + winning_value = self._find_winning_auth_value( + p_opt, config) # if the plugin tells us that this value is required # then error if it's doesn't exist now @@ -865,7 +875,12 @@ def _validate_auth_ksc(self, config, cloud): # Clean up after ourselves for opt in [p_opt.name] + [o.name for o in p_opt.deprecated_opts]: opt = opt.replace('-', '_') - config.pop(opt, None) + # don't do this if the value came from argparse, because we + # don't (yet) know if the value in not-auth came from argparse + # overlay or from someone passing in a dict to kwargs + # TODO(mordred) Fix that data path too + if not found_in_argparse: + config.pop(opt, None) config['auth'].pop(opt, None) if winning_value: @@ -879,25 +894,38 @@ def _validate_auth_ksc(self, config, cloud): return config - def _validate_auth(self, config, loader): + def _validate_auth(self, config, loader, fixed_argparse): # May throw a keystoneauth1.exceptions.NoMatchingPlugin plugin_options = loader.get_options() for p_opt in plugin_options: + # if it's in argparse, it was passed on the command line and wins # if it's in config.auth, win, kill it from config dict # if it's in config and not in config.auth, move it # deprecated loses to current # provided beats default, deprecated or not winning_value = self._find_winning_auth_value( - p_opt, config['auth']) - if not winning_value: - winning_value = self._find_winning_auth_value(p_opt, config) + p_opt, fixed_argparse) + if winning_value: + found_in_argparse = True + else: + found_in_argparse = False + winning_value = self._find_winning_auth_value( + p_opt, config['auth']) + if not winning_value: + winning_value = self._find_winning_auth_value( + p_opt, config) # 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) + # don't do this if the value came from argparse, because we + # don't (yet) know if the value in not-auth came from argparse + # overlay or from someone passing in a dict to kwargs + # TODO(mordred) Fix that data path too + if not found_in_argparse: + config.pop(opt, None) config['auth'].pop(opt, None) if winning_value: @@ -932,6 +960,11 @@ def get_one_cloud(self, cloud=None, validate=True, """ args = self._fix_args(kwargs, argparse=argparse) + # Run the fix just for argparse by itself. We need to + # have a copy of the argparse options separately from + # any merged copied later in validate_auth so that we + # can determine precedence + fixed_argparse = self._fix_args(argparse=argparse) if cloud is None: if 'cloud' in args: @@ -995,7 +1028,7 @@ def get_one_cloud(self, cloud=None, validate=True, if validate: try: loader = self._get_auth_loader(config) - config = self._validate_auth(config, loader) + config = self._validate_auth(config, loader, fixed_argparse) auth_plugin = loader.load_from_options(**config['auth']) except Exception as e: # We WANT the ksa exception normally @@ -1005,7 +1038,8 @@ def get_one_cloud(self, cloud=None, validate=True, self.log.debug("Deferring keystone exception: {e}".format(e=e)) auth_plugin = None try: - config = self._validate_auth_ksc(config, cloud) + config = self._validate_auth_ksc( + config, cloud, fixed_argparse) except Exception: raise e else: From cfa87b1e7f3d6325c4f0ef9f4d2985948e4d99d3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 5 Aug 2016 06:36:14 -0500 Subject: [PATCH 0966/3836] Add test for precedence rules This should cover both the OSC and the ansible incoming use cases. Change-Id: I3fdc83837692d31c5579d91892a387a5d1023785 --- os_client_config/tests/test_config.py | 53 +++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index a0978f059..500887812 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -373,6 +373,59 @@ def test_get_one_cloud_argparse(self): self.assertEqual(cc.region_name, 'region2') self.assertEqual(cc.snack_type, 'cookie') + def test_get_one_cloud_precedence(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + + kwargs = { + 'auth': { + 'username': 'testuser', + 'password': 'authpass', + 'project-id': 'testproject', + 'auth_url': 'http://example.com/v2', + }, + 'region_name': 'kwarg_region', + 'password': 'ansible_password', + 'arbitrary': 'value', + } + + args = dict( + auth_url='http://example.com/v2', + username='user', + password='argpass', + project_name='project', + region_name='region2', + snack_type='cookie', + ) + + options = argparse.Namespace(**args) + cc = c.get_one_cloud( + argparse=options, **kwargs) + self.assertEqual(cc.region_name, 'region2') + self.assertEqual(cc.auth['password'], 'argpass') + self.assertEqual(cc.snack_type, 'cookie') + + def test_get_one_cloud_precedence_no_argparse(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + + kwargs = { + 'auth': { + 'username': 'testuser', + 'password': 'authpass', + 'project-id': 'testproject', + 'auth_url': 'http://example.com/v2', + }, + 'region_name': 'kwarg_region', + 'password': 'ansible_password', + 'arbitrary': 'value', + } + + cc = c.get_one_cloud(**kwargs) + self.assertEqual(cc.region_name, 'kwarg_region') + self.assertEqual(cc.auth['password'], 'authpass') + self.assertIsNone(cc.password) + def test_get_one_cloud_just_argparse(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) From dd7f0c9295465943739929c19b820bd44b549b85 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 19 Jul 2016 09:55:31 -0500 Subject: [PATCH 0967/3836] Move list_magnum_services to OperatorCloud It requires admin credentials, thus it's an Operator task. While this is _technically_ an API break, it's not possible for list_magnum_services to be used by a non-admin user, so I think we're very unlikely to break anyone. Change-Id: If3af5f1ef702da39ac27c01f9792d11f6339c7ab --- shade/openstackcloud.py | 10 ---------- shade/operatorcloud.py | 10 ++++++++++ shade/tests/unit/test_magnum_services.py | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 401f4df39..6b98cc97b 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5894,13 +5894,3 @@ def update_baymodel(self, name_or_id, operation, **kwargs): new_baymodel = self.get_baymodel(name_or_id) return new_baymodel - - def list_magnum_services(self): - """List all Magnum services. - :returns: a list of dicts containing the service details. - - :raises: OpenStackCloudException on operation error. - """ - with _utils.shade_exceptions("Error fetching Magnum services list"): - return self.manager.submitTask( - _tasks.MagnumServicesList()) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 8607a4b23..777604264 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -2104,3 +2104,13 @@ def delete_network_quotas(self, name_or_id): with _utils.neutron_exceptions("network client call failed"): return self.manager.submitTask( _tasks.NeutronQuotasDelete(tenant_id=proj.id)) + + def list_magnum_services(self): + """List all Magnum services. + :returns: a list of dicts containing the service details. + + :raises: OpenStackCloudException on operation error. + """ + with _utils.shade_exceptions("Error fetching Magnum services list"): + return self.manager.submitTask( + _tasks.MagnumServicesList()) diff --git a/shade/tests/unit/test_magnum_services.py b/shade/tests/unit/test_magnum_services.py index d63bb6694..340a07cf5 100644 --- a/shade/tests/unit/test_magnum_services.py +++ b/shade/tests/unit/test_magnum_services.py @@ -35,7 +35,7 @@ class TestMagnumServices(base.TestCase): def setUp(self): super(TestMagnumServices, self).setUp() - self.cloud = shade.openstack_cloud(validate=False) + self.cloud = shade.operator_cloud(validate=False) @mock.patch.object(shade.OpenStackCloud, 'magnum_client') def test_list_magnum_services(self, mock_magnum): From 864df79c000edf73f9e59a9317127ac4a8015632 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 19 Jul 2016 12:55:00 -0500 Subject: [PATCH 0968/3836] Stop creating cloud objects in functional tests We create a demo_cloud and an operator_cloud in the base TestCase for functional tests. We should use those. Also we should use the BaseFunctionalTestCase class. Change-Id: I5949e264eef9b790b7645499b3ed9fea29ca8010 --- shade/tests/functional/test_identity.py | 110 ++++++++++++------------ shade/tests/functional/test_quotas.py | 69 +++++++-------- shade/tests/functional/test_stack.py | 52 +++++------ 3 files changed, 114 insertions(+), 117 deletions(-) diff --git a/shade/tests/functional/test_identity.py b/shade/tests/functional/test_identity.py index cc6b727e4..6c95b788f 100644 --- a/shade/tests/functional/test_identity.py +++ b/shade/tests/functional/test_identity.py @@ -22,7 +22,6 @@ import random import string -from shade import operator_cloud from shade import OpenStackCloudException from shade.tests.functional import base @@ -30,7 +29,6 @@ class TestIdentity(base.BaseFunctionalTestCase): def setUp(self): super(TestIdentity, self).setUp() - self.cloud = operator_cloud(cloud='devstack-admin') self.role_prefix = 'test_role' + ''.join( random.choice(string.ascii_lowercase) for _ in range(5)) self.user_prefix = self.getUniqueString('user') @@ -38,17 +36,17 @@ def setUp(self): self.addCleanup(self._cleanup_users) self.identity_version = \ - self.cloud.cloud_config.get_api_version('identity') + self.operator_cloud.cloud_config.get_api_version('identity') if self.identity_version not in ('2', '2.0'): self.addCleanup(self._cleanup_groups) self.addCleanup(self._cleanup_roles) def _cleanup_groups(self): exception_list = list() - for group in self.cloud.list_groups(): + for group in self.operator_cloud.list_groups(): if group['name'].startswith(self.group_prefix): try: - self.cloud.delete_group(group['id']) + self.operator_cloud.delete_group(group['id']) except Exception as e: exception_list.append(str(e)) continue @@ -60,10 +58,10 @@ def _cleanup_groups(self): def _cleanup_users(self): exception_list = list() - for user in self.cloud.list_users(): + for user in self.operator_cloud.list_users(): if user['name'].startswith(self.user_prefix): try: - self.cloud.delete_user(user['id']) + self.operator_cloud.delete_user(user['id']) except Exception as e: exception_list.append(str(e)) continue @@ -73,10 +71,10 @@ def _cleanup_users(self): def _cleanup_roles(self): exception_list = list() - for role in self.cloud.list_roles(): + for role in self.operator_cloud.list_roles(): if role['name'].startswith(self.role_prefix): try: - self.cloud.delete_role(role['name']) + self.operator_cloud.delete_role(role['name']) except Exception as e: exception_list.append(str(e)) continue @@ -87,31 +85,31 @@ def _cleanup_roles(self): def _create_user(self, **kwargs): domain_id = None if self.identity_version not in ('2', '2.0'): - domain = self.cloud.get_domain('default') + domain = self.operator_cloud.get_domain('default') domain_id = domain['id'] - return self.cloud.create_user(domain_id=domain_id, **kwargs) + return self.operator_cloud.create_user(domain_id=domain_id, **kwargs) def test_list_roles(self): - roles = self.cloud.list_roles() + roles = self.operator_cloud.list_roles() self.assertIsNotNone(roles) self.assertNotEqual([], roles) def test_get_role(self): - role = self.cloud.get_role('admin') + role = self.operator_cloud.get_role('admin') self.assertIsNotNone(role) self.assertIn('id', role) self.assertIn('name', role) self.assertEqual('admin', role['name']) def test_search_roles(self): - roles = self.cloud.search_roles(filters={'name': 'admin'}) + roles = self.operator_cloud.search_roles(filters={'name': 'admin'}) self.assertIsNotNone(roles) self.assertEqual(1, len(roles)) self.assertEqual('admin', roles[0]['name']) def test_create_role(self): role_name = self.role_prefix + '_create_role' - role = self.cloud.create_role(role_name) + role = self.operator_cloud.create_role(role_name) self.assertIsNotNone(role) self.assertIn('id', role) self.assertIn('name', role) @@ -119,9 +117,9 @@ def test_create_role(self): def test_delete_role(self): role_name = self.role_prefix + '_delete_role' - role = self.cloud.create_role(role_name) + role = self.operator_cloud.create_role(role_name) self.assertIsNotNone(role) - self.assertTrue(self.cloud.delete_role(role_name)) + self.assertTrue(self.operator_cloud.delete_role(role_name)) # TODO(Shrews): Once we can support assigning roles within shade, we # need to make this test a little more specific, and add more for testing @@ -129,14 +127,14 @@ def test_delete_role(self): def test_list_role_assignments(self): if self.identity_version in ('2', '2.0'): self.skipTest("Identity service does not support role assignments") - assignments = self.cloud.list_role_assignments() + assignments = self.operator_cloud.list_role_assignments() self.assertIsInstance(assignments, list) self.assertTrue(len(assignments) > 0) def test_list_role_assignments_v2(self): - user = self.cloud.get_user('demo') - project = self.cloud.get_project('demo') - assignments = self.cloud.list_role_assignments( + user = self.operator_cloud.get_user('demo') + project = self.operator_cloud.get_project('demo') + assignments = self.operator_cloud.list_role_assignments( filters={'user': user['id'], 'project': project['id']}) self.assertIsInstance(assignments, list) self.assertTrue(len(assignments) > 0) @@ -145,25 +143,25 @@ def test_grant_revoke_role_user_project(self): user_name = self.user_prefix + '_user_project' user_email = 'nobody@nowhere.com' role_name = self.role_prefix + '_grant_user_project' - role = self.cloud.create_role(role_name) + role = self.operator_cloud.create_role(role_name) user = self._create_user(name=user_name, email=user_email, default_project='demo') - self.assertTrue(self.cloud.grant_role( + self.assertTrue(self.operator_cloud.grant_role( role_name, user=user['id'], project='demo', wait=True)) - assignments = self.cloud.list_role_assignments({ + assignments = self.operator_cloud.list_role_assignments({ 'role': role['id'], 'user': user['id'], - 'project': self.cloud.get_project('demo')['id'] + 'project': self.operator_cloud.get_project('demo')['id'] }) self.assertIsInstance(assignments, list) self.assertTrue(len(assignments) == 1) - self.assertTrue(self.cloud.revoke_role( + self.assertTrue(self.operator_cloud.revoke_role( role_name, user=user['id'], project='demo', wait=True)) - assignments = self.cloud.list_role_assignments({ + assignments = self.operator_cloud.list_role_assignments({ 'role': role['id'], 'user': user['id'], - 'project': self.cloud.get_project('demo')['id'] + 'project': self.operator_cloud.get_project('demo')['id'] }) self.assertIsInstance(assignments, list) self.assertTrue(len(assignments) == 0) @@ -172,26 +170,27 @@ def test_grant_revoke_role_group_project(self): if self.identity_version in ('2', '2.0'): self.skipTest("Identity service does not support group") role_name = self.role_prefix + '_grant_group_project' - role = self.cloud.create_role(role_name) + role = self.operator_cloud.create_role(role_name) group_name = self.group_prefix + '_group_project' - group = self.cloud.create_group(name=group_name, - description='test group', - domain='default') - self.assertTrue(self.cloud.grant_role( + group = self.operator_cloud.create_group( + name=group_name, + description='test group', + domain='default') + self.assertTrue(self.operator_cloud.grant_role( role_name, group=group['id'], project='demo')) - assignments = self.cloud.list_role_assignments({ + assignments = self.operator_cloud.list_role_assignments({ 'role': role['id'], 'group': group['id'], - 'project': self.cloud.get_project('demo')['id'] + 'project': self.operator_cloud.get_project('demo')['id'] }) self.assertIsInstance(assignments, list) self.assertTrue(len(assignments) == 1) - self.assertTrue(self.cloud.revoke_role( + self.assertTrue(self.operator_cloud.revoke_role( role_name, group=group['id'], project='demo')) - assignments = self.cloud.list_role_assignments({ + assignments = self.operator_cloud.list_role_assignments({ 'role': role['id'], 'group': group['id'], - 'project': self.cloud.get_project('demo')['id'] + 'project': self.operator_cloud.get_project('demo')['id'] }) self.assertIsInstance(assignments, list) self.assertTrue(len(assignments) == 0) @@ -200,27 +199,27 @@ def test_grant_revoke_role_user_domain(self): if self.identity_version in ('2', '2.0'): self.skipTest("Identity service does not support domain") role_name = self.role_prefix + '_grant_user_domain' - role = self.cloud.create_role(role_name) + role = self.operator_cloud.create_role(role_name) user_name = self.user_prefix + '_user_domain' user_email = 'nobody@nowhere.com' user = self._create_user(name=user_name, email=user_email, default_project='demo') - self.assertTrue(self.cloud.grant_role( + self.assertTrue(self.operator_cloud.grant_role( role_name, user=user['id'], domain='default')) - assignments = self.cloud.list_role_assignments({ + assignments = self.operator_cloud.list_role_assignments({ 'role': role['id'], 'user': user['id'], - 'domain': self.cloud.get_domain('default')['id'] + 'domain': self.operator_cloud.get_domain('default')['id'] }) self.assertIsInstance(assignments, list) self.assertTrue(len(assignments) == 1) - self.assertTrue(self.cloud.revoke_role( + self.assertTrue(self.operator_cloud.revoke_role( role_name, user=user['id'], domain='default')) - assignments = self.cloud.list_role_assignments({ + assignments = self.operator_cloud.list_role_assignments({ 'role': role['id'], 'user': user['id'], - 'domain': self.cloud.get_domain('default')['id'] + 'domain': self.operator_cloud.get_domain('default')['id'] }) self.assertIsInstance(assignments, list) self.assertTrue(len(assignments) == 0) @@ -229,26 +228,27 @@ def test_grant_revoke_role_group_domain(self): if self.identity_version in ('2', '2.0'): self.skipTest("Identity service does not support domain or group") role_name = self.role_prefix + '_grant_group_domain' - role = self.cloud.create_role(role_name) + role = self.operator_cloud.create_role(role_name) group_name = self.group_prefix + '_group_domain' - group = self.cloud.create_group(name=group_name, - description='test group', - domain='default') - self.assertTrue(self.cloud.grant_role( + group = self.operator_cloud.create_group( + name=group_name, + description='test group', + domain='default') + self.assertTrue(self.operator_cloud.grant_role( role_name, group=group['id'], domain='default')) - assignments = self.cloud.list_role_assignments({ + assignments = self.operator_cloud.list_role_assignments({ 'role': role['id'], 'group': group['id'], - 'domain': self.cloud.get_domain('default')['id'] + 'domain': self.operator_cloud.get_domain('default')['id'] }) self.assertIsInstance(assignments, list) self.assertTrue(len(assignments) == 1) - self.assertTrue(self.cloud.revoke_role( + self.assertTrue(self.operator_cloud.revoke_role( role_name, group=group['id'], domain='default')) - assignments = self.cloud.list_role_assignments({ + assignments = self.operator_cloud.list_role_assignments({ 'role': role['id'], 'group': group['id'], - 'domain': self.cloud.get_domain('default')['id'] + 'domain': self.operator_cloud.get_domain('default')['id'] }) self.assertIsInstance(assignments, list) self.assertTrue(len(assignments) == 0) diff --git a/shade/tests/functional/test_quotas.py b/shade/tests/functional/test_quotas.py index b99e73ea2..3f32a2795 100644 --- a/shade/tests/functional/test_quotas.py +++ b/shade/tests/functional/test_quotas.py @@ -19,66 +19,61 @@ Functional tests for `shade` quotas methods. """ -from shade import operator_cloud -from shade.tests import base +from shade.tests.functional import base -class TestComputeQuotas(base.TestCase): - - def setUp(self): - super(TestComputeQuotas, self).setUp() - self.cloud = operator_cloud(cloud='devstack-admin') - if not self.cloud.has_service('compute'): - self.skipTest('compute service not supported by cloud') +class TestComputeQuotas(base.BaseFunctionalTestCase): def test_quotas(self): '''Test quotas functionality''' - quotas = self.cloud.get_compute_quotas('demo') + quotas = self.operator_cloud.get_compute_quotas('demo') cores = quotas['cores'] - self.cloud.set_compute_quotas('demo', cores=cores + 1) - self.assertEqual(cores + 1, - self.cloud.get_compute_quotas('demo')['cores']) - self.cloud.delete_compute_quotas('demo') - self.assertEqual(cores, self.cloud.get_compute_quotas('demo')['cores']) + self.operator_cloud.set_compute_quotas('demo', cores=cores + 1) + self.assertEqual( + cores + 1, + self.operator_cloud.get_compute_quotas('demo')['cores']) + self.operator_cloud.delete_compute_quotas('demo') + self.assertEqual( + cores, self.operator_cloud.get_compute_quotas('demo')['cores']) -class TestVolumeQuotas(base.TestCase): +class TestVolumeQuotas(base.BaseFunctionalTestCase): def setUp(self): super(TestVolumeQuotas, self).setUp() - self.cloud = operator_cloud(cloud='devstack-admin') - if not self.cloud.has_service('volume'): + if not self.operator_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') def test_quotas(self): '''Test quotas functionality''' - quotas = self.cloud.get_volume_quotas('demo') + quotas = self.operator_cloud.get_volume_quotas('demo') volumes = quotas['volumes'] - self.cloud.set_volume_quotas('demo', volumes=volumes + 1) - self.assertEqual(volumes + 1, - self.cloud.get_volume_quotas('demo')['volumes']) - self.cloud.delete_volume_quotas('demo') - self.assertEqual(volumes, - self.cloud.get_volume_quotas('demo')['volumes']) + self.operator_cloud.set_volume_quotas('demo', volumes=volumes + 1) + self.assertEqual( + volumes + 1, + self.operator_cloud.get_volume_quotas('demo')['volumes']) + self.operator_cloud.delete_volume_quotas('demo') + self.assertEqual( + volumes, + self.operator_cloud.get_volume_quotas('demo')['volumes']) -class TestNetworkQuotas(base.TestCase): +class TestNetworkQuotas(base.BaseFunctionalTestCase): def setUp(self): super(TestNetworkQuotas, self).setUp() - self.cloud = operator_cloud(cloud='devstack-admin') - if not self.cloud.has_service('network'): + if not self.operator_cloud.has_service('network'): self.skipTest('network service not supported by cloud') def test_quotas(self): '''Test quotas functionality''' - quotas = self.cloud.get_network_quotas('demo') + quotas = self.operator_cloud.get_network_quotas('demo') network = quotas['network'] - self.cloud.set_network_quotas('demo', network=network + 1) - self.assertEqual(network + 1, - self.cloud.get_network_quotas('demo')['network'] - ) - self.cloud.delete_network_quotas('demo') - self.assertEqual(network, - self.cloud.get_network_quotas('demo')['network'] - ) + self.operator_cloud.set_network_quotas('demo', network=network + 1) + self.assertEqual( + network + 1, + self.operator_cloud.get_network_quotas('demo')['network']) + self.operator_cloud.delete_network_quotas('demo') + self.assertEqual( + network, + self.operator_cloud.get_network_quotas('demo')['network']) diff --git a/shade/tests/functional/test_stack.py b/shade/tests/functional/test_stack.py index a0e649b86..c06708f5c 100644 --- a/shade/tests/functional/test_stack.py +++ b/shade/tests/functional/test_stack.py @@ -22,8 +22,7 @@ import tempfile from shade import exc -from shade import openstack_cloud -from shade.tests import base +from shade.tests.functional import base simple_template = '''heat_template_version: 2014-10-16 parameters: @@ -74,17 +73,16 @@ validate_template = '''heat_template_version: asdf-no-such-version ''' -class TestStack(base.TestCase): +class TestStack(base.BaseFunctionalTestCase): def setUp(self): super(TestStack, self).setUp() - self.cloud = openstack_cloud(cloud='devstack') - if not self.cloud.has_service('orchestration'): + if not self.demo_cloud.has_service('orchestration'): self.skipTest('Orchestration service not supported by cloud') def _cleanup_stack(self): - self.cloud.delete_stack(self.stack_name, wait=True) - self.assertIsNone(self.cloud.get_stack(self.stack_name)) + self.demo_cloud.delete_stack(self.stack_name, wait=True) + self.assertIsNone(self.demo_cloud.get_stack(self.stack_name)) def test_stack_validation(self): test_template = tempfile.NamedTemporaryFile(delete=False) @@ -92,7 +90,7 @@ def test_stack_validation(self): test_template.close() stack_name = self.getUniqueString('validate_template') self.assertRaises(exc.OpenStackCloudException, - self.cloud.create_stack, + self.demo_cloud.create_stack, name=stack_name, template_file=test_template.name) @@ -102,9 +100,10 @@ def test_stack_simple(self): test_template.close() self.stack_name = self.getUniqueString('simple_stack') self.addCleanup(self._cleanup_stack) - stack = self.cloud.create_stack(name=self.stack_name, - template_file=test_template.name, - wait=True) + stack = self.demo_cloud.create_stack( + name=self.stack_name, + template_file=test_template.name, + wait=True) # assert expected values in stack self.assertEqual('CREATE_COMPLETE', stack['stack_status']) @@ -112,19 +111,20 @@ def test_stack_simple(self): self.assertEqual(10, len(rand)) # assert get_stack matches returned create_stack - stack = self.cloud.get_stack(self.stack_name) + stack = self.demo_cloud.get_stack(self.stack_name) self.assertEqual('CREATE_COMPLETE', stack['stack_status']) self.assertEqual(rand, stack['outputs'][0]['output_value']) # assert stack is in list_stacks - stacks = self.cloud.list_stacks() + stacks = self.demo_cloud.list_stacks() stack_ids = [s['id'] for s in stacks] self.assertIn(stack['id'], stack_ids) # update with no changes - stack = self.cloud.update_stack(self.stack_name, - template_file=test_template.name, - wait=True) + stack = self.demo_cloud.update_stack( + self.stack_name, + template_file=test_template.name, + wait=True) # assert no change in updated stack self.assertEqual('UPDATE_COMPLETE', stack['stack_status']) @@ -132,13 +132,14 @@ def test_stack_simple(self): self.assertEqual(rand, stack['outputs'][0]['output_value']) # update with changes - stack = self.cloud.update_stack(self.stack_name, - template_file=test_template.name, - wait=True, - length=12) + stack = self.demo_cloud.update_stack( + self.stack_name, + template_file=test_template.name, + wait=True, + length=12) # assert changed output in updated stack - stack = self.cloud.get_stack(self.stack_name) + stack = self.demo_cloud.get_stack(self.stack_name) self.assertEqual('UPDATE_COMPLETE', stack['stack_status']) new_rand = stack['outputs'][0]['output_value'] self.assertNotEqual(rand, new_rand) @@ -161,10 +162,11 @@ def test_stack_nested(self): self.stack_name = self.getUniqueString('nested_stack') self.addCleanup(self._cleanup_stack) - stack = self.cloud.create_stack(name=self.stack_name, - template_file=test_template.name, - environment_files=[env.name], - wait=True) + stack = self.demo_cloud.create_stack( + name=self.stack_name, + template_file=test_template.name, + environment_files=[env.name], + wait=True) # assert expected values in stack self.assertEqual('CREATE_COMPLETE', stack['stack_status']) From bbd22abbe3a8527daa11fe0011c58036ab9d5a28 Mon Sep 17 00:00:00 2001 From: Yolanda Robla Date: Wed, 6 Apr 2016 13:38:21 +0200 Subject: [PATCH 0969/3836] Add new test with betamax for create flavors Change-Id: I0aaceb7633a27356e8eaabf70cadfe5e258cedee --- requirements.txt | 2 +- .../unit/fixtures/test_create_flavor.yaml | 2105 +++++++++++++++++ shade/tests/unit/test_create_server.py | 16 +- shade/tests/unit/test_flavors.py | 36 +- 4 files changed, 2143 insertions(+), 16 deletions(-) create mode 100644 shade/tests/unit/fixtures/test_create_flavor.yaml diff --git a/requirements.txt b/requirements.txt index 2b246196a..1a3b1915f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ os-client-config>=1.17.0 requestsexceptions>=1.1.1 six -keystoneauth1>=2.8.0 +keystoneauth1>=2.11.0 netifaces>=0.10.4 python-novaclient>=2.21.0,!=2.27.0,!=2.32.0 python-keystoneclient>=0.11.0 diff --git a/shade/tests/unit/fixtures/test_create_flavor.yaml b/shade/tests/unit/fixtures/test_create_flavor.yaml new file mode 100644 index 000000000..d83ac7268 --- /dev/null +++ b/shade/tests/unit/fixtures/test_create_flavor.yaml @@ -0,0 +1,2105 @@ +http_interactions: +- recorded_at: '2016-04-14T10:09:57' + request: + body: + encoding: utf-8 + string: '' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 + method: GET + uri: http://192.168.0.19:35357/ + response: + body: + encoding: null + string: |- + { + "versions": { + "values": [ + { + "status": "stable", + "updated": "2016-04-04T00:00:00Z", + "media-types": [ + { + "base": "application/json", + "type": "application/vnd.openstack.identity-v3+json" + } + ], + "id": "v3.6", + "links": [ + { + "href": "http://192.168.0.19:35357/v3/", + "rel": "self" + } + ] + }, + { + "status": "stable", + "updated": "2014-04-17T00:00:00Z", + "media-types": [ + { + "base": "application/json", + "type": "application/vnd.openstack.identity-v2.0+json" + } + ], + "id": "v2.0", + "links": [ + { + "href": "http://192.168.0.19:35357/v2.0/", + "rel": "self" + }, + { + "href": "http://docs.openstack.org/", + "type": "text/html", + "rel": "describedby" + } + ] + } + ] + } + } + headers: + Connection: + - Keep-Alive + Content-Length: + - '595' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:09:57 GMT + Keep-Alive: + - timeout=5, max=100 + Server: + - Apache/2.4.7 (Ubuntu) + Vary: + - X-Auth-Token + status: + code: 300 + message: Multiple Choices + url: http://192.168.0.19:35357/ +- recorded_at: '2016-04-14T10:09:58' + request: + body: + encoding: utf-8 + string: |- + { + "auth": { + "tenantName": "dummy", + "passwordCredentials": { + "username": "dummy", + "password": "********" + } + } + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '103' + Content-Type: + - application/json + User-Agent: + - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 + method: POST + uri: http://192.168.0.19:35357/v2.0/tokens + response: + body: + encoding: null + string: |- + { + "access": { + "token": { + "issued_at": "2016-04-14T10:09:58.014014Z", + "expires": "9999-12-31T23:59:59Z", + "id": "7fa3037ae2fe48ada8c626a51dc01ffd", + "tenant": { + "enabled": true, + "description": "Bootstrap project for initializing the cloud.", + "name": "admin", + "id": "1c36b64c840a42cd9e9b931a369337f0" + }, + "audit_ids": [ + "FgG3Q8T3Sh21r_7HyjHP8A" + ] + }, + "serviceCatalog": [ + { + "endpoints_links": [], + "endpoints": [ + { + "adminURL": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0", + "region": "RegionOne", + "publicURL": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0", + "internalURL": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0", + "id": "32466f357f3545248c47471ca51b0d3a" + } + ], + "type": "compute", + "name": "nova" + }, + { + "endpoints_links": [], + "endpoints": [ + { + "adminURL": "http://192.168.0.19:8776/v2/1c36b64c840a42cd9e9b931a369337f0", + "region": "RegionOne", + "publicURL": "http://192.168.0.19:8776/v2/1c36b64c840a42cd9e9b931a369337f0", + "internalURL": "http://192.168.0.19:8776/v2/1c36b64c840a42cd9e9b931a369337f0", + "id": "1e875ca2225b408bbf3520a1b8e1a537" + } + ], + "type": "volumev2", + "name": "cinderv2" + }, + { + "endpoints_links": [], + "endpoints": [ + { + "adminURL": "http://192.168.0.19:9292", + "region": "RegionOne", + "publicURL": "http://192.168.0.19:9292", + "internalURL": "http://192.168.0.19:9292", + "id": "5a64de3c4a614d8d8f8d1ba3dee5f45f" + } + ], + "type": "image", + "name": "glance" + }, + { + "endpoints_links": [], + "endpoints": [ + { + "adminURL": "http://192.168.0.19:8774/v2/1c36b64c840a42cd9e9b931a369337f0", + "region": "RegionOne", + "publicURL": "http://192.168.0.19:8774/v2/1c36b64c840a42cd9e9b931a369337f0", + "internalURL": "http://192.168.0.19:8774/v2/1c36b64c840a42cd9e9b931a369337f0", + "id": "74ba16ca98154dfface2af92e97062a7" + } + ], + "type": "compute_legacy", + "name": "nova_legacy" + }, + { + "endpoints_links": [], + "endpoints": [ + { + "adminURL": "http://192.168.0.19:8776/v1/1c36b64c840a42cd9e9b931a369337f0", + "region": "RegionOne", + "publicURL": "http://192.168.0.19:8776/v1/1c36b64c840a42cd9e9b931a369337f0", + "internalURL": "http://192.168.0.19:8776/v1/1c36b64c840a42cd9e9b931a369337f0", + "id": "3d15fdfc7d424f3c8923324417e1a3d1" + } + ], + "type": "volume", + "name": "cinder" + }, + { + "endpoints_links": [], + "endpoints": [ + { + "adminURL": "http://192.168.0.19:35357/v2.0", + "region": "RegionOne", + "publicURL": "http://192.168.0.19:5000/v2.0", + "internalURL": "http://192.168.0.19:5000/v2.0", + "id": "4deb4d0504a044a395d4480741ba628c" + } + ], + "type": "identity", + "name": "keystone" + } + ], + "user": { + "username": "dummy", + "roles_links": [], + "id": "71675f719c3343e8ac441cc28f396474", + "roles": [ + { + "name": "admin" + } + ], + "name": "admin" + }, + "metadata": { + "is_admin": 0, + "roles": [ + "6d813db50b6e4a1ababdbbb5a83c7de5" + ] + } + } + } + headers: + Connection: + - Keep-Alive + Content-Length: + - '2647' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:09:57 GMT + Keep-Alive: + - timeout=5, max=99 + Server: + - Apache/2.4.7 (Ubuntu) + Vary: + - X-Auth-Token + x-openstack-request-id: + - req-b60e1abb-6819-41c7-8851-e22adf92158e + status: + code: 200 + message: OK + url: http://192.168.0.19:35357/v2.0/tokens +- recorded_at: '2016-04-14T10:09:58' + request: + body: + encoding: utf-8 + string: '' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-novaclient + X-Auth-Token: + - 7fa3037ae2fe48ada8c626a51dc01ffd + method: GET + uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/detail?is_public=None + response: + body: + encoding: null + string: |- + { + "flavors": [ + { + "name": "m1.tiny", + "links": [ + { + "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/1", + "rel": "self" + }, + { + "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/1", + "rel": "bookmark" + } + ], + "ram": 512, + "OS-FLV-DISABLED:disabled": false, + "vcpus": 1, + "swap": "", + "os-flavor-access:is_public": true, + "rxtx_factor": 1.0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "disk": 1, + "id": "1" + }, + { + "name": "m1.small", + "links": [ + { + "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/2", + "rel": "self" + }, + { + "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/2", + "rel": "bookmark" + } + ], + "ram": 2048, + "OS-FLV-DISABLED:disabled": false, + "vcpus": 1, + "swap": "", + "os-flavor-access:is_public": true, + "rxtx_factor": 1.0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "disk": 20, + "id": "2" + }, + { + "name": "m1.medium", + "links": [ + { + "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/3", + "rel": "self" + }, + { + "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/3", + "rel": "bookmark" + } + ], + "ram": 4096, + "OS-FLV-DISABLED:disabled": false, + "vcpus": 2, + "swap": "", + "os-flavor-access:is_public": true, + "rxtx_factor": 1.0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "disk": 40, + "id": "3" + }, + { + "name": "m1.large", + "links": [ + { + "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/4", + "rel": "self" + }, + { + "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/4", + "rel": "bookmark" + } + ], + "ram": 8192, + "OS-FLV-DISABLED:disabled": false, + "vcpus": 4, + "swap": "", + "os-flavor-access:is_public": true, + "rxtx_factor": 1.0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "disk": 80, + "id": "4" + }, + { + "name": "m1.nano", + "links": [ + { + "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/42", + "rel": "self" + }, + { + "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/42", + "rel": "bookmark" + } + ], + "ram": 64, + "OS-FLV-DISABLED:disabled": false, + "vcpus": 1, + "swap": "", + "os-flavor-access:is_public": true, + "rxtx_factor": 1.0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "disk": 0, + "id": "42" + }, + { + "name": "m1.xlarge", + "links": [ + { + "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/5", + "rel": "self" + }, + { + "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/5", + "rel": "bookmark" + } + ], + "ram": 16384, + "OS-FLV-DISABLED:disabled": false, + "vcpus": 8, + "swap": "", + "os-flavor-access:is_public": true, + "rxtx_factor": 1.0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "disk": 160, + "id": "5" + }, + { + "name": "m1.micro", + "links": [ + { + "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/84", + "rel": "self" + }, + { + "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/84", + "rel": "bookmark" + } + ], + "ram": 128, + "OS-FLV-DISABLED:disabled": false, + "vcpus": 1, + "swap": "", + "os-flavor-access:is_public": true, + "rxtx_factor": 1.0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "disk": 0, + "id": "84" + } + ] + } + headers: + Connection: + - keep-alive + Content-Length: + - '2933' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:09:58 GMT + Vary: + - X-OpenStack-Nova-API-Version + X-Compute-Request-Id: + - req-2c57220f-2462-4e60-9561-6d192762f087 + X-Openstack-Nova-Api-Version: + - '2.1' + status: + code: 200 + message: OK + url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/detail?is_public=None +- recorded_at: '2016-04-14T10:09:58' + request: + body: + encoding: utf-8 + string: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 + X-Auth-Token: + - 7fa3037ae2fe48ada8c626a51dc01ffd + method: GET + uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/1/os-extra_specs + response: + body: + encoding: null + string: |- + { + "extra_specs": {} + } + headers: + Connection: + - keep-alive + Content-Length: + - '19' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:09:58 GMT + Vary: + - X-OpenStack-Nova-API-Version + X-Compute-Request-Id: + - req-c2c93548-8671-49ff-9e24-50e7f2f5115a + X-Openstack-Nova-Api-Version: + - '2.1' + status: + code: 200 + message: OK + url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/1/os-extra_specs +- recorded_at: '2016-04-14T10:09:58' + request: + body: + encoding: utf-8 + string: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 + X-Auth-Token: + - 7fa3037ae2fe48ada8c626a51dc01ffd + method: GET + uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/2/os-extra_specs + response: + body: + encoding: null + string: |- + { + "extra_specs": {} + } + headers: + Connection: + - keep-alive + Content-Length: + - '19' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:09:58 GMT + Vary: + - X-OpenStack-Nova-API-Version + X-Compute-Request-Id: + - req-7c377ee6-689a-484a-aee5-691cd97211a6 + X-Openstack-Nova-Api-Version: + - '2.1' + status: + code: 200 + message: OK + url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/2/os-extra_specs +- recorded_at: '2016-04-14T10:09:58' + request: + body: + encoding: utf-8 + string: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 + X-Auth-Token: + - 7fa3037ae2fe48ada8c626a51dc01ffd + method: GET + uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/3/os-extra_specs + response: + body: + encoding: null + string: |- + { + "extra_specs": {} + } + headers: + Connection: + - keep-alive + Content-Length: + - '19' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:09:58 GMT + Vary: + - X-OpenStack-Nova-API-Version + X-Compute-Request-Id: + - req-fa02df4b-b312-46f6-a945-bd5d2f324fa4 + X-Openstack-Nova-Api-Version: + - '2.1' + status: + code: 200 + message: OK + url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/3/os-extra_specs +- recorded_at: '2016-04-14T10:09:59' + request: + body: + encoding: utf-8 + string: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 + X-Auth-Token: + - 7fa3037ae2fe48ada8c626a51dc01ffd + method: GET + uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/4/os-extra_specs + response: + body: + encoding: null + string: |- + { + "extra_specs": {} + } + headers: + Connection: + - keep-alive + Content-Length: + - '19' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:09:59 GMT + Vary: + - X-OpenStack-Nova-API-Version + X-Compute-Request-Id: + - req-0be39be0-2c60-4b0b-879c-a989ce48e68f + X-Openstack-Nova-Api-Version: + - '2.1' + status: + code: 200 + message: OK + url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/4/os-extra_specs +- recorded_at: '2016-04-14T10:09:59' + request: + body: + encoding: utf-8 + string: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 + X-Auth-Token: + - 7fa3037ae2fe48ada8c626a51dc01ffd + method: GET + uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/42/os-extra_specs + response: + body: + encoding: null + string: |- + { + "extra_specs": {} + } + headers: + Connection: + - keep-alive + Content-Length: + - '19' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:09:59 GMT + Vary: + - X-OpenStack-Nova-API-Version + X-Compute-Request-Id: + - req-87a26e5f-c9f2-4ee7-9f9c-a4371ba1da8a + X-Openstack-Nova-Api-Version: + - '2.1' + status: + code: 200 + message: OK + url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/42/os-extra_specs +- recorded_at: '2016-04-14T10:09:59' + request: + body: + encoding: utf-8 + string: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 + X-Auth-Token: + - 7fa3037ae2fe48ada8c626a51dc01ffd + method: GET + uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/5/os-extra_specs + response: + body: + encoding: null + string: |- + { + "extra_specs": {} + } + headers: + Connection: + - keep-alive + Content-Length: + - '19' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:09:59 GMT + Vary: + - X-OpenStack-Nova-API-Version + X-Compute-Request-Id: + - req-466cd9ac-fc04-431e-88b5-9951c8859edb + X-Openstack-Nova-Api-Version: + - '2.1' + status: + code: 200 + message: OK + url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/5/os-extra_specs +- recorded_at: '2016-04-14T10:09:59' + request: + body: + encoding: utf-8 + string: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 + X-Auth-Token: + - 7fa3037ae2fe48ada8c626a51dc01ffd + method: GET + uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/84/os-extra_specs + response: + body: + encoding: null + string: |- + { + "extra_specs": {} + } + headers: + Connection: + - keep-alive + Content-Length: + - '19' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:09:59 GMT + Vary: + - X-OpenStack-Nova-API-Version + X-Compute-Request-Id: + - req-75179ff1-71b6-4635-a0d2-f6b72ac0526f + X-Openstack-Nova-Api-Version: + - '2.1' + status: + code: 200 + message: OK + url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/84/os-extra_specs +- recorded_at: '2016-04-14T10:09:59' + request: + body: + encoding: utf-8 + string: |- + { + "flavor": { + "name": "vanilla", + "ram": 12345, + "vcpus": 4, + "swap": 0, + "os-flavor-access:is_public": true, + "rxtx_factor": 1.0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "disk": 100, + "id": null + } + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '181' + Content-Type: + - application/json + User-Agent: + - python-novaclient + X-Auth-Token: + - 7fa3037ae2fe48ada8c626a51dc01ffd + method: POST + uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors + response: + body: + encoding: null + string: |- + { + "flavor": { + "name": "vanilla", + "links": [ + { + "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30", + "rel": "self" + }, + { + "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30", + "rel": "bookmark" + } + ], + "ram": 12345, + "OS-FLV-DISABLED:disabled": false, + "vcpus": 4, + "swap": "", + "os-flavor-access:is_public": true, + "rxtx_factor": 1.0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "disk": 100, + "id": "8987fc39-f5b8-46ce-bf1e-816e01623b30" + } + } + headers: + Connection: + - keep-alive + Content-Length: + - '533' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:09:59 GMT + Vary: + - X-OpenStack-Nova-API-Version + X-Compute-Request-Id: + - req-a47adf29-b5cc-45af-812b-db35ec6f22ac + X-Openstack-Nova-Api-Version: + - '2.1' + status: + code: 200 + message: OK + url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors +- recorded_at: '2016-04-14T10:09:59' + request: + body: + encoding: utf-8 + string: '' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-novaclient + X-Auth-Token: + - 7fa3037ae2fe48ada8c626a51dc01ffd + method: GET + uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30 + response: + body: + encoding: null + string: |- + { + "flavor": { + "name": "vanilla", + "links": [ + { + "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30", + "rel": "self" + }, + { + "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30", + "rel": "bookmark" + } + ], + "ram": 12345, + "OS-FLV-DISABLED:disabled": false, + "vcpus": 4, + "swap": "", + "os-flavor-access:is_public": true, + "rxtx_factor": 1.0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "disk": 100, + "id": "8987fc39-f5b8-46ce-bf1e-816e01623b30" + } + } + headers: + Connection: + - keep-alive + Content-Length: + - '533' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:09:59 GMT + Vary: + - X-OpenStack-Nova-API-Version + X-Compute-Request-Id: + - req-42ab9a08-608e-4f76-bd26-0a2d6e650099 + X-Openstack-Nova-Api-Version: + - '2.1' + status: + code: 200 + message: OK + url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30 +- recorded_at: '2016-04-14T10:10:00' + request: + body: + encoding: utf-8 + string: '' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-novaclient + X-Auth-Token: + - 7fa3037ae2fe48ada8c626a51dc01ffd + method: GET + uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/detail?is_public=None + response: + body: + encoding: null + string: |- + { + "flavors": [ + { + "name": "m1.tiny", + "links": [ + { + "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/1", + "rel": "self" + }, + { + "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/1", + "rel": "bookmark" + } + ], + "ram": 512, + "OS-FLV-DISABLED:disabled": false, + "vcpus": 1, + "swap": "", + "os-flavor-access:is_public": true, + "rxtx_factor": 1.0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "disk": 1, + "id": "1" + }, + { + "name": "m1.small", + "links": [ + { + "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/2", + "rel": "self" + }, + { + "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/2", + "rel": "bookmark" + } + ], + "ram": 2048, + "OS-FLV-DISABLED:disabled": false, + "vcpus": 1, + "swap": "", + "os-flavor-access:is_public": true, + "rxtx_factor": 1.0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "disk": 20, + "id": "2" + }, + { + "name": "m1.medium", + "links": [ + { + "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/3", + "rel": "self" + }, + { + "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/3", + "rel": "bookmark" + } + ], + "ram": 4096, + "OS-FLV-DISABLED:disabled": false, + "vcpus": 2, + "swap": "", + "os-flavor-access:is_public": true, + "rxtx_factor": 1.0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "disk": 40, + "id": "3" + }, + { + "name": "m1.large", + "links": [ + { + "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/4", + "rel": "self" + }, + { + "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/4", + "rel": "bookmark" + } + ], + "ram": 8192, + "OS-FLV-DISABLED:disabled": false, + "vcpus": 4, + "swap": "", + "os-flavor-access:is_public": true, + "rxtx_factor": 1.0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "disk": 80, + "id": "4" + }, + { + "name": "m1.nano", + "links": [ + { + "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/42", + "rel": "self" + }, + { + "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/42", + "rel": "bookmark" + } + ], + "ram": 64, + "OS-FLV-DISABLED:disabled": false, + "vcpus": 1, + "swap": "", + "os-flavor-access:is_public": true, + "rxtx_factor": 1.0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "disk": 0, + "id": "42" + }, + { + "name": "m1.xlarge", + "links": [ + { + "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/5", + "rel": "self" + }, + { + "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/5", + "rel": "bookmark" + } + ], + "ram": 16384, + "OS-FLV-DISABLED:disabled": false, + "vcpus": 8, + "swap": "", + "os-flavor-access:is_public": true, + "rxtx_factor": 1.0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "disk": 160, + "id": "5" + }, + { + "name": "m1.micro", + "links": [ + { + "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/84", + "rel": "self" + }, + { + "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/84", + "rel": "bookmark" + } + ], + "ram": 128, + "OS-FLV-DISABLED:disabled": false, + "vcpus": 1, + "swap": "", + "os-flavor-access:is_public": true, + "rxtx_factor": 1.0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "disk": 0, + "id": "84" + }, + { + "name": "vanilla", + "links": [ + { + "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30", + "rel": "self" + }, + { + "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30", + "rel": "bookmark" + } + ], + "ram": 12345, + "OS-FLV-DISABLED:disabled": false, + "vcpus": 4, + "swap": "", + "os-flavor-access:is_public": true, + "rxtx_factor": 1.0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "disk": 100, + "id": "8987fc39-f5b8-46ce-bf1e-816e01623b30" + } + ] + } + headers: + Connection: + - keep-alive + Content-Length: + - '3456' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:10:00 GMT + Vary: + - X-OpenStack-Nova-API-Version + X-Compute-Request-Id: + - req-87e35330-ca43-43d7-b42a-422bccca8b94 + X-Openstack-Nova-Api-Version: + - '2.1' + status: + code: 200 + message: OK + url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/detail?is_public=None +- recorded_at: '2016-04-14T10:10:00' + request: + body: + encoding: utf-8 + string: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 + X-Auth-Token: + - 7fa3037ae2fe48ada8c626a51dc01ffd + method: GET + uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/1/os-extra_specs + response: + body: + encoding: null + string: |- + { + "extra_specs": {} + } + headers: + Connection: + - keep-alive + Content-Length: + - '19' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:10:00 GMT + Vary: + - X-OpenStack-Nova-API-Version + X-Compute-Request-Id: + - req-4013b982-885e-4225-bf62-7ecd78d81465 + X-Openstack-Nova-Api-Version: + - '2.1' + status: + code: 200 + message: OK + url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/1/os-extra_specs +- recorded_at: '2016-04-14T10:10:00' + request: + body: + encoding: utf-8 + string: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 + X-Auth-Token: + - 7fa3037ae2fe48ada8c626a51dc01ffd + method: GET + uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/2/os-extra_specs + response: + body: + encoding: null + string: |- + { + "extra_specs": {} + } + headers: + Connection: + - keep-alive + Content-Length: + - '19' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:10:00 GMT + Vary: + - X-OpenStack-Nova-API-Version + X-Compute-Request-Id: + - req-748fb97b-73e4-4857-b2f9-d32ae868097d + X-Openstack-Nova-Api-Version: + - '2.1' + status: + code: 200 + message: OK + url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/2/os-extra_specs +- recorded_at: '2016-04-14T10:10:00' + request: + body: + encoding: utf-8 + string: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 + X-Auth-Token: + - 7fa3037ae2fe48ada8c626a51dc01ffd + method: GET + uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/3/os-extra_specs + response: + body: + encoding: null + string: |- + { + "extra_specs": {} + } + headers: + Connection: + - keep-alive + Content-Length: + - '19' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:10:00 GMT + Vary: + - X-OpenStack-Nova-API-Version + X-Compute-Request-Id: + - req-e7dde10b-483d-4d15-b608-2093dd10c849 + X-Openstack-Nova-Api-Version: + - '2.1' + status: + code: 200 + message: OK + url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/3/os-extra_specs +- recorded_at: '2016-04-14T10:10:00' + request: + body: + encoding: utf-8 + string: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 + X-Auth-Token: + - 7fa3037ae2fe48ada8c626a51dc01ffd + method: GET + uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/4/os-extra_specs + response: + body: + encoding: null + string: |- + { + "extra_specs": {} + } + headers: + Connection: + - keep-alive + Content-Length: + - '19' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:10:00 GMT + Vary: + - X-OpenStack-Nova-API-Version + X-Compute-Request-Id: + - req-7dcb269b-c961-43e5-ac4e-69237edcf6d8 + X-Openstack-Nova-Api-Version: + - '2.1' + status: + code: 200 + message: OK + url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/4/os-extra_specs +- recorded_at: '2016-04-14T10:10:01' + request: + body: + encoding: utf-8 + string: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 + X-Auth-Token: + - 7fa3037ae2fe48ada8c626a51dc01ffd + method: GET + uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/42/os-extra_specs + response: + body: + encoding: null + string: |- + { + "extra_specs": {} + } + headers: + Connection: + - keep-alive + Content-Length: + - '19' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:10:01 GMT + Vary: + - X-OpenStack-Nova-API-Version + X-Compute-Request-Id: + - req-4b048baf-1a27-427f-9771-974288273a95 + X-Openstack-Nova-Api-Version: + - '2.1' + status: + code: 200 + message: OK + url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/42/os-extra_specs +- recorded_at: '2016-04-14T10:10:01' + request: + body: + encoding: utf-8 + string: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 + X-Auth-Token: + - 7fa3037ae2fe48ada8c626a51dc01ffd + method: GET + uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/5/os-extra_specs + response: + body: + encoding: null + string: |- + { + "extra_specs": {} + } + headers: + Connection: + - keep-alive + Content-Length: + - '19' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:10:01 GMT + Vary: + - X-OpenStack-Nova-API-Version + X-Compute-Request-Id: + - req-846cd913-191f-4714-9d1b-6a2e0015b538 + X-Openstack-Nova-Api-Version: + - '2.1' + status: + code: 200 + message: OK + url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/5/os-extra_specs +- recorded_at: '2016-04-14T10:10:01' + request: + body: + encoding: utf-8 + string: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 + X-Auth-Token: + - 7fa3037ae2fe48ada8c626a51dc01ffd + method: GET + uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/84/os-extra_specs + response: + body: + encoding: null + string: |- + { + "extra_specs": {} + } + headers: + Connection: + - keep-alive + Content-Length: + - '19' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:10:01 GMT + Vary: + - X-OpenStack-Nova-API-Version + X-Compute-Request-Id: + - req-a3b21f98-51b8-472c-9ff0-1376beff1498 + X-Openstack-Nova-Api-Version: + - '2.1' + status: + code: 200 + message: OK + url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/84/os-extra_specs +- recorded_at: '2016-04-14T10:10:01' + request: + body: + encoding: utf-8 + string: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 + X-Auth-Token: + - 7fa3037ae2fe48ada8c626a51dc01ffd + method: GET + uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30/os-extra_specs + response: + body: + encoding: null + string: |- + { + "extra_specs": {} + } + headers: + Connection: + - keep-alive + Content-Length: + - '19' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:10:01 GMT + Vary: + - X-OpenStack-Nova-API-Version + X-Compute-Request-Id: + - req-14cc8bb9-27cc-48ba-9b42-c31d1322b8a7 + X-Openstack-Nova-Api-Version: + - '2.1' + status: + code: 200 + message: OK + url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30/os-extra_specs +- recorded_at: '2016-04-14T10:10:01' + request: + body: + encoding: utf-8 + string: '' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-novaclient + X-Auth-Token: + - 7fa3037ae2fe48ada8c626a51dc01ffd + method: GET + uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/detail?is_public=None + response: + body: + encoding: null + string: |- + { + "flavors": [ + { + "name": "m1.tiny", + "links": [ + { + "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/1", + "rel": "self" + }, + { + "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/1", + "rel": "bookmark" + } + ], + "ram": 512, + "OS-FLV-DISABLED:disabled": false, + "vcpus": 1, + "swap": "", + "os-flavor-access:is_public": true, + "rxtx_factor": 1.0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "disk": 1, + "id": "1" + }, + { + "name": "m1.small", + "links": [ + { + "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/2", + "rel": "self" + }, + { + "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/2", + "rel": "bookmark" + } + ], + "ram": 2048, + "OS-FLV-DISABLED:disabled": false, + "vcpus": 1, + "swap": "", + "os-flavor-access:is_public": true, + "rxtx_factor": 1.0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "disk": 20, + "id": "2" + }, + { + "name": "m1.medium", + "links": [ + { + "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/3", + "rel": "self" + }, + { + "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/3", + "rel": "bookmark" + } + ], + "ram": 4096, + "OS-FLV-DISABLED:disabled": false, + "vcpus": 2, + "swap": "", + "os-flavor-access:is_public": true, + "rxtx_factor": 1.0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "disk": 40, + "id": "3" + }, + { + "name": "m1.large", + "links": [ + { + "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/4", + "rel": "self" + }, + { + "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/4", + "rel": "bookmark" + } + ], + "ram": 8192, + "OS-FLV-DISABLED:disabled": false, + "vcpus": 4, + "swap": "", + "os-flavor-access:is_public": true, + "rxtx_factor": 1.0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "disk": 80, + "id": "4" + }, + { + "name": "m1.nano", + "links": [ + { + "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/42", + "rel": "self" + }, + { + "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/42", + "rel": "bookmark" + } + ], + "ram": 64, + "OS-FLV-DISABLED:disabled": false, + "vcpus": 1, + "swap": "", + "os-flavor-access:is_public": true, + "rxtx_factor": 1.0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "disk": 0, + "id": "42" + }, + { + "name": "m1.xlarge", + "links": [ + { + "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/5", + "rel": "self" + }, + { + "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/5", + "rel": "bookmark" + } + ], + "ram": 16384, + "OS-FLV-DISABLED:disabled": false, + "vcpus": 8, + "swap": "", + "os-flavor-access:is_public": true, + "rxtx_factor": 1.0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "disk": 160, + "id": "5" + }, + { + "name": "m1.micro", + "links": [ + { + "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/84", + "rel": "self" + }, + { + "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/84", + "rel": "bookmark" + } + ], + "ram": 128, + "OS-FLV-DISABLED:disabled": false, + "vcpus": 1, + "swap": "", + "os-flavor-access:is_public": true, + "rxtx_factor": 1.0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "disk": 0, + "id": "84" + }, + { + "name": "vanilla", + "links": [ + { + "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30", + "rel": "self" + }, + { + "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30", + "rel": "bookmark" + } + ], + "ram": 12345, + "OS-FLV-DISABLED:disabled": false, + "vcpus": 4, + "swap": "", + "os-flavor-access:is_public": true, + "rxtx_factor": 1.0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "disk": 100, + "id": "8987fc39-f5b8-46ce-bf1e-816e01623b30" + } + ] + } + headers: + Connection: + - keep-alive + Content-Length: + - '3456' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:10:01 GMT + Vary: + - X-OpenStack-Nova-API-Version + X-Compute-Request-Id: + - req-13ba0453-4b97-467c-b996-ba2d7965df01 + X-Openstack-Nova-Api-Version: + - '2.1' + status: + code: 200 + message: OK + url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/detail?is_public=None +- recorded_at: '2016-04-14T10:10:02' + request: + body: + encoding: utf-8 + string: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 + X-Auth-Token: + - 7fa3037ae2fe48ada8c626a51dc01ffd + method: GET + uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/1/os-extra_specs + response: + body: + encoding: null + string: |- + { + "extra_specs": {} + } + headers: + Connection: + - keep-alive + Content-Length: + - '19' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:10:02 GMT + Vary: + - X-OpenStack-Nova-API-Version + X-Compute-Request-Id: + - req-b223f5a5-d817-433c-8f11-61b351b0d964 + X-Openstack-Nova-Api-Version: + - '2.1' + status: + code: 200 + message: OK + url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/1/os-extra_specs +- recorded_at: '2016-04-14T10:10:02' + request: + body: + encoding: utf-8 + string: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 + X-Auth-Token: + - 7fa3037ae2fe48ada8c626a51dc01ffd + method: GET + uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/2/os-extra_specs + response: + body: + encoding: null + string: |- + { + "extra_specs": {} + } + headers: + Connection: + - keep-alive + Content-Length: + - '19' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:10:02 GMT + Vary: + - X-OpenStack-Nova-API-Version + X-Compute-Request-Id: + - req-1fb1cd46-72eb-452a-a937-6f4c99af71f4 + X-Openstack-Nova-Api-Version: + - '2.1' + status: + code: 200 + message: OK + url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/2/os-extra_specs +- recorded_at: '2016-04-14T10:10:02' + request: + body: + encoding: utf-8 + string: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 + X-Auth-Token: + - 7fa3037ae2fe48ada8c626a51dc01ffd + method: GET + uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/3/os-extra_specs + response: + body: + encoding: null + string: |- + { + "extra_specs": {} + } + headers: + Connection: + - keep-alive + Content-Length: + - '19' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:10:02 GMT + Vary: + - X-OpenStack-Nova-API-Version + X-Compute-Request-Id: + - req-3e20c7f7-7583-412c-8382-abe4f8ad1222 + X-Openstack-Nova-Api-Version: + - '2.1' + status: + code: 200 + message: OK + url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/3/os-extra_specs +- recorded_at: '2016-04-14T10:10:02' + request: + body: + encoding: utf-8 + string: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 + X-Auth-Token: + - 7fa3037ae2fe48ada8c626a51dc01ffd + method: GET + uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/4/os-extra_specs + response: + body: + encoding: null + string: |- + { + "extra_specs": {} + } + headers: + Connection: + - keep-alive + Content-Length: + - '19' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:10:02 GMT + Vary: + - X-OpenStack-Nova-API-Version + X-Compute-Request-Id: + - req-dadfb69e-f689-4215-ad82-4a4933536922 + X-Openstack-Nova-Api-Version: + - '2.1' + status: + code: 200 + message: OK + url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/4/os-extra_specs +- recorded_at: '2016-04-14T10:10:02' + request: + body: + encoding: utf-8 + string: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 + X-Auth-Token: + - 7fa3037ae2fe48ada8c626a51dc01ffd + method: GET + uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/42/os-extra_specs + response: + body: + encoding: null + string: |- + { + "extra_specs": {} + } + headers: + Connection: + - keep-alive + Content-Length: + - '19' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:10:02 GMT + Vary: + - X-OpenStack-Nova-API-Version + X-Compute-Request-Id: + - req-e8f3a46a-ca1f-498f-8694-40d9836ca23c + X-Openstack-Nova-Api-Version: + - '2.1' + status: + code: 200 + message: OK + url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/42/os-extra_specs +- recorded_at: '2016-04-14T10:10:02' + request: + body: + encoding: utf-8 + string: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 + X-Auth-Token: + - 7fa3037ae2fe48ada8c626a51dc01ffd + method: GET + uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/5/os-extra_specs + response: + body: + encoding: null + string: |- + { + "extra_specs": {} + } + headers: + Connection: + - keep-alive + Content-Length: + - '19' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:10:02 GMT + Vary: + - X-OpenStack-Nova-API-Version + X-Compute-Request-Id: + - req-4e6ccef8-6b65-4f83-92d4-6a0edcfda130 + X-Openstack-Nova-Api-Version: + - '2.1' + status: + code: 200 + message: OK + url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/5/os-extra_specs +- recorded_at: '2016-04-14T10:10:02' + request: + body: + encoding: utf-8 + string: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 + X-Auth-Token: + - 7fa3037ae2fe48ada8c626a51dc01ffd + method: GET + uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/84/os-extra_specs + response: + body: + encoding: null + string: |- + { + "extra_specs": {} + } + headers: + Connection: + - keep-alive + Content-Length: + - '19' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:10:02 GMT + Vary: + - X-OpenStack-Nova-API-Version + X-Compute-Request-Id: + - req-b7d2664c-ca1f-4271-9935-c5ccef7a3b11 + X-Openstack-Nova-Api-Version: + - '2.1' + status: + code: 200 + message: OK + url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/84/os-extra_specs +- recorded_at: '2016-04-14T10:10:03' + request: + body: + encoding: utf-8 + string: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 + X-Auth-Token: + - 7fa3037ae2fe48ada8c626a51dc01ffd + method: GET + uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30/os-extra_specs + response: + body: + encoding: null + string: |- + { + "extra_specs": {} + } + headers: + Connection: + - keep-alive + Content-Length: + - '19' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:10:03 GMT + Vary: + - X-OpenStack-Nova-API-Version + X-Compute-Request-Id: + - req-b84891a9-84cc-4faf-816a-07d5fefdfc06 + X-Openstack-Nova-Api-Version: + - '2.1' + status: + code: 200 + message: OK + url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30/os-extra_specs +- recorded_at: '2016-04-14T10:10:03' + request: + body: + encoding: utf-8 + string: '' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + User-Agent: + - python-novaclient + X-Auth-Token: + - 7fa3037ae2fe48ada8c626a51dc01ffd + method: DELETE + uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30 + response: + body: + encoding: null + string: '' + headers: + Connection: + - keep-alive + Content-Length: + - '0' + Content-Type: + - application/json + Date: + - Thu, 14 Apr 2016 10:10:03 GMT + Vary: + - X-OpenStack-Nova-API-Version + X-Compute-Request-Id: + - req-3e795065-ba65-437f-af6c-19373da4f3aa + X-Openstack-Nova-Api-Version: + - '2.1' + status: + code: 202 + message: Accepted + url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30 +recorded_with: betamax/0.6.0 + diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index 5c8a79bd2..6d9b5a08c 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -43,7 +43,7 @@ def test_create_server_with_create_exception(self): Test that an exception in the novaclient create raises an exception in create_server. """ - with patch("shade.OpenStackCloud"): + with patch("shade.OpenStackCloud.nova_client"): config = { "servers.create.side_effect": Exception("exception"), } @@ -57,7 +57,7 @@ def test_create_server_with_get_exception(self): Test that an exception when attempting to get the server instance via the novaclient raises an exception in create_server. """ - with patch("shade.OpenStackCloud"): + with patch("shade.OpenStackCloud.nova_client"): config = { "servers.create.return_value": Mock(status="BUILD"), "servers.get.side_effect": Exception("exception") @@ -74,7 +74,7 @@ def test_create_server_with_server_error(self): """ build_server = fakes.FakeServer('1234', '', 'BUILD') error_server = fakes.FakeServer('1234', '', 'ERROR') - with patch("shade.OpenStackCloud"): + with patch("shade.OpenStackCloud.nova_client"): config = { "servers.create.return_value": build_server, "servers.get.return_value": error_server, @@ -89,7 +89,7 @@ def test_create_server_wait_server_error(self): Test that a server error while waiting for the server to spawn raises an exception in create_server. """ - with patch("shade.OpenStackCloud"): + with patch("shade.OpenStackCloud.nova_client"): build_server = fakes.FakeServer('1234', '', 'BUILD') error_server = fakes.FakeServer('1234', '', 'ERROR') fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', @@ -113,7 +113,7 @@ def test_create_server_with_timeout(self): Test that a timeout while waiting for the server to spawn raises an exception in create_server. """ - with patch("shade.OpenStackCloud"): + with patch("shade.OpenStackCloud.nova_client"): fake_server = fakes.FakeServer('1234', '', 'BUILD') config = { "servers.create.return_value": fake_server, @@ -131,7 +131,7 @@ def test_create_server_no_wait(self): Test that create_server with no wait and no exception in the novaclient create call returns the server instance. """ - with patch("shade.OpenStackCloud"): + with patch("shade.OpenStackCloud.nova_client"): fake_server = fakes.FakeServer('1234', '', 'BUILD') fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', '1.1.1.1', '2.2.2.2', @@ -155,7 +155,7 @@ def test_create_server_with_admin_pass_no_wait(self): """ Test that a server with an admin_pass passed returns the password """ - with patch("shade.OpenStackCloud"): + with patch("shade.OpenStackCloud.nova_client"): fake_server = fakes.FakeServer('1234', '', 'BUILD') fake_create_server = fakes.FakeServer('1234', '', 'BUILD', adminPass='ooBootheiX0edoh') @@ -265,7 +265,7 @@ def test_create_server_no_addresses(self, mock_sleep): Test that create_server with a wait throws an exception if the server doesn't have addresses. """ - with patch("shade.OpenStackCloud"): + with patch("shade.OpenStackCloud.nova_client"): build_server = fakes.FakeServer('1234', '', 'BUILD') fake_server = fakes.FakeServer('1234', '', 'ACTIVE') fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', diff --git a/shade/tests/unit/test_flavors.py b/shade/tests/unit/test_flavors.py index 99f6ebdb8..e3277f73b 100644 --- a/shade/tests/unit/test_flavors.py +++ b/shade/tests/unit/test_flavors.py @@ -16,6 +16,8 @@ import mock import shade +from keystoneauth1.fixture import keystoneauth_betamax +from keystoneauth1.fixture import serializer from shade.tests import fakes from shade.tests.unit import base @@ -25,16 +27,36 @@ class TestFlavors(base.TestCase): def setUp(self): super(TestFlavors, self).setUp() - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_create_flavor(self, mock_nova): + def test_create_flavor(self): + self.useFixture(keystoneauth_betamax.BetamaxFixture( + cassette_name='test_create_flavor', + cassette_library_dir=self.fixtures_directory, + record=self.record_fixtures, + serializer=serializer.YamlJsonSerializer)) + + old_flavors = self.op_cloud.list_flavors() self.op_cloud.create_flavor( 'vanilla', 12345, 4, 100 ) - mock_nova.flavors.create.assert_called_once_with( - name='vanilla', ram=12345, vcpus=4, disk=100, - flavorid='auto', ephemeral=0, swap=0, rxtx_factor=1.0, - is_public=True - ) + + # test that we have a new flavor added + new_flavors = self.op_cloud.list_flavors() + self.assertEquals(len(new_flavors) - len(old_flavors), 1) + + # test that new flavor is created correctly + found = False + for flavor in new_flavors: + if flavor['name'] == 'vanilla': + found = True + break + self.assertTrue(found) + needed_keys = {'name', 'ram', 'vcpus', 'id', 'is_public', 'disk'} + if found: + # check flavor content + self.assertTrue(needed_keys.issubset(flavor.keys())) + + # delete created flavor + self.op_cloud.delete_flavor('vanilla') @mock.patch.object(shade.OpenStackCloud, '_compute_client') @mock.patch.object(shade.OpenStackCloud, 'nova_client') From 1a1107ad73b01cd1d80853ddb9faec3dcef8b72b Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 5 Aug 2016 10:08:15 -0400 Subject: [PATCH 0970/3836] Fix requirements for broken os-client-config OCC 1.19.0 breaks all the things. Avoid it. Change-Id: I721d7c937e4a7a5dd8b0d0abfae1bb696d2b2f00 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fd29caa6b..5d57d235b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ munch decorator jsonpatch ipaddress -os-client-config>=1.17.0 +os-client-config>=1.17.0,!=1.19.0 requestsexceptions>=1.1.1 six From 600a638e74d89af55fceaf4017f70269ae6e4f3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20Gagne=CC=81?= Date: Tue, 2 Aug 2016 15:10:43 -0400 Subject: [PATCH 0971/3836] Update Internap information * Add sin01 and sjc01 regions * Add support for Glance v2 Change-Id: Iaf4ad7c807a28c24040b928f65f4aadc1a234d6e --- doc/source/vendor-support.rst | 3 ++- os_client_config/vendors/internap.json | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index 668ddccaa..c27d124f0 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -186,9 +186,10 @@ Region Name Human Name ams01 Amsterdam, NL da01 Dallas, TX nyj01 New York, NY +sin01 Singapore +sjc01 San Jose, CA ============== ================ -* Image API Version is 1 * Floating IPs are not supported osic diff --git a/os_client_config/vendors/internap.json b/os_client_config/vendors/internap.json index d5ad49f6d..b67fc06d4 100644 --- a/os_client_config/vendors/internap.json +++ b/os_client_config/vendors/internap.json @@ -7,10 +7,11 @@ "regions": [ "ams01", "da01", - "nyj01" + "nyj01", + "sin01", + "sjc01" ], "identity_api_version": "3", - "image_api_version": "1", "floating_ip_source": "None" } } From 3ef3864785eeceff6dedc9b873b3fc65a909f769 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 1 Aug 2016 16:19:33 -0500 Subject: [PATCH 0972/3836] Add ability to upload duplicate images In the basic case, avoiding re-uploading images is nice because uploading images is a costly operation. However, there are also usecases for uploading new images with the same name as an operator which are quite valid. Support these by allowing for a flag that overrides the duplicate checking logic. Change-Id: I6a00753654d500d68750e46c5ec2423a36903552 --- shade/openstackcloud.py | 20 ++++++---- shade/tests/functional/test_image.py | 57 ++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 28737975e..332ea5167 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2546,7 +2546,8 @@ def create_image( md5=None, sha256=None, disk_format=None, container_format=None, disable_vendor_agent=True, - wait=False, timeout=3600, **kwargs): + wait=False, timeout=3600, + allow_duplicates=False, **kwargs): """Upload an image to Glance. :param str name: Name of the image to create. If it is a pathname @@ -2578,6 +2579,8 @@ def create_image( true - however, be aware that one of the upload methods is always synchronous. :param timeout: Seconds to wait for image creation. None is forever. + :param allow_duplicates: If true, skips checks that enforce unique + image name. (optional, defaults to False) Additional kwargs will be passed to the image creation as additional metadata for the image. @@ -2603,12 +2606,15 @@ def create_image( container_format = 'bare' if not md5 or not sha256: (md5, sha256) = self._get_file_hashes(filename) - current_image = self.get_image(name) - if (current_image and current_image.get(IMAGE_MD5_KEY, '') == md5 - and current_image.get(IMAGE_SHA256_KEY, '') == sha256): - self.log.debug( - "image {name} exists and is up to date".format(name=name)) - return current_image + if allow_duplicates: + current_image = None + else: + current_image = self.get_image(name) + if (current_image and current_image.get(IMAGE_MD5_KEY, '') == md5 + and current_image.get(IMAGE_SHA256_KEY, '') == sha256): + self.log.debug( + "image {name} exists and is up to date".format(name=name)) + return current_image kwargs[IMAGE_MD5_KEY] = md5 kwargs[IMAGE_SHA256_KEY] = sha256 kwargs[IMAGE_OBJECT_KEY] = '/'.join([container, name]) diff --git a/shade/tests/functional/test_image.py b/shade/tests/functional/test_image.py index 8a08abbf4..a2f17a2f9 100644 --- a/shade/tests/functional/test_image.py +++ b/shade/tests/functional/test_image.py @@ -69,3 +69,60 @@ def test_download_image(self): self.addCleanup(os.remove, output) self.assertTrue(filecmp.cmp(test_image.name, output), "Downloaded contents don't match created image") + + def test_create_image_skip_duplicate(self): + test_image = tempfile.NamedTemporaryFile(delete=False) + test_image.write('\0' * 1024 * 1024) + test_image.close() + image_name = self.getUniqueString('image') + try: + first_image = self.demo_cloud.create_image( + name=image_name, + filename=test_image.name, + disk_format='raw', + container_format='bare', + min_disk=10, + min_ram=1024, + wait=True) + second_image = self.demo_cloud.create_image( + name=image_name, + filename=test_image.name, + disk_format='raw', + container_format='bare', + min_disk=10, + min_ram=1024, + wait=True) + self.assertEqual(first_image.id, second_image.id) + finally: + self.demo_cloud.delete_image(image_name, wait=True) + + def test_create_image_force_duplicate(self): + test_image = tempfile.NamedTemporaryFile(delete=False) + test_image.write('\0' * 1024 * 1024) + test_image.close() + image_name = self.getUniqueString('image') + second_image = None + try: + first_image = self.demo_cloud.create_image( + name=image_name, + filename=test_image.name, + disk_format='raw', + container_format='bare', + min_disk=10, + min_ram=1024, + wait=True) + second_image = self.demo_cloud.create_image( + name=image_name, + filename=test_image.name, + disk_format='raw', + container_format='bare', + min_disk=10, + min_ram=1024, + allow_duplicates=True, + wait=True) + self.assertNotEqual(first_image.id, second_image.id) + finally: + if first_image: + self.demo_cloud.delete_image(first_image.id, wait=True) + if second_image: + self.demo_cloud.delete_image(second_image.id, wait=True) From 8ab5855870739c1d05a11ead01f545a91db3cd8d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 7 Aug 2016 14:33:14 -0500 Subject: [PATCH 0973/3836] Allow creating a floating ip on an arbitrary port There are use cases where creating a FIP on an unbound port is a completely legitimate thing to do. Since our floating ip process already starts with "hey man, find me a port please" - allowing a parameter which short-circuits that and just attaches the FIP to the given port is not too terrible, all things considered. Change-Id: Iee2fc2ac7b26dfa2a48b0404a035e2820befa5ba --- shade/openstackcloud.py | 53 ++++++++++++++------ shade/tests/unit/test_floating_ip_neutron.py | 53 +++++++++++++++++++- 2 files changed, 90 insertions(+), 16 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 5bbb2fcc2..bde4a045c 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3367,7 +3367,7 @@ def _nova_available_floating_ips(self, pool=None): def create_floating_ip(self, network=None, server=None, fixed_address=None, nat_destination=None, - wait=False, timeout=60): + port=None, wait=False, timeout=60): """Allocate a new floating IP from a network or a pool. :param network: Nova pool name or Neutron network name or id @@ -3379,6 +3379,10 @@ def create_floating_ip(self, network=None, server=None, :param nat_destination: (optional) Name or id of the network that the fixed IP to attach the floating IP to should be on. + :param port: (optional) The port id that the floating IP should be + attached to. Specifying a port conflicts + with specifying a server, fixed_address or + nat_destination. :param wait: (optional) Whether to wait for the IP to be active. Defaults to False. Only applies if a server is provided. @@ -3396,6 +3400,7 @@ def create_floating_ip(self, network=None, server=None, network_name_or_id=network, server=server, fixed_address=fixed_address, nat_destination=nat_destination, + port=port, wait=wait, timeout=timeout) except OpenStackCloudURINotFound as e: self.log.debug( @@ -3403,6 +3408,13 @@ def create_floating_ip(self, network=None, server=None, "'{msg}'. Trying with Nova.".format(msg=str(e))) # Fall-through, trying with Nova + if port: + raise OpenStackCloudException( + "This cloud uses nova-network which does not support" + " arbitrary floating-ip/port mappings. Please nudge" + " your cloud provider to upgrade the networking stack" + " to neutron, or alternately provide the server," + " fixed_address and nat_destination arguments as appropriate") # Else, we are using Nova network f_ips = _utils.normalize_nova_floating_ips( [self._nova_create_floating_ip(pool=network)]) @@ -3416,7 +3428,9 @@ def _submit_create_fip(self, kwargs): def _neutron_create_floating_ip( self, network_name_or_id=None, server=None, - fixed_address=None, nat_destination=None, wait=False, timeout=60): + fixed_address=None, nat_destination=None, + port=None, + wait=False, timeout=60): with _utils.neutron_exceptions( "unable to create floating IP for net " "{0}".format(network_name_or_id)): @@ -3435,23 +3449,32 @@ def _neutron_create_floating_ip( kwargs = { 'floating_network_id': networks[0]['id'], } - port = None - if server: - (port, fixed_ip_address) = self._get_free_fixed_port( - server, fixed_address=fixed_address, - nat_destination=nat_destination) - if port: - kwargs['port_id'] = port['id'] - kwargs['fixed_ip_address'] = fixed_ip_address + if not port: + if server: + (port_obj, fixed_ip_address) = self._get_free_fixed_port( + server, fixed_address=fixed_address, + nat_destination=nat_destination) + if port_obj: + port = port_obj['id'] + if fixed_ip_address: + kwargs['fixed_ip_address'] = fixed_ip_address + if port: + kwargs['port_id'] = port + fip = self._submit_create_fip(kwargs) fip_id = fip['id'] if port: - if fip['port_id'] != port['id']: - raise OpenStackCloudException( - "Attempted to create FIP on port {port} for server" - " {server} but something went wrong".format( - port=port['id'], server=server['id'])) + if fip['port_id'] != port: + if server: + raise OpenStackCloudException( + "Attempted to create FIP on port {port} for server" + " {server} but something went wrong".format( + port=port, server=server['id'])) + else: + raise OpenStackCloudException( + "Attempted to create FIP on port {port}" + " but something went wrong".format(port=port)) # The FIP is only going to become active in this context # when we've attached it to something, which only occurs # if we've provided a port as a parameter diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 8e605496a..cf7f15293 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -72,6 +72,19 @@ class TestFloatingIP(base.TestCase): } } + mock_floating_ip_port_rep = { + 'floatingip': { + 'fixed_ip_address': '10.0.0.4', + 'floating_ip_address': '172.24.4.229', + 'floating_network_id': 'my-network-id', + 'id': '2f245a7b-796b-4f26-9cf9-9e82d248fda8', + 'port_id': 'ce705c24-c1ef-408a-bda3-7bbd946164ab', + 'router_id': None, + 'status': 'ACTIVE', + 'tenant_id': '4969c491a3c74ee4af974e6d800c62df' + } + } + mock_get_network_rep = { 'status': 'ACTIVE', 'subnets': [ @@ -216,6 +229,44 @@ def test_create_floating_ip( self.mock_floating_ip_new_rep['floatingip']['floating_ip_address'], ip['floating_ip_address']) + @patch.object(OpenStackCloud, 'neutron_client') + @patch.object(OpenStackCloud, 'search_networks') + @patch.object(OpenStackCloud, 'has_service') + def test_create_floating_ip_port_bad_response( + self, mock_has_service, mock_search_networks, mock_neutron_client): + mock_has_service.return_value = True + mock_search_networks.return_value = [self.mock_get_network_rep] + mock_neutron_client.create_floatingip.return_value = \ + self.mock_floating_ip_new_rep + + self.assertRaises( + exc.OpenStackCloudException, + self.client.create_floating_ip, + network='my-network', port='ce705c24-c1ef-408a-bda3-7bbd946164ab') + + @patch.object(OpenStackCloud, 'neutron_client') + @patch.object(OpenStackCloud, 'search_networks') + @patch.object(OpenStackCloud, 'has_service') + def test_create_floating_ip_port( + self, mock_has_service, mock_search_networks, mock_neutron_client): + mock_has_service.return_value = True + mock_search_networks.return_value = [self.mock_get_network_rep] + mock_neutron_client.create_floatingip.return_value = \ + self.mock_floating_ip_port_rep + + ip = self.client.create_floating_ip( + network='my-network', port='ce705c24-c1ef-408a-bda3-7bbd946164ab') + + mock_neutron_client.create_floatingip.assert_called_with( + body={'floatingip': { + 'floating_network_id': 'my-network-id', + 'port_id': 'ce705c24-c1ef-408a-bda3-7bbd946164ab', + }} + ) + self.assertEqual( + self.mock_floating_ip_new_rep['floatingip']['floating_ip_address'], + ip['floating_ip_address']) + @patch.object(_utils, 'normalize_neutron_floating_ips') @patch.object(OpenStackCloud, '_neutron_available_floating_ips') @patch.object(OpenStackCloud, 'has_service') @@ -352,7 +403,7 @@ def test_auto_ip_pool_no_reuse( fake_server, ip_pool='my-network', reuse=False) mock__neutron_create_floating_ip.assert_called_once_with( - network_name_or_id='my-network', server=None, + network_name_or_id='my-network', server=None, port=None, fixed_address=None, nat_destination=None, wait=False, timeout=60) mock_attach_ip_to_server.assert_called_once_with( server=fake_server, fixed_address=None, From a98be6a6662296e6fb299f408b77d5f0e082b2dd Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 2 Aug 2016 10:15:13 -0500 Subject: [PATCH 0974/3836] Add a 'meta' passthrough parameter for glance images create_image tries to do data type conversion for you so that what you mean is correct 90% of the time. However, inferring intent is hard on people who do know what they want. New parameter 'meta' is a vehicle for non-converted key/value pairs. Change-Id: I99c1a104f6eb8fe72dd4ebab5b3aac8231068eb7 --- .../meta-passthrough-d695bff4f9366b65.yaml | 7 ++ shade/openstackcloud.py | 98 ++++++++++++----- shade/tests/unit/test_caching.py | 102 +++++++++++++++++- 3 files changed, 177 insertions(+), 30 deletions(-) create mode 100644 releasenotes/notes/meta-passthrough-d695bff4f9366b65.yaml diff --git a/releasenotes/notes/meta-passthrough-d695bff4f9366b65.yaml b/releasenotes/notes/meta-passthrough-d695bff4f9366b65.yaml new file mode 100644 index 000000000..13eb7ca2f --- /dev/null +++ b/releasenotes/notes/meta-passthrough-d695bff4f9366b65.yaml @@ -0,0 +1,7 @@ +--- +features: + - Added a parameter to create_image 'meta' which allows + for providing parameters to the API that will not have + any type conversions performed. For the simple case, + the existing kwargs approach to image metadata is still + the best bet. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 332ea5167..97e15cb28 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2547,7 +2547,7 @@ def create_image( disk_format=None, container_format=None, disable_vendor_agent=True, wait=False, timeout=3600, - allow_duplicates=False, **kwargs): + allow_duplicates=False, meta=None, **kwargs): """Upload an image to Glance. :param str name: Name of the image to create. If it is a pathname @@ -2581,9 +2581,19 @@ def create_image( :param timeout: Seconds to wait for image creation. None is forever. :param allow_duplicates: If true, skips checks that enforce unique image name. (optional, defaults to False) + :param meta: A dict of key/value pairs to use for metadata that + bypasses automatic type conversion. Additional kwargs will be passed to the image creation as additional - metadata for the image. + metadata for the image and will have all values converted to string + except for min_disk, min_ram, size and virtual_size which will be + converted to int. + + If you are sure you have all of your data types correct or have an + advanced need to be explicit, use meta. If you are just a normal + consumer, using kwargs is likely the right choice. + + If a value is in meta and kwargs, meta wins. :returns: A ``munch.Munch`` of the Image object @@ -2593,6 +2603,9 @@ def create_image( if not disk_format: disk_format = self.cloud_config.config['image_format'] + if not meta: + meta = {} + # If there is no filename, see if name is actually the filename if not filename: name, filename = self._get_name_and_filename(name) @@ -2640,15 +2653,21 @@ def create_image( return self._upload_image_task( name, filename, container, current_image=current_image, - wait=wait, timeout=timeout, **kwargs) + wait=wait, timeout=timeout, + meta=meta, **kwargs) else: + # If a user used the v1 calling format, they will have + # passed a dict called properties along + properties = kwargs.pop('properties', {}) + kwargs.update(properties) image_kwargs = dict(properties=kwargs) if disk_format: image_kwargs['disk_format'] = disk_format if container_format: image_kwargs['container_format'] = container_format - return self._upload_image_put(name, filename, **image_kwargs) + return self._upload_image_put( + name, filename, meta=meta, **image_kwargs) except OpenStackCloudException: self.log.debug("Image creation failed", exc_info=True) raise @@ -2656,15 +2675,25 @@ def create_image( raise OpenStackCloudException( "Image creation failed: {message}".format(message=str(e))) - def _upload_image_put_v2(self, name, image_data, **image_kwargs): - if 'properties' in image_kwargs: - img_props = image_kwargs.pop('properties') - for k, v in iter(img_props.items()): - image_kwargs[k] = str(v) - # some MUST be integer - for k in ('min_disk', 'min_ram'): - if k in image_kwargs: - image_kwargs[k] = int(image_kwargs[k]) + def _make_v2_image_params(self, meta, properties): + ret = {} + for k, v in iter(properties.items()): + if k in ('min_disk', 'min_ram', 'size', 'virtual_size'): + ret[k] = int(v) + else: + if v is None: + ret[k] = None + else: + ret[k] = str(v) + ret.update(meta) + return ret + + def _upload_image_put_v2(self, name, image_data, meta, **image_kwargs): + + properties = image_kwargs.pop('properties', {}) + + image_kwargs.update(self._make_v2_image_params(meta, properties)) + image = self.manager.submitTask(_tasks.ImageCreate( name=name, **image_kwargs)) try: @@ -2678,7 +2707,10 @@ def _upload_image_put_v2(self, name, image_data, **image_kwargs): return image - def _upload_image_put_v1(self, name, image_data, **image_kwargs): + def _upload_image_put_v1( + self, name, image_data, meta, **image_kwargs): + + image_kwargs['properties'].update(meta) image = self.manager.submitTask(_tasks.ImageCreate( name=name, **image_kwargs)) try: @@ -2691,19 +2723,25 @@ def _upload_image_put_v1(self, name, image_data, **image_kwargs): raise return image - def _upload_image_put(self, name, filename, **image_kwargs): + def _upload_image_put( + self, name, filename, meta, **image_kwargs): image_data = open(filename, 'rb') # Because reasons and crying bunnies if self.cloud_config.get_api_version('image') == '2': - image = self._upload_image_put_v2(name, image_data, **image_kwargs) + image = self._upload_image_put_v2( + name, image_data, meta, **image_kwargs) else: - image = self._upload_image_put_v1(name, image_data, **image_kwargs) + image = self._upload_image_put_v1( + name, image_data, meta, **image_kwargs) self._cache.invalidate() return self.get_image(image.id) def _upload_image_task( self, name, filename, container, current_image, - wait, timeout, **image_properties): + wait, timeout, meta, **image_kwargs): + + parameters = image_kwargs.pop('parameters', {}) + image_kwargs.update(parameters) # get new client sessions with self._swift_client_lock: @@ -2713,8 +2751,8 @@ def _upload_image_task( self.create_object( container, name, filename, - md5=image_properties.get('md5', None), - sha256=image_properties.get('sha256', None)) + md5=image_kwargs.get('md5', None), + sha256=image_kwargs.get('sha256', None)) if not current_image: current_image = self.get_image(name) # TODO(mordred): Can we do something similar to what nodepool does @@ -2752,8 +2790,7 @@ def _upload_image_task( if image is None: continue self.update_image_properties( - image=image, - **image_properties) + image=image, meta=meta, **image_kwargs) return self.get_image(status.result['image_id']) if status.status == 'failure': if status.message == IMAGE_ERROR_396: @@ -2769,9 +2806,11 @@ def _upload_image_task( return glance_task def update_image_properties( - self, image=None, name_or_id=None, **properties): + self, image=None, name_or_id=None, meta=None, **properties): if image is None: image = self.get_image(name_or_id) + if not meta: + meta = {} img_props = {} for k, v in iter(properties.items()): @@ -2782,15 +2821,15 @@ def update_image_properties( # This makes me want to die inside if self.cloud_config.get_api_version('image') == '2': - return self._update_image_properties_v2(image, img_props) + return self._update_image_properties_v2(image, meta, img_props) else: - return self._update_image_properties_v1(image, img_props) + return self._update_image_properties_v1(image, meta, img_props) - def _update_image_properties_v2(self, image, properties): + def _update_image_properties_v2(self, image, meta, properties): img_props = {} - for k, v in iter(properties.items()): + for k, v in iter(self._make_v2_image_params(meta, properties).items()): if image.get(k, None) != v: - img_props[k] = str(v) + img_props[k] = v if not img_props: return False self.manager.submitTask(_tasks.ImageUpdate( @@ -2798,7 +2837,8 @@ def _update_image_properties_v2(self, image, properties): self.list_images.invalidate(self) return True - def _update_image_properties_v1(self, image, properties): + def _update_image_properties_v1(self, image, meta, properties): + properties.update(meta) img_props = {} for k, v in iter(properties.items()): if image.properties.get(k, None) != v: diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 477cd9906..eb1c7729a 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -386,7 +386,7 @@ def test_create_image_put_v2(self, glance_mock, mock_api_version): fake_image = fakes.FakeImage('42', '42 name', 'success') glance_mock.images.create.return_value = fake_image glance_mock.images.list.return_value = [fake_image] - self._call_create_image('42 name', min_disk=0, min_ram=0) + self._call_create_image('42 name', min_disk='0', min_ram=0) args = {'name': '42 name', 'container_format': 'bare', 'disk_format': 'qcow2', 'owner_specified.shade.md5': mock.ANY, @@ -400,6 +400,106 @@ def test_create_image_put_v2(self, glance_mock, mock_api_version): fake_image_dict = meta.obj_to_dict(fake_image) self.assertEqual([fake_image_dict], self.cloud.list_images()) + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, 'glance_client') + def test_create_image_put_bad_int(self, glance_mock, mock_api_version): + mock_api_version.return_value = '2' + self.cloud.image_api_use_tasks = False + + glance_mock.images.list.return_value = [] + self.assertEqual([], self.cloud.list_images()) + + fake_image = fakes.FakeImage('42', '42 name', 'success') + glance_mock.images.create.return_value = fake_image + glance_mock.images.list.return_value = [fake_image] + self.assertRaises( + exc.OpenStackCloudException, + self._call_create_image, '42 name', min_disk='fish', min_ram=0) + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, 'glance_client') + def test_create_image_put_user_int(self, glance_mock, mock_api_version): + mock_api_version.return_value = '2' + self.cloud.image_api_use_tasks = False + + glance_mock.images.list.return_value = [] + self.assertEqual([], self.cloud.list_images()) + + fake_image = fakes.FakeImage('42', '42 name', 'success') + glance_mock.images.create.return_value = fake_image + glance_mock.images.list.return_value = [fake_image] + self._call_create_image( + '42 name', min_disk='0', min_ram=0, int_v=12345) + args = {'name': '42 name', + 'container_format': 'bare', 'disk_format': u'qcow2', + 'owner_specified.shade.md5': mock.ANY, + 'owner_specified.shade.sha256': mock.ANY, + 'owner_specified.shade.object': 'images/42 name', + 'int_v': '12345', + 'visibility': 'private', + 'min_disk': 0, 'min_ram': 0} + glance_mock.images.create.assert_called_with(**args) + glance_mock.images.upload.assert_called_with( + image_data=mock.ANY, image_id=fake_image.id) + fake_image_dict = meta.obj_to_dict(fake_image) + self.assertEqual([fake_image_dict], self.cloud.list_images()) + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, 'glance_client') + def test_create_image_put_meta_int(self, glance_mock, mock_api_version): + mock_api_version.return_value = '2' + self.cloud.image_api_use_tasks = False + + glance_mock.images.list.return_value = [] + self.assertEqual([], self.cloud.list_images()) + + fake_image = fakes.FakeImage('42', '42 name', 'success') + glance_mock.images.create.return_value = fake_image + glance_mock.images.list.return_value = [fake_image] + self._call_create_image( + '42 name', min_disk='0', min_ram=0, meta={'int_v': 12345}) + args = {'name': '42 name', + 'container_format': 'bare', 'disk_format': u'qcow2', + 'owner_specified.shade.md5': mock.ANY, + 'owner_specified.shade.sha256': mock.ANY, + 'owner_specified.shade.object': 'images/42 name', + 'int_v': 12345, + 'visibility': 'private', + 'min_disk': 0, 'min_ram': 0} + glance_mock.images.create.assert_called_with(**args) + glance_mock.images.upload.assert_called_with( + image_data=mock.ANY, image_id=fake_image.id) + fake_image_dict = meta.obj_to_dict(fake_image) + self.assertEqual([fake_image_dict], self.cloud.list_images()) + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, 'glance_client') + def test_create_image_put_user_prop(self, glance_mock, mock_api_version): + mock_api_version.return_value = '2' + self.cloud.image_api_use_tasks = False + + glance_mock.images.list.return_value = [] + self.assertEqual([], self.cloud.list_images()) + + fake_image = fakes.FakeImage('42', '42 name', 'success') + glance_mock.images.create.return_value = fake_image + glance_mock.images.list.return_value = [fake_image] + self._call_create_image( + '42 name', min_disk='0', min_ram=0, properties={'int_v': 12345}) + args = {'name': '42 name', + 'container_format': 'bare', 'disk_format': u'qcow2', + 'owner_specified.shade.md5': mock.ANY, + 'owner_specified.shade.sha256': mock.ANY, + 'owner_specified.shade.object': 'images/42 name', + 'int_v': '12345', + 'visibility': 'private', + 'min_disk': 0, 'min_ram': 0} + glance_mock.images.create.assert_called_with(**args) + glance_mock.images.upload.assert_called_with( + image_data=mock.ANY, image_id=fake_image.id) + fake_image_dict = meta.obj_to_dict(fake_image) + self.assertEqual([fake_image_dict], self.cloud.list_images()) + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, '_get_file_hashes') @mock.patch.object(shade.OpenStackCloud, 'glance_client') From 2556c338595bca0a931e62bf396e479566626958 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 9 Aug 2016 07:27:50 -0500 Subject: [PATCH 0975/3836] Make shared an optional keyword param to create_network shared as a parameter can only be passed by admins. Change-Id: I26c800f6ecba127d4ef4683a18ad2aafefa63606 Story: #2000696 --- shade/openstackcloud.py | 4 +++- shade/tests/unit/test_network.py | 5 ----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 5bbb2fcc2..5c9047d27 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2158,10 +2158,12 @@ def create_network(self, name, shared=False, admin_state_up=True, """ network = { 'name': name, - 'shared': shared, 'admin_state_up': admin_state_up, } + if shared: + network['shared'] = shared + if project_id is not None: network['tenant_id'] = project_id diff --git a/shade/tests/unit/test_network.py b/shade/tests/unit/test_network.py index ee007c91f..378b08159 100644 --- a/shade/tests/unit/test_network.py +++ b/shade/tests/unit/test_network.py @@ -26,7 +26,6 @@ def test_create_network(self, mock_neutron): body=dict( network=dict( name='netname', - shared=False, admin_state_up=True ) ) @@ -39,7 +38,6 @@ def test_create_network_specific_tenant(self, mock_neutron): body=dict( network=dict( name='netname', - shared=False, admin_state_up=True, tenant_id="project_id_value", ) @@ -53,7 +51,6 @@ def test_create_network_external(self, mock_neutron): body=dict( network={ 'name': 'netname', - 'shared': False, 'admin_state_up': True, 'router:external': True } @@ -70,7 +67,6 @@ def test_create_network_provider(self, mock_neutron): body=dict( network={ 'name': 'netname', - 'shared': False, 'admin_state_up': True, 'provider:physical_network': provider_opts['physical_network'], @@ -93,7 +89,6 @@ def test_create_network_provider_ignored_value(self, mock_neutron): body=dict( network={ 'name': 'netname', - 'shared': False, 'admin_state_up': True, 'provider:physical_network': provider_opts['physical_network'], From 3f968d22b750d414fc6f8873e666433d909df294 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 10 Aug 2016 09:50:13 -0500 Subject: [PATCH 0976/3836] Rename baymodel to cluster_template Magnum is renaming baymodel to cluster_template. Rather than releasing a shade version with baymodel and then later switching, go ahead and rename to cluster_template now. However, since the term isn't in wide use yet, provide aliases. Change-Id: Ieb1f067680f2312eacf75eed0d42edbd90c54beb --- ...num_baymodel_support-e35e5aab0b14ff75.yaml | 5 +- shade/_tasks.py | 8 +- shade/_utils.py | 10 +- shade/openstackcloud.py | 135 ++++++++------ shade/tests/functional/test_baymodels.py | 113 ------------ .../functional/test_cluster_templates.py | 115 ++++++++++++ shade/tests/unit/test_baymodels.py | 155 ---------------- shade/tests/unit/test_cluster_templates.py | 169 ++++++++++++++++++ 8 files changed, 374 insertions(+), 336 deletions(-) delete mode 100644 shade/tests/functional/test_baymodels.py create mode 100644 shade/tests/functional/test_cluster_templates.py delete mode 100644 shade/tests/unit/test_baymodels.py create mode 100644 shade/tests/unit/test_cluster_templates.py diff --git a/releasenotes/notes/add_magnum_baymodel_support-e35e5aab0b14ff75.yaml b/releasenotes/notes/add_magnum_baymodel_support-e35e5aab0b14ff75.yaml index 9c4f9b015..21dbed6f1 100644 --- a/releasenotes/notes/add_magnum_baymodel_support-e35e5aab0b14ff75.yaml +++ b/releasenotes/notes/add_magnum_baymodel_support-e35e5aab0b14ff75.yaml @@ -1,4 +1,7 @@ --- features: - Add support for Magnum baymodels, with the - usual methods (search/list/get/create/update/delete). + usual methods (search/list/get/create/update/delete). Due to upcoming + rename in Magnum from baymodel to cluster_template, the shade + functionality uses the term cluster_template. However, baymodel aliases + are provided for each api call. diff --git a/shade/_tasks.py b/shade/_tasks.py index 6947cb0e1..47ffd9538 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -939,22 +939,22 @@ def main(self, client): return client.neutron_client.delete_quota(**self.args) -class BaymodelList(task_manager.Task): +class ClusterTemplateList(task_manager.Task): def main(self, client): return client.magnum_client.baymodels.list(**self.args) -class BaymodelCreate(task_manager.Task): +class ClusterTemplateCreate(task_manager.Task): def main(self, client): return client.magnum_client.baymodels.create(**self.args) -class BaymodelDelete(task_manager.Task): +class ClusterTemplateDelete(task_manager.Task): def main(self, client): return client.magnum_client.baymodels.delete(self.args['id']) -class BaymodelUpdate(task_manager.Task): +class ClusterTemplateUpdate(task_manager.Task): def main(self, client): return client.magnum_client.baymodels.update( self.args['id'], self.args['patch']) diff --git a/shade/_utils.py b/shade/_utils.py index 0e1f622a9..ace3b1cd4 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -495,11 +495,11 @@ def normalize_flavors(flavors): return flavors -def normalize_baymodels(baymodels): - """Normalize Magnum baymodels.""" - for baymodel in baymodels: - baymodel['id'] = baymodel['uuid'] - return baymodels +def normalize_cluster_templates(cluster_templates): + """Normalize Magnum cluster_templates.""" + for cluster_template in cluster_templates: + cluster_template['id'] = cluster_template['uuid'] + return cluster_templates def valid_kwargs(*valid_args): diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 5bbb2fcc2..a4702f884 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5766,43 +5766,52 @@ def delete_recordset(self, zone, name_or_id): return True @_utils.cache_on_arguments() - def list_baymodels(self, detail=False): - """List Magnum baymodels. + def list_cluster_templates(self, detail=False): + """List Magnum ClusterTemplates. + + ClusterTemplate is the new name for BayModel. :param bool detail. Flag to control if we need summarized or detailed output. - :returns: a list of dicts containing the baymodel details. + :returns: a list of dicts containing the cluster template details. :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ - with _utils.shade_exceptions("Error fetching baymodel list"): - baymodels = self.manager.submitTask( - _tasks.BaymodelList(detail=detail)) - return _utils.normalize_baymodels(baymodels) + with _utils.shade_exceptions("Error fetching ClusterTemplate list"): + cluster_templates = self.manager.submitTask( + _tasks.ClusterTemplateList(detail=detail)) + return _utils.normalize_cluster_templates(cluster_templates) + list_baymodels = list_cluster_templates + + def search_cluster_templates( + self, name_or_id=None, filters=None, detail=False): + """Search Magnum ClusterTemplates. - def search_baymodels(self, name_or_id=None, filters=None, detail=False): - """Search Magnum baymodels. + ClusterTemplate is the new name for BayModel. - :param name_or_id: baymodel name or ID. + :param name_or_id: ClusterTemplate name or ID. :param filters: a dict containing additional filters to use. :param detail: a boolean to control if we need summarized or detailed output. - :returns: a list of dict containing the baymodels + :returns: a list of dict containing the ClusterTemplates :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ - baymodels = self.list_baymodels(detail=detail) + cluster_templates = self.list_cluster_templates(detail=detail) return _utils._filter_list( - baymodels, name_or_id, filters) + cluster_templates, name_or_id, filters) + search_baymodels = search_cluster_templates - def get_baymodel(self, name_or_id, filters=None, detail=False): - """Get a baymodel by name or ID. + def get_cluster_template(self, name_or_id, filters=None, detail=False): + """Get a ClusterTemplate by name or ID. - :param name_or_id: Name or ID of the baymodel. + ClusterTemplate is the new name for BayModel. + + :param name_or_id: Name or ID of the ClusterTemplate. :param dict filters: A dictionary of meta data to use for further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: @@ -5814,72 +5823,79 @@ def get_baymodel(self, name_or_id, filters=None, detail=False): } } - :returns: A baymodel dict or None if no matching baymodel is - found. - + :returns: A ClusterTemplate dict or None if no matching + ClusterTemplate is found. """ - return _utils._get_entity(self.search_baymodels, name_or_id, + return _utils._get_entity(self.search_cluster_templates, name_or_id, filters=filters, detail=detail) + get_baymodel = get_cluster_template + + def create_cluster_template( + self, name, image_id=None, keypair_id=None, coe=None, **kwargs): + """Create a Magnum ClusterTemplate. - def create_baymodel(self, name, image_id=None, keypair_id=None, - coe=None, **kwargs): - """Create a Magnum baymodel. + ClusterTemplate is the new name for BayModel. - :param string name: Name of the baymodel. + :param string name: Name of the ClusterTemplate. :param string image_id: Name or ID of the image to use. :param string keypair_id: Name or ID of the keypair to use. - :param string coe: Name of the coe for the baymodel. + :param string coe: Name of the coe for the ClusterTemplate. Other arguments will be passed in kwargs. - :returns: a dict containing the baymodel description + :returns: a dict containing the ClusterTemplate description :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call """ with _utils.shade_exceptions( - "Error creating baymodel of name {baymodel_name}".format( - baymodel_name=name)): - baymodel = self.manager.submitTask( - _tasks.BaymodelCreate( + "Error creating ClusterTemplate of name" + " {cluster_template_name}".format( + cluster_template_name=name)): + cluster_template = self.manager.submitTask( + _tasks.ClusterTemplateCreate( name=name, image_id=image_id, keypair_id=keypair_id, coe=coe, **kwargs)) - self.list_baymodels.invalidate(self) - return baymodel + self.list_cluster_templates.invalidate(self) + return cluster_template + create_baymodel = create_cluster_template - def delete_baymodel(self, name_or_id): - """Delete a baymodel. + def delete_cluster_template(self, name_or_id): + """Delete a ClusterTemplate. - :param name_or_id: Name or unique ID of the baymodel. + ClusterTemplate is the new name for BayModel. + + :param name_or_id: Name or unique ID of the ClusterTemplate. :returns: True if the delete succeeded, False if the - baymodel was not found. + ClusterTemplate was not found. :raises: OpenStackCloudException on operation error. """ - self.list_baymodels.invalidate(self) - baymodel = self.get_baymodel(name_or_id) + self.list_cluster_templates.invalidate(self) + cluster_template = self.get_cluster_template(name_or_id) - if not baymodel: + if not cluster_template: self.log.debug( - "Baymodel {name_or_id} does not exist".format( + "ClusterTemplate {name_or_id} does not exist".format( name_or_id=name_or_id), exc_info=True) return False - with _utils.shade_exceptions("Error in deleting baymodel"): + with _utils.shade_exceptions("Error in deleting ClusterTemplate"): try: self.manager.submitTask( - _tasks.BaymodelDelete(id=baymodel['id'])) + _tasks.ClusterTemplateDelete(id=cluster_template['id'])) except magnum_exceptions.NotFound: self.log.debug( - "Baymodel {id} not found when deleting. Ignoring.".format( - id=baymodel['id'])) + "ClusterTemplate {id} not found when deleting." + " Ignoring.".format(id=cluster_template['id'])) return False - self.list_baymodels.invalidate(self) + self.list_cluster_templates.invalidate(self) return True + delete_baymodel = delete_cluster_template @_utils.valid_kwargs('name', 'image_id', 'flavor_id', 'master_flavor_id', 'keypair_id', 'external_network_id', 'fixed_network', @@ -5887,23 +5903,25 @@ def delete_baymodel(self, name_or_id): 'coe', 'http_proxy', 'https_proxy', 'no_proxy', 'network_driver', 'tls_disabled', 'public', 'registry_enabled', 'volume_driver') - def update_baymodel(self, name_or_id, operation, **kwargs): - """Update a Magnum baymodel. + def update_cluster_template(self, name_or_id, operation, **kwargs): + """Update a Magnum ClusterTemplate. + + ClusterTemplate is the new name for BayModel. - :param name_or_id: Name or ID of the baymodel being updated. + :param name_or_id: Name or ID of the ClusterTemplate being updated. :param operation: Operation to perform - add, remove, replace. Other arguments will be passed with kwargs. - :returns: a dict representing the updated baymodel. + :returns: a dict representing the updated ClusterTemplate. :raises: OpenStackCloudException on operation error. """ - self.list_baymodels.invalidate(self) - baymodel = self.get_baymodel(name_or_id) - if not baymodel: + self.list_cluster_templates.invalidate(self) + cluster_template = self.get_cluster_template(name_or_id) + if not cluster_template: raise OpenStackCloudException( - "Baymodel %s not found." % name_or_id) + "ClusterTemplate %s not found." % name_or_id) if operation not in ['add', 'replace', 'remove']: raise TypeError( @@ -5912,10 +5930,11 @@ def update_baymodel(self, name_or_id, operation, **kwargs): patches = _utils.generate_patches_from_kwargs(operation, **kwargs) with _utils.shade_exceptions( - "Error updating baymodel {0}".format(name_or_id)): + "Error updating ClusterTemplate {0}".format(name_or_id)): self.manager.submitTask( - _tasks.BaymodelUpdate( - id=baymodel['id'], patch=patches)) + _tasks.ClusterTemplateUpdate( + id=cluster_template['id'], patch=patches)) - new_baymodel = self.get_baymodel(name_or_id) - return new_baymodel + new_cluster_template = self.get_cluster_template(name_or_id) + return new_cluster_template + update_baymodel = update_cluster_template diff --git a/shade/tests/functional/test_baymodels.py b/shade/tests/functional/test_baymodels.py deleted file mode 100644 index c3d7e0283..000000000 --- a/shade/tests/functional/test_baymodels.py +++ /dev/null @@ -1,113 +0,0 @@ -# -*- 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. - -""" -test_baymodels ----------------------------------- - -Functional tests for `shade` baymodel methods. -""" - -from testtools import content - -from shade.tests.functional import base - -import os -import subprocess - - -class TestBaymodel(base.BaseFunctionalTestCase): - - def setUp(self): - super(TestBaymodel, self).setUp() - if not self.demo_cloud.has_service('container'): - self.skipTest('Container service not supported by cloud') - self.baymodel = None - - def test_baymodels(self): - '''Test baymodels functionality''' - name = 'fake-baymodel' - server_type = 'vm' - public = False - image_id = 'fedora-atomic-f23-dib' - tls_disabled = False - registry_enabled = False - coe = 'kubernetes' - keypair_id = 'testkey' - - self.addDetail('baymodel', content.text_content(name)) - self.addCleanup(self.cleanup, name) - - # generate a keypair to add to nova - ssh_directory = '/tmp/.ssh' - if not os.path.isdir(ssh_directory): - os.mkdir(ssh_directory) - subprocess.call( - ['ssh-keygen', '-t', 'rsa', '-N', '', '-f', - '%s/id_rsa_shade' % ssh_directory]) - - # add keypair to nova - with open('%s/id_rsa_shade.pub' % ssh_directory) as f: - key_content = f.read() - self.demo_cloud.create_keypair('testkey', key_content) - - # Test we can create a baymodel and we get it returned - self.baymodel = self.demo_cloud.create_baymodel( - name=name, image_id=image_id, - keypair_id=keypair_id, coe=coe) - self.assertEquals(self.baymodel['name'], name) - self.assertEquals(self.baymodel['image_id'], image_id) - self.assertEquals(self.baymodel['keypair_id'], keypair_id) - self.assertEquals(self.baymodel['coe'], coe) - self.assertEquals(self.baymodel['registry_enabled'], registry_enabled) - self.assertEquals(self.baymodel['tls_disabled'], tls_disabled) - self.assertEquals(self.baymodel['public'], public) - self.assertEquals(self.baymodel['server_type'], server_type) - - # Test that we can list baymodels - baymodels = self.demo_cloud.list_baymodels() - self.assertIsNotNone(baymodels) - - # Test we get the same baymodel with the get_baymodel method - baymodel_get = self.demo_cloud.get_baymodel(self.baymodel['uuid']) - self.assertEquals(baymodel_get['uuid'], self.baymodel['uuid']) - - # Test the get method also works by name - baymodel_get = self.demo_cloud.get_baymodel(name) - self.assertEquals(baymodel_get['name'], self.baymodel['name']) - - # Test we can update a field on the baymodel and only that field - # is updated - baymodel_update = self.demo_cloud.update_baymodel( - self.baymodel['uuid'], 'replace', tls_disabled=True) - self.assertEquals(baymodel_update['uuid'], - self.baymodel['uuid']) - self.assertEquals(baymodel_update['tls_disabled'], True) - - # Test we can delete and get True returned - baymodel_delete = self.demo_cloud.delete_baymodel( - self.baymodel['uuid']) - self.assertTrue(baymodel_delete) - - def cleanup(self, name): - if self.baymodel: - try: - self.demo_cloud.delete_baymodel(self.baymodel['name']) - except: - pass - - # delete keypair - self.demo_cloud.delete_keypair('testkey') - os.unlink('/tmp/.ssh/id_rsa_shade') - os.unlink('/tmp/.ssh/id_rsa_shade.pub') diff --git a/shade/tests/functional/test_cluster_templates.py b/shade/tests/functional/test_cluster_templates.py new file mode 100644 index 000000000..92dd7a641 --- /dev/null +++ b/shade/tests/functional/test_cluster_templates.py @@ -0,0 +1,115 @@ +# -*- 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. + +""" +test_cluster_templates +---------------------------------- + +Funself.ctional tests for `shade` cluster_template methods. +""" + +from testtools import content + +from shade.tests.functional import base + +import os +import subprocess + + +class TestClusterTemplate(base.BaseFunctionalTestCase): + + def setUp(self): + super(TestClusterTemplate, self).setUp() + if not self.demo_cloud.has_service('container'): + self.skipTest('Container service not supported by cloud') + self.ct = None + + def test_cluster_templates(self): + '''Test cluster_templates functionality''' + name = 'fake-cluster_template' + server_type = 'vm' + public = False + image_id = 'fedora-atomic-f23-dib' + tls_disabled = False + registry_enabled = False + coe = 'kubernetes' + keypair_id = 'testkey' + + self.addDetail('cluster_template', content.text_content(name)) + self.addCleanup(self.cleanup, name) + + # generate a keypair to add to nova + ssh_directory = '/tmp/.ssh' + if not os.path.isdir(ssh_directory): + os.mkdir(ssh_directory) + subprocess.call( + ['ssh-keygen', '-t', 'rsa', '-N', '', '-f', + '%s/id_rsa_shade' % ssh_directory]) + + # add keypair to nova + with open('%s/id_rsa_shade.pub' % ssh_directory) as f: + key_content = f.read() + self.demo_cloud.create_keypair('testkey', key_content) + + # Test we can create a cluster_template and we get it returned + self.ct = self.demo_cloud.create_cluster_template( + name=name, image_id=image_id, + keypair_id=keypair_id, coe=coe) + self.assertEquals(self.ct['name'], name) + self.assertEquals(self.ct['image_id'], image_id) + self.assertEquals(self.ct['keypair_id'], keypair_id) + self.assertEquals(self.ct['coe'], coe) + self.assertEquals(self.ct['registry_enabled'], registry_enabled) + self.assertEquals(self.ct['tls_disabled'], tls_disabled) + self.assertEquals(self.ct['public'], public) + self.assertEquals(self.ct['server_type'], server_type) + + # Test that we can list cluster_templates + cluster_templates = self.demo_cloud.list_cluster_templates() + self.assertIsNotNone(cluster_templates) + + # Test we get the same cluster_template with the + # get_cluster_template method + cluster_template_get = self.demo_cloud.get_cluster_template( + self.ct['uuid']) + self.assertEquals(cluster_template_get['uuid'], self.ct['uuid']) + + # Test the get method also works by name + cluster_template_get = self.demo_cloud.get_cluster_template(name) + self.assertEquals(cluster_template_get['name'], self.ct['name']) + + # Test we can update a field on the cluster_template and only that + # field is updated + cluster_template_update = self.demo_cloud.update_cluster_template( + self.ct['uuid'], 'replace', tls_disabled=True) + self.assertEquals(cluster_template_update['uuid'], + self.ct['uuid']) + self.assertEquals(cluster_template_update['tls_disabled'], True) + + # Test we can delete and get True returned + cluster_template_delete = self.demo_cloud.delete_cluster_template( + self.ct['uuid']) + self.assertTrue(cluster_template_delete) + + def cleanup(self, name): + if self.ct: + try: + self.demo_cloud.delete_cluster_template(self.ct['name']) + except: + pass + + # delete keypair + self.demo_cloud.delete_keypair('testkey') + os.unlink('/tmp/.ssh/id_rsa_shade') + os.unlink('/tmp/.ssh/id_rsa_shade.pub') diff --git a/shade/tests/unit/test_baymodels.py b/shade/tests/unit/test_baymodels.py deleted file mode 100644 index 04d9c8fc8..000000000 --- a/shade/tests/unit/test_baymodels.py +++ /dev/null @@ -1,155 +0,0 @@ -# -*- 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. - - -import mock -import munch - -import shade -import testtools -from shade.tests.unit import base - - -baymodel_obj = munch.Munch( - apiserver_port=None, - uuid='fake-uuid', - human_id=None, - name='fake-baymodel', - server_type='vm', - public=False, - image_id='fake-image', - tls_disabled=False, - registry_enabled=False, - coe='fake-coe', - keypair_id='fake-key', -) - -baymodel_detail_obj = munch.Munch( - links={}, - labels={}, - apiserver_port=None, - uuid='fake-uuid', - human_id=None, - name='fake-baymodel', - server_type='vm', - public=False, - image_id='fake-image', - tls_disabled=False, - registry_enabled=False, - coe='fake-coe', - created_at='fake-date', - updated_at=None, - master_flavor_id=None, - no_proxy=None, - https_proxy=None, - keypair_id='fake-key', - docker_volume_size=1, - external_network_id='public', - cluster_distro='fake-distro', - volume_driver=None, - network_driver='fake-driver', - fixed_network=None, - flavor_id='fake-flavor', - dns_nameserver='8.8.8.8', -) - - -class TestBaymodels(base.TestCase): - - def setUp(self): - super(TestBaymodels, self).setUp() - self.cloud = shade.openstack_cloud(validate=False) - - @mock.patch.object(shade.OpenStackCloud, 'magnum_client') - def test_list_baymodels_without_detail(self, mock_magnum): - mock_magnum.baymodels.list.return_value = [baymodel_obj, ] - baymodels_list = self.cloud.list_baymodels() - mock_magnum.baymodels.list.assert_called_with(detail=False) - self.assertEqual(baymodels_list[0], baymodel_obj) - - @mock.patch.object(shade.OpenStackCloud, 'magnum_client') - def test_list_baymodels_with_detail(self, mock_magnum): - mock_magnum.baymodels.list.return_value = [baymodel_detail_obj, ] - baymodels_list = self.cloud.list_baymodels(detail=True) - mock_magnum.baymodels.list.assert_called_with(detail=True) - self.assertEqual(baymodels_list[0], baymodel_detail_obj) - - @mock.patch.object(shade.OpenStackCloud, 'magnum_client') - def test_search_baymodels_by_name(self, mock_magnum): - mock_magnum.baymodels.list.return_value = [baymodel_obj, ] - - baymodels = self.cloud.search_baymodels(name_or_id='fake-baymodel') - mock_magnum.baymodels.list.assert_called_with(detail=False) - - self.assertEquals(1, len(baymodels)) - self.assertEquals('fake-uuid', baymodels[0]['uuid']) - - @mock.patch.object(shade.OpenStackCloud, 'magnum_client') - def test_search_baymodels_not_found(self, mock_magnum): - mock_magnum.baymodels.list.return_value = [baymodel_obj, ] - - baymodels = self.cloud.search_baymodels(name_or_id='non-existent') - - mock_magnum.baymodels.list.assert_called_with(detail=False) - self.assertEquals(0, len(baymodels)) - - @mock.patch.object(shade.OpenStackCloud, 'search_baymodels') - def test_get_baymodel(self, mock_search): - mock_search.return_value = [baymodel_obj, ] - r = self.cloud.get_baymodel('fake-baymodel') - self.assertIsNotNone(r) - self.assertDictEqual(baymodel_obj, r) - - @mock.patch.object(shade.OpenStackCloud, 'search_baymodels') - def test_get_baymodel_not_found(self, mock_search): - mock_search.return_value = [] - r = self.cloud.get_baymodel('doesNotExist') - self.assertIsNone(r) - - @mock.patch.object(shade.OpenStackCloud, 'magnum_client') - def test_create_baymodel(self, mock_magnum): - self.cloud.create_baymodel( - name=baymodel_obj.name, image_id=baymodel_obj.image_id, - keypair_id=baymodel_obj.keypair_id, coe=baymodel_obj.coe) - mock_magnum.baymodels.create.assert_called_once_with( - name=baymodel_obj.name, image_id=baymodel_obj.image_id, - keypair_id=baymodel_obj.keypair_id, coe=baymodel_obj.coe - ) - - @mock.patch.object(shade.OpenStackCloud, 'magnum_client') - def test_create_baymodel_exception(self, mock_magnum): - mock_magnum.baymodels.create.side_effect = Exception() - with testtools.ExpectedException( - shade.OpenStackCloudException, - "Error creating baymodel of name fake-baymodel" - ): - self.cloud.create_baymodel('fake-baymodel') - - @mock.patch.object(shade.OpenStackCloud, 'magnum_client') - def test_delete_baymodel(self, mock_magnum): - mock_magnum.baymodels.list.return_value = [baymodel_obj] - self.cloud.delete_baymodel('fake-uuid') - mock_magnum.baymodels.delete.assert_called_once_with( - 'fake-uuid' - ) - - @mock.patch.object(shade.OpenStackCloud, 'magnum_client') - def test_update_baymodel(self, mock_magnum): - new_name = 'new-baymodel' - mock_magnum.baymodels.list.return_value = [baymodel_obj] - self.cloud.update_baymodel('fake-uuid', 'replace', name=new_name) - mock_magnum.baymodels.update.assert_called_once_with( - 'fake-uuid', [{'path': '/name', 'op': 'replace', - 'value': 'new-baymodel'}] - ) diff --git a/shade/tests/unit/test_cluster_templates.py b/shade/tests/unit/test_cluster_templates.py new file mode 100644 index 000000000..8c480e86a --- /dev/null +++ b/shade/tests/unit/test_cluster_templates.py @@ -0,0 +1,169 @@ +# -*- 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. + + +import mock +import munch + +import shade +import testtools +from shade.tests.unit import base + + +cluster_template_obj = munch.Munch( + apiserver_port=None, + uuid='fake-uuid', + human_id=None, + name='fake-cluster-template', + server_type='vm', + public=False, + image_id='fake-image', + tls_disabled=False, + registry_enabled=False, + coe='fake-coe', + keypair_id='fake-key', +) + +cluster_template_detail_obj = munch.Munch( + links={}, + labels={}, + apiserver_port=None, + uuid='fake-uuid', + human_id=None, + name='fake-cluster-template', + server_type='vm', + public=False, + image_id='fake-image', + tls_disabled=False, + registry_enabled=False, + coe='fake-coe', + created_at='fake-date', + updated_at=None, + master_flavor_id=None, + no_proxy=None, + https_proxy=None, + keypair_id='fake-key', + docker_volume_size=1, + external_network_id='public', + cluster_distro='fake-distro', + volume_driver=None, + network_driver='fake-driver', + fixed_network=None, + flavor_id='fake-flavor', + dns_nameserver='8.8.8.8', +) + + +class TestClusterTemplates(base.TestCase): + + def setUp(self): + super(TestClusterTemplates, self).setUp() + self.cloud = shade.openstack_cloud(validate=False) + + @mock.patch.object(shade.OpenStackCloud, 'magnum_client') + def test_list_cluster_templates_without_detail(self, mock_magnum): + mock_magnum.baymodels.list.return_value = [ + cluster_template_obj] + cluster_templates_list = self.cloud.list_cluster_templates() + mock_magnum.baymodels.list.assert_called_with(detail=False) + self.assertEqual(cluster_templates_list[0], cluster_template_obj) + + @mock.patch.object(shade.OpenStackCloud, 'magnum_client') + def test_list_cluster_templates_with_detail(self, mock_magnum): + mock_magnum.baymodels.list.return_value = [ + cluster_template_detail_obj] + cluster_templates_list = self.cloud.list_cluster_templates(detail=True) + mock_magnum.baymodels.list.assert_called_with(detail=True) + self.assertEqual( + cluster_templates_list[0], cluster_template_detail_obj) + + @mock.patch.object(shade.OpenStackCloud, 'magnum_client') + def test_search_cluster_templates_by_name(self, mock_magnum): + mock_magnum.baymodels.list.return_value = [ + cluster_template_obj] + + cluster_templates = self.cloud.search_cluster_templates( + name_or_id='fake-cluster-template') + mock_magnum.baymodels.list.assert_called_with(detail=False) + + self.assertEquals(1, len(cluster_templates)) + self.assertEquals('fake-uuid', cluster_templates[0]['uuid']) + + @mock.patch.object(shade.OpenStackCloud, 'magnum_client') + def test_search_cluster_templates_not_found(self, mock_magnum): + mock_magnum.baymodels.list.return_value = [ + cluster_template_obj] + + cluster_templates = self.cloud.search_cluster_templates( + name_or_id='non-existent') + + mock_magnum.baymodels.list.assert_called_with(detail=False) + self.assertEquals(0, len(cluster_templates)) + + @mock.patch.object(shade.OpenStackCloud, 'search_cluster_templates') + def test_get_cluster_template(self, mock_search): + mock_search.return_value = [cluster_template_obj, ] + r = self.cloud.get_cluster_template('fake-cluster-template') + self.assertIsNotNone(r) + self.assertDictEqual(cluster_template_obj, r) + + @mock.patch.object(shade.OpenStackCloud, 'search_cluster_templates') + def test_get_cluster_template_not_found(self, mock_search): + mock_search.return_value = [] + r = self.cloud.get_cluster_template('doesNotExist') + self.assertIsNone(r) + + @mock.patch.object(shade.OpenStackCloud, 'magnum_client') + def test_create_cluster_template(self, mock_magnum): + self.cloud.create_cluster_template( + name=cluster_template_obj.name, + image_id=cluster_template_obj.image_id, + keypair_id=cluster_template_obj.keypair_id, + coe=cluster_template_obj.coe) + mock_magnum.baymodels.create.assert_called_once_with( + name=cluster_template_obj.name, + image_id=cluster_template_obj.image_id, + keypair_id=cluster_template_obj.keypair_id, + coe=cluster_template_obj.coe + ) + + @mock.patch.object(shade.OpenStackCloud, 'magnum_client') + def test_create_cluster_template_exception(self, mock_magnum): + mock_magnum.baymodels.create.side_effect = Exception() + with testtools.ExpectedException( + shade.OpenStackCloudException, + "Error creating ClusterTemplate of name fake-cluster-template" + ): + self.cloud.create_cluster_template('fake-cluster-template') + + @mock.patch.object(shade.OpenStackCloud, 'magnum_client') + def test_delete_cluster_template(self, mock_magnum): + mock_magnum.baymodels.list.return_value = [ + cluster_template_obj] + self.cloud.delete_cluster_template('fake-uuid') + mock_magnum.baymodels.delete.assert_called_once_with( + 'fake-uuid' + ) + + @mock.patch.object(shade.OpenStackCloud, 'magnum_client') + def test_update_cluster_template(self, mock_magnum): + new_name = 'new-cluster-template' + mock_magnum.baymodels.list.return_value = [ + cluster_template_obj] + self.cloud.update_cluster_template( + 'fake-uuid', 'replace', name=new_name) + mock_magnum.baymodels.update.assert_called_once_with( + 'fake-uuid', [{'path': '/name', 'op': 'replace', + 'value': 'new-cluster-template'}] + ) From a6840f69ff5644065816309776365adccf017772 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 11 Aug 2016 08:14:39 -0500 Subject: [PATCH 0977/3836] Pop domain-id from the config if we infer values If the user specifies a project_{name,id}, then we currently infer that a domain_{name,id} is meant to be shorthand for user_domain_{name,id} and project_domain_{name,id}. However, in other contexts, domain_id is a perfectly valid value to pass to the auth plugins - such as when doing domain-scoped activities. The problem that was uncovered by the correction of argument precedence is that we didn't pop the domain-id out of the root config dict, so if a user set OS_DOMAIN_ID in the environment, we were happily setting the user and project versions, but then leaving domain_id set as well. This then meant that when we do the pass to pull valid arguments from the root dict to the auth dict, we also pulled in domain_id - which led to the error: AuthorizationFailure: Authentication cannot be scoped to multiple targets. Pick one of: project, domain, trust or unscoped Popping the value from the root dict causes the things to work as documented. Change-Id: I6d208e5ec4115d2e72d30b2fedc90a81ea754d5a --- os_client_config/config.py | 1 + os_client_config/tests/test_config.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/os_client_config/config.py b/os_client_config/config.py index 37a31f8d4..7fa670fed 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -562,6 +562,7 @@ def _handle_domain_id(self, cloud): for key in possible_values: if target_key in cloud['auth'] and key not in cloud['auth']: cloud['auth'][key] = cloud['auth'][target_key] + cloud.pop(target_key, None) cloud['auth'].pop(target_key, None) return cloud diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 500887812..baa35cd49 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -102,6 +102,7 @@ def test_get_one_cloud_with_domain_id(self): self.assertEqual('123456789', cc.auth['project_domain_id']) self.assertNotIn('domain_id', cc.auth) self.assertNotIn('domain-id', cc.auth) + self.assertNotIn('domain_id', cc) def test_get_one_cloud_domain_scoped(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], @@ -118,6 +119,7 @@ def test_get_one_cloud_infer_user_domain(self): self.assertEqual('awesome-domain', cc.auth['user_domain_id']) self.assertEqual('awesome-domain', cc.auth['project_domain_id']) self.assertNotIn('domain_id', cc.auth) + self.assertNotIn('domain_id', cc) def test_get_one_cloud_with_hyphenated_project_id(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], From ae25137e36ee044f29ca6db778acc8c77172bc43 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 8 Jul 2016 10:41:41 +0900 Subject: [PATCH 0978/3836] Lay the groundwork for per-resource cache There are currently two caches, the global cache, and the per-resource batch operation caches. The global cache is managed by dogpile. The per-resource caches are managed by hand. However, dogpile has a mode of operation that is precisely what we do in our batched operation per-resource caches by hand. So set ourselves up to have and use per-resource caches. Step one is to make the decorator accept an argument that it'll use to look up the cache. Change-Id: I4fbde2f4528aeaee5c23f0e1d1e4590090acb6ba --- shade/_utils.py | 9 ++++++--- shade/openstackcloud.py | 11 ++++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/shade/_utils.py b/shade/_utils.py index ace3b1cd4..e65f01cf8 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -529,16 +529,19 @@ def func_wrapper(func, *args, **kwargs): def cache_on_arguments(*cache_on_args, **cache_on_kwargs): + _cache_name = cache_on_kwargs.pop('resource', None) + def _inner_cache_on_arguments(func): def _cache_decorator(obj, *args, **kwargs): - the_method = obj._cache.cache_on_arguments( + the_method = obj._get_cache(_cache_name).cache_on_arguments( *cache_on_args, **cache_on_kwargs)( func.__get__(obj, type(obj))) return the_method(*args, **kwargs) def invalidate(obj, *args, **kwargs): - return obj._cache.cache_on_arguments()(func).invalidate( - *args, **kwargs) + return obj._get_cache( + _cache_name).cache_on_arguments()(func).invalidate( + *args, **kwargs) _cache_decorator.invalidate = invalidate _cache_decorator.func = func diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 8d82a2e6f..5aaff661e 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -22,7 +22,7 @@ import time import warnings -from dogpile import cache +import dogpile.cache import requestsexceptions import cinderclient.client @@ -199,7 +199,7 @@ def __init__( if cache_class != 'dogpile.cache.null': self.cache_enabled = True - self._cache = cache.make_region( + self._cache = dogpile.cache.make_region( function_key_generator=self._make_cache_key ).configure( cache_class, @@ -289,6 +289,10 @@ def generate_key(*args, **kwargs): return ans return generate_key + def _get_cache(self, resource_name): + # TODO(mordred) This will eventually be per-resource + return self._cache + def _get_client( self, service_key, client_class, interface_key=None, pass_version_arg=True, **kwargs): @@ -2521,7 +2525,7 @@ def delete_image( for count in _utils._iterate_timeout( timeout, "Timeout waiting for the image to be deleted."): - self._cache.invalidate() + self._get_cache(None).invalidate() if self.get_image(image.id) is None: return @@ -2736,6 +2740,7 @@ def _upload_image_put( image = self._upload_image_put_v1( name, image_data, meta, **image_kwargs) self._cache.invalidate() + self._get_cache(None).invalidate() return self.get_image(image.id) def _upload_image_task( From b02708a9c771f4c605614b98e0f6fa8714826e43 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 8 Jul 2016 11:11:09 +0900 Subject: [PATCH 0979/3836] Create and return per-resource caches As the next step in consolidating per-resource caching, we should actually create more than one dogpile.cache region. We should only do that if the resource is a thing we actually are going to cache (which means it has a list_ method) Finally, we should return it if requested, otherwise we should return the good old self._cache global cache. Change-Id: Id0a5f65437a8bb4b911eddf54f14369d6b79d3fd --- shade/openstackcloud.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 5aaff661e..f0dffc3bf 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -197,14 +197,20 @@ def __init__( cache_class = cloud_config.get_cache_class() cache_arguments = cloud_config.get_cache_arguments() + self._resource_caches = {} + if cache_class != 'dogpile.cache.null': self.cache_enabled = True - self._cache = dogpile.cache.make_region( - function_key_generator=self._make_cache_key - ).configure( - cache_class, - expiration_time=cache_expiration_time, - arguments=cache_arguments) + self._cache = self._make_cache( + cache_class, cache_expiration_time, cache_arguments) + expirations = cloud_config.get_cache_expiration() + for expire_key in expirations.keys(): + # Only build caches for things we have list operations for + if getattr( + self, 'list_{0}'.format(expire_key), None): + self._resource_caches[expire_key] = self._make_cache( + cache_class, expirations[expire_key], cache_arguments) + self._SERVER_AGE = DEFAULT_SERVER_AGE self._PORT_AGE = DEFAULT_PORT_AGE else: @@ -272,6 +278,14 @@ def invalidate(self): self.cloud_config = cloud_config + def _make_cache(self, cache_class, expiration_time, arguments): + return dogpile.cache.make_region( + function_key_generator=self._make_cache_key + ).configure( + cache_class, + expiration_time=expiration_time, + arguments=arguments) + def _make_cache_key(self, namespace, fn): fname = fn.__name__ if namespace is None: @@ -290,8 +304,10 @@ def generate_key(*args, **kwargs): return generate_key def _get_cache(self, resource_name): - # TODO(mordred) This will eventually be per-resource - return self._cache + if resource_name and resource_name in self._resource_caches: + return self._resource_caches[resource_name] + else: + return self._cache def _get_client( self, service_key, client_class, interface_key=None, From 02dd6d7266ff23073a5bb3dc144a4c1ec074022b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 8 Jul 2016 11:44:05 +0900 Subject: [PATCH 0980/3836] Move list_ports to using dogpile.cache A new system is nothing without a good example. Migrate list_ports to the async_creation_runner interface in dogpile.cache. Change-Id: I3a26e63f736cd4fca30efe774fc96fa40b5b31c4 --- shade/_utils.py | 14 +++++++++++ shade/openstackcloud.py | 55 ++++++++++++++--------------------------- 2 files changed, 33 insertions(+), 36 deletions(-) diff --git a/shade/_utils.py b/shade/_utils.py index e65f01cf8..c88c4e9a5 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -45,6 +45,12 @@ def _iterate_timeout(timeout, message, wait=2): """ try: + # None as a wait winds up flowing well in the per-resource cache + # flow. We could spread this logic around to all of the calling + # points, but just having this treat None as "I don't have a value" + # seems friendlier + if wait is None: + wait = 2 wait = float(wait) except ValueError: raise exc.OpenStackCloudException( @@ -528,6 +534,14 @@ def func_wrapper(func, *args, **kwargs): return func_wrapper +def async_creation_runner(cache, somekey, creator, mutex): + try: + value = creator() + cache.set(somekey, value) + finally: + mutex.release() + + def cache_on_arguments(*cache_on_args, **cache_on_kwargs): _cache_name = cache_on_kwargs.pop('resource', None) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index f0dffc3bf..adea6a144 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -64,7 +64,6 @@ # This halves the current default for Swift DEFAULT_MAX_FILE_SIZE = (5 * 1024 * 1024 * 1024 + 2) / 2 DEFAULT_SERVER_AGE = 5 -DEFAULT_PORT_AGE = 5 OBJECT_CONTAINER_ACLS = { @@ -186,10 +185,6 @@ def __init__( self._servers_time = 0 self._servers_lock = threading.Lock() - self._ports = [] - self._ports_time = 0 - self._ports_lock = threading.Lock() - self._networks_lock = threading.Lock() self._reset_network_caches() @@ -209,10 +204,10 @@ def __init__( if getattr( self, 'list_{0}'.format(expire_key), None): self._resource_caches[expire_key] = self._make_cache( - cache_class, expirations[expire_key], cache_arguments) + cache_class, expirations[expire_key], cache_arguments, + async_creation_runner=_utils.async_creation_runner) self._SERVER_AGE = DEFAULT_SERVER_AGE - self._PORT_AGE = DEFAULT_PORT_AGE else: self.cache_enabled = False @@ -227,7 +222,6 @@ def invalidate(self): # Replace this with a more specific cache configuration # soon. self._SERVER_AGE = 0 - self._PORT_AGE = 0 self._cache = _FakeCache() # Undecorate cache decorated methods. Otherwise the call stacks # wind up being stupidly long and hard to debug @@ -245,8 +239,6 @@ def invalidate(self): # fall back to whatever it was before self._SERVER_AGE = cloud_config.get_cache_resource_expiration( 'server', self._SERVER_AGE) - self._PORT_AGE = cloud_config.get_cache_resource_expiration( - 'port', self._PORT_AGE) self._container_cache = dict() self._file_hash_cache = dict() @@ -278,9 +270,12 @@ def invalidate(self): self.cloud_config = cloud_config - def _make_cache(self, cache_class, expiration_time, arguments): + def _make_cache( + self, cache_class, expiration_time, arguments, + async_creation_runner=None): return dogpile.cache.make_region( - function_key_generator=self._make_cache_key + function_key_generator=self._make_cache_key, + async_creation_runner=async_creation_runner ).configure( cache_class, expiration_time=expiration_time, @@ -303,6 +298,10 @@ def generate_key(*args, **kwargs): return ans return generate_key + def _get_cache_time(self, resource_name): + return self.cloud_config.get_cache_resource_expiration( + resource_name, None) + def _get_cache(self, resource_name): if resource_name and resource_name in self._resource_caches: return self._resource_caches[resource_name] @@ -1213,7 +1212,7 @@ def search_ports(self, name_or_id=None, filters=None): # If port caching is enabled, do not push the filter down to # neutron; get all the ports (potentially from the cache) and # filter locally. - if self._PORT_AGE: + if self._get_cache_time('port'): pushdown_filters = None else: pushdown_filters = filters @@ -1340,6 +1339,7 @@ def list_subnets(self, filters=None): return self.manager.submitTask( _tasks.SubnetList(**filters))['subnets'] + @_utils.cache_on_arguments(resource='ports') def list_ports(self, filters=None): """List all available ports. @@ -1347,27 +1347,9 @@ def list_ports(self, filters=None): :returns: A list of port ``munch.Munch``. """ - # If pushdown filters are specified, bypass local caching. - if filters: - return self._list_ports(filters) # Translate None from search interface to empty {} for kwargs below - filters = {} - if (time.time() - self._ports_time) >= self._PORT_AGE: - # Since we're using cached data anyway, we don't need to - # have more than one thread actually submit the list - # ports task. Let the first one submit it while holding - # a lock, and the non-blocking acquire method will cause - # subsequent threads to just skip this and use the old - # data until it succeeds. - if self._ports_lock.acquire(False): - try: - self._ports = self._list_ports(filters) - self._ports_time = time.time() - finally: - self._ports_lock.release() - return self._ports - - def _list_ports(self, filters): + if not filters: + filters = {} with _utils.neutron_exceptions("Error fetching port list"): return self.manager.submitTask( _tasks.PortList(**filters))['ports'] @@ -3755,14 +3737,15 @@ def _get_free_fixed_port(self, server, fixed_address=None, # If we are caching port lists, we may not find the port for # our server if the list is old. Try for at least 2 cache # periods if that is the case. - if self._PORT_AGE: - timeout = self._PORT_AGE * 2 + port_expire_time = self._get_cache_time('ports') + if port_expire_time: + timeout = port_expire_time * 2 else: timeout = None for count in _utils._iterate_timeout( timeout, "Timeout waiting for port to show up in list", - wait=self._PORT_AGE): + wait=port_expire_time): try: port_filter = {'device_id': server['id']} ports = self.search_ports(filters=port_filter) From acf010f463cb583d073113f17d88864c94ef7503 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 12 Aug 2016 07:02:11 -0500 Subject: [PATCH 0981/3836] Don't supplement floating ip list on clouds without Some clouds don't have floating ips at all. We know who they are. Don't try to supplement them. Change-Id: Ib4965ab53f4142811313481cb4e7a70aeeea5b48 --- shade/meta.py | 16 ++++++++--- shade/openstackcloud.py | 6 ++++ shade/tests/unit/test_meta.py | 53 +++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 4 deletions(-) diff --git a/shade/meta.py b/shade/meta.py index 158750bb2..a0abaada3 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -296,10 +296,18 @@ def _get_suplemental_addresses(cloud, server): # We have a floating IP that nova knows about, do nothing return server['addresses'] fixed_ip_mapping[address['addr']] = name - for fip in cloud.list_floating_ips(): - if fip['fixed_ip_address'] in fixed_ip_mapping: - fixed_net = fixed_ip_mapping[fip['fixed_ip_address']] - server['addresses'][fixed_net].append(_make_address_dict(fip)) + try: + if cloud._has_floating_ips(): + for fip in cloud.list_floating_ips(): + if fip['fixed_ip_address'] in fixed_ip_mapping: + fixed_net = fixed_ip_mapping[fip['fixed_ip_address']] + server['addresses'][fixed_net].append( + _make_address_dict(fip)) + except exc.OpenStackCloudException: + # If something goes wrong with a cloud call, that's cool - this is + # an attempt to provide additional data and should not block forward + # progress + pass return server['addresses'] diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 8d82a2e6f..9051c3b65 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1772,6 +1772,12 @@ def get_internal_networks(self): self._find_interesting_networks() return self._internal_networks + def _has_floating_ips(self): + if not self._floating_ip_source: + return False + else: + return self._floating_ip_source.lower() in ('nova', 'neutron') + def _use_neutron_floating(self): return (self.has_service('network') and self._floating_ip_source == 'neutron') diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 406903bf5..6082b6337 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -235,6 +235,59 @@ def test_get_server_private_ip_devstack( mock_list_networks.assert_called_once_with() mock_list_floating_ips.assert_called_once_with() + @mock.patch.object(shade.OpenStackCloud, 'list_floating_ips') + @mock.patch.object(shade.OpenStackCloud, 'list_subnets') + @mock.patch.object(shade.OpenStackCloud, 'list_server_security_groups') + @mock.patch.object(shade.OpenStackCloud, 'get_volumes') + @mock.patch.object(shade.OpenStackCloud, 'get_image_name') + @mock.patch.object(shade.OpenStackCloud, 'get_flavor_name') + @mock.patch.object(shade.OpenStackCloud, 'has_service') + @mock.patch.object(shade.OpenStackCloud, 'list_networks') + def test_get_server_private_ip_no_fip( + self, mock_list_networks, mock_has_service, + mock_get_flavor_name, mock_get_image_name, + mock_get_volumes, + mock_list_server_security_groups, + mock_list_subnets, + mock_list_floating_ips): + self.cloud._floating_ip_source = 'none' + mock_get_image_name.return_value = 'cirros-0.3.4-x86_64-uec' + mock_get_flavor_name.return_value = 'm1.tiny' + mock_has_service.return_value = True + mock_get_volumes.return_value = [] + mock_list_subnets.return_value = SUBNETS_WITH_NAT + mock_list_networks.return_value = [ + { + 'id': 'test_pnztt_net', + 'name': 'test_pnztt_net', + 'router:external': False, + }, + { + 'id': 'private', + 'name': 'private', + }, + ] + + srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( + id='test-id', name='test-name', status='ACTIVE', + flavor={u'id': u'1'}, + image={ + 'name': u'cirros-0.3.4-x86_64-uec', + u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1'}, + addresses={u'test_pnztt_net': [{ + u'OS-EXT-IPS:type': u'fixed', + u'addr': PRIVATE_V4, + u'version': 4, + u'OS-EXT-IPS-MAC:mac_addr': + u'fa:16:3e:ae:7d:42' + }]} + ))) + + self.assertEqual(PRIVATE_V4, srv['private_v4']) + mock_has_service.assert_called_with('volume') + mock_list_networks.assert_called_once_with() + mock_list_floating_ips.assert_not_called() + @mock.patch.object(shade.OpenStackCloud, 'has_service') @mock.patch.object(shade.OpenStackCloud, 'list_subnets') @mock.patch.object(shade.OpenStackCloud, 'list_networks') From cca0fb3e0a1e28cef819505870f8609759b17698 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 12 Aug 2016 06:48:19 -0500 Subject: [PATCH 0982/3836] Do not instantiate logging on import If we run setup_logging in the file context, it runs setup things for loggers. That can cause unintended consequences in consuming programs. Change-Id: Ibc751703b1aa37cfbd6f95adbafb97ff12b7d5e6 --- shade/_utils.py | 5 +---- shade/exc.py | 6 +++--- shade/meta.py | 3 --- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/shade/_utils.py b/shade/_utils.py index ace3b1cd4..198c81a3f 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -28,10 +28,6 @@ from shade import exc from shade import meta - -log = _log.setup_logging(__name__) - - _decorated_methods = [] @@ -43,6 +39,7 @@ def _iterate_timeout(timeout, message, wait=2): with . """ + log = _log.setup_logging(__name__) try: wait = float(wait) diff --git a/shade/exc.py b/shade/exc.py index 39e742f61..a2a95032d 100644 --- a/shade/exc.py +++ b/shade/exc.py @@ -16,8 +16,6 @@ from shade import _log -log = _log.setup_logging(__name__) - class OpenStackCloudException(Exception): @@ -32,7 +30,9 @@ def __init__(self, message, extra_data=None): self.inner_exception = sys.exc_info() self.orig_message = message - def log_error(self, logger=log): + def log_error(self, logger=None): + if not logger: + logger = _log.setup_logging(__name__) if self.inner_exception and self.inner_exception[1]: logger.error(self.orig_message, exc_info=self.inner_exception) diff --git a/shade/meta.py b/shade/meta.py index 158750bb2..ff0f75d2e 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -18,13 +18,10 @@ import six from shade import exc -from shade import _log NON_CALLABLES = (six.string_types, bool, dict, int, float, list, type(None)) -log = _log.setup_logging(__name__) - def find_nova_addresses(addresses, ext_tag=None, key_name=None, version=4): From 71a7058a2d9764eb5fe33ae1400a18c8d1581bce Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 12 Aug 2016 07:28:30 -0500 Subject: [PATCH 0983/3836] Protect cinderclient import Importing cinderclient breaks logging setups. Luckily, we only need it in one function, so late-import it. Change-Id: I86f090e8f3bbd3ac57e221df6629c6e008c99aa8 --- shade/openstackcloud.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 8d82a2e6f..a9ce51206 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -25,7 +25,6 @@ from dogpile import cache import requestsexceptions -import cinderclient.client import cinderclient.exceptions as cinder_exceptions import glanceclient import glanceclient.exc @@ -847,6 +846,9 @@ def swift_service(self): @property def cinder_client(self): + # Import cinderclient late because importing it at the top level + # breaks logging for users of shade + import cinderclient.client if self._cinder_client is None: self._cinder_client = self._get_client( 'volume', cinderclient.client.Client) From 3326bb7706e45de2c00937a0e478012faf555ca3 Mon Sep 17 00:00:00 2001 From: SamYaple Date: Sun, 6 Mar 2016 16:38:09 +0000 Subject: [PATCH 0984/3836] Add update_endpoint() v3 allows us to update the endpoints, we should do that. The other endpoint calls have been validated against v3 api to do what we want them to do. All comments regarding implementing v3 keystone have been removed. Change-Id: Icae8a02815b1416a60bc8c6d57ee892fbfd7ccc4 --- .../update_endpoint-f87c1f42d0c0d1ef.yaml | 8 +++++ shade/_tasks.py | 5 +++ shade/operatorcloud.py | 26 ++++++++++++-- shade/tests/functional/test_endpoints.py | 34 ++++++++++++++++++ shade/tests/unit/test_endpoints.py | 36 +++++++++++++++++++ 5 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/update_endpoint-f87c1f42d0c0d1ef.yaml diff --git a/releasenotes/notes/update_endpoint-f87c1f42d0c0d1ef.yaml b/releasenotes/notes/update_endpoint-f87c1f42d0c0d1ef.yaml new file mode 100644 index 000000000..a7b6a458b --- /dev/null +++ b/releasenotes/notes/update_endpoint-f87c1f42d0c0d1ef.yaml @@ -0,0 +1,8 @@ +--- +features: + - Added update_endpoint as a new function that allows + the user to update a created endpoint with new values + rather than deleting and recreating that endpoint. + This feature only works with keystone v3, with v2 it + will raise an exception stating the feature is not + available. diff --git a/shade/_tasks.py b/shade/_tasks.py index 47ffd9538..238eafe20 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -724,6 +724,11 @@ def main(self, client): return client.keystone_client.endpoints.create(**self.args) +class EndpointUpdate(task_manager.Task): + def main(self, client): + return client.keystone_client.endpoints.update(**self.args) + + class EndpointList(task_manager.Task): def main(self, client): return client.keystone_client.endpoints.list() diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 77c63c38a..c93cabab8 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -980,6 +980,26 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, endpoints.append(endpoint) return endpoints + @_utils.valid_kwargs('enabled', 'service_name_or_id', 'url', 'interface', + 'region') + def update_endpoint(self, endpoint_id, **kwargs): + # NOTE(SamYaple): Endpoint updates are only available on v3 api + if self.cloud_config.get_api_version('identity').startswith('2'): + raise OpenStackCloudUnavailableFeature( + 'Unavailable Feature: Endpoint update' + ) + + service_name_or_id = kwargs.pop('service_name_or_id', None) + if service_name_or_id is not None: + kwargs['service'] = service_name_or_id + + with _utils.shade_exceptions( + "Failed to update endpoint {}".format(endpoint_id) + ): + return self.manager.submitTask(_tasks.EndpointUpdate( + endpoint=endpoint_id, **kwargs + )) + def list_endpoints(self): """List Keystone endpoints. @@ -988,7 +1008,10 @@ def list_endpoints(self): :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ - # ToDo: support v3 api (dguerri) + # NOTE(SamYaple): With keystone v3 we can filter directly via the + # the keystone api, but since the return of all the endpoints even in + # large environments is small, we can continue to filter in shade just + # like the v2 api. with _utils.shade_exceptions("Failed to list endpoints"): endpoints = self.manager.submitTask(_tasks.EndpointList()) @@ -1042,7 +1065,6 @@ def delete_endpoint(self, id): :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call. """ - # ToDo: support v3 api (dguerri) endpoint = self.get_endpoint(id=id) if endpoint is None: self.log.debug("Endpoint %s not found for deleting" % id) diff --git a/shade/tests/functional/test_endpoints.py b/shade/tests/functional/test_endpoints.py index ca79f855f..e7346fe64 100644 --- a/shade/tests/functional/test_endpoints.py +++ b/shade/tests/functional/test_endpoints.py @@ -25,6 +25,7 @@ import random from shade.exc import OpenStackCloudException +from shade.exc import OpenStackCloudUnavailableFeature from shade.tests.functional import base @@ -101,6 +102,39 @@ def test_create_endpoint(self): self.assertNotEqual([], endpoints) self.assertIsNotNone(endpoints[0].get('id')) + def test_update_endpoint(self): + if self.operator_cloud.cloud_config.get_api_version( + 'identity').startswith('2'): + # NOTE(SamYaple): Update endpoint only works with v3 api + self.assertRaises(OpenStackCloudUnavailableFeature, + self.operator_cloud.update_endpoint, + 'endpoint_id1') + else: + service = self.operator_cloud.create_service( + name='service1', type='test_type') + endpoint = self.operator_cloud.create_endpoint( + service_name_or_id=service['id'], + url='http://admin.url/', + interface='admin', + region='orig_region', + enabled=False)[0] + + new_service = self.operator_cloud.create_service( + name='service2', type='test_type') + new_endpoint = self.operator_cloud.update_endpoint( + endpoint.id, + service_name_or_id=new_service.id, + url='http://public.url/', + interface='public', + region='update_region', + enabled=True) + + self.assertEqual(new_endpoint.url, 'http://public.url/') + self.assertEqual(new_endpoint.interface, 'public') + self.assertEqual(new_endpoint.region, 'update_region') + self.assertEqual(new_endpoint.service_id, new_service.id) + self.assertTrue(new_endpoint.enabled) + def test_list_endpoints(self): service_name = self.new_item_name + '_list' diff --git a/shade/tests/unit/test_endpoints.py b/shade/tests/unit/test_endpoints.py index a0c596903..1d85eebfa 100644 --- a/shade/tests/unit/test_endpoints.py +++ b/shade/tests/unit/test_endpoints.py @@ -23,6 +23,7 @@ import os_client_config from shade import OperatorCloud from shade.exc import OpenStackCloudException +from shade.exc import OpenStackCloudUnavailableFeature from shade.tests.fakes import FakeEndpoint from shade.tests.fakes import FakeEndpointv3 from shade.tests.unit import base @@ -170,6 +171,41 @@ def test_create_endpoint_v3(self, mock_api_version, mock_keystone_client, for k, v in self.mock_endpoints_v3[count].items(): self.assertEquals(v, endpoints_2on3[count].get(k)) + @patch.object(os_client_config.cloud_config.CloudConfig, 'get_api_version') + def test_update_endpoint_v2(self, mock_api_version): + mock_api_version.return_value = '2.0' + # NOTE(SamYaple): Update endpoint only works with v3 api + self.assertRaises(OpenStackCloudUnavailableFeature, + self.client.update_endpoint, 'endpoint_id') + + @patch.object(OperatorCloud, 'keystone_client') + @patch.object(os_client_config.cloud_config.CloudConfig, 'get_api_version') + def test_update_endpoint_v3(self, mock_api_version, mock_keystone_client): + mock_api_version.return_value = '3' + mock_keystone_client.endpoints.update.return_value = \ + self.mock_ks_endpoints_v3[0] + + endpoint = self.client.update_endpoint( + 'id1', + service_name_or_id='service_id1', + region='mock_region', + url='mock_url', + interface='mock_interface', + enabled=False + ) + mock_keystone_client.endpoints.update.assert_called_with( + endpoint='id1', + service='service_id1', + region='mock_region', + url='mock_url', + interface='mock_interface', + enabled=False + ) + + # test keys and values are correct + for k, v in self.mock_endpoints_v3[0].items(): + self.assertEquals(v, endpoint.get(k)) + @patch.object(OperatorCloud, 'keystone_client') def test_list_endpoints(self, mock_keystone_client): mock_keystone_client.endpoints.list.return_value = \ From bd3f07140d4ebfa62f63d8ad170da5e43d7bbb74 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 8 Aug 2016 11:09:24 -0500 Subject: [PATCH 0985/3836] Infer nova-net security groups better If a cloud does not have neutron, it's entirely possible to infer that security groups should go through nova. While it's currently possible to set secgroup_source: nova in clouds.yaml or in a vendor file, it's a little overkill for folks who otherwise do not need to express that level of complexity. Change-Id: I8e4dec2cf993a47c8a0d5cb55bbf80b0fdc4dd46 --- ...nfer-secgroup-source-58d840aaf1a1f485.yaml | 9 ++ shade/openstackcloud.py | 107 ++++++++++-------- shade/tests/unit/test_security_groups.py | 24 ++++ 3 files changed, 91 insertions(+), 49 deletions(-) create mode 100644 releasenotes/notes/infer-secgroup-source-58d840aaf1a1f485.yaml diff --git a/releasenotes/notes/infer-secgroup-source-58d840aaf1a1f485.yaml b/releasenotes/notes/infer-secgroup-source-58d840aaf1a1f485.yaml new file mode 100644 index 000000000..f3f35f480 --- /dev/null +++ b/releasenotes/notes/infer-secgroup-source-58d840aaf1a1f485.yaml @@ -0,0 +1,9 @@ +--- +features: + - If a cloud does not have a neutron service, it is now + assumed that Nova will be the source of security groups. + To handle clouds that have nova-network and do not have + the security group extension, setting secgroup_source to + None will prevent attempting to use them at all. If the + cloud has neutron but it is not a functional source of + security groups, set secgroup_source to nova. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 5bbb2fcc2..39a7465ac 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1414,7 +1414,7 @@ def list_server_security_groups(self, server): """ # Don't even try if we're a cloud that doesn't have them - if self.secgroup_source not in ('nova', 'neutron'): + if not self._has_secgroups(): return [] with _utils.shade_exceptions(): @@ -1429,8 +1429,14 @@ def list_security_groups(self): :returns: A list of security group ``munch.Munch``. """ + # Security groups not supported + if not self._has_secgroups(): + raise OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + # Handle neutron security groups - if self.secgroup_source == 'neutron': + if self._use_neutron_secgroups(): # Neutron returns dicts, so no need to convert objects here. with _utils.neutron_exceptions( "Error fetching security group list"): @@ -1438,18 +1444,12 @@ def list_security_groups(self): _tasks.NeutronSecurityGroupList())['security_groups'] # Handle nova security groups - elif self.secgroup_source == 'nova': + else: with _utils.shade_exceptions("Error fetching security group list"): groups = self.manager.submitTask( _tasks.NovaSecurityGroupList()) return _utils.normalize_nova_secgroups(groups) - # Security groups not supported - else: - raise OpenStackCloudUnavailableFeature( - "Unavailable feature: security groups" - ) - def list_servers(self, detailed=False): """List all available servers. @@ -1776,6 +1776,16 @@ def _use_neutron_floating(self): return (self.has_service('network') and self._floating_ip_source == 'neutron') + def _has_secgroups(self): + if not self.secgroup_source: + return False + else: + return self.secgroup_source.lower() in ('nova', 'neutron') + + def _use_neutron_secgroups(self): + return (self.has_service('network') + and self.secgroup_source == 'neutron') + def get_keypair(self, name_or_id, filters=None): """Get a keypair by name or ID. @@ -5244,7 +5254,14 @@ def create_security_group(self, name, description): :raises: OpenStackCloudUnavailableFeature if security groups are not supported on this cloud. """ - if self.secgroup_source == 'neutron': + + # Security groups not supported + if not self._has_secgroups(): + raise OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + + if self._use_neutron_secgroups(): with _utils.neutron_exceptions( "Error creating security group {0}".format(name)): group = self.manager.submitTask( @@ -5255,7 +5272,7 @@ def create_security_group(self, name, description): ) return group['security_group'] - elif self.secgroup_source == 'nova': + else: with _utils.shade_exceptions( "Failed to create security group '{name}'".format( name=name)): @@ -5266,12 +5283,6 @@ def create_security_group(self, name, description): ) return _utils.normalize_nova_secgroups([group])[0] - # Security groups not supported - else: - raise OpenStackCloudUnavailableFeature( - "Unavailable feature: security groups" - ) - def delete_security_group(self, name_or_id): """Delete a security group @@ -5283,13 +5294,19 @@ def delete_security_group(self, name_or_id): :raises: OpenStackCloudUnavailableFeature if security groups are not supported on this cloud. """ + # Security groups not supported + if not self._has_secgroups(): + raise OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + secgroup = self.get_security_group(name_or_id) if secgroup is None: self.log.debug('Security group %s not found for deleting' % name_or_id) return False - if self.secgroup_source == 'neutron': + if self._use_neutron_secgroups(): with _utils.neutron_exceptions( "Error deleting security group {0}".format(name_or_id)): self.manager.submitTask( @@ -5299,7 +5316,7 @@ def delete_security_group(self, name_or_id): ) return True - elif self.secgroup_source == 'nova': + else: with _utils.shade_exceptions( "Failed to delete security group '{group}'".format( group=name_or_id)): @@ -5308,12 +5325,6 @@ def delete_security_group(self, name_or_id): ) return True - # Security groups not supported - else: - raise OpenStackCloudUnavailableFeature( - "Unavailable feature: security groups" - ) - @_utils.valid_kwargs('name', 'description') def update_security_group(self, name_or_id, **kwargs): """Update a security group @@ -5326,13 +5337,19 @@ def update_security_group(self, name_or_id, **kwargs): :raises: OpenStackCloudException on operation error. """ + # Security groups not supported + if not self._has_secgroups(): + raise OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + secgroup = self.get_security_group(name_or_id) if secgroup is None: raise OpenStackCloudException( "Security group %s not found." % name_or_id) - if self.secgroup_source == 'neutron': + if self._use_neutron_secgroups(): with _utils.neutron_exceptions( "Error updating security group {0}".format(name_or_id)): group = self.manager.submitTask( @@ -5342,7 +5359,7 @@ def update_security_group(self, name_or_id, **kwargs): ) return group['security_group'] - elif self.secgroup_source == 'nova': + else: with _utils.shade_exceptions( "Failed to update security group '{group}'".format( group=name_or_id)): @@ -5352,12 +5369,6 @@ def update_security_group(self, name_or_id, **kwargs): ) return _utils.normalize_nova_secgroups([group])[0] - # Security groups not supported - else: - raise OpenStackCloudUnavailableFeature( - "Unavailable feature: security groups" - ) - def create_security_group_rule(self, secgroup_name_or_id, port_range_min=None, @@ -5409,13 +5420,18 @@ def create_security_group_rule(self, :raises: OpenStackCloudException on operation error. """ + # Security groups not supported + if not self._has_secgroups(): + raise OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) secgroup = self.get_security_group(secgroup_name_or_id) if not secgroup: raise OpenStackCloudException( "Security group %s not found." % secgroup_name_or_id) - if self.secgroup_source == 'neutron': + if self._use_neutron_secgroups(): # NOTE: Nova accepts -1 port numbers, but Neutron accepts None # as the equivalent value. rule_def = { @@ -5439,7 +5455,7 @@ def create_security_group_rule(self, ) return rule['security_group_rule'] - elif self.secgroup_source == 'nova': + else: # NOTE: Neutron accepts None for protocol. Nova does not. if protocol is None: raise OpenStackCloudException('Protocol must be specified') @@ -5482,12 +5498,6 @@ def create_security_group_rule(self, ) return _utils.normalize_nova_secgroup_rules([rule])[0] - # Security groups not supported - else: - raise OpenStackCloudUnavailableFeature( - "Unavailable feature: security groups" - ) - def delete_security_group_rule(self, rule_id): """Delete a security group rule @@ -5499,8 +5509,13 @@ def delete_security_group_rule(self, rule_id): :raises: OpenStackCloudUnavailableFeature if security groups are not supported on this cloud. """ + # Security groups not supported + if not self._has_secgroups(): + raise OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) - if self.secgroup_source == 'neutron': + if self._use_neutron_secgroups(): try: with _utils.neutron_exceptions( "Error deleting security group rule " @@ -5513,7 +5528,7 @@ def delete_security_group_rule(self, rule_id): return False return True - elif self.secgroup_source == 'nova': + else: try: self.manager.submitTask( _tasks.NovaSecurityGroupRuleDelete(rule=rule_id) @@ -5528,12 +5543,6 @@ def delete_security_group_rule(self, rule_id): id=rule_id, msg=str(e))) return True - # Security groups not supported - else: - raise OpenStackCloudUnavailableFeature( - "Unavailable feature: security groups" - ) - def list_zones(self): """List all available zones. diff --git a/shade/tests/unit/test_security_groups.py b/shade/tests/unit/test_security_groups.py index 4012c3206..9bfc1946c 100644 --- a/shade/tests/unit/test_security_groups.py +++ b/shade/tests/unit/test_security_groups.py @@ -55,6 +55,14 @@ class TestSecurityGroups(base.TestCase): + def setUp(self): + super(TestSecurityGroups, self).setUp() + self.has_neutron = True + + def fake_has_service(*args, **kwargs): + return self.has_neutron + self.cloud.has_service = fake_has_service + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_list_security_groups_neutron(self, mock_nova, mock_neutron): @@ -67,6 +75,7 @@ def test_list_security_groups_neutron(self, mock_nova, mock_neutron): @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_list_security_groups_nova(self, mock_nova, mock_neutron): self.cloud.secgroup_source = 'nova' + self.has_neutron = False self.cloud.list_security_groups() self.assertFalse(mock_neutron.list_security_groups.called) self.assertTrue(mock_nova.security_groups.list.called) @@ -75,6 +84,7 @@ def test_list_security_groups_nova(self, mock_nova, mock_neutron): @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_list_security_groups_none(self, mock_nova, mock_neutron): self.cloud.secgroup_source = None + self.has_neutron = False self.assertRaises(shade.OpenStackCloudUnavailableFeature, self.cloud.list_security_groups) self.assertFalse(mock_neutron.list_security_groups.called) @@ -93,6 +103,7 @@ def test_delete_security_group_neutron(self, mock_neutron): @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_delete_security_group_nova(self, mock_nova): self.cloud.secgroup_source = 'nova' + self.has_neutron = False nova_return = [nova_grp_obj] mock_nova.security_groups.list.return_value = nova_return self.cloud.delete_security_group('2') @@ -111,6 +122,7 @@ def test_delete_security_group_neutron_not_found(self, mock_neutron): @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_delete_security_group_nova_not_found(self, mock_nova): self.cloud.secgroup_source = 'nova' + self.has_neutron = False nova_return = [nova_grp_obj] mock_nova.security_groups.list.return_value = nova_return self.cloud.delete_security_group('doesNotExist') @@ -140,6 +152,7 @@ def test_create_security_group_neutron(self, mock_neutron): @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_create_security_group_nova(self, mock_nova): group_name = self.getUniqueString() + self.has_neutron = False group_desc = 'security group from test_create_security_group_neutron' new_group = fakes.FakeSecgroup(id='2', name=group_name, @@ -159,6 +172,7 @@ def test_create_security_group_nova(self, mock_nova): @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_create_security_group_none(self, mock_nova, mock_neutron): self.cloud.secgroup_source = None + self.has_neutron = False self.assertRaises(shade.OpenStackCloudUnavailableFeature, self.cloud.create_security_group, '', '') @@ -178,6 +192,7 @@ def test_update_security_group_neutron(self, mock_neutron): @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_update_security_group_nova(self, mock_nova): + self.has_neutron = False new_name = self.getUniqueString() self.cloud.secgroup_source = 'nova' nova_return = [nova_grp_obj] @@ -228,6 +243,7 @@ def test_create_security_group_rule_neutron(self, mock_neutron, mock_get): @mock.patch.object(shade.OpenStackCloud, 'get_security_group') @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_create_security_group_rule_nova(self, mock_nova, mock_get): + self.has_neutron = False self.cloud.secgroup_source = 'nova' new_rule = fakes.FakeNovaSecgroupRule( @@ -249,6 +265,7 @@ def test_create_security_group_rule_nova(self, mock_nova, mock_get): @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_create_security_group_rule_nova_no_ports(self, mock_nova, mock_get): + self.has_neutron = False self.cloud.secgroup_source = 'nova' new_rule = fakes.FakeNovaSecgroupRule( @@ -269,6 +286,7 @@ def test_create_security_group_rule_nova_no_ports(self, @mock.patch.object(shade.OpenStackCloud, 'neutron_client') @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_create_security_group_rule_none(self, mock_nova, mock_neutron): + self.has_neutron = False self.cloud.secgroup_source = None self.assertRaises(shade.OpenStackCloudUnavailableFeature, self.cloud.create_security_group_rule, @@ -286,6 +304,7 @@ def test_delete_security_group_rule_neutron(self, mock_neutron): @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_delete_security_group_rule_nova(self, mock_nova): + self.has_neutron = False self.cloud.secgroup_source = 'nova' r = self.cloud.delete_security_group_rule('xyz') mock_nova.security_group_rules.delete.assert_called_once_with( @@ -295,6 +314,7 @@ def test_delete_security_group_rule_nova(self, mock_nova): @mock.patch.object(shade.OpenStackCloud, 'neutron_client') @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_delete_security_group_rule_none(self, mock_nova, mock_neutron): + self.has_neutron = False self.cloud.secgroup_source = None self.assertRaises(shade.OpenStackCloudUnavailableFeature, self.cloud.delete_security_group_rule, @@ -314,6 +334,7 @@ def test_delete_security_group_rule_not_found(self, r = self.cloud.delete_security_group('doesNotExist') self.assertFalse(r) + self.has_neutron = False self.cloud.secgroup_source = 'nova' mock_neutron.security_group_rules.delete.side_effect = ( nova_exc.NotFound("uh oh") @@ -323,6 +344,7 @@ def test_delete_security_group_rule_not_found(self, @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_nova_egress_security_group_rule(self, mock_nova): + self.has_neutron = False self.cloud.secgroup_source = 'nova' mock_nova.security_groups.list.return_value = [nova_grp_obj] self.assertRaises(shade.OpenStackCloudException, @@ -333,6 +355,7 @@ def test_nova_egress_security_group_rule(self, mock_nova): @mock.patch.object(shade._utils, 'normalize_nova_secgroups') @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_list_server_security_groups(self, mock_nova, mock_norm): + self.has_neutron = False server = dict(id='server_id') self.cloud.list_server_security_groups(server) mock_nova.servers.list_security_group.assert_called_once_with( @@ -342,6 +365,7 @@ def test_list_server_security_groups(self, mock_nova, mock_norm): @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_list_server_security_groups_bad_source(self, mock_nova): + self.has_neutron = False self.cloud.secgroup_source = 'invalid' server = dict(id='server_id') ret = self.cloud.list_server_security_groups(server) From 273ea8c1ebe3cdb3f232494498781339b8ef4226 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 12 Aug 2016 16:10:08 -0500 Subject: [PATCH 0986/3836] Deal with clouds that don't have fips betterer In the code that supplements the nova provided floating IP dict by checking neutron, we actually don't properly account for the case where the cloud just flat doesn't have floating ips. Change-Id: I18ee39e8450e868a842771f2febf4daac606fee5 --- shade/meta.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/meta.py b/shade/meta.py index b6944ec70..013d91cf4 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -289,7 +289,7 @@ def _get_suplemental_addresses(cloud, server): for address in network: if address['version'] == 6: continue - if address['OS-EXT-IPS:type'] == 'floating': + if address.get('OS-EXT-IPS:type') == 'floating': # We have a floating IP that nova knows about, do nothing return server['addresses'] fixed_ip_mapping[address['addr']] = name From 4bdda71fc55e928aa25ce511cd015abc7b8ff871 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 13 Aug 2016 08:06:03 -0500 Subject: [PATCH 0987/3836] Add tests to show IP inference in missed conditions There was no coverage concerning floating ip detection on clouds that didn't have floating ips at all, or even the OS-EXT-IPS extension. Although the problem was fixed in the previous commit, add a test that shows it. Also, add a test for Rackspace specifically, since it's network stack is different, to ensure that shade doesn't break dealling with Infra's gerrit server. Change-Id: I0bad8c69fe456a171d51f477dd6a5f8fb7065f60 --- shade/openstackcloud.py | 10 +++- shade/tests/unit/test_meta.py | 106 +++++++++++++++++++++++++++++++++- 2 files changed, 111 insertions(+), 5 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 23d4df428..a26498550 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -155,7 +155,13 @@ def __init__( self._default_network = cloud_config.get_default_network() self._floating_ip_source = cloud_config.config.get( - 'floating_ip_source').lower() + 'floating_ip_source') + if self._floating_ip_source: + if self._floating_ip_source.lower() == 'none': + self._floating_ip_source = None + else: + self._floating_ip_source = self._floating_ip_source.lower() + self._use_external_network = cloud_config.config.get( 'use_external_network', True) self._use_internal_network = cloud_config.config.get( @@ -1778,7 +1784,7 @@ def _has_floating_ips(self): if not self._floating_ip_source: return False else: - return self._floating_ip_source.lower() in ('nova', 'neutron') + return self._floating_ip_source in ('nova', 'neutron') def _use_neutron_floating(self): return (self.has_service('network') diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 6082b6337..57a81ac4d 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -250,7 +250,7 @@ def test_get_server_private_ip_no_fip( mock_list_server_security_groups, mock_list_subnets, mock_list_floating_ips): - self.cloud._floating_ip_source = 'none' + self.cloud._floating_ip_source = None mock_get_image_name.return_value = 'cirros-0.3.4-x86_64-uec' mock_get_flavor_name.return_value = 'm1.tiny' mock_has_service.return_value = True @@ -278,8 +278,57 @@ def test_get_server_private_ip_no_fip( u'OS-EXT-IPS:type': u'fixed', u'addr': PRIVATE_V4, u'version': 4, - u'OS-EXT-IPS-MAC:mac_addr': - u'fa:16:3e:ae:7d:42' + u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:ae:7d:42' + }]} + ))) + + self.assertEqual(PRIVATE_V4, srv['private_v4']) + mock_has_service.assert_called_with('volume') + mock_list_networks.assert_called_once_with() + mock_list_floating_ips.assert_not_called() + + @mock.patch.object(shade.OpenStackCloud, 'list_floating_ips') + @mock.patch.object(shade.OpenStackCloud, 'list_subnets') + @mock.patch.object(shade.OpenStackCloud, 'list_server_security_groups') + @mock.patch.object(shade.OpenStackCloud, 'get_volumes') + @mock.patch.object(shade.OpenStackCloud, 'get_image_name') + @mock.patch.object(shade.OpenStackCloud, 'get_flavor_name') + @mock.patch.object(shade.OpenStackCloud, 'has_service') + @mock.patch.object(shade.OpenStackCloud, 'list_networks') + def test_get_server_cloud_no_fips( + self, mock_list_networks, mock_has_service, + mock_get_flavor_name, mock_get_image_name, + mock_get_volumes, + mock_list_server_security_groups, + mock_list_subnets, + mock_list_floating_ips): + self.cloud._floating_ip_source = None + mock_get_image_name.return_value = 'cirros-0.3.4-x86_64-uec' + mock_get_flavor_name.return_value = 'm1.tiny' + mock_has_service.return_value = True + mock_get_volumes.return_value = [] + mock_list_subnets.return_value = SUBNETS_WITH_NAT + mock_list_networks.return_value = [ + { + 'id': 'test_pnztt_net', + 'name': 'test_pnztt_net', + 'router:external': False, + }, + { + 'id': 'private', + 'name': 'private', + }, + ] + + srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( + id='test-id', name='test-name', status='ACTIVE', + flavor={u'id': u'1'}, + image={ + 'name': u'cirros-0.3.4-x86_64-uec', + u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1'}, + addresses={u'test_pnztt_net': [{ + u'addr': PRIVATE_V4, + u'version': 4, }]} ))) @@ -288,6 +337,57 @@ def test_get_server_private_ip_no_fip( mock_list_networks.assert_called_once_with() mock_list_floating_ips.assert_not_called() + @mock.patch.object(shade.OpenStackCloud, 'list_floating_ips') + @mock.patch.object(shade.OpenStackCloud, 'list_subnets') + @mock.patch.object(shade.OpenStackCloud, 'list_server_security_groups') + @mock.patch.object(shade.OpenStackCloud, 'get_image_name') + @mock.patch.object(shade.OpenStackCloud, 'get_flavor_name') + @mock.patch.object(shade.OpenStackCloud, 'has_service') + @mock.patch.object(shade.OpenStackCloud, 'list_networks') + def test_get_server_cloud_rackspace_v6( + self, mock_list_networks, mock_has_service, + mock_get_flavor_name, mock_get_image_name, + mock_list_server_security_groups, + mock_list_subnets, + mock_list_floating_ips): + self.cloud._floating_ip_source = None + self.cloud.force_ipv4 = False + self.cloud._local_ipv6 = True + mock_get_image_name.return_value = 'cirros-0.3.4-x86_64-uec' + mock_get_flavor_name.return_value = 'm1.tiny' + mock_has_service.return_value = False + + srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( + id='test-id', name='test-name', status='ACTIVE', + flavor={u'id': u'1'}, + image={ + 'name': u'cirros-0.3.4-x86_64-uec', + u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1'}, + addresses={ + 'private': [{ + 'addr': "10.223.160.141", + 'version': 4 + }], + 'public': [{ + 'addr': "104.130.246.91", + 'version': 4 + }, { + 'addr': "2001:4800:7819:103:be76:4eff:fe05:8525", + 'version': 6 + }] + } + ))) + + self.assertEqual("10.223.160.141", srv['private_v4']) + self.assertEqual("104.130.246.91", srv['public_v4']) + self.assertEqual( + "2001:4800:7819:103:be76:4eff:fe05:8525", srv['public_v6']) + self.assertEqual( + "2001:4800:7819:103:be76:4eff:fe05:8525", srv['interface_ip']) + mock_list_subnets.assert_not_called() + mock_list_networks.assert_not_called() + mock_list_floating_ips.assert_not_called() + @mock.patch.object(shade.OpenStackCloud, 'has_service') @mock.patch.object(shade.OpenStackCloud, 'list_subnets') @mock.patch.object(shade.OpenStackCloud, 'list_networks') From 53b1fc4cdee8e460361c17f70571e883059323c1 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 12 Aug 2016 14:34:43 -0500 Subject: [PATCH 0988/3836] Base auto_ip on interface_ip not public_v4 Currently the auto_ip logic checks for a public v4 address to determine if it should make a FIP. However, we have richer logic that provides interface_ip that takes in to account IPv6 as well as configured choices as to what the "default" network should be. That is what should really be cared about - does the server have an ip that would be considered the thing to connect to it on. Change-Id: Ica1feb31cc3477c6336a749c88f4706efd20484f --- .../use-interface-ip-c5cb3e7c91150096.yaml | 13 ++ shade/openstackcloud.py | 4 +- shade/tests/unit/test_floating_ip_common.py | 112 ++++++++++++++++++ 3 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/use-interface-ip-c5cb3e7c91150096.yaml diff --git a/releasenotes/notes/use-interface-ip-c5cb3e7c91150096.yaml b/releasenotes/notes/use-interface-ip-c5cb3e7c91150096.yaml new file mode 100644 index 000000000..14a4fd4a1 --- /dev/null +++ b/releasenotes/notes/use-interface-ip-c5cb3e7c91150096.yaml @@ -0,0 +1,13 @@ +--- +fixes: + - shade now correctly does not try to attach a floating ip with auto_ip + if the cloud has given a public IPv6 address and the calling context + supports IPv6 routing. shade has always used this logic to determine + the server 'interface_ip', but the auto floating ip was incorrectly only + looking at the 'public_v4' value to determine whether the server needed + additional networking. +upgrade: + - If your cloud presents a default split IPv4/IPv6 stack with a public + v6 and a private v4 address and you have the expectation that auto_ip + should procure a v4 floating ip, you need to set 'force_ipv4' to True in + your clouds.yaml entry for the cloud. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index a26498550..cf115e893 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4028,7 +4028,7 @@ def add_auto_ip(self, server, wait=False, timeout=60, reuse=True): """ server = self._add_auto_ip( server, wait=wait, timeout=timeout, reuse=reuse) - return self.get_server_public_ip(server) + return server['interface_ip'] or None def _add_auto_ip(self, server, wait=False, timeout=60, reuse=True): skip_attach = False @@ -4091,7 +4091,7 @@ def add_ips_to_server( server, ips, wait=wait, timeout=timeout, fixed_address=fixed_address) elif auto_ip: - if not self.get_server_public_ip(server): + if not server['interface_ip']: server = self._add_auto_ip( server, wait=wait, timeout=timeout, reuse=reuse) return server diff --git a/shade/tests/unit/test_floating_ip_common.py b/shade/tests/unit/test_floating_ip_common.py index e6e60ca5b..df91bea17 100644 --- a/shade/tests/unit/test_floating_ip_common.py +++ b/shade/tests/unit/test_floating_ip_common.py @@ -80,6 +80,118 @@ def test_add_ips_to_server_pool( server_dict, pool, reuse=True, wait=False, timeout=60, fixed_address=None, nat_destination=None) + @patch.object(OpenStackCloud, 'has_service') + @patch.object(OpenStackCloud, 'get_floating_ip') + @patch.object(OpenStackCloud, '_add_auto_ip') + def test_add_ips_to_server_ipv6_only( + self, mock_add_auto_ip, + mock_get_floating_ip, + mock_has_service): + self.cloud._floating_ip_source = None + self.cloud.force_ipv4 = False + self.cloud._local_ipv6 = True + mock_has_service.return_value = False + server = FakeServer( + id='server-id', name='test-server', status="ACTIVE", + addresses={ + 'private': [{ + 'addr': "10.223.160.141", + 'version': 4 + }], + 'public': [{ + u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:ae:7d:42', + u'OS-EXT-IPS:type': u'fixed', + 'addr': "2001:4800:7819:103:be76:4eff:fe05:8525", + 'version': 6 + }] + } + ) + server_dict = meta.add_server_interfaces( + self.cloud, meta.obj_to_dict(server)) + + new_server = self.client.add_ips_to_server(server=server_dict) + mock_get_floating_ip.assert_not_called() + mock_add_auto_ip.assert_not_called() + self.assertEqual( + new_server['interface_ip'], + '2001:4800:7819:103:be76:4eff:fe05:8525') + self.assertEqual(new_server['private_v4'], '10.223.160.141') + self.assertEqual(new_server['public_v4'], '') + self.assertEqual( + new_server['public_v6'], '2001:4800:7819:103:be76:4eff:fe05:8525') + + @patch.object(OpenStackCloud, 'has_service') + @patch.object(OpenStackCloud, 'get_floating_ip') + @patch.object(OpenStackCloud, '_add_auto_ip') + def test_add_ips_to_server_rackspace( + self, mock_add_auto_ip, + mock_get_floating_ip, + mock_has_service): + self.cloud._floating_ip_source = None + self.cloud.force_ipv4 = False + self.cloud._local_ipv6 = True + mock_has_service.return_value = False + server = FakeServer( + id='server-id', name='test-server', status="ACTIVE", + addresses={ + 'private': [{ + 'addr': "10.223.160.141", + 'version': 4 + }], + 'public': [{ + 'addr': "104.130.246.91", + 'version': 4 + }, { + 'addr': "2001:4800:7819:103:be76:4eff:fe05:8525", + 'version': 6 + }] + } + ) + server_dict = meta.add_server_interfaces( + self.cloud, meta.obj_to_dict(server)) + + new_server = self.client.add_ips_to_server(server=server_dict) + mock_get_floating_ip.assert_not_called() + mock_add_auto_ip.assert_not_called() + self.assertEqual( + new_server['interface_ip'], + '2001:4800:7819:103:be76:4eff:fe05:8525') + + @patch.object(OpenStackCloud, 'has_service') + @patch.object(OpenStackCloud, 'get_floating_ip') + @patch.object(OpenStackCloud, '_add_auto_ip') + def test_add_ips_to_server_rackspace_local_ipv4( + self, mock_add_auto_ip, + mock_get_floating_ip, + mock_has_service): + self.cloud._floating_ip_source = None + self.cloud.force_ipv4 = False + self.cloud._local_ipv6 = False + mock_has_service.return_value = False + server = FakeServer( + id='server-id', name='test-server', status="ACTIVE", + addresses={ + 'private': [{ + 'addr': "10.223.160.141", + 'version': 4 + }], + 'public': [{ + 'addr': "104.130.246.91", + 'version': 4 + }, { + 'addr': "2001:4800:7819:103:be76:4eff:fe05:8525", + 'version': 6 + }] + } + ) + server_dict = meta.add_server_interfaces( + self.cloud, meta.obj_to_dict(server)) + + new_server = self.client.add_ips_to_server(server=server_dict) + mock_get_floating_ip.assert_not_called() + mock_add_auto_ip.assert_not_called() + self.assertEqual(new_server['interface_ip'], '104.130.246.91') + @patch.object(OpenStackCloud, 'nova_client') @patch.object(OpenStackCloud, 'add_ip_list') def test_add_ips_to_server_ip_list( From 6b3b4974cec74cf9cc55252240475dab771093ca Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 14 Aug 2016 09:31:34 -0500 Subject: [PATCH 0989/3836] Validate config vs reality better than length of list Configuration indicating that a network is external only needs to validate in one direction. Namely, if you indicate that network a is external, and there exists a network be that can also be inferred to be external, that's fine. But if you indicate network a is external and we do not find network a, that's a problem. The previous test using list length was woefully inadequate for this purpose. Change-Id: I3e7ece26de23007d1b97a7b8a25c3db31b124e5f --- shade/openstackcloud.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index cf115e893..d1dc69f56 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1697,21 +1697,20 @@ def _set_interesting_networks(self): default_net=self._default_network)) default_network = network - if (self._external_network_names - and len(self._external_network_names) - != len(external_networks)): - raise OpenStackCloudException( - "Networks: {network} were provided for external" - " access and those networks could not be found".format( - network=",".join(self._external_network_names))) + # Validate config vs. reality + for net_name in self._external_network_names: + if net_name not in [net['name'] for net in external_networks]: + raise OpenStackCloudException( + "Networks: {network} was provided for external" + " access and those networks could not be found".format( + network=net_name)) - if (self._internal_network_names - and len(self._internal_network_names) - != len(internal_networks)): - raise OpenStackCloudException( - "Networks: {network} were provided for internal" - " access and those networks could not be found".format( - network=",".join(self._internal_network_names))) + for net_name in self._internal_network_names: + if net_name not in [net['name'] for net in internal_networks]: + raise OpenStackCloudException( + "Networks: {network} was provided for internal" + " access and those networks could not be found".format( + network=net_name)) if self._nat_destination and not nat_destination: raise OpenStackCloudException( From 552caf5bc1f2ccc63ee7706c8205e05e36b1f1f6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 14 Aug 2016 09:51:28 -0500 Subject: [PATCH 0990/3836] Honor default_interface OCC setting in create_server If a network has been configured as the "default_interface" in a clouds.yaml file, then it should be used as the default value to the nics argument in create_server. If a nics or a network argument are given to create_server, that should, of course, win. Change-Id: I278848f958a464daf2a5249084216b013698d62a --- shade/openstackcloud.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index d1dc69f56..91e76769d 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4284,6 +4284,10 @@ def create_server( cloud=self.name, region=self.region_name)) kwargs['nics'] = [{'net-id': network_obj['id']}] + if not network and ('nics' not in kwargs or not kwargs['nics']): + default_network = self.get_default_network() + if default_network: + kwargs['nics'] = [{'net-id': default_network['id']}] kwargs['image'] = image From 02a99fb3116614039740f8c6d780e88bccfdc5da Mon Sep 17 00:00:00 2001 From: Ian Wienand Date: Tue, 16 Aug 2016 14:24:34 +1000 Subject: [PATCH 0991/3836] Use "image" as argument for Glance V1 upload error path In Glance V1 the argument is "image", not "image_id", so use this on the error path to cleanup. Change-Id: Ie64e2911798cef17f1263d961d70f7715b2ba64b --- shade/openstackcloud.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 23d4df428..88d312b81 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2739,7 +2739,8 @@ def _upload_image_put_v1( except Exception: self.log.debug("Deleting failed upload of image {image}".format( image=image['name'])) - self.manager.submitTask(_tasks.ImageDelete(image_id=image.id)) + # Note argument is "image" here, "image_id" in V2 + self.manager.submitTask(_tasks.ImageDelete(image=image.id)) raise return image From 9d757b3a9a89bdd63f56ce171b7c878ded9a4cd8 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 18 Aug 2016 11:35:50 -0500 Subject: [PATCH 0992/3836] Add support for configuring split-stack networks Some clouds, like OSIC and v1 of DreamCompute, have a split stack network. This means that a single Neutron Network has both an IPv4 and an IPv6 subnet, but that the IPv4 subnet is a private/RFC-1918 and the IPv6 subnet is a Global network. As any inferrance information is attached to the Network, it's impossible to properly categorize IP addresses that are on the Server in such a scenario. Add support for ipv4 and ipv6 versions of the current routes_externally config value, with each of them defaulting to the value of the un-specialized routes_externally. Change-Id: I1e87a1423d20eac31175f44f5f7b38dfcf3a11cb --- doc/source/network-config.rst | 13 ++++++++++++- os_client_config/cloud_config.py | 24 ++++++++++++++++++++++++ os_client_config/config.py | 8 ++++++++ os_client_config/tests/base.py | 8 ++++++++ os_client_config/tests/test_config.py | 16 +++++++++++++--- 5 files changed, 65 insertions(+), 4 deletions(-) diff --git a/doc/source/network-config.rst b/doc/source/network-config.rst index 5a27b8e0e..09571804e 100644 --- a/doc/source/network-config.rst +++ b/doc/source/network-config.rst @@ -19,9 +19,15 @@ allows configuring a list of network metadata. default_interface: true - name: green routes_externally: false - - name: purple + - name: yellow routes_externally: false nat_destination: true + - name: chartreuse + routes_externally: false + routes_ipv6_externally: true + - name: aubergine + routes_ipv4_externally: false + routes_ipv6_externally: true Every entry must have a name field, which can hold either the name or the id of the network. @@ -33,6 +39,11 @@ be an RFC1918 address. In either case, it's provides IPs to servers that things not on the cloud can use. This value defaults to `false`, which indicates only servers on the same network can talk to it. +`routes_ipv4_externally` and `routes_ipv6_externally` are boolean fields to +help handle `routes_externally` in the case where a network has a split stack +with different values for IPv4 and IPv6. Either entry, if not given, defaults +to the value of `routes_externally`. + `default_interface` is a boolean field that indicates that the network is the one that programs should use. It defaults to false. An example of needing to use this value is a cloud with two private networks, and where a user is diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 08c739a4d..1846f5460 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -452,12 +452,36 @@ def get_external_networks(self): net['name'] for net in self.config['networks'] if net['routes_externally']] + def get_external_ipv4_networks(self): + """Get list of network names for external IPv4 networks.""" + return [ + net['name'] for net in self.config['networks'] + if net['routes_ipv4_externally']] + + def get_external_ipv6_networks(self): + """Get list of network names for external IPv6 networks.""" + return [ + net['name'] for net in self.config['networks'] + if net['routes_ipv6_externally']] + def get_internal_networks(self): """Get list of network names for internal networks.""" return [ net['name'] for net in self.config['networks'] if not net['routes_externally']] + def get_internal_ipv4_networks(self): + """Get list of network names for internal IPv4 networks.""" + return [ + net['name'] for net in self.config['networks'] + if not net['routes_ipv4_externally']] + + def get_internal_ipv6_networks(self): + """Get list of network names for internal IPv6 networks.""" + return [ + net['name'] for net in self.config['networks'] + if not net['routes_ipv6_externally']] + def get_default_network(self): """Get network used for default interactions.""" for net in self.config['networks']: diff --git a/os_client_config/config.py b/os_client_config/config.py index 7fa670fed..a51893202 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -520,6 +520,14 @@ def _fix_backwards_networks(self, cloud): nat_destination=get_boolean(net.get('nat_destination')), default_interface=get_boolean(net.get('default_interface')), ) + # routes_ipv4_externally defaults to the value of routes_externally + network['routes_ipv4_externally'] = get_boolean( + net.get( + 'routes_ipv4_externally', network['routes_externally'])) + # routes_ipv6_externally defaults to the value of routes_externally + network['routes_ipv6_externally'] = get_boolean( + net.get( + 'routes_ipv6_externally', network['routes_externally'])) networks.append(network) for key in ('external_network', 'internal_network'): diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py index d046a941f..85a42c593 100644 --- a/os_client_config/tests/base.py +++ b/os_client_config/tests/base.py @@ -113,6 +113,14 @@ 'name': 'another-private', 'routes_externally': False, 'nat_destination': True, + }, { + 'name': 'split-default', + 'routes_externally': True, + 'routes_ipv4_externally': False, + }, { + 'name': 'split-no-default', + 'routes_ipv6_externally': False, + 'routes_ipv4_externally': True, }], 'region_name': 'test-region', }, diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index baa35cd49..b9dcb212a 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -194,11 +194,19 @@ def test_get_one_cloud_networks(self): vendor_files=[self.vendor_yaml]) cc = c.get_one_cloud('_test-cloud-networks_') self.assertEqual( - ['a-public', 'another-public'], cc.get_external_networks()) + ['a-public', 'another-public', 'split-default'], + cc.get_external_networks()) self.assertEqual( - ['a-private', 'another-private'], cc.get_internal_networks()) + ['a-private', 'another-private', 'split-no-default'], + cc.get_internal_networks()) self.assertEqual('another-private', cc.get_nat_destination()) self.assertEqual('another-public', cc.get_default_network()) + self.assertEqual( + ['a-public', 'another-public', 'split-no-default'], + cc.get_external_ipv4_networks()) + self.assertEqual( + ['a-public', 'another-public', 'split-default'], + cc.get_external_ipv6_networks()) def test_get_one_cloud_no_networks(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], @@ -875,7 +883,9 @@ def test_normalize_network(self): expected = { 'networks': [ {'name': 'private', 'routes_externally': False, - 'nat_destination': False, 'default_interface': False}, + 'nat_destination': False, 'default_interface': False, + 'routes_ipv4_externally': False, + 'routes_ipv6_externally': False}, ] } self.assertEqual(expected, result) From 17f6847c20bc4b8ea1c8fc48f81bf1eca4ddb0f5 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Sat, 20 Aug 2016 16:14:36 -0500 Subject: [PATCH 0993/3836] Precedence final solution * Revert most of 'fixed_argparse change' from 1.19.1 * Create a new _validate_auth_correctly() method that contains the logic from 1.19.0 * Create a new get_one_cloud_osc() method for use by OSC to get the correct argument precedence without disrupting anyone else Change-Id: Iae86cc4e267f23dbe8d010688a288db5514f329d --- os_client_config/config.py | 220 +++++++++++++++++++------- os_client_config/tests/test_config.py | 36 +++++ 2 files changed, 200 insertions(+), 56 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 37a31f8d4..c799de0b0 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -836,7 +836,7 @@ def _get_auth_loader(self, config): config['auth']['token'] = 'notused' return loading.get_plugin_loader(config['auth_type']) - def _validate_auth_ksc(self, config, cloud, fixed_argparse): + def _validate_auth_ksc(self, config, cloud): try: import keystoneclient.auth as ksc_auth except ImportError: @@ -847,22 +847,19 @@ def _validate_auth_ksc(self, config, cloud, fixed_argparse): config['auth_type']).get_options() for p_opt in plugin_options: - # if it's in argparse, it was passed on the command line and wins # if it's in config.auth, win, kill it from config dict # if it's in config and not in config.auth, move it # deprecated loses to current # provided beats default, deprecated or not winning_value = self._find_winning_auth_value( - p_opt, fixed_argparse) - if winning_value: - found_in_argparse = True - else: - found_in_argparse = False + p_opt, + config['auth'], + ) + if not winning_value: winning_value = self._find_winning_auth_value( - p_opt, config['auth']) - if not winning_value: - winning_value = self._find_winning_auth_value( - p_opt, config) + p_opt, + config, + ) # if the plugin tells us that this value is required # then error if it's doesn't exist now @@ -878,12 +875,7 @@ def _validate_auth_ksc(self, config, cloud, fixed_argparse): # Clean up after ourselves for opt in [p_opt.name] + [o.name for o in p_opt.deprecated_opts]: opt = opt.replace('-', '_') - # don't do this if the value came from argparse, because we - # don't (yet) know if the value in not-auth came from argparse - # overlay or from someone passing in a dict to kwargs - # TODO(mordred) Fix that data path too - if not found_in_argparse: - config.pop(opt, None) + config.pop(opt, None) config['auth'].pop(opt, None) if winning_value: @@ -897,51 +889,80 @@ def _validate_auth_ksc(self, config, cloud, fixed_argparse): return config - def _validate_auth(self, config, loader, fixed_argparse): + def _validate_auth(self, config, loader): # May throw a keystoneauth1.exceptions.NoMatchingPlugin plugin_options = loader.get_options() for p_opt in plugin_options: - # if it's in argparse, it was passed on the command line and wins # if it's in config.auth, win, kill it from config dict # if it's in config and not in config.auth, move it # deprecated loses to current # provided beats default, deprecated or not winning_value = self._find_winning_auth_value( - p_opt, fixed_argparse) - if winning_value: - found_in_argparse = True - else: - found_in_argparse = False + p_opt, + config['auth'], + ) + if not winning_value: winning_value = self._find_winning_auth_value( - p_opt, config['auth']) - if not winning_value: - winning_value = self._find_winning_auth_value( - p_opt, config) + p_opt, + config, + ) + + config = self._clean_up_after_ourselves( + config, + p_opt, + winning_value, + ) - # Clean up after ourselves - for opt in [p_opt.name] + [o.name for o in p_opt.deprecated]: - opt = opt.replace('-', '_') - # don't do this if the value came from argparse, because we - # don't (yet) know if the value in not-auth came from argparse - # overlay or from someone passing in a dict to kwargs - # TODO(mordred) Fix that data path too - if not found_in_argparse: - config.pop(opt, None) - config['auth'].pop(opt, None) + return config - 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 + def _validate_auth_correctly(self, config, loader): + # May throw a keystoneauth1.exceptions.NoMatchingPlugin + + plugin_options = loader.get_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 it's 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'], + ) + + config = self._clean_up_after_ourselves( + config, + p_opt, + winning_value, + ) return config + def _clean_up_after_ourselves(self, config, p_opt, winning_value): + + # 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 + return config + def magic_fixes(self, config): """Perform the set of magic argument fixups""" @@ -998,11 +1019,6 @@ def get_one_cloud(self, cloud=None, validate=True, """ args = self._fix_args(kwargs, argparse=argparse) - # Run the fix just for argparse by itself. We need to - # have a copy of the argparse options separately from - # any merged copied later in validate_auth so that we - # can determine precedence - fixed_argparse = self._fix_args(argparse=argparse) if cloud is None: if 'cloud' in args: @@ -1042,7 +1058,7 @@ def get_one_cloud(self, cloud=None, validate=True, if validate: try: loader = self._get_auth_loader(config) - config = self._validate_auth(config, loader, fixed_argparse) + config = self._validate_auth(config, loader) auth_plugin = loader.load_from_options(**config['auth']) except Exception as e: # We WANT the ksa exception normally @@ -1052,8 +1068,7 @@ def get_one_cloud(self, cloud=None, validate=True, self.log.debug("Deferring keystone exception: {e}".format(e=e)) auth_plugin = None try: - config = self._validate_auth_ksc( - config, cloud, fixed_argparse) + config = self._validate_auth_ksc(config, cloud) except Exception: raise e else: @@ -1074,12 +1089,105 @@ def get_one_cloud(self, cloud=None, validate=True, else: cloud_name = str(cloud) return cloud_config.CloudConfig( - name=cloud_name, region=config['region_name'], + name=cloud_name, + region=config['region_name'], config=self._normalize_keys(config), force_ipv4=force_ipv4, auth_plugin=auth_plugin, openstack_config=self - ) + ) + + def get_one_cloud_osc( + self, + cloud=None, + validate=True, + argparse=None, + **kwargs + ): + """Retrieve a single cloud configuration and merge additional options + + :param string cloud: + The name of the configuration to load from clouds.yaml + :param boolean validate: + Validate the config. Setting this to False causes no auth plugin + to be created. It's really only useful for testing. + :param Namespace argparse: + An argparse Namespace object; allows direct passing in of + argparse options to be added to the cloud config. Values + of None and '' will be removed. + :param region_name: Name of the region of the cloud. + :param kwargs: Additional configuration options + + :raises: keystoneauth1.exceptions.MissingRequiredOptions + on missing required auth parameters + """ + + args = self._fix_args(kwargs, argparse=argparse) + + if cloud is None: + if 'cloud' in args: + cloud = args['cloud'] + else: + cloud = self.default_cloud + + config = self._get_base_cloud_config(cloud) + + # Get region specific settings + if 'region_name' not in args: + args['region_name'] = '' + region = self._get_region(cloud=cloud, region_name=args['region_name']) + args['region_name'] = region['name'] + region_args = copy.deepcopy(region['values']) + + # Regions is a list that we can use to create a list of cloud/region + # objects. It does not belong in the single-cloud dict + config.pop('regions', None) + + # Can't just do update, because None values take over + for arg_list in region_args, args: + for (key, val) in iter(arg_list.items()): + if val is not None: + if key == 'auth' and config[key] is not None: + config[key] = _auth_update(config[key], val) + else: + config[key] = val + + config = self.magic_fixes(config) + + # NOTE(dtroyer): OSC needs a hook into the auth args before the + # plugin is loaded in order to maintain backward- + # compatible behaviour + config = self.auth_config_hook(config) + + if validate: + loader = self._get_auth_loader(config) + config = self._validate_auth_correctly(config, loader) + auth_plugin = loader.load_from_options(**config['auth']) + else: + auth_plugin = None + + # If any of the defaults reference other values, we need to expand + for (key, value) in config.items(): + if hasattr(value, 'format'): + config[key] = value.format(**config) + + force_ipv4 = config.pop('force_ipv4', self.force_ipv4) + prefer_ipv6 = config.pop('prefer_ipv6', True) + if not prefer_ipv6: + force_ipv4 = True + + if cloud is None: + cloud_name = '' + else: + cloud_name = str(cloud) + return cloud_config.CloudConfig( + name=cloud_name, + region=config['region_name'], + config=self._normalize_keys(config), + force_ipv4=force_ipv4, + auth_plugin=auth_plugin, + openstack_config=self, + ) @staticmethod def set_one_cloud(config_file, cloud, set_config=None): diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 500887812..e1113886b 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -402,6 +402,42 @@ def test_get_one_cloud_precedence(self): cc = c.get_one_cloud( argparse=options, **kwargs) self.assertEqual(cc.region_name, 'region2') + self.assertEqual(cc.auth['password'], 'authpass') + self.assertEqual(cc.snack_type, 'cookie') + + def test_get_one_cloud_precedence_osc(self): + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + ) + + kwargs = { + 'auth': { + 'username': 'testuser', + 'password': 'authpass', + 'project-id': 'testproject', + 'auth_url': 'http://example.com/v2', + }, + 'region_name': 'kwarg_region', + 'password': 'ansible_password', + 'arbitrary': 'value', + } + + args = dict( + auth_url='http://example.com/v2', + username='user', + password='argpass', + project_name='project', + region_name='region2', + snack_type='cookie', + ) + + options = argparse.Namespace(**args) + cc = c.get_one_cloud_osc( + argparse=options, + **kwargs + ) + self.assertEqual(cc.region_name, 'region2') self.assertEqual(cc.auth['password'], 'argpass') self.assertEqual(cc.snack_type, 'cookie') From 84fe77562d8474dbe0ee4c8eb14f51452a857135 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 21 Aug 2016 08:28:18 -0500 Subject: [PATCH 0994/3836] Set physical_network to public in devstack test It seems the name of the physical network in recent devstack has changed. Change-Id: I29495ff4f49a61a350a1cb86f9124f3403c9468b --- shade/tests/functional/test_network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shade/tests/functional/test_network.py b/shade/tests/functional/test_network.py index 51cf9057a..931113cce 100644 --- a/shade/tests/functional/test_network.py +++ b/shade/tests/functional/test_network.py @@ -70,14 +70,14 @@ def test_create_network_provider_flat(self): name=self.network_name, shared=True, provider={ - 'physical_network': 'private', + 'physical_network': 'public', 'network_type': 'flat', } ) self.assertIn('id', net1) self.assertEqual(self.network_name, net1['name']) self.assertEqual('flat', net1['provider:network_type']) - self.assertEqual('private', net1['provider:physical_network']) + self.assertEqual('public', net1['provider:physical_network']) self.assertIsNone(net1['provider:segmentation_id']) def test_list_networks_filtered(self): From 332195f19cb52a0373f581ab4fa65f4be31e8511 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 21 Aug 2016 11:12:00 -0500 Subject: [PATCH 0995/3836] Add bindep.txt file listing distro depends Turns out we don't have many. That's neat. If we list them, then we're nice and friendly to folks. Change-Id: I77dc2e0e48b9f7531dff51eddd4b274367f8ea2d --- bindep.txt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 bindep.txt diff --git a/bindep.txt b/bindep.txt new file mode 100644 index 000000000..9c37fd538 --- /dev/null +++ b/bindep.txt @@ -0,0 +1,6 @@ +# This is a cross-platform list tracking distribution packages needed by tests; +# see http://docs.openstack.org/infra/bindep/ for additional information. + +build-essential [platform:dpkg] +python-dev [platform:dpkg] +python-devel [platform:rpm] From 3462561c46e3cb53afa153e638741d0eb400418b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 20 Aug 2016 10:14:06 -0500 Subject: [PATCH 0996/3836] Only run flake8 on shade directory I frequently have random test files and virtualenvs scattered in my working directory, so tox -epep8 gets frustrating. We don't have any files that need to be linted that are not in the shade directory - so go ahead and explicitly limit the invocation. Change-Id: I16d8f477ae08a1e54724991ccf4a169ae3b35369 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 5caa2b097..90ae81c94 100644 --- a/tox.ini +++ b/tox.ini @@ -36,7 +36,7 @@ passenv = OS_* commands = python setup.py testr --slowest --testr-args='--concurrency=1 {posargs}' [testenv:pep8] -commands = flake8 +commands = flake8 shade [testenv:venv] commands = {posargs} From aa6505af09e2fe69e961cefb1e8af0b751f03455 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 20 Aug 2016 13:30:02 -0500 Subject: [PATCH 0997/3836] Update HACKING.rst with a couple of shade specific notes Reasonable people disagree on finer points of style. However, it's pretty well understood that being consistent within a single codebase is more important that specific choices globally. There are a few things worth pointing out that are true in the context of shade which may not be true in other places, so active communication is likely the best choice here. Change-Id: Ib1ceb5d6f51f84fa4bc40e68e9e231a60138507d --- HACKING.rst | 49 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/HACKING.rst b/HACKING.rst index 7f3d25675..82e34ce37 100644 --- a/HACKING.rst +++ b/HACKING.rst @@ -1,4 +1,49 @@ shade Style Commandments -=============================================== +======================== -Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/ \ No newline at end of file +Read the OpenStack Style Commandments +http://docs.openstack.org/developer/hacking/ + +Indentation +----------- + +PEP-8 allows for 'visual' indentation. Do not use it. Visual indentation looks +like this: + +.. code-block:: python + + return_value = self.some_method(arg1, arg1, + arg3, arg4) + +Visual indentation makes refactoring the code base unneccesarily hard. + +Instead of visual indentation, use this: + +.. code-block:: python + + return_value = self.some_method( + arg1, arg1, arg3, arg4) + +That way, if some_method ever needs to be renamed, the only line that needs +to be touched is the line with some_method. Additionaly, if you need to +line break at the top of a block, please indent the continuation line +an additional 4 spaces, like this: + +.. code-block:: python + + for val in self.some_method( + arg1, arg1, arg3, arg4): + self.do_something_awesome() + +Neither of these are 'mandated' by PEP-8. However, they are prevailing styles +within this code base. + +Unit Tests +---------- + +Unit tests should be virtually instant. If a unit test takes more than 1 second +to run, it is a bad unit test. Honestly, 1 second is too slow. + +All unit test classes should subclass `shade.tests.unit.base.BaseTestCase`. The +base TestCase class takes care of properly creating `OpenStackCloud` objects +in a way that protects against local environment. From 829ebda93f45da01f54e0e6113759769b8df0141 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 20 Aug 2016 12:21:36 -0500 Subject: [PATCH 0998/3836] Add debug logging to unit test base class Sometimes tests run long and it's hard to figure out which operation wasn't mocked out. Enabling debug logging before the logging fixture means that the debug logging is produced, but is still shoved into the fixture. If you run a test via testtools.run or something similar, you'll see both the operations and any print statements, but running normally under testr it's all silent. Change-Id: I38de01f6fdb57274eef93100d885f7a062e7167a --- shade/tests/base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shade/tests/base.py b/shade/tests/base.py index 41d89a04a..382006ca1 100644 --- a/shade/tests/base.py +++ b/shade/tests/base.py @@ -20,6 +20,8 @@ import fixtures import testtools +import shade + _TRUE_VALUES = ('true', '1', 'yes') @@ -50,4 +52,5 @@ def setUp(self): stderr = self.useFixture(fixtures.StringStream('stderr')).stream self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) + shade.simple_logging(debug=True) self.log_fixture = self.useFixture(fixtures.FakeLogger()) From c40cc1938185ad80cc2f62ca4f6e8665a11a6fbf Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 20 Aug 2016 13:08:43 -0500 Subject: [PATCH 0999/3836] Use cloud fixtures from the unittest base class We need to do things to protect against local environment leaking into the test fixtures when we create the cloud objects. Those things are all done in the base unittest TestCase class, but for reasons that are hard to fathom, we ignore that and create our own clouds in many of the unitttest classes. This leads to problems if a user running the unittests has cache config information in their local clouds.yaml. Fix it. Change-Id: I022d541d8e98bf4b6691bf0a91e3b7d20b2b7456 --- shade/tests/unit/base.py | 17 +- shade/tests/unit/test_aggregate.py | 22 +- shade/tests/unit/test_create_server.py | 55 ++-- .../tests/unit/test_create_volume_snapshot.py | 19 +- shade/tests/unit/test_delete_server.py | 8 - .../tests/unit/test_delete_volume_snapshot.py | 19 +- shade/tests/unit/test_domains.py | 49 ++- shade/tests/unit/test_endpoints.py | 27 +- shade/tests/unit/test_flavors.py | 11 +- shade/tests/unit/test_floating_ip_common.py | 20 +- shade/tests/unit/test_floating_ip_neutron.py | 51 ++- shade/tests/unit/test_floating_ip_nova.py | 30 +- shade/tests/unit/test_floating_ip_pool.py | 11 +- shade/tests/unit/test_groups.py | 14 +- shade/tests/unit/test_identity_roles.py | 52 +-- shade/tests/unit/test_image_snapshot.py | 3 +- shade/tests/unit/test_magnum_services.py | 6 +- shade/tests/unit/test_object.py | 8 - shade/tests/unit/test_port.py | 31 +- shade/tests/unit/test_project.py | 24 +- shade/tests/unit/test_quotas.py | 22 +- shade/tests/unit/test_rebuild_server.py | 36 +-- shade/tests/unit/test_role_assignment.py | 296 ++++++++++-------- .../tests/unit/test_server_delete_metadata.py | 13 +- shade/tests/unit/test_server_set_metadata.py | 14 +- shade/tests/unit/test_services.py | 33 +- shade/tests/unit/test_shade_operator.py | 143 +++++---- shade/tests/unit/test_update_server.py | 15 +- shade/tests/unit/test_users.py | 35 +-- 29 files changed, 502 insertions(+), 582 deletions(-) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 4d8661922..fc52e0f72 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -67,11 +67,24 @@ def _nosleep(seconds): test_cloud = os.environ.get('SHADE_OS_CLOUD', '_test_cloud_') self.config = occ.OpenStackConfig( config_files=[config.name], - vendor_files=[vendor.name]) - self.cloud_config = self.config.get_one_cloud(cloud=test_cloud) + vendor_files=[vendor.name], + secure_files=['non-existant']) + self.cloud_config = self.config.get_one_cloud( + cloud=test_cloud, validate=False) self.cloud = shade.OpenStackCloud( cloud_config=self.cloud_config, log_inner_exceptions=True) self.op_cloud = shade.OperatorCloud( cloud_config=self.cloud_config, log_inner_exceptions=True) + + # Any unit tests using betamax directly need a ksa.Session with + # an auth dict. + self.full_cloud_config = self.config.get_one_cloud( + cloud=test_cloud) + self.full_cloud = shade.OpenStackCloud( + cloud_config=self.full_cloud_config, + log_inner_exceptions=True) + self.full_op_cloud = shade.OperatorCloud( + cloud_config=self.full_cloud_config, + log_inner_exceptions=True) diff --git a/shade/tests/unit/test_aggregate.py b/shade/tests/unit/test_aggregate.py index 02f8859df..809cb0f83 100644 --- a/shade/tests/unit/test_aggregate.py +++ b/shade/tests/unit/test_aggregate.py @@ -22,14 +22,10 @@ class TestAggregate(base.TestCase): - def setUp(self): - super(TestAggregate, self).setUp() - self.cloud = shade.operator_cloud(validate=False) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_create_aggregate(self, mock_nova): aggregate_name = 'aggr1' - self.cloud.create_aggregate(name=aggregate_name) + self.op_cloud.create_aggregate(name=aggregate_name) mock_nova.aggregates.create.assert_called_once_with( name=aggregate_name, availability_zone=None @@ -39,8 +35,8 @@ def test_create_aggregate(self, mock_nova): def test_create_aggregate_with_az(self, mock_nova): aggregate_name = 'aggr1' availability_zone = 'az1' - self.cloud.create_aggregate(name=aggregate_name, - availability_zone=availability_zone) + self.op_cloud.create_aggregate( + name=aggregate_name, availability_zone=availability_zone) mock_nova.aggregates.create.assert_called_once_with( name=aggregate_name, availability_zone=availability_zone @@ -51,7 +47,7 @@ def test_delete_aggregate(self, mock_nova): mock_nova.aggregates.list.return_value = [ fakes.FakeAggregate('1234', 'name') ] - self.assertTrue(self.cloud.delete_aggregate('1234')) + self.assertTrue(self.op_cloud.delete_aggregate('1234')) mock_nova.aggregates.list.assert_called_once_with() mock_nova.aggregates.delete.assert_called_once_with( aggregate='1234' @@ -62,7 +58,7 @@ def test_update_aggregate_set_az(self, mock_nova): mock_nova.aggregates.list.return_value = [ fakes.FakeAggregate('1234', 'name') ] - self.cloud.update_aggregate('1234', availability_zone='az') + self.op_cloud.update_aggregate('1234', availability_zone='az') mock_nova.aggregates.update.assert_called_once_with( aggregate='1234', values={'availability_zone': 'az'}, @@ -73,7 +69,7 @@ def test_update_aggregate_unset_az(self, mock_nova): mock_nova.aggregates.list.return_value = [ fakes.FakeAggregate('1234', 'name', availability_zone='az') ] - self.cloud.update_aggregate('1234', availability_zone=None) + self.op_cloud.update_aggregate('1234', availability_zone=None) mock_nova.aggregates.update.assert_called_once_with( aggregate='1234', values={'availability_zone': None}, @@ -85,7 +81,7 @@ def test_set_aggregate_metadata(self, mock_nova): mock_nova.aggregates.list.return_value = [ fakes.FakeAggregate('1234', 'name') ] - self.cloud.set_aggregate_metadata('1234', metadata) + self.op_cloud.set_aggregate_metadata('1234', metadata) mock_nova.aggregates.set_metadata.assert_called_once_with( aggregate='1234', metadata=metadata @@ -97,7 +93,7 @@ def test_add_host_to_aggregate(self, mock_nova): mock_nova.aggregates.list.return_value = [ fakes.FakeAggregate('1234', 'name') ] - self.cloud.add_host_to_aggregate('1234', hostname) + self.op_cloud.add_host_to_aggregate('1234', hostname) mock_nova.aggregates.add_host.assert_called_once_with( aggregate='1234', host=hostname @@ -109,7 +105,7 @@ def test_remove_host_from_aggregate(self, mock_nova): mock_nova.aggregates.list.return_value = [ fakes.FakeAggregate('1234', 'name', hosts=[hostname]) ] - self.cloud.remove_host_from_aggregate('1234', hostname) + self.op_cloud.remove_host_from_aggregate('1234', hostname) mock_nova.aggregates.remove_host.assert_called_once_with( aggregate='1234', host=hostname diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index 6d9b5a08c..6db1589c7 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -21,23 +21,16 @@ from mock import patch, Mock import mock -import os_client_config from shade import _utils from shade import meta from shade import OpenStackCloud from shade.exc import (OpenStackCloudException, OpenStackCloudTimeout) -from shade.tests import base, fakes +from shade.tests import fakes +from shade.tests.unit import base class TestCreateServer(base.TestCase): - def setUp(self): - super(TestCreateServer, self).setUp() - config = os_client_config.OpenStackConfig() - self.client = OpenStackCloud( - cloud_config=config.get_one_cloud(validate=False)) - self.client._SERVER_AGE = 0 - def test_create_server_with_create_exception(self): """ Test that an exception in the novaclient create raises an exception in @@ -49,7 +42,7 @@ def test_create_server_with_create_exception(self): } OpenStackCloud.nova_client = Mock(**config) self.assertRaises( - OpenStackCloudException, self.client.create_server, + OpenStackCloudException, self.cloud.create_server, 'server-name', 'image-id', 'flavor-id') def test_create_server_with_get_exception(self): @@ -64,7 +57,7 @@ def test_create_server_with_get_exception(self): } OpenStackCloud.nova_client = Mock(**config) self.assertRaises( - OpenStackCloudException, self.client.create_server, + OpenStackCloudException, self.cloud.create_server, 'server-name', 'image-id', 'flavor-id') def test_create_server_with_server_error(self): @@ -81,7 +74,7 @@ def test_create_server_with_server_error(self): } OpenStackCloud.nova_client = Mock(**config) self.assertRaises( - OpenStackCloudException, self.client.create_server, + OpenStackCloudException, self.cloud.create_server, 'server-name', 'image-id', 'flavor-id') def test_create_server_wait_server_error(self): @@ -105,7 +98,7 @@ def test_create_server_wait_server_error(self): OpenStackCloud.nova_client = Mock(**config) self.assertRaises( OpenStackCloudException, - self.client.create_server, + self.cloud.create_server, 'server-name', 'image-id', 'flavor-id', wait=True) def test_create_server_with_timeout(self): @@ -123,8 +116,9 @@ def test_create_server_with_timeout(self): OpenStackCloud.nova_client = Mock(**config) self.assertRaises( OpenStackCloudTimeout, - self.client.create_server, - 'server-name', 'image-id', 'flavor-id', wait=True, timeout=1) + self.cloud.create_server, + 'server-name', 'image-id', 'flavor-id', + wait=True, timeout=0.01) def test_create_server_no_wait(self): """ @@ -145,9 +139,9 @@ def test_create_server_no_wait(self): self.assertEqual( _utils.normalize_server( meta.obj_to_dict(fake_server), - cloud_name=self.client.name, - region_name=self.client.region_name), - self.client.create_server( + cloud_name=self.cloud.name, + region_name=self.cloud.region_name), + self.cloud.create_server( name='server-name', image='image=id', flavor='flavor-id')) @@ -171,9 +165,9 @@ def test_create_server_with_admin_pass_no_wait(self): self.assertEqual( _utils.normalize_server( meta.obj_to_dict(fake_create_server), - cloud_name=self.client.name, - region_name=self.client.region_name), - self.client.create_server( + cloud_name=self.cloud.name, + region_name=self.cloud.region_name), + self.cloud.create_server( name='server-name', image='image=id', flavor='flavor-id', admin_pass='ooBootheiX0edoh')) @@ -193,7 +187,7 @@ def test_create_server_with_admin_pass_wait(self, mock_nova, mock_wait): mock_wait.return_value = _utils.normalize_server( meta.obj_to_dict(fake_server), None, None) - server = self.client.create_server( + server = self.cloud.create_server( name='server-name', image='image-id', flavor='flavor-id', admin_pass='ooBootheiX0edoh', wait=True) @@ -221,7 +215,7 @@ def test_wait_for_server(self, mock_get_server, mock_get_active_server): mock_get_active_server.side_effect = iter([ building_server, active_server]) - server = self.client.wait_for_server(building_server) + server = self.cloud.wait_for_server(building_server) self.assertEqual(2, mock_get_server.call_count) mock_get_server.assert_has_calls([ @@ -250,7 +244,7 @@ def test_create_server_wait(self, mock_nova, mock_wait): fake_server = {'id': 'fake_server_id', 'status': 'BUILDING'} mock_nova.servers.create.return_value = fake_server - self.client.create_server( + self.cloud.create_server( 'server-name', 'image-id', 'flavor-id', wait=True), mock_wait.assert_called_once_with( @@ -280,11 +274,11 @@ def test_create_server_no_addresses(self, mock_sleep): "floating_ips.list.return_value": [fake_floating_ip] } OpenStackCloud.nova_client = Mock(**config) - self.client._SERVER_AGE = 0 + self.cloud._SERVER_AGE = 0 with patch.object(OpenStackCloud, "add_ips_to_server", return_value=fake_server): self.assertRaises( - OpenStackCloudException, self.client.create_server, + OpenStackCloudException, self.cloud.create_server, 'server-name', 'image-id', 'flavor-id', wait=True) @@ -296,8 +290,8 @@ def test_create_server_network_with_no_nics(self, mock_get_network, Verify that if 'network' is supplied, and 'nics' is not, that we attempt to get the network for the server. """ - self.client.create_server('server-name', 'image-id', 'flavor-id', - network='network-name') + self.cloud.create_server( + 'server-name', 'image-id', 'flavor-id', network='network-name') mock_get_network.assert_called_once_with(name_or_id='network-name') @patch('shade.OpenStackCloud.nova_client') @@ -309,6 +303,7 @@ def test_create_server_network_with_empty_nics(self, Verify that if 'network' is supplied, along with an empty 'nics' list, it's treated the same as if 'nics' were not included. """ - self.client.create_server('server-name', 'image-id', 'flavor-id', - network='network-name', nics=[]) + self.cloud.create_server( + 'server-name', 'image-id', 'flavor-id', + network='network-name', nics=[]) mock_get_network.assert_called_once_with(name_or_id='network-name') diff --git a/shade/tests/unit/test_create_volume_snapshot.py b/shade/tests/unit/test_create_volume_snapshot.py index bf6f6c458..445945eb7 100644 --- a/shade/tests/unit/test_create_volume_snapshot.py +++ b/shade/tests/unit/test_create_volume_snapshot.py @@ -20,22 +20,16 @@ """ from mock import patch -import os_client_config from shade import _utils from shade import meta from shade import OpenStackCloud -from shade.tests import base, fakes +from shade.tests import fakes +from shade.tests.unit import base from shade.exc import (OpenStackCloudException, OpenStackCloudTimeout) class TestCreateVolumeSnapshot(base.TestCase): - def setUp(self): - super(TestCreateVolumeSnapshot, self).setUp() - config = os_client_config.OpenStackConfig() - self.client = OpenStackCloud( - cloud_config=config.get_one_cloud(validate=False)) - @patch.object(OpenStackCloud, 'cinder_client') def test_create_volume_snapshot_wait(self, mock_cinder): """ @@ -55,8 +49,7 @@ def test_create_volume_snapshot_wait(self, mock_cinder): self.assertEqual( _utils.normalize_volumes( [meta.obj_to_dict(fake_snapshot)])[0], - self.client.create_volume_snapshot(volume_id='1234', - wait=True) + self.cloud.create_volume_snapshot(volume_id='1234', wait=True) ) mock_cinder.volume_snapshots.create.assert_called_with( @@ -81,8 +74,8 @@ def test_create_volume_snapshot_with_timeout(self, mock_cinder): self.assertRaises( OpenStackCloudTimeout, - self.client.create_volume_snapshot, volume_id='1234', - wait=True, timeout=1) + self.cloud.create_volume_snapshot, volume_id='1234', + wait=True, timeout=0.01) mock_cinder.volume_snapshots.create.assert_called_with( force=False, volume_id='1234' @@ -108,7 +101,7 @@ def test_create_volume_snapshot_with_error(self, mock_cinder): self.assertRaises( OpenStackCloudException, - self.client.create_volume_snapshot, volume_id='1234', + self.cloud.create_volume_snapshot, volume_id='1234', wait=True, timeout=5) mock_cinder.volume_snapshots.create.assert_called_with( diff --git a/shade/tests/unit/test_delete_server.py b/shade/tests/unit/test_delete_server.py index 6901bc75e..734a58f7a 100644 --- a/shade/tests/unit/test_delete_server.py +++ b/shade/tests/unit/test_delete_server.py @@ -21,9 +21,7 @@ import mock from novaclient import exceptions as nova_exc -import os_client_config -from shade import OpenStackCloud from shade import exc as shade_exc from shade.tests import fakes from shade.tests.unit import base @@ -39,12 +37,6 @@ class TestDeleteServer(base.TestCase): nova_exc.RateLimit, nova_exc.HTTPNotImplemented) - def setUp(self): - super(TestDeleteServer, self).setUp() - config = os_client_config.OpenStackConfig() - self.cloud = OpenStackCloud( - cloud_config=config.get_one_cloud(validate=False)) - @mock.patch('shade.OpenStackCloud.nova_client') def test_delete_server(self, nova_mock): """ diff --git a/shade/tests/unit/test_delete_volume_snapshot.py b/shade/tests/unit/test_delete_volume_snapshot.py index 14918d409..485f6f559 100644 --- a/shade/tests/unit/test_delete_volume_snapshot.py +++ b/shade/tests/unit/test_delete_volume_snapshot.py @@ -20,20 +20,14 @@ """ from mock import patch -import os_client_config from shade import OpenStackCloud -from shade.tests import base, fakes +from shade.tests import fakes +from shade.tests.unit import base from shade.exc import (OpenStackCloudException, OpenStackCloudTimeout) class TestDeleteVolumeSnapshot(base.TestCase): - def setUp(self): - super(TestDeleteVolumeSnapshot, self).setUp() - config = os_client_config.OpenStackConfig() - self.client = OpenStackCloud( - cloud_config=config.get_one_cloud(validate=False)) - @patch.object(OpenStackCloud, 'cinder_client') def test_delete_volume_snapshot(self, mock_cinder): """ @@ -47,7 +41,7 @@ def test_delete_volume_snapshot(self, mock_cinder): self.assertEqual( True, - self.client.delete_volume_snapshot(name_or_id='1234', wait=False) + self.cloud.delete_volume_snapshot(name_or_id='1234', wait=False) ) mock_cinder.volume_snapshots.list.assert_called_with(detailed=True, @@ -68,8 +62,7 @@ def test_delete_volume_snapshot_with_error(self, mock_cinder): self.assertRaises( OpenStackCloudException, - self.client.delete_volume_snapshot, name_or_id='1234', - wait=True, timeout=1) + self.cloud.delete_volume_snapshot, name_or_id='1234') mock_cinder.volume_snapshots.delete.assert_called_with( snapshot='1234') @@ -87,8 +80,8 @@ def test_delete_volume_snapshot_with_timeout(self, mock_cinder): self.assertRaises( OpenStackCloudTimeout, - self.client.delete_volume_snapshot, name_or_id='1234', - wait=True, timeout=1) + self.cloud.delete_volume_snapshot, name_or_id='1234', + wait=True, timeout=0.01) mock_cinder.volume_snapshots.list.assert_called_with(detailed=True, search_opts=None) diff --git a/shade/tests/unit/test_domains.py b/shade/tests/unit/test_domains.py index 86f1f77f8..143c789b3 100644 --- a/shade/tests/unit/test_domains.py +++ b/shade/tests/unit/test_domains.py @@ -32,19 +32,15 @@ class TestDomains(base.TestCase): - def setUp(self): - super(TestDomains, self).setUp() - self.cloud = shade.operator_cloud(validate=False) - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_list_domains(self, mock_keystone): - self.cloud.list_domains() + self.op_cloud.list_domains() self.assertTrue(mock_keystone.domains.list.called) @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_get_domain(self, mock_keystone): mock_keystone.domains.get.return_value = domain_obj - domain = self.cloud.get_domain(domain_id='1234') + domain = self.op_cloud.get_domain(domain_id='1234') self.assertFalse(mock_keystone.domains.list.called) self.assertTrue(mock_keystone.domains.get.called) self.assertEqual(domain['name'], 'a-domain') @@ -52,7 +48,7 @@ def test_get_domain(self, mock_keystone): @mock.patch.object(shade._utils, '_get_entity') @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_get_domain_with_name_or_id(self, mock_keystone, mock_get): - self.cloud.get_domain(name_or_id='1234') + self.op_cloud.get_domain(name_or_id='1234') mock_get.assert_called_once_with(mock.ANY, None, '1234') @@ -60,8 +56,8 @@ def test_get_domain_with_name_or_id(self, mock_keystone, mock_get): @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_create_domain(self, mock_keystone, mock_normalize): mock_keystone.domains.create.return_value = domain_obj - self.cloud.create_domain(domain_obj.name, - domain_obj.description) + self.op_cloud.create_domain( + domain_obj.name, domain_obj.description) mock_keystone.domains.create.assert_called_once_with( name=domain_obj.name, description=domain_obj.description, enabled=True) @@ -74,13 +70,13 @@ def test_create_domain_exception(self, mock_keystone): shade.OpenStackCloudException, "Failed to create domain domain_name" ): - self.cloud.create_domain('domain_name') + self.op_cloud.create_domain('domain_name') @mock.patch.object(shade.OperatorCloud, 'update_domain') @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_delete_domain(self, mock_keystone, mock_update): mock_update.return_value = dict(id='update_domain_id') - self.cloud.delete_domain('domain_id') + self.op_cloud.delete_domain('domain_id') mock_update.assert_called_once_with('domain_id', enabled=False) mock_keystone.domains.delete.assert_called_once_with( domain='update_domain_id') @@ -88,10 +84,11 @@ def test_delete_domain(self, mock_keystone, mock_update): @mock.patch.object(shade.OperatorCloud, 'get_domain') @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_delete_domain_name_or_id(self, mock_keystone, mock_get): - self.cloud.update_domain(name_or_id='a-domain', - name='new name', - description='new description', - enabled=False) + self.op_cloud.update_domain( + name_or_id='a-domain', + name='new name', + description='new description', + enabled=False) mock_get.assert_called_once_with(None, 'a-domain') @mock.patch.object(shade.OperatorCloud, 'update_domain') @@ -102,16 +99,17 @@ def test_delete_domain_exception(self, mock_keystone, mock_update): shade.OpenStackCloudException, "Failed to delete domain domain_id" ): - self.cloud.delete_domain('domain_id') + self.op_cloud.delete_domain('domain_id') @mock.patch.object(shade._utils, 'normalize_domains') @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_update_domain(self, mock_keystone, mock_normalize): mock_keystone.domains.update.return_value = domain_obj - self.cloud.update_domain('domain_id', - name='new name', - description='new description', - enabled=False) + self.op_cloud.update_domain( + 'domain_id', + name='new name', + description='new description', + enabled=False) mock_keystone.domains.update.assert_called_once_with( domain='domain_id', name='new name', description='new description', enabled=False) @@ -121,10 +119,11 @@ def test_update_domain(self, mock_keystone, mock_normalize): @mock.patch.object(shade.OperatorCloud, 'get_domain') @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_update_domain_name_or_id(self, mock_keystone, mock_get): - self.cloud.update_domain(name_or_id='a-domain', - name='new name', - description='new description', - enabled=False) + self.op_cloud.update_domain( + name_or_id='a-domain', + name='new name', + description='new description', + enabled=False) mock_get.assert_called_once_with(None, 'a-domain') @mock.patch.object(shade.OpenStackCloud, 'keystone_client') @@ -134,4 +133,4 @@ def test_update_domain_exception(self, mock_keystone): shade.OpenStackCloudException, "Error in updating domain domain_id" ): - self.cloud.delete_domain('domain_id') + self.op_cloud.delete_domain('domain_id') diff --git a/shade/tests/unit/test_endpoints.py b/shade/tests/unit/test_endpoints.py index 1d85eebfa..4718dff3f 100644 --- a/shade/tests/unit/test_endpoints.py +++ b/shade/tests/unit/test_endpoints.py @@ -49,9 +49,6 @@ class TestCloudEndpoints(base.TestCase): def setUp(self): super(TestCloudEndpoints, self).setUp() - config = os_client_config.OpenStackConfig() - self.client = OperatorCloud(cloud_config=config.get_one_cloud( - validate=False)) self.mock_ks_endpoints = \ [FakeEndpoint(**kwa) for kwa in self.mock_endpoints] self.mock_ks_endpoints_v3 = \ @@ -74,7 +71,7 @@ def test_create_endpoint_v2(self, mock_api_version, mock_keystone_client, mock_keystone_client.endpoints.create.return_value = \ self.mock_ks_endpoints[2] - endpoints = self.client.create_endpoint( + endpoints = self.op_cloud.create_endpoint( service_name_or_id='service1', region='mock_region', public_url='mock_public_url', @@ -99,12 +96,12 @@ def test_create_endpoint_v2(self, mock_api_version, mock_keystone_client, self.mock_ks_endpoints[0] self.assertRaises(OpenStackCloudException, - self.client.create_endpoint, + self.op_cloud.create_endpoint, service_name_or_id='service1', interface='mock_admin_url', url='admin') - endpoints_3on2 = self.client.create_endpoint( + endpoints_3on2 = self.op_cloud.create_endpoint( service_name_or_id='service1', region='mock_region', interface='public', @@ -132,7 +129,7 @@ def test_create_endpoint_v3(self, mock_api_version, mock_keystone_client, mock_keystone_client.endpoints.create.return_value = \ self.mock_ks_endpoints_v3[0] - endpoints = self.client.create_endpoint( + endpoints = self.op_cloud.create_endpoint( service_name_or_id='service1', region='mock_region', url='mock_url', @@ -155,7 +152,7 @@ def test_create_endpoint_v3(self, mock_api_version, mock_keystone_client, mock_keystone_client.endpoints.create.side_effect = \ self.mock_ks_endpoints_v3 - endpoints_2on3 = self.client.create_endpoint( + endpoints_2on3 = self.op_cloud.create_endpoint( service_name_or_id='service1', region='mock_region', public_url='mock_public_url', @@ -176,7 +173,7 @@ def test_update_endpoint_v2(self, mock_api_version): mock_api_version.return_value = '2.0' # NOTE(SamYaple): Update endpoint only works with v3 api self.assertRaises(OpenStackCloudUnavailableFeature, - self.client.update_endpoint, 'endpoint_id') + self.op_cloud.update_endpoint, 'endpoint_id') @patch.object(OperatorCloud, 'keystone_client') @patch.object(os_client_config.cloud_config.CloudConfig, 'get_api_version') @@ -185,7 +182,7 @@ def test_update_endpoint_v3(self, mock_api_version, mock_keystone_client): mock_keystone_client.endpoints.update.return_value = \ self.mock_ks_endpoints_v3[0] - endpoint = self.client.update_endpoint( + endpoint = self.op_cloud.update_endpoint( 'id1', service_name_or_id='service_id1', region='mock_region', @@ -211,7 +208,7 @@ def test_list_endpoints(self, mock_keystone_client): mock_keystone_client.endpoints.list.return_value = \ self.mock_ks_endpoints - endpoints = self.client.list_endpoints() + endpoints = self.op_cloud.list_endpoints() mock_keystone_client.endpoints.list.assert_called_with() # test we are getting exactly len(self.mock_endpoints) elements @@ -236,18 +233,18 @@ def test_search_endpoints(self, mock_keystone_client): self.mock_ks_endpoints # Search by id - endpoints = self.client.search_endpoints(id='id3') + endpoints = self.op_cloud.search_endpoints(id='id3') # # test we are getting exactly 1 element self.assertEqual(1, len(endpoints)) for k, v in self.mock_endpoints[2].items(): self.assertEquals(v, endpoints[0].get(k)) # Not found - endpoints = self.client.search_endpoints(id='blah!') + endpoints = self.op_cloud.search_endpoints(id='blah!') self.assertEqual(0, len(endpoints)) # Multiple matches - endpoints = self.client.search_endpoints( + endpoints = self.op_cloud.search_endpoints( filters={'region': 'region1'}) # # test we are getting exactly 2 elements self.assertEqual(2, len(endpoints)) @@ -258,5 +255,5 @@ def test_delete_endpoint(self, mock_keystone_client): self.mock_ks_endpoints # Delete by id - self.client.delete_endpoint(id='id2') + self.op_cloud.delete_endpoint(id='id2') mock_keystone_client.endpoints.delete.assert_called_with(id='id2') diff --git a/shade/tests/unit/test_flavors.py b/shade/tests/unit/test_flavors.py index e3277f73b..e1625cb6b 100644 --- a/shade/tests/unit/test_flavors.py +++ b/shade/tests/unit/test_flavors.py @@ -24,9 +24,6 @@ class TestFlavors(base.TestCase): - def setUp(self): - super(TestFlavors, self).setUp() - def test_create_flavor(self): self.useFixture(keystoneauth_betamax.BetamaxFixture( cassette_name='test_create_flavor', @@ -34,13 +31,13 @@ def test_create_flavor(self): record=self.record_fixtures, serializer=serializer.YamlJsonSerializer)) - old_flavors = self.op_cloud.list_flavors() - self.op_cloud.create_flavor( + old_flavors = self.full_op_cloud.list_flavors() + self.full_op_cloud.create_flavor( 'vanilla', 12345, 4, 100 ) # test that we have a new flavor added - new_flavors = self.op_cloud.list_flavors() + new_flavors = self.full_op_cloud.list_flavors() self.assertEquals(len(new_flavors) - len(old_flavors), 1) # test that new flavor is created correctly @@ -56,7 +53,7 @@ def test_create_flavor(self): self.assertTrue(needed_keys.issubset(flavor.keys())) # delete created flavor - self.op_cloud.delete_flavor('vanilla') + self.full_op_cloud.delete_flavor('vanilla') @mock.patch.object(shade.OpenStackCloud, '_compute_client') @mock.patch.object(shade.OpenStackCloud, 'nova_client') diff --git a/shade/tests/unit/test_floating_ip_common.py b/shade/tests/unit/test_floating_ip_common.py index df91bea17..72ea32de0 100644 --- a/shade/tests/unit/test_floating_ip_common.py +++ b/shade/tests/unit/test_floating_ip_common.py @@ -20,7 +20,6 @@ """ from mock import patch -import os_client_config from shade import meta from shade import OpenStackCloud from shade.tests.fakes import FakeServer @@ -28,11 +27,6 @@ class TestFloatingIP(base.TestCase): - def setUp(self): - super(TestFloatingIP, self).setUp() - config = os_client_config.OpenStackConfig() - self.client = OpenStackCloud( - cloud_config=config.get_one_cloud(validate=False)) @patch.object(OpenStackCloud, 'get_floating_ip') @patch.object(OpenStackCloud, '_attach_ip_to_server') @@ -56,7 +50,7 @@ def test_add_auto_ip( mock_available_floating_ip.return_value = floating_ip_dict - self.client.add_auto_ip(server=server_dict) + self.cloud.add_auto_ip(server=server_dict) mock_attach_ip_to_server.assert_called_with( timeout=60, wait=False, server=server_dict, @@ -74,7 +68,7 @@ def test_add_ips_to_server_pool( mock_nova_client.servers.get.return_value = server - self.client.add_ips_to_server(server_dict, ip_pool=pool) + self.cloud.add_ips_to_server(server_dict, ip_pool=pool) mock_add_ip_from_pool.assert_called_with( server_dict, pool, reuse=True, wait=False, timeout=60, @@ -109,7 +103,7 @@ def test_add_ips_to_server_ipv6_only( server_dict = meta.add_server_interfaces( self.cloud, meta.obj_to_dict(server)) - new_server = self.client.add_ips_to_server(server=server_dict) + new_server = self.cloud.add_ips_to_server(server=server_dict) mock_get_floating_ip.assert_not_called() mock_add_auto_ip.assert_not_called() self.assertEqual( @@ -150,7 +144,7 @@ def test_add_ips_to_server_rackspace( server_dict = meta.add_server_interfaces( self.cloud, meta.obj_to_dict(server)) - new_server = self.client.add_ips_to_server(server=server_dict) + new_server = self.cloud.add_ips_to_server(server=server_dict) mock_get_floating_ip.assert_not_called() mock_add_auto_ip.assert_not_called() self.assertEqual( @@ -187,7 +181,7 @@ def test_add_ips_to_server_rackspace_local_ipv4( server_dict = meta.add_server_interfaces( self.cloud, meta.obj_to_dict(server)) - new_server = self.client.add_ips_to_server(server=server_dict) + new_server = self.cloud.add_ips_to_server(server=server_dict) mock_get_floating_ip.assert_not_called() mock_add_auto_ip.assert_not_called() self.assertEqual(new_server['interface_ip'], '104.130.246.91') @@ -203,7 +197,7 @@ def test_add_ips_to_server_ip_list( ips = ['203.0.113.29', '172.24.4.229'] mock_nova_client.servers.get.return_value = server - self.client.add_ips_to_server(server_dict, ips=ips) + self.cloud.add_ips_to_server(server_dict, ips=ips) mock_add_ip_list.assert_called_with( server_dict, ips, wait=False, timeout=60, fixed_address=None) @@ -219,7 +213,7 @@ def test_add_ips_to_server_auto_ip( mock_nova_client.servers.get.return_value = server - self.client.add_ips_to_server(server_dict) + self.cloud.add_ips_to_server(server_dict) mock_add_auto_ip.assert_called_with( server_dict, wait=False, timeout=60, reuse=True) diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index cf7f15293..89de0e1f2 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -21,7 +21,6 @@ import mock from mock import patch -import os_client_config from neutronclient.common import exceptions as n_exc @@ -138,10 +137,6 @@ def assertAreInstances(self, elements, elem_type): def setUp(self): super(TestFloatingIP, self).setUp() - # floating_ip_source='neutron' is default for OpenStackCloud() - config = os_client_config.OpenStackConfig() - self.client = OpenStackCloud( - cloud_config=config.get_one_cloud(validate=False)) self.fake_server = meta.obj_to_dict( fakes.FakeServer( @@ -162,7 +157,7 @@ def test_list_floating_ips(self, mock_has_service, mock_neutron_client): mock_neutron_client.list_floatingips.return_value = \ self.mock_floating_ip_list_rep - floating_ips = self.client.list_floating_ips() + floating_ips = self.cloud.list_floating_ips() mock_neutron_client.list_floatingips.assert_called_with() self.assertIsInstance(floating_ips, list) @@ -176,7 +171,7 @@ def test_search_floating_ips(self, mock_has_service, mock_neutron_client): mock_neutron_client.list_floatingips.return_value = \ self.mock_floating_ip_list_rep - floating_ips = self.client.search_floating_ips( + floating_ips = self.cloud.search_floating_ips( filters={'attached': False}) mock_neutron_client.list_floatingips.assert_called_with() @@ -191,7 +186,7 @@ def test_get_floating_ip(self, mock_has_service, mock_neutron_client): mock_neutron_client.list_floatingips.return_value = \ self.mock_floating_ip_list_rep - floating_ip = self.client.get_floating_ip( + floating_ip = self.cloud.get_floating_ip( id='2f245a7b-796b-4f26-9cf9-9e82d248fda7') mock_neutron_client.list_floatingips.assert_called_with() @@ -206,7 +201,7 @@ def test_get_floating_ip_not_found( mock_neutron_client.list_floatingips.return_value = \ self.mock_floating_ip_list_rep - floating_ip = self.client.get_floating_ip(id='non-existent') + floating_ip = self.cloud.get_floating_ip(id='non-existent') self.assertIsNone(floating_ip) @@ -220,7 +215,7 @@ def test_create_floating_ip( mock_neutron_client.create_floatingip.return_value = \ self.mock_floating_ip_new_rep - ip = self.client.create_floating_ip(network='my-network') + ip = self.cloud.create_floating_ip(network='my-network') mock_neutron_client.create_floatingip.assert_called_with( body={'floatingip': {'floating_network_id': 'my-network-id'}} @@ -241,7 +236,7 @@ def test_create_floating_ip_port_bad_response( self.assertRaises( exc.OpenStackCloudException, - self.client.create_floating_ip, + self.cloud.create_floating_ip, network='my-network', port='ce705c24-c1ef-408a-bda3-7bbd946164ab') @patch.object(OpenStackCloud, 'neutron_client') @@ -254,7 +249,7 @@ def test_create_floating_ip_port( mock_neutron_client.create_floatingip.return_value = \ self.mock_floating_ip_port_rep - ip = self.client.create_floating_ip( + ip = self.cloud.create_floating_ip( network='my-network', port='ce705c24-c1ef-408a-bda3-7bbd946164ab') mock_neutron_client.create_floatingip.assert_called_with( @@ -283,7 +278,7 @@ def test_available_floating_ip_neutron(self, mock_has_service.return_value = True mock__neutron_call.return_value = [] - self.client.available_floating_ip(network='netname') + self.cloud.available_floating_ip(network='netname') mock_has_service.assert_called_once_with('network') mock__neutron_call.assert_called_once_with(network='netname', @@ -311,7 +306,7 @@ def test__neutron_available_floating_ips( mock__filter_list.return_value = [] # Test if first network is selected if no network is given - self.client._neutron_available_floating_ips() + self.cloud._neutron_available_floating_ips() mock_keystone_session.get_project_id.assert_called_once_with() mock_get_ext_nets.assert_called_once_with() @@ -347,7 +342,7 @@ def test__neutron_available_floating_ips_network( mock__neutron_list_fips.return_value = [] mock__filter_list.return_value = [] - self.client._neutron_available_floating_ips( + self.cloud._neutron_available_floating_ips( network=self.mock_get_network_rep['name'] ) @@ -377,7 +372,7 @@ def test__neutron_available_floating_ips_invalid_network( mock_keystone_session.get_project_id.return_value = 'proj-id' mock_get_ext_nets.return_value = [] self.assertRaises(exc.OpenStackCloudException, - self.client._neutron_available_floating_ips, + self.cloud._neutron_available_floating_ips, network='INVALID') @patch.object(OpenStackCloud, 'nova_client') @@ -399,7 +394,7 @@ def test_auto_ip_pool_no_reuse( '4969c491a3c74ee4af974e6d800c62df' fake_server = meta.obj_to_dict(fakes.FakeServer('1234', '', 'ACTIVE')) - self.client.add_ips_to_server( + self.cloud.add_ips_to_server( fake_server, ip_pool='my-network', reuse=False) mock__neutron_create_floating_ip.assert_called_once_with( @@ -426,7 +421,7 @@ def test_available_floating_ip_new( mock_keystone_session.get_project_id.return_value = \ '4969c491a3c74ee4af974e6d800c62df' - ip = self.client.available_floating_ip(network='my-network') + ip = self.cloud.available_floating_ip(network='my-network') self.assertEqual( self.mock_floating_ip_new_rep['floatingip']['floating_ip_address'], @@ -446,7 +441,7 @@ def test_delete_floating_ip_existing( mock_get_floating_ip.side_effect = [fake_fip, fake_fip, None] mock_neutron_client.delete_floatingip.return_value = None - ret = self.client.delete_floating_ip( + ret = self.cloud.delete_floating_ip( floating_ip_id='2f245a7b-796b-4f26-9cf9-9e82d248fda7', retry=2) @@ -472,7 +467,7 @@ def test_delete_floating_ip_existing_no_delete( self.assertRaises( exc.OpenStackCloudException, - self.client.delete_floating_ip, + self.cloud.delete_floating_ip, floating_ip_id='2f245a7b-796b-4f26-9cf9-9e82d248fda7', retry=2) @@ -489,7 +484,7 @@ def test_delete_floating_ip_not_found( mock_neutron_client.delete_floatingip.side_effect = \ n_exc.NotFound() - ret = self.client.delete_floating_ip( + ret = self.cloud.delete_floating_ip( floating_ip_id='a-wild-id-appears') self.assertFalse(ret) @@ -506,7 +501,7 @@ def test_attach_ip_to_server( mock_neutron_client.list_floatingips.return_value = \ self.mock_floating_ip_list_rep - self.client._attach_ip_to_server( + self.cloud._attach_ip_to_server( server=self.fake_server, floating_ip=self.floating_ip) @@ -535,9 +530,9 @@ def test_add_ip_refresh_timeout( self.assertRaises( exc.OpenStackCloudTimeout, - self.client._add_auto_ip, + self.cloud._add_auto_ip, server=self.fake_server, - wait=True, timeout=2, + wait=True, timeout=0.01, reuse=False) mock_delete_floating_ip.assert_called_once_with( @@ -554,7 +549,7 @@ def test_detach_ip_from_server( _utils.normalize_neutron_floating_ips( self.mock_floating_ip_list_rep['floatingips'])[0] - self.client.detach_ip_from_server( + self.cloud.detach_ip_from_server( server_id='server-id', floating_ip_id='2f245a7b-796b-4f26-9cf9-9e82d248fda7') @@ -579,7 +574,7 @@ def test_add_ip_from_pool( self.mock_floating_ip_new_rep['floatingip']])[0] mock_attach_ip_to_server.return_value = self.fake_server - server = self.client._add_ip_from_pool( + server = self.cloud._add_ip_from_pool( server=self.fake_server, network='network-name', fixed_address='192.0.2.129') @@ -613,7 +608,7 @@ def test_cleanup_floating_ips( mock_list_floating_ips.return_value = floating_ips - self.client.delete_unattached_floating_ips() + self.cloud.delete_unattached_floating_ips() mock_delete_floating_ip.assert_called_once_with( floating_ip_id='this-is-a-floating-ip-id', retry=1) @@ -631,5 +626,5 @@ def test_create_floating_ip_no_port( self.assertRaises( exc.OpenStackCloudException, - self.client._neutron_create_floating_ip, + self.cloud._neutron_create_floating_ip, server=dict(id='some-server')) diff --git a/shade/tests/unit/test_floating_ip_nova.py b/shade/tests/unit/test_floating_ip_nova.py index 34bee06c3..854b46cf4 100644 --- a/shade/tests/unit/test_floating_ip_nova.py +++ b/shade/tests/unit/test_floating_ip_nova.py @@ -21,7 +21,6 @@ from mock import patch from novaclient import exceptions as n_exc -import os_client_config from shade import _utils from shade import meta @@ -71,9 +70,6 @@ def assertAreInstances(self, elements, elem_type): def setUp(self): super(TestFloatingIP, self).setUp() - config = os_client_config.OpenStackConfig() - self.client = OpenStackCloud( - cloud_config=config.get_one_cloud(validate=False)) self.floating_ips = [ fakes.FakeFloatingIP(**ip) for ip in self.mock_floating_ip_list_rep ] @@ -97,7 +93,7 @@ def test_list_floating_ips(self, mock_has_service, mock_nova_client): mock_has_service.side_effect = has_service_side_effect mock_nova_client.floating_ips.list.return_value = self.floating_ips - floating_ips = self.client.list_floating_ips() + floating_ips = self.cloud.list_floating_ips() mock_nova_client.floating_ips.list.assert_called_with() self.assertIsInstance(floating_ips, list) @@ -110,7 +106,7 @@ def test_search_floating_ips(self, mock_has_service, mock_nova_client): mock_has_service.side_effect = has_service_side_effect mock_nova_client.floating_ips.list.return_value = self.floating_ips - floating_ips = self.client.search_floating_ips( + floating_ips = self.cloud.search_floating_ips( filters={'attached': False}) mock_nova_client.floating_ips.list.assert_called_with() @@ -124,7 +120,7 @@ def test_get_floating_ip(self, mock_has_service, mock_nova_client): mock_has_service.side_effect = has_service_side_effect mock_nova_client.floating_ips.list.return_value = self.floating_ips - floating_ip = self.client.get_floating_ip(id='29') + floating_ip = self.cloud.get_floating_ip(id='29') mock_nova_client.floating_ips.list.assert_called_with() self.assertIsInstance(floating_ip, dict) @@ -137,7 +133,7 @@ def test_get_floating_ip_not_found( mock_has_service.side_effect = has_service_side_effect mock_nova_client.floating_ips.list.return_value = self.floating_ips - floating_ip = self.client.get_floating_ip(id='666') + floating_ip = self.cloud.get_floating_ip(id='666') self.assertIsNone(floating_ip) @@ -148,7 +144,7 @@ def test_create_floating_ip(self, mock_has_service, mock_nova_client): mock_nova_client.floating_ips.create.return_value =\ fakes.FakeFloatingIP(**self.mock_floating_ip_list_rep[1]) - self.client.create_floating_ip(network='nova') + self.cloud.create_floating_ip(network='nova') mock_nova_client.floating_ips.create.assert_called_with(pool='nova') @@ -160,7 +156,7 @@ def test_available_floating_ip_existing( mock__nova_list_floating_ips.return_value = \ self.mock_floating_ip_list_rep[:1] - ip = self.client.available_floating_ip(network='nova') + ip = self.cloud.available_floating_ip(network='nova') self.assertEqual(self.mock_floating_ip_list_rep[0]['ip'], ip['floating_ip_address']) @@ -176,7 +172,7 @@ def test_available_floating_ip_new( mock_nova_client.floating_ips.create.return_value = \ fakes.FakeFloatingIP(**self.mock_floating_ip_list_rep[0]) - ip = self.client.available_floating_ip(network='nova') + ip = self.cloud.available_floating_ip(network='nova') self.assertEqual(self.mock_floating_ip_list_rep[0]['ip'], ip['floating_ip_address']) @@ -188,7 +184,7 @@ def test_delete_floating_ip_existing( mock_has_service.side_effect = has_service_side_effect mock_nova_client.floating_ips.delete.return_value = None - ret = self.client.delete_floating_ip( + ret = self.cloud.delete_floating_ip( floating_ip_id='a-wild-id-appears') mock_nova_client.floating_ips.delete.assert_called_with( @@ -203,7 +199,7 @@ def test_delete_floating_ip_not_found( mock_nova_client.floating_ips.delete.side_effect = n_exc.NotFound( code=404) - ret = self.client.delete_floating_ip( + ret = self.cloud.delete_floating_ip( floating_ip_id='a-wild-id-appears') self.assertFalse(ret) @@ -214,7 +210,7 @@ def test_attach_ip_to_server(self, mock_has_service, mock_nova_client): mock_has_service.side_effect = has_service_side_effect mock_nova_client.floating_ips.list.return_value = self.floating_ips - self.client._attach_ip_to_server( + self.cloud._attach_ip_to_server( server=self.fake_server, floating_ip=self.floating_ip, fixed_address='192.0.2.129') @@ -230,7 +226,7 @@ def test_detach_ip_from_server(self, mock_has_service, mock_nova_client): fakes.FakeFloatingIP(**ip) for ip in self.mock_floating_ip_list_rep ] - self.client.detach_ip_from_server( + self.cloud.detach_ip_from_server( server_id='server-id', floating_ip_id=1) mock_nova_client.servers.remove_floating_ip.assert_called_with( @@ -242,7 +238,7 @@ def test_add_ip_from_pool(self, mock_has_service, mock_nova_client): mock_has_service.side_effect = has_service_side_effect mock_nova_client.floating_ips.list.return_value = self.floating_ips - server = self.client._add_ip_from_pool( + server = self.cloud._add_ip_from_pool( server=self.fake_server, network='nova', fixed_address='192.0.2.129') @@ -257,7 +253,7 @@ def test_cleanup_floating_ips( mock_delete_floating_ip): mock_use_neutron_floating.return_value = False - self.client.delete_unattached_floating_ips() + self.cloud.delete_unattached_floating_ips() mock_delete_floating_ip.assert_not_called() mock_list_floating_ips.assert_not_called() diff --git a/shade/tests/unit/test_floating_ip_pool.py b/shade/tests/unit/test_floating_ip_pool.py index b3ca44f21..fba39e843 100644 --- a/shade/tests/unit/test_floating_ip_pool.py +++ b/shade/tests/unit/test_floating_ip_pool.py @@ -20,7 +20,6 @@ """ from mock import patch -import os_client_config from shade import OpenStackCloud from shade import OpenStackCloudException from shade.tests.unit import base @@ -32,12 +31,6 @@ class TestFloatingIPPool(base.TestCase): {'id': 'pool1_id', 'name': 'pool1'}, {'id': 'pool2_id', 'name': 'pool2'}] - def setUp(self): - super(TestFloatingIPPool, self).setUp() - config = os_client_config.OpenStackConfig() - self.client = OpenStackCloud( - cloud_config=config.get_one_cloud(validate=False)) - @patch.object(OpenStackCloud, '_has_nova_extension') @patch.object(OpenStackCloud, 'nova_client') def test_list_floating_ip_pools( @@ -47,7 +40,7 @@ def test_list_floating_ip_pools( ] mock__has_nova_extension.return_value = True - floating_ip_pools = self.client.list_floating_ip_pools() + floating_ip_pools = self.cloud.list_floating_ip_pools() self.assertItemsEqual(floating_ip_pools, self.mock_pools) @@ -60,4 +53,4 @@ def test_list_floating_ip_pools_exception( mock__has_nova_extension.return_value = True self.assertRaises( - OpenStackCloudException, self.client.list_floating_ip_pools) + OpenStackCloudException, self.cloud.list_floating_ip_pools) diff --git a/shade/tests/unit/test_groups.py b/shade/tests/unit/test_groups.py index e5973b6ea..7acd23655 100644 --- a/shade/tests/unit/test_groups.py +++ b/shade/tests/unit/test_groups.py @@ -20,18 +20,14 @@ class TestGroups(base.TestCase): - def setUp(self): - super(TestGroups, self).setUp() - self.cloud = shade.operator_cloud(validate=False) - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_list_groups(self, mock_keystone): - self.cloud.list_groups() + self.op_cloud.list_groups() mock_keystone.groups.list.assert_called_once_with() @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_get_group(self, mock_keystone): - self.cloud.get_group('1234') + self.op_cloud.get_group('1234') mock_keystone.groups.list.assert_called_once_with() @mock.patch.object(shade.OpenStackCloud, 'keystone_client') @@ -39,7 +35,7 @@ def test_delete_group(self, mock_keystone): mock_keystone.groups.list.return_value = [ fakes.FakeGroup('1234', 'name', 'desc') ] - self.assertTrue(self.cloud.delete_group('1234')) + self.assertTrue(self.op_cloud.delete_group('1234')) mock_keystone.groups.list.assert_called_once_with() mock_keystone.groups.delete.assert_called_once_with( group='1234' @@ -47,7 +43,7 @@ def test_delete_group(self, mock_keystone): @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_create_group(self, mock_keystone): - self.cloud.create_group('test-group', 'test desc') + self.op_cloud.create_group('test-group', 'test desc') mock_keystone.groups.create.assert_called_once_with( name='test-group', description='test desc', domain=None ) @@ -57,7 +53,7 @@ def test_update_group(self, mock_keystone): mock_keystone.groups.list.return_value = [ fakes.FakeGroup('1234', 'name', 'desc') ] - self.cloud.update_group('1234', 'test-group', 'test desc') + self.op_cloud.update_group('1234', 'test-group', 'test desc') mock_keystone.groups.list.assert_called_once_with() mock_keystone.groups.update.assert_called_once_with( group='1234', name='test-group', description='test desc' diff --git a/shade/tests/unit/test_identity_roles.py b/shade/tests/unit/test_identity_roles.py index 7a7af3363..3582bb334 100644 --- a/shade/tests/unit/test_identity_roles.py +++ b/shade/tests/unit/test_identity_roles.py @@ -40,13 +40,9 @@ class TestIdentityRoles(base.TestCase): - def setUp(self): - super(TestIdentityRoles, self).setUp() - self.cloud = shade.operator_cloud(validate=False) - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_list_roles(self, mock_keystone): - self.cloud.list_roles() + self.op_cloud.list_roles() self.assertTrue(mock_keystone.roles.list.called) @mock.patch.object(shade.OpenStackCloud, 'keystone_client') @@ -54,7 +50,7 @@ def test_get_role(self, mock_keystone): role_obj = fakes.FakeRole(id='1234', name='fake_role') mock_keystone.roles.list.return_value = [role_obj] - role = self.cloud.get_role('fake_role') + role = self.op_cloud.get_role('fake_role') self.assertTrue(mock_keystone.roles.list.called) self.assertIsNotNone(role) @@ -67,7 +63,7 @@ def test_create_role(self, mock_keystone): role_obj = fakes.FakeRole(id='1234', name=role_name) mock_keystone.roles.create.return_value = role_obj - role = self.cloud.create_role(role_name) + role = self.op_cloud.create_role(role_name) mock_keystone.roles.create.assert_called_once_with( name=role_name @@ -80,7 +76,7 @@ def test_create_role(self, mock_keystone): def test_delete_role(self, mock_keystone, mock_get): role_obj = fakes.FakeRole(id='1234', name='aaa') mock_get.return_value = meta.obj_to_dict(role_obj) - self.assertTrue(self.cloud.delete_role('1234')) + self.assertTrue(self.op_cloud.delete_role('1234')) self.assertTrue(mock_keystone.roles.delete.called) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @@ -88,7 +84,7 @@ def test_delete_role(self, mock_keystone, mock_get): def test_list_role_assignments(self, mock_keystone, mock_api_version): mock_api_version.return_value = '3' mock_keystone.role_assignments.list.return_value = RAW_ROLE_ASSIGNMENTS - ret = self.cloud.list_role_assignments() + ret = self.op_cloud.list_role_assignments() mock_keystone.role_assignments.list.assert_called_once_with() normalized_assignments = _utils.normalize_role_assignments( RAW_ROLE_ASSIGNMENTS @@ -101,7 +97,7 @@ def test_list_role_assignments_filters(self, mock_keystone, mock_api_version): mock_api_version.return_value = '3' params = dict(user='123', domain='456', effective=True) - self.cloud.list_role_assignments(filters=params) + self.op_cloud.list_role_assignments(filters=params) mock_keystone.role_assignments.list.assert_called_once_with(**params) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @@ -114,7 +110,7 @@ def test_list_role_assignments_exception(self, mock_keystone, shade.OpenStackCloudException, "Failed to list role assignments" ): - self.cloud.list_role_assignments() + self.op_cloud.list_role_assignments() @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, 'keystone_client') @@ -123,11 +119,15 @@ def test_list_role_assignments_keystone_v2(self, mock_keystone, fake_role = fakes.FakeRole(id='1234', name='fake_role') mock_api_version.return_value = '2.0' mock_keystone.roles.roles_for_user.return_value = [fake_role] - ret = self.cloud.list_role_assignments(filters={'user': '2222', - 'project': '3333'}) - self.assertEqual(ret, [{'id': fake_role.id, - 'project': '3333', - 'user': '2222'}]) + ret = self.op_cloud.list_role_assignments( + filters={ + 'user': '2222', + 'project': '3333'}) + self.assertEqual( + ret, [{ + 'id': fake_role.id, + 'project': '3333', + 'user': '2222'}]) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, 'keystone_client') @@ -138,12 +138,16 @@ def test_list_role_assignments_keystone_v2_with_role(self, mock_keystone, mock_api_version.return_value = '2.0' mock_keystone.roles.roles_for_user.return_value = [fake_role1, fake_role2] - ret = self.cloud.list_role_assignments(filters={'role': fake_role1.id, - 'user': '2222', - 'project': '3333'}) - self.assertEqual(ret, [{'id': fake_role1.id, - 'project': '3333', - 'user': '2222'}]) + ret = self.op_cloud.list_role_assignments( + filters={ + 'role': fake_role1.id, + 'user': '2222', + 'project': '3333'}) + self.assertEqual( + ret, [{ + 'id': fake_role1.id, + 'project': '3333', + 'user': '2222'}]) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, 'keystone_client') @@ -154,7 +158,7 @@ def test_list_role_assignments_exception_v2(self, mock_keystone, shade.OpenStackCloudException, "Must provide project and user for keystone v2" ): - self.cloud.list_role_assignments() + self.op_cloud.list_role_assignments() @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, 'keystone_client') @@ -165,4 +169,4 @@ def test_list_role_assignments_exception_v2_no_project(self, mock_keystone, shade.OpenStackCloudException, "Must provide project and user for keystone v2" ): - self.cloud.list_role_assignments(filters={'user': '12345'}) + self.op_cloud.list_role_assignments(filters={'user': '12345'}) diff --git a/shade/tests/unit/test_image_snapshot.py b/shade/tests/unit/test_image_snapshot.py index 3fdb5b683..3c106a770 100644 --- a/shade/tests/unit/test_image_snapshot.py +++ b/shade/tests/unit/test_image_snapshot.py @@ -39,7 +39,8 @@ def test_create_image_snapshot_wait_until_active_never_active(self, mock_get.return_value = {'status': 'saving', 'id': self.image_id} self.assertRaises(exc.OpenStackCloudTimeout, self.cloud.create_image_snapshot, - 'test-snapshot', 'fake-server', wait=True, timeout=2) + 'test-snapshot', 'fake-server', + wait=True, timeout=0.01) @mock.patch.object(shade.OpenStackCloud, 'nova_client') @mock.patch.object(shade.OpenStackCloud, 'get_image') diff --git a/shade/tests/unit/test_magnum_services.py b/shade/tests/unit/test_magnum_services.py index 340a07cf5..500462358 100644 --- a/shade/tests/unit/test_magnum_services.py +++ b/shade/tests/unit/test_magnum_services.py @@ -33,13 +33,9 @@ class TestMagnumServices(base.TestCase): - def setUp(self): - super(TestMagnumServices, self).setUp() - self.cloud = shade.operator_cloud(validate=False) - @mock.patch.object(shade.OpenStackCloud, 'magnum_client') def test_list_magnum_services(self, mock_magnum): mock_magnum.mservices.list.return_value = [magnum_service_obj, ] - mservices_list = self.cloud.list_magnum_services() + mservices_list = self.op_cloud.list_magnum_services() mock_magnum.mservices.list.assert_called_with(detail=False) self.assertEqual(mservices_list[0], magnum_service_obj) diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index d5f7b22bb..6dc21e808 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -14,7 +14,6 @@ # under the License. import mock -import os_client_config from os_client_config import cloud_config from swiftclient import service as swift_service from swiftclient import exceptions as swift_exc @@ -23,18 +22,11 @@ import shade import shade.openstackcloud from shade import exc -from shade import OpenStackCloud from shade.tests.unit import base class TestObject(base.TestCase): - def setUp(self): - super(TestObject, self).setUp() - config = os_client_config.OpenStackConfig() - self.cloud = OpenStackCloud( - cloud_config=config.get_one_cloud(validate=False)) - @mock.patch.object(cloud_config.CloudConfig, 'get_session') def test_swift_client_no_endpoint(self, get_session_mock): session_mock = mock.Mock() diff --git a/shade/tests/unit/test_port.py b/shade/tests/unit/test_port.py index 03f931d9a..6994337f1 100644 --- a/shade/tests/unit/test_port.py +++ b/shade/tests/unit/test_port.py @@ -20,7 +20,6 @@ """ from mock import patch -import os_client_config from shade import OpenStackCloud from shade.exc import OpenStackCloudException from shade.tests.unit import base @@ -142,18 +141,12 @@ class TestPort(base.TestCase): ] } - def setUp(self): - super(TestPort, self).setUp() - config = os_client_config.OpenStackConfig() - self.client = OpenStackCloud( - cloud_config=config.get_one_cloud(validate=False)) - @patch.object(OpenStackCloud, 'neutron_client') def test_create_port(self, mock_neutron_client): mock_neutron_client.create_port.return_value = \ self.mock_neutron_port_create_rep - port = self.client.create_port( + port = self.cloud.create_port( network_id='test-net-id', name='test-port-name', admin_state_up=True) @@ -165,7 +158,7 @@ def test_create_port(self, mock_neutron_client): def test_create_port_parameters(self): """Test that we detect invalid arguments passed to create_port""" self.assertRaises( - TypeError, self.client.create_port, + TypeError, self.cloud.create_port, network_id='test-net-id', nome='test-port-name', stato_amministrativo_porta=True) @@ -174,7 +167,7 @@ def test_create_port_exception(self, mock_neutron_client): mock_neutron_client.create_port.side_effect = Exception('blah') self.assertRaises( - OpenStackCloudException, self.client.create_port, + OpenStackCloudException, self.cloud.create_port, network_id='test-net-id', name='test-port-name', admin_state_up=True) @@ -185,7 +178,7 @@ def test_update_port(self, mock_neutron_client): mock_neutron_client.update_port.return_value = \ self.mock_neutron_port_update_rep - port = self.client.update_port( + port = self.cloud.update_port( name_or_id='d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b', name='test-port-name-updated') @@ -197,7 +190,7 @@ def test_update_port(self, mock_neutron_client): def test_update_port_parameters(self): """Test that we detect invalid arguments passed to update_port""" self.assertRaises( - TypeError, self.client.update_port, + TypeError, self.cloud.update_port, name_or_id='test-port-id', nome='test-port-name-updated') @patch.object(OpenStackCloud, 'neutron_client') @@ -207,7 +200,7 @@ def test_update_port_exception(self, mock_neutron_client): mock_neutron_client.update_port.side_effect = Exception('blah') self.assertRaises( - OpenStackCloudException, self.client.update_port, + OpenStackCloudException, self.cloud.update_port, name_or_id='d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b', name='test-port-name-updated') @@ -216,7 +209,7 @@ def test_list_ports(self, mock_neutron_client): mock_neutron_client.list_ports.return_value = \ self.mock_neutron_port_list_rep - ports = self.client.list_ports() + ports = self.cloud.list_ports() mock_neutron_client.list_ports.assert_called_with() self.assertItemsEqual(self.mock_neutron_port_list_rep['ports'], ports) @@ -225,14 +218,14 @@ def test_list_ports(self, mock_neutron_client): def test_list_ports_exception(self, mock_neutron_client): mock_neutron_client.list_ports.side_effect = Exception('blah') - self.assertRaises(OpenStackCloudException, self.client.list_ports) + self.assertRaises(OpenStackCloudException, self.cloud.list_ports) @patch.object(OpenStackCloud, 'neutron_client') def test_search_ports_by_id(self, mock_neutron_client): mock_neutron_client.list_ports.return_value = \ self.mock_neutron_port_list_rep - ports = self.client.search_ports( + ports = self.cloud.search_ports( name_or_id='f71a6703-d6de-4be1-a91a-a570ede1d159') mock_neutron_client.list_ports.assert_called_with() @@ -244,7 +237,7 @@ def test_search_ports_by_name(self, mock_neutron_client): mock_neutron_client.list_ports.return_value = \ self.mock_neutron_port_list_rep - ports = self.client.search_ports(name_or_id='first-port') + ports = self.cloud.search_ports(name_or_id='first-port') mock_neutron_client.list_ports.assert_called_with() self.assertEquals(1, len(ports)) @@ -255,7 +248,7 @@ def test_search_ports_not_found(self, mock_neutron_client): mock_neutron_client.list_ports.return_value = \ self.mock_neutron_port_list_rep - ports = self.client.search_ports(name_or_id='non-existent') + ports = self.cloud.search_ports(name_or_id='non-existent') mock_neutron_client.list_ports.assert_called_with() self.assertEquals(0, len(ports)) @@ -265,7 +258,7 @@ def test_delete_port(self, mock_neutron_client): mock_neutron_client.list_ports.return_value = \ self.mock_neutron_port_list_rep - self.client.delete_port(name_or_id='first-port') + self.cloud.delete_port(name_or_id='first-port') mock_neutron_client.delete_port.assert_called_with( port='d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b') diff --git a/shade/tests/unit/test_project.py b/shade/tests/unit/test_project.py index 6b8b1ad62..0efd6cd6e 100644 --- a/shade/tests/unit/test_project.py +++ b/shade/tests/unit/test_project.py @@ -25,17 +25,13 @@ class TestProject(base.TestCase): - def setUp(self): - super(TestProject, self).setUp() - self.cloud = shade.operator_cloud(validate=False) - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_create_project_v2(self, mock_keystone, mock_api_version): mock_api_version.return_value = '2' name = 'project_name' description = 'Project description' - self.cloud.create_project(name=name, description=description) + self.op_cloud.create_project(name=name, description=description) mock_keystone.tenants.create.assert_called_once_with( project_name=name, description=description, enabled=True, tenant_name=name @@ -48,8 +44,8 @@ def test_create_project_v3(self, mock_keystone, mock_api_version): name = 'project_name' description = 'Project description' domain_id = '123' - self.cloud.create_project(name=name, description=description, - domain_id=domain_id) + self.op_cloud.create_project( + name=name, description=description, domain_id=domain_id) mock_keystone.projects.create.assert_called_once_with( project_name=name, description=description, enabled=True, name=name, domain=domain_id @@ -65,7 +61,7 @@ def test_create_project_v3_no_domain(self, mock_keystone, "User or project creation requires an explicit" " domain_id argument." ): - self.cloud.create_project(name='foo', description='bar') + self.op_cloud.create_project(name='foo', description='bar') @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, 'get_project') @@ -74,7 +70,7 @@ def test_delete_project_v2(self, mock_keystone, mock_get, mock_api_version): mock_api_version.return_value = '2' mock_get.return_value = dict(id='123') - self.assertTrue(self.cloud.delete_project('123')) + self.assertTrue(self.op_cloud.delete_project('123')) mock_get.assert_called_once_with('123', domain_id=None) mock_keystone.tenants.delete.assert_called_once_with(tenant='123') @@ -85,7 +81,7 @@ def test_delete_project_v3(self, mock_keystone, mock_get, mock_api_version): mock_api_version.return_value = '3' mock_get.return_value = dict(id='123') - self.assertTrue(self.cloud.delete_project('123')) + self.assertTrue(self.op_cloud.delete_project('123')) mock_get.assert_called_once_with('123', domain_id=None) mock_keystone.projects.delete.assert_called_once_with(project='123') @@ -96,7 +92,7 @@ def test_update_project_not_found(self, mock_get_project): shade.OpenStackCloudException, "Project ABC not found." ): - self.cloud.update_project('ABC') + self.op_cloud.update_project('ABC') @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, 'get_project') @@ -105,7 +101,7 @@ def test_update_project_v2(self, mock_keystone, mock_get_project, mock_api_version): mock_api_version.return_value = '2' mock_get_project.return_value = munch.Munch(dict(id='123')) - self.cloud.update_project('123', description='new', enabled=False) + self.op_cloud.update_project('123', description='new', enabled=False) mock_keystone.tenants.update.assert_called_once_with( description='new', enabled=False, tenant_id='123') @@ -116,7 +112,7 @@ def test_update_project_v3(self, mock_keystone, mock_get_project, mock_api_version): mock_api_version.return_value = '3' mock_get_project.return_value = munch.Munch(dict(id='123')) - self.cloud.update_project('123', description='new', enabled=False) + self.op_cloud.update_project('123', description='new', enabled=False) mock_keystone.projects.update.assert_called_once_with( description='new', enabled=False, project='123') @@ -124,6 +120,6 @@ def test_update_project_v3(self, mock_keystone, mock_get_project, @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_list_projects_v3(self, mock_keystone, mock_api_version): mock_api_version.return_value = '3' - self.cloud.list_projects('123') + self.op_cloud.list_projects('123') mock_keystone.projects.list.assert_called_once_with( domain='123') diff --git a/shade/tests/unit/test_quotas.py b/shade/tests/unit/test_quotas.py index 01160e37e..40565dff4 100644 --- a/shade/tests/unit/test_quotas.py +++ b/shade/tests/unit/test_quotas.py @@ -22,16 +22,12 @@ class TestQuotas(base.TestCase): - def setUp(self): - super(TestQuotas, self).setUp() - self.cloud = shade.operator_cloud(validate=False) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_update_quotas(self, mock_keystone, mock_nova): project = fakes.FakeProject('project_a') mock_keystone.tenants.list.return_value = [project] - self.cloud.set_compute_quotas(project, cores=1) + self.op_cloud.set_compute_quotas(project, cores=1) mock_nova.quotas.update.assert_called_once_with( cores=1, force=True, tenant_id='project_a') @@ -41,7 +37,7 @@ def test_update_quotas(self, mock_keystone, mock_nova): def test_get_quotas(self, mock_keystone, mock_nova): project = fakes.FakeProject('project_a') mock_keystone.tenants.list.return_value = [project] - self.cloud.get_compute_quotas(project) + self.op_cloud.get_compute_quotas(project) mock_nova.quotas.get.assert_called_once_with(tenant_id='project_a') @@ -50,7 +46,7 @@ def test_get_quotas(self, mock_keystone, mock_nova): def test_delete_quotas(self, mock_keystone, mock_nova): project = fakes.FakeProject('project_a') mock_keystone.tenants.list.return_value = [project] - self.cloud.delete_compute_quotas(project) + self.op_cloud.delete_compute_quotas(project) mock_nova.quotas.delete.assert_called_once_with(tenant_id='project_a') @@ -59,7 +55,7 @@ def test_delete_quotas(self, mock_keystone, mock_nova): def test_cinder_update_quotas(self, mock_keystone, mock_cinder): project = fakes.FakeProject('project_a') mock_keystone.tenants.list.return_value = [project] - self.cloud.set_volume_quotas(project, volumes=1) + self.op_cloud.set_volume_quotas(project, volumes=1) mock_cinder.quotas.update.assert_called_once_with( volumes=1, tenant_id='project_a') @@ -69,7 +65,7 @@ def test_cinder_update_quotas(self, mock_keystone, mock_cinder): def test_cinder_get_quotas(self, mock_keystone, mock_cinder): project = fakes.FakeProject('project_a') mock_keystone.tenants.list.return_value = [project] - self.cloud.get_volume_quotas(project) + self.op_cloud.get_volume_quotas(project) mock_cinder.quotas.get.assert_called_once_with(tenant_id='project_a') @@ -78,7 +74,7 @@ def test_cinder_get_quotas(self, mock_keystone, mock_cinder): def test_cinder_delete_quotas(self, mock_keystone, mock_cinder): project = fakes.FakeProject('project_a') mock_keystone.tenants.list.return_value = [project] - self.cloud.delete_volume_quotas(project) + self.op_cloud.delete_volume_quotas(project) mock_cinder.quotas.delete.assert_called_once_with( tenant_id='project_a') @@ -88,7 +84,7 @@ def test_cinder_delete_quotas(self, mock_keystone, mock_cinder): def test_neutron_update_quotas(self, mock_keystone, mock_neutron): project = fakes.FakeProject('project_a') mock_keystone.tenants.list.return_value = [project] - self.cloud.set_network_quotas(project, network=1) + self.op_cloud.set_network_quotas(project, network=1) mock_neutron.update_quota.assert_called_once_with( body={'quota': {'network': 1}}, tenant_id='project_a') @@ -98,7 +94,7 @@ def test_neutron_update_quotas(self, mock_keystone, mock_neutron): def test_neutron_get_quotas(self, mock_keystone, mock_neutron): project = fakes.FakeProject('project_a') mock_keystone.tenants.list.return_value = [project] - self.cloud.get_network_quotas(project) + self.op_cloud.get_network_quotas(project) mock_neutron.show_quota.assert_called_once_with( tenant_id='project_a') @@ -108,7 +104,7 @@ def test_neutron_get_quotas(self, mock_keystone, mock_neutron): def test_neutron_delete_quotas(self, mock_keystone, mock_neutron): project = fakes.FakeProject('project_a') mock_keystone.tenants.list.return_value = [project] - self.cloud.delete_network_quotas(project) + self.op_cloud.delete_network_quotas(project) mock_neutron.delete_quota.assert_called_once_with( tenant_id='project_a') diff --git a/shade/tests/unit/test_rebuild_server.py b/shade/tests/unit/test_rebuild_server.py index 8156e57e4..5cb81eab4 100644 --- a/shade/tests/unit/test_rebuild_server.py +++ b/shade/tests/unit/test_rebuild_server.py @@ -20,22 +20,16 @@ """ from mock import patch, Mock -import os_client_config from shade import _utils from shade import meta from shade import OpenStackCloud from shade.exc import (OpenStackCloudException, OpenStackCloudTimeout) -from shade.tests import base, fakes +from shade.tests import fakes +from shade.tests.unit import base class TestRebuildServer(base.TestCase): - def setUp(self): - super(TestRebuildServer, self).setUp() - config = os_client_config.OpenStackConfig() - self.client = OpenStackCloud( - cloud_config=config.get_one_cloud(validate=False)) - def test_rebuild_server_rebuild_exception(self): """ Test that an exception in the novaclient rebuild raises an exception in @@ -47,7 +41,7 @@ def test_rebuild_server_rebuild_exception(self): } OpenStackCloud.nova_client = Mock(**config) self.assertRaises( - OpenStackCloudException, self.client.rebuild_server, "a", "b") + OpenStackCloudException, self.cloud.rebuild_server, "a", "b") def test_rebuild_server_server_error(self): """ @@ -68,7 +62,7 @@ def test_rebuild_server_server_error(self): OpenStackCloud.nova_client = Mock(**config) self.assertRaises( OpenStackCloudException, - self.client.rebuild_server, "a", "b", wait=True) + self.cloud.rebuild_server, "a", "b", wait=True) def test_rebuild_server_timeout(self): """ @@ -84,7 +78,7 @@ def test_rebuild_server_timeout(self): OpenStackCloud.nova_client = Mock(**config) self.assertRaises( OpenStackCloudTimeout, - self.client.rebuild_server, "a", "b", wait=True, timeout=0.001) + self.cloud.rebuild_server, "a", "b", wait=True, timeout=0.001) def test_rebuild_server_no_wait(self): """ @@ -98,7 +92,7 @@ def test_rebuild_server_no_wait(self): } OpenStackCloud.nova_client = Mock(**config) self.assertEqual(meta.obj_to_dict(rebuild_server), - self.client.rebuild_server("a", "b")) + self.cloud.rebuild_server("a", "b")) def test_rebuild_server_with_admin_pass_no_wait(self): """ @@ -113,8 +107,8 @@ def test_rebuild_server_with_admin_pass_no_wait(self): OpenStackCloud.nova_client = Mock(**config) self.assertEqual( meta.obj_to_dict(rebuild_server), - self.client.rebuild_server('a', 'b', - admin_pass='ooBootheiX0edoh')) + self.cloud.rebuild_server( + 'a', 'b', admin_pass='ooBootheiX0edoh')) def test_rebuild_server_with_admin_pass_wait(self): """ @@ -135,13 +129,13 @@ def test_rebuild_server_with_admin_pass_wait(self): "floating_ips.list.return_value": [fake_floating_ip] } OpenStackCloud.nova_client = Mock(**config) - self.client.name = 'cloud-name' + self.cloud.name = 'cloud-name' self.assertEqual( _utils.normalize_server( meta.obj_to_dict(ret_active_server), - cloud_name='cloud-name', region_name=''), - self.client.rebuild_server("a", "b", wait=True, - admin_pass='ooBootheiX0edoh')) + cloud_name='cloud-name', region_name='RegionOne'), + self.cloud.rebuild_server( + "a", "b", wait=True, admin_pass='ooBootheiX0edoh')) def test_rebuild_server_wait(self): """ @@ -160,9 +154,9 @@ def test_rebuild_server_wait(self): "floating_ips.list.return_value": [fake_floating_ip] } OpenStackCloud.nova_client = Mock(**config) - self.client.name = 'cloud-name' + self.cloud.name = 'cloud-name' self.assertEqual( _utils.normalize_server( meta.obj_to_dict(active_server), - cloud_name='cloud-name', region_name=''), - self.client.rebuild_server("a", "b", wait=True)) + cloud_name='cloud-name', region_name='RegionOne'), + self.cloud.rebuild_server("a", "b", wait=True)) diff --git a/shade/tests/unit/test_role_assignment.py b/shade/tests/unit/test_role_assignment.py index 5263633dc..4aa1eceff 100644 --- a/shade/tests/unit/test_role_assignment.py +++ b/shade/tests/unit/test_role_assignment.py @@ -13,10 +13,11 @@ from mock import patch import os_client_config as occ -from shade import OperatorCloud, operator_cloud +from shade import OperatorCloud from shade.exc import OpenStackCloudException, OpenStackCloudTimeout from shade.meta import obj_to_dict -from shade.tests import base, fakes +from shade.tests import fakes +from shade.tests.unit import base import testtools @@ -24,7 +25,6 @@ class TestRoleAssignment(base.TestCase): def setUp(self): super(TestRoleAssignment, self).setUp() - self.cloud = operator_cloud(validate=False) self.fake_role = obj_to_dict(fakes.FakeRole('12345', 'test')) self.fake_user = obj_to_dict(fakes.FakeUser('12345', 'test@nobody.org', @@ -102,12 +102,16 @@ def test_grant_role_user_v2(self, mock_keystone, mock_api_version): mock_keystone.tenants.list.return_value = [self.fake_project] mock_keystone.roles.roles_for_user.return_value = [] mock_keystone.roles.add_user_role.return_value = self.fake_role - self.assertTrue(self.cloud.grant_role(self.fake_role['name'], - user=self.fake_user['name'], - project=self.fake_project['id'])) - self.assertTrue(self.cloud.grant_role(self.fake_role['name'], - user=self.fake_user['id'], - project=self.fake_project['id'])) + self.assertTrue( + self.op_cloud.grant_role( + self.fake_role['name'], + user=self.fake_user['name'], + project=self.fake_project['id'])) + self.assertTrue( + self.op_cloud.grant_role( + self.fake_role['name'], + user=self.fake_user['id'], + project=self.fake_project['id'])) @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @patch.object(OperatorCloud, 'keystone_client') @@ -118,18 +122,26 @@ def test_grant_role_user_project_v2(self, mock_keystone, mock_api_version): mock_keystone.users.list.return_value = [self.fake_user] mock_keystone.roles.roles_for_user.return_value = [] mock_keystone.roles.add_user_role.return_value = self.fake_role - self.assertTrue(self.cloud.grant_role(self.fake_role['name'], - user=self.fake_user['name'], - project=self.fake_project['id'])) - self.assertTrue(self.cloud.grant_role(self.fake_role['name'], - user=self.fake_user['id'], - project=self.fake_project['id'])) - self.assertTrue(self.cloud.grant_role(self.fake_role['id'], - user=self.fake_user['name'], - project=self.fake_project['id'])) - self.assertTrue(self.cloud.grant_role(self.fake_role['id'], - user=self.fake_user['id'], - project=self.fake_project['id'])) + self.assertTrue( + self.op_cloud.grant_role( + self.fake_role['name'], + user=self.fake_user['name'], + project=self.fake_project['id'])) + self.assertTrue( + self.op_cloud.grant_role( + self.fake_role['name'], + user=self.fake_user['id'], + project=self.fake_project['id'])) + self.assertTrue( + self.op_cloud.grant_role( + self.fake_role['id'], + user=self.fake_user['name'], + project=self.fake_project['id'])) + self.assertTrue( + self.op_cloud.grant_role( + self.fake_role['id'], + user=self.fake_user['id'], + project=self.fake_project['id'])) @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @patch.object(OperatorCloud, 'keystone_client') @@ -141,7 +153,7 @@ def test_grant_role_user_project_v2_exists(self, mock_keystone.tenants.list.return_value = [self.fake_project] mock_keystone.users.list.return_value = [self.fake_user] mock_keystone.roles.roles_for_user.return_value = [self.fake_role] - self.assertFalse(self.cloud.grant_role( + self.assertFalse(self.op_cloud.grant_role( self.fake_role['name'], user=self.fake_user['name'], project=self.fake_project['id'])) @@ -154,12 +166,16 @@ def test_grant_role_user_project(self, mock_keystone, mock_api_version): mock_keystone.projects.list.return_value = [self.fake_project] mock_keystone.users.list.return_value = [self.fake_user] mock_keystone.role_assignments.list.return_value = [] - self.assertTrue(self.cloud.grant_role(self.fake_role['name'], - user=self.fake_user['name'], - project=self.fake_project['id'])) - self.assertTrue(self.cloud.grant_role(self.fake_role['name'], - user=self.fake_user['id'], - project=self.fake_project['id'])) + self.assertTrue( + self.op_cloud.grant_role( + self.fake_role['name'], + user=self.fake_user['name'], + project=self.fake_project['id'])) + self.assertTrue( + self.op_cloud.grant_role( + self.fake_role['name'], + user=self.fake_user['id'], + project=self.fake_project['id'])) @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @patch.object(OperatorCloud, 'keystone_client') @@ -172,11 +188,11 @@ def test_grant_role_user_project_exists(self, mock_keystone.users.list.return_value = [self.fake_user] mock_keystone.role_assignments.list.return_value = \ [self.user_project_assignment] - self.assertFalse(self.cloud.grant_role( + self.assertFalse(self.op_cloud.grant_role( self.fake_role['name'], user=self.fake_user['name'], project=self.fake_project['id'])) - self.assertFalse(self.cloud.grant_role( + self.assertFalse(self.op_cloud.grant_role( self.fake_role['id'], user=self.fake_user['id'], project=self.fake_project['id'])) @@ -189,11 +205,11 @@ def test_grant_role_group_project(self, mock_keystone, mock_api_version): mock_keystone.projects.list.return_value = [self.fake_project] mock_keystone.groups.list.return_value = [self.fake_group] mock_keystone.role_assignments.list.return_value = [] - self.assertTrue(self.cloud.grant_role( + self.assertTrue(self.op_cloud.grant_role( self.fake_role['name'], group=self.fake_group['name'], project=self.fake_project['id'])) - self.assertTrue(self.cloud.grant_role( + self.assertTrue(self.op_cloud.grant_role( self.fake_role['name'], group=self.fake_group['id'], project=self.fake_project['id'])) @@ -209,11 +225,11 @@ def test_grant_role_group_project_exists(self, mock_keystone.groups.list.return_value = [self.fake_group] mock_keystone.role_assignments.list.return_value = \ [self.group_project_assignment] - self.assertFalse(self.cloud.grant_role( + self.assertFalse(self.op_cloud.grant_role( self.fake_role['name'], group=self.fake_group['name'], project=self.fake_project['id'])) - self.assertFalse(self.cloud.grant_role( + self.assertFalse(self.op_cloud.grant_role( self.fake_role['name'], group=self.fake_group['id'], project=self.fake_project['id'])) @@ -226,19 +242,19 @@ def test_grant_role_user_domain(self, mock_keystone, mock_api_version): mock_keystone.users.list.return_value = [self.fake_user] mock_keystone.domains.get.return_value = self.fake_domain mock_keystone.role_assignments.list.return_value = [] - self.assertTrue(self.cloud.grant_role( + self.assertTrue(self.op_cloud.grant_role( self.fake_role['name'], user=self.fake_user['name'], domain=self.fake_domain['id'])) - self.assertTrue(self.cloud.grant_role( + self.assertTrue(self.op_cloud.grant_role( self.fake_role['name'], user=self.fake_user['id'], domain=self.fake_domain['id'])) - self.assertTrue(self.cloud.grant_role( + self.assertTrue(self.op_cloud.grant_role( self.fake_role['name'], user=self.fake_user['name'], domain=self.fake_domain['name'])) - self.assertTrue(self.cloud.grant_role( + self.assertTrue(self.op_cloud.grant_role( self.fake_role['name'], user=self.fake_user['id'], domain=self.fake_domain['name'])) @@ -254,19 +270,19 @@ def test_grant_role_user_domain_exists(self, mock_keystone.domains.get.return_value = self.fake_domain mock_keystone.role_assignments.list.return_value = \ [self.user_domain_assignment] - self.assertFalse(self.cloud.grant_role( + self.assertFalse(self.op_cloud.grant_role( self.fake_role['name'], user=self.fake_user['name'], domain=self.fake_domain['name'])) - self.assertFalse(self.cloud.grant_role( + self.assertFalse(self.op_cloud.grant_role( self.fake_role['name'], user=self.fake_user['id'], domain=self.fake_domain['name'])) - self.assertFalse(self.cloud.grant_role( + self.assertFalse(self.op_cloud.grant_role( self.fake_role['name'], user=self.fake_user['name'], domain=self.fake_domain['id'])) - self.assertFalse(self.cloud.grant_role( + self.assertFalse(self.op_cloud.grant_role( self.fake_role['name'], user=self.fake_user['id'], domain=self.fake_domain['id'])) @@ -279,19 +295,19 @@ def test_grant_role_group_domain(self, mock_keystone, mock_api_version): mock_keystone.roles.list.return_value = [self.fake_role] mock_keystone.domains.get.return_value = self.fake_domain mock_keystone.role_assignments.list.return_value = [] - self.assertTrue(self.cloud.grant_role( + self.assertTrue(self.op_cloud.grant_role( self.fake_role['name'], group=self.fake_group['name'], domain=self.fake_domain['name'])) - self.assertTrue(self.cloud.grant_role( + self.assertTrue(self.op_cloud.grant_role( self.fake_role['name'], group=self.fake_group['id'], domain=self.fake_domain['name'])) - self.assertTrue(self.cloud.grant_role( + self.assertTrue(self.op_cloud.grant_role( self.fake_role['name'], group=self.fake_group['name'], domain=self.fake_domain['id'])) - self.assertTrue(self.cloud.grant_role( + self.assertTrue(self.op_cloud.grant_role( self.fake_role['name'], group=self.fake_group['id'], domain=self.fake_domain['id'])) @@ -307,19 +323,19 @@ def test_grant_role_group_domain_exists(self, mock_keystone.domains.get.return_value = self.fake_domain mock_keystone.role_assignments.list.return_value = \ [self.group_domain_assignment] - self.assertFalse(self.cloud.grant_role( + self.assertFalse(self.op_cloud.grant_role( self.fake_role['name'], group=self.fake_group['name'], domain=self.fake_domain['name'])) - self.assertFalse(self.cloud.grant_role( + self.assertFalse(self.op_cloud.grant_role( self.fake_role['name'], group=self.fake_group['id'], domain=self.fake_domain['name'])) - self.assertFalse(self.cloud.grant_role( + self.assertFalse(self.op_cloud.grant_role( self.fake_role['name'], group=self.fake_group['name'], domain=self.fake_domain['id'])) - self.assertFalse(self.cloud.grant_role( + self.assertFalse(self.op_cloud.grant_role( self.fake_role['name'], group=self.fake_group['id'], domain=self.fake_domain['id'])) @@ -333,11 +349,11 @@ def test_revoke_role_user_v2(self, mock_keystone, mock_api_version): mock_keystone.tenants.list.return_value = [self.fake_project] mock_keystone.roles.roles_for_user.return_value = [self.fake_role] mock_keystone.roles.remove_user_role.return_value = self.fake_role - self.assertTrue(self.cloud.revoke_role( + self.assertTrue(self.op_cloud.revoke_role( self.fake_role['name'], user=self.fake_user['name'], project=self.fake_project['id'])) - self.assertTrue(self.cloud.revoke_role( + self.assertTrue(self.op_cloud.revoke_role( self.fake_role['name'], user=self.fake_user['id'], project=self.fake_project['id'])) @@ -352,19 +368,19 @@ def test_revoke_role_user_project_v2(self, mock_keystone.tenants.list.return_value = [self.fake_project] mock_keystone.users.list.return_value = [self.fake_user] mock_keystone.roles.roles_for_user.return_value = [] - self.assertFalse(self.cloud.revoke_role( + self.assertFalse(self.op_cloud.revoke_role( self.fake_role['name'], user=self.fake_user['name'], project=self.fake_project['id'])) - self.assertFalse(self.cloud.revoke_role( + self.assertFalse(self.op_cloud.revoke_role( self.fake_role['name'], user=self.fake_user['id'], project=self.fake_project['id'])) - self.assertFalse(self.cloud.revoke_role( + self.assertFalse(self.op_cloud.revoke_role( self.fake_role['id'], user=self.fake_user['name'], project=self.fake_project['id'])) - self.assertFalse(self.cloud.revoke_role( + self.assertFalse(self.op_cloud.revoke_role( self.fake_role['id'], user=self.fake_user['id'], project=self.fake_project['id'])) @@ -380,7 +396,7 @@ def test_revoke_role_user_project_v2_exists(self, mock_keystone.users.list.return_value = [self.fake_user] mock_keystone.roles.roles_for_user.return_value = [self.fake_role] mock_keystone.roles.remove_user_role.return_value = self.fake_role - self.assertTrue(self.cloud.revoke_role( + self.assertTrue(self.op_cloud.revoke_role( self.fake_role['name'], user=self.fake_user['name'], project=self.fake_project['id'])) @@ -393,11 +409,11 @@ def test_revoke_role_user_project(self, mock_keystone, mock_api_version): mock_keystone.projects.list.return_value = [self.fake_project] mock_keystone.users.list.return_value = [self.fake_user] mock_keystone.role_assignments.list.return_value = [] - self.assertFalse(self.cloud.revoke_role( + self.assertFalse(self.op_cloud.revoke_role( self.fake_role['name'], user=self.fake_user['name'], project=self.fake_project['id'])) - self.assertFalse(self.cloud.revoke_role( + self.assertFalse(self.op_cloud.revoke_role( self.fake_role['name'], user=self.fake_user['id'], project=self.fake_project['id'])) @@ -413,11 +429,11 @@ def test_revoke_role_user_project_exists(self, mock_keystone.users.list.return_value = [self.fake_user] mock_keystone.role_assignments.list.return_value = \ [self.user_project_assignment] - self.assertTrue(self.cloud.revoke_role( + self.assertTrue(self.op_cloud.revoke_role( self.fake_role['name'], user=self.fake_user['name'], project=self.fake_project['id'])) - self.assertTrue(self.cloud.revoke_role( + self.assertTrue(self.op_cloud.revoke_role( self.fake_role['id'], user=self.fake_user['id'], project=self.fake_project['id'])) @@ -430,11 +446,11 @@ def test_revoke_role_group_project(self, mock_keystone, mock_api_version): mock_keystone.projects.list.return_value = [self.fake_project] mock_keystone.groups.list.return_value = [self.fake_group] mock_keystone.role_assignments.list.return_value = [] - self.assertFalse(self.cloud.revoke_role( + self.assertFalse(self.op_cloud.revoke_role( self.fake_role['name'], group=self.fake_group['name'], project=self.fake_project['id'])) - self.assertFalse(self.cloud.revoke_role( + self.assertFalse(self.op_cloud.revoke_role( self.fake_role['name'], group=self.fake_group['id'], project=self.fake_project['id'])) @@ -450,11 +466,11 @@ def test_revoke_role_group_project_exists(self, mock_keystone.groups.list.return_value = [self.fake_group] mock_keystone.role_assignments.list.return_value = \ [self.group_project_assignment] - self.assertTrue(self.cloud.revoke_role( + self.assertTrue(self.op_cloud.revoke_role( self.fake_role['name'], group=self.fake_group['name'], project=self.fake_project['id'])) - self.assertTrue(self.cloud.revoke_role( + self.assertTrue(self.op_cloud.revoke_role( self.fake_role['name'], group=self.fake_group['id'], project=self.fake_project['id'])) @@ -467,19 +483,19 @@ def test_revoke_role_user_domain(self, mock_keystone, mock_api_version): mock_keystone.users.list.return_value = [self.fake_user] mock_keystone.domains.get.return_value = self.fake_domain mock_keystone.role_assignments.list.return_value = [] - self.assertFalse(self.cloud.revoke_role( + self.assertFalse(self.op_cloud.revoke_role( self.fake_role['name'], user=self.fake_user['name'], domain=self.fake_domain['id'])) - self.assertFalse(self.cloud.revoke_role( + self.assertFalse(self.op_cloud.revoke_role( self.fake_role['name'], user=self.fake_user['id'], domain=self.fake_domain['id'])) - self.assertFalse(self.cloud.revoke_role( + self.assertFalse(self.op_cloud.revoke_role( self.fake_role['name'], user=self.fake_user['name'], domain=self.fake_domain['name'])) - self.assertFalse(self.cloud.revoke_role( + self.assertFalse(self.op_cloud.revoke_role( self.fake_role['name'], user=self.fake_user['id'], domain=self.fake_domain['name'])) @@ -495,19 +511,19 @@ def test_revoke_role_user_domain_exists(self, mock_keystone.domains.get.return_value = self.fake_domain mock_keystone.role_assignments.list.return_value = \ [self.user_domain_assignment] - self.assertTrue(self.cloud.revoke_role( + self.assertTrue(self.op_cloud.revoke_role( self.fake_role['name'], user=self.fake_user['name'], domain=self.fake_domain['name'])) - self.assertTrue(self.cloud.revoke_role( + self.assertTrue(self.op_cloud.revoke_role( self.fake_role['name'], user=self.fake_user['id'], domain=self.fake_domain['name'])) - self.assertTrue(self.cloud.revoke_role( + self.assertTrue(self.op_cloud.revoke_role( self.fake_role['name'], user=self.fake_user['name'], domain=self.fake_domain['id'])) - self.assertTrue(self.cloud.revoke_role( + self.assertTrue(self.op_cloud.revoke_role( self.fake_role['name'], user=self.fake_user['id'], domain=self.fake_domain['id'])) @@ -520,19 +536,19 @@ def test_revoke_role_group_domain(self, mock_keystone, mock_api_version): mock_keystone.roles.list.return_value = [self.fake_role] mock_keystone.domains.get.return_value = self.fake_domain mock_keystone.role_assignments.list.return_value = [] - self.assertFalse(self.cloud.revoke_role( + self.assertFalse(self.op_cloud.revoke_role( self.fake_role['name'], group=self.fake_group['name'], domain=self.fake_domain['name'])) - self.assertFalse(self.cloud.revoke_role( + self.assertFalse(self.op_cloud.revoke_role( self.fake_role['name'], group=self.fake_group['id'], domain=self.fake_domain['name'])) - self.assertFalse(self.cloud.revoke_role( + self.assertFalse(self.op_cloud.revoke_role( self.fake_role['name'], group=self.fake_group['name'], domain=self.fake_domain['id'])) - self.assertFalse(self.cloud.revoke_role( + self.assertFalse(self.op_cloud.revoke_role( self.fake_role['name'], group=self.fake_group['id'], domain=self.fake_domain['id'])) @@ -548,19 +564,19 @@ def test_revoke_role_group_domain_exists(self, mock_keystone.domains.get.return_value = self.fake_domain mock_keystone.role_assignments.list.return_value = \ [self.group_domain_assignment] - self.assertTrue(self.cloud.revoke_role( + self.assertTrue(self.op_cloud.revoke_role( self.fake_role['name'], group=self.fake_group['name'], domain=self.fake_domain['name'])) - self.assertTrue(self.cloud.revoke_role( + self.assertTrue(self.op_cloud.revoke_role( self.fake_role['name'], group=self.fake_group['id'], domain=self.fake_domain['name'])) - self.assertTrue(self.cloud.revoke_role( + self.assertTrue(self.op_cloud.revoke_role( self.fake_role['name'], group=self.fake_group['name'], domain=self.fake_domain['id'])) - self.assertTrue(self.cloud.revoke_role( + self.assertTrue(self.op_cloud.revoke_role( self.fake_role['name'], group=self.fake_group['id'], domain=self.fake_domain['id'])) @@ -575,9 +591,10 @@ def test_grant_no_role(self, mock_keystone, mock_api_version): OpenStackCloudException, 'Role {0} not found'.format(self.fake_role['name']) ): - self.cloud.grant_role(self.fake_role['name'], - group=self.fake_group['name'], - domain=self.fake_domain['name']) + self.op_cloud.grant_role( + self.fake_role['name'], + group=self.fake_group['name'], + domain=self.fake_domain['name']) @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @patch.object(OperatorCloud, 'keystone_client') @@ -588,9 +605,10 @@ def test_revoke_no_role(self, mock_keystone, mock_api_version): OpenStackCloudException, 'Role {0} not found'.format(self.fake_role['name']) ): - self.cloud.revoke_role(self.fake_role['name'], - group=self.fake_group['name'], - domain=self.fake_domain['name']) + self.op_cloud.revoke_role( + self.fake_role['name'], + group=self.fake_group['name'], + domain=self.fake_domain['name']) @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @patch.object(OperatorCloud, 'keystone_client') @@ -603,7 +621,7 @@ def test_grant_no_user_or_group_specified(self, OpenStackCloudException, 'Must specify either a user or a group' ): - self.cloud.grant_role(self.fake_role['name']) + self.op_cloud.grant_role(self.fake_role['name']) @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @patch.object(OperatorCloud, 'keystone_client') @@ -616,7 +634,7 @@ def test_revoke_no_user_or_group_specified(self, OpenStackCloudException, 'Must specify either a user or a group' ): - self.cloud.revoke_role(self.fake_role['name']) + self.op_cloud.revoke_role(self.fake_role['name']) @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @patch.object(OperatorCloud, 'keystone_client') @@ -628,8 +646,9 @@ def test_grant_no_user_or_group(self, mock_keystone, mock_api_version): OpenStackCloudException, 'Must specify either a user or a group' ): - self.cloud.grant_role(self.fake_role['name'], - user=self.fake_user['name']) + self.op_cloud.grant_role( + self.fake_role['name'], + user=self.fake_user['name']) @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @patch.object(OperatorCloud, 'keystone_client') @@ -641,8 +660,9 @@ def test_revoke_no_user_or_group(self, mock_keystone, mock_api_version): OpenStackCloudException, 'Must specify either a user or a group' ): - self.cloud.revoke_role(self.fake_role['name'], - user=self.fake_user['name']) + self.op_cloud.revoke_role( + self.fake_role['name'], + user=self.fake_user['name']) @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @patch.object(OperatorCloud, 'keystone_client') @@ -655,9 +675,10 @@ def test_grant_both_user_and_group(self, mock_keystone, mock_api_version): OpenStackCloudException, 'Specify either a group or a user, not both' ): - self.cloud.grant_role(self.fake_role['name'], - user=self.fake_user['name'], - group=self.fake_group['name']) + self.op_cloud.grant_role( + self.fake_role['name'], + user=self.fake_user['name'], + group=self.fake_group['name']) @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @patch.object(OperatorCloud, 'keystone_client') @@ -670,9 +691,10 @@ def test_revoke_both_user_and_group(self, mock_keystone, mock_api_version): OpenStackCloudException, 'Specify either a group or a user, not both' ): - self.cloud.revoke_role(self.fake_role['name'], - user=self.fake_user['name'], - group=self.fake_group['name']) + self.op_cloud.revoke_role( + self.fake_role['name'], + user=self.fake_user['name'], + group=self.fake_group['name']) @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @patch.object(OperatorCloud, 'keystone_client') @@ -688,10 +710,12 @@ def test_grant_both_project_and_domain(self, mock_keystone.users.list.return_value = [self.fake_user, fake_user2] mock_keystone.projects.list.return_value = [self.fake_project] mock_keystone.domains.get.return_value = self.fake_domain - self.assertTrue(self.cloud.grant_role(self.fake_role['name'], - user=self.fake_user['name'], - project=self.fake_project['id'], - domain=self.fake_domain['name'])) + self.assertTrue( + self.op_cloud.grant_role( + self.fake_role['name'], + user=self.fake_user['name'], + project=self.fake_project['id'], + domain=self.fake_domain['name'])) @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @patch.object(OperatorCloud, 'keystone_client') @@ -709,7 +733,7 @@ def test_revoke_both_project_and_domain(self, mock_keystone.domains.get.return_value = self.fake_domain mock_keystone.role_assignments.list.return_value = \ [self.user_project_assignment] - self.assertTrue(self.cloud.revoke_role( + self.assertTrue(self.op_cloud.revoke_role( self.fake_role['name'], user=self.fake_user['name'], project=self.fake_project['id'], @@ -727,8 +751,9 @@ def test_grant_no_project_or_domain(self, mock_keystone, mock_api_version): OpenStackCloudException, 'Must specify either a domain or project' ): - self.cloud.grant_role(self.fake_role['name'], - user=self.fake_user['name']) + self.op_cloud.grant_role( + self.fake_role['name'], + user=self.fake_user['name']) @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @patch.object(OperatorCloud, 'keystone_client') @@ -746,8 +771,9 @@ def test_revoke_no_project_or_domain(self, OpenStackCloudException, 'Must specify either a domain or project' ): - self.cloud.revoke_role(self.fake_role['name'], - user=self.fake_user['name']) + self.op_cloud.revoke_role( + self.fake_role['name'], + user=self.fake_user['name']) @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @patch.object(OperatorCloud, 'keystone_client') @@ -761,9 +787,10 @@ def test_grant_bad_domain_exception(self, OpenStackCloudException, 'Failed to get domain baddomain \(Inner Exception: test\)' ): - self.cloud.grant_role(self.fake_role['name'], - user=self.fake_user['name'], - domain='baddomain') + self.op_cloud.grant_role( + self.fake_role['name'], + user=self.fake_user['name'], + domain='baddomain') @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @patch.object(OperatorCloud, 'keystone_client') @@ -777,9 +804,10 @@ def test_revoke_bad_domain_exception(self, OpenStackCloudException, 'Failed to get domain baddomain \(Inner Exception: test\)' ): - self.cloud.revoke_role(self.fake_role['name'], - user=self.fake_user['name'], - domain='baddomain') + self.op_cloud.revoke_role( + self.fake_role['name'], + user=self.fake_user['name'], + domain='baddomain') @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @patch.object(OperatorCloud, 'keystone_client') @@ -793,10 +821,12 @@ def test_grant_role_user_project_v2_wait(self, mock_keystone.roles.roles_for_user.side_effect = [ [], [], [self.fake_role]] mock_keystone.roles.add_user_role.return_value = self.fake_role - self.assertTrue(self.cloud.grant_role(self.fake_role['name'], - user=self.fake_user['name'], - project=self.fake_project['id'], - wait=True)) + self.assertTrue( + self.op_cloud.grant_role( + self.fake_role['name'], + user=self.fake_user['name'], + project=self.fake_project['id'], + wait=True)) @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @patch.object(OperatorCloud, 'keystone_client') @@ -807,23 +837,21 @@ def test_grant_role_user_project_v2_wait_exception(self, mock_keystone.roles.list.return_value = [self.fake_role] mock_keystone.tenants.list.return_value = [self.fake_project] mock_keystone.users.list.return_value = [self.fake_user] - mock_keystone.roles.roles_for_user.side_effect = [ - [], [], [self.fake_role]] + mock_keystone.roles.roles_for_user.return_value = [] mock_keystone.roles.add_user_role.return_value = self.fake_role with testtools.ExpectedException( OpenStackCloudTimeout, 'Timeout waiting for role to be granted' ): - self.assertTrue(self.cloud.grant_role( + self.assertTrue(self.op_cloud.grant_role( self.fake_role['name'], user=self.fake_user['name'], - project=self.fake_project['id'], wait=True, timeout=1)) + project=self.fake_project['id'], wait=True, timeout=0.01)) @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @patch.object(OperatorCloud, 'keystone_client') - def test_revoke_role_user_project_v2_wait(self, - mock_keystone, - mock_api_version): + def test_revoke_role_user_project_v2_wait( + self, mock_keystone, mock_api_version): mock_api_version.return_value = '2.0' mock_keystone.roles.list.return_value = [self.fake_role] mock_keystone.tenants.list.return_value = [self.fake_project] @@ -832,10 +860,12 @@ def test_revoke_role_user_project_v2_wait(self, [self.fake_role], [self.fake_role], []] mock_keystone.roles.remove_user_role.return_value = self.fake_role - self.assertTrue(self.cloud.revoke_role(self.fake_role['name'], - user=self.fake_user['name'], - project=self.fake_project['id'], - wait=True)) + self.assertTrue( + self.op_cloud.revoke_role( + self.fake_role['name'], + user=self.fake_user['name'], + project=self.fake_project['id'], + wait=True)) @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @patch.object(OperatorCloud, 'keystone_client') @@ -846,14 +876,12 @@ def test_revoke_role_user_project_v2_wait_exception(self, mock_keystone.roles.list.return_value = [self.fake_role] mock_keystone.tenants.list.return_value = [self.fake_project] mock_keystone.users.list.return_value = [self.fake_user] - mock_keystone.roles.roles_for_user.side_effect = [ - [self.fake_role], [self.fake_role], - []] + mock_keystone.roles.roles_for_user.return_value = [self.fake_role] mock_keystone.roles.remove_user_role.return_value = self.fake_role with testtools.ExpectedException( OpenStackCloudTimeout, 'Timeout waiting for role to be revoked' ): - self.assertTrue(self.cloud.revoke_role( + self.assertTrue(self.op_cloud.revoke_role( self.fake_role['name'], user=self.fake_user['name'], - project=self.fake_project['id'], wait=True, timeout=1)) + project=self.fake_project['id'], wait=True, timeout=0.01)) diff --git a/shade/tests/unit/test_server_delete_metadata.py b/shade/tests/unit/test_server_delete_metadata.py index d734d7114..f7233a91c 100644 --- a/shade/tests/unit/test_server_delete_metadata.py +++ b/shade/tests/unit/test_server_delete_metadata.py @@ -20,19 +20,12 @@ """ from mock import patch, Mock -import os_client_config from shade import OpenStackCloud from shade.exc import OpenStackCloudException -from shade.tests import base +from shade.tests.unit import base class TestServerDeleteMetadata(base.TestCase): - def setUp(self): - super(TestServerDeleteMetadata, self).setUp() - config = os_client_config.OpenStackConfig() - self.client = OpenStackCloud( - cloud_config=config.get_one_cloud(validate=False)) - self.client._SERVER_AGE = 0 def test_server_delete_metadata_with_delete_meta_exception(self): """ @@ -46,7 +39,7 @@ def test_server_delete_metadata_with_delete_meta_exception(self): OpenStackCloud.nova_client = Mock(**config) self.assertRaises( - OpenStackCloudException, self.client.delete_server_metadata, + OpenStackCloudException, self.cloud.delete_server_metadata, {'id': 'server-id'}, ['key']) def test_server_delete_metadata_with_exception_reraise(self): @@ -62,5 +55,5 @@ def test_server_delete_metadata_with_exception_reraise(self): OpenStackCloud.nova_client = Mock(**config) self.assertRaises( - OpenStackCloudException, self.client.delete_server_metadata, + OpenStackCloudException, self.cloud.delete_server_metadata, 'server-id', ['key']) diff --git a/shade/tests/unit/test_server_set_metadata.py b/shade/tests/unit/test_server_set_metadata.py index b0c0a2d88..e31fbd24f 100644 --- a/shade/tests/unit/test_server_set_metadata.py +++ b/shade/tests/unit/test_server_set_metadata.py @@ -20,21 +20,13 @@ """ from mock import patch, Mock -import os_client_config from shade import OpenStackCloud from shade.exc import OpenStackCloudException -from shade.tests import base +from shade.tests.unit import base class TestServerSetMetadata(base.TestCase): - def setUp(self): - super(TestServerSetMetadata, self).setUp() - config = os_client_config.OpenStackConfig() - self.client = OpenStackCloud( - cloud_config=config.get_one_cloud(validate=False)) - self.client._SERVER_AGE = 0 - def test_server_set_metadata_with_set_meta_exception(self): """ Test that a generic exception in the novaclient set_meta raises @@ -47,7 +39,7 @@ def test_server_set_metadata_with_set_meta_exception(self): OpenStackCloud.nova_client = Mock(**config) self.assertRaises( - OpenStackCloudException, self.client.set_server_metadata, + OpenStackCloudException, self.cloud.set_server_metadata, {'id': 'server-id'}, {'meta': 'data'}) def test_server_set_metadata_with_exception_reraise(self): @@ -63,5 +55,5 @@ def test_server_set_metadata_with_exception_reraise(self): OpenStackCloud.nova_client = Mock(**config) self.assertRaises( - OpenStackCloudException, self.client.set_server_metadata, + OpenStackCloudException, self.cloud.set_server_metadata, 'server-id', {'meta': 'data'}) diff --git a/shade/tests/unit/test_services.py b/shade/tests/unit/test_services.py index 0c4dd0087..bfb82f063 100644 --- a/shade/tests/unit/test_services.py +++ b/shade/tests/unit/test_services.py @@ -44,9 +44,6 @@ class CloudServices(base.TestCase): def setUp(self): super(CloudServices, self).setUp() - config = os_client_config.OpenStackConfig() - self.client = OperatorCloud(cloud_config=config.get_one_cloud( - validate=False)) self.mock_ks_services = [FakeService(**kwa) for kwa in self.mock_services] @@ -62,7 +59,7 @@ def test_create_service_v2(self, mock_api_version, mock_keystone_client, 'description': 'This is a test service' } - self.client.create_service(**kwargs) + self.op_cloud.create_service(**kwargs) kwargs['service_type'] = kwargs.pop('type') mock_keystone_client.services.create.assert_called_with(**kwargs) self.assertTrue(mock_norm.called) @@ -80,7 +77,7 @@ def test_create_service_v3(self, mock_api_version, mock_keystone_client, 'enabled': False } - self.client.create_service(**kwargs) + self.op_cloud.create_service(**kwargs) mock_keystone_client.services.create.assert_called_with(**kwargs) self.assertTrue(mock_norm.called) @@ -89,7 +86,7 @@ def test_update_service_v2(self, mock_api_version): mock_api_version.return_value = '2.0' # NOTE(SamYaple): Update service only works with v3 api self.assertRaises(OpenStackCloudUnavailableFeature, - self.client.update_service, + self.op_cloud.update_service, 'service_id', name='new name') @patch.object(_utils, 'normalize_keystone_services') @@ -109,7 +106,7 @@ def test_update_service_v3(self, mock_api_version, mock_keystone_client, service_obj = FakeService(id='id1', **kwargs) mock_keystone_client.services.update.return_value = service_obj - self.client.update_service('id1', **kwargs) + self.op_cloud.update_service('id1', **kwargs) del kwargs['service_type'] mock_keystone_client.services.update.assert_called_once_with( service='id1', **kwargs @@ -120,7 +117,7 @@ def test_update_service_v3(self, mock_api_version, mock_keystone_client, def test_list_services(self, mock_keystone_client): mock_keystone_client.services.list.return_value = \ self.mock_ks_services - services = self.client.list_services() + services = self.op_cloud.list_services() mock_keystone_client.services.list.assert_called_with() self.assertItemsEqual(self.mock_services, services) @@ -130,22 +127,22 @@ def test_get_service(self, mock_keystone_client): self.mock_ks_services # Search by id - service = self.client.get_service(name_or_id='id4') + service = self.op_cloud.get_service(name_or_id='id4') # test we are getting exactly 1 element self.assertEqual(service, self.mock_services[3]) # Search by name - service = self.client.get_service(name_or_id='service2') + service = self.op_cloud.get_service(name_or_id='service2') # test we are getting exactly 1 element self.assertEqual(service, self.mock_services[1]) # Not found - service = self.client.get_service(name_or_id='blah!') + service = self.op_cloud.get_service(name_or_id='blah!') self.assertIs(None, service) # Multiple matches # test we are getting an Exception - self.assertRaises(OpenStackCloudException, self.client.get_service, + self.assertRaises(OpenStackCloudException, self.op_cloud.get_service, name_or_id=None, filters={'type': 'type2'}) @patch.object(OperatorCloud, 'keystone_client') @@ -154,23 +151,23 @@ def test_search_services(self, mock_keystone_client): self.mock_ks_services # Search by id - services = self.client.search_services(name_or_id='id4') + services = self.op_cloud.search_services(name_or_id='id4') # test we are getting exactly 1 element self.assertEqual(1, len(services)) self.assertEqual(services, [self.mock_services[3]]) # Search by name - services = self.client.search_services(name_or_id='service2') + services = self.op_cloud.search_services(name_or_id='service2') # test we are getting exactly 1 element self.assertEqual(1, len(services)) self.assertEqual(services, [self.mock_services[1]]) # Not found - services = self.client.search_services(name_or_id='blah!') + services = self.op_cloud.search_services(name_or_id='blah!') self.assertEqual(0, len(services)) # Multiple matches - services = self.client.search_services( + services = self.op_cloud.search_services( filters={'type': 'type2'}) # test we are getting exactly 2 elements self.assertEqual(2, len(services)) @@ -183,9 +180,9 @@ def test_delete_service(self, mock_keystone_client): self.mock_ks_services # Delete by name - self.client.delete_service(name_or_id='service3') + self.op_cloud.delete_service(name_or_id='service3') mock_keystone_client.services.delete.assert_called_with(id='id3') # Delete by id - self.client.delete_service('id1') + self.op_cloud.delete_service('id1') mock_keystone_client.services.delete.assert_called_with(id='id1') diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index f902eb2ba..6ee7f4e33 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -27,19 +27,15 @@ class TestShadeOperator(base.TestCase): - def setUp(self): - super(TestShadeOperator, self).setUp() - self.cloud = shade.operator_cloud(validate=False) - def test_operator_cloud(self): - self.assertIsInstance(self.cloud, shade.OperatorCloud) + self.assertIsInstance(self.op_cloud, shade.OperatorCloud) @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_get_machine(self, mock_client): node = fakes.FakeMachine(id='00000000-0000-0000-0000-000000000000', name='bigOlFaker') mock_client.node.get.return_value = node - machine = self.cloud.get_machine('bigOlFaker') + machine = self.op_cloud.get_machine('bigOlFaker') mock_client.node.get.assert_called_with(node_id='bigOlFaker') self.assertEqual(meta.obj_to_dict(node), machine) @@ -57,7 +53,7 @@ class node_value: mock_client.port.get_by_address.return_value = port_value mock_client.node.get.return_value = node_value - machine = self.cloud.get_machine_by_mac('00:00:00:00:00:00') + machine = self.op_cloud.get_machine_by_mac('00:00:00:00:00:00') mock_client.port.get_by_address.assert_called_with( address='00:00:00:00:00:00') mock_client.node.get.assert_called_with( @@ -68,14 +64,14 @@ class node_value: def test_list_machines(self, mock_client): m1 = fakes.FakeMachine(1, 'fake_machine1') mock_client.node.list.return_value = [m1] - machines = self.cloud.list_machines() + machines = self.op_cloud.list_machines() self.assertTrue(mock_client.node.list.called) self.assertEqual(meta.obj_to_dict(m1), machines[0]) @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_validate_node(self, mock_client): node_uuid = '123' - self.cloud.validate_node(node_uuid) + self.op_cloud.validate_node(node_uuid) mock_client.node.validate.assert_called_once_with( node_uuid=node_uuid ) @@ -88,7 +84,7 @@ def test_list_nics(self, mock_client): port_dict_list = meta.obj_list_to_dict(port_list) mock_client.port.list.return_value = port_list - nics = self.cloud.list_nics() + nics = self.op_cloud.list_nics() self.assertTrue(mock_client.port.list.called) self.assertEqual(port_dict_list, nics) @@ -97,26 +93,26 @@ def test_list_nics(self, mock_client): def test_list_nics_failure(self, mock_client): mock_client.port.list.side_effect = Exception() self.assertRaises(exc.OpenStackCloudException, - self.cloud.list_nics) + self.op_cloud.list_nics) @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_list_nics_for_machine(self, mock_client): mock_client.node.list_ports.return_value = [] - self.cloud.list_nics_for_machine("123") + self.op_cloud.list_nics_for_machine("123") mock_client.node.list_ports.assert_called_with(node_id="123") @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_list_nics_for_machine_failure(self, mock_client): mock_client.node.list_ports.side_effect = Exception() self.assertRaises(exc.OpenStackCloudException, - self.cloud.list_nics_for_machine, None) + self.op_cloud.list_nics_for_machine, None) @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_patch_machine(self, mock_client): node_id = 'node01' patch = [] patch.append({'op': 'remove', 'path': '/instance_info'}) - self.cloud.patch_machine(node_id, patch) + self.op_cloud.patch_machine(node_id, patch) self.assertTrue(mock_client.node.update.called) @mock.patch.object(shade.OperatorCloud, 'ironic_client') @@ -132,7 +128,7 @@ class client_return_value: ) mock_client.node.get.return_value = client_return_value - update_dict = self.cloud.update_machine('node01') + update_dict = self.op_cloud.update_machine('node01') self.assertIsNone(update_dict['changes']) self.assertFalse(mock_patch.called) self.assertDictEqual(expected_machine, update_dict['node']) @@ -151,7 +147,7 @@ class client_return_value: ) mock_client.node.get.return_value = client_return_value - update_dict = self.cloud.update_machine('node01', name='node01') + update_dict = self.op_cloud.update_machine('node01', name='node01') self.assertIsNone(update_dict['changes']) self.assertFalse(mock_patch.called) self.assertDictEqual(expected_machine, update_dict['node']) @@ -168,7 +164,7 @@ class client_return_value: mock_client.node.get.return_value = client_return_value - update_dict = self.cloud.update_machine('evil', name='good') + update_dict = self.op_cloud.update_machine('evil', name='good') self.assertIsNotNone(update_dict['changes']) self.assertEqual('/name', update_dict['changes'][0]) self.assertTrue(mock_patch.called) @@ -188,7 +184,7 @@ class client_return_value: mock_client.node.get.return_value = client_return_value - update_dict = self.cloud.update_machine('evil', name='good') + update_dict = self.op_cloud.update_machine('evil', name='good') self.assertIsNotNone(update_dict['changes']) self.assertEqual('/name', update_dict['changes'][0]) self.assertTrue(mock_patch.called) @@ -213,7 +209,7 @@ class client_return_value: mock_client.node.get.return_value = client_return_value - update_dict = self.cloud.update_machine( + update_dict = self.op_cloud.update_machine( '00000000-0000-0000-0000-000000000000', chassis_uuid='00000000-0000-0000-0000-000000000001') self.assertIsNotNone(update_dict['changes']) @@ -240,7 +236,7 @@ class client_return_value: mock_client.node.get.return_value = client_return_value - update_dict = self.cloud.update_machine( + update_dict = self.op_cloud.update_machine( '00000000-0000-0000-0000-000000000000', driver='fake' ) @@ -268,7 +264,7 @@ class client_return_value: mock_client.node.get.return_value = client_return_value - update_dict = self.cloud.update_machine( + update_dict = self.op_cloud.update_machine( '00000000-0000-0000-0000-000000000000', driver_info=dict(var="fake") ) @@ -296,7 +292,7 @@ class client_return_value: mock_client.node.get.return_value = client_return_value - update_dict = self.cloud.update_machine( + update_dict = self.op_cloud.update_machine( '00000000-0000-0000-0000-000000000000', instance_info=dict(var="fake") ) @@ -324,7 +320,7 @@ class client_return_value: mock_client.node.get.return_value = client_return_value - update_dict = self.cloud.update_machine( + update_dict = self.op_cloud.update_machine( '00000000-0000-0000-0000-000000000000', instance_uuid='00000000-0000-0000-0000-000000000002' ) @@ -352,7 +348,7 @@ class client_return_value: mock_client.node.get.return_value = client_return_value - update_dict = self.cloud.update_machine( + update_dict = self.op_cloud.update_machine( '00000000-0000-0000-0000-000000000000', properties=dict(var="fake") ) @@ -375,7 +371,7 @@ class active_machine: mock_client.node.get.return_value = active_machine self.assertRaises( shade.OpenStackCloudException, - self.cloud.inspect_machine, + self.op_cloud.inspect_machine, machine_uuid, wait=True, timeout=1) @@ -391,7 +387,7 @@ class inspect_failed_machine: last_error = "kaboom" mock_client.node.get.return_value = inspect_failed_machine - self.cloud.inspect_machine(machine_uuid) + self.op_cloud.inspect_machine(machine_uuid) self.assertTrue(mock_client.node.set_provision_state.called) self.assertEqual( mock_client.node.set_provision_state.call_count, 1) @@ -406,7 +402,7 @@ class manageable_machine: provision_state = "manageable" mock_client.node.get.return_value = manageable_machine - self.cloud.inspect_machine(machine_uuid) + self.op_cloud.inspect_machine(machine_uuid) self.assertEqual( mock_client.node.set_provision_state.call_count, 1) @@ -433,7 +429,7 @@ class inspecting_machine: manageable_machine, manageable_machine, inspecting_machine]) - self.cloud.inspect_machine(machine_uuid) + self.op_cloud.inspect_machine(machine_uuid) self.assertTrue(mock_client.node.set_provision_state.called) self.assertEqual( mock_client.node.set_provision_state.call_count, 3) @@ -468,7 +464,7 @@ class inspecting_machine: provision_state="available" ) - return_value = self.cloud.inspect_machine( + return_value = self.op_cloud.inspect_machine( machine_uuid, wait=True, timeout=1) self.assertTrue(mock_client.node.set_provision_state.called) self.assertEqual( @@ -499,7 +495,7 @@ class inspecting_machine: manageable_machine, manageable_machine]) - return_value = self.cloud.inspect_machine( + return_value = self.op_cloud.inspect_machine( machine_uuid, wait=True, timeout=1) self.assertDictEqual(expected_return_value, return_value) @@ -529,7 +525,7 @@ class inspect_failed_machine: inspect_failed_machine]) self.assertRaises( shade.OpenStackCloudException, - self.cloud.inspect_machine, + self.op_cloud.inspect_machine, machine_uuid, wait=True, timeout=1) @@ -554,7 +550,7 @@ class fake_node: mock_client.node.create.return_value = fake_node mock_client.node.get.return_value = fake_node nics = [{'mac': '00:00:00:00:00:00'}] - return_value = self.cloud.register_machine(nics) + return_value = self.op_cloud.register_machine(nics) self.assertDictEqual(expected_return_value, return_value) self.assertTrue(mock_client.node.create.called) self.assertTrue(mock_client.port.create.called) @@ -612,7 +608,7 @@ class fake_node_post_enroll_failure: fake_node_post_provide]) mock_client.node.create.return_value = fake_node_init_state nics = [{'mac': '00:00:00:00:00:00'}] - return_value = self.cloud.register_machine(nics) + return_value = self.op_cloud.register_machine(nics) self.assertDictEqual(expected_return_value, return_value) self.assertTrue(mock_client.node.create.called) self.assertTrue(mock_client.port.create.called) @@ -623,7 +619,7 @@ class fake_node_post_enroll_failure: fake_node_post_manage, fake_node_post_manage_done, fake_node_post_provide]) - return_value = self.cloud.register_machine(nics, wait=True) + return_value = self.op_cloud.register_machine(nics, wait=True) self.assertDictEqual(expected_return_value, return_value) self.assertTrue(mock_client.node.create.called) self.assertTrue(mock_client.port.create.called) @@ -635,11 +631,11 @@ class fake_node_post_enroll_failure: fake_node_post_enroll_failure]) self.assertRaises( shade.OpenStackCloudException, - self.cloud.register_machine, + self.op_cloud.register_machine, nics) self.assertRaises( shade.OpenStackCloudException, - self.cloud.register_machine, + self.op_cloud.register_machine, nics, wait=True) @@ -662,7 +658,7 @@ class fake_node_init_state: nics = [{'mac': '00:00:00:00:00:00'}] self.assertRaises( shade.OpenStackCloudException, - self.cloud.register_machine, + self.op_cloud.register_machine, nics, lock_timeout=0.001) self.assertTrue(mock_client.node.create.called) @@ -672,7 +668,7 @@ class fake_node_init_state: mock_client.node.create.reset_mock() self.assertRaises( shade.OpenStackCloudException, - self.cloud.register_machine, + self.op_cloud.register_machine, nics, wait=True, timeout=0.001) @@ -693,7 +689,7 @@ class fake_node: mock_client.port.create.side_effect = ( exc.OpenStackCloudException("Error")) self.assertRaises(exc.OpenStackCloudException, - self.cloud.register_machine, + self.op_cloud.register_machine, nics) self.assertTrue(mock_client.node.create.called) self.assertTrue(mock_client.port.create.called) @@ -711,7 +707,7 @@ class fake_port: mock_client.node.get.return_value = fake_node nics = [{'mac': '00:00:00:00:00:00'}] uuid = "00000000-0000-0000-0000-000000000000" - self.cloud.unregister_machine(nics, uuid) + self.op_cloud.unregister_machine(nics, uuid) self.assertTrue(mock_client.node.delete.called) self.assertTrue(mock_client.port.get_by_address.called) self.assertTrue(mock_client.port.delete.called) @@ -733,7 +729,7 @@ class fake_node: mock_client.node.get.return_value = fake_node self.assertRaises( exc.OpenStackCloudException, - self.cloud.unregister_machine, + self.op_cloud.unregister_machine, nics, uuid) self.assertFalse(mock_client.node.delete.called) @@ -753,7 +749,7 @@ class fake_node: uuid = "00000000-0000-0000-0000-000000000000" self.assertRaises( exc.OpenStackCloudException, - self.cloud.unregister_machine, + self.op_cloud.unregister_machine, nics, uuid, wait=True, @@ -768,7 +764,8 @@ def test_set_machine_maintenace_state(self, mock_client): mock_client.node.set_maintenance.return_value = None node_id = 'node01' reason = 'no reason' - self.cloud.set_machine_maintenance_state(node_id, True, reason=reason) + self.op_cloud.set_machine_maintenance_state( + node_id, True, reason=reason) mock_client.node.set_maintenance.assert_called_with( node_id='node01', state='true', @@ -778,7 +775,7 @@ def test_set_machine_maintenace_state(self, mock_client): def test_set_machine_maintenace_state_false(self, mock_client): mock_client.node.set_maintenance.return_value = None node_id = 'node01' - self.cloud.set_machine_maintenance_state(node_id, False) + self.op_cloud.set_machine_maintenance_state(node_id, False) mock_client.node.set_maintenance.assert_called_with( node_id='node01', state='false') @@ -787,7 +784,7 @@ def test_set_machine_maintenace_state_false(self, mock_client): def test_remove_machine_from_maintenance(self, mock_client): mock_client.node.set_maintenance.return_value = None node_id = 'node01' - self.cloud.remove_machine_from_maintenance(node_id) + self.op_cloud.remove_machine_from_maintenance(node_id) mock_client.node.set_maintenance.assert_called_with( node_id='node01', state='false') @@ -796,7 +793,7 @@ def test_remove_machine_from_maintenance(self, mock_client): def test_set_machine_power_on(self, mock_client): mock_client.node.set_power_state.return_value = None node_id = 'node01' - return_value = self.cloud.set_machine_power_on(node_id) + return_value = self.op_cloud.set_machine_power_on(node_id) self.assertEqual(None, return_value) mock_client.node.set_power_state.assert_called_with( node_id='node01', @@ -806,7 +803,7 @@ def test_set_machine_power_on(self, mock_client): def test_set_machine_power_off(self, mock_client): mock_client.node.set_power_state.return_value = None node_id = 'node01' - return_value = self.cloud.set_machine_power_off(node_id) + return_value = self.op_cloud.set_machine_power_off(node_id) self.assertEqual(None, return_value) mock_client.node.set_power_state.assert_called_with( node_id='node01', @@ -816,7 +813,7 @@ def test_set_machine_power_off(self, mock_client): def test_set_machine_power_reboot(self, mock_client): mock_client.node.set_power_state.return_value = None node_id = 'node01' - return_value = self.cloud.set_machine_power_reboot(node_id) + return_value = self.op_cloud.set_machine_power_reboot(node_id) self.assertEqual(None, return_value) mock_client.node.set_power_state.assert_called_with( node_id='node01', @@ -826,7 +823,7 @@ def test_set_machine_power_reboot(self, mock_client): def test_set_machine_power_reboot_failure(self, mock_client): mock_client.node.set_power_state.return_value = 'failure' self.assertRaises(shade.OpenStackCloudException, - self.cloud.set_machine_power_reboot, + self.op_cloud.set_machine_power_reboot, 'node01') mock_client.node.set_power_state.assert_called_with( node_id='node01', @@ -844,7 +841,7 @@ class active_node_state: mock_client.node.set_provision_state.return_value = None mock_client.node.get.return_value = active_node_state node_id = 'node01' - return_value = self.cloud.node_set_provision_state( + return_value = self.op_cloud.node_set_provision_state( node_id, 'active', configdrive='http://127.0.0.1/file.iso') @@ -874,7 +871,7 @@ class available_node_state: mock_client.node.get.return_value = active_node_state mock_client.node.set_provision_state.return_value = None node_id = 'node01' - return_value = self.cloud.node_set_provision_state( + return_value = self.op_cloud.node_set_provision_state( node_id, 'active', configdrive='http://127.0.0.1/file.iso', @@ -890,7 +887,7 @@ class available_node_state: mock_client.node.get.return_value = deploying_node_state self.assertRaises( shade.OpenStackCloudException, - self.cloud.node_set_provision_state, + self.op_cloud.node_set_provision_state, node_id, 'active', configdrive='http://127.0.0.1/file.iso', @@ -917,7 +914,7 @@ class available_node_state: mock_client.node.get.side_effect = iter([ managable_node_state, available_node_state]) - return_value = self.cloud.node_set_provision_state( + return_value = self.op_cloud.node_set_provision_state( 'test_node', 'provide', wait=True) @@ -929,7 +926,7 @@ class available_node_state: def test_activate_node(self, mock_timeout, mock_client): mock_client.node.set_provision_state.return_value = None node_id = 'node02' - return_value = self.cloud.activate_node( + return_value = self.op_cloud.activate_node( node_id, configdrive='http://127.0.0.1/file.iso') self.assertEqual(None, return_value) @@ -954,7 +951,7 @@ class available_node_state: mock_client.node.set_provision_state.return_value = None node_id = 'node04' - return_value = self.cloud.activate_node( + return_value = self.op_cloud.activate_node( node_id, configdrive='http://127.0.0.1/file.iso', wait=True, @@ -971,7 +968,7 @@ class available_node_state: def test_deactivate_node(self, mock_timeout, mock_client): mock_client.node.set_provision_state.return_value = None node_id = 'node03' - return_value = self.cloud.deactivate_node( + return_value = self.op_cloud.deactivate_node( node_id, wait=False) self.assertEqual(None, return_value) mock_client.node.set_provision_state.assert_called_with( @@ -995,7 +992,7 @@ class deactivated_node_state: mock_client.node.set_provision_state.return_value = None node_id = 'node03' - return_value = self.cloud.deactivate_node( + return_value = self.op_cloud.deactivate_node( node_id, wait=True, timeout=2) self.assertEqual(None, return_value) mock_client.node.set_provision_state.assert_called_with( @@ -1008,7 +1005,7 @@ class deactivated_node_state: def test_set_node_instance_info(self, mock_client): uuid = 'aaa' patch = [{'op': 'add', 'foo': 'bar'}] - self.cloud.set_node_instance_info(uuid, patch) + self.op_cloud.set_node_instance_info(uuid, patch) mock_client.node.update.assert_called_with( node_id=uuid, patch=patch ) @@ -1017,7 +1014,7 @@ def test_set_node_instance_info(self, mock_client): def test_purge_node_instance_info(self, mock_client): uuid = 'aaa' expected_patch = [{'op': 'remove', 'path': '/instance_info'}] - self.cloud.purge_node_instance_info(uuid) + self.op_cloud.purge_node_instance_info(uuid) mock_client.node.update.assert_called_with( node_id=uuid, patch=expected_patch ) @@ -1031,8 +1028,8 @@ class Image(object): status = 'success' fake_image = Image() glance_mock.images.list.return_value = [fake_image] - self.assertEqual('22 name', self.cloud.get_image_name('22')) - self.assertEqual('22 name', self.cloud.get_image_name('22 name')) + self.assertEqual('22 name', self.op_cloud.get_image_name('22')) + self.assertEqual('22 name', self.op_cloud.get_image_name('22 name')) @mock.patch.object(shade.OpenStackCloud, 'glance_client') def test_get_image_id(self, glance_mock): @@ -1043,14 +1040,14 @@ class Image(object): status = 'success' fake_image = Image() glance_mock.images.list.return_value = [fake_image] - self.assertEqual('22', self.cloud.get_image_id('22')) - self.assertEqual('22', self.cloud.get_image_id('22 name')) + self.assertEqual('22', self.op_cloud.get_image_id('22')) + self.assertEqual('22', self.op_cloud.get_image_id('22 name')) @mock.patch.object(cloud_config.CloudConfig, 'get_endpoint') def test_get_session_endpoint_provided(self, fake_get_endpoint): fake_get_endpoint.return_value = 'http://fake.url' self.assertEqual( - 'http://fake.url', self.cloud.get_session_endpoint('image')) + 'http://fake.url', self.op_cloud.get_session_endpoint('image')) @mock.patch.object(cloud_config.CloudConfig, 'get_session') def test_get_session_endpoint_session(self, get_session_mock): @@ -1058,7 +1055,7 @@ def test_get_session_endpoint_session(self, get_session_mock): session_mock.get_endpoint.return_value = 'http://fake.url' get_session_mock.return_value = session_mock self.assertEqual( - 'http://fake.url', self.cloud.get_session_endpoint('image')) + 'http://fake.url', self.op_cloud.get_session_endpoint('image')) @mock.patch.object(cloud_config.CloudConfig, 'get_session') def test_get_session_endpoint_exception(self, get_session_mock): @@ -1070,27 +1067,27 @@ def side_effect(*args, **kwargs): session_mock = mock.Mock() session_mock.get_endpoint.side_effect = side_effect get_session_mock.return_value = session_mock - self.cloud.name = 'testcloud' - self.cloud.region_name = 'testregion' + self.op_cloud.name = 'testcloud' + self.op_cloud.region_name = 'testregion' with testtools.ExpectedException( exc.OpenStackCloudException, "Error getting image endpoint on testcloud:testregion:" " No service"): - self.cloud.get_session_endpoint("image") + self.op_cloud.get_session_endpoint("image") @mock.patch.object(cloud_config.CloudConfig, 'get_session') def test_get_session_endpoint_unavailable(self, get_session_mock): session_mock = mock.Mock() session_mock.get_endpoint.return_value = None get_session_mock.return_value = session_mock - image_endpoint = self.cloud.get_session_endpoint("image") + image_endpoint = self.op_cloud.get_session_endpoint("image") self.assertIsNone(image_endpoint) @mock.patch.object(cloud_config.CloudConfig, 'get_session') def test_get_session_endpoint_identity(self, get_session_mock): session_mock = mock.Mock() get_session_mock.return_value = session_mock - self.cloud.get_session_endpoint('identity') + self.op_cloud.get_session_endpoint('identity') session_mock.get_endpoint.assert_called_with( interface=ksa_plugin.AUTH_INTERFACE) @@ -1099,14 +1096,14 @@ def test_has_service_no(self, get_session_mock): session_mock = mock.Mock() session_mock.get_endpoint.return_value = None get_session_mock.return_value = session_mock - self.assertFalse(self.cloud.has_service("image")) + self.assertFalse(self.op_cloud.has_service("image")) @mock.patch.object(cloud_config.CloudConfig, 'get_session') def test_has_service_yes(self, get_session_mock): session_mock = mock.Mock() session_mock.get_endpoint.return_value = 'http://fake.url' get_session_mock.return_value = session_mock - self.assertTrue(self.cloud.has_service("image")) + self.assertTrue(self.op_cloud.has_service("image")) @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_list_hypervisors(self, mock_nova): @@ -1117,7 +1114,7 @@ def test_list_hypervisors(self, mock_nova): fakes.FakeHypervisor('2', 'testserver2'), ] - r = self.cloud.list_hypervisors() + r = self.op_cloud.list_hypervisors() mock_nova.hypervisors.list.assert_called_once_with() self.assertEquals(2, len(r)) self.assertEquals('testserver1', r[0]['hypervisor_hostname']) diff --git a/shade/tests/unit/test_update_server.py b/shade/tests/unit/test_update_server.py index b18f2cb77..95f51fc49 100644 --- a/shade/tests/unit/test_update_server.py +++ b/shade/tests/unit/test_update_server.py @@ -20,21 +20,14 @@ """ from mock import patch, Mock -import os_client_config from shade import OpenStackCloud from shade.exc import OpenStackCloudException -from shade.tests import base, fakes +from shade.tests import fakes +from shade.tests.unit import base class TestUpdateServer(base.TestCase): - def setUp(self): - super(TestUpdateServer, self).setUp() - config = os_client_config.OpenStackConfig() - self.client = OpenStackCloud( - cloud_config=config.get_one_cloud(validate=False)) - self.client._SERVER_AGE = 0 - def test_update_server_with_update_exception(self): """ Test that an exception in the novaclient update raises an exception in @@ -46,7 +39,7 @@ def test_update_server_with_update_exception(self): } OpenStackCloud.nova_client = Mock(**config) self.assertRaises( - OpenStackCloudException, self.client.update_server, + OpenStackCloudException, self.cloud.update_server, 'server-name') def test_update_server_name(self): @@ -68,5 +61,5 @@ def test_update_server_name(self): OpenStackCloud.nova_client = Mock(**config) self.assertEqual( 'server-name2', - self.client.update_server( + self.cloud.update_server( 'server-name', name='server-name2')['name']) diff --git a/shade/tests/unit/test_users.py b/shade/tests/unit/test_users.py index 0304ed41f..2c8fb67e6 100644 --- a/shade/tests/unit/test_users.py +++ b/shade/tests/unit/test_users.py @@ -26,10 +26,6 @@ class TestUsers(base.TestCase): - def setUp(self): - super(TestUsers, self).setUp() - self.cloud = shade.operator_cloud(validate=False) - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_create_user_v2(self, mock_keystone, mock_api_version): @@ -39,8 +35,8 @@ def test_create_user_v2(self, mock_keystone, mock_api_version): password = 'mice-rule' fake_user = fakes.FakeUser('1', email, name) mock_keystone.users.create.return_value = fake_user - user = self.cloud.create_user(name=name, email=email, - password=password) + user = self.op_cloud.create_user( + name=name, email=email, password=password) mock_keystone.users.create.assert_called_once_with( name=name, password=password, email=email, enabled=True, ) @@ -57,9 +53,10 @@ def test_create_user_v3(self, mock_keystone, mock_api_version): domain_id = '456' fake_user = fakes.FakeUser('1', email, name) mock_keystone.users.create.return_value = fake_user - user = self.cloud.create_user(name=name, email=email, - password=password, - domain_id=domain_id) + user = self.op_cloud.create_user( + name=name, email=email, + password=password, + domain_id=domain_id) mock_keystone.users.create.assert_called_once_with( name=name, password=password, email=email, enabled=True, domain=domain_id @@ -82,9 +79,10 @@ def test_update_user_password_v2(self, mock_keystone, mock_api_version): mock_keystone.users.get.return_value = fake_user mock_keystone.users.update.return_value = fake_user mock_keystone.users.update_password.return_value = fake_user - user = self.cloud.update_user(name, name=name, email=email, - password=password, - domain_id=domain_id) + user = self.op_cloud.update_user( + name, name=name, email=email, + password=password, + domain_id=domain_id) mock_keystone.users.update.assert_called_once_with( user=munch_fake_user, name=name, email=email) mock_keystone.users.update_password.assert_called_once_with( @@ -104,7 +102,8 @@ def test_create_user_v3_no_domain(self, mock_keystone, mock_api_version): "User or project creation requires an explicit" " domain_id argument." ): - self.cloud.create_user(name=name, email=email, password=password) + self.op_cloud.create_user( + name=name, email=email, password=password) @mock.patch.object(shade.OpenStackCloud, 'get_user_by_id') @mock.patch.object(shade.OpenStackCloud, 'get_user') @@ -113,7 +112,7 @@ def test_delete_user(self, mock_keystone, mock_get_user, mock_get_by_id): mock_get_user.return_value = dict(id='123') fake_user = fakes.FakeUser('123', 'email', 'name') mock_get_by_id.return_value = fake_user - self.assertTrue(self.cloud.delete_user('name')) + self.assertTrue(self.op_cloud.delete_user('name')) mock_get_by_id.assert_called_once_with('123', normalize=False) mock_keystone.users.delete.assert_called_once_with(user=fake_user) @@ -121,7 +120,7 @@ def test_delete_user(self, mock_keystone, mock_get_user, mock_get_by_id): @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_delete_user_not_found(self, mock_keystone, mock_get_user): mock_get_user.return_value = None - self.assertFalse(self.cloud.delete_user('name')) + self.assertFalse(self.op_cloud.delete_user('name')) self.assertFalse(mock_keystone.users.delete.called) @mock.patch.object(shade.OpenStackCloud, 'get_user') @@ -130,7 +129,7 @@ def test_delete_user_not_found(self, mock_keystone, mock_get_user): def test_add_user_to_group(self, mock_keystone, mock_group, mock_user): mock_user.return_value = munch.Munch(dict(id=1)) mock_group.return_value = munch.Munch(dict(id=2)) - self.cloud.add_user_to_group("user", "group") + self.op_cloud.add_user_to_group("user", "group") mock_keystone.users.add_to_group.assert_called_once_with( user=1, group=2 ) @@ -142,7 +141,7 @@ def test_is_user_in_group(self, mock_keystone, mock_group, mock_user): mock_user.return_value = munch.Munch(dict(id=1)) mock_group.return_value = munch.Munch(dict(id=2)) mock_keystone.users.check_in_group.return_value = True - self.assertTrue(self.cloud.is_user_in_group("user", "group")) + self.assertTrue(self.op_cloud.is_user_in_group("user", "group")) mock_keystone.users.check_in_group.assert_called_once_with( user=1, group=2 ) @@ -154,7 +153,7 @@ def test_remove_user_from_group(self, mock_keystone, mock_group, mock_user): mock_user.return_value = munch.Munch(dict(id=1)) mock_group.return_value = munch.Munch(dict(id=2)) - self.cloud.remove_user_from_group("user", "group") + self.op_cloud.remove_user_from_group("user", "group") mock_keystone.users.remove_from_group.assert_called_once_with( user=1, group=2 ) From ae705ec87857eb8500ccbb56108a0a281d980e82 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 20 Aug 2016 10:37:45 -0500 Subject: [PATCH 1000/3836] Change deprecated assertEquals to assertEqual It literally doesn't matter, but also getting deprecation warnings is equally silly. Change the deprecated things to the non-deprecated one, and then enable the check that prevents this from happening in the first place. Change-Id: Icd714da7ee881fad0c201e9cd56b32a4a330a431 --- .../functional/test_cluster_templates.py | 26 +++++++------- shade/tests/functional/test_compute.py | 4 +-- shade/tests/functional/test_inventory.py | 16 ++++----- shade/tests/functional/test_recordset.py | 28 +++++++-------- shade/tests/functional/test_zone.py | 30 ++++++++-------- shade/tests/unit/test__utils.py | 8 ++--- shade/tests/unit/test_cluster_templates.py | 6 ++-- shade/tests/unit/test_endpoints.py | 16 ++++----- shade/tests/unit/test_flavors.py | 2 +- shade/tests/unit/test_meta.py | 36 +++++++++---------- shade/tests/unit/test_port.py | 10 +++--- shade/tests/unit/test_shade.py | 22 ++++++------ shade/tests/unit/test_shade_operator.py | 6 ++-- tox.ini | 4 ++- 14 files changed, 108 insertions(+), 106 deletions(-) diff --git a/shade/tests/functional/test_cluster_templates.py b/shade/tests/functional/test_cluster_templates.py index 92dd7a641..031458e5a 100644 --- a/shade/tests/functional/test_cluster_templates.py +++ b/shade/tests/functional/test_cluster_templates.py @@ -66,14 +66,14 @@ def test_cluster_templates(self): self.ct = self.demo_cloud.create_cluster_template( name=name, image_id=image_id, keypair_id=keypair_id, coe=coe) - self.assertEquals(self.ct['name'], name) - self.assertEquals(self.ct['image_id'], image_id) - self.assertEquals(self.ct['keypair_id'], keypair_id) - self.assertEquals(self.ct['coe'], coe) - self.assertEquals(self.ct['registry_enabled'], registry_enabled) - self.assertEquals(self.ct['tls_disabled'], tls_disabled) - self.assertEquals(self.ct['public'], public) - self.assertEquals(self.ct['server_type'], server_type) + self.assertEqual(self.ct['name'], name) + self.assertEqual(self.ct['image_id'], image_id) + self.assertEqual(self.ct['keypair_id'], keypair_id) + self.assertEqual(self.ct['coe'], coe) + self.assertEqual(self.ct['registry_enabled'], registry_enabled) + self.assertEqual(self.ct['tls_disabled'], tls_disabled) + self.assertEqual(self.ct['public'], public) + self.assertEqual(self.ct['server_type'], server_type) # Test that we can list cluster_templates cluster_templates = self.demo_cloud.list_cluster_templates() @@ -83,19 +83,19 @@ def test_cluster_templates(self): # get_cluster_template method cluster_template_get = self.demo_cloud.get_cluster_template( self.ct['uuid']) - self.assertEquals(cluster_template_get['uuid'], self.ct['uuid']) + self.assertEqual(cluster_template_get['uuid'], self.ct['uuid']) # Test the get method also works by name cluster_template_get = self.demo_cloud.get_cluster_template(name) - self.assertEquals(cluster_template_get['name'], self.ct['name']) + self.assertEqual(cluster_template_get['name'], self.ct['name']) # Test we can update a field on the cluster_template and only that # field is updated cluster_template_update = self.demo_cloud.update_cluster_template( self.ct['uuid'], 'replace', tls_disabled=True) - self.assertEquals(cluster_template_update['uuid'], - self.ct['uuid']) - self.assertEquals(cluster_template_update['tls_disabled'], True) + self.assertEqual( + cluster_template_update['uuid'], self.ct['uuid']) + self.assertEqual(cluster_template_update['tls_disabled'], True) # Test we can delete and get True returned cluster_template_delete = self.demo_cloud.delete_cluster_template( diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index d17ebfaa1..7a8ddb2af 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -151,7 +151,7 @@ def test_create_terminate_volume_image(self): # We can either get None (if the volume delete was quick), or a volume # that is in the process of being deleted. if volume: - self.assertEquals('deleting', volume.status) + self.assertEqual('deleting', volume.status) self.assertIsNone(self.demo_cloud.get_server(self.server_name)) def test_create_boot_from_volume_preexisting(self): @@ -201,7 +201,7 @@ def test_create_boot_from_volume_preexisting_terminate(self): # We can either get None (if the volume delete was quick), or a volume # that is in the process of being deleted. if volume: - self.assertEquals('deleting', volume.status) + self.assertEqual('deleting', volume.status) self.assertIsNone(self.demo_cloud.get_server(self.server_name)) def test_create_image_snapshot_wait_active(self): diff --git a/shade/tests/functional/test_inventory.py b/shade/tests/functional/test_inventory.py index 13c5a2d42..877051526 100644 --- a/shade/tests/functional/test_inventory.py +++ b/shade/tests/functional/test_inventory.py @@ -50,9 +50,9 @@ def _cleanup_servers(self): self.nova.servers.delete(i) def _test_host_content(self, host): - self.assertEquals(host['image']['id'], self.image.id) + self.assertEqual(host['image']['id'], self.image.id) self.assertNotIn('links', host['image']) - self.assertEquals(host['flavor']['id'], self.flavor.id) + self.assertEqual(host['flavor']['id'], self.flavor.id) self.assertNotIn('links', host['flavor']) self.assertNotIn('links', host) self.assertIsInstance(host['volumes'], list) @@ -60,13 +60,13 @@ def _test_host_content(self, host): self.assertIn('interface_ip', host) def _test_expanded_host_content(self, host): - self.assertEquals(host['image']['name'], self.image.name) - self.assertEquals(host['flavor']['name'], self.flavor.name) + self.assertEqual(host['image']['name'], self.image.name) + self.assertEqual(host['flavor']['name'], self.flavor.name) def test_get_host(self): host = self.inventory.get_host(self.server_name) self.assertIsNotNone(host) - self.assertEquals(host['name'], self.server_name) + self.assertEqual(host['name'], self.server_name) self._test_host_content(host) self._test_expanded_host_content(host) host_found = False @@ -79,12 +79,12 @@ def test_get_host(self): def test_get_host_no_detail(self): host = self.inventory.get_host(self.server_name, expand=False) self.assertIsNotNone(host) - self.assertEquals(host['name'], self.server_name) + self.assertEqual(host['name'], self.server_name) - self.assertEquals(host['image']['id'], self.image.id) + self.assertEqual(host['image']['id'], self.image.id) self.assertNotIn('links', host['image']) self.assertNotIn('name', host['name']) - self.assertEquals(host['flavor']['id'], self.flavor.id) + self.assertEqual(host['flavor']['id'], self.flavor.id) self.assertNotIn('links', host['flavor']) self.assertNotIn('name', host['flavor']) diff --git a/shade/tests/functional/test_recordset.py b/shade/tests/functional/test_recordset.py index 73d9dc72c..7ba356304 100644 --- a/shade/tests/functional/test_recordset.py +++ b/shade/tests/functional/test_recordset.py @@ -52,12 +52,12 @@ def test_recordsets(self): created_recordset = self.demo_cloud.create_recordset(zone, name, type_, records, description, ttl) - self.assertEquals(created_recordset['zone_id'], zone_obj['id']) - self.assertEquals(created_recordset['name'], name + '.' + zone) - self.assertEquals(created_recordset['type'], type_.upper()) - self.assertEquals(created_recordset['records'], records) - self.assertEquals(created_recordset['description'], description) - self.assertEquals(created_recordset['ttl'], ttl) + self.assertEqual(created_recordset['zone_id'], zone_obj['id']) + self.assertEqual(created_recordset['name'], name + '.' + zone) + self.assertEqual(created_recordset['type'], type_.upper()) + self.assertEqual(created_recordset['records'], records) + self.assertEqual(created_recordset['description'], description) + self.assertEqual(created_recordset['ttl'], ttl) # Test that we can list recordsets recordsets = self.demo_cloud.list_recordsets(zone) @@ -66,23 +66,23 @@ def test_recordsets(self): # Test we get the same recordset with the get_recordset method get_recordset = self.demo_cloud.get_recordset(zone, created_recordset['id']) - self.assertEquals(get_recordset['id'], created_recordset['id']) + self.assertEqual(get_recordset['id'], created_recordset['id']) # Test the get method also works by name get_recordset = self.demo_cloud.get_recordset(zone, name + '.' + zone) - self.assertEquals(get_recordset['id'], created_recordset['id']) + self.assertEqual(get_recordset['id'], created_recordset['id']) # Test we can update a field on the recordset and only that field # is updated updated_recordset = self.demo_cloud.update_recordset(zone_obj['id'], name + '.' + zone, ttl=7200) - self.assertEquals(updated_recordset['id'], created_recordset['id']) - self.assertEquals(updated_recordset['name'], name + '.' + zone) - self.assertEquals(updated_recordset['type'], type_.upper()) - self.assertEquals(updated_recordset['records'], records) - self.assertEquals(updated_recordset['description'], description) - self.assertEquals(updated_recordset['ttl'], 7200) + self.assertEqual(updated_recordset['id'], created_recordset['id']) + self.assertEqual(updated_recordset['name'], name + '.' + zone) + self.assertEqual(updated_recordset['type'], type_.upper()) + self.assertEqual(updated_recordset['records'], records) + self.assertEqual(updated_recordset['description'], description) + self.assertEqual(updated_recordset['ttl'], 7200) # Test we can delete and get True returned deleted_recordset = self.demo_cloud.delete_recordset( diff --git a/shade/tests/functional/test_zone.py b/shade/tests/functional/test_zone.py index 62e2d7ce2..032fe07c5 100644 --- a/shade/tests/functional/test_zone.py +++ b/shade/tests/functional/test_zone.py @@ -48,12 +48,12 @@ def test_zones(self): name=name, zone_type=zone_type, email=email, description=description, ttl=ttl, masters=masters) - self.assertEquals(zone['name'], name) - self.assertEquals(zone['type'], zone_type.upper()) - self.assertEquals(zone['email'], email) - self.assertEquals(zone['description'], description) - self.assertEquals(zone['ttl'], ttl) - self.assertEquals(zone['masters'], []) + self.assertEqual(zone['name'], name) + self.assertEqual(zone['type'], zone_type.upper()) + self.assertEqual(zone['email'], email) + self.assertEqual(zone['description'], description) + self.assertEqual(zone['ttl'], ttl) + self.assertEqual(zone['masters'], []) # Test that we can list zones zones = self.demo_cloud.list_zones() @@ -61,22 +61,22 @@ def test_zones(self): # Test we get the same zone with the get_zone method zone_get = self.demo_cloud.get_zone(zone['id']) - self.assertEquals(zone_get['id'], zone['id']) + self.assertEqual(zone_get['id'], zone['id']) # Test the get method also works by name zone_get = self.demo_cloud.get_zone(name) - self.assertEquals(zone_get['name'], zone['name']) + self.assertEqual(zone_get['name'], zone['name']) # Test we can update a field on the zone and only that field # is updated zone_update = self.demo_cloud.update_zone(zone['id'], ttl=7200) - self.assertEquals(zone_update['id'], zone['id']) - self.assertEquals(zone_update['name'], zone['name']) - self.assertEquals(zone_update['type'], zone['type']) - self.assertEquals(zone_update['email'], zone['email']) - self.assertEquals(zone_update['description'], zone['description']) - self.assertEquals(zone_update['ttl'], 7200) - self.assertEquals(zone_update['masters'], zone['masters']) + self.assertEqual(zone_update['id'], zone['id']) + self.assertEqual(zone_update['name'], zone['name']) + self.assertEqual(zone_update['type'], zone['type']) + self.assertEqual(zone_update['email'], zone['email']) + self.assertEqual(zone_update['description'], zone['description']) + self.assertEqual(zone_update['ttl'], 7200) + self.assertEqual(zone_update['masters'], zone['masters']) # Test we can delete and get True returned zone_delete = self.demo_cloud.delete_zone(zone['id']) diff --git a/shade/tests/unit/test__utils.py b/shade/tests/unit/test__utils.py index dff7956e9..8fae4ff71 100644 --- a/shade/tests/unit/test__utils.py +++ b/shade/tests/unit/test__utils.py @@ -36,14 +36,14 @@ def test__filter_list_name_or_id(self): el2 = dict(id=200, name='pluto') data = [el1, el2] ret = _utils._filter_list(data, 'donald', None) - self.assertEquals([el1], ret) + self.assertEqual([el1], ret) def test__filter_list_filter(self): el1 = dict(id=100, name='donald', other='duck') el2 = dict(id=200, name='donald', other='trump') data = [el1, el2] ret = _utils._filter_list(data, 'donald', {'other': 'duck'}) - self.assertEquals([el1], ret) + self.assertEqual([el1], ret) def test__filter_list_dict1(self): el1 = dict(id=100, name='donald', last='duck', @@ -55,7 +55,7 @@ def test__filter_list_dict1(self): data = [el1, el2, el3] ret = _utils._filter_list( data, 'donald', {'other': {'category': 'clown'}}) - self.assertEquals([el3], ret) + self.assertEqual([el3], ret) def test__filter_list_dict2(self): el1 = dict(id=100, name='donald', last='duck', @@ -70,7 +70,7 @@ def test__filter_list_dict2(self): {'other': { 'financial': {'status': 'rich'} }}) - self.assertEquals([el2, el3], ret) + self.assertEqual([el2, el3], ret) def test_normalize_nova_secgroups(self): nova_secgroup = dict( diff --git a/shade/tests/unit/test_cluster_templates.py b/shade/tests/unit/test_cluster_templates.py index 8c480e86a..94493818c 100644 --- a/shade/tests/unit/test_cluster_templates.py +++ b/shade/tests/unit/test_cluster_templates.py @@ -97,8 +97,8 @@ def test_search_cluster_templates_by_name(self, mock_magnum): name_or_id='fake-cluster-template') mock_magnum.baymodels.list.assert_called_with(detail=False) - self.assertEquals(1, len(cluster_templates)) - self.assertEquals('fake-uuid', cluster_templates[0]['uuid']) + self.assertEqual(1, len(cluster_templates)) + self.assertEqual('fake-uuid', cluster_templates[0]['uuid']) @mock.patch.object(shade.OpenStackCloud, 'magnum_client') def test_search_cluster_templates_not_found(self, mock_magnum): @@ -109,7 +109,7 @@ def test_search_cluster_templates_not_found(self, mock_magnum): name_or_id='non-existent') mock_magnum.baymodels.list.assert_called_with(detail=False) - self.assertEquals(0, len(cluster_templates)) + self.assertEqual(0, len(cluster_templates)) @mock.patch.object(shade.OpenStackCloud, 'search_cluster_templates') def test_get_cluster_template(self, mock_search): diff --git a/shade/tests/unit/test_endpoints.py b/shade/tests/unit/test_endpoints.py index 4718dff3f..8e2a8efce 100644 --- a/shade/tests/unit/test_endpoints.py +++ b/shade/tests/unit/test_endpoints.py @@ -89,7 +89,7 @@ def test_create_endpoint_v2(self, mock_api_version, mock_keystone_client, # test keys and values are correct for k, v in self.mock_endpoints[2].items(): - self.assertEquals(v, endpoints[0].get(k)) + self.assertEqual(v, endpoints[0].get(k)) # test v3 semantics on v2.0 endpoint mock_keystone_client.endpoints.create.return_value = \ @@ -110,7 +110,7 @@ def test_create_endpoint_v2(self, mock_api_version, mock_keystone_client, # test keys and values are correct for k, v in self.mock_endpoints[0].items(): - self.assertEquals(v, endpoints_3on2[0].get(k)) + self.assertEqual(v, endpoints_3on2[0].get(k)) @patch.object(OperatorCloud, 'list_services') @patch.object(OperatorCloud, 'keystone_client') @@ -146,7 +146,7 @@ def test_create_endpoint_v3(self, mock_api_version, mock_keystone_client, # test keys and values are correct for k, v in self.mock_endpoints_v3[0].items(): - self.assertEquals(v, endpoints[0].get(k)) + self.assertEqual(v, endpoints[0].get(k)) # test v2.0 semantics on v3 endpoint mock_keystone_client.endpoints.create.side_effect = \ @@ -161,12 +161,12 @@ def test_create_endpoint_v3(self, mock_api_version, mock_keystone_client, ) # Three endpoints should be returned, public, internal, and admin - self.assertEquals(len(endpoints_2on3), 3) + self.assertEqual(len(endpoints_2on3), 3) # test keys and values are correct for count in range(len(endpoints_2on3)): for k, v in self.mock_endpoints_v3[count].items(): - self.assertEquals(v, endpoints_2on3[count].get(k)) + self.assertEqual(v, endpoints_2on3[count].get(k)) @patch.object(os_client_config.cloud_config.CloudConfig, 'get_api_version') def test_update_endpoint_v2(self, mock_api_version): @@ -201,7 +201,7 @@ def test_update_endpoint_v3(self, mock_api_version, mock_keystone_client): # test keys and values are correct for k, v in self.mock_endpoints_v3[0].items(): - self.assertEquals(v, endpoint.get(k)) + self.assertEqual(v, endpoint.get(k)) @patch.object(OperatorCloud, 'keystone_client') def test_list_endpoints(self, mock_keystone_client): @@ -221,7 +221,7 @@ def test_list_endpoints(self, mock_keystone_client): if e['id'] == mock_endpoint['id']: found = True for k, v in mock_endpoint.items(): - self.assertEquals(v, e.get(k)) + self.assertEqual(v, e.get(k)) break self.assertTrue( found, msg="endpoint {id} not found!".format( @@ -237,7 +237,7 @@ def test_search_endpoints(self, mock_keystone_client): # # test we are getting exactly 1 element self.assertEqual(1, len(endpoints)) for k, v in self.mock_endpoints[2].items(): - self.assertEquals(v, endpoints[0].get(k)) + self.assertEqual(v, endpoints[0].get(k)) # Not found endpoints = self.op_cloud.search_endpoints(id='blah!') diff --git a/shade/tests/unit/test_flavors.py b/shade/tests/unit/test_flavors.py index e1625cb6b..3d8982988 100644 --- a/shade/tests/unit/test_flavors.py +++ b/shade/tests/unit/test_flavors.py @@ -38,7 +38,7 @@ def test_create_flavor(self): # test that we have a new flavor added new_flavors = self.full_op_cloud.list_flavors() - self.assertEquals(len(new_flavors) - len(old_flavors), 1) + self.assertEqual(len(new_flavors) - len(old_flavors), 1) # test that new flavor is created correctly found = False diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 57a81ac4d..ece91060b 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -643,19 +643,19 @@ def test_basic_hostvars( self.assertEqual(PUBLIC_V4, hostvars['public_v4']) self.assertEqual(PUBLIC_V6, hostvars['public_v6']) self.assertEqual(PUBLIC_V6, hostvars['interface_ip']) - self.assertEquals('REGION_NAME', hostvars['region']) - self.assertEquals('CLOUD_NAME', hostvars['cloud']) - self.assertEquals("test-image-name", hostvars['image']['name']) - self.assertEquals(standard_fake_server.image['id'], - hostvars['image']['id']) + self.assertEqual('REGION_NAME', hostvars['region']) + self.assertEqual('CLOUD_NAME', hostvars['cloud']) + self.assertEqual("test-image-name", hostvars['image']['name']) + self.assertEqual( + standard_fake_server.image['id'], hostvars['image']['id']) self.assertNotIn('links', hostvars['image']) - self.assertEquals(standard_fake_server.flavor['id'], - hostvars['flavor']['id']) - self.assertEquals("test-flavor-name", hostvars['flavor']['name']) + self.assertEqual( + standard_fake_server.flavor['id'], hostvars['flavor']['id']) + self.assertEqual("test-flavor-name", hostvars['flavor']['name']) self.assertNotIn('links', hostvars['flavor']) # test having volumes # test volume exception - self.assertEquals([], hostvars['volumes']) + self.assertEqual([], hostvars['volumes']) @mock.patch.object(shade.meta, 'get_server_external_ipv6') @mock.patch.object(shade.meta, 'get_server_external_ipv4') @@ -689,7 +689,7 @@ def test_image_string(self, mock_get_server_external_ipv4): server.image = 'fake-image-id' hostvars = meta.get_hostvars_from_server( FakeCloud(), meta.obj_to_dict(server)) - self.assertEquals('fake-image-id', hostvars['image']['id']) + self.assertEqual('fake-image-id', hostvars['image']['id']) def test_az(self): server = standard_fake_server @@ -698,7 +698,7 @@ def test_az(self): hostvars = _utils.normalize_server( meta.obj_to_dict(server), cloud_name='', region_name='') - self.assertEquals('az1', hostvars['az']) + self.assertEqual('az1', hostvars['az']) def test_has_volume(self): mock_cloud = mock.MagicMock() @@ -712,15 +712,15 @@ def test_has_volume(self): mock_cloud.get_volumes.return_value = [fake_volume_dict] hostvars = meta.get_hostvars_from_server( mock_cloud, meta.obj_to_dict(standard_fake_server)) - self.assertEquals('volume1', hostvars['volumes'][0]['id']) - self.assertEquals('/dev/sda0', hostvars['volumes'][0]['device']) + self.assertEqual('volume1', hostvars['volumes'][0]['id']) + self.assertEqual('/dev/sda0', hostvars['volumes'][0]['device']) def test_has_no_volume_service(self): fake_cloud = FakeCloud() fake_cloud.service_val = False hostvars = meta.get_hostvars_from_server( fake_cloud, meta.obj_to_dict(standard_fake_server)) - self.assertEquals([], hostvars['volumes']) + self.assertEqual([], hostvars['volumes']) def test_unknown_volume_exception(self): mock_cloud = mock.MagicMock() @@ -746,7 +746,7 @@ def test_obj_to_dict(self): self.assertNotIn('get_flavor_name', cloud_dict) self.assertNotIn('server', cloud_dict) self.assertTrue(hasattr(cloud_dict, 'name')) - self.assertEquals(cloud_dict.name, cloud_dict['name']) + self.assertEqual(cloud_dict.name, cloud_dict['name']) def test_obj_to_dict_subclass(self): class FakeObjDict(dict): @@ -755,8 +755,8 @@ class FakeObjDict(dict): obj_dict = meta.obj_to_dict(obj) self.assertIn('additional', obj_dict) self.assertIn('foo', obj_dict) - self.assertEquals(obj_dict['additional'], 1) - self.assertEquals(obj_dict['foo'], 'bar') + self.assertEqual(obj_dict['additional'], 1) + self.assertEqual(obj_dict['foo'], 'bar') def test_warlock_to_dict(self): schema = { @@ -775,4 +775,4 @@ def test_warlock_to_dict(self): self.assertNotIn('_unused', test_dict) self.assertEqual('test-image', test_dict['name']) self.assertTrue(hasattr(test_dict, 'name')) - self.assertEquals(test_dict.name, test_dict['name']) + self.assertEqual(test_dict.name, test_dict['name']) diff --git a/shade/tests/unit/test_port.py b/shade/tests/unit/test_port.py index 6994337f1..6f9a21053 100644 --- a/shade/tests/unit/test_port.py +++ b/shade/tests/unit/test_port.py @@ -229,8 +229,8 @@ def test_search_ports_by_id(self, mock_neutron_client): name_or_id='f71a6703-d6de-4be1-a91a-a570ede1d159') mock_neutron_client.list_ports.assert_called_with() - self.assertEquals(1, len(ports)) - self.assertEquals('fa:16:3e:bb:3c:e4', ports[0]['mac_address']) + self.assertEqual(1, len(ports)) + self.assertEqual('fa:16:3e:bb:3c:e4', ports[0]['mac_address']) @patch.object(OpenStackCloud, 'neutron_client') def test_search_ports_by_name(self, mock_neutron_client): @@ -240,8 +240,8 @@ def test_search_ports_by_name(self, mock_neutron_client): ports = self.cloud.search_ports(name_or_id='first-port') mock_neutron_client.list_ports.assert_called_with() - self.assertEquals(1, len(ports)) - self.assertEquals('fa:16:3e:58:42:ed', ports[0]['mac_address']) + self.assertEqual(1, len(ports)) + self.assertEqual('fa:16:3e:58:42:ed', ports[0]['mac_address']) @patch.object(OpenStackCloud, 'neutron_client') def test_search_ports_not_found(self, mock_neutron_client): @@ -251,7 +251,7 @@ def test_search_ports_not_found(self, mock_neutron_client): ports = self.cloud.search_ports(name_or_id='non-existent') mock_neutron_client.list_ports.assert_called_with() - self.assertEquals(0, len(ports)) + self.assertEqual(0, len(ports)) @patch.object(OpenStackCloud, 'neutron_client') def test_delete_port(self, mock_neutron_client): diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 54bceac47..8eeeb85d5 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -513,7 +513,7 @@ def test_get_flavor_by_ram(self, mock_nova_client, mock_compute): mock_response.json.return_value = dict(extra_specs=[]) mock_compute.get.return_value = mock_response flavor = self.cloud.get_flavor_by_ram(ram=150) - self.assertEquals(chocolate.id, flavor['id']) + self.assertEqual(chocolate.id, flavor['id']) @mock.patch.object(shade.OpenStackCloud, '_compute_client') @mock.patch.object(shade.OpenStackCloud, 'nova_client') @@ -528,7 +528,7 @@ def test_get_flavor_by_ram_and_include( mock_nova_client.flavors.list.return_value = [ vanilla, chocolate, strawberry] flavor = self.cloud.get_flavor_by_ram(ram=150, include='strawberry') - self.assertEquals(strawberry.id, flavor['id']) + self.assertEqual(strawberry.id, flavor['id']) @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_get_flavor_by_ram_not_found(self, mock_nova_client): @@ -547,9 +547,9 @@ def test_get_flavor_string_and_int( mock_response.json.return_value = dict(extra_specs=[]) mock_compute.get.return_value = mock_response flavor1 = self.cloud.get_flavor('1') - self.assertEquals(vanilla.id, flavor1['id']) + self.assertEqual(vanilla.id, flavor1['id']) flavor2 = self.cloud.get_flavor(1) - self.assertEquals(vanilla.id, flavor2['id']) + self.assertEqual(vanilla.id, flavor2['id']) def test__neutron_exceptions_resource_not_found(self): with mock.patch.object( @@ -596,9 +596,9 @@ def test_list_servers(self, mock_add_srv_int, mock_serverlist): r = self.cloud.list_servers() - self.assertEquals(1, len(r)) - self.assertEquals(1, mock_add_srv_int.call_count) - self.assertEquals('testserver', r[0]['name']) + self.assertEqual(1, len(r)) + self.assertEqual(1, mock_add_srv_int.call_count) + self.assertEqual('testserver', r[0]['name']) @mock.patch.object(shade._tasks.ServerList, 'main') @mock.patch('shade.meta.get_hostvars_from_server') @@ -619,10 +619,10 @@ def test_list_servers_detailed(self, r = self.cloud.list_servers(detailed=True) - self.assertEquals(2, len(r)) - self.assertEquals(len(r), mock_get_hostvars_from_server.call_count) - self.assertEquals('server1', r[0]['name']) - self.assertEquals('server2', r[1]['name']) + self.assertEqual(2, len(r)) + self.assertEqual(len(r), mock_get_hostvars_from_server.call_count) + self.assertEqual('server1', r[0]['name']) + self.assertEqual('server2', r[1]['name']) def test_iterate_timeout_bad_wait(self): with testtools.ExpectedException( diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index 6ee7f4e33..89daed1a1 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -1116,6 +1116,6 @@ def test_list_hypervisors(self, mock_nova): r = self.op_cloud.list_hypervisors() mock_nova.hypervisors.list.assert_called_once_with() - self.assertEquals(2, len(r)) - self.assertEquals('testserver1', r[0]['hypervisor_hostname']) - self.assertEquals('testserver2', r[1]['hypervisor_hostname']) + self.assertEqual(2, len(r)) + self.assertEqual('testserver1', r[0]['hypervisor_hostname']) + self.assertEqual('testserver2', r[1]['hypervisor_hostname']) diff --git a/tox.ini b/tox.ini index 90ae81c94..4379427ef 100644 --- a/tox.ini +++ b/tox.ini @@ -54,7 +54,9 @@ commands = python setup.py build_sphinx [flake8] # Infra does not follow hacking, nor the broken E12* things -ignore = E123,E125,E129,H +# The string of H ignores is because there are some useful checks +# related to python3 compat. +ignore = E123,E125,E129,H3,H4,H5,H6,H7,H8,H103,H201,H238 show-source = True builtins = _ exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build From 6b0c38d77995475763cb479948549de22f3db706 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 14 Aug 2016 10:09:21 -0500 Subject: [PATCH 1001/3836] Stop getting extra flavor specs where they're useless We supplement flavors with the values in extra_specs as a default. However, there are several places where we're getting flavor objects on behalf of the user where we know that we will not use them. Make sure that we can plumb through the get_extra flag, and then pass it as False in the places where getting the extra specs is just a waste of time. Change-Id: I46d8ed5537a94a0440dd42d305221686cf1b225b --- shade/openstackcloud.py | 20 +++++++++++++------- shade/operatorcloud.py | 2 +- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index ec2a85fa8..7bac622eb 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1079,12 +1079,12 @@ def get_region(self): return self.region_name def get_flavor_name(self, flavor_id): - flavor = self.get_flavor(flavor_id) + flavor = self.get_flavor(flavor_id, get_extra=False) if flavor: return flavor['name'] return None - def get_flavor_by_ram(self, ram, include=None): + def get_flavor_by_ram(self, ram, include=None, get_extra=True): """Get a flavor based on amount of RAM available. Finds the flavor with the least amount of RAM that is at least @@ -1095,7 +1095,7 @@ def get_flavor_by_ram(self, ram, include=None): :param string include: If given, will return a flavor whose name contains this string as a substring. """ - flavors = self.list_flavors() + flavors = self.list_flavors(get_extra=get_extra) for flavor in sorted(flavors, key=operator.itemgetter('ram')): if (flavor['ram'] >= ram and (not include or include in flavor['name'])): @@ -1237,8 +1237,8 @@ def search_volume_snapshots(self, name_or_id=None, filters=None): return _utils._filter_list( volumesnapshots, name_or_id, filters) - def search_flavors(self, name_or_id=None, filters=None): - flavors = self.list_flavors() + def search_flavors(self, name_or_id=None, filters=None, get_extra=True): + flavors = self.list_flavors(get_extra=get_extra) return _utils._filter_list(flavors, name_or_id, filters) def search_security_groups(self, name_or_id=None, filters=None): @@ -1926,7 +1926,7 @@ def get_volume(self, name_or_id, filters=None): """ return _utils._get_entity(self.search_volumes, name_or_id, filters) - def get_flavor(self, name_or_id, filters=None): + def get_flavor(self, name_or_id, filters=None, get_extra=True): """Get a flavor by name or ID. :param name_or_id: Name or ID of the flavor. @@ -1940,12 +1940,18 @@ def get_flavor(self, name_or_id, filters=None): 'gender': 'Female' } } + :param get_extra: + Whether or not the list_flavors call should get the extra flavor + specs. + :returns: A flavor ``munch.Munch`` or None if no matching flavor is found. """ - return _utils._get_entity(self.search_flavors, name_or_id, filters) + search_func = functools.partial( + self.search_flavors, get_extra=get_extra) + return _utils._get_entity(search_func, name_or_id, filters) def get_security_group(self, name_or_id, filters=None): """Get a security group by name or ID. diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index c93cabab8..df4142e20 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1504,7 +1504,7 @@ def delete_flavor(self, name_or_id): :raises: OpenStackCloudException on operation error. """ - flavor = self.get_flavor(name_or_id) + flavor = self.get_flavor(name_or_id, get_extra=False) if flavor is None: self.log.debug( "Flavor {0} not found for deleting".format(name_or_id)) From 1cdf95176786190421d52abc41bb9138bb142df9 Mon Sep 17 00:00:00 2001 From: JP Sullivan Date: Thu, 10 Dec 2015 18:13:33 +0000 Subject: [PATCH 1002/3836] Get the status of the ip with ip.get('status') Ensure that there is no error if the key doesn't exist in the ip. The API spec indicates this field should be a string and should always exist. However, it has been reported in the wild that sometimes it's missing. Yay for specs. If there is no status, set it to 'UNKNOWN' so that there is at least some indication in logging for a user as to what's going on. Change-Id: I27a8bd35e6614ac24b8cbb1ada8c101329fd2f7f --- shade/_utils.py | 2 +- shade/tests/unit/test_floating_ip_neutron.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/shade/_utils.py b/shade/_utils.py index 34e346328..5922d7976 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -329,7 +329,7 @@ def normalize_neutron_floating_ips(ips): router_id=ip.get('router_id'), attached=(ip.get('port_id') is not None and ip.get('port_id') != ''), - status=ip['status'], + status=ip.get('status', 'UNKNOWN') )) return meta.obj_list_to_dict(ret) diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 89de0e1f2..700c14a81 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -150,6 +150,21 @@ def setUp(self): self.floating_ip = _utils.normalize_neutron_floating_ips( self.mock_floating_ip_list_rep['floatingips'])[0] + def test_float_no_status(self): + floating_ips = [ + { + 'fixed_ip_address': '10.0.0.4', + 'floating_ip_address': '172.24.4.229', + 'floating_network_id': 'my-network-id', + 'id': '2f245a7b-796b-4f26-9cf9-9e82d248fda8', + 'port_id': None, + 'router_id': None, + 'tenant_id': '4969c491a3c74ee4af974e6d800c62df' + } + ] + normalized = _utils.normalize_neutron_floating_ips(floating_ips) + self.assertEqual('UNKNOWN', normalized[0]['status']) + @patch.object(OpenStackCloud, 'neutron_client') @patch.object(OpenStackCloud, 'has_service') def test_list_floating_ips(self, mock_has_service, mock_neutron_client): From 376c49777f77dac677f6c16389b139f3bf57473e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 12 May 2016 12:27:45 -0500 Subject: [PATCH 1003/3836] Batch calls to list_floating_ips Similar to servers and ports, we do lists of floating ips a lot. Allow for configuration of batching actual calls to the cloud. Change-Id: I8e8d921caede5d55db2c9ff98d537e648b245970 --- shade/openstackcloud.py | 9 ++++- shade/tests/unit/test_floating_ip_neutron.py | 41 ++++++++------------ shade/tests/unit/test_meta.py | 8 +++- 3 files changed, 31 insertions(+), 27 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index ec2a85fa8..1dd4b68bb 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1562,6 +1562,7 @@ def list_floating_ip_pools(self): with _utils.shade_exceptions("Error fetching floating IP pool list"): return self.manager.submitTask(_tasks.FloatingIPPoolList()) + @_utils.cache_on_arguments(resource='floating_ip') def list_floating_ips(self): """List all available floating IPs. @@ -3558,7 +3559,8 @@ def _neutron_create_floating_ip( for count in _utils._iterate_timeout( timeout, "Timeout waiting for the floating IP" - " to be ACTIVE"): + " to be ACTIVE", + wait=self._get_cache_time('floating_ip')): fip = self.get_floating_ip(fip_id) if fip['status'] == 'ACTIVE': break @@ -3612,6 +3614,11 @@ def delete_floating_ip(self, floating_ip_id, retry=1): if (retry == 0) or not result: return result + # Wait for the cached floating ip list to be regenerated + float_expire_time = self._get_cache_time('floating_ip') + if float_expire_time: + time.sleep(float_expire_time) + # neutron sometimes returns success when deleating a floating # ip. That's awesome. SO - verify that the delete actually # worked. diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 89de0e1f2..0f9957175 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -581,37 +581,28 @@ def test_add_ip_from_pool( self.assertEqual(server, self.fake_server) - @patch.object(OpenStackCloud, 'delete_floating_ip') - @patch.object(OpenStackCloud, 'list_floating_ips') + @patch.object(OpenStackCloud, 'has_service') + @patch.object(OpenStackCloud, 'neutron_client') @patch.object(OpenStackCloud, '_use_neutron_floating') def test_cleanup_floating_ips( - self, mock_use_neutron_floating, mock_list_floating_ips, - mock_delete_floating_ip): + self, mock_use_neutron_floating, mock_neutron_client, + mock_has_service): mock_use_neutron_floating.return_value = True - floating_ips = [{ - "id": "this-is-a-floating-ip-id", - "fixed_ip_address": None, - "internal_network": None, - "floating_ip_address": "203.0.113.29", - "network": "this-is-a-net-or-pool-id", - "attached": False, - "status": "ACTIVE" - }, { - "id": "this-is-an-attached-floating-ip-id", - "fixed_ip_address": None, - "internal_network": None, - "floating_ip_address": "203.0.113.29", - "network": "this-is-a-net-or-pool-id", - "attached": True, - "status": "ACTIVE" - }] - - mock_list_floating_ips.return_value = floating_ips + mock_has_service.return_value = True + + after_delete_rep = dict( + floatingips=self.mock_floating_ip_list_rep['floatingips'][:1]) + + mock_neutron_client.list_floatingips.side_effect = [ + self.mock_floating_ip_list_rep, + after_delete_rep, + after_delete_rep, + ] self.cloud.delete_unattached_floating_ips() - mock_delete_floating_ip.assert_called_once_with( - floating_ip_id='this-is-a-floating-ip-id', retry=1) + mock_neutron_client.delete_floatingip.assert_called_once_with( + floatingip='61cea855-49cb-4846-997d-801b70c71bdd') @patch.object(OpenStackCloud, '_submit_create_fip') @patch.object(OpenStackCloud, '_get_free_fixed_port') diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 57a81ac4d..bd2a5ada9 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -233,7 +233,13 @@ def test_get_server_private_ip_devstack( self.assertEqual(PRIVATE_V4, srv['private_v4']) mock_has_service.assert_called_with('volume') mock_list_networks.assert_called_once_with() - mock_list_floating_ips.assert_called_once_with() + # TODO(mordred) empirical testing shows that list_floating_ips IS + # called, but with the caching decorator mock doesn't see it. I care + # less about fixing the mock as mocking out at this level is the + # wrong idea anyway + # To fix this, we should rewrite this to mock neutron_client - not + # shade's list_floating_ips method + # mock_list_floating_ips.assert_called_once_with() @mock.patch.object(shade.OpenStackCloud, 'list_floating_ips') @mock.patch.object(shade.OpenStackCloud, 'list_subnets') From 3dfa847d79246be2539694de75378d1e3a8f0664 Mon Sep 17 00:00:00 2001 From: Donovan Jones Date: Tue, 23 Aug 2016 15:24:09 +1200 Subject: [PATCH 1004/3836] Allow object storage endpoint to return 404 for missing /info endpoint On some clouds using radosgw to provide a swift API, requests to /info will return a 404 rather than a 412. This patch allows the swift get_object_capabilities function to throw an exception that sets e.http_status to 404 or 412. This allows os_object Ansible tasks to work against such a cloud rather than failing. Change-Id: I65caa43909d8770496264efc472659cb5d4b9719 --- shade/openstackcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 7bac622eb..db5420553 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4753,7 +4753,7 @@ def get_object_segment_size(self, segment_size): try: caps = self.get_object_capabilities() except swift_exceptions.ClientException as e: - if e.http_status == 412: + if e.http_status == 404 or e.http_status == 412: server_max_file_size = DEFAULT_MAX_FILE_SIZE self.log.info( "Swift capabilities not supported. " From 4e7772632a74f34a70890ac631bcaacd7a267df2 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 14 Aug 2016 10:00:56 -0500 Subject: [PATCH 1005/3836] Allow image and flavor by name for create_server We do a bunch of work everywhere else in shade to be friendly - but for some reason we only accept object or id for image and flavor. Fix it. In the unit tests, pass in a dict with an id value to avoid the need to mock the glance client, since that's not really what we're testing in any of those tests ... but add a test that does not do that to verify that the glance and nova clients are used to look at image/flavor lists. Change-Id: I1760a7464e43e19a475f6d277148a3c7e54ac468 --- ...image-flavor-by-name-54865b00ebbf1004.yaml | 9 +++++ shade/openstackcloud.py | 16 ++++++-- shade/tests/unit/test_create_server.py | 37 +++++++++++++------ 3 files changed, 47 insertions(+), 15 deletions(-) create mode 100644 releasenotes/notes/image-flavor-by-name-54865b00ebbf1004.yaml diff --git a/releasenotes/notes/image-flavor-by-name-54865b00ebbf1004.yaml b/releasenotes/notes/image-flavor-by-name-54865b00ebbf1004.yaml new file mode 100644 index 000000000..654812104 --- /dev/null +++ b/releasenotes/notes/image-flavor-by-name-54865b00ebbf1004.yaml @@ -0,0 +1,9 @@ +--- +features: + - The image and flavor parameters for create_server + now accept name in addition to id and dict. If given + as a name or id, shade will do a get_image or a + get_flavor to find the matching image or flavor. + If you have an id already and are not using any caching + and the extra lookup is annoying, passing the id in + as "dict(id='my-id')" will avoid the lookup. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 7bac622eb..376658b19 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4194,8 +4194,8 @@ def create_server( """Create a virtual server instance. :param name: Something to name the server. - :param image: Image dict or id to boot with. - :param flavor: Flavor dict or id to boot onto. + :param image: Image dict, name or id to boot with. + :param flavor: Flavor dict, name or id to boot onto. :param auto_ip: Whether to take actions to find a routable IP for the server. (defaults to True) :param ips: List of IPs to attach to the server (defaults to None) @@ -4300,7 +4300,15 @@ def create_server( if default_network: kwargs['nics'] = [{'net-id': default_network['id']}] - kwargs['image'] = image + if image: + if isinstance(image, dict): + kwargs['image'] = image + else: + kwargs['image'] = self.get_image(image) + if flavor and isinstance(flavor, dict): + kwargs['flavor'] = flavor + else: + kwargs['flavor'] = self.get_flavor(flavor, get_extra=False) kwargs = self._get_boot_from_volume_kwargs( image=image, boot_from_volume=boot_from_volume, @@ -4310,7 +4318,7 @@ def create_server( with _utils.shade_exceptions("Error in creating instance"): server = self.manager.submitTask(_tasks.ServerCreate( - name=name, flavor=flavor, **kwargs)) + name=name, **kwargs)) admin_pass = server.get('adminPass') or kwargs.get('admin_pass') if not wait: # This is a direct get task call to skip the list_servers diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index 6db1589c7..31cc71ddb 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -99,7 +99,8 @@ def test_create_server_wait_server_error(self): self.assertRaises( OpenStackCloudException, self.cloud.create_server, - 'server-name', 'image-id', 'flavor-id', wait=True) + 'server-name', dict(id='image-id'), + dict(id='flavor-id'), wait=True) def test_create_server_with_timeout(self): """ @@ -117,7 +118,8 @@ def test_create_server_with_timeout(self): self.assertRaises( OpenStackCloudTimeout, self.cloud.create_server, - 'server-name', 'image-id', 'flavor-id', + 'server-name', + dict(id='image-id'), dict(id='flavor-id'), wait=True, timeout=0.01) def test_create_server_no_wait(self): @@ -142,8 +144,9 @@ def test_create_server_no_wait(self): cloud_name=self.cloud.name, region_name=self.cloud.region_name), self.cloud.create_server( - name='server-name', image='image=id', - flavor='flavor-id')) + name='server-name', + image=dict(id='image=id'), + flavor=dict(id='flavor-id'))) def test_create_server_with_admin_pass_no_wait(self): """ @@ -168,8 +171,8 @@ def test_create_server_with_admin_pass_no_wait(self): cloud_name=self.cloud.name, region_name=self.cloud.region_name), self.cloud.create_server( - name='server-name', image='image=id', - flavor='flavor-id', admin_pass='ooBootheiX0edoh')) + name='server-name', image=dict(id='image=id'), + flavor=dict(id='flavor-id'), admin_pass='ooBootheiX0edoh')) @patch.object(OpenStackCloud, "wait_for_server") @patch.object(OpenStackCloud, "nova_client") @@ -188,8 +191,9 @@ def test_create_server_with_admin_pass_wait(self, mock_nova, mock_wait): meta.obj_to_dict(fake_server), None, None) server = self.cloud.create_server( - name='server-name', image='image-id', - flavor='flavor-id', admin_pass='ooBootheiX0edoh', wait=True) + name='server-name', image=dict(id='image-id'), + flavor=dict(id='flavor-id'), + admin_pass='ooBootheiX0edoh', wait=True) # Assert that we did wait self.assertTrue(mock_wait.called) @@ -245,7 +249,8 @@ def test_create_server_wait(self, mock_nova, mock_wait): mock_nova.servers.create.return_value = fake_server self.cloud.create_server( - 'server-name', 'image-id', 'flavor-id', wait=True), + 'server-name', + dict(id='image-id'), dict(id='flavor-id'), wait=True), mock_wait.assert_called_once_with( fake_server, auto_ip=True, ips=None, @@ -291,7 +296,8 @@ def test_create_server_network_with_no_nics(self, mock_get_network, attempt to get the network for the server. """ self.cloud.create_server( - 'server-name', 'image-id', 'flavor-id', network='network-name') + 'server-name', + dict(id='image-id'), dict(id='flavor-id'), network='network-name') mock_get_network.assert_called_once_with(name_or_id='network-name') @patch('shade.OpenStackCloud.nova_client') @@ -304,6 +310,15 @@ def test_create_server_network_with_empty_nics(self, it's treated the same as if 'nics' were not included. """ self.cloud.create_server( - 'server-name', 'image-id', 'flavor-id', + 'server-name', dict(id='image-id'), dict(id='flavor-id'), network='network-name', nics=[]) mock_get_network.assert_called_once_with(name_or_id='network-name') + + @patch('shade.OpenStackCloud.glance_client') + @patch('shade.OpenStackCloud.nova_client') + def test_create_server_get_flavor_image( + self, mock_nova, mock_glance): + self.cloud.create_server( + 'server-name', 'image-id', 'flavor-id') + mock_nova.flavors.list.assert_called_once() + mock_glance.images.list.assert_called_once() From 7b3b95988299345d225280385dc9d17cfee11c0e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 20 Aug 2016 09:38:03 -0500 Subject: [PATCH 1006/3836] Add support for fetching console logs from servers Added get_server_console method to fetch the console log from a Server. On clouds that do not expose this feature, a debug line will be logged and an empty string will be returned. Change-Id: Ifb973c7d382118d3a8b6bf406cb7ed463b057180 --- .../add-server-console-078ed2696e5b04d9.yaml | 6 ++ shade/_tasks.py | 5 ++ shade/openstackcloud.py | 34 +++++++++++ shade/task_manager.py | 2 +- shade/tests/functional/test_compute.py | 37 +++++++++++ shade/tests/unit/test_server_console.py | 61 +++++++++++++++++++ 6 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-server-console-078ed2696e5b04d9.yaml create mode 100644 shade/tests/unit/test_server_console.py diff --git a/releasenotes/notes/add-server-console-078ed2696e5b04d9.yaml b/releasenotes/notes/add-server-console-078ed2696e5b04d9.yaml new file mode 100644 index 000000000..a3e76872e --- /dev/null +++ b/releasenotes/notes/add-server-console-078ed2696e5b04d9.yaml @@ -0,0 +1,6 @@ +--- +features: + - Added get_server_console method to fetch the console + log from a Server. On clouds that do not expose this + feature, a debug line will be logged and an empty + string will be returned. diff --git a/shade/_tasks.py b/shade/_tasks.py index 238eafe20..09f1c78b6 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -152,6 +152,11 @@ def main(self, client): return client.nova_client.servers.list_security_group(**self.args) +class ServerConsoleGet(task_manager.Task): + def main(self, client): + return client.nova_client.servers.get_console_output(**self.args) + + class ServerGet(task_manager.Task): def main(self, client): return client.nova_client.servers.get(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index ec2a85fa8..f8c177c71 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1969,6 +1969,40 @@ def get_security_group(self, name_or_id, filters=None): return _utils._get_entity( self.search_security_groups, name_or_id, filters) + def get_server_console(self, server, length=None): + """Get the console log for a server. + + :param server: The server to fetch the console log for. Can be either + a server dict or the Name or ID of the server. + :param int length: The number of lines you would like to retrieve from + the end of the log. (optional, defaults to all) + + :returns: A string containing the text of the console log or an + empty string if the cloud does not support console logs. + :raises: OpenStackCloudException if an invalid server argument is given + or if something else unforseen happens + """ + + if not isinstance(server, dict): + server = self.get_server(server) + + if not server: + raise OpenStackCloudException( + "Console log requested for invalid server") + + try: + return self.manager.submitTask( + _tasks.ServerConsoleGet(server=server['id'], length=length), + raw=True) + except nova_exceptions.BadRequest: + return "" + except OpenStackCloudException: + raise + except Exception as e: + raise OpenStackCloudException( + "Unable to get console log for {server}: {exception}".format( + server=server['id'], exception=str(e))) + def get_server(self, name_or_id=None, filters=None, detailed=False): """Get a server by name or ID. diff --git a/shade/task_manager.py b/shade/task_manager.py index d2dce731d..cca56ad52 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -92,7 +92,7 @@ def wait(self, raw=False): elif (not isinstance(self._result, bool) and not isinstance(self._result, int) and not isinstance(self._result, float) and - not isinstance(self._result, str) and + not isinstance(self._result, six.string_types) and not isinstance(self._result, set) and not isinstance(self._result, tuple) and not isinstance(self._result, types.GeneratorType)): diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index d17ebfaa1..92478b666 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -19,9 +19,12 @@ Functional tests for `shade` compute methods. """ +import six + from shade import exc from shade.tests.functional import base from shade.tests.functional.util import pick_flavor, pick_image +from shade import _utils class TestCompute(base.BaseFunctionalTestCase): @@ -67,6 +70,40 @@ def test_create_and_delete_server(self): self.demo_cloud.delete_server(self.server_name, wait=True)) self.assertIsNone(self.demo_cloud.get_server(self.server_name)) + def test_get_server_console(self): + self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) + server = self.demo_cloud.create_server( + name=self.server_name, + image=self.image, + flavor=self.flavor, + wait=True) + for _ in _utils._iterate_timeout( + 5, "Did not get more than 0 lines in the console log"): + log = self.demo_cloud.get_server_console(server=server) + self.assertTrue(isinstance(log, six.string_types)) + if len(log) > 0: + break + + def test_get_server_console_name_or_id(self): + self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) + self.demo_cloud.create_server( + name=self.server_name, + image=self.image, + flavor=self.flavor, + wait=True) + for _ in _utils._iterate_timeout( + 5, "Did not get more than 0 lines in the console log"): + log = self.demo_cloud.get_server_console(server=self.server_name) + self.assertTrue(isinstance(log, six.string_types)) + if len(log) > 0: + break + + def test_get_server_console_bad_server(self): + self.assertRaises( + exc.OpenStackCloudException, + self.demo_cloud.get_server_console, + server=self.server_name) + def test_create_and_delete_server_with_admin_pass(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) server = self.demo_cloud.create_server( diff --git a/shade/tests/unit/test_server_console.py b/shade/tests/unit/test_server_console.py new file mode 100644 index 000000000..2f5467912 --- /dev/null +++ b/shade/tests/unit/test_server_console.py @@ -0,0 +1,61 @@ +# -*- 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. + + +import mock +import novaclient.exceptions as nova_exceptions + +import shade +from shade.tests.unit import base +from shade.tests import fakes + + +class TestServerConsole(base.TestCase): + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_get_server_console_dict(self, mock_nova): + server = dict(id='12345') + self.cloud.get_server_console(server) + + mock_nova.servers.list.assert_not_called() + mock_nova.servers.get_console_output.assert_called_once_with( + server='12345', length=None) + + @mock.patch.object(shade.OpenStackCloud, 'has_service') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_get_server_console_name_or_id(self, mock_nova, mock_has_service): + server = '12345' + + fake_server = fakes.FakeServer(server, '', 'ACTIVE') + mock_nova.servers.get.return_value = fake_server + mock_nova.servers.list.return_value = [fake_server] + mock_has_service.return_value = False + + self.cloud.get_server_console(server) + + mock_nova.servers.get_console_output.assert_called_once_with( + server='12345', length=None) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_get_server_console_no_console(self, mock_nova): + server = dict(id='12345') + exc = nova_exceptions.BadRequest( + 'There is no such action: os-getConsoleOutput') + mock_nova.servers.get_console_output.side_effect = exc + log = self.cloud.get_server_console(server) + + self.assertEqual('', log) + mock_nova.servers.list.assert_not_called() + mock_nova.servers.get_console_output.assert_called_once_with( + server='12345', length=None) From 2bb6ff9b2116449fa2bd65d70a84c464474724e3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 23 Aug 2016 13:44:32 -0500 Subject: [PATCH 1007/3836] Support more than one network in create_server The format of nics is annoying. Network is more friendly. But network currently only supports a single value. Fix it. Change-Id: Iec63a244d4c2d58cf211dc07921bf5375c9e0d55 --- shade/openstackcloud.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index db5420553..5a111a6be 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4254,8 +4254,10 @@ def create_server( :param reuse_ips: (optional) Whether to attempt to reuse pre-existing floating ips should a floating IP be needed (defaults to True) - :param network: (optional) Network name or id to attach the server to. - Mutually exclusive with the nics parameter. + :param network: (optional) Network dict or name or id to attach the + server to. Mutually exclusive with the nics parameter. + Can also be be a list of network names or ids or + network dicts. :param boot_from_volume: Whether to boot from volume. 'boot_volume' implies True, but boot_from_volume=True with no boot_volume is valid and will create a @@ -4286,15 +4288,23 @@ def create_server( 'nics parameter to create_server takes a list of dicts.' ' Got: {nics}'.format(nics=kwargs['nics'])) if network and ('nics' not in kwargs or not kwargs['nics']): - network_obj = self.get_network(name_or_id=network) - if not network_obj: - raise OpenStackCloudException( - 'Network {network} is not a valid network in' - ' {cloud}:{region}'.format( - network=network, - cloud=self.name, region=self.region_name)) + nics = [] + if not isinstance(network, list): + network = [network] + for net_name in network: + if isinstance(net_name, dict) and 'id' in net_name: + network_obj = net_name + else: + network_obj = self.get_network(name_or_id=net_name) + if not network_obj: + raise OpenStackCloudException( + 'Network {network} is not a valid network in' + ' {cloud}:{region}'.format( + network=network, + cloud=self.name, region=self.region_name)) + nics.append({'net-id': network_obj['id']}) - kwargs['nics'] = [{'net-id': network_obj['id']}] + kwargs['nics'] = nics if not network and ('nics' not in kwargs or not kwargs['nics']): default_network = self.get_default_network() if default_network: From dcce1234d4c0c8d5b1ec5a0f2ea7c6629a862acc Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 22 Aug 2016 11:21:52 -0500 Subject: [PATCH 1008/3836] Ensure per-resource caches work without global cache With the move to dogpile based per-resource caches, we need to make sure they still exist if they are configured, even if global cache isn't. To do this, test to see if any are set, and if so, configure the global to be FakeCache but the per-resource to be memory cache. Change-Id: I8261b807352a4b54b19f6831b795eedc847e86ab --- shade/openstackcloud.py | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 1dd4b68bb..62fe4f3d4 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -199,11 +199,11 @@ def __init__( self._resource_caches = {} + expirations = cloud_config.get_cache_expiration() if cache_class != 'dogpile.cache.null': self.cache_enabled = True self._cache = self._make_cache( cache_class, cache_expiration_time, cache_arguments) - expirations = cloud_config.get_cache_expiration() for expire_key in expirations.keys(): # Only build caches for things we have list operations for if getattr( @@ -213,6 +213,29 @@ def __init__( async_creation_runner=_utils.async_creation_runner) self._SERVER_AGE = DEFAULT_SERVER_AGE + elif expirations: + # We have per-resource expirations configured, but not general + # caching. This is a potential common case for things like + # nodepool. In this case, we want dogpile.cache.null for the + # general cache but dogpile.cache.memory for per-resource. We + # can't remove the decorators in this config, but that's just + # the price of playing ball + cache_class = 'dogpile.cache.memory' + self.cache_enabled = False + self._cache = self._make_cache( + 'dogpile.cache.null', cache_expiration_time, cache_arguments) + # Don't cache list_servers if we're not caching things. + # Replace this with a more specific cache configuration + # soon. + self._SERVER_AGE = 0 + for expire_key in expirations.keys(): + # Only build caches for things we have list operations for + if getattr( + self, 'list_{0}'.format(expire_key), None): + self._resource_caches[expire_key] = self._make_cache( + cache_class, expirations[expire_key], cache_arguments, + async_creation_runner=_utils.async_creation_runner) + else: self.cache_enabled = False @@ -294,8 +317,8 @@ def _make_cache_key(self, namespace, fn): name_key = '%s:%s' % (self.name, namespace) def generate_key(*args, **kwargs): - arg_key = ','.join(args) - kw_keys = sorted(kwargs.keys()) + arg_key = ','.join([str(arg) for arg in args]) if args else '' + kw_keys = sorted(kwargs.keys()) if kwargs else [] kwargs_key = ','.join( ['%s:%s' % (k, kwargs[k]) for k in kw_keys if k != 'cache']) ans = "_".join( From 315a11cbbb9b4e9ca4ba9c9e263a4be8d70f49ef Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 22 Aug 2016 14:40:25 -0500 Subject: [PATCH 1009/3836] Move list_server cache to dogpile We've got ports and floatingips doing caching, go ahead and move the big boy ... servers. Deleting fun is good for the whole family! Change-Id: I10fe3970ac102fc7818c98879b4a9cfc7bc15e76 --- shade/openstackcloud.py | 42 +++----------------------- shade/tests/unit/test_create_server.py | 1 - 2 files changed, 4 insertions(+), 39 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 62fe4f3d4..544c146bc 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -62,7 +62,6 @@ DEFAULT_OBJECT_SEGMENT_SIZE = 1073741824 # 1GB # This halves the current default for Swift DEFAULT_MAX_FILE_SIZE = (5 * 1024 * 1024 * 1024 + 2) / 2 -DEFAULT_SERVER_AGE = 5 OBJECT_CONTAINER_ACLS = { @@ -206,13 +205,11 @@ def __init__( cache_class, cache_expiration_time, cache_arguments) for expire_key in expirations.keys(): # Only build caches for things we have list operations for - if getattr( - self, 'list_{0}'.format(expire_key), None): + if getattr(self, 'list_{0}'.format(expire_key), None): self._resource_caches[expire_key] = self._make_cache( cache_class, expirations[expire_key], cache_arguments, async_creation_runner=_utils.async_creation_runner) - self._SERVER_AGE = DEFAULT_SERVER_AGE elif expirations: # We have per-resource expirations configured, but not general # caching. This is a potential common case for things like @@ -224,10 +221,6 @@ def __init__( self.cache_enabled = False self._cache = self._make_cache( 'dogpile.cache.null', cache_expiration_time, cache_arguments) - # Don't cache list_servers if we're not caching things. - # Replace this with a more specific cache configuration - # soon. - self._SERVER_AGE = 0 for expire_key in expirations.keys(): # Only build caches for things we have list operations for if getattr( @@ -246,10 +239,6 @@ class _FakeCache(object): def invalidate(self): pass - # Don't cache list_servers if we're not caching things. - # Replace this with a more specific cache configuration - # soon. - self._SERVER_AGE = 0 self._cache = _FakeCache() # Undecorate cache decorated methods. Otherwise the call stacks # wind up being stupidly long and hard to debug @@ -263,11 +252,6 @@ def invalidate(self): new_func.invalidate = _fake_invalidate setattr(self, method, new_func) - # If server expiration time is set explicitly, use that. Otherwise - # fall back to whatever it was before - self._SERVER_AGE = cloud_config.get_cache_resource_expiration( - 'server', self._SERVER_AGE) - self._container_cache = dict() self._file_hash_cache = dict() @@ -1483,28 +1467,13 @@ def list_security_groups(self): _tasks.NovaSecurityGroupList()) return _utils.normalize_nova_secgroups(groups) + @_utils.cache_on_arguments(resource='server') def list_servers(self, detailed=False): """List all available servers. :returns: A list of server ``munch.Munch``. """ - if (time.time() - self._servers_time) >= self._SERVER_AGE: - # Since we're using cached data anyway, we don't need to - # have more than one thread actually submit the list - # servers task. Let the first one submit it while holding - # a lock, and the non-blocking acquire method will cause - # subsequent threads to just skip this and use the old - # data until it succeeds. - if self._servers_lock.acquire(False): - try: - self._servers = self._list_servers(detailed=detailed) - self._servers_time = time.time() - finally: - self._servers_lock.release() - return self._servers - - def _list_servers(self, detailed=False): with _utils.shade_exceptions( "Error fetching server list on {cloud}:{region}:".format( cloud=self.name, @@ -4374,7 +4343,7 @@ def wait_for_server( for count in _utils._iterate_timeout( timeout, timeout_message, - wait=self._SERVER_AGE): + wait=self._get_cache_time('server')): try: # Use the get_server call so that the list_servers # cache can be leveraged @@ -4591,7 +4560,7 @@ def _delete_server( for count in _utils._iterate_timeout( timeout, "Timed out waiting for server to get deleted.", - wait=self._SERVER_AGE): + wait=self._get_cache_time('server')): with _utils.shade_exceptions("Error in deleting server"): server = self.get_server(server['id']) if not server: @@ -4600,9 +4569,6 @@ def _delete_server( if reset_volume_cache: self.list_volumes.invalidate(self) - # Reset the list servers cache time so that the next list server - # call gets a new list - self._servers_time = self._servers_time - self._SERVER_AGE return True @_utils.valid_kwargs( diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index 6db1589c7..a0685ee68 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -274,7 +274,6 @@ def test_create_server_no_addresses(self, mock_sleep): "floating_ips.list.return_value": [fake_floating_ip] } OpenStackCloud.nova_client = Mock(**config) - self.cloud._SERVER_AGE = 0 with patch.object(OpenStackCloud, "add_ips_to_server", return_value=fake_server): self.assertRaises( From 35b6f9a053928ac33f7c7593da30dc2648f87104 Mon Sep 17 00:00:00 2001 From: Ian Wienand Date: Thu, 25 Aug 2016 15:51:42 +1000 Subject: [PATCH 1010/3836] Delete objname in image_delete I found this throwning an error about "name" not being defined. I think it wants to use the objname it split out above. Change-Id: If603edf5f9207332dc1370bdde3eb68a7c7e7511 --- shade/openstackcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index e5b93f7b3..ca8f111ac 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2573,7 +2573,7 @@ def delete_image( # Task API means an image was uploaded to swift if self.image_api_use_tasks and IMAGE_OBJECT_KEY in image: (container, objname) = image[IMAGE_OBJECT_KEY].split('/', 1) - self.delete_object(container=container, name=name) + self.delete_object(container=container, name=objname) if wait: for count in _utils._iterate_timeout( From 72c1cd9c41b9e0862d3bde1bea84a60e41dfefd0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 25 Aug 2016 14:32:46 -0500 Subject: [PATCH 1011/3836] Clean up vendor support list The IBM Cloud isn't really a thing at that address yet (jumped the gun) OSIC probably shouldn't have been added either (turns out there are like 8 OSIC clouds currently, and they're test clouds. Also, update the location on the RegionOne clouds, and rename "Human Name" to "Location" - which is the useful information in that column anyway. Change-Id: I04451836330aacc3e2b91cfbe7d7d9bba7a47346 --- doc/source/vendor-support.rst | 84 ++++++++++++----------------------- 1 file changed, 29 insertions(+), 55 deletions(-) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index c27d124f0..cfdc02eeb 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -31,7 +31,7 @@ auro https://api.auro.io:5000/v2.0 ============== ================ -Region Name Human Name +Region Name Location ============== ================ van1 Vancouver, BC ============== ================ @@ -44,7 +44,7 @@ catalyst https://api.cloud.catalyst.net.nz:5000/v2.0 ============== ================ -Region Name Human Name +Region Name Location ============== ================ nz-por-1 Porirua, NZ nz_wlg_2 Wellington, NZ @@ -60,7 +60,7 @@ citycloud https://identity1.citycloud.com:5000/v3/ ============== ================ -Region Name Human Name +Region Name Location ============== ================ Buf1 Buffalo, NY Fra1 Frankfurt, DE @@ -80,7 +80,7 @@ conoha https://identity.%(region_name)s.conoha.io ============== ================ -Region Name Human Name +Region Name Location ============== ================ tyo1 Tokyo, JP sin1 Singapore @@ -95,7 +95,7 @@ datacentred https://compute.datacentred.io:5000 ============== ================ -Region Name Human Name +Region Name Location ============== ================ sal01 Manchester, UK ============== ================ @@ -108,9 +108,9 @@ dreamcompute https://iad2.dream.io:5000 ============== ================ -Region Name Human Name +Region Name Location ============== ================ -RegionOne Region One +RegionOne Ashburn, VA ============== ================ * Identity API Version is 3 @@ -125,9 +125,9 @@ Deprecated, please use dreamcompute https://keystone.dream.io/v2.0 ============== ================ -Region Name Human Name +Region Name Location ============== ================ -RegionOne Region One +RegionOne Ashburn, VA ============== ================ * Images must be in `raw` format @@ -140,9 +140,9 @@ elastx https://ops.elastx.net:5000/v2.0 ============== ================ -Region Name Human Name +Region Name Location ============== ================ -regionOne Region One +regionOne Stockholm, SE ============== ================ * Public IPv4 is provided via NAT with Neutron Floating IP @@ -153,7 +153,7 @@ entercloudsuite https://api.entercloudsuite.com/v2.0 ============== ================ -Region Name Human Name +Region Name Location ============== ================ nl-ams1 Amsterdam, NL it-mil1 Milan, IT @@ -162,26 +162,13 @@ de-fra1 Frankfurt, DE * Volume API Version is 1 -ibmcloud --------- - -https://identity.open.softlayer.com - -============== ================ -Region Name Human Name -============== ================ -london London, UK -============== ================ - -* Public IPv4 is provided via NAT with Neutron Floating IP - internap -------- https://identity.api.cloud.iweb.com/v2.0 ============== ================ -Region Name Human Name +Region Name Location ============== ================ ams01 Amsterdam, NL da01 Dallas, TX @@ -192,26 +179,13 @@ sjc01 San Jose, CA * Floating IPs are not supported -osic ----- - -https://cloud1.osic.org:5000 - -============== ================= -Region Name Human Name -============== ================= -RegionOne RegionOne -============== ================= - -* Public IPv4 is provided via NAT with Neutron Floating IP - ovh --- https://auth.cloud.ovh.net/v2.0 ============== ================ -Region Name Human Name +Region Name Location ============== ================ BHS1 Beauharnois, QC SBG1 Strassbourg, FR @@ -227,14 +201,14 @@ rackspace https://identity.api.rackspacecloud.com/v2.0/ ============== ================ -Region Name Human Name +Region Name Location ============== ================ -DFW Dallas +DFW Dallas, TX HKG Hong Kong IAD Washington, D.C. -LON London -ORD Chicago -SYD Sydney +LON London, UK +ORD Chicago, IL +SYD Sydney, NSW ============== ================ * Database Service Type is `rax:database` @@ -256,7 +230,7 @@ switchengines https://keystone.cloud.switch.ch:5000/v2.0 ============== ================ -Region Name Human Name +Region Name Location ============== ================ LS Lausanne, CH ZH Zurich, CH @@ -272,9 +246,9 @@ ultimum https://console.ultimum-cloud.com:5000/v2.0 ============== ================ -Region Name Human Name +Region Name Location ============== ================ -RegionOne Region One +RegionOne Prague, CZ ============== ================ * Volume API Version is 1 @@ -285,10 +259,10 @@ unitedstack https://identity.api.ustack.com/v3 ============== ================ -Region Name Human Name +Region Name Location ============== ================ -bj1 Beijing -gd1 Guangdong +bj1 Beijing, CN +gd1 Guangdong, CN ============== ================ * Identity API Version is 3 @@ -301,9 +275,9 @@ vexxhost http://auth.vexxhost.net ============== ================ -Region Name Human Name +Region Name Location ============== ================ -ca-ymq-1 Montreal +ca-ymq-1 Montreal, QC ============== ================ * DNS API Version is 1 @@ -315,9 +289,9 @@ zetta https://identity.api.zetta.io/v3 ============== ================ -Region Name Human Name +Region Name Location ============== ================ -no-osl1 Oslo +no-osl1 Oslo, NO ============== ================ * DNS API Version is 2 From dd4bd638933477c87f1c5cc2c9dfe08125c35197 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 25 Aug 2016 14:54:50 -0500 Subject: [PATCH 1012/3836] Detect the need for FIPs better in auto_ip The current logic of looking at interface_ip to see if a FIP is needed is too simplistic and mixes two concerns. interface_ip is dependent on the network context of the calling host, which is great - but may not be the full story for all of the people who would want to interact with the server. Instead of creating a FIP if there is no interface_ip - introduce a richer method that looks at public and private v4 addresses as well as the existence of a FIP-able network. Change-Id: I687375a1433d643e2f1cf18d0bbc00d0e8a0abbf --- shade/openstackcloud.py | 55 ++++++++++++++++++++- shade/tests/unit/test_floating_ip_common.py | 5 +- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index db5420553..7b5e64ef2 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4101,11 +4101,64 @@ def add_ips_to_server( server, ips, wait=wait, timeout=timeout, fixed_address=fixed_address) elif auto_ip: - if not server['interface_ip']: + if self._needs_floating_ip(server, nat_destination): server = self._add_auto_ip( server, wait=wait, timeout=timeout, reuse=reuse) return server + def _needs_floating_ip(self, server, nat_destination): + """Figure out if auto_ip should add a floating ip to this server. + + If the server has a public_v4 it does not need a floating ip. + + If the server does not have a private_v4 it does not need a + floating ip. + + If self.private then the server does not need a floating ip. + + If the cloud runs nova, and the server has a private_v4 and not + a public_v4, then the server needs a floating ip. + + If the server has a private_v4 and no public_v4 and the cloud has + a network from which floating IPs come that is connected via a + router to the network from which the private_v4 address came, + then the server needs a floating ip. + + If the server has a private_v4 and no public_v4 and the cloud + does not have a network from which floating ips come, or it has + one but that network is not connected to the network from which + the server's private_v4 address came via a router, then the + server does not need a floating ip. + """ + if not self._has_floating_ips(): + return False + + if server['public_v4']: + return False + + if not server['private_v4']: + return False + + if self.private: + return False + + if not self.has_service('network'): + return True + + # No external IPv4 network - no FIPs + # TODO(mordred) THIS IS get_external_ipv4_networks IN THE NEXT PATCH + networks = self.get_external_networks() + if not networks: + return False + + (port_obj, fixed_ip_address) = self._get_free_fixed_port( + server, nat_destination=nat_destination) + + if not port_obj or not fixed_ip_address: + return False + + return True + def _get_boot_from_volume_kwargs( self, image, boot_from_volume, boot_volume, volume_size, terminate_volume, volumes, kwargs): diff --git a/shade/tests/unit/test_floating_ip_common.py b/shade/tests/unit/test_floating_ip_common.py index 72ea32de0..ff561c645 100644 --- a/shade/tests/unit/test_floating_ip_common.py +++ b/shade/tests/unit/test_floating_ip_common.py @@ -202,16 +202,19 @@ def test_add_ips_to_server_ip_list( mock_add_ip_list.assert_called_with( server_dict, ips, wait=False, timeout=60, fixed_address=None) + @patch.object(OpenStackCloud, '_needs_floating_ip') @patch.object(OpenStackCloud, 'nova_client') @patch.object(OpenStackCloud, '_add_auto_ip') def test_add_ips_to_server_auto_ip( - self, mock_add_auto_ip, mock_nova_client): + self, mock_add_auto_ip, mock_nova_client, mock_needs_floating_ip): server = FakeServer( id='server-id', name='test-server', status="ACTIVE", addresses={} ) server_dict = meta.obj_to_dict(server) mock_nova_client.servers.get.return_value = server + # TODO(mordred) REMOVE THIS MOCK WHEN THE NEXT PATCH LANDS + mock_needs_floating_ip.return_value = True self.cloud.add_ips_to_server(server_dict) From 1ad8c92d0a230fb942a6dcdf8dc028d3ba782a3c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 26 Aug 2016 07:56:32 -0500 Subject: [PATCH 1013/3836] Log request ids We collect request ids from the cloud if they're there. Then, the current approach is to attach them to the object - but they're ephemeral and not actually a quality of the object - they're useful for debugging. Instead of making an object property, log them. That way they can be used where they're useful - ops logs for after the fact debugging. The logger is named so that it can be explicitly controlled in a logging config. Change-Id: Ie5532cdc316cb00d5f15cae8b2a2f8674ae9da40 --- .../log-request-ids-37507cb6eed9a7da.yaml | 5 +++++ shade/meta.py | 21 ++++++++++++++++--- shade/tests/unit/test_caching.py | 4 +--- 3 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/log-request-ids-37507cb6eed9a7da.yaml diff --git a/releasenotes/notes/log-request-ids-37507cb6eed9a7da.yaml b/releasenotes/notes/log-request-ids-37507cb6eed9a7da.yaml new file mode 100644 index 000000000..8dbb75491 --- /dev/null +++ b/releasenotes/notes/log-request-ids-37507cb6eed9a7da.yaml @@ -0,0 +1,5 @@ +--- +other: + - The contents of x-openstack-request-id are no longer + added to object returned. Instead, they are logged to + a logger named 'shade.request_ids'. diff --git a/shade/meta.py b/shade/meta.py index 013d91cf4..8c489ce19 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -17,6 +17,7 @@ import ipaddress import six +from shade import _log from shade import exc @@ -394,9 +395,23 @@ def get_hostvars_from_server(cloud, server, mounts=None): return server_vars -def _add_request_id(obj, request_id): +def _log_request_id(obj, request_id): + # Add it, if passed in, even though we're going to pop in a second, + # just to make the logic simpler if request_id is not None: obj['x_openstack_request_ids'] = [request_id] + + request_id = None + request_ids = obj.pop('x_openstack_request_ids', None) + if request_ids: + request_id = request_ids[0] + if request_id: + log = _log.setup_logging('shade.request_ids') + # Log the request id and object id in a specific logger. This way + # someone can turn it on if they're interested in this kind of tracing. + log.debug("Retreived object {id}. Request ID {request_id}".format( + id=obj.get('id', obj.get('uuid')), request_id=request_id)) + return obj @@ -417,7 +432,7 @@ def obj_to_dict(obj, request_id=None): return obj elif hasattr(obj, 'schema') and hasattr(obj, 'validate'): # It's a warlock - return _add_request_id(warlock_to_dict(obj), request_id) + return _log_request_id(warlock_to_dict(obj), request_id) elif isinstance(obj, dict): # The new request-id tracking spec: # https://specs.openstack.org/openstack/nova-specs/specs/juno/approved/log-request-id-mappings.html @@ -440,7 +455,7 @@ def obj_to_dict(obj, request_id=None): continue if isinstance(value, NON_CALLABLES) and not key.startswith('_'): instance[key] = value - return _add_request_id(instance, request_id) + return _log_request_id(instance, request_id) def obj_list_to_dict(obj_list, request_id=None): diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index eb1c7729a..14aa50b55 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -288,9 +288,7 @@ def test_list_flavors(self, nova_mock, mock_compute): mock_compute.get.return_value = mock_response self.assertEqual([], self.cloud.list_flavors()) - fake_flavor = fakes.FakeFlavor( - '555', 'vanilla', 100, dict( - x_openstack_request_ids=['request-id'])) + fake_flavor = fakes.FakeFlavor('555', 'vanilla', 100) fake_flavor_dict = _utils.normalize_flavors( [meta.obj_to_dict(fake_flavor)] )[0] From 4d321698f7e66bd779526e309b11b51af690e090 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 26 Aug 2016 10:33:07 -0500 Subject: [PATCH 1014/3836] Rename _get_free_fixed_port to _nat_destination_port The name _get_free_fixed_port is misleading. What the function actually does is find a port attached to the server which is on a network which has a subnet which can be a nat_destination. Such a network is referred to in shade as a "nat_destination" network. So this then is a function which returns a port on that network that is associated with this server. Change-Id: If69ad8f7c08a4df83bc00cb00801565a7473a9a5 --- shade/openstackcloud.py | 18 ++++++++++++------ shade/tests/unit/test_floating_ip_neutron.py | 6 +++--- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 7b5e64ef2..a83e9920a 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3532,7 +3532,7 @@ def _neutron_create_floating_ip( } if not port: if server: - (port_obj, fixed_ip_address) = self._get_free_fixed_port( + (port_obj, fixed_ip_address) = self._nat_destination_port( server, fixed_address=fixed_address, nat_destination=nat_destination) if port_obj: @@ -3756,9 +3756,15 @@ def _attach_ip_to_server( return server return server - def _get_free_fixed_port(self, server, fixed_address=None, - nat_destination=None): - """Returns server's port. + def _nat_destination_port( + self, server, fixed_address=None, nat_destination=None): + """Returns server port that is on a nat_destination network + + Find a port attached to the server which is on a network which + has a subnet which can be the destination of NAT. Such a network + is referred to in shade as a "nat_destination" network. So this + then is a function which returns a port on such a network that is + associated with the given server. :param server: Server dict. :param fixed_address: Fixed ip address of the port @@ -3859,7 +3865,7 @@ def _neutron_attach_ip_to_server( "{0}".format(server['id'])): # Find an available port - (port, fixed_address) = self._get_free_fixed_port( + (port, fixed_address) = self._nat_destination_port( server, fixed_address=fixed_address) if not port: raise OpenStackCloudException( @@ -4151,7 +4157,7 @@ def _needs_floating_ip(self, server, nat_destination): if not networks: return False - (port_obj, fixed_ip_address) = self._get_free_fixed_port( + (port_obj, fixed_ip_address) = self._nat_destination_port( server, nat_destination=nat_destination) if not port_obj or not fixed_ip_address: diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 700c14a81..f26dec6e3 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -629,14 +629,14 @@ def test_cleanup_floating_ips( floating_ip_id='this-is-a-floating-ip-id', retry=1) @patch.object(OpenStackCloud, '_submit_create_fip') - @patch.object(OpenStackCloud, '_get_free_fixed_port') + @patch.object(OpenStackCloud, '_nat_destination_port') @patch.object(OpenStackCloud, 'get_external_networks') def test_create_floating_ip_no_port( - self, mock_get_ext_nets, mock_get_free_fixed_port, + self, mock_get_ext_nets, mock_nat_destination_port, mock_submit_create_fip): fake_port = dict(id='port-id') mock_get_ext_nets.return_value = [self.mock_get_network_rep] - mock_get_free_fixed_port.return_value = (fake_port, '10.0.0.2') + mock_nat_destination_port.return_value = (fake_port, '10.0.0.2') mock_submit_create_fip.return_value = dict(port_id=None) self.assertRaises( From 6832f734d00f1c118068db1fbd65370ad7f5d178 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 18 Aug 2016 17:21:56 -0500 Subject: [PATCH 1015/3836] Support dual-stack neutron networks It is totally possible for a neutron network to have a network with a globally routable IPv6 subnet and an RFC-1918 IPv4 subnet. In fact, the existing OSIC Cloud1 does this, and the original region of Dreamhost did this. The trouble is, it's not possible in a reasonable way to _infer_ this setup, so we rely on brand-new config functions in os-client-config to allow a user to express that a network is external for ipv4 or for ipv6. Depends-On: I40f5165d36060643943bcb91df14e5e34cd5e3fa Change-Id: I12c491ac31b950dde4c1ac55860043fd9d05ece8 --- .../dual-stack-networks-8a81941c97d28deb.yaml | 8 + requirements.txt | 2 +- shade/meta.py | 4 +- shade/openstackcloud.py | 143 +++++++++++++- shade/tests/unit/test_floating_ip_common.py | 3 + shade/tests/unit/test_floating_ip_neutron.py | 8 +- shade/tests/unit/test_meta.py | 177 ++++++++++++++++++ 7 files changed, 330 insertions(+), 15 deletions(-) create mode 100644 releasenotes/notes/dual-stack-networks-8a81941c97d28deb.yaml diff --git a/releasenotes/notes/dual-stack-networks-8a81941c97d28deb.yaml b/releasenotes/notes/dual-stack-networks-8a81941c97d28deb.yaml new file mode 100644 index 000000000..70e28e7b1 --- /dev/null +++ b/releasenotes/notes/dual-stack-networks-8a81941c97d28deb.yaml @@ -0,0 +1,8 @@ +--- +features: + - Added support for dual stack networks where the IPv4 subnet and the + IPv6 subnet have opposite public/private qualities. It is now possible + to add configuration to clouds.yaml that will indicate that a network + is public for v6 and private for v4, which is otherwise very difficult + to correctly infer while setting server attributes like private_v4, + public_v4 and public_v6. diff --git a/requirements.txt b/requirements.txt index c4465074a..54e1d450c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ munch decorator jsonpatch ipaddress -os-client-config>=1.17.0,!=1.19.0 +os-client-config>=1.20.0 requestsexceptions>=1.1.1 six diff --git a/shade/meta.py b/shade/meta.py index 8c489ce19..492367d98 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -81,7 +81,7 @@ def get_server_private_ip(server, cloud=None): # Short circuit the ports/networks search below with a heavily cached # and possibly pre-configured network name if cloud: - int_nets = cloud.get_internal_networks() + int_nets = cloud.get_internal_ipv4_networks() for int_net in int_nets: int_ip = get_server_ip(server, key_name=int_net['name']) if int_ip is not None: @@ -123,7 +123,7 @@ def get_server_external_ipv4(cloud, server): # Short circuit the ports/networks search below with a heavily cached # and possibly pre-configured network name - ext_nets = cloud.get_external_networks() + ext_nets = cloud.get_external_ipv4_networks() for ext_net in ext_nets: ext_ip = get_server_ip(server, key_name=ext_net['name']) if ext_ip is not None: diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 0839b764e..442e65d3b 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -147,8 +147,19 @@ def __init__( self.secgroup_source = cloud_config.config['secgroup_source'] self.force_ipv4 = cloud_config.force_ipv4 + # The first two aren't useful to us anymore, but we still do them + # because there are two methods that won't work without them self._external_network_names = cloud_config.get_external_networks() self._internal_network_names = cloud_config.get_internal_networks() + + # Provide better error message for people with stale OCC + if cloud_config.get_external_ipv4_networks is None: + raise OpenStackCloudException( + "shade requires at least version 1.20.0 of os-client-config") + self._external_ipv4_names = cloud_config.get_external_ipv4_networks() + self._internal_ipv4_names = cloud_config.get_internal_ipv4_networks() + self._external_ipv6_names = cloud_config.get_external_ipv6_networks() + self._internal_ipv6_names = cloud_config.get_internal_ipv6_networks() self._nat_destination = cloud_config.get_nat_destination() self._default_network = cloud_config.get_default_network() @@ -1596,6 +1607,10 @@ def _reset_network_caches(self): with self._networks_lock: self._external_networks = [] self._internal_networks = [] + self._external_ipv4_networks = [] + self._internal_ipv4_networks = [] + self._external_ipv6_networks = [] + self._internal_ipv6_networks = [] self._nat_destination_network = None self._default_network_network = None self._network_list_stamp = False @@ -1603,6 +1618,10 @@ def _reset_network_caches(self): def _set_interesting_networks(self): external_networks = [] internal_networks = [] + external_ipv4_networks = [] + internal_ipv4_networks = [] + external_ipv6_networks = [] + internal_ipv6_networks = [] nat_destination = None default_network = None @@ -1623,7 +1642,7 @@ def _set_interesting_networks(self): return for network in all_networks: - # External networks + # Old External networks if (network['name'] in self._external_network_names or network['id'] in self._external_network_names): external_networks.append(network) @@ -1634,7 +1653,7 @@ def _set_interesting_networks(self): network['id'] not in self._internal_network_names): external_networks.append(network) - # Internal networks + # Old Internal networks if (network['name'] in self._internal_network_names or network['id'] in self._internal_network_names): internal_networks.append(network) @@ -1644,6 +1663,45 @@ def _set_interesting_networks(self): network['id'] not in self._external_network_names): internal_networks.append(network) + # External IPv4 networks + if (network['name'] in self._external_ipv4_names + or network['id'] in self._external_ipv4_names): + external_ipv4_networks.append(network) + elif ((('router:external' in network + and network['router:external']) or + network.get('provider:physical_network')) and + network['name'] not in self._internal_ipv4_names and + network['id'] not in self._internal_ipv4_names): + external_ipv4_networks.append(network) + + # Internal networks + if (network['name'] in self._internal_ipv4_names + or network['id'] in self._internal_ipv4_names): + internal_ipv4_networks.append(network) + elif (not network.get('router:external', False) and + not network.get('provider:physical_network') and + network['name'] not in self._external_ipv4_names and + network['id'] not in self._external_ipv4_names): + internal_ipv4_networks.append(network) + + # External networks + if (network['name'] in self._external_ipv6_names + or network['id'] in self._external_ipv6_names): + external_ipv6_networks.append(network) + elif (network.get('router:external') and + network['name'] not in self._internal_ipv6_names and + network['id'] not in self._internal_ipv6_names): + external_ipv6_networks.append(network) + + # Internal networks + if (network['name'] in self._internal_ipv6_names + or network['id'] in self._internal_ipv6_names): + internal_ipv6_networks.append(network) + elif (not network.get('router:external', False) and + network['name'] not in self._external_ipv6_names and + network['id'] not in self._external_ipv6_names): + internal_ipv6_networks.append(network) + # NAT Destination if self._nat_destination in ( network['name'], network['id']): @@ -1707,6 +1765,34 @@ def _set_interesting_networks(self): " access and those networks could not be found".format( network=net_name)) + for net_name in self._external_ipv4_names: + if net_name not in [net['name'] for net in external_ipv4_networks]: + raise OpenStackCloudException( + "Networks: {network} was provided for external IPv4" + " access and those networks could not be found".format( + network=net_name)) + + for net_name in self._internal_ipv4_names: + if net_name not in [net['name'] for net in internal_ipv4_networks]: + raise OpenStackCloudException( + "Networks: {network} was provided for internal IPv4" + " access and those networks could not be found".format( + network=net_name)) + + for net_name in self._external_ipv6_names: + if net_name not in [net['name'] for net in external_ipv6_networks]: + raise OpenStackCloudException( + "Networks: {network} was provided for external IPv6" + " access and those networks could not be found".format( + network=net_name)) + + for net_name in self._internal_ipv6_names: + if net_name not in [net['name'] for net in internal_ipv6_networks]: + raise OpenStackCloudException( + "Networks: {network} was provided for internal IPv6" + " access and those networks could not be found".format( + network=net_name)) + if self._nat_destination and not nat_destination: raise OpenStackCloudException( 'Network {network} was configured to be the' @@ -1723,6 +1809,10 @@ def _set_interesting_networks(self): self._external_networks = external_networks self._internal_networks = internal_networks + self._external_ipv4_networks = external_ipv4_networks + self._internal_ipv4_networks = internal_ipv4_networks + self._external_ipv6_networks = external_ipv6_networks + self._internal_ipv6_networks = internal_ipv6_networks self._nat_destination_network = nat_destination self._default_network_network = default_network @@ -1761,6 +1851,9 @@ def get_default_network(self): def get_external_networks(self): """Return the networks that are configured to route northbound. + This should be avoided in favor of the specific ipv4/ipv6 method, + but is here for backwards compatibility. + :returns: A list of network ``munch.Munch`` if one is found """ self._find_interesting_networks() @@ -1769,11 +1862,46 @@ def get_external_networks(self): def get_internal_networks(self): """Return the networks that are configured to not route northbound. + This should be avoided in favor of the specific ipv4/ipv6 method, + but is here for backwards compatibility. + :returns: A list of network ``munch.Munch`` if one is found """ self._find_interesting_networks() return self._internal_networks + def get_external_ipv4_networks(self): + """Return the networks that are configured to route northbound. + + :returns: A list of network ``munch.Munch`` if one is found + """ + self._find_interesting_networks() + return self._external_ipv4_networks + + def get_internal_ipv4_networks(self): + """Return the networks that are configured to not route northbound. + + :returns: A list of network ``munch.Munch`` if one is found + """ + self._find_interesting_networks() + return self._internal_ipv4_networks + + def get_external_ipv6_networks(self): + """Return the networks that are configured to route northbound. + + :returns: A list of network ``munch.Munch`` if one is found + """ + self._find_interesting_networks() + return self._external_ipv6_networks + + def get_internal_ipv6_networks(self): + """Return the networks that are configured to not route northbound. + + :returns: A list of network ``munch.Munch`` if one is found + """ + self._find_interesting_networks() + return self._internal_ipv6_networks + def _has_floating_ips(self): if not self._floating_ip_source: return False @@ -3393,7 +3521,7 @@ def _neutron_available_floating_ips( # Use given list to get first matching external network floating_network_id = None for net in network: - for ext_net in self.get_external_networks(): + for ext_net in self.get_external_ipv4_networks(): if net in (ext_net['name'], ext_net['id']): floating_network_id = ext_net['id'] break @@ -3406,8 +3534,8 @@ def _neutron_available_floating_ips( net=network) ) else: - # Get first existing external network - networks = self.get_external_networks() + # Get first existing external IPv4 network + networks = self.get_external_ipv4_networks() if not networks: raise OpenStackCloudResourceNotFound( "unable to find an external network") @@ -3549,7 +3677,7 @@ def _neutron_create_floating_ip( "unable to find network for floating ips with id " "{0}".format(network_name_or_id)) else: - networks = self.get_external_networks() + networks = self.get_external_ipv4_networks() if not networks: raise OpenStackCloudResourceNotFound( "Unable to find an external network in this cloud" @@ -4185,8 +4313,7 @@ def _needs_floating_ip(self, server, nat_destination): return True # No external IPv4 network - no FIPs - # TODO(mordred) THIS IS get_external_ipv4_networks IN THE NEXT PATCH - networks = self.get_external_networks() + networks = self.get_external_ipv4_networks() if not networks: return False diff --git a/shade/tests/unit/test_floating_ip_common.py b/shade/tests/unit/test_floating_ip_common.py index ff561c645..31515f239 100644 --- a/shade/tests/unit/test_floating_ip_common.py +++ b/shade/tests/unit/test_floating_ip_common.py @@ -214,6 +214,9 @@ def test_add_ips_to_server_auto_ip( mock_nova_client.servers.get.return_value = server # TODO(mordred) REMOVE THIS MOCK WHEN THE NEXT PATCH LANDS + # SERIOUSLY THIS TIME. NEXT PATCH - WHICH SHOULD ADD MOCKS FOR + # list_ports AND list_networks AND list_subnets. BUT THAT WOULD + # BE NOT ACTUALLY RELATED TO THIS PATCH. SO DO IT NEXT PATCH mock_needs_floating_ip.return_value = True self.cloud.add_ips_to_server(server_dict) diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 84fbedf25..61713308d 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -303,7 +303,7 @@ def test_available_floating_ip_neutron(self, @patch.object(_utils, '_filter_list') @patch.object(OpenStackCloud, '_neutron_create_floating_ip') @patch.object(OpenStackCloud, '_neutron_list_floating_ips') - @patch.object(OpenStackCloud, 'get_external_networks') + @patch.object(OpenStackCloud, 'get_external_ipv4_networks') @patch.object(OpenStackCloud, 'keystone_session') def test__neutron_available_floating_ips( self, @@ -340,7 +340,7 @@ def test__neutron_available_floating_ips( @patch.object(_utils, '_filter_list') @patch.object(OpenStackCloud, '_neutron_create_floating_ip') @patch.object(OpenStackCloud, '_neutron_list_floating_ips') - @patch.object(OpenStackCloud, 'get_external_networks') + @patch.object(OpenStackCloud, 'get_external_ipv4_networks') @patch.object(OpenStackCloud, 'keystone_session') def test__neutron_available_floating_ips_network( self, @@ -375,7 +375,7 @@ def test__neutron_available_floating_ips_network( server=None ) - @patch.object(OpenStackCloud, 'get_external_networks') + @patch.object(OpenStackCloud, 'get_external_ipv4_networks') @patch.object(OpenStackCloud, 'keystone_session') def test__neutron_available_floating_ips_invalid_network( self, @@ -621,7 +621,7 @@ def test_cleanup_floating_ips( @patch.object(OpenStackCloud, '_submit_create_fip') @patch.object(OpenStackCloud, '_nat_destination_port') - @patch.object(OpenStackCloud, 'get_external_networks') + @patch.object(OpenStackCloud, 'get_external_ipv4_networks') def test_create_floating_ip_no_port( self, mock_get_ext_nets, mock_nat_destination_port, mock_submit_create_fip): diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 2a92a35c8..48d45f6b8 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -61,6 +61,18 @@ def get_internal_networks(self): def get_external_networks(self): return [] + def get_internal_ipv4_networks(self): + return [] + + def get_external_ipv4_networks(self): + return [] + + def get_internal_ipv6_networks(self): + return [] + + def get_external_ipv6_networks(self): + return [] + def list_server_security_groups(self, server): return [] @@ -107,6 +119,116 @@ def get_default_network(self): }, ] +OSIC_NETWORKS = [ + { + u'admin_state_up': True, + u'id': u'7004a83a-13d3-4dcd-8cf5-52af1ace4cae', + u'mtu': 0, + u'name': u'GATEWAY_NET', + u'router:external': True, + u'shared': True, + u'status': u'ACTIVE', + u'subnets': [u'cf785ee0-6cc9-4712-be3d-0bf6c86cf455'], + u'tenant_id': u'7a1ca9f7cc4e4b13ac0ed2957f1e8c32' + }, + { + u'admin_state_up': True, + u'id': u'405abfcc-77dc-49b2-a271-139619ac9b26', + u'mtu': 0, + u'name': u'openstackjenkins-network1', + u'router:external': False, + u'shared': False, + u'status': u'ACTIVE', + u'subnets': [u'a47910bc-f649-45db-98ec-e2421c413f4e'], + u'tenant_id': u'7e9c4d5842b3451d94417bd0af03a0f4' + }, + { + u'admin_state_up': True, + u'id': u'54753d2c-0a58-4928-9b32-084c59dd20a6', + u'mtu': 0, + u'name': u'GATEWAY_NET_V6', + u'router:external': True, + u'shared': True, + u'status': u'ACTIVE', + u'subnets': [u'9c21d704-a8b9-409a-b56d-501cb518d380', + u'7cb0ce07-64c3-4a3d-92d3-6f11419b45b9'], + u'tenant_id': u'7a1ca9f7cc4e4b13ac0ed2957f1e8c32' + } +] + +OSIC_SUBNETS = [ + { + u'allocation_pools': [{ + u'end': u'172.99.106.254', + u'start': u'172.99.106.5'}], + u'cidr': u'172.99.106.0/24', + u'dns_nameservers': [u'69.20.0.164', u'69.20.0.196'], + u'enable_dhcp': True, + u'gateway_ip': u'172.99.106.1', + u'host_routes': [], + u'id': u'cf785ee0-6cc9-4712-be3d-0bf6c86cf455', + u'ip_version': 4, + u'ipv6_address_mode': None, + u'ipv6_ra_mode': None, + u'name': u'GATEWAY_NET', + u'network_id': u'7004a83a-13d3-4dcd-8cf5-52af1ace4cae', + u'subnetpool_id': None, + u'tenant_id': u'7a1ca9f7cc4e4b13ac0ed2957f1e8c32' + }, + { + u'allocation_pools': [{ + u'end': u'10.0.1.254', u'start': u'10.0.1.2'}], + u'cidr': u'10.0.1.0/24', + u'dns_nameservers': [u'8.8.8.8', u'8.8.4.4'], + u'enable_dhcp': True, + u'gateway_ip': u'10.0.1.1', + u'host_routes': [], + u'id': u'a47910bc-f649-45db-98ec-e2421c413f4e', + u'ip_version': 4, + u'ipv6_address_mode': None, + u'ipv6_ra_mode': None, + u'name': u'openstackjenkins-subnet1', + u'network_id': u'405abfcc-77dc-49b2-a271-139619ac9b26', + u'subnetpool_id': None, + u'tenant_id': u'7e9c4d5842b3451d94417bd0af03a0f4' + }, + { + u'allocation_pools': [{ + u'end': u'10.255.255.254', u'start': u'10.0.0.2'}], + u'cidr': u'10.0.0.0/8', + u'dns_nameservers': [u'8.8.8.8', u'8.8.4.4'], + u'enable_dhcp': True, + u'gateway_ip': u'10.0.0.1', + u'host_routes': [], + u'id': u'9c21d704-a8b9-409a-b56d-501cb518d380', + u'ip_version': 4, + u'ipv6_address_mode': None, + u'ipv6_ra_mode': None, + u'name': u'GATEWAY_SUBNET_V6V4', + u'network_id': u'54753d2c-0a58-4928-9b32-084c59dd20a6', + u'subnetpool_id': None, + u'tenant_id': u'7a1ca9f7cc4e4b13ac0ed2957f1e8c32' + }, + { + u'allocation_pools': [{ + u'end': u'2001:4800:1ae1:18:ffff:ffff:ffff:ffff', + u'start': u'2001:4800:1ae1:18::2'}], + u'cidr': u'2001:4800:1ae1:18::/64', + u'dns_nameservers': [u'2001:4860:4860::8888'], + u'enable_dhcp': True, + u'gateway_ip': u'2001:4800:1ae1:18::1', + u'host_routes': [], + u'id': u'7cb0ce07-64c3-4a3d-92d3-6f11419b45b9', + u'ip_version': 6, + u'ipv6_address_mode': u'dhcpv6-stateless', + u'ipv6_ra_mode': None, + u'name': u'GATEWAY_SUBNET_V6V6', + u'network_id': u'54753d2c-0a58-4928-9b32-084c59dd20a6', + u'subnetpool_id': None, + u'tenant_id': u'7a1ca9f7cc4e4b13ac0ed2957f1e8c32' + } +] + class TestMeta(base.TestCase): def test_find_nova_addresses_key_name(self): @@ -394,6 +516,61 @@ def test_get_server_cloud_rackspace_v6( mock_list_networks.assert_not_called() mock_list_floating_ips.assert_not_called() + @mock.patch.object(shade.OpenStackCloud, 'list_floating_ips') + @mock.patch.object(shade.OpenStackCloud, 'list_subnets') + @mock.patch.object(shade.OpenStackCloud, 'list_server_security_groups') + @mock.patch.object(shade.OpenStackCloud, 'get_image_name') + @mock.patch.object(shade.OpenStackCloud, 'get_flavor_name') + @mock.patch.object(shade.OpenStackCloud, 'has_service') + @mock.patch.object(shade.OpenStackCloud, 'list_networks') + def test_get_server_cloud_osic_split( + self, mock_list_networks, mock_has_service, + mock_get_flavor_name, mock_get_image_name, + mock_list_server_security_groups, + mock_list_subnets, + mock_list_floating_ips): + self.cloud._floating_ip_source = None + self.cloud.force_ipv4 = False + self.cloud._local_ipv6 = True + self.cloud._external_ipv4_names = ['GATEWAY_NET'] + self.cloud._external_ipv6_names = ['GATEWAY_NET_V6'] + self.cloud._internal_ipv4_names = ['GATEWAY_NET_V6'] + self.cloud._internal_ipv6_names = [] + mock_get_image_name.return_value = 'cirros-0.3.4-x86_64-uec' + mock_get_flavor_name.return_value = 'm1.tiny' + mock_has_service.return_value = True + mock_list_subnets.return_value = OSIC_SUBNETS + mock_list_networks.return_value = OSIC_NETWORKS + + srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( + id='test-id', name='test-name', status='ACTIVE', + flavor={u'id': u'1'}, + image={ + 'name': u'cirros-0.3.4-x86_64-uec', + u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1'}, + addresses={ + 'private': [{ + 'addr': "10.223.160.141", + 'version': 4 + }], + 'public': [{ + 'addr': "104.130.246.91", + 'version': 4 + }, { + 'addr': "2001:4800:7819:103:be76:4eff:fe05:8525", + 'version': 6 + }] + } + ))) + + self.assertEqual("10.223.160.141", srv['private_v4']) + self.assertEqual("104.130.246.91", srv['public_v4']) + self.assertEqual( + "2001:4800:7819:103:be76:4eff:fe05:8525", srv['public_v6']) + self.assertEqual( + "2001:4800:7819:103:be76:4eff:fe05:8525", srv['interface_ip']) + mock_list_floating_ips.assert_not_called() + @mock.patch.object(shade.OpenStackCloud, 'has_service') @mock.patch.object(shade.OpenStackCloud, 'list_subnets') @mock.patch.object(shade.OpenStackCloud, 'list_networks') From cf9989806d853032d87e55bdfaa1b91d81a1efaa Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 29 Aug 2016 10:57:02 -0500 Subject: [PATCH 1016/3836] Cleanup old internal/external network handling We don't really need the non-ipv4/ipv6 versions of these anymore, but we keep them for backwards compat. Pull out the processing of them and have them instead rely on the underlying ipv4/ipv6 code. Change-Id: I9d3ececbf5de8b2763da0716ef9bec77b8a0b11e --- shade/openstackcloud.py | 53 +++++------------------------------------ 1 file changed, 6 insertions(+), 47 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 442e65d3b..3b1067e80 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -147,11 +147,6 @@ def __init__( self.secgroup_source = cloud_config.config['secgroup_source'] self.force_ipv4 = cloud_config.force_ipv4 - # The first two aren't useful to us anymore, but we still do them - # because there are two methods that won't work without them - self._external_network_names = cloud_config.get_external_networks() - self._internal_network_names = cloud_config.get_internal_networks() - # Provide better error message for people with stale OCC if cloud_config.get_external_ipv4_networks is None: raise OpenStackCloudException( @@ -1605,8 +1600,6 @@ def _reset_network_caches(self): # logic again if we've done it once. This is different from just # the cached value, since "None" is a valid value to find. with self._networks_lock: - self._external_networks = [] - self._internal_networks = [] self._external_ipv4_networks = [] self._internal_ipv4_networks = [] self._external_ipv6_networks = [] @@ -1616,8 +1609,6 @@ def _reset_network_caches(self): self._network_list_stamp = False def _set_interesting_networks(self): - external_networks = [] - internal_networks = [] external_ipv4_networks = [] internal_ipv4_networks = [] external_ipv6_networks = [] @@ -1642,26 +1633,6 @@ def _set_interesting_networks(self): return for network in all_networks: - # Old External networks - if (network['name'] in self._external_network_names - or network['id'] in self._external_network_names): - external_networks.append(network) - elif ((('router:external' in network - and network['router:external']) or - network.get('provider:physical_network')) and - network['name'] not in self._internal_network_names and - network['id'] not in self._internal_network_names): - external_networks.append(network) - - # Old Internal networks - if (network['name'] in self._internal_network_names - or network['id'] in self._internal_network_names): - internal_networks.append(network) - elif (not network.get('router:external', False) and - not network.get('provider:physical_network') and - network['name'] not in self._external_network_names and - network['id'] not in self._external_network_names): - internal_networks.append(network) # External IPv4 networks if (network['name'] in self._external_ipv4_names @@ -1751,20 +1722,6 @@ def _set_interesting_networks(self): default_network = network # Validate config vs. reality - for net_name in self._external_network_names: - if net_name not in [net['name'] for net in external_networks]: - raise OpenStackCloudException( - "Networks: {network} was provided for external" - " access and those networks could not be found".format( - network=net_name)) - - for net_name in self._internal_network_names: - if net_name not in [net['name'] for net in internal_networks]: - raise OpenStackCloudException( - "Networks: {network} was provided for internal" - " access and those networks could not be found".format( - network=net_name)) - for net_name in self._external_ipv4_names: if net_name not in [net['name'] for net in external_ipv4_networks]: raise OpenStackCloudException( @@ -1807,8 +1764,6 @@ def _set_interesting_networks(self): ' found'.format( network=self._default_network)) - self._external_networks = external_networks - self._internal_networks = internal_networks self._external_ipv4_networks = external_ipv4_networks self._internal_ipv4_networks = internal_ipv4_networks self._external_ipv6_networks = external_ipv6_networks @@ -1857,7 +1812,9 @@ def get_external_networks(self): :returns: A list of network ``munch.Munch`` if one is found """ self._find_interesting_networks() - return self._external_networks + return list( + set(self._external_ipv4_networks) | + set(self._external_ipv6_networks)) def get_internal_networks(self): """Return the networks that are configured to not route northbound. @@ -1868,7 +1825,9 @@ def get_internal_networks(self): :returns: A list of network ``munch.Munch`` if one is found """ self._find_interesting_networks() - return self._internal_networks + return list( + set(self._internal_ipv4_networks) | + set(self._internal_ipv6_networks)) def get_external_ipv4_networks(self): """Return the networks that are configured to route northbound. From 229f3daae0b9a8aba6e41eedf90eae20fcb8ecf5 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 27 Aug 2016 08:51:44 -0500 Subject: [PATCH 1017/3836] Poll for image to be ready for PUT protocol When we use PUT to upload images, they are not actually immediately available all the time. They can come back in a "queued" state. We need to wait until they are ready before we return the final image. Also, sometimes the get_image call can return None, so trap for that. Change-Id: I5b92018b848d811148377aa0fd9881d5b5daf4c7 --- shade/openstackcloud.py | 17 +++++++++++++---- shade/tests/unit/test_caching.py | 3 ++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index c1e0dbdae..98f40b234 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2855,7 +2855,9 @@ def create_image( image_kwargs['container_format'] = container_format return self._upload_image_put( - name, filename, meta=meta, **image_kwargs) + name, filename, meta=meta, + wait=wait, timeout=timeout, + **image_kwargs) except OpenStackCloudException: self.log.debug("Image creation failed", exc_info=True) raise @@ -2913,7 +2915,7 @@ def _upload_image_put_v1( return image def _upload_image_put( - self, name, filename, meta, **image_kwargs): + self, name, filename, meta, wait, timeout, **image_kwargs): image_data = open(filename, 'rb') # Because reasons and crying bunnies if self.cloud_config.get_api_version('image') == '2': @@ -2922,9 +2924,16 @@ def _upload_image_put( else: image = self._upload_image_put_v1( name, image_data, meta, **image_kwargs) - self._cache.invalidate() self._get_cache(None).invalidate() - return self.get_image(image.id) + if not wait: + return image + for count in _utils._iterate_timeout( + 60, + "Timeout waiting for the image to finish.", + wait=self._get_cache_time('image')): + image_obj = self.get_image(image.id) + if image_obj and image_obj.status not in ('queued', 'saving'): + return image_obj def _upload_image_task( self, name, filename, container, current_image, diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 14aa50b55..cedcfd52a 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -346,7 +346,8 @@ def _call_create_image(self, name, **kwargs): imagefile.write(b'\0') imagefile.close() self.cloud.create_image( - name, imagefile.name, wait=True, is_public=False, **kwargs) + name, imagefile.name, wait=True, timeout=1, + is_public=False, **kwargs) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, 'glance_client') From 24a61fc53d0a9dc560859bb816796431e5e00cad Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 10 Apr 2016 09:40:56 -0500 Subject: [PATCH 1018/3836] Refactor TaskManager to be more generic The functionality that TaskManager represents could provide value to a lot of people if we put it into the guts of keystoneauth so that a person could do rate-limiting on all REST operations. It would also catch some of the "hidden" operations, like fetching a token. Also, TaskManager is a _very_ stable set of code, so moving it a little further away shouldn't run the risk of deep/long bug chasing. In preparation for that, we need to make the shade/nodepool things in it be explicitly shade/nodepool things, and the base classes actually be generic. Doing the refactoring here first reduces the complexity of a move. Next step would be adding this version of TaskManager to ksa, then in a subsequent patch we can remove it from shade. If we never do that, nothing in this should hurt anything. Change-Id: Id5c8caa924e8abbecef26d54d225bb10e9676d5d --- shade/task_manager.py | 93 ++++++++++++++++++++++++++++--------------- 1 file changed, 62 insertions(+), 31 deletions(-) diff --git a/shade/task_manager.py b/shade/task_manager.py index cca56ad52..5ffb1029a 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -28,8 +28,28 @@ from shade import meta +def _is_listlike(obj): + # NOTE(Shrews): Since the client API might decide to subclass one + # of these result types, we use isinstance() here instead of type(). + return ( + isinstance(obj, list) or + isinstance(obj, types.GeneratorType)) + + +def _is_objlike(obj): + # NOTE(Shrews): Since the client API might decide to subclass one + # of these result types, we use isinstance() here instead of type(). + return ( + not isinstance(obj, bool) and + not isinstance(obj, int) and + not isinstance(obj, float) and + not isinstance(obj, six.string_types) and + not isinstance(obj, set) and + not isinstance(obj, tuple)) + + @six.add_metaclass(abc.ABCMeta) -class Task(object): +class BaseTask(object): """Represent a task to be performed on an OpenStack Cloud. Some consumers need to inject things like rate-limiting or auditing @@ -53,18 +73,14 @@ def __init__(self, **kw): self._response = None self._finished = threading.Event() self.args = kw - self.requests = False - self._request_id = None + self.name = type(self).__name__ @abc.abstractmethod def main(self, client): """ Override this method with the actual workload to be performed """ def done(self, result): - if self.requests: - self._response, self._result = result - else: - self._result = result + self._result = result self._finished.set() def exception(self, e, tb): @@ -79,26 +95,7 @@ def wait(self, raw=False): six.reraise(type(self._exception), self._exception, self._traceback) - if raw: - # Do NOT convert the result. - return self._result - - # NOTE(Shrews): Since the client API might decide to subclass one - # of these result types, we use isinstance() here instead of type(). - if (isinstance(self._result, list) or - isinstance(self._result, types.GeneratorType)): - return meta.obj_list_to_dict( - self._result, request_id=self._request_id) - elif (not isinstance(self._result, bool) and - not isinstance(self._result, int) and - not isinstance(self._result, float) and - not isinstance(self._result, six.string_types) and - not isinstance(self._result, set) and - not isinstance(self._result, tuple) and - not isinstance(self._result, types.GeneratorType)): - return meta.obj_to_dict(self._result, request_id=self._request_id) - else: - return self._result + return self._result def run(self, client): self._client = client @@ -117,7 +114,26 @@ def run(self, client): self.exception(e, sys.exc_info()[2]) -class RequestTask(Task): +class Task(BaseTask): + """ Shade specific additions to the BaseTask Interface. """ + + def wait(self, raw=False): + super(Task, self).wait() + + if raw: + # Do NOT convert the result. + return self._result + + if _is_listlike(self._result): + return meta.obj_list_to_dict(self._result) + elif _is_objlike(self._result): + return meta.obj_to_dict(self._result) + else: + return self._result + + +class RequestTask(BaseTask): + """ Extensions to the Shade Tasks to handle raw requests """ # It's totally legit for calls to not return things result_key = None @@ -138,12 +154,27 @@ def done(self, result): self._result = result_json[self.result_key] else: self._result = result_json + self._request_id = self._response.headers.get('x-openstack-request-id') self._finished.set() + def wait(self, raw=False): + super(RequestTask, self).wait() + + if raw: + # Do NOT convert the result. + return self._result + + if _is_listlike(self._result): + return meta.obj_list_to_dict( + self._result, request_id=self._request_id) + elif _is_objlike(self._result): + return meta.obj_to_dict(self._result, request_id=self._request_id) + return self._result + class TaskManager(object): - log = _log.setup_logging("shade.TaskManager") + log = _log.setup_logging(__name__) def __init__(self, client, name): self.name = name @@ -165,11 +196,11 @@ def submitTask(self, task, raw=False): underlying client call. """ self.log.debug( - "Manager %s running task %s" % (self.name, type(task).__name__)) + "Manager %s running task %s" % (self.name, task.name)) start = time.time() task.run(self._client) end = time.time() self.log.debug( "Manager %s ran task %s in %ss" % ( - self.name, type(task).__name__, (end - start))) + self.name, task.name, (end - start))) return task.wait(raw) From 522db8e6e49752da203cc3532bebc14014b6c076 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 14 Apr 2016 11:43:05 -0500 Subject: [PATCH 1019/3836] Add submit_function method to TaskManager TaskManager operates on Tasks - which is awesome for things like shade and nodepool where the discrete actions embodied by a task can be pre-conceived. However, making a task class is a pretty heavy weight operation to require for ad-hoc tasks. Luckily, Python lets us create classes and closures. Add a method that allows someone to pass in a callable or the name of a callable that should be found on client. Change-Id: I416a7077bdd3427262243d8a39b6ed6b35ae4018 --- shade/task_manager.py | 53 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/shade/task_manager.py b/shade/task_manager.py index 5ffb1029a..3c675e1ac 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -173,12 +173,49 @@ def wait(self, raw=False): return self._result +def _result_filter_cb(result): + return result + + +def generate_task_class(method, name, result_filter_cb): + if name is None: + if callable(method): + name = method.__name__ + else: + name = method + + class RunTask(Task): + def __init__(self, **kw): + super(RunTask, self).__init__(**kw) + self.name = name + self._method = method + + def wait(self, raw=False): + super(RequestTask, self).wait() + + if raw: + # Do NOT convert the result. + return self._result + return result_filter_cb(self._result) + + def main(self, client): + if callable(self._method): + return method(**self.args) + else: + meth = getattr(client, self._method) + return meth(**self.args) + + class TaskManager(object): log = _log.setup_logging(__name__) - def __init__(self, client, name): + def __init__(self, client, name, result_filter_cb=None): self.name = name self._client = client + if not result_filter_cb: + self._result_filter_cb = _result_filter_cb + else: + self._result_filter_cb = result_filter_cb def stop(self): """ This is a direct action passthrough TaskManager """ @@ -204,3 +241,17 @@ def submitTask(self, task, raw=False): "Manager %s ran task %s in %ss" % ( self.name, task.name, (end - start))) return task.wait(raw) + + def submit_function( + self, method, name=None, result_filter_cb=None, **kwargs): + """ Allows submitting an arbitrary method for work. + + :param method: Method to run in the TaskManager. Can be either the + name of a method to find on self.client, or a callable. + """ + if not result_filter_cb: + result_filter_cb = self._result_filter_cb + + task_class = generate_task_class(method, name, result_filter_cb) + + return self.manager.submitTask(task_class(**kwargs)) From 2b52bcf6943369fe96db3aefe2ee7473dd51b934 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 29 Aug 2016 13:07:17 -0500 Subject: [PATCH 1020/3836] Add prompting for KSA options Teach OpenStackConfig to prompt the user for KSA plugin options that have no value but have a prompt string defined. * Add pw_func argument to __init__() to be used as the callback for prompting the user. The default is None which skips the prompt step. * Add option_prompt() method to perform the checks for prompting, call the callback and save the returned value. This is public to handle cases where simply passing in a callback is insufficient for the prompt mechanism. Related-Bug: #1617384 Change-Id: I5faa86e94d6f71282ac270e2acfbd3016638c780 --- os_client_config/config.py | 23 ++++++++++++++++- os_client_config/tests/test_config.py | 37 +++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 586f0a79c..54048aad1 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -173,7 +173,8 @@ class OpenStackConfig(object): def __init__(self, config_files=None, vendor_files=None, override_defaults=None, force_ipv4=None, - envvar_prefix=None, secure_files=None): + envvar_prefix=None, secure_files=None, + pw_func=None): self.log = _log.setup_logging(__name__) self._config_files = config_files or CONFIG_FILES @@ -288,6 +289,10 @@ def __init__(self, config_files=None, vendor_files=None, # Flag location to hold the peeked value of an argparse timeout value self._argv_timeout = False + # Save the password callback + # password = self._pw_callback(prompt="Password: ") + self._pw_callback = pw_func + def get_extra_config(self, key, defaults=None): """Fetch an arbitrary extra chunk of config, laying in defaults. @@ -924,6 +929,9 @@ def _validate_auth(self, config, loader): winning_value, ) + # See if this needs a prompting + config = self.option_prompt(config, p_opt) + return config def _validate_auth_correctly(self, config, loader): @@ -952,6 +960,19 @@ def _validate_auth_correctly(self, config, loader): winning_value, ) + # See if this needs a prompting + config = self.option_prompt(config, p_opt) + + return config + + def option_prompt(self, config, p_opt): + """Prompt user for option that requires a value""" + if ( + p_opt.prompt is not None and + p_opt.dest not in config['auth'] and + self._pw_callback is not None + ): + config['auth'][p_opt.dest] = self._pw_callback(p_opt.prompt) return config def _clean_up_after_ourselves(self, config, p_opt, winning_value): diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 5bcf7660e..5cded544a 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -27,6 +27,11 @@ from os_client_config.tests import base +def prompt_for_password(prompt=None): + """Fake prompt function that just returns a constant string""" + return 'promptpass' + + class TestConfig(base.TestCase): def test_get_all_clouds(self): @@ -787,6 +792,38 @@ def test_register_argparse_network_service_types(self): self.assertNotIn('http_timeout', cloud.config) +class TestConfigPrompt(base.TestCase): + + def setUp(self): + super(TestConfigPrompt, self).setUp() + + self.args = dict( + auth_url='http://example.com/v2', + username='user', + project_name='project', + # region_name='region2', + auth_type='password', + ) + + self.options = argparse.Namespace(**self.args) + + def test_get_one_cloud_prompt(self): + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + pw_func=prompt_for_password, + ) + + # This needs a cloud definition without a password. + # If this starts failing unexpectedly check that the cloud_yaml + # and/or vendor_yaml do not have a password in the selected cloud. + cc = c.get_one_cloud( + cloud='_test_cloud_no_vendor', + argparse=self.options, + ) + self.assertEqual('promptpass', cc.auth['password']) + + class TestConfigDefault(base.TestCase): def setUp(self): From 5c1371b22655235fa6b394c89732ca5326d334b8 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 22 Aug 2016 09:15:10 -0500 Subject: [PATCH 1021/3836] Change naming style of submitTask submitTask is camel cased because it came from nodepool, where camel case is the prevailing style and thus correct. However, in shade, underscores are the prevailing style, so this always feels weird. Make the method submit_task - but add an alias for submitTask to not break any code using the old method name. Change-Id: I782d3b914a1b66b5af20315e58568a715222fe58 --- shade/openstackcloud.py | 258 +++++++++++++------------- shade/operatorcloud.py | 146 +++++++-------- shade/task_manager.py | 6 +- shade/tests/unit/test_task_manager.py | 12 +- 4 files changed, 214 insertions(+), 208 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 98f40b234..da38b1559 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -182,6 +182,10 @@ def __init__( self.manager = task_manager.TaskManager( name=':'.join([self.name, self.region_name]), client=self) + # Work around older TaskManager objects that don't have submit_task + if not hasattr(self.manager, 'submit_task'): + self.manager.submit_task = self.manager.submitTask + (self.verify, self.cert) = cloud_config.get_requests_verify_args() # Turn off urllib3 warnings about insecure certs if we have # explicitly configured requests to tell it we do not want @@ -504,10 +508,10 @@ def list_projects(self, domain_id=None): """ try: if self.cloud_config.get_api_version('identity') == '3': - projects = self.manager.submitTask( + projects = self.manager.submit_task( _tasks.ProjectList(domain=domain_id)) else: - projects = self.manager.submitTask( + projects = self.manager.submit_task( _tasks.ProjectList()) except Exception as e: self.log.debug("Failed to list projects", exc_info=True) @@ -560,7 +564,7 @@ def update_project(self, name_or_id, description=None, enabled=True, else: params['tenant_id'] = proj['id'] - project = self.manager.submitTask(_tasks.ProjectUpdate( + project = self.manager.submit_task(_tasks.ProjectUpdate( description=description, enabled=enabled, **params)) @@ -578,7 +582,7 @@ def create_project( else: params['tenant_name'] = name - project = self.manager.submitTask(_tasks.ProjectCreate( + project = self.manager.submit_task(_tasks.ProjectCreate( project_name=name, description=description, enabled=enabled, **params)) self.list_projects.invalidate(self) @@ -611,7 +615,7 @@ def delete_project(self, name_or_id, domain_id=None): params['project'] = project['id'] else: params['tenant'] = project['id'] - self.manager.submitTask(_tasks.ProjectDelete(**params)) + self.manager.submit_task(_tasks.ProjectDelete(**params)) return True @@ -625,7 +629,7 @@ def list_users(self): the openstack API call. """ with _utils.shade_exceptions("Failed to list users"): - users = self.manager.submitTask(_tasks.UserList()) + users = self.manager.submit_task(_tasks.UserList()) return _utils.normalize_users(users) def search_users(self, name_or_id=None, filters=None): @@ -666,7 +670,7 @@ def get_user_by_id(self, user_id, normalize=True): with _utils.shade_exceptions( "Error getting user with ID {user_id}".format( user_id=user_id)): - user = self.manager.submitTask(_tasks.UserGet(user=user_id)) + user = self.manager.submit_task(_tasks.UserGet(user=user_id)) if user and normalize: return _utils.normalize_users([user])[0] return user @@ -690,7 +694,7 @@ def update_user(self, name_or_id, **kwargs): with _utils.shade_exceptions( "Error updating password for {user}".format( user=name_or_id)): - user = self.manager.submitTask(_tasks.UserPasswordUpdate( + user = self.manager.submit_task(_tasks.UserPasswordUpdate( user=kwargs['user'], password=password)) elif 'domain_id' in kwargs: # The incoming parameter is domain_id in order to match the @@ -700,7 +704,7 @@ def update_user(self, name_or_id, **kwargs): with _utils.shade_exceptions("Error in updating user {user}".format( user=name_or_id)): - user = self.manager.submitTask(_tasks.UserUpdate(**kwargs)) + user = self.manager.submit_task(_tasks.UserUpdate(**kwargs)) self.list_users.invalidate(self) return _utils.normalize_users([user])[0] @@ -712,7 +716,7 @@ def create_user( user=name)): identity_params = self._get_identity_params( domain_id, default_project) - user = self.manager.submitTask(_tasks.UserCreate( + user = self.manager.submit_task(_tasks.UserCreate( name=name, password=password, email=email, enabled=enabled, **identity_params)) self.list_users.invalidate(self) @@ -730,7 +734,7 @@ def delete_user(self, name_or_id): user = self.get_user_by_id(user['id'], normalize=False) with _utils.shade_exceptions("Error in deleting user {user}".format( user=name_or_id)): - self.manager.submitTask(_tasks.UserDelete(user=user)) + self.manager.submit_task(_tasks.UserDelete(user=user)) self.list_users.invalidate(self) return True @@ -762,7 +766,7 @@ def add_user_to_group(self, name_or_id, group_name_or_id): "Error adding user {user} to group {group}".format( user=name_or_id, group=group_name_or_id) ): - self.manager.submitTask( + self.manager.submit_task( _tasks.UserAddToGroup(user=user['id'], group=group['id']) ) @@ -780,7 +784,7 @@ def is_user_in_group(self, name_or_id, group_name_or_id): user, group = self._get_user_and_group(name_or_id, group_name_or_id) try: - return self.manager.submitTask( + return self.manager.submit_task( _tasks.UserCheckInGroup(user=user['id'], group=group['id']) ) except keystoneauth1.exceptions.http.NotFound: @@ -810,7 +814,7 @@ def remove_user_from_group(self, name_or_id, group_name_or_id): "Error removing user {user} from group {group}".format( user=name_or_id, group=group_name_or_id) ): - self.manager.submitTask( + self.manager.submit_task( _tasks.UserRemoveFromGroup(user=user['id'], group=group['id']) ) @@ -974,7 +978,7 @@ def create_stack( ) with _utils.heat_exceptions("Error creating stack {name}".format( name=name)): - self.manager.submitTask(_tasks.StackCreate(**params)) + self.manager.submit_task(_tasks.StackCreate(**params)) if wait: event_utils.poll_for_events(self.heat_client, stack_name=name, action='CREATE') @@ -1037,7 +1041,7 @@ def update_stack( with _utils.heat_exceptions("Error updating stack {name}".format( name=name_or_id)): - self.manager.submitTask(_tasks.StackUpdate(**params)) + self.manager.submit_task(_tasks.StackUpdate(**params)) if wait: event_utils.poll_for_events(self.heat_client, name_or_id, @@ -1071,7 +1075,7 @@ def delete_stack(self, name_or_id, wait=False): with _utils.heat_exceptions("Failed to delete stack {id}".format( id=name_or_id)): - self.manager.submitTask(_tasks.StackDelete(id=stack['id'])) + self.manager.submit_task(_tasks.StackDelete(id=stack['id'])) if wait: try: event_utils.poll_for_events(self.heat_client, @@ -1165,7 +1169,7 @@ def _nova_extensions(self): extensions = set() with _utils.shade_exceptions("Error fetching extension list for nova"): - for extension in self.manager.submitTask( + for extension in self.manager.submit_task( _tasks.NovaListExtensions()): extensions.add(extension['alias']) @@ -1321,7 +1325,7 @@ def list_keypairs(self): """ with _utils.shade_exceptions("Error fetching keypair list"): - return self.manager.submitTask(_tasks.KeypairList()) + return self.manager.submit_task(_tasks.KeypairList()) def list_networks(self, filters=None): """List all available networks. @@ -1334,7 +1338,7 @@ def list_networks(self, filters=None): if not filters: filters = {} with _utils.neutron_exceptions("Error fetching network list"): - return self.manager.submitTask( + return self.manager.submit_task( _tasks.NetworkList(**filters))['networks'] def list_routers(self, filters=None): @@ -1348,7 +1352,7 @@ def list_routers(self, filters=None): if not filters: filters = {} with _utils.neutron_exceptions("Error fetching router list"): - return self.manager.submitTask( + return self.manager.submit_task( _tasks.RouterList(**filters))['routers'] def list_subnets(self, filters=None): @@ -1362,7 +1366,7 @@ def list_subnets(self, filters=None): if not filters: filters = {} with _utils.neutron_exceptions("Error fetching subnet list"): - return self.manager.submitTask( + return self.manager.submit_task( _tasks.SubnetList(**filters))['subnets'] @_utils.cache_on_arguments(resource='ports') @@ -1377,7 +1381,7 @@ def list_ports(self, filters=None): if not filters: filters = {} with _utils.neutron_exceptions("Error fetching port list"): - return self.manager.submitTask( + return self.manager.submit_task( _tasks.PortList(**filters))['ports'] @_utils.cache_on_arguments(should_cache_fn=_no_pending_volumes) @@ -1392,7 +1396,7 @@ def list_volumes(self, cache=True): 'invalidate instead.') with _utils.shade_exceptions("Error fetching volume list"): return _utils.normalize_volumes( - self.manager.submitTask(_tasks.VolumeList())) + self.manager.submit_task(_tasks.VolumeList())) @_utils.cache_on_arguments() def list_flavors(self, get_extra=True): @@ -1402,7 +1406,7 @@ def list_flavors(self, get_extra=True): """ with _utils.shade_exceptions("Error fetching flavor list"): - flavors = self.manager.submitTask( + flavors = self.manager.submit_task( _tasks.FlavorList(is_public=None)) with _utils.shade_exceptions("Error fetching flavor extra specs"): @@ -1412,7 +1416,7 @@ def list_flavors(self, get_extra=True): 'OS-FLV-WITH-EXT-SPECS:extra_specs') elif get_extra: try: - flavor.extra_specs = self.manager.submitTask( + flavor.extra_specs = self.manager.submit_task( _tasks.FlavorGetExtraSpecs(id=flavor.id)) except keystoneauth1.exceptions.http.HttpError as e: flavor.extra_specs = [] @@ -1432,7 +1436,7 @@ def list_stacks(self): openstack API call. """ with _utils.shade_exceptions("Error fetching stack list"): - stacks = self.manager.submitTask(_tasks.StackList()) + stacks = self.manager.submit_task(_tasks.StackList()) return _utils.normalize_stacks(stacks) def list_server_security_groups(self, server): @@ -1446,7 +1450,7 @@ def list_server_security_groups(self, server): return [] with _utils.shade_exceptions(): - groups = self.manager.submitTask( + groups = self.manager.submit_task( _tasks.ServerListSecurityGroups(server=server['id'])) return _utils.normalize_nova_secgroups(groups) @@ -1468,13 +1472,13 @@ def list_security_groups(self): # Neutron returns dicts, so no need to convert objects here. with _utils.neutron_exceptions( "Error fetching security group list"): - return self.manager.submitTask( + return self.manager.submit_task( _tasks.NeutronSecurityGroupList())['security_groups'] # Handle nova security groups else: with _utils.shade_exceptions("Error fetching security group list"): - groups = self.manager.submitTask( + groups = self.manager.submit_task( _tasks.NovaSecurityGroupList()) return _utils.normalize_nova_secgroups(groups) @@ -1490,7 +1494,7 @@ def list_servers(self, detailed=False): cloud=self.name, region=self.region_name)): servers = _utils.normalize_servers( - self.manager.submitTask(_tasks.ServerList()), + self.manager.submit_task(_tasks.ServerList()), cloud_name=self.name, region_name=self.region_name) if detailed: @@ -1511,7 +1515,7 @@ def list_server_groups(self): """ with _utils.shade_exceptions("Error fetching server group list"): - return self.manager.submitTask(_tasks.ServerGroupList()) + return self.manager.submit_task(_tasks.ServerGroupList()) @_utils.cache_on_arguments(should_cache_fn=_no_pending_images) def list_images(self, filter_deleted=True): @@ -1529,14 +1533,14 @@ def list_images(self, filter_deleted=True): # if we want to deal with page size per unit of rate limiting image_gen = self.glance_client.images.list(page_size=1000) # Deal with the generator to make a list - image_list = self.manager.submitTask( + image_list = self.manager.submit_task( _tasks.GlanceImageList(image_gen=image_gen)) except glanceclient.exc.HTTPInternalServerError: # We didn't have glance, let's try nova # If this doesn't work - we just let the exception propagate with _utils.shade_exceptions("Error fetching image list"): - image_list = self.manager.submitTask(_tasks.NovaImageList()) + image_list = self.manager.submit_task(_tasks.NovaImageList()) except OpenStackCloudException: raise except Exception as e: @@ -1563,7 +1567,7 @@ def list_floating_ip_pools(self): 'Floating IP pools extension is not available on target cloud') with _utils.shade_exceptions("Error fetching floating IP pool list"): - return self.manager.submitTask(_tasks.FloatingIPPoolList()) + return self.manager.submit_task(_tasks.FloatingIPPoolList()) @_utils.cache_on_arguments(resource='floating_ip') def list_floating_ips(self): @@ -1587,12 +1591,12 @@ def list_floating_ips(self): def _neutron_list_floating_ips(self): with _utils.neutron_exceptions("error fetching floating IPs list"): - return self.manager.submitTask( + return self.manager.submit_task( _tasks.NeutronFloatingIPList())['floatingips'] def _nova_list_floating_ips(self): with _utils.shade_exceptions("Error fetching floating IPs list"): - return self.manager.submitTask(_tasks.NovaFloatingIPList()) + return self.manager.submit_task(_tasks.NovaFloatingIPList()) def use_external_network(self): return self._use_external_network @@ -2155,7 +2159,7 @@ def get_server(self, name_or_id=None, filters=None, detailed=False): def get_server_by_id(self, id): return meta.add_server_interfaces(self, _utils.normalize_server( - self.manager.submitTask(_tasks.ServerGet(server=id)), + self.manager.submit_task(_tasks.ServerGet(server=id)), cloud_name=self.name, region_name=self.region_name)) def get_server_group(self, name_or_id=None, filters=None): @@ -2277,7 +2281,7 @@ def search_one_stack(name_or_id=None, filters=None): # so a StackGet can always be used for name or ID. with _utils.shade_exceptions("Error fetching stack"): try: - stack = self.manager.submitTask( + stack = self.manager.submit_task( _tasks.StackGet(stack_id=name_or_id)) # Treat DELETE_COMPLETE stacks as a NotFound if stack['stack_status'] == 'DELETE_COMPLETE': @@ -2301,7 +2305,7 @@ def create_keypair(self, name, public_key): """ with _utils.shade_exceptions("Unable to create keypair {name}".format( name=name)): - return self.manager.submitTask(_tasks.KeypairCreate( + return self.manager.submit_task(_tasks.KeypairCreate( name=name, public_key=public_key)) def delete_keypair(self, name): @@ -2314,7 +2318,7 @@ def delete_keypair(self, name): :raises: OpenStackCloudException on operation error. """ try: - self.manager.submitTask(_tasks.KeypairDelete(key=name)) + self.manager.submit_task(_tasks.KeypairDelete(key=name)) except nova_exceptions.NotFound: self.log.debug("Keypair %s not found for deleting" % name) return False @@ -2372,7 +2376,7 @@ def create_network(self, name, shared=False, admin_state_up=True, with _utils.neutron_exceptions( "Error creating network {0}".format(name)): - net = self.manager.submitTask( + net = self.manager.submit_task( _tasks.NetworkCreate(body=dict({'network': network}))) # Reset cache so the new network is picked up @@ -2396,7 +2400,7 @@ def delete_network(self, name_or_id): with _utils.neutron_exceptions( "Error deleting network {0}".format(name_or_id)): - self.manager.submitTask( + self.manager.submit_task( _tasks.NetworkDelete(network=network['id'])) # Reset cache so the deleted network is removed @@ -2444,7 +2448,7 @@ def add_router_interface(self, router, subnet_id=None, port_id=None): with _utils.neutron_exceptions( "Error attaching interface to router {0}".format(router['id']) ): - return self.manager.submitTask( + return self.manager.submit_task( _tasks.RouterAddInterface(router=router['id'], body=body) ) @@ -2472,7 +2476,7 @@ def remove_router_interface(self, router, subnet_id=None, port_id=None): with _utils.neutron_exceptions( "Error detaching interface from router {0}".format(router['id']) ): - return self.manager.submitTask( + return self.manager.submit_task( _tasks.RouterRemoveInterface(router=router['id'], body=body) ) @@ -2554,7 +2558,7 @@ def create_router(self, name=None, admin_state_up=True, with _utils.neutron_exceptions( "Error creating router {0}".format(name)): - new_router = self.manager.submitTask( + new_router = self.manager.submit_task( _tasks.RouterCreate(body=dict(router=router))) return new_router['router'] @@ -2605,7 +2609,7 @@ def update_router(self, name_or_id, name=None, admin_state_up=None, with _utils.neutron_exceptions( "Error updating router {0}".format(name_or_id)): - new_router = self.manager.submitTask( + new_router = self.manager.submit_task( _tasks.RouterUpdate( router=curr_router['id'], body=dict(router=router))) @@ -2631,7 +2635,7 @@ def delete_router(self, name_or_id): with _utils.neutron_exceptions( "Error deleting router {0}".format(name_or_id)): - self.manager.submitTask( + self.manager.submit_task( _tasks.RouterDelete(router=router['id'])) return True @@ -2659,7 +2663,7 @@ def get_image_id(self, image_name, exclude=None): def create_image_snapshot( self, name, server, wait=False, timeout=3600, **metadata): - image_id = str(self.manager.submitTask(_tasks.ImageSnapshotCreate( + image_id = str(self.manager.submit_task(_tasks.ImageSnapshotCreate( image_name=name, server=server, metadata=metadata))) self.list_images.invalidate(self) image = self.get_image(image_id) @@ -2691,10 +2695,10 @@ def delete_image( # it's image_id glance_api_version = self.cloud_config.get_api_version('image') if glance_api_version == '2': - self.manager.submitTask( + self.manager.submit_task( _tasks.ImageDelete(image_id=image.id)) elif glance_api_version == '1': - self.manager.submitTask( + self.manager.submit_task( _tasks.ImageDelete(image=image.id)) self.list_images.invalidate(self) @@ -2884,15 +2888,15 @@ def _upload_image_put_v2(self, name, image_data, meta, **image_kwargs): image_kwargs.update(self._make_v2_image_params(meta, properties)) - image = self.manager.submitTask(_tasks.ImageCreate( + image = self.manager.submit_task(_tasks.ImageCreate( name=name, **image_kwargs)) try: - self.manager.submitTask(_tasks.ImageUpload( + self.manager.submit_task(_tasks.ImageUpload( image_id=image.id, image_data=image_data)) except Exception: self.log.debug("Deleting failed upload of image {image}".format( image=image['name'])) - self.manager.submitTask(_tasks.ImageDelete(image_id=image.id)) + self.manager.submit_task(_tasks.ImageDelete(image_id=image.id)) raise return image @@ -2901,16 +2905,16 @@ def _upload_image_put_v1( self, name, image_data, meta, **image_kwargs): image_kwargs['properties'].update(meta) - image = self.manager.submitTask(_tasks.ImageCreate( + image = self.manager.submit_task(_tasks.ImageCreate( name=name, **image_kwargs)) try: - self.manager.submitTask(_tasks.ImageUpdate( + self.manager.submit_task(_tasks.ImageUpdate( image=image, data=image_data)) except Exception: self.log.debug("Deleting failed upload of image {image}".format( image=image['name'])) # Note argument is "image" here, "image_id" in V2 - self.manager.submitTask(_tasks.ImageDelete(image=image.id)) + self.manager.submit_task(_tasks.ImageDelete(image=image.id)) raise return image @@ -2963,7 +2967,7 @@ def _upload_image_task( import_from='{container}/{name}'.format( container=container, name=name), image_properties=dict(name=name))) - glance_task = self.manager.submitTask( + glance_task = self.manager.submit_task( _tasks.ImageTaskCreate(**task_args)) self.list_images.invalidate(self) if wait: @@ -2973,7 +2977,7 @@ def _upload_image_task( "Timeout waiting for the image to import."): try: if image_id is None: - status = self.manager.submitTask( + status = self.manager.submit_task( _tasks.ImageTaskGet(task_id=glance_task.id)) except glanceclient.exc.HTTPServiceUnavailable: # Intermittent failure - catch and try again @@ -2993,7 +2997,7 @@ def _upload_image_task( return self.get_image(status.result['image_id']) if status.status == 'failure': if status.message == IMAGE_ERROR_396: - glance_task = self.manager.submitTask( + glance_task = self.manager.submit_task( _tasks.ImageTaskCreate(**task_args)) self.list_images.invalidate(self) else: @@ -3031,7 +3035,7 @@ def _update_image_properties_v2(self, image, meta, properties): img_props[k] = v if not img_props: return False - self.manager.submitTask(_tasks.ImageUpdate( + self.manager.submit_task(_tasks.ImageUpdate( image_id=image.id, **img_props)) self.list_images.invalidate(self) return True @@ -3044,7 +3048,7 @@ def _update_image_properties_v1(self, image, meta, properties): img_props[k] = v if not img_props: return False - self.manager.submitTask(_tasks.ImageUpdate( + self.manager.submit_task(_tasks.ImageUpdate( image=image, properties=img_props)) self.list_images.invalidate(self) return True @@ -3079,7 +3083,7 @@ def create_volume( kwargs['imageRef'] = image_obj['id'] kwargs = self._get_volume_kwargs(kwargs) with _utils.shade_exceptions("Error in creating volume"): - volume = self.manager.submitTask(_tasks.VolumeCreate( + volume = self.manager.submit_task(_tasks.VolumeCreate( size=size, **kwargs)) self.list_volumes.invalidate(self) @@ -3128,7 +3132,7 @@ def delete_volume(self, name_or_id=None, wait=True, timeout=None): with _utils.shade_exceptions("Error in deleting volume"): try: - self.manager.submitTask( + self.manager.submit_task( _tasks.VolumeDelete(volume=volume['id'])) except cinder_exceptions.NotFound: self.log.debug( @@ -3201,7 +3205,7 @@ def detach_volume(self, server, volume, wait=True, timeout=None): with _utils.shade_exceptions( "Error detaching volume {volume} from server {server}".format( volume=volume['id'], server=server['id'])): - self.manager.submitTask( + self.manager.submit_task( _tasks.VolumeDetach(attachment_id=volume['id'], server_id=server['id'])) @@ -3264,7 +3268,7 @@ def attach_volume(self, server, volume, device=None, "Error attaching volume {volume_id} to server " "{server_id}".format(volume_id=volume['id'], server_id=server['id'])): - vol = self.manager.submitTask( + vol = self.manager.submit_task( _tasks.VolumeAttach(volume_id=volume['id'], server_id=server['id'], device=device)) @@ -3337,7 +3341,7 @@ def create_volume_snapshot(self, volume_id, force=False, with _utils.shade_exceptions( "Error creating snapshot of volume {volume_id}".format( volume_id=volume_id)): - snapshot = self.manager.submitTask( + snapshot = self.manager.submit_task( _tasks.VolumeSnapshotCreate( volume_id=volume_id, force=force, **kwargs)) @@ -3371,7 +3375,7 @@ def get_volume_snapshot_by_id(self, snapshot_id): with _utils.shade_exceptions( "Error getting snapshot {snapshot_id}".format( snapshot_id=snapshot_id)): - snapshot = self.manager.submitTask( + snapshot = self.manager.submit_task( _tasks.VolumeSnapshotGet( snapshot_id=snapshot_id ) @@ -3409,7 +3413,7 @@ def list_volume_snapshots(self, detailed=True, search_opts=None): """ with _utils.shade_exceptions("Error getting a list of snapshots"): return _utils.normalize_volumes( - self.manager.submitTask( + self.manager.submit_task( _tasks.VolumeSnapshotList( detailed=detailed, search_opts=search_opts))) @@ -3432,7 +3436,7 @@ def delete_volume_snapshot(self, name_or_id=None, wait=False, return False with _utils.shade_exceptions("Error in deleting volume snapshot"): - self.manager.submitTask( + self.manager.submit_task( _tasks.VolumeSnapshotDelete( snapshot=volumesnapshot['id'] ) @@ -3668,7 +3672,7 @@ def create_floating_ip(self, network=None, server=None, def _submit_create_fip(self, kwargs): # Split into a method to aid in test mocking return _utils.normalize_neutron_floating_ips( - [self.manager.submitTask(_tasks.NeutronFloatingIPCreate( + [self.manager.submit_task(_tasks.NeutronFloatingIPCreate( body={'floatingip': kwargs}))['floatingip']])[0] def _neutron_create_floating_ip( @@ -3759,7 +3763,7 @@ def _nova_create_floating_ip(self, pool=None): "unable to find a floating ip pool") pool = pools[0]['name'] - pool_ip = self.manager.submitTask( + pool_ip = self.manager.submit_task( _tasks.NovaFloatingIPCreate(pool=pool)) return pool_ip @@ -3816,7 +3820,7 @@ def _delete_floating_ip(self, floating_ip_id): def _neutron_delete_floating_ip(self, floating_ip_id): try: with _utils.neutron_exceptions("unable to delete floating IP"): - self.manager.submitTask( + self.manager.submit_task( _tasks.NeutronFloatingIPDelete(floatingip=floating_ip_id)) except OpenStackCloudResourceNotFound: return False @@ -3828,7 +3832,7 @@ def _neutron_delete_floating_ip(self, floating_ip_id): def _nova_delete_floating_ip(self, floating_ip_id): try: - self.manager.submitTask( + self.manager.submit_task( _tasks.NovaFloatingIPDelete(floating_ip=floating_ip_id)) except nova_exceptions.NotFound: return False @@ -4046,7 +4050,7 @@ def _neutron_attach_ip_to_server( if fixed_address is not None: floating_ip_args['fixed_ip_address'] = fixed_address - return self.manager.submitTask(_tasks.NeutronFloatingIPUpdate( + return self.manager.submit_task(_tasks.NeutronFloatingIPUpdate( floatingip=floating_ip['id'], body={'floatingip': floating_ip_args} ))['floatingip'] @@ -4057,7 +4061,7 @@ def _nova_attach_ip_to_server(self, server_id, floating_ip_id, "Error attaching IP {ip} to instance {id}".format( ip=floating_ip_id, id=server_id)): f_ip = self.get_floating_ip(id=floating_ip_id) - return self.manager.submitTask(_tasks.NovaFloatingIPAttach( + return self.manager.submit_task(_tasks.NovaFloatingIPAttach( server=server_id, address=f_ip['floating_ip_address'], fixed_address=fixed_address)) @@ -4093,7 +4097,7 @@ def _neutron_detach_ip_from_server(self, server_id, floating_ip_id): f_ip = self.get_floating_ip(id=floating_ip_id) if f_ip is None or not f_ip['attached']: return False - self.manager.submitTask(_tasks.NeutronFloatingIPUpdate( + self.manager.submit_task(_tasks.NeutronFloatingIPUpdate( floatingip=floating_ip_id, body={'floatingip': {'port_id': None}})) @@ -4105,7 +4109,7 @@ def _nova_detach_ip_from_server(self, server_id, floating_ip_id): if f_ip is None: raise OpenStackCloudException( "unable to find floating IP {0}".format(floating_ip_id)) - self.manager.submitTask(_tasks.NovaFloatingIPDetach( + self.manager.submit_task(_tasks.NovaFloatingIPDetach( server=server_id, address=f_ip['floating_ip_address'])) except nova_exceptions.Conflict as e: self.log.debug( @@ -4555,7 +4559,7 @@ def create_server( volumes=volumes, kwargs=kwargs) with _utils.shade_exceptions("Error in creating instance"): - server = self.manager.submitTask(_tasks.ServerCreate( + server = self.manager.submit_task(_tasks.ServerCreate( name=name, **kwargs)) admin_pass = server.get('adminPass') or kwargs.get('admin_pass') if not wait: @@ -4665,7 +4669,7 @@ def get_active_server( def rebuild_server(self, server_id, image_id, admin_pass=None, wait=False, timeout=180): with _utils.shade_exceptions("Error in rebuilding instance"): - server = self.manager.submitTask(_tasks.ServerRebuild( + server = self.manager.submit_task(_tasks.ServerRebuild( server=server_id, image=image_id, password=admin_pass)) if wait: admin_pass = server.get('adminPass') or admin_pass @@ -4700,7 +4704,7 @@ def set_server_metadata(self, name_or_id, metadata): :raises: OpenStackCloudException on operation error. """ try: - self.manager.submitTask( + self.manager.submit_task( _tasks.ServerSetMetadata(server=self.get_server(name_or_id), metadata=metadata)) except OpenStackCloudException: @@ -4720,7 +4724,7 @@ def delete_server_metadata(self, name_or_id, metadata_keys): :raises: OpenStackCloudException on operation error. """ try: - self.manager.submitTask( + self.manager.submit_task( _tasks.ServerDeleteMetadata(server=self.get_server(name_or_id), keys=metadata_keys)) except OpenStackCloudException: @@ -4787,7 +4791,7 @@ def _delete_server( id=server['id'])) try: - self.manager.submitTask( + self.manager.submit_task( _tasks.ServerDelete(server=server['id'])) except nova_exceptions.NotFound: return False @@ -4844,7 +4848,7 @@ def update_server(self, name_or_id, **kwargs): with _utils.shade_exceptions( "Error updating server {0}".format(name_or_id)): - return self.manager.submitTask( + return self.manager.submit_task( _tasks.ServerUpdate( server=server['id'], **kwargs)) @@ -4861,7 +4865,7 @@ def create_server_group(self, name, policies): with _utils.shade_exceptions( "Unable to create server group {name}".format( name=name)): - return self.manager.submitTask(_tasks.ServerGroupCreate( + return self.manager.submit_task(_tasks.ServerGroupCreate( name=name, policies=policies)) def delete_server_group(self, name_or_id): @@ -4881,14 +4885,14 @@ def delete_server_group(self, name_or_id): with _utils.shade_exceptions( "Error deleting server group {name}".format(name=name_or_id)): - self.manager.submitTask( + self.manager.submit_task( _tasks.ServerGroupDelete(id=server_group['id'])) return True def list_containers(self, full_listing=True): try: - return self.manager.submitTask(_tasks.ContainerList( + return self.manager.submit_task(_tasks.ContainerList( full_listing=full_listing)) except swift_exceptions.ClientException as e: raise OpenStackCloudException( @@ -4898,7 +4902,7 @@ def list_containers(self, full_listing=True): def get_container(self, name, skip_cache=False): if skip_cache or name not in self._container_cache: try: - container = self.manager.submitTask( + container = self.manager.submit_task( _tasks.ContainerGet(container=name)) self._container_cache[name] = container except swift_exceptions.ClientException as e: @@ -4914,7 +4918,7 @@ def create_container(self, name, public=False): if container: return container try: - self.manager.submitTask( + self.manager.submit_task( _tasks.ContainerCreate(container=name)) if public: self.set_container_access(name, 'public') @@ -4926,7 +4930,7 @@ def create_container(self, name, public=False): def delete_container(self, name): try: - self.manager.submitTask( + self.manager.submit_task( _tasks.ContainerDelete(container=name)) except swift_exceptions.ClientException as e: if e.http_status == 404: @@ -4937,7 +4941,7 @@ def delete_container(self, name): def update_container(self, name, headers): try: - self.manager.submitTask( + self.manager.submit_task( _tasks.ContainerUpdate(container=name, headers=headers)) except swift_exceptions.ClientException as e: raise OpenStackCloudException( @@ -4986,7 +4990,7 @@ def _get_file_hashes(self, filename): @_utils.cache_on_arguments() def get_object_capabilities(self): - return self.manager.submitTask(_tasks.ObjectCapabilities()) + return self.manager.submit_task(_tasks.ObjectCapabilities()) def get_object_segment_size(self, segment_size): '''get a segment size that will work given capabilities''' @@ -5100,7 +5104,7 @@ def create_object( filename=filename, container=container, name=name)) upload = swiftclient.service.SwiftUploadObject( source=filename, object_name=name) - for r in self.manager.submitTask(_tasks.ObjectCreate( + for r in self.manager.submit_task(_tasks.ObjectCreate( container=container, objects=[upload], options=dict( header=header_list, @@ -5133,7 +5137,7 @@ def update_object(self, container, name, metadata=None, **headers): headers = dict(headers, **metadata_headers) try: - return self.manager.submitTask( + return self.manager.submit_task( _tasks.ObjectUpdate(container=container, obj=name, headers=headers)) except swift_exceptions.ClientException as e: @@ -5143,7 +5147,7 @@ def update_object(self, container, name, metadata=None, **headers): def list_objects(self, container, full_listing=True): try: - return self.manager.submitTask(_tasks.ObjectList( + return self.manager.submit_task(_tasks.ObjectList( container=container, full_listing=full_listing)) except swift_exceptions.ClientException as e: raise OpenStackCloudException( @@ -5163,7 +5167,7 @@ def delete_object(self, container, name): if not self.get_object_metadata(container, name): return False try: - self.manager.submitTask(_tasks.ObjectDelete( + self.manager.submit_task(_tasks.ObjectDelete( container=container, obj=name)) except swift_exceptions.ClientException as e: raise OpenStackCloudException( @@ -5173,7 +5177,7 @@ def delete_object(self, container, name): def get_object_metadata(self, container, name): try: - return self.manager.submitTask(_tasks.ObjectMetadata( + return self.manager.submit_task(_tasks.ObjectMetadata( container=container, obj=name)) except swift_exceptions.ClientException as e: if e.http_status == 404: @@ -5197,7 +5201,7 @@ def get_object(self, container, obj, query_string=None, :raises: OpenStackCloudException on operation error. """ try: - return self.manager.submitTask(_tasks.ObjectGet( + return self.manager.submit_task(_tasks.ObjectGet( container=container, obj=obj, query_string=query_string, resp_chunk_size=resp_chunk_size)) except swift_exceptions.ClientException as e: @@ -5320,7 +5324,7 @@ def create_subnet(self, network_name_or_id, cidr, ip_version=4, with _utils.neutron_exceptions( "Error creating subnet on network " "{0}".format(network_name_or_id)): - new_subnet = self.manager.submitTask( + new_subnet = self.manager.submit_task( _tasks.SubnetCreate(body=dict(subnet=subnet))) return new_subnet['subnet'] @@ -5345,7 +5349,7 @@ def delete_subnet(self, name_or_id): with _utils.neutron_exceptions( "Error deleting subnet {0}".format(name_or_id)): - self.manager.submitTask( + self.manager.submit_task( _tasks.SubnetDelete(subnet=subnet['id'])) return True @@ -5433,7 +5437,7 @@ def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, with _utils.neutron_exceptions( "Error updating subnet {0}".format(name_or_id)): - new_subnet = self.manager.submitTask( + new_subnet = self.manager.submit_task( _tasks.SubnetUpdate( subnet=curr_subnet['id'], body=dict(subnet=subnet))) return new_subnet['subnet'] @@ -5499,7 +5503,7 @@ def create_port(self, network_id, **kwargs): with _utils.neutron_exceptions( "Error creating port for network {0}".format(network_id)): - return self.manager.submitTask( + return self.manager.submit_task( _tasks.PortCreate(body={'port': kwargs}))['port'] @_utils.valid_kwargs('name', 'admin_state_up', 'fixed_ips', @@ -5561,7 +5565,7 @@ def update_port(self, name_or_id, **kwargs): with _utils.neutron_exceptions( "Error updating port {0}".format(name_or_id)): - return self.manager.submitTask( + return self.manager.submit_task( _tasks.PortUpdate( port=port['id'], body={'port': kwargs}))['port'] @@ -5581,7 +5585,7 @@ def delete_port(self, name_or_id): with _utils.neutron_exceptions( "Error deleting port {0}".format(name_or_id)): - self.manager.submitTask(_tasks.PortDelete(port=port['id'])) + self.manager.submit_task(_tasks.PortDelete(port=port['id'])) return True def create_security_group(self, name, description): @@ -5606,7 +5610,7 @@ def create_security_group(self, name, description): if self._use_neutron_secgroups(): with _utils.neutron_exceptions( "Error creating security group {0}".format(name)): - group = self.manager.submitTask( + group = self.manager.submit_task( _tasks.NeutronSecurityGroupCreate( body=dict(security_group=dict(name=name, description=description)) @@ -5618,7 +5622,7 @@ def create_security_group(self, name, description): with _utils.shade_exceptions( "Failed to create security group '{name}'".format( name=name)): - group = self.manager.submitTask( + group = self.manager.submit_task( _tasks.NovaSecurityGroupCreate( name=name, description=description ) @@ -5651,7 +5655,7 @@ def delete_security_group(self, name_or_id): if self._use_neutron_secgroups(): with _utils.neutron_exceptions( "Error deleting security group {0}".format(name_or_id)): - self.manager.submitTask( + self.manager.submit_task( _tasks.NeutronSecurityGroupDelete( security_group=secgroup['id'] ) @@ -5662,7 +5666,7 @@ def delete_security_group(self, name_or_id): with _utils.shade_exceptions( "Failed to delete security group '{group}'".format( group=name_or_id)): - self.manager.submitTask( + self.manager.submit_task( _tasks.NovaSecurityGroupDelete(group=secgroup['id']) ) return True @@ -5694,7 +5698,7 @@ def update_security_group(self, name_or_id, **kwargs): if self._use_neutron_secgroups(): with _utils.neutron_exceptions( "Error updating security group {0}".format(name_or_id)): - group = self.manager.submitTask( + group = self.manager.submit_task( _tasks.NeutronSecurityGroupUpdate( security_group=secgroup['id'], body={'security_group': kwargs}) @@ -5705,7 +5709,7 @@ def update_security_group(self, name_or_id, **kwargs): with _utils.shade_exceptions( "Failed to update security group '{group}'".format( group=name_or_id)): - group = self.manager.submitTask( + group = self.manager.submit_task( _tasks.NovaSecurityGroupUpdate( group=secgroup['id'], **kwargs) ) @@ -5791,7 +5795,7 @@ def create_security_group_rule(self, with _utils.neutron_exceptions( "Error creating security group rule"): - rule = self.manager.submitTask( + rule = self.manager.submit_task( _tasks.NeutronSecurityGroupRuleCreate( body={'security_group_rule': rule_def}) ) @@ -5828,7 +5832,7 @@ def create_security_group_rule(self, with _utils.shade_exceptions( "Failed to create security group rule"): - rule = self.manager.submitTask( + rule = self.manager.submit_task( _tasks.NovaSecurityGroupRuleCreate( parent_group_id=secgroup['id'], ip_protocol=protocol, @@ -5862,7 +5866,7 @@ def delete_security_group_rule(self, rule_id): with _utils.neutron_exceptions( "Error deleting security group rule " "{0}".format(rule_id)): - self.manager.submitTask( + self.manager.submit_task( _tasks.NeutronSecurityGroupRuleDelete( security_group_rule=rule_id) ) @@ -5872,7 +5876,7 @@ def delete_security_group_rule(self, rule_id): else: try: - self.manager.submitTask( + self.manager.submit_task( _tasks.NovaSecurityGroupRuleDelete(rule=rule_id) ) except nova_exceptions.NotFound: @@ -5892,7 +5896,7 @@ def list_zones(self): """ with _utils.shade_exceptions("Error fetching zones list"): - return self.manager.submitTask(_tasks.ZoneList()) + return self.manager.submit_task(_tasks.ZoneList()) def get_zone(self, name_or_id, filters=None): """Get a zone by name or ID. @@ -5940,7 +5944,7 @@ def create_zone(self, name, zone_type=None, email=None, description=None, with _utils.shade_exceptions("Unable to create zone {name}".format( name=name)): - return self.manager.submitTask(_tasks.ZoneCreate( + return self.manager.submit_task(_tasks.ZoneCreate( name=name, type_=zone_type, email=email, description=description, ttl=ttl, masters=masters)) @@ -5967,7 +5971,7 @@ def update_zone(self, name_or_id, **kwargs): with _utils.shade_exceptions( "Error updating zone {0}".format(name_or_id)): - new_zone = self.manager.submitTask( + new_zone = self.manager.submit_task( _tasks.ZoneUpdate( zone=zone['id'], values=kwargs)) @@ -5990,7 +5994,7 @@ def delete_zone(self, name_or_id): with _utils.shade_exceptions( "Error deleting zone {0}".format(name_or_id)): - self.manager.submitTask( + self.manager.submit_task( _tasks.ZoneDelete(zone=zone['id'])) return True @@ -6004,7 +6008,7 @@ def list_recordsets(self, zone): """ with _utils.shade_exceptions("Error fetching recordsets list"): - return self.manager.submitTask(_tasks.RecordSetList(zone=zone)) + return self.manager.submit_task(_tasks.RecordSetList(zone=zone)) def get_recordset(self, zone, name_or_id): """Get a recordset by name or ID. @@ -6017,7 +6021,7 @@ def get_recordset(self, zone, name_or_id): """ try: - return self.manager.submitTask(_tasks.RecordSetGet( + return self.manager.submit_task(_tasks.RecordSetGet( zone=zone, recordset=name_or_id)) except: @@ -6052,7 +6056,7 @@ def create_recordset(self, zone, name, recordset_type, records, with _utils.shade_exceptions( "Unable to create recordset {name}".format(name=name)): - return self.manager.submitTask(_tasks.RecordSetCreate( + return self.manager.submit_task(_tasks.RecordSetCreate( zone=zone, name=name, type_=recordset_type, records=records, description=description, ttl=ttl)) @@ -6082,7 +6086,7 @@ def update_recordset(self, zone, name_or_id, **kwargs): with _utils.shade_exceptions( "Error updating recordset {0}".format(name_or_id)): - new_recordset = self.manager.submitTask( + new_recordset = self.manager.submit_task( _tasks.RecordSetUpdate( zone=zone, recordset=name_or_id, values=kwargs)) @@ -6111,7 +6115,7 @@ def delete_recordset(self, zone, name_or_id): with _utils.shade_exceptions( "Error deleting recordset {0}".format(name_or_id)): - self.manager.submitTask( + self.manager.submit_task( _tasks.RecordSetDelete(zone=zone['id'], recordset=name_or_id)) return True @@ -6131,7 +6135,7 @@ def list_cluster_templates(self, detail=False): the openstack API call. """ with _utils.shade_exceptions("Error fetching ClusterTemplate list"): - cluster_templates = self.manager.submitTask( + cluster_templates = self.manager.submit_task( _tasks.ClusterTemplateList(detail=detail)) return _utils.normalize_cluster_templates(cluster_templates) list_baymodels = list_cluster_templates @@ -6203,7 +6207,7 @@ def create_cluster_template( "Error creating ClusterTemplate of name" " {cluster_template_name}".format( cluster_template_name=name)): - cluster_template = self.manager.submitTask( + cluster_template = self.manager.submit_task( _tasks.ClusterTemplateCreate( name=name, image_id=image_id, keypair_id=keypair_id, coe=coe, **kwargs)) @@ -6236,7 +6240,7 @@ def delete_cluster_template(self, name_or_id): with _utils.shade_exceptions("Error in deleting ClusterTemplate"): try: - self.manager.submitTask( + self.manager.submit_task( _tasks.ClusterTemplateDelete(id=cluster_template['id'])) except magnum_exceptions.NotFound: self.log.debug( @@ -6282,7 +6286,7 @@ def update_cluster_template(self, name_or_id, operation, **kwargs): with _utils.shade_exceptions( "Error updating ClusterTemplate {0}".format(name_or_id)): - self.manager.submitTask( + self.manager.submit_task( _tasks.ClusterTemplateUpdate( id=cluster_template['id'], patch=patches)) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index df4142e20..98dc926ab 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -58,24 +58,24 @@ def ironic_client(self): def list_nics(self): with _utils.shade_exceptions("Error fetching machine port list"): - return self.manager.submitTask(_tasks.MachinePortList()) + return self.manager.submit_task(_tasks.MachinePortList()) def list_nics_for_machine(self, uuid): with _utils.shade_exceptions( "Error fetching port list for node {node_id}".format( node_id=uuid)): - return self.manager.submitTask( + return self.manager.submit_task( _tasks.MachineNodePortList(node_id=uuid)) def get_nic_by_mac(self, mac): try: - return self.manager.submitTask( + return self.manager.submit_task( _tasks.MachineNodePortGet(port_id=mac)) except ironic_exceptions.ClientException: return None def list_machines(self): - return self.manager.submitTask(_tasks.MachineNodeList()) + return self.manager.submit_task(_tasks.MachineNodeList()) def get_machine(self, name_or_id): """Get Machine by name or uuid @@ -89,7 +89,7 @@ def get_machine(self, name_or_id): nodes are found. """ try: - return self.manager.submitTask( + return self.manager.submit_task( _tasks.MachineNodeGet(node_id=name_or_id)) except ironic_exceptions.ClientException: return None @@ -103,9 +103,9 @@ def get_machine_by_mac(self, mac): if the node is not found. """ try: - port = self.manager.submitTask( + port = self.manager.submit_task( _tasks.MachinePortGetByAddress(address=mac)) - return self.manager.submitTask( + return self.manager.submit_task( _tasks.MachineNodeGet(node_id=port.node_uuid)) except ironic_exceptions.ClientException: return None @@ -221,12 +221,12 @@ def register_machine(self, nics, wait=False, timeout=3600, baremetal node. """ with _utils.shade_exceptions("Error registering machine with Ironic"): - machine = self.manager.submitTask(_tasks.MachineCreate(**kwargs)) + machine = self.manager.submit_task(_tasks.MachineCreate(**kwargs)) created_nics = [] try: for row in nics: - nic = self.manager.submitTask( + nic = self.manager.submit_task( _tasks.MachinePortCreate(address=row['mac'], node_uuid=machine['uuid'])) created_nics.append(nic.uuid) @@ -237,13 +237,13 @@ def register_machine(self, nics, wait=False, timeout=3600, try: for uuid in created_nics: try: - self.manager.submitTask( + self.manager.submit_task( _tasks.MachinePortDelete( port_id=uuid)) except: pass finally: - self.manager.submitTask( + self.manager.submit_task( _tasks.MachineDelete(node_id=machine['uuid'])) raise OpenStackCloudException( "Error registering NICs with the baremetal service: %s" @@ -344,14 +344,14 @@ def unregister_machine(self, nics, uuid, wait=False, timeout=600): with _utils.shade_exceptions( "Error removing NIC {nic} from baremetal API for node " "{uuid}".format(nic=nic, uuid=uuid)): - port = self.manager.submitTask( + port = self.manager.submit_task( _tasks.MachinePortGetByAddress(address=nic['mac'])) - self.manager.submitTask( + self.manager.submit_task( _tasks.MachinePortDelete(port_id=port.uuid)) with _utils.shade_exceptions( "Error unregistering machine {node_id} from the baremetal " "API".format(node_id=uuid)): - self.manager.submitTask( + self.manager.submit_task( _tasks.MachineDelete(node_id=uuid)) if wait: for count in _utils._iterate_timeout( @@ -401,7 +401,7 @@ def patch_machine(self, name_or_id, patch): "Error updating machine via patch operation on node " "{node}".format(node=name_or_id) ): - return self.manager.submitTask( + return self.manager.submit_task( _tasks.MachinePatch(node_id=name_or_id, patch=patch, http_method='PATCH')) @@ -517,7 +517,7 @@ def update_machine(self, name_or_id, chassis_uuid=None, driver=None, def validate_node(self, uuid): with _utils.shade_exceptions(): - ifaces = self.manager.submitTask( + ifaces = self.manager.submit_task( _tasks.MachineNodeValidate(node_uuid=uuid)) if not ifaces.deploy or not ifaces.power: @@ -563,7 +563,7 @@ def node_set_provision_state(self, "Baremetal machine node failed change provision state to " "{state}".format(state=state) ): - machine = self.manager.submitTask( + machine = self.manager.submit_task( _tasks.MachineSetProvision(node_uuid=name_or_id, state=state, configdrive=configdrive)) @@ -615,12 +615,12 @@ def set_machine_maintenance_state( "{node}".format(state=state, node=name_or_id) ): if state: - result = self.manager.submitTask( + result = self.manager.submit_task( _tasks.MachineSetMaintenance(node_id=name_or_id, state='true', maint_reason=reason)) else: - result = self.manager.submitTask( + result = self.manager.submit_task( _tasks.MachineSetMaintenance(node_id=name_or_id, state='false')) if result is not None: @@ -669,7 +669,7 @@ def _set_machine_power_state(self, name_or_id, state): "Error setting machine power state to {state} on node " "{node}".format(state=state, node=name_or_id) ): - power = self.manager.submitTask( + power = self.manager.submit_task( _tasks.MachineSetPower(node_id=name_or_id, state=state)) if power is not None: @@ -737,14 +737,14 @@ def deactivate_node(self, uuid, wait=False, def set_node_instance_info(self, uuid, patch): with _utils.shade_exceptions(): - return self.manager.submitTask( + return self.manager.submit_task( _tasks.MachineNodeUpdate(node_id=uuid, patch=patch)) def purge_node_instance_info(self, uuid): patch = [] patch.append({'op': 'remove', 'path': '/instance_info'}) with _utils.shade_exceptions(): - return self.manager.submitTask( + return self.manager.submit_task( _tasks.MachineNodeUpdate(node_id=uuid, patch=patch)) @_utils.valid_kwargs('type', 'service_type', 'description') @@ -781,7 +781,7 @@ def create_service(self, name, enabled=True, **kwargs): with _utils.shade_exceptions( "Failed to create service {name}".format(name=name) ): - service = self.manager.submitTask( + service = self.manager.submit_task( _tasks.ServiceCreate(name=name, **kwargs) ) @@ -807,7 +807,7 @@ def update_service(self, name_or_id, **kwargs): with _utils.shade_exceptions( "Error in updating service {service}".format(service=name_or_id) ): - service = self.manager.submitTask( + service = self.manager.submit_task( _tasks.ServiceUpdate(service=name_or_id, **kwargs) ) @@ -822,7 +822,7 @@ def list_services(self): openstack API call. """ with _utils.shade_exceptions(): - services = self.manager.submitTask(_tasks.ServiceList()) + services = self.manager.submit_task(_tasks.ServiceList()) return _utils.normalize_keystone_services(services) def search_services(self, name_or_id=None, filters=None): @@ -880,7 +880,7 @@ def delete_service(self, name_or_id): service_kwargs = {'service': service['id']} with _utils.shade_exceptions("Failed to delete service {id}".format( id=service['id'])): - self.manager.submitTask(_tasks.ServiceDelete(**service_kwargs)) + self.manager.submit_task(_tasks.ServiceDelete(**service_kwargs)) return True @@ -974,7 +974,7 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, for args in endpoint_args: # NOTE(SamYaple): Add shared kwargs to endpoint args args.update(kwargs) - endpoint = self.manager.submitTask( + endpoint = self.manager.submit_task( _tasks.EndpointCreate(**args) ) endpoints.append(endpoint) @@ -996,7 +996,7 @@ def update_endpoint(self, endpoint_id, **kwargs): with _utils.shade_exceptions( "Failed to update endpoint {}".format(endpoint_id) ): - return self.manager.submitTask(_tasks.EndpointUpdate( + return self.manager.submit_task(_tasks.EndpointUpdate( endpoint=endpoint_id, **kwargs )) @@ -1013,7 +1013,7 @@ def list_endpoints(self): # large environments is small, we can continue to filter in shade just # like the v2 api. with _utils.shade_exceptions("Failed to list endpoints"): - endpoints = self.manager.submitTask(_tasks.EndpointList()) + endpoints = self.manager.submit_task(_tasks.EndpointList()) return endpoints @@ -1076,7 +1076,7 @@ def delete_endpoint(self, id): endpoint_kwargs = {'endpoint': endpoint['id']} with _utils.shade_exceptions("Failed to delete endpoint {id}".format( id=id)): - self.manager.submitTask(_tasks.EndpointDelete(**endpoint_kwargs)) + self.manager.submit_task(_tasks.EndpointDelete(**endpoint_kwargs)) return True @@ -1094,7 +1094,7 @@ def create_domain( """ with _utils.shade_exceptions("Failed to create domain {name}".format( name=name)): - domain = self.manager.submitTask(_tasks.DomainCreate( + domain = self.manager.submit_task(_tasks.DomainCreate( name=name, description=description, enabled=enabled)) @@ -1117,7 +1117,7 @@ def update_domain( with _utils.shade_exceptions( "Error in updating domain {domain}".format(domain=domain_id)): - domain = self.manager.submitTask(_tasks.DomainUpdate( + domain = self.manager.submit_task(_tasks.DomainUpdate( domain=domain_id, name=name, description=description, enabled=enabled)) return _utils.normalize_domains([domain])[0] @@ -1150,7 +1150,7 @@ def delete_domain(self, domain_id=None, name_or_id=None): # Deleting a domain is expensive, so disabling it first increases # the changes of success domain = self.update_domain(domain_id, enabled=False) - self.manager.submitTask(_tasks.DomainDelete(domain=domain['id'])) + self.manager.submit_task(_tasks.DomainDelete(domain=domain['id'])) return True @@ -1163,7 +1163,7 @@ def list_domains(self): the openstack API call. """ with _utils.shade_exceptions("Failed to list domains"): - domains = self.manager.submitTask(_tasks.DomainList()) + domains = self.manager.submit_task(_tasks.DomainList()) return _utils.normalize_domains(domains) def search_domains(self, filters=None, name_or_id=None): @@ -1189,7 +1189,7 @@ def search_domains(self, filters=None, name_or_id=None): return _utils._filter_list(domains, name_or_id, filters) else: with _utils.shade_exceptions("Failed to list domains"): - domains = self.manager.submitTask( + domains = self.manager.submit_task( _tasks.DomainList(**filters)) return _utils.normalize_domains(domains) @@ -1218,7 +1218,7 @@ def get_domain(self, domain_id=None, name_or_id=None, filters=None): "Failed to get domain " "{domain_id}".format(domain_id=domain_id) ): - domain = self.manager.submitTask( + domain = self.manager.submit_task( _tasks.DomainGet(domain=domain_id)) return _utils.normalize_domains([domain])[0] @@ -1232,7 +1232,7 @@ def list_groups(self): the openstack API call. """ with _utils.shade_exceptions("Failed to list groups"): - groups = self.manager.submitTask(_tasks.GroupList()) + groups = self.manager.submit_task(_tasks.GroupList()) return _utils.normalize_groups(groups) def search_groups(self, name_or_id=None, filters=None): @@ -1287,7 +1287,7 @@ def create_group(self, name, description, domain=None): ) domain_id = dom['id'] - group = self.manager.submitTask(_tasks.GroupCreate( + group = self.manager.submit_task(_tasks.GroupCreate( name=name, description=description, domain=domain_id) ) self.list_groups.invalidate(self) @@ -1314,7 +1314,7 @@ def update_group(self, name_or_id, name=None, description=None): with _utils.shade_exceptions( "Unable to update group {name}".format(name=name_or_id) ): - group = self.manager.submitTask(_tasks.GroupUpdate( + group = self.manager.submit_task(_tasks.GroupUpdate( group=group['id'], name=name, description=description)) self.list_groups.invalidate(self) @@ -1339,7 +1339,7 @@ def delete_group(self, name_or_id): with _utils.shade_exceptions( "Unable to delete group {name}".format(name=name_or_id) ): - self.manager.submitTask(_tasks.GroupDelete(group=group['id'])) + self.manager.submit_task(_tasks.GroupDelete(group=group['id'])) self.list_groups.invalidate(self) return True @@ -1353,7 +1353,7 @@ def list_roles(self): the openstack API call. """ with _utils.shade_exceptions(): - roles = self.manager.submitTask(_tasks.RoleList()) + roles = self.manager.submit_task(_tasks.RoleList()) return roles @@ -1397,7 +1397,7 @@ def get_role(self, name_or_id, filters=None): def _keystone_v2_role_assignments(self, user, project=None, role=None, **kwargs): with _utils.shade_exceptions("Failed to list role assignments"): - roles = self.manager.submitTask( + roles = self.manager.submit_task( _tasks.RolesForUser(user=user, tenant=project) ) ret = [] @@ -1461,7 +1461,7 @@ def list_role_assignments(self, filters=None): assignments = self._keystone_v2_role_assignments(**filters) else: with _utils.shade_exceptions("Failed to list role assignments"): - assignments = self.manager.submitTask( + assignments = self.manager.submit_task( _tasks.RoleAssignmentList(**filters) ) return _utils.normalize_role_assignments(assignments) @@ -1486,7 +1486,7 @@ def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", """ with _utils.shade_exceptions("Failed to create flavor {name}".format( name=name)): - flavor = self.manager.submitTask( + flavor = self.manager.submit_task( _tasks.FlavorCreate(name=name, ram=ram, vcpus=vcpus, disk=disk, flavorid=flavorid, ephemeral=ephemeral, swap=swap, rxtx_factor=rxtx_factor, @@ -1512,7 +1512,7 @@ def delete_flavor(self, name_or_id): with _utils.shade_exceptions("Unable to delete flavor {name}".format( name=name_or_id)): - self.manager.submitTask(_tasks.FlavorDelete(flavor=flavor['id'])) + self.manager.submit_task(_tasks.FlavorDelete(flavor=flavor['id'])) return True @@ -1526,7 +1526,7 @@ def set_flavor_specs(self, flavor_id, extra_specs): :raises: OpenStackCloudResourceNotFound if flavor ID is not found. """ try: - self.manager.submitTask( + self.manager.submit_task( _tasks.FlavorSetExtraSpecs( id=flavor_id, json=dict(extra_specs=extra_specs))) except Exception as e: @@ -1545,7 +1545,7 @@ def unset_flavor_specs(self, flavor_id, keys): """ for key in keys: try: - self.manager.submitTask( + self.manager.submit_task( _tasks.FlavorUnsetExtraSpecs(id=flavor_id, key=key)) except Exception as e: raise OpenStackCloudException( @@ -1559,12 +1559,12 @@ def _mod_flavor_access(self, action, flavor_id, project_id): "flavor ID {flavor}".format( action=action, flavor=flavor_id)): if action == 'add': - self.manager.submitTask( + self.manager.submit_task( _tasks.FlavorAddAccess(flavor=flavor_id, tenant=project_id) ) elif action == 'remove': - self.manager.submitTask( + self.manager.submit_task( _tasks.FlavorRemoveAccess(flavor=flavor_id, tenant=project_id) ) @@ -1599,7 +1599,7 @@ def create_role(self, name): :raise OpenStackCloudException: if the role cannot be created """ with _utils.shade_exceptions(): - role = self.manager.submitTask( + role = self.manager.submit_task( _tasks.RoleCreate(name=name) ) return role @@ -1622,7 +1622,7 @@ def delete_role(self, name_or_id): with _utils.shade_exceptions("Unable to delete role {name}".format( name=name_or_id)): - self.manager.submitTask(_tasks.RoleDelete(role=role['id'])) + self.manager.submit_task(_tasks.RoleDelete(role=role['id'])) return True @@ -1703,12 +1703,12 @@ def grant_role(self, name_or_id, user=None, group=None, data)): if self.cloud_config.get_api_version('identity').startswith('2'): data['tenant'] = data.pop('project') - self.manager.submitTask(_tasks.RoleAddUser(**data)) + self.manager.submit_task(_tasks.RoleAddUser(**data)) else: if data.get('project') is None and data.get('domain') is None: raise OpenStackCloudException( 'Must specify either a domain or project') - self.manager.submitTask(_tasks.RoleGrantUser(**data)) + self.manager.submit_task(_tasks.RoleGrantUser(**data)) if wait: for count in _utils._iterate_timeout( timeout, @@ -1766,13 +1766,13 @@ def revoke_role(self, name_or_id, user=None, group=None, data)): if self.cloud_config.get_api_version('identity').startswith('2'): data['tenant'] = data.pop('project') - self.manager.submitTask(_tasks.RoleRemoveUser(**data)) + self.manager.submit_task(_tasks.RoleRemoveUser(**data)) else: if data.get('project') is None \ and data.get('domain') is None: raise OpenStackCloudException( 'Must specify either a domain or project') - self.manager.submitTask(_tasks.RoleRevokeUser(**data)) + self.manager.submit_task(_tasks.RoleRevokeUser(**data)) if wait: for count in _utils._iterate_timeout( timeout, @@ -1788,7 +1788,7 @@ def list_hypervisors(self): """ with _utils.shade_exceptions("Error fetching hypervisor list"): - return self.manager.submitTask(_tasks.HypervisorList()) + return self.manager.submit_task(_tasks.HypervisorList()) def search_aggregates(self, name_or_id=None, filters=None): """Seach host aggregates. @@ -1811,7 +1811,7 @@ def list_aggregates(self): """ with _utils.shade_exceptions("Error fetching aggregate list"): - return self.manager.submitTask(_tasks.AggregateList()) + return self.manager.submit_task(_tasks.AggregateList()) def get_aggregate(self, name_or_id, filters=None): """Get an aggregate by name or ID. @@ -1847,7 +1847,7 @@ def create_aggregate(self, name, availability_zone=None): with _utils.shade_exceptions( "Unable to create host aggregate {name}".format( name=name)): - return self.manager.submitTask(_tasks.AggregateCreate( + return self.manager.submit_task(_tasks.AggregateCreate( name=name, availability_zone=availability_zone)) @_utils.valid_kwargs('name', 'availability_zone') @@ -1869,7 +1869,7 @@ def update_aggregate(self, name_or_id, **kwargs): with _utils.shade_exceptions( "Error updating aggregate {name}".format(name=name_or_id)): - new_aggregate = self.manager.submitTask( + new_aggregate = self.manager.submit_task( _tasks.AggregateUpdate( aggregate=aggregate['id'], values=kwargs)) @@ -1891,7 +1891,7 @@ def delete_aggregate(self, name_or_id): with _utils.shade_exceptions( "Error deleting aggregate {name}".format(name=name_or_id)): - self.manager.submitTask( + self.manager.submit_task( _tasks.AggregateDelete(aggregate=aggregate['id'])) return True @@ -1915,7 +1915,7 @@ def set_aggregate_metadata(self, name_or_id, metadata): with _utils.shade_exceptions( "Unable to set metadata for host aggregate {name}".format( name=name_or_id)): - return self.manager.submitTask(_tasks.AggregateSetMetadata( + return self.manager.submit_task(_tasks.AggregateSetMetadata( aggregate=aggregate['id'], metadata=metadata)) def add_host_to_aggregate(self, name_or_id, host_name): @@ -1934,7 +1934,7 @@ def add_host_to_aggregate(self, name_or_id, host_name): with _utils.shade_exceptions( "Unable to add host {host} to aggregate {name}".format( name=name_or_id, host=host_name)): - return self.manager.submitTask(_tasks.AggregateAddHost( + return self.manager.submit_task(_tasks.AggregateAddHost( aggregate=aggregate['id'], host=host_name)) def remove_host_from_aggregate(self, name_or_id, host_name): @@ -1953,7 +1953,7 @@ def remove_host_from_aggregate(self, name_or_id, host_name): with _utils.shade_exceptions( "Unable to remove host {host} from aggregate {name}".format( name=name_or_id, host=host_name)): - return self.manager.submitTask(_tasks.AggregateRemoveHost( + return self.manager.submit_task(_tasks.AggregateRemoveHost( aggregate=aggregate['id'], host=host_name)) def set_compute_quotas(self, name_or_id, **kwargs): @@ -1979,7 +1979,7 @@ def set_compute_quotas(self, name_or_id, **kwargs): # if key in quota.VOLUME_QUOTAS} try: - self.manager.submitTask( + self.manager.submit_task( _tasks.NovaQuotasSet(tenant_id=proj.id, force=True, **kwargs)) @@ -1998,7 +1998,7 @@ def get_compute_quotas(self, name_or_id): if not proj: raise OpenStackCloudException("project does not exist") try: - return self.manager.submitTask( + return self.manager.submit_task( _tasks.NovaQuotasGet(tenant_id=proj.id)) except nova_exceptions.BadRequest: raise OpenStackCloudException("nova client call failed") @@ -2016,7 +2016,7 @@ def delete_compute_quotas(self, name_or_id): if not proj: raise OpenStackCloudException("project does not exist") try: - return self.manager.submitTask( + return self.manager.submit_task( _tasks.NovaQuotasDelete(tenant_id=proj.id)) except novaclient.exceptions.BadRequest: raise OpenStackCloudException("nova client call failed") @@ -2036,7 +2036,7 @@ def set_volume_quotas(self, name_or_id, **kwargs): raise OpenStackCloudException("project does not exist") try: - self.manager.submitTask( + self.manager.submit_task( _tasks.CinderQuotasSet(tenant_id=proj.id, **kwargs)) except cinder_exceptions.BadRequest: @@ -2054,7 +2054,7 @@ def get_volume_quotas(self, name_or_id): if not proj: raise OpenStackCloudException("project does not exist") try: - return self.manager.submitTask( + return self.manager.submit_task( _tasks.CinderQuotasGet(tenant_id=proj.id)) except cinder_exceptions.BadRequest: raise OpenStackCloudException("cinder client call failed") @@ -2072,7 +2072,7 @@ def delete_volume_quotas(self, name_or_id): if not proj: raise OpenStackCloudException("project does not exist") try: - return self.manager.submitTask( + return self.manager.submit_task( _tasks.CinderQuotasDelete(tenant_id=proj.id)) except cinder_exceptions.BadRequest: raise OpenStackCloudException("cinder client call failed") @@ -2093,7 +2093,7 @@ def set_network_quotas(self, name_or_id, **kwargs): body = {'quota': kwargs} with _utils.neutron_exceptions("network client call failed"): - self.manager.submitTask( + self.manager.submit_task( _tasks.NeutronQuotasSet(tenant_id=proj.id, body=body)) @@ -2109,7 +2109,7 @@ def get_network_quotas(self, name_or_id): if not proj: raise OpenStackCloudException("project does not exist") with _utils.neutron_exceptions("network client call failed"): - return self.manager.submitTask( + return self.manager.submit_task( _tasks.NeutronQuotasGet(tenant_id=proj.id)) def delete_network_quotas(self, name_or_id): @@ -2125,7 +2125,7 @@ def delete_network_quotas(self, name_or_id): if not proj: raise OpenStackCloudException("project does not exist") with _utils.neutron_exceptions("network client call failed"): - return self.manager.submitTask( + return self.manager.submit_task( _tasks.NeutronQuotasDelete(tenant_id=proj.id)) def list_magnum_services(self): @@ -2135,5 +2135,5 @@ def list_magnum_services(self): :raises: OpenStackCloudException on operation error. """ with _utils.shade_exceptions("Error fetching Magnum services list"): - return self.manager.submitTask( + return self.manager.submit_task( _tasks.MagnumServicesList()) diff --git a/shade/task_manager.py b/shade/task_manager.py index 3c675e1ac..8dd5c7154 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -225,7 +225,7 @@ def run(self): """ This is a direct action passthrough TaskManager """ pass - def submitTask(self, task, raw=False): + def submit_task(self, task, raw=False): """Submit and execute the given task. :param task: The task to execute. @@ -241,6 +241,8 @@ def submitTask(self, task, raw=False): "Manager %s ran task %s in %ss" % ( self.name, task.name, (end - start))) return task.wait(raw) + # Backwards compatibility + submitTask = submit_task def submit_function( self, method, name=None, result_filter_cb=None, **kwargs): @@ -254,4 +256,4 @@ def submit_function( task_class = generate_task_class(method, name, result_filter_cb) - return self.manager.submitTask(task_class(**kwargs)) + return self.manager.submit_task(task_class(**kwargs)) diff --git a/shade/tests/unit/test_task_manager.py b/shade/tests/unit/test_task_manager.py index 9918eb2c9..bedd115a8 100644 --- a/shade/tests/unit/test_task_manager.py +++ b/shade/tests/unit/test_task_manager.py @@ -69,24 +69,24 @@ def test_wait_re_raise(self): Specifically, we test if we get the same behaviour with all the configured interpreters (e.g. py27, p34, pypy, ...) """ - self.assertRaises(TestException, self.manager.submitTask, TestTask()) + self.assertRaises(TestException, self.manager.submit_task, TestTask()) def test_dont_munchify_int(self): - ret = self.manager.submitTask(TestTaskInt()) + ret = self.manager.submit_task(TestTaskInt()) self.assertIsInstance(ret, int) def test_dont_munchify_float(self): - ret = self.manager.submitTask(TestTaskFloat()) + ret = self.manager.submit_task(TestTaskFloat()) self.assertIsInstance(ret, float) def test_dont_munchify_str(self): - ret = self.manager.submitTask(TestTaskStr()) + ret = self.manager.submit_task(TestTaskStr()) self.assertIsInstance(ret, str) def test_dont_munchify_bool(self): - ret = self.manager.submitTask(TestTaskBool()) + ret = self.manager.submit_task(TestTaskBool()) self.assertIsInstance(ret, bool) def test_dont_munchify_set(self): - ret = self.manager.submitTask(TestTaskSet()) + ret = self.manager.submit_task(TestTaskSet()) self.assertIsInstance(ret, set) From 42885dc94176eecca1619d7e9c33b5ab3d9a2c0a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 29 Aug 2016 17:49:46 -0500 Subject: [PATCH 1022/3836] Go ahead and handle YAML list in region_name The right thing is to use "regions" as a yaml list. However, it's even more right to be friendly to people - we emit a warning about region_name as a list anyway. Change-Id: Ia0b27ef8d1d52c655c2736a97bd5e59a4a2fe9d8 --- os_client_config/config.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 54048aad1..6eed0f0cd 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -388,7 +388,10 @@ def _get_known_regions(self, cloud): if 'regions' in config: return self._expand_regions(config['regions']) elif 'region_name' in config: - regions = config['region_name'].split(',') + if isinstance(config['region_name'], list): + regions = config['region_name'] + else: + regions = config['region_name'].split(',') if len(regions) > 1: warnings.warn( "Comma separated lists in region_name are deprecated." From 87f90e6ffbc754decb2ce0138b12551089c5b5e1 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 29 Aug 2016 18:39:52 -0500 Subject: [PATCH 1023/3836] Fix two minor bugs in generate_task_class The superclass name was wrong, and then we didn't actually return from it. Change-Id: Id5a45b80741555bf2024b01c6edb771bd7e9b841 --- shade/task_manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shade/task_manager.py b/shade/task_manager.py index 8dd5c7154..97a80dc24 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -191,7 +191,7 @@ def __init__(self, **kw): self._method = method def wait(self, raw=False): - super(RequestTask, self).wait() + super(RunTask, self).wait() if raw: # Do NOT convert the result. @@ -204,6 +204,7 @@ def main(self, client): else: meth = getattr(client, self._method) return meth(**self.args) + return RunTask class TaskManager(object): From 3f226d151181fd476ee719cf2bc523f7c1fd7420 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 30 Aug 2016 09:03:49 -0500 Subject: [PATCH 1024/3836] Revert per-resource dogpile.cache work There is something extra strange going on when nodepool is running where things return None when they shouldn't. Revert this work until we have a chance to dig into the guts and figure it out. Revert "Move list_server cache to dogpile" This reverts commit 315a11cbbb9b4e9ca4ba9c9e263a4be8d70f49ef. Revert "Ensure per-resource caches work without global cache" This reverts commit dcce1234d4c0c8d5b1ec5a0f2ea7c6629a862acc. Revert "Move list_ports to using dogpile.cache" This reverts commit 02dd6d7266ff23073a5bb3dc144a4c1ec074022b. Revert "Batch calls to list_floating_ips" This reverts commit 376c49777f77dac677f6c16389b139f3bf57473e. Change-Id: I2ae46e1b057c39777e56a9402a7b1bda8632eb12 --- shade/_utils.py | 8 -- shade/openstackcloud.py | 126 +++++++++++-------- shade/tests/unit/test_create_server.py | 1 + shade/tests/unit/test_floating_ip_neutron.py | 41 +++--- shade/tests/unit/test_meta.py | 8 +- 5 files changed, 100 insertions(+), 84 deletions(-) diff --git a/shade/_utils.py b/shade/_utils.py index 5922d7976..5e87615cf 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -531,14 +531,6 @@ def func_wrapper(func, *args, **kwargs): return func_wrapper -def async_creation_runner(cache, somekey, creator, mutex): - try: - value = creator() - cache.set(somekey, value) - finally: - mutex.release() - - def cache_on_arguments(*cache_on_args, **cache_on_kwargs): _cache_name = cache_on_kwargs.pop('resource', None) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 30c643b0b..eafdd7979 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -62,6 +62,8 @@ DEFAULT_OBJECT_SEGMENT_SIZE = 1073741824 # 1GB # This halves the current default for Swift DEFAULT_MAX_FILE_SIZE = (5 * 1024 * 1024 * 1024 + 2) / 2 +DEFAULT_SERVER_AGE = 5 +DEFAULT_PORT_AGE = 5 OBJECT_CONTAINER_ACLS = { @@ -199,6 +201,10 @@ def __init__( self._servers_time = 0 self._servers_lock = threading.Lock() + self._ports = [] + self._ports_time = 0 + self._ports_lock = threading.Lock() + self._networks_lock = threading.Lock() self._reset_network_caches() @@ -208,37 +214,20 @@ def __init__( self._resource_caches = {} - expirations = cloud_config.get_cache_expiration() if cache_class != 'dogpile.cache.null': self.cache_enabled = True self._cache = self._make_cache( cache_class, cache_expiration_time, cache_arguments) - for expire_key in expirations.keys(): - # Only build caches for things we have list operations for - if getattr(self, 'list_{0}'.format(expire_key), None): - self._resource_caches[expire_key] = self._make_cache( - cache_class, expirations[expire_key], cache_arguments, - async_creation_runner=_utils.async_creation_runner) - - elif expirations: - # We have per-resource expirations configured, but not general - # caching. This is a potential common case for things like - # nodepool. In this case, we want dogpile.cache.null for the - # general cache but dogpile.cache.memory for per-resource. We - # can't remove the decorators in this config, but that's just - # the price of playing ball - cache_class = 'dogpile.cache.memory' - self.cache_enabled = False - self._cache = self._make_cache( - 'dogpile.cache.null', cache_expiration_time, cache_arguments) + expirations = cloud_config.get_cache_expiration() for expire_key in expirations.keys(): # Only build caches for things we have list operations for if getattr( self, 'list_{0}'.format(expire_key), None): self._resource_caches[expire_key] = self._make_cache( - cache_class, expirations[expire_key], cache_arguments, - async_creation_runner=_utils.async_creation_runner) + cache_class, expirations[expire_key], cache_arguments) + self._SERVER_AGE = DEFAULT_SERVER_AGE + self._PORT_AGE = DEFAULT_PORT_AGE else: self.cache_enabled = False @@ -249,6 +238,11 @@ class _FakeCache(object): def invalidate(self): pass + # Don't cache list_servers if we're not caching things. + # Replace this with a more specific cache configuration + # soon. + self._SERVER_AGE = 0 + self._PORT_AGE = 0 self._cache = _FakeCache() # Undecorate cache decorated methods. Otherwise the call stacks # wind up being stupidly long and hard to debug @@ -262,6 +256,13 @@ def invalidate(self): new_func.invalidate = _fake_invalidate setattr(self, method, new_func) + # If server expiration time is set explicitly, use that. Otherwise + # fall back to whatever it was before + self._SERVER_AGE = cloud_config.get_cache_resource_expiration( + 'server', self._SERVER_AGE) + self._PORT_AGE = cloud_config.get_cache_resource_expiration( + 'port', self._PORT_AGE) + self._container_cache = dict() self._file_hash_cache = dict() @@ -292,12 +293,9 @@ def invalidate(self): self.cloud_config = cloud_config - def _make_cache( - self, cache_class, expiration_time, arguments, - async_creation_runner=None): + def _make_cache(self, cache_class, expiration_time, arguments): return dogpile.cache.make_region( - function_key_generator=self._make_cache_key, - async_creation_runner=async_creation_runner + function_key_generator=self._make_cache_key ).configure( cache_class, expiration_time=expiration_time, @@ -311,8 +309,8 @@ def _make_cache_key(self, namespace, fn): name_key = '%s:%s' % (self.name, namespace) def generate_key(*args, **kwargs): - arg_key = ','.join([str(arg) for arg in args]) if args else '' - kw_keys = sorted(kwargs.keys()) if kwargs else [] + arg_key = ','.join(args) + kw_keys = sorted(kwargs.keys()) kwargs_key = ','.join( ['%s:%s' % (k, kwargs[k]) for k in kw_keys if k != 'cache']) ans = "_".join( @@ -320,10 +318,6 @@ def generate_key(*args, **kwargs): return ans return generate_key - def _get_cache_time(self, resource_name): - return self.cloud_config.get_cache_resource_expiration( - resource_name, None) - def _get_cache(self, resource_name): if resource_name and resource_name in self._resource_caches: return self._resource_caches[resource_name] @@ -1237,7 +1231,7 @@ def search_ports(self, name_or_id=None, filters=None): # If port caching is enabled, do not push the filter down to # neutron; get all the ports (potentially from the cache) and # filter locally. - if self._get_cache_time('port'): + if self._PORT_AGE: pushdown_filters = None else: pushdown_filters = filters @@ -1364,7 +1358,6 @@ def list_subnets(self, filters=None): return self.manager.submit_task( _tasks.SubnetList(**filters))['subnets'] - @_utils.cache_on_arguments(resource='ports') def list_ports(self, filters=None): """List all available ports. @@ -1372,9 +1365,27 @@ def list_ports(self, filters=None): :returns: A list of port ``munch.Munch``. """ + # If pushdown filters are specified, bypass local caching. + if filters: + return self._list_ports(filters) # Translate None from search interface to empty {} for kwargs below - if not filters: - filters = {} + filters = {} + if (time.time() - self._ports_time) >= self._PORT_AGE: + # Since we're using cached data anyway, we don't need to + # have more than one thread actually submit the list + # ports task. Let the first one submit it while holding + # a lock, and the non-blocking acquire method will cause + # subsequent threads to just skip this and use the old + # data until it succeeds. + if self._ports_lock.acquire(False): + try: + self._ports = self._list_ports(filters) + self._ports_time = time.time() + finally: + self._ports_lock.release() + return self._ports + + def _list_ports(self, filters): with _utils.neutron_exceptions("Error fetching port list"): return self.manager.submit_task( _tasks.PortList(**filters))['ports'] @@ -1477,13 +1488,28 @@ def list_security_groups(self): _tasks.NovaSecurityGroupList()) return _utils.normalize_nova_secgroups(groups) - @_utils.cache_on_arguments(resource='server') def list_servers(self, detailed=False): """List all available servers. :returns: A list of server ``munch.Munch``. """ + if (time.time() - self._servers_time) >= self._SERVER_AGE: + # Since we're using cached data anyway, we don't need to + # have more than one thread actually submit the list + # servers task. Let the first one submit it while holding + # a lock, and the non-blocking acquire method will cause + # subsequent threads to just skip this and use the old + # data until it succeeds. + if self._servers_lock.acquire(False): + try: + self._servers = self._list_servers(detailed=detailed) + self._servers_time = time.time() + finally: + self._servers_lock.release() + return self._servers + + def _list_servers(self, detailed=False): with _utils.shade_exceptions( "Error fetching server list on {cloud}:{region}:".format( cloud=self.name, @@ -1564,7 +1590,6 @@ def list_floating_ip_pools(self): with _utils.shade_exceptions("Error fetching floating IP pool list"): return self.manager.submit_task(_tasks.FloatingIPPoolList()) - @_utils.cache_on_arguments(resource='floating_ip') def list_floating_ips(self): """List all available floating IPs. @@ -2892,8 +2917,7 @@ def _upload_image_put( return image for count in _utils._iterate_timeout( 60, - "Timeout waiting for the image to finish.", - wait=self._get_cache_time('image')): + "Timeout waiting for the image to finish."): image_obj = self.get_image(image.id) if image_obj and image_obj.status not in ('queued', 'saving'): return image_obj @@ -3691,8 +3715,7 @@ def _neutron_create_floating_ip( for count in _utils._iterate_timeout( timeout, "Timeout waiting for the floating IP" - " to be ACTIVE", - wait=self._get_cache_time('floating_ip')): + " to be ACTIVE"): fip = self.get_floating_ip(fip_id) if fip['status'] == 'ACTIVE': break @@ -3746,11 +3769,6 @@ def delete_floating_ip(self, floating_ip_id, retry=1): if (retry == 0) or not result: return result - # Wait for the cached floating ip list to be regenerated - float_expire_time = self._get_cache_time('floating_ip') - if float_expire_time: - time.sleep(float_expire_time) - # neutron sometimes returns success when deleating a floating # ip. That's awesome. SO - verify that the delete actually # worked. @@ -3906,15 +3924,14 @@ def _nat_destination_port( # If we are caching port lists, we may not find the port for # our server if the list is old. Try for at least 2 cache # periods if that is the case. - port_expire_time = self._get_cache_time('ports') - if port_expire_time: - timeout = port_expire_time * 2 + if self._PORT_AGE: + timeout = self._PORT_AGE * 2 else: timeout = None for count in _utils._iterate_timeout( timeout, "Timeout waiting for port to show up in list", - wait=port_expire_time): + wait=self._PORT_AGE): try: port_filter = {'device_id': server['id']} ports = self.search_ports(filters=port_filter) @@ -4559,7 +4576,7 @@ def wait_for_server( for count in _utils._iterate_timeout( timeout, timeout_message, - wait=self._get_cache_time('server')): + wait=self._SERVER_AGE): try: # Use the get_server call so that the list_servers # cache can be leveraged @@ -4776,7 +4793,7 @@ def _delete_server( for count in _utils._iterate_timeout( timeout, "Timed out waiting for server to get deleted.", - wait=self._get_cache_time('server')): + wait=self._SERVER_AGE): with _utils.shade_exceptions("Error in deleting server"): server = self.get_server(server['id']) if not server: @@ -4785,6 +4802,9 @@ def _delete_server( if reset_volume_cache: self.list_volumes.invalidate(self) + # Reset the list servers cache time so that the next list server + # call gets a new list + self._servers_time = self._servers_time - self._SERVER_AGE return True @_utils.valid_kwargs( diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index 3428cf4ea..31cc71ddb 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -279,6 +279,7 @@ def test_create_server_no_addresses(self, mock_sleep): "floating_ips.list.return_value": [fake_floating_ip] } OpenStackCloud.nova_client = Mock(**config) + self.cloud._SERVER_AGE = 0 with patch.object(OpenStackCloud, "add_ips_to_server", return_value=fake_server): self.assertRaises( diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 61713308d..262fadbca 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -596,28 +596,37 @@ def test_add_ip_from_pool( self.assertEqual(server, self.fake_server) - @patch.object(OpenStackCloud, 'has_service') - @patch.object(OpenStackCloud, 'neutron_client') + @patch.object(OpenStackCloud, 'delete_floating_ip') + @patch.object(OpenStackCloud, 'list_floating_ips') @patch.object(OpenStackCloud, '_use_neutron_floating') def test_cleanup_floating_ips( - self, mock_use_neutron_floating, mock_neutron_client, - mock_has_service): + self, mock_use_neutron_floating, mock_list_floating_ips, + mock_delete_floating_ip): mock_use_neutron_floating.return_value = True - mock_has_service.return_value = True - - after_delete_rep = dict( - floatingips=self.mock_floating_ip_list_rep['floatingips'][:1]) - - mock_neutron_client.list_floatingips.side_effect = [ - self.mock_floating_ip_list_rep, - after_delete_rep, - after_delete_rep, - ] + floating_ips = [{ + "id": "this-is-a-floating-ip-id", + "fixed_ip_address": None, + "internal_network": None, + "floating_ip_address": "203.0.113.29", + "network": "this-is-a-net-or-pool-id", + "attached": False, + "status": "ACTIVE" + }, { + "id": "this-is-an-attached-floating-ip-id", + "fixed_ip_address": None, + "internal_network": None, + "floating_ip_address": "203.0.113.29", + "network": "this-is-a-net-or-pool-id", + "attached": True, + "status": "ACTIVE" + }] + + mock_list_floating_ips.return_value = floating_ips self.cloud.delete_unattached_floating_ips() - mock_neutron_client.delete_floatingip.assert_called_once_with( - floatingip='61cea855-49cb-4846-997d-801b70c71bdd') + mock_delete_floating_ip.assert_called_once_with( + floating_ip_id='this-is-a-floating-ip-id', retry=1) @patch.object(OpenStackCloud, '_submit_create_fip') @patch.object(OpenStackCloud, '_nat_destination_port') diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 48d45f6b8..53331118b 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -355,13 +355,7 @@ def test_get_server_private_ip_devstack( self.assertEqual(PRIVATE_V4, srv['private_v4']) mock_has_service.assert_called_with('volume') mock_list_networks.assert_called_once_with() - # TODO(mordred) empirical testing shows that list_floating_ips IS - # called, but with the caching decorator mock doesn't see it. I care - # less about fixing the mock as mocking out at this level is the - # wrong idea anyway - # To fix this, we should rewrite this to mock neutron_client - not - # shade's list_floating_ips method - # mock_list_floating_ips.assert_called_once_with() + mock_list_floating_ips.assert_called_once_with() @mock.patch.object(shade.OpenStackCloud, 'list_floating_ips') @mock.patch.object(shade.OpenStackCloud, 'list_subnets') From f616269390740821ee2e8b3483e884a2d5b1a858 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 31 Aug 2016 09:28:01 -0500 Subject: [PATCH 1025/3836] Skip test creating provider network if one exists The current default in devstack-gate for neutron is to create a public provider network using the existing physical network. That's AWESOME and we're highly in support of it - but it breaks our test of creating a public provider network using the existing physical network. Detect that this is the case and skip the test if it is. Change-Id: Iff103154aace62f713b2f7b3303f19d31b20d0a7 --- shade/tests/functional/test_network.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/shade/tests/functional/test_network.py b/shade/tests/functional/test_network.py index 931113cce..7bb47820c 100644 --- a/shade/tests/functional/test_network.py +++ b/shade/tests/functional/test_network.py @@ -66,6 +66,10 @@ def test_create_network_advanced(self): self.assertFalse(net1['admin_state_up']) def test_create_network_provider_flat(self): + existing_public = self.operator_cloud.search_networks( + filters={'provider:network_type': 'flat'}) + if existing_public: + self.skipTest('Physical network already allocated') net1 = self.operator_cloud.create_network( name=self.network_name, shared=True, From 9bbc09d703554e14da8fc3ec1891578dda3c353e Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 31 Aug 2016 14:08:30 -0400 Subject: [PATCH 1026/3836] Allow str for ip_version param in create_subnet The Ansible os_subnet module will send a string for ip_version as the type is not currently enforced. Neutron currently accepts this, but who knows about the future. Rather than break older playbooks by enforcing type, let's just be friendly to our users and accept strings (but they must be valid representations of ints). Change-Id: If8db08d2e93feab3cb21001211ccff70b926ed20 --- shade/openstackcloud.py | 7 +++++++ shade/tests/unit/test_shade.py | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 30c643b0b..ad3296043 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5251,6 +5251,13 @@ def create_subnet(self, network_name_or_id, cidr, ip_version=4, raise OpenStackCloudException( 'arg:disable_gateway_ip is not allowed with arg:gateway_ip') + # Be friendly on ip_version and allow strings + if isinstance(ip_version, six.string_types): + try: + ip_version = int(ip_version) + except ValueError: + raise OpenStackCloudException('ip_version must be an integer') + # The body of the neutron message for the subnet we wish to create. # This includes attributes that are required or have defaults. subnet = { diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 8eeeb85d5..8ecdee2d7 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -371,6 +371,29 @@ def test_create_subnet(self, mock_client, mock_search): host_routes=routes) self.assertTrue(mock_client.create_subnet.called) + @mock.patch.object(shade.OpenStackCloud, 'search_networks') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_subnet_string_ip_version(self, mock_client, mock_search): + '''Allow ip_version as a string''' + net1 = dict(id='123', name='donald') + mock_search.return_value = [net1] + self.cloud.create_subnet('donald', '192.168.199.0/24', ip_version='4') + self.assertTrue(mock_client.create_subnet.called) + + @mock.patch.object(shade.OpenStackCloud, 'search_networks') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_subnet_bad_ip_version(self, mock_client, mock_search): + '''String ip_versions must be convertable to int''' + net1 = dict(id='123', name='donald') + mock_search.return_value = [net1] + + with testtools.ExpectedException( + exc.OpenStackCloudException, + "ip_version must be an integer" + ): + self.cloud.create_subnet('donald', '192.168.199.0/24', + ip_version='4x') + @mock.patch.object(shade.OpenStackCloud, 'search_networks') @mock.patch.object(shade.OpenStackCloud, 'neutron_client') def test_create_subnet_without_gateway_ip(self, mock_client, mock_search): From 8b7859e21e64027d20f158737bbf70bbe409b847 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 1 Sep 2016 09:10:03 -0500 Subject: [PATCH 1027/3836] Split auth plugin loading into its own method In case someone wants to do validate=False at get_one_cloud time, but still would like to get an auth plugin, having this block be its own method is handy. Change-Id: I26137391e67d60d8737473d3d4c6ed15dad53af1 --- os_client_config/config.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 6eed0f0cd..7a70daaf6 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -1031,6 +1031,24 @@ def magic_fixes(self, config): return config + def load_auth_plugin(self, config, cloud): + try: + loader = self._get_auth_loader(config) + config = self._validate_auth(config, loader) + auth_plugin = loader.load_from_options(**config['auth']) + except Exception as e: + # We WANT the ksa exception normally + # but OSC can't handle it right now, so we try deferring + # to ksc. If that ALSO fails, it means there is likely + # a deeper issue, so we assume the ksa error was correct + self.log.debug("Deferring keystone exception: {e}".format(e=e)) + auth_plugin = None + try: + config = self._validate_auth_ksc(config, cloud) + except Exception: + raise e + return auth_plugin + def get_one_cloud(self, cloud=None, validate=True, argparse=None, **kwargs): """Retrieve a single cloud configuration and merge additional options @@ -1089,21 +1107,7 @@ def get_one_cloud(self, cloud=None, validate=True, config = self.auth_config_hook(config) if validate: - try: - loader = self._get_auth_loader(config) - config = self._validate_auth(config, loader) - auth_plugin = loader.load_from_options(**config['auth']) - except Exception as e: - # We WANT the ksa exception normally - # but OSC can't handle it right now, so we try deferring - # to ksc. If that ALSO fails, it means there is likely - # a deeper issue, so we assume the ksa error was correct - self.log.debug("Deferring keystone exception: {e}".format(e=e)) - auth_plugin = None - try: - config = self._validate_auth_ksc(config, cloud) - except Exception: - raise e + auth_plugin = self.load_auth_plugin(config, cloud) else: auth_plugin = None From a3c9a947dcb25cdb6f0cd988695da59c02ce3c8e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 12 May 2016 12:27:45 -0500 Subject: [PATCH 1028/3836] Batch calls to list_floating_ips Similar to servers and ports, we do lists of floating ips a lot. Allow for configuration of batching actual calls to the cloud. Change-Id: Iccbbdfca8a42b0a17855dd0bb30f0def15e1e3da --- shade/openstackcloud.py | 46 ++++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index a1282d242..700928ea2 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -64,6 +64,7 @@ DEFAULT_MAX_FILE_SIZE = (5 * 1024 * 1024 * 1024 + 2) / 2 DEFAULT_SERVER_AGE = 5 DEFAULT_PORT_AGE = 5 +DEFAULT_FLOAT_AGE = 5 OBJECT_CONTAINER_ACLS = { @@ -205,6 +206,10 @@ def __init__( self._ports_time = 0 self._ports_lock = threading.Lock() + self._floating_ips = [] + self._floating_ips_time = 0 + self._floating_ips_lock = threading.Lock() + self._networks_lock = threading.Lock() self._reset_network_caches() @@ -228,6 +233,7 @@ def __init__( self._SERVER_AGE = DEFAULT_SERVER_AGE self._PORT_AGE = DEFAULT_PORT_AGE + self._FLOAT_AGE = DEFAULT_FLOAT_AGE else: self.cache_enabled = False @@ -243,6 +249,7 @@ def invalidate(self): # soon. self._SERVER_AGE = 0 self._PORT_AGE = 0 + self._FLOAT_AGE = 0 self._cache = _FakeCache() # Undecorate cache decorated methods. Otherwise the call stacks # wind up being stupidly long and hard to debug @@ -262,6 +269,8 @@ def invalidate(self): 'server', self._SERVER_AGE) self._PORT_AGE = cloud_config.get_cache_resource_expiration( 'port', self._PORT_AGE) + self._FLOAT_AGE = cloud_config.get_cache_resource_expiration( + 'floating_ip', self._FLOAT_AGE) self._container_cache = dict() self._file_hash_cache = dict() @@ -1590,12 +1599,7 @@ def list_floating_ip_pools(self): with _utils.shade_exceptions("Error fetching floating IP pool list"): return self.manager.submit_task(_tasks.FloatingIPPoolList()) - def list_floating_ips(self): - """List all available floating IPs. - - :returns: A list of floating IP ``munch.Munch``. - - """ + def _list_floating_ips(self): if self._use_neutron_floating(): try: return _utils.normalize_neutron_floating_ips( @@ -1609,6 +1613,27 @@ def list_floating_ips(self): floating_ips = self._nova_list_floating_ips() return _utils.normalize_nova_floating_ips(floating_ips) + def list_floating_ips(self): + """List all available floating IPs. + + :returns: A list of floating IP ``munch.Munch``. + + """ + if (time.time() - self._floating_ips_time) >= self._FLOAT_AGE: + # Since we're using cached data anyway, we don't need to + # have more than one thread actually submit the list + # floating ips task. Let the first one submit it while holding + # a lock, and the non-blocking acquire method will cause + # subsequent threads to just skip this and use the old + # data until it succeeds. + if self._floating_ips_lock.acquire(False): + try: + self._floating_ips = self._list_floating_ips() + self._floating_ips_time = time.time() + finally: + self._floating_ips_lock.release() + return self._floating_ips + def _neutron_list_floating_ips(self): with _utils.neutron_exceptions("error fetching floating IPs list"): return self.manager.submit_task( @@ -3715,9 +3740,10 @@ def _neutron_create_floating_ip( for count in _utils._iterate_timeout( timeout, "Timeout waiting for the floating IP" - " to be ACTIVE"): + " to be ACTIVE", + wait=self._FLOAT_AGE): fip = self.get_floating_ip(fip_id) - if fip['status'] == 'ACTIVE': + if fip and fip['status'] == 'ACTIVE': break except OpenStackCloudTimeout: self.log.error( @@ -3769,6 +3795,10 @@ def delete_floating_ip(self, floating_ip_id, retry=1): if (retry == 0) or not result: return result + # Wait for the cached floating ip list to be regenerated + if self._FLOAT_AGE: + time.sleep(self._FLOAT_AGE) + # neutron sometimes returns success when deleating a floating # ip. That's awesome. SO - verify that the delete actually # worked. From 4decd44861c77348fac0fbe19e7c499fbf3cc6e0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 1 Sep 2016 15:47:53 -0500 Subject: [PATCH 1029/3836] Fix up image and flavor by name in create_server Nodepool has a problem with passing in {'id': image.id} which is what the stated workaround for excess API calls is. Fix the call, and add a functional test that should catch this. Change-Id: I4938037decf51001ab5789ee383f6c7ed34889b1 --- shade/openstackcloud.py | 4 ++-- shade/tests/functional/test_compute.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index a1282d242..b91089140 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4520,11 +4520,11 @@ def create_server( if image: if isinstance(image, dict): - kwargs['image'] = image + kwargs['image'] = image['id'] else: kwargs['image'] = self.get_image(image) if flavor and isinstance(flavor, dict): - kwargs['flavor'] = flavor + kwargs['flavor'] = flavor['id'] else: kwargs['flavor'] = self.get_flavor(flavor, get_extra=False) diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index 5e3f11155..f9b414069 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -70,6 +70,21 @@ def test_create_and_delete_server(self): self.demo_cloud.delete_server(self.server_name, wait=True)) self.assertIsNone(self.demo_cloud.get_server(self.server_name)) + def test_create_server_image_flavor_dict(self): + self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) + server = self.demo_cloud.create_server( + name=self.server_name, + image={'id': self.image.id}, + flavor={'id': self.flavor.id}, + wait=True) + self.assertEqual(self.server_name, server['name']) + self.assertEqual(self.image.id, server['image']['id']) + self.assertEqual(self.flavor.id, server['flavor']['id']) + self.assertIsNotNone(server['adminPass']) + self.assertTrue( + self.demo_cloud.delete_server(self.server_name, wait=True)) + self.assertIsNone(self.demo_cloud.get_server(self.server_name)) + def test_get_server_console(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) server = self.demo_cloud.create_server( From 86b100e400ad9dbda018d4be7f97ce4582737fe7 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 1 Sep 2016 18:08:15 -0500 Subject: [PATCH 1030/3836] Add ability to configure Session constructor Sometimes it's useful to wrapp the keystoneauth.Session object. OSC has a KeystoneSession object that injects timing data. shade is considering one that wraps external calls in shade's TaskManager. Allow for passing in a callable that will return a Session. Almost no people will want to use this - it's a super advanced kind of thing. Change-Id: Ib64260916695e9fbea437862cd669a4fb85ec9e4 --- os_client_config/cloud_config.py | 5 +++-- os_client_config/config.py | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 1846f5460..ae5da3a5a 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -70,7 +70,7 @@ def _make_key(key, service_type): class CloudConfig(object): def __init__(self, name, region, config, force_ipv4=False, auth_plugin=None, - openstack_config=None): + openstack_config=None, session_constructor=None): self.name = name self.region = region self.config = config @@ -79,6 +79,7 @@ def __init__(self, name, region, config, self._auth = auth_plugin self._openstack_config = openstack_config self._keystone_session = None + self._session_constructor = session_constructor or session.Session def __getattr__(self, key): """Return arbitrary attributes.""" @@ -196,7 +197,7 @@ def get_session(self): " since verify=False".format( cloud=self.name, region=self.region)) requestsexceptions.squelch_warnings(insecure_requests=not verify) - self._keystone_session = session.Session( + self._keystone_session = self._session_constructor( auth=self._auth, verify=verify, cert=cert, diff --git a/os_client_config/config.py b/os_client_config/config.py index 6eed0f0cd..0c11230ef 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -174,8 +174,9 @@ class OpenStackConfig(object): def __init__(self, config_files=None, vendor_files=None, override_defaults=None, force_ipv4=None, envvar_prefix=None, secure_files=None, - pw_func=None): + pw_func=None, session_constructor=None): self.log = _log.setup_logging(__name__) + self._session_constructor = session_constructor self._config_files = config_files or CONFIG_FILES self._secure_files = secure_files or SECURE_FILES @@ -1127,7 +1128,8 @@ def get_one_cloud(self, cloud=None, validate=True, config=self._normalize_keys(config), force_ipv4=force_ipv4, auth_plugin=auth_plugin, - openstack_config=self + openstack_config=self, + session_constructor=self._session_constructor, ) def get_one_cloud_osc( From bda72e4adcb33d5a7e6d47fcc46626ebfab1f2f0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 2 Sep 2016 09:58:11 -0500 Subject: [PATCH 1031/3836] Use list_servers for polling rather than get_server_by_id Using get_server_by_id bypasses the list_server poll/cache. There is one legit place where we should do this, but in the other places it causes us to poll with great frequency unnecessarily. The TaskManager test changes are just to get test discover to stop finding then skipping the fixtures in the file. Change-Id: I7f1e360f53d7344dfc996eb4c4813184720f8da0 --- shade/openstackcloud.py | 12 ++++++--- shade/tests/unit/test_floating_ip_neutron.py | 6 ++--- shade/tests/unit/test_rebuild_server.py | 14 +++++------ shade/tests/unit/test_task_manager.py | 26 ++++++++++---------- 4 files changed, 31 insertions(+), 27 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index b91089140..597e1bddf 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3900,8 +3900,9 @@ def _attach_ip_to_server( server_id = server['id'] for _ in _utils._iterate_timeout( timeout, - "Timeout waiting for the floating IP to be attached."): - server = self.get_server_by_id(server_id) + "Timeout waiting for the floating IP to be attached.", + wait=self._SERVER_AGE): + server = self.get_server(server_id) ext_ip = meta.get_server_ip(server, ext_tag='floating') if ext_ip == floating_ip['floating_ip_address']: return server @@ -4652,11 +4653,14 @@ def rebuild_server(self, server_id, image_id, admin_pass=None, for count in _utils._iterate_timeout( timeout, "Timeout waiting for server {0} to " - "rebuild.".format(server_id)): + "rebuild.".format(server_id), + wait=self._SERVER_AGE): try: - server = self.get_server_by_id(server_id) + server = self.get_server(server_id) except Exception: continue + if not server: + continue if server['status'] == 'ACTIVE': server.adminPass = admin_pass diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 262fadbca..34d1083bb 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -532,16 +532,16 @@ def test_attach_ip_to_server( ) @patch.object(OpenStackCloud, 'delete_floating_ip') - @patch.object(OpenStackCloud, 'get_server_by_id') + @patch.object(OpenStackCloud, 'get_server') @patch.object(OpenStackCloud, 'create_floating_ip') @patch.object(OpenStackCloud, 'has_service') def test_add_ip_refresh_timeout( self, mock_has_service, mock_create_floating_ip, - mock_get_server_by_id, mock_delete_floating_ip): + mock_get_server, mock_delete_floating_ip): mock_has_service.return_value = True mock_create_floating_ip.return_value = self.floating_ip - mock_get_server_by_id.return_value = self.fake_server + mock_get_server.return_value = self.fake_server self.assertRaises( exc.OpenStackCloudTimeout, diff --git a/shade/tests/unit/test_rebuild_server.py b/shade/tests/unit/test_rebuild_server.py index 5cb81eab4..509394b99 100644 --- a/shade/tests/unit/test_rebuild_server.py +++ b/shade/tests/unit/test_rebuild_server.py @@ -56,13 +56,13 @@ def test_rebuild_server_server_error(self): with patch("shade.OpenStackCloud"): config = { "servers.rebuild.return_value": rebuild_server, - "servers.get.return_value": error_server, + "servers.list.return_value": [error_server], "floating_ips.list.return_value": [fake_floating_ip] } OpenStackCloud.nova_client = Mock(**config) self.assertRaises( OpenStackCloudException, - self.cloud.rebuild_server, "a", "b", wait=True) + self.cloud.rebuild_server, "1234", "b", wait=True) def test_rebuild_server_timeout(self): """ @@ -73,7 +73,7 @@ def test_rebuild_server_timeout(self): with patch("shade.OpenStackCloud"): config = { "servers.rebuild.return_value": rebuild_server, - "servers.get.return_value": rebuild_server, + "servers.list.return_value": [rebuild_server], } OpenStackCloud.nova_client = Mock(**config) self.assertRaises( @@ -125,7 +125,7 @@ def test_rebuild_server_with_admin_pass_wait(self): '5678') config = { "servers.rebuild.return_value": rebuild_server, - "servers.get.return_value": active_server, + "servers.list.return_value": [active_server], "floating_ips.list.return_value": [fake_floating_ip] } OpenStackCloud.nova_client = Mock(**config) @@ -135,7 +135,7 @@ def test_rebuild_server_with_admin_pass_wait(self): meta.obj_to_dict(ret_active_server), cloud_name='cloud-name', region_name='RegionOne'), self.cloud.rebuild_server( - "a", "b", wait=True, admin_pass='ooBootheiX0edoh')) + "1234", "b", wait=True, admin_pass='ooBootheiX0edoh')) def test_rebuild_server_wait(self): """ @@ -150,7 +150,7 @@ def test_rebuild_server_wait(self): '5678') config = { "servers.rebuild.return_value": rebuild_server, - "servers.get.return_value": active_server, + "servers.list.return_value": [active_server], "floating_ips.list.return_value": [fake_floating_ip] } OpenStackCloud.nova_client = Mock(**config) @@ -159,4 +159,4 @@ def test_rebuild_server_wait(self): _utils.normalize_server( meta.obj_to_dict(active_server), cloud_name='cloud-name', region_name='RegionOne'), - self.cloud.rebuild_server("a", "b", wait=True)) + self.cloud.rebuild_server("1234", "b", wait=True)) diff --git a/shade/tests/unit/test_task_manager.py b/shade/tests/unit/test_task_manager.py index bedd115a8..46531c8e0 100644 --- a/shade/tests/unit/test_task_manager.py +++ b/shade/tests/unit/test_task_manager.py @@ -21,37 +21,37 @@ class TestException(Exception): pass -class TestTask(task_manager.Task): +class TaskTest(task_manager.Task): def main(self, client): raise TestException("This is a test exception") -class TestTaskGenerator(task_manager.Task): +class TaskTestGenerator(task_manager.Task): def main(self, client): yield 1 -class TestTaskInt(task_manager.Task): +class TaskTestInt(task_manager.Task): def main(self, client): return int(1) -class TestTaskFloat(task_manager.Task): +class TaskTestFloat(task_manager.Task): def main(self, client): return float(2.0) -class TestTaskStr(task_manager.Task): +class TaskTestStr(task_manager.Task): def main(self, client): return "test" -class TestTaskBool(task_manager.Task): +class TaskTestBool(task_manager.Task): def main(self, client): return True -class TestTaskSet(task_manager.Task): +class TaskTestSet(task_manager.Task): def main(self, client): return set([1, 2]) @@ -69,24 +69,24 @@ def test_wait_re_raise(self): Specifically, we test if we get the same behaviour with all the configured interpreters (e.g. py27, p34, pypy, ...) """ - self.assertRaises(TestException, self.manager.submit_task, TestTask()) + self.assertRaises(TestException, self.manager.submit_task, TaskTest()) def test_dont_munchify_int(self): - ret = self.manager.submit_task(TestTaskInt()) + ret = self.manager.submit_task(TaskTestInt()) self.assertIsInstance(ret, int) def test_dont_munchify_float(self): - ret = self.manager.submit_task(TestTaskFloat()) + ret = self.manager.submit_task(TaskTestFloat()) self.assertIsInstance(ret, float) def test_dont_munchify_str(self): - ret = self.manager.submit_task(TestTaskStr()) + ret = self.manager.submit_task(TaskTestStr()) self.assertIsInstance(ret, str) def test_dont_munchify_bool(self): - ret = self.manager.submit_task(TestTaskBool()) + ret = self.manager.submit_task(TaskTestBool()) self.assertIsInstance(ret, bool) def test_dont_munchify_set(self): - ret = self.manager.submit_task(TestTaskSet()) + ret = self.manager.submit_task(TaskTestSet()) self.assertIsInstance(ret, set) From 91eb5e062a5ea70a65edb08dd22d053b26a3d0f4 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Fri, 2 Sep 2016 09:38:36 -0400 Subject: [PATCH 1032/3836] Update reno for stable/newton Change-Id: I829a65b2104ec3c039859dce2594b701981b1fa3 --- releasenotes/source/index.rst | 1 + releasenotes/source/newton.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/newton.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 2f4234a88..f708ec8d3 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -9,6 +9,7 @@ Contents :maxdepth: 2 unreleased + newton mitaka Indices and tables diff --git a/releasenotes/source/newton.rst b/releasenotes/source/newton.rst new file mode 100644 index 000000000..97036ed25 --- /dev/null +++ b/releasenotes/source/newton.rst @@ -0,0 +1,6 @@ +=================================== + Newton Series Release Notes +=================================== + +.. release-notes:: + :branch: origin/stable/newton From f91a75426b877edc1f0f2e0abe4db9f40e837242 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 2 Sep 2016 11:21:24 -0500 Subject: [PATCH 1033/3836] Don't build releasenotes in normal docs build The release notes system exists in parallel and publishes to http://docs.openstack.org/releasenotes/os-client-config/. When we build them in the normal doc build, it causes problems for distro packagers. Change-Id: I6b084a1ad6836beac991d03c5f134203512150ac --- doc/source/releasenotes.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/source/releasenotes.rst b/doc/source/releasenotes.rst index 2a4bceb4e..9f41b7e14 100644 --- a/doc/source/releasenotes.rst +++ b/doc/source/releasenotes.rst @@ -2,4 +2,5 @@ Release Notes ============= -.. release-notes:: +Release notes for `os-client-config` can be found at +http://docs.openstack.org/releasenotes/os-client-config/ From 1c00116195272da844b6db076a8aa36604fa16bb Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 6 Sep 2016 14:25:09 -0500 Subject: [PATCH 1034/3836] Add libffi-dev to bindep.txt Change-Id: I3510bcbe140db0a57745c5ddfc6eaf7edec75ee3 --- bindep.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bindep.txt b/bindep.txt index 9c37fd538..e5d10a394 100644 --- a/bindep.txt +++ b/bindep.txt @@ -4,3 +4,5 @@ build-essential [platform:dpkg] python-dev [platform:dpkg] python-devel [platform:rpm] +libffi-dev [platform:dpkg] +libffi-devel [platform:rpm] From e888d8e5cd2a7bf1f1ffd4de875e8c649751157a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 7 Sep 2016 13:51:15 -0500 Subject: [PATCH 1035/3836] Add support for jmespath filter expressions ansible upstream recently added a filter using jmespath to allow for arbitrary querying of complex data structures. While reviewing that patch, it seemed quite flexible and lovely. It's also apparently also used in boto for similar reasons, so we're following a pretty well tested solution - and since it's now a depend on ansible, the extra depend shouldn't be too much of a burden for most folks. Change-Id: Ia4bf455f0e32f29a6fce79c71fecce7b0ed57ea5 --- ...add-jmespath-support-f47b7a503dbbfda1.yaml | 4 + requirements.txt | 1 + shade/_utils.py | 13 ++- shade/openstackcloud.py | 84 +++++++++++++++---- shade/tests/functional/test_users.py | 4 + shade/tests/unit/test__utils.py | 7 ++ 6 files changed, 93 insertions(+), 20 deletions(-) create mode 100644 releasenotes/notes/add-jmespath-support-f47b7a503dbbfda1.yaml diff --git a/releasenotes/notes/add-jmespath-support-f47b7a503dbbfda1.yaml b/releasenotes/notes/add-jmespath-support-f47b7a503dbbfda1.yaml new file mode 100644 index 000000000..2d157a3c8 --- /dev/null +++ b/releasenotes/notes/add-jmespath-support-f47b7a503dbbfda1.yaml @@ -0,0 +1,4 @@ +--- +features: + - All get and search functions can now take a jmespath expression in their + filters parameter. diff --git a/requirements.txt b/requirements.txt index 54e1d450c..e229015b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ pbr>=0.11,<2.0 munch decorator +jmespath jsonpatch ipaddress os-client-config>=1.20.0 diff --git a/shade/_utils.py b/shade/_utils.py index 5e87615cf..7d63fcd39 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -14,6 +14,7 @@ import contextlib import inspect +import jmespath import munch import netifaces import re @@ -73,7 +74,7 @@ def _filter_list(data, name_or_id, filters): key if a value for name_or_id is given. :param string name_or_id: The name or ID of the entity being filtered. - :param dict filters: + :param filters: A dictionary of meta data to use for further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: @@ -83,6 +84,8 @@ def _filter_list(data, name_or_id, filters): 'gender': 'Female' } } + OR + A string containing a jmespath expression for further filtering. """ if name_or_id: identifier_matches = [] @@ -96,6 +99,9 @@ def _filter_list(data, name_or_id, filters): if not filters: return data + if isinstance(filters, six.string_types): + return jmespath.search(filters, data) + def _dict_filter(f, d): if not d: return False @@ -129,8 +135,11 @@ def _get_entity(func, name_or_id, filters, **kwargs): and returns a list of entities to filter. :param string name_or_id: The name or ID of the entity being filtered or a dict - :param dict filters: + :param filters: A dictionary of meta data to use for further filtering. + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" """ # Sometimes in the control flow of shade, we already have an object # fetched. Rather than then needing to pull the name or id out of that diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 08af74bc3..1e9780838 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -464,7 +464,7 @@ def range_search(self, data, filters): operators are one of: <,>,<=,>= :param list data: List of dictionaries to be searched. - :param dict filters: Dict describing the one or more range searches to + :param filters: Dict describing the one or more range searches to perform. If more than one search is given, the result will be the members of the original data set that match ALL searches. An example of filtering by multiple ranges:: @@ -634,7 +634,10 @@ def search_users(self, name_or_id=None, filters=None): """Seach Keystone users. :param string name: user name or id. - :param dict filters: a dict containing additional filters to use. + :param filters: a dict containing additional filters to use. + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: a list of ``munch.Munch`` containing the users @@ -648,7 +651,10 @@ def get_user(self, name_or_id, filters=None): """Get exactly one Keystone user. :param string name_or_id: user name or id. - :param dict filters: a dict containing additional filters to use. + :param filters: a dict containing additional filters to use. + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: a single ``munch.Munch`` containing the user description. @@ -1939,7 +1945,7 @@ def get_keypair(self, name_or_id, filters=None): """Get a keypair by name or ID. :param name_or_id: Name or ID of the keypair. - :param dict filters: + :param filters: A dictionary of meta data to use for further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: @@ -1949,6 +1955,9 @@ def get_keypair(self, name_or_id, filters=None): 'gender': 'Female' } } + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A keypair ``munch.Munch`` or None if no matching keypair is found. @@ -1960,7 +1969,7 @@ def get_network(self, name_or_id, filters=None): """Get a network by name or ID. :param name_or_id: Name or ID of the network. - :param dict filters: + :param filters: A dictionary of meta data to use for further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: @@ -1970,6 +1979,9 @@ def get_network(self, name_or_id, filters=None): 'gender': 'Female' } } + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A network ``munch.Munch`` or None if no matching network is found. @@ -1981,7 +1993,7 @@ def get_router(self, name_or_id, filters=None): """Get a router by name or ID. :param name_or_id: Name or ID of the router. - :param dict filters: + :param filters: A dictionary of meta data to use for further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: @@ -1991,6 +2003,9 @@ def get_router(self, name_or_id, filters=None): 'gender': 'Female' } } + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A router ``munch.Munch`` or None if no matching router is found. @@ -2002,7 +2017,7 @@ def get_subnet(self, name_or_id, filters=None): """Get a subnet by name or ID. :param name_or_id: Name or ID of the subnet. - :param dict filters: + :param filters: A dictionary of meta data to use for further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: @@ -2023,7 +2038,7 @@ def get_port(self, name_or_id, filters=None): """Get a port by name or ID. :param name_or_id: Name or ID of the port. - :param dict filters: + :param filters: A dictionary of meta data to use for further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: @@ -2033,6 +2048,9 @@ def get_port(self, name_or_id, filters=None): 'gender': 'Female' } } + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A port ``munch.Munch`` or None if no matching port is found. @@ -2043,7 +2061,7 @@ def get_volume(self, name_or_id, filters=None): """Get a volume by name or ID. :param name_or_id: Name or ID of the volume. - :param dict filters: + :param filters: A dictionary of meta data to use for further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: @@ -2053,6 +2071,9 @@ def get_volume(self, name_or_id, filters=None): 'gender': 'Female' } } + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A volume ``munch.Munch`` or None if no matching volume is found. @@ -2064,7 +2085,7 @@ def get_flavor(self, name_or_id, filters=None, get_extra=True): """Get a flavor by name or ID. :param name_or_id: Name or ID of the flavor. - :param dict filters: + :param filters: A dictionary of meta data to use for further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: @@ -2074,6 +2095,9 @@ def get_flavor(self, name_or_id, filters=None, get_extra=True): 'gender': 'Female' } } + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :param get_extra: Whether or not the list_flavors call should get the extra flavor specs. @@ -2091,7 +2115,7 @@ def get_security_group(self, name_or_id, filters=None): """Get a security group by name or ID. :param name_or_id: Name or ID of the security group. - :param dict filters: + :param filters: A dictionary of meta data to use for further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: @@ -2101,6 +2125,9 @@ def get_security_group(self, name_or_id, filters=None): 'gender': 'Female' } } + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A security group ``munch.Munch`` or None if no matching security group is found. @@ -2147,7 +2174,7 @@ def get_server(self, name_or_id=None, filters=None, detailed=False): """Get a server by name or ID. :param name_or_id: Name or ID of the server. - :param dict filters: + :param filters: A dictionary of meta data to use for further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: @@ -2157,6 +2184,9 @@ def get_server(self, name_or_id=None, filters=None, detailed=False): 'gender': 'Female' } } + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A server ``munch.Munch`` or None if no matching server is found. @@ -2175,13 +2205,16 @@ def get_server_group(self, name_or_id=None, filters=None): """Get a server group by name or ID. :param name_or_id: Name or ID of the server group. - :param dict filters: + :param filters: A dictionary of meta data to use for further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: { 'policy': 'affinity', } + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A server groups dict or None if no matching server group is found. @@ -2194,7 +2227,7 @@ def get_image(self, name_or_id, filters=None): """Get an image by name or ID. :param name_or_id: Name or ID of the image. - :param dict filters: + :param filters: A dictionary of meta data to use for further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: @@ -2204,6 +2237,9 @@ def get_image(self, name_or_id, filters=None): 'gender': 'Female' } } + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: An image ``munch.Munch`` or None if no matching image is found @@ -2255,7 +2291,7 @@ def get_floating_ip(self, id, filters=None): """Get a floating IP by ID :param id: ID of the floating IP. - :param dict filters: + :param filters: A dictionary of meta data to use for further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: @@ -2265,6 +2301,9 @@ def get_floating_ip(self, id, filters=None): 'gender': 'Female' } } + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A floating IP ``munch.Munch`` or None if no matching floating IP is found. @@ -3395,7 +3434,7 @@ def get_volume_snapshot(self, name_or_id, filters=None): """Get a volume by name or ID. :param name_or_id: Name or ID of the volume snapshot. - :param dict filters: + :param filters: A dictionary of meta data to use for further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: @@ -3405,6 +3444,9 @@ def get_volume_snapshot(self, name_or_id, filters=None): 'gender': 'Female' } } + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A volume ``munch.Munch`` or None if no matching volume is found. @@ -5922,8 +5964,11 @@ def get_zone(self, name_or_id, filters=None): """Get a zone by name or ID. :param name_or_id: Name or ID of the zone - :param dict filters: + :param filters: A dictionary of meta data to use for further filtering + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A zone dict or None if no matching zone is found. @@ -6187,7 +6232,7 @@ def get_cluster_template(self, name_or_id, filters=None, detail=False): ClusterTemplate is the new name for BayModel. :param name_or_id: Name or ID of the ClusterTemplate. - :param dict filters: + :param filters: A dictionary of meta data to use for further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: @@ -6197,6 +6242,9 @@ def get_cluster_template(self, name_or_id, filters=None, detail=False): 'gender': 'Female' } } + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A ClusterTemplate dict or None if no matching ClusterTemplate is found. diff --git a/shade/tests/functional/test_users.py b/shade/tests/functional/test_users.py index 47ea68930..54b32d1bd 100644 --- a/shade/tests/functional/test_users.py +++ b/shade/tests/functional/test_users.py @@ -67,6 +67,10 @@ def test_search_users(self): users = self.operator_cloud.search_users(filters={'enabled': True}) self.assertIsNotNone(users) + def test_search_users_jmespath(self): + users = self.operator_cloud.search_users(filters="[?enabled]") + self.assertIsNotNone(users) + def test_create_user(self): user_name = self.user_prefix + '_create' user_email = 'nobody@nowhere.com' diff --git a/shade/tests/unit/test__utils.py b/shade/tests/unit/test__utils.py index 8fae4ff71..3ef87fcb4 100644 --- a/shade/tests/unit/test__utils.py +++ b/shade/tests/unit/test__utils.py @@ -45,6 +45,13 @@ def test__filter_list_filter(self): ret = _utils._filter_list(data, 'donald', {'other': 'duck'}) self.assertEqual([el1], ret) + def test__filter_list_filter_jmespath(self): + el1 = dict(id=100, name='donald', other='duck') + el2 = dict(id=200, name='donald', other='trump') + data = [el1, el2] + ret = _utils._filter_list(data, 'donald', "[?other == `duck`]") + self.assertEqual([el1], ret) + def test__filter_list_dict1(self): el1 = dict(id=100, name='donald', last='duck', other=dict(category='duck')) From c6d2aeada4d9074f185f9748a81f7c651614e347 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 15 Sep 2016 16:41:17 -0500 Subject: [PATCH 1036/3836] Don't create envvars cloud if cloud or region are set OS_CLOUD and OS_REGION_NAME are both selectors. If they are the only values, we should not be creating an envvars cloud. Change-Id: I1b7c8d7e3b6c300bd2c85ab482a22411370e4d5f --- os_client_config/config.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 646e55ddf..aa6ef7ebf 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -105,12 +105,11 @@ def _get_os_environ(envvar_prefix=None): for k in environkeys: newkey = k.split('_', 1)[-1].lower() ret[newkey] = os.environ[k] - # If the only environ key is region name, don't make a cloud, because - # it's being used as a cloud selector - if not environkeys or ( - len(environkeys) == 1 and 'region_name' in ret): - return None - return ret + # If the only environ keys are cloud and region_name, don't return anything + # because they are cloud selectors + if set(environkeys) - set(['OS_CLOUD', 'OS_REGION_NAME']): + return ret + return None def _merge_clouds(old_dict, new_dict): From eec981c96e3c1824a547c8baa6fc5b08dba57624 Mon Sep 17 00:00:00 2001 From: avnish Date: Tue, 20 Sep 2016 11:39:00 +0530 Subject: [PATCH 1037/3836] modify the home-page info with the developer documentation Change-Id: I72a16e15e61c8e6511b96114cc74e3feb7dc6fd3 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 07ac625ac..f8dcbb096 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,7 @@ description-file = README.rst author = OpenStack author-email = openstack-dev@lists.openstack.org -home-page = http://www.openstack.org/ +home-page = http://docs.openstack.org/developer/os-client-config/ classifier = Environment :: OpenStack Intended Audience :: Information Technology From 0a3e056cce03cdb3dbd62d8eddfaa448d5a0ad5f Mon Sep 17 00:00:00 2001 From: Jordan Pittier Date: Mon, 26 Sep 2016 17:22:27 +0200 Subject: [PATCH 1038/3836] Fix AttributeError in `get_config` Change-Id: I52bdc44800da6c1393a69c4faf96375235ef98bb Closes-Bug: #1627690 --- os_client_config/__init__.py | 2 +- os_client_config/config.py | 5 ++++- os_client_config/tests/test_init.py | 33 +++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 os_client_config/tests/test_init.py diff --git a/os_client_config/__init__.py b/os_client_config/__init__.py index 09d74423b..e8d7fc0f8 100644 --- a/os_client_config/__init__.py +++ b/os_client_config/__init__.py @@ -26,7 +26,7 @@ def get_config(service_key=None, options=None, **kwargs): config = OpenStackConfig() if options: - config.register_argparse_options(options, sys.argv, service_key) + config.register_argparse_arguments(options, sys.argv, service_key) parsed_options = options.parse_known_args(sys.argv) else: parsed_options = None diff --git a/os_client_config/config.py b/os_client_config/config.py index 646e55ddf..01f659e49 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -641,7 +641,7 @@ def _fix_backwards_auth_plugin(self, cloud): # completely broken return cloud - def register_argparse_arguments(self, parser, argv, service_keys=[]): + def register_argparse_arguments(self, parser, argv, service_keys=None): """Register all of the common argparse options needed. Given an argparse parser, register the keystoneauth Session arguments, @@ -660,6 +660,9 @@ def register_argparse_arguments(self, parser, argv, service_keys=[]): is requested """ + if service_keys is None: + service_keys = [] + # Fix argv in place - mapping any keys with embedded _ in them to - _fix_argv(argv) diff --git a/os_client_config/tests/test_init.py b/os_client_config/tests/test_init.py new file mode 100644 index 000000000..15d57f717 --- /dev/null +++ b/os_client_config/tests/test_init.py @@ -0,0 +1,33 @@ +# 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 os_client_config +from os_client_config.tests import base + + +class TestInit(base.TestCase): + def test_get_config_without_arg_parser(self): + cloud_config = os_client_config.get_config(options=None) + self.assertIsInstance( + cloud_config, + os_client_config.cloud_config.CloudConfig + ) + + def test_get_config_with_arg_parser(self): + cloud_config = os_client_config.get_config( + options=argparse.ArgumentParser()) + self.assertIsInstance( + cloud_config, + os_client_config.cloud_config.CloudConfig + ) From bd434bc6268d7d4b872cbf17cf23a4fd5dc1a855 Mon Sep 17 00:00:00 2001 From: Jordan Pittier Date: Mon, 26 Sep 2016 17:39:56 +0200 Subject: [PATCH 1039/3836] List py35 in the default tox env list We really should run py35 tests when we run "tox" without any arguments. I2a4a6ca01d7cca83f594008960c878a18ca08e8e is going to make the py35 job voting. Change-Id: Ibd77e39c53f00357344be8acc2949e1bc1adcc84 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index d2aac05bd..cedf47857 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 1.6 -envlist = py34,py27,pypy,pep8 +envlist = py34,py35,py27,pypy,pep8 skipsdist = True [testenv] From 2d78f1ae940d4b2810437ea3c73379413c3ef6ef Mon Sep 17 00:00:00 2001 From: Tony Xu Date: Mon, 26 Sep 2016 23:51:53 +0800 Subject: [PATCH 1040/3836] Update homepage with developer documentation page Change-Id: Id7df70e3cfaa29218c2e2badefcbc8a296d86f8d --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 07ac625ac..f8dcbb096 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,7 @@ description-file = README.rst author = OpenStack author-email = openstack-dev@lists.openstack.org -home-page = http://www.openstack.org/ +home-page = http://docs.openstack.org/developer/os-client-config/ classifier = Environment :: OpenStack Intended Audience :: Information Technology From afe54856b42e23e6d74da345f4870a95f646bc99 Mon Sep 17 00:00:00 2001 From: Cao Xuan Hoang Date: Wed, 28 Sep 2016 10:09:49 +0700 Subject: [PATCH 1041/3836] Using assertIsNone() instead of assertEqual(None, ...) Following OpenStack Style Guidelines[1]: [H203] Unit test assertions tend to give better messages for more specific assertions. As a result, assertIsNone(...) is preferred over assertEqual(None, ...) and assertIs(None, ...) [1] http://docs.openstack.org/developer/hacking/#unit-tests-and-assertraises Change-Id: I4ce1745a90b043ea342fb157683b01f862c1bc3d --- os_client_config/tests/test_config.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 5cded544a..0ef8da2f2 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -753,8 +753,8 @@ def test_register_argparse_network_service_type(self): opts, _remain = parser.parse_known_args(args) self.assertEqual(opts.os_service_type, 'network') self.assertEqual(opts.os_endpoint_type, 'admin') - self.assertEqual(opts.os_network_service_type, None) - self.assertEqual(opts.os_network_api_version, None) + self.assertIsNone(opts.os_network_service_type) + self.assertIsNone(opts.os_network_api_version) self.assertEqual(opts.network_api_version, '4') cloud = c.get_one_cloud(argparse=opts, verify=False) self.assertEqual(cloud.config['service_type'], 'network') @@ -776,12 +776,12 @@ def test_register_argparse_network_service_types(self): parser, args, ['compute', 'network', 'volume']) opts, _remain = parser.parse_known_args(args) self.assertEqual(opts.os_network_service_type, 'badtype') - self.assertEqual(opts.os_compute_service_type, None) - self.assertEqual(opts.os_volume_service_type, None) + self.assertIsNone(opts.os_compute_service_type) + self.assertIsNone(opts.os_volume_service_type) self.assertEqual(opts.os_service_type, 'compute') self.assertEqual(opts.os_compute_service_name, 'cloudServers') self.assertEqual(opts.os_endpoint_type, 'admin') - self.assertEqual(opts.os_network_api_version, None) + self.assertIsNone(opts.os_network_api_version) self.assertEqual(opts.network_api_version, '4') cloud = c.get_one_cloud(argparse=opts, verify=False) self.assertEqual(cloud.config['service_type'], 'compute') From 1f81bf18865ab8e5a5f595968fe162c1eb183110 Mon Sep 17 00:00:00 2001 From: matthew wagoner Date: Thu, 29 Sep 2016 19:41:04 -0400 Subject: [PATCH 1042/3836] Add test for os_group Ansible module Change-Id: Ibfedb4089b0d4646bd6a63b68f2e83213f0df951 --- .../tests/ansible/roles/group/tasks/main.yml | 19 +++++++++++++++++++ shade/tests/ansible/roles/group/vars/main.yml | 1 + shade/tests/ansible/run.yml | 1 + 3 files changed, 21 insertions(+) create mode 100644 shade/tests/ansible/roles/group/tasks/main.yml create mode 100644 shade/tests/ansible/roles/group/vars/main.yml diff --git a/shade/tests/ansible/roles/group/tasks/main.yml b/shade/tests/ansible/roles/group/tasks/main.yml new file mode 100644 index 000000000..535ed4318 --- /dev/null +++ b/shade/tests/ansible/roles/group/tasks/main.yml @@ -0,0 +1,19 @@ +--- +- name: Create group + os_group: + cloud: "{{ cloud }}" + state: present + name: "{{ group_name }}" + +- name: Update group + os_group: + cloud: "{{ cloud }}" + state: present + name: "{{ group_name }}" + description: "updated description" + +- name: Delete group + os_group: + cloud: "{{ cloud }}" + state: absent + name: "{{ group_name }}" diff --git a/shade/tests/ansible/roles/group/vars/main.yml b/shade/tests/ansible/roles/group/vars/main.yml new file mode 100644 index 000000000..361c01190 --- /dev/null +++ b/shade/tests/ansible/roles/group/vars/main.yml @@ -0,0 +1 @@ +group_name: ansible_group diff --git a/shade/tests/ansible/run.yml b/shade/tests/ansible/run.yml index e935886d3..73fc5792f 100644 --- a/shade/tests/ansible/run.yml +++ b/shade/tests/ansible/run.yml @@ -6,6 +6,7 @@ roles: - { role: auth, tags: auth } - { role: client_config, tags: client_config } + - { role: group, tags: group } - { role: image, tags: image } - { role: keypair, tags: keypair } - { role: network, tags: network } From d009ea21b7b8b95b32812dd5458e44b1f2404384 Mon Sep 17 00:00:00 2001 From: Jordan Pittier Date: Tue, 4 Oct 2016 18:51:59 +0200 Subject: [PATCH 1043/3836] remove_router_interface: check subnet_id or port_id is provided If neither subnet_id nor port_id is provided, Neutron fails badly with a 500 error. Also according to Neutron client CLI (neutron router-interface-delete -h) "Either a subnet or port must be specified." Change-Id: I1c218052e50d10020b699b254b986c23fdbf77a5 --- shade/openstackcloud.py | 6 ++++++ shade/tests/unit/test_shade.py | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 1e9780838..95f961f34 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2503,6 +2503,8 @@ def add_router_interface(self, router, subnet_id=None, port_id=None): def remove_router_interface(self, router, subnet_id=None, port_id=None): """Detach a subnet from an internal router interface. + At least one of subnet_id or port_id must be supplied. + If you specify both subnet and port ID, the subnet ID must correspond to the subnet ID of the first IP address on the port specified by the port ID. Otherwise an error occurs. @@ -2521,6 +2523,10 @@ def remove_router_interface(self, router, subnet_id=None, port_id=None): if port_id: body['port_id'] = port_id + if not body: + raise ValueError( + "At least one of subnet_id or port_id must be supplied.") + with _utils.neutron_exceptions( "Error detaching interface from router {0}".format(router['id']) ): diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 8ecdee2d7..3de161548 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -214,6 +214,11 @@ def test_remove_router_interface(self, mock_client): router='123', body={'subnet_id': 'abc'} ) + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_remove_router_interface_missing_argument(self, mock_client): + self.assertRaises(ValueError, self.cloud.remove_router_interface, + {'id': '123'}) + @mock.patch.object(shade.OpenStackCloud, 'get_router') @mock.patch.object(shade.OpenStackCloud, 'neutron_client') def test_update_router(self, mock_client, mock_get): From db4bace4745953cffd894640d335e095f4d7b3cc Mon Sep 17 00:00:00 2001 From: Jordan Pittier Date: Tue, 4 Oct 2016 19:02:50 +0200 Subject: [PATCH 1044/3836] List py35 in the default tox env list We really should run py35 tests when we run "tox" without any arguments. I0d6c8c8255717770c7e3297bfe5d6130e983d5b0 is going to make the py35 job voting. Change-Id: I8c9b459dfaf1e29e7e8321bfd34061d3d2ad5249 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 4379427ef..60121bb0f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 1.6 -envlist = py34,py27,pypy,pep8 +envlist = py34,py35,py27,pypy,pep8 skipsdist = True [testenv] From 30039f04109efa2263aba6eb302a29bd8d5e8f53 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 26 Sep 2016 14:14:15 -0500 Subject: [PATCH 1045/3836] Add simple field for disabled flavors When we were equalizing out the old silly names from the new pretty ones, we missed disabled. Change-Id: I4cbf5f7c27f640c566460c18951ab9030aae84e4 Depends-On: I523e0ab6e376f5ff6205b1cc1748aa6d546919cb --- shade/_utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shade/_utils.py b/shade/_utils.py index 7d63fcd39..9a9beb1ae 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -498,7 +498,10 @@ def normalize_flavors(flavors): flavor['extra_specs'] = {} ephemeral = flavor.pop('OS-FLV-EXT-DATA:ephemeral', 0) is_public = flavor.pop('os-flavor-access:is_public', True) + disabled = flavor.pop('OS-FLV-DISABLED:disabled', False) # Make sure both the extension version and a sane version are present + flavor['OS-FLV-DISABLED:disabled'] = disabled + flavor['disabled'] = disabled flavor['OS-FLV-EXT-DATA:ephemeral'] = ephemeral flavor['ephemeral'] = ephemeral flavor['os-flavor-access:is_public'] = is_public From f484736b7d087115b8330ae794c101f7f8e7b926 Mon Sep 17 00:00:00 2001 From: Jordan Pittier Date: Thu, 6 Oct 2016 16:00:46 +0200 Subject: [PATCH 1046/3836] cloud_config:get_session_endpoint: catch Keystone EndpointNotFound The docstring of the `get_session_endpoint` says ":returns: Endpoint for the service, or None if not found" but apparently the `None` part was forgotten. This leads to this kind of spectacular traceback where the exception bubles up (even through Shade): Traceback (most recent call last): File ".tox/run/bin/ospurge", line 11, in load_entry_point('ospurge', 'console_scripts', 'ospurge')() File "/path/ospurge/main.py", line 154, in main for resource in resource_manager.list(): File "/path/ospurge/resources/swift.py", line 15, in list for container in self.cloud.list_containers(): File "/path/pkg/shade/openstackcloud.py", line 4909, in list_containers full_listing=full_listing)) File "/path/pkg/shade/task_manager.py", line 244, in submit_task return task.wait(raw) File "/path/pkg/shade/task_manager.py", line 121, in wait super(Task, self).wait() File "/path/pkg/shade/task_manager.py", line 96, in wait self._traceback) File "/path/pkg/six.py", line 686, in reraise raise value File "/path/pkg/shade/task_manager.py", line 105, in run self.done(self.main(client)) File "/path/pkg/shade/_tasks.py", line 549, in main return client.swift_client.get_account(**self.args)[1] File "/path/pkg/shade/openstackcloud.py", line 849, in swift_client 'object-store', swiftclient.client.Connection) File "/path/pkg/shade/openstackcloud.py", line 343, in _get_client **kwargs) File "/path/pkg/os_client_config/cloud_config.py", line 301, in get_legacy_client return self._get_swift_client(client_class=client_class, **kwargs) File "/path/pkg/os_client_config/cloud_config.py", line 369, in _get_swift_client endpoint = self.get_session_endpoint(service_key='object-store') File "/path/pkg/os_client_config/cloud_config.py", line 253, in get_session_endpoint region_name=self.region) File "/path/pkg/keystoneauth1/session.py", line 765, in get_endpoint return auth.get_endpoint(self, **kwargs) File "/path/pkg/keystoneauth1/identity/base.py", line 216, in get_endpoint service_name=service_name) File "/path/pkg/positional/__init__.py", line 101, in inner return wrapped(*args, **kwargs) File "/path/pkg/keystoneauth1/access/service_catalog.py", line 228, in url_for raise exceptions.EndpointNotFound(msg) keystoneauth1.exceptions.catalog.EndpointNotFound: public endpoint for object-store service in RegionOne region not found Change-Id: Idbf5081117bb0a13d04a1a5cb9fd7682baaf04e5 --- os_client_config/cloud_config.py | 18 +++++++++++++----- os_client_config/tests/test_cloud_config.py | 9 +++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index ae5da3a5a..a911d8102 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -16,6 +16,7 @@ import warnings from keystoneauth1 import adapter +import keystoneauth1.exceptions.catalog from keystoneauth1 import plugin from keystoneauth1 import session import requestsexceptions @@ -247,11 +248,18 @@ def get_session_endpoint(self, service_key): if service_key == 'identity': endpoint = session.get_endpoint(interface=plugin.AUTH_INTERFACE) else: - endpoint = session.get_endpoint( - service_type=self.get_service_type(service_key), - service_name=self.get_service_name(service_key), - interface=self.get_interface(service_key), - region_name=self.region) + args = { + 'service_type': self.get_service_type(service_key), + 'service_name': self.get_service_name(service_key), + 'interface': self.get_interface(service_key), + 'region_name': self.region + } + try: + endpoint = session.get_endpoint(**args) + except keystoneauth1.exceptions.catalog.EndpointNotFound: + self.log.warning("Keystone catalog entry not found (%s)", + args) + endpoint = None return endpoint def get_legacy_client( diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index 7a8b77aed..2763f4d3d 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -12,6 +12,7 @@ import copy +from keystoneauth1 import exceptions as ksa_exceptions from keystoneauth1 import plugin as ksa_plugin from keystoneauth1 import session as ksa_session import mock @@ -235,6 +236,14 @@ def test_session_endpoint(self, mock_get_session): region_name='region-al', service_type='orchestration') + @mock.patch.object(cloud_config.CloudConfig, 'get_session') + def test_session_endpoint_not_found(self, mock_get_session): + exc_to_raise = ksa_exceptions.catalog.EndpointNotFound + mock_get_session.return_value.get_endpoint.side_effect = exc_to_raise + cc = cloud_config.CloudConfig( + "test1", "region-al", {}, auth_plugin=mock.Mock()) + self.assertIsNone(cc.get_session_endpoint('notfound')) + @mock.patch.object(cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(cloud_config.CloudConfig, 'get_auth_args') @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') From 8a8a218f090ea7bbb7ce1d3afc11d8f2fda06873 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Thu, 6 Oct 2016 20:41:15 +0200 Subject: [PATCH 1047/3836] Enable release notes translation Releasenote translation publishing is being prepared. 'locale_dirs' needs to be defined in conf.py to generate translated version of the release notes. Note that this repository might not get translated release notes - or no translations at all - but we add the entry here nevertheless to prepare for it. Change-Id: Ic34d4d11adf4aacd91a7fd682a6d15597004ff49 --- releasenotes/source/conf.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py index 282dbd784..e33ee8e57 100644 --- a/releasenotes/source/conf.py +++ b/releasenotes/source/conf.py @@ -259,3 +259,6 @@ # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False + +# -- Options for Internationalization output ------------------------------ +locale_dirs = ['locale/'] From 1653120803e76cf02c2f35d3bdb67e6bb1848817 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 7 Oct 2016 11:35:50 -0400 Subject: [PATCH 1048/3836] Add setter for session constructor shade needs to be able to attach an adapter wrapper to an already constructed CloudConfig object, so add a setter. Change-Id: I640859b5d78d17e3c99e8ec11f1418f275e4dea2 --- os_client_config/cloud_config.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index ae5da3a5a..80cd266ec 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -102,6 +102,10 @@ def __eq__(self, other): def __ne__(self, other): return not self == other + def set_session_constructor(self, session_constructor): + """Sets the Session constructor.""" + self._session_constructor = session_constructor + def get_requests_verify_args(self): """Return the verify and cert values for the requests library.""" if self.config['verify'] and self.config['cacert']: From 9e2004be030ad62a68d63fc9f17dffee62f065d6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 8 Oct 2016 10:54:53 -0500 Subject: [PATCH 1049/3836] Update simple_logging to not not log request ids by default Change-Id: Ieb64f3ed8e359b6af9da3af537e164f3211e31d6 --- shade/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 645f3a596..e4d6d09d7 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -44,6 +44,9 @@ def simple_logging(debug=False, http_debug=False): log = _log.setup_logging('keystoneauth') log.addHandler(logging.StreamHandler()) log.setLevel(log_level) + # Simple case - we do not care about request id log + log = _log.setup_logging('shade.request_ids') + log.setLevel(logging.INFO) log = _log.setup_logging('shade') log.addHandler(logging.StreamHandler()) log.setLevel(log_level) From 05cfd3ba3fdce3b91db5bf8058c2c0a2261dc76c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 2 Oct 2016 11:20:32 -0700 Subject: [PATCH 1050/3836] Add helper properties to generate location info In a following patch we'll start adding locations to normalized dicts. But let's just put the properties here for now. Change-Id: Ibf448d80586a8b44d826678827967c30ddd444b6 --- shade/openstackcloud.py | 37 ++++++++++++++++++++++- shade/tests/unit/base.py | 15 +++++++-- shade/tests/unit/test_flavors.py | 2 +- shade/tests/unit/test_floating_ip_nova.py | 4 ++- shade/tests/unit/test_meta.py | 20 ++++++++++++ 5 files changed, 73 insertions(+), 5 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 1e9780838..8127a8c5f 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -23,6 +23,7 @@ import warnings import dogpile.cache +import munch import requestsexceptions import cinderclient.exceptions as cinder_exceptions @@ -397,6 +398,40 @@ def auth_token(self): # We don't need to track validity here, just get_token() each time. return self.keystone_session.get_token() + @property + def current_project_id(self): + '''Get the current project id. + + Returns the project_id of the current token scope. None means that + the token is domain scoped or unscoped. + + :raises keystoneauth1.exceptions.auth.AuthorizationFailure: + if a new token fetch fails. + :raises keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin: + if a plugin is not available. + ''' + return self.keystone_session.get_project_id() + + @property + def current_project(self): + '''Return a ``munch.Munch`` describing the current project''' + auth_args = self.cloud_config.config.get('auth', {}) + return munch.Munch( + id=self.current_project_id, + name=auth_args.get('project_name'), + domain_id=auth_args.get('domain_id'), + domain_name=auth_args.get('domain_name'), + ) + + @property + def current_location(self): + '''Return a ``munch.Munch`` explaining the current cloud location.''' + return munch.Munch( + cloud=self.name, + region_name=self.region_name, + project=self.current_project, + ) + @property def _project_manager(self): # Keystone v2 calls this attribute tenants @@ -3574,7 +3609,7 @@ def _neutron_available_floating_ips( if project_id is None: # Make sure we are only listing floatingIPs allocated the current # tenant. This is the default behaviour of Nova - project_id = self.keystone_session.get_project_id() + project_id = self.current_project_id with _utils.neutron_exceptions("unable to get available floating IPs"): if network: diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index fc52e0f72..8e3c18b68 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -18,6 +18,7 @@ import time import fixtures +import mock import os import os_client_config as occ import tempfile @@ -26,12 +27,12 @@ from shade.tests import base -class TestCase(base.TestCase): +class BaseTestCase(base.TestCase): def setUp(self, cloud_config_fixture='clouds.yaml'): """Run before each test method to initialize test environment.""" - super(TestCase, self).setUp() + super(BaseTestCase, self).setUp() # Sleeps are for real testing, but unit tests shouldn't need them realsleep = time.sleep @@ -88,3 +89,13 @@ def _nosleep(seconds): self.full_op_cloud = shade.OperatorCloud( cloud_config=self.full_cloud_config, log_inner_exceptions=True) + + +class TestCase(BaseTestCase): + + def setUp(self, cloud_config_fixture='clouds.yaml'): + + super(TestCase, self).setUp(cloud_config_fixture=cloud_config_fixture) + self.session_fixture = self.useFixture(fixtures.MonkeyPatch( + 'os_client_config.cloud_config.CloudConfig.get_session', + mock.Mock())) diff --git a/shade/tests/unit/test_flavors.py b/shade/tests/unit/test_flavors.py index 3d8982988..3707e9c97 100644 --- a/shade/tests/unit/test_flavors.py +++ b/shade/tests/unit/test_flavors.py @@ -22,7 +22,7 @@ from shade.tests.unit import base -class TestFlavors(base.TestCase): +class TestFlavors(base.BaseTestCase): def test_create_flavor(self): self.useFixture(keystoneauth_betamax.BetamaxFixture( diff --git a/shade/tests/unit/test_floating_ip_nova.py b/shade/tests/unit/test_floating_ip_nova.py index 854b46cf4..a19fc4986 100644 --- a/shade/tests/unit/test_floating_ip_nova.py +++ b/shade/tests/unit/test_floating_ip_nova.py @@ -193,8 +193,10 @@ def test_delete_floating_ip_existing( @patch.object(OpenStackCloud, 'nova_client') @patch.object(OpenStackCloud, 'get_floating_ip') + @patch.object(OpenStackCloud, '_use_neutron_floating') def test_delete_floating_ip_not_found( - self, mock_get_floating_ip, mock_nova_client): + self, mock_use_floating, mock_get_floating_ip, mock_nova_client): + mock_use_floating.return_value = False mock_get_floating_ip.return_value = None mock_nova_client.floating_ips.delete.side_effect = n_exc.NotFound( code=404) diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 53331118b..45f503037 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -877,6 +877,26 @@ def test_az(self): cloud_name='', region_name='') self.assertEqual('az1', hostvars['az']) + def test_current_location(self): + self.assertEqual({ + 'cloud': '_test_cloud_', + 'project': { + 'id': mock.ANY, + 'name': 'admin', + 'domain_id': None, + 'domain_name': None + }, + 'region_name': u'RegionOne'}, + self.cloud.current_location) + + def test_current_project(self): + self.assertEqual({ + 'id': mock.ANY, + 'name': 'admin', + 'domain_id': None, + 'domain_name': None}, + self.cloud.current_project) + def test_has_volume(self): mock_cloud = mock.MagicMock() From 42e14bad843bc3efd2ca88a118f696459f648e80 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 26 Sep 2016 21:26:19 -0500 Subject: [PATCH 1051/3836] Normalize images We normalize a bunch of our objects now, but image support is so old we don't. It's also wonky and hand-picked between v1 and v2 as opposed to being both v1 and v2 like other objects that couldn't escape epic API breakage. Change-Id: Ie17b9888c8f5a33231c366abebb8b505fc9592e6 --- .../normalize-images-1331bea7bfffa36a.yaml | 6 +++ shade/_utils.py | 47 ++++++++++++++++++ shade/openstackcloud.py | 2 +- shade/tests/fakes.py | 14 ++++++ shade/tests/unit/test_caching.py | 49 +++++++++---------- 5 files changed, 92 insertions(+), 26 deletions(-) create mode 100644 releasenotes/notes/normalize-images-1331bea7bfffa36a.yaml diff --git a/releasenotes/notes/normalize-images-1331bea7bfffa36a.yaml b/releasenotes/notes/normalize-images-1331bea7bfffa36a.yaml new file mode 100644 index 000000000..bbe2dfb51 --- /dev/null +++ b/releasenotes/notes/normalize-images-1331bea7bfffa36a.yaml @@ -0,0 +1,6 @@ +--- +features: + - Image dicts that are returned are now normalized across glance v1 + and glance v2. Extra key/value properties are now both in the root + dict and in a properties dict. Additionally, cloud and region have + been added like they are for server. diff --git a/shade/_utils.py b/shade/_utils.py index 9a9beb1ae..9040edb02 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -30,6 +30,25 @@ from shade import meta _decorated_methods = [] +_IMAGE_FIELDS = ( + 'checksum', + 'container_format', + 'created_at', + 'disk_format', + 'file', + 'id', + 'min_disk', + 'min_ram', + 'name', + 'owner', + 'protected', + 'schema', + 'size', + 'status', + 'tags', + 'updated_at', + 'virtual_size', +) def _iterate_timeout(timeout, message, wait=2): @@ -187,6 +206,34 @@ def normalize_server(server, cloud_name, region_name): return server +def normalize_images(images, cloud): + ret = [] + for image in images: + ret.append(normalize_image(image, cloud)) + return ret + + +def normalize_image(image, cloud): + new_image = munch.Munch(location=cloud.current_location) + properties = image.pop('properties', {}) + visibility = image.pop('visibility', None) + if visibility: + is_public = (visibility == 'public') + else: + is_public = image.pop('is_public', False) + visibility = 'public' if is_public else 'private' + + for field in _IMAGE_FIELDS: + new_image[field] = image.pop(field, None) + for key, val in image.items(): + properties[key] = val + new_image[key] = val + new_image['properties'] = properties + new_image['visibility'] = visibility + new_image['is_public'] = is_public + return new_image + + def normalize_keystone_services(services): """Normalize the structure of keystone services diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 8127a8c5f..fd9186afa 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1625,7 +1625,7 @@ def list_images(self, filter_deleted=True): images.append(image) elif image.status != 'DELETED': images.append(image) - return images + return _utils.normalize_images(images, cloud=self) def list_floating_ip_pools(self): """List all available floating IP pools. diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 642d7505d..d914555dd 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -74,6 +74,20 @@ def __init__(self, id, name, status): self.id = id self.name = name self.status = status + self.checksum = '' + self.container_format = 'bare' + self.created_at = '' + self.disk_format = 'raw' + self.file = '' + self.min_disk = 0 + self.min_ram = 0 + self.owner = '' + self.protected = False + self.schema = '' + self.size = 0 + self.tags = [] + self.updated_at = '' + self.virtual_size = 0 class FakeProject(object): diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index cedcfd52a..ad19c0d50 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -101,6 +101,9 @@ def setUp(self): super(TestMemoryCache, self).setUp( cloud_config_fixture='clouds_cache.yaml') + def _image_dict(self, fake_image): + return _utils.normalize_image(meta.obj_to_dict(fake_image), self.cloud) + def test_openstack_cloud(self): self.assertIsInstance(self.cloud, shade.OpenStackCloud) @@ -302,7 +305,7 @@ def test_list_images(self, glance_mock): self.assertEqual([], self.cloud.list_images()) fake_image = fakes.FakeImage('22', '22 name', 'success') - fake_image_dict = meta.obj_to_dict(fake_image) + fake_image_dict = self._image_dict(fake_image) glance_mock.images.list.return_value = [fake_image] self.cloud.list_images.invalidate(self.cloud) self.assertEqual([fake_image_dict], self.cloud.list_images()) @@ -310,14 +313,14 @@ def test_list_images(self, glance_mock): @mock.patch.object(shade.OpenStackCloud, 'glance_client') def test_list_images_ignores_unsteady_status(self, glance_mock): steady_image = fakes.FakeImage('68', 'Jagr', 'active') - steady_image_dict = meta.obj_to_dict(steady_image) + steady_image_dict = self._image_dict(steady_image) for status in ('queued', 'saving', 'pending_delete'): active_image = fakes.FakeImage(self.getUniqueString(), self.getUniqueString(), status) glance_mock.images.list.return_value = [active_image] - active_image_dict = meta.obj_to_dict(active_image) - self.assertEqual([active_image_dict], - self.cloud.list_images()) + active_image_dict = self._image_dict(active_image) + + self.assertEqual([active_image_dict], self.cloud.list_images()) glance_mock.images.list.return_value = [active_image, steady_image] # Should expect steady_image to appear if active wasn't cached self.assertEqual([active_image_dict, steady_image_dict], @@ -330,7 +333,7 @@ def test_list_images_caches_steady_status(self, glance_mock): for status in ('active', 'deleted', 'killed'): active_image = fakes.FakeImage(self.getUniqueString(), self.getUniqueString(), status) - active_image_dict = meta.obj_to_dict(active_image) + active_image_dict = self._image_dict(active_image) if not first_image: first_image = active_image_dict glance_mock.images.list.return_value = [active_image] @@ -367,10 +370,10 @@ def test_create_image_put_v1(self, glance_mock, mock_api_version): 'owner_specified.shade.sha256': mock.ANY, 'owner_specified.shade.object': 'images/42 name', 'is_public': False}} - fake_image_dict = meta.obj_to_dict(fake_image) + fake_image_dict = self._image_dict(fake_image) glance_mock.images.create.assert_called_with(**args) glance_mock.images.update.assert_called_with( - data=mock.ANY, image=fake_image_dict) + data=mock.ANY, image=meta.obj_to_dict(fake_image)) self.assertEqual([fake_image_dict], self.cloud.list_images()) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @@ -396,7 +399,7 @@ def test_create_image_put_v2(self, glance_mock, mock_api_version): glance_mock.images.create.assert_called_with(**args) glance_mock.images.upload.assert_called_with( image_data=mock.ANY, image_id=fake_image.id) - fake_image_dict = meta.obj_to_dict(fake_image) + fake_image_dict = self._image_dict(fake_image) self.assertEqual([fake_image_dict], self.cloud.list_images()) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @@ -440,7 +443,7 @@ def test_create_image_put_user_int(self, glance_mock, mock_api_version): glance_mock.images.create.assert_called_with(**args) glance_mock.images.upload.assert_called_with( image_data=mock.ANY, image_id=fake_image.id) - fake_image_dict = meta.obj_to_dict(fake_image) + fake_image_dict = self._image_dict(fake_image) self.assertEqual([fake_image_dict], self.cloud.list_images()) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @@ -468,7 +471,7 @@ def test_create_image_put_meta_int(self, glance_mock, mock_api_version): glance_mock.images.create.assert_called_with(**args) glance_mock.images.upload.assert_called_with( image_data=mock.ANY, image_id=fake_image.id) - fake_image_dict = meta.obj_to_dict(fake_image) + fake_image_dict = self._image_dict(fake_image) self.assertEqual([fake_image_dict], self.cloud.list_images()) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @@ -496,7 +499,7 @@ def test_create_image_put_user_prop(self, glance_mock, mock_api_version): glance_mock.images.create.assert_called_with(**args) glance_mock.images.upload.assert_called_with( image_data=mock.ANY, image_id=fake_image.id) - fake_image_dict = meta.obj_to_dict(fake_image) + fake_image_dict = self._image_dict(fake_image) self.assertEqual([fake_image_dict], self.cloud.list_images()) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @@ -568,34 +571,30 @@ class Container(object): 'owner_specified.shade.object': object_path, 'image_id': 'a35e8afc-cae9-4e38-8441-2cd465f79f7b'} glance_mock.images.update.assert_called_with(**args) - fake_image_dict = meta.obj_to_dict(fake_image) + fake_image_dict = self._image_dict(fake_image) self.assertEqual([fake_image_dict], self.cloud.list_images()) @mock.patch.object(shade.OpenStackCloud, 'glance_client') def test_cache_no_cloud_name(self, glance_mock): - class FakeImage(object): - status = 'active' - name = 'None Test Image' - def __init__(self, id): - self.id = id - - fi = FakeImage(id=1) - glance_mock.images.list.return_value = [fi] self.cloud.name = None + fi = fakes.FakeImage(id=1, name='None Test Image', status='active') + fi_dict = self._image_dict(fi) + glance_mock.images.list.return_value = [fi] self.assertEqual( - meta.obj_list_to_dict([fi]), + [fi_dict], self.cloud.list_images()) # Now test that the list was cached - fi2 = FakeImage(id=2) + fi2 = fakes.FakeImage(id=2, name='None Test Image', status='active') + fi2_dict = self._image_dict(fi2) glance_mock.images.list.return_value = [fi, fi2] self.assertEqual( - meta.obj_list_to_dict([fi]), + [fi_dict], self.cloud.list_images()) # Invalidation too self.cloud.list_images.invalidate(self.cloud) self.assertEqual( - meta.obj_list_to_dict([fi, fi2]), + [fi_dict, fi2_dict], self.cloud.list_images()) From 4c17e908e80727f096524e61accca2da4bd9bb4d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 26 Sep 2016 21:27:39 -0500 Subject: [PATCH 1052/3836] Make sure we're matching image status properly Image status is totes lower case. But we're totes matching upper. Like, srrsly. Change-Id: I2856671b8e08b3657d5f86102bae30b950da0921 --- shade/openstackcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index fd9186afa..8a9f610c5 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1623,7 +1623,7 @@ def list_images(self, filter_deleted=True): # While that's cute and all, that's an implementation detail. if not filter_deleted: images.append(image) - elif image.status != 'DELETED': + elif image.status.lower() != 'deleted': images.append(image) return _utils.normalize_images(images, cloud=self) From 7f0b1de2996e73585593d9b918b4e288af0e9d5a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 3 Oct 2016 16:56:49 -0700 Subject: [PATCH 1053/3836] Start splitting normalize functions into a mixin As we add location information to all of the normalizers, we need to pass in info from the cloud object. Ultimately, that means they need access to cloud members, which means they're no longer really external functions. However, openstackcloud.py is super long as it is, so keep them in their own place by making a base class to shove them in to. Change-Id: I004e54fa26ea3745b96828100cb11dc5824416ac --- shade/_normalize.py | 50 +++++++++++++++++++++++++ shade/_utils.py | 30 --------------- shade/openstackcloud.py | 13 +++---- shade/tests/unit/test_create_server.py | 21 ++++------- shade/tests/unit/test_inventory.py | 6 +-- shade/tests/unit/test_meta.py | 15 +++----- shade/tests/unit/test_rebuild_server.py | 11 ++---- 7 files changed, 75 insertions(+), 71 deletions(-) create mode 100644 shade/_normalize.py diff --git a/shade/_normalize.py b/shade/_normalize.py new file mode 100644 index 000000000..40167ca93 --- /dev/null +++ b/shade/_normalize.py @@ -0,0 +1,50 @@ +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# Copyright (c) 2016 Red Hat, 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. + + +class Normalizer(object): + '''Mix-in class to provide the normalization functions. + + This is in a separate class just for on-disk source code organization + reasons. + ''' + + def _normalize_servers(self, servers): + # Here instead of _utils because we need access to region and cloud + # name from the cloud object + ret = [] + for server in servers: + ret.append(self._normalize_server(server)) + return ret + + def _normalize_server(self, server): + server.pop('links', None) + server['flavor'].pop('links', None) + # OpenStack can return image as a string when you've booted + # from volume + if str(server['image']) != server['image']: + server['image'].pop('links', None) + + server['region'] = self.region_name + server['cloud'] = self.name + + az = server.get('OS-EXT-AZ:availability_zone', None) + if az: + server['az'] = az + + # Ensure volumes is always in the server dict, even if empty + server['volumes'] = [] + + return server diff --git a/shade/_utils.py b/shade/_utils.py index 9040edb02..374e76854 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -176,36 +176,6 @@ def _get_entity(func, name_or_id, filters, **kwargs): return entities[0] -def normalize_servers(servers, cloud_name, region_name): - # Here instead of _utils because we need access to region and cloud - # name from the cloud object - ret = [] - for server in servers: - ret.append(normalize_server(server, cloud_name, region_name)) - return ret - - -def normalize_server(server, cloud_name, region_name): - server.pop('links', None) - server['flavor'].pop('links', None) - # OpenStack can return image as a string when you've booted - # from volume - if str(server['image']) != server['image']: - server['image'].pop('links', None) - - server['region'] = region_name - server['cloud'] = cloud_name - - az = server.get('OS-EXT-AZ:availability_zone', None) - if az: - server['az'] = az - - # Ensure volumes is always in the server dict, even if empty - server['volumes'] = [] - - return server - - def normalize_images(images, cloud): ret = [] for image in images: diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 8a9f610c5..e2de14ca0 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -48,6 +48,7 @@ from shade.exc import * # noqa from shade import _log +from shade import _normalize from shade import meta from shade import task_manager from shade import _tasks @@ -99,7 +100,7 @@ def _no_pending_stacks(stacks): return True -class OpenStackCloud(object): +class OpenStackCloud(_normalize.Normalizer): """Represent a connection to an OpenStack Cloud. OpenStackCloud is the entry point for all cloud operations, regardless @@ -1564,9 +1565,8 @@ def _list_servers(self, detailed=False): "Error fetching server list on {cloud}:{region}:".format( cloud=self.name, region=self.region_name)): - servers = _utils.normalize_servers( - self.manager.submit_task(_tasks.ServerList()), - cloud_name=self.name, region_name=self.region_name) + servers = self._normalize_servers( + self.manager.submit_task(_tasks.ServerList())) if detailed: return [ @@ -2232,9 +2232,8 @@ def get_server(self, name_or_id=None, filters=None, detailed=False): return _utils._get_entity(searchfunc, name_or_id, filters) def get_server_by_id(self, id): - return meta.add_server_interfaces(self, _utils.normalize_server( - self.manager.submit_task(_tasks.ServerGet(server=id)), - cloud_name=self.name, region_name=self.region_name)) + return meta.add_server_interfaces(self, self._normalize_server( + self.manager.submit_task(_tasks.ServerGet(server=id)))) def get_server_group(self, name_or_id=None, filters=None): """Get a server group by name or ID. diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index 31cc71ddb..799c76e35 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -21,7 +21,6 @@ from mock import patch, Mock import mock -from shade import _utils from shade import meta from shade import OpenStackCloud from shade.exc import (OpenStackCloudException, OpenStackCloudTimeout) @@ -139,10 +138,8 @@ def test_create_server_no_wait(self): } OpenStackCloud.nova_client = Mock(**config) self.assertEqual( - _utils.normalize_server( - meta.obj_to_dict(fake_server), - cloud_name=self.cloud.name, - region_name=self.cloud.region_name), + self.cloud._normalize_server( + meta.obj_to_dict(fake_server)), self.cloud.create_server( name='server-name', image=dict(id='image=id'), @@ -166,10 +163,8 @@ def test_create_server_with_admin_pass_no_wait(self): } OpenStackCloud.nova_client = Mock(**config) self.assertEqual( - _utils.normalize_server( - meta.obj_to_dict(fake_create_server), - cloud_name=self.cloud.name, - region_name=self.cloud.region_name), + self.cloud._normalize_server( + meta.obj_to_dict(fake_create_server)), self.cloud.create_server( name='server-name', image=dict(id='image=id'), flavor=dict(id='flavor-id'), admin_pass='ooBootheiX0edoh')) @@ -187,8 +182,8 @@ def test_create_server_with_admin_pass_wait(self, mock_nova, mock_wait): mock_nova.servers.create.return_value = fake_server mock_nova.servers.get.return_value = fake_server # The wait returns non-password server - mock_wait.return_value = _utils.normalize_server( - meta.obj_to_dict(fake_server), None, None) + mock_wait.return_value = self.cloud._normalize_server( + meta.obj_to_dict(fake_server)) server = self.cloud.create_server( name='server-name', image=dict(id='image-id'), @@ -201,8 +196,8 @@ def test_create_server_with_admin_pass_wait(self, mock_nova, mock_wait): # Even with the wait, we should still get back a passworded server self.assertEqual( server, - _utils.normalize_server(meta.obj_to_dict(fake_server_with_pass), - None, None) + self.cloud._normalize_server( + meta.obj_to_dict(fake_server_with_pass)) ) @patch.object(OpenStackCloud, "get_active_server") diff --git a/shade/tests/unit/test_inventory.py b/shade/tests/unit/test_inventory.py index 02e0228ca..b12d5ed3e 100644 --- a/shade/tests/unit/test_inventory.py +++ b/shade/tests/unit/test_inventory.py @@ -18,7 +18,6 @@ from os_client_config import exceptions as occ_exc -from shade import _utils from shade import exc from shade import inventory from shade import meta @@ -103,10 +102,9 @@ def test_list_hosts_no_detail(self, mock_cloud, mock_config): inv = inventory.OpenStackInventory() - server = _utils.normalize_server( + server = self.cloud._normalize_server( meta.obj_to_dict(fakes.FakeServer( - '1234', 'test', 'ACTIVE', addresses={})), - region_name='', cloud_name='') + '1234', 'test', 'ACTIVE', addresses={}))) self.assertIsInstance(inv.clouds, list) self.assertEqual(1, len(inv.clouds)) inv.clouds[0].list_servers.return_value = [server] diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 45f503037..75deebc19 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -18,7 +18,6 @@ from neutronclient.common import exceptions as neutron_exceptions import shade -from shade import _utils from shade import meta from shade.tests import fakes from shade.tests.unit import base @@ -811,17 +810,15 @@ def test_basic_hostvars( mock_get_server_external_ipv6.return_value = PUBLIC_V6 hostvars = meta.get_hostvars_from_server( - FakeCloud(), _utils.normalize_server( - meta.obj_to_dict(standard_fake_server), - cloud_name='CLOUD_NAME', - region_name='REGION_NAME')) + FakeCloud(), self.cloud._normalize_server( + meta.obj_to_dict(standard_fake_server))) self.assertNotIn('links', hostvars) self.assertEqual(PRIVATE_V4, hostvars['private_v4']) self.assertEqual(PUBLIC_V4, hostvars['public_v4']) self.assertEqual(PUBLIC_V6, hostvars['public_v6']) self.assertEqual(PUBLIC_V6, hostvars['interface_ip']) - self.assertEqual('REGION_NAME', hostvars['region']) - self.assertEqual('CLOUD_NAME', hostvars['cloud']) + self.assertEqual('RegionOne', hostvars['region']) + self.assertEqual('_test_cloud_', hostvars['cloud']) self.assertEqual("test-image-name", hostvars['image']['name']) self.assertEqual( standard_fake_server.image['id'], hostvars['image']['id']) @@ -872,9 +869,7 @@ def test_az(self): server = standard_fake_server server.__dict__['OS-EXT-AZ:availability_zone'] = 'az1' - hostvars = _utils.normalize_server( - meta.obj_to_dict(server), - cloud_name='', region_name='') + hostvars = self.cloud._normalize_server(meta.obj_to_dict(server)) self.assertEqual('az1', hostvars['az']) def test_current_location(self): diff --git a/shade/tests/unit/test_rebuild_server.py b/shade/tests/unit/test_rebuild_server.py index 509394b99..3d461d28d 100644 --- a/shade/tests/unit/test_rebuild_server.py +++ b/shade/tests/unit/test_rebuild_server.py @@ -20,7 +20,6 @@ """ from mock import patch, Mock -from shade import _utils from shade import meta from shade import OpenStackCloud from shade.exc import (OpenStackCloudException, OpenStackCloudTimeout) @@ -131,9 +130,8 @@ def test_rebuild_server_with_admin_pass_wait(self): OpenStackCloud.nova_client = Mock(**config) self.cloud.name = 'cloud-name' self.assertEqual( - _utils.normalize_server( - meta.obj_to_dict(ret_active_server), - cloud_name='cloud-name', region_name='RegionOne'), + self.cloud._normalize_server( + meta.obj_to_dict(ret_active_server)), self.cloud.rebuild_server( "1234", "b", wait=True, admin_pass='ooBootheiX0edoh')) @@ -156,7 +154,6 @@ def test_rebuild_server_wait(self): OpenStackCloud.nova_client = Mock(**config) self.cloud.name = 'cloud-name' self.assertEqual( - _utils.normalize_server( - meta.obj_to_dict(active_server), - cloud_name='cloud-name', region_name='RegionOne'), + self.cloud._normalize_server( + meta.obj_to_dict(active_server)), self.cloud.rebuild_server("1234", "b", wait=True)) From 7ed4740802e46323b00b3084ed44ad4f40746cb8 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 3 Oct 2016 17:09:58 -0700 Subject: [PATCH 1054/3836] Add location to server record Change-Id: Icbd8c2248ccd8e1b21cd46e7d291965a8ccec587 --- shade/_normalize.py | 1 + shade/tests/unit/test_meta.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/shade/_normalize.py b/shade/_normalize.py index 40167ca93..8a7572228 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -39,6 +39,7 @@ def _normalize_server(self, server): server['region'] = self.region_name server['cloud'] = self.name + server['location'] = self.current_location az = server.get('OS-EXT-AZ:availability_zone', None) if az: diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 75deebc19..035169c75 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -819,6 +819,10 @@ def test_basic_hostvars( self.assertEqual(PUBLIC_V6, hostvars['interface_ip']) self.assertEqual('RegionOne', hostvars['region']) self.assertEqual('_test_cloud_', hostvars['cloud']) + self.assertIn('location', hostvars) + self.assertEqual('_test_cloud_', hostvars['location']['cloud']) + self.assertEqual('RegionOne', hostvars['location']['region_name']) + self.assertEqual('admin', hostvars['location']['project']['name']) self.assertEqual("test-image-name", hostvars['image']['name']) self.assertEqual( standard_fake_server.image['id'], hostvars['image']['id']) From 5171f7957bcacecde2c45f9ea81e2320294ba388 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 3 Oct 2016 19:55:52 -0500 Subject: [PATCH 1055/3836] Move image normalize calls to _normalize Change-Id: I991bfef850f6f67c723c93e711afa91eecdde3c4 --- shade/_normalize.py | 47 ++++++++++++++++++++++++++++++++ shade/_utils.py | 47 -------------------------------- shade/openstackcloud.py | 2 +- shade/tests/unit/test_caching.py | 2 +- 4 files changed, 49 insertions(+), 49 deletions(-) diff --git a/shade/_normalize.py b/shade/_normalize.py index 8a7572228..3a9608eae 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -12,6 +12,27 @@ # 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 munch + +_IMAGE_FIELDS = ( + 'checksum', + 'container_format', + 'created_at', + 'disk_format', + 'file', + 'id', + 'min_disk', + 'min_ram', + 'name', + 'owner', + 'protected', + 'schema', + 'size', + 'status', + 'tags', + 'updated_at', + 'virtual_size', +) class Normalizer(object): @@ -21,6 +42,32 @@ class Normalizer(object): reasons. ''' + def _normalize_images(self, images): + ret = [] + for image in images: + ret.append(self._normalize_image(image)) + return ret + + def _normalize_image(self, image): + new_image = munch.Munch(location=self.current_location) + properties = image.pop('properties', {}) + visibility = image.pop('visibility', None) + if visibility: + is_public = (visibility == 'public') + else: + is_public = image.pop('is_public', False) + visibility = 'public' if is_public else 'private' + + for field in _IMAGE_FIELDS: + new_image[field] = image.pop(field, None) + for key, val in image.items(): + properties[key] = val + new_image[key] = val + new_image['properties'] = properties + new_image['visibility'] = visibility + new_image['is_public'] = is_public + return new_image + def _normalize_servers(self, servers): # Here instead of _utils because we need access to region and cloud # name from the cloud object diff --git a/shade/_utils.py b/shade/_utils.py index 374e76854..d701251f6 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -30,25 +30,6 @@ from shade import meta _decorated_methods = [] -_IMAGE_FIELDS = ( - 'checksum', - 'container_format', - 'created_at', - 'disk_format', - 'file', - 'id', - 'min_disk', - 'min_ram', - 'name', - 'owner', - 'protected', - 'schema', - 'size', - 'status', - 'tags', - 'updated_at', - 'virtual_size', -) def _iterate_timeout(timeout, message, wait=2): @@ -176,34 +157,6 @@ def _get_entity(func, name_or_id, filters, **kwargs): return entities[0] -def normalize_images(images, cloud): - ret = [] - for image in images: - ret.append(normalize_image(image, cloud)) - return ret - - -def normalize_image(image, cloud): - new_image = munch.Munch(location=cloud.current_location) - properties = image.pop('properties', {}) - visibility = image.pop('visibility', None) - if visibility: - is_public = (visibility == 'public') - else: - is_public = image.pop('is_public', False) - visibility = 'public' if is_public else 'private' - - for field in _IMAGE_FIELDS: - new_image[field] = image.pop(field, None) - for key, val in image.items(): - properties[key] = val - new_image[key] = val - new_image['properties'] = properties - new_image['visibility'] = visibility - new_image['is_public'] = is_public - return new_image - - def normalize_keystone_services(services): """Normalize the structure of keystone services diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index e2de14ca0..8de976e37 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1625,7 +1625,7 @@ def list_images(self, filter_deleted=True): images.append(image) elif image.status.lower() != 'deleted': images.append(image) - return _utils.normalize_images(images, cloud=self) + return self._normalize_images(images) def list_floating_ip_pools(self): """List all available floating IP pools. diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index ad19c0d50..1c28302d6 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -102,7 +102,7 @@ def setUp(self): cloud_config_fixture='clouds_cache.yaml') def _image_dict(self, fake_image): - return _utils.normalize_image(meta.obj_to_dict(fake_image), self.cloud) + return self.cloud._normalize_image(meta.obj_to_dict(fake_image)) def test_openstack_cloud(self): self.assertIsInstance(self.cloud, shade.OpenStackCloud) From b30ff5538f6b8401f06edcbd524e7944c6870f89 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 3 Oct 2016 20:07:06 -0500 Subject: [PATCH 1056/3836] Move normalize_flavors to _normalize Also, add a normalize_flavor - which makes two places of completely ridiculous looking code cease looking completely ridiculous. Change-Id: I196166a1526a01f12669ec1e5c1c4a497342fffc --- shade/_normalize.py | 28 ++++++++++++++++++++++++++++ shade/_utils.py | 23 ----------------------- shade/openstackcloud.py | 2 +- shade/operatorcloud.py | 2 +- shade/tests/unit/test_caching.py | 5 ++--- 5 files changed, 32 insertions(+), 28 deletions(-) diff --git a/shade/_normalize.py b/shade/_normalize.py index 3a9608eae..d6deaf8b7 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -42,6 +42,34 @@ class Normalizer(object): reasons. ''' + def _normalize_flavors(self, flavors): + """ Normalize a list of flavor objects """ + ret = [] + for flavor in flavors: + ret.append(self._normalize_flavor(flavor)) + return ret + + def _normalize_flavor(self, flavor): + """ Normalize a flavor object """ + flavor.pop('links', None) + flavor.pop('NAME_ATTR', None) + flavor.pop('HUMAN_ID', None) + flavor.pop('human_id', None) + if 'extra_specs' not in flavor: + flavor['extra_specs'] = {} + ephemeral = flavor.pop('OS-FLV-EXT-DATA:ephemeral', 0) + is_public = flavor.pop('os-flavor-access:is_public', True) + disabled = flavor.pop('OS-FLV-DISABLED:disabled', False) + # Make sure both the extension version and a sane version are present + flavor['OS-FLV-DISABLED:disabled'] = disabled + flavor['disabled'] = disabled + flavor['OS-FLV-EXT-DATA:ephemeral'] = ephemeral + flavor['ephemeral'] = ephemeral + flavor['os-flavor-access:is_public'] = is_public + flavor['is_public'] = is_public + + return flavor + def _normalize_images(self, images): ret = [] for image in images: diff --git a/shade/_utils.py b/shade/_utils.py index d701251f6..f4b4e6445 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -457,29 +457,6 @@ def normalize_stacks(stacks): return stacks -def normalize_flavors(flavors): - """ Normalize a list of flavor objects """ - for flavor in flavors: - flavor.pop('links', None) - flavor.pop('NAME_ATTR', None) - flavor.pop('HUMAN_ID', None) - flavor.pop('human_id', None) - if 'extra_specs' not in flavor: - flavor['extra_specs'] = {} - ephemeral = flavor.pop('OS-FLV-EXT-DATA:ephemeral', 0) - is_public = flavor.pop('os-flavor-access:is_public', True) - disabled = flavor.pop('OS-FLV-DISABLED:disabled', False) - # Make sure both the extension version and a sane version are present - flavor['OS-FLV-DISABLED:disabled'] = disabled - flavor['disabled'] = disabled - flavor['OS-FLV-EXT-DATA:ephemeral'] = ephemeral - flavor['ephemeral'] = ephemeral - flavor['os-flavor-access:is_public'] = is_public - flavor['is_public'] = is_public - - return flavors - - def normalize_cluster_templates(cluster_templates): """Normalize Magnum cluster_templates.""" for cluster_template in cluster_templates: diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 8de976e37..71077f08d 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1481,7 +1481,7 @@ def list_flavors(self, get_extra=True): 'Fetching extra specs for flavor failed:' ' {msg}'.format(msg=str(e))) - return _utils.normalize_flavors(flavors) + return self._normalize_flavors(flavors) @_utils.cache_on_arguments(should_cache_fn=_no_pending_stacks) def list_stacks(self): diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 98dc926ab..6cd825195 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1493,7 +1493,7 @@ def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", is_public=is_public) ) - return _utils.normalize_flavors([flavor])[0] + return self._normalize_flavor(flavor) def delete_flavor(self, name_or_id): """Delete a flavor diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 1c28302d6..78bbf98f3 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -292,9 +292,8 @@ def test_list_flavors(self, nova_mock, mock_compute): self.assertEqual([], self.cloud.list_flavors()) fake_flavor = fakes.FakeFlavor('555', 'vanilla', 100) - fake_flavor_dict = _utils.normalize_flavors( - [meta.obj_to_dict(fake_flavor)] - )[0] + fake_flavor_dict = self.cloud._normalize_flavor( + meta.obj_to_dict(fake_flavor)) nova_mock.flavors.list.return_value = [fake_flavor] self.cloud.list_flavors.invalidate(self.cloud) self.assertEqual([fake_flavor_dict], self.cloud.list_flavors()) From 7962efccef46b4bc3dcf223f845904891897d3e5 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 3 Oct 2016 20:11:48 -0500 Subject: [PATCH 1057/3836] Add location field to flavors Change-Id: I3273fe20154b4794513e6e012eeb4ced988daaf0 --- shade/_normalize.py | 1 + shade/tests/unit/test_flavors.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/shade/_normalize.py b/shade/_normalize.py index d6deaf8b7..6faf853b3 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -67,6 +67,7 @@ def _normalize_flavor(self, flavor): flavor['ephemeral'] = ephemeral flavor['os-flavor-access:is_public'] = is_public flavor['is_public'] = is_public + flavor['location'] = self.current_location return flavor diff --git a/shade/tests/unit/test_flavors.py b/shade/tests/unit/test_flavors.py index 3707e9c97..d78b9fc5d 100644 --- a/shade/tests/unit/test_flavors.py +++ b/shade/tests/unit/test_flavors.py @@ -22,7 +22,7 @@ from shade.tests.unit import base -class TestFlavors(base.BaseTestCase): +class TestFlavorsBetamax(base.BaseTestCase): def test_create_flavor(self): self.useFixture(keystoneauth_betamax.BetamaxFixture( @@ -55,6 +55,9 @@ def test_create_flavor(self): # delete created flavor self.full_op_cloud.delete_flavor('vanilla') + +class TestFlavors(base.TestCase): + @mock.patch.object(shade.OpenStackCloud, '_compute_client') @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_delete_flavor(self, mock_nova, mock_compute): From 9e72995e218a594019265785ea600d82064987c3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 3 Oct 2016 21:12:22 -0500 Subject: [PATCH 1058/3836] Move and fix security group normalization We've developed a different normalization pattern since we wrote the security group stuff. Also, it wasn't returning munches. Move it over to _normalize, add the tenant_id param to nova objects (sigh) Change-Id: I7a7c88034a78ec218f0f87a608fb67408a849d06 --- shade/_normalize.py | 72 ++++++++++++++++++++++++ shade/_utils.py | 43 -------------- shade/openstackcloud.py | 31 +++++----- shade/tests/unit/test__utils.py | 47 +++++++++++++--- shade/tests/unit/test_security_groups.py | 4 +- 5 files changed, 128 insertions(+), 69 deletions(-) diff --git a/shade/_normalize.py b/shade/_normalize.py index 6faf853b3..78a228c6a 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -97,6 +97,78 @@ def _normalize_image(self, image): new_image['is_public'] = is_public return new_image + def _normalize_secgroups(self, groups): + """Normalize the structure of security groups + + This makes security group dicts, as returned from nova, look like the + security group dicts as returned from neutron. This does not make them + look exactly the same, but it's pretty close. + + :param list groups: A list of security group dicts. + + :returns: A list of normalized dicts. + """ + ret = [] + for group in groups: + ret.append(self._normalize_secgroup(group)) + return ret + + def _normalize_secgroup(self, group): + + rules = group.pop('security_group_rules', None) + if not rules and 'rules' in group: + rules = group.pop('rules') + group['security_group_rules'] = self._normalize_secgroup_rules(rules) + # neutron sets these. we don't care about it, but let's be the same + project_id = group.get('project_id', group.get('tenant_id', '')) + group['tenant_id'] = project_id + group['project_id'] = project_id + group['location'] = self.current_location + return munch.Munch(group) + + def _normalize_secgroup_rules(self, rules): + """Normalize the structure of nova security group rules + + Note that nova uses -1 for non-specific port values, but neutron + represents these with None. + + :param list rules: A list of security group rule dicts. + + :returns: A list of normalized dicts. + """ + ret = [] + for rule in rules: + ret.append(self._normalize_secgroup_rule(rule)) + return ret + + def _normalize_secgroup_rule(self, rule): + ret = munch.Munch() + ret['id'] = rule['id'] + ret['location'] = self.current_location + ret['direction'] = rule.get('direction', 'ingress') + ret['ethertype'] = rule.get('ethertype', 'IPv4') + port_range_min = rule.get( + 'port_range_min', rule.get('from_port', None)) + if port_range_min == -1: + port_range_min = None + ret['port_range_min'] = port_range_min + port_range_max = rule.get( + 'port_range_max', rule.get('to_port', None)) + if port_range_max == -1: + port_range_max = None + ret['port_range_max'] = port_range_max + ret['protocol'] = rule.get('protocol', rule.get('ip_protocol')) + ret['remote_ip_prefix'] = rule.get( + 'remote_ip_prefix', rule.get('ip_range', {}).get('cidr', None)) + ret['security_group_id'] = rule.get( + 'security_group_id', rule.get('parent_group_id')) + ret['remote_group_id'] = rule.get('remote_group_id') + project_id = rule.get('project_id', rule.get('tenant_id', '')) + ret['tenant_id'] = project_id + ret['project_id'] = project_id + ret['remote_group_id'] = rule.get('remote_group_id') + return ret + def _normalize_servers(self, servers): # Here instead of _utils because we need access to region and cloud # name from the cloud object diff --git a/shade/_utils.py b/shade/_utils.py index f4b4e6445..017378202 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -182,49 +182,6 @@ def normalize_keystone_services(services): return meta.obj_list_to_dict(ret) -def normalize_nova_secgroups(groups): - """Normalize the structure of nova security groups - - This makes security group dicts, as returned from nova, look like the - security group dicts as returned from neutron. This does not make them - look exactly the same, but it's pretty close. - - :param list groups: A list of security group dicts. - - :returns: A list of normalized dicts. - """ - ret = [{'id': g['id'], - 'name': g['name'], - 'description': g['description'], - 'security_group_rules': normalize_nova_secgroup_rules(g['rules']) - } for g in groups] - return meta.obj_list_to_dict(ret) - - -def normalize_nova_secgroup_rules(rules): - """Normalize the structure of nova security group rules - - Note that nova uses -1 for non-specific port values, but neutron - represents these with None. - - :param list rules: A list of security group rule dicts. - - :returns: A list of normalized dicts. - """ - ret = [{'id': r['id'], - 'direction': 'ingress', - 'ethertype': 'IPv4', - 'port_range_min': - None if r['from_port'] == -1 else r['from_port'], - 'port_range_max': - None if r['to_port'] == -1 else r['to_port'], - 'protocol': r['ip_protocol'], - 'remote_ip_prefix': r['ip_range'].get('cidr', None), - 'security_group_id': r['parent_group_id'] - } for r in rules] - return meta.obj_list_to_dict(ret) - - def normalize_nova_floating_ips(ips): """Normalize the structure of Neutron floating IPs diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 71077f08d..b06473e5c 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1510,7 +1510,7 @@ def list_server_security_groups(self, server): groups = self.manager.submit_task( _tasks.ServerListSecurityGroups(server=server['id'])) - return _utils.normalize_nova_secgroups(groups) + return self._normalize_secgroups(groups) def list_security_groups(self): """List all available security groups. @@ -1524,12 +1524,13 @@ def list_security_groups(self): "Unavailable feature: security groups" ) + groups = [] # Handle neutron security groups if self._use_neutron_secgroups(): # Neutron returns dicts, so no need to convert objects here. with _utils.neutron_exceptions( "Error fetching security group list"): - return self.manager.submit_task( + groups = self.manager.submit_task( _tasks.NeutronSecurityGroupList())['security_groups'] # Handle nova security groups @@ -1537,7 +1538,7 @@ def list_security_groups(self): with _utils.shade_exceptions("Error fetching security group list"): groups = self.manager.submit_task( _tasks.NovaSecurityGroupList()) - return _utils.normalize_nova_secgroups(groups) + return self._normalize_secgroups(groups) def list_servers(self, detailed=False): """List all available servers. @@ -5703,6 +5704,7 @@ def create_security_group(self, name, description): "Unavailable feature: security groups" ) + group = None if self._use_neutron_secgroups(): with _utils.neutron_exceptions( "Error creating security group {0}".format(name)): @@ -5710,9 +5712,7 @@ def create_security_group(self, name, description): _tasks.NeutronSecurityGroupCreate( body=dict(security_group=dict(name=name, description=description)) - ) - ) - return group['security_group'] + ))['security_group'] else: with _utils.shade_exceptions( @@ -5723,7 +5723,7 @@ def create_security_group(self, name, description): name=name, description=description ) ) - return _utils.normalize_nova_secgroups([group])[0] + return self._normalize_secgroup(group) def delete_security_group(self, name_or_id): """Delete a security group @@ -5785,9 +5785,9 @@ def update_security_group(self, name_or_id, **kwargs): "Unavailable feature: security groups" ) - secgroup = self.get_security_group(name_or_id) + group = self.get_security_group(name_or_id) - if secgroup is None: + if group is None: raise OpenStackCloudException( "Security group %s not found." % name_or_id) @@ -5796,10 +5796,9 @@ def update_security_group(self, name_or_id, **kwargs): "Error updating security group {0}".format(name_or_id)): group = self.manager.submit_task( _tasks.NeutronSecurityGroupUpdate( - security_group=secgroup['id'], + security_group=group['id'], body={'security_group': kwargs}) - ) - return group['security_group'] + )['security_group'] else: with _utils.shade_exceptions( @@ -5807,9 +5806,9 @@ def update_security_group(self, name_or_id, **kwargs): group=name_or_id)): group = self.manager.submit_task( _tasks.NovaSecurityGroupUpdate( - group=secgroup['id'], **kwargs) + group=group['id'], **kwargs) ) - return _utils.normalize_nova_secgroups([group])[0] + return self._normalize_secgroup(group) def create_security_group_rule(self, secgroup_name_or_id, @@ -5895,7 +5894,7 @@ def create_security_group_rule(self, _tasks.NeutronSecurityGroupRuleCreate( body={'security_group_rule': rule_def}) ) - return rule['security_group_rule'] + return self._normalize_secgroup_rule(rule['security_group_rule']) else: # NOTE: Neutron accepts None for protocol. Nova does not. @@ -5938,7 +5937,7 @@ def create_security_group_rule(self, group_id=remote_group_id ) ) - return _utils.normalize_nova_secgroup_rules([rule])[0] + return self._normalize_secgroup_rule(rule) def delete_security_group_rule(self, rule_id): """Delete a security group rule diff --git a/shade/tests/unit/test__utils.py b/shade/tests/unit/test__utils.py index 3ef87fcb4..02bfffd0c 100644 --- a/shade/tests/unit/test__utils.py +++ b/shade/tests/unit/test__utils.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. +import mock import testtools from shade import _utils @@ -79,7 +80,7 @@ def test__filter_list_dict2(self): }}) self.assertEqual([el2, el3], ret) - def test_normalize_nova_secgroups(self): + def test_normalize_secgroups(self): nova_secgroup = dict( id='abc123', name='nova_secgroup', @@ -94,17 +95,38 @@ def test_normalize_nova_secgroups(self): id='abc123', name='nova_secgroup', description='A Nova security group', + tenant_id='', + project_id='', + location=dict( + region_name='RegionOne', + project=dict( + domain_name=None, + id=mock.ANY, + domain_id=None, + name='admin'), + cloud='_test_cloud_'), security_group_rules=[ dict(id='123', direction='ingress', ethertype='IPv4', port_range_min=80, port_range_max=81, protocol='tcp', - remote_ip_prefix='0.0.0.0/0', security_group_id='xyz123') + remote_ip_prefix='0.0.0.0/0', security_group_id='xyz123', + tenant_id='', + project_id='', + remote_group_id=None, + location=dict( + region_name='RegionOne', + project=dict( + domain_name=None, + id=mock.ANY, + domain_id=None, + name='admin'), + cloud='_test_cloud_')) ] ) - retval = _utils.normalize_nova_secgroups([nova_secgroup])[0] + retval = self.cloud._normalize_secgroup(nova_secgroup) self.assertEqual(expected, retval) - def test_normalize_nova_secgroups_negone_port(self): + def test_normalize_secgroups_negone_port(self): nova_secgroup = dict( id='abc123', name='nova_secgroup', @@ -115,11 +137,11 @@ def test_normalize_nova_secgroups_negone_port(self): ] ) - retval = _utils.normalize_nova_secgroups([nova_secgroup])[0] + retval = self.cloud._normalize_secgroup(nova_secgroup) self.assertIsNone(retval['security_group_rules'][0]['port_range_min']) self.assertIsNone(retval['security_group_rules'][0]['port_range_max']) - def test_normalize_nova_secgroup_rules(self): + def test_normalize_secgroup_rules(self): nova_rules = [ dict(id='123', from_port=80, to_port=81, ip_protocol='tcp', ip_range={'cidr': '0.0.0.0/0'}, parent_group_id='xyz123') @@ -127,9 +149,18 @@ def test_normalize_nova_secgroup_rules(self): expected = [ dict(id='123', direction='ingress', ethertype='IPv4', port_range_min=80, port_range_max=81, protocol='tcp', - remote_ip_prefix='0.0.0.0/0', security_group_id='xyz123') + remote_ip_prefix='0.0.0.0/0', security_group_id='xyz123', + tenant_id='', project_id='', remote_group_id=None, + location=dict( + region_name='RegionOne', + project=dict( + domain_name=None, + id=mock.ANY, + domain_id=None, + name='admin'), + cloud='_test_cloud_')) ] - retval = _utils.normalize_nova_secgroup_rules(nova_rules) + retval = self.cloud._normalize_secgroup_rules(nova_rules) self.assertEqual(expected, retval) def test_normalize_volumes_v1(self): diff --git a/shade/tests/unit/test_security_groups.py b/shade/tests/unit/test_security_groups.py index 9bfc1946c..f36882df5 100644 --- a/shade/tests/unit/test_security_groups.py +++ b/shade/tests/unit/test_security_groups.py @@ -352,9 +352,9 @@ def test_nova_egress_security_group_rule(self, mock_nova): secgroup_name_or_id='nova-sec-group', direction='egress') - @mock.patch.object(shade._utils, 'normalize_nova_secgroups') + @mock.patch.object(shade.OpenStackCloud, '_normalize_secgroups') @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_list_server_security_groups(self, mock_nova, mock_norm): + def test_list_server_security_groups_nova(self, mock_nova, mock_norm): self.has_neutron = False server = dict(id='server_id') self.cloud.list_server_security_groups(server) From 4bad936afcfd3e9d29722e4b3badcebfa462c3d3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 9 Oct 2016 06:06:04 -0500 Subject: [PATCH 1059/3836] Update location info to include object owner Many OpenStack objects (but not all, grrr) return information about the project that owns the resource. When we add location info to an object record, that's the information we care about. Update current_location so that it wraps a private method that allows explicit passing in of a project_id to override the token's project_id. If the project_id and the token's project_id are the same, we can continue to pull out project and domain name information for readability. Change-Id: I2a7e6003a2b75894907f90c9ab94244a769a892f --- shade/_normalize.py | 14 ++++++++------ shade/openstackcloud.py | 36 +++++++++++++++++++++++++++++------- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/shade/_normalize.py b/shade/_normalize.py index 78a228c6a..94cccf9da 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -78,7 +78,8 @@ def _normalize_images(self, images): return ret def _normalize_image(self, image): - new_image = munch.Munch(location=self.current_location) + new_image = munch.Munch( + location=self._get_current_location(project_id=image.get('owner'))) properties = image.pop('properties', {}) visibility = image.pop('visibility', None) if visibility: @@ -119,11 +120,11 @@ def _normalize_secgroup(self, group): if not rules and 'rules' in group: rules = group.pop('rules') group['security_group_rules'] = self._normalize_secgroup_rules(rules) - # neutron sets these. we don't care about it, but let's be the same project_id = group.get('project_id', group.get('tenant_id', '')) + group['location'] = self._get_current_location(project_id=project_id) + # neutron sets these. we don't care about it, but let's be the same group['tenant_id'] = project_id group['project_id'] = project_id - group['location'] = self.current_location return munch.Munch(group) def _normalize_secgroup_rules(self, rules): @@ -144,7 +145,6 @@ def _normalize_secgroup_rules(self, rules): def _normalize_secgroup_rule(self, rule): ret = munch.Munch() ret['id'] = rule['id'] - ret['location'] = self.current_location ret['direction'] = rule.get('direction', 'ingress') ret['ethertype'] = rule.get('ethertype', 'IPv4') port_range_min = rule.get( @@ -164,9 +164,10 @@ def _normalize_secgroup_rule(self, rule): 'security_group_id', rule.get('parent_group_id')) ret['remote_group_id'] = rule.get('remote_group_id') project_id = rule.get('project_id', rule.get('tenant_id', '')) + ret['location'] = self._get_current_location(project_id=project_id) + # neutron sets these. we don't care about it, but let's be the same ret['tenant_id'] = project_id ret['project_id'] = project_id - ret['remote_group_id'] = rule.get('remote_group_id') return ret def _normalize_servers(self, servers): @@ -187,7 +188,8 @@ def _normalize_server(self, server): server['region'] = self.region_name server['cloud'] = self.name - server['location'] = self.current_location + server['location'] = self._get_current_location( + project_id=server.get('tenant_id')) az = server.get('OS-EXT-AZ:availability_zone', None) if az: diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index b06473e5c..5df52d89d 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -416,21 +416,43 @@ def current_project_id(self): @property def current_project(self): '''Return a ``munch.Munch`` describing the current project''' - auth_args = self.cloud_config.config.get('auth', {}) - return munch.Munch( - id=self.current_project_id, - name=auth_args.get('project_name'), - domain_id=auth_args.get('domain_id'), - domain_name=auth_args.get('domain_name'), + return self._get_project_info() + + def _get_project_info(self, project_id=None): + project_info = munch.Munch( + id=project_id, + name=None, + domain_id=None, + domain_name=None, ) + if not project_id or project_id == self.current_project_id: + # If we don't have a project_id parameter, it means a user is + # directly asking what the current state is. + # Alternately, if we have one, that means we're calling this + # from within a normalize function, which means the object has + # a project_id associated with it. If the project_id matches + # the project_id of our current token, that means we can supplement + # the info with human readable info about names if we have them. + # If they don't match, that means we're an admin who has pulled + # an object from a different project, so adding info from the + # current token would be wrong. + auth_args = self.cloud_config.config.get('auth', {}) + project_info['id'] = self.current_project_id + project_info['name'] = auth_args.get('project_name') + project_info['domain_id'] = auth_args.get('project_domain_id') + project_info['domain_name'] = auth_args.get('project_domain_name') + return project_info @property def current_location(self): '''Return a ``munch.Munch`` explaining the current cloud location.''' + return self._get_current_location() + + def _get_current_location(self, project_id=None): return munch.Munch( cloud=self.name, region_name=self.region_name, - project=self.current_project, + project=self._get_project_info(project_id), ) @property From d4301046c0ecd9eb5d62eeedcaa9c1a9a729737c Mon Sep 17 00:00:00 2001 From: Matt Fischer Date: Mon, 10 Oct 2016 17:41:29 -0600 Subject: [PATCH 1060/3836] Allow boolean values to pass through to glance When uploading an image it is necessary to set flags such as protected=True. Currently the code blindly converts everything to a string leading to errors. This change will allow booleans to pass through unmolested. Change-Id: Ib0e230cf9c52500973ac2e7636f3dc607c1e8089 --- shade/openstackcloud.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 95f961f34..f80e1c1a4 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2931,6 +2931,8 @@ def _make_v2_image_params(self, meta, properties): else: if v is None: ret[k] = None + elif isinstance(v, bool): + ret[k] = v else: ret[k] = str(v) ret.update(meta) From bbc8816e70fb5e920852363460ceaa051ec774ae Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Mon, 10 Oct 2016 10:28:59 +0000 Subject: [PATCH 1061/3836] Add description field to create_user method Change-Id: I3444502c61de3931b0bd2623373be927682ceacb --- ...scription_create_user-0ddc9a0ef4da840d.yaml | 3 +++ shade/_utils.py | 1 + shade/openstackcloud.py | 18 ++++++++++++++---- shade/tests/fakes.py | 3 ++- shade/tests/unit/test_users.py | 16 +++++++++++----- 5 files changed, 31 insertions(+), 10 deletions(-) create mode 100644 releasenotes/notes/add_description_create_user-0ddc9a0ef4da840d.yaml diff --git a/releasenotes/notes/add_description_create_user-0ddc9a0ef4da840d.yaml b/releasenotes/notes/add_description_create_user-0ddc9a0ef4da840d.yaml new file mode 100644 index 000000000..98dd190bf --- /dev/null +++ b/releasenotes/notes/add_description_create_user-0ddc9a0ef4da840d.yaml @@ -0,0 +1,3 @@ +--- +features: + - Add description parameter to create_user, available on Keystone v3 diff --git a/shade/_utils.py b/shade/_utils.py index 9a9beb1ae..ea7a7acbd 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -365,6 +365,7 @@ def normalize_users(users): user.get('tenantId')), domain_id=user.get('domain_id'), enabled=user.get('enabled'), + description=user.get('description') ) for user in users ] return meta.obj_list_to_dict(ret) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 95f961f34..12aa82c49 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -714,15 +714,25 @@ def update_user(self, name_or_id, **kwargs): def create_user( self, name, password=None, email=None, default_project=None, - enabled=True, domain_id=None): + enabled=True, domain_id=None, description=None): """Create a user.""" with _utils.shade_exceptions("Error in creating user {user}".format( user=name)): identity_params = self._get_identity_params( domain_id, default_project) - user = self.manager.submit_task(_tasks.UserCreate( - name=name, password=password, email=email, - enabled=enabled, **identity_params)) + if self.cloud_config.get_api_version('identity') != '3': + if description is not None: + self.log.info( + "description parameter is not supported on Keystone v2" + ) + user = self.manager.submit_task(_tasks.UserCreate( + name=name, password=password, email=email, + enabled=enabled, **identity_params)) + else: + user = self.manager.submit_task(_tasks.UserCreate( + name=name, password=password, email=email, + enabled=enabled, description=description, + **identity_params)) self.list_users.invalidate(self) return _utils.normalize_users([user])[0] diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 642d7505d..33ddc4ecd 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -133,10 +133,11 @@ def __init__(self, id, name, type, service_type, description='', class FakeUser(object): - def __init__(self, id, email, name, domain_id=None): + def __init__(self, id, email, name, domain_id=None, description=None): self.id = id self.email = email self.name = name + self.description = description if domain_id is not None: self.domain_id = domain_id diff --git a/shade/tests/unit/test_users.py b/shade/tests/unit/test_users.py index 2c8fb67e6..7d4f9121e 100644 --- a/shade/tests/unit/test_users.py +++ b/shade/tests/unit/test_users.py @@ -36,9 +36,11 @@ def test_create_user_v2(self, mock_keystone, mock_api_version): fake_user = fakes.FakeUser('1', email, name) mock_keystone.users.create.return_value = fake_user user = self.op_cloud.create_user( - name=name, email=email, password=password) + name=name, email=email, password=password, + ) mock_keystone.users.create.assert_called_once_with( - name=name, password=password, email=email, enabled=True, + name=name, password=password, email=email, + enabled=True, ) self.assertEqual(name, user.name) self.assertEqual(email, user.email) @@ -51,18 +53,22 @@ def test_create_user_v3(self, mock_keystone, mock_api_version): email = 'mickey@disney.com' password = 'mice-rule' domain_id = '456' - fake_user = fakes.FakeUser('1', email, name) + description = 'fake-description' + fake_user = fakes.FakeUser('1', email, name, description=description) mock_keystone.users.create.return_value = fake_user user = self.op_cloud.create_user( name=name, email=email, password=password, + description=description, domain_id=domain_id) mock_keystone.users.create.assert_called_once_with( - name=name, password=password, email=email, enabled=True, + name=name, password=password, email=email, + description=description, enabled=True, domain=domain_id ) self.assertEqual(name, user.name) self.assertEqual(email, user.email) + self.assertEqual(description, user.description) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, 'keystone_client') @@ -72,7 +78,7 @@ def test_update_user_password_v2(self, mock_keystone, mock_api_version): email = 'mickey@disney.com' password = 'mice-rule' domain_id = '1' - user = {'id': '1', 'name': name, 'email': email} + user = {'id': '1', 'name': name, 'email': email, 'description': None} fake_user = fakes.FakeUser(**user) munch_fake_user = munch.Munch(user) mock_keystone.users.list.return_value = [fake_user] From 6623be208dc009cd1667c7d5db0b9df89ea3a80f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 12 Oct 2016 15:05:02 -0500 Subject: [PATCH 1062/3836] Revert "Split auth plugin loading into its own method" This reverts commit 8b7859e21e64027d20f158737bbf70bbe409b847. python-openstackclient has a subclass that defines this method with a different signature. Change-Id: Ie44f8efb6b93dc0d4754fb316ddb9087ce181275 --- os_client_config/config.py | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 01f659e49..5ab0d907e 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -1035,24 +1035,6 @@ def magic_fixes(self, config): return config - def load_auth_plugin(self, config, cloud): - try: - loader = self._get_auth_loader(config) - config = self._validate_auth(config, loader) - auth_plugin = loader.load_from_options(**config['auth']) - except Exception as e: - # We WANT the ksa exception normally - # but OSC can't handle it right now, so we try deferring - # to ksc. If that ALSO fails, it means there is likely - # a deeper issue, so we assume the ksa error was correct - self.log.debug("Deferring keystone exception: {e}".format(e=e)) - auth_plugin = None - try: - config = self._validate_auth_ksc(config, cloud) - except Exception: - raise e - return auth_plugin - def get_one_cloud(self, cloud=None, validate=True, argparse=None, **kwargs): """Retrieve a single cloud configuration and merge additional options @@ -1111,7 +1093,21 @@ def get_one_cloud(self, cloud=None, validate=True, config = self.auth_config_hook(config) if validate: - auth_plugin = self.load_auth_plugin(config, cloud) + try: + loader = self._get_auth_loader(config) + config = self._validate_auth(config, loader) + auth_plugin = loader.load_from_options(**config['auth']) + except Exception as e: + # We WANT the ksa exception normally + # but OSC can't handle it right now, so we try deferring + # to ksc. If that ALSO fails, it means there is likely + # a deeper issue, so we assume the ksa error was correct + self.log.debug("Deferring keystone exception: {e}".format(e=e)) + auth_plugin = None + try: + config = self._validate_auth_ksc(config, cloud) + except Exception: + raise e else: auth_plugin = None From e27516fa1cb754062663b26c0ba58cd60e471d5b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 13 Oct 2016 17:13:45 -0500 Subject: [PATCH 1063/3836] Delete image if we timeout waiting for it to upload In other places where we have a timeout waiting for a resource creation, we delete the half-finished resource. Do the same here. Change-Id: I6503847a7cde39d3a624c6b0f9ffe9a6fa265a26 --- shade/openstackcloud.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index f9dff4bed..21b5b30a1 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3044,12 +3044,18 @@ def _upload_image_put( self._get_cache(None).invalidate() if not wait: return image - for count in _utils._iterate_timeout( - 60, - "Timeout waiting for the image to finish."): - image_obj = self.get_image(image.id) - if image_obj and image_obj.status not in ('queued', 'saving'): - return image_obj + try: + for count in _utils._iterate_timeout( + 60, + "Timeout waiting for the image to finish."): + image_obj = self.get_image(image.id) + if image_obj and image_obj.status not in ('queued', 'saving'): + return image_obj + except OpenStackCloudTimeout: + self.log.debug( + "Timeout waiting for image to become ready. Deleting.") + self.delete_image(image.id, wait=True) + raise def _upload_image_task( self, name, filename, container, current_image, From efdffe4676f0b479eee78201fcff088362fe2719 Mon Sep 17 00:00:00 2001 From: Jordan Pittier Date: Thu, 13 Oct 2016 12:44:40 +0200 Subject: [PATCH 1064/3836] Move normalize_neutron_floating_ips to _normalize Also return location, project_id and tenant_id properties for each FIP. It helps when listing floating IPs as a cloud admin, IPs from all tenants are returned. Change-Id: Ie51f35451df620b250400378142ec23216113199 --- shade/_normalize.py | 51 ++++++++++++++++++++ shade/_utils.py | 47 ------------------ shade/openstackcloud.py | 6 +-- shade/tests/unit/test_floating_ip_neutron.py | 22 ++++++--- 4 files changed, 70 insertions(+), 56 deletions(-) diff --git a/shade/_normalize.py b/shade/_normalize.py index 94cccf9da..51b4f86c8 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -199,3 +199,54 @@ def _normalize_server(self, server): server['volumes'] = [] return server + + def _normalize_neutron_floating_ips(self, ips): + """Normalize the structure of Neutron floating IPs + + Unfortunately, not all the Neutron floating_ip attributes are available + with Nova and not all Nova floating_ip attributes are available with + Neutron. + This function extract attributes that are common to Nova and Neutron + floating IP resource. + If the whole structure is needed inside shade, shade provides private + methods that returns "original" objects (e.g. + _neutron_allocate_floating_ip) + + :param list ips: A list of Neutron floating IPs. + + :returns: + A list of normalized dicts with the following attributes:: + + [ + { + "id": "this-is-a-floating-ip-id", + "fixed_ip_address": "192.0.2.10", + "floating_ip_address": "198.51.100.10", + "network": "this-is-a-net-or-pool-id", + "attached": True, + "status": "ACTIVE" + }, ... + ] + + """ + return [ + self._normalize_neutron_floating_ip(ip) for ip in ips + ] + + def _normalize_neutron_floating_ip(self, ip): + network_id = ip.get('floating_network_id', ip.get('network')) + project_id = ip.get('project_id', ip.get('tenant_id', '')) + return munch.Munch( + attached=ip.get('port_id') is not None and ip.get('port_id') != '', + fixed_ip_address=ip.get('fixed_ip_address'), + floating_ip_address=ip['floating_ip_address'], + floating_network_id=network_id, + id=ip['id'], + location=self._get_current_location(project_id=project_id), + network=network_id, + port_id=ip.get('port_id'), + project_id=project_id, + router_id=ip.get('router_id'), + status=ip.get('status', 'UNKNOWN'), + tenant_id=project_id + ) diff --git a/shade/_utils.py b/shade/_utils.py index 017378202..e98e26fa2 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -223,53 +223,6 @@ def normalize_nova_floating_ips(ips): return meta.obj_list_to_dict(ret) -def normalize_neutron_floating_ips(ips): - """Normalize the structure of Neutron floating IPs - - Unfortunately, not all the Neutron floating_ip attributes are available - with Nova and not all Nova floating_ip attributes are available with - Neutron. - This function extract attributes that are common to Nova and Neutron - floating IP resource. - If the whole structure is needed inside shade, shade provides private - methods that returns "original" objects (e.g. - _neutron_allocate_floating_ip) - - :param list ips: A list of Neutron floating IPs. - - :returns: - A list of normalized dicts with the following attributes:: - - [ - { - "id": "this-is-a-floating-ip-id", - "fixed_ip_address": "192.0.2.10", - "floating_ip_address": "198.51.100.10", - "network": "this-is-a-net-or-pool-id", - "attached": True, - "status": "ACTIVE" - }, ... - ] - - """ - ret = [] - for ip in ips: - network_id = ip.get('floating_network_id', ip.get('network')) - ret.append(dict( - id=ip['id'], - fixed_ip_address=ip.get('fixed_ip_address'), - floating_ip_address=ip['floating_ip_address'], - network=network_id, - floating_network_id=network_id, - port_id=ip.get('port_id'), - router_id=ip.get('router_id'), - attached=(ip.get('port_id') is not None and - ip.get('port_id') != ''), - status=ip.get('status', 'UNKNOWN') - )) - return meta.obj_list_to_dict(ret) - - def localhost_supports_ipv6(): """Determine whether the local host supports IPv6 diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index f9dff4bed..a09b02a11 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1666,7 +1666,7 @@ def list_floating_ip_pools(self): def _list_floating_ips(self): if self._use_neutron_floating(): try: - return _utils.normalize_neutron_floating_ips( + return self._normalize_neutron_floating_ips( self._neutron_list_floating_ips()) except OpenStackCloudURINotFound as e: self.log.debug( @@ -3606,7 +3606,7 @@ def available_floating_ip(self, network=None, server=None): """ if self._use_neutron_floating(): try: - f_ips = _utils.normalize_neutron_floating_ips( + f_ips = self._normalize_neutron_floating_ips( self._neutron_available_floating_ips( network=network, server=server)) return f_ips[0] @@ -3786,7 +3786,7 @@ def create_floating_ip(self, network=None, server=None, def _submit_create_fip(self, kwargs): # Split into a method to aid in test mocking - return _utils.normalize_neutron_floating_ips( + return self._normalize_neutron_floating_ips( [self.manager.submit_task(_tasks.NeutronFloatingIPCreate( body={'floatingip': kwargs}))['floatingip']])[0] diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 34d1083bb..779cf1324 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -24,6 +24,7 @@ from neutronclient.common import exceptions as n_exc +from shade import _normalize from shade import _utils from shade import exc from shade import meta @@ -147,7 +148,7 @@ def setUp(self): u'version': 4, u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:ae:7d:42'}]})) - self.floating_ip = _utils.normalize_neutron_floating_ips( + self.floating_ip = self.cloud._normalize_neutron_floating_ips( self.mock_floating_ip_list_rep['floatingips'])[0] def test_float_no_status(self): @@ -162,7 +163,7 @@ def test_float_no_status(self): 'tenant_id': '4969c491a3c74ee4af974e6d800c62df' } ] - normalized = _utils.normalize_neutron_floating_ips(floating_ips) + normalized = self.cloud._normalize_neutron_floating_ips(floating_ips) self.assertEqual('UNKNOWN', normalized[0]['status']) @patch.object(OpenStackCloud, 'neutron_client') @@ -207,6 +208,15 @@ def test_get_floating_ip(self, mock_has_service, mock_neutron_client): mock_neutron_client.list_floatingips.assert_called_with() self.assertIsInstance(floating_ip, dict) self.assertEqual('203.0.113.29', floating_ip['floating_ip_address']) + self.assertEqual( + self.mock_floating_ip_list_rep['floatingips'][0]['tenant_id'], + floating_ip['project_id'] + ) + self.assertEqual( + self.mock_floating_ip_list_rep['floatingips'][0]['tenant_id'], + floating_ip['tenant_id'] + ) + self.assertIn('location', floating_ip) @patch.object(OpenStackCloud, 'neutron_client') @patch.object(OpenStackCloud, 'has_service') @@ -277,7 +287,7 @@ def test_create_floating_ip_port( self.mock_floating_ip_new_rep['floatingip']['floating_ip_address'], ip['floating_ip_address']) - @patch.object(_utils, 'normalize_neutron_floating_ips') + @patch.object(_normalize.Normalizer, '_normalize_neutron_floating_ips') @patch.object(OpenStackCloud, '_neutron_available_floating_ips') @patch.object(OpenStackCloud, 'has_service') @patch.object(OpenStackCloud, 'keystone_session') @@ -402,7 +412,7 @@ def test_auto_ip_pool_no_reuse( mock_keystone_session, mock_nova_client): mock_has_service.return_value = True - fip = _utils.normalize_neutron_floating_ips( + fip = self.cloud._normalize_neutron_floating_ips( self.mock_floating_ip_list_rep['floatingips'])[0] mock__neutron_create_floating_ip.return_value = fip mock_keystone_session.get_project_id.return_value = \ @@ -561,7 +571,7 @@ def test_detach_ip_from_server( mock_get_floating_ip): mock_has_service.return_value = True mock_get_floating_ip.return_value = \ - _utils.normalize_neutron_floating_ips( + self.cloud._normalize_neutron_floating_ips( self.mock_floating_ip_list_rep['floatingips'])[0] self.cloud.detach_ip_from_server( @@ -585,7 +595,7 @@ def test_add_ip_from_pool( mock_attach_ip_to_server): mock_has_service.return_value = True mock_available_floating_ip.return_value = \ - _utils.normalize_neutron_floating_ips([ + self.cloud._normalize_neutron_floating_ips([ self.mock_floating_ip_new_rep['floatingip']])[0] mock_attach_ip_to_server.return_value = self.fake_server From 6806fcfbbe0e996d7d0a6ad82663c13821fdf77d Mon Sep 17 00:00:00 2001 From: Jordan Pittier Date: Fri, 7 Oct 2016 16:40:09 +0200 Subject: [PATCH 1065/3836] Implement create/get/list/delete volume backups Code should me mostly straightforward. Things are done for backups as they are done for snapshots. Change-Id: I450356274916e9abef80f270b8a15f94fac6692b --- ...lume_backups_support-6f7ceab440853833.yaml | 4 + shade/_tasks.py | 15 +++ shade/openstackcloud.py | 115 ++++++++++++++++++ shade/tests/base.py | 7 +- shade/tests/functional/test_volume_backup.py | 72 +++++++++++ shade/tests/unit/test_volume_backups.py | 72 +++++++++++ 6 files changed, 283 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/cinder_volume_backups_support-6f7ceab440853833.yaml create mode 100644 shade/tests/functional/test_volume_backup.py create mode 100644 shade/tests/unit/test_volume_backups.py diff --git a/releasenotes/notes/cinder_volume_backups_support-6f7ceab440853833.yaml b/releasenotes/notes/cinder_volume_backups_support-6f7ceab440853833.yaml new file mode 100644 index 000000000..380b653f4 --- /dev/null +++ b/releasenotes/notes/cinder_volume_backups_support-6f7ceab440853833.yaml @@ -0,0 +1,4 @@ +--- +features: + - Add support for Cinder volume backup resources, with the + usual methods (search/list/get/create/delete). diff --git a/shade/_tasks.py b/shade/_tasks.py index 09f1c78b6..5b894e73d 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -409,6 +409,21 @@ def main(self, client): return client.cinder_client.volume_snapshots.list(**self.args) +class VolumeBackupList(task_manager.Task): + def main(self, client): + return client.cinder_client.backups.list(**self.args) + + +class VolumeBackupCreate(task_manager.Task): + def main(self, client): + return client.cinder_client.backups.create(**self.args) + + +class VolumeBackupDelete(task_manager.Task): + def main(self, client): + return client.cinder_client.backups.delete(**self.args) + + class VolumeSnapshotDelete(task_manager.Task): def main(self, client): return client.cinder_client.volume_snapshots.delete(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index f9dff4bed..94e85ba96 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1321,6 +1321,11 @@ def search_volume_snapshots(self, name_or_id=None, filters=None): return _utils._filter_list( volumesnapshots, name_or_id, filters) + def search_volume_backups(self, name_or_id=None, filters=None): + volume_backups = self.list_volume_backups() + return _utils._filter_list( + volume_backups, name_or_id, filters) + def search_flavors(self, name_or_id=None, filters=None, get_extra=True): flavors = self.list_flavors(get_extra=get_extra) return _utils._filter_list(flavors, name_or_id, filters) @@ -3520,6 +3525,63 @@ def get_volume_snapshot(self, name_or_id, filters=None): return _utils._get_entity(self.search_volume_snapshots, name_or_id, filters) + def create_volume_backup(self, volume_id, name=None, description=None, + force=False, wait=True, timeout=None): + """Create a volume backup. + + :param volume_id: the id of the volume to backup. + :param name: name of the backup, one will be generated if one is + not provided + :param description: description of the backup, one will be generated + if one is not provided + :param force: If set to True the backup will be created even if the + volume is attached to an instance, if False it will not + :param wait: If true, waits for volume backup to be created. + :param timeout: Seconds to wait for volume backup creation. None is + forever. + + :returns: The created volume backup object. + + :raises: OpenStackCloudTimeout if wait time exceeded. + :raises: OpenStackCloudException on operation error. + """ + with _utils.shade_exceptions( + "Error creating backup of volume {volume_id}".format( + volume_id=volume_id)): + backup = self.manager.submit_task( + _tasks.VolumeBackupCreate( + volume_id=volume_id, name=name, description=description, + force=force + ) + ) + + if wait: + backup_id = backup['id'] + msg = ("Timeout waiting for the volume backup {} to be " + "available".format(backup_id)) + for _ in _utils._iterate_timeout(timeout, msg): + backup = self.get_volume_backup(backup_id) + + if backup['status'] == 'available': + break + + if backup['status'] == 'error': + msg = ("Error in creating volume " + "backup {}, please check logs".format(backup_id)) + raise OpenStackCloudException(msg) + + return backup + + def get_volume_backup(self, name_or_id, filters=None): + """Get a volume backup by name or ID. + + :returns: A backup ``munch.Munch`` or None if no matching backup is + found. + + """ + return _utils._get_entity(self.search_volume_backups, name_or_id, + filters) + def list_volume_snapshots(self, detailed=True, search_opts=None): """List all volume snapshots. @@ -3532,6 +3594,59 @@ def list_volume_snapshots(self, detailed=True, search_opts=None): _tasks.VolumeSnapshotList( detailed=detailed, search_opts=search_opts))) + def list_volume_backups(self, detailed=True, search_opts=None): + """ + List all volume backups. + + :param bool detailed: Also list details for each entry + :param dict search_opts: Search options + A dictionary of meta data to use for further filtering. Example:: + { + 'name': 'my-volume-backup', + 'status': 'available', + 'volume_id': 'e126044c-7b4c-43be-a32a-c9cbbc9ddb56', + 'all_tenants': 1 + } + :returns: A list of volume backups ``munch.Munch``. + """ + with _utils.shade_exceptions("Error getting a list of backups"): + return self.manager.submit_task( + _tasks.VolumeBackupList( + detailed=detailed, search_opts=search_opts)) + + def delete_volume_backup(self, name_or_id=None, force=False, wait=False, + timeout=None): + """Delete a volume backup. + + :param name_or_id: Name or unique ID of the volume backup. + :param force: Allow delete in state other than error or available. + :param wait: If true, waits for volume backup to be deleted. + :param timeout: Seconds to wait for volume backup deletion. None is + forever. + + :raises: OpenStackCloudTimeout if wait time exceeded. + :raises: OpenStackCloudException on operation error. + """ + + volume_backup = self.get_volume_backup(name_or_id) + + if not volume_backup: + return False + + with _utils.shade_exceptions("Error in deleting volume backup"): + self.manager.submit_task( + _tasks.VolumeBackupDelete( + backup=volume_backup['id'], force=force + ) + ) + if wait: + msg = "Timeout waiting for the volume backup to be deleted." + for count in _utils._iterate_timeout(timeout, msg): + if not self.get_volume_backup(volume_backup['id']): + break + + return True + def delete_volume_snapshot(self, name_or_id=None, wait=False, timeout=None): """Delete a volume snapshot. diff --git a/shade/tests/base.py b/shade/tests/base.py index 382006ca1..3de6f0bd1 100644 --- a/shade/tests/base.py +++ b/shade/tests/base.py @@ -29,13 +29,16 @@ class TestCase(testtools.TestCase): """Test case base class for all tests.""" + # A way to adjust slow test classes + TIMEOUT_SCALING_FACTOR = 1.0 + def setUp(self): """Run before each test method to initialize test environment.""" super(TestCase, self).setUp() - test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0) + test_timeout = int(os.environ.get('OS_TEST_TIMEOUT', 0)) try: - test_timeout = int(test_timeout) + test_timeout = int(test_timeout * self.TIMEOUT_SCALING_FACTOR) except ValueError: # If timeout value is invalid do not set a timeout. test_timeout = 0 diff --git a/shade/tests/functional/test_volume_backup.py b/shade/tests/functional/test_volume_backup.py new file mode 100644 index 000000000..5ea4ef0fd --- /dev/null +++ b/shade/tests/functional/test_volume_backup.py @@ -0,0 +1,72 @@ +# -*- 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. +from shade.tests.functional import base + + +class TestVolume(base.BaseFunctionalTestCase): + # Creating a volume backup is incredibly slow. + TIMEOUT_SCALING_FACTOR = 1.5 + + def setUp(self): + super(TestVolume, self).setUp() + if not self.demo_cloud.has_service('volume'): + self.skipTest('volume service not supported by cloud') + + def test_create_get_delete_volume_backup(self): + volume = self.demo_cloud.create_volume( + display_name=self.getUniqueString(), size=1) + self.addCleanup(self.demo_cloud.delete_volume, volume['id']) + + backup_name_1 = self.getUniqueString() + backup_desc_1 = self.getUniqueString() + backup = self.demo_cloud.create_volume_backup( + volume_id=volume['id'], name=backup_name_1, + description=backup_desc_1, wait=True) + self.assertEqual(backup_name_1, backup['name']) + + backup = self.demo_cloud.get_volume_backup(backup['id']) + self.assertEqual("available", backup['status']) + self.assertEqual(backup_desc_1, backup['description']) + + self.demo_cloud.delete_volume_backup(backup['id'], wait=True) + self.assertIsNone(self.demo_cloud.get_volume_backup(backup['id'])) + + def test_list_volume_backups(self): + vol1 = self.demo_cloud.create_volume( + display_name=self.getUniqueString(), size=1) + self.addCleanup(self.demo_cloud.delete_volume, vol1['id']) + + # We create 2 volumes to create 2 backups. We could have created 2 + # backups from the same volume but taking 2 successive backups seems + # to be race-condition prone. And I didn't want to use an ugly sleep() + # here. + vol2 = self.demo_cloud.create_volume( + display_name=self.getUniqueString(), size=1) + self.addCleanup(self.demo_cloud.delete_volume, vol2['id']) + + backup_name_1 = self.getUniqueString() + backup = self.demo_cloud.create_volume_backup( + volume_id=vol1['id'], name=backup_name_1) + self.addCleanup(self.demo_cloud.delete_volume_backup, backup['id']) + + backup = self.demo_cloud.create_volume_backup(volume_id=vol2['id']) + self.addCleanup(self.demo_cloud.delete_volume_backup, backup['id']) + + backups = self.demo_cloud.list_volume_backups() + self.assertEqual(2, len(backups)) + + backups = self.demo_cloud.list_volume_backups( + search_opts={"name": backup_name_1}) + self.assertEqual(1, len(backups)) + self.assertEqual(backup_name_1, backups[0]['name']) diff --git a/shade/tests/unit/test_volume_backups.py b/shade/tests/unit/test_volume_backups.py new file mode 100644 index 000000000..4121ad462 --- /dev/null +++ b/shade/tests/unit/test_volume_backups.py @@ -0,0 +1,72 @@ +# 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 shade +from shade.tests.unit import base + + +class TestVolumeBackups(base.TestCase): + @mock.patch.object(shade.OpenStackCloud, 'list_volume_backups') + @mock.patch("shade._utils._filter_list") + def test_search_volume_backups(self, m_filter_list, m_list_volume_backups): + result = self.cloud.search_volume_backups( + mock.sentinel.name_or_id, mock.sentinel.filter) + + m_list_volume_backups.assert_called_once_with() + m_filter_list.assert_called_once_with( + m_list_volume_backups.return_value, mock.sentinel.name_or_id, + mock.sentinel.filter) + self.assertIs(m_filter_list.return_value, result) + + @mock.patch("shade._utils._get_entity") + def test_get_volume_backup(self, m_get_entity): + result = self.cloud.get_volume_backup( + mock.sentinel.name_or_id, mock.sentinel.filter) + + self.assertIs(m_get_entity.return_value, result) + m_get_entity.assert_called_once_with( + self.cloud.search_volume_backups, mock.sentinel.name_or_id, + mock.sentinel.filter) + + @mock.patch.object(shade.OpenStackCloud, 'cinder_client') + def test_list_volume_backups(self, m_cinder_client): + backup_id = '6ff16bdf-44d5-4bf9-b0f3-687549c76414' + m_cinder_client.backups.list.return_value = [ + {'id': backup_id} + ] + result = self.cloud.list_volume_backups( + mock.sentinel.detailed, mock.sentinel.search_opts) + + m_cinder_client.backups.list.assert_called_once_with( + detailed=mock.sentinel.detailed, + search_opts=mock.sentinel.search_opts) + self.assertEqual(backup_id, result[0]['id']) + + @mock.patch.object(shade.OpenStackCloud, 'cinder_client') + @mock.patch("shade._utils._iterate_timeout") + @mock.patch.object(shade.OpenStackCloud, 'get_volume_backup') + def test_delete_volume_backup(self, m_get_volume_backup, + m_iterate_timeout, m_cinder_client): + m_get_volume_backup.side_effect = [{'id': 42}, True, False] + self.cloud.delete_volume_backup( + mock.sentinel.name_or_id, mock.sentinel.force, mock.sentinel.wait, + mock.sentinel.timeout) + + m_iterate_timeout.assert_called_once_with( + mock.sentinel.timeout, mock.ANY) + m_cinder_client.backups.delete.assert_called_once_with( + backup=42, force=mock.sentinel.force) + + # We expect 3 calls, the last return_value is False which breaks the + # wait loop. + m_get_volume_backup.call_args_list = [mock.call(42)] * 3 From acac3b986be738c5d3eb5f6a7b7bb736b72bba6c Mon Sep 17 00:00:00 2001 From: Jordan Pittier Date: Fri, 14 Oct 2016 17:08:10 +0200 Subject: [PATCH 1066/3836] Fix a NameError exception in _nat_destination_port Yeah, it was burried deep but still. Added a unit test just to quickly exercise this code path. Change-Id: Ibec3f24483214f8af514c8be512b7c3f3840b32e --- shade/openstackcloud.py | 2 +- shade/tests/unit/test_floating_ip_common.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index dde78d2f6..d7314e504 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4137,7 +4137,7 @@ def _nat_destination_port( return port, fixed_address raise OpenStackCloudException( "unable to find a free fixed IPv4 address for server " - "{0}".format(server_id)) + "{0}".format(server['id'])) # unfortunately a port can have more than one fixed IP: # we can't use the search_ports filtering for fixed_address as # they are contained in a list. e.g. diff --git a/shade/tests/unit/test_floating_ip_common.py b/shade/tests/unit/test_floating_ip_common.py index 31515f239..5031e032d 100644 --- a/shade/tests/unit/test_floating_ip_common.py +++ b/shade/tests/unit/test_floating_ip_common.py @@ -20,6 +20,8 @@ """ from mock import patch + +from shade import exc from shade import meta from shade import OpenStackCloud from shade.tests.fakes import FakeServer @@ -223,3 +225,12 @@ def test_add_ips_to_server_auto_ip( mock_add_auto_ip.assert_called_with( server_dict, wait=False, timeout=60, reuse=True) + + @patch.object(OpenStackCloud, 'search_ports', return_value=[{}]) + def test_nat_destination_port_when_no_free_fixed_ip( + self, mock_search_ports): + server = {'id': 42} + self.assertRaisesRegexp( + exc.OpenStackCloudException, 'server 42$', + self.cloud._nat_destination_port, server + ) From 0087da59b2ad7e3ff1b2df3a96549461cf8fb31e Mon Sep 17 00:00:00 2001 From: Jordan Pittier Date: Fri, 14 Oct 2016 17:59:51 +0200 Subject: [PATCH 1067/3836] Fix some docstrings * Correct typos * Correct parameter name * Use """ over ''' consistently Change-Id: Icf9c7976be1b01a61ec3abb1d6ba7991f85f6410 --- shade/openstackcloud.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index dde78d2f6..f6a66e2f2 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -76,7 +76,7 @@ def _no_pending_volumes(volumes): - '''If there are any volumes not in a steady state, don't cache''' + """If there are any volumes not in a steady state, don't cache""" for volume in volumes: if volume['status'] not in ('available', 'error', 'in-use'): return False @@ -84,7 +84,7 @@ def _no_pending_volumes(volumes): def _no_pending_images(images): - '''If there are any images not in a steady state, don't cache''' + """If there are any images not in a steady state, don't cache""" for image in images: if image.status not in ('active', 'deleted', 'killed'): return False @@ -92,7 +92,7 @@ def _no_pending_images(images): def _no_pending_stacks(stacks): - '''If there are any stacks not in a steady state, don't cache''' + """If there are any stacks not in a steady state, don't cache""" for stack in stacks: status = stack['stack_status'] if '_COMPLETE' not in status and '_FAILED' not in status: @@ -401,7 +401,7 @@ def auth_token(self): @property def current_project_id(self): - '''Get the current project id. + """Get the current project id. Returns the project_id of the current token scope. None means that the token is domain scoped or unscoped. @@ -410,12 +410,12 @@ def current_project_id(self): if a new token fetch fails. :raises keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin: if a plugin is not available. - ''' + """ return self.keystone_session.get_project_id() @property def current_project(self): - '''Return a ``munch.Munch`` describing the current project''' + """Return a ``munch.Munch`` describing the current project""" return self._get_project_info() def _get_project_info(self, project_id=None): @@ -445,7 +445,7 @@ def _get_project_info(self, project_id=None): @property def current_location(self): - '''Return a ``munch.Munch`` explaining the current cloud location.''' + """Return a ``munch.Munch`` explaining the current cloud location.""" return self._get_current_location() def _get_current_location(self, project_id=None): @@ -577,7 +577,7 @@ def list_projects(self, domain_id=None): def search_projects(self, name_or_id=None, filters=None, domain_id=None): """Seach Keystone projects. - :param name: project name or id. + :param name_or_id: project name or id. :param filters: a dict containing additional filters to use. :param domain_id: domain id to scope the searched projects. @@ -592,7 +592,7 @@ def search_projects(self, name_or_id=None, filters=None, domain_id=None): def get_project(self, name_or_id, filters=None, domain_id=None): """Get exactly one Keystone project. - :param id: project name or id. + :param name_or_id: project name or id. :param filters: a dict containing additional filters to use. :param domain_id: domain id (keystone v3 only) @@ -691,7 +691,7 @@ def list_users(self): def search_users(self, name_or_id=None, filters=None): """Seach Keystone users. - :param string name: user name or id. + :param string name_or_id: user name or id. :param filters: a dict containing additional filters to use. OR A string containing a jmespath expression for further filtering. @@ -3916,7 +3916,7 @@ def delete_floating_ip(self, floating_ip_id, retry=1): if self._FLOAT_AGE: time.sleep(self._FLOAT_AGE) - # neutron sometimes returns success when deleating a floating + # neutron sometimes returns success when deleting a floating # ip. That's awesome. SO - verify that the delete actually # worked. f_ip = self.get_floating_ip(id=floating_ip_id) @@ -4356,7 +4356,7 @@ def _add_auto_ip(self, server, wait=False, timeout=60, reuse=True): timeout = timeout - (time.time() - start_time) if server: # This gets passed in for both nova and neutron - # but is only meaninful for the neutron logic branch + # but is only meaningful for the neutron logic branch skip_attach = True created = True @@ -4865,6 +4865,7 @@ def delete_server( delete_ip_retry=1): """Delete a server instance. + :param name_or_id: name or ID of the server to delete :param bool wait: If true, waits for server to be deleted. :param int timeout: Seconds to wait for server deletion. :param bool delete_ips: If true, deletes any floating IPs @@ -5123,7 +5124,7 @@ def get_object_capabilities(self): return self.manager.submit_task(_tasks.ObjectCapabilities()) def get_object_segment_size(self, segment_size): - '''get a segment size that will work given capabilities''' + """Get a segment size that will work given capabilities""" if segment_size is None: segment_size = DEFAULT_OBJECT_SEGMENT_SIZE min_segment_size = 0 @@ -5201,7 +5202,7 @@ def create_object( :param headers: These will be passed through to the object creation API as HTTP Headers. :param use_slo: If the object is large enough to need to be a Large - Object, use a static rather than dyanmic object. Static Objects + Object, use a static rather than dynamic object. Static Objects will delete segment objects when the manifest object is deleted. (optional, defaults to True) :param metadata: This dict will get changed into headers that set From 9acef32eeea34be91b2d25e85a752d3e30b06001 Mon Sep 17 00:00:00 2001 From: Jordan Pittier Date: Fri, 14 Oct 2016 18:11:50 +0200 Subject: [PATCH 1068/3836] Fix a NameError exc in operatorcloud.py novaclient was not defined in this file. Only nova_exceptions is. Also add some unit tests. Change-Id: Id533e302ecace6316746e37f24d207319cdc1eca --- shade/operatorcloud.py | 4 ++-- shade/tests/unit/test_quotas.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 6cd825195..eff98ed8c 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1983,7 +1983,7 @@ def set_compute_quotas(self, name_or_id, **kwargs): _tasks.NovaQuotasSet(tenant_id=proj.id, force=True, **kwargs)) - except novaclient.exceptions.BadRequest: + except nova_exceptions.BadRequest: raise OpenStackCloudException("No valid quota or resource") def get_compute_quotas(self, name_or_id): @@ -2018,7 +2018,7 @@ def delete_compute_quotas(self, name_or_id): try: return self.manager.submit_task( _tasks.NovaQuotasDelete(tenant_id=proj.id)) - except novaclient.exceptions.BadRequest: + except nova_exceptions.BadRequest: raise OpenStackCloudException("nova client call failed") def set_volume_quotas(self, name_or_id, **kwargs): diff --git a/shade/tests/unit/test_quotas.py b/shade/tests/unit/test_quotas.py index 40565dff4..73fdf0aaa 100644 --- a/shade/tests/unit/test_quotas.py +++ b/shade/tests/unit/test_quotas.py @@ -14,8 +14,10 @@ import mock +from novaclient import exceptions as nova_exceptions import shade +from shade import exc from shade.tests.unit import base from shade.tests import fakes @@ -32,6 +34,10 @@ def test_update_quotas(self, mock_keystone, mock_nova): mock_nova.quotas.update.assert_called_once_with( cores=1, force=True, tenant_id='project_a') + mock_nova.quotas.update.side_effect = nova_exceptions.BadRequest(400) + self.assertRaises(exc.OpenStackCloudException, + self.op_cloud.set_compute_quotas, project) + @mock.patch.object(shade.OpenStackCloud, 'nova_client') @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_get_quotas(self, mock_keystone, mock_nova): @@ -50,6 +56,10 @@ def test_delete_quotas(self, mock_keystone, mock_nova): mock_nova.quotas.delete.assert_called_once_with(tenant_id='project_a') + mock_nova.quotas.delete.side_effect = nova_exceptions.BadRequest(400) + self.assertRaises(exc.OpenStackCloudException, + self.op_cloud.delete_compute_quotas, project) + @mock.patch.object(shade.OpenStackCloud, 'cinder_client') @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_cinder_update_quotas(self, mock_keystone, mock_cinder): From 78e999776a78496874ccead05b91519581d0e0ed Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 17 Oct 2016 09:47:20 -0500 Subject: [PATCH 1069/3836] Merge nova and neutron normalize methods In keeping with the other normalize methods, combine these two into a single normalize method. Removed a unittest that did literally nothing but was mocking out things at a crazy level. Remind me that we should REALLY stop mocking out anything other than the requests calls. Depends-On: I59e9834d249ccda0beeb715a8d6271140fb144f4 Change-Id: I077f9d8c4d250d180dec9ce4aabc8f64b507b05f --- shade/_normalize.py | 30 +++++++++----- shade/_utils.py | 41 -------------------- shade/openstackcloud.py | 14 +++---- shade/tests/unit/test_floating_ip_neutron.py | 34 +++------------- shade/tests/unit/test_floating_ip_nova.py | 3 +- 5 files changed, 34 insertions(+), 88 deletions(-) diff --git a/shade/_normalize.py b/shade/_normalize.py index 51b4f86c8..fbcc3ef30 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -200,8 +200,8 @@ def _normalize_server(self, server): return server - def _normalize_neutron_floating_ips(self, ips): - """Normalize the structure of Neutron floating IPs + def _normalize_floating_ips(self, ips): + """Normalize the structure of floating IPs Unfortunately, not all the Neutron floating_ip attributes are available with Nova and not all Nova floating_ip attributes are available with @@ -230,16 +230,28 @@ def _normalize_neutron_floating_ips(self, ips): """ return [ - self._normalize_neutron_floating_ip(ip) for ip in ips + self._normalize_floating_ip(ip) for ip in ips ] - def _normalize_neutron_floating_ip(self, ip): - network_id = ip.get('floating_network_id', ip.get('network')) + def _normalize_floating_ip(self, ip): + fixed_ip_address = ip.get('fixed_ip_address', ip.get('fixed_ip')) + floating_ip_address = ip.get('floating_ip_address', ip.get('ip')) + network_id = ip.get( + 'floating_network_id', ip.get('network', ip.get('pool'))) project_id = ip.get('project_id', ip.get('tenant_id', '')) + if self._use_neutron_floating(): + attached = (ip.get('port_id') is not None and ip['port_id'] != '') + status = ip.get('status', 'UNKNOWN') + else: + instance_id = ip.get('instance_id') + attached = instance_id is not None and instance_id != '' + # In neutron's terms, Nova floating IPs are always ACTIVE + status = 'ACTIVE' + return munch.Munch( - attached=ip.get('port_id') is not None and ip.get('port_id') != '', - fixed_ip_address=ip.get('fixed_ip_address'), - floating_ip_address=ip['floating_ip_address'], + attached=attached, + fixed_ip_address=fixed_ip_address, + floating_ip_address=floating_ip_address, floating_network_id=network_id, id=ip['id'], location=self._get_current_location(project_id=project_id), @@ -247,6 +259,6 @@ def _normalize_neutron_floating_ip(self, ip): port_id=ip.get('port_id'), project_id=project_id, router_id=ip.get('router_id'), - status=ip.get('status', 'UNKNOWN'), + status=status, tenant_id=project_id ) diff --git a/shade/_utils.py b/shade/_utils.py index b9440e646..b3b8b73a3 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -182,47 +182,6 @@ def normalize_keystone_services(services): return meta.obj_list_to_dict(ret) -def normalize_nova_floating_ips(ips): - """Normalize the structure of Neutron floating IPs - - Unfortunately, not all the Neutron floating_ip attributes are available - with Nova and not all Nova floating_ip attributes are available with - Neutron. - This function extract attributes that are common to Nova and Neutron - floating IP resource. - If the whole structure is needed inside shade, shade provides private - methods that returns "original" objects (e.g. _nova_allocate_floating_ip) - - :param list ips: A list of Nova floating IPs. - - :returns: - A list of normalized dicts with the following attributes:: - - [ - { - "id": "this-is-a-floating-ip-id", - "fixed_ip_address": "192.0.2.10", - "floating_ip_address": "198.51.100.10", - "network": "this-is-a-net-or-pool-id", - "attached": True, - "status": "ACTIVE" - }, ... - ] - - """ - ret = [dict( - id=ip['id'], - fixed_ip_address=ip.get('fixed_ip'), - floating_ip_address=ip['ip'], - network=ip['pool'], - attached=(ip.get('instance_id') is not None and - ip.get('instance_id') != ''), - status='ACTIVE' # In neutrons terms, Nova floating IPs are always - # ACTIVE - ) for ip in ips] - return meta.obj_list_to_dict(ret) - - def localhost_supports_ipv6(): """Determine whether the local host supports IPv6 diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 0fbf3743b..a454ac596 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1681,7 +1681,7 @@ def list_floating_ip_pools(self): def _list_floating_ips(self): if self._use_neutron_floating(): try: - return self._normalize_neutron_floating_ips( + return self._normalize_floating_ips( self._neutron_list_floating_ips()) except OpenStackCloudURINotFound as e: self.log.debug( @@ -1690,7 +1690,7 @@ def _list_floating_ips(self): # Fall-through, trying with Nova floating_ips = self._nova_list_floating_ips() - return _utils.normalize_nova_floating_ips(floating_ips) + return self._normalize_floating_ips(floating_ips) def list_floating_ips(self): """List all available floating IPs. @@ -3731,7 +3731,7 @@ def available_floating_ip(self, network=None, server=None): """ if self._use_neutron_floating(): try: - f_ips = self._normalize_neutron_floating_ips( + f_ips = self._normalize_floating_ips( self._neutron_available_floating_ips( network=network, server=server)) return f_ips[0] @@ -3741,7 +3741,7 @@ def available_floating_ip(self, network=None, server=None): "'{msg}'. Trying with Nova.".format(msg=str(e))) # Fall-through, trying with Nova - f_ips = _utils.normalize_nova_floating_ips( + f_ips = self._normalize_floating_ips( self._nova_available_floating_ips(pool=network) ) return f_ips[0] @@ -3800,7 +3800,7 @@ def _neutron_available_floating_ips( 'tenant_id': project_id } - floating_ips = self._neutron_list_floating_ips() + floating_ips = self._list_floating_ips() available_ips = _utils._filter_list( floating_ips, name_or_id=None, filters=filters) if available_ips: @@ -3905,13 +3905,13 @@ def create_floating_ip(self, network=None, server=None, " to neutron, or alternately provide the server," " fixed_address and nat_destination arguments as appropriate") # Else, we are using Nova network - f_ips = _utils.normalize_nova_floating_ips( + f_ips = self._normalize_floating_ips( [self._nova_create_floating_ip(pool=network)]) return f_ips[0] def _submit_create_fip(self, kwargs): # Split into a method to aid in test mocking - return self._normalize_neutron_floating_ips( + return self._normalize_floating_ips( [self.manager.submit_task(_tasks.NeutronFloatingIPCreate( body={'floatingip': kwargs}))['floatingip']])[0] diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 779cf1324..21bb8fadb 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -24,7 +24,6 @@ from neutronclient.common import exceptions as n_exc -from shade import _normalize from shade import _utils from shade import exc from shade import meta @@ -148,7 +147,7 @@ def setUp(self): u'version': 4, u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:ae:7d:42'}]})) - self.floating_ip = self.cloud._normalize_neutron_floating_ips( + self.floating_ip = self.cloud._normalize_floating_ips( self.mock_floating_ip_list_rep['floatingips'])[0] def test_float_no_status(self): @@ -163,7 +162,7 @@ def test_float_no_status(self): 'tenant_id': '4969c491a3c74ee4af974e6d800c62df' } ] - normalized = self.cloud._normalize_neutron_floating_ips(floating_ips) + normalized = self.cloud._normalize_floating_ips(floating_ips) self.assertEqual('UNKNOWN', normalized[0]['status']) @patch.object(OpenStackCloud, 'neutron_client') @@ -287,29 +286,6 @@ def test_create_floating_ip_port( self.mock_floating_ip_new_rep['floatingip']['floating_ip_address'], ip['floating_ip_address']) - @patch.object(_normalize.Normalizer, '_normalize_neutron_floating_ips') - @patch.object(OpenStackCloud, '_neutron_available_floating_ips') - @patch.object(OpenStackCloud, 'has_service') - @patch.object(OpenStackCloud, 'keystone_session') - def test_available_floating_ip_neutron(self, - mock_keystone, - mock_has_service, - mock__neutron_call, - mock_normalize): - """ - Test the correct path is taken when using neutron. - """ - # force neutron path - mock_has_service.return_value = True - mock__neutron_call.return_value = [] - - self.cloud.available_floating_ip(network='netname') - - mock_has_service.assert_called_once_with('network') - mock__neutron_call.assert_called_once_with(network='netname', - server=None) - mock_normalize.assert_called_once_with([]) - @patch.object(_utils, '_filter_list') @patch.object(OpenStackCloud, '_neutron_create_floating_ip') @patch.object(OpenStackCloud, '_neutron_list_floating_ips') @@ -412,7 +388,7 @@ def test_auto_ip_pool_no_reuse( mock_keystone_session, mock_nova_client): mock_has_service.return_value = True - fip = self.cloud._normalize_neutron_floating_ips( + fip = self.cloud._normalize_floating_ips( self.mock_floating_ip_list_rep['floatingips'])[0] mock__neutron_create_floating_ip.return_value = fip mock_keystone_session.get_project_id.return_value = \ @@ -571,7 +547,7 @@ def test_detach_ip_from_server( mock_get_floating_ip): mock_has_service.return_value = True mock_get_floating_ip.return_value = \ - self.cloud._normalize_neutron_floating_ips( + self.cloud._normalize_floating_ips( self.mock_floating_ip_list_rep['floatingips'])[0] self.cloud.detach_ip_from_server( @@ -595,7 +571,7 @@ def test_add_ip_from_pool( mock_attach_ip_to_server): mock_has_service.return_value = True mock_available_floating_ip.return_value = \ - self.cloud._normalize_neutron_floating_ips([ + self.cloud._normalize_floating_ips([ self.mock_floating_ip_new_rep['floatingip']])[0] mock_attach_ip_to_server.return_value = self.fake_server diff --git a/shade/tests/unit/test_floating_ip_nova.py b/shade/tests/unit/test_floating_ip_nova.py index a19fc4986..e05cea7be 100644 --- a/shade/tests/unit/test_floating_ip_nova.py +++ b/shade/tests/unit/test_floating_ip_nova.py @@ -22,7 +22,6 @@ from mock import patch from novaclient import exceptions as n_exc -from shade import _utils from shade import meta from shade import OpenStackCloud from shade.tests import fakes @@ -84,7 +83,7 @@ def setUp(self): u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:ae:7d:42'}]})) - self.floating_ip = _utils.normalize_nova_floating_ips( + self.floating_ip = self.cloud._normalize_floating_ips( meta.obj_list_to_dict(self.floating_ips))[0] @patch.object(OpenStackCloud, 'nova_client') From 261313ead974f8c48195498fe649208cff883a1d Mon Sep 17 00:00:00 2001 From: Jordan Pittier Date: Wed, 19 Oct 2016 11:24:35 +0200 Subject: [PATCH 1070/3836] Fix TypeError in list_router_interfaces It turns out, you can have a router object with a 'external_gateway_info' key that is set to None. In that case, list_router_interfaces fail with File "../shade/openstackcloud.py", line 2624, in list_router_interfaces 'external_fixed_ips' in router['external_gateway_info']): TypeError: argument of type 'NoneType' is not iterable How to repro 1) Create a router: neutron router-create foo 2) Run import os_client_config, shade cloud = os_client_config.make_shade(cloud='devstack') router = cloud.get_router('foo') cloud.list_router_interfaces(router, 'external') Change-Id: I4c2894355958a7142670e186e7932048d66cfd59 --- shade/openstackcloud.py | 2 +- shade/tests/unit/test_shade.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 0fbf3743b..2c17ced5d 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2620,7 +2620,7 @@ def list_router_interfaces(self, router, interface_type=None): if interface_type: filtered_ports = [] - if ('external_gateway_info' in router and + if (router.get('external_gateway_info') and 'external_fixed_ips' in router['external_gateway_info']): ext_fixed = \ router['external_gateway_info']['external_fixed_ips'] diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 3de161548..1f7898d64 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -285,6 +285,12 @@ def test_list_router_interfaces_no_gw(self, mock_client, mock_search): ) self.assertEqual([], ret) + # A router can have its external_gateway_info set to None + router['external_gateway_info'] = None + ret = self.cloud.list_router_interfaces(router, + interface_type='external') + self.assertEqual([], ret) + @mock.patch.object(shade.OpenStackCloud, 'search_ports') @mock.patch.object(shade.OpenStackCloud, 'neutron_client') def test_list_router_interfaces_all(self, mock_client, mock_search): From 57f9ac9ab70f39a787e6cf6dc4cda8b9b6d89c14 Mon Sep 17 00:00:00 2001 From: Jordan Pittier Date: Wed, 19 Oct 2016 12:39:34 +0200 Subject: [PATCH 1071/3836] Remove useless mocking in tests/unit/test_shade.py It only makes the tests run slower and it's confusing. Change-Id: I1b4b92ddecf74648727759d4e60fc030a5c5e167 --- shade/tests/unit/test_shade.py | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 3de161548..19af9f98d 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -214,8 +214,7 @@ def test_remove_router_interface(self, mock_client): router='123', body={'subnet_id': 'abc'} ) - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_remove_router_interface_missing_argument(self, mock_client): + def test_remove_router_interface_missing_argument(self): self.assertRaises(ValueError, self.cloud.remove_router_interface, {'id': '123'}) @@ -264,8 +263,7 @@ def test_delete_router_multiple_using_id(self, mock_client): self.assertTrue(mock_client.delete_router.called) @mock.patch.object(shade.OpenStackCloud, 'search_ports') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_list_router_interfaces_no_gw(self, mock_client, mock_search): + def test_list_router_interfaces_no_gw(self, mock_search): """ If a router does not have external_gateway_info, do not fail. """ @@ -286,8 +284,7 @@ def test_list_router_interfaces_no_gw(self, mock_client, mock_search): self.assertEqual([], ret) @mock.patch.object(shade.OpenStackCloud, 'search_ports') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_list_router_interfaces_all(self, mock_client, mock_search): + def test_list_router_interfaces_all(self, mock_search): internal_port = {'id': 'internal_port_id', 'fixed_ips': [ ('internal_subnet_id', 'ip_address'), @@ -311,8 +308,7 @@ def test_list_router_interfaces_all(self, mock_client, mock_search): self.assertEqual(port_list, ret) @mock.patch.object(shade.OpenStackCloud, 'search_ports') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_list_router_interfaces_internal(self, mock_client, mock_search): + def test_list_router_interfaces_internal(self, mock_search): internal_port = {'id': 'internal_port_id', 'fixed_ips': [ ('internal_subnet_id', 'ip_address'), @@ -337,8 +333,7 @@ def test_list_router_interfaces_internal(self, mock_client, mock_search): self.assertEqual([internal_port], ret) @mock.patch.object(shade.OpenStackCloud, 'search_ports') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_list_router_interfaces_external(self, mock_client, mock_search): + def test_list_router_interfaces_external(self, mock_search): internal_port = {'id': 'internal_port_id', 'fixed_ips': [ ('internal_subnet_id', 'ip_address'), @@ -386,8 +381,7 @@ def test_create_subnet_string_ip_version(self, mock_client, mock_search): self.assertTrue(mock_client.create_subnet.called) @mock.patch.object(shade.OpenStackCloud, 'search_networks') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_subnet_bad_ip_version(self, mock_client, mock_search): + def test_create_subnet_bad_ip_version(self, mock_search): '''String ip_versions must be convertable to int''' net1 = dict(id='123', name='donald') mock_search.return_value = [net1] @@ -427,8 +421,7 @@ def test_create_subnet_with_gateway_ip(self, mock_client, mock_search): self.assertTrue(mock_client.create_subnet.called) @mock.patch.object(shade.OpenStackCloud, 'search_networks') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_subnet_conflict_gw_ops(self, mock_client, mock_search): + def test_create_subnet_conflict_gw_ops(self, mock_search): net1 = dict(id='123', name='donald') mock_search.return_value = [net1] gateway = '192.168.200.3' @@ -522,8 +515,7 @@ def test_update_subnet_disable_gateway_ip(self, mock_client, mock_get): self.assertTrue(mock_client.update_subnet.called) @mock.patch.object(shade.OpenStackCloud, 'get_subnet') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_update_subnet_conflict_gw_ops(self, mock_client, mock_get): + def test_update_subnet_conflict_gw_ops(self, mock_get): subnet1 = dict(id='456', name='kooky') mock_get.return_value = subnet1 gateway = '192.168.200.3' From 9986646a131cf618476d24be18ab22f0da9de926 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 19 Oct 2016 07:50:56 -0500 Subject: [PATCH 1072/3836] Add abililty to find floating IP network by subnet If the cloud has a hidden public network but a visible router, it's not possible to infer network info from metadata, but it is possible to know that the network that the floating IP should be requested on is the one that is in the router's external_gateway_info field. Wrap the search in local caching and a lock to prevernt thundering herd. It's not a thing that will need to be inferred more than once, but has the potential to be hit on every server creation for clouds that need this. Change-Id: Ic98b6059e0ca5d0f1c8e6db16cc89a352c6fbbbd --- shade/openstackcloud.py | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 0fbf3743b..dfd398576 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -212,6 +212,10 @@ def __init__( self._floating_ips_time = 0 self._floating_ips_lock = threading.Lock() + self._floating_network_by_subnet = None + self._floating_network_by_subnet_run = False + self._floating_network_by_subnet_lock = threading.Lock() + self._networks_lock = threading.Lock() self._reset_network_caches() @@ -3718,6 +3722,26 @@ def _expand_server_vars(self, server): # actually want the API to be. return meta.expand_server_vars(self, server) + def _find_floating_network_by_subnet(self): + """Find the network providing floating ips by looking at routers.""" + + if self._floating_network_by_subnet_lock.acquire( + not self._floating_network_by_subnet_run): + if self._floating_network_by_subnet_run: + self._floating_network_by_subnet_lock.release() + return self._floating_network_by_subnet + try: + for router in self.list_routers(): + if router['admin_state_up']: + network_id = router.get( + 'external_gateway_info', {}).get('network_id') + if network: + self._floating_network_by_subnet = network_id + finally: + self._floating_network_by_subnet_run = True + self._floating_network_by_subnet_lock.release() + return self._floating_network_by_subnet + def available_floating_ip(self, network=None, server=None): """Get a floating IP from a network or a pool. @@ -3931,10 +3955,14 @@ def _neutron_create_floating_ip( "{0}".format(network_name_or_id)) else: networks = self.get_external_ipv4_networks() - if not networks: - raise OpenStackCloudResourceNotFound( - "Unable to find an external network in this cloud" - " which makes getting a floating IP impossible") + if networks: + network_id = networks[0]['id'] + else: + network_id = self._find_floating_network_by_router() + if not network_id: + raise OpenStackCloudResourceNotFound( + "Unable to find an external network in this cloud" + " which makes getting a floating IP impossible") kwargs = { 'floating_network_id': networks[0]['id'], } From 29e62e7ae25d160726ae841830f988f93c25b468 Mon Sep 17 00:00:00 2001 From: Matthew Wagoner Date: Wed, 19 Oct 2016 09:20:26 -0400 Subject: [PATCH 1073/3836] Add test for os_keystone_domain Ansible module Change-Id: I2116de5f74c9f0fc9d08ae56fba14ea48f850167 --- .../roles/keystone_domain/tasks/main.yml | 19 +++++++++++++++++++ .../roles/keystone_domain/vars/main.yml | 1 + shade/tests/ansible/run.yml | 1 + 3 files changed, 21 insertions(+) create mode 100644 shade/tests/ansible/roles/keystone_domain/tasks/main.yml create mode 100644 shade/tests/ansible/roles/keystone_domain/vars/main.yml diff --git a/shade/tests/ansible/roles/keystone_domain/tasks/main.yml b/shade/tests/ansible/roles/keystone_domain/tasks/main.yml new file mode 100644 index 000000000..d1ca1273b --- /dev/null +++ b/shade/tests/ansible/roles/keystone_domain/tasks/main.yml @@ -0,0 +1,19 @@ +--- +- name: Create keystone domain + os_keystone_domain: + cloud: "{{ cloud }}" + state: present + name: "{{ domain_name }}" + description: "test description" + +- name: Update keystone domain + os_keystone_domain: + cloud: "{{ cloud }}" + name: "{{ domain_name }}" + description: "updated description" + +- name: Delete keystone domain + os_keystone_domain: + cloud: "{{ cloud }}" + state: absent + name: "{{ domain_name }}" diff --git a/shade/tests/ansible/roles/keystone_domain/vars/main.yml b/shade/tests/ansible/roles/keystone_domain/vars/main.yml new file mode 100644 index 000000000..049e7c378 --- /dev/null +++ b/shade/tests/ansible/roles/keystone_domain/vars/main.yml @@ -0,0 +1 @@ +domain_name: ansible_domain diff --git a/shade/tests/ansible/run.yml b/shade/tests/ansible/run.yml index 73fc5792f..5d4f077e9 100644 --- a/shade/tests/ansible/run.yml +++ b/shade/tests/ansible/run.yml @@ -9,6 +9,7 @@ - { role: group, tags: group } - { role: image, tags: image } - { role: keypair, tags: keypair } + - { role: keystone_domain, tags: keystone_domain } - { role: network, tags: network } - { role: nova_flavor, tags: nova_flavor } - { role: object, tags: object } From 47068d0abbb1018e238213af30cf7840bb2104d9 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 19 Oct 2016 16:17:38 -0500 Subject: [PATCH 1074/3836] Update ECS image_api_version to 1 Turns out they don't run v2. Change-Id: Icd503f2b035400fbb39903b3fe2542ec14b86e93 --- doc/source/vendor-support.rst | 1 + os_client_config/vendors/entercloudsuite.json | 1 + 2 files changed, 2 insertions(+) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index cfdc02eeb..eb7ecf0fe 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -160,6 +160,7 @@ it-mil1 Milan, IT de-fra1 Frankfurt, DE ============== ================ +* Image API Version is 1 * Volume API Version is 1 internap diff --git a/os_client_config/vendors/entercloudsuite.json b/os_client_config/vendors/entercloudsuite.json index 6d2fc129e..c58c478f0 100644 --- a/os_client_config/vendors/entercloudsuite.json +++ b/os_client_config/vendors/entercloudsuite.json @@ -5,6 +5,7 @@ "auth_url": "https://api.entercloudsuite.com/" }, "identity_api_version": "3", + "image_api_version": "1", "volume_api_version": "1", "regions": [ "it-mil1", From ecb317d6108b12643b126a9f6e1338284ae4f610 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Thu, 20 Oct 2016 12:57:31 +1100 Subject: [PATCH 1075/3836] Allow setting env variables for functional options The shade functional tests are hardcoded to use the cloud configuration that is typically provided by devstack. It turns out that shade functional tests are a useful way to quickly test that a cloud is up and configured. Allow providing new values for flavor, image and cloud variables so that the functional tests can be run on a non-devstack cloud. Change-Id: I90ab125be77091eaf16ae16e47f4336d4ef49880 --- shade/tests/functional/base.py | 9 +++++++-- shade/tests/functional/util.py | 15 +++++++++++++++ tox.ini | 2 +- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/shade/tests/functional/base.py b/shade/tests/functional/base.py index 14839d123..c2e83373a 100644 --- a/shade/tests/functional/base.py +++ b/shade/tests/functional/base.py @@ -12,6 +12,8 @@ # License for the specific language governing permissions and limitations # under the License. +import os + import os_client_config as occ import shade @@ -22,12 +24,15 @@ class BaseFunctionalTestCase(base.TestCase): def setUp(self): super(BaseFunctionalTestCase, self).setUp() + demo_name = os.environ.get('SHADE_DEMO_CLOUD', 'devstack') + op_name = os.environ.get('SHADE_OPERATOR_CLOUD', 'devstack-admin') + self.config = occ.OpenStackConfig() - demo_config = self.config.get_one_cloud(cloud='devstack') + demo_config = self.config.get_one_cloud(cloud=demo_name) self.demo_cloud = shade.OpenStackCloud( cloud_config=demo_config, log_inner_exceptions=True) - operator_config = self.config.get_one_cloud(cloud='devstack-admin') + operator_config = self.config.get_one_cloud(cloud=op_name) self.operator_cloud = shade.OperatorCloud( cloud_config=operator_config, log_inner_exceptions=True) diff --git a/shade/tests/functional/util.py b/shade/tests/functional/util.py index 07a9a5031..23ddc3e80 100644 --- a/shade/tests/functional/util.py +++ b/shade/tests/functional/util.py @@ -19,12 +19,20 @@ Util methods for functional tests """ import operator +import os def pick_flavor(flavors): """Given a flavor list pick the smallest one.""" # Enable running functional tests against rax - which requires # performance flavors be used for boot from volume + flavor_name = os.environ.get('SHADE_FLAVOR') + if flavor_name: + for flavor in flavors: + if flavor.name == flavor_name: + return flavor + return None + for flavor in sorted( flavors, key=operator.attrgetter('ram')): @@ -37,6 +45,13 @@ def pick_flavor(flavors): def pick_image(images): + image_name = os.environ.get('SHADE_IMAGE') + if image_name: + for image in images: + if image.name == image_name: + return image + return None + for image in images: if image.name.startswith('cirros') and image.name.endswith('-uec'): return image diff --git a/tox.ini b/tox.ini index 60121bb0f..5ddae0aa5 100644 --- a/tox.ini +++ b/tox.ini @@ -32,7 +32,7 @@ commands = python setup.py testr --slowest --testr-args='--concurrency=1 {posarg [testenv:functional] setenv = OS_TEST_PATH = ./shade/tests/functional -passenv = OS_* +passenv = OS_* SHADE_* commands = python setup.py testr --slowest --testr-args='--concurrency=1 {posargs}' [testenv:pep8] From 0986a623b7603cc85dd10d1f6a575e6bb165e651 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Thu, 20 Oct 2016 12:56:31 +1100 Subject: [PATCH 1076/3836] Add a devstack plugin for shade Install shade with devstack. This is really simple because there's not really any configuration needed. Change-Id: Ic5962bfb0e25d592d50458f32a21ac612479447c --- devstack/plugin.sh | 54 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 devstack/plugin.sh diff --git a/devstack/plugin.sh b/devstack/plugin.sh new file mode 100644 index 000000000..59c130be2 --- /dev/null +++ b/devstack/plugin.sh @@ -0,0 +1,54 @@ +# Install and configure **shade** library in devstack +# +# To enable shade in devstack add an entry to local.conf that looks like +# +# [[local|localrc]] +# enable_plugin shade git://git.openstack.org/openstack-infra/shade + +function preinstall_shade { + : +} + +function install_shade { + if use_library_from_git "shade"; then + # don't clone, it'll be done by the plugin install + setup_dev_lib "shade" + else + pip_install "shade" + fi +} + +function configure_shade { + : +} + +function initialize_shade { + : +} + +function unstack_shade { + : +} + +function clean_shade { + : +} + +# This is the main for plugin.sh +if [[ "$1" == "stack" && "$2" == "pre-install" ]]; then + preinstall_shade +elif [[ "$1" == "stack" && "$2" == "install" ]]; then + install_shade +elif [[ "$1" == "stack" && "$2" == "post-config" ]]; then + configure_shade +elif [[ "$1" == "stack" && "$2" == "extra" ]]; then + initialize_shade +fi + +if [[ "$1" == "unstack" ]]; then + unstack_shade +fi + +if [[ "$1" == "clean" ]]; then + clean_shade +fi From 49f1f191df9c95972189ec9eaaa508ff78c98266 Mon Sep 17 00:00:00 2001 From: Jordan Pittier Date: Thu, 20 Oct 2016 12:01:52 +0200 Subject: [PATCH 1077/3836] Logging: avoid string interpolation when not needed Call to `format` or '%' (basically all string formating) can be expensive and if someone is not interested in the logs produced by shade (log_level set to error or higher) can be avoided. Note: this change is a bit risky given the low unit test coverage. But the longer we wait the harder this change will be. Change-Id: I171f4ee08e5978c7b6e3853ed4ee533682c2ffe6 --- shade/_utils.py | 2 +- shade/meta.py | 5 +- shade/openstackcloud.py | 130 ++++++++++++++++++++-------------------- shade/operatorcloud.py | 18 +++--- shade/task_manager.py | 12 ++-- 5 files changed, 84 insertions(+), 83 deletions(-) diff --git a/shade/_utils.py b/shade/_utils.py index b9440e646..e859edc9d 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -60,7 +60,7 @@ def _iterate_timeout(timeout, message, wait=2): while (timeout is None) or (time.time() < start + timeout): count += 1 yield count - log.debug('Waiting {wait} seconds'.format(wait=wait)) + log.debug('Waiting %s seconds', wait) time.sleep(wait) raise exc.OpenStackCloudTimeout(message) diff --git a/shade/meta.py b/shade/meta.py index 492367d98..1f2f03f17 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -409,8 +409,9 @@ def _log_request_id(obj, request_id): log = _log.setup_logging('shade.request_ids') # Log the request id and object id in a specific logger. This way # someone can turn it on if they're interested in this kind of tracing. - log.debug("Retreived object {id}. Request ID {request_id}".format( - id=obj.get('id', obj.get('uuid')), request_id=request_id)) + log.debug("Retreived object %(id)s. Request ID %(request_id)s", + {'id': obj.get('id', obj.get('uuid')), + 'request_id': request_id}) return obj diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index dfd398576..99aeabf5f 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -349,8 +349,8 @@ def _get_client( **kwargs) except Exception: self.log.debug( - "Couldn't construct {service} object".format( - service=service_key), exc_info=True) + "Couldn't construct %(service)s object", + {'service': service_key}, exc_info=True) raise if client is None: raise OpenStackCloudException( @@ -667,7 +667,7 @@ def delete_project(self, name_or_id, domain_id=None): project = self.get_project(name_or_id, domain_id=domain_id) if project is None: self.log.debug( - "Project {0} not found for deleting".format(name_or_id)) + "Project %s not found for deleting", name_or_id) return False params = {} @@ -1138,7 +1138,7 @@ def delete_stack(self, name_or_id, wait=False): """ stack = self.get_stack(name_or_id) if stack is None: - self.log.debug("Stack %s not found for deleting" % name_or_id) + self.log.debug("Stack %s not found for deleting", name_or_id) return False if wait: @@ -1227,8 +1227,8 @@ def has_service(self, service_key): if not (service_key in self._disable_warnings and self._disable_warnings[service_key]): self.log.debug( - "Disabling {service_key} entry in catalog" - " per config".format(service_key=service_key)) + "Disabling %(service_key)s entry in catalog" + " per config", {'service_key': service_key}) self._disable_warnings[service_key] = True return False try: @@ -1520,7 +1520,7 @@ def list_flavors(self, get_extra=True): flavor.extra_specs = [] self.log.debug( 'Fetching extra specs for flavor failed:' - ' {msg}'.format(msg=str(e))) + ' %(msg)s', {'msg': str(e)}) return self._normalize_flavors(flavors) @@ -1690,7 +1690,7 @@ def _list_floating_ips(self): except OpenStackCloudURINotFound as e: self.log.debug( "Something went wrong talking to neutron API: " - "'{msg}'. Trying with Nova.".format(msg=str(e))) + "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) # Fall-through, trying with Nova floating_ips = self._nova_list_floating_ips() @@ -2444,7 +2444,7 @@ def delete_keypair(self, name): try: self.manager.submit_task(_tasks.KeypairDelete(key=name)) except nova_exceptions.NotFound: - self.log.debug("Keypair %s not found for deleting" % name) + self.log.debug("Keypair %s not found for deleting", name) return False except OpenStackCloudException: raise @@ -2519,7 +2519,7 @@ def delete_network(self, name_or_id): """ network = self.get_network(name_or_id) if not network: - self.log.debug("Network %s not found for deleting" % name_or_id) + self.log.debug("Network %s not found for deleting", name_or_id) return False with _utils.neutron_exceptions( @@ -2760,7 +2760,7 @@ def delete_router(self, name_or_id): """ router = self.get_router(name_or_id) if not router: - self.log.debug("Router %s not found for deleting" % name_or_id) + self.log.debug("Router %s not found for deleting", name_or_id) return False with _utils.neutron_exceptions( @@ -2948,7 +2948,7 @@ def create_image( if (current_image and current_image.get(IMAGE_MD5_KEY, '') == md5 and current_image.get(IMAGE_SHA256_KEY, '') == sha256): self.log.debug( - "image {name} exists and is up to date".format(name=name)) + "image %(name)s exists and is up to date", {'name': name}) return current_image kwargs[IMAGE_MD5_KEY] = md5 kwargs[IMAGE_SHA256_KEY] = sha256 @@ -3026,8 +3026,8 @@ def _upload_image_put_v2(self, name, image_data, meta, **image_kwargs): self.manager.submit_task(_tasks.ImageUpload( image_id=image.id, image_data=image_data)) except Exception: - self.log.debug("Deleting failed upload of image {image}".format( - image=image['name'])) + self.log.debug("Deleting failed upload of image %(image)s", + {'image': image['name']}) self.manager.submit_task(_tasks.ImageDelete(image_id=image.id)) raise @@ -3043,8 +3043,8 @@ def _upload_image_put_v1( self.manager.submit_task(_tasks.ImageUpdate( image=image, data=image_data)) except Exception: - self.log.debug("Deleting failed upload of image {image}".format( - image=image['name'])) + self.log.debug("Deleting failed upload of image %(image)s", + {'image': image['name']}) # Note argument is "image" here, "image_id" in V2 self.manager.submit_task(_tasks.ImageDelete(image=image.id)) raise @@ -3256,8 +3256,8 @@ def delete_volume(self, name_or_id=None, wait=True, timeout=None): if not volume: self.log.debug( - "Volume {name_or_id} does not exist".format( - name_or_id=name_or_id), + "Volume %(name_or_id)s does not exist", + {'name_or_id': name_or_id}, exc_info=True) return False @@ -3348,7 +3348,7 @@ def detach_volume(self, server, volume, wait=True, timeout=None): vol = self.get_volume(volume['id']) except Exception: self.log.debug( - "Error getting volume info %s" % volume['id'], + "Error getting volume info %s", volume['id'], exc_info=True) continue @@ -3413,7 +3413,7 @@ def attach_volume(self, server, volume, device=None, vol = self.get_volume(volume['id']) except Exception: self.log.debug( - "Error getting volume info %s" % volume['id'], + "Error getting volume info %s", volume['id'], exc_info=True) continue @@ -3762,7 +3762,7 @@ def available_floating_ip(self, network=None, server=None): except OpenStackCloudURINotFound as e: self.log.debug( "Something went wrong talking to neutron API: " - "'{msg}'. Trying with Nova.".format(msg=str(e))) + "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) # Fall-through, trying with Nova f_ips = _utils.normalize_nova_floating_ips( @@ -3918,7 +3918,7 @@ def create_floating_ip(self, network=None, server=None, except OpenStackCloudURINotFound as e: self.log.debug( "Something went wrong talking to neutron API: " - "'{msg}'. Trying with Nova.".format(msg=str(e))) + "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) # Fall-through, trying with Nova if port: @@ -4007,16 +4007,16 @@ def _neutron_create_floating_ip( break except OpenStackCloudTimeout: self.log.error( - "Timed out on floating ip {fip} becoming active." - " Deleting".format(fip=fip_id)) + "Timed out on floating ip %(fip)s becoming active." + " Deleting", {'fip': fip_id}) try: self.delete_floating_ip(fip_id) except Exception as e: self.log.error( "FIP LEAK: Attempted to delete floating ip " - "{fip} but received {exc} exception: " - "{err}".format(fip=fip_id, exc=e.__class__, - err=str(e))) + "%(fip)s but received %(exc)s exception: " + "%(err)s", {'fip': fip_id, 'exc': e.__class__, + 'err': str(e)}) raise return fip @@ -4081,7 +4081,7 @@ def _delete_floating_ip(self, floating_ip_id): except OpenStackCloudURINotFound as e: self.log.debug( "Something went wrong talking to neutron API: " - "'{msg}'. Trying with Nova.".format(msg=str(e))) + "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) return self._nova_delete_floating_ip(floating_ip_id) def _neutron_delete_floating_ip(self, floating_ip_id): @@ -4177,7 +4177,7 @@ def _attach_ip_to_server( except OpenStackCloudURINotFound as e: self.log.debug( "Something went wrong talking to neutron API: " - "'{msg}'. Trying with Nova.".format(msg=str(e))) + "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) # Fall-through, trying with Nova else: # Nova network @@ -4350,7 +4350,7 @@ def detach_ip_from_server(self, server_id, floating_ip_id): except OpenStackCloudURINotFound as e: self.log.debug( "Something went wrong talking to neutron API: " - "'{msg}'. Trying with Nova.".format(msg=str(e))) + "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) # Fall-through, trying with Nova # Nova network @@ -4380,7 +4380,7 @@ def _nova_detach_ip_from_server(self, server_id, floating_ip_id): server=server_id, address=f_ip['floating_ip_address'])) except nova_exceptions.Conflict as e: self.log.debug( - "nova floating IP detach failed: {msg}".format(msg=str(e)), + "nova floating IP detach failed: %(msg)s", {'msg': str(e)}, exc_info=True) return False except OpenStackCloudException: @@ -4519,19 +4519,19 @@ def _add_auto_ip(self, server, wait=False, timeout=60, reuse=True): # resource self.log.error( "Timeout waiting for floating IP to become" - " active. Floating IP {ip}:{id} was created for" - " server {server} but is being deleted due to" - " activation failure.".format( - ip=f_ip['floating_ip_address'], - id=f_ip['id'], - server=server['id'])) + " active. Floating IP %(ip)s:%(id)s was created for" + " server %(server)s but is being deleted due to" + " activation failure.", { + 'ip': f_ip['floating_ip_address'], + 'id': f_ip['id'], + 'server': server['id']}) try: self.delete_floating_ip(f_ip['id']) except Exception as e: self.log.error( "FIP LEAK: Attempted to delete floating ip " - "{fip} but received {exc} exception: {err}".format( - fip=f_ip['id'], exc=e.__class__, err=str(e))) + "%(fip)s but received %(exc)s exception: %(err)s", + {'fip': f_ip['id'], 'exc': e.__class__, 'err': str(e)}) raise e raise @@ -4915,9 +4915,9 @@ def get_active_server( wait=wait, timeout=timeout) self.log.debug( - 'Server {server} reached ACTIVE state without' + 'Server %(server)s reached ACTIVE state without' ' being allocated an IP address.' - ' Deleting server.'.format(server=server['id'])) + ' Deleting server.', {'server': server['id']}) try: self._delete_server( server=server, wait=wait, timeout=timeout) @@ -5153,7 +5153,7 @@ def delete_server_group(self, name_or_id): """ server_group = self.get_server_group(name_or_id) if not server_group: - self.log.debug("Server group %s not found for deleting" % + self.log.debug("Server group %s not found for deleting", name_or_id) return False @@ -5245,7 +5245,7 @@ def get_container_access(self, name): def _get_file_hashes(self, filename): if filename not in self._file_hash_cache: self.log.debug( - 'Calculating hashes for {filename}'.format(filename=filename)) + 'Calculating hashes for %(filename)s', {'filename': filename}) md5 = hashlib.md5() sha256 = hashlib.sha256() with open(filename, 'rb') as file_obj: @@ -5255,10 +5255,10 @@ def _get_file_hashes(self, filename): self._file_hash_cache[filename] = dict( md5=md5.hexdigest(), sha256=sha256.hexdigest()) self.log.debug( - "Image file {filename} md5:{md5} sha256:{sha256}".format( - filename=filename, - md5=self._file_hash_cache[filename]['md5'], - sha256=self._file_hash_cache[filename]['sha256'])) + "Image file %(filename)s md5:%(md5)s sha256:%(sha256)s", + {'filename': filename, + 'md5': self._file_hash_cache[filename]['md5'], + 'sha256': self._file_hash_cache[filename]['sha256']}) return (self._file_hash_cache[filename]['md5'], self._file_hash_cache[filename]['sha256']) @@ -5308,18 +5308,18 @@ def is_object_stale( if metadata.get(OBJECT_MD5_KEY, '') != file_md5: self.log.debug( - "swift md5 mismatch: {filename}!={container}/{name}".format( - filename=filename, container=container, name=name)) + "swift md5 mismatch: %(filename)s!=%(container)s/%(name)s", + {'filename': filename, 'container': container, 'name': name}) return True if metadata.get(OBJECT_SHA256_KEY, '') != file_sha256: self.log.debug( - "swift sha256 mismatch: {filename}!={container}/{name}".format( - filename=filename, container=container, name=name)) + "swift sha256 mismatch: %(filename)s!=%(container)s/%(name)s", + {'filename': filename, 'container': container, 'name': name}) return True self.log.debug( - "swift object up to date: {container}/{name}".format( - container=container, name=name)) + "swift object up to date: %(container)s/%(name)s", + {'container': container, 'name': name}) return False def create_object( @@ -5374,8 +5374,8 @@ def create_object( if self.is_object_stale(container, name, filename, md5, sha256): self.log.debug( - "swift uploading {filename} to {container}/{name}".format( - filename=filename, container=container, name=name)) + "swift uploading %(filename)s to %(container)s/%(name)s", + {'filename': filename, 'container': container, 'name': name}) upload = swiftclient.service.SwiftUploadObject( source=filename, object_name=name) for r in self.manager.submit_task(_tasks.ObjectCreate( @@ -5625,7 +5625,7 @@ def delete_subnet(self, name_or_id): """ subnet = self.get_subnet(name_or_id) if not subnet: - self.log.debug("Subnet %s not found for deleting" % name_or_id) + self.log.debug("Subnet %s not found for deleting", name_or_id) return False with _utils.neutron_exceptions( @@ -5861,7 +5861,7 @@ def delete_port(self, name_or_id): """ port = self.get_port(name_or_id=name_or_id) if port is None: - self.log.debug("Port %s not found for deleting" % name_or_id) + self.log.debug("Port %s not found for deleting", name_or_id) return False with _utils.neutron_exceptions( @@ -5928,7 +5928,7 @@ def delete_security_group(self, name_or_id): secgroup = self.get_security_group(name_or_id) if secgroup is None: - self.log.debug('Security group %s not found for deleting' % + self.log.debug('Security group %s not found for deleting', name_or_id) return False @@ -6271,7 +6271,7 @@ def delete_zone(self, name_or_id): zone = self.get_zone(name_or_id) if zone is None: - self.log.debug("Zone %s not found for deleting" % name_or_id) + self.log.debug("Zone %s not found for deleting", name_or_id) return False with _utils.shade_exceptions( @@ -6387,12 +6387,12 @@ def delete_recordset(self, zone, name_or_id): zone = self.get_zone(zone) if zone is None: - self.log.debug("Zone %s not found for deleting" % zone) + self.log.debug("Zone %s not found for deleting", zone) return False recordset = self.get_recordset(zone['id'], name_or_id) if recordset is None: - self.log.debug("Recordset %s not found for deleting" % name_or_id) + self.log.debug("Recordset %s not found for deleting", name_or_id) return False with _utils.shade_exceptions( @@ -6518,8 +6518,8 @@ def delete_cluster_template(self, name_or_id): if not cluster_template: self.log.debug( - "ClusterTemplate {name_or_id} does not exist".format( - name_or_id=name_or_id), + "ClusterTemplate %(name_or_id)s does not exist", + {'name_or_id': name_or_id}, exc_info=True) return False @@ -6529,8 +6529,8 @@ def delete_cluster_template(self, name_or_id): _tasks.ClusterTemplateDelete(id=cluster_template['id'])) except magnum_exceptions.NotFound: self.log.debug( - "ClusterTemplate {id} not found when deleting." - " Ignoring.".format(id=cluster_template['id'])) + "ClusterTemplate %(id)s not found when deleting." + " Ignoring.", {'id': cluster_template['id']}) return False self.list_cluster_templates.invalidate(self) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index eff98ed8c..42263e082 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -481,8 +481,8 @@ def update_machine(self, name_or_id, chassis_uuid=None, driver=None, new_config['properties'] = properties except KeyError as e: self.log.debug( - "Unexpected machine response missing key %s [%s]" % ( - e.args[0], name_or_id)) + "Unexpected machine response missing key %s [%s]", + e.args[0], name_or_id) raise OpenStackCloudException( "Machine update failed - machine [%s] missing key %s. " "Potential API issue." @@ -871,7 +871,7 @@ def delete_service(self, name_or_id): """ service = self.get_service(name_or_id=name_or_id) if service is None: - self.log.debug("Service %s not found for deleting" % name_or_id) + self.log.debug("Service %s not found for deleting", name_or_id) return False if self.cloud_config.get_api_version('identity').startswith('2'): @@ -1067,7 +1067,7 @@ def delete_endpoint(self, id): """ endpoint = self.get_endpoint(id=id) if endpoint is None: - self.log.debug("Endpoint %s not found for deleting" % id) + self.log.debug("Endpoint %s not found for deleting", id) return False if self.cloud_config.get_api_version('identity').startswith('2'): @@ -1141,7 +1141,7 @@ def delete_domain(self, domain_id=None, name_or_id=None): dom = self.get_domain(None, name_or_id) if dom is None: self.log.debug( - "Domain {0} not found for deleting".format(name_or_id)) + "Domain %s not found for deleting", name_or_id) return False domain_id = dom['id'] @@ -1333,7 +1333,7 @@ def delete_group(self, name_or_id): group = self.get_group(name_or_id) if group is None: self.log.debug( - "Group {0} not found for deleting".format(name_or_id)) + "Group %s not found for deleting", name_or_id) return False with _utils.shade_exceptions( @@ -1507,7 +1507,7 @@ def delete_flavor(self, name_or_id): flavor = self.get_flavor(name_or_id, get_extra=False) if flavor is None: self.log.debug( - "Flavor {0} not found for deleting".format(name_or_id)) + "Flavor %s not found for deleting", name_or_id) return False with _utils.shade_exceptions("Unable to delete flavor {name}".format( @@ -1617,7 +1617,7 @@ def delete_role(self, name_or_id): role = self.get_role(name_or_id) if role is None: self.log.debug( - "Role {0} not found for deleting".format(name_or_id)) + "Role %s not found for deleting", name_or_id) return False with _utils.shade_exceptions("Unable to delete role {name}".format( @@ -1886,7 +1886,7 @@ def delete_aggregate(self, name_or_id): """ aggregate = self.get_aggregate(name_or_id) if not aggregate: - self.log.debug("Aggregate %s not found for deleting" % name_or_id) + self.log.debug("Aggregate %s not found for deleting", name_or_id) return False with _utils.shade_exceptions( diff --git a/shade/task_manager.py b/shade/task_manager.py index 97a80dc24..1c01b01ed 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -105,8 +105,8 @@ def run(self, client): self.done(self.main(client)) except keystoneauth1.exceptions.RetriableConnectionFailure: client.log.debug( - "Connection failure for {name}, retrying".format( - name=type(self).__name__)) + "Connection failure for %(name)s, retrying", + {'name': type(self).__name__}) self.done(self.main(client)) except Exception: raise @@ -147,7 +147,7 @@ def done(self, result): except (simplejson.scanner.JSONDecodeError, ValueError) as e: result_json = self._response.text self._client.log.debug( - 'Could not decode json in response: {e}'.format(e=str(e))) + 'Could not decode json in response: %(e)s', {'e': str(e)}) self._client.log.debug(result_json) if self.result_key: @@ -234,13 +234,13 @@ def submit_task(self, task, raw=False): underlying client call. """ self.log.debug( - "Manager %s running task %s" % (self.name, task.name)) + "Manager %s running task %s", self.name, task.name) start = time.time() task.run(self._client) end = time.time() self.log.debug( - "Manager %s ran task %s in %ss" % ( - self.name, task.name, (end - start))) + "Manager %s ran task %s in %ss", + self.name, task.name, (end - start)) return task.wait(raw) # Backwards compatibility submitTask = submit_task From e03f54ada63ff80ca7ef5ca77bad96513e5962ae Mon Sep 17 00:00:00 2001 From: Ghe Rivero Date: Fri, 14 Oct 2016 13:47:46 +0200 Subject: [PATCH 1078/3836] Add external_ipv4_floating_networks Those network not properly tagged as external, even if the provider is "physical_network" shouldn't be part of the list of external networks. When shade tries to get a floating ip, made use of this list, but neutron will fail if the network is not configured as "router:external = True" Change-Id: I2daa02805659ee2510a9203227d662161e60d51d --- shade/openstackcloud.py | 22 +++++++++++++++++--- shade/tests/unit/test_floating_ip_neutron.py | 4 ++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index dfd398576..297b69ce5 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1738,6 +1738,7 @@ def _reset_network_caches(self): # the cached value, since "None" is a valid value to find. with self._networks_lock: self._external_ipv4_networks = [] + self._external_ipv4_floating_networks = [] self._internal_ipv4_networks = [] self._external_ipv6_networks = [] self._internal_ipv6_networks = [] @@ -1747,6 +1748,7 @@ def _reset_network_caches(self): def _set_interesting_networks(self): external_ipv4_networks = [] + external_ipv4_floating_networks = [] internal_ipv4_networks = [] external_ipv6_networks = [] internal_ipv6_networks = [] @@ -1782,6 +1784,11 @@ def _set_interesting_networks(self): network['id'] not in self._internal_ipv4_names): external_ipv4_networks.append(network) + # External Floating IPv4 networks + if ('router:external' in network + and network['router:external']): + external_ipv4_floating_networks.append(network) + # Internal networks if (network['name'] in self._internal_ipv4_names or network['id'] in self._internal_ipv4_names): @@ -1902,6 +1909,7 @@ def _set_interesting_networks(self): network=self._default_network)) self._external_ipv4_networks = external_ipv4_networks + self._external_ipv4_floating_networks = external_ipv4_floating_networks self._internal_ipv4_networks = internal_ipv4_networks self._external_ipv6_networks = external_ipv6_networks self._internal_ipv6_networks = internal_ipv6_networks @@ -1974,6 +1982,14 @@ def get_external_ipv4_networks(self): self._find_interesting_networks() return self._external_ipv4_networks + def get_external_ipv4_floating_networks(self): + """Return the networks that are configured to route northbound. + + :returns: A list of network ``munch.Munch`` if one is found + """ + self._find_interesting_networks() + return self._external_ipv4_floating_networks + def get_internal_ipv4_networks(self): """Return the networks that are configured to not route northbound. @@ -3798,7 +3814,7 @@ def _neutron_available_floating_ips( # Use given list to get first matching external network floating_network_id = None for net in network: - for ext_net in self.get_external_ipv4_networks(): + for ext_net in self.get_external_ipv4_floating_networks(): if net in (ext_net['name'], ext_net['id']): floating_network_id = ext_net['id'] break @@ -3812,7 +3828,7 @@ def _neutron_available_floating_ips( ) else: # Get first existing external IPv4 network - networks = self.get_external_ipv4_networks() + networks = self.get_external_ipv4_floating_networks() if not networks: raise OpenStackCloudResourceNotFound( "unable to find an external network") @@ -3954,7 +3970,7 @@ def _neutron_create_floating_ip( "unable to find network for floating ips with id " "{0}".format(network_name_or_id)) else: - networks = self.get_external_ipv4_networks() + networks = self.get_external_ipv4_floating_networks() if networks: network_id = networks[0]['id'] else: diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 779cf1324..1418fc1d0 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -313,7 +313,7 @@ def test_available_floating_ip_neutron(self, @patch.object(_utils, '_filter_list') @patch.object(OpenStackCloud, '_neutron_create_floating_ip') @patch.object(OpenStackCloud, '_neutron_list_floating_ips') - @patch.object(OpenStackCloud, 'get_external_ipv4_networks') + @patch.object(OpenStackCloud, 'get_external_ipv4_floating_networks') @patch.object(OpenStackCloud, 'keystone_session') def test__neutron_available_floating_ips( self, @@ -350,7 +350,7 @@ def test__neutron_available_floating_ips( @patch.object(_utils, '_filter_list') @patch.object(OpenStackCloud, '_neutron_create_floating_ip') @patch.object(OpenStackCloud, '_neutron_list_floating_ips') - @patch.object(OpenStackCloud, 'get_external_ipv4_networks') + @patch.object(OpenStackCloud, 'get_external_ipv4_floating_networks') @patch.object(OpenStackCloud, 'keystone_session') def test__neutron_available_floating_ips_network( self, From 422ad9ccdb8b08a223af78a1d1f48df703dd1a9d Mon Sep 17 00:00:00 2001 From: Cedric Brandily Date: Tue, 18 Oct 2016 22:20:00 +0200 Subject: [PATCH 1079/3836] Clarify how to set SSL settings This change adds an example in order to clarif how to SSL settings in configuration files. Change-Id: Id047f21d0a51752f46b16e3f4efbfec62cfbd5fb --- README.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.rst b/README.rst index a47e98bed..f3d41c590 100644 --- a/README.rst +++ b/README.rst @@ -185,6 +185,19 @@ Client certs are also configurable. `cert` will be the client cert file location. In case the cert key is not included within the client cert file, its file location needs to be set via `key`. +.. code-block:: yaml + + # clouds.yaml + clouds: + secure: + auth: ... + key: /home/myhome/client-cert.key + cert: /home/myhome/client-cert.crt + cacert: /home/myhome/ca.crt + insecure: + auth: ... + verify: False + Cache Settings -------------- From 1ac6766537f6ac8f2b1c22ac726a2fb8ad9ce6dd Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 8 Sep 2016 16:10:16 -0500 Subject: [PATCH 1080/3836] Fix a bunch of tests There is a bug in validate_auth that gets hidden by validate_auth_ksc. These tests are the test fixes for it. Change-Id: I80d558a1c794725ba2a87fbd87bf8fbdf6633bee --- os_client_config/config.py | 9 +++++++++ os_client_config/tests/test_config.py | 6 +++--- os_client_config/tests/test_environ.py | 26 ++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 6ff2359ff..a0d862d15 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -935,6 +935,15 @@ def _validate_auth(self, config, loader): winning_value, ) + if winning_value: + # Prefer the plugin configuration dest value if the value's key + # is marked as deprecated. + if p_opt.dest is None: + good_name = p_opt.name.replace('-', '_') + config['auth'][good_name] = winning_value + else: + config['auth'][p_opt.dest] = winning_value + # See if this needs a prompting config = self.option_prompt(config, p_opt) diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index 0ef8da2f2..ad3685ab1 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -735,7 +735,7 @@ def test_register_argparse_service_type(self): self.assertEqual(opts.http_timeout, '20') with testtools.ExpectedException(AttributeError): opts.os_network_service_type - cloud = c.get_one_cloud(argparse=opts, verify=False) + cloud = c.get_one_cloud(argparse=opts, validate=False) self.assertEqual(cloud.config['service_type'], 'network') self.assertEqual(cloud.config['interface'], 'admin') self.assertEqual(cloud.config['api_timeout'], '20') @@ -756,7 +756,7 @@ def test_register_argparse_network_service_type(self): self.assertIsNone(opts.os_network_service_type) self.assertIsNone(opts.os_network_api_version) self.assertEqual(opts.network_api_version, '4') - cloud = c.get_one_cloud(argparse=opts, verify=False) + cloud = c.get_one_cloud(argparse=opts, validate=False) self.assertEqual(cloud.config['service_type'], 'network') self.assertEqual(cloud.config['interface'], 'admin') self.assertEqual(cloud.config['network_api_version'], '4') @@ -783,7 +783,7 @@ def test_register_argparse_network_service_types(self): self.assertEqual(opts.os_endpoint_type, 'admin') self.assertIsNone(opts.os_network_api_version) self.assertEqual(opts.network_api_version, '4') - cloud = c.get_one_cloud(argparse=opts, verify=False) + cloud = c.get_one_cloud(argparse=opts, validate=False) self.assertEqual(cloud.config['service_type'], 'compute') self.assertEqual(cloud.config['network_service_type'], 'badtype') self.assertEqual(cloud.config['interface'], 'admin') diff --git a/os_client_config/tests/test_environ.py b/os_client_config/tests/test_environ.py index b75db1c61..35ce2f2bf 100644 --- a/os_client_config/tests/test_environ.py +++ b/os_client_config/tests/test_environ.py @@ -139,11 +139,30 @@ def test_test_envvars(self): self.assertRaises( exceptions.OpenStackConfigException, c.get_one_cloud, 'envvars') + def test_incomplete_envvars(self): + self.useFixture( + fixtures.EnvironmentVariable('NOVA_USERNAME', 'nova')) + self.useFixture( + fixtures.EnvironmentVariable('OS_USERNAME', 'user')) + config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + # This is broken due to an issue that's fixed in a subsequent patch + # commenting it out in this patch to keep the patch size reasonable + # self.assertRaises( + # keystoneauth1.exceptions.auth_plugins.MissingRequiredOptions, + # c.get_one_cloud, 'envvars') + def test_have_envvars(self): self.useFixture( fixtures.EnvironmentVariable('NOVA_USERNAME', 'nova')) + self.useFixture( + fixtures.EnvironmentVariable('OS_AUTH_URL', 'http://example.com')) self.useFixture( fixtures.EnvironmentVariable('OS_USERNAME', 'user')) + self.useFixture( + fixtures.EnvironmentVariable('OS_PASSWORD', 'password')) + self.useFixture( + fixtures.EnvironmentVariable('OS_PROJECT_NAME', 'project')) c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) cc = c.get_one_cloud('envvars') @@ -152,6 +171,13 @@ def test_have_envvars(self): def test_old_envvars(self): self.useFixture( fixtures.EnvironmentVariable('NOVA_USERNAME', 'nova')) + self.useFixture( + fixtures.EnvironmentVariable( + 'NOVA_AUTH_URL', 'http://example.com')) + self.useFixture( + fixtures.EnvironmentVariable('NOVA_PASSWORD', 'password')) + self.useFixture( + fixtures.EnvironmentVariable('NOVA_PROJECT_NAME', 'project')) c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml], envvar_prefix='NOVA_') From 86ade8f5281d2bb8b7c1637436aa8cb0c7cddb98 Mon Sep 17 00:00:00 2001 From: Ghe Rivero Date: Fri, 21 Oct 2016 12:16:51 +0200 Subject: [PATCH 1081/3836] Normalize cloud config before osc-lib call Cloud config was passed to osc-lib before being normalized, causing exceptions when some api versions were stil an int where osc-lib expects a str Change-Id: I7326114d86a4208f1489c302e8bb838dd5b8c5d6 --- os_client_config/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 6ff2359ff..774a2599e 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -1085,6 +1085,7 @@ def get_one_cloud(self, cloud=None, validate=True, config[key] = val config = self.magic_fixes(config) + config = self._normalize_keys(config) # NOTE(dtroyer): OSC needs a hook into the auth args before the # plugin is loaded in order to maintain backward- @@ -1127,7 +1128,7 @@ def get_one_cloud(self, cloud=None, validate=True, return cloud_config.CloudConfig( name=cloud_name, region=config['region_name'], - config=self._normalize_keys(config), + config=config, force_ipv4=force_ipv4, auth_plugin=auth_plugin, openstack_config=self, From 3f525e01798b433732139a9bff88d9360613cd71 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 20 Oct 2016 08:41:18 -0500 Subject: [PATCH 1082/3836] Add support for volumev3 service type Words cannot begin to adequately express the disappointment and rage I felt upon learning that the cinder team had unleashed 'volumev3' upon the world. Woe betide us, the mere users, for wanting to use a 'volume' endpoint and have that choice mean something. Perhaps if we beat ourselves with leather straps while crawling for days across the country we can remove more joy from our lives. In the meantime, until we can fully appreciate the existential crisis of being, let's continue to work around it in os-client-config. Change-Id: I171e3b01497b3e3a06c3a73577f0f67e0c1e6f73 --- os_client_config/cloud_config.py | 9 ++++++--- os_client_config/tests/test_cloud_config.py | 5 +++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 147eb04d4..82442bb78 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -160,9 +160,12 @@ def get_service_type(self, service_type): # atrocity from the as-yet-unsullied eyes of our users. # Of course, if the user requests a volumev2, that structure should # still work. - if (service_type == 'volume' and - self.get_api_version(service_type).startswith('2')): - service_type = 'volumev2' + # What's even more amazing is that they did it AGAIN with cinder v3 + if service_type == 'volume': + if self.get_api_version(service_type).startswith('2'): + service_type = 'volumev2' + elif self.get_api_version(service_type).startswith('3'): + service_type = 'volumev3' return self.config.get(key, service_type) def get_service_name(self, service_type): diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index 2763f4d3d..e3d1d5d6f 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -158,6 +158,11 @@ def test_volume_override(self): cc.config['volume_api_version'] = '2' self.assertEqual('volumev2', cc.get_service_type('volume')) + def test_volume_override_v3(self): + cc = cloud_config.CloudConfig("test1", "region-al", fake_services_dict) + cc.config['volume_api_version'] = '3' + self.assertEqual('volumev3', cc.get_service_type('volume')) + def test_get_session_no_auth(self): config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) From 4dad7b2e693f02da2fe9f1dc64af51b3c4f891dc Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 17 Oct 2016 11:19:04 -0500 Subject: [PATCH 1083/3836] Document and be more explicit in normalization Put extra keys in both the root resource and in a properties dict. Ensure data types are correct. Make sure int, float and bool values are returned as int and bool. Change disabled in flavor to is_disabled for consistency with other bools we've added. There has been no release with the addition of disabled, so changing it now is still safe. Add locations and direct_url to images. They're optional in glance, but that's evil. Let image schema attribute fall through to extra properties. Add zone to current_location. Add readable mappings for power_state, task_state, vm_state, launched_at and terminated_at for Servers. Also add a non-camel-cased host_id. This is a big patch, but it's mostly just reorganizing and adding docs. Looking at the changes to the tests and seeing that the only change is adding zone and properties into a couple of fixtures is a good place to start. Change-Id: If5674c049c8dd85ca0b3483b7c2dc82b9e139bd6 --- doc/source/index.rst | 1 + doc/source/model.rst | 203 ++++++++++++ .../notes/data-model-cf50d86982646370.yaml | 8 + shade/_normalize.py | 291 ++++++++++++++---- shade/openstackcloud.py | 3 +- shade/tests/unit/test__utils.py | 6 + shade/tests/unit/test_meta.py | 3 +- 7 files changed, 447 insertions(+), 68 deletions(-) create mode 100644 doc/source/model.rst create mode 100644 releasenotes/notes/data-model-cf50d86982646370.yaml diff --git a/doc/source/index.rst b/doc/source/index.rst index 2aa8bd35e..ad8969530 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -13,6 +13,7 @@ Contents: installation usage + model contributing coding future diff --git a/doc/source/model.rst b/doc/source/model.rst new file mode 100644 index 000000000..3b50641df --- /dev/null +++ b/doc/source/model.rst @@ -0,0 +1,203 @@ +========== +Data Model +========== + +shade has a very strict policy on not breaking backwards compatability ever. +However, with the data structures returned from OpenStack, there are places +where the resource structures from OpenStack are returned to the user somewhat +directly, leaving a shade user open to changes/differences in result content. + +To combat that, shade 'normalizes' the return structure from OpenStack in many +places, and the results of that normalization are listed below. Where shade +performs normalization, a user can count on any fields declared in the docs +as being completely safe to use - they are as much a part of shade's API +contract as any other Python method. + +Some OpenStack objects allow for arbitrary attributes at +the root of the object. shade will pass those through so as not to break anyone +who may be counting on them, but as they are arbitrary shade can make no +guarantees as to their existence. As part of normalization, shade will put any +attribute from an OpenStack resource that is not in its data model contract +into an attribute called 'properties'. The contents of properties are +defined to be an arbitrary collection of key value pairs with no promises as +to any particular key ever existing. + +Location +-------- + +A Location defines where a resource lives. It includes a cloud name and a +region name, an availability zone as well as information about the project +that owns the resource. + +The project information may contain a project id, or a combination of one or +more of a project name with a domain name or id. If a project id is present, +it should be considered correct. + +Some resources do not carry ownership information with them. For those, the +project information will be filled in from the project the user currently +has a token for. + +Some resources do not have information about availability zones, or may exist +region wide. Those resources will have None as their availability zone. + +If all of the project information is None, then + +.. code-block:: python + + Location = dict( + cloud=str(), + region=str(), + zone=str() or None, + project=dict( + id=str() or None, + name=str() or None, + domain_id=str() or None, + domain_name=str() or None)) + + +Flavor +------ + +A flavor for a Nova Server. + +.. code-block:: python + + Flavor = dict( + location=Location(), + id=str(), + name=str(), + is_public=bool(), + is_disabled=bool(), + ram=int(), + vcpus=int(), + disk=int(), + ephemeral=int(), + swap=int(), + rxtx_factor=float(), + extra_specs=dict(), + properties=dict()) + +Image +----- + +A Glance Image. + +.. code-block:: python + + Image = dict( + location=Location(), + id=str(), + name=str(), + min_ram=int(), + min_disk=int(), + size=int(), + virtual_size=int(), + container_format=str(), + disk_format=str(), + checksum=str(), + created_at=str(), + updated_at=str(), + owner=str(), + is_public=bool(), + is_protected=bool(), + status=str(), + locations=list(), + direct_url=str() or None, + tags=list(), + properties=dict()) + +Security Group +-------------- + +A Security Group from either Nova or Neutron + +.. code-block:: python + + SecurityGroup = dict( + location=Location(), + id=str(), + name=str(), + description=str(), + security_group_rules=list(), + properties=dict()) + +Security Group Rule +------------------- + +A Security Group Rule from either Nova or Neutron + +.. code-block:: python + + SecurityGroupRule = dict( + location=Location(), + id=str(), + direction=str(), # oneof('ingress', 'egress') + ethertype=str(), + port_range_min=int() or None, + port_range_max=int() or None, + protocol=str() or None, + remote_ip_prefix=str() or None, + security_group_id=str() or None, + remote_group_id=str() or None + properties=dict()) + +Server +------ + +A Server from Nova + +.. code-block:: python + + Server = dict( + location=Location(), + id=str(), + name=str(), + image=dict() or str(), + flavor=dict(), + volumes=list(), + interface_ip=str(), + has_config_drive=bool(), + accessIPv4=str(), + accessIPv6=str(), + addresses=dict(), + created=str(), + key_name=str(), + metadata=dict(), + networks=dict(), + private_v4=str(), + progress=int(), + public_v4=str(), + public_v6=str(), + security_groups=list(), + status=str(), + updated=str(), + user_id=str(), + host_id=str() or None, + power_state=str() or None, + task_state=str() or None, + vm_state=str() or None, + launched_at=str() or None, + terminated_at=str() or None, + task_state=str() or None, + properties=dict()) + +Floating IP +----------- + +A Floating IP from Neutron or Nova + + +.. code-block:: python + + FloatingIP = dict( + location=Location(), + id=str(), + attached=bool(), + fixed_ip_address=str() or None, + floating_ip_address=str() or None, + floating_network_id=str() or None, + network=str(), + port_id=str() or None, + router_id=str(), + status=str(), + properties=dict()) diff --git a/releasenotes/notes/data-model-cf50d86982646370.yaml b/releasenotes/notes/data-model-cf50d86982646370.yaml new file mode 100644 index 000000000..66a814aae --- /dev/null +++ b/releasenotes/notes/data-model-cf50d86982646370.yaml @@ -0,0 +1,8 @@ +--- +features: + - Explicit data model contracts are now defined for + Flavors, Images, Security Groups, Security Group Rules, + and Servers. + - Resources with data model contracts are now being returned with + 'location' attribute. The location carries cloud name, region + name and information about the project that owns the resource. diff --git a/shade/_normalize.py b/shade/_normalize.py index fbcc3ef30..24dbbdfaf 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -12,12 +12,16 @@ # 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 ast + import munch +import six _IMAGE_FIELDS = ( 'checksum', 'container_format', 'created_at', + 'direct_url', 'disk_format', 'file', 'id', @@ -25,8 +29,6 @@ 'min_ram', 'name', 'owner', - 'protected', - 'schema', 'size', 'status', 'tags', @@ -34,6 +36,41 @@ 'virtual_size', ) +_SERVER_FIELDS = ( + 'accessIPv4', + 'accessIPv6', + 'addresses', + 'adminPass', + 'created', + 'key_name', + 'metadata', + 'networks', + 'private_v4', + 'public_v4', + 'public_v6', + 'security_groups', + 'status', + 'updated', + 'user_id', +) + + +def _to_bool(value): + if isinstance(value, six.string_types): + # ast.literal_eval becomes VERY unhappy on empty strings + if not value: + return False + return ast.literal_eval(value.lower().capitalize()) + return bool(value) + + +def _pop_int(resource, key): + return int(resource.pop(key, 0) or 0) + + +def _pop_float(resource, key): + return float(resource.pop(key, 0) or 0) + class Normalizer(object): '''Mix-in class to provide the normalization functions. @@ -51,25 +88,47 @@ def _normalize_flavors(self, flavors): def _normalize_flavor(self, flavor): """ Normalize a flavor object """ + new_flavor = munch.Munch() + + # Copy incoming group because of shared dicts in unittests + flavor = flavor.copy() + + # Discard noise flavor.pop('links', None) flavor.pop('NAME_ATTR', None) flavor.pop('HUMAN_ID', None) flavor.pop('human_id', None) - if 'extra_specs' not in flavor: - flavor['extra_specs'] = {} - ephemeral = flavor.pop('OS-FLV-EXT-DATA:ephemeral', 0) - is_public = flavor.pop('os-flavor-access:is_public', True) - disabled = flavor.pop('OS-FLV-DISABLED:disabled', False) - # Make sure both the extension version and a sane version are present - flavor['OS-FLV-DISABLED:disabled'] = disabled - flavor['disabled'] = disabled - flavor['OS-FLV-EXT-DATA:ephemeral'] = ephemeral - flavor['ephemeral'] = ephemeral - flavor['os-flavor-access:is_public'] = is_public - flavor['is_public'] = is_public - flavor['location'] = self.current_location - - return flavor + + ephemeral = int(flavor.pop('OS-FLV-EXT-DATA:ephemeral', 0)) + ephemeral = flavor.pop('ephemeral', ephemeral) + is_public = _to_bool(flavor.pop('os-flavor-access:is_public', True)) + is_public = _to_bool(flavor.pop('is_public', True)) + is_disabled = _to_bool(flavor.pop('OS-FLV-DISABLED:disabled', False)) + extra_specs = flavor.pop('extra_specs', {}) + + new_flavor['location'] = self.current_location + new_flavor['id'] = flavor.pop('id') + new_flavor['name'] = flavor.pop('name') + new_flavor['is_public'] = is_public + new_flavor['is_disabled'] = is_disabled + new_flavor['ram'] = _pop_int(flavor, 'ram') + new_flavor['vcpus'] = _pop_int(flavor, 'vcpus') + new_flavor['disk'] = _pop_int(flavor, 'disk') + new_flavor['ephemeral'] = ephemeral + new_flavor['swap'] = _pop_int(flavor, 'swap') + new_flavor['rxtx_factor'] = _pop_float(flavor, 'rxtx_factor') + + new_flavor['properties'] = flavor.copy() + new_flavor['extra_specs'] = extra_specs + + # Backwards compat with nova - passthrough values + for (k, v) in new_flavor['properties'].items(): + new_flavor.setdefault(k, v) + new_flavor['OS-FLV-DISABLED:disabled'] = is_disabled + new_flavor['OS-FLV-EXT-DATA:ephemeral'] = ephemeral + new_flavor['os-flavor-access:is_public'] = is_public + + return new_flavor def _normalize_images(self, images): ret = [] @@ -80,8 +139,11 @@ def _normalize_images(self, images): def _normalize_image(self, image): new_image = munch.Munch( location=self._get_current_location(project_id=image.get('owner'))) + properties = image.pop('properties', {}) visibility = image.pop('visibility', None) + protected = _to_bool(image.pop('protected', False)) + if visibility: is_public = (visibility == 'public') else: @@ -90,12 +152,21 @@ def _normalize_image(self, image): for field in _IMAGE_FIELDS: new_image[field] = image.pop(field, None) + for field in ('min_ram', 'min_disk', 'size', 'virtual_size'): + new_image[field] = _pop_int(new_image, field) + new_image['is_protected'] = protected + new_image['locations'] = image.pop('locations', []) + for key, val in image.items(): - properties[key] = val - new_image[key] = val + properties.setdefault(key, val) new_image['properties'] = properties new_image['visibility'] = visibility new_image['is_public'] = is_public + + # Backwards compat with glance + for key, val in properties.items(): + new_image[key] = val + new_image['protected'] = protected return new_image def _normalize_secgroups(self, groups): @@ -116,16 +187,29 @@ def _normalize_secgroups(self, groups): def _normalize_secgroup(self, group): - rules = group.pop('security_group_rules', None) - if not rules and 'rules' in group: - rules = group.pop('rules') - group['security_group_rules'] = self._normalize_secgroup_rules(rules) - project_id = group.get('project_id', group.get('tenant_id', '')) - group['location'] = self._get_current_location(project_id=project_id) - # neutron sets these. we don't care about it, but let's be the same - group['tenant_id'] = project_id - group['project_id'] = project_id - return munch.Munch(group) + ret = munch.Munch() + # Copy incoming group because of shared dicts in unittests + group = group.copy() + + rules = self._normalize_secgroup_rules( + group.pop('security_group_rules', group.pop('rules', []))) + project_id = group.pop('tenant_id', '') + project_id = group.pop('project_id', project_id) + + ret['location'] = self._get_current_location(project_id=project_id) + ret['id'] = group.pop('id') + ret['name'] = group.pop('name') + ret['security_group_rules'] = rules + ret['description'] = group.pop('description') + ret['properties'] = group + + # Backwards compat with Neutron + ret['tenant_id'] = project_id + ret['project_id'] = project_id + for key, val in ret['properties'].items(): + ret.setdefault(key, val) + + return ret def _normalize_secgroup_rules(self, rules): """Normalize the structure of nova security group rules @@ -144,30 +228,42 @@ def _normalize_secgroup_rules(self, rules): def _normalize_secgroup_rule(self, rule): ret = munch.Munch() - ret['id'] = rule['id'] - ret['direction'] = rule.get('direction', 'ingress') - ret['ethertype'] = rule.get('ethertype', 'IPv4') + # Copy incoming rule because of shared dicts in unittests + rule = rule.copy() + + ret['id'] = rule.pop('id') + ret['direction'] = rule.pop('direction', 'ingress') + ret['ethertype'] = rule.pop('ethertype', 'IPv4') port_range_min = rule.get( - 'port_range_min', rule.get('from_port', None)) + 'port_range_min', rule.pop('from_port', None)) if port_range_min == -1: port_range_min = None + if port_range_min is not None: + port_range_min = int(port_range_min) ret['port_range_min'] = port_range_min - port_range_max = rule.get( - 'port_range_max', rule.get('to_port', None)) + port_range_max = rule.pop( + 'port_range_max', rule.pop('to_port', None)) if port_range_max == -1: port_range_max = None + if port_range_min is not None: + port_range_min = int(port_range_min) ret['port_range_max'] = port_range_max - ret['protocol'] = rule.get('protocol', rule.get('ip_protocol')) - ret['remote_ip_prefix'] = rule.get( - 'remote_ip_prefix', rule.get('ip_range', {}).get('cidr', None)) - ret['security_group_id'] = rule.get( - 'security_group_id', rule.get('parent_group_id')) - ret['remote_group_id'] = rule.get('remote_group_id') - project_id = rule.get('project_id', rule.get('tenant_id', '')) + ret['protocol'] = rule.pop('protocol', rule.pop('ip_protocol', None)) + ret['remote_ip_prefix'] = rule.pop( + 'remote_ip_prefix', rule.pop('ip_range', {}).get('cidr', None)) + ret['security_group_id'] = rule.pop( + 'security_group_id', rule.pop('parent_group_id', None)) + ret['remote_group_id'] = rule.pop('remote_group_id', None) + project_id = rule.pop('tenant_id', '') + project_id = rule.pop('project_id', project_id) ret['location'] = self._get_current_location(project_id=project_id) - # neutron sets these. we don't care about it, but let's be the same + ret['properties'] = rule + + # Backwards compat with Neutron ret['tenant_id'] = project_id ret['project_id'] = project_id + for key, val in ret['properties'].items(): + ret.setdefault(key, val) return ret def _normalize_servers(self, servers): @@ -179,26 +275,73 @@ def _normalize_servers(self, servers): return ret def _normalize_server(self, server): + ret = munch.Munch() + # Copy incoming server because of shared dicts in unittests + server = server.copy() + server.pop('links', None) + server.pop('NAME_ATTR', None) + server.pop('HUMAN_ID', None) + server.pop('human_id', None) + + ret['id'] = server.pop('id') + ret['name'] = server.pop('name') + server['flavor'].pop('links', None) + ret['flavor'] = server.pop('flavor') + # OpenStack can return image as a string when you've booted # from volume if str(server['image']) != server['image']: server['image'].pop('links', None) + ret['image'] = server.pop('image') - server['region'] = self.region_name - server['cloud'] = self.name - server['location'] = self._get_current_location( - project_id=server.get('tenant_id')) + project_id = server.pop('tenant_id', '') + project_id = server.pop('project_id', project_id) az = server.get('OS-EXT-AZ:availability_zone', None) - if az: - server['az'] = az + ret['location'] = self._get_current_location( + project_id=project_id, zone=az) # Ensure volumes is always in the server dict, even if empty - server['volumes'] = [] - - return server + ret['volumes'] = [] + + config_drive = server.pop('config_drive', False) + ret['has_config_drive'] = _to_bool(config_drive) + + host_id = server.pop('hostId', None) + ret['host_id'] = host_id + + ret['progress'] = _pop_int(server, 'progress') + + # Leave these in so that the general properties handling works + ret['disk_config'] = server.get('OS-DCF:diskConfig') + for key in ( + 'OS-EXT-STS:power_state', + 'OS-EXT-STS:task_state', + 'OS-EXT-STS:vm_state', + 'OS-SRV-USG:launched_at', + 'OS-SRV-USG:terminated_at'): + short_key = key.split(':')[1] + ret[short_key] = server.get(key) + + for field in _SERVER_FIELDS: + ret[field] = server.pop(field, None) + ret['interface_ip'] = '' + + ret['properties'] = server.copy() + for key, val in ret['properties'].items(): + ret.setdefault(key, val) + + # Backwards compat + ret['hostId'] = host_id + ret['config_drive'] = config_drive + ret['project_id'] = project_id + ret['tenant_id'] = project_id + ret['region'] = self.region_name + ret['cloud'] = self.name + ret['az'] = az + return ret def _normalize_floating_ips(self, ips): """Normalize the structure of floating IPs @@ -234,31 +377,47 @@ def _normalize_floating_ips(self, ips): ] def _normalize_floating_ip(self, ip): - fixed_ip_address = ip.get('fixed_ip_address', ip.get('fixed_ip')) - floating_ip_address = ip.get('floating_ip_address', ip.get('ip')) - network_id = ip.get( - 'floating_network_id', ip.get('network', ip.get('pool'))) - project_id = ip.get('project_id', ip.get('tenant_id', '')) + ret = munch.Munch() + + # Copy incoming floating ip because of shared dicts in unittests + ip = ip.copy() + + fixed_ip_address = ip.pop('fixed_ip_address', ip.pop('fixed_ip', None)) + floating_ip_address = ip.pop('floating_ip_address', ip.pop('ip', None)) + network_id = ip.pop( + 'floating_network_id', ip.pop('network', ip.pop('pool', None))) + project_id = ip.pop('tenant_id', '') + project_id = ip.pop('project_id', project_id) + + instance_id = ip.pop('instance_id', None) + router_id = ip.pop('router_id', None) + id = ip.pop('id') + port_id = ip.pop('port_id', None) + if self._use_neutron_floating(): - attached = (ip.get('port_id') is not None and ip['port_id'] != '') - status = ip.get('status', 'UNKNOWN') + attached = bool(port_id) + status = ip.pop('status', 'UNKNOWN') else: - instance_id = ip.get('instance_id') - attached = instance_id is not None and instance_id != '' + attached = bool(instance_id) # In neutron's terms, Nova floating IPs are always ACTIVE status = 'ACTIVE' - return munch.Munch( + ret = munch.Munch( attached=attached, fixed_ip_address=fixed_ip_address, floating_ip_address=floating_ip_address, floating_network_id=network_id, - id=ip['id'], + id=id, location=self._get_current_location(project_id=project_id), network=network_id, - port_id=ip.get('port_id'), + port_id=port_id, project_id=project_id, - router_id=ip.get('router_id'), + router_id=router_id, status=status, - tenant_id=project_id + tenant_id=project_id, + properties=ip.copy(), ) + for key, val in ret['properties'].items(): + ret.setdefault(key, val) + + return ret diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index a454ac596..ce8e6c1a6 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -448,10 +448,11 @@ def current_location(self): """Return a ``munch.Munch`` explaining the current cloud location.""" return self._get_current_location() - def _get_current_location(self, project_id=None): + def _get_current_location(self, project_id=None, zone=None): return munch.Munch( cloud=self.name, region_name=self.region_name, + zone=zone, project=self._get_project_info(project_id), ) diff --git a/shade/tests/unit/test__utils.py b/shade/tests/unit/test__utils.py index 02bfffd0c..704e66e1b 100644 --- a/shade/tests/unit/test__utils.py +++ b/shade/tests/unit/test__utils.py @@ -97,8 +97,10 @@ def test_normalize_secgroups(self): description='A Nova security group', tenant_id='', project_id='', + properties={}, location=dict( region_name='RegionOne', + zone=None, project=dict( domain_name=None, id=mock.ANY, @@ -109,11 +111,13 @@ def test_normalize_secgroups(self): dict(id='123', direction='ingress', ethertype='IPv4', port_range_min=80, port_range_max=81, protocol='tcp', remote_ip_prefix='0.0.0.0/0', security_group_id='xyz123', + properties={}, tenant_id='', project_id='', remote_group_id=None, location=dict( region_name='RegionOne', + zone=None, project=dict( domain_name=None, id=mock.ANY, @@ -151,8 +155,10 @@ def test_normalize_secgroup_rules(self): port_range_min=80, port_range_max=81, protocol='tcp', remote_ip_prefix='0.0.0.0/0', security_group_id='xyz123', tenant_id='', project_id='', remote_group_id=None, + properties={}, location=dict( region_name='RegionOne', + zone=None, project=dict( domain_name=None, id=mock.ANY, diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 035169c75..7570dbe1e 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -885,7 +885,8 @@ def test_current_location(self): 'domain_id': None, 'domain_name': None }, - 'region_name': u'RegionOne'}, + 'region_name': u'RegionOne', + 'zone': None}, self.cloud.current_location) def test_current_project(self): From c1129ea64fd8874c49828ce49ef543c006d84431 Mon Sep 17 00:00:00 2001 From: Matthew Wagoner Date: Sun, 23 Oct 2016 00:22:05 -0400 Subject: [PATCH 1084/3836] Add test for os_keystone_role Ansible module Change-Id: I86b18e894e750e7a7d9860d5b590c7c952722606 --- .../tests/ansible/roles/keystone_role/tasks/main.yml | 12 ++++++++++++ .../tests/ansible/roles/keystone_role/vars/main.yml | 1 + shade/tests/ansible/run.yml | 1 + 3 files changed, 14 insertions(+) create mode 100644 shade/tests/ansible/roles/keystone_role/tasks/main.yml create mode 100644 shade/tests/ansible/roles/keystone_role/vars/main.yml diff --git a/shade/tests/ansible/roles/keystone_role/tasks/main.yml b/shade/tests/ansible/roles/keystone_role/tasks/main.yml new file mode 100644 index 000000000..110b4386b --- /dev/null +++ b/shade/tests/ansible/roles/keystone_role/tasks/main.yml @@ -0,0 +1,12 @@ +--- +- name: Create keystone role + os_keystone_role: + cloud: "{{ cloud }}" + state: present + name: "{{ role_name }}" + +- name: Delete keystone role + os_keystone_role: + cloud: "{{ cloud }}" + state: absent + name: "{{ role_name }}" diff --git a/shade/tests/ansible/roles/keystone_role/vars/main.yml b/shade/tests/ansible/roles/keystone_role/vars/main.yml new file mode 100644 index 000000000..d1ebe5d1c --- /dev/null +++ b/shade/tests/ansible/roles/keystone_role/vars/main.yml @@ -0,0 +1 @@ +role_name: ansible_keystone_role diff --git a/shade/tests/ansible/run.yml b/shade/tests/ansible/run.yml index 5d4f077e9..27ad8af0f 100644 --- a/shade/tests/ansible/run.yml +++ b/shade/tests/ansible/run.yml @@ -10,6 +10,7 @@ - { role: image, tags: image } - { role: keypair, tags: keypair } - { role: keystone_domain, tags: keystone_domain } + - { role: keystone_role, tags: keystone_role } - { role: network, tags: network } - { role: nova_flavor, tags: nova_flavor } - { role: object, tags: object } From b5d65b74f60ce743b03b49a6c176700d658cfe98 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 24 Oct 2016 05:40:32 +0200 Subject: [PATCH 1085/3836] Support token_endpoint as an auth_type For backwards compat with what operators have been trained to do, map token_endpoint to admin_token for them. This has shown up a few times in the wild. Most recently: https://github.com/ansible/ansible-modules-core/issues/5250 Change-Id: Ie083381e7fda19e016b6425939bd3c2dc260fa9b --- os_client_config/config.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/os_client_config/config.py b/os_client_config/config.py index a0d862d15..dc3decd38 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -854,6 +854,11 @@ def _get_auth_loader(self, config): # Set to notused rather than None because validate_auth will # strip the value if it's actually python None config['auth']['token'] = 'notused' + elif config['auth_type'] == 'token_endpoint': + # Humans have been trained to use a thing called token_endpoint + # That it does not exist in keystoneauth is irrelvant- it not + # doing what they want causes them sorrow. + config['auth_type'] = 'admin_token' return loading.get_plugin_loader(config['auth_type']) def _validate_auth_ksc(self, config, cloud): From f0e9a42ac06ba682ae38bbd40ecd5bdf42f08c7c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 24 Oct 2016 05:20:49 +0200 Subject: [PATCH 1086/3836] Try to return working IP if we get more than one Rather than just returning the first one - if we have multiple choice, take a quick stab at picking one if one works and one does not. Change-Id: Ie517af9c10a2a1384f670dca5526f2b919458279 --- shade/meta.py | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/shade/meta.py b/shade/meta.py index 492367d98..e5163bd24 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -16,6 +16,7 @@ import munch import ipaddress import six +import socket from shade import _log from shade import exc @@ -55,9 +56,7 @@ def find_nova_addresses(addresses, ext_tag=None, key_name=None, version=4): def get_server_ip(server, **kwargs): addrs = find_nova_addresses(server['addresses'], **kwargs) - if not addrs: - return None - return addrs[0] + return find_best_address(addrs, socket.AF_INET) def get_server_private_ip(server, cloud=None): @@ -162,6 +161,30 @@ def get_server_external_ipv4(cloud, server): return None +def find_best_address(addresses, family): + if not addresses: + return None + if len(addresses) == 1: + return addresses[0] + if len(addresses) > 1: + for address in addresses: + # Return the first one that is reachable + try: + connect_socket = socket.socket(family, socket.SOCK_STREAM, 0) + connect_socket.settimeout(1) + connect_socket.connect((address, 22, 0, 0)) + return address + except Exception: + pass + # Give up and return the first - none work as far as we can tell + log = _log.setup_logging('shade') + log.debug( + 'The cloud returned multiple addresses, and none of them seem' + ' to work. That might be what you wanted, but we have no clue' + " what's going on, so we just picked one at random") + return addresses[0] + + def get_server_external_ipv6(server): """ Get an IPv6 address reachable from outside the cloud. @@ -174,9 +197,7 @@ def get_server_external_ipv6(server): if server['accessIPv6']: return server['accessIPv6'] addresses = find_nova_addresses(addresses=server['addresses'], version=6) - if addresses: - return addresses[0] - return None + return find_best_address(addresses, socket.AF_INET6) def get_server_default_ip(cloud, server): From e545865f8ff0ec4c9af239231ebba87e3e7592f0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 19 Oct 2016 15:08:00 -0500 Subject: [PATCH 1087/3836] Don't fail image create on failure of cleanup The failure that caused us to run a cleanup delete is the thing we actually care about reporting. If the cleanup has an error, shrug and move on to reporting the creation error. Change-Id: If3956188214b2f627b14e4d820cf27c3080fd4dd --- shade/openstackcloud.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 99aeabf5f..581e93b82 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3026,9 +3026,14 @@ def _upload_image_put_v2(self, name, image_data, meta, **image_kwargs): self.manager.submit_task(_tasks.ImageUpload( image_id=image.id, image_data=image_data)) except Exception: - self.log.debug("Deleting failed upload of image %(image)s", - {'image': image['name']}) - self.manager.submit_task(_tasks.ImageDelete(image_id=image.id)) + self.log.debug("Deleting failed upload of image %s", name) + try: + self.manager.submit_task(_tasks.ImageDelete(image_id=image.id)) + except Exception: + # We're just trying to clean up - if it doesn't work - shrug + self.log.debug( + "Failed deleting image after we failed uploading it.", + exc_info=True) raise return image @@ -3043,10 +3048,15 @@ def _upload_image_put_v1( self.manager.submit_task(_tasks.ImageUpdate( image=image, data=image_data)) except Exception: - self.log.debug("Deleting failed upload of image %(image)s", - {'image': image['name']}) - # Note argument is "image" here, "image_id" in V2 - self.manager.submit_task(_tasks.ImageDelete(image=image.id)) + self.log.debug("Deleting failed upload of image %s", name) + try: + # Note argument is "image" here, "image_id" in V2 + self.manager.submit_task(_tasks.ImageDelete(image=image.id)) + except Exception: + # We're just trying to clean up - if it doesn't work - shrug + self.log.debug( + "Failed deleting image after we failed uploading it.", + exc_info=True) raise return image From dd0ee8eb31d3ceba52cd073e23f2ba9f64213a96 Mon Sep 17 00:00:00 2001 From: Jordan Pittier Date: Mon, 31 Oct 2016 15:10:58 +0100 Subject: [PATCH 1088/3836] list_security_groups: enable server-side filtering For instance, this allows to: * With Nova, list all security-groups, for all tenants * With Neutron, filter security-groups to a specific project. And much more (basically all filtering that Nova or Neutron allows). (Also, this review removes 2 useless setUp methods. Not related to this commit, but it's trivial and let's save some white bears) Change-Id: I3cc5de57d780c73faf252cdcb842e5cff08fee9b --- shade/_tasks.py | 4 +- shade/openstackcloud.py | 18 +++++-- shade/tests/functional/test_aggregate.py | 3 -- .../tests/functional/test_security_groups.py | 54 +++++++++++++++++++ shade/tests/functional/test_server_group.py | 3 -- shade/tests/unit/test_security_groups.py | 11 ++-- 6 files changed, 76 insertions(+), 17 deletions(-) create mode 100644 shade/tests/functional/test_security_groups.py diff --git a/shade/_tasks.py b/shade/_tasks.py index 5b894e73d..d50a755dc 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -431,7 +431,7 @@ def main(self, client): class NeutronSecurityGroupList(task_manager.Task): def main(self, client): - return client.neutron_client.list_security_groups() + return client.neutron_client.list_security_groups(**self.args) class NeutronSecurityGroupCreate(task_manager.Task): @@ -461,7 +461,7 @@ def main(self, client): class NovaSecurityGroupList(task_manager.Task): def main(self, client): - return client.nova_client.security_groups.list() + return client.nova_client.security_groups.list(**self.args) class NovaSecurityGroupCreate(task_manager.Task): diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 79b2c8a09..f407d7d9d 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1345,7 +1345,10 @@ def search_flavors(self, name_or_id=None, filters=None, get_extra=True): return _utils._filter_list(flavors, name_or_id, filters) def search_security_groups(self, name_or_id=None, filters=None): - groups = self.list_security_groups() + # `filters` could be a dict or a jmespath (str) + groups = self.list_security_groups( + filters=filters if isinstance(filters, dict) else None + ) return _utils._filter_list(groups, name_or_id, filters) def search_servers(self, name_or_id=None, filters=None, detailed=False): @@ -1412,7 +1415,7 @@ def list_networks(self, filters=None): """List all available networks. :param filters: (optional) dict of filter conditions to push down - :returns: A list of ``munch.Munc`` containing network info. + :returns: A list of ``munch.Munch`` containing network info. """ # Translate None from search interface to empty {} for kwargs below @@ -1553,9 +1556,10 @@ def list_server_security_groups(self, server): return self._normalize_secgroups(groups) - def list_security_groups(self): + def list_security_groups(self, filters=None): """List all available security groups. + :param filters: (optional) dict of filter conditions to push down :returns: A list of security group ``munch.Munch``. """ @@ -1565,6 +1569,9 @@ def list_security_groups(self): "Unavailable feature: security groups" ) + if not filters: + filters = {} + groups = [] # Handle neutron security groups if self._use_neutron_secgroups(): @@ -1572,13 +1579,14 @@ def list_security_groups(self): with _utils.neutron_exceptions( "Error fetching security group list"): groups = self.manager.submit_task( - _tasks.NeutronSecurityGroupList())['security_groups'] + _tasks.NeutronSecurityGroupList(**filters) + )['security_groups'] # Handle nova security groups else: with _utils.shade_exceptions("Error fetching security group list"): groups = self.manager.submit_task( - _tasks.NovaSecurityGroupList()) + _tasks.NovaSecurityGroupList(search_opts=filters)) return self._normalize_secgroups(groups) def list_servers(self, detailed=False): diff --git a/shade/tests/functional/test_aggregate.py b/shade/tests/functional/test_aggregate.py index 7e734e43c..093016e43 100644 --- a/shade/tests/functional/test_aggregate.py +++ b/shade/tests/functional/test_aggregate.py @@ -24,9 +24,6 @@ class TestAggregate(base.BaseFunctionalTestCase): - def setUp(self): - super(TestAggregate, self).setUp() - def test_aggregates(self): aggregate_name = self.getUniqueString() availability_zone = self.getUniqueString() diff --git a/shade/tests/functional/test_security_groups.py b/shade/tests/functional/test_security_groups.py new file mode 100644 index 000000000..a2aede029 --- /dev/null +++ b/shade/tests/functional/test_security_groups.py @@ -0,0 +1,54 @@ +# -*- 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. + +""" +test_security_groups +---------------------------------- + +Functional tests for `shade` security_groups resource. +""" + +from shade.tests.functional import base + + +class TestSecurityGroups(base.BaseFunctionalTestCase): + def test_create_list_security_groups(self): + sg1 = self.demo_cloud.create_security_group( + name="sg1", description="sg1") + self.addCleanup(self.demo_cloud.delete_security_group, sg1['id']) + sg2 = self.operator_cloud.create_security_group( + name="sg2", description="sg2") + self.addCleanup(self.operator_cloud.delete_security_group, sg2['id']) + + if self.demo_cloud.has_service('network'): + # Neutron defaults to all_tenants=1 when admin + sg_list = self.operator_cloud.list_security_groups() + self.assertIn(sg1['id'], [sg['id'] for sg in sg_list]) + + # Filter by tenant_id (filtering by project_id won't work with + # Keystone V2) + sg_list = self.operator_cloud.list_security_groups( + filters={'tenant_id': self.demo_cloud.current_project_id}) + self.assertIn(sg1['id'], [sg['id'] for sg in sg_list]) + self.assertNotIn(sg2['id'], [sg['id'] for sg in sg_list]) + + else: + # Nova does not list all tenants by default + sg_list = self.operator_cloud.list_security_groups() + self.assertIn(sg2['id'], [sg['id'] for sg in sg_list]) + self.assertNotIn(sg1['id'], [sg['id'] for sg in sg_list]) + + sg_list = self.operator_cloud.list_security_groups( + filters={'all_tenants': 1}) + self.assertIn(sg1['id'], [sg['id'] for sg in sg_list]) diff --git a/shade/tests/functional/test_server_group.py b/shade/tests/functional/test_server_group.py index e300e331a..5b3544723 100644 --- a/shade/tests/functional/test_server_group.py +++ b/shade/tests/functional/test_server_group.py @@ -24,9 +24,6 @@ class TestServerGroup(base.BaseFunctionalTestCase): - def setUp(self): - super(TestServerGroup, self).setUp() - def test_server_group(self): server_group_name = self.getUniqueString() self.addCleanup(self.cleanup, server_group_name) diff --git a/shade/tests/unit/test_security_groups.py b/shade/tests/unit/test_security_groups.py index f36882df5..72b38596a 100644 --- a/shade/tests/unit/test_security_groups.py +++ b/shade/tests/unit/test_security_groups.py @@ -67,8 +67,9 @@ def fake_has_service(*args, **kwargs): @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_list_security_groups_neutron(self, mock_nova, mock_neutron): self.cloud.secgroup_source = 'neutron' - self.cloud.list_security_groups() - self.assertTrue(mock_neutron.list_security_groups.called) + self.cloud.list_security_groups(filters={'project_id': 42}) + mock_neutron.list_security_groups.assert_called_once_with( + project_id=42) self.assertFalse(mock_nova.security_groups.list.called) @mock.patch.object(shade.OpenStackCloud, 'neutron_client') @@ -76,9 +77,11 @@ def test_list_security_groups_neutron(self, mock_nova, mock_neutron): def test_list_security_groups_nova(self, mock_nova, mock_neutron): self.cloud.secgroup_source = 'nova' self.has_neutron = False - self.cloud.list_security_groups() + self.cloud.list_security_groups(filters={'project_id': 42}) self.assertFalse(mock_neutron.list_security_groups.called) - self.assertTrue(mock_nova.security_groups.list.called) + mock_nova.security_groups.list.assert_called_once_with( + search_opts={'project_id': 42} + ) @mock.patch.object(shade.OpenStackCloud, 'neutron_client') @mock.patch.object(shade.OpenStackCloud, 'nova_client') From fa80a51d0f64efc14b33409552d4231ddb244d30 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 18 Oct 2016 06:48:19 -0500 Subject: [PATCH 1089/3836] Add strict mode for trimming out non-API data shade defaults to returning everything under the sun in every form possible in order to ensure maximum backwards compatability - even with systems that are not shade itself. However, passthrough fields from somewhere else could change at any time. This patch adds an opt-in flag that skips returning passthrough fields anywhere other than the properties dict. Change-Id: I7071a406965ed373e77f9592eb76975400cb426b --- doc/source/model.rst | 26 +- .../notes/strict-mode-d493abc0c3e87945.yaml | 6 + shade/__init__.py | 12 +- shade/_normalize.py | 106 +++-- shade/openstackcloud.py | 7 +- shade/tests/unit/base.py | 4 + shade/tests/unit/test__utils.py | 126 ------ shade/tests/unit/test_normalize.py | 394 ++++++++++++++++++ 8 files changed, 496 insertions(+), 185 deletions(-) create mode 100644 releasenotes/notes/strict-mode-d493abc0c3e87945.yaml create mode 100644 shade/tests/unit/test_normalize.py diff --git a/doc/source/model.rst b/doc/source/model.rst index 3b50641df..0154bb9f1 100644 --- a/doc/source/model.rst +++ b/doc/source/model.rst @@ -22,6 +22,16 @@ into an attribute called 'properties'. The contents of properties are defined to be an arbitrary collection of key value pairs with no promises as to any particular key ever existing. +If a user passes `strict=True` to the shade constructor, shade will not pass +through arbitrary objects to the root of the resource, and will instead only +put them in the properties dict. If a user is worried about accidentally +writing code that depends on an attribute that is not part of the API contract, +this can be a useful tool. Keep in mind all data can still be accessed via +the properties dict, but any code touching anything in the properties dict +should be aware that the keys found there are highly user/cloud specific. +Any key that is transformed as part of the shade data model contract will +not wind up with an entry in properties - only keys that are unknown. + Location -------- @@ -154,21 +164,20 @@ A Server from Nova name=str(), image=dict() or str(), flavor=dict(), - volumes=list(), + volumes=list(), # Volume interface_ip=str(), has_config_drive=bool(), accessIPv4=str(), accessIPv6=str(), - addresses=dict(), + addresses=dict(), # string, list(Address) created=str(), key_name=str(), - metadata=dict(), - networks=dict(), + metadata=dict(), # string, string private_v4=str(), progress=int(), public_v4=str(), public_v6=str(), - security_groups=list(), + security_groups=list(), # SecurityGroup status=str(), updated=str(), user_id=str(), @@ -195,9 +204,8 @@ A Floating IP from Neutron or Nova attached=bool(), fixed_ip_address=str() or None, floating_ip_address=str() or None, - floating_network_id=str() or None, - network=str(), - port_id=str() or None, - router_id=str(), + network=str() or None, + port=str() or None, + router=str(), status=str(), properties=dict()) diff --git a/releasenotes/notes/strict-mode-d493abc0c3e87945.yaml b/releasenotes/notes/strict-mode-d493abc0c3e87945.yaml new file mode 100644 index 000000000..ea81b138b --- /dev/null +++ b/releasenotes/notes/strict-mode-d493abc0c3e87945.yaml @@ -0,0 +1,6 @@ +--- +features: + - Added 'strict' mode, which is set by passing strict=True + to the OpenStackCloud constructor. strict mode tells shade + to only return values in resources that are part of shade's + declared data model contract. diff --git a/shade/__init__.py b/shade/__init__.py index e4d6d09d7..fc7fcc22e 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -55,7 +55,7 @@ def simple_logging(debug=False, http_debug=False): log = _log.setup_logging('keystoneauth.identity.generic.base') -def openstack_clouds(config=None, debug=False, cloud=None): +def openstack_clouds(config=None, debug=False, cloud=None, strict=False): if not config: config = os_client_config.OpenStackConfig() try: @@ -64,6 +64,7 @@ def openstack_clouds(config=None, debug=False, cloud=None): OpenStackCloud( cloud=f.name, debug=debug, cloud_config=f, + strict=strict, **f.config) for f in config.get_all_clouds() ] @@ -72,6 +73,7 @@ def openstack_clouds(config=None, debug=False, cloud=None): OpenStackCloud( cloud=f.name, debug=debug, cloud_config=f, + strict=strict, **f.config) for f in config.get_all_clouds() if f.name == cloud @@ -81,7 +83,7 @@ def openstack_clouds(config=None, debug=False, cloud=None): "Invalid cloud configuration: {exc}".format(exc=str(e))) -def openstack_cloud(config=None, **kwargs): +def openstack_cloud(config=None, strict=False, **kwargs): if not config: config = os_client_config.OpenStackConfig() try: @@ -89,10 +91,10 @@ def openstack_cloud(config=None, **kwargs): except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin as e: raise OpenStackCloudException( "Invalid cloud configuration: {exc}".format(exc=str(e))) - return OpenStackCloud(cloud_config=cloud_config) + return OpenStackCloud(cloud_config=cloud_config, strict=strict) -def operator_cloud(config=None, **kwargs): +def operator_cloud(config=None, strict=False, **kwargs): if 'interface' not in kwargs: kwargs['interface'] = 'admin' if not config: @@ -102,4 +104,4 @@ def operator_cloud(config=None, **kwargs): except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin as e: raise OpenStackCloudException( "Invalid cloud configuration: {exc}".format(exc=str(e))) - return OperatorCloud(cloud_config=cloud_config) + return OperatorCloud(cloud_config=cloud_config, strict=strict) diff --git a/shade/_normalize.py b/shade/_normalize.py index 24dbbdfaf..8a9561b39 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -12,8 +12,6 @@ # 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 ast - import munch import six @@ -57,10 +55,10 @@ def _to_bool(value): if isinstance(value, six.string_types): - # ast.literal_eval becomes VERY unhappy on empty strings if not value: return False - return ast.literal_eval(value.lower().capitalize()) + prospective = value.lower().capitalize() + return prospective == 'True' return bool(value) @@ -72,6 +70,13 @@ def _pop_float(resource, key): return float(resource.pop(key, 0) or 0) +def _pop_or_get(resource, key, default, strict): + if strict: + return resource.pop(key, default) + else: + return resource.get(key, default) + + class Normalizer(object): '''Mix-in class to provide the normalization functions. @@ -99,11 +104,14 @@ def _normalize_flavor(self, flavor): flavor.pop('HUMAN_ID', None) flavor.pop('human_id', None) - ephemeral = int(flavor.pop('OS-FLV-EXT-DATA:ephemeral', 0)) + ephemeral = int(_pop_or_get( + flavor, 'OS-FLV-EXT-DATA:ephemeral', 0, self.strict_mode)) ephemeral = flavor.pop('ephemeral', ephemeral) - is_public = _to_bool(flavor.pop('os-flavor-access:is_public', True)) - is_public = _to_bool(flavor.pop('is_public', True)) - is_disabled = _to_bool(flavor.pop('OS-FLV-DISABLED:disabled', False)) + is_public = _to_bool(_pop_or_get( + flavor, 'os-flavor-access:is_public', True, self.strict_mode)) + is_public = _to_bool(flavor.pop('is_public', is_public)) + is_disabled = _to_bool(_pop_or_get( + flavor, 'OS-FLV-DISABLED:disabled', False, self.strict_mode)) extra_specs = flavor.pop('extra_specs', {}) new_flavor['location'] = self.current_location @@ -122,11 +130,9 @@ def _normalize_flavor(self, flavor): new_flavor['extra_specs'] = extra_specs # Backwards compat with nova - passthrough values - for (k, v) in new_flavor['properties'].items(): - new_flavor.setdefault(k, v) - new_flavor['OS-FLV-DISABLED:disabled'] = is_disabled - new_flavor['OS-FLV-EXT-DATA:ephemeral'] = ephemeral - new_flavor['os-flavor-access:is_public'] = is_public + if not self.strict_mode: + for (k, v) in new_flavor['properties'].items(): + new_flavor.setdefault(k, v) return new_flavor @@ -164,9 +170,10 @@ def _normalize_image(self, image): new_image['is_public'] = is_public # Backwards compat with glance - for key, val in properties.items(): - new_image[key] = val - new_image['protected'] = protected + if not self.strict_mode: + for key, val in properties.items(): + new_image[key] = val + new_image['protected'] = protected return new_image def _normalize_secgroups(self, groups): @@ -204,10 +211,11 @@ def _normalize_secgroup(self, group): ret['properties'] = group # Backwards compat with Neutron - ret['tenant_id'] = project_id - ret['project_id'] = project_id - for key, val in ret['properties'].items(): - ret.setdefault(key, val) + if not self.strict_mode: + ret['tenant_id'] = project_id + ret['project_id'] = project_id + for key, val in ret['properties'].items(): + ret.setdefault(key, val) return ret @@ -260,10 +268,11 @@ def _normalize_secgroup_rule(self, rule): ret['properties'] = rule # Backwards compat with Neutron - ret['tenant_id'] = project_id - ret['project_id'] = project_id - for key, val in ret['properties'].items(): - ret.setdefault(key, val) + if not self.strict_mode: + ret['tenant_id'] = project_id + ret['project_id'] = project_id + for key, val in ret['properties'].items(): + ret.setdefault(key, val) return ret def _normalize_servers(self, servers): @@ -299,12 +308,15 @@ def _normalize_server(self, server): project_id = server.pop('tenant_id', '') project_id = server.pop('project_id', project_id) - az = server.get('OS-EXT-AZ:availability_zone', None) + az = _pop_or_get( + server, 'OS-EXT-AZ:availability_zone', None, self.strict_mode) ret['location'] = self._get_current_location( project_id=project_id, zone=az) # Ensure volumes is always in the server dict, even if empty - ret['volumes'] = [] + ret['volumes'] = _pop_or_get( + server, 'os-extended-volumes:volumes_attached', + [], self.strict_mode) config_drive = server.pop('config_drive', False) ret['has_config_drive'] = _to_bool(config_drive) @@ -315,7 +327,8 @@ def _normalize_server(self, server): ret['progress'] = _pop_int(server, 'progress') # Leave these in so that the general properties handling works - ret['disk_config'] = server.get('OS-DCF:diskConfig') + ret['disk_config'] = _pop_or_get( + server, 'OS-DCF:diskConfig', None, self.strict_mode) for key in ( 'OS-EXT-STS:power_state', 'OS-EXT-STS:task_state', @@ -323,24 +336,25 @@ def _normalize_server(self, server): 'OS-SRV-USG:launched_at', 'OS-SRV-USG:terminated_at'): short_key = key.split(':')[1] - ret[short_key] = server.get(key) + ret[short_key] = _pop_or_get(server, key, None, self.strict_mode) for field in _SERVER_FIELDS: ret[field] = server.pop(field, None) ret['interface_ip'] = '' ret['properties'] = server.copy() - for key, val in ret['properties'].items(): - ret.setdefault(key, val) # Backwards compat - ret['hostId'] = host_id - ret['config_drive'] = config_drive - ret['project_id'] = project_id - ret['tenant_id'] = project_id - ret['region'] = self.region_name - ret['cloud'] = self.name - ret['az'] = az + if not self.strict_mode: + ret['hostId'] = host_id + ret['config_drive'] = config_drive + ret['project_id'] = project_id + ret['tenant_id'] = project_id + ret['region'] = self.region_name + ret['cloud'] = self.name + ret['az'] = az + for key, val in ret['properties'].items(): + ret.setdefault(key, val) return ret def _normalize_floating_ips(self, ips): @@ -406,18 +420,22 @@ def _normalize_floating_ip(self, ip): attached=attached, fixed_ip_address=fixed_ip_address, floating_ip_address=floating_ip_address, - floating_network_id=network_id, id=id, location=self._get_current_location(project_id=project_id), network=network_id, - port_id=port_id, - project_id=project_id, - router_id=router_id, + port=port_id, + router=router_id, status=status, - tenant_id=project_id, properties=ip.copy(), ) - for key, val in ret['properties'].items(): - ret.setdefault(key, val) + # Backwards compat + if not self.strict_mode: + ret['port_id'] = port_id + ret['router_id'] = router_id + ret['project_id'] = project_id + ret['tenant_id'] = project_id + ret['floating_network_id'] = network_id, + for key, val in ret['properties'].items(): + ret.setdefault(key, val) return ret diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index ce8e6c1a6..ddcb8b24c 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -123,6 +123,8 @@ class OpenStackCloud(_normalize.Normalizer): have all of the wrapped exceptions be emitted to the error log. This flag will enable that behavior. + :param bool strict: Only return documented attributes for each resource + as per the shade Data Model contract. (Default False) :param CloudConfig cloud_config: Cloud config object from os-client-config In the future, this will be the only way to pass in cloud configuration, but is @@ -132,7 +134,9 @@ class OpenStackCloud(_normalize.Normalizer): def __init__( self, cloud_config=None, - manager=None, log_inner_exceptions=False, **kwargs): + manager=None, log_inner_exceptions=False, + strict=False, + **kwargs): if log_inner_exceptions: OpenStackCloudException.log_inner_exceptions = True @@ -151,6 +155,7 @@ def __init__( self.image_api_use_tasks = cloud_config.config['image_api_use_tasks'] self.secgroup_source = cloud_config.config['secgroup_source'] self.force_ipv4 = cloud_config.force_ipv4 + self.strict_mode = strict # Provide better error message for people with stale OCC if cloud_config.get_external_ipv4_networks is None: diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 8e3c18b68..f731f78f8 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -75,6 +75,10 @@ def _nosleep(seconds): self.cloud = shade.OpenStackCloud( cloud_config=self.cloud_config, log_inner_exceptions=True) + self.strict_cloud = shade.OpenStackCloud( + cloud_config=self.cloud_config, + log_inner_exceptions=True, + strict=True) self.op_cloud = shade.OperatorCloud( cloud_config=self.cloud_config, log_inner_exceptions=True) diff --git a/shade/tests/unit/test__utils.py b/shade/tests/unit/test__utils.py index 704e66e1b..4ca82d52a 100644 --- a/shade/tests/unit/test__utils.py +++ b/shade/tests/unit/test__utils.py @@ -12,7 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. -import mock import testtools from shade import _utils @@ -80,131 +79,6 @@ def test__filter_list_dict2(self): }}) self.assertEqual([el2, el3], ret) - def test_normalize_secgroups(self): - nova_secgroup = dict( - id='abc123', - name='nova_secgroup', - description='A Nova security group', - rules=[ - dict(id='123', from_port=80, to_port=81, ip_protocol='tcp', - ip_range={'cidr': '0.0.0.0/0'}, parent_group_id='xyz123') - ] - ) - - expected = dict( - id='abc123', - name='nova_secgroup', - description='A Nova security group', - tenant_id='', - project_id='', - properties={}, - location=dict( - region_name='RegionOne', - zone=None, - project=dict( - domain_name=None, - id=mock.ANY, - domain_id=None, - name='admin'), - cloud='_test_cloud_'), - security_group_rules=[ - dict(id='123', direction='ingress', ethertype='IPv4', - port_range_min=80, port_range_max=81, protocol='tcp', - remote_ip_prefix='0.0.0.0/0', security_group_id='xyz123', - properties={}, - tenant_id='', - project_id='', - remote_group_id=None, - location=dict( - region_name='RegionOne', - zone=None, - project=dict( - domain_name=None, - id=mock.ANY, - domain_id=None, - name='admin'), - cloud='_test_cloud_')) - ] - ) - - retval = self.cloud._normalize_secgroup(nova_secgroup) - self.assertEqual(expected, retval) - - def test_normalize_secgroups_negone_port(self): - nova_secgroup = dict( - id='abc123', - name='nova_secgroup', - description='A Nova security group with -1 ports', - rules=[ - dict(id='123', from_port=-1, to_port=-1, ip_protocol='icmp', - ip_range={'cidr': '0.0.0.0/0'}, parent_group_id='xyz123') - ] - ) - - retval = self.cloud._normalize_secgroup(nova_secgroup) - self.assertIsNone(retval['security_group_rules'][0]['port_range_min']) - self.assertIsNone(retval['security_group_rules'][0]['port_range_max']) - - def test_normalize_secgroup_rules(self): - nova_rules = [ - dict(id='123', from_port=80, to_port=81, ip_protocol='tcp', - ip_range={'cidr': '0.0.0.0/0'}, parent_group_id='xyz123') - ] - expected = [ - dict(id='123', direction='ingress', ethertype='IPv4', - port_range_min=80, port_range_max=81, protocol='tcp', - remote_ip_prefix='0.0.0.0/0', security_group_id='xyz123', - tenant_id='', project_id='', remote_group_id=None, - properties={}, - location=dict( - region_name='RegionOne', - zone=None, - project=dict( - domain_name=None, - id=mock.ANY, - domain_id=None, - name='admin'), - cloud='_test_cloud_')) - ] - retval = self.cloud._normalize_secgroup_rules(nova_rules) - self.assertEqual(expected, retval) - - def test_normalize_volumes_v1(self): - vol = dict( - display_name='test', - display_description='description', - bootable=u'false', # unicode type - multiattach='true', # str type - ) - expected = dict( - name=vol['display_name'], - display_name=vol['display_name'], - description=vol['display_description'], - display_description=vol['display_description'], - bootable=False, - multiattach=True, - ) - retval = _utils.normalize_volumes([vol]) - self.assertEqual([expected], retval) - - def test_normalize_volumes_v2(self): - vol = dict( - display_name='test', - display_description='description', - bootable=False, - multiattach=True, - ) - expected = dict( - name=vol['display_name'], - display_name=vol['display_name'], - description=vol['display_description'], - display_description=vol['display_description'], - bootable=False, - multiattach=True, - ) - retval = _utils.normalize_volumes([vol]) - self.assertEqual([expected], retval) - def test_safe_dict_min_ints(self): """Test integer comparison""" data = [{'f1': 3}, {'f1': 2}, {'f1': 1}] diff --git a/shade/tests/unit/test_normalize.py b/shade/tests/unit/test_normalize.py new file mode 100644 index 000000000..a3840ac10 --- /dev/null +++ b/shade/tests/unit/test_normalize.py @@ -0,0 +1,394 @@ +# -*- 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. + +import mock + +from shade import _utils +from shade.tests.unit import base + +RAW_SERVER_DICT = { + 'HUMAN_ID': True, + 'NAME_ATTR': 'name', + 'OS-DCF:diskConfig': u'MANUAL', + 'OS-EXT-AZ:availability_zone': u'ca-ymq-2', + 'OS-EXT-STS:power_state': 1, + 'OS-EXT-STS:task_state': None, + 'OS-EXT-STS:vm_state': u'active', + 'OS-SRV-USG:launched_at': u'2015-08-01T19:52:02.000000', + 'OS-SRV-USG:terminated_at': None, + 'accessIPv4': u'', + 'accessIPv6': u'', + 'addresses': { + u'public': [{ + u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:9f:46:3e', + u'OS-EXT-IPS:type': u'fixed', + u'addr': u'2604:e100:1:0:f816:3eff:fe9f:463e', + u'version': 6 + }, { + u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:9f:46:3e', + u'OS-EXT-IPS:type': u'fixed', + u'addr': u'162.253.54.192', + u'version': 4}]}, + 'config_drive': u'True', + 'created': u'2015-08-01T19:52:16Z', + 'flavor': { + u'id': u'bbcb7eb5-5c8d-498f-9d7e-307c575d3566', + u'links': [{ + u'href': u'https://compute-ca-ymq-1.vexxhost.net/db9/flavors/bbc', + u'rel': u'bookmark'}]}, + 'hostId': u'bd37', + 'human_id': u'mordred-irc', + 'id': u'811c5197-dba7-4d3a-a3f6-68ca5328b9a7', + 'image': { + u'id': u'69c99b45-cd53-49de-afdc-f24789eb8f83', + u'links': [{ + u'href': u'https://compute-ca-ymq-1.vexxhost.net/db9/images/69c', + u'rel': u'bookmark'}]}, + 'key_name': u'mordred', + 'links': [{ + u'href': u'https://compute-ca-ymq-1.vexxhost.net/v2/db9/servers/811', + u'rel': u'self' + }, { + u'href': u'https://compute-ca-ymq-1.vexxhost.net/db9/servers/811', + u'rel': u'bookmark'}], + 'metadata': {u'group': u'irc', u'groups': u'irc,enabled'}, + 'name': u'mordred-irc', + 'networks': {u'public': [u'2604:e100:1:0:f816:3eff:fe9f:463e', + u'162.253.54.192']}, + 'os-extended-volumes:volumes_attached': [], + 'progress': 0, + 'request_ids': [], + 'security_groups': [{u'name': u'default'}], + 'status': u'ACTIVE', + 'tenant_id': u'db92b20496ae4fbda850a689ea9d563f', + 'updated': u'2016-10-15T15:49:29Z', + 'user_id': u'e9b21dc437d149858faee0898fb08e92'} + + +class TestUtils(base.TestCase): + + def test_normalize_servers_strict(self): + raw_server = RAW_SERVER_DICT.copy() + expected = { + 'accessIPv4': u'', + 'accessIPv6': u'', + 'addresses': { + u'public': [{ + u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:9f:46:3e', + u'OS-EXT-IPS:type': u'fixed', + u'addr': u'2604:e100:1:0:f816:3eff:fe9f:463e', + u'version': 6 + }, { + u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:9f:46:3e', + u'OS-EXT-IPS:type': u'fixed', + u'addr': u'162.253.54.192', + u'version': 4}]}, + 'adminPass': None, + 'created': u'2015-08-01T19:52:16Z', + 'disk_config': u'MANUAL', + 'flavor': {u'id': u'bbcb7eb5-5c8d-498f-9d7e-307c575d3566'}, + 'has_config_drive': True, + 'host_id': u'bd37', + 'id': u'811c5197-dba7-4d3a-a3f6-68ca5328b9a7', + 'image': {u'id': u'69c99b45-cd53-49de-afdc-f24789eb8f83'}, + 'interface_ip': u'', + 'key_name': u'mordred', + 'launched_at': u'2015-08-01T19:52:02.000000', + 'location': { + 'cloud': '_test_cloud_', + 'project': { + 'domain_id': None, + 'domain_name': None, + 'id': u'db92b20496ae4fbda850a689ea9d563f', + 'name': None}, + 'region_name': u'RegionOne', + 'zone': u'ca-ymq-2'}, + 'metadata': {u'group': u'irc', u'groups': u'irc,enabled'}, + 'name': u'mordred-irc', + 'networks': { + u'public': [ + u'2604:e100:1:0:f816:3eff:fe9f:463e', + u'162.253.54.192']}, + 'power_state': 1, + 'private_v4': None, + 'progress': 0, + 'properties': { + 'request_ids': []}, + 'public_v4': None, + 'public_v6': None, + 'security_groups': [{u'name': u'default'}], + 'status': u'ACTIVE', + 'task_state': None, + 'terminated_at': None, + 'updated': u'2016-10-15T15:49:29Z', + 'user_id': u'e9b21dc437d149858faee0898fb08e92', + 'vm_state': u'active', + 'volumes': []} + retval = self.strict_cloud._normalize_server(raw_server).toDict() + self.assertEqual(expected, retval) + + def test_normalize_servers_normal(self): + raw_server = RAW_SERVER_DICT.copy() + expected = { + 'OS-DCF:diskConfig': u'MANUAL', + 'OS-EXT-AZ:availability_zone': u'ca-ymq-2', + 'OS-EXT-STS:power_state': 1, + 'OS-EXT-STS:task_state': None, + 'OS-EXT-STS:vm_state': u'active', + 'OS-SRV-USG:launched_at': u'2015-08-01T19:52:02.000000', + 'OS-SRV-USG:terminated_at': None, + 'accessIPv4': u'', + 'accessIPv6': u'', + 'addresses': { + u'public': [{ + u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:9f:46:3e', + u'OS-EXT-IPS:type': u'fixed', + u'addr': u'2604:e100:1:0:f816:3eff:fe9f:463e', + u'version': 6 + }, { + u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:9f:46:3e', + u'OS-EXT-IPS:type': u'fixed', + u'addr': u'162.253.54.192', + u'version': 4}]}, + 'adminPass': None, + 'az': u'ca-ymq-2', + 'cloud': '_test_cloud_', + 'config_drive': u'True', + 'created': u'2015-08-01T19:52:16Z', + 'disk_config': u'MANUAL', + 'flavor': {u'id': u'bbcb7eb5-5c8d-498f-9d7e-307c575d3566'}, + 'has_config_drive': True, + 'hostId': u'bd37', + 'host_id': u'bd37', + 'id': u'811c5197-dba7-4d3a-a3f6-68ca5328b9a7', + 'image': {u'id': u'69c99b45-cd53-49de-afdc-f24789eb8f83'}, + 'interface_ip': '', + 'key_name': u'mordred', + 'launched_at': u'2015-08-01T19:52:02.000000', + 'location': { + 'cloud': '_test_cloud_', + 'project': { + 'domain_id': None, + 'domain_name': None, + 'id': u'db92b20496ae4fbda850a689ea9d563f', + 'name': None}, + 'region_name': u'RegionOne', + 'zone': u'ca-ymq-2'}, + 'metadata': {u'group': u'irc', u'groups': u'irc,enabled'}, + 'name': u'mordred-irc', + 'networks': { + u'public': [ + u'2604:e100:1:0:f816:3eff:fe9f:463e', + u'162.253.54.192']}, + 'os-extended-volumes:volumes_attached': [], + 'power_state': 1, + 'private_v4': None, + 'progress': 0, + 'project_id': u'db92b20496ae4fbda850a689ea9d563f', + 'properties': { + 'OS-DCF:diskConfig': u'MANUAL', + 'OS-EXT-AZ:availability_zone': u'ca-ymq-2', + 'OS-EXT-STS:power_state': 1, + 'OS-EXT-STS:task_state': None, + 'OS-EXT-STS:vm_state': u'active', + 'OS-SRV-USG:launched_at': u'2015-08-01T19:52:02.000000', + 'OS-SRV-USG:terminated_at': None, + 'os-extended-volumes:volumes_attached': [], + 'request_ids': []}, + 'public_v4': None, + 'public_v6': None, + 'region': u'RegionOne', + 'request_ids': [], + 'security_groups': [{u'name': u'default'}], + 'status': u'ACTIVE', + 'task_state': None, + 'tenant_id': u'db92b20496ae4fbda850a689ea9d563f', + 'terminated_at': None, + 'updated': u'2016-10-15T15:49:29Z', + 'user_id': u'e9b21dc437d149858faee0898fb08e92', + 'vm_state': u'active', + 'volumes': []} + retval = self.cloud._normalize_server(raw_server).toDict() + self.assertEqual(expected, retval) + + def test_normalize_secgroups_strict(self): + nova_secgroup = dict( + id='abc123', + name='nova_secgroup', + description='A Nova security group', + rules=[ + dict(id='123', from_port=80, to_port=81, ip_protocol='tcp', + ip_range={'cidr': '0.0.0.0/0'}, parent_group_id='xyz123') + ] + ) + + expected = dict( + id='abc123', + name='nova_secgroup', + description='A Nova security group', + properties={}, + location=dict( + region_name='RegionOne', + zone=None, + project=dict( + domain_name=None, + id=mock.ANY, + domain_id=None, + name='admin'), + cloud='_test_cloud_'), + security_group_rules=[ + dict(id='123', direction='ingress', ethertype='IPv4', + port_range_min=80, port_range_max=81, protocol='tcp', + remote_ip_prefix='0.0.0.0/0', security_group_id='xyz123', + properties={}, + remote_group_id=None, + location=dict( + region_name='RegionOne', + zone=None, + project=dict( + domain_name=None, + id=mock.ANY, + domain_id=None, + name='admin'), + cloud='_test_cloud_')) + ] + ) + + retval = self.strict_cloud._normalize_secgroup(nova_secgroup) + self.assertEqual(expected, retval) + + def test_normalize_secgroups(self): + nova_secgroup = dict( + id='abc123', + name='nova_secgroup', + description='A Nova security group', + rules=[ + dict(id='123', from_port=80, to_port=81, ip_protocol='tcp', + ip_range={'cidr': '0.0.0.0/0'}, parent_group_id='xyz123') + ] + ) + + expected = dict( + id='abc123', + name='nova_secgroup', + description='A Nova security group', + tenant_id='', + project_id='', + properties={}, + location=dict( + region_name='RegionOne', + zone=None, + project=dict( + domain_name=None, + id=mock.ANY, + domain_id=None, + name='admin'), + cloud='_test_cloud_'), + security_group_rules=[ + dict(id='123', direction='ingress', ethertype='IPv4', + port_range_min=80, port_range_max=81, protocol='tcp', + remote_ip_prefix='0.0.0.0/0', security_group_id='xyz123', + properties={}, + tenant_id='', + project_id='', + remote_group_id=None, + location=dict( + region_name='RegionOne', + zone=None, + project=dict( + domain_name=None, + id=mock.ANY, + domain_id=None, + name='admin'), + cloud='_test_cloud_')) + ] + ) + + retval = self.cloud._normalize_secgroup(nova_secgroup) + self.assertEqual(expected, retval) + + def test_normalize_secgroups_negone_port(self): + nova_secgroup = dict( + id='abc123', + name='nova_secgroup', + description='A Nova security group with -1 ports', + rules=[ + dict(id='123', from_port=-1, to_port=-1, ip_protocol='icmp', + ip_range={'cidr': '0.0.0.0/0'}, parent_group_id='xyz123') + ] + ) + + retval = self.cloud._normalize_secgroup(nova_secgroup) + self.assertIsNone(retval['security_group_rules'][0]['port_range_min']) + self.assertIsNone(retval['security_group_rules'][0]['port_range_max']) + + def test_normalize_secgroup_rules(self): + nova_rules = [ + dict(id='123', from_port=80, to_port=81, ip_protocol='tcp', + ip_range={'cidr': '0.0.0.0/0'}, parent_group_id='xyz123') + ] + expected = [ + dict(id='123', direction='ingress', ethertype='IPv4', + port_range_min=80, port_range_max=81, protocol='tcp', + remote_ip_prefix='0.0.0.0/0', security_group_id='xyz123', + tenant_id='', project_id='', remote_group_id=None, + properties={}, + location=dict( + region_name='RegionOne', + zone=None, + project=dict( + domain_name=None, + id=mock.ANY, + domain_id=None, + name='admin'), + cloud='_test_cloud_')) + ] + retval = self.cloud._normalize_secgroup_rules(nova_rules) + self.assertEqual(expected, retval) + + def test_normalize_volumes_v1(self): + vol = dict( + display_name='test', + display_description='description', + bootable=u'false', # unicode type + multiattach='true', # str type + ) + expected = dict( + name=vol['display_name'], + display_name=vol['display_name'], + description=vol['display_description'], + display_description=vol['display_description'], + bootable=False, + multiattach=True, + ) + retval = _utils.normalize_volumes([vol]) + self.assertEqual([expected], retval) + + def test_normalize_volumes_v2(self): + vol = dict( + display_name='test', + display_description='description', + bootable=False, + multiattach=True, + ) + expected = dict( + name=vol['display_name'], + display_name=vol['display_name'], + description=vol['display_description'], + display_description=vol['display_description'], + bootable=False, + multiattach=True, + ) + retval = _utils.normalize_volumes([vol]) + self.assertEqual([expected], retval) From 1f1f5ffc04d1a54217bdf9a56a4f3437ccba7880 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 1 Nov 2016 16:16:46 -0500 Subject: [PATCH 1090/3836] Add unit tests for image and flavor normalization Also, while we're at it - it turns out we were NOT doing normalization in any meaningful way on nova-based images. This is ok - I am completely unaware of any clouds that do not expose the glance API any more. But we should fix it. Change-Id: I4a87bb9b83fcf12ad25ec211c5d13fcf34432793 --- shade/_normalize.py | 46 ++- shade/tests/unit/test_normalize.py | 442 +++++++++++++++++++++++++++++ 2 files changed, 479 insertions(+), 9 deletions(-) diff --git a/shade/_normalize.py b/shade/_normalize.py index 8a9561b39..1245dc450 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -18,19 +18,12 @@ _IMAGE_FIELDS = ( 'checksum', 'container_format', - 'created_at', 'direct_url', 'disk_format', 'file', 'id', - 'min_disk', - 'min_ram', 'name', 'owner', - 'size', - 'status', - 'tags', - 'updated_at', 'virtual_size', ) @@ -112,7 +105,9 @@ def _normalize_flavor(self, flavor): is_public = _to_bool(flavor.pop('is_public', is_public)) is_disabled = _to_bool(_pop_or_get( flavor, 'OS-FLV-DISABLED:disabled', False, self.strict_mode)) - extra_specs = flavor.pop('extra_specs', {}) + extra_specs = _pop_or_get( + flavor, 'OS-FLV-WITH-EXT-SPECS:extra_specs', {}, self.strict_mode) + extra_specs = flavor.pop('extra_specs', extra_specs) new_flavor['location'] = self.current_location new_flavor['id'] = flavor.pop('id') @@ -146,6 +141,11 @@ def _normalize_image(self, image): new_image = munch.Munch( location=self._get_current_location(project_id=image.get('owner'))) + image.pop('links', None) + image.pop('NAME_ATTR', None) + image.pop('HUMAN_ID', None) + image.pop('human_id', None) + properties = image.pop('properties', {}) visibility = image.pop('visibility', None) protected = _to_bool(image.pop('protected', False)) @@ -156,17 +156,40 @@ def _normalize_image(self, image): is_public = image.pop('is_public', False) visibility = 'public' if is_public else 'private' + new_image['size'] = image.pop('OS-EXT-IMG-SIZE:size', 0) + new_image['size'] = image.pop('size', new_image['size']) + + new_image['min_ram'] = image.pop('minRam', 0) + new_image['min_ram'] = image.pop('min_ram', new_image['min_ram']) + + new_image['min_disk'] = image.pop('minDisk', 0) + new_image['min_disk'] = image.pop('min_disk', new_image['min_disk']) + + new_image['created_at'] = image.pop('created', '') + new_image['created_at'] = image.pop( + 'created_at', new_image['created_at']) + + new_image['updated_at'] = image.pop('updated', '') + new_image['updated_at'] = image.pop( + 'updated_at', new_image['updated_at']) + for field in _IMAGE_FIELDS: new_image[field] = image.pop(field, None) + + new_image['tags'] = image.pop('tags', []) + new_image['status'] = image.pop('status').lower() for field in ('min_ram', 'min_disk', 'size', 'virtual_size'): new_image[field] = _pop_int(new_image, field) new_image['is_protected'] = protected new_image['locations'] = image.pop('locations', []) + metadata = image.pop('metadata', {}) + for key, val in metadata.items(): + properties.setdefault(key, val) + for key, val in image.items(): properties.setdefault(key, val) new_image['properties'] = properties - new_image['visibility'] = visibility new_image['is_public'] = is_public # Backwards compat with glance @@ -174,6 +197,11 @@ def _normalize_image(self, image): for key, val in properties.items(): new_image[key] = val new_image['protected'] = protected + new_image['created'] = new_image['created_at'] + new_image['updated'] = new_image['updated_at'] + new_image['minDisk'] = new_image['min_disk'] + new_image['minRam'] = new_image['min_ram'] + new_image['visibility'] = visibility return new_image def _normalize_secgroups(self, groups): diff --git a/shade/tests/unit/test_normalize.py b/shade/tests/unit/test_normalize.py index a3840ac10..6211af316 100644 --- a/shade/tests/unit/test_normalize.py +++ b/shade/tests/unit/test_normalize.py @@ -75,9 +75,451 @@ 'updated': u'2016-10-15T15:49:29Z', 'user_id': u'e9b21dc437d149858faee0898fb08e92'} +RAW_GLANCE_IMAGE_DICT = { + u'auto_disk_config': u'False', + u'checksum': u'774f48af604ab1ec319093234c5c0019', + u'com.rackspace__1__build_core': u'1', + u'com.rackspace__1__build_managed': u'1', + u'com.rackspace__1__build_rackconnect': u'1', + u'com.rackspace__1__options': u'0', + u'com.rackspace__1__source': u'import', + u'com.rackspace__1__visible_core': u'1', + u'com.rackspace__1__visible_managed': u'1', + u'com.rackspace__1__visible_rackconnect': u'1', + u'container_format': u'ovf', + u'created_at': u'2015-02-15T22:58:45Z', + u'disk_format': u'vhd', + u'file': u'/v2/images/f2868d7c-63e1-4974-a64d-8670a86df21e/file', + u'id': u'f2868d7c-63e1-4974-a64d-8670a86df21e', + u'image_type': u'import', + u'min_disk': 20, + u'min_ram': 0, + u'name': u'Test Monty Ubuntu', + u'org.openstack__1__architecture': u'x64', + u'os_type': u'linux', + u'owner': u'610275', + u'protected': False, + u'schema': u'/v2/schemas/image', + u'size': 323004185, + u'status': u'active', + u'tags': [], + u'updated_at': u'2015-02-15T23:04:34Z', + u'user_id': u'156284', + u'virtual_size': None, + u'visibility': u'private', + u'vm_mode': u'hvm', + u'xenapi_use_agent': u'False'} + +RAW_NOVA_IMAGE_DICT = { + 'HUMAN_ID': True, + 'NAME_ATTR': 'name', + 'OS-DCF:diskConfig': u'MANUAL', + 'OS-EXT-IMG-SIZE:size': 323004185, + 'created': u'2015-02-15T22:58:45Z', + 'human_id': u'test-monty-ubuntu', + 'id': u'f2868d7c-63e1-4974-a64d-8670a86df21e', + 'links': [{ + u'href': u'https://example.com/v2/610275/images/f2868d7c', + u'rel': u'self' + }, { + u'href': u'https://example.com/610275/images/f2868d7c', + u'rel': u'bookmark' + }, { + u'href': u'https://example.com/images/f2868d7c', + u'rel': u'alternate', + u'type': u'application/vnd.openstack.image'}], + 'metadata': { + u'auto_disk_config': u'False', + u'com.rackspace__1__build_core': u'1', + u'com.rackspace__1__build_managed': u'1', + u'com.rackspace__1__build_rackconnect': u'1', + u'com.rackspace__1__options': u'0', + u'com.rackspace__1__source': u'import', + u'com.rackspace__1__visible_core': u'1', + u'com.rackspace__1__visible_managed': u'1', + u'com.rackspace__1__visible_rackconnect': u'1', + u'image_type': u'import', + u'org.openstack__1__architecture': u'x64', + u'os_type': u'linux', + u'user_id': u'156284', + u'vm_mode': u'hvm', + u'xenapi_use_agent': u'False'}, + 'minDisk': 20, + 'minRam': 0, + 'name': u'Test Monty Ubuntu', + 'progress': 100, + 'request_ids': [], + 'status': u'ACTIVE', + 'updated': u'2015-02-15T23:04:34Z'} + +RAW_FLAVOR_DICT = { + 'HUMAN_ID': True, + 'NAME_ATTR': 'name', + 'OS-FLV-EXT-DATA:ephemeral': 80, + 'OS-FLV-WITH-EXT-SPECS:extra_specs': { + u'class': u'performance1', + u'disk_io_index': u'40', + u'number_of_data_disks': u'1', + u'policy_class': u'performance_flavor', + u'resize_policy_class': u'performance_flavor'}, + 'disk': 40, + 'ephemeral': 80, + 'human_id': u'8-gb-performance', + 'id': u'performance1-8', + 'is_public': 'N/A', + 'links': [{ + u'href': u'https://example.com/v2/610275/flavors/performance1-8', + u'rel': u'self' + }, { + u'href': u'https://example.com/610275/flavors/performance1-8', + u'rel': u'bookmark'}], + 'name': u'8 GB Performance', + 'ram': 8192, + 'request_ids': [], + 'rxtx_factor': 1600.0, + 'swap': u'', + 'vcpus': 8} + class TestUtils(base.TestCase): + def test_normalize_flavors(self): + raw_flavor = RAW_FLAVOR_DICT.copy() + expected = { + 'OS-FLV-EXT-DATA:ephemeral': 80, + 'OS-FLV-WITH-EXT-SPECS:extra_specs': { + u'class': u'performance1', + u'disk_io_index': u'40', + u'number_of_data_disks': u'1', + u'policy_class': u'performance_flavor', + u'resize_policy_class': u'performance_flavor'}, + 'disk': 40, + 'ephemeral': 80, + 'extra_specs': { + u'class': u'performance1', + u'disk_io_index': u'40', + u'number_of_data_disks': u'1', + u'policy_class': u'performance_flavor', + u'resize_policy_class': u'performance_flavor'}, + 'id': u'performance1-8', + 'is_disabled': False, + 'is_public': False, + 'location': { + 'cloud': '_test_cloud_', + 'project': { + 'domain_id': None, + 'domain_name': None, + 'id': mock.ANY, + 'name': 'admin'}, + 'region_name': u'RegionOne', + 'zone': None}, + 'name': u'8 GB Performance', + 'properties': { + 'OS-FLV-EXT-DATA:ephemeral': 80, + 'OS-FLV-WITH-EXT-SPECS:extra_specs': { + u'class': u'performance1', + u'disk_io_index': u'40', + u'number_of_data_disks': u'1', + u'policy_class': u'performance_flavor', + u'resize_policy_class': u'performance_flavor'}, + 'request_ids': []}, + 'ram': 8192, + 'request_ids': [], + 'rxtx_factor': 1600.0, + 'swap': 0, + 'vcpus': 8} + retval = self.cloud._normalize_flavor(raw_flavor).toDict() + self.assertEqual(expected, retval) + + def test_normalize_flavors_strict(self): + raw_flavor = RAW_FLAVOR_DICT.copy() + expected = { + 'disk': 40, + 'ephemeral': 80, + 'extra_specs': { + u'class': u'performance1', + u'disk_io_index': u'40', + u'number_of_data_disks': u'1', + u'policy_class': u'performance_flavor', + u'resize_policy_class': u'performance_flavor'}, + 'id': u'performance1-8', + 'is_disabled': False, + 'is_public': False, + 'location': { + 'cloud': '_test_cloud_', + 'project': { + 'domain_id': None, + 'domain_name': None, + 'id': mock.ANY, + 'name': 'admin'}, + 'region_name': u'RegionOne', + 'zone': None}, + 'name': u'8 GB Performance', + 'properties': { + 'request_ids': []}, + 'ram': 8192, + 'rxtx_factor': 1600.0, + 'swap': 0, + 'vcpus': 8} + retval = self.strict_cloud._normalize_flavor(raw_flavor).toDict() + self.assertEqual(expected, retval) + + def test_normalize_nova_images(self): + raw_image = RAW_NOVA_IMAGE_DICT.copy() + expected = { + u'auto_disk_config': u'False', + u'com.rackspace__1__build_core': u'1', + u'com.rackspace__1__build_managed': u'1', + u'com.rackspace__1__build_rackconnect': u'1', + u'com.rackspace__1__options': u'0', + u'com.rackspace__1__source': u'import', + u'com.rackspace__1__visible_core': u'1', + u'com.rackspace__1__visible_managed': u'1', + u'com.rackspace__1__visible_rackconnect': u'1', + u'image_type': u'import', + u'org.openstack__1__architecture': u'x64', + u'os_type': u'linux', + u'user_id': u'156284', + u'vm_mode': u'hvm', + u'xenapi_use_agent': u'False', + 'OS-DCF:diskConfig': u'MANUAL', + 'checksum': None, + 'container_format': None, + 'created': u'2015-02-15T22:58:45Z', + 'created_at': '2015-02-15T22:58:45Z', + 'direct_url': None, + 'disk_format': None, + 'file': None, + 'id': u'f2868d7c-63e1-4974-a64d-8670a86df21e', + 'is_protected': False, + 'is_public': False, + 'location': { + 'cloud': '_test_cloud_', + 'project': { + 'domain_id': None, + 'domain_name': None, + 'id': mock.ANY, + 'name': 'admin'}, + 'region_name': u'RegionOne', + 'zone': None}, + 'locations': [], + 'minDisk': 20, + 'minRam': 0, + 'min_disk': 20, + 'min_ram': 0, + 'name': u'Test Monty Ubuntu', + 'owner': None, + 'progress': 100, + 'properties': { + u'auto_disk_config': u'False', + u'com.rackspace__1__build_core': u'1', + u'com.rackspace__1__build_managed': u'1', + u'com.rackspace__1__build_rackconnect': u'1', + u'com.rackspace__1__options': u'0', + u'com.rackspace__1__source': u'import', + u'com.rackspace__1__visible_core': u'1', + u'com.rackspace__1__visible_managed': u'1', + u'com.rackspace__1__visible_rackconnect': u'1', + u'image_type': u'import', + u'org.openstack__1__architecture': u'x64', + u'os_type': u'linux', + u'user_id': u'156284', + u'vm_mode': u'hvm', + u'xenapi_use_agent': u'False', + 'OS-DCF:diskConfig': u'MANUAL', + 'progress': 100, + 'request_ids': []}, + 'protected': False, + 'request_ids': [], + 'size': 323004185, + 'status': u'active', + 'tags': [], + 'updated': u'2015-02-15T23:04:34Z', + 'updated_at': u'2015-02-15T23:04:34Z', + 'virtual_size': 0, + 'visibility': 'private'} + retval = self.cloud._normalize_image(raw_image).toDict() + self.assertEqual(expected, retval) + + def test_normalize_nova_images_strict(self): + raw_image = RAW_NOVA_IMAGE_DICT.copy() + expected = { + 'checksum': None, + 'container_format': None, + 'created_at': '2015-02-15T22:58:45Z', + 'direct_url': None, + 'disk_format': None, + 'file': None, + 'id': u'f2868d7c-63e1-4974-a64d-8670a86df21e', + 'is_protected': False, + 'is_public': False, + 'location': { + 'cloud': '_test_cloud_', + 'project': { + 'domain_id': None, + 'domain_name': None, + 'id': mock.ANY, + 'name': 'admin'}, + 'region_name': u'RegionOne', + 'zone': None}, + 'locations': [], + 'min_disk': 20, + 'min_ram': 0, + 'name': u'Test Monty Ubuntu', + 'owner': None, + 'properties': { + u'auto_disk_config': u'False', + u'com.rackspace__1__build_core': u'1', + u'com.rackspace__1__build_managed': u'1', + u'com.rackspace__1__build_rackconnect': u'1', + u'com.rackspace__1__options': u'0', + u'com.rackspace__1__source': u'import', + u'com.rackspace__1__visible_core': u'1', + u'com.rackspace__1__visible_managed': u'1', + u'com.rackspace__1__visible_rackconnect': u'1', + u'image_type': u'import', + u'org.openstack__1__architecture': u'x64', + u'os_type': u'linux', + u'user_id': u'156284', + u'vm_mode': u'hvm', + u'xenapi_use_agent': u'False', + 'OS-DCF:diskConfig': u'MANUAL', + 'progress': 100, + 'request_ids': []}, + 'size': 323004185, + 'status': u'active', + 'tags': [], + 'updated_at': u'2015-02-15T23:04:34Z', + 'virtual_size': 0} + retval = self.strict_cloud._normalize_image(raw_image).toDict() + self.assertEqual(sorted(expected.keys()), sorted(retval.keys())) + self.assertEqual(expected, retval) + + def test_normalize_glance_images(self): + raw_image = RAW_GLANCE_IMAGE_DICT.copy() + expected = { + u'auto_disk_config': u'False', + 'checksum': u'774f48af604ab1ec319093234c5c0019', + u'com.rackspace__1__build_core': u'1', + u'com.rackspace__1__build_managed': u'1', + u'com.rackspace__1__build_rackconnect': u'1', + u'com.rackspace__1__options': u'0', + u'com.rackspace__1__source': u'import', + u'com.rackspace__1__visible_core': u'1', + u'com.rackspace__1__visible_managed': u'1', + u'com.rackspace__1__visible_rackconnect': u'1', + 'container_format': u'ovf', + 'created': u'2015-02-15T22:58:45Z', + 'created_at': u'2015-02-15T22:58:45Z', + 'direct_url': None, + 'disk_format': u'vhd', + 'file': u'/v2/images/f2868d7c-63e1-4974-a64d-8670a86df21e/file', + 'id': u'f2868d7c-63e1-4974-a64d-8670a86df21e', + u'image_type': u'import', + 'is_protected': False, + 'is_public': False, + 'location': { + 'cloud': '_test_cloud_', + 'project': { + 'domain_id': None, + 'domain_name': None, + 'id': u'610275', + 'name': None}, + 'region_name': u'RegionOne', + 'zone': None}, + 'locations': [], + 'minDisk': 20, + 'min_disk': 20, + 'minRam': 0, + 'min_ram': 0, + 'name': u'Test Monty Ubuntu', + u'org.openstack__1__architecture': u'x64', + u'os_type': u'linux', + 'owner': u'610275', + 'properties': { + u'auto_disk_config': u'False', + u'com.rackspace__1__build_core': u'1', + u'com.rackspace__1__build_managed': u'1', + u'com.rackspace__1__build_rackconnect': u'1', + u'com.rackspace__1__options': u'0', + u'com.rackspace__1__source': u'import', + u'com.rackspace__1__visible_core': u'1', + u'com.rackspace__1__visible_managed': u'1', + u'com.rackspace__1__visible_rackconnect': u'1', + u'image_type': u'import', + u'org.openstack__1__architecture': u'x64', + u'os_type': u'linux', + u'schema': u'/v2/schemas/image', + u'user_id': u'156284', + u'vm_mode': u'hvm', + u'xenapi_use_agent': u'False'}, + 'protected': False, + u'schema': u'/v2/schemas/image', + 'size': 323004185, + 'status': u'active', + 'tags': [], + 'updated': u'2015-02-15T23:04:34Z', + 'updated_at': u'2015-02-15T23:04:34Z', + u'user_id': u'156284', + 'virtual_size': 0, + 'visibility': u'private', + u'vm_mode': u'hvm', + u'xenapi_use_agent': u'False'} + retval = self.cloud._normalize_image(raw_image).toDict() + self.assertEqual(expected, retval) + + def test_normalize_glance_images_strict(self): + raw_image = RAW_GLANCE_IMAGE_DICT.copy() + expected = { + 'checksum': u'774f48af604ab1ec319093234c5c0019', + 'container_format': u'ovf', + 'created_at': u'2015-02-15T22:58:45Z', + 'direct_url': None, + 'disk_format': u'vhd', + 'file': u'/v2/images/f2868d7c-63e1-4974-a64d-8670a86df21e/file', + 'id': u'f2868d7c-63e1-4974-a64d-8670a86df21e', + 'is_protected': False, + 'is_public': False, + 'location': { + 'cloud': '_test_cloud_', + 'project': { + 'domain_id': None, + 'domain_name': None, + 'id': u'610275', + 'name': None}, + 'region_name': u'RegionOne', + 'zone': None}, + 'locations': [], + 'min_disk': 20, + 'min_ram': 0, + 'name': u'Test Monty Ubuntu', + 'owner': u'610275', + 'properties': { + u'auto_disk_config': u'False', + u'com.rackspace__1__build_core': u'1', + u'com.rackspace__1__build_managed': u'1', + u'com.rackspace__1__build_rackconnect': u'1', + u'com.rackspace__1__options': u'0', + u'com.rackspace__1__source': u'import', + u'com.rackspace__1__visible_core': u'1', + u'com.rackspace__1__visible_managed': u'1', + u'com.rackspace__1__visible_rackconnect': u'1', + u'image_type': u'import', + u'org.openstack__1__architecture': u'x64', + u'os_type': u'linux', + u'schema': u'/v2/schemas/image', + u'user_id': u'156284', + u'vm_mode': u'hvm', + u'xenapi_use_agent': u'False'}, + 'size': 323004185, + 'status': u'active', + 'tags': [], + 'updated_at': u'2015-02-15T23:04:34Z', + 'virtual_size': 0} + retval = self.strict_cloud._normalize_image(raw_image).toDict() + self.assertEqual(sorted(expected.keys()), sorted(retval.keys())) + self.assertEqual(expected, retval) + def test_normalize_servers_strict(self): raw_server = RAW_SERVER_DICT.copy() expected = { From 6115b001c7d092a1983daaeb9ab3048fdf5443c5 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 1 Nov 2016 17:01:25 -0500 Subject: [PATCH 1091/3836] Depend on normalization in list_flavors We have a logic case in list_flavors that is easier to understand if the normalization is applied first. So apply it first. Change-Id: I27580ff202f71c35dd8099902411fabad005a955 --- shade/openstackcloud.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index ddcb8b24c..88684732c 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1506,15 +1506,13 @@ def list_flavors(self, get_extra=True): """ with _utils.shade_exceptions("Error fetching flavor list"): - flavors = self.manager.submit_task( - _tasks.FlavorList(is_public=None)) + flavors = self._normalize_flavors( + self.manager.submit_task( + _tasks.FlavorList(is_public=None))) with _utils.shade_exceptions("Error fetching flavor extra specs"): for flavor in flavors: - if 'OS-FLV-WITH-EXT-SPECS:extra_specs' in flavor: - flavor.extra_specs = flavor.get( - 'OS-FLV-WITH-EXT-SPECS:extra_specs') - elif get_extra: + if not flavor.extra_specs and get_extra: try: flavor.extra_specs = self.manager.submit_task( _tasks.FlavorGetExtraSpecs(id=flavor.id)) @@ -1524,7 +1522,7 @@ def list_flavors(self, get_extra=True): 'Fetching extra specs for flavor failed:' ' {msg}'.format(msg=str(e))) - return self._normalize_flavors(flavors) + return flavors @_utils.cache_on_arguments(should_cache_fn=_no_pending_stacks) def list_stacks(self): From ea5f8dd6a695b942d2e76045cec432a86f845e21 Mon Sep 17 00:00:00 2001 From: Jordan Pittier Date: Tue, 18 Oct 2016 12:36:22 +0200 Subject: [PATCH 1092/3836] list_servers(): thread safety: never return bogus data. Shade has a caching mechanism which is good. It's okay to return stale data, that is, to return an "old view" of the resources. It's not okay to return an "impossible" set of data, a bogus view of the data. If two threads call list_severs() at the same time, the first will acquire a lock, and start the query to Nova. The second will try to acquire, lock is held, so it will return the cached server list, which initially is '[]'. But there could be some servers, we just don't know at that point. In that case, we should block to acquire the lock, instead of return an invalid view of the list of servers. How to repro (Python3) 1) Create a couple of Nova servers 2) Run import os_client_config, shade, concurrent.futures cloud = os_client_config.make_shade(cloud='devstack') with concurrent.futures.ThreadPoolExecutor(4) as exec: for i in range(3): exec.submit(lambda: print(len(cloud.list_servers()))) Change-Id: I32783d291c5d3d13b2fe86cd876931e25f8c4879 --- shade/openstackcloud.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 3b449092a..604336224 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -200,15 +200,15 @@ def __init__( self._disable_warnings = {} - self._servers = [] + self._servers = None self._servers_time = 0 self._servers_lock = threading.Lock() - self._ports = [] + self._ports = None self._ports_time = 0 self._ports_lock = threading.Lock() - self._floating_ips = [] + self._floating_ips = None self._floating_ips_time = 0 self._floating_ips_lock = threading.Lock() @@ -1470,10 +1470,13 @@ def list_ports(self, filters=None): # a lock, and the non-blocking acquire method will cause # subsequent threads to just skip this and use the old # data until it succeeds. - if self._ports_lock.acquire(False): + # Initially when we never got data, block to retrieve some data. + first_run = self._ports is None + if self._ports_lock.acquire(first_run): try: - self._ports = self._list_ports(filters) - self._ports_time = time.time() + if not (first_run and self._ports is not None): + self._ports = self._list_ports(filters) + self._ports_time = time.time() finally: self._ports_lock.release() return self._ports @@ -1595,10 +1598,13 @@ def list_servers(self, detailed=False): # a lock, and the non-blocking acquire method will cause # subsequent threads to just skip this and use the old # data until it succeeds. - if self._servers_lock.acquire(False): + # Initially when we never got data, block to retrieve some data. + first_run = self._servers is None + if self._servers_lock.acquire(first_run): try: - self._servers = self._list_servers(detailed=detailed) - self._servers_time = time.time() + if not (first_run and self._servers is not None): + self._servers = self._list_servers(detailed=detailed) + self._servers_time = time.time() finally: self._servers_lock.release() return self._servers @@ -1710,10 +1716,13 @@ def list_floating_ips(self): # a lock, and the non-blocking acquire method will cause # subsequent threads to just skip this and use the old # data until it succeeds. - if self._floating_ips_lock.acquire(False): + # Initially when we never got data, block to retrieve some data. + first_run = self._floating_ips is None + if self._floating_ips_lock.acquire(first_run): try: - self._floating_ips = self._list_floating_ips() - self._floating_ips_time = time.time() + if not (first_run and self._floating_ips is not None): + self._floating_ips = self._list_floating_ips() + self._floating_ips_time = time.time() finally: self._floating_ips_lock.release() return self._floating_ips From 9f47acc0cd0c69888b1a3d98b98fcf1a88c248a5 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 5 Nov 2016 10:26:32 -0500 Subject: [PATCH 1093/3836] Add fuga.io to vendors Change-Id: I1704b6e26a3c56b519544ad8ee6d3fd80a2a752a --- doc/source/vendor-support.rst | 14 ++++++++++++++ os_client_config/vendors/fuga.json | 15 +++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 os_client_config/vendors/fuga.json diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index eb7ecf0fe..b301d8017 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -163,6 +163,20 @@ de-fra1 Frankfurt, DE * Image API Version is 1 * Volume API Version is 1 +fuga +---- + +https://identity.api.fuga.io:5000 + +============== ================ +Region Name Location +============== ================ +cystack Netherlands +============== ================ + +* Identity API Version is 3 +* Volume API Version is 3 + internap -------- diff --git a/os_client_config/vendors/fuga.json b/os_client_config/vendors/fuga.json new file mode 100644 index 000000000..388500b1b --- /dev/null +++ b/os_client_config/vendors/fuga.json @@ -0,0 +1,15 @@ +{ + "name": "fuga", + "profile": { + "auth": { + "auth_url": "https://identity.api.fuga.io:5000", + "user_domain_name": "Default", + "project_domain_name": "Default" + }, + "regions": [ + "cystack" + ], + "identity_api_version": "3", + "volume_api_version": "3" + } +} From 958f97255289763b910c37307933cbe57fd484ba Mon Sep 17 00:00:00 2001 From: Jordan Pittier Date: Fri, 4 Nov 2016 18:59:33 +0100 Subject: [PATCH 1094/3836] Allow server-side filtering of Neutron floating IPs Neutron API allows to filter floating IPs based on some fields. This enables server-side (efficient) filtering that Shade should leverage when it can. Unfortunately Nova-net (which is deprecated) doesn't support server-side filtering when it comes to FIP, so we have to distinguish the two at several places. This patch also updates the `search_floating_ips` method to use server-side filtering when appropriate. Change-Id: I28c960091c1679ca588c0cfb3bed1881cd6b03d2 --- shade/openstackcloud.py | 49 +++++++++++++++----- shade/tests/functional/test_floating_ip.py | 49 ++++++++++++++++++++ shade/tests/unit/test_floating_ip_neutron.py | 24 ++++++++-- shade/tests/unit/test_floating_ip_nova.py | 11 +++++ 4 files changed, 117 insertions(+), 16 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index ffe237595..20b326b08 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1383,14 +1383,18 @@ def search_floating_ip_pools(self, name=None, filters=None): pools = self.list_floating_ip_pools() return _utils._filter_list(pools, name, filters) - # Note (dguerri): when using Neutron, this can be optimized using - # server-side search. - # There are some cases in which such optimization is not possible (e.g. - # nested attributes or list of objects) so we need to use the client-side - # filtering when we can't do otherwise. + # With Neutron, there are some cases in which full server side filtering is + # not possible (e.g. nested attributes or list of objects) so we also need + # to use the client-side filtering # The same goes for all neutron-related search/get methods! def search_floating_ips(self, id=None, filters=None): - floating_ips = self.list_floating_ips() + # `filters` could be a jmespath expression which Neutron server doesn't + # understand, obviously. + if self._use_neutron_floating() and isinstance(filters, dict): + kwargs = {'filters': filters} + else: + kwargs = {} + floating_ips = self.list_floating_ips(**kwargs) return _utils._filter_list(floating_ips, id, filters) def search_stacks(self, name_or_id=None, filters=None): @@ -1700,26 +1704,47 @@ def list_floating_ip_pools(self): with _utils.shade_exceptions("Error fetching floating IP pool list"): return self.manager.submit_task(_tasks.FloatingIPPoolList()) - def _list_floating_ips(self): + def _list_floating_ips(self, filters=None): if self._use_neutron_floating(): try: return self._normalize_floating_ips( - self._neutron_list_floating_ips()) + self._neutron_list_floating_ips(filters)) except OpenStackCloudURINotFound as e: + # Nova-network don't support server-side floating ips + # filtering, so it's safer to die hard than to fallback to Nova + # which may return more results that expected. + if filters: + self.log.error( + "Something went wrong talking to neutron API. Can't " + "fallback to Nova since it doesn't support server-side" + " filtering when listing floating ips." + ) + raise self.log.debug( "Something went wrong talking to neutron API: " "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) # Fall-through, trying with Nova + else: + if filters: + raise ValueError( + "Nova-network don't support server-side floating ips " + "filtering. Use the search_floatting_ips method instead" + ) floating_ips = self._nova_list_floating_ips() return self._normalize_floating_ips(floating_ips) - def list_floating_ips(self): + def list_floating_ips(self, filters=None): """List all available floating IPs. + :param filters: (optional) dict of filter conditions to push down :returns: A list of floating IP ``munch.Munch``. """ + # If pushdown filters are specified, bypass local caching. + if filters: + return self._list_floating_ips(filters) + if (time.time() - self._floating_ips_time) >= self._FLOAT_AGE: # Since we're using cached data anyway, we don't need to # have more than one thread actually submit the list @@ -1738,10 +1763,12 @@ def list_floating_ips(self): self._floating_ips_lock.release() return self._floating_ips - def _neutron_list_floating_ips(self): + def _neutron_list_floating_ips(self, filters=None): + if not filters: + filters = {} with _utils.neutron_exceptions("error fetching floating IPs list"): return self.manager.submit_task( - _tasks.NeutronFloatingIPList())['floatingips'] + _tasks.NeutronFloatingIPList(**filters))['floatingips'] def _nova_list_floating_ips(self): with _utils.shade_exceptions("Error fetching floating IPs list"): diff --git a/shade/tests/functional/test_floating_ip.py b/shade/tests/functional/test_floating_ip.py index 17e70d5a6..608aff5a0 100644 --- a/shade/tests/functional/test_floating_ip.py +++ b/shade/tests/functional/test_floating_ip.py @@ -243,3 +243,52 @@ def test_detach_ip_from_server(self): id=None, filters={'floating_ip_address': ip}) self.demo_cloud.detach_ip_from_server( server_id=new_server.id, floating_ip_id=f_ip['id']) + + def test_list_floating_ips(self): + fip_admin = self.operator_cloud.create_floating_ip() + self.addCleanup(self.operator_cloud.delete_floating_ip, fip_admin.id) + fip_user = self.demo_cloud.create_floating_ip() + self.addCleanup(self.demo_cloud.delete_floating_ip, fip_user.id) + + # Get all the floating ips. + fip_id_list = [ + fip.id for fip in self.operator_cloud.list_floating_ips() + ] + if self.demo_cloud.has_service('network'): + # Neutron returns all FIP for all projects by default + self.assertIn(fip_admin.id, fip_id_list) + self.assertIn(fip_user.id, fip_id_list) + + # Ask Neutron for only a subset of all the FIPs. + filtered_fip_id_list = [ + fip.id for fip in self.operator_cloud.list_floating_ips( + {'tenant_id': self.demo_cloud.current_project_id} + ) + ] + self.assertNotIn(fip_admin.id, filtered_fip_id_list) + self.assertIn(fip_user.id, filtered_fip_id_list) + + else: + self.assertIn(fip_admin.id, fip_id_list) + # By default, Nova returns only the FIPs that belong to the + # project which made the listing request. + self.assertNotIn(fip_user.id, fip_id_list) + self.assertRaisesRegex( + ValueError, "Nova-network don't support server-side.*", + self.operator_cloud.list_floating_ips, filters={'foo': 'bar'} + ) + + def test_search_floating_ips(self): + fip_user = self.demo_cloud.create_floating_ip() + self.addCleanup(self.demo_cloud.delete_floating_ip, fip_user.id) + + self.assertIn( + fip_user['id'], + [fip.id for fip in self.demo_cloud.search_floating_ips( + filters={"attached": False})] + ) + self.assertNotIn( + fip_user['id'], + [fip.id for fip in self.demo_cloud.search_floating_ips( + filters={"attached": True})] + ) diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index cb2a4409f..e0ed99679 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -166,9 +166,8 @@ def test_float_no_status(self): self.assertEqual('UNKNOWN', normalized[0]['status']) @patch.object(OpenStackCloud, 'neutron_client') - @patch.object(OpenStackCloud, 'has_service') + @patch.object(OpenStackCloud, 'has_service', return_value=True) def test_list_floating_ips(self, mock_has_service, mock_neutron_client): - mock_has_service.return_value = True mock_neutron_client.list_floatingips.return_value = \ self.mock_floating_ip_list_rep @@ -179,6 +178,21 @@ def test_list_floating_ips(self, mock_has_service, mock_neutron_client): self.assertAreInstances(floating_ips, dict) self.assertEqual(2, len(floating_ips)) + @patch.object(OpenStackCloud, 'neutron_client') + @patch.object(OpenStackCloud, 'has_service', return_value=True) + def test_list_floating_ips_with_filters(self, mock_has_service, + mock_neutron_client): + mock_neutron_client.list_floatingips.side_effect = \ + exc.OpenStackCloudURINotFound("") + + try: + self.cloud.list_floating_ips(filters={'Foo': 42}) + except exc.OpenStackCloudException as e: + self.assertIsInstance( + e.inner_exception[1], exc.OpenStackCloudURINotFound) + + mock_neutron_client.list_floatingips.assert_called_once_with(Foo=42) + @patch.object(OpenStackCloud, 'neutron_client') @patch.object(OpenStackCloud, 'has_service') def test_search_floating_ips(self, mock_has_service, mock_neutron_client): @@ -189,7 +203,7 @@ def test_search_floating_ips(self, mock_has_service, mock_neutron_client): floating_ips = self.cloud.search_floating_ips( filters={'attached': False}) - mock_neutron_client.list_floatingips.assert_called_with() + mock_neutron_client.list_floatingips.assert_called_with(attached=False) self.assertIsInstance(floating_ips, list) self.assertAreInstances(floating_ips, dict) self.assertEqual(1, len(floating_ips)) @@ -311,7 +325,7 @@ def test__neutron_available_floating_ips( mock_keystone_session.get_project_id.assert_called_once_with() mock_get_ext_nets.assert_called_once_with() - mock__neutron_list_fips.assert_called_once_with() + mock__neutron_list_fips.assert_called_once_with(None) mock__filter_list.assert_called_once_with( [], name_or_id=None, filters={'port_id': None, @@ -349,7 +363,7 @@ def test__neutron_available_floating_ips_network( mock_keystone_session.get_project_id.assert_called_once_with() mock_get_ext_nets.assert_called_once_with() - mock__neutron_list_fips.assert_called_once_with() + mock__neutron_list_fips.assert_called_once_with(None) mock__filter_list.assert_called_once_with( [], name_or_id=None, filters={'port_id': None, diff --git a/shade/tests/unit/test_floating_ip_nova.py b/shade/tests/unit/test_floating_ip_nova.py index e05cea7be..adf6ed32d 100644 --- a/shade/tests/unit/test_floating_ip_nova.py +++ b/shade/tests/unit/test_floating_ip_nova.py @@ -99,6 +99,17 @@ def test_list_floating_ips(self, mock_has_service, mock_nova_client): self.assertEqual(3, len(floating_ips)) self.assertAreInstances(floating_ips, dict) + @patch.object(OpenStackCloud, 'nova_client') + @patch.object(OpenStackCloud, 'has_service') + def test_list_floating_ips_with_filters(self, mock_has_service, + mock_nova_client): + mock_has_service.side_effect = has_service_side_effect + + self.assertRaisesRegex( + ValueError, "Nova-network don't support server-side", + self.cloud.list_floating_ips, filters={'Foo': 42} + ) + @patch.object(OpenStackCloud, 'nova_client') @patch.object(OpenStackCloud, 'has_service') def test_search_floating_ips(self, mock_has_service, mock_nova_client): From 6996f2329e9e0d1b3b8c090b64df8737b1495ba4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 19 Oct 2016 07:53:49 -0500 Subject: [PATCH 1095/3836] Don't fail on trying to delete non-existant images This currently throws an attribute exception, which is definitely not the right choice. Change-Id: I2e08ce9f46f4fa8ec1d42508efb16a014d485850 --- .../notes/false-not-attribute-error-49484d0fdc61f75d.yaml | 6 ++++++ shade/openstackcloud.py | 5 ++++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/false-not-attribute-error-49484d0fdc61f75d.yaml diff --git a/releasenotes/notes/false-not-attribute-error-49484d0fdc61f75d.yaml b/releasenotes/notes/false-not-attribute-error-49484d0fdc61f75d.yaml new file mode 100644 index 000000000..e474e0266 --- /dev/null +++ b/releasenotes/notes/false-not-attribute-error-49484d0fdc61f75d.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - delete_image used to fail with an AttributeError if an invalid image + name or id was passed, rather than returning False which was the + intent. This is worthy of note because it's a behavior change, but the + previous behavior was a bug. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 581e93b82..8d3dc4cb5 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2820,6 +2820,8 @@ def delete_image( self, name_or_id, wait=False, timeout=3600, delete_objects=True): image = self.get_image(name_or_id) + if not image: + return False with _utils.shade_exceptions("Error in deleting image"): # Note that in v1, the param name is image, but in v2, # it's image_id @@ -2843,7 +2845,8 @@ def delete_image( "Timeout waiting for the image to be deleted."): self._get_cache(None).invalidate() if self.get_image(image.id) is None: - return + break + return True def _get_name_and_filename(self, name): # See if name points to an existing file From f22af7f1b2e92d71477eabdbcbaa60905438a298 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 20 Oct 2016 16:03:56 -0500 Subject: [PATCH 1096/3836] Use floating-ip-by-router First of all - the method needs to be named by_router, not by_subnet. No clue what happened there. Secondly, there are several places it needs to be referenced. Third, there are are few bugs in the original impl that were discovered while running code against the DT cloud. Change-Id: Ib9256a9b7bd8ebdb56f1e5048e0aaf059e878b9c --- shade/openstackcloud.py | 84 +++++++++++--------- shade/tests/unit/test_floating_ip_neutron.py | 4 +- 2 files changed, 49 insertions(+), 39 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 20b326b08..b77aea33b 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -217,9 +217,9 @@ def __init__( self._floating_ips_time = 0 self._floating_ips_lock = threading.Lock() - self._floating_network_by_subnet = None - self._floating_network_by_subnet_run = False - self._floating_network_by_subnet_lock = threading.Lock() + self._floating_network_by_router = None + self._floating_network_by_router_run = False + self._floating_network_by_router_lock = threading.Lock() self._networks_lock = threading.Lock() self._reset_network_caches() @@ -3796,25 +3796,25 @@ def _expand_server_vars(self, server): # actually want the API to be. return meta.expand_server_vars(self, server) - def _find_floating_network_by_subnet(self): + def _find_floating_network_by_router(self): """Find the network providing floating ips by looking at routers.""" - if self._floating_network_by_subnet_lock.acquire( - not self._floating_network_by_subnet_run): - if self._floating_network_by_subnet_run: - self._floating_network_by_subnet_lock.release() - return self._floating_network_by_subnet + if self._floating_network_by_router_lock.acquire( + not self._floating_network_by_router_run): + if self._floating_network_by_router_run: + self._floating_network_by_router_lock.release() + return self._floating_network_by_router try: for router in self.list_routers(): if router['admin_state_up']: network_id = router.get( 'external_gateway_info', {}).get('network_id') - if network: - self._floating_network_by_subnet = network_id + if network_id: + self._floating_network_by_router = network_id finally: - self._floating_network_by_subnet_run = True - self._floating_network_by_subnet_lock.release() - return self._floating_network_by_subnet + self._floating_network_by_router_run = True + self._floating_network_by_router_lock.release() + return self._floating_network_by_router def available_floating_ip(self, network=None, server=None): """Get a floating IP from a network or a pool. @@ -3887,10 +3887,15 @@ def _neutron_available_floating_ips( else: # Get first existing external IPv4 network networks = self.get_external_ipv4_floating_networks() - if not networks: - raise OpenStackCloudResourceNotFound( - "unable to find an external network") - floating_network_id = networks[0]['id'] + if networks: + floating_network_id = networks[0]['id'] + else: + floating_network = self._find_floating_network_by_router() + if floating_network: + floating_network_id = floating_network + else: + raise OpenStackCloudResourceNotFound( + "unable to find an external network") filters = { 'port_id': None, @@ -3907,7 +3912,7 @@ def _neutron_available_floating_ips( # No available IP found or we didn't try # allocate a new Floating IP f_ip = self._neutron_create_floating_ip( - network_name_or_id=floating_network_id, server=server) + network_id=floating_network_id, server=server) return [f_ip] @@ -4017,28 +4022,31 @@ def _neutron_create_floating_ip( self, network_name_or_id=None, server=None, fixed_address=None, nat_destination=None, port=None, - wait=False, timeout=60): + wait=False, timeout=60, network_id=None): with _utils.neutron_exceptions( "unable to create floating IP for net " "{0}".format(network_name_or_id)): - if network_name_or_id: - networks = [self.get_network(network_name_or_id)] - if not networks: - raise OpenStackCloudResourceNotFound( - "unable to find network for floating ips with id " - "{0}".format(network_name_or_id)) - else: - networks = self.get_external_ipv4_floating_networks() - if networks: - network_id = networks[0]['id'] - else: - network_id = self._find_floating_network_by_router() - if not network_id: + if not network_id: + if network_name_or_id: + network = self.get_network(network_name_or_id) + if not network: raise OpenStackCloudResourceNotFound( - "Unable to find an external network in this cloud" - " which makes getting a floating IP impossible") + "unable to find network for floating ips with id " + "{0}".format(network_name_or_id)) + network_id = network['id'] + else: + networks = self.get_external_ipv4_floating_networks() + if networks: + network_id = networks[0]['id'] + else: + network_id = self._find_floating_network_by_router() + if not network_id: + raise OpenStackCloudResourceNotFound( + "Unable to find an external network in this" + " cloud which makes getting a floating IP" + " impossible") kwargs = { - 'floating_network_id': networks[0]['id'], + 'floating_network_id': network_id, } if not port: if server: @@ -4669,7 +4677,9 @@ def _needs_floating_ip(self, server, nat_destination): # No external IPv4 network - no FIPs networks = self.get_external_ipv4_networks() if not networks: - return False + network = self._find_floating_network_by_router() + if not network: + return False (port_obj, fixed_ip_address) = self._nat_destination_port( server, nat_destination=nat_destination) diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index e0ed99679..cd98148aa 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -333,7 +333,7 @@ def test__neutron_available_floating_ips( 'tenant_id': 'proj-id'} ) mock__neutron_create_fip.assert_called_once_with( - network_name_or_id=self.mock_get_network_rep['id'], + network_id=self.mock_get_network_rep['id'], server=None ) @@ -371,7 +371,7 @@ def test__neutron_available_floating_ips_network( 'tenant_id': 'proj-id'} ) mock__neutron_create_fip.assert_called_once_with( - network_name_or_id=self.mock_get_network_rep['id'], + network_id=self.mock_get_network_rep['id'], server=None ) From d02bd2c0d3b44602bac54828ca30e863b4e6fd19 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 22 Oct 2016 10:36:29 -0500 Subject: [PATCH 1097/3836] Update floating ip polling to account for DOWN status The DT cloud transitions floating ips through the DOWN state as part of normal operation. Don't ask me why. Put the check for port's not matching after waiting for ACTIVE state to be hit, rather than before. Also, don't throw an exception if the fip goes into DOWN while deleting. Finally, skip adding floating ip info to a server from neutron if the server is not in ACTIVE state, as a server is not expected to have valid addresses when it's not in that state anyway. Change-Id: I3020f31783f8d24cbd91942961a7c7868b51698d --- shade/meta.py | 4 ++- shade/openstackcloud.py | 25 +++++++-------- shade/tests/unit/test_floating_ip_neutron.py | 32 ++++++++++++++++++++ 3 files changed, 48 insertions(+), 13 deletions(-) diff --git a/shade/meta.py b/shade/meta.py index 1f2f03f17..c144a8334 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -295,7 +295,9 @@ def _get_suplemental_addresses(cloud, server): return server['addresses'] fixed_ip_mapping[address['addr']] = name try: - if cloud._has_floating_ips(): + # Don't bother doing this before the server is active, it's a waste + # of an API call while polling for a server to come up + if cloud._has_floating_ips() and server['status'] == 'ACTIVE': for fip in cloud.list_floating_ips(): if fip['fixed_ip_address'] in fixed_ip_mapping: fixed_net = fixed_ip_mapping[fip['fixed_ip_address']] diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index b77aea33b..bd70f40d7 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4064,16 +4064,6 @@ def _neutron_create_floating_ip( fip_id = fip['id'] if port: - if fip['port_id'] != port: - if server: - raise OpenStackCloudException( - "Attempted to create FIP on port {port} for server" - " {server} but something went wrong".format( - port=port, server=server['id'])) - else: - raise OpenStackCloudException( - "Attempted to create FIP on port {port}" - " but something went wrong".format(port=port)) # The FIP is only going to become active in this context # when we've attached it to something, which only occurs # if we've provided a port as a parameter @@ -4100,6 +4090,16 @@ def _neutron_create_floating_ip( "%(err)s", {'fip': fip_id, 'exc': e.__class__, 'err': str(e)}) raise + if fip['port_id'] != port: + if server: + raise OpenStackCloudException( + "Attempted to create FIP on port {port} for server" + " {server} but something went wrong".format( + port=port, server=server['id'])) + else: + raise OpenStackCloudException( + "Attempted to create FIP on port {port}" + " but something went wrong".format(port=port)) return fip def _nova_create_floating_ip(self, pool=None): @@ -4143,9 +4143,10 @@ def delete_floating_ip(self, floating_ip_id, retry=1): # neutron sometimes returns success when deleting a floating # ip. That's awesome. SO - verify that the delete actually - # worked. + # worked. Some clouds will set the status to DOWN rather than + # deleting the IP immediately. This is, of course, a bit absurd. f_ip = self.get_floating_ip(id=floating_ip_id) - if not f_ip: + if not f_ip or f_ip['status'] == 'DOWN': return True raise OpenStackCloudException( diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index cd98148aa..041076e85 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -451,6 +451,7 @@ def test_delete_floating_ip_existing( fake_fip = { 'id': '2f245a7b-796b-4f26-9cf9-9e82d248fda7', 'floating_ip_address': '172.99.106.167', + 'status': 'ACTIVE', } mock_get_floating_ip.side_effect = [fake_fip, fake_fip, None] @@ -466,6 +467,36 @@ def test_delete_floating_ip_existing( self.assertEqual(mock_get_floating_ip.call_count, 3) self.assertTrue(ret) + @patch.object(OpenStackCloud, 'get_floating_ip') + @patch.object(OpenStackCloud, 'neutron_client') + @patch.object(OpenStackCloud, 'has_service') + def test_delete_floating_ip_existing_down( + self, mock_has_service, mock_neutron_client, mock_get_floating_ip): + mock_has_service.return_value = True + fake_fip = { + 'id': '2f245a7b-796b-4f26-9cf9-9e82d248fda7', + 'floating_ip_address': '172.99.106.167', + 'status': 'ACTIVE', + } + down_fip = { + 'id': '2f245a7b-796b-4f26-9cf9-9e82d248fda7', + 'floating_ip_address': '172.99.106.167', + 'status': 'DOWN', + } + + mock_get_floating_ip.side_effect = [fake_fip, down_fip, None] + mock_neutron_client.delete_floatingip.return_value = None + + ret = self.cloud.delete_floating_ip( + floating_ip_id='2f245a7b-796b-4f26-9cf9-9e82d248fda7', + retry=2) + + mock_neutron_client.delete_floatingip.assert_called_with( + floatingip='2f245a7b-796b-4f26-9cf9-9e82d248fda7' + ) + self.assertEqual(mock_get_floating_ip.call_count, 2) + self.assertTrue(ret) + @patch.object(OpenStackCloud, 'get_floating_ip') @patch.object(OpenStackCloud, 'neutron_client') @patch.object(OpenStackCloud, 'has_service') @@ -475,6 +506,7 @@ def test_delete_floating_ip_existing_no_delete( fake_fip = { 'id': '2f245a7b-796b-4f26-9cf9-9e82d248fda7', 'floating_ip_address': '172.99.106.167', + 'status': 'ACTIVE', } mock_get_floating_ip.side_effect = [fake_fip, fake_fip, fake_fip] From 6bc9e82c8082838d3a0517d252b471a452585340 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 7 Nov 2016 14:18:57 -0600 Subject: [PATCH 1098/3836] Refactor out the fallback-to-router logic The same chunk of logic for looking at external networks then falling back to neutron router investigation happened in three places. Split it into its own method to reduce duplication. Change-Id: I154c6172e2f8d710b3fe2bea6b6fe7f4c66d3441 --- shade/openstackcloud.py | 48 ++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index b77aea33b..9004293f4 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3844,6 +3844,20 @@ def available_floating_ip(self, network=None, server=None): ) return f_ips[0] + def _get_floating_network_id(self): + # Get first existing external IPv4 network + networks = self.get_external_ipv4_floating_networks() + if networks: + floating_network_id = networks[0]['id'] + else: + floating_network = self._find_floating_network_by_router() + if floating_network: + floating_network_id = floating_network + else: + raise OpenStackCloudResourceNotFound( + "unable to find an external network") + return floating_network_id + def _neutron_available_floating_ips( self, network=None, project_id=None, server=None): """Get a floating IP from a Neutron network. @@ -3885,17 +3899,7 @@ def _neutron_available_floating_ips( net=network) ) else: - # Get first existing external IPv4 network - networks = self.get_external_ipv4_floating_networks() - if networks: - floating_network_id = networks[0]['id'] - else: - floating_network = self._find_floating_network_by_router() - if floating_network: - floating_network_id = floating_network - else: - raise OpenStackCloudResourceNotFound( - "unable to find an external network") + floating_network_id = self._get_floating_network_id() filters = { 'port_id': None, @@ -4035,16 +4039,7 @@ def _neutron_create_floating_ip( "{0}".format(network_name_or_id)) network_id = network['id'] else: - networks = self.get_external_ipv4_floating_networks() - if networks: - network_id = networks[0]['id'] - else: - network_id = self._find_floating_network_by_router() - if not network_id: - raise OpenStackCloudResourceNotFound( - "Unable to find an external network in this" - " cloud which makes getting a floating IP" - " impossible") + network_id = self._get_floating_network_id() kwargs = { 'floating_network_id': network_id, } @@ -4674,12 +4669,11 @@ def _needs_floating_ip(self, server, nat_destination): if not self.has_service('network'): return True - # No external IPv4 network - no FIPs - networks = self.get_external_ipv4_networks() - if not networks: - network = self._find_floating_network_by_router() - if not network: - return False + # No floating ip network - no FIPs + try: + self._get_floating_network_id() + except OpenStackCloudException: + return False (port_obj, fixed_ip_address) = self._nat_destination_port( server, nat_destination=nat_destination) From 071fb5fa8a17fe82107d38c2ec8418686a42289c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 1 Nov 2016 07:10:55 -0500 Subject: [PATCH 1099/3836] Add unit test to show herd protection in action The discussion around the previous patch had a simple test case which was used to show invalid and valid behavior. Turns out, that can be a test. Change-Id: I39550d44f4e83f803624a833a081101bad9dc545 --- shade/tests/unit/test_caching.py | 28 ++++++++++++++++++++++++++++ test-requirements.txt | 1 + 2 files changed, 29 insertions(+) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 78bbf98f3..cf8b15d2d 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -11,7 +11,9 @@ # 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 concurrent import tempfile +import time from glanceclient.v2 import shell import mock @@ -139,6 +141,32 @@ def test_list_projects_v2(self, keystone_mock): meta.obj_list_to_dict([project, project_b]), self.cloud.list_projects()) + @mock.patch('shade.OpenStackCloud.nova_client') + def test_list_servers_herd(self, nova_mock): + self.cloud._SERVER_AGE = 0 + fake_server = fakes.FakeServer('1234', '', 'ACTIVE') + nova_mock.servers.list.return_value = [fake_server] + with concurrent.futures.ThreadPoolExecutor(16) as pool: + for i in range(16): + pool.submit(lambda: self.cloud.list_servers()) + # It's possible to race-condition 16 threads all in the + # single initial lock without a tiny sleep + time.sleep(0.001) + self.assertGreater(nova_mock.servers.list.call_count, 1) + + @mock.patch('shade.OpenStackCloud.nova_client') + def test_list_servers_no_herd(self, nova_mock): + self.cloud._SERVER_AGE = 2 + fake_server = fakes.FakeServer('1234', '', 'ACTIVE') + nova_mock.servers.list.return_value = [fake_server] + with concurrent.futures.ThreadPoolExecutor(16) as pool: + for i in range(16): + pool.submit(lambda: self.cloud.list_servers()) + # It's possible to race-condition 16 threads all in the + # single initial lock without a tiny sleep + time.sleep(0.001) + self.assertEqual(1, nova_mock.servers.list.call_count) + @mock.patch('shade.OpenStackCloud.cinder_client') def test_list_volumes(self, cinder_mock): fake_volume = fakes.FakeVolume('volume1', 'available', diff --git a/test-requirements.txt b/test-requirements.txt index 6c5b8c5ff..a9b65e976 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -13,3 +13,4 @@ testscenarios>=0.4,<0.5 testtools>=0.9.32 warlock>=1.0.1,<2 reno +futures;python_version<'3.2' From b175fe9feca1eaf215f7d7d234206aebfc251f09 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 9 Nov 2016 12:54:54 -0600 Subject: [PATCH 1100/3836] Make a private method more privater This is not actually necessary - but I'm about to do a few things with a regex and this kept tripping up the regex. Might as well 'fix' it. Change-Id: If48295bb552dcbe725c13d14467afad2ff99757b --- shade/openstackcloud.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 3ad80b4d0..756087506 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2464,7 +2464,7 @@ def get_stack(self, name_or_id, filters=None): openstack API call or if multiple matches are found. """ - def search_one_stack(name_or_id=None, filters=None): + def _search_one_stack(name_or_id=None, filters=None): # stack names are mandatory and enforced unique in the project # so a StackGet can always be used for name or ID. with _utils.shade_exceptions("Error fetching stack"): @@ -2481,7 +2481,7 @@ def search_one_stack(name_or_id=None, filters=None): return _utils._filter_list(nstacks, name_or_id, filters) return _utils._get_entity( - search_one_stack, name_or_id, filters) + _search_one_stack, name_or_id, filters) def create_keypair(self, name, public_key): """Create a new keypair. From 73e39a5dd1c3d3276e355f2542e3e2b3a488e323 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 9 Nov 2016 17:16:54 -0600 Subject: [PATCH 1101/3836] Make search_projects a special case of list_projects As the first step in aligning list and search, make list_projects have all the things and search just be an alias. In this case, there were some positional arguments already in place, so it can't be a 100% straight alias - but it can at least only have one set of logic. As part of this, added some infrastructure for dealing with filter logic and pushdown conditions. Change-Id: Iff349ff26e59c9ae8022e3ee8f0987197b0eccc4 --- shade/_normalize.py | 29 +++++++++++++++++ shade/openstackcloud.py | 54 ++++++++++++++++++-------------- shade/tests/unit/test_project.py | 27 ++++++++++++++++ 3 files changed, 86 insertions(+), 24 deletions(-) diff --git a/shade/_normalize.py b/shade/_normalize.py index 1245dc450..0a06fd92e 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -46,6 +46,35 @@ ) +_pushdown_fields = { + 'project': [ + 'domain' + ] +} + + +def _split_filters(obj_name='', filters=None, **kwargs): + # Handle jmsepath filters + if not filters: + filters = {} + if not isinstance(filters, dict): + return {}, filters + # Filter out None values from extra kwargs, because those are + # defaults. If you want to search for things with None values, + # they're going to need to go into the filters dict + for (key, value) in kwargs.items(): + if value is not None: + filters[key] = value + pushdown = {} + client = {} + for (key, value) in filters.items(): + if key in _pushdown_fields.get(obj_name, {}): + pushdown[key] = value + else: + client[key] = value + return pushdown, client + + def _to_bool(value): if isinstance(value, six.string_types): if not value: diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 756087506..a555cb220 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -562,42 +562,48 @@ def range_search(self, data, filters): return filtered @_utils.cache_on_arguments() - def list_projects(self, domain_id=None): - """List Keystone Projects. + def list_projects(self, domain_id=None, name_or_id=None, filters=None): + """List Keystone projects. - :param string domain_id: domain id to scope the listed projects. + With no parameters, returns a full listing of all visible projects. - :returns: a list of ``munch.Munch`` containing the project description. + :param domain_id: domain id to scope the searched projects. + :param name_or_id: project name or id. + :param filters: a dict containing additional filters to use + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: a list of ``munch.Munch`` containing the projects :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ + kwargs = dict( + filters=filters, + domain=domain_id) + if self.cloud_config.get_api_version('identity') == '3': + kwargs['obj_name'] = 'project' + + pushdown, filters = _normalize._split_filters(**kwargs) + try: - if self.cloud_config.get_api_version('identity') == '3': - projects = self.manager.submit_task( - _tasks.ProjectList(domain=domain_id)) - else: - projects = self.manager.submit_task( - _tasks.ProjectList()) + projects = self.manager.submit_task(_tasks.ProjectList(**pushdown)) except Exception as e: self.log.debug("Failed to list projects", exc_info=True) raise OpenStackCloudException(str(e)) - return projects + return _utils._filter_list(projects, name_or_id, filters) def search_projects(self, name_or_id=None, filters=None, domain_id=None): - """Seach Keystone projects. - - :param name_or_id: project name or id. - :param filters: a dict containing additional filters to use. - :param domain_id: domain id to scope the searched projects. - - :returns: a list of ``munch.Munch`` containing the projects - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - projects = self.list_projects(domain_id=domain_id) - return _utils._filter_list(projects, name_or_id, filters) + '''Backwards compatibility method for search_projects + + search_projects originally had a parameter list that was name_or_id, + filters and list had domain_id first. This method exists in this form + to allow code written with positional parameter to still work. But + really, use keyword arguments. + ''' + return self.list_projects( + domain_id=domain_id, name_or_id=name_or_id, filters=filters) def get_project(self, name_or_id, filters=None, domain_id=None): """Get exactly one Keystone project. diff --git a/shade/tests/unit/test_project.py b/shade/tests/unit/test_project.py index 0efd6cd6e..3e8ff42d6 100644 --- a/shade/tests/unit/test_project.py +++ b/shade/tests/unit/test_project.py @@ -20,6 +20,7 @@ import testtools import shade +import shade._utils from shade.tests.unit import base @@ -123,3 +124,29 @@ def test_list_projects_v3(self, mock_keystone, mock_api_version): self.op_cloud.list_projects('123') mock_keystone.projects.list.assert_called_once_with( domain='123') + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_list_projects_v3_kwarg(self, mock_keystone, mock_api_version): + mock_api_version.return_value = '3' + self.op_cloud.list_projects(domain_id='123') + mock_keystone.projects.list.assert_called_once_with( + domain='123') + + @mock.patch.object(shade._utils, '_filter_list') + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_list_projects_search_compat( + self, mock_keystone, mock_api_version, mock_filter_list): + mock_api_version.return_value = '3' + self.op_cloud.search_projects('123') + mock_keystone.projects.list.assert_called_once_with() + mock_filter_list.assert_called_once_with(mock.ANY, '123', mock.ANY) + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_list_projects_search_compat_v3( + self, mock_keystone, mock_api_version): + mock_api_version.return_value = '3' + self.op_cloud.search_projects(domain_id='123') + mock_keystone.projects.list.assert_called_once_with(domain='123') From f5c37ffcd4aa32da88a9afdb576d9773ea1bbfbf Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 10 Nov 2016 11:08:36 -0600 Subject: [PATCH 1102/3836] Only generate checksums if neither is given A single checksum is sufficient for unique checking if given by the user. If the user gives no checksums, then we might as well do both since we're doing it in one pass. Change-Id: Ic589d5771b6f54e6f03d4612b8e7c5a4be6bbc36 --- .../less-file-hashing-d2497337da5acbef.yaml | 5 +++ shade/openstackcloud.py | 36 +++++++++++-------- 2 files changed, 27 insertions(+), 14 deletions(-) create mode 100644 releasenotes/notes/less-file-hashing-d2497337da5acbef.yaml diff --git a/releasenotes/notes/less-file-hashing-d2497337da5acbef.yaml b/releasenotes/notes/less-file-hashing-d2497337da5acbef.yaml new file mode 100644 index 000000000..4d0fd1a1f --- /dev/null +++ b/releasenotes/notes/less-file-hashing-d2497337da5acbef.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - shade will now only generate file hashes for glance + images if both hashes are empty. If only one is given, + the other will be treated as an empty string. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 3ad80b4d0..bd20baae0 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3006,19 +3006,27 @@ def create_image( container_format = 'ovf' else: container_format = 'bare' - if not md5 or not sha256: + if not (md5 or sha256): (md5, sha256) = self._get_file_hashes(filename) if allow_duplicates: current_image = None else: current_image = self.get_image(name) - if (current_image and current_image.get(IMAGE_MD5_KEY, '') == md5 - and current_image.get(IMAGE_SHA256_KEY, '') == sha256): - self.log.debug( - "image %(name)s exists and is up to date", {'name': name}) - return current_image - kwargs[IMAGE_MD5_KEY] = md5 - kwargs[IMAGE_SHA256_KEY] = sha256 + if current_image: + md5_key = current_image.get(IMAGE_MD5_KEY, '') + sha256_key = current_image.get(IMAGE_SHA256_KEY, '') + up_to_date = False + if md5 and md5_key == md5: + up_to_date = True + if sha256 and sha256_key == sha256: + up_to_date = True + if up_to_date: + self.log.debug( + "image %(name)s exists and is up to date", + {'name': name}) + return current_image + kwargs[IMAGE_MD5_KEY] = md5 or '' + kwargs[IMAGE_SHA256_KEY] = sha256 or '' kwargs[IMAGE_OBJECT_KEY] = '/'.join([container, name]) if disable_vendor_agent: @@ -5391,15 +5399,15 @@ def is_object_stale( container=container, name=name)) return True - if file_md5 is None or file_sha256 is None: + if not (file_md5 or file_sha256): (file_md5, file_sha256) = self._get_file_hashes(filename) - if metadata.get(OBJECT_MD5_KEY, '') != file_md5: + if file_md5 and metadata.get(OBJECT_MD5_KEY, '') != file_md5: self.log.debug( "swift md5 mismatch: %(filename)s!=%(container)s/%(name)s", {'filename': filename, 'container': container, 'name': name}) return True - if metadata.get(OBJECT_SHA256_KEY, '') != file_sha256: + if file_sha256 and metadata.get(OBJECT_SHA256_KEY, '') != file_sha256: self.log.debug( "swift sha256 mismatch: %(filename)s!=%(container)s/%(name)s", {'filename': filename, 'container': container, 'name': name}) @@ -5449,10 +5457,10 @@ def create_object( segment_size = self.get_object_segment_size(segment_size) - if not md5 or not sha256: + if not (md5 or sha256): (md5, sha256) = self._get_file_hashes(filename) - headers[OBJECT_MD5_KEY] = md5 - headers[OBJECT_SHA256_KEY] = sha256 + headers[OBJECT_MD5_KEY] = md5 or '' + headers[OBJECT_SHA256_KEY] = sha256 or '' header_list = sorted([':'.join([k, v]) for (k, v) in headers.items()]) for (k, v) in metadata.items(): header_list.append(':'.join(['x-object-meta-' + k, v])) From 6d9552e535d33b26e6b21189e1109ca6af6f2683 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 10 Nov 2016 11:20:12 -0600 Subject: [PATCH 1103/3836] Cache file checksums by filename and mtime The current cache only does by filename. An image could change out from under a long-running shade process - so include mtime in the cache key. Change-Id: Ibe649355fa123727b7b61af478e198a22b56aaae --- shade/openstackcloud.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index bd20baae0..37eadc48b 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5339,7 +5339,10 @@ def get_container_access(self, name): "Could not determine container access for ACL: %s." % acl) def _get_file_hashes(self, filename): - if filename not in self._file_hash_cache: + file_key = "{filename}:{mtime}".format( + filename=filename, + mtime=os.stat(filename).st_mtime) + if file_key not in self._file_hash_cache: self.log.debug( 'Calculating hashes for %(filename)s', {'filename': filename}) md5 = hashlib.md5() @@ -5348,15 +5351,15 @@ def _get_file_hashes(self, filename): for chunk in iter(lambda: file_obj.read(8192), b''): md5.update(chunk) sha256.update(chunk) - self._file_hash_cache[filename] = dict( + self._file_hash_cache[file_key] = dict( md5=md5.hexdigest(), sha256=sha256.hexdigest()) self.log.debug( "Image file %(filename)s md5:%(md5)s sha256:%(sha256)s", {'filename': filename, - 'md5': self._file_hash_cache[filename]['md5'], - 'sha256': self._file_hash_cache[filename]['sha256']}) - return (self._file_hash_cache[filename]['md5'], - self._file_hash_cache[filename]['sha256']) + 'md5': self._file_hash_cache[file_key]['md5'], + 'sha256': self._file_hash_cache[file_key]['sha256']}) + return (self._file_hash_cache[file_key]['md5'], + self._file_hash_cache[file_key]['sha256']) @_utils.cache_on_arguments() def get_object_capabilities(self): From 2bf2729bfc3c2f001785c6a921c5a6d3ecceec1e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 10 Nov 2016 10:08:12 -0600 Subject: [PATCH 1104/3836] Normalize projects We were returning projects un-normalized. That's no good for anybody. Add normalization and documentation of the agreed model. It's worth noting that because it's a project, information about project and domain in the location dict is a bit more specific. Change-Id: I3bbfd010883587857cf09f082124816e701fbe6f --- doc/source/model.rst | 28 +++++++++++++ shade/_normalize.py | 67 ++++++++++++++++++++++++++++++++ shade/openstackcloud.py | 19 +++++---- shade/tests/unit/test_caching.py | 18 ++++++--- 4 files changed, 118 insertions(+), 14 deletions(-) diff --git a/doc/source/model.rst b/doc/source/model.rst index 0154bb9f1..e580c03cd 100644 --- a/doc/source/model.rst +++ b/doc/source/model.rst @@ -209,3 +209,31 @@ A Floating IP from Neutron or Nova router=str(), status=str(), properties=dict()) + +Project +------- + +A Project from Keystone (or a tenant if Keystone v2) + +Location information for Project has some specific semantics. + +If the project has a parent project, that will be in location.project.id, +and if it doesn't that should be None. If the Project is associated with +a domain that will be in location.project.domain_id regardless of the current +user's token scope. location.project.name and location.project.domain_name +will always be None. Finally, location.region_name will always be None as +Projects are global to a cloud. If a deployer happens to deploy OpenStack +in such a way that users and projects are not shared amongst regions, that +necessitates treating each of those regions as separate clouds from shade's +POV. + +.. code-block:: python + + Project = dict( + location=Location(), + id=str(), + name=str(), + description=str(), + is_enabled=bool(), + is_domain=bool(), + properties=dict()) diff --git a/shade/_normalize.py b/shade/_normalize.py index 0a06fd92e..08eda46a7 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -496,3 +496,70 @@ def _normalize_floating_ip(self, ip): ret.setdefault(key, val) return ret + + def _normalize_projects(self, projects): + """Normalize the structure of projects + + This makes tenants from keystone v2 look like projects from v3. + + :param list projects: A list of projects to normalize + + :returns: A list of normalized dicts. + """ + ret = [] + for project in projects: + ret.append(self._normalize_project(project)) + return ret + + def _normalize_project(self, project): + + ret = munch.Munch() + # Copy incoming project because of shared dicts in unittests + project = project.copy() + + # Discard noise + project.pop('links', None) + project.pop('NAME_ATTR', None) + project.pop('HUMAN_ID', None) + project.pop('human_id', None) + + # In both v2 and v3 + project_id = project.pop('id') + name = project.pop('name', '') + description = project.pop('description', '') + is_enabled = project.pop('enabled', True) + + # Projects are global - strip region + location = self._get_current_location(project_id=project_id) + location['region_name'] = None + + # v3 additions + domain_id = project.pop('domain_id', 'default') + parent_id = project.pop('parent_id', None) + is_domain = project.pop('is_domain', False) + + # Projects have a special relationship with location + location['project']['domain_id'] = domain_id + location['project']['domain_name'] = None + location['project']['name'] = None + location['project']['id'] = parent_id + + ret = munch.Munch( + location=location, + id=project_id, + name=name, + description=description, + is_enabled=is_enabled, + is_domain=is_domain, + properties=project.copy() + ) + + # Backwards compat + if not self.strict_mode: + ret['enabled'] = is_enabled + ret['domain_id'] = domain_id + ret['parent_id'] = parent_id + for key, val in ret['properties'].items(): + ret.setdefault(key, val) + + return ret diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index a555cb220..7e7078a78 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -588,7 +588,8 @@ def list_projects(self, domain_id=None, name_or_id=None, filters=None): pushdown, filters = _normalize._split_filters(**kwargs) try: - projects = self.manager.submit_task(_tasks.ProjectList(**pushdown)) + projects = self._normalize_projects( + self.manager.submit_task(_tasks.ProjectList(**pushdown))) except Exception as e: self.log.debug("Failed to list projects", exc_info=True) raise OpenStackCloudException(str(e)) @@ -636,10 +637,11 @@ def update_project(self, name_or_id, description=None, enabled=True, else: params['tenant_id'] = proj['id'] - project = self.manager.submit_task(_tasks.ProjectUpdate( - description=description, - enabled=enabled, - **params)) + project = self._normalize_project( + self.manager.submit_task(_tasks.ProjectUpdate( + description=description, + enabled=enabled, + **params))) self.list_projects.invalidate(self) return project @@ -654,9 +656,10 @@ def create_project( else: params['tenant_name'] = name - project = self.manager.submit_task(_tasks.ProjectCreate( - project_name=name, description=description, enabled=enabled, - **params)) + project = self._normalize_project( + self.manager.submit_task(_tasks.ProjectCreate( + project_name=name, description=description, + enabled=enabled, **params))) self.list_projects.invalidate(self) return project diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index cf8b15d2d..0d18017e5 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -115,14 +115,17 @@ def test_list_projects_v3(self, keystone_mock): keystone_mock.projects.list.return_value = [project] self.cloud.cloud_config.config['identity_api_version'] = '3' self.assertEqual( - meta.obj_list_to_dict([project]), self.cloud.list_projects()) + self.cloud._normalize_projects(meta.obj_list_to_dict([project])), + self.cloud.list_projects()) project_b = fakes.FakeProject('project_b') keystone_mock.projects.list.return_value = [project, project_b] self.assertEqual( - meta.obj_list_to_dict([project]), self.cloud.list_projects()) + self.cloud._normalize_projects(meta.obj_list_to_dict([project])), + self.cloud.list_projects()) self.cloud.list_projects.invalidate(self.cloud) self.assertEqual( - meta.obj_list_to_dict([project, project_b]), + self.cloud._normalize_projects( + meta.obj_list_to_dict([project, project_b])), self.cloud.list_projects()) @mock.patch('shade.OpenStackCloud.keystone_client') @@ -131,14 +134,17 @@ def test_list_projects_v2(self, keystone_mock): keystone_mock.tenants.list.return_value = [project] self.cloud.cloud_config.config['identity_api_version'] = '2' self.assertEqual( - meta.obj_list_to_dict([project]), self.cloud.list_projects()) + self.cloud._normalize_projects(meta.obj_list_to_dict([project])), + self.cloud.list_projects()) project_b = fakes.FakeProject('project_b') keystone_mock.tenants.list.return_value = [project, project_b] self.assertEqual( - meta.obj_list_to_dict([project]), self.cloud.list_projects()) + self.cloud._normalize_projects(meta.obj_list_to_dict([project])), + self.cloud.list_projects()) self.cloud.list_projects.invalidate(self.cloud) self.assertEqual( - meta.obj_list_to_dict([project, project_b]), + self.cloud._normalize_projects( + meta.obj_list_to_dict([project, project_b])), self.cloud.list_projects()) @mock.patch('shade.OpenStackCloud.nova_client') From 42bd7d994f9015031526d3e2b5338df2969f6b9f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 10 Nov 2016 11:27:33 -0600 Subject: [PATCH 1105/3836] Fail up to date check on one out of sync value Make sure that if a checksum is given, it is valid. Change-Id: Iad4a94c45f7e96800c34f1309f186aecfbe53776 --- shade/openstackcloud.py | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 37eadc48b..ea9ae2e63 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2930,6 +2930,23 @@ def _get_name_and_filename(self, name): ' Please provide either a path to an existing file' ' or a name and a filename'.format(name=name)) + def _hashes_up_to_date(self, md5, sha256, md5_key, sha256_key): + '''Compare md5 and sha256 hashes for being up to date + + md5 and sha256 are the current values. + md5_key and sha256_key are the previous values. + ''' + up_to_date = False + if md5 and md5_key == md5: + up_to_date = True + if sha256 and sha256_key == sha256: + up_to_date = True + if md5 and md5_key != md5: + up_to_date = False + if sha256 and sha256_key != sha256: + up_to_date = False + return up_to_date + def create_image( self, name, filename=None, container='images', md5=None, sha256=None, @@ -3015,11 +3032,9 @@ def create_image( if current_image: md5_key = current_image.get(IMAGE_MD5_KEY, '') sha256_key = current_image.get(IMAGE_SHA256_KEY, '') - up_to_date = False - if md5 and md5_key == md5: - up_to_date = True - if sha256 and sha256_key == sha256: - up_to_date = True + up_to_date = self._hashes_up_to_date( + md5=md5, sha256=sha256, + md5_key=md5_key, sha256_key=sha256_key) if up_to_date: self.log.debug( "image %(name)s exists and is up to date", @@ -5404,15 +5419,16 @@ def is_object_stale( if not (file_md5 or file_sha256): (file_md5, file_sha256) = self._get_file_hashes(filename) + md5_key = metadata.get(OBJECT_MD5_KEY, '') + sha256_key = metadata.get(OBJECT_SHA256_KEY, '') + up_to_date = self._hashes_up_to_date( + md5=file_md5, sha256=file_sha256, + md5_key=md5_key, sha256_key=sha256_key) - if file_md5 and metadata.get(OBJECT_MD5_KEY, '') != file_md5: - self.log.debug( - "swift md5 mismatch: %(filename)s!=%(container)s/%(name)s", - {'filename': filename, 'container': container, 'name': name}) - return True - if file_sha256 and metadata.get(OBJECT_SHA256_KEY, '') != file_sha256: + if not up_to_date: self.log.debug( - "swift sha256 mismatch: %(filename)s!=%(container)s/%(name)s", + "swift checksum mismatch: " + " %(filename)s!=%(container)s/%(name)s", {'filename': filename, 'container': container, 'name': name}) return True From 1f9e2cd123b38a7e744fb8a784d0ee3b523de95e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 1 Sep 2016 13:26:00 -0500 Subject: [PATCH 1106/3836] Remove validate_auth_ksc This was a workaround for python-openstackclient back when it was still dependent on keystoneclient. OSC has its own workaround now, so this should no longer be needed. Change-Id: Ib1877b7978b7b016b394232235e887360b6bdf85 --- os_client_config/config.py | 71 ++++---------------------- os_client_config/tests/test_config.py | 28 ++++++++-- os_client_config/tests/test_environ.py | 17 +++--- os_client_config/tests/test_init.py | 5 +- 4 files changed, 45 insertions(+), 76 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index a0d862d15..6078366e6 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -609,15 +609,19 @@ def _fix_backwards_project(self, cloud): 'tenant_id', 'tenant-id', 'project_id', 'project-id') mappings['project_name'] = ( 'tenant_name', 'tenant-name', 'project_name', 'project-name') + # Special-case username and password so that we don't have to load + # the plugins so early + mappings['username'] = ('user-name', 'user_name', 'username') + mappings['password'] = ('password',) for target_key, possible_values in mappings.items(): target = None for key in possible_values: - if key in cloud: - target = str(cloud[key]) - del cloud[key] - if key in cloud['auth']: - target = str(cloud['auth'][key]) - del cloud['auth'][key] + root_target = cloud.pop(key, None) + auth_target = cloud['auth'].pop(key, None) + if root_target: + target = str(root_target) + elif auth_target: + target = str(auth_target) if target: cloud['auth'][target_key] = target return cloud @@ -856,59 +860,6 @@ def _get_auth_loader(self, config): config['auth']['token'] = 'notused' return loading.get_plugin_loader(config['auth_type']) - def _validate_auth_ksc(self, config, cloud): - try: - import keystoneclient.auth as ksc_auth - except ImportError: - return config - - # May throw a keystoneclient.exceptions.NoMatchingPlugin - plugin_options = ksc_auth.get_plugin_class( - config['auth_type']).get_options() - - for p_opt in plugin_options: - # if it's in config.auth, win, kill it from config dict - # if it's in config and not in config.auth, move it - # deprecated loses to current - # provided beats default, deprecated or not - winning_value = self._find_winning_auth_value( - p_opt, - config['auth'], - ) - if not winning_value: - winning_value = self._find_winning_auth_value( - p_opt, - config, - ) - - # 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: - raise exceptions.OpenStackConfigException( - 'Unable to find auth information for cloud' - ' {cloud} in config files {files}' - ' or environment variables. Missing value {auth_key}' - ' required for auth plugin {plugin}'.format( - cloud=cloud, files=','.join(self._config_files), - 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_opts]: - 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 - - return config - def _validate_auth(self, config, loader): # May throw a keystoneauth1.exceptions.NoMatchingPlugin @@ -1016,6 +967,7 @@ def magic_fixes(self, config): ('auth_token' in config and config['auth_token']) or ('token' in config and config['token'])): config.setdefault('token', config.pop('auth_token', None)) + config.setdefault('auth_type', 'token') # These backwards compat values are only set via argparse. If it's # there, it's because it was passed in explicitly, and should win @@ -1062,7 +1014,6 @@ def get_one_cloud(self, cloud=None, validate=True, :raises: keystoneauth1.exceptions.MissingRequiredOptions on missing required auth parameters """ - args = self._fix_args(kwargs, argparse=argparse) if cloud is None: diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index ad3685ab1..efab79792 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -226,7 +226,7 @@ def test_only_secure_yaml(self): c = config.OpenStackConfig(config_files=['nonexistent'], vendor_files=['nonexistent'], secure_files=[self.secure_yaml]) - cc = c.get_one_cloud(cloud='_test_cloud_no_vendor') + cc = c.get_one_cloud(cloud='_test_cloud_no_vendor', validate=False) self.assertEqual('testpass', cc.auth['password']) def test_get_cloud_names(self): @@ -366,7 +366,6 @@ def setUp(self): project_name='project', region_name='region2', snack_type='cookie', - os_auth_token='no-good-things', ) self.options = argparse.Namespace(**self.args) @@ -417,7 +416,7 @@ def test_get_one_cloud_precedence(self): cc = c.get_one_cloud( argparse=options, **kwargs) self.assertEqual(cc.region_name, 'region2') - self.assertEqual(cc.auth['password'], 'authpass') + self.assertEqual(cc.auth['password'], 'argpass') self.assertEqual(cc.snack_type, 'cookie') def test_get_one_cloud_precedence_osc(self): @@ -474,7 +473,7 @@ def test_get_one_cloud_precedence_no_argparse(self): cc = c.get_one_cloud(**kwargs) self.assertEqual(cc.region_name, 'kwarg_region') - self.assertEqual(cc.auth['password'], 'authpass') + self.assertEqual(cc.auth['password'], 'ansible_password') self.assertIsNone(cc.password) def test_get_one_cloud_just_argparse(self): @@ -649,11 +648,30 @@ def test_argparse_token(self): parser.add_argument('--os-auth-token') opts, _remain = parser.parse_known_args( ['--os-auth-token', 'very-bad-things', - '--os-auth-type', 'token']) + '--os-auth-type', 'token', + '--os-auth-url', 'http://example.com/v2', + '--os-project-name', 'project']) cc = c.get_one_cloud(argparse=opts) self.assertEqual(cc.config['auth_type'], 'token') self.assertEqual(cc.config['auth']['token'], 'very-bad-things') + def test_argparse_username_token(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + + parser = argparse.ArgumentParser() + c.register_argparse_arguments(parser, []) + # novaclient will add this + parser.add_argument('--os-auth-token') + opts, _remain = parser.parse_known_args( + ['--os-auth-token', 'very-bad-things', + '--os-auth-type', 'token', + '--os-auth-url', 'http://example.com/v2', + '--os-username', 'user', + '--os-project-name', 'project']) + self.assertRaises( + TypeError, c.get_one_cloud, argparse=opts) + def test_argparse_underscores(self): c = config.OpenStackConfig(config_files=[self.no_yaml], vendor_files=[self.no_yaml], diff --git a/os_client_config/tests/test_environ.py b/os_client_config/tests/test_environ.py index 35ce2f2bf..9cece4887 100644 --- a/os_client_config/tests/test_environ.py +++ b/os_client_config/tests/test_environ.py @@ -19,6 +19,7 @@ from os_client_config.tests import base import fixtures +import keystoneauth1.exceptions class TestEnviron(base.TestCase): @@ -144,13 +145,11 @@ def test_incomplete_envvars(self): fixtures.EnvironmentVariable('NOVA_USERNAME', 'nova')) self.useFixture( fixtures.EnvironmentVariable('OS_USERNAME', 'user')) - config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) - # This is broken due to an issue that's fixed in a subsequent patch - # commenting it out in this patch to keep the patch size reasonable - # self.assertRaises( - # keystoneauth1.exceptions.auth_plugins.MissingRequiredOptions, - # c.get_one_cloud, 'envvars') + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) + self.assertRaises( + keystoneauth1.exceptions.auth_plugins.MissingRequiredOptions, + c.get_one_cloud, 'envvars') def test_have_envvars(self): self.useFixture( @@ -165,7 +164,7 @@ def test_have_envvars(self): fixtures.EnvironmentVariable('OS_PROJECT_NAME', 'project')) c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud('envvars') + cc = c.get_one_cloud('envvars', validate=False) self.assertEqual(cc.config['auth']['username'], 'user') def test_old_envvars(self): @@ -181,5 +180,5 @@ def test_old_envvars(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml], envvar_prefix='NOVA_') - cc = c.get_one_cloud('envvars') + cc = c.get_one_cloud('envvars', validate=False) self.assertEqual(cc.config['auth']['username'], 'nova') diff --git a/os_client_config/tests/test_init.py b/os_client_config/tests/test_init.py index 15d57f717..76ad48597 100644 --- a/os_client_config/tests/test_init.py +++ b/os_client_config/tests/test_init.py @@ -18,7 +18,8 @@ class TestInit(base.TestCase): def test_get_config_without_arg_parser(self): - cloud_config = os_client_config.get_config(options=None) + cloud_config = os_client_config.get_config( + options=None, validate=False) self.assertIsInstance( cloud_config, os_client_config.cloud_config.CloudConfig @@ -26,7 +27,7 @@ def test_get_config_without_arg_parser(self): def test_get_config_with_arg_parser(self): cloud_config = os_client_config.get_config( - options=argparse.ArgumentParser()) + options=argparse.ArgumentParser(), validate=False) self.assertIsInstance( cloud_config, os_client_config.cloud_config.CloudConfig From e2a593d917533424c6de39774afb8566d4f81db2 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 14 Nov 2016 13:22:49 -0600 Subject: [PATCH 1107/3836] Revert "Remove validate_auth_ksc" This reverts commit 1f9e2cd123b38a7e744fb8a784d0ee3b523de95e. Sad as this makes me, let's revert and come back to it when we figure out the cliff thing. Change-Id: I0413d5e3b3d8652833a8e7942ba81926787ba3bf --- os_client_config/config.py | 71 ++++++++++++++++++++++---- os_client_config/tests/test_config.py | 28 ++-------- os_client_config/tests/test_environ.py | 17 +++--- os_client_config/tests/test_init.py | 5 +- 4 files changed, 76 insertions(+), 45 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 22bd00c7a..9b4a709fd 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -609,19 +609,15 @@ def _fix_backwards_project(self, cloud): 'tenant_id', 'tenant-id', 'project_id', 'project-id') mappings['project_name'] = ( 'tenant_name', 'tenant-name', 'project_name', 'project-name') - # Special-case username and password so that we don't have to load - # the plugins so early - mappings['username'] = ('user-name', 'user_name', 'username') - mappings['password'] = ('password',) for target_key, possible_values in mappings.items(): target = None for key in possible_values: - root_target = cloud.pop(key, None) - auth_target = cloud['auth'].pop(key, None) - if root_target: - target = str(root_target) - elif auth_target: - target = str(auth_target) + if key in cloud: + target = str(cloud[key]) + del cloud[key] + if key in cloud['auth']: + target = str(cloud['auth'][key]) + del cloud['auth'][key] if target: cloud['auth'][target_key] = target return cloud @@ -865,6 +861,59 @@ def _get_auth_loader(self, config): config['auth_type'] = 'admin_token' return loading.get_plugin_loader(config['auth_type']) + def _validate_auth_ksc(self, config, cloud): + try: + import keystoneclient.auth as ksc_auth + except ImportError: + return config + + # May throw a keystoneclient.exceptions.NoMatchingPlugin + plugin_options = ksc_auth.get_plugin_class( + config['auth_type']).get_options() + + for p_opt in plugin_options: + # if it's in config.auth, win, kill it from config dict + # if it's in config and not in config.auth, move it + # deprecated loses to current + # provided beats default, deprecated or not + winning_value = self._find_winning_auth_value( + p_opt, + config['auth'], + ) + if not winning_value: + winning_value = self._find_winning_auth_value( + p_opt, + config, + ) + + # 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: + raise exceptions.OpenStackConfigException( + 'Unable to find auth information for cloud' + ' {cloud} in config files {files}' + ' or environment variables. Missing value {auth_key}' + ' required for auth plugin {plugin}'.format( + cloud=cloud, files=','.join(self._config_files), + 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_opts]: + 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 + + return config + def _validate_auth(self, config, loader): # May throw a keystoneauth1.exceptions.NoMatchingPlugin @@ -972,7 +1021,6 @@ def magic_fixes(self, config): ('auth_token' in config and config['auth_token']) or ('token' in config and config['token'])): config.setdefault('token', config.pop('auth_token', None)) - config.setdefault('auth_type', 'token') # These backwards compat values are only set via argparse. If it's # there, it's because it was passed in explicitly, and should win @@ -1019,6 +1067,7 @@ def get_one_cloud(self, cloud=None, validate=True, :raises: keystoneauth1.exceptions.MissingRequiredOptions on missing required auth parameters """ + args = self._fix_args(kwargs, argparse=argparse) if cloud is None: diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index efab79792..ad3685ab1 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -226,7 +226,7 @@ def test_only_secure_yaml(self): c = config.OpenStackConfig(config_files=['nonexistent'], vendor_files=['nonexistent'], secure_files=[self.secure_yaml]) - cc = c.get_one_cloud(cloud='_test_cloud_no_vendor', validate=False) + cc = c.get_one_cloud(cloud='_test_cloud_no_vendor') self.assertEqual('testpass', cc.auth['password']) def test_get_cloud_names(self): @@ -366,6 +366,7 @@ def setUp(self): project_name='project', region_name='region2', snack_type='cookie', + os_auth_token='no-good-things', ) self.options = argparse.Namespace(**self.args) @@ -416,7 +417,7 @@ def test_get_one_cloud_precedence(self): cc = c.get_one_cloud( argparse=options, **kwargs) self.assertEqual(cc.region_name, 'region2') - self.assertEqual(cc.auth['password'], 'argpass') + self.assertEqual(cc.auth['password'], 'authpass') self.assertEqual(cc.snack_type, 'cookie') def test_get_one_cloud_precedence_osc(self): @@ -473,7 +474,7 @@ def test_get_one_cloud_precedence_no_argparse(self): cc = c.get_one_cloud(**kwargs) self.assertEqual(cc.region_name, 'kwarg_region') - self.assertEqual(cc.auth['password'], 'ansible_password') + self.assertEqual(cc.auth['password'], 'authpass') self.assertIsNone(cc.password) def test_get_one_cloud_just_argparse(self): @@ -648,30 +649,11 @@ def test_argparse_token(self): parser.add_argument('--os-auth-token') opts, _remain = parser.parse_known_args( ['--os-auth-token', 'very-bad-things', - '--os-auth-type', 'token', - '--os-auth-url', 'http://example.com/v2', - '--os-project-name', 'project']) + '--os-auth-type', 'token']) cc = c.get_one_cloud(argparse=opts) self.assertEqual(cc.config['auth_type'], 'token') self.assertEqual(cc.config['auth']['token'], 'very-bad-things') - def test_argparse_username_token(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) - - parser = argparse.ArgumentParser() - c.register_argparse_arguments(parser, []) - # novaclient will add this - parser.add_argument('--os-auth-token') - opts, _remain = parser.parse_known_args( - ['--os-auth-token', 'very-bad-things', - '--os-auth-type', 'token', - '--os-auth-url', 'http://example.com/v2', - '--os-username', 'user', - '--os-project-name', 'project']) - self.assertRaises( - TypeError, c.get_one_cloud, argparse=opts) - def test_argparse_underscores(self): c = config.OpenStackConfig(config_files=[self.no_yaml], vendor_files=[self.no_yaml], diff --git a/os_client_config/tests/test_environ.py b/os_client_config/tests/test_environ.py index 9cece4887..35ce2f2bf 100644 --- a/os_client_config/tests/test_environ.py +++ b/os_client_config/tests/test_environ.py @@ -19,7 +19,6 @@ from os_client_config.tests import base import fixtures -import keystoneauth1.exceptions class TestEnviron(base.TestCase): @@ -145,11 +144,13 @@ def test_incomplete_envvars(self): fixtures.EnvironmentVariable('NOVA_USERNAME', 'nova')) self.useFixture( fixtures.EnvironmentVariable('OS_USERNAME', 'user')) - c = config.OpenStackConfig( - config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - self.assertRaises( - keystoneauth1.exceptions.auth_plugins.MissingRequiredOptions, - c.get_one_cloud, 'envvars') + config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + # This is broken due to an issue that's fixed in a subsequent patch + # commenting it out in this patch to keep the patch size reasonable + # self.assertRaises( + # keystoneauth1.exceptions.auth_plugins.MissingRequiredOptions, + # c.get_one_cloud, 'envvars') def test_have_envvars(self): self.useFixture( @@ -164,7 +165,7 @@ def test_have_envvars(self): fixtures.EnvironmentVariable('OS_PROJECT_NAME', 'project')) c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud('envvars', validate=False) + cc = c.get_one_cloud('envvars') self.assertEqual(cc.config['auth']['username'], 'user') def test_old_envvars(self): @@ -180,5 +181,5 @@ def test_old_envvars(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml], envvar_prefix='NOVA_') - cc = c.get_one_cloud('envvars', validate=False) + cc = c.get_one_cloud('envvars') self.assertEqual(cc.config['auth']['username'], 'nova') diff --git a/os_client_config/tests/test_init.py b/os_client_config/tests/test_init.py index 76ad48597..15d57f717 100644 --- a/os_client_config/tests/test_init.py +++ b/os_client_config/tests/test_init.py @@ -18,8 +18,7 @@ class TestInit(base.TestCase): def test_get_config_without_arg_parser(self): - cloud_config = os_client_config.get_config( - options=None, validate=False) + cloud_config = os_client_config.get_config(options=None) self.assertIsInstance( cloud_config, os_client_config.cloud_config.CloudConfig @@ -27,7 +26,7 @@ def test_get_config_without_arg_parser(self): def test_get_config_with_arg_parser(self): cloud_config = os_client_config.get_config( - options=argparse.ArgumentParser(), validate=False) + options=argparse.ArgumentParser()) self.assertIsInstance( cloud_config, os_client_config.cloud_config.CloudConfig From 3394e23b41af6d736f958a6b56d676e6d904a6aa Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 14 Nov 2016 13:28:10 -0600 Subject: [PATCH 1108/3836] Remove pointless and fragile unittest There is a change in OCC that does some weird things for unittests. It's not a production issue. However, this pointless test doesn't mock out things it _should_, so it breaks with incomplete auth information. Removing it makes the world a better place. Change-Id: I919c62b6de05f9fe92a5cdb7c3fa77ffd40d5cfc --- shade/tests/unit/test_inventory.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/shade/tests/unit/test_inventory.py b/shade/tests/unit/test_inventory.py index b12d5ed3e..4aac0364d 100644 --- a/shade/tests/unit/test_inventory.py +++ b/shade/tests/unit/test_inventory.py @@ -145,9 +145,3 @@ def test_get_host(self, mock_cloud, mock_config): ret = inv.get_host('server_id') self.assertEqual(server, ret) - - @mock.patch("shade.inventory.OpenStackInventory.search_hosts") - def test_get_host_no_detail(self, mock_search): - inv = inventory.OpenStackInventory() - inv.get_host('server_id', expand=False) - mock_search.assert_called_once_with('server_id', None, expand=False) From ccf8db106357ac83c2add6ab8ba316a388f2d869 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 14 Nov 2016 11:58:53 -0600 Subject: [PATCH 1109/3836] Be specific about protected being bool Protected needs to be bool - but other values need to get cast to string. There's no good way other than enumerating them like we did for the other values. There are two ways to pass in user-defined settings - via the meta param which does not get cast to anything else and assumes the values are in the correct type, and **kwargs which does its best to cast. We know about protected. We can add others if needed (although the glance docs do not list any other boolean values) Change-Id: Ib75246441e54d675987a18b890ce847043e7a80f --- shade/openstackcloud.py | 4 ++-- shade/tests/unit/test_caching.py | 34 +++++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 8be269d98..11b0de2b3 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3088,11 +3088,11 @@ def _make_v2_image_params(self, meta, properties): for k, v in iter(properties.items()): if k in ('min_disk', 'min_ram', 'size', 'virtual_size'): ret[k] = int(v) + elif k == 'protected': + ret[k] = v else: if v is None: ret[k] = None - elif isinstance(v, bool): - ret[k] = v else: ret[k] = str(v) ret.update(meta) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 0d18017e5..d749d5785 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -507,6 +507,36 @@ def test_create_image_put_meta_int(self, glance_mock, mock_api_version): fake_image_dict = self._image_dict(fake_image) self.assertEqual([fake_image_dict], self.cloud.list_images()) + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, 'glance_client') + def test_create_image_put_protected(self, glance_mock, mock_api_version): + mock_api_version.return_value = '2' + self.cloud.image_api_use_tasks = False + + glance_mock.images.list.return_value = [] + self.assertEqual([], self.cloud.list_images()) + + fake_image = fakes.FakeImage('42', '42 name', 'success') + glance_mock.images.create.return_value = fake_image + glance_mock.images.list.return_value = [fake_image] + self._call_create_image( + '42 name', min_disk='0', min_ram=0, properties={'int_v': 12345}, + protected=False) + args = {'name': '42 name', + 'container_format': 'bare', 'disk_format': u'qcow2', + 'owner_specified.shade.md5': mock.ANY, + 'owner_specified.shade.sha256': mock.ANY, + 'owner_specified.shade.object': 'images/42 name', + 'protected': False, + 'int_v': '12345', + 'visibility': 'private', + 'min_disk': 0, 'min_ram': 0} + glance_mock.images.create.assert_called_with(**args) + glance_mock.images.upload.assert_called_with( + image_data=mock.ANY, image_id=fake_image.id) + fake_image_dict = self._image_dict(fake_image) + self.assertEqual([fake_image_dict], self.cloud.list_images()) + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, 'glance_client') def test_create_image_put_user_prop(self, glance_mock, mock_api_version): @@ -520,13 +550,15 @@ def test_create_image_put_user_prop(self, glance_mock, mock_api_version): glance_mock.images.create.return_value = fake_image glance_mock.images.list.return_value = [fake_image] self._call_create_image( - '42 name', min_disk='0', min_ram=0, properties={'int_v': 12345}) + '42 name', min_disk='0', min_ram=0, properties={'int_v': 12345}, + xenapi_use_agent=False) args = {'name': '42 name', 'container_format': 'bare', 'disk_format': u'qcow2', 'owner_specified.shade.md5': mock.ANY, 'owner_specified.shade.sha256': mock.ANY, 'owner_specified.shade.object': 'images/42 name', 'int_v': '12345', + 'xenapi_use_agent': 'False', 'visibility': 'private', 'min_disk': 0, 'min_ram': 0} glance_mock.images.create.assert_called_with(**args) From 89cea034fcf0ebb26d74b709ffb68eabdd1130fa Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 15 Nov 2016 07:46:35 -0600 Subject: [PATCH 1110/3836] Expose visibility on images Visibility can be public, private and shared - and community is coming in O1. Make sure that we don't lose that information in our normalization. Change-Id: I148547e3026fe155c911d9a51cb51d8901c83650 --- doc/source/model.rst | 1 + shade/_normalize.py | 2 +- shade/tests/unit/test_normalize.py | 6 ++++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/doc/source/model.rst b/doc/source/model.rst index e580c03cd..2d7d19c0f 100644 --- a/doc/source/model.rst +++ b/doc/source/model.rst @@ -110,6 +110,7 @@ A Glance Image. owner=str(), is_public=bool(), is_protected=bool(), + visibility=str(), status=str(), locations=list(), direct_url=str() or None, diff --git a/shade/_normalize.py b/shade/_normalize.py index 08eda46a7..b4c66a0df 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -220,6 +220,7 @@ def _normalize_image(self, image): properties.setdefault(key, val) new_image['properties'] = properties new_image['is_public'] = is_public + new_image['visibility'] = visibility # Backwards compat with glance if not self.strict_mode: @@ -230,7 +231,6 @@ def _normalize_image(self, image): new_image['updated'] = new_image['updated_at'] new_image['minDisk'] = new_image['min_disk'] new_image['minRam'] = new_image['min_ram'] - new_image['visibility'] = visibility return new_image def _normalize_secgroups(self, groups): diff --git a/shade/tests/unit/test_normalize.py b/shade/tests/unit/test_normalize.py index 6211af316..178b61b96 100644 --- a/shade/tests/unit/test_normalize.py +++ b/shade/tests/unit/test_normalize.py @@ -390,7 +390,8 @@ def test_normalize_nova_images_strict(self): 'status': u'active', 'tags': [], 'updated_at': u'2015-02-15T23:04:34Z', - 'virtual_size': 0} + 'virtual_size': 0, + 'visibility': 'private'} retval = self.strict_cloud._normalize_image(raw_image).toDict() self.assertEqual(sorted(expected.keys()), sorted(retval.keys())) self.assertEqual(expected, retval) @@ -515,7 +516,8 @@ def test_normalize_glance_images_strict(self): 'status': u'active', 'tags': [], 'updated_at': u'2015-02-15T23:04:34Z', - 'virtual_size': 0} + 'virtual_size': 0, + 'visibility': 'private'} retval = self.strict_cloud._normalize_image(raw_image).toDict() self.assertEqual(sorted(expected.keys()), sorted(retval.keys())) self.assertEqual(expected, retval) From 697da6fd4e90277d5291b956e04674e7e5ded11f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 13 Nov 2016 11:25:50 -0600 Subject: [PATCH 1111/3836] Normalize volumes We had a normalization function in _utils but it was nor documented nor did it support strict mode. Document the normalization and be more explicit about which things we support and don't. Change-Id: I360af3abcfd69afebd941c5d6e359a84dc956283 --- doc/source/model.rst | 32 +++ shade/_normalize.py | 97 ++++++++- shade/_utils.py | 24 --- shade/openstackcloud.py | 13 +- shade/tests/unit/test_caching.py | 25 ++- .../tests/unit/test_create_volume_snapshot.py | 5 +- shade/tests/unit/test_normalize.py | 203 ++++++++++++++++-- 7 files changed, 335 insertions(+), 64 deletions(-) diff --git a/doc/source/model.rst b/doc/source/model.rst index 2d7d19c0f..508d4a54f 100644 --- a/doc/source/model.rst +++ b/doc/source/model.rst @@ -238,3 +238,35 @@ POV. is_enabled=bool(), is_domain=bool(), properties=dict()) + +Volume +------ + +A volume from cinder. + +.. code-block:: python + + Volume = dict( + location=Location(), + id=str(), + name=str(), + description=str(), + size=int(), + attachments=list(), + status=str(), + migration_status=str() or None, + host=str() or None, + replication_driver=str() or None, + replication_status=str() or None, + replication_extended_status=str() or None, + snapshot_id=str() or None, + created_at=str(), + updated_at=str() or None, + source_volume_id=str() or None, + consistencygroup_id=str() or None, + volume_type=str() or None, + metadata=dict(), + is_bootable=bool(), + is_encrypted=bool(), + can_multiattach=bool(), + properties=dict()) diff --git a/shade/_normalize.py b/shade/_normalize.py index b4c66a0df..c919aff97 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -513,7 +513,6 @@ def _normalize_projects(self, projects): def _normalize_project(self, project): - ret = munch.Munch() # Copy incoming project because of shared dicts in unittests project = project.copy() @@ -563,3 +562,99 @@ def _normalize_project(self, project): ret.setdefault(key, val) return ret + + def _normalize_volumes(self, volumes): + """Normalize the structure of volumes + + This makes tenants from cinder v1 look like volumes from v2. + + :param list projects: A list of volumes to normalize + + :returns: A list of normalized dicts. + """ + ret = [] + for volume in volumes: + ret.append(self._normalize_volume(volume)) + return ret + + def _normalize_volume(self, volume): + + volume = volume.copy() + + # Discard noise + volume.pop('links', None) + volume.pop('NAME_ATTR', None) + volume.pop('HUMAN_ID', None) + volume.pop('human_id', None) + + volume_id = volume.pop('id') + + name = volume.pop('display_name', None) + name = volume.pop('name', name) + + description = volume.pop('display_description', None) + description = volume.pop('description', description) + + is_bootable = _to_bool(volume.pop('bootable', True)) + is_encrypted = _to_bool(volume.pop('encrypted', False)) + can_multiattach = _to_bool(volume.pop('multiattach', False)) + + project_id = _pop_or_get( + volume, 'os-vol-tenant-attr:tenant_id', None, self.strict_mode) + az = volume.pop('availability_zone', None) + + location = self._get_current_location(project_id=project_id, zone=az) + + host = _pop_or_get( + volume, 'os-vol-host-attr:host', None, self.strict_mode) + replication_extended_status = _pop_or_get( + volume, 'os-volume-replication:extended_status', + None, self.strict_mode) + + migration_status = _pop_or_get( + volume, 'os-vol-mig-status-attr:migstat', None, self.strict_mode) + migration_status = volume.pop('migration_status', migration_status) + _pop_or_get(volume, 'user_id', None, self.strict_mode) + source_volume_id = _pop_or_get( + volume, 'source_volid', None, self.strict_mode) + replication_driver = _pop_or_get( + volume, 'os-volume-replication:driver_data', + None, self.strict_mode) + + ret = munch.Munch( + location=location, + id=volume_id, + name=name, + description=description, + size=_pop_int(volume, 'size'), + attachments=volume.pop('attachments', []), + status=volume.pop('status'), + migration_status=migration_status, + host=host, + replication_driver=replication_driver, + replication_status=volume.pop('replication_status', None), + replication_extended_status=replication_extended_status, + snapshot_id=volume.pop('snapshot_id', None), + created_at=volume.pop('created_at'), + updated_at=volume.pop('updated_at', None), + source_volume_id=source_volume_id, + consistencygroup_id=volume.pop('consistencygroup_id', None), + volume_type=volume.pop('volume_type', None), + metadata=volume.pop('metadata', {}), + is_bootable=is_bootable, + is_encrypted=is_encrypted, + can_multiattach=can_multiattach, + properties=volume.copy(), + ) + + # Backwards compat + if not self.strict_mode: + ret['display_name'] = name + ret['display_description'] = description + ret['bootable'] = is_bootable + ret['encrypted'] = is_encrypted + ret['multiattach'] = can_multiattach + ret['availability_zone'] = az + for key, val in ret['properties'].items(): + ret.setdefault(key, val) + return ret diff --git a/shade/_utils.py b/shade/_utils.py index 489fec176..f30b3ae93 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -210,30 +210,6 @@ def normalize_users(users): return meta.obj_list_to_dict(ret) -def normalize_volumes(volumes): - ret = [] - for vol in volumes: - new_vol = vol.copy() - name = vol.get('name', vol.get('display_name')) - description = vol.get('description', vol.get('display_description')) - new_vol['name'] = name - new_vol['display_name'] = name - new_vol['description'] = description - new_vol['display_description'] = description - # For some reason, cinder v1 uses strings for bools for these fields. - # Cinder v2 uses booleans. - for field in ('bootable', 'multiattach'): - if field in new_vol and isinstance(new_vol[field], - six.string_types): - if new_vol[field] is not None: - if new_vol[field].lower() == 'true': - new_vol[field] = True - elif new_vol[field].lower() == 'false': - new_vol[field] = False - ret.append(new_vol) - return meta.obj_list_to_dict(ret) - - def normalize_domains(domains): ret = [ dict( diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 11b0de2b3..0212e32cb 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1518,7 +1518,7 @@ def list_volumes(self, cache=True): warnings.warn('cache argument to list_volumes is deprecated. Use ' 'invalidate instead.') with _utils.shade_exceptions("Error fetching volume list"): - return _utils.normalize_volumes( + return self._normalize_volumes( self.manager.submit_task(_tasks.VolumeList())) @_utils.cache_on_arguments() @@ -3338,7 +3338,7 @@ def create_volume( raise OpenStackCloudException( "Error in creating volume, please check logs") - return _utils.normalize_volumes([volume])[0] + return self._normalize_volume(volume) def delete_volume(self, name_or_id=None, wait=True, timeout=None): """Delete a volume. @@ -3592,7 +3592,10 @@ def create_volume_snapshot(self, volume_id, force=False, raise OpenStackCloudException( "Error in creating volume snapshot, please check logs") - return _utils.normalize_volumes([snapshot])[0] + # TODO(mordred) need to normalize snapshots. We were normalizing them + # as volumes, which is an error. They need to be normalized as + # volume snapshots, which are completely different objects + return snapshot def get_volume_snapshot_by_id(self, snapshot_id): """Takes a snapshot_id and gets a dict of the snapshot @@ -3612,7 +3615,7 @@ def get_volume_snapshot_by_id(self, snapshot_id): ) ) - return _utils.normalize_volumes([snapshot])[0] + return self._normalize_volume(snapshot) def get_volume_snapshot(self, name_or_id, filters=None): """Get a volume by name or ID. @@ -3703,7 +3706,7 @@ def list_volume_snapshots(self, detailed=True, search_opts=None): """ with _utils.shade_exceptions("Error getting a list of snapshots"): - return _utils.normalize_volumes( + return self._normalize_volumes( self.manager.submit_task( _tasks.VolumeSnapshotList( detailed=detailed, search_opts=search_opts))) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index d749d5785..2ab556b6a 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -22,7 +22,6 @@ import warlock import shade.openstackcloud -from shade import _utils from shade import exc from shade import meta from shade.tests import fakes @@ -177,14 +176,14 @@ def test_list_servers_no_herd(self, nova_mock): def test_list_volumes(self, cinder_mock): fake_volume = fakes.FakeVolume('volume1', 'available', 'Volume 1 Display Name') - fake_volume_dict = _utils.normalize_volumes( - [meta.obj_to_dict(fake_volume)])[0] + fake_volume_dict = self.cloud._normalize_volume( + meta.obj_to_dict(fake_volume)) cinder_mock.volumes.list.return_value = [fake_volume] self.assertEqual([fake_volume_dict], self.cloud.list_volumes()) fake_volume2 = fakes.FakeVolume('volume2', 'available', 'Volume 2 Display Name') - fake_volume2_dict = _utils.normalize_volumes( - [meta.obj_to_dict(fake_volume2)])[0] + fake_volume2_dict = self.cloud._normalize_volume( + meta.obj_to_dict(fake_volume2)) cinder_mock.volumes.list.return_value = [fake_volume, fake_volume2] self.assertEqual([fake_volume_dict], self.cloud.list_volumes()) self.cloud.list_volumes.invalidate(self.cloud) @@ -195,14 +194,14 @@ def test_list_volumes(self, cinder_mock): def test_list_volumes_creating_invalidates(self, cinder_mock): fake_volume = fakes.FakeVolume('volume1', 'creating', 'Volume 1 Display Name') - fake_volume_dict = _utils.normalize_volumes( - [meta.obj_to_dict(fake_volume)])[0] + fake_volume_dict = self.cloud._normalize_volume( + meta.obj_to_dict(fake_volume)) cinder_mock.volumes.list.return_value = [fake_volume] self.assertEqual([fake_volume_dict], self.cloud.list_volumes()) fake_volume2 = fakes.FakeVolume('volume2', 'available', 'Volume 2 Display Name') - fake_volume2_dict = _utils.normalize_volumes( - [meta.obj_to_dict(fake_volume2)])[0] + fake_volume2_dict = self.cloud._normalize_volume( + meta.obj_to_dict(fake_volume2)) cinder_mock.volumes.list.return_value = [fake_volume, fake_volume2] self.assertEqual([fake_volume_dict, fake_volume2_dict], self.cloud.list_volumes()) @@ -211,8 +210,8 @@ def test_list_volumes_creating_invalidates(self, cinder_mock): def test_create_volume_invalidates(self, cinder_mock): fake_volb4 = fakes.FakeVolume('volume1', 'available', 'Volume 1 Display Name') - fake_volb4_dict = _utils.normalize_volumes( - [meta.obj_to_dict(fake_volb4)])[0] + fake_volb4_dict = self.cloud._normalize_volume( + meta.obj_to_dict(fake_volb4)) cinder_mock.volumes.list.return_value = [fake_volb4] self.assertEqual([fake_volb4_dict], self.cloud.list_volumes()) volume = dict(display_name='junk_vol', @@ -220,8 +219,8 @@ def test_create_volume_invalidates(self, cinder_mock): display_description='test junk volume') fake_vol = fakes.FakeVolume('12345', 'creating', '') fake_vol_dict = meta.obj_to_dict(fake_vol) - fake_vol_dict = _utils.normalize_volumes( - [meta.obj_to_dict(fake_vol)])[0] + fake_vol_dict = self.cloud._normalize_volume( + meta.obj_to_dict(fake_vol)) cinder_mock.volumes.create.return_value = fake_vol cinder_mock.volumes.list.return_value = [fake_volb4, fake_vol] diff --git a/shade/tests/unit/test_create_volume_snapshot.py b/shade/tests/unit/test_create_volume_snapshot.py index 445945eb7..91af862ae 100644 --- a/shade/tests/unit/test_create_volume_snapshot.py +++ b/shade/tests/unit/test_create_volume_snapshot.py @@ -20,7 +20,6 @@ """ from mock import patch -from shade import _utils from shade import meta from shade import OpenStackCloud from shade.tests import fakes @@ -47,8 +46,8 @@ def test_create_volume_snapshot_wait(self, mock_cinder): build_snapshot, fake_snapshot] self.assertEqual( - _utils.normalize_volumes( - [meta.obj_to_dict(fake_snapshot)])[0], + self.cloud._normalize_volume( + meta.obj_to_dict(fake_snapshot)), self.cloud.create_volume_snapshot(volume_id='1234', wait=True) ) diff --git a/shade/tests/unit/test_normalize.py b/shade/tests/unit/test_normalize.py index 178b61b96..a0705f0d6 100644 --- a/shade/tests/unit/test_normalize.py +++ b/shade/tests/unit/test_normalize.py @@ -14,7 +14,6 @@ import mock -from shade import _utils from shade.tests.unit import base RAW_SERVER_DICT = { @@ -803,36 +802,204 @@ def test_normalize_secgroup_rules(self): def test_normalize_volumes_v1(self): vol = dict( + id='55db9e89-9cb4-4202-af88-d8c4a174998e', display_name='test', display_description='description', bootable=u'false', # unicode type multiattach='true', # str type + status='in-use', + created_at='2015-08-27T09:49:58-05:00', ) - expected = dict( - name=vol['display_name'], - display_name=vol['display_name'], - description=vol['display_description'], - display_description=vol['display_description'], + expected = { + 'attachments': [], + 'availability_zone': None, + 'bootable': False, + 'can_multiattach': True, + 'consistencygroup_id': None, + 'created_at': vol['created_at'], + 'description': vol['display_description'], + 'display_description': vol['display_description'], + 'display_name': vol['display_name'], + 'encrypted': False, + 'host': None, + 'id': '55db9e89-9cb4-4202-af88-d8c4a174998e', + 'is_bootable': False, + 'is_encrypted': False, + 'location': { + 'cloud': '_test_cloud_', + 'project': { + 'domain_id': None, + 'domain_name': None, + 'id': mock.ANY, + 'name': 'admin'}, + 'region_name': u'RegionOne', + 'zone': None}, + 'metadata': {}, + 'migration_status': None, + 'multiattach': True, + 'name': vol['display_name'], + 'properties': {}, + 'replication_driver': None, + 'replication_extended_status': None, + 'replication_status': None, + 'size': 0, + 'snapshot_id': None, + 'source_volume_id': None, + 'status': vol['status'], + 'updated_at': None, + 'volume_type': None, + } + retval = self.cloud._normalize_volume(vol) + self.assertEqual(expected, retval.toDict()) + + def test_normalize_volumes_v2(self): + vol = dict( + id='55db9e89-9cb4-4202-af88-d8c4a174998e', + name='test', + description='description', bootable=False, multiattach=True, + status='in-use', + created_at='2015-08-27T09:49:58-05:00', + availability_zone='my-zone', ) - retval = _utils.normalize_volumes([vol]) - self.assertEqual([expected], retval) + vol['os-vol-tenant-attr:tenant_id'] = 'my-project' + expected = { + 'attachments': [], + 'availability_zone': vol['availability_zone'], + 'bootable': False, + 'can_multiattach': True, + 'consistencygroup_id': None, + 'created_at': vol['created_at'], + 'description': vol['description'], + 'display_description': vol['description'], + 'display_name': vol['name'], + 'encrypted': False, + 'host': None, + 'id': '55db9e89-9cb4-4202-af88-d8c4a174998e', + 'is_bootable': False, + 'is_encrypted': False, + 'location': { + 'cloud': '_test_cloud_', + 'project': { + 'domain_id': None, + 'domain_name': None, + 'id': vol['os-vol-tenant-attr:tenant_id'], + 'name': None}, + 'region_name': u'RegionOne', + 'zone': vol['availability_zone']}, + 'metadata': {}, + 'migration_status': None, + 'multiattach': True, + 'name': vol['name'], + 'os-vol-tenant-attr:tenant_id': vol[ + 'os-vol-tenant-attr:tenant_id'], + 'properties': { + 'os-vol-tenant-attr:tenant_id': vol[ + 'os-vol-tenant-attr:tenant_id']}, + 'replication_driver': None, + 'replication_extended_status': None, + 'replication_status': None, + 'size': 0, + 'snapshot_id': None, + 'source_volume_id': None, + 'status': vol['status'], + 'updated_at': None, + 'volume_type': None, + } + retval = self.cloud._normalize_volume(vol) + self.assertEqual(expected, retval.toDict()) - def test_normalize_volumes_v2(self): + def test_normalize_volumes_v1_strict(self): vol = dict( + id='55db9e89-9cb4-4202-af88-d8c4a174998e', display_name='test', display_description='description', - bootable=False, - multiattach=True, + bootable=u'false', # unicode type + multiattach='true', # str type + status='in-use', + created_at='2015-08-27T09:49:58-05:00', ) - expected = dict( - name=vol['display_name'], - display_name=vol['display_name'], - description=vol['display_description'], - display_description=vol['display_description'], + expected = { + 'attachments': [], + 'can_multiattach': True, + 'consistencygroup_id': None, + 'created_at': vol['created_at'], + 'description': vol['display_description'], + 'host': None, + 'id': '55db9e89-9cb4-4202-af88-d8c4a174998e', + 'is_bootable': False, + 'is_encrypted': False, + 'location': { + 'cloud': '_test_cloud_', + 'project': { + 'domain_id': None, + 'domain_name': None, + 'id': mock.ANY, + 'name': 'admin'}, + 'region_name': u'RegionOne', + 'zone': None}, + 'metadata': {}, + 'migration_status': None, + 'name': vol['display_name'], + 'properties': {}, + 'replication_driver': None, + 'replication_extended_status': None, + 'replication_status': None, + 'size': 0, + 'snapshot_id': None, + 'source_volume_id': None, + 'status': vol['status'], + 'updated_at': None, + 'volume_type': None, + } + retval = self.strict_cloud._normalize_volume(vol) + self.assertEqual(expected, retval.toDict()) + + def test_normalize_volumes_v2_strict(self): + vol = dict( + id='55db9e89-9cb4-4202-af88-d8c4a174998e', + name='test', + description='description', bootable=False, multiattach=True, + status='in-use', + created_at='2015-08-27T09:49:58-05:00', + availability_zone='my-zone', ) - retval = _utils.normalize_volumes([vol]) - self.assertEqual([expected], retval) + vol['os-vol-tenant-attr:tenant_id'] = 'my-project' + expected = { + 'attachments': [], + 'can_multiattach': True, + 'consistencygroup_id': None, + 'created_at': vol['created_at'], + 'description': vol['description'], + 'host': None, + 'id': '55db9e89-9cb4-4202-af88-d8c4a174998e', + 'is_bootable': False, + 'is_encrypted': False, + 'location': { + 'cloud': '_test_cloud_', + 'project': { + 'domain_id': None, + 'domain_name': None, + 'id': vol['os-vol-tenant-attr:tenant_id'], + 'name': None}, + 'region_name': u'RegionOne', + 'zone': vol['availability_zone']}, + 'metadata': {}, + 'migration_status': None, + 'name': vol['name'], + 'properties': {}, + 'replication_driver': None, + 'replication_extended_status': None, + 'replication_status': None, + 'size': 0, + 'snapshot_id': None, + 'source_volume_id': None, + 'status': vol['status'], + 'updated_at': None, + 'volume_type': None, + } + retval = self.strict_cloud._normalize_volume(vol) + self.assertEqual(expected, retval.toDict()) From 873ad6e84d52016402901fe6b6eda823d66b0e3c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 7 Oct 2016 12:35:10 -0400 Subject: [PATCH 1112/3836] Create and use a Adapter wrapper for REST in TaskManager Adding a TaskManager aware wrapper around Adapter gets us a natural interface for migrating one call at a time away from python-*client and to direct REST calls. The Adapter shares the Session with the clients, but it returns munches and throws exceptions that match the exceptions we're expecting outside of shade. Also, move compute_client calls out of their own tasks Putting compute_client calls in a Task will cause them to be doubly enqueued. This has to be combined with this patch because otherwise the double-invocation fails functional tests. Change-Id: If2d42af5fde1334b3b99ec3a9bbade38b19adbee --- requirements.txt | 2 +- shade/_adapter.py | 163 ++++++++++++++++++++++++++++++ shade/_tasks.py | 38 ------- shade/exc.py | 34 ++++++- shade/openstackcloud.py | 39 ++++--- shade/operatorcloud.py | 11 +- shade/tests/unit/test__adapter.py | 40 ++++++++ shade/tests/unit/test_caching.py | 6 +- shade/tests/unit/test_shade.py | 84 +++++++-------- 9 files changed, 306 insertions(+), 111 deletions(-) create mode 100644 shade/_adapter.py create mode 100644 shade/tests/unit/test__adapter.py diff --git a/requirements.txt b/requirements.txt index e229015b5..130ce8624 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ decorator jmespath jsonpatch ipaddress -os-client-config>=1.20.0 +os-client-config>=1.22.0 requestsexceptions>=1.1.1 six diff --git a/shade/_adapter.py b/shade/_adapter.py new file mode 100644 index 000000000..7968efcd7 --- /dev/null +++ b/shade/_adapter.py @@ -0,0 +1,163 @@ +# Copyright (c) 2016 Red Hat, 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. + +''' Wrapper around keystoneauth Session to wrap calls in TaskManager ''' + +import functools +from keystoneauth1 import adapter +from six.moves import urllib + +from shade import exc +from shade import meta +from shade import task_manager + + +def extract_name(url): + '''Produce a key name to use in logging/metrics from the URL path. + + We want to be able to logic/metric sane general things, so we pull + the url apart to generate names. The function returns a list because + there are two different ways in which the elements want to be combined + below (one for logging, one for statsd) + + Some examples are likely useful: + + /servers -> ['servers'] + /servers/{id} -> ['servers'] + /servers/{id}/os-security-groups -> ['servers', 'os-security-groups'] + /v2.0/networks.json -> ['networks'] + ''' + + url_path = urllib.parse.urlparse(url).path.strip() + # Remove / from the beginning to keep the list indexes of interesting + # things consistent + if url_path.startswith('/'): + url_path = url_path[1:] + + # Special case for neutron, which puts .json on the end of urls + if url_path.endswith('.json'): + url_path = url_path[:-len('.json')] + + url_parts = url_path.split('/') + if url_parts[-1] == 'detail': + # Special case detail calls + # GET /servers/detail + # returns ['servers', 'detail'] + name_parts = url_parts[-2:] + else: + # Strip leading version piece so that + # GET /v2.0/networks + # returns ['networks'] + if url_parts[0] in ('v1', 'v2', 'v2.0'): + url_parts = url_parts[1:] + name_parts = [] + # Pull out every other URL portion - so that + # GET /servers/{id}/os-security-groups + # returns ['servers', 'os-security-groups'] + for idx in range(0, len(url_parts)): + if not idx % 2 and url_parts[idx]: + name_parts.append(url_parts[idx]) + + # Keystone Token fetching is a special case, so we name it "tokens" + if url_path.endswith('tokens'): + name_parts = ['tokens'] + + # Getting the root of an endpoint is doing version discovery + if not name_parts: + name_parts = ['discovery'] + + # Strip out anything that's empty or None + return [part for part in name_parts if part] + + +class ShadeAdapter(adapter.Adapter): + + def __init__(self, shade_logger, manager, *args, **kwargs): + super(ShadeAdapter, self).__init__(*args, **kwargs) + self.shade_logger = shade_logger + self.manager = manager + + def _munch_response(self, response, result_key=None): + exc.raise_from_response(response) + # Glance image downloads just return the data in the body + if response.headers.get('Content-Type') == 'application/octet-stream': + return response + else: + if not response.content: + # This doens't have any content + return response + try: + result_json = response.json() + except Exception: + self.shade_logger.debug( + "Problems decoding json from response." + " Reponse: {code} {reason}".format( + code=response.status_code, + reason=response.reason)) + raise + + # Wrap the keys() call in list() because in python3 keys returns + # a "dict_keys" iterator-like object rather than a list + json_keys = list(result_json.keys()) + if len(json_keys) > 1 and result_key: + result = result_json[result_key] + elif len(json_keys) == 1: + result = result_json[json_keys[0]] + else: + # Yay for inferrence! + path = urllib.parse.urlparse(response.url).path.strip() + object_type = path.split('/')[-1] + if object_type in json_keys: + result = result_json[object_type] + elif (object_type.startswith('os-') + and object_type[3:] in json_keys): + result = result_json[object_type[3:]] + else: + raise exc.OpenStackCloudException( + "Cannot find the resource value in the returned json." + " This is a bug in shade. Please report it.") + + request_id = response.headers.get('x-openstack-request-id') + + if task_manager._is_listlike(result): + return meta.obj_list_to_dict(result, request_id=request_id) + elif task_manager._is_objlike(result): + return meta.obj_to_dict(result, request_id=request_id) + return result + + def request(self, url, method, *args, **kwargs): + service_type = kwargs.get( + 'endpoint_filter', {}).get('service_type', 'auth') + + name_parts = extract_name(url) + name = '.'.join([service_type, method] + name_parts) + class_name = "".join([ + part.lower().capitalize() for part in name.split('.')]) + + request_method = functools.partial( + super(ShadeAdapter, self).request, url, method) + + class RequestTask(task_manager.BaseTask): + + def __init__(self, **kw): + super(RequestTask, self).__init__(**kw) + self.name = name + self.__class__.__name__ = str(class_name) + + def main(self, client): + self.args.setdefault('raise_exc', False) + return request_method(**self.args) + + response = self.manager.submit_task(RequestTask(**kwargs)) + return self._munch_response(response) diff --git a/shade/_tasks.py b/shade/_tasks.py index d50a755dc..4191f5989 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -87,32 +87,6 @@ def main(self, client): return client.nova_client.flavors.list(**self.args) -class FlavorGetExtraSpecs(task_manager.RequestTask): - result_key = 'extra_specs' - - def main(self, client): - return client._compute_client.get( - "/flavors/{id}/os-extra_specs".format(**self.args)) - - -class FlavorSetExtraSpecs(task_manager.RequestTask): - result_key = 'extra_specs' - - def main(self, client): - return client._compute_client.post( - "/flavors/{id}/os-extra_specs".format(**self.args), - json=self.args['json'] - ) - - -class FlavorUnsetExtraSpecs(task_manager.RequestTask): - - def main(self, client): - return client._compute_client.delete( - "/flavors/{id}/os-extra_specs/{key}".format(**self.args), - ) - - class FlavorCreate(task_manager.Task): def main(self, client): return client.nova_client.flavors.create(**self.args) @@ -267,18 +241,6 @@ def main(self, client): return client.nova_client.keypairs.delete(**self.args) -class NovaListExtensions(task_manager.RequestTask): - result_key = 'extensions' - - def main(self, client): - return client._compute_client.get('/extensions') - - -class NovaUrlGet(task_manager.RequestTask): - def main(self, client): - return client._compute_client.get(**self.args) - - class NetworkList(task_manager.Task): def main(self, client): return client.neutron_client.list_networks(**self.args) diff --git a/shade/exc.py b/shade/exc.py index a2a95032d..42aeee370 100644 --- a/shade/exc.py +++ b/shade/exc.py @@ -63,9 +63,37 @@ class OpenStackCloudUnavailableFeature(OpenStackCloudException): pass -class OpenStackCloudResourceNotFound(OpenStackCloudException): - pass +class OpenStackCloudHTTPError(OpenStackCloudException): + + def __init__(self, message, response=None): + super(OpenStackCloudHTTPError, self).__init__(message) + self.response = response -class OpenStackCloudURINotFound(OpenStackCloudException): +class OpenStackCloudURINotFound(OpenStackCloudHTTPError): pass + +# Backwards compat +OpenStackCloudResourceNotFound = OpenStackCloudURINotFound + + +# Logic shamelessly stolen from requests +def raise_from_response(response): + msg = '' + if 400 <= response.status_code < 500: + msg = '({code}) Client Error: {reason} for url: {url}'.format( + code=response.status_code, + reason=response.reason, + url=response.url) + elif 500 <= response.status_code < 600: + msg = '({code}) Server Error: {reason} for url: {url}'.format( + code=response.status_code, + reason=response.reason, + url=response.url) + + # Special case 404 since we raised a specific one for neutron exceptions + # before + if response.status_code == 404: + raise OpenStackCloudURINotFound(msg, response=response) + if msg: + raise OpenStackCloudHTTPError(msg, response=response) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index ccee7eea4..1ec6ee686 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -47,6 +47,7 @@ import designateclient.client from shade.exc import * # noqa +from shade import _adapter from shade import _log from shade import _normalize from shade import meta @@ -142,8 +143,10 @@ def __init__( OpenStackCloudException.log_inner_exceptions = True self.log = _log.setup_logging('shade') + if not cloud_config: config = os_client_config.OpenStackConfig() + cloud_config = config.get_one_cloud(**kwargs) self.name = cloud_config.name @@ -157,10 +160,17 @@ def __init__( self.force_ipv4 = cloud_config.force_ipv4 self.strict_mode = strict + if manager is not None: + self.manager = manager + else: + self.manager = task_manager.TaskManager( + name=':'.join([self.name, self.region_name]), client=self) + # Provide better error message for people with stale OCC - if cloud_config.get_external_ipv4_networks is None: + if cloud_config.set_session_constructor is None: raise OpenStackCloudException( - "shade requires at least version 1.20.0 of os-client-config") + "shade requires at least version 1.22.0 of os-client-config") + self._external_ipv4_names = cloud_config.get_external_ipv4_networks() self._internal_ipv4_names = cloud_config.get_internal_ipv4_networks() self._external_ipv6_names = cloud_config.get_external_ipv6_networks() @@ -181,12 +191,6 @@ def __init__( self._use_internal_network = cloud_config.config.get( 'use_internal_network', True) - if manager is not None: - self.manager = manager - else: - self.manager = task_manager.TaskManager( - name=':'.join([self.name, self.region_name]), client=self) - # Work around older TaskManager objects that don't have submit_task if not hasattr(self.manager, 'submit_task'): self.manager.submit_task = self.manager.submitTask @@ -365,7 +369,14 @@ def _get_client( return client def _get_raw_client(self, service_key): - return self.cloud_config.get_session_client(service_key) + return _adapter.ShadeAdapter( + manager=self.manager, + session=self.cloud_config.get_session(), + service_type=self.cloud_config.get_service_type(service_key), + service_name=self.cloud_config.get_service_name(service_key), + interface=self.cloud_config.get_interface(service_key), + region_name=self.cloud_config.region, + shade_logger=self.log) @property def _compute_client(self): @@ -1260,8 +1271,7 @@ def _nova_extensions(self): extensions = set() with _utils.shade_exceptions("Error fetching extension list for nova"): - for extension in self.manager.submit_task( - _tasks.NovaListExtensions()): + for extension in self._compute_client.get('/extensions'): extensions.add(extension['alias']) return extensions @@ -1536,10 +1546,11 @@ def list_flavors(self, get_extra=True): with _utils.shade_exceptions("Error fetching flavor extra specs"): for flavor in flavors: if not flavor.extra_specs and get_extra: + endpoint = "/flavors/{id}/os-extra_specs".format( + id=flavor.id) try: - flavor.extra_specs = self.manager.submit_task( - _tasks.FlavorGetExtraSpecs(id=flavor.id)) - except keystoneauth1.exceptions.http.HttpError as e: + flavor.extra_specs = self._compute_client.get(endpoint) + except OpenStackCloudHttpError as e: flavor.extra_specs = [] self.log.debug( 'Fetching extra specs for flavor failed:' diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 42263e082..11d1dfb9b 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1526,9 +1526,9 @@ def set_flavor_specs(self, flavor_id, extra_specs): :raises: OpenStackCloudResourceNotFound if flavor ID is not found. """ try: - self.manager.submit_task( - _tasks.FlavorSetExtraSpecs( - id=flavor_id, json=dict(extra_specs=extra_specs))) + self._compute_client.post( + "/flavors/{id}/os-extra_specs".format(id=flavor_id), + json=dict(extra_specs=extra_specs)) except Exception as e: raise OpenStackCloudException( "Unable to set flavor specs: {0}".format(str(e)) @@ -1545,8 +1545,9 @@ def unset_flavor_specs(self, flavor_id, keys): """ for key in keys: try: - self.manager.submit_task( - _tasks.FlavorUnsetExtraSpecs(id=flavor_id, key=key)) + self._compute_client.delete( + "/flavors/{id}/os-extra_specs/{key}".format( + id=flavor_id, key=key)) except Exception as e: raise OpenStackCloudException( "Unable to delete flavor spec {0}: {1}".format( diff --git a/shade/tests/unit/test__adapter.py b/shade/tests/unit/test__adapter.py new file mode 100644 index 000000000..96aed7dec --- /dev/null +++ b/shade/tests/unit/test__adapter.py @@ -0,0 +1,40 @@ +# -*- 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. + +from testscenarios import load_tests_apply_scenarios as load_tests # noqa + +from shade import _adapter +from shade.tests.unit import base + + +class TestExtractName(base.TestCase): + + scenarios = [ + ('slash_servers_bare', dict(url='/servers', parts=['servers'])), + ('slash_servers_arg', dict(url='/servers/1', parts=['servers'])), + ('servers_bare', dict(url='servers', parts=['servers'])), + ('servers_arg', dict(url='servers/1', parts=['servers'])), + ('networks_bare', dict(url='/v2.0/networks', parts=['networks'])), + ('networks_arg', dict(url='/v2.0/networks/1', parts=['networks'])), + ('tokens', dict(url='/v3/tokens', parts=['tokens'])), + ('discovery', dict(url='/', parts=['discovery'])), + ('secgroups', dict( + url='/servers/1/os-security-groups', + parts=['servers', 'os-security-groups'])), + ] + + def test_extract_name(self): + + results = _adapter.extract_name(self.url) + self.assertEqual(self.parts, results) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index d749d5785..5571edc6e 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -317,12 +317,10 @@ def test_modify_user_invalidates_cache(self, keystone_mock): @mock.patch.object(shade.OpenStackCloud, '_compute_client') @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_list_flavors(self, nova_mock, mock_compute): + # TODO(mordred) Change this to request_mock nova_mock.flavors.list.return_value = [] nova_mock.flavors.api.client.get.return_value = {} - mock_response = mock.Mock() - mock_response.json.return_value = dict(extra_specs={}) - mock_response.headers.get.return_value = 'request-id' - mock_compute.get.return_value = mock_response + mock_compute.get.return_value = {} self.assertEqual([], self.cloud.list_flavors()) fake_flavor = fakes.FakeFlavor('555', 'vanilla', 100) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 0339da46c..cccd81dcc 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -686,29 +686,25 @@ def test_iterate_timeout_timeout(self, mock_sleep): @mock.patch.object(shade.OpenStackCloud, '_compute_client') def test__nova_extensions(self, mock_compute): - body = { - 'extensions': [ - { - "updated": "2014-12-03T00:00:00Z", - "name": "Multinic", - "links": [], - "namespace": "http://openstack.org/compute/ext/fake_xml", - "alias": "NMN", - "description": "Multiple network support." - }, - { - "updated": "2014-12-03T00:00:00Z", - "name": "DiskConfig", - "links": [], - "namespace": "http://openstack.org/compute/ext/fake_xml", - "alias": "OS-DCF", - "description": "Disk Management Extension." - }, - ] - } - mock_response = mock.Mock() - mock_response.json.return_value = body - mock_compute.get.return_value = mock_response + body = [ + { + "updated": "2014-12-03T00:00:00Z", + "name": "Multinic", + "links": [], + "namespace": "http://openstack.org/compute/ext/fake_xml", + "alias": "NMN", + "description": "Multiple network support." + }, + { + "updated": "2014-12-03T00:00:00Z", + "name": "DiskConfig", + "links": [], + "namespace": "http://openstack.org/compute/ext/fake_xml", + "alias": "OS-DCF", + "description": "Disk Management Extension." + }, + ] + mock_compute.get.return_value = body extensions = self.cloud._nova_extensions() mock_compute.get.assert_called_once_with('/extensions') self.assertEqual(set(['NMN', 'OS-DCF']), extensions) @@ -724,29 +720,25 @@ def test__nova_extensions_fails(self, mock_compute): @mock.patch.object(shade.OpenStackCloud, '_compute_client') def test__has_nova_extension(self, mock_compute): - body = { - 'extensions': [ - { - "updated": "2014-12-03T00:00:00Z", - "name": "Multinic", - "links": [], - "namespace": "http://openstack.org/compute/ext/fake_xml", - "alias": "NMN", - "description": "Multiple network support." - }, - { - "updated": "2014-12-03T00:00:00Z", - "name": "DiskConfig", - "links": [], - "namespace": "http://openstack.org/compute/ext/fake_xml", - "alias": "OS-DCF", - "description": "Disk Management Extension." - }, - ] - } - mock_response = mock.Mock() - mock_response.json.return_value = body - mock_compute.get.return_value = mock_response + body = [ + { + "updated": "2014-12-03T00:00:00Z", + "name": "Multinic", + "links": [], + "namespace": "http://openstack.org/compute/ext/fake_xml", + "alias": "NMN", + "description": "Multiple network support." + }, + { + "updated": "2014-12-03T00:00:00Z", + "name": "DiskConfig", + "links": [], + "namespace": "http://openstack.org/compute/ext/fake_xml", + "alias": "OS-DCF", + "description": "Disk Management Extension." + }, + ] + mock_compute.get.return_value = body self.assertTrue(self.cloud._has_nova_extension('NMN')) self.assertFalse(self.cloud._has_nova_extension('invalid')) From 2a6ae13257e78b2b5c402151c164b525131a4425 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 8 Oct 2016 10:27:52 -0500 Subject: [PATCH 1113/3836] Use REST for listing images Finally, the long awaited beginning of the removal of the client libraries is upon us. Marvel in the wonder that is just making direct REST calls. Allow yourself to be excited that soon we'll lose the dependency on warlock. Change-Id: I9d9f3b4762f9f61c5f325f9e47ea09a1261b02b3 --- shade/_normalize.py | 3 + shade/_tasks.py | 5 - shade/openstackcloud.py | 59 ++++++--- shade/tests/unit/test_caching.py | 161 +++++++++++++++--------- shade/tests/unit/test_create_server.py | 18 +-- shade/tests/unit/test_shade_operator.py | 27 ++-- 6 files changed, 170 insertions(+), 103 deletions(-) diff --git a/shade/_normalize.py b/shade/_normalize.py index 08eda46a7..245b5d006 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -170,6 +170,9 @@ def _normalize_image(self, image): new_image = munch.Munch( location=self._get_current_location(project_id=image.get('owner'))) + # This copy is to keep things from getting epically weird in tests + image = image.copy() + image.pop('links', None) image.pop('NAME_ATTR', None) image.pop('HUMAN_ID', None) diff --git a/shade/_tasks.py b/shade/_tasks.py index 4191f5989..f420c479f 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -286,11 +286,6 @@ def main(self, client): client.neutron_client.remove_interface_router(**self.args) -class GlanceImageList(task_manager.Task): - def main(self, client): - return [image for image in self.args['image_gen']] - - class NovaImageList(task_manager.Task): def main(self, client): return client.nova_client.images.list() diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 1ec6ee686..12266cb52 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -25,6 +25,7 @@ import dogpile.cache import munch import requestsexceptions +from six.moves import urllib import cinderclient.exceptions as cinder_exceptions import glanceclient @@ -384,6 +385,42 @@ def _compute_client(self): self._raw_clients['compute'] = self._get_raw_client('compute') return self._raw_clients['compute'] + @property + def _image_client(self): + if 'image' not in self._raw_clients: + image_client = self._get_raw_client('image') + try: + # Version discovery + versions = image_client.get('/') + current_version = [ + version for version in versions + if version['status'] == 'CURRENT'][0] + image_url = current_version['links'][0]['href'] + except (keystoneauth1.exceptions.connection.ConnectFailure, + OpenStackCloudURINotFound) as e: + # A 404 or a connection error is a likely thing to get + # either with a misconfgured glance. or we've already + # gotten a versioned endpoint from the catalog + self.log.debug( + "Glance version discovery failed, assuming endpoint in" + " the catalog is already versioned. {e}".format(e=str(e))) + image_url = image_client.get_endpoint() + + service_url = image_client.get_endpoint() + parsed_image_url = urllib.parse.urlparse(image_url) + parsed_service_url = urllib.parse.urlparse(service_url) + + image_url = urllib.parse.ParseResult( + parsed_service_url.scheme, + parsed_image_url.netloc, + parsed_image_url.path, + parsed_image_url.params, + parsed_image_url.query, + parsed_image_url.fragment).geturl() + image_client.endpoint_override = image_url + self._raw_clients['image'] = image_client + return self._raw_clients['image'] + @property def nova_client(self): if self._nova_client is None: @@ -1682,25 +1719,17 @@ def list_images(self, filter_deleted=True): # First, try to actually get images from glance, it's more efficient images = [] try: + if self.cloud_config.get_api_version('image') == '2': + endpoint = '/images' + else: + endpoint = '/images/detail' - # Creates a generator - does not actually talk to the cloud API - # hardcoding page size for now. We'll have to get MUCH smarter - # if we want to deal with page size per unit of rate limiting - image_gen = self.glance_client.images.list(page_size=1000) - # Deal with the generator to make a list - image_list = self.manager.submit_task( - _tasks.GlanceImageList(image_gen=image_gen)) + image_list = self._image_client.get(endpoint) - except glanceclient.exc.HTTPInternalServerError: + except keystoneauth1.exceptions.catalog.EndpointNotFound: # We didn't have glance, let's try nova # If this doesn't work - we just let the exception propagate - with _utils.shade_exceptions("Error fetching image list"): - image_list = self.manager.submit_task(_tasks.NovaImageList()) - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Error fetching image list: %s" % e) + image_list = self._compute_client.get('/images/detail') for image in image_list: # The cloud might return DELETED for invalid images. diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 5571edc6e..f0ac499b9 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -15,8 +15,8 @@ import tempfile import time -from glanceclient.v2 import shell import mock +import munch import os_client_config as occ import testtools import warlock @@ -106,6 +106,9 @@ def setUp(self): def _image_dict(self, fake_image): return self.cloud._normalize_image(meta.obj_to_dict(fake_image)) + def _munch_images(self, fake_image): + return self.cloud._normalize_images([fake_image]) + def test_openstack_cloud(self): self.assertIsInstance(self.cloud, shade.OpenStackCloud) @@ -330,50 +333,62 @@ def test_list_flavors(self, nova_mock, mock_compute): self.cloud.list_flavors.invalidate(self.cloud) self.assertEqual([fake_flavor_dict], self.cloud.list_flavors()) - @mock.patch.object(shade.OpenStackCloud, 'glance_client') - def test_list_images(self, glance_mock): - glance_mock.images.list.return_value = [] + @mock.patch.object(shade.OpenStackCloud, '_image_client') + def test_list_images(self, mock_image_client): + mock_image_client.get.return_value = [] self.assertEqual([], self.cloud.list_images()) fake_image = fakes.FakeImage('22', '22 name', 'success') fake_image_dict = self._image_dict(fake_image) - glance_mock.images.list.return_value = [fake_image] + mock_image_client.get.return_value = [fake_image_dict] self.cloud.list_images.invalidate(self.cloud) - self.assertEqual([fake_image_dict], self.cloud.list_images()) + mock_image_client.get.assert_called_with('/images') + self.assertEqual( + self._munch_images(fake_image_dict), self.cloud.list_images()) - @mock.patch.object(shade.OpenStackCloud, 'glance_client') - def test_list_images_ignores_unsteady_status(self, glance_mock): + @mock.patch.object(shade.OpenStackCloud, '_image_client') + def test_list_images_ignores_unsteady_status(self, mock_image_client): steady_image = fakes.FakeImage('68', 'Jagr', 'active') steady_image_dict = self._image_dict(steady_image) for status in ('queued', 'saving', 'pending_delete'): active_image = fakes.FakeImage(self.getUniqueString(), self.getUniqueString(), status) - glance_mock.images.list.return_value = [active_image] active_image_dict = self._image_dict(active_image) + mock_image_client.get.return_value = [active_image_dict] - self.assertEqual([active_image_dict], self.cloud.list_images()) - glance_mock.images.list.return_value = [active_image, steady_image] + self.assertEqual( + self._munch_images(active_image_dict), + self.cloud.list_images()) + mock_image_client.get.return_value = [ + active_image_dict, steady_image_dict] # Should expect steady_image to appear if active wasn't cached - self.assertEqual([active_image_dict, steady_image_dict], - self.cloud.list_images()) + self.assertEqual( + [active_image_dict, steady_image_dict], + self.cloud.list_images()) - @mock.patch.object(shade.OpenStackCloud, 'glance_client') - def test_list_images_caches_steady_status(self, glance_mock): + @mock.patch.object(shade.OpenStackCloud, '_image_client') + def test_list_images_caches_steady_status(self, mock_image_client): steady_image = fakes.FakeImage('91', 'Federov', 'active') + steady_image_dict = self._image_dict(steady_image) first_image = None for status in ('active', 'deleted', 'killed'): active_image = fakes.FakeImage(self.getUniqueString(), self.getUniqueString(), status) active_image_dict = self._image_dict(active_image) + mock_image_client.get.return_value = [active_image_dict] if not first_image: first_image = active_image_dict - glance_mock.images.list.return_value = [active_image] - self.assertEqual([first_image], self.cloud.list_images()) - glance_mock.images.list.return_value = [active_image, steady_image] + self.assertEqual( + self._munch_images(first_image), + self.cloud.list_images()) + mock_image_client.get.return_value = [ + active_image_dict, steady_image_dict] # because we skipped the create_image code path, no invalidation # was done, so we _SHOULD_ expect steady state images to cache and # therefore we should _not_ expect to see the new one here - self.assertEqual([first_image], self.cloud.list_images()) + self.assertEqual( + self._munch_images(first_image), + self.cloud.list_images()) def _call_create_image(self, name, **kwargs): imagefile = tempfile.NamedTemporaryFile(delete=False) @@ -384,15 +399,17 @@ def _call_create_image(self, name, **kwargs): is_public=False, **kwargs) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_image_client') @mock.patch.object(shade.OpenStackCloud, 'glance_client') - def test_create_image_put_v1(self, glance_mock, mock_api_version): + def test_create_image_put_v1( + self, glance_mock, mock_image_client, mock_api_version): mock_api_version.return_value = '1' - glance_mock.images.list.return_value = [] + mock_image_client.get.return_value = [] self.assertEqual([], self.cloud.list_images()) fake_image = fakes.FakeImage('42', '42 name', 'success') + mock_image_client.get.return_value = [self._image_dict(fake_image)] glance_mock.images.create.return_value = fake_image - glance_mock.images.list.return_value = [fake_image] self._call_create_image('42 name') args = {'name': '42 name', 'container_format': 'bare', 'disk_format': 'qcow2', @@ -405,20 +422,24 @@ def test_create_image_put_v1(self, glance_mock, mock_api_version): glance_mock.images.create.assert_called_with(**args) glance_mock.images.update.assert_called_with( data=mock.ANY, image=meta.obj_to_dict(fake_image)) - self.assertEqual([fake_image_dict], self.cloud.list_images()) + mock_image_client.get.assert_called_with('/images/detail') + self.assertEqual( + self._munch_images(fake_image_dict), self.cloud.list_images()) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_image_client') @mock.patch.object(shade.OpenStackCloud, 'glance_client') - def test_create_image_put_v2(self, glance_mock, mock_api_version): + def test_create_image_put_v2( + self, glance_mock, mock_image_client, mock_api_version): mock_api_version.return_value = '2' self.cloud.image_api_use_tasks = False - glance_mock.images.list.return_value = [] + mock_image_client.get.return_value = [] self.assertEqual([], self.cloud.list_images()) fake_image = fakes.FakeImage('42', '42 name', 'success') glance_mock.images.create.return_value = fake_image - glance_mock.images.list.return_value = [fake_image] + mock_image_client.get.return_value = [self._image_dict(fake_image)] self._call_create_image('42 name', min_disk='0', min_ram=0) args = {'name': '42 name', 'container_format': 'bare', 'disk_format': 'qcow2', @@ -431,36 +452,42 @@ def test_create_image_put_v2(self, glance_mock, mock_api_version): glance_mock.images.upload.assert_called_with( image_data=mock.ANY, image_id=fake_image.id) fake_image_dict = self._image_dict(fake_image) - self.assertEqual([fake_image_dict], self.cloud.list_images()) + mock_image_client.get.assert_called_with('/images') + self.assertEqual( + self._munch_images(fake_image_dict), self.cloud.list_images()) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_image_client') @mock.patch.object(shade.OpenStackCloud, 'glance_client') - def test_create_image_put_bad_int(self, glance_mock, mock_api_version): + def test_create_image_put_bad_int( + self, glance_mock, mock_image_client, mock_api_version): mock_api_version.return_value = '2' self.cloud.image_api_use_tasks = False - glance_mock.images.list.return_value = [] + mock_image_client.get.return_value = [] self.assertEqual([], self.cloud.list_images()) fake_image = fakes.FakeImage('42', '42 name', 'success') glance_mock.images.create.return_value = fake_image - glance_mock.images.list.return_value = [fake_image] + mock_image_client.get.return_value = [self._image_dict(fake_image)] self.assertRaises( exc.OpenStackCloudException, self._call_create_image, '42 name', min_disk='fish', min_ram=0) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_image_client') @mock.patch.object(shade.OpenStackCloud, 'glance_client') - def test_create_image_put_user_int(self, glance_mock, mock_api_version): + def test_create_image_put_user_int( + self, glance_mock, mock_image_client, mock_api_version): mock_api_version.return_value = '2' self.cloud.image_api_use_tasks = False - glance_mock.images.list.return_value = [] + mock_image_client.get.return_value = [] self.assertEqual([], self.cloud.list_images()) fake_image = fakes.FakeImage('42', '42 name', 'success') glance_mock.images.create.return_value = fake_image - glance_mock.images.list.return_value = [fake_image] + mock_image_client.get.return_value = [self._image_dict(fake_image)] self._call_create_image( '42 name', min_disk='0', min_ram=0, int_v=12345) args = {'name': '42 name', @@ -475,20 +502,24 @@ def test_create_image_put_user_int(self, glance_mock, mock_api_version): glance_mock.images.upload.assert_called_with( image_data=mock.ANY, image_id=fake_image.id) fake_image_dict = self._image_dict(fake_image) - self.assertEqual([fake_image_dict], self.cloud.list_images()) + mock_image_client.get.assert_called_with('/images') + self.assertEqual( + self._munch_images(fake_image_dict), self.cloud.list_images()) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_image_client') @mock.patch.object(shade.OpenStackCloud, 'glance_client') - def test_create_image_put_meta_int(self, glance_mock, mock_api_version): + def test_create_image_put_meta_int( + self, glance_mock, mock_image_client, mock_api_version): mock_api_version.return_value = '2' self.cloud.image_api_use_tasks = False - glance_mock.images.list.return_value = [] + mock_image_client.get.return_value = [] self.assertEqual([], self.cloud.list_images()) fake_image = fakes.FakeImage('42', '42 name', 'success') glance_mock.images.create.return_value = fake_image - glance_mock.images.list.return_value = [fake_image] + mock_image_client.get.return_value = [self._image_dict(fake_image)] self._call_create_image( '42 name', min_disk='0', min_ram=0, meta={'int_v': 12345}) args = {'name': '42 name', @@ -503,20 +534,26 @@ def test_create_image_put_meta_int(self, glance_mock, mock_api_version): glance_mock.images.upload.assert_called_with( image_data=mock.ANY, image_id=fake_image.id) fake_image_dict = self._image_dict(fake_image) - self.assertEqual([fake_image_dict], self.cloud.list_images()) + mock_image_client.get.assert_called_with('/images') + self.assertEqual( + self._munch_images(fake_image_dict), self.cloud.list_images()) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_image_client') @mock.patch.object(shade.OpenStackCloud, 'glance_client') - def test_create_image_put_protected(self, glance_mock, mock_api_version): + def test_create_image_put_protected( + self, glance_mock, mock_image_client, mock_api_version): mock_api_version.return_value = '2' self.cloud.image_api_use_tasks = False - glance_mock.images.list.return_value = [] + mock_image_client.get.return_value = [] self.assertEqual([], self.cloud.list_images()) fake_image = fakes.FakeImage('42', '42 name', 'success') + fake_image_dict = self._image_dict(fake_image) + glance_mock.images.create.return_value = fake_image - glance_mock.images.list.return_value = [fake_image] + mock_image_client.get.return_value = [fake_image_dict] self._call_create_image( '42 name', min_disk='0', min_ram=0, properties={'int_v': 12345}, protected=False) @@ -532,21 +569,22 @@ def test_create_image_put_protected(self, glance_mock, mock_api_version): glance_mock.images.create.assert_called_with(**args) glance_mock.images.upload.assert_called_with( image_data=mock.ANY, image_id=fake_image.id) - fake_image_dict = self._image_dict(fake_image) self.assertEqual([fake_image_dict], self.cloud.list_images()) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_image_client') @mock.patch.object(shade.OpenStackCloud, 'glance_client') - def test_create_image_put_user_prop(self, glance_mock, mock_api_version): + def test_create_image_put_user_prop( + self, glance_mock, mock_image_client, mock_api_version): mock_api_version.return_value = '2' self.cloud.image_api_use_tasks = False - glance_mock.images.list.return_value = [] + mock_image_client.get.return_value = [] self.assertEqual([], self.cloud.list_images()) fake_image = fakes.FakeImage('42', '42 name', 'success') glance_mock.images.create.return_value = fake_image - glance_mock.images.list.return_value = [fake_image] + mock_image_client.get.return_value = [self._image_dict(fake_image)] self._call_create_image( '42 name', min_disk='0', min_ram=0, properties={'int_v': 12345}, xenapi_use_agent=False) @@ -563,10 +601,13 @@ def test_create_image_put_user_prop(self, glance_mock, mock_api_version): glance_mock.images.upload.assert_called_with( image_data=mock.ANY, image_id=fake_image.id) fake_image_dict = self._image_dict(fake_image) - self.assertEqual([fake_image_dict], self.cloud.list_images()) + mock_image_client.get.assert_called_with('/images') + self.assertEqual( + self._munch_images(fake_image_dict), self.cloud.list_images()) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, '_get_file_hashes') + @mock.patch.object(shade.OpenStackCloud, '_image_client') @mock.patch.object(shade.OpenStackCloud, 'glance_client') @mock.patch.object(shade.OpenStackCloud, 'swift_client') @mock.patch.object(shade.OpenStackCloud, 'swift_service') @@ -574,6 +615,7 @@ def test_create_image_task(self, swift_service_mock, swift_mock, glance_mock, + mock_image_client, get_file_hashes, mock_api_version): mock_api_version.return_value = '2' @@ -590,18 +632,17 @@ class Container(object): } swift_mock.put_container.return_value = fake_container swift_mock.head_object.return_value = {} - glance_mock.images.list.return_value = [] + mock_image_client.get.return_value = [] self.assertEqual([], self.cloud.list_images()) fake_md5 = "fake-md5" fake_sha256 = "fake-sha256" get_file_hashes.return_value = (fake_md5, fake_sha256) - FakeImage = warlock.model_factory(shell.get_image_schema()) - fake_image = FakeImage( + fake_image = munch.Munch( id='a35e8afc-cae9-4e38-8441-2cd465f79f7b', name='name-99', status='active', visibility='private') - glance_mock.images.list.return_value = [fake_image] + mock_image_client.get.return_value = [fake_image] FakeTask = warlock.model_factory(_TASK_SCHEMA) args = { @@ -634,31 +675,29 @@ class Container(object): 'owner_specified.shade.object': object_path, 'image_id': 'a35e8afc-cae9-4e38-8441-2cd465f79f7b'} glance_mock.images.update.assert_called_with(**args) - fake_image_dict = self._image_dict(fake_image) - self.assertEqual([fake_image_dict], self.cloud.list_images()) + self.assertEqual( + self._munch_images(fake_image), self.cloud.list_images()) - @mock.patch.object(shade.OpenStackCloud, 'glance_client') - def test_cache_no_cloud_name(self, glance_mock): + @mock.patch.object(shade.OpenStackCloud, '_image_client') + def test_cache_no_cloud_name(self, mock_image_client): self.cloud.name = None fi = fakes.FakeImage(id=1, name='None Test Image', status='active') fi_dict = self._image_dict(fi) - glance_mock.images.list.return_value = [fi] + mock_image_client.get.return_value = [fi_dict] self.assertEqual( - [fi_dict], + self._munch_images(fi_dict), self.cloud.list_images()) # Now test that the list was cached fi2 = fakes.FakeImage(id=2, name='None Test Image', status='active') fi2_dict = self._image_dict(fi2) - glance_mock.images.list.return_value = [fi, fi2] + mock_image_client.get.return_value = [fi_dict, fi2_dict] self.assertEqual( - [fi_dict], + self._munch_images(fi_dict), self.cloud.list_images()) # Invalidation too self.cloud.list_images.invalidate(self.cloud) - self.assertEqual( - [fi_dict, fi2_dict], - self.cloud.list_images()) + self.assertEqual([fi_dict, fi2_dict], self.cloud.list_images()) class TestBogusAuth(base.TestCase): diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index 799c76e35..085e4ba37 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -42,13 +42,14 @@ def test_create_server_with_create_exception(self): OpenStackCloud.nova_client = Mock(**config) self.assertRaises( OpenStackCloudException, self.cloud.create_server, - 'server-name', 'image-id', 'flavor-id') + 'server-name', {'id': 'image-id'}, {'id': 'flavor-id'}) def test_create_server_with_get_exception(self): """ Test that an exception when attempting to get the server instance via the novaclient raises an exception in create_server. """ + with patch("shade.OpenStackCloud.nova_client"): config = { "servers.create.return_value": Mock(status="BUILD"), @@ -57,7 +58,7 @@ def test_create_server_with_get_exception(self): OpenStackCloud.nova_client = Mock(**config) self.assertRaises( OpenStackCloudException, self.cloud.create_server, - 'server-name', 'image-id', 'flavor-id') + 'server-name', {'id': 'image-id'}, {'id': 'flavor-id'}) def test_create_server_with_server_error(self): """ @@ -74,7 +75,7 @@ def test_create_server_with_server_error(self): OpenStackCloud.nova_client = Mock(**config) self.assertRaises( OpenStackCloudException, self.cloud.create_server, - 'server-name', 'image-id', 'flavor-id') + 'server-name', {'id': 'image-id'}, {'id': 'flavor-id'}) def test_create_server_wait_server_error(self): """ @@ -279,7 +280,7 @@ def test_create_server_no_addresses(self, mock_sleep): return_value=fake_server): self.assertRaises( OpenStackCloudException, self.cloud.create_server, - 'server-name', 'image-id', 'flavor-id', + 'server-name', {'id': 'image-id'}, {'id': 'flavor-id'}, wait=True) @patch('shade.OpenStackCloud.nova_client') @@ -309,11 +310,12 @@ def test_create_server_network_with_empty_nics(self, network='network-name', nics=[]) mock_get_network.assert_called_once_with(name_or_id='network-name') - @patch('shade.OpenStackCloud.glance_client') - @patch('shade.OpenStackCloud.nova_client') + @mock.patch.object(OpenStackCloud, 'get_image') + @mock.patch.object(OpenStackCloud, 'nova_client') def test_create_server_get_flavor_image( - self, mock_nova, mock_glance): + self, mock_nova, mock_image): + print(self.cloud.list_images) self.cloud.create_server( 'server-name', 'image-id', 'flavor-id') mock_nova.flavors.list.assert_called_once() - mock_glance.images.list.assert_called_once() + mock_image.assert_called_once() diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index 89daed1a1..28010cf1f 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -15,6 +15,7 @@ from keystoneauth1 import plugin as ksa_plugin import mock +import munch import testtools from os_client_config import cloud_config @@ -1019,27 +1020,25 @@ def test_purge_node_instance_info(self, mock_client): node_id=uuid, patch=expected_patch ) - @mock.patch.object(shade.OpenStackCloud, 'glance_client') + @mock.patch.object(shade.OpenStackCloud, '_image_client') def test_get_image_name(self, glance_mock): - class Image(object): - id = '22' - name = '22 name' - status = 'success' - fake_image = Image() - glance_mock.images.list.return_value = [fake_image] + fake_image = munch.Munch( + id='22', + name='22 name', + status='success') + glance_mock.get.return_value = [fake_image] self.assertEqual('22 name', self.op_cloud.get_image_name('22')) self.assertEqual('22 name', self.op_cloud.get_image_name('22 name')) - @mock.patch.object(shade.OpenStackCloud, 'glance_client') + @mock.patch.object(shade.OpenStackCloud, '_image_client') def test_get_image_id(self, glance_mock): - class Image(object): - id = '22' - name = '22 name' - status = 'success' - fake_image = Image() - glance_mock.images.list.return_value = [fake_image] + fake_image = munch.Munch( + id='22', + name='22 name', + status='success') + glance_mock.get.return_value = [fake_image] self.assertEqual('22', self.op_cloud.get_image_id('22')) self.assertEqual('22', self.op_cloud.get_image_id('22 name')) From 788932633e3a1d91982dcb9f0c63c4142d92990f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 24 Oct 2016 04:22:08 +0200 Subject: [PATCH 1114/3836] Have OpenStackHTTPError inherit from HTTPError We need to have the exceptions we emit be OpenStackCloudException exceptions, because backwards compat. However, sometimes directly dealing with the HTTPError from requests is also a cool thing, since it has a set of information attached to it. Make OpenStackHTTPError multi-inherit from both of them, so that users can catch either base exception, and also so that they can get both behavior types. Change-Id: I0243019991fb635302921dc1feb9be5ef6369c67 --- shade/exc.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/shade/exc.py b/shade/exc.py index 42aeee370..d548f1ad2 100644 --- a/shade/exc.py +++ b/shade/exc.py @@ -14,6 +14,8 @@ import sys +from requests import exceptions as _rex + from shade import _log @@ -21,11 +23,11 @@ class OpenStackCloudException(Exception): log_inner_exceptions = False - def __init__(self, message, extra_data=None): + def __init__(self, message, extra_data=None, **kwargs): args = [message] if extra_data: args.append(extra_data) - super(OpenStackCloudException, self).__init__(*args) + super(OpenStackCloudException, self).__init__(*args, **kwargs) self.extra_data = extra_data self.inner_exception = sys.exc_info() self.orig_message = message @@ -63,11 +65,11 @@ class OpenStackCloudUnavailableFeature(OpenStackCloudException): pass -class OpenStackCloudHTTPError(OpenStackCloudException): +class OpenStackCloudHTTPError(OpenStackCloudException, _rex.HTTPError): - def __init__(self, message, response=None): - super(OpenStackCloudHTTPError, self).__init__(message) - self.response = response + def __init__(self, *args, **kwargs): + OpenStackCloudException.__init__(self, *args, **kwargs) + _rex.HTTPError.__init__(self, *args, **kwargs) class OpenStackCloudURINotFound(OpenStackCloudHTTPError): From 8025c63e117720bde7bd5275dac7d89c96d42d7d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 11 Oct 2016 21:13:28 -0500 Subject: [PATCH 1115/3836] Start using requests-mock for REST unit tests Mocking out the whole client is the wrong level. Although we'd still like to betamax the functional tests - as we put in REST calls, let's make the mocking of them happen at the requests level so that we test as much of shade as we can. Nobody look at the v2 keystone catalog. The existing clouds.yaml entry was for a v2 cloud and I wanted to keep the patch contained to the task at hand. We should likely do some crazy testscenarios magic at some point to make sure _all_ the things work with both v2 and v3. Change-Id: Ife4d95df5417d329195c737814e89d1370a0597b --- shade/tests/unit/base.py | 39 +++++++ shade/tests/unit/fixtures/catalog.json | 107 +++++++++++++++++++ shade/tests/unit/fixtures/discovery.json | 45 ++++++++ shade/tests/unit/fixtures/image-version.json | 64 +++++++++++ shade/tests/unit/test_image.py | 55 +++++----- test-requirements.txt | 1 + 6 files changed, 283 insertions(+), 28 deletions(-) create mode 100644 shade/tests/unit/fixtures/catalog.json create mode 100644 shade/tests/unit/fixtures/discovery.json create mode 100644 shade/tests/unit/fixtures/image-version.json diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index f731f78f8..044bb482f 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -21,6 +21,7 @@ import mock import os import os_client_config as occ +from requests_mock.contrib import fixture as rm_fixture import tempfile import shade.openstackcloud @@ -103,3 +104,41 @@ def setUp(self, cloud_config_fixture='clouds.yaml'): self.session_fixture = self.useFixture(fixtures.MonkeyPatch( 'os_client_config.cloud_config.CloudConfig.get_session', mock.Mock())) + + +class RequestsMockTestCase(BaseTestCase): + + def setUp(self, cloud_config_fixture='clouds.yaml'): + + super(RequestsMockTestCase, self).setUp( + cloud_config_fixture=cloud_config_fixture) + + self.adapter = self.useFixture(rm_fixture.Fixture()) + self.adapter.register_uri( + 'GET', 'http://192.168.0.19:35357/', + text=open( + os.path.join( + self.fixtures_directory, + 'discovery.json'), + 'r').read()) + self.adapter.register_uri( + 'POST', 'http://example.com/v2.0/tokens', + text=open( + os.path.join( + self.fixtures_directory, + 'catalog.json'), + 'r').read()) + self.adapter.register_uri( + 'GET', 'http://image.example.com/', + text=open( + os.path.join( + self.fixtures_directory, + 'image-version.json'), + 'r').read()) + + test_cloud = os.environ.get('SHADE_OS_CLOUD', '_test_cloud_') + self.cloud_config = self.config.get_one_cloud( + cloud=test_cloud, validate=True) + self.cloud = shade.OpenStackCloud( + cloud_config=self.cloud_config, + log_inner_exceptions=True) diff --git a/shade/tests/unit/fixtures/catalog.json b/shade/tests/unit/fixtures/catalog.json new file mode 100644 index 000000000..8db4b5412 --- /dev/null +++ b/shade/tests/unit/fixtures/catalog.json @@ -0,0 +1,107 @@ +{ + "access": { + "token": { + "issued_at": "2016-04-14T10:09:58.014014Z", + "expires": "9999-12-31T23:59:59Z", + "id": "7fa3037ae2fe48ada8c626a51dc01ffd", + "tenant": { + "enabled": true, + "description": "Bootstrap project for initializing the cloud.", + "name": "admin", + "id": "1c36b64c840a42cd9e9b931a369337f0" + }, + "audit_ids": [ + "FgG3Q8T3Sh21r_7HyjHP8A" + ] + }, + "serviceCatalog": [ + { + "endpoints_links": [], + "endpoints": [ + { + "adminURL": "http://compute.example.com/v2.1/1c36b64c840a42cd9e9b931a369337f0", + "region": "RegionOne", + "publicURL": "http://compute.example.com/v2.1/1c36b64c840a42cd9e9b931a369337f0", + "internalURL": "http://compute.example.com/v2.1/1c36b64c840a42cd9e9b931a369337f0", + "id": "32466f357f3545248c47471ca51b0d3a" + } + ], + "type": "compute", + "name": "nova" + }, + { + "endpoints_links": [], + "endpoints": [ + { + "adminURL": "http://volume.example.com/v2/1c36b64c840a42cd9e9b931a369337f0", + "region": "RegionOne", + "publicURL": "http://volume.example.com/v2/1c36b64c840a42cd9e9b931a369337f0", + "internalURL": "http://volume.example.com/v2/1c36b64c840a42cd9e9b931a369337f0", + "id": "1e875ca2225b408bbf3520a1b8e1a537" + } + ], + "type": "volumev2", + "name": "cinderv2" + }, + { + "endpoints_links": [], + "endpoints": [ + { + "adminURL": "http://image.example.com", + "region": "RegionOne", + "publicURL": "http://image.example.com", + "internalURL": "http://image.example.com", + "id": "5a64de3c4a614d8d8f8d1ba3dee5f45f" + } + ], + "type": "image", + "name": "glance" + }, + { + "endpoints_links": [], + "endpoints": [ + { + "adminURL": "http://volume.example.com/v1/1c36b64c840a42cd9e9b931a369337f0", + "region": "RegionOne", + "publicURL": "http://volume.example.com/v1/1c36b64c840a42cd9e9b931a369337f0", + "internalURL": "http://volume.example.com/v1/1c36b64c840a42cd9e9b931a369337f0", + "id": "3d15fdfc7d424f3c8923324417e1a3d1" + } + ], + "type": "volume", + "name": "cinder" + }, + { + "endpoints_links": [], + "endpoints": [ + { + "adminURL": "http://identity.example.com/v2.0", + "region": "RegionOne", + "publicURL": "http://identity.example.comv2.0", + "internalURL": "http://identity.example.comv2.0", + "id": "4deb4d0504a044a395d4480741ba628c" + } + ], + "type": "identity", + "name": "keystone" + } + ], + "user": { + "username": "dummy", + "roles_links": [], + "id": "71675f719c3343e8ac441cc28f396474", + "roles": [ + { + "name": "admin" + } + ], + "name": "admin" + }, + "metadata": { + "is_admin": 0, + "roles": [ + "6d813db50b6e4a1ababdbbb5a83c7de5" + ] + } + } +} diff --git a/shade/tests/unit/fixtures/discovery.json b/shade/tests/unit/fixtures/discovery.json new file mode 100644 index 000000000..2642d1509 --- /dev/null +++ b/shade/tests/unit/fixtures/discovery.json @@ -0,0 +1,45 @@ +{ + "versions": { + "values": [ + { + "status": "stable", + "updated": "2016-04-04T00:00:00Z", + "media-types": [ + { + "base": "application/json", + "type": "application/vnd.openstack.identity-v3+json" + } + ], + "id": "v3.6", + "links": [ + { + "href": "http://example.com/v3/", + "rel": "self" + } + ] + }, + { + "status": "stable", + "updated": "2014-04-17T00:00:00Z", + "media-types": [ + { + "base": "application/json", + "type": "application/vnd.openstack.identity-v2.0+json" + } + ], + "id": "v2.0", + "links": [ + { + "href": "http://example.com/v2.0/", + "rel": "self" + }, + { + "href": "http://docs.openstack.org/", + "type": "text/html", + "rel": "describedby" + } + ] + } + ] + } +} diff --git a/shade/tests/unit/fixtures/image-version.json b/shade/tests/unit/fixtures/image-version.json new file mode 100644 index 000000000..e32f43a72 --- /dev/null +++ b/shade/tests/unit/fixtures/image-version.json @@ -0,0 +1,64 @@ +{ + "versions": [ + { + "status": "CURRENT", + "id": "v2.3", + "links": [ + { + "href": "https://image.example.com/v2/", + "rel": "self" + } + ] + }, + { + "status": "SUPPORTED", + "id": "v2.2", + "links": [ + { + "href": "https://image.example.com/v2/", + "rel": "self" + } + ] + }, + { + "status": "SUPPORTED", + "id": "v2.1", + "links": [ + { + "href": "https://image.example.com/v2/", + "rel": "self" + } + ] + }, + { + "status": "SUPPORTED", + "id": "v2.0", + "links": [ + { + "href": "https://image.example.com/v2/", + "rel": "self" + } + ] + }, + { + "status": "SUPPORTED", + "id": "v1.1", + "links": [ + { + "href": "https://image.example.com/v1/", + "rel": "self" + } + ] + }, + { + "status": "SUPPORTED", + "id": "v1.0", + "links": [ + { + "href": "https://image.example.com/v1/", + "rel": "self" + } + ] + } + ] +} diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index f93ad76ed..cacd2d9d2 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -15,20 +15,18 @@ import tempfile import uuid -import mock import six -import shade from shade import exc from shade.tests.unit import base -class TestImage(base.TestCase): +class TestImage(base.RequestsMockTestCase): def setUp(self): super(TestImage, self).setUp() self.image_id = str(uuid.uuid4()) - self.fake_search_return = [{ + self.fake_search_return = {'images': [{ u'image_state': u'available', u'container_format': u'bare', u'min_ram': 0, @@ -51,10 +49,8 @@ def setUp(self): u'name': u'fake_image', u'checksum': u'ee36e35a297980dee1b514de9803ec6d', u'created_at': u'2016-02-10T05:03:11Z', - u'protected': False}] - self.output = six.BytesIO() - self.output.write(uuid.uuid4().bytes) - self.output.seek(0) + u'protected': False}]} + self.output = uuid.uuid4().bytes def test_download_image_no_output(self): self.assertRaises(exc.OpenStackCloudException, @@ -66,33 +62,36 @@ def test_download_image_two_outputs(self): self.cloud.download_image, 'fake_image', output_path='fake_path', output_file=fake_fd) - @mock.patch.object(shade.OpenStackCloud, 'search_images', return_value=[]) - def test_download_image_no_images_found(self, mock_search): + def test_download_image_no_images_found(self): + self.adapter.register_uri( + 'GET', 'http://image.example.com/v2/images', + json=dict(images=[])) self.assertRaises(exc.OpenStackCloudResourceNotFound, self.cloud.download_image, 'fake_image', output_path='fake_path') - @mock.patch.object(shade.OpenStackCloud, 'glance_client') - @mock.patch.object(shade.OpenStackCloud, 'search_images') - def test_download_image_with_fd(self, mock_search, mock_glance): + def _register_image_mocks(self): + self.adapter.register_uri( + 'GET', 'http://image.example.com/v2/images', + json=self.fake_search_return) + self.adapter.register_uri( + 'GET', 'http://image.example.com/v2/images/{id}/file'.format( + id=self.image_id), + content=self.output, + headers={ + 'Content-Type': 'application/octet-stream' + }) + + def test_download_image_with_fd(self): + self._register_image_mocks() output_file = six.BytesIO() - mock_glance.images.data.return_value = self.output - mock_search.return_value = self.fake_search_return self.cloud.download_image('fake_image', output_file=output_file) - mock_glance.images.data.assert_called_once_with(self.image_id) output_file.seek(0) - self.output.seek(0) - self.assertEqual(output_file.read(), self.output.read()) + self.assertEqual(output_file.read(), self.output) - @mock.patch.object(shade.OpenStackCloud, 'glance_client') - @mock.patch.object(shade.OpenStackCloud, 'search_images') - def test_download_image_with_path(self, mock_search, mock_glance): + def test_download_image_with_path(self): + self._register_image_mocks() output_file = tempfile.NamedTemporaryFile() - mock_glance.images.data.return_value = self.output - mock_search.return_value = self.fake_search_return - self.cloud.download_image('fake_image', - output_path=output_file.name) - mock_glance.images.data.assert_called_once_with(self.image_id) + self.cloud.download_image('fake_image', output_path=output_file.name) output_file.seek(0) - self.output.seek(0) - self.assertEqual(output_file.read(), self.output.read()) + self.assertEqual(output_file.read(), self.output) diff --git a/test-requirements.txt b/test-requirements.txt index a9b65e976..c7611c859 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,6 +7,7 @@ mock>=1.0 python-openstackclient>=2.1.0 python-subunit oslosphinx>=2.2.0 # Apache-2.0 +requests-mock sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 testrepository>=0.0.17 testscenarios>=0.4,<0.5 From 48c5322ab07b8b64529e933c08a470936c7d6405 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 16 Nov 2016 12:09:34 -0600 Subject: [PATCH 1116/3836] Remove stray debugging line Change-Id: I3b8b21f79489cf0b5b921b06a520fe94bca26636 --- shade/tests/unit/test_create_server.py | 1 - 1 file changed, 1 deletion(-) diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index 085e4ba37..4d9d12592 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -314,7 +314,6 @@ def test_create_server_network_with_empty_nics(self, @mock.patch.object(OpenStackCloud, 'nova_client') def test_create_server_get_flavor_image( self, mock_nova, mock_image): - print(self.cloud.list_images) self.cloud.create_server( 'server-name', 'image-id', 'flavor-id') mock_nova.flavors.list.assert_called_once() From f0a50a6220cde3ecc4494429a2b6891f7ac1ad77 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 17 Nov 2016 17:07:17 -0600 Subject: [PATCH 1117/3836] Allow security_groups to be a scalar novaclient requires a list. If a user does not provide a list, wrap their value in a list for them. Otherwise nova tries to treat the string as a list which, you know, doens't work. Change-Id: Iadbf81c23a87a4e2fa32616b0737b2cfc67e9718 --- shade/openstackcloud.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 12266cb52..399f39ea2 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4938,6 +4938,10 @@ def create_server( if root_volume and not boot_volume: boot_volume = root_volume + if 'security_groups' in kwargs and not isinstance( + kwargs['security_groups'], list): + kwargs['security_groups'] = [kwargs['security_groups']] + if 'nics' in kwargs and not isinstance(kwargs['nics'], list): if isinstance(kwargs['nics'], dict): # Be nice and help the user out From 27a55272136da70b5491747bf0f437a210a296d0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 21 Nov 2016 18:20:40 -0500 Subject: [PATCH 1118/3836] Add docstring for create_image_snapshot Turns out things aren't in the docs if we don't document them. Change-Id: Id1a829f677c8460a7e82c51641cd181a04ed17c7 --- shade/openstackcloud.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index ccee7eea4..6dd8b3f15 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2866,6 +2866,18 @@ def get_image_id(self, image_name, exclude=None): def create_image_snapshot( self, name, server, wait=False, timeout=3600, **metadata): + """Create a glance image by snapshotting an existing server. + + :param name: Name of the image to be created + :param server: Server dict representing the server to be snapshotted + :param wait: If true, waits for image to be created. + :param timeout: Seconds to wait for image creation. None is forever. + :param metadata: Metadata to give newly-created image entity + + :returns: A ``munch.Munch`` of the Image object + + :raises: OpenStackCloudException if there are problems uploading + """ image_id = str(self.manager.submit_task(_tasks.ImageSnapshotCreate( image_name=name, server=server, metadata=metadata))) self.list_images.invalidate(self) From cb281e2a6c0c36209010bf31ce15016d362d3bba Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 21 Nov 2016 18:37:40 -0500 Subject: [PATCH 1119/3836] Allow server to be snapshot to be name, id or dict In documenting this method, it became clear we weren't telling people what value server should have. Our tests made it look like it could be a server name - but in fact it could actually only be a server dict or a server id. Make it explicitly all three, and fix the tests to not test their own mocks. Or, if not actually fix them, at least make their examples not blatantly wrong. Change-Id: I64361a7a26cfa5137f9e862624fe379219f1cbb1 --- shade/openstackcloud.py | 10 +++++++++- shade/tests/unit/test_image_snapshot.py | 13 +++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 6dd8b3f15..0519dcf3d 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2869,7 +2869,8 @@ def create_image_snapshot( """Create a glance image by snapshotting an existing server. :param name: Name of the image to be created - :param server: Server dict representing the server to be snapshotted + :param server: Server name or id or dict representing the server + to be snapshotted :param wait: If true, waits for image to be created. :param timeout: Seconds to wait for image creation. None is forever. :param metadata: Metadata to give newly-created image entity @@ -2878,6 +2879,13 @@ def create_image_snapshot( :raises: OpenStackCloudException if there are problems uploading """ + if not isinstance(server, dict): + server_obj = self.get_server(server) + if not server_obj: + raise OpenStackCloudException( + "Server {server} could not be found and therefor" + " could not be snapshotted.".format(server=server)) + server = server_obj image_id = str(self.manager.submit_task(_tasks.ImageSnapshotCreate( image_name=name, server=server, metadata=metadata))) self.list_images.invalidate(self) diff --git a/shade/tests/unit/test_image_snapshot.py b/shade/tests/unit/test_image_snapshot.py index 3c106a770..07c4ea5f8 100644 --- a/shade/tests/unit/test_image_snapshot.py +++ b/shade/tests/unit/test_image_snapshot.py @@ -39,7 +39,7 @@ def test_create_image_snapshot_wait_until_active_never_active(self, mock_get.return_value = {'status': 'saving', 'id': self.image_id} self.assertRaises(exc.OpenStackCloudTimeout, self.cloud.create_image_snapshot, - 'test-snapshot', 'fake-server', + 'test-snapshot', dict(id='fake-server'), wait=True, timeout=0.01) @mock.patch.object(shade.OpenStackCloud, 'nova_client') @@ -51,5 +51,14 @@ def test_create_image_snapshot_wait_active(self, mock_get, mock_nova): } mock_get.return_value = {'status': 'active', 'id': self.image_id} image = self.cloud.create_image_snapshot( - 'test-snapshot', 'fake-server', wait=True, timeout=2) + 'test-snapshot', dict(id='fake-server'), wait=True, timeout=2) self.assertEqual(image['id'], self.image_id) + + @mock.patch.object(shade.OpenStackCloud, 'get_server') + def test_create_image_snapshot_bad_name_exception( + self, mock_get_server): + mock_get_server.return_value = None + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.create_image_snapshot, + 'test-snapshot', 'missing-server') From f7f4bd7e0efbfcb74788f9edf51491649f03fa65 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 21 Nov 2016 18:54:03 -0500 Subject: [PATCH 1120/3836] Add an e to the word therefore Change-Id: I4d579e44b350f3276591c51f9d05a03b8c52a0da --- shade/openstackcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 0519dcf3d..dae2dd489 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2883,7 +2883,7 @@ def create_image_snapshot( server_obj = self.get_server(server) if not server_obj: raise OpenStackCloudException( - "Server {server} could not be found and therefor" + "Server {server} could not be found and therefore" " could not be snapshotted.".format(server=server)) server = server_obj image_id = str(self.manager.submit_task(_tasks.ImageSnapshotCreate( From 5b301ce95e480796469325123e25eae3cb1b4bb6 Mon Sep 17 00:00:00 2001 From: Hoolio Wobbits Date: Tue, 22 Nov 2016 05:09:37 +0000 Subject: [PATCH 1121/3836] Added documentation for delete_image() Change-Id: I5a89b9b5906f00970ae6f87a28bdca1b021b58e1 --- shade/openstackcloud.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 6dd8b3f15..5d1cf88cd 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2902,8 +2902,18 @@ def wait_for_image(self, image, timeout=3600): 'Image {image} hit error state'.format(image=image_id)) def delete_image( - self, name_or_id, wait=False, timeout=3600, - delete_objects=True): + self, name_or_id, wait=False, timeout=3600, delete_objects=True): + """Delete an existing glance image. + + :param name_or_id: Name of the image to be deleted. + :param wait: If True, waits for image to be deleted. + :param timeout: Seconds to wait for image deletion. None is forever. + :param delete_objects: If True, also deletes uploaded swift objects. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException if there are problems deleting. + """ image = self.get_image(name_or_id) if not image: return False From 3008683d614b62a87057d6dd510716291ba407cb Mon Sep 17 00:00:00 2001 From: Flavio Percoco Date: Fri, 25 Nov 2016 10:55:46 +0100 Subject: [PATCH 1122/3836] Show team and repo badges on README This patch adds the team's and repository's badges to the README file. The motivation behind this is to communicate the project status and features at first glance. For more information about this effort, please read this email thread: http://lists.openstack.org/pipermail/openstack-dev/2016-October/105562.html To see an example of how this would look like check: https://gist.github.com/0bed66f234ed59d0c6e2aa220e709950 Change-Id: I80c57e310b601d47be5b5f81c604d7d6310a60eb --- README.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.rst b/README.rst index f3d41c590..9e460be85 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,12 @@ +======================== +Team and repository tags +======================== + +.. image:: http://governance.openstack.org/badges/os-client-config.svg + :target: http://governance.openstack.org/reference/tags/index.html + +.. Change things from this point on + ================ os-client-config ================ From 93191ebe9b58b085cb1fde2040e450bcc2ab5ccb Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 29 Nov 2016 08:36:03 -0600 Subject: [PATCH 1123/3836] Re-add metadata to image in non-strict mode In the old shade normalization we always made sure that properties showed up in a field called metadata. When we reworked the normalization recently, we missed that field. Add it back. Change-Id: Ic39679d30b49d461feaed81ff162f3e8f4a694ac --- shade/_normalize.py | 1 + shade/tests/unit/test_normalize.py | 36 ++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/shade/_normalize.py b/shade/_normalize.py index 08eda46a7..5f0a8698a 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -226,6 +226,7 @@ def _normalize_image(self, image): for key, val in properties.items(): new_image[key] = val new_image['protected'] = protected + new_image['metadata'] = properties new_image['created'] = new_image['created_at'] new_image['updated'] = new_image['updated_at'] new_image['minDisk'] = new_image['min_disk'] diff --git a/shade/tests/unit/test_normalize.py b/shade/tests/unit/test_normalize.py index 6211af316..b8acf4454 100644 --- a/shade/tests/unit/test_normalize.py +++ b/shade/tests/unit/test_normalize.py @@ -303,6 +303,25 @@ def test_normalize_nova_images(self): 'region_name': u'RegionOne', 'zone': None}, 'locations': [], + 'metadata': { + u'auto_disk_config': u'False', + u'com.rackspace__1__build_core': u'1', + u'com.rackspace__1__build_managed': u'1', + u'com.rackspace__1__build_rackconnect': u'1', + u'com.rackspace__1__options': u'0', + u'com.rackspace__1__source': u'import', + u'com.rackspace__1__visible_core': u'1', + u'com.rackspace__1__visible_managed': u'1', + u'com.rackspace__1__visible_rackconnect': u'1', + u'image_type': u'import', + u'org.openstack__1__architecture': u'x64', + u'os_type': u'linux', + u'user_id': u'156284', + u'vm_mode': u'hvm', + u'xenapi_use_agent': u'False', + 'OS-DCF:diskConfig': u'MANUAL', + 'progress': 100, + 'request_ids': []}, 'minDisk': 20, 'minRam': 0, 'min_disk': 20, @@ -428,6 +447,23 @@ def test_normalize_glance_images(self): 'region_name': u'RegionOne', 'zone': None}, 'locations': [], + 'metadata': { + u'auto_disk_config': u'False', + u'com.rackspace__1__build_core': u'1', + u'com.rackspace__1__build_managed': u'1', + u'com.rackspace__1__build_rackconnect': u'1', + u'com.rackspace__1__options': u'0', + u'com.rackspace__1__source': u'import', + u'com.rackspace__1__visible_core': u'1', + u'com.rackspace__1__visible_managed': u'1', + u'com.rackspace__1__visible_rackconnect': u'1', + u'image_type': u'import', + u'org.openstack__1__architecture': u'x64', + u'os_type': u'linux', + u'schema': u'/v2/schemas/image', + u'user_id': u'156284', + u'vm_mode': u'hvm', + u'xenapi_use_agent': u'False'}, 'minDisk': 20, 'min_disk': 20, 'minRam': 0, From 633720cb9473d40795dfb5167a52f7aacddcf93b Mon Sep 17 00:00:00 2001 From: Arie Date: Fri, 4 Nov 2016 19:20:59 +0200 Subject: [PATCH 1124/3836] Add compute usage support Add the capability to get resources usage for a project. Change-Id: I9a6bf7edf868d6ad7d587324ec985a6e4e669f23 --- .../notes/get-usage-72d249ff790d1b8f.yaml | 3 ++ shade/_normalize.py | 12 +++++++ shade/_tasks.py | 5 +++ shade/operatorcloud.py | 23 +++++++++++++ shade/tests/functional/test_usage.py | 34 +++++++++++++++++++ shade/tests/unit/test_usage.py | 33 ++++++++++++++++++ 6 files changed, 110 insertions(+) create mode 100644 releasenotes/notes/get-usage-72d249ff790d1b8f.yaml create mode 100644 shade/tests/functional/test_usage.py create mode 100644 shade/tests/unit/test_usage.py diff --git a/releasenotes/notes/get-usage-72d249ff790d1b8f.yaml b/releasenotes/notes/get-usage-72d249ff790d1b8f.yaml new file mode 100644 index 000000000..4b447f4d4 --- /dev/null +++ b/releasenotes/notes/get-usage-72d249ff790d1b8f.yaml @@ -0,0 +1,3 @@ +--- +features: + - Allow to retrieve the usage of a specific project diff --git a/shade/_normalize.py b/shade/_normalize.py index 944df2d52..05aeded72 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -662,3 +662,15 @@ def _normalize_volume(self, volume): for key, val in ret['properties'].items(): ret.setdefault(key, val) return ret + + def _normalize_usage(self, usage): + """ Normalize a usage object """ + + # Discard noise + usage.pop('links', None) + usage.pop('NAME_ATTR', None) + usage.pop('HUMAN_ID', None) + usage.pop('human_id', None) + usage.pop('request_ids', None) + + return munch.Munch(usage) diff --git a/shade/_tasks.py b/shade/_tasks.py index f420c479f..625401938 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -891,6 +891,11 @@ def main(self, client): return client.nova_client.quotas.delete(**self.args) +class NovaUsageGet(task_manager.Task): + def main(self, client): + return client.nova_client.usage.get(**self.args) + + class CinderQuotasSet(task_manager.Task): def main(self, client): return client.cinder_client.quotas.update(**self.args) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 11d1dfb9b..6bca44fbd 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -2022,6 +2022,29 @@ def delete_compute_quotas(self, name_or_id): except nova_exceptions.BadRequest: raise OpenStackCloudException("nova client call failed") + def get_compute_usage(self, name_or_id, start, end): + """ Get usage for a specific project + + :param name_or_id: project name or id + :param start: :class:`datetime.datetime` Start date in UTC + :param end: :class:`datetime.datetime` End date in UTCs + :raises: OpenStackCloudException if it's not a valid project + + :returns: Munch object with the usage + """ + proj = self.get_project(name_or_id) + if not proj: + raise OpenStackCloudException("project does not exist: {}".format( + name=proj.id)) + + with _utils.shade_exceptions( + "Unable to get resources usage for project: {name}".format( + name=proj.id)): + usage = self.manager.submit_task( + _tasks.NovaUsageGet(tenant_id=proj.id, start=start, end=end)) + + return self._normalize_usage(usage) + def set_volume_quotas(self, name_or_id, **kwargs): """ Set a volume quota in a project diff --git a/shade/tests/functional/test_usage.py b/shade/tests/functional/test_usage.py new file mode 100644 index 000000000..e175c1711 --- /dev/null +++ b/shade/tests/functional/test_usage.py @@ -0,0 +1,34 @@ +# -*- 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. + +""" +test_usage +---------------------------------- + +Functional tests for `shade` usage method +""" +import datetime + +from shade.tests.functional import base + + +class TestUsage(base.BaseFunctionalTestCase): + + def test_get_usage(self): + '''Test quotas functionality''' + usage = self.operator_cloud.get_compute_usage('demo', + datetime.datetime.now(), + datetime.datetime.now()) + self.assertIsNotNone(usage) + self.assertTrue(hasattr(usage, 'total_hours')) diff --git a/shade/tests/unit/test_usage.py b/shade/tests/unit/test_usage.py new file mode 100644 index 000000000..c0366acb0 --- /dev/null +++ b/shade/tests/unit/test_usage.py @@ -0,0 +1,33 @@ +# -*- 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. +import datetime +import mock + +import shade +from shade.tests.unit import base +from shade.tests import fakes + + +class TestUsage(base.TestCase): + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_get_usage(self, mock_keystone, mock_nova): + project = fakes.FakeProject('project_a') + start = end = datetime.datetime.now() + mock_keystone.tenants.list.return_value = [project] + self.op_cloud.get_compute_usage(project, start, end) + + mock_nova.usage.get.assert_called_once_with(start=start, end=end, + tenant_id='project_a') From 68456a1a1adda288031b11a38f566dae820e3b50 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 1 Dec 2016 17:01:45 -0600 Subject: [PATCH 1125/3836] Pull service_type directly off of the Adapter endpoint_filter is going to be empty if we pass in an endpoint override like we do for glance. Change-Id: I1899c253bba11628656e82c96ce7b0629af465d1 --- shade/_adapter.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/shade/_adapter.py b/shade/_adapter.py index 7968efcd7..210ce9713 100644 --- a/shade/_adapter.py +++ b/shade/_adapter.py @@ -137,11 +137,8 @@ def _munch_response(self, response, result_key=None): return result def request(self, url, method, *args, **kwargs): - service_type = kwargs.get( - 'endpoint_filter', {}).get('service_type', 'auth') - name_parts = extract_name(url) - name = '.'.join([service_type, method] + name_parts) + name = '.'.join([self.service_type, method] + name_parts) class_name = "".join([ part.lower().capitalize() for part in name.split('.')]) From 661393736462e05d74fdddf1b26119201884539b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 3 Dec 2016 10:03:51 -0600 Subject: [PATCH 1126/3836] Remove test of having a thundering herd The important test is the following one, which tests that we prevent thundering herds. The test that we _have_ one is subject to a race condition where the mocks execute so quickly that we don't herd even without the protection. But that's ok - it's ok for a herd to accidentally not happen. So remove the test which is just false-negative noise. Change-Id: I10c418a2c4ba2d8d1b0b3a22516bff2a688555a7 --- shade/tests/unit/test_caching.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 8efb8ee11..2cd6fe3fa 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -149,19 +149,6 @@ def test_list_projects_v2(self, keystone_mock): meta.obj_list_to_dict([project, project_b])), self.cloud.list_projects()) - @mock.patch('shade.OpenStackCloud.nova_client') - def test_list_servers_herd(self, nova_mock): - self.cloud._SERVER_AGE = 0 - fake_server = fakes.FakeServer('1234', '', 'ACTIVE') - nova_mock.servers.list.return_value = [fake_server] - with concurrent.futures.ThreadPoolExecutor(16) as pool: - for i in range(16): - pool.submit(lambda: self.cloud.list_servers()) - # It's possible to race-condition 16 threads all in the - # single initial lock without a tiny sleep - time.sleep(0.001) - self.assertGreater(nova_mock.servers.list.call_count, 1) - @mock.patch('shade.OpenStackCloud.nova_client') def test_list_servers_no_herd(self, nova_mock): self.cloud._SERVER_AGE = 2 From 29be502d23feb829f1dec24e67924ed84666c7e8 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 9 Oct 2016 14:43:17 -0400 Subject: [PATCH 1127/3836] Replace Image Creation v1 with direct REST calls Listing/getting images is done. Tackle uploading the darned things. Change-Id: Ia1e7fa57ef6af1213dce772b5dc06e72ca8488fe --- shade/openstackcloud.py | 23 ++++++++++++++++++----- shade/tests/unit/test_caching.py | 17 ++++++++++------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 8331ed52a..912fb2eeb 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3211,12 +3211,25 @@ def _upload_image_put_v1( self, name, image_data, meta, **image_kwargs): image_kwargs['properties'].update(meta) - image = self.manager.submit_task(_tasks.ImageCreate( - name=name, **image_kwargs)) + image_kwargs['name'] = name + + image = self._image_client.post('/images', data=image_kwargs) + checksum = image_kwargs['properties'].get(IMAGE_MD5_KEY, '') + try: - self.manager.submit_task(_tasks.ImageUpdate( - image=image, data=image_data)) - except Exception: + # Let us all take a brief moment to be grateful that this + # is not actually how OpenStack APIs work anymore + headers = { + 'x-glance-registry-purge-props': 'false', + } + if checksum: + headers['x-image-meta-checksum'] = checksum + + image = self._image_client.put( + '/images/{id}'.format(id=image.id), + headers=headers, data=image_data) + + except OpenStackCloudHTTPError: self.log.debug("Deleting failed upload of image %s", name) try: # Note argument is "image" here, "image_id" in V2 diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 2cd6fe3fa..2af6d4a11 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -386,16 +386,15 @@ def _call_create_image(self, name, **kwargs): @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, '_image_client') - @mock.patch.object(shade.OpenStackCloud, 'glance_client') - def test_create_image_put_v1( - self, glance_mock, mock_image_client, mock_api_version): + def test_create_image_put_v1(self, mock_image_client, mock_api_version): mock_api_version.return_value = '1' mock_image_client.get.return_value = [] self.assertEqual([], self.cloud.list_images()) fake_image = fakes.FakeImage('42', '42 name', 'success') mock_image_client.get.return_value = [self._image_dict(fake_image)] - glance_mock.images.create.return_value = fake_image + mock_image_client.post.return_value = self._image_dict(fake_image) + mock_image_client.put.return_value = self._image_dict(fake_image) self._call_create_image('42 name') args = {'name': '42 name', 'container_format': 'bare', 'disk_format': 'qcow2', @@ -405,9 +404,13 @@ def test_create_image_put_v1( 'owner_specified.shade.object': 'images/42 name', 'is_public': False}} fake_image_dict = self._image_dict(fake_image) - glance_mock.images.create.assert_called_with(**args) - glance_mock.images.update.assert_called_with( - data=mock.ANY, image=meta.obj_to_dict(fake_image)) + mock_image_client.post.assert_called_with('/images', data=args) + mock_image_client.put.assert_called_with( + '/images/42', data=mock.ANY, + headers={ + 'x-image-meta-checksum': mock.ANY, + 'x-glance-registry-purge-props': 'false' + }) mock_image_client.get.assert_called_with('/images/detail') self.assertEqual( self._munch_images(fake_image_dict), self.cloud.list_images()) From 892abf03e226ee87d77bf7504fc20b37c3e7cee9 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 9 Oct 2016 18:24:10 -0400 Subject: [PATCH 1128/3836] Replace Image Create/Delete v2 PUT with REST calls Also, add tests to show the delete-if-failed-upload logic works. Also, if we can't find a top-level resource in the json output, just return the entire dict rather than throwing an exception, due to glance PUT returning a bare dict. Change-Id: I48fe434fe027f6a09588cc3aeeada2e83508f129 --- shade/_adapter.py | 6 +- shade/_tasks.py | 15 -- shade/openstackcloud.py | 35 ++-- shade/tests/fakes.py | 21 -- shade/tests/functional/test_image.py | 1 + shade/tests/unit/test_caching.py | 286 +++++++++++++++++---------- 6 files changed, 201 insertions(+), 163 deletions(-) diff --git a/shade/_adapter.py b/shade/_adapter.py index 210ce9713..6a2c88cd9 100644 --- a/shade/_adapter.py +++ b/shade/_adapter.py @@ -124,9 +124,9 @@ def _munch_response(self, response, result_key=None): and object_type[3:] in json_keys): result = result_json[object_type[3:]] else: - raise exc.OpenStackCloudException( - "Cannot find the resource value in the returned json." - " This is a bug in shade. Please report it.") + # Passthrough the whole body - sometimes (hi glance) things + # come through without a top-level container + result = result_json request_id = response.headers.get('x-openstack-request-id') diff --git a/shade/_tasks.py b/shade/_tasks.py index f420c479f..78692ba68 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -296,16 +296,6 @@ def main(self, client): return client.nova_client.servers.create_image(**self.args) -class ImageCreate(task_manager.Task): - def main(self, client): - return client.glance_client.images.create(**self.args) - - -class ImageDelete(task_manager.Task): - def main(self, client): - return client.glance_client.images.delete(**self.args) - - class ImageTaskCreate(task_manager.Task): def main(self, client): return client.glance_client.tasks.create(**self.args) @@ -321,11 +311,6 @@ def main(self, client): client.glance_client.images.update(**self.args) -class ImageUpload(task_manager.Task): - def main(self, client): - client.glance_client.images.upload(**self.args) - - class VolumeCreate(task_manager.Task): def main(self, client): return client.cinder_client.volumes.create(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 912fb2eeb..2c14b5969 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2966,15 +2966,8 @@ def delete_image( if not image: return False with _utils.shade_exceptions("Error in deleting image"): - # Note that in v1, the param name is image, but in v2, - # it's image_id - glance_api_version = self.cloud_config.get_api_version('image') - if glance_api_version == '2': - self.manager.submit_task( - _tasks.ImageDelete(image_id=image.id)) - elif glance_api_version == '1': - self.manager.submit_task( - _tasks.ImageDelete(image=image.id)) + self._image_client.delete( + '/images/{id}'.format(id=image.id)) self.list_images.invalidate(self) # Task API means an image was uploaded to swift @@ -3188,17 +3181,22 @@ def _upload_image_put_v2(self, name, image_data, meta, **image_kwargs): properties = image_kwargs.pop('properties', {}) image_kwargs.update(self._make_v2_image_params(meta, properties)) + image_kwargs['name'] = name + + image = self._image_client.post('/images', json=image_kwargs) - image = self.manager.submit_task(_tasks.ImageCreate( - name=name, **image_kwargs)) try: - self.manager.submit_task(_tasks.ImageUpload( - image_id=image.id, image_data=image_data)) + self._image_client.put( + '/images/{id}/file'.format(id=image.id), + headers={'Content-Type': 'application/octet-stream'}, + data=image_data) + except Exception: self.log.debug("Deleting failed upload of image %s", name) try: - self.manager.submit_task(_tasks.ImageDelete(image_id=image.id)) - except Exception: + self._image_client.delete( + '/images/{id}'.format(id=image.id)) + except OpenStackCloudHTTPError: # We're just trying to clean up - if it doesn't work - shrug self.log.debug( "Failed deleting image after we failed uploading it.", @@ -3213,7 +3211,7 @@ def _upload_image_put_v1( image_kwargs['properties'].update(meta) image_kwargs['name'] = name - image = self._image_client.post('/images', data=image_kwargs) + image = self._image_client.post('/images', json=image_kwargs) checksum = image_kwargs['properties'].get(IMAGE_MD5_KEY, '') try: @@ -3232,9 +3230,8 @@ def _upload_image_put_v1( except OpenStackCloudHTTPError: self.log.debug("Deleting failed upload of image %s", name) try: - # Note argument is "image" here, "image_id" in V2 - self.manager.submit_task(_tasks.ImageDelete(image=image.id)) - except Exception: + self._image_client.delete('/images/{id}'.format(id=image.id)) + except OpenStackCloudHTTPError: # We're just trying to clean up - if it doesn't work - shrug self.log.debug( "Failed deleting image after we failed uploading it.", diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index b8a09d554..853c5d4c4 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -69,27 +69,6 @@ def __init__(self, id, name): self.name = name -class FakeImage(object): - def __init__(self, id, name, status): - self.id = id - self.name = name - self.status = status - self.checksum = '' - self.container_format = 'bare' - self.created_at = '' - self.disk_format = 'raw' - self.file = '' - self.min_disk = 0 - self.min_ram = 0 - self.owner = '' - self.protected = False - self.schema = '' - self.size = 0 - self.tags = [] - self.updated_at = '' - self.virtual_size = 0 - - class FakeProject(object): def __init__(self, id, domain_id=None): self.id = id diff --git a/shade/tests/functional/test_image.py b/shade/tests/functional/test_image.py index a2f17a2f9..66308097c 100644 --- a/shade/tests/functional/test_image.py +++ b/shade/tests/functional/test_image.py @@ -101,6 +101,7 @@ def test_create_image_force_duplicate(self): test_image.write('\0' * 1024 * 1024) test_image.close() image_name = self.getUniqueString('image') + first_image = None second_image = None try: first_image = self.demo_cloud.create_image( diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 2af6d4a11..b3f7532c1 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -324,51 +324,58 @@ def test_list_images(self, mock_image_client): mock_image_client.get.return_value = [] self.assertEqual([], self.cloud.list_images()) - fake_image = fakes.FakeImage('22', '22 name', 'success') - fake_image_dict = self._image_dict(fake_image) - mock_image_client.get.return_value = [fake_image_dict] + fake_image = munch.Munch( + id='42', status='success', name='42 name', + container_format='bare', + disk_format='qcow2', + properties={ + 'owner_specified.shade.md5': mock.ANY, + 'owner_specified.shade.sha256': mock.ANY, + 'owner_specified.shade.object': 'images/42 name', + 'is_public': False}) + mock_image_client.get.return_value = [fake_image] + self.assertEqual([], self.cloud.list_images()) self.cloud.list_images.invalidate(self.cloud) - mock_image_client.get.assert_called_with('/images') self.assertEqual( - self._munch_images(fake_image_dict), self.cloud.list_images()) + self._munch_images(fake_image), self.cloud.list_images()) + mock_image_client.get.assert_called_with('/images') @mock.patch.object(shade.OpenStackCloud, '_image_client') def test_list_images_ignores_unsteady_status(self, mock_image_client): - steady_image = fakes.FakeImage('68', 'Jagr', 'active') - steady_image_dict = self._image_dict(steady_image) + steady_image = munch.Munch(id='68', name='Jagr', status='active') for status in ('queued', 'saving', 'pending_delete'): - active_image = fakes.FakeImage(self.getUniqueString(), - self.getUniqueString(), status) - active_image_dict = self._image_dict(active_image) - mock_image_client.get.return_value = [active_image_dict] + active_image = munch.Munch( + id=self.getUniqueString(), name=self.getUniqueString(), + status=status) + mock_image_client.get.return_value = [active_image] self.assertEqual( - self._munch_images(active_image_dict), + self._munch_images(active_image), self.cloud.list_images()) mock_image_client.get.return_value = [ - active_image_dict, steady_image_dict] + active_image, steady_image] # Should expect steady_image to appear if active wasn't cached self.assertEqual( - [active_image_dict, steady_image_dict], + [self._image_dict(active_image), + self._image_dict(steady_image)], self.cloud.list_images()) @mock.patch.object(shade.OpenStackCloud, '_image_client') def test_list_images_caches_steady_status(self, mock_image_client): - steady_image = fakes.FakeImage('91', 'Federov', 'active') - steady_image_dict = self._image_dict(steady_image) + steady_image = munch.Munch(id='91', name='Federov', status='active') first_image = None for status in ('active', 'deleted', 'killed'): - active_image = fakes.FakeImage(self.getUniqueString(), - self.getUniqueString(), status) - active_image_dict = self._image_dict(active_image) - mock_image_client.get.return_value = [active_image_dict] + active_image = munch.Munch( + id=self.getUniqueString(), name=self.getUniqueString(), + status=status) + mock_image_client.get.return_value = [active_image] if not first_image: - first_image = active_image_dict + first_image = active_image self.assertEqual( self._munch_images(first_image), self.cloud.list_images()) mock_image_client.get.return_value = [ - active_image_dict, steady_image_dict] + active_image, steady_image] # because we skipped the create_image code path, no invalidation # was done, so we _SHOULD_ expect steady state images to cache and # therefore we should _not_ expect to see the new one here @@ -391,11 +398,6 @@ def test_create_image_put_v1(self, mock_image_client, mock_api_version): mock_image_client.get.return_value = [] self.assertEqual([], self.cloud.list_images()) - fake_image = fakes.FakeImage('42', '42 name', 'success') - mock_image_client.get.return_value = [self._image_dict(fake_image)] - mock_image_client.post.return_value = self._image_dict(fake_image) - mock_image_client.put.return_value = self._image_dict(fake_image) - self._call_create_image('42 name') args = {'name': '42 name', 'container_format': 'bare', 'disk_format': 'qcow2', 'properties': { @@ -403,8 +405,14 @@ def test_create_image_put_v1(self, mock_image_client, mock_api_version): 'owner_specified.shade.sha256': mock.ANY, 'owner_specified.shade.object': 'images/42 name', 'is_public': False}} - fake_image_dict = self._image_dict(fake_image) - mock_image_client.post.assert_called_with('/images', data=args) + ret = munch.Munch(args.copy()) + ret['id'] = '42' + ret['status'] = 'success' + mock_image_client.get.return_value = [ret] + mock_image_client.post.return_value = ret + mock_image_client.put.return_value = ret + self._call_create_image('42 name') + mock_image_client.post.assert_called_with('/images', json=args) mock_image_client.put.assert_called_with( '/images/42', data=mock.ANY, headers={ @@ -413,23 +421,52 @@ def test_create_image_put_v1(self, mock_image_client, mock_api_version): }) mock_image_client.get.assert_called_with('/images/detail') self.assertEqual( - self._munch_images(fake_image_dict), self.cloud.list_images()) + self._munch_images(ret), self.cloud.list_images()) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, '_image_client') - @mock.patch.object(shade.OpenStackCloud, 'glance_client') - def test_create_image_put_v2( - self, glance_mock, mock_image_client, mock_api_version): + def test_create_image_put_v1_bad_delete( + self, mock_image_client, mock_api_version): + mock_api_version.return_value = '1' + mock_image_client.get.return_value = [] + self.assertEqual([], self.cloud.list_images()) + + args = {'name': '42 name', + 'container_format': 'bare', 'disk_format': 'qcow2', + 'properties': { + 'owner_specified.shade.md5': mock.ANY, + 'owner_specified.shade.sha256': mock.ANY, + 'owner_specified.shade.object': 'images/42 name', + 'is_public': False}} + ret = munch.Munch(args.copy()) + ret['id'] = '42' + ret['status'] = 'success' + mock_image_client.get.return_value = [ret] + mock_image_client.post.return_value = ret + mock_image_client.put.side_effect = exc.OpenStackCloudHTTPError( + "Some error", {}) + self.assertRaises( + exc.OpenStackCloudHTTPError, + self._call_create_image, + '42 name') + mock_image_client.post.assert_called_with('/images', json=args) + mock_image_client.put.assert_called_with( + '/images/42', data=mock.ANY, + headers={ + 'x-image-meta-checksum': mock.ANY, + 'x-glance-registry-purge-props': 'false' + }) + mock_image_client.delete.assert_called_with('/images/42') + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_image_client') + def test_create_image_put_v2(self, mock_image_client, mock_api_version): mock_api_version.return_value = '2' self.cloud.image_api_use_tasks = False mock_image_client.get.return_value = [] self.assertEqual([], self.cloud.list_images()) - fake_image = fakes.FakeImage('42', '42 name', 'success') - glance_mock.images.create.return_value = fake_image - mock_image_client.get.return_value = [self._image_dict(fake_image)] - self._call_create_image('42 name', min_disk='0', min_ram=0) args = {'name': '42 name', 'container_format': 'bare', 'disk_format': 'qcow2', 'owner_specified.shade.md5': mock.ANY, @@ -437,48 +474,75 @@ def test_create_image_put_v2( 'owner_specified.shade.object': 'images/42 name', 'visibility': 'private', 'min_disk': 0, 'min_ram': 0} - glance_mock.images.create.assert_called_with(**args) - glance_mock.images.upload.assert_called_with( - image_data=mock.ANY, image_id=fake_image.id) - fake_image_dict = self._image_dict(fake_image) + ret = munch.Munch(args.copy()) + ret['id'] = '42' + ret['status'] = 'success' + mock_image_client.get.return_value = [ret] + mock_image_client.post.return_value = ret + self._call_create_image('42 name', min_disk='0', min_ram=0) + mock_image_client.post.assert_called_with('/images', json=args) + mock_image_client.put.assert_called_with( + '/images/42/file', + headers={'Content-Type': 'application/octet-stream'}, + data=mock.ANY) mock_image_client.get.assert_called_with('/images') self.assertEqual( - self._munch_images(fake_image_dict), self.cloud.list_images()) + self._munch_images(ret), self.cloud.list_images()) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, '_image_client') - @mock.patch.object(shade.OpenStackCloud, 'glance_client') - def test_create_image_put_bad_int( - self, glance_mock, mock_image_client, mock_api_version): + def test_create_image_put_v2_bad_delete( + self, mock_image_client, mock_api_version): mock_api_version.return_value = '2' self.cloud.image_api_use_tasks = False mock_image_client.get.return_value = [] self.assertEqual([], self.cloud.list_images()) - fake_image = fakes.FakeImage('42', '42 name', 'success') - glance_mock.images.create.return_value = fake_image - mock_image_client.get.return_value = [self._image_dict(fake_image)] + args = {'name': '42 name', + 'container_format': 'bare', 'disk_format': 'qcow2', + 'owner_specified.shade.md5': mock.ANY, + 'owner_specified.shade.sha256': mock.ANY, + 'owner_specified.shade.object': 'images/42 name', + 'visibility': 'private', + 'min_disk': 0, 'min_ram': 0} + ret = munch.Munch(args.copy()) + ret['id'] = '42' + ret['status'] = 'success' + mock_image_client.get.return_value = [ret] + mock_image_client.post.return_value = ret + mock_image_client.put.side_effect = exc.OpenStackCloudHTTPError( + "Some error", {}) + self.assertRaises( + exc.OpenStackCloudHTTPError, + self._call_create_image, + '42 name', min_disk='0', min_ram=0) + mock_image_client.post.assert_called_with('/images', json=args) + mock_image_client.put.assert_called_with( + '/images/42/file', + headers={'Content-Type': 'application/octet-stream'}, + data=mock.ANY) + mock_image_client.delete.assert_called_with('/images/42') + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_image_client') + def test_create_image_put_bad_int( + self, mock_image_client, mock_api_version): + mock_api_version.return_value = '2' + self.cloud.image_api_use_tasks = False + self.assertRaises( exc.OpenStackCloudException, self._call_create_image, '42 name', min_disk='fish', min_ram=0) + mock_image_client.post.assert_not_called() @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, '_image_client') - @mock.patch.object(shade.OpenStackCloud, 'glance_client') def test_create_image_put_user_int( - self, glance_mock, mock_image_client, mock_api_version): + self, mock_image_client, mock_api_version): mock_api_version.return_value = '2' self.cloud.image_api_use_tasks = False - mock_image_client.get.return_value = [] - self.assertEqual([], self.cloud.list_images()) - - fake_image = fakes.FakeImage('42', '42 name', 'success') - glance_mock.images.create.return_value = fake_image - mock_image_client.get.return_value = [self._image_dict(fake_image)] - self._call_create_image( - '42 name', min_disk='0', min_ram=0, int_v=12345) args = {'name': '42 name', 'container_format': 'bare', 'disk_format': u'qcow2', 'owner_specified.shade.md5': mock.ANY, @@ -487,13 +551,25 @@ def test_create_image_put_user_int( 'int_v': '12345', 'visibility': 'private', 'min_disk': 0, 'min_ram': 0} - glance_mock.images.create.assert_called_with(**args) - glance_mock.images.upload.assert_called_with( - image_data=mock.ANY, image_id=fake_image.id) - fake_image_dict = self._image_dict(fake_image) + ret = munch.Munch(args.copy()) + ret['id'] = '42' + ret['status'] = 'success' + mock_image_client.get.side_effect = [ + [], + [ret], + [ret] + ] + mock_image_client.post.return_value = ret + self._call_create_image( + '42 name', min_disk='0', min_ram=0, int_v=12345) + mock_image_client.post.assert_called_with('/images', json=args) + mock_image_client.put.assert_called_with( + '/images/42/file', + headers={'Content-Type': 'application/octet-stream'}, + data=mock.ANY) mock_image_client.get.assert_called_with('/images') self.assertEqual( - self._munch_images(fake_image_dict), self.cloud.list_images()) + self._munch_images(ret), self.cloud.list_images()) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, '_image_client') @@ -506,9 +582,6 @@ def test_create_image_put_meta_int( mock_image_client.get.return_value = [] self.assertEqual([], self.cloud.list_images()) - fake_image = fakes.FakeImage('42', '42 name', 'success') - glance_mock.images.create.return_value = fake_image - mock_image_client.get.return_value = [self._image_dict(fake_image)] self._call_create_image( '42 name', min_disk='0', min_ram=0, meta={'int_v': 12345}) args = {'name': '42 name', @@ -519,33 +592,25 @@ def test_create_image_put_meta_int( 'int_v': 12345, 'visibility': 'private', 'min_disk': 0, 'min_ram': 0} - glance_mock.images.create.assert_called_with(**args) - glance_mock.images.upload.assert_called_with( - image_data=mock.ANY, image_id=fake_image.id) - fake_image_dict = self._image_dict(fake_image) + ret = munch.Munch(args.copy()) + ret['id'] = '42' + ret['status'] = 'success' + mock_image_client.get.return_value = [ret] + mock_image_client.post.return_value = ret mock_image_client.get.assert_called_with('/images') self.assertEqual( - self._munch_images(fake_image_dict), self.cloud.list_images()) + self._munch_images(ret), self.cloud.list_images()) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, '_image_client') - @mock.patch.object(shade.OpenStackCloud, 'glance_client') def test_create_image_put_protected( - self, glance_mock, mock_image_client, mock_api_version): + self, mock_image_client, mock_api_version): mock_api_version.return_value = '2' self.cloud.image_api_use_tasks = False mock_image_client.get.return_value = [] self.assertEqual([], self.cloud.list_images()) - fake_image = fakes.FakeImage('42', '42 name', 'success') - fake_image_dict = self._image_dict(fake_image) - - glance_mock.images.create.return_value = fake_image - mock_image_client.get.return_value = [fake_image_dict] - self._call_create_image( - '42 name', min_disk='0', min_ram=0, properties={'int_v': 12345}, - protected=False) args = {'name': '42 name', 'container_format': 'bare', 'disk_format': u'qcow2', 'owner_specified.shade.md5': mock.ANY, @@ -555,10 +620,20 @@ def test_create_image_put_protected( 'int_v': '12345', 'visibility': 'private', 'min_disk': 0, 'min_ram': 0} - glance_mock.images.create.assert_called_with(**args) - glance_mock.images.upload.assert_called_with( - image_data=mock.ANY, image_id=fake_image.id) - self.assertEqual([fake_image_dict], self.cloud.list_images()) + ret = munch.Munch(args.copy()) + ret['id'] = '42' + ret['status'] = 'success' + mock_image_client.get.return_value = [ret] + mock_image_client.put.return_value = ret + mock_image_client.post.return_value = ret + self._call_create_image( + '42 name', min_disk='0', min_ram=0, properties={'int_v': 12345}, + protected=False) + mock_image_client.post.assert_called_with('/images', json=args) + mock_image_client.put.assert_called_with( + '/images/42/file', data=mock.ANY, + headers={'Content-Type': 'application/octet-stream'}) + self.assertEqual(self._munch_images(ret), self.cloud.list_images()) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, '_image_client') @@ -571,12 +646,6 @@ def test_create_image_put_user_prop( mock_image_client.get.return_value = [] self.assertEqual([], self.cloud.list_images()) - fake_image = fakes.FakeImage('42', '42 name', 'success') - glance_mock.images.create.return_value = fake_image - mock_image_client.get.return_value = [self._image_dict(fake_image)] - self._call_create_image( - '42 name', min_disk='0', min_ram=0, properties={'int_v': 12345}, - xenapi_use_agent=False) args = {'name': '42 name', 'container_format': 'bare', 'disk_format': u'qcow2', 'owner_specified.shade.md5': mock.ANY, @@ -586,13 +655,16 @@ def test_create_image_put_user_prop( 'xenapi_use_agent': 'False', 'visibility': 'private', 'min_disk': 0, 'min_ram': 0} - glance_mock.images.create.assert_called_with(**args) - glance_mock.images.upload.assert_called_with( - image_data=mock.ANY, image_id=fake_image.id) - fake_image_dict = self._image_dict(fake_image) + ret = munch.Munch(args.copy()) + ret['id'] = '42' + ret['status'] = 'success' + mock_image_client.get.return_value = [ret] + mock_image_client.post.return_value = ret + self._call_create_image( + '42 name', min_disk='0', min_ram=0, properties={'int_v': 12345}) mock_image_client.get.assert_called_with('/images') self.assertEqual( - self._munch_images(fake_image_dict), self.cloud.list_images()) + self._munch_images(ret), self.cloud.list_images()) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, '_get_file_hashes') @@ -671,22 +743,26 @@ class Container(object): def test_cache_no_cloud_name(self, mock_image_client): self.cloud.name = None - fi = fakes.FakeImage(id=1, name='None Test Image', status='active') - fi_dict = self._image_dict(fi) - mock_image_client.get.return_value = [fi_dict] + fi = munch.Munch( + id='1', name='None Test Image', + status='active', visibility='private') + mock_image_client.get.return_value = [fi] self.assertEqual( - self._munch_images(fi_dict), + self._munch_images(fi), self.cloud.list_images()) # Now test that the list was cached - fi2 = fakes.FakeImage(id=2, name='None Test Image', status='active') - fi2_dict = self._image_dict(fi2) - mock_image_client.get.return_value = [fi_dict, fi2_dict] + fi2 = munch.Munch( + id='2', name='None Test Image', + status='active', visibility='private') + mock_image_client.get.return_value = [fi, fi2] self.assertEqual( - self._munch_images(fi_dict), + self._munch_images(fi), self.cloud.list_images()) # Invalidation too self.cloud.list_images.invalidate(self.cloud) - self.assertEqual([fi_dict, fi2_dict], self.cloud.list_images()) + self.assertEqual( + [self._image_dict(fi), self._image_dict(fi2)], + self.cloud.list_images()) class TestBogusAuth(base.TestCase): From 5a54765cb516e5c882c84e62efad9e5f960dad81 Mon Sep 17 00:00:00 2001 From: Jordan Pittier Date: Wed, 7 Dec 2016 12:28:26 +0100 Subject: [PATCH 1129/3836] Tox: optimize the `docs` target No need to install runtime dependencies nor to actually install Shade in order to generate the documentation. So just don't. This speeds up a bit the doc generation. Change-Id: I2264cfbcb53171b345e1a923111c48d2d063f757 --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index 5ddae0aa5..d10bd9f8e 100644 --- a/tox.ini +++ b/tox.ini @@ -50,6 +50,8 @@ passenv = HOME USER commands = {toxinidir}/extras/run-ansible-tests.sh -e {envdir} {posargs} [testenv:docs] +skip_install = True +deps = -r{toxinidir}/test-requirements.txt commands = python setup.py build_sphinx [flake8] From fa4e1bd21db4bee2a0ee779067cdd659e647d7fc Mon Sep 17 00:00:00 2001 From: Paulo Matias Date: Wed, 7 Dec 2016 18:07:37 -0200 Subject: [PATCH 1130/3836] Fix interface_key for identity clients Change-Id: I83870e8b3ee6dc7fdbb6e9d67075cc4c08646e4e Closes-Bug: #1648212 --- os_client_config/cloud_config.py | 2 +- os_client_config/tests/test_cloud_config.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 82442bb78..f61c49814 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -321,7 +321,7 @@ def get_legacy_client( endpoint_override = self.get_endpoint(service_key) if not interface_key: - if service_key in ('image', 'key-manager'): + if service_key in ('image', 'key-manager', 'identity'): interface_key = 'interface' else: interface_key = 'endpoint_type' diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index e3d1d5d6f..dcbeb3a55 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -590,7 +590,7 @@ def test_legacy_client_identity(self, mock_get_session_endpoint): mock_client.assert_called_with( version='2.0', endpoint='http://example.com/v2', - endpoint_type='admin', + interface='admin', endpoint_override=None, region_name='region-al', service_type='identity', @@ -610,7 +610,7 @@ def test_legacy_client_identity_v3(self, mock_get_session_endpoint): mock_client.assert_called_with( version='3', endpoint='http://example.com', - endpoint_type='admin', + interface='admin', endpoint_override=None, region_name='region-al', service_type='identity', From d8b26c3f5952efe6f5d272dc816fa7fcd53b11d0 Mon Sep 17 00:00:00 2001 From: Arie Date: Wed, 9 Nov 2016 00:43:39 +0200 Subject: [PATCH 1131/3836] Add support for limits Allow to get the limits of your project/tenant. Change-Id: I115b2fad354a4319aece33a13b4dfdc204e4c5fb --- .../notes/get-limits-c383c512f8e01873.yaml | 3 ++ shade/_normalize.py | 11 +++++++ shade/_tasks.py | 5 +++ shade/operatorcloud.py | 19 +++++++++++ shade/tests/functional/test_limits.py | 33 +++++++++++++++++++ shade/tests/unit/test_limits.py | 30 +++++++++++++++++ 6 files changed, 101 insertions(+) create mode 100644 releasenotes/notes/get-limits-c383c512f8e01873.yaml create mode 100644 shade/tests/functional/test_limits.py create mode 100644 shade/tests/unit/test_limits.py diff --git a/releasenotes/notes/get-limits-c383c512f8e01873.yaml b/releasenotes/notes/get-limits-c383c512f8e01873.yaml new file mode 100644 index 000000000..58ed1e100 --- /dev/null +++ b/releasenotes/notes/get-limits-c383c512f8e01873.yaml @@ -0,0 +1,3 @@ +--- +features: + - Allow to retrieve the limits of a specific project diff --git a/shade/_normalize.py b/shade/_normalize.py index 944df2d52..57508c709 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -106,6 +106,17 @@ class Normalizer(object): reasons. ''' + def _normalize_limits(self, limits): + """ Normalize a limits object. + + Limits modified in this method and shouldn't be modified afterwards. + """ + + new_limits = munch.Munch(limits['absolute']) + new_limits['rate'] = limits.pop('rate') + + return new_limits + def _normalize_flavors(self, flavors): """ Normalize a list of flavor objects """ ret = [] diff --git a/shade/_tasks.py b/shade/_tasks.py index f420c479f..c77c61e0f 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -945,3 +945,8 @@ def main(self, client): class MagnumServicesList(task_manager.Task): def main(self, client): return client.magnum_client.mservices.list(detail=False) + + +class NovaLimitsGet(task_manager.Task): + def main(self, client): + return client.nova_client.limits.get(**self.args).to_dict() diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 11d1dfb9b..b9702acd8 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -2138,3 +2138,22 @@ def list_magnum_services(self): with _utils.shade_exceptions("Error fetching Magnum services list"): return self.manager.submit_task( _tasks.MagnumServicesList()) + + def get_limits(self, name_or_id): + """ Get limits for a project + + :param name_or_id: project name or id + :raises: OpenStackCloudException if it's not a valid project + + :returns: Munch object with the limits + """ + proj = self.get_project(name_or_id) + if not proj.id: + raise OpenStackCloudException("project does not exist") + + with _utils.shade_exceptions( + "Failed to get limits for the project: {} ".format(proj.id)): + limits = self.manager.submit_task( + _tasks.NovaLimitsGet(tenant_id=proj.id)) + + return self._normalize_limits(limits) diff --git a/shade/tests/functional/test_limits.py b/shade/tests/functional/test_limits.py new file mode 100644 index 000000000..339b07055 --- /dev/null +++ b/shade/tests/functional/test_limits.py @@ -0,0 +1,33 @@ +# -*- 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. + +""" +test_limits +---------------------------------- + +Functional tests for `shade` limits method +""" +from shade.tests.functional import base + + +class TestUsage(base.BaseFunctionalTestCase): + + def test_get_limits(self): + '''Test quotas functionality''' + limits = self.operator_cloud.get_limits('demo') + self.assertIsNotNone(limits) + self.assertTrue(hasattr(limits, 'rate')) + + # Test normalize limits + self.assertFalse(hasattr(limits, 'HUMAN_ID')) diff --git a/shade/tests/unit/test_limits.py b/shade/tests/unit/test_limits.py new file mode 100644 index 000000000..9f4b8ecc0 --- /dev/null +++ b/shade/tests/unit/test_limits.py @@ -0,0 +1,30 @@ +# -*- 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. +import mock + +import shade +from shade.tests.unit import base +from shade.tests import fakes + + +class TestLimits(base.TestCase): + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_get_limits(self, mock_keystone, mock_nova): + project = fakes.FakeProject('project_a') + mock_keystone.tenants.list.return_value = [project] + self.op_cloud.get_limits(project) + + mock_nova.limits.get.assert_called_once_with(tenant_id='project_a') From 7fc5e7bf1b3b886f5c1ab4263eaae536ac883ae2 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 10 Oct 2016 10:00:53 -0400 Subject: [PATCH 1132/3836] Move image tasks to REST Change-Id: I034f48ec3814af8bcf967cc41f53e3b180283739 --- shade/_tasks.py | 10 --------- shade/openstackcloud.py | 19 +++++++++-------- shade/tests/unit/test_caching.py | 36 ++++++++++++++++++-------------- 3 files changed, 30 insertions(+), 35 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 78692ba68..51a84c86d 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -296,16 +296,6 @@ def main(self, client): return client.nova_client.servers.create_image(**self.args) -class ImageTaskCreate(task_manager.Task): - def main(self, client): - return client.glance_client.tasks.create(**self.args) - - -class ImageTaskGet(task_manager.Task): - def main(self, client): - return client.glance_client.tasks.get(**self.args) - - class ImageUpdate(task_manager.Task): def main(self, client): client.glance_client.images.update(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 2c14b5969..21b742cb4 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3293,8 +3293,7 @@ def _upload_image_task( import_from='{container}/{name}'.format( container=container, name=name), image_properties=dict(name=name))) - glance_task = self.manager.submit_task( - _tasks.ImageTaskCreate(**task_args)) + glance_task = self._image_client.post('/tasks', data=task_args) self.list_images.invalidate(self) if wait: image_id = None @@ -3303,8 +3302,8 @@ def _upload_image_task( "Timeout waiting for the image to import."): try: if image_id is None: - status = self.manager.submit_task( - _tasks.ImageTaskGet(task_id=glance_task.id)) + status = self._image_client.get( + '/tasks/{id}'.format(id=glance_task.id)) except glanceclient.exc.HTTPServiceUnavailable: # Intermittent failure - catch and try again continue @@ -3313,9 +3312,11 @@ def _upload_image_task( image_id = status.result['image_id'] try: image = self.get_image(image_id) - except glanceclient.exc.HTTPServiceUnavailable: - # Intermittent failure - catch and try again - continue + except OpenStackCloudHTTPError as e: + if e.response.status_code == 503: + # Intermittent failure - catch and try again + continue + raise if image is None: continue self.update_image_properties( @@ -3323,8 +3324,8 @@ def _upload_image_task( return self.get_image(status.result['image_id']) if status.status == 'failure': if status.message == IMAGE_ERROR_396: - glance_task = self.manager.submit_task( - _tasks.ImageTaskCreate(**task_args)) + glance_task = self._image_client.post( + '/tasks', data=task_args) self.list_images.invalidate(self) else: raise OpenStackCloudException( diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index b3f7532c1..2f73a2e88 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -19,7 +19,6 @@ import munch import os_client_config as occ import testtools -import warlock import shade.openstackcloud from shade import exc @@ -693,8 +692,6 @@ class Container(object): } swift_mock.put_container.return_value = fake_container swift_mock.head_object.return_value = {} - mock_image_client.get.return_value = [] - self.assertEqual([], self.cloud.list_images()) fake_md5 = "fake-md5" fake_sha256 = "fake-sha256" @@ -703,19 +700,23 @@ class Container(object): fake_image = munch.Munch( id='a35e8afc-cae9-4e38-8441-2cd465f79f7b', name='name-99', status='active', visibility='private') - mock_image_client.get.return_value = [fake_image] - FakeTask = warlock.model_factory(_TASK_SCHEMA) - args = { - 'id': '21FBD9A7-85EC-4E07-9D58-72F1ACF7CB1F', - 'status': 'success', - 'type': 'import', - 'result': { + args = munch.Munch( + id='21FBD9A7-85EC-4E07-9D58-72F1ACF7CB1F', + status='success', + type='import', + result={ 'image_id': 'a35e8afc-cae9-4e38-8441-2cd465f79f7b', }, - } - fake_task = FakeTask(**args) - glance_mock.tasks.get.return_value = fake_task + ) + + mock_image_client.get.side_effect = [ + [], + args, + [fake_image], + [fake_image], + [fake_image], + ] self._call_create_image(name='name-99', container='image_upload_v2_test_container') args = {'header': ['x-object-meta-x-shade-md5:fake-md5', @@ -727,9 +728,12 @@ class Container(object): objects=mock.ANY, options=args) - glance_mock.tasks.create.assert_called_with(type='import', input={ - 'import_from': 'image_upload_v2_test_container/name-99', - 'image_properties': {'name': 'name-99'}}) + mock_image_client.post.assert_called_with( + '/tasks', + data=dict( + type='import', input={ + 'import_from': 'image_upload_v2_test_container/name-99', + 'image_properties': {'name': 'name-99'}})) object_path = 'image_upload_v2_test_container/name-99' args = {'owner_specified.shade.md5': fake_md5, 'owner_specified.shade.sha256': fake_sha256, From acef8451b385b3b656915cade46e298851b6c527 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 11 Oct 2016 09:28:51 -0400 Subject: [PATCH 1133/3836] Update image downloads to use direct REST Change-Id: Ie936fa80a961a427f86f39e498cb631ffbbc73e4 --- shade/openstackcloud.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 21b742cb4..8c32e10ea 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2436,7 +2436,9 @@ def get_image(self, name_or_id, filters=None): """ return _utils._get_entity(self.search_images, name_or_id, filters) - def download_image(self, name_or_id, output_path=None, output_file=None): + def download_image( + self, name_or_id, output_path=None, output_file=None, + chunk_size=1024): """Download an image from glance by name or ID :param str name_or_id: Name or ID of the image. @@ -2445,6 +2447,8 @@ def download_image(self, name_or_id, output_path=None, output_file=None): :param output_file: a file object (or file-like object) to write the image data to. Only write() will be called on this object. Either this or output_path must be specified + :param int chunk_size: size in bytes to read from the wire and buffer + at one time. Defaults to 1024 :raises: OpenStackCloudException in the event download_image is called without exactly one of either output_path or output_file @@ -2463,16 +2467,22 @@ def download_image(self, name_or_id, output_path=None, output_file=None): image = self.search_images(name_or_id) if len(image) == 0: raise OpenStackCloudResourceNotFound( - "No images with name or id %s were found" % name_or_id) - image_contents = self.glance_client.images.data(image[0]['id']) + "No images with name or id %s were found" % name_or_id, None) + if self.cloud_config.get_api_version('image') == '2': + endpoint = '/images/{id}/file'.format(id=image[0]['id']) + else: + endpoint = '/images/{id}'.format(id=image[0]['id']) + + response = self._image_client.get(endpoint, stream=True) + with _utils.shade_exceptions("Unable to download image"): if output_path: with open(output_path, 'wb') as fd: - for chunk in image_contents: + for chunk in response.iter_content(chunk_size=chunk_size): fd.write(chunk) return elif output_file: - for chunk in image_contents: + for chunk in response.iter_content(chunk_size=chunk_size): output_file.write(chunk) return From 37adf27494cf6fc014f1a1d44af1ed3ff3e67e61 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 6 Dec 2016 09:56:59 -0600 Subject: [PATCH 1134/3836] Plumb nat_destination through for ip_pool case Currently, if ip_pool is provided to add_ips_to_server, nat_destination is not passed along to the things that need it. Fix that. Also, while in that, pass server to create_floating_ip so that the better neutron FIP creation routine gets used. Story: #2000820 Change-Id: I32fd485cb7e7064eaae35578a3643100ee85a096 --- shade/openstackcloud.py | 16 +++++++++++----- shade/tests/unit/test_floating_ip_neutron.py | 5 +++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 8331ed52a..a55869a14 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4336,7 +4336,7 @@ def delete_unattached_floating_ips(self, retry=1): def _attach_ip_to_server( self, server, floating_ip, fixed_address=None, wait=False, - timeout=60, skip_attach=False): + timeout=60, skip_attach=False, nat_destination=None): """Attach a floating IP to a server. :param server: Server dict @@ -4349,6 +4349,8 @@ def _attach_ip_to_server( See the ``wait`` parameter. :param skip_attach: (optional) Skip the actual attach and just do the wait. Defaults to False. + :param nat_destination: The fixed network the server's port for the + FIP to attach to will come from. :returns: The server ``munch.Munch`` @@ -4365,7 +4367,8 @@ def _attach_ip_to_server( try: self._neutron_attach_ip_to_server( server=server, floating_ip=floating_ip, - fixed_address=fixed_address) + fixed_address=fixed_address, + nat_destination=nat_destination) except OpenStackCloudURINotFound as e: self.log.debug( "Something went wrong talking to neutron API: " @@ -4492,14 +4495,16 @@ def _nat_destination_port( return (None, None) def _neutron_attach_ip_to_server( - self, server, floating_ip, fixed_address=None): + self, server, floating_ip, fixed_address=None, + nat_destination=None): with _utils.neutron_exceptions( "unable to bind a floating ip to server " "{0}".format(server['id'])): # Find an available port (port, fixed_address) = self._nat_destination_port( - server, fixed_address=fixed_address) + server, fixed_address=fixed_address, + nat_destination=nat_destination) if not port: raise OpenStackCloudException( "unable to find a port for server {0}".format( @@ -4612,6 +4617,7 @@ def _add_ip_from_pool( else: start_time = time.time() f_ip = self.create_floating_ip( + server=server, network=network, nat_destination=nat_destination, wait=wait, timeout=timeout) timeout = timeout - (time.time() - start_time) @@ -4623,7 +4629,7 @@ def _add_ip_from_pool( # with the FIP information. return self._attach_ip_to_server( server=server, floating_ip=f_ip, fixed_address=fixed_address, - wait=wait, timeout=timeout) + wait=wait, timeout=timeout, nat_destination=nat_destination) def add_ip_list( self, server, ips, wait=False, timeout=60, diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 041076e85..630334685 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -413,11 +413,12 @@ def test_auto_ip_pool_no_reuse( fake_server, ip_pool='my-network', reuse=False) mock__neutron_create_floating_ip.assert_called_once_with( - network_name_or_id='my-network', server=None, port=None, + network_name_or_id='my-network', server=fake_server, port=None, fixed_address=None, nat_destination=None, wait=False, timeout=60) mock_attach_ip_to_server.assert_called_once_with( server=fake_server, fixed_address=None, - floating_ip=fip, wait=False, timeout=mock.ANY) + floating_ip=fip, wait=False, timeout=mock.ANY, + nat_destination=None) @patch.object(OpenStackCloud, 'keystone_session') @patch.object(OpenStackCloud, '_neutron_create_floating_ip') From e6691dddc4aaf20bbddb0b704218cc26d09a8d6a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 9 Dec 2016 12:20:52 -0600 Subject: [PATCH 1135/3836] Stop telling users to check logs End users do not have access to operator service logs. If there is a problem creating a volume or a volume backup, telling them to check the logs is just rude. Change-Id: I16aeb4be16c1f43e19f24dc4dc935958a7523b9c --- shade/openstackcloud.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 2c14b5969..e2457413e 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3430,8 +3430,7 @@ def create_volume( return volume if volume['status'] == 'error': - raise OpenStackCloudException( - "Error in creating volume, please check logs") + raise OpenStackCloudException("Error in creating volume") return self._normalize_volume(volume) @@ -3685,7 +3684,7 @@ def create_volume_snapshot(self, volume_id, force=False, if snapshot['status'] == 'error': raise OpenStackCloudException( - "Error in creating volume snapshot, please check logs") + "Error in creating volume snapshot") # TODO(mordred) need to normalize snapshots. We were normalizing them # as volumes, which is an error. They need to be normalized as @@ -3778,9 +3777,9 @@ def create_volume_backup(self, volume_id, name=None, description=None, break if backup['status'] == 'error': - msg = ("Error in creating volume " - "backup {}, please check logs".format(backup_id)) - raise OpenStackCloudException(msg) + raise OpenStackCloudException( + "Error in creating volume backup {id}".format( + id=backup_id)) return backup From 0b47f865a7030e31c5314c7cb69aa8402c5265c4 Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Sun, 11 Dec 2016 16:18:17 +0100 Subject: [PATCH 1136/3836] Fix _neutron_available_floating_ips filtering Change-Id: Ibc5a730fca5a8049fd54279c0d6ced026dd90e1d Task: 3426 Story: 2000829 --- shade/_normalize.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/_normalize.py b/shade/_normalize.py index 944df2d52..22f77d22b 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -495,7 +495,7 @@ def _normalize_floating_ip(self, ip): ret['router_id'] = router_id ret['project_id'] = project_id ret['tenant_id'] = project_id - ret['floating_network_id'] = network_id, + ret['floating_network_id'] = network_id for key, val in ret['properties'].items(): ret.setdefault(key, val) From ec2d0c20d1b02cf82c407874ef697120fd2c0049 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 12 Dec 2016 09:04:56 -0600 Subject: [PATCH 1137/3836] Make available_floating_ips use normalized keys We have an api we commit to - we should use it interally. Otherwise, if someone provides strict=True, functionality will fail. Change-Id: Iafc7c34df991775f22ebe8cfc845f1f974c079d7 --- shade/openstackcloud.py | 6 +++--- shade/tests/unit/test_floating_ip_neutron.py | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 8c32e10ea..0617107d5 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4037,9 +4037,9 @@ def _neutron_available_floating_ips( floating_network_id = self._get_floating_network_id() filters = { - 'port_id': None, - 'floating_network_id': floating_network_id, - 'tenant_id': project_id + 'port': None, + 'network': floating_network_id, + 'location': {'project': {'id': project_id}}, } floating_ips = self._list_floating_ips() diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 041076e85..f953c9345 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -328,9 +328,9 @@ def test__neutron_available_floating_ips( mock__neutron_list_fips.assert_called_once_with(None) mock__filter_list.assert_called_once_with( [], name_or_id=None, - filters={'port_id': None, - 'floating_network_id': self.mock_get_network_rep['id'], - 'tenant_id': 'proj-id'} + filters={'port': None, + 'network': self.mock_get_network_rep['id'], + 'location': {'project': {'id': 'proj-id'}}} ) mock__neutron_create_fip.assert_called_once_with( network_id=self.mock_get_network_rep['id'], @@ -366,9 +366,9 @@ def test__neutron_available_floating_ips_network( mock__neutron_list_fips.assert_called_once_with(None) mock__filter_list.assert_called_once_with( [], name_or_id=None, - filters={'port_id': None, - 'floating_network_id': self.mock_get_network_rep['id'], - 'tenant_id': 'proj-id'} + filters={'port': None, + 'network': self.mock_get_network_rep['id'], + 'location': {'project': {'id': 'proj-id'}}} ) mock__neutron_create_fip.assert_called_once_with( network_id=self.mock_get_network_rep['id'], From 790d8d8a82435994991bb7e9b6213cc3cc38029d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 11 Oct 2016 19:05:54 -0400 Subject: [PATCH 1138/3836] Change image update to REST Change-Id: I036611df202c4c063db01fca27e10a3437bc5799 --- shade/_tasks.py | 5 --- shade/openstackcloud.py | 34 ++++++++++++------- shade/tests/functional/test_image.py | 24 ++++++++++++++ shade/tests/unit/test_caching.py | 49 ++++++++++++++++++++++++++-- shade/tests/unit/test_shade.py | 15 --------- 5 files changed, 93 insertions(+), 34 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 51a84c86d..110c50a33 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -296,11 +296,6 @@ def main(self, client): return client.nova_client.servers.create_image(**self.args) -class ImageUpdate(task_manager.Task): - def main(self, client): - client.glance_client.images.update(**self.args) - - class VolumeCreate(task_manager.Task): def main(self, client): return client.cinder_client.volumes.create(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 8c32e10ea..181f23f21 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -13,6 +13,7 @@ import functools import hashlib import ipaddress +import jsonpatch import operator import os import os_client_config @@ -29,7 +30,6 @@ import cinderclient.exceptions as cinder_exceptions import glanceclient -import glanceclient.exc import heatclient.client import magnumclient.exceptions as magnum_exceptions from heatclient.common import event_utils @@ -3297,7 +3297,6 @@ def _upload_image_task( # TODO(mordred): Can we do something similar to what nodepool does # using glance properties to not delete then upload but instead make a # new "good" image and then mark the old one as "bad" - # self.glance_client.images.delete(current_image) task_args = dict( type='import', input=dict( import_from='{container}/{name}'.format( @@ -3314,9 +3313,11 @@ def _upload_image_task( if image_id is None: status = self._image_client.get( '/tasks/{id}'.format(id=glance_task.id)) - except glanceclient.exc.HTTPServiceUnavailable: - # Intermittent failure - catch and try again - continue + except OpenStackCloudHTTPError as e: + if e.response.status_code == 503: + # Intermittent failure - catch and try again + continue + raise if status.status == 'success': image_id = status.result['image_id'] @@ -3349,6 +3350,7 @@ def update_image_properties( self, image=None, name_or_id=None, meta=None, **properties): if image is None: image = self.get_image(name_or_id) + if not meta: meta = {} @@ -3366,14 +3368,24 @@ def update_image_properties( return self._update_image_properties_v1(image, meta, img_props) def _update_image_properties_v2(self, image, meta, properties): - img_props = {} + img_props = image.properties.copy() for k, v in iter(self._make_v2_image_params(meta, properties).items()): if image.get(k, None) != v: img_props[k] = v if not img_props: return False - self.manager.submit_task(_tasks.ImageUpdate( - image_id=image.id, **img_props)) + headers = { + 'Content-Type': 'application/openstack-images-v2.1-json-patch'} + patch = sorted(list(jsonpatch.JsonPatch.from_diff( + image.properties, img_props)), key=operator.itemgetter('value')) + + # No need to fire an API call if there is an empty patch + if patch: + self._image_client.patch( + '/images/{id}'.format(id=image.id), + headers=headers, + json=patch) + self.list_images.invalidate(self) return True @@ -3382,11 +3394,11 @@ def _update_image_properties_v1(self, image, meta, properties): img_props = {} for k, v in iter(properties.items()): if image.properties.get(k, None) != v: - img_props[k] = v + img_props['x-image-meta-{key}'.format(key=k)] = v if not img_props: return False - self.manager.submit_task(_tasks.ImageUpdate( - image=image, properties=img_props)) + self._image_client.put( + '/images/{id}'.format(image.id), headers=img_props) self.list_images.invalidate(self) return True diff --git a/shade/tests/functional/test_image.py b/shade/tests/functional/test_image.py index 66308097c..a7657b08a 100644 --- a/shade/tests/functional/test_image.py +++ b/shade/tests/functional/test_image.py @@ -127,3 +127,27 @@ def test_create_image_force_duplicate(self): self.demo_cloud.delete_image(first_image.id, wait=True) if second_image: self.demo_cloud.delete_image(second_image.id, wait=True) + + def test_create_image_update_properties(self): + test_image = tempfile.NamedTemporaryFile(delete=False) + test_image.write('\0' * 1024 * 1024) + test_image.close() + image_name = self.getUniqueString('image') + try: + image = self.demo_cloud.create_image( + name=image_name, + filename=test_image.name, + disk_format='raw', + container_format='bare', + min_disk=10, + min_ram=1024, + wait=True) + self.demo_cloud.update_image_properties( + image=image, + name=image_name, + foo='bar') + image = self.demo_cloud.get_image(image_name) + self.assertIn('foo', image.properties) + self.assertEqual(image.properties['foo'], 'bar') + finally: + self.demo_cloud.delete_image(image_name, wait=True) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 2f73a2e88..c6c6c1dd5 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -17,6 +17,7 @@ import mock import munch +import operator import os_client_config as occ import testtools @@ -488,6 +489,32 @@ def test_create_image_put_v2(self, mock_image_client, mock_api_version): self.assertEqual( self._munch_images(ret), self.cloud.list_images()) + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_image_client') + def test_update_image_no_patch(self, mock_image_client, mock_api_version): + mock_api_version.return_value = '2' + self.cloud.image_api_use_tasks = False + + mock_image_client.get.return_value = [] + self.assertEqual([], self.cloud.list_images()) + + args = {'name': '42 name', + 'container_format': 'bare', 'disk_format': 'qcow2', + 'owner_specified.shade.md5': mock.ANY, + 'owner_specified.shade.sha256': mock.ANY, + 'owner_specified.shade.object': 'images/42 name', + 'visibility': 'private', + 'min_disk': 0, 'min_ram': 0} + ret = munch.Munch(args.copy()) + ret['id'] = '42' + ret['status'] = 'success' + mock_image_client.get.return_value = [ret] + self.cloud.update_image_properties( + image=self._image_dict(ret), + **{'owner_specified.shade.object': 'images/42 name'}) + mock_image_client.get.assert_called_with('/images') + mock_image_client.patch.assert_not_called() + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, '_image_client') def test_create_image_put_v2_bad_delete( @@ -668,13 +695,11 @@ def test_create_image_put_user_prop( @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, '_get_file_hashes') @mock.patch.object(shade.OpenStackCloud, '_image_client') - @mock.patch.object(shade.OpenStackCloud, 'glance_client') @mock.patch.object(shade.OpenStackCloud, 'swift_client') @mock.patch.object(shade.OpenStackCloud, 'swift_service') def test_create_image_task(self, swift_service_mock, swift_mock, - glance_mock, mock_image_client, get_file_hashes, mock_api_version): @@ -739,7 +764,25 @@ class Container(object): 'owner_specified.shade.sha256': fake_sha256, 'owner_specified.shade.object': object_path, 'image_id': 'a35e8afc-cae9-4e38-8441-2cd465f79f7b'} - glance_mock.images.update.assert_called_with(**args) + mock_image_client.patch.assert_called_with( + '/images/a35e8afc-cae9-4e38-8441-2cd465f79f7b', + json=sorted([ + { + u'op': u'add', + u'value': 'image_upload_v2_test_container/name-99', + u'path': u'/owner_specified.shade.object' + }, { + u'op': u'add', + u'value': 'fake-md5', + u'path': u'/owner_specified.shade.md5' + }, { + u'op': u'add', u'value': 'fake-sha256', + u'path': u'/owner_specified.shade.sha256' + }], key=operator.itemgetter('value')), + headers={ + 'Content-Type': 'application/openstack-images-v2.1-json-patch' + }) + self.assertEqual( self._munch_images(fake_image), self.cloud.list_images()) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index cccd81dcc..ff438392c 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -15,7 +15,6 @@ import mock import munch -import glanceclient from heatclient import client as heat_client from neutronclient.common import exceptions as n_exc import testtools @@ -77,20 +76,6 @@ def test_list_servers_exception(self, mock_client): self.assertRaises(exc.OpenStackCloudException, self.cloud.list_servers) - @mock.patch.object(cloud_config.CloudConfig, 'get_session') - @mock.patch.object(cloud_config.CloudConfig, 'get_legacy_client') - def test_glance_args(self, get_legacy_client_mock, get_session_mock): - session_mock = mock.Mock() - session_mock.get_endpoint.return_value = 'http://example.com/v2' - get_session_mock.return_value = session_mock - self.cloud.glance_client - get_legacy_client_mock.assert_called_once_with( - service_key='image', - client_class=glanceclient.Client, - interface_key=None, - pass_version_arg=True, - ) - @mock.patch.object(cloud_config.CloudConfig, 'get_session') @mock.patch.object(cloud_config.CloudConfig, 'get_legacy_client') def test_heat_args(self, get_legacy_client_mock, get_session_mock): From dd25a3c41603ce254bb1ba1e81e6f43b33cf6eb8 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 10 Oct 2016 10:03:10 -0400 Subject: [PATCH 1139/3836] Remove a few glance client mocks we missed Change-Id: Id4b5bc89e8829b3aebe67053b16c49abc60248c6 --- shade/tests/unit/test_caching.py | 6 ++---- shade/tests/unit/test_shade_operator.py | 8 ++++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index c6c6c1dd5..dfe43db71 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -599,9 +599,8 @@ def test_create_image_put_user_int( @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, '_image_client') - @mock.patch.object(shade.OpenStackCloud, 'glance_client') def test_create_image_put_meta_int( - self, glance_mock, mock_image_client, mock_api_version): + self, mock_image_client, mock_api_version): mock_api_version.return_value = '2' self.cloud.image_api_use_tasks = False @@ -663,9 +662,8 @@ def test_create_image_put_protected( @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, '_image_client') - @mock.patch.object(shade.OpenStackCloud, 'glance_client') def test_create_image_put_user_prop( - self, glance_mock, mock_image_client, mock_api_version): + self, mock_image_client, mock_api_version): mock_api_version.return_value = '2' self.cloud.image_api_use_tasks = False diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index 28010cf1f..abae45f8e 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -1021,24 +1021,24 @@ def test_purge_node_instance_info(self, mock_client): ) @mock.patch.object(shade.OpenStackCloud, '_image_client') - def test_get_image_name(self, glance_mock): + def test_get_image_name(self, mock_client): fake_image = munch.Munch( id='22', name='22 name', status='success') - glance_mock.get.return_value = [fake_image] + mock_client.get.return_value = [fake_image] self.assertEqual('22 name', self.op_cloud.get_image_name('22')) self.assertEqual('22 name', self.op_cloud.get_image_name('22 name')) @mock.patch.object(shade.OpenStackCloud, '_image_client') - def test_get_image_id(self, glance_mock): + def test_get_image_id(self, mock_client): fake_image = munch.Munch( id='22', name='22 name', status='success') - glance_mock.get.return_value = [fake_image] + mock_client.get.return_value = [fake_image] self.assertEqual('22', self.op_cloud.get_image_id('22')) self.assertEqual('22', self.op_cloud.get_image_id('22 name')) From 24acc1888bff40fed9d51dcb2a876023cdcd1a96 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 11 Oct 2016 19:09:19 -0400 Subject: [PATCH 1140/3836] Remove glanceclient and warlock from shade We've removed the uses - remove both from requirements. Change-Id: Ie3f2354d2034a695ae9086f84f5d6c7f3698d90b --- requirements.txt | 1 - shade/meta.py | 7 ++++--- shade/openstackcloud.py | 2 +- shade/tests/unit/test_meta.py | 20 -------------------- test-requirements.txt | 1 - 5 files changed, 5 insertions(+), 26 deletions(-) diff --git a/requirements.txt b/requirements.txt index 130ce8624..000072795 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,6 @@ keystoneauth1>=2.11.0 netifaces>=0.10.4 python-novaclient>=2.21.0,!=2.27.0,!=2.32.0 python-keystoneclient>=0.11.0 -python-glanceclient>=1.0.0 python-cinderclient>=1.3.1 python-neutronclient>=2.3.10 python-troveclient>=1.2.0 diff --git a/shade/meta.py b/shade/meta.py index 019551454..f81f808ed 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -454,9 +454,6 @@ def obj_to_dict(obj, request_id=None): # If we obj_to_dict twice, don't fail, just return the munch # Also, don't try to modify Mock objects - that way lies madness return obj - elif hasattr(obj, 'schema') and hasattr(obj, 'validate'): - # It's a warlock - return _log_request_id(warlock_to_dict(obj), request_id) elif isinstance(obj, dict): # The new request-id tracking spec: # https://specs.openstack.org/openstack/nova-specs/specs/juno/approved/log-request-id-mappings.html @@ -496,6 +493,10 @@ def obj_list_to_dict(obj_list, request_id=None): def warlock_to_dict(obj): + # This function is unused in shade - but it is a public function, so + # removing it would be rude. We don't actually have to depend on warlock + # ourselves to keep this - so just leave it here. + # # glanceclient v2 uses warlock to construct its objects. Warlock does # deep black magic to attribute look up to support validation things that # means we cannot use normal obj_to_dict diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 181f23f21..2f92d3fed 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -29,7 +29,6 @@ from six.moves import urllib import cinderclient.exceptions as cinder_exceptions -import glanceclient import heatclient.client import magnumclient.exceptions as magnum_exceptions from heatclient.common import event_utils @@ -959,6 +958,7 @@ def remove_user_from_group(self, name_or_id, group_name_or_id): @property def glance_client(self): + import glanceclient if self._glance_client is None: self._glance_client = self._get_client( 'image', glanceclient.Client) diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 7570dbe1e..9e24ef607 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -13,7 +13,6 @@ # limitations under the License. import mock -import warlock from neutronclient.common import exceptions as neutron_exceptions @@ -954,22 +953,3 @@ class FakeObjDict(dict): self.assertIn('foo', obj_dict) self.assertEqual(obj_dict['additional'], 1) self.assertEqual(obj_dict['foo'], 'bar') - - def test_warlock_to_dict(self): - schema = { - 'name': 'Test', - 'properties': { - 'id': {'type': 'string'}, - 'name': {'type': 'string'}, - '_unused': {'type': 'string'}, - } - } - test_model = warlock.model_factory(schema) - test_obj = test_model( - id='471c2475-da2f-47ac-aba5-cb4aa3d546f5', - name='test-image') - test_dict = meta.obj_to_dict(test_obj) - self.assertNotIn('_unused', test_dict) - self.assertEqual('test-image', test_dict['name']) - self.assertTrue(hasattr(test_dict, 'name')) - self.assertEqual(test_dict.name, test_dict['name']) diff --git a/test-requirements.txt b/test-requirements.txt index c7611c859..cf8c7fd72 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -12,6 +12,5 @@ sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 testrepository>=0.0.17 testscenarios>=0.4,<0.5 testtools>=0.9.32 -warlock>=1.0.1,<2 reno futures;python_version<'3.2' From 9de5c7806bf54ef8dcf70c0dfa6c31ea33b3d210 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 6 Dec 2016 12:05:35 -0600 Subject: [PATCH 1141/3836] Move image tests from caching to image test file There are literally no changes to these tests. They are moved because they are testing image code, not caching code. Next will come refactoring them to use requests_mock instead of mocking the _image_client itself. Change-Id: I749db302f1612a01c726224a68193add1472b5cb --- shade/tests/unit/test_caching.py | 404 ----------------------------- shade/tests/unit/test_image.py | 422 +++++++++++++++++++++++++++++++ 2 files changed, 422 insertions(+), 404 deletions(-) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index dfe43db71..1612ab92b 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -12,13 +12,10 @@ # License for the specific language governing permissions and limitations # under the License. import concurrent -import tempfile import time import mock import munch -import operator -import os_client_config as occ import testtools import shade.openstackcloud @@ -383,407 +380,6 @@ def test_list_images_caches_steady_status(self, mock_image_client): self._munch_images(first_image), self.cloud.list_images()) - def _call_create_image(self, name, **kwargs): - imagefile = tempfile.NamedTemporaryFile(delete=False) - imagefile.write(b'\0') - imagefile.close() - self.cloud.create_image( - name, imagefile.name, wait=True, timeout=1, - is_public=False, **kwargs) - - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, '_image_client') - def test_create_image_put_v1(self, mock_image_client, mock_api_version): - mock_api_version.return_value = '1' - mock_image_client.get.return_value = [] - self.assertEqual([], self.cloud.list_images()) - - args = {'name': '42 name', - 'container_format': 'bare', 'disk_format': 'qcow2', - 'properties': { - 'owner_specified.shade.md5': mock.ANY, - 'owner_specified.shade.sha256': mock.ANY, - 'owner_specified.shade.object': 'images/42 name', - 'is_public': False}} - ret = munch.Munch(args.copy()) - ret['id'] = '42' - ret['status'] = 'success' - mock_image_client.get.return_value = [ret] - mock_image_client.post.return_value = ret - mock_image_client.put.return_value = ret - self._call_create_image('42 name') - mock_image_client.post.assert_called_with('/images', json=args) - mock_image_client.put.assert_called_with( - '/images/42', data=mock.ANY, - headers={ - 'x-image-meta-checksum': mock.ANY, - 'x-glance-registry-purge-props': 'false' - }) - mock_image_client.get.assert_called_with('/images/detail') - self.assertEqual( - self._munch_images(ret), self.cloud.list_images()) - - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, '_image_client') - def test_create_image_put_v1_bad_delete( - self, mock_image_client, mock_api_version): - mock_api_version.return_value = '1' - mock_image_client.get.return_value = [] - self.assertEqual([], self.cloud.list_images()) - - args = {'name': '42 name', - 'container_format': 'bare', 'disk_format': 'qcow2', - 'properties': { - 'owner_specified.shade.md5': mock.ANY, - 'owner_specified.shade.sha256': mock.ANY, - 'owner_specified.shade.object': 'images/42 name', - 'is_public': False}} - ret = munch.Munch(args.copy()) - ret['id'] = '42' - ret['status'] = 'success' - mock_image_client.get.return_value = [ret] - mock_image_client.post.return_value = ret - mock_image_client.put.side_effect = exc.OpenStackCloudHTTPError( - "Some error", {}) - self.assertRaises( - exc.OpenStackCloudHTTPError, - self._call_create_image, - '42 name') - mock_image_client.post.assert_called_with('/images', json=args) - mock_image_client.put.assert_called_with( - '/images/42', data=mock.ANY, - headers={ - 'x-image-meta-checksum': mock.ANY, - 'x-glance-registry-purge-props': 'false' - }) - mock_image_client.delete.assert_called_with('/images/42') - - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, '_image_client') - def test_create_image_put_v2(self, mock_image_client, mock_api_version): - mock_api_version.return_value = '2' - self.cloud.image_api_use_tasks = False - - mock_image_client.get.return_value = [] - self.assertEqual([], self.cloud.list_images()) - - args = {'name': '42 name', - 'container_format': 'bare', 'disk_format': 'qcow2', - 'owner_specified.shade.md5': mock.ANY, - 'owner_specified.shade.sha256': mock.ANY, - 'owner_specified.shade.object': 'images/42 name', - 'visibility': 'private', - 'min_disk': 0, 'min_ram': 0} - ret = munch.Munch(args.copy()) - ret['id'] = '42' - ret['status'] = 'success' - mock_image_client.get.return_value = [ret] - mock_image_client.post.return_value = ret - self._call_create_image('42 name', min_disk='0', min_ram=0) - mock_image_client.post.assert_called_with('/images', json=args) - mock_image_client.put.assert_called_with( - '/images/42/file', - headers={'Content-Type': 'application/octet-stream'}, - data=mock.ANY) - mock_image_client.get.assert_called_with('/images') - self.assertEqual( - self._munch_images(ret), self.cloud.list_images()) - - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, '_image_client') - def test_update_image_no_patch(self, mock_image_client, mock_api_version): - mock_api_version.return_value = '2' - self.cloud.image_api_use_tasks = False - - mock_image_client.get.return_value = [] - self.assertEqual([], self.cloud.list_images()) - - args = {'name': '42 name', - 'container_format': 'bare', 'disk_format': 'qcow2', - 'owner_specified.shade.md5': mock.ANY, - 'owner_specified.shade.sha256': mock.ANY, - 'owner_specified.shade.object': 'images/42 name', - 'visibility': 'private', - 'min_disk': 0, 'min_ram': 0} - ret = munch.Munch(args.copy()) - ret['id'] = '42' - ret['status'] = 'success' - mock_image_client.get.return_value = [ret] - self.cloud.update_image_properties( - image=self._image_dict(ret), - **{'owner_specified.shade.object': 'images/42 name'}) - mock_image_client.get.assert_called_with('/images') - mock_image_client.patch.assert_not_called() - - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, '_image_client') - def test_create_image_put_v2_bad_delete( - self, mock_image_client, mock_api_version): - mock_api_version.return_value = '2' - self.cloud.image_api_use_tasks = False - - mock_image_client.get.return_value = [] - self.assertEqual([], self.cloud.list_images()) - - args = {'name': '42 name', - 'container_format': 'bare', 'disk_format': 'qcow2', - 'owner_specified.shade.md5': mock.ANY, - 'owner_specified.shade.sha256': mock.ANY, - 'owner_specified.shade.object': 'images/42 name', - 'visibility': 'private', - 'min_disk': 0, 'min_ram': 0} - ret = munch.Munch(args.copy()) - ret['id'] = '42' - ret['status'] = 'success' - mock_image_client.get.return_value = [ret] - mock_image_client.post.return_value = ret - mock_image_client.put.side_effect = exc.OpenStackCloudHTTPError( - "Some error", {}) - self.assertRaises( - exc.OpenStackCloudHTTPError, - self._call_create_image, - '42 name', min_disk='0', min_ram=0) - mock_image_client.post.assert_called_with('/images', json=args) - mock_image_client.put.assert_called_with( - '/images/42/file', - headers={'Content-Type': 'application/octet-stream'}, - data=mock.ANY) - mock_image_client.delete.assert_called_with('/images/42') - - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, '_image_client') - def test_create_image_put_bad_int( - self, mock_image_client, mock_api_version): - mock_api_version.return_value = '2' - self.cloud.image_api_use_tasks = False - - self.assertRaises( - exc.OpenStackCloudException, - self._call_create_image, '42 name', min_disk='fish', min_ram=0) - mock_image_client.post.assert_not_called() - - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, '_image_client') - def test_create_image_put_user_int( - self, mock_image_client, mock_api_version): - mock_api_version.return_value = '2' - self.cloud.image_api_use_tasks = False - - args = {'name': '42 name', - 'container_format': 'bare', 'disk_format': u'qcow2', - 'owner_specified.shade.md5': mock.ANY, - 'owner_specified.shade.sha256': mock.ANY, - 'owner_specified.shade.object': 'images/42 name', - 'int_v': '12345', - 'visibility': 'private', - 'min_disk': 0, 'min_ram': 0} - ret = munch.Munch(args.copy()) - ret['id'] = '42' - ret['status'] = 'success' - mock_image_client.get.side_effect = [ - [], - [ret], - [ret] - ] - mock_image_client.post.return_value = ret - self._call_create_image( - '42 name', min_disk='0', min_ram=0, int_v=12345) - mock_image_client.post.assert_called_with('/images', json=args) - mock_image_client.put.assert_called_with( - '/images/42/file', - headers={'Content-Type': 'application/octet-stream'}, - data=mock.ANY) - mock_image_client.get.assert_called_with('/images') - self.assertEqual( - self._munch_images(ret), self.cloud.list_images()) - - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, '_image_client') - def test_create_image_put_meta_int( - self, mock_image_client, mock_api_version): - mock_api_version.return_value = '2' - self.cloud.image_api_use_tasks = False - - mock_image_client.get.return_value = [] - self.assertEqual([], self.cloud.list_images()) - - self._call_create_image( - '42 name', min_disk='0', min_ram=0, meta={'int_v': 12345}) - args = {'name': '42 name', - 'container_format': 'bare', 'disk_format': u'qcow2', - 'owner_specified.shade.md5': mock.ANY, - 'owner_specified.shade.sha256': mock.ANY, - 'owner_specified.shade.object': 'images/42 name', - 'int_v': 12345, - 'visibility': 'private', - 'min_disk': 0, 'min_ram': 0} - ret = munch.Munch(args.copy()) - ret['id'] = '42' - ret['status'] = 'success' - mock_image_client.get.return_value = [ret] - mock_image_client.post.return_value = ret - mock_image_client.get.assert_called_with('/images') - self.assertEqual( - self._munch_images(ret), self.cloud.list_images()) - - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, '_image_client') - def test_create_image_put_protected( - self, mock_image_client, mock_api_version): - mock_api_version.return_value = '2' - self.cloud.image_api_use_tasks = False - - mock_image_client.get.return_value = [] - self.assertEqual([], self.cloud.list_images()) - - args = {'name': '42 name', - 'container_format': 'bare', 'disk_format': u'qcow2', - 'owner_specified.shade.md5': mock.ANY, - 'owner_specified.shade.sha256': mock.ANY, - 'owner_specified.shade.object': 'images/42 name', - 'protected': False, - 'int_v': '12345', - 'visibility': 'private', - 'min_disk': 0, 'min_ram': 0} - ret = munch.Munch(args.copy()) - ret['id'] = '42' - ret['status'] = 'success' - mock_image_client.get.return_value = [ret] - mock_image_client.put.return_value = ret - mock_image_client.post.return_value = ret - self._call_create_image( - '42 name', min_disk='0', min_ram=0, properties={'int_v': 12345}, - protected=False) - mock_image_client.post.assert_called_with('/images', json=args) - mock_image_client.put.assert_called_with( - '/images/42/file', data=mock.ANY, - headers={'Content-Type': 'application/octet-stream'}) - self.assertEqual(self._munch_images(ret), self.cloud.list_images()) - - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, '_image_client') - def test_create_image_put_user_prop( - self, mock_image_client, mock_api_version): - mock_api_version.return_value = '2' - self.cloud.image_api_use_tasks = False - - mock_image_client.get.return_value = [] - self.assertEqual([], self.cloud.list_images()) - - args = {'name': '42 name', - 'container_format': 'bare', 'disk_format': u'qcow2', - 'owner_specified.shade.md5': mock.ANY, - 'owner_specified.shade.sha256': mock.ANY, - 'owner_specified.shade.object': 'images/42 name', - 'int_v': '12345', - 'xenapi_use_agent': 'False', - 'visibility': 'private', - 'min_disk': 0, 'min_ram': 0} - ret = munch.Munch(args.copy()) - ret['id'] = '42' - ret['status'] = 'success' - mock_image_client.get.return_value = [ret] - mock_image_client.post.return_value = ret - self._call_create_image( - '42 name', min_disk='0', min_ram=0, properties={'int_v': 12345}) - mock_image_client.get.assert_called_with('/images') - self.assertEqual( - self._munch_images(ret), self.cloud.list_images()) - - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, '_get_file_hashes') - @mock.patch.object(shade.OpenStackCloud, '_image_client') - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - @mock.patch.object(shade.OpenStackCloud, 'swift_service') - def test_create_image_task(self, - swift_service_mock, - swift_mock, - mock_image_client, - get_file_hashes, - mock_api_version): - mock_api_version.return_value = '2' - self.cloud.image_api_use_tasks = True - - class Container(object): - name = 'image_upload_v2_test_container' - - fake_container = Container() - swift_mock.get_capabilities.return_value = { - 'swift': { - 'max_file_size': 1000 - } - } - swift_mock.put_container.return_value = fake_container - swift_mock.head_object.return_value = {} - - fake_md5 = "fake-md5" - fake_sha256 = "fake-sha256" - get_file_hashes.return_value = (fake_md5, fake_sha256) - - fake_image = munch.Munch( - id='a35e8afc-cae9-4e38-8441-2cd465f79f7b', name='name-99', - status='active', visibility='private') - - args = munch.Munch( - id='21FBD9A7-85EC-4E07-9D58-72F1ACF7CB1F', - status='success', - type='import', - result={ - 'image_id': 'a35e8afc-cae9-4e38-8441-2cd465f79f7b', - }, - ) - - mock_image_client.get.side_effect = [ - [], - args, - [fake_image], - [fake_image], - [fake_image], - ] - self._call_create_image(name='name-99', - container='image_upload_v2_test_container') - args = {'header': ['x-object-meta-x-shade-md5:fake-md5', - 'x-object-meta-x-shade-sha256:fake-sha256'], - 'segment_size': 1000, - 'use_slo': True} - swift_service_mock.upload.assert_called_with( - container='image_upload_v2_test_container', - objects=mock.ANY, - options=args) - - mock_image_client.post.assert_called_with( - '/tasks', - data=dict( - type='import', input={ - 'import_from': 'image_upload_v2_test_container/name-99', - 'image_properties': {'name': 'name-99'}})) - object_path = 'image_upload_v2_test_container/name-99' - args = {'owner_specified.shade.md5': fake_md5, - 'owner_specified.shade.sha256': fake_sha256, - 'owner_specified.shade.object': object_path, - 'image_id': 'a35e8afc-cae9-4e38-8441-2cd465f79f7b'} - mock_image_client.patch.assert_called_with( - '/images/a35e8afc-cae9-4e38-8441-2cd465f79f7b', - json=sorted([ - { - u'op': u'add', - u'value': 'image_upload_v2_test_container/name-99', - u'path': u'/owner_specified.shade.object' - }, { - u'op': u'add', - u'value': 'fake-md5', - u'path': u'/owner_specified.shade.md5' - }, { - u'op': u'add', u'value': 'fake-sha256', - u'path': u'/owner_specified.shade.sha256' - }], key=operator.itemgetter('value')), - headers={ - 'Content-Type': 'application/openstack-images-v2.1-json-patch' - }) - - self.assertEqual( - self._munch_images(fake_image), self.cloud.list_images()) - @mock.patch.object(shade.OpenStackCloud, '_image_client') def test_cache_no_cloud_name(self, mock_image_client): diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index cacd2d9d2..0daf6fb77 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -12,12 +12,18 @@ # License for the specific language governing permissions and limitations # under the License. +import operator import tempfile import uuid +import mock +import munch +import os_client_config as occ import six +import shade from shade import exc +from shade import meta from shade.tests.unit import base @@ -95,3 +101,419 @@ def test_download_image_with_path(self): self.cloud.download_image('fake_image', output_path=output_file.name) output_file.seek(0) self.assertEqual(output_file.read(), self.output) + + +class TestMockImage(base.TestCase): + + def setUp(self): + super(TestMockImage, self).setUp( + cloud_config_fixture='clouds_cache.yaml') + + def _image_dict(self, fake_image): + return self.cloud._normalize_image(meta.obj_to_dict(fake_image)) + + def _munch_images(self, fake_image): + return self.cloud._normalize_images([fake_image]) + + def _call_create_image(self, name, **kwargs): + imagefile = tempfile.NamedTemporaryFile(delete=False) + imagefile.write(b'\0') + imagefile.close() + self.cloud.create_image( + name, imagefile.name, wait=True, timeout=1, + is_public=False, **kwargs) + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_image_client') + def test_create_image_put_v1(self, mock_image_client, mock_api_version): + mock_api_version.return_value = '1' + mock_image_client.get.return_value = [] + self.assertEqual([], self.cloud.list_images()) + + args = {'name': '42 name', + 'container_format': 'bare', 'disk_format': 'qcow2', + 'properties': { + 'owner_specified.shade.md5': mock.ANY, + 'owner_specified.shade.sha256': mock.ANY, + 'owner_specified.shade.object': 'images/42 name', + 'is_public': False}} + ret = munch.Munch(args.copy()) + ret['id'] = '42' + ret['status'] = 'success' + mock_image_client.get.return_value = [ret] + mock_image_client.post.return_value = ret + mock_image_client.put.return_value = ret + self._call_create_image('42 name') + mock_image_client.post.assert_called_with('/images', json=args) + mock_image_client.put.assert_called_with( + '/images/42', data=mock.ANY, + headers={ + 'x-image-meta-checksum': mock.ANY, + 'x-glance-registry-purge-props': 'false' + }) + mock_image_client.get.assert_called_with('/images/detail') + self.assertEqual( + self._munch_images(ret), self.cloud.list_images()) + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_image_client') + def test_create_image_put_v1_bad_delete( + self, mock_image_client, mock_api_version): + mock_api_version.return_value = '1' + mock_image_client.get.return_value = [] + self.assertEqual([], self.cloud.list_images()) + + args = {'name': '42 name', + 'container_format': 'bare', 'disk_format': 'qcow2', + 'properties': { + 'owner_specified.shade.md5': mock.ANY, + 'owner_specified.shade.sha256': mock.ANY, + 'owner_specified.shade.object': 'images/42 name', + 'is_public': False}} + ret = munch.Munch(args.copy()) + ret['id'] = '42' + ret['status'] = 'success' + mock_image_client.get.return_value = [ret] + mock_image_client.post.return_value = ret + mock_image_client.put.side_effect = exc.OpenStackCloudHTTPError( + "Some error", {}) + self.assertRaises( + exc.OpenStackCloudHTTPError, + self._call_create_image, + '42 name') + mock_image_client.post.assert_called_with('/images', json=args) + mock_image_client.put.assert_called_with( + '/images/42', data=mock.ANY, + headers={ + 'x-image-meta-checksum': mock.ANY, + 'x-glance-registry-purge-props': 'false' + }) + mock_image_client.delete.assert_called_with('/images/42') + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_image_client') + def test_create_image_put_v2(self, mock_image_client, mock_api_version): + mock_api_version.return_value = '2' + self.cloud.image_api_use_tasks = False + + mock_image_client.get.return_value = [] + self.assertEqual([], self.cloud.list_images()) + + args = {'name': '42 name', + 'container_format': 'bare', 'disk_format': 'qcow2', + 'owner_specified.shade.md5': mock.ANY, + 'owner_specified.shade.sha256': mock.ANY, + 'owner_specified.shade.object': 'images/42 name', + 'visibility': 'private', + 'min_disk': 0, 'min_ram': 0} + ret = munch.Munch(args.copy()) + ret['id'] = '42' + ret['status'] = 'success' + mock_image_client.get.return_value = [ret] + mock_image_client.post.return_value = ret + self._call_create_image('42 name', min_disk='0', min_ram=0) + mock_image_client.post.assert_called_with('/images', json=args) + mock_image_client.put.assert_called_with( + '/images/42/file', + headers={'Content-Type': 'application/octet-stream'}, + data=mock.ANY) + mock_image_client.get.assert_called_with('/images') + self.assertEqual( + self._munch_images(ret), self.cloud.list_images()) + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_image_client') + def test_update_image_no_patch(self, mock_image_client, mock_api_version): + mock_api_version.return_value = '2' + self.cloud.image_api_use_tasks = False + + mock_image_client.get.return_value = [] + self.assertEqual([], self.cloud.list_images()) + + args = {'name': '42 name', + 'container_format': 'bare', 'disk_format': 'qcow2', + 'owner_specified.shade.md5': mock.ANY, + 'owner_specified.shade.sha256': mock.ANY, + 'owner_specified.shade.object': 'images/42 name', + 'visibility': 'private', + 'min_disk': 0, 'min_ram': 0} + ret = munch.Munch(args.copy()) + ret['id'] = '42' + ret['status'] = 'success' + mock_image_client.get.return_value = [ret] + self.cloud.update_image_properties( + image=self._image_dict(ret), + **{'owner_specified.shade.object': 'images/42 name'}) + mock_image_client.get.assert_called_with('/images') + mock_image_client.patch.assert_not_called() + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_image_client') + def test_create_image_put_v2_bad_delete( + self, mock_image_client, mock_api_version): + mock_api_version.return_value = '2' + self.cloud.image_api_use_tasks = False + + mock_image_client.get.return_value = [] + self.assertEqual([], self.cloud.list_images()) + + args = {'name': '42 name', + 'container_format': 'bare', 'disk_format': 'qcow2', + 'owner_specified.shade.md5': mock.ANY, + 'owner_specified.shade.sha256': mock.ANY, + 'owner_specified.shade.object': 'images/42 name', + 'visibility': 'private', + 'min_disk': 0, 'min_ram': 0} + ret = munch.Munch(args.copy()) + ret['id'] = '42' + ret['status'] = 'success' + mock_image_client.get.return_value = [ret] + mock_image_client.post.return_value = ret + mock_image_client.put.side_effect = exc.OpenStackCloudHTTPError( + "Some error", {}) + self.assertRaises( + exc.OpenStackCloudHTTPError, + self._call_create_image, + '42 name', min_disk='0', min_ram=0) + mock_image_client.post.assert_called_with('/images', json=args) + mock_image_client.put.assert_called_with( + '/images/42/file', + headers={'Content-Type': 'application/octet-stream'}, + data=mock.ANY) + mock_image_client.delete.assert_called_with('/images/42') + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_image_client') + def test_create_image_put_bad_int( + self, mock_image_client, mock_api_version): + mock_api_version.return_value = '2' + self.cloud.image_api_use_tasks = False + + self.assertRaises( + exc.OpenStackCloudException, + self._call_create_image, '42 name', min_disk='fish', min_ram=0) + mock_image_client.post.assert_not_called() + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_image_client') + def test_create_image_put_user_int( + self, mock_image_client, mock_api_version): + mock_api_version.return_value = '2' + self.cloud.image_api_use_tasks = False + + args = {'name': '42 name', + 'container_format': 'bare', 'disk_format': u'qcow2', + 'owner_specified.shade.md5': mock.ANY, + 'owner_specified.shade.sha256': mock.ANY, + 'owner_specified.shade.object': 'images/42 name', + 'int_v': '12345', + 'visibility': 'private', + 'min_disk': 0, 'min_ram': 0} + ret = munch.Munch(args.copy()) + ret['id'] = '42' + ret['status'] = 'success' + mock_image_client.get.side_effect = [ + [], + [ret], + [ret] + ] + mock_image_client.post.return_value = ret + self._call_create_image( + '42 name', min_disk='0', min_ram=0, int_v=12345) + mock_image_client.post.assert_called_with('/images', json=args) + mock_image_client.put.assert_called_with( + '/images/42/file', + headers={'Content-Type': 'application/octet-stream'}, + data=mock.ANY) + mock_image_client.get.assert_called_with('/images') + self.assertEqual( + self._munch_images(ret), self.cloud.list_images()) + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_image_client') + def test_create_image_put_meta_int( + self, mock_image_client, mock_api_version): + mock_api_version.return_value = '2' + self.cloud.image_api_use_tasks = False + + mock_image_client.get.return_value = [] + self.assertEqual([], self.cloud.list_images()) + + self._call_create_image( + '42 name', min_disk='0', min_ram=0, meta={'int_v': 12345}) + args = {'name': '42 name', + 'container_format': 'bare', 'disk_format': u'qcow2', + 'owner_specified.shade.md5': mock.ANY, + 'owner_specified.shade.sha256': mock.ANY, + 'owner_specified.shade.object': 'images/42 name', + 'int_v': 12345, + 'visibility': 'private', + 'min_disk': 0, 'min_ram': 0} + ret = munch.Munch(args.copy()) + ret['id'] = '42' + ret['status'] = 'success' + mock_image_client.get.return_value = [ret] + mock_image_client.post.return_value = ret + mock_image_client.get.assert_called_with('/images') + self.assertEqual( + self._munch_images(ret), self.cloud.list_images()) + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_image_client') + def test_create_image_put_protected( + self, mock_image_client, mock_api_version): + mock_api_version.return_value = '2' + self.cloud.image_api_use_tasks = False + + mock_image_client.get.return_value = [] + self.assertEqual([], self.cloud.list_images()) + + args = {'name': '42 name', + 'container_format': 'bare', 'disk_format': u'qcow2', + 'owner_specified.shade.md5': mock.ANY, + 'owner_specified.shade.sha256': mock.ANY, + 'owner_specified.shade.object': 'images/42 name', + 'protected': False, + 'int_v': '12345', + 'visibility': 'private', + 'min_disk': 0, 'min_ram': 0} + ret = munch.Munch(args.copy()) + ret['id'] = '42' + ret['status'] = 'success' + mock_image_client.get.return_value = [ret] + mock_image_client.put.return_value = ret + mock_image_client.post.return_value = ret + self._call_create_image( + '42 name', min_disk='0', min_ram=0, properties={'int_v': 12345}, + protected=False) + mock_image_client.post.assert_called_with('/images', json=args) + mock_image_client.put.assert_called_with( + '/images/42/file', data=mock.ANY, + headers={'Content-Type': 'application/octet-stream'}) + self.assertEqual(self._munch_images(ret), self.cloud.list_images()) + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_image_client') + def test_create_image_put_user_prop( + self, mock_image_client, mock_api_version): + mock_api_version.return_value = '2' + self.cloud.image_api_use_tasks = False + + mock_image_client.get.return_value = [] + self.assertEqual([], self.cloud.list_images()) + + args = {'name': '42 name', + 'container_format': 'bare', 'disk_format': u'qcow2', + 'owner_specified.shade.md5': mock.ANY, + 'owner_specified.shade.sha256': mock.ANY, + 'owner_specified.shade.object': 'images/42 name', + 'int_v': '12345', + 'xenapi_use_agent': 'False', + 'visibility': 'private', + 'min_disk': 0, 'min_ram': 0} + ret = munch.Munch(args.copy()) + ret['id'] = '42' + ret['status'] = 'success' + mock_image_client.get.return_value = [ret] + mock_image_client.post.return_value = ret + self._call_create_image( + '42 name', min_disk='0', min_ram=0, properties={'int_v': 12345}) + mock_image_client.get.assert_called_with('/images') + self.assertEqual( + self._munch_images(ret), self.cloud.list_images()) + + @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_get_file_hashes') + @mock.patch.object(shade.OpenStackCloud, '_image_client') + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + @mock.patch.object(shade.OpenStackCloud, 'swift_service') + def test_create_image_task(self, + swift_service_mock, + swift_mock, + mock_image_client, + get_file_hashes, + mock_api_version): + mock_api_version.return_value = '2' + self.cloud.image_api_use_tasks = True + + class Container(object): + name = 'image_upload_v2_test_container' + + fake_container = Container() + swift_mock.get_capabilities.return_value = { + 'swift': { + 'max_file_size': 1000 + } + } + swift_mock.put_container.return_value = fake_container + swift_mock.head_object.return_value = {} + + fake_md5 = "fake-md5" + fake_sha256 = "fake-sha256" + get_file_hashes.return_value = (fake_md5, fake_sha256) + + fake_image = munch.Munch( + id='a35e8afc-cae9-4e38-8441-2cd465f79f7b', name='name-99', + status='active', visibility='private') + + args = munch.Munch( + id='21FBD9A7-85EC-4E07-9D58-72F1ACF7CB1F', + status='success', + type='import', + result={ + 'image_id': 'a35e8afc-cae9-4e38-8441-2cd465f79f7b', + }, + ) + + # TODO(mordred): When we move this to requests_mock, we need to + # add a test that throwing a 503 response causes a retry + mock_image_client.get.side_effect = [ + [], + args, + [fake_image], + [fake_image], + [fake_image], + ] + self._call_create_image(name='name-99', + container='image_upload_v2_test_container') + args = {'header': ['x-object-meta-x-shade-md5:fake-md5', + 'x-object-meta-x-shade-sha256:fake-sha256'], + 'segment_size': 1000, + 'use_slo': True} + swift_service_mock.upload.assert_called_with( + container='image_upload_v2_test_container', + objects=mock.ANY, + options=args) + + mock_image_client.post.assert_called_with( + '/tasks', + data=dict( + type='import', input={ + 'import_from': 'image_upload_v2_test_container/name-99', + 'image_properties': {'name': 'name-99'}})) + object_path = 'image_upload_v2_test_container/name-99' + args = {'owner_specified.shade.md5': fake_md5, + 'owner_specified.shade.sha256': fake_sha256, + 'owner_specified.shade.object': object_path, + 'image_id': 'a35e8afc-cae9-4e38-8441-2cd465f79f7b'} + mock_image_client.patch.assert_called_with( + '/images/a35e8afc-cae9-4e38-8441-2cd465f79f7b', + json=sorted([ + { + u'op': u'add', + u'value': 'image_upload_v2_test_container/name-99', + u'path': u'/owner_specified.shade.object' + }, { + u'op': u'add', + u'value': 'fake-md5', + u'path': u'/owner_specified.shade.md5' + }, { + u'op': u'add', u'value': 'fake-sha256', + u'path': u'/owner_specified.shade.sha256' + }], key=operator.itemgetter('value')), + headers={ + 'Content-Type': 'application/openstack-images-v2.1-json-patch' + }) + + self.assertEqual( + self._munch_images(fake_image), self.cloud.list_images()) From bd545ceea138f03e7347ef95efae06713fba8805 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 6 Dec 2016 12:16:19 -0600 Subject: [PATCH 1142/3836] Remove caching config from test_image The caching config isn't the actual config we need. Remove it so that the refactor can be cleaner. Change-Id: I6bbc17d78e49c98394e8f391192c2dbc35889ae8 --- shade/tests/unit/test_image.py | 40 +++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index 0daf6fb77..8839bd858 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -105,10 +105,6 @@ def test_download_image_with_path(self): class TestMockImage(base.TestCase): - def setUp(self): - super(TestMockImage, self).setUp( - cloud_config_fixture='clouds_cache.yaml') - def _image_dict(self, fake_image): return self.cloud._normalize_image(meta.obj_to_dict(fake_image)) @@ -140,7 +136,11 @@ def test_create_image_put_v1(self, mock_image_client, mock_api_version): ret = munch.Munch(args.copy()) ret['id'] = '42' ret['status'] = 'success' - mock_image_client.get.return_value = [ret] + mock_image_client.get.side_effect = [ + [], + [ret], + [ret], + ] mock_image_client.post.return_value = ret mock_image_client.put.return_value = ret self._call_create_image('42 name') @@ -173,7 +173,10 @@ def test_create_image_put_v1_bad_delete( ret = munch.Munch(args.copy()) ret['id'] = '42' ret['status'] = 'success' - mock_image_client.get.return_value = [ret] + mock_image_client.get.side_effect = [ + [], + [ret], + ] mock_image_client.post.return_value = ret mock_image_client.put.side_effect = exc.OpenStackCloudHTTPError( "Some error", {}) @@ -209,7 +212,11 @@ def test_create_image_put_v2(self, mock_image_client, mock_api_version): ret = munch.Munch(args.copy()) ret['id'] = '42' ret['status'] = 'success' - mock_image_client.get.return_value = [ret] + mock_image_client.get.side_effect = [ + [], + [ret], + [ret], + ] mock_image_client.post.return_value = ret self._call_create_image('42 name', min_disk='0', min_ram=0) mock_image_client.post.assert_called_with('/images', json=args) @@ -240,7 +247,11 @@ def test_update_image_no_patch(self, mock_image_client, mock_api_version): ret = munch.Munch(args.copy()) ret['id'] = '42' ret['status'] = 'success' - mock_image_client.get.return_value = [ret] + mock_image_client.get.side_effect = [ + [], + [ret], + [ret], + ] self.cloud.update_image_properties( image=self._image_dict(ret), **{'owner_specified.shade.object': 'images/42 name'}) @@ -267,7 +278,11 @@ def test_create_image_put_v2_bad_delete( ret = munch.Munch(args.copy()) ret['id'] = '42' ret['status'] = 'success' - mock_image_client.get.return_value = [ret] + mock_image_client.get.side_effect = [ + [], + [ret], + [ret], + ] mock_image_client.post.return_value = ret mock_image_client.put.side_effect = exc.OpenStackCloudHTTPError( "Some error", {}) @@ -380,7 +395,11 @@ def test_create_image_put_protected( ret = munch.Munch(args.copy()) ret['id'] = '42' ret['status'] = 'success' - mock_image_client.get.return_value = [ret] + mock_image_client.get.side_effect = [ + [], + [ret], + [ret], + ] mock_image_client.put.return_value = ret mock_image_client.post.return_value = ret self._call_create_image( @@ -468,6 +487,7 @@ class Container(object): # TODO(mordred): When we move this to requests_mock, we need to # add a test that throwing a 503 response causes a retry mock_image_client.get.side_effect = [ + [], [], args, [fake_image], From 06ea154ec5947611865b851a8f4f5a4e4d308222 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 6 Dec 2016 13:21:27 -0600 Subject: [PATCH 1143/3836] Convert test_create_image_put_v2 to requests_mock Also, add a test for listing an empty list of images as well as to make sure that an image dict gets propertly normalized when it comes through in the request payload. There are likely some more things we can/should assert in the request history objects. Co-Authored-By: Jamie Lennox Change-Id: Iede134002cb75cb382d07b32115d56fc50a656c6 --- shade/tests/unit/test_image.py | 113 ++++++++++++++++++++++----------- 1 file changed, 76 insertions(+), 37 deletions(-) diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index 8839bd858..1a468be66 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -27,12 +27,19 @@ from shade.tests.unit import base +NO_MD5 = '93b885adfe0da089cdf634904fd59f71' +NO_SHA256 = '6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d' + + class TestImage(base.RequestsMockTestCase): def setUp(self): super(TestImage, self).setUp() self.image_id = str(uuid.uuid4()) - self.fake_search_return = {'images': [{ + self.imagefile = tempfile.NamedTemporaryFile(delete=False) + self.imagefile.write(b'\0') + self.imagefile.close() + self.fake_image_dict = { u'image_state': u'available', u'container_format': u'bare', u'min_ram': 0, @@ -55,7 +62,11 @@ def setUp(self): u'name': u'fake_image', u'checksum': u'ee36e35a297980dee1b514de9803ec6d', u'created_at': u'2016-02-10T05:03:11Z', - u'protected': False}]} + u'owner_specified.shade.md5': NO_MD5, + u'owner_specified.shade.sha256': NO_SHA256, + u'owner_specified.shade.object': 'images/fake_image', + u'protected': False} + self.fake_search_return = {'images': [self.fake_image_dict]} self.output = uuid.uuid4().bytes def test_download_image_no_output(self): @@ -102,6 +113,69 @@ def test_download_image_with_path(self): output_file.seek(0) self.assertEqual(output_file.read(), self.output) + def test_empty_list_images(self): + self.adapter.register_uri( + 'GET', 'http://image.example.com/v2/images', json={'images': []}) + self.assertEqual([], self.cloud.list_images()) + + def test_list_images(self): + self.adapter.register_uri( + 'GET', 'http://image.example.com/v2/images', + json=self.fake_search_return) + self.assertEqual( + self.cloud._normalize_images([self.fake_image_dict]), + self.cloud.list_images()) + + def test_create_image_put_v2(self): + self.cloud.image_api_use_tasks = False + + self.adapter.register_uri( + 'GET', 'http://image.example.com/v2/images', [ + dict(json={'images': []}), + dict(json=self.fake_search_return), + ]) + self.adapter.register_uri( + 'POST', 'http://image.example.com/v2/images', + json=self.fake_image_dict, + ) + self.adapter.register_uri( + 'PUT', 'http://image.example.com/v2/images/{id}/file'.format( + id=self.image_id), + request_headers={'Content-Type': 'application/octet-stream'}) + + self.cloud.create_image( + 'fake_image', self.imagefile.name, wait=True, timeout=1, + is_public=False) + + calls = [ + dict(method='GET', url='http://192.168.0.19:35357/'), + dict(method='POST', url='http://example.com/v2.0/tokens'), + dict(method='GET', url='http://image.example.com/'), + dict(method='GET', url='http://image.example.com/v2/images'), + dict(method='POST', url='http://image.example.com/v2/images'), + dict( + method='PUT', + url='http://image.example.com/v2/images/{id}/file'.format( + id=self.image_id)), + dict(method='GET', url='http://image.example.com/v2/images'), + ] + for x in range(0, len(calls)): + self.assertEqual( + calls[x]['method'], self.adapter.request_history[x].method) + self.assertEqual( + calls[x]['url'], self.adapter.request_history[x].url) + self.assertEqual( + self.adapter.request_history[4].json(), { + u'container_format': u'bare', + u'disk_format': u'qcow2', + u'name': u'fake_image', + u'owner_specified.shade.md5': NO_MD5, + u'owner_specified.shade.object': u'images/fake_image', + u'owner_specified.shade.sha256': NO_SHA256, + u'visibility': u'private' + }) + self.assertEqual(self.adapter.request_history[5].text.read(), b'\x00') + class TestMockImage(base.TestCase): @@ -193,41 +267,6 @@ def test_create_image_put_v1_bad_delete( }) mock_image_client.delete.assert_called_with('/images/42') - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, '_image_client') - def test_create_image_put_v2(self, mock_image_client, mock_api_version): - mock_api_version.return_value = '2' - self.cloud.image_api_use_tasks = False - - mock_image_client.get.return_value = [] - self.assertEqual([], self.cloud.list_images()) - - args = {'name': '42 name', - 'container_format': 'bare', 'disk_format': 'qcow2', - 'owner_specified.shade.md5': mock.ANY, - 'owner_specified.shade.sha256': mock.ANY, - 'owner_specified.shade.object': 'images/42 name', - 'visibility': 'private', - 'min_disk': 0, 'min_ram': 0} - ret = munch.Munch(args.copy()) - ret['id'] = '42' - ret['status'] = 'success' - mock_image_client.get.side_effect = [ - [], - [ret], - [ret], - ] - mock_image_client.post.return_value = ret - self._call_create_image('42 name', min_disk='0', min_ram=0) - mock_image_client.post.assert_called_with('/images', json=args) - mock_image_client.put.assert_called_with( - '/images/42/file', - headers={'Content-Type': 'application/octet-stream'}, - data=mock.ANY) - mock_image_client.get.assert_called_with('/images') - self.assertEqual( - self._munch_images(ret), self.cloud.list_images()) - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, '_image_client') def test_update_image_no_patch(self, mock_image_client, mock_api_version): From bef38e438d6c4819ffb6058d161b5a694d7e7135 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 6 Dec 2016 17:56:13 -0600 Subject: [PATCH 1144/3836] Change register_uri to use the per-method calls Turns out register_uri('GET', *) can be written get(*). Let's have that be our pattern. Change-Id: Ifbe18ac398f717c8ee8d3e8d284283fb2c4b2d11 --- shade/tests/unit/base.py | 12 ++++++------ shade/tests/unit/test_image.py | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 044bb482f..7a585f4a1 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -114,22 +114,22 @@ def setUp(self, cloud_config_fixture='clouds.yaml'): cloud_config_fixture=cloud_config_fixture) self.adapter = self.useFixture(rm_fixture.Fixture()) - self.adapter.register_uri( - 'GET', 'http://192.168.0.19:35357/', + self.adapter.get( + 'http://192.168.0.19:35357/', text=open( os.path.join( self.fixtures_directory, 'discovery.json'), 'r').read()) - self.adapter.register_uri( - 'POST', 'http://example.com/v2.0/tokens', + self.adapter.post( + 'http://example.com/v2.0/tokens', text=open( os.path.join( self.fixtures_directory, 'catalog.json'), 'r').read()) - self.adapter.register_uri( - 'GET', 'http://image.example.com/', + self.adapter.get( + 'http://image.example.com/', text=open( os.path.join( self.fixtures_directory, diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index 1a468be66..a31a5241a 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -80,19 +80,19 @@ def test_download_image_two_outputs(self): output_path='fake_path', output_file=fake_fd) def test_download_image_no_images_found(self): - self.adapter.register_uri( - 'GET', 'http://image.example.com/v2/images', + self.adapter.get( + 'http://image.example.com/v2/images', json=dict(images=[])) self.assertRaises(exc.OpenStackCloudResourceNotFound, self.cloud.download_image, 'fake_image', output_path='fake_path') def _register_image_mocks(self): - self.adapter.register_uri( - 'GET', 'http://image.example.com/v2/images', + self.adapter.get( + 'http://image.example.com/v2/images', json=self.fake_search_return) - self.adapter.register_uri( - 'GET', 'http://image.example.com/v2/images/{id}/file'.format( + self.adapter.get( + 'http://image.example.com/v2/images/{id}/file'.format( id=self.image_id), content=self.output, headers={ From 51598130b6d56f5f371d9725b6fdd9283baf759a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 8 Dec 2016 18:42:27 -0600 Subject: [PATCH 1145/3836] Replace swift capabilities call with REST This is the first step of removing swiftclient. Change-Id: I50dd5d262d8ecea02e49aeef087a13fde9604502 --- shade/_tasks.py | 5 - shade/openstackcloud.py | 24 +++- shade/tests/unit/fixtures/catalog.json | 14 ++ shade/tests/unit/test_image.py | 192 ++++++++++++------------- shade/tests/unit/test_object.py | 47 +++--- 5 files changed, 158 insertions(+), 124 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 110c50a33..be60dc4ce 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -491,11 +491,6 @@ def main(self, client): return client.swift_client.get_account(**self.args)[1] -class ObjectCapabilities(task_manager.Task): - def main(self, client): - return client.swift_client.get_capabilities(**self.args) - - class ObjectDelete(task_manager.Task): def main(self, client): return client.swift_client.delete_object(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 2f92d3fed..2412b2825 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -384,6 +384,13 @@ def _compute_client(self): self._raw_clients['compute'] = self._get_raw_client('compute') return self._raw_clients['compute'] + @property + def _object_store_client(self): + if 'object-store' not in self._raw_clients: + raw_client = self._get_raw_client('object-store') + self._raw_clients['object-store'] = raw_client + return self._raw_clients['object-store'] + @property def _image_client(self): if 'image' not in self._raw_clients: @@ -5497,7 +5504,15 @@ def _get_file_hashes(self, filename): @_utils.cache_on_arguments() def get_object_capabilities(self): - return self.manager.submit_task(_tasks.ObjectCapabilities()) + # The endpoint in the catalog has version and project-id in it + # To get capabilities, we have to disassemble and reassemble the URL + # This logic is taken from swiftclient + endpoint = urllib.parse.urlparse( + self._object_store_client.get_endpoint()) + url = "{scheme}://{netloc}/info".format( + scheme=endpoint.scheme, netloc=endpoint.netloc) + + return self._object_store_client.get(url) def get_object_segment_size(self, segment_size): """Get a segment size that will work given capabilities""" @@ -5506,15 +5521,14 @@ def get_object_segment_size(self, segment_size): min_segment_size = 0 try: caps = self.get_object_capabilities() - except swift_exceptions.ClientException as e: - if e.http_status == 404 or e.http_status == 412: + except OpenStackCloudHTTPError as e: + if e.response.status_code in (404, 412): server_max_file_size = DEFAULT_MAX_FILE_SIZE self.log.info( "Swift capabilities not supported. " "Using default max file size.") else: - raise OpenStackCloudException( - "Could not determine capabilities") + raise else: server_max_file_size = caps.get('swift', {}).get('max_file_size', 0) diff --git a/shade/tests/unit/fixtures/catalog.json b/shade/tests/unit/fixtures/catalog.json index 8db4b5412..687e832c7 100644 --- a/shade/tests/unit/fixtures/catalog.json +++ b/shade/tests/unit/fixtures/catalog.json @@ -84,6 +84,20 @@ ], "type": "identity", "name": "keystone" + }, + { + "endpoints_links": [], + "endpoints": [ + { + "adminURL": "http://object-store.example.com/v1/1c36b64c840a42cd9e9b931a369337f0", + "region": "RegionOne", + "publicURL": "http://object-store.example.com/v1/1c36b64c840a42cd9e9b931a369337f0", + "internalURL": "http://object-store.example.com/v1/1c36b64c840a42cd9e9b931a369337f0", + "id": "4deb4d0504a044a395d4480741ba628c" + } + ], + "type": "object-store", + "name": "swift" } ], "user": { diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index a31a5241a..4e62367b9 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -176,6 +176,101 @@ def test_create_image_put_v2(self): }) self.assertEqual(self.adapter.request_history[5].text.read(), b'\x00') + @mock.patch.object(shade.OpenStackCloud, '_image_client') + @mock.patch.object(shade.OpenStackCloud, 'swift_client') + @mock.patch.object(shade.OpenStackCloud, 'swift_service') + def test_create_image_task(self, + swift_service_mock, + swift_mock, + mock_image_client): + self.cloud.image_api_use_tasks = True + + self.adapter.get( + 'http://object-store.example.com/info', + json=dict( + swift={'max_file_size': 1000}, + slo={'min_segment_size': 500})) + + class Container(object): + name = 'image_upload_v2_test_container' + + fake_container = Container() + swift_mock.put_container.return_value = fake_container + swift_mock.head_object.return_value = {} + + fake_image = munch.Munch( + id='a35e8afc-cae9-4e38-8441-2cd465f79f7b', name='name-99', + status='active', visibility='private') + + args = munch.Munch( + id='21FBD9A7-85EC-4E07-9D58-72F1ACF7CB1F', + status='success', + type='import', + result={ + 'image_id': 'a35e8afc-cae9-4e38-8441-2cd465f79f7b', + }, + ) + + # TODO(mordred): When we move this to requests_mock, we need to + # add a test that throwing a 503 response causes a retry + mock_image_client.get.side_effect = [ + [], + [], + args, + [fake_image], + [fake_image], + [fake_image], + ] + self.cloud.create_image( + 'name-99', self.imagefile.name, wait=True, timeout=1, + is_public=False, container='image_upload_v2_test_container') + + args = { + 'header': [ + 'x-object-meta-x-shade-md5:{md5}'.format(md5=NO_MD5), + 'x-object-meta-x-shade-sha256:{sha}'.format(sha=NO_SHA256), + ], + 'segment_size': 1000, + 'use_slo': True} + swift_service_mock.upload.assert_called_with( + container='image_upload_v2_test_container', + objects=mock.ANY, + options=args) + + mock_image_client.post.assert_called_with( + '/tasks', + data=dict( + type='import', input={ + 'import_from': 'image_upload_v2_test_container/name-99', + 'image_properties': {'name': 'name-99'}})) + object_path = 'image_upload_v2_test_container/name-99' + args = {'owner_specified.shade.md5': NO_MD5, + 'owner_specified.shade.sha256': NO_SHA256, + 'owner_specified.shade.object': object_path, + 'image_id': 'a35e8afc-cae9-4e38-8441-2cd465f79f7b'} + mock_image_client.patch.assert_called_with( + '/images/a35e8afc-cae9-4e38-8441-2cd465f79f7b', + json=sorted([ + { + u'op': u'add', + u'value': 'image_upload_v2_test_container/name-99', + u'path': u'/owner_specified.shade.object' + }, { + u'op': u'add', + u'value': NO_MD5, + u'path': u'/owner_specified.shade.md5' + }, { + u'op': u'add', u'value': NO_SHA256, + u'path': u'/owner_specified.shade.sha256' + }], key=operator.itemgetter('value')), + headers={ + 'Content-Type': 'application/openstack-images-v2.1-json-patch' + }) + + self.assertEqual( + self.cloud._normalize_images([fake_image]), + self.cloud.list_images()) + class TestMockImage(base.TestCase): @@ -479,100 +574,3 @@ def test_create_image_put_user_prop( mock_image_client.get.assert_called_with('/images') self.assertEqual( self._munch_images(ret), self.cloud.list_images()) - - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, '_get_file_hashes') - @mock.patch.object(shade.OpenStackCloud, '_image_client') - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - @mock.patch.object(shade.OpenStackCloud, 'swift_service') - def test_create_image_task(self, - swift_service_mock, - swift_mock, - mock_image_client, - get_file_hashes, - mock_api_version): - mock_api_version.return_value = '2' - self.cloud.image_api_use_tasks = True - - class Container(object): - name = 'image_upload_v2_test_container' - - fake_container = Container() - swift_mock.get_capabilities.return_value = { - 'swift': { - 'max_file_size': 1000 - } - } - swift_mock.put_container.return_value = fake_container - swift_mock.head_object.return_value = {} - - fake_md5 = "fake-md5" - fake_sha256 = "fake-sha256" - get_file_hashes.return_value = (fake_md5, fake_sha256) - - fake_image = munch.Munch( - id='a35e8afc-cae9-4e38-8441-2cd465f79f7b', name='name-99', - status='active', visibility='private') - - args = munch.Munch( - id='21FBD9A7-85EC-4E07-9D58-72F1ACF7CB1F', - status='success', - type='import', - result={ - 'image_id': 'a35e8afc-cae9-4e38-8441-2cd465f79f7b', - }, - ) - - # TODO(mordred): When we move this to requests_mock, we need to - # add a test that throwing a 503 response causes a retry - mock_image_client.get.side_effect = [ - [], - [], - args, - [fake_image], - [fake_image], - [fake_image], - ] - self._call_create_image(name='name-99', - container='image_upload_v2_test_container') - args = {'header': ['x-object-meta-x-shade-md5:fake-md5', - 'x-object-meta-x-shade-sha256:fake-sha256'], - 'segment_size': 1000, - 'use_slo': True} - swift_service_mock.upload.assert_called_with( - container='image_upload_v2_test_container', - objects=mock.ANY, - options=args) - - mock_image_client.post.assert_called_with( - '/tasks', - data=dict( - type='import', input={ - 'import_from': 'image_upload_v2_test_container/name-99', - 'image_properties': {'name': 'name-99'}})) - object_path = 'image_upload_v2_test_container/name-99' - args = {'owner_specified.shade.md5': fake_md5, - 'owner_specified.shade.sha256': fake_sha256, - 'owner_specified.shade.object': object_path, - 'image_id': 'a35e8afc-cae9-4e38-8441-2cd465f79f7b'} - mock_image_client.patch.assert_called_with( - '/images/a35e8afc-cae9-4e38-8441-2cd465f79f7b', - json=sorted([ - { - u'op': u'add', - u'value': 'image_upload_v2_test_container/name-99', - u'path': u'/owner_specified.shade.object' - }, { - u'op': u'add', - u'value': 'fake-md5', - u'path': u'/owner_specified.shade.md5' - }, { - u'op': u'add', u'value': 'fake-sha256', - u'path': u'/owner_specified.shade.sha256' - }], key=operator.itemgetter('value')), - headers={ - 'Content-Type': 'application/openstack-images-v2.1-json-patch' - }) - - self.assertEqual( - self._munch_images(fake_image), self.cloud.list_images()) diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index 6dc21e808..28702bd96 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -54,23 +54,6 @@ def test_swift_service_no_endpoint(self, endpoint_mock): self.assertIn( 'Error constructing swift client', str(e)) - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_get_object_segment_size(self, swift_mock): - swift_mock.get_capabilities.return_value = { - 'swift': {'max_file_size': 1000}, - 'slo': {'min_segment_size': 500}} - self.assertEqual(500, self.cloud.get_object_segment_size(400)) - self.assertEqual(900, self.cloud.get_object_segment_size(900)) - self.assertEqual(1000, self.cloud.get_object_segment_size(1000)) - self.assertEqual(1000, self.cloud.get_object_segment_size(1100)) - - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_get_object_segment_size_http_412(self, swift_mock): - swift_mock.get_capabilities.side_effect = swift_exc.ClientException( - "Precondition failed", http_status=412) - self.assertEqual(shade.openstackcloud.DEFAULT_OBJECT_SEGMENT_SIZE, - self.cloud.get_object_segment_size(None)) - @mock.patch.object(shade.OpenStackCloud, 'swift_client') def test_create_container(self, mock_swift): """Test creating a (private) container""" @@ -326,3 +309,33 @@ def test_get_object_exception(self, mock_swift): self.assertRaises(shade.OpenStackCloudException, self.cloud.get_object, container_name, object_name) + + +class TestRESTObject(base.RequestsMockTestCase): + + def test_get_object_segment_size(self): + self.adapter.get( + 'http://object-store.example.com/info', + json=dict( + swift={'max_file_size': 1000}, + slo={'min_segment_size': 500})) + self.assertEqual(500, self.cloud.get_object_segment_size(400)) + self.assertEqual(900, self.cloud.get_object_segment_size(900)) + self.assertEqual(1000, self.cloud.get_object_segment_size(1000)) + self.assertEqual(1000, self.cloud.get_object_segment_size(1100)) + + def test_get_object_segment_size_http_404(self): + self.adapter.get( + 'http://object-store.example.com/info', + status_code=404, + reason='Not Found') + self.assertEqual(shade.openstackcloud.DEFAULT_OBJECT_SEGMENT_SIZE, + self.cloud.get_object_segment_size(None)) + + def test_get_object_segment_size_http_412(self): + self.adapter.get( + 'http://object-store.example.com/info', + status_code=412, + reason='Precondition failed') + self.assertEqual(shade.openstackcloud.DEFAULT_OBJECT_SEGMENT_SIZE, + self.cloud.get_object_segment_size(None)) From 766297d010f5631c241e770b94ddff07b23538de Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 9 Dec 2016 11:28:08 -0600 Subject: [PATCH 1146/3836] Honor image_api_version when doing version discovery We want to grab the latest version available - unless the user has configured themselves to request a previous version. Enter Cloud Suite is an example of a cloud that has v2 but v1 must be used for uploads. Change-Id: I1603fd567a59b5c123ab2f84c7a9571381245e97 --- shade/openstackcloud.py | 36 ++++++++- shade/tests/unit/base.py | 10 ++- .../tests/unit/fixtures/image-version-v1.json | 24 ++++++ .../tests/unit/fixtures/image-version-v2.json | 44 +++++++++++ shade/tests/unit/test_image.py | 74 +++++++++++++++++++ 5 files changed, 181 insertions(+), 7 deletions(-) create mode 100644 shade/tests/unit/fixtures/image-version-v1.json create mode 100644 shade/tests/unit/fixtures/image-version-v2.json diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 2412b2825..0c167da36 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -394,14 +394,42 @@ def _object_store_client(self): @property def _image_client(self): if 'image' not in self._raw_clients: + # Get configured api version for downgrades + config_version = self.cloud_config.get_api_version('image') image_client = self._get_raw_client('image') try: # Version discovery versions = image_client.get('/') - current_version = [ - version for version in versions - if version['status'] == 'CURRENT'][0] - image_url = current_version['links'][0]['href'] + api_version = None + if config_version.startswith('1'): + api_version = [ + version for version in versions + if version['id'] in ('v1.0', 'v1.1')] + if api_version: + api_version = api_version[0] + if not api_version: + api_version = [ + version for version in versions + if version['status'] == 'CURRENT'][0] + + image_url = api_version['links'][0]['href'] + # If we detect a different version that was configured, + # set the version in occ because we have logic elsewhere + # that is different depending on which version we're using + warning_msg = None + if (config_version.startswith('2') + and api_version['id'].startswith('v1')): + self.cloud_config.config['image_api_version'] = '1' + warning_msg = ( + 'image_api_version is 2 but only 1 is available.') + elif (config_version.startswith('1') + and api_version['id'].startswith('v2')): + self.cloud_config.config['image_api_version'] = '2' + warning_msg = ( + 'image_api_version is 1 but only 2 is available.') + if warning_msg: + self.log.debug(warning_msg) + warnings.warn(warning_msg) except (keystoneauth1.exceptions.connection.ConnectFailure, OpenStackCloudURINotFound) as e: # A 404 or a connection error is a likely thing to get diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 7a585f4a1..8d1ea025d 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -108,7 +108,11 @@ def setUp(self, cloud_config_fixture='clouds.yaml'): class RequestsMockTestCase(BaseTestCase): - def setUp(self, cloud_config_fixture='clouds.yaml'): + def setUp( + self, + cloud_config_fixture='clouds.yaml', + discovery_json='discovery.json', + image_version_json='image-version.json'): super(RequestsMockTestCase, self).setUp( cloud_config_fixture=cloud_config_fixture) @@ -119,7 +123,7 @@ def setUp(self, cloud_config_fixture='clouds.yaml'): text=open( os.path.join( self.fixtures_directory, - 'discovery.json'), + discovery_json), 'r').read()) self.adapter.post( 'http://example.com/v2.0/tokens', @@ -133,7 +137,7 @@ def setUp(self, cloud_config_fixture='clouds.yaml'): text=open( os.path.join( self.fixtures_directory, - 'image-version.json'), + image_version_json), 'r').read()) test_cloud = os.environ.get('SHADE_OS_CLOUD', '_test_cloud_') diff --git a/shade/tests/unit/fixtures/image-version-v1.json b/shade/tests/unit/fixtures/image-version-v1.json new file mode 100644 index 000000000..874cbbaa0 --- /dev/null +++ b/shade/tests/unit/fixtures/image-version-v1.json @@ -0,0 +1,24 @@ +{ + "versions": [ + { + "status": "CURRENT", + "id": "v1.1", + "links": [ + { + "href": "https://image.example.com/v1/", + "rel": "self" + } + ] + }, + { + "status": "SUPPORTED", + "id": "v1.0", + "links": [ + { + "href": "https://image.example.com/v1/", + "rel": "self" + } + ] + } + ] +} diff --git a/shade/tests/unit/fixtures/image-version-v2.json b/shade/tests/unit/fixtures/image-version-v2.json new file mode 100644 index 000000000..15874b87c --- /dev/null +++ b/shade/tests/unit/fixtures/image-version-v2.json @@ -0,0 +1,44 @@ +{ + "versions": [ + { + "status": "CURRENT", + "id": "v2.3", + "links": [ + { + "href": "https://image.example.com/v2/", + "rel": "self" + } + ] + }, + { + "status": "SUPPORTED", + "id": "v2.2", + "links": [ + { + "href": "https://image.example.com/v2/", + "rel": "self" + } + ] + }, + { + "status": "SUPPORTED", + "id": "v2.1", + "links": [ + { + "href": "https://image.example.com/v2/", + "rel": "self" + } + ] + }, + { + "status": "SUPPORTED", + "id": "v2.0", + "links": [ + { + "href": "https://image.example.com/v2/", + "rel": "self" + } + ] + } + ] +} diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index 4e62367b9..4a493be10 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -69,6 +69,26 @@ def setUp(self): self.fake_search_return = {'images': [self.fake_image_dict]} self.output = uuid.uuid4().bytes + def test_config_v1(self): + self.cloud.cloud_config.config['image_api_version'] = '1' + # We override the scheme of the endpoint with the scheme of the service + # because glance has a bug where it doesn't return https properly. + self.assertEqual( + 'http://image.example.com/v1/', + self.cloud._image_client.get_endpoint()) + self.assertEqual( + '1', self.cloud_config.get_api_version('image')) + + def test_config_v2(self): + self.cloud.cloud_config.config['image_api_version'] = '2' + # We override the scheme of the endpoint with the scheme of the service + # because glance has a bug where it doesn't return https properly. + self.assertEqual( + 'http://image.example.com/v2/', + self.cloud._image_client.get_endpoint()) + self.assertEqual( + '2', self.cloud_config.get_api_version('image')) + def test_download_image_no_output(self): self.assertRaises(exc.OpenStackCloudException, self.cloud.download_image, 'fake_image') @@ -574,3 +594,57 @@ def test_create_image_put_user_prop( mock_image_client.get.assert_called_with('/images') self.assertEqual( self._munch_images(ret), self.cloud.list_images()) + + +class TestImageV1Only(base.RequestsMockTestCase): + + def setUp(self): + super(TestImageV1Only, self).setUp( + image_version_json='image-version-v1.json') + + def test_config_v1(self): + self.cloud.cloud_config.config['image_api_version'] = '1' + # We override the scheme of the endpoint with the scheme of the service + # because glance has a bug where it doesn't return https properly. + self.assertEqual( + 'http://image.example.com/v1/', + self.cloud._image_client.get_endpoint()) + self.assertEqual( + '1', self.cloud_config.get_api_version('image')) + + def test_config_v2(self): + self.cloud.cloud_config.config['image_api_version'] = '2' + # We override the scheme of the endpoint with the scheme of the service + # because glance has a bug where it doesn't return https properly. + self.assertEqual( + 'http://image.example.com/v1/', + self.cloud._image_client.get_endpoint()) + self.assertEqual( + '1', self.cloud_config.get_api_version('image')) + + +class TestImageV2Only(base.RequestsMockTestCase): + + def setUp(self): + super(TestImageV2Only, self).setUp( + image_version_json='image-version-v2.json') + + def test_config_v1(self): + self.cloud.cloud_config.config['image_api_version'] = '1' + # We override the scheme of the endpoint with the scheme of the service + # because glance has a bug where it doesn't return https properly. + self.assertEqual( + 'http://image.example.com/v2/', + self.cloud._image_client.get_endpoint()) + self.assertEqual( + '2', self.cloud_config.get_api_version('image')) + + def test_config_v2(self): + self.cloud.cloud_config.config['image_api_version'] = '2' + # We override the scheme of the endpoint with the scheme of the service + # because glance has a bug where it doesn't return https properly. + self.assertEqual( + 'http://image.example.com/v2/', + self.cloud._image_client.get_endpoint()) + self.assertEqual( + '2', self.cloud_config.get_api_version('image')) From a868c0a81bc687fc4c3be8ddade2fe8ac4aec47e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 9 Dec 2016 11:39:14 -0600 Subject: [PATCH 1147/3836] Change fixtures to use https It's not REAL traffic, but shade upgrades connections to https in some places where half of the config erroneously shows http, so it's good to have the fixtures look correct. Change-Id: I4f47981fd114cc0bd622338174f764b374fc4047 --- shade/tests/unit/base.py | 4 +- shade/tests/unit/fixtures/catalog.json | 36 ++++++++-------- shade/tests/unit/fixtures/discovery.json | 4 +- .../tests/unit/fixtures/image-version-v1.json | 4 +- .../tests/unit/fixtures/image-version-v2.json | 8 ++-- shade/tests/unit/fixtures/image-version.json | 12 +++--- shade/tests/unit/test_image.py | 42 +++++++++---------- shade/tests/unit/test_object.py | 6 +-- 8 files changed, 58 insertions(+), 58 deletions(-) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 8d1ea025d..3848a4579 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -126,14 +126,14 @@ def setUp( discovery_json), 'r').read()) self.adapter.post( - 'http://example.com/v2.0/tokens', + 'https://example.com/v2.0/tokens', text=open( os.path.join( self.fixtures_directory, 'catalog.json'), 'r').read()) self.adapter.get( - 'http://image.example.com/', + 'https://image.example.com/', text=open( os.path.join( self.fixtures_directory, diff --git a/shade/tests/unit/fixtures/catalog.json b/shade/tests/unit/fixtures/catalog.json index 687e832c7..b9a589eb6 100644 --- a/shade/tests/unit/fixtures/catalog.json +++ b/shade/tests/unit/fixtures/catalog.json @@ -19,10 +19,10 @@ "endpoints_links": [], "endpoints": [ { - "adminURL": "http://compute.example.com/v2.1/1c36b64c840a42cd9e9b931a369337f0", + "adminURL": "https://compute.example.com/v2.1/1c36b64c840a42cd9e9b931a369337f0", "region": "RegionOne", - "publicURL": "http://compute.example.com/v2.1/1c36b64c840a42cd9e9b931a369337f0", - "internalURL": "http://compute.example.com/v2.1/1c36b64c840a42cd9e9b931a369337f0", + "publicURL": "https://compute.example.com/v2.1/1c36b64c840a42cd9e9b931a369337f0", + "internalURL": "https://compute.example.com/v2.1/1c36b64c840a42cd9e9b931a369337f0", "id": "32466f357f3545248c47471ca51b0d3a" } ], @@ -33,10 +33,10 @@ "endpoints_links": [], "endpoints": [ { - "adminURL": "http://volume.example.com/v2/1c36b64c840a42cd9e9b931a369337f0", + "adminURL": "https://volume.example.com/v2/1c36b64c840a42cd9e9b931a369337f0", "region": "RegionOne", - "publicURL": "http://volume.example.com/v2/1c36b64c840a42cd9e9b931a369337f0", - "internalURL": "http://volume.example.com/v2/1c36b64c840a42cd9e9b931a369337f0", + "publicURL": "https://volume.example.com/v2/1c36b64c840a42cd9e9b931a369337f0", + "internalURL": "https://volume.example.com/v2/1c36b64c840a42cd9e9b931a369337f0", "id": "1e875ca2225b408bbf3520a1b8e1a537" } ], @@ -47,10 +47,10 @@ "endpoints_links": [], "endpoints": [ { - "adminURL": "http://image.example.com", + "adminURL": "https://image.example.com", "region": "RegionOne", - "publicURL": "http://image.example.com", - "internalURL": "http://image.example.com", + "publicURL": "https://image.example.com", + "internalURL": "https://image.example.com", "id": "5a64de3c4a614d8d8f8d1ba3dee5f45f" } ], @@ -61,10 +61,10 @@ "endpoints_links": [], "endpoints": [ { - "adminURL": "http://volume.example.com/v1/1c36b64c840a42cd9e9b931a369337f0", + "adminURL": "https://volume.example.com/v1/1c36b64c840a42cd9e9b931a369337f0", "region": "RegionOne", - "publicURL": "http://volume.example.com/v1/1c36b64c840a42cd9e9b931a369337f0", - "internalURL": "http://volume.example.com/v1/1c36b64c840a42cd9e9b931a369337f0", + "publicURL": "https://volume.example.com/v1/1c36b64c840a42cd9e9b931a369337f0", + "internalURL": "https://volume.example.com/v1/1c36b64c840a42cd9e9b931a369337f0", "id": "3d15fdfc7d424f3c8923324417e1a3d1" } ], @@ -75,10 +75,10 @@ "endpoints_links": [], "endpoints": [ { - "adminURL": "http://identity.example.com/v2.0", + "adminURL": "https://identity.example.com/v2.0", "region": "RegionOne", - "publicURL": "http://identity.example.comv2.0", - "internalURL": "http://identity.example.comv2.0", + "publicURL": "https://identity.example.comv2.0", + "internalURL": "https://identity.example.comv2.0", "id": "4deb4d0504a044a395d4480741ba628c" } ], @@ -89,10 +89,10 @@ "endpoints_links": [], "endpoints": [ { - "adminURL": "http://object-store.example.com/v1/1c36b64c840a42cd9e9b931a369337f0", + "adminURL": "https://object-store.example.com/v1/1c36b64c840a42cd9e9b931a369337f0", "region": "RegionOne", - "publicURL": "http://object-store.example.com/v1/1c36b64c840a42cd9e9b931a369337f0", - "internalURL": "http://object-store.example.com/v1/1c36b64c840a42cd9e9b931a369337f0", + "publicURL": "https://object-store.example.com/v1/1c36b64c840a42cd9e9b931a369337f0", + "internalURL": "https://object-store.example.com/v1/1c36b64c840a42cd9e9b931a369337f0", "id": "4deb4d0504a044a395d4480741ba628c" } ], diff --git a/shade/tests/unit/fixtures/discovery.json b/shade/tests/unit/fixtures/discovery.json index 2642d1509..e61f812d0 100644 --- a/shade/tests/unit/fixtures/discovery.json +++ b/shade/tests/unit/fixtures/discovery.json @@ -13,7 +13,7 @@ "id": "v3.6", "links": [ { - "href": "http://example.com/v3/", + "href": "https://example.com/v3/", "rel": "self" } ] @@ -30,7 +30,7 @@ "id": "v2.0", "links": [ { - "href": "http://example.com/v2.0/", + "href": "https://example.com/v2.0/", "rel": "self" }, { diff --git a/shade/tests/unit/fixtures/image-version-v1.json b/shade/tests/unit/fixtures/image-version-v1.json index 874cbbaa0..60b0a3bd3 100644 --- a/shade/tests/unit/fixtures/image-version-v1.json +++ b/shade/tests/unit/fixtures/image-version-v1.json @@ -5,7 +5,7 @@ "id": "v1.1", "links": [ { - "href": "https://image.example.com/v1/", + "href": "http://image.example.com/v1/", "rel": "self" } ] @@ -15,7 +15,7 @@ "id": "v1.0", "links": [ { - "href": "https://image.example.com/v1/", + "href": "http://image.example.com/v1/", "rel": "self" } ] diff --git a/shade/tests/unit/fixtures/image-version-v2.json b/shade/tests/unit/fixtures/image-version-v2.json index 15874b87c..399a53aa9 100644 --- a/shade/tests/unit/fixtures/image-version-v2.json +++ b/shade/tests/unit/fixtures/image-version-v2.json @@ -5,7 +5,7 @@ "id": "v2.3", "links": [ { - "href": "https://image.example.com/v2/", + "href": "http://image.example.com/v2/", "rel": "self" } ] @@ -15,7 +15,7 @@ "id": "v2.2", "links": [ { - "href": "https://image.example.com/v2/", + "href": "http://image.example.com/v2/", "rel": "self" } ] @@ -25,7 +25,7 @@ "id": "v2.1", "links": [ { - "href": "https://image.example.com/v2/", + "href": "http://image.example.com/v2/", "rel": "self" } ] @@ -35,7 +35,7 @@ "id": "v2.0", "links": [ { - "href": "https://image.example.com/v2/", + "href": "http://image.example.com/v2/", "rel": "self" } ] diff --git a/shade/tests/unit/fixtures/image-version.json b/shade/tests/unit/fixtures/image-version.json index e32f43a72..bd688ee3b 100644 --- a/shade/tests/unit/fixtures/image-version.json +++ b/shade/tests/unit/fixtures/image-version.json @@ -5,7 +5,7 @@ "id": "v2.3", "links": [ { - "href": "https://image.example.com/v2/", + "href": "http://image.example.com/v2/", "rel": "self" } ] @@ -15,7 +15,7 @@ "id": "v2.2", "links": [ { - "href": "https://image.example.com/v2/", + "href": "http://image.example.com/v2/", "rel": "self" } ] @@ -25,7 +25,7 @@ "id": "v2.1", "links": [ { - "href": "https://image.example.com/v2/", + "href": "http://image.example.com/v2/", "rel": "self" } ] @@ -35,7 +35,7 @@ "id": "v2.0", "links": [ { - "href": "https://image.example.com/v2/", + "href": "http://image.example.com/v2/", "rel": "self" } ] @@ -45,7 +45,7 @@ "id": "v1.1", "links": [ { - "href": "https://image.example.com/v1/", + "href": "http://image.example.com/v1/", "rel": "self" } ] @@ -55,7 +55,7 @@ "id": "v1.0", "links": [ { - "href": "https://image.example.com/v1/", + "href": "http://image.example.com/v1/", "rel": "self" } ] diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index 4a493be10..45b2c25ec 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -74,7 +74,7 @@ def test_config_v1(self): # We override the scheme of the endpoint with the scheme of the service # because glance has a bug where it doesn't return https properly. self.assertEqual( - 'http://image.example.com/v1/', + 'https://image.example.com/v1/', self.cloud._image_client.get_endpoint()) self.assertEqual( '1', self.cloud_config.get_api_version('image')) @@ -84,7 +84,7 @@ def test_config_v2(self): # We override the scheme of the endpoint with the scheme of the service # because glance has a bug where it doesn't return https properly. self.assertEqual( - 'http://image.example.com/v2/', + 'https://image.example.com/v2/', self.cloud._image_client.get_endpoint()) self.assertEqual( '2', self.cloud_config.get_api_version('image')) @@ -101,7 +101,7 @@ def test_download_image_two_outputs(self): def test_download_image_no_images_found(self): self.adapter.get( - 'http://image.example.com/v2/images', + 'https://image.example.com/v2/images', json=dict(images=[])) self.assertRaises(exc.OpenStackCloudResourceNotFound, self.cloud.download_image, 'fake_image', @@ -109,10 +109,10 @@ def test_download_image_no_images_found(self): def _register_image_mocks(self): self.adapter.get( - 'http://image.example.com/v2/images', + 'https://image.example.com/v2/images', json=self.fake_search_return) self.adapter.get( - 'http://image.example.com/v2/images/{id}/file'.format( + 'https://image.example.com/v2/images/{id}/file'.format( id=self.image_id), content=self.output, headers={ @@ -135,12 +135,12 @@ def test_download_image_with_path(self): def test_empty_list_images(self): self.adapter.register_uri( - 'GET', 'http://image.example.com/v2/images', json={'images': []}) + 'GET', 'https://image.example.com/v2/images', json={'images': []}) self.assertEqual([], self.cloud.list_images()) def test_list_images(self): self.adapter.register_uri( - 'GET', 'http://image.example.com/v2/images', + 'GET', 'https://image.example.com/v2/images', json=self.fake_search_return) self.assertEqual( self.cloud._normalize_images([self.fake_image_dict]), @@ -150,16 +150,16 @@ def test_create_image_put_v2(self): self.cloud.image_api_use_tasks = False self.adapter.register_uri( - 'GET', 'http://image.example.com/v2/images', [ + 'GET', 'https://image.example.com/v2/images', [ dict(json={'images': []}), dict(json=self.fake_search_return), ]) self.adapter.register_uri( - 'POST', 'http://image.example.com/v2/images', + 'POST', 'https://image.example.com/v2/images', json=self.fake_image_dict, ) self.adapter.register_uri( - 'PUT', 'http://image.example.com/v2/images/{id}/file'.format( + 'PUT', 'https://image.example.com/v2/images/{id}/file'.format( id=self.image_id), request_headers={'Content-Type': 'application/octet-stream'}) @@ -169,15 +169,15 @@ def test_create_image_put_v2(self): calls = [ dict(method='GET', url='http://192.168.0.19:35357/'), - dict(method='POST', url='http://example.com/v2.0/tokens'), - dict(method='GET', url='http://image.example.com/'), - dict(method='GET', url='http://image.example.com/v2/images'), - dict(method='POST', url='http://image.example.com/v2/images'), + dict(method='POST', url='https://example.com/v2.0/tokens'), + dict(method='GET', url='https://image.example.com/'), + dict(method='GET', url='https://image.example.com/v2/images'), + dict(method='POST', url='https://image.example.com/v2/images'), dict( method='PUT', - url='http://image.example.com/v2/images/{id}/file'.format( + url='https://image.example.com/v2/images/{id}/file'.format( id=self.image_id)), - dict(method='GET', url='http://image.example.com/v2/images'), + dict(method='GET', url='https://image.example.com/v2/images'), ] for x in range(0, len(calls)): self.assertEqual( @@ -206,7 +206,7 @@ def test_create_image_task(self, self.cloud.image_api_use_tasks = True self.adapter.get( - 'http://object-store.example.com/info', + 'https://object-store.example.com/info', json=dict( swift={'max_file_size': 1000}, slo={'min_segment_size': 500})) @@ -607,7 +607,7 @@ def test_config_v1(self): # We override the scheme of the endpoint with the scheme of the service # because glance has a bug where it doesn't return https properly. self.assertEqual( - 'http://image.example.com/v1/', + 'https://image.example.com/v1/', self.cloud._image_client.get_endpoint()) self.assertEqual( '1', self.cloud_config.get_api_version('image')) @@ -617,7 +617,7 @@ def test_config_v2(self): # We override the scheme of the endpoint with the scheme of the service # because glance has a bug where it doesn't return https properly. self.assertEqual( - 'http://image.example.com/v1/', + 'https://image.example.com/v1/', self.cloud._image_client.get_endpoint()) self.assertEqual( '1', self.cloud_config.get_api_version('image')) @@ -634,7 +634,7 @@ def test_config_v1(self): # We override the scheme of the endpoint with the scheme of the service # because glance has a bug where it doesn't return https properly. self.assertEqual( - 'http://image.example.com/v2/', + 'https://image.example.com/v2/', self.cloud._image_client.get_endpoint()) self.assertEqual( '2', self.cloud_config.get_api_version('image')) @@ -644,7 +644,7 @@ def test_config_v2(self): # We override the scheme of the endpoint with the scheme of the service # because glance has a bug where it doesn't return https properly. self.assertEqual( - 'http://image.example.com/v2/', + 'https://image.example.com/v2/', self.cloud._image_client.get_endpoint()) self.assertEqual( '2', self.cloud_config.get_api_version('image')) diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index 28702bd96..1c61697bf 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -315,7 +315,7 @@ class TestRESTObject(base.RequestsMockTestCase): def test_get_object_segment_size(self): self.adapter.get( - 'http://object-store.example.com/info', + 'https://object-store.example.com/info', json=dict( swift={'max_file_size': 1000}, slo={'min_segment_size': 500})) @@ -326,7 +326,7 @@ def test_get_object_segment_size(self): def test_get_object_segment_size_http_404(self): self.adapter.get( - 'http://object-store.example.com/info', + 'https://object-store.example.com/info', status_code=404, reason='Not Found') self.assertEqual(shade.openstackcloud.DEFAULT_OBJECT_SEGMENT_SIZE, @@ -334,7 +334,7 @@ def test_get_object_segment_size_http_404(self): def test_get_object_segment_size_http_412(self): self.adapter.get( - 'http://object-store.example.com/info', + 'https://object-store.example.com/info', status_code=412, reason='Precondition failed') self.assertEqual(shade.openstackcloud.DEFAULT_OBJECT_SEGMENT_SIZE, From ecce72199a8c9f0f333715419d572444d5b9fc90 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 12 Dec 2016 11:09:04 -0600 Subject: [PATCH 1148/3836] Add test to trap for missing services Recently when there was an issue with the magnum devstack plugin causing the shade gate to not have swift, we didn't notice except through the ansible tests. That's because we have a bunch of has_service checks in the tests themselves to deal with different configs. Unfortunately, that makes it easy to fail open. Put in a test, along with changes to devstack-gate jobs, to throw errors if services do not show up that should. Depends-On: I2433c7bced6c8ca785634056de45ddf624031509 Change-Id: I16f477c405583b315fff24929d6c7b2ca4f2eae3 --- shade/tests/functional/test_devstack.py | 42 +++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 shade/tests/functional/test_devstack.py diff --git a/shade/tests/functional/test_devstack.py b/shade/tests/functional/test_devstack.py new file mode 100644 index 000000000..e24b97376 --- /dev/null +++ b/shade/tests/functional/test_devstack.py @@ -0,0 +1,42 @@ +# Copyright (c) 2016 Red Hat, 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. + +""" +test_devstack +------------- + +Throw errors if we do not actually detect the services we're supposed to. +""" +import os + +from testscenarios import load_tests_apply_scenarios as load_tests # noqa + +from shade.tests.functional import base + + +class TestDevstack(base.BaseFunctionalTestCase): + + scenarios = [ + ('designate', dict(env='DESIGNATE', service='dns')), + ('heat', dict(env='HEAT', service='orchestration')), + ('magnum', dict(env='MAGNUM', service='container')), + ('neutron', dict(env='NEUTRON', service='network')), + ('swift', dict(env='SWIFT', service='object-store')), + ] + + def test_has_service(self): + if os.environ.get('SHADE_HAS_{env}'.format(env=self.env), '0') == '1': + self.assertTrue(self.demo_cloud.has_service(self.service)) From 65f3d49cf75e4496e4420fef4c0537f9f43f35f3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 13 Dec 2016 07:45:12 -0600 Subject: [PATCH 1149/3836] Add new attributes to floating ips There are four attributes that show up by default in devstack installs. They come from the timestamp and standard attributes extensions, so it's possible they may not always be present. But we can make them always be present. Change-Id: I1a05ef735c24600821856c6ec36df11e981b3d36 --- doc/source/model.rst | 4 ++++ .../new-floating-attributes-213cdf5681d337e1.yaml | 4 ++++ shade/_normalize.py | 11 +++++++++++ 3 files changed, 19 insertions(+) create mode 100644 releasenotes/notes/new-floating-attributes-213cdf5681d337e1.yaml diff --git a/doc/source/model.rst b/doc/source/model.rst index 508d4a54f..0f5f2aff9 100644 --- a/doc/source/model.rst +++ b/doc/source/model.rst @@ -202,6 +202,7 @@ A Floating IP from Neutron or Nova FloatingIP = dict( location=Location(), id=str(), + description=str(), attached=bool(), fixed_ip_address=str() or None, floating_ip_address=str() or None, @@ -209,6 +210,9 @@ A Floating IP from Neutron or Nova port=str() or None, router=str(), status=str(), + created_at=str() or None, + updated_at=str() or None, + revision_number=int() or None, properties=dict()) Project diff --git a/releasenotes/notes/new-floating-attributes-213cdf5681d337e1.yaml b/releasenotes/notes/new-floating-attributes-213cdf5681d337e1.yaml new file mode 100644 index 000000000..61f4ec1db --- /dev/null +++ b/releasenotes/notes/new-floating-attributes-213cdf5681d337e1.yaml @@ -0,0 +1,4 @@ +--- +features: + - Added support for created_at, updated_at, description + and revision_number attributes for floating ips. diff --git a/shade/_normalize.py b/shade/_normalize.py index 944df2d52..95b00bf12 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -468,6 +468,13 @@ def _normalize_floating_ip(self, ip): router_id = ip.pop('router_id', None) id = ip.pop('id') port_id = ip.pop('port_id', None) + created_at = ip.pop('created_at', None) + updated_at = ip.pop('updated_at', None) + # Note - description may not always be on the underlying cloud. + # Normalizing it here is easy - what do we do when people want to + # set a description? + description = ip.pop('description', '') + revision_number = ip.pop('revision_number', None) if self._use_neutron_floating(): attached = bool(port_id) @@ -487,6 +494,10 @@ def _normalize_floating_ip(self, ip): port=port_id, router=router_id, status=status, + created_at=created_at, + updated_at=updated_at, + description=description, + revision_number=revision_number, properties=ip.copy(), ) # Backwards compat From 8eb62897a993c8d91f7d2fe8792ff789ab980513 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 13 Dec 2016 12:25:12 -0600 Subject: [PATCH 1150/3836] Skip volume backup tests on clouds without swift It's also technically possible for a cloud to configure itself to use NFS for volume backups, but it doesn't seem anyone does, and also isn't how devstack is configured. Change-Id: I76ada67272fb16c01888fd25904e76bf9f7e3970 --- shade/tests/functional/test_volume_backup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shade/tests/functional/test_volume_backup.py b/shade/tests/functional/test_volume_backup.py index 5ea4ef0fd..09f678590 100644 --- a/shade/tests/functional/test_volume_backup.py +++ b/shade/tests/functional/test_volume_backup.py @@ -23,6 +23,9 @@ def setUp(self): if not self.demo_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') + if not self.demo_cloud.has_service('object-store'): + self.skipTest('volume backups require swift') + def test_create_get_delete_volume_backup(self): volume = self.demo_cloud.create_volume( display_name=self.getUniqueString(), size=1) From 532cab320050a9d67862d68b404736335e1be02b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 13 Dec 2016 15:17:35 -0600 Subject: [PATCH 1151/3836] Collapse base classes in test_image Turns out we don't need a separate test class for REST and client changes. Collapsing them means we can have mixed tests (like we already do for testing tasks) and we can follow up with patches here to just replace the mocks. Change-Id: I1fd9691dea4db79fd997e231e9d46cae1885c5f7 --- shade/tests/unit/test_image.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index 45b2c25ec..0265bf39d 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -291,9 +291,6 @@ class Container(object): self.cloud._normalize_images([fake_image]), self.cloud.list_images()) - -class TestMockImage(base.TestCase): - def _image_dict(self, fake_image): return self.cloud._normalize_image(meta.obj_to_dict(fake_image)) From 8c0ea00215e12ff32daad1a2664fddd14b89b838 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 13 Dec 2016 16:06:11 -0600 Subject: [PATCH 1152/3836] Convert glance parts of task test to requests_mock Remove the mocking of _image_client and go straight for requests_mock. It's worth noting doing this caught a bug in the POST to /tasks where we were using data= instead of json=. Yay tests. Also - we've added a failure in the task poll loop. The first GET for the task status returns a 503 which is then appropriately skipped. Change-Id: Ia50333d8a1c62b68e89f08aafe4b7e6caee8ef5a --- shade/openstackcloud.py | 2 +- shade/tests/unit/test_image.py | 109 +++++++++++++++++++++------------ 2 files changed, 72 insertions(+), 39 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 0c167da36..f407a5302 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3337,7 +3337,7 @@ def _upload_image_task( import_from='{container}/{name}'.format( container=container, name=name), image_properties=dict(name=name))) - glance_task = self._image_client.post('/tasks', data=task_args) + glance_task = self._image_client.post('/tasks', json=task_args) self.list_images.invalidate(self) if wait: image_id = None diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index 0265bf39d..ef2789180 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -196,13 +196,11 @@ def test_create_image_put_v2(self): }) self.assertEqual(self.adapter.request_history[5].text.read(), b'\x00') - @mock.patch.object(shade.OpenStackCloud, '_image_client') @mock.patch.object(shade.OpenStackCloud, 'swift_client') @mock.patch.object(shade.OpenStackCloud, 'swift_service') def test_create_image_task(self, swift_service_mock, - swift_mock, - mock_image_client): + swift_mock): self.cloud.image_api_use_tasks = True self.adapter.get( @@ -218,29 +216,44 @@ class Container(object): swift_mock.put_container.return_value = fake_container swift_mock.head_object.return_value = {} - fake_image = munch.Munch( - id='a35e8afc-cae9-4e38-8441-2cd465f79f7b', name='name-99', - status='active', visibility='private') - - args = munch.Munch( - id='21FBD9A7-85EC-4E07-9D58-72F1ACF7CB1F', + task_id = str(uuid.uuid4()) + args = dict( + id=task_id, status='success', type='import', result={ - 'image_id': 'a35e8afc-cae9-4e38-8441-2cd465f79f7b', + 'image_id': self.image_id, }, ) - # TODO(mordred): When we move this to requests_mock, we need to - # add a test that throwing a 503 response causes a retry - mock_image_client.get.side_effect = [ - [], - [], - args, - [fake_image], - [fake_image], - [fake_image], - ] + image_no_checksums = self.fake_image_dict.copy() + del(image_no_checksums['owner_specified.shade.md5']) + del(image_no_checksums['owner_specified.shade.sha256']) + del(image_no_checksums['owner_specified.shade.object']) + + self.adapter.register_uri( + 'GET', 'https://image.example.com/v2/images', [ + dict(json={'images': []}), + dict(json={'images': []}), + dict(json={'images': [image_no_checksums]}), + dict(json=self.fake_search_return), + ]) + self.adapter.register_uri( + 'POST', 'https://image.example.com/v2/tasks', + json=args) + self.adapter.register_uri( + 'PATCH', + 'https://image.example.com/v2/images/{id}'.format( + id=self.image_id)) + self.adapter.register_uri( + 'GET', + 'https://image.example.com/v2/tasks/{id}'.format(id=task_id), + [ + dict(status_code=503, text='Random error'), + dict(json={'images': args}), + ] + ) + self.cloud.create_image( 'name-99', self.imagefile.name, wait=True, timeout=1, is_public=False, container='image_upload_v2_test_container') @@ -257,20 +270,44 @@ class Container(object): objects=mock.ANY, options=args) - mock_image_client.post.assert_called_with( - '/tasks', - data=dict( + calls = [ + dict(method='GET', url='http://192.168.0.19:35357/'), + dict(method='POST', url='https://example.com/v2.0/tokens'), + dict(method='GET', url='https://image.example.com/'), + dict(method='GET', url='https://image.example.com/v2/images'), + dict(method='GET', url='https://object-store.example.com/info'), + dict(method='GET', url='https://image.example.com/v2/images'), + dict(method='POST', url='https://image.example.com/v2/tasks'), + dict( + method='GET', + url='https://image.example.com/v2/tasks/{id}'.format( + id=task_id)), + dict( + method='GET', + url='https://image.example.com/v2/tasks/{id}'.format( + id=task_id)), + dict(method='GET', url='https://image.example.com/v2/images'), + dict( + method='PATCH', + url='https://image.example.com/v2/images/{id}'.format( + id=self.image_id)), + dict(method='GET', url='https://image.example.com/v2/images'), + ] + + for x in range(0, len(calls)): + self.assertEqual( + calls[x]['method'], self.adapter.request_history[x].method) + self.assertEqual( + calls[x]['url'], self.adapter.request_history[x].url) + self.assertEqual( + self.adapter.request_history[6].json(), + dict( type='import', input={ 'import_from': 'image_upload_v2_test_container/name-99', 'image_properties': {'name': 'name-99'}})) - object_path = 'image_upload_v2_test_container/name-99' - args = {'owner_specified.shade.md5': NO_MD5, - 'owner_specified.shade.sha256': NO_SHA256, - 'owner_specified.shade.object': object_path, - 'image_id': 'a35e8afc-cae9-4e38-8441-2cd465f79f7b'} - mock_image_client.patch.assert_called_with( - '/images/a35e8afc-cae9-4e38-8441-2cd465f79f7b', - json=sorted([ + self.assertEqual( + self.adapter.request_history[10].json(), + sorted([ { u'op': u'add', u'value': 'image_upload_v2_test_container/name-99', @@ -282,14 +319,10 @@ class Container(object): }, { u'op': u'add', u'value': NO_SHA256, u'path': u'/owner_specified.shade.sha256' - }], key=operator.itemgetter('value')), - headers={ - 'Content-Type': 'application/openstack-images-v2.1-json-patch' - }) - + }], key=operator.itemgetter('value'))) self.assertEqual( - self.cloud._normalize_images([fake_image]), - self.cloud.list_images()) + self.adapter.request_history[10].headers['Content-Type'], + 'application/openstack-images-v2.1-json-patch') def _image_dict(self, fake_image): return self.cloud._normalize_image(meta.obj_to_dict(fake_image)) From 916ba3812362722e836be122765531daa0e9e63d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 15 Dec 2016 07:11:57 -0600 Subject: [PATCH 1153/3836] Pass md5 and sha256 to create_object sanely. The logic to skip md5/sha256 calculation was flawed, as it pulled the wrong keys from the image_kwargs. The whole idea of pulling parameters from the image create kwargs is too clevel though. Just go ahead and pass the values as real python parameters. Change-Id: I516b0143d6874bd31f5050fc6bee0dc8227dfcda --- shade/openstackcloud.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index f407a5302..85530efec 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3183,6 +3183,7 @@ def create_image( name, filename, container, current_image=current_image, wait=wait, timeout=timeout, + md5=md5, sha256=sha256, meta=meta, **kwargs) else: # If a user used the v1 calling format, they will have @@ -3312,7 +3313,7 @@ def _upload_image_put( def _upload_image_task( self, name, filename, container, current_image, - wait, timeout, meta, **image_kwargs): + wait, timeout, meta, md5=None, sha256=None, **image_kwargs): parameters = image_kwargs.pop('parameters', {}) image_kwargs.update(parameters) @@ -3325,8 +3326,7 @@ def _upload_image_task( self.create_object( container, name, filename, - md5=image_kwargs.get('md5', None), - sha256=image_kwargs.get('sha256', None)) + md5=md5, sha256=sha256) if not current_image: current_image = self.get_image(name) # TODO(mordred): Can we do something similar to what nodepool does From 485a4c171bdcec337e5287410a3c45792a2df0fe Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 15 Dec 2016 09:03:23 -0600 Subject: [PATCH 1154/3836] Stop double-reporting extra_data in exceptions We append extra_data to the args we send to Exception, but we also append extra_data to the str output. Thing is - Exception already does that with all the args - so we wind up with something like: shade.exc.OpenStackCloudException: ("Image creation failed: ", Munch({u'status': u'failure'...)) (Extra: Munch({u'status': u'failure'...) Also - the Munch output isn't terribly useful in the exception, so go ahead and dict-ify it if we've passed a munch to extra_data. Change-Id: Ie3271093fba0ac6f074e1cfd4e6a79455dc82be4 --- shade/exc.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/shade/exc.py b/shade/exc.py index d548f1ad2..3ce23b901 100644 --- a/shade/exc.py +++ b/shade/exc.py @@ -14,6 +14,7 @@ import sys +import munch from requests import exceptions as _rex from shade import _log @@ -26,7 +27,9 @@ class OpenStackCloudException(Exception): def __init__(self, message, extra_data=None, **kwargs): args = [message] if extra_data: - args.append(extra_data) + if isinstance(extra_data, munch.Munch): + extra_data = extra_data.toDict() + args.append("Extra: {0}".format(str(extra_data))) super(OpenStackCloudException, self).__init__(*args, **kwargs) self.extra_data = extra_data self.inner_exception = sys.exc_info() @@ -40,8 +43,6 @@ def log_error(self, logger=None): def __str__(self): message = Exception.__str__(self) - if self.extra_data is not None: - message = "%s (Extra: %s)" % (message, self.extra_data) if (self.inner_exception and self.inner_exception[1] and not self.orig_message.endswith( str(self.inner_exception[1]))): From 0dff88869195c937a23071cdf89b783c116556f6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 15 Dec 2016 10:10:25 -0600 Subject: [PATCH 1155/3836] Magnum's keystone id is container-infra, not container Our use of 'container' as the key for magnum service detection and for client construction has been wrong, but was missed because we failed open on missing services. Change-Id: I181f33388bbeb793fb8b5c47dd40188e7826cd43 --- shade/openstackcloud.py | 2 +- shade/tests/functional/test_cluster_templates.py | 2 +- shade/tests/functional/test_devstack.py | 2 +- shade/tests/functional/test_magnum_services.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 82ce19e9a..d8e6b1f36 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1086,7 +1086,7 @@ def trove_client(self): def magnum_client(self): if self._magnum_client is None: self._magnum_client = self._get_client( - 'container', magnumclient.client.Client) + 'container-infra', magnumclient.client.Client) return self._magnum_client @property diff --git a/shade/tests/functional/test_cluster_templates.py b/shade/tests/functional/test_cluster_templates.py index 031458e5a..4d39ded7b 100644 --- a/shade/tests/functional/test_cluster_templates.py +++ b/shade/tests/functional/test_cluster_templates.py @@ -31,7 +31,7 @@ class TestClusterTemplate(base.BaseFunctionalTestCase): def setUp(self): super(TestClusterTemplate, self).setUp() - if not self.demo_cloud.has_service('container'): + if not self.demo_cloud.has_service('container-infra'): self.skipTest('Container service not supported by cloud') self.ct = None diff --git a/shade/tests/functional/test_devstack.py b/shade/tests/functional/test_devstack.py index e24b97376..54fd10f19 100644 --- a/shade/tests/functional/test_devstack.py +++ b/shade/tests/functional/test_devstack.py @@ -32,7 +32,7 @@ class TestDevstack(base.BaseFunctionalTestCase): scenarios = [ ('designate', dict(env='DESIGNATE', service='dns')), ('heat', dict(env='HEAT', service='orchestration')), - ('magnum', dict(env='MAGNUM', service='container')), + ('magnum', dict(env='MAGNUM', service='container-infra')), ('neutron', dict(env='NEUTRON', service='network')), ('swift', dict(env='SWIFT', service='object-store')), ] diff --git a/shade/tests/functional/test_magnum_services.py b/shade/tests/functional/test_magnum_services.py index 17c622c95..57f682371 100644 --- a/shade/tests/functional/test_magnum_services.py +++ b/shade/tests/functional/test_magnum_services.py @@ -26,7 +26,7 @@ class TestMagnumServices(base.BaseFunctionalTestCase): def setUp(self): super(TestMagnumServices, self).setUp() - if not self.operator_cloud.has_service('container'): + if not self.operator_cloud.has_service('container-infra'): self.skipTest('Container service not supported by cloud') def test_magnum_services(self): From ace76cfd2f450d3c8eeafb9e8153c0a00f0a2bad Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 15 Dec 2016 09:08:31 -0600 Subject: [PATCH 1156/3836] Clear the exception stack when we catch and continue When we don't care about an exception in one of the poll loops, but then there is a different unrelated error later on, the caught and handled exception can linger. This is seen in output like this: shade.exc.OpenStackCloudException: ("Image creation failed: ", Munch({u'status': u'failure',...)) (Extra: Munch({u'status': u'failure', ...)) (Inner Exception: (503) Server Error: Service Unavailable for url: ...) The 503 error is caught and skipped as a normal matter of course. Then the task later fails and returns a status: failed, which we throw a new exception for - but the earlier 503 error shows up in the exc_info call so it gets reported. Since the 503 ISN'T related to the actual error we're reporting, the output is erroenous and confusing. Change-Id: I87b521df98435d541d62269c0987313add74d260 --- shade/_utils.py | 7 +++++++ shade/openstackcloud.py | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/shade/_utils.py b/shade/_utils.py index f30b3ae93..1e938fceb 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -19,6 +19,7 @@ import netifaces import re import six +import sys import time from decorator import decorator @@ -32,6 +33,12 @@ _decorated_methods = [] +def _exc_clear(): + """Because sys.exc_clear is gone in py3 and is not in six.""" + if sys.version_info[0] == 2: + sys.exc_clear() + + def _iterate_timeout(timeout, message, wait=2): """Iterate and raise an exception on timeout. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 85530efec..f22d5410a 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3350,6 +3350,9 @@ def _upload_image_task( '/tasks/{id}'.format(id=glance_task.id)) except OpenStackCloudHTTPError as e: if e.response.status_code == 503: + # Clear the exception so that it doesn't linger + # and get reported as an Inner Exception later + _utils._exc_clear() # Intermittent failure - catch and try again continue raise @@ -3360,6 +3363,9 @@ def _upload_image_task( image = self.get_image(image_id) except OpenStackCloudHTTPError as e: if e.response.status_code == 503: + # Clear the exception so that it doesn't linger + # and get reported as an Inner Exception later + _utils._exc_clear() # Intermittent failure - catch and try again continue raise @@ -5551,6 +5557,9 @@ def get_object_segment_size(self, segment_size): caps = self.get_object_capabilities() except OpenStackCloudHTTPError as e: if e.response.status_code in (404, 412): + # Clear the exception so that it doesn't linger + # and get reported as an Inner Exception later + _utils._exc_clear() server_max_file_size = DEFAULT_MAX_FILE_SIZE self.log.info( "Swift capabilities not supported. " From 7e0fbb3707e1b60157a5662ada2c0c9177b9a2d7 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 15 Dec 2016 09:29:34 -0600 Subject: [PATCH 1157/3836] Add total image import time to debug log Because the image import happens in a poll loop on tasks, there is no real tracking of the total time elapsed. While watching logs or reading them, it's a number that's interesting - so calculate and report it. Change-Id: I23dfa5d8a2fe7b105389b23681effbde8c528a6d --- shade/openstackcloud.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index f22d5410a..9f4948a56 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3340,6 +3340,7 @@ def _upload_image_task( glance_task = self._image_client.post('/tasks', json=task_args) self.list_images.invalidate(self) if wait: + start = time.time() image_id = None for count in _utils._iterate_timeout( timeout, @@ -3373,7 +3374,10 @@ def _upload_image_task( continue self.update_image_properties( image=image, meta=meta, **image_kwargs) - return self.get_image(status.result['image_id']) + self.log.debug( + "Image Task %s imported %s in %s", + glance_task.id, image_id, (time.time() - start)) + return self.get_image(image_id) if status.status == 'failure': if status.message == IMAGE_ERROR_396: glance_task = self._image_client.post( From e2d1008aed7f0d3046e1aa539c03837318c4263a Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Thu, 15 Dec 2016 12:34:32 -0500 Subject: [PATCH 1158/3836] Add docutils contraint on 0.13.1 to fix building See: http://lists.openstack.org/pipermail/openstack-dev/2016-December/108742.html Change-Id: Id183b0d3a6339e10f2f1f4a5dc78352d5c3f27ed --- test-requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/test-requirements.txt b/test-requirements.txt index 0138f13f6..f9908d6e0 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,6 +5,7 @@ hacking>=0.10.2,<0.11 # Apache-2.0 coverage>=3.6 +docutils>=0.11,!=0.13.1 # OSI-Approved Open Source, Public Domain extras fixtures>=0.3.14 jsonschema>=2.0.0,<3.0.0,!=2.5.0 From 6615160361f375f480ce66947957513ee3dd0134 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 15 Dec 2016 10:07:55 -0600 Subject: [PATCH 1159/3836] Magnum's service_type is container_infra Leave container_api_version since people might be depending on it. Also, add it to the constructors list, since we do know about it. Change-Id: I3bcb966154ac53269614c943ad9c2675b27d62d0 --- os_client_config/constructors.json | 1 + os_client_config/defaults.json | 1 + 2 files changed, 2 insertions(+) diff --git a/os_client_config/constructors.json b/os_client_config/constructors.json index 89c844c55..a78be6bcb 100644 --- a/os_client_config/constructors.json +++ b/os_client_config/constructors.json @@ -1,5 +1,6 @@ { "compute": "novaclient.client.Client", + "container-infra": "magnumclient.client.Client", "database": "troveclient.client.Client", "identity": "keystoneclient.client.Client", "image": "glanceclient.Client", diff --git a/os_client_config/defaults.json b/os_client_config/defaults.json index ba8bf3923..b4e9dea67 100644 --- a/os_client_config/defaults.json +++ b/os_client_config/defaults.json @@ -2,6 +2,7 @@ "auth_type": "password", "baremetal_api_version": "1", "container_api_version": "1", + "container_infra_api_version": "1", "compute_api_version": "2", "database_api_version": "1.0", "disable_vendor_agent": {}, From ea061e8031a4b9176390820175c60f836311f2f6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 16 Dec 2016 08:03:01 -0600 Subject: [PATCH 1160/3836] Add release notes and an error message for release In prepping for 1.14.0, it seemed there were a couple of important release notes missing, as well as an informative error message in case someone installs shade fresh and then tries to use glance_client. Change-Id: I17b8e7d3d1f6ac8172e49f4473137f51351f7e5d --- .../notes/fixed-magnum-type-7406f0a60525f858.yaml | 6 ++++++ .../notes/removed-glanceclient-105c7fba9481b9be.yaml | 9 +++++++++ shade/openstackcloud.py | 12 +++++++++++- 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/fixed-magnum-type-7406f0a60525f858.yaml create mode 100644 releasenotes/notes/removed-glanceclient-105c7fba9481b9be.yaml diff --git a/releasenotes/notes/fixed-magnum-type-7406f0a60525f858.yaml b/releasenotes/notes/fixed-magnum-type-7406f0a60525f858.yaml new file mode 100644 index 000000000..bc0f768bd --- /dev/null +++ b/releasenotes/notes/fixed-magnum-type-7406f0a60525f858.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - Fixed magnum service_type. shade was using it as 'container' + but the correct type is 'container-infra'. It's possible that on + old clouds with magnum shade may now do the wrong thing. If that + occurs, please file a bug. diff --git a/releasenotes/notes/removed-glanceclient-105c7fba9481b9be.yaml b/releasenotes/notes/removed-glanceclient-105c7fba9481b9be.yaml new file mode 100644 index 000000000..78559f9a3 --- /dev/null +++ b/releasenotes/notes/removed-glanceclient-105c7fba9481b9be.yaml @@ -0,0 +1,9 @@ +--- +prelude: > + This release marks the beginning of the path towards removing all + of the 'python-*client' libraries as dependencies. Subsequent releases + should expect to have fewer and fewer library depdencies. +upgrade: + - Removed glanceclient as a dependency. All glance operations + are now performed with direct REST calls using keystoneauth + Adapter. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 807beadaf..1f7324574 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -993,7 +993,17 @@ def remove_user_from_group(self, name_or_id, group_name_or_id): @property def glance_client(self): - import glanceclient + warnings.warn( + 'Using shade to get a glance_client object is deprecated. If you' + ' need a raw glanceclient.Client object, please use' + ' make_legacy_client in os-client-config instead') + try: + import glanceclient + except ImportError: + self.log.error( + 'glanceclient is no longer a dependency of shade. You need to' + ' install python-glanceclient directly.') + raise if self._glance_client is None: self._glance_client = self._get_client( 'image', glanceclient.Client) From 1beac18cd0eeab6ff36367e8e0d322235dbe478d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 16 Dec 2016 10:41:12 -0600 Subject: [PATCH 1161/3836] Update test of object metadata to mock requests Replace all of the swift_client mock calls with requests_mock calls. Change-Id: I59fed67aad05d3c38add9c918f01420181d75fe9 --- shade/tests/unit/test_image.py | 83 +++++++++++++++++++++++++++------- 1 file changed, 66 insertions(+), 17 deletions(-) diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index ef2789180..77f7c5a65 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -196,12 +196,13 @@ def test_create_image_put_v2(self): }) self.assertEqual(self.adapter.request_history[5].text.read(), b'\x00') - @mock.patch.object(shade.OpenStackCloud, 'swift_client') @mock.patch.object(shade.OpenStackCloud, 'swift_service') def test_create_image_task(self, - swift_service_mock, - swift_mock): + swift_service_mock): self.cloud.image_api_use_tasks = True + image_name = 'name-99' + container_name = 'image_upload_v2_test_container' + endpoint = self.cloud._object_store_client.get_endpoint() self.adapter.get( 'https://object-store.example.com/info', @@ -209,12 +210,38 @@ def test_create_image_task(self, swift={'max_file_size': 1000}, slo={'min_segment_size': 500})) - class Container(object): - name = 'image_upload_v2_test_container' - - fake_container = Container() - swift_mock.put_container.return_value = fake_container - swift_mock.head_object.return_value = {} + self.adapter.put( + '{endpoint}/{container}'.format( + endpoint=endpoint, + container=container_name,), + status_code=201, + headers={ + 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=UTF-8', + }) + self.adapter.head( + '{endpoint}/{container}'.format( + endpoint=endpoint, + container=container_name), + [ + dict(status_code=404), + dict(headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}), + ]) + self.adapter.head( + '{endpoint}/{container}/{object}'.format( + endpoint=endpoint, + container=container_name, object=image_name), + status_code=404) task_id = str(uuid.uuid4()) args = dict( @@ -255,8 +282,8 @@ class Container(object): ) self.cloud.create_image( - 'name-99', self.imagefile.name, wait=True, timeout=1, - is_public=False, container='image_upload_v2_test_container') + image_name, self.imagefile.name, wait=True, timeout=1, + is_public=False, container=container_name) args = { 'header': [ @@ -276,6 +303,26 @@ class Container(object): dict(method='GET', url='https://image.example.com/'), dict(method='GET', url='https://image.example.com/v2/images'), dict(method='GET', url='https://object-store.example.com/info'), + dict( + method='HEAD', + url='{endpoint}/{container}'.format( + endpoint=endpoint, + container=container_name)), + dict( + method='PUT', + url='{endpoint}/{container}'.format( + endpoint=endpoint, + container=container_name)), + dict( + method='HEAD', + url='{endpoint}/{container}'.format( + endpoint=endpoint, + container=container_name)), + dict( + method='HEAD', + url='{endpoint}/{container}/{object}'.format( + endpoint=endpoint, + container=container_name, object=image_name)), dict(method='GET', url='https://image.example.com/v2/images'), dict(method='POST', url='https://image.example.com/v2/tasks'), dict( @@ -300,17 +347,19 @@ class Container(object): self.assertEqual( calls[x]['url'], self.adapter.request_history[x].url) self.assertEqual( - self.adapter.request_history[6].json(), + self.adapter.request_history[10].json(), dict( type='import', input={ - 'import_from': 'image_upload_v2_test_container/name-99', - 'image_properties': {'name': 'name-99'}})) + 'import_from': '{container}/{object}'.format( + container=container_name, object=image_name), + 'image_properties': {'name': image_name}})) self.assertEqual( - self.adapter.request_history[10].json(), + self.adapter.request_history[14].json(), sorted([ { u'op': u'add', - u'value': 'image_upload_v2_test_container/name-99', + u'value': '{container}/{object}'.format( + container=container_name, object=image_name), u'path': u'/owner_specified.shade.object' }, { u'op': u'add', @@ -321,7 +370,7 @@ class Container(object): u'path': u'/owner_specified.shade.sha256' }], key=operator.itemgetter('value'))) self.assertEqual( - self.adapter.request_history[10].headers['Content-Type'], + self.adapter.request_history[14].headers['Content-Type'], 'application/openstack-images-v2.1-json-patch') def _image_dict(self, fake_image): From 63ead1466886b346db10014279e18f8af464e63e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 16 Dec 2016 10:41:38 -0600 Subject: [PATCH 1162/3836] Change get_object_metadata to use REST Change-Id: Ie473afecaa863bd5ee44a159a3f7e30ad943c674 --- shade/_tasks.py | 5 ----- shade/openstackcloud.py | 13 ++++++------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index be60dc4ce..88b4423ee 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -511,11 +511,6 @@ def main(self, client): return client.swift_client.get_container(**self.args)[1] -class ObjectMetadata(task_manager.Task): - def main(self, client): - return client.swift_client.head_object(**self.args) - - class ObjectGet(task_manager.Task): def main(self, client): return client.swift_client.get_object(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 807beadaf..cb1826fb8 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5745,14 +5745,13 @@ def delete_object(self, container, name): def get_object_metadata(self, container, name): try: - return self.manager.submit_task(_tasks.ObjectMetadata( - container=container, obj=name)) - except swift_exceptions.ClientException as e: - if e.http_status == 404: + return self._object_store_client.head( + '/{container}/{object}'.format( + container=container, object=name)).headers + except OpenStackCloudException as e: + if e.response.status_code == 404: return None - raise OpenStackCloudException( - "Object metadata fetch failed: %s (%s/%s)" % ( - e.http_reason, e.http_host, e.http_path)) + raise def get_object(self, container, obj, query_string=None, resp_chunk_size=None): From e5e3f374e9dac4942035fa2212c9d0a817f060e3 Mon Sep 17 00:00:00 2001 From: tengqm Date: Sat, 17 Dec 2016 08:06:23 -0500 Subject: [PATCH 1163/3836] Base for workflow service (mistral) Change-Id: I1b6754673b21a92aad1e8f0e10283859987108f6 --- openstack/tests/unit/workflow/__init__.py | 0 openstack/tests/unit/workflow/test_version.py | 43 +++++++++++++++++++ .../unit/workflow/test_workflow_service.py | 28 ++++++++++++ openstack/workflow/__init__.py | 0 openstack/workflow/v2/__init__.py | 0 openstack/workflow/version.py | 31 +++++++++++++ openstack/workflow/workflow_service.py | 26 +++++++++++ 7 files changed, 128 insertions(+) create mode 100644 openstack/tests/unit/workflow/__init__.py create mode 100644 openstack/tests/unit/workflow/test_version.py create mode 100644 openstack/tests/unit/workflow/test_workflow_service.py create mode 100644 openstack/workflow/__init__.py create mode 100644 openstack/workflow/v2/__init__.py create mode 100644 openstack/workflow/version.py create mode 100644 openstack/workflow/workflow_service.py diff --git a/openstack/tests/unit/workflow/__init__.py b/openstack/tests/unit/workflow/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/unit/workflow/test_version.py b/openstack/tests/unit/workflow/test_version.py new file mode 100644 index 000000000..1aca9a8fb --- /dev/null +++ b/openstack/tests/unit/workflow/test_version.py @@ -0,0 +1,43 @@ +# 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 testtools + +from openstack.workflow import version + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'id': IDENTIFIER, + 'links': '2', + 'status': '3', +} + + +class TestVersion(testtools.TestCase): + + def test_basic(self): + sot = version.Version() + self.assertEqual('version', sot.resource_key) + self.assertEqual('versions', sot.resources_key) + self.assertEqual('/', sot.base_path) + self.assertEqual('workflowv2', sot.service.service_type) + self.assertFalse(sot.allow_create) + self.assertFalse(sot.allow_get) + self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = version.Version(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['links'], sot.links) + self.assertEqual(EXAMPLE['status'], sot.status) diff --git a/openstack/tests/unit/workflow/test_workflow_service.py b/openstack/tests/unit/workflow/test_workflow_service.py new file mode 100644 index 000000000..cc5dd9b0e --- /dev/null +++ b/openstack/tests/unit/workflow/test_workflow_service.py @@ -0,0 +1,28 @@ +# 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 testtools + +from openstack.workflow import workflow_service + + +class TestWorkflowService(testtools.TestCase): + + def test_service(self): + sot = workflow_service.WorkflowService() + self.assertEqual('workflowv2', sot.service_type) + self.assertEqual('public', sot.interface) + self.assertIsNone(sot.region) + self.assertIsNone(sot.service_name) + self.assertEqual(1, len(sot.valid_versions)) + self.assertEqual('v2', sot.valid_versions[0].module) + self.assertEqual('v2', sot.valid_versions[0].path) diff --git a/openstack/workflow/__init__.py b/openstack/workflow/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/workflow/v2/__init__.py b/openstack/workflow/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/workflow/version.py b/openstack/workflow/version.py new file mode 100644 index 000000000..4834e952a --- /dev/null +++ b/openstack/workflow/version.py @@ -0,0 +1,31 @@ +# 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 resource2 as resource +from openstack.workflow import workflow_service + + +class Version(resource.Resource): + resource_key = 'version' + resources_key = 'versions' + base_path = '/' + service = workflow_service.WorkflowService( + version=workflow_service.WorkflowService.UNVERSIONED + ) + + # capabilities + allow_list = True + + # Properties + links = resource.Body('links') + status = resource.Body('status') diff --git a/openstack/workflow/workflow_service.py b/openstack/workflow/workflow_service.py new file mode 100644 index 000000000..8adc89a6c --- /dev/null +++ b/openstack/workflow/workflow_service.py @@ -0,0 +1,26 @@ +# 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 service_filter + + +class WorkflowService(service_filter.ServiceFilter): + """The workflow service.""" + + valid_versions = [service_filter.ValidVersion('v2')] + + def __init__(self, version=None): + """Create a workflow service.""" + super(WorkflowService, self).__init__( + service_type='workflowv2', + version=version + ) From b3b833c69e23193aa28a9eef230e1775d2743ef4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 17 Dec 2016 07:24:30 -0600 Subject: [PATCH 1164/3836] Extract assertion method for asserting calls made The pattern of asserting a list of calls were made comes up over and over. Let's encode it into a method. Change-Id: I43186abbc807eab4da1b88469fe29bb7f0dd83b9 --- shade/tests/unit/base.py | 14 ++++++++++++++ shade/tests/unit/test_image.py | 20 ++++---------------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 3848a4579..23c9f1caf 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -146,3 +146,17 @@ def setUp( self.cloud = shade.OpenStackCloud( cloud_config=self.cloud_config, log_inner_exceptions=True) + self.calls = [ + dict(method='GET', url='http://192.168.0.19:35357/'), + dict(method='POST', url='https://example.com/v2.0/tokens'), + ] + + def assert_calls(self): + self.assertEqual(len(self.calls), len(self.adapter.request_history)) + for x in range(0, len(self.calls)): + self.assertEqual( + self.calls[x]['method'], + self.adapter.request_history[x].method) + self.assertEqual( + self.calls[x]['url'], + self.adapter.request_history[x].url) diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index 77f7c5a65..7cc554796 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -167,9 +167,7 @@ def test_create_image_put_v2(self): 'fake_image', self.imagefile.name, wait=True, timeout=1, is_public=False) - calls = [ - dict(method='GET', url='http://192.168.0.19:35357/'), - dict(method='POST', url='https://example.com/v2.0/tokens'), + self.calls += [ dict(method='GET', url='https://image.example.com/'), dict(method='GET', url='https://image.example.com/v2/images'), dict(method='POST', url='https://image.example.com/v2/images'), @@ -179,11 +177,7 @@ def test_create_image_put_v2(self): id=self.image_id)), dict(method='GET', url='https://image.example.com/v2/images'), ] - for x in range(0, len(calls)): - self.assertEqual( - calls[x]['method'], self.adapter.request_history[x].method) - self.assertEqual( - calls[x]['url'], self.adapter.request_history[x].url) + self.assert_calls() self.assertEqual( self.adapter.request_history[4].json(), { u'container_format': u'bare', @@ -297,9 +291,7 @@ def test_create_image_task(self, objects=mock.ANY, options=args) - calls = [ - dict(method='GET', url='http://192.168.0.19:35357/'), - dict(method='POST', url='https://example.com/v2.0/tokens'), + self.calls += [ dict(method='GET', url='https://image.example.com/'), dict(method='GET', url='https://image.example.com/v2/images'), dict(method='GET', url='https://object-store.example.com/info'), @@ -341,11 +333,7 @@ def test_create_image_task(self, dict(method='GET', url='https://image.example.com/v2/images'), ] - for x in range(0, len(calls)): - self.assertEqual( - calls[x]['method'], self.adapter.request_history[x].method) - self.assertEqual( - calls[x]['url'], self.adapter.request_history[x].url) + self.assert_calls() self.assertEqual( self.adapter.request_history[10].json(), dict( From f80d1717dcc2d2137dd25760b8da627638954d60 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 17 Dec 2016 09:06:22 -0600 Subject: [PATCH 1165/3836] Make munch aware assertEqual test method Many of our assertEqual calls use compare munches. When there is a problem, the printout of the munch is all on one line, which makes reading it hard. Make an assertEqual in our base class which turns munches into dicts if they're passed in. Change-Id: I478548c5146cd6f86b8957f527ccd3d78268156f --- shade/tests/base.py | 10 ++++++++++ shade/tests/unit/test_normalize.py | 24 ++++++++++++------------ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/shade/tests/base.py b/shade/tests/base.py index 3de6f0bd1..f4b7af8df 100644 --- a/shade/tests/base.py +++ b/shade/tests/base.py @@ -18,6 +18,7 @@ import os import fixtures +import munch import testtools import shade @@ -57,3 +58,12 @@ def setUp(self): shade.simple_logging(debug=True) self.log_fixture = self.useFixture(fixtures.FakeLogger()) + + def assertEqual(self, first, second, *args, **kwargs): + '''Munch aware wrapper''' + if isinstance(first, munch.Munch): + first = first.toDict() + if isinstance(second, munch.Munch): + second = second.toDict() + return super(TestCase, self).assertEqual( + first, second, *args, **kwargs) diff --git a/shade/tests/unit/test_normalize.py b/shade/tests/unit/test_normalize.py index ea7a14ea5..f8a052c07 100644 --- a/shade/tests/unit/test_normalize.py +++ b/shade/tests/unit/test_normalize.py @@ -227,7 +227,7 @@ def test_normalize_flavors(self): 'rxtx_factor': 1600.0, 'swap': 0, 'vcpus': 8} - retval = self.cloud._normalize_flavor(raw_flavor).toDict() + retval = self.cloud._normalize_flavor(raw_flavor) self.assertEqual(expected, retval) def test_normalize_flavors_strict(self): @@ -260,7 +260,7 @@ def test_normalize_flavors_strict(self): 'rxtx_factor': 1600.0, 'swap': 0, 'vcpus': 8} - retval = self.strict_cloud._normalize_flavor(raw_flavor).toDict() + retval = self.strict_cloud._normalize_flavor(raw_flavor) self.assertEqual(expected, retval) def test_normalize_nova_images(self): @@ -356,7 +356,7 @@ def test_normalize_nova_images(self): 'updated_at': u'2015-02-15T23:04:34Z', 'virtual_size': 0, 'visibility': 'private'} - retval = self.cloud._normalize_image(raw_image).toDict() + retval = self.cloud._normalize_image(raw_image) self.assertEqual(expected, retval) def test_normalize_nova_images_strict(self): @@ -410,7 +410,7 @@ def test_normalize_nova_images_strict(self): 'updated_at': u'2015-02-15T23:04:34Z', 'virtual_size': 0, 'visibility': 'private'} - retval = self.strict_cloud._normalize_image(raw_image).toDict() + retval = self.strict_cloud._normalize_image(raw_image) self.assertEqual(sorted(expected.keys()), sorted(retval.keys())) self.assertEqual(expected, retval) @@ -501,7 +501,7 @@ def test_normalize_glance_images(self): 'visibility': u'private', u'vm_mode': u'hvm', u'xenapi_use_agent': u'False'} - retval = self.cloud._normalize_image(raw_image).toDict() + retval = self.cloud._normalize_image(raw_image) self.assertEqual(expected, retval) def test_normalize_glance_images_strict(self): @@ -553,7 +553,7 @@ def test_normalize_glance_images_strict(self): 'updated_at': u'2015-02-15T23:04:34Z', 'virtual_size': 0, 'visibility': 'private'} - retval = self.strict_cloud._normalize_image(raw_image).toDict() + retval = self.strict_cloud._normalize_image(raw_image) self.assertEqual(sorted(expected.keys()), sorted(retval.keys())) self.assertEqual(expected, retval) @@ -614,7 +614,7 @@ def test_normalize_servers_strict(self): 'user_id': u'e9b21dc437d149858faee0898fb08e92', 'vm_state': u'active', 'volumes': []} - retval = self.strict_cloud._normalize_server(raw_server).toDict() + retval = self.strict_cloud._normalize_server(raw_server) self.assertEqual(expected, retval) def test_normalize_servers_normal(self): @@ -698,7 +698,7 @@ def test_normalize_servers_normal(self): 'user_id': u'e9b21dc437d149858faee0898fb08e92', 'vm_state': u'active', 'volumes': []} - retval = self.cloud._normalize_server(raw_server).toDict() + retval = self.cloud._normalize_server(raw_server) self.assertEqual(expected, retval) def test_normalize_secgroups_strict(self): @@ -886,7 +886,7 @@ def test_normalize_volumes_v1(self): 'volume_type': None, } retval = self.cloud._normalize_volume(vol) - self.assertEqual(expected, retval.toDict()) + self.assertEqual(expected, retval) def test_normalize_volumes_v2(self): vol = dict( @@ -944,7 +944,7 @@ def test_normalize_volumes_v2(self): 'volume_type': None, } retval = self.cloud._normalize_volume(vol) - self.assertEqual(expected, retval.toDict()) + self.assertEqual(expected, retval) def test_normalize_volumes_v1_strict(self): vol = dict( @@ -990,7 +990,7 @@ def test_normalize_volumes_v1_strict(self): 'volume_type': None, } retval = self.strict_cloud._normalize_volume(vol) - self.assertEqual(expected, retval.toDict()) + self.assertEqual(expected, retval) def test_normalize_volumes_v2_strict(self): vol = dict( @@ -1038,4 +1038,4 @@ def test_normalize_volumes_v2_strict(self): 'volume_type': None, } retval = self.strict_cloud._normalize_volume(vol) - self.assertEqual(expected, retval.toDict()) + self.assertEqual(expected, retval) From 7d463839d1389e87024e5dd23aac8134c33f0d00 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 17 Dec 2016 09:07:35 -0600 Subject: [PATCH 1166/3836] Extract helper methods and change test default to v3 Our unit tests default to v2, which is sad. Change that to v3. Also, make it easy to write tests that use v2. Also, start building more scaffolding for making requests_mock tests easier to write and grok. Change-Id: I83e2b7c9cf91c2b3128df1a77debd47b1b5ce8c7 --- shade/tests/unit/base.py | 61 ++++++---- .../{catalog.json => catalog-v2.json} | 0 shade/tests/unit/fixtures/catalog-v3.json | 114 ++++++++++++++++++ shade/tests/unit/fixtures/clouds/clouds.yaml | 9 ++ .../unit/fixtures/clouds/clouds_cache.yaml | 9 ++ shade/tests/unit/test_image.py | 10 +- shade/tests/unit/test_meta.py | 4 +- shade/tests/unit/test_normalize.py | 22 ++-- 8 files changed, 191 insertions(+), 38 deletions(-) rename shade/tests/unit/fixtures/{catalog.json => catalog-v2.json} (100%) create mode 100644 shade/tests/unit/fixtures/catalog-v3.json diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 23c9f1caf..36b418001 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -85,9 +85,9 @@ def _nosleep(seconds): log_inner_exceptions=True) # Any unit tests using betamax directly need a ksa.Session with - # an auth dict. + # an auth dict. The cassette is currently written with v2 as well self.full_cloud_config = self.config.get_one_cloud( - cloud=test_cloud) + cloud='_test_cloud_v2_') self.full_cloud = shade.OpenStackCloud( cloud_config=self.full_cloud_config, log_inner_exceptions=True) @@ -108,48 +108,67 @@ def setUp(self, cloud_config_fixture='clouds.yaml'): class RequestsMockTestCase(BaseTestCase): - def setUp( - self, - cloud_config_fixture='clouds.yaml', - discovery_json='discovery.json', - image_version_json='image-version.json'): + def setUp(self, cloud_config_fixture='clouds.yaml'): super(RequestsMockTestCase, self).setUp( cloud_config_fixture=cloud_config_fixture) + self.discovery_json = os.path.join( + self.fixtures_directory, 'discovery.json') + self.use_keystone_v3() + + def use_keystone_v3(self): self.adapter = self.useFixture(rm_fixture.Fixture()) self.adapter.get( 'http://192.168.0.19:35357/', - text=open( - os.path.join( - self.fixtures_directory, - discovery_json), - 'r').read()) + text=open(self.discovery_json, 'r').read()) self.adapter.post( - 'https://example.com/v2.0/tokens', + 'https://example.com/v3/auth/tokens', + headers={ + 'X-Subject-Token': self.getUniqueString()}, text=open( os.path.join( self.fixtures_directory, - 'catalog.json'), + 'catalog-v3.json'), 'r').read()) + self.calls = [ + dict(method='GET', url='http://192.168.0.19:35357/'), + dict(method='POST', url='https://example.com/v3/auth/tokens'), + ] + self._make_test_cloud(identity_api_version='3') + + def use_keystone_v2(self): + self.adapter = self.useFixture(rm_fixture.Fixture()) self.adapter.get( - 'https://image.example.com/', + 'http://192.168.0.19:35357/', + text=open(self.discovery_json, 'r').read()) + self.adapter.post( + 'https://example.com/v2.0/tokens', text=open( os.path.join( self.fixtures_directory, - image_version_json), + 'catalog-v2.json'), 'r').read()) + self.calls = [ + dict(method='GET', url='http://192.168.0.19:35357/'), + dict(method='POST', url='https://example.com/v2.0/tokens'), + ] + self._make_test_cloud(identity_api_version='2.0') + def _make_test_cloud(self, **kwargs): test_cloud = os.environ.get('SHADE_OS_CLOUD', '_test_cloud_') self.cloud_config = self.config.get_one_cloud( - cloud=test_cloud, validate=True) + cloud=test_cloud, validate=True, **kwargs) self.cloud = shade.OpenStackCloud( cloud_config=self.cloud_config, log_inner_exceptions=True) - self.calls = [ - dict(method='GET', url='http://192.168.0.19:35357/'), - dict(method='POST', url='https://example.com/v2.0/tokens'), - ] + + def use_glance(self, image_version_json='image-version.json'): + discovery_fixture = os.path.join( + self.fixtures_directory, image_version_json) + self.adapter.get( + 'https://image.example.com/', + text=open(discovery_fixture, 'r').read()) def assert_calls(self): self.assertEqual(len(self.calls), len(self.adapter.request_history)) diff --git a/shade/tests/unit/fixtures/catalog.json b/shade/tests/unit/fixtures/catalog-v2.json similarity index 100% rename from shade/tests/unit/fixtures/catalog.json rename to shade/tests/unit/fixtures/catalog-v2.json diff --git a/shade/tests/unit/fixtures/catalog-v3.json b/shade/tests/unit/fixtures/catalog-v3.json new file mode 100644 index 000000000..55722e3a4 --- /dev/null +++ b/shade/tests/unit/fixtures/catalog-v3.json @@ -0,0 +1,114 @@ +{ + "token": { + "audit_ids": [ + "Rvn7eHkiSeOwucBIPaKdYA" + ], + "catalog": [ + { + "endpoints": [ + { + "id": "32466f357f3545248c47471ca51b0d3a", + "interface": "public", + "region": "RegionOne", + "url": "https://compute.example.com/v2.1/1c36b64c840a42cd9e9b931a369337f0" + } + ], + "name": "nova", + "type": "compute" + }, + { + "endpoints": [ + { + "id": "1e875ca2225b408bbf3520a1b8e1a537", + "interface": "public", + "region": "RegionOne", + "url": "https://volume.example.com/v2/1c36b64c840a42cd9e9b931a369337f0" + } + ], + "name": "cinderv2", + "type": "volumev2" + }, + { + "endpoints": [ + { + "id": "5a64de3c4a614d8d8f8d1ba3dee5f45f", + "interface": "public", + "region": "RegionOne", + "url": "https://image.example.com" + } + ], + "name": "glance", + "type": "image" + }, + { + "endpoints": [ + { + "id": "3d15fdfc7d424f3c8923324417e1a3d1", + "interface": "public", + "region": "RegionOne", + "url": "https://volume.example.com/v1/1c36b64c840a42cd9e9b931a369337f0" + } + ], + "name": "cinder", + "type": "volume" + }, + { + "endpoints": [ + { + "id": "4deb4d0504a044a395d4480741ba628c", + "interface": "public", + "region": "RegionOne", + "url": "https://identity.example.com" + } + ], + "endpoints_links": [], + "name": "keystone", + "type": "identity" + }, + { + "endpoints": [ + { + "id": "4deb4d0504a044a395d4480741ba628c", + "interface": "public", + "region": "RegionOne", + "url": "https://object-store.example.com/v1/1c36b64c840a42cd9e9b931a369337f0" + } + ], + "endpoints_links": [], + "name": "swift", + "type": "object-store" + } + ], + "expires_at": "9999-12-31T23:59:59Z", + "issued_at": "2016-12-17T14:25:05.000000Z", + "methods": [ + "password" + ], + "project": { + "domain": { + "id": "default", + "name": "default" + }, + "id": "1c36b64c840a42cd9e9b931a369337f0", + "name": "Default Project" + }, + "roles": [ + { + "id": "9fe2ff9ee4384b1894a90878d3e92bab", + "name": "_member_" + }, + { + "id": "37071fc082e14c2284c32a2761f71c63", + "name": "swiftoperator" + } + ], + "user": { + "domain": { + "id": "default", + "name": "default" + }, + "id": "c17534835f8f42bf98fc367e0bf35e09", + "name": "mordred" + } + } +} diff --git a/shade/tests/unit/fixtures/clouds/clouds.yaml b/shade/tests/unit/fixtures/clouds/clouds.yaml index 6be237471..1db8d90aa 100644 --- a/shade/tests/unit/fixtures/clouds/clouds.yaml +++ b/shade/tests/unit/fixtures/clouds/clouds.yaml @@ -1,5 +1,14 @@ clouds: _test_cloud_: + auth: + auth_url: http://192.168.0.19:35357 + password: password + project_name: admin + username: admin + user_domain_name: default + project_domain_name: default + region_name: RegionOne + _test_cloud_v2_: auth: auth_url: http://192.168.0.19:35357 password: password diff --git a/shade/tests/unit/fixtures/clouds/clouds_cache.yaml b/shade/tests/unit/fixtures/clouds/clouds_cache.yaml index bc39142e9..614f34502 100644 --- a/shade/tests/unit/fixtures/clouds/clouds_cache.yaml +++ b/shade/tests/unit/fixtures/clouds/clouds_cache.yaml @@ -5,6 +5,15 @@ cache: server: 1 clouds: _test_cloud_: + auth: + auth_url: http://192.168.0.19:35357 + password: password + project_name: admin + username: admin + user_domain_name: default + project_domain_name: default + region_name: RegionOne + _test_cloud_v2_: auth: auth_url: http://192.168.0.19:35357 password: password diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index 7cc554796..6567f8418 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -68,6 +68,7 @@ def setUp(self): u'protected': False} self.fake_search_return = {'images': [self.fake_image_dict]} self.output = uuid.uuid4().bytes + self.use_glance() def test_config_v1(self): self.cloud.cloud_config.config['image_api_version'] = '1' @@ -666,10 +667,11 @@ def test_create_image_put_user_prop( class TestImageV1Only(base.RequestsMockTestCase): def setUp(self): - super(TestImageV1Only, self).setUp( - image_version_json='image-version-v1.json') + super(TestImageV1Only, self).setUp() + self.use_glance(image_version_json='image-version-v1.json') def test_config_v1(self): + self.cloud.cloud_config.config['image_api_version'] = '1' # We override the scheme of the endpoint with the scheme of the service # because glance has a bug where it doesn't return https properly. @@ -693,8 +695,8 @@ def test_config_v2(self): class TestImageV2Only(base.RequestsMockTestCase): def setUp(self): - super(TestImageV2Only, self).setUp( - image_version_json='image-version-v2.json') + super(TestImageV2Only, self).setUp() + self.use_glance(image_version_json='image-version-v2.json') def test_config_v1(self): self.cloud.cloud_config.config['image_api_version'] = '1' diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 9e24ef607..096af7d62 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -882,7 +882,7 @@ def test_current_location(self): 'id': mock.ANY, 'name': 'admin', 'domain_id': None, - 'domain_name': None + 'domain_name': 'default' }, 'region_name': u'RegionOne', 'zone': None}, @@ -893,7 +893,7 @@ def test_current_project(self): 'id': mock.ANY, 'name': 'admin', 'domain_id': None, - 'domain_name': None}, + 'domain_name': 'default'}, self.cloud.current_project) def test_has_volume(self): diff --git a/shade/tests/unit/test_normalize.py b/shade/tests/unit/test_normalize.py index f8a052c07..bd4b6897d 100644 --- a/shade/tests/unit/test_normalize.py +++ b/shade/tests/unit/test_normalize.py @@ -207,7 +207,7 @@ def test_normalize_flavors(self): 'cloud': '_test_cloud_', 'project': { 'domain_id': None, - 'domain_name': None, + 'domain_name': 'default', 'id': mock.ANY, 'name': 'admin'}, 'region_name': u'RegionOne', @@ -248,7 +248,7 @@ def test_normalize_flavors_strict(self): 'cloud': '_test_cloud_', 'project': { 'domain_id': None, - 'domain_name': None, + 'domain_name': 'default', 'id': mock.ANY, 'name': 'admin'}, 'region_name': u'RegionOne', @@ -296,7 +296,7 @@ def test_normalize_nova_images(self): 'cloud': '_test_cloud_', 'project': { 'domain_id': None, - 'domain_name': None, + 'domain_name': 'default', 'id': mock.ANY, 'name': 'admin'}, 'region_name': u'RegionOne', @@ -375,7 +375,7 @@ def test_normalize_nova_images_strict(self): 'cloud': '_test_cloud_', 'project': { 'domain_id': None, - 'domain_name': None, + 'domain_name': 'default', 'id': mock.ANY, 'name': 'admin'}, 'region_name': u'RegionOne', @@ -721,7 +721,7 @@ def test_normalize_secgroups_strict(self): region_name='RegionOne', zone=None, project=dict( - domain_name=None, + domain_name='default', id=mock.ANY, domain_id=None, name='admin'), @@ -736,7 +736,7 @@ def test_normalize_secgroups_strict(self): region_name='RegionOne', zone=None, project=dict( - domain_name=None, + domain_name='default', id=mock.ANY, domain_id=None, name='admin'), @@ -769,7 +769,7 @@ def test_normalize_secgroups(self): region_name='RegionOne', zone=None, project=dict( - domain_name=None, + domain_name='default', id=mock.ANY, domain_id=None, name='admin'), @@ -786,7 +786,7 @@ def test_normalize_secgroups(self): region_name='RegionOne', zone=None, project=dict( - domain_name=None, + domain_name='default', id=mock.ANY, domain_id=None, name='admin'), @@ -827,7 +827,7 @@ def test_normalize_secgroup_rules(self): region_name='RegionOne', zone=None, project=dict( - domain_name=None, + domain_name='default', id=mock.ANY, domain_id=None, name='admin'), @@ -865,7 +865,7 @@ def test_normalize_volumes_v1(self): 'cloud': '_test_cloud_', 'project': { 'domain_id': None, - 'domain_name': None, + 'domain_name': 'default', 'id': mock.ANY, 'name': 'admin'}, 'region_name': u'RegionOne', @@ -970,7 +970,7 @@ def test_normalize_volumes_v1_strict(self): 'cloud': '_test_cloud_', 'project': { 'domain_id': None, - 'domain_name': None, + 'domain_name': 'default', 'id': mock.ANY, 'name': 'admin'}, 'region_name': u'RegionOne', From 0232a12d04ca4976399fb3740414f541662108ab Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 17 Dec 2016 09:46:03 -0600 Subject: [PATCH 1167/3836] Combine list of calls with list of request assertions It's really awkward to assert that the request to call 14 had a json body that looks like something. Instead, it's more natural to say "there's going to be a POST call to $url and it should have this body" Make the assert_calls helper method also look for optional json and headers content. Change-Id: I12a50d46416c80b052cf30b605b2820899a1eb77 --- shade/tests/unit/base.py | 11 +++++ shade/tests/unit/test_image.py | 78 +++++++++++++++++----------------- 2 files changed, 50 insertions(+), 39 deletions(-) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 36b418001..52d4c3813 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -179,3 +179,14 @@ def assert_calls(self): self.assertEqual( self.calls[x]['url'], self.adapter.request_history[x].url) + if 'json' in self.calls[x]: + self.assertEqual( + self.calls[x]['json'], + self.adapter.request_history[x].json()) + # headers in a call isn't exhaustive - it's checking to make sure + # a specific header or headers are there, not that they are the + # only headers + if 'headers' in self.calls[x]: + for key, value in self.calls[x]['headers'].items(): + self.assertEqual( + value, self.adapter.request_history[x].headers[key]) diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index 6567f8418..a4e717cca 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -171,7 +171,18 @@ def test_create_image_put_v2(self): self.calls += [ dict(method='GET', url='https://image.example.com/'), dict(method='GET', url='https://image.example.com/v2/images'), - dict(method='POST', url='https://image.example.com/v2/images'), + dict( + method='POST', + url='https://image.example.com/v2/images', + json={ + u'container_format': u'bare', + u'disk_format': u'qcow2', + u'name': u'fake_image', + u'owner_specified.shade.md5': NO_MD5, + u'owner_specified.shade.object': u'images/fake_image', + u'owner_specified.shade.sha256': NO_SHA256, + u'visibility': u'private' + }), dict( method='PUT', url='https://image.example.com/v2/images/{id}/file'.format( @@ -179,16 +190,6 @@ def test_create_image_put_v2(self): dict(method='GET', url='https://image.example.com/v2/images'), ] self.assert_calls() - self.assertEqual( - self.adapter.request_history[4].json(), { - u'container_format': u'bare', - u'disk_format': u'qcow2', - u'name': u'fake_image', - u'owner_specified.shade.md5': NO_MD5, - u'owner_specified.shade.object': u'images/fake_image', - u'owner_specified.shade.sha256': NO_SHA256, - u'visibility': u'private' - }) self.assertEqual(self.adapter.request_history[5].text.read(), b'\x00') @mock.patch.object(shade.OpenStackCloud, 'swift_service') @@ -317,7 +318,14 @@ def test_create_image_task(self, endpoint=endpoint, container=container_name, object=image_name)), dict(method='GET', url='https://image.example.com/v2/images'), - dict(method='POST', url='https://image.example.com/v2/tasks'), + dict( + method='POST', + url='https://image.example.com/v2/tasks', + json=dict( + type='import', input={ + 'import_from': '{container}/{object}'.format( + container=container_name, object=image_name), + 'image_properties': {'name': image_name}})), dict( method='GET', url='https://image.example.com/v2/tasks/{id}'.format( @@ -330,37 +338,29 @@ def test_create_image_task(self, dict( method='PATCH', url='https://image.example.com/v2/images/{id}'.format( - id=self.image_id)), + id=self.image_id), + json=sorted([ + { + u'op': u'add', + u'value': '{container}/{object}'.format( + container=container_name, object=image_name), + u'path': u'/owner_specified.shade.object' + }, { + u'op': u'add', + u'value': NO_MD5, + u'path': u'/owner_specified.shade.md5' + }, { + u'op': u'add', u'value': NO_SHA256, + u'path': u'/owner_specified.shade.sha256' + }], key=operator.itemgetter('value')), + headers={ + 'Content-Type': + 'application/openstack-images-v2.1-json-patch' + }), dict(method='GET', url='https://image.example.com/v2/images'), ] self.assert_calls() - self.assertEqual( - self.adapter.request_history[10].json(), - dict( - type='import', input={ - 'import_from': '{container}/{object}'.format( - container=container_name, object=image_name), - 'image_properties': {'name': image_name}})) - self.assertEqual( - self.adapter.request_history[14].json(), - sorted([ - { - u'op': u'add', - u'value': '{container}/{object}'.format( - container=container_name, object=image_name), - u'path': u'/owner_specified.shade.object' - }, { - u'op': u'add', - u'value': NO_MD5, - u'path': u'/owner_specified.shade.md5' - }, { - u'op': u'add', u'value': NO_SHA256, - u'path': u'/owner_specified.shade.sha256' - }], key=operator.itemgetter('value'))) - self.assertEqual( - self.adapter.request_history[14].headers['Content-Type'], - 'application/openstack-images-v2.1-json-patch') def _image_dict(self, fake_image): return self.cloud._normalize_image(meta.obj_to_dict(fake_image)) From a32d451690cc21fcec505193540d9ce21e5d0a42 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 17 Dec 2016 10:20:06 -0600 Subject: [PATCH 1168/3836] Let use_glance handle adding the entry to self.calls Change-Id: Iac4a6e6b428fac897104c25d5b139f3de40b21d7 --- shade/tests/unit/base.py | 3 +++ shade/tests/unit/test_image.py | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 52d4c3813..a86d657c2 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -169,6 +169,9 @@ def use_glance(self, image_version_json='image-version.json'): self.adapter.get( 'https://image.example.com/', text=open(discovery_fixture, 'r').read()) + self.calls += [ + dict(method='GET', url='https://image.example.com/'), + ] def assert_calls(self): self.assertEqual(len(self.calls), len(self.adapter.request_history)) diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index a4e717cca..309cd3ef4 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -169,7 +169,6 @@ def test_create_image_put_v2(self): is_public=False) self.calls += [ - dict(method='GET', url='https://image.example.com/'), dict(method='GET', url='https://image.example.com/v2/images'), dict( method='POST', @@ -294,7 +293,6 @@ def test_create_image_task(self, options=args) self.calls += [ - dict(method='GET', url='https://image.example.com/'), dict(method='GET', url='https://image.example.com/v2/images'), dict(method='GET', url='https://object-store.example.com/info'), dict( From 98f1bd57de405e92eff768c6a968d32e6b41a703 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 17 Dec 2016 07:13:19 -0600 Subject: [PATCH 1169/3836] Put in magnumclient service_type workaround Older versions of os-client-config only have an api version for 'container'. It's pretty easy to put in a workaround for that. Change-Id: I1edaa7ee416e4cc85bffb999a0e66b9494dabc37 --- shade/openstackcloud.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 807beadaf..b4a5bd3cf 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1085,8 +1085,15 @@ def trove_client(self): @property def magnum_client(self): if self._magnum_client is None: + # Workaround for os-client-config <=1.24.0 which thought of + # this as container rather than container-infra (so did we all) + version = self.cloud_config.get_api_version('container-infra') + if not version: + version = self.cloud_config.get_api_version('container') self._magnum_client = self._get_client( - 'container-infra', magnumclient.client.Client) + service_key='container-infra', + client_class=magnumclient.client.Client, + version=version) return self._magnum_client @property From 29d15878a7b2fe3596516a5f2e5aaa8f6554deb3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 17 Dec 2016 07:44:56 -0600 Subject: [PATCH 1170/3836] Replace mocks of swiftclient with request_mock The string handling around headers and swiftclient seems a little off. Note the b'' we had to add to the public acl as well as the str() in get_container_access and the str() around the header in test_object. The swift docs say that headers should be unicode strings - but in 3.5 we get bytes in some places that it seems like we should be getting strings. The b'' and str() casting make the tests work - but I'm not convinced that's the correct thing to do in these cases. Change-Id: I2d7c77eaab690f578eaba80490145bc16a23222c --- shade/openstackcloud.py | 18 +- shade/tests/unit/test_object.py | 724 ++++++++++++++++++++------------ 2 files changed, 475 insertions(+), 267 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index cb1826fb8..312afd0c5 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -71,8 +71,8 @@ OBJECT_CONTAINER_ACLS = { - 'public': ".r:*,.rlistings", - 'private': '', + 'public': b'.r:*,.rlistings', + 'private': b'', } @@ -5515,12 +5515,14 @@ def get_container_access(self, name): if not container: raise OpenStackCloudException("Container not found: %s" % name) acl = container.get('x-container-read', '') - try: - return [p for p, a in OBJECT_CONTAINER_ACLS.items() - if acl == a].pop() - except IndexError: - raise OpenStackCloudException( - "Could not determine container access for ACL: %s." % acl) + for key, value in OBJECT_CONTAINER_ACLS.items(): + # Convert to string for the comparison because swiftclient + # returns byte values as bytes sometimes and apparently == + # on bytes doesn't work like you'd think + if str(acl) == str(value): + return key + raise OpenStackCloudException( + "Could not determine container access for ACL: %s." % acl) def _get_file_hashes(self, filename): file_key = "{filename}:{mtime}".format( diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index 1c61697bf..1eea660c9 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -13,10 +13,6 @@ # License for the specific language governing permissions and limitations # under the License. -import mock -from os_client_config import cloud_config -from swiftclient import service as swift_service -from swiftclient import exceptions as swift_exc import testtools import shade @@ -25,293 +21,503 @@ from shade.tests.unit import base -class TestObject(base.TestCase): - - @mock.patch.object(cloud_config.CloudConfig, 'get_session') - def test_swift_client_no_endpoint(self, get_session_mock): - session_mock = mock.Mock() - session_mock.get_endpoint.return_value = None - get_session_mock.return_value = session_mock - e = self.assertRaises( - exc.OpenStackCloudException, lambda: self.cloud.swift_client) - self.assertIn( - 'Failed to instantiate object-store client.', str(e)) - - @mock.patch.object(shade.OpenStackCloud, 'auth_token') - @mock.patch.object(shade.OpenStackCloud, 'get_session_endpoint') - def test_swift_service(self, endpoint_mock, auth_mock): - endpoint_mock.return_value = 'slayer' - auth_mock.return_value = 'zulu' - self.assertIsInstance(self.cloud.swift_service, - swift_service.SwiftService) - endpoint_mock.assert_called_with(service_key='object-store') - - @mock.patch.object(shade.OpenStackCloud, 'get_session_endpoint') - def test_swift_service_no_endpoint(self, endpoint_mock): - endpoint_mock.side_effect = KeyError - e = self.assertRaises(exc.OpenStackCloudException, lambda: - self.cloud.swift_service) - self.assertIn( - 'Error constructing swift client', str(e)) - - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_create_container(self, mock_swift): - """Test creating a (private) container""" - name = 'test_container' - mock_swift.head_container.return_value = None +class TestObject(base.RequestsMockTestCase): - self.cloud.create_container(name) + def setUp(self): + super(TestObject, self).setUp() - expected_head_container_calls = [ - # once for exist test - mock.call(container=name), - # once for the final return - mock.call(container=name, skip_cache=True) - ] - self.assertTrue(expected_head_container_calls, - mock_swift.head_container.call_args_list) - mock_swift.put_container.assert_called_once_with(container=name) - # Because the default is 'private', we shouldn't be calling update - self.assertFalse(mock_swift.post_container.called) - - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_create_container_public(self, mock_swift): - """Test creating a public container""" - name = 'test_container' - mock_swift.head_container.return_value = None + self.container = self.getUniqueString() + self.object = self.getUniqueString() + self.endpoint = self.cloud._object_store_client.get_endpoint() + self.container_endpoint = '{endpoint}/{container}'.format( + endpoint=self.endpoint, container=self.container) + self.object_endpoint = '{endpoint}/{object}'.format( + endpoint=self.container_endpoint, object=self.object) - self.cloud.create_container(name, public=True) + def test_create_container(self): + """Test creating a (private) container""" + self.adapter.head( + self.container_endpoint, + [ + dict(status_code=404), + dict(headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}), + ]) + self.adapter.put( + self.container_endpoint, + status_code=201, + headers={ + 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=UTF-8', + }) + + self.cloud.create_container(self.container) + + self.calls += [ + dict(method='HEAD', url=self.container_endpoint), + dict(method='PUT', url=self.container_endpoint), + dict(method='HEAD', url=self.container_endpoint), + ] + self.assert_calls() - expected_head_container_calls = [ - # once for exist test - mock.call(container=name), - # once for the final return - mock.call(container=name, skip_cache=True) + def test_create_container_public(self): + """Test creating a public container""" + self.adapter.head( + self.container_endpoint, + [ + dict(status_code=404), + dict(headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}), + ]) + self.adapter.put( + self.container_endpoint, + status_code=201, + headers={ + 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=UTF-8', + }) + self.adapter.post( + self.container_endpoint, + status_code=201) + + self.cloud.create_container(self.container, public=True) + + self.calls += [ + dict(method='HEAD', url=self.container_endpoint), + dict( + method='PUT', + url=self.container_endpoint), + dict( + method='POST', + url=self.container_endpoint, + headers={ + 'x-container-read': + shade.openstackcloud.OBJECT_CONTAINER_ACLS['public']}), + dict(method='HEAD', url=self.container_endpoint), + ] + self.assert_calls() + + def test_create_container_exists(self): + """Test creating a container that exists.""" + self.adapter.head( + self.container_endpoint, + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}) + + container = self.cloud.create_container(self.container) + + self.calls += [ + dict(method='HEAD', url=self.container_endpoint), + ] + self.assert_calls() + self.assertIsNotNone(container) + + def test_delete_container(self): + self.adapter.delete(self.container_endpoint) + deleted = self.cloud.delete_container(self.container) + self.calls += [ + dict(method='DELETE', url=self.container_endpoint), ] - self.assertTrue(expected_head_container_calls, - mock_swift.head_container.call_args_list) - mock_swift.put_container.assert_called_once_with(container=name) - mock_swift.post_container.assert_called_once_with( - container=name, - headers={'x-container-read': - shade.openstackcloud.OBJECT_CONTAINER_ACLS['public']} - ) - - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_create_container_exists(self, mock_swift): - """Test creating a container that already exists""" - name = 'test_container' - fake_container = dict(id='1', name='name') - mock_swift.head_container.return_value = fake_container - container = self.cloud.create_container(name) - mock_swift.head_container.assert_called_once_with(container=name) - self.assertEqual(fake_container, container) - - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_delete_container(self, mock_swift): - name = 'test_container' - self.cloud.delete_container(name) - mock_swift.delete_container.assert_called_once_with(container=name) - - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_delete_container_404(self, mock_swift): + self.assert_calls() + # TODO(mordred) This should be True/False not None all the time + self.assertIsNone(deleted) + + def test_delete_container_404(self): """No exception when deleting a container that does not exist""" - name = 'test_container' - mock_swift.delete_container.side_effect = swift_exc.ClientException( - 'ERROR', http_status=404) - self.cloud.delete_container(name) - mock_swift.delete_container.assert_called_once_with(container=name) - - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_delete_container_error(self, mock_swift): + self.adapter.delete( + self.container_endpoint, + status_code=404) + deleted = self.cloud.delete_container(self.container) + self.calls += [ + dict(method='DELETE', url=self.container_endpoint), + ] + self.assert_calls() + # TODO(mordred) This should be True/False not None all the time + self.assertIsNone(deleted) + + def test_delete_container_error(self): """Non-404 swift error re-raised as OSCE""" - mock_swift.delete_container.side_effect = swift_exc.ClientException( - 'ERROR') - self.assertRaises(shade.OpenStackCloudException, - self.cloud.delete_container, '') - - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_update_container(self, mock_swift): - name = 'test_container' + self.adapter.delete( + self.container_endpoint, + status_code=409) + self.assertRaises( + shade.OpenStackCloudException, + self.cloud.delete_container, self.container) + self.calls += [ + dict(method='DELETE', url=self.container_endpoint), + ] + self.assert_calls() + + def test_update_container(self): + self.adapter.post( + self.container_endpoint, + status_code=204) headers = {'x-container-read': shade.openstackcloud.OBJECT_CONTAINER_ACLS['public']} - self.cloud.update_container(name, headers) - mock_swift.post_container.assert_called_once_with( - container=name, headers=headers) + assert_headers = headers.copy() + + self.cloud.update_container(self.container, headers) - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_update_container_error(self, mock_swift): + self.calls += [ + dict( + method='POST', + url=self.container_endpoint, + headers=assert_headers), + ] + self.assert_calls() + + def test_update_container_error(self): """Swift error re-raised as OSCE""" - mock_swift.post_container.side_effect = swift_exc.ClientException( - 'ERROR') - self.assertRaises(shade.OpenStackCloudException, - self.cloud.update_container, '', '') - - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_set_container_access_public(self, mock_swift): - name = 'test_container' - self.cloud.set_container_access(name, 'public') - mock_swift.post_container.assert_called_once_with( - container=name, - headers={'x-container-read': - shade.openstackcloud.OBJECT_CONTAINER_ACLS['public']}) - - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_set_container_access_private(self, mock_swift): - name = 'test_container' - self.cloud.set_container_access(name, 'private') - mock_swift.post_container.assert_called_once_with( - container=name, - headers={'x-container-read': - shade.openstackcloud.OBJECT_CONTAINER_ACLS['private']}) - - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_set_container_access_invalid(self, mock_swift): - self.assertRaises(shade.OpenStackCloudException, - self.cloud.set_container_access, '', 'invalid') - - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_get_container(self, mock_swift): - fake_container = { - 'x-container-read': - shade.openstackcloud.OBJECT_CONTAINER_ACLS['public'] - } - mock_swift.head_container.return_value = fake_container - access = self.cloud.get_container_access('foo') + # This test is of questionable value - the swift API docs do not + # declare error codes (other than 404 for the container) for this + # method, and I cannot make a synthetic failure to validate a real + # error code. So we're really just testing the shade adapter error + # raising logic here, rather than anything specific to swift. + self.adapter.post( + self.container_endpoint, + status_code=409) + self.assertRaises( + shade.OpenStackCloudException, + self.cloud.update_container, self.container, dict(foo='bar')) + + def test_set_container_access_public(self): + self.adapter.post( + self.container_endpoint, + status_code=204) + + self.cloud.set_container_access(self.container, 'public') + + self.calls += [ + dict( + method='POST', + url=self.container_endpoint, + headers={ + 'x-container-read': + shade.openstackcloud.OBJECT_CONTAINER_ACLS['public']}), + ] + self.assert_calls() + + def test_set_container_access_private(self): + self.adapter.post( + self.container_endpoint, + status_code=204) + + self.cloud.set_container_access(self.container, 'private') + + self.calls += [ + dict( + method='POST', + url=self.container_endpoint, + headers={ + 'x-container-read': + shade.openstackcloud.OBJECT_CONTAINER_ACLS['private']}), + ] + self.assert_calls() + + def test_set_container_access_invalid(self): + self.assertRaises( + shade.OpenStackCloudException, + self.cloud.set_container_access, self.container, 'invalid') + + def test_get_container_access(self): + self.adapter.head( + self.container_endpoint, + headers={ + 'x-container-read': + str(shade.openstackcloud.OBJECT_CONTAINER_ACLS['public'])}) + + access = self.cloud.get_container_access(self.container) self.assertEqual('public', access) - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_get_container_invalid(self, mock_swift): - fake_container = {'x-container-read': 'invalid'} - mock_swift.head_container.return_value = fake_container + def test_get_container_invalid(self): + self.adapter.head( + self.container_endpoint, + headers={ + 'x-container-read': 'invalid'}) + with testtools.ExpectedException( exc.OpenStackCloudException, "Could not determine container access for ACL: invalid" ): - self.cloud.get_container_access('foo') + self.cloud.get_container_access(self.container) - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_get_container_access_not_found(self, mock_swift): - name = 'invalid_container' - mock_swift.head_container.return_value = None + def test_get_container_access_not_found(self): + self.adapter.head( + self.container_endpoint, + status_code=404) with testtools.ExpectedException( exc.OpenStackCloudException, - "Container not found: %s" % name + "Container not found: %s" % self.container ): - self.cloud.get_container_access(name) + self.cloud.get_container_access(self.container) + + def test_list_containers(self): + # TODO(mordred) swiftclient sends format=json in the query string + # we'll want to send it in the accept header. Also, swiftclient + # always sends a second GET with marker set to the name of the last + # element returned previously. We should be able to infer if this is + # needed by checking swfit.account_listing_limit in the capabilities + # OR by looking at the value of 'X-Account-Container-Count' in the + # returned headers and seeing if it's larger than the number of + # containers returned. + first = '{endpoint}?format=json'.format( + endpoint=self.endpoint) + second = '{endpoint}?format=json&marker={first}'.format( + endpoint=self.endpoint, first=self.container) + containers = [ + {u'count': 0, u'bytes': 0, u'name': self.container}] + + self.adapter.get(first, complete_qs=True, json=containers) + self.adapter.get(second, complete_qs=True, status_code=204) - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_list_containers(self, mock_swift): - containers = [dict(id='1', name='containter1')] - mock_swift.get_account.return_value = ('response_headers', containers) ret = self.cloud.list_containers() - mock_swift.get_account.assert_called_once_with(full_listing=True) + self.calls += [ + dict(method='GET', url=first), + dict(method='GET', url=second), + ] + self.assert_calls() self.assertEqual(containers, ret) - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_list_containers_not_full(self, mock_swift): - containers = [dict(id='1', name='containter1')] - mock_swift.get_account.return_value = ('response_headers', containers) + def test_list_containers_not_full(self): + endpoint = '{endpoint}?format=json'.format( + endpoint=self.endpoint) + containers = [ + {u'count': 0, u'bytes': 0, u'name': self.container}] + + self.adapter.get(endpoint, complete_qs=True, json=containers) + ret = self.cloud.list_containers(full_listing=False) - mock_swift.get_account.assert_called_once_with(full_listing=False) + + self.calls += [ + dict(method='GET', url=endpoint), + ] + self.assert_calls() self.assertEqual(containers, ret) - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_list_containers_exception(self, mock_swift): - mock_swift.get_account.side_effect = swift_exc.ClientException("ERROR") - self.assertRaises(exc.OpenStackCloudException, - self.cloud.list_containers) - - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_list_objects(self, mock_swift): - objects = [dict(id='1', name='object1')] - mock_swift.get_container.return_value = ('response_headers', objects) - ret = self.cloud.list_objects('container_name') - mock_swift.get_container.assert_called_once_with( - container='container_name', full_listing=True) + def test_list_containers_exception(self): + # TODO(mordred) There are no error codes I can see. The 409 is fake. + endpoint = '{endpoint}?format=json'.format( + endpoint=self.endpoint) + self.adapter.get(endpoint, complete_qs=True, status_code=409) + + self.assertRaises( + exc.OpenStackCloudException, self.cloud.list_containers) + + def test_list_objects(self): + first = '{endpoint}?format=json'.format( + endpoint=self.container_endpoint) + second = '{endpoint}?format=json&marker={first}'.format( + endpoint=self.container_endpoint, first=self.object) + + objects = [{ + u'bytes': 20304400896, + u'last_modified': u'2016-12-15T13:34:13.650090', + u'hash': u'daaf9ed2106d09bba96cf193d866445e', + u'name': self.object, + u'content_type': u'application/octet-stream'}] + + self.adapter.get(first, complete_qs=True, json=objects) + self.adapter.get(second, complete_qs=True, status_code=204) + + ret = self.cloud.list_objects(self.container) + + self.calls += [ + dict(method='GET', url=first), + dict(method='GET', url=second), + ] + self.assert_calls() self.assertEqual(objects, ret) - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_list_objects_not_full(self, mock_swift): - objects = [dict(id='1', name='object1')] - mock_swift.get_container.return_value = ('response_headers', objects) - ret = self.cloud.list_objects('container_name', full_listing=False) - mock_swift.get_container.assert_called_once_with( - container='container_name', full_listing=False) + def test_list_objects_not_full(self): + endpoint = '{endpoint}?format=json'.format( + endpoint=self.container_endpoint) + + objects = [{ + u'bytes': 20304400896, + u'last_modified': u'2016-12-15T13:34:13.650090', + u'hash': u'daaf9ed2106d09bba96cf193d866445e', + u'name': self.object, + u'content_type': u'application/octet-stream'}] + + self.adapter.get(endpoint, complete_qs=True, json=objects) + + ret = self.cloud.list_objects(self.container, full_listing=False) + + self.calls += [ + dict(method='GET', url=endpoint), + ] + self.assert_calls() self.assertEqual(objects, ret) - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_list_objects_exception(self, mock_swift): - mock_swift.get_container.side_effect = swift_exc.ClientException( - "ERROR") - self.assertRaises(exc.OpenStackCloudException, - self.cloud.list_objects, 'container_name') - - @mock.patch.object(shade.OpenStackCloud, 'get_object_metadata') - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_delete_object(self, mock_swift, mock_get_meta): - container_name = 'container_name' - object_name = 'object_name' - mock_get_meta.return_value = {'object': object_name} - self.assertTrue(self.cloud.delete_object(container_name, object_name)) - mock_get_meta.assert_called_once_with(container_name, object_name) - mock_swift.delete_object.assert_called_once_with( - container=container_name, obj=object_name - ) - - @mock.patch.object(shade.OpenStackCloud, 'get_object_metadata') - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_delete_object_not_found(self, mock_swift, mock_get_meta): - container_name = 'container_name' - object_name = 'object_name' - mock_get_meta.return_value = None - self.assertFalse(self.cloud.delete_object(container_name, object_name)) - mock_get_meta.assert_called_once_with(container_name, object_name) - self.assertFalse(mock_swift.delete_object.called) - - @mock.patch.object(shade.OpenStackCloud, 'get_object_metadata') - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_delete_object_exception(self, mock_swift, mock_get_meta): - container_name = 'container_name' - object_name = 'object_name' - mock_get_meta.return_value = {'object': object_name} - mock_swift.delete_object.side_effect = swift_exc.ClientException( - "ERROR") - self.assertRaises(shade.OpenStackCloudException, - self.cloud.delete_object, - container_name, object_name) - - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_get_object(self, mock_swift): - fake_resp = ({'headers': 'yup'}, 'test body') - mock_swift.get_object.return_value = fake_resp - container_name = 'container_name' - object_name = 'object_name' - resp = self.cloud.get_object(container_name, object_name) - self.assertEqual(fake_resp, resp) - - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_get_object_not_found(self, mock_swift): - mock_swift.get_object.side_effect = swift_exc.ClientException( - 'ERROR', http_status=404) - container_name = 'container_name' - object_name = 'object_name' - self.assertIsNone(self.cloud.get_object(container_name, object_name)) - mock_swift.get_object.assert_called_once_with( - container=container_name, obj=object_name, - query_string=None, resp_chunk_size=None) - - @mock.patch.object(shade.OpenStackCloud, 'swift_client') - def test_get_object_exception(self, mock_swift): - mock_swift.get_object.side_effect = swift_exc.ClientException("ERROR") - container_name = 'container_name' - object_name = 'object_name' - self.assertRaises(shade.OpenStackCloudException, - self.cloud.get_object, - container_name, object_name) - - -class TestRESTObject(base.RequestsMockTestCase): + def test_list_objects_exception(self): + endpoint = '{endpoint}?format=json'.format( + endpoint=self.container_endpoint) + self.adapter.get(endpoint, complete_qs=True, status_code=409) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.list_objects, self.container) + + def test_delete_object(self): + # TODO(mordred) calling get_object_metadata first is stupid. We should + # just make the delete call and if it 404's, then the object didn't + # exist. + self.adapter.head( + self.object_endpoint, + headers={ + 'Content-Length': '20304400896', + 'Content-Type': 'application/octet-stream', + 'Accept-Ranges': 'bytes', + 'Last-Modified': 'Thu, 15 Dec 2016 13:34:14 GMT', + 'Etag': '"b5c454b44fbd5344793e3fb7e3850768"', + 'X-Timestamp': '1481808853.65009', + 'X-Trans-Id': 'tx68c2a2278f0c469bb6de1-005857ed80dfw1', + 'Date': 'Mon, 19 Dec 2016 14:24:00 GMT', + 'X-Static-Large-Object': 'True', + 'X-Object-Meta-Mtime': '1481513709.168512', + }) + self.adapter.delete(self.object_endpoint, status_code=204) + + self.assertTrue(self.cloud.delete_object(self.container, self.object)) + + self.calls += [ + dict(method='HEAD', url=self.object_endpoint), + dict(method='DELETE', url=self.object_endpoint), + ] + self.assert_calls() + + def test_delete_object_not_found(self): + self.adapter.head(self.object_endpoint, status_code=404) + + self.assertFalse(self.cloud.delete_object(self.container, self.object)) + + self.calls += [ + dict(method='HEAD', url=self.object_endpoint), + ] + self.assert_calls() + + def test_delete_object_exception(self): + self.adapter.head( + self.object_endpoint, + headers={ + 'Content-Length': '20304400896', + 'Content-Type': 'application/octet-stream', + 'Accept-Ranges': 'bytes', + 'Last-Modified': 'Thu, 15 Dec 2016 13:34:14 GMT', + 'Etag': '"b5c454b44fbd5344793e3fb7e3850768"', + 'X-Timestamp': '1481808853.65009', + 'X-Trans-Id': 'tx68c2a2278f0c469bb6de1-005857ed80dfw1', + 'Date': 'Mon, 19 Dec 2016 14:24:00 GMT', + 'X-Static-Large-Object': 'True', + 'X-Object-Meta-Mtime': '1481513709.168512', + }) + + # TODO(mordred) This version of the code is prone to race conditions + # When we stop doing HEAD first, we can kill this test. + self.adapter.delete(self.object_endpoint, status_code=404) + + self.assertRaises( + shade.OpenStackCloudException, + self.cloud.delete_object, + self.container, + self.object) + + self.calls += [ + dict(method='HEAD', url=self.object_endpoint), + dict(method='DELETE', url=self.object_endpoint), + ] + self.assert_calls() + + def test_get_object(self): + headers = { + 'Content-Length': '20304400896', + 'Content-Type': 'application/octet-stream', + 'Accept-Ranges': 'bytes', + 'Last-Modified': 'Thu, 15 Dec 2016 13:34:14 GMT', + 'Etag': '"b5c454b44fbd5344793e3fb7e3850768"', + 'X-Timestamp': '1481808853.65009', + 'X-Trans-Id': 'tx68c2a2278f0c469bb6de1-005857ed80dfw1', + 'Date': 'Mon, 19 Dec 2016 14:24:00 GMT', + 'X-Static-Large-Object': 'True', + 'X-Object-Meta-Mtime': '1481513709.168512', + } + response_headers = {k.lower(): v for k, v in headers.items()} + # TODO(mordred) Is this a bug in requests_mock or swiftclient that + # I have to mark this as b'test body' ? + text = b'test body' + self.adapter.get( + self.object_endpoint, + headers={ + 'Content-Length': '20304400896', + 'Content-Type': 'application/octet-stream', + 'Accept-Ranges': 'bytes', + 'Last-Modified': 'Thu, 15 Dec 2016 13:34:14 GMT', + 'Etag': '"b5c454b44fbd5344793e3fb7e3850768"', + 'X-Timestamp': '1481808853.65009', + 'X-Trans-Id': 'tx68c2a2278f0c469bb6de1-005857ed80dfw1', + 'Date': 'Mon, 19 Dec 2016 14:24:00 GMT', + 'X-Static-Large-Object': 'True', + 'X-Object-Meta-Mtime': '1481513709.168512', + }, + text='test body') + + resp = self.cloud.get_object(self.container, self.object) + + self.calls += [ + dict(method='GET', url=self.object_endpoint), + ] + self.assert_calls() + + self.assertEqual((response_headers, text), resp) + + def test_get_object_not_found(self): + self.adapter.get(self.object_endpoint, status_code=404) + + self.assertIsNone(self.cloud.get_object(self.container, self.object)) + + self.calls += [ + dict(method='GET', url=self.object_endpoint), + ] + self.assert_calls() + + def test_get_object_exception(self): + # TODO(mordred) Bogus error code - what are we testing here? + self.adapter.get(self.object_endpoint, status_code=409) + + self.assertRaises( + shade.OpenStackCloudException, + self.cloud.get_object, + self.container, self.object) + + self.calls += [ + dict(method='GET', url=self.object_endpoint), + ] + self.assert_calls() def test_get_object_segment_size(self): self.adapter.get( From de9a51958568bd7f93dcb12edebd30fcea674c51 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 19 Dec 2016 09:03:16 -0600 Subject: [PATCH 1171/3836] Stop calling HEAD before DELETE for objects It's totally inefficient. Delete throws a 404 if the object isn't there. There's no reason to do a HEAD first. Change-Id: I1e7e0a37d12d15f79786e87dc87afc047495e142 --- shade/openstackcloud.py | 10 ++----- shade/tests/unit/test_object.py | 52 +-------------------------------- 2 files changed, 4 insertions(+), 58 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 312afd0c5..76c23bd0d 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5734,16 +5734,12 @@ def delete_object(self, container, name): :raises: OpenStackCloudException on operation error. """ - if not self.get_object_metadata(container, name): - return False try: self.manager.submit_task(_tasks.ObjectDelete( container=container, obj=name)) - except swift_exceptions.ClientException as e: - raise OpenStackCloudException( - "Object deletion failed: %s (%s/%s)" % ( - e.http_reason, e.http_host, e.http_path)) - return True + return True + except swift_exceptions.ClientException: + return False def get_object_metadata(self, container, name): try: diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index 1eea660c9..145e3f1b9 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -384,71 +384,21 @@ def test_list_objects_exception(self): self.cloud.list_objects, self.container) def test_delete_object(self): - # TODO(mordred) calling get_object_metadata first is stupid. We should - # just make the delete call and if it 404's, then the object didn't - # exist. - self.adapter.head( - self.object_endpoint, - headers={ - 'Content-Length': '20304400896', - 'Content-Type': 'application/octet-stream', - 'Accept-Ranges': 'bytes', - 'Last-Modified': 'Thu, 15 Dec 2016 13:34:14 GMT', - 'Etag': '"b5c454b44fbd5344793e3fb7e3850768"', - 'X-Timestamp': '1481808853.65009', - 'X-Trans-Id': 'tx68c2a2278f0c469bb6de1-005857ed80dfw1', - 'Date': 'Mon, 19 Dec 2016 14:24:00 GMT', - 'X-Static-Large-Object': 'True', - 'X-Object-Meta-Mtime': '1481513709.168512', - }) self.adapter.delete(self.object_endpoint, status_code=204) self.assertTrue(self.cloud.delete_object(self.container, self.object)) self.calls += [ - dict(method='HEAD', url=self.object_endpoint), dict(method='DELETE', url=self.object_endpoint), ] self.assert_calls() def test_delete_object_not_found(self): - self.adapter.head(self.object_endpoint, status_code=404) - - self.assertFalse(self.cloud.delete_object(self.container, self.object)) - - self.calls += [ - dict(method='HEAD', url=self.object_endpoint), - ] - self.assert_calls() - - def test_delete_object_exception(self): - self.adapter.head( - self.object_endpoint, - headers={ - 'Content-Length': '20304400896', - 'Content-Type': 'application/octet-stream', - 'Accept-Ranges': 'bytes', - 'Last-Modified': 'Thu, 15 Dec 2016 13:34:14 GMT', - 'Etag': '"b5c454b44fbd5344793e3fb7e3850768"', - 'X-Timestamp': '1481808853.65009', - 'X-Trans-Id': 'tx68c2a2278f0c469bb6de1-005857ed80dfw1', - 'Date': 'Mon, 19 Dec 2016 14:24:00 GMT', - 'X-Static-Large-Object': 'True', - 'X-Object-Meta-Mtime': '1481513709.168512', - }) - - # TODO(mordred) This version of the code is prone to race conditions - # When we stop doing HEAD first, we can kill this test. self.adapter.delete(self.object_endpoint, status_code=404) - self.assertRaises( - shade.OpenStackCloudException, - self.cloud.delete_object, - self.container, - self.object) + self.assertFalse(self.cloud.delete_object(self.container, self.object)) self.calls += [ - dict(method='HEAD', url=self.object_endpoint), dict(method='DELETE', url=self.object_endpoint), ] self.assert_calls() From 4d9cac215b577ef93d08268e00a7d98635ffd9bb Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 19 Dec 2016 09:34:20 -0600 Subject: [PATCH 1172/3836] Stop using full_listing in prep for REST calls The full_listing parameter to swiftclient causes it to do a batching/paging approach all the time. We can do it smarter, but for now, just remove it so that we can do an equal logic REST conversion. Then we'll add paging support smartly to the REST calls and always do it. Change-Id: I743871c5f87fba28d16b38fe23c51fd0cdd7eecb --- shade/openstackcloud.py | 22 +++++++++++-- shade/tests/unit/test_object.py | 56 ++------------------------------- 2 files changed, 21 insertions(+), 57 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 76c23bd0d..13690f8ea 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5445,9 +5445,16 @@ def delete_server_group(self, name_or_id): return True def list_containers(self, full_listing=True): + """List containers. + + :param full_listing: Ignored. Present for backwards compat + + :returns: list of Munch of the container objects + + :raises: OpenStackCloudException on operation error. + """ try: - return self.manager.submit_task(_tasks.ContainerList( - full_listing=full_listing)) + return self.manager.submit_task(_tasks.ContainerList()) except swift_exceptions.ClientException as e: raise OpenStackCloudException( "Container list failed: %s (%s/%s)" % ( @@ -5716,9 +5723,18 @@ def update_object(self, container, name, metadata=None, **headers): e.http_reason, e.http_host, e.http_path)) def list_objects(self, container, full_listing=True): + """List objects. + + :param container: Name of the container to list objects in. + :param full_listing: Ignored. Present for backwards compat + + :returns: list of Munch of the objects + + :raises: OpenStackCloudException on operation error. + """ try: return self.manager.submit_task(_tasks.ObjectList( - container=container, full_listing=full_listing)) + container=container)) except swift_exceptions.ClientException as e: raise OpenStackCloudException( "Object list failed: %s (%s/%s)" % ( diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index 145e3f1b9..8d23605ab 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -278,33 +278,6 @@ def test_get_container_access_not_found(self): self.cloud.get_container_access(self.container) def test_list_containers(self): - # TODO(mordred) swiftclient sends format=json in the query string - # we'll want to send it in the accept header. Also, swiftclient - # always sends a second GET with marker set to the name of the last - # element returned previously. We should be able to infer if this is - # needed by checking swfit.account_listing_limit in the capabilities - # OR by looking at the value of 'X-Account-Container-Count' in the - # returned headers and seeing if it's larger than the number of - # containers returned. - first = '{endpoint}?format=json'.format( - endpoint=self.endpoint) - second = '{endpoint}?format=json&marker={first}'.format( - endpoint=self.endpoint, first=self.container) - containers = [ - {u'count': 0, u'bytes': 0, u'name': self.container}] - - self.adapter.get(first, complete_qs=True, json=containers) - self.adapter.get(second, complete_qs=True, status_code=204) - - ret = self.cloud.list_containers() - self.calls += [ - dict(method='GET', url=first), - dict(method='GET', url=second), - ] - self.assert_calls() - self.assertEqual(containers, ret) - - def test_list_containers_not_full(self): endpoint = '{endpoint}?format=json'.format( endpoint=self.endpoint) containers = [ @@ -312,7 +285,7 @@ def test_list_containers_not_full(self): self.adapter.get(endpoint, complete_qs=True, json=containers) - ret = self.cloud.list_containers(full_listing=False) + ret = self.cloud.list_containers() self.calls += [ dict(method='GET', url=endpoint), @@ -330,31 +303,6 @@ def test_list_containers_exception(self): exc.OpenStackCloudException, self.cloud.list_containers) def test_list_objects(self): - first = '{endpoint}?format=json'.format( - endpoint=self.container_endpoint) - second = '{endpoint}?format=json&marker={first}'.format( - endpoint=self.container_endpoint, first=self.object) - - objects = [{ - u'bytes': 20304400896, - u'last_modified': u'2016-12-15T13:34:13.650090', - u'hash': u'daaf9ed2106d09bba96cf193d866445e', - u'name': self.object, - u'content_type': u'application/octet-stream'}] - - self.adapter.get(first, complete_qs=True, json=objects) - self.adapter.get(second, complete_qs=True, status_code=204) - - ret = self.cloud.list_objects(self.container) - - self.calls += [ - dict(method='GET', url=first), - dict(method='GET', url=second), - ] - self.assert_calls() - self.assertEqual(objects, ret) - - def test_list_objects_not_full(self): endpoint = '{endpoint}?format=json'.format( endpoint=self.container_endpoint) @@ -367,7 +315,7 @@ def test_list_objects_not_full(self): self.adapter.get(endpoint, complete_qs=True, json=objects) - ret = self.cloud.list_objects(self.container, full_listing=False) + ret = self.cloud.list_objects(self.container) self.calls += [ dict(method='GET', url=endpoint), From 9e9e2e702253540f28ca994f1c96f9b1fb82c8d5 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 19 Dec 2016 09:30:15 -0600 Subject: [PATCH 1173/3836] Switch swift calls to REST Added support for calls that directly return lists to the adapter. There are a few minor changes in the test suite. swiftclient appended query strings differently than how requests is doing it. A comment was adding clarifying why we're testing a 409. And the b'test body' workaround for python3 issue we saw with request_mock and swiftclient does not occur now, so we removed the b''. Change-Id: I7e962f99cbab07ed1d4fea6e2c6df10776a4be80 --- shade/_adapter.py | 10 ++- shade/_tasks.py | 45 ----------- shade/openstackcloud.py | 137 ++++++++++++++------------------ shade/tests/unit/test_object.py | 9 +-- 4 files changed, 70 insertions(+), 131 deletions(-) diff --git a/shade/_adapter.py b/shade/_adapter.py index 6a2c88cd9..d15214a00 100644 --- a/shade/_adapter.py +++ b/shade/_adapter.py @@ -107,6 +107,12 @@ def _munch_response(self, response, result_key=None): reason=response.reason)) raise + request_id = response.headers.get('x-openstack-request-id') + + if task_manager._is_listlike(result_json): + return meta.obj_list_to_dict( + result_json, request_id=request_id) + # Wrap the keys() call in list() because in python3 keys returns # a "dict_keys" iterator-like object rather than a list json_keys = list(result_json.keys()) @@ -128,11 +134,9 @@ def _munch_response(self, response, result_key=None): # come through without a top-level container result = result_json - request_id = response.headers.get('x-openstack-request-id') - if task_manager._is_listlike(result): return meta.obj_list_to_dict(result, request_id=request_id) - elif task_manager._is_objlike(result): + if task_manager._is_objlike(result): return meta.obj_to_dict(result, request_id=request_id) return result diff --git a/shade/_tasks.py b/shade/_tasks.py index 88b4423ee..95c0002cf 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -466,56 +466,11 @@ def main(self, client): return client.nova_client.floating_ip_pools.list() -class ContainerGet(task_manager.Task): - def main(self, client): - return client.swift_client.head_container(**self.args) - - -class ContainerCreate(task_manager.Task): - def main(self, client): - client.swift_client.put_container(**self.args) - - -class ContainerDelete(task_manager.Task): - def main(self, client): - client.swift_client.delete_container(**self.args) - - -class ContainerUpdate(task_manager.Task): - def main(self, client): - client.swift_client.post_container(**self.args) - - -class ContainerList(task_manager.Task): - def main(self, client): - return client.swift_client.get_account(**self.args)[1] - - -class ObjectDelete(task_manager.Task): - def main(self, client): - return client.swift_client.delete_object(**self.args) - - class ObjectCreate(task_manager.Task): def main(self, client): return client.swift_service.upload(**self.args) -class ObjectUpdate(task_manager.Task): - def main(self, client): - return client.swift_client.post_object(**self.args) - - -class ObjectList(task_manager.Task): - def main(self, client): - return client.swift_client.get_container(**self.args)[1] - - -class ObjectGet(task_manager.Task): - def main(self, client): - return client.swift_client.get_object(**self.args) - - class SubnetCreate(task_manager.Task): def main(self, client): return client.neutron_client.create_subnet(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 13690f8ea..f1154e061 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -40,9 +40,7 @@ import neutronclient.neutron.client import novaclient.client import novaclient.exceptions as nova_exceptions -import swiftclient.client import swiftclient.service -import swiftclient.exceptions as swift_exceptions import troveclient.client import designateclient.client @@ -71,8 +69,8 @@ OBJECT_CONTAINER_ACLS = { - 'public': b'.r:*,.rlistings', - 'private': b'', + 'public': '.r:*,.rlistings', + 'private': '', } @@ -299,13 +297,11 @@ def invalidate(self): self._keystone_client = None self._neutron_client = None self._nova_client = None - self._swift_client = None self._swift_service = None # Lock used to reset swift client. Since swift client does not # support keystone sessions, we we have to make a new client # in order to get new auth prior to operations, otherwise # long-running sessions will fail. - self._swift_client_lock = threading.Lock() self._swift_service_lock = threading.Lock() self._trove_client = None self._designate_client = None @@ -1019,11 +1015,18 @@ def get_template_contents( @property def swift_client(self): - with self._swift_client_lock: - if self._swift_client is None: - self._swift_client = self._get_client( - 'object-store', swiftclient.client.Connection) - return self._swift_client + warnings.warn( + 'Using shade to get a swift object is deprecated. If you' + ' need a raw swiftclient.Connection object, please use' + ' make_legacy_client in os-client-config instead') + try: + import swiftclient.client + except ImportError: + self.log.error( + 'swiftclient is no longer a dependency of shade. You need to' + ' install python-swiftclient directly.') + return self._get_client( + 'object-store', swiftclient.client.Connection) def _get_swift_kwargs(self): auth_version = self.cloud_config.get_api_version('identity') @@ -3319,8 +3322,6 @@ def _upload_image_task( image_kwargs.update(parameters) # get new client sessions - with self._swift_client_lock: - self._swift_client = None with self._swift_service_lock: self._swift_service = None @@ -5453,61 +5454,44 @@ def list_containers(self, full_listing=True): :raises: OpenStackCloudException on operation error. """ - try: - return self.manager.submit_task(_tasks.ContainerList()) - except swift_exceptions.ClientException as e: - raise OpenStackCloudException( - "Container list failed: %s (%s/%s)" % ( - e.http_reason, e.http_host, e.http_path)) + return self._object_store_client.get('/', params=dict(format='json')) def get_container(self, name, skip_cache=False): if skip_cache or name not in self._container_cache: try: - container = self.manager.submit_task( - _tasks.ContainerGet(container=name)) - self._container_cache[name] = container - except swift_exceptions.ClientException as e: - if e.http_status == 404: + container = self._object_store_client.head(name) + self._container_cache[name] = container.headers + except OpenStackCloudHTTPError as e: + if e.response.status_code == 404: return None - raise OpenStackCloudException( - "Container fetch failed: %s (%s/%s)" % ( - e.http_reason, e.http_host, e.http_path)) + raise return self._container_cache[name] def create_container(self, name, public=False): container = self.get_container(name) if container: return container - try: - self.manager.submit_task( - _tasks.ContainerCreate(container=name)) - if public: - self.set_container_access(name, 'public') - return self.get_container(name, skip_cache=True) - except swift_exceptions.ClientException as e: - raise OpenStackCloudException( - "Container creation failed: %s (%s/%s)" % ( - e.http_reason, e.http_host, e.http_path)) + self._object_store_client.put(name) + if public: + self.set_container_access(name, 'public') + return self.get_container(name, skip_cache=True) def delete_container(self, name): try: - self.manager.submit_task( - _tasks.ContainerDelete(container=name)) - except swift_exceptions.ClientException as e: - if e.http_status == 404: + self._object_store_client.delete(name) + except OpenStackCloudHTTPError as e: + if e.response.status_code == 404: return - raise OpenStackCloudException( - "Container deletion failed: %s (%s/%s)" % ( - e.http_reason, e.http_host, e.http_path)) + if e.response.status_code == 409: + raise OpenStackCloudException( + 'Attempt to delete container {container} failed. The' + ' container is not empty. Please delete the objects' + ' inside it before deleting the container'.format( + container=name)) + raise def update_container(self, name, headers): - try: - self.manager.submit_task( - _tasks.ContainerUpdate(container=name, headers=headers)) - except swift_exceptions.ClientException as e: - raise OpenStackCloudException( - "Container update failed: %s (%s/%s)" % ( - e.http_reason, e.http_host, e.http_path)) + self._object_store_client.post(name, headers=headers) def set_container_access(self, name, access): if access not in OBJECT_CONTAINER_ACLS: @@ -5713,14 +5697,10 @@ def update_object(self, container, name, metadata=None, **headers): headers = dict(headers, **metadata_headers) - try: - return self.manager.submit_task( - _tasks.ObjectUpdate(container=container, obj=name, - headers=headers)) - except swift_exceptions.ClientException as e: - raise OpenStackCloudException( - "Object update failed: %s (%s/%s)" % ( - e.http_reason, e.http_host, e.http_path)) + return self._object_store_client.post( + '{container}/{object}'.format( + container=container, object=name), + headers=headers) def list_objects(self, container, full_listing=True): """List objects. @@ -5732,13 +5712,8 @@ def list_objects(self, container, full_listing=True): :raises: OpenStackCloudException on operation error. """ - try: - return self.manager.submit_task(_tasks.ObjectList( - container=container)) - except swift_exceptions.ClientException as e: - raise OpenStackCloudException( - "Object list failed: %s (%s/%s)" % ( - e.http_reason, e.http_host, e.http_path)) + return self._object_store_client.get( + container, params=dict(format='json')) def delete_object(self, container, name): """Delete an object from a container. @@ -5751,16 +5726,17 @@ def delete_object(self, container, name): :raises: OpenStackCloudException on operation error. """ try: - self.manager.submit_task(_tasks.ObjectDelete( - container=container, obj=name)) + self._object_store_client.delete( + '{container}/{object}'.format( + container=container, object=name)) return True - except swift_exceptions.ClientException: + except OpenStackCloudHTTPError: return False def get_object_metadata(self, container, name): try: return self._object_store_client.head( - '/{container}/{object}'.format( + '{container}/{object}'.format( container=container, object=name)).headers except OpenStackCloudException as e: if e.response.status_code == 404: @@ -5781,16 +5757,21 @@ def get_object(self, container, obj, query_string=None, is not found (404) :raises: OpenStackCloudException on operation error. """ + # TODO(mordred) implement resp_chunk_size try: - return self.manager.submit_task(_tasks.ObjectGet( - container=container, obj=obj, query_string=query_string, - resp_chunk_size=resp_chunk_size)) - except swift_exceptions.ClientException as e: - if e.http_status == 404: + endpoint = '{container}/{object}'.format( + container=container, object=obj) + if query_string: + endpoint = '{endpoint}?{query_string}'.format( + endpoint=endpoint, query_string=query_string) + response = self._object_store_client.get(endpoint) + response_headers = { + k.lower(): v for k, v in response.headers.items()} + return (response_headers, response.text) + except OpenStackCloudHTTPError as e: + if e.response.status_code == 404: return None - raise OpenStackCloudException( - "Object fetch failed: %s (%s/%s)" % ( - e.http_reason, e.http_host, e.http_path)) + raise def create_subnet(self, network_name_or_id, cidr, ip_version=4, enable_dhcp=False, subnet_name=None, tenant_id=None, diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index 8d23605ab..863f67b30 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -163,6 +163,7 @@ def test_delete_container_404(self): def test_delete_container_error(self): """Non-404 swift error re-raised as OSCE""" + # 409 happens if the container is not empty self.adapter.delete( self.container_endpoint, status_code=409) @@ -278,7 +279,7 @@ def test_get_container_access_not_found(self): self.cloud.get_container_access(self.container) def test_list_containers(self): - endpoint = '{endpoint}?format=json'.format( + endpoint = '{endpoint}/?format=json'.format( endpoint=self.endpoint) containers = [ {u'count': 0, u'bytes': 0, u'name': self.container}] @@ -295,7 +296,7 @@ def test_list_containers(self): def test_list_containers_exception(self): # TODO(mordred) There are no error codes I can see. The 409 is fake. - endpoint = '{endpoint}?format=json'.format( + endpoint = '{endpoint}/?format=json'.format( endpoint=self.endpoint) self.adapter.get(endpoint, complete_qs=True, status_code=409) @@ -365,9 +366,7 @@ def test_get_object(self): 'X-Object-Meta-Mtime': '1481513709.168512', } response_headers = {k.lower(): v for k, v in headers.items()} - # TODO(mordred) Is this a bug in requests_mock or swiftclient that - # I have to mark this as b'test body' ? - text = b'test body' + text = 'test body' self.adapter.get( self.object_endpoint, headers={ From c8a3952152d9a5e7bb0d0007c94ee61f4e9db2a4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 19 Dec 2016 11:18:52 -0600 Subject: [PATCH 1174/3836] Make delete_object return True and False Delete methods in shade return True if they deleted something and False if they didn't. Change-Id: Id5b1cea3bf582783ca5bdcd865435b27855fb0a6 --- shade/openstackcloud.py | 3 ++- shade/tests/unit/test_object.py | 12 ++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index f1154e061..9bf16a81e 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5479,9 +5479,10 @@ def create_container(self, name, public=False): def delete_container(self, name): try: self._object_store_client.delete(name) + return True except OpenStackCloudHTTPError as e: if e.response.status_code == 404: - return + return False if e.response.status_code == 409: raise OpenStackCloudException( 'Attempt to delete container {container} failed. The' diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index 863f67b30..b72ea21c6 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -140,26 +140,26 @@ def test_create_container_exists(self): def test_delete_container(self): self.adapter.delete(self.container_endpoint) - deleted = self.cloud.delete_container(self.container) + + self.assertTrue(self.cloud.delete_container(self.container)) + self.calls += [ dict(method='DELETE', url=self.container_endpoint), ] self.assert_calls() - # TODO(mordred) This should be True/False not None all the time - self.assertIsNone(deleted) def test_delete_container_404(self): """No exception when deleting a container that does not exist""" self.adapter.delete( self.container_endpoint, status_code=404) - deleted = self.cloud.delete_container(self.container) + + self.assertFalse(self.cloud.delete_container(self.container)) + self.calls += [ dict(method='DELETE', url=self.container_endpoint), ] self.assert_calls() - # TODO(mordred) This should be True/False not None all the time - self.assertIsNone(deleted) def test_delete_container_error(self): """Non-404 swift error re-raised as OSCE""" From 45737abbbaa86e0b3073d09fdacd8cd1c9355a86 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 19 Dec 2016 11:26:00 -0600 Subject: [PATCH 1175/3836] Update swift exception tests to use 416 On GET queries, swift can return 416 which indicates an invalid range was requested. This is a thing where we'll just throw the error to the user, so makes it a good thing to test. Change-Id: I6b4274c42b7f708d1c7707535715321bb337f97c --- shade/tests/unit/test_object.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index b72ea21c6..89139cc0b 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -295,10 +295,9 @@ def test_list_containers(self): self.assertEqual(containers, ret) def test_list_containers_exception(self): - # TODO(mordred) There are no error codes I can see. The 409 is fake. endpoint = '{endpoint}/?format=json'.format( endpoint=self.endpoint) - self.adapter.get(endpoint, complete_qs=True, status_code=409) + self.adapter.get(endpoint, complete_qs=True, status_code=416) self.assertRaises( exc.OpenStackCloudException, self.cloud.list_containers) @@ -327,7 +326,7 @@ def test_list_objects(self): def test_list_objects_exception(self): endpoint = '{endpoint}?format=json'.format( endpoint=self.container_endpoint) - self.adapter.get(endpoint, complete_qs=True, status_code=409) + self.adapter.get(endpoint, complete_qs=True, status_code=416) self.assertRaises( exc.OpenStackCloudException, self.cloud.list_objects, self.container) @@ -403,8 +402,7 @@ def test_get_object_not_found(self): self.assert_calls() def test_get_object_exception(self): - # TODO(mordred) Bogus error code - what are we testing here? - self.adapter.get(self.object_endpoint, status_code=409) + self.adapter.get(self.object_endpoint, status_code=416) self.assertRaises( shade.OpenStackCloudException, From 4c1494cd921ebd6258406ab2aa1d1970dd4461b4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 19 Dec 2016 11:39:11 -0600 Subject: [PATCH 1176/3836] Make assert_calls a bit more readable Output helpful error message about _which_ of the list of calls is broken if something is broken. Also, use zip and enumerate to save us having to dereference [x] all over the place. Change-Id: I8f07d70450251c45038c8123274424e1b0097065 --- shade/tests/unit/base.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index a86d657c2..321e9bb2c 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -175,21 +175,23 @@ def use_glance(self, image_version_json='image-version.json'): def assert_calls(self): self.assertEqual(len(self.calls), len(self.adapter.request_history)) - for x in range(0, len(self.calls)): + for (x, (call, history)) in enumerate( + zip(self.calls, self.adapter.request_history)): self.assertEqual( - self.calls[x]['method'], - self.adapter.request_history[x].method) + call['method'], history.method, + 'Method mismatch on call {index}'.format(index=x)) self.assertEqual( - self.calls[x]['url'], - self.adapter.request_history[x].url) - if 'json' in self.calls[x]: + call['url'], history.url, + 'URL mismatch on call {index}'.format(index=x)) + if 'json' in call: self.assertEqual( - self.calls[x]['json'], - self.adapter.request_history[x].json()) + call['json'], history.json(), + 'json content mismatch in call {index}'.format(index=x)) # headers in a call isn't exhaustive - it's checking to make sure # a specific header or headers are there, not that they are the # only headers - if 'headers' in self.calls[x]: - for key, value in self.calls[x]['headers'].items(): + if 'headers' in call: + for key, value in call['headers'].items(): self.assertEqual( - value, self.adapter.request_history[x].headers[key]) + value, history.headers[key], + 'header mismatch in call {index}'.format(index=x)) From 33548e54c0b77438446450d350d94164586d28d5 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 20 Dec 2016 09:24:57 -0600 Subject: [PATCH 1177/3836] Add test to verify devstack keystone config Similar to the services, we also configure keystone v2 for some test. Make sure that in the tests where we're doing that we actually end up with keystone v2. Change-Id: Ic8fdeae2ac82145e6a8c254c6953419e827216d3 --- shade/tests/functional/base.py | 3 +++ shade/tests/functional/test_devstack.py | 10 ++++++++++ shade/tests/functional/test_identity.py | 2 -- shade/tests/functional/test_project.py | 2 -- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/shade/tests/functional/base.py b/shade/tests/functional/base.py index c2e83373a..ce05ddb4e 100644 --- a/shade/tests/functional/base.py +++ b/shade/tests/functional/base.py @@ -36,3 +36,6 @@ def setUp(self): self.operator_cloud = shade.OperatorCloud( cloud_config=operator_config, log_inner_exceptions=True) + + self.identity_version = \ + self.operator_cloud.cloud_config.get_api_version('identity') diff --git a/shade/tests/functional/test_devstack.py b/shade/tests/functional/test_devstack.py index 54fd10f19..45453cb8d 100644 --- a/shade/tests/functional/test_devstack.py +++ b/shade/tests/functional/test_devstack.py @@ -40,3 +40,13 @@ class TestDevstack(base.BaseFunctionalTestCase): def test_has_service(self): if os.environ.get('SHADE_HAS_{env}'.format(env=self.env), '0') == '1': self.assertTrue(self.demo_cloud.has_service(self.service)) + + +class TestKeystoneVersion(base.BaseFunctionalTestCase): + + def test_keystone_version(self): + use_keystone_v2 = os.environ.get('SHADE_USE_KEYSTONE_V2', False) + if use_keystone_v2 and use_keystone_v2 != '0': + self.assertEqual('2.0', self.identity_version) + else: + self.assertEqual('3', self.identity_version) diff --git a/shade/tests/functional/test_identity.py b/shade/tests/functional/test_identity.py index 6c95b788f..67660b1ef 100644 --- a/shade/tests/functional/test_identity.py +++ b/shade/tests/functional/test_identity.py @@ -35,8 +35,6 @@ def setUp(self): self.group_prefix = self.getUniqueString('group') self.addCleanup(self._cleanup_users) - self.identity_version = \ - self.operator_cloud.cloud_config.get_api_version('identity') if self.identity_version not in ('2', '2.0'): self.addCleanup(self._cleanup_groups) self.addCleanup(self._cleanup_roles) diff --git a/shade/tests/functional/test_project.py b/shade/tests/functional/test_project.py index 4e1e26d48..41b640ebc 100644 --- a/shade/tests/functional/test_project.py +++ b/shade/tests/functional/test_project.py @@ -30,8 +30,6 @@ class TestProject(base.BaseFunctionalTestCase): def setUp(self): super(TestProject, self).setUp() self.new_project_name = self.getUniqueString('project') - self.identity_version = \ - self.operator_cloud.cloud_config.get_api_version('identity') self.addCleanup(self._cleanup_projects) def _cleanup_projects(self): From fcead29c3faa0d7f33e05cd527cbb475542d32ab Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 22 Dec 2016 06:42:25 -0600 Subject: [PATCH 1178/3836] Update swift constructor to be Session aware python-swiftclient has Session support now! Use it, and remove a pile of customization specific for swift. We still need to put a few parameters in different places, but that's no worse than glance or trove. Change-Id: Ic51aee2bc7b535aa4b6e261fb3deb59bd921f563 --- os_client_config/cloud_config.py | 80 ++++------------- os_client_config/tests/test_cloud_config.py | 96 ++++----------------- 2 files changed, 34 insertions(+), 142 deletions(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index f61c49814..ecf60a6e1 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -312,9 +312,6 @@ def get_legacy_client( if not client_class: client_class = _get_client(service_key) - # Because of course swift is different - if service_key == 'object-store': - return self._get_swift_client(client_class=client_class, **kwargs) interface = self.get_interface(service_key) # trigger exception on lack of service endpoint = self.get_session_endpoint(service_key) @@ -326,12 +323,20 @@ def get_legacy_client( else: interface_key = 'endpoint_type' - constructor_kwargs = dict( - session=self.get_session(), - service_name=self.get_service_name(service_key), - service_type=self.get_service_type(service_key), - endpoint_override=endpoint_override, - region_name=self.region) + if service_key == 'object-store': + constructor_kwargs = dict( + session=self.get_session(), + os_options=dict( + service_type=self.get_service_type(service_key), + object_storage_url=endpoint_override, + region_name=self.region)) + else: + constructor_kwargs = dict( + session=self.get_session(), + service_name=self.get_service_name(service_key), + service_type=self.get_service_type(service_key), + endpoint_override=endpoint_override, + region_name=self.region) if service_key == 'image': # os-client-config does not depend on glanceclient, but if @@ -350,8 +355,11 @@ def get_legacy_client( if not endpoint_override: constructor_kwargs['endpoint_override'] = endpoint constructor_kwargs.update(kwargs) - constructor_kwargs[interface_key] = interface - if pass_version_arg: + if service_key == 'object-store': + constructor_kwargs['os_options'][interface_key] = interface + else: + constructor_kwargs[interface_key] = interface + if pass_version_arg and service_key != 'object-store': if not version: version = self.get_api_version(service_key) # Temporary workaround while we wait for python-openstackclient @@ -377,56 +385,6 @@ def get_legacy_client( return client_class(**constructor_kwargs) - def _get_swift_client(self, client_class, **kwargs): - auth_args = self.get_auth_args() - auth_version = self.get_api_version('identity') - session = self.get_session() - token = session.get_token() - endpoint = self.get_session_endpoint(service_key='object-store') - if not endpoint: - return None - # If we have a username/password, we want to pass them to - # swift - because otherwise it will not re-up tokens appropriately - # However, if we only have non-password auth, then get a token - # and pass it in - swift_kwargs = dict( - auth_version=auth_version, - preauthurl=endpoint, - preauthtoken=token, - os_options=dict( - region_name=self.get_region_name(), - auth_token=token, - object_storage_url=endpoint, - service_type=self.get_service_type('object-store'), - endpoint_type=self.get_interface('object-store'), - - )) - if self.config['api_timeout'] is not None: - swift_kwargs['timeout'] = float(self.config['api_timeout']) - - # create with password - swift_kwargs['user'] = auth_args.get('username') - swift_kwargs['key'] = auth_args.get('password') - swift_kwargs['authurl'] = auth_args.get('auth_url') - os_options = {} - if auth_version == '2.0': - os_options['tenant_name'] = auth_args.get('project_name') - os_options['tenant_id'] = auth_args.get('project_id') - else: - os_options['project_name'] = auth_args.get('project_name') - os_options['project_id'] = auth_args.get('project_id') - - for key in ( - 'user_id', - 'project_domain_id', - 'project_domain_name', - 'user_domain_id', - 'user_domain_name'): - os_options[key] = auth_args.get(key) - swift_kwargs['os_options'].update(os_options) - - return client_class(**swift_kwargs) - def get_cache_expiration_time(self): if self._openstack_config: return self._openstack_config.get_cache_expiration_time() diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index dcbeb3a55..f97f85b67 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -272,26 +272,13 @@ def test_legacy_client_object_store_password( "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('object-store', mock_client) mock_client.assert_called_with( - preauthtoken=mock.ANY, - auth_version=u'3', - authurl='http://example.com', - key='testpassword', + session=mock.ANY, os_options={ - 'auth_token': mock.ANY, 'region_name': 'region-al', - 'object_storage_url': 'http://swift.example.com', - 'user_id': None, - 'user_domain_name': None, - 'project_name': 'testproject', - 'project_domain_name': None, - 'project_domain_id': None, - 'project_id': None, 'service_type': 'object-store', + 'object_storage_url': None, 'endpoint_type': 'public', - 'user_domain_id': None - }, - preauthurl='http://swift.example.com', - user='testuser') + }) @mock.patch.object(cloud_config.CloudConfig, 'get_auth_args') @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') @@ -311,26 +298,13 @@ def test_legacy_client_object_store_password_v2( "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('object-store', mock_client) mock_client.assert_called_with( - preauthtoken=mock.ANY, - auth_version=u'2.0', - authurl='http://example.com', - key='testpassword', + session=mock.ANY, os_options={ - 'auth_token': mock.ANY, 'region_name': 'region-al', - 'object_storage_url': 'http://swift.example.com', - 'user_id': None, - 'user_domain_name': None, - 'tenant_name': 'testproject', - 'project_domain_name': None, - 'project_domain_id': None, - 'tenant_id': None, 'service_type': 'object-store', + 'object_storage_url': None, 'endpoint_type': 'public', - 'user_domain_id': None - }, - preauthurl='http://swift.example.com', - user='testuser') + }) @mock.patch.object(cloud_config.CloudConfig, 'get_auth_args') @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') @@ -345,26 +319,13 @@ def test_legacy_client_object_store( "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('object-store', mock_client) mock_client.assert_called_with( - preauthtoken=mock.ANY, - auth_version=u'2.0', - authurl=None, - key=None, + session=mock.ANY, os_options={ - 'auth_token': mock.ANY, 'region_name': 'region-al', - 'object_storage_url': 'http://example.com/v2', - 'user_id': None, - 'user_domain_name': None, - 'tenant_name': None, - 'project_domain_name': None, - 'project_domain_id': None, - 'tenant_id': None, 'service_type': 'object-store', + 'object_storage_url': None, 'endpoint_type': 'public', - 'user_domain_id': None - }, - preauthurl='http://example.com/v2', - user=None) + }) @mock.patch.object(cloud_config.CloudConfig, 'get_auth_args') @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') @@ -380,27 +341,13 @@ def test_legacy_client_object_store_timeout( "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('object-store', mock_client) mock_client.assert_called_with( - preauthtoken=mock.ANY, - auth_version=u'2.0', - authurl=None, - key=None, + session=mock.ANY, os_options={ - 'auth_token': mock.ANY, 'region_name': 'region-al', - 'object_storage_url': 'http://example.com/v2', - 'user_id': None, - 'user_domain_name': None, - 'tenant_name': None, - 'project_domain_name': None, - 'project_domain_id': None, - 'tenant_id': None, 'service_type': 'object-store', + 'object_storage_url': None, 'endpoint_type': 'public', - 'user_domain_id': None - }, - preauthurl='http://example.com/v2', - timeout=9.0, - user=None) + }) @mock.patch.object(cloud_config.CloudConfig, 'get_auth_args') def test_legacy_client_object_store_endpoint( @@ -414,26 +361,13 @@ def test_legacy_client_object_store_endpoint( "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('object-store', mock_client) mock_client.assert_called_with( - preauthtoken=mock.ANY, - auth_version=u'2.0', - authurl=None, - key=None, + session=mock.ANY, os_options={ - 'auth_token': mock.ANY, 'region_name': 'region-al', - 'object_storage_url': 'http://example.com/swift', - 'user_id': None, - 'user_domain_name': None, - 'tenant_name': None, - 'project_domain_name': None, - 'project_domain_id': None, - 'tenant_id': None, 'service_type': 'object-store', + 'object_storage_url': 'http://example.com/swift', 'endpoint_type': 'public', - 'user_domain_id': None - }, - preauthurl='http://example.com/swift', - user=None) + }) @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') def test_legacy_client_image(self, mock_get_session_endpoint): From 18916dea5712c1b24737b698d7182dced222a85f Mon Sep 17 00:00:00 2001 From: Andrey Shestakov Date: Thu, 22 Dec 2016 16:31:49 +0200 Subject: [PATCH 1179/3836] Add failure check to node_set_provision_state When 'wait' is enabled in node_set_provision_state, exception should be raised when machine failure occurred. Change-Id: I5cc96127b4f44fdadbe75f5df5264565d4e646c9 --- shade/operatorcloud.py | 3 ++ shade/tests/unit/test_shade_operator.py | 58 +++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 11d1dfb9b..5f8f4e0f0 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -574,6 +574,9 @@ def node_set_provision_state(self, "Timeout waiting for node transition to " "target state of '%s'" % state): machine = self.get_machine(name_or_id) + if 'failed' in machine['provision_state']: + raise OpenStackCloudException( + "Machine encountered a failure.") # NOTE(TheJulia): This performs matching if the requested # end state matches the state the node has reached. if state in machine['provision_state']: diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index abae45f8e..de97e6347 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -900,6 +900,64 @@ class available_node_state: state='active', configdrive='http://127.0.0.1/file.iso') + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + def test_node_set_provision_state_wait_failure(self, mock_client): + class active_node_state: + provision_state = "active" + + class deploy_failed_node_state: + provision_state = "deploy failed" + + class clean_failed_node_state: + provision_state = "clean failed" + + active_return_value = dict( + provision_state="active") + mock_client.node.get.return_value = active_node_state + mock_client.node.set_provision_state.return_value = None + node_id = 'node01' + return_value = self.op_cloud.node_set_provision_state( + node_id, + 'active', + configdrive='http://127.0.0.1/file.iso', + wait=True) + + self.assertEqual(active_return_value, return_value) + mock_client.node.set_provision_state.assert_called_with( + node_uuid='node01', + state='active', + configdrive='http://127.0.0.1/file.iso') + self.assertTrue(mock_client.node.get.called) + mock_client.mock_reset() + mock_client.node.get.return_value = deploy_failed_node_state + self.assertRaises( + shade.OpenStackCloudException, + self.op_cloud.node_set_provision_state, + node_id, + 'active', + configdrive='http://127.0.0.1/file.iso', + wait=True, + timeout=300) + self.assertTrue(mock_client.node.get.called) + mock_client.node.set_provision_state.assert_called_with( + node_uuid='node01', + state='active', + configdrive='http://127.0.0.1/file.iso') + mock_client.mock_reset() + mock_client.node.get.return_value = clean_failed_node_state + self.assertRaises( + shade.OpenStackCloudException, + self.op_cloud.node_set_provision_state, + node_id, + 'deleted', + wait=True, + timeout=300) + self.assertTrue(mock_client.node.get.called) + mock_client.node.set_provision_state.assert_called_with( + node_uuid='node01', + state='deleted', + configdrive=None) + @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_node_set_provision_state_wait_provide(self, mock_client): From 197ca1b8c700223c253ada0e6d04660ce18858fa Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 22 Dec 2016 13:44:42 -0600 Subject: [PATCH 1180/3836] Fix exception name typo Change-Id: I2ecfe8a3d614d734bee03ae9412044704887c1d0 --- shade/openstackcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 6088078cf..ce0bdde04 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1642,7 +1642,7 @@ def list_flavors(self, get_extra=True): id=flavor.id) try: flavor.extra_specs = self._compute_client.get(endpoint) - except OpenStackCloudHttpError as e: + except OpenStackCloudHTTPError as e: flavor.extra_specs = [] self.log.debug( 'Fetching extra specs for flavor failed:' From 228b50dea0748d3a079fd4a1c24b38699a146e34 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Wed, 28 Dec 2016 16:13:26 +0100 Subject: [PATCH 1181/3836] Remove link to modindex The documentation build does not generate any module index, thus remove the link to the page. The page http://docs.openstack.org/infra/shade/py-modindex.html does not exist. Change-Id: Ibfc9f74bbebe3ad2c0d3d37eba3e65a44a602dfe --- doc/source/index.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/source/index.rst b/doc/source/index.rst index ad8969530..c298c6ac2 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -25,5 +25,4 @@ Indices and tables ================== * :ref:`genindex` -* :ref:`modindex` * :ref:`search` From 94af4ac99af149cb9c02e78feb60b10a7ed5a1f1 Mon Sep 17 00:00:00 2001 From: Tony Breeds Date: Tue, 3 Jan 2017 14:02:45 +1100 Subject: [PATCH 1182/3836] Remove discover from test-requirements It's only needed for python < 2.7 which is not supported Change-Id: I1225bc24f61ed5f0f2537099d6f79ec94070dc5a --- test-requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 75e7af76d..7557c662f 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,7 +4,6 @@ hacking<0.11,>=0.10.0 coverage>=4.0 # Apache-2.0 -discover # BSD fixtures>=3.0.0 # Apache-2.0/BSD mock>=2.0 # BSD python-subunit>=0.0.18 # Apache-2.0/BSD From d0e967274b3b88a2a3ac21385e8d303f3dbb6199 Mon Sep 17 00:00:00 2001 From: jolie Date: Tue, 3 Jan 2017 11:43:49 +0800 Subject: [PATCH 1183/3836] Add params to ClusterDelNodes action Change-Id: I8b76cbebf1c973ea39d34f11b1b8c2f19fa2416f --- openstack/cluster/v1/_proxy.py | 8 +++++-- openstack/cluster/v1/cluster.py | 8 +++---- .../tests/unit/cluster/v1/test_cluster.py | 21 +++++++++++++++++++ openstack/tests/unit/cluster/v1/test_proxy.py | 4 +++- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/openstack/cluster/v1/_proxy.py b/openstack/cluster/v1/_proxy.py index f5526c69a..2a70848f9 100644 --- a/openstack/cluster/v1/_proxy.py +++ b/openstack/cluster/v1/_proxy.py @@ -289,19 +289,23 @@ def cluster_add_nodes(self, cluster, nodes): obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) return obj.add_nodes(self.session, nodes) - def cluster_del_nodes(self, cluster, nodes): + def cluster_del_nodes(self, cluster, nodes, **params): """Remove nodes from a cluster. :param cluster: Either the name or the ID of the cluster, or an instance of :class:`~openstack.cluster.v1.cluster.Cluster`. :param nodes: List of nodes to be removed from the cluster. + :param kwargs \*\*params: Optional query parameters to be sent to + restrict the nodes to be returned. Available parameters include: + * destroy_after_deletion: A boolean value indicating whether the + deleted nodes to be destroyed right away. :returns: A dict containing the action initiated by this operation. """ if isinstance(cluster, _cluster.Cluster): obj = cluster else: obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) - return obj.del_nodes(self.session, nodes) + return obj.del_nodes(self.session, nodes, **params) def cluster_replace_nodes(self, cluster, nodes): """Replace the nodes in a cluster with specified nodes. diff --git a/openstack/cluster/v1/cluster.py b/openstack/cluster/v1/cluster.py index a2dd62a65..ceadb7a13 100644 --- a/openstack/cluster/v1/cluster.py +++ b/openstack/cluster/v1/cluster.py @@ -90,11 +90,11 @@ def add_nodes(self, session, nodes): } return self.action(session, body) - def del_nodes(self, session, nodes): + def del_nodes(self, session, nodes, **params): + data = {'nodes': nodes} + data.update(params) body = { - 'del_nodes': { - 'nodes': nodes, - } + 'del_nodes': data } return self.action(session, body) diff --git a/openstack/tests/unit/cluster/v1/test_cluster.py b/openstack/tests/unit/cluster/v1/test_cluster.py index 2eb616c42..e65bb232c 100644 --- a/openstack/tests/unit/cluster/v1/test_cluster.py +++ b/openstack/tests/unit/cluster/v1/test_cluster.py @@ -165,6 +165,27 @@ def test_del_nodes(self): sess.post.assert_called_once_with(url, endpoint_filter=sot.service, json=body) + def test_del_nodes_with_params(self): + sot = cluster.Cluster(**FAKE) + + resp = mock.Mock() + resp.json = mock.Mock(return_value='') + sess = mock.Mock() + sess.post = mock.Mock(return_value=resp) + params = { + 'destroy_after_deletion': True, + } + self.assertEqual('', sot.del_nodes(sess, ['node-11'], **params)) + url = 'clusters/%s/actions' % sot.id + body = { + 'del_nodes': { + 'nodes': ['node-11'], + 'destroy_after_deletion': True, + } + } + sess.post.assert_called_once_with(url, endpoint_filter=sot.service, + json=body) + def test_replace_nodes(self): sot = cluster.Cluster(**FAKE) diff --git a/openstack/tests/unit/cluster/v1/test_proxy.py b/openstack/tests/unit/cluster/v1/test_proxy.py index b6643bace..f45c498df 100644 --- a/openstack/tests/unit/cluster/v1/test_proxy.py +++ b/openstack/tests/unit/cluster/v1/test_proxy.py @@ -141,7 +141,9 @@ def test_cluster_del_nodes_with_obj(self): self._verify("openstack.cluster.v1.cluster.Cluster.del_nodes", self.proxy.cluster_del_nodes, method_args=[mock_cluster, ["node1"]], - expected_args=[["node1"]]) + method_kwargs={"key": "value"}, + expected_args=[["node1"]], + expected_kwargs={"key": "value"}) @mock.patch.object(proxy_base.BaseProxy, '_find') def test_cluster_replace_nodes(self, mock_find): From d38571499cd4c9774fc2837a5065de7f812141da Mon Sep 17 00:00:00 2001 From: miaohb Date: Wed, 4 Jan 2017 18:00:47 +0800 Subject: [PATCH 1184/3836] Remove unnecessary coding format in the head of files The line of "coding utf8" is added by some editors automatically. It's useless, can be removed. Change-Id: I066a91972750bc6ef79858dc434b7e7be9dcf52f --- doc/source/conf.py | 1 - openstack/tests/unit/base.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 05443eb38..7086ccf0d 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,4 +1,3 @@ -# -*- 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 diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 0b5b1f446..eae41c16d 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Copyright 2010-2011 OpenStack Foundation # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # From 9e818c0afdb280a374cb2418cf4a277c3867e218 Mon Sep 17 00:00:00 2001 From: Paulo Matias Date: Tue, 3 Jan 2017 16:30:39 -0200 Subject: [PATCH 1185/3836] Add support for using the default subnetpool Allows to pass use_default_subnetpool instead of a cidr. This is required when adding an IPv6 subnet if BGP or prefix delegation is enabled. Change-Id: Icf7c212ee730d631599fba732f66679c5cd2916d --- shade/openstackcloud.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index ce0bdde04..209bc1286 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5791,12 +5791,13 @@ def get_object(self, container, obj, query_string=None, return None raise - def create_subnet(self, network_name_or_id, cidr, ip_version=4, + def create_subnet(self, network_name_or_id, cidr=None, ip_version=4, enable_dhcp=False, subnet_name=None, tenant_id=None, allocation_pools=None, gateway_ip=None, disable_gateway_ip=False, dns_nameservers=None, host_routes=None, - ipv6_ra_mode=None, ipv6_address_mode=None): + ipv6_ra_mode=None, ipv6_address_mode=None, + use_default_subnetpool=False): """Create a subnet on a specified network. :param string network_name_or_id: @@ -5858,6 +5859,10 @@ def create_subnet(self, network_name_or_id, cidr, ip_version=4, :param string ipv6_address_mode: IPv6 address mode. Valid values are: 'dhcpv6-stateful', 'dhcpv6-stateless', or 'slaac'. + :param bool use_default_subnetpool: + Use the default subnetpool for ``ip_version`` to obtain a CIDR. It + is required to pass ``None`` to the ``cidr`` argument when enabling + this option. :returns: The new subnet object. :raises: OpenStackCloudException on operation error. @@ -5872,6 +5877,15 @@ def create_subnet(self, network_name_or_id, cidr, ip_version=4, raise OpenStackCloudException( 'arg:disable_gateway_ip is not allowed with arg:gateway_ip') + if not cidr and not use_default_subnetpool: + raise OpenStackCloudException( + 'arg:cidr is required when a subnetpool is not used') + + if cidr and use_default_subnetpool: + raise OpenStackCloudException( + 'arg:cidr must be set to None when use_default_subnetpool == ' + 'True') + # Be friendly on ip_version and allow strings if isinstance(ip_version, six.string_types): try: @@ -5883,12 +5897,13 @@ def create_subnet(self, network_name_or_id, cidr, ip_version=4, # This includes attributes that are required or have defaults. subnet = { 'network_id': network['id'], - 'cidr': cidr, 'ip_version': ip_version, 'enable_dhcp': enable_dhcp } # Add optional attributes to the message. + if cidr: + subnet['cidr'] = cidr if subnet_name: subnet['name'] = subnet_name if tenant_id: @@ -5907,6 +5922,8 @@ def create_subnet(self, network_name_or_id, cidr, ip_version=4, subnet['ipv6_ra_mode'] = ipv6_ra_mode if ipv6_address_mode: subnet['ipv6_address_mode'] = ipv6_address_mode + if use_default_subnetpool: + subnet['use_default_subnetpool'] = True with _utils.neutron_exceptions( "Error creating subnet on network " From 68a8d513dd0cd6c2f24642f2f02e4c61f3a863a7 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 6 Jan 2017 09:51:28 -0600 Subject: [PATCH 1186/3836] Handle pagination for glance images The default glance image list pagination seems to be about 20, which means for v2 you really need to deal with pagination every time. It also seems that the limit parameter does _not_ allow you to get more items than the server default, so you can't just say "limit 100000" and be done with it. In order to accomplish this, we need to have the adapter stop trying to return only the image list when there are other top level keys (so the code can read the next link) and then do a loop requesting the next link. To make us even happier, glance returns the next link as '/v2/images' but we have already set the adapter to 'https://example.com/v2' due to version discovery. Since we're setting the endpoint_override on the adapater, it treats that as the root, leaving us with https://example.com/v2/v2/images. To deal with that, introduce a 'raw' adapter which is bound to whatever is in the catalog, rather than whatever we found through version discovery. Change-Id: I030147e0275d0c4ee89588e21b5970f7d81800d3 Story: 2000837 --- ...nce-image-pagination-0b4dfef22b25852b.yaml | 4 ++++ shade/_adapter.py | 16 ++++---------- shade/openstackcloud.py | 22 +++++++++++++++++-- shade/tests/unit/test_image.py | 18 +++++++++++++++ 4 files changed, 46 insertions(+), 14 deletions(-) create mode 100644 releasenotes/notes/glance-image-pagination-0b4dfef22b25852b.yaml diff --git a/releasenotes/notes/glance-image-pagination-0b4dfef22b25852b.yaml b/releasenotes/notes/glance-image-pagination-0b4dfef22b25852b.yaml new file mode 100644 index 000000000..3b134fcb5 --- /dev/null +++ b/releasenotes/notes/glance-image-pagination-0b4dfef22b25852b.yaml @@ -0,0 +1,4 @@ +--- +issues: + - Fixed an issue where glance image list pagination was being ignored, + leading to truncated image lists. diff --git a/shade/_adapter.py b/shade/_adapter.py index d15214a00..ac184e0d4 100644 --- a/shade/_adapter.py +++ b/shade/_adapter.py @@ -121,18 +121,10 @@ def _munch_response(self, response, result_key=None): elif len(json_keys) == 1: result = result_json[json_keys[0]] else: - # Yay for inferrence! - path = urllib.parse.urlparse(response.url).path.strip() - object_type = path.split('/')[-1] - if object_type in json_keys: - result = result_json[object_type] - elif (object_type.startswith('os-') - and object_type[3:] in json_keys): - result = result_json[object_type[3:]] - else: - # Passthrough the whole body - sometimes (hi glance) things - # come through without a top-level container - result = result_json + # Passthrough the whole body - sometimes (hi glance) things + # come through without a top-level container. Also, sometimes + # you need to deal with pagination + result = result_json if task_manager._is_listlike(result): return meta.obj_list_to_dict(result, request_id=request_id) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index ce0bdde04..d8d2b7e4f 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -387,6 +387,13 @@ def _object_store_client(self): self._raw_clients['object-store'] = raw_client return self._raw_clients['object-store'] + @property + def _raw_image_client(self): + if 'raw-image' not in self._raw_clients: + image_client = self._get_raw_client('image') + self._raw_clients['raw-image'] = image_client + return self._raw_clients['raw-image'] + @property def _image_client(self): if 'image' not in self._raw_clients: @@ -1773,18 +1780,29 @@ def list_images(self, filter_deleted=True): """ # First, try to actually get images from glance, it's more efficient images = [] + image_list = [] try: if self.cloud_config.get_api_version('image') == '2': endpoint = '/images' else: endpoint = '/images/detail' - image_list = self._image_client.get(endpoint) + response = self._image_client.get(endpoint) except keystoneauth1.exceptions.catalog.EndpointNotFound: # We didn't have glance, let's try nova # If this doesn't work - we just let the exception propagate - image_list = self._compute_client.get('/images/detail') + response = self._compute_client.get('/images/detail') + while 'next' in response: + image_list.extend(meta.obj_list_to_dict(response['images'])) + endpoint = response['next'] + # Use the raw endpoint from the catalog not the one from + # version discovery so that the next links will work right + response = self._raw_image_client.get(endpoint) + if 'images' in response: + image_list.extend(meta.obj_list_to_dict(response['images'])) + else: + image_list.extend(response) for image in image_list: # The cloud might return DELETED for invalid images. diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index 309cd3ef4..0b6ea254a 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -147,6 +147,24 @@ def test_list_images(self): self.cloud._normalize_images([self.fake_image_dict]), self.cloud.list_images()) + def test_list_images_paginated(self): + marker = str(uuid.uuid4()) + self.adapter.register_uri( + 'GET', 'https://image.example.com/v2/images', + json={ + 'images': [self.fake_image_dict], + 'next': '/v2/images?marker={marker}'.format(marker=marker), + }) + self.adapter.register_uri( + 'GET', + 'https://image.example.com/v2/images?marker={marker}'.format( + marker=marker), + json=self.fake_search_return) + self.assertEqual( + self.cloud._normalize_images([ + self.fake_image_dict, self.fake_image_dict]), + self.cloud.list_images()) + def test_create_image_put_v2(self): self.cloud.image_api_use_tasks = False From 11d66e34e497f965348ca266ee3007afb122b1e5 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 5 Jan 2017 10:33:12 -0600 Subject: [PATCH 1187/3836] Rework limits normalization There isn't good docs or a guide on what we're looking for with normalization - so rather than nitpick a review to death, I just made a patch. We should make a doc ... Changes included: - We need to be explicit about every key we're going to support. Some of the limits keys are deprecated, so just don't return them. - Change them all from camel case to underscore to match other resources. - Document them in model.rst - Change the name from limits to compute_limits, since it's only limits for the compute service. - Allow usage by a normal user (it's not an admin function) There will be a follow up patch to convert this to direct rest calls, which will be fun due to the ability of this to work against other projects. Change-Id: Icfb118d4289263c0dd906f600e370242f191f708 --- doc/source/model.rst | 24 +++++++++++++++++++++ shade/_normalize.py | 31 ++++++++++++++++++++++++--- shade/openstackcloud.py | 28 ++++++++++++++++++++++++ shade/operatorcloud.py | 19 ---------------- shade/tests/functional/test_limits.py | 17 +++++++++++---- shade/tests/unit/test_limits.py | 10 +++++++-- 6 files changed, 101 insertions(+), 28 deletions(-) diff --git a/doc/source/model.rst b/doc/source/model.rst index 508d4a54f..cab7a70d8 100644 --- a/doc/source/model.rst +++ b/doc/source/model.rst @@ -191,6 +191,30 @@ A Server from Nova task_state=str() or None, properties=dict()) +ComputeLimits +------------- + +Limits and current usage for a project in Nova + +.. code-block:: python + + ComputeLimits = dict( + location=Location(), + max_personality=int(), + max_personality_size=int(), + max_server_group_members=int(), + max_server_groups=int(), + max_server_meta=int(), + max_total_cores=int(), + max_total_instances=int(), + max_total_keypairs=int(), + max_total_ram_size=int(), + total_cores_used=int(), + total_instances_used=int(), + total_ram_used=int(), + total_server_groups_used=int(), + properties=dict()) + Floating IP ----------- diff --git a/shade/_normalize.py b/shade/_normalize.py index 57508c709..4d5a76354 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -45,6 +45,22 @@ 'user_id', ) +_COMPUTE_LIMITS_FIELDS = ( + ('maxPersonality', 'max_personality'), + ('maxPersonalitySize', 'max_personality_size'), + ('maxServerGroupMembers', 'max_server_group_members'), + ('maxServerGroups', 'max_server_groups'), + ('maxServerMeta', 'max_server_meta'), + ('maxTotalCores', 'max_total_cores'), + ('maxTotalInstances', 'max_total_instances'), + ('maxTotalKeypairs', 'max_total_keypairs'), + ('maxTotalRAMSize', 'max_total_ram_size'), + ('totalCoresUsed', 'total_cores_used'), + ('totalInstancesUsed', 'total_instances_used'), + ('totalRAMUsed', 'total_ram_used'), + ('totalServerGroupsUsed', 'total_server_groups_used'), +) + _pushdown_fields = { 'project': [ @@ -106,14 +122,23 @@ class Normalizer(object): reasons. ''' - def _normalize_limits(self, limits): + def _normalize_compute_limits(self, limits, project_id=None): """ Normalize a limits object. Limits modified in this method and shouldn't be modified afterwards. """ - new_limits = munch.Munch(limits['absolute']) - new_limits['rate'] = limits.pop('rate') + # Copy incoming limits because of shared dicts in unittests + limits = limits['absolute'].copy() + + new_limits = munch.Munch() + new_limits['location'] = self._get_current_location( + project_id=project_id) + + for field in _COMPUTE_LIMITS_FIELDS: + new_limits[field[1]] = limits.pop(field[0], None) + + new_limits['properties'] = limits.copy() return new_limits diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 8331ed52a..fc5d64cd6 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1709,6 +1709,34 @@ def list_server_groups(self): with _utils.shade_exceptions("Error fetching server group list"): return self.manager.submit_task(_tasks.ServerGroupList()) + def get_compute_limits(self, name_or_id=None): + """ Get compute limits for a project + + :param name_or_id: (optional) project name or id to get limits for + if different from the current project + :raises: OpenStackCloudException if it's not a valid project + + :returns: Munch object with the limits + """ + kwargs = {} + project_id = None + if name_or_id: + + proj = self.get_project(name_or_id) + if not proj: + raise OpenStackCloudException("project does not exist") + project_id = proj.id + kwargs['tenant_id'] = project_id + + with _utils.shade_exceptions( + "Failed to get limits for the project: {} ".format( + name_or_id)): + # TODO(mordred) Before we convert this to REST, we need to add + # in support for running calls with a different project context + limits = self.manager.submit_task(_tasks.NovaLimitsGet(**kwargs)) + + return self._normalize_compute_limits(limits, project_id=project_id) + @_utils.cache_on_arguments(should_cache_fn=_no_pending_images) def list_images(self, filter_deleted=True): """Get available glance images. diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index b9702acd8..11d1dfb9b 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -2138,22 +2138,3 @@ def list_magnum_services(self): with _utils.shade_exceptions("Error fetching Magnum services list"): return self.manager.submit_task( _tasks.MagnumServicesList()) - - def get_limits(self, name_or_id): - """ Get limits for a project - - :param name_or_id: project name or id - :raises: OpenStackCloudException if it's not a valid project - - :returns: Munch object with the limits - """ - proj = self.get_project(name_or_id) - if not proj.id: - raise OpenStackCloudException("project does not exist") - - with _utils.shade_exceptions( - "Failed to get limits for the project: {} ".format(proj.id)): - limits = self.manager.submit_task( - _tasks.NovaLimitsGet(tenant_id=proj.id)) - - return self._normalize_limits(limits) diff --git a/shade/tests/functional/test_limits.py b/shade/tests/functional/test_limits.py index 339b07055..2777caab9 100644 --- a/shade/tests/functional/test_limits.py +++ b/shade/tests/functional/test_limits.py @@ -23,11 +23,20 @@ class TestUsage(base.BaseFunctionalTestCase): - def test_get_limits(self): + def test_get_our_limits(self): '''Test quotas functionality''' - limits = self.operator_cloud.get_limits('demo') + limits = self.demo_cloud.get_compute_limits() self.assertIsNotNone(limits) - self.assertTrue(hasattr(limits, 'rate')) + self.assertTrue(hasattr(limits, 'max_server_meta')) # Test normalize limits - self.assertFalse(hasattr(limits, 'HUMAN_ID')) + self.assertFalse(hasattr(limits, 'maxImageMeta')) + + def test_get_other_limits(self): + '''Test quotas functionality''' + limits = self.operator_cloud.get_compute_limits('demo') + self.assertIsNotNone(limits) + self.assertTrue(hasattr(limits, 'max_server_meta')) + + # Test normalize limits + self.assertFalse(hasattr(limits, 'maxImageMeta')) diff --git a/shade/tests/unit/test_limits.py b/shade/tests/unit/test_limits.py index 9f4b8ecc0..6f5d4e4da 100644 --- a/shade/tests/unit/test_limits.py +++ b/shade/tests/unit/test_limits.py @@ -20,11 +20,17 @@ class TestLimits(base.TestCase): + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_get_compute_limits(self, mock_nova): + self.cloud.get_compute_limits() + + mock_nova.limits.get.assert_called_once_with() + @mock.patch.object(shade.OpenStackCloud, 'nova_client') @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_get_limits(self, mock_keystone, mock_nova): + def test_other_get_compute_limits(self, mock_keystone, mock_nova): project = fakes.FakeProject('project_a') mock_keystone.tenants.list.return_value = [project] - self.op_cloud.get_limits(project) + self.op_cloud.get_compute_limits(project) mock_nova.limits.get.assert_called_once_with(tenant_id='project_a') From 362c6febe24f7f95d47c7cdab964d0f249e75fd5 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 10 Jan 2017 09:58:34 -0500 Subject: [PATCH 1188/3836] Honor image_endpoint_override for image discovery If an endpoint_override is given, we don't need to do discovery. Clearly the user is telling us what they want. We should do that. Change-Id: I68a534b15d20db5909179ad26c54b47c9eae8d47 --- shade/openstackcloud.py | 115 +++++++++++++++++---------------- shade/tests/unit/test_image.py | 31 ++++++++- 2 files changed, 90 insertions(+), 56 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index d8d2b7e4f..6af3e3e31 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -394,66 +394,73 @@ def _raw_image_client(self): self._raw_clients['raw-image'] = image_client return self._raw_clients['raw-image'] + def _discover_image_endpoint(self, config_version, image_client): + try: + # Version discovery + versions = image_client.get('/') + api_version = None + if config_version.startswith('1'): + api_version = [ + version for version in versions + if version['id'] in ('v1.0', 'v1.1')] + if api_version: + api_version = api_version[0] + if not api_version: + api_version = [ + version for version in versions + if version['status'] == 'CURRENT'][0] + + image_url = api_version['links'][0]['href'] + # If we detect a different version that was configured, + # set the version in occ because we have logic elsewhere + # that is different depending on which version we're using + warning_msg = None + if (config_version.startswith('2') + and api_version['id'].startswith('v1')): + self.cloud_config.config['image_api_version'] = '1' + warning_msg = ( + 'image_api_version is 2 but only 1 is available.') + elif (config_version.startswith('1') + and api_version['id'].startswith('v2')): + self.cloud_config.config['image_api_version'] = '2' + warning_msg = ( + 'image_api_version is 1 but only 2 is available.') + if warning_msg: + self.log.debug(warning_msg) + warnings.warn(warning_msg) + except (keystoneauth1.exceptions.connection.ConnectFailure, + OpenStackCloudURINotFound) as e: + # A 404 or a connection error is a likely thing to get + # either with a misconfgured glance. or we've already + # gotten a versioned endpoint from the catalog + self.log.debug( + "Glance version discovery failed, assuming endpoint in" + " the catalog is already versioned. {e}".format(e=str(e))) + image_url = image_client.get_endpoint() + + service_url = image_client.get_endpoint() + parsed_image_url = urllib.parse.urlparse(image_url) + parsed_service_url = urllib.parse.urlparse(service_url) + + image_url = urllib.parse.ParseResult( + parsed_service_url.scheme, + parsed_image_url.netloc, + parsed_image_url.path, + parsed_image_url.params, + parsed_image_url.query, + parsed_image_url.fragment).geturl() + return image_url + @property def _image_client(self): if 'image' not in self._raw_clients: # Get configured api version for downgrades config_version = self.cloud_config.get_api_version('image') image_client = self._get_raw_client('image') - try: - # Version discovery - versions = image_client.get('/') - api_version = None - if config_version.startswith('1'): - api_version = [ - version for version in versions - if version['id'] in ('v1.0', 'v1.1')] - if api_version: - api_version = api_version[0] - if not api_version: - api_version = [ - version for version in versions - if version['status'] == 'CURRENT'][0] - - image_url = api_version['links'][0]['href'] - # If we detect a different version that was configured, - # set the version in occ because we have logic elsewhere - # that is different depending on which version we're using - warning_msg = None - if (config_version.startswith('2') - and api_version['id'].startswith('v1')): - self.cloud_config.config['image_api_version'] = '1' - warning_msg = ( - 'image_api_version is 2 but only 1 is available.') - elif (config_version.startswith('1') - and api_version['id'].startswith('v2')): - self.cloud_config.config['image_api_version'] = '2' - warning_msg = ( - 'image_api_version is 1 but only 2 is available.') - if warning_msg: - self.log.debug(warning_msg) - warnings.warn(warning_msg) - except (keystoneauth1.exceptions.connection.ConnectFailure, - OpenStackCloudURINotFound) as e: - # A 404 or a connection error is a likely thing to get - # either with a misconfgured glance. or we've already - # gotten a versioned endpoint from the catalog - self.log.debug( - "Glance version discovery failed, assuming endpoint in" - " the catalog is already versioned. {e}".format(e=str(e))) - image_url = image_client.get_endpoint() - - service_url = image_client.get_endpoint() - parsed_image_url = urllib.parse.urlparse(image_url) - parsed_service_url = urllib.parse.urlparse(service_url) - - image_url = urllib.parse.ParseResult( - parsed_service_url.scheme, - parsed_image_url.netloc, - parsed_image_url.path, - parsed_image_url.params, - parsed_image_url.query, - parsed_image_url.fragment).geturl() + image_url = self.cloud_config.config.get('image_endpoint_override') + if not image_url: + image_url = self._discover_image_endpoint( + config_version, image_client) image_client.endpoint_override = image_url self._raw_clients['image'] = image_client return self._raw_clients['image'] diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index 0b6ea254a..cd0ae847e 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -31,10 +31,10 @@ NO_SHA256 = '6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d' -class TestImage(base.RequestsMockTestCase): +class BaseTestImage(base.RequestsMockTestCase): def setUp(self): - super(TestImage, self).setUp() + super(BaseTestImage, self).setUp() self.image_id = str(uuid.uuid4()) self.imagefile = tempfile.NamedTemporaryFile(delete=False) self.imagefile.write(b'\0') @@ -68,6 +68,12 @@ def setUp(self): u'protected': False} self.fake_search_return = {'images': [self.fake_image_dict]} self.output = uuid.uuid4().bytes + + +class TestImage(BaseTestImage): + + def setUp(self): + super(TestImage, self).setUp() self.use_glance() def test_config_v1(self): @@ -733,3 +739,24 @@ def test_config_v2(self): self.cloud._image_client.get_endpoint()) self.assertEqual( '2', self.cloud_config.get_api_version('image')) + + +class TestImageVersionDiscovery(BaseTestImage): + + def test_version_discovery_skip(self): + self.cloud.cloud_config.config['image_endpoint_override'] = \ + 'https://image.example.com/v2/override' + + self.adapter.register_uri( + 'GET', 'https://image.example.com/v2/override/images', + json={'images': []}) + self.assertEqual([], self.cloud.list_images()) + self.assertEqual( + self.cloud._image_client.endpoint_override, + 'https://image.example.com/v2/override') + self.calls += [ + dict( + method='GET', + url='https://image.example.com/v2/override/images'), + ] + self.assert_calls() From 28e2b4c6943d759f67e31ebac7977ce465916a5d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 9 Jan 2017 09:42:00 -0600 Subject: [PATCH 1189/3836] Log request ids when debug logging is enabled If debug logging is enabled, it's fairly likely that's because a user wants to debug things. Often times having the request id in the log is a useful part of that. Also, while we're in there, don't try to log information about the object returned when there is no object to query. And log a useful message on lists, and when we get objects from nova. Change-Id: I02579227f3475a952006689182f6ca112fa1f7ed --- shade/__init__.py | 4 ++-- shade/meta.py | 23 ++++++++++++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index fc7fcc22e..89133cb72 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -44,9 +44,9 @@ def simple_logging(debug=False, http_debug=False): log = _log.setup_logging('keystoneauth') log.addHandler(logging.StreamHandler()) log.setLevel(log_level) - # Simple case - we do not care about request id log + # Simple case - we only care about request id log during debug log = _log.setup_logging('shade.request_ids') - log.setLevel(logging.INFO) + log.setLevel(log_level) log = _log.setup_logging('shade') log.addHandler(logging.StreamHandler()) log.setLevel(log_level) diff --git a/shade/meta.py b/shade/meta.py index f81f808ed..b90f704be 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -429,12 +429,19 @@ def _log_request_id(obj, request_id): if request_ids: request_id = request_ids[0] if request_id: - log = _log.setup_logging('shade.request_ids') # Log the request id and object id in a specific logger. This way # someone can turn it on if they're interested in this kind of tracing. - log.debug("Retreived object %(id)s. Request ID %(request_id)s", - {'id': obj.get('id', obj.get('uuid')), - 'request_id': request_id}) + log = _log.setup_logging('shade.request_ids') + obj_id = None + if isinstance(obj, dict): + obj_id = obj.get('id', obj.get('uuid')) + if obj_id: + log.debug("Retrieved object %(id)s. Request ID %(request_id)s", + {'id': obj.get('id', obj.get('uuid')), + 'request_id': request_id}) + else: + log.debug("Retrieved a response. Request ID %(request_id)s", + {'request_id': request_id}) return obj @@ -487,8 +494,14 @@ def obj_list_to_dict(obj_list, request_id=None): the conversion to lists of dictonaries. """ new_list = [] + if not request_id: + request_id = getattr(obj_list, 'request_ids', [None])[0] + if request_id: + log = _log.setup_logging('shade.request_ids') + log.debug("Retrieved a list. Request ID %(request_id)s", + {'request_id': request_id}) for obj in obj_list: - new_list.append(obj_to_dict(obj, request_id=request_id)) + new_list.append(obj_to_dict(obj)) return new_list From 25405851b72c608b08444578c9f256fcf653c3a9 Mon Sep 17 00:00:00 2001 From: Iswarya_Vakati Date: Wed, 11 Jan 2017 16:41:53 +0530 Subject: [PATCH 1190/3836] Removes unnecessary utf-8 encoding This patches removes unnecessary utf-8 encoding Change-Id: Ida121eb29eb8ea1b5fb40c3eb6843c4f9cb1caff --- shade/tests/ansible/hooks/post_test_hook.sh | 1 - shade/tests/base.py | 2 -- shade/tests/fakes.py | 2 -- shade/tests/functional/base.py | 2 -- shade/tests/functional/hooks/post_test_hook.sh | 1 - shade/tests/functional/test_aggregate.py | 2 -- shade/tests/functional/test_cluster_templates.py | 2 -- shade/tests/functional/test_compute.py | 2 -- shade/tests/functional/test_identity.py | 2 -- shade/tests/functional/test_image.py | 2 -- shade/tests/functional/test_limits.py | 2 -- shade/tests/functional/test_magnum_services.py | 2 -- shade/tests/functional/test_network.py | 2 -- shade/tests/functional/test_object.py | 2 -- shade/tests/functional/test_quotas.py | 2 -- shade/tests/functional/test_recordset.py | 2 -- shade/tests/functional/test_router.py | 2 -- shade/tests/functional/test_security_groups.py | 2 -- shade/tests/functional/test_server_group.py | 2 -- shade/tests/functional/test_stack.py | 2 -- shade/tests/functional/test_users.py | 2 -- shade/tests/functional/test_volume.py | 2 -- shade/tests/functional/test_volume_backup.py | 2 -- shade/tests/functional/test_zone.py | 2 -- shade/tests/functional/util.py | 2 -- shade/tests/unit/base.py | 2 -- shade/tests/unit/test__adapter.py | 2 -- shade/tests/unit/test__utils.py | 2 -- shade/tests/unit/test_aggregate.py | 2 -- shade/tests/unit/test_caching.py | 2 -- shade/tests/unit/test_cluster_templates.py | 2 -- shade/tests/unit/test_create_server.py | 2 -- shade/tests/unit/test_create_volume_snapshot.py | 2 -- shade/tests/unit/test_delete_server.py | 2 -- shade/tests/unit/test_delete_volume_snapshot.py | 2 -- shade/tests/unit/test_domain_params.py | 2 -- shade/tests/unit/test_flavors.py | 2 -- shade/tests/unit/test_inventory.py | 2 -- shade/tests/unit/test_limits.py | 2 -- shade/tests/unit/test_magnum_services.py | 2 -- shade/tests/unit/test_normalize.py | 2 -- shade/tests/unit/test_object.py | 1 - shade/tests/unit/test_project.py | 2 -- shade/tests/unit/test_quotas.py | 2 -- shade/tests/unit/test_recordset.py | 2 -- shade/tests/unit/test_security_groups.py | 2 -- shade/tests/unit/test_server_console.py | 2 -- shade/tests/unit/test_server_delete_metadata.py | 2 -- shade/tests/unit/test_server_group.py | 2 -- shade/tests/unit/test_server_set_metadata.py | 2 -- shade/tests/unit/test_shade.py | 2 -- shade/tests/unit/test_shade_operator.py | 2 -- shade/tests/unit/test_stack.py | 2 -- shade/tests/unit/test_update_server.py | 2 -- shade/tests/unit/test_users.py | 2 -- shade/tests/unit/test_volume.py | 2 -- shade/tests/unit/test_zone.py | 2 -- 57 files changed, 111 deletions(-) diff --git a/shade/tests/ansible/hooks/post_test_hook.sh b/shade/tests/ansible/hooks/post_test_hook.sh index 986d76775..0c2bbcfd2 100755 --- a/shade/tests/ansible/hooks/post_test_hook.sh +++ b/shade/tests/ansible/hooks/post_test_hook.sh @@ -1,5 +1,4 @@ #!/bin/sh -# -*- 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 diff --git a/shade/tests/base.py b/shade/tests/base.py index f4b7af8df..7c8ed59ae 100644 --- a/shade/tests/base.py +++ b/shade/tests/base.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Copyright 2010-2011 OpenStack Foundation # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 853c5d4c4..e9c01998c 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/functional/base.py b/shade/tests/functional/base.py index ce05ddb4e..308cc3e07 100644 --- a/shade/tests/functional/base.py +++ b/shade/tests/functional/base.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/functional/hooks/post_test_hook.sh b/shade/tests/functional/hooks/post_test_hook.sh index 956dbdaeb..41bdd6f21 100755 --- a/shade/tests/functional/hooks/post_test_hook.sh +++ b/shade/tests/functional/hooks/post_test_hook.sh @@ -1,5 +1,4 @@ #!/bin/sh -# -*- 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 diff --git a/shade/tests/functional/test_aggregate.py b/shade/tests/functional/test_aggregate.py index 093016e43..db41afd5a 100644 --- a/shade/tests/functional/test_aggregate.py +++ b/shade/tests/functional/test_aggregate.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/functional/test_cluster_templates.py b/shade/tests/functional/test_cluster_templates.py index 4d39ded7b..bdbd2e43a 100644 --- a/shade/tests/functional/test_cluster_templates.py +++ b/shade/tests/functional/test_cluster_templates.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index f9b414069..adeff2f0f 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/functional/test_identity.py b/shade/tests/functional/test_identity.py index 67660b1ef..6cd3bf9fd 100644 --- a/shade/tests/functional/test_identity.py +++ b/shade/tests/functional/test_identity.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/functional/test_image.py b/shade/tests/functional/test_image.py index a7657b08a..8eec489ae 100644 --- a/shade/tests/functional/test_image.py +++ b/shade/tests/functional/test_image.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/functional/test_limits.py b/shade/tests/functional/test_limits.py index 2777caab9..47feafa36 100644 --- a/shade/tests/functional/test_limits.py +++ b/shade/tests/functional/test_limits.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/functional/test_magnum_services.py b/shade/tests/functional/test_magnum_services.py index 57f682371..d77468618 100644 --- a/shade/tests/functional/test_magnum_services.py +++ b/shade/tests/functional/test_magnum_services.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/functional/test_network.py b/shade/tests/functional/test_network.py index 7bb47820c..a50692747 100644 --- a/shade/tests/functional/test_network.py +++ b/shade/tests/functional/test_network.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/functional/test_object.py b/shade/tests/functional/test_object.py index 202e7063c..f6d5d9c16 100644 --- a/shade/tests/functional/test_object.py +++ b/shade/tests/functional/test_object.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/functional/test_quotas.py b/shade/tests/functional/test_quotas.py index 3f32a2795..26ffc755c 100644 --- a/shade/tests/functional/test_quotas.py +++ b/shade/tests/functional/test_quotas.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/functional/test_recordset.py b/shade/tests/functional/test_recordset.py index 7ba356304..97ccb5dc7 100644 --- a/shade/tests/functional/test_recordset.py +++ b/shade/tests/functional/test_recordset.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/functional/test_router.py b/shade/tests/functional/test_router.py index 986650744..6572c12ba 100644 --- a/shade/tests/functional/test_router.py +++ b/shade/tests/functional/test_router.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/functional/test_security_groups.py b/shade/tests/functional/test_security_groups.py index a2aede029..853684d69 100644 --- a/shade/tests/functional/test_security_groups.py +++ b/shade/tests/functional/test_security_groups.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/functional/test_server_group.py b/shade/tests/functional/test_server_group.py index 5b3544723..b92914472 100644 --- a/shade/tests/functional/test_server_group.py +++ b/shade/tests/functional/test_server_group.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/functional/test_stack.py b/shade/tests/functional/test_stack.py index c06708f5c..a711c54a3 100644 --- a/shade/tests/functional/test_stack.py +++ b/shade/tests/functional/test_stack.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/functional/test_users.py b/shade/tests/functional/test_users.py index 54b32d1bd..2ea3d57ce 100644 --- a/shade/tests/functional/test_users.py +++ b/shade/tests/functional/test_users.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/functional/test_volume.py b/shade/tests/functional/test_volume.py index d040bb720..23aa8cbbf 100644 --- a/shade/tests/functional/test_volume.py +++ b/shade/tests/functional/test_volume.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/functional/test_volume_backup.py b/shade/tests/functional/test_volume_backup.py index 09f678590..2efe94350 100644 --- a/shade/tests/functional/test_volume_backup.py +++ b/shade/tests/functional/test_volume_backup.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/functional/test_zone.py b/shade/tests/functional/test_zone.py index 032fe07c5..80ca09130 100644 --- a/shade/tests/functional/test_zone.py +++ b/shade/tests/functional/test_zone.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/functional/util.py b/shade/tests/functional/util.py index 23ddc3e80..a88e47de9 100644 --- a/shade/tests/functional/util.py +++ b/shade/tests/functional/util.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 321e9bb2c..3bff06361 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Copyright 2010-2011 OpenStack Foundation # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # diff --git a/shade/tests/unit/test__adapter.py b/shade/tests/unit/test__adapter.py index 96aed7dec..68063d65f 100644 --- a/shade/tests/unit/test__adapter.py +++ b/shade/tests/unit/test__adapter.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/test__utils.py b/shade/tests/unit/test__utils.py index 4ca82d52a..997ae874d 100644 --- a/shade/tests/unit/test__utils.py +++ b/shade/tests/unit/test__utils.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/test_aggregate.py b/shade/tests/unit/test_aggregate.py index 809cb0f83..d739e2cd3 100644 --- a/shade/tests/unit/test_aggregate.py +++ b/shade/tests/unit/test_aggregate.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 1612ab92b..10127f23c 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/test_cluster_templates.py b/shade/tests/unit/test_cluster_templates.py index 94493818c..e5f11847c 100644 --- a/shade/tests/unit/test_cluster_templates.py +++ b/shade/tests/unit/test_cluster_templates.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index 4d9d12592..b6c402f7f 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/test_create_volume_snapshot.py b/shade/tests/unit/test_create_volume_snapshot.py index 91af862ae..016c8c1ba 100644 --- a/shade/tests/unit/test_create_volume_snapshot.py +++ b/shade/tests/unit/test_create_volume_snapshot.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/test_delete_server.py b/shade/tests/unit/test_delete_server.py index 734a58f7a..785d19d3e 100644 --- a/shade/tests/unit/test_delete_server.py +++ b/shade/tests/unit/test_delete_server.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/test_delete_volume_snapshot.py b/shade/tests/unit/test_delete_volume_snapshot.py index 485f6f559..816051c1f 100644 --- a/shade/tests/unit/test_delete_volume_snapshot.py +++ b/shade/tests/unit/test_delete_volume_snapshot.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/test_domain_params.py b/shade/tests/unit/test_domain_params.py index 628fe9f3c..930f2f4a0 100644 --- a/shade/tests/unit/test_domain_params.py +++ b/shade/tests/unit/test_domain_params.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/test_flavors.py b/shade/tests/unit/test_flavors.py index d78b9fc5d..c56e12e19 100644 --- a/shade/tests/unit/test_flavors.py +++ b/shade/tests/unit/test_flavors.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/test_inventory.py b/shade/tests/unit/test_inventory.py index 4aac0364d..34914fdb6 100644 --- a/shade/tests/unit/test_inventory.py +++ b/shade/tests/unit/test_inventory.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/test_limits.py b/shade/tests/unit/test_limits.py index 6f5d4e4da..551f47d2e 100644 --- a/shade/tests/unit/test_limits.py +++ b/shade/tests/unit/test_limits.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/test_magnum_services.py b/shade/tests/unit/test_magnum_services.py index 500462358..c15f912ab 100644 --- a/shade/tests/unit/test_magnum_services.py +++ b/shade/tests/unit/test_magnum_services.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/test_normalize.py b/shade/tests/unit/test_normalize.py index bd4b6897d..446165203 100644 --- a/shade/tests/unit/test_normalize.py +++ b/shade/tests/unit/test_normalize.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index 89139cc0b..5d2ac6304 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2015 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/shade/tests/unit/test_project.py b/shade/tests/unit/test_project.py index 3e8ff42d6..1f4125d92 100644 --- a/shade/tests/unit/test_project.py +++ b/shade/tests/unit/test_project.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/test_quotas.py b/shade/tests/unit/test_quotas.py index 73fdf0aaa..b7050dfa8 100644 --- a/shade/tests/unit/test_quotas.py +++ b/shade/tests/unit/test_quotas.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/test_recordset.py b/shade/tests/unit/test_recordset.py index 7a5b37b39..484c82c03 100644 --- a/shade/tests/unit/test_recordset.py +++ b/shade/tests/unit/test_recordset.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/test_security_groups.py b/shade/tests/unit/test_security_groups.py index 72b38596a..026096ee7 100644 --- a/shade/tests/unit/test_security_groups.py +++ b/shade/tests/unit/test_security_groups.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/test_server_console.py b/shade/tests/unit/test_server_console.py index 2f5467912..2debf4635 100644 --- a/shade/tests/unit/test_server_console.py +++ b/shade/tests/unit/test_server_console.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/test_server_delete_metadata.py b/shade/tests/unit/test_server_delete_metadata.py index f7233a91c..3ba7da8a5 100644 --- a/shade/tests/unit/test_server_delete_metadata.py +++ b/shade/tests/unit/test_server_delete_metadata.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/test_server_group.py b/shade/tests/unit/test_server_group.py index 88fe2bb49..87ae9d9b5 100644 --- a/shade/tests/unit/test_server_group.py +++ b/shade/tests/unit/test_server_group.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/test_server_set_metadata.py b/shade/tests/unit/test_server_set_metadata.py index e31fbd24f..a4257d7a7 100644 --- a/shade/tests/unit/test_server_set_metadata.py +++ b/shade/tests/unit/test_server_set_metadata.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index ff438392c..1f06335c0 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index de97e6347..9693bda95 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/test_stack.py b/shade/tests/unit/test_stack.py index 5e1280a28..62a3458b4 100644 --- a/shade/tests/unit/test_stack.py +++ b/shade/tests/unit/test_stack.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/test_update_server.py b/shade/tests/unit/test_update_server.py index 95f51fc49..28e311ddc 100644 --- a/shade/tests/unit/test_update_server.py +++ b/shade/tests/unit/test_update_server.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/test_users.py b/shade/tests/unit/test_users.py index 7d4f9121e..3d2722a61 100644 --- a/shade/tests/unit/test_users.py +++ b/shade/tests/unit/test_users.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/test_volume.py b/shade/tests/unit/test_volume.py index 6d6383bee..7f0b2d175 100644 --- a/shade/tests/unit/test_volume.py +++ b/shade/tests/unit/test_volume.py @@ -1,5 +1,3 @@ -# -*- 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 diff --git a/shade/tests/unit/test_zone.py b/shade/tests/unit/test_zone.py index 26315cc0d..a5a8e161d 100644 --- a/shade/tests/unit/test_zone.py +++ b/shade/tests/unit/test_zone.py @@ -1,5 +1,3 @@ -# -*- 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 From 091fde16a38acf17e0d98db44cedc179464aa2f0 Mon Sep 17 00:00:00 2001 From: avnish Date: Thu, 12 Jan 2017 10:17:37 +0530 Subject: [PATCH 1191/3836] Use upper-constraints for tox envs Pin tox environments to upper-constraints to avoid conflicts with library releases. Change-Id: I17664e0794de05fb9661050018dff2a07b077826 Closes-Bug: #1628597 --- tools/tox_install.sh | 30 ++++++++++++++++++++++++++++++ tox.ini | 6 +++++- 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100755 tools/tox_install.sh diff --git a/tools/tox_install.sh b/tools/tox_install.sh new file mode 100755 index 000000000..43468e450 --- /dev/null +++ b/tools/tox_install.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +# Client constraint file contains this client version pin that is in conflict +# with installing the client from source. We should remove the version pin in +# the constraints file before applying it for from-source installation. + +CONSTRAINTS_FILE=$1 +shift 1 + +set -e + +# NOTE(tonyb): Place this in the tox enviroment's log dir so it will get +# published to logs.openstack.org for easy debugging. +localfile="$VIRTUAL_ENV/log/upper-constraints.txt" + +if [[ $CONSTRAINTS_FILE != http* ]]; then + CONSTRAINTS_FILE=file://$CONSTRAINTS_FILE +fi +# NOTE(tonyb): need to add curl to bindep.txt if the project supports bindep +curl $CONSTRAINTS_FILE --insecure --progress-bar --output $localfile + +pip install -c$localfile openstack-requirements + +# This is the main purpose of the script: Allow local installation of +# the current repo. It is listed in constraints file and thus any +# install will be constrained and we need to unconstrain it. +edit-constraints $localfile -- $CLIENT_NAME + +pip install -c$localfile -U $* +exit $? diff --git a/tox.ini b/tox.ini index cedf47857..08ba56e92 100644 --- a/tox.ini +++ b/tox.ini @@ -5,9 +5,13 @@ skipsdist = True [testenv] usedevelop = True -install_command = pip install -U {opts} {packages} +passenv = ZUUL_CACHE_DIR + REQUIREMENTS_PIP_LOCATION +install_command = {toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} setenv = VIRTUAL_ENV={envdir} + BRANCH_NAME=master + CLIENT_NAME=os-client-config deps = -r{toxinidir}/test-requirements.txt commands = python setup.py testr --slowest --testr-args='{posargs}' From 53858f340eeb55cf233a56fc6f63bd60f58251b3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 12 Jan 2017 08:17:22 -0500 Subject: [PATCH 1192/3836] Remove 3.4 from tox envlist We don't support it. Change-Id: I43c9bc965374cb8fbbe9c40f366aed31d8cf9022 --- setup.cfg | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index f8dcbb096..b87bd6ab3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,7 +16,7 @@ classifier = Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 [files] packages = diff --git a/tox.ini b/tox.ini index 08ba56e92..bf0fd8bf9 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 1.6 -envlist = py34,py35,py27,pypy,pep8 +envlist = py35,py27,pypy,pep8 skipsdist = True [testenv] From 9d145e0be29c996015f9019307aef88a553e01ad Mon Sep 17 00:00:00 2001 From: Roberto Polli Date: Fri, 23 Dec 2016 14:52:20 +0100 Subject: [PATCH 1193/3836] Added list_flavor_access. Change-Id: Ia983486ec4d587dd436e6a9c0b443be8ce1a7102 --- doc/source/model.rst | 13 +++++++++ ...d-list_flavor_access-e038253e953e6586.yaml | 4 +++ shade/_tasks.py | 5 ++++ shade/_utils.py | 11 ++++++++ shade/operatorcloud.py | 17 ++++++++++++ shade/tests/functional/test_flavor.py | 6 +++++ shade/tests/unit/test_flavors.py | 27 +++++++++++++++++++ 7 files changed, 83 insertions(+) create mode 100644 releasenotes/notes/add-list_flavor_access-e038253e953e6586.yaml diff --git a/doc/source/model.rst b/doc/source/model.rst index 0f5f2aff9..e084578fd 100644 --- a/doc/source/model.rst +++ b/doc/source/model.rst @@ -87,6 +87,19 @@ A flavor for a Nova Server. extra_specs=dict(), properties=dict()) + +Flavor Access +------ + +An access entry for a Nova Flavor. + +.. code-block:: python + + FlavorAccess = dict( + flavor_id=str(), + project_id=str()) + + Image ----- diff --git a/releasenotes/notes/add-list_flavor_access-e038253e953e6586.yaml b/releasenotes/notes/add-list_flavor_access-e038253e953e6586.yaml new file mode 100644 index 000000000..12f289f8b --- /dev/null +++ b/releasenotes/notes/add-list_flavor_access-e038253e953e6586.yaml @@ -0,0 +1,4 @@ +--- +features: + - Add a list_flavor_access method to list all + the projects/tenants allowed to access a given flavor. diff --git a/shade/_tasks.py b/shade/_tasks.py index 95c0002cf..0f697758c 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -102,6 +102,11 @@ def main(self, client): return client.nova_client.flavors.get(**self.args) +class FlavorListAccess(task_manager.Task): + def main(self, client): + return client.nova_client.flavor_access.list(**self.args) + + class FlavorAddAccess(task_manager.Task): def main(self, client): return client.nova_client.flavor_access.add_tenant_access( diff --git a/shade/_utils.py b/shade/_utils.py index 1e938fceb..9b2e94003 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -303,6 +303,17 @@ def normalize_roles(roles): return meta.obj_list_to_dict(ret) +def normalize_flavor_accesses(flavor_accesses): + """Normalize Flavor access list.""" + return [munch.Munch( + dict( + flavor_id=acl.get('flavor_id'), + project_id=acl.get('project_id') or acl.get('tenant_id'), + ) + ) for acl in flavor_accesses + ] + + def normalize_stacks(stacks): """ Normalize Stack Object """ for stack in stacks: diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 5f8f4e0f0..20bf3b5eb 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1593,6 +1593,23 @@ def remove_flavor_access(self, flavor_id, project_id): """ self._mod_flavor_access('remove', flavor_id, project_id) + def list_flavor_access(self, flavor_id): + """List access from a private flavor for a project/tenant. + + :param string flavor_id: ID of the private flavor. + + :returns: a list of ``munch.Munch`` containing the access description + + :raises: OpenStackCloudException on operation error. + """ + with _utils.shade_exceptions("Error trying to list access from " + "flavor ID {flavor}".format( + flavor=flavor_id)): + projects = self.manager.submit_task( + _tasks.FlavorListAccess(flavor=flavor_id) + ) + return _utils.normalize_flavor_accesses(projects) + def create_role(self, name): """Create a Keystone role. diff --git a/shade/tests/functional/test_flavor.py b/shade/tests/functional/test_flavor.py index 90a21ab96..6dbe91cee 100644 --- a/shade/tests/functional/test_flavor.py +++ b/shade/tests/functional/test_flavor.py @@ -129,6 +129,12 @@ def test_flavor_access(self): self.assertEqual(1, len(flavors)) self.assertEqual(priv_flavor_name, flavors[0]['name']) + # Now see if the 'demo' user has access to it without needing + # the demo_cloud access. + acls = self.operator_cloud.list_flavor_access(new_flavor['id']) + self.assertEqual(1, len(acls)) + self.assertEqual(project['id'], acls[0]['project_id']) + # Now revoke the access and make sure we can't find it self.operator_cloud.remove_flavor_access(new_flavor['id'], project['id']) diff --git a/shade/tests/unit/test_flavors.py b/shade/tests/unit/test_flavors.py index d78b9fc5d..6943a9213 100644 --- a/shade/tests/unit/test_flavors.py +++ b/shade/tests/unit/test_flavors.py @@ -14,6 +14,7 @@ import mock +from shade.tests.fakes import FakeFlavor, FakeProject import shade from keystoneauth1.fixture import keystoneauth_betamax @@ -117,9 +118,35 @@ def test_add_flavor_access(self, mock_nova): flavor='flavor_id', tenant='tenant_id' ) + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_add_flavor_access_by_flavor(self, mock_nova): + flavor = FakeFlavor(id='flavor_id', name='flavor_name', ram=None) + tenant = FakeProject('tenant_id') + self.op_cloud.add_flavor_access(flavor, tenant) + mock_nova.flavor_access.add_tenant_access.assert_called_once_with( + flavor=flavor, tenant=tenant + ) + @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_remove_flavor_access(self, mock_nova): self.op_cloud.remove_flavor_access('flavor_id', 'tenant_id') mock_nova.flavor_access.remove_tenant_access.assert_called_once_with( flavor='flavor_id', tenant='tenant_id' ) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_list_flavor_access(self, mock_nova): + mock_nova.flavors.list.return_value = [FakeFlavor( + id='flavor_id', name='flavor_name', ram=None)] + self.op_cloud.list_flavor_access('flavor_id') + mock_nova.flavor_access.list.assert_called_once_with( + flavor='flavor_id' + ) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_list_flavor_access_by_flavor(self, mock_nova): + flavor = FakeFlavor(id='flavor_id', name='flavor_name', ram=None) + self.op_cloud.list_flavor_access(flavor) + mock_nova.flavor_access.list.assert_called_once_with( + flavor=flavor + ) From 3280a731f3d5ccc9650de28c697f6da834e0aa31 Mon Sep 17 00:00:00 2001 From: tengqm Date: Fri, 13 Jan 2017 01:45:14 -0500 Subject: [PATCH 1194/3836] Add cluster_operation and node_operation This patch adds the support to 'cluster_operation' and 'node_operation' which were added in Senlin API microversion 1.4. These APIs enable a user to perform any operation that are provided by a backend service but not directly modeled by Senlin clustering service. Change-Id: I40e228227accac066b218b14227ae7904a270468 --- openstack/cluster/v1/_proxy.py | 85 +++++++++++++------ openstack/cluster/v1/cluster.py | 14 +++ openstack/cluster/v1/node.py | 14 +++ .../tests/unit/cluster/v1/test_cluster.py | 13 +++ openstack/tests/unit/cluster/v1/test_node.py | 12 +++ openstack/tests/unit/cluster/v1/test_proxy.py | 20 +++++ 6 files changed, 131 insertions(+), 27 deletions(-) diff --git a/openstack/cluster/v1/_proxy.py b/openstack/cluster/v1/_proxy.py index f5526c69a..e29d2ac35 100644 --- a/openstack/cluster/v1/_proxy.py +++ b/openstack/cluster/v1/_proxy.py @@ -421,11 +421,11 @@ def collect_cluster_attrs(self, cluster, path): cluster_id=cluster, path=path) def check_cluster(self, cluster, **params): - """check a cluster. + """Check a cluster. :param cluster: The value can be either the ID of a cluster or a :class:`~openstack.cluster.v1.cluster.Cluster` instance. - :param dict \*\*params: A dictionary providing the parameters for the + :param dict params: A dictionary providing the parameters for the check action. :returns: A dictionary containing the action ID. @@ -434,18 +434,32 @@ def check_cluster(self, cluster, **params): return obj.check(self.session, **params) def recover_cluster(self, cluster, **params): - """recover a node. + """recover a cluster. :param cluster: The value can be either the ID of a cluster or a :class:`~openstack.cluster.v1.cluster.Cluster` instance. - :param dict \*\*params: A dictionary providing the parameters for the - check action. + :param dict params: A dictionary providing the parameters for the + recover action. :returns: A dictionary containing the action ID. """ obj = self._get_resource(_cluster.Cluster, cluster) return obj.recover(self.session, **params) + def cluster_operation(self, cluster, operation, **params): + """Perform an operation on the specified cluster. + + :param cluster: The value can be either the ID of a cluster or a + :class:`~openstack.cluster.v1.cluster.Cluster` instance. + :param operation: A string specifying the operation to be performed. + :param dict params: A dictionary providing the parameters for the + operation. + + :returns: A dictionary containing the action ID. + """ + obj = self._get_resource(_cluster.Cluster, cluster) + return obj.op(self.session, operation, **params) + def create_node(self, **attrs): """Create a new node from attributes. @@ -473,28 +487,6 @@ def delete_node(self, node, ignore_missing=True): """ return self._delete(_node.Node, node, ignore_missing=ignore_missing) - def check_node(self, node, **params): - """check a node. - - :param node: The value can be either the ID of a node or a - :class:`~openstack.cluster.v1.node.Node` instance. - - :returns: A dictionary containing the action ID. - """ - obj = self._get_resource(_node.Node, node) - return obj.check(self.session, **params) - - def recover_node(self, node, **params): - """recover a node. - - :param node: The value can be either the ID of a node or a - :class:`~openstack.cluster.v1.node.Node` instance. - - :returns: A dictionary containing the action ID. - """ - obj = self._get_resource(_node.Node, node) - return obj.recover(self.session, **params) - def find_node(self, name_or_id, ignore_missing=True): """Find a single node. @@ -563,6 +555,45 @@ def update_node(self, node, **attrs): """ return self._update(_node.Node, node, **attrs) + def check_node(self, node, **params): + """Check the health of the specified node. + + :param node: The value can be either the ID of a node or a + :class:`~openstack.cluster.v1.node.Node` instance. + :param dict params: A dictionary providing the parametes to the check + action. + + :returns: A dictionary containing the action ID. + """ + obj = self._get_resource(_node.Node, node) + return obj.check(self.session, **params) + + def recover_node(self, node, **params): + """Recover the specified node into healthy status. + + :param node: The value can be either the ID of a node or a + :class:`~openstack.cluster.v1.node.Node` instance. + :param dict params: A dict supplying parameters to the recover action. + + :returns: A dictionary containing the action ID. + """ + obj = self._get_resource(_node.Node, node) + return obj.recover(self.session, **params) + + def node_operation(self, node, operation, **params): + """Perform an operation on the specified node. + + :param cluster: The value can be either the ID of a node or a + :class:`~openstack.cluster.v1.node.Node` instance. + :param operation: A string specifying the operation to be performed. + :param dict params: A dictionary providing the parameters for the + operation. + + :returns: A dictionary containing the action ID. + """ + obj = self._get_resource(_node.Node, node) + return obj.op(self.session, operation, **params) + def create_policy(self, **attrs): """Create a new policy from attributes. diff --git a/openstack/cluster/v1/cluster.py b/openstack/cluster/v1/cluster.py index a2dd62a65..53aa0cedf 100644 --- a/openstack/cluster/v1/cluster.py +++ b/openstack/cluster/v1/cluster.py @@ -163,3 +163,17 @@ def recover(self, session, **params): 'recover': params } return self.action(session, body) + + def op(self, session, operation, **params): + """Perform an operation on the cluster. + + :param session: A session object used for sending request. + :param operation: A string representing the operation to be performed. + :param dict params: An optional dict providing the parameters for the + operation. + :returns: A dictionary containing the action ID. + """ + url = utils.urljoin(self.base_path, self.id, 'ops') + resp = session.post(url, endpoint_filter=self.service, + json={operation: params}) + return resp.json() diff --git a/openstack/cluster/v1/node.py b/openstack/cluster/v1/node.py index 9593c7e9f..3b8bfa261 100644 --- a/openstack/cluster/v1/node.py +++ b/openstack/cluster/v1/node.py @@ -111,6 +111,20 @@ def recover(self, session, **params): } return self._action(session, body) + def op(self, session, operation, **params): + """Perform an operation on the specified node. + + :param session: A session object used for sending request. + :param operation: A string representing the operation to be performed. + :param dict params: An optional dict providing the parameters for the + operation. + :returns: A dictionary containing the action ID. + """ + url = utils.urljoin(self.base_path, self.id, 'ops') + resp = session.post(url, endpoint_filter=self.service, + json={operation: params}) + return resp.json() + class NodeDetail(Node): base_path = '/nodes/%(node_id)s?show_details=True' diff --git a/openstack/tests/unit/cluster/v1/test_cluster.py b/openstack/tests/unit/cluster/v1/test_cluster.py index 2eb616c42..ba0367a4c 100644 --- a/openstack/tests/unit/cluster/v1/test_cluster.py +++ b/openstack/tests/unit/cluster/v1/test_cluster.py @@ -261,3 +261,16 @@ def test_recover(self): body = {'recover': {}} sess.post.assert_called_once_with(url, endpoint_filter=sot.service, json=body) + + def test_operation(self): + sot = cluster.Cluster(**FAKE) + + resp = mock.Mock() + resp.json = mock.Mock(return_value='') + sess = mock.Mock() + sess.post = mock.Mock(return_value=resp) + self.assertEqual('', sot.op(sess, 'dance', style='tango')) + url = 'clusters/%s/ops' % sot.id + body = {'dance': {'style': 'tango'}} + sess.post.assert_called_once_with(url, endpoint_filter=sot.service, + json=body) diff --git a/openstack/tests/unit/cluster/v1/test_node.py b/openstack/tests/unit/cluster/v1/test_node.py index afc3cdeab..aefffd037 100644 --- a/openstack/tests/unit/cluster/v1/test_node.py +++ b/openstack/tests/unit/cluster/v1/test_node.py @@ -92,6 +92,18 @@ def test_recover(self): sess.post.assert_called_once_with(url, endpoint_filter=sot.service, json=body) + def test_operation(self): + sot = node.Node(**FAKE) + + resp = mock.Mock() + resp.json = mock.Mock(return_value='') + sess = mock.Mock() + sess.post = mock.Mock(return_value=resp) + self.assertEqual('', sot.op(sess, 'dance', style='tango')) + url = 'nodes/%s/ops' % sot.id + sess.post.assert_called_once_with(url, endpoint_filter=sot.service, + json={'dance': {'style': 'tango'}}) + class TestNodeDetail(testtools.TestCase): diff --git a/openstack/tests/unit/cluster/v1/test_proxy.py b/openstack/tests/unit/cluster/v1/test_proxy.py index b6643bace..2e19d6090 100644 --- a/openstack/tests/unit/cluster/v1/test_proxy.py +++ b/openstack/tests/unit/cluster/v1/test_proxy.py @@ -304,6 +304,16 @@ def test_cluster_recover(self, mock_get): method_args=["FAKE_CLUSTER"]) mock_get.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER") + @mock.patch.object(proxy_base.BaseProxy, '_get_resource') + def test_cluster_operation(self, mock_get): + mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') + mock_get.return_value = mock_cluster + self._verify("openstack.cluster.v1.cluster.Cluster.op", + self.proxy.cluster_operation, + method_args=["FAKE_CLUSTER", "dance"], + expected_args=["dance"]) + mock_get.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER") + def test_node_create(self): self.verify_create(self.proxy.create_node, node.Node) @@ -355,6 +365,16 @@ def test_node_recover(self, mock_get): method_args=["FAKE_NODE"]) mock_get.assert_called_once_with(node.Node, "FAKE_NODE") + @mock.patch.object(proxy_base.BaseProxy, '_get_resource') + def test_node_operation(self, mock_get): + mock_node = node.Node.new(id='FAKE_CLUSTER') + mock_get.return_value = mock_node + self._verify("openstack.cluster.v1.node.Node.op", + self.proxy.node_operation, + method_args=["FAKE_NODE", "dance"], + expected_args=["dance"]) + mock_get.assert_called_once_with(node.Node, "FAKE_NODE") + def test_policy_create(self): self.verify_create(self.proxy.create_policy, policy.Policy) From 5c95942a21990ea3b9144713659d1362e58f643a Mon Sep 17 00:00:00 2001 From: lixinhui Date: Sun, 15 Jan 2017 19:07:21 +0800 Subject: [PATCH 1195/3836] Add workflow service (mistral) This patch targets to support mistral service by SDK. Depends-On: I1b6754673b21a92aad1e8f0e10283859987108f6 Change-Id: Ie925f62b9e66091c3612a959d5ff281071dfefc4 --- openstack/profile.py | 2 + openstack/tests/unit/test_connection.py | 2 + openstack/tests/unit/test_profile.py | 1 + openstack/tests/unit/workflow/__init__.py | 0 .../tests/unit/workflow/test_execution.py | 50 ++++++ openstack/tests/unit/workflow/test_proxy.py | 64 +++++++ .../tests/unit/workflow/test_workflow.py | 45 +++++ openstack/workflow/v2/__init__.py | 0 openstack/workflow/v2/_proxy.py | 168 ++++++++++++++++++ openstack/workflow/v2/execution.py | 55 ++++++ openstack/workflow/v2/workflow.py | 62 +++++++ 11 files changed, 449 insertions(+) create mode 100644 openstack/tests/unit/workflow/__init__.py create mode 100644 openstack/tests/unit/workflow/test_execution.py create mode 100644 openstack/tests/unit/workflow/test_proxy.py create mode 100644 openstack/tests/unit/workflow/test_workflow.py create mode 100644 openstack/workflow/v2/__init__.py create mode 100644 openstack/workflow/v2/_proxy.py create mode 100644 openstack/workflow/v2/execution.py create mode 100644 openstack/workflow/v2/workflow.py diff --git a/openstack/profile.py b/openstack/profile.py index 1fed6564a..2cd7f2de8 100644 --- a/openstack/profile.py +++ b/openstack/profile.py @@ -71,6 +71,7 @@ from openstack.orchestration import orchestration_service from openstack.telemetry.alarm import alarm_service from openstack.telemetry import telemetry_service +from openstack.workflow import workflow_service _logger = logging.getLogger(__name__) @@ -108,6 +109,7 @@ def __init__(self, plugins=None): self._add_service( orchestration_service.OrchestrationService(version="v1")) self._add_service(telemetry_service.TelemetryService(version="v2")) + self._add_service(workflow_service.WorkflowService(version="v2")) if plugins: for plugin in plugins: diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index 909cca5ea..f84abf7fe 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -151,6 +151,8 @@ def test_create_session(self): conn.orchestration.__class__.__module__) self.assertEqual('openstack.telemetry.v2._proxy', conn.telemetry.__class__.__module__) + self.assertEqual('openstack.workflow.v2._proxy', + conn.workflow.__class__.__module__) def _prepare_test_config(self): # Create a temporary directory where our test config will live diff --git a/openstack/tests/unit/test_profile.py b/openstack/tests/unit/test_profile.py index b18b7eaa4..2ca67e028 100644 --- a/openstack/tests/unit/test_profile.py +++ b/openstack/tests/unit/test_profile.py @@ -33,6 +33,7 @@ def test_init(self): 'object-store', 'orchestration', 'volume', + 'workflowv2', ] self.assertEqual(expected, prof.service_keys) diff --git a/openstack/tests/unit/workflow/__init__.py b/openstack/tests/unit/workflow/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/unit/workflow/test_execution.py b/openstack/tests/unit/workflow/test_execution.py new file mode 100644 index 000000000..bb8a4c001 --- /dev/null +++ b/openstack/tests/unit/workflow/test_execution.py @@ -0,0 +1,50 @@ +# 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 testtools + +from openstack.workflow.v2 import execution + +FAKE_INPUT = { + 'cluster_id': '8c74607c-5a74-4490-9414-a3475b1926c2', + 'node_id': 'fba2cc5d-706f-4631-9577-3956048d13a2', + 'flavor_id': '1' +} + +FAKE = { + 'id': 'ffaed25e-46f5-4089-8e20-b3b4722fd597', + 'workflow_name': 'cluster-coldmigration', + 'input': FAKE_INPUT, +} + + +class TestExecution(testtools.TestCase): + + def setUp(self): + super(TestExecution, self).setUp() + + def test_basic(self): + sot = execution.Execution() + self.assertEqual('execution', sot.resource_key) + self.assertEqual('executions', sot.resources_key) + self.assertEqual('/executions', sot.base_path) + self.assertEqual('workflowv2', sot.service.service_type) + self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_delete) + + def test_instantiate(self): + sot = execution.Execution(**FAKE) + self.assertEqual(FAKE['id'], sot.id) + self.assertEqual(FAKE['workflow_name'], sot.workflow_name) + self.assertEqual(FAKE['input'], sot.input) diff --git a/openstack/tests/unit/workflow/test_proxy.py b/openstack/tests/unit/workflow/test_proxy.py new file mode 100644 index 000000000..3f01420c6 --- /dev/null +++ b/openstack/tests/unit/workflow/test_proxy.py @@ -0,0 +1,64 @@ +# 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.tests.unit import test_proxy_base2 +from openstack.workflow.v2 import _proxy +from openstack.workflow.v2 import execution +from openstack.workflow.v2 import workflow + + +class TestWorkflowProxy(test_proxy_base2.TestProxyBase): + def setUp(self): + super(TestWorkflowProxy, self).setUp() + self.proxy = _proxy.Proxy(self.session) + + def test_workflows(self): + self.verify_list(self.proxy.workflows, + workflow.Workflow, + paginated=True) + + def test_executions(self): + self.verify_list(self.proxy.executions, + execution.Execution, + paginated=True) + + def test_workflow_get(self): + self.verify_get(self.proxy.get_workflow, + workflow.Workflow) + + def test_execution_get(self): + self.verify_get(self.proxy.get_execution, + execution.Execution) + + def test_workflow_create(self): + self.verify_create(self.proxy.create_workflow, + workflow.Workflow) + + def test_execution_create(self): + self.verify_create(self.proxy.create_execution, + execution.Execution) + + def test_workflow_delete(self): + self.verify_delete(self.proxy.delete_workflow, + workflow.Workflow, True) + + def test_execution_delete(self): + self.verify_delete(self.proxy.delete_execution, + execution.Execution, True) + + def test_workflow_find(self): + self.verify_find(self.proxy.find_workflow, + workflow.Workflow) + + def test_execution_find(self): + self.verify_find(self.proxy.find_execution, + execution.Execution) diff --git a/openstack/tests/unit/workflow/test_workflow.py b/openstack/tests/unit/workflow/test_workflow.py new file mode 100644 index 000000000..c5b2e22a8 --- /dev/null +++ b/openstack/tests/unit/workflow/test_workflow.py @@ -0,0 +1,45 @@ +# 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 testtools + +from openstack.workflow.v2 import workflow + + +FAKE = { + 'scope': 'private', + 'id': 'ffaed25e-46f5-4089-8e20-b3b4722fd597', + 'definition': 'workflow_def', +} + + +class TestWorkflow(testtools.TestCase): + + def setUp(self): + super(TestWorkflow, self).setUp() + + def test_basic(self): + sot = workflow.Workflow() + self.assertEqual('workflow', sot.resource_key) + self.assertEqual('workflows', sot.resources_key) + self.assertEqual('/workflows', sot.base_path) + self.assertEqual('workflowv2', sot.service.service_type) + self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_delete) + + def test_instantiate(self): + sot = workflow.Workflow(**FAKE) + self.assertEqual(FAKE['id'], sot.id) + self.assertEqual(FAKE['scope'], sot.scope) + self.assertEqual(FAKE['definition'], sot.definition) diff --git a/openstack/workflow/v2/__init__.py b/openstack/workflow/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/workflow/v2/_proxy.py b/openstack/workflow/v2/_proxy.py new file mode 100644 index 000000000..8a99531e4 --- /dev/null +++ b/openstack/workflow/v2/_proxy.py @@ -0,0 +1,168 @@ +# 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 proxy2 +from openstack.workflow.v2 import execution as _execution +from openstack.workflow.v2 import workflow as _workflow + + +class Proxy(proxy2.BaseProxy): + + def create_workflow(self, **attrs): + """Create a new workflow from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.workflow.v2.workflow.Workflow`, + comprised of the properties on the Workflow class. + + :returns: The results of workflow creation + :rtype: :class:`~openstack.workflow.v2.workflow.Workflow` + """ + return self._create(_workflow.Workflow, **attrs) + + def get_workflow(self, *attrs): + """Get a workflow + + :param workflow: The value can be the name of a workflow or + :class:`~openstack.workflow.v2.workflow.Workflow` instance. + + :returns: One :class:`~openstack.workflow.v2.workflow.Workflow` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + workflow matching the name could be found. + """ + return self._get(_workflow.Workflow, *attrs) + + def workflows(self, **query): + """Retrieve a generator of workflows + + :param kwargs \*\*query: Optional query parameters to be sent to + restrict the workflows to be returned. Available parameters + include: + + * limit: Requests at most the specified number of items be + returned from the query. + * marker: Specifies the ID of the last-seen workflow. Use the + limit parameter to make an initial limited request and use + the ID of the last-seen workflow from the response as the + marker parameter value in a subsequent limited request. + + :returns: A generator of workflow instances. + """ + return self._list(_workflow.Workflow, paginated=True, **query) + + def delete_workflow(self, value, ignore_missing=True): + """Delete a workflow + + :param value: The value can be either the name of a workflow or a + :class:`~openstack.workflow.v2.workflow.Workflow` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will + be raised when the workflow does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent workflow. + + :returns: ``None`` + """ + return self._delete(_workflow.Workflow, value, + ignore_missing=ignore_missing) + + def find_workflow(self, name_or_id, ignore_missing=True): + """Find a single workflow + + :param name_or_id: The name or ID of an workflow. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :returns: One :class:`~openstack.compute.v2.workflow.Extension` or + None + """ + return self._find(_workflow.Workflow, name_or_id, + ignore_missing=ignore_missing) + + def create_execution(self, **attrs): + """Create a new execution from attributes + + :param workflow_name: The name of target workflow to execute. + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.workflow.v2.execution.Execution`, + comprised of the properties on the Execution class. + + :returns: The results of execution creation + :rtype: :class:`~openstack.workflow.v2.execution.Execution` + """ + return self._create(_execution.Execution, **attrs) + + def get_execution(self, *attrs): + """Get a execution + + :param workflow_name: The name of target workflow to execute. + :param execution: The value can be either the ID of a execution or a + :class:`~openstack.workflow.v2.execution.Execution` instance. + + :returns: One :class:`~openstack.workflow.v2.execution.Execution` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + execution matching the criteria could be found. + """ + return self._get(_execution.Execution, *attrs) + + def executions(self, **query): + """Retrieve a generator of executions + + :param kwargs \*\*query: Optional query parameters to be sent to + restrict the executions to be returned. Available parameters + include: + + * limit: Requests at most the specified number of items be + returned from the query. + * marker: Specifies the ID of the last-seen execution. Use the + limit parameter to make an initial limited request and use + the ID of the last-seen execution from the response as the + marker parameter value in a subsequent limited request. + + :returns: A generator of execution instances. + """ + return self._list(_execution.Execution, paginated=True, **query) + + def delete_execution(self, value, ignore_missing=True): + """Delete an execution + + :param value: The value can be either the name of a execution or a + :class:`~openstack.workflow.v2.execute.Execution` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the execution does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent execution. + + :returns: ``None`` + """ + return self._delete(_execution.Execution, value, + ignore_missing=ignore_missing) + + def find_execution(self, name_or_id, ignore_missing=True): + """Find a single execution + + :param name_or_id: The name or ID of an execution. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :returns: One :class:`~openstack.compute.v2.execution.Execution` or + None + """ + return self._find(_execution.Execution, name_or_id, + ignore_missing=ignore_missing) diff --git a/openstack/workflow/v2/execution.py b/openstack/workflow/v2/execution.py new file mode 100644 index 000000000..ef4a4a394 --- /dev/null +++ b/openstack/workflow/v2/execution.py @@ -0,0 +1,55 @@ +# 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 resource2 as resource +from openstack.workflow import workflow_service + + +class Execution(resource.Resource): + resource_key = 'execution' + resources_key = 'executions' + base_path = '/executions' + service = workflow_service.WorkflowService() + + # capabilities + allow_create = True + allow_list = True + allow_get = True + allow_delete = True + + _query_mapping = resource.QueryParameters( + 'marker', 'limit', 'sort_keys', 'sort_dirs', 'fields', 'params', + 'include_output') + + workflow_name = resource.Body("workflow_name") + workflow_id = resource.Body("workflow_id") + description = resource.Body("description") + task_execution_id = resource.Body("task_execution_id") + status = resource.Body("state") + status_info = resource.Body("state_info") + input = resource.Body("input") + output = resource.Body("output") + created_at = resource.Body("created_at") + updated_at = resource.Body("updated_at") + + def create(self, session, prepend_key=True): + request = self._prepare_request(requires_id=False, + prepend_key=prepend_key) + + request_body = request.body["execution"] + response = session.post(request.uri, + endpoint_filter=self.service, + json=request_body, + headers=request.headers) + + self._translate_response(response, has_body=True) + return self diff --git a/openstack/workflow/v2/workflow.py b/openstack/workflow/v2/workflow.py new file mode 100644 index 000000000..3039dd7df --- /dev/null +++ b/openstack/workflow/v2/workflow.py @@ -0,0 +1,62 @@ +# 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 resource2 as resource +from openstack.workflow import workflow_service + + +class Workflow(resource.Resource): + resource_key = 'workflow' + resources_key = 'workflows' + base_path = '/workflows' + service = workflow_service.WorkflowService() + + # capabilities + allow_create = True + allow_list = True + allow_get = True + allow_delete = True + + _query_mapping = resource.QueryParameters( + 'marker', 'limit', 'sort_keys', 'sort_dirs', 'fields') + + name = resource.Body("name") + input = resource.Body("input") + definition = resource.Body("definition") + tags = resource.Body("tags") + scope = resource.Body("scope") + project_id = resource.Body("project_id") + created_at = resource.Body("created_at") + updated_at = resource.Body("updated_at") + + def create(self, session, prepend_key=True): + request = self._prepare_request(requires_id=False, + prepend_key=prepend_key) + + headers = { + "Content-Type": 'text/plain' + } + kwargs = { + "data": self.definition, + } + + scope = "?scope=%s" % self.scope + uri = request.uri + scope + + request.headers.update(headers) + response = session.post(uri, + endpoint_filter=self.service, + json=None, + headers=request.headers, **kwargs) + + self._translate_response(response, has_body=False) + return self From 8b7e0492bcc640d0f488f7ace0854a2d35e94649 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 16 Jan 2017 17:28:13 +0000 Subject: [PATCH 1196/3836] Updated from global requirements Change-Id: Iff51fa5488115fc51c42bade6008228a3b4df06f --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7cae9d233..ba9f17a0d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,4 @@ pbr>=1.8 # Apache-2.0 six>=1.9.0 # MIT stevedore>=1.17.1 # Apache-2.0 os-client-config>=1.22.0 # Apache-2.0 -keystoneauth1>=2.16.0 # Apache-2.0 +keystoneauth1>=2.17.0 # Apache-2.0 From 0851770b0f56eb3b48e50eb8b5bd6ad8b41e882e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 25 Dec 2016 08:41:30 -0600 Subject: [PATCH 1197/3836] Add support to task manager for async tasks In order to support replacing SwiftService, we need to be able to spin some tasks into async threads. We'll allow for calculation of rate limiting to match with the start of the call, but will return a Future to the call site. This patch is followed by a patch to Nodepool to have Nodepool's TaskManager subclass shade's TaskManager. Co-Authored-By: David Shrewsbury Change-Id: Ib473ceece7169d76dd37702d66b188a841ec3f59 --- shade/_adapter.py | 3 ++- shade/task_manager.py | 39 +++++++++++++++++++++++---- shade/tests/unit/test_task_manager.py | 17 ++++++++++++ 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/shade/_adapter.py b/shade/_adapter.py index d15214a00..94dde19ed 100644 --- a/shade/_adapter.py +++ b/shade/_adapter.py @@ -140,7 +140,7 @@ def _munch_response(self, response, result_key=None): return meta.obj_to_dict(result, request_id=request_id) return result - def request(self, url, method, *args, **kwargs): + def request(self, url, method, run_async=False, *args, **kwargs): name_parts = extract_name(url) name = '.'.join([self.service_type, method] + name_parts) class_name = "".join([ @@ -155,6 +155,7 @@ def __init__(self, **kw): super(RequestTask, self).__init__(**kw) self.name = name self.__class__.__name__ = str(class_name) + self.run_async = run_async def main(self, client): self.args.setdefault('raise_exc', False) diff --git a/shade/task_manager.py b/shade/task_manager.py index 1c01b01ed..5144af01f 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -15,6 +15,7 @@ # limitations under the License. import abc +import concurrent.futures import sys import threading import time @@ -72,6 +73,7 @@ def __init__(self, **kw): self._result = None self._response = None self._finished = threading.Event() + self.run_async = False self.args = kw self.name = type(self).__name__ @@ -210,17 +212,23 @@ def main(self, client): class TaskManager(object): log = _log.setup_logging(__name__) - def __init__(self, client, name, result_filter_cb=None): + def __init__( + self, client, name, result_filter_cb=None, workers=5, **kwargs): self.name = name self._client = client + self._executor = concurrent.futures.ThreadPoolExecutor( + max_workers=workers) if not result_filter_cb: self._result_filter_cb = _result_filter_cb else: self._result_filter_cb = result_filter_cb + def set_client(self, client): + self._client = client + def stop(self): """ This is a direct action passthrough TaskManager """ - pass + self._executor.shutdown(wait=True) def run(self): """ This is a direct action passthrough TaskManager """ @@ -233,15 +241,36 @@ def submit_task(self, task, raw=False): :param bool raw: If True, return the raw result as received from the underlying client call. """ + return self.run_task(task=task, raw=raw) + + def _run_task_async(self, task, raw=False): + self.log.debug( + "Manager %s submitting task %s", self.name, task.name) + return self._executor.submit(self._run_task, task, raw=raw) + + def run_task(self, task, raw=False): + if hasattr(task, 'run_async') and task.run_async: + return self._run_task_async(task, raw=raw) + else: + return self._run_task(task, raw=raw) + + def _run_task(self, task, raw=False): self.log.debug( "Manager %s running task %s", self.name, task.name) start = time.time() task.run(self._client) end = time.time() + dt = end - start self.log.debug( - "Manager %s ran task %s in %ss", - self.name, task.name, (end - start)) + "Manager %s ran task %s in %ss", self.name, task.name, dt) + + self.post_run_task(dt) + return task.wait(raw) + + def post_run_task(self, elasped_time): + pass + # Backwards compatibility submitTask = submit_task @@ -257,4 +286,4 @@ def submit_function( task_class = generate_task_class(method, name, result_filter_cb) - return self.manager.submit_task(task_class(**kwargs)) + return self._executor.submit_task(task_class(**kwargs)) diff --git a/shade/tests/unit/test_task_manager.py b/shade/tests/unit/test_task_manager.py index 46531c8e0..1a416ee44 100644 --- a/shade/tests/unit/test_task_manager.py +++ b/shade/tests/unit/test_task_manager.py @@ -13,6 +13,9 @@ # limitations under the License. +import concurrent.futures +import mock + from shade import task_manager from shade.tests.unit import base @@ -56,6 +59,15 @@ def main(self, client): return set([1, 2]) +class TaskTestAsync(task_manager.Task): + def __init__(self): + super(task_manager.Task, self).__init__() + self.run_async = True + + def main(self, client): + pass + + class TestTaskManager(base.TestCase): def setUp(self): @@ -90,3 +102,8 @@ def test_dont_munchify_bool(self): def test_dont_munchify_set(self): ret = self.manager.submit_task(TaskTestSet()) self.assertIsInstance(ret, set) + + @mock.patch.object(concurrent.futures.ThreadPoolExecutor, 'submit') + def test_async(self, mock_submit): + self.manager.submit_task(TaskTestAsync()) + self.assertTrue(mock_submit.called) From bedc9c57c2f28f30bdc200f7c6edf653db4cde90 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 19 Oct 2016 16:17:13 -0500 Subject: [PATCH 1198/3836] Add OpenTelekomCloud to the vendors Change-Id: I82fad53ad2078f58ba14e16a7199b7b730a37457 --- doc/source/vendor-support.rst | 15 +++++++++++++++ os_client_config/vendors/otc.json | 13 +++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 os_client_config/vendors/otc.json diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index b301d8017..ff0669102 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -134,6 +134,21 @@ RegionOne Ashburn, VA * Public IPv4 is provided via NAT with Neutron Floating IP * IPv6 is provided to every server +otc +--- + +https://iam.%(region_name)s.otc.t-systems.com/v3 + +============== ================ +Region Name Location +============== ================ +eu-de Germany +============== ================ + +* Identity API Version is 3 +* Images must be in `vhd` format +* Public IPv4 is provided via NAT with Neutron Floating IP + elastx ------ diff --git a/os_client_config/vendors/otc.json b/os_client_config/vendors/otc.json new file mode 100644 index 000000000..b0c1b116f --- /dev/null +++ b/os_client_config/vendors/otc.json @@ -0,0 +1,13 @@ +{ + "name": "otc", + "profile": { + "auth": { + "auth_url": "https://iam.%(region_name)s.otc.t-systems.com/v3" + }, + "regions": [ + "eu-de" + ], + "identity_api_version": "3", + "image_format": "vhd" + } +} From 0f706a7ded8ace403c5bfac0e58d3a644285a6f3 Mon Sep 17 00:00:00 2001 From: Roberto Polli Date: Fri, 23 Dec 2016 13:03:07 +0100 Subject: [PATCH 1199/3836] Basic volume_type access Change-Id: I86405e7272c4101f6f03841ff65deec057fbed0b --- doc/source/model.rst | 32 +++++ .../notes/volume-types-a07a14ae668e7dd2.yaml | 4 + shade/_normalize.py | 50 +++++++ shade/_tasks.py | 22 ++++ shade/openstackcloud.py | 41 ++++++ shade/operatorcloud.py | 64 +++++++++ shade/tests/functional/test_volume_type.py | 114 ++++++++++++++++ shade/tests/unit/test_volume_access.py | 122 ++++++++++++++++++ 8 files changed, 449 insertions(+) create mode 100644 releasenotes/notes/volume-types-a07a14ae668e7dd2.yaml create mode 100644 shade/tests/functional/test_volume_type.py create mode 100644 shade/tests/unit/test_volume_access.py diff --git a/doc/source/model.rst b/doc/source/model.rst index 0f5f2aff9..28652ae49 100644 --- a/doc/source/model.rst +++ b/doc/source/model.rst @@ -274,3 +274,35 @@ A volume from cinder. is_encrypted=bool(), can_multiattach=bool(), properties=dict()) + + +VolumeType +------ + +A volume type from cinder. + +.. code-block:: python + + VolumeType = dict( + location=Location(), + id=str(), + name=str(), + description=str() or None, + is_public=bool(), + qos_specs_id=str() or None, + extra_specs=dict(), + properties=dict()) + + +VolumeTypeAccess +------ + +A volume type access from cinder. + +.. code-block:: python + + VolumeTypeAccess = dict( + location=Location(), + volume_type_id=str(), + project_id=str(), + properties=dict()) diff --git a/releasenotes/notes/volume-types-a07a14ae668e7dd2.yaml b/releasenotes/notes/volume-types-a07a14ae668e7dd2.yaml new file mode 100644 index 000000000..59fea21bb --- /dev/null +++ b/releasenotes/notes/volume-types-a07a14ae668e7dd2.yaml @@ -0,0 +1,4 @@ +--- +features: + - Add support for listing volume types. + - Add support for managing volume type access. diff --git a/shade/_normalize.py b/shade/_normalize.py index d2c2da8cd..50144ad01 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -578,6 +578,56 @@ def _normalize_project(self, project): return ret + def _normalize_volume_type_access(self, volume_type_access): + + volume_type_access = volume_type_access.copy() + + volume_type_id = volume_type_access.pop('volume_type_id') + project_id = volume_type_access.pop('project_id') + ret = munch.Munch( + location=self.current_location, + project_id=project_id, + volume_type_id=volume_type_id, + properties=volume_type_access.copy(), + ) + return ret + + def _normalize_volume_type_accesses(self, volume_type_accesses): + ret = [] + for volume_type_access in volume_type_accesses: + ret.append(self._normalize_volume_type_access(volume_type_access)) + return ret + + def _normalize_volume_type(self, volume_type): + + volume_type = volume_type.copy() + + volume_id = volume_type.pop('id') + description = volume_type.pop('description', None) + name = volume_type.pop('name', None) + old_is_public = volume_type.pop('os-volume-type-access:is_public', + False) + is_public = volume_type.pop('is_public', old_is_public) + qos_specs_id = volume_type.pop('qos_specs_id', None) + extra_specs = volume_type.pop('extra_specs', {}) + ret = munch.Munch( + location=self.current_location, + is_public=is_public, + id=volume_id, + name=name, + description=description, + qos_specs_id=qos_specs_id, + extra_specs=extra_specs, + properties=volume_type.copy(), + ) + return ret + + def _normalize_volume_types(self, volume_types): + ret = [] + for volume in volume_types: + ret.append(self._normalize_volume_type(volume)) + return ret + def _normalize_volumes(self, volumes): """Normalize the structure of volumes diff --git a/shade/_tasks.py b/shade/_tasks.py index 95c0002cf..530ca887a 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -296,6 +296,28 @@ def main(self, client): return client.nova_client.servers.create_image(**self.args) +class VolumeTypeList(task_manager.Task): + def main(self, client): + return client.cinder_client.volume_types.list() + + +class VolumeTypeAccessList(task_manager.Task): + def main(self, client): + return client.cinder_client.volume_type_access.list(**self.args) + + +class VolumeTypeAccessAdd(task_manager.Task): + def main(self, client): + return client.cinder_client.volume_type_access.add_project_access( + **self.args) + + +class VolumeTypeAccessRemove(task_manager.Task): + def main(self, client): + return client.cinder_client.volume_type_access.remove_project_access( + **self.args) + + class VolumeCreate(task_manager.Task): def main(self, client): return client.cinder_client.volumes.create(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index ce0bdde04..ed158dce7 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1457,6 +1457,11 @@ def search_volume_backups(self, name_or_id=None, filters=None): return _utils._filter_list( volume_backups, name_or_id, filters) + def search_volume_types( + self, name_or_id=None, filters=None, get_extra=True): + volume_types = self.list_volume_types(get_extra=get_extra) + return _utils._filter_list(volume_types, name_or_id, filters) + def search_flavors(self, name_or_id=None, filters=None, get_extra=True): flavors = self.list_flavors(get_extra=get_extra) return _utils._filter_list(flavors, name_or_id, filters) @@ -1623,6 +1628,17 @@ def list_volumes(self, cache=True): return self._normalize_volumes( self.manager.submit_task(_tasks.VolumeList())) + @_utils.cache_on_arguments() + def list_volume_types(self, get_extra=True): + """List all available volume types. + + :returns: A list of volume ``munch.Munch``. + + """ + with _utils.shade_exceptions("Error fetching volume_type list"): + return self._normalize_volume_types( + self.manager.submit_task(_tasks.VolumeTypeList())) + @_utils.cache_on_arguments() def list_flavors(self, get_extra=True): """List all available flavors. @@ -2326,6 +2342,31 @@ def get_volume(self, name_or_id, filters=None): """ return _utils._get_entity(self.search_volumes, name_or_id, filters) + def get_volume_type(self, name_or_id, filters=None): + """Get a volume type by name or ID. + + :param name_or_id: Name or ID of the volume. + :param filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: A volume ``munch.Munch`` or None if no matching volume is + found. + + """ + return _utils._get_entity( + self.search_volume_types, name_or_id, filters) + def get_flavor(self, name_or_id, filters=None, get_extra=True): """Get a flavor by name or ID. diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 5f8f4e0f0..f20187aee 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1960,6 +1960,70 @@ def remove_host_from_aggregate(self, name_or_id, host_name): return self.manager.submit_task(_tasks.AggregateRemoveHost( aggregate=aggregate['id'], host=host_name)) + def get_volume_type_access(self, name_or_id): + """Return a list of volume_type_access. + + :param name_or_id: Name or ID of the volume type. + + :raises: OpenStackCloudException on operation error. + """ + volume_type = self.get_volume_type(name_or_id) + if not volume_type: + raise OpenStackCloudException( + "VolumeType not found: %s" % name_or_id) + + with _utils.shade_exceptions( + "Unable to get volume type access {name}".format( + name=name_or_id)): + return self._normalize_volume_type_accesses( + self.manager.submit_task( + _tasks.VolumeTypeAccessList(volume_type=volume_type)) + ) + + def add_volume_type_access(self, name_or_id, project_id): + """Grant access on a volume_type to a project. + + :param name_or_id: ID or name of a volume_type + :param project_id: A project id + + NOTE: the call works even if the project does not exist. + + :raises: OpenStackCloudException on operation error. + """ + volume_type = self.get_volume_type(name_or_id) + if not volume_type: + raise OpenStackCloudException( + "VolumeType not found: %s" % name_or_id) + with _utils.shade_exceptions( + "Unable to authorize {project} " + "to use volume type {name}".format( + name=name_or_id, project=project_id + )): + self.manager.submit_task( + _tasks.VolumeTypeAccessAdd( + volume_type=volume_type, project=project_id)) + + def remove_volume_type_access(self, name_or_id, project_id): + """Revoke access on a volume_type to a project. + + :param name_or_id: ID or name of a volume_type + :param project_id: A project id + + :raises: OpenStackCloudException on operation error. + """ + volume_type = self.get_volume_type(name_or_id) + if not volume_type: + raise OpenStackCloudException( + "VolumeType not found: %s" % name_or_id) + with _utils.shade_exceptions( + "Unable to revoke {project} " + "to use volume type {name}".format( + name=name_or_id, project=project_id + )): + self.manager.submit_task( + _tasks.VolumeTypeAccessRemove( + volume_type=volume_type, project=project_id)) + def set_compute_quotas(self, name_or_id, **kwargs): """ Set a quota in a project diff --git a/shade/tests/functional/test_volume_type.py b/shade/tests/functional/test_volume_type.py new file mode 100644 index 000000000..bb3226fc0 --- /dev/null +++ b/shade/tests/functional/test_volume_type.py @@ -0,0 +1,114 @@ +# -*- 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. + +""" +test_volume +---------------------------------- + +Functional tests for `shade` block storage methods. +""" +import testtools +from shade.exc import OpenStackCloudException +from shade.tests.functional import base + + +class TestVolumeType(base.BaseFunctionalTestCase): + + def _assert_project(self, volume_name_or_id, project_id, allowed=True): + acls = self.operator_cloud.get_volume_type_access(volume_name_or_id) + allowed_projects = [x.get('project_id') for x in acls] + self.assertEqual(allowed, project_id in allowed_projects) + + def setUp(self): + super(TestVolumeType, self).setUp() + if not self.demo_cloud.has_service('volume'): + self.skipTest('volume service not supported by cloud') + self.operator_cloud.cinder_client.volume_types.create( + 'test-volume-type', is_public=False) + + def tearDown(self): + ret = self.operator_cloud.get_volume_type('test-volume-type') + if ret.get('id'): + self.operator_cloud.cinder_client.volume_types.delete(ret.id) + super(TestVolumeType, self).tearDown() + + def test_list_volume_types(self): + volume_types = self.operator_cloud.list_volume_types() + self.assertTrue(volume_types) + self.assertTrue(any( + x for x in volume_types if x.name == 'test-volume-type')) + + def test_add_remove_volume_type_access(self): + volume_type = self.operator_cloud.get_volume_type('test-volume-type') + self.assertEqual('test-volume-type', volume_type.name) + + self.operator_cloud.add_volume_type_access( + 'test-volume-type', + self.operator_cloud.current_project_id) + self._assert_project( + 'test-volume-type', self.operator_cloud.current_project_id, + allowed=True) + + self.operator_cloud.remove_volume_type_access( + 'test-volume-type', + self.operator_cloud.current_project_id) + self._assert_project( + 'test-volume-type', self.operator_cloud.current_project_id, + allowed=False) + + def test_add_volume_type_access_missing_project(self): + # Project id is not valitaded and it may not exist. + self.operator_cloud.add_volume_type_access( + 'test-volume-type', + '00000000000000000000000000000000') + + self.operator_cloud.remove_volume_type_access( + 'test-volume-type', + '00000000000000000000000000000000') + + def test_add_volume_type_access_missing_volume(self): + with testtools.ExpectedException( + OpenStackCloudException, + "VolumeType not found.*" + ): + self.operator_cloud.add_volume_type_access( + 'MISSING_VOLUME_TYPE', + self.operator_cloud.current_project_id) + + def test_remove_volume_type_access_missing_volume(self): + with testtools.ExpectedException( + OpenStackCloudException, + "VolumeType not found.*" + ): + self.operator_cloud.remove_volume_type_access( + 'MISSING_VOLUME_TYPE', + self.operator_cloud.current_project_id) + + def test_add_volume_type_access_bad_project(self): + with testtools.ExpectedException( + OpenStackCloudException, + "Unable to authorize.*" + ): + self.operator_cloud.add_volume_type_access( + 'test-volume-type', + 'BAD_PROJECT_ID') + + def test_remove_volume_type_access_missing_project(self): + with testtools.ExpectedException( + OpenStackCloudException, + "Unable to revoke.*" + ): + self.operator_cloud.remove_volume_type_access( + 'test-volume-type', + '00000000000000000000000000000000') diff --git a/shade/tests/unit/test_volume_access.py b/shade/tests/unit/test_volume_access.py new file mode 100644 index 000000000..29a8e2067 --- /dev/null +++ b/shade/tests/unit/test_volume_access.py @@ -0,0 +1,122 @@ +# -*- 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. + + +import mock +import testtools + +import shade +from shade.tests.unit import base + + +class TestVolumeAccess(base.TestCase): + @mock.patch.object(shade.OpenStackCloud, 'cinder_client') + def test_list_volume_types(self, mock_cinder): + volume_type = dict( + id='voltype01', description='volume type description', + name='name', is_public=False) + mock_cinder.volume_types.list.return_value = [volume_type] + + self.assertTrue(self.cloud.list_volume_types()) + + @mock.patch.object(shade.OpenStackCloud, 'cinder_client') + def test_get_volume_type(self, mock_cinder): + volume_type = dict( + id='voltype01', description='volume type description', name='name', + is_public=False) + mock_cinder.volume_types.list.return_value = [volume_type] + + volume_type_got = self.cloud.get_volume_type('name') + self.assertEqual(volume_type_got.id, volume_type['id']) + + @mock.patch.object(shade.OpenStackCloud, 'cinder_client') + def test_get_volume_type_access(self, mock_cinder): + volume_type = dict( + id='voltype01', description='volume type description', name='name', + is_public=False) + volume_type_access = [ + dict(volume_type_id='voltype01', name='name', project_id='prj01'), + dict(volume_type_id='voltype01', name='name', project_id='prj02') + ] + mock_cinder.volume_types.list.return_value = [volume_type] + mock_cinder.volume_type_access.list.return_value = volume_type_access + + self.assertEqual( + len(self.op_cloud.get_volume_type_access('name')), 2) + + @mock.patch.object(shade.OpenStackCloud, 'cinder_client') + def test_remove_volume_type_access(self, mock_cinder): + volume_type = dict( + id='voltype01', description='volume type description', name='name', + is_public=False) + project_001 = dict(volume_type_id='voltype01', name='name', + project_id='prj01') + project_002 = dict(volume_type_id='voltype01', name='name', + project_id='prj02') + volume_type_access = [project_001, project_002] + mock_cinder.volume_types.list.return_value = [volume_type] + mock_cinder.volume_type_access.list.return_value = volume_type_access + + def _fake_remove(*args, **kwargs): + volume_type_access.pop() + + mock_cinder.volume_type_access.remove_project_access.side_effect = \ + _fake_remove + + self.assertEqual( + len(self.op_cloud.get_volume_type_access( + volume_type['name'])), 2) + self.op_cloud.remove_volume_type_access( + volume_type['name'], project_001['project_id']) + + self.assertEqual( + len(self.op_cloud.get_volume_type_access('name')), 1) + + @mock.patch.object(shade.OpenStackCloud, 'cinder_client') + def test_add_volume_type_access(self, mock_cinder): + volume_type = dict( + id='voltype01', description='volume type description', name='name', + is_public=False) + project_001 = dict(volume_type_id='voltype01', name='name', + project_id='prj01') + project_002 = dict(volume_type_id='voltype01', name='name', + project_id='prj02') + volume_type_access = [project_001] + mock_cinder.volume_types.list.return_value = [volume_type] + mock_cinder.volume_type_access.list.return_value = volume_type_access + mock_cinder.volume_type_access.add_project_access.return_value = None + + def _fake_add(*args, **kwargs): + volume_type_access.append(project_002) + + mock_cinder.volume_type_access.add_project_access.side_effect = \ + _fake_add + + self.op_cloud.add_volume_type_access( + volume_type['name'], project_002['project_id']) + self.assertEqual( + len(self.op_cloud.get_volume_type_access('name')), 2) + + @mock.patch.object(shade.OpenStackCloud, 'cinder_client') + def test_add_volume_type_access_missing(self, mock_cinder): + volume_type = dict( + id='voltype01', description='volume type description', name='name', + is_public=False) + project_001 = dict(volume_type_id='voltype01', name='name', + project_id='prj01') + mock_cinder.volume_types.list.return_value = [volume_type] + with testtools.ExpectedException(shade.OpenStackCloudException, + "VolumeType not found: MISSING"): + self.op_cloud.add_volume_type_access( + "MISSING", project_001['project_id']) From 991b9ebb208a89a34a5ada7f3a838a4099b5d617 Mon Sep 17 00:00:00 2001 From: Shashank Kumar Shankar Date: Tue, 17 Jan 2017 22:26:28 +0000 Subject: [PATCH 1200/3836] Corrections in DHCP Agent Resource listing. In patch [1] DHCP Agent resource was introduced. But while implementing the respective OpenStack clients [2], it was seen that the agent and network listing classes needs to swapped for correct output at the OpenStack client side. This patch fixes the issue. [1] - https://review.openstack.org/#/c/387602/ [2] - https://review.openstack.org/#/c/387611/ Change-Id: I5abe69859081615aa3b02ac6fb37beaaac5ff634 --- openstack/network/v2/_proxy.py | 4 ++-- openstack/network/v2/agent.py | 12 ++++++------ openstack/network/v2/network.py | 12 ++++++------ openstack/tests/unit/network/v2/test_agent.py | 12 ++++++------ openstack/tests/unit/network/v2/test_network.py | 12 ++++++------ openstack/tests/unit/network/v2/test_proxy.py | 4 ++-- 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index d4ab2393b..1b32a27b8 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -212,7 +212,7 @@ def dhcp_agent_hosting_networks(self, agent, **query): :return: A generator of networks """ agent_obj = self._get_resource(_agent.Agent, agent) - return self._list(_agent.DHCPAgentHostingNetwork, paginated=False, + return self._list(_network.DHCPAgentHostingNetwork, paginated=False, agent_id=agent_obj.id, **query) def add_dhcp_agent_to_network(self, agent, network): @@ -252,7 +252,7 @@ def network_hosting_dhcp_agents(self, network, **query): :return: A generator of hosted DHCP agents """ net = self._get_resource(_network.Network, network) - return self._list(_network.NetworkHostingDHCPAgent, paginated=False, + return self._list(_agent.NetworkHostingDHCPAgent, paginated=False, network_id=net.id, **query) def get_auto_allocated_topology(self, project=None): diff --git a/openstack/network/v2/agent.py b/openstack/network/v2/agent.py index bfda9d2b0..549456afa 100644 --- a/openstack/network/v2/agent.py +++ b/openstack/network/v2/agent.py @@ -76,11 +76,11 @@ def remove_agent_from_network(self, session, **body): session.delete(url, endpoint_filter=self.service, json=body) -class DHCPAgentHostingNetwork(resource.Resource): - resource_key = 'network' - resources_key = 'networks' - base_path = '/agents/%(agent_id)s/dhcp-networks' - resource_name = 'dhcp-network' +class NetworkHostingDHCPAgent(Agent): + resource_key = 'agent' + resources_key = 'agents' + resource_name = 'dhcp-agent' + base_path = '/networks/%(network_id)s/dhcp-agents' service = network_service.NetworkService() # capabilities @@ -90,4 +90,4 @@ class DHCPAgentHostingNetwork(resource.Resource): allow_delete = False allow_list = True - # NOTE: No query parameter is supported + # NOTE: Doesn't support query yet. diff --git a/openstack/network/v2/network.py b/openstack/network/v2/network.py index aa875875c..9e79b519e 100644 --- a/openstack/network/v2/network.py +++ b/openstack/network/v2/network.py @@ -110,11 +110,11 @@ class Network(resource.Resource): updated_at = resource.Body('updated_at') -class NetworkHostingDHCPAgent(resource.Resource): - resource_key = 'agent' - resources_key = 'agents' - resource_name = 'dhcp-agent' - base_path = '/networks/%(network_id)s/dhcp-agents' +class DHCPAgentHostingNetwork(Network): + resource_key = 'network' + resources_key = 'networks' + base_path = '/agents/%(agent_id)s/dhcp-networks' + resource_name = 'dhcp-network' service = network_service.NetworkService() # capabilities @@ -124,4 +124,4 @@ class NetworkHostingDHCPAgent(resource.Resource): allow_delete = False allow_list = True - # NOTE: Doesn't support query yet. + # NOTE: No query parameter is supported diff --git a/openstack/tests/unit/network/v2/test_agent.py b/openstack/tests/unit/network/v2/test_agent.py index 841af05d7..003520b7a 100644 --- a/openstack/tests/unit/network/v2/test_agent.py +++ b/openstack/tests/unit/network/v2/test_agent.py @@ -90,14 +90,14 @@ def test_remove_agent_from_network(self): endpoint_filter=net.service, json=body) -class TestDHCPAgentHostingNetwork(testtools.TestCase): +class TestNetworkHostingDHCPAgent(testtools.TestCase): def test_basic(self): - net = agent.DHCPAgentHostingNetwork() - self.assertEqual('network', net.resource_key) - self.assertEqual('networks', net.resources_key) - self.assertEqual('/agents/%(agent_id)s/dhcp-networks', net.base_path) - self.assertEqual('dhcp-network', net.resource_name) + net = agent.NetworkHostingDHCPAgent() + self.assertEqual('agent', net.resource_key) + self.assertEqual('agents', net.resources_key) + self.assertEqual('/networks/%(network_id)s/dhcp-agents', net.base_path) + self.assertEqual('dhcp-agent', net.resource_name) self.assertEqual('network', net.service.service_type) self.assertFalse(net.allow_create) self.assertTrue(net.allow_get) diff --git a/openstack/tests/unit/network/v2/test_network.py b/openstack/tests/unit/network/v2/test_network.py index 9007c9451..34f5cdfa7 100644 --- a/openstack/tests/unit/network/v2/test_network.py +++ b/openstack/tests/unit/network/v2/test_network.py @@ -94,14 +94,14 @@ def test_make_it(self): self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) -class TestNetworkHostingDHCPAgent(testtools.TestCase): +class TestDHCPAgentHostingNetwork(testtools.TestCase): def test_basic(self): - net = network.NetworkHostingDHCPAgent() - self.assertEqual('agent', net.resource_key) - self.assertEqual('agents', net.resources_key) - self.assertEqual('/networks/%(network_id)s/dhcp-agents', net.base_path) - self.assertEqual('dhcp-agent', net.resource_name) + net = network.DHCPAgentHostingNetwork() + self.assertEqual('network', net.resource_key) + self.assertEqual('networks', net.resources_key) + self.assertEqual('/agents/%(agent_id)s/dhcp-networks', net.base_path) + self.assertEqual('dhcp-network', net.resource_name) self.assertEqual('network', net.service.service_type) self.assertFalse(net.allow_create) self.assertTrue(net.allow_get) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 41dbdd636..c01e79be1 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -114,7 +114,7 @@ def test_availability_zones(self): def test_dhcp_agent_hosting_networks(self): self.verify_list( self.proxy.dhcp_agent_hosting_networks, - agent.DHCPAgentHostingNetwork, + network.DHCPAgentHostingNetwork, paginated=False, method_kwargs={'agent': AGENT_ID}, expected_kwargs={'agent_id': AGENT_ID} @@ -123,7 +123,7 @@ def test_dhcp_agent_hosting_networks(self): def test_network_hosting_dhcp_agents(self): self.verify_list( self.proxy.network_hosting_dhcp_agents, - network.NetworkHostingDHCPAgent, + agent.NetworkHostingDHCPAgent, paginated=False, method_kwargs={'network': NETWORK_ID}, expected_kwargs={'network_id': NETWORK_ID} From cc2b337dcd55d8d2edd53b6000f1dd128636b529 Mon Sep 17 00:00:00 2001 From: Andy Botting Date: Wed, 18 Jan 2017 15:54:00 +1100 Subject: [PATCH 1201/3836] Add support for Murano Add Murano support with the service name 'application-catalog' Change-Id: I42794993b8f6208d40786e83ec80ee64a0879415 --- os_client_config/constructors.json | 1 + os_client_config/defaults.json | 1 + 2 files changed, 2 insertions(+) diff --git a/os_client_config/constructors.json b/os_client_config/constructors.json index a78be6bcb..7ad420e61 100644 --- a/os_client_config/constructors.json +++ b/os_client_config/constructors.json @@ -1,4 +1,5 @@ { + "application-catalog": "muranoclient.client.Client", "compute": "novaclient.client.Client", "container-infra": "magnumclient.client.Client", "database": "troveclient.client.Client", diff --git a/os_client_config/defaults.json b/os_client_config/defaults.json index b4e9dea67..65f896151 100644 --- a/os_client_config/defaults.json +++ b/os_client_config/defaults.json @@ -1,4 +1,5 @@ { + "application_catalog_api_version": "1", "auth_type": "password", "baremetal_api_version": "1", "container_api_version": "1", From 3c47e251c978cf9c5e393f032c0c70d67c1dcb00 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 18 Jan 2017 14:25:53 -0600 Subject: [PATCH 1202/3836] Revert "Fix interface_key for identity clients" This patch breaks stable/newton devstack-gate of shade. This reverts commit fa4e1bd21db4bee2a0ee779067cdd659e647d7fc. Change-Id: I31a7831693f567a0717a9b41c242453fb937d6d7 --- os_client_config/cloud_config.py | 2 +- os_client_config/tests/test_cloud_config.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index ecf60a6e1..a7fc0582f 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -318,7 +318,7 @@ def get_legacy_client( endpoint_override = self.get_endpoint(service_key) if not interface_key: - if service_key in ('image', 'key-manager', 'identity'): + if service_key in ('image', 'key-manager'): interface_key = 'interface' else: interface_key = 'endpoint_type' diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index f97f85b67..6f960e6e1 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -524,7 +524,7 @@ def test_legacy_client_identity(self, mock_get_session_endpoint): mock_client.assert_called_with( version='2.0', endpoint='http://example.com/v2', - interface='admin', + endpoint_type='admin', endpoint_override=None, region_name='region-al', service_type='identity', @@ -544,7 +544,7 @@ def test_legacy_client_identity_v3(self, mock_get_session_endpoint): mock_client.assert_called_with( version='3', endpoint='http://example.com', - interface='admin', + endpoint_type='admin', endpoint_override=None, region_name='region-al', service_type='identity', From cf2d3500271469fcec692c2c96ad358e22c831c9 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 18 Jan 2017 16:39:28 -0600 Subject: [PATCH 1203/3836] Add helper script to install branch tips shade has a functional test that intends to test shade against the current tip of client libs. Unfortunately, what it's really doing is installing latest release of the library into the shade virtualenv that's used for functional testing and using tip of library for the OpenStack install. This is not a combo we care about. Instead, install the library tips into the virtualenv. To do this, make a functional-tips venv for tox, and make the post_test_hook optionally use it. Change-Id: Ibac2982e88439362c7af933c3a07c4d581ee6f2b --- extras/install-tips.sh | 34 +++++++++++++++++++ .../tests/functional/hooks/post_test_hook.sh | 7 +++- tox.ini | 9 +++++ 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 extras/install-tips.sh diff --git a/extras/install-tips.sh b/extras/install-tips.sh new file mode 100644 index 000000000..b450e9124 --- /dev/null +++ b/extras/install-tips.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Copyright (c) 2017 Red Hat, 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. + +for lib in \ + os-client-config \ + keystoneauth \ + python-novaclient \ + python-keystoneclient \ + python-cinderclient \ + python-neutronclient \ + python-troveclient \ + python-ironicclient \ + python-heatclient \ + python-designateclient \ + python-magnumclient +do + egg=$(echo $lib | tr '-' '_' | sed 's/python-//') + if [ -d /opt/stack/new/$lib ] ; then + pip install -q -U -e "git+file:///opt/stack/new/$lib#egg=$egg" + fi +done diff --git a/shade/tests/functional/hooks/post_test_hook.sh b/shade/tests/functional/hooks/post_test_hook.sh index 41bdd6f21..22ae99ec4 100755 --- a/shade/tests/functional/hooks/post_test_hook.sh +++ b/shade/tests/functional/hooks/post_test_hook.sh @@ -36,9 +36,14 @@ then sudo sed -ie '/^.*domain_id.*$/d' $CLOUDS_YAML fi +if [ "x$1" = "xtips" ] ; then + tox_env=functional-tips +else + tox_env=functional +fi echo "Running shade functional test suite" set +e -sudo -E -H -u jenkins tox -efunctional +sudo -E -H -u jenkins tox -e$tox_env EXIT_CODE=$? sudo testr last --subunit > $WORKSPACE/tempest.subunit set -e diff --git a/tox.ini b/tox.ini index d10bd9f8e..94b0bec65 100644 --- a/tox.ini +++ b/tox.ini @@ -35,6 +35,15 @@ setenv = passenv = OS_* SHADE_* commands = python setup.py testr --slowest --testr-args='--concurrency=1 {posargs}' +[testenv:functional-tips] +setenv = + OS_TEST_PATH = ./shade/tests/functional +passenv = OS_* SHADE_* +whitelist_externals = bash +commands = + bash extras/install-tips.sh + python setup.py testr --slowest --testr-args='--concurrency=1 {posargs}' + [testenv:pep8] commands = flake8 shade From 96f025afce7561e05eb19928c845c25918e4391b Mon Sep 17 00:00:00 2001 From: Ankur Gupta Date: Wed, 12 Oct 2016 22:38:39 -0500 Subject: [PATCH 1204/3836] Network L3 Router Commands SDK implementation of L3 Agent commands which would allow for list, add, remove of routers to L3 Agents. Partially Implements: blueprint network-l3-commands Change-Id: Iab11cfae17153a64f09590b6577c3c800dec0266 --- openstack/network/v2/_proxy.py | 56 +++++++++++++++++ openstack/network/v2/agent.py | 28 +++++++++ openstack/network/v2/router.py | 17 +++++ .../v2/test_agent_add_remove_router.py | 63 +++++++++++++++++++ openstack/tests/unit/network/v2/test_agent.py | 43 +++++++++++++ openstack/tests/unit/network/v2/test_proxy.py | 19 ++++++ .../tests/unit/network/v2/test_router.py | 16 +++++ 7 files changed, 242 insertions(+) create mode 100644 openstack/tests/functional/network/v2/test_agent_add_remove_router.py diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 1b32a27b8..8640f8129 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -2255,6 +2255,62 @@ def remove_gateway_from_router(self, router, **body): """ return router.remove_gateway(self.session, **body) + def routers_hosting_l3_agents(self, router, **query): + """Return a generator of L3 agent hosting a router + + :param router: Either the router id or an instance of + :class:`~openstack.network.v2.router.Router` + :param kwargs \*\*query: Optional query parameters to be sent to limit + the resources returned + + :returns: A generator of Router L3 Agents + :rtype: :class:`~openstack.network.v2.router.RouterL3Agents` + """ + router = self._get_resource(_router.Router, router) + return self._list(_agent.RouterL3Agent, paginated=False, + router_id=router.id, **query) + + def agent_hosted_routers(self, agent, **query): + """Return a generator of routers hosted by a L3 agent + + :param agent: Either the agent id of an instance of + :class:`~openstack.network.v2.network_agent.Agent` + :param kwargs \*\*query: Optional query parameters to be sent to limit + the resources returned + + :returns: A generator of routers + :rtype: :class:`~openstack.network.v2.agent.L3AgentRouters` + """ + agent = self._get_resource(_agent.Agent, agent) + return self._list(_router.L3AgentRouter, paginated=False, + agent_id=agent.id, **query) + + def add_router_to_agent(self, agent, router): + """Add router to L3 agent + + :param agent: Either the id of an agent + :class:`~openstack.network.v2.agent.Agent` instance + :param router: A router instance + :returns: Agent with attached router + :rtype: :class:`~openstack.network.v2.agent.Agent` + """ + agent = self._get_resource(_agent.Agent, agent) + router = self._get_resource(_router.Router, router) + return agent.add_router_to_agent(self.session, router.id) + + def remove_router_from_agent(self, agent, router): + """Remove router from L3 agent + + :param agent: Either the id of an agent or an + :class:`~openstack.network.v2.agent.Agent` instance + :param router: A router instance + :returns: Agent with removed router + :rtype: :class:`~openstack.network.v2.agent.Agent` + """ + agent = self._get_resource(_agent.Agent, agent) + router = self._get_resource(_router.Router, router) + return agent.remove_router_from_agent(self.session, router.id) + def create_security_group(self, **attrs): """Create a new security group from attributes diff --git a/openstack/network/v2/agent.py b/openstack/network/v2/agent.py index 549456afa..e31123a5b 100644 --- a/openstack/network/v2/agent.py +++ b/openstack/network/v2/agent.py @@ -75,6 +75,17 @@ def remove_agent_from_network(self, session, **body): network_id) session.delete(url, endpoint_filter=self.service, json=body) + def add_router_to_agent(self, session, router): + body = {'router_id': router} + url = utils.urljoin(self.base_path, self.id, 'l3-routers') + resp = session.post(url, endpoint_filter=self.service, json=body) + return resp.json() + + def remove_router_from_agent(self, session, router): + body = {'router_id': router} + url = utils.urljoin(self.base_path, self.id, 'l3-routers', router) + session.delete(url, endpoint_filter=self.service, json=body) + class NetworkHostingDHCPAgent(Agent): resource_key = 'agent' @@ -91,3 +102,20 @@ class NetworkHostingDHCPAgent(Agent): allow_list = True # NOTE: Doesn't support query yet. + + +class RouterL3Agent(Agent): + resource_key = 'agent' + resources_key = 'agents' + base_path = '/routers/%(router_id)s/l3-agents' + resource_name = 'l3-agent' + service = network_service.NetworkService() + + # capabilities + allow_create = False + allow_retrieve = True + allow_update = False + allow_delete = False + allow_list = True + + # NOTE: No query parameter is supported diff --git a/openstack/network/v2/router.py b/openstack/network/v2/router.py index 5043ce13a..89539110f 100644 --- a/openstack/network/v2/router.py +++ b/openstack/network/v2/router.py @@ -128,3 +128,20 @@ def remove_gateway(self, session, **body): 'remove_gateway_router') resp = session.put(url, endpoint_filter=self.service, json=body) return resp.json() + + +class L3AgentRouter(Router): + resource_key = 'router' + resources_key = 'routers' + base_path = '/agents/%(agent_id)s/l3-routers' + resource_name = 'l3-router' + service = network_service.NetworkService() + + # capabilities + allow_create = False + allow_retrieve = True + allow_update = False + allow_delete = False + allow_list = True + +# NOTE: No query parameter is supported diff --git a/openstack/tests/functional/network/v2/test_agent_add_remove_router.py b/openstack/tests/functional/network/v2/test_agent_add_remove_router.py new file mode 100644 index 000000000..4f4f4c669 --- /dev/null +++ b/openstack/tests/functional/network/v2/test_agent_add_remove_router.py @@ -0,0 +1,63 @@ +# 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 uuid + +from openstack.network.v2 import router +from openstack.tests.functional import base + + +class TestAgentRouters(base.BaseFunctionalTest): + + ROUTER_NAME = 'router-name-' + uuid.uuid4().hex + ROUTER_ID = None + AGENT = None + AGENT_ID = None + + @classmethod + def setUpClass(cls): + super(TestAgentRouters, cls).setUpClass() + + rot = cls.conn.network.create_router(name=cls.ROUTER_NAME) + assert isinstance(rot, router.Router) + cls.ROUTER_ID = rot.id + agent_list = list(cls.conn.network.agents()) + agents = [agent for agent in agent_list + if agent.agent_type == 'L3 agent'] + cls.AGENT = agents[0] + cls.AGENT_ID = cls.AGENT.id + + @classmethod + def tearDownClass(cls): + rot = cls.conn.network.delete_router(cls.ROUTER_ID, + ignore_missing=False) + cls.assertIs(None, rot) + + def test_add_router_to_agent(self): + sot = self.AGENT.add_router_to_agent(self.conn.session, + router_id=self.ROUTER_ID) + self._verify_add(sot) + + def test_remove_router_from_agent(self): + sot = self.AGENT.remove_router_from_agent(self.conn.session, + router_id=self.ROUTER_ID) + self._verify_remove(sot) + + def _verify_add(self, sot): + rots = self.conn.network.agent_hosted_routers(self.AGENT_ID) + routers = [router.id for router in rots] + self.assertIn(self.ROUTER_ID, routers) + + def _verify_remove(self, sot): + rots = self.conn.network.agent_hosted_routers(self.AGENT_ID) + routers = [router.id for router in rots] + self.assertNotIn(self.ROUTER_ID, routers) diff --git a/openstack/tests/unit/network/v2/test_agent.py b/openstack/tests/unit/network/v2/test_agent.py index 003520b7a..0ab82ebae 100644 --- a/openstack/tests/unit/network/v2/test_agent.py +++ b/openstack/tests/unit/network/v2/test_agent.py @@ -89,6 +89,33 @@ def test_remove_agent_from_network(self): sess.delete.assert_called_with('agents/IDENTIFIER/dhcp-networks/', endpoint_filter=net.service, json=body) + def test_add_router_to_agent(self): + # Add router to agent + sot = agent.Agent(**EXAMPLE) + response = mock.Mock() + response.body = {'router_id': '1'} + response.json = mock.Mock(return_value=response.body) + sess = mock.Mock() + sess.post = mock.Mock(return_value=response) + router_id = '1' + self.assertEqual(response.body, + sot.add_router_to_agent(sess, router_id)) + body = {'router_id': router_id} + url = 'agents/IDENTIFIER/l3-routers' + sess.post.assert_called_with(url, endpoint_filter=sot.service, + json=body) + + def test_remove_router_from_agent(self): + # Remove router from agent + sot = agent.Agent(**EXAMPLE) + sess = mock.Mock() + router_id = {} + self.assertIsNone(sot.remove_router_from_agent(sess, router_id)) + body = {'router_id': {}} + + sess.delete.assert_called_with('agents/IDENTIFIER/l3-routers/', + endpoint_filter=sot.service, json=body) + class TestNetworkHostingDHCPAgent(testtools.TestCase): @@ -104,3 +131,19 @@ def test_basic(self): self.assertFalse(net.allow_update) self.assertFalse(net.allow_delete) self.assertTrue(net.allow_list) + + +class TestRouterL3Agent(testtools.TestCase): + + def test_basic(self): + sot = agent.RouterL3Agent() + self.assertEqual('agent', sot.resource_key) + self.assertEqual('agents', sot.resources_key) + self.assertEqual('/routers/%(router_id)s/l3-agents', sot.base_path) + self.assertEqual('l3-agent', sot.resource_name) + self.assertEqual('network', sot.service.service_type) + self.assertFalse(sot.allow_create) + self.assertTrue(sot.allow_retrieve) + self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_delete) + self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index c01e79be1..7779ea27a 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -55,6 +55,7 @@ QOS_RULE_ID = 'qos-rule-id-' + uuid.uuid4().hex NETWORK_ID = 'network-id-' + uuid.uuid4().hex AGENT_ID = 'agent-id-' + uuid.uuid4().hex +ROUTER_ID = 'router-id-' + uuid.uuid4().hex class TestNetworkProxy(test_proxy_base2.TestProxyBase): @@ -739,6 +740,24 @@ def test_routers(self): def test_router_update(self): self.verify_update(self.proxy.update_router, router.Router) + def test_router_hosting_l3_agents_list(self): + self.verify_list( + self.proxy.routers_hosting_l3_agents, + agent.RouterL3Agent, + paginated=False, + method_kwargs={'router': ROUTER_ID}, + expected_kwargs={'router_id': ROUTER_ID}, + ) + + def test_agent_hosted_routers_list(self): + self.verify_list( + self.proxy.agent_hosted_routers, + router.L3AgentRouter, + paginated=False, + method_kwargs={'agent': AGENT_ID}, + expected_kwargs={'agent_id': AGENT_ID}, + ) + def test_security_group_create_attrs(self): self.verify_create(self.proxy.create_security_group, security_group.SecurityGroup) diff --git a/openstack/tests/unit/network/v2/test_router.py b/openstack/tests/unit/network/v2/test_router.py index 900f144ef..5fb87f16c 100644 --- a/openstack/tests/unit/network/v2/test_router.py +++ b/openstack/tests/unit/network/v2/test_router.py @@ -203,3 +203,19 @@ def test_remove_router_gateway(self): url = 'routers/IDENTIFIER/remove_gateway_router' sess.put.assert_called_with(url, endpoint_filter=sot.service, json=body) + + +class TestL3AgentRouters(testtools.TestCase): + + def test_basic(self): + sot = router.L3AgentRouter() + self.assertEqual('router', sot.resource_key) + self.assertEqual('routers', sot.resources_key) + self.assertEqual('/agents/%(agent_id)s/l3-routers', sot.base_path) + self.assertEqual('l3-router', sot.resource_name) + self.assertEqual('network', sot.service.service_type) + self.assertFalse(sot.allow_create) + self.assertTrue(sot.allow_retrieve) + self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_delete) + self.assertTrue(sot.allow_list) From 08b7ce9331a32731533cc5158d3fa3d3bf03a0d3 Mon Sep 17 00:00:00 2001 From: Andy Botting Date: Thu, 19 Jan 2017 15:30:25 +1100 Subject: [PATCH 1205/3836] Fix typo for baremetal_service_type Fix a copy-and-paste error for the baremetal service. Change-Id: Ifbef9d0ad01c57bd98f06f7f10f9d632753d8221 --- os_client_config/vendor-schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/os_client_config/vendor-schema.json b/os_client_config/vendor-schema.json index 6a6f4561e..692b79c51 100644 --- a/os_client_config/vendor-schema.json +++ b/os_client_config/vendor-schema.json @@ -157,7 +157,7 @@ "description": "Object Storage API Service Type", "type": "string" }, - "baremetal_api_version": { + "baremetal_service_type": { "name": "Baremetal API Service Type", "description": "Baremetal API Service Type", "type": "string" From 362af901d00b8e46ef734a9e33e29f831fbc4a24 Mon Sep 17 00:00:00 2001 From: Roberto Polli Date: Thu, 19 Jan 2017 09:21:20 +0100 Subject: [PATCH 1206/3836] Fix spin-lock behavior in _iterate_timeout. Change-Id: I95dc8f1500c954951afaf4d5c7d3b4094010d536 --- shade/_utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shade/_utils.py b/shade/_utils.py index 9b2e94003..7ab9bfe29 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -56,6 +56,9 @@ def _iterate_timeout(timeout, message, wait=2): # seems friendlier if wait is None: wait = 2 + elif wait == 0: + # wait should be < timeout, unless timeout is None + wait = 0.1 if timeout is None else min(0.1, timeout) wait = float(wait) except ValueError: raise exc.OpenStackCloudException( From e681725fb8c06541467b571c4158766f1396fa0b Mon Sep 17 00:00:00 2001 From: Ankur Gupta Date: Fri, 20 Jan 2017 15:30:33 -0500 Subject: [PATCH 1207/3836] Modified DHCP/Network Resource As per comments on https://review.openstack.org/#/c/385728/19 Not building body dict in proxy. Change-Id: I13c7afb7c5ad8d448a679b6b63b4f91b613f4962 --- openstack/network/v2/_proxy.py | 7 ++----- openstack/network/v2/agent.py | 7 ++++--- openstack/tests/unit/network/v2/test_agent.py | 5 +++-- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 8640f8129..8dee56d1f 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -225,8 +225,7 @@ def add_dhcp_agent_to_network(self, agent, network): """ network = self._get_resource(_network.Network, network) agent = self._get_resource(_agent.Agent, agent) - body = {'network_id': network.id} - return agent.add_agent_to_network(self.session, **body) + return agent.add_agent_to_network(self.session, network.id) def remove_dhcp_agent_from_network(self, agent, network): """Remove a DHCP Agent from a network @@ -236,11 +235,9 @@ def remove_dhcp_agent_from_network(self, agent, network): :param network: Network instance :return: """ - # network_id = resource.Resource.get_id(network) network = self._get_resource(_network.Network, network) agent = self._get_resource(_agent.Agent, agent) - body = {'network_id': network.id} - return agent.remove_agent_from_network(self.session, **body) + return agent.remove_agent_from_network(self.session, network.id) def network_hosting_dhcp_agents(self, network, **query): """A generator of DHCP agents hosted on a network. diff --git a/openstack/network/v2/agent.py b/openstack/network/v2/agent.py index e31123a5b..0f0dac83b 100644 --- a/openstack/network/v2/agent.py +++ b/openstack/network/v2/agent.py @@ -64,13 +64,14 @@ class Agent(resource.Resource): #: The messaging queue topic the network agent subscribes to. topic = resource.Body('topic') - def add_agent_to_network(self, session, **body): + def add_agent_to_network(self, session, network_id): + body = {'network_id': network_id} url = utils.urljoin(self.base_path, self.id, 'dhcp-networks') resp = session.post(url, endpoint_filter=self.service, json=body) return resp.json() - def remove_agent_from_network(self, session, **body): - network_id = body.get('network_id') + def remove_agent_from_network(self, session, network_id): + body = {'network_id': network_id} url = utils.urljoin(self.base_path, self.id, 'dhcp-networks', network_id) session.delete(url, endpoint_filter=self.service, json=body) diff --git a/openstack/tests/unit/network/v2/test_agent.py b/openstack/tests/unit/network/v2/test_agent.py index 0ab82ebae..1980d4118 100644 --- a/openstack/tests/unit/network/v2/test_agent.py +++ b/openstack/tests/unit/network/v2/test_agent.py @@ -83,8 +83,9 @@ def test_remove_agent_from_network(self): # Remove agent from agent net = agent.Agent(**EXAMPLE) sess = mock.Mock() - self.assertIsNone(net.remove_agent_from_network(sess)) - body = {} + network_id = {} + self.assertIsNone(net.remove_agent_from_network(sess, network_id)) + body = {'network_id': {}} sess.delete.assert_called_with('agents/IDENTIFIER/dhcp-networks/', endpoint_filter=net.service, json=body) From b7ea6c7150cad5e17404edb5401d5eead827041d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 29 Dec 2016 07:37:06 -1000 Subject: [PATCH 1208/3836] Replace SwiftService with direct REST uploads SwiftService uploads large objects using a thread pool. (The pool defaults to 5 and we're not currently configuring it larger or smaller) Instead of using that, spin up upload threads on our own so that we can get rid of the swiftclient depend. A few notes: - We're using the new async feature of the Adapter wrapper, which rate limits at the _start_ of a REST call. This is sane as far as we can tell, but also might not be what someone is expecting. - We'll skip the thread pool uploader for objects that are smaller than the default max segment size. - In splitting the file into segments, we'd like to avoid reading all of the segments into RAM when we don't need to - so there is a file-like wrapper class which can be passed to requests. This implements a read-view of a portion of the file. In a pathological case, this could be slower due to disk seeking on the read side. However, let's go back and deal with buffering when we have a problem - I imagine that the REST upload will be the bottleneck long before the overhead of interleaved disk seeks will be. Change-Id: Id9258980d2e0782e4e3c0ac26c7f11dc4db80354 --- .../removed-swiftclient-aff22bfaeee5f59f.yaml | 5 + requirements.txt | 1 - shade/_adapter.py | 21 +- shade/_tasks.py | 5 - shade/_utils.py | 37 + shade/openstackcloud.py | 205 ++++- shade/task_manager.py | 31 + shade/tests/functional/test_object.py | 41 +- shade/tests/unit/base.py | 4 +- shade/tests/unit/test__utils.py | 27 + shade/tests/unit/test_image.py | 31 +- shade/tests/unit/test_object.py | 743 +++++++++++++++++- 12 files changed, 1067 insertions(+), 84 deletions(-) create mode 100644 releasenotes/notes/removed-swiftclient-aff22bfaeee5f59f.yaml diff --git a/releasenotes/notes/removed-swiftclient-aff22bfaeee5f59f.yaml b/releasenotes/notes/removed-swiftclient-aff22bfaeee5f59f.yaml new file mode 100644 index 000000000..4927c1e68 --- /dev/null +++ b/releasenotes/notes/removed-swiftclient-aff22bfaeee5f59f.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - Removed swiftclient as a dependency. All swift operations + are now performed with direct REST calls using keystoneauth + Adapter. diff --git a/requirements.txt b/requirements.txt index 000072795..c0187fe74 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,6 @@ python-cinderclient>=1.3.1 python-neutronclient>=2.3.10 python-troveclient>=1.2.0 python-ironicclient>=0.10.0 -python-swiftclient>=2.5.0 python-heatclient>=1.0.0 python-designateclient>=2.1.0 python-magnumclient>=2.1.0 diff --git a/shade/_adapter.py b/shade/_adapter.py index 5d91ef81a..e8549b5ea 100644 --- a/shade/_adapter.py +++ b/shade/_adapter.py @@ -91,7 +91,15 @@ def __init__(self, shade_logger, manager, *args, **kwargs): def _munch_response(self, response, result_key=None): exc.raise_from_response(response) # Glance image downloads just return the data in the body - if response.headers.get('Content-Type') == 'application/octet-stream': + if response.headers.get('Content-Type') in ( + 'text/plain', + 'application/octet-stream'): + return response + elif response.headers.get('X-Static-Large-Object'): + # Workaround what seems to be a bug in swift where SLO objects + # return Content-Type application/json but contain + # application/octet-stream + # Bug filed: https://bugs.launchpad.net/swift/+bug/1658295 return response else: if not response.content: @@ -100,12 +108,12 @@ def _munch_response(self, response, result_key=None): try: result_json = response.json() except Exception: - self.shade_logger.debug( + raise exc.OpenStackCloudHTTPError( "Problems decoding json from response." " Reponse: {code} {reason}".format( code=response.status_code, - reason=response.reason)) - raise + reason=response.reason), + response=response) request_id = response.headers.get('x-openstack-request-id') @@ -154,4 +162,7 @@ def main(self, client): return request_method(**self.args) response = self.manager.submit_task(RequestTask(**kwargs)) - return self._munch_response(response) + if run_async: + return response + else: + return self._munch_response(response) diff --git a/shade/_tasks.py b/shade/_tasks.py index b46eab4d8..cfb13cc87 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -493,11 +493,6 @@ def main(self, client): return client.nova_client.floating_ip_pools.list() -class ObjectCreate(task_manager.Task): - def main(self, client): - return client.swift_service.upload(**self.args) - - class SubnetCreate(task_manager.Task): def main(self, client): return client.neutron_client.create_subnet(**self.args) diff --git a/shade/_utils.py b/shade/_utils.py index 7ab9bfe29..bfcfbd416 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -616,3 +616,40 @@ def generate_patches_from_kwargs(operation, **kwargs): 'path': '/%s' % k} patches.append(patch) return patches + + +class FileSegment(object): + """File-like object to pass to requests.""" + + def __init__(self, filename, offset, length): + self.filename = filename + self.offset = offset + self.length = length + self.pos = 0 + self._file = open(filename, 'rb') + self.seek(0) + + def tell(self): + return self._file.tell() - self.offset + + def seek(self, offset, whence=0): + if whence == 0: + self._file.seek(self.offset + offset, whence) + elif whence == 1: + self._file.seek(offset, whence) + elif whence == 2: + self._file.seek(self.offset + self.length - offset, 0) + + def read(self, size=-1): + remaining = self.length - self.pos + if remaining <= 0: + return b'' + + to_read = remaining if size < 0 else min(size, remaining) + chunk = self._file.read(to_read) + self.pos += len(chunk) + + return chunk + + def reset(self): + self._file.seek(self.offset, 0) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index e695c33d7..549e848f4 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -10,6 +10,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import collections import functools import hashlib import ipaddress @@ -40,7 +41,6 @@ import neutronclient.neutron.client import novaclient.client import novaclient.exceptions as nova_exceptions -import swiftclient.service import troveclient.client import designateclient.client @@ -297,12 +297,6 @@ def invalidate(self): self._keystone_client = None self._neutron_client = None self._nova_client = None - self._swift_service = None - # Lock used to reset swift client. Since swift client does not - # support keystone sessions, we we have to make a new client - # in order to get new auth prior to operations, otherwise - # long-running sessions will fail. - self._swift_service_lock = threading.Lock() self._trove_client = None self._designate_client = None self._magnum_client = None @@ -1077,19 +1071,25 @@ def _get_swift_kwargs(self): @property def swift_service(self): - with self._swift_service_lock: - if self._swift_service is None: - with _utils.shade_exceptions("Error constructing " - "swift client"): - endpoint = self.get_session_endpoint( - service_key='object-store') - options = dict(os_auth_token=self.auth_token, - os_storage_url=endpoint, - os_region_name=self.region_name) - options.update(self._get_swift_kwargs()) - self._swift_service = swiftclient.service.SwiftService( - options=options) - return self._swift_service + warnings.warn( + 'Using shade to get a swift object is deprecated. If you' + ' need a raw swiftclient.service.SwiftService object, please use' + ' make_legacy_client in os-client-config instead') + try: + import swiftclient.service + except ImportError: + self.log.error( + 'swiftclient is no longer a dependency of shade. You need to' + ' install python-swiftclient directly.') + with _utils.shade_exceptions("Error constructing " + "swift client"): + endpoint = self.get_session_endpoint( + service_key='object-store') + options = dict(os_auth_token=self.auth_token, + os_storage_url=endpoint, + os_region_name=self.region_name) + options.update(self._get_swift_kwargs()) + return swiftclient.service.SwiftService(options=options) @property def cinder_client(self): @@ -3432,10 +3432,6 @@ def _upload_image_task( parameters = image_kwargs.pop('parameters', {}) image_kwargs.update(parameters) - # get new client sessions - with self._swift_service_lock: - self._swift_service = None - self.create_object( container, name, filename, md5=md5, sha256=sha256) @@ -5758,34 +5754,148 @@ def create_object( if not filename: filename = name + # segment_size gets used as a step value in a range call, so needs + # to be an int + if segment_size: + segment_size = int(segment_size) segment_size = self.get_object_segment_size(segment_size) + file_size = os.path.getsize(filename) if not (md5 or sha256): (md5, sha256) = self._get_file_hashes(filename) headers[OBJECT_MD5_KEY] = md5 or '' headers[OBJECT_SHA256_KEY] = sha256 or '' - header_list = sorted([':'.join([k, v]) for (k, v) in headers.items()]) for (k, v) in metadata.items(): - header_list.append(':'.join(['x-object-meta-' + k, v])) + headers['x-object-meta-' + k] = v # On some clouds this is not necessary. On others it is. I'm confused. self.create_container(container) if self.is_object_stale(container, name, filename, md5, sha256): + + endpoint = '{container}/{name}'.format( + container=container, name=name) self.log.debug( - "swift uploading %(filename)s to %(container)s/%(name)s", - {'filename': filename, 'container': container, 'name': name}) - upload = swiftclient.service.SwiftUploadObject( - source=filename, object_name=name) - for r in self.manager.submit_task(_tasks.ObjectCreate( - container=container, objects=[upload], - options=dict( - header=header_list, - segment_size=segment_size, - use_slo=use_slo))): - if not r['success']: - raise OpenStackCloudException( - 'Failed at action ({action}) [{error}]:'.format(**r)) + "swift uploading %(filename)s to %(endpoint)s", + {'filename': filename, 'endpoint': endpoint}) + + if file_size <= segment_size: + self._upload_object(endpoint, filename, headers) + else: + self._upload_large_object( + endpoint, filename, headers, + file_size, segment_size, use_slo) + + def _upload_object(self, endpoint, filename, headers): + return self._object_store_client.put( + endpoint, headers=headers, data=open(filename, 'r')) + + def _get_file_segments(self, endpoint, filename, file_size, segment_size): + # Use an ordered dict here so that testing can replicate things + segments = collections.OrderedDict() + for (index, offset) in enumerate(range(0, file_size, segment_size)): + remaining = file_size - (index * segment_size) + segment = _utils.FileSegment( + filename, offset, + segment_size if segment_size < remaining else remaining) + name = '{endpoint}/{index:0>6}'.format( + endpoint=endpoint, index=index) + segments[name] = segment + return segments + + def _object_name_from_url(self, url): + '''Get container_name/object_name from the full URL called. + + Remove the Swift endpoint from the front of the URL, and remove + the leaving / that will leave behind.''' + endpoint = self._object_store_client.get_endpoint() + object_name = url.replace(endpoint, '') + if object_name.startswith('/'): + object_name = object_name[1:] + return object_name + + def _add_etag_to_manifest(self, segment_results, manifest): + for result in segment_results: + if 'Etag' not in result.headers: + continue + name = self._object_name_from_url(result.url) + for entry in manifest: + if entry['path'] == '/{name}'.format(name=name): + entry['etag'] = result.headers['Etag'] + + def _upload_large_object( + self, endpoint, filename, headers, file_size, segment_size, use_slo): + # If the object is big, we need to break it up into segments that + # are no larger than segment_size, upload each of them individually + # and then upload a manifest object. The segments can be uploaded in + # parallel, so we'll use the async feature of the TaskManager. + + segment_futures = [] + segment_results = [] + retry_results = [] + retry_futures = [] + manifest = [] + + # Get an OrderedDict with keys being the swift location for the + # segment, the value a FileSegment file-like object that is a + # slice of the data for the segment. + segments = self._get_file_segments( + endpoint, filename, file_size, segment_size) + + # Schedule the segments for upload + for name, segment in segments.items(): + # Async call to put - schedules execution and returns a future + segment_future = self._object_store_client.put( + name, headers=headers, data=segment, run_async=True) + segment_futures.append(segment_future) + # TODO(mordred) Collect etags from results to add to this manifest + # dict. Then sort the list of dicts by path. + manifest.append(dict( + path='/{name}'.format(name=name), + size_bytes=segment.length)) + + # Try once and collect failed results to retry + segment_results, retry_results = task_manager.wait_for_futures( + segment_futures, raise_on_error=False) + + self._add_etag_to_manifest(segment_results, manifest) + + for result in retry_results: + # Grab the FileSegment for the failed upload so we can retry + name = self._object_name_from_url(result.url) + segment = segments[name] + segment.seek(0) + # Async call to put - schedules execution and returns a future + segment_future = self._object_store_client.put( + name, headers=headers, data=segment, run_async=True) + # TODO(mordred) Collect etags from results to add to this manifest + # dict. Then sort the list of dicts by path. + retry_futures.append(segment_future) + + # If any segments fail the second time, just throw the error + segment_results, retry_results = task_manager.wait_for_futures( + retry_futures, raise_on_error=True) + + self._add_etag_to_manifest(segment_results, manifest) + + if use_slo: + return self._finish_large_object_slo(endpoint, headers, manifest) + else: + return self._finish_large_object_dlo(endpoint, headers) + + def _finish_large_object_slo(self, endpoint, headers, manifest): + # TODO(mordred) send an etag of the manifest, which is the md5sum + # of the concatenation of the etags of the results + headers = headers.copy() + return self._object_store_client.put( + endpoint, + params={'multipart-manifest': 'put'}, + headers=headers, json=manifest) + + def _finish_large_object_dlo(self, endpoint, headers): + headers = headers.copy() + headers['X-Object-Manifest'] = endpoint + return self._object_store_client.put(endpoint, headers=headers) def update_object(self, container, name, metadata=None, **headers): """Update the metadata of an object @@ -5837,10 +5947,25 @@ def delete_object(self, container, name): :raises: OpenStackCloudException on operation error. """ + # TODO(mordred) DELETE for swift returns status in text/plain format + # like so: + # Number Deleted: 15 + # Number Not Found: 0 + # Response Body: + # Response Status: 200 OK + # Errors: + # We should ultimately do something with that try: + meta = self.get_object_metadata(container, name) + if not meta: + return False + params = {} + if meta.get('X-Static-Large-Object', None) == 'True': + params['multipart-manifest'] = 'delete' self._object_store_client.delete( '{container}/{object}'.format( - container=container, object=name)) + container=container, object=name), + params=params) return True except OpenStackCloudHTTPError: return False diff --git a/shade/task_manager.py b/shade/task_manager.py index 5144af01f..bfad5a241 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -26,6 +26,7 @@ import six from shade import _log +from shade import exc from shade import meta @@ -287,3 +288,33 @@ def submit_function( task_class = generate_task_class(method, name, result_filter_cb) return self._executor.submit_task(task_class(**kwargs)) + + +def wait_for_futures(futures, raise_on_error=True, log=None): + '''Collect results or failures from a list of running future tasks.''' + + results = [] + retries = [] + + # Check on each result as its thread finishes + for completed in concurrent.futures.as_completed(futures): + try: + result = completed.result() + # We have to do this here because munch_response doesn't + # get called on async job results + exc.raise_from_response(result) + results.append(result) + except (keystoneauth1.exceptions.RetriableConnectionFailure, + exc.OpenStackCloudException) as e: + if log: + log.debug( + "Exception processing async task: {e}".format( + e=str(e)), + exc_info=True) + # If we get an exception, put the result into a list so we + # can try again + if raise_on_error: + raise + else: + retries.append(result) + return results, retries diff --git a/shade/tests/functional/test_object.py b/shade/tests/functional/test_object.py index f6d5d9c16..f32802e74 100644 --- a/shade/tests/functional/test_object.py +++ b/shade/tests/functional/test_object.py @@ -17,10 +17,13 @@ Functional tests for `shade` object methods. """ +import random +import string import tempfile from testtools import content +from shade import exc from shade.tests.functional import base @@ -40,24 +43,29 @@ def test_create_object(self): self.assertEqual(container_name, self.demo_cloud.list_containers()[0]['name']) sizes = ( - (64 * 1024, 1), # 64K, one segment - (50 * 1024 ** 2, 5) # 50MB, 5 segments + (64 * 1024, 1), # 64K, one segment + (64 * 1024, 5) # 64MB, 5 segments ) for size, nseg in sizes: - segment_size = round(size / nseg) - with tempfile.NamedTemporaryFile() as sparse_file: - sparse_file.seek(size) - sparse_file.write("\0") - sparse_file.flush() + segment_size = int(round(size / nseg)) + with tempfile.NamedTemporaryFile() as fake_file: + fake_content = ''.join(random.SystemRandom().choice( + string.ascii_uppercase + string.digits) + for _ in range(size)).encode('latin-1') + + fake_file.write(fake_content) + fake_file.flush() name = 'test-%d' % size + self.addCleanup( + self.demo_cloud.delete_object, container_name, name) self.demo_cloud.create_object( container_name, name, - sparse_file.name, + fake_file.name, segment_size=segment_size, metadata={'foo': 'bar'}) self.assertFalse(self.demo_cloud.is_object_stale( container_name, name, - sparse_file.name + fake_file.name ) ) self.assertEqual( @@ -70,12 +78,21 @@ def test_create_object(self): 'testv', self.demo_cloud.get_object_metadata( container_name, name)['x-object-meta-testk'] ) - self.assertIsNotNone( - self.demo_cloud.get_object(container_name, name)) + try: + self.assertIsNotNone( + self.demo_cloud.get_object(container_name, name)) + except exc.OpenStackCloudException as e: + self.addDetail( + 'failed_response', + content.text_content(str(e.response.headers))) + self.addDetail( + 'failed_response', + content.text_content(e.response.text)) self.assertEqual( name, self.demo_cloud.list_objects(container_name)[0]['name']) - self.demo_cloud.delete_object(container_name, name) + self.assertTrue( + self.demo_cloud.delete_object(container_name, name)) self.assertEqual([], self.demo_cloud.list_objects(container_name)) self.assertEqual(container_name, self.demo_cloud.list_containers()[0]['name']) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 3bff06361..14683c19b 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -171,10 +171,12 @@ def use_glance(self, image_version_json='image-version.json'): dict(method='GET', url='https://image.example.com/'), ] - def assert_calls(self): + def assert_calls(self, stop_after=None): self.assertEqual(len(self.calls), len(self.adapter.request_history)) for (x, (call, history)) in enumerate( zip(self.calls, self.adapter.request_history)): + if stop_after and x > stop_after: + break self.assertEqual( call['method'], history.method, 'Method mismatch on call {index}'.format(index=x)) diff --git a/shade/tests/unit/test__utils.py b/shade/tests/unit/test__utils.py index 997ae874d..bfe566bd4 100644 --- a/shade/tests/unit/test__utils.py +++ b/shade/tests/unit/test__utils.py @@ -10,6 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. +import random +import string +import tempfile + import testtools from shade import _utils @@ -230,3 +234,26 @@ def test_range_filter_invalid_op(self): "Invalid range value: <>100" ): _utils.range_filter(RANGE_DATA, "key1", "<>100") + + def test_file_segment(self): + file_size = 4200 + content = ''.join(random.SystemRandom().choice( + string.ascii_uppercase + string.digits) + for _ in range(file_size)).encode('latin-1') + self.imagefile = tempfile.NamedTemporaryFile(delete=False) + self.imagefile.write(content) + self.imagefile.close() + + segments = self.cloud._get_file_segments( + endpoint='test_container/test_image', + filename=self.imagefile.name, + file_size=file_size, + segment_size=1000) + self.assertEqual(len(segments), 5) + segment_content = b'' + for (index, (name, segment)) in enumerate(segments.items()): + self.assertEqual( + 'test_container/test_image/{index:0>6}'.format(index=index), + name) + segment_content += segment.read() + self.assertEqual(content, segment_content) diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index cd0ae847e..43dab0302 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -215,9 +215,7 @@ def test_create_image_put_v2(self): self.assert_calls() self.assertEqual(self.adapter.request_history[5].text.read(), b'\x00') - @mock.patch.object(shade.OpenStackCloud, 'swift_service') - def test_create_image_task(self, - swift_service_mock): + def test_create_image_task(self): self.cloud.image_api_use_tasks = True image_name = 'name-99' container_name = 'image_upload_v2_test_container' @@ -262,6 +260,12 @@ def test_create_image_task(self, container=container_name, object=image_name), status_code=404) + self.adapter.put( + '{endpoint}/{container}/{object}'.format( + endpoint=endpoint, + container=container_name, object=image_name), + status_code=201) + task_id = str(uuid.uuid4()) args = dict( id=task_id, @@ -304,18 +308,6 @@ def test_create_image_task(self, image_name, self.imagefile.name, wait=True, timeout=1, is_public=False, container=container_name) - args = { - 'header': [ - 'x-object-meta-x-shade-md5:{md5}'.format(md5=NO_MD5), - 'x-object-meta-x-shade-sha256:{sha}'.format(sha=NO_SHA256), - ], - 'segment_size': 1000, - 'use_slo': True} - swift_service_mock.upload.assert_called_with( - container='image_upload_v2_test_container', - objects=mock.ANY, - options=args) - self.calls += [ dict(method='GET', url='https://image.example.com/v2/images'), dict(method='GET', url='https://object-store.example.com/info'), @@ -339,6 +331,15 @@ def test_create_image_task(self, url='{endpoint}/{container}/{object}'.format( endpoint=endpoint, container=container_name, object=image_name)), + dict( + method='PUT', + url='{endpoint}/{container}/{object}'.format( + endpoint=endpoint, + container=container_name, object=image_name), + headers={ + 'x-object-meta-x-shade-md5': NO_MD5, + 'x-object-meta-x-shade-sha256': NO_SHA256, + }), dict(method='GET', url='https://image.example.com/v2/images'), dict( method='POST', diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index 5d2ac6304..a99d53177 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -12,6 +12,8 @@ # License for the specific language governing permissions and limitations # under the License. +import tempfile + import testtools import shade @@ -20,10 +22,10 @@ from shade.tests.unit import base -class TestObject(base.RequestsMockTestCase): +class BaseTestObject(base.RequestsMockTestCase): def setUp(self): - super(TestObject, self).setUp() + super(BaseTestObject, self).setUp() self.container = self.getUniqueString() self.object = self.getUniqueString() @@ -33,6 +35,9 @@ def setUp(self): self.object_endpoint = '{endpoint}/{object}'.format( endpoint=self.container_endpoint, object=self.object) + +class TestObject(BaseTestObject): + def test_create_container(self): """Test creating a (private) container""" self.adapter.head( @@ -331,22 +336,31 @@ def test_list_objects_exception(self): self.cloud.list_objects, self.container) def test_delete_object(self): + self.adapter.head( + self.object_endpoint, headers={'X-Object-Meta': 'foo'}) self.adapter.delete(self.object_endpoint, status_code=204) self.assertTrue(self.cloud.delete_object(self.container, self.object)) self.calls += [ - dict(method='DELETE', url=self.object_endpoint), + dict( + method='HEAD', + url=self.object_endpoint), + dict( + method='DELETE', + url=self.object_endpoint), ] self.assert_calls() def test_delete_object_not_found(self): - self.adapter.delete(self.object_endpoint, status_code=404) + self.adapter.head(self.object_endpoint, status_code=404) self.assertFalse(self.cloud.delete_object(self.container, self.object)) self.calls += [ - dict(method='DELETE', url=self.object_endpoint), + dict( + method='HEAD', + url=self.object_endpoint), ] self.assert_calls() @@ -439,3 +453,722 @@ def test_get_object_segment_size_http_412(self): reason='Precondition failed') self.assertEqual(shade.openstackcloud.DEFAULT_OBJECT_SEGMENT_SIZE, self.cloud.get_object_segment_size(None)) + + +class TestObjectUploads(BaseTestObject): + + def setUp(self): + super(TestObjectUploads, self).setUp() + + self.content = self.getUniqueString().encode('latin-1') + self.object_file = tempfile.NamedTemporaryFile(delete=False) + self.object_file.write(self.content) + self.object_file.close() + (self.md5, self.sha256) = self.cloud._get_file_hashes( + self.object_file.name) + self.endpoint = self.cloud._object_store_client.get_endpoint() + + def test_create_object(self): + + self.adapter.get( + 'https://object-store.example.com/info', + json=dict( + swift={'max_file_size': 1000}, + slo={'min_segment_size': 500})) + + self.adapter.put( + '{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container,), + status_code=201, + headers={ + 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=UTF-8', + }) + self.adapter.head( + '{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container), + [ + dict(status_code=404), + dict(headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}), + ]) + self.adapter.head( + '{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=404) + + self.adapter.put( + '{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=201) + + self.cloud.create_object( + container=self.container, name=self.object, + filename=self.object_file.name) + + self.calls += [ + dict(method='GET', url='https://object-store.example.com/info'), + dict( + method='HEAD', + url='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container)), + dict( + method='PUT', + url='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container)), + dict( + method='HEAD', + url='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container)), + dict( + method='HEAD', + url='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object)), + dict( + method='PUT', + url='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + headers={ + 'x-object-meta-x-shade-md5': self.md5, + 'x-object-meta-x-shade-sha256': self.sha256, + }), + ] + + self.assert_calls() + + def test_create_dynamic_large_object(self): + + max_file_size = 2 + min_file_size = 1 + + self.adapter.get( + 'https://object-store.example.com/info', + json=dict( + swift={'max_file_size': max_file_size}, + slo={'min_segment_size': min_file_size})) + + self.adapter.put( + '{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container,), + status_code=201, + headers={ + 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=UTF-8', + }) + self.adapter.head( + '{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container), + [ + dict(status_code=404), + dict(headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}), + ]) + self.adapter.head( + '{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=404) + + self.adapter.put( + '{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=201) + + self.calls += [ + dict(method='GET', url='https://object-store.example.com/info'), + dict( + method='HEAD', + url='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container)), + dict( + method='PUT', + url='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container)), + dict( + method='HEAD', + url='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container)), + dict( + method='HEAD', + url='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object)), + ] + + for index, offset in enumerate( + range(0, len(self.content), max_file_size)): + + self.adapter.put( + '{endpoint}/{container}/{object}/{index:0>6}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + index=index), + status_code=201) + + self.calls += [ + dict( + method='PUT', + url='{endpoint}/{container}/{object}/{index:0>6}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + index=index))] + + self.calls += [ + dict( + method='PUT', + url='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + headers={ + 'x-object-manifest': '{container}/{object}'.format( + container=self.container, object=self.object), + 'x-object-meta-x-shade-md5': self.md5, + 'x-object-meta-x-shade-sha256': self.sha256, + }), + ] + + self.cloud.create_object( + container=self.container, name=self.object, + filename=self.object_file.name, use_slo=False) + + # After call 6, order become indeterminate because of thread pool + self.assert_calls(stop_after=6) + + for key, value in self.calls[-1]['headers'].items(): + self.assertEqual( + value, self.adapter.request_history[-1].headers[key], + 'header mismatch in manifest call') + + def test_create_static_large_object(self): + + max_file_size = 25 + min_file_size = 1 + + self.adapter.get( + 'https://object-store.example.com/info', + json=dict( + swift={'max_file_size': max_file_size}, + slo={'min_segment_size': min_file_size})) + + self.adapter.put( + '{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container,), + status_code=201, + headers={ + 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=UTF-8', + }) + self.adapter.head( + '{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container), + [ + dict(status_code=404), + dict(headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}), + ]) + self.adapter.head( + '{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=404) + + self.adapter.put( + '{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=201) + + self.calls += [ + dict(method='GET', url='https://object-store.example.com/info'), + dict( + method='HEAD', + url='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container)), + dict( + method='PUT', + url='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container)), + dict( + method='HEAD', + url='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container)), + dict( + method='HEAD', + url='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object)), + ] + + for index, offset in enumerate( + range(0, len(self.content), max_file_size)): + + self.adapter.put( + '{endpoint}/{container}/{object}/{index:0>6}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + index=index), + status_code=201, + headers=dict(Etag='etag{index}'.format(index=index))) + + self.calls += [ + dict( + method='PUT', + url='{endpoint}/{container}/{object}/{index:0>6}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + index=index))] + + self.calls += [ + dict( + method='PUT', + url='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + params={ + 'multipart-manifest', 'put' + }, + headers={ + 'x-object-meta-x-shade-md5': self.md5, + 'x-object-meta-x-shade-sha256': self.sha256, + }), + ] + + self.cloud.create_object( + container=self.container, name=self.object, + filename=self.object_file.name, use_slo=True) + + # After call 6, order become indeterminate because of thread pool + self.assert_calls(stop_after=6) + + for key, value in self.calls[-1]['headers'].items(): + self.assertEqual( + value, self.adapter.request_history[-1].headers[key], + 'header mismatch in manifest call') + + base_object = '/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object) + + self.assertEqual([ + { + 'path': "{base_object}/000000".format( + base_object=base_object), + 'size_bytes': 25, + 'etag': 'etag0', + }, + { + 'path': "{base_object}/000001".format( + base_object=base_object), + 'size_bytes': 25, + 'etag': 'etag1', + }, + { + 'path': "{base_object}/000002".format( + base_object=base_object), + 'size_bytes': 25, + 'etag': 'etag2', + }, + { + 'path': "{base_object}/000003".format( + base_object=base_object), + 'size_bytes': 5, + 'etag': 'etag3', + }, + ], self.adapter.request_history[-1].json()) + + def test_object_segment_retry_failure(self): + + max_file_size = 25 + min_file_size = 1 + + self.adapter.get( + 'https://object-store.example.com/info', + json=dict( + swift={'max_file_size': max_file_size}, + slo={'min_segment_size': min_file_size})) + + self.adapter.put( + '{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container,), + status_code=201, + headers={ + 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=UTF-8', + }) + self.adapter.head( + '{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container), + [ + dict(status_code=404), + dict(headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}), + ]) + self.adapter.head( + '{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=404) + + self.adapter.put( + '{endpoint}/{container}/{object}/000000'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object), + status_code=201) + + self.adapter.put( + '{endpoint}/{container}/{object}/000001'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object), + status_code=201) + + self.adapter.put( + '{endpoint}/{container}/{object}/000002'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object), + status_code=201) + + self.adapter.put( + '{endpoint}/{container}/{object}/000003'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object), + status_code=501) + + self.adapter.put( + '{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=201) + + self.calls += [ + dict(method='GET', url='https://object-store.example.com/info'), + dict( + method='HEAD', + url='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container)), + dict( + method='PUT', + url='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container)), + dict( + method='HEAD', + url='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container)), + dict( + method='HEAD', + url='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object)), + + dict( + method='PUT', + url='{endpoint}/{container}/{object}/000000'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object)), + + dict( + method='PUT', + url='{endpoint}/{container}/{object}/000001'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object)), + + dict( + method='PUT', + url='{endpoint}/{container}/{object}/000002'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object)), + + dict( + method='PUT', + url='{endpoint}/{container}/{object}/000003'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object)), + + dict( + method='PUT', + url='{endpoint}/{container}/{object}/000003'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object)), + ] + + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.create_object, + container=self.container, name=self.object, + filename=self.object_file.name, use_slo=True) + + # After call 6, order become indeterminate because of thread pool + self.assert_calls(stop_after=6) + + def test_object_segment_retries(self): + + max_file_size = 25 + min_file_size = 1 + + self.adapter.get( + 'https://object-store.example.com/info', + json=dict( + swift={'max_file_size': max_file_size}, + slo={'min_segment_size': min_file_size})) + + self.adapter.put( + '{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container,), + status_code=201, + headers={ + 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=UTF-8', + }) + self.adapter.head( + '{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container), + [ + dict(status_code=404), + dict(headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}), + ]) + self.adapter.head( + '{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=404) + + self.adapter.put( + '{endpoint}/{container}/{object}/000000'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object), + headers={'etag': 'etag0'}, + status_code=201) + + self.adapter.put( + '{endpoint}/{container}/{object}/000001'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object), + headers={'etag': 'etag1'}, + status_code=201) + + self.adapter.put( + '{endpoint}/{container}/{object}/000002'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object), + headers={'etag': 'etag2'}, + status_code=201) + + self.adapter.put( + '{endpoint}/{container}/{object}/000003'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object), [ + dict(status_code=501), + dict(status_code=201, headers={'etag': 'etag3'}), + ]) + + self.adapter.put( + '{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=201) + + self.calls += [ + dict(method='GET', url='https://object-store.example.com/info'), + dict( + method='HEAD', + url='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container)), + dict( + method='PUT', + url='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container)), + dict( + method='HEAD', + url='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container)), + dict( + method='HEAD', + url='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object)), + + dict( + method='PUT', + url='{endpoint}/{container}/{object}/000000'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object)), + + dict( + method='PUT', + url='{endpoint}/{container}/{object}/000001'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object)), + + dict( + method='PUT', + url='{endpoint}/{container}/{object}/000002'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object)), + + dict( + method='PUT', + url='{endpoint}/{container}/{object}/000003'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object)), + + dict( + method='PUT', + url='{endpoint}/{container}/{object}/000003'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object)), + + dict( + method='PUT', + url='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + params={ + 'multipart-manifest', 'put' + }, + headers={ + 'x-object-meta-x-shade-md5': self.md5, + 'x-object-meta-x-shade-sha256': self.sha256, + }), + ] + + self.cloud.create_object( + container=self.container, name=self.object, + filename=self.object_file.name, use_slo=True) + + # After call 6, order become indeterminate because of thread pool + self.assert_calls(stop_after=6) + + for key, value in self.calls[-1]['headers'].items(): + self.assertEqual( + value, self.adapter.request_history[-1].headers[key], + 'header mismatch in manifest call') + + base_object = '/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object) + + self.assertEqual([ + { + 'path': "{base_object}/000000".format( + base_object=base_object), + 'size_bytes': 25, + 'etag': 'etag0', + }, + { + 'path': "{base_object}/000001".format( + base_object=base_object), + 'size_bytes': 25, + 'etag': 'etag1', + }, + { + 'path': "{base_object}/000002".format( + base_object=base_object), + 'size_bytes': 25, + 'etag': 'etag2', + }, + { + 'path': "{base_object}/000003".format( + base_object=base_object), + 'size_bytes': 1, + 'etag': 'etag3', + }, + ], self.adapter.request_history[-1].json()) From b08629040eecfa15aec8a02eab9d21a956b47425 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 19 Jan 2017 09:00:30 -0600 Subject: [PATCH 1209/3836] Add raw client constructors for all the things We want to start taking new functionality in with raw REST. However, before we point people at doing that, we kind of need for there to be clients people can use to make REST calls. Go ahead and add everything os-client-config has default api versions for. Note - we could almost certainly get fancy with some sort of metaprogramming to avoid the copy-pasta here - but I do not think the complexity is worth it - it's not that much pasta. Change-Id: I4691bc7c4943136b1e3fb9a9a8981e682b42ec8a --- shade/openstackcloud.py | 68 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 5 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 549e848f4..902881382 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -368,18 +368,50 @@ def _get_raw_client(self, service_key): region_name=self.cloud_config.region, shade_logger=self.log) + @property + def _application_catalog_client(self): + if 'application-catalog' not in self._raw_clients: + self._raw_clients['application-catalog'] = self._get_raw_client( + 'application-catalog') + return self._raw_clients['application-catalog'] + + @property + def _baremetal_client(self): + if 'baremetal' not in self._raw_clients: + self._raw_clients['baremetal'] = self._get_raw_client('baremetal') + return self._raw_clients['baremetal'] + + @property + def _container_infra_client(self): + if 'container-infra' not in self._raw_clients: + self._raw_clients['container-infra'] = self._get_raw_client( + 'container-infra') + return self._raw_clients['container-infra'] + @property def _compute_client(self): + # TODO(mordred) Deal with microversions if 'compute' not in self._raw_clients: self._raw_clients['compute'] = self._get_raw_client('compute') return self._raw_clients['compute'] @property - def _object_store_client(self): - if 'object-store' not in self._raw_clients: - raw_client = self._get_raw_client('object-store') - self._raw_clients['object-store'] = raw_client - return self._raw_clients['object-store'] + def _database_client(self): + if 'database' not in self._raw_clients: + self._raw_clients['database'] = self._get_raw_client('database') + return self._raw_clients['database'] + + @property + def _dns_client(self): + if 'dns' not in self._raw_clients: + self._raw_clients['dns'] = self._get_raw_client('dns') + return self._raw_clients['dns'] + + @property + def _identity_client(self): + if 'identity' not in self._raw_clients: + self._raw_clients['identity'] = self._get_raw_client('identity') + return self._raw_clients['identity'] @property def _raw_image_client(self): @@ -459,6 +491,32 @@ def _image_client(self): self._raw_clients['image'] = image_client return self._raw_clients['image'] + @property + def _network_client(self): + if 'network' not in self._raw_clients: + self._raw_clients['network'] = self._get_raw_client('network') + return self._raw_clients['network'] + + @property + def _object_store_client(self): + if 'object-store' not in self._raw_clients: + raw_client = self._get_raw_client('object-store') + self._raw_clients['object-store'] = raw_client + return self._raw_clients['object-store'] + + @property + def _orchestration_client(self): + if 'orchestration' not in self._raw_clients: + raw_client = self._get_raw_client('orchestration') + self._raw_clients['orchestration'] = raw_client + return self._raw_clients['orchestration'] + + @property + def _volume_client(self): + if 'volume' not in self._raw_clients: + self._raw_clients['volume'] = self._get_raw_client('volume') + return self._raw_clients['volume'] + @property def nova_client(self): if self._nova_client is None: From 8c0c2db7fc75c05258d80788f29ba6cccba84fa0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 21 Jan 2017 07:58:26 +0100 Subject: [PATCH 1210/3836] Do neutron version discovery and change one test Make the neutron client perform version discovery. (Note: do we really want to do this and not just cheat and append a v2.0 to the URL?) More importantly, convert one of the unit tests. Note that neutronclient doesn't seem to do version discovery itself. Change-Id: I57a78736e48eae52f51f4d9315e117de861240e8 --- shade/openstackcloud.py | 23 ++++++++++++++++++- shade/tests/unit/base.py | 10 ++++++++ shade/tests/unit/fixtures/catalog-v2.json | 14 +++++++++++ shade/tests/unit/fixtures/catalog-v3.json | 13 +++++++++++ .../tests/unit/fixtures/network-version.json | 14 +++++++++++ shade/tests/unit/test_floating_ip_neutron.py | 20 ++++++++++------ 6 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 shade/tests/unit/fixtures/network-version.json diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 902881382..87f41c09a 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -420,6 +420,25 @@ def _raw_image_client(self): self._raw_clients['raw-image'] = image_client return self._raw_clients['raw-image'] + def _discover_latest_version(self, client): + # Used to get the versioned endpoint for a service with one version + try: + # Version discovery + versions = client.get('/') + api_version = [ + version for version in versions + if version['status'] == 'CURRENT'][0] + return api_version['links'][0]['href'] + except (keystoneauth1.exceptions.connection.ConnectFailure, + OpenStackCloudURINotFound) as e: + # A 404 or a connection error is a likely thing to get + # either with a misconfgured glance. or we've already + # gotten a versioned endpoint from the catalog + self.log.debug( + "Version discovery failed, assuming endpoint in" + " the catalog is already versioned. {e}".format(e=str(e))) + return image_client.get_endpoint() + def _discover_image_endpoint(self, config_version, image_client): try: # Version discovery @@ -494,7 +513,9 @@ def _image_client(self): @property def _network_client(self): if 'network' not in self._raw_clients: - self._raw_clients['network'] = self._get_raw_client('network') + client = self._get_raw_client('network') + client.endpoint_override = self._discover_latest_version(client) + self._raw_clients['network'] = client return self._raw_clients['network'] @property diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 14683c19b..98328b89f 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -171,6 +171,16 @@ def use_glance(self, image_version_json='image-version.json'): dict(method='GET', url='https://image.example.com/'), ] + def use_neutron(self, network_version_json='network-version.json'): + discovery_fixture = os.path.join( + self.fixtures_directory, network_version_json) + self.adapter.get( + 'https://network.example.com/', + text=open(discovery_fixture, 'r').read()) + self.calls += [ + dict(method='GET', url='https://network.example.com/'), + ] + def assert_calls(self, stop_after=None): self.assertEqual(len(self.calls), len(self.adapter.request_history)) for (x, (call, history)) in enumerate( diff --git a/shade/tests/unit/fixtures/catalog-v2.json b/shade/tests/unit/fixtures/catalog-v2.json index b9a589eb6..5130acd36 100644 --- a/shade/tests/unit/fixtures/catalog-v2.json +++ b/shade/tests/unit/fixtures/catalog-v2.json @@ -85,6 +85,20 @@ "type": "identity", "name": "keystone" }, + { + "endpoints_links": [], + "endpoints": [ + { + "adminURL": "https://network.example.com", + "region": "RegionOne", + "publicURL": "https://network.example.com", + "internalURL": "https://network.example.com", + "id": "4deb4d0504a044a395d4480741ba628d" + } + ], + "type": "network", + "name": "neutron" + }, { "endpoints_links": [], "endpoints": [ diff --git a/shade/tests/unit/fixtures/catalog-v3.json b/shade/tests/unit/fixtures/catalog-v3.json index 55722e3a4..41cf79a23 100644 --- a/shade/tests/unit/fixtures/catalog-v3.json +++ b/shade/tests/unit/fixtures/catalog-v3.json @@ -65,6 +65,19 @@ "name": "keystone", "type": "identity" }, + { + "endpoints": [ + { + "id": "4deb4d0504a044a395d4480741ba628d", + "interface": "public", + "region": "RegionOne", + "url": "https://network.example.com" + } + ], + "endpoints_links": [], + "name": "neutron", + "type": "network" + }, { "endpoints": [ { diff --git a/shade/tests/unit/fixtures/network-version.json b/shade/tests/unit/fixtures/network-version.json new file mode 100644 index 000000000..2ad76f4ec --- /dev/null +++ b/shade/tests/unit/fixtures/network-version.json @@ -0,0 +1,14 @@ +{ + "versions": [ + { + "id": "v2.0", + "links": [ + { + "href": "http://network.example.com/v2.0", + "rel": "self" + } + ], + "status": "CURRENT" + } + ] +} diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 3833b8224..303e268aa 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -32,7 +32,7 @@ from shade.tests.unit import base -class TestFloatingIP(base.TestCase): +class TestFloatingIP(base.RequestsMockTestCase): mock_floating_ip_list_rep = { 'floatingips': [ { @@ -165,19 +165,25 @@ def test_float_no_status(self): normalized = self.cloud._normalize_floating_ips(floating_ips) self.assertEqual('UNKNOWN', normalized[0]['status']) - @patch.object(OpenStackCloud, 'neutron_client') - @patch.object(OpenStackCloud, 'has_service', return_value=True) - def test_list_floating_ips(self, mock_has_service, mock_neutron_client): - mock_neutron_client.list_floatingips.return_value = \ - self.mock_floating_ip_list_rep + def test_list_floating_ips(self): + self.adapter.get( + 'https://network.example.com/v2.0/floatingips.json', + json=self.mock_floating_ip_list_rep) + + self.calls += [ + dict( + method='GET', + url='https://network.example.com/v2.0/floatingips.json'), + ] floating_ips = self.cloud.list_floating_ips() - mock_neutron_client.list_floatingips.assert_called_with() self.assertIsInstance(floating_ips, list) self.assertAreInstances(floating_ips, dict) self.assertEqual(2, len(floating_ips)) + self.assert_calls() + @patch.object(OpenStackCloud, 'neutron_client') @patch.object(OpenStackCloud, 'has_service', return_value=True) def test_list_floating_ips_with_filters(self, mock_has_service, From a616b64402c658c09632bb8b5961878cd06b0d26 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 21 Jan 2017 08:49:29 +0100 Subject: [PATCH 1211/3836] Add helper test method for registering REST calls We have a pattern in our tests where we tell requests_mock about a URI we're going to call, then we save a list of the things we want to check to see have been called. There is more duplicated data thre than I tend to feel like typing. SO - make a helper method that allows us to register a call (and optionally some things to check) and save it to the list of things to assert at the end. Change-Id: Ie542efd55dc61999c7bad4787427f7664c0ecfb0 --- shade/tests/unit/base.py | 54 ++-- shade/tests/unit/test_floating_ip_neutron.py | 10 +- shade/tests/unit/test_image.py | 257 ++++++++----------- shade/tests/unit/test_object.py | 74 ++---- 4 files changed, 170 insertions(+), 225 deletions(-) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 98328b89f..3a02e1c18 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -111,17 +111,20 @@ def setUp(self, cloud_config_fixture='clouds.yaml'): super(RequestsMockTestCase, self).setUp( cloud_config_fixture=cloud_config_fixture) + self._uri_registry = {} + self.discovery_json = os.path.join( self.fixtures_directory, 'discovery.json') self.use_keystone_v3() def use_keystone_v3(self): self.adapter = self.useFixture(rm_fixture.Fixture()) - self.adapter.get( - 'http://192.168.0.19:35357/', + self.calls = [] + self.register_uri( + 'GET', 'http://192.168.0.19:35357/', text=open(self.discovery_json, 'r').read()) - self.adapter.post( - 'https://example.com/v3/auth/tokens', + self.register_uri( + 'POST', 'https://example.com/v3/auth/tokens', headers={ 'X-Subject-Token': self.getUniqueString()}, text=open( @@ -129,28 +132,21 @@ def use_keystone_v3(self): self.fixtures_directory, 'catalog-v3.json'), 'r').read()) - self.calls = [ - dict(method='GET', url='http://192.168.0.19:35357/'), - dict(method='POST', url='https://example.com/v3/auth/tokens'), - ] self._make_test_cloud(identity_api_version='3') def use_keystone_v2(self): self.adapter = self.useFixture(rm_fixture.Fixture()) - self.adapter.get( - 'http://192.168.0.19:35357/', + self.calls = [] + self.register_uri( + 'GET', 'http://192.168.0.19:35357/', text=open(self.discovery_json, 'r').read()) - self.adapter.post( - 'https://example.com/v2.0/tokens', + self.register_uri( + 'POST', 'https://example.com/v2.0/tokens', text=open( os.path.join( self.fixtures_directory, 'catalog-v2.json'), 'r').read()) - self.calls = [ - dict(method='GET', url='http://192.168.0.19:35357/'), - dict(method='POST', url='https://example.com/v2.0/tokens'), - ] self._make_test_cloud(identity_api_version='2.0') def _make_test_cloud(self, **kwargs): @@ -164,21 +160,31 @@ def _make_test_cloud(self, **kwargs): def use_glance(self, image_version_json='image-version.json'): discovery_fixture = os.path.join( self.fixtures_directory, image_version_json) - self.adapter.get( - 'https://image.example.com/', + self.register_uri( + 'GET', 'https://image.example.com/', text=open(discovery_fixture, 'r').read()) - self.calls += [ - dict(method='GET', url='https://image.example.com/'), - ] def use_neutron(self, network_version_json='network-version.json'): discovery_fixture = os.path.join( self.fixtures_directory, network_version_json) - self.adapter.get( - 'https://network.example.com/', + self.register_uri( + 'GET', 'https://network.example.com/', text=open(discovery_fixture, 'r').read()) + + def register_uri(self, method, uri, **kwargs): + validate = kwargs.pop('validate', {}) + key = '{method}:{uri}'.format(method=method, uri=uri) + if key in self._uri_registry: + self._uri_registry[key].append(kwargs) + self.adapter.register_uri(method, uri, self._uri_registry[key]) + else: + self._uri_registry[key] = [kwargs] + self.adapter.register_uri(method, uri, **kwargs) + self.calls += [ - dict(method='GET', url='https://network.example.com/'), + dict( + method=method, + url=uri, **validate) ] def assert_calls(self, stop_after=None): diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 303e268aa..91e609f01 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -166,16 +166,10 @@ def test_float_no_status(self): self.assertEqual('UNKNOWN', normalized[0]['status']) def test_list_floating_ips(self): - self.adapter.get( - 'https://network.example.com/v2.0/floatingips.json', + self.register_uri( + 'GET', 'https://network.example.com/v2.0/floatingips.json', json=self.mock_floating_ip_list_rep) - self.calls += [ - dict( - method='GET', - url='https://network.example.com/v2.0/floatingips.json'), - ] - floating_ips = self.cloud.list_floating_ips() self.assertIsInstance(floating_ips, list) diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index 43dab0302..34ec5d2b9 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -107,19 +107,20 @@ def test_download_image_two_outputs(self): output_path='fake_path', output_file=fake_fd) def test_download_image_no_images_found(self): - self.adapter.get( - 'https://image.example.com/v2/images', + self.register_uri( + 'GET', 'https://image.example.com/v2/images', json=dict(images=[])) self.assertRaises(exc.OpenStackCloudResourceNotFound, self.cloud.download_image, 'fake_image', output_path='fake_path') + self.assert_calls() def _register_image_mocks(self): - self.adapter.get( - 'https://image.example.com/v2/images', + self.register_uri( + 'GET', 'https://image.example.com/v2/images', json=self.fake_search_return) - self.adapter.get( - 'https://image.example.com/v2/images/{id}/file'.format( + self.register_uri( + 'GET', 'https://image.example.com/v2/images/{id}/file'.format( id=self.image_id), content=self.output, headers={ @@ -132,6 +133,7 @@ def test_download_image_with_fd(self): self.cloud.download_image('fake_image', output_file=output_file) output_file.seek(0) self.assertEqual(output_file.read(), self.output) + self.assert_calls() def test_download_image_with_path(self): self._register_image_mocks() @@ -139,29 +141,32 @@ def test_download_image_with_path(self): self.cloud.download_image('fake_image', output_path=output_file.name) output_file.seek(0) self.assertEqual(output_file.read(), self.output) + self.assert_calls() def test_empty_list_images(self): - self.adapter.register_uri( + self.register_uri( 'GET', 'https://image.example.com/v2/images', json={'images': []}) self.assertEqual([], self.cloud.list_images()) + self.assert_calls() def test_list_images(self): - self.adapter.register_uri( + self.register_uri( 'GET', 'https://image.example.com/v2/images', json=self.fake_search_return) self.assertEqual( self.cloud._normalize_images([self.fake_image_dict]), self.cloud.list_images()) + self.assert_calls() def test_list_images_paginated(self): marker = str(uuid.uuid4()) - self.adapter.register_uri( + self.register_uri( 'GET', 'https://image.example.com/v2/images', json={ 'images': [self.fake_image_dict], 'next': '/v2/images?marker={marker}'.format(marker=marker), }) - self.adapter.register_uri( + self.register_uri( 'GET', 'https://image.example.com/v2/images?marker={marker}'.format( marker=marker), @@ -170,33 +175,19 @@ def test_list_images_paginated(self): self.cloud._normalize_images([ self.fake_image_dict, self.fake_image_dict]), self.cloud.list_images()) + self.assert_calls() def test_create_image_put_v2(self): self.cloud.image_api_use_tasks = False - self.adapter.register_uri( - 'GET', 'https://image.example.com/v2/images', [ - dict(json={'images': []}), - dict(json=self.fake_search_return), - ]) - self.adapter.register_uri( + self.register_uri( + 'GET', 'https://image.example.com/v2/images', + json={'images': []}) + + self.register_uri( 'POST', 'https://image.example.com/v2/images', json=self.fake_image_dict, - ) - self.adapter.register_uri( - 'PUT', 'https://image.example.com/v2/images/{id}/file'.format( - id=self.image_id), - request_headers={'Content-Type': 'application/octet-stream'}) - - self.cloud.create_image( - 'fake_image', self.imagefile.name, wait=True, timeout=1, - is_public=False) - - self.calls += [ - dict(method='GET', url='https://image.example.com/v2/images'), - dict( - method='POST', - url='https://image.example.com/v2/images', + validate=dict( json={ u'container_format': u'bare', u'disk_format': u'qcow2', @@ -206,12 +197,20 @@ def test_create_image_put_v2(self): u'owner_specified.shade.sha256': NO_SHA256, u'visibility': u'private' }), - dict( - method='PUT', - url='https://image.example.com/v2/images/{id}/file'.format( - id=self.image_id)), - dict(method='GET', url='https://image.example.com/v2/images'), - ] + ) + self.register_uri( + 'PUT', 'https://image.example.com/v2/images/{id}/file'.format( + id=self.image_id), + request_headers={'Content-Type': 'application/octet-stream'}) + + self.register_uri( + 'GET', 'https://image.example.com/v2/images', + json=self.fake_search_return) + + self.cloud.create_image( + 'fake_image', self.imagefile.name, wait=True, timeout=1, + is_public=False) + self.assert_calls() self.assertEqual(self.adapter.request_history[5].text.read(), b'\x00') @@ -221,14 +220,23 @@ def test_create_image_task(self): container_name = 'image_upload_v2_test_container' endpoint = self.cloud._object_store_client.get_endpoint() - self.adapter.get( - 'https://object-store.example.com/info', + self.register_uri( + 'GET', 'https://image.example.com/v2/images', json={'images': []}) + + self.register_uri( + 'GET', 'https://object-store.example.com/info', json=dict( swift={'max_file_size': 1000}, slo={'min_segment_size': 500})) - self.adapter.put( - '{endpoint}/{container}'.format( + self.register_uri( + 'HEAD', '{endpoint}/{container}'.format( + endpoint=endpoint, + container=container_name), + status_code=404) + + self.register_uri( + 'PUT', '{endpoint}/{container}'.format( endpoint=endpoint, container=container_name,), status_code=201, @@ -237,34 +245,38 @@ def test_create_image_task(self): 'Content-Length': '0', 'Content-Type': 'text/html; charset=UTF-8', }) - self.adapter.head( - '{endpoint}/{container}'.format( + + self.register_uri( + 'HEAD', '{endpoint}/{container}'.format( endpoint=endpoint, container=container_name), - [ - dict(status_code=404), - dict(headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}), - ]) - self.adapter.head( - '{endpoint}/{container}/{object}'.format( + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}) + + self.register_uri( + 'HEAD', '{endpoint}/{container}/{object}'.format( endpoint=endpoint, container=container_name, object=image_name), status_code=404) - self.adapter.put( - '{endpoint}/{container}/{object}'.format( + self.register_uri( + 'PUT', '{endpoint}/{container}/{object}'.format( endpoint=endpoint, container=container_name, object=image_name), - status_code=201) + status_code=201, + validate=dict( + headers={ + 'x-object-meta-x-shade-md5': NO_MD5, + 'x-object-meta-x-shade-sha256': NO_SHA256, + })) task_id = str(uuid.uuid4()) args = dict( @@ -281,87 +293,39 @@ def test_create_image_task(self): del(image_no_checksums['owner_specified.shade.sha256']) del(image_no_checksums['owner_specified.shade.object']) - self.adapter.register_uri( - 'GET', 'https://image.example.com/v2/images', [ - dict(json={'images': []}), - dict(json={'images': []}), - dict(json={'images': [image_no_checksums]}), - dict(json=self.fake_search_return), - ]) - self.adapter.register_uri( - 'POST', 'https://image.example.com/v2/tasks', - json=args) - self.adapter.register_uri( - 'PATCH', - 'https://image.example.com/v2/images/{id}'.format( - id=self.image_id)) - self.adapter.register_uri( - 'GET', - 'https://image.example.com/v2/tasks/{id}'.format(id=task_id), - [ - dict(status_code=503, text='Random error'), - dict(json={'images': args}), - ] - ) - - self.cloud.create_image( - image_name, self.imagefile.name, wait=True, timeout=1, - is_public=False, container=container_name) + self.register_uri( + 'GET', 'https://image.example.com/v2/images', + json={'images': []}) - self.calls += [ - dict(method='GET', url='https://image.example.com/v2/images'), - dict(method='GET', url='https://object-store.example.com/info'), - dict( - method='HEAD', - url='{endpoint}/{container}'.format( - endpoint=endpoint, - container=container_name)), - dict( - method='PUT', - url='{endpoint}/{container}'.format( - endpoint=endpoint, - container=container_name)), - dict( - method='HEAD', - url='{endpoint}/{container}'.format( - endpoint=endpoint, - container=container_name)), - dict( - method='HEAD', - url='{endpoint}/{container}/{object}'.format( - endpoint=endpoint, - container=container_name, object=image_name)), - dict( - method='PUT', - url='{endpoint}/{container}/{object}'.format( - endpoint=endpoint, - container=container_name, object=image_name), - headers={ - 'x-object-meta-x-shade-md5': NO_MD5, - 'x-object-meta-x-shade-sha256': NO_SHA256, - }), - dict(method='GET', url='https://image.example.com/v2/images'), - dict( - method='POST', - url='https://image.example.com/v2/tasks', + self.register_uri( + 'POST', 'https://image.example.com/v2/tasks', + json=args, + validate=dict( json=dict( type='import', input={ 'import_from': '{container}/{object}'.format( container=container_name, object=image_name), - 'image_properties': {'name': image_name}})), - dict( - method='GET', - url='https://image.example.com/v2/tasks/{id}'.format( - id=task_id)), - dict( - method='GET', - url='https://image.example.com/v2/tasks/{id}'.format( - id=task_id)), - dict(method='GET', url='https://image.example.com/v2/images'), - dict( - method='PATCH', - url='https://image.example.com/v2/images/{id}'.format( - id=self.image_id), + 'image_properties': {'name': image_name}}))) + + self.register_uri( + 'GET', + 'https://image.example.com/v2/tasks/{id}'.format(id=task_id), + status_code=503, text='Random error') + + self.register_uri( + 'GET', + 'https://image.example.com/v2/tasks/{id}'.format(id=task_id), + json={'images': args}) + + self.register_uri( + 'GET', 'https://image.example.com/v2/images', + json={'images': [image_no_checksums]}) + + self.register_uri( + 'PATCH', + 'https://image.example.com/v2/images/{id}'.format( + id=self.image_id), + validate=dict( json=sorted([ { u'op': u'add', @@ -379,9 +343,15 @@ def test_create_image_task(self): headers={ 'Content-Type': 'application/openstack-images-v2.1-json-patch' - }), - dict(method='GET', url='https://image.example.com/v2/images'), - ] + })) + + self.register_uri( + 'GET', 'https://image.example.com/v2/images', + json=self.fake_search_return) + + self.cloud.create_image( + image_name, self.imagefile.name, wait=True, timeout=1, + is_public=False, container=container_name) self.assert_calls() @@ -748,16 +718,11 @@ def test_version_discovery_skip(self): self.cloud.cloud_config.config['image_endpoint_override'] = \ 'https://image.example.com/v2/override' - self.adapter.register_uri( + self.register_uri( 'GET', 'https://image.example.com/v2/override/images', json={'images': []}) self.assertEqual([], self.cloud.list_images()) self.assertEqual( self.cloud._image_client.endpoint_override, 'https://image.example.com/v2/override') - self.calls += [ - dict( - method='GET', - url='https://image.example.com/v2/override/images'), - ] self.assert_calls() diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index a99d53177..f3d4cac6d 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -288,23 +288,21 @@ def test_list_containers(self): containers = [ {u'count': 0, u'bytes': 0, u'name': self.container}] - self.adapter.get(endpoint, complete_qs=True, json=containers) + self.register_uri('GET', endpoint, complete_qs=True, json=containers) ret = self.cloud.list_containers() - self.calls += [ - dict(method='GET', url=endpoint), - ] self.assert_calls() self.assertEqual(containers, ret) def test_list_containers_exception(self): endpoint = '{endpoint}/?format=json'.format( endpoint=self.endpoint) - self.adapter.get(endpoint, complete_qs=True, status_code=416) + self.register_uri('GET', endpoint, complete_qs=True, status_code=416) self.assertRaises( exc.OpenStackCloudException, self.cloud.list_containers) + self.assert_calls() def test_list_objects(self): endpoint = '{endpoint}?format=json'.format( @@ -317,51 +315,37 @@ def test_list_objects(self): u'name': self.object, u'content_type': u'application/octet-stream'}] - self.adapter.get(endpoint, complete_qs=True, json=objects) + self.register_uri('GET', endpoint, complete_qs=True, json=objects) ret = self.cloud.list_objects(self.container) - self.calls += [ - dict(method='GET', url=endpoint), - ] self.assert_calls() self.assertEqual(objects, ret) def test_list_objects_exception(self): endpoint = '{endpoint}?format=json'.format( endpoint=self.container_endpoint) - self.adapter.get(endpoint, complete_qs=True, status_code=416) + self.register_uri('GET', endpoint, complete_qs=True, status_code=416) self.assertRaises( exc.OpenStackCloudException, self.cloud.list_objects, self.container) + self.assert_calls() def test_delete_object(self): - self.adapter.head( - self.object_endpoint, headers={'X-Object-Meta': 'foo'}) - self.adapter.delete(self.object_endpoint, status_code=204) + self.register_uri( + 'HEAD', self.object_endpoint, + headers={'X-Object-Meta': 'foo'}) + self.register_uri('DELETE', self.object_endpoint, status_code=204) self.assertTrue(self.cloud.delete_object(self.container, self.object)) - self.calls += [ - dict( - method='HEAD', - url=self.object_endpoint), - dict( - method='DELETE', - url=self.object_endpoint), - ] self.assert_calls() def test_delete_object_not_found(self): - self.adapter.head(self.object_endpoint, status_code=404) + self.register_uri('HEAD', self.object_endpoint, status_code=404) self.assertFalse(self.cloud.delete_object(self.container, self.object)) - self.calls += [ - dict( - method='HEAD', - url=self.object_endpoint), - ] self.assert_calls() def test_get_object(self): @@ -379,8 +363,8 @@ def test_get_object(self): } response_headers = {k.lower(): v for k, v in headers.items()} text = 'test body' - self.adapter.get( - self.object_endpoint, + self.register_uri( + 'GET', self.object_endpoint, headers={ 'Content-Length': '20304400896', 'Content-Type': 'application/octet-stream', @@ -397,39 +381,33 @@ def test_get_object(self): resp = self.cloud.get_object(self.container, self.object) - self.calls += [ - dict(method='GET', url=self.object_endpoint), - ] self.assert_calls() self.assertEqual((response_headers, text), resp) def test_get_object_not_found(self): - self.adapter.get(self.object_endpoint, status_code=404) + self.register_uri('GET', self.object_endpoint, status_code=404) self.assertIsNone(self.cloud.get_object(self.container, self.object)) - self.calls += [ - dict(method='GET', url=self.object_endpoint), - ] self.assert_calls() def test_get_object_exception(self): - self.adapter.get(self.object_endpoint, status_code=416) + self.register_uri('GET', self.object_endpoint, status_code=416) self.assertRaises( shade.OpenStackCloudException, self.cloud.get_object, self.container, self.object) - self.calls += [ - dict(method='GET', url=self.object_endpoint), - ] self.assert_calls() - def test_get_object_segment_size(self): - self.adapter.get( - 'https://object-store.example.com/info', + def test_get_object_segment_size_below_min(self): + # Register directly becuase we make multiple calls. The number + # of calls we make isn't interesting - what we do with the return + # values is. Don't run assert_calls for the same reason. + self.adapter.register_uri( + 'GET', 'https://object-store.example.com/info', json=dict( swift={'max_file_size': 1000}, slo={'min_segment_size': 500})) @@ -439,20 +417,22 @@ def test_get_object_segment_size(self): self.assertEqual(1000, self.cloud.get_object_segment_size(1100)) def test_get_object_segment_size_http_404(self): - self.adapter.get( - 'https://object-store.example.com/info', + self.register_uri( + 'GET', 'https://object-store.example.com/info', status_code=404, reason='Not Found') self.assertEqual(shade.openstackcloud.DEFAULT_OBJECT_SEGMENT_SIZE, self.cloud.get_object_segment_size(None)) + self.assert_calls() def test_get_object_segment_size_http_412(self): - self.adapter.get( - 'https://object-store.example.com/info', + self.register_uri( + 'GET', 'https://object-store.example.com/info', status_code=412, reason='Precondition failed') self.assertEqual(shade.openstackcloud.DEFAULT_OBJECT_SEGMENT_SIZE, self.cloud.get_object_segment_size(None)) + self.assert_calls() class TestObjectUploads(BaseTestObject): From 3ac4915109235d1456585a96af48ebef7ce669f7 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 21 Jan 2017 09:34:22 +0100 Subject: [PATCH 1212/3836] Skip discovery for neutron It's a waste of energy to do discovery. We can always revert if we find some place where it's a problem. Change-Id: I8c15a8d775df8a19110dc9ceb6c8e5c8df8fdee0 --- shade/openstackcloud.py | 5 ++++- shade/tests/unit/base.py | 7 ------- shade/tests/unit/fixtures/network-version.json | 14 -------------- 3 files changed, 4 insertions(+), 22 deletions(-) delete mode 100644 shade/tests/unit/fixtures/network-version.json diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 87f41c09a..361e08ad0 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -514,7 +514,10 @@ def _image_client(self): def _network_client(self): if 'network' not in self._raw_clients: client = self._get_raw_client('network') - client.endpoint_override = self._discover_latest_version(client) + # Don't bother with version discovery - there is only one version + # of neutron. This is what neutronclient does, fwiw. + client.endpoint_override = urllib.parse.urljoin( + client.get_endpoint(), 'v2.0') self._raw_clients['network'] = client return self._raw_clients['network'] diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 3a02e1c18..02d47c4e9 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -164,13 +164,6 @@ def use_glance(self, image_version_json='image-version.json'): 'GET', 'https://image.example.com/', text=open(discovery_fixture, 'r').read()) - def use_neutron(self, network_version_json='network-version.json'): - discovery_fixture = os.path.join( - self.fixtures_directory, network_version_json) - self.register_uri( - 'GET', 'https://network.example.com/', - text=open(discovery_fixture, 'r').read()) - def register_uri(self, method, uri, **kwargs): validate = kwargs.pop('validate', {}) key = '{method}:{uri}'.format(method=method, uri=uri) diff --git a/shade/tests/unit/fixtures/network-version.json b/shade/tests/unit/fixtures/network-version.json deleted file mode 100644 index 2ad76f4ec..000000000 --- a/shade/tests/unit/fixtures/network-version.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "versions": [ - { - "id": "v2.0", - "links": [ - { - "href": "http://network.example.com/v2.0", - "rel": "self" - } - ], - "status": "CURRENT" - } - ] -} From 12b226d28eb0d62e06e501be4808bb34537b6a32 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 21 Jan 2017 15:57:01 +0000 Subject: [PATCH 1213/3836] Updated from global requirements Change-Id: Ib91389116639398a53d74851f3828719de9b98aa --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ba9f17a0d..c8282921a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,4 @@ pbr>=1.8 # Apache-2.0 six>=1.9.0 # MIT stevedore>=1.17.1 # Apache-2.0 os-client-config>=1.22.0 # Apache-2.0 -keystoneauth1>=2.17.0 # Apache-2.0 +keystoneauth1>=2.18.0 # Apache-2.0 From 0a7483ed952c82d006b0c55ce1ff77212a0c7729 Mon Sep 17 00:00:00 2001 From: Reedip Date: Wed, 18 Jan 2017 03:49:23 -0500 Subject: [PATCH 1214/3836] Fix Setting Quotas in Neutron Currently Quota Set command doenst work in SDK version 0.9.12 as the request formed for the Neutron API is not correct. This patch attempts to fix the same. Change-Id: Id58b05bcdbfee73cb9b93dd5533b4c7d93dd03aa Partial-Bug:#1652317 Closes-Bug:#1655445 --- openstack/network/v2/quota.py | 11 +++++++++++ openstack/tests/functional/network/v2/test_quota.py | 7 +++++++ openstack/tests/unit/network/v2/test_quota.py | 6 ++++++ 3 files changed, 24 insertions(+) diff --git a/openstack/network/v2/quota.py b/openstack/network/v2/quota.py index 8620fbaac..a77f5aac4 100644 --- a/openstack/network/v2/quota.py +++ b/openstack/network/v2/quota.py @@ -58,6 +58,17 @@ class Quota(resource.Resource): #: The maximum amount of security groups you can create. *Type: int* security_groups = resource.Body('security_group', type=int) + def _prepare_request(self, requires_id=True, prepend_key=False): + _request = super(Quota, self)._prepare_request(requires_id, + prepend_key) + if self.resource_key in _request.body: + _body = _request.body[self.resource_key] + else: + _body = _request.body + if 'id' in _body: + del _body['id'] + return _request + class QuotaDefault(Quota): base_path = '/quotas/%(project)s/default' diff --git a/openstack/tests/functional/network/v2/test_quota.py b/openstack/tests/functional/network/v2/test_quota.py index 16fb87da2..17f1fbb29 100644 --- a/openstack/tests/functional/network/v2/test_quota.py +++ b/openstack/tests/functional/network/v2/test_quota.py @@ -27,3 +27,10 @@ def test_list(self): self.assertIn('security_group', qot) self.assertIn('subnetpool', qot) self.assertIn('rbac_policy', qot) + + def test_set(self): + attrs = {'network': 123456789} + self.conn.network.update_quota(**attrs) + quota_list = self.conn.network.get_quota() + for quota in quota_list: + self.assertIn('123456789', quota) diff --git a/openstack/tests/unit/network/v2/test_quota.py b/openstack/tests/unit/network/v2/test_quota.py index 0c06a54d3..26b18ca42 100644 --- a/openstack/tests/unit/network/v2/test_quota.py +++ b/openstack/tests/unit/network/v2/test_quota.py @@ -67,6 +67,12 @@ def test_make_it(self): self.assertEqual(EXAMPLE['l7policy'], sot.l7_policies) self.assertEqual(EXAMPLE['pool'], sot.pools) + def test_prepare_request(self): + body = {'id': 'ABCDEFGH', 'network': '12345'} + quota_obj = quota.Quota(**body) + response = quota_obj._prepare_request() + self.assertNotIn('id', response) + class TestQuotaDefault(testtools.TestCase): From 6d185df4983ef824d629769c11dde5dd9a6297b9 Mon Sep 17 00:00:00 2001 From: Reedip Date: Wed, 18 Jan 2017 03:52:23 -0500 Subject: [PATCH 1215/3836] Add project ID in QuotaDefault requests Currently QuotaDefault doesnt need ID to work with, but it needs the URI to have the project ID so that it can reference the same in the Neutron API. This patch fixes this issue. Partial-Bug:#1652317 Change-Id: I9288b1c9fc64c6326231d535d9c8574b24afeb3a --- openstack/network/v2/_proxy.py | 3 ++- openstack/tests/unit/network/v2/test_proxy.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index d4ab2393b..97b4cda8c 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -1995,7 +1995,8 @@ def get_quota_default(self, quota): when no resource can be found. """ quota_obj = self._get_resource(_quota.Quota, quota) - return self._get(_quota.QuotaDefault, project=quota_obj.project_id) + return self._get(_quota.QuotaDefault, project=quota_obj.id, + requires_id=False) def quotas(self, **query): """Return a generator of quotas diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 41dbdd636..6a8b61bf2 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -683,7 +683,8 @@ def test_quota_default_get(self, mock_get): self.proxy.get_quota_default, method_args=['QUOTA_ID'], expected_args=[quota.QuotaDefault], - expected_kwargs={'project': "PROJECT"}) + expected_kwargs={'project': fake_quota.id, + 'requires_id': False}) mock_get.assert_called_once_with(quota.Quota, 'QUOTA_ID') def test_quotas(self): From 85719a71943573b1e0131033d707ec0fbd46d6b1 Mon Sep 17 00:00:00 2001 From: Dinesh Bhor Date: Wed, 4 Jan 2017 14:22:18 +0530 Subject: [PATCH 1216/3836] Fix error messages are not displayed correctly Error messages are not displayed correctly for segment create and host create commands. 'response' object has the actual error message returned from service in 'text' attribute. This patch saves the exception message returned from the service to 'details' attribute so that it can be displayed properly to the user. Closes-Bug: #1656826 Change-Id: I6d728abbcc5e914b5bd025bc4e059cdcb13e2109 --- openstack/exceptions.py | 37 +++++++++ openstack/session.py | 13 +-- openstack/tests/unit/test_session.py | 114 ++++++++++++++++++++++++--- 3 files changed, 139 insertions(+), 25 deletions(-) diff --git a/openstack/exceptions.py b/openstack/exceptions.py index ff3176fd8..9fa8c87fa 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -16,6 +16,8 @@ Exception definitions. """ +import re + import six @@ -108,3 +110,38 @@ class ResourceTimeout(SDKException): class ResourceFailure(SDKException): """General resource failure.""" pass + + +def from_exception(exc): + """Return an instance of an HTTPException based on httplib response.""" + if exc.response.status_code == 404: + cls = NotFoundException + else: + cls = HttpException + + resp = exc.response + details = resp.text + resp_body = resp.content + content_type = resp.headers.get('content-type', '') + if resp_body and 'application/json' in content_type: + # Iterate over the nested objects to retrieve "message" attribute. + messages = [obj.get('message') for obj in resp.json().values()] + # Join all of the messages together nicely and filter out any objects + # that don't have a "message" attr. + details = '\n'.join(msg for msg in messages if msg) + + elif resp_body and 'text/html' in content_type: + # Split the lines, strip whitespace and inline HTML from the response. + details = [re.sub(r'<.+?>', '', i.strip()) + for i in details.splitlines()] + details = [msg for msg in details if msg] + # Remove duplicates from the list. + details_temp = [] + for detail in details: + if detail not in details_temp: + details_temp.append(detail) + # Return joined string separated by colons. + details = ': '.join(details_temp) + return cls(details=details, message=exc.message, response=exc.response, + request_id=exc.request_id, url=exc.url, method=exc.method, + http_status=exc.http_status, cause=exc) diff --git a/openstack/session.py b/openstack/session.py index 5b087fcf3..3d3323cc9 100644 --- a/openstack/session.py +++ b/openstack/session.py @@ -63,18 +63,7 @@ def map_exceptions_wrapper(*args, **kwargs): try: return func(*args, **kwargs) except _exceptions.HttpError as e: - if e.http_status == 404: - raise exceptions.NotFoundException( - message=e.message, details=e.details, - response=e.response, request_id=e.request_id, - url=e.url, method=e.method, - http_status=e.http_status, cause=e) - else: - raise exceptions.HttpException( - message=e.message, details=e.details, - response=e.response, request_id=e.request_id, - url=e.url, method=e.method, - http_status=e.http_status, cause=e) + raise exceptions.from_exception(e) except _exceptions.ClientException as e: raise exceptions.SDKException(message=e.message, cause=e) diff --git a/openstack/tests/unit/test_session.py b/openstack/tests/unit/test_session.py index 89e8f452d..a3bdb74c1 100644 --- a/openstack/tests/unit/test_session.py +++ b/openstack/tests/unit/test_session.py @@ -20,6 +20,17 @@ from openstack import session from openstack import utils +HTML_MSG = """ + + 404 Entity Not Found + + +

404 Entity Not Found

+ Entity could not be found +

+ +""" + class TestSession(testtools.TestCase): @@ -60,27 +71,104 @@ def test_init_with_no_api_requests(self): self.assertEqual({}, sot.additional_headers) - def test_map_exceptions_not_found_exception(self): - ksa_exc = _exceptions.HttpError(message="test", http_status=404) - func = mock.Mock(side_effect=ksa_exc) - + def _assert_map_exceptions(self, expected_exc, ksa_exc, func): os_exc = self.assertRaises( - exceptions.NotFoundException, session.map_exceptions(func)) - self.assertIsInstance(os_exc, exceptions.NotFoundException) + expected_exc, session.map_exceptions(func)) + self.assertIsInstance(os_exc, expected_exc) self.assertEqual(ksa_exc.message, os_exc.message) self.assertEqual(ksa_exc.http_status, os_exc.http_status) self.assertEqual(ksa_exc, os_exc.cause) + return os_exc + + def test_map_exceptions_not_found_exception(self): + response = mock.Mock() + response_body = {'NotFoundException': { + 'message': 'Resource not found'}} + response.json = mock.Mock(return_value=response_body) + response.headers = {"content-type": "application/json"} + response.status_code = 404 + ksa_exc = _exceptions.HttpError( + message="test", http_status=404, response=response) + func = mock.Mock(side_effect=ksa_exc) + os_exc = self._assert_map_exceptions( + exceptions.NotFoundException, ksa_exc, func) + self.assertEqual('Resource not found', os_exc.details) def test_map_exceptions_http_exception(self): - ksa_exc = _exceptions.HttpError(message="test", http_status=400) + response = mock.Mock() + response_body = {'HTTPBadRequest': { + 'message': 'request is invalid'}} + response.json = mock.Mock(return_value=response_body) + response.headers = {"content-type": "application/json"} + response.status_code = 400 + ksa_exc = _exceptions.HttpError( + message="test", http_status=400, response=response) func = mock.Mock(side_effect=ksa_exc) - os_exc = self.assertRaises( - exceptions.HttpException, session.map_exceptions(func)) - self.assertIsInstance(os_exc, exceptions.HttpException) - self.assertEqual(ksa_exc.message, os_exc.message) - self.assertEqual(ksa_exc.http_status, os_exc.http_status) - self.assertEqual(ksa_exc, os_exc.cause) + os_exc = self._assert_map_exceptions( + exceptions.HttpException, ksa_exc, func) + self.assertEqual('request is invalid', os_exc.details) + + def test_map_exceptions_http_exception_handle_json(self): + mock_resp = mock.Mock() + mock_resp.status_code = 413 + mock_resp.json.return_value = { + "overLimit": { + "message": "OverLimit413...", + "retryAt": "2017-01-03T13:33:06Z" + }, + "overLimitRetry": { + "message": "OverLimit Retry...", + "retryAt": "2017-01-03T13:33:06Z" + } + } + mock_resp.headers = { + "content-type": "application/json" + } + ksa_exc = _exceptions.HttpError( + message="test", http_status=413, response=mock_resp) + func = mock.Mock(side_effect=ksa_exc) + + os_exc = self._assert_map_exceptions( + exceptions.HttpException, ksa_exc, func) + # It's not sure that which 'message' will be first so exact checking is + # difficult here. It can be 'OverLimit413...\nOverLimit Retry...' or + # it can be 'OverLimit Retry...\nOverLimit413...'. + self.assertIn('OverLimit413...', os_exc.details) + self.assertIn('OverLimit Retry...', os_exc.details) + + def test_map_exceptions_notfound_exception_handle_html(self): + mock_resp = mock.Mock() + mock_resp.status_code = 404 + mock_resp.text = HTML_MSG + mock_resp.headers = { + "content-type": "text/html" + } + ksa_exc = _exceptions.HttpError( + message="test", http_status=404, response=mock_resp) + func = mock.Mock(side_effect=ksa_exc) + + os_exc = self._assert_map_exceptions( + exceptions.NotFoundException, ksa_exc, func) + self.assertEqual('404 Entity Not Found: Entity could not be found', + os_exc.details) + + def test_map_exceptions_notfound_exception_handle_other_content_type(self): + mock_resp = mock.Mock() + mock_resp.status_code = 404 + fake_text = ("{'UnknownException': {'message': " + "'UnknownException occurred...'}}") + mock_resp.text = fake_text + mock_resp.headers = { + "content-type": 'application/octet-stream' + } + ksa_exc = _exceptions.HttpError( + message="test", http_status=404, response=mock_resp) + func = mock.Mock(side_effect=ksa_exc) + + os_exc = self._assert_map_exceptions( + exceptions.NotFoundException, ksa_exc, func) + self.assertEqual(fake_text, os_exc.details) def test_map_exceptions_sdk_exception_1(self): ksa_exc = _exceptions.ClientException() From e71e79c397385a245c319400f74aba8f06e0b295 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 21 Jan 2017 16:35:21 +0100 Subject: [PATCH 1217/3836] Update coding document to mention direct REST calls Now that we've got glance and swift done and have started on neutron, let's update the doc to tell people that it's the way forward. Change-Id: I16f7ae58f5ae280bc4b1533f62bf3e3a09dbaeb0 --- doc/source/coding.rst | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/doc/source/coding.rst b/doc/source/coding.rst index 160aceb04..778089e6d 100644 --- a/doc/source/coding.rst +++ b/doc/source/coding.rst @@ -56,22 +56,32 @@ Exceptions All underlying client exceptions must be captured and converted to an `OpenStackCloudException` or one of its derivatives. -Client Calls +REST Calls ============ -All underlying client calls (novaclient, swiftclient, etc.) must be -wrapped by a Task object. +All interactions with the cloud should be done with direct REST using +the appropriate `keystoneauth1.adapter.Adapter`. See Glance and Swift +calls for examples. Returned Resources ================== -Complex objects returned to the caller must be a dict type. The -methods `obj_to_dict()` or `obj_list_to_dict()` should be used for this. +Complex objects returned to the caller must be a `munch.Munch` type. The +`shade._adapter.Adapter` class makes resources into `munch.Munch`. -As of this writing, those two methods are returning Munch objects, which help -to maintain backward compatibility with a time when shade returned raw -objects. Munch allows the returned resource to act as *both* an object -and a dict. +All objects should be normalized. It is shade's purpose in life to make +OpenStack consistent for end users, and this means not trusting the clouds +to return consistent objects. There should be a normalize function in +`shade/_normalize.py` that is applied to objects before returning them to +the user. See :doc:`model` for further details on object model requirements. + +Fields should not be in the normalization contract if we cannot commit to +providing them to all users. + +Fields should be renamed in normalization to be consistent with +the rest of shade. For instance, nothing in shade exposes the legacy OpenStack +concept of "tenant" to a user, but instead uses "project" even if the +cloud uses tenant. Nova vs. Neutron ================ @@ -90,6 +100,10 @@ Tests - New API methods *must* have unit tests! +- New unit tests should only mock at the REST layer using `requests_mock`. + Any mocking of shade itself or of legacy client libraries should be + considered legacy and to be avoided. + - Functional tests should be added, when possible. - In functional tests, always use unique names (for resources that have this From 14c371e944eabafa213142bfe35a4932f1bf00e0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 21 Jan 2017 12:15:22 +0100 Subject: [PATCH 1218/3836] Add ability to stream object directly to file For object downloads, allow the user to specify a file to write the content to, rather than returning it all in memory. Change-Id: Ic926fd63a9801049e8120d7597df9961a5f9e657 --- .../stream-to-file-91f48d6dcea399c6.yaml | 3 + shade/openstackcloud.py | 30 ++++++-- shade/tests/functional/test_object.py | 69 +++++++++++++++++++ 3 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/stream-to-file-91f48d6dcea399c6.yaml diff --git a/releasenotes/notes/stream-to-file-91f48d6dcea399c6.yaml b/releasenotes/notes/stream-to-file-91f48d6dcea399c6.yaml new file mode 100644 index 000000000..60e6d64c8 --- /dev/null +++ b/releasenotes/notes/stream-to-file-91f48d6dcea399c6.yaml @@ -0,0 +1,3 @@ +--- +features: + - get_object now supports streaming output directly to a file. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 361e08ad0..afc523211 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -6063,14 +6063,21 @@ def get_object_metadata(self, container, name): raise def get_object(self, container, obj, query_string=None, - resp_chunk_size=None): + resp_chunk_size=1024, outfile=None): """Get the headers and body of an object from swift :param string container: name of the container. :param string obj: name of the object. :param string query_string: query args for uri. (delimiter, prefix, etc.) - :param int resp_chunk_size: chunk size of data to read. + :param int resp_chunk_size: chunk size of data to read. Only used + if the results are being written to a + file. (optional, defaults to 1k) + :param outfile: Write the object to a file instead of + returning the contents. If this option is + given, body in the return tuple will be None. outfile + can either be a file path given as a string, or a + File like object. :returns: Tuple (headers, body) of the object, or None if the object is not found (404) @@ -6083,10 +6090,25 @@ def get_object(self, container, obj, query_string=None, if query_string: endpoint = '{endpoint}?{query_string}'.format( endpoint=endpoint, query_string=query_string) - response = self._object_store_client.get(endpoint) + response = self._object_store_client.get( + endpoint, stream=True) response_headers = { k.lower(): v for k, v in response.headers.items()} - return (response_headers, response.text) + if outfile: + if isinstance(outfile, six.string_types): + outfile_handle = open(outfile, 'wb') + else: + outfile_handle = outfile + for chunk in response.iter_content( + resp_chunk_size, decode_unicode=False): + outfile_handle.write(chunk) + if isinstance(outfile, six.string_types): + outfile_handle.close() + else: + outfile_handle.flush() + return (response_headers, None) + else: + return (response_headers, response.text) except OpenStackCloudHTTPError as e: if e.response.status_code == 404: return None diff --git a/shade/tests/functional/test_object.py b/shade/tests/functional/test_object.py index f32802e74..5d624e765 100644 --- a/shade/tests/functional/test_object.py +++ b/shade/tests/functional/test_object.py @@ -97,3 +97,72 @@ def test_create_object(self): self.assertEqual(container_name, self.demo_cloud.list_containers()[0]['name']) self.demo_cloud.delete_container(container_name) + + def test_download_object_to_file(self): + '''Test uploading small and large files.''' + container_name = self.getUniqueString('container') + self.addDetail('container', content.text_content(container_name)) + self.addCleanup(self.demo_cloud.delete_container, container_name) + self.demo_cloud.create_container(container_name) + self.assertEqual(container_name, + self.demo_cloud.list_containers()[0]['name']) + sizes = ( + (64 * 1024, 1), # 64K, one segment + (64 * 1024, 5) # 64MB, 5 segments + ) + for size, nseg in sizes: + fake_content = '' + segment_size = int(round(size / nseg)) + with tempfile.NamedTemporaryFile() as fake_file: + fake_content = ''.join(random.SystemRandom().choice( + string.ascii_uppercase + string.digits) + for _ in range(size)).encode('latin-1') + + fake_file.write(fake_content) + fake_file.flush() + name = 'test-%d' % size + self.addCleanup( + self.demo_cloud.delete_object, container_name, name) + self.demo_cloud.create_object( + container_name, name, + fake_file.name, + segment_size=segment_size, + metadata={'foo': 'bar'}) + self.assertFalse(self.demo_cloud.is_object_stale( + container_name, name, + fake_file.name + ) + ) + self.assertEqual( + 'bar', self.demo_cloud.get_object_metadata( + container_name, name)['x-object-meta-foo'] + ) + self.demo_cloud.update_object(container=container_name, name=name, + metadata={'testk': 'testv'}) + self.assertEqual( + 'testv', self.demo_cloud.get_object_metadata( + container_name, name)['x-object-meta-testk'] + ) + try: + with tempfile.NamedTemporaryFile() as fake_file: + self.demo_cloud.get_object( + container_name, name, outfile=fake_file.name) + downloaded_content = open(fake_file.name, 'rb').read() + self.assertEqual(fake_content, downloaded_content) + except exc.OpenStackCloudException as e: + self.addDetail( + 'failed_response', + content.text_content(str(e.response.headers))) + self.addDetail( + 'failed_response', + content.text_content(e.response.text)) + raise + self.assertEqual( + name, + self.demo_cloud.list_objects(container_name)[0]['name']) + self.assertTrue( + self.demo_cloud.delete_object(container_name, name)) + self.assertEqual([], self.demo_cloud.list_objects(container_name)) + self.assertEqual(container_name, + self.demo_cloud.list_containers()[0]['name']) + self.demo_cloud.delete_container(container_name) From 48e7eb06cd6467bb50b28f110800b9e9d507d41b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 23 Jan 2017 19:12:39 +0100 Subject: [PATCH 1219/3836] Upload images to swift as application/octet-stream To be fair, this should have been pretty obvious when we wrote it. Of course glance images are application/octet-stream. Also, update the comment about the swift bug, since it's not a swift bug - it's doing exactly what we told it to do. Change-Id: Id992ef12e017d84a01249555518c86fb20280053 --- shade/_adapter.py | 6 ------ shade/openstackcloud.py | 6 ++++-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/shade/_adapter.py b/shade/_adapter.py index e8549b5ea..c9f6f0afb 100644 --- a/shade/_adapter.py +++ b/shade/_adapter.py @@ -95,12 +95,6 @@ def _munch_response(self, response, result_key=None): 'text/plain', 'application/octet-stream'): return response - elif response.headers.get('X-Static-Large-Object'): - # Workaround what seems to be a bug in swift where SLO objects - # return Content-Type application/json but contain - # application/octet-stream - # Bug filed: https://bugs.launchpad.net/swift/+bug/1658295 - return response else: if not response.content: # This doens't have any content diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index afc523211..83b38e72d 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -14,6 +14,7 @@ import functools import hashlib import ipaddress +import json import jsonpatch import operator import os @@ -3516,7 +3517,8 @@ def _upload_image_task( self.create_object( container, name, filename, - md5=md5, sha256=sha256) + md5=md5, sha256=sha256, + **{'content-type': 'application/octet-stream'}) if not current_image: current_image = self.get_image(name) # TODO(mordred): Can we do something similar to what nodepool does @@ -5972,7 +5974,7 @@ def _finish_large_object_slo(self, endpoint, headers, manifest): return self._object_store_client.put( endpoint, params={'multipart-manifest': 'put'}, - headers=headers, json=manifest) + headers=headers, data=json.dumps(manifest)) def _finish_large_object_dlo(self, endpoint, headers): headers = headers.copy() From 54e405552a1c3fc957f7537928dafafc31f9daca Mon Sep 17 00:00:00 2001 From: tengqm Date: Thu, 15 Dec 2016 01:30:19 -0500 Subject: [PATCH 1220/3836] Initial docs for bare-metal service Change-Id: Ie7304ef5272869060d520c23181f19de81da5006 --- doc/source/users/guides/bare_metal.rst | 9 +++++++++ doc/source/users/index.rst | 3 +++ doc/source/users/proxies/bare_metal.rst | 16 ++++++++++++++++ doc/source/users/resources/bare_metal/index.rst | 11 +++++++++++ .../users/resources/bare_metal/v1/chassis.rst | 12 ++++++++++++ .../users/resources/bare_metal/v1/driver.rst | 12 ++++++++++++ .../users/resources/bare_metal/v1/node.rst | 12 ++++++++++++ .../users/resources/bare_metal/v1/port.rst | 12 ++++++++++++ .../users/resources/bare_metal/v1/port_group.rst | 12 ++++++++++++ 9 files changed, 99 insertions(+) create mode 100644 doc/source/users/guides/bare_metal.rst create mode 100644 doc/source/users/proxies/bare_metal.rst create mode 100644 doc/source/users/resources/bare_metal/index.rst create mode 100644 doc/source/users/resources/bare_metal/v1/chassis.rst create mode 100644 doc/source/users/resources/bare_metal/v1/driver.rst create mode 100644 doc/source/users/resources/bare_metal/v1/node.rst create mode 100644 doc/source/users/resources/bare_metal/v1/port.rst create mode 100644 doc/source/users/resources/bare_metal/v1/port_group.rst diff --git a/doc/source/users/guides/bare_metal.rst b/doc/source/users/guides/bare_metal.rst new file mode 100644 index 000000000..10e96561c --- /dev/null +++ b/doc/source/users/guides/bare_metal.rst @@ -0,0 +1,9 @@ +Using OpenStack Bare Metal +=========================== + +Before working with the Bare Metal service, you'll need to create a +connection to your OpenStack cloud by following the :doc:`connect` user +guide. This will provide you with the ``conn`` variable used in the examples +below. + +.. TODO(Qiming): Implement this guide diff --git a/doc/source/users/index.rst b/doc/source/users/index.rst index b03cb4c92..b4a97c61f 100644 --- a/doc/source/users/index.rst +++ b/doc/source/users/index.rst @@ -28,6 +28,7 @@ approach, this is where you'll want to begin. Connect to an OpenStack Cloud Connect to an OpenStack Cloud Using a Config File Logging + Bare Metal Block Store Cluster Compute @@ -69,6 +70,7 @@ but listed below are the ones provided by this SDK by default. .. toctree:: :maxdepth: 1 + Bare Metal Block Store Cluster Compute @@ -96,6 +98,7 @@ The following services have exposed *Resource* classes. .. toctree:: :maxdepth: 1 + Bare Metal Block Store Cluster Compute diff --git a/doc/source/users/proxies/bare_metal.rst b/doc/source/users/proxies/bare_metal.rst new file mode 100644 index 000000000..58c7d23be --- /dev/null +++ b/doc/source/users/proxies/bare_metal.rst @@ -0,0 +1,16 @@ +Bare Metal API +============== + +For details on how to use bare_metal, see :doc:`/users/guides/bare_metal` + +.. automodule:: openstack.bare_metal.v1._proxy + +The BareMetal Class +-------------------- + +The bare_metal high-level interface is available through the ``bare_metal`` +member of a :class:`~openstack.connection.Connection` object. +The ``bare_metal`` member will only be added if the service is detected. + +.. autoclass:: openstack.bare_metal.v1._proxy.Proxy + :members: diff --git a/doc/source/users/resources/bare_metal/index.rst b/doc/source/users/resources/bare_metal/index.rst new file mode 100644 index 000000000..4aa391c86 --- /dev/null +++ b/doc/source/users/resources/bare_metal/index.rst @@ -0,0 +1,11 @@ +Bare Metal Resources +===================== + +.. toctree:: + :maxdepth: 1 + + v1/driver + v1/chassis + v1/node + v1/port + v1/portgroup diff --git a/doc/source/users/resources/bare_metal/v1/chassis.rst b/doc/source/users/resources/bare_metal/v1/chassis.rst new file mode 100644 index 000000000..303896378 --- /dev/null +++ b/doc/source/users/resources/bare_metal/v1/chassis.rst @@ -0,0 +1,12 @@ +openstack.bare_metal.v1.chassis +=============================== + +.. automodule:: openstack.bare_metal.v1.chassis + +The Chassis Class +----------------- + +The ``Chassis`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.bare_metal.v1.chassis.Chassis + :members: diff --git a/doc/source/users/resources/bare_metal/v1/driver.rst b/doc/source/users/resources/bare_metal/v1/driver.rst new file mode 100644 index 000000000..d45379e38 --- /dev/null +++ b/doc/source/users/resources/bare_metal/v1/driver.rst @@ -0,0 +1,12 @@ +openstack.bare_metal.v1.driver +============================== + +.. automodule:: openstack.bare_metal.v1.driver + +The Driver Class +---------------- + +The ``Driver`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.bare_metal.v1.driver.Driver + :members: diff --git a/doc/source/users/resources/bare_metal/v1/node.rst b/doc/source/users/resources/bare_metal/v1/node.rst new file mode 100644 index 000000000..7900c5598 --- /dev/null +++ b/doc/source/users/resources/bare_metal/v1/node.rst @@ -0,0 +1,12 @@ +openstack.bare_metal.v1.Node +============================ + +.. automodule:: openstack.bare_metal.v1.node + +The Node Class +-------------- + +The ``Node`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.bare_metal.v1.node.Node + :members: diff --git a/doc/source/users/resources/bare_metal/v1/port.rst b/doc/source/users/resources/bare_metal/v1/port.rst new file mode 100644 index 000000000..b0ed31b47 --- /dev/null +++ b/doc/source/users/resources/bare_metal/v1/port.rst @@ -0,0 +1,12 @@ +openstack.bare_metal.v1.port +============================ + +.. automodule:: openstack.bare_metal.v1.port + +The Port Class +-------------- + +The ``Port`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.bare_metal.v1.port.Port + :members: diff --git a/doc/source/users/resources/bare_metal/v1/port_group.rst b/doc/source/users/resources/bare_metal/v1/port_group.rst new file mode 100644 index 000000000..3feb4e2f4 --- /dev/null +++ b/doc/source/users/resources/bare_metal/v1/port_group.rst @@ -0,0 +1,12 @@ +openstack.bare_metal.v1.port_group +================================== + +.. automodule:: openstack.bare_metal.v1.port_group + +The PortGroup Class +------------------- + +The ``PortGroup`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.bare_metal.v1.port_group.PortGroup + :members: From 6854e05a5f5e90e1ba2b05d9dd1cc9a547b71395 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Tue, 24 Jan 2017 12:17:46 -0500 Subject: [PATCH 1221/3836] Add docs for the workflow service Initial docs for workflow. Doesn't include a user guide as I don't know how to use it. Change-Id: I7fa19fbb4cd6684782b8fc7a424d871a8b5d93ad --- doc/source/users/index.rst | 2 ++ doc/source/users/proxies/workflow.rst | 16 ++++++++++++++++ doc/source/users/resources/workflow/index.rst | 8 ++++++++ .../users/resources/workflow/v2/execution.rst | 12 ++++++++++++ .../users/resources/workflow/v2/workflow.rst | 12 ++++++++++++ openstack/workflow/v2/execution.py | 11 +++++++++++ openstack/workflow/v2/workflow.py | 10 ++++++++++ 7 files changed, 71 insertions(+) create mode 100644 doc/source/users/proxies/workflow.rst create mode 100644 doc/source/users/resources/workflow/index.rst create mode 100644 doc/source/users/resources/workflow/v2/execution.rst create mode 100644 doc/source/users/resources/workflow/v2/workflow.rst diff --git a/doc/source/users/index.rst b/doc/source/users/index.rst index b03cb4c92..1d60513c9 100644 --- a/doc/source/users/index.rst +++ b/doc/source/users/index.rst @@ -80,6 +80,7 @@ but listed below are the ones provided by this SDK by default. Object Store Orchestration Telemetry + Workflow Resource Interface ****************** @@ -108,6 +109,7 @@ The following services have exposed *Resource* classes. Orchestration Object Store Telemetry + Workflow Low-Level Classes ***************** diff --git a/doc/source/users/proxies/workflow.rst b/doc/source/users/proxies/workflow.rst new file mode 100644 index 000000000..9fea0c466 --- /dev/null +++ b/doc/source/users/proxies/workflow.rst @@ -0,0 +1,16 @@ +Workflow API +============ + +For details on how to use block_store, see :doc:`/users/guides/block_store` + +.. automodule:: openstack.workflow.v2._proxy + +The Workflow Class +------------------ + +The workflow high-level interface is available through the ``workflow`` +member of a :class:`~openstack.connection.Connection` object. +The ``workflow`` member will only be added if the service is detected. + +.. autoclass:: openstack.workflow.v2._proxy.Proxy + :members: diff --git a/doc/source/users/resources/workflow/index.rst b/doc/source/users/resources/workflow/index.rst new file mode 100644 index 000000000..30221b15d --- /dev/null +++ b/doc/source/users/resources/workflow/index.rst @@ -0,0 +1,8 @@ +Object Store Resources +====================== + +.. toctree:: + :maxdepth: 1 + + v2/execution + v2/workflow diff --git a/doc/source/users/resources/workflow/v2/execution.rst b/doc/source/users/resources/workflow/v2/execution.rst new file mode 100644 index 000000000..a0f2aac65 --- /dev/null +++ b/doc/source/users/resources/workflow/v2/execution.rst @@ -0,0 +1,12 @@ +openstack.workflow.v2.execution +=============================== + +.. automodule:: openstack.workflow.v2.execution + +The Execution Class +------------------- + +The ``Execution`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.workflow.v2.execution + :members: diff --git a/doc/source/users/resources/workflow/v2/workflow.rst b/doc/source/users/resources/workflow/v2/workflow.rst new file mode 100644 index 000000000..115cb9f33 --- /dev/null +++ b/doc/source/users/resources/workflow/v2/workflow.rst @@ -0,0 +1,12 @@ +openstack.workflow.v2.workflow +============================== + +.. automodule:: openstack.workflow.v2.workflow + +The Workflow Class +------------------ + +The ``Workflow`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.workflow.v2.workflow + :members: diff --git a/openstack/workflow/v2/execution.py b/openstack/workflow/v2/execution.py index ef4a4a394..d54160f1e 100644 --- a/openstack/workflow/v2/execution.py +++ b/openstack/workflow/v2/execution.py @@ -30,15 +30,26 @@ class Execution(resource.Resource): 'marker', 'limit', 'sort_keys', 'sort_dirs', 'fields', 'params', 'include_output') + #: The name of the workflow workflow_name = resource.Body("workflow_name") + #: The ID of the workflow workflow_id = resource.Body("workflow_id") + #: A description of the workflow execution description = resource.Body("description") + #: A reference to the parent task execution task_execution_id = resource.Body("task_execution_id") + #: Status can be one of: IDLE, RUNNING, SUCCESS, ERROR, or PAUSED status = resource.Body("state") + #: An optional information string about the status status_info = resource.Body("state_info") + #: A JSON structure containing workflow input values + # TODO(briancurtin): type=dict input = resource.Body("input") + #: The output of the workflow output = resource.Body("output") + #: The time at which the Execution was created created_at = resource.Body("created_at") + #: The time at which the Execution was updated updated_at = resource.Body("updated_at") def create(self, session, prepend_key=True): diff --git a/openstack/workflow/v2/workflow.py b/openstack/workflow/v2/workflow.py index 3039dd7df..6624c87b3 100644 --- a/openstack/workflow/v2/workflow.py +++ b/openstack/workflow/v2/workflow.py @@ -29,13 +29,23 @@ class Workflow(resource.Resource): _query_mapping = resource.QueryParameters( 'marker', 'limit', 'sort_keys', 'sort_dirs', 'fields') + #: The name of this Workflow name = resource.Body("name") + #: The inputs for this Workflow input = resource.Body("input") + #: A Workflow definition using the Mistral v2 DSL definition = resource.Body("definition") + #: A list of values associated with a workflow that users can use + #: to group workflows by some criteria + # TODO(briancurtin): type=list tags = resource.Body("tags") + #: Can be either "private" or "public" scope = resource.Body("scope") + #: The ID of the associated project project_id = resource.Body("project_id") + #: The time at which the workflow was created created_at = resource.Body("created_at") + #: The time at which the workflow was created updated_at = resource.Body("updated_at") def create(self, session, prepend_key=True): From 95de0fc56f3afb9e4b6c3684f2dab93c43ba528c Mon Sep 17 00:00:00 2001 From: tengqm Date: Mon, 30 Jan 2017 09:31:55 -0500 Subject: [PATCH 1222/3836] Add 'tags' property to orchestration stack The 'tags' property was missing from the current resource class. Change-Id: I65f710e36b067925f586771f45ef521573e13ca1 --- openstack/orchestration/v1/stack.py | 2 ++ openstack/tests/unit/orchestration/v1/test_stack.py | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/openstack/orchestration/v1/stack.py b/openstack/orchestration/v1/stack.py index 397cd2434..5445d11fc 100644 --- a/openstack/orchestration/v1/stack.py +++ b/openstack/orchestration/v1/stack.py @@ -60,6 +60,8 @@ class Stack(resource.Resource): status = resource.Body('stack_status') #: A text explaining how the stack transits to its current status. status_reason = resource.Body('stack_status_reason') + #: A list of strings used as tags on the stack + tags = resource.Body('tags') #: A dict containing the template use for stack creation. template = resource.Body('template', type=dict) #: Stack template description text. Currently contains the same text diff --git a/openstack/tests/unit/orchestration/v1/test_stack.py b/openstack/tests/unit/orchestration/v1/test_stack.py index 13ed0d292..4ca0d2bad 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack.py +++ b/openstack/tests/unit/orchestration/v1/test_stack.py @@ -36,6 +36,7 @@ 'name': FAKE_NAME, 'status': '11', 'status_reason': '12', + 'tags': ['FOO', 'bar:1'], 'template_description': '13', 'template_url': 'http://www.example.com/wordpress.yaml', 'timeout_mins': '14', @@ -78,8 +79,8 @@ def test_make_it(self): self.assertEqual(FAKE['parameters'], sot.parameters) self.assertEqual(FAKE['name'], sot.name) self.assertEqual(FAKE['status'], sot.status) - self.assertEqual(FAKE['status_reason'], - sot.status_reason) + self.assertEqual(FAKE['status_reason'], sot.status_reason) + self.assertEqual(FAKE['tags'], sot.tags) self.assertEqual(FAKE['template_description'], sot.template_description) self.assertEqual(FAKE['template_url'], sot.template_url) From 4662a9f2be83432aafffd33a2ff8040d726a7d77 Mon Sep 17 00:00:00 2001 From: tengqm Date: Tue, 31 Jan 2017 01:56:16 -0500 Subject: [PATCH 1223/3836] Fix exception parsing error The JSON body from a HTTP exception may contain non-dict values. Previous attempt to fix exception parsing is failing this case. This patch fixes it. Closes-Bug: #1660531 Change-Id: I560439167cf6384ef17ce429d663279502bf9b18 --- openstack/exceptions.py | 3 ++- openstack/tests/unit/test_session.py | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/openstack/exceptions.py b/openstack/exceptions.py index 9fa8c87fa..069f020d1 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -125,7 +125,8 @@ def from_exception(exc): content_type = resp.headers.get('content-type', '') if resp_body and 'application/json' in content_type: # Iterate over the nested objects to retrieve "message" attribute. - messages = [obj.get('message') for obj in resp.json().values()] + messages = [obj.get('message') for obj in resp.json().values() + if isinstance(obj, dict)] # Join all of the messages together nicely and filter out any objects # that don't have a "message" attr. details = '\n'.join(msg for msg in messages if msg) diff --git a/openstack/tests/unit/test_session.py b/openstack/tests/unit/test_session.py index a3bdb74c1..528fe507d 100644 --- a/openstack/tests/unit/test_session.py +++ b/openstack/tests/unit/test_session.py @@ -137,6 +137,27 @@ def test_map_exceptions_http_exception_handle_json(self): self.assertIn('OverLimit413...', os_exc.details) self.assertIn('OverLimit Retry...', os_exc.details) + def test_map_exceptions_http_exception_handle_json_1(self): + # A test for json containing non-dict values + mock_resp = mock.Mock() + mock_resp.status_code = 404 + mock_resp.json.return_value = { + "code": 404, + "error": { + "message": "resource not found", + }, + } + mock_resp.headers = { + "content-type": "application/json" + } + ksa_exc = _exceptions.HttpError(message="test", http_status=404, + response=mock_resp) + func = mock.Mock(side_effect=ksa_exc) + + os_exc = self._assert_map_exceptions( + exceptions.HttpException, ksa_exc, func) + self.assertIn('not found', os_exc.details) + def test_map_exceptions_notfound_exception_handle_html(self): mock_resp = mock.Mock() mock_resp.status_code = 404 From fa0a133a2f477836f912ae0d34da8d799b5f4f51 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 21 Jan 2017 17:31:03 +0100 Subject: [PATCH 1224/3836] Copy in needed template processing utils from heatclient There is a LOT of client-side file processing that goes on before uploading heat templates to the cloud. Rather than reimplement it all from scratch, copy in the relevant bits. With this done, removing heatclient itself should be fairly easy. This will also allow us to fix a long-standing bug, which is that heat event polling bypassed the TaskManager since the utils make their own API calls. Depends-On: Id608025d610de2099d7be37dcff35de33c10b9d5 Change-Id: I384f81b6198f874e78a434515123f955017e0172 --- shade/_heat/__init__.py | 0 shade/_heat/environment_format.py | 56 ++++++ shade/_heat/event_utils.py | 177 +++++++++++++++++ shade/_heat/template_format.py | 69 +++++++ shade/_heat/template_utils.py | 314 ++++++++++++++++++++++++++++++ shade/_heat/utils.py | 61 ++++++ shade/openstackcloud.py | 4 +- shade/tests/unit/test_stack.py | 5 +- 8 files changed, 681 insertions(+), 5 deletions(-) create mode 100644 shade/_heat/__init__.py create mode 100644 shade/_heat/environment_format.py create mode 100644 shade/_heat/event_utils.py create mode 100644 shade/_heat/template_format.py create mode 100644 shade/_heat/template_utils.py create mode 100644 shade/_heat/utils.py diff --git a/shade/_heat/__init__.py b/shade/_heat/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/shade/_heat/environment_format.py b/shade/_heat/environment_format.py new file mode 100644 index 000000000..56bc2c1c0 --- /dev/null +++ b/shade/_heat/environment_format.py @@ -0,0 +1,56 @@ +# 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 yaml + +from shade._heat import template_format + + +SECTIONS = ( + PARAMETER_DEFAULTS, PARAMETERS, RESOURCE_REGISTRY, + ENCRYPTED_PARAM_NAMES, EVENT_SINKS, + PARAMETER_MERGE_STRATEGIES +) = ( + 'parameter_defaults', 'parameters', 'resource_registry', + 'encrypted_param_names', 'event_sinks', + 'parameter_merge_strategies' +) + + +def parse(env_str): + """Takes a string and returns a dict containing the parsed structure. + + This includes determination of whether the string is using the + YAML format. + """ + try: + env = yaml.load(env_str, Loader=template_format.yaml_loader) + except yaml.YAMLError: + # NOTE(prazumovsky): we need to return more informative error for + # user, so use SafeLoader, which return error message with template + # snippet where error has been occurred. + try: + env = yaml.load(env_str, Loader=yaml.SafeLoader) + except yaml.YAMLError as yea: + raise ValueError(yea) + else: + if env is None: + env = {} + elif not isinstance(env, dict): + raise ValueError( + 'The environment is not a valid YAML mapping data type.') + + for param in env: + if param not in SECTIONS: + raise ValueError('environment has wrong section "%s"' % param) + + return env diff --git a/shade/_heat/event_utils.py b/shade/_heat/event_utils.py new file mode 100644 index 000000000..ab3c27cd0 --- /dev/null +++ b/shade/_heat/event_utils.py @@ -0,0 +1,177 @@ +# Copyright 2015 Red Hat 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 time + +from shade._heat import utils +import heatclient.exc as exc + + +def get_events(hc, stack_id, event_args, nested_depth=0, + marker=None, limit=None): + event_args = dict(event_args) + if marker: + event_args['marker'] = marker + if limit: + event_args['limit'] = limit + if not nested_depth: + # simple call with no nested_depth + return _get_stack_events(hc, stack_id, event_args) + + # assume an API which supports nested_depth + event_args['nested_depth'] = nested_depth + events = _get_stack_events(hc, stack_id, event_args) + + if not events: + return events + + first_links = getattr(events[0], 'links', []) + root_stack_link = [l for l in first_links + if l.get('rel') == 'root_stack'] + if root_stack_link: + # response has a root_stack link, indicating this is an API which + # supports nested_depth + return events + + # API doesn't support nested_depth, do client-side paging and recursive + # event fetch + marker = event_args.pop('marker', None) + limit = event_args.pop('limit', None) + event_args.pop('nested_depth', None) + events = _get_stack_events(hc, stack_id, event_args) + events.extend(_get_nested_events(hc, nested_depth, + stack_id, event_args)) + # Because there have been multiple stacks events mangled into + # one list, we need to sort before passing to print_list + # Note we can't use the prettytable sortby_index here, because + # the "start" option doesn't allow post-sort slicing, which + # will be needed to make "--marker" work for nested_depth lists + events.sort(key=lambda x: x.event_time) + + # Slice the list if marker is specified + if marker: + try: + marker_index = [e.id for e in events].index(marker) + events = events[marker_index:] + except ValueError: + pass + + # Slice the list if limit is specified + if limit: + limit_index = min(int(limit), len(events)) + events = events[:limit_index] + return events + + +def _get_nested_ids(hc, stack_id): + nested_ids = [] + try: + resources = hc.resources.list(stack_id=stack_id) + except exc.HTTPNotFound: + raise exc.CommandError('Stack not found: %s' % stack_id) + for r in resources: + nested_id = utils.resource_nested_identifier(r) + if nested_id: + nested_ids.append(nested_id) + return nested_ids + + +def _get_nested_events(hc, nested_depth, stack_id, event_args): + # FIXME(shardy): this is very inefficient, we should add nested_depth to + # the event_list API in a future heat version, but this will be required + # until kilo heat is EOL. + nested_ids = _get_nested_ids(hc, stack_id) + nested_events = [] + for n_id in nested_ids: + stack_events = _get_stack_events(hc, n_id, event_args) + if stack_events: + nested_events.extend(stack_events) + if nested_depth > 1: + next_depth = nested_depth - 1 + nested_events.extend(_get_nested_events( + hc, next_depth, n_id, event_args)) + return nested_events + + +def _get_stack_events(hc, stack_id, event_args): + event_args['stack_id'] = stack_id + try: + events = hc.events.list(**event_args) + except exc.HTTPNotFound as ex: + # it could be the stack or resource that is not found + # just use the message that the server sent us. + raise exc.CommandError(str(ex)) + else: + # Show which stack the event comes from (for nested events) + for e in events: + e.stack_name = stack_id.split("/")[0] + return events + + +def poll_for_events(hc, stack_name, action=None, poll_period=5, marker=None, + nested_depth=0): + """Continuously poll events and logs for performed action on stack.""" + + if action: + stop_status = ('%s_FAILED' % action, '%s_COMPLETE' % action) + stop_check = lambda a: a in stop_status + else: + stop_check = lambda a: a.endswith('_COMPLETE') or a.endswith('_FAILED') + + no_event_polls = 0 + msg_template = "\n Stack %(name)s %(status)s \n" + + def is_stack_event(event): + if getattr(event, 'resource_name', '') != stack_name: + return False + + phys_id = getattr(event, 'physical_resource_id', '') + links = dict((l.get('rel'), + l.get('href')) for l in getattr(event, 'links', [])) + stack_id = links.get('stack', phys_id).rsplit('/', 1)[-1] + return stack_id == phys_id + + while True: + events = get_events(hc, stack_id=stack_name, nested_depth=nested_depth, + event_args={'sort_dir': 'asc', + 'marker': marker}) + + if len(events) == 0: + no_event_polls += 1 + else: + no_event_polls = 0 + # set marker to last event that was received. + marker = getattr(events[-1], 'id', None) + + for event in events: + # check if stack event was also received + if is_stack_event(event): + stack_status = getattr(event, 'resource_status', '') + msg = msg_template % dict( + name=stack_name, status=stack_status) + if stop_check(stack_status): + return stack_status, msg + + if no_event_polls >= 2: + # after 2 polls with no events, fall back to a stack get + stack = hc.stacks.get(stack_name, resolve_outputs=False) + stack_status = stack.stack_status + msg = msg_template % dict( + name=stack_name, status=stack_status) + if stop_check(stack_status): + return stack_status, msg + # go back to event polling again + no_event_polls = 0 + + time.sleep(poll_period) diff --git a/shade/_heat/template_format.py b/shade/_heat/template_format.py new file mode 100644 index 000000000..4bb6098dc --- /dev/null +++ b/shade/_heat/template_format.py @@ -0,0 +1,69 @@ +# 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 json +import yaml + +if hasattr(yaml, 'CSafeLoader'): + yaml_loader = yaml.CSafeLoader +else: + yaml_loader = yaml.SafeLoader + +if hasattr(yaml, 'CSafeDumper'): + yaml_dumper = yaml.CSafeDumper +else: + yaml_dumper = yaml.SafeDumper + + +def _construct_yaml_str(self, node): + # Override the default string handling function + # to always return unicode objects + return self.construct_scalar(node) +yaml_loader.add_constructor(u'tag:yaml.org,2002:str', _construct_yaml_str) +# Unquoted dates like 2013-05-23 in yaml files get loaded as objects of type +# datetime.data which causes problems in API layer when being processed by +# openstack.common.jsonutils. Therefore, make unicode string out of timestamps +# until jsonutils can handle dates. +yaml_loader.add_constructor(u'tag:yaml.org,2002:timestamp', + _construct_yaml_str) + + +def parse(tmpl_str): + """Takes a string and returns a dict containing the parsed structure. + + This includes determination of whether the string is using the + JSON or YAML format. + """ + # strip any whitespace before the check + tmpl_str = tmpl_str.strip() + if tmpl_str.startswith('{'): + tpl = json.loads(tmpl_str) + else: + try: + tpl = yaml.load(tmpl_str, Loader=yaml_loader) + except yaml.YAMLError: + # NOTE(prazumovsky): we need to return more informative error for + # user, so use SafeLoader, which return error message with template + # snippet where error has been occurred. + try: + tpl = yaml.load(tmpl_str, Loader=yaml.SafeLoader) + except yaml.YAMLError as yea: + raise ValueError(yea) + else: + if tpl is None: + tpl = {} + # Looking for supported version keys in the loaded template + if not ('HeatTemplateFormatVersion' in tpl + or 'heat_template_version' in tpl + or 'AWSTemplateFormatVersion' in tpl): + raise ValueError("Template format version not found.") + return tpl diff --git a/shade/_heat/template_utils.py b/shade/_heat/template_utils.py new file mode 100644 index 000000000..1b2aeb766 --- /dev/null +++ b/shade/_heat/template_utils.py @@ -0,0 +1,314 @@ +# Copyright 2012 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 collections +import json +import six +from six.moves.urllib import parse +from six.moves.urllib import request + +from shade._heat import environment_format +from shade._heat import template_format +from shade._heat import utils +from shade import exc + + +def get_template_contents(template_file=None, template_url=None, + template_object=None, object_request=None, + files=None, existing=False): + + is_object = False + tpl = None + + # Transform a bare file path to a file:// URL. + if template_file: + template_url = utils.normalise_file_path_to_url(template_file) + + if template_url: + tpl = request.urlopen(template_url).read() + + elif template_object: + is_object = True + template_url = template_object + tpl = object_request and object_request('GET', + template_object) + elif existing: + return {}, None + else: + raise exc.OpenStackCloudException( + 'Must provide one of template_file,' + ' template_url or template_object') + + if not tpl: + raise exc.OpenStackCloudException( + 'Could not fetch template from %s' % template_url) + + try: + if isinstance(tpl, six.binary_type): + tpl = tpl.decode('utf-8') + template = template_format.parse(tpl) + except ValueError as e: + raise exc.OpenStackCloudException( + 'Error parsing template %(url)s %(error)s' % + {'url': template_url, 'error': e}) + + tmpl_base_url = utils.base_url_for_url(template_url) + if files is None: + files = {} + resolve_template_get_files(template, files, tmpl_base_url, is_object, + object_request) + return files, template + + +def resolve_template_get_files(template, files, template_base_url, + is_object=False, object_request=None): + + def ignore_if(key, value): + if key != 'get_file' and key != 'type': + return True + if not isinstance(value, six.string_types): + return True + if (key == 'type' and + not value.endswith(('.yaml', '.template'))): + return True + return False + + def recurse_if(value): + return isinstance(value, (dict, list)) + + get_file_contents(template, files, template_base_url, + ignore_if, recurse_if, is_object, object_request) + + +def is_template(file_content): + try: + if isinstance(file_content, six.binary_type): + file_content = file_content.decode('utf-8') + template_format.parse(file_content) + except (ValueError, TypeError): + return False + return True + + +def get_file_contents(from_data, files, base_url=None, + ignore_if=None, recurse_if=None, + is_object=False, object_request=None): + + if recurse_if and recurse_if(from_data): + if isinstance(from_data, dict): + recurse_data = six.itervalues(from_data) + else: + recurse_data = from_data + for value in recurse_data: + get_file_contents(value, files, base_url, ignore_if, recurse_if, + is_object, object_request) + + if isinstance(from_data, dict): + for key, value in six.iteritems(from_data): + if ignore_if and ignore_if(key, value): + continue + + if base_url and not base_url.endswith('/'): + base_url = base_url + '/' + + str_url = parse.urljoin(base_url, value) + if str_url not in files: + if is_object and object_request: + file_content = object_request('GET', str_url) + else: + file_content = utils.read_url_content(str_url) + if is_template(file_content): + if is_object: + template = get_template_contents( + template_object=str_url, files=files, + object_request=object_request)[1] + else: + template = get_template_contents( + template_url=str_url, files=files)[1] + file_content = json.dumps(template) + files[str_url] = file_content + # replace the data value with the normalised absolute URL + from_data[key] = str_url + + +def deep_update(old, new): + '''Merge nested dictionaries.''' + + # Prevents an error if in a previous iteration + # old[k] = None but v[k] = {...}, + if old is None: + old = {} + + for k, v in new.items(): + if isinstance(v, collections.Mapping): + r = deep_update(old.get(k, {}), v) + old[k] = r + else: + old[k] = new[k] + return old + + +def process_multiple_environments_and_files(env_paths=None, template=None, + template_url=None, + env_path_is_object=None, + object_request=None, + env_list_tracker=None): + """Reads one or more environment files. + + Reads in each specified environment file and returns a dictionary + of the filenames->contents (suitable for the files dict) + and the consolidated environment (after having applied the correct + overrides based on order). + + If a list is provided in the env_list_tracker parameter, the behavior + is altered to take advantage of server-side environment resolution. + Specifically, this means: + + * Populating env_list_tracker with an ordered list of environment file + URLs to be passed to the server + * Including the contents of each environment file in the returned + files dict, keyed by one of the URLs in env_list_tracker + + :param env_paths: list of paths to the environment files to load; if + None, empty results will be returned + :type env_paths: list or None + :param template: unused; only included for API compatibility + :param template_url: unused; only included for API compatibility + :param env_list_tracker: if specified, environment filenames will be + stored within + :type env_list_tracker: list or None + :return: tuple of files dict and a dict of the consolidated environment + :rtype: tuple + """ + merged_files = {} + merged_env = {} + + # If we're keeping a list of environment files separately, include the + # contents of the files in the files dict + include_env_in_files = env_list_tracker is not None + + if env_paths: + for env_path in env_paths: + files, env = process_environment_and_files( + env_path=env_path, + template=template, + template_url=template_url, + env_path_is_object=env_path_is_object, + object_request=object_request, + include_env_in_files=include_env_in_files) + + # 'files' looks like {"filename1": contents, "filename2": contents} + # so a simple update is enough for merging + merged_files.update(files) + + # 'env' can be a deeply nested dictionary, so a simple update is + # not enough + merged_env = deep_update(merged_env, env) + + if env_list_tracker is not None: + env_url = utils.normalise_file_path_to_url(env_path) + env_list_tracker.append(env_url) + + return merged_files, merged_env + + +def process_environment_and_files(env_path=None, + template=None, + template_url=None, + env_path_is_object=None, + object_request=None, + include_env_in_files=False): + """Loads a single environment file. + + Returns an entry suitable for the files dict which maps the environment + filename to its contents. + + :param env_path: full path to the file to load + :type env_path: str or None + :param include_env_in_files: if specified, the raw environment file itself + will be included in the returned files dict + :type include_env_in_files: bool + :return: tuple of files dict and the loaded environment as a dict + :rtype: (dict, dict) + """ + files = {} + env = {} + + is_object = env_path_is_object and env_path_is_object(env_path) + + if is_object: + raw_env = object_request and object_request('GET', env_path) + env = environment_format.parse(raw_env) + env_base_url = utils.base_url_for_url(env_path) + + resolve_environment_urls( + env.get('resource_registry'), + files, + env_base_url, is_object=True, object_request=object_request) + + elif env_path: + env_url = utils.normalise_file_path_to_url(env_path) + env_base_url = utils.base_url_for_url(env_url) + raw_env = request.urlopen(env_url).read() + + env = environment_format.parse(raw_env) + + resolve_environment_urls( + env.get('resource_registry'), + files, + env_base_url) + + if include_env_in_files: + files[env_url] = json.dumps(env) + + return files, env + + +def resolve_environment_urls(resource_registry, files, env_base_url, + is_object=False, object_request=None): + """Handles any resource URLs specified in an environment. + + :param resource_registry: mapping of type name to template filename + :type resource_registry: dict + :param files: dict to store loaded file contents into + :type files: dict + :param env_base_url: base URL to look in when loading files + :type env_base_url: str or None + """ + if resource_registry is None: + return + + rr = resource_registry + base_url = rr.get('base_url', env_base_url) + + def ignore_if(key, value): + if key == 'base_url': + return True + if isinstance(value, dict): + return True + if '::' in value: + # Built in providers like: "X::Compute::Server" + # don't need downloading. + return True + if key in ['hooks', 'restricted_actions']: + return True + + get_file_contents(rr, files, base_url, ignore_if, + is_object=is_object, object_request=object_request) + + for res_name, res_dict in six.iteritems(rr.get('resources', {})): + res_base_url = res_dict.get('base_url', base_url) + get_file_contents( + res_dict, files, res_base_url, ignore_if, + is_object=is_object, object_request=object_request) diff --git a/shade/_heat/utils.py b/shade/_heat/utils.py new file mode 100644 index 000000000..24cb0b071 --- /dev/null +++ b/shade/_heat/utils.py @@ -0,0 +1,61 @@ +# Copyright 2012 OpenStack Foundation +# 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 base64 +import os + +from six.moves.urllib import error +from six.moves.urllib import parse +from six.moves.urllib import request + +from shade import exc + + +def base_url_for_url(url): + parsed = parse.urlparse(url) + parsed_dir = os.path.dirname(parsed.path) + return parse.urljoin(url, parsed_dir) + + +def normalise_file_path_to_url(path): + if parse.urlparse(path).scheme: + return path + path = os.path.abspath(path) + return parse.urljoin('file:', request.pathname2url(path)) + + +def read_url_content(url): + try: + # TODO(mordred) Use requests + content = request.urlopen(url).read() + except error.URLError: + raise exc.OpenStackCloudException( + 'Could not fetch contents for %s' % url) + + if content: + try: + content.decode('utf-8') + except ValueError: + content = base64.encodestring(content) + return content + + +def resource_nested_identifier(rsrc): + nested_link = [l for l in rsrc.links or [] + if l.get('rel') == 'nested'] + if nested_link: + nested_href = nested_link[0].get('href') + nested_identifier = nested_href.split("/")[-2:] + return "/".join(nested_identifier) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index e695c33d7..7d6f18f23 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -31,8 +31,6 @@ import cinderclient.exceptions as cinder_exceptions import heatclient.client import magnumclient.exceptions as magnum_exceptions -from heatclient.common import event_utils -from heatclient.common import template_utils from heatclient import exc as heat_exceptions import keystoneauth1.exceptions import keystoneclient.client @@ -46,6 +44,8 @@ from shade.exc import * # noqa from shade import _adapter +from shade._heat import event_utils +from shade._heat import template_utils from shade import _log from shade import _normalize from shade import meta diff --git a/shade/tests/unit/test_stack.py b/shade/tests/unit/test_stack.py index 62a3458b4..f0d1e2f51 100644 --- a/shade/tests/unit/test_stack.py +++ b/shade/tests/unit/test_stack.py @@ -14,10 +14,9 @@ import mock import testtools -from heatclient.common import event_utils -from heatclient.common import template_utils - import shade +from shade._heat import event_utils +from shade._heat import template_utils from shade import meta from shade.tests import fakes from shade.tests.unit import base From 02116c41ef7cbf05698cfd01d28ce276d3f4f23d Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Tue, 31 Jan 2017 16:03:05 -0500 Subject: [PATCH 1225/3836] fix location of team tags in README Remove the extraneous title markup and move the team tag include instructions below the main project title in the readme so it renders more nicely. Change-Id: Icd384c81a455a3e1a86abd1f2ef84e775e06c307 Signed-off-by: Doug Hellmann --- README.rst | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index 9e460be85..67aa91a34 100644 --- a/README.rst +++ b/README.rst @@ -1,16 +1,10 @@ -======================== -Team and repository tags -======================== - -.. image:: http://governance.openstack.org/badges/os-client-config.svg - :target: http://governance.openstack.org/reference/tags/index.html - -.. Change things from this point on - ================ os-client-config ================ +.. image:: http://governance.openstack.org/badges/os-client-config.svg + :target: http://governance.openstack.org/reference/tags/index.html + `os-client-config` is a library for collecting client configuration for using an OpenStack cloud in a consistent and comprehensive manner. It will find cloud config for as few as 1 cloud and as many as you want to From 707adab1bc8726a7bf9c910e5bd2650bfe420ad1 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Tue, 31 Jan 2017 21:04:24 +0000 Subject: [PATCH 1226/3836] Update reno for stable/ocata Change-Id: Iace25f1919632b5de8d6bf81add7ce0416a874ef --- releasenotes/source/index.rst | 1 + releasenotes/source/ocata.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/ocata.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index f708ec8d3..22609515d 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -9,6 +9,7 @@ Contents :maxdepth: 2 unreleased + ocata newton mitaka diff --git a/releasenotes/source/ocata.rst b/releasenotes/source/ocata.rst new file mode 100644 index 000000000..ebe62f42e --- /dev/null +++ b/releasenotes/source/ocata.rst @@ -0,0 +1,6 @@ +=================================== + Ocata Series Release Notes +=================================== + +.. release-notes:: + :branch: origin/stable/ocata From 16a058f16e552668d6db8d806242dd1211559a6d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 26 Jan 2017 15:09:44 -0600 Subject: [PATCH 1227/3836] Process json based on content-type OpenStack provides content-type which we can respond to. That we do not currently provide it in our unittests is our fault. So let's provide it. Change-Id: Ib20f4df950cbedf404c0fbe3ef4c39660eb1b70f Depends-On: Iaef2a140e33fc48f8bfa8ff4769eded37ce152c6 --- shade/_adapter.py | 65 ++- shade/tests/unit/base.py | 6 + shade/tests/unit/test_object.py | 783 +++++++++++--------------------- 3 files changed, 308 insertions(+), 546 deletions(-) diff --git a/shade/_adapter.py b/shade/_adapter.py index c9f6f0afb..ed431728e 100644 --- a/shade/_adapter.py +++ b/shade/_adapter.py @@ -90,43 +90,38 @@ def __init__(self, shade_logger, manager, *args, **kwargs): def _munch_response(self, response, result_key=None): exc.raise_from_response(response) - # Glance image downloads just return the data in the body - if response.headers.get('Content-Type') in ( - 'text/plain', - 'application/octet-stream'): + + if not response.content: + # This doens't have any content + return response + + # Some REST calls do not return json content. Don't decode it. + if 'application/json' not in response.headers.get('Content-Type'): + return response + + try: + result_json = response.json() + except Exception: return response + + request_id = response.headers.get('x-openstack-request-id') + + if task_manager._is_listlike(result_json): + return meta.obj_list_to_dict( + result_json, request_id=request_id) + + # Wrap the keys() call in list() because in python3 keys returns + # a "dict_keys" iterator-like object rather than a list + json_keys = list(result_json.keys()) + if len(json_keys) > 1 and result_key: + result = result_json[result_key] + elif len(json_keys) == 1: + result = result_json[json_keys[0]] else: - if not response.content: - # This doens't have any content - return response - try: - result_json = response.json() - except Exception: - raise exc.OpenStackCloudHTTPError( - "Problems decoding json from response." - " Reponse: {code} {reason}".format( - code=response.status_code, - reason=response.reason), - response=response) - - request_id = response.headers.get('x-openstack-request-id') - - if task_manager._is_listlike(result_json): - return meta.obj_list_to_dict( - result_json, request_id=request_id) - - # Wrap the keys() call in list() because in python3 keys returns - # a "dict_keys" iterator-like object rather than a list - json_keys = list(result_json.keys()) - if len(json_keys) > 1 and result_key: - result = result_json[result_key] - elif len(json_keys) == 1: - result = result_json[json_keys[0]] - else: - # Passthrough the whole body - sometimes (hi glance) things - # come through without a top-level container. Also, sometimes - # you need to deal with pagination - result = result_json + # Passthrough the whole body - sometimes (hi glance) things + # come through without a top-level container. Also, sometimes + # you need to deal with pagination + result = result_json if task_manager._is_listlike(result): return meta.obj_list_to_dict(result, request_id=request_id) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 02d47c4e9..98ac217b4 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -19,6 +19,7 @@ import mock import os import os_client_config as occ +from requests import structures from requests_mock.contrib import fixture as rm_fixture import tempfile @@ -167,6 +168,11 @@ def use_glance(self, image_version_json='image-version.json'): def register_uri(self, method, uri, **kwargs): validate = kwargs.pop('validate', {}) key = '{method}:{uri}'.format(method=method, uri=uri) + headers = structures.CaseInsensitiveDict(kwargs.pop('headers', {})) + if 'content-type' not in headers: + headers[u'content-type'] = 'application/json' + kwargs['headers'] = headers + if key in self._uri_registry: self._uri_registry[key].append(kwargs) self.adapter.register_uri(method, uri, self._uri_registry[key]) diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index f3d4cac6d..3e1117e51 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -40,23 +40,11 @@ class TestObject(BaseTestObject): def test_create_container(self): """Test creating a (private) container""" - self.adapter.head( - self.container_endpoint, - [ - dict(status_code=404), - dict(headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}), - ]) - self.adapter.put( - self.container_endpoint, + self.register_uri( + 'HEAD', self.container_endpoint, status_code=404), + + self.register_uri( + 'PUT', self.container_endpoint, status_code=201, headers={ 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', @@ -64,65 +52,65 @@ def test_create_container(self): 'Content-Type': 'text/html; charset=UTF-8', }) - self.cloud.create_container(self.container) + self.register_uri( + 'HEAD', self.container_endpoint, + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}) - self.calls += [ - dict(method='HEAD', url=self.container_endpoint), - dict(method='PUT', url=self.container_endpoint), - dict(method='HEAD', url=self.container_endpoint), - ] + self.cloud.create_container(self.container) self.assert_calls() def test_create_container_public(self): """Test creating a public container""" - self.adapter.head( - self.container_endpoint, - [ - dict(status_code=404), - dict(headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}), - ]) - self.adapter.put( - self.container_endpoint, + self.register_uri( + 'HEAD', self.container_endpoint, + status_code=404) + + self.register_uri( + 'PUT', self.container_endpoint, status_code=201, headers={ 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', 'Content-Length': '0', 'Content-Type': 'text/html; charset=UTF-8', }) - self.adapter.post( - self.container_endpoint, - status_code=201) - - self.cloud.create_container(self.container, public=True) - self.calls += [ - dict(method='HEAD', url=self.container_endpoint), - dict( - method='PUT', - url=self.container_endpoint), - dict( - method='POST', - url=self.container_endpoint, + self.register_uri( + 'POST', self.container_endpoint, + status_code=201, + validate=dict( headers={ 'x-container-read': - shade.openstackcloud.OBJECT_CONTAINER_ACLS['public']}), - dict(method='HEAD', url=self.container_endpoint), - ] + shade.openstackcloud.OBJECT_CONTAINER_ACLS['public']})) + + self.register_uri( + 'HEAD', self.container_endpoint, + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}) + + self.cloud.create_container(self.container, public=True) self.assert_calls() def test_create_container_exists(self): """Test creating a container that exists.""" - self.adapter.head( - self.container_endpoint, + self.register_uri( + 'HEAD', self.container_endpoint, headers={ 'Content-Length': '0', 'X-Container-Object-Count': '0', @@ -136,65 +124,45 @@ def test_create_container_exists(self): container = self.cloud.create_container(self.container) - self.calls += [ - dict(method='HEAD', url=self.container_endpoint), - ] self.assert_calls() self.assertIsNotNone(container) def test_delete_container(self): - self.adapter.delete(self.container_endpoint) + self.register_uri('DELETE', self.container_endpoint) self.assertTrue(self.cloud.delete_container(self.container)) - - self.calls += [ - dict(method='DELETE', url=self.container_endpoint), - ] self.assert_calls() def test_delete_container_404(self): """No exception when deleting a container that does not exist""" - self.adapter.delete( - self.container_endpoint, + self.register_uri( + 'DELETE', self.container_endpoint, status_code=404) self.assertFalse(self.cloud.delete_container(self.container)) - - self.calls += [ - dict(method='DELETE', url=self.container_endpoint), - ] self.assert_calls() def test_delete_container_error(self): """Non-404 swift error re-raised as OSCE""" # 409 happens if the container is not empty - self.adapter.delete( - self.container_endpoint, + self.register_uri( + 'DELETE', self.container_endpoint, status_code=409) self.assertRaises( shade.OpenStackCloudException, self.cloud.delete_container, self.container) - self.calls += [ - dict(method='DELETE', url=self.container_endpoint), - ] self.assert_calls() def test_update_container(self): - self.adapter.post( - self.container_endpoint, - status_code=204) - headers = {'x-container-read': - shade.openstackcloud.OBJECT_CONTAINER_ACLS['public']} - assert_headers = headers.copy() + headers = { + 'x-container-read': + shade.openstackcloud.OBJECT_CONTAINER_ACLS['public']} + self.register_uri( + 'POST', self.container_endpoint, + status_code=204, + validate=dict(headers=headers)) self.cloud.update_container(self.container, headers) - - self.calls += [ - dict( - method='POST', - url=self.container_endpoint, - headers=assert_headers), - ] self.assert_calls() def test_update_container_error(self): @@ -204,45 +172,38 @@ def test_update_container_error(self): # method, and I cannot make a synthetic failure to validate a real # error code. So we're really just testing the shade adapter error # raising logic here, rather than anything specific to swift. - self.adapter.post( - self.container_endpoint, + self.register_uri( + 'POST', self.container_endpoint, status_code=409) self.assertRaises( shade.OpenStackCloudException, self.cloud.update_container, self.container, dict(foo='bar')) + self.assert_calls() def test_set_container_access_public(self): - self.adapter.post( - self.container_endpoint, - status_code=204) + self.register_uri( + 'POST', self.container_endpoint, + status_code=204, + validate=dict( + headers={ + 'x-container-read': + shade.openstackcloud.OBJECT_CONTAINER_ACLS['public']})) self.cloud.set_container_access(self.container, 'public') - self.calls += [ - dict( - method='POST', - url=self.container_endpoint, - headers={ - 'x-container-read': - shade.openstackcloud.OBJECT_CONTAINER_ACLS['public']}), - ] self.assert_calls() def test_set_container_access_private(self): - self.adapter.post( - self.container_endpoint, - status_code=204) + self.register_uri( + 'POST', self.container_endpoint, + status_code=204, + validate=dict( + headers={ + 'x-container-read': + shade.openstackcloud.OBJECT_CONTAINER_ACLS['private']})) self.cloud.set_container_access(self.container, 'private') - self.calls += [ - dict( - method='POST', - url=self.container_endpoint, - headers={ - 'x-container-read': - shade.openstackcloud.OBJECT_CONTAINER_ACLS['private']}), - ] self.assert_calls() def test_set_container_access_invalid(self): @@ -251,8 +212,8 @@ def test_set_container_access_invalid(self): self.cloud.set_container_access, self.container, 'invalid') def test_get_container_access(self): - self.adapter.head( - self.container_endpoint, + self.register_uri( + 'HEAD', self.container_endpoint, headers={ 'x-container-read': str(shade.openstackcloud.OBJECT_CONTAINER_ACLS['public'])}) @@ -261,8 +222,8 @@ def test_get_container_access(self): self.assertEqual('public', access) def test_get_container_invalid(self): - self.adapter.head( - self.container_endpoint, + self.register_uri( + 'HEAD', self.container_endpoint, headers={ 'x-container-read': 'invalid'}) @@ -273,8 +234,8 @@ def test_get_container_invalid(self): self.cloud.get_container_access(self.container) def test_get_container_access_not_found(self): - self.adapter.head( - self.container_endpoint, + self.register_uri( + 'HEAD', self.container_endpoint, status_code=404) with testtools.ExpectedException( exc.OpenStackCloudException, @@ -410,7 +371,8 @@ def test_get_object_segment_size_below_min(self): 'GET', 'https://object-store.example.com/info', json=dict( swift={'max_file_size': 1000}, - slo={'min_segment_size': 500})) + slo={'min_segment_size': 500}), + headers={'Content-Type': 'application/json'}) self.assertEqual(500, self.cloud.get_object_segment_size(400)) self.assertEqual(900, self.cloud.get_object_segment_size(900)) self.assertEqual(1000, self.cloud.get_object_segment_size(1000)) @@ -450,88 +412,64 @@ def setUp(self): def test_create_object(self): - self.adapter.get( - 'https://object-store.example.com/info', + self.register_uri( + 'GET', 'https://object-store.example.com/info', json=dict( swift={'max_file_size': 1000}, slo={'min_segment_size': 500})) - self.adapter.put( - '{endpoint}/{container}'.format( + self.register_uri( + 'HEAD', '{endpoint}/{container}'.format( endpoint=self.endpoint, - container=self.container,), + container=self.container), + status_code=404) + + self.register_uri( + 'PUT', '{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container), status_code=201, headers={ 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', 'Content-Length': '0', 'Content-Type': 'text/html; charset=UTF-8', }) - self.adapter.head( - '{endpoint}/{container}'.format( + self.register_uri( + 'HEAD', '{endpoint}/{container}'.format( endpoint=self.endpoint, container=self.container), - [ - dict(status_code=404), - dict(headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}), - ]) - self.adapter.head( - '{endpoint}/{container}/{object}'.format( + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}) + + self.register_uri( + 'HEAD', '{endpoint}/{container}/{object}'.format( endpoint=self.endpoint, container=self.container, object=self.object), status_code=404) - self.adapter.put( - '{endpoint}/{container}/{object}'.format( + self.register_uri( + 'PUT', '{endpoint}/{container}/{object}'.format( endpoint=self.endpoint, container=self.container, object=self.object), - status_code=201) + status_code=201, + validate=dict( + headers={ + 'x-object-meta-x-shade-md5': self.md5, + 'x-object-meta-x-shade-sha256': self.sha256, + })) self.cloud.create_object( container=self.container, name=self.object, filename=self.object_file.name) - self.calls += [ - dict(method='GET', url='https://object-store.example.com/info'), - dict( - method='HEAD', - url='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container)), - dict( - method='PUT', - url='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container)), - dict( - method='HEAD', - url='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container)), - dict( - method='HEAD', - url='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object)), - dict( - method='PUT', - url='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - headers={ - 'x-object-meta-x-shade-md5': self.md5, - 'x-object-meta-x-shade-sha256': self.sha256, - }), - ] - self.assert_calls() def test_create_dynamic_large_object(self): @@ -539,14 +477,20 @@ def test_create_dynamic_large_object(self): max_file_size = 2 min_file_size = 1 - self.adapter.get( - 'https://object-store.example.com/info', + self.register_uri( + 'GET', 'https://object-store.example.com/info', json=dict( swift={'max_file_size': max_file_size}, slo={'min_segment_size': min_file_size})) - self.adapter.put( - '{endpoint}/{container}'.format( + self.register_uri( + 'HEAD', '{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container), + status_code=404) + + self.register_uri( + 'PUT', '{endpoint}/{container}'.format( endpoint=self.endpoint, container=self.container,), status_code=201, @@ -555,92 +499,51 @@ def test_create_dynamic_large_object(self): 'Content-Length': '0', 'Content-Type': 'text/html; charset=UTF-8', }) - self.adapter.head( - '{endpoint}/{container}'.format( + + self.register_uri( + 'HEAD', '{endpoint}/{container}'.format( endpoint=self.endpoint, container=self.container), - [ - dict(status_code=404), - dict(headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}), - ]) - self.adapter.head( - '{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=404) + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}) - self.adapter.put( - '{endpoint}/{container}/{object}'.format( + self.register_uri( + 'HEAD', '{endpoint}/{container}/{object}'.format( endpoint=self.endpoint, container=self.container, object=self.object), - status_code=201) - - self.calls += [ - dict(method='GET', url='https://object-store.example.com/info'), - dict( - method='HEAD', - url='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container)), - dict( - method='PUT', - url='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container)), - dict( - method='HEAD', - url='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container)), - dict( - method='HEAD', - url='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object)), - ] + status_code=404) for index, offset in enumerate( range(0, len(self.content), max_file_size)): - self.adapter.put( - '{endpoint}/{container}/{object}/{index:0>6}'.format( + self.register_uri( + 'PUT', '{endpoint}/{container}/{object}/{index:0>6}'.format( endpoint=self.endpoint, container=self.container, object=self.object, index=index), status_code=201) - self.calls += [ - dict( - method='PUT', - url='{endpoint}/{container}/{object}/{index:0>6}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - index=index))] - - self.calls += [ - dict( - method='PUT', - url='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), + self.register_uri( + 'PUT', '{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=201, + validate=dict( headers={ 'x-object-manifest': '{container}/{object}'.format( container=self.container, object=self.object), 'x-object-meta-x-shade-md5': self.md5, 'x-object-meta-x-shade-sha256': self.sha256, - }), - ] + })) self.cloud.create_object( container=self.container, name=self.object, @@ -659,14 +562,20 @@ def test_create_static_large_object(self): max_file_size = 25 min_file_size = 1 - self.adapter.get( - 'https://object-store.example.com/info', + self.register_uri( + 'GET', 'https://object-store.example.com/info', json=dict( swift={'max_file_size': max_file_size}, slo={'min_segment_size': min_file_size})) - self.adapter.put( - '{endpoint}/{container}'.format( + self.register_uri( + 'HEAD', '{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container), + status_code=404) + + self.register_uri( + 'PUT', '{endpoint}/{container}'.format( endpoint=self.endpoint, container=self.container,), status_code=201, @@ -675,64 +584,33 @@ def test_create_static_large_object(self): 'Content-Length': '0', 'Content-Type': 'text/html; charset=UTF-8', }) - self.adapter.head( - '{endpoint}/{container}'.format( + + self.register_uri( + 'HEAD', '{endpoint}/{container}'.format( endpoint=self.endpoint, container=self.container), - [ - dict(status_code=404), - dict(headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}), - ]) - self.adapter.head( - '{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=404) + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}), - self.adapter.put( - '{endpoint}/{container}/{object}'.format( + self.register_uri( + 'HEAD', '{endpoint}/{container}/{object}'.format( endpoint=self.endpoint, container=self.container, object=self.object), - status_code=201) - - self.calls += [ - dict(method='GET', url='https://object-store.example.com/info'), - dict( - method='HEAD', - url='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container)), - dict( - method='PUT', - url='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container)), - dict( - method='HEAD', - url='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container)), - dict( - method='HEAD', - url='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object)), - ] + status_code=404) for index, offset in enumerate( range(0, len(self.content), max_file_size)): - self.adapter.put( - '{endpoint}/{container}/{object}/{index:0>6}'.format( + self.register_uri( + 'PUT', '{endpoint}/{container}/{object}/{index:0>6}'.format( endpoint=self.endpoint, container=self.container, object=self.object, @@ -740,29 +618,19 @@ def test_create_static_large_object(self): status_code=201, headers=dict(Etag='etag{index}'.format(index=index))) - self.calls += [ - dict( - method='PUT', - url='{endpoint}/{container}/{object}/{index:0>6}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - index=index))] - - self.calls += [ - dict( - method='PUT', - url='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), + self.register_uri( + 'PUT', '{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=201, + validate=dict( params={ 'multipart-manifest', 'put' }, headers={ 'x-object-meta-x-shade-md5': self.md5, 'x-object-meta-x-shade-sha256': self.sha256, - }), - ] + })) self.cloud.create_object( container=self.container, name=self.object, @@ -813,14 +681,20 @@ def test_object_segment_retry_failure(self): max_file_size = 25 min_file_size = 1 - self.adapter.get( - 'https://object-store.example.com/info', + self.register_uri( + 'GET', 'https://object-store.example.com/info', json=dict( swift={'max_file_size': max_file_size}, slo={'min_segment_size': min_file_size})) - self.adapter.put( - '{endpoint}/{container}'.format( + self.register_uri( + 'HEAD', '{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container), + status_code=404) + + self.register_uri( + 'PUT', '{endpoint}/{container}'.format( endpoint=self.endpoint, container=self.container,), status_code=201, @@ -829,122 +703,62 @@ def test_object_segment_retry_failure(self): 'Content-Length': '0', 'Content-Type': 'text/html; charset=UTF-8', }) - self.adapter.head( - '{endpoint}/{container}'.format( + + self.register_uri( + 'HEAD', '{endpoint}/{container}'.format( endpoint=self.endpoint, container=self.container), - [ - dict(status_code=404), - dict(headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}), - ]) - self.adapter.head( - '{endpoint}/{container}/{object}'.format( + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}) + + self.register_uri( + 'HEAD', '{endpoint}/{container}/{object}'.format( endpoint=self.endpoint, container=self.container, object=self.object), status_code=404) - self.adapter.put( - '{endpoint}/{container}/{object}/000000'.format( + self.register_uri( + 'PUT', '{endpoint}/{container}/{object}/000000'.format( endpoint=self.endpoint, container=self.container, object=self.object), status_code=201) - self.adapter.put( - '{endpoint}/{container}/{object}/000001'.format( + self.register_uri( + 'PUT', '{endpoint}/{container}/{object}/000001'.format( endpoint=self.endpoint, container=self.container, object=self.object), status_code=201) - self.adapter.put( - '{endpoint}/{container}/{object}/000002'.format( + self.register_uri( + 'PUT', '{endpoint}/{container}/{object}/000002'.format( endpoint=self.endpoint, container=self.container, object=self.object), status_code=201) - self.adapter.put( - '{endpoint}/{container}/{object}/000003'.format( + self.register_uri( + 'PUT', '{endpoint}/{container}/{object}/000003'.format( endpoint=self.endpoint, container=self.container, object=self.object), status_code=501) - self.adapter.put( - '{endpoint}/{container}/{object}'.format( + self.register_uri( + 'PUT', '{endpoint}/{container}/{object}'.format( endpoint=self.endpoint, container=self.container, object=self.object), status_code=201) - self.calls += [ - dict(method='GET', url='https://object-store.example.com/info'), - dict( - method='HEAD', - url='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container)), - dict( - method='PUT', - url='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container)), - dict( - method='HEAD', - url='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container)), - dict( - method='HEAD', - url='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object)), - - dict( - method='PUT', - url='{endpoint}/{container}/{object}/000000'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object)), - - dict( - method='PUT', - url='{endpoint}/{container}/{object}/000001'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object)), - - dict( - method='PUT', - url='{endpoint}/{container}/{object}/000002'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object)), - - dict( - method='PUT', - url='{endpoint}/{container}/{object}/000003'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object)), - - dict( - method='PUT', - url='{endpoint}/{container}/{object}/000003'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object)), - ] - self.assertRaises( exc.OpenStackCloudException, self.cloud.create_object, @@ -959,14 +773,20 @@ def test_object_segment_retries(self): max_file_size = 25 min_file_size = 1 - self.adapter.get( - 'https://object-store.example.com/info', + self.register_uri( + 'GET', 'https://object-store.example.com/info', json=dict( swift={'max_file_size': max_file_size}, slo={'min_segment_size': min_file_size})) - self.adapter.put( - '{endpoint}/{container}'.format( + self.register_uri( + 'HEAD', '{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container), + status_code=404) + + self.register_uri( + 'PUT', '{endpoint}/{container}'.format( endpoint=self.endpoint, container=self.container,), status_code=201, @@ -975,139 +795,80 @@ def test_object_segment_retries(self): 'Content-Length': '0', 'Content-Type': 'text/html; charset=UTF-8', }) - self.adapter.head( - '{endpoint}/{container}'.format( + + self.register_uri( + 'HEAD', '{endpoint}/{container}'.format( endpoint=self.endpoint, container=self.container), - [ - dict(status_code=404), - dict(headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}), - ]) - self.adapter.head( - '{endpoint}/{container}/{object}'.format( + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}) + + self.register_uri( + 'HEAD', '{endpoint}/{container}/{object}'.format( endpoint=self.endpoint, container=self.container, object=self.object), status_code=404) - self.adapter.put( - '{endpoint}/{container}/{object}/000000'.format( + self.register_uri( + 'PUT', '{endpoint}/{container}/{object}/000000'.format( endpoint=self.endpoint, container=self.container, object=self.object), headers={'etag': 'etag0'}, status_code=201) - self.adapter.put( - '{endpoint}/{container}/{object}/000001'.format( + self.register_uri( + 'PUT', '{endpoint}/{container}/{object}/000001'.format( endpoint=self.endpoint, container=self.container, object=self.object), headers={'etag': 'etag1'}, status_code=201) - self.adapter.put( - '{endpoint}/{container}/{object}/000002'.format( + self.register_uri( + 'PUT', '{endpoint}/{container}/{object}/000002'.format( endpoint=self.endpoint, container=self.container, object=self.object), headers={'etag': 'etag2'}, status_code=201) - self.adapter.put( - '{endpoint}/{container}/{object}/000003'.format( + self.register_uri( + 'PUT', '{endpoint}/{container}/{object}/000003'.format( endpoint=self.endpoint, container=self.container, - object=self.object), [ - dict(status_code=501), - dict(status_code=201, headers={'etag': 'etag3'}), - ]) + object=self.object), + status_code=501) - self.adapter.put( - '{endpoint}/{container}/{object}'.format( + self.register_uri( + 'PUT', '{endpoint}/{container}/{object}/000003'.format( endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=201) - - self.calls += [ - dict(method='GET', url='https://object-store.example.com/info'), - dict( - method='HEAD', - url='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container)), - dict( - method='PUT', - url='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container)), - dict( - method='HEAD', - url='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container)), - dict( - method='HEAD', - url='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object)), - - dict( - method='PUT', - url='{endpoint}/{container}/{object}/000000'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object)), - - dict( - method='PUT', - url='{endpoint}/{container}/{object}/000001'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object)), - - dict( - method='PUT', - url='{endpoint}/{container}/{object}/000002'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object)), - - dict( - method='PUT', - url='{endpoint}/{container}/{object}/000003'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object)), - - dict( - method='PUT', - url='{endpoint}/{container}/{object}/000003'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object)), + container=self.container, + object=self.object), + status_code=201, + headers={'etag': 'etag3'}) - dict( - method='PUT', - url='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), + self.register_uri( + 'PUT', '{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=201, + validate=dict( params={ 'multipart-manifest', 'put' }, headers={ 'x-object-meta-x-shade-md5': self.md5, 'x-object-meta-x-shade-sha256': self.sha256, - }), - ] + })) self.cloud.create_object( container=self.container, name=self.object, From cc78a7fbad1ef7bfb71d0775280d6e891686ab7a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 23 Jan 2017 13:21:24 +0100 Subject: [PATCH 1228/3836] Use port list to find missing floating ips It's possible for a cloud to have multiple private networks with overlapping IP ranges. In that case, the check for missing floating ips can erroneously match a floating ip for a different server. Ports are actually unique, and are the foreign key between these things. Instead of starting with list_floating_ips, start with listing the ports for the server. In the case where OpenStack isn't broken, this will be the same number of API calls. In the case where it is, there will be one extra call per server, but ultimately the output will be more correct - and the fix for the extra load on the cloud is to fix the nova/neutron port mapping. Also, fixed the spelling of supplemental. Story: 2000845 Change-Id: Ie53a2a144ca2ed812d5441868917996f67b6f454 --- ...ix-supplemental-fips-c9cd58aac12eb30e.yaml | 7 ++ shade/meta.py | 29 ++++-- shade/tests/unit/test_meta.py | 94 ++++++++++++++++++- 3 files changed, 116 insertions(+), 14 deletions(-) create mode 100644 releasenotes/notes/fix-supplemental-fips-c9cd58aac12eb30e.yaml diff --git a/releasenotes/notes/fix-supplemental-fips-c9cd58aac12eb30e.yaml b/releasenotes/notes/fix-supplemental-fips-c9cd58aac12eb30e.yaml new file mode 100644 index 000000000..66a5f33c9 --- /dev/null +++ b/releasenotes/notes/fix-supplemental-fips-c9cd58aac12eb30e.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - Fixed an issue where shade could report a floating IP being attached + to a server erroneously due to only matching on fixed ip. Changed the + lookup to match on port ids. This adds an API call in the case where + the workaround is needed because of a bug in the cloud, but in most + cases it should have no difference. diff --git a/shade/meta.py b/shade/meta.py index b90f704be..27951e308 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -296,16 +296,14 @@ def expand_server_vars(cloud, server): return add_server_interfaces(cloud, server) -def _make_address_dict(fip): +def _make_address_dict(fip, port): address = dict(version=4, addr=fip['floating_ip_address']) address['OS-EXT-IPS:type'] = 'floating' - # MAC address comes from the port, not the FIP. It also doesn't matter - # to anyone at the moment, so just make a fake one - address['OS-EXT-IPS-MAC:mac_addr'] = 'de:ad:be:ef:be:ef' + address['OS-EXT-IPS-MAC:mac_addr'] = port['mac_address'] return address -def _get_suplemental_addresses(cloud, server): +def _get_supplemental_addresses(cloud, server): fixed_ip_mapping = {} for name, network in server['addresses'].items(): for address in network: @@ -319,11 +317,24 @@ def _get_suplemental_addresses(cloud, server): # Don't bother doing this before the server is active, it's a waste # of an API call while polling for a server to come up if cloud._has_floating_ips() and server['status'] == 'ACTIVE': - for fip in cloud.list_floating_ips(): - if fip['fixed_ip_address'] in fixed_ip_mapping: + for port in cloud.search_ports( + filters=dict(device_id=server['id'])): + for fip in cloud.search_floating_ips( + filters=dict(port_id=port['id'])): + # This SHOULD return one and only one FIP - but doing + # it as a search/list lets the logic work regardless + if fip['fixed_ip_address'] not in fixed_ip_mapping: + log = _log.setup_logging('shade') + log.debug( + "The cloud returned floating ip %(fip)s attached" + " to server %(server)s but the fixed ip associated" + " with the floating ip in the neutron listing" + " does not exist in the nova listing. Something" + " is exceptionally broken.", + dict(fip=fip['id'], server=server['id'])) fixed_net = fixed_ip_mapping[fip['fixed_ip_address']] server['addresses'][fixed_net].append( - _make_address_dict(fip)) + _make_address_dict(fip, port)) except exc.OpenStackCloudException: # If something goes wrong with a cloud call, that's cool - this is # an attempt to provide additional data and should not block forward @@ -343,7 +354,7 @@ def add_server_interfaces(cloud, server): """ # First, add an IP address. Set it to '' rather than None if it does # not exist to remain consistent with the pre-existing missing values - server['addresses'] = _get_suplemental_addresses(cloud, server) + server['addresses'] = _get_supplemental_addresses(cloud, server) server['public_v4'] = get_server_external_ipv4(cloud, server) or '' server['public_v6'] = get_server_external_ipv6(server) or '' server['private_v4'] = get_server_private_ip(server, cloud) or '' diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 096af7d62..efecabce3 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -302,6 +302,7 @@ def test_get_server_private_ip( mock_has_service.assert_called_with('network') mock_list_networks.assert_called_once_with() + @mock.patch.object(shade.OpenStackCloud, 'list_ports') @mock.patch.object(shade.OpenStackCloud, 'list_floating_ips') @mock.patch.object(shade.OpenStackCloud, 'list_subnets') @mock.patch.object(shade.OpenStackCloud, 'list_server_security_groups') @@ -316,7 +317,8 @@ def test_get_server_private_ip_devstack( mock_get_volumes, mock_list_server_security_groups, mock_list_subnets, - mock_list_floating_ips): + mock_list_floating_ips, + mock_list_ports): mock_get_image_name.return_value = 'cirros-0.3.4-x86_64-uec' mock_get_flavor_name.return_value = 'm1.tiny' mock_has_service.return_value = True @@ -333,7 +335,20 @@ def test_get_server_private_ip_devstack( 'name': 'private', }, ] - mock_list_floating_ips.return_value = [] + mock_list_floating_ips.return_value = [ + { + 'port_id': 'test_port_id', + 'fixed_ip_address': PRIVATE_V4, + 'floating_ip_address': PUBLIC_V4, + } + ] + mock_list_ports.return_value = [ + { + 'id': 'test_port_id', + 'mac_address': 'fa:16:3e:ae:7d:42', + 'device_id': 'test-id', + } + ] srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', @@ -345,15 +360,16 @@ def test_get_server_private_ip_devstack( u'OS-EXT-IPS:type': u'fixed', u'addr': PRIVATE_V4, u'version': 4, - u'OS-EXT-IPS-MAC:mac_addr': - u'fa:16:3e:ae:7d:42' + u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:ae:7d:42' }]} ))) self.assertEqual(PRIVATE_V4, srv['private_v4']) mock_has_service.assert_called_with('volume') mock_list_networks.assert_called_once_with() - mock_list_floating_ips.assert_called_once_with() + mock_list_floating_ips.assert_called_once_with( + filters={'port_id': 'test_port_id'}) + mock_list_ports.assert_called_once_with({'device_id': 'test-id'}) @mock.patch.object(shade.OpenStackCloud, 'list_floating_ips') @mock.patch.object(shade.OpenStackCloud, 'list_subnets') @@ -457,6 +473,74 @@ def test_get_server_cloud_no_fips( mock_list_networks.assert_called_once_with() mock_list_floating_ips.assert_not_called() + @mock.patch.object(shade.OpenStackCloud, 'list_floating_ips') + @mock.patch.object(shade.OpenStackCloud, 'list_ports') + @mock.patch.object(shade.OpenStackCloud, 'list_subnets') + @mock.patch.object(shade.OpenStackCloud, 'list_server_security_groups') + @mock.patch.object(shade.OpenStackCloud, 'get_volumes') + @mock.patch.object(shade.OpenStackCloud, 'get_image_name') + @mock.patch.object(shade.OpenStackCloud, 'get_flavor_name') + @mock.patch.object(shade.OpenStackCloud, 'has_service') + @mock.patch.object(shade.OpenStackCloud, 'list_networks') + def test_get_server_cloud_missing_fips( + self, mock_list_networks, mock_has_service, + mock_get_flavor_name, mock_get_image_name, + mock_get_volumes, + mock_list_server_security_groups, + mock_list_subnets, + mock_list_ports, + mock_list_floating_ips): + self.cloud._floating_ip_source = 'neutron' + mock_get_image_name.return_value = 'cirros-0.3.4-x86_64-uec' + mock_get_flavor_name.return_value = 'm1.tiny' + mock_has_service.return_value = True + mock_get_volumes.return_value = [] + mock_list_subnets.return_value = SUBNETS_WITH_NAT + mock_list_floating_ips.return_value = [ + { + 'port_id': 'test_port_id', + 'fixed_ip_address': PRIVATE_V4, + 'floating_ip_address': PUBLIC_V4, + } + ] + mock_list_ports.return_value = [ + { + 'id': 'test_port_id', + 'mac_address': 'fa:16:3e:ae:7d:42', + 'device_id': 'test-id', + } + ] + mock_list_networks.return_value = [ + { + 'id': 'test_pnztt_net', + 'name': 'test_pnztt_net', + 'router:external': False, + }, + { + 'id': 'private', + 'name': 'private', + }, + ] + + srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( + id='test-id', name='test-name', status='ACTIVE', + flavor={u'id': u'1'}, + image={ + 'name': u'cirros-0.3.4-x86_64-uec', + u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1'}, + addresses={u'test_pnztt_net': [{ + u'addr': PRIVATE_V4, + u'version': 4, + 'OS-EXT-IPS-MAC:mac_addr': 'fa:16:3e:ae:7d:42', + }]} + ))) + + self.assertEqual(PUBLIC_V4, srv['public_v4']) + mock_list_networks.assert_called_once_with() + mock_list_floating_ips.assert_called_once_with( + filters={'port_id': 'test_port_id'}) + mock_list_ports.assert_called_once_with({'device_id': 'test-id'}) + @mock.patch.object(shade.OpenStackCloud, 'list_floating_ips') @mock.patch.object(shade.OpenStackCloud, 'list_subnets') @mock.patch.object(shade.OpenStackCloud, 'list_server_security_groups') From 504cb0565836376b4d271a4d8324d3684b7f39fb Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 31 Jan 2017 12:19:07 -0600 Subject: [PATCH 1229/3836] Add ability to create image from volume OpenStack has the ability to create an image from a volume - expose it. It should be noted that literally nothing about this API is documented, although it is exposed in python-cinderclient and python-openstackclient. Change-Id: Icb06d43a63d0b120a17ce6c19807abcb3de71bcb --- .../image-from-volume-9acf7379f5995b5b.yaml | 3 + shade/openstackcloud.py | 55 ++++++++++++++++-- shade/tests/functional/test_volume.py | 41 ++++++++++--- shade/tests/unit/test_image.py | 58 +++++++++++++++++++ 4 files changed, 145 insertions(+), 12 deletions(-) create mode 100644 releasenotes/notes/image-from-volume-9acf7379f5995b5b.yaml diff --git a/releasenotes/notes/image-from-volume-9acf7379f5995b5b.yaml b/releasenotes/notes/image-from-volume-9acf7379f5995b5b.yaml new file mode 100644 index 000000000..6461f5edf --- /dev/null +++ b/releasenotes/notes/image-from-volume-9acf7379f5995b5b.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added ability to create an image from a volume. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 83b38e72d..4c2da8379 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3267,7 +3267,7 @@ def create_image( disk_format=None, container_format=None, disable_vendor_agent=True, wait=False, timeout=3600, - allow_duplicates=False, meta=None, **kwargs): + allow_duplicates=False, meta=None, volume=None, **kwargs): """Upload an image to Glance. :param str name: Name of the image to create. If it is a pathname @@ -3303,6 +3303,9 @@ def create_image( image name. (optional, defaults to False) :param meta: A dict of key/value pairs to use for metadata that bypasses automatic type conversion. + :param volume: Name or ID or volume object of a volume to create an + image from. Mutually exclusive with (optional, defaults + to None) Additional kwargs will be passed to the image creation as additional metadata for the image and will have all values converted to string @@ -3326,10 +3329,6 @@ def create_image( if not meta: meta = {} - # If there is no filename, see if name is actually the filename - if not filename: - name, filename = self._get_name_and_filename(name) - if not disk_format: disk_format = self.cloud_config.config['image_format'] if not container_format: @@ -3337,6 +3336,26 @@ def create_image( container_format = 'ovf' else: container_format = 'bare' + + if volume: + if 'id' in volume: + volume_id = volume['id'] + else: + volume_obj = self.get_volume(volume) + if not volume_obj: + raise OpenStackCloudException( + "Volume {volume} given to create_image could" + " not be foud".format(volume=volume)) + volume_id = volume_obj['id'] + return self._upload_image_from_volume( + name=name, volume_id=volume_id, + allow_duplicates=allow_duplicates, + container_format=container_format, disk_format=disk_format, + wait=wait, timeout=timeout) + + # If there is no filename, see if name is actually the filename + if not filename: + name, filename = self._get_name_and_filename(name) if not (md5 or sha256): (md5, sha256) = self._get_file_hashes(filename) if allow_duplicates: @@ -3419,6 +3438,32 @@ def _make_v2_image_params(self, meta, properties): ret.update(meta) return ret + def _upload_image_from_volume( + self, name, volume_id, allow_duplicates, + container_format, disk_format, wait, timeout): + response = self._volume_client.post( + '/volumes/{id}/action'.format(id=volume_id), + json={ + 'os-volume_upload_image': { + 'force': allow_duplicates, + 'image_name': name, + 'container_format': container_format, + 'disk_format': disk_format}}) + if not wait: + return self.get_image(response['image_id']) + try: + for count in _utils._iterate_timeout( + timeout, + "Timeout waiting for the image to finish."): + image_obj = self.get_image(response['image_id']) + if image_obj and image_obj.status not in ('queued', 'saving'): + return image_obj + except OpenStackCloudTimeout: + self.log.debug( + "Timeout waiting for image to become ready. Deleting.") + self.delete_image(response['image_id'], wait=True) + raise + def _upload_image_put_v2(self, name, image_data, meta, **image_kwargs): properties = image_kwargs.pop('properties', {}) diff --git a/shade/tests/functional/test_volume.py b/shade/tests/functional/test_volume.py index 23aa8cbbf..a625b7a7a 100644 --- a/shade/tests/functional/test_volume.py +++ b/shade/tests/functional/test_volume.py @@ -34,7 +34,7 @@ def test_volumes(self): volume_name = self.getUniqueString() snapshot_name = self.getUniqueString() self.addDetail('volume', content.text_content(volume_name)) - self.addCleanup(self.cleanup, volume_name, snapshot_name) + self.addCleanup(self.cleanup, volume_name, snapshot_name=snapshot_name) volume = self.demo_cloud.create_volume( display_name=volume_name, size=1) snapshot = self.demo_cloud.create_volume_snapshot( @@ -56,11 +56,38 @@ def test_volumes(self): self.demo_cloud.delete_volume_snapshot(snapshot_name, wait=True) self.demo_cloud.delete_volume(volume_name, wait=True) - def cleanup(self, volume_name, snapshot_name): - volume = self.demo_cloud.get_volume(volume_name) - snapshot = self.demo_cloud.get_volume_snapshot(snapshot_name) + def test_volume_to_image(self): + '''Test volume export to image functionality''' + volume_name = self.getUniqueString() + image_name = self.getUniqueString() + self.addDetail('volume', content.text_content(volume_name)) + self.addCleanup(self.cleanup, volume_name, image_name=image_name) + volume = self.demo_cloud.create_volume( + display_name=volume_name, size=1) + image = self.demo_cloud.create_image( + image_name, volume=volume, wait=True) + + volume_ids = [v['id'] for v in self.demo_cloud.list_volumes()] + self.assertIn(volume['id'], volume_ids) + + image_list = self.demo_cloud.list_images() + image_ids = [s['id'] for s in image_list] + self.assertIn(image['id'], image_ids) + + self.demo_cloud.delete_image(image_name, wait=True) + self.demo_cloud.delete_volume(volume_name, wait=True) + + def cleanup(self, volume_name, snapshot_name=None, image_name=None): # Need to delete snapshots before volumes - if snapshot: - self.demo_cloud.delete_volume_snapshot(snapshot_name) + if snapshot_name: + snapshot = self.demo_cloud.get_volume_snapshot(snapshot_name) + if snapshot: + self.demo_cloud.delete_volume_snapshot( + snapshot_name, wait=True) + if image_name: + image = self.demo_cloud.get_image(image_name) + if image: + self.demo_cloud.delete_image(image_name, wait=True) + volume = self.demo_cloud.get_volume(volume_name) if volume: - self.demo_cloud.delete_volume(volume_name) + self.demo_cloud.delete_volume(volume_name, wait=True) diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index 34ec5d2b9..551ae815a 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -29,6 +29,7 @@ NO_MD5 = '93b885adfe0da089cdf634904fd59f71' NO_SHA256 = '6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d' +CINDER_URL = 'https://volume.example.com/v2/1c36b64c840a42cd9e9b931a369337f0' class BaseTestImage(base.RequestsMockTestCase): @@ -726,3 +727,60 @@ def test_version_discovery_skip(self): self.cloud._image_client.endpoint_override, 'https://image.example.com/v2/override') self.assert_calls() + + +class TestImageVolume(BaseTestImage): + + def test_create_image_volume(self): + + volume_id = 'some-volume' + + self.register_uri( + 'POST', '{endpoint}/volumes/{id}/action'.format( + endpoint=CINDER_URL, id=volume_id), + json={'os-volume_upload_image': {'image_id': self.image_id}}, + validate=dict(json={ + u'os-volume_upload_image': { + u'container_format': u'bare', + u'disk_format': u'qcow2', + u'force': False, + u'image_name': u'fake_image'}})) + + self.use_glance() + + self.register_uri( + 'GET', 'https://image.example.com/v2/images', + json=self.fake_search_return) + + self.cloud.create_image( + 'fake_image', self.imagefile.name, wait=True, timeout=1, + volume={'id': volume_id}) + + self.assert_calls() + + def test_create_image_volume_duplicate(self): + + volume_id = 'some-volume' + + self.register_uri( + 'POST', '{endpoint}/volumes/{id}/action'.format( + endpoint=CINDER_URL, id=volume_id), + json={'os-volume_upload_image': {'image_id': self.image_id}}, + validate=dict(json={ + u'os-volume_upload_image': { + u'container_format': u'bare', + u'disk_format': u'qcow2', + u'force': True, + u'image_name': u'fake_image'}})) + + self.use_glance() + + self.register_uri( + 'GET', 'https://image.example.com/v2/images', + json=self.fake_search_return) + + self.cloud.create_image( + 'fake_image', self.imagefile.name, wait=True, timeout=1, + volume={'id': volume_id}, allow_duplicates=True) + + self.assert_calls() From 32d53d58ce0190e05dc9cf8e7b414f7c950f9008 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 31 Jan 2017 08:16:02 -0600 Subject: [PATCH 1230/3836] Transition nova flavor tests to requests_mock We had added a betamax fixture for create_flavor, but the requests_mock approach is actually working out much better. Go ahead and replace it to simplify the test suite a little bit. Remove _by_flavor tests as they tested a behavior that's actually invalid but worked in the test by happenstance. Transition the rest of the file while we're in there. Change-Id: Ic2457d7380a8af41ed7bf6b264cbdc2240780ff3 --- shade/tests/unit/base.py | 16 +- .../unit/fixtures/test_create_flavor.yaml | 2105 ----------------- shade/tests/unit/test_flavors.py | 266 ++- test-requirements.txt | 2 - 4 files changed, 160 insertions(+), 2229 deletions(-) delete mode 100644 shade/tests/unit/fixtures/test_create_flavor.yaml diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 98ac217b4..4fb7dd82b 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -83,17 +83,6 @@ def _nosleep(seconds): cloud_config=self.cloud_config, log_inner_exceptions=True) - # Any unit tests using betamax directly need a ksa.Session with - # an auth dict. The cassette is currently written with v2 as well - self.full_cloud_config = self.config.get_one_cloud( - cloud='_test_cloud_v2_') - self.full_cloud = shade.OpenStackCloud( - cloud_config=self.full_cloud_config, - log_inner_exceptions=True) - self.full_op_cloud = shade.OperatorCloud( - cloud_config=self.full_cloud_config, - log_inner_exceptions=True) - class TestCase(BaseTestCase): @@ -157,6 +146,9 @@ def _make_test_cloud(self, **kwargs): self.cloud = shade.OpenStackCloud( cloud_config=self.cloud_config, log_inner_exceptions=True) + self.op_cloud = shade.OperatorCloud( + cloud_config=self.cloud_config, + log_inner_exceptions=True) def use_glance(self, image_version_json='image-version.json'): discovery_fixture = os.path.join( @@ -187,7 +179,6 @@ def register_uri(self, method, uri, **kwargs): ] def assert_calls(self, stop_after=None): - self.assertEqual(len(self.calls), len(self.adapter.request_history)) for (x, (call, history)) in enumerate( zip(self.calls, self.adapter.request_history)): if stop_after and x > stop_after: @@ -210,3 +201,4 @@ def assert_calls(self, stop_after=None): self.assertEqual( value, history.headers[key], 'header mismatch in call {index}'.format(index=x)) + self.assertEqual(len(self.calls), len(self.adapter.request_history)) diff --git a/shade/tests/unit/fixtures/test_create_flavor.yaml b/shade/tests/unit/fixtures/test_create_flavor.yaml deleted file mode 100644 index d83ac7268..000000000 --- a/shade/tests/unit/fixtures/test_create_flavor.yaml +++ /dev/null @@ -1,2105 +0,0 @@ -http_interactions: -- recorded_at: '2016-04-14T10:09:57' - request: - body: - encoding: utf-8 - string: '' - headers: - Accept: - - application/json - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 - method: GET - uri: http://192.168.0.19:35357/ - response: - body: - encoding: null - string: |- - { - "versions": { - "values": [ - { - "status": "stable", - "updated": "2016-04-04T00:00:00Z", - "media-types": [ - { - "base": "application/json", - "type": "application/vnd.openstack.identity-v3+json" - } - ], - "id": "v3.6", - "links": [ - { - "href": "http://192.168.0.19:35357/v3/", - "rel": "self" - } - ] - }, - { - "status": "stable", - "updated": "2014-04-17T00:00:00Z", - "media-types": [ - { - "base": "application/json", - "type": "application/vnd.openstack.identity-v2.0+json" - } - ], - "id": "v2.0", - "links": [ - { - "href": "http://192.168.0.19:35357/v2.0/", - "rel": "self" - }, - { - "href": "http://docs.openstack.org/", - "type": "text/html", - "rel": "describedby" - } - ] - } - ] - } - } - headers: - Connection: - - Keep-Alive - Content-Length: - - '595' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:09:57 GMT - Keep-Alive: - - timeout=5, max=100 - Server: - - Apache/2.4.7 (Ubuntu) - Vary: - - X-Auth-Token - status: - code: 300 - message: Multiple Choices - url: http://192.168.0.19:35357/ -- recorded_at: '2016-04-14T10:09:58' - request: - body: - encoding: utf-8 - string: |- - { - "auth": { - "tenantName": "dummy", - "passwordCredentials": { - "username": "dummy", - "password": "********" - } - } - } - headers: - Accept: - - application/json - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - Content-Length: - - '103' - Content-Type: - - application/json - User-Agent: - - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 - method: POST - uri: http://192.168.0.19:35357/v2.0/tokens - response: - body: - encoding: null - string: |- - { - "access": { - "token": { - "issued_at": "2016-04-14T10:09:58.014014Z", - "expires": "9999-12-31T23:59:59Z", - "id": "7fa3037ae2fe48ada8c626a51dc01ffd", - "tenant": { - "enabled": true, - "description": "Bootstrap project for initializing the cloud.", - "name": "admin", - "id": "1c36b64c840a42cd9e9b931a369337f0" - }, - "audit_ids": [ - "FgG3Q8T3Sh21r_7HyjHP8A" - ] - }, - "serviceCatalog": [ - { - "endpoints_links": [], - "endpoints": [ - { - "adminURL": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0", - "region": "RegionOne", - "publicURL": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0", - "internalURL": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0", - "id": "32466f357f3545248c47471ca51b0d3a" - } - ], - "type": "compute", - "name": "nova" - }, - { - "endpoints_links": [], - "endpoints": [ - { - "adminURL": "http://192.168.0.19:8776/v2/1c36b64c840a42cd9e9b931a369337f0", - "region": "RegionOne", - "publicURL": "http://192.168.0.19:8776/v2/1c36b64c840a42cd9e9b931a369337f0", - "internalURL": "http://192.168.0.19:8776/v2/1c36b64c840a42cd9e9b931a369337f0", - "id": "1e875ca2225b408bbf3520a1b8e1a537" - } - ], - "type": "volumev2", - "name": "cinderv2" - }, - { - "endpoints_links": [], - "endpoints": [ - { - "adminURL": "http://192.168.0.19:9292", - "region": "RegionOne", - "publicURL": "http://192.168.0.19:9292", - "internalURL": "http://192.168.0.19:9292", - "id": "5a64de3c4a614d8d8f8d1ba3dee5f45f" - } - ], - "type": "image", - "name": "glance" - }, - { - "endpoints_links": [], - "endpoints": [ - { - "adminURL": "http://192.168.0.19:8774/v2/1c36b64c840a42cd9e9b931a369337f0", - "region": "RegionOne", - "publicURL": "http://192.168.0.19:8774/v2/1c36b64c840a42cd9e9b931a369337f0", - "internalURL": "http://192.168.0.19:8774/v2/1c36b64c840a42cd9e9b931a369337f0", - "id": "74ba16ca98154dfface2af92e97062a7" - } - ], - "type": "compute_legacy", - "name": "nova_legacy" - }, - { - "endpoints_links": [], - "endpoints": [ - { - "adminURL": "http://192.168.0.19:8776/v1/1c36b64c840a42cd9e9b931a369337f0", - "region": "RegionOne", - "publicURL": "http://192.168.0.19:8776/v1/1c36b64c840a42cd9e9b931a369337f0", - "internalURL": "http://192.168.0.19:8776/v1/1c36b64c840a42cd9e9b931a369337f0", - "id": "3d15fdfc7d424f3c8923324417e1a3d1" - } - ], - "type": "volume", - "name": "cinder" - }, - { - "endpoints_links": [], - "endpoints": [ - { - "adminURL": "http://192.168.0.19:35357/v2.0", - "region": "RegionOne", - "publicURL": "http://192.168.0.19:5000/v2.0", - "internalURL": "http://192.168.0.19:5000/v2.0", - "id": "4deb4d0504a044a395d4480741ba628c" - } - ], - "type": "identity", - "name": "keystone" - } - ], - "user": { - "username": "dummy", - "roles_links": [], - "id": "71675f719c3343e8ac441cc28f396474", - "roles": [ - { - "name": "admin" - } - ], - "name": "admin" - }, - "metadata": { - "is_admin": 0, - "roles": [ - "6d813db50b6e4a1ababdbbb5a83c7de5" - ] - } - } - } - headers: - Connection: - - Keep-Alive - Content-Length: - - '2647' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:09:57 GMT - Keep-Alive: - - timeout=5, max=99 - Server: - - Apache/2.4.7 (Ubuntu) - Vary: - - X-Auth-Token - x-openstack-request-id: - - req-b60e1abb-6819-41c7-8851-e22adf92158e - status: - code: 200 - message: OK - url: http://192.168.0.19:35357/v2.0/tokens -- recorded_at: '2016-04-14T10:09:58' - request: - body: - encoding: utf-8 - string: '' - headers: - Accept: - - application/json - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - python-novaclient - X-Auth-Token: - - 7fa3037ae2fe48ada8c626a51dc01ffd - method: GET - uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/detail?is_public=None - response: - body: - encoding: null - string: |- - { - "flavors": [ - { - "name": "m1.tiny", - "links": [ - { - "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/1", - "rel": "self" - }, - { - "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/1", - "rel": "bookmark" - } - ], - "ram": 512, - "OS-FLV-DISABLED:disabled": false, - "vcpus": 1, - "swap": "", - "os-flavor-access:is_public": true, - "rxtx_factor": 1.0, - "OS-FLV-EXT-DATA:ephemeral": 0, - "disk": 1, - "id": "1" - }, - { - "name": "m1.small", - "links": [ - { - "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/2", - "rel": "self" - }, - { - "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/2", - "rel": "bookmark" - } - ], - "ram": 2048, - "OS-FLV-DISABLED:disabled": false, - "vcpus": 1, - "swap": "", - "os-flavor-access:is_public": true, - "rxtx_factor": 1.0, - "OS-FLV-EXT-DATA:ephemeral": 0, - "disk": 20, - "id": "2" - }, - { - "name": "m1.medium", - "links": [ - { - "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/3", - "rel": "self" - }, - { - "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/3", - "rel": "bookmark" - } - ], - "ram": 4096, - "OS-FLV-DISABLED:disabled": false, - "vcpus": 2, - "swap": "", - "os-flavor-access:is_public": true, - "rxtx_factor": 1.0, - "OS-FLV-EXT-DATA:ephemeral": 0, - "disk": 40, - "id": "3" - }, - { - "name": "m1.large", - "links": [ - { - "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/4", - "rel": "self" - }, - { - "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/4", - "rel": "bookmark" - } - ], - "ram": 8192, - "OS-FLV-DISABLED:disabled": false, - "vcpus": 4, - "swap": "", - "os-flavor-access:is_public": true, - "rxtx_factor": 1.0, - "OS-FLV-EXT-DATA:ephemeral": 0, - "disk": 80, - "id": "4" - }, - { - "name": "m1.nano", - "links": [ - { - "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/42", - "rel": "self" - }, - { - "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/42", - "rel": "bookmark" - } - ], - "ram": 64, - "OS-FLV-DISABLED:disabled": false, - "vcpus": 1, - "swap": "", - "os-flavor-access:is_public": true, - "rxtx_factor": 1.0, - "OS-FLV-EXT-DATA:ephemeral": 0, - "disk": 0, - "id": "42" - }, - { - "name": "m1.xlarge", - "links": [ - { - "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/5", - "rel": "self" - }, - { - "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/5", - "rel": "bookmark" - } - ], - "ram": 16384, - "OS-FLV-DISABLED:disabled": false, - "vcpus": 8, - "swap": "", - "os-flavor-access:is_public": true, - "rxtx_factor": 1.0, - "OS-FLV-EXT-DATA:ephemeral": 0, - "disk": 160, - "id": "5" - }, - { - "name": "m1.micro", - "links": [ - { - "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/84", - "rel": "self" - }, - { - "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/84", - "rel": "bookmark" - } - ], - "ram": 128, - "OS-FLV-DISABLED:disabled": false, - "vcpus": 1, - "swap": "", - "os-flavor-access:is_public": true, - "rxtx_factor": 1.0, - "OS-FLV-EXT-DATA:ephemeral": 0, - "disk": 0, - "id": "84" - } - ] - } - headers: - Connection: - - keep-alive - Content-Length: - - '2933' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:09:58 GMT - Vary: - - X-OpenStack-Nova-API-Version - X-Compute-Request-Id: - - req-2c57220f-2462-4e60-9561-6d192762f087 - X-Openstack-Nova-Api-Version: - - '2.1' - status: - code: 200 - message: OK - url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/detail?is_public=None -- recorded_at: '2016-04-14T10:09:58' - request: - body: - encoding: utf-8 - string: '' - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 - X-Auth-Token: - - 7fa3037ae2fe48ada8c626a51dc01ffd - method: GET - uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/1/os-extra_specs - response: - body: - encoding: null - string: |- - { - "extra_specs": {} - } - headers: - Connection: - - keep-alive - Content-Length: - - '19' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:09:58 GMT - Vary: - - X-OpenStack-Nova-API-Version - X-Compute-Request-Id: - - req-c2c93548-8671-49ff-9e24-50e7f2f5115a - X-Openstack-Nova-Api-Version: - - '2.1' - status: - code: 200 - message: OK - url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/1/os-extra_specs -- recorded_at: '2016-04-14T10:09:58' - request: - body: - encoding: utf-8 - string: '' - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 - X-Auth-Token: - - 7fa3037ae2fe48ada8c626a51dc01ffd - method: GET - uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/2/os-extra_specs - response: - body: - encoding: null - string: |- - { - "extra_specs": {} - } - headers: - Connection: - - keep-alive - Content-Length: - - '19' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:09:58 GMT - Vary: - - X-OpenStack-Nova-API-Version - X-Compute-Request-Id: - - req-7c377ee6-689a-484a-aee5-691cd97211a6 - X-Openstack-Nova-Api-Version: - - '2.1' - status: - code: 200 - message: OK - url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/2/os-extra_specs -- recorded_at: '2016-04-14T10:09:58' - request: - body: - encoding: utf-8 - string: '' - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 - X-Auth-Token: - - 7fa3037ae2fe48ada8c626a51dc01ffd - method: GET - uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/3/os-extra_specs - response: - body: - encoding: null - string: |- - { - "extra_specs": {} - } - headers: - Connection: - - keep-alive - Content-Length: - - '19' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:09:58 GMT - Vary: - - X-OpenStack-Nova-API-Version - X-Compute-Request-Id: - - req-fa02df4b-b312-46f6-a945-bd5d2f324fa4 - X-Openstack-Nova-Api-Version: - - '2.1' - status: - code: 200 - message: OK - url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/3/os-extra_specs -- recorded_at: '2016-04-14T10:09:59' - request: - body: - encoding: utf-8 - string: '' - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 - X-Auth-Token: - - 7fa3037ae2fe48ada8c626a51dc01ffd - method: GET - uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/4/os-extra_specs - response: - body: - encoding: null - string: |- - { - "extra_specs": {} - } - headers: - Connection: - - keep-alive - Content-Length: - - '19' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:09:59 GMT - Vary: - - X-OpenStack-Nova-API-Version - X-Compute-Request-Id: - - req-0be39be0-2c60-4b0b-879c-a989ce48e68f - X-Openstack-Nova-Api-Version: - - '2.1' - status: - code: 200 - message: OK - url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/4/os-extra_specs -- recorded_at: '2016-04-14T10:09:59' - request: - body: - encoding: utf-8 - string: '' - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 - X-Auth-Token: - - 7fa3037ae2fe48ada8c626a51dc01ffd - method: GET - uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/42/os-extra_specs - response: - body: - encoding: null - string: |- - { - "extra_specs": {} - } - headers: - Connection: - - keep-alive - Content-Length: - - '19' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:09:59 GMT - Vary: - - X-OpenStack-Nova-API-Version - X-Compute-Request-Id: - - req-87a26e5f-c9f2-4ee7-9f9c-a4371ba1da8a - X-Openstack-Nova-Api-Version: - - '2.1' - status: - code: 200 - message: OK - url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/42/os-extra_specs -- recorded_at: '2016-04-14T10:09:59' - request: - body: - encoding: utf-8 - string: '' - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 - X-Auth-Token: - - 7fa3037ae2fe48ada8c626a51dc01ffd - method: GET - uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/5/os-extra_specs - response: - body: - encoding: null - string: |- - { - "extra_specs": {} - } - headers: - Connection: - - keep-alive - Content-Length: - - '19' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:09:59 GMT - Vary: - - X-OpenStack-Nova-API-Version - X-Compute-Request-Id: - - req-466cd9ac-fc04-431e-88b5-9951c8859edb - X-Openstack-Nova-Api-Version: - - '2.1' - status: - code: 200 - message: OK - url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/5/os-extra_specs -- recorded_at: '2016-04-14T10:09:59' - request: - body: - encoding: utf-8 - string: '' - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 - X-Auth-Token: - - 7fa3037ae2fe48ada8c626a51dc01ffd - method: GET - uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/84/os-extra_specs - response: - body: - encoding: null - string: |- - { - "extra_specs": {} - } - headers: - Connection: - - keep-alive - Content-Length: - - '19' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:09:59 GMT - Vary: - - X-OpenStack-Nova-API-Version - X-Compute-Request-Id: - - req-75179ff1-71b6-4635-a0d2-f6b72ac0526f - X-Openstack-Nova-Api-Version: - - '2.1' - status: - code: 200 - message: OK - url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/84/os-extra_specs -- recorded_at: '2016-04-14T10:09:59' - request: - body: - encoding: utf-8 - string: |- - { - "flavor": { - "name": "vanilla", - "ram": 12345, - "vcpus": 4, - "swap": 0, - "os-flavor-access:is_public": true, - "rxtx_factor": 1.0, - "OS-FLV-EXT-DATA:ephemeral": 0, - "disk": 100, - "id": null - } - } - headers: - Accept: - - application/json - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - Content-Length: - - '181' - Content-Type: - - application/json - User-Agent: - - python-novaclient - X-Auth-Token: - - 7fa3037ae2fe48ada8c626a51dc01ffd - method: POST - uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors - response: - body: - encoding: null - string: |- - { - "flavor": { - "name": "vanilla", - "links": [ - { - "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30", - "rel": "self" - }, - { - "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30", - "rel": "bookmark" - } - ], - "ram": 12345, - "OS-FLV-DISABLED:disabled": false, - "vcpus": 4, - "swap": "", - "os-flavor-access:is_public": true, - "rxtx_factor": 1.0, - "OS-FLV-EXT-DATA:ephemeral": 0, - "disk": 100, - "id": "8987fc39-f5b8-46ce-bf1e-816e01623b30" - } - } - headers: - Connection: - - keep-alive - Content-Length: - - '533' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:09:59 GMT - Vary: - - X-OpenStack-Nova-API-Version - X-Compute-Request-Id: - - req-a47adf29-b5cc-45af-812b-db35ec6f22ac - X-Openstack-Nova-Api-Version: - - '2.1' - status: - code: 200 - message: OK - url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors -- recorded_at: '2016-04-14T10:09:59' - request: - body: - encoding: utf-8 - string: '' - headers: - Accept: - - application/json - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - python-novaclient - X-Auth-Token: - - 7fa3037ae2fe48ada8c626a51dc01ffd - method: GET - uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30 - response: - body: - encoding: null - string: |- - { - "flavor": { - "name": "vanilla", - "links": [ - { - "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30", - "rel": "self" - }, - { - "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30", - "rel": "bookmark" - } - ], - "ram": 12345, - "OS-FLV-DISABLED:disabled": false, - "vcpus": 4, - "swap": "", - "os-flavor-access:is_public": true, - "rxtx_factor": 1.0, - "OS-FLV-EXT-DATA:ephemeral": 0, - "disk": 100, - "id": "8987fc39-f5b8-46ce-bf1e-816e01623b30" - } - } - headers: - Connection: - - keep-alive - Content-Length: - - '533' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:09:59 GMT - Vary: - - X-OpenStack-Nova-API-Version - X-Compute-Request-Id: - - req-42ab9a08-608e-4f76-bd26-0a2d6e650099 - X-Openstack-Nova-Api-Version: - - '2.1' - status: - code: 200 - message: OK - url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30 -- recorded_at: '2016-04-14T10:10:00' - request: - body: - encoding: utf-8 - string: '' - headers: - Accept: - - application/json - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - python-novaclient - X-Auth-Token: - - 7fa3037ae2fe48ada8c626a51dc01ffd - method: GET - uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/detail?is_public=None - response: - body: - encoding: null - string: |- - { - "flavors": [ - { - "name": "m1.tiny", - "links": [ - { - "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/1", - "rel": "self" - }, - { - "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/1", - "rel": "bookmark" - } - ], - "ram": 512, - "OS-FLV-DISABLED:disabled": false, - "vcpus": 1, - "swap": "", - "os-flavor-access:is_public": true, - "rxtx_factor": 1.0, - "OS-FLV-EXT-DATA:ephemeral": 0, - "disk": 1, - "id": "1" - }, - { - "name": "m1.small", - "links": [ - { - "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/2", - "rel": "self" - }, - { - "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/2", - "rel": "bookmark" - } - ], - "ram": 2048, - "OS-FLV-DISABLED:disabled": false, - "vcpus": 1, - "swap": "", - "os-flavor-access:is_public": true, - "rxtx_factor": 1.0, - "OS-FLV-EXT-DATA:ephemeral": 0, - "disk": 20, - "id": "2" - }, - { - "name": "m1.medium", - "links": [ - { - "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/3", - "rel": "self" - }, - { - "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/3", - "rel": "bookmark" - } - ], - "ram": 4096, - "OS-FLV-DISABLED:disabled": false, - "vcpus": 2, - "swap": "", - "os-flavor-access:is_public": true, - "rxtx_factor": 1.0, - "OS-FLV-EXT-DATA:ephemeral": 0, - "disk": 40, - "id": "3" - }, - { - "name": "m1.large", - "links": [ - { - "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/4", - "rel": "self" - }, - { - "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/4", - "rel": "bookmark" - } - ], - "ram": 8192, - "OS-FLV-DISABLED:disabled": false, - "vcpus": 4, - "swap": "", - "os-flavor-access:is_public": true, - "rxtx_factor": 1.0, - "OS-FLV-EXT-DATA:ephemeral": 0, - "disk": 80, - "id": "4" - }, - { - "name": "m1.nano", - "links": [ - { - "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/42", - "rel": "self" - }, - { - "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/42", - "rel": "bookmark" - } - ], - "ram": 64, - "OS-FLV-DISABLED:disabled": false, - "vcpus": 1, - "swap": "", - "os-flavor-access:is_public": true, - "rxtx_factor": 1.0, - "OS-FLV-EXT-DATA:ephemeral": 0, - "disk": 0, - "id": "42" - }, - { - "name": "m1.xlarge", - "links": [ - { - "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/5", - "rel": "self" - }, - { - "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/5", - "rel": "bookmark" - } - ], - "ram": 16384, - "OS-FLV-DISABLED:disabled": false, - "vcpus": 8, - "swap": "", - "os-flavor-access:is_public": true, - "rxtx_factor": 1.0, - "OS-FLV-EXT-DATA:ephemeral": 0, - "disk": 160, - "id": "5" - }, - { - "name": "m1.micro", - "links": [ - { - "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/84", - "rel": "self" - }, - { - "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/84", - "rel": "bookmark" - } - ], - "ram": 128, - "OS-FLV-DISABLED:disabled": false, - "vcpus": 1, - "swap": "", - "os-flavor-access:is_public": true, - "rxtx_factor": 1.0, - "OS-FLV-EXT-DATA:ephemeral": 0, - "disk": 0, - "id": "84" - }, - { - "name": "vanilla", - "links": [ - { - "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30", - "rel": "self" - }, - { - "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30", - "rel": "bookmark" - } - ], - "ram": 12345, - "OS-FLV-DISABLED:disabled": false, - "vcpus": 4, - "swap": "", - "os-flavor-access:is_public": true, - "rxtx_factor": 1.0, - "OS-FLV-EXT-DATA:ephemeral": 0, - "disk": 100, - "id": "8987fc39-f5b8-46ce-bf1e-816e01623b30" - } - ] - } - headers: - Connection: - - keep-alive - Content-Length: - - '3456' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:10:00 GMT - Vary: - - X-OpenStack-Nova-API-Version - X-Compute-Request-Id: - - req-87e35330-ca43-43d7-b42a-422bccca8b94 - X-Openstack-Nova-Api-Version: - - '2.1' - status: - code: 200 - message: OK - url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/detail?is_public=None -- recorded_at: '2016-04-14T10:10:00' - request: - body: - encoding: utf-8 - string: '' - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 - X-Auth-Token: - - 7fa3037ae2fe48ada8c626a51dc01ffd - method: GET - uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/1/os-extra_specs - response: - body: - encoding: null - string: |- - { - "extra_specs": {} - } - headers: - Connection: - - keep-alive - Content-Length: - - '19' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:10:00 GMT - Vary: - - X-OpenStack-Nova-API-Version - X-Compute-Request-Id: - - req-4013b982-885e-4225-bf62-7ecd78d81465 - X-Openstack-Nova-Api-Version: - - '2.1' - status: - code: 200 - message: OK - url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/1/os-extra_specs -- recorded_at: '2016-04-14T10:10:00' - request: - body: - encoding: utf-8 - string: '' - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 - X-Auth-Token: - - 7fa3037ae2fe48ada8c626a51dc01ffd - method: GET - uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/2/os-extra_specs - response: - body: - encoding: null - string: |- - { - "extra_specs": {} - } - headers: - Connection: - - keep-alive - Content-Length: - - '19' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:10:00 GMT - Vary: - - X-OpenStack-Nova-API-Version - X-Compute-Request-Id: - - req-748fb97b-73e4-4857-b2f9-d32ae868097d - X-Openstack-Nova-Api-Version: - - '2.1' - status: - code: 200 - message: OK - url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/2/os-extra_specs -- recorded_at: '2016-04-14T10:10:00' - request: - body: - encoding: utf-8 - string: '' - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 - X-Auth-Token: - - 7fa3037ae2fe48ada8c626a51dc01ffd - method: GET - uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/3/os-extra_specs - response: - body: - encoding: null - string: |- - { - "extra_specs": {} - } - headers: - Connection: - - keep-alive - Content-Length: - - '19' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:10:00 GMT - Vary: - - X-OpenStack-Nova-API-Version - X-Compute-Request-Id: - - req-e7dde10b-483d-4d15-b608-2093dd10c849 - X-Openstack-Nova-Api-Version: - - '2.1' - status: - code: 200 - message: OK - url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/3/os-extra_specs -- recorded_at: '2016-04-14T10:10:00' - request: - body: - encoding: utf-8 - string: '' - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 - X-Auth-Token: - - 7fa3037ae2fe48ada8c626a51dc01ffd - method: GET - uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/4/os-extra_specs - response: - body: - encoding: null - string: |- - { - "extra_specs": {} - } - headers: - Connection: - - keep-alive - Content-Length: - - '19' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:10:00 GMT - Vary: - - X-OpenStack-Nova-API-Version - X-Compute-Request-Id: - - req-7dcb269b-c961-43e5-ac4e-69237edcf6d8 - X-Openstack-Nova-Api-Version: - - '2.1' - status: - code: 200 - message: OK - url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/4/os-extra_specs -- recorded_at: '2016-04-14T10:10:01' - request: - body: - encoding: utf-8 - string: '' - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 - X-Auth-Token: - - 7fa3037ae2fe48ada8c626a51dc01ffd - method: GET - uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/42/os-extra_specs - response: - body: - encoding: null - string: |- - { - "extra_specs": {} - } - headers: - Connection: - - keep-alive - Content-Length: - - '19' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:10:01 GMT - Vary: - - X-OpenStack-Nova-API-Version - X-Compute-Request-Id: - - req-4b048baf-1a27-427f-9771-974288273a95 - X-Openstack-Nova-Api-Version: - - '2.1' - status: - code: 200 - message: OK - url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/42/os-extra_specs -- recorded_at: '2016-04-14T10:10:01' - request: - body: - encoding: utf-8 - string: '' - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 - X-Auth-Token: - - 7fa3037ae2fe48ada8c626a51dc01ffd - method: GET - uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/5/os-extra_specs - response: - body: - encoding: null - string: |- - { - "extra_specs": {} - } - headers: - Connection: - - keep-alive - Content-Length: - - '19' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:10:01 GMT - Vary: - - X-OpenStack-Nova-API-Version - X-Compute-Request-Id: - - req-846cd913-191f-4714-9d1b-6a2e0015b538 - X-Openstack-Nova-Api-Version: - - '2.1' - status: - code: 200 - message: OK - url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/5/os-extra_specs -- recorded_at: '2016-04-14T10:10:01' - request: - body: - encoding: utf-8 - string: '' - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 - X-Auth-Token: - - 7fa3037ae2fe48ada8c626a51dc01ffd - method: GET - uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/84/os-extra_specs - response: - body: - encoding: null - string: |- - { - "extra_specs": {} - } - headers: - Connection: - - keep-alive - Content-Length: - - '19' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:10:01 GMT - Vary: - - X-OpenStack-Nova-API-Version - X-Compute-Request-Id: - - req-a3b21f98-51b8-472c-9ff0-1376beff1498 - X-Openstack-Nova-Api-Version: - - '2.1' - status: - code: 200 - message: OK - url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/84/os-extra_specs -- recorded_at: '2016-04-14T10:10:01' - request: - body: - encoding: utf-8 - string: '' - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 - X-Auth-Token: - - 7fa3037ae2fe48ada8c626a51dc01ffd - method: GET - uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30/os-extra_specs - response: - body: - encoding: null - string: |- - { - "extra_specs": {} - } - headers: - Connection: - - keep-alive - Content-Length: - - '19' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:10:01 GMT - Vary: - - X-OpenStack-Nova-API-Version - X-Compute-Request-Id: - - req-14cc8bb9-27cc-48ba-9b42-c31d1322b8a7 - X-Openstack-Nova-Api-Version: - - '2.1' - status: - code: 200 - message: OK - url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30/os-extra_specs -- recorded_at: '2016-04-14T10:10:01' - request: - body: - encoding: utf-8 - string: '' - headers: - Accept: - - application/json - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - python-novaclient - X-Auth-Token: - - 7fa3037ae2fe48ada8c626a51dc01ffd - method: GET - uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/detail?is_public=None - response: - body: - encoding: null - string: |- - { - "flavors": [ - { - "name": "m1.tiny", - "links": [ - { - "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/1", - "rel": "self" - }, - { - "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/1", - "rel": "bookmark" - } - ], - "ram": 512, - "OS-FLV-DISABLED:disabled": false, - "vcpus": 1, - "swap": "", - "os-flavor-access:is_public": true, - "rxtx_factor": 1.0, - "OS-FLV-EXT-DATA:ephemeral": 0, - "disk": 1, - "id": "1" - }, - { - "name": "m1.small", - "links": [ - { - "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/2", - "rel": "self" - }, - { - "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/2", - "rel": "bookmark" - } - ], - "ram": 2048, - "OS-FLV-DISABLED:disabled": false, - "vcpus": 1, - "swap": "", - "os-flavor-access:is_public": true, - "rxtx_factor": 1.0, - "OS-FLV-EXT-DATA:ephemeral": 0, - "disk": 20, - "id": "2" - }, - { - "name": "m1.medium", - "links": [ - { - "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/3", - "rel": "self" - }, - { - "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/3", - "rel": "bookmark" - } - ], - "ram": 4096, - "OS-FLV-DISABLED:disabled": false, - "vcpus": 2, - "swap": "", - "os-flavor-access:is_public": true, - "rxtx_factor": 1.0, - "OS-FLV-EXT-DATA:ephemeral": 0, - "disk": 40, - "id": "3" - }, - { - "name": "m1.large", - "links": [ - { - "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/4", - "rel": "self" - }, - { - "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/4", - "rel": "bookmark" - } - ], - "ram": 8192, - "OS-FLV-DISABLED:disabled": false, - "vcpus": 4, - "swap": "", - "os-flavor-access:is_public": true, - "rxtx_factor": 1.0, - "OS-FLV-EXT-DATA:ephemeral": 0, - "disk": 80, - "id": "4" - }, - { - "name": "m1.nano", - "links": [ - { - "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/42", - "rel": "self" - }, - { - "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/42", - "rel": "bookmark" - } - ], - "ram": 64, - "OS-FLV-DISABLED:disabled": false, - "vcpus": 1, - "swap": "", - "os-flavor-access:is_public": true, - "rxtx_factor": 1.0, - "OS-FLV-EXT-DATA:ephemeral": 0, - "disk": 0, - "id": "42" - }, - { - "name": "m1.xlarge", - "links": [ - { - "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/5", - "rel": "self" - }, - { - "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/5", - "rel": "bookmark" - } - ], - "ram": 16384, - "OS-FLV-DISABLED:disabled": false, - "vcpus": 8, - "swap": "", - "os-flavor-access:is_public": true, - "rxtx_factor": 1.0, - "OS-FLV-EXT-DATA:ephemeral": 0, - "disk": 160, - "id": "5" - }, - { - "name": "m1.micro", - "links": [ - { - "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/84", - "rel": "self" - }, - { - "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/84", - "rel": "bookmark" - } - ], - "ram": 128, - "OS-FLV-DISABLED:disabled": false, - "vcpus": 1, - "swap": "", - "os-flavor-access:is_public": true, - "rxtx_factor": 1.0, - "OS-FLV-EXT-DATA:ephemeral": 0, - "disk": 0, - "id": "84" - }, - { - "name": "vanilla", - "links": [ - { - "href": "http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30", - "rel": "self" - }, - { - "href": "http://192.168.0.19:8774/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30", - "rel": "bookmark" - } - ], - "ram": 12345, - "OS-FLV-DISABLED:disabled": false, - "vcpus": 4, - "swap": "", - "os-flavor-access:is_public": true, - "rxtx_factor": 1.0, - "OS-FLV-EXT-DATA:ephemeral": 0, - "disk": 100, - "id": "8987fc39-f5b8-46ce-bf1e-816e01623b30" - } - ] - } - headers: - Connection: - - keep-alive - Content-Length: - - '3456' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:10:01 GMT - Vary: - - X-OpenStack-Nova-API-Version - X-Compute-Request-Id: - - req-13ba0453-4b97-467c-b996-ba2d7965df01 - X-Openstack-Nova-Api-Version: - - '2.1' - status: - code: 200 - message: OK - url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/detail?is_public=None -- recorded_at: '2016-04-14T10:10:02' - request: - body: - encoding: utf-8 - string: '' - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 - X-Auth-Token: - - 7fa3037ae2fe48ada8c626a51dc01ffd - method: GET - uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/1/os-extra_specs - response: - body: - encoding: null - string: |- - { - "extra_specs": {} - } - headers: - Connection: - - keep-alive - Content-Length: - - '19' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:10:02 GMT - Vary: - - X-OpenStack-Nova-API-Version - X-Compute-Request-Id: - - req-b223f5a5-d817-433c-8f11-61b351b0d964 - X-Openstack-Nova-Api-Version: - - '2.1' - status: - code: 200 - message: OK - url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/1/os-extra_specs -- recorded_at: '2016-04-14T10:10:02' - request: - body: - encoding: utf-8 - string: '' - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 - X-Auth-Token: - - 7fa3037ae2fe48ada8c626a51dc01ffd - method: GET - uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/2/os-extra_specs - response: - body: - encoding: null - string: |- - { - "extra_specs": {} - } - headers: - Connection: - - keep-alive - Content-Length: - - '19' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:10:02 GMT - Vary: - - X-OpenStack-Nova-API-Version - X-Compute-Request-Id: - - req-1fb1cd46-72eb-452a-a937-6f4c99af71f4 - X-Openstack-Nova-Api-Version: - - '2.1' - status: - code: 200 - message: OK - url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/2/os-extra_specs -- recorded_at: '2016-04-14T10:10:02' - request: - body: - encoding: utf-8 - string: '' - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 - X-Auth-Token: - - 7fa3037ae2fe48ada8c626a51dc01ffd - method: GET - uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/3/os-extra_specs - response: - body: - encoding: null - string: |- - { - "extra_specs": {} - } - headers: - Connection: - - keep-alive - Content-Length: - - '19' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:10:02 GMT - Vary: - - X-OpenStack-Nova-API-Version - X-Compute-Request-Id: - - req-3e20c7f7-7583-412c-8382-abe4f8ad1222 - X-Openstack-Nova-Api-Version: - - '2.1' - status: - code: 200 - message: OK - url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/3/os-extra_specs -- recorded_at: '2016-04-14T10:10:02' - request: - body: - encoding: utf-8 - string: '' - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 - X-Auth-Token: - - 7fa3037ae2fe48ada8c626a51dc01ffd - method: GET - uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/4/os-extra_specs - response: - body: - encoding: null - string: |- - { - "extra_specs": {} - } - headers: - Connection: - - keep-alive - Content-Length: - - '19' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:10:02 GMT - Vary: - - X-OpenStack-Nova-API-Version - X-Compute-Request-Id: - - req-dadfb69e-f689-4215-ad82-4a4933536922 - X-Openstack-Nova-Api-Version: - - '2.1' - status: - code: 200 - message: OK - url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/4/os-extra_specs -- recorded_at: '2016-04-14T10:10:02' - request: - body: - encoding: utf-8 - string: '' - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 - X-Auth-Token: - - 7fa3037ae2fe48ada8c626a51dc01ffd - method: GET - uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/42/os-extra_specs - response: - body: - encoding: null - string: |- - { - "extra_specs": {} - } - headers: - Connection: - - keep-alive - Content-Length: - - '19' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:10:02 GMT - Vary: - - X-OpenStack-Nova-API-Version - X-Compute-Request-Id: - - req-e8f3a46a-ca1f-498f-8694-40d9836ca23c - X-Openstack-Nova-Api-Version: - - '2.1' - status: - code: 200 - message: OK - url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/42/os-extra_specs -- recorded_at: '2016-04-14T10:10:02' - request: - body: - encoding: utf-8 - string: '' - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 - X-Auth-Token: - - 7fa3037ae2fe48ada8c626a51dc01ffd - method: GET - uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/5/os-extra_specs - response: - body: - encoding: null - string: |- - { - "extra_specs": {} - } - headers: - Connection: - - keep-alive - Content-Length: - - '19' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:10:02 GMT - Vary: - - X-OpenStack-Nova-API-Version - X-Compute-Request-Id: - - req-4e6ccef8-6b65-4f83-92d4-6a0edcfda130 - X-Openstack-Nova-Api-Version: - - '2.1' - status: - code: 200 - message: OK - url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/5/os-extra_specs -- recorded_at: '2016-04-14T10:10:02' - request: - body: - encoding: utf-8 - string: '' - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 - X-Auth-Token: - - 7fa3037ae2fe48ada8c626a51dc01ffd - method: GET - uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/84/os-extra_specs - response: - body: - encoding: null - string: |- - { - "extra_specs": {} - } - headers: - Connection: - - keep-alive - Content-Length: - - '19' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:10:02 GMT - Vary: - - X-OpenStack-Nova-API-Version - X-Compute-Request-Id: - - req-b7d2664c-ca1f-4271-9935-c5ccef7a3b11 - X-Openstack-Nova-Api-Version: - - '2.1' - status: - code: 200 - message: OK - url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/84/os-extra_specs -- recorded_at: '2016-04-14T10:10:03' - request: - body: - encoding: utf-8 - string: '' - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - User-Agent: - - keystoneauth1/2.6.0 python-requests/2.9.1 CPython/2.7.6 - X-Auth-Token: - - 7fa3037ae2fe48ada8c626a51dc01ffd - method: GET - uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30/os-extra_specs - response: - body: - encoding: null - string: |- - { - "extra_specs": {} - } - headers: - Connection: - - keep-alive - Content-Length: - - '19' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:10:03 GMT - Vary: - - X-OpenStack-Nova-API-Version - X-Compute-Request-Id: - - req-b84891a9-84cc-4faf-816a-07d5fefdfc06 - X-Openstack-Nova-Api-Version: - - '2.1' - status: - code: 200 - message: OK - url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30/os-extra_specs -- recorded_at: '2016-04-14T10:10:03' - request: - body: - encoding: utf-8 - string: '' - headers: - Accept: - - application/json - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - Content-Length: - - '0' - User-Agent: - - python-novaclient - X-Auth-Token: - - 7fa3037ae2fe48ada8c626a51dc01ffd - method: DELETE - uri: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30 - response: - body: - encoding: null - string: '' - headers: - Connection: - - keep-alive - Content-Length: - - '0' - Content-Type: - - application/json - Date: - - Thu, 14 Apr 2016 10:10:03 GMT - Vary: - - X-OpenStack-Nova-API-Version - X-Compute-Request-Id: - - req-3e795065-ba65-437f-af6c-19373da4f3aa - X-Openstack-Nova-Api-Version: - - '2.1' - status: - code: 202 - message: Accepted - url: http://192.168.0.19:8774/v2.1/1c36b64c840a42cd9e9b931a369337f0/flavors/8987fc39-f5b8-46ce-bf1e-816e01623b30 -recorded_with: betamax/0.6.0 - diff --git a/shade/tests/unit/test_flavors.py b/shade/tests/unit/test_flavors.py index 9c82b29fe..e0288ba6f 100644 --- a/shade/tests/unit/test_flavors.py +++ b/shade/tests/unit/test_flavors.py @@ -11,37 +11,115 @@ # under the License. -import mock -from shade.tests.fakes import FakeFlavor, FakeProject - import shade -from keystoneauth1.fixture import keystoneauth_betamax -from keystoneauth1.fixture import serializer -from shade.tests import fakes from shade.tests.unit import base - -class TestFlavorsBetamax(base.BaseTestCase): +FLAVOR_ID = '0c1d9008-f546-4608-9e8f-f8bdaec8dddd' +ENDPOINT = 'https://compute.example.com/v2.1/1c36b64c840a42cd9e9b931a369337f0' +FAKE_FLAVOR = { + u'OS-FLV-DISABLED:disabled': False, + u'OS-FLV-EXT-DATA:ephemeral': 0, + u'disk': 1600, + u'id': u'0c1d9008-f546-4608-9e8f-f8bdaec8dddd', + u'links': [{ + u'href': u'{endpoint}/flavors/{id}'.format( + endpoint=ENDPOINT, id=FLAVOR_ID), + u'rel': u'self' + }, { + u'href': u'{endpoint}/flavors/{id}'.format( + endpoint=ENDPOINT, id=FLAVOR_ID), + u'rel': u'bookmark' + }], + u'name': u'vanilla', + u'os-flavor-access:is_public': True, + u'ram': 65536, + u'rxtx_factor': 1.0, + u'swap': u'', + u'vcpus': 24 +} +FAKE_FLAVOR_LIST = [FAKE_FLAVOR] + + +class TestFlavors(base.RequestsMockTestCase): def test_create_flavor(self): - self.useFixture(keystoneauth_betamax.BetamaxFixture( - cassette_name='test_create_flavor', - cassette_library_dir=self.fixtures_directory, - record=self.record_fixtures, - serializer=serializer.YamlJsonSerializer)) - - old_flavors = self.full_op_cloud.list_flavors() - self.full_op_cloud.create_flavor( - 'vanilla', 12345, 4, 100 + + self.register_uri( + 'POST', '{endpoint}/flavors'.format( + endpoint=ENDPOINT), + json={'flavor': FAKE_FLAVOR}, + validate=dict( + json={'flavor': { + "name": "vanilla", + "ram": 65536, + "vcpus": 24, + "swap": 0, + "os-flavor-access:is_public": True, + "rxtx_factor": 1.0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "disk": 1600, + "id": None + }})) + + self.register_uri( + 'GET', '{endpoint}/flavors/{id}'.format( + endpoint=ENDPOINT, id=FLAVOR_ID), + json={'flavor': FAKE_FLAVOR}) + + self.op_cloud.create_flavor( + 'vanilla', ram=65536, disk=1600, vcpus=24, ) + self.assert_calls() + + def test_delete_flavor(self): + self.register_uri( + 'GET', '{endpoint}/flavors/detail?is_public=None'.format( + endpoint=ENDPOINT), + json={'flavors': FAKE_FLAVOR_LIST}) + self.register_uri( + 'DELETE', '{endpoint}/flavors/{id}'.format( + endpoint=ENDPOINT, id=FLAVOR_ID)) + self.assertTrue(self.op_cloud.delete_flavor('vanilla')) + + self.assert_calls() + + def test_delete_flavor_not_found(self): + self.register_uri( + 'GET', '{endpoint}/flavors/detail?is_public=None'.format( + endpoint=ENDPOINT), + json={'flavors': []}) + + self.assertFalse(self.op_cloud.delete_flavor('invalid')) + + self.assert_calls() + + def test_delete_flavor_exception(self): + self.register_uri( + 'GET', '{endpoint}/flavors/detail?is_public=None'.format( + endpoint=ENDPOINT), + json={'flavors': FAKE_FLAVOR_LIST}) + self.register_uri( + 'DELETE', '{endpoint}/flavors/{id}'.format( + endpoint=ENDPOINT, id=FLAVOR_ID), + status_code=503) + self.assertRaises(shade.OpenStackCloudException, + self.op_cloud.delete_flavor, 'vanilla') + + def test_list_flavors(self): + self.register_uri( + 'GET', '{endpoint}/flavors/detail?is_public=None'.format( + endpoint=ENDPOINT), + json={'flavors': FAKE_FLAVOR_LIST}) + self.register_uri( + 'GET', '{endpoint}/flavors/{id}/os-extra_specs'.format( + endpoint=ENDPOINT, id=FLAVOR_ID), + json={'extra_specs': {}}) - # test that we have a new flavor added - new_flavors = self.full_op_cloud.list_flavors() - self.assertEqual(len(new_flavors) - len(old_flavors), 1) + flavors = self.cloud.list_flavors() # test that new flavor is created correctly found = False - for flavor in new_flavors: + for flavor in flavors: if flavor['name'] == 'vanilla': found = True break @@ -50,101 +128,69 @@ def test_create_flavor(self): if found: # check flavor content self.assertTrue(needed_keys.issubset(flavor.keys())) + self.assert_calls() - # delete created flavor - self.full_op_cloud.delete_flavor('vanilla') - - -class TestFlavors(base.TestCase): - - @mock.patch.object(shade.OpenStackCloud, '_compute_client') - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_delete_flavor(self, mock_nova, mock_compute): - mock_response = mock.Mock() - mock_response.json.return_value = dict(extra_specs=[]) - mock_compute.get.return_value = mock_response - mock_nova.flavors.list.return_value = [ - fakes.FakeFlavor('123', 'lemon', 100) - ] - self.assertTrue(self.op_cloud.delete_flavor('lemon')) - mock_nova.flavors.delete.assert_called_once_with(flavor='123') - - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_delete_flavor_not_found(self, mock_nova): - mock_nova.flavors.list.return_value = [] - self.assertFalse(self.op_cloud.delete_flavor('invalid')) - self.assertFalse(mock_nova.flavors.delete.called) - - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_delete_flavor_exception(self, mock_nova): - mock_nova.flavors.list.return_value = [ - fakes.FakeFlavor('123', 'lemon', 100) - ] - mock_nova.flavors.delete.side_effect = Exception() - self.assertRaises(shade.OpenStackCloudException, - self.op_cloud.delete_flavor, '') - - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_list_flavors(self, mock_nova): - self.op_cloud.list_flavors() - mock_nova.flavors.list.assert_called_once_with(is_public=None) - - @mock.patch.object(shade.OpenStackCloud, '_compute_client') - def test_set_flavor_specs(self, mock_compute): + def test_set_flavor_specs(self): extra_specs = dict(key1='value1') - self.op_cloud.set_flavor_specs(1, extra_specs) - mock_compute.post.assert_called_once_with( - '/flavors/{id}/os-extra_specs'.format(id=1), + self.register_uri( + 'POST', '{endpoint}/flavors/{id}/os-extra_specs'.format( + endpoint=ENDPOINT, id=1), json=dict(extra_specs=extra_specs)) - @mock.patch.object(shade.OpenStackCloud, '_compute_client') - def test_unset_flavor_specs(self, mock_compute): + self.op_cloud.set_flavor_specs(1, extra_specs) + self.assert_calls() + + def test_unset_flavor_specs(self): keys = ['key1', 'key2'] + for key in keys: + self.register_uri( + 'DELETE', + '{endpoint}/flavors/{id}/os-extra_specs/{key}'.format( + endpoint=ENDPOINT, id=1, key=key)) + self.op_cloud.unset_flavor_specs(1, keys) - api_spec = '/flavors/{id}/os-extra_specs/{key}' - self.assertEqual( - mock_compute.delete.call_args_list[0], - mock.call(api_spec.format(id=1, key='key1'))) - self.assertEqual( - mock_compute.delete.call_args_list[1], - mock.call(api_spec.format(id=1, key='key2'))) - - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_add_flavor_access(self, mock_nova): - self.op_cloud.add_flavor_access('flavor_id', 'tenant_id') - mock_nova.flavor_access.add_tenant_access.assert_called_once_with( - flavor='flavor_id', tenant='tenant_id' - ) + self.assert_calls() + + def test_add_flavor_access(self): + self.register_uri( + 'POST', '{endpoint}/flavors/{id}/action'.format( + endpoint=ENDPOINT, id='flavor_id'), + json={ + 'flavor_access': [{ + 'flavor_id': 'flavor_id', + 'tenant_id': 'tenant_id', + }]}, + validate=dict( + json={ + 'addTenantAccess': { + 'tenant': 'tenant_id', + }})) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_add_flavor_access_by_flavor(self, mock_nova): - flavor = FakeFlavor(id='flavor_id', name='flavor_name', ram=None) - tenant = FakeProject('tenant_id') - self.op_cloud.add_flavor_access(flavor, tenant) - mock_nova.flavor_access.add_tenant_access.assert_called_once_with( - flavor=flavor, tenant=tenant - ) + self.op_cloud.add_flavor_access('flavor_id', 'tenant_id') + self.assert_calls() + + def test_remove_flavor_access(self): + self.register_uri( + 'POST', '{endpoint}/flavors/{id}/action'.format( + endpoint=ENDPOINT, id='flavor_id'), + json={'flavor_access': []}, + validate=dict( + json={ + 'removeTenantAccess': { + 'tenant': 'tenant_id', + }})) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_remove_flavor_access(self, mock_nova): self.op_cloud.remove_flavor_access('flavor_id', 'tenant_id') - mock_nova.flavor_access.remove_tenant_access.assert_called_once_with( - flavor='flavor_id', tenant='tenant_id' - ) - - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_list_flavor_access(self, mock_nova): - mock_nova.flavors.list.return_value = [FakeFlavor( - id='flavor_id', name='flavor_name', ram=None)] - self.op_cloud.list_flavor_access('flavor_id') - mock_nova.flavor_access.list.assert_called_once_with( - flavor='flavor_id' - ) - - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_list_flavor_access_by_flavor(self, mock_nova): - flavor = FakeFlavor(id='flavor_id', name='flavor_name', ram=None) - self.op_cloud.list_flavor_access(flavor) - mock_nova.flavor_access.list.assert_called_once_with( - flavor=flavor - ) + self.assert_calls() + + def test_list_flavor_access(self): + self.register_uri( + 'GET', '{endpoint}/flavors/vanilla/os-flavor-access'.format( + endpoint=ENDPOINT), + json={ + 'flavor_access': [{ + 'flavor_id': 'vanilla', + 'tenant_id': 'tenant_id', + }]}) + self.op_cloud.list_flavor_access('vanilla') + self.assert_calls() diff --git a/test-requirements.txt b/test-requirements.txt index cf8c7fd72..b3cd3ae07 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,10 +1,8 @@ hacking>=0.11.0,<0.12 # Apache-2.0 -betamax-serializers>=0.1.1 coverage>=3.6 fixtures>=0.3.14 mock>=1.0 -python-openstackclient>=2.1.0 python-subunit oslosphinx>=2.2.0 # Apache-2.0 requests-mock From 7ca9d762f14ec5292e073b207844e66b142b255f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 31 Jan 2017 09:19:52 -0600 Subject: [PATCH 1231/3836] Move flavor cache tests to requests_mock Move the fake flavors from test_flavors to fakes so we can reuse them. This also fixes two small bugs that were found. extra_specs were Munch if fetched from nova and plain dict if they were defaulted. Also, if there was a problem fetching extra_specs, we defaulted them to empty list which is just plain wrong. Also, we were letting the request_ids attribute sneak through. Change-Id: I8ecf05580c557b21d123097e1f4be5c5664d366c --- shade/_normalize.py | 2 + shade/openstackcloud.py | 2 +- shade/tests/fakes.py | 26 ++++++++++++ shade/tests/unit/test_caching.py | 34 ++++++++++------ shade/tests/unit/test_flavors.py | 64 ++++++++++-------------------- shade/tests/unit/test_normalize.py | 7 +--- 6 files changed, 73 insertions(+), 62 deletions(-) diff --git a/shade/_normalize.py b/shade/_normalize.py index dd56d9d68..7ea68e970 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -161,6 +161,7 @@ def _normalize_flavor(self, flavor): flavor.pop('NAME_ATTR', None) flavor.pop('HUMAN_ID', None) flavor.pop('human_id', None) + flavor.pop('request_ids', None) ephemeral = int(_pop_or_get( flavor, 'OS-FLV-EXT-DATA:ephemeral', 0, self.strict_mode)) @@ -173,6 +174,7 @@ def _normalize_flavor(self, flavor): extra_specs = _pop_or_get( flavor, 'OS-FLV-WITH-EXT-SPECS:extra_specs', {}, self.strict_mode) extra_specs = flavor.pop('extra_specs', extra_specs) + extra_specs = munch.Munch(extra_specs) new_flavor['location'] = self.current_location new_flavor['id'] = flavor.pop('id') diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 83b38e72d..87cf70656 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1756,7 +1756,7 @@ def list_flavors(self, get_extra=True): try: flavor.extra_specs = self._compute_client.get(endpoint) except OpenStackCloudHTTPError as e: - flavor.extra_specs = [] + flavor.extra_specs = {} self.log.debug( 'Fetching extra specs for flavor failed:' ' %(msg)s', {'msg': str(e)}) diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index e9c01998c..8658067ea 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -18,6 +18,32 @@ """ +FLAVOR_ID = '0c1d9008-f546-4608-9e8f-f8bdaec8dddd' +ENDPOINT = 'https://compute.example.com/v2.1/1c36b64c840a42cd9e9b931a369337f0' +FAKE_FLAVOR = { + u'OS-FLV-DISABLED:disabled': False, + u'OS-FLV-EXT-DATA:ephemeral': 0, + u'disk': 1600, + u'id': u'0c1d9008-f546-4608-9e8f-f8bdaec8dddd', + u'links': [{ + u'href': u'{endpoint}/flavors/{id}'.format( + endpoint=ENDPOINT, id=FLAVOR_ID), + u'rel': u'self' + }, { + u'href': u'{endpoint}/flavors/{id}'.format( + endpoint=ENDPOINT, id=FLAVOR_ID), + u'rel': u'bookmark' + }], + u'name': u'vanilla', + u'os-flavor-access:is_public': True, + u'ram': 65536, + u'rxtx_factor': 1.0, + u'swap': u'', + u'vcpus': 24 +} +FAKE_FLAVOR_LIST = [FAKE_FLAVOR] + + class FakeEndpoint(object): def __init__(self, id, service_id, region, publicurl, internalurl=None, adminurl=None): diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 10127f23c..9ff92d425 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -91,7 +91,7 @@ def _(msg): ) -class TestMemoryCache(base.TestCase): +class TestMemoryCache(base.RequestsMockTestCase): def setUp(self): super(TestMemoryCache, self).setUp( @@ -255,6 +255,7 @@ def test_list_users(self, keystone_mock): @mock.patch.object(shade.OpenStackCloud, 'keystone_client') def test_modify_user_invalidates_cache(self, keystone_mock): + self.use_keystone_v2() fake_user = fakes.FakeUser('abc123', 'abc123@domain.test', 'abc123 name') # first cache an empty list @@ -298,22 +299,31 @@ def test_modify_user_invalidates_cache(self, keystone_mock): self.assertEqual([], self.cloud.list_users()) self.assertTrue(keystone_mock.users.delete.was_called) - @mock.patch.object(shade.OpenStackCloud, '_compute_client') - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_list_flavors(self, nova_mock, mock_compute): - # TODO(mordred) Change this to request_mock - nova_mock.flavors.list.return_value = [] - nova_mock.flavors.api.client.get.return_value = {} - mock_compute.get.return_value = {} + def test_list_flavors(self): + self.register_uri( + 'GET', '{endpoint}/flavors/detail?is_public=None'.format( + endpoint=fakes.ENDPOINT), + json={'flavors': []}) + + self.register_uri( + 'GET', '{endpoint}/flavors/detail?is_public=None'.format( + endpoint=fakes.ENDPOINT), + json={'flavors': fakes.FAKE_FLAVOR_LIST}) + self.register_uri( + 'GET', '{endpoint}/flavors/{id}/os-extra_specs'.format( + endpoint=fakes.ENDPOINT, id=fakes.FLAVOR_ID), + json={'extra_specs': {}}) + + self.assertEqual([], self.cloud.list_flavors()) + self.assertEqual([], self.cloud.list_flavors()) - fake_flavor = fakes.FakeFlavor('555', 'vanilla', 100) - fake_flavor_dict = self.cloud._normalize_flavor( - meta.obj_to_dict(fake_flavor)) - nova_mock.flavors.list.return_value = [fake_flavor] + fake_flavor_dict = self.cloud._normalize_flavor(fakes.FAKE_FLAVOR) self.cloud.list_flavors.invalidate(self.cloud) self.assertEqual([fake_flavor_dict], self.cloud.list_flavors()) + self.assert_calls() + @mock.patch.object(shade.OpenStackCloud, '_image_client') def test_list_images(self, mock_image_client): mock_image_client.get.return_value = [] diff --git a/shade/tests/unit/test_flavors.py b/shade/tests/unit/test_flavors.py index e0288ba6f..d144bd760 100644 --- a/shade/tests/unit/test_flavors.py +++ b/shade/tests/unit/test_flavors.py @@ -12,33 +12,9 @@ import shade +from shade.tests import fakes from shade.tests.unit import base -FLAVOR_ID = '0c1d9008-f546-4608-9e8f-f8bdaec8dddd' -ENDPOINT = 'https://compute.example.com/v2.1/1c36b64c840a42cd9e9b931a369337f0' -FAKE_FLAVOR = { - u'OS-FLV-DISABLED:disabled': False, - u'OS-FLV-EXT-DATA:ephemeral': 0, - u'disk': 1600, - u'id': u'0c1d9008-f546-4608-9e8f-f8bdaec8dddd', - u'links': [{ - u'href': u'{endpoint}/flavors/{id}'.format( - endpoint=ENDPOINT, id=FLAVOR_ID), - u'rel': u'self' - }, { - u'href': u'{endpoint}/flavors/{id}'.format( - endpoint=ENDPOINT, id=FLAVOR_ID), - u'rel': u'bookmark' - }], - u'name': u'vanilla', - u'os-flavor-access:is_public': True, - u'ram': 65536, - u'rxtx_factor': 1.0, - u'swap': u'', - u'vcpus': 24 -} -FAKE_FLAVOR_LIST = [FAKE_FLAVOR] - class TestFlavors(base.RequestsMockTestCase): @@ -46,8 +22,8 @@ def test_create_flavor(self): self.register_uri( 'POST', '{endpoint}/flavors'.format( - endpoint=ENDPOINT), - json={'flavor': FAKE_FLAVOR}, + endpoint=fakes.ENDPOINT), + json={'flavor': fakes.FAKE_FLAVOR}, validate=dict( json={'flavor': { "name": "vanilla", @@ -63,8 +39,8 @@ def test_create_flavor(self): self.register_uri( 'GET', '{endpoint}/flavors/{id}'.format( - endpoint=ENDPOINT, id=FLAVOR_ID), - json={'flavor': FAKE_FLAVOR}) + endpoint=fakes.ENDPOINT, id=fakes.FLAVOR_ID), + json={'flavor': fakes.FAKE_FLAVOR}) self.op_cloud.create_flavor( 'vanilla', ram=65536, disk=1600, vcpus=24, @@ -74,11 +50,11 @@ def test_create_flavor(self): def test_delete_flavor(self): self.register_uri( 'GET', '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=ENDPOINT), - json={'flavors': FAKE_FLAVOR_LIST}) + endpoint=fakes.ENDPOINT), + json={'flavors': fakes.FAKE_FLAVOR_LIST}) self.register_uri( 'DELETE', '{endpoint}/flavors/{id}'.format( - endpoint=ENDPOINT, id=FLAVOR_ID)) + endpoint=fakes.ENDPOINT, id=fakes.FLAVOR_ID)) self.assertTrue(self.op_cloud.delete_flavor('vanilla')) self.assert_calls() @@ -86,7 +62,7 @@ def test_delete_flavor(self): def test_delete_flavor_not_found(self): self.register_uri( 'GET', '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=ENDPOINT), + endpoint=fakes.ENDPOINT), json={'flavors': []}) self.assertFalse(self.op_cloud.delete_flavor('invalid')) @@ -96,11 +72,11 @@ def test_delete_flavor_not_found(self): def test_delete_flavor_exception(self): self.register_uri( 'GET', '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=ENDPOINT), - json={'flavors': FAKE_FLAVOR_LIST}) + endpoint=fakes.ENDPOINT), + json={'flavors': fakes.FAKE_FLAVOR_LIST}) self.register_uri( 'DELETE', '{endpoint}/flavors/{id}'.format( - endpoint=ENDPOINT, id=FLAVOR_ID), + endpoint=fakes.ENDPOINT, id=fakes.FLAVOR_ID), status_code=503) self.assertRaises(shade.OpenStackCloudException, self.op_cloud.delete_flavor, 'vanilla') @@ -108,11 +84,11 @@ def test_delete_flavor_exception(self): def test_list_flavors(self): self.register_uri( 'GET', '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=ENDPOINT), - json={'flavors': FAKE_FLAVOR_LIST}) + endpoint=fakes.ENDPOINT), + json={'flavors': fakes.FAKE_FLAVOR_LIST}) self.register_uri( 'GET', '{endpoint}/flavors/{id}/os-extra_specs'.format( - endpoint=ENDPOINT, id=FLAVOR_ID), + endpoint=fakes.ENDPOINT, id=fakes.FLAVOR_ID), json={'extra_specs': {}}) flavors = self.cloud.list_flavors() @@ -134,7 +110,7 @@ def test_set_flavor_specs(self): extra_specs = dict(key1='value1') self.register_uri( 'POST', '{endpoint}/flavors/{id}/os-extra_specs'.format( - endpoint=ENDPOINT, id=1), + endpoint=fakes.ENDPOINT, id=1), json=dict(extra_specs=extra_specs)) self.op_cloud.set_flavor_specs(1, extra_specs) @@ -146,7 +122,7 @@ def test_unset_flavor_specs(self): self.register_uri( 'DELETE', '{endpoint}/flavors/{id}/os-extra_specs/{key}'.format( - endpoint=ENDPOINT, id=1, key=key)) + endpoint=fakes.ENDPOINT, id=1, key=key)) self.op_cloud.unset_flavor_specs(1, keys) self.assert_calls() @@ -154,7 +130,7 @@ def test_unset_flavor_specs(self): def test_add_flavor_access(self): self.register_uri( 'POST', '{endpoint}/flavors/{id}/action'.format( - endpoint=ENDPOINT, id='flavor_id'), + endpoint=fakes.ENDPOINT, id='flavor_id'), json={ 'flavor_access': [{ 'flavor_id': 'flavor_id', @@ -172,7 +148,7 @@ def test_add_flavor_access(self): def test_remove_flavor_access(self): self.register_uri( 'POST', '{endpoint}/flavors/{id}/action'.format( - endpoint=ENDPOINT, id='flavor_id'), + endpoint=fakes.ENDPOINT, id='flavor_id'), json={'flavor_access': []}, validate=dict( json={ @@ -186,7 +162,7 @@ def test_remove_flavor_access(self): def test_list_flavor_access(self): self.register_uri( 'GET', '{endpoint}/flavors/vanilla/os-flavor-access'.format( - endpoint=ENDPOINT), + endpoint=fakes.ENDPOINT), json={ 'flavor_access': [{ 'flavor_id': 'vanilla', diff --git a/shade/tests/unit/test_normalize.py b/shade/tests/unit/test_normalize.py index 446165203..651ba78df 100644 --- a/shade/tests/unit/test_normalize.py +++ b/shade/tests/unit/test_normalize.py @@ -218,10 +218,8 @@ def test_normalize_flavors(self): u'disk_io_index': u'40', u'number_of_data_disks': u'1', u'policy_class': u'performance_flavor', - u'resize_policy_class': u'performance_flavor'}, - 'request_ids': []}, + u'resize_policy_class': u'performance_flavor'}}, 'ram': 8192, - 'request_ids': [], 'rxtx_factor': 1600.0, 'swap': 0, 'vcpus': 8} @@ -252,8 +250,7 @@ def test_normalize_flavors_strict(self): 'region_name': u'RegionOne', 'zone': None}, 'name': u'8 GB Performance', - 'properties': { - 'request_ids': []}, + 'properties': {}, 'ram': 8192, 'rxtx_factor': 1600.0, 'swap': 0, From 2e79cffb982087117cd8bfb0186b999f7856dfeb Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 31 Jan 2017 09:56:58 -0600 Subject: [PATCH 1232/3836] Migrate final flavor tests to requests_mock There were some tests in test_shade that should really be in test_flavors. Move them there, then transition them to requests_mock. To support the actions they need, add some additional flavors to the fake flavor list. Change-Id: Ide92fb0a926ba69b03132205f1467816844377d8 --- shade/tests/fakes.py | 56 ++++++++++++++----------- shade/tests/unit/test_caching.py | 14 ++++--- shade/tests/unit/test_flavors.py | 71 +++++++++++++++++++++++++++++--- shade/tests/unit/test_shade.py | 48 --------------------- 4 files changed, 107 insertions(+), 82 deletions(-) diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 8658067ea..5bcb7ffb9 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -18,30 +18,40 @@ """ -FLAVOR_ID = '0c1d9008-f546-4608-9e8f-f8bdaec8dddd' +FLAVOR_ID = u'0c1d9008-f546-4608-9e8f-f8bdaec8dddd' +CHOCOLATE_FLAVOR_ID = u'0c1d9008-f546-4608-9e8f-f8bdaec8ddde' +STRAWBERRY_FLAVOR_ID = u'0c1d9008-f546-4608-9e8f-f8bdaec8dddf' ENDPOINT = 'https://compute.example.com/v2.1/1c36b64c840a42cd9e9b931a369337f0' -FAKE_FLAVOR = { - u'OS-FLV-DISABLED:disabled': False, - u'OS-FLV-EXT-DATA:ephemeral': 0, - u'disk': 1600, - u'id': u'0c1d9008-f546-4608-9e8f-f8bdaec8dddd', - u'links': [{ - u'href': u'{endpoint}/flavors/{id}'.format( - endpoint=ENDPOINT, id=FLAVOR_ID), - u'rel': u'self' - }, { - u'href': u'{endpoint}/flavors/{id}'.format( - endpoint=ENDPOINT, id=FLAVOR_ID), - u'rel': u'bookmark' - }], - u'name': u'vanilla', - u'os-flavor-access:is_public': True, - u'ram': 65536, - u'rxtx_factor': 1.0, - u'swap': u'', - u'vcpus': 24 -} -FAKE_FLAVOR_LIST = [FAKE_FLAVOR] + + +def make_fake_flavor(flavor_id, name, ram=100, disk=1600, vcpus=24): + return { + u'OS-FLV-DISABLED:disabled': False, + u'OS-FLV-EXT-DATA:ephemeral': 0, + u'disk': disk, + u'id': flavor_id, + u'links': [{ + u'href': u'{endpoint}/flavors/{id}'.format( + endpoint=ENDPOINT, id=flavor_id), + u'rel': u'self' + }, { + u'href': u'{endpoint}/flavors/{id}'.format( + endpoint=ENDPOINT, id=flavor_id), + u'rel': u'bookmark' + }], + u'name': name, + u'os-flavor-access:is_public': True, + u'ram': ram, + u'rxtx_factor': 1.0, + u'swap': u'', + u'vcpus': vcpus + } +FAKE_FLAVOR = make_fake_flavor(FLAVOR_ID, 'vanilla') +FAKE_CHOCOLATE_FLAVOR = make_fake_flavor( + CHOCOLATE_FLAVOR_ID, 'chocolate', ram=200) +FAKE_STRAWBERRY_FLAVOR = make_fake_flavor( + STRAWBERRY_FLAVOR_ID, 'strawberry', ram=300) +FAKE_FLAVOR_LIST = [FAKE_FLAVOR, FAKE_CHOCOLATE_FLAVOR, FAKE_STRAWBERRY_FLAVOR] class FakeEndpoint(object): diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 9ff92d425..2313c19a0 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -309,18 +309,20 @@ def test_list_flavors(self): 'GET', '{endpoint}/flavors/detail?is_public=None'.format( endpoint=fakes.ENDPOINT), json={'flavors': fakes.FAKE_FLAVOR_LIST}) - self.register_uri( - 'GET', '{endpoint}/flavors/{id}/os-extra_specs'.format( - endpoint=fakes.ENDPOINT, id=fakes.FLAVOR_ID), - json={'extra_specs': {}}) + for flavor in fakes.FAKE_FLAVOR_LIST: + self.register_uri( + 'GET', '{endpoint}/flavors/{id}/os-extra_specs'.format( + endpoint=fakes.ENDPOINT, id=flavor['id']), + json={'extra_specs': {}}) self.assertEqual([], self.cloud.list_flavors()) self.assertEqual([], self.cloud.list_flavors()) - fake_flavor_dict = self.cloud._normalize_flavor(fakes.FAKE_FLAVOR) + fake_flavor_dicts = self.cloud._normalize_flavors( + fakes.FAKE_FLAVOR_LIST) self.cloud.list_flavors.invalidate(self.cloud) - self.assertEqual([fake_flavor_dict], self.cloud.list_flavors()) + self.assertEqual(fake_flavor_dicts, self.cloud.list_flavors()) self.assert_calls() diff --git a/shade/tests/unit/test_flavors.py b/shade/tests/unit/test_flavors.py index d144bd760..da06ca601 100644 --- a/shade/tests/unit/test_flavors.py +++ b/shade/tests/unit/test_flavors.py @@ -63,7 +63,7 @@ def test_delete_flavor_not_found(self): self.register_uri( 'GET', '{endpoint}/flavors/detail?is_public=None'.format( endpoint=fakes.ENDPOINT), - json={'flavors': []}) + json={'flavors': fakes.FAKE_FLAVOR_LIST}) self.assertFalse(self.op_cloud.delete_flavor('invalid')) @@ -86,10 +86,11 @@ def test_list_flavors(self): 'GET', '{endpoint}/flavors/detail?is_public=None'.format( endpoint=fakes.ENDPOINT), json={'flavors': fakes.FAKE_FLAVOR_LIST}) - self.register_uri( - 'GET', '{endpoint}/flavors/{id}/os-extra_specs'.format( - endpoint=fakes.ENDPOINT, id=fakes.FLAVOR_ID), - json={'extra_specs': {}}) + for flavor in fakes.FAKE_FLAVOR_LIST: + self.register_uri( + 'GET', '{endpoint}/flavors/{id}/os-extra_specs'.format( + endpoint=fakes.ENDPOINT, id=flavor['id']), + json={'extra_specs': {}}) flavors = self.cloud.list_flavors() @@ -106,6 +107,66 @@ def test_list_flavors(self): self.assertTrue(needed_keys.issubset(flavor.keys())) self.assert_calls() + def test_get_flavor_by_ram(self): + self.register_uri( + 'GET', '{endpoint}/flavors/detail?is_public=None'.format( + endpoint=fakes.ENDPOINT), + json={'flavors': fakes.FAKE_FLAVOR_LIST}) + for flavor in fakes.FAKE_FLAVOR_LIST: + self.register_uri( + 'GET', '{endpoint}/flavors/{id}/os-extra_specs'.format( + endpoint=fakes.ENDPOINT, id=flavor['id']), + json={'extra_specs': {}}) + + flavor = self.cloud.get_flavor_by_ram(ram=250) + self.assertEqual(fakes.STRAWBERRY_FLAVOR_ID, flavor['id']) + + def test_get_flavor_by_ram_and_include(self): + self.register_uri( + 'GET', '{endpoint}/flavors/detail?is_public=None'.format( + endpoint=fakes.ENDPOINT), + json={'flavors': fakes.FAKE_FLAVOR_LIST}) + for flavor in fakes.FAKE_FLAVOR_LIST: + self.register_uri( + 'GET', '{endpoint}/flavors/{id}/os-extra_specs'.format( + endpoint=fakes.ENDPOINT, id=flavor['id']), + json={'extra_specs': {}}) + flavor = self.cloud.get_flavor_by_ram(ram=150, include='strawberry') + self.assertEqual(fakes.STRAWBERRY_FLAVOR_ID, flavor['id']) + + def test_get_flavor_by_ram_not_found(self): + self.register_uri( + 'GET', '{endpoint}/flavors/detail?is_public=None'.format( + endpoint=fakes.ENDPOINT), + json={'flavors': []}) + self.assertRaises( + shade.OpenStackCloudException, + self.cloud.get_flavor_by_ram, + ram=100) + + def test_get_flavor_string_and_int(self): + self.register_uri( + 'GET', '{endpoint}/flavors/detail?is_public=None'.format( + endpoint=fakes.ENDPOINT), + json={'flavors': [fakes.make_fake_flavor('1', 'vanilla')]}) + self.register_uri( + 'GET', '{endpoint}/flavors/1/os-extra_specs'.format( + endpoint=fakes.ENDPOINT), + json={'extra_specs': {}}) + self.register_uri( + 'GET', '{endpoint}/flavors/detail?is_public=None'.format( + endpoint=fakes.ENDPOINT), + json={'flavors': [fakes.make_fake_flavor('1', 'vanilla')]}) + self.register_uri( + 'GET', '{endpoint}/flavors/1/os-extra_specs'.format( + endpoint=fakes.ENDPOINT), + json={'extra_specs': {}}) + + flavor1 = self.cloud.get_flavor('1') + self.assertEqual('1', flavor1['id']) + flavor2 = self.cloud.get_flavor(1) + self.assertEqual('1', flavor2['id']) + def test_set_flavor_specs(self): extra_specs = dict(key1='value1') self.register_uri( diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 1f06335c0..69992731c 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -512,54 +512,6 @@ def test_update_subnet_conflict_gw_ops(self, mock_get): self.cloud.update_subnet, '456', gateway_ip=gateway, disable_gateway_ip=True) - @mock.patch.object(shade.OpenStackCloud, '_compute_client') - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_get_flavor_by_ram(self, mock_nova_client, mock_compute): - vanilla = fakes.FakeFlavor('1', 'vanilla ice cream', 100) - chocolate = fakes.FakeFlavor('1', 'chocolate ice cream', 200) - mock_nova_client.flavors.list.return_value = [vanilla, chocolate] - mock_response = mock.Mock() - mock_response.json.return_value = dict(extra_specs=[]) - mock_compute.get.return_value = mock_response - flavor = self.cloud.get_flavor_by_ram(ram=150) - self.assertEqual(chocolate.id, flavor['id']) - - @mock.patch.object(shade.OpenStackCloud, '_compute_client') - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_get_flavor_by_ram_and_include( - self, mock_nova_client, mock_compute): - vanilla = fakes.FakeFlavor('1', 'vanilla ice cream', 100) - chocolate = fakes.FakeFlavor('2', 'chocoliate ice cream', 200) - strawberry = fakes.FakeFlavor('3', 'strawberry ice cream', 250) - mock_response = mock.Mock() - mock_response.json.return_value = dict(extra_specs=[]) - mock_compute.get.return_value = mock_response - mock_nova_client.flavors.list.return_value = [ - vanilla, chocolate, strawberry] - flavor = self.cloud.get_flavor_by_ram(ram=150, include='strawberry') - self.assertEqual(strawberry.id, flavor['id']) - - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_get_flavor_by_ram_not_found(self, mock_nova_client): - mock_nova_client.flavors.list.return_value = [] - self.assertRaises(shade.OpenStackCloudException, - self.cloud.get_flavor_by_ram, - ram=100) - - @mock.patch.object(shade.OpenStackCloud, '_compute_client') - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_get_flavor_string_and_int( - self, mock_nova_client, mock_compute): - vanilla = fakes.FakeFlavor('1', 'vanilla ice cream', 100) - mock_nova_client.flavors.list.return_value = [vanilla] - mock_response = mock.Mock() - mock_response.json.return_value = dict(extra_specs=[]) - mock_compute.get.return_value = mock_response - flavor1 = self.cloud.get_flavor('1') - self.assertEqual(vanilla.id, flavor1['id']) - flavor2 = self.cloud.get_flavor(1) - self.assertEqual(vanilla.id, flavor2['id']) - def test__neutron_exceptions_resource_not_found(self): with mock.patch.object( shade._tasks, 'NetworkList', From 7d982af93e849ee75ad0c1cd24684a96188d32c3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 31 Jan 2017 10:07:47 -0600 Subject: [PATCH 1233/3836] Migrate flavor usage in test_create_server to request_mock Don't fix the whole tests, since it does a bunch of neutron introspection. Also, make sure it doesn't call neutron yet. We'll get to that when we migrate neutron calls. Change-Id: I9faaecedb6c888119e0c1370e9c6ca1a2f6a2fb8 --- shade/tests/unit/test_create_server.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index b6c402f7f..d51800a9d 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -26,7 +26,7 @@ from shade.tests.unit import base -class TestCreateServer(base.TestCase): +class TestCreateServer(base.RequestsMockTestCase): def test_create_server_with_create_exception(self): """ @@ -308,11 +308,16 @@ def test_create_server_network_with_empty_nics(self, network='network-name', nics=[]) mock_get_network.assert_called_once_with(name_or_id='network-name') + @mock.patch.object(OpenStackCloud, 'get_server_by_id') @mock.patch.object(OpenStackCloud, 'get_image') @mock.patch.object(OpenStackCloud, 'nova_client') def test_create_server_get_flavor_image( - self, mock_nova, mock_image): + self, mock_nova, mock_image, mock_get_server_by_id): + self.register_uri( + 'GET', '{endpoint}/flavors/detail?is_public=None'.format( + endpoint=fakes.ENDPOINT), + json={'flavors': fakes.FAKE_FLAVOR_LIST}) self.cloud.create_server( - 'server-name', 'image-id', 'flavor-id') - mock_nova.flavors.list.assert_called_once() + 'server-name', 'image-id', 'vanilla', + nics=[{'net-id': 'some-network'}]) mock_image.assert_called_once() From 28fc6b02faed384337b187a13e7dc7ce01e3b318 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 31 Jan 2017 11:33:52 -0600 Subject: [PATCH 1234/3836] Move nova flavor interactions to REST One REST call is removed because python-novaclient made an extra get that we do not need to make. Change-Id: Idfc72a0f353c248e5749aea926031e70dcdf4f94 --- .../nova-flavor-to-rest-0a5757e35714a690.yaml | 5 +++ shade/_tasks.py | 39 ---------------- shade/openstackcloud.py | 4 +- shade/operatorcloud.py | 45 ++++++++++--------- shade/tests/unit/test_create_server.py | 2 + shade/tests/unit/test_flavors.py | 5 --- 6 files changed, 34 insertions(+), 66 deletions(-) create mode 100644 releasenotes/notes/nova-flavor-to-rest-0a5757e35714a690.yaml diff --git a/releasenotes/notes/nova-flavor-to-rest-0a5757e35714a690.yaml b/releasenotes/notes/nova-flavor-to-rest-0a5757e35714a690.yaml new file mode 100644 index 000000000..1e1f501c2 --- /dev/null +++ b/releasenotes/notes/nova-flavor-to-rest-0a5757e35714a690.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - Nova flavor operations are now handled via REST calls + instead of via novaclient. There should be no noticable + difference. diff --git a/shade/_tasks.py b/shade/_tasks.py index cfb13cc87..372c9eab7 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -82,45 +82,6 @@ def main(self, client): return client._project_manager.update(**self.args) -class FlavorList(task_manager.Task): - def main(self, client): - return client.nova_client.flavors.list(**self.args) - - -class FlavorCreate(task_manager.Task): - def main(self, client): - return client.nova_client.flavors.create(**self.args) - - -class FlavorDelete(task_manager.Task): - def main(self, client): - return client.nova_client.flavors.delete(**self.args) - - -class FlavorGet(task_manager.Task): - def main(self, client): - return client.nova_client.flavors.get(**self.args) - - -class FlavorListAccess(task_manager.Task): - def main(self, client): - return client.nova_client.flavor_access.list(**self.args) - - -class FlavorAddAccess(task_manager.Task): - def main(self, client): - return client.nova_client.flavor_access.add_tenant_access( - **self.args - ) - - -class FlavorRemoveAccess(task_manager.Task): - def main(self, client): - return client.nova_client.flavor_access.remove_tenant_access( - **self.args - ) - - class ServerList(task_manager.Task): def main(self, client): return client.nova_client.servers.list(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 87cf70656..6bac67d5f 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1745,8 +1745,8 @@ def list_flavors(self, get_extra=True): """ with _utils.shade_exceptions("Error fetching flavor list"): flavors = self._normalize_flavors( - self.manager.submit_task( - _tasks.FlavorList(is_public=None))) + self._compute_client.get( + '/flavors/detail', params=dict(is_public='None'))) with _utils.shade_exceptions("Error fetching flavor extra specs"): for flavor in flavors: diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 0c59e80d7..301d34e14 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1489,12 +1489,22 @@ def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", """ with _utils.shade_exceptions("Failed to create flavor {name}".format( name=name)): - flavor = self.manager.submit_task( - _tasks.FlavorCreate(name=name, ram=ram, vcpus=vcpus, disk=disk, - flavorid=flavorid, ephemeral=ephemeral, - swap=swap, rxtx_factor=rxtx_factor, - is_public=is_public) - ) + payload = { + 'disk': disk, + 'OS-FLV-EXT-DATA:ephemeral': ephemeral, + 'id': flavorid, + 'os-flavor-access:is_public': is_public, + 'name': name, + 'ram': ram, + 'rxtx_factor': rxtx_factor, + 'swap': swap, + 'vcpus': vcpus, + } + if flavorid == 'auto': + payload['id'] = None + flavor = self._compute_client.post( + '/flavors', + json=dict(flavor=payload)) return self._normalize_flavor(flavor) @@ -1515,7 +1525,8 @@ def delete_flavor(self, name_or_id): with _utils.shade_exceptions("Unable to delete flavor {name}".format( name=name_or_id)): - self.manager.submit_task(_tasks.FlavorDelete(flavor=flavor['id'])) + self._compute_client.delete( + '/flavors/{id}'.format(id=flavor['id'])) return True @@ -1562,16 +1573,11 @@ def _mod_flavor_access(self, action, flavor_id, project_id): with _utils.shade_exceptions("Error trying to {action} access from " "flavor ID {flavor}".format( action=action, flavor=flavor_id)): - if action == 'add': - self.manager.submit_task( - _tasks.FlavorAddAccess(flavor=flavor_id, - tenant=project_id) - ) - elif action == 'remove': - self.manager.submit_task( - _tasks.FlavorRemoveAccess(flavor=flavor_id, - tenant=project_id) - ) + endpoint = '/flavors/{id}/action'.format(id=flavor_id) + access = {'tenant': project_id} + access_key = '{action}TenantAccess'.format(action=action) + + self._compute_client.post(endpoint, json={access_key: access}) def add_flavor_access(self, flavor_id, project_id): """Grant access to a private flavor for a project/tenant. @@ -1605,9 +1611,8 @@ def list_flavor_access(self, flavor_id): with _utils.shade_exceptions("Error trying to list access from " "flavor ID {flavor}".format( flavor=flavor_id)): - projects = self.manager.submit_task( - _tasks.FlavorListAccess(flavor=flavor_id) - ) + projects = self._compute_client.get( + '/flavors/{id}/os-flavor-access'.format(id=flavor_id)) return _utils.normalize_flavor_accesses(projects) def create_role(self, name): diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index d51800a9d..5f3b018ff 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -321,3 +321,5 @@ def test_create_server_get_flavor_image( 'server-name', 'image-id', 'vanilla', nics=[{'net-id': 'some-network'}]) mock_image.assert_called_once() + + self.assert_calls() diff --git a/shade/tests/unit/test_flavors.py b/shade/tests/unit/test_flavors.py index da06ca601..9881b231c 100644 --- a/shade/tests/unit/test_flavors.py +++ b/shade/tests/unit/test_flavors.py @@ -37,11 +37,6 @@ def test_create_flavor(self): "id": None }})) - self.register_uri( - 'GET', '{endpoint}/flavors/{id}'.format( - endpoint=fakes.ENDPOINT, id=fakes.FLAVOR_ID), - json={'flavor': fakes.FAKE_FLAVOR}) - self.op_cloud.create_flavor( 'vanilla', ram=65536, disk=1600, vcpus=24, ) From 1dfe5e54fb71876ff21e2aade623cd6ada4ab782 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 31 Jan 2017 13:38:08 -0600 Subject: [PATCH 1235/3836] Remove troveclient from the direct dependency list We don't even actually use troveclient in shade other than the constructor. Make it an optional import. Change-Id: I60b924eb8dfb48c53da0befb39cf84e6c1ea8863 --- extras/install-tips.sh | 1 - .../notes/no-more-troveclient-0a4739c21432ac63.yaml | 6 ++++++ requirements.txt | 1 - shade/openstackcloud.py | 6 +++++- 4 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/no-more-troveclient-0a4739c21432ac63.yaml diff --git a/extras/install-tips.sh b/extras/install-tips.sh index b450e9124..66b7a0af1 100644 --- a/extras/install-tips.sh +++ b/extras/install-tips.sh @@ -21,7 +21,6 @@ for lib in \ python-keystoneclient \ python-cinderclient \ python-neutronclient \ - python-troveclient \ python-ironicclient \ python-heatclient \ python-designateclient \ diff --git a/releasenotes/notes/no-more-troveclient-0a4739c21432ac63.yaml b/releasenotes/notes/no-more-troveclient-0a4739c21432ac63.yaml new file mode 100644 index 000000000..1096921a5 --- /dev/null +++ b/releasenotes/notes/no-more-troveclient-0a4739c21432ac63.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - troveclient is no longer a hard dependency. Users + who were using shade to construct a troveclient + Client object should use os_client_config.make_legacy_client + instead. diff --git a/requirements.txt b/requirements.txt index c0187fe74..80e8951eb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,6 @@ python-novaclient>=2.21.0,!=2.27.0,!=2.32.0 python-keystoneclient>=0.11.0 python-cinderclient>=1.3.1 python-neutronclient>=2.3.10 -python-troveclient>=1.2.0 python-ironicclient>=0.10.0 python-heatclient>=1.0.0 python-designateclient>=2.1.0 diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 6bac67d5f..5c027c2e5 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -42,7 +42,6 @@ import neutronclient.neutron.client import novaclient.client import novaclient.exceptions as nova_exceptions -import troveclient.client import designateclient.client from shade.exc import * # noqa @@ -1187,6 +1186,11 @@ def cinder_client(self): @property def trove_client(self): + warnings.warn( + 'Using shade to get a trove_client object is deprecated. If you' + ' need a raw troveclient.client.Client object, please use' + ' make_legacy_client in os-client-config instead') + import troveclient.client if self._trove_client is None: self._trove_client = self._get_client( 'database', troveclient.client.Client) From 5139309492261927e9724dfabc0cd2157ef565c5 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 31 Jan 2017 18:14:00 -0600 Subject: [PATCH 1236/3836] Stop spamming logs with unreachable address message We emit a helpful debug warning when we have to pick from amongst random addresses and we couldn't choose one based on being able to reach it. However, there are a number of addresses for a server that EXPECT to not be reachable. We call them private addresses. Or maybe non-routable. Or something fancier and full of more networking acronyms that I'm not cool enough to know. In any case, only emit the warning when we expect to be able to route to the address and we still can't figure it out. Change-Id: I72d6f4745fe9f4169671bc933e92871358cebbf2 --- shade/meta.py | 32 ++++++++++++++++++-------------- shade/openstackcloud.py | 7 +++++-- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/shade/meta.py b/shade/meta.py index b90f704be..80c44431d 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -54,9 +54,9 @@ def find_nova_addresses(addresses, ext_tag=None, key_name=None, version=4): return ret -def get_server_ip(server, **kwargs): +def get_server_ip(server, public=False, **kwargs): addrs = find_nova_addresses(server['addresses'], **kwargs) - return find_best_address(addrs, socket.AF_INET) + return find_best_address(addrs, socket.AF_INET, public=public) def get_server_private_ip(server, cloud=None): @@ -124,14 +124,14 @@ def get_server_external_ipv4(cloud, server): # and possibly pre-configured network name ext_nets = cloud.get_external_ipv4_networks() for ext_net in ext_nets: - ext_ip = get_server_ip(server, key_name=ext_net['name']) + ext_ip = get_server_ip(server, key_name=ext_net['name'], public=True) if ext_ip is not None: return ext_ip # Try to get a floating IP address # Much as I might find floating IPs annoying, if it has one, that's # almost certainly the one that wants to be used - ext_ip = get_server_ip(server, ext_tag='floating') + ext_ip = get_server_ip(server, ext_tag='floating', public=True) if ext_ip is not None: return ext_ip @@ -140,7 +140,7 @@ def get_server_external_ipv4(cloud, server): # cloud (e.g. Rax) or have plain ol' floating IPs # Try to get an address from a network named 'public' - ext_ip = get_server_ip(server, key_name='public') + ext_ip = get_server_ip(server, key_name='public', public=True) if ext_ip is not None: return ext_ip @@ -161,12 +161,15 @@ def get_server_external_ipv4(cloud, server): return None -def find_best_address(addresses, family): +def find_best_address(addresses, family, public=False): if not addresses: return None if len(addresses) == 1: return addresses[0] - if len(addresses) > 1: + if len(addresses) > 1 and public: + # We only want to do this check if the address is supposed to be + # reachable. Otherwise we're just debug log spamming on every listing + # of private ip addresses for address in addresses: # Return the first one that is reachable try: @@ -177,11 +180,12 @@ def find_best_address(addresses, family): except Exception: pass # Give up and return the first - none work as far as we can tell - log = _log.setup_logging('shade') - log.debug( - 'The cloud returned multiple addresses, and none of them seem' - ' to work. That might be what you wanted, but we have no clue' - " what's going on, so we just picked one at random") + if public: + log = _log.setup_logging('shade') + log.debug( + 'The cloud returned multiple addresses, and none of them seem' + ' to work. That might be what you wanted, but we have no clue' + " what's going on, so we just picked one at random") return addresses[0] @@ -197,7 +201,7 @@ def get_server_external_ipv6(server): if server['accessIPv6']: return server['accessIPv6'] addresses = find_nova_addresses(addresses=server['addresses'], version=6) - return find_best_address(addresses, socket.AF_INET6) + return find_best_address(addresses, socket.AF_INET6, public=True) def get_server_default_ip(cloud, server): @@ -221,7 +225,7 @@ def get_server_default_ip(cloud, server): versions = [4] for version in versions: ext_ip = get_server_ip( - server, key_name=ext_net['name'], version=version) + server, key_name=ext_net['name'], version=version, public=True) if ext_ip is not None: return ext_ip return None diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 83b38e72d..f84736e58 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4627,7 +4627,7 @@ def _attach_ip_to_server( """ # Short circuit if we're asking to attach an IP that's already # attached - ext_ip = meta.get_server_ip(server, ext_tag='floating') + ext_ip = meta.get_server_ip(server, ext_tag='floating', public=True) if ext_ip == floating_ip['floating_ip_address']: return server @@ -4657,7 +4657,8 @@ def _attach_ip_to_server( "Timeout waiting for the floating IP to be attached.", wait=self._SERVER_AGE): server = self.get_server(server_id) - ext_ip = meta.get_server_ip(server, ext_tag='floating') + ext_ip = meta.get_server_ip( + server, ext_tag='floating', public=True) if ext_ip == floating_ip['floating_ip_address']: return server return server @@ -5511,6 +5512,8 @@ def _delete_server( return False if delete_ips: + # Don't pass public=True because we're just deleting. Testing + # for connectivity is not useful. floating_ip = meta.get_server_ip(server, ext_tag='floating') if floating_ip: ips = self.search_floating_ips(filters={ From 7102440687bc0c49601bfdc4e2925f731a9134da Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 1 Feb 2017 11:09:40 -0600 Subject: [PATCH 1237/3836] Support globbing in name or id checks The ansible os_server_facts module support doing this, and a recent change to add support for returning servers by ids there made it seem like perhaps the functionality should get rolled up into the shade layer and be available to everyone. Change-Id: Ib1096d606840767ce884e6dd58a6928fee0ae6e2 --- .../fnmatch-name-or-id-f658fe26f84086c8.yaml | 5 +++++ shade/_utils.py | 10 +++++++--- shade/tests/unit/test__utils.py | 16 ++++++++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/fnmatch-name-or-id-f658fe26f84086c8.yaml diff --git a/releasenotes/notes/fnmatch-name-or-id-f658fe26f84086c8.yaml b/releasenotes/notes/fnmatch-name-or-id-f658fe26f84086c8.yaml new file mode 100644 index 000000000..dcdccd249 --- /dev/null +++ b/releasenotes/notes/fnmatch-name-or-id-f658fe26f84086c8.yaml @@ -0,0 +1,5 @@ +--- +features: + - name_or_id parameters to search/get methods now support + filename-like globbing. This means search_servers('nb0*') + will return all servers whose names start with 'nb0'. diff --git a/shade/_utils.py b/shade/_utils.py index bfcfbd416..b3da5b5c9 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -13,6 +13,7 @@ # limitations under the License. import contextlib +import fnmatch import inspect import jmespath import munch @@ -83,7 +84,8 @@ def _filter_list(data, name_or_id, filters): each dictionary contains an 'id' and 'name' key if a value for name_or_id is given. :param string name_or_id: - The name or ID of the entity being filtered. + The name or ID of the entity being filtered. Can be a glob pattern, + such as 'nb01*'. :param filters: A dictionary of meta data to use for further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: @@ -100,9 +102,11 @@ def _filter_list(data, name_or_id, filters): if name_or_id: identifier_matches = [] for e in data: - e_id = str(e.get('id', None)) + e_id = e.get('id', None) e_name = e.get('name', None) - if str(name_or_id) in (e_id, e_name): + if ((e_id and fnmatch.fnmatch(str(e_id), str(name_or_id))) or + (e_name and fnmatch.fnmatch( + str(e_name), str(name_or_id)))): identifier_matches.append(e) data = identifier_matches diff --git a/shade/tests/unit/test__utils.py b/shade/tests/unit/test__utils.py index bfe566bd4..28235e8dd 100644 --- a/shade/tests/unit/test__utils.py +++ b/shade/tests/unit/test__utils.py @@ -40,6 +40,22 @@ def test__filter_list_name_or_id(self): ret = _utils._filter_list(data, 'donald', None) self.assertEqual([el1], ret) + def test__filter_list_name_or_id_glob(self): + el1 = dict(id=100, name='donald') + el2 = dict(id=200, name='pluto') + el3 = dict(id=200, name='pluto-2') + data = [el1, el2, el3] + ret = _utils._filter_list(data, 'pluto*', None) + self.assertEqual([el2, el3], ret) + + def test__filter_list_name_or_id_glob_not_found(self): + el1 = dict(id=100, name='donald') + el2 = dict(id=200, name='pluto') + el3 = dict(id=200, name='pluto-2') + data = [el1, el2, el3] + ret = _utils._filter_list(data, 'q*', None) + self.assertEqual([], ret) + def test__filter_list_filter(self): el1 = dict(id=100, name='donald', other='duck') el2 = dict(id=200, name='donald', other='trump') From 93894627ecfeda5e7b85b0880899241eb2ed1467 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Thu, 2 Feb 2017 14:35:48 -0500 Subject: [PATCH 1238/3836] Cleanup various Sphinx warnings during doc build This primarily cleans up doc files and mentions for modules which no longer exist. There's one cleanup to a docstring in the Compute proxy about a long class being split on two lines without a continuation backslash. Change-Id: I9f40d17789d7587e6964c3b655f4dae226dfc6a8 --- doc/source/users/resources/bare_metal/index.rst | 2 +- doc/source/users/resources/compute/index.rst | 2 -- doc/source/users/resources/compute/v2/limits.rst | 8 ++++---- .../users/resources/compute/v2/server_meta.rst | 12 ------------ .../users/resources/compute/v2/server_metadata.rst | 12 ------------ doc/source/users/resources/image/index.rst | 1 - doc/source/users/resources/image/v2/tag.rst | 12 ------------ doc/source/users/resources/telemetry/index.rst | 2 -- doc/source/users/resources/telemetry/v2/alarm.rst | 12 ------------ .../users/resources/telemetry/v2/alarm_change.rst | 12 ------------ openstack/compute/v2/_proxy.py | 4 ++-- 11 files changed, 7 insertions(+), 72 deletions(-) delete mode 100644 doc/source/users/resources/compute/v2/server_meta.rst delete mode 100644 doc/source/users/resources/compute/v2/server_metadata.rst delete mode 100644 doc/source/users/resources/image/v2/tag.rst delete mode 100644 doc/source/users/resources/telemetry/v2/alarm.rst delete mode 100644 doc/source/users/resources/telemetry/v2/alarm_change.rst diff --git a/doc/source/users/resources/bare_metal/index.rst b/doc/source/users/resources/bare_metal/index.rst index 4aa391c86..348a6e4f6 100644 --- a/doc/source/users/resources/bare_metal/index.rst +++ b/doc/source/users/resources/bare_metal/index.rst @@ -8,4 +8,4 @@ Bare Metal Resources v1/chassis v1/node v1/port - v1/portgroup + v1/port_group diff --git a/doc/source/users/resources/compute/index.rst b/doc/source/users/resources/compute/index.rst index 1731a574c..f597c7d86 100644 --- a/doc/source/users/resources/compute/index.rst +++ b/doc/source/users/resources/compute/index.rst @@ -12,5 +12,3 @@ Compute Resources v2/server v2/server_interface v2/server_ip - v2/server_meta - v2/server_metadata diff --git a/doc/source/users/resources/compute/v2/limits.rst b/doc/source/users/resources/compute/v2/limits.rst index ecea1f946..090988bb9 100644 --- a/doc/source/users/resources/compute/v2/limits.rst +++ b/doc/source/users/resources/compute/v2/limits.rst @@ -19,10 +19,10 @@ The ``AbsoluteLimits`` class inherits from :class:`~openstack.resource.Resource` .. autoclass:: openstack.compute.v2.limits.AbsoluteLimits :members: -The RateLimits Class --------------------- +The RateLimit Class +------------------- -The ``RateLimits`` class inherits from :class:`~openstack.resource.Resource`. +The ``RateLimit`` class inherits from :class:`~openstack.resource.Resource`. -.. autoclass:: openstack.compute.v2.limits.RateLimits +.. autoclass:: openstack.compute.v2.limits.RateLimit :members: diff --git a/doc/source/users/resources/compute/v2/server_meta.rst b/doc/source/users/resources/compute/v2/server_meta.rst deleted file mode 100644 index cd06b5692..000000000 --- a/doc/source/users/resources/compute/v2/server_meta.rst +++ /dev/null @@ -1,12 +0,0 @@ -openstack.compute.v2.server_meta -================================ - -.. automodule:: openstack.compute.v2.server_meta - -The ServerMeta Class --------------------- - -The ``ServerMeta`` class inherits from :class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.compute.v2.server_meta.ServerMeta - :members: diff --git a/doc/source/users/resources/compute/v2/server_metadata.rst b/doc/source/users/resources/compute/v2/server_metadata.rst deleted file mode 100644 index 0248a1eb3..000000000 --- a/doc/source/users/resources/compute/v2/server_metadata.rst +++ /dev/null @@ -1,12 +0,0 @@ -openstack.compute.v2.server_metadata -==================================== - -.. automodule:: openstack.compute.v2.server_metadata - -The ServerMetadata Class ------------------------- - -The ``ServerMetadata`` class inherits from :class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.compute.v2.server_metadata.ServerMetadata - :members: diff --git a/doc/source/users/resources/image/index.rst b/doc/source/users/resources/image/index.rst index 3cd17ed8d..2696ab1e1 100644 --- a/doc/source/users/resources/image/index.rst +++ b/doc/source/users/resources/image/index.rst @@ -14,4 +14,3 @@ Image v2 Resources v2/image v2/member - v2/tag diff --git a/doc/source/users/resources/image/v2/tag.rst b/doc/source/users/resources/image/v2/tag.rst deleted file mode 100644 index 1947fc827..000000000 --- a/doc/source/users/resources/image/v2/tag.rst +++ /dev/null @@ -1,12 +0,0 @@ -openstack.image.v2.tag -====================== - -.. automodule:: openstack.image.v2.tag - -The Tag Class -------------- - -The ``Tag`` class inherits from :class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.image.v2.tag.Tag - :members: diff --git a/doc/source/users/resources/telemetry/index.rst b/doc/source/users/resources/telemetry/index.rst index f377bc749..b429949d6 100644 --- a/doc/source/users/resources/telemetry/index.rst +++ b/doc/source/users/resources/telemetry/index.rst @@ -4,8 +4,6 @@ Telemetry Resources .. toctree:: :maxdepth: 1 - v2/alarm - v2/alarm_change v2/capability v2/meter v2/resource diff --git a/doc/source/users/resources/telemetry/v2/alarm.rst b/doc/source/users/resources/telemetry/v2/alarm.rst deleted file mode 100644 index 3109d90e5..000000000 --- a/doc/source/users/resources/telemetry/v2/alarm.rst +++ /dev/null @@ -1,12 +0,0 @@ -openstack.telemetry.v2.alarm -============================ - -.. automodule:: openstack.telemetry.v2.alarm - -The Alarm Class ---------------- - -The ``Alarm`` class inherits from :class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.telemetry.v2.alarm.Alarm - :members: diff --git a/doc/source/users/resources/telemetry/v2/alarm_change.rst b/doc/source/users/resources/telemetry/v2/alarm_change.rst deleted file mode 100644 index 55c8d1e0c..000000000 --- a/doc/source/users/resources/telemetry/v2/alarm_change.rst +++ /dev/null @@ -1,12 +0,0 @@ -openstack.telemetry.v2.alarm_change -=================================== - -.. automodule:: openstack.telemetry.v2.alarm_change - -The AlarmChange Class ---------------------- - -The ``AlarmChange`` class inherits from :class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.telemetry.v2.alarm_change.AlarmChange - :members: diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index d049bb1bf..d20af42d3 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -884,8 +884,8 @@ def availability_zones(self, details=False): requires extra permission. :returns: A generator of availability zone - :rtype: :class:`~openstack.compute.v2.availability_zone. - AvailabilityZone` + :rtype: :class:`~openstack.compute.v2.availability_zone.\ + AvailabilityZone` """ if details: az = availability_zone.AvailabilityZoneDetail From b223649239ac124a118e450882d667f6384b1daa Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 1 Feb 2017 08:36:21 -0600 Subject: [PATCH 1239/3836] Honor cloud.private in the check for public connectivity While we don't want to do connectivity checks on private addresses most of the time, if we've been told that we prefer private addreses to public, then we can expect to be able to connect to those. Change-Id: Ia81df87de7d8f4ba3d4ecc9c68906f14efffa6bc --- shade/meta.py | 48 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/shade/meta.py b/shade/meta.py index 80c44431d..ea57fad93 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -54,9 +54,21 @@ def find_nova_addresses(addresses, ext_tag=None, key_name=None, version=4): return ret -def get_server_ip(server, public=False, **kwargs): +def get_server_ip(server, public=False, cloud_public=True, **kwargs): + """Get an IP from the Nova addresses dict + + :param server: The server to pull the address from + :param public: Whether the address we're looking for should be considered + 'public' and therefore reachabiliity tests should be + used. (defaults to False) + :param cloud_public: Whether the cloud has been configured to use private + IPs from servers as the interface_ip. This inverts the + public reachability logic, as in this case it's the + private ip we expect shade to be able to reach + """ addrs = find_nova_addresses(server['addresses'], **kwargs) - return find_best_address(addrs, socket.AF_INET, public=public) + return find_best_address( + addrs, socket.AF_INET, public=public, cloud_public=cloud_public) def get_server_private_ip(server, cloud=None): @@ -82,16 +94,20 @@ def get_server_private_ip(server, cloud=None): if cloud: int_nets = cloud.get_internal_ipv4_networks() for int_net in int_nets: - int_ip = get_server_ip(server, key_name=int_net['name']) + int_ip = get_server_ip( + server, key_name=int_net['name'], + cloud_public=not cloud.private) if int_ip is not None: return int_ip - ip = get_server_ip(server, ext_tag='fixed', key_name='private') + ip = get_server_ip( + server, ext_tag='fixed', key_name='private') if ip: return ip # Last resort, and Rackspace - return get_server_ip(server, key_name='private') + return get_server_ip( + server, key_name='private') def get_server_external_ipv4(cloud, server): @@ -124,14 +140,18 @@ def get_server_external_ipv4(cloud, server): # and possibly pre-configured network name ext_nets = cloud.get_external_ipv4_networks() for ext_net in ext_nets: - ext_ip = get_server_ip(server, key_name=ext_net['name'], public=True) + ext_ip = get_server_ip( + server, key_name=ext_net['name'], public=True, + cloud_public=not cloud.private) if ext_ip is not None: return ext_ip # Try to get a floating IP address # Much as I might find floating IPs annoying, if it has one, that's # almost certainly the one that wants to be used - ext_ip = get_server_ip(server, ext_tag='floating', public=True) + ext_ip = get_server_ip( + server, ext_tag='floating', public=True, + cloud_public=not cloud.private) if ext_ip is not None: return ext_ip @@ -140,7 +160,9 @@ def get_server_external_ipv4(cloud, server): # cloud (e.g. Rax) or have plain ol' floating IPs # Try to get an address from a network named 'public' - ext_ip = get_server_ip(server, key_name='public', public=True) + ext_ip = get_server_ip( + server, key_name='public', public=True, + cloud_public=not cloud.private) if ext_ip is not None: return ext_ip @@ -161,12 +183,13 @@ def get_server_external_ipv4(cloud, server): return None -def find_best_address(addresses, family, public=False): +def find_best_address(addresses, family, public=False, cloud_public=True): + do_check = public == cloud_public if not addresses: return None if len(addresses) == 1: return addresses[0] - if len(addresses) > 1 and public: + if len(addresses) > 1 and do_check: # We only want to do this check if the address is supposed to be # reachable. Otherwise we're just debug log spamming on every listing # of private ip addresses @@ -180,7 +203,7 @@ def find_best_address(addresses, family, public=False): except Exception: pass # Give up and return the first - none work as far as we can tell - if public: + if do_check: log = _log.setup_logging('shade') log.debug( 'The cloud returned multiple addresses, and none of them seem' @@ -225,7 +248,8 @@ def get_server_default_ip(cloud, server): versions = [4] for version in versions: ext_ip = get_server_ip( - server, key_name=ext_net['name'], version=version, public=True) + server, key_name=ext_net['name'], version=version, public=True, + cloud_public=not cloud.private) if ext_ip is not None: return ext_ip return None From 7878a9daf60d5b65115fb930ff6ffcda3f5eb07b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 4 Feb 2017 07:32:10 -0600 Subject: [PATCH 1240/3836] Port in log-on-failure code from zuul v3 Zuul v3 picked up a great technique for logging a ton of useful information for errors but also not blowing out the subunit stream on success. We do it a little differently here - we don't have the need for the flexibility in logging config that zuul has. Also, add the ability to set an env var and print the logging even on success. (useful when developing single tests when one wants to inspect that a success really did what we think. Change-Id: Ibcb9a4aa33c16450df252c5e0f32a62c5cc821bc --- shade/tests/base.py | 44 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/shade/tests/base.py b/shade/tests/base.py index 7c8ed59ae..8915e2b08 100644 --- a/shade/tests/base.py +++ b/shade/tests/base.py @@ -16,10 +16,11 @@ import os import fixtures +import logging import munch +from six import StringIO import testtools - -import shade +import testtools.content _TRUE_VALUES = ('true', '1', 'yes') @@ -54,8 +55,25 @@ def setUp(self): stderr = self.useFixture(fixtures.StringStream('stderr')).stream self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) - shade.simple_logging(debug=True) - self.log_fixture = self.useFixture(fixtures.FakeLogger()) + self._log_stream = StringIO() + if os.environ.get('OS_ALWAYS_LOG') in _TRUE_VALUES: + self.addCleanup(self.printLogs) + else: + self.addOnException(self.attachLogs) + + handler = logging.StreamHandler(self._log_stream) + formatter = logging.Formatter('%(asctime)s %(name)-32s %(message)s') + handler.setFormatter(formatter) + + logger = logging.getLogger('shade') + logger.setLevel(logging.DEBUG) + logger.addHandler(handler) + + # Enable HTTP level tracing + logger = logging.getLogger('keystoneauth') + logger.setLevel(logging.DEBUG) + logger.addHandler(handler) + logger.propagate = False def assertEqual(self, first, second, *args, **kwargs): '''Munch aware wrapper''' @@ -65,3 +83,21 @@ def assertEqual(self, first, second, *args, **kwargs): second = second.toDict() return super(TestCase, self).assertEqual( first, second, *args, **kwargs) + + def printLogs(self, *args): + self._log_stream.seek(0) + print(self._log_stream.read()) + + def attachLogs(self, *args): + def reader(): + self._log_stream.seek(0) + while True: + x = self._log_stream.read(4096) + if not x: + break + yield x.encode('utf8') + content = testtools.content.content_from_reader( + reader, + testtools.content_type.UTF8_TEXT, + False) + self.addDetail('logging', content) From d8f62dc0e9a07a5c6ad42a50962c2c4869833821 Mon Sep 17 00:00:00 2001 From: Yuval Shalev Date: Wed, 4 Jan 2017 17:54:43 +0200 Subject: [PATCH 1241/3836] Added project role assignment Change-Id: I4eb3d430365f3b22c1a77faea71f710fb565082f --- examples/identity/list.py | 28 +++++++ openstack/identity/v3/_proxy.py | 82 +++++++++++++++++++ openstack/identity/v3/domain.py | 55 +++++++++++++ openstack/identity/v3/project.py | 55 +++++++++++++ openstack/identity/v3/role.py | 5 -- openstack/identity/v3/role_assignment.py | 41 ++++++++++ .../v3/role_domain_group_assignment.py | 34 ++++++++ .../v3/role_domain_user_assignment.py | 34 ++++++++ .../v3/role_project_group_assignment.py | 34 ++++++++ .../v3/role_project_user_assignment.py | 34 ++++++++ openstack/tests/unit/identity/v3/test_role.py | 2 - .../unit/identity/v3/test_role_assignment.py | 44 ++++++++++ .../v3/test_role_domain_group_assignment.py | 45 ++++++++++ .../v3/test_role_domain_user_assignment.py | 45 ++++++++++ .../v3/test_role_project_group_assignment.py | 45 ++++++++++ .../v3/test_role_project_user_assignment.py | 45 ++++++++++ 16 files changed, 621 insertions(+), 7 deletions(-) create mode 100644 openstack/identity/v3/role_assignment.py create mode 100644 openstack/identity/v3/role_domain_group_assignment.py create mode 100644 openstack/identity/v3/role_domain_user_assignment.py create mode 100644 openstack/identity/v3/role_project_group_assignment.py create mode 100644 openstack/identity/v3/role_project_user_assignment.py create mode 100644 openstack/tests/unit/identity/v3/test_role_assignment.py create mode 100644 openstack/tests/unit/identity/v3/test_role_domain_group_assignment.py create mode 100644 openstack/tests/unit/identity/v3/test_role_domain_user_assignment.py create mode 100644 openstack/tests/unit/identity/v3/test_role_project_group_assignment.py create mode 100644 openstack/tests/unit/identity/v3/test_role_project_user_assignment.py diff --git a/examples/identity/list.py b/examples/identity/list.py index e7662b043..fff73a50f 100644 --- a/examples/identity/list.py +++ b/examples/identity/list.py @@ -78,3 +78,31 @@ def list_roles(conn): for role in conn.identity.roles(): print(role) + + +def list_role_domain_group_assignments(conn): + print("List Roles assignments for a group on domain:") + + for role in conn.identity.role_domain_group_assignments(): + print(role) + + +def list_role_domain_user_assignments(conn): + print("List Roles assignments for a user on domain:") + + for role in conn.identity.role_project_user_assignments(): + print(role) + + +def list_role_project_group_assignments(conn): + print("List Roles assignments for a group on project:") + + for role in conn.identity.role_project_group_assignments(): + print(role) + + +def list_role_project_user_assignments(conn): + print("List Roles assignments for a user on project:") + + for role in conn.identity.role_project_user_assignments(): + print(role) diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 5b7bd3cb3..81e295ee1 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import openstack.exceptions as exception from openstack.identity.v3 import credential as _credential from openstack.identity.v3 import domain as _domain from openstack.identity.v3 import endpoint as _endpoint @@ -18,6 +19,15 @@ from openstack.identity.v3 import project as _project from openstack.identity.v3 import region as _region from openstack.identity.v3 import role as _role +from openstack.identity.v3 import role_assignment as _role_assignment +from openstack.identity.v3 import role_domain_group_assignment \ + as _role_domain_group_assignment +from openstack.identity.v3 import role_domain_user_assignment \ + as _role_domain_user_assignment +from openstack.identity.v3 import role_project_group_assignment \ + as _role_project_group_assignment +from openstack.identity.v3 import role_project_user_assignment \ + as _role_project_user_assignment from openstack.identity.v3 import service as _service from openstack.identity.v3 import trust as _trust from openstack.identity.v3 import user as _user @@ -875,3 +885,75 @@ def update_role(self, role, **attrs): :rtype: :class:`~openstack.identity.v3.role.Role` """ return self._update(_role.Role, role, **attrs) + + def role_assignments_filter(self, domain=None, project=None, group=None, + user=None): + """Retrieve a generator of roles assigned to user/group + + :param domain: Either the ID of a domain or a + :class:`~openstack.identity.v3.domain.Domain` instance. + :param project: Either the ID of a project or a + :class:`~openstack.identity.v3.project.Project` + instance. + :param group: Either the ID of a group or a + :class:`~openstack.identity.v3.group.Group` instance. + :param user: Either the ID of a user or a + :class:`~openstack.identity.v3.user.User` instance. + :return: A generator of role instances. + :rtype: :class:`~openstack.identity.v3.role.Role` + """ + if domain and project: + raise exception.InvalidRequest( + 'Only one of domain or project can be specified') + + if domain is None and project is None: + raise exception.InvalidRequest( + 'Either domain or project should be specified') + + if group and user: + raise exception.InvalidRequest( + 'Only one of group or user can be specified') + + if group is None and user is None: + raise exception.InvalidRequest( + 'Either group or user should be specified') + + if domain: + domain = self._get_resource(_domain.Domain, domain) + if group: + group = self._get_resource(_group.Group, group) + return self._list( + _role_domain_group_assignment.RoleDomainGroupAssignment, + paginated=False, domain_id=domain.id, group_id=group.id) + else: + user = self._get_resource(_user.User, user) + return self._list( + _role_domain_user_assignment.RoleDomainUserAssignment, + paginated=False, domain_id=domain.id, user_id=user.id) + else: + project = self._get_resource(_project.Project, project) + if group: + group = self._get_resource(_group.Group, group) + return self._list( + _role_project_group_assignment.RoleProjectGroupAssignment, + paginated=False, project_id=project.id, group_id=group.id) + else: + user = self._get_resource(_user.User, user) + return self._list( + _role_project_user_assignment.RoleProjectUserAssignment, + paginated=False, project_id=project.id, user_id=user.id) + + def role_assignments(self, **query): + """Retrieve a generator of role_assignment + + :class:`~openstack.identity.v3.user.User` instance. + :param kwargs \*\*query: Optional query parameters to be sent to limit + the resources being returned. The options + are: group_id, role_id, scope_domain_id, + scope_project_id, user_id, include_names, + include_subtree. + :return: + :class:`~openstack.identity.v3.role_assignment.RoleAssignment` + """ + return self._list(_role_assignment.RoleAssignment, + paginated=False, **query) diff --git a/openstack/identity/v3/domain.py b/openstack/identity/v3/domain.py index b7f330ad9..42e57649d 100644 --- a/openstack/identity/v3/domain.py +++ b/openstack/identity/v3/domain.py @@ -12,6 +12,7 @@ from openstack.identity import identity_service from openstack import resource2 as resource +from openstack import utils class Domain(resource.Resource): @@ -43,3 +44,57 @@ class Domain(resource.Resource): name = resource.Body('name') #: The links related to the domain resource. links = resource.Body('links') + + def assign_role_to_user(self, session, user, role): + """Assign role to user on domain""" + url = utils.urljoin(self.base_path, self.id, 'users', + user.id, 'roles', role.id) + resp = session.put(url, endpoint_filter=self.service) + if resp.status_code == 204: + return True + return False + + def validate_user_has_role(self, session, user, role): + """Validates that a user has a role on a domain""" + url = utils.urljoin(self.base_path, self.id, 'users', + user.id, 'roles', role.id) + resp = session.head(url, endpoint_filter=self.service) + if resp.status_code == 201: + return True + return False + + def unassign_role_from_user(self, session, user, role): + """Unassigns a role from a user on a domain""" + url = utils.urljoin(self.base_path, self.id, 'users', + user.id, 'roles', role.id) + resp = session.delete(url, endpoint_filter=self.service) + if resp.status_code == 204: + return True + return False + + def assign_role_to_group(self, session, group, role): + """Assign role to group on domain""" + url = utils.urljoin(self.base_path, self.id, 'groups', + group.id, 'roles', role.id) + resp = session.put(url, endpoint_filter=self.service) + if resp.status_code == 204: + return True + return False + + def validate_group_has_role(self, session, group, role): + """Validates that a group has a role on a domain""" + url = utils.urljoin(self.base_path, self.id, 'groups', + group.id, 'roles', role.id) + resp = session.head(url, endpoint_filter=self.service) + if resp.status_code == 201: + return True + return False + + def unassign_role_from_group(self, session, group, role): + """Unassigns a role from a group on a domain""" + url = utils.urljoin(self.base_path, self.id, 'groups', + group.id, 'roles', role.id) + resp = session.delete(url, endpoint_filter=self.service) + if resp.status_code == 204: + return True + return False diff --git a/openstack/identity/v3/project.py b/openstack/identity/v3/project.py index 439962a32..3a1ab714c 100644 --- a/openstack/identity/v3/project.py +++ b/openstack/identity/v3/project.py @@ -12,6 +12,7 @@ from openstack.identity import identity_service from openstack import resource2 as resource +from openstack import utils class Project(resource.Resource): @@ -50,3 +51,57 @@ class Project(resource.Resource): #: The ID of the parent of the project. #: New in version 3.4 parent_id = resource.Body('parent_id') + + def assign_role_to_user(self, session, user, role): + """Assign role to user on project""" + url = utils.urljoin(self.base_path, self.id, 'users', + user.id, 'roles', role.id) + resp = session.put(url, endpoint_filter=self.service) + if resp.status_code == 204: + return True + return False + + def validate_user_has_role(self, session, user, role): + """Validates that a user has a role on a project""" + url = utils.urljoin(self.base_path, self.id, 'users', + user.id, 'roles', role.id) + resp = session.head(url, endpoint_filter=self.service) + if resp.status_code == 201: + return True + return False + + def unassign_role_from_user(self, session, user, role): + """Unassigns a role from a user on a project""" + url = utils.urljoin(self.base_path, self.id, 'users', + user.id, 'roles', role.id) + resp = session.delete(url, endpoint_filter=self.service) + if resp.status_code == 204: + return True + return False + + def assign_role_to_group(self, session, group, role): + """Assign role to group on project""" + url = utils.urljoin(self.base_path, self.id, 'groups', + group.id, 'roles', role.id) + resp = session.put(url, endpoint_filter=self.service) + if resp.status_code == 204: + return True + return False + + def validate_group_has_role(self, session, group, role): + """Validates that a group has a role on a project""" + url = utils.urljoin(self.base_path, self.id, 'groups', + group.id, 'roles', role.id) + resp = session.head(url, endpoint_filter=self.service) + if resp.status_code == 201: + return True + return False + + def unassign_role_from_group(self, session, group, role): + """Unassigns a role from a group on a project""" + url = utils.urljoin(self.base_path, self.id, 'groups', + group.id, 'roles', role.id) + resp = session.delete(url, endpoint_filter=self.service) + if resp.status_code == 204: + return True + return False diff --git a/openstack/identity/v3/role.py b/openstack/identity/v3/role.py index 7c7d6b1a2..bd946d3d2 100644 --- a/openstack/identity/v3/role.py +++ b/openstack/identity/v3/role.py @@ -32,11 +32,6 @@ class Role(resource.Resource): 'name', 'domain_id') # Properties - #: References the domain ID which owns the role; if a domain ID is not - #: specified by the client, the Identity service implementation will - #: default it to the domain ID to which the client's token is scoped. - #: *Type: string* - domain_id = resource.Body('domain_id') #: Unique role name, within the owning domain. *Type: string* name = resource.Body('name') #: The links for the service resource. diff --git a/openstack/identity/v3/role_assignment.py b/openstack/identity/v3/role_assignment.py new file mode 100644 index 000000000..459c275b7 --- /dev/null +++ b/openstack/identity/v3/role_assignment.py @@ -0,0 +1,41 @@ +# 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.identity import identity_service +from openstack import resource2 as resource + + +class RoleAssignment(resource.Resource): + resource_key = 'role_assignment' + resources_key = 'role_assignments' + base_path = '/role_assignments' + service = identity_service.IdentityService() + + # capabilities + allow_list = True + + _query_mapping = resource.QueryParameters( + 'group_id', 'role_id', 'scope_domain_id', 'scope_project_id', + 'user_id', 'effective', 'include_names', 'include_subtree' + ) + + # Properties + #: The links for the service resource. + links = resource.Body('links') + #: The role (dictionary contains only id) *Type: dict* + role = resource.Body('role', type=dict) + #: The scope (either domain or group dictionary contains id) *Type: dict* + scope = resource.Body('scope', type=dict) + #: The user (dictionary contains only id) *Type: dict* + user = resource.Body('user', type=dict) + #: The group (dictionary contains only id) *Type: dict* + group = resource.Body('group', type=dict) diff --git a/openstack/identity/v3/role_domain_group_assignment.py b/openstack/identity/v3/role_domain_group_assignment.py new file mode 100644 index 000000000..d7fadc3cc --- /dev/null +++ b/openstack/identity/v3/role_domain_group_assignment.py @@ -0,0 +1,34 @@ +# 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.identity import identity_service +from openstack import resource2 as resource + + +class RoleDomainGroupAssignment(resource.Resource): + resource_key = 'role' + resources_key = 'roles' + base_path = '/domains/%(domain_id)s/groups/%(group_id)s/roles' + service = identity_service.IdentityService() + + # capabilities + allow_list = True + + # Properties + #: name of the role *Type: string* + name = resource.Body('name') + #: The links for the service resource. + links = resource.Body('links') + #: The ID of the domain to list assignment from. *Type: string* + domain_id = resource.URI('domain_id') + #: The ID of the group to list assignment from. *Type: string* + group_id = resource.URI('group_id') diff --git a/openstack/identity/v3/role_domain_user_assignment.py b/openstack/identity/v3/role_domain_user_assignment.py new file mode 100644 index 000000000..b4db0f522 --- /dev/null +++ b/openstack/identity/v3/role_domain_user_assignment.py @@ -0,0 +1,34 @@ +# 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.identity import identity_service +from openstack import resource2 as resource + + +class RoleDomainUserAssignment(resource.Resource): + resource_key = 'role' + resources_key = 'roles' + base_path = '/domains/%(domain_id)s/users/%(user_id)s/roles' + service = identity_service.IdentityService() + + # capabilities + allow_list = True + + # Properties + #: name of the role *Type: string* + name = resource.Body('name') + #: The links for the service resource. + links = resource.Body('links') + #: The ID of the domain to list assignment from. *Type: string* + domain_id = resource.URI('domain_id') + #: The ID of the user to list assignment from. *Type: string* + user_id = resource.URI('user_id') diff --git a/openstack/identity/v3/role_project_group_assignment.py b/openstack/identity/v3/role_project_group_assignment.py new file mode 100644 index 000000000..225410b5d --- /dev/null +++ b/openstack/identity/v3/role_project_group_assignment.py @@ -0,0 +1,34 @@ +# 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.identity import identity_service +from openstack import resource2 as resource + + +class RoleProjectGroupAssignment(resource.Resource): + resource_key = 'role' + resources_key = 'roles' + base_path = '/projects/%(project_id)s/groups/%(group_id)s/roles' + service = identity_service.IdentityService() + + # capabilities + allow_list = True + + # Properties + #: name of the role *Type: string* + name = resource.Body('name') + #: The links for the service resource. + links = resource.Body('links') + #: The ID of the project to list assignment from. *Type: string* + project_id = resource.URI('project_id') + #: The ID of the group to list assignment from. *Type: string* + group_id = resource.URI('group_id') diff --git a/openstack/identity/v3/role_project_user_assignment.py b/openstack/identity/v3/role_project_user_assignment.py new file mode 100644 index 000000000..b1989794a --- /dev/null +++ b/openstack/identity/v3/role_project_user_assignment.py @@ -0,0 +1,34 @@ +# 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.identity import identity_service +from openstack import resource2 as resource + + +class RoleProjectUserAssignment(resource.Resource): + resource_key = 'role' + resources_key = 'roles' + base_path = '/projects/%(project_id)s/users/%(user_id)s/roles' + service = identity_service.IdentityService() + + # capabilities + allow_list = True + + # Properties + #: name of the role *Type: string* + name = resource.Body('name') + #: The links for the service resource. + links = resource.Body('links') + #: The ID of the project to list assignment from. *Type: string* + project_id = resource.URI('project_id') + #: The ID of the user to list assignment from. *Type: string* + user_id = resource.URI('user_id') diff --git a/openstack/tests/unit/identity/v3/test_role.py b/openstack/tests/unit/identity/v3/test_role.py index ef1663ffb..e338657a0 100644 --- a/openstack/tests/unit/identity/v3/test_role.py +++ b/openstack/tests/unit/identity/v3/test_role.py @@ -16,7 +16,6 @@ IDENTIFIER = 'IDENTIFIER' EXAMPLE = { - 'domain_id': '1', 'id': IDENTIFIER, 'links': {'self': 'http://example.com/user1'}, 'name': '2', @@ -40,7 +39,6 @@ def test_basic(self): def test_make_it(self): sot = role.Role(**EXAMPLE) - self.assertEqual(EXAMPLE['domain_id'], sot.domain_id) self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['links'], sot.links) self.assertEqual(EXAMPLE['name'], sot.name) diff --git a/openstack/tests/unit/identity/v3/test_role_assignment.py b/openstack/tests/unit/identity/v3/test_role_assignment.py new file mode 100644 index 000000000..a0a1e5050 --- /dev/null +++ b/openstack/tests/unit/identity/v3/test_role_assignment.py @@ -0,0 +1,44 @@ +# 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 testtools + +from openstack.identity.v3 import role_assignment + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'id': IDENTIFIER, + 'links': {'self': 'http://example.com/user1'}, + 'scope': {'domain': {'id': '2'}}, + 'user': {'id': '3'}, + 'group': {'id': '4'} +} + + +class TestRoleAssignment(testtools.TestCase): + + def test_basic(self): + sot = role_assignment.RoleAssignment() + self.assertEqual('role_assignment', sot.resource_key) + self.assertEqual('role_assignments', sot.resources_key) + self.assertEqual('/role_assignments', + sot.base_path) + self.assertEqual('identity', sot.service.service_type) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = role_assignment.RoleAssignment(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['links'], sot.links) + self.assertEqual(EXAMPLE['scope'], sot.scope) + self.assertEqual(EXAMPLE['user'], sot.user) + self.assertEqual(EXAMPLE['group'], sot.group) diff --git a/openstack/tests/unit/identity/v3/test_role_domain_group_assignment.py b/openstack/tests/unit/identity/v3/test_role_domain_group_assignment.py new file mode 100644 index 000000000..5e0777d20 --- /dev/null +++ b/openstack/tests/unit/identity/v3/test_role_domain_group_assignment.py @@ -0,0 +1,45 @@ +# 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 testtools + +from openstack.identity.v3 import role_domain_group_assignment + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'id': IDENTIFIER, + 'links': {'self': 'http://example.com/user1'}, + 'name': '2', + 'domain_id': '3', + 'group_id': '4' +} + + +class TestRoleDomainGroupAssignment(testtools.TestCase): + + def test_basic(self): + sot = role_domain_group_assignment.RoleDomainGroupAssignment() + self.assertEqual('role', sot.resource_key) + self.assertEqual('roles', sot.resources_key) + self.assertEqual('/domains/%(domain_id)s/groups/%(group_id)s/roles', + sot.base_path) + self.assertEqual('identity', sot.service.service_type) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = \ + role_domain_group_assignment.RoleDomainGroupAssignment(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['links'], sot.links) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['domain_id'], sot.domain_id) + self.assertEqual(EXAMPLE['group_id'], sot.group_id) diff --git a/openstack/tests/unit/identity/v3/test_role_domain_user_assignment.py b/openstack/tests/unit/identity/v3/test_role_domain_user_assignment.py new file mode 100644 index 000000000..cc7de8bd7 --- /dev/null +++ b/openstack/tests/unit/identity/v3/test_role_domain_user_assignment.py @@ -0,0 +1,45 @@ +# 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 testtools + +from openstack.identity.v3 import role_domain_user_assignment + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'id': IDENTIFIER, + 'links': {'self': 'http://example.com/user1'}, + 'name': '2', + 'domain_id': '3', + 'user_id': '4' +} + + +class TestRoleDomainUserAssignment(testtools.TestCase): + + def test_basic(self): + sot = role_domain_user_assignment.RoleDomainUserAssignment() + self.assertEqual('role', sot.resource_key) + self.assertEqual('roles', sot.resources_key) + self.assertEqual('/domains/%(domain_id)s/users/%(user_id)s/roles', + sot.base_path) + self.assertEqual('identity', sot.service.service_type) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = \ + role_domain_user_assignment.RoleDomainUserAssignment(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['links'], sot.links) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['domain_id'], sot.domain_id) + self.assertEqual(EXAMPLE['user_id'], sot.user_id) diff --git a/openstack/tests/unit/identity/v3/test_role_project_group_assignment.py b/openstack/tests/unit/identity/v3/test_role_project_group_assignment.py new file mode 100644 index 000000000..81c96e7db --- /dev/null +++ b/openstack/tests/unit/identity/v3/test_role_project_group_assignment.py @@ -0,0 +1,45 @@ +# 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 testtools + +from openstack.identity.v3 import role_project_group_assignment + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'id': IDENTIFIER, + 'links': {'self': 'http://example.com/user1'}, + 'name': '2', + 'project_id': '3', + 'group_id': '4' +} + + +class TestRoleProjectGroupAssignment(testtools.TestCase): + + def test_basic(self): + sot = role_project_group_assignment.RoleProjectGroupAssignment() + self.assertEqual('role', sot.resource_key) + self.assertEqual('roles', sot.resources_key) + self.assertEqual('/projects/%(project_id)s/groups/%(group_id)s/roles', + sot.base_path) + self.assertEqual('identity', sot.service.service_type) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = \ + role_project_group_assignment.RoleProjectGroupAssignment(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['links'], sot.links) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + self.assertEqual(EXAMPLE['group_id'], sot.group_id) diff --git a/openstack/tests/unit/identity/v3/test_role_project_user_assignment.py b/openstack/tests/unit/identity/v3/test_role_project_user_assignment.py new file mode 100644 index 000000000..5314c0bc5 --- /dev/null +++ b/openstack/tests/unit/identity/v3/test_role_project_user_assignment.py @@ -0,0 +1,45 @@ +# 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 testtools + +from openstack.identity.v3 import role_project_user_assignment + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'id': IDENTIFIER, + 'links': {'self': 'http://example.com/user1'}, + 'name': '2', + 'project_id': '3', + 'user_id': '4' +} + + +class TestRoleProjectUserAssignment(testtools.TestCase): + + def test_basic(self): + sot = role_project_user_assignment.RoleProjectUserAssignment() + self.assertEqual('role', sot.resource_key) + self.assertEqual('roles', sot.resources_key) + self.assertEqual('/projects/%(project_id)s/users/%(user_id)s/roles', + sot.base_path) + self.assertEqual('identity', sot.service.service_type) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = \ + role_project_user_assignment.RoleProjectUserAssignment(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['links'], sot.links) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + self.assertEqual(EXAMPLE['user_id'], sot.user_id) From 994cd9992c51764246e91a6d71e853bb9a4aa10f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 4 Feb 2017 06:55:24 -0600 Subject: [PATCH 1242/3836] Start switching neutron tests Change-Id: I87a02155de3fb8330256bc51cf4499c9e7e339a2 --- shade/tests/unit/test_meta.py | 501 ++++++++++++++++------------------ 1 file changed, 236 insertions(+), 265 deletions(-) diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index efecabce3..17dd93d5e 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -14,8 +14,6 @@ import mock -from neutronclient.common import exceptions as neutron_exceptions - import shade from shade import meta from shade.tests import fakes @@ -228,7 +226,7 @@ def get_default_network(self): ] -class TestMeta(base.TestCase): +class TestMeta(base.RequestsMockTestCase): def test_find_nova_addresses_key_name(self): # Note 198.51.100.0/24 is TEST-NET-2 from rfc5737 addrs = {'public': [{'addr': '198.51.100.1', 'version': 4}], @@ -275,17 +273,16 @@ def test_get_server_ip(self): self.assertEqual( PUBLIC_V4, meta.get_server_ip(srv, ext_tag='floating')) - @mock.patch.object(shade.OpenStackCloud, 'has_service') - @mock.patch.object(shade.OpenStackCloud, 'list_subnets') - @mock.patch.object(shade.OpenStackCloud, 'list_networks') - def test_get_server_private_ip( - self, mock_list_networks, mock_list_subnets, mock_has_service): - mock_has_service.return_value = True - mock_list_subnets.return_value = SUBNETS_WITH_NAT - mock_list_networks.return_value = [{ - 'id': 'test-net-id', - 'name': 'test-net-name' - }] + def test_get_server_private_ip(self): + self.register_uri( + 'GET', 'https://network.example.com/v2.0/networks.json', + json={'networks': [{ + 'id': 'test-net-id', + 'name': 'test-net-name' + }]}) + self.register_uri( + 'GET', 'https://network.example.com/v2.0/subnets.json', + json={'subnets': SUBNETS_WITH_NAT}) srv = meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', @@ -299,56 +296,57 @@ def test_get_server_private_ip( self.assertEqual( PRIVATE_V4, meta.get_server_private_ip(srv, self.cloud)) - mock_has_service.assert_called_with('network') - mock_list_networks.assert_called_once_with() + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'list_ports') - @mock.patch.object(shade.OpenStackCloud, 'list_floating_ips') - @mock.patch.object(shade.OpenStackCloud, 'list_subnets') - @mock.patch.object(shade.OpenStackCloud, 'list_server_security_groups') @mock.patch.object(shade.OpenStackCloud, 'get_volumes') @mock.patch.object(shade.OpenStackCloud, 'get_image_name') @mock.patch.object(shade.OpenStackCloud, 'get_flavor_name') - @mock.patch.object(shade.OpenStackCloud, 'has_service') - @mock.patch.object(shade.OpenStackCloud, 'list_networks') def test_get_server_private_ip_devstack( - self, mock_list_networks, mock_has_service, + self, mock_get_flavor_name, mock_get_image_name, - mock_get_volumes, - mock_list_server_security_groups, - mock_list_subnets, - mock_list_floating_ips, - mock_list_ports): + mock_get_volumes): + mock_get_image_name.return_value = 'cirros-0.3.4-x86_64-uec' mock_get_flavor_name.return_value = 'm1.tiny' - mock_has_service.return_value = True mock_get_volumes.return_value = [] - mock_list_subnets.return_value = SUBNETS_WITH_NAT - mock_list_networks.return_value = [ - { - 'id': 'test_pnztt_net', - 'name': 'test_pnztt_net', - 'router:external': False, - }, - { - 'id': 'private', - 'name': 'private', - }, - ] - mock_list_floating_ips.return_value = [ - { - 'port_id': 'test_port_id', - 'fixed_ip_address': PRIVATE_V4, - 'floating_ip_address': PUBLIC_V4, - } - ] - mock_list_ports.return_value = [ - { + + self.register_uri( + 'GET', + 'https://network.example.com/v2.0/ports.json?device_id=test-id', + json={'ports': [{ 'id': 'test_port_id', 'mac_address': 'fa:16:3e:ae:7d:42', 'device_id': 'test-id', - } - ] + }]}) + + self.register_uri( + 'GET', + 'https://network.example.com/v2.0/floatingips.json' + '?port_id=test_port_id', + json={'floatingips': []}) + + self.register_uri( + 'GET', 'https://network.example.com/v2.0/networks.json', + json={'networks': [ + { + 'id': 'test_pnztt_net', + 'name': 'test_pnztt_net', + 'router:external': False, + }, + { + 'id': 'private', + 'name': 'private', + } + ]}) + + self.register_uri( + 'GET', 'https://network.example.com/v2.0/subnets.json', + json={'subnets': SUBNETS_WITH_NAT}) + + self.register_uri( + 'GET', '{endpoint}/servers/test-id/os-security-groups'.format( + endpoint=fakes.ENDPOINT), + json={'security_groups': []}) srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', @@ -365,44 +363,41 @@ def test_get_server_private_ip_devstack( ))) self.assertEqual(PRIVATE_V4, srv['private_v4']) - mock_has_service.assert_called_with('volume') - mock_list_networks.assert_called_once_with() - mock_list_floating_ips.assert_called_once_with( - filters={'port_id': 'test_port_id'}) - mock_list_ports.assert_called_once_with({'device_id': 'test-id'}) - - @mock.patch.object(shade.OpenStackCloud, 'list_floating_ips') - @mock.patch.object(shade.OpenStackCloud, 'list_subnets') - @mock.patch.object(shade.OpenStackCloud, 'list_server_security_groups') + self.assert_calls() + @mock.patch.object(shade.OpenStackCloud, 'get_volumes') @mock.patch.object(shade.OpenStackCloud, 'get_image_name') @mock.patch.object(shade.OpenStackCloud, 'get_flavor_name') - @mock.patch.object(shade.OpenStackCloud, 'has_service') - @mock.patch.object(shade.OpenStackCloud, 'list_networks') def test_get_server_private_ip_no_fip( - self, mock_list_networks, mock_has_service, + self, mock_get_flavor_name, mock_get_image_name, - mock_get_volumes, - mock_list_server_security_groups, - mock_list_subnets, - mock_list_floating_ips): + mock_get_volumes): self.cloud._floating_ip_source = None + mock_get_image_name.return_value = 'cirros-0.3.4-x86_64-uec' mock_get_flavor_name.return_value = 'm1.tiny' - mock_has_service.return_value = True mock_get_volumes.return_value = [] - mock_list_subnets.return_value = SUBNETS_WITH_NAT - mock_list_networks.return_value = [ - { - 'id': 'test_pnztt_net', - 'name': 'test_pnztt_net', - 'router:external': False, - }, - { - 'id': 'private', - 'name': 'private', - }, - ] + + self.register_uri( + 'GET', 'https://network.example.com/v2.0/networks.json', + json={'networks': [ + { + 'id': 'test_pnztt_net', + 'name': 'test_pnztt_net', + 'router:external': False, + }, + { + 'id': 'private', + 'name': 'private', + } + ]}) + self.register_uri( + 'GET', 'https://network.example.com/v2.0/subnets.json', + json={'subnets': SUBNETS_WITH_NAT}) + self.register_uri( + 'GET', '{endpoint}/servers/test-id/os-security-groups'.format( + endpoint=fakes.ENDPOINT), + json={'security_groups': []}) srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', @@ -419,42 +414,39 @@ def test_get_server_private_ip_no_fip( ))) self.assertEqual(PRIVATE_V4, srv['private_v4']) - mock_has_service.assert_called_with('volume') - mock_list_networks.assert_called_once_with() - mock_list_floating_ips.assert_not_called() + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'list_floating_ips') - @mock.patch.object(shade.OpenStackCloud, 'list_subnets') - @mock.patch.object(shade.OpenStackCloud, 'list_server_security_groups') @mock.patch.object(shade.OpenStackCloud, 'get_volumes') @mock.patch.object(shade.OpenStackCloud, 'get_image_name') @mock.patch.object(shade.OpenStackCloud, 'get_flavor_name') - @mock.patch.object(shade.OpenStackCloud, 'has_service') - @mock.patch.object(shade.OpenStackCloud, 'list_networks') def test_get_server_cloud_no_fips( - self, mock_list_networks, mock_has_service, + self, mock_get_flavor_name, mock_get_image_name, - mock_get_volumes, - mock_list_server_security_groups, - mock_list_subnets, - mock_list_floating_ips): + mock_get_volumes): self.cloud._floating_ip_source = None mock_get_image_name.return_value = 'cirros-0.3.4-x86_64-uec' mock_get_flavor_name.return_value = 'm1.tiny' - mock_has_service.return_value = True mock_get_volumes.return_value = [] - mock_list_subnets.return_value = SUBNETS_WITH_NAT - mock_list_networks.return_value = [ - { - 'id': 'test_pnztt_net', - 'name': 'test_pnztt_net', - 'router:external': False, - }, - { - 'id': 'private', - 'name': 'private', - }, - ] + self.register_uri( + 'GET', 'https://network.example.com/v2.0/networks.json', + json={'networks': [ + { + 'id': 'test_pnztt_net', + 'name': 'test_pnztt_net', + 'router:external': False, + }, + { + 'id': 'private', + 'name': 'private', + } + ]}) + self.register_uri( + 'GET', 'https://network.example.com/v2.0/subnets.json', + json={'subnets': SUBNETS_WITH_NAT}) + self.register_uri( + 'GET', '{endpoint}/servers/test-id/os-security-groups'.format( + endpoint=fakes.ENDPOINT), + json={'security_groups': []}) srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', @@ -469,58 +461,60 @@ def test_get_server_cloud_no_fips( ))) self.assertEqual(PRIVATE_V4, srv['private_v4']) - mock_has_service.assert_called_with('volume') - mock_list_networks.assert_called_once_with() - mock_list_floating_ips.assert_not_called() - - @mock.patch.object(shade.OpenStackCloud, 'list_floating_ips') - @mock.patch.object(shade.OpenStackCloud, 'list_ports') - @mock.patch.object(shade.OpenStackCloud, 'list_subnets') - @mock.patch.object(shade.OpenStackCloud, 'list_server_security_groups') + self.assert_calls() + @mock.patch.object(shade.OpenStackCloud, 'get_volumes') @mock.patch.object(shade.OpenStackCloud, 'get_image_name') @mock.patch.object(shade.OpenStackCloud, 'get_flavor_name') - @mock.patch.object(shade.OpenStackCloud, 'has_service') - @mock.patch.object(shade.OpenStackCloud, 'list_networks') def test_get_server_cloud_missing_fips( - self, mock_list_networks, mock_has_service, + self, mock_get_flavor_name, mock_get_image_name, - mock_get_volumes, - mock_list_server_security_groups, - mock_list_subnets, - mock_list_ports, - mock_list_floating_ips): - self.cloud._floating_ip_source = 'neutron' + mock_get_volumes): mock_get_image_name.return_value = 'cirros-0.3.4-x86_64-uec' mock_get_flavor_name.return_value = 'm1.tiny' - mock_has_service.return_value = True mock_get_volumes.return_value = [] - mock_list_subnets.return_value = SUBNETS_WITH_NAT - mock_list_floating_ips.return_value = [ - { - 'port_id': 'test_port_id', - 'fixed_ip_address': PRIVATE_V4, - 'floating_ip_address': PUBLIC_V4, - } - ] - mock_list_ports.return_value = [ - { + + self.register_uri( + 'GET', + 'https://network.example.com/v2.0/ports.json?device_id=test-id', + json={'ports': [{ 'id': 'test_port_id', 'mac_address': 'fa:16:3e:ae:7d:42', 'device_id': 'test-id', - } - ] - mock_list_networks.return_value = [ - { - 'id': 'test_pnztt_net', - 'name': 'test_pnztt_net', - 'router:external': False, - }, - { - 'id': 'private', - 'name': 'private', - }, - ] + }]}) + + self.register_uri( + 'GET', + 'https://network.example.com/v2.0/floatingips.json' + '?port_id=test_port_id', + json={'floatingips': [{ + 'id': 'floating-ip-id', + 'port_id': 'test_port_id', + 'fixed_ip_address': PRIVATE_V4, + 'floating_ip_address': PUBLIC_V4, + }]}) + + self.register_uri( + 'GET', 'https://network.example.com/v2.0/networks.json', + json={'networks': [ + { + 'id': 'test_pnztt_net', + 'name': 'test_pnztt_net', + 'router:external': False, + }, + { + 'id': 'private', + 'name': 'private', + } + ]}) + self.register_uri( + 'GET', 'https://network.example.com/v2.0/subnets.json', + json={'subnets': SUBNETS_WITH_NAT}) + + self.register_uri( + 'GET', '{endpoint}/servers/test-id/os-security-groups'.format( + endpoint=fakes.ENDPOINT), + json={'security_groups': []}) srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', @@ -536,30 +530,26 @@ def test_get_server_cloud_missing_fips( ))) self.assertEqual(PUBLIC_V4, srv['public_v4']) - mock_list_networks.assert_called_once_with() - mock_list_floating_ips.assert_called_once_with( - filters={'port_id': 'test_port_id'}) - mock_list_ports.assert_called_once_with({'device_id': 'test-id'}) - - @mock.patch.object(shade.OpenStackCloud, 'list_floating_ips') - @mock.patch.object(shade.OpenStackCloud, 'list_subnets') - @mock.patch.object(shade.OpenStackCloud, 'list_server_security_groups') + self.assert_calls() + + @mock.patch.object(shade.OpenStackCloud, 'get_volumes') @mock.patch.object(shade.OpenStackCloud, 'get_image_name') @mock.patch.object(shade.OpenStackCloud, 'get_flavor_name') - @mock.patch.object(shade.OpenStackCloud, 'has_service') - @mock.patch.object(shade.OpenStackCloud, 'list_networks') def test_get_server_cloud_rackspace_v6( - self, mock_list_networks, mock_has_service, - mock_get_flavor_name, mock_get_image_name, - mock_list_server_security_groups, - mock_list_subnets, - mock_list_floating_ips): + self, mock_get_flavor_name, mock_get_image_name, + mock_get_volumes): + self.cloud.cloud_config.config['has_network'] = False self.cloud._floating_ip_source = None self.cloud.force_ipv4 = False self.cloud._local_ipv6 = True mock_get_image_name.return_value = 'cirros-0.3.4-x86_64-uec' mock_get_flavor_name.return_value = 'm1.tiny' - mock_has_service.return_value = False + mock_get_volumes.return_value = [] + + self.register_uri( + 'GET', '{endpoint}/servers/test-id/os-security-groups'.format( + endpoint=fakes.ENDPOINT), + json={'security_groups': []}) srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', @@ -588,23 +578,14 @@ def test_get_server_cloud_rackspace_v6( "2001:4800:7819:103:be76:4eff:fe05:8525", srv['public_v6']) self.assertEqual( "2001:4800:7819:103:be76:4eff:fe05:8525", srv['interface_ip']) - mock_list_subnets.assert_not_called() - mock_list_networks.assert_not_called() - mock_list_floating_ips.assert_not_called() + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'list_floating_ips') - @mock.patch.object(shade.OpenStackCloud, 'list_subnets') - @mock.patch.object(shade.OpenStackCloud, 'list_server_security_groups') + @mock.patch.object(shade.OpenStackCloud, 'get_volumes') @mock.patch.object(shade.OpenStackCloud, 'get_image_name') @mock.patch.object(shade.OpenStackCloud, 'get_flavor_name') - @mock.patch.object(shade.OpenStackCloud, 'has_service') - @mock.patch.object(shade.OpenStackCloud, 'list_networks') def test_get_server_cloud_osic_split( - self, mock_list_networks, mock_has_service, - mock_get_flavor_name, mock_get_image_name, - mock_list_server_security_groups, - mock_list_subnets, - mock_list_floating_ips): + self, mock_get_flavor_name, mock_get_image_name, + mock_get_volumes): self.cloud._floating_ip_source = None self.cloud.force_ipv4 = False self.cloud._local_ipv6 = True @@ -614,9 +595,18 @@ def test_get_server_cloud_osic_split( self.cloud._internal_ipv6_names = [] mock_get_image_name.return_value = 'cirros-0.3.4-x86_64-uec' mock_get_flavor_name.return_value = 'm1.tiny' - mock_has_service.return_value = True - mock_list_subnets.return_value = OSIC_SUBNETS - mock_list_networks.return_value = OSIC_NETWORKS + mock_get_volumes.return_value = [] + + self.register_uri( + 'GET', 'https://network.example.com/v2.0/networks.json', + json={'networks': OSIC_NETWORKS}) + self.register_uri( + 'GET', 'https://network.example.com/v2.0/subnets.json', + json={'subnets': OSIC_SUBNETS}) + self.register_uri( + 'GET', '{endpoint}/servers/test-id/os-security-groups'.format( + endpoint=fakes.ENDPOINT), + json={'security_groups': []}) srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', @@ -645,22 +635,20 @@ def test_get_server_cloud_osic_split( "2001:4800:7819:103:be76:4eff:fe05:8525", srv['public_v6']) self.assertEqual( "2001:4800:7819:103:be76:4eff:fe05:8525", srv['interface_ip']) - mock_list_floating_ips.assert_not_called() - - @mock.patch.object(shade.OpenStackCloud, 'has_service') - @mock.patch.object(shade.OpenStackCloud, 'list_subnets') - @mock.patch.object(shade.OpenStackCloud, 'list_networks') - def test_get_server_external_ipv4_neutron( - self, mock_list_networks, mock_list_subnets, - mock_has_service): + self.assert_calls() + + def test_get_server_external_ipv4_neutron(self): # Testing Clouds with Neutron - mock_has_service.return_value = True - mock_list_subnets.return_value = [] - mock_list_networks.return_value = [{ - 'id': 'test-net-id', - 'name': 'test-net', - 'router:external': True, - }] + self.register_uri( + 'GET', 'https://network.example.com/v2.0/networks.json', + json={'networks': [{ + 'id': 'test-net-id', + 'name': 'test-net', + 'router:external': True, + }]}) + self.register_uri( + 'GET', 'https://network.example.com/v2.0/subnets.json', + json={'subnets': SUBNETS_WITH_NAT}) srv = meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', @@ -671,22 +659,21 @@ def test_get_server_external_ipv4_neutron( ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) self.assertEqual(PUBLIC_V4, ip) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'has_service') - @mock.patch.object(shade.OpenStackCloud, 'list_subnets') - @mock.patch.object(shade.OpenStackCloud, 'list_networks') - def test_get_server_external_provider_ipv4_neutron( - self, mock_list_networks, mock_list_subnets, - mock_has_service): + def test_get_server_external_provider_ipv4_neutron(self): # Testing Clouds with Neutron - mock_has_service.return_value = True - mock_list_subnets.return_value = SUBNETS_WITH_NAT - mock_list_networks.return_value = [{ - 'id': 'test-net-id', - 'name': 'test-net', - 'provider:network_type': 'vlan', - 'provider:physical_network': 'vlan', - }] + self.register_uri( + 'GET', 'https://network.example.com/v2.0/networks.json', + json={'networks': [{ + 'id': 'test-net-id', + 'name': 'test-net', + 'provider:network_type': 'vlan', + 'provider:physical_network': 'vlan', + }]}) + self.register_uri( + 'GET', 'https://network.example.com/v2.0/subnets.json', + json={'subnets': SUBNETS_WITH_NAT}) srv = meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', @@ -697,23 +684,22 @@ def test_get_server_external_provider_ipv4_neutron( ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) self.assertEqual(PUBLIC_V4, ip) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'has_service') - @mock.patch.object(shade.OpenStackCloud, 'list_subnets') - @mock.patch.object(shade.OpenStackCloud, 'list_networks') - def test_get_server_internal_provider_ipv4_neutron( - self, mock_list_networks, mock_list_subnets, - mock_has_service): + def test_get_server_internal_provider_ipv4_neutron(self): # Testing Clouds with Neutron - mock_has_service.return_value = True - mock_list_subnets.return_value = SUBNETS_WITH_NAT - mock_list_networks.return_value = [{ - 'id': 'test-net-id', - 'name': 'test-net', - 'router:external': False, - 'provider:network_type': 'vxlan', - 'provider:physical_network': None, - }] + self.register_uri( + 'GET', 'https://network.example.com/v2.0/networks.json', + json={'networks': [{ + 'id': 'test-net-id', + 'name': 'test-net', + 'router:external': False, + 'provider:network_type': 'vxlan', + 'provider:physical_network': None, + }]}) + self.register_uri( + 'GET', 'https://network.example.com/v2.0/subnets.json', + json={'subnets': SUBNETS_WITH_NAT}) srv = meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', @@ -726,21 +712,20 @@ def test_get_server_internal_provider_ipv4_neutron( int_ip = meta.get_server_private_ip(cloud=self.cloud, server=srv) self.assertEqual(PRIVATE_V4, int_ip) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'has_service') - @mock.patch.object(shade.OpenStackCloud, 'list_subnets') - @mock.patch.object(shade.OpenStackCloud, 'list_networks') - def test_get_server_external_none_ipv4_neutron( - self, mock_list_networks, mock_list_subnets, - mock_has_service): + def test_get_server_external_none_ipv4_neutron(self): # Testing Clouds with Neutron - mock_has_service.return_value = True - mock_list_subnets.return_value = SUBNETS_WITH_NAT - mock_list_networks.return_value = [{ - 'id': 'test-net-id', - 'name': 'test-net', - 'router:external': False, - }] + self.register_uri( + 'GET', 'https://network.example.com/v2.0/networks.json', + json={'networks': [{ + 'id': 'test-net-id', + 'name': 'test-net', + 'router:external': False, + }]}) + self.register_uri( + 'GET', 'https://network.example.com/v2.0/subnets.json', + json={'subnets': SUBNETS_WITH_NAT}) srv = meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', @@ -751,6 +736,7 @@ def test_get_server_external_none_ipv4_neutron( ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) self.assertEqual(None, ip) + self.assert_calls() def test_get_server_external_ipv4_neutron_accessIPv4(self): srv = meta.obj_to_dict(fakes.FakeServer( @@ -768,34 +754,24 @@ def test_get_server_external_ipv4_neutron_accessIPv6(self): self.assertEqual(PUBLIC_V6, ip) - @mock.patch.object(shade.OpenStackCloud, 'has_service') - @mock.patch.object(shade.OpenStackCloud, 'list_subnets') - @mock.patch.object(shade.OpenStackCloud, 'list_networks') - @mock.patch.object(shade.OpenStackCloud, 'search_ports') - @mock.patch.object(meta, 'get_server_ip') - def test_get_server_external_ipv4_neutron_exception( - self, mock_get_server_ip, mock_search_ports, - mock_list_networks, mock_list_subnets, - mock_has_service): + def test_get_server_external_ipv4_neutron_exception(self): # Testing Clouds with a non working Neutron - mock_has_service.return_value = True - mock_list_subnets.return_value = [] - mock_list_networks.return_value = [] - mock_search_ports.side_effect = neutron_exceptions.NotFound() - mock_get_server_ip.return_value = PUBLIC_V4 + self.register_uri( + 'GET', 'https://network.example.com/v2.0/networks.json', + status_code=404) srv = meta.obj_to_dict(fakes.FakeServer( - id='test-id', name='test-name', status='ACTIVE')) + id='test-id', name='test-name', status='ACTIVE', + addresses={'public': [{'addr': PUBLIC_V4, 'version': 4}]} + )) ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) self.assertEqual(PUBLIC_V4, ip) - self.assertTrue(mock_get_server_ip.called) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'has_service') - def test_get_server_external_ipv4_nova_public( - self, mock_has_service): + def test_get_server_external_ipv4_nova_public(self): # Testing Clouds w/o Neutron and a network named public - mock_has_service.return_value = False + self.cloud.cloud_config.config['has_network'] = False srv = meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', @@ -804,13 +780,9 @@ def test_get_server_external_ipv4_nova_public( self.assertEqual(PUBLIC_V4, ip) - @mock.patch.object(shade.OpenStackCloud, 'has_service') - @mock.patch.object(meta, 'get_server_ip') - def test_get_server_external_ipv4_nova_none( - self, mock_get_server_ip, mock_has_service): - # Testing Clouds w/o Neutron and a globally routable IP - mock_has_service.return_value = False - mock_get_server_ip.return_value = None + def test_get_server_external_ipv4_nova_none(self): + # Testing Clouds w/o Neutron or a globally routable IP + self.cloud.cloud_config.config['has_network'] = False srv = meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', @@ -818,7 +790,6 @@ def test_get_server_external_ipv4_nova_none( ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) self.assertIsNone(ip) - self.assertTrue(mock_get_server_ip.called) def test_get_server_external_ipv6(self): srv = meta.obj_to_dict(fakes.FakeServer( From 3cb1ad4683b1eee9395b52391e868235c789eb8c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 4 Feb 2017 08:51:56 -0600 Subject: [PATCH 1243/3836] Transition half of test_floating_ip_neutron to requests_mock There is too much going on here for a single patch, and my brain hurts too. We need to come back through and refactor test_auto_ip_pool_no_reuse. The resources involved are all accurate (took them from a live interaction with citycloud for verification purposes) but doing complex tests that way is a pile of copy-pasta. Also, found a logic error in the flow of having created a server and then separately requested a FIP be attached to it with reuse=False. The logic error is that the check to see if the server has a FIP already is using a stale copy of the server dict which cannot possibly have a FIP attached. Change-Id: Ia38294cf48bcb46c18892605fd52da7dadaeefc8 --- shade/openstackcloud.py | 9 +- shade/tests/fakes.py | 4 +- shade/tests/unit/test_floating_ip_neutron.py | 524 ++++++++++++------- 3 files changed, 348 insertions(+), 189 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 1904750ed..4b8093a5f 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4521,8 +4521,9 @@ def _neutron_create_floating_ip( if server: raise OpenStackCloudException( "Attempted to create FIP on port {port} for server" - " {server} but something went wrong".format( - port=port, server=server['id'])) + " {server} but FIP has port {port_id}".format( + port=port, port_id=fip['port_id'], + server=server['id'])) else: raise OpenStackCloudException( "Attempted to create FIP on port {port}" @@ -4940,6 +4941,10 @@ def _add_ip_from_pool( network=network, nat_destination=nat_destination, wait=wait, timeout=timeout) timeout = timeout - (time.time() - start_time) + # Wait for cache invalidation time so that we don't try + # to attach the FIP a second time below + time.sleep(self._SERVER_AGE) + server = self.get_server(server.id) # We run attach as a second call rather than in the create call # because there are code flows where we will not have an attached diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 5bcb7ffb9..c36007fe3 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -18,10 +18,12 @@ """ +PROJECT_ID = '1c36b64c840a42cd9e9b931a369337f0' FLAVOR_ID = u'0c1d9008-f546-4608-9e8f-f8bdaec8dddd' CHOCOLATE_FLAVOR_ID = u'0c1d9008-f546-4608-9e8f-f8bdaec8ddde' STRAWBERRY_FLAVOR_ID = u'0c1d9008-f546-4608-9e8f-f8bdaec8dddf' -ENDPOINT = 'https://compute.example.com/v2.1/1c36b64c840a42cd9e9b931a369337f0' +ENDPOINT = 'https://compute.example.com/v2.1/{project_id}'.format( + project_id=PROJECT_ID) def make_fake_flavor(flavor_id, name, ram=100, disk=1600, vcpus=24): diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 91e609f01..b66f1e976 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -19,12 +19,9 @@ Tests Floating IP resource methods for Neutron """ -import mock from mock import patch +import munch -from neutronclient.common import exceptions as n_exc - -from shade import _utils from shade import exc from shade import meta from shade import OpenStackCloud @@ -39,9 +36,9 @@ class TestFloatingIP(base.RequestsMockTestCase): 'router_id': 'd23abc8d-2991-4a55-ba98-2aaea84cc72f', 'tenant_id': '4969c491a3c74ee4af974e6d800c62de', 'floating_network_id': '376da547-b977-4cfe-9cba-275c80debf57', - 'fixed_ip_address': '192.0.2.29', - 'floating_ip_address': '203.0.113.29', - 'port_id': 'ce705c24-c1ef-408a-bda3-7bbd946164ab', + 'fixed_ip_address': '10.0.0.4', + 'floating_ip_address': '172.24.4.229', + 'port_id': 'ce705c24-c1ef-408a-bda3-7bbd946164ac', 'id': '2f245a7b-796b-4f26-9cf9-9e82d248fda7', 'status': 'ACTIVE' }, @@ -77,7 +74,7 @@ class TestFloatingIP(base.RequestsMockTestCase): 'floating_ip_address': '172.24.4.229', 'floating_network_id': 'my-network-id', 'id': '2f245a7b-796b-4f26-9cf9-9e82d248fda8', - 'port_id': 'ce705c24-c1ef-408a-bda3-7bbd946164ab', + 'port_id': 'ce705c24-c1ef-408a-bda3-7bbd946164ac', 'router_id': None, 'status': 'ACTIVE', 'tenant_id': '4969c491a3c74ee4af974e6d800c62df' @@ -127,7 +124,7 @@ class TestFloatingIP(base.RequestsMockTestCase): ], 'id': 'ce705c24-c1ef-408a-bda3-7bbd946164ac', 'security_groups': [], - 'device_id': 'server_id' + 'device_id': 'server-id' } ] @@ -178,49 +175,40 @@ def test_list_floating_ips(self): self.assert_calls() - @patch.object(OpenStackCloud, 'neutron_client') - @patch.object(OpenStackCloud, 'has_service', return_value=True) - def test_list_floating_ips_with_filters(self, mock_has_service, - mock_neutron_client): - mock_neutron_client.list_floatingips.side_effect = \ - exc.OpenStackCloudURINotFound("") + def test_list_floating_ips_with_filters(self): - try: - self.cloud.list_floating_ips(filters={'Foo': 42}) - except exc.OpenStackCloudException as e: - self.assertIsInstance( - e.inner_exception[1], exc.OpenStackCloudURINotFound) + self.register_uri( + 'GET', 'https://network.example.com/v2.0/floatingips.json?Foo=42', + json={'floatingips': []}) - mock_neutron_client.list_floatingips.assert_called_once_with(Foo=42) + self.cloud.list_floating_ips(filters={'Foo': 42}) - @patch.object(OpenStackCloud, 'neutron_client') - @patch.object(OpenStackCloud, 'has_service') - def test_search_floating_ips(self, mock_has_service, mock_neutron_client): - mock_has_service.return_value = True - mock_neutron_client.list_floatingips.return_value = \ - self.mock_floating_ip_list_rep + self.assert_calls() + + def test_search_floating_ips(self): + self.register_uri( + 'GET', + 'https://network.example.com/v2.0/floatingips.json?attached=False', + json=self.mock_floating_ip_list_rep) floating_ips = self.cloud.search_floating_ips( filters={'attached': False}) - mock_neutron_client.list_floatingips.assert_called_with(attached=False) self.assertIsInstance(floating_ips, list) self.assertAreInstances(floating_ips, dict) self.assertEqual(1, len(floating_ips)) + self.assert_calls() - @patch.object(OpenStackCloud, 'neutron_client') - @patch.object(OpenStackCloud, 'has_service') - def test_get_floating_ip(self, mock_has_service, mock_neutron_client): - mock_has_service.return_value = True - mock_neutron_client.list_floatingips.return_value = \ - self.mock_floating_ip_list_rep + def test_get_floating_ip(self): + self.register_uri( + 'GET', 'https://network.example.com/v2.0/floatingips.json', + json=self.mock_floating_ip_list_rep) floating_ip = self.cloud.get_floating_ip( id='2f245a7b-796b-4f26-9cf9-9e82d248fda7') - mock_neutron_client.list_floatingips.assert_called_with() self.assertIsInstance(floating_ip, dict) - self.assertEqual('203.0.113.29', floating_ip['floating_ip_address']) + self.assertEqual('172.24.4.229', floating_ip['floating_ip_address']) self.assertEqual( self.mock_floating_ip_list_rep['floatingips'][0]['tenant_id'], floating_ip['project_id'] @@ -230,195 +218,359 @@ def test_get_floating_ip(self, mock_has_service, mock_neutron_client): floating_ip['tenant_id'] ) self.assertIn('location', floating_ip) + self.assert_calls() - @patch.object(OpenStackCloud, 'neutron_client') - @patch.object(OpenStackCloud, 'has_service') - def test_get_floating_ip_not_found( - self, mock_has_service, mock_neutron_client): - mock_has_service.return_value = True - mock_neutron_client.list_floatingips.return_value = \ - self.mock_floating_ip_list_rep + def test_get_floating_ip_not_found(self): + self.register_uri( + 'GET', 'https://network.example.com/v2.0/floatingips.json', + json=self.mock_floating_ip_list_rep) floating_ip = self.cloud.get_floating_ip(id='non-existent') self.assertIsNone(floating_ip) + self.assert_calls() - @patch.object(OpenStackCloud, 'neutron_client') - @patch.object(OpenStackCloud, 'search_networks') - @patch.object(OpenStackCloud, 'has_service') - def test_create_floating_ip( - self, mock_has_service, mock_search_networks, mock_neutron_client): - mock_has_service.return_value = True - mock_search_networks.return_value = [self.mock_get_network_rep] - mock_neutron_client.create_floatingip.return_value = \ - self.mock_floating_ip_new_rep - + def test_create_floating_ip(self): + self.register_uri( + 'GET', 'https://network.example.com/v2.0/networks.json', + json={'networks': [self.mock_get_network_rep]}) + self.register_uri( + 'POST', 'https://network.example.com/v2.0/floatingips.json', + json=self.mock_floating_ip_new_rep, + validate=dict( + json={'floatingip': {'floating_network_id': 'my-network-id'}})) ip = self.cloud.create_floating_ip(network='my-network') - mock_neutron_client.create_floatingip.assert_called_with( - body={'floatingip': {'floating_network_id': 'my-network-id'}} - ) self.assertEqual( self.mock_floating_ip_new_rep['floatingip']['floating_ip_address'], ip['floating_ip_address']) + self.assert_calls() - @patch.object(OpenStackCloud, 'neutron_client') - @patch.object(OpenStackCloud, 'search_networks') - @patch.object(OpenStackCloud, 'has_service') - def test_create_floating_ip_port_bad_response( - self, mock_has_service, mock_search_networks, mock_neutron_client): - mock_has_service.return_value = True - mock_search_networks.return_value = [self.mock_get_network_rep] - mock_neutron_client.create_floatingip.return_value = \ - self.mock_floating_ip_new_rep - + def test_create_floating_ip_port_bad_response(self): + self.register_uri( + 'GET', 'https://network.example.com/v2.0/networks.json', + json={'networks': [self.mock_get_network_rep]}) + self.register_uri( + 'POST', 'https://network.example.com/v2.0/floatingips.json', + json=self.mock_floating_ip_new_rep, + validate=dict( + json={'floatingip': { + 'floating_network_id': 'my-network-id', + 'port_id': u'ce705c24-c1ef-408a-bda3-7bbd946164ab', + }})) + + # Fails because we requested a port and the returned FIP has no port self.assertRaises( exc.OpenStackCloudException, self.cloud.create_floating_ip, network='my-network', port='ce705c24-c1ef-408a-bda3-7bbd946164ab') + self.assert_calls() - @patch.object(OpenStackCloud, 'neutron_client') - @patch.object(OpenStackCloud, 'search_networks') - @patch.object(OpenStackCloud, 'has_service') - def test_create_floating_ip_port( - self, mock_has_service, mock_search_networks, mock_neutron_client): - mock_has_service.return_value = True - mock_search_networks.return_value = [self.mock_get_network_rep] - mock_neutron_client.create_floatingip.return_value = \ - self.mock_floating_ip_port_rep + def test_create_floating_ip_port(self): + self.register_uri( + 'GET', 'https://network.example.com/v2.0/networks.json', + json={'networks': [self.mock_get_network_rep]}) + self.register_uri( + 'POST', 'https://network.example.com/v2.0/floatingips.json', + json=self.mock_floating_ip_port_rep, + validate=dict( + json={'floatingip': { + 'floating_network_id': 'my-network-id', + 'port_id': u'ce705c24-c1ef-408a-bda3-7bbd946164ac', + }})) ip = self.cloud.create_floating_ip( - network='my-network', port='ce705c24-c1ef-408a-bda3-7bbd946164ab') + network='my-network', port='ce705c24-c1ef-408a-bda3-7bbd946164ac') - mock_neutron_client.create_floatingip.assert_called_with( - body={'floatingip': { - 'floating_network_id': 'my-network-id', - 'port_id': 'ce705c24-c1ef-408a-bda3-7bbd946164ab', - }} - ) self.assertEqual( self.mock_floating_ip_new_rep['floatingip']['floating_ip_address'], ip['floating_ip_address']) + self.assert_calls() - @patch.object(_utils, '_filter_list') - @patch.object(OpenStackCloud, '_neutron_create_floating_ip') - @patch.object(OpenStackCloud, '_neutron_list_floating_ips') - @patch.object(OpenStackCloud, 'get_external_ipv4_floating_networks') - @patch.object(OpenStackCloud, 'keystone_session') - def test__neutron_available_floating_ips( - self, - mock_keystone_session, - mock_get_ext_nets, - mock__neutron_list_fips, - mock__neutron_create_fip, - mock__filter_list): + def test__neutron_available_floating_ips(self): """ Test without specifying a network name. """ - mock_keystone_session.get_project_id.return_value = 'proj-id' - mock_get_ext_nets.return_value = [self.mock_get_network_rep] - mock__neutron_list_fips.return_value = [] - mock__filter_list.return_value = [] + self.register_uri( + 'GET', 'https://network.example.com/v2.0/networks.json', + json={'networks': [self.mock_get_network_rep]}) + self.register_uri( + 'GET', 'https://network.example.com/v2.0/subnets.json', + json={'subnets': []}) + self.register_uri( + 'GET', 'https://network.example.com/v2.0/floatingips.json', + json={'floatingips': []}) + self.register_uri( + 'POST', 'https://network.example.com/v2.0/floatingips.json', + json=self.mock_floating_ip_new_rep, + validate=dict(json={ + 'floatingip': { + 'floating_network_id': self.mock_get_network_rep['id'], + }})) # Test if first network is selected if no network is given self.cloud._neutron_available_floating_ips() + self.assert_calls() - mock_keystone_session.get_project_id.assert_called_once_with() - mock_get_ext_nets.assert_called_once_with() - mock__neutron_list_fips.assert_called_once_with(None) - mock__filter_list.assert_called_once_with( - [], name_or_id=None, - filters={'port': None, - 'network': self.mock_get_network_rep['id'], - 'location': {'project': {'id': 'proj-id'}}} - ) - mock__neutron_create_fip.assert_called_once_with( - network_id=self.mock_get_network_rep['id'], - server=None - ) - - @patch.object(_utils, '_filter_list') - @patch.object(OpenStackCloud, '_neutron_create_floating_ip') - @patch.object(OpenStackCloud, '_neutron_list_floating_ips') - @patch.object(OpenStackCloud, 'get_external_ipv4_floating_networks') - @patch.object(OpenStackCloud, 'keystone_session') - def test__neutron_available_floating_ips_network( - self, - mock_keystone_session, - mock_get_ext_nets, - mock__neutron_list_fips, - mock__neutron_create_fip, - mock__filter_list): + def test__neutron_available_floating_ips_network(self): """ Test with specifying a network name. """ - mock_keystone_session.get_project_id.return_value = 'proj-id' - mock_get_ext_nets.return_value = [self.mock_get_network_rep] - mock__neutron_list_fips.return_value = [] - mock__filter_list.return_value = [] + self.register_uri( + 'GET', 'https://network.example.com/v2.0/networks.json', + json={'networks': [self.mock_get_network_rep]}) + self.register_uri( + 'GET', 'https://network.example.com/v2.0/subnets.json', + json={'subnets': []}) + self.register_uri( + 'GET', 'https://network.example.com/v2.0/floatingips.json', + json={'floatingips': []}) + self.register_uri( + 'POST', 'https://network.example.com/v2.0/floatingips.json', + json=self.mock_floating_ip_new_rep, + validate=dict(json={ + 'floatingip': { + 'floating_network_id': self.mock_get_network_rep['id'], + }})) + # Test if first network is selected if no network is given self.cloud._neutron_available_floating_ips( network=self.mock_get_network_rep['name'] ) + self.assert_calls() - mock_keystone_session.get_project_id.assert_called_once_with() - mock_get_ext_nets.assert_called_once_with() - mock__neutron_list_fips.assert_called_once_with(None) - mock__filter_list.assert_called_once_with( - [], name_or_id=None, - filters={'port': None, - 'network': self.mock_get_network_rep['id'], - 'location': {'project': {'id': 'proj-id'}}} - ) - mock__neutron_create_fip.assert_called_once_with( - network_id=self.mock_get_network_rep['id'], - server=None - ) - - @patch.object(OpenStackCloud, 'get_external_ipv4_networks') - @patch.object(OpenStackCloud, 'keystone_session') - def test__neutron_available_floating_ips_invalid_network( - self, - mock_keystone_session, - mock_get_ext_nets): + def test__neutron_available_floating_ips_invalid_network(self): """ Test with an invalid network name. """ - mock_keystone_session.get_project_id.return_value = 'proj-id' - mock_get_ext_nets.return_value = [] - self.assertRaises(exc.OpenStackCloudException, - self.cloud._neutron_available_floating_ips, - network='INVALID') + self.register_uri( + 'GET', 'https://network.example.com/v2.0/networks.json', + json={'networks': [self.mock_get_network_rep]}) + self.register_uri( + 'GET', 'https://network.example.com/v2.0/subnets.json', + json={'subnets': []}) - @patch.object(OpenStackCloud, 'nova_client') - @patch.object(OpenStackCloud, 'keystone_session') - @patch.object(OpenStackCloud, '_neutron_create_floating_ip') - @patch.object(OpenStackCloud, '_attach_ip_to_server') - @patch.object(OpenStackCloud, 'has_service') - def test_auto_ip_pool_no_reuse( - self, mock_has_service, - mock_attach_ip_to_server, - mock__neutron_create_floating_ip, - mock_keystone_session, - mock_nova_client): - mock_has_service.return_value = True - fip = self.cloud._normalize_floating_ips( - self.mock_floating_ip_list_rep['floatingips'])[0] - mock__neutron_create_floating_ip.return_value = fip - mock_keystone_session.get_project_id.return_value = \ - '4969c491a3c74ee4af974e6d800c62df' - fake_server = meta.obj_to_dict(fakes.FakeServer('1234', '', 'ACTIVE')) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud._neutron_available_floating_ips, + network='INVALID') + + self.assert_calls() + + def test_auto_ip_pool_no_reuse(self): + # payloads taken from citycloud + self.register_uri( + 'GET', 'https://network.example.com/v2.0/networks.json', + json={"networks": [{ + "status": "ACTIVE", + "subnets": [ + "df3e17fa-a4b2-47ae-9015-bc93eb076ba2", + "6b0c3dc9-b0b8-4d87-976a-7f2ebf13e7ec", + "fc541f48-fc7f-48c0-a063-18de6ee7bdd7"], + "availability_zone_hints": [], + "availability_zones": ["nova"], + "name": "ext-net", + "admin_state_up": True, + "tenant_id": "a564613210ee43708b8a7fc6274ebd63", + "tags": [], + "ipv6_address_scope": "9f03124f-89af-483a-b6fd-10f08079db4d", + "mtu": 0, + "is_default": False, + "router:external": True, + "ipv4_address_scope": None, + "shared": False, + "id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", + "description": None + }, { + "status": "ACTIVE", + "subnets": ["f0ad1df5-53ee-473f-b86b-3604ea5591e9"], + "availability_zone_hints": [], + "availability_zones": ["nova"], + "name": "private", + "admin_state_up": True, + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", + "created_at": "2016-10-22T13:46:26", + "tags": [], + "updated_at": "2016-10-22T13:46:26", + "ipv6_address_scope": None, + "router:external": False, + "ipv4_address_scope": None, + "shared": False, + "mtu": 1450, + "id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", + "description": "" + }]}) + self.register_uri( + 'GET', + 'https://network.example.com/v2.0/ports.json' + '?device_id=f80e3ad0-e13e-41d4-8e9c-be79bccdb8f7', + json={"ports": [{ + "status": "ACTIVE", + "created_at": "2017-02-06T20:59:45", + "description": "", + "allowed_address_pairs": [], + "admin_state_up": True, + "network_id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", + "dns_name": None, + "extra_dhcp_opts": [], + "mac_address": "fa:16:3e:e8:7f:03", + "updated_at": "2017-02-06T20:59:49", + "name": "", + "device_owner": "compute:None", + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", + "binding:vnic_type": "normal", + "fixed_ips": [{ + "subnet_id": "f0ad1df5-53ee-473f-b86b-3604ea5591e9", + "ip_address": "10.4.0.16"}], + "id": "a767944e-057a-47d1-a669-824a21b8fb7b", + "security_groups": ["9fb5ba44-5c46-4357-8e60-8b55526cab54"], + "device_id": "f80e3ad0-e13e-41d4-8e9c-be79bccdb8f7", + }]}) + + self.register_uri( + 'POST', + 'https://network.example.com/v2.0/floatingips.json', + json={"floatingip": { + "router_id": "9de9c787-8f89-4a53-8468-a5533d6d7fd1", + "status": "DOWN", + "description": "", + "dns_domain": "", + "floating_network_id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", + "fixed_ip_address": "10.4.0.16", + "floating_ip_address": "89.40.216.153", + "port_id": "a767944e-057a-47d1-a669-824a21b8fb7b", + "id": "e69179dc-a904-4c9a-a4c9-891e2ecb984c", + "dns_name": "", + "tenant_id": "65222a4d09ea4c68934fa1028c77f394" + }}, + validate=dict(json={"floatingip": { + "floating_network_id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", + "fixed_ip_address": "10.4.0.16", + "port_id": "a767944e-057a-47d1-a669-824a21b8fb7b", + }})) + + self.register_uri( + 'GET', + '{endpoint}/servers/detail'.format(endpoint=fakes.ENDPOINT), + json={"servers": [{ + "status": "ACTIVE", + "updated": "2017-02-06T20:59:49Z", + "addresses": { + "private": [{ + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:e8:7f:03", + "version": 4, + "addr": "10.4.0.16", + "OS-EXT-IPS:type": "fixed" + }, { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:e8:7f:03", + "version": 4, + "addr": "89.40.216.153", + "OS-EXT-IPS:type": "floating" + }]}, + "key_name": None, + "image": {"id": "95e4c449-8abf-486e-97d9-dc3f82417d2d"}, + "OS-EXT-STS:task_state": None, + "OS-EXT-STS:vm_state": "active", + "OS-SRV-USG:launched_at": "2017-02-06T20:59:48.000000", + "flavor": {"id": "2186bd79-a05e-4953-9dde-ddefb63c88d4"}, + "id": "f80e3ad0-e13e-41d4-8e9c-be79bccdb8f7", + "security_groups": [{"name": "default"}], + "OS-SRV-USG:terminated_at": None, + "OS-EXT-AZ:availability_zone": "nova", + "user_id": "c17534835f8f42bf98fc367e0bf35e09", + "name": "testmt", + "created": "2017-02-06T20:59:44Z", + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": {} + }]}) + + self.register_uri( + 'GET', 'https://network.example.com/v2.0/networks.json', + json={"networks": [{ + "status": "ACTIVE", + "subnets": [ + "df3e17fa-a4b2-47ae-9015-bc93eb076ba2", + "6b0c3dc9-b0b8-4d87-976a-7f2ebf13e7ec", + "fc541f48-fc7f-48c0-a063-18de6ee7bdd7"], + "availability_zone_hints": [], + "availability_zones": ["nova"], + "name": "ext-net", + "admin_state_up": True, + "tenant_id": "a564613210ee43708b8a7fc6274ebd63", + "tags": [], + "ipv6_address_scope": "9f03124f-89af-483a-b6fd-10f08079db4d", + "mtu": 0, + "is_default": False, + "router:external": True, + "ipv4_address_scope": None, + "shared": False, + "id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", + "description": None + }, { + "status": "ACTIVE", + "subnets": ["f0ad1df5-53ee-473f-b86b-3604ea5591e9"], + "availability_zone_hints": [], + "availability_zones": ["nova"], + "name": "private", + "admin_state_up": True, + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", + "created_at": "2016-10-22T13:46:26", + "tags": [], + "updated_at": "2016-10-22T13:46:26", + "ipv6_address_scope": None, + "router:external": False, + "ipv4_address_scope": None, + "shared": False, + "mtu": 1450, + "id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", + "description": "" + }]}) + self.register_uri( + 'GET', 'https://network.example.com/v2.0/subnets.json', + json={"subnets": [{ + "description": "", + "enable_dhcp": True, + "network_id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", + "created_at": "2016-10-22T13:46:26", + "dns_nameservers": [ + "89.36.90.101", + "89.36.90.102"], + "updated_at": "2016-10-22T13:46:26", + "gateway_ip": "10.4.0.1", + "ipv6_ra_mode": None, + "allocation_pools": [{ + "start": "10.4.0.2", + "end": "10.4.0.200"}], + "host_routes": [], + "ip_version": 4, + "ipv6_address_mode": None, + "cidr": "10.4.0.0/24", + "id": "f0ad1df5-53ee-473f-b86b-3604ea5591e9", + "subnetpool_id": None, + "name": "private-subnet-ipv4", + }]}) self.cloud.add_ips_to_server( - fake_server, ip_pool='my-network', reuse=False) + munch.Munch( + id='f80e3ad0-e13e-41d4-8e9c-be79bccdb8f7', + addresses={ + "private": [{ + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:e8:7f:03", + "version": 4, + "addr": "10.4.0.16", + "OS-EXT-IPS:type": "fixed" + }]}), + ip_pool='ext-net', reuse=False) - mock__neutron_create_floating_ip.assert_called_once_with( - network_name_or_id='my-network', server=fake_server, port=None, - fixed_address=None, nat_destination=None, wait=False, timeout=60) - mock_attach_ip_to_server.assert_called_once_with( - server=fake_server, fixed_address=None, - floating_ip=fip, wait=False, timeout=mock.ANY, - nat_destination=None) + self.assert_calls() @patch.object(OpenStackCloud, 'keystone_session') @patch.object(OpenStackCloud, '_neutron_create_floating_ip') @@ -524,18 +676,18 @@ def test_delete_floating_ip_existing_no_delete( ) self.assertEqual(mock_get_floating_ip.call_count, 3) - @patch.object(OpenStackCloud, 'neutron_client') - @patch.object(OpenStackCloud, 'has_service') - def test_delete_floating_ip_not_found( - self, mock_has_service, mock_neutron_client): - mock_has_service.return_value = True - mock_neutron_client.delete_floatingip.side_effect = \ - n_exc.NotFound() + def test_delete_floating_ip_not_found(self): + self.register_uri( + 'DELETE', + 'https://network.example.com/v2.0/floatingips' + '/a-wild-id-appears.json', + status_code=404) ret = self.cloud.delete_floating_ip( floating_ip_id='a-wild-id-appears') self.assertFalse(ret) + self.assert_calls() @patch.object(OpenStackCloud, 'search_ports') @patch.object(OpenStackCloud, 'neutron_client') From 6edafb98ea5778e42a6e5f7eaa70ca1742071336 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 6 Feb 2017 16:41:29 -0600 Subject: [PATCH 1244/3836] Rename ENDPOINT to COMPUTE_ENDPOINT Wow, there is more than one type of endpoint after all!!! Change-Id: I1a3dd675d6ad9bfed51a3a8aa05502ed22f42203 --- shade/tests/fakes.py | 6 +-- shade/tests/unit/test_caching.py | 6 +-- shade/tests/unit/test_create_server.py | 2 +- shade/tests/unit/test_flavors.py | 44 ++++++++++---------- shade/tests/unit/test_floating_ip_neutron.py | 3 +- shade/tests/unit/test_meta.py | 12 +++--- 6 files changed, 37 insertions(+), 36 deletions(-) diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index c36007fe3..45ab55420 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -22,7 +22,7 @@ FLAVOR_ID = u'0c1d9008-f546-4608-9e8f-f8bdaec8dddd' CHOCOLATE_FLAVOR_ID = u'0c1d9008-f546-4608-9e8f-f8bdaec8ddde' STRAWBERRY_FLAVOR_ID = u'0c1d9008-f546-4608-9e8f-f8bdaec8dddf' -ENDPOINT = 'https://compute.example.com/v2.1/{project_id}'.format( +COMPUTE_ENDPOINT = 'https://compute.example.com/v2.1/{project_id}'.format( project_id=PROJECT_ID) @@ -34,11 +34,11 @@ def make_fake_flavor(flavor_id, name, ram=100, disk=1600, vcpus=24): u'id': flavor_id, u'links': [{ u'href': u'{endpoint}/flavors/{id}'.format( - endpoint=ENDPOINT, id=flavor_id), + endpoint=COMPUTE_ENDPOINT, id=flavor_id), u'rel': u'self' }, { u'href': u'{endpoint}/flavors/{id}'.format( - endpoint=ENDPOINT, id=flavor_id), + endpoint=COMPUTE_ENDPOINT, id=flavor_id), u'rel': u'bookmark' }], u'name': name, diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 2313c19a0..611e55ceb 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -302,17 +302,17 @@ def test_modify_user_invalidates_cache(self, keystone_mock): def test_list_flavors(self): self.register_uri( 'GET', '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.ENDPOINT), + endpoint=fakes.COMPUTE_ENDPOINT), json={'flavors': []}) self.register_uri( 'GET', '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.ENDPOINT), + endpoint=fakes.COMPUTE_ENDPOINT), json={'flavors': fakes.FAKE_FLAVOR_LIST}) for flavor in fakes.FAKE_FLAVOR_LIST: self.register_uri( 'GET', '{endpoint}/flavors/{id}/os-extra_specs'.format( - endpoint=fakes.ENDPOINT, id=flavor['id']), + endpoint=fakes.COMPUTE_ENDPOINT, id=flavor['id']), json={'extra_specs': {}}) self.assertEqual([], self.cloud.list_flavors()) diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index 5f3b018ff..fedbb6f2d 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -315,7 +315,7 @@ def test_create_server_get_flavor_image( self, mock_nova, mock_image, mock_get_server_by_id): self.register_uri( 'GET', '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.ENDPOINT), + endpoint=fakes.COMPUTE_ENDPOINT), json={'flavors': fakes.FAKE_FLAVOR_LIST}) self.cloud.create_server( 'server-name', 'image-id', 'vanilla', diff --git a/shade/tests/unit/test_flavors.py b/shade/tests/unit/test_flavors.py index 9881b231c..ded2806cd 100644 --- a/shade/tests/unit/test_flavors.py +++ b/shade/tests/unit/test_flavors.py @@ -22,7 +22,7 @@ def test_create_flavor(self): self.register_uri( 'POST', '{endpoint}/flavors'.format( - endpoint=fakes.ENDPOINT), + endpoint=fakes.COMPUTE_ENDPOINT), json={'flavor': fakes.FAKE_FLAVOR}, validate=dict( json={'flavor': { @@ -45,11 +45,11 @@ def test_create_flavor(self): def test_delete_flavor(self): self.register_uri( 'GET', '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.ENDPOINT), + endpoint=fakes.COMPUTE_ENDPOINT), json={'flavors': fakes.FAKE_FLAVOR_LIST}) self.register_uri( 'DELETE', '{endpoint}/flavors/{id}'.format( - endpoint=fakes.ENDPOINT, id=fakes.FLAVOR_ID)) + endpoint=fakes.COMPUTE_ENDPOINT, id=fakes.FLAVOR_ID)) self.assertTrue(self.op_cloud.delete_flavor('vanilla')) self.assert_calls() @@ -57,7 +57,7 @@ def test_delete_flavor(self): def test_delete_flavor_not_found(self): self.register_uri( 'GET', '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.ENDPOINT), + endpoint=fakes.COMPUTE_ENDPOINT), json={'flavors': fakes.FAKE_FLAVOR_LIST}) self.assertFalse(self.op_cloud.delete_flavor('invalid')) @@ -67,11 +67,11 @@ def test_delete_flavor_not_found(self): def test_delete_flavor_exception(self): self.register_uri( 'GET', '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.ENDPOINT), + endpoint=fakes.COMPUTE_ENDPOINT), json={'flavors': fakes.FAKE_FLAVOR_LIST}) self.register_uri( 'DELETE', '{endpoint}/flavors/{id}'.format( - endpoint=fakes.ENDPOINT, id=fakes.FLAVOR_ID), + endpoint=fakes.COMPUTE_ENDPOINT, id=fakes.FLAVOR_ID), status_code=503) self.assertRaises(shade.OpenStackCloudException, self.op_cloud.delete_flavor, 'vanilla') @@ -79,12 +79,12 @@ def test_delete_flavor_exception(self): def test_list_flavors(self): self.register_uri( 'GET', '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.ENDPOINT), + endpoint=fakes.COMPUTE_ENDPOINT), json={'flavors': fakes.FAKE_FLAVOR_LIST}) for flavor in fakes.FAKE_FLAVOR_LIST: self.register_uri( 'GET', '{endpoint}/flavors/{id}/os-extra_specs'.format( - endpoint=fakes.ENDPOINT, id=flavor['id']), + endpoint=fakes.COMPUTE_ENDPOINT, id=flavor['id']), json={'extra_specs': {}}) flavors = self.cloud.list_flavors() @@ -105,12 +105,12 @@ def test_list_flavors(self): def test_get_flavor_by_ram(self): self.register_uri( 'GET', '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.ENDPOINT), + endpoint=fakes.COMPUTE_ENDPOINT), json={'flavors': fakes.FAKE_FLAVOR_LIST}) for flavor in fakes.FAKE_FLAVOR_LIST: self.register_uri( 'GET', '{endpoint}/flavors/{id}/os-extra_specs'.format( - endpoint=fakes.ENDPOINT, id=flavor['id']), + endpoint=fakes.COMPUTE_ENDPOINT, id=flavor['id']), json={'extra_specs': {}}) flavor = self.cloud.get_flavor_by_ram(ram=250) @@ -119,12 +119,12 @@ def test_get_flavor_by_ram(self): def test_get_flavor_by_ram_and_include(self): self.register_uri( 'GET', '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.ENDPOINT), + endpoint=fakes.COMPUTE_ENDPOINT), json={'flavors': fakes.FAKE_FLAVOR_LIST}) for flavor in fakes.FAKE_FLAVOR_LIST: self.register_uri( 'GET', '{endpoint}/flavors/{id}/os-extra_specs'.format( - endpoint=fakes.ENDPOINT, id=flavor['id']), + endpoint=fakes.COMPUTE_ENDPOINT, id=flavor['id']), json={'extra_specs': {}}) flavor = self.cloud.get_flavor_by_ram(ram=150, include='strawberry') self.assertEqual(fakes.STRAWBERRY_FLAVOR_ID, flavor['id']) @@ -132,7 +132,7 @@ def test_get_flavor_by_ram_and_include(self): def test_get_flavor_by_ram_not_found(self): self.register_uri( 'GET', '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.ENDPOINT), + endpoint=fakes.COMPUTE_ENDPOINT), json={'flavors': []}) self.assertRaises( shade.OpenStackCloudException, @@ -142,19 +142,19 @@ def test_get_flavor_by_ram_not_found(self): def test_get_flavor_string_and_int(self): self.register_uri( 'GET', '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.ENDPOINT), + endpoint=fakes.COMPUTE_ENDPOINT), json={'flavors': [fakes.make_fake_flavor('1', 'vanilla')]}) self.register_uri( 'GET', '{endpoint}/flavors/1/os-extra_specs'.format( - endpoint=fakes.ENDPOINT), + endpoint=fakes.COMPUTE_ENDPOINT), json={'extra_specs': {}}) self.register_uri( 'GET', '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.ENDPOINT), + endpoint=fakes.COMPUTE_ENDPOINT), json={'flavors': [fakes.make_fake_flavor('1', 'vanilla')]}) self.register_uri( 'GET', '{endpoint}/flavors/1/os-extra_specs'.format( - endpoint=fakes.ENDPOINT), + endpoint=fakes.COMPUTE_ENDPOINT), json={'extra_specs': {}}) flavor1 = self.cloud.get_flavor('1') @@ -166,7 +166,7 @@ def test_set_flavor_specs(self): extra_specs = dict(key1='value1') self.register_uri( 'POST', '{endpoint}/flavors/{id}/os-extra_specs'.format( - endpoint=fakes.ENDPOINT, id=1), + endpoint=fakes.COMPUTE_ENDPOINT, id=1), json=dict(extra_specs=extra_specs)) self.op_cloud.set_flavor_specs(1, extra_specs) @@ -178,7 +178,7 @@ def test_unset_flavor_specs(self): self.register_uri( 'DELETE', '{endpoint}/flavors/{id}/os-extra_specs/{key}'.format( - endpoint=fakes.ENDPOINT, id=1, key=key)) + endpoint=fakes.COMPUTE_ENDPOINT, id=1, key=key)) self.op_cloud.unset_flavor_specs(1, keys) self.assert_calls() @@ -186,7 +186,7 @@ def test_unset_flavor_specs(self): def test_add_flavor_access(self): self.register_uri( 'POST', '{endpoint}/flavors/{id}/action'.format( - endpoint=fakes.ENDPOINT, id='flavor_id'), + endpoint=fakes.COMPUTE_ENDPOINT, id='flavor_id'), json={ 'flavor_access': [{ 'flavor_id': 'flavor_id', @@ -204,7 +204,7 @@ def test_add_flavor_access(self): def test_remove_flavor_access(self): self.register_uri( 'POST', '{endpoint}/flavors/{id}/action'.format( - endpoint=fakes.ENDPOINT, id='flavor_id'), + endpoint=fakes.COMPUTE_ENDPOINT, id='flavor_id'), json={'flavor_access': []}, validate=dict( json={ @@ -218,7 +218,7 @@ def test_remove_flavor_access(self): def test_list_flavor_access(self): self.register_uri( 'GET', '{endpoint}/flavors/vanilla/os-flavor-access'.format( - endpoint=fakes.ENDPOINT), + endpoint=fakes.COMPUTE_ENDPOINT), json={ 'flavor_access': [{ 'flavor_id': 'vanilla', diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index b66f1e976..befda7c8b 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -451,7 +451,8 @@ def test_auto_ip_pool_no_reuse(self): self.register_uri( 'GET', - '{endpoint}/servers/detail'.format(endpoint=fakes.ENDPOINT), + '{endpoint}/servers/detail'.format( + endpoint=fakes.COMPUTE_ENDPOINT), json={"servers": [{ "status": "ACTIVE", "updated": "2017-02-06T20:59:49Z", diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 17dd93d5e..f91a86055 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -345,7 +345,7 @@ def test_get_server_private_ip_devstack( self.register_uri( 'GET', '{endpoint}/servers/test-id/os-security-groups'.format( - endpoint=fakes.ENDPOINT), + endpoint=fakes.COMPUTE_ENDPOINT), json={'security_groups': []}) srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( @@ -396,7 +396,7 @@ def test_get_server_private_ip_no_fip( json={'subnets': SUBNETS_WITH_NAT}) self.register_uri( 'GET', '{endpoint}/servers/test-id/os-security-groups'.format( - endpoint=fakes.ENDPOINT), + endpoint=fakes.COMPUTE_ENDPOINT), json={'security_groups': []}) srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( @@ -445,7 +445,7 @@ def test_get_server_cloud_no_fips( json={'subnets': SUBNETS_WITH_NAT}) self.register_uri( 'GET', '{endpoint}/servers/test-id/os-security-groups'.format( - endpoint=fakes.ENDPOINT), + endpoint=fakes.COMPUTE_ENDPOINT), json={'security_groups': []}) srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( @@ -513,7 +513,7 @@ def test_get_server_cloud_missing_fips( self.register_uri( 'GET', '{endpoint}/servers/test-id/os-security-groups'.format( - endpoint=fakes.ENDPOINT), + endpoint=fakes.COMPUTE_ENDPOINT), json={'security_groups': []}) srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( @@ -548,7 +548,7 @@ def test_get_server_cloud_rackspace_v6( self.register_uri( 'GET', '{endpoint}/servers/test-id/os-security-groups'.format( - endpoint=fakes.ENDPOINT), + endpoint=fakes.COMPUTE_ENDPOINT), json={'security_groups': []}) srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( @@ -605,7 +605,7 @@ def test_get_server_cloud_osic_split( json={'subnets': OSIC_SUBNETS}) self.register_uri( 'GET', '{endpoint}/servers/test-id/os-security-groups'.format( - endpoint=fakes.ENDPOINT), + endpoint=fakes.COMPUTE_ENDPOINT), json={'security_groups': []}) srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( From 175b256161d15916e44a62d5b3cdebacdedb84cc Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 6 Feb 2017 17:21:05 -0600 Subject: [PATCH 1245/3836] Pass task to post_task_run hook It's hard for consumers to do anything useful in post_task_run without a copy of the task. Also - not adding this as an optional becaues the consumer this was written for, nodepool, has not started using it yet, and I find it VERY hard to believe anyone else will have picked up on this. Change-Id: Ib48fe54feb8b6ed10da3e5fa9accf985e4b22623 --- shade/task_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shade/task_manager.py b/shade/task_manager.py index bfad5a241..3b0c281be 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -265,11 +265,11 @@ def _run_task(self, task, raw=False): self.log.debug( "Manager %s ran task %s in %ss", self.name, task.name, dt) - self.post_run_task(dt) + self.post_run_task(dt, task) return task.wait(raw) - def post_run_task(self, elasped_time): + def post_run_task(self, elasped_time, task): pass # Backwards compatibility From e97a7550467ccf2bcd69bec57025c2b1d1ce615d Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Wed, 1 Feb 2017 15:00:34 -0500 Subject: [PATCH 1246/3836] Adjust some proxy method names in cluster MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In auditing our proxy method names heading into the 1.0 release, where we'll need to be more careful about backwards compatibility, some names in the cluster proxy use structures that are different than we're using both around the other services and also within cluster itself. We've converged on several naming formats for add/remove, attach/detach, and several other grammar styles when naming operations that are done on a certain resource. This change introduces a deprecation decorator using the `deprecation` library, which was recently added to global-requirements. This is also the first change of several in the auditing process, just going alphabetical order through the services. The plan with these is that they're marked for deprecation in the next release (0.9.14), both here in the code and in the documentation via the deprecation library's ability to modify the docstring, which then shows up in our built documentation. In the meantime, we should help any users we know of—in this case we have cluster developers on this project—in updating calling code to use the newer format before 1.0 happens. The following deprecations were made: * cluster_add_nodes -> add_nodes_to_cluster * cluster_del_nodes -> remove_nodes_from_cluster * cluster_replace_nodes -> replace_nodes_in_cluster * cluster_scale_out -> scale_out_cluster * cluster_scale_in -> scale_in_cluster * cluster_attach_policy -> attach_policy_to_cluster * cluster_detach_policy -> detach_policy_from_cluster * cluster_update_policy -> update_cluster_policy * cluster_operation -> perform_operation_on_cluster * node_operation -> perform_operation_on_node Partial-Bug: 1657498 Change-Id: I3df0494f9ec0097aee7d47e05fb42094439bc4a4 --- openstack/cluster/v1/_proxy.py | 150 ++++++++++++++++++ openstack/tests/unit/cluster/v1/test_proxy.py | 19 +++ openstack/utils.py | 28 ++++ requirements.txt | 1 + 4 files changed, 198 insertions(+) diff --git a/openstack/cluster/v1/_proxy.py b/openstack/cluster/v1/_proxy.py index 1286942ce..914ec0e4b 100644 --- a/openstack/cluster/v1/_proxy.py +++ b/openstack/cluster/v1/_proxy.py @@ -24,6 +24,7 @@ from openstack.cluster.v1 import receiver as _receiver from openstack import proxy2 from openstack import resource2 +from openstack import utils class Proxy(proxy2.BaseProxy): @@ -275,9 +276,21 @@ def update_cluster(self, cluster, **attrs): """ return self._update(_cluster.Cluster, cluster, **attrs) + @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", + details="Use add_nodes_to_cluster instead") def cluster_add_nodes(self, cluster, nodes): """Add nodes to a cluster. + :param cluster: Either the name or the ID of the cluster, or an + instance of :class:`~openstack.cluster.v1.cluster.Cluster`. + :param nodes: List of nodes to be added to the cluster. + :returns: A dict containing the action initiated by this operation. + """ + return self.add_nodes_to_cluster(cluster, nodes) + + def add_nodes_to_cluster(self, cluster, nodes): + """Add nodes to a cluster. + :param cluster: Either the name or the ID of the cluster, or an instance of :class:`~openstack.cluster.v1.cluster.Cluster`. :param nodes: List of nodes to be added to the cluster. @@ -289,9 +302,25 @@ def cluster_add_nodes(self, cluster, nodes): obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) return obj.add_nodes(self.session, nodes) + @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", + details="Use remove_nodes_from_cluster instead") def cluster_del_nodes(self, cluster, nodes, **params): """Remove nodes from a cluster. + :param cluster: Either the name or the ID of the cluster, or an + instance of :class:`~openstack.cluster.v1.cluster.Cluster`. + :param nodes: List of nodes to be removed from the cluster. + :param kwargs \*\*params: Optional query parameters to be sent to + restrict the nodes to be returned. Available parameters include: + * destroy_after_deletion: A boolean value indicating whether the + deleted nodes to be destroyed right away. + :returns: A dict containing the action initiated by this operation. + """ + return self.remove_nodes_from_cluster(cluster, nodes, **params) + + def remove_nodes_from_cluster(self, cluster, nodes, **params): + """Remove nodes from a cluster. + :param cluster: Either the name or the ID of the cluster, or an instance of :class:`~openstack.cluster.v1.cluster.Cluster`. :param nodes: List of nodes to be removed from the cluster. @@ -307,9 +336,21 @@ def cluster_del_nodes(self, cluster, nodes, **params): obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) return obj.del_nodes(self.session, nodes, **params) + @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", + details="Use replace_nodes_in_cluster instead") def cluster_replace_nodes(self, cluster, nodes): """Replace the nodes in a cluster with specified nodes. + :param cluster: Either the name or the ID of the cluster, or an + instance of :class:`~openstack.cluster.v1.cluster.Cluster`. + :param nodes: List of nodes to be deleted/added to the cluster. + :returns: A dict containing the action initiated by this operation. + """ + return self.replace_nodes_in_cluster(cluster, nodes) + + def replace_nodes_in_cluster(self, cluster, nodes): + """Replace the nodes in a cluster with specified nodes. + :param cluster: Either the name or the ID of the cluster, or an instance of :class:`~openstack.cluster.v1.cluster.Cluster`. :param nodes: List of nodes to be deleted/added to the cluster. @@ -321,9 +362,22 @@ def cluster_replace_nodes(self, cluster, nodes): obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) return obj.replace_nodes(self.session, nodes) + @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", + details="Use scale_out_cluster instead") def cluster_scale_out(self, cluster, count=None): """Inflate the size of a cluster. + :param cluster: Either the name or the ID of the cluster, or an + instance of :class:`~openstack.cluster.v1.cluster.Cluster`. + :param count: Optional parameter specifying the number of nodes to + be added. + :returns: A dict containing the action initiated by this operation. + """ + return self.scale_out_cluster(cluster, count) + + def scale_out_cluster(self, cluster, count=None): + """Inflate the size of a cluster. + :param cluster: Either the name or the ID of the cluster, or an instance of :class:`~openstack.cluster.v1.cluster.Cluster`. :param count: Optional parameter specifying the number of nodes to @@ -336,9 +390,22 @@ def cluster_scale_out(self, cluster, count=None): obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) return obj.scale_out(self.session, count) + @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", + details="Use scale_in_cluster instead") def cluster_scale_in(self, cluster, count=None): """Shrink the size of a cluster. + :param cluster: Either the name or the ID of the cluster, or an + instance of :class:`~openstack.cluster.v1.cluster.Cluster`. + :param count: Optional parameter specifying the number of nodes to + be removed. + :returns: A dict containing the action initiated by this operation. + """ + return self.scale_in_cluster(cluster, count) + + def scale_in_cluster(self, cluster, count=None): + """Shrink the size of a cluster. + :param cluster: Either the name or the ID of the cluster, or an instance of :class:`~openstack.cluster.v1.cluster.Cluster`. :param count: Optional parameter specifying the number of nodes to @@ -351,9 +418,22 @@ def cluster_scale_in(self, cluster, count=None): obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) return obj.scale_in(self.session, count) + @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", + details="Use resize_cluster instead") def cluster_resize(self, cluster, **params): """Resize of cluster. + :param cluster: Either the name or the ID of the cluster, or an + instance of :class:`~openstack.cluster.v1.cluster.Cluster`. + :param dict \*\*params: A dictionary providing the parameters for the + resize action. + :returns: A dict containing the action initiated by this operation. + """ + return self.resize_cluster(cluster, **params) + + def resize_cluster(self, cluster, **params): + """Resize of cluster. + :param cluster: Either the name or the ID of the cluster, or an instance of :class:`~openstack.cluster.v1.cluster.Cluster`. :param dict \*\*params: A dictionary providing the parameters for the @@ -366,9 +446,23 @@ def cluster_resize(self, cluster, **params): obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) return obj.resize(self.session, **params) + @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", + details="Use attach_policy_to_cluster instead") def cluster_attach_policy(self, cluster, policy, **params): """Attach a policy to a cluster. + :param cluster: Either the name or the ID of the cluster, or an + instance of :class:`~openstack.cluster.v1.cluster.Cluster`. + :param policy: Either the name or the ID of a policy. + :param dict \*\*params: A dictionary containing the properties for the + policy to be attached. + :returns: A dict containing the action initiated by this operation. + """ + return self.attach_policy_to_cluster(cluster, policy, **params) + + def attach_policy_to_cluster(self, cluster, policy, **params): + """Attach a policy to a cluster. + :param cluster: Either the name or the ID of the cluster, or an instance of :class:`~openstack.cluster.v1.cluster.Cluster`. :param policy: Either the name or the ID of a policy. @@ -382,9 +476,21 @@ def cluster_attach_policy(self, cluster, policy, **params): obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) return obj.policy_attach(self.session, policy, **params) + @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", + details="Use detach_policy_from_cluster instead") def cluster_detach_policy(self, cluster, policy): """Attach a policy to a cluster. + :param cluster: Either the name or the ID of the cluster, or an + instance of :class:`~openstack.cluster.v1.cluster.Cluster`. + :param policy: Either the name or the ID of a policy. + :returns: A dict containing the action initiated by this operation. + """ + return self.detach_policy_from_cluster(cluster, policy) + + def detach_policy_from_cluster(self, cluster, policy): + """Detach a policy from a cluster. + :param cluster: Either the name or the ID of the cluster, or an instance of :class:`~openstack.cluster.v1.cluster.Cluster`. :param policy: Either the name or the ID of a policy. @@ -396,9 +502,23 @@ def cluster_detach_policy(self, cluster, policy): obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) return obj.policy_detach(self.session, policy) + @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", + details="Use update_cluster_policy instead") def cluster_update_policy(self, cluster, policy, **params): """Change properties of a policy which is bound to the cluster. + :param cluster: Either the name or the ID of the cluster, or an + instance of :class:`~openstack.cluster.v1.cluster.Cluster`. + :param policy: Either the name or the ID of a policy. + :param dict \*\*params: A dictionary containing the new properties for + the policy. + :returns: A dict containing the action initiated by this operation. + """ + return self.update_cluster_policy(cluster, policy, **params) + + def update_cluster_policy(self, cluster, policy, **params): + """Change properties of a policy which is bound to the cluster. + :param cluster: Either the name or the ID of the cluster, or an instance of :class:`~openstack.cluster.v1.cluster.Cluster`. :param policy: Either the name or the ID of a policy. @@ -450,9 +570,24 @@ def recover_cluster(self, cluster, **params): obj = self._get_resource(_cluster.Cluster, cluster) return obj.recover(self.session, **params) + @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", + details="Use perform_operation_on_cluster instead") def cluster_operation(self, cluster, operation, **params): """Perform an operation on the specified cluster. + :param cluster: The value can be either the ID of a cluster or a + :class:`~openstack.cluster.v1.cluster.Cluster` instance. + :param operation: A string specifying the operation to be performed. + :param dict params: A dictionary providing the parameters for the + operation. + + :returns: A dictionary containing the action ID. + """ + return self.perform_operation_on_cluster(cluster, operation, **params) + + def perform_operation_on_cluster(self, cluster, operation, **params): + """Perform an operation on the specified cluster. + :param cluster: The value can be either the ID of a cluster or a :class:`~openstack.cluster.v1.cluster.Cluster` instance. :param operation: A string specifying the operation to be performed. @@ -584,9 +719,24 @@ def recover_node(self, node, **params): obj = self._get_resource(_node.Node, node) return obj.recover(self.session, **params) + @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", + details="Use perform_operation_on_node instead") def node_operation(self, node, operation, **params): """Perform an operation on the specified node. + :param cluster: The value can be either the ID of a node or a + :class:`~openstack.cluster.v1.node.Node` instance. + :param operation: A string specifying the operation to be performed. + :param dict params: A dictionary providing the parameters for the + operation. + + :returns: A dictionary containing the action ID. + """ + return self.perform_operation_on_node(node, operation, **params) + + def perform_operation_on_node(self, node, operation, **params): + """Perform an operation on the specified node. + :param cluster: The value can be either the ID of a node or a :class:`~openstack.cluster.v1.node.Node` instance. :param operation: A string specifying the operation to be performed. diff --git a/openstack/tests/unit/cluster/v1/test_proxy.py b/openstack/tests/unit/cluster/v1/test_proxy.py index 62c244e50..10b7cae8c 100644 --- a/openstack/tests/unit/cluster/v1/test_proxy.py +++ b/openstack/tests/unit/cluster/v1/test_proxy.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import deprecation import mock from openstack.cluster.v1 import _proxy @@ -107,6 +108,7 @@ def test_clusters(self): def test_cluster_update(self): self.verify_update(self.proxy.update_cluster, cluster.Cluster) + @deprecation.fail_if_not_removed @mock.patch.object(proxy_base.BaseProxy, '_find') def test_cluster_add_nodes(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') @@ -118,6 +120,7 @@ def test_cluster_add_nodes(self, mock_find): mock_find.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER", ignore_missing=False) + @deprecation.fail_if_not_removed def test_cluster_add_nodes_with_obj(self): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') self._verify("openstack.cluster.v1.cluster.Cluster.add_nodes", @@ -125,6 +128,7 @@ def test_cluster_add_nodes_with_obj(self): method_args=[mock_cluster, ["node1"]], expected_args=[["node1"]]) + @deprecation.fail_if_not_removed @mock.patch.object(proxy_base.BaseProxy, '_find') def test_cluster_del_nodes(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') @@ -136,6 +140,7 @@ def test_cluster_del_nodes(self, mock_find): mock_find.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER", ignore_missing=False) + @deprecation.fail_if_not_removed def test_cluster_del_nodes_with_obj(self): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') self._verify("openstack.cluster.v1.cluster.Cluster.del_nodes", @@ -145,6 +150,7 @@ def test_cluster_del_nodes_with_obj(self): expected_args=[["node1"]], expected_kwargs={"key": "value"}) + @deprecation.fail_if_not_removed @mock.patch.object(proxy_base.BaseProxy, '_find') def test_cluster_replace_nodes(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') @@ -156,6 +162,7 @@ def test_cluster_replace_nodes(self, mock_find): mock_find.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER", ignore_missing=False) + @deprecation.fail_if_not_removed def test_cluster_replace_nodes_with_obj(self): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') self._verify("openstack.cluster.v1.cluster.Cluster.replace_nodes", @@ -163,6 +170,7 @@ def test_cluster_replace_nodes_with_obj(self): method_args=[mock_cluster, {"node1": "node2"}], expected_args=[{"node1": "node2"}]) + @deprecation.fail_if_not_removed @mock.patch.object(proxy_base.BaseProxy, '_find') def test_cluster_scale_out(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') @@ -174,6 +182,7 @@ def test_cluster_scale_out(self, mock_find): mock_find.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER", ignore_missing=False) + @deprecation.fail_if_not_removed def test_cluster_scale_out_with_obj(self): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') self._verify("openstack.cluster.v1.cluster.Cluster.scale_out", @@ -181,6 +190,7 @@ def test_cluster_scale_out_with_obj(self): method_args=[mock_cluster, 5], expected_args=[5]) + @deprecation.fail_if_not_removed @mock.patch.object(proxy_base.BaseProxy, '_find') def test_cluster_scale_in(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') @@ -192,6 +202,7 @@ def test_cluster_scale_in(self, mock_find): mock_find.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER", ignore_missing=False) + @deprecation.fail_if_not_removed def test_cluster_scale_in_with_obj(self): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') self._verify("openstack.cluster.v1.cluster.Cluster.scale_in", @@ -219,6 +230,7 @@ def test_cluster_resize_with_obj(self): method_kwargs={'k1': 'v1', 'k2': 'v2'}, expected_kwargs={'k1': 'v1', 'k2': 'v2'}) + @deprecation.fail_if_not_removed @mock.patch.object(proxy_base.BaseProxy, '_find') def test_cluster_attach_policy(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') @@ -232,6 +244,7 @@ def test_cluster_attach_policy(self, mock_find): mock_find.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER", ignore_missing=False) + @deprecation.fail_if_not_removed def test_cluster_attach_policy_with_obj(self): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') self._verify("openstack.cluster.v1.cluster.Cluster.policy_attach", @@ -241,6 +254,7 @@ def test_cluster_attach_policy_with_obj(self): expected_args=["FAKE_POLICY"], expected_kwargs={"k1": "v1", 'k2': "v2"}) + @deprecation.fail_if_not_removed @mock.patch.object(proxy_base.BaseProxy, '_find') def test_cluster_detach_policy(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') @@ -252,6 +266,7 @@ def test_cluster_detach_policy(self, mock_find): mock_find.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER", ignore_missing=False) + @deprecation.fail_if_not_removed def test_cluster_detach_policy_with_obj(self): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') self._verify("openstack.cluster.v1.cluster.Cluster.policy_detach", @@ -259,6 +274,7 @@ def test_cluster_detach_policy_with_obj(self): method_args=[mock_cluster, "FAKE_POLICY"], expected_args=["FAKE_POLICY"]) + @deprecation.fail_if_not_removed @mock.patch.object(proxy_base.BaseProxy, '_find') def test_cluster_update_policy(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') @@ -272,6 +288,7 @@ def test_cluster_update_policy(self, mock_find): mock_find.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER", ignore_missing=False) + @deprecation.fail_if_not_removed def test_cluster_update_policy_with_obj(self): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') self._verify("openstack.cluster.v1.cluster.Cluster.policy_update", @@ -306,6 +323,7 @@ def test_cluster_recover(self, mock_get): method_args=["FAKE_CLUSTER"]) mock_get.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER") + @deprecation.fail_if_not_removed @mock.patch.object(proxy_base.BaseProxy, '_get_resource') def test_cluster_operation(self, mock_get): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') @@ -367,6 +385,7 @@ def test_node_recover(self, mock_get): method_args=["FAKE_NODE"]) mock_get.assert_called_once_with(node.Node, "FAKE_NODE") + @deprecation.fail_if_not_removed @mock.patch.object(proxy_base.BaseProxy, '_get_resource') def test_node_operation(self, mock_get): mock_node = node.Node.new(id='FAKE_CLUSTER') diff --git a/openstack/utils.py b/openstack/utils.py index 56eedee37..1864ecc57 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -10,8 +10,36 @@ # License for the specific language governing permissions and limitations # under the License. +import functools import logging +import deprecation + +from openstack import version + + +def deprecated(deprecated_in=None, removed_in=None, + details=""): + """Mark a method as deprecated + + :param deprecated_in: The version string where this method is deprecated. + Generally this is the next version to be released. + :param removed_in: The version where this method will be removed + from the code base. Generally this is the next + major version. This argument is helpful for the + tests when using ``deprecation.fail_if_not_removed``. + :param str details: Helpful details to callers and the documentation. + This will usually be a recommendation for alternate + code to use. + """ + # As all deprecations within this library have the same current_version, + # return a partial function with the library version always set. + partial = functools.partial(deprecation.deprecated, + current_version=version.__version__) + + return partial(deprecated_in=deprecated_in, removed_in=removed_in, + details=details) + def enable_logging(debug=False, path=None, stream=None): """Enable logging to a file at path and/or a console stream. diff --git a/requirements.txt b/requirements.txt index c8282921a..f51c90d02 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ six>=1.9.0 # MIT stevedore>=1.17.1 # Apache-2.0 os-client-config>=1.22.0 # Apache-2.0 keystoneauth1>=2.18.0 # Apache-2.0 +deprecation>=1.0 # Apache-2.0 From 113543b47c2ec8090ec1ba1dcb1b12fd8efb8925 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Tue, 7 Feb 2017 12:38:24 -0500 Subject: [PATCH 1247/3836] Adjust some proxy method names in bare_metal The method names for PortGroup are wrong. They were named properly in the resource module name (port_group) but are imported under a different name in the proxy (_portgroup) and then the proxy removes the underscore from the method names. They are now correct and will be removed at 1.0. Change-Id: I98e040e1b3185723d08d0cde7fe56e07a0b84470 --- openstack/bare_metal/v1/_proxy.py | 183 +++++++++++++++--- .../tests/unit/bare_metal/v1/test_proxy.py | 10 + 2 files changed, 166 insertions(+), 27 deletions(-) diff --git a/openstack/bare_metal/v1/_proxy.py b/openstack/bare_metal/v1/_proxy.py index f9f8e5f33..0c3cccfb0 100644 --- a/openstack/bare_metal/v1/_proxy.py +++ b/openstack/bare_metal/v1/_proxy.py @@ -16,6 +16,7 @@ from openstack.bare_metal.v1 import port as _port from openstack.bare_metal.v1 import port_group as _portgroup from openstack import proxy2 +from openstack import utils class Proxy(proxy2.BaseProxy): @@ -374,8 +375,10 @@ def delete_port(self, port, ignore_missing=True): """ return self._delete(_port.Port, port, ignore_missing=ignore_missing) + @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", + details="Use port_groups instead") def portgroups(self, details=False, **query): - """Retrieve a generator of portgroups. + """Retrieve a generator of port groups. :param details: A boolean indicating whether the detailed information for every portgroup should be returned. @@ -411,40 +414,110 @@ def portgroups(self, details=False, **query): :returns: A generator of portgroup instances. """ + return self.port_groups(details=details, **query) + + def port_groups(self, details=False, **query): + """Retrieve a generator of port groups. + + :param details: A boolean indicating whether the detailed information + for every port group should be returned. + :param dict query: Optional query parameters to be sent to restrict + the port groups returned. Available parameters include: + + * ``address``: Only return portgroups with the specified physical + hardware address, typically a MAC address. + * ``fields``: A list containing one or more fields to be returned + in the response. This may lead to some performance gain + because other fields of the resource are not refreshed. + * ``limit``: Requests at most the specified number of portgroups + returned from the query. + * ``marker``: Specifies the ID of the last-seen portgroup. Use the + ``limit`` parameter to make an initial limited request and + use the ID of the last-seen portgroup from the response as + the ``marker`` value in a subsequent limited request. + * ``node``:only return the ones associated with this specific node + (name or UUID), or an empty set if not found. + * ``sort_dir``: Sorts the response by the requested sort direction. + A valid value is ``asc`` (ascending) or ``desc`` + (descending). Default is ``asc``. You can specify multiple + pairs of sort key and sort direction query parameters. If + you omit the sort direction in a pair, the API uses the + natural sorting direction of the server attribute that is + provided as the ``sort_key``. + * ``sort_key``: Sorts the response by the this attribute value. + Default is ``id``. You can specify multiple pairs of sort + key and sort direction query parameters. If you omit the + sort direction in a pair, the API uses the natural sorting + direction of the server attribute that is provided as the + ``sort_key``. + + :returns: A generator of port group instances. + """ cls = _portgroup.PortGroupDetail if details else _portgroup.PortGroup return self._list(cls, paginated=True, **query) + @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", + details="Use create_port_group instead") def create_portgroup(self, **attrs): + """Create a new port group from attributes. + + :param dict attrs: Keyword arguments that will be used to create a + :class:`~openstack.bare_metal.v1.port_group.PortGroup`, it + comprises of the properties on the ``PortGroup`` class. + + :returns: The results of portgroup creation. + :rtype: :class:`~openstack.bare_metal.v1.port_group.PortGroup`. + """ + return self.create_port_group(**attrs) + + def create_port_group(self, **attrs): """Create a new portgroup from attributes. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.bare_metal.v1.portgroup.PortGroup`, it + :class:`~openstack.bare_metal.v1.port_group.PortGroup`, it comprises of the properties on the ``PortGroup`` class. :returns: The results of portgroup creation. - :rtype: :class:`~openstack.bare_metal.v1.portgroup.PortGroup`. + :rtype: :class:`~openstack.bare_metal.v1.port_group.PortGroup`. """ return self._create(_portgroup.PortGroup, **attrs) + @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", + details="Use find_port_group instead") def find_portgroup(self, name_or_id, ignore_missing=True): - """Find a single portgroup. + """Find a single port group. :param str name_or_id: The name or ID of a portgroup. :param bool ignore_missing: When set to ``False``, an exception of :class:`~openstack.exceptions.ResourceNotFound` will be raised - when the portgroup does not exist. When set to `True``, None will - be returned when attempting to find a nonexistent portgroup. - :returns: One :class:`~openstack.bare_metal.v1.portgroup.PortGroup` + when the port group does not exist. When set to `True``, None will + be returned when attempting to find a nonexistent port group. + :returns: One :class:`~openstack.bare_metal.v1.port_group.PortGroup` + object or None. + """ + return self.find_port_group(name_or_id, ignore_missing=ignore_missing) + + def find_port_group(self, name_or_id, ignore_missing=True): + """Find a single port group. + + :param str name_or_id: The name or ID of a portgroup. + :param bool ignore_missing: When set to ``False``, an exception of + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the port group does not exist. When set to `True``, None will + be returned when attempting to find a nonexistent port group. + :returns: One :class:`~openstack.bare_metal.v1.port_group.PortGroup` object or None. """ return self._find(_portgroup.PortGroup, name_or_id, ignore_missing=ignore_missing) + @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", + details="Use get_port_group instead") def get_portgroup(self, portgroup, **query): - """Get a specific portgroup. + """Get a specific port group. :param portgroup: The value can be the name or ID of a chassis or a - :class:`~openstack.bare_metal.v1.portgroup.PortGroup` instance. + :class:`~openstack.bare_metal.v1.port_group.PortGroup` instance. :param dict query: Optional query parameters to be sent to restrict the portgroup properties returned. Available parameters include: @@ -452,39 +525,95 @@ def get_portgroup(self, portgroup, **query): in the response. This may lead to some performance gain because other fields of the resource are not refreshed. - :returns: One :class:`~openstack.bare_metal.v1.portgroup.PortGroup` + :returns: One :class:`~openstack.bare_metal.v1.port_group.PortGroup` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + port group matching the name or ID could be found. + """ + return self.get_port_group(portgroup, **query) + + def get_port_group(self, port_group, **query): + """Get a specific port group. + + :param port_group: The value can be the name or ID of a chassis or a + :class:`~openstack.bare_metal.v1.port_group.PortGroup` instance. + :param dict query: Optional query parameters to be sent to restrict + the port group properties returned. Available parameters include: + + * ``fields``: A list containing one or more fields to be returned + in the response. This may lead to some performance gain + because other fields of the resource are not refreshed. + + :returns: One :class:`~openstack.bare_metal.v1.port_group.PortGroup` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no - portgroup matching the name or ID could be found. + port group matching the name or ID could be found. """ - return self._get(_portgroup.PortGroup, portgroup, **query) + return self._get(_portgroup.PortGroup, port_group, **query) + @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", + details="Use update_port_group instead") def update_portgroup(self, portgroup, **attrs): - """Update a portgroup. + """Update a port group. - :param chassis: Either the name or the ID of a portgroup or an instance - of :class:`~openstack.bare_metal.v1.portgroup.PortGroup`. - :param dict attrs: The attributes to update on the portgroup + :param chassis: Either the name or the ID of a port group or + an instance of + :class:`~openstack.bare_metal.v1.port_group.PortGroup`. + :param dict attrs: The attributes to update on the port group represented by the ``portgroup`` parameter. - :returns: The updated portgroup. - :rtype: :class:`~openstack.bare_metal.v1.portgroup.PortGroup` + :returns: The updated port group. + :rtype: :class:`~openstack.bare_metal.v1.port_group.PortGroup` + """ + return self.update_port_group(portgroup, **attrs) + + def update_port_group(self, port_group, **attrs): + """Update a port group. + + :param chassis: Either the name or the ID of a port group or + an instance of + :class:`~openstack.bare_metal.v1.port_group.PortGroup`. + :param dict attrs: The attributes to update on the port group + represented by the ``port_group`` parameter. + + :returns: The updated port group. + :rtype: :class:`~openstack.bare_metal.v1.port_group.PortGroup` """ - return self._update(_portgroup.PortGroup, portgroup, **attrs) + return self._update(_portgroup.PortGroup, port_group, **attrs) + @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", + details="Use delete_port_group instead") def delete_portgroup(self, portgroup, ignore_missing=True): - """Delete a portgroup. + """Delete a port group. + + :param portgroup: The value can be either the name or ID of a port + group or a + :class:`~openstack.bare_metal.v1.port_group.PortGroup` + instance. + :param bool ignore_missing: When set to ``False``, an exception + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the port group could not be found. When set to ``True``, no + exception will be raised when attempting to delete a non-existent + port group. + + :returns: The instance of the port group which was deleted. + :rtype: :class:`~openstack.bare_metal.v1.port_group.PortGroup`. + """ + return self.delete_port_group(portgroup, ignore_missing=ignore_missing) + + def delete_port_group(self, port_group, ignore_missing=True): + """Delete a port group. - :param portgroup: The value can be either the name or ID of a portgroup - or a :class:`~openstack.bare_metal.v1.portgroup.PortGroup` + :param port_group: The value can be either the name or ID of + a port group or a + :class:`~openstack.bare_metal.v1.port_group.PortGroup` instance. :param bool ignore_missing: When set to ``False``, an exception :class:`~openstack.exceptions.ResourceNotFound` will be raised - when the portgroup could not be found. When set to ``True``, no + when the port group could not be found. When set to ``True``, no exception will be raised when attempting to delete a non-existent - portgroup. + port group. - :returns: The instance of the portgroup which was deleted. - :rtype: :class:`~openstack.bare_metal.v1.portgroup.PortGroup`. + :returns: The instance of the port group which was deleted. + :rtype: :class:`~openstack.bare_metal.v1.port_group.PortGroup`. """ - return self._delete(_portgroup.PortGroup, portgroup, + return self._delete(_portgroup.PortGroup, port_group, ignore_missing=ignore_missing) diff --git a/openstack/tests/unit/bare_metal/v1/test_proxy.py b/openstack/tests/unit/bare_metal/v1/test_proxy.py index f5d530824..34212a6a0 100644 --- a/openstack/tests/unit/bare_metal/v1/test_proxy.py +++ b/openstack/tests/unit/bare_metal/v1/test_proxy.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import deprecation + from openstack.bare_metal.v1 import _proxy from openstack.bare_metal.v1 import chassis from openstack.bare_metal.v1 import driver @@ -121,34 +123,42 @@ def test_delete_port(self): def test_delete_port_ignore(self): self.verify_delete(self.proxy.delete_port, port.Port, True) + @deprecation.fail_if_not_removed def test_portgroups_detailed(self): self.verify_list(self.proxy.portgroups, port_group.PortGroupDetail, paginated=True, method_kwargs={"details": True, "query": 1}, expected_kwargs={"query": 1}) + @deprecation.fail_if_not_removed def test_portgroups_not_detailed(self): self.verify_list(self.proxy.portgroups, port_group.PortGroup, paginated=True, method_kwargs={"details": False, "query": 1}, expected_kwargs={"query": 1}) + @deprecation.fail_if_not_removed def test_create_portgroup(self): self.verify_create(self.proxy.create_portgroup, port_group.PortGroup) + @deprecation.fail_if_not_removed def test_find_portgroup(self): self.verify_find(self.proxy.find_portgroup, port_group.PortGroup) + @deprecation.fail_if_not_removed def test_get_portgroup(self): self.verify_get(self.proxy.get_portgroup, port_group.PortGroup) + @deprecation.fail_if_not_removed def test_update_portgroup(self): self.verify_update(self.proxy.update_portgroup, port_group.PortGroup) + @deprecation.fail_if_not_removed def test_delete_portgroup(self): self.verify_delete(self.proxy.delete_portgroup, port_group.PortGroup, False) + @deprecation.fail_if_not_removed def test_delete_portgroup_ignore(self): self.verify_delete(self.proxy.delete_portgroup, port_group.PortGroup, True) From 93c2e17c8ec1280fec4473e9f583cb2b29c03073 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Tue, 7 Feb 2017 22:35:19 +0000 Subject: [PATCH 1248/3836] pass -1 for boot_index of non-boot volumes Although the API documentation for nova [1] says to pass None to indicate a volume that should not be used for booting, doing so produces an error because None is not an integer. Change shade to use '-1' (to comply with the declared string type) instead of None. [1] http://developer.openstack.org/api-ref/compute/?expanded=create-server-detail#create-server Change-Id: I5732575501005f4acc6320578882a079adc8be38 Signed-off-by: Doug Hellmann --- shade/openstackcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 1904750ed..d92a57de8 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5182,7 +5182,7 @@ def _get_boot_from_volume_kwargs( volume=volume, cloud=self.name, region=self.region_name)) block_mapping = { - 'boot_index': None, + 'boot_index': '-1', 'delete_on_termination': False, 'destination_type': 'volume', 'uuid': volume_obj['id'], From 50efb434d874ca74e6c89424c43389408ab9b584 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 8 Feb 2017 08:25:21 -0600 Subject: [PATCH 1249/3836] Add support for indicating required floating IPs Some clouds require that users add a floating IP to a server if the user wants that server to be able to talk to things that are not on the cloud. Some clouds do not require this and instead give servers a directly attached IP. The only way a user can know is to boot a server, then ask neutron for the port associated with that server, then find the network the port came from and then try to infer whether or not that network has the ability to route packets northbound. Of course, networks don't actually communicate that quality directly, (router:external doesn't mean a network routes externally, it means the network can have a router attached to it to provide floating ips) so it's still hit and miss. Where we can, save the user the stress and strain of not knowing how their cloud wants them to get an externally routable IP. Change-Id: I1baf804ce28bc1997b2347c4648c5cc56c750ead --- doc/source/vendor-support.rst | 1 + os_client_config/cloud_config.py | 13 ++++++++++++- os_client_config/vendor-schema.json | 6 ++++++ os_client_config/vendors/auro.json | 3 ++- os_client_config/vendors/citycloud.json | 1 + os_client_config/vendors/rackspace.json | 1 + os_client_config/vendors/vexxhost.json | 3 ++- 7 files changed, 25 insertions(+), 3 deletions(-) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index ff0669102..577093c57 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -21,6 +21,7 @@ These are the default behaviors unless a cloud is configured differently. * Images are uploaded using PUT interface * Public IPv4 is directly routable via DHCP from Neutron * IPv6 is not provided +* Floating IPs are not required * Floating IPs are provided by Neutron * Security groups are provided by Neutron * Vendor specific agents are not used diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index a7fc0582f..f52756fd8 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -244,7 +244,6 @@ def get_session_endpoint(self, service_key): :param service_key: Generic key for service, such as 'compute' or 'network' - :returns: Endpoint for the service, or None if not found """ override_endpoint = self.get_endpoint(service_key) @@ -420,6 +419,18 @@ def get_cache_resource_expiration(self, resource, default=None): return default return float(expiration[resource]) + def requires_floating_ip(self): + """Return whether or not this cloud requires floating ips. + + + :returns: True of False if know, None if discovery is needed. + If requires_floating_ip is not configured but the cloud is + known to not provide floating ips, will return False. + """ + if self.config['floating_ip_source'] == "None": + return False + return self.config['requires_floating_ip'] + def get_external_networks(self): """Get list of network names for external networks.""" return [ diff --git a/os_client_config/vendor-schema.json b/os_client_config/vendor-schema.json index 692b79c51..8193a19ba 100644 --- a/os_client_config/vendor-schema.json +++ b/os_client_config/vendor-schema.json @@ -60,6 +60,12 @@ "description": "Optional message with information related to status", "type": "string" }, + "requires_floating_ip": { + "name": "Requires Floating IP", + "description": "Whether the cloud requires a floating IP to route traffic off of the cloud", + "default": null, + "type": ["boolean", "null"] + }, "secgroup_source": { "name": "Security Group Source", "description": "Which service provides security groups", diff --git a/os_client_config/vendors/auro.json b/os_client_config/vendors/auro.json index a9e709bea..410a8e19c 100644 --- a/os_client_config/vendors/auro.json +++ b/os_client_config/vendors/auro.json @@ -5,6 +5,7 @@ "auth_url": "https://api.van1.auro.io:5000/v2.0" }, "identity_api_version": "2", - "region_name": "van1" + "region_name": "van1", + "requires_floating_ip": true } } diff --git a/os_client_config/vendors/citycloud.json b/os_client_config/vendors/citycloud.json index 097ddfdb3..c9ac335c8 100644 --- a/os_client_config/vendors/citycloud.json +++ b/os_client_config/vendors/citycloud.json @@ -12,6 +12,7 @@ "Sto2", "Kna1" ], + "requires_floating_ip": true, "volume_api_version": "1", "identity_api_version": "3" } diff --git a/os_client_config/vendors/rackspace.json b/os_client_config/vendors/rackspace.json index 3fbbacd90..6a4590f67 100644 --- a/os_client_config/vendors/rackspace.json +++ b/os_client_config/vendors/rackspace.json @@ -18,6 +18,7 @@ "image_format": "vhd", "floating_ip_source": "None", "secgroup_source": "None", + "requires_floating_ip": false, "volume_api_version": "1", "disable_vendor_agent": { "vm_mode": "hvm", diff --git a/os_client_config/vendors/vexxhost.json b/os_client_config/vendors/vexxhost.json index aa2cedc68..2227fff4f 100644 --- a/os_client_config/vendors/vexxhost.json +++ b/os_client_config/vendors/vexxhost.json @@ -9,6 +9,7 @@ ], "dns_api_version": "1", "identity_api_version": "3", - "floating_ip_source": "None" + "floating_ip_source": "None", + "requires_floating_ip": false } } From adc2cb89046afa8a9bdc4401673dca638fcee384 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Wed, 8 Feb 2017 12:00:30 -0500 Subject: [PATCH 1250/3836] Cleanup more Sphinx warnings during doc build This is a second pass at cleaning up Sphinx issues with the doc build. These ones only show themselves on a full rebuild after you blow away doc/build/* so they weren't always seen. Change-Id: I44e39b7e1f2bcf7a5eb420dee1569eb0b84929a4 --- doc/source/users/guides/cluster/policy.rst | 2 +- doc/source/users/guides/cluster/policy_type.rst | 2 +- doc/source/users/guides/connect_from_config.rst | 4 ++-- doc/source/users/guides/network.rst | 2 +- .../resources/network/v2/auto_allocated_topology.rst | 2 +- .../resources/network/v2/network_ip_availability.rst | 2 +- doc/source/users/resources/workflow/v2/execution.rst | 2 +- doc/source/users/resources/workflow/v2/workflow.rst | 2 +- openstack/cluster/v1/_proxy.py | 4 +++- openstack/connection.py | 8 ++++---- openstack/network/v2/_proxy.py | 9 ++++----- openstack/network/v2/router.py | 8 ++++---- openstack/orchestration/v1/_proxy.py | 12 ++++++------ 13 files changed, 30 insertions(+), 29 deletions(-) diff --git a/doc/source/users/guides/cluster/policy.rst b/doc/source/users/guides/cluster/policy.rst index 7c708cac1..c0995840c 100644 --- a/doc/source/users/guides/cluster/policy.rst +++ b/doc/source/users/guides/cluster/policy.rst @@ -27,7 +27,7 @@ List Policies To examine the list of policies: .. literalinclude:: ../../examples/cluster/policy.py - :pyobject: list_policys + :pyobject: list_policies When listing policies, you can specify the sorting option using the ``sort`` parameter and you can do pagination using the ``limit`` and ``marker`` diff --git a/doc/source/users/guides/cluster/policy_type.rst b/doc/source/users/guides/cluster/policy_type.rst index 3211564d4..346ed7838 100644 --- a/doc/source/users/guides/cluster/policy_type.rst +++ b/doc/source/users/guides/cluster/policy_type.rst @@ -42,4 +42,4 @@ it. Full example: `manage policy type`_ -.. _manage profile type: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/cluster/policy_type.py +.. _manage policy type: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/cluster/policy_type.py diff --git a/doc/source/users/guides/connect_from_config.rst b/doc/source/users/guides/connect_from_config.rst index 042ee83c3..2e5769ac2 100644 --- a/doc/source/users/guides/connect_from_config.rst +++ b/doc/source/users/guides/connect_from_config.rst @@ -36,9 +36,9 @@ function takes three optional arguments: * **cloud_name** allows you to specify a cloud from your ``clouds.yaml`` file. * **cloud_config** allows you to pass in an existing -``os_client_config.config.OpenStackConfig``` object. + ``os_client_config.config.OpenStackConfig``` object. * **options** allows you to specify a namespace object with options to be -added to the cloud config. + added to the cloud config. .. literalinclude:: ../examples/connect.py :pyobject: Opts diff --git a/doc/source/users/guides/network.rst b/doc/source/users/guides/network.rst index e0701bc59..7fd58797d 100644 --- a/doc/source/users/guides/network.rst +++ b/doc/source/users/guides/network.rst @@ -77,7 +77,7 @@ provide external network access for servers on project networks. Full example: `network resource list`_ List Network Agents ------------- +------------------- A **network agent** is a plugin that handles various tasks used to implement virtual networks. These agents include neutron-dhcp-agent, diff --git a/doc/source/users/resources/network/v2/auto_allocated_topology.rst b/doc/source/users/resources/network/v2/auto_allocated_topology.rst index 828f1a561..f27241c50 100644 --- a/doc/source/users/resources/network/v2/auto_allocated_topology.rst +++ b/doc/source/users/resources/network/v2/auto_allocated_topology.rst @@ -8,5 +8,5 @@ The Auto Allocated Topology Class The ``Auto Allocated Toplogy`` class inherits from :class:`~openstack.resource.Resource`. -.. autoclass:: openstack.network.v2.auto_allocated_topology:AutoAllocatedTopology +.. autoclass:: openstack.network.v2.auto_allocated_topology.AutoAllocatedTopology :members: diff --git a/doc/source/users/resources/network/v2/network_ip_availability.rst b/doc/source/users/resources/network/v2/network_ip_availability.rst index 80bc576a8..3cbbd9f27 100644 --- a/doc/source/users/resources/network/v2/network_ip_availability.rst +++ b/doc/source/users/resources/network/v2/network_ip_availability.rst @@ -1,5 +1,5 @@ openstack.network.v2.network_ip_availability -=========================================== +============================================ .. automodule:: openstack.network.v2.network_ip_availability diff --git a/doc/source/users/resources/workflow/v2/execution.rst b/doc/source/users/resources/workflow/v2/execution.rst index a0f2aac65..62ec283a1 100644 --- a/doc/source/users/resources/workflow/v2/execution.rst +++ b/doc/source/users/resources/workflow/v2/execution.rst @@ -8,5 +8,5 @@ The Execution Class The ``Execution`` class inherits from :class:`~openstack.resource.Resource`. -.. autoclass:: openstack.workflow.v2.execution +.. autoclass:: openstack.workflow.v2.execution.Execution :members: diff --git a/doc/source/users/resources/workflow/v2/workflow.rst b/doc/source/users/resources/workflow/v2/workflow.rst index 115cb9f33..8f8950e52 100644 --- a/doc/source/users/resources/workflow/v2/workflow.rst +++ b/doc/source/users/resources/workflow/v2/workflow.rst @@ -8,5 +8,5 @@ The Workflow Class The ``Workflow`` class inherits from :class:`~openstack.resource.Resource`. -.. autoclass:: openstack.workflow.v2.workflow +.. autoclass:: openstack.workflow.v2.workflow.Workflow :members: diff --git a/openstack/cluster/v1/_proxy.py b/openstack/cluster/v1/_proxy.py index 1286942ce..9ed0e65b8 100644 --- a/openstack/cluster/v1/_proxy.py +++ b/openstack/cluster/v1/_proxy.py @@ -297,8 +297,9 @@ def cluster_del_nodes(self, cluster, nodes, **params): :param nodes: List of nodes to be removed from the cluster. :param kwargs \*\*params: Optional query parameters to be sent to restrict the nodes to be returned. Available parameters include: + * destroy_after_deletion: A boolean value indicating whether the - deleted nodes to be destroyed right away. + deleted nodes to be destroyed right away. :returns: A dict containing the action initiated by this operation. """ if isinstance(cluster, _cluster.Cluster): @@ -709,6 +710,7 @@ def cluster_policies(self, cluster, **query): :class:`~openstack.cluster.v1.cluster.Cluster` instance. :param kwargs \*\*query: Optional query parameters to be sent to restrict the policies to be returned. Available parameters include: + * enabled: A boolean value indicating whether the policy is enabled on the cluster. :returns: A generator of cluster-policy binding instances. diff --git a/openstack/connection.py b/openstack/connection.py index fe114372a..6f42d35d4 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -276,10 +276,10 @@ def authorize(self): :returns: A string token. - :raises:`~openstack.exceptions.HttpException` if the authorization - fails due to reasons like the credentials provided are unable - to be authorized or the `auth_plugin` argument is missing, - etc. + :raises: :class:`~openstack.exceptions.HttpException` if the + authorization fails due to reasons like the credentials + provided are unable to be authorized or the `auth_plugin` + argument is missing, etc. """ headers = self.session.get_auth_headers() diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index ee461dbb9..6e1f6a525 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -154,7 +154,7 @@ def agents(self, **query): running on. * ``topic``: The message queue topic used. * ``is_admin_state_up``: The administrative state of the agent. - : ``is_alive``: Whether the agent is alive. + * ``is_alive``: Whether the agent is alive. :returns: A generator of agents :rtype: :class:`~openstack.network.v2.agent.Agent` @@ -2662,9 +2662,8 @@ def get_service_profile(self, service_profile): """Get a single network service flavor profile :param service_profile: The value can be the ID of a service_profile or - a - :class:`~openstack.network.v2.service_profile - .ServiceProfile` instance. + a :class:`~openstack.network.v2.service_profile.ServiceProfile` + instance. :returns: One :class:`~openstack.network.v2.service_profile .ServiceProfile` @@ -2682,7 +2681,7 @@ def service_profiles(self, **query): * ``description``: The description of the service flavor profile * ``driver``: Provider driver for the service flavor profile * ``is_enabled``: Whether the profile is enabled - * ``project_id``: The owner project ID + * ``project_id``: The owner project ID :returns: A generator of service profile objects :rtype: :class:`~openstack.network.v2.service_profile.ServiceProfile` diff --git a/openstack/network/v2/router.py b/openstack/network/v2/router.py index 89539110f..3c21b113d 100644 --- a/openstack/network/v2/router.py +++ b/openstack/network/v2/router.py @@ -80,7 +80,7 @@ def add_interface(self, session, **body): :param session: The session to communicate through. :type session: :class:`~openstack.session.Session` - :param dict body : The body requested to be updated on the outer + :param dict body: The body requested to be updated on the router :returns: The body of the response as a dictionary. """ @@ -93,7 +93,7 @@ def remove_interface(self, session, **body): :param session: The session to communicate through. :type session: :class:`~openstack.session.Session` - :param dict body : The body requested to be updated on the outer + :param dict body: The body requested to be updated on the router :returns: The body of the response as a dictionary. """ @@ -106,7 +106,7 @@ def add_gateway(self, session, **body): :param session: The session to communicate through. :type session: :class:`~openstack.session.Session` - :param dict body : The body requested to be updated on the outer + :param dict body: The body requested to be updated on the router :returns: The body of the response as a dictionary. """ @@ -120,7 +120,7 @@ def remove_gateway(self, session, **body): :param session: The session to communicate through. :type session: :class:`~openstack.session.Session` - :param dict body : The body requested to be updated on the outer + :param dict body: The body requested to be updated on the router :returns: The body of the response as a dictionary. """ diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index 7065b96e6..654cc9e3e 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -168,8 +168,8 @@ def software_configs(self, **query): :param dict query: Optional query parameters to be sent to limit the software configs returned. :returns: A generator of software config objects. - :rtype: - :class:`~openstack.orchestration.v1.software_config.SoftwareConfig` + :rtype: :class:`~openstack.orchestration.v1.software_config.\ + SoftwareConfig` """ return self._list(_sc.SoftwareConfig, paginated=True, **query) @@ -220,8 +220,8 @@ def software_deployments(self, **query): :param dict query: Optional query parameters to be sent to limit the software deployments returned. :returns: A generator of software deployment objects. - :rtype: - :class:`~openstack.orchestration.v1.software_deployment.SoftwareDeployment` + :rtype: :class:`~openstack.orchestration.v1.software_deployment.\ + SoftwareDeployment` """ return self._list(_sd.SoftwareDeployment, paginated=False, **query) @@ -263,8 +263,8 @@ def update_software_deployment(self, software_deployment, **attrs): represented by ``software_deployment``. :returns: The updated software deployment - :rtype: - :class:`~openstack.orchestration.v1.software_deployment.SoftwareDeployment` + :rtype: :class:`~openstack.orchestration.v1.software_deployment.\ + SoftwareDeployment` """ return self._update(_sd.SoftwareDeployment, software_deployment, **attrs) From 5eb32e562b850b4808ba63b8a1121987bcdd42d1 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 7 Feb 2017 16:51:41 -0600 Subject: [PATCH 1251/3836] Add test of attaching a volume at boot time In addition to booting from volume, we also support booting from image and attaching a volume. Test it to make it true. Run of this test without the previous patch fixing it: http://logs.openstack.org/68/430468/2/check/gate-shade-dsvm-functional-neutron/51ab62a/console.html Change-Id: Ia7a76cefa9d01909723f6df1f9055e3f73c0f703 --- shade/tests/functional/test_compute.py | 30 ++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index adeff2f0f..a06c23376 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -145,9 +145,9 @@ def test_get_image_name(self): self.assertEqual( self.image.name, self.demo_cloud.get_image_name(self.image.name)) - def _assert_volume_attach(self, server, volume_id=None): + def _assert_volume_attach(self, server, volume_id=None, image=''): self.assertEqual(self.server_name, server['name']) - self.assertEqual('', server['image']) + self.assertEqual(image, server['image']) self.assertEqual(self.flavor.id, server['flavor']['id']) volumes = self.demo_cloud.get_volumes(server) self.assertEqual(1, len(volumes)) @@ -230,6 +230,32 @@ def test_create_boot_from_volume_preexisting(self): self.assertIsNone(self.demo_cloud.get_server(self.server_name)) self.assertIsNone(self.demo_cloud.get_volume(volume_id)) + def test_create_boot_attach_volume(self): + if not self.demo_cloud.has_service('volume'): + self.skipTest('volume service not supported by cloud') + self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) + volume = self.demo_cloud.create_volume( + size=1, name=self.server_name, image=self.image, wait=True) + self.addCleanup(self.demo_cloud.delete_volume, volume['id']) + server = self.demo_cloud.create_server( + name=self.server_name, + flavor=self.flavor, + image=self.image, + boot_from_volume=False, + volumes=[volume], + wait=True) + volume_id = self._assert_volume_attach( + server, volume_id=volume['id'], image={'id': self.image['id']}) + self.assertTrue( + self.demo_cloud.delete_server(self.server_name, wait=True)) + volume = self.demo_cloud.get_volume(volume_id) + self.assertIsNotNone(volume) + self.assertEqual(volume['name'], volume['display_name']) + self.assertEqual([], volume['attachments']) + self.assertTrue(self.demo_cloud.delete_volume(volume_id)) + self.assertIsNone(self.demo_cloud.get_server(self.server_name)) + self.assertIsNone(self.demo_cloud.get_volume(volume_id)) + def test_create_boot_from_volume_preexisting_terminate(self): if not self.demo_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') From 23cead49192fce6703d263b8125be8022fb91fc3 Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Thu, 9 Feb 2017 12:31:58 -0800 Subject: [PATCH 1252/3836] First keystone test using request_mock Update and correct issues with shade to support use of request_mock instead of keystoneclient. This solves issues with the catalog v2, the discovery.json, and leans on the fixes for the test_create_user_v2. Change-Id: I78666d44d0a2ff9f013a68c09c76d092fea3a586 --- shade/tests/unit/base.py | 21 +++++----- shade/tests/unit/fixtures/catalog-v2.json | 4 +- shade/tests/unit/fixtures/clouds/clouds.yaml | 6 +-- .../unit/fixtures/clouds/clouds_cache.yaml | 6 +-- shade/tests/unit/fixtures/discovery.json | 4 +- shade/tests/unit/test_users.py | 39 +++++++++++++------ 6 files changed, 48 insertions(+), 32 deletions(-) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 4fb7dd82b..4d9b203aa 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -110,11 +110,10 @@ def setUp(self, cloud_config_fixture='clouds.yaml'): def use_keystone_v3(self): self.adapter = self.useFixture(rm_fixture.Fixture()) self.calls = [] + self.register_uri('GET', 'https://identity.example.com/', + text=open(self.discovery_json, 'r').read()) self.register_uri( - 'GET', 'http://192.168.0.19:35357/', - text=open(self.discovery_json, 'r').read()) - self.register_uri( - 'POST', 'https://example.com/v3/auth/tokens', + 'POST', 'https://identity.example.com/v3/auth/tokens', headers={ 'X-Subject-Token': self.getUniqueString()}, text=open( @@ -127,20 +126,20 @@ def use_keystone_v3(self): def use_keystone_v2(self): self.adapter = self.useFixture(rm_fixture.Fixture()) self.calls = [] + self.register_uri('GET', 'https://identity.example.com/', + text=open(self.discovery_json, 'r').read()) self.register_uri( - 'GET', 'http://192.168.0.19:35357/', - text=open(self.discovery_json, 'r').read()) - self.register_uri( - 'POST', 'https://example.com/v2.0/tokens', + 'POST', 'https://identity.example.com/v2.0/tokens', text=open( os.path.join( self.fixtures_directory, 'catalog-v2.json'), 'r').read()) - self._make_test_cloud(identity_api_version='2.0') + self._make_test_cloud(cloud_name='_test_cloud_v2_', + identity_api_version='2.0') - def _make_test_cloud(self, **kwargs): - test_cloud = os.environ.get('SHADE_OS_CLOUD', '_test_cloud_') + def _make_test_cloud(self, cloud_name='_test_cloud_', **kwargs): + test_cloud = os.environ.get('SHADE_OS_CLOUD', cloud_name) self.cloud_config = self.config.get_one_cloud( cloud=test_cloud, validate=True, **kwargs) self.cloud = shade.OpenStackCloud( diff --git a/shade/tests/unit/fixtures/catalog-v2.json b/shade/tests/unit/fixtures/catalog-v2.json index 5130acd36..653ca1070 100644 --- a/shade/tests/unit/fixtures/catalog-v2.json +++ b/shade/tests/unit/fixtures/catalog-v2.json @@ -77,8 +77,8 @@ { "adminURL": "https://identity.example.com/v2.0", "region": "RegionOne", - "publicURL": "https://identity.example.comv2.0", - "internalURL": "https://identity.example.comv2.0", + "publicURL": "https://identity.example.com/v2.0", + "internalURL": "https://identity.example.com/v2.0", "id": "4deb4d0504a044a395d4480741ba628c" } ], diff --git a/shade/tests/unit/fixtures/clouds/clouds.yaml b/shade/tests/unit/fixtures/clouds/clouds.yaml index 1db8d90aa..ebd4cc0ba 100644 --- a/shade/tests/unit/fixtures/clouds/clouds.yaml +++ b/shade/tests/unit/fixtures/clouds/clouds.yaml @@ -1,7 +1,7 @@ clouds: _test_cloud_: auth: - auth_url: http://192.168.0.19:35357 + auth_url: https://identity.example.com password: password project_name: admin username: admin @@ -10,7 +10,7 @@ clouds: region_name: RegionOne _test_cloud_v2_: auth: - auth_url: http://192.168.0.19:35357 + auth_url: https://identity.example.com password: password project_name: admin username: admin @@ -19,7 +19,7 @@ clouds: _bogus_test_: auth_type: bogus auth: - auth_url: http://198.51.100.1:35357/v2.0 + auth_url: https://identity.example.com/v2.0 username: _test_user_ password: _test_pass_ project_name: _test_project_ diff --git a/shade/tests/unit/fixtures/clouds/clouds_cache.yaml b/shade/tests/unit/fixtures/clouds/clouds_cache.yaml index 614f34502..eb01d37ec 100644 --- a/shade/tests/unit/fixtures/clouds/clouds_cache.yaml +++ b/shade/tests/unit/fixtures/clouds/clouds_cache.yaml @@ -6,7 +6,7 @@ cache: clouds: _test_cloud_: auth: - auth_url: http://192.168.0.19:35357 + auth_url: https://identity.example.com password: password project_name: admin username: admin @@ -15,7 +15,7 @@ clouds: region_name: RegionOne _test_cloud_v2_: auth: - auth_url: http://192.168.0.19:35357 + auth_url: https://identity.example.com password: password project_name: admin username: admin @@ -24,7 +24,7 @@ clouds: _bogus_test_: auth_type: bogus auth: - auth_url: http://198.51.100.1:35357/v2.0 + auth_url: http://identity.example.com/v2.0 username: _test_user_ password: _test_pass_ project_name: _test_project_ diff --git a/shade/tests/unit/fixtures/discovery.json b/shade/tests/unit/fixtures/discovery.json index e61f812d0..9162ecc9d 100644 --- a/shade/tests/unit/fixtures/discovery.json +++ b/shade/tests/unit/fixtures/discovery.json @@ -13,7 +13,7 @@ "id": "v3.6", "links": [ { - "href": "https://example.com/v3/", + "href": "https://identity.example.com/v3/", "rel": "self" } ] @@ -30,7 +30,7 @@ "id": "v2.0", "links": [ { - "href": "https://example.com/v2.0/", + "href": "https://identity.example.com/v2.0/", "rel": "self" }, { diff --git a/shade/tests/unit/test_users.py b/shade/tests/unit/test_users.py index 3d2722a61..1bb525436 100644 --- a/shade/tests/unit/test_users.py +++ b/shade/tests/unit/test_users.py @@ -12,6 +12,7 @@ import mock +import uuid import munch import os_client_config as occ @@ -22,26 +23,42 @@ from shade.tests.unit import base -class TestUsers(base.TestCase): +class TestUsers(base.RequestsMockTestCase): + + def _get_mock_url(self): + service_catalog = self.cloud.keystone_session.auth.get_access( + self.cloud.keystone_session).service_catalog + endpoint_url = service_catalog.url_for( + service_type='identity', + interface='admin') + return '/'.join([endpoint_url, 'users']) + + def test_create_user_v2(self): + self.use_keystone_v2() - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_create_user_v2(self, mock_keystone, mock_api_version): - mock_api_version.return_value = '2' name = 'Mickey Mouse' email = 'mickey@disney.com' password = 'mice-rule' - fake_user = fakes.FakeUser('1', email, name) - mock_keystone.users.create.return_value = fake_user + user_id = uuid.uuid4().hex + + response_json = { + 'user': + {'name': name, + 'email': email, + 'id': user_id + } + } + self.register_uri('POST', self._get_mock_url(), status_code=204, + json=response_json) + self.register_uri('GET', '/'.join([self._get_mock_url(), user_id]), + status_code=200, json=response_json) user = self.op_cloud.create_user( name=name, email=email, password=password, ) - mock_keystone.users.create.assert_called_once_with( - name=name, password=password, email=email, - enabled=True, - ) + self.assertEqual(name, user.name) self.assertEqual(email, user.email) + self.assertEqual(user_id, user.id) @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, 'keystone_client') From 5a3ffeb31f674bee7f6a2668056b5f375ad2ffc5 Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Thu, 9 Feb 2017 14:53:29 -0800 Subject: [PATCH 1253/3836] Cleanup new requests_mock stuff for test_users Cleanup the requests_mock to be replicated for the rest of test_users Change-Id: Ie38689adc33c3035a6f1def7d350e2a8b961a235 --- shade/tests/unit/base.py | 6 ++++ shade/tests/unit/test_users.py | 59 ++++++++++++++++++++++------------ 2 files changed, 45 insertions(+), 20 deletions(-) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 4d9b203aa..25e80f6ca 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -110,6 +110,7 @@ def setUp(self, cloud_config_fixture='clouds.yaml'): def use_keystone_v3(self): self.adapter = self.useFixture(rm_fixture.Fixture()) self.calls = [] + self._uri_registry.clear() self.register_uri('GET', 'https://identity.example.com/', text=open(self.discovery_json, 'r').read()) self.register_uri( @@ -126,6 +127,7 @@ def use_keystone_v3(self): def use_keystone_v2(self): self.adapter = self.useFixture(rm_fixture.Fixture()) self.calls = [] + self._uri_registry.clear() self.register_uri('GET', 'https://identity.example.com/', text=open(self.discovery_json, 'r').read()) self.register_uri( @@ -135,6 +137,10 @@ def use_keystone_v2(self): self.fixtures_directory, 'catalog-v2.json'), 'r').read()) + self.register_uri('GET', 'https://identity.example.com/', + text=open(self.discovery_json, 'r').read()) + self.register_uri('GET', 'https://identity.example.com/', + text=open(self.discovery_json, 'r').read()) self._make_test_cloud(cloud_name='_test_cloud_v2_', identity_api_version='2.0') diff --git a/shade/tests/unit/test_users.py b/shade/tests/unit/test_users.py index 1bb525436..81d1af75a 100644 --- a/shade/tests/unit/test_users.py +++ b/shade/tests/unit/test_users.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. - +import collections import mock import uuid @@ -23,6 +23,12 @@ from shade.tests.unit import base +_UserData = collections.namedtuple( + 'UserData', + 'user_id, password, name, email, description, domain_id, enabled, ' + 'json_response') + + class TestUsers(base.RequestsMockTestCase): def _get_mock_url(self): @@ -33,32 +39,45 @@ def _get_mock_url(self): interface='admin') return '/'.join([endpoint_url, 'users']) + def _get_user_data(self, name=None, password=None, **kwargs): + + name = name or self.getUniqueString('username') + password = password or self.getUniqueString('user_password') + user_id = uuid.uuid4().hex + + response = {'name': name, 'id': user_id} + + if kwargs.get('domain_id'): + kwargs['domain_id'] = uuid.UUID(kwargs['domain_id']) + response['domain_id'] = kwargs.pop('domain_id') + + for item in ('email', 'description', 'enabled'): + response[item] = kwargs.pop(item, None) + self.assertIs(0, len(kwargs), message='extra key-word args received ' + 'on _get_user_data') + + return _UserData(user_id, password, name, response['email'], + response['description'], response.get('domain_id'), + response.get('enabled'), {'user': response}) + def test_create_user_v2(self): self.use_keystone_v2() - name = 'Mickey Mouse' - email = 'mickey@disney.com' - password = 'mice-rule' - user_id = uuid.uuid4().hex + user_data = self._get_user_data() - response_json = { - 'user': - {'name': name, - 'email': email, - 'id': user_id - } - } self.register_uri('POST', self._get_mock_url(), status_code=204, - json=response_json) - self.register_uri('GET', '/'.join([self._get_mock_url(), user_id]), - status_code=200, json=response_json) + json=user_data.json_response) + self.register_uri('GET', + '/'.join([self._get_mock_url(), user_data.user_id]), + status_code=200, json=user_data.json_response) user = self.op_cloud.create_user( - name=name, email=email, password=password, - ) + name=user_data.name, email=user_data.email, + password=user_data.password) - self.assertEqual(name, user.name) - self.assertEqual(email, user.email) - self.assertEqual(user_id, user.id) + self.assertEqual(user_data.name, user.name) + self.assertEqual(user_data.email, user.email) + self.assertEqual(user_data.user_id, user.id) + self.assert_calls() @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, 'keystone_client') From e100a5846370de54a3a22c5010f42d65f45760be Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Thu, 9 Feb 2017 15:43:22 -0800 Subject: [PATCH 1254/3836] Convert first V3 keystone test to requests_mock This introduces further cleanup needed for the first v3 test to be converted to using requests mock and converts create_user_v3 to use requests_mock Change-Id: Ieb5299eb9ca774576070f8fcfe2eb583ef591fb1 --- shade/tests/unit/base.py | 9 ++++ shade/tests/unit/fixtures/catalog-v3.json | 6 +++ shade/tests/unit/test_users.py | 60 ++++++++++++----------- 3 files changed, 47 insertions(+), 28 deletions(-) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 25e80f6ca..02700a22c 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -144,6 +144,15 @@ def use_keystone_v2(self): self._make_test_cloud(cloud_name='_test_cloud_v2_', identity_api_version='2.0') + def _add_discovery_uri_call(self): + # NOTE(notmorgan): Temp workaround for transition to requests + # mock for cases keystoneclient is still mocked directly. This allows + # us to inject another call to discovery where needed in a test that + # no longer mocks out kyestoneclient and performs the extra round + # trips. + self.register_uri('GET', 'https://identity.example.com/', + text=open(self.discovery_json, 'r').read()) + def _make_test_cloud(self, cloud_name='_test_cloud_', **kwargs): test_cloud = os.environ.get('SHADE_OS_CLOUD', cloud_name) self.cloud_config = self.config.get_one_cloud( diff --git a/shade/tests/unit/fixtures/catalog-v3.json b/shade/tests/unit/fixtures/catalog-v3.json index 41cf79a23..66df0f5c1 100644 --- a/shade/tests/unit/fixtures/catalog-v3.json +++ b/shade/tests/unit/fixtures/catalog-v3.json @@ -59,6 +59,12 @@ "interface": "public", "region": "RegionOne", "url": "https://identity.example.com" + }, + { + "id": "012322eeedcd459edabb4933021112bc", + "interface": "admin", + "region": "RegionOne", + "url": "https://identity.example.com" } ], "endpoints_links": [], diff --git a/shade/tests/unit/test_users.py b/shade/tests/unit/test_users.py index 81d1af75a..91a31b179 100644 --- a/shade/tests/unit/test_users.py +++ b/shade/tests/unit/test_users.py @@ -31,13 +31,17 @@ class TestUsers(base.RequestsMockTestCase): - def _get_mock_url(self): + def _get_mock_url(self, v3=False, append=None, interface='admin'): service_catalog = self.cloud.keystone_session.auth.get_access( self.cloud.keystone_session).service_catalog endpoint_url = service_catalog.url_for( service_type='identity', - interface='admin') - return '/'.join([endpoint_url, 'users']) + interface=interface) + to_join = [endpoint_url, 'users'] + if v3: + to_join.insert(1, 'v3') + to_join.extend(append or []) + return '/'.join(to_join) def _get_user_data(self, name=None, password=None, **kwargs): @@ -48,7 +52,7 @@ def _get_user_data(self, name=None, password=None, **kwargs): response = {'name': name, 'id': user_id} if kwargs.get('domain_id'): - kwargs['domain_id'] = uuid.UUID(kwargs['domain_id']) + kwargs['domain_id'] = uuid.UUID(kwargs['domain_id']).hex response['domain_id'] = kwargs.pop('domain_id') for item in ('email', 'description', 'enabled'): @@ -68,7 +72,7 @@ def test_create_user_v2(self): self.register_uri('POST', self._get_mock_url(), status_code=204, json=user_data.json_response) self.register_uri('GET', - '/'.join([self._get_mock_url(), user_data.user_id]), + self._get_mock_url(append=[user_data.user_id]), status_code=200, json=user_data.json_response) user = self.op_cloud.create_user( name=user_data.name, email=user_data.email, @@ -79,30 +83,30 @@ def test_create_user_v2(self): self.assertEqual(user_data.user_id, user.id) self.assert_calls() - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_create_user_v3(self, mock_keystone, mock_api_version): - mock_api_version.return_value = '3' - name = 'Mickey Mouse' - email = 'mickey@disney.com' - password = 'mice-rule' - domain_id = '456' - description = 'fake-description' - fake_user = fakes.FakeUser('1', email, name, description=description) - mock_keystone.users.create.return_value = fake_user + def test_create_user_v3(self): + self._add_discovery_uri_call() + user_data = self._get_user_data( + domain_id=uuid.uuid4().hex, + description=self.getUniqueString('description')) + self.register_uri( + 'POST', + self._get_mock_url(v3=True), status_code=204, + json=user_data.json_response) + self.register_uri( + 'GET', + self._get_mock_url(v3=True, append=[user_data.user_id]), + status_code=200, json=user_data.json_response) user = self.op_cloud.create_user( - name=name, email=email, - password=password, - description=description, - domain_id=domain_id) - mock_keystone.users.create.assert_called_once_with( - name=name, password=password, email=email, - description=description, enabled=True, - domain=domain_id - ) - self.assertEqual(name, user.name) - self.assertEqual(email, user.email) - self.assertEqual(description, user.description) + name=user_data.name, email=user_data.email, + password=user_data.password, + description=user_data.description, + domain_id=user_data.domain_id) + + self.assertEqual(user_data.name, user.name) + self.assertEqual(user_data.email, user.email) + self.assertEqual(user_data.description, user.description) + self.assertEqual(user_data.user_id, user.id) + self.assert_calls() @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, 'keystone_client') From 52eeaa554db445ed368d6afab9f0103a47d58a4e Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 10 Feb 2017 05:59:27 +0000 Subject: [PATCH 1255/3836] Updated from global requirements Change-Id: I9ca1636b5b294de7163c52a7f35bb4f4417f320b --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 7557c662f..a42add4f7 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -11,7 +11,7 @@ openstackdocstheme>=1.5.0 # Apache-2.0 os-testr>=0.8.0 # Apache-2.0 requests!=2.12.2,>=2.10.0 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 -sphinx!=1.3b1,<1.4,>=1.2.1 # BSD +sphinx>=1.5.1 # BSD testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=1.4.0 # MIT From ad3a0fcf448471c5115ec7de8e0ce90da1965f55 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Mon, 6 Feb 2017 11:35:13 -0500 Subject: [PATCH 1256/3836] Enforce inclusion of pulic proxy methods in docs Per the approach on https://review.openstack.org/#/c/428276/, and in general even outside of that change, we should have more strict enforcement on the inclusion of our public proxy APIs in the documentation. This tool runs as a part of our doc build, via `tox -e docs`, and collects all of the public proxy methods and then compares that list to the list of methods that were produced by Sphinx. For right now this will only output warnings, but we should eventually turn on enforcer_warnings_as_errors in doc/source/conf.py so that we can truly enforce these things. If you don't document something, it'll kill the doc build and then kill the docs gate job, so undocumented code won't be accepted. We'll need to leave it off for the time being as we transition into it. Change-Id: I96743de7e0790da98d758415e084a26a92aa3c70 --- doc/source/conf.py | 5 ++ doc/source/enforcer.py | 113 +++++++++++++++++++++++++++++++++++++++++ test-requirements.txt | 1 + 3 files changed, 119 insertions(+) create mode 100644 doc/source/enforcer.py diff --git a/doc/source/conf.py b/doc/source/conf.py index 05443eb38..f31d3b962 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -18,6 +18,7 @@ import openstackdocstheme sys.path.insert(0, os.path.abspath('../..')) +sys.path.insert(0, os.path.abspath('.')) # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be @@ -25,8 +26,12 @@ extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', + 'enforcer' ] +# When True, this will raise an exception that kills sphinx-build. +enforcer_warnings_as_errors = False + # autodoc generation is a bit aggressive and a nuisance when doing heavy # text edit cycles. # execute "export SPHINX_DEBUG=1" in your terminal to disable diff --git a/doc/source/enforcer.py b/doc/source/enforcer.py new file mode 100644 index 000000000..f75f9a307 --- /dev/null +++ b/doc/source/enforcer.py @@ -0,0 +1,113 @@ +import importlib +import os + +from bs4 import BeautifulSoup +from sphinx import errors + +# NOTE: We do this because I can't find any way to pass "-v" +# into sphinx-build through pbr... +DEBUG = True if os.getenv("ENFORCER_DEBUG") else False + +WRITTEN_METHODS = set() + + +class EnforcementError(errors.SphinxError): + """A mismatch between what exists and what's documented""" + category = "Enforcer" + + +def get_proxy_methods(): + """Return a set of public names on all proxies""" + names = ["openstack.bare_metal.v1._proxy", + "openstack.block_store.v2._proxy", + "openstack.cluster.v1._proxy", + "openstack.compute.v2._proxy", + "openstack.database.v1._proxy", + "openstack.identity.v2._proxy", + "openstack.identity.v3._proxy", + "openstack.image.v1._proxy", + "openstack.image.v2._proxy", + "openstack.key_manager.v1._proxy", + "openstack.message.v1._proxy", + "openstack.message.v2._proxy", + "openstack.metric.v1._proxy", + "openstack.network.v2._proxy", + "openstack.object_store.v1._proxy", + "openstack.orchestration.v1._proxy", + "openstack.telemetry.v2._proxy", + "openstack.telemetry.alarm.v2._proxy", + "openstack.workflow.v2._proxy"] + + modules = (importlib.import_module(name) for name in names) + + methods = set() + for module in modules: + # We're not going to use the Proxy for anything other than a `dir` + # so just pass a dummy value so we can create the instance. + instance = module.Proxy("") + # We only document public names + names = [name for name in dir(instance) if not name.startswith("_")] + good_names = [module.__name__ + ".Proxy." + name for name in names] + methods.update(good_names) + + return methods + + +def page_context(app, pagename, templatename, context, doctree): + """Handle html-page-context-event + + This event is emitted once the builder has the contents to create + an HTML page, but before the template is rendered. This is the point + where we'll know what documentation is going to be written, so + gather all of the method names that are about to be included + so we can check which ones were or were not processed earlier + by autodoc. + """ + if "users/proxies" in pagename: + soup = BeautifulSoup(context["body"], "html.parser") + dts = soup.find_all("dt") + ids = [dt.get("id") for dt in dts] + + written = 0 + for id in ids: + if id is not None and "_proxy.Proxy" in id: + WRITTEN_METHODS.add(id) + written += 1 + + if DEBUG: + app.info("ENFORCER: Wrote %d proxy methods for %s" % ( + written, pagename)) + + +def build_finished(app, exception): + """Handle build-finished event + + This event is emitted once the builder has written all of the output. + At this point we just compare what we know was written to what we know + exists within the modules and share the results. + + When enforcer_warnings_as_errors=True in conf.py, this method + will raise EnforcementError on any failures in order to signal failure. + """ + all_methods = get_proxy_methods() + + app.info("ENFORCER: %d proxy methods exist" % len(all_methods)) + app.info("ENFORCER: %d proxy methods written" % len(WRITTEN_METHODS)) + missing = all_methods - WRITTEN_METHODS + missing_count = len(missing) + app.info("ENFORCER: Found %d missing proxy methods " + "in the output" % missing_count) + + for name in sorted(missing): + app.warn("ENFORCER: %s was not included in the output" % name) + + if app.config.enforcer_warnings_as_errors: + raise EnforcementError( + "There are %d undocumented proxy methods" % missing_count) + + +def setup(app): + app.add_config_value("enforcer_warnings_as_errors", False, "env") + + app.connect("html-page-context", page_context) + app.connect("build-finished", build_finished) diff --git a/test-requirements.txt b/test-requirements.txt index 7557c662f..b6f7509d4 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,6 +3,7 @@ # process, which may cause wedges in the gate later. hacking<0.11,>=0.10.0 +beautifulsoup4 # MIT coverage>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD mock>=2.0 # BSD From 068470d4a58908c269d482380109635d3f9cf2ce Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 10 Feb 2017 10:04:19 -0600 Subject: [PATCH 1257/3836] Add request validation to user v2 test We need to check that we are sending what we think we're sending. Change-Id: Ib8dea57cd04582d60c7e44492daeb2f24067e4c7 --- shade/tests/unit/test_users.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/shade/tests/unit/test_users.py b/shade/tests/unit/test_users.py index 91a31b179..ae3d451e0 100644 --- a/shade/tests/unit/test_users.py +++ b/shade/tests/unit/test_users.py @@ -26,7 +26,7 @@ _UserData = collections.namedtuple( 'UserData', 'user_id, password, name, email, description, domain_id, enabled, ' - 'json_response') + 'json_response, json_request') class TestUsers(base.RequestsMockTestCase): @@ -50,19 +50,29 @@ def _get_user_data(self, name=None, password=None, **kwargs): user_id = uuid.uuid4().hex response = {'name': name, 'id': user_id} + request = {'name': name, 'password': password, 'tenantId': None} if kwargs.get('domain_id'): kwargs['domain_id'] = uuid.UUID(kwargs['domain_id']).hex response['domain_id'] = kwargs.pop('domain_id') - for item in ('email', 'description', 'enabled'): - response[item] = kwargs.pop(item, None) + response['email'] = kwargs.pop('email', None) + request['email'] = response['email'] + + response['enabled'] = kwargs.pop('enabled', True) + request['enabled'] = response['enabled'] + + response['description'] = kwargs.pop('description', None) + if response['description']: + request['description'] = response['description'] + self.assertIs(0, len(kwargs), message='extra key-word args received ' 'on _get_user_data') return _UserData(user_id, password, name, response['email'], response['description'], response.get('domain_id'), - response.get('enabled'), {'user': response}) + response.get('enabled'), {'user': response}, + {'user': request}) def test_create_user_v2(self): self.use_keystone_v2() @@ -70,7 +80,8 @@ def test_create_user_v2(self): user_data = self._get_user_data() self.register_uri('POST', self._get_mock_url(), status_code=204, - json=user_data.json_response) + json=user_data.json_response, + validate=dict(json=user_data.json_request)) self.register_uri('GET', self._get_mock_url(append=[user_data.user_id]), status_code=200, json=user_data.json_response) From 4161e5c79aead7861ed8c4737164bad914a0fdb5 Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Fri, 10 Feb 2017 13:51:50 -0800 Subject: [PATCH 1258/3836] Convert test_users to requests_mock Convert the remaining tests for test_users to use requests mock. Change-Id: I504cc3c0be883c8e5c6323cd535a04834eb97119 --- shade/tests/unit/test_users.py | 245 ++++++++++++++++++++------------- 1 file changed, 152 insertions(+), 93 deletions(-) diff --git a/shade/tests/unit/test_users.py b/shade/tests/unit/test_users.py index ae3d451e0..05175a5a8 100644 --- a/shade/tests/unit/test_users.py +++ b/shade/tests/unit/test_users.py @@ -11,15 +11,11 @@ # under the License. import collections -import mock import uuid -import munch -import os_client_config as occ import testtools import shade -from shade.tests import fakes from shade.tests.unit import base @@ -29,20 +25,37 @@ 'json_response, json_request') +_GroupData = collections.namedtuple( + 'GroupData', + 'group_id, group_name, domain_id, description, json_response') + + class TestUsers(base.RequestsMockTestCase): - def _get_mock_url(self, v3=False, append=None, interface='admin'): + def _get_mock_url(self, v3=False, append=None, resource='users', + interface='admin'): service_catalog = self.cloud.keystone_session.auth.get_access( self.cloud.keystone_session).service_catalog endpoint_url = service_catalog.url_for( service_type='identity', interface=interface) - to_join = [endpoint_url, 'users'] + to_join = [endpoint_url, resource] if v3: to_join.insert(1, 'v3') to_join.extend(append or []) return '/'.join(to_join) + def _get_group_data(self, name=None, domain_id=None, description=None): + group_id = uuid.uuid4().hex + name or self.getUniqueString('groupname') + domain_id = uuid.UUID(domain_id or uuid.uuid4().hex).hex + response = {'id': group_id, 'name': name, 'domain_id': domain_id} + if description is not None: + response['description'] = description + + return _GroupData(group_id, name, domain_id, description, + {'group': response}) + def _get_user_data(self, name=None, password=None, **kwargs): name = name or self.getUniqueString('username') @@ -102,7 +115,8 @@ def test_create_user_v3(self): self.register_uri( 'POST', self._get_mock_url(v3=True), status_code=204, - json=user_data.json_response) + json=user_data.json_response, + validate=user_data.json_request) self.register_uri( 'GET', self._get_mock_url(v3=True, append=[user_data.user_id]), @@ -119,96 +133,141 @@ def test_create_user_v3(self): self.assertEqual(user_data.user_id, user.id) self.assert_calls() - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_update_user_password_v2(self, mock_keystone, mock_api_version): - mock_api_version.return_value = '2' - name = 'Mickey Mouse' - email = 'mickey@disney.com' - password = 'mice-rule' - domain_id = '1' - user = {'id': '1', 'name': name, 'email': email, 'description': None} - fake_user = fakes.FakeUser(**user) - munch_fake_user = munch.Munch(user) - mock_keystone.users.list.return_value = [fake_user] - mock_keystone.users.get.return_value = fake_user - mock_keystone.users.update.return_value = fake_user - mock_keystone.users.update_password.return_value = fake_user + def test_update_user_password_v2(self): + self.use_keystone_v2() + user_data = self._get_user_data(email='test@example.com') + self.register_uri('GET', self._get_mock_url(), status_code=200, + json={'users': [user_data.json_response['user']]}) + self.register_uri('GET', + self._get_mock_url(append=[user_data.user_id]), + json=user_data.json_response) + self.register_uri( + 'PUT', + self._get_mock_url( + append=[user_data.user_id, 'OS-KSADM', 'password']), + status_code=204, json=user_data.json_response, + validate=dict(json={'user': {'password': user_data.password}})) + self.register_uri('GET', + self._get_mock_url(append=[user_data.user_id]), + json=user_data.json_response) + # NOTE(notmorgan): when keystoneclient is dropped, the extra call is + # not needed as it is a blank put. Keystoneclient has very limited + # logic and does odd things when updates inclue passwords in v2 + # keystone. + self.register_uri( + 'PUT', + self._get_mock_url(append=[user_data.user_id]), + status_code=204, json=user_data.json_response, + validate=dict(json={'user': {}})) + self.register_uri('GET', + self._get_mock_url(append=[user_data.user_id]), + json=user_data.json_response) user = self.op_cloud.update_user( - name, name=name, email=email, - password=password, - domain_id=domain_id) - mock_keystone.users.update.assert_called_once_with( - user=munch_fake_user, name=name, email=email) - mock_keystone.users.update_password.assert_called_once_with( - user=munch_fake_user, password=password) - self.assertEqual(name, user.name) - self.assertEqual(email, user.email) - - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_create_user_v3_no_domain(self, mock_keystone, mock_api_version): - mock_api_version.return_value = '3' - name = 'Mickey Mouse' - email = 'mickey@disney.com' - password = 'mice-rule' + user_data.user_id, password=user_data.password) + self.assertEqual(user_data.name, user.name) + self.assertEqual(user_data.email, user.email) + self.assert_calls() + + def test_create_user_v3_no_domain(self): + user_data = self._get_user_data(domain_id=uuid.uuid4().hex, + email='test@example.com') with testtools.ExpectedException( shade.OpenStackCloudException, "User or project creation requires an explicit" " domain_id argument." ): self.op_cloud.create_user( - name=name, email=email, password=password) - - @mock.patch.object(shade.OpenStackCloud, 'get_user_by_id') - @mock.patch.object(shade.OpenStackCloud, 'get_user') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_delete_user(self, mock_keystone, mock_get_user, mock_get_by_id): - mock_get_user.return_value = dict(id='123') - fake_user = fakes.FakeUser('123', 'email', 'name') - mock_get_by_id.return_value = fake_user - self.assertTrue(self.op_cloud.delete_user('name')) - mock_get_by_id.assert_called_once_with('123', normalize=False) - mock_keystone.users.delete.assert_called_once_with(user=fake_user) - - @mock.patch.object(shade.OpenStackCloud, 'get_user') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_delete_user_not_found(self, mock_keystone, mock_get_user): - mock_get_user.return_value = None - self.assertFalse(self.op_cloud.delete_user('name')) - self.assertFalse(mock_keystone.users.delete.called) - - @mock.patch.object(shade.OpenStackCloud, 'get_user') - @mock.patch.object(shade.OperatorCloud, 'get_group') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_add_user_to_group(self, mock_keystone, mock_group, mock_user): - mock_user.return_value = munch.Munch(dict(id=1)) - mock_group.return_value = munch.Munch(dict(id=2)) - self.op_cloud.add_user_to_group("user", "group") - mock_keystone.users.add_to_group.assert_called_once_with( - user=1, group=2 - ) - - @mock.patch.object(shade.OpenStackCloud, 'get_user') - @mock.patch.object(shade.OperatorCloud, 'get_group') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_is_user_in_group(self, mock_keystone, mock_group, mock_user): - mock_user.return_value = munch.Munch(dict(id=1)) - mock_group.return_value = munch.Munch(dict(id=2)) - mock_keystone.users.check_in_group.return_value = True - self.assertTrue(self.op_cloud.is_user_in_group("user", "group")) - mock_keystone.users.check_in_group.assert_called_once_with( - user=1, group=2 - ) - - @mock.patch.object(shade.OpenStackCloud, 'get_user') - @mock.patch.object(shade.OperatorCloud, 'get_group') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_remove_user_from_group(self, mock_keystone, mock_group, - mock_user): - mock_user.return_value = munch.Munch(dict(id=1)) - mock_group.return_value = munch.Munch(dict(id=2)) - self.op_cloud.remove_user_from_group("user", "group") - mock_keystone.users.remove_from_group.assert_called_once_with( - user=1, group=2 - ) + name=user_data.name, email=user_data.email, + password=user_data.password) + + def test_delete_user(self): + self._add_discovery_uri_call() + user_data = self._get_user_data(domain_id=uuid.uuid4().hex) + self.register_uri('GET', self._get_mock_url(v3=True), status_code=200, + json={'users': [user_data.json_response['user']]}) + self.register_uri('GET', + self._get_mock_url(v3=True, + append=[user_data.user_id]), + status_code=200, json=user_data.json_response) + self.register_uri('DELETE', + self._get_mock_url(v3=True, + append=[user_data.user_id]), + status_code=204) + self.op_cloud.delete_user(user_data.name) + self.assert_calls() + + def test_delete_user_not_found(self): + self._add_discovery_uri_call() + self.register_uri('GET', + self._get_mock_url(v3=True), + status_code=200, + json={'users': []}) + self.assertFalse(self.op_cloud.delete_user(self.getUniqueString())) + + def test_add_user_to_group(self): + self._add_discovery_uri_call() + user_data = self._get_user_data() + group_data = self._get_group_data() + self.register_uri('GET', + self._get_mock_url(v3=True), + status_code=200, + json={'users': [user_data.json_response['user']]}) + self.register_uri( + 'GET', + self._get_mock_url(v3=True, resource='groups'), + status_code=200, + json={'groups': [group_data.json_response['group']]}) + self.register_uri( + 'PUT', + self._get_mock_url( + v3=True, resource='groups', + append=[group_data.group_id, 'users', user_data.user_id]), + status_code=200) + self.op_cloud.add_user_to_group(user_data.user_id, group_data.group_id) + self.assert_calls() + + def test_is_user_in_group(self): + self._add_discovery_uri_call() + user_data = self._get_user_data() + group_data = self._get_group_data() + self.register_uri('GET', + self._get_mock_url(v3=True), + status_code=200, + json={'users': [user_data.json_response['user']]}) + self.register_uri( + 'GET', + self._get_mock_url(v3=True, resource='groups'), + status_code=200, + json={'groups': [group_data.json_response['group']]}) + self.register_uri( + 'HEAD', + self._get_mock_url( + v3=True, resource='groups', + append=[group_data.group_id, 'users', user_data.user_id]), + status_code=204) + self.assertTrue(self.op_cloud.is_user_in_group( + user_data.user_id, group_data.group_id)) + self.assert_calls() + + def test_remove_user_from_group(self): + self._add_discovery_uri_call() + user_data = self._get_user_data() + group_data = self._get_group_data() + self.register_uri('GET', + self._get_mock_url(v3=True), + status_code=200, + json={'users': [user_data.json_response['user']]}) + self.register_uri( + 'GET', + self._get_mock_url(v3=True, resource='groups'), + status_code=200, + json={'groups': [group_data.json_response['group']]}) + self.register_uri( + 'DELETE', + self._get_mock_url( + v3=True, resource='groups', + append=[group_data.group_id, 'users', user_data.user_id]), + status_code=204) + self.op_cloud.remove_user_from_group(user_data.user_id, + group_data.group_id) + self.assert_calls() From b5fb06e8e4b0ca5094e3bb9b3181842512b330a1 Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Fri, 10 Feb 2017 14:40:17 -0800 Subject: [PATCH 1259/3836] Move mock utilies into base Move the utilities developed to mock keystoneclient calls into base test and make more generic for other non-keystone uses. Change-Id: I82d8cf9ff4a5f207890d01a19aee68385f3ff0a3 --- shade/tests/unit/base.py | 71 ++++++++++++++++ shade/tests/unit/test_users.py | 150 +++++++++++++-------------------- 2 files changed, 128 insertions(+), 93 deletions(-) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 02700a22c..788bb4b4e 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -13,7 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. +import collections import time +import uuid import fixtures import mock @@ -27,6 +29,17 @@ from shade.tests import base +_UserData = collections.namedtuple( + 'UserData', + 'user_id, password, name, email, description, domain_id, enabled, ' + 'json_response, json_request') + + +_GroupData = collections.namedtuple( + 'GroupData', + 'group_id, group_name, domain_id, description, json_response') + + class BaseTestCase(base.TestCase): def setUp(self, cloud_config_fixture='clouds.yaml'): @@ -107,6 +120,63 @@ def setUp(self, cloud_config_fixture='clouds.yaml'): self.fixtures_directory, 'discovery.json') self.use_keystone_v3() + def get_mock_url(self, service_type, interface, resource=None, + append=None, base_url_append=None): + service_catalog = self.cloud.keystone_session.auth.get_access( + self.cloud.keystone_session).service_catalog + endpoint_url = service_catalog.url_for( + service_type=service_type, + interface=interface) + to_join = [endpoint_url] + if base_url_append: + to_join.append(base_url_append) + if resource: + to_join.append(resource) + to_join.extend(append or []) + return '/'.join(to_join) + + def _get_group_data(self, name=None, domain_id=None, description=None): + group_id = uuid.uuid4().hex + name or self.getUniqueString('groupname') + domain_id = uuid.UUID(domain_id or uuid.uuid4().hex).hex + response = {'id': group_id, 'name': name, 'domain_id': domain_id} + if description is not None: + response['description'] = description + + return _GroupData(group_id, name, domain_id, description, + {'group': response}) + + def _get_user_data(self, name=None, password=None, **kwargs): + + name = name or self.getUniqueString('username') + password = password or self.getUniqueString('user_password') + user_id = uuid.uuid4().hex + + response = {'name': name, 'id': user_id} + request = {'name': name, 'password': password, 'tenantId': None} + + if kwargs.get('domain_id'): + kwargs['domain_id'] = uuid.UUID(kwargs['domain_id']).hex + response['domain_id'] = kwargs.pop('domain_id') + + response['email'] = kwargs.pop('email', None) + request['email'] = response['email'] + + response['enabled'] = kwargs.pop('enabled', True) + request['enabled'] = response['enabled'] + + response['description'] = kwargs.pop('description', None) + if response['description']: + request['description'] = response['description'] + + self.assertIs(0, len(kwargs), message='extra key-word args received ' + 'on _get_user_data') + + return _UserData(user_id, password, name, response['email'], + response['description'], response.get('domain_id'), + response.get('enabled'), {'user': response}, + {'user': request}) + def use_keystone_v3(self): self.adapter = self.useFixture(rm_fixture.Fixture()) self.calls = [] @@ -141,6 +211,7 @@ def use_keystone_v2(self): text=open(self.discovery_json, 'r').read()) self.register_uri('GET', 'https://identity.example.com/', text=open(self.discovery_json, 'r').read()) + self._make_test_cloud(cloud_name='_test_cloud_v2_', identity_api_version='2.0') diff --git a/shade/tests/unit/test_users.py b/shade/tests/unit/test_users.py index 05175a5a8..adcf5dba2 100644 --- a/shade/tests/unit/test_users.py +++ b/shade/tests/unit/test_users.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import collections import uuid import testtools @@ -19,84 +18,32 @@ from shade.tests.unit import base -_UserData = collections.namedtuple( - 'UserData', - 'user_id, password, name, email, description, domain_id, enabled, ' - 'json_response, json_request') - - -_GroupData = collections.namedtuple( - 'GroupData', - 'group_id, group_name, domain_id, description, json_response') - - class TestUsers(base.RequestsMockTestCase): - def _get_mock_url(self, v3=False, append=None, resource='users', - interface='admin'): - service_catalog = self.cloud.keystone_session.auth.get_access( - self.cloud.keystone_session).service_catalog - endpoint_url = service_catalog.url_for( - service_type='identity', - interface=interface) - to_join = [endpoint_url, resource] + def _get_keystone_mock_url(self, resource, append=None, v3=True): + base_url_append = None if v3: - to_join.insert(1, 'v3') - to_join.extend(append or []) - return '/'.join(to_join) - - def _get_group_data(self, name=None, domain_id=None, description=None): - group_id = uuid.uuid4().hex - name or self.getUniqueString('groupname') - domain_id = uuid.UUID(domain_id or uuid.uuid4().hex).hex - response = {'id': group_id, 'name': name, 'domain_id': domain_id} - if description is not None: - response['description'] = description - - return _GroupData(group_id, name, domain_id, description, - {'group': response}) - - def _get_user_data(self, name=None, password=None, **kwargs): - - name = name or self.getUniqueString('username') - password = password or self.getUniqueString('user_password') - user_id = uuid.uuid4().hex - - response = {'name': name, 'id': user_id} - request = {'name': name, 'password': password, 'tenantId': None} - - if kwargs.get('domain_id'): - kwargs['domain_id'] = uuid.UUID(kwargs['domain_id']).hex - response['domain_id'] = kwargs.pop('domain_id') - - response['email'] = kwargs.pop('email', None) - request['email'] = response['email'] - - response['enabled'] = kwargs.pop('enabled', True) - request['enabled'] = response['enabled'] - - response['description'] = kwargs.pop('description', None) - if response['description']: - request['description'] = response['description'] - - self.assertIs(0, len(kwargs), message='extra key-word args received ' - 'on _get_user_data') - - return _UserData(user_id, password, name, response['email'], - response['description'], response.get('domain_id'), - response.get('enabled'), {'user': response}, - {'user': request}) + base_url_append = 'v3' + return self.get_mock_url( + service_type='identity', interface='admin', resource=resource, + append=append, base_url_append=base_url_append) def test_create_user_v2(self): self.use_keystone_v2() user_data = self._get_user_data() - self.register_uri('POST', self._get_mock_url(), status_code=204, + self.register_uri('POST', + self._get_keystone_mock_url(resource='users', + v3=False), + status_code=204, json=user_data.json_response, validate=dict(json=user_data.json_request)) self.register_uri('GET', - self._get_mock_url(append=[user_data.user_id]), + self._get_keystone_mock_url( + resource='users', + append=[user_data.user_id], + v3=False), status_code=200, json=user_data.json_response) user = self.op_cloud.create_user( name=user_data.name, email=user_data.email, @@ -114,12 +61,14 @@ def test_create_user_v3(self): description=self.getUniqueString('description')) self.register_uri( 'POST', - self._get_mock_url(v3=True), status_code=204, + self._get_keystone_mock_url(resource='users'), + status_code=204, json=user_data.json_response, validate=user_data.json_request) self.register_uri( 'GET', - self._get_mock_url(v3=True, append=[user_data.user_id]), + self._get_keystone_mock_url(resource='users', + append=[user_data.user_id]), status_code=200, json=user_data.json_response) user = self.op_cloud.create_user( name=user_data.name, email=user_data.email, @@ -136,19 +85,28 @@ def test_create_user_v3(self): def test_update_user_password_v2(self): self.use_keystone_v2() user_data = self._get_user_data(email='test@example.com') - self.register_uri('GET', self._get_mock_url(), status_code=200, + self.register_uri('GET', + self._get_keystone_mock_url(resource='users', + v3=False), + status_code=200, json={'users': [user_data.json_response['user']]}) self.register_uri('GET', - self._get_mock_url(append=[user_data.user_id]), + self._get_keystone_mock_url( + resource='users', + v3=False, + append=[user_data.user_id]), json=user_data.json_response) self.register_uri( 'PUT', - self._get_mock_url( + self._get_keystone_mock_url( + resource='users', v3=False, append=[user_data.user_id, 'OS-KSADM', 'password']), status_code=204, json=user_data.json_response, validate=dict(json={'user': {'password': user_data.password}})) self.register_uri('GET', - self._get_mock_url(append=[user_data.user_id]), + self._get_keystone_mock_url( + resource='users', v3=False, + append=[user_data.user_id]), json=user_data.json_response) # NOTE(notmorgan): when keystoneclient is dropped, the extra call is # not needed as it is a blank put. Keystoneclient has very limited @@ -156,11 +114,16 @@ def test_update_user_password_v2(self): # keystone. self.register_uri( 'PUT', - self._get_mock_url(append=[user_data.user_id]), + self._get_keystone_mock_url(resource='users', + append=[user_data.user_id], + v3=False), status_code=204, json=user_data.json_response, validate=dict(json={'user': {}})) self.register_uri('GET', - self._get_mock_url(append=[user_data.user_id]), + self._get_keystone_mock_url( + resource='users', + v3=False, + append=[user_data.user_id]), json=user_data.json_response) user = self.op_cloud.update_user( user_data.user_id, password=user_data.password) @@ -183,15 +146,16 @@ def test_create_user_v3_no_domain(self): def test_delete_user(self): self._add_discovery_uri_call() user_data = self._get_user_data(domain_id=uuid.uuid4().hex) - self.register_uri('GET', self._get_mock_url(v3=True), status_code=200, + self.register_uri('GET', self._get_keystone_mock_url(resource='users'), + status_code=200, json={'users': [user_data.json_response['user']]}) self.register_uri('GET', - self._get_mock_url(v3=True, - append=[user_data.user_id]), + self._get_keystone_mock_url( + resource='users', append=[user_data.user_id]), status_code=200, json=user_data.json_response) self.register_uri('DELETE', - self._get_mock_url(v3=True, - append=[user_data.user_id]), + self._get_keystone_mock_url( + resource='users', append=[user_data.user_id]), status_code=204) self.op_cloud.delete_user(user_data.name) self.assert_calls() @@ -199,7 +163,7 @@ def test_delete_user(self): def test_delete_user_not_found(self): self._add_discovery_uri_call() self.register_uri('GET', - self._get_mock_url(v3=True), + self._get_keystone_mock_url(resource='users'), status_code=200, json={'users': []}) self.assertFalse(self.op_cloud.delete_user(self.getUniqueString())) @@ -209,18 +173,18 @@ def test_add_user_to_group(self): user_data = self._get_user_data() group_data = self._get_group_data() self.register_uri('GET', - self._get_mock_url(v3=True), + self._get_keystone_mock_url(resource='users'), status_code=200, json={'users': [user_data.json_response['user']]}) self.register_uri( 'GET', - self._get_mock_url(v3=True, resource='groups'), + self._get_keystone_mock_url(resource='groups'), status_code=200, json={'groups': [group_data.json_response['group']]}) self.register_uri( 'PUT', - self._get_mock_url( - v3=True, resource='groups', + self._get_keystone_mock_url( + resource='groups', append=[group_data.group_id, 'users', user_data.user_id]), status_code=200) self.op_cloud.add_user_to_group(user_data.user_id, group_data.group_id) @@ -231,18 +195,18 @@ def test_is_user_in_group(self): user_data = self._get_user_data() group_data = self._get_group_data() self.register_uri('GET', - self._get_mock_url(v3=True), + self._get_keystone_mock_url(resource='users'), status_code=200, json={'users': [user_data.json_response['user']]}) self.register_uri( 'GET', - self._get_mock_url(v3=True, resource='groups'), + self._get_keystone_mock_url(resource='groups'), status_code=200, json={'groups': [group_data.json_response['group']]}) self.register_uri( 'HEAD', - self._get_mock_url( - v3=True, resource='groups', + self._get_keystone_mock_url( + resource='groups', append=[group_data.group_id, 'users', user_data.user_id]), status_code=204) self.assertTrue(self.op_cloud.is_user_in_group( @@ -254,18 +218,18 @@ def test_remove_user_from_group(self): user_data = self._get_user_data() group_data = self._get_group_data() self.register_uri('GET', - self._get_mock_url(v3=True), + self._get_keystone_mock_url(resource='users'), status_code=200, json={'users': [user_data.json_response['user']]}) self.register_uri( 'GET', - self._get_mock_url(v3=True, resource='groups'), + self._get_keystone_mock_url(resource='groups'), status_code=200, json={'groups': [group_data.json_response['group']]}) self.register_uri( 'DELETE', - self._get_mock_url( - v3=True, resource='groups', + self._get_keystone_mock_url( + resource='groups', append=[group_data.group_id, 'users', user_data.user_id]), status_code=204) self.op_cloud.remove_user_from_group(user_data.user_id, From 2fd4bdaff149be49430878135cf62fc2fa5c3326 Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Fri, 10 Feb 2017 15:50:30 -0800 Subject: [PATCH 1260/3836] convert test_domain to use requests_mock convert all of the test_domain cases to use requests_mock. Change-Id: I3f24eaaab236c136690d3314b242c1e6ae5b6720 --- shade/tests/unit/test_domains.py | 350 ++++++++++++++++++++++--------- 1 file changed, 256 insertions(+), 94 deletions(-) diff --git a/shade/tests/unit/test_domains.py b/shade/tests/unit/test_domains.py index 143c789b3..7a2f80542 100644 --- a/shade/tests/unit/test_domains.py +++ b/shade/tests/unit/test_domains.py @@ -13,11 +13,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -import mock +import collections +import uuid + import testtools +from testtools import matchers import shade -from shade import meta from shade.tests.unit import base from shade.tests import fakes @@ -30,107 +32,267 @@ ) -class TestDomains(base.TestCase): - - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_list_domains(self, mock_keystone): - self.op_cloud.list_domains() - self.assertTrue(mock_keystone.domains.list.called) - - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_get_domain(self, mock_keystone): - mock_keystone.domains.get.return_value = domain_obj - domain = self.op_cloud.get_domain(domain_id='1234') - self.assertFalse(mock_keystone.domains.list.called) - self.assertTrue(mock_keystone.domains.get.called) - self.assertEqual(domain['name'], 'a-domain') - - @mock.patch.object(shade._utils, '_get_entity') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_get_domain_with_name_or_id(self, mock_keystone, mock_get): - self.op_cloud.get_domain(name_or_id='1234') - mock_get.assert_called_once_with(mock.ANY, - None, '1234') - - @mock.patch.object(shade._utils, 'normalize_domains') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_create_domain(self, mock_keystone, mock_normalize): - mock_keystone.domains.create.return_value = domain_obj - self.op_cloud.create_domain( - domain_obj.name, domain_obj.description) - mock_keystone.domains.create.assert_called_once_with( - name=domain_obj.name, description=domain_obj.description, - enabled=True) - mock_normalize.assert_called_once_with([meta.obj_to_dict(domain_obj)]) - - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_create_domain_exception(self, mock_keystone): - mock_keystone.domains.create.side_effect = Exception() +_DomainData = collections.namedtuple( + 'DomainData', + 'domain_id, domain_name, description, json_response, ' + 'json_request') + + +class TestDomains(base.RequestsMockTestCase): + + def get_mock_url(self, service_type='identity', + interface='admin', resource='domains', + append=None, base_url_append='v3'): + return super(TestDomains, self).get_mock_url( + service_type=service_type, interface=interface, resource=resource, + append=append, base_url_append=base_url_append) + + def _get_domain_data(self, domain_name=None, description=None, + enabled=None): + domain_id = uuid.uuid4().hex + domain_name = domain_name or self.getUniqueString('domainName') + response = {'id': domain_id, 'name': domain_name} + request = {'name': domain_name} + if enabled is not None: + request['enabled'] = bool(enabled) + response['enabled'] = bool(enabled) + if description: + response['description'] = description + request['description'] = description + response.setdefault('enabled', True) + return _DomainData(domain_id, domain_name, description, + {'domain': response}, {'domain': request}) + + def test_list_domains(self): + self._add_discovery_uri_call() + domain_data = self._get_domain_data() + self.register_uri( + 'GET', self.get_mock_url(), status_code=200, + json={'domains': [domain_data.json_response['domain']]}) + domains = self.op_cloud.list_domains() + self.assertThat(len(domains), matchers.Equals(1)) + self.assertThat(domains[0].name, + matchers.Equals(domain_data.domain_name)) + self.assertThat(domains[0].id, + matchers.Equals(domain_data.domain_id)) + self.assert_calls() + + def test_get_domain(self): + self._add_discovery_uri_call() + domain_data = self._get_domain_data() + self.register_uri( + 'GET', self.get_mock_url(append=[domain_data.domain_id]), + status_code=200, + json=domain_data.json_response) + domain = self.op_cloud.get_domain(domain_id=domain_data.domain_id) + self.assertThat(domain.id, matchers.Equals(domain_data.domain_id)) + self.assertThat(domain.name, matchers.Equals(domain_data.domain_name)) + self.assert_calls() + + def test_get_domain_with_name_or_id(self): + self._add_discovery_uri_call() + domain_data = self._get_domain_data() + self.register_uri( + 'GET', self.get_mock_url(), status_code=200, + json={'domains': [domain_data.json_response['domain']]}) + self.register_uri( + 'GET', self.get_mock_url(), status_code=200, + json={'domains': [domain_data.json_response['domain']]}) + domain = self.op_cloud.get_domain(name_or_id=domain_data.domain_id) + domain_by_name = self.op_cloud.get_domain( + name_or_id=domain_data.domain_name) + self.assertThat(domain.id, matchers.Equals(domain_data.domain_id)) + self.assertThat(domain.name, matchers.Equals(domain_data.domain_name)) + self.assertThat(domain_by_name.id, + matchers.Equals(domain_data.domain_id)) + self.assertThat(domain_by_name.name, + matchers.Equals(domain_data.domain_name)) + self.assert_calls() + + def test_create_domain(self): + self._add_discovery_uri_call() + domain_data = self._get_domain_data(description=uuid.uuid4().hex, + enabled=True) + self.register_uri( + 'POST', + self.get_mock_url(), + status_code=200, + json=domain_data.json_response, + validate=dict(json=domain_data.json_request)) + self.register_uri( + 'GET', + self.get_mock_url(append=[domain_data.domain_id]), + status_code=200, + json=domain_data.json_response) + domain = self.op_cloud.create_domain( + domain_data.domain_name, domain_data.description) + self.assertThat(domain.id, matchers.Equals(domain_data.domain_id)) + self.assertThat(domain.name, matchers.Equals(domain_data.domain_name)) + self.assertThat( + domain.description, matchers.Equals(domain_data.description)) + self.assert_calls() + + def test_create_domain_exception(self): + self._add_discovery_uri_call() with testtools.ExpectedException( shade.OpenStackCloudException, "Failed to create domain domain_name" ): + self.register_uri( + 'POST', + self.get_mock_url(), + status_code=409) self.op_cloud.create_domain('domain_name') + self.assert_calls() + + def test_delete_domain(self): + self._add_discovery_uri_call() + domain_data = self._get_domain_data() + new_resp = domain_data.json_response.copy() + new_resp['domain']['enabled'] = False + self.register_uri( + 'PATCH', + self.get_mock_url(append=[domain_data.domain_id]), + status_code=200, + json=new_resp, + validate={'domain': {'enabled': False}}) + self.register_uri( + 'GET', + self.get_mock_url(append=[domain_data.domain_id]), + status_code=200, + json=new_resp) + self.register_uri( + 'DELETE', + self.get_mock_url(append=[domain_data.domain_id]), + status_code=204) + self.op_cloud.delete_domain(domain_data.domain_id) + self.assert_calls() + + def test_delete_domain_name_or_id(self): + self._add_discovery_uri_call() + domain_data = self._get_domain_data() + new_resp = domain_data.json_response.copy() + new_resp['domain']['enabled'] = False + self.register_uri( + 'GET', + self.get_mock_url(), + status_code=200, + json={'domains': [domain_data.json_response['domain']]}) + self.register_uri( + 'PATCH', + self.get_mock_url(append=[domain_data.domain_id]), + status_code=200, + json=new_resp, + validate={'domain': {'enabled': False}}) + self.register_uri( + 'GET', + self.get_mock_url(append=[domain_data.domain_id]), + status_code=200, + json=new_resp) + self.register_uri( + 'DELETE', + self.get_mock_url(append=[domain_data.domain_id]), + status_code=204) + self.op_cloud.delete_domain(name_or_id=domain_data.domain_id) + self.assert_calls() - @mock.patch.object(shade.OperatorCloud, 'update_domain') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_delete_domain(self, mock_keystone, mock_update): - mock_update.return_value = dict(id='update_domain_id') - self.op_cloud.delete_domain('domain_id') - mock_update.assert_called_once_with('domain_id', enabled=False) - mock_keystone.domains.delete.assert_called_once_with( - domain='update_domain_id') - - @mock.patch.object(shade.OperatorCloud, 'get_domain') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_delete_domain_name_or_id(self, mock_keystone, mock_get): - self.op_cloud.update_domain( - name_or_id='a-domain', - name='new name', - description='new description', - enabled=False) - mock_get.assert_called_once_with(None, 'a-domain') - - @mock.patch.object(shade.OperatorCloud, 'update_domain') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_delete_domain_exception(self, mock_keystone, mock_update): - mock_keystone.domains.delete.side_effect = Exception() + def test_delete_domain_exception(self): + # NOTE(notmorgan): This test does not reflect the case where the domain + # cannot be updated to be disabled, Shade raises that as an unable + # to update domain even though it is called via delete_domain. This + # should be fixed in shade to catch either a failure on PATCH, + # subsequent GET, or DELETE call(s). + self._add_discovery_uri_call() + domain_data = self._get_domain_data() + new_resp = domain_data.json_response.copy() + new_resp['domain']['enabled'] = False + self.register_uri( + 'PATCH', + self.get_mock_url(append=[domain_data.domain_id]), + status_code=200, + json=new_resp, + validate={'domain': {'enabled': False}}) + self.register_uri( + 'GET', + self.get_mock_url(append=[domain_data.domain_id]), + status_code=200, + json=new_resp) + self.register_uri( + 'DELETE', + self.get_mock_url(append=[domain_data.domain_id]), + status_code=404) with testtools.ExpectedException( shade.OpenStackCloudException, - "Failed to delete domain domain_id" + "Failed to delete domain %s" % domain_data.domain_id ): - self.op_cloud.delete_domain('domain_id') - - @mock.patch.object(shade._utils, 'normalize_domains') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_update_domain(self, mock_keystone, mock_normalize): - mock_keystone.domains.update.return_value = domain_obj - self.op_cloud.update_domain( - 'domain_id', - name='new name', - description='new description', - enabled=False) - mock_keystone.domains.update.assert_called_once_with( - domain='domain_id', name='new name', - description='new description', enabled=False) - mock_normalize.assert_called_once_with( - [meta.obj_to_dict(domain_obj)]) - - @mock.patch.object(shade.OperatorCloud, 'get_domain') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_update_domain_name_or_id(self, mock_keystone, mock_get): - self.op_cloud.update_domain( - name_or_id='a-domain', - name='new name', - description='new description', - enabled=False) - mock_get.assert_called_once_with(None, 'a-domain') - - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_update_domain_exception(self, mock_keystone): - mock_keystone.domains.update.side_effect = Exception() + self.op_cloud.delete_domain(domain_data.domain_id) + self.assert_calls() + + def test_update_domain(self): + self._add_discovery_uri_call() + domain_data = self._get_domain_data( + description=self.getUniqueString('domainDesc')) + self.register_uri( + 'PATCH', + self.get_mock_url(append=[domain_data.domain_id]), + status_code=200, + json=domain_data.json_response, + validate=dict(json=domain_data.json_request)) + self.register_uri( + 'GET', + self.get_mock_url(append=[domain_data.domain_id]), + status_code=200, + json=domain_data.json_response) + domain = self.op_cloud.update_domain( + domain_data.domain_id, + name=domain_data.domain_name, + description=domain_data.description) + self.assertThat(domain.id, matchers.Equals(domain_data.domain_id)) + self.assertThat(domain.name, matchers.Equals(domain_data.domain_name)) + self.assertThat( + domain.description, matchers.Equals(domain_data.description)) + self.assert_calls() + + def test_update_domain_name_or_id(self): + self._add_discovery_uri_call() + domain_data = self._get_domain_data( + description=self.getUniqueString('domainDesc')) + self.register_uri( + 'GET', + self.get_mock_url(), status_code=200, + json={'domains': [domain_data.json_response['domain']]}) + self.register_uri( + 'PATCH', + self.get_mock_url(append=[domain_data.domain_id]), + status_code=200, + json=domain_data.json_response, + validate=dict(json=domain_data.json_request)) + self.register_uri( + 'GET', + self.get_mock_url(append=[domain_data.domain_id]), + status_code=200, + json=domain_data.json_response) + domain = self.op_cloud.update_domain( + name_or_id=domain_data.domain_id, + name=domain_data.domain_name, + description=domain_data.description) + self.assertThat(domain.id, matchers.Equals(domain_data.domain_id)) + self.assertThat(domain.name, matchers.Equals(domain_data.domain_name)) + self.assertThat( + domain.description, matchers.Equals(domain_data.description)) + self.assert_calls() + + def test_update_domain_exception(self): + self._add_discovery_uri_call() + domain_data = self._get_domain_data( + description=self.getUniqueString('domainDesc')) + self.register_uri( + 'PATCH', + self.get_mock_url(append=[domain_data.domain_id]), + status_code=409) with testtools.ExpectedException( shade.OpenStackCloudException, - "Error in updating domain domain_id" + "Error in updating domain %s" % domain_data.domain_id ): - self.op_cloud.delete_domain('domain_id') + self.op_cloud.delete_domain(domain_data.domain_id) + self.assert_calls() From b0bbb8ca0809b60536113bbe5c280679bdc25965 Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Fri, 10 Feb 2017 21:13:34 -0800 Subject: [PATCH 1261/3836] Convert test_project to requests_mock Convert tests from test_project to requests_mock instead of direct mock of keystoneclient. Change-Id: I73fef17367dcaa7d66a3d3d3661775b880225cea --- shade/tests/unit/test_project.py | 396 +++++++++++++++++++++---------- 1 file changed, 276 insertions(+), 120 deletions(-) diff --git a/shade/tests/unit/test_project.py b/shade/tests/unit/test_project.py index 1f4125d92..fbdc5ebb6 100644 --- a/shade/tests/unit/test_project.py +++ b/shade/tests/unit/test_project.py @@ -10,51 +10,116 @@ # License for the specific language governing permissions and limitations # under the License. +import collections +import uuid -import mock - -import munch -import os_client_config as occ import testtools +from testtools import matchers import shade import shade._utils from shade.tests.unit import base -class TestProject(base.TestCase): - - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_create_project_v2(self, mock_keystone, mock_api_version): - mock_api_version.return_value = '2' - name = 'project_name' - description = 'Project description' - self.op_cloud.create_project(name=name, description=description) - mock_keystone.tenants.create.assert_called_once_with( - project_name=name, description=description, enabled=True, - tenant_name=name - ) - - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_create_project_v3(self, mock_keystone, mock_api_version): - mock_api_version.return_value = '3' - name = 'project_name' - description = 'Project description' - domain_id = '123' - self.op_cloud.create_project( - name=name, description=description, domain_id=domain_id) - mock_keystone.projects.create.assert_called_once_with( - project_name=name, description=description, enabled=True, - name=name, domain=domain_id - ) - - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_create_project_v3_no_domain(self, mock_keystone, - mock_api_version): - mock_api_version.return_value = '3' +_ProjectData = collections.namedtuple( + 'ProjectData', + 'project_id, project_name, enabled, domain_id, description, ' + 'json_response, json_request') + + +class TestProject(base.RequestsMockTestCase): + + def get_mock_url(self, service_type='identity', interface='admin', + resource=None, append=None, base_url_append=None, + v3=True): + if v3 and resource is None: + resource = 'projects' + elif not v3 and resource is None: + resource = 'tenants' + if base_url_append is None and v3: + base_url_append = 'v3' + return super(TestProject, self).get_mock_url( + service_type=service_type, interface=interface, resource=resource, + append=append, base_url_append=base_url_append) + + def _get_project_data(self, project_name=None, enabled=None, + description=None, v3=True): + project_name = project_name or self.getUniqueString('projectName') + project_id = uuid.uuid4().hex + response = {'id': project_id, 'name': project_name} + request = {'name': project_name} + domain_id = None + if v3: + domain_id = uuid.uuid4().hex + request['domain_id'] = domain_id + response['domain_id'] = domain_id + if enabled is not None: + enabled = bool(enabled) + response['enabled'] = enabled + request['enabled'] = enabled + response.setdefault('enabled', True) + if description: + response['description'] = description + request['description'] = description + if v3: + project_key = 'project' + else: + project_key = 'tenant' + return _ProjectData(project_id, project_name, enabled, domain_id, + description, {project_key: response}, + {project_key: request}) + + def test_create_project_v2(self): + self.use_keystone_v2() + project_data = self._get_project_data(v3=False) + self.register_uri( + 'POST', + self.get_mock_url(v3=False), + status_code=200, + json=project_data.json_response, + validate=project_data.json_request) + self.register_uri( + 'GET', + self.get_mock_url(v3=False, append=[project_data.project_id]), + status_code=200, + json=project_data.json_response) + project = self.op_cloud.create_project( + name=project_data.project_name, + description=project_data.description) + self.assertThat(project.id, matchers.Equals(project_data.project_id)) + self.assertThat( + project.name, matchers.Equals(project_data.project_name)) + self.assert_calls() + + def test_create_project_v3(self,): + self._add_discovery_uri_call() + project_data = self._get_project_data( + description=self.getUniqueString('projectDesc')) + self.register_uri( + 'POST', + self.get_mock_url(), + status_code=200, + json=project_data.json_response, + validate=project_data.json_request) + self.register_uri( + 'GET', + self.get_mock_url(append=[project_data.project_id]), + status_code=200, + json=project_data.json_response) + project = self.op_cloud.create_project( + name=project_data.project_name, + description=project_data.description, + domain_id=project_data.domain_id) + self.assertThat(project.id, matchers.Equals(project_data.project_id)) + self.assertThat( + project.name, matchers.Equals(project_data.project_name)) + self.assertThat( + project.description, matchers.Equals(project_data.description)) + self.assertThat( + project.domain_id, matchers.Equals(project_data.domain_id)) + self.assert_calls() + + def test_create_project_v3_no_domain(self): with testtools.ExpectedException( shade.OpenStackCloudException, "User or project creation requires an explicit" @@ -62,89 +127,180 @@ def test_create_project_v3_no_domain(self, mock_keystone, ): self.op_cloud.create_project(name='foo', description='bar') - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, 'get_project') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_delete_project_v2(self, mock_keystone, mock_get, - mock_api_version): - mock_api_version.return_value = '2' - mock_get.return_value = dict(id='123') - self.assertTrue(self.op_cloud.delete_project('123')) - mock_get.assert_called_once_with('123', domain_id=None) - mock_keystone.tenants.delete.assert_called_once_with(tenant='123') - - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, 'get_project') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_delete_project_v3(self, mock_keystone, mock_get, - mock_api_version): - mock_api_version.return_value = '3' - mock_get.return_value = dict(id='123') - self.assertTrue(self.op_cloud.delete_project('123')) - mock_get.assert_called_once_with('123', domain_id=None) - mock_keystone.projects.delete.assert_called_once_with(project='123') - - @mock.patch.object(shade.OpenStackCloud, 'get_project') - def test_update_project_not_found(self, mock_get_project): - mock_get_project.return_value = None + def test_delete_project_v2(self): + self.use_keystone_v2() + project_data = self._get_project_data(v3=False) + self.register_uri( + 'GET', + self.get_mock_url(v3=False), + status_code=200, + json={'tenants': [project_data.json_response['tenant']]}) + self.register_uri( + 'DELETE', + self.get_mock_url(v3=False, append=[project_data.project_id]), + status_code=204) + self.op_cloud.delete_project(project_data.project_id) + self.assert_calls() + + def test_delete_project_v3(self): + self._add_discovery_uri_call() + project_data = self._get_project_data(v3=False) + self.register_uri( + 'GET', + self.get_mock_url(), + status_code=200, + json={'projects': [project_data.json_response['tenant']]}) + self.register_uri( + 'DELETE', + self.get_mock_url(append=[project_data.project_id]), + status_code=204) + self.op_cloud.delete_project(project_data.project_id) + self.assert_calls() + + def test_update_project_not_found(self): + self._add_discovery_uri_call() + project_data = self._get_project_data() + self.register_uri( + 'GET', + self.get_mock_url(), + status_code=200, + json={'projects': []}) + # NOTE(notmorgan): This test (and shade) does not represent a case + # where the project is in the project list but a 404 is raised when + # the PATCH is issued. This is a bug in shade and should be fixed, + # shade will raise an attribute error instead of the proper + # project not found exception. with testtools.ExpectedException( shade.OpenStackCloudException, - "Project ABC not found." + "Project %s not found." % project_data.project_id ): - self.op_cloud.update_project('ABC') - - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, 'get_project') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_update_project_v2(self, mock_keystone, mock_get_project, - mock_api_version): - mock_api_version.return_value = '2' - mock_get_project.return_value = munch.Munch(dict(id='123')) - self.op_cloud.update_project('123', description='new', enabled=False) - mock_keystone.tenants.update.assert_called_once_with( - description='new', enabled=False, tenant_id='123') - - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, 'get_project') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_update_project_v3(self, mock_keystone, mock_get_project, - mock_api_version): - mock_api_version.return_value = '3' - mock_get_project.return_value = munch.Munch(dict(id='123')) - self.op_cloud.update_project('123', description='new', enabled=False) - mock_keystone.projects.update.assert_called_once_with( - description='new', enabled=False, project='123') - - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_list_projects_v3(self, mock_keystone, mock_api_version): - mock_api_version.return_value = '3' - self.op_cloud.list_projects('123') - mock_keystone.projects.list.assert_called_once_with( - domain='123') - - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_list_projects_v3_kwarg(self, mock_keystone, mock_api_version): - mock_api_version.return_value = '3' - self.op_cloud.list_projects(domain_id='123') - mock_keystone.projects.list.assert_called_once_with( - domain='123') - - @mock.patch.object(shade._utils, '_filter_list') - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_list_projects_search_compat( - self, mock_keystone, mock_api_version, mock_filter_list): - mock_api_version.return_value = '3' - self.op_cloud.search_projects('123') - mock_keystone.projects.list.assert_called_once_with() - mock_filter_list.assert_called_once_with(mock.ANY, '123', mock.ANY) - - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_list_projects_search_compat_v3( - self, mock_keystone, mock_api_version): - mock_api_version.return_value = '3' - self.op_cloud.search_projects(domain_id='123') - mock_keystone.projects.list.assert_called_once_with(domain='123') + self.op_cloud.update_project(project_data.project_id) + self.assert_calls() + + def test_update_project_v2(self): + self.use_keystone_v2() + project_data = self._get_project_data( + v3=False, + description=self.getUniqueString('projectDesc')) + self.register_uri( + 'GET', + self.get_mock_url(v3=False), + status_code=200, + json={'tenants': [project_data.json_response['tenant']]}) + self.register_uri( + 'POST', + self.get_mock_url(v3=False, append=[project_data.project_id]), + status_code=200, + json=project_data.json_response, + validate=project_data.json_request) + self.register_uri( + 'GET', + self.get_mock_url(v3=False, append=[project_data.project_id]), + status_code=200, + json=project_data.json_response) + project = self.op_cloud.update_project( + project_data.project_id, + description=project_data.description) + self.assertThat(project.id, matchers.Equals(project_data.project_id)) + self.assertThat( + project.name, matchers.Equals(project_data.project_name)) + self.assertThat( + project.description, matchers.Equals(project_data.description)) + self.assert_calls() + + def test_update_project_v3(self): + self._add_discovery_uri_call() + project_data = self._get_project_data( + description=self.getUniqueString('projectDesc')) + self.register_uri( + 'GET', + self.get_mock_url( + resource='projects?domain_id=%s' % project_data.domain_id), + status_code=200, + json={'projects': [project_data.json_response['project']]}) + self.register_uri( + 'PATCH', + self.get_mock_url(append=[project_data.project_id]), + status_code=200, + json=project_data.json_response, + validate=project_data.json_request) + self.register_uri( + 'GET', + self.get_mock_url(append=[project_data.project_id]), + status_code=200, + json=project_data.json_response) + project = self.op_cloud.update_project( + project_data.project_id, + description=project_data.description, + domain_id=project_data.domain_id) + self.assertThat(project.id, matchers.Equals(project_data.project_id)) + self.assertThat( + project.name, matchers.Equals(project_data.project_name)) + self.assertThat( + project.description, matchers.Equals(project_data.description)) + self.assert_calls() + + def test_list_projects_v3(self): + self._add_discovery_uri_call() + project_data = self._get_project_data( + description=self.getUniqueString('projectDesc')) + self.register_uri( + 'GET', + self.get_mock_url( + resource='projects?domain_id=%s' % project_data.domain_id), + status_code=200, + json={'projects': [project_data.json_response['project']]}) + projects = self.op_cloud.list_projects(project_data.domain_id) + self.assertThat(len(projects), matchers.Equals(1)) + self.assertThat( + projects[0].id, matchers.Equals(project_data.project_id)) + self.assert_calls() + + def test_list_projects_v3_kwarg(self): + self._add_discovery_uri_call() + project_data = self._get_project_data( + description=self.getUniqueString('projectDesc')) + self.register_uri( + 'GET', + self.get_mock_url( + resource='projects?domain_id=%s' % project_data.domain_id), + status_code=200, + json={'projects': [project_data.json_response['project']]}) + projects = self.op_cloud.list_projects( + domain_id=project_data.domain_id) + self.assertThat(len(projects), matchers.Equals(1)) + self.assertThat( + projects[0].id, matchers.Equals(project_data.project_id)) + self.assert_calls() + + def test_list_projects_search_compat(self): + self._add_discovery_uri_call() + project_data = self._get_project_data( + description=self.getUniqueString('projectDesc')) + self.register_uri( + 'GET', + self.get_mock_url(), + status_code=200, + json={'projects': [project_data.json_response['project']]}) + projects = self.op_cloud.search_projects(project_data.project_id) + self.assertThat(len(projects), matchers.Equals(1)) + self.assertThat( + projects[0].id, matchers.Equals(project_data.project_id)) + self.assert_calls() + + def test_list_projects_search_compat_v3(self): + self._add_discovery_uri_call() + project_data = self._get_project_data( + description=self.getUniqueString('projectDesc')) + self.register_uri( + 'GET', + self.get_mock_url( + resource='projects?domain_id=%s' % project_data.domain_id), + status_code=200, + json={'projects': [project_data.json_response['project']]}) + projects = self.op_cloud.search_projects( + domain_id=project_data.domain_id) + self.assertThat(len(projects), matchers.Equals(1)) + self.assertThat( + projects[0].id, matchers.Equals(project_data.project_id)) + self.assert_calls() From bf6deba1740cc160eaca2fbdd939608f6241f501 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 11 Feb 2017 17:52:03 +0000 Subject: [PATCH 1262/3836] Updated from global requirements Change-Id: If0958bd90449c12785e8ff451cf334a7e6fd8ff7 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 3a561bbac..4b9d3df52 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,7 +10,7 @@ mock>=2.0 # BSD python-subunit>=0.0.18 # Apache-2.0/BSD openstackdocstheme>=1.5.0 # Apache-2.0 os-testr>=0.8.0 # Apache-2.0 -requests!=2.12.2,>=2.10.0 # Apache-2.0 +requests!=2.12.2,!=2.13.0,>=2.10.0 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 sphinx>=1.5.1 # BSD testrepository>=0.0.18 # Apache-2.0/BSD From d2df08ecd82da45b2cb0076978f9c3342aa1540e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 10 Feb 2017 10:44:29 -0600 Subject: [PATCH 1263/3836] Add all_projects parameter to list and search servers It is possible to tell nova to return the servers for all of the projects rather than just for the currently scoped project. Expose that. Change-Id: I4308758ac1c37ee7c1e63e0f2f58699966eb8364 --- ...servers-all-projects-349e6dc665ba2e8d.yaml | 6 +++++ shade/openstackcloud.py | 20 ++++++++++++----- shade/tests/functional/test_compute.py | 22 +++++++++++++++++++ shade/tests/unit/test_shade.py | 14 ++++++++++++ 4 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/list-servers-all-projects-349e6dc665ba2e8d.yaml diff --git a/releasenotes/notes/list-servers-all-projects-349e6dc665ba2e8d.yaml b/releasenotes/notes/list-servers-all-projects-349e6dc665ba2e8d.yaml new file mode 100644 index 000000000..c993d2d81 --- /dev/null +++ b/releasenotes/notes/list-servers-all-projects-349e6dc665ba2e8d.yaml @@ -0,0 +1,6 @@ +--- +features: + - Add 'all_projects' parameter to list_servers and + search_servers which will tell Nova to return servers for all projects + rather than just for the current project. This is only available to + cloud admins. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 168a912f3..19714c476 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1574,8 +1574,11 @@ def search_security_groups(self, name_or_id=None, filters=None): ) return _utils._filter_list(groups, name_or_id, filters) - def search_servers(self, name_or_id=None, filters=None, detailed=False): - servers = self.list_servers(detailed=detailed) + def search_servers( + self, name_or_id=None, filters=None, detailed=False, + all_projects=False): + servers = self.list_servers( + detailed=detailed, all_projects=all_projects) return _utils._filter_list(servers, name_or_id, filters) def search_server_groups(self, name_or_id=None, filters=None): @@ -1829,7 +1832,7 @@ def list_security_groups(self, filters=None): _tasks.NovaSecurityGroupList(search_opts=filters)) return self._normalize_secgroups(groups) - def list_servers(self, detailed=False): + def list_servers(self, detailed=False, all_projects=False): """List all available servers. :returns: A list of server ``munch.Munch``. @@ -1847,19 +1850,24 @@ def list_servers(self, detailed=False): if self._servers_lock.acquire(first_run): try: if not (first_run and self._servers is not None): - self._servers = self._list_servers(detailed=detailed) + self._servers = self._list_servers( + detailed=detailed, + all_projects=all_projects) self._servers_time = time.time() finally: self._servers_lock.release() return self._servers - def _list_servers(self, detailed=False): + def _list_servers(self, detailed=False, all_projects=False): with _utils.shade_exceptions( "Error fetching server list on {cloud}:{region}:".format( cloud=self.name, region=self.region_name)): + kwargs = {} + if all_projects: + kwargs['search_opts'] = {'all_tenants': True} servers = self._normalize_servers( - self.manager.submit_task(_tasks.ServerList())) + self.manager.submit_task(_tasks.ServerList(**kwargs))) if detailed: return [ diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index a06c23376..399b37eb4 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -68,6 +68,28 @@ def test_create_and_delete_server(self): self.demo_cloud.delete_server(self.server_name, wait=True)) self.assertIsNone(self.demo_cloud.get_server(self.server_name)) + def test_list_all_servers(self): + self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) + server = self.demo_cloud.create_server( + name=self.server_name, + image=self.image, + flavor=self.flavor, + wait=True) + # We're going to get servers from other tests, but that's ok, as long + # as we get the server we created with the demo user. + found_server = False + for s in self.operator_cloud.list_servers(all_projects=True): + if s.name == server.name: + found_server = True + self.assertTrue(found_server) + + def test_list_all_servers_bad_permissions(self): + # Normal users are not allowed to pass all_projects=True + self.assertRaises( + exc.OpenStackCloudException, + self.demo_cloud.list_servers, + all_projects=True) + def test_create_server_image_flavor_dict(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) server = self.demo_cloud.create_server( diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 69992731c..568a52907 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -585,6 +585,20 @@ def test_list_servers_detailed(self, self.assertEqual('server1', r[0]['name']) self.assertEqual('server2', r[1]['name']) + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_list_servers_all_projects(self, mock_nova_client): + '''This test verifies that when list_servers is called with + `all_projects=True` that it passes `all_tenants=1` to novaclient.''' + mock_nova_client.servers.list.return_value = [ + fakes.FakeServer('server1', '', 'ACTIVE'), + fakes.FakeServer('server2', '', 'ACTIVE'), + ] + + self.cloud.list_servers(all_projects=True) + + mock_nova_client.servers.list.assert_called_with( + search_opts={'all_tenants': True}) + def test_iterate_timeout_bad_wait(self): with testtools.ExpectedException( exc.OpenStackCloudException, From eee55a8966898f1c610c052c5aad7aed2d8d0959 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 11 Feb 2017 08:37:24 -0600 Subject: [PATCH 1264/3836] Rename demo_cloud to user_cloud The operator cloud is "operator_cloud" which is clear. Even though the devstack user is called "demo" - the point of the non-operator cloud is that it's an end-user connection. Rename demo to user to remove any confusion/ambiguity. (also, it's the same number of letters so pep8 doesn't get borked) Change-Id: Icfd2dd1687099798ed2cdf9349cc5b7d27dade58 --- shade/tests/functional/base.py | 2 +- .../functional/test_cluster_templates.py | 20 +-- shade/tests/functional/test_compute.py | 156 +++++++++--------- shade/tests/functional/test_devstack.py | 2 +- shade/tests/functional/test_flavor.py | 6 +- shade/tests/functional/test_floating_ip.py | 78 ++++----- .../tests/functional/test_floating_ip_pool.py | 4 +- shade/tests/functional/test_image.py | 34 ++-- shade/tests/functional/test_limits.py | 2 +- shade/tests/functional/test_object.py | 62 +++---- shade/tests/functional/test_range_search.py | 48 +++--- shade/tests/functional/test_recordset.py | 20 +-- .../tests/functional/test_security_groups.py | 8 +- shade/tests/functional/test_server_group.py | 10 +- shade/tests/functional/test_stack.py | 22 +-- shade/tests/functional/test_volume.py | 40 ++--- shade/tests/functional/test_volume_backup.py | 36 ++-- shade/tests/functional/test_volume_type.py | 2 +- shade/tests/functional/test_zone.py | 16 +- 19 files changed, 284 insertions(+), 284 deletions(-) diff --git a/shade/tests/functional/base.py b/shade/tests/functional/base.py index 308cc3e07..6d64cb527 100644 --- a/shade/tests/functional/base.py +++ b/shade/tests/functional/base.py @@ -27,7 +27,7 @@ def setUp(self): self.config = occ.OpenStackConfig() demo_config = self.config.get_one_cloud(cloud=demo_name) - self.demo_cloud = shade.OpenStackCloud( + self.user_cloud = shade.OpenStackCloud( cloud_config=demo_config, log_inner_exceptions=True) operator_config = self.config.get_one_cloud(cloud=op_name) diff --git a/shade/tests/functional/test_cluster_templates.py b/shade/tests/functional/test_cluster_templates.py index bdbd2e43a..8d550e69e 100644 --- a/shade/tests/functional/test_cluster_templates.py +++ b/shade/tests/functional/test_cluster_templates.py @@ -29,7 +29,7 @@ class TestClusterTemplate(base.BaseFunctionalTestCase): def setUp(self): super(TestClusterTemplate, self).setUp() - if not self.demo_cloud.has_service('container-infra'): + if not self.user_cloud.has_service('container-infra'): self.skipTest('Container service not supported by cloud') self.ct = None @@ -58,10 +58,10 @@ def test_cluster_templates(self): # add keypair to nova with open('%s/id_rsa_shade.pub' % ssh_directory) as f: key_content = f.read() - self.demo_cloud.create_keypair('testkey', key_content) + self.user_cloud.create_keypair('testkey', key_content) # Test we can create a cluster_template and we get it returned - self.ct = self.demo_cloud.create_cluster_template( + self.ct = self.user_cloud.create_cluster_template( name=name, image_id=image_id, keypair_id=keypair_id, coe=coe) self.assertEqual(self.ct['name'], name) @@ -74,40 +74,40 @@ def test_cluster_templates(self): self.assertEqual(self.ct['server_type'], server_type) # Test that we can list cluster_templates - cluster_templates = self.demo_cloud.list_cluster_templates() + cluster_templates = self.user_cloud.list_cluster_templates() self.assertIsNotNone(cluster_templates) # Test we get the same cluster_template with the # get_cluster_template method - cluster_template_get = self.demo_cloud.get_cluster_template( + cluster_template_get = self.user_cloud.get_cluster_template( self.ct['uuid']) self.assertEqual(cluster_template_get['uuid'], self.ct['uuid']) # Test the get method also works by name - cluster_template_get = self.demo_cloud.get_cluster_template(name) + cluster_template_get = self.user_cloud.get_cluster_template(name) self.assertEqual(cluster_template_get['name'], self.ct['name']) # Test we can update a field on the cluster_template and only that # field is updated - cluster_template_update = self.demo_cloud.update_cluster_template( + cluster_template_update = self.user_cloud.update_cluster_template( self.ct['uuid'], 'replace', tls_disabled=True) self.assertEqual( cluster_template_update['uuid'], self.ct['uuid']) self.assertEqual(cluster_template_update['tls_disabled'], True) # Test we can delete and get True returned - cluster_template_delete = self.demo_cloud.delete_cluster_template( + cluster_template_delete = self.user_cloud.delete_cluster_template( self.ct['uuid']) self.assertTrue(cluster_template_delete) def cleanup(self, name): if self.ct: try: - self.demo_cloud.delete_cluster_template(self.ct['name']) + self.user_cloud.delete_cluster_template(self.ct['name']) except: pass # delete keypair - self.demo_cloud.delete_keypair('testkey') + self.user_cloud.delete_keypair('testkey') os.unlink('/tmp/.ssh/id_rsa_shade') os.unlink('/tmp/.ssh/id_rsa_shade.pub') diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index 399b37eb4..b0a33794b 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -28,10 +28,10 @@ class TestCompute(base.BaseFunctionalTestCase): def setUp(self): super(TestCompute, self).setUp() - self.flavor = pick_flavor(self.demo_cloud.list_flavors()) + self.flavor = pick_flavor(self.user_cloud.list_flavors()) if self.flavor is None: self.assertFalse('no sensible flavor available') - self.image = pick_image(self.demo_cloud.list_images()) + self.image = pick_image(self.user_cloud.list_images()) if self.image is None: self.assertFalse('no sensible image available') self.server_name = self.getUniqueString() @@ -44,18 +44,18 @@ def _cleanup_servers_and_volumes(self, server_name): a server can start the process of deleting a volume if it is booted from that volume. This encapsulates that logic. """ - server = self.demo_cloud.get_server(server_name) + server = self.user_cloud.get_server(server_name) if not server: return - volumes = self.demo_cloud.get_volumes(server) - self.demo_cloud.delete_server(server.name, wait=True) + volumes = self.user_cloud.get_volumes(server) + self.user_cloud.delete_server(server.name, wait=True) for volume in volumes: if volume.status != 'deleting': - self.demo_cloud.delete_volume(volume.id, wait=True) + self.user_cloud.delete_volume(volume.id, wait=True) def test_create_and_delete_server(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) - server = self.demo_cloud.create_server( + server = self.user_cloud.create_server( name=self.server_name, image=self.image, flavor=self.flavor, @@ -65,12 +65,12 @@ def test_create_and_delete_server(self): self.assertEqual(self.flavor.id, server['flavor']['id']) self.assertIsNotNone(server['adminPass']) self.assertTrue( - self.demo_cloud.delete_server(self.server_name, wait=True)) - self.assertIsNone(self.demo_cloud.get_server(self.server_name)) + self.user_cloud.delete_server(self.server_name, wait=True)) + self.assertIsNone(self.user_cloud.get_server(self.server_name)) def test_list_all_servers(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) - server = self.demo_cloud.create_server( + server = self.user_cloud.create_server( name=self.server_name, image=self.image, flavor=self.flavor, @@ -87,12 +87,12 @@ def test_list_all_servers_bad_permissions(self): # Normal users are not allowed to pass all_projects=True self.assertRaises( exc.OpenStackCloudException, - self.demo_cloud.list_servers, + self.user_cloud.list_servers, all_projects=True) def test_create_server_image_flavor_dict(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) - server = self.demo_cloud.create_server( + server = self.user_cloud.create_server( name=self.server_name, image={'id': self.image.id}, flavor={'id': self.flavor.id}, @@ -102,33 +102,33 @@ def test_create_server_image_flavor_dict(self): self.assertEqual(self.flavor.id, server['flavor']['id']) self.assertIsNotNone(server['adminPass']) self.assertTrue( - self.demo_cloud.delete_server(self.server_name, wait=True)) - self.assertIsNone(self.demo_cloud.get_server(self.server_name)) + self.user_cloud.delete_server(self.server_name, wait=True)) + self.assertIsNone(self.user_cloud.get_server(self.server_name)) def test_get_server_console(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) - server = self.demo_cloud.create_server( + server = self.user_cloud.create_server( name=self.server_name, image=self.image, flavor=self.flavor, wait=True) for _ in _utils._iterate_timeout( 5, "Did not get more than 0 lines in the console log"): - log = self.demo_cloud.get_server_console(server=server) + log = self.user_cloud.get_server_console(server=server) self.assertTrue(isinstance(log, six.string_types)) if len(log) > 0: break def test_get_server_console_name_or_id(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) - self.demo_cloud.create_server( + self.user_cloud.create_server( name=self.server_name, image=self.image, flavor=self.flavor, wait=True) for _ in _utils._iterate_timeout( 5, "Did not get more than 0 lines in the console log"): - log = self.demo_cloud.get_server_console(server=self.server_name) + log = self.user_cloud.get_server_console(server=self.server_name) self.assertTrue(isinstance(log, six.string_types)) if len(log) > 0: break @@ -136,12 +136,12 @@ def test_get_server_console_name_or_id(self): def test_get_server_console_bad_server(self): self.assertRaises( exc.OpenStackCloudException, - self.demo_cloud.get_server_console, + self.user_cloud.get_server_console, server=self.server_name) def test_create_and_delete_server_with_admin_pass(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) - server = self.demo_cloud.create_server( + server = self.user_cloud.create_server( name=self.server_name, image=self.image, flavor=self.flavor, @@ -152,26 +152,26 @@ def test_create_and_delete_server_with_admin_pass(self): self.assertEqual(self.flavor.id, server['flavor']['id']) self.assertEqual(server['adminPass'], 'sheiqu9loegahSh') self.assertTrue( - self.demo_cloud.delete_server(self.server_name, wait=True)) - self.assertIsNone(self.demo_cloud.get_server(self.server_name)) + self.user_cloud.delete_server(self.server_name, wait=True)) + self.assertIsNone(self.user_cloud.get_server(self.server_name)) def test_get_image_id(self): self.assertEqual( - self.image.id, self.demo_cloud.get_image_id(self.image.id)) + self.image.id, self.user_cloud.get_image_id(self.image.id)) self.assertEqual( - self.image.id, self.demo_cloud.get_image_id(self.image.name)) + self.image.id, self.user_cloud.get_image_id(self.image.name)) def test_get_image_name(self): self.assertEqual( - self.image.name, self.demo_cloud.get_image_name(self.image.id)) + self.image.name, self.user_cloud.get_image_name(self.image.id)) self.assertEqual( - self.image.name, self.demo_cloud.get_image_name(self.image.name)) + self.image.name, self.user_cloud.get_image_name(self.image.name)) def _assert_volume_attach(self, server, volume_id=None, image=''): self.assertEqual(self.server_name, server['name']) self.assertEqual(image, server['image']) self.assertEqual(self.flavor.id, server['flavor']['id']) - volumes = self.demo_cloud.get_volumes(server) + volumes = self.user_cloud.get_volumes(server) self.assertEqual(1, len(volumes)) volume = volumes[0] if volume_id: @@ -183,10 +183,10 @@ def _assert_volume_attach(self, server, volume_id=None, image=''): return volume_id def test_create_boot_from_volume_image(self): - if not self.demo_cloud.has_service('volume'): + if not self.user_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) - server = self.demo_cloud.create_server( + server = self.user_cloud.create_server( name=self.server_name, image=self.image, flavor=self.flavor, @@ -194,21 +194,21 @@ def test_create_boot_from_volume_image(self): volume_size=1, wait=True) volume_id = self._assert_volume_attach(server) - volume = self.demo_cloud.get_volume(volume_id) + volume = self.user_cloud.get_volume(volume_id) self.assertIsNotNone(volume) self.assertEqual(volume['name'], volume['display_name']) self.assertEqual(True, volume['bootable']) self.assertEqual(server['id'], volume['attachments'][0]['server_id']) - self.assertTrue(self.demo_cloud.delete_server(server.id, wait=True)) - self.assertTrue(self.demo_cloud.delete_volume(volume.id, wait=True)) - self.assertIsNone(self.demo_cloud.get_server(server.id)) - self.assertIsNone(self.demo_cloud.get_volume(volume.id)) + self.assertTrue(self.user_cloud.delete_server(server.id, wait=True)) + self.assertTrue(self.user_cloud.delete_volume(volume.id, wait=True)) + self.assertIsNone(self.user_cloud.get_server(server.id)) + self.assertIsNone(self.user_cloud.get_volume(volume.id)) def test_create_terminate_volume_image(self): - if not self.demo_cloud.has_service('volume'): + if not self.user_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) - server = self.demo_cloud.create_server( + server = self.user_cloud.create_server( name=self.server_name, image=self.image, flavor=self.flavor, @@ -218,21 +218,21 @@ def test_create_terminate_volume_image(self): wait=True) volume_id = self._assert_volume_attach(server) self.assertTrue( - self.demo_cloud.delete_server(self.server_name, wait=True)) - volume = self.demo_cloud.get_volume(volume_id) + self.user_cloud.delete_server(self.server_name, wait=True)) + volume = self.user_cloud.get_volume(volume_id) # We can either get None (if the volume delete was quick), or a volume # that is in the process of being deleted. if volume: self.assertEqual('deleting', volume.status) - self.assertIsNone(self.demo_cloud.get_server(self.server_name)) + self.assertIsNone(self.user_cloud.get_server(self.server_name)) def test_create_boot_from_volume_preexisting(self): - if not self.demo_cloud.has_service('volume'): + if not self.user_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) - volume = self.demo_cloud.create_volume( + volume = self.user_cloud.create_volume( size=1, name=self.server_name, image=self.image, wait=True) - server = self.demo_cloud.create_server( + server = self.user_cloud.create_server( name=self.server_name, image=None, flavor=self.flavor, @@ -241,25 +241,25 @@ def test_create_boot_from_volume_preexisting(self): wait=True) volume_id = self._assert_volume_attach(server, volume_id=volume['id']) self.assertTrue( - self.demo_cloud.delete_server(self.server_name, wait=True)) - self.addCleanup(self.demo_cloud.delete_volume, volume_id) - volume = self.demo_cloud.get_volume(volume_id) + self.user_cloud.delete_server(self.server_name, wait=True)) + self.addCleanup(self.user_cloud.delete_volume, volume_id) + volume = self.user_cloud.get_volume(volume_id) self.assertIsNotNone(volume) self.assertEqual(volume['name'], volume['display_name']) self.assertEqual(True, volume['bootable']) self.assertEqual([], volume['attachments']) - self.assertTrue(self.demo_cloud.delete_volume(volume_id)) - self.assertIsNone(self.demo_cloud.get_server(self.server_name)) - self.assertIsNone(self.demo_cloud.get_volume(volume_id)) + self.assertTrue(self.user_cloud.delete_volume(volume_id)) + self.assertIsNone(self.user_cloud.get_server(self.server_name)) + self.assertIsNone(self.user_cloud.get_volume(volume_id)) def test_create_boot_attach_volume(self): - if not self.demo_cloud.has_service('volume'): + if not self.user_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) - volume = self.demo_cloud.create_volume( + volume = self.user_cloud.create_volume( size=1, name=self.server_name, image=self.image, wait=True) - self.addCleanup(self.demo_cloud.delete_volume, volume['id']) - server = self.demo_cloud.create_server( + self.addCleanup(self.user_cloud.delete_volume, volume['id']) + server = self.user_cloud.create_server( name=self.server_name, flavor=self.flavor, image=self.image, @@ -269,22 +269,22 @@ def test_create_boot_attach_volume(self): volume_id = self._assert_volume_attach( server, volume_id=volume['id'], image={'id': self.image['id']}) self.assertTrue( - self.demo_cloud.delete_server(self.server_name, wait=True)) - volume = self.demo_cloud.get_volume(volume_id) + self.user_cloud.delete_server(self.server_name, wait=True)) + volume = self.user_cloud.get_volume(volume_id) self.assertIsNotNone(volume) self.assertEqual(volume['name'], volume['display_name']) self.assertEqual([], volume['attachments']) - self.assertTrue(self.demo_cloud.delete_volume(volume_id)) - self.assertIsNone(self.demo_cloud.get_server(self.server_name)) - self.assertIsNone(self.demo_cloud.get_volume(volume_id)) + self.assertTrue(self.user_cloud.delete_volume(volume_id)) + self.assertIsNone(self.user_cloud.get_server(self.server_name)) + self.assertIsNone(self.user_cloud.get_volume(volume_id)) def test_create_boot_from_volume_preexisting_terminate(self): - if not self.demo_cloud.has_service('volume'): + if not self.user_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) - volume = self.demo_cloud.create_volume( + volume = self.user_cloud.create_volume( size=1, name=self.server_name, image=self.image, wait=True) - server = self.demo_cloud.create_server( + server = self.user_cloud.create_server( name=self.server_name, image=None, flavor=self.flavor, @@ -294,69 +294,69 @@ def test_create_boot_from_volume_preexisting_terminate(self): wait=True) volume_id = self._assert_volume_attach(server, volume_id=volume['id']) self.assertTrue( - self.demo_cloud.delete_server(self.server_name, wait=True)) - volume = self.demo_cloud.get_volume(volume_id) + self.user_cloud.delete_server(self.server_name, wait=True)) + volume = self.user_cloud.get_volume(volume_id) # We can either get None (if the volume delete was quick), or a volume # that is in the process of being deleted. if volume: self.assertEqual('deleting', volume.status) - self.assertIsNone(self.demo_cloud.get_server(self.server_name)) + self.assertIsNone(self.user_cloud.get_server(self.server_name)) def test_create_image_snapshot_wait_active(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) - server = self.demo_cloud.create_server( + server = self.user_cloud.create_server( name=self.server_name, image=self.image, flavor=self.flavor, admin_pass='sheiqu9loegahSh', wait=True) - image = self.demo_cloud.create_image_snapshot('test-snapshot', server, + image = self.user_cloud.create_image_snapshot('test-snapshot', server, wait=True) - self.addCleanup(self.demo_cloud.delete_image, image['id']) + self.addCleanup(self.user_cloud.delete_image, image['id']) self.assertEqual('active', image['status']) def test_set_and_delete_metadata(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) - self.demo_cloud.create_server( + self.user_cloud.create_server( name=self.server_name, image=self.image, flavor=self.flavor, wait=True) - self.demo_cloud.set_server_metadata(self.server_name, + self.user_cloud.set_server_metadata(self.server_name, {'key1': 'value1', 'key2': 'value2'}) - updated_server = self.demo_cloud.get_server(self.server_name) + updated_server = self.user_cloud.get_server(self.server_name) self.assertEqual(set(updated_server.metadata.items()), set({'key1': 'value1', 'key2': 'value2'}.items())) - self.demo_cloud.set_server_metadata(self.server_name, + self.user_cloud.set_server_metadata(self.server_name, {'key2': 'value3'}) - updated_server = self.demo_cloud.get_server(self.server_name) + updated_server = self.user_cloud.get_server(self.server_name) self.assertEqual(set(updated_server.metadata.items()), set({'key1': 'value1', 'key2': 'value3'}.items())) - self.demo_cloud.delete_server_metadata(self.server_name, ['key2']) - updated_server = self.demo_cloud.get_server(self.server_name) + self.user_cloud.delete_server_metadata(self.server_name, ['key2']) + updated_server = self.user_cloud.get_server(self.server_name) self.assertEqual(set(updated_server.metadata.items()), set({'key1': 'value1'}.items())) - self.demo_cloud.delete_server_metadata(self.server_name, ['key1']) - updated_server = self.demo_cloud.get_server(self.server_name) + self.user_cloud.delete_server_metadata(self.server_name, ['key1']) + updated_server = self.user_cloud.get_server(self.server_name) self.assertEqual(set(updated_server.metadata.items()), set([])) self.assertRaises( exc.OpenStackCloudException, - self.demo_cloud.delete_server_metadata, + self.user_cloud.delete_server_metadata, self.server_name, ['key1']) def test_update_server(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) - self.demo_cloud.create_server( + self.user_cloud.create_server( name=self.server_name, image=self.image, flavor=self.flavor, wait=True) - server_updated = self.demo_cloud.update_server( + server_updated = self.user_cloud.update_server( self.server_name, name='new_name' ) diff --git a/shade/tests/functional/test_devstack.py b/shade/tests/functional/test_devstack.py index 45453cb8d..d14392007 100644 --- a/shade/tests/functional/test_devstack.py +++ b/shade/tests/functional/test_devstack.py @@ -39,7 +39,7 @@ class TestDevstack(base.BaseFunctionalTestCase): def test_has_service(self): if os.environ.get('SHADE_HAS_{env}'.format(env=self.env), '0') == '1': - self.assertTrue(self.demo_cloud.has_service(self.service)) + self.assertTrue(self.user_cloud.has_service(self.service)) class TestKeystoneVersion(base.BaseFunctionalTestCase): diff --git a/shade/tests/functional/test_flavor.py b/shade/tests/functional/test_flavor.py index 6dbe91cee..1e3590c05 100644 --- a/shade/tests/functional/test_flavor.py +++ b/shade/tests/functional/test_flavor.py @@ -114,7 +114,7 @@ def test_flavor_access(self): new_flavor = self.operator_cloud.create_flavor(**private_kwargs) # Validate the 'demo' user cannot see the new flavor - flavors = self.demo_cloud.search_flavors(priv_flavor_name) + flavors = self.user_cloud.search_flavors(priv_flavor_name) self.assertEqual(0, len(flavors)) # We need the tenant ID for the 'demo' user @@ -125,7 +125,7 @@ def test_flavor_access(self): self.operator_cloud.add_flavor_access(new_flavor['id'], project['id']) # Now see if the 'demo' user has access to it - flavors = self.demo_cloud.search_flavors(priv_flavor_name) + flavors = self.user_cloud.search_flavors(priv_flavor_name) self.assertEqual(1, len(flavors)) self.assertEqual(priv_flavor_name, flavors[0]['name']) @@ -138,7 +138,7 @@ def test_flavor_access(self): # Now revoke the access and make sure we can't find it self.operator_cloud.remove_flavor_access(new_flavor['id'], project['id']) - flavors = self.demo_cloud.search_flavors(priv_flavor_name) + flavors = self.user_cloud.search_flavors(priv_flavor_name) self.assertEqual(0, len(flavors)) def test_set_unset_flavor_specs(self): diff --git a/shade/tests/functional/test_floating_ip.py b/shade/tests/functional/test_floating_ip.py index 608aff5a0..7bd8f2685 100644 --- a/shade/tests/functional/test_floating_ip.py +++ b/shade/tests/functional/test_floating_ip.py @@ -36,9 +36,9 @@ class TestFloatingIP(base.BaseFunctionalTestCase): def setUp(self): super(TestFloatingIP, self).setUp() - self.nova = self.demo_cloud.nova_client - if self.demo_cloud.has_service('network'): - self.neutron = self.demo_cloud.neutron_client + self.nova = self.user_cloud.nova_client + if self.user_cloud.has_service('network'): + self.neutron = self.user_cloud.neutron_client self.flavor = pick_flavor(self.nova.flavors.list()) if self.flavor is None: self.assertFalse('no sensible flavor available') @@ -56,9 +56,9 @@ def _cleanup_network(self): exception_list = list() # Delete stale networks as well as networks created for this test - if self.demo_cloud.has_service('network'): + if self.user_cloud.has_service('network'): # Delete routers - for r in self.demo_cloud.list_routers(): + for r in self.user_cloud.list_routers(): try: if r['name'].startswith(self.new_item_name): # ToDo: update_router currently won't allow removing @@ -69,7 +69,7 @@ def _cleanup_network(self): self.neutron.update_router( router=r['id'], body={'router': router}) # ToDo: Shade currently doesn't have methods for this - for s in self.demo_cloud.list_subnets(): + for s in self.user_cloud.list_subnets(): if s['name'].startswith(self.new_item_name): try: self.neutron.remove_interface_router( @@ -77,23 +77,23 @@ def _cleanup_network(self): body={'subnet_id': s['id']}) except Exception: pass - self.demo_cloud.delete_router(name_or_id=r['id']) + self.user_cloud.delete_router(name_or_id=r['id']) except Exception as e: exception_list.append(str(e)) continue # Delete subnets - for s in self.demo_cloud.list_subnets(): + for s in self.user_cloud.list_subnets(): if s['name'].startswith(self.new_item_name): try: - self.demo_cloud.delete_subnet(name_or_id=s['id']) + self.user_cloud.delete_subnet(name_or_id=s['id']) except Exception as e: exception_list.append(str(e)) continue # Delete networks - for n in self.demo_cloud.list_networks(): + for n in self.user_cloud.list_networks(): if n['name'].startswith(self.new_item_name): try: - self.demo_cloud.delete_network(name_or_id=n['id']) + self.user_cloud.delete_network(name_or_id=n['id']) except Exception as e: exception_list.append(str(e)) continue @@ -131,11 +131,11 @@ def _cleanup_ips(self, server): fixed_ip = meta.get_server_private_ip(server) - for ip in self.demo_cloud.list_floating_ips(): + for ip in self.user_cloud.list_floating_ips(): if (ip.get('fixed_ip', None) == fixed_ip or ip.get('fixed_ip_address', None) == fixed_ip): try: - self.demo_cloud.delete_floating_ip(ip['id']) + self.user_cloud.delete_floating_ip(ip['id']) except Exception as e: exception_list.append(str(e)) continue @@ -146,24 +146,24 @@ def _cleanup_ips(self, server): raise OpenStackCloudException('\n'.join(exception_list)) def _setup_networks(self): - if self.demo_cloud.has_service('network'): + if self.user_cloud.has_service('network'): # Create a network - self.test_net = self.demo_cloud.create_network( + self.test_net = self.user_cloud.create_network( name=self.new_item_name + '_net') # Create a subnet on it - self.test_subnet = self.demo_cloud.create_subnet( + self.test_subnet = self.user_cloud.create_subnet( subnet_name=self.new_item_name + '_subnet', network_name_or_id=self.test_net['id'], cidr='10.24.4.0/24', enable_dhcp=True ) # Create a router - self.test_router = self.demo_cloud.create_router( + self.test_router = self.user_cloud.create_router( name=self.new_item_name + '_router') # Attach the router to an external network - ext_nets = self.demo_cloud.search_networks( + ext_nets = self.user_cloud.search_networks( filters={'router:external': True}) - self.demo_cloud.update_router( + self.user_cloud.update_router( name_or_id=self.test_router['id'], ext_gateway_net_id=ext_nets[0]['id']) # Attach the router to the internal subnet @@ -176,10 +176,10 @@ def _setup_networks(self): self.addDetail( 'networks-neutron', content.text_content(pprint.pformat( - self.demo_cloud.list_networks()))) + self.user_cloud.list_networks()))) else: # ToDo: remove once we have list/get methods for nova networks - nets = self.demo_cloud.nova_client.networks.list() + nets = self.user_cloud.nova_client.networks.list() self.addDetail( 'networks-nova', content.text_content(pprint.pformat( @@ -189,8 +189,8 @@ def _setup_networks(self): def test_private_ip(self): self._setup_networks() - new_server = self.demo_cloud.get_openstack_vars( - self.demo_cloud.create_server( + new_server = self.user_cloud.get_openstack_vars( + self.user_cloud.create_server( wait=True, name=self.new_item_name + '_server', image=self.image, flavor=self.flavor, nics=[self.nic])) @@ -202,7 +202,7 @@ def test_private_ip(self): def test_add_auto_ip(self): self._setup_networks() - new_server = self.demo_cloud.create_server( + new_server = self.user_cloud.create_server( wait=True, name=self.new_item_name + '_server', image=self.image, flavor=self.flavor, nics=[self.nic]) @@ -212,17 +212,17 @@ def test_add_auto_ip(self): ip = None for _ in _utils._iterate_timeout( self.timeout, "Timeout waiting for IP address to be attached"): - ip = meta.get_server_external_ipv4(self.demo_cloud, new_server) + ip = meta.get_server_external_ipv4(self.user_cloud, new_server) if ip is not None: break - new_server = self.demo_cloud.get_server(new_server.id) + new_server = self.user_cloud.get_server(new_server.id) self.addCleanup(self._cleanup_ips, new_server) def test_detach_ip_from_server(self): self._setup_networks() - new_server = self.demo_cloud.create_server( + new_server = self.user_cloud.create_server( wait=True, name=self.new_item_name + '_server', image=self.image, flavor=self.flavor, nics=[self.nic]) @@ -232,29 +232,29 @@ def test_detach_ip_from_server(self): ip = None for _ in _utils._iterate_timeout( self.timeout, "Timeout waiting for IP address to be attached"): - ip = meta.get_server_external_ipv4(self.demo_cloud, new_server) + ip = meta.get_server_external_ipv4(self.user_cloud, new_server) if ip is not None: break - new_server = self.demo_cloud.get_server(new_server.id) + new_server = self.user_cloud.get_server(new_server.id) self.addCleanup(self._cleanup_ips, new_server) - f_ip = self.demo_cloud.get_floating_ip( + f_ip = self.user_cloud.get_floating_ip( id=None, filters={'floating_ip_address': ip}) - self.demo_cloud.detach_ip_from_server( + self.user_cloud.detach_ip_from_server( server_id=new_server.id, floating_ip_id=f_ip['id']) def test_list_floating_ips(self): fip_admin = self.operator_cloud.create_floating_ip() self.addCleanup(self.operator_cloud.delete_floating_ip, fip_admin.id) - fip_user = self.demo_cloud.create_floating_ip() - self.addCleanup(self.demo_cloud.delete_floating_ip, fip_user.id) + fip_user = self.user_cloud.create_floating_ip() + self.addCleanup(self.user_cloud.delete_floating_ip, fip_user.id) # Get all the floating ips. fip_id_list = [ fip.id for fip in self.operator_cloud.list_floating_ips() ] - if self.demo_cloud.has_service('network'): + if self.user_cloud.has_service('network'): # Neutron returns all FIP for all projects by default self.assertIn(fip_admin.id, fip_id_list) self.assertIn(fip_user.id, fip_id_list) @@ -262,7 +262,7 @@ def test_list_floating_ips(self): # Ask Neutron for only a subset of all the FIPs. filtered_fip_id_list = [ fip.id for fip in self.operator_cloud.list_floating_ips( - {'tenant_id': self.demo_cloud.current_project_id} + {'tenant_id': self.user_cloud.current_project_id} ) ] self.assertNotIn(fip_admin.id, filtered_fip_id_list) @@ -279,16 +279,16 @@ def test_list_floating_ips(self): ) def test_search_floating_ips(self): - fip_user = self.demo_cloud.create_floating_ip() - self.addCleanup(self.demo_cloud.delete_floating_ip, fip_user.id) + fip_user = self.user_cloud.create_floating_ip() + self.addCleanup(self.user_cloud.delete_floating_ip, fip_user.id) self.assertIn( fip_user['id'], - [fip.id for fip in self.demo_cloud.search_floating_ips( + [fip.id for fip in self.user_cloud.search_floating_ips( filters={"attached": False})] ) self.assertNotIn( fip_user['id'], - [fip.id for fip in self.demo_cloud.search_floating_ips( + [fip.id for fip in self.user_cloud.search_floating_ips( filters={"attached": True})] ) diff --git a/shade/tests/functional/test_floating_ip_pool.py b/shade/tests/functional/test_floating_ip_pool.py index d22c52648..a6b98e08c 100644 --- a/shade/tests/functional/test_floating_ip_pool.py +++ b/shade/tests/functional/test_floating_ip_pool.py @@ -35,14 +35,14 @@ class TestFloatingIPPool(base.BaseFunctionalTestCase): def setUp(self): super(TestFloatingIPPool, self).setUp() - if not self.demo_cloud._has_nova_extension('os-floating-ip-pools'): + if not self.user_cloud._has_nova_extension('os-floating-ip-pools'): # Skipping this test is floating-ip-pool extension is not # available on the testing cloud self.skip( 'Floating IP pools extension is not available') def test_list_floating_ip_pools(self): - pools = self.demo_cloud.list_floating_ip_pools() + pools = self.user_cloud.list_floating_ip_pools() if not pools: self.assertFalse('no floating-ip pool available') diff --git a/shade/tests/functional/test_image.py b/shade/tests/functional/test_image.py index 8eec489ae..f890b0567 100644 --- a/shade/tests/functional/test_image.py +++ b/shade/tests/functional/test_image.py @@ -28,7 +28,7 @@ class TestImage(base.BaseFunctionalTestCase): def setUp(self): super(TestImage, self).setUp() - self.image = pick_image(self.demo_cloud.nova_client.images.list()) + self.image = pick_image(self.user_cloud.nova_client.images.list()) def test_create_image(self): test_image = tempfile.NamedTemporaryFile(delete=False) @@ -36,7 +36,7 @@ def test_create_image(self): test_image.close() image_name = self.getUniqueString('image') try: - self.demo_cloud.create_image( + self.user_cloud.create_image( name=image_name, filename=test_image.name, disk_format='raw', @@ -45,7 +45,7 @@ def test_create_image(self): min_ram=1024, wait=True) finally: - self.demo_cloud.delete_image(image_name, wait=True) + self.user_cloud.delete_image(image_name, wait=True) def test_download_image(self): test_image = tempfile.NamedTemporaryFile(delete=False) @@ -53,7 +53,7 @@ def test_download_image(self): test_image.write('\0' * 1024 * 1024) test_image.close() image_name = self.getUniqueString('image') - self.demo_cloud.create_image( + self.user_cloud.create_image( name=image_name, filename=test_image.name, disk_format='raw', @@ -61,9 +61,9 @@ def test_download_image(self): min_disk=10, min_ram=1024, wait=True) - self.addCleanup(self.demo_cloud.delete_image, image_name, wait=True) + self.addCleanup(self.user_cloud.delete_image, image_name, wait=True) output = os.path.join(tempfile.gettempdir(), self.getUniqueString()) - self.demo_cloud.download_image(image_name, output) + self.user_cloud.download_image(image_name, output) self.addCleanup(os.remove, output) self.assertTrue(filecmp.cmp(test_image.name, output), "Downloaded contents don't match created image") @@ -74,7 +74,7 @@ def test_create_image_skip_duplicate(self): test_image.close() image_name = self.getUniqueString('image') try: - first_image = self.demo_cloud.create_image( + first_image = self.user_cloud.create_image( name=image_name, filename=test_image.name, disk_format='raw', @@ -82,7 +82,7 @@ def test_create_image_skip_duplicate(self): min_disk=10, min_ram=1024, wait=True) - second_image = self.demo_cloud.create_image( + second_image = self.user_cloud.create_image( name=image_name, filename=test_image.name, disk_format='raw', @@ -92,7 +92,7 @@ def test_create_image_skip_duplicate(self): wait=True) self.assertEqual(first_image.id, second_image.id) finally: - self.demo_cloud.delete_image(image_name, wait=True) + self.user_cloud.delete_image(image_name, wait=True) def test_create_image_force_duplicate(self): test_image = tempfile.NamedTemporaryFile(delete=False) @@ -102,7 +102,7 @@ def test_create_image_force_duplicate(self): first_image = None second_image = None try: - first_image = self.demo_cloud.create_image( + first_image = self.user_cloud.create_image( name=image_name, filename=test_image.name, disk_format='raw', @@ -110,7 +110,7 @@ def test_create_image_force_duplicate(self): min_disk=10, min_ram=1024, wait=True) - second_image = self.demo_cloud.create_image( + second_image = self.user_cloud.create_image( name=image_name, filename=test_image.name, disk_format='raw', @@ -122,9 +122,9 @@ def test_create_image_force_duplicate(self): self.assertNotEqual(first_image.id, second_image.id) finally: if first_image: - self.demo_cloud.delete_image(first_image.id, wait=True) + self.user_cloud.delete_image(first_image.id, wait=True) if second_image: - self.demo_cloud.delete_image(second_image.id, wait=True) + self.user_cloud.delete_image(second_image.id, wait=True) def test_create_image_update_properties(self): test_image = tempfile.NamedTemporaryFile(delete=False) @@ -132,7 +132,7 @@ def test_create_image_update_properties(self): test_image.close() image_name = self.getUniqueString('image') try: - image = self.demo_cloud.create_image( + image = self.user_cloud.create_image( name=image_name, filename=test_image.name, disk_format='raw', @@ -140,12 +140,12 @@ def test_create_image_update_properties(self): min_disk=10, min_ram=1024, wait=True) - self.demo_cloud.update_image_properties( + self.user_cloud.update_image_properties( image=image, name=image_name, foo='bar') - image = self.demo_cloud.get_image(image_name) + image = self.user_cloud.get_image(image_name) self.assertIn('foo', image.properties) self.assertEqual(image.properties['foo'], 'bar') finally: - self.demo_cloud.delete_image(image_name, wait=True) + self.user_cloud.delete_image(image_name, wait=True) diff --git a/shade/tests/functional/test_limits.py b/shade/tests/functional/test_limits.py index 47feafa36..015dafec0 100644 --- a/shade/tests/functional/test_limits.py +++ b/shade/tests/functional/test_limits.py @@ -23,7 +23,7 @@ class TestUsage(base.BaseFunctionalTestCase): def test_get_our_limits(self): '''Test quotas functionality''' - limits = self.demo_cloud.get_compute_limits() + limits = self.user_cloud.get_compute_limits() self.assertIsNotNone(limits) self.assertTrue(hasattr(limits, 'max_server_meta')) diff --git a/shade/tests/functional/test_object.py b/shade/tests/functional/test_object.py index 5d624e765..b14834df3 100644 --- a/shade/tests/functional/test_object.py +++ b/shade/tests/functional/test_object.py @@ -31,17 +31,17 @@ class TestObject(base.BaseFunctionalTestCase): def setUp(self): super(TestObject, self).setUp() - if not self.demo_cloud.has_service('object-store'): + if not self.user_cloud.has_service('object-store'): self.skipTest('Object service not supported by cloud') def test_create_object(self): '''Test uploading small and large files.''' container_name = self.getUniqueString('container') self.addDetail('container', content.text_content(container_name)) - self.addCleanup(self.demo_cloud.delete_container, container_name) - self.demo_cloud.create_container(container_name) + self.addCleanup(self.user_cloud.delete_container, container_name) + self.user_cloud.create_container(container_name) self.assertEqual(container_name, - self.demo_cloud.list_containers()[0]['name']) + self.user_cloud.list_containers()[0]['name']) sizes = ( (64 * 1024, 1), # 64K, one segment (64 * 1024, 5) # 64MB, 5 segments @@ -57,30 +57,30 @@ def test_create_object(self): fake_file.flush() name = 'test-%d' % size self.addCleanup( - self.demo_cloud.delete_object, container_name, name) - self.demo_cloud.create_object( + self.user_cloud.delete_object, container_name, name) + self.user_cloud.create_object( container_name, name, fake_file.name, segment_size=segment_size, metadata={'foo': 'bar'}) - self.assertFalse(self.demo_cloud.is_object_stale( + self.assertFalse(self.user_cloud.is_object_stale( container_name, name, fake_file.name ) ) self.assertEqual( - 'bar', self.demo_cloud.get_object_metadata( + 'bar', self.user_cloud.get_object_metadata( container_name, name)['x-object-meta-foo'] ) - self.demo_cloud.update_object(container=container_name, name=name, + self.user_cloud.update_object(container=container_name, name=name, metadata={'testk': 'testv'}) self.assertEqual( - 'testv', self.demo_cloud.get_object_metadata( + 'testv', self.user_cloud.get_object_metadata( container_name, name)['x-object-meta-testk'] ) try: self.assertIsNotNone( - self.demo_cloud.get_object(container_name, name)) + self.user_cloud.get_object(container_name, name)) except exc.OpenStackCloudException as e: self.addDetail( 'failed_response', @@ -90,22 +90,22 @@ def test_create_object(self): content.text_content(e.response.text)) self.assertEqual( name, - self.demo_cloud.list_objects(container_name)[0]['name']) + self.user_cloud.list_objects(container_name)[0]['name']) self.assertTrue( - self.demo_cloud.delete_object(container_name, name)) - self.assertEqual([], self.demo_cloud.list_objects(container_name)) + self.user_cloud.delete_object(container_name, name)) + self.assertEqual([], self.user_cloud.list_objects(container_name)) self.assertEqual(container_name, - self.demo_cloud.list_containers()[0]['name']) - self.demo_cloud.delete_container(container_name) + self.user_cloud.list_containers()[0]['name']) + self.user_cloud.delete_container(container_name) def test_download_object_to_file(self): '''Test uploading small and large files.''' container_name = self.getUniqueString('container') self.addDetail('container', content.text_content(container_name)) - self.addCleanup(self.demo_cloud.delete_container, container_name) - self.demo_cloud.create_container(container_name) + self.addCleanup(self.user_cloud.delete_container, container_name) + self.user_cloud.create_container(container_name) self.assertEqual(container_name, - self.demo_cloud.list_containers()[0]['name']) + self.user_cloud.list_containers()[0]['name']) sizes = ( (64 * 1024, 1), # 64K, one segment (64 * 1024, 5) # 64MB, 5 segments @@ -122,30 +122,30 @@ def test_download_object_to_file(self): fake_file.flush() name = 'test-%d' % size self.addCleanup( - self.demo_cloud.delete_object, container_name, name) - self.demo_cloud.create_object( + self.user_cloud.delete_object, container_name, name) + self.user_cloud.create_object( container_name, name, fake_file.name, segment_size=segment_size, metadata={'foo': 'bar'}) - self.assertFalse(self.demo_cloud.is_object_stale( + self.assertFalse(self.user_cloud.is_object_stale( container_name, name, fake_file.name ) ) self.assertEqual( - 'bar', self.demo_cloud.get_object_metadata( + 'bar', self.user_cloud.get_object_metadata( container_name, name)['x-object-meta-foo'] ) - self.demo_cloud.update_object(container=container_name, name=name, + self.user_cloud.update_object(container=container_name, name=name, metadata={'testk': 'testv'}) self.assertEqual( - 'testv', self.demo_cloud.get_object_metadata( + 'testv', self.user_cloud.get_object_metadata( container_name, name)['x-object-meta-testk'] ) try: with tempfile.NamedTemporaryFile() as fake_file: - self.demo_cloud.get_object( + self.user_cloud.get_object( container_name, name, outfile=fake_file.name) downloaded_content = open(fake_file.name, 'rb').read() self.assertEqual(fake_content, downloaded_content) @@ -159,10 +159,10 @@ def test_download_object_to_file(self): raise self.assertEqual( name, - self.demo_cloud.list_objects(container_name)[0]['name']) + self.user_cloud.list_objects(container_name)[0]['name']) self.assertTrue( - self.demo_cloud.delete_object(container_name, name)) - self.assertEqual([], self.demo_cloud.list_objects(container_name)) + self.user_cloud.delete_object(container_name, name)) + self.assertEqual([], self.user_cloud.list_objects(container_name)) self.assertEqual(container_name, - self.demo_cloud.list_containers()[0]['name']) - self.demo_cloud.delete_container(container_name) + self.user_cloud.list_containers()[0]['name']) + self.user_cloud.delete_container(container_name) diff --git a/shade/tests/functional/test_range_search.py b/shade/tests/functional/test_range_search.py index f988c6b6b..4b97f3c5e 100644 --- a/shade/tests/functional/test_range_search.py +++ b/shade/tests/functional/test_range_search.py @@ -30,14 +30,14 @@ def _filter_m1_flavors(self, results): return new_results def test_range_search_bad_range(self): - flavors = self.demo_cloud.list_flavors() + flavors = self.user_cloud.list_flavors() self.assertRaises( exc.OpenStackCloudException, - self.demo_cloud.range_search, flavors, {"ram": "<1a0"}) + self.user_cloud.range_search, flavors, {"ram": "<1a0"}) def test_range_search_exact(self): - flavors = self.demo_cloud.list_flavors() - result = self.demo_cloud.range_search(flavors, {"ram": "4096"}) + flavors = self.user_cloud.list_flavors() + result = self.user_cloud.range_search(flavors, {"ram": "4096"}) self.assertIsInstance(result, list) # should only be 1 m1 flavor with 4096 ram result = self._filter_m1_flavors(result) @@ -45,23 +45,23 @@ def test_range_search_exact(self): self.assertEqual("m1.medium", result[0]['name']) def test_range_search_min(self): - flavors = self.demo_cloud.list_flavors() - result = self.demo_cloud.range_search(flavors, {"ram": "MIN"}) + flavors = self.user_cloud.list_flavors() + result = self.user_cloud.range_search(flavors, {"ram": "MIN"}) self.assertIsInstance(result, list) self.assertEqual(1, len(result)) # older devstack does not have cirros256 self.assertTrue(result[0]['name'] in ('cirros256', 'm1.tiny')) def test_range_search_max(self): - flavors = self.demo_cloud.list_flavors() - result = self.demo_cloud.range_search(flavors, {"ram": "MAX"}) + flavors = self.user_cloud.list_flavors() + result = self.user_cloud.range_search(flavors, {"ram": "MAX"}) self.assertIsInstance(result, list) self.assertEqual(1, len(result)) self.assertEqual("m1.xlarge", result[0]['name']) def test_range_search_lt(self): - flavors = self.demo_cloud.list_flavors() - result = self.demo_cloud.range_search(flavors, {"ram": "<1024"}) + flavors = self.user_cloud.list_flavors() + result = self.user_cloud.range_search(flavors, {"ram": "<1024"}) self.assertIsInstance(result, list) # should only be 1 m1 flavor with <1024 ram result = self._filter_m1_flavors(result) @@ -69,8 +69,8 @@ def test_range_search_lt(self): self.assertEqual("m1.tiny", result[0]['name']) def test_range_search_gt(self): - flavors = self.demo_cloud.list_flavors() - result = self.demo_cloud.range_search(flavors, {"ram": ">4096"}) + flavors = self.user_cloud.list_flavors() + result = self.user_cloud.range_search(flavors, {"ram": ">4096"}) self.assertIsInstance(result, list) # should only be 2 m1 flavors with >4096 ram result = self._filter_m1_flavors(result) @@ -80,8 +80,8 @@ def test_range_search_gt(self): self.assertIn("m1.xlarge", flavor_names) def test_range_search_le(self): - flavors = self.demo_cloud.list_flavors() - result = self.demo_cloud.range_search(flavors, {"ram": "<=4096"}) + flavors = self.user_cloud.list_flavors() + result = self.user_cloud.range_search(flavors, {"ram": "<=4096"}) self.assertIsInstance(result, list) # should only be 3 m1 flavors with <=4096 ram result = self._filter_m1_flavors(result) @@ -92,8 +92,8 @@ def test_range_search_le(self): self.assertIn("m1.medium", flavor_names) def test_range_search_ge(self): - flavors = self.demo_cloud.list_flavors() - result = self.demo_cloud.range_search(flavors, {"ram": ">=4096"}) + flavors = self.user_cloud.list_flavors() + result = self.user_cloud.range_search(flavors, {"ram": ">=4096"}) self.assertIsInstance(result, list) # should only be 3 m1 flavors with >=4096 ram result = self._filter_m1_flavors(result) @@ -104,8 +104,8 @@ def test_range_search_ge(self): self.assertIn("m1.xlarge", flavor_names) def test_range_search_multi_1(self): - flavors = self.demo_cloud.list_flavors() - result = self.demo_cloud.range_search( + flavors = self.user_cloud.list_flavors() + result = self.user_cloud.range_search( flavors, {"ram": "MIN", "vcpus": "MIN"}) self.assertIsInstance(result, list) self.assertEqual(1, len(result)) @@ -113,8 +113,8 @@ def test_range_search_multi_1(self): self.assertTrue(result[0]['name'] in ('cirros256', 'm1.tiny')) def test_range_search_multi_2(self): - flavors = self.demo_cloud.list_flavors() - result = self.demo_cloud.range_search( + flavors = self.user_cloud.list_flavors() + result = self.user_cloud.range_search( flavors, {"ram": "<1024", "vcpus": "MIN"}) self.assertIsInstance(result, list) result = self._filter_m1_flavors(result) @@ -123,8 +123,8 @@ def test_range_search_multi_2(self): self.assertIn("m1.tiny", flavor_names) def test_range_search_multi_3(self): - flavors = self.demo_cloud.list_flavors() - result = self.demo_cloud.range_search( + flavors = self.user_cloud.list_flavors() + result = self.user_cloud.range_search( flavors, {"ram": ">=4096", "vcpus": "<6"}) self.assertIsInstance(result, list) result = self._filter_m1_flavors(result) @@ -134,8 +134,8 @@ def test_range_search_multi_3(self): self.assertIn("m1.large", flavor_names) def test_range_search_multi_4(self): - flavors = self.demo_cloud.list_flavors() - result = self.demo_cloud.range_search( + flavors = self.user_cloud.list_flavors() + result = self.user_cloud.range_search( flavors, {"ram": ">=4096", "vcpus": "MAX"}) self.assertIsInstance(result, list) self.assertEqual(1, len(result)) diff --git a/shade/tests/functional/test_recordset.py b/shade/tests/functional/test_recordset.py index 97ccb5dc7..98e209f83 100644 --- a/shade/tests/functional/test_recordset.py +++ b/shade/tests/functional/test_recordset.py @@ -26,7 +26,7 @@ class TestRecordset(base.BaseFunctionalTestCase): def setUp(self): super(TestRecordset, self).setUp() - if not self.demo_cloud.has_service('dns'): + if not self.user_cloud.has_service('dns'): self.skipTest('dns service not supported by cloud') def test_recordsets(self): @@ -44,10 +44,10 @@ def test_recordsets(self): self.addCleanup(self.cleanup, zone, name) # Create a zone to hold the tested recordset - zone_obj = self.demo_cloud.create_zone(name=zone, email=email) + zone_obj = self.user_cloud.create_zone(name=zone, email=email) # Test we can create a recordset and we get it returned - created_recordset = self.demo_cloud.create_recordset(zone, name, type_, + created_recordset = self.user_cloud.create_recordset(zone, name, type_, records, description, ttl) self.assertEqual(created_recordset['zone_id'], zone_obj['id']) @@ -58,21 +58,21 @@ def test_recordsets(self): self.assertEqual(created_recordset['ttl'], ttl) # Test that we can list recordsets - recordsets = self.demo_cloud.list_recordsets(zone) + recordsets = self.user_cloud.list_recordsets(zone) self.assertIsNotNone(recordsets) # Test we get the same recordset with the get_recordset method - get_recordset = self.demo_cloud.get_recordset(zone, + get_recordset = self.user_cloud.get_recordset(zone, created_recordset['id']) self.assertEqual(get_recordset['id'], created_recordset['id']) # Test the get method also works by name - get_recordset = self.demo_cloud.get_recordset(zone, name + '.' + zone) + get_recordset = self.user_cloud.get_recordset(zone, name + '.' + zone) self.assertEqual(get_recordset['id'], created_recordset['id']) # Test we can update a field on the recordset and only that field # is updated - updated_recordset = self.demo_cloud.update_recordset(zone_obj['id'], + updated_recordset = self.user_cloud.update_recordset(zone_obj['id'], name + '.' + zone, ttl=7200) self.assertEqual(updated_recordset['id'], created_recordset['id']) @@ -83,11 +83,11 @@ def test_recordsets(self): self.assertEqual(updated_recordset['ttl'], 7200) # Test we can delete and get True returned - deleted_recordset = self.demo_cloud.delete_recordset( + deleted_recordset = self.user_cloud.delete_recordset( zone, name + '.' + zone) self.assertTrue(deleted_recordset) def cleanup(self, zone_name, recordset_name): - self.demo_cloud.delete_recordset( + self.user_cloud.delete_recordset( zone_name, recordset_name + '.' + zone_name) - self.demo_cloud.delete_zone(zone_name) + self.user_cloud.delete_zone(zone_name) diff --git a/shade/tests/functional/test_security_groups.py b/shade/tests/functional/test_security_groups.py index 853684d69..97819c7a0 100644 --- a/shade/tests/functional/test_security_groups.py +++ b/shade/tests/functional/test_security_groups.py @@ -22,14 +22,14 @@ class TestSecurityGroups(base.BaseFunctionalTestCase): def test_create_list_security_groups(self): - sg1 = self.demo_cloud.create_security_group( + sg1 = self.user_cloud.create_security_group( name="sg1", description="sg1") - self.addCleanup(self.demo_cloud.delete_security_group, sg1['id']) + self.addCleanup(self.user_cloud.delete_security_group, sg1['id']) sg2 = self.operator_cloud.create_security_group( name="sg2", description="sg2") self.addCleanup(self.operator_cloud.delete_security_group, sg2['id']) - if self.demo_cloud.has_service('network'): + if self.user_cloud.has_service('network'): # Neutron defaults to all_tenants=1 when admin sg_list = self.operator_cloud.list_security_groups() self.assertIn(sg1['id'], [sg['id'] for sg in sg_list]) @@ -37,7 +37,7 @@ def test_create_list_security_groups(self): # Filter by tenant_id (filtering by project_id won't work with # Keystone V2) sg_list = self.operator_cloud.list_security_groups( - filters={'tenant_id': self.demo_cloud.current_project_id}) + filters={'tenant_id': self.user_cloud.current_project_id}) self.assertIn(sg1['id'], [sg['id'] for sg in sg_list]) self.assertNotIn(sg2['id'], [sg['id'] for sg in sg_list]) diff --git a/shade/tests/functional/test_server_group.py b/shade/tests/functional/test_server_group.py index b92914472..e4e7f8ecf 100644 --- a/shade/tests/functional/test_server_group.py +++ b/shade/tests/functional/test_server_group.py @@ -25,16 +25,16 @@ class TestServerGroup(base.BaseFunctionalTestCase): def test_server_group(self): server_group_name = self.getUniqueString() self.addCleanup(self.cleanup, server_group_name) - server_group = self.demo_cloud.create_server_group( + server_group = self.user_cloud.create_server_group( server_group_name, ['affinity']) server_group_ids = [v['id'] - for v in self.demo_cloud.list_server_groups()] + for v in self.user_cloud.list_server_groups()] self.assertIn(server_group['id'], server_group_ids) - self.demo_cloud.delete_server_group(server_group_name) + self.user_cloud.delete_server_group(server_group_name) def cleanup(self, server_group_name): - server_group = self.demo_cloud.get_server_group(server_group_name) + server_group = self.user_cloud.get_server_group(server_group_name) if server_group: - self.demo_cloud.delete_server_group(server_group['id']) + self.user_cloud.delete_server_group(server_group['id']) diff --git a/shade/tests/functional/test_stack.py b/shade/tests/functional/test_stack.py index a711c54a3..739d05f84 100644 --- a/shade/tests/functional/test_stack.py +++ b/shade/tests/functional/test_stack.py @@ -75,12 +75,12 @@ class TestStack(base.BaseFunctionalTestCase): def setUp(self): super(TestStack, self).setUp() - if not self.demo_cloud.has_service('orchestration'): + if not self.user_cloud.has_service('orchestration'): self.skipTest('Orchestration service not supported by cloud') def _cleanup_stack(self): - self.demo_cloud.delete_stack(self.stack_name, wait=True) - self.assertIsNone(self.demo_cloud.get_stack(self.stack_name)) + self.user_cloud.delete_stack(self.stack_name, wait=True) + self.assertIsNone(self.user_cloud.get_stack(self.stack_name)) def test_stack_validation(self): test_template = tempfile.NamedTemporaryFile(delete=False) @@ -88,7 +88,7 @@ def test_stack_validation(self): test_template.close() stack_name = self.getUniqueString('validate_template') self.assertRaises(exc.OpenStackCloudException, - self.demo_cloud.create_stack, + self.user_cloud.create_stack, name=stack_name, template_file=test_template.name) @@ -98,7 +98,7 @@ def test_stack_simple(self): test_template.close() self.stack_name = self.getUniqueString('simple_stack') self.addCleanup(self._cleanup_stack) - stack = self.demo_cloud.create_stack( + stack = self.user_cloud.create_stack( name=self.stack_name, template_file=test_template.name, wait=True) @@ -109,17 +109,17 @@ def test_stack_simple(self): self.assertEqual(10, len(rand)) # assert get_stack matches returned create_stack - stack = self.demo_cloud.get_stack(self.stack_name) + stack = self.user_cloud.get_stack(self.stack_name) self.assertEqual('CREATE_COMPLETE', stack['stack_status']) self.assertEqual(rand, stack['outputs'][0]['output_value']) # assert stack is in list_stacks - stacks = self.demo_cloud.list_stacks() + stacks = self.user_cloud.list_stacks() stack_ids = [s['id'] for s in stacks] self.assertIn(stack['id'], stack_ids) # update with no changes - stack = self.demo_cloud.update_stack( + stack = self.user_cloud.update_stack( self.stack_name, template_file=test_template.name, wait=True) @@ -130,14 +130,14 @@ def test_stack_simple(self): self.assertEqual(rand, stack['outputs'][0]['output_value']) # update with changes - stack = self.demo_cloud.update_stack( + stack = self.user_cloud.update_stack( self.stack_name, template_file=test_template.name, wait=True, length=12) # assert changed output in updated stack - stack = self.demo_cloud.get_stack(self.stack_name) + stack = self.user_cloud.get_stack(self.stack_name) self.assertEqual('UPDATE_COMPLETE', stack['stack_status']) new_rand = stack['outputs'][0]['output_value'] self.assertNotEqual(rand, new_rand) @@ -160,7 +160,7 @@ def test_stack_nested(self): self.stack_name = self.getUniqueString('nested_stack') self.addCleanup(self._cleanup_stack) - stack = self.demo_cloud.create_stack( + stack = self.user_cloud.create_stack( name=self.stack_name, template_file=test_template.name, environment_files=[env.name], diff --git a/shade/tests/functional/test_volume.py b/shade/tests/functional/test_volume.py index a625b7a7a..e3707139c 100644 --- a/shade/tests/functional/test_volume.py +++ b/shade/tests/functional/test_volume.py @@ -26,7 +26,7 @@ class TestVolume(base.BaseFunctionalTestCase): def setUp(self): super(TestVolume, self).setUp() - if not self.demo_cloud.has_service('volume'): + if not self.user_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') def test_volumes(self): @@ -35,26 +35,26 @@ def test_volumes(self): snapshot_name = self.getUniqueString() self.addDetail('volume', content.text_content(volume_name)) self.addCleanup(self.cleanup, volume_name, snapshot_name=snapshot_name) - volume = self.demo_cloud.create_volume( + volume = self.user_cloud.create_volume( display_name=volume_name, size=1) - snapshot = self.demo_cloud.create_volume_snapshot( + snapshot = self.user_cloud.create_volume_snapshot( volume['id'], display_name=snapshot_name ) - volume_ids = [v['id'] for v in self.demo_cloud.list_volumes()] + volume_ids = [v['id'] for v in self.user_cloud.list_volumes()] self.assertIn(volume['id'], volume_ids) - snapshot_list = self.demo_cloud.list_volume_snapshots() + snapshot_list = self.user_cloud.list_volume_snapshots() snapshot_ids = [s['id'] for s in snapshot_list] self.assertIn(snapshot['id'], snapshot_ids) - ret_snapshot = self.demo_cloud.get_volume_snapshot_by_id( + ret_snapshot = self.user_cloud.get_volume_snapshot_by_id( snapshot['id']) self.assertEqual(snapshot['id'], ret_snapshot['id']) - self.demo_cloud.delete_volume_snapshot(snapshot_name, wait=True) - self.demo_cloud.delete_volume(volume_name, wait=True) + self.user_cloud.delete_volume_snapshot(snapshot_name, wait=True) + self.user_cloud.delete_volume(volume_name, wait=True) def test_volume_to_image(self): '''Test volume export to image functionality''' @@ -62,32 +62,32 @@ def test_volume_to_image(self): image_name = self.getUniqueString() self.addDetail('volume', content.text_content(volume_name)) self.addCleanup(self.cleanup, volume_name, image_name=image_name) - volume = self.demo_cloud.create_volume( + volume = self.user_cloud.create_volume( display_name=volume_name, size=1) - image = self.demo_cloud.create_image( + image = self.user_cloud.create_image( image_name, volume=volume, wait=True) - volume_ids = [v['id'] for v in self.demo_cloud.list_volumes()] + volume_ids = [v['id'] for v in self.user_cloud.list_volumes()] self.assertIn(volume['id'], volume_ids) - image_list = self.demo_cloud.list_images() + image_list = self.user_cloud.list_images() image_ids = [s['id'] for s in image_list] self.assertIn(image['id'], image_ids) - self.demo_cloud.delete_image(image_name, wait=True) - self.demo_cloud.delete_volume(volume_name, wait=True) + self.user_cloud.delete_image(image_name, wait=True) + self.user_cloud.delete_volume(volume_name, wait=True) def cleanup(self, volume_name, snapshot_name=None, image_name=None): # Need to delete snapshots before volumes if snapshot_name: - snapshot = self.demo_cloud.get_volume_snapshot(snapshot_name) + snapshot = self.user_cloud.get_volume_snapshot(snapshot_name) if snapshot: - self.demo_cloud.delete_volume_snapshot( + self.user_cloud.delete_volume_snapshot( snapshot_name, wait=True) if image_name: - image = self.demo_cloud.get_image(image_name) + image = self.user_cloud.get_image(image_name) if image: - self.demo_cloud.delete_image(image_name, wait=True) - volume = self.demo_cloud.get_volume(volume_name) + self.user_cloud.delete_image(image_name, wait=True) + volume = self.user_cloud.get_volume(volume_name) if volume: - self.demo_cloud.delete_volume(volume_name, wait=True) + self.user_cloud.delete_volume(volume_name, wait=True) diff --git a/shade/tests/functional/test_volume_backup.py b/shade/tests/functional/test_volume_backup.py index 2efe94350..3a7d4bff9 100644 --- a/shade/tests/functional/test_volume_backup.py +++ b/shade/tests/functional/test_volume_backup.py @@ -18,56 +18,56 @@ class TestVolume(base.BaseFunctionalTestCase): def setUp(self): super(TestVolume, self).setUp() - if not self.demo_cloud.has_service('volume'): + if not self.user_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') - if not self.demo_cloud.has_service('object-store'): + if not self.user_cloud.has_service('object-store'): self.skipTest('volume backups require swift') def test_create_get_delete_volume_backup(self): - volume = self.demo_cloud.create_volume( + volume = self.user_cloud.create_volume( display_name=self.getUniqueString(), size=1) - self.addCleanup(self.demo_cloud.delete_volume, volume['id']) + self.addCleanup(self.user_cloud.delete_volume, volume['id']) backup_name_1 = self.getUniqueString() backup_desc_1 = self.getUniqueString() - backup = self.demo_cloud.create_volume_backup( + backup = self.user_cloud.create_volume_backup( volume_id=volume['id'], name=backup_name_1, description=backup_desc_1, wait=True) self.assertEqual(backup_name_1, backup['name']) - backup = self.demo_cloud.get_volume_backup(backup['id']) + backup = self.user_cloud.get_volume_backup(backup['id']) self.assertEqual("available", backup['status']) self.assertEqual(backup_desc_1, backup['description']) - self.demo_cloud.delete_volume_backup(backup['id'], wait=True) - self.assertIsNone(self.demo_cloud.get_volume_backup(backup['id'])) + self.user_cloud.delete_volume_backup(backup['id'], wait=True) + self.assertIsNone(self.user_cloud.get_volume_backup(backup['id'])) def test_list_volume_backups(self): - vol1 = self.demo_cloud.create_volume( + vol1 = self.user_cloud.create_volume( display_name=self.getUniqueString(), size=1) - self.addCleanup(self.demo_cloud.delete_volume, vol1['id']) + self.addCleanup(self.user_cloud.delete_volume, vol1['id']) # We create 2 volumes to create 2 backups. We could have created 2 # backups from the same volume but taking 2 successive backups seems # to be race-condition prone. And I didn't want to use an ugly sleep() # here. - vol2 = self.demo_cloud.create_volume( + vol2 = self.user_cloud.create_volume( display_name=self.getUniqueString(), size=1) - self.addCleanup(self.demo_cloud.delete_volume, vol2['id']) + self.addCleanup(self.user_cloud.delete_volume, vol2['id']) backup_name_1 = self.getUniqueString() - backup = self.demo_cloud.create_volume_backup( + backup = self.user_cloud.create_volume_backup( volume_id=vol1['id'], name=backup_name_1) - self.addCleanup(self.demo_cloud.delete_volume_backup, backup['id']) + self.addCleanup(self.user_cloud.delete_volume_backup, backup['id']) - backup = self.demo_cloud.create_volume_backup(volume_id=vol2['id']) - self.addCleanup(self.demo_cloud.delete_volume_backup, backup['id']) + backup = self.user_cloud.create_volume_backup(volume_id=vol2['id']) + self.addCleanup(self.user_cloud.delete_volume_backup, backup['id']) - backups = self.demo_cloud.list_volume_backups() + backups = self.user_cloud.list_volume_backups() self.assertEqual(2, len(backups)) - backups = self.demo_cloud.list_volume_backups( + backups = self.user_cloud.list_volume_backups( search_opts={"name": backup_name_1}) self.assertEqual(1, len(backups)) self.assertEqual(backup_name_1, backups[0]['name']) diff --git a/shade/tests/functional/test_volume_type.py b/shade/tests/functional/test_volume_type.py index bb3226fc0..b04284282 100644 --- a/shade/tests/functional/test_volume_type.py +++ b/shade/tests/functional/test_volume_type.py @@ -32,7 +32,7 @@ def _assert_project(self, volume_name_or_id, project_id, allowed=True): def setUp(self): super(TestVolumeType, self).setUp() - if not self.demo_cloud.has_service('volume'): + if not self.user_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') self.operator_cloud.cinder_client.volume_types.create( 'test-volume-type', is_public=False) diff --git a/shade/tests/functional/test_zone.py b/shade/tests/functional/test_zone.py index 80ca09130..3a90d9b78 100644 --- a/shade/tests/functional/test_zone.py +++ b/shade/tests/functional/test_zone.py @@ -26,7 +26,7 @@ class TestZone(base.BaseFunctionalTestCase): def setUp(self): super(TestZone, self).setUp() - if not self.demo_cloud.has_service('dns'): + if not self.user_cloud.has_service('dns'): self.skipTest('dns service not supported by cloud') def test_zones(self): @@ -42,7 +42,7 @@ def test_zones(self): self.addCleanup(self.cleanup, name) # Test we can create a zone and we get it returned - zone = self.demo_cloud.create_zone( + zone = self.user_cloud.create_zone( name=name, zone_type=zone_type, email=email, description=description, ttl=ttl, masters=masters) @@ -54,20 +54,20 @@ def test_zones(self): self.assertEqual(zone['masters'], []) # Test that we can list zones - zones = self.demo_cloud.list_zones() + zones = self.user_cloud.list_zones() self.assertIsNotNone(zones) # Test we get the same zone with the get_zone method - zone_get = self.demo_cloud.get_zone(zone['id']) + zone_get = self.user_cloud.get_zone(zone['id']) self.assertEqual(zone_get['id'], zone['id']) # Test the get method also works by name - zone_get = self.demo_cloud.get_zone(name) + zone_get = self.user_cloud.get_zone(name) self.assertEqual(zone_get['name'], zone['name']) # Test we can update a field on the zone and only that field # is updated - zone_update = self.demo_cloud.update_zone(zone['id'], ttl=7200) + zone_update = self.user_cloud.update_zone(zone['id'], ttl=7200) self.assertEqual(zone_update['id'], zone['id']) self.assertEqual(zone_update['name'], zone['name']) self.assertEqual(zone_update['type'], zone['type']) @@ -77,8 +77,8 @@ def test_zones(self): self.assertEqual(zone_update['masters'], zone['masters']) # Test we can delete and get True returned - zone_delete = self.demo_cloud.delete_zone(zone['id']) + zone_delete = self.user_cloud.delete_zone(zone['id']) self.assertTrue(zone_delete) def cleanup(self, name): - self.demo_cloud.delete_zone(name) + self.user_cloud.delete_zone(name) From 76a01bf851c437698d03dc246a28b7ebf5ff5fe2 Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Sun, 12 Feb 2017 14:19:23 -0800 Subject: [PATCH 1265/3836] Remove mock of keystone where single projects are consumed Any case that mocked keystoneclient for simple get of a single project has been updated to now use requests mock. This change required some minor adjusting to the base RequestMock test case to make it easy to mock out for projects without needing to duplicate a ton of code. Change-Id: Ic19ec7ea199759a0aa69d107636fadbaf27dae5e --- shade/tests/unit/base.py | 87 ++++++++++++++++++++- shade/tests/unit/test_limits.py | 17 +++-- shade/tests/unit/test_project.py | 36 --------- shade/tests/unit/test_quotas.py | 125 +++++++++++++++++-------------- shade/tests/unit/test_usage.py | 18 ++--- 5 files changed, 173 insertions(+), 110 deletions(-) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 788bb4b4e..cc1e40d81 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -29,6 +29,12 @@ from shade.tests import base +_ProjectData = collections.namedtuple( + 'ProjectData', + 'project_id, project_name, enabled, domain_id, description, ' + 'json_response, json_request') + + _UserData = collections.namedtuple( 'UserData', 'user_id, password, name, email, description, domain_id, enabled, ' @@ -114,8 +120,15 @@ def setUp(self, cloud_config_fixture='clouds.yaml'): super(RequestsMockTestCase, self).setUp( cloud_config_fixture=cloud_config_fixture) + # FIXME(notmorgan): Convert the uri_registry, discovery.json, and + # use of keystone_v3/v2 to a proper fixtures.Fixture. For now this + # is acceptable, but eventually this should become it's own fixture + # that encapsulates the registry, registering the URIs, and + # assert_calls (and calling assert_calls every test case that uses + # it on cleanup). Subclassing here could be 100% eliminated in the + # future allowing any class to simply + # self.useFixture(shade.RequestsMockFixture) and get all the benefits. self._uri_registry = {} - self.discovery_json = os.path.join( self.fixtures_directory, 'discovery.json') self.use_keystone_v3() @@ -135,6 +148,78 @@ def get_mock_url(self, service_type, interface, resource=None, to_join.extend(append or []) return '/'.join(to_join) + def mock_for_keystone_projects(self, project=None, v3=True, + list_get=False, id_get=False, + project_list=None, project_count=None): + if project: + assert not (project_list or project_count) + elif project_list: + assert not (project or project_count) + elif project_count: + assert not (project or project_list) + else: + raise Exception('Must specify a project, project_list, ' + 'or project_count') + assert list_get or id_get + + base_url_append = 'v3' if v3 else None + if project: + project_list = [project] + elif project_count: + # Generate multiple projects + project_list = [self._get_project_data(v3=v3) + for c in range(0, project_count)] + if list_get: + self.register_uri( + 'GET', + self.get_mock_url( + service_type='identity', + interface='admin', + resource='projects', + base_url_append=base_url_append), + status_code=200, + json={'projects': [p.json_response['project'] + for p in project_list]}) + if id_get: + for p in project_list: + self.register_uri( + 'GET', + self.get_mock_url( + service_type='identity', + interface='admin', + resource='projects', + append=[p.project_id], + base_url_append=base_url_append), + status_code=200, + json=p.json_response) + return project_list + + def _get_project_data(self, project_name=None, enabled=None, + description=None, v3=True): + project_name = project_name or self.getUniqueString('projectName') + project_id = uuid.uuid4().hex + response = {'id': project_id, 'name': project_name} + request = {'name': project_name} + domain_id = uuid.uuid4().hex if v3 else None + if domain_id: + request['domain_id'] = domain_id + response['domain_id'] = domain_id + if enabled is not None: + enabled = bool(enabled) + response['enabled'] = enabled + request['enabled'] = enabled + response.setdefault('enabled', True) + if description: + response['description'] = description + request['description'] = description + if v3: + project_key = 'project' + else: + project_key = 'tenant' + return _ProjectData(project_id, project_name, enabled, domain_id, + description, {project_key: response}, + {project_key: request}) + def _get_group_data(self, name=None, domain_id=None, description=None): group_id = uuid.uuid4().hex name or self.getUniqueString('groupname') diff --git a/shade/tests/unit/test_limits.py b/shade/tests/unit/test_limits.py index 551f47d2e..8ccb3adbd 100644 --- a/shade/tests/unit/test_limits.py +++ b/shade/tests/unit/test_limits.py @@ -13,10 +13,9 @@ import shade from shade.tests.unit import base -from shade.tests import fakes -class TestLimits(base.TestCase): +class TestLimits(base.RequestsMockTestCase): @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_get_compute_limits(self, mock_nova): @@ -25,10 +24,12 @@ def test_get_compute_limits(self, mock_nova): mock_nova.limits.get.assert_called_once_with() @mock.patch.object(shade.OpenStackCloud, 'nova_client') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_other_get_compute_limits(self, mock_keystone, mock_nova): - project = fakes.FakeProject('project_a') - mock_keystone.tenants.list.return_value = [project] - self.op_cloud.get_compute_limits(project) + def test_other_get_compute_limits(self, mock_nova): + self._add_discovery_uri_call() + project = self.mock_for_keystone_projects(project_count=1, + list_get=True)[0] + self.op_cloud.get_compute_limits(project.project_id) - mock_nova.limits.get.assert_called_once_with(tenant_id='project_a') + mock_nova.limits.get.assert_called_once_with( + tenant_id=project.project_id) + self.assert_calls() diff --git a/shade/tests/unit/test_project.py b/shade/tests/unit/test_project.py index fbdc5ebb6..d6d206cf5 100644 --- a/shade/tests/unit/test_project.py +++ b/shade/tests/unit/test_project.py @@ -10,9 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import collections -import uuid - import testtools from testtools import matchers @@ -21,12 +18,6 @@ from shade.tests.unit import base -_ProjectData = collections.namedtuple( - 'ProjectData', - 'project_id, project_name, enabled, domain_id, description, ' - 'json_response, json_request') - - class TestProject(base.RequestsMockTestCase): def get_mock_url(self, service_type='identity', interface='admin', @@ -42,33 +33,6 @@ def get_mock_url(self, service_type='identity', interface='admin', service_type=service_type, interface=interface, resource=resource, append=append, base_url_append=base_url_append) - def _get_project_data(self, project_name=None, enabled=None, - description=None, v3=True): - project_name = project_name or self.getUniqueString('projectName') - project_id = uuid.uuid4().hex - response = {'id': project_id, 'name': project_name} - request = {'name': project_name} - domain_id = None - if v3: - domain_id = uuid.uuid4().hex - request['domain_id'] = domain_id - response['domain_id'] = domain_id - if enabled is not None: - enabled = bool(enabled) - response['enabled'] = enabled - request['enabled'] = enabled - response.setdefault('enabled', True) - if description: - response['description'] = description - request['description'] = description - if v3: - project_key = 'project' - else: - project_key = 'tenant' - return _ProjectData(project_id, project_name, enabled, domain_id, - description, {project_key: response}, - {project_key: request}) - def test_create_project_v2(self): self.use_keystone_v2() project_data = self._get_project_data(v3=False) diff --git a/shade/tests/unit/test_quotas.py b/shade/tests/unit/test_quotas.py index b7050dfa8..91d87ea44 100644 --- a/shade/tests/unit/test_quotas.py +++ b/shade/tests/unit/test_quotas.py @@ -17,102 +17,115 @@ import shade from shade import exc from shade.tests.unit import base -from shade.tests import fakes -class TestQuotas(base.TestCase): +class TestQuotas(base.RequestsMockTestCase): + def setUp(self, cloud_config_fixture='clouds.yaml'): + super(TestQuotas, self).setUp( + cloud_config_fixture=cloud_config_fixture) + self._add_discovery_uri_call() @mock.patch.object(shade.OpenStackCloud, 'nova_client') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_update_quotas(self, mock_keystone, mock_nova): - project = fakes.FakeProject('project_a') - mock_keystone.tenants.list.return_value = [project] - self.op_cloud.set_compute_quotas(project, cores=1) + def test_update_quotas(self, mock_nova): + project = self.mock_for_keystone_projects(project_count=1, + list_get=True)[0] + # re-mock the list-get as the call to set_compute_quotas when + # bad-request is raised, still calls out to get the project data. + self.mock_for_keystone_projects(project=project, list_get=True) + + self.op_cloud.set_compute_quotas(project.project_id, cores=1) mock_nova.quotas.update.assert_called_once_with( - cores=1, force=True, tenant_id='project_a') + cores=1, force=True, tenant_id=project.project_id) mock_nova.quotas.update.side_effect = nova_exceptions.BadRequest(400) self.assertRaises(exc.OpenStackCloudException, self.op_cloud.set_compute_quotas, project) + self.assert_calls() @mock.patch.object(shade.OpenStackCloud, 'nova_client') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_get_quotas(self, mock_keystone, mock_nova): - project = fakes.FakeProject('project_a') - mock_keystone.tenants.list.return_value = [project] - self.op_cloud.get_compute_quotas(project) + def test_get_quotas(self, mock_nova): + project = self.mock_for_keystone_projects(project_count=1, + list_get=True)[0] + self.op_cloud.get_compute_quotas(project.project_id) - mock_nova.quotas.get.assert_called_once_with(tenant_id='project_a') + mock_nova.quotas.get.assert_called_once_with( + tenant_id=project.project_id) @mock.patch.object(shade.OpenStackCloud, 'nova_client') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_delete_quotas(self, mock_keystone, mock_nova): - project = fakes.FakeProject('project_a') - mock_keystone.tenants.list.return_value = [project] - self.op_cloud.delete_compute_quotas(project) + def test_delete_quotas(self, mock_nova): + project = self.mock_for_keystone_projects(project_count=1, + list_get=True)[0] + # re-mock the list-get as the call to set_delete_compute_quotas when + # bad-request is raised, still calls out to get the project data. + self.mock_for_keystone_projects(project=project, list_get=True) + + self.op_cloud.delete_compute_quotas(project.project_id) - mock_nova.quotas.delete.assert_called_once_with(tenant_id='project_a') + mock_nova.quotas.delete.assert_called_once_with( + tenant_id=project.project_id) mock_nova.quotas.delete.side_effect = nova_exceptions.BadRequest(400) self.assertRaises(exc.OpenStackCloudException, self.op_cloud.delete_compute_quotas, project) + self.assert_calls() @mock.patch.object(shade.OpenStackCloud, 'cinder_client') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_cinder_update_quotas(self, mock_keystone, mock_cinder): - project = fakes.FakeProject('project_a') - mock_keystone.tenants.list.return_value = [project] - self.op_cloud.set_volume_quotas(project, volumes=1) + def test_cinder_update_quotas(self, mock_cinder): + project = self.mock_for_keystone_projects(project_count=1, + list_get=True)[0] + self.op_cloud.set_volume_quotas(project.project_id, volumes=1) mock_cinder.quotas.update.assert_called_once_with( - volumes=1, tenant_id='project_a') + volumes=1, tenant_id=project.project_id) + self.assert_calls() @mock.patch.object(shade.OpenStackCloud, 'cinder_client') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_cinder_get_quotas(self, mock_keystone, mock_cinder): - project = fakes.FakeProject('project_a') - mock_keystone.tenants.list.return_value = [project] - self.op_cloud.get_volume_quotas(project) + def test_cinder_get_quotas(self, mock_cinder): + project = self.mock_for_keystone_projects(project_count=1, + list_get=True)[0] + self.op_cloud.get_volume_quotas(project.project_id) - mock_cinder.quotas.get.assert_called_once_with(tenant_id='project_a') + mock_cinder.quotas.get.assert_called_once_with( + tenant_id=project.project_id) + self.assert_calls() @mock.patch.object(shade.OpenStackCloud, 'cinder_client') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_cinder_delete_quotas(self, mock_keystone, mock_cinder): - project = fakes.FakeProject('project_a') - mock_keystone.tenants.list.return_value = [project] - self.op_cloud.delete_volume_quotas(project) + def test_cinder_delete_quotas(self, mock_cinder): + project = self.mock_for_keystone_projects(project_count=1, + list_get=True)[0] + self.op_cloud.delete_volume_quotas(project.project_id) mock_cinder.quotas.delete.assert_called_once_with( - tenant_id='project_a') + tenant_id=project.project_id) + self.assert_calls() @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_neutron_update_quotas(self, mock_keystone, mock_neutron): - project = fakes.FakeProject('project_a') - mock_keystone.tenants.list.return_value = [project] - self.op_cloud.set_network_quotas(project, network=1) + def test_neutron_update_quotas(self, mock_neutron): + project = self.mock_for_keystone_projects(project_count=1, + list_get=True)[0] + self.op_cloud.set_network_quotas(project.project_id, network=1) mock_neutron.update_quota.assert_called_once_with( - body={'quota': {'network': 1}}, tenant_id='project_a') + body={'quota': {'network': 1}}, tenant_id=project.project_id) + self.assert_calls() @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_neutron_get_quotas(self, mock_keystone, mock_neutron): - project = fakes.FakeProject('project_a') - mock_keystone.tenants.list.return_value = [project] - self.op_cloud.get_network_quotas(project) + def test_neutron_get_quotas(self, mock_neutron): + project = self.mock_for_keystone_projects(project_count=1, + list_get=True)[0] + self.op_cloud.get_network_quotas(project.project_id) mock_neutron.show_quota.assert_called_once_with( - tenant_id='project_a') + tenant_id=project.project_id) + self.assert_calls() @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_neutron_delete_quotas(self, mock_keystone, mock_neutron): - project = fakes.FakeProject('project_a') - mock_keystone.tenants.list.return_value = [project] - self.op_cloud.delete_network_quotas(project) + def test_neutron_delete_quotas(self, mock_neutron): + project = self.mock_for_keystone_projects(project_count=1, + list_get=True)[0] + self.op_cloud.delete_network_quotas(project.project_id) mock_neutron.delete_quota.assert_called_once_with( - tenant_id='project_a') + tenant_id=project.project_id) + self.assert_calls() diff --git a/shade/tests/unit/test_usage.py b/shade/tests/unit/test_usage.py index c0366acb0..10948e14f 100644 --- a/shade/tests/unit/test_usage.py +++ b/shade/tests/unit/test_usage.py @@ -16,18 +16,18 @@ import shade from shade.tests.unit import base -from shade.tests import fakes -class TestUsage(base.TestCase): +class TestUsage(base.RequestsMockTestCase): @mock.patch.object(shade.OpenStackCloud, 'nova_client') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_get_usage(self, mock_keystone, mock_nova): - project = fakes.FakeProject('project_a') + def test_get_usage(self, mock_nova): + self._add_discovery_uri_call() + project = self.mock_for_keystone_projects(project_count=1, + list_get=True)[0] start = end = datetime.datetime.now() - mock_keystone.tenants.list.return_value = [project] - self.op_cloud.get_compute_usage(project, start, end) + self.op_cloud.get_compute_usage(project.project_id, start, end) - mock_nova.usage.get.assert_called_once_with(start=start, end=end, - tenant_id='project_a') + mock_nova.usage.get.assert_called_once_with( + start=start, end=end, tenant_id=project.project_id) + self.assert_calls() From aff1f8f8b6868960d5fd02923d105b5c977156b2 Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Mon, 13 Feb 2017 11:14:21 -0800 Subject: [PATCH 1266/3836] Remove mock of keystoneclient for test_caching for projects Update test_caching to use requests_mock instead of direct mocking of the keystoneclient when calling keystoneclient to get projects. Change-Id: I865d2320c28c52318c243c9a927baa71324a7311 --- shade/tests/unit/test_caching.py | 90 +++++++++++++++++++++++++------- 1 file changed, 70 insertions(+), 20 deletions(-) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 611e55ceb..885c8d9ed 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -106,43 +106,93 @@ def _munch_images(self, fake_image): def test_openstack_cloud(self): self.assertIsInstance(self.cloud, shade.OpenStackCloud) - @mock.patch('shade.OpenStackCloud.keystone_client') - def test_list_projects_v3(self, keystone_mock): - project = fakes.FakeProject('project_a') - keystone_mock.projects.list.return_value = [project] - self.cloud.cloud_config.config['identity_api_version'] = '3' + def test_list_projects_v3(self): + self._add_discovery_uri_call() + project_one = self._get_project_data() + project_two = self._get_project_data() + project_list = [project_one, project_two] + + first_response = {'projects': [project_one.json_response['project']]} + second_response = {'projects': [p.json_response['project'] + for p in project_list]} + + self.register_uri( + 'GET', + self.get_mock_url( + service_type='identity', + interface='admin', + resource='projects', + base_url_append='v3'), + status_code=200, + json=first_response) + self.register_uri( + 'GET', + self.get_mock_url( + service_type='identity', + interface='admin', + resource='projects', + base_url_append='v3'), + status_code=200, + json=second_response) self.assertEqual( - self.cloud._normalize_projects(meta.obj_list_to_dict([project])), + self.cloud._normalize_projects( + meta.obj_list_to_dict(first_response['projects'])), self.cloud.list_projects()) - project_b = fakes.FakeProject('project_b') - keystone_mock.projects.list.return_value = [project, project_b] self.assertEqual( - self.cloud._normalize_projects(meta.obj_list_to_dict([project])), + self.cloud._normalize_projects( + meta.obj_list_to_dict(first_response['projects'])), self.cloud.list_projects()) + # invalidate the list_projects cache self.cloud.list_projects.invalidate(self.cloud) + # ensure the new values are now retrieved self.assertEqual( self.cloud._normalize_projects( - meta.obj_list_to_dict([project, project_b])), + meta.obj_list_to_dict(second_response['projects'])), self.cloud.list_projects()) + self.assert_calls() - @mock.patch('shade.OpenStackCloud.keystone_client') - def test_list_projects_v2(self, keystone_mock): - project = fakes.FakeProject('project_a') - keystone_mock.tenants.list.return_value = [project] - self.cloud.cloud_config.config['identity_api_version'] = '2' + def test_list_projects_v2(self): + self.use_keystone_v2() + project_one = self._get_project_data(v3=False) + project_two = self._get_project_data(v3=False) + project_list = [project_one, project_two] + + first_response = {'tenants': [project_one.json_response['tenant']]} + second_response = {'tenants': [p.json_response['tenant'] + for p in project_list]} + + self.register_uri( + 'GET', + self.get_mock_url( + service_type='identity', + interface='admin', + resource='tenants'), + status_code=200, + json=first_response) + self.register_uri( + 'GET', + self.get_mock_url( + service_type='identity', + interface='admin', + resource='tenants'), + status_code=200, + json=second_response) self.assertEqual( - self.cloud._normalize_projects(meta.obj_list_to_dict([project])), + self.cloud._normalize_projects( + meta.obj_list_to_dict(first_response['tenants'])), self.cloud.list_projects()) - project_b = fakes.FakeProject('project_b') - keystone_mock.tenants.list.return_value = [project, project_b] self.assertEqual( - self.cloud._normalize_projects(meta.obj_list_to_dict([project])), + self.cloud._normalize_projects( + meta.obj_list_to_dict(first_response['tenants'])), self.cloud.list_projects()) + # invalidate the list_projects cache self.cloud.list_projects.invalidate(self.cloud) + # ensure the new values are now retrieved self.assertEqual( self.cloud._normalize_projects( - meta.obj_list_to_dict([project, project_b])), + meta.obj_list_to_dict(second_response['tenants'])), self.cloud.list_projects()) + self.assert_calls() @mock.patch('shade.OpenStackCloud.nova_client') def test_list_servers_no_herd(self, nova_mock): From ea83e73be05a47ea93ac1d8428c033897e480d20 Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Mon, 13 Feb 2017 11:55:08 -0800 Subject: [PATCH 1267/3836] Remove keystoneclient mocks in test_caching for users Remove the direct keystoneclient mocks in test_caching for user cases. This implements "register_uris" which takes a list of dictionaries describing the mocked URIs for ease of reading when dealing with highly complex request mocking such as the test_modify_user_invalidates_cache test case. Change-Id: I75f62a7b2c6d0838dd4a4bdf10cb74e4baf4aa7e --- shade/tests/unit/base.py | 55 ++++++++++++ shade/tests/unit/test_caching.py | 140 ++++++++++++++++++++++--------- 2 files changed, 155 insertions(+), 40 deletions(-) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index cc1e40d81..6d5345e62 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -132,6 +132,7 @@ def setUp(self, cloud_config_fixture='clouds.yaml'): self.discovery_json = os.path.join( self.fixtures_directory, 'discovery.json') self.use_keystone_v3() + self.__register_uris_called = False def get_mock_url(self, service_type, interface, resource=None, append=None, base_url_append=None): @@ -327,6 +328,60 @@ def use_glance(self, image_version_json='image-version.json'): 'GET', 'https://image.example.com/', text=open(discovery_fixture, 'r').read()) + def register_uris(self, uri_mock_list): + """Mock a list of URIs and responses via requests mock. + + This method may be called only once per test-case to avoid odd + and difficult to debug interactions. Discovery and Auth request mocking + happens separately from this method. + + :param uri_mock_list: List of dictionaries that template out what is + passed to requests_mock fixture's `register_uri`. + Format is: + {'method': , + 'uri': , + ... + } + + Common keys to pass in the dictionary: + * json: the json response (dict) + * status_code: the HTTP status (int) + * validate: The request body (dict) to + validate with assert_calls + all key-word arguments that are valid to send to + requests_mock are supported. + + This list should be in the order in which calls + are made. When `assert_calls` is executed, order + here will be validated. Duplicate URIs and + Methods are allowed and will be collapsed into a + single matcher. Each response will be returned + in order as the URI+Method is hit. + :return: None + """ + assert not self.__register_uris_called + for to_mock in uri_mock_list: + method = to_mock.pop('method') + uri = to_mock.pop('uri') + key = '{method}:{uri}'.format(method=method, uri=uri) + headers = structures.CaseInsensitiveDict(to_mock.pop('headers', + {})) + validate = to_mock.pop('validate', {}) + if 'content-type' not in headers: + headers[u'content-type'] = 'application/json' + + self.calls += [ + dict( + method=method, + url=uri, **validate) + ] + self._uri_registry.setdefault(key, []).append(to_mock) + + for mock_method_uri, params in self._uri_registry.items(): + mock_method, mock_uri = mock_method_uri.split(':', 1) + self.adapter.register_uri(mock_method, mock_uri, params) + self.__register_uris_called = True + def register_uri(self, method, uri, **kwargs): validate = kwargs.pop('validate', {}) key = '{method}:{uri}'.format(method=method, uri=uri) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 885c8d9ed..dd1795624 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -293,61 +293,121 @@ def now_gone(): self.cloud.delete_volume('12345') self.assertEqual([fake_volb4_dict], self.cloud.list_volumes()) - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_list_users(self, keystone_mock): - fake_user = fakes.FakeUser('999', '', '') - keystone_mock.users.list.return_value = [fake_user] + def test_list_users(self): + self._add_discovery_uri_call() + user_data = self._get_user_data(email='test@example.com') + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + service_type='identity', + interface='admin', + resource='users', + base_url_append='v3'), + status_code=200, + json={'users': [user_data.json_response['user']]})]) users = self.cloud.list_users() self.assertEqual(1, len(users)) - self.assertEqual('999', users[0]['id']) - self.assertEqual('', users[0]['name']) - self.assertEqual('', users[0]['email']) + self.assertEqual(user_data.user_id, users[0]['id']) + self.assertEqual(user_data.name, users[0]['name']) + self.assertEqual(user_data.email, users[0]['email']) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_modify_user_invalidates_cache(self, keystone_mock): + def test_modify_user_invalidates_cache(self): self.use_keystone_v2() - fake_user = fakes.FakeUser('abc123', 'abc123@domain.test', - 'abc123 name') + + user_data = self._get_user_data(email='test@example.com') + new_resp = {'user': user_data.json_response['user'].copy()} + new_resp['user']['email'] = 'Nope@Nope.Nope' + new_req = {'user': {'email': new_resp['user']['email']}} + + mock_users_url = self.get_mock_url( + service_type='identity', + interface='admin', + resource='users') + mock_user_resource_url = self.get_mock_url( + service_type='identity', + interface='admin', + resource='users', + append=[user_data.user_id]) + + empty_user_list_resp = {'users': []} + users_list_resp = {'users': [user_data.json_response['user']]} + updated_users_list_resp = {'users': [new_resp['user']]} + + uris_to_mock = [ + # Inital User List is Empty + dict(method='GET', uri=mock_users_url, status_code=200, + json=empty_user_list_resp), + # POST to create the user + # GET to get the user data after POST + dict(method='POST', uri=mock_users_url, status_code=200, + json=user_data.json_response, + validate=user_data.json_request), + dict(method='GET', uri=mock_user_resource_url, status_code=200, + json=user_data.json_response), + # List Users Call + dict(method='GET', uri=mock_users_url, status_code=200, + json=users_list_resp), + # List users to get ID for update + # Get user using user_id from list + # Update user + # Get updated user + dict(method='GET', uri=mock_users_url, status_code=200, + json=users_list_resp), + dict(method='GET', uri=mock_user_resource_url, status_code=200, + json=user_data.json_response), + dict(method='PUT', uri=mock_user_resource_url, status_code=200, + json=new_resp, validate=new_req), + dict(method='GET', uri=mock_user_resource_url, status_code=200, + json=new_resp), + # List Users Call + dict(method='GET', uri=mock_users_url, status_code=200, + json=updated_users_list_resp), + # List User to get ID for delete + # Get user using user_id from list + # delete user + dict(method='GET', uri=mock_users_url, status_code=200, + json=updated_users_list_resp), + dict(method='GET', uri=mock_user_resource_url, status_code=200, + json=new_resp), + dict(method='DELETE', uri=mock_user_resource_url, status_code=204), + # List Users Call (empty post delete) + dict(method='GET', uri=mock_users_url, status_code=200, + json=empty_user_list_resp) + ] + + self.register_uris(uris_to_mock) + # first cache an empty list - keystone_mock.users.list.return_value = [] self.assertEqual([], self.cloud.list_users()) - # now add one - keystone_mock.users.list.return_value = [fake_user] - keystone_mock.users.create.return_value = fake_user - created = self.cloud.create_user(name='abc123 name', - email='abc123@domain.test') - self.assertEqual('abc123', created['id']) - self.assertEqual('abc123 name', created['name']) - self.assertEqual('abc123@domain.test', created['email']) + # now add one + created = self.cloud.create_user(name=user_data.name, + email=user_data.email) + self.assertEqual(user_data.user_id, created['id']) + self.assertEqual(user_data.name, created['name']) + self.assertEqual(user_data.email, created['email']) # Cache should have been invalidated users = self.cloud.list_users() - self.assertEqual(1, len(users)) - self.assertEqual('abc123', users[0]['id']) - self.assertEqual('abc123 name', users[0]['name']) - self.assertEqual('abc123@domain.test', users[0]['email']) + self.assertEqual(user_data.user_id, users[0]['id']) + self.assertEqual(user_data.name, users[0]['name']) + self.assertEqual(user_data.email, users[0]['email']) # Update and check to see if it is updated - fake_user2 = fakes.FakeUser('abc123', - 'abc123-changed@domain.test', - 'abc123 name') - fake_user2_dict = meta.obj_to_dict(fake_user2) - keystone_mock.users.update.return_value = fake_user2 - keystone_mock.users.list.return_value = [fake_user2] - keystone_mock.users.get.return_value = fake_user2_dict - self.cloud.update_user('abc123', email='abc123-changed@domain.test') - keystone_mock.users.update.assert_called_with( - user=fake_user2_dict, email='abc123-changed@domain.test') + updated = self.cloud.update_user(user_data.user_id, + email=new_resp['user']['email']) + self.assertEqual(user_data.user_id, updated.id) + self.assertEqual(user_data.name, updated.name) + self.assertEqual(new_resp['user']['email'], updated.email) users = self.cloud.list_users() self.assertEqual(1, len(users)) - self.assertEqual('abc123', users[0]['id']) - self.assertEqual('abc123 name', users[0]['name']) - self.assertEqual('abc123-changed@domain.test', users[0]['email']) + self.assertEqual(user_data.user_id, users[0]['id']) + self.assertEqual(user_data.name, users[0]['name']) + self.assertEqual(new_resp['user']['email'], users[0]['email']) # Now delete and ensure it disappears - keystone_mock.users.list.return_value = [] - self.cloud.delete_user('abc123') + self.cloud.delete_user(user_data.user_id) self.assertEqual([], self.cloud.list_users()) - self.assertTrue(keystone_mock.users.delete.was_called) + self.assert_calls() def test_list_flavors(self): self.register_uri( From 820885c1a164bf01365dd1bd57fddd58dc664e56 Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Mon, 13 Feb 2017 14:59:49 -0800 Subject: [PATCH 1268/3836] Convert use of .register_uri to .register_uris This patch converts all use of .register_uri to .register_uris for the following files: test_caching, test_create_server, test_domains, test_flavors, test_floating_ip_neutron, test_users. Change-Id: I48d90465f22bfa27e9de552e135f42967a9e21fa --- shade/tests/unit/base.py | 4 +- shade/tests/unit/test_caching.py | 83 ++- shade/tests/unit/test_create_server.py | 11 +- shade/tests/unit/test_domains.py | 176 +++--- shade/tests/unit/test_flavors.py | 252 ++++---- shade/tests/unit/test_floating_ip_neutron.py | 592 ++++++++++--------- shade/tests/unit/test_users.py | 240 ++++---- 7 files changed, 662 insertions(+), 696 deletions(-) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 6d5345e62..307c00346 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -364,12 +364,14 @@ def register_uris(self, uri_mock_list): method = to_mock.pop('method') uri = to_mock.pop('uri') key = '{method}:{uri}'.format(method=method, uri=uri) + validate = to_mock.pop('validate', {}) headers = structures.CaseInsensitiveDict(to_mock.pop('headers', {})) - validate = to_mock.pop('validate', {}) if 'content-type' not in headers: headers[u'content-type'] = 'application/json' + to_mock['headers'] = headers + self.calls += [ dict( method=method, diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index dd1795624..18f5cfb55 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -116,24 +116,16 @@ def test_list_projects_v3(self): second_response = {'projects': [p.json_response['project'] for p in project_list]} - self.register_uri( - 'GET', - self.get_mock_url( - service_type='identity', - interface='admin', - resource='projects', - base_url_append='v3'), - status_code=200, - json=first_response) - self.register_uri( - 'GET', - self.get_mock_url( - service_type='identity', - interface='admin', - resource='projects', - base_url_append='v3'), - status_code=200, - json=second_response) + mock_uri = self.get_mock_url( + service_type='identity', interface='admin', resource='projects', + base_url_append='v3') + + self.register_uris([ + dict(method='GET', uri=mock_uri, status_code=200, + json=first_response), + dict(method='GET', uri=mock_uri, status_code=200, + json=second_response)]) + self.assertEqual( self.cloud._normalize_projects( meta.obj_list_to_dict(first_response['projects'])), @@ -161,22 +153,15 @@ def test_list_projects_v2(self): second_response = {'tenants': [p.json_response['tenant'] for p in project_list]} - self.register_uri( - 'GET', - self.get_mock_url( - service_type='identity', - interface='admin', - resource='tenants'), - status_code=200, - json=first_response) - self.register_uri( - 'GET', - self.get_mock_url( - service_type='identity', - interface='admin', - resource='tenants'), - status_code=200, - json=second_response) + mock_uri = self.get_mock_url( + service_type='identity', interface='admin', resource='tenants') + + self.register_uris([ + dict(method='GET', uri=mock_uri, status_code=200, + json=first_response), + dict(method='GET', uri=mock_uri, status_code=200, + json=second_response)]) + self.assertEqual( self.cloud._normalize_projects( meta.obj_list_to_dict(first_response['tenants'])), @@ -410,20 +395,22 @@ def test_modify_user_invalidates_cache(self): self.assert_calls() def test_list_flavors(self): - self.register_uri( - 'GET', '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'flavors': []}) - - self.register_uri( - 'GET', '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'flavors': fakes.FAKE_FLAVOR_LIST}) - for flavor in fakes.FAKE_FLAVOR_LIST: - self.register_uri( - 'GET', '{endpoint}/flavors/{id}/os-extra_specs'.format( - endpoint=fakes.COMPUTE_ENDPOINT, id=flavor['id']), - json={'extra_specs': {}}) + mock_uri = '{endpoint}/flavors/detail?is_public=None'.format( + endpoint=fakes.COMPUTE_ENDPOINT) + + uris_to_mock = [ + dict(method='GET', uri=mock_uri, json={'flavors': []}), + dict(method='GET', uri=mock_uri, + json={'flavors': fakes.FAKE_FLAVOR_LIST}) + ] + uris_to_mock.extend([ + dict(method='GET', + uri='{endpoint}/flavors/{id}/os-extra_specs'.format( + endpoint=fakes.COMPUTE_ENDPOINT, id=flavor['id']), + json={'extra_specs': {}}) + for flavor in fakes.FAKE_FLAVOR_LIST]) + + self.register_uris(uris_to_mock) self.assertEqual([], self.cloud.list_flavors()) diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index fedbb6f2d..fc483c731 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -313,10 +313,13 @@ def test_create_server_network_with_empty_nics(self, @mock.patch.object(OpenStackCloud, 'nova_client') def test_create_server_get_flavor_image( self, mock_nova, mock_image, mock_get_server_by_id): - self.register_uri( - 'GET', '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'flavors': fakes.FAKE_FLAVOR_LIST}) + + self.register_uris([ + dict(method='GET', + uri='{endpoint}/flavors/detail?is_public=None'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'flavors': fakes.FAKE_FLAVOR_LIST})]) + self.cloud.create_server( 'server-name', 'image-id', 'vanilla', nics=[{'net-id': 'some-network'}]) diff --git a/shade/tests/unit/test_domains.py b/shade/tests/unit/test_domains.py index 7a2f80542..eec700b2c 100644 --- a/shade/tests/unit/test_domains.py +++ b/shade/tests/unit/test_domains.py @@ -66,9 +66,9 @@ def _get_domain_data(self, domain_name=None, description=None, def test_list_domains(self): self._add_discovery_uri_call() domain_data = self._get_domain_data() - self.register_uri( - 'GET', self.get_mock_url(), status_code=200, - json={'domains': [domain_data.json_response['domain']]}) + self.register_uris([ + dict(method='GET', uri=self.get_mock_url(), status_code=200, + json={'domains': [domain_data.json_response['domain']]})]) domains = self.op_cloud.list_domains() self.assertThat(len(domains), matchers.Equals(1)) self.assertThat(domains[0].name, @@ -80,10 +80,11 @@ def test_list_domains(self): def test_get_domain(self): self._add_discovery_uri_call() domain_data = self._get_domain_data() - self.register_uri( - 'GET', self.get_mock_url(append=[domain_data.domain_id]), - status_code=200, - json=domain_data.json_response) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(append=[domain_data.domain_id]), + status_code=200, + json=domain_data.json_response)]) domain = self.op_cloud.get_domain(domain_id=domain_data.domain_id) self.assertThat(domain.id, matchers.Equals(domain_data.domain_id)) self.assertThat(domain.name, matchers.Equals(domain_data.domain_name)) @@ -92,12 +93,12 @@ def test_get_domain(self): def test_get_domain_with_name_or_id(self): self._add_discovery_uri_call() domain_data = self._get_domain_data() - self.register_uri( - 'GET', self.get_mock_url(), status_code=200, - json={'domains': [domain_data.json_response['domain']]}) - self.register_uri( - 'GET', self.get_mock_url(), status_code=200, - json={'domains': [domain_data.json_response['domain']]}) + response = {'domains': [domain_data.json_response['domain']]} + self.register_uris([ + dict(method='GET', uri=self.get_mock_url(), status_code=200, + json=response), + dict(method='GET', uri=self.get_mock_url(), status_code=200, + json=response)]) domain = self.op_cloud.get_domain(name_or_id=domain_data.domain_id) domain_by_name = self.op_cloud.get_domain( name_or_id=domain_data.domain_name) @@ -113,17 +114,14 @@ def test_create_domain(self): self._add_discovery_uri_call() domain_data = self._get_domain_data(description=uuid.uuid4().hex, enabled=True) - self.register_uri( - 'POST', - self.get_mock_url(), - status_code=200, - json=domain_data.json_response, - validate=dict(json=domain_data.json_request)) - self.register_uri( - 'GET', - self.get_mock_url(append=[domain_data.domain_id]), - status_code=200, - json=domain_data.json_response) + self.register_uris([ + dict(method='POST', uri=self.get_mock_url(), status_code=200, + json=domain_data.json_response, + validate=dict(json=domain_data.json_request)), + dict(method='GET', + uri=self.get_mock_url(append=[domain_data.domain_id]), + status_code=200, + json=domain_data.json_request)]) domain = self.op_cloud.create_domain( domain_data.domain_name, domain_data.description) self.assertThat(domain.id, matchers.Equals(domain_data.domain_id)) @@ -138,10 +136,8 @@ def test_create_domain_exception(self): shade.OpenStackCloudException, "Failed to create domain domain_name" ): - self.register_uri( - 'POST', - self.get_mock_url(), - status_code=409) + self.register_uris([ + dict(method='POST', uri=self.get_mock_url(), status_code=409)]) self.op_cloud.create_domain('domain_name') self.assert_calls() @@ -150,21 +146,13 @@ def test_delete_domain(self): domain_data = self._get_domain_data() new_resp = domain_data.json_response.copy() new_resp['domain']['enabled'] = False - self.register_uri( - 'PATCH', - self.get_mock_url(append=[domain_data.domain_id]), - status_code=200, - json=new_resp, - validate={'domain': {'enabled': False}}) - self.register_uri( - 'GET', - self.get_mock_url(append=[domain_data.domain_id]), - status_code=200, - json=new_resp) - self.register_uri( - 'DELETE', - self.get_mock_url(append=[domain_data.domain_id]), - status_code=204) + domain_resource_uri = self.get_mock_url(append=[domain_data.domain_id]) + self.register_uris([ + dict(method='PATCH', uri=domain_resource_uri, status_code=200, + json=new_resp, validate={'domain': {'enabled': False}}), + dict(method='GET', uri=domain_resource_uri, status_code=200, + json=new_resp), + dict(method='DELETE', uri=domain_resource_uri, status_code=204)]) self.op_cloud.delete_domain(domain_data.domain_id) self.assert_calls() @@ -173,26 +161,16 @@ def test_delete_domain_name_or_id(self): domain_data = self._get_domain_data() new_resp = domain_data.json_response.copy() new_resp['domain']['enabled'] = False - self.register_uri( - 'GET', - self.get_mock_url(), - status_code=200, - json={'domains': [domain_data.json_response['domain']]}) - self.register_uri( - 'PATCH', - self.get_mock_url(append=[domain_data.domain_id]), - status_code=200, - json=new_resp, - validate={'domain': {'enabled': False}}) - self.register_uri( - 'GET', - self.get_mock_url(append=[domain_data.domain_id]), - status_code=200, - json=new_resp) - self.register_uri( - 'DELETE', - self.get_mock_url(append=[domain_data.domain_id]), - status_code=204) + + domain_resource_uri = self.get_mock_url(append=[domain_data.domain_id]) + self.register_uris([ + dict(method='GET', uri=self.get_mock_url(), status_code=200, + json={'domains': [domain_data.json_response['domain']]}), + dict(method='PATCH', uri=domain_resource_uri, status_code=200, + json=new_resp, validate={'domain': {'enabled': False}}), + dict(method='GET', uri=domain_resource_uri, status_code=200, + json=new_resp), + dict(method='DELETE', uri=domain_resource_uri, status_code=204)]) self.op_cloud.delete_domain(name_or_id=domain_data.domain_id) self.assert_calls() @@ -206,21 +184,13 @@ def test_delete_domain_exception(self): domain_data = self._get_domain_data() new_resp = domain_data.json_response.copy() new_resp['domain']['enabled'] = False - self.register_uri( - 'PATCH', - self.get_mock_url(append=[domain_data.domain_id]), - status_code=200, - json=new_resp, - validate={'domain': {'enabled': False}}) - self.register_uri( - 'GET', - self.get_mock_url(append=[domain_data.domain_id]), - status_code=200, - json=new_resp) - self.register_uri( - 'DELETE', - self.get_mock_url(append=[domain_data.domain_id]), - status_code=404) + domain_resource_uri = self.get_mock_url(append=[domain_data.domain_id]) + self.register_uris([ + dict(method='PATCH', uri=domain_resource_uri, status_code=200, + json=new_resp, validate={'domain': {'enabled': False}}), + dict(method='GET', uri=domain_resource_uri, status_code=200, + json=new_resp), + dict(method='DELETE', uri=domain_resource_uri, status_code=404)]) with testtools.ExpectedException( shade.OpenStackCloudException, "Failed to delete domain %s" % domain_data.domain_id @@ -232,17 +202,13 @@ def test_update_domain(self): self._add_discovery_uri_call() domain_data = self._get_domain_data( description=self.getUniqueString('domainDesc')) - self.register_uri( - 'PATCH', - self.get_mock_url(append=[domain_data.domain_id]), - status_code=200, - json=domain_data.json_response, - validate=dict(json=domain_data.json_request)) - self.register_uri( - 'GET', - self.get_mock_url(append=[domain_data.domain_id]), - status_code=200, - json=domain_data.json_response) + domain_resource_uri = self.get_mock_url(append=[domain_data.domain_id]) + self.register_uris([ + dict(method='PATCH', uri=domain_resource_uri, status_code=200, + json=domain_data.json_response, + validate=dict(json=domain_data.json_request)), + dict(method='GET', uri=domain_resource_uri, status_code=200, + json=domain_data.json_response)]) domain = self.op_cloud.update_domain( domain_data.domain_id, name=domain_data.domain_name, @@ -257,21 +223,15 @@ def test_update_domain_name_or_id(self): self._add_discovery_uri_call() domain_data = self._get_domain_data( description=self.getUniqueString('domainDesc')) - self.register_uri( - 'GET', - self.get_mock_url(), status_code=200, - json={'domains': [domain_data.json_response['domain']]}) - self.register_uri( - 'PATCH', - self.get_mock_url(append=[domain_data.domain_id]), - status_code=200, - json=domain_data.json_response, - validate=dict(json=domain_data.json_request)) - self.register_uri( - 'GET', - self.get_mock_url(append=[domain_data.domain_id]), - status_code=200, - json=domain_data.json_response) + domain_resource_uri = self.get_mock_url(append=[domain_data.domain_id]) + self.register_uris([ + dict(method='GET', uri=self.get_mock_url(), status_code=200, + json={'domains': [domain_data.json_response['domain']]}), + dict(method='PATCH', uri=domain_resource_uri, status_code=200, + json=domain_data.json_response, + validate=dict(json=domain_data.json_request)), + dict(method='GET', uri=domain_resource_uri, status_code=200, + json=domain_data.json_response)]) domain = self.op_cloud.update_domain( name_or_id=domain_data.domain_id, name=domain_data.domain_name, @@ -286,10 +246,10 @@ def test_update_domain_exception(self): self._add_discovery_uri_call() domain_data = self._get_domain_data( description=self.getUniqueString('domainDesc')) - self.register_uri( - 'PATCH', - self.get_mock_url(append=[domain_data.domain_id]), - status_code=409) + self.register_uris([ + dict(method='PATCH', + uri=self.get_mock_url(append=[domain_data.domain_id]), + status_code=409)]) with testtools.ExpectedException( shade.OpenStackCloudException, "Error in updating domain %s" % domain_data.domain_id diff --git a/shade/tests/unit/test_flavors.py b/shade/tests/unit/test_flavors.py index ded2806cd..a01de4028 100644 --- a/shade/tests/unit/test_flavors.py +++ b/shade/tests/unit/test_flavors.py @@ -20,22 +20,23 @@ class TestFlavors(base.RequestsMockTestCase): def test_create_flavor(self): - self.register_uri( - 'POST', '{endpoint}/flavors'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'flavor': fakes.FAKE_FLAVOR}, - validate=dict( - json={'flavor': { - "name": "vanilla", - "ram": 65536, - "vcpus": 24, - "swap": 0, - "os-flavor-access:is_public": True, - "rxtx_factor": 1.0, - "OS-FLV-EXT-DATA:ephemeral": 0, - "disk": 1600, - "id": None - }})) + self.register_uris([ + dict(method='POST', + uri='{endpoint}/flavors'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'flavor': fakes.FAKE_FLAVOR}, + validate=dict( + json={ + 'flavor': { + "name": "vanilla", + "ram": 65536, + "vcpus": 24, + "swap": 0, + "os-flavor-access:is_public": True, + "rxtx_factor": 1.0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "disk": 1600, + "id": None}}))]) self.op_cloud.create_flavor( 'vanilla', ram=65536, disk=1600, vcpus=24, @@ -43,49 +44,57 @@ def test_create_flavor(self): self.assert_calls() def test_delete_flavor(self): - self.register_uri( - 'GET', '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'flavors': fakes.FAKE_FLAVOR_LIST}) - self.register_uri( - 'DELETE', '{endpoint}/flavors/{id}'.format( - endpoint=fakes.COMPUTE_ENDPOINT, id=fakes.FLAVOR_ID)) + self.register_uris([ + dict(method='GET', + uri='{endpoint}/flavors/detail?is_public=None'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'flavors': fakes.FAKE_FLAVOR_LIST}), + dict(method='DELETE', + uri='{endpoint}/flavors/{id}'.format( + endpoint=fakes.COMPUTE_ENDPOINT, id=fakes.FLAVOR_ID))]) self.assertTrue(self.op_cloud.delete_flavor('vanilla')) self.assert_calls() def test_delete_flavor_not_found(self): - self.register_uri( - 'GET', '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'flavors': fakes.FAKE_FLAVOR_LIST}) + self.register_uris([ + dict(method='GET', + uri='{endpoint}/flavors/detail?is_public=None'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'flavors': fakes.FAKE_FLAVOR_LIST})]) self.assertFalse(self.op_cloud.delete_flavor('invalid')) self.assert_calls() def test_delete_flavor_exception(self): - self.register_uri( - 'GET', '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'flavors': fakes.FAKE_FLAVOR_LIST}) - self.register_uri( - 'DELETE', '{endpoint}/flavors/{id}'.format( - endpoint=fakes.COMPUTE_ENDPOINT, id=fakes.FLAVOR_ID), - status_code=503) + self.register_uris([ + dict(method='GET', + uri='{endpoint}/flavors/detail?is_public=None'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'flavors': fakes.FAKE_FLAVOR_LIST}), + dict(method='DELETE', + uri='{endpoint}/flavors/{id}'.format( + endpoint=fakes.FAKE_FLAVOR_LIST, id=fakes.FLAVOR_ID), + status_code=503)]) + self.assertRaises(shade.OpenStackCloudException, self.op_cloud.delete_flavor, 'vanilla') def test_list_flavors(self): - self.register_uri( - 'GET', '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'flavors': fakes.FAKE_FLAVOR_LIST}) - for flavor in fakes.FAKE_FLAVOR_LIST: - self.register_uri( - 'GET', '{endpoint}/flavors/{id}/os-extra_specs'.format( - endpoint=fakes.COMPUTE_ENDPOINT, id=flavor['id']), - json={'extra_specs': {}}) + uris_to_mock = [ + dict(method='GET', + uri='{endpoint}/flavors/detail?is_public=None'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'flavors': fakes.FAKE_FLAVOR_LIST}), + ] + uris_to_mock.extend([ + dict(method='GET', + uri='{endpoint}/flavors/{id}/os-extra_specs'.format( + endpoint=fakes.COMPUTE_ENDPOINT, id=flavor['id']), + json={'extra_specs': {}}) + for flavor in fakes.FAKE_FLAVOR_LIST]) + self.register_uris(uris_to_mock) flavors = self.cloud.list_flavors() @@ -103,59 +112,65 @@ def test_list_flavors(self): self.assert_calls() def test_get_flavor_by_ram(self): - self.register_uri( - 'GET', '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'flavors': fakes.FAKE_FLAVOR_LIST}) - for flavor in fakes.FAKE_FLAVOR_LIST: - self.register_uri( - 'GET', '{endpoint}/flavors/{id}/os-extra_specs'.format( - endpoint=fakes.COMPUTE_ENDPOINT, id=flavor['id']), - json={'extra_specs': {}}) + uris_to_mock = [ + dict(method='GET', + uri='{endpoint}/flavors/detail?is_public=None'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'flavors': fakes.FAKE_FLAVOR_LIST}), + ] + uris_to_mock.extend([ + dict(method='GET', + uri='{endpoint}/flavors/{id}/os-extra_specs'.format( + endpoint=fakes.COMPUTE_ENDPOINT, id=flavor['id']), + json={'extra_specs': {}}) + for flavor in fakes.FAKE_FLAVOR_LIST]) + self.register_uris(uris_to_mock) flavor = self.cloud.get_flavor_by_ram(ram=250) self.assertEqual(fakes.STRAWBERRY_FLAVOR_ID, flavor['id']) def test_get_flavor_by_ram_and_include(self): - self.register_uri( - 'GET', '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'flavors': fakes.FAKE_FLAVOR_LIST}) - for flavor in fakes.FAKE_FLAVOR_LIST: - self.register_uri( - 'GET', '{endpoint}/flavors/{id}/os-extra_specs'.format( - endpoint=fakes.COMPUTE_ENDPOINT, id=flavor['id']), - json={'extra_specs': {}}) + uris_to_mock = [ + dict(method='GET', + uri='{endpoint}/flavors/detail?is_public=None'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'flavors': fakes.FAKE_FLAVOR_LIST}), + ] + uris_to_mock.extend([ + dict(method='GET', + uri='{endpoint}/flavors/{id}/os-extra_specs'.format( + endpoint=fakes.COMPUTE_ENDPOINT, id=flavor['id']), + json={'extra_specs': {}}) + for flavor in fakes.FAKE_FLAVOR_LIST]) + self.register_uris(uris_to_mock) flavor = self.cloud.get_flavor_by_ram(ram=150, include='strawberry') self.assertEqual(fakes.STRAWBERRY_FLAVOR_ID, flavor['id']) def test_get_flavor_by_ram_not_found(self): - self.register_uri( - 'GET', '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'flavors': []}) + self.register_uris([ + dict(method='GET', + uri='{endpoint}/flavors/detail?is_public=None'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'flavors': []})]) self.assertRaises( shade.OpenStackCloudException, self.cloud.get_flavor_by_ram, ram=100) def test_get_flavor_string_and_int(self): - self.register_uri( - 'GET', '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'flavors': [fakes.make_fake_flavor('1', 'vanilla')]}) - self.register_uri( - 'GET', '{endpoint}/flavors/1/os-extra_specs'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'extra_specs': {}}) - self.register_uri( - 'GET', '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'flavors': [fakes.make_fake_flavor('1', 'vanilla')]}) - self.register_uri( - 'GET', '{endpoint}/flavors/1/os-extra_specs'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'extra_specs': {}}) + flavor_list_uri = '{endpoint}/flavors/detail?is_public=None'.format( + endpoint=fakes.COMPUTE_ENDPOINT) + flavor_resource_uri = '{endpoint}/flavors/1/os-extra_specs'.format( + endpoint=fakes.COMPUTE_ENDPOINT) + flavor_list_json = {'flavors': [fakes.make_fake_flavor( + '1', 'vanilla')]} + flavor_json = {'extra_specs': {}} + + self.register_uris([ + dict(method='GET', uri=flavor_list_uri, json=flavor_list_json), + dict(method='GET', uri=flavor_resource_uri, json=flavor_json), + dict(method='GET', uri=flavor_list_uri, json=flavor_list_json), + dict(method='GET', uri=flavor_resource_uri, json=flavor_json)]) flavor1 = self.cloud.get_flavor('1') self.assertEqual('1', flavor1['id']) @@ -164,65 +179,60 @@ def test_get_flavor_string_and_int(self): def test_set_flavor_specs(self): extra_specs = dict(key1='value1') - self.register_uri( - 'POST', '{endpoint}/flavors/{id}/os-extra_specs'.format( - endpoint=fakes.COMPUTE_ENDPOINT, id=1), - json=dict(extra_specs=extra_specs)) + self.register_uris([ + dict(method='POST', + uri='{endpoint}/flavors/{id}/os-extra_specs'.format( + endpoint=fakes.COMPUTE_ENDPOINT, id=1), + json=dict(extra_specs=extra_specs))]) self.op_cloud.set_flavor_specs(1, extra_specs) self.assert_calls() def test_unset_flavor_specs(self): keys = ['key1', 'key2'] - for key in keys: - self.register_uri( - 'DELETE', - '{endpoint}/flavors/{id}/os-extra_specs/{key}'.format( - endpoint=fakes.COMPUTE_ENDPOINT, id=1, key=key)) + self.register_uris([ + dict(method='DELETE', + uri='{endpoint}/flavors/{id}/os-extra_specs/{key}'.format( + endpoint=fakes.COMPUTE_ENDPOINT, id=1, key=key)) + for key in keys]) self.op_cloud.unset_flavor_specs(1, keys) self.assert_calls() def test_add_flavor_access(self): - self.register_uri( - 'POST', '{endpoint}/flavors/{id}/action'.format( - endpoint=fakes.COMPUTE_ENDPOINT, id='flavor_id'), - json={ - 'flavor_access': [{ - 'flavor_id': 'flavor_id', - 'tenant_id': 'tenant_id', - }]}, - validate=dict( - json={ - 'addTenantAccess': { - 'tenant': 'tenant_id', - }})) + self.register_uris([ + dict(method='POST', + uri='{endpoint}/flavors/{id}/action'.format( + endpoint=fakes.COMPUTE_ENDPOINT, id='flavor_id'), + json={ + 'flavor_access': [{ + 'flavor_id': 'flavor_id', 'tenant_id': 'tenant_id'}]}, + validate=dict( + json={'addTenantAccess': {'tenant': 'tenant_id'}}))]) self.op_cloud.add_flavor_access('flavor_id', 'tenant_id') self.assert_calls() def test_remove_flavor_access(self): - self.register_uri( - 'POST', '{endpoint}/flavors/{id}/action'.format( - endpoint=fakes.COMPUTE_ENDPOINT, id='flavor_id'), - json={'flavor_access': []}, - validate=dict( - json={ - 'removeTenantAccess': { - 'tenant': 'tenant_id', - }})) + self.register_uris([ + dict(method='POST', + uri='{endpoint}/flavors/{id}/action'.format( + endpoint=fakes.COMPUTE_ENDPOINT, id='flavor_id'), + json={'flavor_access': []}, + validate=dict( + json={'removeTenantAccess': {'tenant': 'tenant_id'}}))]) self.op_cloud.remove_flavor_access('flavor_id', 'tenant_id') self.assert_calls() def test_list_flavor_access(self): - self.register_uri( - 'GET', '{endpoint}/flavors/vanilla/os-flavor-access'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={ - 'flavor_access': [{ - 'flavor_id': 'vanilla', - 'tenant_id': 'tenant_id', - }]}) + self.register_uris([ + dict(method='GET', + uri='{endpoint}/flavors/vanilla/os-flavor-access'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={ + 'flavor_access': [ + {'flavor_id': 'vanilla', 'tenant_id': 'tenant_id'}]}) + ]) self.op_cloud.list_flavor_access('vanilla') self.assert_calls() diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index befda7c8b..3c5d2bac6 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -163,9 +163,10 @@ def test_float_no_status(self): self.assertEqual('UNKNOWN', normalized[0]['status']) def test_list_floating_ips(self): - self.register_uri( - 'GET', 'https://network.example.com/v2.0/floatingips.json', - json=self.mock_floating_ip_list_rep) + self.register_uris([ + dict(method='GET', + uri='https://network.example.com/v2.0/floatingips.json', + json=self.mock_floating_ip_list_rep)]) floating_ips = self.cloud.list_floating_ips() @@ -177,19 +178,22 @@ def test_list_floating_ips(self): def test_list_floating_ips_with_filters(self): - self.register_uri( - 'GET', 'https://network.example.com/v2.0/floatingips.json?Foo=42', - json={'floatingips': []}) + self.register_uris([ + dict(method='GET', + uri=('https://network.example.com/v2.0/floatingips.json?' + 'Foo=42'), + json={'floatingips': []})]) self.cloud.list_floating_ips(filters={'Foo': 42}) self.assert_calls() def test_search_floating_ips(self): - self.register_uri( - 'GET', - 'https://network.example.com/v2.0/floatingips.json?attached=False', - json=self.mock_floating_ip_list_rep) + self.register_uris([ + dict(method='GET', + uri=('https://network.example.com/v2.0/floatingips.json' + '?attached=False'), + json=self.mock_floating_ip_list_rep)]) floating_ips = self.cloud.search_floating_ips( filters={'attached': False}) @@ -200,9 +204,10 @@ def test_search_floating_ips(self): self.assert_calls() def test_get_floating_ip(self): - self.register_uri( - 'GET', 'https://network.example.com/v2.0/floatingips.json', - json=self.mock_floating_ip_list_rep) + self.register_uris([ + dict(method='GET', + uri='https://network.example.com/v2.0/floatingips.json', + json=self.mock_floating_ip_list_rep)]) floating_ip = self.cloud.get_floating_ip( id='2f245a7b-796b-4f26-9cf9-9e82d248fda7') @@ -221,9 +226,10 @@ def test_get_floating_ip(self): self.assert_calls() def test_get_floating_ip_not_found(self): - self.register_uri( - 'GET', 'https://network.example.com/v2.0/floatingips.json', - json=self.mock_floating_ip_list_rep) + self.register_uris([ + dict(method='GET', + uri='https://network.example.com/v2.0/floatingips.json', + json=self.mock_floating_ip_list_rep)]) floating_ip = self.cloud.get_floating_ip(id='non-existent') @@ -231,14 +237,17 @@ def test_get_floating_ip_not_found(self): self.assert_calls() def test_create_floating_ip(self): - self.register_uri( - 'GET', 'https://network.example.com/v2.0/networks.json', - json={'networks': [self.mock_get_network_rep]}) - self.register_uri( - 'POST', 'https://network.example.com/v2.0/floatingips.json', - json=self.mock_floating_ip_new_rep, - validate=dict( - json={'floatingip': {'floating_network_id': 'my-network-id'}})) + self.register_uris([ + dict(method='GET', + uri='https://network.example.com/v2.0/networks.json', + json={'networks': [self.mock_get_network_rep]}), + dict(method='POST', + uri='https://network.example.com/v2.0/floatingips.json', + json=self.mock_floating_ip_new_rep, + validate=dict( + json={'floatingip': { + 'floating_network_id': 'my-network-id'}})) + ]) ip = self.cloud.create_floating_ip(network='my-network') self.assertEqual( @@ -247,17 +256,18 @@ def test_create_floating_ip(self): self.assert_calls() def test_create_floating_ip_port_bad_response(self): - self.register_uri( - 'GET', 'https://network.example.com/v2.0/networks.json', - json={'networks': [self.mock_get_network_rep]}) - self.register_uri( - 'POST', 'https://network.example.com/v2.0/floatingips.json', - json=self.mock_floating_ip_new_rep, - validate=dict( - json={'floatingip': { - 'floating_network_id': 'my-network-id', - 'port_id': u'ce705c24-c1ef-408a-bda3-7bbd946164ab', - }})) + self.register_uris([ + dict(method='GET', + uri='https://network.example.com/v2.0/networks.json', + json={'networks': [self.mock_get_network_rep]}), + dict(method='POST', + uri='https://network.example.com/v2.0/floatingips.json', + json=self.mock_floating_ip_new_rep, + validate=dict( + json={'floatingip': { + 'floating_network_id': 'my-network-id', + 'port_id': u'ce705c24-c1ef-408a-bda3-7bbd946164ab'}})) + ]) # Fails because we requested a port and the returned FIP has no port self.assertRaises( @@ -267,17 +277,18 @@ def test_create_floating_ip_port_bad_response(self): self.assert_calls() def test_create_floating_ip_port(self): - self.register_uri( - 'GET', 'https://network.example.com/v2.0/networks.json', - json={'networks': [self.mock_get_network_rep]}) - self.register_uri( - 'POST', 'https://network.example.com/v2.0/floatingips.json', - json=self.mock_floating_ip_port_rep, - validate=dict( - json={'floatingip': { - 'floating_network_id': 'my-network-id', - 'port_id': u'ce705c24-c1ef-408a-bda3-7bbd946164ac', - }})) + self.register_uris([ + dict(method='GET', + uri='https://network.example.com/v2.0/networks.json', + json={'networks': [self.mock_get_network_rep]}), + dict(method='POST', + uri='https://network.example.com/v2.0/floatingips.json', + json=self.mock_floating_ip_port_rep, + validate=dict( + json={'floatingip': { + 'floating_network_id': 'my-network-id', + 'port_id': u'ce705c24-c1ef-408a-bda3-7bbd946164ac'}})) + ]) ip = self.cloud.create_floating_ip( network='my-network', port='ce705c24-c1ef-408a-bda3-7bbd946164ac') @@ -287,51 +298,51 @@ def test_create_floating_ip_port(self): ip['floating_ip_address']) self.assert_calls() - def test__neutron_available_floating_ips(self): + def test_neutron_available_floating_ips(self): """ Test without specifying a network name. """ - self.register_uri( - 'GET', 'https://network.example.com/v2.0/networks.json', - json={'networks': [self.mock_get_network_rep]}) - self.register_uri( - 'GET', 'https://network.example.com/v2.0/subnets.json', - json={'subnets': []}) - self.register_uri( - 'GET', 'https://network.example.com/v2.0/floatingips.json', - json={'floatingips': []}) - self.register_uri( - 'POST', 'https://network.example.com/v2.0/floatingips.json', - json=self.mock_floating_ip_new_rep, - validate=dict(json={ - 'floatingip': { - 'floating_network_id': self.mock_get_network_rep['id'], - }})) + fips_mock_uri = 'https://network.example.com/v2.0/floatingips.json' + self.register_uris([ + dict(method='GET', + uri='https://network.example.com/v2.0/networks.json', + json={'networks': [self.mock_get_network_rep]}), + dict(method='GET', + uri='https://network.example.com/v2.0/subnets.json', + json={'subnets': []}), + dict(method='GET', uri=fips_mock_uri, json={'floatingips': []}), + dict(method='POST', uri=fips_mock_uri, + json=self.mock_floating_ip_new_rep, + validate=dict(json={ + 'floatingip': { + 'floating_network_id': self.mock_get_network_rep['id'] + }})) + ]) # Test if first network is selected if no network is given self.cloud._neutron_available_floating_ips() self.assert_calls() - def test__neutron_available_floating_ips_network(self): + def test_neutron_available_floating_ips_network(self): """ Test with specifying a network name. """ - self.register_uri( - 'GET', 'https://network.example.com/v2.0/networks.json', - json={'networks': [self.mock_get_network_rep]}) - self.register_uri( - 'GET', 'https://network.example.com/v2.0/subnets.json', - json={'subnets': []}) - self.register_uri( - 'GET', 'https://network.example.com/v2.0/floatingips.json', - json={'floatingips': []}) - self.register_uri( - 'POST', 'https://network.example.com/v2.0/floatingips.json', - json=self.mock_floating_ip_new_rep, - validate=dict(json={ - 'floatingip': { - 'floating_network_id': self.mock_get_network_rep['id'], - }})) + fips_mock_uri = 'https://network.example.com/v2.0/floatingips.json' + self.register_uris([ + dict(method='GET', + uri='https://network.example.com/v2.0/networks.json', + json={'networks': [self.mock_get_network_rep]}), + dict(method='GET', + uri='https://network.example.com/v2.0/subnets.json', + json={'subnets': []}), + dict(method='GET', uri=fips_mock_uri, json={'floatingips': []}), + dict(method='POST', uri=fips_mock_uri, + json=self.mock_floating_ip_new_rep, + validate=dict(json={ + 'floatingip': { + 'floating_network_id': self.mock_get_network_rep['id'] + }})) + ]) # Test if first network is selected if no network is given self.cloud._neutron_available_floating_ips( @@ -339,16 +350,18 @@ def test__neutron_available_floating_ips_network(self): ) self.assert_calls() - def test__neutron_available_floating_ips_invalid_network(self): + def test_neutron_available_floating_ips_invalid_network(self): """ Test with an invalid network name. """ - self.register_uri( - 'GET', 'https://network.example.com/v2.0/networks.json', - json={'networks': [self.mock_get_network_rep]}) - self.register_uri( - 'GET', 'https://network.example.com/v2.0/subnets.json', - json={'subnets': []}) + self.register_uris([ + dict(method='GET', + uri='https://network.example.com/v2.0/networks.json', + json={'networks': [self.mock_get_network_rep]}), + dict(method='GET', + uri='https://network.example.com/v2.0/subnets.json', + json={'subnets': []}) + ]) self.assertRaises( exc.OpenStackCloudException, @@ -359,205 +372,202 @@ def test__neutron_available_floating_ips_invalid_network(self): def test_auto_ip_pool_no_reuse(self): # payloads taken from citycloud - self.register_uri( - 'GET', 'https://network.example.com/v2.0/networks.json', - json={"networks": [{ - "status": "ACTIVE", - "subnets": [ - "df3e17fa-a4b2-47ae-9015-bc93eb076ba2", - "6b0c3dc9-b0b8-4d87-976a-7f2ebf13e7ec", - "fc541f48-fc7f-48c0-a063-18de6ee7bdd7"], - "availability_zone_hints": [], - "availability_zones": ["nova"], - "name": "ext-net", - "admin_state_up": True, - "tenant_id": "a564613210ee43708b8a7fc6274ebd63", - "tags": [], - "ipv6_address_scope": "9f03124f-89af-483a-b6fd-10f08079db4d", - "mtu": 0, - "is_default": False, - "router:external": True, - "ipv4_address_scope": None, - "shared": False, - "id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", - "description": None - }, { - "status": "ACTIVE", - "subnets": ["f0ad1df5-53ee-473f-b86b-3604ea5591e9"], - "availability_zone_hints": [], - "availability_zones": ["nova"], - "name": "private", - "admin_state_up": True, - "tenant_id": "65222a4d09ea4c68934fa1028c77f394", - "created_at": "2016-10-22T13:46:26", - "tags": [], - "updated_at": "2016-10-22T13:46:26", - "ipv6_address_scope": None, - "router:external": False, - "ipv4_address_scope": None, - "shared": False, - "mtu": 1450, - "id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", - "description": "" - }]}) - self.register_uri( - 'GET', - 'https://network.example.com/v2.0/ports.json' - '?device_id=f80e3ad0-e13e-41d4-8e9c-be79bccdb8f7', - json={"ports": [{ - "status": "ACTIVE", - "created_at": "2017-02-06T20:59:45", - "description": "", - "allowed_address_pairs": [], - "admin_state_up": True, - "network_id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", - "dns_name": None, - "extra_dhcp_opts": [], - "mac_address": "fa:16:3e:e8:7f:03", - "updated_at": "2017-02-06T20:59:49", - "name": "", - "device_owner": "compute:None", - "tenant_id": "65222a4d09ea4c68934fa1028c77f394", - "binding:vnic_type": "normal", - "fixed_ips": [{ - "subnet_id": "f0ad1df5-53ee-473f-b86b-3604ea5591e9", - "ip_address": "10.4.0.16"}], - "id": "a767944e-057a-47d1-a669-824a21b8fb7b", - "security_groups": ["9fb5ba44-5c46-4357-8e60-8b55526cab54"], - "device_id": "f80e3ad0-e13e-41d4-8e9c-be79bccdb8f7", - }]}) - - self.register_uri( - 'POST', - 'https://network.example.com/v2.0/floatingips.json', - json={"floatingip": { - "router_id": "9de9c787-8f89-4a53-8468-a5533d6d7fd1", - "status": "DOWN", - "description": "", - "dns_domain": "", - "floating_network_id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", - "fixed_ip_address": "10.4.0.16", - "floating_ip_address": "89.40.216.153", - "port_id": "a767944e-057a-47d1-a669-824a21b8fb7b", - "id": "e69179dc-a904-4c9a-a4c9-891e2ecb984c", - "dns_name": "", - "tenant_id": "65222a4d09ea4c68934fa1028c77f394" - }}, - validate=dict(json={"floatingip": { - "floating_network_id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", - "fixed_ip_address": "10.4.0.16", - "port_id": "a767944e-057a-47d1-a669-824a21b8fb7b", - }})) - - self.register_uri( - 'GET', - '{endpoint}/servers/detail'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={"servers": [{ - "status": "ACTIVE", - "updated": "2017-02-06T20:59:49Z", - "addresses": { - "private": [{ - "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:e8:7f:03", - "version": 4, - "addr": "10.4.0.16", - "OS-EXT-IPS:type": "fixed" - }, { - "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:e8:7f:03", - "version": 4, - "addr": "89.40.216.153", - "OS-EXT-IPS:type": "floating" - }]}, - "key_name": None, - "image": {"id": "95e4c449-8abf-486e-97d9-dc3f82417d2d"}, - "OS-EXT-STS:task_state": None, - "OS-EXT-STS:vm_state": "active", - "OS-SRV-USG:launched_at": "2017-02-06T20:59:48.000000", - "flavor": {"id": "2186bd79-a05e-4953-9dde-ddefb63c88d4"}, - "id": "f80e3ad0-e13e-41d4-8e9c-be79bccdb8f7", - "security_groups": [{"name": "default"}], - "OS-SRV-USG:terminated_at": None, - "OS-EXT-AZ:availability_zone": "nova", - "user_id": "c17534835f8f42bf98fc367e0bf35e09", - "name": "testmt", - "created": "2017-02-06T20:59:44Z", - "tenant_id": "65222a4d09ea4c68934fa1028c77f394", - "OS-DCF:diskConfig": "MANUAL", - "os-extended-volumes:volumes_attached": [], - "accessIPv4": "", - "accessIPv6": "", - "progress": 0, - "OS-EXT-STS:power_state": 1, - "config_drive": "", - "metadata": {} - }]}) - - self.register_uri( - 'GET', 'https://network.example.com/v2.0/networks.json', - json={"networks": [{ - "status": "ACTIVE", - "subnets": [ - "df3e17fa-a4b2-47ae-9015-bc93eb076ba2", - "6b0c3dc9-b0b8-4d87-976a-7f2ebf13e7ec", - "fc541f48-fc7f-48c0-a063-18de6ee7bdd7"], - "availability_zone_hints": [], - "availability_zones": ["nova"], - "name": "ext-net", - "admin_state_up": True, - "tenant_id": "a564613210ee43708b8a7fc6274ebd63", - "tags": [], - "ipv6_address_scope": "9f03124f-89af-483a-b6fd-10f08079db4d", - "mtu": 0, - "is_default": False, - "router:external": True, - "ipv4_address_scope": None, - "shared": False, - "id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", - "description": None - }, { - "status": "ACTIVE", - "subnets": ["f0ad1df5-53ee-473f-b86b-3604ea5591e9"], - "availability_zone_hints": [], - "availability_zones": ["nova"], - "name": "private", - "admin_state_up": True, - "tenant_id": "65222a4d09ea4c68934fa1028c77f394", - "created_at": "2016-10-22T13:46:26", - "tags": [], - "updated_at": "2016-10-22T13:46:26", - "ipv6_address_scope": None, - "router:external": False, - "ipv4_address_scope": None, - "shared": False, - "mtu": 1450, - "id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", - "description": "" - }]}) - self.register_uri( - 'GET', 'https://network.example.com/v2.0/subnets.json', - json={"subnets": [{ - "description": "", - "enable_dhcp": True, - "network_id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", - "tenant_id": "65222a4d09ea4c68934fa1028c77f394", - "created_at": "2016-10-22T13:46:26", - "dns_nameservers": [ - "89.36.90.101", - "89.36.90.102"], - "updated_at": "2016-10-22T13:46:26", - "gateway_ip": "10.4.0.1", - "ipv6_ra_mode": None, - "allocation_pools": [{ - "start": "10.4.0.2", - "end": "10.4.0.200"}], - "host_routes": [], - "ip_version": 4, - "ipv6_address_mode": None, - "cidr": "10.4.0.0/24", - "id": "f0ad1df5-53ee-473f-b86b-3604ea5591e9", - "subnetpool_id": None, - "name": "private-subnet-ipv4", - }]}) + self.register_uris([ + dict(method='GET', + uri='https://network.example.com/v2.0/networks.json', + json={"networks": [{ + "status": "ACTIVE", + "subnets": [ + "df3e17fa-a4b2-47ae-9015-bc93eb076ba2", + "6b0c3dc9-b0b8-4d87-976a-7f2ebf13e7ec", + "fc541f48-fc7f-48c0-a063-18de6ee7bdd7"], + "availability_zone_hints": [], + "availability_zones": ["nova"], + "name": "ext-net", + "admin_state_up": True, + "tenant_id": "a564613210ee43708b8a7fc6274ebd63", + "tags": [], + "ipv6_address_scope": "9f03124f-89af-483a-b6fd-10f08079db4d", # noqa + "mtu": 0, + "is_default": False, + "router:external": True, + "ipv4_address_scope": None, + "shared": False, + "id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", + "description": None + }, { + "status": "ACTIVE", + "subnets": ["f0ad1df5-53ee-473f-b86b-3604ea5591e9"], + "availability_zone_hints": [], + "availability_zones": ["nova"], + "name": "private", + "admin_state_up": True, + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", + "created_at": "2016-10-22T13:46:26", + "tags": [], + "updated_at": "2016-10-22T13:46:26", + "ipv6_address_scope": None, + "router:external": False, + "ipv4_address_scope": None, + "shared": False, + "mtu": 1450, + "id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", + "description": "" + }]}), + dict(method='GET', + uri='https://network.example.com/v2.0/ports.json' + '?device_id=f80e3ad0-e13e-41d4-8e9c-be79bccdb8f7', + json={"ports": [{ + "status": "ACTIVE", + "created_at": "2017-02-06T20:59:45", + "description": "", + "allowed_address_pairs": [], + "admin_state_up": True, + "network_id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", + "dns_name": None, + "extra_dhcp_opts": [], + "mac_address": "fa:16:3e:e8:7f:03", + "updated_at": "2017-02-06T20:59:49", + "name": "", + "device_owner": "compute:None", + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", + "binding:vnic_type": "normal", + "fixed_ips": [{ + "subnet_id": "f0ad1df5-53ee-473f-b86b-3604ea5591e9", + "ip_address": "10.4.0.16"}], + "id": "a767944e-057a-47d1-a669-824a21b8fb7b", + "security_groups": [ + "9fb5ba44-5c46-4357-8e60-8b55526cab54"], + "device_id": "f80e3ad0-e13e-41d4-8e9c-be79bccdb8f7", + }]}), + + dict(method='POST', + uri='https://network.example.com/v2.0/floatingips.json', + json={"floatingip": { + "router_id": "9de9c787-8f89-4a53-8468-a5533d6d7fd1", + "status": "DOWN", + "description": "", + "dns_domain": "", + "floating_network_id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", # noqa + "fixed_ip_address": "10.4.0.16", + "floating_ip_address": "89.40.216.153", + "port_id": "a767944e-057a-47d1-a669-824a21b8fb7b", + "id": "e69179dc-a904-4c9a-a4c9-891e2ecb984c", + "dns_name": "", + "tenant_id": "65222a4d09ea4c68934fa1028c77f394" + }}, + validate=dict(json={"floatingip": { + "floating_network_id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", # noqa + "fixed_ip_address": "10.4.0.16", + "port_id": "a767944e-057a-47d1-a669-824a21b8fb7b", + }})), + dict(method='GET', + uri='{endpoint}/servers/detail'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={"servers": [{ + "status": "ACTIVE", + "updated": "2017-02-06T20:59:49Z", + "addresses": { + "private": [{ + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:e8:7f:03", + "version": 4, + "addr": "10.4.0.16", + "OS-EXT-IPS:type": "fixed" + }, { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:e8:7f:03", + "version": 4, + "addr": "89.40.216.153", + "OS-EXT-IPS:type": "floating" + }]}, + "key_name": None, + "image": {"id": "95e4c449-8abf-486e-97d9-dc3f82417d2d"}, + "OS-EXT-STS:task_state": None, + "OS-EXT-STS:vm_state": "active", + "OS-SRV-USG:launched_at": "2017-02-06T20:59:48.000000", + "flavor": {"id": "2186bd79-a05e-4953-9dde-ddefb63c88d4"}, + "id": "f80e3ad0-e13e-41d4-8e9c-be79bccdb8f7", + "security_groups": [{"name": "default"}], + "OS-SRV-USG:terminated_at": None, + "OS-EXT-AZ:availability_zone": "nova", + "user_id": "c17534835f8f42bf98fc367e0bf35e09", + "name": "testmt", + "created": "2017-02-06T20:59:44Z", + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": {} + }]}), + dict(method='GET', + uri='https://network.example.com/v2.0/networks.json', + json={"networks": [{ + "status": "ACTIVE", + "subnets": [ + "df3e17fa-a4b2-47ae-9015-bc93eb076ba2", + "6b0c3dc9-b0b8-4d87-976a-7f2ebf13e7ec", + "fc541f48-fc7f-48c0-a063-18de6ee7bdd7"], + "availability_zone_hints": [], + "availability_zones": ["nova"], + "name": "ext-net", + "admin_state_up": True, + "tenant_id": "a564613210ee43708b8a7fc6274ebd63", + "tags": [], + "ipv6_address_scope": "9f03124f-89af-483a-b6fd-10f08079db4d", # noqa + "mtu": 0, + "is_default": False, + "router:external": True, + "ipv4_address_scope": None, + "shared": False, + "id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", + "description": None + }, { + "status": "ACTIVE", + "subnets": ["f0ad1df5-53ee-473f-b86b-3604ea5591e9"], + "availability_zone_hints": [], + "availability_zones": ["nova"], + "name": "private", + "admin_state_up": True, + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", + "created_at": "2016-10-22T13:46:26", + "tags": [], + "updated_at": "2016-10-22T13:46:26", + "ipv6_address_scope": None, + "router:external": False, + "ipv4_address_scope": None, + "shared": False, + "mtu": 1450, + "id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", + "description": "" + }]}), + dict(method='GET', + uri='https://network.example.com/v2.0/subnets.json', + json={"subnets": [{ + "description": "", + "enable_dhcp": True, + "network_id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", + "created_at": "2016-10-22T13:46:26", + "dns_nameservers": [ + "89.36.90.101", + "89.36.90.102"], + "updated_at": "2016-10-22T13:46:26", + "gateway_ip": "10.4.0.1", + "ipv6_ra_mode": None, + "allocation_pools": [{ + "start": "10.4.0.2", + "end": "10.4.0.200"}], + "host_routes": [], + "ip_version": 4, + "ipv6_address_mode": None, + "cidr": "10.4.0.0/24", + "id": "f0ad1df5-53ee-473f-b86b-3604ea5591e9", + "subnetpool_id": None, + "name": "private-subnet-ipv4", + }]})]) self.cloud.add_ips_to_server( munch.Munch( @@ -678,11 +688,11 @@ def test_delete_floating_ip_existing_no_delete( self.assertEqual(mock_get_floating_ip.call_count, 3) def test_delete_floating_ip_not_found(self): - self.register_uri( - 'DELETE', - 'https://network.example.com/v2.0/floatingips' - '/a-wild-id-appears.json', - status_code=404) + self.register_uris([ + dict(method='DELETE', + uri=('https://network.example.com/v2.0/floatingips/' + 'a-wild-id-appears.json'), + status_code=404)]) ret = self.cloud.delete_floating_ip( floating_ip_id='a-wild-id-appears') diff --git a/shade/tests/unit/test_users.py b/shade/tests/unit/test_users.py index adcf5dba2..cb714537f 100644 --- a/shade/tests/unit/test_users.py +++ b/shade/tests/unit/test_users.py @@ -33,18 +33,17 @@ def test_create_user_v2(self): user_data = self._get_user_data() - self.register_uri('POST', - self._get_keystone_mock_url(resource='users', - v3=False), - status_code=204, - json=user_data.json_response, - validate=dict(json=user_data.json_request)) - self.register_uri('GET', - self._get_keystone_mock_url( - resource='users', - append=[user_data.user_id], - v3=False), - status_code=200, json=user_data.json_response) + self.register_uris([ + dict(method='POST', + uri=self._get_keystone_mock_url(resource='users', v3=False), + status_code=200, + json=user_data.json_response, + validate=user_data.json_request), + dict(method='GET', + uri=self._get_keystone_mock_url(resource='users', v3=False, + append=[user_data.user_id]), + status_code=200, json=user_data.json_response)]) + user = self.op_cloud.create_user( name=user_data.name, email=user_data.email, password=user_data.password) @@ -59,17 +58,17 @@ def test_create_user_v3(self): user_data = self._get_user_data( domain_id=uuid.uuid4().hex, description=self.getUniqueString('description')) - self.register_uri( - 'POST', - self._get_keystone_mock_url(resource='users'), - status_code=204, - json=user_data.json_response, - validate=user_data.json_request) - self.register_uri( - 'GET', - self._get_keystone_mock_url(resource='users', - append=[user_data.user_id]), - status_code=200, json=user_data.json_response) + + self.register_uris([ + dict(method='POST', + uri=self._get_keystone_mock_url(resource='users'), + status_code=200, json=user_data.json_response, + validate=user_data.json_request), + dict(method='GET', + uri=self._get_keystone_mock_url( + resource='users', append=[user_data.user_id]), + status_code=200, json=user_data.json_response)]) + user = self.op_cloud.create_user( name=user_data.name, email=user_data.email, password=user_data.password, @@ -84,47 +83,40 @@ def test_create_user_v3(self): def test_update_user_password_v2(self): self.use_keystone_v2() + user_data = self._get_user_data(email='test@example.com') - self.register_uri('GET', - self._get_keystone_mock_url(resource='users', - v3=False), - status_code=200, - json={'users': [user_data.json_response['user']]}) - self.register_uri('GET', - self._get_keystone_mock_url( - resource='users', - v3=False, - append=[user_data.user_id]), - json=user_data.json_response) - self.register_uri( - 'PUT', - self._get_keystone_mock_url( - resource='users', v3=False, - append=[user_data.user_id, 'OS-KSADM', 'password']), - status_code=204, json=user_data.json_response, - validate=dict(json={'user': {'password': user_data.password}})) - self.register_uri('GET', - self._get_keystone_mock_url( - resource='users', v3=False, - append=[user_data.user_id]), - json=user_data.json_response) - # NOTE(notmorgan): when keystoneclient is dropped, the extra call is - # not needed as it is a blank put. Keystoneclient has very limited - # logic and does odd things when updates inclue passwords in v2 - # keystone. - self.register_uri( - 'PUT', - self._get_keystone_mock_url(resource='users', - append=[user_data.user_id], - v3=False), - status_code=204, json=user_data.json_response, - validate=dict(json={'user': {}})) - self.register_uri('GET', - self._get_keystone_mock_url( - resource='users', - v3=False, - append=[user_data.user_id]), - json=user_data.json_response) + mock_user_resource_uri = self._get_keystone_mock_url( + resource='users', append=[user_data.user_id], v3=False) + mock_users_uri = self._get_keystone_mock_url( + resource='users', v3=False) + + self.register_uris([ + # GET list to find user id + # GET user info with user_id from list + # PUT user with password update + # GET user info with id after update + # PUT empty update (password change is different than update) + # but is always chained together [keystoneclient oddity] + # GET user info after user update + dict(method='GET', uri=mock_users_uri, status_code=200, + json={'users': [user_data.json_response['user']]}), + dict(method='GET', uri=mock_user_resource_uri, status_code=200, + json=user_data.json_response), + dict(method='PUT', + uri=self._get_keystone_mock_url( + resource='users', v3=False, + append=[user_data.user_id, 'OS-KSADM', 'password']), + status_code=200, json=user_data.json_response, + validate=dict( + json={'user': {'password': user_data.password}})), + dict(method='GET', uri=mock_user_resource_uri, status_code=200, + json=user_data.json_response), + dict(method='PUT', uri=mock_user_resource_uri, status_code=200, + json=user_data.json_response, + validate=dict(json={'user': {}})), + dict(method='GET', uri=mock_user_resource_uri, status_code=200, + json=user_data.json_response)]) + user = self.op_cloud.update_user( user_data.user_id, password=user_data.password) self.assertEqual(user_data.name, user.name) @@ -146,47 +138,48 @@ def test_create_user_v3_no_domain(self): def test_delete_user(self): self._add_discovery_uri_call() user_data = self._get_user_data(domain_id=uuid.uuid4().hex) - self.register_uri('GET', self._get_keystone_mock_url(resource='users'), - status_code=200, - json={'users': [user_data.json_response['user']]}) - self.register_uri('GET', - self._get_keystone_mock_url( - resource='users', append=[user_data.user_id]), - status_code=200, json=user_data.json_response) - self.register_uri('DELETE', - self._get_keystone_mock_url( - resource='users', append=[user_data.user_id]), - status_code=204) + user_resource_uri = self._get_keystone_mock_url( + resource='users', append=[user_data.user_id]) + + self.register_uris([ + dict(method='GET', + uri=self._get_keystone_mock_url(resource='users'), + status_code=200, + json={'users': [user_data.json_response['user']]}), + dict(method='GET', uri=user_resource_uri, status_code=200, + json=user_data.json_response), + dict(method='DELETE', uri=user_resource_uri, status_code=204)]) + self.op_cloud.delete_user(user_data.name) self.assert_calls() def test_delete_user_not_found(self): self._add_discovery_uri_call() - self.register_uri('GET', - self._get_keystone_mock_url(resource='users'), - status_code=200, - json={'users': []}) + self.register_uris([ + dict(method='GET', + uri=self._get_keystone_mock_url(resource='users'), + status_code=200, json={'users': []})]) self.assertFalse(self.op_cloud.delete_user(self.getUniqueString())) def test_add_user_to_group(self): self._add_discovery_uri_call() user_data = self._get_user_data() group_data = self._get_group_data() - self.register_uri('GET', - self._get_keystone_mock_url(resource='users'), - status_code=200, - json={'users': [user_data.json_response['user']]}) - self.register_uri( - 'GET', - self._get_keystone_mock_url(resource='groups'), - status_code=200, - json={'groups': [group_data.json_response['group']]}) - self.register_uri( - 'PUT', - self._get_keystone_mock_url( - resource='groups', - append=[group_data.group_id, 'users', user_data.user_id]), - status_code=200) + + self.register_uris([ + dict(method='GET', + uri=self._get_keystone_mock_url(resource='users'), + status_code=200, + json={'users': [user_data.json_response['user']]}), + dict(method='GET', + uri=self._get_keystone_mock_url(resource='groups'), + status_code=200, + json={'groups': [group_data.json_response['group']]}), + dict(method='PUT', + uri=self._get_keystone_mock_url( + resource='groups', + append=[group_data.group_id, 'users', user_data.user_id]), + status_code=200)]) self.op_cloud.add_user_to_group(user_data.user_id, group_data.group_id) self.assert_calls() @@ -194,21 +187,22 @@ def test_is_user_in_group(self): self._add_discovery_uri_call() user_data = self._get_user_data() group_data = self._get_group_data() - self.register_uri('GET', - self._get_keystone_mock_url(resource='users'), - status_code=200, - json={'users': [user_data.json_response['user']]}) - self.register_uri( - 'GET', - self._get_keystone_mock_url(resource='groups'), - status_code=200, - json={'groups': [group_data.json_response['group']]}) - self.register_uri( - 'HEAD', - self._get_keystone_mock_url( - resource='groups', - append=[group_data.group_id, 'users', user_data.user_id]), - status_code=204) + + self.register_uris([ + dict(method='GET', + uri=self._get_keystone_mock_url(resource='users'), + status_code=200, + json={'users': [user_data.json_response['user']]}), + dict(method='GET', + uri=self._get_keystone_mock_url(resource='groups'), + status_code=200, + json={'groups': [group_data.json_response['group']]}), + dict(method='HEAD', + uri=self._get_keystone_mock_url( + resource='groups', + append=[group_data.group_id, 'users', user_data.user_id]), + status_code=204)]) + self.assertTrue(self.op_cloud.is_user_in_group( user_data.user_id, group_data.group_id)) self.assert_calls() @@ -217,21 +211,21 @@ def test_remove_user_from_group(self): self._add_discovery_uri_call() user_data = self._get_user_data() group_data = self._get_group_data() - self.register_uri('GET', - self._get_keystone_mock_url(resource='users'), - status_code=200, - json={'users': [user_data.json_response['user']]}) - self.register_uri( - 'GET', - self._get_keystone_mock_url(resource='groups'), - status_code=200, - json={'groups': [group_data.json_response['group']]}) - self.register_uri( - 'DELETE', - self._get_keystone_mock_url( - resource='groups', - append=[group_data.group_id, 'users', user_data.user_id]), - status_code=204) + + self.register_uris([ + dict(method='GET', + uri=self._get_keystone_mock_url(resource='users'), + json={'users': [user_data.json_response['user']]}), + dict(method='GET', + uri=self._get_keystone_mock_url(resource='groups'), + status_code=200, + json={'groups': [group_data.json_response['group']]}), + dict(method='DELETE', + uri=self._get_keystone_mock_url( + resource='groups', + append=[group_data.group_id, 'users', user_data.user_id]), + status_code=204)]) + self.op_cloud.remove_user_from_group(user_data.user_id, group_data.group_id) self.assert_calls() From 4f98c8b23d82ea15151ef0ffb49f39987eb088b0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 10 Feb 2017 09:43:18 -0600 Subject: [PATCH 1269/3836] Add accessor method to pull URLs from the catalog It's handy sometimes and the specifics of how to do so is complex. Change-Id: I90d5c8c2709cf9d8bd76c3dbcbe171c7d75572a8 --- .../endpoint-from-catalog-bad36cb0409a4e6a.yaml | 4 ++++ shade/openstackcloud.py | 12 ++++++++++-- shade/tests/unit/base.py | 7 ++----- 3 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/endpoint-from-catalog-bad36cb0409a4e6a.yaml diff --git a/releasenotes/notes/endpoint-from-catalog-bad36cb0409a4e6a.yaml b/releasenotes/notes/endpoint-from-catalog-bad36cb0409a4e6a.yaml new file mode 100644 index 000000000..2db7bc947 --- /dev/null +++ b/releasenotes/notes/endpoint-from-catalog-bad36cb0409a4e6a.yaml @@ -0,0 +1,4 @@ +--- +features: + - Add new method, 'endpoint_for' which will return the + raw endpoint for a given service from the current catalog. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 19714c476..78124749f 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -566,9 +566,17 @@ def keystone_client(self): return self._keystone_client @property - def service_catalog(self): + def _keystone_catalog(self): return self.keystone_session.auth.get_access( - self.keystone_session).service_catalog.catalog + self.keystone_session).service_catalog + + @property + def service_catalog(self): + return self._keystone_catalog.catalog + + def endpoint_for(self, service_type, interface='public'): + return self._keystone_catalog.url_for( + service_type=service_type, interface=interface) @property def auth_token(self): diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 788bb4b4e..1557d90eb 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -122,11 +122,8 @@ def setUp(self, cloud_config_fixture='clouds.yaml'): def get_mock_url(self, service_type, interface, resource=None, append=None, base_url_append=None): - service_catalog = self.cloud.keystone_session.auth.get_access( - self.cloud.keystone_session).service_catalog - endpoint_url = service_catalog.url_for( - service_type=service_type, - interface=interface) + endpoint_url = self.cloud.endpoint_for( + service_type=service_type, interface=interface) to_join = [endpoint_url] if base_url_append: to_join.append(base_url_append) From 3c8c7299f84e46da9cd2feeb6125aad2c86fd7bd Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Fri, 3 Feb 2017 11:14:18 -0500 Subject: [PATCH 1270/3836] Deprecate port and ping methods in Network proxy The Network proxy contains two methods that are mostly there for example purposes: security_group_allow_ping and security_group_open_port. They haven't been included in documentation due to missing docstrings, and they aren't used anywhere in the SDK or within OSC. They're better off as actual examples in our documentation than being within code that we need to support throughout the future. Change-Id: I4c6b47860b0c2b952e50d4b564451780008b631a --- doc/source/users/guides/network.rst | 24 ++++++++ examples/network/security_group_rules.py | 57 +++++++++++++++++++ openstack/network/v2/_proxy.py | 5 ++ openstack/tests/unit/network/v2/test_proxy.py | 3 + 4 files changed, 89 insertions(+) create mode 100644 examples/network/security_group_rules.py diff --git a/doc/source/users/guides/network.rst b/doc/source/users/guides/network.rst index 7fd58797d..ae9a28b69 100644 --- a/doc/source/users/guides/network.rst +++ b/doc/source/users/guides/network.rst @@ -101,6 +101,29 @@ same project network. Full example: `network resource create`_ +Open a Port +----------- + +When creating a security group for a network, you will need to open certain +ports to allow communication via them. For example, you may need to enable +HTTPS access on port 443. + +.. literalinclude:: ../examples/network/security_group_rules.py + :pyobject: open_port + +Full example: `network security group create`_ + +Accept Pings +------------ + +In order to ping a machine on your network within a security group, +you will need to create a rule to allow inbound ICMP packets. + +.. literalinclude:: ../examples/network/security_group_rules.py + :pyobject: allow_ping + +Full example: `network security group create`_ + Delete Network -------------- @@ -114,3 +137,4 @@ Full example: `network resource delete`_ .. _network resource create: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/network/create.py .. _network resource delete: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/network/delete.py .. _network resource list: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/network/list.py +.. _network security group create: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/network/security_group_rules.py diff --git a/examples/network/security_group_rules.py b/examples/network/security_group_rules.py new file mode 100644 index 000000000..b2cf42dda --- /dev/null +++ b/examples/network/security_group_rules.py @@ -0,0 +1,57 @@ +# 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. + +""" +Create resources with the Network service. + +For a full guide see TODO(etoews):link to docs on developer.openstack.org +""" + + +def open_port(conn): + print("Open a port:") + + example_sec_group = conn.network.create_security_group( + name='openstacksdk-example-security-group') + + print(example_sec_group) + + example_rule = conn.network.create_security_group_rule( + security_group_id=example_sec_group.id, + direction='ingress', + remote_ip_prefix='0.0.0.0/0', + protocol='HTTPS', + port_range_max='443', + port_range_min='443', + ethertype='IPv4') + + print(example_rule) + + +def allow_ping(conn): + print("Allow pings:") + + example_sec_group = conn.network.create_security_group( + name='openstacksdk-example-security-group2') + + print(example_sec_group) + + example_rule = conn.network.create_security_group_rule( + security_group_id=example_sec_group.id, + direction='ingress', + remote_ip_prefix='0.0.0.0/0', + protocol='icmp', + port_range_max=None, + port_range_min=None, + ethertype='IPv4') + + print(example_rule) diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 6e1f6a525..aaea030b8 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -48,6 +48,7 @@ from openstack.network.v2 import subnet_pool as _subnet_pool from openstack.network.v2 import vpn_service as _vpn_service from openstack import proxy2 +from openstack import utils class Proxy(proxy2.BaseProxy): @@ -2400,6 +2401,8 @@ def update_security_group(self, security_group, **attrs): return self._update(_security_group.SecurityGroup, security_group, **attrs) + @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", + details="See the Network user guide for an example") def security_group_open_port(self, sgid, port, protocol='tcp'): rule = { 'direction': 'ingress', @@ -2412,6 +2415,8 @@ def security_group_open_port(self, sgid, port, protocol='tcp'): } return self.create_security_group_rule(**rule) + @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", + details="See the Network user guide for an example") def security_group_allow_ping(self, sgid): rule = { 'direction': 'ingress', diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 5648d878d..3d4121676 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import deprecation import mock import uuid @@ -788,6 +789,7 @@ def test_security_group_update(self): self.verify_update(self.proxy.update_security_group, security_group.SecurityGroup) + @deprecation.fail_if_not_removed def test_security_group_open_port(self): mock_class = 'openstack.network.v2._proxy.Proxy' mock_method = mock_class + '.create_security_group_rule' @@ -809,6 +811,7 @@ def test_security_group_open_port(self): } mocked.assert_called_with(**expected_args) + @deprecation.fail_if_not_removed def test_security_group_allow_ping(self): mock_class = 'openstack.network.v2._proxy.Proxy' mock_method = mock_class + '.create_security_group_rule' From 508061a2a04ace3036c30ab5e5b494d408deae7a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 12 Feb 2017 09:55:25 -0600 Subject: [PATCH 1271/3836] Wait for volumes to detach before deleting them In our functional tests, we keep hitting errors with deleting volumes. It would stand to reason that the problem is that we try to delete them too quickly after deleting the server so cinder doens't yet know they're detached. Put in a wait loop to try to get to a state where the volume can be deleted. If this works, it may be worth adding a method to shade itself to expose this - or alternatively a wait_for_detach flag to delete server that would not return until volumes are also detached. Or to add a "delete_volumes" flag to delete server. However, deleting volumes involves deleting data, so maybe it's fine. Change-Id: Ic460b6bc7f65e8761b2d765b88502ef6b5a79973 --- shade/tests/functional/test_compute.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index b0a33794b..4c34576ef 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -200,10 +200,25 @@ def test_create_boot_from_volume_image(self): self.assertEqual(True, volume['bootable']) self.assertEqual(server['id'], volume['attachments'][0]['server_id']) self.assertTrue(self.user_cloud.delete_server(server.id, wait=True)) + self._wait_for_detach(volume.id) self.assertTrue(self.user_cloud.delete_volume(volume.id, wait=True)) self.assertIsNone(self.user_cloud.get_server(server.id)) self.assertIsNone(self.user_cloud.get_volume(volume.id)) + def _wait_for_detach(self, volume_id): + # Volumes do not show up as unattached for a bit immediately after + # deleting a server that had had a volume attached. Yay for eventual + # consistency! + for count in _utils._iterate_timeout( + 60, + 'Timeout waiting for volume {volume_id} to detach'.format( + volume_id=volume_id)): + volume = self.user_cloud.get_volume(volume_id) + if volume.status in ( + 'available', 'error', + 'error_restoring', 'error_extending'): + return + def test_create_terminate_volume_image(self): if not self.user_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') @@ -232,6 +247,7 @@ def test_create_boot_from_volume_preexisting(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) volume = self.user_cloud.create_volume( size=1, name=self.server_name, image=self.image, wait=True) + self.addCleanup(self.user_cloud.delete_volume, volume.id) server = self.user_cloud.create_server( name=self.server_name, image=None, @@ -242,12 +258,12 @@ def test_create_boot_from_volume_preexisting(self): volume_id = self._assert_volume_attach(server, volume_id=volume['id']) self.assertTrue( self.user_cloud.delete_server(self.server_name, wait=True)) - self.addCleanup(self.user_cloud.delete_volume, volume_id) volume = self.user_cloud.get_volume(volume_id) self.assertIsNotNone(volume) self.assertEqual(volume['name'], volume['display_name']) self.assertEqual(True, volume['bootable']) self.assertEqual([], volume['attachments']) + self._wait_for_detach(volume.id) self.assertTrue(self.user_cloud.delete_volume(volume_id)) self.assertIsNone(self.user_cloud.get_server(self.server_name)) self.assertIsNone(self.user_cloud.get_volume(volume_id)) @@ -274,6 +290,7 @@ def test_create_boot_attach_volume(self): self.assertIsNotNone(volume) self.assertEqual(volume['name'], volume['display_name']) self.assertEqual([], volume['attachments']) + self._wait_for_detach(volume.id) self.assertTrue(self.user_cloud.delete_volume(volume_id)) self.assertIsNone(self.user_cloud.get_server(self.server_name)) self.assertIsNone(self.user_cloud.get_volume(volume_id)) From cbb38f38bceee7977952651bd0466902c087de8d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 9 Feb 2017 13:13:58 -0600 Subject: [PATCH 1272/3836] Add helper scripts to print version discovery info These are simple scripts I made to investigate things. Each show the version discovery info for all of the clouds in a clouds.yaml. Change-Id: I742a59c737c53c05851015b9734c7aa85a5466ca --- tools/keystone_version.py | 89 +++++++++++++++++++++++++++++++++++++++ tools/nova_version.py | 56 ++++++++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 tools/keystone_version.py create mode 100644 tools/nova_version.py diff --git a/tools/keystone_version.py b/tools/keystone_version.py new file mode 100644 index 000000000..a81bdabf2 --- /dev/null +++ b/tools/keystone_version.py @@ -0,0 +1,89 @@ +# Copyright (c) 2017 Red Hat, 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 os_client_config +import pprint +import sys +import urlparse + + +def print_versions(r): + if 'version' in r: + for version in r['version']: + print_version(version) + if 'values' in r: + for version in r['values']: + print_version(version) + if isinstance(r, list): + for version in r: + print_version(version) + + +def print_version(version): + if version['status'] in ('CURRENT', 'stable'): + print( + "\tVersion ID: {id} updated {updated}".format( + id=version.get('id'), + updated=version.get('updated'))) + + +verbose = '-v' in sys.argv +ran = [] +for cloud in os_client_config.OpenStackConfig().get_all_clouds(): + if cloud.name in ran: + continue + ran.append(cloud.name) + # We don't actually need a compute client - but we'll be getting full urls + # anyway. Without this SSL cert info becomes wrong. + c = cloud.get_session_client('compute') + endpoint = cloud.config['auth']['auth_url'] + try: + print(endpoint) + r = c.get(endpoint).json() + if verbose: + pprint.pprint(r) + except Exception as e: + print("Error with {cloud}: {e}".format(cloud=cloud.name, e=str(e))) + continue + if 'version' in r: + print_version(r['version']) + url = urlparse.urlparse(endpoint) + parts = url.path.split(':') + if len(parts) == 2: + path, port = parts + else: + path = url.path + port = None + stripped = path.rsplit('/', 2)[0] + if port: + stripped = '{stripped}:{port}'.format(stripped=stripped, port=port) + endpoint = urlparse.urlunsplit( + (url.scheme, url.netloc, stripped, url.params, url.query)) + print(" also {endpoint}".format(endpoint=endpoint)) + try: + r = c.get(endpoint).json() + if verbose: + pprint.pprint(r) + except Exception: + print("\tUnauthorized") + continue + if 'version' in r: + print_version(r) + elif 'versions' in r: + print_versions(r['versions']) + else: + print("\n\nUNKNOWN\n\n{r}".format(r=r)) + else: + print_versions(r['versions']) diff --git a/tools/nova_version.py b/tools/nova_version.py new file mode 100644 index 000000000..20603db41 --- /dev/null +++ b/tools/nova_version.py @@ -0,0 +1,56 @@ +# Copyright (c) 2017 Red Hat, 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 os_client_config + +ran = [] +for cloud in os_client_config.OpenStackConfig().get_all_clouds(): + if cloud.name in ran: + continue + ran.append(cloud.name) + c = cloud.get_session_client('compute') + try: + raw_endpoint = c.get_endpoint() + have_current = False + endpoint = raw_endpoint.rsplit('/', 2)[0] + print(endpoint) + r = c.get(endpoint).json() + except Exception: + print("Error with %s" % cloud.name) + continue + for version in r['versions']: + if version['status'] == 'CURRENT': + have_current = True + print( + "\tVersion ID: {id} updated {updated}".format( + id=version.get('id'), + updated=version.get('updated'))) + print( + "\tVersion Max: {max}".format(max=version.get('version'))) + print( + "\tVersion Min: {min}".format(min=version.get('min_version'))) + if not have_current: + for version in r['versions']: + if version['status'] == 'SUPPORTED': + have_current = True + print( + "\tVersion ID: {id} updated {updated}".format( + id=version.get('id'), + updated=version.get('updated'))) + print( + "\tVersion Max: {max}".format(max=version.get('version'))) + print( + "\tVersion Min: {min}".format( + min=version.get('min_version'))) From 0e039e67c2e3496628141b7b48aff2ff541096c6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 15 Feb 2017 09:31:12 -0600 Subject: [PATCH 1273/3836] Add support for overriding mistral service type The mistral team copied the heinous pervsion that the cinder team propagated upon the world and appended a version to their service_type. That's ok - there is nice copy-pastable code here we can use to prevent users from feeling the pain. Change-Id: Icf280f932014e4d9abeab3e944aece125988562e --- os_client_config/cloud_config.py | 4 ++++ os_client_config/defaults.json | 3 ++- os_client_config/tests/test_cloud_config.py | 5 +++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index f52756fd8..5c4c03e52 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -161,11 +161,15 @@ def get_service_type(self, service_type): # Of course, if the user requests a volumev2, that structure should # still work. # What's even more amazing is that they did it AGAIN with cinder v3 + # And then I learned that mistral copied it. if service_type == 'volume': if self.get_api_version(service_type).startswith('2'): service_type = 'volumev2' elif self.get_api_version(service_type).startswith('3'): service_type = 'volumev3' + elif service_type == 'workflow': + if self.get_api_version(service_type).startswith('2'): + service_type = 'workflowv2' return self.config.get(key, service_type) def get_service_name(self, service_type): diff --git a/os_client_config/defaults.json b/os_client_config/defaults.json index 65f896151..2a195c426 100644 --- a/os_client_config/defaults.json +++ b/os_client_config/defaults.json @@ -22,5 +22,6 @@ "orchestration_api_version": "1", "secgroup_source": "neutron", "status": "active", - "volume_api_version": "2" + "volume_api_version": "2", + "workflow_api_version": "2" } diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index 6f960e6e1..ce724cb56 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -163,6 +163,11 @@ def test_volume_override_v3(self): cc.config['volume_api_version'] = '3' self.assertEqual('volumev3', cc.get_service_type('volume')) + def test_workflow_override_v2(self): + cc = cloud_config.CloudConfig("test1", "region-al", fake_services_dict) + cc.config['workflow_api_version'] = '2' + self.assertEqual('workflowv2', cc.get_service_type('workflow')) + def test_get_session_no_auth(self): config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) From 486b59534a1fc5c42e548b5c53d4b3400f8f8845 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Wed, 15 Feb 2017 10:34:56 -0500 Subject: [PATCH 1274/3836] Remove two remaining doc warnings Before I begin a mass doc reorganization I wanted to get these warnings out of the way so they don't get dragged on. Change-Id: Ibefa3d18fda70eafc98108050a89c0d05aaa673c --- openstack/cluster/v1/_proxy.py | 3 ++- openstack/identity/v3/_proxy.py | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openstack/cluster/v1/_proxy.py b/openstack/cluster/v1/_proxy.py index 2df9efb5a..f0415b3b2 100644 --- a/openstack/cluster/v1/_proxy.py +++ b/openstack/cluster/v1/_proxy.py @@ -312,8 +312,9 @@ def cluster_del_nodes(self, cluster, nodes, **params): :param nodes: List of nodes to be removed from the cluster. :param kwargs \*\*params: Optional query parameters to be sent to restrict the nodes to be returned. Available parameters include: + * destroy_after_deletion: A boolean value indicating whether the - deleted nodes to be destroyed right away. + deleted nodes to be destroyed right away. :returns: A dict containing the action initiated by this operation. """ return self.remove_nodes_from_cluster(cluster, nodes, **params) diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 81e295ee1..29f90f12b 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -944,9 +944,8 @@ def role_assignments_filter(self, domain=None, project=None, group=None, paginated=False, project_id=project.id, user_id=user.id) def role_assignments(self, **query): - """Retrieve a generator of role_assignment + """Retrieve a generator of role assignments - :class:`~openstack.identity.v3.user.User` instance. :param kwargs \*\*query: Optional query parameters to be sent to limit the resources being returned. The options are: group_id, role_id, scope_domain_id, From e6755872ada4978f585bdf15edf623dbcf72c4ee Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 15 Feb 2017 10:30:12 -0600 Subject: [PATCH 1275/3836] Remove the keystoneclient auth fallback OSC doesn't use this codepath anyway (to my knowledge) and it masks errors in exceptionally strange ways. Change-Id: I15ec5aacb037813a98ac9ea8e9504a5d1cc90837 --- os_client_config/config.py | 71 ++------------------------- os_client_config/tests/test_config.py | 12 ++--- os_client_config/tests/test_init.py | 6 ++- test-requirements.txt | 1 - 4 files changed, 13 insertions(+), 77 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 9b4a709fd..de8dd3806 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -861,59 +861,6 @@ def _get_auth_loader(self, config): config['auth_type'] = 'admin_token' return loading.get_plugin_loader(config['auth_type']) - def _validate_auth_ksc(self, config, cloud): - try: - import keystoneclient.auth as ksc_auth - except ImportError: - return config - - # May throw a keystoneclient.exceptions.NoMatchingPlugin - plugin_options = ksc_auth.get_plugin_class( - config['auth_type']).get_options() - - for p_opt in plugin_options: - # if it's in config.auth, win, kill it from config dict - # if it's in config and not in config.auth, move it - # deprecated loses to current - # provided beats default, deprecated or not - winning_value = self._find_winning_auth_value( - p_opt, - config['auth'], - ) - if not winning_value: - winning_value = self._find_winning_auth_value( - p_opt, - config, - ) - - # 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: - raise exceptions.OpenStackConfigException( - 'Unable to find auth information for cloud' - ' {cloud} in config files {files}' - ' or environment variables. Missing value {auth_key}' - ' required for auth plugin {plugin}'.format( - cloud=cloud, files=','.join(self._config_files), - 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_opts]: - 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 - - return config - def _validate_auth(self, config, loader): # May throw a keystoneauth1.exceptions.NoMatchingPlugin @@ -1107,21 +1054,9 @@ def get_one_cloud(self, cloud=None, validate=True, config = self.auth_config_hook(config) if validate: - try: - loader = self._get_auth_loader(config) - config = self._validate_auth(config, loader) - auth_plugin = loader.load_from_options(**config['auth']) - except Exception as e: - # We WANT the ksa exception normally - # but OSC can't handle it right now, so we try deferring - # to ksc. If that ALSO fails, it means there is likely - # a deeper issue, so we assume the ksa error was correct - self.log.debug("Deferring keystone exception: {e}".format(e=e)) - auth_plugin = None - try: - config = self._validate_auth_ksc(config, cloud) - except Exception: - raise e + loader = self._get_auth_loader(config) + config = self._validate_auth(config, loader) + auth_plugin = loader.load_from_options(**config['auth']) else: auth_plugin = None diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index ad3685ab1..aa5935acf 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -226,7 +226,7 @@ def test_only_secure_yaml(self): c = config.OpenStackConfig(config_files=['nonexistent'], vendor_files=['nonexistent'], secure_files=[self.secure_yaml]) - cc = c.get_one_cloud(cloud='_test_cloud_no_vendor') + cc = c.get_one_cloud(cloud='_test_cloud_no_vendor', validate=False) self.assertEqual('testpass', cc.auth['password']) def test_get_cloud_names(self): @@ -384,7 +384,7 @@ def test_get_one_cloud_argparse(self): vendor_files=[self.vendor_yaml]) cc = c.get_one_cloud( - cloud='_test_cloud_regions', argparse=self.options) + cloud='_test_cloud_regions', argparse=self.options, validate=False) self.assertEqual(cc.region_name, 'region2') self.assertEqual(cc.snack_type, 'cookie') @@ -481,7 +481,7 @@ def test_get_one_cloud_just_argparse(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud(argparse=self.options) + cc = c.get_one_cloud(argparse=self.options, validate=False) self.assertIsNone(cc.cloud) self.assertEqual(cc.region_name, 'region2') self.assertEqual(cc.snack_type, 'cookie') @@ -490,7 +490,7 @@ def test_get_one_cloud_just_kwargs(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud(**self.args) + cc = c.get_one_cloud(validate=False, **self.args) self.assertIsNone(cc.cloud) self.assertEqual(cc.region_name, 'region2') self.assertEqual(cc.snack_type, 'cookie') @@ -622,7 +622,7 @@ def test_env_argparse_precedence(self): vendor_files=[self.vendor_yaml]) cc = c.get_one_cloud( - cloud='envvars', argparse=self.options) + cloud='envvars', argparse=self.options, validate=False) self.assertEqual(cc.auth['project_name'], 'project') def test_argparse_default_no_token(self): @@ -650,7 +650,7 @@ def test_argparse_token(self): opts, _remain = parser.parse_known_args( ['--os-auth-token', 'very-bad-things', '--os-auth-type', 'token']) - cc = c.get_one_cloud(argparse=opts) + cc = c.get_one_cloud(argparse=opts, validate=False) self.assertEqual(cc.config['auth_type'], 'token') self.assertEqual(cc.config['auth']['token'], 'very-bad-things') diff --git a/os_client_config/tests/test_init.py b/os_client_config/tests/test_init.py index 15d57f717..5b4fab998 100644 --- a/os_client_config/tests/test_init.py +++ b/os_client_config/tests/test_init.py @@ -18,7 +18,8 @@ class TestInit(base.TestCase): def test_get_config_without_arg_parser(self): - cloud_config = os_client_config.get_config(options=None) + cloud_config = os_client_config.get_config( + options=None, validate=False) self.assertIsInstance( cloud_config, os_client_config.cloud_config.CloudConfig @@ -26,7 +27,8 @@ def test_get_config_without_arg_parser(self): def test_get_config_with_arg_parser(self): cloud_config = os_client_config.get_config( - options=argparse.ArgumentParser()) + options=argparse.ArgumentParser(), + validate=False) self.assertIsInstance( cloud_config, os_client_config.cloud_config.CloudConfig diff --git a/test-requirements.txt b/test-requirements.txt index f9908d6e0..dd58260f4 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -11,7 +11,6 @@ fixtures>=0.3.14 jsonschema>=2.0.0,<3.0.0,!=2.5.0 mock>=1.2 python-glanceclient>=0.18.0 -python-keystoneclient>=1.1.0 python-subunit>=0.0.18 sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 oslosphinx>=2.5.0,<2.6.0 # Apache-2.0 From 9260f8404ca0cfa7260638bceede0839fff94bcf Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Wed, 15 Feb 2017 11:32:18 -0500 Subject: [PATCH 1276/3836] Deprecate "wait_for" methods on ProxyBase The wait_for_status and wait_for_delete methods on ProxyBase end up attached to every service-specific proxy, though they're not generally applicable. The proxy methods as they are just simply expose what's available on the resource base class, so any direct users in a proxy subclass should instead implement this on their own when it's needed. Currently, only compute uses this kind of functionality, and it rightly implements it itself by going to the resource2 module and calling the module-level functions. Internally, we do have a few uses of the wait_for methods attached to proxies, though they're all in functional tests (for block_store, compute, and orchestration). We should consider exposing those methods directly on those proxies rather than exposing the methods through the base class to dozens of services that don't have those needs. These should be removed for 1.0. They're also currently generating warnings on every service's documentation via the enforcer tool for not being documented (which was how this was originally found). Change-Id: I4733161959ab282915f1cc2956245a377417fced --- openstack/proxy2.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openstack/proxy2.py b/openstack/proxy2.py index 6d528d8d0..350b5491d 100644 --- a/openstack/proxy2.py +++ b/openstack/proxy2.py @@ -12,6 +12,7 @@ from openstack import exceptions from openstack import resource2 +from openstack import utils # The _check_resource decorator is used on BaseProxy methods to ensure that @@ -269,6 +270,11 @@ def _head(self, resource_type, value=None, **attrs): res = self._get_resource(resource_type, value, **attrs) return res.head(self.session) + @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", + details=("This is no longer a part of the proxy base, " + "service-specific subclasses should expose " + "this as needed. See resource2.wait_for_status " + "for this behavior")) def wait_for_status(self, value, status, failures=[], interval=2, wait=120): """Wait for a resource to be in a particular status. @@ -293,6 +299,11 @@ def wait_for_status(self, value, status, failures=[], interval=2, return resource2.wait_for_status(self.session, value, status, failures, interval, wait) + @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", + details=("This is no longer a part of the proxy base, " + "service-specific subclasses should expose " + "this as needed. See resource2.wait_for_delete " + "for this behavior")) def wait_for_delete(self, value, interval=2, wait=120): """Wait for the resource to be deleted. From 87c253c03f4c931e01d2c26e531902e4f8f89401 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Wed, 15 Feb 2017 13:25:33 -0500 Subject: [PATCH 1277/3836] Privatize session instance on Proxy subclasses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each Proxy subclass has a session, but it's currently a public name, and as such is accessible. Making it accessible is sort of a low risk thing, but isn't really necessary—if someone needs a session instance they will perhaps be creating their own which they pass into a Connection, or they would be more likely to get the session off of a Connection instance (which is fine being public there, I think). This came up in the proxy doc work I'm doing, where every proxy has a warning for not documenting the `session` name. At the proxy level it really should just be a private name and a part of the implementation. This change basically does a s/self.session/self._session/ Change-Id: Iccb423aad801115d593a6ad5ead408f48054c5d4 --- openstack/cluster/v1/_proxy.py | 30 ++++---- openstack/compute/v2/_proxy.py | 77 ++++++++++--------- openstack/image/v2/_proxy.py | 12 +-- openstack/message/v1/_proxy.py | 6 +- openstack/message/v2/_proxy.py | 2 +- openstack/network/v2/_proxy.py | 28 +++---- openstack/object_store/v1/_proxy.py | 16 ++-- openstack/orchestration/v1/_proxy.py | 4 +- openstack/proxy.py | 22 +++--- openstack/proxy2.py | 20 ++--- .../tests/unit/orchestration/v1/test_proxy.py | 6 +- 11 files changed, 112 insertions(+), 111 deletions(-) diff --git a/openstack/cluster/v1/_proxy.py b/openstack/cluster/v1/_proxy.py index f0415b3b2..9a517aeb6 100644 --- a/openstack/cluster/v1/_proxy.py +++ b/openstack/cluster/v1/_proxy.py @@ -300,7 +300,7 @@ def add_nodes_to_cluster(self, cluster, nodes): obj = cluster else: obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) - return obj.add_nodes(self.session, nodes) + return obj.add_nodes(self._session, nodes) @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", details="Use remove_nodes_from_cluster instead") @@ -336,7 +336,7 @@ def remove_nodes_from_cluster(self, cluster, nodes, **params): obj = cluster else: obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) - return obj.del_nodes(self.session, nodes, **params) + return obj.del_nodes(self._session, nodes, **params) @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", details="Use replace_nodes_in_cluster instead") @@ -362,7 +362,7 @@ def replace_nodes_in_cluster(self, cluster, nodes): obj = cluster else: obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) - return obj.replace_nodes(self.session, nodes) + return obj.replace_nodes(self._session, nodes) @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", details="Use scale_out_cluster instead") @@ -390,7 +390,7 @@ def scale_out_cluster(self, cluster, count=None): obj = cluster else: obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) - return obj.scale_out(self.session, count) + return obj.scale_out(self._session, count) @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", details="Use scale_in_cluster instead") @@ -418,7 +418,7 @@ def scale_in_cluster(self, cluster, count=None): obj = cluster else: obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) - return obj.scale_in(self.session, count) + return obj.scale_in(self._session, count) @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", details="Use resize_cluster instead") @@ -446,7 +446,7 @@ def resize_cluster(self, cluster, **params): obj = cluster else: obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) - return obj.resize(self.session, **params) + return obj.resize(self._session, **params) @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", details="Use attach_policy_to_cluster instead") @@ -476,7 +476,7 @@ def attach_policy_to_cluster(self, cluster, policy, **params): obj = cluster else: obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) - return obj.policy_attach(self.session, policy, **params) + return obj.policy_attach(self._session, policy, **params) @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", details="Use detach_policy_from_cluster instead") @@ -502,7 +502,7 @@ def detach_policy_from_cluster(self, cluster, policy): obj = cluster else: obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) - return obj.policy_detach(self.session, policy) + return obj.policy_detach(self._session, policy) @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", details="Use update_cluster_policy instead") @@ -532,7 +532,7 @@ def update_cluster_policy(self, cluster, policy, **params): obj = cluster else: obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) - return obj.policy_update(self.session, policy, **params) + return obj.policy_update(self._session, policy, **params) def collect_cluster_attrs(self, cluster, path): """Collect attribute values across a cluster. @@ -557,7 +557,7 @@ def check_cluster(self, cluster, **params): :returns: A dictionary containing the action ID. """ obj = self._get_resource(_cluster.Cluster, cluster) - return obj.check(self.session, **params) + return obj.check(self._session, **params) def recover_cluster(self, cluster, **params): """recover a cluster. @@ -570,7 +570,7 @@ def recover_cluster(self, cluster, **params): :returns: A dictionary containing the action ID. """ obj = self._get_resource(_cluster.Cluster, cluster) - return obj.recover(self.session, **params) + return obj.recover(self._session, **params) @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", details="Use perform_operation_on_cluster instead") @@ -599,7 +599,7 @@ def perform_operation_on_cluster(self, cluster, operation, **params): :returns: A dictionary containing the action ID. """ obj = self._get_resource(_cluster.Cluster, cluster) - return obj.op(self.session, operation, **params) + return obj.op(self._session, operation, **params) def create_node(self, **attrs): """Create a new node from attributes. @@ -707,7 +707,7 @@ def check_node(self, node, **params): :returns: A dictionary containing the action ID. """ obj = self._get_resource(_node.Node, node) - return obj.check(self.session, **params) + return obj.check(self._session, **params) def recover_node(self, node, **params): """Recover the specified node into healthy status. @@ -719,7 +719,7 @@ def recover_node(self, node, **params): :returns: A dictionary containing the action ID. """ obj = self._get_resource(_node.Node, node) - return obj.recover(self.session, **params) + return obj.recover(self._session, **params) @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", details="Use perform_operation_on_node instead") @@ -748,7 +748,7 @@ def perform_operation_on_node(self, node, operation, **params): :returns: A dictionary containing the action ID. """ obj = self._get_resource(_node.Node, node) - return obj.op(self.session, operation, **params) + return obj.op(self._session, operation, **params) def create_policy(self, **attrs): """Create a new policy from attributes. diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index d20af42d3..1fd9bf24f 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -197,7 +197,7 @@ def get_image_metadata(self, image): :rtype: :class:`~openstack.compute.v2.image.Image` """ res = self._get_base_resource(image, _image.Image) - metadata = res.get_metadata(self.session) + metadata = res.get_metadata(self._session) result = _image.Image.existing(id=res.id, metadata=metadata) return result @@ -218,7 +218,7 @@ def set_image_metadata(self, image, **metadata): :rtype: :class:`~openstack.compute.v2.image.Image` """ res = self._get_base_resource(image, _image.Image) - metadata = res.set_metadata(self.session, **metadata) + metadata = res.set_metadata(self._session, **metadata) result = _image.Image.existing(id=res.id, metadata=metadata) return result @@ -236,7 +236,7 @@ def delete_image_metadata(self, image, keys): :rtype: ``None`` """ res = self._get_base_resource(image, _image.Image) - return res.delete_metadata(self.session, keys) + return res.delete_metadata(self._session, keys) def create_keypair(self, **attrs): """Create a new keypair from attributes @@ -340,7 +340,7 @@ def delete_server(self, server, ignore_missing=True, force=False): """ if force: server = self._get_resource(_server.Server, server) - server.force_delete(self.session) + server.force_delete(self._session) else: self._delete(_server.Server, server, ignore_missing=ignore_missing) @@ -433,7 +433,7 @@ def change_server_password(self, server, new_password): :returns: None """ server = self._get_resource(_server.Server, server) - server.change_password(self.session, new_password) + server.change_password(self._session, new_password) def reset_server_state(self, server, state): """Reset the state of server @@ -446,7 +446,7 @@ def reset_server_state(self, server, state): :returns: None """ res = self._get_base_resource(server, _server.Server) - res.reset_state(self.session, state) + res.reset_state(self._session, state) def reboot_server(self, server, reboot_type): """Reboot a server @@ -459,7 +459,7 @@ def reboot_server(self, server, reboot_type): :returns: None """ server = self._get_resource(_server.Server, server) - server.reboot(self.session, reboot_type) + server.reboot(self._session, reboot_type) def rebuild_server(self, server, name, admin_password, **attrs): """Rebuild a server @@ -487,7 +487,7 @@ def rebuild_server(self, server, name, admin_password, **attrs): instance. """ server = self._get_resource(_server.Server, server) - return server.rebuild(self.session, name, admin_password, **attrs) + return server.rebuild(self._session, name, admin_password, **attrs) def resize_server(self, server, flavor): """Resize a server @@ -501,7 +501,7 @@ def resize_server(self, server, flavor): """ server = self._get_resource(_server.Server, server) flavor_id = resource2.Resource._get_id(flavor) - server.resize(self.session, flavor_id) + server.resize(self._session, flavor_id) def confirm_server_resize(self, server): """Confirm a server resize @@ -512,7 +512,7 @@ def confirm_server_resize(self, server): :returns: None """ server = self._get_resource(_server.Server, server) - server.confirm_resize(self.session) + server.confirm_resize(self._session) def revert_server_resize(self, server): """Revert a server resize @@ -523,7 +523,7 @@ def revert_server_resize(self, server): :returns: None """ server = self._get_resource(_server.Server, server) - server.revert_resize(self.session) + server.revert_resize(self._session) def create_server_image(self, server, name, metadata=None): """Create an image from a server @@ -536,7 +536,7 @@ def create_server_image(self, server, name, metadata=None): :returns: None """ server = self._get_resource(_server.Server, server) - server.create_image(self.session, name, metadata) + server.create_image(self._session, name, metadata) def add_security_group_to_server(self, server, security_group): """Add a security group to a server @@ -551,7 +551,7 @@ def add_security_group_to_server(self, server, security_group): """ server = self._get_resource(_server.Server, server) security_group_id = resource2.Resource._get_id(security_group) - server.add_security_group(self.session, security_group_id) + server.add_security_group(self._session, security_group_id) def remove_security_group_from_server(self, server, security_group): """Add a security group to a server @@ -566,7 +566,7 @@ def remove_security_group_from_server(self, server, security_group): """ server = self._get_resource(_server.Server, server) security_group_id = resource2.Resource._get_id(security_group) - server.remove_security_group(self.session, security_group_id) + server.remove_security_group(self._session, security_group_id) def add_fixed_ip_to_server(self, server, network_id): """Adds a fixed IP address to a server instance. @@ -578,7 +578,7 @@ def add_fixed_ip_to_server(self, server, network_id): :returns: None """ server = self._get_resource(_server.Server, server) - server.add_fixed_ip(self.session, network_id) + server.add_fixed_ip(self._session, network_id) def remove_fixed_ip_from_server(self, server, address): """Removes a fixed IP address from a server instance. @@ -590,7 +590,7 @@ def remove_fixed_ip_from_server(self, server, address): :returns: None """ server = self._get_resource(_server.Server, server) - server.remove_fixed_ip(self.session, address) + server.remove_fixed_ip(self._session, address) def add_floating_ip_to_server(self, server, address, fixed_address=None): """Adds a floating IP address to a server instance. @@ -604,7 +604,7 @@ def add_floating_ip_to_server(self, server, address, fixed_address=None): :returns: None """ server = self._get_resource(_server.Server, server) - server.add_floating_ip(self.session, address, + server.add_floating_ip(self._session, address, fixed_address=fixed_address) def remove_floating_ip_from_server(self, server, address): @@ -617,7 +617,7 @@ def remove_floating_ip_from_server(self, server, address): :returns: None """ server = self._get_resource(_server.Server, server) - server.remove_floating_ip(self.session, address) + server.remove_floating_ip(self._session, address) def pause_server(self, server): """Pauses a server and changes its status to ``PAUSED``. @@ -627,7 +627,7 @@ def pause_server(self, server): :returns: None """ server = self._get_resource(_server.Server, server) - server.pause(self.session) + server.pause(self._session) def unpause_server(self, server): """Unpauses a paused server and changes its status to ``ACTIVE``. @@ -637,7 +637,7 @@ def unpause_server(self, server): :returns: None """ server = self._get_resource(_server.Server, server) - server.unpause(self.session) + server.unpause(self._session) def suspend_server(self, server): """Suspends a server and changes its status to ``SUSPENDED``. @@ -647,7 +647,7 @@ def suspend_server(self, server): :returns: None """ server = self._get_resource(_server.Server, server) - server.suspend(self.session) + server.suspend(self._session) def resume_server(self, server): """Resumes a suspended server and changes its status to ``ACTIVE``. @@ -657,7 +657,7 @@ def resume_server(self, server): :returns: None """ server = self._get_resource(_server.Server, server) - server.resume(self.session) + server.resume(self._session) def lock_server(self, server): """Locks a server. @@ -667,7 +667,7 @@ def lock_server(self, server): :returns: None """ server = self._get_resource(_server.Server, server) - server.lock(self.session) + server.lock(self._session) def unlock_server(self, server): """Unlocks a locked server. @@ -677,7 +677,7 @@ def unlock_server(self, server): :returns: None """ server = self._get_resource(_server.Server, server) - server.unlock(self.session) + server.unlock(self._session) def rescue_server(self, server, admin_pass=None, image_ref=None): """Puts a server in rescue mode and changes it status to ``RESCUE``. @@ -694,7 +694,8 @@ def rescue_server(self, server, admin_pass=None, image_ref=None): :returns: None """ server = self._get_resource(_server.Server, server) - server.rescue(self.session, admin_pass=admin_pass, image_ref=image_ref) + server.rescue(self._session, admin_pass=admin_pass, + image_ref=image_ref) def unrescue_server(self, server): """Unrescues a server and changes its status to ``ACTIVE``. @@ -704,7 +705,7 @@ def unrescue_server(self, server): :returns: None """ server = self._get_resource(_server.Server, server) - server.unrescue(self.session) + server.unrescue(self._session) def evacuate_server(self, server, host=None, admin_pass=None, force=None): """Evacuates a server from a failed host to a new host. @@ -721,7 +722,7 @@ def evacuate_server(self, server, host=None, admin_pass=None, force=None): :returns: None """ server = self._get_resource(_server.Server, server) - server.evacuate(self.session, host=host, admin_pass=admin_pass, + server.evacuate(self._session, host=host, admin_pass=admin_pass, force=force) def start_server(self, server): @@ -732,7 +733,7 @@ def start_server(self, server): :returns: None """ server = self._get_resource(_server.Server, server) - server.start(self.session) + server.start(self._session) def stop_server(self, server): """Stops a running server and changes its state to ``SHUTOFF``. @@ -742,7 +743,7 @@ def stop_server(self, server): :returns: None """ server = self._get_resource(_server.Server, server) - server.stop(self.session) + server.stop(self._session) def shelve_server(self, server): """Shelves a server. @@ -757,7 +758,7 @@ def shelve_server(self, server): :returns: None """ server = self._get_resource(_server.Server, server) - server.shelve(self.session) + server.shelve(self._session) def unshelve_server(self, server): """Unselves or restores a shelved server. @@ -771,11 +772,11 @@ def unshelve_server(self, server): :returns: None """ server = self._get_resource(_server.Server, server) - server.unshelve(self.session) + server.unshelve(self._session) def wait_for_server(self, server, status='ACTIVE', failures=['ERROR'], interval=2, wait=120): - return resource2.wait_for_status(self.session, server, status, + return resource2.wait_for_status(self._session, server, status, failures, interval, wait) def create_server_interface(self, server, **attrs): @@ -907,7 +908,7 @@ def get_server_metadata(self, server): :rtype: :class:`~openstack.compute.v2.server.Server` """ res = self._get_base_resource(server, _server.Server) - metadata = res.get_metadata(self.session) + metadata = res.get_metadata(self._session) result = _server.Server.existing(id=res.id, metadata=metadata) return result @@ -928,7 +929,7 @@ def set_server_metadata(self, server, **metadata): :rtype: :class:`~openstack.compute.v2.server.Server` """ res = self._get_base_resource(server, _server.Server) - metadata = res.set_metadata(self.session, **metadata) + metadata = res.set_metadata(self._session, **metadata) result = _server.Server.existing(id=res.id, metadata=metadata) return result @@ -946,7 +947,7 @@ def delete_server_metadata(self, server, keys): :rtype: ``None`` """ res = self._get_base_resource(server, _server.Server) - return res.delete_metadata(self.session, keys) + return res.delete_metadata(self._session, keys) def create_server_group(self, **attrs): """Create a new server group from attributes @@ -1079,7 +1080,7 @@ def force_service_down(self, service, host, binary): :returns: None """ service = self._get_resource(_service.Service, service) - service.force_down(self.session, host, binary) + service.force_down(self._session, host, binary) def disable_service(self, service, host, binary, disabled_reason=None): """Disable a service @@ -1093,7 +1094,7 @@ def disable_service(self, service, host, binary, disabled_reason=None): :returns: None """ service = self._get_resource(_service.Service, service) - service.disable(self.session, + service.disable(self._session, host, binary, disabled_reason) @@ -1109,7 +1110,7 @@ def enable_service(self, service, host, binary): :returns: None """ service = self._get_resource(_service.Service, service) - service.enable(self.session, host, binary) + service.enable(self._session, host, binary) def services(self): """Return a generator of service diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 8d6ab6793..410d2f13d 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -56,7 +56,7 @@ def upload_image(self, container_format=None, disk_format=None, # return anything anyway. Otherwise this blocks while uploading # significant amounts of image data. img.data = data - img.upload(self.session) + img.upload(self._session) return img @@ -69,7 +69,7 @@ def download_image(self, image): :returns: The bytes comprising the given Image. """ image = self._get_resource(_image.Image, image) - return image.download(self.session) + return image.download(self._session) def delete_image(self, image, ignore_missing=True): """Delete an image @@ -145,7 +145,7 @@ def deactivate_image(self, image): :returns: None """ image = self._get_resource(_image.Image, image) - image.deactivate(self.session) + image.deactivate(self._session) def reactivate_image(self, image): """Deactivate an image @@ -156,7 +156,7 @@ def reactivate_image(self, image): :returns: None """ image = self._get_resource(_image.Image, image) - image.reactivate(self.session) + image.reactivate(self._session) def add_tag(self, image, tag): """Add a tag to an image @@ -169,7 +169,7 @@ def add_tag(self, image, tag): :returns: None """ image = self._get_resource(_image.Image, image) - image.add_tag(self.session, tag) + image.add_tag(self._session, tag) def remove_tag(self, image, tag): """Remove a tag to an image @@ -182,7 +182,7 @@ def remove_tag(self, image, tag): :returns: None """ image = self._get_resource(_image.Image, image) - image.remove_tag(self.session, tag) + image.remove_tag(self._session, tag) def add_member(self, image, **attrs): """Create a new member from attributes diff --git a/openstack/message/v1/_proxy.py b/openstack/message/v1/_proxy.py index ea6c205e6..bc034d3d6 100644 --- a/openstack/message/v1/_proxy.py +++ b/openstack/message/v1/_proxy.py @@ -55,7 +55,7 @@ def create_messages(self, values): :rtype: list messages: The list of :class:`~openstack.message.v1.message.Message`s created. """ - return message.Message.create_messages(self.session, values) + return message.Message.create_messages(self._session, values) def claim_messages(self, value): """Claims a set of messages. @@ -67,7 +67,7 @@ def claim_messages(self, value): :rtype: list messages: The list of :class:`~openstack.message.v1.message.Message`s claimed. """ - return claim.Claim.claim_messages(self.session, value) + return claim.Claim.claim_messages(self._session, value) def delete_message(self, value): """Delete a message @@ -77,4 +77,4 @@ def delete_message(self, value): :returns: ``None`` """ - message.Message.delete_by_id(self.session, value) + message.Message.delete_by_id(self._session, value) diff --git a/openstack/message/v2/_proxy.py b/openstack/message/v2/_proxy.py index 66aaee126..3fbe75bd6 100644 --- a/openstack/message/v2/_proxy.py +++ b/openstack/message/v2/_proxy.py @@ -86,7 +86,7 @@ def post_message(self, queue_name, messages): """ message = self._get_resource(_message.Message, None, queue_name=queue_name) - return message.post(self.session, messages) + return message.post(self._session, messages) def messages(self, queue_name, **query): """Retrieve a generator of messages diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index aaea030b8..a06db9db3 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -226,7 +226,7 @@ def add_dhcp_agent_to_network(self, agent, network): """ network = self._get_resource(_network.Network, network) agent = self._get_resource(_agent.Agent, agent) - return agent.add_agent_to_network(self.session, network.id) + return agent.add_agent_to_network(self._session, network.id) def remove_dhcp_agent_from_network(self, agent, network): """Remove a DHCP Agent from a network @@ -238,7 +238,7 @@ def remove_dhcp_agent_from_network(self, agent, network): """ network = self._get_resource(_network.Network, network) agent = self._get_resource(_agent.Agent, agent) - return agent.remove_agent_from_network(self.session, network.id) + return agent.remove_agent_from_network(self._session, network.id) def network_hosting_dhcp_agents(self, network, **query): """A generator of DHCP agents hosted on a network. @@ -266,7 +266,7 @@ def get_auto_allocated_topology(self, project=None): # If project option is not given, grab project id from session if project is None: - project = self.session.get_project_id() + project = self._session.get_project_id() return self._get(_auto_allocated_topology.AutoAllocatedTopology, project) @@ -287,7 +287,7 @@ def delete_auto_allocated_topology(self, project=None, # If project option is not given, grab project id from session if project is None: - project = self.session.get_project_id() + project = self._session.get_project_id() self._delete(_auto_allocated_topology.AutoAllocatedTopology, project, ignore_missing=ignore_missing) @@ -304,7 +304,7 @@ def validate_auto_allocated_topology(self, project=None): # If project option is not given, grab project id from session if project is None: - project = self.session.get_project_id() + project = self._session.get_project_id() return self._get(_auto_allocated_topology.ValidateTopology, project=project) @@ -471,7 +471,7 @@ def find_available_ip(self): :returns: One :class:`~openstack.network.v2.floating_ip.FloatingIP` or None """ - return _floating_ip.FloatingIP.find_available(self.session) + return _floating_ip.FloatingIP.find_available(self._session) def find_ip(self, name_or_id, ignore_missing=True): """Find a single IP @@ -1468,11 +1468,11 @@ def update_port(self, port, **attrs): def add_ip_to_port(self, port, ip): ip['port_id'] = port.id - return ip.update(self.session) + return ip.update(self._session) def remove_ip_from_port(self, ip): ip['port_id'] = None - return ip.update(self.session) + return ip.update(self._session) def get_subnet_ports(self, subnet_id): result = [] @@ -2211,7 +2211,7 @@ def add_interface_to_router(self, router, subnet_id=None, port_id=None): body = {'port_id': port_id} else: body = {'subnet_id': subnet_id} - return router.add_interface(self.session, **body) + return router.add_interface(self._session, **body) def remove_interface_from_router(self, router, subnet_id=None, port_id=None): @@ -2230,7 +2230,7 @@ def remove_interface_from_router(self, router, subnet_id=None, body = {'port_id': port_id} else: body = {'subnet_id': subnet_id} - return router.remove_interface(self.session, **body) + return router.remove_interface(self._session, **body) def add_gateway_to_router(self, router, **body): """Add Gateway to a router @@ -2241,7 +2241,7 @@ def add_gateway_to_router(self, router, **body): :returns: Router with updated interface :rtype: :class: `~openstack.network.v2.router.Router` """ - return router.add_gateway(self.session, **body) + return router.add_gateway(self._session, **body) def remove_gateway_from_router(self, router, **body): """Remove Gateway from a router @@ -2252,7 +2252,7 @@ def remove_gateway_from_router(self, router, **body): :returns: Router with updated interface :rtype: :class: `~openstack.network.v2.router.Router` """ - return router.remove_gateway(self.session, **body) + return router.remove_gateway(self._session, **body) def routers_hosting_l3_agents(self, router, **query): """Return a generator of L3 agent hosting a router @@ -2295,7 +2295,7 @@ def add_router_to_agent(self, agent, router): """ agent = self._get_resource(_agent.Agent, agent) router = self._get_resource(_router.Router, router) - return agent.add_router_to_agent(self.session, router.id) + return agent.add_router_to_agent(self._session, router.id) def remove_router_from_agent(self, agent, router): """Remove router from L3 agent @@ -2308,7 +2308,7 @@ def remove_router_from_agent(self, agent, router): """ agent = self._get_resource(_agent.Agent, agent) router = self._get_resource(_router.Router, router) - return agent.remove_router_from_agent(self.session, router.id) + return agent.remove_router_from_agent(self._session, router.id) def create_security_group(self, **attrs): """Create a new security group from attributes diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index a38171fce..78a48902a 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -35,7 +35,7 @@ def set_account_metadata(self, **metadata): by the user. """ account = self._get_resource(_account.Account, None) - account.set_metadata(self.session, metadata) + account.set_metadata(self._session, metadata) def delete_account_metadata(self, keys): """Delete metadata for this account. @@ -43,7 +43,7 @@ def delete_account_metadata(self, keys): :param keys: The keys of metadata to be deleted. """ account = self._get_resource(_account.Account, None) - account.delete_metadata(self.session, keys) + account.delete_metadata(self._session, keys) def containers(self, **query): """Obtain Container objects for this account. @@ -54,7 +54,7 @@ def containers(self, **query): :rtype: A generator of :class:`~openstack.object_store.v1.container.Container` objects. """ - return _container.Container.list(self.session, **query) + return _container.Container.list(self._session, **query) def create_container(self, **attrs): """Create a new container from attributes @@ -121,7 +121,7 @@ def set_container_metadata(self, container, **metadata): - `sync_key` """ res = self._get_resource(_container.Container, container) - res.set_metadata(self.session, metadata) + res.set_metadata(self._session, metadata) def delete_container_metadata(self, container, keys): """Delete metadata for a container. @@ -132,7 +132,7 @@ def delete_container_metadata(self, container, keys): :param keys: The keys of metadata to be deleted. """ res = self._get_resource(_container.Container, container) - res.delete_metadata(self.session, keys) + res.delete_metadata(self._session, keys) def objects(self, container, **query): """Return a generator that yields the Container's objects. @@ -149,7 +149,7 @@ def objects(self, container, **query): """ container = _container.Container.from_id(container) - objs = _obj.Object.list(self.session, + objs = _obj.Object.list(self._session, path_args={"container": container.name}, **query) for obj in objs: @@ -300,7 +300,7 @@ def set_object_metadata(self, obj, container=None, **metadata): container_name = self._get_container_name(obj, container) res = self._get_resource(_obj.Object, obj, path_args={"container": container_name}) - res.set_metadata(self.session, metadata) + res.set_metadata(self._session, metadata) def delete_object_metadata(self, obj, container=None, keys=None): """Delete metadata for an object. @@ -315,4 +315,4 @@ def delete_object_metadata(self, obj, container=None, keys=None): container_name = self._get_container_name(obj, container) res = self._get_resource(_obj.Object, obj, path_args={"container": container_name}) - res.delete_metadata(self.session, keys) + res.delete_metadata(self._session, keys) diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index 654cc9e3e..560219da2 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -122,7 +122,7 @@ def check_stack(self, stack): else: stk_obj = _stack.Stack.existing(id=stack) - stk_obj.check(self.session) + stk_obj.check(self._session) def resources(self, stack, **query): """Return a generator of resources @@ -295,6 +295,6 @@ def validate_template(self, template, environment=None, template_url=None, "'template_url' must be specified when template is None") tmpl = _template.Template.new() - return tmpl.validate(self.session, template, environment=environment, + return tmpl.validate(self._session, template, environment=environment, template_url=template_url, ignore_errors=ignore_errors) diff --git a/openstack/proxy.py b/openstack/proxy.py index a3f71c7a1..cbe8616ee 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -41,7 +41,7 @@ def check(self, expected, actual=None, *args, **kwargs): class BaseProxy(object): def __init__(self, session): - self.session = session + self._session = session def _get_resource(self, resource_type, value, path_args=None): """Get a resource object to work on @@ -87,7 +87,7 @@ def _find(self, resource_type, name_or_id, path_args=None, :returns: An instance of ``resource_type`` or None """ - return resource_type.find(self.session, name_or_id, + return resource_type.find(self._session, name_or_id, path_args=path_args, ignore_missing=ignore_missing) @@ -122,7 +122,7 @@ def _delete(self, resource_type, value, path_args=None, res = self._get_resource(resource_type, value, path_args) try: - rv = res.delete(self.session) + rv = res.delete(self._session) except exceptions.NotFoundException as e: if ignore_missing: return None @@ -157,7 +157,7 @@ def _update(self, resource_type, value, path_args=None, **attrs): """ res = self._get_resource(resource_type, value, path_args) res.update_attrs(attrs) - return res.update(self.session) + return res.update(self._session) def _create(self, resource_type, path_args=None, **attrs): """Create a resource from attributes @@ -176,7 +176,7 @@ def _create(self, resource_type, path_args=None, **attrs): res = resource_type.new(**attrs) if path_args is not None: res.update_attrs(path_args) - return res.create(self.session) + return res.create(self._session) @_check_resource(strict=False) def _get(self, resource_type, value=None, path_args=None, args=None): @@ -198,7 +198,7 @@ def _get(self, resource_type, value=None, path_args=None, args=None): res = self._get_resource(resource_type, value, path_args) try: - return res.get(self.session, args=args) + return res.get(self._session, args=args) except exceptions.NotFoundException as e: raise exceptions.ResourceNotFound( message="No %s found for %s" % @@ -235,8 +235,8 @@ def _list(self, resource_type, value=None, paginated=False, res = self._get_resource(resource_type, value, path_args) query = res.convert_ids(query) - return res.list(self.session, path_args=path_args, paginated=paginated, - params=query) + return res.list(self._session, path_args=path_args, + paginated=paginated, params=query) def _head(self, resource_type, value=None, path_args=None): """Retrieve a resource's header @@ -255,7 +255,7 @@ def _head(self, resource_type, value=None, path_args=None): """ res = self._get_resource(resource_type, value, path_args) - return res.head(self.session) + return res.head(self._session) def wait_for_status(self, value, status, failures=[], interval=2, wait=120): @@ -278,7 +278,7 @@ def wait_for_status(self, value, status, failures=[], interval=2, :raises: :class:`~AttributeError` if the resource does not have a status attribute """ - return resource.wait_for_status(self.session, value, status, + return resource.wait_for_status(self._session, value, status, failures, interval, wait) def wait_for_delete(self, value, interval=2, wait=120): @@ -293,4 +293,4 @@ def wait_for_delete(self, value, interval=2, wait=120): :raises: :class:`~openstack.exceptions.ResourceTimeout` transition to delete failed to occur in wait seconds. """ - return resource.wait_for_delete(self.session, value, interval, wait) + return resource.wait_for_delete(self._session, value, interval, wait) diff --git a/openstack/proxy2.py b/openstack/proxy2.py index 6d528d8d0..e018d254e 100644 --- a/openstack/proxy2.py +++ b/openstack/proxy2.py @@ -41,7 +41,7 @@ def check(self, expected, actual=None, *args, **kwargs): class BaseProxy(object): def __init__(self, session): - self.session = session + self._session = session def _get_resource(self, resource_type, value, **attrs): """Get a resource object to work on @@ -100,7 +100,7 @@ def _find(self, resource_type, name_or_id, ignore_missing=True, :returns: An instance of ``resource_type`` or None """ - return resource_type.find(self.session, name_or_id, + return resource_type.find(self._session, name_or_id, ignore_missing=ignore_missing, **attrs) @@ -135,7 +135,7 @@ def _delete(self, resource_type, value, ignore_missing=True, **attrs): res = self._get_resource(resource_type, value, **attrs) try: - rv = res.delete(self.session) + rv = res.delete(self._session) except exceptions.NotFoundException as e: if ignore_missing: return None @@ -170,7 +170,7 @@ def _update(self, resource_type, value, **attrs): :rtype: :class:`~openstack.resource2.Resource` """ res = self._get_resource(resource_type, value, **attrs) - return res.update(self.session) + return res.update(self._session) def _create(self, resource_type, **attrs): """Create a resource from attributes @@ -190,7 +190,7 @@ def _create(self, resource_type, **attrs): :rtype: :class:`~openstack.resource2.Resource` """ res = resource_type.new(**attrs) - return res.create(self.session) + return res.create(self._session) @_check_resource(strict=False) def _get(self, resource_type, value=None, requires_id=True, **attrs): @@ -214,7 +214,7 @@ def _get(self, resource_type, value=None, requires_id=True, **attrs): res = self._get_resource(resource_type, value, **attrs) try: - return res.get(self.session, requires_id=requires_id) + return res.get(self._session, requires_id=requires_id) except exceptions.NotFoundException as e: raise exceptions.ResourceNotFound( message="No %s found for %s" % @@ -247,7 +247,7 @@ def _list(self, resource_type, value=None, paginated=False, **attrs): the ``resource_type``. """ res = self._get_resource(resource_type, value, **attrs) - return res.list(self.session, paginated=paginated, **attrs) + return res.list(self._session, paginated=paginated, **attrs) def _head(self, resource_type, value=None, **attrs): """Retrieve a resource's header @@ -267,7 +267,7 @@ def _head(self, resource_type, value=None, **attrs): :rtype: :class:`~openstack.resource2.Resource` """ res = self._get_resource(resource_type, value, **attrs) - return res.head(self.session) + return res.head(self._session) def wait_for_status(self, value, status, failures=[], interval=2, wait=120): @@ -290,7 +290,7 @@ def wait_for_status(self, value, status, failures=[], interval=2, :raises: :class:`~AttributeError` if the resource does not have a status attribute """ - return resource2.wait_for_status(self.session, value, status, + return resource2.wait_for_status(self._session, value, status, failures, interval, wait) def wait_for_delete(self, value, interval=2, wait=120): @@ -305,4 +305,4 @@ def wait_for_delete(self, value, interval=2, wait=120): :raises: :class:`~openstack.exceptions.ResourceTimeout` transition to delete failed to occur in wait seconds. """ - return resource2.wait_for_delete(self.session, value, interval, wait) + return resource2.wait_for_delete(self._session, value, interval, wait) diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index 0da8050de..cac11515d 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -61,7 +61,7 @@ def test_check_stack_with_stack_object(self, mock_check): res = self.proxy.check_stack(stk) self.assertIsNone(res) - mock_check.assert_called_once_with(self.proxy.session) + mock_check.assert_called_once_with(self.proxy._session) @mock.patch.object(stack.Stack, 'existing') def test_check_stack_with_stack_ID(self, mock_stack): @@ -72,7 +72,7 @@ def test_check_stack_with_stack_ID(self, mock_stack): self.assertIsNone(res) mock_stack.assert_called_once_with(id='FAKE_ID') - stk.check.assert_called_once_with(self.proxy.session) + stk.check.assert_called_once_with(self.proxy._session) @mock.patch.object(stack.Stack, 'find') def test_resources_with_stack_object(self, mock_find): @@ -163,7 +163,7 @@ def test_validate_template(self, mock_validate): res = self.proxy.validate_template(tmpl, env, tmpl_url, ignore_errors) mock_validate.assert_called_once_with( - self.proxy.session, tmpl, environment=env, template_url=tmpl_url, + self.proxy._session, tmpl, environment=env, template_url=tmpl_url, ignore_errors=ignore_errors) self.assertEqual(mock_validate.return_value, res) From f211be85ea0a1ef4990676f2a86113e93aa72193 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Wed, 15 Feb 2017 11:03:22 -0500 Subject: [PATCH 1278/3836] Reorganize bare_metal docs This change organizes the bare_metal docs by topic rather than letting autodoc organize methods by the order they appear in the _proxy.py file. Change-Id: I24ec3db3fc24f1417d13c91a344e3ebeb622bb40 --- doc/source/users/proxies/bare_metal.rst | 62 ++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/doc/source/users/proxies/bare_metal.rst b/doc/source/users/proxies/bare_metal.rst index 58c7d23be..8317b86be 100644 --- a/doc/source/users/proxies/bare_metal.rst +++ b/doc/source/users/proxies/bare_metal.rst @@ -12,5 +12,65 @@ The bare_metal high-level interface is available through the ``bare_metal`` member of a :class:`~openstack.connection.Connection` object. The ``bare_metal`` member will only be added if the service is detected. +Node Operations +^^^^^^^^^^^^^^^ .. autoclass:: openstack.bare_metal.v1._proxy.Proxy - :members: + + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.create_node + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.update_node + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.delete_node + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.get_node + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.find_node + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.nodes + +Port Operations +^^^^^^^^^^^^^^^ +.. autoclass:: openstack.bare_metal.v1._proxy.Proxy + + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.create_port + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.update_port + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.delete_port + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.get_port + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.find_port + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.ports + +Port Group Operations +^^^^^^^^^^^^^^^^^^^^^ +.. autoclass:: openstack.bare_metal.v1._proxy.Proxy + + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.create_port_group + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.update_port_group + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.delete_port_group + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.get_port_group + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.find_port_group + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.port_groups + +Driver Operations +^^^^^^^^^^^^^^^^^ +.. autoclass:: openstack.bare_metal.v1._proxy.Proxy + + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.drivers + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.get_driver + +Chassis Operations +^^^^^^^^^^^^^^^^^^ +.. autoclass:: openstack.bare_metal.v1._proxy.Proxy + + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.create_chassis + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.update_chassis + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.delete_chassis + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.get_chassis + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.find_chassis + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.chassis + +Deprecated Methods +^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.bare_metal.v1._proxy.Proxy + + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.create_portgroup + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.update_portgroup + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.delete_portgroup + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.get_portgroup + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.find_portgroup + .. automethod:: openstack.bare_metal.v1._proxy.Proxy.portgroups From 71322c7bbc70e24b119d30d6f1aab02934a83583 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 15 Feb 2017 11:05:41 -0600 Subject: [PATCH 1279/3836] Fix several concurrent shade gate issues This is a big patch because there are more than one issue happening at the same time and we have to fix all of them to fix any of them. Force nova microversion to 2.0 The current use of novaclient is to get the latest microversion. So far this has not been a problem, as shade deals with different payloads across clouds all the time. However, the latest microversion to nova broke shade's expectations about how usage reports work. Actual microversion support is coming soon to shade, but is too much of a task for a gate fix. In the meantime, pin to 2.0 which is available on all of the clouds. Produce some debug details about nova usage objects Capture novaclient debug logging In chasing down the usage issue, we were missing the REST interactions we needed to be effective in chasing down the problem. novaclient passes its own logger to keystoneauth Session, so we needed to include it in the debug logging setup. Also, add a helper function to make adding things like this easier. Consume cirros qcow2 image if it's there The move from ami to qcow2 for cirros broke shade's finding of it as a candidate image. Move pick_image into the base class so that we can include add_on_exception and error messages everywhere consistently. Add image list to debug output on failure. When we can't find a sensible image, add the list of images to the test output so that we can examine them. Change-Id: Ifae65e6cdf48921eaa379b803913277affbfe22a --- ...ova-old-microversion-5e4b8e239ba44096.yaml | 5 ++++ shade/openstackcloud.py | 2 +- shade/tests/base.py | 13 +++++++++ shade/tests/functional/base.py | 27 +++++++++++++++++++ shade/tests/functional/test_compute.py | 6 ++--- shade/tests/functional/test_floating_ip.py | 6 ++--- shade/tests/functional/test_image.py | 3 +-- shade/tests/functional/test_inventory.py | 6 ++--- shade/tests/functional/test_usage.py | 1 + shade/tests/functional/util.py | 19 ------------- 10 files changed, 54 insertions(+), 34 deletions(-) create mode 100644 releasenotes/notes/nova-old-microversion-5e4b8e239ba44096.yaml diff --git a/releasenotes/notes/nova-old-microversion-5e4b8e239ba44096.yaml b/releasenotes/notes/nova-old-microversion-5e4b8e239ba44096.yaml new file mode 100644 index 000000000..013ed82fa --- /dev/null +++ b/releasenotes/notes/nova-old-microversion-5e4b8e239ba44096.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - Nova microversion is being requested. Since shade is not yet + actively microversion aware, but has been dealing with the 2.0 structures + anyway, this should not affect anyone. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 78124749f..b42481e21 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -545,7 +545,7 @@ def _volume_client(self): def nova_client(self): if self._nova_client is None: self._nova_client = self._get_client( - 'compute', novaclient.client.Client) + 'compute', novaclient.client.Client, version='2.0') return self._nova_client @property diff --git a/shade/tests/base.py b/shade/tests/base.py index 8915e2b08..73d1e86bb 100644 --- a/shade/tests/base.py +++ b/shade/tests/base.py @@ -18,6 +18,7 @@ import fixtures import logging import munch +import pprint from six import StringIO import testtools import testtools.content @@ -75,6 +76,12 @@ def setUp(self): logger.addHandler(handler) logger.propagate = False + # Enable HTTP level tracing + logger = logging.getLogger('novaclient') + logger.setLevel(logging.DEBUG) + logger.addHandler(handler) + logger.propagate = False + def assertEqual(self, first, second, *args, **kwargs): '''Munch aware wrapper''' if isinstance(first, munch.Munch): @@ -101,3 +108,9 @@ def reader(): testtools.content_type.UTF8_TEXT, False) self.addDetail('logging', content) + + def add_info_on_exception(self, name, text): + def add_content(unused): + self.addDetail(name, testtools.content.text_content( + pprint.pformat(text))) + self.addOnException(add_content) diff --git a/shade/tests/functional/base.py b/shade/tests/functional/base.py index 6d64cb527..919a8ef03 100644 --- a/shade/tests/functional/base.py +++ b/shade/tests/functional/base.py @@ -37,3 +37,30 @@ def setUp(self): self.identity_version = \ self.operator_cloud.cloud_config.get_api_version('identity') + + def pick_image(self): + images = self.user_cloud.list_images() + self.add_info_on_exception('images', images) + + image_name = os.environ.get('SHADE_IMAGE') + if image_name: + for image in images: + if image.name == image_name: + return image + self.assertFalse( + "Cloud does not have {image}".format(image=image_name)) + + for image in images: + if image.name.startswith('cirros') and image.name.endswith('-uec'): + return image + for image in images: + if (image.name.startswith('cirros') + and image.disk_format == 'qcow2'): + return image + for image in images: + if image.name.lower().startswith('ubuntu'): + return image + for image in images: + if image.name.lower().startswith('centos'): + return image + self.assertFalse('no sensible image available') diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index b0a33794b..06322f6a2 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -21,7 +21,7 @@ from shade import exc from shade.tests.functional import base -from shade.tests.functional.util import pick_flavor, pick_image +from shade.tests.functional.util import pick_flavor from shade import _utils @@ -31,9 +31,7 @@ def setUp(self): self.flavor = pick_flavor(self.user_cloud.list_flavors()) if self.flavor is None: self.assertFalse('no sensible flavor available') - self.image = pick_image(self.user_cloud.list_images()) - if self.image is None: - self.assertFalse('no sensible image available') + self.image = self.pick_image() self.server_name = self.getUniqueString() def _cleanup_servers_and_volumes(self, server_name): diff --git a/shade/tests/functional/test_floating_ip.py b/shade/tests/functional/test_floating_ip.py index 7bd8f2685..24ab87d1e 100644 --- a/shade/tests/functional/test_floating_ip.py +++ b/shade/tests/functional/test_floating_ip.py @@ -28,7 +28,7 @@ from shade import meta from shade.exc import OpenStackCloudException from shade.tests.functional import base -from shade.tests.functional.util import pick_flavor, pick_image +from shade.tests.functional.util import pick_flavor class TestFloatingIP(base.BaseFunctionalTestCase): @@ -42,9 +42,7 @@ def setUp(self): self.flavor = pick_flavor(self.nova.flavors.list()) if self.flavor is None: self.assertFalse('no sensible flavor available') - self.image = pick_image(self.nova.images.list()) - if self.image is None: - self.assertFalse('no sensible image available') + self.image = self.pick_image() # Generate a random name for these tests self.new_item_name = self.getUniqueString() diff --git a/shade/tests/functional/test_image.py b/shade/tests/functional/test_image.py index f890b0567..7280bd36d 100644 --- a/shade/tests/functional/test_image.py +++ b/shade/tests/functional/test_image.py @@ -22,13 +22,12 @@ import tempfile from shade.tests.functional import base -from shade.tests.functional.util import pick_image class TestImage(base.BaseFunctionalTestCase): def setUp(self): super(TestImage, self).setUp() - self.image = pick_image(self.user_cloud.nova_client.images.list()) + self.image = self.pick_image() def test_create_image(self): test_image = tempfile.NamedTemporaryFile(delete=False) diff --git a/shade/tests/functional/test_inventory.py b/shade/tests/functional/test_inventory.py index 877051526..2fd0e38e0 100644 --- a/shade/tests/functional/test_inventory.py +++ b/shade/tests/functional/test_inventory.py @@ -22,7 +22,7 @@ from shade import inventory from shade.tests.functional import base -from shade.tests.functional.util import pick_flavor, pick_image +from shade.tests.functional.util import pick_flavor class TestInventory(base.BaseFunctionalTestCase): @@ -36,9 +36,7 @@ def setUp(self): self.flavor = pick_flavor(self.nova.flavors.list()) if self.flavor is None: self.assertTrue(False, 'no sensible flavor available') - self.image = pick_image(self.nova.images.list()) - if self.image is None: - self.assertTrue(False, 'no sensible image available') + self.image = self.pick_image() self.addCleanup(self._cleanup_servers) self.operator_cloud.create_server( name=self.server_name, image=self.image, flavor=self.flavor, diff --git a/shade/tests/functional/test_usage.py b/shade/tests/functional/test_usage.py index e175c1711..35d15b0c9 100644 --- a/shade/tests/functional/test_usage.py +++ b/shade/tests/functional/test_usage.py @@ -30,5 +30,6 @@ def test_get_usage(self): usage = self.operator_cloud.get_compute_usage('demo', datetime.datetime.now(), datetime.datetime.now()) + self.add_info_on_exception('usage', usage) self.assertIsNotNone(usage) self.assertTrue(hasattr(usage, 'total_hours')) diff --git a/shade/tests/functional/util.py b/shade/tests/functional/util.py index a88e47de9..180f08f76 100644 --- a/shade/tests/functional/util.py +++ b/shade/tests/functional/util.py @@ -40,22 +40,3 @@ def pick_flavor(flavors): flavors, key=operator.attrgetter('ram')): return flavor - - -def pick_image(images): - image_name = os.environ.get('SHADE_IMAGE') - if image_name: - for image in images: - if image.name == image_name: - return image - return None - - for image in images: - if image.name.startswith('cirros') and image.name.endswith('-uec'): - return image - for image in images: - if image.name.lower().startswith('ubuntu'): - return image - for image in images: - if image.name.lower().startswith('centos'): - return image From 8858995c97a49e43a120d43f79ada1cb51908919 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Wed, 15 Feb 2017 14:42:39 -0500 Subject: [PATCH 1280/3836] Update intersphinx linking to python.org We're getting a message in the docs build about the intersphinx location having moved. This moves it accordingly. Running Sphinx v1.5.2 loading pickled environment... not yet created loading intersphinx inventory from http://docs.python.org/3/objects.inv... intersphinx inventory has moved: http://docs.python.org/3/objects.inv -> https://docs.python.org/3/objects.inv Change-Id: I9ae3fd19a157dfcee0357ddfaab9e8933af7624c --- doc/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index f31d3b962..3b81e8b8e 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -120,7 +120,7 @@ ] # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'http://docs.python.org/3/': None} +intersphinx_mapping = {'https://docs.python.org/3/': None} # Include both the class and __init__ docstrings when describing the class autoclass_content = "both" From 50fedcd50f9bdebcc155be05c0195ea6fe0e6631 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Wed, 15 Feb 2017 14:38:34 -0500 Subject: [PATCH 1281/3836] Reorganize compute docs This change organizes the compute docs by topic rather than letting autodoc organize methods by the order they appear in the _proxy.py file. Change-Id: Ie4955223a07c5ddbf0c5a06330521c69063bde28 --- doc/source/users/proxies/compute.rst | 166 ++++++++++++++++++++++++++- 1 file changed, 165 insertions(+), 1 deletion(-) diff --git a/doc/source/users/proxies/compute.rst b/doc/source/users/proxies/compute.rst index c57a31599..83b031269 100644 --- a/doc/source/users/proxies/compute.rst +++ b/doc/source/users/proxies/compute.rst @@ -12,5 +12,169 @@ The compute high-level interface is available through the ``compute`` member of a :class:`~openstack.connection.Connection` object. The ``compute`` member will only be added if the service is detected. + +Server Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.compute.v2._proxy.Proxy + + .. automethod:: openstack.compute.v2._proxy.Proxy.create_server + .. automethod:: openstack.compute.v2._proxy.Proxy.update_server + .. automethod:: openstack.compute.v2._proxy.Proxy.delete_server + .. automethod:: openstack.compute.v2._proxy.Proxy.get_server + .. automethod:: openstack.compute.v2._proxy.Proxy.find_server + .. automethod:: openstack.compute.v2._proxy.Proxy.servers + .. automethod:: openstack.compute.v2._proxy.Proxy.get_server_metadata + .. automethod:: openstack.compute.v2._proxy.Proxy.set_server_metadata + .. automethod:: openstack.compute.v2._proxy.Proxy.delete_server_metadata + .. automethod:: openstack.compute.v2._proxy.Proxy.wait_for_server + .. automethod:: openstack.compute.v2._proxy.Proxy.create_server_image + +Network Actions +*************** + +.. autoclass:: openstack.compute.v2._proxy.Proxy + + .. automethod:: openstack.compute.v2._proxy.Proxy.add_fixed_ip_to_server + .. automethod:: openstack.compute.v2._proxy.Proxy.remove_fixed_ip_from_server + .. automethod:: openstack.compute.v2._proxy.Proxy.add_floating_ip_to_server + .. automethod:: openstack.compute.v2._proxy.Proxy.remove_floating_ip_from_server + .. automethod:: openstack.compute.v2._proxy.Proxy.add_security_group_to_server + .. automethod:: openstack.compute.v2._proxy.Proxy.remove_security_group_from_server + +Starting, Stopping, etc. +************************ + +.. autoclass:: openstack.compute.v2._proxy.Proxy + + .. automethod:: openstack.compute.v2._proxy.Proxy.start_server + .. automethod:: openstack.compute.v2._proxy.Proxy.stop_server + .. automethod:: openstack.compute.v2._proxy.Proxy.suspend_server + .. automethod:: openstack.compute.v2._proxy.Proxy.resume_server + .. automethod:: openstack.compute.v2._proxy.Proxy.reboot_server + .. automethod:: openstack.compute.v2._proxy.Proxy.shelve_server + .. automethod:: openstack.compute.v2._proxy.Proxy.unshelve_server + .. automethod:: openstack.compute.v2._proxy.Proxy.lock_server + .. automethod:: openstack.compute.v2._proxy.Proxy.unlock_server + .. automethod:: openstack.compute.v2._proxy.Proxy.pause_server + .. automethod:: openstack.compute.v2._proxy.Proxy.unpause_server + .. automethod:: openstack.compute.v2._proxy.Proxy.rescue_server + .. automethod:: openstack.compute.v2._proxy.Proxy.unrescue_server + .. automethod:: openstack.compute.v2._proxy.Proxy.evacuate_server + +Modifying a Server +****************** + +.. autoclass:: openstack.compute.v2._proxy.Proxy + + .. automethod:: openstack.compute.v2._proxy.Proxy.resize_server + .. automethod:: openstack.compute.v2._proxy.Proxy.confirm_server_resize + .. automethod:: openstack.compute.v2._proxy.Proxy.revert_server_resize + .. automethod:: openstack.compute.v2._proxy.Proxy.rebuild_server + .. automethod:: openstack.compute.v2._proxy.Proxy.reset_server_state + .. automethod:: openstack.compute.v2._proxy.Proxy.change_server_password + +Image Operations +^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.compute.v2._proxy.Proxy + + .. automethod:: openstack.compute.v2._proxy.Proxy.images + .. automethod:: openstack.compute.v2._proxy.Proxy.get_image + .. automethod:: openstack.compute.v2._proxy.Proxy.find_image + .. automethod:: openstack.compute.v2._proxy.Proxy.delete_image + .. automethod:: openstack.compute.v2._proxy.Proxy.get_image_metadata + .. automethod:: openstack.compute.v2._proxy.Proxy.set_image_metadata + .. automethod:: openstack.compute.v2._proxy.Proxy.delete_image_metadata + +Flavor Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.compute.v2._proxy.Proxy + + .. automethod:: openstack.compute.v2._proxy.Proxy.create_flavor + .. automethod:: openstack.compute.v2._proxy.Proxy.delete_flavor + .. automethod:: openstack.compute.v2._proxy.Proxy.get_flavor + .. automethod:: openstack.compute.v2._proxy.Proxy.find_flavor + .. automethod:: openstack.compute.v2._proxy.Proxy.flavors + +Service Operations +^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.compute.v2._proxy.Proxy + + .. automethod:: openstack.compute.v2._proxy.Proxy.get_service + .. automethod:: openstack.compute.v2._proxy.Proxy.services + .. automethod:: openstack.compute.v2._proxy.Proxy.enable_service + .. automethod:: openstack.compute.v2._proxy.Proxy.disable_service + .. automethod:: openstack.compute.v2._proxy.Proxy.force_service_down + +Keypair Operations +^^^^^^^^^^^^^^^^^^ + .. autoclass:: openstack.compute.v2._proxy.Proxy - :members: + + .. automethod:: openstack.compute.v2._proxy.Proxy.create_keypair + .. automethod:: openstack.compute.v2._proxy.Proxy.delete_keypair + .. automethod:: openstack.compute.v2._proxy.Proxy.get_keypair + .. automethod:: openstack.compute.v2._proxy.Proxy.find_keypair + .. automethod:: openstack.compute.v2._proxy.Proxy.keypairs + +Server IPs +^^^^^^^^^^ + +.. autoclass:: openstack.compute.v2._proxy.Proxy + + .. automethod:: openstack.compute.v2._proxy.Proxy.server_ips + +Server Group Operations +^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.compute.v2._proxy.Proxy + + .. automethod:: openstack.compute.v2._proxy.Proxy.create_server_group + .. automethod:: openstack.compute.v2._proxy.Proxy.delete_server_group + .. automethod:: openstack.compute.v2._proxy.Proxy.get_server_group + .. automethod:: openstack.compute.v2._proxy.Proxy.find_server_group + .. automethod:: openstack.compute.v2._proxy.Proxy.server_groups + +Server Interface Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.compute.v2._proxy.Proxy + + .. automethod:: openstack.compute.v2._proxy.Proxy.create_server_interface + .. automethod:: openstack.compute.v2._proxy.Proxy.delete_server_interface + .. automethod:: openstack.compute.v2._proxy.Proxy.get_server_interface + .. automethod:: openstack.compute.v2._proxy.Proxy.server_interfaces + +Availability Zone Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.compute.v2._proxy.Proxy + + .. automethod:: openstack.compute.v2._proxy.Proxy.availability_zones + +Limits Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.compute.v2._proxy.Proxy + + .. automethod:: openstack.compute.v2._proxy.Proxy.get_limits + +Hypervisor Operations +^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.compute.v2._proxy.Proxy + + .. automethod:: openstack.compute.v2._proxy.Proxy.get_hypervisor + .. automethod:: openstack.compute.v2._proxy.Proxy.find_hypervisor + .. automethod:: openstack.compute.v2._proxy.Proxy.hypervisors + +Extension Operations +^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.compute.v2._proxy.Proxy + + .. automethod:: openstack.compute.v2._proxy.Proxy.find_extension + .. automethod:: openstack.compute.v2._proxy.Proxy.extensions From ce2627d327c9aa286fcf65a2095854ea2bba9930 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Wed, 15 Feb 2017 15:21:21 -0500 Subject: [PATCH 1282/3836] Reorganize database docs This change organizes the database docs by topic rather than letting autodoc organize methods by the order they appear in the _proxy.py file. Change-Id: I79e9ab9abb33fde72f48c10227a6b6140b2133ba --- doc/source/users/proxies/database.rst | 42 ++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/doc/source/users/proxies/database.rst b/doc/source/users/proxies/database.rst index 784eb0999..493589b7b 100644 --- a/doc/source/users/proxies/database.rst +++ b/doc/source/users/proxies/database.rst @@ -12,5 +12,45 @@ The database high-level interface is available through the ``database`` member of a :class:`~openstack.connection.Connection` object. The ``database`` member will only be added if the service is detected. +Database Operations +^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.database.v1._proxy.Proxy + + .. automethod:: openstack.database.v1._proxy.Proxy.create_database + .. automethod:: openstack.database.v1._proxy.Proxy.delete_database + .. automethod:: openstack.database.v1._proxy.Proxy.get_database + .. automethod:: openstack.database.v1._proxy.Proxy.find_database + .. automethod:: openstack.database.v1._proxy.Proxy.databases + +Flavor Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.database.v1._proxy.Proxy + + .. automethod:: openstack.database.v1._proxy.Proxy.get_flavor + .. automethod:: openstack.database.v1._proxy.Proxy.find_flavor + .. automethod:: openstack.database.v1._proxy.Proxy.flavors + +Instance Operations +^^^^^^^^^^^^^^^^^^^ + .. autoclass:: openstack.database.v1._proxy.Proxy - :members: + + .. automethod:: openstack.database.v1._proxy.Proxy.create_instance + .. automethod:: openstack.database.v1._proxy.Proxy.update_instance + .. automethod:: openstack.database.v1._proxy.Proxy.delete_instance + .. automethod:: openstack.database.v1._proxy.Proxy.get_instance + .. automethod:: openstack.database.v1._proxy.Proxy.find_instance + .. automethod:: openstack.database.v1._proxy.Proxy.instances + +User Operations +^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.database.v1._proxy.Proxy + + .. automethod:: openstack.database.v1._proxy.Proxy.create_user + .. automethod:: openstack.database.v1._proxy.Proxy.delete_user + .. automethod:: openstack.database.v1._proxy.Proxy.get_user + .. automethod:: openstack.database.v1._proxy.Proxy.find_user + .. automethod:: openstack.database.v1._proxy.Proxy.users From 059d89c4a1402dc8969be3bafb6dae8666da00a8 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Wed, 15 Feb 2017 17:15:55 -0500 Subject: [PATCH 1283/3836] Reorganize image docs This change organizes the image docs by topic rather than letting autodoc organize methods by the order they appear in the _proxy.py file. Change-Id: I798931fd3fecf51b922526fadb2ae243c4fc8c52 --- doc/source/users/index.rst | 3 +- doc/source/users/proxies/image.rst | 33 --------------------- doc/source/users/proxies/image_v1.rst | 22 ++++++++++++++ doc/source/users/proxies/image_v2.rst | 42 +++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 34 deletions(-) delete mode 100644 doc/source/users/proxies/image.rst create mode 100644 doc/source/users/proxies/image_v1.rst create mode 100644 doc/source/users/proxies/image_v2.rst diff --git a/doc/source/users/index.rst b/doc/source/users/index.rst index f9e14e672..fbac4aafe 100644 --- a/doc/source/users/index.rst +++ b/doc/source/users/index.rst @@ -76,7 +76,8 @@ but listed below are the ones provided by this SDK by default. Compute Database Identity - Image + Image v1 + Image v2 Key Manager Network Object Store diff --git a/doc/source/users/proxies/image.rst b/doc/source/users/proxies/image.rst deleted file mode 100644 index 0542671e5..000000000 --- a/doc/source/users/proxies/image.rst +++ /dev/null @@ -1,33 +0,0 @@ -Image API v1 -============ - -For details on how to use image, see :doc:`/users/guides/image` - -.. automodule:: openstack.image.v1._proxy - -The Image v1 Class ------------------- - -The image high-level interface is available through the ``image`` member of a -:class:`~openstack.connection.Connection` object. The ``image`` member will -only be added if the service is detected. - -.. autoclass:: openstack.image.v1._proxy.Proxy - :members: - -Image API v2 -============ - -For details on how to use image, see :doc:`/users/guides/image` - -.. automodule:: openstack.image.v2._proxy - -The Image v2 Class ------------------- - -The image high-level interface is available through the ``image`` member of a -:class:`~openstack.connection.Connection` object. The ``image`` member will -only be added if the service is detected. - -.. autoclass:: openstack.image.v2._proxy.Proxy - :members: diff --git a/doc/source/users/proxies/image_v1.rst b/doc/source/users/proxies/image_v1.rst new file mode 100644 index 000000000..185d60eb8 --- /dev/null +++ b/doc/source/users/proxies/image_v1.rst @@ -0,0 +1,22 @@ +Image API v1 +============ + +For details on how to use image, see :doc:`/users/guides/image` + +.. automodule:: openstack.image.v1._proxy + +The Image v1 Class +------------------ + +The image high-level interface is available through the ``image`` member of a +:class:`~openstack.connection.Connection` object. The ``image`` member will +only be added if the service is detected. + +.. autoclass:: openstack.image.v1._proxy.Proxy + + .. automethod:: openstack.image.v1._proxy.Proxy.upload_image + .. automethod:: openstack.image.v1._proxy.Proxy.update_image + .. automethod:: openstack.image.v1._proxy.Proxy.delete_image + .. automethod:: openstack.image.v1._proxy.Proxy.get_image + .. automethod:: openstack.image.v1._proxy.Proxy.find_image + .. automethod:: openstack.image.v1._proxy.Proxy.images diff --git a/doc/source/users/proxies/image_v2.rst b/doc/source/users/proxies/image_v2.rst new file mode 100644 index 000000000..f88d7450a --- /dev/null +++ b/doc/source/users/proxies/image_v2.rst @@ -0,0 +1,42 @@ +Image API v2 +============ + +For details on how to use image, see :doc:`/users/guides/image` + +.. automodule:: openstack.image.v2._proxy + +The Image v2 Class +------------------ + +The image high-level interface is available through the ``image`` member of a +:class:`~openstack.connection.Connection` object. The ``image`` member will +only be added if the service is detected. + +Image Operations +^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.image.v2._proxy.Proxy + + .. automethod:: openstack.image.v2._proxy.Proxy.upload_image + .. automethod:: openstack.image.v2._proxy.Proxy.download_image + .. automethod:: openstack.image.v2._proxy.Proxy.update_image + .. automethod:: openstack.image.v2._proxy.Proxy.delete_image + .. automethod:: openstack.image.v2._proxy.Proxy.get_image + .. automethod:: openstack.image.v2._proxy.Proxy.find_image + .. automethod:: openstack.image.v2._proxy.Proxy.images + .. automethod:: openstack.image.v2._proxy.Proxy.deactivate_image + .. automethod:: openstack.image.v2._proxy.Proxy.reactivate_image + .. automethod:: openstack.image.v2._proxy.Proxy.add_tag + .. automethod:: openstack.image.v2._proxy.Proxy.remove_tag + +Member Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.image.v2._proxy.Proxy + + .. automethod:: openstack.image.v2._proxy.Proxy.add_member + .. automethod:: openstack.image.v2._proxy.Proxy.remove_member + .. automethod:: openstack.image.v2._proxy.Proxy.update_member + .. automethod:: openstack.image.v2._proxy.Proxy.get_member + .. automethod:: openstack.image.v2._proxy.Proxy.find_member + .. automethod:: openstack.image.v2._proxy.Proxy.members From 2db90b40334c8e04acdd4cf5033e83563b100076 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Wed, 15 Feb 2017 16:54:09 -0500 Subject: [PATCH 1284/3836] Reorganize identity docs This change organizes the identity docs by topic rather than letting autodoc organize methods by the order they appear in the _proxy.py file. Change-Id: I991b810bad8e5932764186e52970748d47e0e4b4 --- doc/source/users/index.rst | 3 +- doc/source/users/proxies/identity.rst | 33 ----- doc/source/users/proxies/identity_v2.rst | 49 ++++++++ doc/source/users/proxies/identity_v3.rst | 146 +++++++++++++++++++++++ 4 files changed, 197 insertions(+), 34 deletions(-) delete mode 100644 doc/source/users/proxies/identity.rst create mode 100644 doc/source/users/proxies/identity_v2.rst create mode 100644 doc/source/users/proxies/identity_v3.rst diff --git a/doc/source/users/index.rst b/doc/source/users/index.rst index fbac4aafe..d715489ba 100644 --- a/doc/source/users/index.rst +++ b/doc/source/users/index.rst @@ -75,7 +75,8 @@ but listed below are the ones provided by this SDK by default. Cluster Compute Database - Identity + Identity v2 + Identity v3 Image v1 Image v2 Key Manager diff --git a/doc/source/users/proxies/identity.rst b/doc/source/users/proxies/identity.rst deleted file mode 100644 index 037398caf..000000000 --- a/doc/source/users/proxies/identity.rst +++ /dev/null @@ -1,33 +0,0 @@ -Identity API v2 -=============== - -For details on how to use identity, see :doc:`/users/guides/identity` - -.. automodule:: openstack.identity.v2._proxy - -The Identity v2 Class ---------------------- - -The identity high-level interface is available through the ``identity`` -member of a :class:`~openstack.connection.Connection` object. The -``identity`` member will only be added if the service is detected. - -.. autoclass:: openstack.identity.v2._proxy.Proxy - :members: - -Identity API v3 -=============== - -For details on how to use identity, see :doc:`/users/guides/identity` - -.. automodule:: openstack.identity.v3._proxy - -The Identity v3 Class ---------------------- - -The identity high-level interface is available through the ``identity`` -member of a :class:`~openstack.connection.Connection` object. The -``identity`` member will only be added if the service is detected. - -.. autoclass:: openstack.identity.v3._proxy.Proxy - :members: diff --git a/doc/source/users/proxies/identity_v2.rst b/doc/source/users/proxies/identity_v2.rst new file mode 100644 index 000000000..66f4b528c --- /dev/null +++ b/doc/source/users/proxies/identity_v2.rst @@ -0,0 +1,49 @@ +Identity API v2 +=============== + +For details on how to use identity, see :doc:`/users/guides/identity` + +.. automodule:: openstack.identity.v2._proxy + +The Identity v2 Class +--------------------- + +The identity high-level interface is available through the ``identity`` +member of a :class:`~openstack.connection.Connection` object. The +``identity`` member will only be added if the service is detected. + +User Operations +^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.identity.v2._proxy.Proxy + + .. automethod:: openstack.identity.v2._proxy.Proxy.create_user + .. automethod:: openstack.identity.v2._proxy.Proxy.update_user + .. automethod:: openstack.identity.v2._proxy.Proxy.delete_user + .. automethod:: openstack.identity.v2._proxy.Proxy.get_user + .. automethod:: openstack.identity.v2._proxy.Proxy.find_user + .. automethod:: openstack.identity.v2._proxy.Proxy.users + +Role Operations +^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.identity.v2._proxy.Proxy + + .. automethod:: openstack.identity.v2._proxy.Proxy.create_role + .. automethod:: openstack.identity.v2._proxy.Proxy.update_role + .. automethod:: openstack.identity.v2._proxy.Proxy.delete_role + .. automethod:: openstack.identity.v2._proxy.Proxy.get_role + .. automethod:: openstack.identity.v2._proxy.Proxy.find_role + .. automethod:: openstack.identity.v2._proxy.Proxy.roles + +Tenant Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.identity.v2._proxy.Proxy + + .. automethod:: openstack.identity.v2._proxy.Proxy.create_tenant + .. automethod:: openstack.identity.v2._proxy.Proxy.update_tenant + .. automethod:: openstack.identity.v2._proxy.Proxy.delete_tenant + .. automethod:: openstack.identity.v2._proxy.Proxy.get_tenant + .. automethod:: openstack.identity.v2._proxy.Proxy.find_tenant + .. automethod:: openstack.identity.v2._proxy.Proxy.tenants diff --git a/doc/source/users/proxies/identity_v3.rst b/doc/source/users/proxies/identity_v3.rst new file mode 100644 index 000000000..a5366a019 --- /dev/null +++ b/doc/source/users/proxies/identity_v3.rst @@ -0,0 +1,146 @@ +Identity API v3 +=============== + +For details on how to use identity, see :doc:`/users/guides/identity` + +.. automodule:: openstack.identity.v3._proxy + +The Identity v3 Class +--------------------- + +The identity high-level interface is available through the ``identity`` +member of a :class:`~openstack.connection.Connection` object. The +``identity`` member will only be added if the service is detected. + +Credential Operations +^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.identity.v3._proxy.Proxy + + .. automethod:: openstack.identity.v3._proxy.Proxy.create_credential + .. automethod:: openstack.identity.v3._proxy.Proxy.update_credential + .. automethod:: openstack.identity.v3._proxy.Proxy.delete_credential + .. automethod:: openstack.identity.v3._proxy.Proxy.get_credential + .. automethod:: openstack.identity.v3._proxy.Proxy.find_credential + .. automethod:: openstack.identity.v3._proxy.Proxy.credentials + +Domain Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.identity.v3._proxy.Proxy + + .. automethod:: openstack.identity.v3._proxy.Proxy.create_domain + .. automethod:: openstack.identity.v3._proxy.Proxy.update_domain + .. automethod:: openstack.identity.v3._proxy.Proxy.delete_domain + .. automethod:: openstack.identity.v3._proxy.Proxy.get_domain + .. automethod:: openstack.identity.v3._proxy.Proxy.find_domain + .. automethod:: openstack.identity.v3._proxy.Proxy.domains + +Endpoint Operations +^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.identity.v3._proxy.Proxy + + .. automethod:: openstack.identity.v3._proxy.Proxy.create_endpoint + .. automethod:: openstack.identity.v3._proxy.Proxy.update_endpoint + .. automethod:: openstack.identity.v3._proxy.Proxy.delete_endpoint + .. automethod:: openstack.identity.v3._proxy.Proxy.get_endpoint + .. automethod:: openstack.identity.v3._proxy.Proxy.find_endpoint + .. automethod:: openstack.identity.v3._proxy.Proxy.endpoints + +Group Operations +^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.identity.v3._proxy.Proxy + + .. automethod:: openstack.identity.v3._proxy.Proxy.create_group + .. automethod:: openstack.identity.v3._proxy.Proxy.update_group + .. automethod:: openstack.identity.v3._proxy.Proxy.delete_group + .. automethod:: openstack.identity.v3._proxy.Proxy.get_group + .. automethod:: openstack.identity.v3._proxy.Proxy.find_group + .. automethod:: openstack.identity.v3._proxy.Proxy.groups + +Policy Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.identity.v3._proxy.Proxy + + .. automethod:: openstack.identity.v3._proxy.Proxy.create_policy + .. automethod:: openstack.identity.v3._proxy.Proxy.update_policy + .. automethod:: openstack.identity.v3._proxy.Proxy.delete_policy + .. automethod:: openstack.identity.v3._proxy.Proxy.get_policy + .. automethod:: openstack.identity.v3._proxy.Proxy.find_policy + .. automethod:: openstack.identity.v3._proxy.Proxy.policies + +Project Operations +^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.identity.v3._proxy.Proxy + + .. automethod:: openstack.identity.v3._proxy.Proxy.create_project + .. automethod:: openstack.identity.v3._proxy.Proxy.update_project + .. automethod:: openstack.identity.v3._proxy.Proxy.delete_project + .. automethod:: openstack.identity.v3._proxy.Proxy.get_project + .. automethod:: openstack.identity.v3._proxy.Proxy.find_project + .. automethod:: openstack.identity.v3._proxy.Proxy.projects + +Region Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.identity.v3._proxy.Proxy + + .. automethod:: openstack.identity.v3._proxy.Proxy.create_region + .. automethod:: openstack.identity.v3._proxy.Proxy.update_region + .. automethod:: openstack.identity.v3._proxy.Proxy.delete_region + .. automethod:: openstack.identity.v3._proxy.Proxy.get_region + .. automethod:: openstack.identity.v3._proxy.Proxy.find_region + .. automethod:: openstack.identity.v3._proxy.Proxy.regions + +Role Operations +^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.identity.v3._proxy.Proxy + + .. automethod:: openstack.identity.v3._proxy.Proxy.create_role + .. automethod:: openstack.identity.v3._proxy.Proxy.update_role + .. automethod:: openstack.identity.v3._proxy.Proxy.delete_role + .. automethod:: openstack.identity.v3._proxy.Proxy.get_role + .. automethod:: openstack.identity.v3._proxy.Proxy.find_role + .. automethod:: openstack.identity.v3._proxy.Proxy.roles + .. automethod:: openstack.identity.v3._proxy.Proxy.role_assignments + .. automethod:: openstack.identity.v3._proxy.Proxy.role_assignments_filter + +Service Operations +^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.identity.v3._proxy.Proxy + + .. automethod:: openstack.identity.v3._proxy.Proxy.create_service + .. automethod:: openstack.identity.v3._proxy.Proxy.update_service + .. automethod:: openstack.identity.v3._proxy.Proxy.delete_service + .. automethod:: openstack.identity.v3._proxy.Proxy.get_service + .. automethod:: openstack.identity.v3._proxy.Proxy.find_service + .. automethod:: openstack.identity.v3._proxy.Proxy.services + +Trust Operations +^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.identity.v3._proxy.Proxy + + .. automethod:: openstack.identity.v3._proxy.Proxy.create_trust + .. automethod:: openstack.identity.v3._proxy.Proxy.delete_trust + .. automethod:: openstack.identity.v3._proxy.Proxy.get_trust + .. automethod:: openstack.identity.v3._proxy.Proxy.find_trust + .. automethod:: openstack.identity.v3._proxy.Proxy.trusts + +User Operations +^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.identity.v3._proxy.Proxy + + .. automethod:: openstack.identity.v3._proxy.Proxy.create_user + .. automethod:: openstack.identity.v3._proxy.Proxy.update_user + .. automethod:: openstack.identity.v3._proxy.Proxy.delete_user + .. automethod:: openstack.identity.v3._proxy.Proxy.get_user + .. automethod:: openstack.identity.v3._proxy.Proxy.find_user + .. automethod:: openstack.identity.v3._proxy.Proxy.users From 60ce27ea81bc178783ec54a2f7771f7dd7242dda Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 15 Feb 2017 13:39:53 -0600 Subject: [PATCH 1285/3836] Actually normalize nova usage data Turns out usage reports are empty when there is no usage - so direct passthrough is not so much a thing. Fix it. Change-Id: I6a2f2e737f792ba74a191d688b3380dc333e34fe --- doc/source/model.rst | 52 +++++++++++++++++++++ shade/_normalize.py | 69 ++++++++++++++++++++++++++-- shade/operatorcloud.py | 9 ++-- shade/tests/functional/test_usage.py | 14 +++--- 4 files changed, 132 insertions(+), 12 deletions(-) diff --git a/doc/source/model.rst b/doc/source/model.rst index db3a9d69a..4941cf506 100644 --- a/doc/source/model.rst +++ b/doc/source/model.rst @@ -228,6 +228,58 @@ Limits and current usage for a project in Nova total_server_groups_used=int(), properties=dict()) +ComputeUsage +------------ + +Current usage for a project in Nova + +.. code-block:: python + + ComputeUsage = dict( + location=Location(), + started_at=str(), + stopped_at=str(), + server_usages=list(), + max_personality=int(), + max_personality_size=int(), + max_server_group_members=int(), + max_server_groups=int(), + max_server_meta=int(), + max_total_cores=int(), + max_total_instances=int(), + max_total_keypairs=int(), + max_total_ram_size=int(), + total_cores_used=int(), + total_hours=int(), + total_instances_used=int(), + total_local_gb_usage=int(), + total_memory_mb_usage=int(), + total_ram_used=int(), + total_server_groups_used=int(), + total_vcpus_usage=int(), + properties=dict()) + +ServerUsage +----------- + +Current usage for a server in Nova + +.. code-block:: python + + ComputeUsage = dict( + started_at=str(), + ended_at=str(), + flavor=str(), + hours=int(), + instance_id=str(), + local_gb=int(), + memory_mb=int(), + name=str(), + state=str(), + uptime=int(), + vcpus=int(), + properties=dict()) + Floating IP ----------- diff --git a/shade/_normalize.py b/shade/_normalize.py index 7ea68e970..3621e7805 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -762,8 +762,10 @@ def _normalize_volume(self, volume): ret.setdefault(key, val) return ret - def _normalize_usage(self, usage): - """ Normalize a usage object """ + def _normalize_compute_usage(self, usage): + """ Normalize a compute usage object """ + + usage = usage.copy() # Discard noise usage.pop('links', None) @@ -771,5 +773,66 @@ def _normalize_usage(self, usage): usage.pop('HUMAN_ID', None) usage.pop('human_id', None) usage.pop('request_ids', None) + project_id = usage.pop('tenant_id', None) + + ret = munch.Munch( + location=self._get_current_location(project_id=project_id), + ) + for key in ( + 'max_personality', + 'max_personality_size', + 'max_server_group_members', + 'max_server_groups', + 'max_server_meta', + 'max_total_cores', + 'max_total_instances', + 'max_total_keypairs', + 'max_total_ram_size', + 'total_cores_used', + 'total_hours', + 'total_instances_used', + 'total_local_gb_usage', + 'total_memory_mb_usage', + 'total_ram_used', + 'total_server_groups_used', + 'total_vcpus_usage'): + ret[key] = usage.pop(key, 0) + ret['started_at'] = usage.pop('start') + ret['stopped_at'] = usage.pop('stop') + ret['server_usages'] = self._normalize_server_usages( + usage.pop('server_usages', [])) + ret['properties'] = usage + return ret + + def _normalize_server_usage(self, server_usage): + """ Normalize a server usage object """ - return munch.Munch(usage) + server_usage = server_usage.copy() + # TODO(mordred) Right now there is already a location on the usage + # object. Including one here seems verbose. + server_usage.pop('tenant_id') + ret = munch.Munch() + + ret['ended_at'] = server_usage.pop('ended_at', None) + ret['started_at'] = server_usage.pop('started_at', None) + for key in ( + 'flavor', + 'instance_id', + 'name', + 'state'): + ret[key] = server_usage.pop(key, '') + for key in ( + 'hours', + 'local_gb', + 'memory_mb', + 'uptime', + 'vcpus'): + ret[key] = server_usage.pop(key, 0) + ret['properties'] = server_usage + return ret + + def _normalize_server_usages(self, server_usages): + ret = [] + for server_usage in server_usages: + ret.append(self._normalize_server_usage(server_usage)) + return ret diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 301d34e14..53a1029da 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -10,6 +10,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import datetime import jsonpatch from ironicclient import client as ironic_client @@ -2111,12 +2112,12 @@ def delete_compute_quotas(self, name_or_id): except nova_exceptions.BadRequest: raise OpenStackCloudException("nova client call failed") - def get_compute_usage(self, name_or_id, start, end): + def get_compute_usage(self, name_or_id, start, end=None): """ Get usage for a specific project :param name_or_id: project name or id :param start: :class:`datetime.datetime` Start date in UTC - :param end: :class:`datetime.datetime` End date in UTCs + :param end: :class:`datetime.datetime` End date in UTC. Defaults to now :raises: OpenStackCloudException if it's not a valid project :returns: Munch object with the usage @@ -2125,6 +2126,8 @@ def get_compute_usage(self, name_or_id, start, end): if not proj: raise OpenStackCloudException("project does not exist: {}".format( name=proj.id)) + if not end: + end = datetime.datetime.now() with _utils.shade_exceptions( "Unable to get resources usage for project: {name}".format( @@ -2132,7 +2135,7 @@ def get_compute_usage(self, name_or_id, start, end): usage = self.manager.submit_task( _tasks.NovaUsageGet(tenant_id=proj.id, start=start, end=end)) - return self._normalize_usage(usage) + return self._normalize_compute_usage(usage) def set_volume_quotas(self, name_or_id, **kwargs): """ Set a volume quota in a project diff --git a/shade/tests/functional/test_usage.py b/shade/tests/functional/test_usage.py index 35d15b0c9..ae36ab11d 100644 --- a/shade/tests/functional/test_usage.py +++ b/shade/tests/functional/test_usage.py @@ -25,11 +25,13 @@ class TestUsage(base.BaseFunctionalTestCase): - def test_get_usage(self): - '''Test quotas functionality''' - usage = self.operator_cloud.get_compute_usage('demo', - datetime.datetime.now(), - datetime.datetime.now()) + def test_get_compute_usage(self): + '''Test usage functionality''' + start = datetime.datetime.now() - datetime.timedelta(seconds=5) + usage = self.operator_cloud.get_compute_usage('demo', start) self.add_info_on_exception('usage', usage) self.assertIsNotNone(usage) - self.assertTrue(hasattr(usage, 'total_hours')) + self.assertIn('total_hours', usage) + self.assertIn('started_at', usage) + self.assertEqual(start.isoformat(), usage['started_at']) + self.assertIn('location', usage) From efc41d8624becbd7835e7f17eedccecd0a29fa2f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 16 Feb 2017 07:38:55 -0600 Subject: [PATCH 1286/3836] Change request_id logging to match nova format Nova has a very nice format for logging request_ids that is more readable than what we were putting into the logs. Copy it. Also, stop trying to log request ids for objects we get from python*client. They already have loggers. Change-Id: Ibe4bff3cf91f282920138fe1d9fe7be3198ba6e3 --- shade/_adapter.py | 69 ++++++++++++++++++++++++++++++++--------------- shade/meta.py | 26 +++--------------- 2 files changed, 51 insertions(+), 44 deletions(-) diff --git a/shade/_adapter.py b/shade/_adapter.py index ed431728e..b6cb2530e 100644 --- a/shade/_adapter.py +++ b/shade/_adapter.py @@ -18,6 +18,7 @@ from keystoneauth1 import adapter from six.moves import urllib +from shade import _log from shade import exc from shade import meta from shade import task_manager @@ -87,46 +88,70 @@ def __init__(self, shade_logger, manager, *args, **kwargs): super(ShadeAdapter, self).__init__(*args, **kwargs) self.shade_logger = shade_logger self.manager = manager + self.request_log = _log.setup_logging('shade.request_ids') + + def _log_request_id(self, response, obj=None): + # Log the request id and object id in a specific logger. This way + # someone can turn it on if they're interested in this kind of tracing. + request_id = response.headers.get('x-openstack-request-id') + if not request_id: + return response + tmpl = "{meth} call to {service} for {url} used request id {req}" + kwargs = dict( + meth=response.request.method, + service=self.service_type, + url=response.request.url, + req=request_id) + + if isinstance(obj, dict): + obj_id = obj.get('id', obj.get('uuid')) + if obj_id: + kwargs['obj_id'] = obj_id + tmpl += " returning object {obj_id}" + self.request_log.debug(tmpl.format(**kwargs)) + return response def _munch_response(self, response, result_key=None): exc.raise_from_response(response) if not response.content: # This doens't have any content - return response + return self._log_request_id(response) # Some REST calls do not return json content. Don't decode it. if 'application/json' not in response.headers.get('Content-Type'): - return response + return self._log_request_id(response) try: result_json = response.json() except Exception: - return response - - request_id = response.headers.get('x-openstack-request-id') - - if task_manager._is_listlike(result_json): - return meta.obj_list_to_dict( - result_json, request_id=request_id) - - # Wrap the keys() call in list() because in python3 keys returns - # a "dict_keys" iterator-like object rather than a list - json_keys = list(result_json.keys()) - if len(json_keys) > 1 and result_key: - result = result_json[result_key] - elif len(json_keys) == 1: - result = result_json[json_keys[0]] - else: + return self._log_request_id(response) + + if isinstance(result_json, list): + self._log_request_id(response) + return meta.obj_list_to_dict(result_json) + + result = None + if isinstance(result_json, dict): + # Wrap the keys() call in list() because in python3 keys returns + # a "dict_keys" iterator-like object rather than a list + json_keys = list(result_json.keys()) + if len(json_keys) > 1 and result_key: + result = result_json[result_key] + elif len(json_keys) == 1: + result = result_json[json_keys[0]] + if result is None: # Passthrough the whole body - sometimes (hi glance) things # come through without a top-level container. Also, sometimes # you need to deal with pagination result = result_json - if task_manager._is_listlike(result): - return meta.obj_list_to_dict(result, request_id=request_id) - if task_manager._is_objlike(result): - return meta.obj_to_dict(result, request_id=request_id) + self._log_request_id(response, result) + + if isinstance(result, list): + return meta.obj_list_to_dict(result) + elif isinstance(result, dict): + return meta.obj_to_dict(result) return result def request(self, url, method, run_async=False, *args, **kwargs): diff --git a/shade/meta.py b/shade/meta.py index 0e03718f2..cfc01996e 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -458,15 +458,6 @@ def get_hostvars_from_server(cloud, server, mounts=None): def _log_request_id(obj, request_id): - # Add it, if passed in, even though we're going to pop in a second, - # just to make the logic simpler - if request_id is not None: - obj['x_openstack_request_ids'] = [request_id] - - request_id = None - request_ids = obj.pop('x_openstack_request_ids', None) - if request_ids: - request_id = request_ids[0] if request_id: # Log the request id and object id in a specific logger. This way # someone can turn it on if they're interested in this kind of tracing. @@ -485,7 +476,7 @@ def _log_request_id(obj, request_id): return obj -def obj_to_dict(obj, request_id=None): +def obj_to_dict(obj): """ Turn an object with attributes into a dict suitable for serializing. Some of the things that are returned in OpenStack are objects with @@ -522,26 +513,17 @@ def obj_to_dict(obj, request_id=None): continue if isinstance(value, NON_CALLABLES) and not key.startswith('_'): instance[key] = value - return _log_request_id(instance, request_id) + return instance -def obj_list_to_dict(obj_list, request_id=None): +def obj_list_to_dict(obj_list): """Enumerate through lists of objects and return lists of dictonaries. Some of the objects returned in OpenStack are actually lists of objects, and in order to expose the data structures as JSON, we need to facilitate the conversion to lists of dictonaries. """ - new_list = [] - if not request_id: - request_id = getattr(obj_list, 'request_ids', [None])[0] - if request_id: - log = _log.setup_logging('shade.request_ids') - log.debug("Retrieved a list. Request ID %(request_id)s", - {'request_id': request_id}) - for obj in obj_list: - new_list.append(obj_to_dict(obj)) - return new_list + return [obj_to_dict(obj) for obj in obj_list] def warlock_to_dict(obj): From 3f8f5fd5c54a8cccae6cb0a57df69c4c49dfdf35 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Thu, 16 Feb 2017 14:11:35 -0500 Subject: [PATCH 1287/3836] Reorganize key_manager docs This change organizes the key_manager docs by topic rather than letting autodoc organize methods by the order they appear in the _proxy.py file. Change-Id: I9caefcd9d1847da6689f6680fb420a9ea41a1c93 --- doc/source/users/proxies/key_manager.rst | 35 +++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/doc/source/users/proxies/key_manager.rst b/doc/source/users/proxies/key_manager.rst index 455e1b5d2..7d24bb13e 100644 --- a/doc/source/users/proxies/key_manager.rst +++ b/doc/source/users/proxies/key_manager.rst @@ -14,5 +14,38 @@ The key_management high-level interface is available through the object. The ``key_manager`` member will only be added if the service is detected. +Secret Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.key_manager.v1._proxy.Proxy + + .. automethod:: openstack.key_manager.v1._proxy.Proxy.create_secret + .. automethod:: openstack.key_manager.v1._proxy.Proxy.update_secret + .. automethod:: openstack.key_manager.v1._proxy.Proxy.delete_secret + .. automethod:: openstack.key_manager.v1._proxy.Proxy.get_secret + .. automethod:: openstack.key_manager.v1._proxy.Proxy.find_secret + .. automethod:: openstack.key_manager.v1._proxy.Proxy.secrets + +Container Operations +^^^^^^^^^^^^^^^^^^^^ + .. autoclass:: openstack.key_manager.v1._proxy.Proxy - :members: + + .. automethod:: openstack.key_manager.v1._proxy.Proxy.create_container + .. automethod:: openstack.key_manager.v1._proxy.Proxy.update_container + .. automethod:: openstack.key_manager.v1._proxy.Proxy.delete_container + .. automethod:: openstack.key_manager.v1._proxy.Proxy.get_container + .. automethod:: openstack.key_manager.v1._proxy.Proxy.find_container + .. automethod:: openstack.key_manager.v1._proxy.Proxy.containers + +Order Operations +^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.key_manager.v1._proxy.Proxy + + .. automethod:: openstack.key_manager.v1._proxy.Proxy.create_order + .. automethod:: openstack.key_manager.v1._proxy.Proxy.update_order + .. automethod:: openstack.key_manager.v1._proxy.Proxy.delete_order + .. automethod:: openstack.key_manager.v1._proxy.Proxy.get_order + .. automethod:: openstack.key_manager.v1._proxy.Proxy.find_order + .. automethod:: openstack.key_manager.v1._proxy.Proxy.orders From 6f59061ec4f1cf275e8739d7117a6e01f4fd2c90 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Thu, 16 Feb 2017 15:39:31 -0500 Subject: [PATCH 1288/3836] Implement message docs This change organizes the message docs by topic rather than letting autodoc organize methods by the order they appear in the _proxy.py file. These were not previously included so they're technically all brand new. There are some minor adjustments made to the docstrings as they weren't known to be raising warnings before due to not actually being included anywhere. Change-Id: Id4b8f08015a6f00b8134fcd2a01506562d9787fb --- doc/source/users/guides/message.rst | 8 ++++ doc/source/users/index.rst | 3 ++ doc/source/users/proxies/message_v1.rst | 30 ++++++++++++++ doc/source/users/proxies/message_v2.rst | 53 +++++++++++++++++++++++++ openstack/message/v1/_proxy.py | 20 +++++----- openstack/message/v2/_proxy.py | 3 +- 6 files changed, 107 insertions(+), 10 deletions(-) create mode 100644 doc/source/users/guides/message.rst create mode 100644 doc/source/users/proxies/message_v1.rst create mode 100644 doc/source/users/proxies/message_v2.rst diff --git a/doc/source/users/guides/message.rst b/doc/source/users/guides/message.rst new file mode 100644 index 000000000..ac30f8830 --- /dev/null +++ b/doc/source/users/guides/message.rst @@ -0,0 +1,8 @@ +Using OpenStack Message +======================= + +Before working with the Message service, you'll need to create a connection +to your OpenStack cloud by following the :doc:`connect` user guide. This will +provide you with the ``conn`` variable used in the examples below. + +.. TODO(briancurtin): Implement this guide diff --git a/doc/source/users/index.rst b/doc/source/users/index.rst index fbac4aafe..cedb32135 100644 --- a/doc/source/users/index.rst +++ b/doc/source/users/index.rst @@ -36,6 +36,7 @@ approach, this is where you'll want to begin. Identity Image Key Manager + Message Network Object Store Orchestration @@ -79,6 +80,8 @@ but listed below are the ones provided by this SDK by default. Image v1 Image v2 Key Manager + Message v1 + Message v2 Network Object Store Orchestration diff --git a/doc/source/users/proxies/message_v1.rst b/doc/source/users/proxies/message_v1.rst new file mode 100644 index 000000000..3803eaf24 --- /dev/null +++ b/doc/source/users/proxies/message_v1.rst @@ -0,0 +1,30 @@ +Message API v1 +============== + +For details on how to use message, see :doc:`/users/guides/message` + +.. automodule:: openstack.message.v1._proxy + +The Message v1 Class +-------------------- + +The message high-level interface is available through the ``message`` member +of a :class:`~openstack.connection.Connection` object. The ``message`` +member will only be added if the service is detected. + +Message Operations +^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.message.v1._proxy.Proxy + + .. automethod:: openstack.message.v1._proxy.Proxy.claim_messages + .. automethod:: openstack.message.v1._proxy.Proxy.create_messages + .. automethod:: openstack.message.v1._proxy.Proxy.delete_message + +Queue Operations +^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.message.v1._proxy.Proxy + + .. automethod:: openstack.message.v1._proxy.Proxy.create_queue + .. automethod:: openstack.message.v1._proxy.Proxy.delete_queue diff --git a/doc/source/users/proxies/message_v2.rst b/doc/source/users/proxies/message_v2.rst new file mode 100644 index 000000000..5575663a9 --- /dev/null +++ b/doc/source/users/proxies/message_v2.rst @@ -0,0 +1,53 @@ +Message API v2 +============== + +For details on how to use message, see :doc:`/users/guides/message` + +.. automodule:: openstack.message.v2._proxy + +The Message v2 Class +-------------------- + +The message high-level interface is available through the ``message`` member +of a :class:`~openstack.connection.Connection` object. The ``message`` +member will only be added if the service is detected. + +Message Operations +^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.message.v2._proxy.Proxy + + .. automethod:: openstack.message.v2._proxy.Proxy.post_message + .. automethod:: openstack.message.v2._proxy.Proxy.delete_message + .. automethod:: openstack.message.v2._proxy.Proxy.get_message + .. automethod:: openstack.message.v2._proxy.Proxy.messages + +Queue Operations +^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.message.v2._proxy.Proxy + + .. automethod:: openstack.message.v2._proxy.Proxy.create_queue + .. automethod:: openstack.message.v2._proxy.Proxy.delete_queue + .. automethod:: openstack.message.v2._proxy.Proxy.get_queue + .. automethod:: openstack.message.v2._proxy.Proxy.queues + +Claim Operations +^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.message.v2._proxy.Proxy + + .. automethod:: openstack.message.v2._proxy.Proxy.create_claim + .. automethod:: openstack.message.v2._proxy.Proxy.update_claim + .. automethod:: openstack.message.v2._proxy.Proxy.delete_claim + .. automethod:: openstack.message.v2._proxy.Proxy.get_claim + +Subscription Operations +^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.message.v2._proxy.Proxy + + .. automethod:: openstack.message.v2._proxy.Proxy.create_subscription + .. automethod:: openstack.message.v2._proxy.Proxy.delete_subscription + .. automethod:: openstack.message.v2._proxy.Proxy.get_subscription + .. automethod:: openstack.message.v2._proxy.Proxy.subscriptions diff --git a/openstack/message/v1/_proxy.py b/openstack/message/v1/_proxy.py index bc034d3d6..ab31d07f8 100644 --- a/openstack/message/v1/_proxy.py +++ b/openstack/message/v1/_proxy.py @@ -48,12 +48,14 @@ def delete_queue(self, value, ignore_missing=True): def create_messages(self, values): """Create new messages - :param list values: The list of - :class:`~openstack.message.v1.message.Message`s to create. - - :returns: The results of message creation - :rtype: list messages: The list of - :class:`~openstack.message.v1.message.Message`s created. + :param values: The list of + :class:`~openstack.message.v1.message.Message` objects + to create. + :type values: :py:class:`list` + + :returns: The list of + :class:`~openstack.message.v1.message.Message` objects + that were created. """ return message.Message.create_messages(self._session, values) @@ -63,9 +65,9 @@ def claim_messages(self, value): :param value: The value must be a :class:`~openstack.message.v1.claim.Claim` instance. - :returns: The results of a claim - :rtype: list messages: The list of - :class:`~openstack.message.v1.message.Message`s claimed. + :returns: The list of + :class:`~openstack.message.v1.message.Message` objects + that were claimed. """ return claim.Claim.claim_messages(self._session, value) diff --git a/openstack/message/v2/_proxy.py b/openstack/message/v2/_proxy.py index 3fbe75bd6..7652ec6c7 100644 --- a/openstack/message/v2/_proxy.py +++ b/openstack/message/v2/_proxy.py @@ -80,7 +80,8 @@ def post_message(self, queue_name, messages): """Post messages to given queue :param queue_name: The name of target queue to post message to. - :param list messages: List of messages body and TTL to post. + :param messages: List of messages body and TTL to post. + :type messages: :py:class:`list` :returns: A string includes location of messages successfully posted. """ From 693bdaf947ad35153aa0e2c618c59de5ca5998e7 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Thu, 16 Feb 2017 16:47:52 -0500 Subject: [PATCH 1289/3836] Reorganize orchestration docs This change organizes the orchestration docs by topic rather than letting autodoc organize methods by the order they appear in the _proxy.py file. Change-Id: Ia7df2887c0b1fa1d4eb04a9372dab247f21311a4 --- doc/source/users/proxies/orchestration.rst | 35 +++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/doc/source/users/proxies/orchestration.rst b/doc/source/users/proxies/orchestration.rst index 9621a6479..4ad00e1d0 100644 --- a/doc/source/users/proxies/orchestration.rst +++ b/doc/source/users/proxies/orchestration.rst @@ -13,5 +13,38 @@ The orchestration high-level interface is available through the object. The ``orchestration`` member will only be added if the service is detected. +Stack Operations +^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.orchestration.v1._proxy.Proxy + + .. automethod:: openstack.orchestration.v1._proxy.Proxy.create_stack + .. automethod:: openstack.orchestration.v1._proxy.Proxy.check_stack + .. automethod:: openstack.orchestration.v1._proxy.Proxy.update_stack + .. automethod:: openstack.orchestration.v1._proxy.Proxy.delete_stack + .. automethod:: openstack.orchestration.v1._proxy.Proxy.find_stack + .. automethod:: openstack.orchestration.v1._proxy.Proxy.get_stack + .. automethod:: openstack.orchestration.v1._proxy.Proxy.stacks + .. automethod:: openstack.orchestration.v1._proxy.Proxy.validate_template + .. automethod:: openstack.orchestration.v1._proxy.Proxy.resources + +Software Configuration Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + .. autoclass:: openstack.orchestration.v1._proxy.Proxy - :members: + + .. automethod:: openstack.orchestration.v1._proxy.Proxy.create_software_config + .. automethod:: openstack.orchestration.v1._proxy.Proxy.delete_software_config + .. automethod:: openstack.orchestration.v1._proxy.Proxy.get_software_config + .. automethod:: openstack.orchestration.v1._proxy.Proxy.software_configs + +Software Deployment Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.orchestration.v1._proxy.Proxy + + .. automethod:: openstack.orchestration.v1._proxy.Proxy.create_software_deployment + .. automethod:: openstack.orchestration.v1._proxy.Proxy.update_software_deployment + .. automethod:: openstack.orchestration.v1._proxy.Proxy.delete_software_deployment + .. automethod:: openstack.orchestration.v1._proxy.Proxy.get_software_deployment + .. automethod:: openstack.orchestration.v1._proxy.Proxy.software_deployments From 759b2b4845fcfe2b7865ccd4469797c980c17720 Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Wed, 15 Feb 2017 10:16:59 -0800 Subject: [PATCH 1290/3836] Convert use of .register_uri to .register_uris This patch converts the use of .register_uri to .register_uris for the following files: test_image, test_project, test_meta. Change-Id: Ibf909d4b81361fab690c5514e616870bf9274a60 --- shade/tests/unit/base.py | 183 ++++++++------- shade/tests/unit/test_image.py | 387 +++++++++++++++---------------- shade/tests/unit/test_meta.py | 385 +++++++++++++++--------------- shade/tests/unit/test_project.py | 209 +++++++++-------- 4 files changed, 593 insertions(+), 571 deletions(-) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index f5ae962fe..1e5fd53de 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -128,7 +128,13 @@ def setUp(self, cloud_config_fixture='clouds.yaml'): # it on cleanup). Subclassing here could be 100% eliminated in the # future allowing any class to simply # self.useFixture(shade.RequestsMockFixture) and get all the benefits. - self._uri_registry = {} + + # NOTE(notmorgan): use an ordered dict here to ensure we preserve the + # order in which items are added to the uri_registry. This makes + # the behavior more consistent when dealing with ensuring the + # requests_mock uri/query_string matchers are ordered and parse the + # request in the correct orders. + self._uri_registry = collections.OrderedDict() self.discovery_json = os.path.join( self.fixtures_directory, 'discovery.json') self.use_keystone_v3() @@ -167,29 +173,33 @@ def mock_for_keystone_projects(self, project=None, v3=True, # Generate multiple projects project_list = [self._get_project_data(v3=v3) for c in range(0, project_count)] + uri_mock_list = [] if list_get: - self.register_uri( - 'GET', - self.get_mock_url( - service_type='identity', - interface='admin', - resource='projects', - base_url_append=base_url_append), - status_code=200, - json={'projects': [p.json_response['project'] - for p in project_list]}) + uri_mock_list.append( + dict(method='GET', + uri=self.get_mock_url( + service_type='identity', + interface='admin', + resource='projects', + base_url_append=base_url_append), + status_code=200, + json={'projects': [p.json_response['project'] + for p in project_list]}) + ) if id_get: for p in project_list: - self.register_uri( - 'GET', - self.get_mock_url( - service_type='identity', - interface='admin', - resource='projects', - append=[p.project_id], - base_url_append=base_url_append), - status_code=200, - json=p.json_response) + uri_mock_list.append( + dict(method='GET', + uri=self.get_mock_url( + service_type='identity', + interface='admin', + resource='projects', + append=[p.project_id], + base_url_append=base_url_append), + status_code=200, + json=p.json_response) + ) + self.__do_register_uris(uri_mock_list) return project_list def _get_project_data(self, project_name=None, enabled=None, @@ -264,36 +274,35 @@ def use_keystone_v3(self): self.adapter = self.useFixture(rm_fixture.Fixture()) self.calls = [] self._uri_registry.clear() - self.register_uri('GET', 'https://identity.example.com/', - text=open(self.discovery_json, 'r').read()) - self.register_uri( - 'POST', 'https://identity.example.com/v3/auth/tokens', - headers={ - 'X-Subject-Token': self.getUniqueString()}, - text=open( - os.path.join( - self.fixtures_directory, - 'catalog-v3.json'), - 'r').read()) + self.__do_register_uris([ + dict(method='GET', uri='https://identity.example.com/', + text=open(self.discovery_json, 'r').read()), + dict(method='POST', + uri='https://identity.example.com/v3/auth/tokens', + headers={ + 'X-Subject-Token': self.getUniqueString('KeystoneToken')}, + text=open(os.path.join( + self.fixtures_directory, 'catalog-v3.json'), 'r').read() + ) + ]) self._make_test_cloud(identity_api_version='3') def use_keystone_v2(self): self.adapter = self.useFixture(rm_fixture.Fixture()) self.calls = [] self._uri_registry.clear() - self.register_uri('GET', 'https://identity.example.com/', - text=open(self.discovery_json, 'r').read()) - self.register_uri( - 'POST', 'https://identity.example.com/v2.0/tokens', - text=open( - os.path.join( - self.fixtures_directory, - 'catalog-v2.json'), - 'r').read()) - self.register_uri('GET', 'https://identity.example.com/', - text=open(self.discovery_json, 'r').read()) - self.register_uri('GET', 'https://identity.example.com/', - text=open(self.discovery_json, 'r').read()) + self.__do_register_uris([ + dict(method='GET', uri='https://identity.example.com/', + text=open(self.discovery_json, 'r').read()), + dict(method='POST', uri='https://identity.example.com/v2.0/tokens', + text=open(os.path.join( + self.fixtures_directory, 'catalog-v2.json'), 'r').read() + ), + dict(method='GET', uri='https://identity.example.com/', + text=open(self.discovery_json, 'r').read()), + dict(method='GET', uri='https://identity.example.com/', + text=open(self.discovery_json, 'r').read()) + ]) self._make_test_cloud(cloud_name='_test_cloud_v2_', identity_api_version='2.0') @@ -304,8 +313,9 @@ def _add_discovery_uri_call(self): # us to inject another call to discovery where needed in a test that # no longer mocks out kyestoneclient and performs the extra round # trips. - self.register_uri('GET', 'https://identity.example.com/', - text=open(self.discovery_json, 'r').read()) + self.__do_register_uris([ + dict(method='GET', uri='https://identity.example.com/', + text=open(self.discovery_json, 'r').read())]) def _make_test_cloud(self, cloud_name='_test_cloud_', **kwargs): test_cloud = os.environ.get('SHADE_OS_CLOUD', cloud_name) @@ -318,14 +328,23 @@ def _make_test_cloud(self, cloud_name='_test_cloud_', **kwargs): cloud_config=self.cloud_config, log_inner_exceptions=True) - def use_glance(self, image_version_json='image-version.json'): + def get_glance_discovery_mock_dict( + self, image_version_json='image-version.json'): discovery_fixture = os.path.join( self.fixtures_directory, image_version_json) - self.register_uri( - 'GET', 'https://image.example.com/', - text=open(discovery_fixture, 'r').read()) + return dict(method='GET', uri='https://image.example.com/', + text=open(discovery_fixture, 'r').read()) - def register_uris(self, uri_mock_list): + def use_glance(self, image_version_json='image-version.json'): + # NOTE(notmorgan): This method is only meant to be used in "setUp" + # where the ordering of the url being registered is tightly controlled + # if the functionality of .use_glance is meant to be used during an + # actual test case, use .get_glance_discovery_mock and apply to the + # right location in the mock_uris when calling .register_uris + self.__do_register_uris([ + self.get_glance_discovery_mock_dict(image_version_json)]) + + def register_uris(self, uri_mock_list=None): """Mock a list of URIs and responses via requests mock. This method may be called only once per test-case to avoid odd @@ -354,13 +373,27 @@ def register_uris(self, uri_mock_list): Methods are allowed and will be collapsed into a single matcher. Each response will be returned in order as the URI+Method is hit. + :type uri_mock_list: list :return: None """ assert not self.__register_uris_called + self.__do_register_uris(uri_mock_list or []) + self.__register_uris_called = True + + def __do_register_uris(self, uri_mock_list=None): for to_mock in uri_mock_list: + kw_params = {k: to_mock.pop(k) + for k in ('request_headers', 'complete_qs', + '_real_http') + if k in to_mock} + method = to_mock.pop('method') uri = to_mock.pop('uri') - key = '{method}:{uri}'.format(method=method, uri=uri) + # NOTE(notmorgan): make sure the delimiter is non-url-safe, in this + # case "|" is used so that the split can be a bit easier on + # maintainers of this code. + key = '{method}|{uri}|{params}'.format( + method=method, uri=uri, params=kw_params) validate = to_mock.pop('validate', {}) headers = structures.CaseInsensitiveDict(to_mock.pop('headers', {})) @@ -374,33 +407,27 @@ def register_uris(self, uri_mock_list): method=method, url=uri, **validate) ] - self._uri_registry.setdefault(key, []).append(to_mock) - - for mock_method_uri, params in self._uri_registry.items(): - mock_method, mock_uri = mock_method_uri.split(':', 1) - self.adapter.register_uri(mock_method, mock_uri, params) - self.__register_uris_called = True + self._uri_registry.setdefault( + key, {'response_list': [], 'kw_params': kw_params}) + if self._uri_registry[key]['kw_params'] != kw_params: + raise AssertionError( + 'PROGRAMMING ERROR: key-word-params ' + 'should be part of the uri_key and cannot change, ' + 'it will affect the matcher in requests_mock. ' + '%(old)r != %(new)r' % + {'old': self._uri_registry[key]['kw_params'], + 'new': kw_params}) + self._uri_registry[key]['response_list'].append(to_mock) + + for mocked, params in self._uri_registry.items(): + mock_method, mock_uri, _ignored = mocked.split('|', 2) + self.adapter.register_uri( + mock_method, mock_uri, params['response_list'], + **params['kw_params']) def register_uri(self, method, uri, **kwargs): - validate = kwargs.pop('validate', {}) - key = '{method}:{uri}'.format(method=method, uri=uri) - headers = structures.CaseInsensitiveDict(kwargs.pop('headers', {})) - if 'content-type' not in headers: - headers[u'content-type'] = 'application/json' - kwargs['headers'] = headers - - if key in self._uri_registry: - self._uri_registry[key].append(kwargs) - self.adapter.register_uri(method, uri, self._uri_registry[key]) - else: - self._uri_registry[key] = [kwargs] - self.adapter.register_uri(method, uri, **kwargs) - - self.calls += [ - dict( - method=method, - url=uri, **validate) - ] + self.__do_register_uris([ + dict(method=method, uri=uri, **kwargs)]) def assert_calls(self, stop_after=None): for (x, (call, history)) in enumerate( diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index 551ae815a..d16835497 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -108,25 +108,26 @@ def test_download_image_two_outputs(self): output_path='fake_path', output_file=fake_fd) def test_download_image_no_images_found(self): - self.register_uri( - 'GET', 'https://image.example.com/v2/images', - json=dict(images=[])) + self.register_uris([ + dict(method='GET', + uri='https://image.example.com/v2/images', + json=dict(images=[]))]) self.assertRaises(exc.OpenStackCloudResourceNotFound, self.cloud.download_image, 'fake_image', output_path='fake_path') self.assert_calls() def _register_image_mocks(self): - self.register_uri( - 'GET', 'https://image.example.com/v2/images', - json=self.fake_search_return) - self.register_uri( - 'GET', 'https://image.example.com/v2/images/{id}/file'.format( - id=self.image_id), - content=self.output, - headers={ - 'Content-Type': 'application/octet-stream' - }) + self.register_uris([ + dict(method='GET', + uri='https://image.example.com/v2/images', + json=self.fake_search_return), + dict(method='GET', + uri='https://image.example.com/v2/images/{id}/file'.format( + id=self.image_id), + content=self.output, + headers={'Content-Type': 'application/octet-stream'}) + ]) def test_download_image_with_fd(self): self._register_image_mocks() @@ -145,15 +146,18 @@ def test_download_image_with_path(self): self.assert_calls() def test_empty_list_images(self): - self.register_uri( - 'GET', 'https://image.example.com/v2/images', json={'images': []}) + self.register_uris([ + dict(method='GET', uri='https://image.example.com/v2/images', + json={'images': []}) + ]) self.assertEqual([], self.cloud.list_images()) self.assert_calls() def test_list_images(self): - self.register_uri( - 'GET', 'https://image.example.com/v2/images', - json=self.fake_search_return) + self.register_uris([ + dict(method='GET', uri='https://image.example.com/v2/images', + json=self.fake_search_return) + ]) self.assertEqual( self.cloud._normalize_images([self.fake_image_dict]), self.cloud.list_images()) @@ -161,17 +165,16 @@ def test_list_images(self): def test_list_images_paginated(self): marker = str(uuid.uuid4()) - self.register_uri( - 'GET', 'https://image.example.com/v2/images', - json={ - 'images': [self.fake_image_dict], - 'next': '/v2/images?marker={marker}'.format(marker=marker), - }) - self.register_uri( - 'GET', - 'https://image.example.com/v2/images?marker={marker}'.format( - marker=marker), - json=self.fake_search_return) + self.register_uris([ + dict(method='GET', uri='https://image.example.com/v2/images', + json={'images': [self.fake_image_dict], + 'next': '/v2/images?marker={marker}'.format( + marker=marker)}), + dict(method='GET', + uri=('https://image.example.com/v2/images?' + 'marker={marker}'.format(marker=marker)), + json=self.fake_search_return) + ]) self.assertEqual( self.cloud._normalize_images([ self.fake_image_dict, self.fake_image_dict]), @@ -181,32 +184,27 @@ def test_list_images_paginated(self): def test_create_image_put_v2(self): self.cloud.image_api_use_tasks = False - self.register_uri( - 'GET', 'https://image.example.com/v2/images', - json={'images': []}) - - self.register_uri( - 'POST', 'https://image.example.com/v2/images', - json=self.fake_image_dict, - validate=dict( - json={ - u'container_format': u'bare', - u'disk_format': u'qcow2', - u'name': u'fake_image', - u'owner_specified.shade.md5': NO_MD5, - u'owner_specified.shade.object': u'images/fake_image', - u'owner_specified.shade.sha256': NO_SHA256, - u'visibility': u'private' - }), - ) - self.register_uri( - 'PUT', 'https://image.example.com/v2/images/{id}/file'.format( - id=self.image_id), - request_headers={'Content-Type': 'application/octet-stream'}) - - self.register_uri( - 'GET', 'https://image.example.com/v2/images', - json=self.fake_search_return) + self.register_uris([ + dict(method='GET', uri='https://image.example.com/v2/images', + json={'images': []}), + dict(method='POST', uri='https://image.example.com/v2/images', + json=self.fake_image_dict, + validate=dict( + json={u'container_format': u'bare', + u'disk_format': u'qcow2', + u'name': u'fake_image', + u'owner_specified.shade.md5': NO_MD5, + u'owner_specified.shade.object': u'images/fake_image', # noqa + u'owner_specified.shade.sha256': NO_SHA256, + u'visibility': u'private'}) + ), + dict(method='PUT', + uri='https://image.example.com/v2/images/{id}/file'.format( + id=self.image_id), + request_headers={'Content-Type': 'application/octet-stream'}), + dict(method='GET', uri='https://image.example.com/v2/images', + json=self.fake_search_return) + ]) self.cloud.create_image( 'fake_image', self.imagefile.name, wait=True, timeout=1, @@ -221,64 +219,6 @@ def test_create_image_task(self): container_name = 'image_upload_v2_test_container' endpoint = self.cloud._object_store_client.get_endpoint() - self.register_uri( - 'GET', 'https://image.example.com/v2/images', json={'images': []}) - - self.register_uri( - 'GET', 'https://object-store.example.com/info', - json=dict( - swift={'max_file_size': 1000}, - slo={'min_segment_size': 500})) - - self.register_uri( - 'HEAD', '{endpoint}/{container}'.format( - endpoint=endpoint, - container=container_name), - status_code=404) - - self.register_uri( - 'PUT', '{endpoint}/{container}'.format( - endpoint=endpoint, - container=container_name,), - status_code=201, - headers={ - 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', - 'Content-Length': '0', - 'Content-Type': 'text/html; charset=UTF-8', - }) - - self.register_uri( - 'HEAD', '{endpoint}/{container}'.format( - endpoint=endpoint, - container=container_name), - headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}) - - self.register_uri( - 'HEAD', '{endpoint}/{container}/{object}'.format( - endpoint=endpoint, - container=container_name, object=image_name), - status_code=404) - - self.register_uri( - 'PUT', '{endpoint}/{container}/{object}'.format( - endpoint=endpoint, - container=container_name, object=image_name), - status_code=201, - validate=dict( - headers={ - 'x-object-meta-x-shade-md5': NO_MD5, - 'x-object-meta-x-shade-sha256': NO_SHA256, - })) - task_id = str(uuid.uuid4()) args = dict( id=task_id, @@ -294,61 +234,92 @@ def test_create_image_task(self): del(image_no_checksums['owner_specified.shade.sha256']) del(image_no_checksums['owner_specified.shade.object']) - self.register_uri( - 'GET', 'https://image.example.com/v2/images', - json={'images': []}) - - self.register_uri( - 'POST', 'https://image.example.com/v2/tasks', - json=args, - validate=dict( - json=dict( - type='import', input={ - 'import_from': '{container}/{object}'.format( - container=container_name, object=image_name), - 'image_properties': {'name': image_name}}))) - - self.register_uri( - 'GET', - 'https://image.example.com/v2/tasks/{id}'.format(id=task_id), - status_code=503, text='Random error') - - self.register_uri( - 'GET', - 'https://image.example.com/v2/tasks/{id}'.format(id=task_id), - json={'images': args}) - - self.register_uri( - 'GET', 'https://image.example.com/v2/images', - json={'images': [image_no_checksums]}) - - self.register_uri( - 'PATCH', - 'https://image.example.com/v2/images/{id}'.format( - id=self.image_id), - validate=dict( - json=sorted([ - { - u'op': u'add', - u'value': '{container}/{object}'.format( - container=container_name, object=image_name), - u'path': u'/owner_specified.shade.object' - }, { - u'op': u'add', - u'value': NO_MD5, - u'path': u'/owner_specified.shade.md5' - }, { - u'op': u'add', u'value': NO_SHA256, - u'path': u'/owner_specified.shade.sha256' - }], key=operator.itemgetter('value')), - headers={ - 'Content-Type': - 'application/openstack-images-v2.1-json-patch' - })) - - self.register_uri( - 'GET', 'https://image.example.com/v2/images', - json=self.fake_search_return) + self.register_uris([ + dict(method='GET', uri='https://image.example.com/v2/images', + json={'images': []}), + dict(method='GET', uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': 1000}, + slo={'min_segment_size': 500})), + dict(method='HEAD', + uri='{endpoint}/{container}'.format( + endpoint=endpoint, container=container_name), + status_code=404), + dict(method='PUT', + uri='{endpoint}/{container}'.format( + endpoint=endpoint, container=container_name), + status_code=201, + headers={'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=UTF-8'}), + dict(method='HEAD', + uri='{endpoint}/{container}'.format( + endpoint=endpoint, container=container_name), + headers={'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', # noqa + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}), + dict(method='HEAD', + uri='{endpoint}/{container}/{object}'.format( + endpoint=endpoint, container=container_name, + object=image_name), + status_code=404), + dict(method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=endpoint, container=container_name, + object=image_name), + status_code=201, + validate=dict( + headers={'x-object-meta-x-shade-md5': NO_MD5, + 'x-object-meta-x-shade-sha256': NO_SHA256}) + ), + dict(method='GET', uri='https://image.example.com/v2/images', + json={'images': []}), + dict(method='POST', uri='https://image.example.com/v2/tasks', + json=args, + validate=dict( + json=dict( + type='import', input={ + 'import_from': '{container}/{object}'.format( + container=container_name, object=image_name), + 'image_properties': {'name': image_name}})) + ), + dict(method='GET', + uri='https://image.example.com/v2/tasks/{id}'.format( + id=task_id), + status_code=503, text='Random error'), + dict(method='GET', + uri='https://image.example.com/v2/tasks/{id}'.format( + id=task_id), + json={'images': args}), + dict(method='GET', uri='https://image.example.com/v2/images', + json={'images': [image_no_checksums]}), + dict(method='PATCH', + uri='https://image.example.com/v2/images/{id}'.format( + id=self.image_id), + validate=dict( + json=sorted([{u'op': u'add', + u'value': '{container}/{object}'.format( + container=container_name, + object=image_name), + u'path': u'/owner_specified.shade.object'}, + {u'op': u'add', u'value': NO_MD5, + u'path': u'/owner_specified.shade.md5'}, + {u'op': u'add', u'value': NO_SHA256, + u'path': u'/owner_specified.shade.sha256'}], + key=operator.itemgetter('value')), + headers={ + 'Content-Type': + 'application/openstack-images-v2.1-json-patch'}) + ), + dict(method='GET', uri='https://image.example.com/v2/images', + json=self.fake_search_return) + ]) self.cloud.create_image( image_name, self.imagefile.name, wait=True, timeout=1, @@ -719,9 +690,11 @@ def test_version_discovery_skip(self): self.cloud.cloud_config.config['image_endpoint_override'] = \ 'https://image.example.com/v2/override' - self.register_uri( - 'GET', 'https://image.example.com/v2/override/images', - json={'images': []}) + self.register_uris([ + dict(method='GET', + uri='https://image.example.com/v2/override/images', + json={'images': []}) + ]) self.assertEqual([], self.cloud.list_images()) self.assertEqual( self.cloud._image_client.endpoint_override, @@ -735,22 +708,26 @@ def test_create_image_volume(self): volume_id = 'some-volume' - self.register_uri( - 'POST', '{endpoint}/volumes/{id}/action'.format( - endpoint=CINDER_URL, id=volume_id), - json={'os-volume_upload_image': {'image_id': self.image_id}}, - validate=dict(json={ - u'os-volume_upload_image': { - u'container_format': u'bare', - u'disk_format': u'qcow2', - u'force': False, - u'image_name': u'fake_image'}})) - - self.use_glance() - - self.register_uri( - 'GET', 'https://image.example.com/v2/images', - json=self.fake_search_return) + self.register_uris([ + dict(method='POST', + uri='{endpoint}/volumes/{id}/action'.format( + endpoint=CINDER_URL, id=volume_id), + json={'os-volume_upload_image': {'image_id': self.image_id}}, + validate=dict(json={ + u'os-volume_upload_image': { + u'container_format': u'bare', + u'disk_format': u'qcow2', + u'force': False, + u'image_name': u'fake_image'}}) + ), + # NOTE(notmorgan): Glance discovery happens here, insert the + # glance discovery mock at this point, DO NOT use the + # .use_glance() method, that is intended only for use in + # .setUp + self.get_glance_discovery_mock_dict(), + dict(method='GET', uri='https://image.example.com/v2/images', + json=self.fake_search_return) + ]) self.cloud.create_image( 'fake_image', self.imagefile.name, wait=True, timeout=1, @@ -762,22 +739,26 @@ def test_create_image_volume_duplicate(self): volume_id = 'some-volume' - self.register_uri( - 'POST', '{endpoint}/volumes/{id}/action'.format( - endpoint=CINDER_URL, id=volume_id), - json={'os-volume_upload_image': {'image_id': self.image_id}}, - validate=dict(json={ - u'os-volume_upload_image': { - u'container_format': u'bare', - u'disk_format': u'qcow2', - u'force': True, - u'image_name': u'fake_image'}})) - - self.use_glance() - - self.register_uri( - 'GET', 'https://image.example.com/v2/images', - json=self.fake_search_return) + self.register_uris([ + dict(method='POST', + uri='{endpoint}/volumes/{id}/action'.format( + endpoint=CINDER_URL, id=volume_id), + json={'os-volume_upload_image': {'image_id': self.image_id}}, + validate=dict(json={ + u'os-volume_upload_image': { + u'container_format': u'bare', + u'disk_format': u'qcow2', + u'force': True, + u'image_name': u'fake_image'}}) + ), + # NOTE(notmorgan): Glance discovery happens here, insert the + # glance discovery mock at this point, DO NOT use the + # .use_glance() method, that is intended only for use in + # .setUp + self.get_glance_discovery_mock_dict(), + dict(method='GET', uri='https://image.example.com/v2/images', + json=self.fake_search_return) + ]) self.cloud.create_image( 'fake_image', self.imagefile.name, wait=True, timeout=1, diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index f91a86055..292a68f45 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -274,15 +274,17 @@ def test_get_server_ip(self): PUBLIC_V4, meta.get_server_ip(srv, ext_tag='floating')) def test_get_server_private_ip(self): - self.register_uri( - 'GET', 'https://network.example.com/v2.0/networks.json', - json={'networks': [{ - 'id': 'test-net-id', - 'name': 'test-net-name' - }]}) - self.register_uri( - 'GET', 'https://network.example.com/v2.0/subnets.json', - json={'subnets': SUBNETS_WITH_NAT}) + self.register_uris([ + dict(method='GET', + uri='https://network.example.com/v2.0/networks.json', + json={'networks': [{ + 'id': 'test-net-id', + 'name': 'test-net-name'}]} + ), + dict(method='GET', + uri='https://network.example.com/v2.0/subnets.json', + json={'subnets': SUBNETS_WITH_NAT}) + ]) srv = meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', @@ -310,43 +312,39 @@ def test_get_server_private_ip_devstack( mock_get_flavor_name.return_value = 'm1.tiny' mock_get_volumes.return_value = [] - self.register_uri( - 'GET', - 'https://network.example.com/v2.0/ports.json?device_id=test-id', - json={'ports': [{ - 'id': 'test_port_id', - 'mac_address': 'fa:16:3e:ae:7d:42', - 'device_id': 'test-id', - }]}) - - self.register_uri( - 'GET', - 'https://network.example.com/v2.0/floatingips.json' - '?port_id=test_port_id', - json={'floatingips': []}) - - self.register_uri( - 'GET', 'https://network.example.com/v2.0/networks.json', - json={'networks': [ - { - 'id': 'test_pnztt_net', - 'name': 'test_pnztt_net', - 'router:external': False, - }, - { - 'id': 'private', - 'name': 'private', - } - ]}) - - self.register_uri( - 'GET', 'https://network.example.com/v2.0/subnets.json', - json={'subnets': SUBNETS_WITH_NAT}) - - self.register_uri( - 'GET', '{endpoint}/servers/test-id/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_groups': []}) + self.register_uris([ + dict(method='GET', + uri=('https://network.example.com/v2.0/ports.json?' + 'device_id=test-id'), + json={'ports': [{ + 'id': 'test_port_id', + 'mac_address': 'fa:16:3e:ae:7d:42', + 'device_id': 'test-id'}]} + ), + dict(method='GET', + uri=('https://network.example.com/v2.0/' + 'floatingips.json?port_id=test_port_id'), + json={'floatingips': []}), + + dict(method='GET', + uri='https://network.example.com/v2.0/networks.json', + json={'networks': [ + {'id': 'test_pnztt_net', + 'name': 'test_pnztt_net', + 'router:external': False + }, + {'id': 'private', + 'name': 'private'}]} + ), + dict(method='GET', + uri='https://network.example.com/v2.0/subnets.json', + json={'subnets': SUBNETS_WITH_NAT}), + + dict(method='GET', + uri='{endpoint}/servers/test-id/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'security_groups': []}) + ]) srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', @@ -378,26 +376,25 @@ def test_get_server_private_ip_no_fip( mock_get_flavor_name.return_value = 'm1.tiny' mock_get_volumes.return_value = [] - self.register_uri( - 'GET', 'https://network.example.com/v2.0/networks.json', - json={'networks': [ - { - 'id': 'test_pnztt_net', - 'name': 'test_pnztt_net', - 'router:external': False, - }, - { - 'id': 'private', - 'name': 'private', - } - ]}) - self.register_uri( - 'GET', 'https://network.example.com/v2.0/subnets.json', - json={'subnets': SUBNETS_WITH_NAT}) - self.register_uri( - 'GET', '{endpoint}/servers/test-id/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_groups': []}) + self.register_uris([ + dict(method='GET', + uri='https://network.example.com/v2.0/networks.json', + json={'networks': [ + {'id': 'test_pnztt_net', + 'name': 'test_pnztt_net', + 'router:external': False, + }, + {'id': 'private', + 'name': 'private'}]} + ), + dict(method='GET', + uri='https://network.example.com/v2.0/subnets.json', + json={'subnets': SUBNETS_WITH_NAT}), + dict(method='GET', + uri='{endpoint}/servers/test-id/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'security_groups': []}) + ]) srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', @@ -427,26 +424,27 @@ def test_get_server_cloud_no_fips( mock_get_image_name.return_value = 'cirros-0.3.4-x86_64-uec' mock_get_flavor_name.return_value = 'm1.tiny' mock_get_volumes.return_value = [] - self.register_uri( - 'GET', 'https://network.example.com/v2.0/networks.json', - json={'networks': [ - { - 'id': 'test_pnztt_net', - 'name': 'test_pnztt_net', - 'router:external': False, - }, - { - 'id': 'private', - 'name': 'private', - } - ]}) - self.register_uri( - 'GET', 'https://network.example.com/v2.0/subnets.json', - json={'subnets': SUBNETS_WITH_NAT}) - self.register_uri( - 'GET', '{endpoint}/servers/test-id/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_groups': []}) + self.register_uris([ + dict(method='GET', + uri='https://network.example.com/v2.0/networks.json', + json={'networks': [ + { + 'id': 'test_pnztt_net', + 'name': 'test_pnztt_net', + 'router:external': False, + }, + { + 'id': 'private', + 'name': 'private'}]} + ), + dict(method='GET', + uri='https://network.example.com/v2.0/subnets.json', + json={'subnets': SUBNETS_WITH_NAT}), + dict(method='GET', + uri='{endpoint}/servers/test-id/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'security_groups': []}) + ]) srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', @@ -474,47 +472,45 @@ def test_get_server_cloud_missing_fips( mock_get_flavor_name.return_value = 'm1.tiny' mock_get_volumes.return_value = [] - self.register_uri( - 'GET', - 'https://network.example.com/v2.0/ports.json?device_id=test-id', - json={'ports': [{ - 'id': 'test_port_id', - 'mac_address': 'fa:16:3e:ae:7d:42', - 'device_id': 'test-id', - }]}) - - self.register_uri( - 'GET', - 'https://network.example.com/v2.0/floatingips.json' - '?port_id=test_port_id', - json={'floatingips': [{ - 'id': 'floating-ip-id', - 'port_id': 'test_port_id', - 'fixed_ip_address': PRIVATE_V4, - 'floating_ip_address': PUBLIC_V4, - }]}) - - self.register_uri( - 'GET', 'https://network.example.com/v2.0/networks.json', - json={'networks': [ - { - 'id': 'test_pnztt_net', - 'name': 'test_pnztt_net', - 'router:external': False, - }, - { - 'id': 'private', - 'name': 'private', - } - ]}) - self.register_uri( - 'GET', 'https://network.example.com/v2.0/subnets.json', - json={'subnets': SUBNETS_WITH_NAT}) - - self.register_uri( - 'GET', '{endpoint}/servers/test-id/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_groups': []}) + self.register_uris([ + dict(method='GET', + uri=('https://network.example.com/v2.0/ports.json?' + 'device_id=test-id'), + json={'ports': [{ + 'id': 'test_port_id', + 'mac_address': 'fa:16:3e:ae:7d:42', + 'device_id': 'test-id'}]} + ), + dict(method='GET', + uri=('https://network.example.com/v2.0/floatingips.json' + '?port_id=test_port_id'), + json={'floatingips': [{ + 'id': 'floating-ip-id', + 'port_id': 'test_port_id', + 'fixed_ip_address': PRIVATE_V4, + 'floating_ip_address': PUBLIC_V4, + }]}), + dict(method='GET', + uri='https://network.example.com/v2.0/networks.json', + json={'networks': [ + { + 'id': 'test_pnztt_net', + 'name': 'test_pnztt_net', + 'router:external': False, + }, + { + 'id': 'private', + 'name': 'private', + } + ]}), + dict(method='GET', + uri='https://network.example.com/v2.0/subnets.json', + json={'subnets': SUBNETS_WITH_NAT}), + dict(method='GET', + uri='{endpoint}/servers/test-id/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'security_groups': []}) + ]) srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', @@ -546,10 +542,12 @@ def test_get_server_cloud_rackspace_v6( mock_get_flavor_name.return_value = 'm1.tiny' mock_get_volumes.return_value = [] - self.register_uri( - 'GET', '{endpoint}/servers/test-id/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_groups': []}) + self.register_uris([ + dict(method='GET', + uri='{endpoint}/servers/test-id/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'security_groups': []}) + ]) srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', @@ -597,16 +595,18 @@ def test_get_server_cloud_osic_split( mock_get_flavor_name.return_value = 'm1.tiny' mock_get_volumes.return_value = [] - self.register_uri( - 'GET', 'https://network.example.com/v2.0/networks.json', - json={'networks': OSIC_NETWORKS}) - self.register_uri( - 'GET', 'https://network.example.com/v2.0/subnets.json', - json={'subnets': OSIC_SUBNETS}) - self.register_uri( - 'GET', '{endpoint}/servers/test-id/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_groups': []}) + self.register_uris([ + dict(method='GET', + uri='https://network.example.com/v2.0/networks.json', + json={'networks': OSIC_NETWORKS}), + dict(method='GET', + uri='https://network.example.com/v2.0/subnets.json', + json={'subnets': OSIC_SUBNETS}), + dict(method='GET', + uri='{endpoint}/servers/test-id/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'security_groups': []}) + ]) srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', @@ -639,17 +639,18 @@ def test_get_server_cloud_osic_split( def test_get_server_external_ipv4_neutron(self): # Testing Clouds with Neutron - self.register_uri( - 'GET', 'https://network.example.com/v2.0/networks.json', - json={'networks': [{ - 'id': 'test-net-id', - 'name': 'test-net', - 'router:external': True, - }]}) - self.register_uri( - 'GET', 'https://network.example.com/v2.0/subnets.json', - json={'subnets': SUBNETS_WITH_NAT}) - + self.register_uris([ + dict(method='GET', + uri='https://network.example.com/v2.0/networks.json', + json={'networks': [{ + 'id': 'test-net-id', + 'name': 'test-net', + 'router:external': True + }]}), + dict(method='GET', + uri='https://network.example.com/v2.0/subnets.json', + json={'subnets': SUBNETS_WITH_NAT}) + ]) srv = meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', addresses={'test-net': [{ @@ -663,17 +664,19 @@ def test_get_server_external_ipv4_neutron(self): def test_get_server_external_provider_ipv4_neutron(self): # Testing Clouds with Neutron - self.register_uri( - 'GET', 'https://network.example.com/v2.0/networks.json', - json={'networks': [{ - 'id': 'test-net-id', - 'name': 'test-net', - 'provider:network_type': 'vlan', - 'provider:physical_network': 'vlan', - }]}) - self.register_uri( - 'GET', 'https://network.example.com/v2.0/subnets.json', - json={'subnets': SUBNETS_WITH_NAT}) + self.register_uris([ + dict(method='GET', + uri='https://network.example.com/v2.0/networks.json', + json={'networks': [{ + 'id': 'test-net-id', + 'name': 'test-net', + 'provider:network_type': 'vlan', + 'provider:physical_network': 'vlan', + }]}), + dict(method='GET', + uri='https://network.example.com/v2.0/subnets.json', + json={'subnets': SUBNETS_WITH_NAT}) + ]) srv = meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', @@ -688,19 +691,20 @@ def test_get_server_external_provider_ipv4_neutron(self): def test_get_server_internal_provider_ipv4_neutron(self): # Testing Clouds with Neutron - self.register_uri( - 'GET', 'https://network.example.com/v2.0/networks.json', - json={'networks': [{ - 'id': 'test-net-id', - 'name': 'test-net', - 'router:external': False, - 'provider:network_type': 'vxlan', - 'provider:physical_network': None, - }]}) - self.register_uri( - 'GET', 'https://network.example.com/v2.0/subnets.json', - json={'subnets': SUBNETS_WITH_NAT}) - + self.register_uris([ + dict(method='GET', + uri='https://network.example.com/v2.0/networks.json', + json={'networks': [{ + 'id': 'test-net-id', + 'name': 'test-net', + 'router:external': False, + 'provider:network_type': 'vxlan', + 'provider:physical_network': None, + }]}), + dict(method='GET', + uri='https://network.example.com/v2.0/subnets.json', + json={'subnets': SUBNETS_WITH_NAT}) + ]) srv = meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', addresses={'test-net': [{ @@ -716,16 +720,18 @@ def test_get_server_internal_provider_ipv4_neutron(self): def test_get_server_external_none_ipv4_neutron(self): # Testing Clouds with Neutron - self.register_uri( - 'GET', 'https://network.example.com/v2.0/networks.json', - json={'networks': [{ - 'id': 'test-net-id', - 'name': 'test-net', - 'router:external': False, - }]}) - self.register_uri( - 'GET', 'https://network.example.com/v2.0/subnets.json', - json={'subnets': SUBNETS_WITH_NAT}) + self.register_uris([ + dict(method='GET', + uri='https://network.example.com/v2.0/networks.json', + json={'networks': [{ + 'id': 'test-net-id', + 'name': 'test-net', + 'router:external': False, + }]}), + dict(method='GET', + uri='https://network.example.com/v2.0/subnets.json', + json={'subnets': SUBNETS_WITH_NAT}) + ]) srv = meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', @@ -756,9 +762,10 @@ def test_get_server_external_ipv4_neutron_accessIPv6(self): def test_get_server_external_ipv4_neutron_exception(self): # Testing Clouds with a non working Neutron - self.register_uri( - 'GET', 'https://network.example.com/v2.0/networks.json', - status_code=404) + self.register_uris([ + dict(method='GET', + uri='https://network.example.com/v2.0/networks.json', + status_code=404)]) srv = meta.obj_to_dict(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', diff --git a/shade/tests/unit/test_project.py b/shade/tests/unit/test_project.py index d6d206cf5..1751376d8 100644 --- a/shade/tests/unit/test_project.py +++ b/shade/tests/unit/test_project.py @@ -36,17 +36,16 @@ def get_mock_url(self, service_type='identity', interface='admin', def test_create_project_v2(self): self.use_keystone_v2() project_data = self._get_project_data(v3=False) - self.register_uri( - 'POST', - self.get_mock_url(v3=False), - status_code=200, - json=project_data.json_response, - validate=project_data.json_request) - self.register_uri( - 'GET', - self.get_mock_url(v3=False, append=[project_data.project_id]), - status_code=200, - json=project_data.json_response) + self.register_uris([ + dict(method='POST', uri=self.get_mock_url(v3=False), + status_code=200, json=project_data.json_response, + validate=project_data.json_request), + dict(method='GET', + uri=self.get_mock_url( + v3=False, append=[project_data.project_id]), + status_code=200, + json=project_data.json_response) + ]) project = self.op_cloud.create_project( name=project_data.project_name, description=project_data.description) @@ -59,17 +58,17 @@ def test_create_project_v3(self,): self._add_discovery_uri_call() project_data = self._get_project_data( description=self.getUniqueString('projectDesc')) - self.register_uri( - 'POST', - self.get_mock_url(), - status_code=200, - json=project_data.json_response, - validate=project_data.json_request) - self.register_uri( - 'GET', - self.get_mock_url(append=[project_data.project_id]), - status_code=200, - json=project_data.json_response) + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url(), + status_code=200, + json=project_data.json_response, + validate=project_data.json_request), + dict(method='GET', + uri=self.get_mock_url(append=[project_data.project_id]), + status_code=200, + json=project_data.json_response) + ]) project = self.op_cloud.create_project( name=project_data.project_name, description=project_data.description, @@ -94,41 +93,43 @@ def test_create_project_v3_no_domain(self): def test_delete_project_v2(self): self.use_keystone_v2() project_data = self._get_project_data(v3=False) - self.register_uri( - 'GET', - self.get_mock_url(v3=False), - status_code=200, - json={'tenants': [project_data.json_response['tenant']]}) - self.register_uri( - 'DELETE', - self.get_mock_url(v3=False, append=[project_data.project_id]), - status_code=204) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(v3=False), + status_code=200, + json={'tenants': [project_data.json_response['tenant']]}), + dict(method='DELETE', + uri=self.get_mock_url( + v3=False, append=[project_data.project_id]), + status_code=204) + ]) self.op_cloud.delete_project(project_data.project_id) self.assert_calls() def test_delete_project_v3(self): self._add_discovery_uri_call() project_data = self._get_project_data(v3=False) - self.register_uri( - 'GET', - self.get_mock_url(), - status_code=200, - json={'projects': [project_data.json_response['tenant']]}) - self.register_uri( - 'DELETE', - self.get_mock_url(append=[project_data.project_id]), - status_code=204) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'projects': [project_data.json_response['tenant']]}), + dict(method='DELETE', + uri=self.get_mock_url(append=[project_data.project_id]), + status_code=204) + ]) self.op_cloud.delete_project(project_data.project_id) self.assert_calls() def test_update_project_not_found(self): self._add_discovery_uri_call() project_data = self._get_project_data() - self.register_uri( - 'GET', - self.get_mock_url(), - status_code=200, - json={'projects': []}) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'projects': []}) + ]) # NOTE(notmorgan): This test (and shade) does not represent a case # where the project is in the project list but a 404 is raised when # the PATCH is issued. This is a bug in shade and should be fixed, @@ -146,22 +147,23 @@ def test_update_project_v2(self): project_data = self._get_project_data( v3=False, description=self.getUniqueString('projectDesc')) - self.register_uri( - 'GET', - self.get_mock_url(v3=False), - status_code=200, - json={'tenants': [project_data.json_response['tenant']]}) - self.register_uri( - 'POST', - self.get_mock_url(v3=False, append=[project_data.project_id]), - status_code=200, - json=project_data.json_response, - validate=project_data.json_request) - self.register_uri( - 'GET', - self.get_mock_url(v3=False, append=[project_data.project_id]), - status_code=200, - json=project_data.json_response) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(v3=False), + status_code=200, + json={'tenants': [project_data.json_response['tenant']]}), + dict(method='POST', + uri=self.get_mock_url( + v3=False, append=[project_data.project_id]), + status_code=200, + json=project_data.json_response, + validate=project_data.json_request), + dict(method='GET', + uri=self.get_mock_url( + v3=False, append=[project_data.project_id]), + status_code=200, + json=project_data.json_response) + ]) project = self.op_cloud.update_project( project_data.project_id, description=project_data.description) @@ -176,23 +178,21 @@ def test_update_project_v3(self): self._add_discovery_uri_call() project_data = self._get_project_data( description=self.getUniqueString('projectDesc')) - self.register_uri( - 'GET', - self.get_mock_url( - resource='projects?domain_id=%s' % project_data.domain_id), - status_code=200, - json={'projects': [project_data.json_response['project']]}) - self.register_uri( - 'PATCH', - self.get_mock_url(append=[project_data.project_id]), - status_code=200, - json=project_data.json_response, - validate=project_data.json_request) - self.register_uri( - 'GET', - self.get_mock_url(append=[project_data.project_id]), - status_code=200, - json=project_data.json_response) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + resource=('projects?domain_id=%s' % + project_data.domain_id)), + status_code=200, + json={'projects': [project_data.json_response['project']]}), + dict(method='PATCH', + uri=self.get_mock_url(append=[project_data.project_id]), + status_code=200, json=project_data.json_response, + validate=project_data.json_request), + dict(method='GET', + uri=self.get_mock_url(append=[project_data.project_id]), + status_code=200, json=project_data.json_response) + ]) project = self.op_cloud.update_project( project_data.project_id, description=project_data.description, @@ -208,12 +208,14 @@ def test_list_projects_v3(self): self._add_discovery_uri_call() project_data = self._get_project_data( description=self.getUniqueString('projectDesc')) - self.register_uri( - 'GET', - self.get_mock_url( - resource='projects?domain_id=%s' % project_data.domain_id), - status_code=200, - json={'projects': [project_data.json_response['project']]}) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + resource=('projects?domain_id=%s' % + project_data.domain_id)), + status_code=200, + json={'projects': [project_data.json_response['project']]}) + ]) projects = self.op_cloud.list_projects(project_data.domain_id) self.assertThat(len(projects), matchers.Equals(1)) self.assertThat( @@ -224,12 +226,14 @@ def test_list_projects_v3_kwarg(self): self._add_discovery_uri_call() project_data = self._get_project_data( description=self.getUniqueString('projectDesc')) - self.register_uri( - 'GET', - self.get_mock_url( - resource='projects?domain_id=%s' % project_data.domain_id), - status_code=200, - json={'projects': [project_data.json_response['project']]}) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + resource=('projects?domain_id=%s' % + project_data.domain_id)), + status_code=200, + json={'projects': [project_data.json_response['project']]}) + ]) projects = self.op_cloud.list_projects( domain_id=project_data.domain_id) self.assertThat(len(projects), matchers.Equals(1)) @@ -241,11 +245,12 @@ def test_list_projects_search_compat(self): self._add_discovery_uri_call() project_data = self._get_project_data( description=self.getUniqueString('projectDesc')) - self.register_uri( - 'GET', - self.get_mock_url(), - status_code=200, - json={'projects': [project_data.json_response['project']]}) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'projects': [project_data.json_response['project']]}) + ]) projects = self.op_cloud.search_projects(project_data.project_id) self.assertThat(len(projects), matchers.Equals(1)) self.assertThat( @@ -256,12 +261,14 @@ def test_list_projects_search_compat_v3(self): self._add_discovery_uri_call() project_data = self._get_project_data( description=self.getUniqueString('projectDesc')) - self.register_uri( - 'GET', - self.get_mock_url( - resource='projects?domain_id=%s' % project_data.domain_id), - status_code=200, - json={'projects': [project_data.json_response['project']]}) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + resource=('projects?domain_id=%s' % + project_data.domain_id)), + status_code=200, + json={'projects': [project_data.json_response['project']]}) + ]) projects = self.op_cloud.search_projects( domain_id=project_data.domain_id) self.assertThat(len(projects), matchers.Equals(1)) From 80709fd9a488d83563ed5c5354cca6628c08445a Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Wed, 15 Feb 2017 13:20:09 -0800 Subject: [PATCH 1291/3836] Convert test_object to use .register_uris Convert test_object tests to use .register_uris. .register_uri is no longer used and deleted. Change-Id: Iee156afd5ee5931ca4087a70dd92bb3b0df2f62d --- shade/tests/unit/base.py | 4 - shade/tests/unit/test_object.py | 1007 +++++++++++++++---------------- 2 files changed, 494 insertions(+), 517 deletions(-) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 1e5fd53de..242e44ef6 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -425,10 +425,6 @@ def __do_register_uris(self, uri_mock_list=None): mock_method, mock_uri, params['response_list'], **params['kw_params']) - def register_uri(self, method, uri, **kwargs): - self.__do_register_uris([ - dict(method=method, uri=uri, **kwargs)]) - def assert_calls(self, stop_after=None): for (x, (call, history)) in enumerate( zip(self.calls, self.adapter.request_history)): diff --git a/shade/tests/unit/test_object.py b/shade/tests/unit/test_object.py index 3e1117e51..54fb65b6e 100644 --- a/shade/tests/unit/test_object.py +++ b/shade/tests/unit/test_object.py @@ -40,87 +40,81 @@ class TestObject(BaseTestObject): def test_create_container(self): """Test creating a (private) container""" - self.register_uri( - 'HEAD', self.container_endpoint, status_code=404), - - self.register_uri( - 'PUT', self.container_endpoint, - status_code=201, - headers={ - 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', - 'Content-Length': '0', - 'Content-Type': 'text/html; charset=UTF-8', - }) - - self.register_uri( - 'HEAD', self.container_endpoint, - headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}) + self.register_uris([ + dict(method='HEAD', uri=self.container_endpoint, status_code=404), + dict(method='PUT', uri=self.container_endpoint, + status_code=201, + headers={ + 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=UTF-8', + }), + dict(method='HEAD', uri=self.container_endpoint, + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}) + ]) self.cloud.create_container(self.container) self.assert_calls() def test_create_container_public(self): """Test creating a public container""" - self.register_uri( - 'HEAD', self.container_endpoint, - status_code=404) - - self.register_uri( - 'PUT', self.container_endpoint, - status_code=201, - headers={ - 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', - 'Content-Length': '0', - 'Content-Type': 'text/html; charset=UTF-8', - }) - - self.register_uri( - 'POST', self.container_endpoint, - status_code=201, - validate=dict( - headers={ - 'x-container-read': - shade.openstackcloud.OBJECT_CONTAINER_ACLS['public']})) - - self.register_uri( - 'HEAD', self.container_endpoint, - headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}) + self.register_uris([ + dict(method='HEAD', uri=self.container_endpoint, + status_code=404), + dict(method='PUT', uri=self.container_endpoint, + status_code=201, + headers={ + 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=UTF-8', + }), + dict(method='POST', uri=self.container_endpoint, + status_code=201, + validate=dict( + headers={ + 'x-container-read': + shade.openstackcloud.OBJECT_CONTAINER_ACLS[ + 'public']})), + dict(method='HEAD', uri=self.container_endpoint, + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}) + ]) self.cloud.create_container(self.container, public=True) self.assert_calls() def test_create_container_exists(self): """Test creating a container that exists.""" - self.register_uri( - 'HEAD', self.container_endpoint, - headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}) + self.register_uris([ + dict(method='HEAD', uri=self.container_endpoint, + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}) + ]) container = self.cloud.create_container(self.container) @@ -128,16 +122,17 @@ def test_create_container_exists(self): self.assertIsNotNone(container) def test_delete_container(self): - self.register_uri('DELETE', self.container_endpoint) + self.register_uris([ + dict(method='DELETE', uri=self.container_endpoint)]) self.assertTrue(self.cloud.delete_container(self.container)) self.assert_calls() def test_delete_container_404(self): """No exception when deleting a container that does not exist""" - self.register_uri( - 'DELETE', self.container_endpoint, - status_code=404) + self.register_uris([ + dict(method='DELETE', uri=self.container_endpoint, + status_code=404)]) self.assertFalse(self.cloud.delete_container(self.container)) self.assert_calls() @@ -145,9 +140,9 @@ def test_delete_container_404(self): def test_delete_container_error(self): """Non-404 swift error re-raised as OSCE""" # 409 happens if the container is not empty - self.register_uri( - 'DELETE', self.container_endpoint, - status_code=409) + self.register_uris([ + dict(method='DELETE', uri=self.container_endpoint, + status_code=409)]) self.assertRaises( shade.OpenStackCloudException, self.cloud.delete_container, self.container) @@ -157,10 +152,10 @@ def test_update_container(self): headers = { 'x-container-read': shade.openstackcloud.OBJECT_CONTAINER_ACLS['public']} - self.register_uri( - 'POST', self.container_endpoint, - status_code=204, - validate=dict(headers=headers)) + self.register_uris([ + dict(method='POST', uri=self.container_endpoint, + status_code=204, + validate=dict(headers=headers))]) self.cloud.update_container(self.container, headers) self.assert_calls() @@ -172,35 +167,37 @@ def test_update_container_error(self): # method, and I cannot make a synthetic failure to validate a real # error code. So we're really just testing the shade adapter error # raising logic here, rather than anything specific to swift. - self.register_uri( - 'POST', self.container_endpoint, - status_code=409) + self.register_uris([ + dict(method='POST', uri=self.container_endpoint, + status_code=409)]) self.assertRaises( shade.OpenStackCloudException, self.cloud.update_container, self.container, dict(foo='bar')) self.assert_calls() def test_set_container_access_public(self): - self.register_uri( - 'POST', self.container_endpoint, - status_code=204, - validate=dict( - headers={ - 'x-container-read': - shade.openstackcloud.OBJECT_CONTAINER_ACLS['public']})) + self.register_uris([ + dict(method='POST', uri=self.container_endpoint, + status_code=204, + validate=dict( + headers={ + 'x-container-read': + shade.openstackcloud.OBJECT_CONTAINER_ACLS[ + 'public']}))]) self.cloud.set_container_access(self.container, 'public') self.assert_calls() def test_set_container_access_private(self): - self.register_uri( - 'POST', self.container_endpoint, - status_code=204, - validate=dict( - headers={ - 'x-container-read': - shade.openstackcloud.OBJECT_CONTAINER_ACLS['private']})) + self.register_uris([ + dict(method='POST', uri=self.container_endpoint, + status_code=204, + validate=dict( + headers={ + 'x-container-read': + shade.openstackcloud.OBJECT_CONTAINER_ACLS[ + 'private']}))]) self.cloud.set_container_access(self.container, 'private') @@ -212,20 +209,19 @@ def test_set_container_access_invalid(self): self.cloud.set_container_access, self.container, 'invalid') def test_get_container_access(self): - self.register_uri( - 'HEAD', self.container_endpoint, - headers={ - 'x-container-read': - str(shade.openstackcloud.OBJECT_CONTAINER_ACLS['public'])}) - + self.register_uris([ + dict(method='HEAD', uri=self.container_endpoint, + headers={ + 'x-container-read': + str(shade.openstackcloud.OBJECT_CONTAINER_ACLS[ + 'public'])})]) access = self.cloud.get_container_access(self.container) self.assertEqual('public', access) def test_get_container_invalid(self): - self.register_uri( - 'HEAD', self.container_endpoint, - headers={ - 'x-container-read': 'invalid'}) + self.register_uris([ + dict(method='HEAD', uri=self.container_endpoint, + headers={'x-container-read': 'invalid'})]) with testtools.ExpectedException( exc.OpenStackCloudException, @@ -234,9 +230,9 @@ def test_get_container_invalid(self): self.cloud.get_container_access(self.container) def test_get_container_access_not_found(self): - self.register_uri( - 'HEAD', self.container_endpoint, - status_code=404) + self.register_uris([ + dict(method='HEAD', uri=self.container_endpoint, + status_code=404)]) with testtools.ExpectedException( exc.OpenStackCloudException, "Container not found: %s" % self.container @@ -249,7 +245,8 @@ def test_list_containers(self): containers = [ {u'count': 0, u'bytes': 0, u'name': self.container}] - self.register_uri('GET', endpoint, complete_qs=True, json=containers) + self.register_uris([dict(method='GET', uri=endpoint, complete_qs=True, + json=containers)]) ret = self.cloud.list_containers() @@ -259,7 +256,8 @@ def test_list_containers(self): def test_list_containers_exception(self): endpoint = '{endpoint}/?format=json'.format( endpoint=self.endpoint) - self.register_uri('GET', endpoint, complete_qs=True, status_code=416) + self.register_uris([dict(method='GET', uri=endpoint, complete_qs=True, + status_code=416)]) self.assertRaises( exc.OpenStackCloudException, self.cloud.list_containers) @@ -276,7 +274,8 @@ def test_list_objects(self): u'name': self.object, u'content_type': u'application/octet-stream'}] - self.register_uri('GET', endpoint, complete_qs=True, json=objects) + self.register_uris([dict(method='GET', uri=endpoint, complete_qs=True, + json=objects)]) ret = self.cloud.list_objects(self.container) @@ -286,24 +285,26 @@ def test_list_objects(self): def test_list_objects_exception(self): endpoint = '{endpoint}?format=json'.format( endpoint=self.container_endpoint) - self.register_uri('GET', endpoint, complete_qs=True, status_code=416) + self.register_uris([dict(method='GET', uri=endpoint, complete_qs=True, + status_code=416)]) self.assertRaises( exc.OpenStackCloudException, self.cloud.list_objects, self.container) self.assert_calls() def test_delete_object(self): - self.register_uri( - 'HEAD', self.object_endpoint, - headers={'X-Object-Meta': 'foo'}) - self.register_uri('DELETE', self.object_endpoint, status_code=204) + self.register_uris([ + dict(method='HEAD', uri=self.object_endpoint, + headers={'X-Object-Meta': 'foo'}), + dict(method='DELETE', uri=self.object_endpoint, status_code=204)]) self.assertTrue(self.cloud.delete_object(self.container, self.object)) self.assert_calls() def test_delete_object_not_found(self): - self.register_uri('HEAD', self.object_endpoint, status_code=404) + self.register_uris([dict(method='HEAD', uri=self.object_endpoint, + status_code=404)]) self.assertFalse(self.cloud.delete_object(self.container, self.object)) @@ -324,21 +325,21 @@ def test_get_object(self): } response_headers = {k.lower(): v for k, v in headers.items()} text = 'test body' - self.register_uri( - 'GET', self.object_endpoint, - headers={ - 'Content-Length': '20304400896', - 'Content-Type': 'application/octet-stream', - 'Accept-Ranges': 'bytes', - 'Last-Modified': 'Thu, 15 Dec 2016 13:34:14 GMT', - 'Etag': '"b5c454b44fbd5344793e3fb7e3850768"', - 'X-Timestamp': '1481808853.65009', - 'X-Trans-Id': 'tx68c2a2278f0c469bb6de1-005857ed80dfw1', - 'Date': 'Mon, 19 Dec 2016 14:24:00 GMT', - 'X-Static-Large-Object': 'True', - 'X-Object-Meta-Mtime': '1481513709.168512', - }, - text='test body') + self.register_uris([ + dict(method='GET', uri=self.object_endpoint, + headers={ + 'Content-Length': '20304400896', + 'Content-Type': 'application/octet-stream', + 'Accept-Ranges': 'bytes', + 'Last-Modified': 'Thu, 15 Dec 2016 13:34:14 GMT', + 'Etag': '"b5c454b44fbd5344793e3fb7e3850768"', + 'X-Timestamp': '1481808853.65009', + 'X-Trans-Id': 'tx68c2a2278f0c469bb6de1-005857ed80dfw1', + 'Date': 'Mon, 19 Dec 2016 14:24:00 GMT', + 'X-Static-Large-Object': 'True', + 'X-Object-Meta-Mtime': '1481513709.168512', + }, + text='test body')]) resp = self.cloud.get_object(self.container, self.object) @@ -347,14 +348,16 @@ def test_get_object(self): self.assertEqual((response_headers, text), resp) def test_get_object_not_found(self): - self.register_uri('GET', self.object_endpoint, status_code=404) + self.register_uris([dict(method='GET', + uri=self.object_endpoint, status_code=404)]) self.assertIsNone(self.cloud.get_object(self.container, self.object)) self.assert_calls() def test_get_object_exception(self): - self.register_uri('GET', self.object_endpoint, status_code=416) + self.register_uris([dict(method='GET', uri=self.object_endpoint, + status_code=416)]) self.assertRaises( shade.OpenStackCloudException, @@ -367,31 +370,29 @@ def test_get_object_segment_size_below_min(self): # Register directly becuase we make multiple calls. The number # of calls we make isn't interesting - what we do with the return # values is. Don't run assert_calls for the same reason. - self.adapter.register_uri( - 'GET', 'https://object-store.example.com/info', - json=dict( - swift={'max_file_size': 1000}, - slo={'min_segment_size': 500}), - headers={'Content-Type': 'application/json'}) + self.register_uris([ + dict(method='GET', uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': 1000}, + slo={'min_segment_size': 500}), + headers={'Content-Type': 'application/json'})]) self.assertEqual(500, self.cloud.get_object_segment_size(400)) self.assertEqual(900, self.cloud.get_object_segment_size(900)) self.assertEqual(1000, self.cloud.get_object_segment_size(1000)) self.assertEqual(1000, self.cloud.get_object_segment_size(1100)) def test_get_object_segment_size_http_404(self): - self.register_uri( - 'GET', 'https://object-store.example.com/info', - status_code=404, - reason='Not Found') + self.register_uris([ + dict(method='GET', uri='https://object-store.example.com/info', + status_code=404, reason='Not Found')]) self.assertEqual(shade.openstackcloud.DEFAULT_OBJECT_SEGMENT_SIZE, self.cloud.get_object_segment_size(None)) self.assert_calls() def test_get_object_segment_size_http_412(self): - self.register_uri( - 'GET', 'https://object-store.example.com/info', - status_code=412, - reason='Precondition failed') + self.register_uris([ + dict(method='GET', uri='https://object-store.example.com/info', + status_code=412, reason='Precondition failed')]) self.assertEqual(shade.openstackcloud.DEFAULT_OBJECT_SEGMENT_SIZE, self.cloud.get_object_segment_size(None)) self.assert_calls() @@ -412,59 +413,55 @@ def setUp(self): def test_create_object(self): - self.register_uri( - 'GET', 'https://object-store.example.com/info', - json=dict( - swift={'max_file_size': 1000}, - slo={'min_segment_size': 500})) - - self.register_uri( - 'HEAD', '{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container), - status_code=404) - - self.register_uri( - 'PUT', '{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container), - status_code=201, - headers={ - 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', - 'Content-Length': '0', - 'Content-Type': 'text/html; charset=UTF-8', - }) - self.register_uri( - 'HEAD', '{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container), - headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}) - - self.register_uri( - 'HEAD', '{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=404) - - self.register_uri( - 'PUT', '{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=201, - validate=dict( - headers={ - 'x-object-meta-x-shade-md5': self.md5, - 'x-object-meta-x-shade-sha256': self.sha256, - })) + self.register_uris([ + dict(method='GET', + uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': 1000}, + slo={'min_segment_size': 500})), + dict(method='HEAD', + uri='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container), + status_code=404), + dict(method='PUT', + uri='{endpoint}/{container}'.format( + endpoint=self.endpoint, container=self.container), + status_code=201, + headers={ + 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=UTF-8', + }), + dict(method='HEAD', + uri='{endpoint}/{container}'.format( + endpoint=self.endpoint, container=self.container), + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}), + dict(method='HEAD', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, container=self.container, + object=self.object), + status_code=404), + dict(method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=201, + validate=dict( + headers={ + 'x-object-meta-x-shade-md5': self.md5, + 'x-object-meta-x-shade-sha256': self.sha256, + })) + ]) self.cloud.create_object( container=self.container, name=self.object, @@ -477,74 +474,75 @@ def test_create_dynamic_large_object(self): max_file_size = 2 min_file_size = 1 - self.register_uri( - 'GET', 'https://object-store.example.com/info', - json=dict( - swift={'max_file_size': max_file_size}, - slo={'min_segment_size': min_file_size})) - - self.register_uri( - 'HEAD', '{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container), - status_code=404) - - self.register_uri( - 'PUT', '{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container,), - status_code=201, - headers={ - 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', - 'Content-Length': '0', - 'Content-Type': 'text/html; charset=UTF-8', - }) - - self.register_uri( - 'HEAD', '{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container), - headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}) - - self.register_uri( - 'HEAD', '{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=404) - - for index, offset in enumerate( - range(0, len(self.content), max_file_size)): - - self.register_uri( - 'PUT', '{endpoint}/{container}/{object}/{index:0>6}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - index=index), - status_code=201) - - self.register_uri( - 'PUT', '{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=201, - validate=dict( - headers={ - 'x-object-manifest': '{container}/{object}'.format( - container=self.container, object=self.object), - 'x-object-meta-x-shade-md5': self.md5, - 'x-object-meta-x-shade-sha256': self.sha256, - })) - + uris_to_mock = [ + dict(method='GET', + uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': max_file_size}, + slo={'min_segment_size': min_file_size})), + + dict(method='HEAD', + uri='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container), + status_code=404), + dict(method='PUT', + uri='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container, ), + status_code=201, + headers={ + 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=UTF-8', + }), + dict(method='HEAD', + uri='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container), + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}), + dict(method='HEAD', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, container=self.container, + object=self.object), + status_code=404) + ] + + uris_to_mock.extend( + [dict(method='PUT', + uri='{endpoint}/{container}/{object}/{index:0>6}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + index=index), + status_code=201) + for index, offset in enumerate( + range(0, len(self.content), max_file_size))] + ) + + uris_to_mock.append( + dict(method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=201, + validate=dict( + headers={ + 'x-object-manifest': '{container}/{object}'.format( + container=self.container, object=self.object), + 'x-object-meta-x-shade-md5': self.md5, + 'x-object-meta-x-shade-sha256': self.sha256, + }))) + self.register_uris(uris_to_mock) self.cloud.create_object( container=self.container, name=self.object, filename=self.object_file.name, use_slo=False) @@ -562,75 +560,75 @@ def test_create_static_large_object(self): max_file_size = 25 min_file_size = 1 - self.register_uri( - 'GET', 'https://object-store.example.com/info', - json=dict( - swift={'max_file_size': max_file_size}, - slo={'min_segment_size': min_file_size})) - - self.register_uri( - 'HEAD', '{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container), - status_code=404) - - self.register_uri( - 'PUT', '{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container,), - status_code=201, - headers={ - 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', - 'Content-Length': '0', - 'Content-Type': 'text/html; charset=UTF-8', - }) - - self.register_uri( - 'HEAD', '{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container), - headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}), - - self.register_uri( - 'HEAD', '{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=404) - - for index, offset in enumerate( - range(0, len(self.content), max_file_size)): - - self.register_uri( - 'PUT', '{endpoint}/{container}/{object}/{index:0>6}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - index=index), - status_code=201, - headers=dict(Etag='etag{index}'.format(index=index))) - - self.register_uri( - 'PUT', '{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=201, - validate=dict( - params={ - 'multipart-manifest', 'put' - }, - headers={ - 'x-object-meta-x-shade-md5': self.md5, - 'x-object-meta-x-shade-sha256': self.sha256, - })) + uris_to_mock = [ + dict(method='GET', uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': max_file_size}, + slo={'min_segment_size': min_file_size})), + dict(method='HEAD', + uri='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container), + status_code=404), + dict(method='PUT', + uri='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container, ), + status_code=201, + headers={ + 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=UTF-8', + }), + dict(method='HEAD', + uri='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container), + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}), + dict(method='HEAD', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=404) + ] + + uris_to_mock.extend([ + dict(method='PUT', + uri='{endpoint}/{container}/{object}/{index:0>6}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + index=index), + status_code=201, + headers=dict(Etag='etag{index}'.format(index=index))) + for index, offset in enumerate( + range(0, len(self.content), max_file_size)) + ]) + + uris_to_mock.append( + dict(method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=201, + validate=dict( + params={ + 'multipart-manifest', 'put' + }, + headers={ + 'x-object-meta-x-shade-md5': self.md5, + 'x-object-meta-x-shade-sha256': self.sha256, + }))) + self.register_uris(uris_to_mock) self.cloud.create_object( container=self.container, name=self.object, @@ -681,83 +679,75 @@ def test_object_segment_retry_failure(self): max_file_size = 25 min_file_size = 1 - self.register_uri( - 'GET', 'https://object-store.example.com/info', - json=dict( - swift={'max_file_size': max_file_size}, - slo={'min_segment_size': min_file_size})) - - self.register_uri( - 'HEAD', '{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container), - status_code=404) - - self.register_uri( - 'PUT', '{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container,), - status_code=201, - headers={ - 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', - 'Content-Length': '0', - 'Content-Type': 'text/html; charset=UTF-8', - }) - - self.register_uri( - 'HEAD', '{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container), - headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}) - - self.register_uri( - 'HEAD', '{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=404) - - self.register_uri( - 'PUT', '{endpoint}/{container}/{object}/000000'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object), - status_code=201) - - self.register_uri( - 'PUT', '{endpoint}/{container}/{object}/000001'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object), - status_code=201) - - self.register_uri( - 'PUT', '{endpoint}/{container}/{object}/000002'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object), - status_code=201) - - self.register_uri( - 'PUT', '{endpoint}/{container}/{object}/000003'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object), - status_code=501) - - self.register_uri( - 'PUT', '{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=201) + self.register_uris([ + dict(method='GET', uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': max_file_size}, + slo={'min_segment_size': min_file_size})), + dict(method='HEAD', + uri='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container), + status_code=404), + dict(method='PUT', + uri='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container, ), + status_code=201, + headers={ + 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=UTF-8', + }), + dict(method='HEAD', + uri='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container), + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}), + dict(method='HEAD', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=404), + dict(method='PUT', + uri='{endpoint}/{container}/{object}/000000'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object), + status_code=201), + dict(method='PUT', + uri='{endpoint}/{container}/{object}/000001'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object), + status_code=201), + dict(method='PUT', + uri='{endpoint}/{container}/{object}/000002'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object), + status_code=201), + dict(method='PUT', + uri='{endpoint}/{container}/{object}/000003'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object), + status_code=501), + dict(method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=201) + ]) self.assertRaises( exc.OpenStackCloudException, @@ -773,102 +763,93 @@ def test_object_segment_retries(self): max_file_size = 25 min_file_size = 1 - self.register_uri( - 'GET', 'https://object-store.example.com/info', - json=dict( - swift={'max_file_size': max_file_size}, - slo={'min_segment_size': min_file_size})) - - self.register_uri( - 'HEAD', '{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container), - status_code=404) - - self.register_uri( - 'PUT', '{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container,), - status_code=201, - headers={ - 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', - 'Content-Length': '0', - 'Content-Type': 'text/html; charset=UTF-8', - }) - - self.register_uri( - 'HEAD', '{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container), - headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}) - - self.register_uri( - 'HEAD', '{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=404) - - self.register_uri( - 'PUT', '{endpoint}/{container}/{object}/000000'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object), - headers={'etag': 'etag0'}, - status_code=201) - - self.register_uri( - 'PUT', '{endpoint}/{container}/{object}/000001'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object), - headers={'etag': 'etag1'}, - status_code=201) - - self.register_uri( - 'PUT', '{endpoint}/{container}/{object}/000002'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object), - headers={'etag': 'etag2'}, - status_code=201) - - self.register_uri( - 'PUT', '{endpoint}/{container}/{object}/000003'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object), - status_code=501) - - self.register_uri( - 'PUT', '{endpoint}/{container}/{object}/000003'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object), - status_code=201, - headers={'etag': 'etag3'}) - - self.register_uri( - 'PUT', '{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=201, - validate=dict( - params={ - 'multipart-manifest', 'put' - }, - headers={ - 'x-object-meta-x-shade-md5': self.md5, - 'x-object-meta-x-shade-sha256': self.sha256, - })) + self.register_uris([ + dict(method='GET', uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': max_file_size}, + slo={'min_segment_size': min_file_size})), + dict(method='HEAD', + uri='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container), + status_code=404), + dict(method='PUT', + uri='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container, ), + status_code=201, + headers={ + 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=UTF-8', + }), + dict(method='HEAD', + uri='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container), + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}), + dict(method='HEAD', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=404), + dict(method='PUT', + uri='{endpoint}/{container}/{object}/000000'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object), + headers={'etag': 'etag0'}, + status_code=201), + dict(method='PUT', + uri='{endpoint}/{container}/{object}/000001'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object), + headers={'etag': 'etag1'}, + status_code=201), + dict(method='PUT', + uri='{endpoint}/{container}/{object}/000002'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object), + headers={'etag': 'etag2'}, + status_code=201), + dict(method='PUT', + uri='{endpoint}/{container}/{object}/000003'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object), + status_code=501), + dict(method='PUT', + uri='{endpoint}/{container}/{object}/000003'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object), + status_code=201, + headers={'etag': 'etag3'}), + dict(method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=201, + validate=dict( + params={ + 'multipart-manifest', 'put' + }, + headers={ + 'x-object-meta-x-shade-md5': self.md5, + 'x-object-meta-x-shade-sha256': self.sha256, + })) + ]) self.cloud.create_object( container=self.container, name=self.object, From 78d1a2400d0cf923cfa737cff59d6c9951dbe314 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Fri, 17 Feb 2017 07:44:34 -0700 Subject: [PATCH 1292/3836] Fix function test for compute images The functional test for compute image metadata did not expect to get anything back for some reason. Partial-bug: #1665495 Change-Id: If2d0e0a402bf4fc530187ec07dfd3f69057104bf --- openstack/tests/functional/compute/v2/test_image.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openstack/tests/functional/compute/v2/test_image.py b/openstack/tests/functional/compute/v2/test_image.py index 09069d697..d71f30d79 100644 --- a/openstack/tests/functional/compute/v2/test_image.py +++ b/openstack/tests/functional/compute/v2/test_image.py @@ -73,13 +73,14 @@ def test_image_metadata(self): # set empty metadata self.conn.compute.set_image_metadata(image, k0='') image = self.conn.compute.get_image_metadata(image) - self.assertFalse(image.metadata) + self.assertIn('k0', image.metadata) + self.assertEqual('', image.metadata['k0']) # set metadata self.conn.compute.set_image_metadata(image, k1='v1') image = self.conn.compute.get_image_metadata(image) self.assertTrue(image.metadata) - self.assertEqual(1, len(image.metadata)) + self.assertEqual(2, len(image.metadata)) self.assertIn('k1', image.metadata) self.assertEqual('v1', image.metadata['k1']) @@ -87,7 +88,7 @@ def test_image_metadata(self): self.conn.compute.set_image_metadata(image, k2='v2') image = self.conn.compute.get_image_metadata(image) self.assertTrue(image.metadata) - self.assertEqual(2, len(image.metadata)) + self.assertEqual(3, len(image.metadata)) self.assertIn('k1', image.metadata) self.assertEqual('v1', image.metadata['k1']) self.assertIn('k2', image.metadata) @@ -97,7 +98,7 @@ def test_image_metadata(self): self.conn.compute.set_image_metadata(image, k1='v1.1') image = self.conn.compute.get_image_metadata(image) self.assertTrue(image.metadata) - self.assertEqual(2, len(image.metadata)) + self.assertEqual(3, len(image.metadata)) self.assertIn('k1', image.metadata) self.assertEqual('v1.1', image.metadata['k1']) self.assertIn('k2', image.metadata) From 6e7840842a9fd314554ae9353ded1869c19787ad Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Thu, 16 Feb 2017 15:58:21 -0500 Subject: [PATCH 1293/3836] Implement metric docs This change introduces metric docs. There is only one method in the proxy as of right now, so that was easy. Change-Id: Ica327f27b26e5dd4b6744566eb09ea81a6e9583e --- doc/source/users/index.rst | 1 + doc/source/users/proxies/metric.rst | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 doc/source/users/proxies/metric.rst diff --git a/doc/source/users/index.rst b/doc/source/users/index.rst index fbac4aafe..811eff398 100644 --- a/doc/source/users/index.rst +++ b/doc/source/users/index.rst @@ -80,6 +80,7 @@ but listed below are the ones provided by this SDK by default. Image v2 Key Manager Network + Metric Object Store Orchestration Telemetry diff --git a/doc/source/users/proxies/metric.rst b/doc/source/users/proxies/metric.rst new file mode 100644 index 000000000..3abd98f9d --- /dev/null +++ b/doc/source/users/proxies/metric.rst @@ -0,0 +1,18 @@ +Metric API +========== + +.. automodule:: openstack.metric.v1._proxy + +The Metric Class +---------------- + +The metric high-level interface is available through the ``metric`` +member of a :class:`~openstack.connection.Connection` object. The +``metric`` member will only be added if the service is detected. + +Capability Operations +^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.metric.v1._proxy.Proxy + + .. automethod:: openstack.metric.v1._proxy.Proxy.capabilities From 52bd73e45dfe9436daef3237a2f4fa969344cff4 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Fri, 17 Feb 2017 21:55:50 -0700 Subject: [PATCH 1294/3836] Update the image used for functional tests The image used for functional tests is out of date. This simeple changes gets more tests to pass. Change-Id: I60967b6da183ae88270c541abdb4d93913f45558 Partial-bug: #1665495 --- examples/connect.py | 2 +- openstack/tests/functional/base.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/connect.py b/examples/connect.py index e1ea259dd..1cf195ff4 100644 --- a/examples/connect.py +++ b/examples/connect.py @@ -53,7 +53,7 @@ def _get_resource_value(resource_key, default): cloud = occ.get_one_cloud(TEST_CLOUD) SERVER_NAME = 'openstacksdk-example' -IMAGE_NAME = _get_resource_value('image_name', 'cirros-0.3.4-x86_64-uec') +IMAGE_NAME = _get_resource_value('image_name', 'cirros-0.3.5-x86_64-disk') FLAVOR_NAME = _get_resource_value('flavor_name', 'm1.small') NETWORK_NAME = _get_resource_value('network_name', 'private') KEYPAIR_NAME = _get_resource_value('keypair_name', 'openstacksdk-example') diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index 3d37081b0..57544a918 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -42,7 +42,7 @@ def _get_resource_value(resource_key, default): occ = os_client_config.OpenStackConfig() cloud = occ.get_one_cloud(opts.cloud, argparse=opts) -IMAGE_NAME = _get_resource_value('image_name', 'cirros-0.3.4-x86_64-uec') +IMAGE_NAME = _get_resource_value('image_name', 'cirros-0.3.5-x86_64-disk') FLAVOR_NAME = _get_resource_value('flavor_name', 'm1.small') From 93f21be04ab9462481e5ebd4f42d97a1a2630a69 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Sat, 18 Feb 2017 09:45:05 -0700 Subject: [PATCH 1295/3836] Fix the nextwork agent add remove test There are some obvious errors in the network agent add and remove test. Partial-bug: #1665495 Change-Id: Iaea6683aec7c036b6561c33d2bce03055d74d07d --- .../network/v2/test_agent_add_remove_network.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openstack/tests/functional/network/v2/test_agent_add_remove_network.py b/openstack/tests/functional/network/v2/test_agent_add_remove_network.py index 4aed6e6dd..cae3dddb6 100644 --- a/openstack/tests/functional/network/v2/test_agent_add_remove_network.py +++ b/openstack/tests/functional/network/v2/test_agent_add_remove_network.py @@ -18,7 +18,7 @@ class TestAgentNetworks(base.BaseFunctionalTest): - NETWORK_NAME = 'network-name'.join(uuid.uuid4().hex) + NETWORK_NAME = 'network-' + uuid.uuid4().hex NETWORK_ID = None AGENT = None AGENT_ID = None @@ -38,9 +38,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): - net = cls.conn.network.delete_router(cls.NETWORK_ID, - ignore_missing=False) - cls.assertIs(None, net) + cls.conn.network.delete_network(cls.NETWORK_ID) def test_add_agent_to_network(self): net = self.AGENT.add_agent_to_network(self.conn.session, @@ -55,9 +53,9 @@ def test_remove_agent_from_network(self): def _verify_add(self, network): net = self.conn.network.dhcp_agent_hosting_networks(self.AGENT_ID) net_ids = [n.id for n in net] - self.asserIn(self.NETWORK_ID, net_ids) + self.assertIn(self.NETWORK_ID, net_ids) - def _verify_network(self, network): + def _verify_remove(self, network): net = self.conn.network.dhcp_agent_hosting_networks(self.AGENT_ID) net_ids = [n.id for n in net] self.assertNotIn(self.NETWORK_ID, net_ids) From e5a07834f8c6d7e15bb63d711d58b345663b33cc Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Sat, 18 Feb 2017 11:09:53 -0700 Subject: [PATCH 1296/3836] Fix the agent add remove test These tests should use the proxy first off and there must of been some api change in the past that broke the test. Partial-bug: #1665495 Change-Id: Icef5cb0ce2816a10e990945af0baf60e215b1d28 --- .../v2/test_agent_add_remove_router.py | 35 ++++++------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/openstack/tests/functional/network/v2/test_agent_add_remove_router.py b/openstack/tests/functional/network/v2/test_agent_add_remove_router.py index 4f4f4c669..ee3b43467 100644 --- a/openstack/tests/functional/network/v2/test_agent_add_remove_router.py +++ b/openstack/tests/functional/network/v2/test_agent_add_remove_router.py @@ -19,45 +19,32 @@ class TestAgentRouters(base.BaseFunctionalTest): ROUTER_NAME = 'router-name-' + uuid.uuid4().hex - ROUTER_ID = None + ROUTER = None AGENT = None - AGENT_ID = None @classmethod def setUpClass(cls): super(TestAgentRouters, cls).setUpClass() - rot = cls.conn.network.create_router(name=cls.ROUTER_NAME) - assert isinstance(rot, router.Router) - cls.ROUTER_ID = rot.id + cls.ROUTER = cls.conn.network.create_router(name=cls.ROUTER_NAME) + assert isinstance(cls.ROUTER, router.Router) agent_list = list(cls.conn.network.agents()) agents = [agent for agent in agent_list if agent.agent_type == 'L3 agent'] cls.AGENT = agents[0] - cls.AGENT_ID = cls.AGENT.id @classmethod def tearDownClass(cls): - rot = cls.conn.network.delete_router(cls.ROUTER_ID, - ignore_missing=False) - cls.assertIs(None, rot) + cls.conn.network.delete_router(cls.ROUTER) def test_add_router_to_agent(self): - sot = self.AGENT.add_router_to_agent(self.conn.session, - router_id=self.ROUTER_ID) - self._verify_add(sot) - - def test_remove_router_from_agent(self): - sot = self.AGENT.remove_router_from_agent(self.conn.session, - router_id=self.ROUTER_ID) - self._verify_remove(sot) - - def _verify_add(self, sot): - rots = self.conn.network.agent_hosted_routers(self.AGENT_ID) + self.conn.network.add_router_to_agent(self.AGENT, self.ROUTER) + rots = self.conn.network.agent_hosted_routers(self.AGENT) routers = [router.id for router in rots] - self.assertIn(self.ROUTER_ID, routers) + self.assertIn(self.ROUTER.id, routers) - def _verify_remove(self, sot): - rots = self.conn.network.agent_hosted_routers(self.AGENT_ID) + def test_remove_router_from_agent(self): + self.conn.network.remove_router_from_agent(self.AGENT, self.ROUTER) + rots = self.conn.network.agent_hosted_routers(self.AGENT) routers = [router.id for router in rots] - self.assertNotIn(self.ROUTER_ID, routers) + self.assertNotIn(self.ROUTER.id, routers) From 7bfae5d76be175a743631dea74613813ceba4df9 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Sat, 18 Feb 2017 14:45:30 -0700 Subject: [PATCH 1297/3836] Fix the service profile meta info test At some point metainfo was changed to meta_info and the functional test was not updated. Partial-bug: #1665495 Change-Id: I4b6d7471f2d5cf4296c9c1244cef94e17f176799 --- .../tests/functional/network/v2/test_service_profile.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openstack/tests/functional/network/v2/test_service_profile.py b/openstack/tests/functional/network/v2/test_service_profile.py index 34fe31ac0..9b9fd73f8 100644 --- a/openstack/tests/functional/network/v2/test_service_profile.py +++ b/openstack/tests/functional/network/v2/test_service_profile.py @@ -30,7 +30,7 @@ def setUpClass(cls): assert isinstance(service_profiles, _service_profile.ServiceProfile) cls.assertIs(cls.SERVICE_PROFILE_DESCRIPTION, service_profiles.description) - cls.assertIs(cls.METAINFO, service_profiles.metainfo) + cls.assertIs(cls.METAINFO, service_profiles.meta_info) cls.ID = service_profiles.id @@ -45,11 +45,11 @@ def test_find(self): service_profiles = self.conn.network.find_service_profile( self.ID) self.assertEqual(self.METAINFO, - service_profiles.metainfo) + service_profiles.meta_info) def test_get(self): service_profiles = self.conn.network.get_service_profile(self.ID) - self.assertEqual(self.METAINFO, service_profiles.metainfo) + self.assertEqual(self.METAINFO, service_profiles.meta_info) self.assertEqual(self.SERVICE_PROFILE_DESCRIPTION, service_profiles.description) @@ -60,5 +60,5 @@ def test_update(self): self.assertEqual(self.UPDATE_DESCRIPTION, service_profiles.description) def test_list(self): - metainfos = [f.metainfo for f in self.conn.network.service_profiles()] + metainfos = [f.meta_info for f in self.conn.network.service_profiles()] self.assertIn(self.METAINFO, metainfos) From f5e77c6be9b3c33b8031c310526bbebbfbd7aee8 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Sat, 18 Feb 2017 17:10:22 -0700 Subject: [PATCH 1298/3836] Fix the network quota tests These network quota tests were badly broken. I simplified them up a lot so they will require less maintenance. Change-Id: I9738b9283de2e731510f6ed5ff208c075ea48940 Partial-bug: #1665495 --- create_yaml.sh | 1 + .../tests/functional/network/v2/test_quota.py | 39 +++++++++++-------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/create_yaml.sh b/create_yaml.sh index 3da207841..4443986ee 100755 --- a/create_yaml.sh +++ b/create_yaml.sh @@ -5,6 +5,7 @@ # mkdir -p ~/.config/openstack/ FILE=~/.config/openstack/clouds.yaml +export OS_IDENTITY_API_VERSION=3 # force v3 identity echo 'clouds:' >$FILE echo ' test_cloud:' >>$FILE env | grep OS_ | tr '=' ' ' | while read k v diff --git a/openstack/tests/functional/network/v2/test_quota.py b/openstack/tests/functional/network/v2/test_quota.py index 17f1fbb29..b02f18f4b 100644 --- a/openstack/tests/functional/network/v2/test_quota.py +++ b/openstack/tests/functional/network/v2/test_quota.py @@ -10,27 +10,34 @@ # License for the specific language governing permissions and limitations # under the License. +import uuid + from openstack.tests.functional import base class TestQuota(base.BaseFunctionalTest): + PROJECT_NAME = 'project-' + uuid.uuid4().hex + PROJECT = None + + @classmethod + def setUpClass(cls): + super(TestQuota, cls).setUpClass() + # Need a project to have a quota + cls.PROJECT = cls.conn.identity.create_project(name=cls.PROJECT_NAME) + + @classmethod + def tearDownClass(cls): + cls.conn.identity.delete_project(cls.PROJECT.id) + def test_list(self): - sot = self.conn.network.quotas() - for qot in sot: - self.assertIn('subnet', qot) - self.assertIn('network', qot) - self.assertIn('router', qot) - self.assertIn('port', qot) - self.assertIn('floatingip', qot) - self.assertIn('security_group_rule', qot) - self.assertIn('security_group', qot) - self.assertIn('subnetpool', qot) - self.assertIn('rbac_policy', qot) + qot = self.conn.network.quotas().next() + self.assertIsNotNone(qot.project_id) + self.assertIsNotNone(qot.networks) def test_set(self): - attrs = {'network': 123456789} - self.conn.network.update_quota(**attrs) - quota_list = self.conn.network.get_quota() - for quota in quota_list: - self.assertIn('123456789', quota) + attrs = {'networks': 123456789} + project_quota = self.conn.network.quotas().next() + self.conn.network.update_quota(project_quota, **attrs) + new_quota = self.conn.network.get_quota(project_quota.project_id) + self.assertEqual(123456789, new_quota.networks) From eadf481f352e4277001f3b9e83c7ffbbd58c789c Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Sun, 19 Feb 2017 09:58:30 -0700 Subject: [PATCH 1299/3836] Fix the network service provider test This test was pretty lame before since it just verified that the result was a string. Now it verifies that at least one service provider exists and I think I picked one that should be aroudn for a while. The test failure message also prints the list of providers now so it should be easier to debug. Partial-bug: #1665495 Change-Id: Ief96558770d81dca81091e235b8d370883b14b94 --- .../functional/network/v2/test_service_provider.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openstack/tests/functional/network/v2/test_service_provider.py b/openstack/tests/functional/network/v2/test_service_provider.py index 5db2d2bf1..fca659c05 100644 --- a/openstack/tests/functional/network/v2/test_service_provider.py +++ b/openstack/tests/functional/network/v2/test_service_provider.py @@ -10,15 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -import six - from openstack.tests.functional import base class TestServiceProvider(base.BaseFunctionalTest): def test_list(self): providers = list(self.conn.network.service_providers()) - - for provide in providers: - self.assertIsInstance(provide.name, six.string_type) - self.assertIsInstance(provide.service_type, six.string_types) + names = [o.name for o in providers] + service_types = [o.service_type for o in providers] + self.assertIn('ha', names) + self.assertIn('L3_ROUTER_NAT', service_types) From 04430af53366795c47c52920864334d4220f4214 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Sun, 19 Feb 2017 10:12:08 -0700 Subject: [PATCH 1300/3836] Fix the network floating ip test for get Get test was expecting dictionary like resource. Partial-bug: #1665495 Change-Id: Ia681498916e19b959183bb591f95c71263c94e3b --- .../functional/network/v2/test_floating_ip.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/openstack/tests/functional/network/v2/test_floating_ip.py b/openstack/tests/functional/network/v2/test_floating_ip.py index de6b6ed57..83f48225e 100644 --- a/openstack/tests/functional/network/v2/test_floating_ip.py +++ b/openstack/tests/functional/network/v2/test_floating_ip.py @@ -36,7 +36,7 @@ class TestFloatingIP(base.BaseFunctionalTest): INT_SUB_ID = None ROT_ID = None PORT_ID = None - FIP_ID = None + FIP = None @classmethod def setUpClass(cls): @@ -71,11 +71,11 @@ def setUpClass(cls): # Create Floating IP. fip = cls.conn.network.create_ip(floating_network_id=cls.EXT_NET_ID) assert isinstance(fip, floating_ip.FloatingIP) - cls.FIP_ID = fip.id + cls.FIP = fip @classmethod def tearDownClass(cls): - sot = cls.conn.network.delete_ip(cls.FIP_ID, ignore_missing=False) + sot = cls.conn.network.delete_ip(cls.FIP.id, ignore_missing=False) cls.assertIs(None, sot) sot = cls.conn.network.delete_port(cls.PORT_ID, ignore_missing=False) cls.assertIs(None, sot) @@ -119,8 +119,8 @@ def _create_subnet(cls, name, net_id, cidr): return sub def test_find(self): - sot = self.conn.network.find_ip(self.FIP_ID) - self.assertEqual(self.FIP_ID, sot.id) + sot = self.conn.network.find_ip(self.FIP.id) + self.assertEqual(self.FIP.id, sot.id) def test_find_available_ip(self): sot = self.conn.network.find_available_ip() @@ -128,19 +128,19 @@ def test_find_available_ip(self): self.assertIsNone(sot.port_id) def test_get(self): - sot = self.conn.network.get_ip(self.FIP_ID) + sot = self.conn.network.get_ip(self.FIP.id) self.assertEqual(self.EXT_NET_ID, sot.floating_network_id) - self.assertEqual(self.FIP_ID, sot.id) - self.assertIn('floating_ip_address', sot) - self.assertIn('fixed_ip_address', sot) - self.assertIn('port_id', sot) - self.assertIn('router_id', sot) + self.assertEqual(self.FIP.id, sot.id) + self.assertEqual(self.FIP.floating_ip_address, sot.floating_ip_address) + self.assertEqual(self.FIP.fixed_ip_address, sot.fixed_ip_address) + self.assertEqual(self.FIP.port_id, sot.port_id) + self.assertEqual(self.FIP.router_id, sot.router_id) def test_list(self): ids = [o.id for o in self.conn.network.ips()] - self.assertIn(self.FIP_ID, ids) + self.assertIn(self.FIP.id, ids) def test_update(self): - sot = self.conn.network.update_ip(self.FIP_ID, port_id=self.PORT_ID) + sot = self.conn.network.update_ip(self.FIP.id, port_id=self.PORT_ID) self.assertEqual(self.PORT_ID, sot.port_id) - self.assertEqual(self.FIP_ID, sot.id) + self.assertEqual(self.FIP.id, sot.id) From e7f7b400bbdf5facaab6756344ae18ce07f756d8 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Mon, 20 Feb 2017 14:39:16 -0700 Subject: [PATCH 1301/3836] Remove unnecessary get_id call in _prepare_request I'm trying to take some baby steps here to clean up _prepare_request and the call to get_id doesn't make any sense here. The get_id method is designed to be used if you don't know if you are getting a string id or a resource and if we are in _prepare_request, we know we must have a resource. Change-Id: Iaaf9b5c91015fa9e5a97c63a7818afc8561aa70d --- openstack/resource2.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openstack/resource2.py b/openstack/resource2.py index 057fc27af..eb095b0b4 100644 --- a/openstack/resource2.py +++ b/openstack/resource2.py @@ -521,12 +521,11 @@ def _prepare_request(self, requires_id=True, prepend_key=False): uri = self.base_path % self._uri.attributes if requires_id: - id = self._get_id(self) - if id is None: + if self.id is None: raise exceptions.InvalidRequest( "Request requires an ID but none was found") - uri = utils.urljoin(uri, id) + uri = utils.urljoin(uri, self.id) return _Request(uri, body, headers) From e50853dc4b05410748bfe8535c72d2d7935d6ac7 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Tue, 21 Feb 2017 10:33:16 -0700 Subject: [PATCH 1302/3836] Remove old telemetry capability The events:query:simple capability is no longer a diffult capability on the test gate. Partial-bug: #1665495 Change-Id: Ic9b7f7a9081dcf9a064028e79ec5b3cb5bb23880 --- openstack/tests/functional/telemetry/v2/test_capability.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openstack/tests/functional/telemetry/v2/test_capability.py b/openstack/tests/functional/telemetry/v2/test_capability.py index 682ded41f..8db9d4e60 100644 --- a/openstack/tests/functional/telemetry/v2/test_capability.py +++ b/openstack/tests/functional/telemetry/v2/test_capability.py @@ -22,7 +22,6 @@ class TestCapability(base.BaseFunctionalTest): def test_list(self): ids = [o.id for o in self.conn.telemetry.capabilities()] self.assertIn('resources:query:simple', ids) - self.assertIn('events:query:simple', ids) self.assertIn('meters:query:simple', ids) self.assertIn('statistics:query:simple', ids) self.assertIn('samples:query:simple', ids) From e5068ec46aa7bb6b1738b8a64c29e44307f4ba98 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Tue, 21 Feb 2017 10:16:00 -0700 Subject: [PATCH 1303/3836] Fix the network auto allocate validate The network auto allocate was failing because it did not have an id because the id is a URI attribute. There was also a name conflict with the base class. Partial-bug: #1665495 Change-Id: I3dfdd3178978676c974af2c67f9297e435c7b46b --- openstack/network/v2/_proxy.py | 2 +- openstack/network/v2/auto_allocated_topology.py | 2 +- openstack/tests/unit/network/v2/test_proxy.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index a06db9db3..769f47871 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -306,7 +306,7 @@ def validate_auto_allocated_topology(self, project=None): if project is None: project = self._session.get_project_id() return self._get(_auto_allocated_topology.ValidateTopology, - project=project) + project=project, requires_id=False) def availability_zones(self, **query): """Return a generator of availability zones diff --git a/openstack/network/v2/auto_allocated_topology.py b/openstack/network/v2/auto_allocated_topology.py index a5cddac4b..84cf0bfa3 100644 --- a/openstack/network/v2/auto_allocated_topology.py +++ b/openstack/network/v2/auto_allocated_topology.py @@ -46,4 +46,4 @@ class ValidateTopology(AutoAllocatedTopology): #: Will return "Deployment error:" if the resources required have not #: been correctly set up. dry_run = resource.Body('dry_run') - project_id = resource.URI('project') + project = resource.URI('project') diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 3d4121676..c375dddeb 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -980,4 +980,5 @@ def test_validate_topology(self): value=[mock.sentinel.project_id], expected_args=[ auto_allocated_topology.ValidateTopology], - expected_kwargs={"project": mock.sentinel.project_id}) + expected_kwargs={"project": mock.sentinel.project_id, + "requires_id": False}) From c042de0c4732e2bee3fe3028600c1f42f86c0d4e Mon Sep 17 00:00:00 2001 From: chohoor Date: Wed, 22 Feb 2017 15:10:48 +0800 Subject: [PATCH 1304/3836] Support profile-only to cluster update This path add a parameter to cluster. Then openstacksdk could support parameter profile_only when cluster update. partial-blueprint: add-profile-only-to-cluster-update Change-Id: Ib8ed4c9bcd4c9bc84edd8b1a2a547a0c27ab1202 --- openstack/cluster/v1/cluster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/cluster/v1/cluster.py b/openstack/cluster/v1/cluster.py index 63ec4f55f..9e82c29b9 100644 --- a/openstack/cluster/v1/cluster.py +++ b/openstack/cluster/v1/cluster.py @@ -30,7 +30,7 @@ class Cluster(resource.Resource): patch_update = True _query_mapping = resource.QueryParameters( - 'name', 'status', 'sort', 'global_project') + 'name', 'status', 'sort', 'global_project', 'profile_only') # Properties #: The name of the cluster. From d56535d4d349cd85965c6035904c5b059ca0685c Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Tue, 21 Feb 2017 09:20:54 -0700 Subject: [PATCH 1305/3836] Fix problem with update including id The update includes the id which is obviously not a field that can be updated. I would imagine most services would ignore that, but it probably isn't good policy. Partial-bug: #1665495 Change-Id: I6c84b549dae53f91d1662f6e41ea632477c7652c --- openstack/resource2.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openstack/resource2.py b/openstack/resource2.py index eb095b0b4..7d8b0403c 100644 --- a/openstack/resource2.py +++ b/openstack/resource2.py @@ -644,6 +644,9 @@ def update(self, session, prepend_key=True, has_body=True): :raises: :exc:`~openstack.exceptions.MethodNotSupported` if :data:`Resource.allow_update` is not set to ``True``. """ + # The id cannot be dirty for an update + self._body._dirty.discard("id") + # Only try to update if we actually have anything to update. if not any([self._body.dirty, self._header.dirty]): return self From f4e4d1496f22440a9fb8d2b2e462566ac0b8b4c2 Mon Sep 17 00:00:00 2001 From: Shashank Kumar Shankar Date: Mon, 28 Nov 2016 21:02:02 +0000 Subject: [PATCH 1306/3836] Add network flavor associate, disassociate to SDK This patch introduces network flavor associate and disassociate to the OpenStack SDK. This is used in the implementation of the following neutron equivalent commands in OSC: - flavor-associate - flavor-disassociate Partially-Implements: blueprint neutron-client-flavors Change-Id: I0f1b331b0814b60bd97d518203213d47b3b41351 --- openstack/network/v2/_proxy.py | 37 +++++++++++++++++++ openstack/network/v2/flavor.py | 16 ++++++++ .../functional/network/v2/test_flavor.py | 23 ++++++++++++ .../tests/unit/network/v2/test_flavor.py | 35 ++++++++++++++++++ 4 files changed, 111 insertions(+) diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 769f47871..74721b08a 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -436,6 +436,43 @@ def flavors(self, **query): """ return self._list(_flavor.Flavor, paginated=True, **query) + def associate_flavor_with_service_profile(self, flavor, service_profile): + """Associate network flavor with service profile. + + :param flavor: + Either the id of a flavor or a + :class:`~openstack.network.v2.flavor.Flavor` instance. + :param service_profile: + The value can be either the ID of a service profile or a + :class:`~openstack.network.v2.service_profile.ServiceProfile` + instance. + :return: + """ + flavor = self._get_resource(_flavor.Flavor, flavor) + service_profile = self._get_resource( + _service_profile.ServiceProfile, service_profile) + return flavor.associate_flavor_with_service_profile( + self._session, service_profile.id) + + def disassociate_flavor_from_service_profile( + self, flavor, service_profile): + """Disassociate network flavor from service profile. + + :param flavor: + Either the id of a flavor or a + :class:`~openstack.network.v2.flavor.Flavor` instance. + :param service_profile: + The value can be either the ID of a service profile or a + :class:`~openstack.network.v2.service_profile.ServiceProfile` + instance. + :return: + """ + flavor = self._get_resource(_flavor.Flavor, flavor) + service_profile = self._get_resource( + _service_profile.ServiceProfile, service_profile) + return flavor.disassociate_flavor_from_service_profile( + self._session, service_profile.id) + def create_ip(self, **attrs): """Create a new floating ip from attributes diff --git a/openstack/network/v2/flavor.py b/openstack/network/v2/flavor.py index 32321378e..4ebcc8d25 100644 --- a/openstack/network/v2/flavor.py +++ b/openstack/network/v2/flavor.py @@ -12,6 +12,7 @@ from openstack.network import network_service from openstack import resource2 as resource +from openstack import utils class Flavor(resource.Resource): @@ -41,3 +42,18 @@ class Flavor(resource.Resource): service_type = resource.Body('service_type') #: IDs of service profiles associated with this flavor service_profile_ids = resource.Body('service_profiles', type=list) + + def associate_flavor_with_service_profile( + self, session, service_profile_id=None): + flavor_id = self.id + url = utils.urljoin(self.base_path, flavor_id, 'service_profiles') + body = {"service_profile": {"id": service_profile_id}} + resp = session.post(url, endpoint_filter=self.service, json=body) + return resp.json() + + def disassociate_flavor_from_service_profile( + self, session, service_profile_id=None): + flavor_id = self.id + url = utils.urljoin( + self.base_path, flavor_id, 'service_profiles', service_profile_id) + session.delete(url, endpoint_filter=self.service) diff --git a/openstack/tests/functional/network/v2/test_flavor.py b/openstack/tests/functional/network/v2/test_flavor.py index 2d04bd05c..5cef564ea 100644 --- a/openstack/tests/functional/network/v2/test_flavor.py +++ b/openstack/tests/functional/network/v2/test_flavor.py @@ -23,6 +23,9 @@ class TestFlavor(base.BaseFunctionalTest): SERVICE_TYPE = "FLAVORS" ID = None + SERVICE_PROFILE_DESCRIPTION = "DESCRIPTION" + METAINFO = "FlAVOR_PROFILE_METAINFO" + @classmethod def setUpClass(cls): super(TestFlavor, cls).setUpClass() @@ -34,11 +37,19 @@ def setUpClass(cls): cls.ID = flavors.id + cls.service_profiles = cls.conn.network.create_service_profile( + description=cls.SERVICE_PROFILE_DESCRIPTION, + metainfo=cls.METAINFO,) + @classmethod def tearDownClass(cls): flavors = cls.conn.network.delete_flavor(cls.ID, ignore_missing=True) cls.assertIs(None, flavors) + service_profiles = cls.conn.network.delete_service_profile( + cls.ID, ignore_missing=True) + cls.assertIs(None, service_profiles) + def test_find(self): flavors = self.conn.network.find_flavor(self.FLAVOR_NAME) self.assertEqual(self.ID, flavors.id) @@ -56,3 +67,15 @@ def test_update(self): flavor = self.conn.network.update_flavor(self.ID, name=self.UPDATE_NAME) self.assertEqual(self.UPDATE_NAME, flavor.name) + + def test_associate_flavor_with_service_profile(self): + response = \ + self.conn.network.associate_flavor_with_service_profile( + self.ID, self.service_profiles.id) + self.assertIsNotNone(response) + + def test_disassociate_flavor_from_service_profile(self): + response = \ + self.conn.network.disassociate_flavor_from_service_profile( + self.ID, self.service_profiles.id) + self.assertIsNotNone(response) diff --git a/openstack/tests/unit/network/v2/test_flavor.py b/openstack/tests/unit/network/v2/test_flavor.py index ffce61694..9bf70d5a9 100644 --- a/openstack/tests/unit/network/v2/test_flavor.py +++ b/openstack/tests/unit/network/v2/test_flavor.py @@ -10,12 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. +import mock import testtools from openstack.network.v2 import flavor IDENTIFIER = 'IDENTIFIER' EXAMPLE_WITH_OPTIONAL = { + 'id': IDENTIFIER, 'name': 'test-flavor', 'service_type': 'VPN', 'description': 'VPN flavor', @@ -24,6 +26,7 @@ } EXAMPLE = { + 'id': IDENTIFIER, 'name': 'test-flavor', 'service_type': 'VPN', } @@ -57,3 +60,35 @@ def test_make_it_with_optional(self): self.assertEqual(EXAMPLE_WITH_OPTIONAL['enabled'], flavors.is_enabled) self.assertEqual(EXAMPLE_WITH_OPTIONAL['service_profiles'], flavors.service_profile_ids) + + def test_associate_flavor_with_service_profile(self): + flav = flavor.Flavor(EXAMPLE) + response = mock.Mock() + response.body = { + 'service_profile': {'id': '1'}, + } + response.json = mock.Mock(return_value=response.body) + sess = mock.Mock() + sess.post = mock.Mock(return_value=response) + flav.id = 'IDENTIFIER' + self.assertEqual( + response.body, flav.associate_flavor_with_service_profile( + sess, '1')) + + url = 'flavors/IDENTIFIER/service_profiles' + sess.post.assert_called_with(url, endpoint_filter=flav.service, + json=response.body) + + def test_disassociate_flavor_from_service_profile(self): + flav = flavor.Flavor(EXAMPLE) + response = mock.Mock() + response.json = mock.Mock(return_value=response.body) + sess = mock.Mock() + sess.post = mock.Mock(return_value=response) + flav.id = 'IDENTIFIER' + self.assertEqual( + None, flav.disassociate_flavor_from_service_profile( + sess, '1')) + + url = 'flavors/IDENTIFIER/service_profiles/1' + sess.delete.assert_called_with(url, endpoint_filter=flav.service) From 96dc965381c4ba7915c66d6939ad4140cebfa06c Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Wed, 22 Feb 2017 11:22:59 -0700 Subject: [PATCH 1307/3836] Remove unsupported telemetry create_sample method Telemetry create_sample support was removed a while back, but it is still availble in the proxy. It does not appear to be supported in the current API documentation, so remove it. Fix the functional test while I'm at it. Partial-bug: #1665495 Change-Id: I49f7bd59335603284e7ed64ce83c0152f6c5ce6f --- openstack/telemetry/v2/_proxy.py | 12 ---------- .../functional/telemetry/v2/test_sample.py | 24 +++---------------- .../tests/unit/telemetry/v2/test_proxy.py | 3 --- 3 files changed, 3 insertions(+), 36 deletions(-) diff --git a/openstack/telemetry/v2/_proxy.py b/openstack/telemetry/v2/_proxy.py index b50d37f8f..054becca3 100644 --- a/openstack/telemetry/v2/_proxy.py +++ b/openstack/telemetry/v2/_proxy.py @@ -111,18 +111,6 @@ def resources(self, **query): """ return self._list(_resource.Resource, paginated=False, **query) - def create_sample(self, **attrs): - """Create a new sample from attributes - - :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.telemetry.v2.sample.Sample`, - comprised of the properties on the Sample class. - - :returns: The results of sample creation - :rtype: :class:`~openstack.telemetry.v2.sample.Sample` - """ - return self._create(sample.Sample, **attrs) - def find_sample(self, name_or_id, ignore_missing=True): """Find a single sample diff --git a/openstack/tests/functional/telemetry/v2/test_sample.py b/openstack/tests/functional/telemetry/v2/test_sample.py index 3aad42d28..46afc5690 100644 --- a/openstack/tests/functional/telemetry/v2/test_sample.py +++ b/openstack/tests/functional/telemetry/v2/test_sample.py @@ -20,25 +20,7 @@ "Metering service does not exist") class TestSample(base.BaseFunctionalTest): - meter = None - sample = None - - @classmethod - def setUpClass(cls): - super(TestSample, cls).setUpClass() - cls.meter = next(cls.conn.telemetry.meters()) - resource = next(cls.conn.telemetry.resources()) - sot = cls.conn.telemetry.create_sample( - counter_name=cls.meter.name, - meter=cls.meter.name, - counter_type='gauge', - counter_unit='instance', - counter_volume=1.0, - resource_id=resource.id, - ) - assert isinstance(sot, sample.Sample) - cls.sample = sot - def test_list(self): - ids = [o.id for o in self.conn.telemetry.samples(self.meter)] - self.assertIn(self.sample.id, ids) + for meter in self.conn.telemetry.meters(): + sot = next(self.conn.telemetry.samples(meter)) + assert isinstance(sot, sample.Sample) diff --git a/openstack/tests/unit/telemetry/v2/test_proxy.py b/openstack/tests/unit/telemetry/v2/test_proxy.py index 428a0e0e7..974dd8f01 100644 --- a/openstack/tests/unit/telemetry/v2/test_proxy.py +++ b/openstack/tests/unit/telemetry/v2/test_proxy.py @@ -47,9 +47,6 @@ def test_resources(self): self.verify_list(self.proxy.resources, resource.Resource, paginated=False) - def test_sample_create_attrs(self): - self.verify_create(self.proxy.create_sample, sample.Sample) - def test_sample_find(self): self.verify_find(self.proxy.find_sample, sample.Sample) From 20ba3174bbf33f0a33d934d432401cb0136a50fc Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Thu, 23 Feb 2017 11:48:14 -0700 Subject: [PATCH 1308/3836] Fix the object store set metadata functional test The metadata set functional test is seems to be failing because scheduled deletes are not available on the test gate. The test is cool, but not really required because the custom and system metadata sets are covered in other test cases. Partial-bug: #1665495 Change-Id: I321dca1657ba31722e5806c83ff457bb1339a370 --- .../tests/functional/object_store/v1/test_obj.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/openstack/tests/functional/object_store/v1/test_obj.py b/openstack/tests/functional/object_store/v1/test_obj.py index a96a18eed..89990f20c 100644 --- a/openstack/tests/functional/object_store/v1/test_obj.py +++ b/openstack/tests/functional/object_store/v1/test_obj.py @@ -71,24 +71,13 @@ def test_system_metadata(self): self.assertEqual('attachment', obj.content_disposition) self.assertEqual('deflate', obj.content_encoding) - # set system metadata and custom metadata - self.conn.object_store.set_object_metadata( - obj, k0='v0', delete_after=100) - obj = self.conn.object_store.get_object_metadata(obj) - self.assertIn('k0', obj.metadata) - self.assertEqual('v0', obj.metadata['k0']) - self.assertEqual('attachment', obj.content_disposition) - self.assertEqual('deflate', obj.content_encoding) - - # unset system metadata - self.conn.object_store.delete_object_metadata( - obj, keys=['delete_after']) + # set custom metadata + self.conn.object_store.set_object_metadata(obj, k0='v0') obj = self.conn.object_store.get_object_metadata(obj) self.assertIn('k0', obj.metadata) self.assertEqual('v0', obj.metadata['k0']) self.assertEqual('attachment', obj.content_disposition) self.assertEqual('deflate', obj.content_encoding) - self.assertIsNone(obj.delete_at) # unset more system metadata self.conn.object_store.delete_object_metadata( From f82fe0605998358a74f1c6e349a96225cd468c1e Mon Sep 17 00:00:00 2001 From: Lars Kellogg-Stedman Date: Thu, 23 Feb 2017 14:31:06 -0500 Subject: [PATCH 1309/3836] the role resource should not have put_create=True according to the api docs [1], the role create operation is an HTTP POST request [1] https://developer.openstack.org/api-ref/identity/v3/#create-role Change-Id: I0f6efe51abf15b298ce90d41f2071edd9c1211b6 --- openstack/identity/v3/role.py | 1 - openstack/tests/unit/identity/v3/test_role.py | 1 - 2 files changed, 2 deletions(-) diff --git a/openstack/identity/v3/role.py b/openstack/identity/v3/role.py index bd946d3d2..58c8b0fbc 100644 --- a/openstack/identity/v3/role.py +++ b/openstack/identity/v3/role.py @@ -26,7 +26,6 @@ class Role(resource.Resource): allow_update = True allow_delete = True allow_list = True - put_create = True _query_mapping = resource.QueryParameters( 'name', 'domain_id') diff --git a/openstack/tests/unit/identity/v3/test_role.py b/openstack/tests/unit/identity/v3/test_role.py index e338657a0..8f9e9809d 100644 --- a/openstack/tests/unit/identity/v3/test_role.py +++ b/openstack/tests/unit/identity/v3/test_role.py @@ -35,7 +35,6 @@ def test_basic(self): self.assertTrue(sot.allow_update) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertTrue(sot.put_create) def test_make_it(self): sot = role.Role(**EXAMPLE) From 951d4e29d3a3675a6cb472d5b4c4836f6d0b8e20 Mon Sep 17 00:00:00 2001 From: Lars Kellogg-Stedman Date: Thu, 23 Feb 2017 16:31:56 -0500 Subject: [PATCH 1310/3836] keystone api v2.0 does not paginate roles or users when attempting to enumerate users or roles with against a v2.0 keystone api, openstacksdk gets stuck in a loop because it is attempting to use pagination, but the server does not support pagination for anything other than tenants. From #keystone: 2017-02-23 16:13 larsks: only tenants were supposed to be paginated in v2.0. Other things afaik no. Change-Id: Id10f603d3e74dbb1821c016eaf648b5cc531de5a --- openstack/identity/v2/_proxy.py | 4 ++-- openstack/tests/unit/identity/v2/test_proxy.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openstack/identity/v2/_proxy.py b/openstack/identity/v2/_proxy.py index 348bcb97e..a25d83241 100644 --- a/openstack/identity/v2/_proxy.py +++ b/openstack/identity/v2/_proxy.py @@ -80,7 +80,7 @@ def roles(self, **query): :returns: A generator of role instances. :rtype: :class:`~openstack.identity.v2.role.Role` """ - return self._list(_role.Role, paginated=True, **query) + return self._list(_role.Role, paginated=False, **query) def update_role(self, role, **attrs): """Update a role @@ -234,7 +234,7 @@ def users(self, **query): :returns: A generator of user instances. :rtype: :class:`~openstack.identity.v2.user.User` """ - return self._list(_user.User, paginated=True, **query) + return self._list(_user.User, paginated=False, **query) def update_user(self, user, **attrs): """Update a user diff --git a/openstack/tests/unit/identity/v2/test_proxy.py b/openstack/tests/unit/identity/v2/test_proxy.py index 83a39c8ff..5080cc9e0 100644 --- a/openstack/tests/unit/identity/v2/test_proxy.py +++ b/openstack/tests/unit/identity/v2/test_proxy.py @@ -38,7 +38,7 @@ def test_role_get(self): self.verify_get(self.proxy.get_role, role.Role) def test_roles(self): - self.verify_list(self.proxy.roles, role.Role, paginated=True) + self.verify_list(self.proxy.roles, role.Role) def test_role_update(self): self.verify_update(self.proxy.update_role, role.Role) @@ -80,7 +80,7 @@ def test_user_get(self): self.verify_get(self.proxy.get_user, user.User) def test_users(self): - self.verify_list(self.proxy.users, user.User, paginated=True) + self.verify_list(self.proxy.users, user.User) def test_user_update(self): self.verify_update(self.proxy.update_user, user.User) From 81e04fcec6cb333a7d44124c045358c4904cf765 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 27 Feb 2017 09:34:24 -0600 Subject: [PATCH 1311/3836] Add ability to skip yaml loading Added a flag, 'load_yaml_config' that defaults to True. If set to false, no clouds.yaml files will be loaded. This is beneficial if os-client-config wants to be used inside of a service where end-user clouds.yaml files would make things more confusing. Change-Id: Idbc82bb931e9edf1bbcc575237c0e202e219c218 --- os_client_config/__init__.py | 3 +- os_client_config/config.py | 14 +++++--- os_client_config/tests/test_config.py | 32 +++++++++++++++++++ .../notes/load-yaml-3177efca78e5c67a.yaml | 7 ++++ 4 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/load-yaml-3177efca78e5c67a.yaml diff --git a/os_client_config/__init__.py b/os_client_config/__init__.py index e8d7fc0f8..a36a13061 100644 --- a/os_client_config/__init__.py +++ b/os_client_config/__init__.py @@ -24,7 +24,8 @@ def get_config(service_key=None, options=None, **kwargs): - config = OpenStackConfig() + load_yaml_config = kwargs.pop('load_yaml_config', True) + config = OpenStackConfig(load_yaml_config=load_yaml_config) if options: config.register_argparse_arguments(options, sys.argv, service_key) parsed_options = options.parse_known_args(sys.argv) diff --git a/os_client_config/config.py b/os_client_config/config.py index 9b4a709fd..0e241dfee 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -173,13 +173,19 @@ class OpenStackConfig(object): def __init__(self, config_files=None, vendor_files=None, override_defaults=None, force_ipv4=None, envvar_prefix=None, secure_files=None, - pw_func=None, session_constructor=None): + pw_func=None, session_constructor=None, + load_yaml_config=True): self.log = _log.setup_logging(__name__) self._session_constructor = session_constructor - self._config_files = config_files or CONFIG_FILES - self._secure_files = secure_files or SECURE_FILES - self._vendor_files = vendor_files or VENDOR_FILES + if load_yaml_config: + self._config_files = config_files or CONFIG_FILES + self._secure_files = secure_files or SECURE_FILES + self._vendor_files = vendor_files or VENDOR_FILES + else: + self._config_files = [] + self._secure_files = [] + self._vendor_files = [] config_file_override = os.environ.pop('OS_CLIENT_CONFIG_FILE', None) if config_file_override: diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index ad3685ab1..195488d92 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -16,6 +16,7 @@ import copy import os +import extras import fixtures import testtools import yaml @@ -577,6 +578,37 @@ def test_get_one_cloud_per_region_network(self): self.assertEqual(cc.region_name, 'region2') self.assertEqual('my-network', cc.config['external_network']) + def test_get_one_cloud_no_yaml_no_cloud(self): + c = config.OpenStackConfig(load_yaml_config=False) + + self.assertRaises( + exceptions.OpenStackConfigException, + c.get_one_cloud, + cloud='_test_cloud_regions', region_name='region2', argparse=None) + + def test_get_one_cloud_no_yaml(self): + c = config.OpenStackConfig(load_yaml_config=False) + + cc = c.get_one_cloud( + region_name='region2', argparse=None, + **base.USER_CONF['clouds']['_test_cloud_regions']) + # Not using assert_cloud_details because of cache settings which + # are not present without the file + self.assertIsInstance(cc, cloud_config.CloudConfig) + self.assertTrue(extras.safe_hasattr(cc, 'auth')) + self.assertIsInstance(cc.auth, dict) + self.assertIsNone(cc.cloud) + self.assertIn('username', cc.auth) + self.assertEqual('testuser', cc.auth['username']) + self.assertEqual('testpass', cc.auth['password']) + self.assertFalse(cc.config['image_api_use_tasks']) + self.assertTrue('project_name' in cc.auth or 'project_id' in cc.auth) + if 'project_name' in cc.auth: + self.assertEqual('testproject', cc.auth['project_name']) + elif 'project_id' in cc.auth: + self.assertEqual('testproject', cc.auth['project_id']) + self.assertEqual(cc.region_name, 'region2') + def test_fix_env_args(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) diff --git a/releasenotes/notes/load-yaml-3177efca78e5c67a.yaml b/releasenotes/notes/load-yaml-3177efca78e5c67a.yaml new file mode 100644 index 000000000..2438f83a4 --- /dev/null +++ b/releasenotes/notes/load-yaml-3177efca78e5c67a.yaml @@ -0,0 +1,7 @@ +--- +features: + - Added a flag, 'load_yaml_config' that defaults to True. + If set to false, no clouds.yaml files will be loaded. This + is beneficial if os-client-config wants to be used inside of + a service where end-user clouds.yaml files would make things + more confusing. From 0d044b2ecd432f82b7551f7e44ff501d2235f3f1 Mon Sep 17 00:00:00 2001 From: Jens Rosenboom Date: Mon, 27 Feb 2017 12:09:39 +0100 Subject: [PATCH 1312/3836] Add missing attribute to Subnet resource The use_default_subnet_pool attribute was missing, leading to errors when trying to create a subnet using the default subnet pool. Change-Id: I72c0be77d96f3891748cdd69c382211dc20dbf5e Partial-Bug: 1668223 --- openstack/network/v2/subnet.py | 6 ++++++ openstack/tests/unit/network/v2/test_subnet.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/openstack/network/v2/subnet.py b/openstack/network/v2/subnet.py index e52229dcc..1466017b7 100644 --- a/openstack/network/v2/subnet.py +++ b/openstack/network/v2/subnet.py @@ -35,6 +35,7 @@ class Subnet(resource.Resource): is_dhcp_enabled='enable_dhcp', project_id='tenant_id', subnet_pool_id='subnetpool_id', + use_default_subnet_pool='use_default_subnetpool', ) # Properties @@ -81,3 +82,8 @@ class Subnet(resource.Resource): subnet_pool_id = resource.Body('subnetpool_id') #: Timestamp when the subnet was last updated. updated_at = resource.Body('updated_at') + #: Whether to use the default subnet pool to obtain a CIDR. + use_default_subnet_pool = resource.Body( + 'use_default_subnetpool', + type=bool + ) diff --git a/openstack/tests/unit/network/v2/test_subnet.py b/openstack/tests/unit/network/v2/test_subnet.py index f315d4a53..2e344a9a8 100644 --- a/openstack/tests/unit/network/v2/test_subnet.py +++ b/openstack/tests/unit/network/v2/test_subnet.py @@ -36,6 +36,7 @@ 'subnetpool_id': '16', 'tenant_id': '17', 'updated_at': '18', + 'use_default_subnetpool': True, } @@ -75,3 +76,4 @@ def test_make_it(self): self.assertEqual(EXAMPLE['subnetpool_id'], sot.subnet_pool_id) self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) + self.assertTrue(sot.use_default_subnet_pool) From 0e26e7ddd03003a4980baeca4f649eacdd2cb8ff Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Tue, 28 Feb 2017 09:58:45 -0500 Subject: [PATCH 1313/3836] Reorganize block store docs This change organizes the block store docs by topic rather than letting autodoc organize methods by the order they appear in the _proxy.py file. Change-Id: I743562a8c9a1dc7aaceef1d14c571c7a07855802 --- doc/source/users/proxies/block_store.rst | 29 +++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/doc/source/users/proxies/block_store.rst b/doc/source/users/proxies/block_store.rst index fcc09c4c0..92c12cd15 100644 --- a/doc/source/users/proxies/block_store.rst +++ b/doc/source/users/proxies/block_store.rst @@ -12,5 +12,32 @@ The block_store high-level interface is available through the ``block_store`` member of a :class:`~openstack.connection.Connection` object. The ``block_store`` member will only be added if the service is detected. +Volume Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_store.v2._proxy.Proxy + + .. automethod:: openstack.block_store.v2._proxy.Proxy.create_volume + .. automethod:: openstack.block_store.v2._proxy.Proxy.delete_volume + .. automethod:: openstack.block_store.v2._proxy.Proxy.get_volume + .. automethod:: openstack.block_store.v2._proxy.Proxy.volumes + +Type Operations +^^^^^^^^^^^^^^^ + .. autoclass:: openstack.block_store.v2._proxy.Proxy - :members: + + .. automethod:: openstack.block_store.v2._proxy.Proxy.create_type + .. automethod:: openstack.block_store.v2._proxy.Proxy.delete_type + .. automethod:: openstack.block_store.v2._proxy.Proxy.get_type + .. automethod:: openstack.block_store.v2._proxy.Proxy.types + +Snapshot Operations +^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_store.v2._proxy.Proxy + + .. automethod:: openstack.block_store.v2._proxy.Proxy.create_snapshot + .. automethod:: openstack.block_store.v2._proxy.Proxy.delete_snapshot + .. automethod:: openstack.block_store.v2._proxy.Proxy.get_snapshot + .. automethod:: openstack.block_store.v2._proxy.Proxy.snapshots From 38c6ba8345aaeea46411e1aba123a9dbf5b185b6 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Tue, 28 Feb 2017 10:31:53 -0500 Subject: [PATCH 1314/3836] Reorganize telemetry docs This change organizes the telemetry docs by topic rather than letting autodoc organize methods by the order they appear in the _proxy.py file. This also adds telemetry.alarm docs, which weren't previously included. Change-Id: Id5d8e201f99d9f8fc78d09694e4cdc576af2fefd --- doc/source/users/proxies/telemetry.rst | 70 +++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/doc/source/users/proxies/telemetry.rst b/doc/source/users/proxies/telemetry.rst index ebd8d88de..465ba7d9f 100644 --- a/doc/source/users/proxies/telemetry.rst +++ b/doc/source/users/proxies/telemetry.rst @@ -6,7 +6,7 @@ Telemetry API For details on how to use telemetry, see :doc:`/users/guides/telemetry` -.. automodule:: openstack.telemetry.v2._proxy +.. automethod:: openstack.telemetry.v2._proxy The Telemetry Class ------------------- @@ -15,5 +15,71 @@ The telemetry high-level interface is available through the ``telemetry`` member of a :class:`~openstack.connection.Connection` object. The ``telemetry`` member will only be added if the service is detected. +Sample Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.telemetry.v2._proxy.Proxy + + .. automethod:: openstack.telemetry.v2._proxy.Proxy.find_sample + .. automethod:: openstack.telemetry.v2._proxy.Proxy.samples + +Statistic Operations +^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.telemetry.v2._proxy.Proxy + + .. automethod:: openstack.telemetry.v2._proxy.Proxy.find_statistics + .. automethod:: openstack.telemetry.v2._proxy.Proxy.statistics + +Resource Operations +^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.telemetry.v2._proxy.Proxy + + .. automethod:: openstack.telemetry.v2._proxy.Proxy.get_resource + .. automethod:: openstack.telemetry.v2._proxy.Proxy.find_resource + .. automethod:: openstack.telemetry.v2._proxy.Proxy.resources + +Meter Operations +^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.telemetry.v2._proxy.Proxy + + .. automethod:: openstack.telemetry.v2._proxy.Proxy.find_meter + .. automethod:: openstack.telemetry.v2._proxy.Proxy.meters + +Capability Operations +^^^^^^^^^^^^^^^^^^^^^ + .. autoclass:: openstack.telemetry.v2._proxy.Proxy - :members: + + .. automethod:: openstack.telemetry.v2._proxy.Proxy.find_capability + .. automethod:: openstack.telemetry.v2._proxy.Proxy.capabilities + +The Alarm Class +--------------- + +The alarm high-level interface is available through the ``telemetry.alarm`` +member of a :class:`~openstack.connection.Connection` object. The +``telemetry.alarm`` member will only be added if the service is detected. + +Alarm Operations +^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.telemetry.alarm.v2._proxy.Proxy + + .. automethod:: openstack.telemetry.alarm.v2._proxy.Proxy.create_alarm + .. automethod:: openstack.telemetry.alarm.v2._proxy.Proxy.update_alarm + .. automethod:: openstack.telemetry.alarm.v2._proxy.Proxy.delete_alarm + .. automethod:: openstack.telemetry.alarm.v2._proxy.Proxy.get_alarm + .. automethod:: openstack.telemetry.alarm.v2._proxy.Proxy.find_alarm + .. automethod:: openstack.telemetry.alarm.v2._proxy.Proxy.alarms + + +Alarm Change Operations +^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.telemetry.alarm.v2._proxy.Proxy + + .. automethod:: openstack.telemetry.alarm.v2._proxy.Proxy.find_alarm_change + .. automethod:: openstack.telemetry.alarm.v2._proxy.Proxy.alarm_changes From d321a14ecbe79c888e891424250c1b5bdfd2ea65 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 28 Feb 2017 11:22:30 -0600 Subject: [PATCH 1315/3836] Pass ironic microversion through from api_version If someone sets baremetal_api_version to 1.29 right now, we don't really do anything with that information. Pass it through to the constructor for ironicclient in get_legacy_client(). Change-Id: I470fbb8852eac7d5cb35aef549ac591d63f3636f --- os_client_config/cloud_config.py | 6 ++++++ .../notes/ironic-microversion-ba5b0f36f11196a6.yaml | 4 ++++ 2 files changed, 10 insertions(+) create mode 100644 releasenotes/notes/ironic-microversion-ba5b0f36f11196a6.yaml diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index f52756fd8..3521920d4 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -372,6 +372,12 @@ def get_legacy_client( constructor_kwargs['endpoint'] = endpoint if service_key == 'network': constructor_kwargs['api_version'] = version + elif service_key == 'baremetal': + if version != '1': + # Set Ironic Microversion + constructor_kwargs['os_ironic_api_version'] = version + # Version arg is the major version, not the full microstring + constructor_kwargs['version'] = version[0] else: constructor_kwargs['version'] = version if service_key == 'database': diff --git a/releasenotes/notes/ironic-microversion-ba5b0f36f11196a6.yaml b/releasenotes/notes/ironic-microversion-ba5b0f36f11196a6.yaml new file mode 100644 index 000000000..62e36277d --- /dev/null +++ b/releasenotes/notes/ironic-microversion-ba5b0f36f11196a6.yaml @@ -0,0 +1,4 @@ +--- +features: + - Add support for passing Ironic microversion to the ironicclient + constructor in get_legacy_client. From baa9008846fe2cba1492f305f4488a006720b187 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Tue, 28 Feb 2017 12:44:53 -0500 Subject: [PATCH 1316/3836] Reorganize network docs This change organizes the network docs by topic rather than letting autodoc organize methods by the order they appear in the _proxy.py file. Change-Id: I6366911a8410b43a567ca0d9afc22994639c80c8 --- doc/source/users/proxies/network.rst | 346 ++++++++++++++++++++++++++- 1 file changed, 345 insertions(+), 1 deletion(-) diff --git a/doc/source/users/proxies/network.rst b/doc/source/users/proxies/network.rst index 5d9a17130..690e87552 100644 --- a/doc/source/users/proxies/network.rst +++ b/doc/source/users/proxies/network.rst @@ -12,5 +12,349 @@ The network high-level interface is available through the ``network`` member of a :class:`~openstack.connection.Connection` object. The ``network`` member will only be added if the service is detected. +Network Operations +^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + + .. automethod:: openstack.network.v2._proxy.Proxy.create_network + .. automethod:: openstack.network.v2._proxy.Proxy.update_network + .. automethod:: openstack.network.v2._proxy.Proxy.delete_network + .. automethod:: openstack.network.v2._proxy.Proxy.get_network + .. automethod:: openstack.network.v2._proxy.Proxy.find_network + .. automethod:: openstack.network.v2._proxy.Proxy.networks + + .. automethod:: openstack.network.v2._proxy.Proxy.get_network_ip_availability + .. automethod:: openstack.network.v2._proxy.Proxy.find_network_ip_availability + .. automethod:: openstack.network.v2._proxy.Proxy.network_ip_availabilities + + .. automethod:: openstack.network.v2._proxy.Proxy.add_dhcp_agent_to_network + .. automethod:: openstack.network.v2._proxy.Proxy.remove_dhcp_agent_from_network + .. automethod:: openstack.network.v2._proxy.Proxy.dhcp_agent_hosting_networks + +Port Operations +^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + + .. automethod:: openstack.network.v2._proxy.Proxy.create_port + .. automethod:: openstack.network.v2._proxy.Proxy.update_port + .. automethod:: openstack.network.v2._proxy.Proxy.delete_port + .. automethod:: openstack.network.v2._proxy.Proxy.get_port + .. automethod:: openstack.network.v2._proxy.Proxy.find_port + .. automethod:: openstack.network.v2._proxy.Proxy.ports + + .. automethod:: openstack.network.v2._proxy.Proxy.add_ip_to_port + .. automethod:: openstack.network.v2._proxy.Proxy.remove_ip_from_port + +Router Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + + .. automethod:: openstack.network.v2._proxy.Proxy.create_router + .. automethod:: openstack.network.v2._proxy.Proxy.update_router + .. automethod:: openstack.network.v2._proxy.Proxy.delete_router + .. automethod:: openstack.network.v2._proxy.Proxy.get_router + .. automethod:: openstack.network.v2._proxy.Proxy.find_router + .. automethod:: openstack.network.v2._proxy.Proxy.routers + + .. automethod:: openstack.network.v2._proxy.Proxy.add_gateway_to_router + .. automethod:: openstack.network.v2._proxy.Proxy.remove_gateway_from_router + .. automethod:: openstack.network.v2._proxy.Proxy.add_interface_to_router + .. automethod:: openstack.network.v2._proxy.Proxy.remove_interface_from_router + +Floating IP Operations +^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + + .. automethod:: openstack.network.v2._proxy.Proxy.create_ip + .. automethod:: openstack.network.v2._proxy.Proxy.update_ip + .. automethod:: openstack.network.v2._proxy.Proxy.delete_ip + .. automethod:: openstack.network.v2._proxy.Proxy.get_ip + .. automethod:: openstack.network.v2._proxy.Proxy.find_ip + .. automethod:: openstack.network.v2._proxy.Proxy.find_available_ip + .. automethod:: openstack.network.v2._proxy.Proxy.ips + +Pool Operations +^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + + .. automethod:: openstack.network.v2._proxy.Proxy.create_pool + .. automethod:: openstack.network.v2._proxy.Proxy.update_pool + .. automethod:: openstack.network.v2._proxy.Proxy.delete_pool + .. automethod:: openstack.network.v2._proxy.Proxy.get_pool + .. automethod:: openstack.network.v2._proxy.Proxy.find_pool + .. automethod:: openstack.network.v2._proxy.Proxy.pools + + .. automethod:: openstack.network.v2._proxy.Proxy.create_pool_member + .. automethod:: openstack.network.v2._proxy.Proxy.update_pool_member + .. automethod:: openstack.network.v2._proxy.Proxy.delete_pool_member + .. automethod:: openstack.network.v2._proxy.Proxy.get_pool_member + .. automethod:: openstack.network.v2._proxy.Proxy.find_pool_member + .. automethod:: openstack.network.v2._proxy.Proxy.pool_members + +Auto Allocated Topology Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + + .. automethod:: openstack.network.v2._proxy.Proxy.delete_auto_allocated_topology + .. automethod:: openstack.network.v2._proxy.Proxy.get_auto_allocated_topology + .. automethod:: openstack.network.v2._proxy.Proxy.validate_auto_allocated_topology + +Security Group Operations +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + + .. automethod:: openstack.network.v2._proxy.Proxy.create_security_group + .. automethod:: openstack.network.v2._proxy.Proxy.update_security_group + .. automethod:: openstack.network.v2._proxy.Proxy.delete_security_group + .. automethod:: openstack.network.v2._proxy.Proxy.get_security_group + .. automethod:: openstack.network.v2._proxy.Proxy.get_security_group_rule + .. automethod:: openstack.network.v2._proxy.Proxy.find_security_group + .. automethod:: openstack.network.v2._proxy.Proxy.find_security_group_rule + .. automethod:: openstack.network.v2._proxy.Proxy.security_group_rules + .. automethod:: openstack.network.v2._proxy.Proxy.security_groups + + .. automethod:: openstack.network.v2._proxy.Proxy.security_group_allow_ping + .. automethod:: openstack.network.v2._proxy.Proxy.security_group_open_port + + .. automethod:: openstack.network.v2._proxy.Proxy.create_security_group_rule + .. automethod:: openstack.network.v2._proxy.Proxy.delete_security_group_rule + +Availability Zone Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + + .. automethod:: openstack.network.v2._proxy.Proxy.availability_zones + +Address Scope Operations +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + + .. automethod:: openstack.network.v2._proxy.Proxy.create_address_scope + .. automethod:: openstack.network.v2._proxy.Proxy.update_address_scope + .. automethod:: openstack.network.v2._proxy.Proxy.delete_address_scope + .. automethod:: openstack.network.v2._proxy.Proxy.get_address_scope + .. automethod:: openstack.network.v2._proxy.Proxy.find_address_scope + .. automethod:: openstack.network.v2._proxy.Proxy.address_scopes + +Quota Operations +^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + + .. automethod:: openstack.network.v2._proxy.Proxy.update_quota + .. automethod:: openstack.network.v2._proxy.Proxy.delete_quota + .. automethod:: openstack.network.v2._proxy.Proxy.get_quota + .. automethod:: openstack.network.v2._proxy.Proxy.get_quota_default + .. automethod:: openstack.network.v2._proxy.Proxy.quotas + +QoS Operations +^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + + .. automethod:: openstack.network.v2._proxy.Proxy.create_qos_policy + .. automethod:: openstack.network.v2._proxy.Proxy.update_qos_policy + .. automethod:: openstack.network.v2._proxy.Proxy.delete_qos_policy + .. automethod:: openstack.network.v2._proxy.Proxy.get_qos_policy + .. automethod:: openstack.network.v2._proxy.Proxy.find_qos_policy + .. automethod:: openstack.network.v2._proxy.Proxy.qos_policies + .. automethod:: openstack.network.v2._proxy.Proxy.qos_rule_types + + .. automethod:: openstack.network.v2._proxy.Proxy.create_qos_minimum_bandwidth_rule + .. automethod:: openstack.network.v2._proxy.Proxy.update_qos_minimum_bandwidth_rule + .. automethod:: openstack.network.v2._proxy.Proxy.delete_qos_minimum_bandwidth_rule + .. automethod:: openstack.network.v2._proxy.Proxy.get_qos_minimum_bandwidth_rule + .. automethod:: openstack.network.v2._proxy.Proxy.find_qos_minimum_bandwidth_rule + .. automethod:: openstack.network.v2._proxy.Proxy.qos_minimum_bandwidth_rules + + .. automethod:: openstack.network.v2._proxy.Proxy.create_qos_bandwidth_limit_rule + .. automethod:: openstack.network.v2._proxy.Proxy.update_qos_bandwidth_limit_rule + .. automethod:: openstack.network.v2._proxy.Proxy.delete_qos_bandwidth_limit_rule + .. automethod:: openstack.network.v2._proxy.Proxy.get_qos_bandwidth_limit_rule + .. automethod:: openstack.network.v2._proxy.Proxy.find_qos_bandwidth_limit_rule + .. automethod:: openstack.network.v2._proxy.Proxy.qos_bandwidth_limit_rules + + .. automethod:: openstack.network.v2._proxy.Proxy.create_qos_dscp_marking_rule + .. automethod:: openstack.network.v2._proxy.Proxy.update_qos_dscp_marking_rule + .. automethod:: openstack.network.v2._proxy.Proxy.delete_qos_dscp_marking_rule + .. automethod:: openstack.network.v2._proxy.Proxy.get_qos_dscp_marking_rule + .. automethod:: openstack.network.v2._proxy.Proxy.find_qos_dscp_marking_rule + .. automethod:: openstack.network.v2._proxy.Proxy.qos_dscp_marking_rules + +Agent Operations +^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + + .. automethod:: openstack.network.v2._proxy.Proxy.delete_agent + .. automethod:: openstack.network.v2._proxy.Proxy.update_agent + .. automethod:: openstack.network.v2._proxy.Proxy.get_agent + .. automethod:: openstack.network.v2._proxy.Proxy.agents + .. automethod:: openstack.network.v2._proxy.Proxy.agent_hosted_routers + .. automethod:: openstack.network.v2._proxy.Proxy.routers_hosting_l3_agents + .. automethod:: openstack.network.v2._proxy.Proxy.network_hosting_dhcp_agents + + .. automethod:: openstack.network.v2._proxy.Proxy.add_router_to_agent + .. automethod:: openstack.network.v2._proxy.Proxy.remove_router_from_agent + +RBAC Operations +^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + + .. automethod:: openstack.network.v2._proxy.Proxy.create_rbac_policy + .. automethod:: openstack.network.v2._proxy.Proxy.update_rbac_policy + .. automethod:: openstack.network.v2._proxy.Proxy.delete_rbac_policy + .. automethod:: openstack.network.v2._proxy.Proxy.get_rbac_policy + .. automethod:: openstack.network.v2._proxy.Proxy.find_rbac_policy + .. automethod:: openstack.network.v2._proxy.Proxy.rbac_policies + +Listener Operations +^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + + .. automethod:: openstack.network.v2._proxy.Proxy.create_listener + .. automethod:: openstack.network.v2._proxy.Proxy.update_listener + .. automethod:: openstack.network.v2._proxy.Proxy.delete_listener + .. automethod:: openstack.network.v2._proxy.Proxy.get_listener + .. automethod:: openstack.network.v2._proxy.Proxy.find_listener + .. automethod:: openstack.network.v2._proxy.Proxy.listeners + +Subnet Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + + .. automethod:: openstack.network.v2._proxy.Proxy.create_subnet + .. automethod:: openstack.network.v2._proxy.Proxy.update_subnet + .. automethod:: openstack.network.v2._proxy.Proxy.delete_subnet + .. automethod:: openstack.network.v2._proxy.Proxy.get_subnet + .. automethod:: openstack.network.v2._proxy.Proxy.get_subnet_ports + .. automethod:: openstack.network.v2._proxy.Proxy.find_subnet + .. automethod:: openstack.network.v2._proxy.Proxy.subnets + + .. automethod:: openstack.network.v2._proxy.Proxy.create_subnet_pool + .. automethod:: openstack.network.v2._proxy.Proxy.update_subnet_pool + .. automethod:: openstack.network.v2._proxy.Proxy.delete_subnet_pool + .. automethod:: openstack.network.v2._proxy.Proxy.get_subnet_pool + .. automethod:: openstack.network.v2._proxy.Proxy.find_subnet_pool + .. automethod:: openstack.network.v2._proxy.Proxy.subnet_pools + +Load Balancer Operations +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + + .. automethod:: openstack.network.v2._proxy.Proxy.create_load_balancer + .. automethod:: openstack.network.v2._proxy.Proxy.update_load_balancer + .. automethod:: openstack.network.v2._proxy.Proxy.delete_load_balancer + .. automethod:: openstack.network.v2._proxy.Proxy.get_load_balancer + .. automethod:: openstack.network.v2._proxy.Proxy.find_load_balancer + .. automethod:: openstack.network.v2._proxy.Proxy.load_balancers + +Health Monitor Operations +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + + .. automethod:: openstack.network.v2._proxy.Proxy.create_health_monitor + .. automethod:: openstack.network.v2._proxy.Proxy.update_health_monitor + .. automethod:: openstack.network.v2._proxy.Proxy.delete_health_monitor + .. automethod:: openstack.network.v2._proxy.Proxy.get_health_monitor + .. automethod:: openstack.network.v2._proxy.Proxy.find_health_monitor + .. automethod:: openstack.network.v2._proxy.Proxy.health_monitors + +Metering Label Operations +^^^^^^^^^^^^^^^^^^^^^^^^^ + .. autoclass:: openstack.network.v2._proxy.Proxy - :members: + + .. automethod:: openstack.network.v2._proxy.Proxy.create_metering_label + .. automethod:: openstack.network.v2._proxy.Proxy.update_metering_label + .. automethod:: openstack.network.v2._proxy.Proxy.delete_metering_label + .. automethod:: openstack.network.v2._proxy.Proxy.get_metering_label + .. automethod:: openstack.network.v2._proxy.Proxy.find_metering_label + .. automethod:: openstack.network.v2._proxy.Proxy.metering_labels + + .. automethod:: openstack.network.v2._proxy.Proxy.create_metering_label_rule + .. automethod:: openstack.network.v2._proxy.Proxy.update_metering_label_rule + .. automethod:: openstack.network.v2._proxy.Proxy.delete_metering_label_rule + .. automethod:: openstack.network.v2._proxy.Proxy.get_metering_label_rule + .. automethod:: openstack.network.v2._proxy.Proxy.find_metering_label_rule + .. automethod:: openstack.network.v2._proxy.Proxy.metering_label_rules + +Segment Operations +^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + + .. automethod:: openstack.network.v2._proxy.Proxy.create_segment + .. automethod:: openstack.network.v2._proxy.Proxy.update_segment + .. automethod:: openstack.network.v2._proxy.Proxy.delete_segment + .. automethod:: openstack.network.v2._proxy.Proxy.get_segment + .. automethod:: openstack.network.v2._proxy.Proxy.find_segment + .. automethod:: openstack.network.v2._proxy.Proxy.segments + +Flavor Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + + .. automethod:: openstack.network.v2._proxy.Proxy.create_flavor + .. automethod:: openstack.network.v2._proxy.Proxy.update_flavor + .. automethod:: openstack.network.v2._proxy.Proxy.delete_flavor + .. automethod:: openstack.network.v2._proxy.Proxy.get_flavor + .. automethod:: openstack.network.v2._proxy.Proxy.find_flavor + .. automethod:: openstack.network.v2._proxy.Proxy.flavors + +Service Profile Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + + .. automethod:: openstack.network.v2._proxy.Proxy.create_service_profile + .. automethod:: openstack.network.v2._proxy.Proxy.update_service_profile + .. automethod:: openstack.network.v2._proxy.Proxy.delete_service_profile + .. automethod:: openstack.network.v2._proxy.Proxy.get_service_profile + .. automethod:: openstack.network.v2._proxy.Proxy.find_service_profile + .. automethod:: openstack.network.v2._proxy.Proxy.service_profiles + + .. automethod:: openstack.network.v2._proxy.Proxy.associate_flavor_with_service_profile + .. automethod:: openstack.network.v2._proxy.Proxy.disassociate_flavor_from_service_profile + +VPN Operations +^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + + .. automethod:: openstack.network.v2._proxy.Proxy.create_vpn_service + .. automethod:: openstack.network.v2._proxy.Proxy.update_vpn_service + .. automethod:: openstack.network.v2._proxy.Proxy.delete_vpn_service + .. automethod:: openstack.network.v2._proxy.Proxy.get_vpn_service + .. automethod:: openstack.network.v2._proxy.Proxy.find_vpn_service + .. automethod:: openstack.network.v2._proxy.Proxy.vpn_services + +Extension Operations +^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + + .. automethod:: openstack.network.v2._proxy.Proxy.find_extension + .. automethod:: openstack.network.v2._proxy.Proxy.extensions + +Service Provider Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + + .. automethod:: openstack.network.v2._proxy.Proxy.service_providers From 1ddaa6bef9467097cbd3f2b8ed76b8cb1e8a7995 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Tue, 28 Feb 2017 10:46:00 -0500 Subject: [PATCH 1317/3836] Reorganize workflow docs This change organizes the workflow docs by topic rather than letting autodoc organize methods by the order they appear in the _proxy.py file. Change-Id: I3f220829ae7039a38bf76dee59131fce11af95f0 --- doc/source/users/proxies/workflow.rst | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/doc/source/users/proxies/workflow.rst b/doc/source/users/proxies/workflow.rst index 9fea0c466..5f8cbe5b5 100644 --- a/doc/source/users/proxies/workflow.rst +++ b/doc/source/users/proxies/workflow.rst @@ -1,7 +1,7 @@ Workflow API ============ -For details on how to use block_store, see :doc:`/users/guides/block_store` +For details on how to use workflow, see :doc:`/users/guides/workflow` .. automodule:: openstack.workflow.v2._proxy @@ -12,5 +12,24 @@ The workflow high-level interface is available through the ``workflow`` member of a :class:`~openstack.connection.Connection` object. The ``workflow`` member will only be added if the service is detected. +Workflow Operations +^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.workflow.v2._proxy.Proxy + + .. automethod:: openstack.workflow.v2._proxy.Proxy.create_workflow + .. automethod:: openstack.workflow.v2._proxy.Proxy.delete_workflow + .. automethod:: openstack.workflow.v2._proxy.Proxy.get_workflow + .. automethod:: openstack.workflow.v2._proxy.Proxy.find_workflow + .. automethod:: openstack.workflow.v2._proxy.Proxy.workflows + +Execution Operations +^^^^^^^^^^^^^^^^^^^^ + .. autoclass:: openstack.workflow.v2._proxy.Proxy - :members: + + .. automethod:: openstack.workflow.v2._proxy.Proxy.create_execution + .. automethod:: openstack.workflow.v2._proxy.Proxy.delete_execution + .. automethod:: openstack.workflow.v2._proxy.Proxy.get_execution + .. automethod:: openstack.workflow.v2._proxy.Proxy.find_execution + .. automethod:: openstack.workflow.v2._proxy.Proxy.executions From 650c8a5b44b3a4f2e03563256d359ff4b229dbe9 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Tue, 28 Feb 2017 13:29:47 -0500 Subject: [PATCH 1318/3836] Reorganize object_store docs This change organizes the object_store docs by topic rather than letting autodoc organize methods by the order they appear in the _proxy.py file. Change-Id: I495770aaaa1bf97dd728339b3dde42ad1770e384 --- doc/source/users/proxies/object_store.rst | 37 ++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/doc/source/users/proxies/object_store.rst b/doc/source/users/proxies/object_store.rst index 3294d8b6a..db685e4b9 100644 --- a/doc/source/users/proxies/object_store.rst +++ b/doc/source/users/proxies/object_store.rst @@ -11,5 +11,40 @@ The Object Store Class The Object Store high-level interface is exposed as the ``object_store`` object on :class:`~openstack.connection.Connection` objects. +Account Operations +^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.object_store.v1._proxy.Proxy + + .. automethod:: openstack.object_store.v1._proxy.Proxy.get_account_metadata + .. automethod:: openstack.object_store.v1._proxy.Proxy.set_account_metadata + .. automethod:: openstack.object_store.v1._proxy.Proxy.delete_account_metadata + +Container Operations +^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.object_store.v1._proxy.Proxy + + .. automethod:: openstack.object_store.v1._proxy.Proxy.create_container + .. automethod:: openstack.object_store.v1._proxy.Proxy.delete_container + .. automethod:: openstack.object_store.v1._proxy.Proxy.containers + + .. automethod:: openstack.object_store.v1._proxy.Proxy.get_container_metadata + .. automethod:: openstack.object_store.v1._proxy.Proxy.set_container_metadata + .. automethod:: openstack.object_store.v1._proxy.Proxy.delete_container_metadata + +Object Operations +^^^^^^^^^^^^^^^^^ + .. autoclass:: openstack.object_store.v1._proxy.Proxy - :members: + + .. automethod:: openstack.object_store.v1._proxy.Proxy.upload_object + .. automethod:: openstack.object_store.v1._proxy.Proxy.download_object + .. automethod:: openstack.object_store.v1._proxy.Proxy.copy_object + .. automethod:: openstack.object_store.v1._proxy.Proxy.delete_object + .. automethod:: openstack.object_store.v1._proxy.Proxy.get_object + .. automethod:: openstack.object_store.v1._proxy.Proxy.objects + + .. automethod:: openstack.object_store.v1._proxy.Proxy.get_object_metadata + .. automethod:: openstack.object_store.v1._proxy.Proxy.set_object_metadata + .. automethod:: openstack.object_store.v1._proxy.Proxy.delete_object_metadata From 87f5b3b3cb9d8e56bc12315e0ff40d3ecd3bb9f4 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Tue, 28 Feb 2017 13:59:25 -0500 Subject: [PATCH 1319/3836] Reorganize cluster docs This change organizes the cluster docs by topic rather than letting autodoc organize methods by the order they appear in the _proxy.py file. Change-Id: Ieca92ea97d5bfcb781fcb614275d947e222a8783 --- doc/source/users/proxies/cluster.rst | 146 ++++++++++++++++++++++++++- 1 file changed, 145 insertions(+), 1 deletion(-) diff --git a/doc/source/users/proxies/cluster.rst b/doc/source/users/proxies/cluster.rst index 6715fd383..d40721007 100644 --- a/doc/source/users/proxies/cluster.rst +++ b/doc/source/users/proxies/cluster.rst @@ -10,5 +10,149 @@ The cluster high-level interface is available through the ``cluster`` member of a :class:`~openstack.connection.Connection` object. The ``cluster`` member will only be added if the service is detected. + +Build Info Operations +^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.cluster.v1._proxy.Proxy + + .. automethod:: openstack.cluster.v1._proxy.Proxy.get_build_info + + +Profile Type Operations +^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.cluster.v1._proxy.Proxy + + .. automethod:: openstack.cluster.v1._proxy.Proxy.profile_types + .. automethod:: openstack.cluster.v1._proxy.Proxy.get_profile_type + + +Profile Operations +^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.cluster.v1._proxy.Proxy + + .. automethod:: openstack.cluster.v1._proxy.Proxy.create_profile + .. automethod:: openstack.cluster.v1._proxy.Proxy.update_profile + .. automethod:: openstack.cluster.v1._proxy.Proxy.delete_profile + .. automethod:: openstack.cluster.v1._proxy.Proxy.get_profile + .. automethod:: openstack.cluster.v1._proxy.Proxy.find_profile + .. automethod:: openstack.cluster.v1._proxy.Proxy.profiles + + .. automethod:: openstack.cluster.v1._proxy.Proxy.validate_profile + + +Policy Type Operations +^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.cluster.v1._proxy.Proxy + + .. automethod:: openstack.cluster.v1._proxy.Proxy.policy_types + .. automethod:: openstack.cluster.v1._proxy.Proxy.get_policy_type + + +Policy Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.cluster.v1._proxy.Proxy + + .. automethod:: openstack.cluster.v1._proxy.Proxy.create_policy + .. automethod:: openstack.cluster.v1._proxy.Proxy.update_policy + .. automethod:: openstack.cluster.v1._proxy.Proxy.delete_policy + .. automethod:: openstack.cluster.v1._proxy.Proxy.get_policy + .. automethod:: openstack.cluster.v1._proxy.Proxy.find_policy + .. automethod:: openstack.cluster.v1._proxy.Proxy.policies + + .. automethod:: openstack.cluster.v1._proxy.Proxy.validate_policy + + +Cluster Operations +^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.cluster.v1._proxy.Proxy + + .. automethod:: openstack.cluster.v1._proxy.Proxy.create_cluster + .. automethod:: openstack.cluster.v1._proxy.Proxy.update_cluster + .. automethod:: openstack.cluster.v1._proxy.Proxy.delete_cluster + .. automethod:: openstack.cluster.v1._proxy.Proxy.get_cluster + .. automethod:: openstack.cluster.v1._proxy.Proxy.find_cluster + .. automethod:: openstack.cluster.v1._proxy.Proxy.clusters + + .. automethod:: openstack.cluster.v1._proxy.Proxy.check_cluster + .. automethod:: openstack.cluster.v1._proxy.Proxy.recover_cluster + .. automethod:: openstack.cluster.v1._proxy.Proxy.resize_cluster + .. automethod:: openstack.cluster.v1._proxy.Proxy.scale_in_cluster + .. automethod:: openstack.cluster.v1._proxy.Proxy.scale_out_cluster + .. automethod:: openstack.cluster.v1._proxy.Proxy.collect_cluster_attrs + .. automethod:: openstack.cluster.v1._proxy.Proxy.perform_operation_on_cluster + + .. automethod:: openstack.cluster.v1._proxy.Proxy.add_nodes_to_cluster + .. automethod:: openstack.cluster.v1._proxy.Proxy.remove_nodes_from_cluster + .. automethod:: openstack.cluster.v1._proxy.Proxy.replace_nodes_in_cluster + + .. automethod:: openstack.cluster.v1._proxy.Proxy.attach_policy_to_cluster + .. automethod:: openstack.cluster.v1._proxy.Proxy.update_cluster_policy + .. automethod:: openstack.cluster.v1._proxy.Proxy.detach_policy_from_cluster + .. automethod:: openstack.cluster.v1._proxy.Proxy.get_cluster_policy + .. automethod:: openstack.cluster.v1._proxy.Proxy.cluster_policies + + .. automethod:: openstack.cluster.v1._proxy.Proxy.cluster_add_nodes + .. automethod:: openstack.cluster.v1._proxy.Proxy.cluster_attach_policy + .. automethod:: openstack.cluster.v1._proxy.Proxy.cluster_del_nodes + .. automethod:: openstack.cluster.v1._proxy.Proxy.cluster_detach_policy + .. automethod:: openstack.cluster.v1._proxy.Proxy.cluster_operation + .. automethod:: openstack.cluster.v1._proxy.Proxy.cluster_replace_nodes + .. automethod:: openstack.cluster.v1._proxy.Proxy.cluster_resize + .. automethod:: openstack.cluster.v1._proxy.Proxy.cluster_scale_in + .. automethod:: openstack.cluster.v1._proxy.Proxy.cluster_scale_out + .. automethod:: openstack.cluster.v1._proxy.Proxy.cluster_update_policy + + +Node Operations +^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.cluster.v1._proxy.Proxy + + .. automethod:: openstack.cluster.v1._proxy.Proxy.create_node + .. automethod:: openstack.cluster.v1._proxy.Proxy.update_node + .. automethod:: openstack.cluster.v1._proxy.Proxy.delete_node + .. automethod:: openstack.cluster.v1._proxy.Proxy.get_node + .. automethod:: openstack.cluster.v1._proxy.Proxy.find_node + .. automethod:: openstack.cluster.v1._proxy.Proxy.nodes + + .. automethod:: openstack.cluster.v1._proxy.Proxy.check_node + .. automethod:: openstack.cluster.v1._proxy.Proxy.recover_node + .. automethod:: openstack.cluster.v1._proxy.Proxy.perform_operation_on_node + + .. automethod:: openstack.cluster.v1._proxy.Proxy.node_operation + + +Receiver Operations +^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.cluster.v1._proxy.Proxy + + .. automethod:: openstack.cluster.v1._proxy.Proxy.create_receiver + .. automethod:: openstack.cluster.v1._proxy.Proxy.delete_receiver + .. automethod:: openstack.cluster.v1._proxy.Proxy.get_receiver + .. automethod:: openstack.cluster.v1._proxy.Proxy.find_receiver + .. automethod:: openstack.cluster.v1._proxy.Proxy.receivers + + +Action Operations +^^^^^^^^^^^^^^^^^ + .. autoclass:: openstack.cluster.v1._proxy.Proxy - :members: + + .. automethod:: openstack.cluster.v1._proxy.Proxy.get_action + .. automethod:: openstack.cluster.v1._proxy.Proxy.actions + + +Event Operations +^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.cluster.v1._proxy.Proxy + + .. automethod:: openstack.cluster.v1._proxy.Proxy.get_event + .. automethod:: openstack.cluster.v1._proxy.Proxy.events From 45dc2525e829faab526c25d5b2979b04a3db4c09 Mon Sep 17 00:00:00 2001 From: tengqm Date: Wed, 1 Mar 2017 09:54:39 -0500 Subject: [PATCH 1320/3836] Change version of hacking in test-requirements This changes the hacking version requirement to conform with global requirements. It is not synced from global requirements for some reasons and it is causing pep8 errors - pbr version conflicts. Change-Id: Ia8c6dce7562738628884596973df236be74aabd4 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 4b9d3df52..e16cb83f3 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,7 @@ # 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 +hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 beautifulsoup4 # MIT coverage>=4.0 # Apache-2.0 From 02d7e1b3112dbc114ff8efd965fa62823c1bc50f Mon Sep 17 00:00:00 2001 From: tengqm Date: Wed, 1 Mar 2017 04:35:27 -0500 Subject: [PATCH 1321/3836] Add wait_for_xxx methods to cluster proxy The 'wait_for_status' and 'wait_for_delete' proxy methods are heavily used in the senlin code base. This patch revives the two proxy methods in case we may forget to add it back when the corresponding ones in ProxyBase are removed. Change-Id: I1de6fe031901cf5d319496b881815f0f757c6279 --- doc/source/users/proxies/cluster.rst | 9 +++++ openstack/cluster/v1/_proxy.py | 40 +++++++++++++++++++ openstack/tests/unit/cluster/v1/test_proxy.py | 38 ++++++++++++++++++ 3 files changed, 87 insertions(+) diff --git a/doc/source/users/proxies/cluster.rst b/doc/source/users/proxies/cluster.rst index d40721007..fe5c502ef 100644 --- a/doc/source/users/proxies/cluster.rst +++ b/doc/source/users/proxies/cluster.rst @@ -156,3 +156,12 @@ Event Operations .. automethod:: openstack.cluster.v1._proxy.Proxy.get_event .. automethod:: openstack.cluster.v1._proxy.Proxy.events + + +Helper Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.cluster.v1._proxy.Proxy + + .. automethod:: openstack.cluster.v1._proxy.Proxy.wait_for_delete + .. automethod:: openstack.cluster.v1._proxy.Proxy.wait_for_status diff --git a/openstack/cluster/v1/_proxy.py b/openstack/cluster/v1/_proxy.py index 9a517aeb6..5621be5b8 100644 --- a/openstack/cluster/v1/_proxy.py +++ b/openstack/cluster/v1/_proxy.py @@ -1042,3 +1042,43 @@ def events(self, **query): :returns: A generator of event instances. """ return self._list(_event.Event, paginated=True, **query) + + def wait_for_status(self, resource, status, failures=[], interval=2, + wait=120): + """Wait for a resource to be in a particular status. + + :param resource: The resource to wait on to reach the specified status. + The resource must have a ``status`` attribute. + :type resource: A :class:`~openstack.resource2.Resource` object. + :param status: Desired status. + :param list failures: Statuses that would be interpreted as failures. + :param interval: Number of seconds to wait before to consecutive + checks. Default to 2. + :param wait: Maximum number of seconds to wait before the change. + Default to 120. + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to the desired status failed to occur in specified seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + has transited to one of the failure statuses. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute. + """ + return resource2.wait_for_status(self._session, resource, status, + failures, interval, wait) + + def wait_for_delete(self, resource, interval=2, wait=120): + """Wait for a resource to be deleted. + + :param resource: The resource to wait on to be deleted. + :type resource: A :class:`~openstack.resource2.Resource` object. + :param interval: Number of seconds to wait before to consecutive + checks. Default to 2. + :param wait: Maximum number of seconds to wait before the change. + Default to 120. + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource2.wait_for_delete(self._session, resource, interval, + wait) diff --git a/openstack/tests/unit/cluster/v1/test_proxy.py b/openstack/tests/unit/cluster/v1/test_proxy.py index 10b7cae8c..0e7992d34 100644 --- a/openstack/tests/unit/cluster/v1/test_proxy.py +++ b/openstack/tests/unit/cluster/v1/test_proxy.py @@ -497,3 +497,41 @@ def test_events(self): paginated=True, method_kwargs={'limit': 2}, expected_kwargs={'limit': 2}) + + @mock.patch("openstack.resource2.wait_for_status") + def test_wait_for(self, mock_wait): + mock_resource = mock.Mock() + mock_wait.return_value = mock_resource + + self.proxy.wait_for_status(mock_resource, 'ACTIVE') + + mock_wait.assert_called_once_with(self.session, mock_resource, + 'ACTIVE', [], 2, 120) + + @mock.patch("openstack.resource2.wait_for_status") + def test_wait_for_params(self, mock_wait): + mock_resource = mock.Mock() + mock_wait.return_value = mock_resource + + self.proxy.wait_for_status(mock_resource, 'ACTIVE', ['ERROR'], 1, 2) + + mock_wait.assert_called_once_with(self.session, mock_resource, + 'ACTIVE', ['ERROR'], 1, 2) + + @mock.patch("openstack.resource2.wait_for_delete") + def test_wait_for_delete(self, mock_wait): + mock_resource = mock.Mock() + mock_wait.return_value = mock_resource + + self.proxy.wait_for_delete(mock_resource) + + mock_wait.assert_called_once_with(self.session, mock_resource, 2, 120) + + @mock.patch("openstack.resource2.wait_for_delete") + def test_wait_for_delete_params(self, mock_wait): + mock_resource = mock.Mock() + mock_wait.return_value = mock_resource + + self.proxy.wait_for_delete(mock_resource, 1, 2) + + mock_wait.assert_called_once_with(self.session, mock_resource, 1, 2) From 56ea59b13fbbfd70cd54f2742150aaab396ba80e Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Wed, 1 Mar 2017 13:38:30 -0300 Subject: [PATCH 1322/3836] Remove service names in OpenStackCloud docs OpenStackCloud users are not necessarily admins. They do not need to map cloud resources to the underlying services managing them. Change-Id: I2b52680ef620191c71e75eed4f035985c4125e3d --- shade/openstackcloud.py | 80 ++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index b42481e21..8378628a2 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -739,7 +739,7 @@ def range_search(self, data, filters): @_utils.cache_on_arguments() def list_projects(self, domain_id=None, name_or_id=None, filters=None): - """List Keystone projects. + """List projects. With no parameters, returns a full listing of all visible projects. @@ -783,11 +783,11 @@ def search_projects(self, name_or_id=None, filters=None, domain_id=None): domain_id=domain_id, name_or_id=name_or_id, filters=filters) def get_project(self, name_or_id, filters=None, domain_id=None): - """Get exactly one Keystone project. + """Get exactly one project. :param name_or_id: project name or id. :param filters: a dict containing additional filters to use. - :param domain_id: domain id (keystone v3 only) + :param domain_id: domain id (identity v3 only). :returns: a list of ``munch.Munch`` containing the project description. @@ -840,11 +840,11 @@ def create_project( return project def delete_project(self, name_or_id, domain_id=None): - """Delete a project + """Delete a project. :param string name_or_id: Project name or id. - :param string domain_id: Domain id containing the project (keystone - v3 only). + :param string domain_id: Domain id containing the project (identity v3 + only). :returns: True if delete succeeded, False if the project was not found. @@ -872,7 +872,7 @@ def delete_project(self, name_or_id, domain_id=None): @_utils.cache_on_arguments() def list_users(self): - """List Keystone Users. + """List users. :returns: a list of ``munch.Munch`` containing the user description. @@ -884,7 +884,7 @@ def list_users(self): return _utils.normalize_users(users) def search_users(self, name_or_id=None, filters=None): - """Seach Keystone users. + """Search users. :param string name_or_id: user name or id. :param filters: a dict containing additional filters to use. @@ -901,7 +901,7 @@ def search_users(self, name_or_id=None, filters=None): return _utils._filter_list(users, name_or_id, filters) def get_user(self, name_or_id, filters=None): - """Get exactly one Keystone user. + """Get exactly one user. :param string name_or_id: user name or id. :param filters: a dict containing additional filters to use. @@ -917,7 +917,7 @@ def get_user(self, name_or_id, filters=None): return _utils._get_entity(self.search_users, name_or_id, filters) def get_user_by_id(self, user_id, normalize=True): - """Get a Keystone user by ID. + """Get a user by ID. :param string user_id: user ID :param bool normalize: Flag to control dict normalization @@ -1240,7 +1240,7 @@ def create_stack( wait=False, timeout=3600, environment_files=None, **parameters): - """Create a Heat Stack. + """Create a stack. :param string name: Name of the stack. :param string template_file: Path to the template. @@ -1295,7 +1295,7 @@ def update_stack( wait=False, timeout=3600, environment_files=None, **parameters): - """Update a Heat Stack. + """Update a stack. :param string name_or_id: Name or id of the stack to update. :param string template_file: Path to the template. @@ -1353,7 +1353,7 @@ def update_stack( return self.get_stack(name_or_id) def delete_stack(self, name_or_id, wait=False): - """Delete a Heat Stack + """Delete a stack :param string name_or_id: Stack name or id. :param boolean wait: Whether to wait for the delete to finish @@ -1485,7 +1485,7 @@ def search_keypairs(self, name_or_id=None, filters=None): return _utils._filter_list(keypairs, name_or_id, filters) def search_networks(self, name_or_id=None, filters=None): - """Search OpenStack networks + """Search networks :param name_or_id: Name or id of the desired network. :param filters: a dict containing additional filters to use. e.g. @@ -1500,7 +1500,7 @@ def search_networks(self, name_or_id=None, filters=None): return _utils._filter_list(networks, name_or_id, filters) def search_routers(self, name_or_id=None, filters=None): - """Search OpenStack routers + """Search routers :param name_or_id: Name or id of the desired router. :param filters: a dict containing additional filters to use. e.g. @@ -1515,7 +1515,7 @@ def search_routers(self, name_or_id=None, filters=None): return _utils._filter_list(routers, name_or_id, filters) def search_subnets(self, name_or_id=None, filters=None): - """Search OpenStack subnets + """Search subnets :param name_or_id: Name or id of the desired subnet. :param filters: a dict containing additional filters to use. e.g. @@ -1530,7 +1530,7 @@ def search_subnets(self, name_or_id=None, filters=None): return _utils._filter_list(subnets, name_or_id, filters) def search_ports(self, name_or_id=None, filters=None): - """Search OpenStack ports + """Search ports :param name_or_id: Name or id of the desired port. :param filters: a dict containing additional filters to use. e.g. @@ -1626,7 +1626,7 @@ def search_floating_ips(self, id=None, filters=None): return _utils._filter_list(floating_ips, id, filters) def search_stacks(self, name_or_id=None, filters=None): - """Search Heat stacks. + """Search stacks. :param name_or_id: Name or id of the desired stack. :param filters: a dict containing additional filters to use. e.g. @@ -1780,7 +1780,7 @@ def list_flavors(self, get_extra=True): @_utils.cache_on_arguments(should_cache_fn=_no_pending_stacks) def list_stacks(self): - """List all Heat stacks. + """List all stacks. :returns: a list of ``munch.Munch`` containing the stack description. @@ -1927,7 +1927,7 @@ def get_compute_limits(self, name_or_id=None): @_utils.cache_on_arguments(should_cache_fn=_no_pending_images) def list_images(self, filter_deleted=True): - """Get available glance images. + """Get available images. :param filter_deleted: Control whether deleted images are returned. :returns: A list of glance images. @@ -2691,7 +2691,7 @@ def get_image(self, name_or_id, filters=None): def download_image( self, name_or_id, output_path=None, output_file=None, chunk_size=1024): - """Download an image from glance by name or ID + """Download an image by name or ID :param str name_or_id: Name or ID of the image. :param output_path: the output path to write the image to. Either this @@ -2763,7 +2763,7 @@ def get_floating_ip(self, id, filters=None): return _utils._get_entity(self.search_floating_ips, id, filters) def get_stack(self, name_or_id, filters=None): - """Get exactly one Heat stack. + """Get exactly one stack. :param name_or_id: Name or id of the desired stack. :param filters: a dict containing additional filters to use. e.g. @@ -3168,7 +3168,7 @@ def get_image_id(self, image_name, exclude=None): def create_image_snapshot( self, name, server, wait=False, timeout=3600, **metadata): - """Create a glance image by snapshotting an existing server. + """Create an image by snapshotting an existing server. :param name: Name of the image to be created :param server: Server name or id or dict representing the server @@ -3213,7 +3213,7 @@ def wait_for_image(self, image, timeout=3600): def delete_image( self, name_or_id, wait=False, timeout=3600, delete_objects=True): - """Delete an existing glance image. + """Delete an existing image. :param name_or_id: Name of the image to be deleted. :param wait: If True, waits for image to be deleted. @@ -3288,7 +3288,7 @@ def create_image( disable_vendor_agent=True, wait=False, timeout=3600, allow_duplicates=False, meta=None, volume=None, **kwargs): - """Upload an image to Glance. + """Upload an image. :param str name: Name of the image to create. If it is a pathname of an image, the name will be constructed from the @@ -4269,7 +4269,7 @@ def available_floating_ip(self, network=None, server=None): Return the first available floating IP or allocate a new one. - :param network: Nova pool name or Neutron network name or id. + :param network: Name or ID of the network. :param server: Server the IP is for if known :returns: a (normalized) structure with a floating IP address @@ -4308,12 +4308,12 @@ def _get_floating_network_id(self): def _neutron_available_floating_ips( self, network=None, project_id=None, server=None): - """Get a floating IP from a Neutron network. + """Get a floating IP from a network. Return a list of available floating IPs or allocate a new one and return it in a list of 1 element. - :param network: A single Neutron network name or id, or a list of them. + :param network: A single network name or id, or a list of them. :param server: (server) Server the Floating IP is for :returns: a list of floating IP addresses. @@ -4414,7 +4414,7 @@ def create_floating_ip(self, network=None, server=None, port=None, wait=False, timeout=60): """Allocate a new floating IP from a network or a pool. - :param network: Nova pool name or Neutron network name or id + :param network: Name or ID of the network that the floating IP should come from. :param server: (optional) Server dict for the server to create the IP for and to which it should be attached. @@ -4679,7 +4679,7 @@ def _attach_ip_to_server( :param fixed_address: (optional) fixed address to which attach the floating IP to. :param wait: (optional) Wait for the address to appear as assigned - to the server in Nova. Defaults to False. + to the server. Defaults to False. :param timeout: (optional) Seconds to wait, defaults to 60. See the ``wait`` parameter. :param skip_attach: (optional) Skip the actual attach and just do @@ -4936,11 +4936,11 @@ def _add_ip_from_pool( first server port/fixed address :param server: Server dict - :param network: Nova pool name or Neutron network name or id. + :param network: Name or ID of the network. :param fixed_address: a fixed address :param reuse: Try to reuse existing ips. Defaults to True. :param wait: (optional) Wait for the address to appear as assigned - to the server in Nova. Defaults to False. + to the server. Defaults to False. :param timeout: (optional) Seconds to wait, defaults to 60. See the ``wait`` parameter. :param nat_destination: (optional) the name of the network of the @@ -4979,7 +4979,7 @@ def add_ip_list( :param server: a server object :param ips: list of floating IP addresses or a single address :param wait: (optional) Wait for the address to appear as assigned - to the server in Nova. Defaults to False. + to the server. Defaults to False. :param timeout: (optional) Seconds to wait, defaults to 60. See the ``wait`` parameter. :param fixed_address: (optional) Fixed address of the server to @@ -5013,7 +5013,7 @@ def add_auto_ip(self, server, wait=False, timeout=60, reuse=True): :param reuse: Whether or not to attempt to reuse IPs, defaults to True. :param wait: (optional) Wait for the address to appear as assigned - to the server in Nova. Defaults to False. + to the server. Defaults to False. :param timeout: (optional) Seconds to wait, defaults to 60. See the ``wait`` parameter. :param reuse: Try to reuse existing ips. Defaults to True. @@ -5286,7 +5286,7 @@ def create_server( :param admin_pass: (optional extension) add a user supplied admin password. :param wait: (optional) Wait for the address to appear as assigned - to the server in Nova. Defaults to False. + to the server. Defaults to False. :param timeout: (optional) Seconds to wait, defaults to 60. See the ``wait`` parameter. :param reuse_ips: (optional) Whether to attempt to reuse pre-existing @@ -6139,7 +6139,7 @@ def get_object_metadata(self, container, name): def get_object(self, container, obj, query_string=None, resp_chunk_size=1024, outfile=None): - """Get the headers and body of an object from swift + """Get the headers and body of an object :param string container: name of the container. :param string obj: name of the object. @@ -7125,7 +7125,7 @@ def delete_recordset(self, zone, name_or_id): @_utils.cache_on_arguments() def list_cluster_templates(self, detail=False): - """List Magnum ClusterTemplates. + """List ClusterTemplates. ClusterTemplate is the new name for BayModel. @@ -7145,7 +7145,7 @@ def list_cluster_templates(self, detail=False): def search_cluster_templates( self, name_or_id=None, filters=None, detail=False): - """Search Magnum ClusterTemplates. + """Search ClusterTemplates. ClusterTemplate is the new name for BayModel. @@ -7193,7 +7193,7 @@ def get_cluster_template(self, name_or_id, filters=None, detail=False): def create_cluster_template( self, name, image_id=None, keypair_id=None, coe=None, **kwargs): - """Create a Magnum ClusterTemplate. + """Create a ClusterTemplate. ClusterTemplate is the new name for BayModel. @@ -7265,7 +7265,7 @@ def delete_cluster_template(self, name_or_id): 'network_driver', 'tls_disabled', 'public', 'registry_enabled', 'volume_driver') def update_cluster_template(self, name_or_id, operation, **kwargs): - """Update a Magnum ClusterTemplate. + """Update a ClusterTemplate. ClusterTemplate is the new name for BayModel. From 0613d189f750a566366e694ca77511df9eadb396 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Wed, 1 Mar 2017 14:04:36 -0300 Subject: [PATCH 1323/3836] Fix OpenStack and ID misspellings Change-Id: I7d8c04268a95164f2c6e2f740d1c79499c93d3bb --- shade/openstackcloud.py | 148 ++++++++++++++++++++-------------------- 1 file changed, 74 insertions(+), 74 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 8378628a2..c93e96987 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -586,7 +586,7 @@ def auth_token(self): @property def current_project_id(self): - """Get the current project id. + """Get the current project ID. Returns the project_id of the current token scope. None means that the token is domain scoped or unscoped. @@ -743,8 +743,8 @@ def list_projects(self, domain_id=None, name_or_id=None, filters=None): With no parameters, returns a full listing of all visible projects. - :param domain_id: domain id to scope the searched projects. - :param name_or_id: project name or id. + :param domain_id: domain ID to scope the searched projects. + :param name_or_id: project name or ID. :param filters: a dict containing additional filters to use OR A string containing a jmespath expression for further filtering. @@ -753,7 +753,7 @@ def list_projects(self, domain_id=None, name_or_id=None, filters=None): :returns: a list of ``munch.Munch`` containing the projects :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. + the OpenStack API call. """ kwargs = dict( filters=filters, @@ -785,14 +785,14 @@ def search_projects(self, name_or_id=None, filters=None, domain_id=None): def get_project(self, name_or_id, filters=None, domain_id=None): """Get exactly one project. - :param name_or_id: project name or id. + :param name_or_id: project name or ID. :param filters: a dict containing additional filters to use. - :param domain_id: domain id (identity v3 only). + :param domain_id: domain ID (identity v3 only). :returns: a list of ``munch.Munch`` containing the project description. :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. + the OpenStack API call. """ return _utils._get_entity(self.search_projects, name_or_id, filters, domain_id=domain_id) @@ -842,14 +842,14 @@ def create_project( def delete_project(self, name_or_id, domain_id=None): """Delete a project. - :param string name_or_id: Project name or id. - :param string domain_id: Domain id containing the project (identity v3 + :param string name_or_id: Project name or ID. + :param string domain_id: Domain ID containing the project(identity v3 only). :returns: True if delete succeeded, False if the project was not found. :raises: ``OpenStackCloudException`` if something goes wrong during - the openstack API call + the OpenStack API call """ with _utils.shade_exceptions( @@ -877,7 +877,7 @@ def list_users(self): :returns: a list of ``munch.Munch`` containing the user description. :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. + the OpenStack API call. """ with _utils.shade_exceptions("Failed to list users"): users = self.manager.submit_task(_tasks.UserList()) @@ -886,7 +886,7 @@ def list_users(self): def search_users(self, name_or_id=None, filters=None): """Search users. - :param string name_or_id: user name or id. + :param string name_or_id: user name or ID. :param filters: a dict containing additional filters to use. OR A string containing a jmespath expression for further filtering. @@ -895,7 +895,7 @@ def search_users(self, name_or_id=None, filters=None): :returns: a list of ``munch.Munch`` containing the users :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. + the OpenStack API call. """ users = self.list_users() return _utils._filter_list(users, name_or_id, filters) @@ -903,7 +903,7 @@ def search_users(self, name_or_id=None, filters=None): def get_user(self, name_or_id, filters=None): """Get exactly one user. - :param string name_or_id: user name or id. + :param string name_or_id: user name or ID. :param filters: a dict containing additional filters to use. OR A string containing a jmespath expression for further filtering. @@ -912,7 +912,7 @@ def get_user(self, name_or_id, filters=None): :returns: a single ``munch.Munch`` containing the user description. :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. + the OpenStack API call. """ return _utils._get_entity(self.search_users, name_or_id, filters) @@ -1025,7 +1025,7 @@ def add_user_to_group(self, name_or_id, group_name_or_id): :param string group_name_or_id: Group name or ID :raises: ``OpenStackCloudException`` if something goes wrong during - the openstack API call + the OpenStack API call """ user, group = self._get_user_and_group(name_or_id, group_name_or_id) @@ -1046,7 +1046,7 @@ def is_user_in_group(self, name_or_id, group_name_or_id): :returns: True if user is in the group, False otherwise :raises: ``OpenStackCloudException`` if something goes wrong during - the openstack API call + the OpenStack API call """ user, group = self._get_user_and_group(name_or_id, group_name_or_id) @@ -1073,7 +1073,7 @@ def remove_user_from_group(self, name_or_id, group_name_or_id): :param string group_name_or_id: Group name or ID :raises: ``OpenStackCloudException`` if something goes wrong during - the openstack API call + the OpenStack API call """ user, group = self._get_user_and_group(name_or_id, group_name_or_id) @@ -1261,7 +1261,7 @@ def create_stack( :returns: a dict containing the stack description :raises: ``OpenStackCloudException`` if something goes wrong during - the openstack API call + the OpenStack API call """ envfiles, env = template_utils.process_multiple_environments_and_files( env_paths=environment_files) @@ -1297,7 +1297,7 @@ def update_stack( **parameters): """Update a stack. - :param string name_or_id: Name or id of the stack to update. + :param string name_or_id: Name or ID of the stack to update. :param string template_file: Path to the template. :param string template_url: URL of template. :param string template_object: URL to retrieve template object. @@ -1316,7 +1316,7 @@ def update_stack( :returns: a dict containing the stack description :raises: ``OpenStackCloudException`` if something goes wrong during - the openstack API calls + the OpenStack API calls """ envfiles, env = template_utils.process_multiple_environments_and_files( env_paths=environment_files) @@ -1355,13 +1355,13 @@ def update_stack( def delete_stack(self, name_or_id, wait=False): """Delete a stack - :param string name_or_id: Stack name or id. + :param string name_or_id: Stack name or ID. :param boolean wait: Whether to wait for the delete to finish :returns: True if delete succeeded, False if the stack was not found. :raises: ``OpenStackCloudException`` if something goes wrong during - the openstack API call + the OpenStack API call """ stack = self.get_stack(name_or_id) if stack is None: @@ -1487,14 +1487,14 @@ def search_keypairs(self, name_or_id=None, filters=None): def search_networks(self, name_or_id=None, filters=None): """Search networks - :param name_or_id: Name or id of the desired network. + :param name_or_id: Name or ID of the desired network. :param filters: a dict containing additional filters to use. e.g. {'router:external': True} :returns: a list of ``munch.Munch`` containing the network description. :raises: ``OpenStackCloudException`` if something goes wrong during the - openstack API call. + OpenStack API call. """ networks = self.list_networks(filters) return _utils._filter_list(networks, name_or_id, filters) @@ -1502,14 +1502,14 @@ def search_networks(self, name_or_id=None, filters=None): def search_routers(self, name_or_id=None, filters=None): """Search routers - :param name_or_id: Name or id of the desired router. + :param name_or_id: Name or ID of the desired router. :param filters: a dict containing additional filters to use. e.g. {'admin_state_up': True} :returns: a list of ``munch.Munch`` containing the router description. :raises: ``OpenStackCloudException`` if something goes wrong during the - openstack API call. + OpenStack API call. """ routers = self.list_routers(filters) return _utils._filter_list(routers, name_or_id, filters) @@ -1517,14 +1517,14 @@ def search_routers(self, name_or_id=None, filters=None): def search_subnets(self, name_or_id=None, filters=None): """Search subnets - :param name_or_id: Name or id of the desired subnet. + :param name_or_id: Name or ID of the desired subnet. :param filters: a dict containing additional filters to use. e.g. {'enable_dhcp': True} :returns: a list of ``munch.Munch`` containing the subnet description. :raises: ``OpenStackCloudException`` if something goes wrong during the - openstack API call. + OpenStack API call. """ subnets = self.list_subnets(filters) return _utils._filter_list(subnets, name_or_id, filters) @@ -1532,14 +1532,14 @@ def search_subnets(self, name_or_id=None, filters=None): def search_ports(self, name_or_id=None, filters=None): """Search ports - :param name_or_id: Name or id of the desired port. + :param name_or_id: Name or ID of the desired port. :param filters: a dict containing additional filters to use. e.g. {'device_id': '2711c67a-b4a7-43dd-ace7-6187b791c3f0'} :returns: a list of ``munch.Munch`` containing the port description. :raises: ``OpenStackCloudException`` if something goes wrong during the - openstack API call. + OpenStack API call. """ # If port caching is enabled, do not push the filter down to # neutron; get all the ports (potentially from the cache) and @@ -1592,13 +1592,13 @@ def search_servers( def search_server_groups(self, name_or_id=None, filters=None): """Seach server groups. - :param name: server group name or id. + :param name: server group name or ID. :param filters: a dict containing additional filters to use. :returns: a list of dicts containing the server groups :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. + the OpenStack API call. """ server_groups = self.list_server_groups() return _utils._filter_list(server_groups, name_or_id, filters) @@ -1628,14 +1628,14 @@ def search_floating_ips(self, id=None, filters=None): def search_stacks(self, name_or_id=None, filters=None): """Search stacks. - :param name_or_id: Name or id of the desired stack. + :param name_or_id: Name or ID of the desired stack. :param filters: a dict containing additional filters to use. e.g. {'stack_status': 'CREATE_COMPLETE'} :returns: a list of ``munch.Munch`` containing the stack description. :raises: ``OpenStackCloudException`` if something goes wrong during the - openstack API call. + OpenStack API call. """ stacks = self.list_stacks() return _utils._filter_list(stacks, name_or_id, filters) @@ -1785,7 +1785,7 @@ def list_stacks(self): :returns: a list of ``munch.Munch`` containing the stack description. :raises: ``OpenStackCloudException`` if something goes wrong during the - openstack API call. + OpenStack API call. """ with _utils.shade_exceptions("Error fetching stack list"): stacks = self.manager.submit_task(_tasks.StackList()) @@ -1900,7 +1900,7 @@ def list_server_groups(self): def get_compute_limits(self, name_or_id=None): """ Get compute limits for a project - :param name_or_id: (optional) project name or id to get limits for + :param name_or_id: (optional) project name or ID to get limits for if different from the current project :raises: OpenStackCloudException if it's not a valid project @@ -2150,7 +2150,7 @@ def _set_interesting_networks(self): ' {nat_net} which is the network configured' ' to be the NAT destination. Please check your' ' cloud resources. It is probably a good idea' - ' to configure this network by id rather than' + ' to configure this network by ID rather than' ' by name.'.format( nat_net=self._nat_destination)) nat_destination = network @@ -2184,7 +2184,7 @@ def _set_interesting_networks(self): ' configured to be the default interface' ' network. Please check your cloud resources.' ' It is probably a good idea' - ' to configure this network by id rather than' + ' to configure this network by ID rather than' ' by name.'.format( default_net=self._default_network)) default_network = network @@ -2705,7 +2705,7 @@ def download_image( :raises: OpenStackCloudException in the event download_image is called without exactly one of either output_path or output_file :raises: OpenStackCloudResourceNotFound if no images are found matching - the name or id provided + the name or ID provided """ if output_path is None and output_file is None: raise OpenStackCloudException('No output specified, an output path' @@ -2719,7 +2719,7 @@ def download_image( image = self.search_images(name_or_id) if len(image) == 0: raise OpenStackCloudResourceNotFound( - "No images with name or id %s were found" % name_or_id, None) + "No images with name or ID %s were found" % name_or_id, None) if self.cloud_config.get_api_version('image') == '2': endpoint = '/images/{id}/file'.format(id=image[0]['id']) else: @@ -2765,14 +2765,14 @@ def get_floating_ip(self, id, filters=None): def get_stack(self, name_or_id, filters=None): """Get exactly one stack. - :param name_or_id: Name or id of the desired stack. + :param name_or_id: Name or ID of the desired stack. :param filters: a dict containing additional filters to use. e.g. {'stack_status': 'CREATE_COMPLETE'} :returns: a ``munch.Munch`` containing the stack description :raises: ``OpenStackCloudException`` if something goes wrong during the - openstack API call or if multiple matches are found. + OpenStack API call or if multiple matches are found. """ def _search_one_stack(name_or_id=None, filters=None): @@ -2933,7 +2933,7 @@ def add_router_interface(self, router, subnet_id=None, port_id=None): :param string subnet_id: The ID of the subnet to use for the interface :param string port_id: The ID of the port to use for the interface - :returns: A ``munch.Munch`` with the router id (id), + :returns: A ``munch.Munch`` with the router ID (ID), subnet ID (subnet_id), port ID (port_id) and tenant ID (tenant_id). :raises: OpenStackCloudException on operation error. @@ -3171,7 +3171,7 @@ def create_image_snapshot( """Create an image by snapshotting an existing server. :param name: Name of the image to be created - :param server: Server name or id or dict representing the server + :param server: Server name or ID or dict representing the server to be snapshotted :param wait: If true, waits for image to be created. :param timeout: Seconds to wait for image creation. None is forever. @@ -3714,7 +3714,7 @@ def create_volume( :param description: (optional) Name for the volume. :param wait: If true, waits for volume to be created. :param timeout: Seconds to wait for volume creation. None is forever. - :param image: (optional) Image name, id or object from which to create + :param image: (optional) Image name, ID or object from which to create the volume :param kwargs: Keyword arguments as expected for cinder client. @@ -3970,7 +3970,7 @@ def create_volume_snapshot(self, volume_id, force=False, wait=True, timeout=None, **kwargs): """Create a volume. - :param volume_id: the id of the volume to snapshot. + :param volume_id: the ID of the volume to snapshot. :param force: If set to True the snapshot will be created even if the volume is attached to an instance, if False it will not :param name: name of the snapshot, one will be generated if one is @@ -4018,7 +4018,7 @@ def create_volume_snapshot(self, volume_id, force=False, def get_volume_snapshot_by_id(self, snapshot_id): """Takes a snapshot_id and gets a dict of the snapshot - that maches that id. + that maches that ID. Note: This is more efficient than get_volume_snapshot. @@ -4065,7 +4065,7 @@ def create_volume_backup(self, volume_id, name=None, description=None, force=False, wait=True, timeout=None): """Create a volume backup. - :param volume_id: the id of the volume to backup. + :param volume_id: the ID of the volume to backup. :param name: name of the backup, one will be generated if one is not provided :param description: description of the backup, one will be generated @@ -4313,7 +4313,7 @@ def _neutron_available_floating_ips( Return a list of available floating IPs or allocate a new one and return it in a list of 1 element. - :param network: A single network name or id, or a list of them. + :param network: A single network name or ID, or a list of them. :param server: (server) Server the Floating IP is for :returns: a list of floating IP addresses. @@ -4420,10 +4420,10 @@ def create_floating_ip(self, network=None, server=None, the IP for and to which it should be attached. :param fixed_address: (optional) Fixed IP to attach the floating ip to. - :param nat_destination: (optional) Name or id of the network + :param nat_destination: (optional) Name or ID of the network that the fixed IP to attach the floating IP to should be on. - :param port: (optional) The port id that the floating IP should be + :param port: (optional) The port ID that the floating IP should be attached to. Specifying a port conflicts with specifying a server, fixed_address or nat_destination. @@ -4483,7 +4483,7 @@ def _neutron_create_floating_ip( network = self.get_network(network_name_or_id) if not network: raise OpenStackCloudResourceNotFound( - "unable to find network for floating ips with id " + "unable to find network for floating ips with ID " "{0}".format(network_name_or_id)) network_id = network['id'] else: @@ -4564,7 +4564,7 @@ def _nova_create_floating_ip(self, pool=None): def delete_floating_ip(self, floating_ip_id, retry=1): """Deallocate a floating IP from a project. - :param floating_ip_id: a floating IP address id. + :param floating_ip_id: a floating IP address ID. :param retry: number of times to retry. Optional, defaults to 1, which is in addition to the initial delete call. A value of 0 will also cause no checking of results to @@ -4594,7 +4594,7 @@ def delete_floating_ip(self, floating_ip_id, retry=1): return True raise OpenStackCloudException( - "Attempted to delete Floating IP {ip} with id {id} a total of" + "Attempted to delete Floating IP {ip} with ID {id} a total of" " {retry} times. Although the cloud did not indicate any errors" " the floating ip is still in existence. Aborting further" " operations.".format( @@ -4620,7 +4620,7 @@ def _neutron_delete_floating_ip(self, floating_ip_id): return False except Exception as e: raise OpenStackCloudException( - "Unable to delete floating IP id {fip_id}: {msg}".format( + "Unable to delete floating IP ID {fip_id}: {msg}".format( fip_id=floating_ip_id, msg=str(e))) return True @@ -4634,7 +4634,7 @@ def _nova_delete_floating_ip(self, floating_ip_id): raise except Exception as e: raise OpenStackCloudException( - "Unable to delete floating IP id {fip_id}: {msg}".format( + "Unable to delete floating IP ID {fip_id}: {msg}".format( fip_id=floating_ip_id, msg=str(e))) return True @@ -4868,7 +4868,7 @@ def _nova_attach_ip_to_server(self, server_id, floating_ip_id, def detach_ip_from_server(self, server_id, floating_ip_id): """Detach a floating IP from a server. - :param server_id: id of a server. + :param server_id: ID of a server. :param floating_ip_id: Id of the floating IP to detach. :returns: True if the IP has been detached, or False if the IP wasn't @@ -5232,16 +5232,16 @@ def create_server( """Create a virtual server instance. :param name: Something to name the server. - :param image: Image dict, name or id to boot with. - :param flavor: Flavor dict, name or id to boot onto. + :param image: Image dict, name or ID to boot with. + :param flavor: Flavor dict, name or ID to boot onto. :param auto_ip: Whether to take actions to find a routable IP for the server. (defaults to True) :param ips: List of IPs to attach to the server (defaults to None) :param ip_pool: Name of the network or floating IP pool to get an address from. (defaults to None) - :param root_volume: Name or id of a volume to boot from + :param root_volume: Name or ID of a volume to boot from (defaults to None - deprecated, use boot_volume) - :param boot_volume: Name or id of a volume to boot from + :param boot_volume: Name or ID of a volume to boot from (defaults to None) :param terminate_volume: If booting from a volume, whether it should be deleted when the server is destroyed. @@ -5292,9 +5292,9 @@ def create_server( :param reuse_ips: (optional) Whether to attempt to reuse pre-existing floating ips should a floating IP be needed (defaults to True) - :param network: (optional) Network dict or name or id to attach the + :param network: (optional) Network dict or name or ID to attach the server to. Mutually exclusive with the nics parameter. - Can also be be a list of network names or ids or + Can also be be a list of network names or IDs or network dicts. :param boot_from_volume: Whether to boot from volume. 'boot_volume' implies True, but boot_from_volume=True with @@ -5508,7 +5508,7 @@ def rebuild_server(self, server_id, image_id, admin_pass=None, def set_server_metadata(self, name_or_id, metadata): """Set metadata in a server instance. - :param str name_or_id: The name or id of the server instance + :param str name_or_id: The name or ID of the server instance to update. :param dict metadata: A dictionary with the key=value pairs to set in the server instance. It only updates the key=value @@ -5529,7 +5529,7 @@ def set_server_metadata(self, name_or_id, metadata): def delete_server_metadata(self, name_or_id, metadata_keys): """Delete metadata from a server instance. - :param str name_or_id: The name or id of the server instance + :param str name_or_id: The name or ID of the server instance to update. :param list metadata_keys: A list with the keys to be deleted from the server instance. @@ -5690,7 +5690,7 @@ def create_server_group(self, name, policies): def delete_server_group(self, name_or_id): """Delete a server group. - :param name_or_id: Name or id of the server group to delete + :param name_or_id: Name or ID of the server group to delete :returns: True if delete succeeded, False otherwise @@ -6517,7 +6517,7 @@ def update_port(self, name_or_id, **kwargs): Note: to unset an attribute use None value. To leave an attribute untouched just omit it. - :param name_or_id: name or id of the port to update. (Required) + :param name_or_id: name or ID of the port to update. (Required) :param name: A symbolic name for the port. (Optional) :param admin_state_up: The administrative status of the port, which is up (true) or down (false). (Optional) @@ -6574,7 +6574,7 @@ def update_port(self, name_or_id, **kwargs): def delete_port(self, name_or_id): """Delete a port - :param name_or_id: id or name of the port to delete. + :param name_or_id: ID or name of the port to delete. :returns: True if delete succeeded, False otherwise. @@ -7005,7 +7005,7 @@ def delete_zone(self, name_or_id): def list_recordsets(self, zone): """List all available recordsets. - :param zone: Name or id of the zone managing the recordset + :param zone: Name or ID of the zone managing the recordset :returns: A list of recordsets. @@ -7067,7 +7067,7 @@ def create_recordset(self, zone, name, recordset_type, records, def update_recordset(self, zone, name_or_id, **kwargs): """Update a recordset. - :param zone: Name or id of the zone managing the recordset + :param zone: Name or ID of the zone managing the recordset :param name_or_id: Name or ID of the recordset being updated. :param records: List of the recordset definitions :param description: Description of the recordset @@ -7135,7 +7135,7 @@ def list_cluster_templates(self, detail=False): :returns: a list of dicts containing the cluster template details. :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. + the OpenStack API call. """ with _utils.shade_exceptions("Error fetching ClusterTemplate list"): cluster_templates = self.manager.submit_task( @@ -7157,7 +7157,7 @@ def search_cluster_templates( :returns: a list of dict containing the ClusterTemplates :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. + the OpenStack API call. """ cluster_templates = self.list_cluster_templates(detail=detail) return _utils._filter_list( @@ -7207,7 +7207,7 @@ def create_cluster_template( :returns: a dict containing the ClusterTemplate description :raises: ``OpenStackCloudException`` if something goes wrong during - the openstack API call + the OpenStack API call """ with _utils.shade_exceptions( "Error creating ClusterTemplate of name" From 708049351fcf13634d84e05e97eae3b0a5f0972c Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Wed, 1 Mar 2017 15:54:16 -0300 Subject: [PATCH 1324/3836] Rename ClusterTemplate in OpenStackCloud docs There is no ClusterTemplate object in shade. A cluster template is a resource in shade as any other, such as a project or a server, which is returned as a dict. This patch renames "ClusterTemplate" to "cluster template" in order to be consistent with how shade refers to the rest of the resources in the OpenStackCloud docs. In addition, it is not useful for shade users to know ClusterTemplate is the new name for BayModel. If shade had ever supported a BayModel resource, it would still support it. Change-Id: Icbf4b310c5bbfb912c902f66974e11d8cefe1e23 --- shade/openstackcloud.py | 62 +++++++++------------- shade/tests/unit/test_cluster_templates.py | 2 +- 2 files changed, 26 insertions(+), 38 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index c93e96987..c69f21843 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -7125,9 +7125,7 @@ def delete_recordset(self, zone, name_or_id): @_utils.cache_on_arguments() def list_cluster_templates(self, detail=False): - """List ClusterTemplates. - - ClusterTemplate is the new name for BayModel. + """List cluster templates. :param bool detail. Flag to control if we need summarized or detailed output. @@ -7137,7 +7135,7 @@ def list_cluster_templates(self, detail=False): :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ - with _utils.shade_exceptions("Error fetching ClusterTemplate list"): + with _utils.shade_exceptions("Error fetching cluster template list"): cluster_templates = self.manager.submit_task( _tasks.ClusterTemplateList(detail=detail)) return _utils.normalize_cluster_templates(cluster_templates) @@ -7145,16 +7143,14 @@ def list_cluster_templates(self, detail=False): def search_cluster_templates( self, name_or_id=None, filters=None, detail=False): - """Search ClusterTemplates. - - ClusterTemplate is the new name for BayModel. + """Search cluster templates. - :param name_or_id: ClusterTemplate name or ID. + :param name_or_id: cluster template name or ID. :param filters: a dict containing additional filters to use. :param detail: a boolean to control if we need summarized or detailed output. - :returns: a list of dict containing the ClusterTemplates + :returns: a list of dict containing the cluster templates :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. @@ -7165,11 +7161,9 @@ def search_cluster_templates( search_baymodels = search_cluster_templates def get_cluster_template(self, name_or_id, filters=None, detail=False): - """Get a ClusterTemplate by name or ID. - - ClusterTemplate is the new name for BayModel. + """Get a cluster template by name or ID. - :param name_or_id: Name or ID of the ClusterTemplate. + :param name_or_id: Name or ID of the cluster template. :param filters: A dictionary of meta data to use for further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: @@ -7184,8 +7178,8 @@ def get_cluster_template(self, name_or_id, filters=None, detail=False): A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - :returns: A ClusterTemplate dict or None if no matching - ClusterTemplate is found. + :returns: A cluster template dict or None if no matching + cluster template is found. """ return _utils._get_entity(self.search_cluster_templates, name_or_id, filters=filters, detail=detail) @@ -7193,24 +7187,22 @@ def get_cluster_template(self, name_or_id, filters=None, detail=False): def create_cluster_template( self, name, image_id=None, keypair_id=None, coe=None, **kwargs): - """Create a ClusterTemplate. + """Create a cluster template. - ClusterTemplate is the new name for BayModel. - - :param string name: Name of the ClusterTemplate. + :param string name: Name of the cluster template. :param string image_id: Name or ID of the image to use. :param string keypair_id: Name or ID of the keypair to use. - :param string coe: Name of the coe for the ClusterTemplate. + :param string coe: Name of the coe for the cluster template. Other arguments will be passed in kwargs. - :returns: a dict containing the ClusterTemplate description + :returns: a dict containing the cluster template description :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call """ with _utils.shade_exceptions( - "Error creating ClusterTemplate of name" + "Error creating cluster template of name" " {cluster_template_name}".format( cluster_template_name=name)): cluster_template = self.manager.submit_task( @@ -7223,13 +7215,11 @@ def create_cluster_template( create_baymodel = create_cluster_template def delete_cluster_template(self, name_or_id): - """Delete a ClusterTemplate. - - ClusterTemplate is the new name for BayModel. + """Delete a cluster template. - :param name_or_id: Name or unique ID of the ClusterTemplate. + :param name_or_id: Name or unique ID of the cluster template. :returns: True if the delete succeeded, False if the - ClusterTemplate was not found. + cluster template was not found. :raises: OpenStackCloudException on operation error. """ @@ -7239,18 +7229,18 @@ def delete_cluster_template(self, name_or_id): if not cluster_template: self.log.debug( - "ClusterTemplate %(name_or_id)s does not exist", + "Cluster template %(name_or_id)s does not exist", {'name_or_id': name_or_id}, exc_info=True) return False - with _utils.shade_exceptions("Error in deleting ClusterTemplate"): + with _utils.shade_exceptions("Error in deleting cluster template"): try: self.manager.submit_task( _tasks.ClusterTemplateDelete(id=cluster_template['id'])) except magnum_exceptions.NotFound: self.log.debug( - "ClusterTemplate %(id)s not found when deleting." + "Cluster template %(id)s not found when deleting." " Ignoring.", {'id': cluster_template['id']}) return False @@ -7265,16 +7255,14 @@ def delete_cluster_template(self, name_or_id): 'network_driver', 'tls_disabled', 'public', 'registry_enabled', 'volume_driver') def update_cluster_template(self, name_or_id, operation, **kwargs): - """Update a ClusterTemplate. - - ClusterTemplate is the new name for BayModel. + """Update a cluster template. - :param name_or_id: Name or ID of the ClusterTemplate being updated. + :param name_or_id: Name or ID of the cluster template being updated. :param operation: Operation to perform - add, remove, replace. Other arguments will be passed with kwargs. - :returns: a dict representing the updated ClusterTemplate. + :returns: a dict representing the updated cluster template. :raises: OpenStackCloudException on operation error. """ @@ -7282,7 +7270,7 @@ def update_cluster_template(self, name_or_id, operation, **kwargs): cluster_template = self.get_cluster_template(name_or_id) if not cluster_template: raise OpenStackCloudException( - "ClusterTemplate %s not found." % name_or_id) + "Cluster template %s not found." % name_or_id) if operation not in ['add', 'replace', 'remove']: raise TypeError( @@ -7291,7 +7279,7 @@ def update_cluster_template(self, name_or_id, operation, **kwargs): patches = _utils.generate_patches_from_kwargs(operation, **kwargs) with _utils.shade_exceptions( - "Error updating ClusterTemplate {0}".format(name_or_id)): + "Error updating cluster template {0}".format(name_or_id)): self.manager.submit_task( _tasks.ClusterTemplateUpdate( id=cluster_template['id'], patch=patches)) diff --git a/shade/tests/unit/test_cluster_templates.py b/shade/tests/unit/test_cluster_templates.py index e5f11847c..ff52556b5 100644 --- a/shade/tests/unit/test_cluster_templates.py +++ b/shade/tests/unit/test_cluster_templates.py @@ -141,7 +141,7 @@ def test_create_cluster_template_exception(self, mock_magnum): mock_magnum.baymodels.create.side_effect = Exception() with testtools.ExpectedException( shade.OpenStackCloudException, - "Error creating ClusterTemplate of name fake-cluster-template" + "Error creating cluster template of name fake-cluster-template" ): self.cloud.create_cluster_template('fake-cluster-template') From 292e297e49479aa3eeeb15c72048b31896f43d16 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Wed, 1 Mar 2017 15:31:30 -0500 Subject: [PATCH 1325/3836] Update devstack config to point to a valid image The devstack config we link to in the docs has been out of date for quite some time. I usually just comment out the IMAGE_URLS line so it doesn't even bother, but we should actually update it. The change falls in line with what the Heat docs recommend, and it's because of Heat that we have this configuration line in the first place. See https://docs.openstack.org/developer/heat/getting_started/on_devstack.html for their recommended config. Change-Id: I3f55c576f0e8aefa776e40509f8b7b098665ff39 --- doc/source/contributors/local.conf | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/source/contributors/local.conf b/doc/source/contributors/local.conf index d47116217..4aaa56b09 100644 --- a/doc/source/contributors/local.conf +++ b/doc/source/contributors/local.conf @@ -58,7 +58,10 @@ enable_service h-api-cw # Automatically download and register a VM image that Heat can launch # For more information on Heat and DevStack see # http://docs.openstack.org/developer/heat/getting_started/on_devstack.html -IMAGE_URLS+=",http://cloud.fedoraproject.org/fedora-20.x86_64.qcow2" +IMAGE_URL_SITE="http://download.fedoraproject.org" +IMAGE_URL_PATH="/pub/fedora/linux/releases/25/CloudImages/x86_64/images/" +IMAGE_URL_FILE="Fedora-Cloud-Base-25-1.3.x86_64.qcow2" +IMAGE_URLS+=","$IMAGE_URL_SITE$IMAGE_URL_PATH$IMAGE_URL_FILE # Logging LOGDAYS=1 From 71128e7212bfd87fc468268ab7d09862c912bcb4 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 2 Mar 2017 11:55:11 +0000 Subject: [PATCH 1326/3836] Updated from global requirements Change-Id: I75c7591ea4dd4007e578d76d209fd6de10587f76 --- requirements.txt | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index f51c90d02..dd39ef67f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ # 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>=1.8 # Apache-2.0 +pbr>=2.0.0 # Apache-2.0 six>=1.9.0 # MIT -stevedore>=1.17.1 # Apache-2.0 +stevedore>=1.20.0 # Apache-2.0 os-client-config>=1.22.0 # Apache-2.0 keystoneauth1>=2.18.0 # Apache-2.0 deprecation>=1.0 # Apache-2.0 diff --git a/setup.py b/setup.py index 782bb21f0..566d84432 100644 --- a/setup.py +++ b/setup.py @@ -25,5 +25,5 @@ pass setuptools.setup( - setup_requires=['pbr>=1.8'], + setup_requires=['pbr>=2.0.0'], pbr=True) From 40c416cbad77d74d7d8fb964193b309d77ecb628 Mon Sep 17 00:00:00 2001 From: ricolin Date: Thu, 2 Mar 2017 22:26:45 +0800 Subject: [PATCH 1327/3836] [Fix gate]Update test requirement Since pbr already landed and the old version of hacking seems not work very well with pbr>=2, we should update it to match global requirement. Partial-Bug: #1668848 Change-Id: I09ae994782889aae05250a8e5bf9f5b630b2d502 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index f9908d6e0..6208fb511 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,7 +2,7 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. -hacking>=0.10.2,<0.11 # Apache-2.0 +hacking>=0.12.0,!=0.13.0,<0.14 # Apache-2.0 coverage>=3.6 docutils>=0.11,!=0.13.1 # OSI-Approved Open Source, Public Domain From 5b2df7e724e53fe7834868850db601eb187e26e9 Mon Sep 17 00:00:00 2001 From: Lars Kellogg-Stedman Date: Thu, 23 Feb 2017 14:50:42 -0500 Subject: [PATCH 1328/3836] Enable streaming responses in download_image Previously, the openstack.image.image_download method would place the contents of a remote image into a Python variable. With this change, the download_image method can optionally return the requests.Response object returned by session.get(), which permits the caller to download the image in chunks using the iter_content method. This can prevent performance issues when dealing with large images. Change-Id: Ie62ebcc895ca893321a10def18ac5d74c7c843b9 --- doc/source/conf.py | 3 +- doc/source/users/guides/image.rst | 35 +++++++++++++++++++++ openstack/image/v2/_proxy.py | 30 ++++++++++++++++-- openstack/image/v2/image.py | 12 +++++-- openstack/tests/unit/image/v2/test_image.py | 24 ++++++++++++-- 5 files changed, 95 insertions(+), 9 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 66c9702e6..c12c69e10 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -119,7 +119,8 @@ ] # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/3/': None} +intersphinx_mapping = {'https://docs.python.org/3/': None, + 'http://docs.python-requests.org/en/master/': None} # Include both the class and __init__ docstrings when describing the class autoclass_content = "both" diff --git a/doc/source/users/guides/image.rst b/doc/source/users/guides/image.rst index 322628933..6b78a06e5 100644 --- a/doc/source/users/guides/image.rst +++ b/doc/source/users/guides/image.rst @@ -32,6 +32,40 @@ Create an image by uploading its data and setting its attributes. Full example: `image resource create`_ +.. _download_image-stream-true: + +Downloading an Image with stream=True +------------------------------------- + +As images are often very large pieces of data, storing their entire contents +in the memory of your application can be less than desirable. A more +efficient method may be to iterate over a stream of the response data. + +By choosing to stream the response content, you determine the ``chunk_size`` +that is appropriate for your needs, meaning only that many bytes of data are +read for each iteration of the loop until all data has been consumed. +See :meth:`requests.Response.iter_content` for more information, as well +as Requests' :ref:`body-content-workflow`. + +When you choose to stream an image download, openstacksdk is no longer +able to compute the checksum of the response data for you. This example +shows how you might do that yourself, in a very similar manner to how +the library calculates checksums for non-streamed responses. + +.. literalinclude:: ../examples/image/download.py + :pyobject: download_image_stream + +Downloading an Image with stream=False +-------------------------------------- + +If you wish to download an image's contents all at once and to memory, +simply set ``stream=False``, which is the default. + +.. literalinclude:: ../examples/image/download.py + :pyobject: download_image + +Full example: `image resource download`_ + Delete Image ------------ @@ -45,3 +79,4 @@ Full example: `image resource delete`_ .. _image resource create: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/image/create.py .. _image resource delete: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/image/delete.py .. _image resource list: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/image/list.py +.. _image resource download: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/image/download.py diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 410d2f13d..c886b305f 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -60,16 +60,40 @@ def upload_image(self, container_format=None, disk_format=None, return img - def download_image(self, image): + def download_image(self, image, stream=False): """Download an image + This will download an image to memory when ``stream=False``, or allow + streaming downloads using an iterator when ``stream=True``. + For examples of working with streamed responses, see + :ref:`download_image-stream-true` and the Requests documentation + :ref:`body-content-workflow`. + :param image: The value can be either the ID of an image or a :class:`~openstack.image.v2.image.Image` instance. - :returns: The bytes comprising the given Image. + :param bool stream: When ``True``, return a :class:`requests.Response` + instance allowing you to iterate over the + response data stream instead of storing its entire + contents in memory. See + :meth:`requests.Response.iter_content` for more + details. *NOTE*: If you do not consume + the entirety of the response you must explicitly + call :meth:`requests.Response.close` or otherwise + risk inefficiencies with the ``requests`` + library's handling of connections. + + + When ``False``, return the entire + contents of the response. + + :returns: The bytes comprising the given Image when stream is + False, otherwise a :class:`requests.Response` + instance. """ + image = self._get_resource(_image.Image, image) - return image.download(self._session) + return image.download(self._session, stream=stream) def delete_image(self, image, ignore_missing=True): """Delete an image diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index 1cd450afb..d3298c0d0 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -246,12 +246,12 @@ def upload(self, session): headers={"Content-Type": "application/octet-stream", "Accept": ""}) - def download(self, session): + def download(self, session, stream=False): """Download the data contained in an image""" # TODO(briancurtin): This method should probably offload the get # operation into another thread or something of that nature. url = utils.urljoin(self.base_path, self.id, 'file') - resp = session.get(url, endpoint_filter=self.service) + resp = session.get(url, endpoint_filter=self.service, stream=stream) # See the following bug report for details on why the checksum # code may sometimes depend on a second GET call. @@ -265,6 +265,14 @@ def download(self, session): details = self.get(session) checksum = details.checksum + # if we are returning the repsonse object, ensure that it + # has the content-md5 header so that the caller doesn't + # need to jump through the same hoops through which we + # just jumped. + if stream: + resp.headers['content-md5'] = checksum + return resp + if checksum is not None: digest = hashlib.md5(resp.content).hexdigest() if digest != checksum: diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 1e3d674c6..bebb4c535 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -212,7 +212,8 @@ def test_download_checksum_match(self): rv = sot.download(self.sess) self.sess.get.assert_called_with('images/IDENTIFIER/file', - endpoint_filter=sot.service) + endpoint_filter=sot.service, + stream=False) self.assertEqual(rv, resp.content) @@ -242,7 +243,8 @@ def test_download_no_checksum_header(self): rv = sot.download(self.sess) self.sess.get.assert_has_calls( - [mock.call('images/IDENTIFIER/file', endpoint_filter=sot.service), + [mock.call('images/IDENTIFIER/file', endpoint_filter=sot.service, + stream=False), mock.call('images/IDENTIFIER', endpoint_filter=sot.service)]) self.assertEqual(rv, resp1.content) @@ -270,7 +272,23 @@ def test_download_no_checksum_at_all2(self): log.records[0].msg) self.sess.get.assert_has_calls( - [mock.call('images/IDENTIFIER/file', endpoint_filter=sot.service), + [mock.call('images/IDENTIFIER/file', endpoint_filter=sot.service, + stream=False), mock.call('images/IDENTIFIER', endpoint_filter=sot.service)]) self.assertEqual(rv, resp1.content) + + def test_download_stream(self): + sot = image.Image(**EXAMPLE) + + resp = mock.Mock() + resp.content = b"abc" + resp.headers = {"Content-MD5": "900150983cd24fb0d6963f7d28e17f72"} + self.sess.get.return_value = resp + + rv = sot.download(self.sess, stream=True) + self.sess.get.assert_called_with('images/IDENTIFIER/file', + endpoint_filter=sot.service, + stream=True) + + self.assertEqual(rv, resp) From 0ecbb22547c470fca6d50cfad2ee94e1241f83f4 Mon Sep 17 00:00:00 2001 From: Ian Wienand Date: Fri, 3 Mar 2017 14:06:34 +1100 Subject: [PATCH 1329/3836] Don't glob match name_or_id I was trying to bring up a host on vexxhost using the image name "Ubuntu 16.04.1 LTS [2017-03-02]". As described in the change, because this gets run through fnmatch() directly it makes everything barf as "[2017-03-02]" gets matched as an incorrect glob character class. This is a little tricky because all the external list_*, search_*, get_* API functions basically pass name_or_id through directly. None of them have ever explicitly said that you could use globbing, although it probably would have worked. Any such use would have been relying on implementation details of the _util functions, and I feel like the fact it's _underscore_utils means that you probably had fair warning not to do that. And obviously the "filters" argument is meant for this type of filtering. Additionally, fnmatch() says that it normalises case depending on OS filename case-sensitivity, which seems like incorrect behaviour for these functions which are not actually related to filenames. Thus I believe the matching should be just be a straight string comparison. I have added a test for this, but we also have to remove the tests for globbing behaviour as they are incorrect now. Change-Id: Id89bbf6306adebe96c1db417a4b8b5ae51023af0 --- shade/_utils.py | 9 +++------ shade/tests/unit/test__utils.py | 19 +++++-------------- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/shade/_utils.py b/shade/_utils.py index b3da5b5c9..cbb9b7a13 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -13,7 +13,6 @@ # limitations under the License. import contextlib -import fnmatch import inspect import jmespath import munch @@ -84,8 +83,7 @@ def _filter_list(data, name_or_id, filters): each dictionary contains an 'id' and 'name' key if a value for name_or_id is given. :param string name_or_id: - The name or ID of the entity being filtered. Can be a glob pattern, - such as 'nb01*'. + The name or ID of the entity being filtered. :param filters: A dictionary of meta data to use for further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: @@ -104,9 +102,8 @@ def _filter_list(data, name_or_id, filters): for e in data: e_id = e.get('id', None) e_name = e.get('name', None) - if ((e_id and fnmatch.fnmatch(str(e_id), str(name_or_id))) or - (e_name and fnmatch.fnmatch( - str(e_name), str(name_or_id)))): + if ((e_id and str(e_id) == str(name_or_id)) or + (e_name and str(e_name) == str(name_or_id))): identifier_matches.append(e) data = identifier_matches diff --git a/shade/tests/unit/test__utils.py b/shade/tests/unit/test__utils.py index 28235e8dd..6e611d73b 100644 --- a/shade/tests/unit/test__utils.py +++ b/shade/tests/unit/test__utils.py @@ -40,21 +40,12 @@ def test__filter_list_name_or_id(self): ret = _utils._filter_list(data, 'donald', None) self.assertEqual([el1], ret) - def test__filter_list_name_or_id_glob(self): + def test__filter_list_name_or_id_special(self): el1 = dict(id=100, name='donald') - el2 = dict(id=200, name='pluto') - el3 = dict(id=200, name='pluto-2') - data = [el1, el2, el3] - ret = _utils._filter_list(data, 'pluto*', None) - self.assertEqual([el2, el3], ret) - - def test__filter_list_name_or_id_glob_not_found(self): - el1 = dict(id=100, name='donald') - el2 = dict(id=200, name='pluto') - el3 = dict(id=200, name='pluto-2') - data = [el1, el2, el3] - ret = _utils._filter_list(data, 'q*', None) - self.assertEqual([], ret) + el2 = dict(id=200, name='pluto[2017-01-10]') + data = [el1, el2] + ret = _utils._filter_list(data, 'pluto[2017-01-10]', None) + self.assertEqual([el2], ret) def test__filter_list_filter(self): el1 = dict(id=100, name='donald', other='duck') From 127049fe53ee93fa64472571f8cd0f860c79f701 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 3 Mar 2017 08:18:20 -0800 Subject: [PATCH 1330/3836] Raise a more specific exception on nova 400 errors We may want to take some actions in nodepool, such as cache invalidation, when we specifically get BadRequest errors. We may not, but throwing the more specific exception doesn't hurt. Change-Id: I748519daba2781bf83d6dc0151a1c12dd12d0e3f --- shade/_utils.py | 5 +++++ shade/exc.py | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/shade/_utils.py b/shade/_utils.py index b3da5b5c9..abda4706a 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -26,6 +26,7 @@ from decorator import decorator from heatclient import exc as heat_exc from neutronclient.common import exceptions as neutron_exc +from novaclient import exceptions as nova_exc from shade import _log from shade import exc @@ -434,6 +435,10 @@ def shade_exceptions(error_message=None): yield except exc.OpenStackCloudException: raise + except nova_exc.BadRequest as e: + if error_message is None: + error_message = str(e) + raise exc.OpenStackCloudBadRequest(error_message) except Exception as e: if error_message is None: error_message = str(e) diff --git a/shade/exc.py b/shade/exc.py index 3ce23b901..fbb67564e 100644 --- a/shade/exc.py +++ b/shade/exc.py @@ -73,6 +73,14 @@ def __init__(self, *args, **kwargs): _rex.HTTPError.__init__(self, *args, **kwargs) +class OpenStackCloudBadRequest(OpenStackCloudHTTPError): + """There is something wrong with the request payload. + + Possible reasons can include malformed json or invalid values to parameters + such as flavorRef to a server create. + """ + + class OpenStackCloudURINotFound(OpenStackCloudHTTPError): pass @@ -98,5 +106,7 @@ def raise_from_response(response): # before if response.status_code == 404: raise OpenStackCloudURINotFound(msg, response=response) + elif response.status_code == 400: + raise OpenStackCloudBadRequest(msg, response=response) if msg: raise OpenStackCloudHTTPError(msg, response=response) From 7883171eca469ab0314369d11c1e401e284ec66e Mon Sep 17 00:00:00 2001 From: Ian Wienand Date: Fri, 3 Mar 2017 15:10:31 +1100 Subject: [PATCH 1331/3836] Use unicode match for name_or_id The name_or_id field might be a unicode string. Make sure we don't run into problems comparing it by converting all comparison objects. This is a follow-on to Id89bbf6306adebe96c1db417a4b8b5ae51023af0 based on/as an alternative to I546b1fbd3375861173f411a7cfdb9e1baa5e6250 Closes-Bug: 1668849 Change-Id: I4e7fa49bd8525148961f02b0e0d7b567145a68b6 Co-Authored-By: Liu Qing --- shade/_utils.py | 29 +++++++++++++++++++++++++---- shade/tests/unit/test__utils.py | 17 +++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/shade/_utils.py b/shade/_utils.py index cbb9b7a13..38467785c 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -75,6 +75,25 @@ def _iterate_timeout(timeout, message, wait=2): raise exc.OpenStackCloudTimeout(message) +def _make_unicode(input): + """Turn an input into unicode unconditionally + + :param input: + A unicode, string or other object + """ + try: + if isinstance(input, unicode): + return input + if isinstance(input, str): + return input.decode('utf-8') + else: + # int, for example + return unicode(input) + except NameError: + # python3! + return str(input) + + def _filter_list(data, name_or_id, filters): """Filter a list by name/ID and arbitrary meta data. @@ -98,12 +117,14 @@ def _filter_list(data, name_or_id, filters): A string containing a jmespath expression for further filtering. """ if name_or_id: + # name_or_id might already be unicode + name_or_id = _make_unicode(name_or_id) identifier_matches = [] for e in data: - e_id = e.get('id', None) - e_name = e.get('name', None) - if ((e_id and str(e_id) == str(name_or_id)) or - (e_name and str(e_name) == str(name_or_id))): + e_id = _make_unicode(e.get('id', None)) + e_name = _make_unicode(e.get('name', None)) + if ((e_id and e_id == name_or_id) or + (e_name and e_name == name_or_id)): identifier_matches.append(e) data = identifier_matches diff --git a/shade/tests/unit/test__utils.py b/shade/tests/unit/test__utils.py index 6e611d73b..4b178e2fb 100644 --- a/shade/tests/unit/test__utils.py +++ b/shade/tests/unit/test__utils.py @@ -1,3 +1,5 @@ +# -*- 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 @@ -47,6 +49,21 @@ def test__filter_list_name_or_id_special(self): ret = _utils._filter_list(data, 'pluto[2017-01-10]', None) self.assertEqual([el2], ret) + def test__filter_list_unicode(self): + el1 = dict(id=100, name=u'中文', last='duck', + other=dict(category='duck', financial=dict(status='poor'))) + el2 = dict(id=200, name=u'中文', last='trump', + other=dict(category='human', financial=dict(status='rich'))) + el3 = dict(id=300, name='donald', last='ronald mac', + other=dict(category='clown', financial=dict(status='rich'))) + data = [el1, el2, el3] + ret = _utils._filter_list( + data, u'中文', + {'other': { + 'financial': {'status': 'rich'} + }}) + self.assertEqual([el2], ret) + def test__filter_list_filter(self): el1 = dict(id=100, name='donald', other='duck') el2 = dict(id=200, name='donald', other='trump') From c58b86838c00bbc53461a37576dc91d23508da4c Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Wed, 8 Mar 2017 12:37:33 -0800 Subject: [PATCH 1332/3836] Replace keystone_client mock in test_groups Replace keystone_client direct mocks with requests_mock in test_groups. Change-Id: I31fa69950488292d8765908fc193c288e9c18b20 --- shade/tests/unit/base.py | 28 ++++++- shade/tests/unit/test_domains.py | 32 -------- shade/tests/unit/test_groups.py | 128 +++++++++++++++++++++---------- 3 files changed, 112 insertions(+), 76 deletions(-) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 242e44ef6..c47eaf352 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -43,7 +43,13 @@ _GroupData = collections.namedtuple( 'GroupData', - 'group_id, group_name, domain_id, description, json_response') + 'group_id, group_name, domain_id, description, json_response, ' + 'json_request') + +_DomainData = collections.namedtuple( + 'DomainData', + 'domain_id, domain_name, description, json_response, ' + 'json_request') class BaseTestCase(base.TestCase): @@ -233,11 +239,13 @@ def _get_group_data(self, name=None, domain_id=None, description=None): name or self.getUniqueString('groupname') domain_id = uuid.UUID(domain_id or uuid.uuid4().hex).hex response = {'id': group_id, 'name': name, 'domain_id': domain_id} + request = {'name': name} if description is not None: response['description'] = description + request['description'] = description return _GroupData(group_id, name, domain_id, description, - {'group': response}) + {'group': response}, {'group': request}) def _get_user_data(self, name=None, password=None, **kwargs): @@ -270,6 +278,22 @@ def _get_user_data(self, name=None, password=None, **kwargs): response.get('enabled'), {'user': response}, {'user': request}) + def _get_domain_data(self, domain_name=None, description=None, + enabled=None): + domain_id = uuid.uuid4().hex + domain_name = domain_name or self.getUniqueString('domainName') + response = {'id': domain_id, 'name': domain_name} + request = {'name': domain_name} + if enabled is not None: + request['enabled'] = bool(enabled) + response['enabled'] = bool(enabled) + if description: + response['description'] = description + request['description'] = description + response.setdefault('enabled', True) + return _DomainData(domain_id, domain_name, description, + {'domain': response}, {'domain': request}) + def use_keystone_v3(self): self.adapter = self.useFixture(rm_fixture.Fixture()) self.calls = [] diff --git a/shade/tests/unit/test_domains.py b/shade/tests/unit/test_domains.py index eec700b2c..c63f045dd 100644 --- a/shade/tests/unit/test_domains.py +++ b/shade/tests/unit/test_domains.py @@ -13,7 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import collections import uuid import testtools @@ -21,21 +20,6 @@ import shade from shade.tests.unit import base -from shade.tests import fakes - - -domain_obj = fakes.FakeDomain( - id='1', - name='a-domain', - description='A wonderful keystone domain', - enabled=True, -) - - -_DomainData = collections.namedtuple( - 'DomainData', - 'domain_id, domain_name, description, json_response, ' - 'json_request') class TestDomains(base.RequestsMockTestCase): @@ -47,22 +31,6 @@ def get_mock_url(self, service_type='identity', service_type=service_type, interface=interface, resource=resource, append=append, base_url_append=base_url_append) - def _get_domain_data(self, domain_name=None, description=None, - enabled=None): - domain_id = uuid.uuid4().hex - domain_name = domain_name or self.getUniqueString('domainName') - response = {'id': domain_id, 'name': domain_name} - request = {'name': domain_name} - if enabled is not None: - request['enabled'] = bool(enabled) - response['enabled'] = bool(enabled) - if description: - response['description'] = description - request['description'] = description - response.setdefault('enabled', True) - return _DomainData(domain_id, domain_name, description, - {'domain': response}, {'domain': request}) - def test_list_domains(self): self._add_discovery_uri_call() domain_data = self._get_domain_data() diff --git a/shade/tests/unit/test_groups.py b/shade/tests/unit/test_groups.py index 7acd23655..80b5aa2af 100644 --- a/shade/tests/unit/test_groups.py +++ b/shade/tests/unit/test_groups.py @@ -11,50 +11,94 @@ # See the License for the specific language governing permissions and # limitations under the License. -import mock - -import shade from shade.tests.unit import base -from shade.tests import fakes -class TestGroups(base.TestCase): +class TestGroups(base.RequestsMockTestCase): + def setUp(self, cloud_config_fixture='clouds.yaml'): + super(TestGroups, self).setUp( + cloud_config_fixture=cloud_config_fixture) + self._add_discovery_uri_call() + self.addCleanup(self.assert_calls) + + def get_mock_url(self, service_type='identity', interface='admin', + resource='groups', append=None, base_url_append='v3'): + return super(TestGroups, self).get_mock_url( + service_type='identity', interface='admin', resource=resource, + append=append, base_url_append=base_url_append) - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_list_groups(self, mock_keystone): + def test_list_groups(self): + group_data = self._get_group_data() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'groups': [group_data.json_response['group']]}) + ]) self.op_cloud.list_groups() - mock_keystone.groups.list.assert_called_once_with() - - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_get_group(self, mock_keystone): - self.op_cloud.get_group('1234') - mock_keystone.groups.list.assert_called_once_with() - - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_delete_group(self, mock_keystone): - mock_keystone.groups.list.return_value = [ - fakes.FakeGroup('1234', 'name', 'desc') - ] - self.assertTrue(self.op_cloud.delete_group('1234')) - mock_keystone.groups.list.assert_called_once_with() - mock_keystone.groups.delete.assert_called_once_with( - group='1234' - ) - - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_create_group(self, mock_keystone): - self.op_cloud.create_group('test-group', 'test desc') - mock_keystone.groups.create.assert_called_once_with( - name='test-group', description='test desc', domain=None - ) - - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_update_group(self, mock_keystone): - mock_keystone.groups.list.return_value = [ - fakes.FakeGroup('1234', 'name', 'desc') - ] - self.op_cloud.update_group('1234', 'test-group', 'test desc') - mock_keystone.groups.list.assert_called_once_with() - mock_keystone.groups.update.assert_called_once_with( - group='1234', name='test-group', description='test desc' - ) + + def test_get_group(self): + group_data = self._get_group_data() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'groups': [group_data.json_response['group']]}), + ]) + self.op_cloud.get_group(group_data.group_id) + + def test_delete_group(self): + group_data = self._get_group_data() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'groups': [group_data.json_response['group']]}), + dict(method='DELETE', + uri=self.get_mock_url(append=[group_data.group_id]), + status_code=204), + ]) + self.assertTrue(self.op_cloud.delete_group(group_data.group_id)) + + def test_create_group(self): + domain_data = self._get_domain_data() + group_data = self._get_group_data(domain_id=domain_data.domain_id) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[domain_data.domain_id]), + status_code=200, + json=domain_data.json_response), + dict(method='POST', + uri=self.get_mock_url(), + status_code=200, + json=group_data.json_response, + validate=group_data.json_request), + dict(method='GET', + uri=self.get_mock_url(append=[group_data.group_id]), + status_code=200, + json=group_data.json_response) + ]) + self.op_cloud.create_group( + name=group_data.group_name, description=group_data.description, + domain=group_data.domain_id) + + def test_update_group(self): + group_data = self._get_group_data() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'groups': [group_data.json_response['group']]}), + dict(method='PATCH', + uri=self.get_mock_url(append=[group_data.group_id]), + status_code=200, + json=group_data.json_response, + validate=group_data.json_request), + dict(method='GET', + uri=self.get_mock_url(append=[group_data.group_id]), + status_code=200, + json=group_data.json_response) + ]) + self.op_cloud.update_group(group_data.group_id, group_data.group_name, + group_data.description) From 0a956c1d281d8ffe984fac0bd5588fad5d00a00d Mon Sep 17 00:00:00 2001 From: xhzhf Date: Tue, 21 Feb 2017 17:44:56 +0800 Subject: [PATCH 1333/3836] modify test-requirement according to requirements project the OpenStack requirements project has modify version requirement of docutils/oslosphinx. The link is below https://review.openstack.org/#/c/418772/ So modify test-requirement like other project Closes-Bug: #1666149 Change-Id: I145ba596926cac0efab75fb4a1548eea338a2d5a --- test-requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index f9908d6e0..10abdfe3d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,7 +5,7 @@ hacking>=0.10.2,<0.11 # Apache-2.0 coverage>=3.6 -docutils>=0.11,!=0.13.1 # OSI-Approved Open Source, Public Domain +docutils>=0.11 # OSI-Approved Open Source, Public Domain extras fixtures>=0.3.14 jsonschema>=2.0.0,<3.0.0,!=2.5.0 @@ -13,8 +13,8 @@ mock>=1.2 python-glanceclient>=0.18.0 python-keystoneclient>=1.1.0 python-subunit>=0.0.18 -sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 -oslosphinx>=2.5.0,<2.6.0 # Apache-2.0 +sphinx>=1.5.1 # BSD +oslosphinx>=4.7.0 # Apache-2.0 oslotest>=1.5.1,<1.6.0 # Apache-2.0 reno>=0.1.1 # Apache2 testrepository>=0.0.18 From dee0749330a0c9d6b99553b97467c3b2457e5ce0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 8 Mar 2017 11:46:59 -0600 Subject: [PATCH 1334/3836] Put fnmatch code back, but safely this time If we do a strict equality test FIRST, then the fnmatch, then we don't have to remove the fnmatch functionality, but we should also be able to match normal names with wildcards in them. This reverts commit 0ecbb22547c470fca6d50cfad2ee94e1241f83f4. Change-Id: Ibc08cc09e0e3db2e194f1b8d0cf98c08500d87c5 --- shade/_utils.py | 22 +++++++++++++++++++- shade/tests/unit/test__utils.py | 37 +++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/shade/_utils.py b/shade/_utils.py index 38467785c..20648da7a 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -13,12 +13,14 @@ # limitations under the License. import contextlib +import fnmatch import inspect import jmespath import munch import netifaces import re import six +import sre_constants import sys import time @@ -102,7 +104,8 @@ def _filter_list(data, name_or_id, filters): each dictionary contains an 'id' and 'name' key if a value for name_or_id is given. :param string name_or_id: - The name or ID of the entity being filtered. + The name or ID of the entity being filtered. Can be a glob pattern, + such as 'nb01*'. :param filters: A dictionary of meta data to use for further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: @@ -116,6 +119,10 @@ def _filter_list(data, name_or_id, filters): OR A string containing a jmespath expression for further filtering. """ + # The logger is shade.fmmatch to allow a user/operator to configure logging + # not to communicate about fnmatch misses (they shouldn't be too spammy, + # but one never knows) + log = _log.setup_logging('shade.fnmatch') if name_or_id: # name_or_id might already be unicode name_or_id = _make_unicode(name_or_id) @@ -123,9 +130,22 @@ def _filter_list(data, name_or_id, filters): for e in data: e_id = _make_unicode(e.get('id', None)) e_name = _make_unicode(e.get('name', None)) + if ((e_id and e_id == name_or_id) or (e_name and e_name == name_or_id)): identifier_matches.append(e) + else: + # Only try fnmatch if we don't match exactly + try: + if ((e_id and fnmatch.fnmatch(e_id, name_or_id)) or + (e_name and fnmatch.fnmatch(e_name, name_or_id))): + identifier_matches.append(e) + except sre_constants.error: + # If the fnmatch re doesn't compile, then we don't care, + # but log it in case the user DID pass a pattern but did + # it poorly and wants to know what went wrong with their + # search + log.debug("Bad pattern passed to fnmatch", exc_info=True) data = identifier_matches if not filters: diff --git a/shade/tests/unit/test__utils.py b/shade/tests/unit/test__utils.py index 4b178e2fb..8296f471e 100644 --- a/shade/tests/unit/test__utils.py +++ b/shade/tests/unit/test__utils.py @@ -49,6 +49,43 @@ def test__filter_list_name_or_id_special(self): ret = _utils._filter_list(data, 'pluto[2017-01-10]', None) self.assertEqual([el2], ret) + def test__filter_list_name_or_id_partial_bad(self): + el1 = dict(id=100, name='donald') + el2 = dict(id=200, name='pluto[2017-01-10]') + data = [el1, el2] + ret = _utils._filter_list(data, 'pluto[2017-01]', None) + self.assertEqual([], ret) + + def test__filter_list_name_or_id_partial_glob(self): + el1 = dict(id=100, name='donald') + el2 = dict(id=200, name='pluto[2017-01-10]') + data = [el1, el2] + ret = _utils._filter_list(data, 'pluto*', None) + self.assertEqual([el2], ret) + + def test__filter_list_name_or_id_non_glob_glob(self): + el1 = dict(id=100, name='donald') + el2 = dict(id=200, name='pluto[2017-01-10]') + data = [el1, el2] + ret = _utils._filter_list(data, 'pluto', None) + self.assertEqual([], ret) + + def test__filter_list_name_or_id_glob(self): + el1 = dict(id=100, name='donald') + el2 = dict(id=200, name='pluto') + el3 = dict(id=200, name='pluto-2') + data = [el1, el2, el3] + ret = _utils._filter_list(data, 'pluto*', None) + self.assertEqual([el2, el3], ret) + + def test__filter_list_name_or_id_glob_not_found(self): + el1 = dict(id=100, name='donald') + el2 = dict(id=200, name='pluto') + el3 = dict(id=200, name='pluto-2') + data = [el1, el2, el3] + ret = _utils._filter_list(data, 'q*', None) + self.assertEqual([], ret) + def test__filter_list_unicode(self): el1 = dict(id=100, name=u'中文', last='duck', other=dict(category='duck', financial=dict(status='poor'))) From b2f7ceadb1a99bd0f5fb17b9298c6f962414aa9f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 28 Feb 2017 09:07:27 -0600 Subject: [PATCH 1335/3836] Add support for bailing on invalid service versions At least for cinder for now, allow a consumer of get_legacy_client to express the minimum version they find acceptable. This will use cinder_client logic to figure out the version from the url. As a follow on, expand this to all of the clients and make it support microversions for the clients that support microversions. (Right now it's just going to be major versions, so min_version=1 will throw an exception if the cinder service returns a v1 endpoint. Also, because we override the volume/volumev2/volumev3 service type stuff, we need to do extra special logic in get_session_endpoint to try all three in the case where do not have a configured api_version. Change-Id: I7b6b3588fec9a6be892cf20d344667f0b9a62f0a --- os_client_config/cloud_config.py | 87 +++++++++++++++---- os_client_config/exceptions.py | 8 ++ ...n-max-legacy-version-301242466ddefa93.yaml | 15 ++++ 3 files changed, 93 insertions(+), 17 deletions(-) create mode 100644 releasenotes/notes/min-max-legacy-version-301242466ddefa93.yaml diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 3521920d4..22b7d4aab 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -13,6 +13,7 @@ # under the License. import importlib +import math import warnings from keystoneauth1 import adapter @@ -234,7 +235,19 @@ def get_session_client(self, service_key): interface=self.get_interface(service_key), region_name=self.region) - def get_session_endpoint(self, service_key): + def _get_highest_endpoint(self, service_types, kwargs): + session = self.get_session() + for service_type in service_types: + kwargs['service_type'] = service_type + try: + # Return the highest version we find that matches + # the request + return session.get_endpoint(**kwargs) + except keystoneauth1.exceptions.catalog.EndpointNotFound: + pass + + def get_session_endpoint( + self, service_key, min_version=None, max_version=None): """Return the endpoint from config or the catalog. If a configuration lists an explicit endpoint for a service, @@ -250,27 +263,51 @@ def get_session_endpoint(self, service_key): if override_endpoint: return override_endpoint # keystone is a special case in keystone, because what? - session = self.get_session() + endpoint = None + kwargs = { + 'service_name': self.get_service_name(service_key), + 'region_name': self.region + } if service_key == 'identity': - endpoint = session.get_endpoint(interface=plugin.AUTH_INTERFACE) + # setting interface in kwargs dict even though we don't use kwargs + # dict here just for ease of warning text later + kwargs['interface'] = plugin.AUTH_INTERFACE + session = self.get_session() + endpoint = session.get_endpoint(interface=kwargs['interface']) else: - args = { - 'service_type': self.get_service_type(service_key), - 'service_name': self.get_service_name(service_key), - 'interface': self.get_interface(service_key), - 'region_name': self.region - } - try: - endpoint = session.get_endpoint(**args) - except keystoneauth1.exceptions.catalog.EndpointNotFound: - self.log.warning("Keystone catalog entry not found (%s)", - args) - endpoint = None + kwargs['interface'] = self.get_interface(service_key) + if service_key == 'volume' and not self.get_api_version('volume'): + # If we don't have a configured cinder version, we can't know + # to request a different service_type + min_version = float(min_version or 1) + max_version = float(max_version or 3) + min_major = math.trunc(float(min_version)) + max_major = math.trunc(float(max_version)) + versions = range(int(max_major) + 1, int(min_major), -1) + service_types = [] + for version in versions: + if version == 1: + service_types.append('volume') + else: + service_types.append('volumev{v}'.format(v=version)) + else: + service_types = [self.get_service_type(service_key)] + endpoint = self._get_highest_endpoint(service_types, kwargs) + if not endpoint: + self.log.warning( + "Keystone catalog entry not found (" + "service_type=%s,service_name=%s" + "interface=%s,region_name=%s)", + service_key, + kwargs['service_name'], + kwargs['interface'], + kwargs['region_name']) return endpoint def get_legacy_client( self, service_key, client_class=None, interface_key=None, - pass_version_arg=True, version=None, **kwargs): + pass_version_arg=True, version=None, min_version=None, + max_version=None, **kwargs): """Return a legacy OpenStack client object for the given config. Most of the OpenStack python-*client libraries have the same @@ -304,6 +341,8 @@ def get_legacy_client( that case. :param version: (optional) Version string to override the configured version string. + :param min_version: (options) Minimum version acceptable. + :param max_version: (options) Maximum version acceptable. :param kwargs: (optional) keyword args are passed through to the Client constructor, so this is in case anything additional needs to be passed in. @@ -313,7 +352,8 @@ def get_legacy_client( interface = self.get_interface(service_key) # trigger exception on lack of service - endpoint = self.get_session_endpoint(service_key) + endpoint = self.get_session_endpoint( + service_key, min_version=min_version, max_version=max_version) endpoint_override = self.get_endpoint(service_key) if not interface_key: @@ -361,6 +401,9 @@ def get_legacy_client( if pass_version_arg and service_key != 'object-store': if not version: version = self.get_api_version(service_key) + if not version and service_key == 'volume': + from cinderclient import client as cinder_client + version = cinder_client.get_volume_api_from_url(endpoint) # Temporary workaround while we wait for python-openstackclient # to be able to handle 2.0 which is what neutronclient expects if service_key == 'network' and version == '2': @@ -380,6 +423,16 @@ def get_legacy_client( constructor_kwargs['version'] = version[0] else: constructor_kwargs['version'] = version + if min_version and min_version > float(version): + raise exceptions.OpenStackConfigVersionException( + "Minimum version {min_version} requested but {version}" + " found".format(min_version=min_version, version=version), + version=version) + if max_version and max_version < float(version): + raise exceptions.OpenStackConfigVersionException( + "Maximum version {max_version} requested but {version}" + " found".format(max_version=max_version, version=version), + version=version) if service_key == 'database': # TODO(mordred) Remove when https://review.openstack.org/314032 # has landed and released. We're passing in a Session, but the diff --git a/os_client_config/exceptions.py b/os_client_config/exceptions.py index ab78dc2e5..556dd49bc 100644 --- a/os_client_config/exceptions.py +++ b/os_client_config/exceptions.py @@ -15,3 +15,11 @@ class OpenStackConfigException(Exception): """Something went wrong with parsing your OpenStack Config.""" + + +class OpenStackConfigVersionException(OpenStackConfigException): + """A version was requested that is different than what was found.""" + + def __init__(self, version): + super(OpenStackConfigVersionException, self).__init__() + self.version = version diff --git a/releasenotes/notes/min-max-legacy-version-301242466ddefa93.yaml b/releasenotes/notes/min-max-legacy-version-301242466ddefa93.yaml new file mode 100644 index 000000000..30a380225 --- /dev/null +++ b/releasenotes/notes/min-max-legacy-version-301242466ddefa93.yaml @@ -0,0 +1,15 @@ +--- +features: + - Add min_version and max_version to get_legacy_client + and to get_session_endpoint. At the moment this is only + really fully plumbed through for cinder, which has extra + special fun around volume, volumev2 and volumev3. Min and max + versions to both methods will look through the options available + in the service catalog and try to return the latest one available + from the span of requested versions. This means a user can say + volume_api_version=None, min_version=2, max_version=3 will get + an endpoint from get_session_endpoint or a Client from cinderclient + that will be either v2 or v3 but not v1. In the future, min and max + version for get_session_endpoint should be able to sort out + appropriate endpoints via version discovery, but that does not + currently exist. From 38e5eba621e48d74c05315da2b89e6c801f4c43f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 9 Mar 2017 09:32:15 -0600 Subject: [PATCH 1336/3836] Use interface not endpoint_type for keystoneclient keystoneclient wants the interface argument. Change-Id: I5898d8621259256f962fc006df38049d0cb059f8 --- os_client_config/cloud_config.py | 2 +- os_client_config/tests/test_cloud_config.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 8eae86f77..ae74e5524 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -321,7 +321,7 @@ def get_legacy_client( endpoint_override = self.get_endpoint(service_key) if not interface_key: - if service_key in ('image', 'key-manager'): + if service_key in ('image', 'key-manager', 'identity'): interface_key = 'interface' else: interface_key = 'endpoint_type' diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index ce724cb56..3c1ae1f34 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -529,7 +529,7 @@ def test_legacy_client_identity(self, mock_get_session_endpoint): mock_client.assert_called_with( version='2.0', endpoint='http://example.com/v2', - endpoint_type='admin', + interface='admin', endpoint_override=None, region_name='region-al', service_type='identity', @@ -549,7 +549,7 @@ def test_legacy_client_identity_v3(self, mock_get_session_endpoint): mock_client.assert_called_with( version='3', endpoint='http://example.com', - endpoint_type='admin', + interface='admin', endpoint_override=None, region_name='region-al', service_type='identity', From 189353e4eb110facbabf9882e0af1ef16ced600f Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Thu, 9 Mar 2017 11:36:51 -0700 Subject: [PATCH 1337/3836] Fix network quota test so it works on gate The gate does not create quotas by default, but devstack does. This test is not important enough to make work for the gate which would probably require some reconfiguration, but it is nice to have it for devstack. Change-Id: I6618b5ee8c1dde7773b83e8ba97092f30d595e8a Partial-bug: #1665495 --- .../tests/functional/network/v2/test_quota.py | 29 +++++-------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/openstack/tests/functional/network/v2/test_quota.py b/openstack/tests/functional/network/v2/test_quota.py index b02f18f4b..9fa100453 100644 --- a/openstack/tests/functional/network/v2/test_quota.py +++ b/openstack/tests/functional/network/v2/test_quota.py @@ -10,34 +10,19 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid - from openstack.tests.functional import base class TestQuota(base.BaseFunctionalTest): - PROJECT_NAME = 'project-' + uuid.uuid4().hex - PROJECT = None - - @classmethod - def setUpClass(cls): - super(TestQuota, cls).setUpClass() - # Need a project to have a quota - cls.PROJECT = cls.conn.identity.create_project(name=cls.PROJECT_NAME) - - @classmethod - def tearDownClass(cls): - cls.conn.identity.delete_project(cls.PROJECT.id) - def test_list(self): - qot = self.conn.network.quotas().next() - self.assertIsNotNone(qot.project_id) - self.assertIsNotNone(qot.networks) + for qot in self.conn.network.quotas(): + self.assertIsNotNone(qot.project_id) + self.assertIsNotNone(qot.networks) def test_set(self): attrs = {'networks': 123456789} - project_quota = self.conn.network.quotas().next() - self.conn.network.update_quota(project_quota, **attrs) - new_quota = self.conn.network.get_quota(project_quota.project_id) - self.assertEqual(123456789, new_quota.networks) + for project_quota in self.conn.network.quotas(): + self.conn.network.update_quota(project_quota, **attrs) + new_quota = self.conn.network.get_quota(project_quota.project_id) + self.assertEqual(123456789, new_quota.networks) From f22cabf494f13535cdbb489f12e98c7358a29f74 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Thu, 9 Mar 2017 15:29:19 -0700 Subject: [PATCH 1338/3836] Fix the telemetry sample test This test works fine on devstack, but on the test gate not all the meters have samples, so only iterate over them if there are samples. Partial-bug: #1665495 Change-Id: I8f327737a53194aeba08925391f1976f1b506aa0 --- openstack/tests/functional/telemetry/v2/test_sample.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openstack/tests/functional/telemetry/v2/test_sample.py b/openstack/tests/functional/telemetry/v2/test_sample.py index 46afc5690..49c209ec5 100644 --- a/openstack/tests/functional/telemetry/v2/test_sample.py +++ b/openstack/tests/functional/telemetry/v2/test_sample.py @@ -22,5 +22,5 @@ class TestSample(base.BaseFunctionalTest): def test_list(self): for meter in self.conn.telemetry.meters(): - sot = next(self.conn.telemetry.samples(meter)) - assert isinstance(sot, sample.Sample) + for sot in self.conn.telemetry.samples(meter): + assert isinstance(sot, sample.Sample) From 7e1555fdd1e923e199025344294bfecc31f2f92c Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Fri, 10 Mar 2017 08:31:00 -0500 Subject: [PATCH 1339/3836] Correct a copy/paste mistake in a docstring remove_security_group_from_server has the add-to docstring. Change-Id: I52c6406e89142b3802ecff2ae4d39f696b9100c9 Closes-Bug: 1671708 --- openstack/compute/v2/_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 1fd9bf24f..fef87f179 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -554,7 +554,7 @@ def add_security_group_to_server(self, server, security_group): server.add_security_group(self._session, security_group_id) def remove_security_group_from_server(self, server, security_group): - """Add a security group to a server + """Remove a security group from a server :param server: Either the ID of a server or a :class:`~openstack.compute.v2.server.Server` instance. From 134bbe79c693665911f8d697e7d669064af4bac2 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 10 Mar 2017 08:29:15 -0600 Subject: [PATCH 1340/3836] Only do fnmatch compilation and logging once per loop We don't need to compile the fnmatch regex in every loop pass - so compile it at the top of the loop. Also, logging about a bad regex on every loop is silly. Only log if we don't have a match at the end. Change-Id: Iffa039237042d6a2f75aa08f2c949d508b367ec4 --- shade/_utils.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/shade/_utils.py b/shade/_utils.py index 20648da7a..3ca1c6434 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -127,6 +127,15 @@ def _filter_list(data, name_or_id, filters): # name_or_id might already be unicode name_or_id = _make_unicode(name_or_id) identifier_matches = [] + bad_pattern = False + try: + fn_reg = re.compile(fnmatch.translate(name_or_id)) + except sre_constants.error: + # If the fnmatch re doesn't compile, then we don't care, + # but log it in case the user DID pass a pattern but did + # it poorly and wants to know what went wrong with their + # search + fn_reg = None for e in data: e_id = _make_unicode(e.get('id', None)) e_name = _make_unicode(e.get('name', None)) @@ -136,16 +145,16 @@ def _filter_list(data, name_or_id, filters): identifier_matches.append(e) else: # Only try fnmatch if we don't match exactly - try: - if ((e_id and fnmatch.fnmatch(e_id, name_or_id)) or - (e_name and fnmatch.fnmatch(e_name, name_or_id))): - identifier_matches.append(e) - except sre_constants.error: - # If the fnmatch re doesn't compile, then we don't care, - # but log it in case the user DID pass a pattern but did - # it poorly and wants to know what went wrong with their - # search - log.debug("Bad pattern passed to fnmatch", exc_info=True) + if not fn_reg: + # If we don't have a pattern, skip this, but set the flag + # so that we log the bad pattern + bad_pattern = True + continue + if ((e_id and fn_reg.match(e_id)) or + (e_name and fn_reg.match(e_name))): + identifier_matches.append(e) + if not identifier_matches and bad_pattern: + log.debug("Bad pattern passed to fnmatch", exc_info=True) data = identifier_matches if not filters: From 4f4c3fabe1ccb91ca8f510a6ab81b6f2eb588c17 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Fri, 10 Mar 2017 08:49:46 -0700 Subject: [PATCH 1341/3836] Fix the telemetry statistics test This test worked fine on devstack, but failed on the test gate because not all meters have statistics. Look for a meter with statistics. Partial-bug: #1665495 Change-Id: Ife0f1f11c70e926801b48000dd0b4e9d863a865f --- .../tests/functional/telemetry/v2/test_statistics.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/openstack/tests/functional/telemetry/v2/test_statistics.py b/openstack/tests/functional/telemetry/v2/test_statistics.py index d0e6c6ab1..7db971357 100644 --- a/openstack/tests/functional/telemetry/v2/test_statistics.py +++ b/openstack/tests/functional/telemetry/v2/test_statistics.py @@ -20,12 +20,7 @@ class TestStatistics(base.BaseFunctionalTest): def test_list(self): - found_something = False for met in self.conn.telemetry.meters(): - try: - stat = next(self.conn.telemetry.statistics(met)) - self.assertIn('period_end', stat) - found_something = True - except Exception: - pass - self.assertTrue(found_something) + for stat in self.conn.telemetry.statistics(met): + self.assertTrue(stat.period_end_at is not None) + break From a93b3e2af4461cdb0812745b7a2cf19b11ec2c47 Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Fri, 10 Mar 2017 11:15:21 -0800 Subject: [PATCH 1342/3836] Convert test_services to requests_mock convert tests in test_services to use requests_mock Change-Id: I2128025cfe981288cd4f7f1538f153b9d5e60f33 --- shade/tests/unit/base.py | 23 ++ shade/tests/unit/test_services.py | 353 +++++++++++++++++++----------- 2 files changed, 252 insertions(+), 124 deletions(-) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index c47eaf352..c28a2d3cc 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -46,12 +46,19 @@ 'group_id, group_name, domain_id, description, json_response, ' 'json_request') + _DomainData = collections.namedtuple( 'DomainData', 'domain_id, domain_name, description, json_response, ' 'json_request') +_ServiceData = collections.namedtuple( + 'Servicedata', + 'service_id, service_name, service_type, description, enabled, ' + 'json_response_v3, json_response_v2, json_request') + + class BaseTestCase(base.TestCase): def setUp(self, cloud_config_fixture='clouds.yaml'): @@ -294,6 +301,22 @@ def _get_domain_data(self, domain_name=None, description=None, return _DomainData(domain_id, domain_name, description, {'domain': response}, {'domain': request}) + def _get_service_data(self, type=None, name=None, description=None, + enabled=True): + service_id = uuid.uuid4().hex + name = name or uuid.uuid4().hex + type = type or uuid.uuid4().hex + + response = {'id': service_id, 'name': name, 'type': type, + 'enabled': enabled} + if description is not None: + response['description'] = description + request = response.copy() + request.pop('id') + return _ServiceData(service_id, name, type, description, enabled, + {'service': response}, + {'OS-KSADM:service': response}, request) + def use_keystone_v3(self): self.adapter = self.useFixture(rm_fixture.Fixture()) self.calls = [] diff --git a/shade/tests/unit/test_services.py b/shade/tests/unit/test_services.py index bfb82f063..0226cc2c7 100644 --- a/shade/tests/unit/test_services.py +++ b/shade/tests/unit/test_services.py @@ -19,170 +19,275 @@ Tests Keystone services commands. """ -from mock import patch -import os_client_config -from shade import _utils -from shade import meta from shade import OpenStackCloudException from shade.exc import OpenStackCloudUnavailableFeature -from shade import OperatorCloud -from shade.tests.fakes import FakeService from shade.tests.unit import base +from testtools import matchers -class CloudServices(base.TestCase): - mock_services = [ - {'id': 'id1', 'name': 'service1', 'type': 'type1', - 'service_type': 'type1', 'description': 'desc1', 'enabled': True}, - {'id': 'id2', 'name': 'service2', 'type': 'type2', - 'service_type': 'type2', 'description': 'desc2', 'enabled': True}, - {'id': 'id3', 'name': 'service3', 'type': 'type2', - 'service_type': 'type2', 'description': 'desc3', 'enabled': True}, - {'id': 'id4', 'name': 'service4', 'type': 'type3', - 'service_type': 'type3', 'description': 'desc4', 'enabled': True} - ] - - def setUp(self): - super(CloudServices, self).setUp() - self.mock_ks_services = [FakeService(**kwa) for kwa in - self.mock_services] - - @patch.object(_utils, 'normalize_keystone_services') - @patch.object(OperatorCloud, 'keystone_client') - @patch.object(os_client_config.cloud_config.CloudConfig, 'get_api_version') - def test_create_service_v2(self, mock_api_version, mock_keystone_client, - mock_norm): - mock_api_version.return_value = '2.0' - kwargs = { - 'name': 'a service', - 'type': 'network', - 'description': 'This is a test service' - } - - self.op_cloud.create_service(**kwargs) - kwargs['service_type'] = kwargs.pop('type') - mock_keystone_client.services.create.assert_called_with(**kwargs) - self.assertTrue(mock_norm.called) - - @patch.object(_utils, 'normalize_keystone_services') - @patch.object(OperatorCloud, 'keystone_client') - @patch.object(os_client_config.cloud_config.CloudConfig, 'get_api_version') - def test_create_service_v3(self, mock_api_version, mock_keystone_client, - mock_norm): - mock_api_version.return_value = '3' - kwargs = { - 'name': 'a v3 service', - 'type': 'cinderv2', - 'description': 'This is a test service', - 'enabled': False - } - - self.op_cloud.create_service(**kwargs) - mock_keystone_client.services.create.assert_called_with(**kwargs) - self.assertTrue(mock_norm.called) - - @patch.object(os_client_config.cloud_config.CloudConfig, 'get_api_version') - def test_update_service_v2(self, mock_api_version): - mock_api_version.return_value = '2.0' +class CloudServices(base.RequestsMockTestCase): + + def setUp(self, cloud_config_fixture='clouds.yaml'): + super(CloudServices, self).setUp(cloud_config_fixture) + + def get_mock_url(self, service_type='identity', interface='admin', + resource='services', append=None, base_url_append='v3'): + + return super(CloudServices, self).get_mock_url( + service_type, interface, resource, append, base_url_append) + + def test_create_service_v2(self): + self.use_keystone_v2() + service_data = self._get_service_data(name='a service', type='network', + description='A test service') + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url(base_url_append='OS-KSADM'), + status_code=200, + json=service_data.json_response_v2, + validate=service_data.json_request), + dict(method='GET', + uri=self.get_mock_url(base_url_append='OS-KSADM', + append=[service_data.service_id]), + status_code=200, + json=service_data.json_response_v2) + ]) + + service = self.op_cloud.create_service( + name=service_data.service_name, + service_type=service_data.service_type, + description=service_data.description) + self.assertThat(service.name, + matchers.Equals(service_data.service_name)) + self.assertThat(service.id, matchers.Equals(service_data.service_id)) + self.assertThat(service.description, + matchers.Equals(service_data.description)) + self.assertThat(service.type, + matchers.Equals(service_data.service_type)) + self.assert_calls() + + def test_create_service_v3(self): + self._add_discovery_uri_call() + service_data = self._get_service_data(name='a service', type='network', + description='A test service') + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url(), + status_code=200, + json=service_data.json_response_v3, + validate=service_data.json_request), + dict(method='GET', + uri=self.get_mock_url(append=[service_data.service_id]), + status_code=200, + json=service_data.json_response_v3) + ]) + + service = self.op_cloud.create_service( + name=service_data.service_name, + service_type=service_data.service_type, + description=service_data.description) + self.assertThat(service.name, + matchers.Equals(service_data.service_name)) + self.assertThat(service.id, matchers.Equals(service_data.service_id)) + self.assertThat(service.description, + matchers.Equals(service_data.description)) + self.assertThat(service.type, + matchers.Equals(service_data.service_type)) + self.assert_calls() + + def test_update_service_v2(self): + self.use_keystone_v2() # NOTE(SamYaple): Update service only works with v3 api self.assertRaises(OpenStackCloudUnavailableFeature, self.op_cloud.update_service, 'service_id', name='new name') - @patch.object(_utils, 'normalize_keystone_services') - @patch.object(OperatorCloud, 'keystone_client') - @patch.object(os_client_config.cloud_config.CloudConfig, 'get_api_version') - def test_update_service_v3(self, mock_api_version, mock_keystone_client, - mock_norm): - mock_api_version.return_value = '3' - kwargs = { - 'name': 'updated_name', - 'type': 'updated_type', - 'service_type': 'updated_type', - 'description': 'updated_name', - 'enabled': False - } - - service_obj = FakeService(id='id1', **kwargs) - mock_keystone_client.services.update.return_value = service_obj - - self.op_cloud.update_service('id1', **kwargs) - del kwargs['service_type'] - mock_keystone_client.services.update.assert_called_once_with( - service='id1', **kwargs - ) - mock_norm.assert_called_once_with([meta.obj_to_dict(service_obj)]) - - @patch.object(OperatorCloud, 'keystone_client') - def test_list_services(self, mock_keystone_client): - mock_keystone_client.services.list.return_value = \ - self.mock_ks_services + def test_update_service_v3(self): + self._add_discovery_uri_call() + service_data = self._get_service_data(name='a service', type='network', + description='A test service') + request = service_data.json_request.copy() + request['enabled'] = False + resp = service_data.json_response_v3.copy() + resp['enabled'] = False + self.register_uris([ + dict(method='PATCH', + uri=self.get_mock_url(append=[service_data.service_id]), + status_code=200, + json=resp, + validate=request), + dict(method='GET', + uri=self.get_mock_url(append=[service_data.service_id]), + status_code=200, + json=resp), + ]) + + service = self.op_cloud.update_service(service_data.service_id, + enabled=False) + self.assertThat(service.name, + matchers.Equals(service_data.service_name)) + self.assertThat(service.id, matchers.Equals(service_data.service_id)) + self.assertThat(service.description, + matchers.Equals(service_data.description)) + self.assertThat(service.type, + matchers.Equals(service_data.service_type)) + self.assert_calls() + + def test_list_services(self): + self._add_discovery_uri_call() + service_data = self._get_service_data() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'services': [service_data.json_response_v3['service']]}) + ]) services = self.op_cloud.list_services() - mock_keystone_client.services.list.assert_called_with() - self.assertItemsEqual(self.mock_services, services) + self.assertThat(len(services), matchers.Equals(1)) + self.assertThat(services[0].id, + matchers.Equals(service_data.service_id)) + self.assertThat(services[0].name, + matchers.Equals(service_data.service_name)) + self.assertThat(services[0].type, + matchers.Equals(service_data.service_type)) + self.assert_calls() - @patch.object(OperatorCloud, 'keystone_client') - def test_get_service(self, mock_keystone_client): - mock_keystone_client.services.list.return_value = \ - self.mock_ks_services + def test_get_service(self): + self._add_discovery_uri_call() + service_data = self._get_service_data() + service2_data = self._get_service_data() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'services': [ + service_data.json_response_v3['service'], + service2_data.json_response_v3['service']]}), + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'services': [ + service_data.json_response_v3['service'], + service2_data.json_response_v3['service']]}), + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'services': [ + service_data.json_response_v3['service'], + service2_data.json_response_v3['service']]}), + dict(method='GET', + uri=self.get_mock_url(), + status_code=400), + ]) # Search by id - service = self.op_cloud.get_service(name_or_id='id4') - # test we are getting exactly 1 element - self.assertEqual(service, self.mock_services[3]) + service = self.op_cloud.get_service(name_or_id=service_data.service_id) + self.assertThat(service.id, matchers.Equals(service_data.service_id)) # Search by name - service = self.op_cloud.get_service(name_or_id='service2') + service = self.op_cloud.get_service( + name_or_id=service_data.service_name) # test we are getting exactly 1 element - self.assertEqual(service, self.mock_services[1]) + self.assertThat(service.id, matchers.Equals(service_data.service_id)) # Not found - service = self.op_cloud.get_service(name_or_id='blah!') + service = self.op_cloud.get_service(name_or_id='INVALID SERVICE') self.assertIs(None, service) # Multiple matches # test we are getting an Exception self.assertRaises(OpenStackCloudException, self.op_cloud.get_service, name_or_id=None, filters={'type': 'type2'}) + self.assert_calls() - @patch.object(OperatorCloud, 'keystone_client') - def test_search_services(self, mock_keystone_client): - mock_keystone_client.services.list.return_value = \ - self.mock_ks_services + def test_search_services(self): + self._add_discovery_uri_call() + service_data = self._get_service_data() + service2_data = self._get_service_data(type=service_data.service_type) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'services': [ + service_data.json_response_v3['service'], + service2_data.json_response_v3['service']]}), + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'services': [ + service_data.json_response_v3['service'], + service2_data.json_response_v3['service']]}), + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'services': [ + service_data.json_response_v3['service'], + service2_data.json_response_v3['service']]}), + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'services': [ + service_data.json_response_v3['service'], + service2_data.json_response_v3['service']]}), + ]) # Search by id - services = self.op_cloud.search_services(name_or_id='id4') + services = self.op_cloud.search_services( + name_or_id=service_data.service_id) # test we are getting exactly 1 element - self.assertEqual(1, len(services)) - self.assertEqual(services, [self.mock_services[3]]) + self.assertThat(len(services), matchers.Equals(1)) + self.assertThat(services[0].id, + matchers.Equals(service_data.service_id)) # Search by name - services = self.op_cloud.search_services(name_or_id='service2') + services = self.op_cloud.search_services( + name_or_id=service_data.service_name) # test we are getting exactly 1 element - self.assertEqual(1, len(services)) - self.assertEqual(services, [self.mock_services[1]]) + self.assertThat(len(services), matchers.Equals(1)) + self.assertThat(services[0].name, + matchers.Equals(service_data.service_name)) # Not found - services = self.op_cloud.search_services(name_or_id='blah!') - self.assertEqual(0, len(services)) + services = self.op_cloud.search_services(name_or_id='!INVALID!') + self.assertThat(len(services), matchers.Equals(0)) # Multiple matches services = self.op_cloud.search_services( - filters={'type': 'type2'}) + filters={'type': service_data.service_type}) # test we are getting exactly 2 elements - self.assertEqual(2, len(services)) - self.assertEqual(services, [self.mock_services[1], - self.mock_services[2]]) + self.assertThat(len(services), matchers.Equals(2)) + self.assertThat(services[0].id, + matchers.Equals(service_data.service_id)) + self.assertThat(services[1].id, + matchers.Equals(service2_data.service_id)) + self.assert_calls() - @patch.object(OperatorCloud, 'keystone_client') - def test_delete_service(self, mock_keystone_client): - mock_keystone_client.services.list.return_value = \ - self.mock_ks_services + def test_delete_service(self): + self._add_discovery_uri_call() + service_data = self._get_service_data() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'services': [ + service_data.json_response_v3['service']]}), + dict(method='DELETE', + uri=self.get_mock_url(append=[service_data.service_id]), + status_code=204), + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'services': [ + service_data.json_response_v3['service']]}), + dict(method='DELETE', + uri=self.get_mock_url(append=[service_data.service_id]), + status_code=204) + ]) # Delete by name - self.op_cloud.delete_service(name_or_id='service3') - mock_keystone_client.services.delete.assert_called_with(id='id3') + self.op_cloud.delete_service(name_or_id=service_data.service_name) # Delete by id - self.op_cloud.delete_service('id1') - mock_keystone_client.services.delete.assert_called_with(id='id1') + self.op_cloud.delete_service(service_data.service_id) + + self.assert_calls() From e840a8e7cc8b9727461cd21432b55dc5c927559f Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Fri, 10 Mar 2017 10:54:29 -0700 Subject: [PATCH 1343/3836] Fix the network flavor disassociate method This test probably passed when we were using our own session or the keystoneauth session changed at some point. Anyway, I think it makes sense to have this method to return None on success. The delete method will throw an exception on failure. Change-Id: I4213724380d189e44f3fec05b7d8065a6419b452 Partial-bug: #1665495 --- openstack/network/v2/flavor.py | 1 + openstack/tests/functional/network/v2/test_flavor.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/openstack/network/v2/flavor.py b/openstack/network/v2/flavor.py index 4ebcc8d25..46e9579a2 100644 --- a/openstack/network/v2/flavor.py +++ b/openstack/network/v2/flavor.py @@ -57,3 +57,4 @@ def disassociate_flavor_from_service_profile( url = utils.urljoin( self.base_path, flavor_id, 'service_profiles', service_profile_id) session.delete(url, endpoint_filter=self.service) + return None diff --git a/openstack/tests/functional/network/v2/test_flavor.py b/openstack/tests/functional/network/v2/test_flavor.py index 5cef564ea..ad486e9e0 100644 --- a/openstack/tests/functional/network/v2/test_flavor.py +++ b/openstack/tests/functional/network/v2/test_flavor.py @@ -78,4 +78,4 @@ def test_disassociate_flavor_from_service_profile(self): response = \ self.conn.network.disassociate_flavor_from_service_profile( self.ID, self.service_profiles.id) - self.assertIsNotNone(response) + self.assertIsNone(response) From 2a8d0c57b2903215f6b30d667ad33e93447a8012 Mon Sep 17 00:00:00 2001 From: Thanh Ha Date: Fri, 10 Mar 2017 15:33:41 -0500 Subject: [PATCH 1344/3836] Depend on pbr>=2.0.0 Several of shade's dependencies now require pbr>=2.0 however the cap is preventing `pip install shade` from just working after install completes. Update shade to also require pbr>=2.0.0. Change-Id: I5aa5081c200d24f9f43253d409f7b80e52c8841f Signed-off-by: Thanh Ha --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 80e8951eb..cd4099331 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -pbr>=0.11,<2.0 +pbr>=2.0.0 # Apache-2.0 munch decorator From 237041aff9d99ac840572742467772edf1f4d5ef Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Mon, 13 Mar 2017 11:14:45 -0400 Subject: [PATCH 1345/3836] Add image download example I forgot to `git add` this file to the Ie62ebcc895ca893321a10def18ac5d74c7c843b9 change and recently noticed it causing `tox -e pep8` to fail locally. Change-Id: Ib80eda80f27457f2db2672d58c4a6c4a7ecd3d7c --- examples/image/download.py | 64 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 examples/image/download.py diff --git a/examples/image/download.py b/examples/image/download.py new file mode 100644 index 000000000..8b68dd6aa --- /dev/null +++ b/examples/image/download.py @@ -0,0 +1,64 @@ +# 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 hashlib + +""" +Download an image with the Image service. + +For a full guide see +http://developer.openstack.org/sdks/python/openstacksdk/users/guides/image.html +""" + + +def download_image_stream(conn): + print("Download Image via streaming:") + + # Find the image you would like to download. + image = conn.image.find_image("myimage") + + # As the actual download now takes place outside of the library + # and in your own code, you are now responsible for checking + # the integrity of the data. Create an MD5 has to be computed + # after all of the data has been consumed. + md5 = hashlib.md5() + + with open("myimage.qcow2", "wb") as local_image: + response = conn.image.download_image(image, stream=True) + + # Read only 1024 bytes of memory at a time until + # all of the image data has been consumed. + for chunk in response.iter_content(chunk_size=1024): + + # With each chunk, add it to the hash to be computed. + md5.update(chunk) + + local_image.write(chunk) + + # Now that you've consumed all of the data the response gave you, + # ensure that the checksums of what the server offered and + # what you downloaded are the same. + if response.headers["Content-MD5"] != md5.hexdigest(): + raise Exception("Checksum mismatch in downloaded content") + + +def download_image(conn): + print("Download Image:") + + # Find the image you would like to download. + image = conn.image.find_image("myimage") + + with open("myimage.qcow2", "w") as local_image: + response = conn.image.download_image(image) + + # Response will contain the entire contents of the Image. + local_image.write(response) From 194e53c84e32d2fde851f03992788d3b95fab3e3 Mon Sep 17 00:00:00 2001 From: Clark Boylan Date: Mon, 13 Mar 2017 11:36:28 -0700 Subject: [PATCH 1346/3836] OVH supports qcow2 OVH supports qcow2 images too. Update the docs and vendor json file to reflect this. You can continue to use raw images just fine as well. Change-Id: Ic7dc4c70c681947a0475bbabf5621672825dfb3c --- doc/source/vendor-support.rst | 2 +- os_client_config/vendors/ovh.json | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index 577093c57..4400e5e46 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -223,7 +223,7 @@ SBG1 Strassbourg, FR GRA1 Gravelines, FR ============== ================ -* Images must be in `raw` format +* Images may be in `raw` format. The `qcow2` default is also supported * Floating IPs are not supported rackspace diff --git a/os_client_config/vendors/ovh.json b/os_client_config/vendors/ovh.json index 664f1617f..f17dc2b68 100644 --- a/os_client_config/vendors/ovh.json +++ b/os_client_config/vendors/ovh.json @@ -10,7 +10,6 @@ "SBG1" ], "identity_api_version": "3", - "image_format": "raw", "floating_ip_source": "None" } } From 528e6d93b4527ca2b9d1fa81d09c285c7c88ad35 Mon Sep 17 00:00:00 2001 From: Bence Romsics Date: Tue, 14 Mar 2017 11:29:48 +0100 Subject: [PATCH 1347/3836] Add port property: trunk_details Since the introduction of the neutron trunk port feature in newton some ports (ie. the trunk parent ports) have a new attribute: trunk_details. Let the openstack sdk know about it. See also: https://developer.openstack.org/api-ref/networking/v2/ \ ?expanded=show-trunk-details-detail Change-Id: I7a675f4a1cbd040102174123c354ec4b6a40fad1 Closes-Bug: #1672724 --- openstack/network/v2/port.py | 6 ++++++ openstack/tests/unit/network/v2/test_port.py | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index 63ef801a4..6234ea452 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -117,5 +117,11 @@ class Port(resource.Resource): #: If you specify both a subnet ID and an IP address, OpenStack networking #: tries to allocate the address to the port. subnet_id = resource.Body('subnet_id') + #: Read-only. The trunk referring to this parent port and its subports. + #: Present for trunk parent ports if ``trunk-details`` extension is loaded. + #: *Type: dict with keys: trunk_id, sub_ports. + #: sub_ports is a list of dicts with keys: + #: port_id, segmentation_type, segmentation_id, mac_address* + trunk_details = resource.Body('trunk_details', type=dict) #: Timestamp when the port was last updated. updated_at = resource.Body('updated_at') diff --git a/openstack/tests/unit/network/v2/test_port.py b/openstack/tests/unit/network/v2/test_port.py index 160546545..b61418ce7 100644 --- a/openstack/tests/unit/network/v2/test_port.py +++ b/openstack/tests/unit/network/v2/test_port.py @@ -45,6 +45,13 @@ 'subnet_id': '24', 'status': '25', 'tenant_id': '26', + 'trunk_details': { + 'trunk_id': '27', + 'sub_ports': [{ + 'port_id': '28', + 'segmentation_id': 29, + 'segmentation_type': '30', + 'mac_address': '31'}]}, 'updated_at': '2016-07-09T12:14:57.233772', } @@ -96,4 +103,5 @@ def test_make_it(self): self.assertEqual(EXAMPLE['status'], sot.status) self.assertEqual(EXAMPLE['subnet_id'], sot.subnet_id) self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertEqual(EXAMPLE['trunk_details'], sot.trunk_details) self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) From 8ad6941b9dec9c593de6c597143436d0d931c33c Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Fri, 10 Mar 2017 15:43:34 -0800 Subject: [PATCH 1348/3836] change test_endpoints to use requests mock change test_endpoints to use requests mock. Change-Id: I5ba1557af61512334bf85d96bfb90bbc10a7cfd3 --- shade/tests/unit/base.py | 57 ++++ shade/tests/unit/test_endpoints.py | 505 ++++++++++++++++++----------- 2 files changed, 378 insertions(+), 184 deletions(-) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index c28a2d3cc..f54f102d8 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -58,6 +58,18 @@ 'service_id, service_name, service_type, description, enabled, ' 'json_response_v3, json_response_v2, json_request') +_EndpointDataV3 = collections.namedtuple( + 'EndpointData', + 'endpoint_id, service_id, interface, region, url, enabled, ' + 'json_response, json_request') + + +_EndpointDataV2 = collections.namedtuple( + 'EndpointData', + 'endpoint_id, service_id, region, public_url, internal_url, ' + 'admin_url, v3_endpoint_list, json_response, ' + 'json_request') + class BaseTestCase(base.TestCase): @@ -317,6 +329,51 @@ def _get_service_data(self, type=None, name=None, description=None, {'service': response}, {'OS-KSADM:service': response}, request) + def _get_endpoint_v3_data(self, service_id=None, region=None, + url=None, interface=None, enabled=True): + endpoint_id = uuid.uuid4().hex + service_id = service_id or uuid.uuid4().hex + region = region or uuid.uuid4().hex + url = url or 'https://example.com/' + interface = interface or uuid.uuid4().hex + + response = {'id': endpoint_id, 'service_id': service_id, + 'region': region, 'interface': interface, + 'url': url, 'enabled': enabled} + request = response.copy() + request.pop('id') + response['region_id'] = response['region'] + return _EndpointDataV3(endpoint_id, service_id, interface, region, + url, enabled, {'endpoint': response}, + {'endpoint': request}) + + def _get_endpoint_v2_data(self, service_id=None, region=None, + public_url=None, admin_url=None, + internal_url=None): + endpoint_id = uuid.uuid4().hex + service_id = service_id or uuid.uuid4().hex + region = region or uuid.uuid4().hex + response = {'id': endpoint_id, 'service_id': service_id, + 'region': region} + v3_endpoints = {} + if admin_url: + response['adminURL'] = admin_url + v3_endpoints['admin'] = self._get_endpoint_v3_data( + service_id, region, public_url, interface='admin') + if internal_url: + response['internalURL'] = internal_url + v3_endpoints['internal'] = self._get_endpoint_v3_data( + service_id, region, internal_url, interface='internal') + if public_url: + response['publicURL'] = public_url + v3_endpoints['public'] = self._get_endpoint_v3_data( + service_id, region, public_url, interface='public') + request = response.copy() + request.pop('id') + return _EndpointDataV2(endpoint_id, service_id, region, public_url, + internal_url, admin_url, v3_endpoints, + {'endpoint': response}, {'endpoint': request}) + def use_keystone_v3(self): self.adapter = self.useFixture(rm_fixture.Fixture()) self.calls = [] diff --git a/shade/tests/unit/test_endpoints.py b/shade/tests/unit/test_endpoints.py index 8e2a8efce..762341115 100644 --- a/shade/tests/unit/test_endpoints.py +++ b/shade/tests/unit/test_endpoints.py @@ -19,82 +19,87 @@ Tests Keystone endpoints commands. """ -from mock import patch -import os_client_config -from shade import OperatorCloud +import uuid + from shade.exc import OpenStackCloudException from shade.exc import OpenStackCloudUnavailableFeature -from shade.tests.fakes import FakeEndpoint -from shade.tests.fakes import FakeEndpointv3 from shade.tests.unit import base - - -class TestCloudEndpoints(base.TestCase): - mock_endpoints = [ - {'id': 'id1', 'service_id': 'sid1', 'region': 'region1', - 'publicurl': 'purl1', 'internalurl': None, 'adminurl': None}, - {'id': 'id2', 'service_id': 'sid2', 'region': 'region1', - 'publicurl': 'purl2', 'internalurl': None, 'adminurl': None}, - {'id': 'id3', 'service_id': 'sid3', 'region': 'region2', - 'publicurl': 'purl3', 'internalurl': 'iurl3', 'adminurl': 'aurl3'} - ] - mock_endpoints_v3 = [ - {'id': 'id1_v3', 'service_id': 'sid1', 'region': 'region1', - 'url': 'url1', 'interface': 'public'}, - {'id': 'id2_v3', 'service_id': 'sid1', 'region': 'region1', - 'url': 'url2', 'interface': 'admin'}, - {'id': 'id3_v3', 'service_id': 'sid1', 'region': 'region1', - 'url': 'url3', 'interface': 'internal'} - ] - - def setUp(self): - super(TestCloudEndpoints, self).setUp() - self.mock_ks_endpoints = \ - [FakeEndpoint(**kwa) for kwa in self.mock_endpoints] - self.mock_ks_endpoints_v3 = \ - [FakeEndpointv3(**kwa) for kwa in self.mock_endpoints_v3] - - @patch.object(OperatorCloud, 'list_services') - @patch.object(OperatorCloud, 'keystone_client') - @patch.object(os_client_config.cloud_config.CloudConfig, 'get_api_version') - def test_create_endpoint_v2(self, mock_api_version, mock_keystone_client, - mock_list_services): - mock_api_version.return_value = '2.0' - mock_list_services.return_value = [ - { - 'id': 'service_id1', - 'name': 'service1', - 'type': 'type1', - 'description': 'desc1' - } - ] - mock_keystone_client.endpoints.create.return_value = \ - self.mock_ks_endpoints[2] +from testtools import matchers + + +class TestCloudEndpoints(base.RequestsMockTestCase): + + def get_mock_url(self, service_type='identity', interface='admin', + resource='endpoints', append=None, base_url_append='v3'): + return super(TestCloudEndpoints, self).get_mock_url( + service_type, interface, resource, append, base_url_append) + + def _dummy_url(self): + return 'https://%s.example.com/' % uuid.uuid4().hex + + def test_create_endpoint_v2(self): + self.use_keystone_v2() + service_data = self._get_service_data() + endpoint_data = self._get_endpoint_v2_data( + service_data.service_id, public_url=self._dummy_url(), + internal_url=self._dummy_url(), admin_url=self._dummy_url()) + other_endpoint_data = self._get_endpoint_v2_data( + service_data.service_id, public_url=self._dummy_url()) + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + resource='services', base_url_append='OS-KSADM'), + status_code=200, + json={'OS-KSADM:services': [ + service_data.json_response_v2['OS-KSADM:service']]}), + dict(method='POST', + uri=self.get_mock_url(base_url_append=None), + status_code=200, + json=endpoint_data.json_response, + validate=endpoint_data.json_request), + dict(method='GET', + uri=self.get_mock_url( + resource='services', base_url_append='OS-KSADM'), + status_code=200, + json={'OS-KSADM:services': [ + service_data.json_response_v2['OS-KSADM:service']]}), + # NOTE(notmorgan): There is a stupid happening here, we do two + # gets on the services for some insane reason (read: keystoneclient + # is bad and should feel bad). + dict(method='GET', + uri=self.get_mock_url( + resource='services', base_url_append='OS-KSADM'), + status_code=200, + json={'OS-KSADM:services': [ + service_data.json_response_v2['OS-KSADM:service']]}), + dict(method='POST', + uri=self.get_mock_url(base_url_append=None), + status_code=200, + json=other_endpoint_data.json_response, + validate=other_endpoint_data.json_request) + ]) endpoints = self.op_cloud.create_endpoint( - service_name_or_id='service1', - region='mock_region', - public_url='mock_public_url', - internal_url='mock_internal_url', - admin_url='mock_admin_url' + service_name_or_id=service_data.service_id, + region=endpoint_data.region, + public_url=endpoint_data.public_url, + internal_url=endpoint_data.internal_url, + admin_url=endpoint_data.admin_url ) - mock_keystone_client.endpoints.create.assert_called_with( - service_id='service_id1', - region='mock_region', - publicurl='mock_public_url', - internalurl='mock_internal_url', - adminurl='mock_admin_url', - ) - - # test keys and values are correct - for k, v in self.mock_endpoints[2].items(): - self.assertEqual(v, endpoints[0].get(k)) + self.assertThat(endpoints[0].id, + matchers.Equals(endpoint_data.endpoint_id)) + self.assertThat(endpoints[0].region, + matchers.Equals(endpoint_data.region)) + self.assertThat(endpoints[0].publicURL, + matchers.Equals(endpoint_data.public_url)) + self.assertThat(endpoints[0].internalURL, + matchers.Equals(endpoint_data.internal_url)) + self.assertThat(endpoints[0].adminURL, + matchers.Equals(endpoint_data.admin_url)) # test v3 semantics on v2.0 endpoint - mock_keystone_client.endpoints.create.return_value = \ - self.mock_ks_endpoints[0] - self.assertRaises(OpenStackCloudException, self.op_cloud.create_endpoint, service_name_or_id='service1', @@ -102,158 +107,290 @@ def test_create_endpoint_v2(self, mock_api_version, mock_keystone_client, url='admin') endpoints_3on2 = self.op_cloud.create_endpoint( - service_name_or_id='service1', - region='mock_region', + service_name_or_id=service_data.service_id, + region=endpoint_data.region, interface='public', - url='mock_public_url' + url=endpoint_data.public_url ) # test keys and values are correct - for k, v in self.mock_endpoints[0].items(): - self.assertEqual(v, endpoints_3on2[0].get(k)) - - @patch.object(OperatorCloud, 'list_services') - @patch.object(OperatorCloud, 'keystone_client') - @patch.object(os_client_config.cloud_config.CloudConfig, 'get_api_version') - def test_create_endpoint_v3(self, mock_api_version, mock_keystone_client, - mock_list_services): - mock_api_version.return_value = '3' - mock_list_services.return_value = [ - { - 'id': 'service_id1', - 'name': 'service1', - 'type': 'type1', - 'description': 'desc1' - } - ] - mock_keystone_client.endpoints.create.return_value = \ - self.mock_ks_endpoints_v3[0] + self.assertThat( + endpoints_3on2[0].region, + matchers.Equals(other_endpoint_data.region)) + self.assertThat( + endpoints_3on2[0].publicURL, + matchers.Equals(other_endpoint_data.public_url)) + self.assertThat(endpoints_3on2[0].get('internalURL'), + matchers.Equals(None)) + self.assertThat(endpoints_3on2[0].get('adminURL'), + matchers.Equals(None)) + self.assert_calls() + + def test_create_endpoint_v3(self): + self._add_discovery_uri_call() + service_data = self._get_service_data() + public_endpoint_data = self._get_endpoint_v3_data( + service_id=service_data.service_id, interface='public', + url=self._dummy_url()) + public_endpoint_data_disabled = self._get_endpoint_v3_data( + service_id=service_data.service_id, interface='public', + url=self._dummy_url(), enabled=False) + admin_endpoint_data = self._get_endpoint_v3_data( + service_id=service_data.service_id, interface='admin', + url=self._dummy_url(), region=public_endpoint_data.region) + internal_endpoint_data = self._get_endpoint_v3_data( + service_id=service_data.service_id, interface='internal', + url=self._dummy_url(), region=public_endpoint_data.region) + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='services'), + status_code=200, + json={'services': [ + service_data.json_response_v3['service']]}), + dict(method='POST', + uri=self.get_mock_url(), + status_code=200, + json=public_endpoint_data_disabled.json_response, + validate=public_endpoint_data_disabled.json_request), + dict(method='GET', + uri=self.get_mock_url( + append=[public_endpoint_data_disabled.endpoint_id]), + status_code=200, + json=public_endpoint_data_disabled.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='services'), + status_code=200, + json={'services': [ + service_data.json_response_v3['service']]}), + dict(method='POST', + uri=self.get_mock_url(), + status_code=200, + json=public_endpoint_data.json_response, + validate=public_endpoint_data.json_request), + dict(method='GET', + uri=self.get_mock_url( + append=[public_endpoint_data.endpoint_id]), + status_code=200, + json=public_endpoint_data.json_response), + dict(method='POST', + uri=self.get_mock_url(), + status_code=200, + json=internal_endpoint_data.json_response, + validate=internal_endpoint_data.json_request), + dict(method='GET', + uri=self.get_mock_url( + append=[internal_endpoint_data.endpoint_id]), + status_code=200, + json=internal_endpoint_data.json_response), + dict(method='POST', + uri=self.get_mock_url(), + status_code=200, + json=admin_endpoint_data.json_response, + validate=admin_endpoint_data.json_request), + dict(method='GET', + uri=self.get_mock_url( + append=[admin_endpoint_data.endpoint_id]), + status_code=200, + json=admin_endpoint_data.json_response), + ]) endpoints = self.op_cloud.create_endpoint( - service_name_or_id='service1', - region='mock_region', - url='mock_url', - interface='mock_interface', - enabled=False - ) - mock_keystone_client.endpoints.create.assert_called_with( - service='service_id1', - region='mock_region', - url='mock_url', - interface='mock_interface', - enabled=False - ) - - # test keys and values are correct - for k, v in self.mock_endpoints_v3[0].items(): - self.assertEqual(v, endpoints[0].get(k)) - - # test v2.0 semantics on v3 endpoint - mock_keystone_client.endpoints.create.side_effect = \ - self.mock_ks_endpoints_v3 + service_name_or_id=service_data.service_id, + region=public_endpoint_data_disabled.region, + url=public_endpoint_data_disabled.url, + interface=public_endpoint_data_disabled.interface, + enabled=False) + + # Test endpoint values + self.assertThat( + endpoints[0].id, + matchers.Equals(public_endpoint_data_disabled.endpoint_id)) + self.assertThat(endpoints[0].url, + matchers.Equals(public_endpoint_data_disabled.url)) + self.assertThat( + endpoints[0].interface, + matchers.Equals(public_endpoint_data_disabled.interface)) + self.assertThat( + endpoints[0].region, + matchers.Equals(public_endpoint_data_disabled.region)) + self.assertThat( + endpoints[0].region_id, + matchers.Equals(public_endpoint_data_disabled.region)) + self.assertThat(endpoints[0].enabled, + matchers.Equals(public_endpoint_data_disabled.enabled)) endpoints_2on3 = self.op_cloud.create_endpoint( - service_name_or_id='service1', - region='mock_region', - public_url='mock_public_url', - internal_url='mock_internal_url', - admin_url='mock_admin_url', - ) + service_name_or_id=service_data.service_id, + region=public_endpoint_data.region, + public_url=public_endpoint_data.url, + internal_url=internal_endpoint_data.url, + admin_url=admin_endpoint_data.url) # Three endpoints should be returned, public, internal, and admin - self.assertEqual(len(endpoints_2on3), 3) - - # test keys and values are correct - for count in range(len(endpoints_2on3)): - for k, v in self.mock_endpoints_v3[count].items(): - self.assertEqual(v, endpoints_2on3[count].get(k)) - - @patch.object(os_client_config.cloud_config.CloudConfig, 'get_api_version') - def test_update_endpoint_v2(self, mock_api_version): - mock_api_version.return_value = '2.0' - # NOTE(SamYaple): Update endpoint only works with v3 api + self.assertThat(len(endpoints_2on3), matchers.Equals(3)) + + # test keys and values are correct for each endpoint created + for result, reference in zip( + endpoints_2on3, [public_endpoint_data, + internal_endpoint_data, + admin_endpoint_data] + ): + self.assertThat(result.id, matchers.Equals(reference.endpoint_id)) + self.assertThat(result.url, matchers.Equals(reference.url)) + self.assertThat(result.interface, + matchers.Equals(reference.interface)) + self.assertThat(result.region, + matchers.Equals(reference.region)) + self.assertThat(result.enabled, matchers.Equals(reference.enabled)) + self.assert_calls() + + def test_update_endpoint_v2(self): + self.use_keystone_v2() self.assertRaises(OpenStackCloudUnavailableFeature, self.op_cloud.update_endpoint, 'endpoint_id') - @patch.object(OperatorCloud, 'keystone_client') - @patch.object(os_client_config.cloud_config.CloudConfig, 'get_api_version') - def test_update_endpoint_v3(self, mock_api_version, mock_keystone_client): - mock_api_version.return_value = '3' - mock_keystone_client.endpoints.update.return_value = \ - self.mock_ks_endpoints_v3[0] - + def test_update_endpoint_v3(self): + self._add_discovery_uri_call() + service_data = self._get_service_data() + endpoint_data = self._get_endpoint_v3_data( + service_id=service_data.service_id, interface='admin', + enabled=False) + self.register_uris([ + dict(method='PATCH', + uri=self.get_mock_url(append=[endpoint_data.endpoint_id]), + status_code=200, + json=endpoint_data.json_response, + validate=endpoint_data.json_request), + dict(method='GET', + uri=self.get_mock_url(append=[endpoint_data.endpoint_id]), + status_code=200, + json=endpoint_data.json_response) + ]) endpoint = self.op_cloud.update_endpoint( - 'id1', - service_name_or_id='service_id1', - region='mock_region', - url='mock_url', - interface='mock_interface', - enabled=False - ) - mock_keystone_client.endpoints.update.assert_called_with( - endpoint='id1', - service='service_id1', - region='mock_region', - url='mock_url', - interface='mock_interface', + endpoint_data.endpoint_id, + service_name_or_id=service_data.service_id, + region=endpoint_data.region, + url=self._dummy_url(), + interface=endpoint_data.interface, enabled=False ) # test keys and values are correct - for k, v in self.mock_endpoints_v3[0].items(): - self.assertEqual(v, endpoint.get(k)) - - @patch.object(OperatorCloud, 'keystone_client') - def test_list_endpoints(self, mock_keystone_client): - mock_keystone_client.endpoints.list.return_value = \ - self.mock_ks_endpoints + self.assertThat(endpoint.id, + matchers.Equals(endpoint_data.endpoint_id)) + self.assertThat(endpoint.service_id, + matchers.Equals(service_data.service_id)) + self.assertThat(endpoint.url, + matchers.Equals(endpoint_data.url)) + self.assertThat(endpoint.interface, + matchers.Equals(endpoint_data.interface)) + + self.assert_calls() + + def test_list_endpoints(self): + self._add_discovery_uri_call() + endpoints_data = [self._get_endpoint_v3_data() for e in range(1, 10)] + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'endpoints': [e.json_response['endpoint'] + for e in endpoints_data]}) + ]) endpoints = self.op_cloud.list_endpoints() - mock_keystone_client.endpoints.list.assert_called_with() - # test we are getting exactly len(self.mock_endpoints) elements - self.assertEqual(len(self.mock_endpoints), len(endpoints)) + self.assertThat(len(endpoints), matchers.Equals(len(endpoints_data))) # test keys and values are correct - for mock_endpoint in self.mock_endpoints: - found = False - for e in endpoints: - if e['id'] == mock_endpoint['id']: - found = True - for k, v in mock_endpoint.items(): - self.assertEqual(v, e.get(k)) - break - self.assertTrue( - found, msg="endpoint {id} not found!".format( - id=mock_endpoint['id'])) - - @patch.object(OperatorCloud, 'keystone_client') - def test_search_endpoints(self, mock_keystone_client): - mock_keystone_client.endpoints.list.return_value = \ - self.mock_ks_endpoints + for i, ep in enumerate(endpoints_data): + self.assertThat(endpoints[i].id, + matchers.Equals(ep.endpoint_id)) + self.assertThat(endpoints[i].service_id, + matchers.Equals(ep.service_id)) + self.assertThat(endpoints[i].url, + matchers.Equals(ep.url)) + self.assertThat(endpoints[i].interface, + matchers.Equals(ep.interface)) + + self.assert_calls() + + def test_search_endpoints(self): + self._add_discovery_uri_call() + endpoints_data = [self._get_endpoint_v3_data(region='region1') + for e in range(0, 2)] + endpoints_data.extend([self._get_endpoint_v3_data() + for e in range(1, 8)]) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'endpoints': [e.json_response['endpoint'] + for e in endpoints_data]}), + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'endpoints': [e.json_response['endpoint'] + for e in endpoints_data]}), + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'endpoints': [e.json_response['endpoint'] + for e in endpoints_data]}), + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'endpoints': [e.json_response['endpoint'] + for e in endpoints_data]}) + ]) # Search by id - endpoints = self.op_cloud.search_endpoints(id='id3') + endpoints = self.op_cloud.search_endpoints( + id=endpoints_data[-1].endpoint_id) # # test we are getting exactly 1 element self.assertEqual(1, len(endpoints)) - for k, v in self.mock_endpoints[2].items(): - self.assertEqual(v, endpoints[0].get(k)) + self.assertThat(endpoints[0].id, + matchers.Equals(endpoints_data[-1].endpoint_id)) + self.assertThat(endpoints[0].service_id, + matchers.Equals(endpoints_data[-1].service_id)) + self.assertThat(endpoints[0].url, + matchers.Equals(endpoints_data[-1].url)) + self.assertThat(endpoints[0].interface, + matchers.Equals(endpoints_data[-1].interface)) # Not found - endpoints = self.op_cloud.search_endpoints(id='blah!') + endpoints = self.op_cloud.search_endpoints(id='!invalid!') self.assertEqual(0, len(endpoints)) # Multiple matches endpoints = self.op_cloud.search_endpoints( - filters={'region': 'region1'}) + filters={'region_id': 'region1'}) # # test we are getting exactly 2 elements self.assertEqual(2, len(endpoints)) - @patch.object(OperatorCloud, 'keystone_client') - def test_delete_endpoint(self, mock_keystone_client): - mock_keystone_client.endpoints.list.return_value = \ - self.mock_ks_endpoints + # test we are getting the correct response for region/region_id compat + endpoints = self.op_cloud.search_endpoints( + filters={'region': 'region1'}) + # # test we are getting exactly 2 elements, this is v3 + self.assertEqual(2, len(endpoints)) + + self.assert_calls() + + def test_delete_endpoint(self): + self._add_discovery_uri_call() + endpoint_data = self._get_endpoint_v3_data() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'endpoints': [ + endpoint_data.json_response['endpoint']]}), + dict(method='DELETE', + uri=self.get_mock_url(append=[endpoint_data.endpoint_id]), + status_code=204) + ]) # Delete by id - self.op_cloud.delete_endpoint(id='id2') - mock_keystone_client.endpoints.delete.assert_called_with(id='id2') + self.op_cloud.delete_endpoint(id=endpoint_data.endpoint_id) + self.assert_calls() From d37805e18f510113e2377377cb970f9154d47631 Mon Sep 17 00:00:00 2001 From: Pip Oomen Date: Wed, 15 Mar 2017 19:18:40 +0100 Subject: [PATCH 1349/3836] Expose OS-EXT-SRV-ATTR:{hypervisor_hostname,instance_name} for Server. This patch exposes these properties for the openstack.compute.v2.Server class, making them available for inspection. Change-Id: I4958450e9853ad52e817da382907517dc3b46e6a --- openstack/compute/v2/server.py | 7 +++++++ openstack/tests/unit/compute/v2/test_server.py | 8 +++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 24a579a76..b75ba47c5 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -130,6 +130,13 @@ class Server(resource2.Resource, metadata.MetadataMixin): #: networks parameter, the server attaches to the only network #: created for the current tenant. networks = resource2.Body('networks') + #: The hypervisor host name. Appears in the response for administrative + #: users only. + hypervisor_hostname = resource2.Body('OS-EXT-SRV-ATTR:hypervisor_hostname') + #: The instance name. The Compute API generates the instance name from the + #: instance name template. Appears in the response for administrative users + #: only. + instance_name = resource2.Body('OS-EXT-SRV-ATTR:instance_name') def _action(self, session, body): """Preform server actions given the message body.""" diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 124903a53..88a6a475c 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -51,7 +51,9 @@ 'personality': '28', 'block_device_mapping_v2': {'key': '29'}, 'os:scheduler_hints': {'key': '30'}, - 'user_data': '31' + 'user_data': '31', + 'OS-EXT-SRV-ATTR:hypervisor_hostname': 'hypervisor.example.com', + 'OS-EXT-SRV-ATTR:instance_name': 'instance-00000001' } @@ -140,6 +142,10 @@ def test_make_it(self): sot.block_device_mapping) self.assertEqual(EXAMPLE['os:scheduler_hints'], sot.scheduler_hints) self.assertEqual(EXAMPLE['user_data'], sot.user_data) + self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:hypervisor_hostname'], + sot.hypervisor_hostname) + self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:instance_name'], + sot.instance_name) def test_detail(self): sot = server.ServerDetail() From 1c87c8aa0d8310cc4dd121931a6b3e4b6c8a6171 Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Tue, 14 Mar 2017 15:31:38 -0700 Subject: [PATCH 1350/3836] Convert test_identity_roles to requests mock convert test_identity_roles to requests mock instead of direct client mock. Change-Id: Ia382fa2f0c8ee8cdf9820ee35ed98f304dd4df4d --- shade/tests/unit/base.py | 29 +- shade/tests/unit/test_identity_roles.py | 335 ++++++++++++++++-------- 2 files changed, 255 insertions(+), 109 deletions(-) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index f54f102d8..452f029be 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -58,6 +58,7 @@ 'service_id, service_name, service_type, description, enabled, ' 'json_response_v3, json_response_v2, json_request') + _EndpointDataV3 = collections.namedtuple( 'EndpointData', 'endpoint_id, service_id, interface, region, url, enabled, ' @@ -71,6 +72,13 @@ 'json_request') +# NOTE(notmorgan): Shade does not support domain-specific roles +# This should eventually be fixed if it becomes a main-stream feature. +_RoleData = collections.namedtuple( + 'RoleData', + 'role_id, role_name, json_response, json_request') + + class BaseTestCase(base.TestCase): def setUp(self, cloud_config_fixture='clouds.yaml'): @@ -166,16 +174,20 @@ def setUp(self, cloud_config_fixture='clouds.yaml'): self.__register_uris_called = False def get_mock_url(self, service_type, interface, resource=None, - append=None, base_url_append=None): + append=None, base_url_append=None, + qs_elements=None): endpoint_url = self.cloud.endpoint_for( service_type=service_type, interface=interface) to_join = [endpoint_url] + qs = '' if base_url_append: to_join.append(base_url_append) if resource: to_join.append(resource) to_join.extend(append or []) - return '/'.join(to_join) + if qs_elements is not None: + qs = '?%s' % '&'.join(qs_elements) + return '%(uri)s%(qs)s' % {'uri': '/'.join(to_join), 'qs': qs} def mock_for_keystone_projects(self, project=None, v3=True, list_get=False, id_get=False, @@ -228,12 +240,12 @@ def mock_for_keystone_projects(self, project=None, v3=True, return project_list def _get_project_data(self, project_name=None, enabled=None, - description=None, v3=True): + domain_id=None, description=None, v3=True): project_name = project_name or self.getUniqueString('projectName') project_id = uuid.uuid4().hex response = {'id': project_id, 'name': project_name} request = {'name': project_name} - domain_id = uuid.uuid4().hex if v3 else None + domain_id = (domain_id or uuid.uuid4().hex) if v3 else None if domain_id: request['domain_id'] = domain_id response['domain_id'] = domain_id @@ -374,6 +386,15 @@ def _get_endpoint_v2_data(self, service_id=None, region=None, internal_url, admin_url, v3_endpoints, {'endpoint': response}, {'endpoint': request}) + def _get_role_data(self, role_name=None): + role_id = uuid.uuid4().hex + role_name = role_name or uuid.uuid4().hex + request = {'name': role_name} + response = request.copy() + response['id'] = role_id + return _RoleData(role_id, role_name, {'role': response}, + {'role': request}) + def use_keystone_v3(self): self.adapter = self.useFixture(rm_fixture.Fixture()) self.calls = [] diff --git a/shade/tests/unit/test_identity_roles.py b/shade/tests/unit/test_identity_roles.py index 3582bb334..d7f676c83 100644 --- a/shade/tests/unit/test_identity_roles.py +++ b/shade/tests/unit/test_identity_roles.py @@ -11,15 +11,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import mock import testtools -import os_client_config as occ import shade -from shade import meta -from shade import _utils from shade.tests.unit import base -from shade.tests import fakes +from testtools import matchers RAW_ROLE_ASSIGNMENTS = [ @@ -38,135 +34,264 @@ ] -class TestIdentityRoles(base.TestCase): +class TestIdentityRoles(base.RequestsMockTestCase): - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_list_roles(self, mock_keystone): + def get_mock_url(self, service_type='identity', interface='admin', + resource='roles', append=None, base_url_append='v3', + qs_elements=None): + return super(TestIdentityRoles, self).get_mock_url( + service_type, interface, resource, append, base_url_append, + qs_elements) + + def test_list_roles(self): + self._add_discovery_uri_call() + role_data = self._get_role_data() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'roles': [role_data.json_response['role']]}) + ]) self.op_cloud.list_roles() - self.assertTrue(mock_keystone.roles.list.called) + self.assert_calls() + + def test_get_role_by_name(self): + self._add_discovery_uri_call() + role_data = self._get_role_data() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'roles': [role_data.json_response['role']]}) + ]) + role = self.op_cloud.get_role(role_data.role_name) - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_get_role(self, mock_keystone): - role_obj = fakes.FakeRole(id='1234', name='fake_role') - mock_keystone.roles.list.return_value = [role_obj] + self.assertIsNotNone(role) + self.assertThat(role.id, matchers.Equals(role_data.role_id)) + self.assertThat(role.name, matchers.Equals(role_data.role_name)) + self.assert_calls() - role = self.op_cloud.get_role('fake_role') + def test_get_role_by_id(self): + self._add_discovery_uri_call() + role_data = self._get_role_data() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'roles': [role_data.json_response['role']]}) + ]) + role = self.op_cloud.get_role(role_data.role_id) - self.assertTrue(mock_keystone.roles.list.called) self.assertIsNotNone(role) - self.assertEqual('1234', role['id']) - self.assertEqual('fake_role', role['name']) + self.assertThat(role.id, matchers.Equals(role_data.role_id)) + self.assertThat(role.name, matchers.Equals(role_data.role_name)) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_create_role(self, mock_keystone): - role_name = 'tootsie_roll' - role_obj = fakes.FakeRole(id='1234', name=role_name) - mock_keystone.roles.create.return_value = role_obj + def test_create_role(self): + self._add_discovery_uri_call() + role_data = self._get_role_data() + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url(), + status_code=200, + json=role_data.json_response, + validate=role_data.json_request), + dict(method='GET', + uri=self.get_mock_url(append=[role_data.role_id]), + status_code=200, + json=role_data.json_response) + ]) - role = self.op_cloud.create_role(role_name) + role = self.op_cloud.create_role(role_data.role_name) - mock_keystone.roles.create.assert_called_once_with( - name=role_name - ) self.assertIsNotNone(role) - self.assertEqual(role_name, role['name']) - - @mock.patch.object(shade.OperatorCloud, 'get_role') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_delete_role(self, mock_keystone, mock_get): - role_obj = fakes.FakeRole(id='1234', name='aaa') - mock_get.return_value = meta.obj_to_dict(role_obj) - self.assertTrue(self.op_cloud.delete_role('1234')) - self.assertTrue(mock_keystone.roles.delete.called) - - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_list_role_assignments(self, mock_keystone, mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.role_assignments.list.return_value = RAW_ROLE_ASSIGNMENTS + self.assertThat(role.name, matchers.Equals(role_data.role_name)) + self.assertThat(role.id, matchers.Equals(role_data.role_id)) + self.assert_calls() + + def test_delete_role_by_id(self): + self._add_discovery_uri_call() + role_data = self._get_role_data() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'roles': [role_data.json_response['role']]}), + dict(method='DELETE', + uri=self.get_mock_url(append=[role_data.role_id]), + status_code=204) + ]) + role = self.op_cloud.delete_role(role_data.role_id) + self.assertThat(role, matchers.Equals(True)) + self.assert_calls() + + def test_delete_role_by_name(self): + self._add_discovery_uri_call() + role_data = self._get_role_data() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'roles': [role_data.json_response['role']]}), + dict(method='DELETE', + uri=self.get_mock_url(append=[role_data.role_id]), + status_code=204) + ]) + role = self.op_cloud.delete_role(role_data.role_name) + self.assertThat(role, matchers.Equals(True)) + self.assert_calls() + + def test_list_role_assignments(self): + self._add_discovery_uri_call() + domain_data = self._get_domain_data() + user_data = self._get_user_data(domain_id=domain_data.domain_id) + group_data = self._get_group_data(domain_id=domain_data.domain_id) + project_data = self._get_project_data(domain_id=domain_data.domain_id) + role_data = self._get_role_data() + response = [ + {'links': 'https://example.com', + 'role': {'id': role_data.role_id}, + 'scope': {'domain': {'id': domain_data.domain_id}}, + 'user': {'id': user_data.user_id}}, + {'links': 'https://example.com', + 'role': {'id': role_data.role_id}, + 'scope': {'project': {'id': project_data.project_id}}, + 'group': {'id': group_data.group_id}}, + ] + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments'), + status_code=200, + json={'role_assignments': response}, + complete_qs=True) + ]) ret = self.op_cloud.list_role_assignments() - mock_keystone.role_assignments.list.assert_called_once_with() - normalized_assignments = _utils.normalize_role_assignments( - RAW_ROLE_ASSIGNMENTS - ) - self.assertEqual(normalized_assignments, ret) - - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_list_role_assignments_filters(self, mock_keystone, - mock_api_version): - mock_api_version.return_value = '3' - params = dict(user='123', domain='456', effective=True) - self.op_cloud.list_role_assignments(filters=params) - mock_keystone.role_assignments.list.assert_called_once_with(**params) - - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_list_role_assignments_exception(self, mock_keystone, - mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.role_assignments.list.side_effect = Exception() + self.assertThat(len(ret), matchers.Equals(2)) + self.assertThat(ret[0].user, matchers.Equals(user_data.user_id)) + self.assertThat(ret[0].id, matchers.Equals(role_data.role_id)) + self.assertThat(ret[0].domain, matchers.Equals(domain_data.domain_id)) + self.assertThat(ret[1].group, matchers.Equals(group_data.group_id)) + self.assertThat(ret[1].id, matchers.Equals(role_data.role_id)) + self.assertThat(ret[1].project, + matchers.Equals(project_data.project_id)) + + def test_list_role_assignments_filters(self): + self._add_discovery_uri_call() + domain_data = self._get_domain_data() + user_data = self._get_user_data(domain_id=domain_data.domain_id) + role_data = self._get_role_data() + response = [ + {'links': 'https://example.com', + 'role': {'id': role_data.role_id}, + 'scope': {'domain': {'id': domain_data.domain_id}}, + 'user': {'id': user_data.user_id}} + ] + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=['scope.domain.id=%s' % domain_data.domain_id, + 'user.id=%s' % user_data.user_id, + 'effective=True']), + status_code=200, + json={'role_assignments': response}, + complete_qs=True) + ]) + params = dict(user=user_data.user_id, domain=domain_data.domain_id, + effective=True) + ret = self.op_cloud.list_role_assignments(filters=params) + self.assertThat(len(ret), matchers.Equals(1)) + self.assertThat(ret[0].user, matchers.Equals(user_data.user_id)) + self.assertThat(ret[0].id, matchers.Equals(role_data.role_id)) + self.assertThat(ret[0].domain, matchers.Equals(domain_data.domain_id)) + + def test_list_role_assignments_exception(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='role_assignments'), + status_code=403) + ]) with testtools.ExpectedException( shade.OpenStackCloudException, "Failed to list role assignments" ): self.op_cloud.list_role_assignments() + self.assert_calls() - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_list_role_assignments_keystone_v2(self, mock_keystone, - mock_api_version): - fake_role = fakes.FakeRole(id='1234', name='fake_role') - mock_api_version.return_value = '2.0' - mock_keystone.roles.roles_for_user.return_value = [fake_role] + def test_list_role_assignments_keystone_v2(self): + self.use_keystone_v2() + role_data = self._get_role_data() + user_data = self._get_user_data() + project_data = self._get_project_data(v3=False) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + resource='tenants', + append=[project_data.project_id, + 'users', + user_data.user_id, + 'roles'], + base_url_append=None), + status_code=200, + json={'roles': [role_data.json_response['role']]}) + ]) ret = self.op_cloud.list_role_assignments( filters={ - 'user': '2222', - 'project': '3333'}) - self.assertEqual( - ret, [{ - 'id': fake_role.id, - 'project': '3333', - 'user': '2222'}]) - - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_list_role_assignments_keystone_v2_with_role(self, mock_keystone, - mock_api_version): - fake_role1 = fakes.FakeRole(id='1234', name='fake_role') - fake_role2 = fakes.FakeRole(id='4321', name='fake_role') - mock_api_version.return_value = '2.0' - mock_keystone.roles.roles_for_user.return_value = [fake_role1, - fake_role2] + 'user': user_data.user_id, + 'project': project_data.project_id}) + self.assertThat(len(ret), matchers.Equals(1)) + self.assertThat(ret[0].project, + matchers.Equals(project_data.project_id)) + self.assertThat(ret[0].id, matchers.Equals(role_data.role_id)) + self.assertThat(ret[0].user, matchers.Equals(user_data.user_id)) + self.assert_calls() + + def test_list_role_assignments_keystone_v2_with_role(self): + self.use_keystone_v2() + roles_data = [self._get_role_data() for r in range(0, 2)] + user_data = self._get_user_data() + project_data = self._get_project_data(v3=False) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + resource='tenants', + append=[project_data.project_id, + 'users', + user_data.user_id, + 'roles'], + base_url_append=None), + status_code=200, + json={'roles': [r.json_response['role'] for r in roles_data]}) + ]) ret = self.op_cloud.list_role_assignments( filters={ - 'role': fake_role1.id, - 'user': '2222', - 'project': '3333'}) - self.assertEqual( - ret, [{ - 'id': fake_role1.id, - 'project': '3333', - 'user': '2222'}]) - - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_list_role_assignments_exception_v2(self, mock_keystone, - mock_api_version): - mock_api_version.return_value = '2.0' + 'role': roles_data[0].role_id, + 'user': user_data.user_id, + 'project': project_data.project_id}) + self.assertThat(len(ret), matchers.Equals(1)) + self.assertThat(ret[0].project, + matchers.Equals(project_data.project_id)) + self.assertThat(ret[0].id, matchers.Equals(roles_data[0].role_id)) + self.assertThat(ret[0].user, matchers.Equals(user_data.user_id)) + self.assert_calls() + + def test_list_role_assignments_exception_v2(self): + self.use_keystone_v2() with testtools.ExpectedException( shade.OpenStackCloudException, "Must provide project and user for keystone v2" ): self.op_cloud.list_role_assignments() + self.assert_calls() - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(shade.OpenStackCloud, 'keystone_client') - def test_list_role_assignments_exception_v2_no_project(self, mock_keystone, - mock_api_version): - mock_api_version.return_value = '2.0' + def test_list_role_assignments_exception_v2_no_project(self): + self.use_keystone_v2() with testtools.ExpectedException( shade.OpenStackCloudException, "Must provide project and user for keystone v2" ): self.op_cloud.list_role_assignments(filters={'user': '12345'}) + self.assert_calls() From db45323809b208cc098f2a60fd9ddf4b66790ea9 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 16 Mar 2017 12:17:51 +0100 Subject: [PATCH 1351/3836] Update sphinx and turn on warnings-is-error In order to do that, fix the warnings that are now errors. Change-Id: I1e582a6763fc82496e4ab33f60ced193b0534f28 --- doc/source/model.rst | 6 +-- ...removed-glanceclient-105c7fba9481b9be.yaml | 2 +- setup.cfg | 1 + shade/openstackcloud.py | 50 ++++++++++++------- shade/operatorcloud.py | 32 ++++++------ test-requirements.txt | 2 +- 6 files changed, 53 insertions(+), 40 deletions(-) diff --git a/doc/source/model.rst b/doc/source/model.rst index 4941cf506..37aa5d1db 100644 --- a/doc/source/model.rst +++ b/doc/source/model.rst @@ -89,7 +89,7 @@ A flavor for a Nova Server. Flavor Access ------- +------------- An access entry for a Nova Flavor. @@ -366,7 +366,7 @@ A volume from cinder. VolumeType ------- +---------- A volume type from cinder. @@ -384,7 +384,7 @@ A volume type from cinder. VolumeTypeAccess ------- +---------------- A volume type access from cinder. diff --git a/releasenotes/notes/removed-glanceclient-105c7fba9481b9be.yaml b/releasenotes/notes/removed-glanceclient-105c7fba9481b9be.yaml index 78559f9a3..157e90e83 100644 --- a/releasenotes/notes/removed-glanceclient-105c7fba9481b9be.yaml +++ b/releasenotes/notes/removed-glanceclient-105c7fba9481b9be.yaml @@ -1,7 +1,7 @@ --- prelude: > This release marks the beginning of the path towards removing all - of the 'python-*client' libraries as dependencies. Subsequent releases + of the 'python-\*client' libraries as dependencies. Subsequent releases should expect to have fewer and fewer library depdencies. upgrade: - Removed glanceclient as a dependency. All glance operations diff --git a/setup.cfg b/setup.cfg index 7e7b2c9a5..6223d6a1a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,6 +26,7 @@ console_scripts = source-dir = doc/source build-dir = doc/build all_files = 1 +warning-is-error = 1 [upload_sphinx] upload-dir = doc/build/html diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index c69f21843..781381b64 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2372,13 +2372,13 @@ def get_keypair(self, name_or_id, filters=None): 'gender': 'Female' } } + OR A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A keypair ``munch.Munch`` or None if no matching keypair is - found. - + found. """ return _utils._get_entity(self.search_keypairs, name_or_id, filters) @@ -2396,12 +2396,13 @@ def get_network(self, name_or_id, filters=None): 'gender': 'Female' } } + OR A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A network ``munch.Munch`` or None if no matching network is - found. + found. """ return _utils._get_entity(self.search_networks, name_or_id, filters) @@ -2420,12 +2421,13 @@ def get_router(self, name_or_id, filters=None): 'gender': 'Female' } } + OR A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A router ``munch.Munch`` or None if no matching router is - found. + found. """ return _utils._get_entity(self.search_routers, name_or_id, filters) @@ -2446,7 +2448,7 @@ def get_subnet(self, name_or_id, filters=None): } :returns: A subnet ``munch.Munch`` or None if no matching subnet is - found. + found. """ return _utils._get_entity(self.search_subnets, name_or_id, filters) @@ -2465,6 +2467,7 @@ def get_port(self, name_or_id, filters=None): 'gender': 'Female' } } + OR A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" @@ -2488,12 +2491,13 @@ def get_volume(self, name_or_id, filters=None): 'gender': 'Female' } } + OR A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A volume ``munch.Munch`` or None if no matching volume is - found. + found. """ return _utils._get_entity(self.search_volumes, name_or_id, filters) @@ -2512,12 +2516,13 @@ def get_volume_type(self, name_or_id, filters=None): 'gender': 'Female' } } + OR A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A volume ``munch.Munch`` or None if no matching volume is - found. + found. """ return _utils._get_entity( @@ -2537,6 +2542,7 @@ def get_flavor(self, name_or_id, filters=None, get_extra=True): 'gender': 'Female' } } + OR A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" @@ -2544,9 +2550,8 @@ def get_flavor(self, name_or_id, filters=None, get_extra=True): Whether or not the list_flavors call should get the extra flavor specs. - :returns: A flavor ``munch.Munch`` or None if no matching flavor is - found. + found. """ search_func = functools.partial( @@ -2567,6 +2572,7 @@ def get_security_group(self, name_or_id, filters=None): 'gender': 'Female' } } + OR A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" @@ -2626,12 +2632,13 @@ def get_server(self, name_or_id=None, filters=None, detailed=False): 'gender': 'Female' } } + OR A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A server ``munch.Munch`` or None if no matching server is - found. + found. """ searchfunc = functools.partial(self.search_servers, @@ -2653,12 +2660,13 @@ def get_server_group(self, name_or_id=None, filters=None): { 'policy': 'affinity', } + OR A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A server groups dict or None if no matching server group - is found. + is found. """ return _utils._get_entity(self.search_server_groups, name_or_id, @@ -2678,6 +2686,7 @@ def get_image(self, name_or_id, filters=None): 'gender': 'Female' } } + OR A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" @@ -2752,12 +2761,13 @@ def get_floating_ip(self, id, filters=None): 'gender': 'Female' } } + OR A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A floating IP ``munch.Munch`` or None if no matching floating - IP is found. + IP is found. """ return _utils._get_entity(self.search_floating_ips, id, filters) @@ -2934,7 +2944,8 @@ def add_router_interface(self, router, subnet_id=None, port_id=None): :param string port_id: The ID of the port to use for the interface :returns: A ``munch.Munch`` with the router ID (ID), - subnet ID (subnet_id), port ID (port_id) and tenant ID (tenant_id). + subnet ID (subnet_id), port ID (port_id) and tenant ID + (tenant_id). :raises: OpenStackCloudException on operation error. """ @@ -4050,13 +4061,13 @@ def get_volume_snapshot(self, name_or_id, filters=None): 'gender': 'Female' } } + OR A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A volume ``munch.Munch`` or None if no matching volume is - found. - + found. """ return _utils._get_entity(self.search_volume_snapshots, name_or_id, filters) @@ -4112,8 +4123,7 @@ def get_volume_backup(self, name_or_id, filters=None): """Get a volume backup by name or ID. :returns: A backup ``munch.Munch`` or None if no matching backup is - found. - + found. """ return _utils._get_entity(self.search_volume_backups, name_or_id, filters) @@ -4137,12 +4147,14 @@ def list_volume_backups(self, detailed=True, search_opts=None): :param bool detailed: Also list details for each entry :param dict search_opts: Search options A dictionary of meta data to use for further filtering. Example:: + { 'name': 'my-volume-backup', 'status': 'available', 'volume_id': 'e126044c-7b4c-43be-a32a-c9cbbc9ddb56', 'all_tenants': 1 } + :returns: A list of volume backups ``munch.Munch``. """ with _utils.shade_exceptions("Error getting a list of backups"): @@ -6908,8 +6920,7 @@ def get_zone(self, name_or_id, filters=None): A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - :returns: A zone dict or None if no matching zone is - found. + :returns: A zone dict or None if no matching zone is found. """ return _utils._get_entity(self.search_zones, name_or_id, filters) @@ -7174,6 +7185,7 @@ def get_cluster_template(self, name_or_id, filters=None, detail=False): 'gender': 'Female' } } + OR A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 53a1029da..1b98ee60c 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1426,23 +1426,23 @@ def _keystone_v2_role_assignments(self, user, project=None, def list_role_assignments(self, filters=None): """List Keystone role assignments - :param dict filters: Dict of filter conditions. Acceptable keys are:: - - - 'user' (string) - User ID to be used as query filter. - - 'group' (string) - Group ID to be used as query filter. - - 'project' (string) - Project ID to be used as query filter. - - 'domain' (string) - Domain ID to be used as query filter. - - 'role' (string) - Role ID to be used as query filter. - - 'os_inherit_extension_inherited_to' (string) - Return inherited + :param dict filters: Dict of filter conditions. Acceptable keys are: + + * 'user' (string) - User ID to be used as query filter. + * 'group' (string) - Group ID to be used as query filter. + * 'project' (string) - Project ID to be used as query filter. + * 'domain' (string) - Domain ID to be used as query filter. + * 'role' (string) - Role ID to be used as query filter. + * 'os_inherit_extension_inherited_to' (string) - Return inherited role assignments for either 'projects' or 'domains' - - 'effective' (boolean) - Return effective role assignments. - - 'include_subtree' (boolean) - Include subtree + * 'effective' (boolean) - Return effective role assignments. + * 'include_subtree' (boolean) - Include subtree 'user' and 'group' are mutually exclusive, as are 'domain' and 'project'. - NOTE: For keystone v2, only user, project, and role are used. - Project and user are both required in filters. + NOTE: For keystone v2, only user, project, and role are used. + Project and user are both required in filters. :returns: a list of ``munch.Munch`` containing the role assignment description. Contains the following attributes:: @@ -1856,7 +1856,7 @@ def get_aggregate(self, name_or_id, filters=None): } :returns: An aggregate dict or None if no matching aggregate is - found. + found. """ return _utils._get_entity(self.search_aggregates, name_or_id, filters) @@ -2099,7 +2099,7 @@ def delete_compute_quotas(self, name_or_id): :param name_or_id: project name or id :raises: OpenStackCloudException if it's not a valid project or the - nova client call failed + nova client call failed :returns: dict with the quotas """ @@ -2180,7 +2180,7 @@ def delete_volume_quotas(self, name_or_id): :param name_or_id: project name or id :raises: OpenStackCloudException if it's not a valid project or the - cinder client call failed + cinder client call failed :returns: dict with the quotas """ @@ -2233,7 +2233,7 @@ def delete_network_quotas(self, name_or_id): :param name_or_id: project name or id :raises: OpenStackCloudException if it's not a valid project or the - network client call failed + network client call failed :returns: dict with the quotas """ diff --git a/test-requirements.txt b/test-requirements.txt index b3cd3ae07..e7aa54668 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -6,7 +6,7 @@ mock>=1.0 python-subunit oslosphinx>=2.2.0 # Apache-2.0 requests-mock -sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 +sphinx>=1.5.0 testrepository>=0.0.17 testscenarios>=0.4,<0.5 testtools>=0.9.32 From d79de9b860dbf255f3fa28692aed716e0beb8d4d Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Thu, 16 Mar 2017 13:46:59 -0400 Subject: [PATCH 1352/3836] add separate releasenotes build Add the sphinx files needed for a separate release notes document build. Change-Id: I176e70614051e0d830fb59b05da2248bd484e57a Signed-off-by: Doug Hellmann --- releasenotes/source/_static/.placeholder | 0 releasenotes/source/_templates/.placeholder | 0 releasenotes/source/conf.py | 279 ++++++++++++++++++++ releasenotes/source/index.rst | 9 + releasenotes/source/mainline.rst | 5 + releasenotes/source/unreleased.rst | 11 + tox.ini | 3 + 7 files changed, 307 insertions(+) create mode 100644 releasenotes/source/_static/.placeholder create mode 100644 releasenotes/source/_templates/.placeholder create mode 100644 releasenotes/source/conf.py create mode 100644 releasenotes/source/index.rst create mode 100644 releasenotes/source/mainline.rst create mode 100644 releasenotes/source/unreleased.rst diff --git a/releasenotes/source/_static/.placeholder b/releasenotes/source/_static/.placeholder new file mode 100644 index 000000000..e69de29bb diff --git a/releasenotes/source/_templates/.placeholder b/releasenotes/source/_templates/.placeholder new file mode 100644 index 000000000..e69de29bb diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py new file mode 100644 index 000000000..1abd91cc2 --- /dev/null +++ b/releasenotes/source/conf.py @@ -0,0 +1,279 @@ +# -*- 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. + +# oslo.config Release Notes documentation build configuration file, created by +# sphinx-quickstart on Tue Nov 3 17:40:50 2015. +# +# 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. +# 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 = [ + 'oslosphinx', + 'reno.sphinxext', +] + +# 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'Shade Release Notes' +copyright = u'2017, Shade Developers' + +# 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. +# +import pbr.version +version_info = pbr.version.VersionInfo('shade') +# The full version, including alpha/beta/rc tags. +release = version_info.version_string_with_vcs() +# The short X.Y version. +version = version_info.canonical_version_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 = [] + +# 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 = 'default' + +# 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 = [] + +# 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 '', 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 = 'shadeReleaseNotesdoc' + + +# -- 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', 'shadeReleaseNotes.tex', + u'Shade Release Notes Documentation', + u'Shade 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', 'shadereleasenotes', + u'shade Release Notes Documentation', + [u'shade 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', 'shadeReleaseNotes', + u'shade Release Notes Documentation', + u'shade Developers', 'shadeReleaseNotes', + u'A client library for interacting with OpenStack clouds', + u'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/'] diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst new file mode 100644 index 000000000..353e35068 --- /dev/null +++ b/releasenotes/source/index.rst @@ -0,0 +1,9 @@ +===================== + Shade Release Notes +===================== + + .. toctree:: + :maxdepth: 1 + + mainline + unreleased diff --git a/releasenotes/source/mainline.rst b/releasenotes/source/mainline.rst new file mode 100644 index 000000000..065da7150 --- /dev/null +++ b/releasenotes/source/mainline.rst @@ -0,0 +1,5 @@ +========================= + Mainline Release Series +========================= + +.. release-notes:: diff --git a/releasenotes/source/unreleased.rst b/releasenotes/source/unreleased.rst new file mode 100644 index 000000000..abed3d2e3 --- /dev/null +++ b/releasenotes/source/unreleased.rst @@ -0,0 +1,11 @@ +===================== + Unreleased Versions +===================== + +.. NOTE(dhellmann): The earliest-version field is set to avoid + duplicating *all* of the history on this page. When we start + creating stable branches the history should be truncated + automatically and we can remove the setting. + +.. release-notes:: + :earliest-version: 1.17.0 diff --git a/tox.ini b/tox.ini index 94b0bec65..3a82b0c17 100644 --- a/tox.ini +++ b/tox.ini @@ -63,6 +63,9 @@ skip_install = True deps = -r{toxinidir}/test-requirements.txt commands = python setup.py build_sphinx +[testenv:releasenotes] +commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html + [flake8] # Infra does not follow hacking, nor the broken E12* things # The string of H ignores is because there are some useful checks From ca292847ef5cc3e6be0ef3e0ea3bfa35905fa65d Mon Sep 17 00:00:00 2001 From: tengqm Date: Thu, 16 Mar 2017 23:01:10 -0400 Subject: [PATCH 1353/3836] Avoid imports in openstack/__init__.py These imports are creating problems when installing sdk. The problem might be caused by new pbr logics. However, I think fixing them from SDK side is much easier. Change-Id: I2f028e383d790edd1c28dea74056b96e87fe285c Related-Bug: #1672238 --- openstack/__init__.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/openstack/__init__.py b/openstack/__init__.py index f23de5312..e69de29bb 100644 --- a/openstack/__init__.py +++ b/openstack/__init__.py @@ -1,16 +0,0 @@ -# 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 connection # NOQA -from openstack import exceptions # NOQA -from openstack import profile # NOQA -from openstack import utils # NOQA From 23edc31de8cb575d3c58cd8fb9b590f5c6062ecb Mon Sep 17 00:00:00 2001 From: tengqm Date: Thu, 16 Mar 2017 22:59:41 -0400 Subject: [PATCH 1354/3836] Trivial: fix Template resource in orchestration Change-Id: I347e4e7f49c956615eea141df4fe8f3bbc59472e --- openstack/orchestration/v1/template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/orchestration/v1/template.py b/openstack/orchestration/v1/template.py index be2752dd5..5c8227261 100644 --- a/openstack/orchestration/v1/template.py +++ b/openstack/orchestration/v1/template.py @@ -22,7 +22,7 @@ class Template(resource.Resource): # capabilities allow_create = False allow_list = False - allow_retrieve = False + allow_get = False allow_delete = False allow_update = False From e4d550e7a45ba940dd8dfb3e6e1529c7c839b979 Mon Sep 17 00:00:00 2001 From: tengqm Date: Fri, 17 Mar 2017 01:53:48 -0400 Subject: [PATCH 1355/3836] StackTemplate resource for orchestration This adds the StackTemplate resource for orchestration service (heat) v1. Change-Id: Ic4ea1c58b8c43b2e4217b4a96195b338a3f4310b --- openstack/orchestration/v1/_proxy.py | 19 +++++++ openstack/orchestration/v1/stack_template.py | 44 +++++++++++++++ .../tests/unit/orchestration/v1/test_proxy.py | 31 +++++++++++ .../orchestration/v1/test_stack_template.py | 55 +++++++++++++++++++ 4 files changed, 149 insertions(+) create mode 100644 openstack/orchestration/v1/stack_template.py create mode 100644 openstack/tests/unit/orchestration/v1/test_stack_template.py diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index 560219da2..ec6f5208a 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -15,6 +15,7 @@ from openstack.orchestration.v1 import software_config as _sc from openstack.orchestration.v1 import software_deployment as _sd from openstack.orchestration.v1 import stack as _stack +from openstack.orchestration.v1 import stack_template as _stack_template from openstack.orchestration.v1 import template as _template from openstack import proxy2 @@ -124,6 +125,24 @@ def check_stack(self, stack): stk_obj.check(self._session) + def get_stack_template(self, stack): + """Get a single stack + + :param stack: The value can be the ID of a stack or a + :class:`~openstack.orchestration.v1.stack.Stack` instance. + + :returns: One :class:`~openstack.orchestration.v1.stack.Stack` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + if isinstance(stack, _stack.Stack): + obj = stack + else: + obj = self._find(_stack.Stack, stack, ignore_missing=False) + + return self._get(_stack_template.StackTemplate, requires_id=False, + stack_name=obj.name, stack_id=obj.id) + def resources(self, stack, **query): """Return a generator of resources diff --git a/openstack/orchestration/v1/stack_template.py b/openstack/orchestration/v1/stack_template.py new file mode 100644 index 000000000..35b2db4cb --- /dev/null +++ b/openstack/orchestration/v1/stack_template.py @@ -0,0 +1,44 @@ +# 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.orchestration import orchestration_service +from openstack import resource2 as resource + + +class StackTemplate(resource.Resource): + + service = orchestration_service.OrchestrationService() + base_path = "/stacks/%(stack_name)s/%(stack_id)s/template" + + # capabilities + allow_create = False + allow_list = False + allow_get = True + allow_delete = False + allow_update = False + + # Properties + #: Name of the stack where the template is referenced. + stack_name = resource.URI('stack_name') + #: ID of the stack where the template is referenced. + stack_id = resource.URI('stack_id') + #: The description specified in the template + description = resource.Body('Description') + #: The version of the orchestration HOT template. + heat_template_version = resource.Body('heat_template_version') + #: Key and value that contain output data. + outputs = resource.Body('outputs', type=dict) + #: Key and value pairs that contain template parameters + parameters = resource.Body('parameters', type=dict) + #: Key and value pairs that contain definition of resources in the + #: template + resources = resource.Body('resources', type=dict) diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index cac11515d..593a3b8a8 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -19,6 +19,7 @@ from openstack.orchestration.v1 import software_config as sc from openstack.orchestration.v1 import software_deployment as sd from openstack.orchestration.v1 import stack +from openstack.orchestration.v1 import stack_template from openstack.orchestration.v1 import template from openstack.tests.unit import test_proxy_base2 @@ -74,6 +75,36 @@ def test_check_stack_with_stack_ID(self, mock_stack): mock_stack.assert_called_once_with(id='FAKE_ID') stk.check.assert_called_once_with(self.proxy._session) + @mock.patch.object(stack.Stack, 'find') + def test_get_stack_template_with_stack_identity(self, mock_find): + stack_id = '1234' + stack_name = 'test_stack' + stk = stack.Stack(id=stack_id, name=stack_name) + mock_find.return_value = stk + + self._verify2('openstack.proxy2.BaseProxy._get', + self.proxy.get_stack_template, + method_args=['IDENTITY'], + expected_args=[stack_template.StackTemplate], + expected_kwargs={'requires_id': False, + 'stack_name': stack_name, + 'stack_id': stack_id}) + mock_find.assert_called_once_with(mock.ANY, 'IDENTITY', + ignore_missing=False) + + def test_get_stack_template_with_stack_object(self): + stack_id = '1234' + stack_name = 'test_stack' + stk = stack.Stack(id=stack_id, name=stack_name) + + self._verify2('openstack.proxy2.BaseProxy._get', + self.proxy.get_stack_template, + method_args=[stk], + expected_args=[stack_template.StackTemplate], + expected_kwargs={'requires_id': False, + 'stack_name': stack_name, + 'stack_id': stack_id}) + @mock.patch.object(stack.Stack, 'find') def test_resources_with_stack_object(self, mock_find): stack_id = '1234' diff --git a/openstack/tests/unit/orchestration/v1/test_stack_template.py b/openstack/tests/unit/orchestration/v1/test_stack_template.py new file mode 100644 index 000000000..592d644ee --- /dev/null +++ b/openstack/tests/unit/orchestration/v1/test_stack_template.py @@ -0,0 +1,55 @@ +# 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 testtools + +from openstack.orchestration.v1 import stack_template + + +FAKE = { + 'description': 'template description', + 'heat_template_version': '2014-10-16', + 'parameters': { + 'key_name': { + 'type': 'string' + } + }, + 'resources': { + 'resource1': { + 'type': 'ResourceType' + } + }, + 'outputs': { + 'key1': 'value1' + } +} + + +class TestStackTemplate(testtools.TestCase): + + def test_basic(self): + sot = stack_template.StackTemplate() + self.assertEqual('orchestration', sot.service.service_type) + self.assertFalse(sot.allow_create) + self.assertTrue(sot.allow_get) + self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_delete) + self.assertFalse(sot.allow_list) + + def test_make_it(self): + sot = stack_template.StackTemplate(**FAKE) + self.assertEqual(FAKE['description'], sot.description) + self.assertEqual(FAKE['heat_template_version'], + sot.heat_template_version) + self.assertEqual(FAKE['outputs'], sot.outputs) + self.assertEqual(FAKE['parameters'], sot.parameters) + self.assertEqual(FAKE['resources'], sot.resources) From feaa7cc7d597b9b04c4c4ebc06f786aebd657559 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Mon, 13 Mar 2017 10:34:27 -0400 Subject: [PATCH 1356/3836] Shift some compute attributes within request body The scheduler_hints attribute exists as a top-level key rather than under the "server" key like most other things, so if we have it set, shift it out of the resource_key body and into its own top-level key. The availability_zone attribute exists with two different names, depending on whether it's in a request or a response. When we're preparing a request, include it in the request body as availability_zone, otherwise expect OS-EXT-AZ:availability_zone in the response body such as for a GET. The same goes for the user_data attribute, which exists as OS-EXT-SRV-ATTR:user_data in responses. Change-Id: Ifc261f982f8a13f8ab467350615e08f268a22049 Closes-Bug: 1671073 Closes-Bug: 1671886 --- openstack/__init__.py | 16 --------- openstack/compute/v2/server.py | 35 +++++++++++++++++-- .../tests/unit/compute/v2/test_server.py | 34 +++++++++++++++--- 3 files changed, 62 insertions(+), 23 deletions(-) diff --git a/openstack/__init__.py b/openstack/__init__.py index f23de5312..e69de29bb 100644 --- a/openstack/__init__.py +++ b/openstack/__init__.py @@ -1,16 +0,0 @@ -# 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 connection # NOQA -from openstack import exceptions # NOQA -from openstack import profile # NOQA -from openstack import utils # NOQA diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index b75ba47c5..8a91fee07 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -119,12 +119,12 @@ class Server(resource2.Resource, metadata.MetadataMixin): personality = resource2.Body('personality') #: Configuration information or scripts to use upon launch. #: Must be Base64 encoded. - user_data = resource2.Body('user_data') + user_data = resource2.Body('OS-EXT-SRV-ATTR:user_data') #: Enables fine grained control of the block device mapping for an #: instance. This is typically used for booting servers from volumes. block_device_mapping = resource2.Body('block_device_mapping_v2', type=dict) #: The dictionary of data to send to the scheduler. - scheduler_hints = resource2.Body('os:scheduler_hints', type=dict) + scheduler_hints = resource2.Body('OS-SCH-HNT:scheduler_hints', type=dict) #: A networks object. Required parameter when there are multiple #: networks defined for the tenant. When you do not specify the #: networks parameter, the server attaches to the only network @@ -138,6 +138,37 @@ class Server(resource2.Resource, metadata.MetadataMixin): #: only. instance_name = resource2.Body('OS-EXT-SRV-ATTR:instance_name') + def _prepare_request(self, requires_id=True, prepend_key=True): + request = super(Server, self)._prepare_request(requires_id=requires_id, + prepend_key=prepend_key) + + server_body = request.body[self.resource_key] + + # Some names exist without prefix on requests but with a prefix + # on responses. If we find that we've populated one of these + # attributes with something and then go to make a request, swap out + # the name to the bare version. + + # Availability Zones exist with a prefix on response, but not request + az_key = "OS-EXT-AZ:availability_zone" + if az_key in server_body: + server_body["availability_zone"] = server_body.pop(az_key) + + # User Data exists with a prefix on response, but not request + ud_key = "OS-EXT-SRV-ATTR:user_data" + if ud_key in server_body: + server_body["user_data"] = server_body.pop(ud_key) + + # Scheduler hints are sent in a top-level scope, not within the + # resource_key scope like everything else. If we try to send + # scheduler_hints, pop them out of the resource_key scope and into + # their own top-level scope. + hint_key = "OS-SCH-HNT:scheduler_hints" + if hint_key in server_body: + request.body[hint_key] = server_body.pop(hint_key) + + return request + def _action(self, session, body): """Preform server actions given the message body.""" # NOTE: This is using Server.base_path instead of self.base_path diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 88a6a475c..67daf6795 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -50,10 +50,10 @@ 'adminPass': '27', 'personality': '28', 'block_device_mapping_v2': {'key': '29'}, - 'os:scheduler_hints': {'key': '30'}, - 'user_data': '31', 'OS-EXT-SRV-ATTR:hypervisor_hostname': 'hypervisor.example.com', - 'OS-EXT-SRV-ATTR:instance_name': 'instance-00000001' + 'OS-EXT-SRV-ATTR:instance_name': 'instance-00000001', + 'OS-SCH-HNT:scheduler_hints': {'key': '30'}, + 'OS-EXT-SRV-ATTR:user_data': '31' } @@ -140,12 +140,13 @@ def test_make_it(self): self.assertEqual(EXAMPLE['personality'], sot.personality) self.assertEqual(EXAMPLE['block_device_mapping_v2'], sot.block_device_mapping) - self.assertEqual(EXAMPLE['os:scheduler_hints'], sot.scheduler_hints) - self.assertEqual(EXAMPLE['user_data'], sot.user_data) self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:hypervisor_hostname'], sot.hypervisor_hostname) self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:instance_name'], sot.instance_name) + self.assertEqual(EXAMPLE['OS-SCH-HNT:scheduler_hints'], + sot.scheduler_hints) + self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:user_data'], sot.user_data) def test_detail(self): sot = server.ServerDetail() @@ -159,6 +160,29 @@ def test_detail(self): self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) + def test__prepare_server(self): + zone = 1 + data = 2 + hints = {"hint": 3} + + sot = server.Server(id=1, availability_zone=zone, user_data=data, + scheduler_hints=hints) + request = sot._prepare_request() + + self.assertNotIn("OS-EXT-AZ:availability_zone", + request.body[sot.resource_key]) + self.assertEqual(request.body[sot.resource_key]["availability_zone"], + zone) + + self.assertNotIn("OS-EXT-SRV-ATTR:user_data", + request.body[sot.resource_key]) + self.assertEqual(request.body[sot.resource_key]["user_data"], + data) + + self.assertNotIn("OS-SCH-HNT:scheduler_hints", + request.body[sot.resource_key]) + self.assertEqual(request.body["OS-SCH-HNT:scheduler_hints"], hints) + def test_change_password(self): sot = server.Server(**EXAMPLE) From 8a8a5435adb0d0025c91d196a4a067edbb812ce9 Mon Sep 17 00:00:00 2001 From: tengqm Date: Fri, 17 Mar 2017 04:14:43 -0400 Subject: [PATCH 1357/3836] Add StackEnvironment resource to orchestration v1 This adds a new resource type to orchestration (heat) v1 so that we can retrieve back the environment used to create a stack. Change-Id: Ib528604e9ebb1709cc87da3fb08c61625de37a1c --- openstack/orchestration/v1/_proxy.py | 30 ++++++++-- .../orchestration/v1/stack_environment.py | 43 ++++++++++++++ .../tests/unit/orchestration/v1/test_proxy.py | 31 ++++++++++ .../v1/test_stack_environment.py | 57 +++++++++++++++++++ 4 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 openstack/orchestration/v1/stack_environment.py create mode 100644 openstack/tests/unit/orchestration/v1/test_stack_environment.py diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index ec6f5208a..ffa2368b6 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -15,6 +15,7 @@ from openstack.orchestration.v1 import software_config as _sc from openstack.orchestration.v1 import software_deployment as _sd from openstack.orchestration.v1 import stack as _stack +from openstack.orchestration.v1 import stack_environment as _stack_environment from openstack.orchestration.v1 import stack_template as _stack_template from openstack.orchestration.v1 import template as _template from openstack import proxy2 @@ -126,12 +127,13 @@ def check_stack(self, stack): stk_obj.check(self._session) def get_stack_template(self, stack): - """Get a single stack + """Get template used by a stack - :param stack: The value can be the ID of a stack or a - :class:`~openstack.orchestration.v1.stack.Stack` instance. + :param stack: The value can be the ID of a stack or an instance of + :class:`~openstack.orchestration.v1.stack_template.StackTemplate` - :returns: One :class:`~openstack.orchestration.v1.stack.Stack` + :returns: One object of + :class:`~openstack.orchestration.v1.stack_template.StackTemplate` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ @@ -143,6 +145,26 @@ def get_stack_template(self, stack): return self._get(_stack_template.StackTemplate, requires_id=False, stack_name=obj.name, stack_id=obj.id) + def get_stack_environment(self, stack): + """Get environment used by a stack + + :param stack: The value can be the ID of a stack or an instance of + :class:`~openstack.orchestration.v1.stack_environment.StackEnvironment` + + :returns: One object of + :class:`~openstack.orchestration.v1.stack_environment.StackEnvironment` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + if isinstance(stack, _stack.Stack): + obj = stack + else: + obj = self._find(_stack.Stack, stack, ignore_missing=False) + + return self._get(_stack_environment.StackEnvironment, + requires_id=False, stack_name=obj.name, + stack_id=obj.id) + def resources(self, stack, **query): """Return a generator of resources diff --git a/openstack/orchestration/v1/stack_environment.py b/openstack/orchestration/v1/stack_environment.py new file mode 100644 index 000000000..7ffc5d5f8 --- /dev/null +++ b/openstack/orchestration/v1/stack_environment.py @@ -0,0 +1,43 @@ +# 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.orchestration import orchestration_service +from openstack import resource2 as resource + + +class StackEnvironment(resource.Resource): + + service = orchestration_service.OrchestrationService() + base_path = "/stacks/%(stack_name)s/%(stack_id)s/environment" + + # capabilities + allow_create = False + allow_list = False + allow_get = True + allow_delete = False + allow_update = False + + # Properties + #: Name of the stack where the template is referenced. + stack_name = resource.URI('stack_name') + #: ID of the stack where the template is referenced. + stack_id = resource.URI('stack_id') + #: A list of parameter names whose values are encrypted + encrypted_param_names = resource.Body('encrypted_param_names') + #: A list of event sinks + event_sinks = resource.Body('event_sinks') + #: A map of parameters and their default values defined for the stack. + parameter_defaults = resource.Body('parameter_defaults') + #: A map of parametes defined in the stack template. + parameters = resource.Body('parameters', type=dict) + #: A map containing customized resource definitions. + resource_registry = resource.Body('resource_registry', type=dict) diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index 593a3b8a8..8e9965008 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -19,6 +19,7 @@ from openstack.orchestration.v1 import software_config as sc from openstack.orchestration.v1 import software_deployment as sd from openstack.orchestration.v1 import stack +from openstack.orchestration.v1 import stack_environment from openstack.orchestration.v1 import stack_template from openstack.orchestration.v1 import template from openstack.tests.unit import test_proxy_base2 @@ -75,6 +76,36 @@ def test_check_stack_with_stack_ID(self, mock_stack): mock_stack.assert_called_once_with(id='FAKE_ID') stk.check.assert_called_once_with(self.proxy._session) + @mock.patch.object(stack.Stack, 'find') + def test_get_stack_environment_with_stack_identity(self, mock_find): + stack_id = '1234' + stack_name = 'test_stack' + stk = stack.Stack(id=stack_id, name=stack_name) + mock_find.return_value = stk + + self._verify2('openstack.proxy2.BaseProxy._get', + self.proxy.get_stack_environment, + method_args=['IDENTITY'], + expected_args=[stack_environment.StackEnvironment], + expected_kwargs={'requires_id': False, + 'stack_name': stack_name, + 'stack_id': stack_id}) + mock_find.assert_called_once_with(mock.ANY, 'IDENTITY', + ignore_missing=False) + + def test_get_stack_environment_with_stack_object(self): + stack_id = '1234' + stack_name = 'test_stack' + stk = stack.Stack(id=stack_id, name=stack_name) + + self._verify2('openstack.proxy2.BaseProxy._get', + self.proxy.get_stack_environment, + method_args=[stk], + expected_args=[stack_environment.StackEnvironment], + expected_kwargs={'requires_id': False, + 'stack_name': stack_name, + 'stack_id': stack_id}) + @mock.patch.object(stack.Stack, 'find') def test_get_stack_template_with_stack_identity(self, mock_find): stack_id = '1234' diff --git a/openstack/tests/unit/orchestration/v1/test_stack_environment.py b/openstack/tests/unit/orchestration/v1/test_stack_environment.py new file mode 100644 index 000000000..93b7286e1 --- /dev/null +++ b/openstack/tests/unit/orchestration/v1/test_stack_environment.py @@ -0,0 +1,57 @@ +# 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 testtools + +from openstack.orchestration.v1 import stack_environment as se + + +FAKE = { + 'encrypted_param_names': ['n1', 'n2'], + 'event_sinks': { + 's1': 'v1' + }, + 'parameters': { + 'key_name': { + 'type': 'string' + } + }, + 'parameter_defaults': { + 'p1': 'def1' + }, + 'resource_registry': { + 'resources': { + 'type1': 'type2' + } + }, +} + + +class TestStackTemplate(testtools.TestCase): + + def test_basic(self): + sot = se.StackEnvironment() + self.assertEqual('orchestration', sot.service.service_type) + self.assertFalse(sot.allow_create) + self.assertTrue(sot.allow_get) + self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_delete) + self.assertFalse(sot.allow_list) + + def test_make_it(self): + sot = se.StackEnvironment(**FAKE) + self.assertEqual(FAKE['encrypted_param_names'], + sot.encrypted_param_names) + self.assertEqual(FAKE['event_sinks'], sot.event_sinks) + self.assertEqual(FAKE['parameters'], sot.parameters) + self.assertEqual(FAKE['parameter_defaults'], sot.parameter_defaults) + self.assertEqual(FAKE['resource_registry'], sot.resource_registry) From 4c807a04f7c83bdcef61626ee4376ac3becb2f2b Mon Sep 17 00:00:00 2001 From: Ethan Lynn Lin Date: Fri, 17 Mar 2017 17:59:39 +0800 Subject: [PATCH 1358/3836] Remove type restrict of block_device_mapping Server.block_device_mapping is a list instead of a dict, if create a server with this attribute set, to_dict() won't work. Change-Id: Iddd5ad44d21fa4e7e25728a2be1aaf5821df3f13 --- openstack/compute/v2/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 8a91fee07..96634c40a 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -122,7 +122,7 @@ class Server(resource2.Resource, metadata.MetadataMixin): user_data = resource2.Body('OS-EXT-SRV-ATTR:user_data') #: Enables fine grained control of the block device mapping for an #: instance. This is typically used for booting servers from volumes. - block_device_mapping = resource2.Body('block_device_mapping_v2', type=dict) + block_device_mapping = resource2.Body('block_device_mapping_v2') #: The dictionary of data to send to the scheduler. scheduler_hints = resource2.Body('OS-SCH-HNT:scheduler_hints', type=dict) #: A networks object. Required parameter when there are multiple From 3bde55272f4899ee9e89df3edf3e4b6edf23b5be Mon Sep 17 00:00:00 2001 From: Pip Oomen Date: Tue, 14 Mar 2017 14:20:27 +0100 Subject: [PATCH 1359/3836] Expose ha_state property from HA enabled L3 Agents. The blueprint 'Report HA Router Master' describes how the REST interface now exposes the HA state of a L3 agent. The property is connected to the L3 agent instance which represents the router. As such it represents the state of a single L3 agent router instance, and will return different state for different L3 agents connected to the same router. I.e. a HA router is implemented by (at least) two L3 agent instances, and those instances will have different states. This patch exposes this property for the openstack.network.v2.Agent class, making it available for inspection. Change-Id: I30fc4cec3efb98aa37c6173ffecaf9ade7e2d212 --- openstack/network/v2/agent.py | 3 +++ openstack/tests/unit/network/v2/test_agent.py | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/openstack/network/v2/agent.py b/openstack/network/v2/agent.py index 0f0dac83b..bb58ed415 100644 --- a/openstack/network/v2/agent.py +++ b/openstack/network/v2/agent.py @@ -63,6 +63,9 @@ class Agent(resource.Resource): started_at = resource.Body('started_at') #: The messaging queue topic the network agent subscribes to. topic = resource.Body('topic') + #: The HA state of the L3 agent. This is one of 'active', 'standby' or + #: 'fault' for HA routers, or None for other types of routers. + ha_state = resource.Body('ha_state') def add_agent_to_network(self, session, network_id): body = {'network_id': network_id} diff --git a/openstack/tests/unit/network/v2/test_agent.py b/openstack/tests/unit/network/v2/test_agent.py index 1980d4118..6eddcb27e 100644 --- a/openstack/tests/unit/network/v2/test_agent.py +++ b/openstack/tests/unit/network/v2/test_agent.py @@ -29,7 +29,8 @@ 'host': 'test-host', 'id': IDENTIFIER, 'started_at': '2016-07-09T12:14:57.233772', - 'topic': 'test-topic' + 'topic': 'test-topic', + 'ha_state': 'active' } @@ -63,6 +64,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['started_at'], sot.started_at) self.assertEqual(EXAMPLE['topic'], sot.topic) + self.assertEqual(EXAMPLE['ha_state'], sot.ha_state) def test_add_agent_to_network(self): # Add agent to network From f29624aced9f872e04bb0a4623c7b843b41fa4a8 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 20 Mar 2017 07:06:52 -0500 Subject: [PATCH 1360/3836] Cleanup some workarounds for old OCC versions We don't need to pass constructor objects to os-client-config - os-client-config is quite happy to do this for us. We do need to bump the os-client-config version, because the magnum container workaround didn't go in until then. Change-Id: I6a7958eaf6745e6714a0c03b7e829c29c4059b07 --- requirements.txt | 2 +- shade/openstackcloud.py | 51 ++++++++++------------------------ shade/tests/unit/test_shade.py | 3 +- 3 files changed, 17 insertions(+), 39 deletions(-) diff --git a/requirements.txt b/requirements.txt index cd4099331..f7efdcfb9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ decorator jmespath jsonpatch ipaddress -os-client-config>=1.22.0 +os-client-config>=1.25.0 requestsexceptions>=1.1.1 six diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 781381b64..cbe580356 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -31,16 +31,10 @@ from six.moves import urllib import cinderclient.exceptions as cinder_exceptions -import heatclient.client import magnumclient.exceptions as magnum_exceptions from heatclient import exc as heat_exceptions import keystoneauth1.exceptions -import keystoneclient.client -import magnumclient.client -import neutronclient.neutron.client -import novaclient.client import novaclient.exceptions as nova_exceptions -import designateclient.client from shade.exc import * # noqa from shade import _adapter @@ -339,7 +333,7 @@ def _get_cache(self, resource_name): return self._cache def _get_client( - self, service_key, client_class, interface_key=None, + self, service_key, client_class=None, interface_key=None, pass_version_arg=True, **kwargs): try: client = self.cloud_config.get_legacy_client( @@ -544,8 +538,7 @@ def _volume_client(self): @property def nova_client(self): if self._nova_client is None: - self._nova_client = self._get_client( - 'compute', novaclient.client.Client, version='2.0') + self._nova_client = self._get_client('compute', version='2.0') return self._nova_client @property @@ -561,8 +554,7 @@ def keystone_session(self): @property def keystone_client(self): if self._keystone_client is None: - self._keystone_client = self._get_client( - 'identity', keystoneclient.client.Client) + self._keystone_client = self._get_client('identity') return self._keystone_client @property @@ -1092,22 +1084,20 @@ def glance_client(self): ' need a raw glanceclient.Client object, please use' ' make_legacy_client in os-client-config instead') try: - import glanceclient + import glanceclient # flake8: noqa except ImportError: self.log.error( 'glanceclient is no longer a dependency of shade. You need to' ' install python-glanceclient directly.') raise if self._glance_client is None: - self._glance_client = self._get_client( - 'image', glanceclient.Client) + self._glance_client = self._get_client('image') return self._glance_client @property def heat_client(self): if self._heat_client is None: - self._heat_client = self._get_client( - 'orchestration', heatclient.client.Client) + self._heat_client = self._get_client('orchestration') return self._heat_client def get_template_contents( @@ -1128,13 +1118,12 @@ def swift_client(self): ' need a raw swiftclient.Connection object, please use' ' make_legacy_client in os-client-config instead') try: - import swiftclient.client + import swiftclient.client # flake8: noqa except ImportError: self.log.error( 'swiftclient is no longer a dependency of shade. You need to' ' install python-swiftclient directly.') - return self._get_client( - 'object-store', swiftclient.client.Connection) + return self._get_client('object-store') def _get_swift_kwargs(self): auth_version = self.cloud_config.get_api_version('identity') @@ -1186,10 +1175,9 @@ def cinder_client(self): # Import cinderclient late because importing it at the top level # breaks logging for users of shade - import cinderclient.client + import cinderclient.client # flake8: noqa if self._cinder_client is None: - self._cinder_client = self._get_client( - 'volume', cinderclient.client.Client) + self._cinder_client = self._get_client('volume') return self._cinder_client @property @@ -1198,35 +1186,26 @@ def trove_client(self): 'Using shade to get a trove_client object is deprecated. If you' ' need a raw troveclient.client.Client object, please use' ' make_legacy_client in os-client-config instead') - import troveclient.client if self._trove_client is None: - self._trove_client = self._get_client( - 'database', troveclient.client.Client) + self._trove_client = self._get_client('database') return self._trove_client @property def magnum_client(self): if self._magnum_client is None: - # Workaround for os-client-config <=1.24.0 which thought of - # this as container rather than container-infra (so did we all) - version = self.cloud_config.get_api_version('container-infra') - if not version: - version = self.cloud_config.get_api_version('container') - self._magnum_client = self._get_client( - service_key='container-infra', - client_class=magnumclient.client.Client, - version=version) + self._magnum_client = self._get_client('container-infra') return self._magnum_client @property def neutron_client(self): if self._neutron_client is None: - self._neutron_client = self._get_client( - 'network', neutronclient.neutron.client.Client) + self._neutron_client = self._get_client('network') return self._neutron_client @property def designate_client(self): + # Note: Explicit constructor is needed until occ 1.27.0 + import designateclient.client # flake8: noqa if self._designate_client is None: self._designate_client = self._get_client( 'dns', designateclient.client.Client) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 568a52907..aef3e8948 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -13,7 +13,6 @@ import mock import munch -from heatclient import client as heat_client from neutronclient.common import exceptions as n_exc import testtools @@ -82,7 +81,7 @@ def test_heat_args(self, get_legacy_client_mock, get_session_mock): self.cloud.heat_client get_legacy_client_mock.assert_called_once_with( service_key='orchestration', - client_class=heat_client.Client, + client_class=None, interface_key=None, pass_version_arg=True, ) From 55868939c7995641dc0fcb1b4e405c7292312ba8 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 20 Mar 2017 11:27:12 -0500 Subject: [PATCH 1361/3836] Use data when the request has a non-json content type Using the json= argument to the request call when the content/type is not intended to be application/json is crossing wires, and in those cases we should call data=json.dumps(). This actually breaks older versions of keystoneauth, which are otherwise perfectly fine. For newer, it works either way, but maybe let's just learn to send json payloads as data in any case where the content-type isn't application/json. Change-Id: Ie93840625e1418eaffa442fb0d93080040d2a06d --- shade/openstackcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index c69f21843..1d73fe045 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3686,7 +3686,7 @@ def _update_image_properties_v2(self, image, meta, properties): self._image_client.patch( '/images/{id}'.format(id=image.id), headers=headers, - json=patch) + data=json.dumps(patch)) self.list_images.invalidate(self) return True From 2f009fd4b86d45ecf4ed8db126cdef31b7872142 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 20 Mar 2017 07:52:02 -0500 Subject: [PATCH 1362/3836] Get the ball rolling on magnumclient I think I can get this done on the plane. Also - WOAH Change-Id: I69aa673b1ea114999fb66e836fd23345106a06bb --- shade/_utils.py | 1 + shade/tests/unit/fixtures/catalog-v3.json | 13 ++++++++ shade/tests/unit/test_cluster_templates.py | 38 ++++++++++++---------- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/shade/_utils.py b/shade/_utils.py index b7604bad4..6500556fd 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -379,6 +379,7 @@ def normalize_stacks(stacks): def normalize_cluster_templates(cluster_templates): """Normalize Magnum cluster_templates.""" for cluster_template in cluster_templates: + cluster_template.pop('model_name', None) cluster_template['id'] = cluster_template['uuid'] return cluster_templates diff --git a/shade/tests/unit/fixtures/catalog-v3.json b/shade/tests/unit/fixtures/catalog-v3.json index 66df0f5c1..bb74465c0 100644 --- a/shade/tests/unit/fixtures/catalog-v3.json +++ b/shade/tests/unit/fixtures/catalog-v3.json @@ -84,6 +84,19 @@ "name": "neutron", "type": "network" }, + { + "endpoints": [ + { + "id": "4deb4d0504a044a395d4480741ba628e", + "interface": "public", + "region": "RegionOne", + "url": "https://container-infra.example.com/v1" + } + ], + "endpoints_links": [], + "name": "magnum", + "type": "container-infra" + }, { "endpoints": [ { diff --git a/shade/tests/unit/test_cluster_templates.py b/shade/tests/unit/test_cluster_templates.py index ff52556b5..0ab8cdd60 100644 --- a/shade/tests/unit/test_cluster_templates.py +++ b/shade/tests/unit/test_cluster_templates.py @@ -15,6 +15,7 @@ import munch import shade +from shade import _utils import testtools from shade.tests.unit import base @@ -63,28 +64,31 @@ ) -class TestClusterTemplates(base.TestCase): +class TestClusterTemplates(base.RequestsMockTestCase): - def setUp(self): - super(TestClusterTemplates, self).setUp() - self.cloud = shade.openstack_cloud(validate=False) + def test_list_cluster_templates_without_detail(self): - @mock.patch.object(shade.OpenStackCloud, 'magnum_client') - def test_list_cluster_templates_without_detail(self, mock_magnum): - mock_magnum.baymodels.list.return_value = [ - cluster_template_obj] + self.register_uris([dict( + method='GET', + uri='https://container-infra.example.com/v1/baymodels', + json=dict(baymodels=[cluster_template_obj.toDict()]))]) cluster_templates_list = self.cloud.list_cluster_templates() - mock_magnum.baymodels.list.assert_called_with(detail=False) - self.assertEqual(cluster_templates_list[0], cluster_template_obj) - - @mock.patch.object(shade.OpenStackCloud, 'magnum_client') - def test_list_cluster_templates_with_detail(self, mock_magnum): - mock_magnum.baymodels.list.return_value = [ - cluster_template_detail_obj] + self.assertEqual( + cluster_templates_list[0], + _utils.normalize_cluster_templates([cluster_template_obj])[0]) + self.assert_calls() + + def test_list_cluster_templates_with_detail(self): + self.register_uris([dict( + method='GET', + uri='https://container-infra.example.com/v1/baymodels/detail', + json=dict(baymodels=[cluster_template_detail_obj.toDict()]))]) cluster_templates_list = self.cloud.list_cluster_templates(detail=True) - mock_magnum.baymodels.list.assert_called_with(detail=True) self.assertEqual( - cluster_templates_list[0], cluster_template_detail_obj) + cluster_templates_list[0], + _utils.normalize_cluster_templates( + [cluster_template_detail_obj])[0]) + self.assert_calls() @mock.patch.object(shade.OpenStackCloud, 'magnum_client') def test_search_cluster_templates_by_name(self, mock_magnum): From f32591ab21bbd7395fe9f77a102c982ee733a4a1 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 20 Mar 2017 09:18:05 -0500 Subject: [PATCH 1363/3836] Add normalization for cluster templates Every time the magic of flexible return schemas rears its ugly head, a pony becomes a less happy pony. It's worth noting that this changes behavior to _always_ get detailed records. This is consistent with our other things and should not break anyone. Change-Id: I4932e424ec7db8b6cea014ec668b30bb29f3e1f9 --- doc/source/model.rst | 38 ++++++++++ ...il-cluster-templates-3eb4b5744ba327ac.yaml | 5 ++ shade/_normalize.py | 69 +++++++++++++++++++ shade/_utils.py | 8 --- shade/openstackcloud.py | 8 +-- shade/tests/unit/test_cluster_templates.py | 64 +++++++---------- 6 files changed, 141 insertions(+), 51 deletions(-) create mode 100644 releasenotes/notes/always-detail-cluster-templates-3eb4b5744ba327ac.yaml diff --git a/doc/source/model.rst b/doc/source/model.rst index 37aa5d1db..93ae95cff 100644 --- a/doc/source/model.rst +++ b/doc/source/model.rst @@ -395,3 +395,41 @@ A volume type access from cinder. volume_type_id=str(), project_id=str(), properties=dict()) + + +ClusterTemplate +--------------- + +A Cluster Template from magnum. + +.. code-block:: python + + ClusterTemplate = dict( + location=Location(), + apiserver_port=int(), + cluster_distro=str(), + coe=str(), + created_at=str(), + dns_nameserver=str(), + docker_volume_size=int(), + external_network_id=str(), + fixed_network=str() or None, + flavor_id=str(), + http_proxy=str() or None, + https_proxy=str() or None, + id=str(), + image_id=str(), + insecure_registry=str(), + is_public=bool(), + is_registry_enabled=bool(), + is_tls_disabled=bool(), + keypair_id=str(), + labels=dict(), + master_flavor_id=str() or None, + name=str(), + network_driver=str(), + no_proxy=str() or None, + server_type=str(), + updated_at=str() or None, + volume_driver=str(), + properties=dict()) diff --git a/releasenotes/notes/always-detail-cluster-templates-3eb4b5744ba327ac.yaml b/releasenotes/notes/always-detail-cluster-templates-3eb4b5744ba327ac.yaml new file mode 100644 index 000000000..cc98f8c9f --- /dev/null +++ b/releasenotes/notes/always-detail-cluster-templates-3eb4b5744ba327ac.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - Cluster Templates have data model and normalization + now. As a result, the detail parameter is now ignored + and detailed records are always returned. diff --git a/shade/_normalize.py b/shade/_normalize.py index 3621e7805..79fa3f518 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -836,3 +836,72 @@ def _normalize_server_usages(self, server_usages): for server_usage in server_usages: ret.append(self._normalize_server_usage(server_usage)) return ret + + def _normalize_cluster_templates(self, cluster_templates): + ret = [] + for cluster_template in cluster_templates: + ret.append(self._normalize_cluster_template(cluster_template)) + return ret + + def _normalize_cluster_template(self, cluster_template): + """Normalize Magnum cluster_templates.""" + cluster_template = cluster_template.copy() + + # Discard noise + cluster_template.pop('links', None) + cluster_template.pop('human_id', None) + # model_name is a magnumclient-ism + cluster_template.pop('model_name', None) + + ct_id = cluster_template.pop('uuid') + + ret = munch.Munch( + id=ct_id, + location=self._get_current_location(), + ) + ret['is_public'] = cluster_template.pop('public') + ret['is_registry_enabled'] = cluster_template.pop('registry_enabled') + ret['is_tls_disabled'] = cluster_template.pop('tls_disabled') + # pop floating_ip_enabled since we want to hide it in a future patch + fip_enabled = cluster_template.pop('floating_ip_enabled', None) + if not self.strict_mode: + ret['uuid'] = ct_id + if fip_enabled is not None: + ret['floating_ip_enabled'] = fip_enabled + ret['public'] = ret['is_public'] + ret['registry_enabled'] = ret['is_registry_enabled'] + ret['tls_disabled'] = ret['is_tls_disabled'] + + # Optional keys + for (key, default) in ( + ('fixed_network', None), + ('fixed_subnet', None), + ('http_proxy', None), + ('https_proxy', None), + ('labels', {}), + ('master_flavor_id', None), + ('no_proxy', None)): + if key in cluster_template: + ret[key] = cluster_template.pop(key, default) + + for key in ( + 'apiserver_port', + 'cluster_distro', + 'coe', + 'created_at', + 'dns_nameserver', + 'docker_volume_size', + 'external_network_id', + 'flavor_id', + 'image_id', + 'insecure_registry', + 'keypair_id', + 'name', + 'network_driver', + 'server_type', + 'updated_at', + 'volume_driver'): + ret[key] = cluster_template.pop(key) + + ret['properties'] = cluster_template + return ret diff --git a/shade/_utils.py b/shade/_utils.py index 6500556fd..bf2153cff 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -376,14 +376,6 @@ def normalize_stacks(stacks): return stacks -def normalize_cluster_templates(cluster_templates): - """Normalize Magnum cluster_templates.""" - for cluster_template in cluster_templates: - cluster_template.pop('model_name', None) - cluster_template['id'] = cluster_template['uuid'] - return cluster_templates - - def valid_kwargs(*valid_args): # This decorator checks if argument passed as **kwargs to a function are # present in valid_args. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index cbe580356..f7dc0be25 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -7117,8 +7117,8 @@ def delete_recordset(self, zone, name_or_id): def list_cluster_templates(self, detail=False): """List cluster templates. - :param bool detail. Flag to control if we need summarized or - detailed output. + :param bool detail. Ignored. Included for backwards compat. + ClusterTemplates are always returned with full details. :returns: a list of dicts containing the cluster template details. @@ -7127,8 +7127,8 @@ def list_cluster_templates(self, detail=False): """ with _utils.shade_exceptions("Error fetching cluster template list"): cluster_templates = self.manager.submit_task( - _tasks.ClusterTemplateList(detail=detail)) - return _utils.normalize_cluster_templates(cluster_templates) + _tasks.ClusterTemplateList(detail=True)) + return self._normalize_cluster_templates(cluster_templates) list_baymodels = list_cluster_templates def search_cluster_templates( diff --git a/shade/tests/unit/test_cluster_templates.py b/shade/tests/unit/test_cluster_templates.py index 0ab8cdd60..fff08ef49 100644 --- a/shade/tests/unit/test_cluster_templates.py +++ b/shade/tests/unit/test_cluster_templates.py @@ -15,52 +15,38 @@ import munch import shade -from shade import _utils import testtools from shade.tests.unit import base cluster_template_obj = munch.Munch( - apiserver_port=None, - uuid='fake-uuid', + apiserver_port=12345, + cluster_distro='fake-distro', + coe='fake-coe', + created_at='fake-date', + dns_nameserver='8.8.8.8', + docker_volume_size=1, + external_network_id='public', + fixed_network=None, + flavor_id='fake-flavor', + https_proxy=None, human_id=None, - name='fake-cluster-template', - server_type='vm', - public=False, image_id='fake-image', - tls_disabled=False, - registry_enabled=False, - coe='fake-coe', + insecure_registry='https://192.168.0.10', keypair_id='fake-key', -) - -cluster_template_detail_obj = munch.Munch( - links={}, labels={}, - apiserver_port=None, - uuid='fake-uuid', - human_id=None, + links={}, + master_flavor_id=None, name='fake-cluster-template', - server_type='vm', + network_driver='fake-driver', + no_proxy=None, public=False, - image_id='fake-image', - tls_disabled=False, registry_enabled=False, - coe='fake-coe', - created_at='fake-date', + server_type='vm', + tls_disabled=False, updated_at=None, - master_flavor_id=None, - no_proxy=None, - https_proxy=None, - keypair_id='fake-key', - docker_volume_size=1, - external_network_id='public', - cluster_distro='fake-distro', + uuid='fake-uuid', volume_driver=None, - network_driver='fake-driver', - fixed_network=None, - flavor_id='fake-flavor', - dns_nameserver='8.8.8.8', ) @@ -70,24 +56,24 @@ def test_list_cluster_templates_without_detail(self): self.register_uris([dict( method='GET', - uri='https://container-infra.example.com/v1/baymodels', + uri='https://container-infra.example.com/v1/baymodels/detail', json=dict(baymodels=[cluster_template_obj.toDict()]))]) cluster_templates_list = self.cloud.list_cluster_templates() self.assertEqual( cluster_templates_list[0], - _utils.normalize_cluster_templates([cluster_template_obj])[0]) + self.cloud._normalize_cluster_template(cluster_template_obj)) self.assert_calls() def test_list_cluster_templates_with_detail(self): self.register_uris([dict( method='GET', uri='https://container-infra.example.com/v1/baymodels/detail', - json=dict(baymodels=[cluster_template_detail_obj.toDict()]))]) + json=dict(baymodels=[cluster_template_obj.toDict()]))]) cluster_templates_list = self.cloud.list_cluster_templates(detail=True) self.assertEqual( cluster_templates_list[0], - _utils.normalize_cluster_templates( - [cluster_template_detail_obj])[0]) + self.cloud._normalize_cluster_template( + cluster_template_obj)) self.assert_calls() @mock.patch.object(shade.OpenStackCloud, 'magnum_client') @@ -97,7 +83,7 @@ def test_search_cluster_templates_by_name(self, mock_magnum): cluster_templates = self.cloud.search_cluster_templates( name_or_id='fake-cluster-template') - mock_magnum.baymodels.list.assert_called_with(detail=False) + mock_magnum.baymodels.list.assert_called_with(detail=True) self.assertEqual(1, len(cluster_templates)) self.assertEqual('fake-uuid', cluster_templates[0]['uuid']) @@ -110,7 +96,7 @@ def test_search_cluster_templates_not_found(self, mock_magnum): cluster_templates = self.cloud.search_cluster_templates( name_or_id='non-existent') - mock_magnum.baymodels.list.assert_called_with(detail=False) + mock_magnum.baymodels.list.assert_called_with(detail=True) self.assertEqual(0, len(cluster_templates)) @mock.patch.object(shade.OpenStackCloud, 'search_cluster_templates') From 360a87fe1690df4f0102c5a0975fe9af3d8fe681 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 20 Mar 2017 09:30:58 -0500 Subject: [PATCH 1364/3836] RESTify cluster template tests Change-Id: Id8d00657f714e9dee7f42fe1fc6b9e151964e1ae --- shade/tests/unit/test_cluster_templates.py | 131 +++++++++++++-------- 1 file changed, 85 insertions(+), 46 deletions(-) diff --git a/shade/tests/unit/test_cluster_templates.py b/shade/tests/unit/test_cluster_templates.py index fff08ef49..b3e2776ee 100644 --- a/shade/tests/unit/test_cluster_templates.py +++ b/shade/tests/unit/test_cluster_templates.py @@ -11,7 +11,6 @@ # under the License. -import mock import munch import shade @@ -72,86 +71,126 @@ def test_list_cluster_templates_with_detail(self): cluster_templates_list = self.cloud.list_cluster_templates(detail=True) self.assertEqual( cluster_templates_list[0], - self.cloud._normalize_cluster_template( - cluster_template_obj)) + self.cloud._normalize_cluster_template(cluster_template_obj)) self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'magnum_client') - def test_search_cluster_templates_by_name(self, mock_magnum): - mock_magnum.baymodels.list.return_value = [ - cluster_template_obj] + def test_search_cluster_templates_by_name(self): + self.register_uris([dict( + method='GET', + uri='https://container-infra.example.com/v1/baymodels/detail', + json=dict(baymodels=[cluster_template_obj.toDict()]))]) cluster_templates = self.cloud.search_cluster_templates( name_or_id='fake-cluster-template') - mock_magnum.baymodels.list.assert_called_with(detail=True) self.assertEqual(1, len(cluster_templates)) self.assertEqual('fake-uuid', cluster_templates[0]['uuid']) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'magnum_client') - def test_search_cluster_templates_not_found(self, mock_magnum): - mock_magnum.baymodels.list.return_value = [ - cluster_template_obj] + def test_search_cluster_templates_not_found(self): + + self.register_uris([dict( + method='GET', + uri='https://container-infra.example.com/v1/baymodels/detail', + json=dict(baymodels=[cluster_template_obj.toDict()]))]) cluster_templates = self.cloud.search_cluster_templates( name_or_id='non-existent') - mock_magnum.baymodels.list.assert_called_with(detail=True) self.assertEqual(0, len(cluster_templates)) + self.assert_calls() + + def test_get_cluster_template(self): + self.register_uris([dict( + method='GET', + uri='https://container-infra.example.com/v1/baymodels/detail', + json=dict(baymodels=[cluster_template_obj.toDict()]))]) - @mock.patch.object(shade.OpenStackCloud, 'search_cluster_templates') - def test_get_cluster_template(self, mock_search): - mock_search.return_value = [cluster_template_obj, ] r = self.cloud.get_cluster_template('fake-cluster-template') self.assertIsNotNone(r) - self.assertDictEqual(cluster_template_obj, r) + self.assertDictEqual( + r, self.cloud._normalize_cluster_template(cluster_template_obj)) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'search_cluster_templates') - def test_get_cluster_template_not_found(self, mock_search): - mock_search.return_value = [] + def test_get_cluster_template_not_found(self): + self.register_uris([dict( + method='GET', + uri='https://container-infra.example.com/v1/baymodels/detail', + json=dict(baymodels=[]))]) r = self.cloud.get_cluster_template('doesNotExist') self.assertIsNone(r) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'magnum_client') - def test_create_cluster_template(self, mock_magnum): + def test_create_cluster_template(self): + self.register_uris([dict( + method='POST', + uri='https://container-infra.example.com/v1/baymodels', + json=dict(baymodels=[cluster_template_obj.toDict()]), + validate=dict(json={ + 'coe': 'fake-coe', + 'image_id': 'fake-image', + 'keypair_id': 'fake-key', + 'name': 'fake-cluster-template'}), + )]) self.cloud.create_cluster_template( name=cluster_template_obj.name, image_id=cluster_template_obj.image_id, keypair_id=cluster_template_obj.keypair_id, coe=cluster_template_obj.coe) - mock_magnum.baymodels.create.assert_called_once_with( - name=cluster_template_obj.name, - image_id=cluster_template_obj.image_id, - keypair_id=cluster_template_obj.keypair_id, - coe=cluster_template_obj.coe - ) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'magnum_client') - def test_create_cluster_template_exception(self, mock_magnum): - mock_magnum.baymodels.create.side_effect = Exception() + def test_create_cluster_template_exception(self): + self.register_uris([dict( + method='POST', + uri='https://container-infra.example.com/v1/baymodels', + status_code=403)]) with testtools.ExpectedException( shade.OpenStackCloudException, "Error creating cluster template of name fake-cluster-template" ): self.cloud.create_cluster_template('fake-cluster-template') + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'magnum_client') - def test_delete_cluster_template(self, mock_magnum): - mock_magnum.baymodels.list.return_value = [ - cluster_template_obj] + def test_delete_cluster_template(self): + uri = 'https://container-infra.example.com/v1/baymodels/fake-uuid' + self.register_uris([ + dict( + method='GET', + uri='https://container-infra.example.com/v1/baymodels/detail', + json=dict(baymodels=[cluster_template_obj.toDict()])), + dict( + method='DELETE', + uri=uri), + ]) self.cloud.delete_cluster_template('fake-uuid') - mock_magnum.baymodels.delete.assert_called_once_with( - 'fake-uuid' - ) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'magnum_client') - def test_update_cluster_template(self, mock_magnum): + def test_update_cluster_template(self): + uri = 'https://container-infra.example.com/v1/baymodels/fake-uuid' + self.register_uris([ + dict( + method='GET', + uri='https://container-infra.example.com/v1/baymodels/detail', + json=dict(baymodels=[cluster_template_obj.toDict()])), + dict( + method='PATCH', + uri=uri, + status_code=200, + validate=dict( + json=[{ + u'op': u'replace', + u'path': u'/name', + u'value': u'new-cluster-template' + }] + )), + dict( + method='GET', + uri='https://container-infra.example.com/v1/baymodels/detail', + # This json value is not meaningful to the test - it just has + # to be valid. + json=dict(baymodels=[cluster_template_obj.toDict()])), + ]) new_name = 'new-cluster-template' - mock_magnum.baymodels.list.return_value = [ - cluster_template_obj] self.cloud.update_cluster_template( 'fake-uuid', 'replace', name=new_name) - mock_magnum.baymodels.update.assert_called_once_with( - 'fake-uuid', [{'path': '/name', 'op': 'replace', - 'value': 'new-cluster-template'}] - ) + self.assert_calls() From 7311bf01879cf1572a368ae6237e697663c753d0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 20 Mar 2017 10:15:12 -0500 Subject: [PATCH 1365/3836] Convert magnum service to requests_mock Also add documentation and normalization, of which there was previously not. Change-Id: Ib4ba4fc748117ae4339683ae9fd4f9f958c3cc0e --- doc/source/model.rst | 18 +++++++++++++ shade/_normalize.py | 31 ++++++++++++++++++++++ shade/operatorcloud.py | 4 +-- shade/tests/unit/test_magnum_services.py | 33 ++++++++++++------------ 4 files changed, 68 insertions(+), 18 deletions(-) diff --git a/doc/source/model.rst b/doc/source/model.rst index 93ae95cff..ece33ce9e 100644 --- a/doc/source/model.rst +++ b/doc/source/model.rst @@ -433,3 +433,21 @@ A Cluster Template from magnum. updated_at=str() or None, volume_driver=str(), properties=dict()) + +MagnumService +------------- + +A Magnum Service from magnum + +.. code-block:: python + + MagnumService = dict( + location=Location(), + binary=str(), + created_at=str(), + disabled_reason=str() or None, + host=str(), + id=str(), + report_count=int(), + state=str(), + properties=dict()) diff --git a/shade/_normalize.py b/shade/_normalize.py index 79fa3f518..9a8103ece 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -905,3 +905,34 @@ def _normalize_cluster_template(self, cluster_template): ret['properties'] = cluster_template return ret + + def _normalize_magnum_services(self, magnum_services): + ret = [] + for magnum_service in magnum_services: + ret.append(self._normalize_magnum_service(magnum_service)) + return ret + + def _normalize_magnum_service(self, magnum_service): + """Normalize Magnum magnum_services.""" + magnum_service = magnum_service.copy() + + # Discard noise + magnum_service.pop('links', None) + magnum_service.pop('human_id', None) + # model_name is a magnumclient-ism + magnum_service.pop('model_name', None) + + ret = munch.Munch(location=self._get_current_location()) + + for key in ( + 'binary', + 'created_at', + 'disabled_reason', + 'host', + 'id', + 'report_count', + 'state', + 'updated_at'): + ret[key] = magnum_service.pop(key) + ret['properties'] = magnum_service + return ret diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 1b98ee60c..908bb2319 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -2251,5 +2251,5 @@ def list_magnum_services(self): :raises: OpenStackCloudException on operation error. """ with _utils.shade_exceptions("Error fetching Magnum services list"): - return self.manager.submit_task( - _tasks.MagnumServicesList()) + return self._normalize_magnum_services( + self.manager.submit_task(_tasks.MagnumServicesList())) diff --git a/shade/tests/unit/test_magnum_services.py b/shade/tests/unit/test_magnum_services.py index c15f912ab..24499f1fb 100644 --- a/shade/tests/unit/test_magnum_services.py +++ b/shade/tests/unit/test_magnum_services.py @@ -10,30 +10,31 @@ # License for the specific language governing permissions and limitations # under the License. - -import mock -import munch - -import shade from shade.tests.unit import base -magnum_service_obj = munch.Munch( +magnum_service_obj = dict( binary='fake-service', - state='up', - report_count=1, - human_id=None, + created_at='2015-08-27T09:49:58-05:00', + disabled_reason=None, host='fake-host', + human_id=None, id=1, - disabled_reason=None + report_count=1, + state='up', + updated_at=None, ) -class TestMagnumServices(base.TestCase): +class TestMagnumServices(base.RequestsMockTestCase): - @mock.patch.object(shade.OpenStackCloud, 'magnum_client') - def test_list_magnum_services(self, mock_magnum): - mock_magnum.mservices.list.return_value = [magnum_service_obj, ] + def test_list_magnum_services(self): + self.register_uris([dict( + method='GET', + uri='https://container-infra.example.com/v1/mservices', + json=dict(mservices=[magnum_service_obj]))]) mservices_list = self.op_cloud.list_magnum_services() - mock_magnum.mservices.list.assert_called_with(detail=False) - self.assertEqual(mservices_list[0], magnum_service_obj) + self.assertEqual( + mservices_list[0], + self.cloud._normalize_magnum_service(magnum_service_obj)) + self.assert_calls() From bd725e5f524d726e76c29f3df61e65ae58d13d4b Mon Sep 17 00:00:00 2001 From: tengqm Date: Fri, 17 Mar 2017 05:15:12 -0400 Subject: [PATCH 1366/3836] Add StackFiles resource to orchestration v1 This adds a StackFiles resource type to orchestration (heat) v1. Change-Id: I77f84fb5b02ed98dd066ae97e528afa8f7ece373 --- openstack/orchestration/v1/_proxy.py | 24 +++++++++++- openstack/orchestration/v1/stack_files.py | 39 +++++++++++++++++++ .../tests/unit/orchestration/v1/test_proxy.py | 29 ++++++++++++++ .../unit/orchestration/v1/test_stack_files.py | 37 ++++++++++++++++++ 4 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 openstack/orchestration/v1/stack_files.py create mode 100644 openstack/tests/unit/orchestration/v1/test_stack_files.py diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index ffa2368b6..08bf503e8 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -16,6 +16,7 @@ from openstack.orchestration.v1 import software_deployment as _sd from openstack.orchestration.v1 import stack as _stack from openstack.orchestration.v1 import stack_environment as _stack_environment +from openstack.orchestration.v1 import stack_files as _stack_files from openstack.orchestration.v1 import stack_template as _stack_template from openstack.orchestration.v1 import template as _template from openstack import proxy2 @@ -130,7 +131,7 @@ def get_stack_template(self, stack): """Get template used by a stack :param stack: The value can be the ID of a stack or an instance of - :class:`~openstack.orchestration.v1.stack_template.StackTemplate` + :class:`~openstack.orchestration.v1.stack.Stack` :returns: One object of :class:`~openstack.orchestration.v1.stack_template.StackTemplate` @@ -149,7 +150,7 @@ def get_stack_environment(self, stack): """Get environment used by a stack :param stack: The value can be the ID of a stack or an instance of - :class:`~openstack.orchestration.v1.stack_environment.StackEnvironment` + :class:`~openstack.orchestration.v1.stack.Stack` :returns: One object of :class:`~openstack.orchestration.v1.stack_environment.StackEnvironment` @@ -165,6 +166,25 @@ def get_stack_environment(self, stack): requires_id=False, stack_name=obj.name, stack_id=obj.id) + def get_stack_files(self, stack): + """Get files used by a stack + + :param stack: The value can be the ID of a stack or an instance of + :class:`~openstack.orchestration.v1.stack.Stack` + + :returns: A dictionary containing the names and contents of all files + used by the stack. + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when the stack cannot be found. + """ + if isinstance(stack, _stack.Stack): + stk = stack + else: + stk = self._find(_stack.Stack, stack, ignore_missing=False) + + obj = _stack_files.StackFiles(stack_name=stk.name, stack_id=stk.id) + return obj.get(self._session) + def resources(self, stack, **query): """Return a generator of resources diff --git a/openstack/orchestration/v1/stack_files.py b/openstack/orchestration/v1/stack_files.py new file mode 100644 index 000000000..b1fb24574 --- /dev/null +++ b/openstack/orchestration/v1/stack_files.py @@ -0,0 +1,39 @@ +# 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.orchestration import orchestration_service +from openstack import resource2 as resource + + +class StackFiles(resource.Resource): + + service = orchestration_service.OrchestrationService() + base_path = "/stacks/%(stack_name)s/%(stack_id)s/files" + + # capabilities + allow_create = False + allow_list = False + allow_get = True + allow_delete = False + allow_update = False + + # Properties + #: Name of the stack where the template is referenced. + stack_name = resource.URI('stack_name') + #: ID of the stack where the template is referenced. + stack_id = resource.URI('stack_id') + + def get(self, session): + # The stack files response contains a map of filenames and file + # contents. + request = self._prepare_request(requires_id=False) + return session.get(request.uri, endpoint_filter=self.service) diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index 8e9965008..9628198a7 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -20,6 +20,7 @@ from openstack.orchestration.v1 import software_deployment as sd from openstack.orchestration.v1 import stack from openstack.orchestration.v1 import stack_environment +from openstack.orchestration.v1 import stack_files from openstack.orchestration.v1 import stack_template from openstack.orchestration.v1 import template from openstack.tests.unit import test_proxy_base2 @@ -106,6 +107,34 @@ def test_get_stack_environment_with_stack_object(self): 'stack_name': stack_name, 'stack_id': stack_id}) + @mock.patch.object(stack_files.StackFiles, 'get') + @mock.patch.object(stack.Stack, 'find') + def test_get_stack_files_with_stack_identity(self, mock_find, mock_get): + stack_id = '1234' + stack_name = 'test_stack' + stk = stack.Stack(id=stack_id, name=stack_name) + mock_find.return_value = stk + mock_get.return_value = {'file': 'content'} + + res = self.proxy.get_stack_files('IDENTITY') + + self.assertEqual({'file': 'content'}, res) + mock_find.assert_called_once_with(mock.ANY, 'IDENTITY', + ignore_missing=False) + mock_get.assert_called_once_with(self.proxy._session) + + @mock.patch.object(stack_files.StackFiles, 'get') + def test_get_stack_files_with_stack_object(self, mock_get): + stack_id = '1234' + stack_name = 'test_stack' + stk = stack.Stack(id=stack_id, name=stack_name) + mock_get.return_value = {'file': 'content'} + + res = self.proxy.get_stack_files(stk) + + self.assertEqual({'file': 'content'}, res) + mock_get.assert_called_once_with(self.proxy._session) + @mock.patch.object(stack.Stack, 'find') def test_get_stack_template_with_stack_identity(self, mock_find): stack_id = '1234' diff --git a/openstack/tests/unit/orchestration/v1/test_stack_files.py b/openstack/tests/unit/orchestration/v1/test_stack_files.py new file mode 100644 index 000000000..58d8def54 --- /dev/null +++ b/openstack/tests/unit/orchestration/v1/test_stack_files.py @@ -0,0 +1,37 @@ +# 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 testtools + +from openstack.orchestration.v1 import stack_files as sf + +FAKE = { + 'stack_id': 'ID', + 'stack_name': 'NAME' +} + + +class TestStackFiles(testtools.TestCase): + + def test_basic(self): + sot = sf.StackFiles() + self.assertEqual('orchestration', sot.service.service_type) + self.assertFalse(sot.allow_create) + self.assertTrue(sot.allow_get) + self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_delete) + self.assertFalse(sot.allow_list) + + def test_make_it(self): + sot = sf.StackFiles(**FAKE) + self.assertEqual(FAKE['stack_id'], sot.stack_id) + self.assertEqual(FAKE['stack_name'], sot.stack_name) From bd0a40ecc646e9fe1d8bad7eb21abc58b1d9baaa Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 20 Mar 2017 11:19:08 -0500 Subject: [PATCH 1367/3836] Add designateclient to constructors list We've had this mapping over in shade for a while. No sense in keeping the fun all to ourselves. Change-Id: Icb2b98b621cfa8cff86c534bfba8f5de2c818e95 --- os_client_config/constructors.json | 1 + 1 file changed, 1 insertion(+) diff --git a/os_client_config/constructors.json b/os_client_config/constructors.json index 7ad420e61..2819bf380 100644 --- a/os_client_config/constructors.json +++ b/os_client_config/constructors.json @@ -3,6 +3,7 @@ "compute": "novaclient.client.Client", "container-infra": "magnumclient.client.Client", "database": "troveclient.client.Client", + "dns": "designateclient.client.Client", "identity": "keystoneclient.client.Client", "image": "glanceclient.Client", "key-manager": "barbicanclient.client.Client", From 07eca13c5fac9523538cbed3f9f2f1f530ea28a6 Mon Sep 17 00:00:00 2001 From: Jordan Pittier Date: Tue, 21 Mar 2017 18:53:35 +0100 Subject: [PATCH 1368/3836] delete_server: make sure we sleep a bit when waiting for server deletion If we call delete_server(.., wait=True) then self._SERVER_AGE is the 'sleeping time' between 2 calls to 'get_server'. But by default self._SERVER_AGE is 0, so we are hitting the Nova server hard. This patch is to make sure that we wait for some time (2sec) if self._SERVER_AGE has not been tweaked. Change-Id: I4aa7b3554348bd0b8fec3069fe8d0e3142e47bb3 --- shade/openstackcloud.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 781381b64..9e3ab66b5 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5645,7 +5645,9 @@ def _delete_server( for count in _utils._iterate_timeout( timeout, "Timed out waiting for server to get deleted.", - wait=self._SERVER_AGE): + # if _SERVER_AGE is 0 we still want to wait a bit + # to be friendly with the server. + wait=self._SERVER_AGE or 2): with _utils.shade_exceptions("Error in deleting server"): server = self.get_server(server['id']) if not server: From 91905270213d034d05ced25b19f849a5ac676684 Mon Sep 17 00:00:00 2001 From: Jordan Pittier Date: Tue, 21 Mar 2017 19:13:40 +0100 Subject: [PATCH 1369/3836] wait_for_server: ensure we sleep a bit when waiting for server If we call wait_for_server() then self._SERVER_AGE is the 'sleeping time' between 2 calls to 'get_server'. But by default self._SERVER_AGE is 0, so we are hitting the Nova server hard. This patch is to make sure that we wait for some time (2sec) if self._SERVER_AGE has not been tweaked. Change-Id: Ib9a93e922ff2e26f90d8d2e72f5cf6589054078a --- shade/openstackcloud.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 781381b64..d896c0b04 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5422,7 +5422,9 @@ def wait_for_server( for count in _utils._iterate_timeout( timeout, timeout_message, - wait=self._SERVER_AGE): + # if _SERVER_AGE is 0 we still want to wait a bit + # to be friendly with the server. + wait=self._SERVER_AGE or 2): try: # Use the get_server call so that the list_servers # cache can be leveraged From d809981e5b967bddf46b4eed917af90bf58d78b0 Mon Sep 17 00:00:00 2001 From: Jordan Pittier Date: Thu, 16 Feb 2017 16:01:15 +0100 Subject: [PATCH 1370/3836] attach_volume should always return a vol attachment. attach_volume method has inconsistent return value: if we pass it wait=True, the methods returns a volume object but if we pass it wait=False it returns a volume_attachment object. We should always return a volume_attachment object. Also don't try to be too clever in pre-emptively refusing to detach a volume that we think is not attached. It prevents the following calls, which looks good without knowing Shade internals: server = create_server(...); volume = create_volume(...); attach_volume(server, volume); detach_volume(server, volume) Change-Id: Ia1da29ec6286dbbed0a77d6abcf89e95a055ac9a --- ...ach-vol-return-value-4834a1f78392abb1.yaml | 8 ++++++ shade/openstackcloud.py | 12 +++----- shade/tests/functional/test_compute.py | 17 +++++++++++ shade/tests/unit/test_volume.py | 28 +++++-------------- 4 files changed, 36 insertions(+), 29 deletions(-) create mode 100644 releasenotes/notes/change-attach-vol-return-value-4834a1f78392abb1.yaml diff --git a/releasenotes/notes/change-attach-vol-return-value-4834a1f78392abb1.yaml b/releasenotes/notes/change-attach-vol-return-value-4834a1f78392abb1.yaml new file mode 100644 index 000000000..19db8ebef --- /dev/null +++ b/releasenotes/notes/change-attach-vol-return-value-4834a1f78392abb1.yaml @@ -0,0 +1,8 @@ +--- +upgrade: + - | + The ``attach_volume`` method now always returns a ``volume_attachment`` + object. Previously, ``attach_volume`` would return a ``volume`` object if + it was called with ``wait=True`` and a ``volume_attachment`` object + otherwise. + diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 781381b64..abcc3fb02 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3856,12 +3856,6 @@ def detach_volume(self, server, volume, wait=True, timeout=None): :raises: OpenStackCloudTimeout if wait time exceeded. :raises: OpenStackCloudException on operation error. """ - dev = self.get_volume_attach_device(volume, server['id']) - if not dev: - raise OpenStackCloudException( - "Volume %s is not attached to server %s" - % (volume['id'], server['id']) - ) with _utils.shade_exceptions( "Error detaching volume {volume} from server {server}".format( @@ -3909,6 +3903,8 @@ def attach_volume(self, server, volume, device=None, :param wait: If true, waits for volume to be attached. :param timeout: Seconds to wait for volume attachment. None is forever. + :returns: a volume attachment object. + :raises: OpenStackCloudTimeout if wait time exceeded. :raises: OpenStackCloudException on operation error. """ @@ -3929,7 +3925,7 @@ def attach_volume(self, server, volume, device=None, "Error attaching volume {volume_id} to server " "{server_id}".format(volume_id=volume['id'], server_id=server['id'])): - vol = self.manager.submit_task( + vol_attachment = self.manager.submit_task( _tasks.VolumeAttach(volume_id=volume['id'], server_id=server['id'], device=device)) @@ -3957,7 +3953,7 @@ def attach_volume(self, server, volume, device=None, raise OpenStackCloudException( "Error in attaching volume %s" % volume['id'] ) - return vol + return vol_attachment def _get_volume_kwargs(self, kwargs): name = kwargs.pop('name', kwargs.pop('display_name', None)) diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index 754b47874..ef4cacdcb 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -27,6 +27,10 @@ class TestCompute(base.BaseFunctionalTestCase): def setUp(self): + # OS_TEST_TIMEOUT is 60 sec by default + # but on a bad day, test_attach_detach_volume can take more time. + self.TIMEOUT_SCALING_FACTOR = 1.5 + super(TestCompute, self).setUp() self.flavor = pick_flavor(self.user_cloud.list_flavors()) if self.flavor is None: @@ -66,6 +70,19 @@ def test_create_and_delete_server(self): self.user_cloud.delete_server(self.server_name, wait=True)) self.assertIsNone(self.user_cloud.get_server(self.server_name)) + def test_attach_detach_volume(self): + server_name = self.getUniqueString() + self.addCleanup(self._cleanup_servers_and_volumes, server_name) + server = self.user_cloud.create_server( + name=server_name, image=self.image, flavor=self.flavor, + wait=True) + volume = self.user_cloud.create_volume(1) + vol_attachment = self.user_cloud.attach_volume(server, volume) + for key in ('device', 'serverId', 'volumeId'): + self.assertIn(key, vol_attachment) + self.assertTrue(vol_attachment[key]) # assert string is not empty + self.assertIsNone(self.user_cloud.detach_volume(server, volume)) + def test_list_all_servers(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) server = self.user_cloud.create_server( diff --git a/shade/tests/unit/test_volume.py b/shade/tests/unit/test_volume.py index 7f0b2d175..e5a410105 100644 --- a/shade/tests/unit/test_volume.py +++ b/shade/tests/unit/test_volume.py @@ -25,15 +25,13 @@ class TestVolume(base.TestCase): def test_attach_volume(self, mock_nova): server = dict(id='server001') volume = dict(id='volume001', status='available', attachments=[]) - rvol = dict(id='volume001', status='attached', - attachments=[ - {'server_id': server['id'], 'device': 'device001'} - ]) - mock_nova.volumes.create_server_volume.return_value = rvol + rattach = {'server_id': server['id'], 'device': 'device001', + 'volumeId': volume['id'], 'id': 'attachmentId'} + mock_nova.volumes.create_server_volume.return_value = rattach ret = self.cloud.attach_volume(server, volume, wait=False) - self.assertEqual(rvol, ret) + self.assertEqual(rattach, ret) mock_nova.volumes.create_server_volume.assert_called_once_with( volume_id=volume['id'], server_id=server['id'], device=None ) @@ -60,6 +58,7 @@ def test_attach_volume_wait(self, mock_nova, mock_get): id=volume['id'], status='attached', attachments=[{'server_id': server['id'], 'device': 'device001'}] ) + mock_get.side_effect = iter([volume, attached_volume]) # defaults to wait=True @@ -69,7 +68,8 @@ def test_attach_volume_wait(self, mock_nova, mock_get): volume_id=volume['id'], server_id=server['id'], device=None ) self.assertEqual(2, mock_get.call_count) - self.assertEqual(attached_volume, ret) + self.assertEqual(mock_nova.volumes.create_server_volume.return_value, + ret) @mock.patch.object(shade.OpenStackCloud, 'get_volume') @mock.patch.object(shade.OpenStackCloud, 'nova_client') @@ -140,20 +140,6 @@ def test_detach_volume_exception(self, mock_nova): ): self.cloud.detach_volume(server, volume, wait=False) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_detach_volume_not_attached(self, mock_nova): - server = dict(id='server001') - volume = dict(id='volume001', - attachments=[ - {'server_id': 'server999', 'device': 'device001'} - ]) - with testtools.ExpectedException( - shade.OpenStackCloudException, - "Volume %s is not attached to server %s" % ( - volume['id'], server['id']) - ): - self.cloud.detach_volume(server, volume, wait=False) - @mock.patch.object(shade.OpenStackCloud, 'get_volume') @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_detach_volume_wait(self, mock_nova, mock_get): From 204fb73dcdf54b7952e2ab34488a6168b7e03a2a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 20 Mar 2017 11:12:19 -0500 Subject: [PATCH 1371/3836] Get rid of magnumclient dependency One more client library down. Note there is a change to one of the tests. That's mainly because testtools is not matching the exception the way an end user would. We'll follow up on it, but it's not a real issue. Change-Id: Ic6d7a37799e72bfb1bef7aeaf8f4894aed27dcea --- .../remove-magnumclient-875b3e513f98f57c.yaml | 4 ++ requirements.txt | 1 - shade/_tasks.py | 26 ---------- shade/_utils.py | 2 +- shade/openstackcloud.py | 51 ++++++++++--------- shade/operatorcloud.py | 2 +- shade/tests/unit/test_cluster_templates.py | 12 +++-- 7 files changed, 41 insertions(+), 57 deletions(-) create mode 100644 releasenotes/notes/remove-magnumclient-875b3e513f98f57c.yaml diff --git a/releasenotes/notes/remove-magnumclient-875b3e513f98f57c.yaml b/releasenotes/notes/remove-magnumclient-875b3e513f98f57c.yaml new file mode 100644 index 000000000..249d1725b --- /dev/null +++ b/releasenotes/notes/remove-magnumclient-875b3e513f98f57c.yaml @@ -0,0 +1,4 @@ +--- +upgrade: + - magnumclient is no longer a direct dependency as + magnum API calls are now made directly via REST. diff --git a/requirements.txt b/requirements.txt index f7efdcfb9..d671b3a60 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,6 +18,5 @@ python-neutronclient>=2.3.10 python-ironicclient>=0.10.0 python-heatclient>=1.0.0 python-designateclient>=2.1.0 -python-magnumclient>=2.1.0 dogpile.cache>=0.5.3 diff --git a/shade/_tasks.py b/shade/_tasks.py index 372c9eab7..ef101015c 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -824,32 +824,6 @@ def main(self, client): return client.neutron_client.delete_quota(**self.args) -class ClusterTemplateList(task_manager.Task): - def main(self, client): - return client.magnum_client.baymodels.list(**self.args) - - -class ClusterTemplateCreate(task_manager.Task): - def main(self, client): - return client.magnum_client.baymodels.create(**self.args) - - -class ClusterTemplateDelete(task_manager.Task): - def main(self, client): - return client.magnum_client.baymodels.delete(self.args['id']) - - -class ClusterTemplateUpdate(task_manager.Task): - def main(self, client): - return client.magnum_client.baymodels.update( - self.args['id'], self.args['patch']) - - -class MagnumServicesList(task_manager.Task): - def main(self, client): - return client.magnum_client.mservices.list(detail=False) - - class NovaLimitsGet(task_manager.Task): def main(self, client): return client.nova_client.limits.get(**self.args).to_dict() diff --git a/shade/_utils.py b/shade/_utils.py index bf2153cff..f54e8809b 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -664,7 +664,7 @@ def generate_patches_from_kwargs(operation, **kwargs): 'value': v, 'path': '/%s' % k} patches.append(patch) - return patches + return sorted(patches) class FileSegment(object): diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index f7dc0be25..7062776eb 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -31,7 +31,6 @@ from six.moves import urllib import cinderclient.exceptions as cinder_exceptions -import magnumclient.exceptions as magnum_exceptions from heatclient import exc as heat_exceptions import keystoneauth1.exceptions import novaclient.exceptions as nova_exceptions @@ -1192,6 +1191,10 @@ def trove_client(self): @property def magnum_client(self): + warnings.warn( + 'Using shade to get a magnum object is deprecated. If you' + ' need a raw magnumclient.client.Client object, please use' + ' make_legacy_client in os-client-config instead') if self._magnum_client is None: self._magnum_client = self._get_client('container-infra') return self._magnum_client @@ -7126,8 +7129,8 @@ def list_cluster_templates(self, detail=False): the OpenStack API call. """ with _utils.shade_exceptions("Error fetching cluster template list"): - cluster_templates = self.manager.submit_task( - _tasks.ClusterTemplateList(detail=True)) + cluster_templates = self._container_infra_client.get( + '/baymodels/detail') return self._normalize_cluster_templates(cluster_templates) list_baymodels = list_cluster_templates @@ -7192,14 +7195,18 @@ def create_cluster_template( :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call """ - with _utils.shade_exceptions( - "Error creating cluster template of name" - " {cluster_template_name}".format( - cluster_template_name=name)): - cluster_template = self.manager.submit_task( - _tasks.ClusterTemplateCreate( - name=name, image_id=image_id, - keypair_id=keypair_id, coe=coe, **kwargs)) + error_message = ("Error creating cluster template of name" + " {cluster_template_name}".format( + cluster_template_name=name)) + with _utils.shade_exceptions(error_message): + body = kwargs.copy() + body['name'] = name + body['image_id'] = image_id + body['keypair_id'] = keypair_id + body['coe'] = coe + + cluster_template = self._container_infra_client.post( + '/baymodels', json=body) self.list_cluster_templates.invalidate(self) return cluster_template @@ -7215,7 +7222,6 @@ def delete_cluster_template(self, name_or_id): :raises: OpenStackCloudException on operation error. """ - self.list_cluster_templates.invalidate(self) cluster_template = self.get_cluster_template(name_or_id) if not cluster_template: @@ -7226,16 +7232,10 @@ def delete_cluster_template(self, name_or_id): return False with _utils.shade_exceptions("Error in deleting cluster template"): - try: - self.manager.submit_task( - _tasks.ClusterTemplateDelete(id=cluster_template['id'])) - except magnum_exceptions.NotFound: - self.log.debug( - "Cluster template %(id)s not found when deleting." - " Ignoring.", {'id': cluster_template['id']}) - return False + self._container_infra_client.delete( + '/baymodels/{id}'.format(id=cluster_template['id'])) + self.list_cluster_templates.invalidate(self) - self.list_cluster_templates.invalidate(self) return True delete_baymodel = delete_cluster_template @@ -7268,12 +7268,15 @@ def update_cluster_template(self, name_or_id, operation, **kwargs): "%s operation not in 'add', 'replace', 'remove'" % operation) patches = _utils.generate_patches_from_kwargs(operation, **kwargs) + # No need to fire an API call if there is an empty patch + if not patches: + return cluster_template with _utils.shade_exceptions( "Error updating cluster template {0}".format(name_or_id)): - self.manager.submit_task( - _tasks.ClusterTemplateUpdate( - id=cluster_template['id'], patch=patches)) + self._container_infra_client.patch( + '/baymodels/{id}'.format(id=cluster_template['id']), + json=patches) new_cluster_template = self.get_cluster_template(name_or_id) return new_cluster_template diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 908bb2319..bed069af8 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -2252,4 +2252,4 @@ def list_magnum_services(self): """ with _utils.shade_exceptions("Error fetching Magnum services list"): return self._normalize_magnum_services( - self.manager.submit_task(_tasks.MagnumServicesList())) + self._container_infra_client.get('/mservices')) diff --git a/shade/tests/unit/test_cluster_templates.py b/shade/tests/unit/test_cluster_templates.py index b3e2776ee..dbab19bdb 100644 --- a/shade/tests/unit/test_cluster_templates.py +++ b/shade/tests/unit/test_cluster_templates.py @@ -144,10 +144,14 @@ def test_create_cluster_template_exception(self): method='POST', uri='https://container-infra.example.com/v1/baymodels', status_code=403)]) - with testtools.ExpectedException( - shade.OpenStackCloudException, - "Error creating cluster template of name fake-cluster-template" - ): + # TODO(mordred) requests here doens't give us a great story + # for matching the old error message text. Investigate plumbing + # an error message in to the adapter call so that we can give a + # more informative error. Also, the test was originally catching + # OpenStackCloudException - but for some reason testtools will not + # match the more specific HTTPError, even though it's a subclass + # of OpenStackCloudException. + with testtools.ExpectedException(shade.OpenStackCloudHTTPError): self.cloud.create_cluster_template('fake-cluster-template') self.assert_calls() From 9f2287c2df9030db1be1415b75a1a910545f0d8d Mon Sep 17 00:00:00 2001 From: Eric Lafontaine Date: Wed, 22 Mar 2017 15:54:59 -0400 Subject: [PATCH 1372/3836] Accept device_id option when updating ports This sets device_id as a valid kwarg so that it can be passed in when updating ports. Fixes ansible/ansbile#22662 Change-Id: I8d1f481e22ed5e667687b8cd793da2a31fa6a105 --- shade/openstackcloud.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index b40cbff8a..65cff6ddc 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -6504,7 +6504,7 @@ def create_port(self, network_id, **kwargs): @_utils.valid_kwargs('name', 'admin_state_up', 'fixed_ips', 'security_groups', 'allowed_address_pairs', - 'extra_dhcp_opts', 'device_owner') + 'extra_dhcp_opts', 'device_owner','device_id') def update_port(self, name_or_id, **kwargs): """Update a port @@ -6549,6 +6549,7 @@ def update_port(self, name_or_id, **kwargs): ] :param device_owner: The ID of the entity that uses this port. For example, a DHCP agent. (Optional) + :param device_id: The ID of the resource this port is attached to. :returns: a ``munch.Munch`` describing the updated port. From da7f7ccb2165b24e00eb749ad099611efafc391a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 24 Mar 2017 08:37:31 -0500 Subject: [PATCH 1373/3836] Fix old-style mocking of nova_client When we assign to nova_client, rather than using the decorator form, it persists through the process lifetime, which pollutes other tests. This shows up when you start trying to replace nova_client mocks with requests_mock. Change-Id: I056085c9b80868a78c95ddda0aeae11865015ad1 --- shade/tests/unit/test_create_server.py | 267 ++++++++---------- shade/tests/unit/test_rebuild_server.py | 162 +++++------ .../tests/unit/test_server_delete_metadata.py | 36 +-- shade/tests/unit/test_server_set_metadata.py | 36 +-- shade/tests/unit/test_update_server.py | 53 ++-- 5 files changed, 246 insertions(+), 308 deletions(-) diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index fc483c731..0e4816f8c 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -17,7 +17,6 @@ Tests for the `create_server` command. """ -from mock import patch, Mock import mock from shade import meta from shade import OpenStackCloud @@ -28,148 +27,126 @@ class TestCreateServer(base.RequestsMockTestCase): - def test_create_server_with_create_exception(self): + @mock.patch.object(OpenStackCloud, 'nova_client') + def test_create_server_with_create_exception(self, mock_nova): """ Test that an exception in the novaclient create raises an exception in create_server. """ - with patch("shade.OpenStackCloud.nova_client"): - config = { - "servers.create.side_effect": Exception("exception"), - } - OpenStackCloud.nova_client = Mock(**config) - self.assertRaises( - OpenStackCloudException, self.cloud.create_server, - 'server-name', {'id': 'image-id'}, {'id': 'flavor-id'}) - - def test_create_server_with_get_exception(self): + mock_nova.servers.create.side_effect = Exception("exception") + self.assertRaises( + OpenStackCloudException, self.cloud.create_server, + 'server-name', {'id': 'image-id'}, {'id': 'flavor-id'}) + + @mock.patch.object(OpenStackCloud, 'nova_client') + def test_create_server_with_get_exception(self, mock_nova): """ Test that an exception when attempting to get the server instance via the novaclient raises an exception in create_server. """ - with patch("shade.OpenStackCloud.nova_client"): - config = { - "servers.create.return_value": Mock(status="BUILD"), - "servers.get.side_effect": Exception("exception") - } - OpenStackCloud.nova_client = Mock(**config) - self.assertRaises( - OpenStackCloudException, self.cloud.create_server, - 'server-name', {'id': 'image-id'}, {'id': 'flavor-id'}) - - def test_create_server_with_server_error(self): + mock_nova.servers.create.return_value = mock.Mock(status="BUILD") + mock_nova.servers.get.side_effect = Exception("exception") + self.assertRaises( + OpenStackCloudException, self.cloud.create_server, + 'server-name', {'id': 'image-id'}, {'id': 'flavor-id'}) + + @mock.patch.object(OpenStackCloud, 'nova_client') + def test_create_server_with_server_error(self, mock_nova): """ Test that a server error before we return or begin waiting for the server instance spawn raises an exception in create_server. """ build_server = fakes.FakeServer('1234', '', 'BUILD') error_server = fakes.FakeServer('1234', '', 'ERROR') - with patch("shade.OpenStackCloud.nova_client"): - config = { - "servers.create.return_value": build_server, - "servers.get.return_value": error_server, - } - OpenStackCloud.nova_client = Mock(**config) - self.assertRaises( - OpenStackCloudException, self.cloud.create_server, - 'server-name', {'id': 'image-id'}, {'id': 'flavor-id'}) - - def test_create_server_wait_server_error(self): + mock_nova.servers.create.return_value = build_server + mock_nova.servers.get.return_value = error_server + self.assertRaises( + OpenStackCloudException, self.cloud.create_server, + 'server-name', {'id': 'image-id'}, {'id': 'flavor-id'}) + + @mock.patch.object(OpenStackCloud, 'nova_client') + def test_create_server_wait_server_error(self, mock_nova): """ Test that a server error while waiting for the server to spawn raises an exception in create_server. """ - with patch("shade.OpenStackCloud.nova_client"): - build_server = fakes.FakeServer('1234', '', 'BUILD') - error_server = fakes.FakeServer('1234', '', 'ERROR') - fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', - '1.1.1.1', '2.2.2.2', - '5678') - config = { - "servers.create.return_value": build_server, - "servers.get.return_value": build_server, - "servers.list.side_effect": [ - [build_server], [error_server]], - "floating_ips.list.return_value": [fake_floating_ip] - } - OpenStackCloud.nova_client = Mock(**config) - self.assertRaises( - OpenStackCloudException, - self.cloud.create_server, - 'server-name', dict(id='image-id'), - dict(id='flavor-id'), wait=True) - - def test_create_server_with_timeout(self): + build_server = fakes.FakeServer('1234', '', 'BUILD') + error_server = fakes.FakeServer('1234', '', 'ERROR') + fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', + '1.1.1.1', '2.2.2.2', + '5678') + mock_nova.servers.create.return_value = build_server + mock_nova.servers.get.return_value = build_server + mock_nova.servers.list.side_effect = [[build_server], [error_server]] + mock_nova.floating_ips.list.return_value = [fake_floating_ip] + self.assertRaises( + OpenStackCloudException, + self.cloud.create_server, + 'server-name', dict(id='image-id'), + dict(id='flavor-id'), wait=True) + + @mock.patch.object(OpenStackCloud, 'nova_client') + def test_create_server_with_timeout(self, mock_nova): """ Test that a timeout while waiting for the server to spawn raises an exception in create_server. """ - with patch("shade.OpenStackCloud.nova_client"): - fake_server = fakes.FakeServer('1234', '', 'BUILD') - config = { - "servers.create.return_value": fake_server, - "servers.get.return_value": fake_server, - "servers.list.return_value": [fake_server], - } - OpenStackCloud.nova_client = Mock(**config) - self.assertRaises( - OpenStackCloudTimeout, - self.cloud.create_server, - 'server-name', - dict(id='image-id'), dict(id='flavor-id'), - wait=True, timeout=0.01) - - def test_create_server_no_wait(self): + fake_server = fakes.FakeServer('1234', '', 'BUILD') + mock_nova.servers.create.return_value = fake_server + mock_nova.servers.get.return_value = fake_server + mock_nova.servers.list.return_value = [fake_server] + self.assertRaises( + OpenStackCloudTimeout, + self.cloud.create_server, + 'server-name', + dict(id='image-id'), dict(id='flavor-id'), + wait=True, timeout=0.01) + + @mock.patch.object(OpenStackCloud, 'nova_client') + def test_create_server_no_wait(self, mock_nova): """ Test that create_server with no wait and no exception in the novaclient create call returns the server instance. """ - with patch("shade.OpenStackCloud.nova_client"): - fake_server = fakes.FakeServer('1234', '', 'BUILD') - fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', - '1.1.1.1', '2.2.2.2', - '5678') - config = { - "servers.create.return_value": fake_server, - "servers.get.return_value": fake_server, - "floating_ips.list.return_value": [fake_floating_ip] - } - OpenStackCloud.nova_client = Mock(**config) - self.assertEqual( - self.cloud._normalize_server( - meta.obj_to_dict(fake_server)), - self.cloud.create_server( - name='server-name', - image=dict(id='image=id'), - flavor=dict(id='flavor-id'))) - - def test_create_server_with_admin_pass_no_wait(self): + fake_server = fakes.FakeServer('1234', '', 'BUILD') + fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', + '1.1.1.1', '2.2.2.2', + '5678') + mock_nova.servers.create.return_value = fake_server + mock_nova.servers.get.return_value = fake_server + mock_nova.floating_ips.list.return_value = [fake_floating_ip] + self.assertEqual( + self.cloud._normalize_server( + meta.obj_to_dict(fake_server)), + self.cloud.create_server( + name='server-name', + image=dict(id='image=id'), + flavor=dict(id='flavor-id'))) + + @mock.patch.object(OpenStackCloud, 'nova_client') + def test_create_server_with_admin_pass_no_wait(self, mock_nova): """ Test that a server with an admin_pass passed returns the password """ - with patch("shade.OpenStackCloud.nova_client"): - fake_server = fakes.FakeServer('1234', '', 'BUILD') - fake_create_server = fakes.FakeServer('1234', '', 'BUILD', - adminPass='ooBootheiX0edoh') - fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', - '1.1.1.1', '2.2.2.2', - '5678') - config = { - "servers.create.return_value": fake_create_server, - "servers.get.return_value": fake_server, - "floating_ips.list.return_value": [fake_floating_ip] - } - OpenStackCloud.nova_client = Mock(**config) - self.assertEqual( - self.cloud._normalize_server( - meta.obj_to_dict(fake_create_server)), - self.cloud.create_server( - name='server-name', image=dict(id='image=id'), - flavor=dict(id='flavor-id'), admin_pass='ooBootheiX0edoh')) - - @patch.object(OpenStackCloud, "wait_for_server") - @patch.object(OpenStackCloud, "nova_client") + fake_server = fakes.FakeServer('1234', '', 'BUILD') + fake_create_server = fakes.FakeServer('1234', '', 'BUILD', + adminPass='ooBootheiX0edoh') + fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', + '1.1.1.1', '2.2.2.2', + '5678') + mock_nova.servers.create.return_value = fake_create_server + mock_nova.servers.get.return_value = fake_server + mock_nova.floating_ips.list.return_value = [fake_floating_ip] + self.assertEqual( + self.cloud._normalize_server( + meta.obj_to_dict(fake_create_server)), + self.cloud.create_server( + name='server-name', image=dict(id='image=id'), + flavor=dict(id='flavor-id'), admin_pass='ooBootheiX0edoh')) + + @mock.patch.object(OpenStackCloud, "wait_for_server") + @mock.patch.object(OpenStackCloud, "nova_client") def test_create_server_with_admin_pass_wait(self, mock_nova, mock_wait): """ Test that a server with an admin_pass passed returns the password @@ -199,8 +176,8 @@ def test_create_server_with_admin_pass_wait(self, mock_nova, mock_wait): meta.obj_to_dict(fake_server_with_pass)) ) - @patch.object(OpenStackCloud, "get_active_server") - @patch.object(OpenStackCloud, "get_server") + @mock.patch.object(OpenStackCloud, "get_active_server") + @mock.patch.object(OpenStackCloud, "get_server") def test_wait_for_server(self, mock_get_server, mock_get_active_server): """ Test that waiting for a server returns the server instance when @@ -233,8 +210,8 @@ def test_wait_for_server(self, mock_get_server, mock_get_active_server): self.assertEqual('ACTIVE', server['status']) - @patch.object(OpenStackCloud, 'wait_for_server') - @patch.object(OpenStackCloud, 'nova_client') + @mock.patch.object(OpenStackCloud, 'wait_for_server') + @mock.patch.object(OpenStackCloud, 'nova_client') def test_create_server_wait(self, mock_nova, mock_wait): """ Test that create_server with a wait actually does the wait. @@ -252,37 +229,35 @@ def test_create_server_wait(self, mock_nova, mock_wait): nat_destination=None, ) - @patch('time.sleep') - def test_create_server_no_addresses(self, mock_sleep): + @mock.patch.object(OpenStackCloud, 'add_ips_to_server') + @mock.patch.object(OpenStackCloud, 'nova_client') + @mock.patch('time.sleep') + def test_create_server_no_addresses( + self, mock_sleep, mock_nova, mock_add_ips_to_server): """ Test that create_server with a wait throws an exception if the server doesn't have addresses. """ - with patch("shade.OpenStackCloud.nova_client"): - build_server = fakes.FakeServer('1234', '', 'BUILD') - fake_server = fakes.FakeServer('1234', '', 'ACTIVE') - fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', - '1.1.1.1', '2.2.2.2', - '5678') - config = { - "servers.create.return_value": build_server, - "servers.get.return_value": [build_server, None], - "servers.list.side_effect": [ - [build_server], [fake_server]], - "servers.delete.return_value": None, - "floating_ips.list.return_value": [fake_floating_ip] - } - OpenStackCloud.nova_client = Mock(**config) - self.cloud._SERVER_AGE = 0 - with patch.object(OpenStackCloud, "add_ips_to_server", - return_value=fake_server): - self.assertRaises( - OpenStackCloudException, self.cloud.create_server, - 'server-name', {'id': 'image-id'}, {'id': 'flavor-id'}, - wait=True) - - @patch('shade.OpenStackCloud.nova_client') - @patch('shade.OpenStackCloud.get_network') + build_server = fakes.FakeServer('1234', '', 'BUILD') + fake_server = fakes.FakeServer('1234', '', 'ACTIVE') + fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', + '1.1.1.1', '2.2.2.2', + '5678') + mock_nova.servers.create.return_value = build_server + mock_nova.servers.get.return_value = [build_server, None] + mock_nova.servers.list.side_effect = [[build_server], [fake_server]] + mock_nova.servers.delete.return_value = None + mock_nova.floating_ips.list.return_value = [fake_floating_ip] + mock_add_ips_to_server.return_value = fake_server + self.cloud._SERVER_AGE = 0 + + self.assertRaises( + OpenStackCloudException, self.cloud.create_server, + 'server-name', {'id': 'image-id'}, {'id': 'flavor-id'}, + wait=True) + + @mock.patch('shade.OpenStackCloud.nova_client') + @mock.patch('shade.OpenStackCloud.get_network') def test_create_server_network_with_no_nics(self, mock_get_network, mock_nova): """ @@ -294,8 +269,8 @@ def test_create_server_network_with_no_nics(self, mock_get_network, dict(id='image-id'), dict(id='flavor-id'), network='network-name') mock_get_network.assert_called_once_with(name_or_id='network-name') - @patch('shade.OpenStackCloud.nova_client') - @patch('shade.OpenStackCloud.get_network') + @mock.patch('shade.OpenStackCloud.nova_client') + @mock.patch('shade.OpenStackCloud.get_network') def test_create_server_network_with_empty_nics(self, mock_get_network, mock_nova): diff --git a/shade/tests/unit/test_rebuild_server.py b/shade/tests/unit/test_rebuild_server.py index 3d461d28d..dc28a73ad 100644 --- a/shade/tests/unit/test_rebuild_server.py +++ b/shade/tests/unit/test_rebuild_server.py @@ -19,7 +19,8 @@ Tests for the `rebuild_server` command. """ -from mock import patch, Mock +import mock + from shade import meta from shade import OpenStackCloud from shade.exc import (OpenStackCloudException, OpenStackCloudTimeout) @@ -29,20 +30,18 @@ class TestRebuildServer(base.TestCase): - def test_rebuild_server_rebuild_exception(self): + @mock.patch.object(OpenStackCloud, 'nova_client') + def test_rebuild_server_rebuild_exception(self, mock_nova): """ Test that an exception in the novaclient rebuild raises an exception in rebuild_server. """ - with patch("shade.OpenStackCloud"): - config = { - "servers.rebuild.side_effect": Exception("exception"), - } - OpenStackCloud.nova_client = Mock(**config) - self.assertRaises( - OpenStackCloudException, self.cloud.rebuild_server, "a", "b") + mock_nova.servers.rebuild.side_effect = Exception("exception") + self.assertRaises( + OpenStackCloudException, self.cloud.rebuild_server, "a", "b") - def test_rebuild_server_server_error(self): + @mock.patch.object(OpenStackCloud, 'nova_client') + def test_rebuild_server_server_error(self, mock_nova): """ Test that a server error while waiting for the server to rebuild raises an exception in rebuild_server. @@ -52,108 +51,89 @@ def test_rebuild_server_server_error(self): fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', '1.1.1.1', '2.2.2.2', '5678') - with patch("shade.OpenStackCloud"): - config = { - "servers.rebuild.return_value": rebuild_server, - "servers.list.return_value": [error_server], - "floating_ips.list.return_value": [fake_floating_ip] - } - OpenStackCloud.nova_client = Mock(**config) - self.assertRaises( - OpenStackCloudException, - self.cloud.rebuild_server, "1234", "b", wait=True) + mock_nova.servers.rebuild.return_value = rebuild_server + mock_nova.servers.list.return_value = [error_server] + mock_nova.floating_ips.list.return_value = [fake_floating_ip] + self.assertRaises( + OpenStackCloudException, + self.cloud.rebuild_server, "1234", "b", wait=True) - def test_rebuild_server_timeout(self): + @mock.patch.object(OpenStackCloud, 'nova_client') + def test_rebuild_server_timeout(self, mock_nova): """ Test that a timeout while waiting for the server to rebuild raises an exception in rebuild_server. """ rebuild_server = fakes.FakeServer('1234', '', 'REBUILD') - with patch("shade.OpenStackCloud"): - config = { - "servers.rebuild.return_value": rebuild_server, - "servers.list.return_value": [rebuild_server], - } - OpenStackCloud.nova_client = Mock(**config) - self.assertRaises( - OpenStackCloudTimeout, - self.cloud.rebuild_server, "a", "b", wait=True, timeout=0.001) + mock_nova.servers.rebuild.return_value = rebuild_server + mock_nova.servers.list.return_value = [rebuild_server] + self.assertRaises( + OpenStackCloudTimeout, + self.cloud.rebuild_server, "a", "b", wait=True, timeout=0.001) - def test_rebuild_server_no_wait(self): + @mock.patch.object(OpenStackCloud, 'nova_client') + def test_rebuild_server_no_wait(self, mock_nova): """ Test that rebuild_server with no wait and no exception in the novaclient rebuild call returns the server instance. """ - with patch("shade.OpenStackCloud"): - rebuild_server = fakes.FakeServer('1234', '', 'REBUILD') - config = { - "servers.rebuild.return_value": rebuild_server - } - OpenStackCloud.nova_client = Mock(**config) - self.assertEqual(meta.obj_to_dict(rebuild_server), - self.cloud.rebuild_server("a", "b")) + rebuild_server = fakes.FakeServer('1234', '', 'REBUILD') + mock_nova.servers.rebuild.return_value = rebuild_server + self.assertEqual(meta.obj_to_dict(rebuild_server), + self.cloud.rebuild_server("a", "b")) - def test_rebuild_server_with_admin_pass_no_wait(self): + @mock.patch.object(OpenStackCloud, 'nova_client') + def test_rebuild_server_with_admin_pass_no_wait(self, mock_nova): """ Test that a server with an admin_pass passed returns the password """ - with patch("shade.OpenStackCloud"): - rebuild_server = fakes.FakeServer('1234', '', 'REBUILD', - adminPass='ooBootheiX0edoh') - config = { - "servers.rebuild.return_value": rebuild_server, - } - OpenStackCloud.nova_client = Mock(**config) - self.assertEqual( - meta.obj_to_dict(rebuild_server), - self.cloud.rebuild_server( - 'a', 'b', admin_pass='ooBootheiX0edoh')) + rebuild_server = fakes.FakeServer('1234', '', 'REBUILD', + adminPass='ooBootheiX0edoh') + mock_nova.servers.rebuild.return_value = rebuild_server + self.assertEqual( + meta.obj_to_dict(rebuild_server), + self.cloud.rebuild_server( + 'a', 'b', admin_pass='ooBootheiX0edoh')) - def test_rebuild_server_with_admin_pass_wait(self): + @mock.patch.object(OpenStackCloud, 'nova_client') + def test_rebuild_server_with_admin_pass_wait(self, mock_nova): """ Test that a server with an admin_pass passed returns the password """ - with patch("shade.OpenStackCloud"): - rebuild_server = fakes.FakeServer('1234', '', 'REBUILD', - adminPass='ooBootheiX0edoh') - active_server = fakes.FakeServer('1234', '', 'ACTIVE') - ret_active_server = fakes.FakeServer('1234', '', 'ACTIVE', - adminPass='ooBootheiX0edoh') - fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', - '1.1.1.1', '2.2.2.2', - '5678') - config = { - "servers.rebuild.return_value": rebuild_server, - "servers.list.return_value": [active_server], - "floating_ips.list.return_value": [fake_floating_ip] - } - OpenStackCloud.nova_client = Mock(**config) - self.cloud.name = 'cloud-name' - self.assertEqual( - self.cloud._normalize_server( - meta.obj_to_dict(ret_active_server)), - self.cloud.rebuild_server( - "1234", "b", wait=True, admin_pass='ooBootheiX0edoh')) + rebuild_server = fakes.FakeServer('1234', '', 'REBUILD', + adminPass='ooBootheiX0edoh') + active_server = fakes.FakeServer('1234', '', 'ACTIVE') + ret_active_server = fakes.FakeServer('1234', '', 'ACTIVE', + adminPass='ooBootheiX0edoh') + fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', + '1.1.1.1', '2.2.2.2', + '5678') + mock_nova.servers.rebuild.return_value = rebuild_server + mock_nova.servers.list.return_value = [active_server] + mock_nova.floating_ips.list.return_value = [fake_floating_ip] + self.cloud.name = 'cloud-name' + self.assertEqual( + self.cloud._normalize_server( + meta.obj_to_dict(ret_active_server)), + self.cloud.rebuild_server( + "1234", "b", wait=True, admin_pass='ooBootheiX0edoh')) - def test_rebuild_server_wait(self): + @mock.patch.object(OpenStackCloud, 'nova_client') + def test_rebuild_server_wait(self, mock_nova): """ Test that rebuild_server with a wait returns the server instance when its status changes to "ACTIVE". """ - with patch("shade.OpenStackCloud"): - rebuild_server = fakes.FakeServer('1234', '', 'REBUILD') - active_server = fakes.FakeServer('1234', '', 'ACTIVE') - fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', - '1.1.1.1', '2.2.2.2', - '5678') - config = { - "servers.rebuild.return_value": rebuild_server, - "servers.list.return_value": [active_server], - "floating_ips.list.return_value": [fake_floating_ip] - } - OpenStackCloud.nova_client = Mock(**config) - self.cloud.name = 'cloud-name' - self.assertEqual( - self.cloud._normalize_server( - meta.obj_to_dict(active_server)), - self.cloud.rebuild_server("1234", "b", wait=True)) + rebuild_server = fakes.FakeServer('1234', '', 'REBUILD') + active_server = fakes.FakeServer('1234', '', 'ACTIVE') + fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', + '1.1.1.1', '2.2.2.2', + '5678') + mock_nova.servers.rebuild.return_value = rebuild_server + mock_nova.servers.list.return_value = [active_server] + mock_nova.floating_ips.list.return_value = [fake_floating_ip] + self.cloud.name = 'cloud-name' + self.assertEqual( + self.cloud._normalize_server( + meta.obj_to_dict(active_server)), + self.cloud.rebuild_server("1234", "b", wait=True)) diff --git a/shade/tests/unit/test_server_delete_metadata.py b/shade/tests/unit/test_server_delete_metadata.py index 3ba7da8a5..b34cf507f 100644 --- a/shade/tests/unit/test_server_delete_metadata.py +++ b/shade/tests/unit/test_server_delete_metadata.py @@ -17,7 +17,8 @@ Tests for the `delete_server_metadata` command. """ -from mock import patch, Mock +import mock + from shade import OpenStackCloud from shade.exc import OpenStackCloudException from shade.tests.unit import base @@ -25,33 +26,26 @@ class TestServerDeleteMetadata(base.TestCase): - def test_server_delete_metadata_with_delete_meta_exception(self): + @mock.patch.object(OpenStackCloud, 'nova_client') + def test_server_delete_metadata_with_exception(self, mock_nova): """ Test that a generic exception in the novaclient delete_meta raises an exception in delete_server_metadata. """ - with patch("shade.OpenStackCloud"): - config = { - "servers.delete_meta.side_effect": Exception("exception"), - } - OpenStackCloud.nova_client = Mock(**config) + mock_nova.servers.delete_meta.side_effect = Exception("exception") - self.assertRaises( - OpenStackCloudException, self.cloud.delete_server_metadata, - {'id': 'server-id'}, ['key']) + self.assertRaises( + OpenStackCloudException, self.cloud.delete_server_metadata, + {'id': 'server-id'}, ['key']) - def test_server_delete_metadata_with_exception_reraise(self): + @mock.patch.object(OpenStackCloud, 'nova_client') + def test_server_delete_metadata_with_exception_reraise(self, mock_nova): """ Test that an OpenStackCloudException exception gets re-raised in delete_server_metadata. """ - with patch("shade.OpenStackCloud"): - config = { - "servers.delete_meta.side_effect": - OpenStackCloudException("exception"), - } - OpenStackCloud.nova_client = Mock(**config) - - self.assertRaises( - OpenStackCloudException, self.cloud.delete_server_metadata, - 'server-id', ['key']) + mock_nova.servers.delete_meta.side_effect = OpenStackCloudException("") + + self.assertRaises( + OpenStackCloudException, self.cloud.delete_server_metadata, + 'server-id', ['key']) diff --git a/shade/tests/unit/test_server_set_metadata.py b/shade/tests/unit/test_server_set_metadata.py index a4257d7a7..a892cddaf 100644 --- a/shade/tests/unit/test_server_set_metadata.py +++ b/shade/tests/unit/test_server_set_metadata.py @@ -17,7 +17,8 @@ Tests for the `set_server_metadata` command. """ -from mock import patch, Mock +import mock + from shade import OpenStackCloud from shade.exc import OpenStackCloudException from shade.tests.unit import base @@ -25,33 +26,26 @@ class TestServerSetMetadata(base.TestCase): - def test_server_set_metadata_with_set_meta_exception(self): + @mock.patch.object(OpenStackCloud, 'nova_client') + def test_server_set_metadata_with_set_meta_exception(self, mock_nova): """ Test that a generic exception in the novaclient set_meta raises an exception in set_server_metadata. """ - with patch("shade.OpenStackCloud"): - config = { - "servers.set_meta.side_effect": Exception("exception"), - } - OpenStackCloud.nova_client = Mock(**config) + mock_nova.servers.set_meta.side_effect = Exception("exception") - self.assertRaises( - OpenStackCloudException, self.cloud.set_server_metadata, - {'id': 'server-id'}, {'meta': 'data'}) + self.assertRaises( + OpenStackCloudException, self.cloud.set_server_metadata, + {'id': 'server-id'}, {'meta': 'data'}) - def test_server_set_metadata_with_exception_reraise(self): + @mock.patch.object(OpenStackCloud, 'nova_client') + def test_server_set_metadata_with_exception_reraise(self, mock_nova): """ Test that an OpenStackCloudException exception gets re-raised in set_server_metadata. """ - with patch("shade.OpenStackCloud"): - config = { - "servers.set_meta.side_effect": - OpenStackCloudException("exception"), - } - OpenStackCloud.nova_client = Mock(**config) - - self.assertRaises( - OpenStackCloudException, self.cloud.set_server_metadata, - 'server-id', {'meta': 'data'}) + mock_nova.servers.set_meta.side_effect = OpenStackCloudException("") + + self.assertRaises( + OpenStackCloudException, self.cloud.set_server_metadata, + 'server-id', {'meta': 'data'}) diff --git a/shade/tests/unit/test_update_server.py b/shade/tests/unit/test_update_server.py index 28e311ddc..6c503d33b 100644 --- a/shade/tests/unit/test_update_server.py +++ b/shade/tests/unit/test_update_server.py @@ -17,7 +17,8 @@ Tests for the `update_server` command. """ -from mock import patch, Mock +import mock + from shade import OpenStackCloud from shade.exc import OpenStackCloudException from shade.tests import fakes @@ -26,38 +27,32 @@ class TestUpdateServer(base.TestCase): - def test_update_server_with_update_exception(self): + @mock.patch.object(OpenStackCloud, 'nova_client') + def test_update_server_with_update_exception(self, mock_nova): """ Test that an exception in the novaclient update raises an exception in update_server. """ - with patch("shade.OpenStackCloud"): - config = { - "servers.update.side_effect": Exception("exception"), - } - OpenStackCloud.nova_client = Mock(**config) - self.assertRaises( - OpenStackCloudException, self.cloud.update_server, - 'server-name') - - def test_update_server_name(self): + mock_nova.servers.update.side_effect = Exception("exception") + self.assertRaises( + OpenStackCloudException, self.cloud.update_server, + 'server-name') + + @mock.patch.object(OpenStackCloud, 'nova_client') + def test_update_server_name(self, mock_nova): """ Test that update_server updates the name without raising any exception """ - with patch("shade.OpenStackCloud"): - fake_server = fakes.FakeServer('1234', 'server-name', 'ACTIVE') - fake_update_server = fakes.FakeServer('1234', 'server-name2', - 'ACTIVE') - fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', - '1.1.1.1', '2.2.2.2', - '5678') - config = { - "servers.list.return_value": [fake_server], - "servers.update.return_value": fake_update_server, - "floating_ips.list.return_value": [fake_floating_ip] - } - OpenStackCloud.nova_client = Mock(**config) - self.assertEqual( - 'server-name2', - self.cloud.update_server( - 'server-name', name='server-name2')['name']) + fake_server = fakes.FakeServer('1234', 'server-name', 'ACTIVE') + fake_update_server = fakes.FakeServer('1234', 'server-name2', + 'ACTIVE') + fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', + '1.1.1.1', '2.2.2.2', + '5678') + mock_nova.servers.list.return_value = [fake_server] + mock_nova.servers.update.return_value = fake_update_server + mock_nova.floating_ips.list.return_value = [fake_floating_ip] + self.assertEqual( + 'server-name2', + self.cloud.update_server( + 'server-name', name='server-name2')['name']) From 0112f8522db493d428e3db47fb560ed2be1439c9 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 24 Mar 2017 08:49:27 -0500 Subject: [PATCH 1374/3836] Migrate server console tests to requests_mock Change-Id: Ic585d1c7ba43a4ba65b0fcfbe03fe9445a9c6ad6 --- shade/tests/fakes.py | 49 ++++++++++++++ shade/tests/unit/test_server_console.py | 90 ++++++++++++++++--------- 2 files changed, 109 insertions(+), 30 deletions(-) diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 45ab55420..3497d7f34 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -56,6 +56,55 @@ def make_fake_flavor(flavor_id, name, ram=100, disk=1600, vcpus=24): FAKE_FLAVOR_LIST = [FAKE_FLAVOR, FAKE_CHOCOLATE_FLAVOR, FAKE_STRAWBERRY_FLAVOR] +def make_fake_server(server_id, name, status='ACTIVE'): + return { + "OS-EXT-STS:task_state": None, + "addresses": { + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:df:b0:8d", + "version": 6, + "addr": "fddb:b018:307:0:f816:3eff:fedf:b08d", + "OS-EXT-IPS:type": "fixed"}, + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:df:b0:8d", + "version": 4, + "addr": "10.1.0.9", + "OS-EXT-IPS:type": "fixed"}, + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:df:b0:8d", + "version": 4, + "addr": "172.24.5.5", + "OS-EXT-IPS:type": "floating"}]}, + "links": [], + "image": {"id": "217f3ab1-03e0-4450-bf27-63d52b421e9e", + "links": []}, + "OS-EXT-STS:vm_state": "active", + "OS-SRV-USG:launched_at": "2017-03-23T23:57:38.000000", + "flavor": {"id": "64", + "links": []}, + "id": server_id, + "security_groups": [{"name": "default"}], + "user_id": "9c119f4beaaa438792ce89387362b3ad", + "OS-DCF:diskConfig": "MANUAL", + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "OS-EXT-AZ:availability_zone": "nova", + "metadata": {}, + "status": status, + "updated": "2017-03-23T23:57:39Z", + "hostId": "89d165f04384e3ffa4b6536669eb49104d30d6ca832bba2684605dbc", + "OS-SRV-USG:terminated_at": None, + "key_name": None, + "name": name, + "created": "2017-03-23T23:57:12Z", + "tenant_id": "fdbf563e9d474696b35667254e65b45b", + "os-extended-volumes:volumes_attached": [], + "config_drive": "True"} + + class FakeEndpoint(object): def __init__(self, id, service_id, region, publicurl, internalurl=None, adminurl=None): diff --git a/shade/tests/unit/test_server_console.py b/shade/tests/unit/test_server_console.py index 2debf4635..ebd934a90 100644 --- a/shade/tests/unit/test_server_console.py +++ b/shade/tests/unit/test_server_console.py @@ -12,48 +12,78 @@ import mock -import novaclient.exceptions as nova_exceptions +import uuid import shade from shade.tests.unit import base from shade.tests import fakes -class TestServerConsole(base.TestCase): +class TestServerConsole(base.RequestsMockTestCase): - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_get_server_console_dict(self, mock_nova): - server = dict(id='12345') - self.cloud.get_server_console(server) + def setUp(self): + super(TestServerConsole, self).setUp() - mock_nova.servers.list.assert_not_called() - mock_nova.servers.get_console_output.assert_called_once_with( - server='12345', length=None) + self.server_id = str(uuid.uuid4()) + self.server_name = self.getUniqueString('name') + self.server = fakes.make_fake_server( + server_id=self.server_id, name=self.server_name) + self.output = self.getUniqueString('output') - @mock.patch.object(shade.OpenStackCloud, 'has_service') - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_get_server_console_name_or_id(self, mock_nova, mock_has_service): - server = '12345' + def test_get_server_console_dict(self): + self.register_uris([ + dict(method='POST', + uri='{endpoint}/servers/{id}/action'.format( + endpoint=fakes.COMPUTE_ENDPOINT, + id=self.server_id), + json={"output": self.output}, + validate=dict( + json={'os-getConsoleOutput': {'length': None}})) + ]) + + self.assertEqual( + self.output, self.cloud.get_server_console(self.server)) + self.assert_calls() - fake_server = fakes.FakeServer(server, '', 'ACTIVE') - mock_nova.servers.get.return_value = fake_server - mock_nova.servers.list.return_value = [fake_server] + @mock.patch.object(shade.OpenStackCloud, 'has_service') + def test_get_server_console_name_or_id(self, mock_has_service): + # Turn off neutron for now - we don't _actually_ want to show all + # of the nova normalization calls. + # TODO(mordred) Tell get_server_console to tell shade to skip + # adding normalization, since we don't consume them mock_has_service.return_value = False - self.cloud.get_server_console(server) + self.register_uris([ + dict(method='GET', + uri='{endpoint}/servers/detail'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={"servers": [self.server]}), + dict(method='POST', + uri='{endpoint}/servers/{id}/action'.format( + endpoint=fakes.COMPUTE_ENDPOINT, + id=self.server_id), + json={"output": self.output}, + validate=dict( + json={'os-getConsoleOutput': {'length': None}})) + ]) + + self.assertEqual( + self.output, self.cloud.get_server_console(self.server['id'])) + + self.assert_calls() + + def test_get_server_console_no_console(self): - mock_nova.servers.get_console_output.assert_called_once_with( - server='12345', length=None) + self.register_uris([ + dict(method='POST', + uri='{endpoint}/servers/{id}/action'.format( + endpoint=fakes.COMPUTE_ENDPOINT, + id=self.server_id), + status_code=400, + validate=dict( + json={'os-getConsoleOutput': {'length': None}})) + ]) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_get_server_console_no_console(self, mock_nova): - server = dict(id='12345') - exc = nova_exceptions.BadRequest( - 'There is no such action: os-getConsoleOutput') - mock_nova.servers.get_console_output.side_effect = exc - log = self.cloud.get_server_console(server) + self.assertEqual('', self.cloud.get_server_console(self.server)) - self.assertEqual('', log) - mock_nova.servers.list.assert_not_called() - mock_nova.servers.get_console_output.assert_called_once_with( - server='12345', length=None) + self.assert_calls() From 7ef65f109b06151391344c37017f0f73400d64c2 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 24 Mar 2017 09:23:12 -0500 Subject: [PATCH 1375/3836] Migrate get_server_console to REST It was using an unfortunate 'feature' of shade's TaskManager ... raw=True. Since we're migrating to REST anyway, just go ahead and do this one. Change-Id: Id44b89af66fbf4df202653c6f658603518537944 --- shade/openstackcloud.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index b40cbff8a..405d46904 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2588,17 +2588,11 @@ def get_server_console(self, server, length=None): "Console log requested for invalid server") try: - return self.manager.submitTask( - _tasks.ServerConsoleGet(server=server['id'], length=length), - raw=True) - except nova_exceptions.BadRequest: + return self._compute_client.post( + '/servers/{server}/action'.format(server=server['id']), + json={'os-getConsoleOutput': {'length': length}}) + except OpenStackCloudBadRequest: return "" - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Unable to get console log for {server}: {exception}".format( - server=server['id'], exception=str(e))) def get_server(self, name_or_id=None, filters=None, detailed=False): """Get a server by name or ID. From 76d78507fa6c40e98b732cda9d014f9713a5a9c2 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 25 Mar 2017 09:53:02 -0500 Subject: [PATCH 1376/3836] Convert floating_ip_pools unittest to requests_mock Change-Id: I95057bb5357757767f2513eccc0e2a1a864bcdeb --- shade/tests/unit/test_floating_ip_pool.py | 72 ++++++++++++++++------- 1 file changed, 50 insertions(+), 22 deletions(-) diff --git a/shade/tests/unit/test_floating_ip_pool.py b/shade/tests/unit/test_floating_ip_pool.py index fba39e843..2870c5267 100644 --- a/shade/tests/unit/test_floating_ip_pool.py +++ b/shade/tests/unit/test_floating_ip_pool.py @@ -19,38 +19,66 @@ Test floating IP pool resource (managed by nova) """ -from mock import patch -from shade import OpenStackCloud from shade import OpenStackCloudException from shade.tests.unit import base -from shade.tests.fakes import FakeFloatingIPPool +from shade.tests import fakes -class TestFloatingIPPool(base.TestCase): - mock_pools = [ - {'id': 'pool1_id', 'name': 'pool1'}, - {'id': 'pool2_id', 'name': 'pool2'}] +class TestFloatingIPPool(base.RequestsMockTestCase): + mock_pools = [{ + 'NAME_ATTR': 'name', + 'name': u'public', + 'x_openstack_request_ids': [], + 'request_ids': [], + 'HUMAN_ID': False, + 'human_id': None}] - @patch.object(OpenStackCloud, '_has_nova_extension') - @patch.object(OpenStackCloud, 'nova_client') - def test_list_floating_ip_pools( - self, mock_nova_client, mock__has_nova_extension): - mock_nova_client.floating_ip_pools.list.return_value = [ - FakeFloatingIPPool(**p) for p in self.mock_pools - ] - mock__has_nova_extension.return_value = True + def test_list_floating_ip_pools(self): + + self.register_uris([ + dict(method='GET', + uri='{endpoint}/extensions'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'extensions': [{ + u'alias': u'os-floating-ip-pools', + u'updated': u'2014-12-03T00:00:00Z', + u'name': u'FloatingIpPools', + u'links': [], + u'namespace': + u'http://docs.openstack.org/compute/ext/fake_xml', + u'description': u'Floating IPs support.'}]}), + dict(method='GET', + uri='{endpoint}/os-floating-ip-pools'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={"floating_ip_pools": [{"name": "public"}]}) + ]) floating_ip_pools = self.cloud.list_floating_ip_pools() self.assertItemsEqual(floating_ip_pools, self.mock_pools) - @patch.object(OpenStackCloud, '_has_nova_extension') - @patch.object(OpenStackCloud, 'nova_client') - def test_list_floating_ip_pools_exception( - self, mock_nova_client, mock__has_nova_extension): - mock_nova_client.floating_ip_pools.list.side_effect = \ - Exception('whatever') - mock__has_nova_extension.return_value = True + self.assert_calls() + + def test_list_floating_ip_pools_exception(self): + + self.register_uris([ + dict(method='GET', + uri='{endpoint}/extensions'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'extensions': [{ + u'alias': u'os-floating-ip-pools', + u'updated': u'2014-12-03T00:00:00Z', + u'name': u'FloatingIpPools', + u'links': [], + u'namespace': + u'http://docs.openstack.org/compute/ext/fake_xml', + u'description': u'Floating IPs support.'}]}), + dict(method='GET', + uri='{endpoint}/os-floating-ip-pools'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + status_code=404)]) self.assertRaises( OpenStackCloudException, self.cloud.list_floating_ip_pools) + + self.assert_calls() From 35ab23efc8bdf7bdc97b127d7712662e596f2b69 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 25 Mar 2017 09:55:13 -0500 Subject: [PATCH 1377/3836] Strip out novaclient extra attributes Change-Id: Iec82b2d1b2d03ddc47e19646bf13148255d92bc4 --- shade/openstackcloud.py | 4 +++- shade/tests/unit/test_floating_ip_pool.py | 10 ++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 405d46904..29dc76326 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1960,7 +1960,9 @@ def list_floating_ip_pools(self): 'Floating IP pools extension is not available on target cloud') with _utils.shade_exceptions("Error fetching floating IP pool list"): - return self.manager.submit_task(_tasks.FloatingIPPoolList()) + return [ + {'name': p['name']} for p in self.manager.submit_task( + _tasks.FloatingIPPoolList())] def _list_floating_ips(self, filters=None): if self._use_neutron_floating(): diff --git a/shade/tests/unit/test_floating_ip_pool.py b/shade/tests/unit/test_floating_ip_pool.py index 2870c5267..6ebdd9597 100644 --- a/shade/tests/unit/test_floating_ip_pool.py +++ b/shade/tests/unit/test_floating_ip_pool.py @@ -25,13 +25,7 @@ class TestFloatingIPPool(base.RequestsMockTestCase): - mock_pools = [{ - 'NAME_ATTR': 'name', - 'name': u'public', - 'x_openstack_request_ids': [], - 'request_ids': [], - 'HUMAN_ID': False, - 'human_id': None}] + pools = [{'name': u'public'}] def test_list_floating_ip_pools(self): @@ -55,7 +49,7 @@ def test_list_floating_ip_pools(self): floating_ip_pools = self.cloud.list_floating_ip_pools() - self.assertItemsEqual(floating_ip_pools, self.mock_pools) + self.assertItemsEqual(floating_ip_pools, self.pools) self.assert_calls() From 52e68c810a5a3a69239fa0b78b4f4a8440b3f14b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 25 Mar 2017 09:57:03 -0500 Subject: [PATCH 1378/3836] Switch list_floating_ip_pools to REST Change-Id: I28cb3e901b81fa34f3e36f4e3ad70eb798fb3114 --- shade/_tasks.py | 5 ----- shade/openstackcloud.py | 9 +++++++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index ef101015c..9904eba8f 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -449,11 +449,6 @@ def main(self, client): return client.neutron_client.update_floatingip(**self.args) -class FloatingIPPoolList(task_manager.Task): - def main(self, client): - return client.nova_client.floating_ip_pools.list() - - class SubnetCreate(task_manager.Task): def main(self, client): return client.neutron_client.create_subnet(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 29dc76326..b8ac28842 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1952,6 +1952,11 @@ def list_images(self, filter_deleted=True): def list_floating_ip_pools(self): """List all available floating IP pools. + NOTE: This function supports the nova-net view of the world. nova-net + has been deprecated, so it's highly recommended to switch to using + neutron. `get_external_ipv4_floating_networks` is what you should + almost certainly be using. + :returns: A list of floating IP pool ``munch.Munch``. """ @@ -1961,8 +1966,8 @@ def list_floating_ip_pools(self): with _utils.shade_exceptions("Error fetching floating IP pool list"): return [ - {'name': p['name']} for p in self.manager.submit_task( - _tasks.FloatingIPPoolList())] + {'name': p['name']} + for p in self._compute_client.get('os-floating-ip-pools')] def _list_floating_ips(self, filters=None): if self._use_neutron_floating(): From 8059b3c80416297c9ebbfdf689c6c27611ff8ed7 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 27 Mar 2017 11:32:39 -0500 Subject: [PATCH 1379/3836] Add list_availability_zone_names method Nodepool would like to do smart things with scheduling across AZs. In order for it to that, it needs a list of them, which we can provide. Change-Id: I9a3b97ccb797ea7a66a832b03da2bd4afd659097 --- .../notes/list-az-names-a38c277d1192471b.yaml | 3 + shade/openstackcloud.py | 23 ++++++ shade/tests/functional/test_compute.py | 4 + shade/tests/unit/test_availability_zones.py | 79 +++++++++++++++++++ 4 files changed, 109 insertions(+) create mode 100644 releasenotes/notes/list-az-names-a38c277d1192471b.yaml create mode 100644 shade/tests/unit/test_availability_zones.py diff --git a/releasenotes/notes/list-az-names-a38c277d1192471b.yaml b/releasenotes/notes/list-az-names-a38c277d1192471b.yaml new file mode 100644 index 000000000..7b492716d --- /dev/null +++ b/releasenotes/notes/list-az-names-a38c277d1192471b.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added list_availability_zone_names API call. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 2c7f988f2..45b48bab5 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1733,6 +1733,29 @@ def list_volume_types(self, get_extra=True): return self._normalize_volume_types( self.manager.submit_task(_tasks.VolumeTypeList())) + @_utils.cache_on_arguments() + def list_availability_zone_names(self, unavailable=False): + """List names of availability zones. + + :param bool unavailable: Whether or not to include unavailable zones + in the output. Defaults to False. + + :returns: A list of availability zone names, or an empty list if the + list could not be fetched. + """ + try: + zones = self._compute_client.get('/os-availability-zone') + except OpenStackCloudHTTPError: + self.log.debug( + "Availability zone list could not be fetched", + exc_info=True) + return [] + ret = [] + for zone in zones: + if zone['zoneState']['available'] or unavailable: + ret.append(zone['zoneName']) + return ret + @_utils.cache_on_arguments() def list_flavors(self, get_extra=True): """List all available flavors. diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index ef4cacdcb..25e609e7f 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -148,6 +148,10 @@ def test_get_server_console_name_or_id(self): if len(log) > 0: break + def test_list_availability_zone_names(self): + self.assertEqual( + ['nova'], self.user_cloud.list_availability_zone_names()) + def test_get_server_console_bad_server(self): self.assertRaises( exc.OpenStackCloudException, diff --git a/shade/tests/unit/test_availability_zones.py b/shade/tests/unit/test_availability_zones.py new file mode 100644 index 000000000..fbbd8cc99 --- /dev/null +++ b/shade/tests/unit/test_availability_zones.py @@ -0,0 +1,79 @@ +# Copyright (c) 2017 Red Hat, 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. + + +from shade.tests.unit import base +from shade.tests import fakes + + +_fake_zone_list = { + "availabilityZoneInfo": [ + { + "hosts": None, + "zoneName": "az1", + "zoneState": { + "available": True + } + }, + { + "hosts": None, + "zoneName": "nova", + "zoneState": { + "available": False + } + } + ] +} + + +class TestAvailabilityZoneNames(base.RequestsMockTestCase): + + def test_list_availability_zone_names(self): + self.register_uris([ + dict(method='GET', + uri='{endpoint}/os-availability-zone'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json=_fake_zone_list), + ]) + + self.assertEqual( + ['az1'], self.cloud.list_availability_zone_names()) + + self.assert_calls() + + def test_unauthorized_availability_zone_names(self): + self.register_uris([ + dict(method='GET', + uri='{endpoint}/os-availability-zone'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + status_code=403), + ]) + + self.assertEqual( + [], self.cloud.list_availability_zone_names()) + + self.assert_calls() + + def test_list_all_availability_zone_names(self): + self.register_uris([ + dict(method='GET', + uri='{endpoint}/os-availability-zone'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json=_fake_zone_list), + ]) + + self.assertEqual( + ['az1', 'nova'], + self.cloud.list_availability_zone_names(unavailable=True)) + + self.assert_calls() From 12523389a811fc3ee15ddfaabf16e4f57ffa2980 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 26 Mar 2017 11:27:45 -0500 Subject: [PATCH 1380/3836] Add normalization for heat stacks Change-Id: I564268d910a558288e661c15e279ca7fdfcc80f3 --- doc/source/model.rst | 31 ++++++++++++++++++ shade/_normalize.py | 59 ++++++++++++++++++++++++++++++++++ shade/_utils.py | 7 ---- shade/openstackcloud.py | 4 +-- shade/tests/unit/test_stack.py | 25 ++++++++++---- 5 files changed, 111 insertions(+), 15 deletions(-) diff --git a/doc/source/model.rst b/doc/source/model.rst index ece33ce9e..70818f586 100644 --- a/doc/source/model.rst +++ b/doc/source/model.rst @@ -451,3 +451,34 @@ A Magnum Service from magnum report_count=int(), state=str(), properties=dict()) + +Stack +----- + +A Stack from Heat + +.. code-block:: python + + Stack = dict( + location=Location(), + id=str(), + name=str(), + created_at=str(), + deleted_at=str(), + updated_at=str(), + description=str(), + action=str(), + identifier=str(), + is_rollback_enabled=bool(), + notification_topics=list(), + outputs=list(), + owner=str(), + parameters=dict(), + parent=str(), + stack_user_project_id=str(), + status=str(), + status_reason=str(), + tags=dict(), + tempate_description=str(), + timeout_mins=int(), + properties=dict()) diff --git a/shade/_normalize.py b/shade/_normalize.py index 9a8103ece..7e7f12c3e 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -936,3 +936,62 @@ def _normalize_magnum_service(self, magnum_service): ret[key] = magnum_service.pop(key) ret['properties'] = magnum_service return ret + + def _normalize_stacks(self, stacks): + """Normalize Heat Stacks""" + ret = [] + for stack in stacks: + ret.append(self._normalize_stack(stack)) + return ret + + def _normalize_stack(self, stack): + """Normalize Heat Stack""" + stack = stack.copy() + + # Discard noise + stack.pop('HUMAN_ID', None) + stack.pop('human_id', None) + stack.pop('NAME_ATTR', None) + stack.pop('links', None) + # Discard things heatclient adds that aren't in the REST + stack.pop('action', None) + stack.pop('status', None) + stack.pop('identifier', None) + + stack_status = stack.pop('stack_status') + (action, status) = stack_status.split('_') + + ret = munch.Munch( + id=stack.pop('id'), + location=self._get_current_location(), + action=action, + status=status, + ) + if not self.strict_mode: + ret['stack_status'] = stack_status + + for (new_name, old_name) in ( + ('name', 'stack_name'), + ('created_at', 'creation_time'), + ('deleted_at', 'deletion_time'), + ('updated_at', 'updated_time'), + ('description', 'description'), + ('is_rollback_enabled', 'disable_rollback'), + ('parent', 'parent'), + ('notification_topics', 'notification_topics'), + ('parameters', 'parameters'), + ('outputs', 'outputs'), + ('owner', 'stack_owner'), + ('status_reason', 'stack_status_reason'), + ('stack_user_project_id', 'stack_user_project_id'), + ('tempate_description', 'template_description'), + ('timeout_mins', 'timeout_mins'), + ('tags', 'tags')): + value = stack.pop(old_name, None) + ret[new_name] = value + if not self.strict_mode: + ret[old_name] = value + ret['identifier'] = '{name}/{id}'.format( + name=ret['name'], id=ret['id']) + ret['properties'] = stack + return ret diff --git a/shade/_utils.py b/shade/_utils.py index f54e8809b..238986685 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -369,13 +369,6 @@ def normalize_flavor_accesses(flavor_accesses): ] -def normalize_stacks(stacks): - """ Normalize Stack Object """ - for stack in stacks: - stack['name'] = stack['stack_name'] - return stacks - - def valid_kwargs(*valid_args): # This decorator checks if argument passed as **kwargs to a function are # present in valid_args. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 2c7f988f2..32a84f8f4 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1771,7 +1771,7 @@ def list_stacks(self): """ with _utils.shade_exceptions("Error fetching stack list"): stacks = self.manager.submit_task(_tasks.StackList()) - return _utils.normalize_stacks(stacks) + return self._normalize_stacks(stacks) def list_server_security_groups(self, server): """List all security groups associated with the given server. @@ -2781,7 +2781,7 @@ def _search_one_stack(name_or_id=None, filters=None): stacks = [stack] except heat_exceptions.NotFound: return [] - nstacks = _utils.normalize_stacks(stacks) + nstacks = self._normalize_stacks(stacks) return _utils._filter_list(nstacks, name_or_id, filters) return _utils._get_entity( diff --git a/shade/tests/unit/test_stack.py b/shade/tests/unit/test_stack.py index f0d1e2f51..1618b9d3b 100644 --- a/shade/tests/unit/test_stack.py +++ b/shade/tests/unit/test_stack.py @@ -33,7 +33,9 @@ def test_list_stacks(self, mock_heat): mock_heat.stacks.list.return_value = fake_stacks stacks = self.cloud.list_stacks() mock_heat.stacks.list.assert_called_once_with() - self.assertEqual(meta.obj_list_to_dict(fake_stacks), stacks) + self.assertEqual( + self.cloud._normalize_stacks(meta.obj_list_to_dict(fake_stacks)), + stacks) @mock.patch.object(shade.OpenStackCloud, 'heat_client') def test_list_stacks_exception(self, mock_heat): @@ -53,19 +55,24 @@ def test_search_stacks(self, mock_heat): mock_heat.stacks.list.return_value = fake_stacks stacks = self.cloud.search_stacks() mock_heat.stacks.list.assert_called_once_with() - self.assertEqual(meta.obj_list_to_dict(fake_stacks), stacks) + self.assertEqual( + self.cloud._normalize_stacks(meta.obj_list_to_dict(fake_stacks)), + stacks) @mock.patch.object(shade.OpenStackCloud, 'heat_client') def test_search_stacks_filters(self, mock_heat): fake_stacks = [ - fakes.FakeStack('001', 'stack1', status='GOOD'), - fakes.FakeStack('002', 'stack2', status='BAD'), + fakes.FakeStack('001', 'stack1', status='CREATE_COMPLETE'), + fakes.FakeStack('002', 'stack2', status='CREATE_FAILED'), ] mock_heat.stacks.list.return_value = fake_stacks - filters = {'stack_status': 'GOOD'} + filters = {'status': 'COMPLETE'} stacks = self.cloud.search_stacks(filters=filters) mock_heat.stacks.list.assert_called_once_with() - self.assertEqual(meta.obj_list_to_dict(fake_stacks[:1]), stacks) + self.assertEqual( + self.cloud._normalize_stacks( + meta.obj_list_to_dict(fake_stacks[:1])), + stacks) @mock.patch.object(shade.OpenStackCloud, 'heat_client') def test_search_stacks_exception(self, mock_heat): @@ -138,6 +145,8 @@ def test_delete_stack_wait_failed(self, mock_heat, mock_get, mock_poll): @mock.patch.object(shade.OpenStackCloud, 'heat_client') def test_create_stack(self, mock_heat, mock_template): mock_template.return_value = ({}, {}) + mock_heat.stacks.create.return_value = fakes.FakeStack('001', 'stack1') + mock_heat.stacks.get.return_value = fakes.FakeStack('001', 'stack1') self.cloud.create_stack('stack_name') self.assertTrue(mock_template.called) mock_heat.stacks.create.assert_called_once_with( @@ -159,6 +168,8 @@ def test_create_stack_wait(self, mock_heat, mock_get, mock_template, stack = {'id': 'stack_id', 'name': 'stack_name'} mock_template.return_value = ({}, {}) mock_get.return_value = stack + mock_heat.stacks.create.return_value = fakes.FakeStack('001', 'stack1') + mock_heat.stacks.get.return_value = fakes.FakeStack('001', 'stack1') ret = self.cloud.create_stack('stack_name', wait=True) self.assertTrue(mock_template.called) mock_heat.stacks.create.assert_called_once_with( @@ -178,6 +189,8 @@ def test_create_stack_wait(self, mock_heat, mock_get, mock_template, @mock.patch.object(shade.OpenStackCloud, 'heat_client') def test_update_stack(self, mock_heat, mock_template): mock_template.return_value = ({}, {}) + mock_heat.stacks.update.return_value = fakes.FakeStack('001', 'stack1') + mock_heat.stacks.get.return_value = fakes.FakeStack('001', 'stack1') self.cloud.update_stack('stack_name') self.assertTrue(mock_template.called) mock_heat.stacks.update.assert_called_once_with( From 510075d49451bfb699ec09faf2b1ac119f808f0f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 26 Mar 2017 12:18:56 -0500 Subject: [PATCH 1381/3836] Replace heatclient testing with requests_mock Change-Id: Ib5aa24911e5ffc7426ed5d3e651367cc86e03269 --- shade/tests/fakes.py | 73 +++ shade/tests/functional/test_stack.py | 5 +- shade/tests/unit/base.py | 7 +- shade/tests/unit/fixtures/catalog-v3.json | 13 + shade/tests/unit/test_stack.py | 613 +++++++++++++++------- 5 files changed, 524 insertions(+), 187 deletions(-) diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 3497d7f34..7b33759dc 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -17,6 +17,9 @@ Fakes used for testing """ +import uuid + +from shade._heat import template_format PROJECT_ID = '1c36b64c840a42cd9e9b931a369337f0' FLAVOR_ID = u'0c1d9008-f546-4608-9e8f-f8bdaec8dddd' @@ -24,6 +27,8 @@ STRAWBERRY_FLAVOR_ID = u'0c1d9008-f546-4608-9e8f-f8bdaec8dddf' COMPUTE_ENDPOINT = 'https://compute.example.com/v2.1/{project_id}'.format( project_id=PROJECT_ID) +ORCHESTRATION_ENDPOINT = 'https://orchestration.example.com/v1/{p}'.format( + p=PROJECT_ID) def make_fake_flavor(flavor_id, name, ram=100, disk=1600, vcpus=24): @@ -54,6 +59,24 @@ def make_fake_flavor(flavor_id, name, ram=100, disk=1600, vcpus=24): FAKE_STRAWBERRY_FLAVOR = make_fake_flavor( STRAWBERRY_FLAVOR_ID, 'strawberry', ram=300) FAKE_FLAVOR_LIST = [FAKE_FLAVOR, FAKE_CHOCOLATE_FLAVOR, FAKE_STRAWBERRY_FLAVOR] +FAKE_TEMPLATE = '''heat_template_version: 2014-10-16 + +parameters: + length: + type: number + default: 10 + +resources: + my_rand: + type: OS::Heat::RandomString + properties: + length: {get_param: length} +outputs: + rand: + value: + get_attr: [my_rand, value] +''' +FAKE_TEMPLATE_CONTENT = template_format.parse(FAKE_TEMPLATE) def make_fake_server(server_id, name, status='ACTIVE'): @@ -105,6 +128,56 @@ def make_fake_server(server_id, name, status='ACTIVE'): "config_drive": "True"} +def make_fake_stack(id, name, description=None, status='CREATE_COMPLETE'): + return { + 'creation_time': '2017-03-23T23:57:12Z', + 'deletion_time': '2017-03-23T23:57:12Z', + 'description': description, + 'id': id, + 'links': [], + 'parent': None, + 'stack_name': name, + 'stack_owner': None, + 'stack_status': status, + 'stack_user_project_id': PROJECT_ID, + 'tags': None, + 'updated_time': '2017-03-23T23:57:12Z', + } + + +def make_fake_stack_event( + id, name, status='CREATE_COMPLETED', resource_name='id'): + event_id = uuid.uuid4().hex + self_url = "{endpoint}/stacks/{name}/{id}/resources/{name}/events/{event}" + resource_url = "{endpoint}/stacks/{name}/{id}/resources/{name}" + return { + "resource_name": id if resource_name == 'id' else name, + "event_time": "2017-03-26T19:38:18", + "links": [ + { + "href": self_url.format( + endpoint=ORCHESTRATION_ENDPOINT, + name=name, id=id, event=event_id), + "rel": "self" + }, { + "href": resource_url.format( + endpoint=ORCHESTRATION_ENDPOINT, + name=name, id=id), + "rel": "resource" + }, { + "href": "{endpoint}/stacks/{name}/{id}".format( + endpoint=ORCHESTRATION_ENDPOINT, + name=name, id=id), + "rel": "stack" + }], + "logical_resource_id": name, + "resource_status": status, + "resource_status_reason": "", + "physical_resource_id": id, + "id": event_id, + } + + class FakeEndpoint(object): def __init__(self, id, service_id, region, publicurl, internalurl=None, adminurl=None): diff --git a/shade/tests/functional/test_stack.py b/shade/tests/functional/test_stack.py index 739d05f84..9416119ea 100644 --- a/shade/tests/functional/test_stack.py +++ b/shade/tests/functional/test_stack.py @@ -20,6 +20,7 @@ import tempfile from shade import exc +from shade.tests import fakes from shade.tests.functional import base simple_template = '''heat_template_version: 2014-10-16 @@ -94,7 +95,7 @@ def test_stack_validation(self): def test_stack_simple(self): test_template = tempfile.NamedTemporaryFile(delete=False) - test_template.write(simple_template) + test_template.write(fakes.FAKE_TEMPLATE) test_template.close() self.stack_name = self.getUniqueString('simple_stack') self.addCleanup(self._cleanup_stack) @@ -151,7 +152,7 @@ def test_stack_nested(self): test_template.close() simple_tmpl = tempfile.NamedTemporaryFile(suffix='.yaml', delete=False) - simple_tmpl.write(simple_template) + simple_tmpl.write(fakes.FAKE_TEMPLATE) simple_tmpl.close() env = tempfile.NamedTemporaryFile(suffix='.yaml', delete=False) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 452f029be..2f91c87c5 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -556,11 +556,8 @@ def assert_calls(self, stop_after=None): if stop_after and x > stop_after: break self.assertEqual( - call['method'], history.method, - 'Method mismatch on call {index}'.format(index=x)) - self.assertEqual( - call['url'], history.url, - 'URL mismatch on call {index}'.format(index=x)) + (call['method'], call['url']), (history.method, history.url), + 'REST mismatch on call {index}'.format(index=x)) if 'json' in call: self.assertEqual( call['json'], history.json(), diff --git a/shade/tests/unit/fixtures/catalog-v3.json b/shade/tests/unit/fixtures/catalog-v3.json index bb74465c0..3f9ba217a 100644 --- a/shade/tests/unit/fixtures/catalog-v3.json +++ b/shade/tests/unit/fixtures/catalog-v3.json @@ -109,6 +109,19 @@ "endpoints_links": [], "name": "swift", "type": "object-store" + }, + { + "endpoints": [ + { + "id": "4deb4d0504a044a395d4480741ba628c", + "interface": "public", + "region": "RegionOne", + "url": "https://orchestration.example.com/v1/1c36b64c840a42cd9e9b931a369337f0" + } + ], + "endpoints_links": [], + "name": "heat", + "type": "orchestration" } ], "expires_at": "9999-12-31T23:59:59Z", diff --git a/shade/tests/unit/test_stack.py b/shade/tests/unit/test_stack.py index 1618b9d3b..45f3de1e8 100644 --- a/shade/tests/unit/test_stack.py +++ b/shade/tests/unit/test_stack.py @@ -11,228 +11,481 @@ # under the License. -import mock +import tempfile import testtools import shade -from shade._heat import event_utils -from shade._heat import template_utils from shade import meta from shade.tests import fakes from shade.tests.unit import base -class TestStack(base.TestCase): +class TestStack(base.RequestsMockTestCase): - @mock.patch.object(shade.OpenStackCloud, 'heat_client') - def test_list_stacks(self, mock_heat): + def setUp(self): + super(TestStack, self).setUp() + self.stack_id = self.getUniqueString('id') + self.stack_name = self.getUniqueString('name') + self.stack = fakes.make_fake_stack(self.stack_id, self.stack_name) + + def test_list_stacks(self): fake_stacks = [ - fakes.FakeStack('001', 'stack1'), - fakes.FakeStack('002', 'stack2'), + self.stack, + fakes.make_fake_stack( + self.getUniqueString('id'), + self.getUniqueString('name')) ] - mock_heat.stacks.list.return_value = fake_stacks + self.register_uris([ + dict(method='GET', + uri='{endpoint}/stacks'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT), + json={"stacks": fake_stacks}), + ]) stacks = self.cloud.list_stacks() - mock_heat.stacks.list.assert_called_once_with() self.assertEqual( - self.cloud._normalize_stacks(meta.obj_list_to_dict(fake_stacks)), - stacks) + [f.toDict() for f in self.cloud._normalize_stacks(fake_stacks)], + [f.toDict() for f in stacks]) - @mock.patch.object(shade.OpenStackCloud, 'heat_client') - def test_list_stacks_exception(self, mock_heat): - mock_heat.stacks.list.side_effect = Exception() + self.assert_calls() + + def test_list_stacks_exception(self): + self.register_uris([ + dict(method='GET', + uri='{endpoint}/stacks'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT), + status_code=404) + ]) with testtools.ExpectedException( shade.OpenStackCloudException, "Error fetching stack list" ): self.cloud.list_stacks() + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'heat_client') - def test_search_stacks(self, mock_heat): + def test_search_stacks(self): fake_stacks = [ - fakes.FakeStack('001', 'stack1'), - fakes.FakeStack('002', 'stack2'), + self.stack, + fakes.make_fake_stack( + self.getUniqueString('id'), + self.getUniqueString('name')) ] - mock_heat.stacks.list.return_value = fake_stacks + self.register_uris([ + dict(method='GET', + uri='{endpoint}/stacks'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT), + json={"stacks": fake_stacks}), + ]) stacks = self.cloud.search_stacks() - mock_heat.stacks.list.assert_called_once_with() self.assertEqual( self.cloud._normalize_stacks(meta.obj_list_to_dict(fake_stacks)), stacks) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'heat_client') - def test_search_stacks_filters(self, mock_heat): + def test_search_stacks_filters(self): fake_stacks = [ - fakes.FakeStack('001', 'stack1', status='CREATE_COMPLETE'), - fakes.FakeStack('002', 'stack2', status='CREATE_FAILED'), + self.stack, + fakes.make_fake_stack( + self.getUniqueString('id'), + self.getUniqueString('name'), + status='CREATE_FAILED') ] - mock_heat.stacks.list.return_value = fake_stacks - filters = {'status': 'COMPLETE'} + self.register_uris([ + dict(method='GET', + uri='{endpoint}/stacks'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT), + json={"stacks": fake_stacks}), + ]) + filters = {'status': 'FAILED'} stacks = self.cloud.search_stacks(filters=filters) - mock_heat.stacks.list.assert_called_once_with() self.assertEqual( self.cloud._normalize_stacks( - meta.obj_list_to_dict(fake_stacks[:1])), + meta.obj_list_to_dict(fake_stacks[1:])), stacks) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'heat_client') - def test_search_stacks_exception(self, mock_heat): - mock_heat.stacks.list.side_effect = Exception() + def test_search_stacks_exception(self): + self.register_uris([ + dict(method='GET', + uri='{endpoint}/stacks'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT), + status_code=404) + ]) with testtools.ExpectedException( shade.OpenStackCloudException, "Error fetching stack list" ): self.cloud.search_stacks() - @mock.patch.object(shade.OpenStackCloud, 'get_stack') - @mock.patch.object(shade.OpenStackCloud, 'heat_client') - def test_delete_stack(self, mock_heat, mock_get): - stack = {'id': 'stack_id', 'name': 'stack_name'} - mock_get.return_value = stack - self.assertTrue(self.cloud.delete_stack('stack_name')) - mock_get.assert_called_once_with('stack_name') - mock_heat.stacks.delete.assert_called_once_with(stack['id']) - - @mock.patch.object(shade.OpenStackCloud, 'get_stack') - @mock.patch.object(shade.OpenStackCloud, 'heat_client') - def test_delete_stack_not_found(self, mock_heat, mock_get): - mock_get.return_value = None + def test_delete_stack(self): + self.register_uris([ + dict(method='GET', + uri='{endpoint}/stacks/{name}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name), + json={"stack": self.stack}), + dict(method='GET', + uri='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, name=self.stack_name), + json={"stack": self.stack}), + dict(method='DELETE', + uri='{endpoint}/stacks/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id)), + ]) + self.assertTrue(self.cloud.delete_stack(self.stack_name)) + self.assert_calls() + + def test_delete_stack_not_found(self): + self.register_uris([ + dict(method='GET', + uri='{endpoint}/stacks/stack_name'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT), + status_code=404), + ]) self.assertFalse(self.cloud.delete_stack('stack_name')) - mock_get.assert_called_once_with('stack_name') - self.assertFalse(mock_heat.stacks.delete.called) - - @mock.patch.object(shade.OpenStackCloud, 'get_stack') - @mock.patch.object(shade.OpenStackCloud, 'heat_client') - def test_delete_stack_exception(self, mock_heat, mock_get): - stack = {'id': 'stack_id', 'name': 'stack_name'} - mock_get.return_value = stack - mock_heat.stacks.delete.side_effect = Exception('ouch') - with testtools.ExpectedException( - shade.OpenStackCloudException, - "Failed to delete stack stack_name: ouch" - ): - self.cloud.delete_stack('stack_name') - - @mock.patch.object(event_utils, 'poll_for_events') - @mock.patch.object(shade.OpenStackCloud, 'get_stack') - @mock.patch.object(shade.OpenStackCloud, 'heat_client') - def test_delete_stack_wait(self, mock_heat, mock_get, mock_poll): - stack = {'id': 'stack_id', 'name': 'stack_name'} - mock_get.side_effect = (stack, None) - self.assertTrue(self.cloud.delete_stack('stack_name', wait=True)) - mock_heat.stacks.delete.assert_called_once_with(stack['id']) - self.assertEqual(2, mock_get.call_count) - self.assertEqual(1, mock_poll.call_count) - - @mock.patch.object(event_utils, 'poll_for_events') - @mock.patch.object(shade.OpenStackCloud, 'get_stack') - @mock.patch.object(shade.OpenStackCloud, 'heat_client') - def test_delete_stack_wait_failed(self, mock_heat, mock_get, mock_poll): - stack = {'id': 'stack_id', 'name': 'stack_name'} - stack_failed = {'id': 'stack_id', 'name': 'stack_name', - 'stack_status': 'DELETE_FAILED', - 'stack_status_reason': 'ouch'} - mock_get.side_effect = (stack, stack_failed) - with testtools.ExpectedException( - shade.OpenStackCloudException, - "Failed to delete stack stack_name: ouch" - ): - self.cloud.delete_stack('stack_name', wait=True) - mock_heat.stacks.delete.assert_called_once_with(stack['id']) - self.assertEqual(2, mock_get.call_count) - self.assertEqual(1, mock_poll.call_count) - - @mock.patch.object(template_utils, 'get_template_contents') - @mock.patch.object(shade.OpenStackCloud, 'heat_client') - def test_create_stack(self, mock_heat, mock_template): - mock_template.return_value = ({}, {}) - mock_heat.stacks.create.return_value = fakes.FakeStack('001', 'stack1') - mock_heat.stacks.get.return_value = fakes.FakeStack('001', 'stack1') - self.cloud.create_stack('stack_name') - self.assertTrue(mock_template.called) - mock_heat.stacks.create.assert_called_once_with( - stack_name='stack_name', - disable_rollback=False, - environment={}, - parameters={}, - template={}, - files={}, - timeout_mins=60, - ) + self.assert_calls() - @mock.patch.object(event_utils, 'poll_for_events') - @mock.patch.object(template_utils, 'get_template_contents') - @mock.patch.object(shade.OpenStackCloud, 'get_stack') - @mock.patch.object(shade.OpenStackCloud, 'heat_client') - def test_create_stack_wait(self, mock_heat, mock_get, mock_template, - mock_poll): - stack = {'id': 'stack_id', 'name': 'stack_name'} - mock_template.return_value = ({}, {}) - mock_get.return_value = stack - mock_heat.stacks.create.return_value = fakes.FakeStack('001', 'stack1') - mock_heat.stacks.get.return_value = fakes.FakeStack('001', 'stack1') - ret = self.cloud.create_stack('stack_name', wait=True) - self.assertTrue(mock_template.called) - mock_heat.stacks.create.assert_called_once_with( - stack_name='stack_name', - disable_rollback=False, - environment={}, - parameters={}, - template={}, - files={}, - timeout_mins=60, - ) - self.assertEqual(1, mock_get.call_count) - self.assertEqual(1, mock_poll.call_count) - self.assertEqual(stack, ret) - - @mock.patch.object(template_utils, 'get_template_contents') - @mock.patch.object(shade.OpenStackCloud, 'heat_client') - def test_update_stack(self, mock_heat, mock_template): - mock_template.return_value = ({}, {}) - mock_heat.stacks.update.return_value = fakes.FakeStack('001', 'stack1') - mock_heat.stacks.get.return_value = fakes.FakeStack('001', 'stack1') - self.cloud.update_stack('stack_name') - self.assertTrue(mock_template.called) - mock_heat.stacks.update.assert_called_once_with( - stack_id='stack_name', - disable_rollback=False, - environment={}, - parameters={}, - template={}, - files={}, - timeout_mins=60, - ) + def test_delete_stack_exception(self): + self.register_uris([ + dict(method='GET', + uri='{endpoint}/stacks/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id), + json={"stack": self.stack}), + dict(method='GET', + uri='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, name=self.stack_name), + json={"stack": self.stack}), + dict(method='DELETE', + uri='{endpoint}/stacks/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id), + status_code=400, + reason="ouch"), + ]) + with testtools.ExpectedException(shade.OpenStackCloudException): + self.cloud.delete_stack(self.stack_id) + self.assert_calls() + + def test_delete_stack_wait(self): + marker_event = fakes.make_fake_stack_event( + self.stack_id, self.stack_name, status='CREATE_COMPLETE') + marker_qs = 'marker={e_id}&sort_dir=asc'.format( + e_id=marker_event['id']) + self.register_uris([ + dict(method='GET', + uri='{endpoint}/stacks/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id), + json={"stack": self.stack}), + dict(method='GET', + uri='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, name=self.stack_name), + json={"stack": self.stack}), + dict(method='GET', + uri='{endpoint}/stacks/{id}/events?{qs}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + qs='limit=1&sort_dir=desc'), + complete_qs=True, + json={"events": [marker_event]}), + dict(method='DELETE', + uri='{endpoint}/stacks/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id)), + dict(method='GET', + uri='{endpoint}/stacks/{id}/events?{qs}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + qs=marker_qs), + complete_qs=True, + json={"events": [ + fakes.make_fake_stack_event( + self.stack_id, self.stack_name, + status='DELETE_COMPLETE'), + ]}), + dict(method='GET', + uri='{endpoint}/stacks/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, name=self.stack_name), + status_code=404), + ]) + + self.assertTrue(self.cloud.delete_stack(self.stack_id, wait=True)) + self.assert_calls() + + def test_delete_stack_wait_failed(self): + failed_stack = self.stack.copy() + failed_stack['stack_status'] = 'DELETE_FAILED' + marker_event = fakes.make_fake_stack_event( + self.stack_id, self.stack_name, status='CREATE_COMPLETE') + marker_qs = 'marker={e_id}&sort_dir=asc'.format( + e_id=marker_event['id']) + self.register_uris([ + dict(method='GET', + uri='{endpoint}/stacks/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id), + json={"stack": self.stack}), + dict(method='GET', + uri='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, name=self.stack_name), + json={"stack": self.stack}), + dict(method='GET', + uri='{endpoint}/stacks/{id}/events?{qs}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + qs='limit=1&sort_dir=desc'), + complete_qs=True, + json={"events": [marker_event]}), + dict(method='DELETE', + uri='{endpoint}/stacks/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id)), + dict(method='GET', + uri='{endpoint}/stacks/{id}/events?{qs}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + qs=marker_qs), + complete_qs=True, + json={"events": [ + fakes.make_fake_stack_event( + self.stack_id, self.stack_name, + status='DELETE_COMPLETE'), + ]}), + dict(method='GET', + uri='{endpoint}/stacks/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, name=self.stack_name), + json={'stack': failed_stack}), + dict(method='GET', + uri='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, name=self.stack_name), + json={"stack": failed_stack}), + ]) - @mock.patch.object(event_utils, 'poll_for_events') - @mock.patch.object(template_utils, 'get_template_contents') - @mock.patch.object(shade.OpenStackCloud, 'get_stack') - @mock.patch.object(shade.OpenStackCloud, 'heat_client') - def test_update_stack_wait(self, mock_heat, mock_get, mock_template, - mock_poll): - stack = {'id': 'stack_id', 'name': 'stack_name'} - mock_template.return_value = ({}, {}) - mock_get.return_value = stack - ret = self.cloud.update_stack('stack_name', wait=True) - self.assertTrue(mock_template.called) - mock_heat.stacks.update.assert_called_once_with( - stack_id='stack_name', - disable_rollback=False, - environment={}, - parameters={}, - template={}, - files={}, - timeout_mins=60, + with testtools.ExpectedException(shade.OpenStackCloudException): + self.cloud.delete_stack(self.stack_id, wait=True) + + self.assert_calls() + + def test_create_stack(self): + test_template = tempfile.NamedTemporaryFile(delete=False) + test_template.write(fakes.FAKE_TEMPLATE.encode('utf-8')) + test_template.close() + self.register_uris([ + dict( + method='POST', uri='{endpoint}/stacks'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT), + json={"stack": self.stack}, + validate=dict( + json={ + 'disable_rollback': False, + 'environment': {}, + 'files': {}, + 'parameters': {}, + 'stack_name': self.stack_name, + 'template': fakes.FAKE_TEMPLATE_CONTENT, + 'timeout_mins': 60} + )), + dict( + method='GET', + uri='{endpoint}/stacks/{name}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name), + json={"stack": self.stack}), + dict( + method='GET', + uri='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, name=self.stack_name), + json={"stack": self.stack}), + ]) + + self.cloud.create_stack( + self.stack_name, + template_file=test_template.name ) - self.assertEqual(1, mock_get.call_count) - self.assertEqual(1, mock_poll.call_count) - self.assertEqual(stack, ret) - - @mock.patch.object(shade.OpenStackCloud, 'heat_client') - def test_get_stack(self, mock_heat): - stack = fakes.FakeStack('azerty', 'stack',) - mock_heat.stacks.get.return_value = stack - res = self.cloud.get_stack('stack') + + self.assert_calls() + + def test_create_stack_wait(self): + + test_template = tempfile.NamedTemporaryFile(delete=False) + test_template.write(fakes.FAKE_TEMPLATE.encode('utf-8')) + test_template.close() + + self.register_uris([ + dict( + method='POST', uri='{endpoint}/stacks'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT), + json={"stack": self.stack}, + validate=dict( + json={ + 'disable_rollback': False, + 'environment': {}, + 'files': {}, + 'parameters': {}, + 'stack_name': self.stack_name, + 'template': fakes.FAKE_TEMPLATE_CONTENT, + 'timeout_mins': 60} + )), + dict( + method='GET', + uri='{endpoint}/stacks/{name}/events?sort_dir=asc'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name), + json={"events": [ + fakes.make_fake_stack_event( + self.stack_id, self.stack_name, + status='CREATE_COMPLETE', + resource_name='name'), + ]}), + dict( + method='GET', + uri='{endpoint}/stacks/{name}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name), + json={"stack": self.stack}), + dict( + method='GET', + uri='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, name=self.stack_name), + json={"stack": self.stack}), + ]) + self.cloud.create_stack( + self.stack_name, + template_file=test_template.name, + wait=True) + + self.assert_calls() + + def test_update_stack(self): + test_template = tempfile.NamedTemporaryFile(delete=False) + test_template.write(fakes.FAKE_TEMPLATE.encode('utf-8')) + test_template.close() + + self.register_uris([ + dict( + method='PUT', + uri='{endpoint}/stacks/{name}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name), + validate=dict( + json={ + 'disable_rollback': False, + 'environment': {}, + 'files': {}, + 'parameters': {}, + 'template': fakes.FAKE_TEMPLATE_CONTENT, + 'timeout_mins': 60})), + dict( + method='GET', + uri='{endpoint}/stacks/{name}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name), + json={"stack": self.stack}), + dict( + method='GET', + uri='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, name=self.stack_name), + json={"stack": self.stack}), + ]) + self.cloud.update_stack( + self.stack_name, + template_file=test_template.name) + + self.assert_calls() + + def test_update_stack_wait(self): + marker_event = fakes.make_fake_stack_event( + self.stack_id, self.stack_name, status='CREATE_COMPLETE', + resource_name='name') + marker_qs = 'marker={e_id}&sort_dir=asc'.format( + e_id=marker_event['id']) + test_template = tempfile.NamedTemporaryFile(delete=False) + test_template.write(fakes.FAKE_TEMPLATE.encode('utf-8')) + test_template.close() + + self.register_uris([ + dict( + method='GET', + uri='{endpoint}/stacks/{name}/events?{qs}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name, + qs='limit=1&sort_dir=desc'), + json={"events": [marker_event]}), + dict( + method='PUT', + uri='{endpoint}/stacks/{name}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name), + validate=dict( + json={ + 'disable_rollback': False, + 'environment': {}, + 'files': {}, + 'parameters': {}, + 'template': fakes.FAKE_TEMPLATE_CONTENT, + 'timeout_mins': 60})), + dict( + method='GET', + uri='{endpoint}/stacks/{name}/events?{qs}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name, + qs=marker_qs), + json={"events": [ + fakes.make_fake_stack_event( + self.stack_id, self.stack_name, + status='UPDATE_COMPLETE', + resource_name='name'), + ]}), + dict( + method='GET', + uri='{endpoint}/stacks/{name}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name), + json={"stack": self.stack}), + dict( + method='GET', + uri='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, name=self.stack_name), + json={"stack": self.stack}), + ]) + self.cloud.update_stack( + self.stack_name, + template_file=test_template.name, + wait=True) + + self.assert_calls() + + def test_get_stack(self): + self.register_uris([ + dict(method='GET', + uri='{endpoint}/stacks/{name}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name), + json={"stack": self.stack}), + dict(method='GET', + uri='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, name=self.stack_name), + json={"stack": self.stack}), + ]) + + res = self.cloud.get_stack(self.stack_name) self.assertIsNotNone(res) - self.assertEqual(stack.stack_name, res['stack_name']) - self.assertEqual(stack.stack_name, res['name']) - self.assertEqual(stack.stack_status, res['stack_status']) + self.assertEqual(self.stack['stack_name'], res['stack_name']) + self.assertEqual(self.stack['stack_name'], res['name']) + self.assertEqual(self.stack['stack_status'], res['stack_status']) + self.assertEqual('COMPLETE', res['status']) + + self.assert_calls() From c8c098b3f5232616dccb8564b91805354c13452a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 27 Mar 2017 09:35:34 -0500 Subject: [PATCH 1382/3836] Remove python-heatclient and replace with REST The changes in the unittests are because testtools does explicit exception matching, not subclass matching. Also the first GET call is actually supposed to be a 302 redirect. I do not understand why the first form worked with heatclient. Change-Id: I4b23304c09a1b985cc595a75587dbbc0472d450a --- requirements.txt | 1 - shade/_heat/event_utils.py | 128 ++++++--------------------------- shade/_tasks.py | 25 ------- shade/_utils.py | 13 ---- shade/openstackcloud.py | 63 ++++++++-------- shade/tests/unit/base.py | 1 + shade/tests/unit/test_shade.py | 14 ---- shade/tests/unit/test_stack.py | 72 ++++++++++++++----- 8 files changed, 108 insertions(+), 209 deletions(-) diff --git a/requirements.txt b/requirements.txt index d671b3a60..5b5550669 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,6 @@ python-keystoneclient>=0.11.0 python-cinderclient>=1.3.1 python-neutronclient>=2.3.10 python-ironicclient>=0.10.0 -python-heatclient>=1.0.0 python-designateclient>=2.1.0 dogpile.cache>=0.5.3 diff --git a/shade/_heat/event_utils.py b/shade/_heat/event_utils.py index ab3c27cd0..8f63daef1 100644 --- a/shade/_heat/event_utils.py +++ b/shade/_heat/event_utils.py @@ -12,115 +12,33 @@ # License for the specific language governing permissions and limitations # under the License. +import collections import time -from shade._heat import utils -import heatclient.exc as exc +def get_events(cloud, stack_id, event_args, marker=None, limit=None): + # TODO(mordred) FIX THIS ONCE assert_calls CAN HANDLE QUERY STRINGS + params = collections.OrderedDict() + for k in sorted(event_args.keys()): + params[k] = event_args[k] -def get_events(hc, stack_id, event_args, nested_depth=0, - marker=None, limit=None): - event_args = dict(event_args) if marker: event_args['marker'] = marker if limit: event_args['limit'] = limit - if not nested_depth: - # simple call with no nested_depth - return _get_stack_events(hc, stack_id, event_args) - - # assume an API which supports nested_depth - event_args['nested_depth'] = nested_depth - events = _get_stack_events(hc, stack_id, event_args) - - if not events: - return events - - first_links = getattr(events[0], 'links', []) - root_stack_link = [l for l in first_links - if l.get('rel') == 'root_stack'] - if root_stack_link: - # response has a root_stack link, indicating this is an API which - # supports nested_depth - return events - - # API doesn't support nested_depth, do client-side paging and recursive - # event fetch - marker = event_args.pop('marker', None) - limit = event_args.pop('limit', None) - event_args.pop('nested_depth', None) - events = _get_stack_events(hc, stack_id, event_args) - events.extend(_get_nested_events(hc, nested_depth, - stack_id, event_args)) - # Because there have been multiple stacks events mangled into - # one list, we need to sort before passing to print_list - # Note we can't use the prettytable sortby_index here, because - # the "start" option doesn't allow post-sort slicing, which - # will be needed to make "--marker" work for nested_depth lists - events.sort(key=lambda x: x.event_time) - - # Slice the list if marker is specified - if marker: - try: - marker_index = [e.id for e in events].index(marker) - events = events[marker_index:] - except ValueError: - pass - - # Slice the list if limit is specified - if limit: - limit_index = min(int(limit), len(events)) - events = events[:limit_index] - return events + events = cloud._orchestration_client.get( + '/stacks/{id}/events'.format(id=stack_id), + params=params) -def _get_nested_ids(hc, stack_id): - nested_ids = [] - try: - resources = hc.resources.list(stack_id=stack_id) - except exc.HTTPNotFound: - raise exc.CommandError('Stack not found: %s' % stack_id) - for r in resources: - nested_id = utils.resource_nested_identifier(r) - if nested_id: - nested_ids.append(nested_id) - return nested_ids - - -def _get_nested_events(hc, nested_depth, stack_id, event_args): - # FIXME(shardy): this is very inefficient, we should add nested_depth to - # the event_list API in a future heat version, but this will be required - # until kilo heat is EOL. - nested_ids = _get_nested_ids(hc, stack_id) - nested_events = [] - for n_id in nested_ids: - stack_events = _get_stack_events(hc, n_id, event_args) - if stack_events: - nested_events.extend(stack_events) - if nested_depth > 1: - next_depth = nested_depth - 1 - nested_events.extend(_get_nested_events( - hc, next_depth, n_id, event_args)) - return nested_events - - -def _get_stack_events(hc, stack_id, event_args): - event_args['stack_id'] = stack_id - try: - events = hc.events.list(**event_args) - except exc.HTTPNotFound as ex: - # it could be the stack or resource that is not found - # just use the message that the server sent us. - raise exc.CommandError(str(ex)) - else: - # Show which stack the event comes from (for nested events) - for e in events: - e.stack_name = stack_id.split("/")[0] - return events + # Show which stack the event comes from (for nested events) + for e in events: + e['stack_name'] = stack_id.split("/")[0] + return events -def poll_for_events(hc, stack_name, action=None, poll_period=5, marker=None, - nested_depth=0): +def poll_for_events( + cloud, stack_name, action=None, poll_period=5, marker=None): """Continuously poll events and logs for performed action on stack.""" if action: @@ -133,19 +51,19 @@ def poll_for_events(hc, stack_name, action=None, poll_period=5, marker=None, msg_template = "\n Stack %(name)s %(status)s \n" def is_stack_event(event): - if getattr(event, 'resource_name', '') != stack_name: + if event.get('resource_name', '') != stack_name: return False - phys_id = getattr(event, 'physical_resource_id', '') + phys_id = event.get('physical_resource_id', '') links = dict((l.get('rel'), - l.get('href')) for l in getattr(event, 'links', [])) + l.get('href')) for l in event.get('links', [])) stack_id = links.get('stack', phys_id).rsplit('/', 1)[-1] return stack_id == phys_id while True: - events = get_events(hc, stack_id=stack_name, nested_depth=nested_depth, - event_args={'sort_dir': 'asc', - 'marker': marker}) + events = get_events( + cloud, stack_id=stack_name, + event_args={'sort_dir': 'asc', 'marker': marker}) if len(events) == 0: no_event_polls += 1 @@ -165,8 +83,8 @@ def is_stack_event(event): if no_event_polls >= 2: # after 2 polls with no events, fall back to a stack get - stack = hc.stacks.get(stack_name, resolve_outputs=False) - stack_status = stack.stack_status + stack = cloud.get_stack(stack_name) + stack_status = stack['stack_status'] msg = msg_template % dict( name=stack_name, status=stack_status) if stop_check(stack_status): diff --git a/shade/_tasks.py b/shade/_tasks.py index 9904eba8f..d0975b3b9 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -699,31 +699,6 @@ def main(self, client): return client.keystone_client.roles.roles_for_user(**self.args) -class StackList(task_manager.Task): - def main(self, client): - return client.heat_client.stacks.list() - - -class StackCreate(task_manager.Task): - def main(self, client): - return client.heat_client.stacks.create(**self.args) - - -class StackUpdate(task_manager.Task): - def main(self, client): - return client.heat_client.stacks.update(**self.args) - - -class StackDelete(task_manager.Task): - def main(self, client): - return client.heat_client.stacks.delete(self.args['id']) - - -class StackGet(task_manager.Task): - def main(self, client): - return client.heat_client.stacks.get(**self.args) - - class ZoneList(task_manager.Task): def main(self, client): return client.designate_client.zones.list() diff --git a/shade/_utils.py b/shade/_utils.py index 238986685..82233f795 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -25,7 +25,6 @@ import time from decorator import decorator -from heatclient import exc as heat_exc from neutronclient.common import exceptions as neutron_exc from novaclient import exceptions as nova_exc @@ -418,18 +417,6 @@ def invalidate(obj, *args, **kwargs): return _inner_cache_on_arguments -@contextlib.contextmanager -def heat_exceptions(error_message): - try: - yield - except heat_exc.NotFound as e: - raise exc.OpenStackCloudResourceNotFound( - "{msg}: {exc}".format(msg=error_message, exc=str(e))) - except Exception as e: - raise exc.OpenStackCloudException( - "{msg}: {exc}".format(msg=error_message, exc=str(e))) - - @contextlib.contextmanager def neutron_exceptions(error_message): try: diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 32a84f8f4..988a81172 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -31,7 +31,6 @@ from six.moves import urllib import cinderclient.exceptions as cinder_exceptions -from heatclient import exc as heat_exceptions import keystoneauth1.exceptions import novaclient.exceptions as nova_exceptions @@ -1095,6 +1094,17 @@ def glance_client(self): @property def heat_client(self): + warnings.warn( + 'Using shade to get a heat_client object is deprecated. If you' + ' need a raw heatclient.client.Client object, please use' + ' make_legacy_client in os-client-config instead') + try: + import heatclient # flake8: noqa + except ImportError: + self.log.error( + 'heatclient is no longer a dependency of shade. You need to' + ' install python-heatclient directly.') + raise if self._heat_client is None: self._heat_client = self._get_client('orchestration') return self._heat_client @@ -1261,11 +1271,9 @@ def create_stack( environment=env, timeout_mins=timeout // 60, ) - with _utils.heat_exceptions("Error creating stack {name}".format( - name=name)): - self.manager.submit_task(_tasks.StackCreate(**params)) + self._orchestration_client.post('/stacks', json=params) if wait: - event_utils.poll_for_events(self.heat_client, stack_name=name, + event_utils.poll_for_events(self, stack_name=name, action='CREATE') return self.get_stack(name) @@ -1308,7 +1316,6 @@ def update_stack( template_object=template_object, files=files) params = dict( - stack_id=name_or_id, disable_rollback=not rollback, parameters=parameters, template=template, @@ -1318,17 +1325,14 @@ def update_stack( ) if wait: # find the last event to use as the marker - events = event_utils.get_events(self.heat_client, - name_or_id, - event_args={'sort_dir': 'desc', - 'limit': 1}) + events = event_utils.get_events( + self, name_or_id, event_args={'sort_dir': 'desc', 'limit': 1}) marker = events[0].id if events else None - with _utils.heat_exceptions("Error updating stack {name}".format( - name=name_or_id)): - self.manager.submit_task(_tasks.StackUpdate(**params)) + self._orchestration_client.put( + '/stacks/{name_or_id}'.format(name_or_id=name_or_id), json=params) if wait: - event_utils.poll_for_events(self.heat_client, + event_utils.poll_for_events(self, name_or_id, action='UPDATE', marker=marker) @@ -1352,24 +1356,20 @@ def delete_stack(self, name_or_id, wait=False): if wait: # find the last event to use as the marker - events = event_utils.get_events(self.heat_client, - name_or_id, - event_args={'sort_dir': 'desc', - 'limit': 1}) + events = event_utils.get_events( + self, name_or_id, event_args={'sort_dir': 'desc', 'limit': 1}) marker = events[0].id if events else None - with _utils.heat_exceptions("Failed to delete stack {id}".format( - id=name_or_id)): - self.manager.submit_task(_tasks.StackDelete(id=stack['id'])) + self._orchestration_client.delete( + '/stacks/{id}'.format(id=stack['id'])) + if wait: try: - event_utils.poll_for_events(self.heat_client, + event_utils.poll_for_events(self, stack_name=name_or_id, action='DELETE', marker=marker) - except (heat_exceptions.NotFound, heat_exceptions.CommandError): - # heatclient might raise NotFound or CommandError on - # not found during poll_for_events + except OpenStackCloudHTTPError: pass stack = self.get_stack(name_or_id) if stack and stack['stack_status'] == 'DELETE_FAILED': @@ -1770,7 +1770,7 @@ def list_stacks(self): OpenStack API call. """ with _utils.shade_exceptions("Error fetching stack list"): - stacks = self.manager.submit_task(_tasks.StackList()) + stacks = self._orchestration_client.get('/stacks') return self._normalize_stacks(stacks) def list_server_security_groups(self, server): @@ -2773,16 +2773,15 @@ def _search_one_stack(name_or_id=None, filters=None): # so a StackGet can always be used for name or ID. with _utils.shade_exceptions("Error fetching stack"): try: - stack = self.manager.submit_task( - _tasks.StackGet(stack_id=name_or_id)) + stack = self._orchestration_client.get( + '/stacks/{name_or_id}'.format(name_or_id=name_or_id)) # Treat DELETE_COMPLETE stacks as a NotFound if stack['stack_status'] == 'DELETE_COMPLETE': return [] - stacks = [stack] - except heat_exceptions.NotFound: + except OpenStackCloudURINotFound: return [] - nstacks = self._normalize_stacks(stacks) - return _utils._filter_list(nstacks, name_or_id, filters) + stack = self._normalize_stack(stack) + return _utils._filter_list([stack], name_or_id, filters) return _utils._get_entity( _search_one_stack, name_or_id, filters) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 2f91c87c5..bb9f225d9 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -555,6 +555,7 @@ def assert_calls(self, stop_after=None): zip(self.calls, self.adapter.request_history)): if stop_after and x > stop_after: break + self.assertEqual( (call['method'], call['url']), (history.method, history.url), 'REST mismatch on call {index}'.format(index=x)) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index aef3e8948..3dbc2e232 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -16,7 +16,6 @@ from neutronclient.common import exceptions as n_exc import testtools -from os_client_config import cloud_config import shade from shade import _utils from shade import exc @@ -73,19 +72,6 @@ def test_list_servers_exception(self, mock_client): self.assertRaises(exc.OpenStackCloudException, self.cloud.list_servers) - @mock.patch.object(cloud_config.CloudConfig, 'get_session') - @mock.patch.object(cloud_config.CloudConfig, 'get_legacy_client') - def test_heat_args(self, get_legacy_client_mock, get_session_mock): - session_mock = mock.Mock() - get_session_mock.return_value = session_mock - self.cloud.heat_client - get_legacy_client_mock.assert_called_once_with( - service_key='orchestration', - client_class=None, - interface_key=None, - pass_version_arg=True, - ) - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') def test_list_networks(self, mock_neutron): net1 = {'id': '1', 'name': 'net1'} diff --git a/shade/tests/unit/test_stack.py b/shade/tests/unit/test_stack.py index 45f3de1e8..c32022992 100644 --- a/shade/tests/unit/test_stack.py +++ b/shade/tests/unit/test_stack.py @@ -55,10 +55,7 @@ def test_list_stacks_exception(self): endpoint=fakes.ORCHESTRATION_ENDPOINT), status_code=404) ]) - with testtools.ExpectedException( - shade.OpenStackCloudException, - "Error fetching stack list" - ): + with testtools.ExpectedException(shade.OpenStackCloudURINotFound): self.cloud.list_stacks() self.assert_calls() @@ -110,10 +107,7 @@ def test_search_stacks_exception(self): endpoint=fakes.ORCHESTRATION_ENDPOINT), status_code=404) ]) - with testtools.ExpectedException( - shade.OpenStackCloudException, - "Error fetching stack list" - ): + with testtools.ExpectedException(shade.OpenStackCloudURINotFound): self.cloud.search_stacks() def test_delete_stack(self): @@ -122,7 +116,11 @@ def test_delete_stack(self): uri='{endpoint}/stacks/{name}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, name=self.stack_name), - json={"stack": self.stack}), + status_code=302, + headers=dict( + location='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, name=self.stack_name))), dict(method='GET', uri='{endpoint}/stacks/{name}/{id}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, @@ -152,7 +150,11 @@ def test_delete_stack_exception(self): uri='{endpoint}/stacks/{id}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, id=self.stack_id), - json={"stack": self.stack}), + status_code=302, + headers=dict( + location='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, name=self.stack_name))), dict(method='GET', uri='{endpoint}/stacks/{name}/{id}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, @@ -165,7 +167,7 @@ def test_delete_stack_exception(self): status_code=400, reason="ouch"), ]) - with testtools.ExpectedException(shade.OpenStackCloudException): + with testtools.ExpectedException(shade.OpenStackCloudBadRequest): self.cloud.delete_stack(self.stack_id) self.assert_calls() @@ -179,7 +181,11 @@ def test_delete_stack_wait(self): uri='{endpoint}/stacks/{id}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, id=self.stack_id), - json={"stack": self.stack}), + status_code=302, + headers=dict( + location='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, name=self.stack_name))), dict(method='GET', uri='{endpoint}/stacks/{name}/{id}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, @@ -229,7 +235,11 @@ def test_delete_stack_wait_failed(self): uri='{endpoint}/stacks/{id}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, id=self.stack_id), - json={"stack": self.stack}), + status_code=302, + headers=dict( + location='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, name=self.stack_name))), dict(method='GET', uri='{endpoint}/stacks/{name}/{id}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, @@ -261,7 +271,11 @@ def test_delete_stack_wait_failed(self): uri='{endpoint}/stacks/{id}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, id=self.stack_id, name=self.stack_name), - json={'stack': failed_stack}), + status_code=302, + headers=dict( + location='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, name=self.stack_name))), dict(method='GET', uri='{endpoint}/stacks/{name}/{id}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, @@ -298,7 +312,11 @@ def test_create_stack(self): uri='{endpoint}/stacks/{name}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, name=self.stack_name), - json={"stack": self.stack}), + status_code=302, + headers=dict( + location='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, name=self.stack_name))), dict( method='GET', uri='{endpoint}/stacks/{name}/{id}'.format( @@ -351,7 +369,11 @@ def test_create_stack_wait(self): uri='{endpoint}/stacks/{name}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, name=self.stack_name), - json={"stack": self.stack}), + status_code=302, + headers=dict( + location='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, name=self.stack_name))), dict( method='GET', uri='{endpoint}/stacks/{name}/{id}'.format( @@ -390,7 +412,11 @@ def test_update_stack(self): uri='{endpoint}/stacks/{name}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, name=self.stack_name), - json={"stack": self.stack}), + status_code=302, + headers=dict( + location='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, name=self.stack_name))), dict( method='GET', uri='{endpoint}/stacks/{name}/{id}'.format( @@ -452,7 +478,11 @@ def test_update_stack_wait(self): uri='{endpoint}/stacks/{name}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, name=self.stack_name), - json={"stack": self.stack}), + status_code=302, + headers=dict( + location='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, name=self.stack_name))), dict( method='GET', uri='{endpoint}/stacks/{name}/{id}'.format( @@ -473,7 +503,11 @@ def test_get_stack(self): uri='{endpoint}/stacks/{name}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, name=self.stack_name), - json={"stack": self.stack}), + status_code=302, + headers=dict( + location='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, name=self.stack_name))), dict(method='GET', uri='{endpoint}/stacks/{name}/{id}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, From 86ff77be2d2fad7b42e02e0635622ca7a56f489b Mon Sep 17 00:00:00 2001 From: Rui Chen Date: Sat, 25 Mar 2017 15:24:50 +0800 Subject: [PATCH 1383/3836] Find floating ip by ip address Currently network.find_ip() function only support to search floating ip by UUID, the argument is called "name_or_id", but actually no "name" attribute exist in floating ip object that return by neutron server, reset "name" as "floating_ip_address" in openstacksdk, that make user could find floating ip by address, it's an useful case. Change-Id: I0dfb213251118157e50b630aebb536208b1676f2 --- openstack/network/v2/floating_ip.py | 3 +++ openstack/tests/functional/network/v2/test_floating_ip.py | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/openstack/network/v2/floating_ip.py b/openstack/network/v2/floating_ip.py index 630d10342..9a7385096 100644 --- a/openstack/network/v2/floating_ip.py +++ b/openstack/network/v2/floating_ip.py @@ -48,6 +48,9 @@ class FloatingIP(resource.Resource): fixed_ip_address = resource.Body('fixed_ip_address') #: The floating IP address. floating_ip_address = resource.Body('floating_ip_address') + #: Floating IP object doesn't have name attribute, set ip address to name + #: so that user could find floating IP by UUID or IP address using find_ip + name = floating_ip_address #: The ID of the network associated with the floating IP. floating_network_id = resource.Body('floating_network_id') #: The port ID. diff --git a/openstack/tests/functional/network/v2/test_floating_ip.py b/openstack/tests/functional/network/v2/test_floating_ip.py index 83f48225e..b2274ce56 100644 --- a/openstack/tests/functional/network/v2/test_floating_ip.py +++ b/openstack/tests/functional/network/v2/test_floating_ip.py @@ -118,10 +118,15 @@ def _create_subnet(cls, name, net_id, cidr): cls.assertIs(cls.name, sub.name) return sub - def test_find(self): + def test_find_by_id(self): sot = self.conn.network.find_ip(self.FIP.id) self.assertEqual(self.FIP.id, sot.id) + def test_find_by_ip_address(self): + sot = self.conn.network.find_ip(self.FIP.floating_ip_address) + self.assertEqual(self.FIP.floating_ip_address, sot.floating_ip_address) + self.assertEqual(self.FIP.floating_ip_address, sot.name) + def test_find_available_ip(self): sot = self.conn.network.find_available_ip() self.assertIsNotNone(sot.id) From 01ff292e078206e487751228be4a7062ba0c6048 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 27 Mar 2017 09:16:35 -0500 Subject: [PATCH 1384/3836] Stop special-casing idenity catalog lookups We have a special case to work around a thing we're pretty sure keystoneclient used to do but apparently doesn't do anymore. Remove the workaround. Co-Authored-By: Jamie Lennox Change-Id: I873ad91816150b593d4aef13dcd1520e8c91b22a --- os_client_config/cloud_config.py | 42 +++++++++------------ os_client_config/tests/test_cloud_config.py | 13 ------- 2 files changed, 17 insertions(+), 38 deletions(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 99ef2ba73..3ba3541ea 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -18,7 +18,6 @@ from keystoneauth1 import adapter import keystoneauth1.exceptions.catalog -from keystoneauth1 import plugin from keystoneauth1 import session import requestsexceptions @@ -272,31 +271,24 @@ def get_session_endpoint( 'service_name': self.get_service_name(service_key), 'region_name': self.region } - if service_key == 'identity': - # setting interface in kwargs dict even though we don't use kwargs - # dict here just for ease of warning text later - kwargs['interface'] = plugin.AUTH_INTERFACE - session = self.get_session() - endpoint = session.get_endpoint(interface=kwargs['interface']) + kwargs['interface'] = self.get_interface(service_key) + if service_key == 'volume' and not self.get_api_version('volume'): + # If we don't have a configured cinder version, we can't know + # to request a different service_type + min_version = float(min_version or 1) + max_version = float(max_version or 3) + min_major = math.trunc(float(min_version)) + max_major = math.trunc(float(max_version)) + versions = range(int(max_major) + 1, int(min_major), -1) + service_types = [] + for version in versions: + if version == 1: + service_types.append('volume') + else: + service_types.append('volumev{v}'.format(v=version)) else: - kwargs['interface'] = self.get_interface(service_key) - if service_key == 'volume' and not self.get_api_version('volume'): - # If we don't have a configured cinder version, we can't know - # to request a different service_type - min_version = float(min_version or 1) - max_version = float(max_version or 3) - min_major = math.trunc(float(min_version)) - max_major = math.trunc(float(max_version)) - versions = range(int(max_major) + 1, int(min_major), -1) - service_types = [] - for version in versions: - if version == 1: - service_types.append('volume') - else: - service_types.append('volumev{v}'.format(v=version)) - else: - service_types = [self.get_service_type(service_key)] - endpoint = self._get_highest_endpoint(service_types, kwargs) + service_types = [self.get_service_type(service_key)] + endpoint = self._get_highest_endpoint(service_types, kwargs) if not endpoint: self.log.warning( "Keystone catalog entry not found (" diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index 3c1ae1f34..cb6d91c3d 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -13,7 +13,6 @@ import copy from keystoneauth1 import exceptions as ksa_exceptions -from keystoneauth1 import plugin as ksa_plugin from keystoneauth1 import session as ksa_session import mock @@ -219,18 +218,6 @@ def test_override_session_endpoint(self, mock_session): cc.get_session_endpoint('telemetry'), fake_services_dict['telemetry_endpoint']) - @mock.patch.object(cloud_config.CloudConfig, 'get_session') - def test_session_endpoint_identity(self, mock_get_session): - mock_session = mock.Mock() - mock_get_session.return_value = mock_session - config_dict = defaults.get_defaults() - config_dict.update(fake_services_dict) - cc = cloud_config.CloudConfig( - "test1", "region-al", config_dict, auth_plugin=mock.Mock()) - cc.get_session_endpoint('identity') - mock_session.get_endpoint.assert_called_with( - interface=ksa_plugin.AUTH_INTERFACE) - @mock.patch.object(cloud_config.CloudConfig, 'get_session') def test_session_endpoint(self, mock_get_session): mock_session = mock.Mock() From 8aa8688ba8a578728ed5c33dc14c0eecf631225b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 28 Mar 2017 07:48:17 -0500 Subject: [PATCH 1385/3836] Move futures to requirements Wow. We had futures in our test-requirements so we never noticed it wasn't in our regular requirements. Change-Id: I6ae3434effc778524908d009627e9c0da6258f7c --- releasenotes/notes/fix-missing-futures-a0617a1c1ce6e659.yaml | 5 +++++ requirements.txt | 1 + test-requirements.txt | 1 - 3 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/fix-missing-futures-a0617a1c1ce6e659.yaml diff --git a/releasenotes/notes/fix-missing-futures-a0617a1c1ce6e659.yaml b/releasenotes/notes/fix-missing-futures-a0617a1c1ce6e659.yaml new file mode 100644 index 000000000..94a2ab857 --- /dev/null +++ b/releasenotes/notes/fix-missing-futures-a0617a1c1ce6e659.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - Added missing dependency on futures library for python 2. + The depend was missed in testing due to it having been listed + in test-requirements already. diff --git a/requirements.txt b/requirements.txt index 5b5550669..fe1070350 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,6 +8,7 @@ ipaddress os-client-config>=1.25.0 requestsexceptions>=1.1.1 six +futures;python_version<'3.2' keystoneauth1>=2.11.0 netifaces>=0.10.4 diff --git a/test-requirements.txt b/test-requirements.txt index e7aa54668..b3fea41a3 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -11,4 +11,3 @@ testrepository>=0.0.17 testscenarios>=0.4,<0.5 testtools>=0.9.32 reno -futures;python_version<'3.2' From 8dc68b89ff00d9f490e1c63882386d18974158eb Mon Sep 17 00:00:00 2001 From: Akihiro Motoki Date: Mon, 27 Mar 2017 10:14:43 +0000 Subject: [PATCH 1386/3836] Filtering support by is_router_external to network resource Filtering networks by is_router_external (router:external) is missing in the current SDK. This is important from the perspective of migration from neutron CLI to OSC because networks with router:external True are regarded as floating IP pools. Change-Id: I164bee3c9de00bdd0513114577e94b68f6cc7e5a Closes-Bug: #1676355 --- openstack/network/v2/network.py | 1 + .../tests/unit/network/v2/test_network.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/openstack/network/v2/network.py b/openstack/network/v2/network.py index 9e79b519e..57439dd59 100644 --- a/openstack/network/v2/network.py +++ b/openstack/network/v2/network.py @@ -34,6 +34,7 @@ class Network(resource.Resource): ipv6_address_scope_id='ipv6_address_scope', is_admin_state_up='admin_state_up', is_port_security_enabled='port_security_enabled', + is_router_external='router:external', is_shared='shared', provider_network_type='provider:network_type', provider_physical_network='provider:physical_network', diff --git a/openstack/tests/unit/network/v2/test_network.py b/openstack/tests/unit/network/v2/test_network.py index 34f5cdfa7..8fc21ea6d 100644 --- a/openstack/tests/unit/network/v2/test_network.py +++ b/openstack/tests/unit/network/v2/test_network.py @@ -93,6 +93,25 @@ def test_make_it(self): self.assertEqual(EXAMPLE['subnets'], sot.subnet_ids) self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) + self.assertDictEqual( + {'limit': 'limit', + 'marker': 'marker', + 'description': 'description', + 'name': 'name', + 'project_id': 'project_id', + 'status': 'status', + 'ipv4_address_scope_id': 'ipv4_address_scope', + 'ipv6_address_scope_id': 'ipv6_address_scope', + 'is_admin_state_up': 'admin_state_up', + 'is_port_security_enabled': 'port_security_enabled', + 'is_router_external': 'router:external', + 'is_shared': 'shared', + 'provider_network_type': 'provider:network_type', + 'provider_physical_network': 'provider:physical_network', + 'provider_segmentation_id': 'provider:segmentation_id' + }, + sot._query_mapping._mapping) + class TestDHCPAgentHostingNetwork(testtools.TestCase): From 451ec8daadfb6702841878cac1a8d4e6012b838d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 28 Mar 2017 10:59:32 -0500 Subject: [PATCH 1387/3836] Remove out of date comment Change-Id: I8a26f5952456a96429ff1413b90aef3091a8b5bf --- os_client_config/cloud_config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 3ba3541ea..45bb82599 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -265,7 +265,6 @@ def get_session_endpoint( override_endpoint = self.get_endpoint(service_key) if override_endpoint: return override_endpoint - # keystone is a special case in keystone, because what? endpoint = None kwargs = { 'service_name': self.get_service_name(service_key), From e2cbd3253e8595c50b2f07a38edb0b2fcd872525 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 28 Mar 2017 16:10:24 -0500 Subject: [PATCH 1388/3836] Change metadata to align with team affiliation Shade is its own team now, update "author" to be OpenStack. Note, the homepage setting does not exist until we tag a release after the depends-on project-config patch lands. But this metadata also will not go to PyPI until we tag a release - thus the depends-on. Depends-On: Id3a087a41491eb8bf8eb51a0247183c6b91a297a Change-Id: I4fc7ed2ecca0d218be7f1e1e34dcfa4c4b623d4e --- setup.cfg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 6223d6a1a..4f3139ec0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,9 +3,9 @@ name = shade summary = Simple client library for interacting with OpenStack clouds description-file = README.rst -author = OpenStack Infrastructure Team -author-email = openstack-infra@lists.openstack.org -home-page = http://docs.openstack.org/infra/shade/ +author = OpenStack +author-email = openstack-dev@lists.openstack.org +home-page = http://docs.openstack.org/developer/shade/ classifier = Environment :: OpenStack Intended Audience :: Information Technology From 0b8a6d0718b614522e88217b10d28363ec047a58 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Wed, 29 Mar 2017 11:15:34 +1100 Subject: [PATCH 1389/3836] Don't use project-id in catalog tests Nova shouldn't need the project-id in operations any more. Whether it uses it or not will be based on the URL that comes from the catalog so it's not going to break existing deployments that do need it. Change-Id: I484254054377b5760a68c2792bc1391a8de48482 --- shade/tests/fakes.py | 3 +-- shade/tests/unit/fixtures/catalog-v3.json | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 7b33759dc..40a1736bc 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -25,8 +25,7 @@ FLAVOR_ID = u'0c1d9008-f546-4608-9e8f-f8bdaec8dddd' CHOCOLATE_FLAVOR_ID = u'0c1d9008-f546-4608-9e8f-f8bdaec8ddde' STRAWBERRY_FLAVOR_ID = u'0c1d9008-f546-4608-9e8f-f8bdaec8dddf' -COMPUTE_ENDPOINT = 'https://compute.example.com/v2.1/{project_id}'.format( - project_id=PROJECT_ID) +COMPUTE_ENDPOINT = 'https://compute.example.com/v2.1' ORCHESTRATION_ENDPOINT = 'https://orchestration.example.com/v1/{p}'.format( p=PROJECT_ID) diff --git a/shade/tests/unit/fixtures/catalog-v3.json b/shade/tests/unit/fixtures/catalog-v3.json index 3f9ba217a..0c372603e 100644 --- a/shade/tests/unit/fixtures/catalog-v3.json +++ b/shade/tests/unit/fixtures/catalog-v3.json @@ -10,7 +10,7 @@ "id": "32466f357f3545248c47471ca51b0d3a", "interface": "public", "region": "RegionOne", - "url": "https://compute.example.com/v2.1/1c36b64c840a42cd9e9b931a369337f0" + "url": "https://compute.example.com/v2.1/" } ], "name": "nova", From ce06555c20e7fd4daf12a1bd8972cce4bc49c34f Mon Sep 17 00:00:00 2001 From: Ethan Lynn Date: Mon, 27 Mar 2017 13:18:31 +0800 Subject: [PATCH 1390/3836] Add vlan_transparent property to network resource vlan_transparent property indicates the VLAN transparency mode of the network, which can be set to true or false. Change-Id: Ibb00e8a1124eaea5434134ea52e4a01de20e8f51 --- openstack/network/v2/network.py | 2 ++ openstack/tests/unit/network/v2/test_network.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/openstack/network/v2/network.py b/openstack/network/v2/network.py index 9e79b519e..f2f76c357 100644 --- a/openstack/network/v2/network.py +++ b/openstack/network/v2/network.py @@ -108,6 +108,8 @@ class Network(resource.Resource): subnet_ids = resource.Body('subnets', type=list) #: Timestamp when the network was last updated. updated_at = resource.Body('updated_at') + #: Indicates the VLAN transparency mode of the network + is_vlan_transparent = resource.Body('vlan_transparent', type=bool) class DHCPAgentHostingNetwork(Network): diff --git a/openstack/tests/unit/network/v2/test_network.py b/openstack/tests/unit/network/v2/test_network.py index 34f5cdfa7..b4070b84e 100644 --- a/openstack/tests/unit/network/v2/test_network.py +++ b/openstack/tests/unit/network/v2/test_network.py @@ -41,6 +41,7 @@ 'status': '17', 'subnets': ['18', '19'], 'updated_at': '2016-07-09T12:14:57.233772', + 'vlan_transparent': False, } @@ -92,6 +93,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['status'], sot.status) self.assertEqual(EXAMPLE['subnets'], sot.subnet_ids) self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) + self.assertEqual(EXAMPLE['vlan_transparent'], sot.is_vlan_transparent) class TestDHCPAgentHostingNetwork(testtools.TestCase): From c9e9ef9f43dce18e6cdcd528414a05b9ad78134a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 15 Mar 2017 18:06:28 +0100 Subject: [PATCH 1391/3836] Fix a few minor annoyances that snuck in We had the normal infra pep8 ignores marked, but follow some of them by hand in this codebase anyway. While E12* are not valid pep8, the prevailing style of shade is in agreement with them. While we weren't enforcing them, a few instances snuck in. Fix them and just turn the flags on. Change-Id: Iea2bd9a99eb8b20dd40418ac8479b809d3872de8 --- shade/openstackcloud.py | 12 ++++++------ shade/operatorcloud.py | 2 +- shade/tests/functional/test_endpoints.py | 4 ++-- shade/tests/functional/test_object.py | 6 ++---- shade/tests/functional/test_services.py | 4 ++-- shade/tests/unit/test__utils.py | 4 ++-- tox.ini | 3 +-- 7 files changed, 16 insertions(+), 19 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index a27c996e7..d00e38016 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2121,8 +2121,7 @@ def _set_interesting_networks(self): external_ipv4_networks.append(network) # External Floating IPv4 networks - if ('router:external' in network - and network['router:external']): + if ('router:external' in network and network['router:external']): external_ipv4_floating_networks.append(network) # Internal networks @@ -3016,7 +3015,7 @@ def list_router_interfaces(self, router, interface_type=None): if interface_type: filtered_ports = [] if (router.get('external_gateway_info') and - 'external_fixed_ips' in router['external_gateway_info']): + 'external_fixed_ips' in router['external_gateway_info']): ext_fixed = \ router['external_gateway_info']['external_fixed_ips'] else: @@ -4013,7 +4012,7 @@ def create_volume_snapshot(self, volume_id, force=False, for count in _utils._iterate_timeout( timeout, "Timeout waiting for the volume snapshot to be available." - ): + ): snapshot = self.get_volume_snapshot_by_id(snapshot_id) if snapshot['status'] == 'available': @@ -5862,7 +5861,7 @@ def get_object_segment_size(self, segment_size): return segment_size def is_object_stale( - self, container, name, filename, file_md5=None, file_sha256=None): + self, container, name, filename, file_md5=None, file_sha256=None): metadata = self.get_object_metadata(container, name) if not metadata: @@ -5998,7 +5997,8 @@ def _add_etag_to_manifest(self, segment_results, manifest): entry['etag'] = result.headers['Etag'] def _upload_large_object( - self, endpoint, filename, headers, file_size, segment_size, use_slo): + self, endpoint, filename, + headers, file_size, segment_size, use_slo): # If the object is big, we need to break it up into segments that # are no larger than segment_size, upload each of them individually # and then upload a manifest object. The segments can be uploaded in diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index bed069af8..02ddd4709 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1150,7 +1150,7 @@ def delete_domain(self, domain_id=None, name_or_id=None): domain_id = dom['id'] with _utils.shade_exceptions( - "Failed to delete domain {id}".format(id=domain_id)): + "Failed to delete domain {id}".format(id=domain_id)): # Deleting a domain is expensive, so disabling it first increases # the changes of success domain = self.update_domain(domain_id, enabled=False) diff --git a/shade/tests/functional/test_endpoints.py b/shade/tests/functional/test_endpoints.py index e7346fe64..4ef648c47 100644 --- a/shade/tests/functional/test_endpoints.py +++ b/shade/tests/functional/test_endpoints.py @@ -103,8 +103,8 @@ def test_create_endpoint(self): self.assertIsNotNone(endpoints[0].get('id')) def test_update_endpoint(self): - if self.operator_cloud.cloud_config.get_api_version( - 'identity').startswith('2'): + ver = self.operator_cloud.cloud_config.get_api_version('identity') + if ver.startswith('2'): # NOTE(SamYaple): Update endpoint only works with v3 api self.assertRaises(OpenStackCloudUnavailableFeature, self.operator_cloud.update_endpoint, diff --git a/shade/tests/functional/test_object.py b/shade/tests/functional/test_object.py index b14834df3..a13b27ac3 100644 --- a/shade/tests/functional/test_object.py +++ b/shade/tests/functional/test_object.py @@ -66,8 +66,7 @@ def test_create_object(self): self.assertFalse(self.user_cloud.is_object_stale( container_name, name, fake_file.name - ) - ) + )) self.assertEqual( 'bar', self.user_cloud.get_object_metadata( container_name, name)['x-object-meta-foo'] @@ -131,8 +130,7 @@ def test_download_object_to_file(self): self.assertFalse(self.user_cloud.is_object_stale( container_name, name, fake_file.name - ) - ) + )) self.assertEqual( 'bar', self.user_cloud.get_object_metadata( container_name, name)['x-object-meta-foo'] diff --git a/shade/tests/functional/test_services.py b/shade/tests/functional/test_services.py index bd43deb67..729c6c7af 100644 --- a/shade/tests/functional/test_services.py +++ b/shade/tests/functional/test_services.py @@ -65,8 +65,8 @@ def test_create_service(self): self.assertIsNotNone(service.get('id')) def test_update_service(self): - if self.operator_cloud.cloud_config.get_api_version( - 'identity').startswith('2'): + ver = self.operator_cloud.cloud_config.get_api_version('identity') + if ver.startswith('2'): # NOTE(SamYaple): Update service only works with v3 api self.assertRaises(OpenStackCloudUnavailableFeature, self.operator_cloud.update_service, diff --git a/shade/tests/unit/test__utils.py b/shade/tests/unit/test__utils.py index 8296f471e..24da072f2 100644 --- a/shade/tests/unit/test__utils.py +++ b/shade/tests/unit/test__utils.py @@ -98,7 +98,7 @@ def test__filter_list_unicode(self): data, u'中文', {'other': { 'financial': {'status': 'rich'} - }}) + }}) self.assertEqual([el2], ret) def test__filter_list_filter(self): @@ -139,7 +139,7 @@ def test__filter_list_dict2(self): data, 'donald', {'other': { 'financial': {'status': 'rich'} - }}) + }}) self.assertEqual([el2, el3], ret) def test_safe_dict_min_ints(self): diff --git a/tox.ini b/tox.ini index 3a82b0c17..c6272e480 100644 --- a/tox.ini +++ b/tox.ini @@ -67,10 +67,9 @@ commands = python setup.py build_sphinx commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [flake8] -# Infra does not follow hacking, nor the broken E12* things # The string of H ignores is because there are some useful checks # related to python3 compat. -ignore = E123,E125,E129,H3,H4,H5,H6,H7,H8,H103,H201,H238 +ignore = H3,H4,H5,H6,H7,H8,H103,H201,H238 show-source = True builtins = _ exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build From fe130c620b9eb39eb30dccf853d973b829056db3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 15 Mar 2017 18:19:59 +0100 Subject: [PATCH 1392/3836] Enable H238 - classes should be subclasses of object As shade is no longer an Infra project, it's no longer appropriate to ignore hacking. Luckily, shade is mostly hacking clean anyway, so this isn't terribly disruptive. Change-Id: I607f1835abb328e3b4a4a33d80a1848ab11779be --- shade/tests/unit/test_shade_operator.py | 104 ++++++++++++------------ tox.ini | 2 +- 2 files changed, 53 insertions(+), 53 deletions(-) diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index 9693bda95..bafda1db9 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -40,11 +40,11 @@ def test_get_machine(self, mock_client): @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_get_machine_by_mac(self, mock_client): - class port_value: + class port_value(object): node_uuid = '00000000-0000-0000-0000-000000000000' address = '00:00:00:00:00:00' - class node_value: + class node_value(object): uuid = '00000000-0000-0000-0000-000000000000' expected_value = dict( @@ -117,7 +117,7 @@ def test_patch_machine(self, mock_client): @mock.patch.object(shade.OperatorCloud, 'ironic_client') @mock.patch.object(shade.OperatorCloud, 'patch_machine') def test_update_machine_patch_no_action(self, mock_patch, mock_client): - class client_return_value: + class client_return_value(object): uuid = '00000000-0000-0000-0000-000000000000' name = 'node01' @@ -136,7 +136,7 @@ class client_return_value: @mock.patch.object(shade.OperatorCloud, 'patch_machine') def test_update_machine_patch_no_action_name(self, mock_patch, mock_client): - class client_return_value: + class client_return_value(object): uuid = '00000000-0000-0000-0000-000000000000' name = 'node01' @@ -155,7 +155,7 @@ class client_return_value: @mock.patch.object(shade.OperatorCloud, 'patch_machine') def test_update_machine_patch_action_name(self, mock_patch, mock_client): - class client_return_value: + class client_return_value(object): uuid = '00000000-0000-0000-0000-000000000000' name = 'evil' @@ -175,7 +175,7 @@ class client_return_value: @mock.patch.object(shade.OperatorCloud, 'patch_machine') def test_update_machine_patch_update_name(self, mock_patch, mock_client): - class client_return_value: + class client_return_value(object): uuid = '00000000-0000-0000-0000-000000000000' name = 'evil' @@ -195,7 +195,7 @@ class client_return_value: @mock.patch.object(shade.OperatorCloud, 'patch_machine') def test_update_machine_patch_update_chassis_uuid(self, mock_patch, mock_client): - class client_return_value: + class client_return_value(object): uuid = '00000000-0000-0000-0000-000000000000' chassis_uuid = None @@ -222,7 +222,7 @@ class client_return_value: @mock.patch.object(shade.OperatorCloud, 'patch_machine') def test_update_machine_patch_update_driver(self, mock_patch, mock_client): - class client_return_value: + class client_return_value(object): uuid = '00000000-0000-0000-0000-000000000000' driver = None @@ -250,7 +250,7 @@ class client_return_value: @mock.patch.object(shade.OperatorCloud, 'patch_machine') def test_update_machine_patch_update_driver_info(self, mock_patch, mock_client): - class client_return_value: + class client_return_value(object): uuid = '00000000-0000-0000-0000-000000000000' driver_info = None @@ -278,7 +278,7 @@ class client_return_value: @mock.patch.object(shade.OperatorCloud, 'patch_machine') def test_update_machine_patch_update_instance_info(self, mock_patch, mock_client): - class client_return_value: + class client_return_value(object): uuid = '00000000-0000-0000-0000-000000000000' instance_info = None @@ -306,7 +306,7 @@ class client_return_value: @mock.patch.object(shade.OperatorCloud, 'patch_machine') def test_update_machine_patch_update_instance_uuid(self, mock_patch, mock_client): - class client_return_value: + class client_return_value(object): uuid = '00000000-0000-0000-0000-000000000000' instance_uuid = None @@ -334,7 +334,7 @@ class client_return_value: @mock.patch.object(shade.OperatorCloud, 'patch_machine') def test_update_machine_patch_update_properties(self, mock_patch, mock_client): - class client_return_value: + class client_return_value(object): uuid = '00000000-0000-0000-0000-000000000000' properties = None @@ -363,7 +363,7 @@ def test_inspect_machine_fail_active(self, mock_client): machine_uuid = '00000000-0000-0000-0000-000000000000' - class active_machine: + class active_machine(object): uuid = machine_uuid provision_state = "active" @@ -380,7 +380,7 @@ def test_inspect_machine_failed(self, mock_client): machine_uuid = '00000000-0000-0000-0000-000000000000' - class inspect_failed_machine: + class inspect_failed_machine(object): uuid = machine_uuid provision_state = "inspect failed" last_error = "kaboom" @@ -396,7 +396,7 @@ def test_inspect_machine_managable(self, mock_client): machine_uuid = '00000000-0000-0000-0000-000000000000' - class manageable_machine: + class manageable_machine(object): uuid = machine_uuid provision_state = "manageable" @@ -410,15 +410,15 @@ def test_inspect_machine_available(self, mock_client): machine_uuid = '00000000-0000-0000-0000-000000000000' - class available_machine: + class available_machine(object): uuid = machine_uuid provision_state = "available" - class manageable_machine: + class manageable_machine(object): uuid = machine_uuid provision_state = "manageable" - class inspecting_machine: + class inspecting_machine(object): uuid = machine_uuid provision_state = "inspecting" @@ -438,15 +438,15 @@ def test_inspect_machine_available_wait(self, mock_client): machine_uuid = '00000000-0000-0000-0000-000000000000' - class available_machine: + class available_machine(object): uuid = machine_uuid provision_state = "available" - class manageable_machine: + class manageable_machine(object): uuid = machine_uuid provision_state = "manageable" - class inspecting_machine: + class inspecting_machine(object): uuid = machine_uuid provision_state = "inspecting" @@ -475,11 +475,11 @@ def test_inspect_machine_wait(self, mock_client): machine_uuid = '00000000-0000-0000-0000-000000000000' - class manageable_machine: + class manageable_machine(object): uuid = machine_uuid provision_state = "manageable" - class inspecting_machine: + class inspecting_machine(object): uuid = machine_uuid provision_state = "inspecting" @@ -503,17 +503,17 @@ def test_inspect_machine_inspect_failed(self, mock_client): machine_uuid = '00000000-0000-0000-0000-000000000000' - class manageable_machine: + class manageable_machine(object): uuid = machine_uuid provision_state = "manageable" last_error = None - class inspecting_machine: + class inspecting_machine(object): uuid = machine_uuid provision_state = "inspecting" last_error = None - class inspect_failed_machine: + class inspect_failed_machine(object): uuid = machine_uuid provision_state = "inspect failed" last_error = "kaboom" @@ -534,7 +534,7 @@ class inspect_failed_machine: @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_register_machine(self, mock_client): - class fake_node: + class fake_node(object): uuid = "00000000-0000-0000-0000-000000000000" provision_state = "available" reservation = None @@ -563,31 +563,31 @@ def test_register_machine_enroll( mock_client): machine_uuid = "00000000-0000-0000-0000-000000000000" - class fake_node_init_state: + class fake_node_init_state(object): uuid = machine_uuid provision_state = "enroll" reservation = None last_error = None - class fake_node_post_manage: + class fake_node_post_manage(object): uuid = machine_uuid provision_state = "enroll" reservation = "do you have a flag?" last_error = None - class fake_node_post_manage_done: + class fake_node_post_manage_done(object): uuid = machine_uuid provision_state = "manage" reservation = None last_error = None - class fake_node_post_provide: + class fake_node_post_provide(object): uuid = machine_uuid provision_state = "available" reservation = None last_error = None - class fake_node_post_enroll_failure: + class fake_node_post_enroll_failure(object): uuid = machine_uuid provision_state = "enroll" reservation = None @@ -646,7 +646,7 @@ def test_register_machine_enroll_timeout( mock_client): machine_uuid = "00000000-0000-0000-0000-000000000000" - class fake_node_init_state: + class fake_node_init_state(object): uuid = machine_uuid provision_state = "enroll" reservation = "do you have a flag?" @@ -677,7 +677,7 @@ class fake_node_init_state: @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_register_machine_port_create_failed(self, mock_client): - class fake_node: + class fake_node(object): uuid = "00000000-0000-0000-0000-000000000000" provision_state = "available" resevation = None @@ -696,10 +696,10 @@ class fake_node: @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_unregister_machine(self, mock_client): - class fake_node: + class fake_node(object): provision_state = 'available' - class fake_port: + class fake_port(object): uuid = '00000000-0000-0000-0000-000000000001' mock_client.port.get_by_address.return_value = fake_port @@ -722,7 +722,7 @@ def test_unregister_machine_unavailable(self, mock_client): nics = [{'mac': '00:00:00:00:00:00'}] uuid = "00000000-0000-0000-0000-000000000000" for state in invalid_states: - class fake_node: + class fake_node(object): provision_state = state mock_client.node.get.return_value = fake_node @@ -740,7 +740,7 @@ class fake_node: @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_unregister_machine_timeout(self, mock_client): - class fake_node: + class fake_node(object): provision_state = 'available' mock_client.node.get.return_value = fake_node @@ -831,7 +831,7 @@ def test_set_machine_power_reboot_failure(self, mock_client): @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_node_set_provision_state(self, mock_client): - class active_node_state: + class active_node_state(object): provision_state = "active" active_return_value = dict( @@ -853,16 +853,16 @@ class active_node_state: @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_node_set_provision_state_wait_timeout(self, mock_client): - class deploying_node_state: + class deploying_node_state(object): provision_state = "deploying" - class active_node_state: + class active_node_state(object): provision_state = "active" - class managable_node_state: + class managable_node_state(object): provision_state = "managable" - class available_node_state: + class available_node_state(object): provision_state = "available" active_return_value = dict( @@ -900,13 +900,13 @@ class available_node_state: @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_node_set_provision_state_wait_failure(self, mock_client): - class active_node_state: + class active_node_state(object): provision_state = "active" - class deploy_failed_node_state: + class deploy_failed_node_state(object): provision_state = "deploy failed" - class clean_failed_node_state: + class clean_failed_node_state(object): provision_state = "clean failed" active_return_value = dict( @@ -959,10 +959,10 @@ class clean_failed_node_state: @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_node_set_provision_state_wait_provide(self, mock_client): - class managable_node_state: + class managable_node_state(object): provision_state = "managable" - class available_node_state: + class available_node_state(object): provision_state = "available" node_provide_return_value = dict( @@ -996,10 +996,10 @@ def test_activate_node(self, mock_timeout, mock_client): @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_activate_node_timeout(self, mock_client): - class active_node_state: + class active_node_state(object): provision_state = 'active' - class available_node_state: + class available_node_state(object): provision_state = 'available' mock_client.node.get.side_effect = iter([ @@ -1037,10 +1037,10 @@ def test_deactivate_node(self, mock_timeout, mock_client): @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_deactivate_node_timeout(self, mock_client): - class active_node_state: + class active_node_state(object): provision_state = 'active' - class deactivated_node_state: + class deactivated_node_state(object): provision_state = 'available' mock_client.node.get.side_effect = iter([ diff --git a/tox.ini b/tox.ini index c6272e480..1ae0d6d0f 100644 --- a/tox.ini +++ b/tox.ini @@ -69,7 +69,7 @@ commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasen [flake8] # The string of H ignores is because there are some useful checks # related to python3 compat. -ignore = H3,H4,H5,H6,H7,H8,H103,H201,H238 +ignore = H3,H4,H5,H6,H7,H8,H103,H201 show-source = True builtins = _ exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build From 7ac3c0321246816abb4fb59011dbb259590e78d4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 15 Mar 2017 18:21:46 +0100 Subject: [PATCH 1393/3836] Enable H201 - don't throw bare exceptions I agree - bare except is bad - it can cause ctrl-c to stop working, which is totally evil. (I mean, unless that is the intent, in which case it's the right thing to do. None of these cases are cases when we desire ctrl-c to stop working) Change-Id: Ieffa26e8da77199c9ef7238a6c9f0704fc581ba6 --- shade/openstackcloud.py | 2 +- shade/operatorcloud.py | 2 +- shade/tests/functional/test_cluster_templates.py | 2 +- tox.ini | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index d00e38016..0df926fa8 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -7044,7 +7044,7 @@ def get_recordset(self, zone, name_or_id): return self.manager.submit_task(_tasks.RecordSetGet( zone=zone, recordset=name_or_id)) - except: + except Exception: return None def search_recordsets(self, zone, name_or_id=None, filters=None): diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 02ddd4709..c1f7db730 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -241,7 +241,7 @@ def register_machine(self, nics, wait=False, timeout=3600, self.manager.submit_task( _tasks.MachinePortDelete( port_id=uuid)) - except: + except Exception: pass finally: self.manager.submit_task( diff --git a/shade/tests/functional/test_cluster_templates.py b/shade/tests/functional/test_cluster_templates.py index 8d550e69e..634858d97 100644 --- a/shade/tests/functional/test_cluster_templates.py +++ b/shade/tests/functional/test_cluster_templates.py @@ -104,7 +104,7 @@ def cleanup(self, name): if self.ct: try: self.user_cloud.delete_cluster_template(self.ct['name']) - except: + except Exception: pass # delete keypair diff --git a/tox.ini b/tox.ini index 1ae0d6d0f..0daecfff5 100644 --- a/tox.ini +++ b/tox.ini @@ -69,7 +69,7 @@ commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasen [flake8] # The string of H ignores is because there are some useful checks # related to python3 compat. -ignore = H3,H4,H5,H6,H7,H8,H103,H201 +ignore = H3,H4,H5,H6,H7,H8,H103 show-source = True builtins = _ exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build From b34e06f81b251a405638ac0698fef7943f3d3bfc Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 25 Mar 2017 17:12:28 -0500 Subject: [PATCH 1394/3836] Reenable hacking tests that already pass Several of the ignores are things we're already clean on. Just reenable them. Change-Id: Ic6ac2123ca2574a089aa2882f84cf0d4a9d47b1e --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 0daecfff5..a6ca42e66 100644 --- a/tox.ini +++ b/tox.ini @@ -69,7 +69,7 @@ commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasen [flake8] # The string of H ignores is because there are some useful checks # related to python3 compat. -ignore = H3,H4,H5,H6,H7,H8,H103 +ignore = H3,H4,H103 show-source = True builtins = _ exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build From b447fa4356c9af229f9fb49226f02711d936f390 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 25 Mar 2017 17:22:23 -0500 Subject: [PATCH 1395/3836] Take care of multiple imports and update explanation The explanation for what's up with pep8 checks is a little different than it used to be. Get it up to current. Change-Id: I2c03a5aec02dd41ed5cdfb4c4082e17339ed2765 --- shade/tests/unit/test_create_server.py | 52 +++++++++---------- .../tests/unit/test_create_volume_snapshot.py | 14 ++--- .../tests/unit/test_delete_volume_snapshot.py | 14 ++--- shade/tests/unit/test_rebuild_server.py | 8 +-- shade/tests/unit/test_role_assignment.py | 30 +++++------ shade/tests/unit/test_zone.py | 2 +- tox.ini | 10 ++-- 7 files changed, 67 insertions(+), 63 deletions(-) diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index 0e4816f8c..064da412a 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -18,16 +18,16 @@ """ import mock +import shade +from shade import exc from shade import meta -from shade import OpenStackCloud -from shade.exc import (OpenStackCloudException, OpenStackCloudTimeout) from shade.tests import fakes from shade.tests.unit import base class TestCreateServer(base.RequestsMockTestCase): - @mock.patch.object(OpenStackCloud, 'nova_client') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_create_server_with_create_exception(self, mock_nova): """ Test that an exception in the novaclient create raises an exception in @@ -35,10 +35,10 @@ def test_create_server_with_create_exception(self, mock_nova): """ mock_nova.servers.create.side_effect = Exception("exception") self.assertRaises( - OpenStackCloudException, self.cloud.create_server, + exc.OpenStackCloudException, self.cloud.create_server, 'server-name', {'id': 'image-id'}, {'id': 'flavor-id'}) - @mock.patch.object(OpenStackCloud, 'nova_client') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_create_server_with_get_exception(self, mock_nova): """ Test that an exception when attempting to get the server instance via @@ -48,10 +48,10 @@ def test_create_server_with_get_exception(self, mock_nova): mock_nova.servers.create.return_value = mock.Mock(status="BUILD") mock_nova.servers.get.side_effect = Exception("exception") self.assertRaises( - OpenStackCloudException, self.cloud.create_server, + exc.OpenStackCloudException, self.cloud.create_server, 'server-name', {'id': 'image-id'}, {'id': 'flavor-id'}) - @mock.patch.object(OpenStackCloud, 'nova_client') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_create_server_with_server_error(self, mock_nova): """ Test that a server error before we return or begin waiting for the @@ -62,10 +62,10 @@ def test_create_server_with_server_error(self, mock_nova): mock_nova.servers.create.return_value = build_server mock_nova.servers.get.return_value = error_server self.assertRaises( - OpenStackCloudException, self.cloud.create_server, + exc.OpenStackCloudException, self.cloud.create_server, 'server-name', {'id': 'image-id'}, {'id': 'flavor-id'}) - @mock.patch.object(OpenStackCloud, 'nova_client') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_create_server_wait_server_error(self, mock_nova): """ Test that a server error while waiting for the server to spawn @@ -81,12 +81,12 @@ def test_create_server_wait_server_error(self, mock_nova): mock_nova.servers.list.side_effect = [[build_server], [error_server]] mock_nova.floating_ips.list.return_value = [fake_floating_ip] self.assertRaises( - OpenStackCloudException, + exc.OpenStackCloudException, self.cloud.create_server, 'server-name', dict(id='image-id'), dict(id='flavor-id'), wait=True) - @mock.patch.object(OpenStackCloud, 'nova_client') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_create_server_with_timeout(self, mock_nova): """ Test that a timeout while waiting for the server to spawn raises an @@ -97,13 +97,13 @@ def test_create_server_with_timeout(self, mock_nova): mock_nova.servers.get.return_value = fake_server mock_nova.servers.list.return_value = [fake_server] self.assertRaises( - OpenStackCloudTimeout, + exc.OpenStackCloudTimeout, self.cloud.create_server, 'server-name', dict(id='image-id'), dict(id='flavor-id'), wait=True, timeout=0.01) - @mock.patch.object(OpenStackCloud, 'nova_client') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_create_server_no_wait(self, mock_nova): """ Test that create_server with no wait and no exception in the @@ -124,7 +124,7 @@ def test_create_server_no_wait(self, mock_nova): image=dict(id='image=id'), flavor=dict(id='flavor-id'))) - @mock.patch.object(OpenStackCloud, 'nova_client') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_create_server_with_admin_pass_no_wait(self, mock_nova): """ Test that a server with an admin_pass passed returns the password @@ -145,8 +145,8 @@ def test_create_server_with_admin_pass_no_wait(self, mock_nova): name='server-name', image=dict(id='image=id'), flavor=dict(id='flavor-id'), admin_pass='ooBootheiX0edoh')) - @mock.patch.object(OpenStackCloud, "wait_for_server") - @mock.patch.object(OpenStackCloud, "nova_client") + @mock.patch.object(shade.OpenStackCloud, "wait_for_server") + @mock.patch.object(shade.OpenStackCloud, "nova_client") def test_create_server_with_admin_pass_wait(self, mock_nova, mock_wait): """ Test that a server with an admin_pass passed returns the password @@ -176,8 +176,8 @@ def test_create_server_with_admin_pass_wait(self, mock_nova, mock_wait): meta.obj_to_dict(fake_server_with_pass)) ) - @mock.patch.object(OpenStackCloud, "get_active_server") - @mock.patch.object(OpenStackCloud, "get_server") + @mock.patch.object(shade.OpenStackCloud, "get_active_server") + @mock.patch.object(shade.OpenStackCloud, "get_server") def test_wait_for_server(self, mock_get_server, mock_get_active_server): """ Test that waiting for a server returns the server instance when @@ -210,8 +210,8 @@ def test_wait_for_server(self, mock_get_server, mock_get_active_server): self.assertEqual('ACTIVE', server['status']) - @mock.patch.object(OpenStackCloud, 'wait_for_server') - @mock.patch.object(OpenStackCloud, 'nova_client') + @mock.patch.object(shade.OpenStackCloud, 'wait_for_server') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_create_server_wait(self, mock_nova, mock_wait): """ Test that create_server with a wait actually does the wait. @@ -229,8 +229,8 @@ def test_create_server_wait(self, mock_nova, mock_wait): nat_destination=None, ) - @mock.patch.object(OpenStackCloud, 'add_ips_to_server') - @mock.patch.object(OpenStackCloud, 'nova_client') + @mock.patch.object(shade.OpenStackCloud, 'add_ips_to_server') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') @mock.patch('time.sleep') def test_create_server_no_addresses( self, mock_sleep, mock_nova, mock_add_ips_to_server): @@ -252,7 +252,7 @@ def test_create_server_no_addresses( self.cloud._SERVER_AGE = 0 self.assertRaises( - OpenStackCloudException, self.cloud.create_server, + exc.OpenStackCloudException, self.cloud.create_server, 'server-name', {'id': 'image-id'}, {'id': 'flavor-id'}, wait=True) @@ -283,9 +283,9 @@ def test_create_server_network_with_empty_nics(self, network='network-name', nics=[]) mock_get_network.assert_called_once_with(name_or_id='network-name') - @mock.patch.object(OpenStackCloud, 'get_server_by_id') - @mock.patch.object(OpenStackCloud, 'get_image') - @mock.patch.object(OpenStackCloud, 'nova_client') + @mock.patch.object(shade.OpenStackCloud, 'get_server_by_id') + @mock.patch.object(shade.OpenStackCloud, 'get_image') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_create_server_get_flavor_image( self, mock_nova, mock_image, mock_get_server_by_id): diff --git a/shade/tests/unit/test_create_volume_snapshot.py b/shade/tests/unit/test_create_volume_snapshot.py index 016c8c1ba..e6fb267df 100644 --- a/shade/tests/unit/test_create_volume_snapshot.py +++ b/shade/tests/unit/test_create_volume_snapshot.py @@ -18,16 +18,16 @@ """ from mock import patch +import shade +from shade import exc from shade import meta -from shade import OpenStackCloud from shade.tests import fakes from shade.tests.unit import base -from shade.exc import (OpenStackCloudException, OpenStackCloudTimeout) class TestCreateVolumeSnapshot(base.TestCase): - @patch.object(OpenStackCloud, 'cinder_client') + @patch.object(shade.OpenStackCloud, 'cinder_client') def test_create_volume_snapshot_wait(self, mock_cinder): """ Test that create_volume_snapshot with a wait returns the volume @@ -56,7 +56,7 @@ def test_create_volume_snapshot_wait(self, mock_cinder): snapshot_id=meta.obj_to_dict(build_snapshot)['id'] ) - @patch.object(OpenStackCloud, 'cinder_client') + @patch.object(shade.OpenStackCloud, 'cinder_client') def test_create_volume_snapshot_with_timeout(self, mock_cinder): """ Test that a timeout while waiting for the volume snapshot to create @@ -70,7 +70,7 @@ def test_create_volume_snapshot_with_timeout(self, mock_cinder): mock_cinder.volume_snapshots.list.return_value = [build_snapshot] self.assertRaises( - OpenStackCloudTimeout, + exc.OpenStackCloudTimeout, self.cloud.create_volume_snapshot, volume_id='1234', wait=True, timeout=0.01) @@ -81,7 +81,7 @@ def test_create_volume_snapshot_with_timeout(self, mock_cinder): snapshot_id=meta.obj_to_dict(build_snapshot)['id'] ) - @patch.object(OpenStackCloud, 'cinder_client') + @patch.object(shade.OpenStackCloud, 'cinder_client') def test_create_volume_snapshot_with_error(self, mock_cinder): """ Test that a error status while waiting for the volume snapshot to @@ -97,7 +97,7 @@ def test_create_volume_snapshot_with_error(self, mock_cinder): mock_cinder.volume_snapshots.list.return_value = [error_snapshot] self.assertRaises( - OpenStackCloudException, + exc.OpenStackCloudException, self.cloud.create_volume_snapshot, volume_id='1234', wait=True, timeout=5) diff --git a/shade/tests/unit/test_delete_volume_snapshot.py b/shade/tests/unit/test_delete_volume_snapshot.py index 816051c1f..a9a663a28 100644 --- a/shade/tests/unit/test_delete_volume_snapshot.py +++ b/shade/tests/unit/test_delete_volume_snapshot.py @@ -18,15 +18,15 @@ """ from mock import patch -from shade import OpenStackCloud +import shade +from shade import exc from shade.tests import fakes from shade.tests.unit import base -from shade.exc import (OpenStackCloudException, OpenStackCloudTimeout) class TestDeleteVolumeSnapshot(base.TestCase): - @patch.object(OpenStackCloud, 'cinder_client') + @patch.object(shade.OpenStackCloud, 'cinder_client') def test_delete_volume_snapshot(self, mock_cinder): """ Test that delete_volume_snapshot without a wait returns True instance @@ -45,7 +45,7 @@ def test_delete_volume_snapshot(self, mock_cinder): mock_cinder.volume_snapshots.list.assert_called_with(detailed=True, search_opts=None) - @patch.object(OpenStackCloud, 'cinder_client') + @patch.object(shade.OpenStackCloud, 'cinder_client') def test_delete_volume_snapshot_with_error(self, mock_cinder): """ Test that a exception while deleting a volume snapshot will cause an @@ -59,13 +59,13 @@ def test_delete_volume_snapshot_with_error(self, mock_cinder): mock_cinder.volume_snapshots.list.return_value = [fake_snapshot] self.assertRaises( - OpenStackCloudException, + exc.OpenStackCloudException, self.cloud.delete_volume_snapshot, name_or_id='1234') mock_cinder.volume_snapshots.delete.assert_called_with( snapshot='1234') - @patch.object(OpenStackCloud, 'cinder_client') + @patch.object(shade.OpenStackCloud, 'cinder_client') def test_delete_volume_snapshot_with_timeout(self, mock_cinder): """ Test that a timeout while waiting for the volume snapshot to delete @@ -77,7 +77,7 @@ def test_delete_volume_snapshot_with_timeout(self, mock_cinder): mock_cinder.volume_snapshots.list.return_value = [fake_snapshot] self.assertRaises( - OpenStackCloudTimeout, + exc.OpenStackCloudTimeout, self.cloud.delete_volume_snapshot, name_or_id='1234', wait=True, timeout=0.01) diff --git a/shade/tests/unit/test_rebuild_server.py b/shade/tests/unit/test_rebuild_server.py index dc28a73ad..fbd22ec86 100644 --- a/shade/tests/unit/test_rebuild_server.py +++ b/shade/tests/unit/test_rebuild_server.py @@ -21,9 +21,9 @@ import mock +from shade import exc from shade import meta from shade import OpenStackCloud -from shade.exc import (OpenStackCloudException, OpenStackCloudTimeout) from shade.tests import fakes from shade.tests.unit import base @@ -38,7 +38,7 @@ def test_rebuild_server_rebuild_exception(self, mock_nova): """ mock_nova.servers.rebuild.side_effect = Exception("exception") self.assertRaises( - OpenStackCloudException, self.cloud.rebuild_server, "a", "b") + exc.OpenStackCloudException, self.cloud.rebuild_server, "a", "b") @mock.patch.object(OpenStackCloud, 'nova_client') def test_rebuild_server_server_error(self, mock_nova): @@ -55,7 +55,7 @@ def test_rebuild_server_server_error(self, mock_nova): mock_nova.servers.list.return_value = [error_server] mock_nova.floating_ips.list.return_value = [fake_floating_ip] self.assertRaises( - OpenStackCloudException, + exc.OpenStackCloudException, self.cloud.rebuild_server, "1234", "b", wait=True) @mock.patch.object(OpenStackCloud, 'nova_client') @@ -68,7 +68,7 @@ def test_rebuild_server_timeout(self, mock_nova): mock_nova.servers.rebuild.return_value = rebuild_server mock_nova.servers.list.return_value = [rebuild_server] self.assertRaises( - OpenStackCloudTimeout, + exc.OpenStackCloudTimeout, self.cloud.rebuild_server, "a", "b", wait=True, timeout=0.001) @mock.patch.object(OpenStackCloud, 'nova_client') diff --git a/shade/tests/unit/test_role_assignment.py b/shade/tests/unit/test_role_assignment.py index 4aa1eceff..14acda7c3 100644 --- a/shade/tests/unit/test_role_assignment.py +++ b/shade/tests/unit/test_role_assignment.py @@ -14,7 +14,7 @@ from mock import patch import os_client_config as occ from shade import OperatorCloud -from shade.exc import OpenStackCloudException, OpenStackCloudTimeout +from shade import exc from shade.meta import obj_to_dict from shade.tests import fakes from shade.tests.unit import base @@ -588,7 +588,7 @@ def test_grant_no_role(self, mock_keystone, mock_api_version): mock_keystone.roles.list.return_value = [] with testtools.ExpectedException( - OpenStackCloudException, + exc.OpenStackCloudException, 'Role {0} not found'.format(self.fake_role['name']) ): self.op_cloud.grant_role( @@ -602,7 +602,7 @@ def test_revoke_no_role(self, mock_keystone, mock_api_version): mock_api_version.return_value = '3' mock_keystone.roles.list.return_value = [] with testtools.ExpectedException( - OpenStackCloudException, + exc.OpenStackCloudException, 'Role {0} not found'.format(self.fake_role['name']) ): self.op_cloud.revoke_role( @@ -618,7 +618,7 @@ def test_grant_no_user_or_group_specified(self, mock_api_version.return_value = '3' mock_keystone.roles.list.return_value = [self.fake_role] with testtools.ExpectedException( - OpenStackCloudException, + exc.OpenStackCloudException, 'Must specify either a user or a group' ): self.op_cloud.grant_role(self.fake_role['name']) @@ -631,7 +631,7 @@ def test_revoke_no_user_or_group_specified(self, mock_api_version.return_value = '3' mock_keystone.roles.list.return_value = [self.fake_role] with testtools.ExpectedException( - OpenStackCloudException, + exc.OpenStackCloudException, 'Must specify either a user or a group' ): self.op_cloud.revoke_role(self.fake_role['name']) @@ -643,7 +643,7 @@ def test_grant_no_user_or_group(self, mock_keystone, mock_api_version): mock_keystone.roles.list.return_value = [self.fake_role] mock_keystone.users.list.return_value = [] with testtools.ExpectedException( - OpenStackCloudException, + exc.OpenStackCloudException, 'Must specify either a user or a group' ): self.op_cloud.grant_role( @@ -657,7 +657,7 @@ def test_revoke_no_user_or_group(self, mock_keystone, mock_api_version): mock_keystone.roles.list.return_value = [self.fake_role] mock_keystone.users.list.return_value = [] with testtools.ExpectedException( - OpenStackCloudException, + exc.OpenStackCloudException, 'Must specify either a user or a group' ): self.op_cloud.revoke_role( @@ -672,7 +672,7 @@ def test_grant_both_user_and_group(self, mock_keystone, mock_api_version): mock_keystone.users.list.return_value = [self.fake_user] mock_keystone.groups.list.return_value = [self.fake_group] with testtools.ExpectedException( - OpenStackCloudException, + exc.OpenStackCloudException, 'Specify either a group or a user, not both' ): self.op_cloud.grant_role( @@ -688,7 +688,7 @@ def test_revoke_both_user_and_group(self, mock_keystone, mock_api_version): mock_keystone.users.list.return_value = [self.fake_user] mock_keystone.groups.list.return_value = [self.fake_group] with testtools.ExpectedException( - OpenStackCloudException, + exc.OpenStackCloudException, 'Specify either a group or a user, not both' ): self.op_cloud.revoke_role( @@ -748,7 +748,7 @@ def test_grant_no_project_or_domain(self, mock_keystone, mock_api_version): mock_keystone.projects.list.return_value = [] mock_keystone.domains.get.return_value = None with testtools.ExpectedException( - OpenStackCloudException, + exc.OpenStackCloudException, 'Must specify either a domain or project' ): self.op_cloud.grant_role( @@ -768,7 +768,7 @@ def test_revoke_no_project_or_domain(self, mock_keystone.role_assignments.list.return_value = \ [self.user_project_assignment] with testtools.ExpectedException( - OpenStackCloudException, + exc.OpenStackCloudException, 'Must specify either a domain or project' ): self.op_cloud.revoke_role( @@ -784,7 +784,7 @@ def test_grant_bad_domain_exception(self, mock_keystone.roles.list.return_value = [self.fake_role] mock_keystone.domains.get.side_effect = Exception('test') with testtools.ExpectedException( - OpenStackCloudException, + exc.OpenStackCloudException, 'Failed to get domain baddomain \(Inner Exception: test\)' ): self.op_cloud.grant_role( @@ -801,7 +801,7 @@ def test_revoke_bad_domain_exception(self, mock_keystone.roles.list.return_value = [self.fake_role] mock_keystone.domains.get.side_effect = Exception('test') with testtools.ExpectedException( - OpenStackCloudException, + exc.OpenStackCloudException, 'Failed to get domain baddomain \(Inner Exception: test\)' ): self.op_cloud.revoke_role( @@ -841,7 +841,7 @@ def test_grant_role_user_project_v2_wait_exception(self, mock_keystone.roles.add_user_role.return_value = self.fake_role with testtools.ExpectedException( - OpenStackCloudTimeout, + exc.OpenStackCloudTimeout, 'Timeout waiting for role to be granted' ): self.assertTrue(self.op_cloud.grant_role( @@ -879,7 +879,7 @@ def test_revoke_role_user_project_v2_wait_exception(self, mock_keystone.roles.roles_for_user.return_value = [self.fake_role] mock_keystone.roles.remove_user_role.return_value = self.fake_role with testtools.ExpectedException( - OpenStackCloudTimeout, + exc.OpenStackCloudTimeout, 'Timeout waiting for role to be revoked' ): self.assertTrue(self.op_cloud.revoke_role( diff --git a/shade/tests/unit/test_zone.py b/shade/tests/unit/test_zone.py index a5a8e161d..2049fcbf9 100644 --- a/shade/tests/unit/test_zone.py +++ b/shade/tests/unit/test_zone.py @@ -15,8 +15,8 @@ import testtools import shade -from shade.tests.unit import base from shade.tests import fakes +from shade.tests.unit import base zone_obj = fakes.FakeZone( diff --git a/tox.ini b/tox.ini index a6ca42e66..acb66325f 100644 --- a/tox.ini +++ b/tox.ini @@ -67,9 +67,13 @@ commands = python setup.py build_sphinx commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [flake8] -# The string of H ignores is because there are some useful checks -# related to python3 compat. -ignore = H3,H4,H103 +# The following are ignored on purpose - please do not submit patches to "fix" +# without first verifying with a core that fixing them is non-disruptive. +# H103 Is about the Apache license. It's strangely strict about the use of +# single vs double quotes in the license text. Fixing is not worth it +# H306 Is about alphabetical imports - there's a lot to fix +# H4 Are about docstrings - and there's just too many of them to fix +ignore = H103,H306,H4 show-source = True builtins = _ exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build From bb4eededb059efc42b1b0b251f21305f00bbee26 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 29 Mar 2017 05:13:18 -0500 Subject: [PATCH 1396/3836] Update tox build settings Remove heatclient and magnumclient from install-tips We do not consume them anymore, so do not need to install them as part of our tips testing. Install from pip using upper-constraints. To ensure we work with OpenStack releases. Change-Id: I98f39d2e2a591eb84a1ebcd033eb51e1758f1f39 --- extras/install-tips.sh | 4 +--- tox.ini | 19 ++----------------- 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/extras/install-tips.sh b/extras/install-tips.sh index 66b7a0af1..562270189 100644 --- a/extras/install-tips.sh +++ b/extras/install-tips.sh @@ -16,15 +16,13 @@ for lib in \ os-client-config \ - keystoneauth \ python-novaclient \ python-keystoneclient \ python-cinderclient \ python-neutronclient \ python-ironicclient \ - python-heatclient \ python-designateclient \ - python-magnumclient + keystoneauth do egg=$(echo $lib | tr '-' '_' | sed 's/python-//') if [ -d /opt/stack/new/$lib ] ; then diff --git a/tox.ini b/tox.ini index 3a82b0c17..2c43c7776 100644 --- a/tox.ini +++ b/tox.ini @@ -5,30 +5,15 @@ skipsdist = True [testenv] usedevelop = True -install_command = pip install -U {opts} {packages} +install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} setenv = VIRTUAL_ENV={envdir} LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=C -deps = -r{toxinidir}/requirements.txt - -r{toxinidir}/test-requirements.txt +deps = -r{toxinidir}/test-requirements.txt commands = python setup.py testr --slowest --testr-args='{posargs}' -[testenv:record] -usedevelop = True -install_command = pip install -U {opts} {packages} -setenv = - VIRTUAL_ENV={envdir} - LANG=en_US.UTF-8 - LANGUAGE=en_US:en - LC_ALL=C - BETAMAX_RECORD_FIXTURES=1 -passenv = SHADE_OS_CLOUD -deps = -r{toxinidir}/requirements.txt - -r{toxinidir}/test-requirements.txt -commands = python setup.py testr --slowest --testr-args='--concurrency=1 {posargs}' - [testenv:functional] setenv = OS_TEST_PATH = ./shade/tests/functional From b31e9aa777a028e34814499d6821ac1c1bfbb881 Mon Sep 17 00:00:00 2001 From: Jim Rollenhagen Date: Wed, 29 Mar 2017 16:54:30 -0400 Subject: [PATCH 1397/3836] Docs: add a note about rackspace API keys Some users are forced to use these instead of passwords (whether because they use 2-factor auth or by policy). Document it so they know how. Change-Id: I558c2e8d3e8b0fad0a96a361232f14443e82a35f --- doc/source/vendor-support.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst index 4400e5e46..449fc5af8 100644 --- a/doc/source/vendor-support.rst +++ b/doc/source/vendor-support.rst @@ -254,6 +254,14 @@ SYD Sydney, NSW :vm_mode: hvm :xenapi_use_agent: False * Volume API Version is 1 +* While passwords are recommended for use, API keys do work as well. + The `rackspaceauth` python package must be installed, and then the following + can be added to clouds.yaml:: + + auth: + username: myusername + api_key: myapikey + auth_type: rackspace_apikey switchengines ------------- From c1984b636ee9fc6101c28bb57723d2d490b04586 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 29 Mar 2017 03:53:14 -0500 Subject: [PATCH 1398/3836] Add bare parameter to get/list/search server The bare parameter tells shade to skip all additional calls it makes to fill in extra information on the server record from other services, such as fixing the addresses dict. This is useful internally in shade where there are times when one is getting the server dict just to get its id so other operations, such as getting the console log or attaching security groups, can be performed. Change-Id: Ic9cc6e8be4f4e30f76d3abb655d9a3cf0cb9918b --- shade/openstackcloud.py | 59 +++++++++++++++++-------- shade/tests/unit/test_server_console.py | 10 +---- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index a27c996e7..d1d6b2708 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1566,9 +1566,9 @@ def search_security_groups(self, name_or_id=None, filters=None): def search_servers( self, name_or_id=None, filters=None, detailed=False, - all_projects=False): + all_projects=False, bare=False): servers = self.list_servers( - detailed=detailed, all_projects=all_projects) + detailed=detailed, all_projects=all_projects, bare=bare) return _utils._filter_list(servers, name_or_id, filters) def search_server_groups(self, name_or_id=None, filters=None): @@ -1845,9 +1845,18 @@ def list_security_groups(self, filters=None): _tasks.NovaSecurityGroupList(search_opts=filters)) return self._normalize_secgroups(groups) - def list_servers(self, detailed=False, all_projects=False): + def list_servers(self, detailed=False, all_projects=False, bare=False): """List all available servers. + :param detailed: Whether or not to add detailed additional information. + Defaults to False. + :param all_projects: Whether to list servers from all projects or just + the current auth scoped project. + :param bare: Whether to skip adding any additional information to the + server record. Defaults to False, meaning the addresses + dict will be populated as needed from neutron. Setting + to True implies detailed = False. + :returns: A list of server ``munch.Munch``. """ @@ -1865,13 +1874,14 @@ def list_servers(self, detailed=False, all_projects=False): if not (first_run and self._servers is not None): self._servers = self._list_servers( detailed=detailed, - all_projects=all_projects) + all_projects=all_projects, + bare=bare) self._servers_time = time.time() finally: self._servers_lock.release() return self._servers - def _list_servers(self, detailed=False, all_projects=False): + def _list_servers(self, detailed=False, all_projects=False, bare=False): with _utils.shade_exceptions( "Error fetching server list on {cloud}:{region}:".format( cloud=self.name, @@ -1882,7 +1892,9 @@ def _list_servers(self, detailed=False, all_projects=False): servers = self._normalize_servers( self.manager.submit_task(_tasks.ServerList(**kwargs))) - if detailed: + if bare: + return servers + elif detailed: return [ meta.get_hostvars_from_server(self, server) for server in servers @@ -2611,7 +2623,7 @@ def get_server_console(self, server, length=None): """ if not isinstance(server, dict): - server = self.get_server(server) + server = self.get_server(server, bare=True) if not server: raise OpenStackCloudException( @@ -2624,7 +2636,8 @@ def get_server_console(self, server, length=None): except OpenStackCloudBadRequest: return "" - def get_server(self, name_or_id=None, filters=None, detailed=False): + def get_server( + self, name_or_id=None, filters=None, detailed=False, bare=False): """Get a server by name or ID. :param name_or_id: Name or ID of the server. @@ -2642,13 +2655,19 @@ def get_server(self, name_or_id=None, filters=None, detailed=False): OR A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :param detailed: Whether or not to add detailed additional information. + Defaults to False. + :param bare: Whether to skip adding any additional information to the + server record. Defaults to False, meaning the addresses + dict will be populated as needed from neutron. Setting + to True implies detailed = False. :returns: A server ``munch.Munch`` or None if no matching server is found. """ searchfunc = functools.partial(self.search_servers, - detailed=detailed) + detailed=detailed, bare=bare) return _utils._get_entity(searchfunc, name_or_id, filters) def get_server_by_id(self, id): @@ -3198,7 +3217,7 @@ def create_image_snapshot( :raises: OpenStackCloudException if there are problems uploading """ if not isinstance(server, dict): - server_obj = self.get_server(server) + server_obj = self.get_server(server, bare=True) if not server_obj: raise OpenStackCloudException( "Server {server} could not be found and therefore" @@ -4231,7 +4250,7 @@ def delete_volume_snapshot(self, name_or_id=None, wait=False, return True def get_server_id(self, name_or_id): - server = self.get_server(name_or_id) + server = self.get_server(name_or_id, bare=True) if server: return server['id'] return None @@ -5533,8 +5552,9 @@ def set_server_metadata(self, name_or_id, metadata): """ try: self.manager.submit_task( - _tasks.ServerSetMetadata(server=self.get_server(name_or_id), - metadata=metadata)) + _tasks.ServerSetMetadata( + server=self.get_server(name_or_id, bare=True), + metadata=metadata)) except OpenStackCloudException: raise except Exception as e: @@ -5553,8 +5573,9 @@ def delete_server_metadata(self, name_or_id, metadata_keys): """ try: self.manager.submit_task( - _tasks.ServerDeleteMetadata(server=self.get_server(name_or_id), - keys=metadata_keys)) + _tasks.ServerDeleteMetadata( + server=self.get_server(name_or_id, bare=True), + keys=metadata_keys)) except OpenStackCloudException: raise except Exception as e: @@ -5579,7 +5600,8 @@ def delete_server( :raises: OpenStackCloudException on operation error. """ - server = self.get_server(name_or_id) + # If delete_ips is True, we need the server to not be bare. + server = self.get_server(name_or_id, bare=not delete_ips) if not server: return False @@ -5652,7 +5674,7 @@ def _delete_server( # to be friendly with the server. wait=self._SERVER_AGE or 2): with _utils.shade_exceptions("Error in deleting server"): - server = self.get_server(server['id']) + server = self.get_server(server['id'], bare=True) if not server: break @@ -5677,13 +5699,14 @@ def update_server(self, name_or_id, **kwargs): :raises: OpenStackCloudException on operation error. """ - server = self.get_server(name_or_id=name_or_id) + server = self.get_server(name_or_id=name_or_id, bare=True) if server is None: raise OpenStackCloudException( "failed to find server '{server}'".format(server=name_or_id)) with _utils.shade_exceptions( "Error updating server {0}".format(name_or_id)): + # TODO(mordred) This is not sending back a normalized server return self.manager.submit_task( _tasks.ServerUpdate( server=server['id'], **kwargs)) diff --git a/shade/tests/unit/test_server_console.py b/shade/tests/unit/test_server_console.py index ebd934a90..bf534aa2c 100644 --- a/shade/tests/unit/test_server_console.py +++ b/shade/tests/unit/test_server_console.py @@ -11,10 +11,8 @@ # under the License. -import mock import uuid -import shade from shade.tests.unit import base from shade.tests import fakes @@ -45,13 +43,7 @@ def test_get_server_console_dict(self): self.output, self.cloud.get_server_console(self.server)) self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'has_service') - def test_get_server_console_name_or_id(self, mock_has_service): - # Turn off neutron for now - we don't _actually_ want to show all - # of the nova normalization calls. - # TODO(mordred) Tell get_server_console to tell shade to skip - # adding normalization, since we don't consume them - mock_has_service.return_value = False + def test_get_server_console_name_or_id(self): self.register_uris([ dict(method='GET', From 7822e33622da67143cc79ebd88ab9d397b87dc90 Mon Sep 17 00:00:00 2001 From: Thomas Bechtold Date: Thu, 30 Mar 2017 08:16:56 +0200 Subject: [PATCH 1399/3836] Fix doc build if git is absent When building packages if git is absent, do not try to get the git sha via the git cli. That can not work. Change-Id: I3bde4df045287cbda07c9447ef3397de74f42910 Closes-Bug: #1677463 --- doc/source/conf.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index c12c69e10..8dee8f737 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -13,6 +13,7 @@ import os import sys +import warnings import openstackdocstheme @@ -67,7 +68,12 @@ # These variables are passed to the logabug code via html_context. giturl = u'http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/doc/source' git_cmd = "/usr/bin/git log | head -n1 | cut -f2 -d' '" -gitsha = os.popen(git_cmd).read().strip('\n') +try: + gitsha = os.popen(git_cmd).read().strip('\n') +except Exception: + warnings.warn("Can not get git sha.") + gitsha = "unknown" + bug_tag = "docs" # source tree pwd = os.getcwd() From 7e92e93104cfef6a2e6271d8d81fb4f2961f0c50 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 30 Mar 2017 14:03:25 +0000 Subject: [PATCH 1400/3836] Updated from global requirements Change-Id: I87c2f41a041ebf80a97f1f4b6100800b68f4f137 --- requirements.txt | 41 ++++++++++++++++++++++------------------- setup.py | 12 ++++++++++-- test-requirements.txt | 27 +++++++++++++++------------ 3 files changed, 47 insertions(+), 33 deletions(-) mode change 100755 => 100644 setup.py diff --git a/requirements.txt b/requirements.txt index fe1070350..ae41fbe02 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,22 +1,25 @@ -pbr>=2.0.0 # Apache-2.0 +# 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.0.0 # Apache-2.0 -munch -decorator -jmespath -jsonpatch -ipaddress -os-client-config>=1.25.0 -requestsexceptions>=1.1.1 -six -futures;python_version<'3.2' +munch>=2.1.0 # MIT +decorator>=3.4.0 # BSD +jmespath>=0.9.0 # MIT +jsonpatch>=1.1 # BSD +ipaddress>=1.0.7;python_version<'3.3' # PSF +os-client-config>=1.22.0 # Apache-2.0 +requestsexceptions>=1.2.0 # Apache-2.0 +six>=1.9.0 # MIT +futures>=3.0;python_version=='2.7' or python_version=='2.6' # BSD -keystoneauth1>=2.11.0 -netifaces>=0.10.4 -python-novaclient>=2.21.0,!=2.27.0,!=2.32.0 -python-keystoneclient>=0.11.0 -python-cinderclient>=1.3.1 -python-neutronclient>=2.3.10 -python-ironicclient>=0.10.0 -python-designateclient>=2.1.0 +keystoneauth1>=2.18.0 # Apache-2.0 +netifaces>=0.10.4 # MIT +python-novaclient>=7.1.0 # Apache-2.0 +python-keystoneclient>=3.8.0 # Apache-2.0 +python-cinderclient>=2.0.1 # Apache-2.0 +python-neutronclient>=5.1.0 # Apache-2.0 +python-ironicclient>=1.11.0 # Apache-2.0 +python-designateclient>=1.5.0 # Apache-2.0 -dogpile.cache>=0.5.3 +dogpile.cache>=0.6.2 # BSD diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 index c0a24eab2..566d84432 --- a/setup.py +++ b/setup.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +13,17 @@ # 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'], + setup_requires=['pbr>=2.0.0'], pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt index b3fea41a3..d1c79ca35 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,13 +1,16 @@ -hacking>=0.11.0,<0.12 # Apache-2.0 +# 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.12,>=0.11.0 # Apache-2.0 -coverage>=3.6 -fixtures>=0.3.14 -mock>=1.0 -python-subunit -oslosphinx>=2.2.0 # Apache-2.0 -requests-mock -sphinx>=1.5.0 -testrepository>=0.0.17 -testscenarios>=0.4,<0.5 -testtools>=0.9.32 -reno +coverage>=4.0 # Apache-2.0 +fixtures>=3.0.0 # Apache-2.0/BSD +mock>=2.0 # BSD +python-subunit>=0.0.18 # Apache-2.0/BSD +oslosphinx>=4.7.0 # Apache-2.0 +requests-mock>=1.1 # Apache-2.0 +sphinx>=1.5.1 # BSD +testrepository>=0.0.18 # Apache-2.0/BSD +testscenarios>=0.4 # Apache-2.0/BSD +testtools>=1.4.0 # MIT +reno>=1.8.0 # Apache-2.0 From 88d8a37cfae9b23acfeda76b354880e68de8c96f Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Wed, 29 Mar 2017 11:11:59 +1100 Subject: [PATCH 1401/3836] Add server security groups to shade We need a way to add and remove security groups to a server in shade. This will let us modify them from ansible. Change-Id: I5602e44720485e20491cf4930605362312274b20 --- ...rver-security-groups-840ab28c04f359de.yaml | 4 + shade/openstackcloud.py | 91 +++++++++ shade/tests/unit/test_security_groups.py | 181 ++++++++++++++++++ 3 files changed, 276 insertions(+) create mode 100644 releasenotes/notes/server-security-groups-840ab28c04f359de.yaml diff --git a/releasenotes/notes/server-security-groups-840ab28c04f359de.yaml b/releasenotes/notes/server-security-groups-840ab28c04f359de.yaml new file mode 100644 index 000000000..d9de793e9 --- /dev/null +++ b/releasenotes/notes/server-security-groups-840ab28c04f359de.yaml @@ -0,0 +1,4 @@ +--- +features: + - Add the `add_server_security_groups` and `remove_server_security_groups` + functions to add and remove security groups from a specific server. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index ed4f2f8ba..ec2dc085f 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1812,6 +1812,97 @@ def list_server_security_groups(self, server): return self._normalize_secgroups(groups) + def _get_server_security_groups(self, server, security_groups): + if not self._has_secgroups(): + raise OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + + if not isinstance(server, dict): + server = self.get_server(server, bare=True) + + if server is None: + self.log.debug('Server %s not found', server) + return None, None + + if not isinstance(security_groups, (list, tuple)): + security_groups = [security_groups] + + sec_group_objs = [] + + for sg in security_groups: + if not isinstance(sg, dict): + sg = self.get_security_group(sg) + + if sg is None: + self.log.debug('Security group %s not found for adding', + sg) + + return None, None + + sec_group_objs.append(sg) + + return server, sec_group_objs + + def add_server_security_groups(self, server, security_groups): + """Add security groups to a server. + + Add existing security groups to an existing server. If the security + groups are already present on the server this will continue unaffected. + + :returns: False if server or security groups are undefined, True + otherwise. + + :raises: ``OpenStackCloudException``, on operation error. + """ + server, security_groups = self._get_server_security_groups( + server, security_groups) + + if not (server and security_groups): + return False + + for sg in security_groups: + self._compute_client.post( + '/servers/%s/action' % server.id, + json={'addSecurityGroup': {'name': sg.name}}) + + return True + + def remove_server_security_groups(self, server, security_groups): + """Remove security groups from a server + + Remove existing security groups from an existing server. If the + security groups are not present on the server this will continue + unaffected. + + :returns: False if server or security groups are undefined, True + otherwise. + + :raises: ``OpenStackCloudException``, on operation error. + """ + server, security_groups = self._get_server_security_groups( + server, security_groups) + + if not (server and security_groups): + return False + + for sg in security_groups: + try: + self._compute_client.post( + '/servers/%s/action' % server.id, + json={'removeSecurityGroup': {'name': sg.name}}) + + except OpenStackCloudURINotFound as e: + # NOTE(jamielennox): Is this ok? If we remove something that + # isn't present should we just conclude job done or is that an + # error? Nova returns ok if you try to add a group twice. + self.log.debug( + "The security group %s was not present on server %s so " + "no action was performed", sg.name, server.name) + + return True + + def list_security_groups(self, filters=None): """List all available security groups. diff --git a/shade/tests/unit/test_security_groups.py b/shade/tests/unit/test_security_groups.py index 026096ee7..5ce6765d0 100644 --- a/shade/tests/unit/test_security_groups.py +++ b/shade/tests/unit/test_security_groups.py @@ -372,3 +372,184 @@ def test_list_server_security_groups_bad_source(self, mock_nova): ret = self.cloud.list_server_security_groups(server) self.assertEqual([], ret) self.assertFalse(mock_nova.servers.list_security_group.called) + + +class TestServerSecurityGroups(base.RequestsMockTestCase): + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_add_security_group_to_server_nova(self, mock_nova): + # fake to get server by name, server-name must match + fake_server = fakes.FakeServer('1234', 'server-name', 'ACTIVE') + mock_nova.servers.list.return_value = [fake_server] + + # use nova for secgroup list and return an existing fake + self.has_neutron = False + self.cloud.secgroup_source = 'nova' + mock_nova.security_groups.list.return_value = [nova_grp_obj] + + self.register_uris([ + dict( + method='POST', + uri='%s/servers/%s/action' % (fakes.COMPUTE_ENDPOINT, '1234'), + validate={'addSecurityGroup': {'name': 'nova-sec-group'}}, + status_code=202, + ), + ]) + + ret = self.cloud.add_server_security_groups('server-name', + 'nova-sec-group') + self.assertTrue(ret) + + self.assertTrue(mock_nova.servers.list.called_once) + self.assertTrue(mock_nova.security_groups.list.called_once) + + self.assert_calls() + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_add_security_group_to_server_neutron(self, + mock_neutron, + mock_nova): + # fake to get server by name, server-name must match + fake_server = fakes.FakeServer('1234', 'server-name', 'ACTIVE') + mock_nova.servers.list.return_value = [fake_server] + + # use neutron for secgroup list and return an existing fake + self.cloud.secgroup_source = 'neutron' + neutron_return = dict(security_groups=[neutron_grp_dict]) + mock_neutron.list_security_groups.return_value = neutron_return + + self.register_uris([ + dict( + method='POST', + uri='%s/servers/%s/action' % (fakes.COMPUTE_ENDPOINT, '1234'), + validate={'addSecurityGroup': {'name': 'neutron-sec-group'}}, + status_code=202, + ), + ]) + + ret = self.cloud.add_server_security_groups('server-name', + 'neutron-sec-group') + self.assertTrue(ret) + + self.assertTrue(mock_nova.servers.list.called_once) + self.assertTrue(mock_neutron.list_securit_groups.called_once) + + self.assert_calls() + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_remove_security_group_from_server_nova(self, mock_nova): + # fake to get server by name, server-name must match + fake_server = fakes.FakeServer('1234', 'server-name', 'ACTIVE') + mock_nova.servers.list.return_value = [fake_server] + + # use nova for secgroup list and return an existing fake + self.has_neutron = False + self.cloud.secgroup_source = 'nova' + mock_nova.security_groups.list.return_value = [nova_grp_obj] + + self.register_uris([ + dict( + method='POST', + uri='%s/servers/%s/action' % (fakes.COMPUTE_ENDPOINT, '1234'), + validate={'removeSecurityGroup': {'name': 'nova-sec-group'}}, + ), + ]) + + ret = self.cloud.remove_server_security_groups('server-name', + 'nova-sec-group') + self.assertTrue(ret) + + self.assertTrue(mock_nova.servers.list.called_once) + self.assertTrue(mock_nova.security_groups.list.called_once) + + self.assert_calls() + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_remove_security_group_from_server_neutron(self, + mock_neutron, + mock_nova): + # fake to get server by name, server-name must match + fake_server = fakes.FakeServer('1234', 'server-name', 'ACTIVE') + mock_nova.servers.list.return_value = [fake_server] + + # use neutron for secgroup list and return an existing fake + self.cloud.secgroup_source = 'neutron' + neutron_return = dict(security_groups=[neutron_grp_dict]) + mock_neutron.list_security_groups.return_value = neutron_return + + validate = {'removeSecurityGroup': {'name': 'neutron-sec-group'}} + self.register_uris([ + dict( + method='POST', + uri='%s/servers/%s/action' % (fakes.COMPUTE_ENDPOINT, '1234'), + validate=validate, + ), + ]) + + ret = self.cloud.remove_server_security_groups('server-name', + 'neutron-sec-group') + self.assertTrue(ret) + + self.assertTrue(mock_nova.servers.list.called_once) + self.assertTrue(mock_neutron.list_security_groups.called_once) + + self.assert_calls() + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_add_bad_security_group_to_server_nova(self, mock_nova): + # fake to get server by name, server-name must match + fake_server = fakes.FakeServer('1234', 'server-name', 'ACTIVE') + mock_nova.servers.list.return_value = [fake_server] + + # use nova for secgroup list and return an existing fake + self.has_neutron = False + self.cloud.secgroup_source = 'nova' + mock_nova.security_groups.list.return_value = [nova_grp_obj] + + ret = self.cloud.add_server_security_groups('server-name', + 'unknown-sec-group') + self.assertFalse(ret) + + self.assertTrue(mock_nova.servers.list.called_once) + self.assertTrue(mock_nova.security_groups.list.called_once) + + self.assert_calls() + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_add_bad_security_group_to_server_neutron(self, + mock_neutron, + mock_nova): + # fake to get server by name, server-name must match + fake_server = fakes.FakeServer('1234', 'server-name', 'ACTIVE') + mock_nova.servers.list.return_value = [fake_server] + + # use neutron for secgroup list and return an existing fake + self.cloud.secgroup_source = 'neutron' + neutron_return = dict(security_groups=[neutron_grp_dict]) + mock_neutron.list_security_groups.return_value = neutron_return + + ret = self.cloud.add_server_security_groups('server-name', + 'unknown-sec-group') + self.assertFalse(ret) + + self.assertTrue(mock_nova.servers.list.called_once) + self.assertTrue(mock_neutron.list_security_groups.called_once) + + self.assert_calls() + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_add_security_group_to_bad_server(self, mock_nova): + # fake to get server by name, server-name must match + fake_server = fakes.FakeServer('1234', 'server-name', 'ACTIVE') + mock_nova.servers.list.return_value = [fake_server] + + ret = self.cloud.add_server_security_groups('unknown-server-name', + 'nova-sec-group') + self.assertFalse(ret) + + self.assertTrue(mock_nova.servers.list.called_once) + + self.assert_calls() From 97b9ac6beb730b00201351bad4efed32cf0fc605 Mon Sep 17 00:00:00 2001 From: Sorin Sbarnea Date: Fri, 31 Mar 2017 17:56:43 +0100 Subject: [PATCH 1402/3836] Fixed stack_status.split() exception Change-Id: Idc76dde81ee71a0a81dfb9db33627c15e878268f Signed-off-by: Sorin Sbarnea --- shade/_normalize.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/_normalize.py b/shade/_normalize.py index 7e7f12c3e..c94fcca1b 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -959,7 +959,7 @@ def _normalize_stack(self, stack): stack.pop('identifier', None) stack_status = stack.pop('stack_status') - (action, status) = stack_status.split('_') + (action, status) = stack_status.split('_', 1) ret = munch.Munch( id=stack.pop('id'), From cf54ef6b9203d9afcb9c9e50ab19bad73094015d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 31 Mar 2017 11:58:00 -0500 Subject: [PATCH 1403/3836] Add test to validate multi _ heat stack_status According to the Heat API docs, there is a status of IN_PROGRESS which can cause the old code to fail. Add a test that sets that stack_status so that we ensure we don't regress on splitting the stack_status. Change-Id: Ibb1143ffd47465d1640f17bf4319425e6db1f4d1 --- shade/tests/unit/test_stack.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/shade/tests/unit/test_stack.py b/shade/tests/unit/test_stack.py index c32022992..52cbde33d 100644 --- a/shade/tests/unit/test_stack.py +++ b/shade/tests/unit/test_stack.py @@ -523,3 +523,33 @@ def test_get_stack(self): self.assertEqual('COMPLETE', res['status']) self.assert_calls() + + def test_get_stack_in_progress(self): + in_progress = self.stack.copy() + in_progress['stack_status'] = 'CREATE_IN_PROGRESS' + self.register_uris([ + dict(method='GET', + uri='{endpoint}/stacks/{name}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name), + status_code=302, + headers=dict( + location='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, name=self.stack_name))), + dict(method='GET', + uri='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, name=self.stack_name), + json={"stack": in_progress}), + ]) + + res = self.cloud.get_stack(self.stack_name) + self.assertIsNotNone(res) + self.assertEqual(in_progress['stack_name'], res['stack_name']) + self.assertEqual(in_progress['stack_name'], res['name']) + self.assertEqual(in_progress['stack_status'], res['stack_status']) + self.assertEqual('CREATE', res['action']) + self.assertEqual('IN_PROGRESS', res['status']) + + self.assert_calls() From 413965c33101197ed86b742e3f8aee1764fda323 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 2 Apr 2017 09:11:02 -0500 Subject: [PATCH 1404/3836] Migrate server snapshot tests to requests_mock Change-Id: Ic6a489ac25688b7666a6a130f7daf43841909f30 --- shade/openstackcloud.py | 2 +- shade/tests/fakes.py | 33 +++++++ shade/tests/unit/base.py | 6 +- shade/tests/unit/test_image.py | 43 ++------- shade/tests/unit/test_image_snapshot.py | 113 +++++++++++++++++------- 5 files changed, 127 insertions(+), 70 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index ec2dc085f..ca95ff241 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3314,7 +3314,7 @@ def create_image_snapshot( " could not be snapshotted.".format(server=server)) server = server_obj image_id = str(self.manager.submit_task(_tasks.ImageSnapshotCreate( - image_name=name, server=server, metadata=metadata))) + image_name=name, server=server['id'], metadata=metadata))) self.list_images.invalidate(self) image = self.get_image(image_id) diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 40a1736bc..f153b4f85 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -28,6 +28,8 @@ COMPUTE_ENDPOINT = 'https://compute.example.com/v2.1' ORCHESTRATION_ENDPOINT = 'https://orchestration.example.com/v1/{p}'.format( p=PROJECT_ID) +NO_MD5 = '93b885adfe0da089cdf634904fd59f71' +NO_SHA256 = '6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d' def make_fake_flavor(flavor_id, name, ram=100, disk=1600, vcpus=24): @@ -177,6 +179,37 @@ def make_fake_stack_event( } +def make_fake_image( + image_id=None, md5=NO_MD5, sha256=NO_SHA256, status='active'): + return { + u'image_state': u'available', + u'container_format': u'bare', + u'min_ram': 0, + u'ramdisk_id': None, + u'updated_at': u'2016-02-10T05:05:02Z', + u'file': '/v2/images/' + image_id + '/file', + u'size': 3402170368, + u'image_type': u'snapshot', + u'disk_format': u'qcow2', + u'id': image_id, + u'schema': u'/v2/schemas/image', + u'status': status, + u'tags': [], + u'visibility': u'private', + u'locations': [{ + u'url': u'http://127.0.0.1/images/' + image_id, + u'metadata': {}}], + u'min_disk': 40, + u'virtual_size': None, + u'name': u'fake_image', + u'checksum': u'ee36e35a297980dee1b514de9803ec6d', + u'created_at': u'2016-02-10T05:03:11Z', + u'owner_specified.shade.md5': NO_MD5, + u'owner_specified.shade.sha256': NO_SHA256, + u'owner_specified.shade.object': 'images/fake_image', + u'protected': False} + + class FakeEndpoint(object): def __init__(self, id, service_id, region, publicurl, internalurl=None, adminurl=None): diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index bb9f225d9..ed182ce80 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -550,7 +550,7 @@ def __do_register_uris(self, uri_mock_list=None): mock_method, mock_uri, params['response_list'], **params['kw_params']) - def assert_calls(self, stop_after=None): + def assert_calls(self, stop_after=None, do_count=True): for (x, (call, history)) in enumerate( zip(self.calls, self.adapter.request_history)): if stop_after and x > stop_after: @@ -571,4 +571,6 @@ def assert_calls(self, stop_after=None): self.assertEqual( value, history.headers[key], 'header mismatch in call {index}'.format(index=x)) - self.assertEqual(len(self.calls), len(self.adapter.request_history)) + if do_count: + self.assertEqual( + len(self.calls), len(self.adapter.request_history)) diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index d16835497..ce206cf51 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -24,11 +24,10 @@ import shade from shade import exc from shade import meta +from shade.tests import fakes from shade.tests.unit import base -NO_MD5 = '93b885adfe0da089cdf634904fd59f71' -NO_SHA256 = '6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d' CINDER_URL = 'https://volume.example.com/v2/1c36b64c840a42cd9e9b931a369337f0' @@ -40,33 +39,7 @@ def setUp(self): self.imagefile = tempfile.NamedTemporaryFile(delete=False) self.imagefile.write(b'\0') self.imagefile.close() - self.fake_image_dict = { - u'image_state': u'available', - u'container_format': u'bare', - u'min_ram': 0, - u'ramdisk_id': None, - u'updated_at': u'2016-02-10T05:05:02Z', - u'file': '/v2/images/' + self.image_id + '/file', - u'size': 3402170368, - u'image_type': u'snapshot', - u'disk_format': u'qcow2', - u'id': self.image_id, - u'schema': u'/v2/schemas/image', - u'status': u'active', - u'tags': [], - u'visibility': u'private', - u'locations': [{ - u'url': u'http://127.0.0.1/images/' + self.image_id, - u'metadata': {}}], - u'min_disk': 40, - u'virtual_size': None, - u'name': u'fake_image', - u'checksum': u'ee36e35a297980dee1b514de9803ec6d', - u'created_at': u'2016-02-10T05:03:11Z', - u'owner_specified.shade.md5': NO_MD5, - u'owner_specified.shade.sha256': NO_SHA256, - u'owner_specified.shade.object': 'images/fake_image', - u'protected': False} + self.fake_image_dict = fakes.make_fake_image(image_id=self.image_id) self.fake_search_return = {'images': [self.fake_image_dict]} self.output = uuid.uuid4().bytes @@ -193,9 +166,9 @@ def test_create_image_put_v2(self): json={u'container_format': u'bare', u'disk_format': u'qcow2', u'name': u'fake_image', - u'owner_specified.shade.md5': NO_MD5, + u'owner_specified.shade.md5': fakes.NO_MD5, u'owner_specified.shade.object': u'images/fake_image', # noqa - u'owner_specified.shade.sha256': NO_SHA256, + u'owner_specified.shade.sha256': fakes.NO_SHA256, u'visibility': u'private'}) ), dict(method='PUT', @@ -275,8 +248,8 @@ def test_create_image_task(self): object=image_name), status_code=201, validate=dict( - headers={'x-object-meta-x-shade-md5': NO_MD5, - 'x-object-meta-x-shade-sha256': NO_SHA256}) + headers={'x-object-meta-x-shade-md5': fakes.NO_MD5, + 'x-object-meta-x-shade-sha256': fakes.NO_SHA256}) ), dict(method='GET', uri='https://image.example.com/v2/images', json={'images': []}), @@ -308,9 +281,9 @@ def test_create_image_task(self): container=container_name, object=image_name), u'path': u'/owner_specified.shade.object'}, - {u'op': u'add', u'value': NO_MD5, + {u'op': u'add', u'value': fakes.NO_MD5, u'path': u'/owner_specified.shade.md5'}, - {u'op': u'add', u'value': NO_SHA256, + {u'op': u'add', u'value': fakes.NO_SHA256, u'path': u'/owner_specified.shade.sha256'}], key=operator.itemgetter('value')), headers={ diff --git a/shade/tests/unit/test_image_snapshot.py b/shade/tests/unit/test_image_snapshot.py index 07c4ea5f8..860cb19e1 100644 --- a/shade/tests/unit/test_image_snapshot.py +++ b/shade/tests/unit/test_image_snapshot.py @@ -14,51 +14,100 @@ import uuid -import mock - -import shade from shade import exc +from shade.tests import fakes from shade.tests.unit import base -class TestImageSnapshot(base.TestCase): +class TestImageSnapshot(base.RequestsMockTestCase): def setUp(self): super(TestImageSnapshot, self).setUp() + self.server_id = str(uuid.uuid4()) self.image_id = str(uuid.uuid4()) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - @mock.patch.object(shade.OpenStackCloud, 'get_image') - def test_create_image_snapshot_wait_until_active_never_active(self, - mock_get, - mock_nova): - mock_nova.servers.create_image.return_value = { - 'status': 'queued', - 'id': self.image_id, - } - mock_get.return_value = {'status': 'saving', 'id': self.image_id} - self.assertRaises(exc.OpenStackCloudTimeout, - self.cloud.create_image_snapshot, - 'test-snapshot', dict(id='fake-server'), - wait=True, timeout=0.01) + def test_create_image_snapshot_wait_until_active_never_active(self): + snapshot_name = 'test-snapshot' + fake_image = fakes.make_fake_image(self.image_id, status='pending') + self.register_uris([ + dict( + method='POST', + uri='{endpoint}/servers/{server_id}/action'.format( + endpoint=fakes.COMPUTE_ENDPOINT, + server_id=self.server_id), + headers=dict( + Location='{endpoint}/images/{image_id}'.format( + endpoint='https://images.example.com', + image_id=self.image_id)), + validate=dict( + json={ + "createImage": { + "name": snapshot_name, + "metadata": {}, + }})), + self.get_glance_discovery_mock_dict(), + dict( + method='GET', + uri='https://image.example.com/v2/images', + json=dict(images=[fake_image])), + ]) + + self.assertRaises( + exc.OpenStackCloudTimeout, + self.cloud.create_image_snapshot, + snapshot_name, dict(id=self.server_id), + wait=True, timeout=0.01) + + # After the fifth call, we just keep polling get images for status. + # Due to mocking sleep, we have no clue how many times we'll call it. + self.assert_calls(stop_after=5, do_count=False) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - @mock.patch.object(shade.OpenStackCloud, 'get_image') - def test_create_image_snapshot_wait_active(self, mock_get, mock_nova): - mock_nova.servers.create_image.return_value = { - 'status': 'queued', - 'id': self.image_id, - } - mock_get.return_value = {'status': 'active', 'id': self.image_id} + def test_create_image_snapshot_wait_active(self): + snapshot_name = 'test-snapshot' + pending_image = fakes.make_fake_image(self.image_id, status='pending') + fake_image = fakes.make_fake_image(self.image_id) + self.register_uris([ + dict( + method='POST', + uri='{endpoint}/servers/{server_id}/action'.format( + endpoint=fakes.COMPUTE_ENDPOINT, + server_id=self.server_id), + headers=dict( + Location='{endpoint}/images/{image_id}'.format( + endpoint='https://images.example.com', + image_id=self.image_id)), + validate=dict( + json={ + "createImage": { + "name": snapshot_name, + "metadata": {}, + }})), + self.get_glance_discovery_mock_dict(), + dict( + method='GET', + uri='https://image.example.com/v2/images', + json=dict(images=[pending_image])), + dict( + method='GET', + uri='https://image.example.com/v2/images', + json=dict(images=[fake_image])), + ]) image = self.cloud.create_image_snapshot( - 'test-snapshot', dict(id='fake-server'), wait=True, timeout=2) + 'test-snapshot', dict(id=self.server_id), wait=True, timeout=2) self.assertEqual(image['id'], self.image_id) - @mock.patch.object(shade.OpenStackCloud, 'get_server') - def test_create_image_snapshot_bad_name_exception( - self, mock_get_server): - mock_get_server.return_value = None + self.assert_calls() + + def test_create_image_snapshot_bad_name_exception(self): + self.register_uris([ + dict( + method='POST', + uri='{endpoint}/servers/{server_id}/action'.format( + endpoint=fakes.COMPUTE_ENDPOINT, + server_id=self.server_id), + json=dict(servers=[])), + ]) self.assertRaises( exc.OpenStackCloudException, self.cloud.create_image_snapshot, - 'test-snapshot', 'missing-server') + 'test-snapshot', self.server_id) From aa58173f0d60a8f04318b51ff6d1490e7ee10cfb Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 2 Apr 2017 15:53:06 -0500 Subject: [PATCH 1405/3836] Add ability to configure extra_specs to be off We fetch flavor extra_specs for each flavor for consistency with old behavior. However, it's costly and often just simply not needed. Query clouds.yaml for extra client settings and if "get_flavor_extra_specs" is set to false, set the default for list_flavors to false. Change-Id: Iaea1c41e8e0ae504cb080d7d37407de12be96fd1 --- .../config-flavor-specs-ca712e17971482b6.yaml | 4 ++++ shade/openstackcloud.py | 14 +++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/config-flavor-specs-ca712e17971482b6.yaml diff --git a/releasenotes/notes/config-flavor-specs-ca712e17971482b6.yaml b/releasenotes/notes/config-flavor-specs-ca712e17971482b6.yaml new file mode 100644 index 000000000..4bb1e9013 --- /dev/null +++ b/releasenotes/notes/config-flavor-specs-ca712e17971482b6.yaml @@ -0,0 +1,4 @@ +--- +features: + - Adds ability to add a config setting to clouds.yaml to + disable fetching extra_specs from flavors. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index ec2dc085f..59af8ac76 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -149,6 +149,12 @@ def __init__( self.secgroup_source = cloud_config.config['secgroup_source'] self.force_ipv4 = cloud_config.force_ipv4 self.strict_mode = strict + # TODO(mordred) When os-client-config adds a "get_client_settings()" + # method to CloudConfig - remove this. + self._extra_config = cloud_config._openstack_config.get_extra_config( + 'shade', { + 'get_flavor_extra_specs': True, + }) if manager is not None: self.manager = manager @@ -1757,12 +1763,18 @@ def list_availability_zone_names(self, unavailable=False): return ret @_utils.cache_on_arguments() - def list_flavors(self, get_extra=True): + def list_flavors(self, get_extra=None): """List all available flavors. + :param get_extra: Whether or not to fetch extra specs for each flavor. + Defaults to True. Default behavior value can be + overridden in clouds.yaml by setting + shade.get_extra_specs to False. :returns: A list of flavor ``munch.Munch``. """ + if get_extra is None: + get_extra = self._extra_config['get_flavor_extra_specs'] with _utils.shade_exceptions("Error fetching flavor list"): flavors = self._normalize_flavors( self._compute_client.get( From a4d75498b4961ddf8873d1ac05226f2de4c65c55 Mon Sep 17 00:00:00 2001 From: Ankur Gupta Date: Thu, 2 Feb 2017 14:42:24 -0600 Subject: [PATCH 1406/3836] Introduce Base for Octavia (load balancing) Change-Id: I3aa5b8af9e5f6dfbd3e71645fc7324e19933afef --- doc/source/users/proxies/load_balancer_v2.rst | 22 +++++ openstack/load_balancer/__init__.py | 0 .../load_balancer/load_balancer_service.py | 26 ++++++ openstack/load_balancer/v2/__init__.py | 0 openstack/load_balancer/v2/_proxy.py | 82 +++++++++++++++++++ openstack/load_balancer/v2/load_balancer.py | 55 +++++++++++++ openstack/load_balancer/version.py | 30 +++++++ openstack/profile.py | 2 + .../functional/load_balancer/__init__.py | 0 .../functional/load_balancer/v2/__init__.py | 0 .../load_balancer/v2/test_load_balancer.py | 52 ++++++++++++ .../tests/unit/load_balancer/__init__.py | 0 .../unit/load_balancer/test_load_balancer.py | 69 ++++++++++++++++ .../test_load_balancer_service.py | 28 +++++++ .../tests/unit/load_balancer/test_proxy.py | 42 ++++++++++ .../tests/unit/load_balancer/test_version.py | 43 ++++++++++ openstack/tests/unit/test_connection.py | 2 + openstack/tests/unit/test_profile.py | 2 + 18 files changed, 455 insertions(+) create mode 100644 doc/source/users/proxies/load_balancer_v2.rst create mode 100644 openstack/load_balancer/__init__.py create mode 100644 openstack/load_balancer/load_balancer_service.py create mode 100644 openstack/load_balancer/v2/__init__.py create mode 100644 openstack/load_balancer/v2/_proxy.py create mode 100644 openstack/load_balancer/v2/load_balancer.py create mode 100644 openstack/load_balancer/version.py create mode 100644 openstack/tests/functional/load_balancer/__init__.py create mode 100644 openstack/tests/functional/load_balancer/v2/__init__.py create mode 100644 openstack/tests/functional/load_balancer/v2/test_load_balancer.py create mode 100644 openstack/tests/unit/load_balancer/__init__.py create mode 100644 openstack/tests/unit/load_balancer/test_load_balancer.py create mode 100644 openstack/tests/unit/load_balancer/test_load_balancer_service.py create mode 100644 openstack/tests/unit/load_balancer/test_proxy.py create mode 100644 openstack/tests/unit/load_balancer/test_version.py diff --git a/doc/source/users/proxies/load_balancer_v2.rst b/doc/source/users/proxies/load_balancer_v2.rst new file mode 100644 index 000000000..220c4f708 --- /dev/null +++ b/doc/source/users/proxies/load_balancer_v2.rst @@ -0,0 +1,22 @@ +Load Balancer v2 API +==================== + +.. automodule:: openstack.load_balancer.v2._proxy + +The LoadBalancer Class +---------------------- + +The load_balancer high-level interface is available through the +``load_balancer`` member of a :class:`~openstack.connection.Connection` object. +The ``load_balancer`` member will only be added if the service is detected. + +Load Balancer Operations +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.load_balancer.v2._proxy.Proxy + + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.create_load_balancer + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_load_balancer + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.load_balancers + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_load_balancer + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_load_balancer diff --git a/openstack/load_balancer/__init__.py b/openstack/load_balancer/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/load_balancer/load_balancer_service.py b/openstack/load_balancer/load_balancer_service.py new file mode 100644 index 000000000..1ff22b22f --- /dev/null +++ b/openstack/load_balancer/load_balancer_service.py @@ -0,0 +1,26 @@ +# 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 service_filter + + +class LoadBalancerService(service_filter.ServiceFilter): + """The load balancer service.""" + + valid_versions = [service_filter.ValidVersion('v2')] + + def __init__(self, version=None): + """Create a load balancer service.""" + super(LoadBalancerService, self).__init__( + service_type='load_balancer', + version=version + ) diff --git a/openstack/load_balancer/v2/__init__.py b/openstack/load_balancer/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py new file mode 100644 index 000000000..224fddb57 --- /dev/null +++ b/openstack/load_balancer/v2/_proxy.py @@ -0,0 +1,82 @@ +# 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.load_balancer.v2 import load_balancer as _lb +from openstack import proxy2 + + +class Proxy(proxy2.BaseProxy): + + def create_load_balancer(self, **attrs): + """Create a new load balancer from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.load_balancer.v2. + load_balancer.LoadBalancer`, + comprised of the properties on the + LoadBalancer class. + + :returns: The results of load balancer creation + :rtype: :class:`~openstack.load_balancer.v2.load_balancer.LoadBalancer` + """ + return self._create(_lb.LoadBalancer, **attrs) + + def get_load_balancer(self, *attrs): + """Get a load balancer + + :param load_balancer: The value can be the name of a load balancer + or :class:`~openstack.load_balancer.v2.load_balancer.LoadBalancer` + instance. + + :returns: One + :class:`~openstack.load_balancer.v2.load_balancer.LoadBalancer` + """ + return self._get(_lb.LoadBalancer, *attrs) + + def load_balancers(self, **query): + """Retrieve a generator of load balancers + + :returns: A generator of load balancer instances + """ + return self._list(_lb.LoadBalancer, paginated=True, **query) + + def delete_load_balancer(self, load_balancer, ignore_missing=True): + """Delete a load balancer + + :param load_balancer: The load_balancer can be either the name or a + :class:`~openstack.load_balancer.v2.load_balancer.LoadBalancer` + instance + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the load balancer does not exist. + When set to ``True``, no exception will be set when attempting to + delete a nonexistent load balancer. + + :returns: ``None`` + """ + return self._delete(_lb.LoadBalancer, load_balancer, + ignore_missing=ignore_missing) + + def find_load_balancer(self, name_or_id, ignore_missing=True): + """Find a single load balancer + + :param name_or_id: The name or ID of a load balancer + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the load balancer does not exist. + When set to ``True``, no exception will be set when attempting + to delete a nonexistent load balancer. + + :returns: ``None`` + """ + return self._find(_lb.LoadBalancer, name_or_id, + ignore_missing=ignore_missing) diff --git a/openstack/load_balancer/v2/load_balancer.py b/openstack/load_balancer/v2/load_balancer.py new file mode 100644 index 000000000..509f0ae80 --- /dev/null +++ b/openstack/load_balancer/v2/load_balancer.py @@ -0,0 +1,55 @@ +# 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.load_balancer import load_balancer_service as lb_service +from openstack import resource2 as resource + + +class LoadBalancer(resource.Resource): + resource_key = 'loadbalancer' + resources_key = 'loadbalancers' + base_path = '/loadbalancers' + service = lb_service.LoadBalancerService() + + # capabilities + allow_create = True + allow_list = True + allow_get = True + allow_delete = True + + #: Properties + #: Timestamp when the load balancer was created + created_at = resource.Body('created_at') + #: The load balancer description + description = resource.Body('description') + #: The administrative state of the load balancer *Type: bool* + is_admin_state_up = resource.Body('admin_state_up', type=bool) + #: List of listeners associated with this load balancer + listeners = resource.Body('listeners', type=list) + #: The load balancer name + name = resource.Body('name') + #: Operating status of the load balancer + operating_status = resource.Body('operating_status') + #: List of pools associated with this load balancer + pools = resource.Body('pools', type=list) + #: The ID of the project this load balancer is associated with. + project_id = resource.Body('project_id') + #: The provisioning status of this load balancer + provisioning_status = resource.Body('provisioning_status') + #: VIP address of load balancer + vip_address = resource.Body('vip_address') + #: VIP port ID + vip_port_id = resource.Body('vip_port_id') + #: VIP subnet ID + vip_subnet_id = resource.Body('vip_subnet_id') + #: Timestamp when the load balancer was last updated + updated_at = resource.Body('updated_at') diff --git a/openstack/load_balancer/version.py b/openstack/load_balancer/version.py new file mode 100644 index 000000000..4a829c2e3 --- /dev/null +++ b/openstack/load_balancer/version.py @@ -0,0 +1,30 @@ +# 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.load_balancer import load_balancer_service as lb_service +from openstack import resource2 as resource + + +class Version(resource.Resource): + resource_key = 'version' + resources_key = 'versions' + base_path = '/' + service = lb_service.LoadBalancerService( + version=lb_service.LoadBalancerService.UNVERSIONED + ) + + # capabilities + allow_list = True + + # Properties + links = resource.Body('links') + status = resource.Body('status') diff --git a/openstack/profile.py b/openstack/profile.py index 2cd7f2de8..d82b88a6d 100644 --- a/openstack/profile.py +++ b/openstack/profile.py @@ -64,6 +64,7 @@ from openstack.identity import identity_service from openstack.image import image_service from openstack.key_manager import key_manager_service +from openstack.load_balancer import load_balancer_service as lb_service from openstack.message import message_service from openstack import module_loader from openstack.network import network_service @@ -102,6 +103,7 @@ def __init__(self, plugins=None): self._add_service(identity_service.IdentityService(version="v3")) self._add_service(image_service.ImageService(version="v2")) self._add_service(key_manager_service.KeyManagerService(version="v1")) + self._add_service(lb_service.LoadBalancerService(version="v2")) self._add_service(message_service.MessageService(version="v1")) self._add_service(network_service.NetworkService(version="v2")) self._add_service( diff --git a/openstack/tests/functional/load_balancer/__init__.py b/openstack/tests/functional/load_balancer/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/functional/load_balancer/v2/__init__.py b/openstack/tests/functional/load_balancer/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py new file mode 100644 index 000000000..4082a1194 --- /dev/null +++ b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py @@ -0,0 +1,52 @@ +# 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 uuid + +from openstack.load_balancer.v2 import load_balancer +from openstack.tests.functional import base + + +class TestLoadBalancer(base.BaseFunctionalTest): + + NAME = uuid.uuid4().hex + ID = None + VIP_SUBNET_ID = uuid.uuid4().hex + + @classmethod + def setUpClass(cls): + super(TestLoadBalancer, cls).setUpClass() + test_lb = cls.conn.load_balancer.create_load_balancer( + name=cls.NAME, vip_subnet_id=cls.VIP_SUBNET_ID) + assert isinstance(test_lb, load_balancer.LoadBalancer) + cls.assertIs(cls.NAME, test_lb.name) + cls.ID = test_lb.id + + @classmethod + def tearDownClass(cls): + test_lb = cls.conn.load_balancer.delete_load_balancer( + cls.ID, ignore_missing=False) + cls.assertIs(None, test_lb) + + def test_find(self): + test_lb = self.conn.load_balancer.find_load_balancer(self.NAME) + self.assertEqual(self.ID, test_lb.id) + + def test_get(self): + test_lb = self.conn.load_balancer.get_load_balancer(self.ID) + self.assertEqual(self.NAME, test_lb.name) + self.assertEqual(self.ID, test_lb.id) + self.assertEqual(self.VIP_SUBNET_ID, test_lb.vip_subnet_id) + + def test_list(self): + names = [lb.name for lb in self.conn.load_balancer.load_balancers()] + self.assertIn(self.NAME, names) diff --git a/openstack/tests/unit/load_balancer/__init__.py b/openstack/tests/unit/load_balancer/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/unit/load_balancer/test_load_balancer.py b/openstack/tests/unit/load_balancer/test_load_balancer.py new file mode 100644 index 000000000..0e832ab82 --- /dev/null +++ b/openstack/tests/unit/load_balancer/test_load_balancer.py @@ -0,0 +1,69 @@ +# 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 testtools + +from openstack.load_balancer.v2 import load_balancer + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'admin_state_up': True, + 'created_at': '3', + 'description': 'fake_description', + 'id': IDENTIFIER, + 'listeners': [{'id', '4'}], + 'name': 'test_load_balancer', + 'operating_status': '6', + 'provisioning_status': '7', + 'project_id': '8', + 'vip_address': '9', + 'vip_subnet_id': '10', + 'vip_port_id': '11', + 'pools': [{'id', '13'}], +} + + +class TestLoadBalancer(testtools.TestCase): + + def test_basic(self): + test_load_balancer = load_balancer.LoadBalancer() + self.assertEqual('loadbalancer', test_load_balancer.resource_key) + self.assertEqual('loadbalancers', test_load_balancer.resources_key) + self.assertEqual('/loadbalancers', test_load_balancer.base_path) + self.assertEqual('load_balancer', + test_load_balancer.service.service_type) + self.assertTrue(test_load_balancer.allow_create) + self.assertTrue(test_load_balancer.allow_get) + self.assertTrue(test_load_balancer.allow_delete) + self.assertTrue(test_load_balancer.allow_list) + + def test_make_it(self): + test_load_balancer = load_balancer.LoadBalancer(**EXAMPLE) + self.assertTrue(test_load_balancer.is_admin_state_up) + self.assertEqual(EXAMPLE['created_at'], test_load_balancer.created_at), + self.assertEqual(EXAMPLE['description'], + test_load_balancer.description) + self.assertEqual(EXAMPLE['id'], test_load_balancer.id) + self.assertEqual(EXAMPLE['listeners'], test_load_balancer.listeners) + self.assertEqual(EXAMPLE['name'], test_load_balancer.name) + self.assertEqual(EXAMPLE['operating_status'], + test_load_balancer.operating_status) + self.assertEqual(EXAMPLE['provisioning_status'], + test_load_balancer.provisioning_status) + self.assertEqual(EXAMPLE['project_id'], test_load_balancer.project_id) + self.assertEqual(EXAMPLE['vip_address'], + test_load_balancer.vip_address) + self.assertEqual(EXAMPLE['vip_subnet_id'], + test_load_balancer.vip_subnet_id) + self.assertEqual(EXAMPLE['vip_port_id'], + test_load_balancer.vip_port_id) + self.assertEqual(EXAMPLE['pools'], test_load_balancer.pools) diff --git a/openstack/tests/unit/load_balancer/test_load_balancer_service.py b/openstack/tests/unit/load_balancer/test_load_balancer_service.py new file mode 100644 index 000000000..dd11ae55f --- /dev/null +++ b/openstack/tests/unit/load_balancer/test_load_balancer_service.py @@ -0,0 +1,28 @@ +# 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 testtools + +from openstack.load_balancer import load_balancer_service as lb_service + + +class TestLoadBalancingService(testtools.TestCase): + + def test_service(self): + sot = lb_service.LoadBalancerService() + self.assertEqual('load_balancer', sot.service_type) + self.assertEqual('public', sot.interface) + self.assertIsNone(sot.region) + self.assertIsNone(sot.service_name) + self.assertEqual(1, len(sot.valid_versions)) + self.assertEqual('v2', sot.valid_versions[0].module) + self.assertEqual('v2', sot.valid_versions[0].path) diff --git a/openstack/tests/unit/load_balancer/test_proxy.py b/openstack/tests/unit/load_balancer/test_proxy.py new file mode 100644 index 000000000..97cdec134 --- /dev/null +++ b/openstack/tests/unit/load_balancer/test_proxy.py @@ -0,0 +1,42 @@ +# 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.load_balancer.v2 import _proxy +from openstack.load_balancer.v2 import load_balancer as lb +from openstack.tests.unit import test_proxy_base2 + + +class TestLoadBalancerProxy(test_proxy_base2.TestProxyBase): + def setUp(self): + super(TestLoadBalancerProxy, self).setUp() + self.proxy = _proxy.Proxy(self.session) + + def test_load_balancers(self): + self.verify_list(self.proxy.load_balancers, + lb.LoadBalancer, + paginated=True) + + def test_load_balancer_get(self): + self.verify_get(self.proxy.get_load_balancer, + lb.LoadBalancer) + + def test_load_balancer_create(self): + self.verify_create(self.proxy.create_load_balancer, + lb.LoadBalancer) + + def test_load_balancer_delete(self): + self.verify_delete(self.proxy.delete_load_balancer, + lb.LoadBalancer, True) + + def test_load_balancer_find(self): + self.verify_find(self.proxy.find_load_balancer, + lb.LoadBalancer) diff --git a/openstack/tests/unit/load_balancer/test_version.py b/openstack/tests/unit/load_balancer/test_version.py new file mode 100644 index 000000000..1b77eda26 --- /dev/null +++ b/openstack/tests/unit/load_balancer/test_version.py @@ -0,0 +1,43 @@ +# 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 testtools + +from openstack.load_balancer import version + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'id': IDENTIFIER, + 'links': '2', + 'status': '3', +} + + +class TestVersion(testtools.TestCase): + + def test_basic(self): + sot = version.Version() + self.assertEqual('version', sot.resource_key) + self.assertEqual('versions', sot.resources_key) + self.assertEqual('/', sot.base_path) + self.assertEqual('load_balancer', sot.service.service_type) + self.assertFalse(sot.allow_create) + self.assertFalse(sot.allow_get) + self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = version.Version(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['links'], sot.links) + self.assertEqual(EXAMPLE['status'], sot.status) diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index f84abf7fe..229dbb728 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -147,6 +147,8 @@ def test_create_session(self): conn.network.__class__.__module__) self.assertEqual('openstack.object_store.v1._proxy', conn.object_store.__class__.__module__) + self.assertEqual('openstack.load_balancer.v2._proxy', + conn.load_balancer.__class__.__module__) self.assertEqual('openstack.orchestration.v1._proxy', conn.orchestration.__class__.__module__) self.assertEqual('openstack.telemetry.v2._proxy', diff --git a/openstack/tests/unit/test_profile.py b/openstack/tests/unit/test_profile.py index 2ca67e028..272918171 100644 --- a/openstack/tests/unit/test_profile.py +++ b/openstack/tests/unit/test_profile.py @@ -27,6 +27,7 @@ def test_init(self): 'identity', 'image', 'key-manager', + 'load_balancer', 'messaging', 'metering', 'network', @@ -45,6 +46,7 @@ def test_default_versions(self): self.assertEqual('v1', prof.get_filter('database').version) self.assertEqual('v3', prof.get_filter('identity').version) self.assertEqual('v2', prof.get_filter('image').version) + self.assertEqual('v2', prof.get_filter('load_balancer').version) self.assertEqual('v2', prof.get_filter('network').version) self.assertEqual('v1', prof.get_filter('object-store').version) self.assertEqual('v1', prof.get_filter('orchestration').version) From f956aed405c2837bcef48e0e7ddd34e06ad046a5 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 2 Apr 2017 11:14:31 -0500 Subject: [PATCH 1407/3836] Migrate create_image_snapshot to REST Change-Id: I6cb8a26bf9d6189ecc18ee7fbde44f37c131aaa8 --- shade/openstackcloud.py | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index ca95ff241..872f7d95d 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3295,6 +3295,12 @@ def create_image_snapshot( self, name, server, wait=False, timeout=3600, **metadata): """Create an image by snapshotting an existing server. + ..note:: + On most clouds this is a cold snapshot - meaning that the server + in question will be shutdown before taking the snapshot. It is + possible that it's a live snapshot - but there is no way to know + as a user, so caveat emptor. + :param name: Name of the image to be created :param server: Server name or ID or dict representing the server to be snapshotted @@ -3313,8 +3319,33 @@ def create_image_snapshot( "Server {server} could not be found and therefore" " could not be snapshotted.".format(server=server)) server = server_obj - image_id = str(self.manager.submit_task(_tasks.ImageSnapshotCreate( - image_name=name, server=server['id'], metadata=metadata))) + response = self._compute_client.post( + '/servers/{server_id}/action'.format(server_id=server['id']), + json={ + "createImage": { + "name": name, + "metadata": metadata, + } + }) + # You won't believe it - wait, who am I kidding - of course you will! + # Nova returns the URL of the image created in the Location + # header of the response. (what?) But, even better, the URL it responds + # with has a very good chance of being wrong (it is built from + # nova.conf values that point to internal API servers in any cloud + # large enough to have both public and internal endpoints. + # However, nobody has ever noticed this because novaclient doesn't + # actually use that URL - it extracts the id from the end of + # the url, then returns the id. This leads us to question: + # a) why Nova is going to return a value in a header + # b) why it's going to return data that probably broken + # c) indeed the very nature of the fabric of reality + # Although it fills us with existential dread, we have no choice but + # to follow suit like a lemming being forced over a cliff by evil + # producers from Disney. + # TODO(mordred) Update this to consume json microversion when it is + # available. + # blueprint:remove-create-image-location-header-response + image_id = response.headers['Location'].rsplit('/', 1)[1] self.list_images.invalidate(self) image = self.get_image(image_id) From 9a3c49952910ac8498e1c60e333a1f8deb962de2 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Wed, 5 Apr 2017 10:05:48 -0400 Subject: [PATCH 1408/3836] Add get_stack_* methods to documentation Recent get_stack_* methods were left out of the documentation. This also adjusts a doc warning about indentation being off. Change-Id: I5bde59cfb3da4508300f5535e3c13c0265b7460f --- doc/source/users/proxies/orchestration.rst | 3 +++ openstack/orchestration/v1/_proxy.py | 7 ++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/doc/source/users/proxies/orchestration.rst b/doc/source/users/proxies/orchestration.rst index 4ad00e1d0..e663e64ed 100644 --- a/doc/source/users/proxies/orchestration.rst +++ b/doc/source/users/proxies/orchestration.rst @@ -24,6 +24,9 @@ Stack Operations .. automethod:: openstack.orchestration.v1._proxy.Proxy.delete_stack .. automethod:: openstack.orchestration.v1._proxy.Proxy.find_stack .. automethod:: openstack.orchestration.v1._proxy.Proxy.get_stack + .. automethod:: openstack.orchestration.v1._proxy.Proxy.get_stack_environment + .. automethod:: openstack.orchestration.v1._proxy.Proxy.get_stack_files + .. automethod:: openstack.orchestration.v1._proxy.Proxy.get_stack_template .. automethod:: openstack.orchestration.v1._proxy.Proxy.stacks .. automethod:: openstack.orchestration.v1._proxy.Proxy.validate_template .. automethod:: openstack.orchestration.v1._proxy.Proxy.resources diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index 08bf503e8..fad49ccf5 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -153,9 +153,10 @@ def get_stack_environment(self, stack): :class:`~openstack.orchestration.v1.stack.Stack` :returns: One object of - :class:`~openstack.orchestration.v1.stack_environment.StackEnvironment` - :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + :class:`~openstack.orchestration.v1.stack_environment.\ + StackEnvironment` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource can be found. """ if isinstance(stack, _stack.Stack): obj = stack From 442bed2658dcf246732180f6813caa13d2966ce6 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Wed, 5 Apr 2017 10:23:37 -0400 Subject: [PATCH 1409/3836] Clean up some errant doc warnings/errors This change cleans up a couple of minor doc warnings that don't usually get caught. I'm making this small change now so that I can turn on warnings_as_errors in the enforcer script. Change-Id: Ib0100116a4f7a38ad882d20a56ae99d15d49766a --- doc/source/users/index.rst | 1 + doc/source/users/proxies/telemetry.rst | 2 +- doc/source/users/proxies/workflow.rst | 2 -- openstack/cluster/v1/_proxy.py | 3 ++- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/source/users/index.rst b/doc/source/users/index.rst index 498c6c327..2209a451d 100644 --- a/doc/source/users/index.rst +++ b/doc/source/users/index.rst @@ -81,6 +81,7 @@ but listed below are the ones provided by this SDK by default. Image v1 Image v2 Key Manager + Load Balancer Message v1 Message v2 Network diff --git a/doc/source/users/proxies/telemetry.rst b/doc/source/users/proxies/telemetry.rst index 465ba7d9f..fc29a13a4 100644 --- a/doc/source/users/proxies/telemetry.rst +++ b/doc/source/users/proxies/telemetry.rst @@ -6,7 +6,7 @@ Telemetry API For details on how to use telemetry, see :doc:`/users/guides/telemetry` -.. automethod:: openstack.telemetry.v2._proxy +.. automodule:: openstack.telemetry.v2._proxy The Telemetry Class ------------------- diff --git a/doc/source/users/proxies/workflow.rst b/doc/source/users/proxies/workflow.rst index 5f8cbe5b5..429d6d7ac 100644 --- a/doc/source/users/proxies/workflow.rst +++ b/doc/source/users/proxies/workflow.rst @@ -1,8 +1,6 @@ Workflow API ============ -For details on how to use workflow, see :doc:`/users/guides/workflow` - .. automodule:: openstack.workflow.v2._proxy The Workflow Class diff --git a/openstack/cluster/v1/_proxy.py b/openstack/cluster/v1/_proxy.py index 5621be5b8..903caaaf2 100644 --- a/openstack/cluster/v1/_proxy.py +++ b/openstack/cluster/v1/_proxy.py @@ -1051,7 +1051,8 @@ def wait_for_status(self, resource, status, failures=[], interval=2, The resource must have a ``status`` attribute. :type resource: A :class:`~openstack.resource2.Resource` object. :param status: Desired status. - :param list failures: Statuses that would be interpreted as failures. + :param failures: Statuses that would be interpreted as failures. + :type failures: :py:class:`list` :param interval: Number of seconds to wait before to consecutive checks. Default to 2. :param wait: Maximum number of seconds to wait before the change. From ccf7cca47372249f6df392670e8c1baa061278b7 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 6 Apr 2017 09:23:40 -0500 Subject: [PATCH 1410/3836] Use REST for neutron floating IP list Wow, turns out we already had the tests fixed. How did we do that? Change-Id: I1c26a6f817ef7e7d67ba2e560da1149bc4345fbc --- shade/openstackcloud.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index ec2dc085f..4e3e79970 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2157,9 +2157,7 @@ def list_floating_ips(self, filters=None): def _neutron_list_floating_ips(self, filters=None): if not filters: filters = {} - with _utils.neutron_exceptions("error fetching floating IPs list"): - return self.manager.submit_task( - _tasks.NeutronFloatingIPList(**filters))['floatingips'] + return self._network_client.get('/floatingips.json', params=filters) def _nova_list_floating_ips(self): with _utils.shade_exceptions("Error fetching floating IPs list"): From a0b4c49f43e4b6627db5f2c443e60e62a0e5a85c Mon Sep 17 00:00:00 2001 From: Valery Tschopp Date: Thu, 6 Apr 2017 17:43:40 +0200 Subject: [PATCH 1411/3836] Add 'project_id' to Server query parameters This allow to list servers filtered by project. Change-Id: I0c169b5d47e645585855d953a9dfa33bf87de894 Closes-bug: #1680512 --- openstack/compute/v2/server.py | 1 + openstack/tests/unit/compute/v2/test_server.py | 1 + 2 files changed, 2 insertions(+) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 96634c40a..75c89a364 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -33,6 +33,7 @@ class Server(resource2.Resource, metadata.MetadataMixin): "status", "host", "all_tenants", "sort_key", "sort_dir", "reservation_id", "tags", + "project_id", tags_any="tags-any", not_tags="not-tags", not_tags_any="not-tags-any", diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 67daf6795..0c5109e36 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -91,6 +91,7 @@ def test_basic(self): "sort_key": "sort_key", "sort_dir": "sort_dir", "reservation_id": "reservation_id", + "project_id": "project_id", "tags": "tags", "tags_any": "tags-any", "not_tags": "not-tags", From 049b4cef420d239210148790d7b627bfb5197435 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 6 Apr 2017 10:26:29 -0500 Subject: [PATCH 1412/3836] Don't run extra server info on every server in list If someone is just doing a get, there is no reason to fill in the extra information for EVERY server in the list. This makes ansible modules especially slow. Instead, if get_server is called, prevent list_servers from doing the expansion and instead do it late just on the one requested. Fixes: ansible/ansbile#23354 Change-Id: I42773b292b11f1faa26cf9193b2d0435d9455dd0 --- shade/openstackcloud.py | 31 ++++++++++++++++--------------- shade/tests/unit/test_shade.py | 12 +++++++++--- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 4e3e79970..2b09a4f7a 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1983,18 +1983,10 @@ def _list_servers(self, detailed=False, all_projects=False, bare=False): servers = self._normalize_servers( self.manager.submit_task(_tasks.ServerList(**kwargs))) - if bare: - return servers - elif detailed: - return [ - meta.get_hostvars_from_server(self, server) - for server in servers - ] - else: - return [ - meta.add_server_interfaces(self, server) - for server in servers - ] + return [ + self._expand_server(server, detailed, bare) + for server in servers + ] def list_server_groups(self): """List all available server groups. @@ -2755,8 +2747,17 @@ def get_server( """ searchfunc = functools.partial(self.search_servers, - detailed=detailed, bare=bare) - return _utils._get_entity(searchfunc, name_or_id, filters) + detailed=detailed, bare=True) + server = _utils._get_entity(searchfunc, name_or_id, filters) + return self._expand_server(server, detailed, bare) + + def _expand_server(self, server, detailed, bare): + if bare or not server: + return server + elif detailed: + return meta.get_hostvars_from_server(self, server) + else: + return meta.add_server_interfaces(self, server) def get_server_by_id(self, id): return meta.add_server_interfaces(self, self._normalize_server( @@ -5048,7 +5049,7 @@ def _nova_detach_ip_from_server(self, server_id, floating_ip_id): def _add_ip_from_pool( self, server, network, fixed_address=None, reuse=True, wait=False, timeout=60, nat_destination=None): - """Add a floating IP to a sever from a given pool + """Add a floating IP to a server from a given pool This method reuses available IPs, when possible, or allocate new IPs to the current tenant. diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 3dbc2e232..56deb811d 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -52,10 +52,16 @@ def test_get_image_not_found(self, mock_search): r = self.cloud.get_image('doesNotExist') self.assertIsNone(r) - @mock.patch.object(shade.OpenStackCloud, 'search_servers') - def test_get_server(self, mock_search): + @mock.patch.object(shade.OpenStackCloud, '_expand_server') + @mock.patch.object(shade.OpenStackCloud, 'list_servers') + def test_get_server(self, mock_list, mock_expand): server1 = dict(id='123', name='mickey') - mock_search.return_value = [server1] + server2 = dict(id='345', name='mouse') + + def expand_server(server, detailed, bare): + return server + mock_expand.side_effect = expand_server + mock_list.return_value = [server1, server2] r = self.cloud.get_server('mickey') self.assertIsNotNone(r) self.assertDictEqual(server1, r) From d12032d14752a7574e414ad0e089d39f34cba2ef Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 11 Apr 2017 15:13:57 -0500 Subject: [PATCH 1413/3836] Stop defaulting container_format to ovf for vhd It turns out a basic vhd file is in bare format, not ovf - and bare is a perfectly sensible thing for vhd files to be. Actually, even past that, it turns out that container_format is essentially ignored by glance, not being very useful. disk_format is the important bit for external consumers. Also, we grab disk_format twice. Change-Id: Idc90a8542e1351da641e42aa67d8fa1c37b544e1 --- shade/openstackcloud.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 2b09a4f7a..0408e4368 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3466,20 +3466,14 @@ def create_image( :raises: OpenStackCloudException if there are problems uploading """ - - if not disk_format: - disk_format = self.cloud_config.config['image_format'] - if not meta: meta = {} if not disk_format: disk_format = self.cloud_config.config['image_format'] if not container_format: - if disk_format == 'vhd': - container_format = 'ovf' - else: - container_format = 'bare' + # https://docs.openstack.org/image-guide/image-formats.html + container_format = 'bare' if volume: if 'id' in volume: From 2d7eaeb113d6d976a605e4bf956b05bca01e0075 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 12 Apr 2017 04:21:56 +0000 Subject: [PATCH 1414/3836] Updated from global requirements Change-Id: I18492d7e1fe772e27af56ce2da68a9f277d2fb21 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index dd39ef67f..8bd3069ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ # 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.0.0 # Apache-2.0 +pbr!=2.1.0,>=2.0.0 # Apache-2.0 six>=1.9.0 # MIT stevedore>=1.20.0 # Apache-2.0 os-client-config>=1.22.0 # Apache-2.0 From d5982e8ffd75f51af1eeeb9568bf34bd9a2e482c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 12 Apr 2017 09:18:11 -0500 Subject: [PATCH 1415/3836] Don't fail on security_groups=None We currently put whatever is in security_groups into a list to be friendly, but this breaks if security_groups is None. It should stay None if it's None. Change-Id: I571c05bf8353f2ec5ad34ed31040032f2daefd25 Story: 2000828 --- shade/openstackcloud.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 0408e4368..dbf8b2034 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5432,9 +5432,9 @@ def create_server( if root_volume and not boot_volume: boot_volume = root_volume - if 'security_groups' in kwargs and not isinstance( - kwargs['security_groups'], list): - kwargs['security_groups'] = [kwargs['security_groups']] + security_groups = kwargs.get('security_groups') + if security_groups and not isinstance(kwargs['security_groups'], list): + kwargs['security_groups'] = [security_groups] if 'nics' in kwargs and not isinstance(kwargs['nics'], list): if isinstance(kwargs['nics'], dict): From 885ae5f2b7f80d8653beb8f02b39fcba9511ea53 Mon Sep 17 00:00:00 2001 From: Yuval Shalev Date: Mon, 10 Apr 2017 17:08:27 +0300 Subject: [PATCH 1416/3836] Add support for volume attachments in compute v2 This change adds a VolumeAttachment resource and the accompanying proxy methods for Compute v2. Change-Id: Ib9a10599cdc83426163668bbcc6f845218fa1a59 --- openstack/compute/v2/_proxy.py | 124 ++++++++++++++++++ openstack/compute/v2/volume_attachment.py | 41 ++++++ .../unit/compute/v2/test_volume_attachment.py | 47 +++++++ 3 files changed, 212 insertions(+) create mode 100644 openstack/compute/v2/volume_attachment.py create mode 100644 openstack/tests/unit/compute/v2/test_volume_attachment.py diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index fef87f179..6d342d91a 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -22,6 +22,7 @@ from openstack.compute.v2 import server_interface as _server_interface from openstack.compute.v2 import server_ip from openstack.compute.v2 import service as _service +from openstack.compute.v2 import volume_attachment as _volume_attachment from openstack import proxy2 from openstack import resource2 @@ -1120,3 +1121,126 @@ def services(self): """ return self._list(_service.Service, paginated=False) + + def create_volume_attachment(self, server, **attrs): + """Create a new volume attachment from attributes + + :param server: The server can be either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance. + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.compute.v2.volume_attachment.VolumeAttachment`, + comprised of the properties on the VolumeAttachment class. + + :returns: The results of volume attachment creation + :rtype: + :class:`~openstack.compute.v2.volume_attachment.VolumeAttachment` + """ + server_id = resource2.Resource._get_id(server) + return self._create(_volume_attachment.VolumeAttachment, + server_id=server_id, **attrs) + + def update_volume_attachment(self, volume_attachment, server, + **attrs): + """update a volume attachment + + :param volume_attachment: + The value can be either the ID of a volume attachment or a + :class:`~openstack.compute.v2.volume_attachment.VolumeAttachment` + instance. + :param server: This parameter need to be specified when + VolumeAttachment ID is given as value. It can be + either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` + instance that the attachment belongs to. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the volume attachment does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent volume attachment. + + :returns: ``None`` + """ + server_id = self._get_uri_attribute(volume_attachment, server, + "server_id") + volume_attachment = resource2.Resource._get_id(volume_attachment) + + return self._update(_volume_attachment.VolumeAttachment, + attachment_id=volume_attachment, + server_id=server_id) + + def delete_volume_attachment(self, volume_attachment, server, + ignore_missing=True): + """Delete a volume attachment + + :param volume_attachment: + The value can be either the ID of a volume attachment or a + :class:`~openstack.compute.v2.volume_attachment.VolumeAttachment` + instance. + :param server: This parameter need to be specified when + VolumeAttachment ID is given as value. It can be either + the ID of a server or a + :class:`~openstack.compute.v2.server.Server` + instance that the attachment belongs to. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the volume attachment does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent volume attachment. + + :returns: ``None`` + """ + server_id = self._get_uri_attribute(volume_attachment, server, + "server_id") + volume_attachment = resource2.Resource._get_id(volume_attachment) + + self._delete(_volume_attachment.VolumeAttachment, + attachment_id=volume_attachment, + server_id=server_id, + ignore_missing=ignore_missing) + + def get_volume_attachment(self, volume_attachment, server, + ignore_missing=True): + """Get a single volume attachment + + :param volume_attachment: + The value can be the ID of a volume attachment or a + :class:`~openstack.compute.v2.volume_attachment.VolumeAttachment` + instance. + :param server: This parameter need to be specified when + VolumeAttachment ID is given as value. It can be either + the ID of a server or a + :class:`~openstack.compute.v2.server.Server` + instance that the attachment belongs to. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the volume attachment does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent volume attachment. + + :returns: One + :class:`~openstack.compute.v2.volume_attachment.VolumeAttachment` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + server_id = self._get_uri_attribute(volume_attachment, server, + "server_id") + volume_attachment = resource2.Resource._get_id(volume_attachment) + + return self._get(_volume_attachment.VolumeAttachment, + server_id=server_id, + attachment_id=volume_attachment, + ignore_missing=ignore_missing) + + def volume_attachments(self, server): + """Return a generator of volume attachments + + :param server: The server can be either the ID of a server or a + :class:`~openstack.compute.v2.server.Server`. + + :returns: A generator of VolumeAttachment objects + :rtype: + :class:`~openstack.compute.v2.volume_attachment.VolumeAttachment` + """ + server_id = resource2.Resource._get_id(server) + return self._list(_volume_attachment.VolumeAttachment, paginated=False, + server_id=server_id) diff --git a/openstack/compute/v2/volume_attachment.py b/openstack/compute/v2/volume_attachment.py new file mode 100644 index 000000000..44ae47f61 --- /dev/null +++ b/openstack/compute/v2/volume_attachment.py @@ -0,0 +1,41 @@ +# 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.compute import compute_service +from openstack import resource2 + + +class VolumeAttachment(resource2.Resource): + resource_key = 'volumeAttachment' + resources_key = 'volumeAttachments' + base_path = '/servers/%(server_id)s/os-volume_attachments' + service = compute_service.ComputeService() + + # capabilities + allow_create = True + allow_get = True + allow_update = False + allow_delete = True + allow_list = True + + _query_mapping = resource2.QueryParameters("limit", "offset") + + #: Name of the device such as, /dev/vdb. + device = resource2.Body('device') + #: The ID of the attachment. + id = resource2.Body('id') + #: The ID for the server. + server_id = resource2.URI('server_id') + #: The ID of the attached volume. + volume_id = resource2.Body('volumeId') + #: The ID of the attachment you want to delete or update. + attachment_id = resource2.Body('attachment_id', alternate_id=True) diff --git a/openstack/tests/unit/compute/v2/test_volume_attachment.py b/openstack/tests/unit/compute/v2/test_volume_attachment.py new file mode 100644 index 000000000..87d4cdbd5 --- /dev/null +++ b/openstack/tests/unit/compute/v2/test_volume_attachment.py @@ -0,0 +1,47 @@ +# 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 testtools + +from openstack.compute.v2 import volume_attachment + +EXAMPLE = { + 'device': '1', + 'id': '2', + 'volume_id': '3', +} + + +class TestServerInterface(testtools.TestCase): + + def test_basic(self): + sot = volume_attachment.VolumeAttachment() + self.assertEqual('volumeAttachment', sot.resource_key) + self.assertEqual('volumeAttachments', sot.resources_key) + self.assertEqual('/servers/%(server_id)s/os-volume_attachments', + sot.base_path) + self.assertEqual('compute', sot.service.service_type) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_get) + self.assertFalse(sot.allow_update) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertDictEqual({"limit": "limit", + "offset": "offset", + "marker": "marker"}, + sot._query_mapping._mapping) + + def test_make_it(self): + sot = volume_attachment.VolumeAttachment(**EXAMPLE) + self.assertEqual(EXAMPLE['device'], sot.device) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['volume_id'], sot.volume_id) From 91bffa57e33022b9f3bb6832cf79aa7bbb3417bd Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Thu, 13 Apr 2017 09:43:02 -0400 Subject: [PATCH 1417/3836] Add docs for volume_attachment compute methods Change-Id: I153ebc1ac27efc7d9b5f8f73249b904dfa644bad --- doc/source/users/proxies/compute.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/doc/source/users/proxies/compute.rst b/doc/source/users/proxies/compute.rst index 83b031269..5a5f25364 100644 --- a/doc/source/users/proxies/compute.rst +++ b/doc/source/users/proxies/compute.rst @@ -109,6 +109,17 @@ Service Operations .. automethod:: openstack.compute.v2._proxy.Proxy.disable_service .. automethod:: openstack.compute.v2._proxy.Proxy.force_service_down +Volume Attachment Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.compute.v2._proxy.Proxy + + .. automethod:: openstack.compute.v2._proxy.Proxy.create_volume_attachment + .. automethod:: openstack.compute.v2._proxy.Proxy.update_volume_attachment + .. automethod:: openstack.compute.v2._proxy.Proxy.delete_volume_attachment + .. automethod:: openstack.compute.v2._proxy.Proxy.get_volume_attachment + .. automethod:: openstack.compute.v2._proxy.Proxy.volume_attachments + Keypair Operations ^^^^^^^^^^^^^^^^^^ From 9cd5bf900437101476e447d4e15eddf144199a23 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 13 Apr 2017 09:06:39 -0500 Subject: [PATCH 1418/3836] Use keystone_session in _get_raw_client We're bypassing our local session object when we construct the adapter and instead getting a raw one from occ. This becomes aparent when we try to do things to our local one and they don't show up in the one used by the adapter. Change-Id: I91673527ebe1e1446dfc6efb8c495694a4a65c6b --- shade/openstackcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index dbf8b2034..663283bd9 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -353,7 +353,7 @@ def _get_client( def _get_raw_client(self, service_key): return _adapter.ShadeAdapter( manager=self.manager, - session=self.cloud_config.get_session(), + session=self.keystone_session, service_type=self.cloud_config.get_service_type(service_key), service_name=self.cloud_config.get_service_name(service_key), interface=self.cloud_config.get_interface(service_key), From 5e883940ad1ed8ddd75bfa8519efce9f7b35afe8 Mon Sep 17 00:00:00 2001 From: "Yuanbin.Chen" Date: Mon, 10 Apr 2017 18:13:12 +0800 Subject: [PATCH 1419/3836] Add is_profile_only to Cluster resource Additionally, this adds is_profile_only to the query parameters. It was previously listed as `profile_only` so they'll both live together until 1.0, after which we remove the `profile_only` one and keep the prefixed one. Closes-Bug:1681385 Change-Id: I97793af0b0007b4c71e71cfac172533501da8225 Signed-off-by: Yuanbin.Chen --- openstack/cluster/v1/cluster.py | 4 +++- openstack/tests/unit/cluster/v1/test_cluster.py | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/openstack/cluster/v1/cluster.py b/openstack/cluster/v1/cluster.py index 9e82c29b9..988c77d2c 100644 --- a/openstack/cluster/v1/cluster.py +++ b/openstack/cluster/v1/cluster.py @@ -30,7 +30,7 @@ class Cluster(resource.Resource): patch_update = True _query_mapping = resource.QueryParameters( - 'name', 'status', 'sort', 'global_project', 'profile_only') + 'name', 'status', 'sort', 'global_project') # Properties #: The name of the cluster. @@ -74,6 +74,8 @@ class Cluster(resource.Resource): node_ids = resource.Body('nodes') #: Name of the profile used by the cluster. profile_name = resource.Body('profile_name') + #: Specify whether the cluster update should only pertain to the profile. + is_profile_only = resource.Body('profile_only', type=bool) #: A dictionary with dependency information of the cluster dependents = resource.Body('dependents', type=dict) diff --git a/openstack/tests/unit/cluster/v1/test_cluster.py b/openstack/tests/unit/cluster/v1/test_cluster.py index 54225e342..9ffb2bc80 100644 --- a/openstack/tests/unit/cluster/v1/test_cluster.py +++ b/openstack/tests/unit/cluster/v1/test_cluster.py @@ -26,6 +26,7 @@ 'min_size': 0, 'name': FAKE_NAME, 'profile_id': 'myserver', + 'profile_only': True, 'metadata': {}, 'dependents': {}, 'timeout': None, @@ -99,6 +100,15 @@ def test_instantiate(self): self.assertEqual(FAKE['created_at'], sot.created_at) self.assertEqual(FAKE['updated_at'], sot.updated_at) self.assertEqual(FAKE['dependents'], sot.dependents) + self.assertTrue(sot.is_profile_only) + + self.assertDictEqual({"limit": "limit", + "marker": "marker", + "name": "name", + "status": "status", + "sort": "sort", + "global_project": "global_project"}, + sot._query_mapping._mapping) def test_scale_in(self): sot = cluster.Cluster(**FAKE) From b835543f65a23c76224c9558bbda0a19f3be938d Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Wed, 5 Apr 2017 10:37:47 -0400 Subject: [PATCH 1420/3836] Enable warnings_as_errors in doc enforcer Rather than occasionally playing catch-up until the wait_for methods are removed from the base Proxy in 1.0, we can temporarily ignore those two names. This allows us to make the doc build fail if undocumented proxy methods occur. After 1.0 we should remove the special cases for wait_for names, but leave warnings_as_errors on. Change-Id: I8fe41ed639ec318a18db3022371b52382c53aa99 --- doc/source/conf.py | 2 +- doc/source/enforcer.py | 26 +++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 8dee8f737..6c4cbcf16 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -30,7 +30,7 @@ ] # When True, this will raise an exception that kills sphinx-build. -enforcer_warnings_as_errors = False +enforcer_warnings_as_errors = True # autodoc generation is a bit aggressive and a nuisance when doing heavy # text edit cycles. diff --git a/doc/source/enforcer.py b/doc/source/enforcer.py index f75f9a307..8f653b099 100644 --- a/doc/source/enforcer.py +++ b/doc/source/enforcer.py @@ -1,4 +1,5 @@ import importlib +import itertools import os from bs4 import BeautifulSoup @@ -10,6 +11,13 @@ WRITTEN_METHODS = set() +# NOTE: This is temporary! These methods currently exist on the base +# Proxy class as public methods, but they're deprecated in favor of +# subclasses actually exposing them if necessary. However, as they're +# public and purposely undocumented, they cause spurious warnings. +# Ignore these methods until they're actually removed from the API, +# and then we can take this special case out. +IGNORED_METHODS = ("wait_for_delete", "wait_for_status") class EnforcementError(errors.SphinxError): """A mismatch between what exists and what's documented""" @@ -47,6 +55,11 @@ def get_proxy_methods(): instance = module.Proxy("") # We only document public names names = [name for name in dir(instance) if not name.startswith("_")] + + # Remove the wait_for_* names temporarily. + for name in IGNORED_METHODS: + names.remove(name) + good_names = [module.__name__ + ".Proxy." + name for name in names] methods.update(good_names) @@ -94,6 +107,17 @@ def build_finished(app, exception): app.info("ENFORCER: %d proxy methods exist" % len(all_methods)) app.info("ENFORCER: %d proxy methods written" % len(WRITTEN_METHODS)) missing = all_methods - WRITTEN_METHODS + + def is_ignored(name): + for ignored_name in IGNORED_METHODS: + if ignored_name in name: + return True + return False + + # TEMPORARY: Ignore the wait_for names when determining what is missing. + app.info("ENFORCER: Ignoring wait_for_* names...") + missing = set(itertools.ifilterfalse(is_ignored, missing)) + missing_count = len(missing) app.info("ENFORCER: Found %d missing proxy methods " "in the output" % missing_count) @@ -101,7 +125,7 @@ def build_finished(app, exception): for name in sorted(missing): app.warn("ENFORCER: %s was not included in the output" % name) - if app.config.enforcer_warnings_as_errors: + if app.config.enforcer_warnings_as_errors and missing_count > 0: raise EnforcementError( "There are %d undocumented proxy methods" % missing_count) From 8082cdff7129c999ab0bee3961bb6cf5369e24cf Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 13 Apr 2017 08:59:55 -0500 Subject: [PATCH 1421/3836] Pass shade version info to session user_agent After chatting with jamielennox, the better way to deal with user agent setting in shade is to push it onto the additional user agent stack. Older keystoneauth doesn't have this attribute, so protect it with a hasattr. Change-Id: I35985c834a899a6ef3279dae25eb453d953ff70f --- shade/openstackcloud.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 663283bd9..b679a23fa 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -34,6 +34,7 @@ import keystoneauth1.exceptions import novaclient.exceptions as nova_exceptions +import shade from shade.exc import * # noqa from shade import _adapter from shade._heat import event_utils @@ -544,6 +545,9 @@ def keystone_session(self): if self._keystone_session is None: try: self._keystone_session = self.cloud_config.get_session() + if hasattr(self._keystone_session, 'additional_user_agent'): + self._keystone_session.additional_user_agent.append( + ('shade', shade.__version__)) except Exception as e: raise OpenStackCloudException( "Error authenticating to keystone: %s " % str(e)) From 15d64f2af5d9f45ba91bc7f4f5f48402ab8c339b Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 17 Apr 2017 17:11:00 +0000 Subject: [PATCH 1422/3836] Updated from global requirements Change-Id: I84c807adf93913bc411954c59040ab780e2eb04e --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ae41fbe02..30a7fdd6d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ # 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.0.0 # Apache-2.0 +pbr!=2.1.0,>=2.0.0 # Apache-2.0 munch>=2.1.0 # MIT decorator>=3.4.0 # BSD From dc6ed6965537ab3f3bfb4666f5b82676cc7eefb8 Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Thu, 13 Apr 2017 12:04:05 -0700 Subject: [PATCH 1423/3836] Upgrade list volumes tests to use requests-mock Replace the python-cinderclient with a direct REST call for the `list_volumes` method. Change-Id: I2859d73a70a78236c254aa5cf8a18c3e3d93df1e Signed-off-by: Rosario Di Somma --- shade/_normalize.py | 3 +- shade/tests/unit/test_caching.py | 165 +++++++++++++++++++------------ 2 files changed, 103 insertions(+), 65 deletions(-) diff --git a/shade/_normalize.py b/shade/_normalize.py index c94fcca1b..aaaf34b92 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -689,7 +689,8 @@ def _normalize_volume(self, volume): volume.pop('NAME_ATTR', None) volume.pop('HUMAN_ID', None) volume.pop('human_id', None) - + volume.pop('request_ids', None) + volume.pop('x_openstack_request_ids', None) volume_id = volume.pop('id') name = volume.pop('display_name', None) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 18f5cfb55..6b876e643 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -192,91 +192,128 @@ def test_list_servers_no_herd(self, nova_mock): time.sleep(0.001) self.assertEqual(1, nova_mock.servers.list.call_count) - @mock.patch('shade.OpenStackCloud.cinder_client') - def test_list_volumes(self, cinder_mock): + def test_list_volumes(self): fake_volume = fakes.FakeVolume('volume1', 'available', 'Volume 1 Display Name') - fake_volume_dict = self.cloud._normalize_volume( - meta.obj_to_dict(fake_volume)) - cinder_mock.volumes.list.return_value = [fake_volume] - self.assertEqual([fake_volume_dict], self.cloud.list_volumes()) + fake_volume_dict = meta.obj_to_dict(fake_volume) fake_volume2 = fakes.FakeVolume('volume2', 'available', 'Volume 2 Display Name') - fake_volume2_dict = self.cloud._normalize_volume( - meta.obj_to_dict(fake_volume2)) - cinder_mock.volumes.list.return_value = [fake_volume, fake_volume2] - self.assertEqual([fake_volume_dict], self.cloud.list_volumes()) + fake_volume2_dict = meta.obj_to_dict(fake_volume2) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', 'detail']), + json={'volumes': [fake_volume_dict]}), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', 'detail']), + json={'volumes': [fake_volume_dict, fake_volume2_dict]})]) + self.assertEqual( + [self.cloud._normalize_volume(fake_volume_dict)], + self.cloud.list_volumes()) + # this call should hit the cache + self.assertEqual( + [self.cloud._normalize_volume(fake_volume_dict)], + self.cloud.list_volumes()) self.cloud.list_volumes.invalidate(self.cloud) - self.assertEqual([fake_volume_dict, fake_volume2_dict], - self.cloud.list_volumes()) + self.assertEqual( + [self.cloud._normalize_volume(fake_volume_dict), + self.cloud._normalize_volume(fake_volume2_dict)], + self.cloud.list_volumes()) + self.assert_calls() - @mock.patch('shade.OpenStackCloud.cinder_client') - def test_list_volumes_creating_invalidates(self, cinder_mock): + def test_list_volumes_creating_invalidates(self): fake_volume = fakes.FakeVolume('volume1', 'creating', 'Volume 1 Display Name') - fake_volume_dict = self.cloud._normalize_volume( - meta.obj_to_dict(fake_volume)) - cinder_mock.volumes.list.return_value = [fake_volume] - self.assertEqual([fake_volume_dict], self.cloud.list_volumes()) + fake_volume_dict = meta.obj_to_dict(fake_volume) fake_volume2 = fakes.FakeVolume('volume2', 'available', 'Volume 2 Display Name') - fake_volume2_dict = self.cloud._normalize_volume( - meta.obj_to_dict(fake_volume2)) - cinder_mock.volumes.list.return_value = [fake_volume, fake_volume2] - self.assertEqual([fake_volume_dict, fake_volume2_dict], - self.cloud.list_volumes()) - - @mock.patch.object(shade.OpenStackCloud, 'cinder_client') - def test_create_volume_invalidates(self, cinder_mock): + fake_volume2_dict = meta.obj_to_dict(fake_volume2) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', 'detail']), + json={'volumes': [fake_volume_dict]}), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', 'detail']), + json={'volumes': [fake_volume_dict, fake_volume2_dict]})]) + self.assertEqual( + [self.cloud._normalize_volume(fake_volume_dict)], + self.cloud.list_volumes()) + self.assertEqual( + [self.cloud._normalize_volume(fake_volume_dict), + self.cloud._normalize_volume(fake_volume2_dict)], + self.cloud.list_volumes()) + self.assert_calls() + + def test_create_volume_invalidates(self): fake_volb4 = fakes.FakeVolume('volume1', 'available', 'Volume 1 Display Name') - fake_volb4_dict = self.cloud._normalize_volume( - meta.obj_to_dict(fake_volb4)) - cinder_mock.volumes.list.return_value = [fake_volb4] - self.assertEqual([fake_volb4_dict], self.cloud.list_volumes()) + fake_volb4_dict = meta.obj_to_dict(fake_volb4) + fake_vol = fakes.FakeVolume('12345', 'creating', '') + fake_vol_dict = meta.obj_to_dict(fake_vol) + + def now_available(request, context): + fake_vol.status = 'available' + fake_vol_dict['status'] = 'available' + return {'volume': fake_vol_dict} + + def now_deleting(request, context): + fake_vol.status = 'deleting' + fake_vol_dict['status'] = 'deleting' + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', 'detail']), + json={'volumes': [fake_volb4_dict]}), + dict(method='POST', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes']), + json={'volume': fake_vol_dict}), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', fake_vol.id]), + json=now_available), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', 'detail']), + json={'volumes': [fake_volb4_dict, fake_vol_dict]}), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', 'detail']), + json={'volumes': [fake_volb4_dict, fake_vol_dict]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', fake_vol.id]), + json=now_deleting), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', 'detail']), + json={'volumes': [fake_volb4_dict]})]) + + self.assertEqual( + [self.cloud._normalize_volume(fake_volb4_dict)], + self.cloud.list_volumes()) volume = dict(display_name='junk_vol', size=1, display_description='test junk volume') - fake_vol = fakes.FakeVolume('12345', 'creating', '') - fake_vol_dict = meta.obj_to_dict(fake_vol) - fake_vol_dict = self.cloud._normalize_volume( - meta.obj_to_dict(fake_vol)) - cinder_mock.volumes.create.return_value = fake_vol - cinder_mock.volumes.list.return_value = [fake_volb4, fake_vol] - - def creating_available(): - def now_available(): - fake_vol.status = 'available' - fake_vol_dict['status'] = 'available' - return mock.DEFAULT - cinder_mock.volumes.list.side_effect = now_available - return mock.DEFAULT - cinder_mock.volumes.list.side_effect = creating_available self.cloud.create_volume(wait=True, timeout=None, **volume) - self.assertTrue(cinder_mock.volumes.create.called) - self.assertEqual(3, cinder_mock.volumes.list.call_count) # If cache was not invalidated, we would not see our own volume here # because the first volume was available and thus would already be # cached. - self.assertEqual([fake_volb4_dict, fake_vol_dict], - self.cloud.list_volumes()) - + self.assertEqual( + [self.cloud._normalize_volume(fake_volb4_dict), + self.cloud._normalize_volume(fake_vol_dict)], + self.cloud.list_volumes()) + self.cloud.delete_volume(fake_vol.id) # And now delete and check same thing since list is cached as all # available - fake_vol.status = 'deleting' - fake_vol_dict = meta.obj_to_dict(fake_vol) - - def deleting_gone(): - def now_gone(): - cinder_mock.volumes.list.return_value = [fake_volb4] - return mock.DEFAULT - cinder_mock.volumes.list.side_effect = now_gone - return mock.DEFAULT - cinder_mock.volumes.list.return_value = [fake_volb4, fake_vol] - cinder_mock.volumes.list.side_effect = deleting_gone - cinder_mock.volumes.delete.return_value = fake_vol_dict - self.cloud.delete_volume('12345') - self.assertEqual([fake_volb4_dict], self.cloud.list_volumes()) + self.assertEqual( + [self.cloud._normalize_volume(fake_volb4_dict)], + self.cloud.list_volumes()) + self.assert_calls() def test_list_users(self): self._add_discovery_uri_call() From 64b28d42eda595a6fb4ee8b46d93cd61e612aae1 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 2 Apr 2017 11:22:34 -0500 Subject: [PATCH 1424/3836] Add ability to pass in user_agent keystoneauth supports adding a user_agent info to the Session and Adapter via app_name. Allow users to add app_name/app_name and versions as desired. Also, add os-client-config into additional_user_agent. As an example, once this is landed and plumbed through shade, nodepool will set app_name='nodepool' and we'll have: User-Agent: nodepool/0.4.0 os-client-config/1.26.1 shade/1.19.1 keystoneauth1/2.18.0 python-requests/2.13.0 CPython/2.7.12 Change-Id: I1eb4dbd2587dcbe297b5c060c3c34b68ef51ef5e --- os_client_config/__init__.py | 19 +++++++++++++++---- os_client_config/cloud_config.py | 11 ++++++++++- os_client_config/config.py | 5 +++++ os_client_config/tests/base.py | 2 ++ os_client_config/tests/test_cloud_config.py | 18 ++++++++++++++++-- 5 files changed, 48 insertions(+), 7 deletions(-) diff --git a/os_client_config/__init__.py b/os_client_config/__init__.py index a36a13061..1f1266ce1 100644 --- a/os_client_config/__init__.py +++ b/os_client_config/__init__.py @@ -23,9 +23,14 @@ __version__ = pbr.version.VersionInfo('os_client_config').version_string() -def get_config(service_key=None, options=None, **kwargs): +def get_config( + service_key=None, options=None, + app_name=None, app_version=None, + **kwargs): load_yaml_config = kwargs.pop('load_yaml_config', True) - config = OpenStackConfig(load_yaml_config=load_yaml_config) + config = OpenStackConfig( + load_yaml_config=load_yaml_config, + app_name=app_name, app_version=app_version) if options: config.register_argparse_arguments(options, sys.argv, service_key) parsed_options = options.parse_known_args(sys.argv) @@ -35,7 +40,10 @@ def get_config(service_key=None, options=None, **kwargs): return config.get_one_cloud(options=parsed_options, **kwargs) -def make_rest_client(service_key, options=None, **kwargs): +def make_rest_client( + service_key, options=None, + app_name=None, app_version=None, + **kwargs): """Simple wrapper function. It has almost no features. This will get you a raw requests Session Adapter that is mounted @@ -48,7 +56,10 @@ def make_rest_client(service_key, options=None, **kwargs): get_session_client on it. This function is to make it easy to poke at OpenStack REST APIs with a properly configured keystone session. """ - cloud = get_config(service_key=service_key, options=options, **kwargs) + cloud = get_config( + service_key=service_key, options=options, + app_name=app_name, app_version=app_version, + **kwargs) return cloud.get_session_client(service_key) # Backwards compat - simple_client was a terrible name simple_client = make_rest_client diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 45bb82599..086092488 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -21,6 +21,7 @@ from keystoneauth1 import session import requestsexceptions +import os_client_config from os_client_config import _log from os_client_config import constructors from os_client_config import exceptions @@ -71,7 +72,8 @@ def _make_key(key, service_type): class CloudConfig(object): def __init__(self, name, region, config, force_ipv4=False, auth_plugin=None, - openstack_config=None, session_constructor=None): + openstack_config=None, session_constructor=None, + app_name=None, app_version=None): self.name = name self.region = region self.config = config @@ -81,6 +83,8 @@ def __init__(self, name, region, config, self._openstack_config = openstack_config self._keystone_session = None self._session_constructor = session_constructor or session.Session + self._app_name = app_name + self._app_version = app_version def __getattr__(self, key): """Return arbitrary attributes.""" @@ -211,9 +215,14 @@ def get_session(self): requestsexceptions.squelch_warnings(insecure_requests=not verify) self._keystone_session = self._session_constructor( auth=self._auth, + app_name=self._app_name, + app_version=self._app_version, verify=verify, cert=cert, timeout=self.config['api_timeout']) + if hasattr(self._keystone_session, 'additional_user_agent'): + self._keystone_session.additional_user_agent.append( + ('os-client-config', os_client_config.__version__)) return self._keystone_session def get_session_client(self, service_key): diff --git a/os_client_config/config.py b/os_client_config/config.py index 64e3a13e6..89b5c6ccf 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -174,9 +174,12 @@ def __init__(self, config_files=None, vendor_files=None, override_defaults=None, force_ipv4=None, envvar_prefix=None, secure_files=None, pw_func=None, session_constructor=None, + app_name=None, app_version=None, load_yaml_config=True): self.log = _log.setup_logging(__name__) self._session_constructor = session_constructor + self._app_name = app_name + self._app_version = app_version if load_yaml_config: self._config_files = config_files or CONFIG_FILES @@ -1088,6 +1091,8 @@ def get_one_cloud(self, cloud=None, validate=True, auth_plugin=auth_plugin, openstack_config=self, session_constructor=self._session_constructor, + app_name=self._app_name, + app_version=self._app_version, ) def get_one_cloud_osc( diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py index 85a42c593..9710782d4 100644 --- a/os_client_config/tests/base.py +++ b/os_client_config/tests/base.py @@ -212,6 +212,8 @@ def setUp(self): self.secure_yaml = _write_yaml(SECURE_CONF) self.vendor_yaml = _write_yaml(VENDOR_CONF) self.no_yaml = _write_yaml(NO_CONF) + self.useFixture(fixtures.MonkeyPatch( + 'os_client_config.__version__', '1.2.3')) # Isolate the test runs from the environment # Do this as two loops because you can't modify the dict in a loop diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index cb6d91c3d..0671fcc8e 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -179,15 +179,25 @@ def test_get_session_no_auth(self): def test_get_session(self, mock_session): config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) + fake_session = mock.Mock() + fake_session.additional_user_agent = [] + mock_session.return_value = fake_session cc = cloud_config.CloudConfig( "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_session() mock_session.assert_called_with( auth=mock.ANY, - verify=True, cert=None, timeout=None) + verify=True, cert=None, timeout=None, + app_name=None, app_version=None) + self.assertEqual( + fake_session.additional_user_agent, + [('os-client-config', '1.2.3')]) @mock.patch.object(ksa_session, 'Session') def test_get_session_with_timeout(self, mock_session): + fake_session = mock.Mock() + fake_session.additional_user_agent = [] + mock_session.return_value = fake_session config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) config_dict['api_timeout'] = 9 @@ -196,7 +206,11 @@ def test_get_session_with_timeout(self, mock_session): cc.get_session() mock_session.assert_called_with( auth=mock.ANY, - verify=True, cert=None, timeout=9) + verify=True, cert=None, timeout=9, + app_name=None, app_version=None) + self.assertEqual( + fake_session.additional_user_agent, + [('os-client-config', '1.2.3')]) @mock.patch.object(ksa_session, 'Session') def test_override_session_endpoint_override(self, mock_session): From 5adcef04eb6f42d80c93a8875a2090cfd424aad5 Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Tue, 18 Apr 2017 07:22:01 -0700 Subject: [PATCH 1425/3836] Use REST for cinder list volumes Change-Id: Ifacc5fac67f08b62886ac64652996263d6642cfd Signed-off-by: Rosario Di Somma --- shade/_tasks.py | 5 ----- shade/openstackcloud.py | 5 ++--- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index d0975b3b9..3bd767faf 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -294,11 +294,6 @@ def main(self, client): client.cinder_client.volumes.delete(**self.args) -class VolumeList(task_manager.Task): - def main(self, client): - return client.cinder_client.volumes.list() - - class VolumeDetach(task_manager.Task): def main(self, client): client.nova_client.volumes.delete_server_volume(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index ebab6b892..9ab3b504e 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1728,9 +1728,8 @@ def list_volumes(self, cache=True): if not cache: warnings.warn('cache argument to list_volumes is deprecated. Use ' 'invalidate instead.') - with _utils.shade_exceptions("Error fetching volume list"): - return self._normalize_volumes( - self.manager.submit_task(_tasks.VolumeList())) + return self._normalize_volumes( + self._volume_client.get('/volumes/detail')) @_utils.cache_on_arguments() def list_volume_types(self, get_extra=True): From 0cbed4fbafb7d9adff2f43a9dba342084aec80d6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 12 Apr 2017 09:59:25 -0500 Subject: [PATCH 1426/3836] Pass in app_name information to keystoneauth keystoneauth (and os-client-config) support passing through app_name and app_version. These affect the user-agent string. Allow our users to set app_name and app_version. So as not to cause an os-client-config minimum bump, check the arg list of the occ constructor and only pass in app_name/app_version if it accepts them. (This is informative and neat, but not, you know, essential) Change-Id: I515f93168adf35adc2521339255369b1ccfdfe0a --- shade/__init__.py | 32 ++++++++++++++++++++++++++------ shade/openstackcloud.py | 9 ++++++++- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 89133cb72..568ba868d 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -32,6 +32,22 @@ 'ignore', category=requestsexceptions.SubjectAltNameWarning) +def _get_openstack_config(app_name=None, app_version=None): + # Protect against older versions of os-client-config that don't expose this + kwargs = {} + try: + init = os_client_config.OpenStackConfig.__init__ + if 'app_name' in init.im_func.func_code.co_varnames: + kwargs['app_name'] = app_name + kwargs['app_version'] = app_version + except AttributeError: + # If we get an attribute error, it's actually likely some mocking issue + # but basically nothing about this is important enough to break things + # for someone. + pass + return os_client_config.OpenStackConfig(**kwargs) + + def simple_logging(debug=False, http_debug=False): if http_debug: debug = True @@ -55,9 +71,11 @@ def simple_logging(debug=False, http_debug=False): log = _log.setup_logging('keystoneauth.identity.generic.base') -def openstack_clouds(config=None, debug=False, cloud=None, strict=False): +def openstack_clouds( + config=None, debug=False, cloud=None, strict=False, + app_name=None, app_version=None): if not config: - config = os_client_config.OpenStackConfig() + config = _get_openstack_config(app_name, app_version) try: if cloud is None: return [ @@ -83,9 +101,10 @@ def openstack_clouds(config=None, debug=False, cloud=None, strict=False): "Invalid cloud configuration: {exc}".format(exc=str(e))) -def openstack_cloud(config=None, strict=False, **kwargs): +def openstack_cloud( + config=None, strict=False, app_name=None, app_version=None, **kwargs): if not config: - config = os_client_config.OpenStackConfig() + config = _get_openstack_config(app_name, app_version) try: cloud_config = config.get_one_cloud(**kwargs) except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin as e: @@ -94,11 +113,12 @@ def openstack_cloud(config=None, strict=False, **kwargs): return OpenStackCloud(cloud_config=cloud_config, strict=strict) -def operator_cloud(config=None, strict=False, **kwargs): +def operator_cloud( + config=None, strict=False, app_name=None, app_version=None, **kwargs): if 'interface' not in kwargs: kwargs['interface'] = 'admin' if not config: - config = os_client_config.OpenStackConfig() + config = _get_openstack_config(app_name, app_version) try: cloud_config = config.get_one_cloud(**kwargs) except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin as e: diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index ebab6b892..cc14d42d0 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -117,6 +117,10 @@ class OpenStackCloud(_normalize.Normalizer): will enable that behavior. :param bool strict: Only return documented attributes for each resource as per the shade Data Model contract. (Default False) + :param app_name: Name of the application to be appended to the user-agent + string. Optional, defaults to None. + :param app_version: Version of the application to be appended to the + user-agent string. Optional, defaults to None. :param CloudConfig cloud_config: Cloud config object from os-client-config In the future, this will be the only way to pass in cloud configuration, but is @@ -128,6 +132,8 @@ def __init__( cloud_config=None, manager=None, log_inner_exceptions=False, strict=False, + app_name=None, + app_version=None, **kwargs): if log_inner_exceptions: @@ -136,7 +142,8 @@ def __init__( self.log = _log.setup_logging('shade') if not cloud_config: - config = os_client_config.OpenStackConfig() + config = os_client_config.OpenStackConfig( + app_name=app_name, app_version=app_version) cloud_config = config.get_one_cloud(**kwargs) From dfec0ca2afe4035c62d3f3e99b5ab973e9dbb698 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 18 Apr 2017 04:32:29 -0500 Subject: [PATCH 1427/3836] Remove dead ImageSnapshotCreate task We removed the use of this already. Remove the task. Change-Id: Iba30a78f591257ba7a10f90b823f95cc91bb98b6 --- shade/_tasks.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index d0975b3b9..5fbce9f2a 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -257,11 +257,6 @@ def main(self, client): return client.nova_client.images.list() -class ImageSnapshotCreate(task_manager.Task): - def main(self, client): - return client.nova_client.servers.create_image(**self.args) - - class VolumeTypeList(task_manager.Task): def main(self, client): return client.cinder_client.volume_types.list() From 8d94ef15a485ef83cda4f42ecea78758c1b228f0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 18 Apr 2017 05:01:48 -0500 Subject: [PATCH 1428/3836] Transition nova security group tests to REST novaclient v8 doesn't support these calls anymore. Transition them. A few fixes in the non-test areas related to normalization. Also, we allow users to pass in a dict to bypass server lookup, but then we assume an object when we use it. mocking the clients hides terible things. Yay requests_mock. Change-Id: I39952800414058d7f8a77dfca7610bc8c1fcfd18 --- shade/_normalize.py | 8 + shade/openstackcloud.py | 8 +- shade/tests/unit/test_security_groups.py | 394 +++++++++++++---------- 3 files changed, 245 insertions(+), 165 deletions(-) diff --git a/shade/_normalize.py b/shade/_normalize.py index c94fcca1b..f3413f821 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -297,6 +297,14 @@ def _normalize_secgroup(self, group): # Copy incoming group because of shared dicts in unittests group = group.copy() + # Remove novaclient artifacts + group.pop('links', None) + group.pop('NAME_ATTR', None) + group.pop('HUMAN_ID', None) + group.pop('human_id', None) + group.pop('request_ids', None) + group.pop('x_openstack_request_ids', None) + rules = self._normalize_secgroup_rules( group.pop('security_group_rules', group.pop('rules', []))) project_id = group.pop('tenant_id', '') diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index cc14d42d0..871e8051b 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1886,7 +1886,7 @@ def add_server_security_groups(self, server, security_groups): for sg in security_groups: self._compute_client.post( - '/servers/%s/action' % server.id, + '/servers/%s/action' % server['id'], json={'addSecurityGroup': {'name': sg.name}}) return True @@ -1912,7 +1912,7 @@ def remove_server_security_groups(self, server, security_groups): for sg in security_groups: try: self._compute_client.post( - '/servers/%s/action' % server.id, + '/servers/%s/action' % server['id'], json={'removeSecurityGroup': {'name': sg.name}}) except OpenStackCloudURINotFound as e: @@ -6826,6 +6826,8 @@ def delete_security_group(self, name_or_id): "Unavailable feature: security groups" ) + # TODO(mordred): Let's come back and stop doing a GET before we do + # the delete. secgroup = self.get_security_group(name_or_id) if secgroup is None: self.log.debug('Security group %s not found for deleting', @@ -6888,6 +6890,8 @@ def update_security_group(self, name_or_id, **kwargs): with _utils.shade_exceptions( "Failed to update security group '{group}'".format( group=name_or_id)): + for key in ('name', 'description'): + kwargs.setdefault(key, group[key]) group = self.manager.submit_task( _tasks.NovaSecurityGroupUpdate( group=group['id'], **kwargs) diff --git a/shade/tests/unit/test_security_groups.py b/shade/tests/unit/test_security_groups.py index 5ce6765d0..0578b21a8 100644 --- a/shade/tests/unit/test_security_groups.py +++ b/shade/tests/unit/test_security_groups.py @@ -11,10 +11,8 @@ # under the License. -import copy import mock -from novaclient import exceptions as nova_exc from neutronclient.common import exceptions as neutron_exc import shade @@ -22,6 +20,10 @@ from shade.tests.unit import base from shade.tests import fakes +# TODO(mordred): Make a fakes.make_fake_nova_security_group and a +# fakes.make_fake_nova_security_group and remove all uses of +# meta.obj_to_dict here. Also, we have hardcoded id names - +# move those to using a getUniqueString() value. neutron_grp_obj = fakes.FakeSecgroup( id='1', @@ -51,7 +53,7 @@ nova_grp_dict = meta.obj_to_dict(nova_grp_obj) -class TestSecurityGroups(base.TestCase): +class TestSecurityGroups(base.RequestsMockTestCase): def setUp(self): super(TestSecurityGroups, self).setUp() @@ -62,34 +64,31 @@ def fake_has_service(*args, **kwargs): self.cloud.has_service = fake_has_service @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_list_security_groups_neutron(self, mock_nova, mock_neutron): + def test_list_security_groups_neutron(self, mock_neutron): self.cloud.secgroup_source = 'neutron' self.cloud.list_security_groups(filters={'project_id': 42}) mock_neutron.list_security_groups.assert_called_once_with( project_id=42) - self.assertFalse(mock_nova.security_groups.list.called) - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_list_security_groups_nova(self, mock_nova, mock_neutron): + def test_list_security_groups_nova(self): + self.register_uris([ + dict(method='GET', + uri='{endpoint}/os-security-groups?project_id=42'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'security_groups': []}), + ]) self.cloud.secgroup_source = 'nova' self.has_neutron = False self.cloud.list_security_groups(filters={'project_id': 42}) - self.assertFalse(mock_neutron.list_security_groups.called) - mock_nova.security_groups.list.assert_called_once_with( - search_opts={'project_id': 42} - ) - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_list_security_groups_none(self, mock_nova, mock_neutron): + self.assert_calls() + + def test_list_security_groups_none(self): + self.cloud.secgroup_source = None self.has_neutron = False self.assertRaises(shade.OpenStackCloudUnavailableFeature, self.cloud.list_security_groups) - self.assertFalse(mock_neutron.list_security_groups.called) - self.assertFalse(mock_nova.security_groups.list.called) @mock.patch.object(shade.OpenStackCloud, 'neutron_client') def test_delete_security_group_neutron(self, mock_neutron): @@ -101,16 +100,21 @@ def test_delete_security_group_neutron(self, mock_neutron): security_group='1' ) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_delete_security_group_nova(self, mock_nova): + def test_delete_security_group_nova(self): self.cloud.secgroup_source = 'nova' self.has_neutron = False - nova_return = [nova_grp_obj] - mock_nova.security_groups.list.return_value = nova_return + nova_return = [nova_grp_dict] + self.register_uris([ + dict(method='GET', + uri='{endpoint}/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'security_groups': nova_return}), + dict(method='DELETE', + uri='{endpoint}/os-security-groups/2'.format( + endpoint=fakes.COMPUTE_ENDPOINT)), + ]) self.cloud.delete_security_group('2') - mock_nova.security_groups.delete.assert_called_once_with( - group='2' - ) + self.assert_calls() @mock.patch.object(shade.OpenStackCloud, 'neutron_client') def test_delete_security_group_neutron_not_found(self, mock_neutron): @@ -120,24 +124,23 @@ def test_delete_security_group_neutron_not_found(self, mock_neutron): self.cloud.delete_security_group('doesNotExist') self.assertFalse(mock_neutron.delete_security_group.called) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_delete_security_group_nova_not_found(self, mock_nova): + def test_delete_security_group_nova_not_found(self): self.cloud.secgroup_source = 'nova' self.has_neutron = False - nova_return = [nova_grp_obj] - mock_nova.security_groups.list.return_value = nova_return - self.cloud.delete_security_group('doesNotExist') - self.assertFalse(mock_nova.security_groups.delete.called) + nova_return = [nova_grp_dict] + self.register_uris([ + dict(method='GET', + uri='{endpoint}/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'security_groups': nova_return}), + ]) + self.assertFalse(self.cloud.delete_security_group('doesNotExist')) - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_delete_security_group_none(self, mock_nova, mock_neutron): + def test_delete_security_group_none(self): self.cloud.secgroup_source = None self.assertRaises(shade.OpenStackCloudUnavailableFeature, self.cloud.delete_security_group, 'doesNotExist') - self.assertFalse(mock_neutron.delete_security_group.called) - self.assertFalse(mock_nova.security_groups.delete.called) @mock.patch.object(shade.OpenStackCloud, 'neutron_client') def test_create_security_group_neutron(self, mock_neutron): @@ -150,35 +153,45 @@ def test_create_security_group_neutron(self, mock_neutron): description=group_desc)) ) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_create_security_group_nova(self, mock_nova): + def test_create_security_group_nova(self): group_name = self.getUniqueString() self.has_neutron = False group_desc = 'security group from test_create_security_group_neutron' - new_group = fakes.FakeSecgroup(id='2', - name=group_name, - description=group_desc, - rules=[]) + new_group = meta.obj_to_dict( + fakes.FakeSecgroup( + id='2', + name=group_name, + description=group_desc, + rules=[])) + self.register_uris([ + dict(method='POST', + uri='{endpoint}/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'security_group': new_group}, + validate=dict(json={ + 'security_group': { + 'name': group_name, + 'description': group_desc, + }})), + dict(method='GET', + uri='{endpoint}/os-security-groups/2'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'security_group': new_group}), + ]) - mock_nova.security_groups.create.return_value = new_group self.cloud.secgroup_source = 'nova' r = self.cloud.create_security_group(group_name, group_desc) - mock_nova.security_groups.create.assert_called_once_with( - name=group_name, description=group_desc - ) self.assertEqual(group_name, r['name']) self.assertEqual(group_desc, r['description']) - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_create_security_group_none(self, mock_nova, mock_neutron): + self.assert_calls() + + def test_create_security_group_none(self): self.cloud.secgroup_source = None self.has_neutron = False self.assertRaises(shade.OpenStackCloudUnavailableFeature, self.cloud.create_security_group, '', '') - self.assertFalse(mock_neutron.create_security_group.called) - self.assertFalse(mock_nova.security_groups.create.called) @mock.patch.object(shade.OpenStackCloud, 'neutron_client') def test_update_security_group_neutron(self, mock_neutron): @@ -191,30 +204,37 @@ def test_update_security_group_neutron(self, mock_neutron): body={'security_group': {'name': 'new_name'}} ) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_update_security_group_nova(self, mock_nova): + def test_update_security_group_nova(self): self.has_neutron = False new_name = self.getUniqueString() self.cloud.secgroup_source = 'nova' - nova_return = [nova_grp_obj] - update_return = copy.deepcopy(nova_grp_obj) - update_return.name = new_name - mock_nova.security_groups.list.return_value = nova_return - mock_nova.security_groups.update.return_value = update_return + nova_return = [nova_grp_dict] + update_return = meta.obj_to_dict(nova_grp_obj) + update_return['name'] = new_name + + self.register_uris([ + dict(method='GET', + uri='{endpoint}/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'security_groups': nova_return}), + dict(method='PUT', + uri='{endpoint}/os-security-groups/2'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'security_group': update_return}), + dict(method='GET', + uri='{endpoint}/os-security-groups/2'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'security_group': update_return}), + ]) + r = self.cloud.update_security_group(nova_grp_obj.id, name=new_name) - mock_nova.security_groups.update.assert_called_once_with( - group=nova_grp_obj.id, name=new_name - ) self.assertEqual(r['name'], new_name) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_update_security_group_bad_kwarg(self, mock_nova, mock_neutron): + def test_update_security_group_bad_kwarg(self): self.assertRaises(TypeError, self.cloud.update_security_group, 'doesNotExist', bad_arg='') - self.assertFalse(mock_neutron.create_security_group.called) - self.assertFalse(mock_nova.security_groups.create.called) @mock.patch.object(shade.OpenStackCloud, 'get_security_group') @mock.patch.object(shade.OpenStackCloud, 'neutron_client') @@ -241,59 +261,87 @@ def test_create_security_group_rule_neutron(self, mock_neutron, mock_get): body={'security_group_rule': args} ) - @mock.patch.object(shade.OpenStackCloud, 'get_security_group') - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_create_security_group_rule_nova(self, mock_nova, mock_get): + def test_create_security_group_rule_nova(self): self.has_neutron = False self.cloud.secgroup_source = 'nova' - new_rule = fakes.FakeNovaSecgroupRule( + nova_return = [nova_grp_dict] + + new_rule = meta.obj_to_dict(fakes.FakeNovaSecgroupRule( id='xyz', from_port=1, to_port=2000, ip_protocol='tcp', - cidr='1.2.3.4/32') - mock_nova.security_group_rules.create.return_value = new_rule - mock_get.return_value = {'id': 'abc'} + cidr='1.2.3.4/32')) + + self.register_uris([ + dict(method='GET', + uri='{endpoint}/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'security_groups': nova_return}), + dict(method='POST', + uri='{endpoint}/os-security-group-rules'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'security_group_rule': new_rule}, + validate=dict(json={ + "security_group_rule": { + "from_port": 1, + "ip_protocol": "tcp", + "to_port": 2000, + "parent_group_id": "2", + "cidr": "1.2.3.4/32", + "group_id": "123"}})), + ]) self.cloud.create_security_group_rule( - 'abc', port_range_min=1, port_range_max=2000, protocol='tcp', + '2', port_range_min=1, port_range_max=2000, protocol='tcp', remote_ip_prefix='1.2.3.4/32', remote_group_id='123') - mock_nova.security_group_rules.create.assert_called_once_with( - parent_group_id='abc', ip_protocol='tcp', from_port=1, - to_port=2000, cidr='1.2.3.4/32', group_id='123' - ) + self.assert_calls() + + def test_create_security_group_rule_nova_no_ports(self): - @mock.patch.object(shade.OpenStackCloud, 'get_security_group') - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_create_security_group_rule_nova_no_ports(self, - mock_nova, mock_get): self.has_neutron = False self.cloud.secgroup_source = 'nova' new_rule = fakes.FakeNovaSecgroupRule( id='xyz', from_port=1, to_port=65535, ip_protocol='tcp', cidr='1.2.3.4/32') - mock_nova.security_group_rules.create.return_value = new_rule - mock_get.return_value = {'id': 'abc'} + + nova_return = [nova_grp_dict] + + new_rule = meta.obj_to_dict(fakes.FakeNovaSecgroupRule( + id='xyz', from_port=1, to_port=65535, ip_protocol='tcp', + cidr='1.2.3.4/32')) + + self.register_uris([ + dict(method='GET', + uri='{endpoint}/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'security_groups': nova_return}), + dict(method='POST', + uri='{endpoint}/os-security-group-rules'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'security_group_rule': new_rule}, + validate=dict(json={ + "security_group_rule": { + "from_port": 1, + "ip_protocol": "tcp", + "to_port": 65535, + "parent_group_id": "2", + "cidr": "1.2.3.4/32", + "group_id": "123"}})), + ]) self.cloud.create_security_group_rule( - 'abc', protocol='tcp', + '2', protocol='tcp', remote_ip_prefix='1.2.3.4/32', remote_group_id='123') - mock_nova.security_group_rules.create.assert_called_once_with( - parent_group_id='abc', ip_protocol='tcp', from_port=1, - to_port=65535, cidr='1.2.3.4/32', group_id='123' - ) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_create_security_group_rule_none(self, mock_nova, mock_neutron): + def test_create_security_group_rule_none(self): self.has_neutron = False self.cloud.secgroup_source = None self.assertRaises(shade.OpenStackCloudUnavailableFeature, self.cloud.create_security_group_rule, '') - self.assertFalse(mock_neutron.create_security_group.called) - self.assertFalse(mock_nova.security_groups.create.called) @mock.patch.object(shade.OpenStackCloud, 'neutron_client') def test_delete_security_group_rule_neutron(self, mock_neutron): @@ -303,30 +351,27 @@ def test_delete_security_group_rule_neutron(self, mock_neutron): security_group_rule='xyz') self.assertTrue(r) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_delete_security_group_rule_nova(self, mock_nova): + def test_delete_security_group_rule_nova(self): self.has_neutron = False self.cloud.secgroup_source = 'nova' + self.register_uris([ + dict(method='DELETE', + uri='{endpoint}/os-security-group-rules/xyz'.format( + endpoint=fakes.COMPUTE_ENDPOINT)), + ]) r = self.cloud.delete_security_group_rule('xyz') - mock_nova.security_group_rules.delete.assert_called_once_with( - rule='xyz') self.assertTrue(r) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_delete_security_group_rule_none(self, mock_nova, mock_neutron): + def test_delete_security_group_rule_none(self): self.has_neutron = False self.cloud.secgroup_source = None self.assertRaises(shade.OpenStackCloudUnavailableFeature, self.cloud.delete_security_group_rule, '') - self.assertFalse(mock_neutron.create_security_group.called) - self.assertFalse(mock_nova.security_groups.create.called) @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_delete_security_group_rule_not_found(self, - mock_nova, mock_neutron): self.cloud.secgroup_source = 'neutron' mock_neutron.delete_security_group_rule.side_effect = ( @@ -335,59 +380,76 @@ def test_delete_security_group_rule_not_found(self, r = self.cloud.delete_security_group('doesNotExist') self.assertFalse(r) + def test_delete_security_group_rule_not_found_nova(self): self.has_neutron = False self.cloud.secgroup_source = 'nova' - mock_neutron.security_group_rules.delete.side_effect = ( - nova_exc.NotFound("uh oh") - ) + self.register_uris([ + dict(method='GET', + uri='{endpoint}/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'security_groups': [nova_grp_dict]}), + ]) r = self.cloud.delete_security_group('doesNotExist') self.assertFalse(r) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_nova_egress_security_group_rule(self, mock_nova): + self.assert_calls() + + def test_nova_egress_security_group_rule(self): self.has_neutron = False self.cloud.secgroup_source = 'nova' - mock_nova.security_groups.list.return_value = [nova_grp_obj] + self.register_uris([ + dict(method='GET', + uri='{endpoint}/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'security_groups': [nova_grp_dict]}), + ]) self.assertRaises(shade.OpenStackCloudException, self.cloud.create_security_group_rule, secgroup_name_or_id='nova-sec-group', direction='egress') - @mock.patch.object(shade.OpenStackCloud, '_normalize_secgroups') - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_list_server_security_groups_nova(self, mock_nova, mock_norm): + self.assert_calls() + + def test_list_server_security_groups_nova(self): self.has_neutron = False + server = dict(id='server_id') - self.cloud.list_server_security_groups(server) - mock_nova.servers.list_security_group.assert_called_once_with( - server='server_id' - ) - self.assertTrue(mock_norm.called) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_list_server_security_groups_bad_source(self, mock_nova): + self.register_uris([ + dict( + method='GET', + uri='{endpoint}/servers/{id}/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT, + id='server_id'), + json={'security_groups': [nova_grp_dict]}), + ]) + groups = self.cloud.list_server_security_groups(server) + self.assertIn('location', groups[0]) + self.assertEqual( + groups[0]['security_group_rules'][0]['remote_ip_prefix'], + nova_grp_dict['rules'][0]['ip_range']['cidr']) + + self.assert_calls() + + def test_list_server_security_groups_bad_source(self): self.has_neutron = False self.cloud.secgroup_source = 'invalid' server = dict(id='server_id') ret = self.cloud.list_server_security_groups(server) self.assertEqual([], ret) - self.assertFalse(mock_nova.servers.list_security_group.called) - -class TestServerSecurityGroups(base.RequestsMockTestCase): - - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_add_security_group_to_server_nova(self, mock_nova): - # fake to get server by name, server-name must match - fake_server = fakes.FakeServer('1234', 'server-name', 'ACTIVE') - mock_nova.servers.list.return_value = [fake_server] + def test_add_security_group_to_server_nova(self): - # use nova for secgroup list and return an existing fake self.has_neutron = False self.cloud.secgroup_source = 'nova' - mock_nova.security_groups.list.return_value = [nova_grp_obj] self.register_uris([ + dict( + method='GET', + uri='{endpoint}/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT, + id='server_id'), + json={'security_groups': [nova_grp_dict]}), dict( method='POST', uri='%s/servers/%s/action' % (fakes.COMPUTE_ENDPOINT, '1234'), @@ -396,12 +458,10 @@ def test_add_security_group_to_server_nova(self, mock_nova): ), ]) - ret = self.cloud.add_server_security_groups('server-name', - 'nova-sec-group') - self.assertTrue(ret) + ret = self.cloud.add_server_security_groups( + dict(id='1234'), 'nova-sec-group') - self.assertTrue(mock_nova.servers.list.called_once) - self.assertTrue(mock_nova.security_groups.list.called_once) + self.assertTrue(ret) self.assert_calls() @@ -437,18 +497,17 @@ def test_add_security_group_to_server_neutron(self, self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_remove_security_group_from_server_nova(self, mock_nova): - # fake to get server by name, server-name must match - fake_server = fakes.FakeServer('1234', 'server-name', 'ACTIVE') - mock_nova.servers.list.return_value = [fake_server] + def test_remove_security_group_from_server_nova(self): - # use nova for secgroup list and return an existing fake self.has_neutron = False self.cloud.secgroup_source = 'nova' - mock_nova.security_groups.list.return_value = [nova_grp_obj] self.register_uris([ + dict( + method='GET', + uri='{endpoint}/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'security_groups': [nova_grp_dict]}), dict( method='POST', uri='%s/servers/%s/action' % (fakes.COMPUTE_ENDPOINT, '1234'), @@ -456,13 +515,10 @@ def test_remove_security_group_from_server_nova(self, mock_nova): ), ]) - ret = self.cloud.remove_server_security_groups('server-name', - 'nova-sec-group') + ret = self.cloud.remove_server_security_groups( + dict(id='1234'), 'nova-sec-group') self.assertTrue(ret) - self.assertTrue(mock_nova.servers.list.called_once) - self.assertTrue(mock_nova.security_groups.list.called_once) - self.assert_calls() @mock.patch.object(shade.OpenStackCloud, 'nova_client') @@ -497,24 +553,31 @@ def test_remove_security_group_from_server_neutron(self, self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_add_bad_security_group_to_server_nova(self, mock_nova): + def test_add_bad_security_group_to_server_nova(self): # fake to get server by name, server-name must match - fake_server = fakes.FakeServer('1234', 'server-name', 'ACTIVE') - mock_nova.servers.list.return_value = [fake_server] + fake_server = meta.obj_to_dict( + fakes.FakeServer('1234', 'server-name', 'ACTIVE')) # use nova for secgroup list and return an existing fake self.has_neutron = False self.cloud.secgroup_source = 'nova' - mock_nova.security_groups.list.return_value = [nova_grp_obj] + self.register_uris([ + dict( + method='GET', + uri='{endpoint}/servers/detail'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'servers': [fake_server]}), + dict( + method='GET', + uri='{endpoint}/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'security_groups': [nova_grp_dict]}), + ]) ret = self.cloud.add_server_security_groups('server-name', 'unknown-sec-group') self.assertFalse(ret) - self.assertTrue(mock_nova.servers.list.called_once) - self.assertTrue(mock_nova.security_groups.list.called_once) - self.assert_calls() @mock.patch.object(shade.OpenStackCloud, 'nova_client') @@ -540,16 +603,21 @@ def test_add_bad_security_group_to_server_neutron(self, self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_add_security_group_to_bad_server(self, mock_nova): + def test_add_security_group_to_bad_server(self): # fake to get server by name, server-name must match - fake_server = fakes.FakeServer('1234', 'server-name', 'ACTIVE') - mock_nova.servers.list.return_value = [fake_server] + fake_server = meta.obj_to_dict( + fakes.FakeServer('1234', 'server-name', 'ACTIVE')) + + self.register_uris([ + dict( + method='GET', + uri='{endpoint}/servers/detail'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'servers': [fake_server]}), + ]) ret = self.cloud.add_server_security_groups('unknown-server-name', 'nova-sec-group') self.assertFalse(ret) - self.assertTrue(mock_nova.servers.list.called_once) - self.assert_calls() From 26c834c10efbcb338d26ca02fabf79f704e3b6de Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 18 Apr 2017 09:49:29 -0500 Subject: [PATCH 1429/3836] Replace nova security groups with REST One more set of things down. Change-Id: I02f81382693e30513a3fe85c49450f84f8c3d64d --- shade/_tasks.py | 30 --------------- shade/openstackcloud.py | 84 +++++++++++++++++------------------------ 2 files changed, 34 insertions(+), 80 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 5fbce9f2a..a4a9cc965 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -369,36 +369,6 @@ def main(self, client): return client.neutron_client.delete_security_group_rule(**self.args) -class NovaSecurityGroupList(task_manager.Task): - def main(self, client): - return client.nova_client.security_groups.list(**self.args) - - -class NovaSecurityGroupCreate(task_manager.Task): - def main(self, client): - return client.nova_client.security_groups.create(**self.args) - - -class NovaSecurityGroupDelete(task_manager.Task): - def main(self, client): - return client.nova_client.security_groups.delete(**self.args) - - -class NovaSecurityGroupUpdate(task_manager.Task): - def main(self, client): - return client.nova_client.security_groups.update(**self.args) - - -class NovaSecurityGroupRuleCreate(task_manager.Task): - def main(self, client): - return client.nova_client.security_group_rules.create(**self.args) - - -class NovaSecurityGroupRuleDelete(task_manager.Task): - def main(self, client): - return client.nova_client.security_group_rules.delete(**self.args) - - class NeutronFloatingIPList(task_manager.Task): def main(self, client): return client.neutron_client.list_floatingips(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 871e8051b..8f92de05a 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1954,9 +1954,8 @@ def list_security_groups(self, filters=None): # Handle nova security groups else: - with _utils.shade_exceptions("Error fetching security group list"): - groups = self.manager.submit_task( - _tasks.NovaSecurityGroupList(search_opts=filters)) + groups = self._compute_client.get( + '/os-security-groups', params=filters) return self._normalize_secgroups(groups) def list_servers(self, detailed=False, all_projects=False, bare=False): @@ -6799,14 +6798,15 @@ def create_security_group(self, name, description): ))['security_group'] else: - with _utils.shade_exceptions( - "Failed to create security group '{name}'".format( - name=name)): - group = self.manager.submit_task( - _tasks.NovaSecurityGroupCreate( - name=name, description=description - ) - ) + group = self._compute_client.post( + '/os-security-groups', json={ + 'security_group': { + 'name': name, 'description': description} + }) + # TODO(mordred) Remove this, it's a waste of a call. It's here for + # now for consistency with novaclient + group = self._compute_client.get( + '/os-security-groups/{id}'.format(id=group['id'])) return self._normalize_secgroup(group) def delete_security_group(self, name_or_id): @@ -6845,12 +6845,8 @@ def delete_security_group(self, name_or_id): return True else: - with _utils.shade_exceptions( - "Failed to delete security group '{group}'".format( - group=name_or_id)): - self.manager.submit_task( - _tasks.NovaSecurityGroupDelete(group=secgroup['id']) - ) + self._compute_client.delete( + '/os-security-groups/{id}'.format(id=secgroup['id'])) return True @_utils.valid_kwargs('name', 'description') @@ -6887,15 +6883,15 @@ def update_security_group(self, name_or_id, **kwargs): )['security_group'] else: - with _utils.shade_exceptions( - "Failed to update security group '{group}'".format( - group=name_or_id)): - for key in ('name', 'description'): - kwargs.setdefault(key, group[key]) - group = self.manager.submit_task( - _tasks.NovaSecurityGroupUpdate( - group=group['id'], **kwargs) - ) + for key in ('name', 'description'): + kwargs.setdefault(key, group[key]) + group = self._compute_client.put( + '/os-security-groups/{id}'.format(id=group['id']), + json={'security-group': kwargs}) + # TODO(mordred) Remove this, it's a waste of a call. It's here for + # now for consistency with novaclient + group = self._compute_client.get( + '/os-security-groups/{id}'.format(id=group['id'])) return self._normalize_secgroup(group) def create_security_group_rule(self, @@ -7013,18 +7009,16 @@ def create_security_group_rule(self, port_range_min = 1 port_range_max = 65535 - with _utils.shade_exceptions( - "Failed to create security group rule"): - rule = self.manager.submit_task( - _tasks.NovaSecurityGroupRuleCreate( - parent_group_id=secgroup['id'], - ip_protocol=protocol, - from_port=port_range_min, - to_port=port_range_max, - cidr=remote_ip_prefix, - group_id=remote_group_id - ) - ) + rule = self._compute_client.post( + '/os-security-group-rules', json=dict(security_group_rule=dict( + parent_group_id=secgroup['id'], + ip_protocol=protocol, + from_port=port_range_min, + to_port=port_range_max, + cidr=remote_ip_prefix, + group_id=remote_group_id + )) + ) return self._normalize_secgroup_rule(rule) def delete_security_group_rule(self, rule_id): @@ -7058,18 +7052,8 @@ def delete_security_group_rule(self, rule_id): return True else: - try: - self.manager.submit_task( - _tasks.NovaSecurityGroupRuleDelete(rule=rule_id) - ) - except nova_exceptions.NotFound: - return False - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Failed to delete security group rule {id}: {msg}".format( - id=rule_id, msg=str(e))) + self._compute_client.delete( + '/os-security-group-rules/{id}'.format(id=rule_id)) return True def list_zones(self): From 3b8ef1b88abe01b9b7510d8a0cd4179e3f0e59d5 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 18 Apr 2017 11:13:02 -0500 Subject: [PATCH 1430/3836] Actually fix the app_name protection The previous "fix" just disabled the app_name protection for python3. Go with Jens' suggestion (the easy route) of trying, and if that doesn't work, retrying. Change-Id: Ie4d1091b0f1fc6e1c9d9a2c935a458ea5ce55af3 --- shade/__init__.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 568ba868d..615987fee 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -34,18 +34,11 @@ def _get_openstack_config(app_name=None, app_version=None): # Protect against older versions of os-client-config that don't expose this - kwargs = {} try: - init = os_client_config.OpenStackConfig.__init__ - if 'app_name' in init.im_func.func_code.co_varnames: - kwargs['app_name'] = app_name - kwargs['app_version'] = app_version - except AttributeError: - # If we get an attribute error, it's actually likely some mocking issue - # but basically nothing about this is important enough to break things - # for someone. - pass - return os_client_config.OpenStackConfig(**kwargs) + return os_client_config.OpenStackConfig( + app_name=app_name, app_version=app_version) + except Exception: + return os_client_config.OpenStackConfig() def simple_logging(debug=False, http_debug=False): From b459c8de5cba1692db0c21e8e45d04a11d182e7e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 18 Apr 2017 11:30:59 -0500 Subject: [PATCH 1431/3836] Futureproof keystone unit tests against new occ We have a version of OCC coming out that fixes how keystoneclient is constructed. The fix is good - however, it breaks a couple of places where we're mocking to the old behavior. This will all go away once we're done with keystoneclient-ectomy, so for now just put in a version detection switch. Change-Id: I9d4908ef12d40868dc207b130e3244577b9870e9 --- shade/tests/unit/base.py | 13 ++++++++++++- shade/tests/unit/test_shade_operator.py | 16 ++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index ed182ce80..3eb66e974 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -17,6 +17,7 @@ import time import uuid +from distutils import version as du_version import fixtures import mock import os @@ -416,6 +417,16 @@ def use_keystone_v2(self): self.adapter = self.useFixture(rm_fixture.Fixture()) self.calls = [] self._uri_registry.clear() + + # occ > 1.26.0 fixes keystoneclient construction. Unfortunately, it + # breaks our mocking of what keystoneclient does here. Since we're + # close to just getting rid of ksc anyway, just put in a version match + occ_version = du_version.StrictVersion(occ.__version__) + if occ_version > du_version.StrictVersion('1.26.0'): + versioned_endpoint = 'https://identity.example.com/v2.0' + else: + versioned_endpoint = 'https://identity.example.com/' + self.__do_register_uris([ dict(method='GET', uri='https://identity.example.com/', text=open(self.discovery_json, 'r').read()), @@ -423,7 +434,7 @@ def use_keystone_v2(self): text=open(os.path.join( self.fixtures_directory, 'catalog-v2.json'), 'r').read() ), - dict(method='GET', uri='https://identity.example.com/', + dict(method='GET', uri=versioned_endpoint, text=open(self.discovery_json, 'r').read()), dict(method='GET', uri='https://identity.example.com/', text=open(self.discovery_json, 'r').read()) diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index bafda1db9..1c64b43f2 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -12,10 +12,12 @@ from keystoneauth1 import plugin as ksa_plugin +from distutils import version as du_version import mock import munch import testtools +import os_client_config as occ from os_client_config import cloud_config import shade from shade import exc @@ -1143,8 +1145,18 @@ def test_get_session_endpoint_identity(self, get_session_mock): session_mock = mock.Mock() get_session_mock.return_value = session_mock self.op_cloud.get_session_endpoint('identity') - session_mock.get_endpoint.assert_called_with( - interface=ksa_plugin.AUTH_INTERFACE) + # occ > 1.26.0 fixes keystoneclient construction. Unfortunately, it + # breaks our mocking of what keystoneclient does here. Since we're + # close to just getting rid of ksc anyway, just put in a version match + occ_version = du_version.StrictVersion(occ.__version__) + if occ_version > du_version.StrictVersion('1.26.0'): + kwargs = dict( + interface='public', region_name='RegionOne', + service_name=None, service_type='identity') + else: + kwargs = dict(interface=ksa_plugin.AUTH_INTERFACE) + + session_mock.get_endpoint.assert_called_with(**kwargs) @mock.patch.object(cloud_config.CloudConfig, 'get_session') def test_has_service_no(self, get_session_mock): From 4c67e76376569cb6f33653c4554b2f9ec514d33d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 18 Apr 2017 11:53:55 -0500 Subject: [PATCH 1432/3836] Change versioned_endpoint to endpoint_uri In this context, the word versioned_endpoint is confusing. Change-Id: I89b6faadbb3666e867e61d321e1cc096589589cf --- shade/tests/unit/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 3eb66e974..97b4b2f77 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -423,9 +423,9 @@ def use_keystone_v2(self): # close to just getting rid of ksc anyway, just put in a version match occ_version = du_version.StrictVersion(occ.__version__) if occ_version > du_version.StrictVersion('1.26.0'): - versioned_endpoint = 'https://identity.example.com/v2.0' + endpoint_uri = 'https://identity.example.com/v2.0' else: - versioned_endpoint = 'https://identity.example.com/' + endpoint_uri = 'https://identity.example.com/' self.__do_register_uris([ dict(method='GET', uri='https://identity.example.com/', @@ -434,7 +434,7 @@ def use_keystone_v2(self): text=open(os.path.join( self.fixtures_directory, 'catalog-v2.json'), 'r').read() ), - dict(method='GET', uri=versioned_endpoint, + dict(method='GET', uri=endpoint_uri, text=open(self.discovery_json, 'r').read()), dict(method='GET', uri='https://identity.example.com/', text=open(self.discovery_json, 'r').read()) From afed15f37130b841d6e523a468cd9d6cfcdfc791 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 18 Apr 2017 13:57:58 -0500 Subject: [PATCH 1433/3836] Remove extra unneeded API calls The create and update calls to nova security groups return the security group. There is no need to issue a GET. Change-Id: I068ae5b46954d0c3b0c8be67b80f3eed1be1f55b --- shade/openstackcloud.py | 8 -------- shade/tests/unit/test_security_groups.py | 8 -------- 2 files changed, 16 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 8f92de05a..a291ccb3c 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -6803,10 +6803,6 @@ def create_security_group(self, name, description): 'security_group': { 'name': name, 'description': description} }) - # TODO(mordred) Remove this, it's a waste of a call. It's here for - # now for consistency with novaclient - group = self._compute_client.get( - '/os-security-groups/{id}'.format(id=group['id'])) return self._normalize_secgroup(group) def delete_security_group(self, name_or_id): @@ -6888,10 +6884,6 @@ def update_security_group(self, name_or_id, **kwargs): group = self._compute_client.put( '/os-security-groups/{id}'.format(id=group['id']), json={'security-group': kwargs}) - # TODO(mordred) Remove this, it's a waste of a call. It's here for - # now for consistency with novaclient - group = self._compute_client.get( - '/os-security-groups/{id}'.format(id=group['id'])) return self._normalize_secgroup(group) def create_security_group_rule(self, diff --git a/shade/tests/unit/test_security_groups.py b/shade/tests/unit/test_security_groups.py index 0578b21a8..a792d80c6 100644 --- a/shade/tests/unit/test_security_groups.py +++ b/shade/tests/unit/test_security_groups.py @@ -173,10 +173,6 @@ def test_create_security_group_nova(self): 'name': group_name, 'description': group_desc, }})), - dict(method='GET', - uri='{endpoint}/os-security-groups/2'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_group': new_group}), ]) self.cloud.secgroup_source = 'nova' @@ -221,10 +217,6 @@ def test_update_security_group_nova(self): uri='{endpoint}/os-security-groups/2'.format( endpoint=fakes.COMPUTE_ENDPOINT), json={'security_group': update_return}), - dict(method='GET', - uri='{endpoint}/os-security-groups/2'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_group': update_return}), ]) r = self.cloud.update_security_group(nova_grp_obj.id, name=new_name) From 02e6371b8115fb1e8c2a0d9288e49a3f9712b302 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Tue, 18 Apr 2017 17:20:49 -0300 Subject: [PATCH 1434/3836] _discover_latest_version is private and not used Change-Id: I978fd77457791e2005edd1050886ce5840daf8d4 --- shade/openstackcloud.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index ebab6b892..e7ff33918 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -419,25 +419,6 @@ def _raw_image_client(self): self._raw_clients['raw-image'] = image_client return self._raw_clients['raw-image'] - def _discover_latest_version(self, client): - # Used to get the versioned endpoint for a service with one version - try: - # Version discovery - versions = client.get('/') - api_version = [ - version for version in versions - if version['status'] == 'CURRENT'][0] - return api_version['links'][0]['href'] - except (keystoneauth1.exceptions.connection.ConnectFailure, - OpenStackCloudURINotFound) as e: - # A 404 or a connection error is a likely thing to get - # either with a misconfgured glance. or we've already - # gotten a versioned endpoint from the catalog - self.log.debug( - "Version discovery failed, assuming endpoint in" - " the catalog is already versioned. {e}".format(e=str(e))) - return image_client.get_endpoint() - def _discover_image_endpoint(self, config_version, image_client): try: # Version discovery From cf0e6f9cfbfd7149b5fd58756a130426a4b9243a Mon Sep 17 00:00:00 2001 From: Alon Bar Tzlil Date: Tue, 18 Apr 2017 16:55:04 +0300 Subject: [PATCH 1435/3836] Allow router related functions to receive an ID Add support for receiving an id as the argument for some router related functions in the network proxy class. Change-Id: Id80685bd4721f6ed12e5535fe0c6d49ecf10e135 Closes-Bug: 1683787 --- openstack/network/v2/_proxy.py | 4 + openstack/tests/unit/network/v2/test_proxy.py | 82 +++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 74721b08a..591b095c6 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -2248,6 +2248,7 @@ def add_interface_to_router(self, router, subnet_id=None, port_id=None): body = {'port_id': port_id} else: body = {'subnet_id': subnet_id} + router = self._get_resource(_router.Router, router) return router.add_interface(self._session, **body) def remove_interface_from_router(self, router, subnet_id=None, @@ -2267,6 +2268,7 @@ def remove_interface_from_router(self, router, subnet_id=None, body = {'port_id': port_id} else: body = {'subnet_id': subnet_id} + router = self._get_resource(_router.Router, router) return router.remove_interface(self._session, **body) def add_gateway_to_router(self, router, **body): @@ -2278,6 +2280,7 @@ def add_gateway_to_router(self, router, **body): :returns: Router with updated interface :rtype: :class: `~openstack.network.v2.router.Router` """ + router = self._get_resource(_router.Router, router) return router.add_gateway(self._session, **body) def remove_gateway_from_router(self, router, **body): @@ -2289,6 +2292,7 @@ def remove_gateway_from_router(self, router, **body): :returns: Router with updated interface :rtype: :class: `~openstack.network.v2.router.Router` """ + router = self._get_resource(_router.Router, router) return router.remove_gateway(self._session, **body) def routers_hosting_l3_agents(self, router, **query): diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index c375dddeb..ff11c8caf 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -742,6 +742,88 @@ def test_routers(self): def test_router_update(self): self.verify_update(self.proxy.update_router, router.Router) + @mock.patch.object(proxy_base2.BaseProxy, '_get_resource') + @mock.patch.object(router.Router, 'add_interface') + def test_add_interface_to_router_with_port(self, mock_add_interface, + mock_get): + x_router = router.Router.new(id="ROUTER_ID") + mock_get.return_value = x_router + + self._verify("openstack.network.v2.router.Router.add_interface", + self.proxy.add_interface_to_router, + method_args=["FAKE_ROUTER"], + method_kwargs={"port_id": "PORT"}, + expected_kwargs={"port_id": "PORT"}) + mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") + + @mock.patch.object(proxy_base2.BaseProxy, '_get_resource') + @mock.patch.object(router.Router, 'add_interface') + def test_add_interface_to_router_with_subnet(self, mock_add_interface, + mock_get): + x_router = router.Router.new(id="ROUTER_ID") + mock_get.return_value = x_router + + self._verify("openstack.network.v2.router.Router.add_interface", + self.proxy.add_interface_to_router, + method_args=["FAKE_ROUTER"], + method_kwargs={"subnet_id": "SUBNET"}, + expected_kwargs={"subnet_id": "SUBNET"}) + mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") + + @mock.patch.object(proxy_base2.BaseProxy, '_get_resource') + @mock.patch.object(router.Router, 'remove_interface') + def test_remove_interface_from_router_with_port(self, mock_remove, + mock_get): + x_router = router.Router.new(id="ROUTER_ID") + mock_get.return_value = x_router + + self._verify("openstack.network.v2.router.Router.remove_interface", + self.proxy.remove_interface_from_router, + method_args=["FAKE_ROUTER"], + method_kwargs={"port_id": "PORT"}, + expected_kwargs={"port_id": "PORT"}) + mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") + + @mock.patch.object(proxy_base2.BaseProxy, '_get_resource') + @mock.patch.object(router.Router, 'remove_interface') + def test_remove_interface_from_router_with_subnet(self, mock_remove, + mock_get): + x_router = router.Router.new(id="ROUTER_ID") + mock_get.return_value = x_router + + self._verify("openstack.network.v2.router.Router.remove_interface", + self.proxy.remove_interface_from_router, + method_args=["FAKE_ROUTER"], + method_kwargs={"subnet_id": "SUBNET"}, + expected_kwargs={"subnet_id": "SUBNET"}) + mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") + + @mock.patch.object(proxy_base2.BaseProxy, '_get_resource') + @mock.patch.object(router.Router, 'add_gateway') + def test_add_gateway_to_router(self, mock_add, mock_get): + x_router = router.Router.new(id="ROUTER_ID") + mock_get.return_value = x_router + + self._verify("openstack.network.v2.router.Router.add_gateway", + self.proxy.add_gateway_to_router, + method_args=["FAKE_ROUTER"], + method_kwargs={"foo": "bar"}, + expected_kwargs={"foo": "bar"}) + mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") + + @mock.patch.object(proxy_base2.BaseProxy, '_get_resource') + @mock.patch.object(router.Router, 'remove_gateway') + def test_remove_gateway_from_router(self, mock_remove, mock_get): + x_router = router.Router.new(id="ROUTER_ID") + mock_get.return_value = x_router + + self._verify("openstack.network.v2.router.Router.remove_gateway", + self.proxy.remove_gateway_from_router, + method_args=["FAKE_ROUTER"], + method_kwargs={"foo": "bar"}, + expected_kwargs={"foo": "bar"}) + mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") + def test_router_hosting_l3_agents_list(self): self.verify_list( self.proxy.routers_hosting_l3_agents, From 49fe112b85a80935065809f0c6b80b4c867caae5 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 19 Apr 2017 07:41:21 -0500 Subject: [PATCH 1436/3836] Clarify some variable names in glance discovery I'm bad at naming things. This was unreadable. Update the variable names so it makes sense and add a comment. Change-Id: Iacbaeed91d71dca35c6fdd7d07999bd5956430fa --- shade/openstackcloud.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index a291ccb3c..c9e127843 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -489,17 +489,18 @@ def _discover_image_endpoint(self, config_version, image_client): " the catalog is already versioned. {e}".format(e=str(e))) image_url = image_client.get_endpoint() - service_url = image_client.get_endpoint() - parsed_image_url = urllib.parse.urlparse(image_url) - parsed_service_url = urllib.parse.urlparse(service_url) - - image_url = urllib.parse.ParseResult( - parsed_service_url.scheme, - parsed_image_url.netloc, - parsed_image_url.path, - parsed_image_url.params, - parsed_image_url.query, - parsed_image_url.fragment).geturl() + # Sometimes version discovery documents have broken endpoints, but + # the catalog has good ones (what?) + catalog_endpoint = urllib.parse.urlparse(image_client.get_endpoint()) + discovered_endpoint = urllib.parse.urlparse(image_url) + + return urllib.parse.ParseResult( + catalog_endpoint.scheme, + discovered_endpoint.netloc, + discovered_endpoint.path, + discovered_endpoint.params, + discovered_endpoint.query, + discovered_endpoint.fragment).geturl() return image_url @property From d5396cf35cd5d4b792d24590375401aa1177ad0e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 19 Apr 2017 10:40:22 -0500 Subject: [PATCH 1437/3836] Strip trailing slashes in test helper method A recent patch by Rosario pointed out a flaw in get_mock_url. Luckily, it's an easy fix. Change-Id: Iff12fb36832a5757321bbcc36728b959899b45cd --- shade/tests/unit/base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 97b4b2f77..9f3913098 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -179,6 +179,9 @@ def get_mock_url(self, service_type, interface, resource=None, qs_elements=None): endpoint_url = self.cloud.endpoint_for( service_type=service_type, interface=interface) + # Strip trailing slashes, so as not to produce double-slashes below + if endpoint_url.endswith('/'): + endpoint_url = endpoint_url[:-1] to_join = [endpoint_url] qs = '' if base_url_append: From 8ae82037dde45019cae8912f45a36cf3a362c444 Mon Sep 17 00:00:00 2001 From: Doug Wiegley Date: Thu, 18 Aug 2016 09:06:04 +0000 Subject: [PATCH 1438/3836] Revert "HAProxy uses milliseconds ..." This is a backwards incompatible change, and lbaas has more than one backend driver. The correct documentation is seconds here. This reverts commit ec5881fd2cfb229b877803dc342a2ca35debc1d4. Change-Id: Ie0a3ee764887fa128b87811c3ecfeb81593e47a3 --- openstack/network/v2/health_monitor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openstack/network/v2/health_monitor.py b/openstack/network/v2/health_monitor.py index a0aea9328..1a636fe1b 100644 --- a/openstack/network/v2/health_monitor.py +++ b/openstack/network/v2/health_monitor.py @@ -35,7 +35,7 @@ class HealthMonitor(resource.Resource): ) # Properties - #: The time, in milliseconds, between sending probes to members. + #: The time, in seconds, between sending probes to members. delay = resource.Body('delay') #: Expected HTTP codes for a passing HTTP(S) monitor. expected_codes = resource.Body('expected_codes') @@ -53,7 +53,7 @@ class HealthMonitor(resource.Resource): pool_ids = resource.Body('pools', type=list) #: The ID of the project this health monitor is associated with. project_id = resource.Body('tenant_id') - #: The maximum number of milliseconds for a monitor to wait for a + #: The maximum number of seconds for a monitor to wait for a #: connection to be established before it times out. This value must #: be less than the delay value. timeout = resource.Body('timeout') From 5c0f3f5a5de0a128a4a02342c83415fb73895705 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 20 Apr 2017 10:55:57 -0500 Subject: [PATCH 1439/3836] Remove stray line Thanks Jens! Change-Id: I88801842705dcb1374640b3178423f6f987799e4 --- shade/openstackcloud.py | 1 - 1 file changed, 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index c9e127843..bfd8804bc 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -501,7 +501,6 @@ def _discover_image_endpoint(self, config_version, image_client): discovered_endpoint.params, discovered_endpoint.query, discovered_endpoint.fragment).geturl() - return image_url @property def _image_client(self): From a98d3ada2a4d51bd5fbd676fe3306871ad8228eb Mon Sep 17 00:00:00 2001 From: Carlos Goncalves Date: Thu, 20 Apr 2017 17:52:43 +0000 Subject: [PATCH 1440/3836] Add data plane status support to Network Port obj Added 'data_plane_status' parameter to Port class. Partial-Bug: #1684989 Change-Id: I716ee25d1e7e4f81319f66b7f7457db243b4ffe3 --- openstack/network/v2/port.py | 2 ++ openstack/tests/unit/network/v2/test_port.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index 6234ea452..e98de3747 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -67,6 +67,8 @@ class Port(resource.Resource): binding_vnic_type = resource.Body('binding:vnic_type') #: Timestamp when the port was created. created_at = resource.Body('created_at') + #: Underlying data plane status of this port. + data_plane_status = resource.Body('data_plane_status') #: The port description. description = resource.Body('description') #: Device ID of this port. diff --git a/openstack/tests/unit/network/v2/test_port.py b/openstack/tests/unit/network/v2/test_port.py index b61418ce7..dd931a765 100644 --- a/openstack/tests/unit/network/v2/test_port.py +++ b/openstack/tests/unit/network/v2/test_port.py @@ -24,6 +24,7 @@ 'binding:vif_type': '6', 'binding:vnic_type': '7', 'created_at': '2016-03-09T12:14:57.233772', + 'data_plane_status': '32', 'description': '8', 'device_id': '9', 'device_owner': '10', @@ -82,6 +83,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['binding:vif_type'], sot.binding_vif_type) self.assertEqual(EXAMPLE['binding:vnic_type'], sot.binding_vnic_type) self.assertEqual(EXAMPLE['created_at'], sot.created_at) + self.assertEqual(EXAMPLE['data_plane_status'], sot.data_plane_status) self.assertEqual(EXAMPLE['description'], sot.description) self.assertEqual(EXAMPLE['device_id'], sot.device_id) self.assertEqual(EXAMPLE['device_owner'], sot.device_owner) From 0bfdaf4e0bda20cb9aa8d2a22688eb4b55da0d6e Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Wed, 19 Apr 2017 07:05:07 -0700 Subject: [PATCH 1441/3836] Use requests-mock for all the attach/detach/delete tests Change-Id: Ibc70e1b9565d1494e196e818274c5fcee6e8bc05 Signed-off-by: Rosario Di Somma --- shade/_normalize.py | 14 ++ shade/openstackcloud.py | 2 +- shade/tests/unit/test_volume.py | 270 +++++++++++++++++++++----------- 3 files changed, 194 insertions(+), 92 deletions(-) diff --git a/shade/_normalize.py b/shade/_normalize.py index 001e0a66f..3993cac79 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -771,6 +771,20 @@ def _normalize_volume(self, volume): ret.setdefault(key, val) return ret + def _normalize_volume_attachment(self, attachment): + """ Normalize a volume attachment object""" + + attachment = attachment.copy() + + # Discard noise + attachment.pop('NAME_ATTR', None) + attachment.pop('HUMAN_ID', None) + attachment.pop('human_id', None) + attachment.pop('request_ids', None) + attachment.pop('x_openstack_request_ids', None) + + return munch.Munch(**attachment) + def _normalize_compute_usage(self, usage): """ Normalize a compute usage object """ diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 956640c08..134fd60f0 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4094,7 +4094,7 @@ def attach_volume(self, server, volume, device=None, raise OpenStackCloudException( "Error in attaching volume %s" % volume['id'] ) - return vol_attachment + return self._normalize_volume_attachment(vol_attachment) def _get_volume_kwargs(self, kwargs): name = kwargs.pop('name', kwargs.pop('display_name', None)) diff --git a/shade/tests/unit/test_volume.py b/shade/tests/unit/test_volume.py index e5a410105..909073896 100644 --- a/shade/tests/unit/test_volume.py +++ b/shade/tests/unit/test_volume.py @@ -11,82 +11,128 @@ # under the License. -import cinderclient.exceptions as cinder_exc -import mock import testtools import shade +from shade import meta +from shade.tests import fakes from shade.tests.unit import base -class TestVolume(base.TestCase): +class TestVolume(base.RequestsMockTestCase): - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_attach_volume(self, mock_nova): + def test_attach_volume(self): server = dict(id='server001') - volume = dict(id='volume001', status='available', attachments=[]) + vol = {'id': 'volume001', 'status': 'available', + 'name': '', 'attachments': []} + volume = meta.obj_to_dict(fakes.FakeVolume(**vol)) rattach = {'server_id': server['id'], 'device': 'device001', 'volumeId': volume['id'], 'id': 'attachmentId'} - mock_nova.volumes.create_server_volume.return_value = rattach - + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', server['id'], + 'os-volume_attachments']), + json={'volumeAttachment': rattach}, + validate=dict(json={ + 'volumeAttachment': { + 'volumeId': vol['id']}}) + )]) ret = self.cloud.attach_volume(server, volume, wait=False) - self.assertEqual(rattach, ret) - mock_nova.volumes.create_server_volume.assert_called_once_with( - volume_id=volume['id'], server_id=server['id'], device=None - ) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_attach_volume_exception(self, mock_nova): + def test_attach_volume_exception(self): server = dict(id='server001') - volume = dict(id='volume001', status='available', attachments=[]) - mock_nova.volumes.create_server_volume.side_effect = Exception() - + vol = {'id': 'volume001', 'status': 'available', + 'name': '', 'attachments': []} + volume = meta.obj_to_dict(fakes.FakeVolume(**vol)) + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', server['id'], + 'os-volume_attachments']), + status_code=404, + validate=dict(json={ + 'volumeAttachment': { + 'volumeId': vol['id']}}) + )]) with testtools.ExpectedException( shade.OpenStackCloudException, "Error attaching volume %s to server %s" % ( volume['id'], server['id']) ): self.cloud.attach_volume(server, volume, wait=False) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'get_volume') - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_attach_volume_wait(self, mock_nova, mock_get): + def test_attach_volume_wait(self): server = dict(id='server001') - volume = dict(id='volume001', status='available', attachments=[]) - attached_volume = dict( - id=volume['id'], status='attached', - attachments=[{'server_id': server['id'], 'device': 'device001'}] - ) - - mock_get.side_effect = iter([volume, attached_volume]) - + vol = {'id': 'volume001', 'status': 'available', + 'name': '', 'attachments': []} + volume = meta.obj_to_dict(fakes.FakeVolume(**vol)) + vol['attachments'] = [{'server_id': server['id'], + 'device': 'device001'}] + vol['status'] = 'attached' + attached_volume = meta.obj_to_dict(fakes.FakeVolume(**vol)) + rattach = {'server_id': server['id'], 'device': 'device001', + 'volumeId': volume['id'], 'id': 'attachmentId'} + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', server['id'], + 'os-volume_attachments']), + json={'volumeAttachment': rattach}, + validate=dict(json={ + 'volumeAttachment': { + 'volumeId': vol['id']}})), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', 'detail']), + json={'volumes': [volume]}), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', 'detail']), + json={'volumes': [attached_volume]})]) # defaults to wait=True ret = self.cloud.attach_volume(server, volume) + self.assertEqual(rattach, ret) + self.assert_calls() - mock_nova.volumes.create_server_volume.assert_called_once_with( - volume_id=volume['id'], server_id=server['id'], device=None - ) - self.assertEqual(2, mock_get.call_count) - self.assertEqual(mock_nova.volumes.create_server_volume.return_value, - ret) - - @mock.patch.object(shade.OpenStackCloud, 'get_volume') - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_attach_volume_wait_error(self, mock_nova, mock_get): + def test_attach_volume_wait_error(self): server = dict(id='server001') - volume = dict(id='volume001', status='available', attachments=[]) - errored_volume = dict(id=volume['id'], status='error', attachments=[]) - mock_get.side_effect = iter([volume, errored_volume]) + vol = {'id': 'volume001', 'status': 'available', + 'name': '', 'attachments': []} + volume = meta.obj_to_dict(fakes.FakeVolume(**vol)) + vol['status'] = 'error' + errored_volume = meta.obj_to_dict(fakes.FakeVolume(**vol)) + rattach = {'server_id': server['id'], 'device': 'device001', + 'volumeId': volume['id'], 'id': 'attachmentId'} + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', server['id'], + 'os-volume_attachments']), + json={'volumeAttachment': rattach}, + validate=dict(json={ + 'volumeAttachment': { + 'volumeId': vol['id']}})), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', 'detail']), + json={'volumes': [errored_volume]})]) with testtools.ExpectedException( shade.OpenStackCloudException, "Error in attaching volume %s" % errored_volume['id'] ): self.cloud.attach_volume(server, volume) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_attach_volume_not_available(self, mock_nova): + def test_attach_volume_not_available(self): server = dict(id='server001') volume = dict(id='volume001', status='error', attachments=[]) @@ -96,9 +142,9 @@ def test_attach_volume_not_available(self, mock_nova): volume['id'], volume['status']) ): self.cloud.attach_volume(server, volume) + self.assertEqual(0, len(self.adapter.request_history)) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_attach_volume_already_attached(self, mock_nova): + def test_attach_volume_already_attached(self): device_id = 'device001' server = dict(id='server001') volume = dict(id='volume001', @@ -112,81 +158,123 @@ def test_attach_volume_already_attached(self, mock_nova): volume['id'], server['id'], device_id) ): self.cloud.attach_volume(server, volume) + self.assertEqual(0, len(self.adapter.request_history)) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_detach_volume(self, mock_nova): + def test_detach_volume(self): server = dict(id='server001') volume = dict(id='volume001', attachments=[ {'server_id': 'server001', 'device': 'device001'} ]) + self.register_uris([ + dict(method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', server['id'], + 'os-volume_attachments', volume['id']]))]) self.cloud.detach_volume(server, volume, wait=False) - mock_nova.volumes.delete_server_volume.assert_called_once_with( - attachment_id=volume['id'], server_id=server['id'] - ) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_detach_volume_exception(self, mock_nova): + def test_detach_volume_exception(self): server = dict(id='server001') volume = dict(id='volume001', attachments=[ {'server_id': 'server001', 'device': 'device001'} ]) - mock_nova.volumes.delete_server_volume.side_effect = Exception() + self.register_uris([ + dict(method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', server['id'], + 'os-volume_attachments', volume['id']]), + status_code=404)]) with testtools.ExpectedException( shade.OpenStackCloudException, "Error detaching volume %s from server %s" % ( volume['id'], server['id']) ): self.cloud.detach_volume(server, volume, wait=False) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'get_volume') - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_detach_volume_wait(self, mock_nova, mock_get): + def test_detach_volume_wait(self): server = dict(id='server001') - volume = dict(id='volume001', status='attached', - attachments=[ - {'server_id': 'server001', 'device': 'device001'} - ]) - avail_volume = dict(id=volume['id'], status='available', - attachments=[]) - mock_get.side_effect = iter([volume, avail_volume]) + attachments = [{'server_id': 'server001', 'device': 'device001'}] + vol = {'id': 'volume001', 'status': 'attached', 'name': '', + 'attachments': attachments} + volume = meta.obj_to_dict(fakes.FakeVolume(**vol)) + vol['status'] = 'available' + vol['attachments'] = [] + avail_volume = meta.obj_to_dict(fakes.FakeVolume(**vol)) + self.register_uris([ + dict(method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', server['id'], + 'os-volume_attachments', volume.id])), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', 'detail']), + json={'volumes': [avail_volume]})]) self.cloud.detach_volume(server, volume) - mock_nova.volumes.delete_server_volume.assert_called_once_with( - attachment_id=volume['id'], server_id=server['id'] - ) - self.assertEqual(2, mock_get.call_count) - - @mock.patch.object(shade.OpenStackCloud, 'get_volume') - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_detach_volume_wait_error(self, mock_nova, mock_get): - server = dict(id='server001') - volume = dict(id='volume001', status='attached', - attachments=[ - {'server_id': 'server001', 'device': 'device001'} - ]) - errored_volume = dict(id=volume['id'], status='error', attachments=[]) - mock_get.side_effect = iter([volume, errored_volume]) + self.assert_calls() + def test_detach_volume_wait_error(self): + server = dict(id='server001') + attachments = [{'server_id': 'server001', 'device': 'device001'}] + vol = {'id': 'volume001', 'status': 'attached', 'name': '', + 'attachments': attachments} + volume = meta.obj_to_dict(fakes.FakeVolume(**vol)) + vol['status'] = 'error' + vol['attachments'] = [] + errored_volume = meta.obj_to_dict(fakes.FakeVolume(**vol)) + self.register_uris([ + dict(method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', server['id'], + 'os-volume_attachments', volume.id])), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', 'detail']), + json={'volumes': [errored_volume]})]) with testtools.ExpectedException( shade.OpenStackCloudException, "Error in detaching volume %s" % errored_volume['id'] ): self.cloud.detach_volume(server, volume) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'get_volume') - @mock.patch.object(shade.OpenStackCloud, 'cinder_client') - def test_delete_volume_deletes(self, mock_cinder, mock_get): - volume = dict(id='volume001', status='attached') - mock_get.side_effect = iter([volume, None]) - + def test_delete_volume_deletes(self): + vol = {'id': 'volume001', 'status': 'attached', + 'name': '', 'attachments': []} + volume = meta.obj_to_dict(fakes.FakeVolume(**vol)) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', 'detail']), + json={'volumes': [volume]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', volume.id])), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', 'detail']), + json={'volumes': []})]) self.assertTrue(self.cloud.delete_volume(volume['id'])) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'get_volume') - @mock.patch.object(shade.OpenStackCloud, 'cinder_client') - def test_delete_volume_gone_away(self, mock_cinder, mock_get): - volume = dict(id='volume001', status='attached') - mock_get.side_effect = iter([volume]) - mock_cinder.volumes.delete.side_effect = cinder_exc.NotFound('N/A') - + def test_delete_volume_gone_away(self): + vol = {'id': 'volume001', 'status': 'attached', + 'name': '', 'attachments': []} + volume = meta.obj_to_dict(fakes.FakeVolume(**vol)) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', 'detail']), + json={'volumes': [volume]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', volume.id]), + status_code=404)]) self.assertFalse(self.cloud.delete_volume(volume['id'])) + self.assert_calls() From 4f807b8c60f32f61003d5008ed715e802f0df039 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 21 Apr 2017 09:57:09 -0500 Subject: [PATCH 1442/3836] Include two transitive dependencies to work around conflicts For people installing released shade from pip and then trying to use it somehow through entrypoints, 1.20.0 is currently broken because of some transitive dependencies, ordering issues and conflicting exclusion ranges. While this is by no means a comprehensive or sustainable solution to the problem, it does unbreak the end users currently broken and is not terribly onerous. Both of these additions are tracked in g-r and both can be removed when the dependencies that pull in conflicting ranges of things are removed. Change-Id: I6a4a1ab5ab109f0650873201868e0f1c4d09c564 --- .../workaround-transitive-deps-1e7a214f3256b77e.yaml | 9 +++++++++ requirements.txt | 7 +++++++ 2 files changed, 16 insertions(+) create mode 100644 releasenotes/notes/workaround-transitive-deps-1e7a214f3256b77e.yaml diff --git a/releasenotes/notes/workaround-transitive-deps-1e7a214f3256b77e.yaml b/releasenotes/notes/workaround-transitive-deps-1e7a214f3256b77e.yaml new file mode 100644 index 000000000..aa1b361dd --- /dev/null +++ b/releasenotes/notes/workaround-transitive-deps-1e7a214f3256b77e.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - Added requests and Babel to the direct dependencies list to work around + issues with pip installation, entrypoints and transitive dependencies + with conflicting exclusion ranges. Packagers of shade do not need to + add these two new requirements to shade's dependency list - they are + transitive depends and should be satisfied by the other things in the + requirements list. Both will be removed from the list again once the + python client libraries that pull them in have been removed. diff --git a/requirements.txt b/requirements.txt index 30a7fdd6d..e4d049970 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,13 @@ jmespath>=0.9.0 # MIT jsonpatch>=1.1 # BSD ipaddress>=1.0.7;python_version<'3.3' # PSF os-client-config>=1.22.0 # Apache-2.0 +# These two are here to prevent issues with version pin mismatches from our +# client library transitive depends. +# Babel can be removed when ironicclient is removed (because of openstackclient +# transitive depend) +Babel>=2.3.4,!=2.4.0 # BSD +# requests can be removed when designateclient is removed +requests>=2.10.0,!=2.12.2,!=2.13.0 # Apache-2.0 requestsexceptions>=1.2.0 # Apache-2.0 six>=1.9.0 # MIT futures>=3.0;python_version=='2.7' or python_version=='2.6' # BSD From 5dae1875ec70ef32791be41acd0a4a441b7afbd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Fri, 21 Apr 2017 21:03:58 +0000 Subject: [PATCH 1443/3836] Move unit tests for list networks to test_network.py file This file is place where all tests related to networks lives so tests for list_networks should be also there. Change-Id: I3aa7254f66a100d269deefef725dff036488efd2 --- shade/tests/unit/test_network.py | 26 ++++++++++++++++++++++++++ shade/tests/unit/test_shade.py | 25 ------------------------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/shade/tests/unit/test_network.py b/shade/tests/unit/test_network.py index 378b08159..3fe83ba5d 100644 --- a/shade/tests/unit/test_network.py +++ b/shade/tests/unit/test_network.py @@ -14,11 +14,37 @@ import testtools import shade +from shade import exc from shade.tests.unit import base class TestNetwork(base.TestCase): + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_list_networks(self, mock_neutron): + net1 = {'id': '1', 'name': 'net1'} + net2 = {'id': '2', 'name': 'net2'} + mock_neutron.list_networks.return_value = { + 'networks': [net1, net2] + } + nets = self.cloud.list_networks() + mock_neutron.list_networks.assert_called_once_with() + self.assertEqual([net1, net2], nets) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_list_networks_filtered(self, mock_neutron): + self.cloud.list_networks(filters={'name': 'test'}) + mock_neutron.list_networks.assert_called_once_with(name='test') + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_list_networks_exception(self, mock_neutron): + mock_neutron.list_networks.side_effect = Exception() + with testtools.ExpectedException( + exc.OpenStackCloudException, + "Error fetching network list" + ): + self.cloud.list_networks() + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') def test_create_network(self, mock_neutron): self.cloud.create_network("netname") diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 56deb811d..3d9f5c187 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -78,31 +78,6 @@ def test_list_servers_exception(self, mock_client): self.assertRaises(exc.OpenStackCloudException, self.cloud.list_servers) - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_list_networks(self, mock_neutron): - net1 = {'id': '1', 'name': 'net1'} - net2 = {'id': '2', 'name': 'net2'} - mock_neutron.list_networks.return_value = { - 'networks': [net1, net2] - } - nets = self.cloud.list_networks() - mock_neutron.list_networks.assert_called_once_with() - self.assertEqual([net1, net2], nets) - - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_list_networks_filtered(self, mock_neutron): - self.cloud.list_networks(filters={'name': 'test'}) - mock_neutron.list_networks.assert_called_once_with(name='test') - - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_list_networks_exception(self, mock_neutron): - mock_neutron.list_networks.side_effect = Exception() - with testtools.ExpectedException( - exc.OpenStackCloudException, - "Error fetching network list" - ): - self.cloud.list_networks() - @mock.patch.object(shade.OpenStackCloud, 'search_subnets') def test_get_subnet(self, mock_search): subnet = dict(id='123', name='mickey') From 7a2b5792522a14c0fc39490e407bbddddc8d7189 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 21 Apr 2017 17:20:26 -0500 Subject: [PATCH 1444/3836] Don't do all the network stuff in the rebuild poll When we're polling for completeness during rebuild server, we do not need to make all of the network calls to fill in network info. If we wait for the server though, we do return the server to the user, so fill in the info once at the end. Change-Id: I78aaf995cb83a88fef9414c17b6bd7fca21fe3c3 --- shade/openstackcloud.py | 45 ++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 956640c08..fb7f62289 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5628,33 +5628,36 @@ def get_active_server( return None def rebuild_server(self, server_id, image_id, admin_pass=None, + detailed=False, bare=False, wait=False, timeout=180): with _utils.shade_exceptions("Error in rebuilding instance"): server = self.manager.submit_task(_tasks.ServerRebuild( server=server_id, image=image_id, password=admin_pass)) - if wait: - admin_pass = server.get('adminPass') or admin_pass - for count in _utils._iterate_timeout( - timeout, - "Timeout waiting for server {0} to " - "rebuild.".format(server_id), - wait=self._SERVER_AGE): - try: - server = self.get_server(server_id) - except Exception: - continue - if not server: - continue + if not wait: + return server + admin_pass = server.get('adminPass') or admin_pass + for count in _utils._iterate_timeout( + timeout, + "Timeout waiting for server {0} to " + "rebuild.".format(server_id), + wait=self._SERVER_AGE): + try: + server = self.get_server(server_id, bare=True) + except Exception: + continue + if not server: + continue - if server['status'] == 'ACTIVE': - server.adminPass = admin_pass - return server + if server['status'] == 'ERROR': + raise OpenStackCloudException( + "Error in rebuilding the server", + extra_data=dict(server=server)) - if server['status'] == 'ERROR': - raise OpenStackCloudException( - "Error in rebuilding the server", - extra_data=dict(server=server)) - return server + if server['status'] == 'ACTIVE': + server.adminPass = admin_pass + break + + return self._expand_server(server, detailed=detailed, bare=bare) def set_server_metadata(self, name_or_id, metadata): """Set metadata in a server instance. From 09af82b11770a4586dca8714f907bffd65dab3c2 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 21 Apr 2017 17:44:01 -0500 Subject: [PATCH 1445/3836] Fix create/rebuild tests to not need a ton of neutron The tests are testing nova interactions, but because of _expand_server they call neutron a bunch. This is hidden with neutronclient for some reason - I'd look in to that more but we're deleting neutronclient anyway. To avoid us having to add a bunch of network mocks that are noise to the tests, fake out has_service to return false for neutron to skip the calls. We can then add them in on a case-by-case basis as needed. Change-Id: Ia877232b8dc167a8414f1601ea82708ff5aa2be9 --- shade/tests/unit/test_caching.py | 2 +- shade/tests/unit/test_create_server.py | 33 ++++++++++++++++++++++++- shade/tests/unit/test_rebuild_server.py | 24 +++++++++++++++++- 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 6b876e643..e11d2ea5a 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -186,7 +186,7 @@ def test_list_servers_no_herd(self, nova_mock): nova_mock.servers.list.return_value = [fake_server] with concurrent.futures.ThreadPoolExecutor(16) as pool: for i in range(16): - pool.submit(lambda: self.cloud.list_servers()) + pool.submit(lambda: self.cloud.list_servers(bare=True)) # It's possible to race-condition 16 threads all in the # single initial lock without a tiny sleep time.sleep(0.001) diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index 064da412a..d44f1d20a 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -27,6 +27,22 @@ class TestCreateServer(base.RequestsMockTestCase): + def setUp(self): + # This set of tests are not testing neutron, they're testing + # creating servers, but we do several network calls in service + # of a NORMAL create_server to find the default_network. Putting + # in all of the neutron mocks for that will make the tests harder + # to read. SO - we're going mock neutron into the off position + # and then turn it back on in the few tests that specifically do. + # Maybe we should reorg these into two classes - one with neutron + # mocked out - and one with it not mocked out + super(TestCreateServer, self).setUp() + self.has_neutron = False + + def fake_has_service(*args, **kwargs): + return self.has_neutron + self.cloud.has_service = fake_has_service + @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_create_server_with_create_exception(self, mock_nova): """ @@ -44,7 +60,6 @@ def test_create_server_with_get_exception(self, mock_nova): Test that an exception when attempting to get the server instance via the novaclient raises an exception in create_server. """ - mock_nova.servers.create.return_value = mock.Mock(status="BUILD") mock_nova.servers.get.side_effect = Exception("exception") self.assertRaises( @@ -79,6 +94,8 @@ def test_create_server_wait_server_error(self, mock_nova): mock_nova.servers.create.return_value = build_server mock_nova.servers.get.return_value = build_server mock_nova.servers.list.side_effect = [[build_server], [error_server]] + # TODO(slaweq): change this to neutron floating ips and turn neutron + # back on for this test when you get to floating ips mock_nova.floating_ips.list.return_value = [fake_floating_ip] self.assertRaises( exc.OpenStackCloudException, @@ -115,6 +132,8 @@ def test_create_server_no_wait(self, mock_nova): '5678') mock_nova.servers.create.return_value = fake_server mock_nova.servers.get.return_value = fake_server + # TODO(slaweq): change this to neutron floating ips and turn neutron + # back on for this test when you get to floating ips mock_nova.floating_ips.list.return_value = [fake_floating_ip] self.assertEqual( self.cloud._normalize_server( @@ -137,6 +156,8 @@ def test_create_server_with_admin_pass_no_wait(self, mock_nova): '5678') mock_nova.servers.create.return_value = fake_create_server mock_nova.servers.get.return_value = fake_server + # TODO(slaweq): change this to neutron floating ips and turn neutron + # back on for this test when you get to floating ips mock_nova.floating_ips.list.return_value = [fake_floating_ip] self.assertEqual( self.cloud._normalize_server( @@ -247,6 +268,8 @@ def test_create_server_no_addresses( mock_nova.servers.get.return_value = [build_server, None] mock_nova.servers.list.side_effect = [[build_server], [fake_server]] mock_nova.servers.delete.return_value = None + # TODO(slaweq): change this to neutron floating ips and turn neutron + # back on for this test when you get to floating ips mock_nova.floating_ips.list.return_value = [fake_floating_ip] mock_add_ips_to_server.return_value = fake_server self.cloud._SERVER_AGE = 0 @@ -264,6 +287,10 @@ def test_create_server_network_with_no_nics(self, mock_get_network, Verify that if 'network' is supplied, and 'nics' is not, that we attempt to get the network for the server. """ + # TODO(slaweq): self.has_neutron = True should be added, the mock of + # get_network should be removed, and a register_uris containing + # a list with a network dict matching "network-name" should be used + # instead. self.cloud.create_server( 'server-name', dict(id='image-id'), dict(id='flavor-id'), network='network-name') @@ -278,6 +305,10 @@ def test_create_server_network_with_empty_nics(self, Verify that if 'network' is supplied, along with an empty 'nics' list, it's treated the same as if 'nics' were not included. """ + # TODO(slaweq): self.has_neutron = True should be added, the mock of + # get_network should be removed, and a register_uris containing + # a list with a network dict matching "network-name" should be used + # instead. self.cloud.create_server( 'server-name', dict(id='image-id'), dict(id='flavor-id'), network='network-name', nics=[]) diff --git a/shade/tests/unit/test_rebuild_server.py b/shade/tests/unit/test_rebuild_server.py index fbd22ec86..eb5b8b72e 100644 --- a/shade/tests/unit/test_rebuild_server.py +++ b/shade/tests/unit/test_rebuild_server.py @@ -28,7 +28,23 @@ from shade.tests.unit import base -class TestRebuildServer(base.TestCase): +class TestRebuildServer(base.RequestsMockTestCase): + + def setUp(self): + # This set of tests are not testing neutron, they're testing + # rebuilding servers, but we do several network calls in service + # of a NORMAL rebuild to find the default_network. Putting + # in all of the neutron mocks for that will make the tests harder + # to read. SO - we're going mock neutron into the off position + # and then turn it back on in the few tests that specifically do. + # Maybe we should reorg these into two classes - one with neutron + # mocked out - and one with it not mocked out + super(TestRebuildServer, self).setUp() + self.has_neutron = False + + def fake_has_service(*args, **kwargs): + return self.has_neutron + self.cloud.has_service = fake_has_service @mock.patch.object(OpenStackCloud, 'nova_client') def test_rebuild_server_rebuild_exception(self, mock_nova): @@ -53,6 +69,8 @@ def test_rebuild_server_server_error(self, mock_nova): '5678') mock_nova.servers.rebuild.return_value = rebuild_server mock_nova.servers.list.return_value = [error_server] + # TODO(slaweq): change this to neutron floating ips and turn neutron + # back on for this test when you get to floating ips mock_nova.floating_ips.list.return_value = [fake_floating_ip] self.assertRaises( exc.OpenStackCloudException, @@ -110,6 +128,8 @@ def test_rebuild_server_with_admin_pass_wait(self, mock_nova): '5678') mock_nova.servers.rebuild.return_value = rebuild_server mock_nova.servers.list.return_value = [active_server] + # TODO(slaweq): change this to neutron floating ips and turn neutron + # back on for this test when you get to floating ips mock_nova.floating_ips.list.return_value = [fake_floating_ip] self.cloud.name = 'cloud-name' self.assertEqual( @@ -131,6 +151,8 @@ def test_rebuild_server_wait(self, mock_nova): '5678') mock_nova.servers.rebuild.return_value = rebuild_server mock_nova.servers.list.return_value = [active_server] + # TODO(slaweq): change this to neutron floating ips and turn neutron + # back on for this test when you get to floating ips mock_nova.floating_ips.list.return_value = [fake_floating_ip] self.cloud.name = 'cloud-name' self.assertEqual( From 258a6a33ba375fbf638426fd178aae498d8e86d6 Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Fri, 21 Apr 2017 16:16:40 -0700 Subject: [PATCH 1446/3836] Use requests-mock for the list/add/remove volume types tests Change-Id: I03aac75375b0f34678f8dc0b953fb2a0cd797150 Signed-off-by: Rosario Di Somma --- shade/tests/unit/test_volume_access.py | 165 ++++++++++++++++++------- 1 file changed, 118 insertions(+), 47 deletions(-) diff --git a/shade/tests/unit/test_volume_access.py b/shade/tests/unit/test_volume_access.py index 29a8e2067..39ee64ead 100644 --- a/shade/tests/unit/test_volume_access.py +++ b/shade/tests/unit/test_volume_access.py @@ -13,35 +13,42 @@ # under the License. -import mock import testtools import shade from shade.tests.unit import base -class TestVolumeAccess(base.TestCase): - @mock.patch.object(shade.OpenStackCloud, 'cinder_client') - def test_list_volume_types(self, mock_cinder): +class TestVolumeAccess(base.RequestsMockTestCase): + def test_list_volume_types(self): volume_type = dict( id='voltype01', description='volume type description', name='name', is_public=False) - mock_cinder.volume_types.list.return_value = [volume_type] - + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['types'], + qs_elements=['is_public=None']), + json={'volume_types': [volume_type]})]) self.assertTrue(self.cloud.list_volume_types()) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'cinder_client') - def test_get_volume_type(self, mock_cinder): + def test_get_volume_type(self): volume_type = dict( id='voltype01', description='volume type description', name='name', is_public=False) - mock_cinder.volume_types.list.return_value = [volume_type] - - volume_type_got = self.cloud.get_volume_type('name') + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['types'], + qs_elements=['is_public=None']), + json={'volume_types': [volume_type]})]) + volume_type_got = self.cloud.get_volume_type(volume_type['name']) self.assertEqual(volume_type_got.id, volume_type['id']) - @mock.patch.object(shade.OpenStackCloud, 'cinder_client') - def test_get_volume_type_access(self, mock_cinder): + def test_get_volume_type_access(self): volume_type = dict( id='voltype01', description='volume type description', name='name', is_public=False) @@ -49,14 +56,24 @@ def test_get_volume_type_access(self, mock_cinder): dict(volume_type_id='voltype01', name='name', project_id='prj01'), dict(volume_type_id='voltype01', name='name', project_id='prj02') ] - mock_cinder.volume_types.list.return_value = [volume_type] - mock_cinder.volume_type_access.list.return_value = volume_type_access - + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['types'], + qs_elements=['is_public=None']), + json={'volume_types': [volume_type]}), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['types', volume_type['id'], + 'os-volume-type-access']), + json={'volume_type_access': volume_type_access})]) self.assertEqual( - len(self.op_cloud.get_volume_type_access('name')), 2) + len(self.op_cloud.get_volume_type_access(volume_type['name'])), 2) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'cinder_client') - def test_remove_volume_type_access(self, mock_cinder): + def test_remove_volume_type_access(self): volume_type = dict( id='voltype01', description='volume type description', name='name', is_public=False) @@ -65,26 +82,55 @@ def test_remove_volume_type_access(self, mock_cinder): project_002 = dict(volume_type_id='voltype01', name='name', project_id='prj02') volume_type_access = [project_001, project_002] - mock_cinder.volume_types.list.return_value = [volume_type] - mock_cinder.volume_type_access.list.return_value = volume_type_access - - def _fake_remove(*args, **kwargs): - volume_type_access.pop() - - mock_cinder.volume_type_access.remove_project_access.side_effect = \ - _fake_remove - + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['types'], + qs_elements=['is_public=None']), + json={'volume_types': [volume_type]}), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['types', volume_type['id'], + 'os-volume-type-access']), + json={'volume_type_access': volume_type_access}), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['types'], qs_elements=['is_public=None']), + json={'volume_types': [volume_type]}), + dict(method='POST', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['types', volume_type['id'], 'action']), + json={'removeProjectAccess': { + 'project': project_001['project_id']}}, + validate=dict( + json={'removeProjectAccess': { + 'project': project_001['project_id']}})), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['types'], + qs_elements=['is_public=None']), + json={'volume_types': [volume_type]}), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['types', volume_type['id'], + 'os-volume-type-access']), + json={'volume_type_access': [project_001]})]) self.assertEqual( len(self.op_cloud.get_volume_type_access( volume_type['name'])), 2) self.op_cloud.remove_volume_type_access( volume_type['name'], project_001['project_id']) - self.assertEqual( - len(self.op_cloud.get_volume_type_access('name')), 1) + len(self.op_cloud.get_volume_type_access(volume_type['name'])), 1) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'cinder_client') - def test_add_volume_type_access(self, mock_cinder): + def test_add_volume_type_access(self): volume_type = dict( id='voltype01', description='volume type description', name='name', is_public=False) @@ -92,31 +138,56 @@ def test_add_volume_type_access(self, mock_cinder): project_id='prj01') project_002 = dict(volume_type_id='voltype01', name='name', project_id='prj02') - volume_type_access = [project_001] - mock_cinder.volume_types.list.return_value = [volume_type] - mock_cinder.volume_type_access.list.return_value = volume_type_access - mock_cinder.volume_type_access.add_project_access.return_value = None - - def _fake_add(*args, **kwargs): - volume_type_access.append(project_002) - - mock_cinder.volume_type_access.add_project_access.side_effect = \ - _fake_add - + volume_type_access = [project_001, project_002] + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['types'], + qs_elements=['is_public=None']), + json={'volume_types': [volume_type]}), + dict(method='POST', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['types', volume_type['id'], 'action']), + json={'addProjectAccess': { + 'project': project_002['project_id']}}, + validate=dict( + json={'addProjectAccess': { + 'project': project_002['project_id']}})), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['types'], + qs_elements=['is_public=None']), + json={'volume_types': [volume_type]}), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['types', volume_type['id'], + 'os-volume-type-access']), + json={'volume_type_access': volume_type_access})]) self.op_cloud.add_volume_type_access( volume_type['name'], project_002['project_id']) self.assertEqual( - len(self.op_cloud.get_volume_type_access('name')), 2) + len(self.op_cloud.get_volume_type_access(volume_type['name'])), 2) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'cinder_client') - def test_add_volume_type_access_missing(self, mock_cinder): + def test_add_volume_type_access_missing(self): volume_type = dict( id='voltype01', description='volume type description', name='name', is_public=False) project_001 = dict(volume_type_id='voltype01', name='name', project_id='prj01') - mock_cinder.volume_types.list.return_value = [volume_type] + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['types'], + qs_elements=['is_public=None']), + json={'volume_types': [volume_type]})]) with testtools.ExpectedException(shade.OpenStackCloudException, "VolumeType not found: MISSING"): self.op_cloud.add_volume_type_access( "MISSING", project_001['project_id']) + self.assert_calls() From e61a5c0418db13da8d9d13f460b9a7acccdd8b97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Fri, 21 Apr 2017 20:28:50 +0000 Subject: [PATCH 1447/3836] Remove neutronclient mocks from network list tests Neutronclient mock is replaced by mocking REST calls using base.RequestsMockTestCase class Change-Id: I8fd72845afa62fa5fdf351c666d23aae31757bba --- shade/tests/unit/test_network.py | 40 +++++++++++++++----------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/shade/tests/unit/test_network.py b/shade/tests/unit/test_network.py index 3fe83ba5d..9ea510590 100644 --- a/shade/tests/unit/test_network.py +++ b/shade/tests/unit/test_network.py @@ -14,36 +14,34 @@ import testtools import shade -from shade import exc from shade.tests.unit import base -class TestNetwork(base.TestCase): +class TestNetwork(base.RequestsMockTestCase): - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_list_networks(self, mock_neutron): + def test_list_networks(self): net1 = {'id': '1', 'name': 'net1'} net2 = {'id': '2', 'name': 'net2'} - mock_neutron.list_networks.return_value = { - 'networks': [net1, net2] - } + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': [net1, net2]}) + ]) nets = self.cloud.list_networks() - mock_neutron.list_networks.assert_called_once_with() self.assertEqual([net1, net2], nets) - - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_list_networks_filtered(self, mock_neutron): + self.assert_calls() + + def test_list_networks_filtered(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json'], + qs_elements=["name=test"]), + json={'networks': []}) + ]) self.cloud.list_networks(filters={'name': 'test'}) - mock_neutron.list_networks.assert_called_once_with(name='test') - - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_list_networks_exception(self, mock_neutron): - mock_neutron.list_networks.side_effect = Exception() - with testtools.ExpectedException( - exc.OpenStackCloudException, - "Error fetching network list" - ): - self.cloud.list_networks() + self.assert_calls() @mock.patch.object(shade.OpenStackCloud, 'neutron_client') def test_create_network(self, mock_neutron): From 0a39c015bccebd86af331bb39709e670631b2a9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Sat, 22 Apr 2017 20:43:00 +0000 Subject: [PATCH 1448/3836] Remove neutronclient mocks from network delete tests Neutronclient mock is replaced by mocking REST calls using base.RequestsMockTestCase class Change-Id: I71df04e886d0c08ca29fe141f41961d833138b74 --- shade/tests/unit/test_network.py | 66 +++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/shade/tests/unit/test_network.py b/shade/tests/unit/test_network.py index 9ea510590..111c43d36 100644 --- a/shade/tests/unit/test_network.py +++ b/shade/tests/unit/test_network.py @@ -132,29 +132,49 @@ def test_create_network_provider_wrong_type(self): ): self.cloud.create_network("netname", provider=provider_opts) - @mock.patch.object(shade.OpenStackCloud, 'get_network') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_network(self, mock_neutron, mock_get): - mock_get.return_value = dict(id='net-id', name='test-net') - self.assertTrue(self.cloud.delete_network('test-net')) - mock_get.assert_called_once_with('test-net') - mock_neutron.delete_network.assert_called_once_with(network='net-id') + def test_delete_network(self): + network_id = "test-net-id" + network_name = "network" + network = {'id': network_id, 'name': network_name} + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': [network]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'networks', "%s.json" % network_id]), + json={}) + ]) + self.assertTrue(self.cloud.delete_network(network_name)) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'get_network') - def test_delete_network_not_found(self, mock_get): - mock_get.return_value = None + def test_delete_network_not_found(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': []}), + ]) self.assertFalse(self.cloud.delete_network('test-net')) - mock_get.assert_called_once_with('test-net') + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'get_network') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_network_exception(self, mock_neutron, mock_get): - mock_get.return_value = dict(id='net-id', name='test-net') - mock_neutron.delete_network.side_effect = Exception() - with testtools.ExpectedException( - shade.OpenStackCloudException, - "Error deleting network test-net" - ): - self.cloud.delete_network('test-net') - mock_get.assert_called_once_with('test-net') - mock_neutron.delete_network.assert_called_once_with(network='net-id') + def test_delete_network_exception(self): + network_id = "test-net-id" + network_name = "network" + network = {'id': network_id, 'name': network_name} + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': [network]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'networks', "%s.json" % network_id]), + status_code=503) + ]) + self.assertRaises(shade.OpenStackCloudException, + self.cloud.delete_network, network_name) + self.assert_calls() From d1d670666ba81faed8cc7898ab0dc77ef80ac05e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Sun, 23 Apr 2017 08:15:21 +0000 Subject: [PATCH 1449/3836] Remove neutronclient mocks from network exceptions tests Neutronclient mock is replaced by mocking REST calls using base.RequestsMockTestCase class Change-Id: I488de8613a2e2580f49533b319603d159f7dc832 --- shade/tests/unit/test_shade.py | 45 ++++++++++++++-------------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 3d9f5c187..39f670646 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -13,7 +13,6 @@ import mock import munch -from neutronclient.common import exceptions as n_exc import testtools import shade @@ -33,7 +32,7 @@ ] -class TestShade(base.TestCase): +class TestShade(base.RequestsMockTestCase): def test_openstack_cloud(self): self.assertIsInstance(self.cloud, shade.OpenStackCloud) @@ -479,32 +478,26 @@ def test_update_subnet_conflict_gw_ops(self, mock_get): '456', gateway_ip=gateway, disable_gateway_ip=True) def test__neutron_exceptions_resource_not_found(self): - with mock.patch.object( - shade._tasks, 'NetworkList', - side_effect=n_exc.NotFound()): - self.assertRaises(exc.OpenStackCloudResourceNotFound, - self.cloud.list_networks) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + status_code=404) + ]) + self.assertRaises(exc.OpenStackCloudResourceNotFound, + self.cloud.list_networks) + self.assert_calls() def test__neutron_exceptions_url_not_found(self): - with mock.patch.object( - shade._tasks, 'NetworkList', - side_effect=n_exc.NeutronClientException(status_code=404)): - self.assertRaises(exc.OpenStackCloudURINotFound, - self.cloud.list_networks) - - def test__neutron_exceptions_neutron_client_generic(self): - with mock.patch.object( - shade._tasks, 'NetworkList', - side_effect=n_exc.NeutronClientException()): - self.assertRaises(exc.OpenStackCloudException, - self.cloud.list_networks) - - def test__neutron_exceptions_generic(self): - with mock.patch.object( - shade._tasks, 'NetworkList', - side_effect=Exception()): - self.assertRaises(exc.OpenStackCloudException, - self.cloud.list_networks) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + status_code=404) + ]) + self.assertRaises(exc.OpenStackCloudURINotFound, + self.cloud.list_networks) + self.assert_calls() @mock.patch.object(shade._tasks.ServerList, 'main') @mock.patch('shade.meta.add_server_interfaces') From ff2c06c30538edb9cb47b83f0a194f7893a7f458 Mon Sep 17 00:00:00 2001 From: Jordan Pittier Date: Sun, 23 Apr 2017 17:42:47 +0200 Subject: [PATCH 1450/3836] Make _fix_argv() somewhat compatible with Argparse action='append' Python Argparse supports the 'append' action [1] which is super handy to allow a user to repeat several times the same argument, each time with different values. This doesn't work with occ that tries to "fix argv" but raises this error: os_client_config.exceptions.OpenStackConfigException: The following options were given: '--foo,--foo' which contain duplicates except that one has _ and one has -. There is no sane way for us to know what you're doing. Remove the duplicate option and try again This patch tweak the _fix_argv() function so that it doesn't explode if the duplicate option has no '_' not '-' in its name. Change-Id: I4f06b6aff8d3ab1df45637399bc3a9b4b61764a9 Related-bug: #1685630 --- os_client_config/config.py | 5 ++++- os_client_config/tests/test_config.py | 11 +++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 89b5c6ccf..96d7f5355 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -19,6 +19,7 @@ import copy import json import os +import re import sys import warnings @@ -146,7 +147,9 @@ def _fix_argv(argv): # over the place. processed = collections.defaultdict(list) for index in range(0, len(argv)): - if argv[index].startswith('--'): + # If the value starts with '--' and has '-' or '_' in it, then + # it's worth looking at it + if re.match('^--.*(_|-)+.*', argv[index]): split_args = argv[index].split('=') orig = split_args[0] new = orig.replace('_', '-') diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py index dcc841ae9..09a11d28f 100644 --- a/os_client_config/tests/test_config.py +++ b/os_client_config/tests/test_config.py @@ -702,6 +702,17 @@ def test_argparse_underscores(self): self.assertEqual(cc.config['auth']['password'], 'pass') self.assertEqual(cc.config['auth']['auth_url'], 'auth-url') + def test_argparse_action_append_no_underscore(self): + c = config.OpenStackConfig(config_files=[self.no_yaml], + vendor_files=[self.no_yaml], + secure_files=[self.no_yaml]) + parser = argparse.ArgumentParser() + parser.add_argument('--foo', action='append') + argv = ['--foo', '1', '--foo', '2'] + c.register_argparse_arguments(parser, argv=argv) + opts, _remain = parser.parse_known_args(argv) + self.assertEqual(opts.foo, ['1', '2']) + def test_argparse_underscores_duplicate(self): c = config.OpenStackConfig(config_files=[self.no_yaml], vendor_files=[self.no_yaml], From 950c4500e2f2bcff370146d88f016e6ef84e56ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Sat, 22 Apr 2017 20:06:57 +0000 Subject: [PATCH 1451/3836] Remove neutronclient mocks from network create tests Neutronclient mock is replaced by mocking REST calls using base.RequestsMockTestCase class Change-Id: Ied57f41694d1c4f934439c52799f4c87794db0a5 --- shade/tests/unit/test_network.py | 204 ++++++++++++++++++++----------- 1 file changed, 130 insertions(+), 74 deletions(-) diff --git a/shade/tests/unit/test_network.py b/shade/tests/unit/test_network.py index 111c43d36..5893d8b4e 100644 --- a/shade/tests/unit/test_network.py +++ b/shade/tests/unit/test_network.py @@ -10,7 +10,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -import mock +import copy import testtools import shade @@ -19,6 +19,34 @@ class TestNetwork(base.RequestsMockTestCase): + mock_new_network_rep = { + 'provider:physical_network': None, + 'ipv6_address_scope': None, + 'revision_number': 3, + 'port_security_enabled': True, + 'provider:network_type': 'local', + 'id': '881d1bb7-a663-44c0-8f9f-ee2765b74486', + 'router:external': False, + 'availability_zone_hints': [], + 'availability_zones': [], + 'provider:segmentation_id': None, + 'ipv4_address_scope': None, + 'shared': False, + 'project_id': '861808a93da0484ea1767967c4df8a23', + 'status': 'ACTIVE', + 'subnets': [], + 'description': '', + 'tags': [], + 'updated_at': '2017-04-22T19:22:53Z', + 'is_default': False, + 'qos_policy_id': None, + 'name': 'netname', + 'admin_state_up': True, + 'tenant_id': '861808a93da0484ea1767967c4df8a23', + 'created_at': '2017-04-22T19:22:53Z', + 'mtu': 0 + } + def test_list_networks(self): net1 = {'id': '1', 'name': 'net1'} net2 = {'id': '2', 'name': 'net2'} @@ -43,86 +71,114 @@ def test_list_networks_filtered(self): self.cloud.list_networks(filters={'name': 'test'}) self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_network(self, mock_neutron): - self.cloud.create_network("netname") - mock_neutron.create_network.assert_called_with( - body=dict( - network=dict( - name='netname', - admin_state_up=True - ) - ) - ) - - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_network_specific_tenant(self, mock_neutron): - self.cloud.create_network("netname", project_id="project_id_value") - mock_neutron.create_network.assert_called_with( - body=dict( - network=dict( - name='netname', - admin_state_up=True, - tenant_id="project_id_value", - ) - ) - ) - - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_network_external(self, mock_neutron): - self.cloud.create_network("netname", external=True) - mock_neutron.create_network.assert_called_with( - body=dict( - network={ - 'name': 'netname', - 'admin_state_up': True, - 'router:external': True - } - ) - ) - - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_network_provider(self, mock_neutron): + def test_create_network(self): + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'network': self.mock_new_network_rep}, + validate=dict( + json={'network': { + 'admin_state_up': True, + 'name': 'netname'}})) + ]) + network = self.cloud.create_network("netname") + self.assertEqual(self.mock_new_network_rep, network) + self.assert_calls() + + def test_create_network_specific_tenant(self): + project_id = "project_id_value" + mock_new_network_rep = copy.copy(self.mock_new_network_rep) + mock_new_network_rep['project_id'] = project_id + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'network': mock_new_network_rep}, + validate=dict( + json={'network': { + 'admin_state_up': True, + 'name': 'netname', + 'tenant_id': project_id}})) + ]) + network = self.cloud.create_network("netname", project_id=project_id) + self.assertEqual(mock_new_network_rep, network) + self.assert_calls() + + def test_create_network_external(self): + mock_new_network_rep = copy.copy(self.mock_new_network_rep) + mock_new_network_rep['router:external'] = True + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'network': mock_new_network_rep}, + validate=dict( + json={'network': { + 'admin_state_up': True, + 'name': 'netname', + 'router:external': True}})) + ]) + network = self.cloud.create_network("netname", external=True) + self.assertEqual(mock_new_network_rep, network) + self.assert_calls() + + def test_create_network_provider(self): provider_opts = {'physical_network': 'mynet', 'network_type': 'vlan', 'segmentation_id': 'vlan1'} - self.cloud.create_network("netname", provider=provider_opts) - mock_neutron.create_network.assert_called_once_with( - body=dict( - network={ - 'name': 'netname', - 'admin_state_up': True, - 'provider:physical_network': - provider_opts['physical_network'], - 'provider:network_type': - provider_opts['network_type'], - 'provider:segmentation_id': - provider_opts['segmentation_id'], - } - ) - ) - - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_network_provider_ignored_value(self, mock_neutron): + new_network_provider_opts = { + 'provider:physical_network': 'mynet', + 'provider:network_type': 'vlan', + 'provider:segmentation_id': 'vlan1' + } + mock_new_network_rep = copy.copy(self.mock_new_network_rep) + mock_new_network_rep.update(new_network_provider_opts) + expected_send_params = { + 'admin_state_up': True, + 'name': 'netname' + } + expected_send_params.update(new_network_provider_opts) + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'network': mock_new_network_rep}, + validate=dict( + json={'network': expected_send_params})) + ]) + network = self.cloud.create_network("netname", provider=provider_opts) + self.assertEqual(mock_new_network_rep, network) + self.assert_calls() + + def test_create_network_provider_ignored_value(self): provider_opts = {'physical_network': 'mynet', 'network_type': 'vlan', 'segmentation_id': 'vlan1', 'should_not_be_passed': 1} - self.cloud.create_network("netname", provider=provider_opts) - mock_neutron.create_network.assert_called_once_with( - body=dict( - network={ - 'name': 'netname', - 'admin_state_up': True, - 'provider:physical_network': - provider_opts['physical_network'], - 'provider:network_type': - provider_opts['network_type'], - 'provider:segmentation_id': - provider_opts['segmentation_id'], - } - ) - ) + new_network_provider_opts = { + 'provider:physical_network': 'mynet', + 'provider:network_type': 'vlan', + 'provider:segmentation_id': 'vlan1' + } + mock_new_network_rep = copy.copy(self.mock_new_network_rep) + mock_new_network_rep.update(new_network_provider_opts) + expected_send_params = { + 'admin_state_up': True, + 'name': 'netname' + } + expected_send_params.update(new_network_provider_opts) + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'network': mock_new_network_rep}, + validate=dict( + json={'network': expected_send_params})) + ]) + network = self.cloud.create_network("netname", provider=provider_opts) + self.assertEqual(mock_new_network_rep, network) + self.assert_calls() def test_create_network_provider_wrong_type(self): provider_opts = "invalid" From cee8b6c56440819d5f14ce947cfc511d4f83021d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Sun, 23 Apr 2017 19:58:34 +0000 Subject: [PATCH 1452/3836] Fix list_servers tests to not need a ton of neutron It was fixed for rebuild/create servers in Ia877232b8dc167a8414f1601ea82708ff5aa2be9 but same issue was also in test_list_servers_all_projects and this commit fixes it for this test. Change-Id: Id901c307f1e11e2da48e41b29c363870d4c61bdf --- shade/tests/unit/test_shade.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 39f670646..dfc221997 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -34,6 +34,23 @@ class TestShade(base.RequestsMockTestCase): + def setUp(self): + # This set of tests are not testing neutron, they're testing + # rebuilding servers, but we do several network calls in service + # of a NORMAL rebuild to find the default_network. Putting + # in all of the neutron mocks for that will make the tests harder + # to read. SO - we're going mock neutron into the off position + # and then turn it back on in the few tests that specifically do. + # Maybe we should reorg these into two classes - one with neutron + # mocked out - and one with it not mocked out + super(TestShade, self).setUp() + self.has_neutron = False + + def fake_has_service(*args, **kwargs): + return self.has_neutron + + self.cloud.has_service = fake_has_service + def test_openstack_cloud(self): self.assertIsInstance(self.cloud, shade.OpenStackCloud) From 65c350f7847ded8cc98b6a5993d07d53ce11f53f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Sun, 23 Apr 2017 20:09:17 +0000 Subject: [PATCH 1453/3836] Move subnet related tests to separate module All subnet tests were in common test_shade.py file. Now all those tests will be in separate test module, like it is already done for network related tests. Change-Id: Id026ba11d4ea26dc94fb3670ec93ab2f46813236 --- shade/tests/unit/test_shade.py | 174 ---------------------------- shade/tests/unit/test_subnet.py | 198 ++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+), 174 deletions(-) create mode 100644 shade/tests/unit/test_subnet.py diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 39f670646..7277ef038 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -77,14 +77,6 @@ def test_list_servers_exception(self, mock_client): self.assertRaises(exc.OpenStackCloudException, self.cloud.list_servers) - @mock.patch.object(shade.OpenStackCloud, 'search_subnets') - def test_get_subnet(self, mock_search): - subnet = dict(id='123', name='mickey') - mock_search.return_value = [subnet] - r = self.cloud.get_subnet('mickey') - self.assertIsNotNone(r) - self.assertDictEqual(subnet, r) - @mock.patch.object(shade.OpenStackCloud, 'search_routers') def test_get_router(self, mock_search): router1 = dict(id='123', name='mickey') @@ -311,172 +303,6 @@ def test_list_router_interfaces_external(self, mock_search): ) self.assertEqual([external_port], ret) - @mock.patch.object(shade.OpenStackCloud, 'search_networks') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_subnet(self, mock_client, mock_search): - net1 = dict(id='123', name='donald') - mock_search.return_value = [net1] - pool = [{'start': '192.168.199.2', 'end': '192.168.199.254'}] - dns = ['8.8.8.8'] - routes = [{"destination": "0.0.0.0/0", "nexthop": "123.456.78.9"}] - self.cloud.create_subnet('donald', '192.168.199.0/24', - allocation_pools=pool, - dns_nameservers=dns, - host_routes=routes) - self.assertTrue(mock_client.create_subnet.called) - - @mock.patch.object(shade.OpenStackCloud, 'search_networks') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_subnet_string_ip_version(self, mock_client, mock_search): - '''Allow ip_version as a string''' - net1 = dict(id='123', name='donald') - mock_search.return_value = [net1] - self.cloud.create_subnet('donald', '192.168.199.0/24', ip_version='4') - self.assertTrue(mock_client.create_subnet.called) - - @mock.patch.object(shade.OpenStackCloud, 'search_networks') - def test_create_subnet_bad_ip_version(self, mock_search): - '''String ip_versions must be convertable to int''' - net1 = dict(id='123', name='donald') - mock_search.return_value = [net1] - - with testtools.ExpectedException( - exc.OpenStackCloudException, - "ip_version must be an integer" - ): - self.cloud.create_subnet('donald', '192.168.199.0/24', - ip_version='4x') - - @mock.patch.object(shade.OpenStackCloud, 'search_networks') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_subnet_without_gateway_ip(self, mock_client, mock_search): - net1 = dict(id='123', name='donald') - mock_search.return_value = [net1] - pool = [{'start': '192.168.200.2', 'end': '192.168.200.254'}] - dns = ['8.8.8.8'] - self.cloud.create_subnet('kooky', '192.168.200.0/24', - allocation_pools=pool, - dns_nameservers=dns, - disable_gateway_ip=True) - self.assertTrue(mock_client.create_subnet.called) - - @mock.patch.object(shade.OpenStackCloud, 'search_networks') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_subnet_with_gateway_ip(self, mock_client, mock_search): - net1 = dict(id='123', name='donald') - mock_search.return_value = [net1] - pool = [{'start': '192.168.200.8', 'end': '192.168.200.254'}] - dns = ['8.8.8.8'] - gateway = '192.168.200.2' - self.cloud.create_subnet('kooky', '192.168.200.0/24', - allocation_pools=pool, - dns_nameservers=dns, - gateway_ip=gateway) - self.assertTrue(mock_client.create_subnet.called) - - @mock.patch.object(shade.OpenStackCloud, 'search_networks') - def test_create_subnet_conflict_gw_ops(self, mock_search): - net1 = dict(id='123', name='donald') - mock_search.return_value = [net1] - gateway = '192.168.200.3' - self.assertRaises(exc.OpenStackCloudException, - self.cloud.create_subnet, 'kooky', - '192.168.200.0/24', gateway_ip=gateway, - disable_gateway_ip=True) - - @mock.patch.object(shade.OpenStackCloud, 'list_networks') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_subnet_bad_network(self, mock_client, mock_list): - net1 = dict(id='123', name='donald') - mock_list.return_value = [net1] - self.assertRaises(exc.OpenStackCloudException, - self.cloud.create_subnet, - 'duck', '192.168.199.0/24') - self.assertFalse(mock_client.create_subnet.called) - - @mock.patch.object(shade.OpenStackCloud, 'search_networks') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_subnet_non_unique_network(self, mock_client, mock_search): - net1 = dict(id='123', name='donald') - net2 = dict(id='456', name='donald') - mock_search.return_value = [net1, net2] - self.assertRaises(exc.OpenStackCloudException, - self.cloud.create_subnet, - 'donald', '192.168.199.0/24') - self.assertFalse(mock_client.create_subnet.called) - - @mock.patch.object(shade.OpenStackCloud, 'search_subnets') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_subnet(self, mock_client, mock_search): - subnet1 = dict(id='123', name='mickey') - mock_search.return_value = [subnet1] - self.cloud.delete_subnet('mickey') - self.assertTrue(mock_client.delete_subnet.called) - - @mock.patch.object(shade.OpenStackCloud, 'search_subnets') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_subnet_not_found(self, mock_client, mock_search): - mock_search.return_value = [] - r = self.cloud.delete_subnet('goofy') - self.assertFalse(r) - self.assertFalse(mock_client.delete_subnet.called) - - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_subnet_multiple_found(self, mock_client): - subnet1 = dict(id='123', name='mickey') - subnet2 = dict(id='456', name='mickey') - mock_client.list_subnets.return_value = dict(subnets=[subnet1, - subnet2]) - self.assertRaises(exc.OpenStackCloudException, - self.cloud.delete_subnet, - 'mickey') - self.assertFalse(mock_client.delete_subnet.called) - - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_subnet_multiple_using_id(self, mock_client): - subnet1 = dict(id='123', name='mickey') - subnet2 = dict(id='456', name='mickey') - mock_client.list_subnets.return_value = dict(subnets=[subnet1, - subnet2]) - self.cloud.delete_subnet('123') - self.assertTrue(mock_client.delete_subnet.called) - - @mock.patch.object(shade.OpenStackCloud, 'get_subnet') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_update_subnet(self, mock_client, mock_get): - subnet1 = dict(id='123', name='mickey') - mock_get.return_value = subnet1 - self.cloud.update_subnet('123', subnet_name='goofy') - self.assertTrue(mock_client.update_subnet.called) - - @mock.patch.object(shade.OpenStackCloud, 'get_subnet') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_update_subnet_gateway_ip(self, mock_client, mock_get): - subnet1 = dict(id='456', name='kooky') - mock_get.return_value = subnet1 - gateway = '192.168.200.3' - self.cloud.update_subnet( - '456', gateway_ip=gateway) - self.assertTrue(mock_client.update_subnet.called) - - @mock.patch.object(shade.OpenStackCloud, 'get_subnet') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_update_subnet_disable_gateway_ip(self, mock_client, mock_get): - subnet1 = dict(id='456', name='kooky') - mock_get.return_value = subnet1 - self.cloud.update_subnet( - '456', disable_gateway_ip=True) - self.assertTrue(mock_client.update_subnet.called) - - @mock.patch.object(shade.OpenStackCloud, 'get_subnet') - def test_update_subnet_conflict_gw_ops(self, mock_get): - subnet1 = dict(id='456', name='kooky') - mock_get.return_value = subnet1 - gateway = '192.168.200.3' - self.assertRaises(exc.OpenStackCloudException, - self.cloud.update_subnet, - '456', gateway_ip=gateway, disable_gateway_ip=True) - def test__neutron_exceptions_resource_not_found(self): self.register_uris([ dict(method='GET', diff --git a/shade/tests/unit/test_subnet.py b/shade/tests/unit/test_subnet.py new file mode 100644 index 000000000..186780711 --- /dev/null +++ b/shade/tests/unit/test_subnet.py @@ -0,0 +1,198 @@ +# Copyright 2017 OVH SAS +# 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 mock +import testtools + +import shade +from shade import exc +from shade.tests.unit import base + + +class TestSubnet(base.RequestsMockTestCase): + + @mock.patch.object(shade.OpenStackCloud, 'search_subnets') + def test_get_subnet(self, mock_search): + subnet = dict(id='123', name='mickey') + mock_search.return_value = [subnet] + r = self.cloud.get_subnet('mickey') + self.assertIsNotNone(r) + self.assertDictEqual(subnet, r) + + @mock.patch.object(shade.OpenStackCloud, 'search_networks') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_subnet(self, mock_client, mock_search): + net1 = dict(id='123', name='donald') + mock_search.return_value = [net1] + pool = [{'start': '192.168.199.2', 'end': '192.168.199.254'}] + dns = ['8.8.8.8'] + routes = [{"destination": "0.0.0.0/0", "nexthop": "123.456.78.9"}] + self.cloud.create_subnet('donald', '192.168.199.0/24', + allocation_pools=pool, + dns_nameservers=dns, + host_routes=routes) + self.assertTrue(mock_client.create_subnet.called) + + @mock.patch.object(shade.OpenStackCloud, 'search_networks') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_subnet_string_ip_version(self, mock_client, mock_search): + '''Allow ip_version as a string''' + net1 = dict(id='123', name='donald') + mock_search.return_value = [net1] + self.cloud.create_subnet('donald', '192.168.199.0/24', ip_version='4') + self.assertTrue(mock_client.create_subnet.called) + + @mock.patch.object(shade.OpenStackCloud, 'search_networks') + def test_create_subnet_bad_ip_version(self, mock_search): + '''String ip_versions must be convertable to int''' + net1 = dict(id='123', name='donald') + mock_search.return_value = [net1] + + with testtools.ExpectedException( + exc.OpenStackCloudException, + "ip_version must be an integer" + ): + self.cloud.create_subnet('donald', '192.168.199.0/24', + ip_version='4x') + + @mock.patch.object(shade.OpenStackCloud, 'search_networks') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_subnet_without_gateway_ip(self, mock_client, mock_search): + net1 = dict(id='123', name='donald') + mock_search.return_value = [net1] + pool = [{'start': '192.168.200.2', 'end': '192.168.200.254'}] + dns = ['8.8.8.8'] + self.cloud.create_subnet('kooky', '192.168.200.0/24', + allocation_pools=pool, + dns_nameservers=dns, + disable_gateway_ip=True) + self.assertTrue(mock_client.create_subnet.called) + + @mock.patch.object(shade.OpenStackCloud, 'search_networks') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_subnet_with_gateway_ip(self, mock_client, mock_search): + net1 = dict(id='123', name='donald') + mock_search.return_value = [net1] + pool = [{'start': '192.168.200.8', 'end': '192.168.200.254'}] + dns = ['8.8.8.8'] + gateway = '192.168.200.2' + self.cloud.create_subnet('kooky', '192.168.200.0/24', + allocation_pools=pool, + dns_nameservers=dns, + gateway_ip=gateway) + self.assertTrue(mock_client.create_subnet.called) + + @mock.patch.object(shade.OpenStackCloud, 'search_networks') + def test_create_subnet_conflict_gw_ops(self, mock_search): + net1 = dict(id='123', name='donald') + mock_search.return_value = [net1] + gateway = '192.168.200.3' + self.assertRaises(exc.OpenStackCloudException, + self.cloud.create_subnet, 'kooky', + '192.168.200.0/24', gateway_ip=gateway, + disable_gateway_ip=True) + + @mock.patch.object(shade.OpenStackCloud, 'list_networks') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_subnet_bad_network(self, mock_client, mock_list): + net1 = dict(id='123', name='donald') + mock_list.return_value = [net1] + self.assertRaises(exc.OpenStackCloudException, + self.cloud.create_subnet, + 'duck', '192.168.199.0/24') + self.assertFalse(mock_client.create_subnet.called) + + @mock.patch.object(shade.OpenStackCloud, 'search_networks') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_subnet_non_unique_network(self, mock_client, mock_search): + net1 = dict(id='123', name='donald') + net2 = dict(id='456', name='donald') + mock_search.return_value = [net1, net2] + self.assertRaises(exc.OpenStackCloudException, + self.cloud.create_subnet, + 'donald', '192.168.199.0/24') + self.assertFalse(mock_client.create_subnet.called) + + @mock.patch.object(shade.OpenStackCloud, 'search_subnets') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_delete_subnet(self, mock_client, mock_search): + subnet1 = dict(id='123', name='mickey') + mock_search.return_value = [subnet1] + self.cloud.delete_subnet('mickey') + self.assertTrue(mock_client.delete_subnet.called) + + @mock.patch.object(shade.OpenStackCloud, 'search_subnets') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_delete_subnet_not_found(self, mock_client, mock_search): + mock_search.return_value = [] + r = self.cloud.delete_subnet('goofy') + self.assertFalse(r) + self.assertFalse(mock_client.delete_subnet.called) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_delete_subnet_multiple_found(self, mock_client): + subnet1 = dict(id='123', name='mickey') + subnet2 = dict(id='456', name='mickey') + mock_client.list_subnets.return_value = dict(subnets=[subnet1, + subnet2]) + self.assertRaises(exc.OpenStackCloudException, + self.cloud.delete_subnet, + 'mickey') + self.assertFalse(mock_client.delete_subnet.called) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_delete_subnet_multiple_using_id(self, mock_client): + subnet1 = dict(id='123', name='mickey') + subnet2 = dict(id='456', name='mickey') + mock_client.list_subnets.return_value = dict(subnets=[subnet1, + subnet2]) + self.cloud.delete_subnet('123') + self.assertTrue(mock_client.delete_subnet.called) + + @mock.patch.object(shade.OpenStackCloud, 'get_subnet') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_update_subnet(self, mock_client, mock_get): + subnet1 = dict(id='123', name='mickey') + mock_get.return_value = subnet1 + self.cloud.update_subnet('123', subnet_name='goofy') + self.assertTrue(mock_client.update_subnet.called) + + @mock.patch.object(shade.OpenStackCloud, 'get_subnet') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_update_subnet_gateway_ip(self, mock_client, mock_get): + subnet1 = dict(id='456', name='kooky') + mock_get.return_value = subnet1 + gateway = '192.168.200.3' + self.cloud.update_subnet( + '456', gateway_ip=gateway) + self.assertTrue(mock_client.update_subnet.called) + + @mock.patch.object(shade.OpenStackCloud, 'get_subnet') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_update_subnet_disable_gateway_ip(self, mock_client, mock_get): + subnet1 = dict(id='456', name='kooky') + mock_get.return_value = subnet1 + self.cloud.update_subnet( + '456', disable_gateway_ip=True) + self.assertTrue(mock_client.update_subnet.called) + + @mock.patch.object(shade.OpenStackCloud, 'get_subnet') + def test_update_subnet_conflict_gw_ops(self, mock_get): + subnet1 = dict(id='456', name='kooky') + mock_get.return_value = subnet1 + gateway = '192.168.200.3' + self.assertRaises(exc.OpenStackCloudException, + self.cloud.update_subnet, + '456', gateway_ip=gateway, disable_gateway_ip=True) From c21447733a40db1fa4a21440118295cba4176e29 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sun, 23 Apr 2017 20:41:20 +0000 Subject: [PATCH 1454/3836] Updated from global requirements Change-Id: I1318331dd1b8bae151416b35016e11e7223c34e8 --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index e4d049970..7bbe8aea9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,9 +13,9 @@ os-client-config>=1.22.0 # Apache-2.0 # client library transitive depends. # Babel can be removed when ironicclient is removed (because of openstackclient # transitive depend) -Babel>=2.3.4,!=2.4.0 # BSD +Babel!=2.4.0,>=2.3.4 # BSD # requests can be removed when designateclient is removed -requests>=2.10.0,!=2.12.2,!=2.13.0 # Apache-2.0 +requests!=2.12.2,!=2.13.0,>=2.10.0 # Apache-2.0 requestsexceptions>=1.2.0 # Apache-2.0 six>=1.9.0 # MIT futures>=3.0;python_version=='2.7' or python_version=='2.6' # BSD From 1c0c81fe09ba69413e6b6e55d8823741dc58be47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Sun, 23 Apr 2017 20:19:31 +0000 Subject: [PATCH 1455/3836] Move router related tests to separate module All router tests were in common test_shade.py file. Now all those tests are in separate test module, like it is done for network related tests. Change-Id: I9eee8c676280ea08b8e2c5fb70c1b63f746f4819 --- shade/tests/unit/test_router.py | 249 ++++++++++++++++++++++++++++++++ shade/tests/unit/test_shade.py | 226 ----------------------------- 2 files changed, 249 insertions(+), 226 deletions(-) create mode 100644 shade/tests/unit/test_router.py diff --git a/shade/tests/unit/test_router.py b/shade/tests/unit/test_router.py new file mode 100644 index 000000000..63e885454 --- /dev/null +++ b/shade/tests/unit/test_router.py @@ -0,0 +1,249 @@ +# Copyright 2017 OVH SAS +# 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 mock + +import shade +from shade import exc +from shade.tests.unit import base + + +class TestRouter(base.RequestsMockTestCase): + + @mock.patch.object(shade.OpenStackCloud, 'search_routers') + def test_get_router(self, mock_search): + router1 = dict(id='123', name='mickey') + mock_search.return_value = [router1] + r = self.cloud.get_router('mickey') + self.assertIsNotNone(r) + self.assertDictEqual(router1, r) + + @mock.patch.object(shade.OpenStackCloud, 'search_routers') + def test_get_router_not_found(self, mock_search): + mock_search.return_value = [] + r = self.cloud.get_router('goofy') + self.assertIsNone(r) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_router(self, mock_client): + self.cloud.create_router(name='goofy', admin_state_up=True) + self.assertTrue(mock_client.create_router.called) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_router_specific_tenant(self, mock_client): + self.cloud.create_router("goofy", project_id="project_id_value") + mock_client.create_router.assert_called_once_with( + body=dict( + router=dict( + name='goofy', + admin_state_up=True, + tenant_id="project_id_value", + ) + ) + ) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_router_with_enable_snat_True(self, mock_client): + """Do not send enable_snat when same as neutron default.""" + self.cloud.create_router(name='goofy', admin_state_up=True, + enable_snat=True) + mock_client.create_router.assert_called_once_with( + body=dict( + router=dict( + name='goofy', + admin_state_up=True, + ) + ) + ) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_router_with_enable_snat_False(self, mock_client): + """Send enable_snat when it is False.""" + self.cloud.create_router(name='goofy', admin_state_up=True, + enable_snat=False) + mock_client.create_router.assert_called_once_with( + body=dict( + router=dict( + name='goofy', + admin_state_up=True, + external_gateway_info=dict( + enable_snat=False + ) + ) + ) + ) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_add_router_interface(self, mock_client): + self.cloud.add_router_interface({'id': '123'}, subnet_id='abc') + mock_client.add_interface_router.assert_called_once_with( + router='123', body={'subnet_id': 'abc'} + ) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_remove_router_interface(self, mock_client): + self.cloud.remove_router_interface({'id': '123'}, subnet_id='abc') + mock_client.remove_interface_router.assert_called_once_with( + router='123', body={'subnet_id': 'abc'} + ) + + def test_remove_router_interface_missing_argument(self): + self.assertRaises(ValueError, self.cloud.remove_router_interface, + {'id': '123'}) + + @mock.patch.object(shade.OpenStackCloud, 'get_router') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_update_router(self, mock_client, mock_get): + router1 = dict(id='123', name='mickey') + mock_get.return_value = router1 + self.cloud.update_router('123', name='goofy') + self.assertTrue(mock_client.update_router.called) + + @mock.patch.object(shade.OpenStackCloud, 'search_routers') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_delete_router(self, mock_client, mock_search): + router1 = dict(id='123', name='mickey') + mock_search.return_value = [router1] + self.cloud.delete_router('mickey') + self.assertTrue(mock_client.delete_router.called) + + @mock.patch.object(shade.OpenStackCloud, 'search_routers') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_delete_router_not_found(self, mock_client, mock_search): + mock_search.return_value = [] + r = self.cloud.delete_router('goofy') + self.assertFalse(r) + self.assertFalse(mock_client.delete_router.called) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_delete_router_multiple_found(self, mock_client): + router1 = dict(id='123', name='mickey') + router2 = dict(id='456', name='mickey') + mock_client.list_routers.return_value = dict(routers=[router1, + router2]) + self.assertRaises(exc.OpenStackCloudException, + self.cloud.delete_router, + 'mickey') + self.assertFalse(mock_client.delete_router.called) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_delete_router_multiple_using_id(self, mock_client): + router1 = dict(id='123', name='mickey') + router2 = dict(id='456', name='mickey') + mock_client.list_routers.return_value = dict(routers=[router1, + router2]) + self.cloud.delete_router('123') + self.assertTrue(mock_client.delete_router.called) + + @mock.patch.object(shade.OpenStackCloud, 'search_ports') + def test_list_router_interfaces_no_gw(self, mock_search): + """ + If a router does not have external_gateway_info, do not fail. + """ + external_port = {'id': 'external_port_id', + 'fixed_ips': [ + ('external_subnet_id', 'ip_address'), + ]} + port_list = [external_port] + router = { + 'id': 'router_id', + } + mock_search.return_value = port_list + ret = self.cloud.list_router_interfaces(router, + interface_type='external') + mock_search.assert_called_once_with( + filters={'device_id': router['id']} + ) + self.assertEqual([], ret) + + # A router can have its external_gateway_info set to None + router['external_gateway_info'] = None + ret = self.cloud.list_router_interfaces(router, + interface_type='external') + self.assertEqual([], ret) + + @mock.patch.object(shade.OpenStackCloud, 'search_ports') + def test_list_router_interfaces_all(self, mock_search): + internal_port = {'id': 'internal_port_id', + 'fixed_ips': [ + ('internal_subnet_id', 'ip_address'), + ]} + external_port = {'id': 'external_port_id', + 'fixed_ips': [ + ('external_subnet_id', 'ip_address'), + ]} + port_list = [internal_port, external_port] + router = { + 'id': 'router_id', + 'external_gateway_info': { + 'external_fixed_ips': [('external_subnet_id', 'ip_address')] + } + } + mock_search.return_value = port_list + ret = self.cloud.list_router_interfaces(router) + mock_search.assert_called_once_with( + filters={'device_id': router['id']} + ) + self.assertEqual(port_list, ret) + + @mock.patch.object(shade.OpenStackCloud, 'search_ports') + def test_list_router_interfaces_internal(self, mock_search): + internal_port = {'id': 'internal_port_id', + 'fixed_ips': [ + ('internal_subnet_id', 'ip_address'), + ]} + external_port = {'id': 'external_port_id', + 'fixed_ips': [ + ('external_subnet_id', 'ip_address'), + ]} + port_list = [internal_port, external_port] + router = { + 'id': 'router_id', + 'external_gateway_info': { + 'external_fixed_ips': [('external_subnet_id', 'ip_address')] + } + } + mock_search.return_value = port_list + ret = self.cloud.list_router_interfaces(router, + interface_type='internal') + mock_search.assert_called_once_with( + filters={'device_id': router['id']} + ) + self.assertEqual([internal_port], ret) + + @mock.patch.object(shade.OpenStackCloud, 'search_ports') + def test_list_router_interfaces_external(self, mock_search): + internal_port = {'id': 'internal_port_id', + 'fixed_ips': [ + ('internal_subnet_id', 'ip_address'), + ]} + external_port = {'id': 'external_port_id', + 'fixed_ips': [ + ('external_subnet_id', 'ip_address'), + ]} + port_list = [internal_port, external_port] + router = { + 'id': 'router_id', + 'external_gateway_info': { + 'external_fixed_ips': [('external_subnet_id', 'ip_address')] + } + } + mock_search.return_value = port_list + ret = self.cloud.list_router_interfaces(router, + interface_type='external') + mock_search.assert_called_once_with( + filters={'device_id': router['id']} + ) + self.assertEqual([external_port], ret) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 54a41f52a..e9b4fed71 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -94,232 +94,6 @@ def test_list_servers_exception(self, mock_client): self.assertRaises(exc.OpenStackCloudException, self.cloud.list_servers) - @mock.patch.object(shade.OpenStackCloud, 'search_routers') - def test_get_router(self, mock_search): - router1 = dict(id='123', name='mickey') - mock_search.return_value = [router1] - r = self.cloud.get_router('mickey') - self.assertIsNotNone(r) - self.assertDictEqual(router1, r) - - @mock.patch.object(shade.OpenStackCloud, 'search_routers') - def test_get_router_not_found(self, mock_search): - mock_search.return_value = [] - r = self.cloud.get_router('goofy') - self.assertIsNone(r) - - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_router(self, mock_client): - self.cloud.create_router(name='goofy', admin_state_up=True) - self.assertTrue(mock_client.create_router.called) - - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_router_specific_tenant(self, mock_client): - self.cloud.create_router("goofy", project_id="project_id_value") - mock_client.create_router.assert_called_once_with( - body=dict( - router=dict( - name='goofy', - admin_state_up=True, - tenant_id="project_id_value", - ) - ) - ) - - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_router_with_enable_snat_True(self, mock_client): - """Do not send enable_snat when same as neutron default.""" - self.cloud.create_router(name='goofy', admin_state_up=True, - enable_snat=True) - mock_client.create_router.assert_called_once_with( - body=dict( - router=dict( - name='goofy', - admin_state_up=True, - ) - ) - ) - - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_router_with_enable_snat_False(self, mock_client): - """Send enable_snat when it is False.""" - self.cloud.create_router(name='goofy', admin_state_up=True, - enable_snat=False) - mock_client.create_router.assert_called_once_with( - body=dict( - router=dict( - name='goofy', - admin_state_up=True, - external_gateway_info=dict( - enable_snat=False - ) - ) - ) - ) - - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_add_router_interface(self, mock_client): - self.cloud.add_router_interface({'id': '123'}, subnet_id='abc') - mock_client.add_interface_router.assert_called_once_with( - router='123', body={'subnet_id': 'abc'} - ) - - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_remove_router_interface(self, mock_client): - self.cloud.remove_router_interface({'id': '123'}, subnet_id='abc') - mock_client.remove_interface_router.assert_called_once_with( - router='123', body={'subnet_id': 'abc'} - ) - - def test_remove_router_interface_missing_argument(self): - self.assertRaises(ValueError, self.cloud.remove_router_interface, - {'id': '123'}) - - @mock.patch.object(shade.OpenStackCloud, 'get_router') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_update_router(self, mock_client, mock_get): - router1 = dict(id='123', name='mickey') - mock_get.return_value = router1 - self.cloud.update_router('123', name='goofy') - self.assertTrue(mock_client.update_router.called) - - @mock.patch.object(shade.OpenStackCloud, 'search_routers') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_router(self, mock_client, mock_search): - router1 = dict(id='123', name='mickey') - mock_search.return_value = [router1] - self.cloud.delete_router('mickey') - self.assertTrue(mock_client.delete_router.called) - - @mock.patch.object(shade.OpenStackCloud, 'search_routers') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_router_not_found(self, mock_client, mock_search): - mock_search.return_value = [] - r = self.cloud.delete_router('goofy') - self.assertFalse(r) - self.assertFalse(mock_client.delete_router.called) - - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_router_multiple_found(self, mock_client): - router1 = dict(id='123', name='mickey') - router2 = dict(id='456', name='mickey') - mock_client.list_routers.return_value = dict(routers=[router1, - router2]) - self.assertRaises(exc.OpenStackCloudException, - self.cloud.delete_router, - 'mickey') - self.assertFalse(mock_client.delete_router.called) - - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_router_multiple_using_id(self, mock_client): - router1 = dict(id='123', name='mickey') - router2 = dict(id='456', name='mickey') - mock_client.list_routers.return_value = dict(routers=[router1, - router2]) - self.cloud.delete_router('123') - self.assertTrue(mock_client.delete_router.called) - - @mock.patch.object(shade.OpenStackCloud, 'search_ports') - def test_list_router_interfaces_no_gw(self, mock_search): - """ - If a router does not have external_gateway_info, do not fail. - """ - external_port = {'id': 'external_port_id', - 'fixed_ips': [ - ('external_subnet_id', 'ip_address'), - ]} - port_list = [external_port] - router = { - 'id': 'router_id', - } - mock_search.return_value = port_list - ret = self.cloud.list_router_interfaces(router, - interface_type='external') - mock_search.assert_called_once_with( - filters={'device_id': router['id']} - ) - self.assertEqual([], ret) - - # A router can have its external_gateway_info set to None - router['external_gateway_info'] = None - ret = self.cloud.list_router_interfaces(router, - interface_type='external') - self.assertEqual([], ret) - - @mock.patch.object(shade.OpenStackCloud, 'search_ports') - def test_list_router_interfaces_all(self, mock_search): - internal_port = {'id': 'internal_port_id', - 'fixed_ips': [ - ('internal_subnet_id', 'ip_address'), - ]} - external_port = {'id': 'external_port_id', - 'fixed_ips': [ - ('external_subnet_id', 'ip_address'), - ]} - port_list = [internal_port, external_port] - router = { - 'id': 'router_id', - 'external_gateway_info': { - 'external_fixed_ips': [('external_subnet_id', 'ip_address')] - } - } - mock_search.return_value = port_list - ret = self.cloud.list_router_interfaces(router) - mock_search.assert_called_once_with( - filters={'device_id': router['id']} - ) - self.assertEqual(port_list, ret) - - @mock.patch.object(shade.OpenStackCloud, 'search_ports') - def test_list_router_interfaces_internal(self, mock_search): - internal_port = {'id': 'internal_port_id', - 'fixed_ips': [ - ('internal_subnet_id', 'ip_address'), - ]} - external_port = {'id': 'external_port_id', - 'fixed_ips': [ - ('external_subnet_id', 'ip_address'), - ]} - port_list = [internal_port, external_port] - router = { - 'id': 'router_id', - 'external_gateway_info': { - 'external_fixed_ips': [('external_subnet_id', 'ip_address')] - } - } - mock_search.return_value = port_list - ret = self.cloud.list_router_interfaces(router, - interface_type='internal') - mock_search.assert_called_once_with( - filters={'device_id': router['id']} - ) - self.assertEqual([internal_port], ret) - - @mock.patch.object(shade.OpenStackCloud, 'search_ports') - def test_list_router_interfaces_external(self, mock_search): - internal_port = {'id': 'internal_port_id', - 'fixed_ips': [ - ('internal_subnet_id', 'ip_address'), - ]} - external_port = {'id': 'external_port_id', - 'fixed_ips': [ - ('external_subnet_id', 'ip_address'), - ]} - port_list = [internal_port, external_port] - router = { - 'id': 'router_id', - 'external_gateway_info': { - 'external_fixed_ips': [('external_subnet_id', 'ip_address')] - } - } - mock_search.return_value = port_list - ret = self.cloud.list_router_interfaces(router, - interface_type='external') - mock_search.assert_called_once_with( - filters={'device_id': router['id']} - ) - self.assertEqual([external_port], ret) - def test__neutron_exceptions_resource_not_found(self): self.register_uris([ dict(method='GET', From 80ebf88bc28be12ea4d09135fc996787b8baec2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Sat, 22 Apr 2017 19:00:52 +0000 Subject: [PATCH 1456/3836] Replace neutronclient with REST API calls in network commands All network related commands to Neutron (list/create/delete) are now made via keystoneauth Change-Id: Ifa179c062c164d7e48e8a09c52e554dce5ddd1cb --- shade/_tasks.py | 15 --------------- shade/openstackcloud.py | 18 ++++++------------ 2 files changed, 6 insertions(+), 27 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 0ee5d9ee5..a9cab011a 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -207,21 +207,6 @@ def main(self, client): return client.nova_client.keypairs.delete(**self.args) -class NetworkList(task_manager.Task): - def main(self, client): - return client.neutron_client.list_networks(**self.args) - - -class NetworkCreate(task_manager.Task): - def main(self, client): - return client.neutron_client.create_network(**self.args) - - -class NetworkDelete(task_manager.Task): - def main(self, client): - return client.neutron_client.delete_network(**self.args) - - class RouterList(task_manager.Task): def main(self, client): return client.neutron_client.list_routers() diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index b8220b628..336105ba1 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1639,9 +1639,7 @@ def list_networks(self, filters=None): # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} - with _utils.neutron_exceptions("Error fetching network list"): - return self.manager.submit_task( - _tasks.NetworkList(**filters))['networks'] + return self._network_client.get("/networks.json", params=filters) def list_routers(self, filters=None): """List all available routers. @@ -2998,15 +2996,13 @@ def create_network(self, name, shared=False, admin_state_up=True, if external: network['router:external'] = True - with _utils.neutron_exceptions( - "Error creating network {0}".format(name)): - net = self.manager.submit_task( - _tasks.NetworkCreate(body=dict({'network': network}))) + net = self._network_client.post("/networks.json", + json={'network': network}) # Reset cache so the new network is picked up self._reset_network_caches() - return net['network'] + return net def delete_network(self, name_or_id): """Delete a network. @@ -3022,10 +3018,8 @@ def delete_network(self, name_or_id): self.log.debug("Network %s not found for deleting", name_or_id) return False - with _utils.neutron_exceptions( - "Error deleting network {0}".format(name_or_id)): - self.manager.submit_task( - _tasks.NetworkDelete(network=network['id'])) + self._network_client.delete( + "/networks/{network_id}.json".format(network_id=network['id'])) # Reset cache so the deleted network is removed self._reset_network_caches() From 66e3168f954082173d0bce78df9f2c6bcf1ec29e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Mon, 24 Apr 2017 21:37:53 +0000 Subject: [PATCH 1457/3836] Remove neutronclient mocks from subnet tests Neutronclient mock is replaced by mocking REST calls using base.RequestsMockTestCase class Change-Id: Id6cf281d9a9e8f891812ad081c32272a3e7f3438 --- shade/tests/unit/test_subnet.py | 472 ++++++++++++++++++++++---------- 1 file changed, 324 insertions(+), 148 deletions(-) diff --git a/shade/tests/unit/test_subnet.py b/shade/tests/unit/test_subnet.py index 186780711..815c4af42 100644 --- a/shade/tests/unit/test_subnet.py +++ b/shade/tests/unit/test_subnet.py @@ -13,186 +13,362 @@ # See the License for the specific language governing permissions and # limitations under the License. -import mock +import copy import testtools -import shade from shade import exc from shade.tests.unit import base class TestSubnet(base.RequestsMockTestCase): - @mock.patch.object(shade.OpenStackCloud, 'search_subnets') - def test_get_subnet(self, mock_search): - subnet = dict(id='123', name='mickey') - mock_search.return_value = [subnet] - r = self.cloud.get_subnet('mickey') + network_name = 'network_name' + subnet_name = 'subnet_name' + subnet_id = '1f1696eb-7f47-47f6-835c-4889bff88604' + subnet_cidr = '192.168.199.0/24' + + mock_network_rep = { + 'id': '881d1bb7-a663-44c0-8f9f-ee2765b74486', + 'name': network_name, + } + + mock_subnet_rep = { + 'allocation_pools': [{ + 'start': u'192.168.199.2', + 'end': u'192.168.199.254' + }], + 'cidr': subnet_cidr, + 'created_at': '2017-04-24T20:22:23Z', + 'description': '', + 'dns_nameservers': [], + 'enable_dhcp': False, + 'gateway_ip': '192.168.199.1', + 'host_routes': [], + 'id': subnet_id, + 'ip_version': 4, + 'ipv6_address_mode': None, + 'ipv6_ra_mode': None, + 'name': subnet_name, + 'network_id': mock_network_rep['id'], + 'project_id': '861808a93da0484ea1767967c4df8a23', + 'revision_number': 2, + 'service_types': [], + 'subnetpool_id': None, + 'tags': [] + } + + def test_get_subnet(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets.json']), + json={'subnets': [self.mock_subnet_rep]}) + ]) + r = self.cloud.get_subnet(self.subnet_name) self.assertIsNotNone(r) - self.assertDictEqual(subnet, r) + self.assertDictEqual(self.mock_subnet_rep, r) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'search_networks') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_subnet(self, mock_client, mock_search): - net1 = dict(id='123', name='donald') - mock_search.return_value = [net1] + def test_create_subnet(self): pool = [{'start': '192.168.199.2', 'end': '192.168.199.254'}] dns = ['8.8.8.8'] routes = [{"destination": "0.0.0.0/0", "nexthop": "123.456.78.9"}] - self.cloud.create_subnet('donald', '192.168.199.0/24', - allocation_pools=pool, - dns_nameservers=dns, - host_routes=routes) - self.assertTrue(mock_client.create_subnet.called) - - @mock.patch.object(shade.OpenStackCloud, 'search_networks') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_subnet_string_ip_version(self, mock_client, mock_search): + mock_subnet_rep = copy.copy(self.mock_subnet_rep) + mock_subnet_rep['allocation_pools'] = pool + mock_subnet_rep['dns_nameservers'] = dns + mock_subnet_rep['host_routes'] = routes + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': [self.mock_network_rep]}), + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets.json']), + json={'subnet': mock_subnet_rep}, + validate=dict( + json={'subnet': { + 'cidr': self.subnet_cidr, + 'enable_dhcp': False, + 'ip_version': 4, + 'network_id': self.mock_network_rep['id'], + 'allocation_pools': pool, + 'dns_nameservers': dns, + 'host_routes': routes}})) + ]) + subnet = self.cloud.create_subnet(self.network_name, self.subnet_cidr, + allocation_pools=pool, + dns_nameservers=dns, + host_routes=routes) + self.assertDictEqual(mock_subnet_rep, subnet) + self.assert_calls() + + def test_create_subnet_string_ip_version(self): '''Allow ip_version as a string''' - net1 = dict(id='123', name='donald') - mock_search.return_value = [net1] - self.cloud.create_subnet('donald', '192.168.199.0/24', ip_version='4') - self.assertTrue(mock_client.create_subnet.called) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': [self.mock_network_rep]}), + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets.json']), + json={'subnet': self.mock_subnet_rep}, + validate=dict( + json={'subnet': { + 'cidr': self.subnet_cidr, + 'enable_dhcp': False, + 'ip_version': 4, + 'network_id': self.mock_network_rep['id']}})) + ]) + subnet = self.cloud.create_subnet( + self.network_name, self.subnet_cidr, ip_version='4') + self.assertDictEqual(self.mock_subnet_rep, subnet) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'search_networks') - def test_create_subnet_bad_ip_version(self, mock_search): + def test_create_subnet_bad_ip_version(self): '''String ip_versions must be convertable to int''' - net1 = dict(id='123', name='donald') - mock_search.return_value = [net1] - + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': [self.mock_network_rep]}) + ]) with testtools.ExpectedException( exc.OpenStackCloudException, "ip_version must be an integer" ): - self.cloud.create_subnet('donald', '192.168.199.0/24', - ip_version='4x') - - @mock.patch.object(shade.OpenStackCloud, 'search_networks') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_subnet_without_gateway_ip(self, mock_client, mock_search): - net1 = dict(id='123', name='donald') - mock_search.return_value = [net1] - pool = [{'start': '192.168.200.2', 'end': '192.168.200.254'}] + self.cloud.create_subnet( + self.network_name, self.subnet_cidr, ip_version='4x') + self.assert_calls() + + def test_create_subnet_without_gateway_ip(self): + pool = [{'start': '192.168.199.2', 'end': '192.168.199.254'}] dns = ['8.8.8.8'] - self.cloud.create_subnet('kooky', '192.168.200.0/24', - allocation_pools=pool, - dns_nameservers=dns, - disable_gateway_ip=True) - self.assertTrue(mock_client.create_subnet.called) - - @mock.patch.object(shade.OpenStackCloud, 'search_networks') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_subnet_with_gateway_ip(self, mock_client, mock_search): - net1 = dict(id='123', name='donald') - mock_search.return_value = [net1] - pool = [{'start': '192.168.200.8', 'end': '192.168.200.254'}] + mock_subnet_rep = copy.copy(self.mock_subnet_rep) + mock_subnet_rep['allocation_pools'] = pool + mock_subnet_rep['dns_nameservers'] = dns + mock_subnet_rep['gateway_ip'] = None + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': [self.mock_network_rep]}), + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets.json']), + json={'subnet': mock_subnet_rep}, + validate=dict( + json={'subnet': { + 'cidr': self.subnet_cidr, + 'enable_dhcp': False, + 'ip_version': 4, + 'network_id': self.mock_network_rep['id'], + 'allocation_pools': pool, + 'gateway_ip': None, + 'dns_nameservers': dns}})) + ]) + subnet = self.cloud.create_subnet(self.network_name, self.subnet_cidr, + allocation_pools=pool, + dns_nameservers=dns, + disable_gateway_ip=True) + self.assertDictEqual(mock_subnet_rep, subnet) + self.assert_calls() + + def test_create_subnet_with_gateway_ip(self): + pool = [{'start': '192.168.199.8', 'end': '192.168.199.254'}] + gateway = '192.168.199.2' dns = ['8.8.8.8'] - gateway = '192.168.200.2' - self.cloud.create_subnet('kooky', '192.168.200.0/24', - allocation_pools=pool, - dns_nameservers=dns, - gateway_ip=gateway) - self.assertTrue(mock_client.create_subnet.called) - - @mock.patch.object(shade.OpenStackCloud, 'search_networks') - def test_create_subnet_conflict_gw_ops(self, mock_search): - net1 = dict(id='123', name='donald') - mock_search.return_value = [net1] + mock_subnet_rep = copy.copy(self.mock_subnet_rep) + mock_subnet_rep['allocation_pools'] = pool + mock_subnet_rep['dns_nameservers'] = dns + mock_subnet_rep['gateway_ip'] = gateway + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': [self.mock_network_rep]}), + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets.json']), + json={'subnet': mock_subnet_rep}, + validate=dict( + json={'subnet': { + 'cidr': self.subnet_cidr, + 'enable_dhcp': False, + 'ip_version': 4, + 'network_id': self.mock_network_rep['id'], + 'allocation_pools': pool, + 'gateway_ip': gateway, + 'dns_nameservers': dns}})) + ]) + subnet = self.cloud.create_subnet(self.network_name, self.subnet_cidr, + allocation_pools=pool, + dns_nameservers=dns, + gateway_ip=gateway) + self.assertDictEqual(mock_subnet_rep, subnet) + self.assert_calls() + + def test_create_subnet_conflict_gw_ops(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': [self.mock_network_rep]}) + ]) gateway = '192.168.200.3' self.assertRaises(exc.OpenStackCloudException, self.cloud.create_subnet, 'kooky', - '192.168.200.0/24', gateway_ip=gateway, + self.subnet_cidr, gateway_ip=gateway, disable_gateway_ip=True) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'list_networks') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_subnet_bad_network(self, mock_client, mock_list): - net1 = dict(id='123', name='donald') - mock_list.return_value = [net1] + def test_create_subnet_bad_network(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': [self.mock_network_rep]}) + ]) self.assertRaises(exc.OpenStackCloudException, self.cloud.create_subnet, - 'duck', '192.168.199.0/24') - self.assertFalse(mock_client.create_subnet.called) - - @mock.patch.object(shade.OpenStackCloud, 'search_networks') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_subnet_non_unique_network(self, mock_client, mock_search): - net1 = dict(id='123', name='donald') - net2 = dict(id='456', name='donald') - mock_search.return_value = [net1, net2] + 'duck', self.subnet_cidr) + self.assert_calls() + + def test_create_subnet_non_unique_network(self): + net1 = dict(id='123', name=self.network_name) + net2 = dict(id='456', name=self.network_name) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': [net1, net2]}) + ]) self.assertRaises(exc.OpenStackCloudException, self.cloud.create_subnet, - 'donald', '192.168.199.0/24') - self.assertFalse(mock_client.create_subnet.called) - - @mock.patch.object(shade.OpenStackCloud, 'search_subnets') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_subnet(self, mock_client, mock_search): - subnet1 = dict(id='123', name='mickey') - mock_search.return_value = [subnet1] - self.cloud.delete_subnet('mickey') - self.assertTrue(mock_client.delete_subnet.called) - - @mock.patch.object(shade.OpenStackCloud, 'search_subnets') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_subnet_not_found(self, mock_client, mock_search): - mock_search.return_value = [] - r = self.cloud.delete_subnet('goofy') - self.assertFalse(r) - self.assertFalse(mock_client.delete_subnet.called) - - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_subnet_multiple_found(self, mock_client): - subnet1 = dict(id='123', name='mickey') - subnet2 = dict(id='456', name='mickey') - mock_client.list_subnets.return_value = dict(subnets=[subnet1, - subnet2]) + self.network_name, self.subnet_cidr) + self.assert_calls() + + def test_delete_subnet(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets.json']), + json={'subnets': [self.mock_subnet_rep]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'subnets', '%s.json' % self.subnet_id]), + json={}) + ]) + self.assertTrue(self.cloud.delete_subnet(self.subnet_name)) + self.assert_calls() + + def test_delete_subnet_not_found(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets.json']), + json={'subnets': []}) + ]) + self.assertFalse(self.cloud.delete_subnet('goofy')) + self.assert_calls() + + def test_delete_subnet_multiple_found(self): + subnet1 = dict(id='123', name=self.subnet_name) + subnet2 = dict(id='456', name=self.subnet_name) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets.json']), + json={'subnets': [subnet1, subnet2]}) + ]) self.assertRaises(exc.OpenStackCloudException, self.cloud.delete_subnet, - 'mickey') - self.assertFalse(mock_client.delete_subnet.called) - - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_subnet_multiple_using_id(self, mock_client): - subnet1 = dict(id='123', name='mickey') - subnet2 = dict(id='456', name='mickey') - mock_client.list_subnets.return_value = dict(subnets=[subnet1, - subnet2]) - self.cloud.delete_subnet('123') - self.assertTrue(mock_client.delete_subnet.called) - - @mock.patch.object(shade.OpenStackCloud, 'get_subnet') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_update_subnet(self, mock_client, mock_get): - subnet1 = dict(id='123', name='mickey') - mock_get.return_value = subnet1 - self.cloud.update_subnet('123', subnet_name='goofy') - self.assertTrue(mock_client.update_subnet.called) - - @mock.patch.object(shade.OpenStackCloud, 'get_subnet') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_update_subnet_gateway_ip(self, mock_client, mock_get): - subnet1 = dict(id='456', name='kooky') - mock_get.return_value = subnet1 - gateway = '192.168.200.3' - self.cloud.update_subnet( - '456', gateway_ip=gateway) - self.assertTrue(mock_client.update_subnet.called) - - @mock.patch.object(shade.OpenStackCloud, 'get_subnet') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_update_subnet_disable_gateway_ip(self, mock_client, mock_get): - subnet1 = dict(id='456', name='kooky') - mock_get.return_value = subnet1 - self.cloud.update_subnet( - '456', disable_gateway_ip=True) - self.assertTrue(mock_client.update_subnet.called) - - @mock.patch.object(shade.OpenStackCloud, 'get_subnet') - def test_update_subnet_conflict_gw_ops(self, mock_get): - subnet1 = dict(id='456', name='kooky') - mock_get.return_value = subnet1 - gateway = '192.168.200.3' + self.subnet_name) + self.assert_calls() + + def test_delete_subnet_multiple_using_id(self): + subnet1 = dict(id='123', name=self.subnet_name) + subnet2 = dict(id='456', name=self.subnet_name) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets.json']), + json={'subnets': [subnet1, subnet2]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'subnets', '%s.json' % subnet1['id']]), + json={}) + ]) + self.assertTrue(self.cloud.delete_subnet(subnet1['id'])) + self.assert_calls() + + def test_update_subnet(self): + expected_subnet = copy.copy(self.mock_subnet_rep) + expected_subnet['name'] = 'goofy' + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets.json']), + json={'subnets': [self.mock_subnet_rep]}), + dict(method='PUT', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'subnets', '%s.json' % self.subnet_id]), + json={'subnet': expected_subnet}, + validate=dict( + json={'subnet': {'name': 'goofy'}})) + ]) + subnet = self.cloud.update_subnet(self.subnet_id, subnet_name='goofy') + self.assertDictEqual(expected_subnet, subnet) + self.assert_calls() + + def test_update_subnet_gateway_ip(self): + expected_subnet = copy.copy(self.mock_subnet_rep) + gateway = '192.168.199.3' + expected_subnet['gateway_ip'] = gateway + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets.json']), + json={'subnets': [self.mock_subnet_rep]}), + dict(method='PUT', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'subnets', '%s.json' % self.subnet_id]), + json={'subnet': expected_subnet}, + validate=dict( + json={'subnet': {'gateway_ip': gateway}})) + ]) + subnet = self.cloud.update_subnet(self.subnet_id, gateway_ip=gateway) + self.assertDictEqual(expected_subnet, subnet) + self.assert_calls() + + def test_update_subnet_disable_gateway_ip(self): + expected_subnet = copy.copy(self.mock_subnet_rep) + expected_subnet['gateway_ip'] = None + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets.json']), + json={'subnets': [self.mock_subnet_rep]}), + dict(method='PUT', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'subnets', '%s.json' % self.subnet_id]), + json={'subnet': expected_subnet}, + validate=dict( + json={'subnet': {'gateway_ip': None}})) + ]) + subnet = self.cloud.update_subnet(self.subnet_id, + disable_gateway_ip=True) + self.assertDictEqual(expected_subnet, subnet) + self.assert_calls() + + def test_update_subnet_conflict_gw_ops(self): self.assertRaises(exc.OpenStackCloudException, self.cloud.update_subnet, - '456', gateway_ip=gateway, disable_gateway_ip=True) + self.subnet_id, gateway_ip="192.168.199.3", + disable_gateway_ip=True) From b964757de8dbf9bf0ea7d79923fd5f040816ece4 Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Mon, 24 Apr 2017 11:22:02 -0700 Subject: [PATCH 1458/3836] Define a base function to remove unneeded attributes Many of the _normalize_* methods are removing the same attributes, let's have just define a base function that does that. Change-Id: I5fb9f8fdb14db0889b2f7a63415218b8774cc7bc Signed-off-by: Rosario Di Somma --- shade/_normalize.py | 65 ++++++++++-------------------- shade/tests/unit/test_normalize.py | 17 +++----- 2 files changed, 26 insertions(+), 56 deletions(-) diff --git a/shade/_normalize.py b/shade/_normalize.py index 3993cac79..d71841911 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -142,6 +142,15 @@ def _normalize_compute_limits(self, limits, project_id=None): return new_limits + def _remove_novaclient_artifacts(self, item): + # Remove novaclient artifacts + item.pop('links', None) + item.pop('NAME_ATTR', None) + item.pop('HUMAN_ID', None) + item.pop('human_id', None) + item.pop('request_ids', None) + item.pop('x_openstack_request_ids', None) + def _normalize_flavors(self, flavors): """ Normalize a list of flavor objects """ ret = [] @@ -157,11 +166,8 @@ def _normalize_flavor(self, flavor): flavor = flavor.copy() # Discard noise + self._remove_novaclient_artifacts(flavor) flavor.pop('links', None) - flavor.pop('NAME_ATTR', None) - flavor.pop('HUMAN_ID', None) - flavor.pop('human_id', None) - flavor.pop('request_ids', None) ephemeral = int(_pop_or_get( flavor, 'OS-FLV-EXT-DATA:ephemeral', 0, self.strict_mode)) @@ -211,10 +217,8 @@ def _normalize_image(self, image): # This copy is to keep things from getting epically weird in tests image = image.copy() - image.pop('links', None) - image.pop('NAME_ATTR', None) - image.pop('HUMAN_ID', None) - image.pop('human_id', None) + # Discard noise + self._remove_novaclient_artifacts(image) properties = image.pop('properties', {}) visibility = image.pop('visibility', None) @@ -297,13 +301,8 @@ def _normalize_secgroup(self, group): # Copy incoming group because of shared dicts in unittests group = group.copy() - # Remove novaclient artifacts - group.pop('links', None) - group.pop('NAME_ATTR', None) - group.pop('HUMAN_ID', None) - group.pop('human_id', None) - group.pop('request_ids', None) - group.pop('x_openstack_request_ids', None) + # Discard noise + self._remove_novaclient_artifacts(group) rules = self._normalize_secgroup_rules( group.pop('security_group_rules', group.pop('rules', []))) @@ -395,10 +394,7 @@ def _normalize_server(self, server): # Copy incoming server because of shared dicts in unittests server = server.copy() - server.pop('links', None) - server.pop('NAME_ATTR', None) - server.pop('HUMAN_ID', None) - server.pop('human_id', None) + self._remove_novaclient_artifacts(server) ret['id'] = server.pop('id') ret['name'] = server.pop('name') @@ -578,10 +574,7 @@ def _normalize_project(self, project): project = project.copy() # Discard noise - project.pop('links', None) - project.pop('NAME_ATTR', None) - project.pop('HUMAN_ID', None) - project.pop('human_id', None) + self._remove_novaclient_artifacts(project) # In both v2 and v3 project_id = project.pop('id') @@ -693,12 +686,7 @@ def _normalize_volume(self, volume): volume = volume.copy() # Discard noise - volume.pop('links', None) - volume.pop('NAME_ATTR', None) - volume.pop('HUMAN_ID', None) - volume.pop('human_id', None) - volume.pop('request_ids', None) - volume.pop('x_openstack_request_ids', None) + self._remove_novaclient_artifacts(volume) volume_id = volume.pop('id') name = volume.pop('display_name', None) @@ -777,12 +765,7 @@ def _normalize_volume_attachment(self, attachment): attachment = attachment.copy() # Discard noise - attachment.pop('NAME_ATTR', None) - attachment.pop('HUMAN_ID', None) - attachment.pop('human_id', None) - attachment.pop('request_ids', None) - attachment.pop('x_openstack_request_ids', None) - + self._remove_novaclient_artifacts(attachment) return munch.Munch(**attachment) def _normalize_compute_usage(self, usage): @@ -791,11 +774,7 @@ def _normalize_compute_usage(self, usage): usage = usage.copy() # Discard noise - usage.pop('links', None) - usage.pop('NAME_ATTR', None) - usage.pop('HUMAN_ID', None) - usage.pop('human_id', None) - usage.pop('request_ids', None) + self._remove_novaclient_artifacts(usage) project_id = usage.pop('tenant_id', None) ret = munch.Munch( @@ -972,10 +951,8 @@ def _normalize_stack(self, stack): stack = stack.copy() # Discard noise - stack.pop('HUMAN_ID', None) - stack.pop('human_id', None) - stack.pop('NAME_ATTR', None) - stack.pop('links', None) + self._remove_novaclient_artifacts(stack) + # Discard things heatclient adds that aren't in the REST stack.pop('action', None) stack.pop('status', None) diff --git a/shade/tests/unit/test_normalize.py b/shade/tests/unit/test_normalize.py index 651ba78df..0be594022 100644 --- a/shade/tests/unit/test_normalize.py +++ b/shade/tests/unit/test_normalize.py @@ -314,8 +314,7 @@ def test_normalize_nova_images(self): u'vm_mode': u'hvm', u'xenapi_use_agent': u'False', 'OS-DCF:diskConfig': u'MANUAL', - 'progress': 100, - 'request_ids': []}, + 'progress': 100}, 'minDisk': 20, 'minRam': 0, 'min_disk': 20, @@ -340,10 +339,8 @@ def test_normalize_nova_images(self): u'vm_mode': u'hvm', u'xenapi_use_agent': u'False', 'OS-DCF:diskConfig': u'MANUAL', - 'progress': 100, - 'request_ids': []}, + 'progress': 100}, 'protected': False, - 'request_ids': [], 'size': 323004185, 'status': u'active', 'tags': [], @@ -397,8 +394,7 @@ def test_normalize_nova_images_strict(self): u'vm_mode': u'hvm', u'xenapi_use_agent': u'False', 'OS-DCF:diskConfig': u'MANUAL', - 'progress': 100, - 'request_ids': []}, + 'progress': 100}, 'size': 323004185, 'status': u'active', 'tags': [], @@ -597,8 +593,7 @@ def test_normalize_servers_strict(self): 'power_state': 1, 'private_v4': None, 'progress': 0, - 'properties': { - 'request_ids': []}, + 'properties': {}, 'public_v4': None, 'public_v6': None, 'security_groups': [{u'name': u'default'}], @@ -678,12 +673,10 @@ def test_normalize_servers_normal(self): 'OS-EXT-STS:vm_state': u'active', 'OS-SRV-USG:launched_at': u'2015-08-01T19:52:02.000000', 'OS-SRV-USG:terminated_at': None, - 'os-extended-volumes:volumes_attached': [], - 'request_ids': []}, + 'os-extended-volumes:volumes_attached': []}, 'public_v4': None, 'public_v6': None, 'region': u'RegionOne', - 'request_ids': [], 'security_groups': [{u'name': u'default'}], 'status': u'ACTIVE', 'task_state': None, From 8b6ae89268f90e90c6b564e5fcd0615476448c88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Tue, 25 Apr 2017 12:07:00 +0000 Subject: [PATCH 1459/3836] Replace neutronclient with REST API calls in subnet commands All subnet related commands to Neutron (list/create/update/delete) are now made via keystoneauth Change-Id: I0e0f16f5fb9be7d288b5aa503f4fa6ad51a17aea --- shade/_tasks.py | 20 -------------- shade/openstackcloud.py | 29 +++++++------------- shade/tests/unit/test_floating_ip_neutron.py | 4 ++- 3 files changed, 13 insertions(+), 40 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index a9cab011a..f6af97f9a 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -394,26 +394,6 @@ def main(self, client): return client.neutron_client.update_floatingip(**self.args) -class SubnetCreate(task_manager.Task): - def main(self, client): - return client.neutron_client.create_subnet(**self.args) - - -class SubnetList(task_manager.Task): - def main(self, client): - return client.neutron_client.list_subnets() - - -class SubnetDelete(task_manager.Task): - def main(self, client): - client.neutron_client.delete_subnet(**self.args) - - -class SubnetUpdate(task_manager.Task): - def main(self, client): - return client.neutron_client.update_subnet(**self.args) - - class PortList(task_manager.Task): def main(self, client): return client.neutron_client.list_ports(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 336105ba1..a9628ed0a 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1665,9 +1665,7 @@ def list_subnets(self, filters=None): # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} - with _utils.neutron_exceptions("Error fetching subnet list"): - return self.manager.submit_task( - _tasks.SubnetList(**filters))['subnets'] + return self._network_client.get("/subnets.json", params=filters) def list_ports(self, filters=None): """List all available ports. @@ -6477,13 +6475,10 @@ def create_subnet(self, network_name_or_id, cidr=None, ip_version=4, if use_default_subnetpool: subnet['use_default_subnetpool'] = True - with _utils.neutron_exceptions( - "Error creating subnet on network " - "{0}".format(network_name_or_id)): - new_subnet = self.manager.submit_task( - _tasks.SubnetCreate(body=dict(subnet=subnet))) + new_subnet = self._network_client.post("/subnets.json", + json={"subnet": subnet}) - return new_subnet['subnet'] + return new_subnet def delete_subnet(self, name_or_id): """Delete a subnet. @@ -6503,10 +6498,8 @@ def delete_subnet(self, name_or_id): self.log.debug("Subnet %s not found for deleting", name_or_id) return False - with _utils.neutron_exceptions( - "Error deleting subnet {0}".format(name_or_id)): - self.manager.submit_task( - _tasks.SubnetDelete(subnet=subnet['id'])) + self._network_client.delete( + "/subnets/{subnet_id}.json".format(subnet_id=subnet['id'])) return True def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, @@ -6591,12 +6584,10 @@ def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, raise OpenStackCloudException( "Subnet %s not found." % name_or_id) - with _utils.neutron_exceptions( - "Error updating subnet {0}".format(name_or_id)): - new_subnet = self.manager.submit_task( - _tasks.SubnetUpdate( - subnet=curr_subnet['id'], body=dict(subnet=subnet))) - return new_subnet['subnet'] + new_subnet = self._network_client.put( + "/subnets/{subnet_id}.json".format(subnet_id=curr_subnet['id']), + json={"subnet": subnet}) + return new_subnet @_utils.valid_kwargs('name', 'admin_state_up', 'mac_address', 'fixed_ips', 'subnet_id', 'ip_address', 'security_groups', diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 3c5d2bac6..865987c52 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -587,13 +587,15 @@ def test_auto_ip_pool_no_reuse(self): @patch.object(OpenStackCloud, '_neutron_create_floating_ip') @patch.object(OpenStackCloud, '_neutron_list_floating_ips') @patch.object(OpenStackCloud, 'list_networks') + @patch.object(OpenStackCloud, 'list_subnets') @patch.object(OpenStackCloud, 'has_service') def test_available_floating_ip_new( - self, mock_has_service, mock_list_networks, + self, mock_has_service, mock_list_subnets, mock_list_networks, mock__neutron_list_floating_ips, mock__neutron_create_floating_ip, mock_keystone_session): mock_has_service.return_value = True mock_list_networks.return_value = [self.mock_get_network_rep] + mock_list_subnets.return_value = [] mock__neutron_list_floating_ips.return_value = [] mock__neutron_create_floating_ip.return_value = \ self.mock_floating_ip_new_rep['floatingip'] From a71bea882d9157d19e829dd2cc61fe6ec48575a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Tue, 25 Apr 2017 21:39:41 +0000 Subject: [PATCH 1460/3836] Remove neutronclient mocks from router tests Neutronclient mock is replaced by mocking REST calls using base.RequestsMockTestCase class Change-Id: I54769c34fef4f64150461fef47881bd400313335 --- shade/tests/unit/test_router.py | 484 +++++++++++++++++++------------- 1 file changed, 295 insertions(+), 189 deletions(-) diff --git a/shade/tests/unit/test_router.py b/shade/tests/unit/test_router.py index 63e885454..f9030e4df 100644 --- a/shade/tests/unit/test_router.py +++ b/shade/tests/unit/test_router.py @@ -13,237 +13,343 @@ # See the License for the specific language governing permissions and # limitations under the License. -import mock +import copy -import shade from shade import exc from shade.tests.unit import base class TestRouter(base.RequestsMockTestCase): - @mock.patch.object(shade.OpenStackCloud, 'search_routers') - def test_get_router(self, mock_search): - router1 = dict(id='123', name='mickey') - mock_search.return_value = [router1] - r = self.cloud.get_router('mickey') + router_name = 'goofy' + router_id = '57076620-dcfb-42ed-8ad6-79ccb4a79ed2' + subnet_id = '1f1696eb-7f47-47f6-835c-4889bff88604' + + mock_router_rep = { + 'admin_state_up': True, + 'availability_zone_hints': [], + 'availability_zones': [], + 'description': u'', + 'distributed': False, + 'external_gateway_info': None, + 'flavor_id': None, + 'ha': False, + 'id': router_id, + 'name': router_name, + 'project_id': u'861808a93da0484ea1767967c4df8a23', + 'routes': [], + 'status': u'ACTIVE', + 'tenant_id': u'861808a93da0484ea1767967c4df8a23' + } + + mock_router_interface_rep = { + 'network_id': '53aee281-b06d-47fc-9e1a-37f045182b8e', + 'subnet_id': '1f1696eb-7f47-47f6-835c-4889bff88604', + 'tenant_id': '861808a93da0484ea1767967c4df8a23', + 'subnet_ids': [subnet_id], + 'port_id': '23999891-78b3-4a6b-818d-d1b713f67848', + 'id': '57076620-dcfb-42ed-8ad6-79ccb4a79ed2', + 'request_ids': ['req-f1b0b1b4-ae51-4ef9-b371-0cc3c3402cf7'] + } + + def test_get_router(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'routers.json']), + json={'routers': [self.mock_router_rep]}) + ]) + r = self.cloud.get_router(self.router_name) self.assertIsNotNone(r) - self.assertDictEqual(router1, r) + self.assertDictEqual(self.mock_router_rep, r) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'search_routers') - def test_get_router_not_found(self, mock_search): - mock_search.return_value = [] - r = self.cloud.get_router('goofy') + def test_get_router_not_found(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'routers.json']), + json={'routers': []}) + ]) + r = self.cloud.get_router('mickey') self.assertIsNone(r) + self.assert_calls() + + def test_create_router(self): + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'routers.json']), + json={'router': self.mock_router_rep}, + validate=dict( + json={'router': { + 'name': self.router_name, + 'admin_state_up': True}})) + ]) + new_router = self.cloud.create_router(name=self.router_name, + admin_state_up=True) + self.assertDictEqual(self.mock_router_rep, new_router) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_router(self, mock_client): - self.cloud.create_router(name='goofy', admin_state_up=True) - self.assertTrue(mock_client.create_router.called) - - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_router_specific_tenant(self, mock_client): - self.cloud.create_router("goofy", project_id="project_id_value") - mock_client.create_router.assert_called_once_with( - body=dict( - router=dict( - name='goofy', - admin_state_up=True, - tenant_id="project_id_value", - ) - ) - ) - - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_router_with_enable_snat_True(self, mock_client): + def test_create_router_specific_tenant(self): + new_router_tenant_id = "project_id_value" + mock_router_rep = copy.copy(self.mock_router_rep) + mock_router_rep['tenant_id'] = new_router_tenant_id + mock_router_rep['project_id'] = new_router_tenant_id + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'routers.json']), + json={'router': mock_router_rep}, + validate=dict( + json={'router': { + 'name': self.router_name, + 'admin_state_up': True, + 'tenant_id': new_router_tenant_id}})) + ]) + + self.cloud.create_router(self.router_name, + project_id=new_router_tenant_id) + self.assert_calls() + + def test_create_router_with_enable_snat_True(self): """Do not send enable_snat when same as neutron default.""" - self.cloud.create_router(name='goofy', admin_state_up=True, - enable_snat=True) - mock_client.create_router.assert_called_once_with( - body=dict( - router=dict( - name='goofy', - admin_state_up=True, - ) - ) - ) - - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_router_with_enable_snat_False(self, mock_client): + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'routers.json']), + json={'router': self.mock_router_rep}, + validate=dict( + json={'router': { + 'name': self.router_name, + 'admin_state_up': True}})) + ]) + self.cloud.create_router( + name=self.router_name, admin_state_up=True, enable_snat=True) + self.assert_calls() + + def test_create_router_with_enable_snat_False(self): """Send enable_snat when it is False.""" - self.cloud.create_router(name='goofy', admin_state_up=True, - enable_snat=False) - mock_client.create_router.assert_called_once_with( - body=dict( - router=dict( - name='goofy', - admin_state_up=True, - external_gateway_info=dict( - enable_snat=False - ) - ) - ) - ) - - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_add_router_interface(self, mock_client): - self.cloud.add_router_interface({'id': '123'}, subnet_id='abc') - mock_client.add_interface_router.assert_called_once_with( - router='123', body={'subnet_id': 'abc'} - ) - - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_remove_router_interface(self, mock_client): - self.cloud.remove_router_interface({'id': '123'}, subnet_id='abc') - mock_client.remove_interface_router.assert_called_once_with( - router='123', body={'subnet_id': 'abc'} - ) + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'routers.json']), + json={'router': self.mock_router_rep}, + validate=dict( + json={'router': { + 'name': self.router_name, + 'external_gateway_info': {'enable_snat': False}, + 'admin_state_up': True}})) + ]) + self.cloud.create_router( + name=self.router_name, admin_state_up=True, enable_snat=False) + self.assert_calls() + + def test_add_router_interface(self): + self.register_uris([ + dict(method='PUT', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'routers', self.router_id, + 'add_router_interface.json']), + json={'port': self.mock_router_interface_rep}, + validate=dict( + json={'subnet_id': self.subnet_id})) + ]) + self.cloud.add_router_interface( + {'id': self.router_id}, subnet_id=self.subnet_id) + self.assert_calls() + + def test_remove_router_interface(self): + self.register_uris([ + dict(method='PUT', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'routers', self.router_id, + 'remove_router_interface.json']), + json={'port': self.mock_router_interface_rep}, + validate=dict( + json={'subnet_id': self.subnet_id})) + ]) + self.cloud.remove_router_interface( + {'id': self.router_id}, subnet_id=self.subnet_id) + self.assert_calls() def test_remove_router_interface_missing_argument(self): self.assertRaises(ValueError, self.cloud.remove_router_interface, {'id': '123'}) - @mock.patch.object(shade.OpenStackCloud, 'get_router') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_update_router(self, mock_client, mock_get): - router1 = dict(id='123', name='mickey') - mock_get.return_value = router1 - self.cloud.update_router('123', name='goofy') - self.assertTrue(mock_client.update_router.called) + def test_update_router(self): + new_router_name = "mickey" + expected_router_rep = copy.copy(self.mock_router_rep) + expected_router_rep['name'] = new_router_name + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'routers.json']), + json={'routers': [self.mock_router_rep]}), + dict(method='PUT', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'routers', '%s.json' % self.router_id]), + json={'router': expected_router_rep}, + validate=dict( + json={'router': { + 'name': new_router_name}})) + ]) + new_router = self.cloud.update_router( + self.router_id, name=new_router_name) + self.assertDictEqual(expected_router_rep, new_router) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'search_routers') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_router(self, mock_client, mock_search): - router1 = dict(id='123', name='mickey') - mock_search.return_value = [router1] - self.cloud.delete_router('mickey') - self.assertTrue(mock_client.delete_router.called) - - @mock.patch.object(shade.OpenStackCloud, 'search_routers') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_router_not_found(self, mock_client, mock_search): - mock_search.return_value = [] - r = self.cloud.delete_router('goofy') - self.assertFalse(r) - self.assertFalse(mock_client.delete_router.called) - - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_router_multiple_found(self, mock_client): + def test_delete_router(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'routers.json']), + json={'routers': [self.mock_router_rep]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'routers', '%s.json' % self.router_id]), + json={}) + ]) + self.assertTrue(self.cloud.delete_router(self.router_name)) + self.assert_calls() + + def test_delete_router_not_found(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'routers.json']), + json={'routers': []}), + ]) + self.assertFalse(self.cloud.delete_router(self.router_name)) + self.assert_calls() + + def test_delete_router_multiple_found(self): router1 = dict(id='123', name='mickey') router2 = dict(id='456', name='mickey') - mock_client.list_routers.return_value = dict(routers=[router1, - router2]) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'routers.json']), + json={'routers': [router1, router2]}), + ]) self.assertRaises(exc.OpenStackCloudException, self.cloud.delete_router, 'mickey') - self.assertFalse(mock_client.delete_router.called) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_router_multiple_using_id(self, mock_client): + def test_delete_router_multiple_using_id(self): router1 = dict(id='123', name='mickey') router2 = dict(id='456', name='mickey') - mock_client.list_routers.return_value = dict(routers=[router1, - router2]) - self.cloud.delete_router('123') - self.assertTrue(mock_client.delete_router.called) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'routers.json']), + json={'routers': [router1, router2]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'routers', '123.json']), + json={}) + ]) + self.assertTrue(self.cloud.delete_router("123")) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'search_ports') - def test_list_router_interfaces_no_gw(self, mock_search): + def _test_list_router_interfaces(self, router, interface_type, + expected_result=None): + internal_port = { + 'id': 'internal_port_id', + 'fixed_ips': [{ + 'subnet_id': 'internal_subnet_id', + 'ip_address': "10.0.0.1" + }], + 'device_id': self.router_id + } + external_port = { + 'id': 'external_port_id', + 'fixed_ips': [{ + 'subnet_id': 'external_subnet_id', + 'ip_address': "1.2.3.4" + }], + 'device_id': self.router_id + } + if expected_result is None: + if interface_type == "internal": + expected_result = [internal_port] + elif interface_type == "external": + expected_result = [external_port] + else: + expected_result = [internal_port, external_port] + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json'], + qs_elements=["device_id=%s" % self.router_id]), + json={'ports': [internal_port, external_port]}) + ]) + ret = self.cloud.list_router_interfaces(router, interface_type) + self.assertEqual(expected_result, ret) + self.assert_calls() + + def test_list_router_interfaces_no_gw(self): """ If a router does not have external_gateway_info, do not fail. """ - external_port = {'id': 'external_port_id', - 'fixed_ips': [ - ('external_subnet_id', 'ip_address'), - ]} - port_list = [external_port] router = { - 'id': 'router_id', + 'id': self.router_id + } + self._test_list_router_interfaces(router, + interface_type="external", + expected_result=[]) + + def test_list_router_interfaces_gw_none(self): + """ + If a router does have external_gateway_info set to None, do not fail. + """ + router = { + 'id': self.router_id, + 'external_gateway_info': None } - mock_search.return_value = port_list - ret = self.cloud.list_router_interfaces(router, - interface_type='external') - mock_search.assert_called_once_with( - filters={'device_id': router['id']} - ) - self.assertEqual([], ret) - - # A router can have its external_gateway_info set to None - router['external_gateway_info'] = None - ret = self.cloud.list_router_interfaces(router, - interface_type='external') - self.assertEqual([], ret) - - @mock.patch.object(shade.OpenStackCloud, 'search_ports') - def test_list_router_interfaces_all(self, mock_search): - internal_port = {'id': 'internal_port_id', - 'fixed_ips': [ - ('internal_subnet_id', 'ip_address'), - ]} - external_port = {'id': 'external_port_id', - 'fixed_ips': [ - ('external_subnet_id', 'ip_address'), - ]} - port_list = [internal_port, external_port] + self._test_list_router_interfaces(router, + interface_type="external", + expected_result=[]) + + def test_list_router_interfaces_all(self): router = { - 'id': 'router_id', + 'id': self.router_id, 'external_gateway_info': { - 'external_fixed_ips': [('external_subnet_id', 'ip_address')] + 'external_fixed_ips': [{ + 'subnet_id': 'external_subnet_id', + 'ip_address': '1.2.3.4'}] } } - mock_search.return_value = port_list - ret = self.cloud.list_router_interfaces(router) - mock_search.assert_called_once_with( - filters={'device_id': router['id']} - ) - self.assertEqual(port_list, ret) - - @mock.patch.object(shade.OpenStackCloud, 'search_ports') - def test_list_router_interfaces_internal(self, mock_search): - internal_port = {'id': 'internal_port_id', - 'fixed_ips': [ - ('internal_subnet_id', 'ip_address'), - ]} - external_port = {'id': 'external_port_id', - 'fixed_ips': [ - ('external_subnet_id', 'ip_address'), - ]} - port_list = [internal_port, external_port] + self._test_list_router_interfaces(router, + interface_type=None) + + def test_list_router_interfaces_internal(self): router = { - 'id': 'router_id', + 'id': self.router_id, 'external_gateway_info': { - 'external_fixed_ips': [('external_subnet_id', 'ip_address')] + 'external_fixed_ips': [{ + 'subnet_id': 'external_subnet_id', + 'ip_address': '1.2.3.4'}] } } - mock_search.return_value = port_list - ret = self.cloud.list_router_interfaces(router, - interface_type='internal') - mock_search.assert_called_once_with( - filters={'device_id': router['id']} - ) - self.assertEqual([internal_port], ret) - - @mock.patch.object(shade.OpenStackCloud, 'search_ports') - def test_list_router_interfaces_external(self, mock_search): - internal_port = {'id': 'internal_port_id', - 'fixed_ips': [ - ('internal_subnet_id', 'ip_address'), - ]} - external_port = {'id': 'external_port_id', - 'fixed_ips': [ - ('external_subnet_id', 'ip_address'), - ]} - port_list = [internal_port, external_port] + self._test_list_router_interfaces(router, + interface_type="internal") + + def test_list_router_interfaces_external(self): router = { - 'id': 'router_id', + 'id': self.router_id, 'external_gateway_info': { - 'external_fixed_ips': [('external_subnet_id', 'ip_address')] + 'external_fixed_ips': [{ + 'subnet_id': 'external_subnet_id', + 'ip_address': '1.2.3.4'}] } } - mock_search.return_value = port_list - ret = self.cloud.list_router_interfaces(router, - interface_type='external') - mock_search.assert_called_once_with( - filters={'device_id': router['id']} - ) - self.assertEqual([external_port], ret) + self._test_list_router_interfaces(router, + interface_type="external") From ca2f10b2e65cddf634d5b9d6f7b252e43e4bc93b Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Tue, 25 Apr 2017 17:27:55 -0700 Subject: [PATCH 1461/3836] Use requests-mock for the volume backup tests Change-Id: I88d7351a2fc147224542898d08e2a769a62577a0 Signed-off-by: Rosario Di Somma --- shade/tests/unit/test_volume_backups.py | 153 ++++++++++++++++-------- 1 file changed, 102 insertions(+), 51 deletions(-) diff --git a/shade/tests/unit/test_volume_backups.py b/shade/tests/unit/test_volume_backups.py index 4121ad462..84afadfaa 100644 --- a/shade/tests/unit/test_volume_backups.py +++ b/shade/tests/unit/test_volume_backups.py @@ -9,64 +9,115 @@ # 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 shade +from shade import meta from shade.tests.unit import base -class TestVolumeBackups(base.TestCase): - @mock.patch.object(shade.OpenStackCloud, 'list_volume_backups') - @mock.patch("shade._utils._filter_list") - def test_search_volume_backups(self, m_filter_list, m_list_volume_backups): - result = self.cloud.search_volume_backups( - mock.sentinel.name_or_id, mock.sentinel.filter) +class TestVolumeBackups(base.RequestsMockTestCase): + def _get_normalized_dict_from_object(self, backup): + self.cloud._remove_novaclient_artifacts(backup) + return meta.obj_to_dict(backup) - m_list_volume_backups.assert_called_once_with() - m_filter_list.assert_called_once_with( - m_list_volume_backups.return_value, mock.sentinel.name_or_id, - mock.sentinel.filter) - self.assertIs(m_filter_list.return_value, result) + def test_search_volume_backups(self): + name = 'Volume1' + vol1 = {'name': name, 'availability_zone': 'az1'} + vol2 = {'name': name, 'availability_zone': 'az1'} + vol3 = {'name': 'Volume2', 'availability_zone': 'az2'} + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['backups', 'detail']), + json={"backups": [vol1, vol2, vol3]})]) + result = self.cloud.search_volume_backups( + name, {'availability_zone': 'az1'}) + result = [self._get_normalized_dict_from_object(i) for i in result] + self.assertEqual(len(result), 2) + self.assertEqual([vol1, vol2], result) + self.assert_calls() - @mock.patch("shade._utils._get_entity") - def test_get_volume_backup(self, m_get_entity): + def test_get_volume_backup(self): + name = 'Volume1' + vol1 = {'name': name, 'availability_zone': 'az1'} + vol2 = {'name': name, 'availability_zone': 'az2'} + vol3 = {'name': 'Volume2', 'availability_zone': 'az1'} + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['backups', 'detail']), + json={"backups": [vol1, vol2, vol3]})]) result = self.cloud.get_volume_backup( - mock.sentinel.name_or_id, mock.sentinel.filter) + name, {'availability_zone': 'az1'}) + result = self._get_normalized_dict_from_object(result) + self.assertEqual(vol1, result) + self.assert_calls() - self.assertIs(m_get_entity.return_value, result) - m_get_entity.assert_called_once_with( - self.cloud.search_volume_backups, mock.sentinel.name_or_id, - mock.sentinel.filter) + def test_list_volume_backups(self): + backup = {'id': '6ff16bdf-44d5-4bf9-b0f3-687549c76414', + 'status': 'available'} + search_opts = {'status': 'available'} + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['backups', 'detail'], + qs_elements=['='.join(i) for i in search_opts.items()]), + json={"backups": [backup]})]) + result = self.cloud.list_volume_backups(True, search_opts) + result = [self._get_normalized_dict_from_object(i) for i in result] + self.assertEqual(len(result), 1) + self.assertEqual([backup], result) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'cinder_client') - def test_list_volume_backups(self, m_cinder_client): + def test_delete_volume_backup_wait(self): backup_id = '6ff16bdf-44d5-4bf9-b0f3-687549c76414' - m_cinder_client.backups.list.return_value = [ - {'id': backup_id} - ] - result = self.cloud.list_volume_backups( - mock.sentinel.detailed, mock.sentinel.search_opts) - - m_cinder_client.backups.list.assert_called_once_with( - detailed=mock.sentinel.detailed, - search_opts=mock.sentinel.search_opts) - self.assertEqual(backup_id, result[0]['id']) + backup = {'id': backup_id} + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['backups', 'detail']), + json={"backups": [backup]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['backups', backup_id])), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['backups', 'detail']), + json={"backups": [backup]}), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['backups', 'detail']), + json={"backups": []})]) + self.cloud.delete_volume_backup(backup_id, False, True, 1) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'cinder_client') - @mock.patch("shade._utils._iterate_timeout") - @mock.patch.object(shade.OpenStackCloud, 'get_volume_backup') - def test_delete_volume_backup(self, m_get_volume_backup, - m_iterate_timeout, m_cinder_client): - m_get_volume_backup.side_effect = [{'id': 42}, True, False] - self.cloud.delete_volume_backup( - mock.sentinel.name_or_id, mock.sentinel.force, mock.sentinel.wait, - mock.sentinel.timeout) - - m_iterate_timeout.assert_called_once_with( - mock.sentinel.timeout, mock.ANY) - m_cinder_client.backups.delete.assert_called_once_with( - backup=42, force=mock.sentinel.force) - - # We expect 3 calls, the last return_value is False which breaks the - # wait loop. - m_get_volume_backup.call_args_list = [mock.call(42)] * 3 + def test_delete_volume_backup_force(self): + backup_id = '6ff16bdf-44d5-4bf9-b0f3-687549c76414' + backup = {'id': backup_id} + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['backups', 'detail']), + json={"backups": [backup]}), + dict(method='POST', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['backups', backup_id, 'action']), + json={'os-force_delete': {}}, + validate=dict(json={u'os-force_delete': None})), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['backups', 'detail']), + json={"backups": [backup]}), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['backups', 'detail']), + json={"backups": []}) + ]) + self.cloud.delete_volume_backup(backup_id, True, True, 1) + self.assert_calls() From 749abc7195241533faabe207c4b28f31b690e239 Mon Sep 17 00:00:00 2001 From: Duan Jiong Date: Wed, 26 Apr 2017 14:31:22 +0800 Subject: [PATCH 1462/3836] Correct Network `ports` query parameters ip_address is spelled incorrectly as ip_adress Change-Id: I0975d0bb7e81df048d8a59a755c338b2277025df --- openstack/network/v2/_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 591b095c6..d0d3a936a 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -1475,7 +1475,7 @@ def ports(self, **query): * ``description``: The port description. * ``device_id``: Port device ID. * ``device_owner``: Port device owner (e.g. ``network:dhcp``). - * ``ip_adress``: IP addresses of an allowed address pair. + * ``ip_address``: IP addresses of an allowed address pair. * ``is_admin_state_up``: The administrative state of the port. * ``is_port_security_enabled``: The port security status. * ``mac_address``: Port MAC address. From 60569b2fbc24c8a07c07d47b0608744e654d807d Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Wed, 26 Apr 2017 07:40:25 -0700 Subject: [PATCH 1463/3836] Add a _normalize_volume_backups method Change-Id: I597a8a8b113ebded06efb727169998dd0ba98c2c Signed-off-by: Rosario Di Somma --- shade/_normalize.py | 14 ++++++++++++++ shade/openstackcloud.py | 7 ++++--- shade/tests/unit/test_volume_backups.py | 20 ++++++++++---------- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/shade/_normalize.py b/shade/_normalize.py index d71841911..3b3d3c5cc 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -768,6 +768,20 @@ def _normalize_volume_attachment(self, attachment): self._remove_novaclient_artifacts(attachment) return munch.Munch(**attachment) + def _normalize_volume_backups(self, backups): + ret = [] + for backup in backups: + ret.append(self._normalize_volume_backup(backup)) + return ret + + def _normalize_volume_backup(self, backup): + """ Normalize a valume backup object""" + + backup = backup.copy() + # Discard noise + self._remove_novaclient_artifacts(backup) + return munch.Munch(**backup) + def _normalize_compute_usage(self, usage): """ Normalize a compute usage object """ diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 336105ba1..83d90fe90 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4288,9 +4288,10 @@ def list_volume_backups(self, detailed=True, search_opts=None): :returns: A list of volume backups ``munch.Munch``. """ with _utils.shade_exceptions("Error getting a list of backups"): - return self.manager.submit_task( - _tasks.VolumeBackupList( - detailed=detailed, search_opts=search_opts)) + return self._normalize_volume_backups( + self.manager.submit_task( + _tasks.VolumeBackupList( + detailed=detailed, search_opts=search_opts))) def delete_volume_backup(self, name_or_id=None, force=False, wait=False, timeout=None): diff --git a/shade/tests/unit/test_volume_backups.py b/shade/tests/unit/test_volume_backups.py index 84afadfaa..17298937a 100644 --- a/shade/tests/unit/test_volume_backups.py +++ b/shade/tests/unit/test_volume_backups.py @@ -14,10 +14,6 @@ class TestVolumeBackups(base.RequestsMockTestCase): - def _get_normalized_dict_from_object(self, backup): - self.cloud._remove_novaclient_artifacts(backup) - return meta.obj_to_dict(backup) - def test_search_volume_backups(self): name = 'Volume1' vol1 = {'name': name, 'availability_zone': 'az1'} @@ -30,9 +26,10 @@ def test_search_volume_backups(self): json={"backups": [vol1, vol2, vol3]})]) result = self.cloud.search_volume_backups( name, {'availability_zone': 'az1'}) - result = [self._get_normalized_dict_from_object(i) for i in result] self.assertEqual(len(result), 2) - self.assertEqual([vol1, vol2], result) + self.assertEqual( + meta.obj_list_to_dict([vol1, vol2]), + result) self.assert_calls() def test_get_volume_backup(self): @@ -47,8 +44,10 @@ def test_get_volume_backup(self): json={"backups": [vol1, vol2, vol3]})]) result = self.cloud.get_volume_backup( name, {'availability_zone': 'az1'}) - result = self._get_normalized_dict_from_object(result) - self.assertEqual(vol1, result) + result = meta.obj_to_dict(result) + self.assertEqual( + meta.obj_to_dict(vol1), + result) self.assert_calls() def test_list_volume_backups(self): @@ -62,9 +61,10 @@ def test_list_volume_backups(self): qs_elements=['='.join(i) for i in search_opts.items()]), json={"backups": [backup]})]) result = self.cloud.list_volume_backups(True, search_opts) - result = [self._get_normalized_dict_from_object(i) for i in result] self.assertEqual(len(result), 1) - self.assertEqual([backup], result) + self.assertEqual( + meta.obj_list_to_dict([backup]), + result) self.assert_calls() def test_delete_volume_backup_wait(self): From 7fb7c105508df70a17e583a16ad21f2fe100a2f4 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Wed, 26 Apr 2017 08:56:45 -0400 Subject: [PATCH 1464/3836] Port database v1 to resource2 This change ports the database service over to resource2. It's generally a renaming, though there are some changes from the old path_args way to the new one in the proxy. Change-Id: Ib7e0ac2cf449ea1e60fcbf45ff1e46d690c1cf77 --- openstack/database/v1/_proxy.py | 82 ++++++++++++++----- openstack/database/v1/database.py | 11 ++- openstack/database/v1/flavor.py | 10 +-- openstack/database/v1/instance.py | 27 ++++-- openstack/database/v1/user.py | 34 ++++---- .../tests/unit/database/v1/test_database.py | 5 +- .../tests/unit/database/v1/test_flavor.py | 4 +- .../tests/unit/database/v1/test_instance.py | 24 ++++-- .../tests/unit/database/v1/test_proxy.py | 49 ++++++++--- openstack/tests/unit/database/v1/test_user.py | 30 ++----- 10 files changed, 177 insertions(+), 99 deletions(-) diff --git a/openstack/database/v1/_proxy.py b/openstack/database/v1/_proxy.py index cb8351e5f..b06e4fdb5 100644 --- a/openstack/database/v1/_proxy.py +++ b/openstack/database/v1/_proxy.py @@ -14,14 +14,16 @@ from openstack.database.v1 import flavor as _flavor from openstack.database.v1 import instance as _instance from openstack.database.v1 import user as _user -from openstack import proxy +from openstack import proxy2 -class Proxy(proxy.BaseProxy): +class Proxy(proxy2.BaseProxy): - def create_database(self, **attrs): + def create_database(self, instance, **attrs): """Create a new database from attributes + :param instance: This can be either the ID of an instance + or a :class:`~openstack.database.v1.instance.Instance` :param dict attrs: Keyword arguments which will be used to create a :class:`~openstack.database.v1.database.Database`, comprised of the properties on the Database class. @@ -29,13 +31,19 @@ def create_database(self, **attrs): :returns: The results of server creation :rtype: :class:`~openstack.database.v1.database.Database` """ - return self._create(_database.Database, **attrs) + instance = self._get_resource(_instance.Instance, instance) + return self._create(_database.Database, instance_id=instance.id, + **attrs) - def delete_database(self, database, ignore_missing=True): + def delete_database(self, database, instance=None, ignore_missing=True): """Delete a database :param database: The value can be either the ID of a database or a :class:`~openstack.database.v1.database.Database` instance. + :param instance: This parameter needs to be specified when + an ID is given as `database`. + It can be either the ID of an instance + or a :class:`~openstack.database.v1.instance.Instance` :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the database does not exist. @@ -44,13 +52,17 @@ def delete_database(self, database, ignore_missing=True): :returns: ``None`` """ - self._delete(_database.Database, database, + instance_id = self._get_uri_attribute(database, instance, + "instance_id") + self._delete(_database.Database, database, instance_id=instance_id, ignore_missing=ignore_missing) - def find_database(self, name_or_id, ignore_missing=True): + def find_database(self, name_or_id, instance, ignore_missing=True): """Find a single database :param name_or_id: The name or ID of a database. + :param instance: This can be either the ID of an instance + or a :class:`~openstack.database.v1.instance.Instance` :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the resource does not exist. @@ -58,23 +70,34 @@ def find_database(self, name_or_id, ignore_missing=True): attempting to find a nonexistent resource. :returns: One :class:`~openstack.database.v1.database.Database` or None """ + instance = self._get_resource(_instance.Instance, instance) return self._find(_database.Database, name_or_id, + instance_id=instance.id, ignore_missing=ignore_missing) - def databases(self, **query): + def databases(self, instance, **query): """Return a generator of databases + :param instance: This can be either the ID of an instance + or a :class:`~openstack.database.v1.instance.Instance` + instance that the interface belongs to. :param kwargs \*\*query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of database objects :rtype: :class:`~openstack.database.v1.database.Database` """ - return self._list(_database.Database, paginated=False, **query) + instance = self._get_resource(_instance.Instance, instance) + return self._list(_database.Database, paginated=False, + instance_id=instance.id, **query) - def get_database(self, database): + def get_database(self, database, instance=None): """Get a single database + :param instance: This parameter needs to be specified when + an ID is given as `database`. + It can be either the ID of an instance + or a :class:`~openstack.database.v1.instance.Instance` :param database: The value can be the ID of a database or a :class:`~openstack.database.v1.database.Database` instance. @@ -202,9 +225,11 @@ def update_instance(self, instance, **attrs): """ return self._update(_instance.Instance, instance, **attrs) - def create_user(self, **attrs): + def create_user(self, instance, **attrs): """Create a new user from attributes + :param instance: This can be either the ID of an instance + or a :class:`~openstack.database.v1.instance.Instance` :param dict attrs: Keyword arguments which will be used to create a :class:`~openstack.database.v1.user.User`, comprised of the properties on the User class. @@ -212,13 +237,18 @@ def create_user(self, **attrs): :returns: The results of server creation :rtype: :class:`~openstack.database.v1.user.User` """ - return self._create(_user.User, **attrs) + instance = self._get_resource(_instance.Instance, instance) + return self._create(_user.User, instance_id=instance.id, **attrs) - def delete_user(self, user, ignore_missing=True): + def delete_user(self, user, instance=None, ignore_missing=True): """Delete a user :param user: The value can be either the ID of a user or a :class:`~openstack.database.v1.user.User` instance. + :param instance: This parameter needs to be specified when + an ID is given as `user`. + It can be either the ID of an instance + or a :class:`~openstack.database.v1.instance.Instance` :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the user does not exist. @@ -227,12 +257,16 @@ def delete_user(self, user, ignore_missing=True): :returns: ``None`` """ - self._delete(_user.User, user, ignore_missing=ignore_missing) + instance = self._get_resource(_instance.Instance, instance) + self._delete(_user.User, user, ignore_missing=ignore_missing, + instance_id=instance.id) - def find_user(self, name_or_id, ignore_missing=True): + def find_user(self, name_or_id, instance, ignore_missing=True): """Find a single user :param name_or_id: The name or ID of a user. + :param instance: This can be either the ID of an instance + or a :class:`~openstack.database.v1.instance.Instance` :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the resource does not exist. @@ -240,28 +274,38 @@ def find_user(self, name_or_id, ignore_missing=True): attempting to find a nonexistent resource. :returns: One :class:`~openstack.database.v1.user.User` or None """ - return self._find(_user.User, name_or_id, + instance = self._get_resource(_instance.Instance, instance) + return self._find(_user.User, name_or_id, instance_id=instance.id, ignore_missing=ignore_missing) - def users(self, **query): + def users(self, instance, **query): """Return a generator of users + :param instance: This can be either the ID of an instance + or a :class:`~openstack.database.v1.instance.Instance` :param kwargs \*\*query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of user objects :rtype: :class:`~openstack.database.v1.user.User` """ - return self._list(_user.User, paginated=False, **query) + instance = self._get_resource(_instance.Instance, instance) + return self._list(_user.User, instance_id=instance.id, + paginated=False, **query) - def get_user(self, user): + def get_user(self, user, instance=None): """Get a single user :param user: The value can be the ID of a user or a :class:`~openstack.database.v1.user.User` instance. + :param instance: This parameter needs to be specified when + an ID is given as `database`. + It can be either the ID of an instance + or a :class:`~openstack.database.v1.instance.Instance` :returns: One :class:`~openstack.database.v1.user.User` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ + instance = self._get_resource(_instance.Instance, instance) return self._get(_user.User, user) diff --git a/openstack/database/v1/database.py b/openstack/database/v1/database.py index 6181a977c..3270ea98a 100644 --- a/openstack/database/v1/database.py +++ b/openstack/database/v1/database.py @@ -11,11 +11,10 @@ # under the License. from openstack.database import database_service -from openstack import resource +from openstack import resource2 as resource class Database(resource.Resource): - id_attribute = 'name' resource_key = 'database' resources_key = 'databases' base_path = '/instances/%(instance_id)s/databases' @@ -28,11 +27,11 @@ class Database(resource.Resource): # Properties #: Set of symbols and encodings. The default character set is ``utf8``. - character_set = resource.prop('character_set') + character_set = resource.Body('character_set') #: Set of rules for comparing characters in a character set. #: The default value for collate is ``utf8_general_ci``. - collate = resource.prop('collate') + collate = resource.Body('collate') #: The ID of the instance - instance_id = resource.prop('instance_id') + instance_id = resource.URI('instance_id') #: The name of the database - name = resource.prop('name') + name = resource.Body('name', alternate_id=True) diff --git a/openstack/database/v1/flavor.py b/openstack/database/v1/flavor.py index 54cbecee4..38a06098e 100644 --- a/openstack/database/v1/flavor.py +++ b/openstack/database/v1/flavor.py @@ -11,7 +11,7 @@ # under the License. from openstack.database import database_service -from openstack import resource +from openstack import resource2 as resource class Flavor(resource.Resource): @@ -22,12 +22,12 @@ class Flavor(resource.Resource): # capabilities allow_list = True - allow_retrieve = True + allow_get = True # Properties #: Links associated with the flavor - links = resource.prop('links') + links = resource.Body('links') #: The name of the flavor - name = resource.prop('name') + name = resource.Body('name') #: The size in MB of RAM the flavor has - ram = resource.prop('ram') + ram = resource.Body('ram') diff --git a/openstack/database/v1/instance.py b/openstack/database/v1/instance.py index 18865eae8..97660a981 100644 --- a/openstack/database/v1/instance.py +++ b/openstack/database/v1/instance.py @@ -11,7 +11,7 @@ # under the License. from openstack.database import database_service -from openstack import resource +from openstack import resource2 as resource from openstack import utils @@ -23,22 +23,35 @@ class Instance(resource.Resource): # capabilities allow_create = True - allow_retrieve = True + allow_get = True allow_update = True allow_delete = True allow_list = True # Properties #: The flavor of the instance - flavor = resource.prop('flavor') + flavor = resource.Body('flavor') #: Links associated with the instance - links = resource.prop('links') + links = resource.Body('links') #: The name of the instance - name = resource.prop('name') + name = resource.Body('name') #: The status of the instance - status = resource.prop('status') + status = resource.Body('status') #: The size of the volume - volume = resource.prop('volume') + volume = resource.Body('volume') + #: A dictionary of datastore details, often including 'type' and 'version' + #: keys + datastore = resource.Body('datastore', type=dict) + #: The ID of this instance + id = resource.Body('id') + #: The region this instance resides in + region = resource.Body('region') + #: The name of the host + hostname = resource.Body('hostname') + #: The timestamp when this instance was created + created_at = resource.Body('created') + #: The timestamp when this instance was updated + updated_at = resource.Body('updated') def enable_root_user(self, session): """Enable login for the root user. diff --git a/openstack/database/v1/user.py b/openstack/database/v1/user.py index 2abbfec4f..a2f4116cb 100644 --- a/openstack/database/v1/user.py +++ b/openstack/database/v1/user.py @@ -11,11 +11,11 @@ # under the License. from openstack.database import database_service -from openstack import resource +from openstack import resource2 as resource +from openstack import utils class User(resource.Resource): - id_attribute = 'name' resource_key = 'user' resources_key = 'users' base_path = '/instances/%(instance_id)s/users' @@ -26,21 +26,25 @@ class User(resource.Resource): allow_delete = True allow_list = True - # path args - instance_id = resource.prop('instance_id') + instance_id = resource.URI('instance_id') # Properties #: Databases the user has access to - databases = resource.prop('databases') + databases = resource.Body('databases') #: The name of the user - name = resource.prop('name') + name = resource.Body('name', alternate_id=True) #: The password of the user - password = resource.prop('password') - - @classmethod - def create_by_id(cls, session, attrs, r_id=None, path_args=None): - url = cls._get_url(path_args) - # Create expects an array of users - body = {'users': [attrs]} - resp = session.post(url, endpoint_filter=cls.service, json=body) - return resp.json() + password = resource.Body('password') + + def _prepare_request(self, requires_id=True, prepend_key=True): + """Prepare a request for the database service's create call + + User.create calls require the resources_key. + The base_prepare_request would insert the resource_key (singular) + """ + body = {self.resources_key: self._body.dirty} + + uri = self.base_path % self._uri.attributes + uri = utils.urljoin(uri, self.id) + + return resource._Request(uri, body, None) diff --git a/openstack/tests/unit/database/v1/test_database.py b/openstack/tests/unit/database/v1/test_database.py index 345780d07..6f8cda2ca 100644 --- a/openstack/tests/unit/database/v1/test_database.py +++ b/openstack/tests/unit/database/v1/test_database.py @@ -36,14 +36,15 @@ def test_basic(self): self.assertEqual('database', sot.service.service_type) self.assertTrue(sot.allow_list) self.assertTrue(sot.allow_create) - self.assertFalse(sot.allow_retrieve) + self.assertFalse(sot.allow_get) self.assertFalse(sot.allow_update) self.assertTrue(sot.allow_delete) def test_make_it(self): - sot = database.Database(EXAMPLE) + sot = database.Database(**EXAMPLE) self.assertEqual(IDENTIFIER, sot.id) self.assertEqual(EXAMPLE['character_set'], sot.character_set) self.assertEqual(EXAMPLE['collate'], sot.collate) self.assertEqual(EXAMPLE['instance_id'], sot.instance_id) self.assertEqual(IDENTIFIER, sot.name) + self.assertEqual(IDENTIFIER, sot.id) diff --git a/openstack/tests/unit/database/v1/test_flavor.py b/openstack/tests/unit/database/v1/test_flavor.py index 180ffb0dd..a851e45d2 100644 --- a/openstack/tests/unit/database/v1/test_flavor.py +++ b/openstack/tests/unit/database/v1/test_flavor.py @@ -33,12 +33,12 @@ def test_basic(self): self.assertEqual('database', sot.service.service_type) self.assertTrue(sot.allow_list) self.assertFalse(sot.allow_create) - self.assertTrue(sot.allow_retrieve) + self.assertTrue(sot.allow_get) self.assertFalse(sot.allow_update) self.assertFalse(sot.allow_delete) def test_make_it(self): - sot = flavor.Flavor(EXAMPLE) + sot = flavor.Flavor(**EXAMPLE) self.assertEqual(IDENTIFIER, sot.id) self.assertEqual(EXAMPLE['links'], sot.links) self.assertEqual(EXAMPLE['name'], sot.name) diff --git a/openstack/tests/unit/database/v1/test_instance.py b/openstack/tests/unit/database/v1/test_instance.py index 13687d17e..a13bed7b3 100644 --- a/openstack/tests/unit/database/v1/test_instance.py +++ b/openstack/tests/unit/database/v1/test_instance.py @@ -23,6 +23,11 @@ 'name': '4', 'status': '5', 'volume': '6', + 'datastore': {'7': 'seven'}, + 'region': '8', + 'hostname': '9', + 'created': '10', + 'updated': '11', } @@ -35,22 +40,27 @@ def test_basic(self): self.assertEqual('/instances', sot.base_path) self.assertEqual('database', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_retrieve) + self.assertTrue(sot.allow_get) self.assertTrue(sot.allow_update) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) def test_make_it(self): - sot = instance.Instance(EXAMPLE) + sot = instance.Instance(**EXAMPLE) self.assertEqual(EXAMPLE['flavor'], sot.flavor) self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['links'], sot.links) self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['status'], sot.status) self.assertEqual(EXAMPLE['volume'], sot.volume) + self.assertEqual(EXAMPLE['datastore'], sot.datastore) + self.assertEqual(EXAMPLE['region'], sot.region) + self.assertEqual(EXAMPLE['hostname'], sot.hostname) + self.assertEqual(EXAMPLE['created'], sot.created_at) + self.assertEqual(EXAMPLE['updated'], sot.updated_at) def test_enable_root_user(self): - sot = instance.Instance(EXAMPLE) + sot = instance.Instance(**EXAMPLE) response = mock.Mock() response.body = {'user': {'name': 'root', 'password': 'foo'}} response.json = mock.Mock(return_value=response.body) @@ -63,7 +73,7 @@ def test_enable_root_user(self): sess.post.assert_called_with(url, endpoint_filter=sot.service) def test_is_root_enabled(self): - sot = instance.Instance(EXAMPLE) + sot = instance.Instance(**EXAMPLE) response = mock.Mock() response.body = {'rootEnabled': True} response.json = mock.Mock(return_value=response.body) @@ -76,7 +86,7 @@ def test_is_root_enabled(self): sess.get.assert_called_with(url, endpoint_filter=sot.service) def test_action_restart(self): - sot = instance.Instance(EXAMPLE) + sot = instance.Instance(**EXAMPLE) response = mock.Mock() response.json = mock.Mock(return_value='') sess = mock.Mock() @@ -90,7 +100,7 @@ def test_action_restart(self): json=body) def test_action_resize(self): - sot = instance.Instance(EXAMPLE) + sot = instance.Instance(**EXAMPLE) response = mock.Mock() response.json = mock.Mock(return_value='') sess = mock.Mock() @@ -105,7 +115,7 @@ def test_action_resize(self): json=body) def test_action_resize_volume(self): - sot = instance.Instance(EXAMPLE) + sot = instance.Instance(**EXAMPLE) response = mock.Mock() response.json = mock.Mock(return_value='') sess = mock.Mock() diff --git a/openstack/tests/unit/database/v1/test_proxy.py b/openstack/tests/unit/database/v1/test_proxy.py index 41d2ce13a..ff54e5b20 100644 --- a/openstack/tests/unit/database/v1/test_proxy.py +++ b/openstack/tests/unit/database/v1/test_proxy.py @@ -15,31 +15,43 @@ from openstack.database.v1 import flavor from openstack.database.v1 import instance from openstack.database.v1 import user -from openstack.tests.unit import test_proxy_base +from openstack.tests.unit import test_proxy_base2 -class TestDatabaseProxy(test_proxy_base.TestProxyBase): +class TestDatabaseProxy(test_proxy_base2.TestProxyBase): def setUp(self): super(TestDatabaseProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) def test_database_create_attrs(self): - self.verify_create(self.proxy.create_database, database.Database) + self.verify_create(self.proxy.create_database, database.Database, + method_kwargs={"instance": "id"}, + expected_kwargs={"instance_id": "id"}) def test_database_delete(self): self.verify_delete(self.proxy.delete_database, - database.Database, False) + database.Database, False, + input_path_args={"instance": "test_id"}, + expected_path_args={"instance_id": "test_id"}) def test_database_delete_ignore(self): self.verify_delete(self.proxy.delete_database, - database.Database, True) + database.Database, True, + input_path_args={"instance": "test_id"}, + expected_path_args={"instance_id": "test_id"}) def test_database_find(self): - self.verify_find(self.proxy.find_database, database.Database) + self._verify2('openstack.proxy2.BaseProxy._find', + self.proxy.find_database, + method_args=["db", "instance"], + expected_args=[database.Database, "db"], + expected_kwargs={"instance_id": "instance", + "ignore_missing": True}) def test_databases(self): self.verify_list(self.proxy.databases, database.Database, - paginated=False) + paginated=False, method_args=["id"], + expected_kwargs={"instance_id": "id"}) def test_database_get(self): self.verify_get(self.proxy.get_database, database.Database) @@ -79,19 +91,32 @@ def test_instance_update(self): self.verify_update(self.proxy.update_instance, instance.Instance) def test_user_create_attrs(self): - self.verify_create(self.proxy.create_user, user.User) + self.verify_create(self.proxy.create_user, user.User, + method_kwargs={"instance": "id"}, + expected_kwargs={"instance_id": "id"}) def test_user_delete(self): - self.verify_delete(self.proxy.delete_user, user.User, False) + self.verify_delete(self.proxy.delete_user, user.User, False, + input_path_args={"instance": "id"}, + expected_path_args={"instance_id": "id"}) def test_user_delete_ignore(self): - self.verify_delete(self.proxy.delete_user, user.User, True) + self.verify_delete(self.proxy.delete_user, user.User, True, + input_path_args={"instance": "id"}, + expected_path_args={"instance_id": "id"}) def test_user_find(self): - self.verify_find(self.proxy.find_user, user.User) + self._verify2('openstack.proxy2.BaseProxy._find', + self.proxy.find_user, + method_args=["user", "instance"], + expected_args=[user.User, "user"], + expected_kwargs={"instance_id": "instance", + "ignore_missing": True}) def test_users(self): - self.verify_list(self.proxy.users, user.User, paginated=False) + self.verify_list(self.proxy.users, user.User, paginated=False, + method_args=["test_instance"], + expected_kwargs={"instance_id": "test_instance"}) def test_user_get(self): self.verify_get(self.proxy.get_user, user.User) diff --git a/openstack/tests/unit/database/v1/test_user.py b/openstack/tests/unit/database/v1/test_user.py index 85e05bf23..884a906e9 100644 --- a/openstack/tests/unit/database/v1/test_user.py +++ b/openstack/tests/unit/database/v1/test_user.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import mock import testtools from openstack.database.v1 import user @@ -22,10 +21,6 @@ 'name': '2', 'password': '3', } -EXISTING = { - 'databases': '1', - 'name': '2', -} class TestUser(testtools.TestCase): @@ -37,33 +32,20 @@ def test_basic(self): self.assertEqual('/instances/%(instance_id)s/users', sot.base_path) self.assertEqual('database', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertFalse(sot.allow_retrieve) + self.assertFalse(sot.allow_get) self.assertFalse(sot.allow_update) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) def test_make(self): - sot = user.User(CREATING) + sot = user.User(**CREATING) self.assertEqual(CREATING['name'], sot.id) self.assertEqual(CREATING['databases'], sot.databases) self.assertEqual(CREATING['name'], sot.name) + self.assertEqual(CREATING['name'], sot.id) self.assertEqual(CREATING['password'], sot.password) - def test_existing(self): - sot = user.User(EXISTING) - self.assertEqual(EXISTING['name'], sot.id) - self.assertEqual(EXISTING['databases'], sot.databases) - self.assertEqual(EXISTING['name'], sot.name) - self.assertIsNone(sot.password) - def test_create(self): - sess = mock.Mock() - resp = mock.Mock() - sess.post = mock.Mock(return_value=resp) - path_args = {'instance_id': INSTANCE_ID} - url = '/instances/%(instance_id)s/users' % path_args - payload = {'users': [CREATING]} - - user.User.create_by_id(sess, CREATING, path_args=path_args) - sess.post.assert_called_with(url, endpoint_filter=user.User.service, - json=payload) + sot = user.User(instance_id=INSTANCE_ID, **CREATING) + result = sot._prepare_request() + self.assertEqual(result.body, {sot.resources_key: CREATING}) From 21dcba4d530e9fd6705a08507f66f44e5985b2b1 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Wed, 26 Apr 2017 15:34:19 -0400 Subject: [PATCH 1465/3836] Port identity v2 to resource2 This change moves over the identity v2 service to resource2/proxy2. It's mostly a renaming and updating to fit the new API. Change-Id: I95ab21c13bcdc24bf5daadf58f79d0f53c8bd75e --- doc/source/users/proxies/identity_v2.rst | 10 +++++++- openstack/identity/v2/_proxy.py | 24 ++++++++++++++++++- openstack/identity/v2/extension.py | 17 ++++++------- openstack/identity/v2/role.py | 10 ++++---- openstack/identity/v2/tenant.py | 10 ++++---- openstack/identity/v2/user.py | 10 ++++---- .../tests/unit/identity/v2/test_extension.py | 6 ++--- .../tests/unit/identity/v2/test_proxy.py | 2 +- openstack/tests/unit/identity/v2/test_role.py | 5 ++-- .../tests/unit/identity/v2/test_tenant.py | 4 ++-- openstack/tests/unit/identity/v2/test_user.py | 4 ++-- 11 files changed, 66 insertions(+), 36 deletions(-) diff --git a/doc/source/users/proxies/identity_v2.rst b/doc/source/users/proxies/identity_v2.rst index 66f4b528c..3f2c65aeb 100644 --- a/doc/source/users/proxies/identity_v2.rst +++ b/doc/source/users/proxies/identity_v2.rst @@ -12,7 +12,15 @@ The identity high-level interface is available through the ``identity`` member of a :class:`~openstack.connection.Connection` object. The ``identity`` member will only be added if the service is detected. -User Operations +Extension Operations +^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.identity.v2._proxy.Proxy + + .. automethod:: openstack.identity.v2._proxy.Proxy.get_extension + .. automethod:: openstack.identity.v2._proxy.Proxy.extensions + + User Operations ^^^^^^^^^^^^^^^ .. autoclass:: openstack.identity.v2._proxy.Proxy diff --git a/openstack/identity/v2/_proxy.py b/openstack/identity/v2/_proxy.py index a25d83241..e371da5e8 100644 --- a/openstack/identity/v2/_proxy.py +++ b/openstack/identity/v2/_proxy.py @@ -10,14 +10,36 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.identity.v2 import extension as _extension from openstack.identity.v2 import role as _role from openstack.identity.v2 import tenant as _tenant from openstack.identity.v2 import user as _user -from openstack import proxy +from openstack import proxy2 as proxy class Proxy(proxy.BaseProxy): + def extensions(self): + """Retrieve a generator of extensions + + :returns: A generator of extension instances. + :rtype: :class:`~openstack.identity.v2.extension.Extension` + """ + return self._list(_extension.Extension, paginated=False) + + def get_extension(self, extension): + """Get a single extension + + :param extension: The value can be the ID of an extension or a + :class:`~openstack.identity.v2.extension.Extension` + instance. + + :returns: One :class:`~openstack.identity.v2.extension.Extension` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no extension can be found. + """ + return self._get(_extension.Extension, extension) + def create_role(self, **attrs): """Create a new role from attributes diff --git a/openstack/identity/v2/extension.py b/openstack/identity/v2/extension.py index 19c8ab448..b4160aad4 100644 --- a/openstack/identity/v2/extension.py +++ b/openstack/identity/v2/extension.py @@ -11,7 +11,7 @@ # under the License. from openstack.identity import identity_service -from openstack import resource +from openstack import resource2 as resource class Extension(resource.Resource): @@ -22,29 +22,30 @@ class Extension(resource.Resource): # capabilities allow_list = True + allow_get = True # Properties #: A unique identifier, which will be used for accessing the extension #: through a dedicated url ``/extensions/*alias*``. The extension #: alias uniquely identifies an extension and is prefixed by a vendor #: identifier. *Type: string* - alias = resource.prop('alias') + alias = resource.Body('alias', alternate_id=True) #: A description of the extension. *Type: string* - description = resource.prop('description') + description = resource.Body('description') #: Links to the documentation in various format. *Type: string* - links = resource.prop('links') + links = resource.Body('links') #: The name of the extension. *Type: string* - name = resource.prop('name') + name = resource.Body('name') #: The second unique identifier of the extension after the alias. #: It is usually a URL which will be used. Example: #: "http://docs.openstack.org/identity/api/ext/s3tokens/v1.0" #: *Type: string* - namespace = resource.prop('namespace') + namespace = resource.Body('namespace') #: The last time the extension has been modified (update date). - updated_at = resource.prop('updated') + updated_at = resource.Body('updated') @classmethod - def list(cls, session, **params): + def list(cls, session, paginated=False, **params): resp = session.get(cls.base_path, endpoint_filter=cls.service, params=params) resp = resp.json() diff --git a/openstack/identity/v2/role.py b/openstack/identity/v2/role.py index 536b19280..b3b0a0363 100644 --- a/openstack/identity/v2/role.py +++ b/openstack/identity/v2/role.py @@ -12,7 +12,7 @@ from openstack import format from openstack.identity import identity_service -from openstack import resource +from openstack import resource2 as resource class Role(resource.Resource): @@ -23,16 +23,16 @@ class Role(resource.Resource): # capabilities allow_create = True - allow_retrieve = True + allow_get = True allow_update = True allow_delete = True allow_list = True # Properties #: The description of the role. *Type: string* - description = resource.prop('description') + description = resource.Body('description') #: Setting this attribute to ``False`` prevents this role from being #: available in the role list. *Type: bool* - is_enabled = resource.prop('enabled', type=format.BoolStr) + is_enabled = resource.Body('enabled', type=format.BoolStr) #: Unique role name. *Type: string* - name = resource.prop('name') + name = resource.Body('name') diff --git a/openstack/identity/v2/tenant.py b/openstack/identity/v2/tenant.py index 93630c992..f6933d5a4 100644 --- a/openstack/identity/v2/tenant.py +++ b/openstack/identity/v2/tenant.py @@ -11,7 +11,7 @@ # under the License. from openstack.identity import identity_service -from openstack import resource +from openstack import resource2 as resource class Tenant(resource.Resource): @@ -22,18 +22,18 @@ class Tenant(resource.Resource): # capabilities allow_create = True - allow_retrieve = True + allow_get = True allow_update = True allow_delete = True allow_list = True # Properties #: The description of the tenant. *Type: string* - description = resource.prop('description') + description = resource.Body('description') #: Setting this attribute to ``False`` prevents users from authorizing #: against this tenant. Additionally, all pre-existing tokens authorized #: for the tenant are immediately invalidated. Re-enabling a tenant #: does not re-enable pre-existing tokens. *Type: bool* - is_enabled = resource.prop('enabled', type=bool) + is_enabled = resource.Body('enabled', type=bool) #: Unique tenant name. *Type: string* - name = resource.prop('name') + name = resource.Body('name') diff --git a/openstack/identity/v2/user.py b/openstack/identity/v2/user.py index 930494c90..0e5a5846b 100644 --- a/openstack/identity/v2/user.py +++ b/openstack/identity/v2/user.py @@ -11,7 +11,7 @@ # under the License. from openstack.identity import identity_service -from openstack import resource +from openstack import resource2 as resource class User(resource.Resource): @@ -22,18 +22,18 @@ class User(resource.Resource): # capabilities allow_create = True - allow_retrieve = True + allow_get = True allow_update = True allow_delete = True allow_list = True # Properties #: The email of this user. *Type: string* - email = resource.prop('email') + email = resource.Body('email') #: Setting this value to ``False`` prevents the user from authenticating or #: receiving authorization. Additionally, all pre-existing tokens held by #: the user are immediately invalidated. Re-enabling a user does not #: re-enable pre-existing tokens. *Type: bool* - is_enabled = resource.prop('enabled', type=bool) + is_enabled = resource.Body('enabled', type=bool) #: The name of this user. *Type: string* - name = resource.prop('name') + name = resource.Body('name') diff --git a/openstack/tests/unit/identity/v2/test_extension.py b/openstack/tests/unit/identity/v2/test_extension.py index 3c9471d5c..13d0d29b1 100644 --- a/openstack/tests/unit/identity/v2/test_extension.py +++ b/openstack/tests/unit/identity/v2/test_extension.py @@ -35,13 +35,13 @@ def test_basic(self): self.assertEqual('/extensions', sot.base_path) self.assertEqual('identity', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_retrieve) + self.assertTrue(sot.allow_get) self.assertFalse(sot.allow_update) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) def test_make_it(self): - sot = extension.Extension(EXAMPLE) + sot = extension.Extension(**EXAMPLE) self.assertEqual(EXAMPLE['alias'], sot.alias) self.assertEqual(EXAMPLE['description'], sot.description) self.assertEqual(EXAMPLE['links'], sot.links) @@ -62,7 +62,7 @@ def test_list(self): resp.json = mock.Mock(return_value=resp.body) session = mock.Mock() session.get = mock.Mock(return_value=resp) - sot = extension.Extension(EXAMPLE) + sot = extension.Extension(**EXAMPLE) result = sot.list(session) self.assertEqual(next(result).name, 'a') self.assertEqual(next(result).name, 'b') diff --git a/openstack/tests/unit/identity/v2/test_proxy.py b/openstack/tests/unit/identity/v2/test_proxy.py index 5080cc9e0..cbca1fd7b 100644 --- a/openstack/tests/unit/identity/v2/test_proxy.py +++ b/openstack/tests/unit/identity/v2/test_proxy.py @@ -14,7 +14,7 @@ from openstack.identity.v2 import role from openstack.identity.v2 import tenant from openstack.identity.v2 import user -from openstack.tests.unit import test_proxy_base +from openstack.tests.unit import test_proxy_base2 as test_proxy_base class TestIdentityProxy(test_proxy_base.TestProxyBase): diff --git a/openstack/tests/unit/identity/v2/test_role.py b/openstack/tests/unit/identity/v2/test_role.py index 55967c25e..e1f67ba36 100644 --- a/openstack/tests/unit/identity/v2/test_role.py +++ b/openstack/tests/unit/identity/v2/test_role.py @@ -32,14 +32,13 @@ def test_basic(self): self.assertEqual('/OS-KSADM/roles', sot.base_path) self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_retrieve) + self.assertTrue(sot.allow_get) self.assertTrue(sot.allow_update) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) def test_make_it(self): - sot = role.Role(EXAMPLE) - self.assertTrue(sot.enabled) + sot = role.Role(**EXAMPLE) self.assertEqual(EXAMPLE['description'], sot.description) self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['name'], sot.name) diff --git a/openstack/tests/unit/identity/v2/test_tenant.py b/openstack/tests/unit/identity/v2/test_tenant.py index 3430284b0..b123ce40c 100644 --- a/openstack/tests/unit/identity/v2/test_tenant.py +++ b/openstack/tests/unit/identity/v2/test_tenant.py @@ -32,13 +32,13 @@ def test_basic(self): self.assertEqual('/tenants', sot.base_path) self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_retrieve) + self.assertTrue(sot.allow_get) self.assertTrue(sot.allow_update) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) def test_make_it(self): - sot = tenant.Tenant(EXAMPLE) + sot = tenant.Tenant(**EXAMPLE) self.assertEqual(EXAMPLE['description'], sot.description) self.assertTrue(sot.is_enabled) self.assertEqual(EXAMPLE['id'], sot.id) diff --git a/openstack/tests/unit/identity/v2/test_user.py b/openstack/tests/unit/identity/v2/test_user.py index d6ec1ee39..161b8bade 100644 --- a/openstack/tests/unit/identity/v2/test_user.py +++ b/openstack/tests/unit/identity/v2/test_user.py @@ -32,13 +32,13 @@ def test_basic(self): self.assertEqual('/users', sot.base_path) self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_retrieve) + self.assertTrue(sot.allow_get) self.assertTrue(sot.allow_update) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) def test_make_it(self): - sot = user.User(EXAMPLE) + sot = user.User(**EXAMPLE) self.assertEqual(EXAMPLE['email'], sot.email) self.assertTrue(sot.is_enabled) self.assertEqual(EXAMPLE['id'], sot.id) From 6c60da098743f4710e0d99cd305fdc922a5bbd11 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Wed, 26 Apr 2017 15:52:05 -0400 Subject: [PATCH 1466/3836] Port image v1 to resource2 This change moves image v1 over to resource2. Change-Id: I0f072ff4ae46cc755052c68dc2f0509c06c6e06a --- openstack/image/v1/_proxy.py | 2 +- openstack/image/v1/image.py | 36 ++++++++++----------- openstack/tests/unit/image/v1/test_image.py | 4 +-- openstack/tests/unit/image/v1/test_proxy.py | 2 +- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/openstack/image/v1/_proxy.py b/openstack/image/v1/_proxy.py index aa4ba6fbe..e8d2ff534 100644 --- a/openstack/image/v1/_proxy.py +++ b/openstack/image/v1/_proxy.py @@ -11,7 +11,7 @@ # under the License. from openstack.image.v1 import image as _image -from openstack import proxy +from openstack import proxy2 as proxy class Proxy(proxy.BaseProxy): diff --git a/openstack/image/v1/image.py b/openstack/image/v1/image.py index bbe0f55df..882f61334 100644 --- a/openstack/image/v1/image.py +++ b/openstack/image/v1/image.py @@ -11,7 +11,7 @@ # under the License. from openstack.image import image_service -from openstack import resource +from openstack import resource2 as resource class Image(resource.Resource): @@ -22,52 +22,52 @@ class Image(resource.Resource): # capabilities allow_create = True - allow_retrieve = True + allow_get = True allow_update = True allow_delete = True allow_list = True #: Hash of the image data used. The Image service uses this value #: for verification. - checksum = resource.prop('checksum') + checksum = resource.Body('checksum') #: The container format refers to whether the VM image is in a file #: format that also contains metadata about the actual VM. #: Container formats include OVF and Amazon AMI. In addition, #: a VM image might not have a container format - instead, #: the image is just a blob of unstructured data. - container_format = resource.prop('container_format') + container_format = resource.Body('container_format') #: A URL to copy an image from - copy_from = resource.prop('copy_from') + copy_from = resource.Body('copy_from') #: The timestamp when this image was created. - created_at = resource.prop('created_at') + created_at = resource.Body('created_at') #: Valid values are: aki, ari, ami, raw, iso, vhd, vdi, qcow2, or vmdk. #: The disk format of a VM image is the format of the underlying #: disk image. Virtual appliance vendors have different formats for #: laying out the information contained in a VM disk image. - disk_format = resource.prop('disk_format') + disk_format = resource.Body('disk_format') #: Defines whether the image can be deleted. #: *Type: bool* - is_protected = resource.prop('protected', type=bool) + is_protected = resource.Body('protected', type=bool) #: ``True`` if this is a public image. #: *Type: bool* - is_public = resource.prop('is_public', type=bool) + is_public = resource.Body('is_public', type=bool) #: A location for the image identified by a URI - location = resource.prop('location') + location = resource.Body('location') #: The minimum disk size in GB that is required to boot the image. - min_disk = resource.prop('min_disk') + min_disk = resource.Body('min_disk') #: The minimum amount of RAM in MB that is required to boot the image. - min_ram = resource.prop('min_ram') + min_ram = resource.Body('min_ram') #: Name for the image. Note that the name of an image is not unique #: to a Glance node. The API cannot expect users to know the names #: of images owned by others. - name = resource.prop('name') + name = resource.Body('name') #: The ID of the owner, or project, of the image. - owner_id = resource.prop('owner') + owner_id = resource.Body('owner') #: Properties, if any, that are associated with the image. - properties = resource.prop('properties') + properties = resource.Body('properties') #: The size of the image data, in bytes. - size = resource.prop('size') + size = resource.Body('size') #: The image status. - status = resource.prop('status') + status = resource.Body('status') #: The timestamp when this image was last updated. - updated_at = resource.prop('updated_at') + updated_at = resource.Body('updated_at') diff --git a/openstack/tests/unit/image/v1/test_image.py b/openstack/tests/unit/image/v1/test_image.py index a9fb33257..4f0a4554d 100644 --- a/openstack/tests/unit/image/v1/test_image.py +++ b/openstack/tests/unit/image/v1/test_image.py @@ -45,13 +45,13 @@ def test_basic(self): self.assertEqual('/images', sot.base_path) self.assertEqual('image', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_retrieve) + self.assertTrue(sot.allow_get) self.assertTrue(sot.allow_update) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) def test_make_it(self): - sot = image.Image(EXAMPLE) + sot = image.Image(**EXAMPLE) self.assertEqual(EXAMPLE['checksum'], sot.checksum) self.assertEqual(EXAMPLE['container_format'], sot.container_format) self.assertEqual(EXAMPLE['copy_from'], sot.copy_from) diff --git a/openstack/tests/unit/image/v1/test_proxy.py b/openstack/tests/unit/image/v1/test_proxy.py index e8be29560..e0dbd679f 100644 --- a/openstack/tests/unit/image/v1/test_proxy.py +++ b/openstack/tests/unit/image/v1/test_proxy.py @@ -12,7 +12,7 @@ from openstack.image.v1 import _proxy from openstack.image.v1 import image -from openstack.tests.unit import test_proxy_base +from openstack.tests.unit import test_proxy_base2 as test_proxy_base class TestImageProxy(test_proxy_base.TestProxyBase): From 2a0d5fcb97572ba950e9a3b1b4c930f023dbf94f Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Thu, 27 Apr 2017 09:20:50 -0400 Subject: [PATCH 1467/3836] Deprecate Message v1 Rather than porting message v1 to resource2, we are going to remove it in the next release. It's marked as deprecated for 0.9.16 (the next release) and slated for removal in 0.9.17. The message v1 REST API has been deprecated since 2014. Change-Id: I3b7af3b7aa2b0aef847376e04b0cde5c97ac5dbb --- openstack/message/v1/_proxy.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openstack/message/v1/_proxy.py b/openstack/message/v1/_proxy.py index ab31d07f8..bdd25ef04 100644 --- a/openstack/message/v1/_proxy.py +++ b/openstack/message/v1/_proxy.py @@ -14,10 +14,13 @@ from openstack.message.v1 import message from openstack.message.v1 import queue from openstack import proxy +from openstack import utils class Proxy(proxy.BaseProxy): + @utils.deprecated(deprecated_in="0.9.16", removed_in="0.9.17", + details="Message v1 is deprecated since 2014. Use v2.") def create_queue(self, **attrs): """Create a new queue from attributes @@ -30,6 +33,8 @@ def create_queue(self, **attrs): """ return self._create(queue.Queue, **attrs) + @utils.deprecated(deprecated_in="0.9.16", removed_in="0.9.17", + details="Message v1 is deprecated since 2014. Use v2.") def delete_queue(self, value, ignore_missing=True): """Delete a queue @@ -45,6 +50,8 @@ def delete_queue(self, value, ignore_missing=True): """ return self._delete(queue.Queue, value, ignore_missing=ignore_missing) + @utils.deprecated(deprecated_in="0.9.16", removed_in="0.9.17", + details="Message v1 is deprecated since 2014. Use v2.") def create_messages(self, values): """Create new messages @@ -59,6 +66,8 @@ def create_messages(self, values): """ return message.Message.create_messages(self._session, values) + @utils.deprecated(deprecated_in="0.9.16", removed_in="0.9.17", + details="Message v1 is deprecated since 2014. Use v2.") def claim_messages(self, value): """Claims a set of messages. @@ -71,6 +80,8 @@ def claim_messages(self, value): """ return claim.Claim.claim_messages(self._session, value) + @utils.deprecated(deprecated_in="0.9.16", removed_in="0.9.17", + details="Message v1 is deprecated since 2014. Use v2.") def delete_message(self, value): """Delete a message From fb4a329501d6b45de098f6464085586f94490c47 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Thu, 27 Apr 2017 12:49:53 -0400 Subject: [PATCH 1468/3836] Port metric v1 to resource2 This change ports metric v1 to resource2. Note that most of this stuff isn't exposed in the proxy, but that change should be made elsewhere. I also removed the resource.Generic.create method as that doesn't seem necessary based on what's documented. Change-Id: I662ad4811ff84bd7b7726d02d676e9f5f05e3808 --- openstack/metric/v1/_proxy.py | 2 +- openstack/metric/v1/archive_policy.py | 14 ++++------ openstack/metric/v1/capabilities.py | 6 ++-- openstack/metric/v1/metric.py | 16 +++++------ openstack/metric/v1/resource.py | 28 ++++++++----------- .../unit/metric/v1/test_archive_policy.py | 5 ++-- .../tests/unit/metric/v1/test_capabilities.py | 4 +-- openstack/tests/unit/metric/v1/test_metric.py | 6 ++-- openstack/tests/unit/metric/v1/test_proxy.py | 2 +- .../tests/unit/metric/v1/test_resource.py | 4 +-- 10 files changed, 41 insertions(+), 46 deletions(-) diff --git a/openstack/metric/v1/_proxy.py b/openstack/metric/v1/_proxy.py index e25e9303e..31958e943 100644 --- a/openstack/metric/v1/_proxy.py +++ b/openstack/metric/v1/_proxy.py @@ -11,7 +11,7 @@ # under the License. from openstack.metric.v1 import capabilities -from openstack import proxy +from openstack import proxy2 as proxy class Proxy(proxy.BaseProxy): diff --git a/openstack/metric/v1/archive_policy.py b/openstack/metric/v1/archive_policy.py index df2524603..1c0d844ca 100644 --- a/openstack/metric/v1/archive_policy.py +++ b/openstack/metric/v1/archive_policy.py @@ -11,7 +11,7 @@ # under the License. from openstack.metric import metric_service -from openstack import resource +from openstack import resource2 as resource class ArchivePolicy(resource.Resource): @@ -20,18 +20,16 @@ class ArchivePolicy(resource.Resource): # Supported Operations allow_create = True - allow_retrieve = True + allow_get = True allow_delete = True allow_list = True - id_attribute = "name" - # Properties #: The name of this policy - name = resource.prop('name') + name = resource.Body('name', alternate_id=True) #: The definition of this policy - definition = resource.prop('definition', type=list) + definition = resource.Body('definition', type=list) #: The window of time older than the period that archives can be requested - back_window = resource.prop('back_window') + back_window = resource.Body('back_window') #: A list of the aggregation methods supported - aggregation_methods = resource.prop("aggregation_methods", type=list) + aggregation_methods = resource.Body("aggregation_methods", type=list) diff --git a/openstack/metric/v1/capabilities.py b/openstack/metric/v1/capabilities.py index e085492d0..906574758 100644 --- a/openstack/metric/v1/capabilities.py +++ b/openstack/metric/v1/capabilities.py @@ -11,7 +11,7 @@ # under the License. from openstack.metric import metric_service -from openstack import resource +from openstack import resource2 as resource class Capabilities(resource.Resource): @@ -19,7 +19,7 @@ class Capabilities(resource.Resource): service = metric_service.MetricService() # Supported Operations - allow_retrieve = True + allow_get = True #: The supported methods of aggregation. - aggregation_methods = resource.prop('aggregation_methods', type=list) + aggregation_methods = resource.Body('aggregation_methods', type=list) diff --git a/openstack/metric/v1/metric.py b/openstack/metric/v1/metric.py index 3fb095ee9..3e71b2c0d 100644 --- a/openstack/metric/v1/metric.py +++ b/openstack/metric/v1/metric.py @@ -11,7 +11,7 @@ # under the License. from openstack.metric import metric_service -from openstack import resource +from openstack import resource2 as resource class Metric(resource.Resource): @@ -20,20 +20,20 @@ class Metric(resource.Resource): # Supported Operations allow_create = True - allow_retrieve = True + allow_get = True allow_delete = True allow_list = True # Properties #: The name of the archive policy - archive_policy_name = resource.prop('archive_policy_name') + archive_policy_name = resource.Body('archive_policy_name') #: The archive policy - archive_policy = resource.prop('archive_policy') + archive_policy = resource.Body('archive_policy') #: The ID of the user who created this metric - created_by_user_id = resource.prop('created_by_user_id') + created_by_user_id = resource.Body('created_by_user_id') #: The ID of the project this metric was created under - created_by_project_id = resource.prop('created_by_project_id') + created_by_project_id = resource.Body('created_by_project_id') #: The identifier of this metric - resource_id = resource.prop('resource_id') + resource_id = resource.Body('resource_id') #: The name of this metric - name = resource.prop('name') + name = resource.Body('name') diff --git a/openstack/metric/v1/resource.py b/openstack/metric/v1/resource.py index f6f8c8420..f66d5661b 100644 --- a/openstack/metric/v1/resource.py +++ b/openstack/metric/v1/resource.py @@ -11,7 +11,7 @@ # under the License. from openstack.metric import metric_service -from openstack import resource +from openstack import resource2 as resource class Generic(resource.Resource): @@ -20,31 +20,27 @@ class Generic(resource.Resource): # Supported Operations allow_create = True - allow_retrieve = True + allow_get = True allow_delete = True allow_list = True allow_update = True # Properties #: The identifier of this resource - id = resource.prop('id', alias="resource_id") + id = resource.Body('id') #: The ID of the user who created this resource - created_by_user_id = resource.prop('created_by_user_id') + created_by_user_id = resource.Body('created_by_user_id') #: The ID of the project this resource was created under - created_by_project_id = resource.prop('created_by_project_id') + created_by_project_id = resource.Body('created_by_project_id') #: The ID of the user - user_id = resource.prop('user_id') + user_id = resource.Body('user_id') #: The ID of the project - project_id = resource.prop('project_id') + project_id = resource.Body('project_id') #: Timestamp when this resource was started - started_at = resource.prop('started_at') + started_at = resource.Body('started_at') #: Timestamp when this resource was ended - ended_at = resource.prop('ended_at') + ended_at = resource.Body('ended_at') #: A dictionary of metrics collected on this resource - metrics = resource.prop('metrics', type=dict) - - def create(self, session): - resp = self.create_by_id(session, self._attrs) - self._attrs[self.id_attribute] = resp[self.id_attribute] - self._reset_dirty() - return self + metrics = resource.Body('metrics', type=dict) + #: The type of resource + type = resource.Body('type') diff --git a/openstack/tests/unit/metric/v1/test_archive_policy.py b/openstack/tests/unit/metric/v1/test_archive_policy.py index ef0affca7..e8c08c968 100644 --- a/openstack/tests/unit/metric/v1/test_archive_policy.py +++ b/openstack/tests/unit/metric/v1/test_archive_policy.py @@ -47,14 +47,15 @@ def test_basic(self): self.assertEqual('/archive_policy', m.base_path) self.assertEqual('metric', m.service.service_type) self.assertTrue(m.allow_create) - self.assertTrue(m.allow_retrieve) + self.assertTrue(m.allow_get) self.assertFalse(m.allow_update) self.assertTrue(m.allow_delete) self.assertTrue(m.allow_list) def test_make_it(self): - m = archive_policy.ArchivePolicy(EXAMPLE) + m = archive_policy.ArchivePolicy(**EXAMPLE) self.assertEqual(EXAMPLE['name'], m.name) + self.assertEqual(EXAMPLE['name'], m.id) self.assertEqual(EXAMPLE['definition'], m.definition) self.assertEqual(EXAMPLE['back_window'], m.back_window) self.assertEqual(EXAMPLE['aggregation_methods'], m.aggregation_methods) diff --git a/openstack/tests/unit/metric/v1/test_capabilities.py b/openstack/tests/unit/metric/v1/test_capabilities.py index 0f2ee38ce..72ee1cbe6 100644 --- a/openstack/tests/unit/metric/v1/test_capabilities.py +++ b/openstack/tests/unit/metric/v1/test_capabilities.py @@ -25,12 +25,12 @@ def test_basic(self): self.assertEqual('/capabilities', sot.base_path) self.assertEqual('metric', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertTrue(sot.allow_retrieve) + self.assertTrue(sot.allow_get) self.assertFalse(sot.allow_update) self.assertFalse(sot.allow_delete) self.assertFalse(sot.allow_list) def test_make_it(self): - sot = capabilities.Capabilities(BODY) + sot = capabilities.Capabilities(**BODY) self.assertEqual(BODY['aggregation_methods'], sot.aggregation_methods) diff --git a/openstack/tests/unit/metric/v1/test_metric.py b/openstack/tests/unit/metric/v1/test_metric.py index 24a9aac4c..86512982a 100644 --- a/openstack/tests/unit/metric/v1/test_metric.py +++ b/openstack/tests/unit/metric/v1/test_metric.py @@ -52,13 +52,13 @@ def test_basic(self): self.assertEqual('/metric', m.base_path) self.assertEqual('metric', m.service.service_type) self.assertTrue(m.allow_create) - self.assertTrue(m.allow_retrieve) + self.assertTrue(m.allow_get) self.assertFalse(m.allow_update) self.assertTrue(m.allow_delete) self.assertTrue(m.allow_list) def test_make_it(self): - m = metric.Metric(EXAMPLE) + m = metric.Metric(**EXAMPLE) self.assertEqual(EXAMPLE['id'], m.id) self.assertEqual(EXAMPLE['archive_policy_name'], m.archive_policy_name) self.assertEqual(EXAMPLE['created_by_user_id'], m.created_by_user_id) @@ -67,7 +67,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['resource_id'], m.resource_id) self.assertEqual(EXAMPLE['name'], m.name) - m = metric.Metric(EXAMPLE_AP) + m = metric.Metric(**EXAMPLE_AP) self.assertEqual(EXAMPLE_AP['id'], m.id) self.assertEqual(EXAMPLE_AP['archive_policy'], m.archive_policy) self.assertEqual(EXAMPLE_AP['created_by_user_id'], diff --git a/openstack/tests/unit/metric/v1/test_proxy.py b/openstack/tests/unit/metric/v1/test_proxy.py index ceb900149..c3abc4ace 100644 --- a/openstack/tests/unit/metric/v1/test_proxy.py +++ b/openstack/tests/unit/metric/v1/test_proxy.py @@ -12,7 +12,7 @@ from openstack.metric.v1 import _proxy from openstack.metric.v1 import capabilities -from openstack.tests.unit import test_proxy_base +from openstack.tests.unit import test_proxy_base2 as test_proxy_base class TestMetricProxy(test_proxy_base.TestProxyBase): diff --git a/openstack/tests/unit/metric/v1/test_resource.py b/openstack/tests/unit/metric/v1/test_resource.py index 0a8617c0d..80dad8c52 100644 --- a/openstack/tests/unit/metric/v1/test_resource.py +++ b/openstack/tests/unit/metric/v1/test_resource.py @@ -36,13 +36,13 @@ def test_generic(self): self.assertEqual('/resource/generic', m.base_path) self.assertEqual('metric', m.service.service_type) self.assertTrue(m.allow_create) - self.assertTrue(m.allow_retrieve) + self.assertTrue(m.allow_get) self.assertTrue(m.allow_update) self.assertTrue(m.allow_delete) self.assertTrue(m.allow_list) def test_make_generic(self): - r = resource.Generic(EXAMPLE_GENERIC) + r = resource.Generic(**EXAMPLE_GENERIC) self.assertEqual(EXAMPLE_GENERIC['created_by_user_id'], r.created_by_user_id) self.assertEqual(EXAMPLE_GENERIC['created_by_project_id'], From 9d36974c2dfdf4579b3f1d7c27979a7b005d34b0 Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Thu, 27 Apr 2017 16:23:17 -0400 Subject: [PATCH 1469/3836] Port unversioned Version resources to resource2 Most services have a Version resource in their unversioned namespace, generally predating the proxy interface. As such none of them are being exposed or used as of right now, but rather than remove them we should port them over as-is, then later figure out if we need to expose them for use or if we don't actually need them. Change-Id: I8f4ed67f6185bd1928ce73f8059d629dc0952f6b --- openstack/cluster/version.py | 6 +++--- openstack/compute/version.py | 8 ++++---- openstack/identity/version.py | 10 +++++----- openstack/network/version.py | 6 +++--- openstack/orchestration/version.py | 6 +++--- openstack/tests/unit/cluster/test_version.py | 4 ++-- openstack/tests/unit/compute/test_version.py | 4 ++-- openstack/tests/unit/identity/test_version.py | 10 +++++----- openstack/tests/unit/network/test_version.py | 4 ++-- openstack/tests/unit/orchestration/test_version.py | 4 ++-- 10 files changed, 31 insertions(+), 31 deletions(-) diff --git a/openstack/cluster/version.py b/openstack/cluster/version.py index 070497b67..44549b60f 100644 --- a/openstack/cluster/version.py +++ b/openstack/cluster/version.py @@ -12,7 +12,7 @@ from openstack.cluster import cluster_service -from openstack import resource +from openstack import resource2 as resource class Version(resource.Resource): @@ -27,5 +27,5 @@ class Version(resource.Resource): allow_list = True # Properties - links = resource.prop('links') - status = resource.prop('status') + links = resource.Body('links') + status = resource.Body('status') diff --git a/openstack/compute/version.py b/openstack/compute/version.py index 23a2398f5..186f35e18 100644 --- a/openstack/compute/version.py +++ b/openstack/compute/version.py @@ -11,7 +11,7 @@ # under the License. from openstack.compute import compute_service -from openstack import resource +from openstack import resource2 as resource class Version(resource.Resource): @@ -26,6 +26,6 @@ class Version(resource.Resource): allow_list = True # Properties - links = resource.prop('links') - status = resource.prop('status') - updated = resource.prop('updated') + links = resource.Body('links') + status = resource.Body('status') + updated = resource.Body('updated') diff --git a/openstack/identity/version.py b/openstack/identity/version.py index bfea2a3a2..4564cab9d 100644 --- a/openstack/identity/version.py +++ b/openstack/identity/version.py @@ -11,7 +11,7 @@ # under the License. from openstack.identity import identity_service -from openstack import resource +from openstack import resource2 as resource class Version(resource.Resource): @@ -26,12 +26,12 @@ class Version(resource.Resource): allow_list = True # Properties - media_types = resource.prop('media-types') - status = resource.prop('status') - updated = resource.prop('updated') + media_types = resource.Body('media-types') + status = resource.Body('status') + updated = resource.Body('updated') @classmethod - def list(cls, session, **params): + def list(cls, session, paginated=False, **params): resp = session.get(cls.base_path, endpoint_filter=cls.service, params=params) resp = resp.json() diff --git a/openstack/network/version.py b/openstack/network/version.py index 5f1962810..a3b520b72 100644 --- a/openstack/network/version.py +++ b/openstack/network/version.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource +from openstack import resource2 as resource class Version(resource.Resource): @@ -26,5 +26,5 @@ class Version(resource.Resource): allow_list = True # Properties - links = resource.prop('links') - status = resource.prop('status') + links = resource.Body('links') + status = resource.Body('status') diff --git a/openstack/orchestration/version.py b/openstack/orchestration/version.py index f44118122..ecfdbc8eb 100644 --- a/openstack/orchestration/version.py +++ b/openstack/orchestration/version.py @@ -11,7 +11,7 @@ # under the License. from openstack.orchestration import orchestration_service -from openstack import resource +from openstack import resource2 as resource class Version(resource.Resource): @@ -26,5 +26,5 @@ class Version(resource.Resource): allow_list = True # Properties - links = resource.prop('links') - status = resource.prop('status') + links = resource.Body('links') + status = resource.Body('status') diff --git a/openstack/tests/unit/cluster/test_version.py b/openstack/tests/unit/cluster/test_version.py index 0efe92cfc..c9b0a5bd6 100644 --- a/openstack/tests/unit/cluster/test_version.py +++ b/openstack/tests/unit/cluster/test_version.py @@ -31,13 +31,13 @@ def test_basic(self): self.assertEqual('/', sot.base_path) self.assertEqual('clustering', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_retrieve) + self.assertFalse(sot.allow_get) self.assertFalse(sot.allow_update) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) def test_make_it(self): - sot = version.Version(EXAMPLE) + sot = version.Version(**EXAMPLE) self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['links'], sot.links) self.assertEqual(EXAMPLE['status'], sot.status) diff --git a/openstack/tests/unit/compute/test_version.py b/openstack/tests/unit/compute/test_version.py index 4e0ce5ec3..0940b2cf4 100644 --- a/openstack/tests/unit/compute/test_version.py +++ b/openstack/tests/unit/compute/test_version.py @@ -32,13 +32,13 @@ def test_basic(self): self.assertEqual('/', sot.base_path) self.assertEqual('compute', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_retrieve) + self.assertFalse(sot.allow_get) self.assertFalse(sot.allow_update) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) def test_make_it(self): - sot = version.Version(EXAMPLE) + sot = version.Version(**EXAMPLE) self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['links'], sot.links) self.assertEqual(EXAMPLE['status'], sot.status) diff --git a/openstack/tests/unit/identity/test_version.py b/openstack/tests/unit/identity/test_version.py index 0fac7cbb6..a3693f0da 100644 --- a/openstack/tests/unit/identity/test_version.py +++ b/openstack/tests/unit/identity/test_version.py @@ -33,13 +33,13 @@ def test_basic(self): self.assertEqual('/', sot.base_path) self.assertEqual('identity', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_retrieve) + self.assertFalse(sot.allow_get) self.assertFalse(sot.allow_update) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) def test_make_it(self): - sot = version.Version(EXAMPLE) + sot = version.Version(**EXAMPLE) self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['media-types'], sot.media_types) self.assertEqual(EXAMPLE['status'], sot.status) @@ -58,8 +58,8 @@ def test_list(self): resp.json = mock.Mock(return_value=resp.body) session = mock.Mock() session.get = mock.Mock(return_value=resp) - sot = version.Version(EXAMPLE) + sot = version.Version(**EXAMPLE) result = sot.list(session) - self.assertEqual(next(result)['id'], 'v1.0') - self.assertEqual(next(result)['id'], 'v1.1') + self.assertEqual(next(result).id, 'v1.0') + self.assertEqual(next(result).id, 'v1.1') self.assertRaises(StopIteration, next, result) diff --git a/openstack/tests/unit/network/test_version.py b/openstack/tests/unit/network/test_version.py index cfd1951a7..6f3def463 100644 --- a/openstack/tests/unit/network/test_version.py +++ b/openstack/tests/unit/network/test_version.py @@ -31,13 +31,13 @@ def test_basic(self): self.assertEqual('/', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_retrieve) + self.assertFalse(sot.allow_get) self.assertFalse(sot.allow_update) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) def test_make_it(self): - sot = version.Version(EXAMPLE) + sot = version.Version(**EXAMPLE) self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['links'], sot.links) self.assertEqual(EXAMPLE['status'], sot.status) diff --git a/openstack/tests/unit/orchestration/test_version.py b/openstack/tests/unit/orchestration/test_version.py index 9943fece0..5da8a37c7 100644 --- a/openstack/tests/unit/orchestration/test_version.py +++ b/openstack/tests/unit/orchestration/test_version.py @@ -31,13 +31,13 @@ def test_basic(self): self.assertEqual('/', sot.base_path) self.assertEqual('orchestration', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_retrieve) + self.assertFalse(sot.allow_get) self.assertFalse(sot.allow_update) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) def test_make_it(self): - sot = version.Version(EXAMPLE) + sot = version.Version(**EXAMPLE) self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['links'], sot.links) self.assertEqual(EXAMPLE['status'], sot.status) From a5c49cb0ef04b9e076546527cd03d775ab5b81ff Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 27 Apr 2017 19:01:30 -0500 Subject: [PATCH 1470/3836] Set interface=admin for keystonev2 keystone tests Keystone v2 needs to perform admin tasks over the admin interface. Be explicit about doing that. Change-Id: If3352fccd244a5515af8d4f95c9992df74cfb39d --- shade/tests/functional/base.py | 41 +++++++++++++++++++----- shade/tests/functional/test_endpoints.py | 2 +- shade/tests/functional/test_identity.py | 2 +- shade/tests/functional/test_project.py | 2 +- shade/tests/functional/test_services.py | 2 +- shade/tests/functional/test_users.py | 2 +- 6 files changed, 38 insertions(+), 13 deletions(-) diff --git a/shade/tests/functional/base.py b/shade/tests/functional/base.py index 919a8ef03..f2fc32525 100644 --- a/shade/tests/functional/base.py +++ b/shade/tests/functional/base.py @@ -22,22 +22,31 @@ class BaseFunctionalTestCase(base.TestCase): def setUp(self): super(BaseFunctionalTestCase, self).setUp() - demo_name = os.environ.get('SHADE_DEMO_CLOUD', 'devstack') - op_name = os.environ.get('SHADE_OPERATOR_CLOUD', 'devstack-admin') + self._demo_name = os.environ.get('SHADE_DEMO_CLOUD', 'devstack') + self._op_name = os.environ.get( + 'SHADE_OPERATOR_CLOUD', 'devstack-admin') self.config = occ.OpenStackConfig() - demo_config = self.config.get_one_cloud(cloud=demo_name) + self._set_user_cloud() + self._set_operator_cloud() + + self.identity_version = \ + self.operator_cloud.cloud_config.get_api_version('identity') + + def _set_user_cloud(self, **kwargs): + user_config = self.config.get_one_cloud( + cloud=self._demo_name, **kwargs) self.user_cloud = shade.OpenStackCloud( - cloud_config=demo_config, + cloud_config=user_config, log_inner_exceptions=True) - operator_config = self.config.get_one_cloud(cloud=op_name) + + def _set_operator_cloud(self, **kwargs): + operator_config = self.config.get_one_cloud( + cloud=self._op_name, **kwargs) self.operator_cloud = shade.OperatorCloud( cloud_config=operator_config, log_inner_exceptions=True) - self.identity_version = \ - self.operator_cloud.cloud_config.get_api_version('identity') - def pick_image(self): images = self.user_cloud.list_images() self.add_info_on_exception('images', images) @@ -64,3 +73,19 @@ def pick_image(self): if image.name.lower().startswith('centos'): return image self.assertFalse('no sensible image available') + + +class KeystoneBaseFunctionalTestCase(BaseFunctionalTestCase): + + def setUp(self): + super(KeystoneBaseFunctionalTestCase, self).setUp() + + use_keystone_v2 = os.environ.get('SHADE_USE_KEYSTONE_V2', False) + if use_keystone_v2: + # keystone v2 has special behavior for the admin + # interface and some of the operations, so make a new cloud + # object with interface set to admin. + # We only do it for keystone tests on v2 because otherwise + # the admin interface is not a thing that wants to actually + # be used + self._set_operator_cloud(interface='admin') diff --git a/shade/tests/functional/test_endpoints.py b/shade/tests/functional/test_endpoints.py index 4ef648c47..da39cb33b 100644 --- a/shade/tests/functional/test_endpoints.py +++ b/shade/tests/functional/test_endpoints.py @@ -29,7 +29,7 @@ from shade.tests.functional import base -class TestEndpoints(base.BaseFunctionalTestCase): +class TestEndpoints(base.KeystoneBaseFunctionalTestCase): endpoint_attributes = ['id', 'region', 'publicurl', 'internalurl', 'service_id', 'adminurl'] diff --git a/shade/tests/functional/test_identity.py b/shade/tests/functional/test_identity.py index 6cd3bf9fd..655407a9e 100644 --- a/shade/tests/functional/test_identity.py +++ b/shade/tests/functional/test_identity.py @@ -24,7 +24,7 @@ from shade.tests.functional import base -class TestIdentity(base.BaseFunctionalTestCase): +class TestIdentity(base.KeystoneBaseFunctionalTestCase): def setUp(self): super(TestIdentity, self).setUp() self.role_prefix = 'test_role' + ''.join( diff --git a/shade/tests/functional/test_project.py b/shade/tests/functional/test_project.py index 41b640ebc..9ffd4150f 100644 --- a/shade/tests/functional/test_project.py +++ b/shade/tests/functional/test_project.py @@ -25,7 +25,7 @@ from shade.tests.functional import base -class TestProject(base.BaseFunctionalTestCase): +class TestProject(base.KeystoneBaseFunctionalTestCase): def setUp(self): super(TestProject, self).setUp() diff --git a/shade/tests/functional/test_services.py b/shade/tests/functional/test_services.py index 729c6c7af..92920d53f 100644 --- a/shade/tests/functional/test_services.py +++ b/shade/tests/functional/test_services.py @@ -29,7 +29,7 @@ from shade.tests.functional import base -class TestServices(base.BaseFunctionalTestCase): +class TestServices(base.KeystoneBaseFunctionalTestCase): service_attributes = ['id', 'name', 'type', 'description'] diff --git a/shade/tests/functional/test_users.py b/shade/tests/functional/test_users.py index 2ea3d57ce..772bdcc34 100644 --- a/shade/tests/functional/test_users.py +++ b/shade/tests/functional/test_users.py @@ -22,7 +22,7 @@ from shade.tests.functional import base -class TestUsers(base.BaseFunctionalTestCase): +class TestUsers(base.KeystoneBaseFunctionalTestCase): def setUp(self): super(TestUsers, self).setUp() self.user_prefix = self.getUniqueString('user') From 8a4f99720f9b84f49dee9793c7712b4646485420 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 27 Apr 2017 19:42:01 -0500 Subject: [PATCH 1471/3836] Add in a bunch of TODOs about interface=admin keystone v2 needs to use the admin interface for a set of calls. We can't change that on a per-call basis until we're using REST - but once we are, by golly, make it happen. Change-Id: I8e77c1b0babb44bb44411f87b6567118a99d1aeb --- shade/openstackcloud.py | 3 +++ shade/operatorcloud.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index fd40bdc9d..109fc4b48 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -929,6 +929,9 @@ def update_user(self, name_or_id, **kwargs): # normalized dict won't work kwargs['user'] = self.get_user_by_id(user['id'], normalize=False) + # TODO(mordred) When this changes to REST, force interface=admin + # in the adapter call if it's an admin force call (and figure out how + # to make that disctinction) if self.cloud_config.get_api_version('identity') != '3': # Do not pass v3 args to a v2 keystone. kwargs.pop('domain_id', None) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index c1f7db730..56de2f51e 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -776,6 +776,8 @@ def create_service(self, name, enabled=True, **kwargs): type_ = kwargs.pop('type', None) service_type = kwargs.pop('service_type', None) + # TODO(mordred) When this changes to REST, force interface=admin + # in the adapter call if self.cloud_config.get_api_version('identity').startswith('2'): kwargs['service_type'] = type_ or service_type else: @@ -800,6 +802,8 @@ def update_service(self, name_or_id, **kwargs): 'Unavailable Feature: Service update requires Identity v3' ) + # TODO(mordred) When this changes to REST, force interface=admin + # in the adapter call # NOTE(SamYaple): Keystone v3 only accepts 'type' but shade accepts # both 'type' and 'service_type' with a preference # towards 'type' @@ -825,6 +829,8 @@ def list_services(self): :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call. """ + # TODO(mordred) When this changes to REST, force interface=admin + # in the adapter call with _utils.shade_exceptions(): services = self.manager.submit_task(_tasks.ServiceList()) return _utils.normalize_keystone_services(services) @@ -882,6 +888,8 @@ def delete_service(self, name_or_id): service_kwargs = {'id': service['id']} else: service_kwargs = {'service': service['id']} + # TODO(mordred) When this changes to REST, force interface=admin + # in the adapter call with _utils.shade_exceptions("Failed to delete service {id}".format( id=service['id'])): self.manager.submit_task(_tasks.ServiceDelete(**service_kwargs)) @@ -971,6 +979,8 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, if region is not None: kwargs['region'] = region + # TODO(mordred) When this changes to REST, force interface=admin + # in the adapter call with _utils.shade_exceptions( "Failed to create endpoint for service" " {service}".format(service=service['name']) @@ -997,6 +1007,8 @@ def update_endpoint(self, endpoint_id, **kwargs): if service_name_or_id is not None: kwargs['service'] = service_name_or_id + # TODO(mordred) When this changes to REST, force interface=admin + # in the adapter call with _utils.shade_exceptions( "Failed to update endpoint {}".format(endpoint_id) ): @@ -1016,6 +1028,8 @@ def list_endpoints(self): # the keystone api, but since the return of all the endpoints even in # large environments is small, we can continue to filter in shade just # like the v2 api. + # TODO(mordred) When this changes to REST, force interface=admin + # in the adapter call with _utils.shade_exceptions("Failed to list endpoints"): endpoints = self.manager.submit_task(_tasks.EndpointList()) @@ -1074,6 +1088,8 @@ def delete_endpoint(self, id): self.log.debug("Endpoint %s not found for deleting", id) return False + # TODO(mordred) When this changes to REST, force interface=admin + # in the adapter call if self.cloud_config.get_api_version('identity').startswith('2'): endpoint_kwargs = {'id': endpoint['id']} else: From 17debbb09907b83d5781ad683f4b72c9eb1ce221 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 28 Apr 2017 12:46:44 -0500 Subject: [PATCH 1472/3836] Fix interactions with keystoneauth from newton keystoneauth in newton did not have app_name or app_version as Session parameters. Although it isn't a super common combination, user agent strings aren't a reason to break something. Add a simple workaround. Change-Id: Ib5774389fefdbc190a4b78dd6784c8006afbb270 --- os_client_config/cloud_config.py | 10 +++++-- os_client_config/tests/test_cloud_config.py | 28 ++++++++++++++++--- ...ith-old-keystoneauth-66e11ee9d008b962.yaml | 7 +++++ 3 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/fix-compat-with-old-keystoneauth-66e11ee9d008b962.yaml diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 086092488..fec350094 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -215,14 +215,20 @@ def get_session(self): requestsexceptions.squelch_warnings(insecure_requests=not verify) self._keystone_session = self._session_constructor( auth=self._auth, - app_name=self._app_name, - app_version=self._app_version, verify=verify, cert=cert, timeout=self.config['api_timeout']) if hasattr(self._keystone_session, 'additional_user_agent'): self._keystone_session.additional_user_agent.append( ('os-client-config', os_client_config.__version__)) + # Using old keystoneauth with new os-client-config fails if + # we pass in app_name and app_version. Those are not essential, + # nor a reason to bump our minimum, so just test for the session + # having the attribute post creation and set them then. + if hasattr(self._keystone_session, 'app_name'): + self._keystone_session.app_name = self._app_name + if hasattr(self._keystone_session, 'app_version'): + self._keystone_session.app_version = self._app_version return self._keystone_session def get_session_client(self, service_key): diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index 0671fcc8e..5df1fafa2 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -187,8 +187,29 @@ def test_get_session(self, mock_session): cc.get_session() mock_session.assert_called_with( auth=mock.ANY, - verify=True, cert=None, timeout=None, - app_name=None, app_version=None) + verify=True, cert=None, timeout=None) + self.assertEqual( + fake_session.additional_user_agent, + [('os-client-config', '1.2.3')]) + + @mock.patch.object(ksa_session, 'Session') + def test_get_session_with_app_name(self, mock_session): + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + fake_session = mock.Mock() + fake_session.additional_user_agent = [] + fake_session.app_name = None + fake_session.app_version = None + mock_session.return_value = fake_session + cc = cloud_config.CloudConfig( + "test1", "region-al", config_dict, auth_plugin=mock.Mock(), + app_name="test_app", app_version="test_version") + cc.get_session() + mock_session.assert_called_with( + auth=mock.ANY, + verify=True, cert=None, timeout=None) + self.assertEqual(fake_session.app_name, "test_app") + self.assertEqual(fake_session.app_version, "test_version") self.assertEqual( fake_session.additional_user_agent, [('os-client-config', '1.2.3')]) @@ -206,8 +227,7 @@ def test_get_session_with_timeout(self, mock_session): cc.get_session() mock_session.assert_called_with( auth=mock.ANY, - verify=True, cert=None, timeout=9, - app_name=None, app_version=None) + verify=True, cert=None, timeout=9) self.assertEqual( fake_session.additional_user_agent, [('os-client-config', '1.2.3')]) diff --git a/releasenotes/notes/fix-compat-with-old-keystoneauth-66e11ee9d008b962.yaml b/releasenotes/notes/fix-compat-with-old-keystoneauth-66e11ee9d008b962.yaml new file mode 100644 index 000000000..80d09fb83 --- /dev/null +++ b/releasenotes/notes/fix-compat-with-old-keystoneauth-66e11ee9d008b962.yaml @@ -0,0 +1,7 @@ +--- +issues: + - Fixed a regression when using latest os-client-config with + the keystoneauth from stable/newton. Although this isn't a + super common combination, the added feature that broke the + interaction is really not worthy of the incompatibility, so + a workaround was added. From e81bec084a7154a8bac206eded94587dafb950b0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 28 Apr 2017 08:12:45 -0500 Subject: [PATCH 1473/3836] Add optional error_message to adapter.request When we make REST cals, sometimes we want to provide a more helpful error string. Add an optional argument to the calls to allow this. Change-Id: I797e34cda9f27c18d66a42fd5860fe572fa55ada --- shade/_adapter.py | 10 ++++++---- shade/exc.py | 32 +++++++++++++++++++++++++------- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/shade/_adapter.py b/shade/_adapter.py index b6cb2530e..f7a9241cb 100644 --- a/shade/_adapter.py +++ b/shade/_adapter.py @@ -111,8 +111,8 @@ def _log_request_id(self, response, obj=None): self.request_log.debug(tmpl.format(**kwargs)) return response - def _munch_response(self, response, result_key=None): - exc.raise_from_response(response) + def _munch_response(self, response, result_key=None, error_message=None): + exc.raise_from_response(response, error_message=error_message) if not response.content: # This doens't have any content @@ -154,7 +154,9 @@ def _munch_response(self, response, result_key=None): return meta.obj_to_dict(result) return result - def request(self, url, method, run_async=False, *args, **kwargs): + def request( + self, url, method, run_async=False, error_message=None, + *args, **kwargs): name_parts = extract_name(url) name = '.'.join([self.service_type, method] + name_parts) class_name = "".join([ @@ -179,4 +181,4 @@ def main(self, client): if run_async: return response else: - return self._munch_response(response) + return self._munch_response(response, error_message=error_message) diff --git a/shade/exc.py b/shade/exc.py index fbb67564e..913a2f299 100644 --- a/shade/exc.py +++ b/shade/exc.py @@ -89,18 +89,36 @@ class OpenStackCloudURINotFound(OpenStackCloudHTTPError): # Logic shamelessly stolen from requests -def raise_from_response(response): +def raise_from_response(response, error_message=None): msg = '' + if error_message: + msg = _ if 400 <= response.status_code < 500: - msg = '({code}) Client Error: {reason} for url: {url}'.format( - code=response.status_code, - reason=response.reason, - url=response.url) + source = "Client" elif 500 <= response.status_code < 600: - msg = '({code}) Server Error: {reason} for url: {url}'.format( - code=response.status_code, + source = "Server" + else: + source = None + + if response.reason: + remote_error = "Error: {reason} for {url}".format( reason=response.reason, url=response.url) + else: + remote_error = "Error for url: {url}".format(url=response.url) + + if source: + if error_message: + msg = '{error_message}. ({code}) {source} {remote_error}'.format( + error_message=error_message, + source=source, + code=response.status_code, + remote_error=remote_error) + else: + msg = '({code}) {source} {remote_error}'.format( + code=response.status_code, + source=source, + remote_error=remote_error) # Special case 404 since we raised a specific one for neutron exceptions # before From b9073b5698a708e82dc0cddd218cb922871fdcd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Fri, 28 Apr 2017 20:40:26 +0000 Subject: [PATCH 1474/3836] Remove neutronclient mocks from ports tests Neutronclient mock is replaced by mocking REST calls using base.RequestsMockTestCase class Change-Id: If6cbe3c926ead8e0fbabcf602cf0ea8db9fec922 --- shade/tests/unit/test_port.py | 248 +++++++++++++++++++++++----------- 1 file changed, 166 insertions(+), 82 deletions(-) diff --git a/shade/tests/unit/test_port.py b/shade/tests/unit/test_port.py index 6f9a21053..4fd2ad4e0 100644 --- a/shade/tests/unit/test_port.py +++ b/shade/tests/unit/test_port.py @@ -19,13 +19,11 @@ Test port resource (managed by neutron) """ -from mock import patch -from shade import OpenStackCloud from shade.exc import OpenStackCloudException from shade.tests.unit import base -class TestPort(base.TestCase): +class TestPort(base.RequestsMockTestCase): mock_neutron_port_create_rep = { 'port': { 'status': 'DOWN', @@ -141,19 +139,23 @@ class TestPort(base.TestCase): ] } - @patch.object(OpenStackCloud, 'neutron_client') - def test_create_port(self, mock_neutron_client): - mock_neutron_client.create_port.return_value = \ - self.mock_neutron_port_create_rep - + def test_create_port(self): + self.register_uris([ + dict(method="POST", + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json']), + json=self.mock_neutron_port_create_rep, + validate=dict( + json={'port': { + 'network_id': 'test-net-id', + 'name': 'test-port-name', + 'admin_state_up': True}})) + ]) port = self.cloud.create_port( network_id='test-net-id', name='test-port-name', admin_state_up=True) - - mock_neutron_client.create_port.assert_called_with( - body={'port': dict(network_id='test-net-id', name='test-port-name', - admin_state_up=True)}) self.assertEqual(self.mock_neutron_port_create_rep['port'], port) + self.assert_calls() def test_create_port_parameters(self): """Test that we detect invalid arguments passed to create_port""" @@ -162,30 +164,44 @@ def test_create_port_parameters(self): network_id='test-net-id', nome='test-port-name', stato_amministrativo_porta=True) - @patch.object(OpenStackCloud, 'neutron_client') - def test_create_port_exception(self, mock_neutron_client): - mock_neutron_client.create_port.side_effect = Exception('blah') - + def test_create_port_exception(self): + self.register_uris([ + dict(method="POST", + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json']), + status_code=500, + validate=dict( + json={'port': { + 'network_id': 'test-net-id', + 'name': 'test-port-name', + 'admin_state_up': True}})) + ]) self.assertRaises( OpenStackCloudException, self.cloud.create_port, network_id='test-net-id', name='test-port-name', admin_state_up=True) - - @patch.object(OpenStackCloud, 'neutron_client') - def test_update_port(self, mock_neutron_client): - mock_neutron_client.list_ports.return_value = \ - self.mock_neutron_port_list_rep - mock_neutron_client.update_port.return_value = \ - self.mock_neutron_port_update_rep - + self.assert_calls() + + def test_update_port(self): + port_id = 'd80b1a3b-4fc1-49f3-952e-1e2ab7081d8b' + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json']), + json=self.mock_neutron_port_list_rep), + dict(method='PUT', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'ports', '%s.json' % port_id]), + json=self.mock_neutron_port_update_rep, + validate=dict( + json={'port': {'name': 'test-port-name-updated'}})) + ]) port = self.cloud.update_port( - name_or_id='d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b', - name='test-port-name-updated') + name_or_id=port_id, name='test-port-name-updated') - mock_neutron_client.update_port.assert_called_with( - port='d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b', - body={'port': dict(name='test-port-name-updated')}) self.assertEqual(self.mock_neutron_port_update_rep['port'], port) + self.assert_calls() def test_update_port_parameters(self): """Test that we detect invalid arguments passed to update_port""" @@ -193,72 +209,140 @@ def test_update_port_parameters(self): TypeError, self.cloud.update_port, name_or_id='test-port-id', nome='test-port-name-updated') - @patch.object(OpenStackCloud, 'neutron_client') - def test_update_port_exception(self, mock_neutron_client): - mock_neutron_client.list_ports.return_value = \ - self.mock_neutron_port_list_rep - mock_neutron_client.update_port.side_effect = Exception('blah') - + def test_update_port_exception(self): + port_id = 'd80b1a3b-4fc1-49f3-952e-1e2ab7081d8b' + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json']), + json=self.mock_neutron_port_list_rep), + dict(method='PUT', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'ports', '%s.json' % port_id]), + status_code=500, + validate=dict( + json={'port': {'name': 'test-port-name-updated'}})) + ]) self.assertRaises( OpenStackCloudException, self.cloud.update_port, name_or_id='d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b', name='test-port-name-updated') - - @patch.object(OpenStackCloud, 'neutron_client') - def test_list_ports(self, mock_neutron_client): - mock_neutron_client.list_ports.return_value = \ - self.mock_neutron_port_list_rep - + self.assert_calls() + + def test_list_ports(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json']), + json=self.mock_neutron_port_list_rep) + ]) ports = self.cloud.list_ports() - - mock_neutron_client.list_ports.assert_called_with() self.assertItemsEqual(self.mock_neutron_port_list_rep['ports'], ports) - - @patch.object(OpenStackCloud, 'neutron_client') - def test_list_ports_exception(self, mock_neutron_client): - mock_neutron_client.list_ports.side_effect = Exception('blah') - + self.assert_calls() + + def test_list_ports_exception(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json']), + status_code=500) + ]) self.assertRaises(OpenStackCloudException, self.cloud.list_ports) - @patch.object(OpenStackCloud, 'neutron_client') - def test_search_ports_by_id(self, mock_neutron_client): - mock_neutron_client.list_ports.return_value = \ - self.mock_neutron_port_list_rep - - ports = self.cloud.search_ports( - name_or_id='f71a6703-d6de-4be1-a91a-a570ede1d159') + def test_search_ports_by_id(self): + port_id = 'f71a6703-d6de-4be1-a91a-a570ede1d159' + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json']), + json=self.mock_neutron_port_list_rep) + ]) + ports = self.cloud.search_ports(name_or_id=port_id) - mock_neutron_client.list_ports.assert_called_with() self.assertEqual(1, len(ports)) self.assertEqual('fa:16:3e:bb:3c:e4', ports[0]['mac_address']) + self.assert_calls() + + def test_search_ports_by_name(self): + port_name = "first-port" + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json']), + json=self.mock_neutron_port_list_rep) + ]) + ports = self.cloud.search_ports(name_or_id=port_name) - @patch.object(OpenStackCloud, 'neutron_client') - def test_search_ports_by_name(self, mock_neutron_client): - mock_neutron_client.list_ports.return_value = \ - self.mock_neutron_port_list_rep - - ports = self.cloud.search_ports(name_or_id='first-port') - - mock_neutron_client.list_ports.assert_called_with() self.assertEqual(1, len(ports)) self.assertEqual('fa:16:3e:58:42:ed', ports[0]['mac_address']) - - @patch.object(OpenStackCloud, 'neutron_client') - def test_search_ports_not_found(self, mock_neutron_client): - mock_neutron_client.list_ports.return_value = \ - self.mock_neutron_port_list_rep - + self.assert_calls() + + def test_search_ports_not_found(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json']), + json=self.mock_neutron_port_list_rep) + ]) ports = self.cloud.search_ports(name_or_id='non-existent') - - mock_neutron_client.list_ports.assert_called_with() self.assertEqual(0, len(ports)) - - @patch.object(OpenStackCloud, 'neutron_client') - def test_delete_port(self, mock_neutron_client): - mock_neutron_client.list_ports.return_value = \ - self.mock_neutron_port_list_rep - - self.cloud.delete_port(name_or_id='first-port') - - mock_neutron_client.delete_port.assert_called_with( - port='d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b') + self.assert_calls() + + def test_delete_port(self): + port_id = 'd80b1a3b-4fc1-49f3-952e-1e2ab7081d8b' + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json']), + json=self.mock_neutron_port_list_rep), + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'ports', '%s.json' % port_id]), + json={}) + ]) + + self.assertTrue(self.cloud.delete_port(name_or_id='first-port')) + + def test_delete_port_not_found(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json']), + json=self.mock_neutron_port_list_rep) + ]) + self.assertFalse(self.cloud.delete_port(name_or_id='non-existent')) + self.assert_calls() + + def test_delete_subnet_multiple_found(self): + port_name = "port-name" + port1 = dict(id='123', name=port_name) + port2 = dict(id='456', name=port_name) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json']), + json={'ports': [port1, port2]}) + ]) + self.assertRaises(OpenStackCloudException, + self.cloud.delete_port, port_name) + self.assert_calls() + + def test_delete_subnet_multiple_using_id(self): + port_name = "port-name" + port1 = dict(id='123', name=port_name) + port2 = dict(id='456', name=port_name) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json']), + json={'ports': [port1, port2]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'ports', '%s.json' % port1['id']]), + json={}) + ]) + self.assertTrue(self.cloud.delete_port(name_or_id=port1['id'])) + self.assert_calls() From 20793660ec0b0429c5aacaa1c5fd70c8ea8f576c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Fri, 28 Apr 2017 20:59:01 +0000 Subject: [PATCH 1475/3836] Remove neutronclient mocks from quotas tests Neutronclient mock is replaced by mocking REST calls using base.RequestsMockTestCase class Change-Id: Ic6fc4f4f29bb28cd865e39191c2ed79977500cd5 --- shade/tests/unit/test_quotas.py | 58 ++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/shade/tests/unit/test_quotas.py b/shade/tests/unit/test_quotas.py index 91d87ea44..3cd35eb65 100644 --- a/shade/tests/unit/test_quotas.py +++ b/shade/tests/unit/test_quotas.py @@ -100,32 +100,58 @@ def test_cinder_delete_quotas(self, mock_cinder): tenant_id=project.project_id) self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_neutron_update_quotas(self, mock_neutron): + def test_neutron_update_quotas(self): project = self.mock_for_keystone_projects(project_count=1, list_get=True)[0] + self.register_uris([ + dict(method='PUT', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'quotas', + '%s.json' % project.project_id]), + json={}, + validate=dict( + json={'quota': {'network': 1}})) + ]) self.op_cloud.set_network_quotas(project.project_id, network=1) - - mock_neutron.update_quota.assert_called_once_with( - body={'quota': {'network': 1}}, tenant_id=project.project_id) self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_neutron_get_quotas(self, mock_neutron): + def test_neutron_get_quotas(self): + quota = { + 'subnet': 100, + 'network': 100, + 'floatingip': 50, + 'subnetpool': -1, + 'security_group_rule': 100, + 'security_group': 10, + 'router': 10, + 'rbac_policy': 10, + 'port': 500 + } project = self.mock_for_keystone_projects(project_count=1, list_get=True)[0] - self.op_cloud.get_network_quotas(project.project_id) - - mock_neutron.show_quota.assert_called_once_with( - tenant_id=project.project_id) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'quotas', + '%s.json' % project.project_id]), + json={'quota': quota}) + ]) + received_quota = self.op_cloud.get_network_quotas(project.project_id) + self.assertDictEqual(quota, received_quota) self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_neutron_delete_quotas(self, mock_neutron): + def test_neutron_delete_quotas(self): project = self.mock_for_keystone_projects(project_count=1, list_get=True)[0] + self.register_uris([ + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'quotas', + '%s.json' % project.project_id]), + json={}) + ]) self.op_cloud.delete_network_quotas(project.project_id) - - mock_neutron.delete_quota.assert_called_once_with( - tenant_id=project.project_id) self.assert_calls() From dc9a3458add51d3cd48b0594e38b8842fc1fc39c Mon Sep 17 00:00:00 2001 From: Akihiro Motoki Date: Sat, 29 Apr 2017 14:39:35 +0000 Subject: [PATCH 1476/3836] functional tests: minor cleanup * Check service availability for swift, heat and octavia * Use OS_CLOUD=devstack-admin. DevStack sets up default clouds.yaml for member and admin roles. The current OpenStackSDK functest prepares its own clouds.yaml based on DevStack admin account, so there is no difference from 'devstack-admin'. It simplifies the post_gate_hook process. * Skip openstack.tests.functional.network.v2.test_quota.TestQuota.test_set (Related-Bug: #1687202) Change-Id: I571e437dd7e439d2ad8a1d7ed9a5a24361b1308e --- examples/connect.py | 4 ++-- openstack/tests/functional/base.py | 4 ++-- .../tests/functional/load_balancer/v2/test_load_balancer.py | 3 +++ openstack/tests/functional/network/v2/test_quota.py | 3 +++ openstack/tests/functional/object_store/v1/test_account.py | 4 ++++ .../tests/functional/object_store/v1/test_container.py | 3 +++ openstack/tests/functional/object_store/v1/test_obj.py | 3 +++ openstack/tests/functional/orchestration/v1/test_stack.py | 2 ++ post_test_hook.sh | 6 ++---- 9 files changed, 24 insertions(+), 8 deletions(-) diff --git a/examples/connect.py b/examples/connect.py index 1cf195ff4..07216ae16 100644 --- a/examples/connect.py +++ b/examples/connect.py @@ -32,11 +32,11 @@ #: file, typically in $HOME/.config/openstack/clouds.yaml. That configuration #: will determine where the examples will be run and what resource defaults #: will be used to run the examples. -TEST_CLOUD = os.getenv('OS_TEST_CLOUD', 'test_cloud') +TEST_CLOUD = os.getenv('OS_TEST_CLOUD', 'devstack-admin') class Opts(object): - def __init__(self, cloud_name='test_cloud', debug=False): + def __init__(self, cloud_name='devstack-admin', debug=False): self.cloud = cloud_name self.debug = debug # Use identity v3 API for examples. diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index 57544a918..9eb4eba4d 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -23,11 +23,11 @@ #: file, typically in $HOME/.config/openstack/clouds.yaml. That configuration #: will determine where the functional tests will be run and what resource #: defaults will be used to run the functional tests. -TEST_CLOUD = os.getenv('OS_CLOUD', 'test_cloud') +TEST_CLOUD = os.getenv('OS_CLOUD', 'devstack-admin') class Opts(object): - def __init__(self, cloud_name='test_cloud', debug=False): + def __init__(self, cloud_name='devstack-admin', debug=False): self.cloud = cloud_name self.debug = debug diff --git a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py index 4082a1194..acf953498 100644 --- a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py +++ b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py @@ -10,12 +10,15 @@ # License for the specific language governing permissions and limitations # under the License. +import unittest import uuid from openstack.load_balancer.v2 import load_balancer from openstack.tests.functional import base +@unittest.skipUnless(base.service_exists(service_type='load_balancer'), + 'Load-balancing service does not exist') class TestLoadBalancer(base.BaseFunctionalTest): NAME = uuid.uuid4().hex diff --git a/openstack/tests/functional/network/v2/test_quota.py b/openstack/tests/functional/network/v2/test_quota.py index 9fa100453..14be76dde 100644 --- a/openstack/tests/functional/network/v2/test_quota.py +++ b/openstack/tests/functional/network/v2/test_quota.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import unittest + from openstack.tests.functional import base @@ -20,6 +22,7 @@ def test_list(self): self.assertIsNotNone(qot.project_id) self.assertIsNotNone(qot.networks) + @unittest.skip('bug/1687202') def test_set(self): attrs = {'networks': 123456789} for project_quota in self.conn.network.quotas(): diff --git a/openstack/tests/functional/object_store/v1/test_account.py b/openstack/tests/functional/object_store/v1/test_account.py index 879f15267..fe258db88 100644 --- a/openstack/tests/functional/object_store/v1/test_account.py +++ b/openstack/tests/functional/object_store/v1/test_account.py @@ -10,9 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. +import unittest + from openstack.tests.functional import base +@unittest.skipUnless(base.service_exists(service_type='object-store'), + 'Object Storage service does not exist') class TestAccount(base.BaseFunctionalTest): @classmethod diff --git a/openstack/tests/functional/object_store/v1/test_container.py b/openstack/tests/functional/object_store/v1/test_container.py index 206f37211..0a6dc754f 100644 --- a/openstack/tests/functional/object_store/v1/test_container.py +++ b/openstack/tests/functional/object_store/v1/test_container.py @@ -10,12 +10,15 @@ # License for the specific language governing permissions and limitations # under the License. +import unittest import uuid from openstack.object_store.v1 import container as _container from openstack.tests.functional import base +@unittest.skipUnless(base.service_exists(service_type='object-store'), + 'Object Storage service does not exist') class TestContainer(base.BaseFunctionalTest): NAME = uuid.uuid4().hex diff --git a/openstack/tests/functional/object_store/v1/test_obj.py b/openstack/tests/functional/object_store/v1/test_obj.py index 89990f20c..ace53cedc 100644 --- a/openstack/tests/functional/object_store/v1/test_obj.py +++ b/openstack/tests/functional/object_store/v1/test_obj.py @@ -10,11 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. +import unittest import uuid from openstack.tests.functional import base +@unittest.skipUnless(base.service_exists(service_type='object-store'), + 'Object Storage service does not exist') class TestObject(base.BaseFunctionalTest): FOLDER = uuid.uuid4().hex diff --git a/openstack/tests/functional/orchestration/v1/test_stack.py b/openstack/tests/functional/orchestration/v1/test_stack.py index 6da674840..73dc19339 100644 --- a/openstack/tests/functional/orchestration/v1/test_stack.py +++ b/openstack/tests/functional/orchestration/v1/test_stack.py @@ -19,6 +19,8 @@ @unittest.skip("bug/1525005") +@unittest.skipUnless(base.service_exists(service_type='orchestration'), + 'Orchestration service does not exist') class TestStack(base.BaseFunctionalTest): NAME = 'test_stack' diff --git a/post_test_hook.sh b/post_test_hook.sh index b40b0d98a..567dd1f7d 100755 --- a/post_test_hook.sh +++ b/post_test_hook.sh @@ -8,11 +8,9 @@ DIR=$(cd $(dirname "$0") && pwd) echo "Running SDK functional test suite" sudo -H -u stack -i < Date: Sat, 29 Apr 2017 17:28:01 +0000 Subject: [PATCH 1477/3836] Fix document warnings Also warning-is-error is set in setup.cfg to avoid future warnings. Change-Id: I4381a47d35b5736c9af0a2cb8bdd330266825922 --- doc/source/users/proxies/identity_v2.rst | 2 +- openstack/database/v1/_proxy.py | 2 +- setup.cfg | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/source/users/proxies/identity_v2.rst b/doc/source/users/proxies/identity_v2.rst index 3f2c65aeb..0cd4b77e2 100644 --- a/doc/source/users/proxies/identity_v2.rst +++ b/doc/source/users/proxies/identity_v2.rst @@ -20,7 +20,7 @@ Extension Operations .. automethod:: openstack.identity.v2._proxy.Proxy.get_extension .. automethod:: openstack.identity.v2._proxy.Proxy.extensions - User Operations +User Operations ^^^^^^^^^^^^^^^ .. autoclass:: openstack.identity.v2._proxy.Proxy diff --git a/openstack/database/v1/_proxy.py b/openstack/database/v1/_proxy.py index b06e4fdb5..89a1702cd 100644 --- a/openstack/database/v1/_proxy.py +++ b/openstack/database/v1/_proxy.py @@ -80,7 +80,7 @@ def databases(self, instance, **query): :param instance: This can be either the ID of an instance or a :class:`~openstack.database.v1.instance.Instance` - instance that the interface belongs to. + instance that the interface belongs to. :param kwargs \*\*query: Optional query parameters to be sent to limit the resources being returned. diff --git a/setup.cfg b/setup.cfg index a11e58a1a..2c6b5a0a4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,6 +28,7 @@ packages = source-dir = doc/source build-dir = doc/build all_files = 1 +warning-is-error = 1 [upload_sphinx] upload-dir = doc/build/html From 05f16004187a8e1fbf42d1b164e37b5d98deef7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Sat, 29 Apr 2017 20:45:41 +0000 Subject: [PATCH 1478/3836] Remove neutronclient mocks from sec groups tests Neutronclient mock is replaced by mocking REST calls using base.RequestsMockTestCase class Change-Id: I2a9516e7d7da7f604efb7a4c747674dc761de192 --- shade/tests/unit/test_security_groups.py | 307 ++++++++++++++--------- 1 file changed, 187 insertions(+), 120 deletions(-) diff --git a/shade/tests/unit/test_security_groups.py b/shade/tests/unit/test_security_groups.py index a792d80c6..e41186ea1 100644 --- a/shade/tests/unit/test_security_groups.py +++ b/shade/tests/unit/test_security_groups.py @@ -11,9 +11,7 @@ # under the License. -import mock - -from neutronclient.common import exceptions as neutron_exc +import copy import shade from shade import meta @@ -63,12 +61,19 @@ def fake_has_service(*args, **kwargs): return self.has_neutron self.cloud.has_service = fake_has_service - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_list_security_groups_neutron(self, mock_neutron): + def test_list_security_groups_neutron(self): + project_id = 42 self.cloud.secgroup_source = 'neutron' - self.cloud.list_security_groups(filters={'project_id': 42}) - mock_neutron.list_security_groups.assert_called_once_with( - project_id=42) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'security-groups.json'], + qs_elements=["project_id=%s" % project_id]), + json={'security_groups': [neutron_grp_dict]}) + ]) + self.cloud.list_security_groups(filters={'project_id': project_id}) + self.assert_calls() def test_list_security_groups_nova(self): self.register_uris([ @@ -90,15 +95,23 @@ def test_list_security_groups_none(self): self.assertRaises(shade.OpenStackCloudUnavailableFeature, self.cloud.list_security_groups) - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_security_group_neutron(self, mock_neutron): + def test_delete_security_group_neutron(self): + sg_id = neutron_grp_dict['id'] self.cloud.secgroup_source = 'neutron' - neutron_return = dict(security_groups=[neutron_grp_dict]) - mock_neutron.list_security_groups.return_value = neutron_return - self.cloud.delete_security_group('1') - mock_neutron.delete_security_group.assert_called_once_with( - security_group='1' - ) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'security-groups.json']), + json={'security_groups': [neutron_grp_dict]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'security-groups', '%s.json' % sg_id]), + json={}) + ]) + self.assertTrue(self.cloud.delete_security_group('1')) + self.assert_calls() def test_delete_security_group_nova(self): self.cloud.secgroup_source = 'nova' @@ -116,13 +129,17 @@ def test_delete_security_group_nova(self): self.cloud.delete_security_group('2') self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_security_group_neutron_not_found(self, mock_neutron): + def test_delete_security_group_neutron_not_found(self): self.cloud.secgroup_source = 'neutron' - neutron_return = dict(security_groups=[neutron_grp_dict]) - mock_neutron.list_security_groups.return_value = neutron_return - self.cloud.delete_security_group('doesNotExist') - self.assertFalse(mock_neutron.delete_security_group.called) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'security-groups.json']), + json={'security_groups': [neutron_grp_dict]}) + ]) + self.assertFalse(self.cloud.delete_security_group('10')) + self.assert_calls() def test_delete_security_group_nova_not_found(self): self.cloud.secgroup_source = 'nova' @@ -142,16 +159,34 @@ def test_delete_security_group_none(self): self.cloud.delete_security_group, 'doesNotExist') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_security_group_neutron(self, mock_neutron): + def test_create_security_group_neutron(self): self.cloud.secgroup_source = 'neutron' group_name = self.getUniqueString() group_desc = 'security group from test_create_security_group_neutron' - self.cloud.create_security_group(group_name, group_desc) - mock_neutron.create_security_group.assert_called_once_with( - body=dict(security_group=dict(name=group_name, - description=group_desc)) - ) + new_group = meta.obj_to_dict( + fakes.FakeSecgroup( + id='2', + name=group_name, + description=group_desc, + rules=[])) + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'security-groups.json']), + json={'security_group': new_group}, + validate=dict( + json={'security_group': { + 'name': group_name, + 'description': group_desc + }})) + ]) + + r = self.cloud.create_security_group(group_name, group_desc) + self.assertEqual(group_name, r['name']) + self.assertEqual(group_desc, r['description']) + + self.assert_calls() def test_create_security_group_nova(self): group_name = self.getUniqueString() @@ -189,16 +224,29 @@ def test_create_security_group_none(self): self.cloud.create_security_group, '', '') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_update_security_group_neutron(self, mock_neutron): + def test_update_security_group_neutron(self): self.cloud.secgroup_source = 'neutron' - neutron_return = dict(security_groups=[neutron_grp_dict]) - mock_neutron.list_security_groups.return_value = neutron_return - self.cloud.update_security_group(neutron_grp_obj.id, name='new_name') - mock_neutron.update_security_group.assert_called_once_with( - security_group=neutron_grp_dict['id'], - body={'security_group': {'name': 'new_name'}} - ) + new_name = self.getUniqueString() + sg_id = neutron_grp_obj.id + update_return = meta.obj_to_dict(neutron_grp_obj) + update_return['name'] = new_name + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'security-groups.json']), + json={'security_groups': [neutron_grp_dict]}), + dict(method='PUT', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'security-groups', '%s.json' % sg_id]), + json={'security_group': update_return}, + validate=dict(json={ + 'security_group': {'name': new_name}})) + ]) + r = self.cloud.update_security_group(neutron_grp_obj.id, name=new_name) + self.assertEqual(r['name'], new_name) + self.assert_calls() def test_update_security_group_nova(self): self.has_neutron = False @@ -228,9 +276,7 @@ def test_update_security_group_bad_kwarg(self): self.cloud.update_security_group, 'doesNotExist', bad_arg='') - @mock.patch.object(shade.OpenStackCloud, 'get_security_group') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_create_security_group_rule_neutron(self, mock_neutron, mock_get): + def test_create_security_group_rule_neutron(self): self.cloud.secgroup_source = 'neutron' args = dict( port_range_min=-1, @@ -241,17 +287,37 @@ def test_create_security_group_rule_neutron(self, mock_neutron, mock_get): direction='egress', ethertype='IPv6' ) - mock_get.return_value = {'id': 'abc'} - self.cloud.create_security_group_rule(secgroup_name_or_id='abc', - **args) - + expected_args = copy.copy(args) # For neutron, -1 port should be converted to None - args['port_range_min'] = None - args['security_group_id'] = 'abc' + expected_args['port_range_min'] = None + expected_args['security_group_id'] = neutron_grp_dict['id'] - mock_neutron.create_security_group_rule.assert_called_once_with( - body={'security_group_rule': args} - ) + expected_new_rule = copy.copy(expected_args) + expected_new_rule['id'] = '1234' + expected_new_rule['project_id'] = '' + expected_new_rule['tenant_id'] = expected_new_rule['project_id'] + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'security-groups.json']), + json={'security_groups': [neutron_grp_dict]}), + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'security-group-rules.json']), + json={'security_group_rule': expected_new_rule}, + validate=dict(json={ + 'security_group_rule': expected_args})) + ]) + new_rule = self.cloud.create_security_group_rule( + secgroup_name_or_id=neutron_grp_dict['id'], **args) + # NOTE(slaweq): don't check location and properties in new rule + new_rule.pop("location") + new_rule.pop("properties") + self.assertEqual(expected_new_rule, new_rule) + self.assert_calls() def test_create_security_group_rule_nova(self): self.has_neutron = False @@ -335,13 +401,19 @@ def test_create_security_group_rule_none(self): self.cloud.create_security_group_rule, '') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_security_group_rule_neutron(self, mock_neutron): + def test_delete_security_group_rule_neutron(self): + rule_id = "xyz" self.cloud.secgroup_source = 'neutron' - r = self.cloud.delete_security_group_rule('xyz') - mock_neutron.delete_security_group_rule.assert_called_once_with( - security_group_rule='xyz') - self.assertTrue(r) + self.register_uris([ + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'security-group-rules', + '%s.json' % rule_id]), + json={}) + ]) + self.assertTrue(self.cloud.delete_security_group_rule(rule_id)) + self.assert_calls() def test_delete_security_group_rule_nova(self): self.has_neutron = False @@ -362,15 +434,18 @@ def test_delete_security_group_rule_none(self): self.cloud.delete_security_group_rule, '') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_delete_security_group_rule_not_found(self, - mock_neutron): + def test_delete_security_group_rule_not_found(self): + rule_id = "doesNotExist" self.cloud.secgroup_source = 'neutron' - mock_neutron.delete_security_group_rule.side_effect = ( - neutron_exc.NotFound() - ) - r = self.cloud.delete_security_group('doesNotExist') - self.assertFalse(r) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'security-groups.json']), + json={'security_groups': [neutron_grp_dict]}) + ]) + self.assertFalse(self.cloud.delete_security_group(rule_id)) + self.assert_calls() def test_delete_security_group_rule_not_found_nova(self): self.has_neutron = False @@ -457,36 +532,32 @@ def test_add_security_group_to_server_nova(self): self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_add_security_group_to_server_neutron(self, - mock_neutron, - mock_nova): + def test_add_security_group_to_server_neutron(self): # fake to get server by name, server-name must match fake_server = fakes.FakeServer('1234', 'server-name', 'ACTIVE') - mock_nova.servers.list.return_value = [fake_server] # use neutron for secgroup list and return an existing fake self.cloud.secgroup_source = 'neutron' - neutron_return = dict(security_groups=[neutron_grp_dict]) - mock_neutron.list_security_groups.return_value = neutron_return self.register_uris([ - dict( - method='POST', - uri='%s/servers/%s/action' % (fakes.COMPUTE_ENDPOINT, '1234'), - validate={'addSecurityGroup': {'name': 'neutron-sec-group'}}, - status_code=202, - ), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', 'detail']), + json={'servers': [meta.obj_to_dict(fake_server)]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'security-groups.json']), + json={'security_groups': [neutron_grp_dict]}), + dict(method='POST', + uri='%s/servers/%s/action' % (fakes.COMPUTE_ENDPOINT, '1234'), + validate={'addSecurityGroup': {'name': 'neutron-sec-group'}}, + status_code=202), ]) - ret = self.cloud.add_server_security_groups('server-name', - 'neutron-sec-group') - self.assertTrue(ret) - - self.assertTrue(mock_nova.servers.list.called_once) - self.assertTrue(mock_neutron.list_securit_groups.called_once) - + self.assertTrue(self.cloud.add_server_security_groups( + 'server-name', 'neutron-sec-group')) self.assert_calls() def test_remove_security_group_from_server_nova(self): @@ -513,36 +584,32 @@ def test_remove_security_group_from_server_nova(self): self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_remove_security_group_from_server_neutron(self, - mock_neutron, - mock_nova): + def test_remove_security_group_from_server_neutron(self): # fake to get server by name, server-name must match fake_server = fakes.FakeServer('1234', 'server-name', 'ACTIVE') - mock_nova.servers.list.return_value = [fake_server] # use neutron for secgroup list and return an existing fake self.cloud.secgroup_source = 'neutron' - neutron_return = dict(security_groups=[neutron_grp_dict]) - mock_neutron.list_security_groups.return_value = neutron_return validate = {'removeSecurityGroup': {'name': 'neutron-sec-group'}} self.register_uris([ - dict( - method='POST', - uri='%s/servers/%s/action' % (fakes.COMPUTE_ENDPOINT, '1234'), - validate=validate, - ), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', 'detail']), + json={'servers': [meta.obj_to_dict(fake_server)]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'security-groups.json']), + json={'security_groups': [neutron_grp_dict]}), + dict(method='POST', + uri='%s/servers/%s/action' % (fakes.COMPUTE_ENDPOINT, '1234'), + validate=validate), ]) - ret = self.cloud.remove_server_security_groups('server-name', - 'neutron-sec-group') - self.assertTrue(ret) - - self.assertTrue(mock_nova.servers.list.called_once) - self.assertTrue(mock_neutron.list_security_groups.called_once) - + self.assertTrue(self.cloud.remove_server_security_groups( + 'server-name', 'neutron-sec-group')) self.assert_calls() def test_add_bad_security_group_to_server_nova(self): @@ -572,27 +639,27 @@ def test_add_bad_security_group_to_server_nova(self): self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - @mock.patch.object(shade.OpenStackCloud, 'neutron_client') - def test_add_bad_security_group_to_server_neutron(self, - mock_neutron, - mock_nova): + def test_add_bad_security_group_to_server_neutron(self): # fake to get server by name, server-name must match fake_server = fakes.FakeServer('1234', 'server-name', 'ACTIVE') - mock_nova.servers.list.return_value = [fake_server] # use neutron for secgroup list and return an existing fake self.cloud.secgroup_source = 'neutron' - neutron_return = dict(security_groups=[neutron_grp_dict]) - mock_neutron.list_security_groups.return_value = neutron_return - - ret = self.cloud.add_server_security_groups('server-name', - 'unknown-sec-group') - self.assertFalse(ret) - - self.assertTrue(mock_nova.servers.list.called_once) - self.assertTrue(mock_neutron.list_security_groups.called_once) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', 'detail']), + json={'servers': [meta.obj_to_dict(fake_server)]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'security-groups.json']), + json={'security_groups': [neutron_grp_dict]}) + ]) + self.assertFalse(self.cloud.add_server_security_groups( + 'server-name', 'unknown-sec-group')) self.assert_calls() def test_add_security_group_to_bad_server(self): From 17ab75b0a3350bcc87673d11cce12551fcec2a77 Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Tue, 21 Mar 2017 16:58:29 -0700 Subject: [PATCH 1479/3836] Convert test_role_assignments to requests mock Stop mocking keystoneclient objects directly. Change-Id: Ibb441ba7a3de50201e96310b9aca5bea628fae01 --- shade/operatorcloud.py | 3 +- shade/tests/unit/base.py | 30 +- shade/tests/unit/test_role_assignment.py | 3478 +++++++++++++++++----- 3 files changed, 2786 insertions(+), 725 deletions(-) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index c1f7db730..e2821fa0b 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1795,8 +1795,7 @@ def revoke_role(self, name_or_id, user=None, group=None, data['tenant'] = data.pop('project') self.manager.submit_task(_tasks.RoleRemoveUser(**data)) else: - if data.get('project') is None \ - and data.get('domain') is None: + if data.get('project') is None and data.get('domain') is None: raise OpenStackCloudException( 'Must specify either a domain or project') self.manager.submit_task(_tasks.RoleRevokeUser(**data)) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 9f3913098..fa7ddf21f 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -24,6 +24,7 @@ import os_client_config as occ from requests import structures from requests_mock.contrib import fixture as rm_fixture +from six.moves import urllib import tempfile import shade.openstackcloud @@ -244,9 +245,10 @@ def mock_for_keystone_projects(self, project=None, v3=True, return project_list def _get_project_data(self, project_name=None, enabled=None, - domain_id=None, description=None, v3=True): + domain_id=None, description=None, v3=True, + project_id=None): project_name = project_name or self.getUniqueString('projectName') - project_id = uuid.uuid4().hex + project_id = uuid.UUID(project_id or uuid.uuid4().hex).hex response = {'id': project_id, 'name': project_name} request = {'name': project_name} domain_id = (domain_id or uuid.uuid4().hex) if v3 else None @@ -271,7 +273,7 @@ def _get_project_data(self, project_name=None, enabled=None, def _get_group_data(self, name=None, domain_id=None, description=None): group_id = uuid.uuid4().hex - name or self.getUniqueString('groupname') + name = name or self.getUniqueString('groupname') domain_id = uuid.UUID(domain_id or uuid.uuid4().hex).hex response = {'id': group_id, 'name': name, 'domain_id': domain_id} request = {'name': name} @@ -570,9 +572,27 @@ def assert_calls(self, stop_after=None, do_count=True): if stop_after and x > stop_after: break + call_uri_parts = urllib.parse.urlparse(call['url']) + history_uri_parts = urllib.parse.urlparse(history.url) self.assertEqual( - (call['method'], call['url']), (history.method, history.url), - 'REST mismatch on call {index}'.format(index=x)) + (call['method'], call_uri_parts.scheme, call_uri_parts.netloc, + call_uri_parts.path, call_uri_parts.params, + urllib.parse.parse_qs(call_uri_parts.query)), + (history.method, history_uri_parts.scheme, + history_uri_parts.netloc, history_uri_parts.path, + history_uri_parts.params, + urllib.parse.parse_qs(history_uri_parts.query)), + ('REST mismatch on call %(index)d. Expected %(call)r. ' + 'Got %(history)r). ' + 'NOTE: query string order differences wont cause mismatch' % + { + 'index': x, + 'call': '{method} {url}'.format(method=call['method'], + url=call['url']), + 'history': '{method} {url}'.format( + method=history.method, + url=history.url)}) + ) if 'json' in call: self.assertEqual( call['json'], history.json(), diff --git a/shade/tests/unit/test_role_assignment.py b/shade/tests/unit/test_role_assignment.py index 14acda7c3..324920467 100644 --- a/shade/tests/unit/test_role_assignment.py +++ b/shade/tests/unit/test_role_assignment.py @@ -11,877 +11,2919 @@ # See the License for the specific language governing permissions and # limitations under the License. -from mock import patch -import os_client_config as occ -from shade import OperatorCloud from shade import exc -from shade.meta import obj_to_dict -from shade.tests import fakes from shade.tests.unit import base import testtools +from testtools import matchers -class TestRoleAssignment(base.TestCase): - - def setUp(self): - super(TestRoleAssignment, self).setUp() - self.fake_role = obj_to_dict(fakes.FakeRole('12345', 'test')) - self.fake_user = obj_to_dict(fakes.FakeUser('12345', - 'test@nobody.org', - 'test', - domain_id='test-domain')) - self.fake_group = obj_to_dict(fakes.FakeGroup('12345', - 'test', - 'test group', - domain_id='test-domain')) - self.fake_project = obj_to_dict( - fakes.FakeProject('12345', domain_id='test-domain')) - self.fake_domain = obj_to_dict(fakes.FakeDomain('test-domain', - 'test', - 'test domain', - enabled=True)) - self.user_project_assignment = obj_to_dict({ - 'role': { - 'id': self.fake_role['id'] - }, - 'scope': { - 'project': { - 'id': self.fake_project['id'] - } - }, - 'user': { - 'id': self.fake_user['id'] - } - }) - self.group_project_assignment = obj_to_dict({ - 'role': { - 'id': self.fake_role['id'] - }, - 'scope': { - 'project': { - 'id': self.fake_project['id'] - } - }, - 'group': { - 'id': self.fake_group['id'] - } - }) - self.user_domain_assignment = obj_to_dict({ - 'role': { - 'id': self.fake_role['id'] - }, - 'scope': { - 'domain': { - 'id': self.fake_domain['id'] - } - }, - 'user': { - 'id': self.fake_user['id'] - } - }) - self.group_domain_assignment = obj_to_dict({ - 'role': { - 'id': self.fake_role['id'] - }, - 'scope': { - 'domain': { - 'id': self.fake_domain['id'] - } - }, - 'group': { - 'id': self.fake_group['id'] - } - }) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_grant_role_user_v2(self, mock_keystone, mock_api_version): - mock_api_version.return_value = '2.0' - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.users.list.return_value = [self.fake_user] - mock_keystone.tenants.list.return_value = [self.fake_project] - mock_keystone.roles.roles_for_user.return_value = [] - mock_keystone.roles.add_user_role.return_value = self.fake_role +class TestRoleAssignment(base.RequestsMockTestCase): + + def _build_role_assignment_response(self, role_id, scope_type, scope_id, + entity_type, entity_id): + self.assertThat(['group', 'user'], matchers.Contains(entity_type)) + self.assertThat(['project', 'domain'], matchers.Contains(scope_type)) + # NOTE(notmorgan): Links are thrown out by shade, but we construct them + # for corectness. + link_str = ('https://identity.example.com/identity/v3/{scope_t}s' + '/{scopeid}/{entity_t}s/{entityid}/roles/{roleid}') + return [{ + 'links': {'assignment': link_str.format( + scope_t=scope_type, scopeid=scope_id, entity_t=entity_type, + entityid=entity_id, roleid=role_id)}, + 'role': {'id': role_id}, + 'scope': {scope_type: {'id': scope_id}}, + entity_type: {'id': entity_id} + }] + + def setUp(self, cloud_config_fixture='clouds.yaml'): + super(TestRoleAssignment, self).setUp(cloud_config_fixture) + self.role_data = self._get_role_data() + self.domain_data = self._get_domain_data() + self.user_data = self._get_user_data( + domain_id=self.domain_data.domain_id) + self.project_data = self._get_project_data( + domain_id=self.domain_data.domain_id) + self.project_data_v2 = self._get_project_data( + project_name=self.project_data.project_name, + project_id=self.project_data.project_id, + v3=False) + self.group_data = self._get_group_data( + domain_id=self.domain_data.domain_id) + + self.user_project_assignment = self._build_role_assignment_response( + role_id=self.role_data.role_id, scope_type='project', + scope_id=self.project_data.project_id, entity_type='user', + entity_id=self.user_data.user_id) + + self.group_project_assignment = self._build_role_assignment_response( + role_id=self.role_data.role_id, scope_type='project', + scope_id=self.project_data.project_id, entity_type='group', + entity_id=self.group_data.group_id) + + self.user_domain_assignment = self._build_role_assignment_response( + role_id=self.role_data.role_id, scope_type='domain', + scope_id=self.domain_data.domain_id, entity_type='user', + entity_id=self.user_data.user_id) + + self.group_domain_assignment = self._build_role_assignment_response( + role_id=self.role_data.role_id, scope_type='domain', + scope_id=self.domain_data.domain_id, entity_type='group', + entity_id=self.group_data.group_id) + + # Cleanup of instances to ensure garbage collection/no leaking memory + # in tests. + self.addCleanup(delattr, self, 'role_data') + self.addCleanup(delattr, self, 'user_data') + self.addCleanup(delattr, self, 'domain_data') + self.addCleanup(delattr, self, 'group_data') + self.addCleanup(delattr, self, 'project_data') + self.addCleanup(delattr, self, 'project_data_v2') + self.addCleanup(delattr, self, 'user_project_assignment') + self.addCleanup(delattr, self, 'group_project_assignment') + self.addCleanup(delattr, self, 'user_domain_assignment') + self.addCleanup(delattr, self, 'group_domain_assignment') + + def get_mock_url(self, service_type='identity', interface='admin', + resource='role_assignments', append=None, + base_url_append='v3', qs_elements=None): + return super(TestRoleAssignment, self).get_mock_url( + service_type, interface, resource, append, base_url_append, + qs_elements) + + def test_grant_role_user_v2(self): + self.use_keystone_v2() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(base_url_append='OS-KSADM', + resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants'), + status_code=200, + json={'tenants': [ + self.project_data_v2.json_response['tenant']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', + self.user_data.user_id, + 'roles']), + status_code=200, + json={'roles': []}), + dict(method='PUT', + status_code=201, + uri=self.get_mock_url( + base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', self.user_data.user_id, 'roles', + 'OS-KSADM', self.role_data.role_id])), + dict(method='GET', + uri=self.get_mock_url(base_url_append='OS-KSADM', + resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants'), + status_code=200, + json={'tenants': [ + self.project_data_v2.json_response['tenant']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', + self.user_data.user_id, + 'roles']), + status_code=200, + json={'roles': []}), + dict(method='PUT', + uri=self.get_mock_url( + base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', self.user_data.user_id, 'roles', + 'OS-KSADM', self.role_data.role_id]), + status_code=201) + ]) self.assertTrue( self.op_cloud.grant_role( - self.fake_role['name'], - user=self.fake_user['name'], - project=self.fake_project['id'])) + self.role_data.role_name, + user=self.user_data.name, + project=self.project_data.project_id)) self.assertTrue( self.op_cloud.grant_role( - self.fake_role['name'], - user=self.fake_user['id'], - project=self.fake_project['id'])) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_grant_role_user_project_v2(self, mock_keystone, mock_api_version): - mock_api_version.return_value = '2.0' - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.tenants.list.return_value = [self.fake_project] - mock_keystone.users.list.return_value = [self.fake_user] - mock_keystone.roles.roles_for_user.return_value = [] - mock_keystone.roles.add_user_role.return_value = self.fake_role + self.role_data.role_name, + user=self.user_data.user_id, + project=self.project_data.project_id)) + self.assert_calls() + + def test_grant_role_user_project_v2(self): + self.use_keystone_v2() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(base_url_append='OS-KSADM', + resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants'), + status_code=200, + json={'tenants': [ + self.project_data_v2.json_response['tenant']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', + self.user_data.user_id, + 'roles']), + status_code=200, + json={'roles': []}), + dict(method='PUT', + uri=self.get_mock_url( + base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', self.user_data.user_id, 'roles', + 'OS-KSADM', self.role_data.role_id]), + status_code=201), + dict(method='GET', + uri=self.get_mock_url(base_url_append='OS-KSADM', + resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants'), + status_code=200, + json={'tenants': [ + self.project_data_v2.json_response['tenant']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', + self.user_data.user_id, + 'roles']), + status_code=200, + json={'roles': []}), + dict(method='PUT', + uri=self.get_mock_url( + base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', self.user_data.user_id, 'roles', + 'OS-KSADM', self.role_data.role_id]), + status_code=201), + dict(method='GET', + uri=self.get_mock_url(base_url_append='OS-KSADM', + resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants'), + status_code=200, + json={'tenants': [ + self.project_data_v2.json_response['tenant']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', + self.user_data.user_id, + 'roles']), + status_code=200, + json={'roles': []}), + dict(method='PUT', + uri=self.get_mock_url( + base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', self.user_data.user_id, 'roles', + 'OS-KSADM', self.role_data.role_id]), + status_code=201, + ), + dict(method='GET', + uri=self.get_mock_url(base_url_append='OS-KSADM', + resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants'), + status_code=200, + json={'tenants': [ + self.project_data_v2.json_response['tenant']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', + self.user_data.user_id, + 'roles']), + status_code=200, + json={'roles': []}), + dict(method='PUT', + uri=self.get_mock_url( + base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', self.user_data.user_id, 'roles', + 'OS-KSADM', self.role_data.role_id]), + status_code=201) + ]) self.assertTrue( self.op_cloud.grant_role( - self.fake_role['name'], - user=self.fake_user['name'], - project=self.fake_project['id'])) + self.role_data.role_name, + user=self.user_data.name, + project=self.project_data_v2.project_id)) self.assertTrue( self.op_cloud.grant_role( - self.fake_role['name'], - user=self.fake_user['id'], - project=self.fake_project['id'])) + self.role_data.role_name, + user=self.user_data.user_id, + project=self.project_data_v2.project_id)) self.assertTrue( self.op_cloud.grant_role( - self.fake_role['id'], - user=self.fake_user['name'], - project=self.fake_project['id'])) + self.role_data.role_id, + user=self.user_data.name, + project=self.project_data_v2.project_id)) self.assertTrue( self.op_cloud.grant_role( - self.fake_role['id'], - user=self.fake_user['id'], - project=self.fake_project['id'])) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_grant_role_user_project_v2_exists(self, - mock_keystone, - mock_api_version): - mock_api_version.return_value = '2.0' - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.tenants.list.return_value = [self.fake_project] - mock_keystone.users.list.return_value = [self.fake_user] - mock_keystone.roles.roles_for_user.return_value = [self.fake_role] + self.role_data.role_id, + user=self.user_data.user_id, + project=self.project_data_v2.project_id)) + self.assert_calls() + + def test_grant_role_user_project_v2_exists(self): + self.use_keystone_v2() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(base_url_append='OS-KSADM', + resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants'), + status_code=200, + json={'tenants': [ + self.project_data_v2.json_response['tenant']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', + self.user_data.user_id, + 'roles']), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + ]) self.assertFalse(self.op_cloud.grant_role( - self.fake_role['name'], - user=self.fake_user['name'], - project=self.fake_project['id'])) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_grant_role_user_project(self, mock_keystone, mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.projects.list.return_value = [self.fake_project] - mock_keystone.users.list.return_value = [self.fake_user] - mock_keystone.role_assignments.list.return_value = [] + self.role_data.role_name, + user=self.user_data.name, + project=self.project_data_v2.project_id)) + self.assert_calls() + + def test_grant_role_user_project(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url(resource='projects'), + status_code=200, + json={'projects': [ + self.project_data.json_response['project']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'user.id=%s' % self.user_data.user_id, + 'scope.project.id=%s' % self.project_data.project_id, + 'role.id=%s' % self.role_data.role_id]), + complete_qs=True, + status_code=200, + json={'role_assignments': []}), + dict(method='PUT', + uri=self.get_mock_url( + resource='projects', + append=[self.project_data.project_id, 'users', + self.user_data.user_id, 'roles', + self.role_data.role_id]), + status_code=204), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url(resource='projects'), + status_code=200, + json={'projects': [ + self.project_data.json_response['project']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'user.id=%s' % self.user_data.user_id, + 'scope.project.id=%s' % self.project_data.project_id, + 'role.id=%s' % self.role_data.role_id]), + complete_qs=True, + status_code=200, + json={'role_assignments': []}), + dict(method='PUT', + uri=self.get_mock_url( + resource='projects', + append=[self.project_data.project_id, 'users', + self.user_data.user_id, 'roles', + self.role_data.role_id]), + status_code=204), + ]) self.assertTrue( self.op_cloud.grant_role( - self.fake_role['name'], - user=self.fake_user['name'], - project=self.fake_project['id'])) + self.role_data.role_name, + user=self.user_data.name, + project=self.project_data.project_id)) self.assertTrue( self.op_cloud.grant_role( - self.fake_role['name'], - user=self.fake_user['id'], - project=self.fake_project['id'])) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_grant_role_user_project_exists(self, - mock_keystone, - mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.projects.list.return_value = [self.fake_project] - mock_keystone.users.list.return_value = [self.fake_user] - mock_keystone.role_assignments.list.return_value = \ - [self.user_project_assignment] + self.role_data.role_name, + user=self.user_data.user_id, + project=self.project_data.project_id)) + self.assert_calls() + + def test_grant_role_user_project_exists(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url(resource='projects'), + status_code=200, + json={'projects': [ + self.project_data.json_response['project']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'user.id=%s' % self.user_data.user_id, + 'scope.project.id=%s' % self.project_data.project_id, + 'role.id=%s' % self.role_data.role_id]), + complete_qs=True, + status_code=200, + json={ + 'role_assignments': self._build_role_assignment_response( + role_id=self.role_data.role_id, + scope_type='project', + scope_id=self.project_data.project_id, + entity_type='user', + entity_id=self.user_data.user_id)}), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url(resource='projects'), + status_code=200, + json={'projects': [ + self.project_data.json_response['project']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'user.id=%s' % self.user_data.user_id, + 'scope.project.id=%s' % self.project_data.project_id, + 'role.id=%s' % self.role_data.role_id]), + complete_qs=True, + status_code=200, + json={ + 'role_assignments': self._build_role_assignment_response( + role_id=self.role_data.role_id, + scope_type='project', + scope_id=self.project_data.project_id, + entity_type='user', + entity_id=self.user_data.user_id)}), + ]) self.assertFalse(self.op_cloud.grant_role( - self.fake_role['name'], - user=self.fake_user['name'], - project=self.fake_project['id'])) + self.role_data.role_name, + user=self.user_data.name, + project=self.project_data.project_id)) self.assertFalse(self.op_cloud.grant_role( - self.fake_role['id'], - user=self.fake_user['id'], - project=self.fake_project['id'])) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_grant_role_group_project(self, mock_keystone, mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.projects.list.return_value = [self.fake_project] - mock_keystone.groups.list.return_value = [self.fake_group] - mock_keystone.role_assignments.list.return_value = [] + self.role_data.role_id, + user=self.user_data.user_id, + project=self.project_data.project_id)) + self.assert_calls() + + def test_grant_role_group_project(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='projects'), + status_code=200, + json={'projects': [ + self.project_data.json_response['project']]}), + dict(method='GET', + uri=self.get_mock_url(resource='groups'), + status_code=200, + json={'groups': [self.group_data.json_response['group']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'group.id=%s' % self.group_data.group_id, + 'scope.project.id=%s' % self.project_data.project_id, + 'role.id=%s' % self.role_data.role_id]), + complete_qs=True, + status_code=200, + json={'role_assignments': []}), + dict(method='PUT', + uri=self.get_mock_url( + resource='projects', + append=[self.project_data.project_id, 'groups', + self.group_data.group_id, 'roles', + self.role_data.role_id]), + status_code=204), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='projects'), + status_code=200, + json={'projects': [ + self.project_data.json_response['project']]}), + dict(method='GET', + uri=self.get_mock_url(resource='groups'), + status_code=200, + json={'groups': [self.group_data.json_response['group']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'group.id=%s' % self.group_data.group_id, + 'scope.project.id=%s' % self.project_data.project_id, + 'role.id=%s' % self.role_data.role_id]), + complete_qs=True, + status_code=200, + json={'role_assignments': []}), + dict(method='PUT', + uri=self.get_mock_url( + resource='projects', + append=[self.project_data.project_id, 'groups', + self.group_data.group_id, 'roles', + self.role_data.role_id]), + status_code=204), + ]) self.assertTrue(self.op_cloud.grant_role( - self.fake_role['name'], - group=self.fake_group['name'], - project=self.fake_project['id'])) + self.role_data.role_name, + group=self.group_data.group_name, + project=self.project_data.project_id)) self.assertTrue(self.op_cloud.grant_role( - self.fake_role['name'], - group=self.fake_group['id'], - project=self.fake_project['id'])) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_grant_role_group_project_exists(self, - mock_keystone, - mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.projects.list.return_value = [self.fake_project] - mock_keystone.groups.list.return_value = [self.fake_group] - mock_keystone.role_assignments.list.return_value = \ - [self.group_project_assignment] + self.role_data.role_name, + group=self.group_data.group_id, + project=self.project_data.project_id)) + self.assert_calls() + + def test_grant_role_group_project_exists(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='projects'), + status_code=200, + json={'projects': [ + self.project_data.json_response['project']]}), + dict(method='GET', + uri=self.get_mock_url(resource='groups'), + status_code=200, + json={'groups': [self.group_data.json_response['group']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'group.id=%s' % self.group_data.group_id, + 'scope.project.id=%s' % self.project_data.project_id, + 'role.id=%s' % self.role_data.role_id]), + complete_qs=True, + status_code=200, + json={ + 'role_assignments': self._build_role_assignment_response( + role_id=self.role_data.role_id, + scope_type='project', + scope_id=self.project_data.project_id, + entity_type='group', + entity_id=self.group_data.group_id)}), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='projects'), + status_code=200, + json={'projects': [ + self.project_data.json_response['project']]}), + dict(method='GET', + uri=self.get_mock_url(resource='groups'), + status_code=200, + json={'groups': [self.group_data.json_response['group']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'group.id=%s' % self.group_data.group_id, + 'scope.project.id=%s' % self.project_data.project_id, + 'role.id=%s' % self.role_data.role_id]), + complete_qs=True, + status_code=200, + json={ + 'role_assignments': self._build_role_assignment_response( + role_id=self.role_data.role_id, + scope_type='project', + scope_id=self.project_data.project_id, + entity_type='group', + entity_id=self.group_data.group_id)}), + ]) self.assertFalse(self.op_cloud.grant_role( - self.fake_role['name'], - group=self.fake_group['name'], - project=self.fake_project['id'])) + self.role_data.role_name, + group=self.group_data.group_name, + project=self.project_data.project_id)) self.assertFalse(self.op_cloud.grant_role( - self.fake_role['name'], - group=self.fake_group['id'], - project=self.fake_project['id'])) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_grant_role_user_domain(self, mock_keystone, mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.users.list.return_value = [self.fake_user] - mock_keystone.domains.get.return_value = self.fake_domain - mock_keystone.role_assignments.list.return_value = [] + self.role_data.role_name, + group=self.group_data.group_id, + project=self.project_data.project_id)) + self.assert_calls() + + def test_grant_role_user_domain(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'role.id=%s' % self.role_data.role_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'user.id=%s' % self.user_data.user_id]), + complete_qs=True, + status_code=200, + json={'role_assignments': []}), + dict(method='PUT', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id]), + status_code=204), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'role.id=%s' % self.role_data.role_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'user.id=%s' % self.user_data.user_id]), + complete_qs=True, + status_code=200, + json={'role_assignments': []}), + dict(method='PUT', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id]), + status_code=204), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_name]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'role.id=%s' % self.role_data.role_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'user.id=%s' % self.user_data.user_id]), + complete_qs=True, + status_code=200, + json={'role_assignments': []}), + dict(method='PUT', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id]), + status_code=204), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_name]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'role.id=%s' % self.role_data.role_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'user.id=%s' % self.user_data.user_id]), + complete_qs=True, + status_code=200, + json={'role_assignments': []}), + dict(method='PUT', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id]), + status_code=204), + ]) self.assertTrue(self.op_cloud.grant_role( - self.fake_role['name'], - user=self.fake_user['name'], - domain=self.fake_domain['id'])) + self.role_data.role_name, + user=self.user_data.name, + domain=self.domain_data.domain_id)) self.assertTrue(self.op_cloud.grant_role( - self.fake_role['name'], - user=self.fake_user['id'], - domain=self.fake_domain['id'])) + self.role_data.role_name, + user=self.user_data.user_id, + domain=self.domain_data.domain_id)) self.assertTrue(self.op_cloud.grant_role( - self.fake_role['name'], - user=self.fake_user['name'], - domain=self.fake_domain['name'])) + self.role_data.role_name, + user=self.user_data.name, + domain=self.domain_data.domain_name)) self.assertTrue(self.op_cloud.grant_role( - self.fake_role['name'], - user=self.fake_user['id'], - domain=self.fake_domain['name'])) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_grant_role_user_domain_exists(self, - mock_keystone, - mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.users.list.return_value = [self.fake_user] - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.domains.get.return_value = self.fake_domain - mock_keystone.role_assignments.list.return_value = \ - [self.user_domain_assignment] + self.role_data.role_name, + user=self.user_data.user_id, + domain=self.domain_data.domain_name)) + self.assert_calls() + + def test_grant_role_user_domain_exists(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'role.id=%s' % self.role_data.role_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'user.id=%s' % self.user_data.user_id]), + complete_qs=True, + status_code=200, + json={ + 'role_assignments': self._build_role_assignment_response( + role_id=self.role_data.role_id, + scope_type='domain', + scope_id=self.domain_data.domain_id, + entity_type='user', + entity_id=self.user_data.user_id)}), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'role.id=%s' % self.role_data.role_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'user.id=%s' % self.user_data.user_id]), + complete_qs=True, + status_code=200, + json={ + 'role_assignments': self._build_role_assignment_response( + role_id=self.role_data.role_id, + scope_type='domain', + scope_id=self.domain_data.domain_id, + entity_type='user', + entity_id=self.user_data.user_id)}), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_name]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'role.id=%s' % self.role_data.role_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'user.id=%s' % self.user_data.user_id]), + complete_qs=True, + status_code=200, + json={ + 'role_assignments': self._build_role_assignment_response( + role_id=self.role_data.role_id, + scope_type='domain', + scope_id=self.domain_data.domain_id, + entity_type='user', + entity_id=self.user_data.user_id)}), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_name]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'role.id=%s' % self.role_data.role_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'user.id=%s' % self.user_data.user_id]), + complete_qs=True, + status_code=200, + json={ + 'role_assignments': self._build_role_assignment_response( + role_id=self.role_data.role_id, + scope_type='domain', + scope_id=self.domain_data.domain_id, + entity_type='user', + entity_id=self.user_data.user_id)}), + ]) self.assertFalse(self.op_cloud.grant_role( - self.fake_role['name'], - user=self.fake_user['name'], - domain=self.fake_domain['name'])) + self.role_data.role_name, + user=self.user_data.name, + domain=self.domain_data.domain_id)) self.assertFalse(self.op_cloud.grant_role( - self.fake_role['name'], - user=self.fake_user['id'], - domain=self.fake_domain['name'])) + self.role_data.role_name, + user=self.user_data.user_id, + domain=self.domain_data.domain_id)) self.assertFalse(self.op_cloud.grant_role( - self.fake_role['name'], - user=self.fake_user['name'], - domain=self.fake_domain['id'])) + self.role_data.role_name, + user=self.user_data.name, + domain=self.domain_data.domain_name)) self.assertFalse(self.op_cloud.grant_role( - self.fake_role['name'], - user=self.fake_user['id'], - domain=self.fake_domain['id'])) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_grant_role_group_domain(self, mock_keystone, mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.groups.list.return_value = [self.fake_group] - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.domains.get.return_value = self.fake_domain - mock_keystone.role_assignments.list.return_value = [] + self.role_data.role_name, + user=self.user_data.user_id, + domain=self.domain_data.domain_name)) + self.assert_calls() + + def test_grant_role_group_domain(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='groups'), + status_code=200, + json={'groups': [self.group_data.json_response['group']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'role.id=%s' % self.role_data.role_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'group.id=%s' % self.group_data.group_id]), + complete_qs=True, + status_code=200, + json={'role_assignments': []}), + dict(method='PUT', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id]), + status_code=204), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='groups'), + status_code=200, + json={'groups': [self.group_data.json_response['group']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'role.id=%s' % self.role_data.role_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'group.id=%s' % self.group_data.group_id]), + complete_qs=True, + status_code=200, + json={'role_assignments': []}), + dict(method='PUT', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id]), + status_code=204), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_name]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='groups'), + status_code=200, + json={'groups': [self.group_data.json_response['group']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'role.id=%s' % self.role_data.role_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'group.id=%s' % self.group_data.group_id]), + complete_qs=True, + status_code=200, + json={'role_assignments': []}), + dict(method='PUT', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id]), + status_code=204), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_name]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='groups'), + status_code=200, + json={'groups': [self.group_data.json_response['group']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'role.id=%s' % self.role_data.role_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'group.id=%s' % self.group_data.group_id]), + complete_qs=True, + status_code=200, + json={'role_assignments': []}), + dict(method='PUT', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id]), + status_code=204), + ]) self.assertTrue(self.op_cloud.grant_role( - self.fake_role['name'], - group=self.fake_group['name'], - domain=self.fake_domain['name'])) + self.role_data.role_name, + group=self.group_data.group_name, + domain=self.domain_data.domain_id)) self.assertTrue(self.op_cloud.grant_role( - self.fake_role['name'], - group=self.fake_group['id'], - domain=self.fake_domain['name'])) + self.role_data.role_name, + group=self.group_data.group_id, + domain=self.domain_data.domain_id)) self.assertTrue(self.op_cloud.grant_role( - self.fake_role['name'], - group=self.fake_group['name'], - domain=self.fake_domain['id'])) + self.role_data.role_name, + group=self.group_data.group_name, + domain=self.domain_data.domain_name)) self.assertTrue(self.op_cloud.grant_role( - self.fake_role['name'], - group=self.fake_group['id'], - domain=self.fake_domain['id'])) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_grant_role_group_domain_exists(self, - mock_keystone, - mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.groups.list.return_value = [self.fake_group] - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.domains.get.return_value = self.fake_domain - mock_keystone.role_assignments.list.return_value = \ - [self.group_domain_assignment] + self.role_data.role_name, + group=self.group_data.group_id, + domain=self.domain_data.domain_name)) + self.assert_calls() + + def test_grant_role_group_domain_exists(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='groups'), + status_code=200, + json={'groups': [self.group_data.json_response['group']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'role.id=%s' % self.role_data.role_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'group.id=%s' % self.group_data.group_id]), + complete_qs=True, + status_code=200, + json={ + 'role_assignments': self._build_role_assignment_response( + role_id=self.role_data.role_id, + scope_type='domain', + scope_id=self.domain_data.domain_id, + entity_type='group', + entity_id=self.group_data.group_id)}), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='groups'), + status_code=200, + json={'groups': [self.group_data.json_response['group']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'role.id=%s' % self.role_data.role_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'group.id=%s' % self.group_data.group_id]), + complete_qs=True, + status_code=200, + json={ + 'role_assignments': self._build_role_assignment_response( + role_id=self.role_data.role_id, + scope_type='domain', + scope_id=self.domain_data.domain_id, + entity_type='group', + entity_id=self.group_data.group_id)}), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_name]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='groups'), + status_code=200, + json={'groups': [self.group_data.json_response['group']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'role.id=%s' % self.role_data.role_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'group.id=%s' % self.group_data.group_id]), + complete_qs=True, + status_code=200, + json={ + 'role_assignments': self._build_role_assignment_response( + role_id=self.role_data.role_id, + scope_type='domain', + scope_id=self.domain_data.domain_id, + entity_type='group', + entity_id=self.group_data.group_id)}), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_name]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='groups'), + status_code=200, + json={'groups': [self.group_data.json_response['group']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'role.id=%s' % self.role_data.role_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'group.id=%s' % self.group_data.group_id]), + complete_qs=True, + status_code=200, + json={ + 'role_assignments': self._build_role_assignment_response( + role_id=self.role_data.role_id, + scope_type='domain', + scope_id=self.domain_data.domain_id, + entity_type='group', + entity_id=self.group_data.group_id)}), + ]) self.assertFalse(self.op_cloud.grant_role( - self.fake_role['name'], - group=self.fake_group['name'], - domain=self.fake_domain['name'])) + self.role_data.role_name, + group=self.group_data.group_name, + domain=self.domain_data.domain_id)) self.assertFalse(self.op_cloud.grant_role( - self.fake_role['name'], - group=self.fake_group['id'], - domain=self.fake_domain['name'])) + self.role_data.role_name, + group=self.group_data.group_id, + domain=self.domain_data.domain_id)) self.assertFalse(self.op_cloud.grant_role( - self.fake_role['name'], - group=self.fake_group['name'], - domain=self.fake_domain['id'])) + self.role_data.role_name, + group=self.group_data.group_name, + domain=self.domain_data.domain_name)) self.assertFalse(self.op_cloud.grant_role( - self.fake_role['name'], - group=self.fake_group['id'], - domain=self.fake_domain['id'])) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_revoke_role_user_v2(self, mock_keystone, mock_api_version): - mock_api_version.return_value = '2.0' - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.users.list.return_value = [self.fake_user] - mock_keystone.tenants.list.return_value = [self.fake_project] - mock_keystone.roles.roles_for_user.return_value = [self.fake_role] - mock_keystone.roles.remove_user_role.return_value = self.fake_role + self.role_data.role_name, + group=self.group_data.group_id, + domain=self.domain_data.domain_name)) + self.assert_calls() + + def test_revoke_role_user_v2(self): + self.use_keystone_v2() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + base_url_append='OS-KSADM', + resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url( + base_url_append=None, + resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url( + base_url_append=None, + resource='tenants'), + status_code=200, + json={ + 'tenants': [ + self.project_data_v2.json_response['tenant']]}), + dict(method='GET', + uri=self.get_mock_url( + base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, 'users', + self.user_data.user_id, 'roles']), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='DELETE', + uri=self.get_mock_url(base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', + self.user_data.user_id, + 'roles', 'OS-KSADM', + self.role_data.role_id]), + status_code=204), + dict(method='GET', + uri=self.get_mock_url( + base_url_append='OS-KSADM', + resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url( + base_url_append=None, + resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url( + base_url_append=None, + resource='tenants'), + status_code=200, + json={ + 'tenants': [ + self.project_data_v2.json_response['tenant']]}), + dict(method='GET', + uri=self.get_mock_url( + base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, 'users', + self.user_data.user_id, 'roles']), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='DELETE', + uri=self.get_mock_url(base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', + self.user_data.user_id, + 'roles', 'OS-KSADM', + self.role_data.role_id]), + status_code=204), + ]) self.assertTrue(self.op_cloud.revoke_role( - self.fake_role['name'], - user=self.fake_user['name'], - project=self.fake_project['id'])) + self.role_data.role_name, + user=self.user_data.name, + project=self.project_data.project_id)) self.assertTrue(self.op_cloud.revoke_role( - self.fake_role['name'], - user=self.fake_user['id'], - project=self.fake_project['id'])) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_revoke_role_user_project_v2(self, - mock_keystone, - mock_api_version): - mock_api_version.return_value = '2.0' - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.tenants.list.return_value = [self.fake_project] - mock_keystone.users.list.return_value = [self.fake_user] - mock_keystone.roles.roles_for_user.return_value = [] + self.role_data.role_name, + user=self.user_data.user_id, + project=self.project_data.project_id)) + self.assert_calls() + + def test_revoke_role_user_project_v2(self): + self.use_keystone_v2() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(base_url_append='OS-KSADM', + resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants'), + status_code=200, + json={ + 'tenants': [ + self.project_data_v2.json_response['tenant']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', + self.user_data.user_id, + 'roles']), + status_code=200, + json={'roles': []}), + dict(method='GET', + uri=self.get_mock_url(base_url_append='OS-KSADM', + resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants'), + status_code=200, + json={ + 'tenants': [ + self.project_data_v2.json_response['tenant']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', + self.user_data.user_id, + 'roles']), + status_code=200, + json={'roles': []}), + dict(method='GET', + uri=self.get_mock_url(base_url_append='OS-KSADM', + resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants'), + status_code=200, + json={ + 'tenants': [ + self.project_data_v2.json_response['tenant']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', + self.user_data.user_id, + 'roles']), + status_code=200, + json={'roles': []}), + dict(method='GET', + uri=self.get_mock_url(base_url_append='OS-KSADM', + resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants'), + status_code=200, + json={ + 'tenants': [ + self.project_data_v2.json_response['tenant']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', + self.user_data.user_id, + 'roles']), + status_code=200, + json={'roles': []}) + ]) self.assertFalse(self.op_cloud.revoke_role( - self.fake_role['name'], - user=self.fake_user['name'], - project=self.fake_project['id'])) + self.role_data.role_name, + user=self.user_data.name, + project=self.project_data.project_id)) self.assertFalse(self.op_cloud.revoke_role( - self.fake_role['name'], - user=self.fake_user['id'], - project=self.fake_project['id'])) + self.role_data.role_name, + user=self.user_data.user_id, + project=self.project_data.project_id)) self.assertFalse(self.op_cloud.revoke_role( - self.fake_role['id'], - user=self.fake_user['name'], - project=self.fake_project['id'])) + self.role_data.role_id, + user=self.user_data.name, + project=self.project_data.project_id)) self.assertFalse(self.op_cloud.revoke_role( - self.fake_role['id'], - user=self.fake_user['id'], - project=self.fake_project['id'])) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_revoke_role_user_project_v2_exists(self, - mock_keystone, - mock_api_version): - mock_api_version.return_value = '2.0' - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.tenants.list.return_value = [self.fake_project] - mock_keystone.users.list.return_value = [self.fake_user] - mock_keystone.roles.roles_for_user.return_value = [self.fake_role] - mock_keystone.roles.remove_user_role.return_value = self.fake_role + self.role_data.role_id, + user=self.user_data.user_id, + project=self.project_data.project_id)) + self.assert_calls() + + def test_revoke_role_user_project_v2_exists(self): + self.use_keystone_v2() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(base_url_append='OS-KSADM', + resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants'), + status_code=200, + json={ + 'tenants': [ + self.project_data_v2.json_response['tenant']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', + self.user_data.user_id, + 'roles']), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='DELETE', + uri=self.get_mock_url(base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', + self.user_data.user_id, + 'roles', 'OS-KSADM', + self.role_data.role_id]), + status_code=204), + ]) self.assertTrue(self.op_cloud.revoke_role( - self.fake_role['name'], - user=self.fake_user['name'], - project=self.fake_project['id'])) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_revoke_role_user_project(self, mock_keystone, mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.projects.list.return_value = [self.fake_project] - mock_keystone.users.list.return_value = [self.fake_user] - mock_keystone.role_assignments.list.return_value = [] + self.role_data.role_name, + user=self.user_data.name, + project=self.project_data.project_id)) + self.assert_calls() + + def test_revoke_role_user_project(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url(resource='projects'), + status_code=200, + json={'projects': [ + self.project_data.json_response['project']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'user.id=%s' % self.user_data.user_id, + 'scope.project.id=%s' % self.project_data.project_id, + 'role.id=%s' % self.role_data.role_id]), + status_code=200, + complete_qs=True, + json={'role_assignments': []}), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url(resource='projects'), + status_code=200, + json={'projects': [ + self.project_data.json_response['project']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'user.id=%s' % self.user_data.user_id, + 'scope.project.id=%s' % self.project_data.project_id, + 'role.id=%s' % self.role_data.role_id]), + status_code=200, + complete_qs=True, + json={'role_assignments': []}), + ]) self.assertFalse(self.op_cloud.revoke_role( - self.fake_role['name'], - user=self.fake_user['name'], - project=self.fake_project['id'])) + self.role_data.role_name, + user=self.user_data.name, + project=self.project_data.project_id)) self.assertFalse(self.op_cloud.revoke_role( - self.fake_role['name'], - user=self.fake_user['id'], - project=self.fake_project['id'])) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_revoke_role_user_project_exists(self, - mock_keystone, - mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.projects.list.return_value = [self.fake_project] - mock_keystone.users.list.return_value = [self.fake_user] - mock_keystone.role_assignments.list.return_value = \ - [self.user_project_assignment] + self.role_data.role_name, + user=self.user_data.user_id, + project=self.project_data.project_id)) + self.assert_calls() + + def test_revoke_role_user_project_exists(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url(resource='projects'), + status_code=200, + json={'projects': [ + self.project_data.json_response['project']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'user.id=%s' % self.user_data.user_id, + 'scope.project.id=%s' % self.project_data.project_id, + 'role.id=%s' % self.role_data.role_id]), + status_code=200, + complete_qs=True, + json={'role_assignments': + self._build_role_assignment_response( + role_id=self.role_data.role_id, + scope_type='project', + scope_id=self.project_data.project_id, + entity_type='user', + entity_id=self.user_data.user_id)}), + dict(method='DELETE', + uri=self.get_mock_url(resource='projects', + append=[self.project_data.project_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id])), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url(resource='projects'), + status_code=200, + json={'projects': [ + self.project_data.json_response['project']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'user.id=%s' % self.user_data.user_id, + 'scope.project.id=%s' % self.project_data.project_id, + 'role.id=%s' % self.role_data.role_id]), + status_code=200, + complete_qs=True, + json={'role_assignments': + self._build_role_assignment_response( + role_id=self.role_data.role_id, + scope_type='project', + scope_id=self.project_data.project_id, + entity_type='user', + entity_id=self.user_data.user_id)}), + dict(method='DELETE', + uri=self.get_mock_url(resource='projects', + append=[self.project_data.project_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id])), + ]) self.assertTrue(self.op_cloud.revoke_role( - self.fake_role['name'], - user=self.fake_user['name'], - project=self.fake_project['id'])) + self.role_data.role_name, + user=self.user_data.name, + project=self.project_data.project_id)) self.assertTrue(self.op_cloud.revoke_role( - self.fake_role['id'], - user=self.fake_user['id'], - project=self.fake_project['id'])) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_revoke_role_group_project(self, mock_keystone, mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.projects.list.return_value = [self.fake_project] - mock_keystone.groups.list.return_value = [self.fake_group] - mock_keystone.role_assignments.list.return_value = [] + self.role_data.role_id, + user=self.user_data.user_id, + project=self.project_data.project_id)) + self.assert_calls() + + def test_revoke_role_group_project(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='projects'), + status_code=200, + json={'projects': [ + self.project_data.json_response['project']]}), + dict(method='GET', + uri=self.get_mock_url(resource='groups'), + status_code=200, + json={'groups': [self.group_data.json_response['group']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'group.id=%s' % self.group_data.group_id, + 'scope.project.id=%s' % self.project_data.project_id, + 'role.id=%s' % self.role_data.role_id]), + status_code=200, + complete_qs=True, + json={'role_assignments': []}), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='projects'), + status_code=200, + json={'projects': [ + self.project_data.json_response['project']]}), + dict(method='GET', + uri=self.get_mock_url(resource='groups'), + status_code=200, + json={'groups': [self.group_data.json_response['group']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'group.id=%s' % self.group_data.group_id, + 'scope.project.id=%s' % self.project_data.project_id, + 'role.id=%s' % self.role_data.role_id]), + status_code=200, + complete_qs=True, + json={'role_assignments': []}), + ]) self.assertFalse(self.op_cloud.revoke_role( - self.fake_role['name'], - group=self.fake_group['name'], - project=self.fake_project['id'])) + self.role_data.role_name, + group=self.group_data.group_name, + project=self.project_data.project_id)) self.assertFalse(self.op_cloud.revoke_role( - self.fake_role['name'], - group=self.fake_group['id'], - project=self.fake_project['id'])) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_revoke_role_group_project_exists(self, - mock_keystone, - mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.projects.list.return_value = [self.fake_project] - mock_keystone.groups.list.return_value = [self.fake_group] - mock_keystone.role_assignments.list.return_value = \ - [self.group_project_assignment] + self.role_data.role_name, + group=self.group_data.group_id, + project=self.project_data.project_id)) + self.assert_calls() + + def test_revoke_role_group_project_exists(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='projects'), + status_code=200, + json={'projects': [ + self.project_data.json_response['project']]}), + dict(method='GET', + uri=self.get_mock_url(resource='groups'), + status_code=200, + json={'groups': [self.group_data.json_response['group']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'group.id=%s' % self.group_data.group_id, + 'scope.project.id=%s' % self.project_data.project_id, + 'role.id=%s' % self.role_data.role_id]), + status_code=200, + complete_qs=True, + json={'role_assignments': + self._build_role_assignment_response( + role_id=self.role_data.role_id, + scope_type='project', + scope_id=self.project_data.project_id, + entity_type='group', + entity_id=self.group_data.group_id)}), + dict(method='DELETE', + uri=self.get_mock_url(resource='projects', + append=[self.project_data.project_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id])), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='projects'), + status_code=200, + json={'projects': [ + self.project_data.json_response['project']]}), + dict(method='GET', + uri=self.get_mock_url(resource='groups'), + status_code=200, + json={'groups': [self.group_data.json_response['group']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'group.id=%s' % self.group_data.group_id, + 'scope.project.id=%s' % self.project_data.project_id, + 'role.id=%s' % self.role_data.role_id]), + status_code=200, + complete_qs=True, + json={'role_assignments': + self._build_role_assignment_response( + role_id=self.role_data.role_id, + scope_type='project', + scope_id=self.project_data.project_id, + entity_type='group', + entity_id=self.group_data.group_id)}), + dict(method='DELETE', + uri=self.get_mock_url(resource='projects', + append=[self.project_data.project_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id])), + ]) self.assertTrue(self.op_cloud.revoke_role( - self.fake_role['name'], - group=self.fake_group['name'], - project=self.fake_project['id'])) + self.role_data.role_name, + group=self.group_data.group_name, + project=self.project_data.project_id)) self.assertTrue(self.op_cloud.revoke_role( - self.fake_role['name'], - group=self.fake_group['id'], - project=self.fake_project['id'])) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_revoke_role_user_domain(self, mock_keystone, mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.users.list.return_value = [self.fake_user] - mock_keystone.domains.get.return_value = self.fake_domain - mock_keystone.role_assignments.list.return_value = [] + self.role_data.role_name, + group=self.group_data.group_id, + project=self.project_data.project_id)) + self.assert_calls() + + def test_revoke_role_user_domain(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'user.id=%s' % self.user_data.user_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'role.id=%s' % self.role_data.role_id]), + status_code=200, + complete_qs=True, + json={'role_assignments': []}), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'user.id=%s' % self.user_data.user_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'role.id=%s' % self.role_data.role_id]), + status_code=200, + complete_qs=True, + json={'role_assignments': []}), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_name]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'user.id=%s' % self.user_data.user_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'role.id=%s' % self.role_data.role_id]), + status_code=200, + complete_qs=True, + json={'role_assignments': []}), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_name]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'user.id=%s' % self.user_data.user_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'role.id=%s' % self.role_data.role_id]), + status_code=200, + complete_qs=True, + json={'role_assignments': []}), + ]) self.assertFalse(self.op_cloud.revoke_role( - self.fake_role['name'], - user=self.fake_user['name'], - domain=self.fake_domain['id'])) + self.role_data.role_name, + user=self.user_data.name, + domain=self.domain_data.domain_id)) self.assertFalse(self.op_cloud.revoke_role( - self.fake_role['name'], - user=self.fake_user['id'], - domain=self.fake_domain['id'])) + self.role_data.role_name, + user=self.user_data.user_id, + domain=self.domain_data.domain_id)) self.assertFalse(self.op_cloud.revoke_role( - self.fake_role['name'], - user=self.fake_user['name'], - domain=self.fake_domain['name'])) + self.role_data.role_name, + user=self.user_data.name, + domain=self.domain_data.domain_name)) self.assertFalse(self.op_cloud.revoke_role( - self.fake_role['name'], - user=self.fake_user['id'], - domain=self.fake_domain['name'])) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_revoke_role_user_domain_exists(self, - mock_keystone, - mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.users.list.return_value = [self.fake_user] - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.domains.get.return_value = self.fake_domain - mock_keystone.role_assignments.list.return_value = \ - [self.user_domain_assignment] + self.role_data.role_name, + user=self.user_data.user_id, + domain=self.domain_data.domain_name)) + self.assert_calls() + + def test_revoke_role_user_domain_exists(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_name]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'user.id=%s' % self.user_data.user_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'role.id=%s' % self.role_data.role_id]), + status_code=200, + complete_qs=True, + json={'role_assignments': + self._build_role_assignment_response( + role_id=self.role_data.role_id, + scope_type='domain', + scope_id=self.domain_data.domain_id, + entity_type='user', + entity_id=self.user_data.user_id)}), + dict(method='DELETE', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id])), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_name]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'user.id=%s' % self.user_data.user_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'role.id=%s' % self.role_data.role_id]), + status_code=200, + complete_qs=True, + json={ + 'role_assignments': self._build_role_assignment_response( + role_id=self.role_data.role_id, + scope_type='domain', + scope_id=self.domain_data.domain_id, + entity_type='user', + entity_id=self.user_data.user_id)}), + dict(method='DELETE', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id])), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'user.id=%s' % self.user_data.user_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'role.id=%s' % self.role_data.role_id]), + status_code=200, + complete_qs=True, + json={'role_assignments': + self._build_role_assignment_response( + role_id=self.role_data.role_id, + scope_type='domain', + scope_id=self.domain_data.domain_id, + entity_type='user', + entity_id=self.user_data.user_id)}), + dict(method='DELETE', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id])), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'user.id=%s' % self.user_data.user_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'role.id=%s' % self.role_data.role_id]), + status_code=200, + complete_qs=True, + json={ + 'role_assignments': self._build_role_assignment_response( + role_id=self.role_data.role_id, + scope_type='domain', + scope_id=self.domain_data.domain_id, + entity_type='user', + entity_id=self.user_data.user_id)}), + dict(method='DELETE', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id])), + ]) self.assertTrue(self.op_cloud.revoke_role( - self.fake_role['name'], - user=self.fake_user['name'], - domain=self.fake_domain['name'])) + self.role_data.role_name, + user=self.user_data.name, + domain=self.domain_data.domain_name)) self.assertTrue(self.op_cloud.revoke_role( - self.fake_role['name'], - user=self.fake_user['id'], - domain=self.fake_domain['name'])) + self.role_data.role_name, + user=self.user_data.user_id, + domain=self.domain_data.domain_name)) self.assertTrue(self.op_cloud.revoke_role( - self.fake_role['name'], - user=self.fake_user['name'], - domain=self.fake_domain['id'])) + self.role_data.role_name, + user=self.user_data.name, + domain=self.domain_data.domain_id)) self.assertTrue(self.op_cloud.revoke_role( - self.fake_role['name'], - user=self.fake_user['id'], - domain=self.fake_domain['id'])) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_revoke_role_group_domain(self, mock_keystone, mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.groups.list.return_value = [self.fake_group] - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.domains.get.return_value = self.fake_domain - mock_keystone.role_assignments.list.return_value = [] + self.role_data.role_name, + user=self.user_data.user_id, + domain=self.domain_data.domain_id)) + self.assert_calls() + + def test_revoke_role_group_domain(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_name]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='groups'), + status_code=200, + json={'groups': [self.group_data.json_response['group']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'group.id=%s' % self.group_data.group_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'role.id=%s' % self.role_data.role_id]), + status_code=200, + complete_qs=True, + json={'role_assignments': []}), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_name]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='groups'), + status_code=200, + json={'groups': [self.group_data.json_response['group']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'group.id=%s' % self.group_data.group_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'role.id=%s' % self.role_data.role_id]), + status_code=200, + complete_qs=True, + json={'role_assignments': []}), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='groups'), + status_code=200, + json={'groups': [self.group_data.json_response['group']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'group.id=%s' % self.group_data.group_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'role.id=%s' % self.role_data.role_id]), + status_code=200, + complete_qs=True, + json={'role_assignments': []}), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='groups'), + status_code=200, + json={'groups': [self.group_data.json_response['group']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'group.id=%s' % self.group_data.group_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'role.id=%s' % self.role_data.role_id]), + status_code=200, + complete_qs=True, + json={'role_assignments': []}), + ]) self.assertFalse(self.op_cloud.revoke_role( - self.fake_role['name'], - group=self.fake_group['name'], - domain=self.fake_domain['name'])) + self.role_data.role_name, + group=self.group_data.group_name, + domain=self.domain_data.domain_name)) self.assertFalse(self.op_cloud.revoke_role( - self.fake_role['name'], - group=self.fake_group['id'], - domain=self.fake_domain['name'])) + self.role_data.role_name, + group=self.group_data.group_id, + domain=self.domain_data.domain_name)) self.assertFalse(self.op_cloud.revoke_role( - self.fake_role['name'], - group=self.fake_group['name'], - domain=self.fake_domain['id'])) + self.role_data.role_name, + group=self.group_data.group_name, + domain=self.domain_data.domain_id)) self.assertFalse(self.op_cloud.revoke_role( - self.fake_role['name'], - group=self.fake_group['id'], - domain=self.fake_domain['id'])) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_revoke_role_group_domain_exists(self, - mock_keystone, - mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.groups.list.return_value = [self.fake_group] - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.domains.get.return_value = self.fake_domain - mock_keystone.role_assignments.list.return_value = \ - [self.group_domain_assignment] + self.role_data.role_name, + group=self.group_data.group_id, + domain=self.domain_data.domain_id)) + self.assert_calls() + + def test_revoke_role_group_domain_exists(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_name]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='groups'), + status_code=200, + json={'groups': [self.group_data.json_response['group']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'group.id=%s' % self.group_data.group_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'role.id=%s' % self.role_data.role_id]), + status_code=200, + complete_qs=True, + json={'role_assignments': + self._build_role_assignment_response( + role_id=self.role_data.role_id, + scope_type='domain', + scope_id=self.domain_data.domain_id, + entity_type='group', + entity_id=self.group_data.group_id)}), + dict(method='DELETE', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id])), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_name]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='groups'), + status_code=200, + json={'groups': [self.group_data.json_response['group']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'group.id=%s' % self.group_data.group_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'role.id=%s' % self.role_data.role_id]), + status_code=200, + complete_qs=True, + json={ + 'role_assignments': self._build_role_assignment_response( + role_id=self.role_data.role_id, + scope_type='domain', + scope_id=self.domain_data.domain_id, + entity_type='group', + entity_id=self.group_data.group_id)}), + dict(method='DELETE', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id])), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='groups'), + status_code=200, + json={'groups': [self.group_data.json_response['group']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'group.id=%s' % self.group_data.group_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'role.id=%s' % self.role_data.role_id]), + status_code=200, + complete_qs=True, + json={'role_assignments': + self._build_role_assignment_response( + role_id=self.role_data.role_id, + scope_type='domain', + scope_id=self.domain_data.domain_id, + entity_type='group', + entity_id=self.group_data.group_id)}), + dict(method='DELETE', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id])), + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='groups'), + status_code=200, + json={'groups': [self.group_data.json_response['group']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'group.id=%s' % self.group_data.group_id, + 'scope.domain.id=%s' % self.domain_data.domain_id, + 'role.id=%s' % self.role_data.role_id]), + status_code=200, + complete_qs=True, + json={ + 'role_assignments': self._build_role_assignment_response( + role_id=self.role_data.role_id, + scope_type='domain', + scope_id=self.domain_data.domain_id, + entity_type='group', + entity_id=self.group_data.group_id)}), + dict(method='DELETE', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id])), + ]) self.assertTrue(self.op_cloud.revoke_role( - self.fake_role['name'], - group=self.fake_group['name'], - domain=self.fake_domain['name'])) + self.role_data.role_name, + group=self.group_data.group_name, + domain=self.domain_data.domain_name)) self.assertTrue(self.op_cloud.revoke_role( - self.fake_role['name'], - group=self.fake_group['id'], - domain=self.fake_domain['name'])) + self.role_data.role_name, + group=self.group_data.group_id, + domain=self.domain_data.domain_name)) self.assertTrue(self.op_cloud.revoke_role( - self.fake_role['name'], - group=self.fake_group['name'], - domain=self.fake_domain['id'])) + self.role_data.role_name, + group=self.group_data.group_name, + domain=self.domain_data.domain_id)) self.assertTrue(self.op_cloud.revoke_role( - self.fake_role['name'], - group=self.fake_group['id'], - domain=self.fake_domain['id'])) + self.role_data.role_name, + group=self.group_data.group_id, + domain=self.domain_data.domain_id)) + self.assert_calls() - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_grant_no_role(self, mock_keystone, mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.roles.list.return_value = [] + def test_grant_no_role(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': []}) + ]) with testtools.ExpectedException( exc.OpenStackCloudException, - 'Role {0} not found'.format(self.fake_role['name']) + 'Role {0} not found'.format(self.role_data.role_name) ): self.op_cloud.grant_role( - self.fake_role['name'], - group=self.fake_group['name'], - domain=self.fake_domain['name']) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_revoke_no_role(self, mock_keystone, mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.roles.list.return_value = [] + self.role_data.role_name, + group=self.group_data.group_name, + domain=self.domain_data.domain_name) + self.assert_calls() + + def test_revoke_no_role(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': []}) + ]) + with testtools.ExpectedException( exc.OpenStackCloudException, - 'Role {0} not found'.format(self.fake_role['name']) + 'Role {0} not found'.format(self.role_data.role_name) ): self.op_cloud.revoke_role( - self.fake_role['name'], - group=self.fake_group['name'], - domain=self.fake_domain['name']) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_grant_no_user_or_group_specified(self, - mock_keystone, - mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.roles.list.return_value = [self.fake_role] + self.role_data.role_name, + group=self.group_data.group_name, + domain=self.domain_data.domain_name) + self.assert_calls() + + def test_grant_no_user_or_group_specified(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}) + ]) with testtools.ExpectedException( exc.OpenStackCloudException, 'Must specify either a user or a group' ): - self.op_cloud.grant_role(self.fake_role['name']) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_revoke_no_user_or_group_specified(self, - mock_keystone, - mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.roles.list.return_value = [self.fake_role] + self.op_cloud.grant_role(self.role_data.role_name) + self.assert_calls() + + def test_revoke_no_user_or_group_specified(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}) + ]) with testtools.ExpectedException( exc.OpenStackCloudException, 'Must specify either a user or a group' ): - self.op_cloud.revoke_role(self.fake_role['name']) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_grant_no_user_or_group(self, mock_keystone, mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.users.list.return_value = [] + self.op_cloud.revoke_role(self.role_data.role_name) + self.assert_calls() + + def test_grant_no_user_or_group(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': []}) + ]) with testtools.ExpectedException( exc.OpenStackCloudException, 'Must specify either a user or a group' ): self.op_cloud.grant_role( - self.fake_role['name'], - user=self.fake_user['name']) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_revoke_no_user_or_group(self, mock_keystone, mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.users.list.return_value = [] + self.role_data.role_name, + user=self.user_data.name) + self.assert_calls() + + def test_revoke_no_user_or_group(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': []}) + ]) with testtools.ExpectedException( exc.OpenStackCloudException, 'Must specify either a user or a group' ): self.op_cloud.revoke_role( - self.fake_role['name'], - user=self.fake_user['name']) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_grant_both_user_and_group(self, mock_keystone, mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.users.list.return_value = [self.fake_user] - mock_keystone.groups.list.return_value = [self.fake_group] + self.role_data.role_name, + user=self.user_data.name) + self.assert_calls() + + def test_grant_both_user_and_group(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url(resource='groups'), + status_code=200, + json={'groups': [self.group_data.json_response['group']]}), + ]) with testtools.ExpectedException( exc.OpenStackCloudException, 'Specify either a group or a user, not both' ): self.op_cloud.grant_role( - self.fake_role['name'], - user=self.fake_user['name'], - group=self.fake_group['name']) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_revoke_both_user_and_group(self, mock_keystone, mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.users.list.return_value = [self.fake_user] - mock_keystone.groups.list.return_value = [self.fake_group] + self.role_data.role_name, + user=self.user_data.name, + group=self.group_data.group_name) + self.assert_calls() + + def test_revoke_both_user_and_group(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url(resource='groups'), + status_code=200, + json={'groups': [self.group_data.json_response['group']]}), + ]) with testtools.ExpectedException( exc.OpenStackCloudException, 'Specify either a group or a user, not both' ): self.op_cloud.revoke_role( - self.fake_role['name'], - user=self.fake_user['name'], - group=self.fake_group['name']) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_grant_both_project_and_domain(self, - mock_keystone, - mock_api_version): - mock_api_version.return_value = '3' - fake_user2 = fakes.FakeUser('12345', - 'test@nobody.org', - 'test', - domain_id='default') - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.users.list.return_value = [self.fake_user, fake_user2] - mock_keystone.projects.list.return_value = [self.fake_project] - mock_keystone.domains.get.return_value = self.fake_domain + self.role_data.role_name, + user=self.user_data.name, + group=self.group_data.group_name) + self.assert_calls() + + def test_grant_both_project_and_domain(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_name]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url(resource='projects'), + status_code=200, + json={'projects': [ + self.project_data.json_response['project']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'user.id=%s' % self.user_data.user_id, + 'scope.project.id=%s' % self.project_data.project_id, + 'role.id=%s' % self.role_data.role_id]), + complete_qs=True, + status_code=200, + json={'role_assignments': []}), + dict(method='PUT', + uri=self.get_mock_url(resource='projects', + append=[self.project_data.project_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id]), + status_code=204) + ]) self.assertTrue( self.op_cloud.grant_role( - self.fake_role['name'], - user=self.fake_user['name'], - project=self.fake_project['id'], - domain=self.fake_domain['name'])) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_revoke_both_project_and_domain(self, - mock_keystone, - mock_api_version): - mock_api_version.return_value = '3' - fake_user2 = fakes.FakeUser('12345', - 'test@nobody.org', - 'test', - domain_id='default') - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.users.list.return_value = [self.fake_user, fake_user2] - mock_keystone.projects.list.return_value = [self.fake_project] - mock_keystone.domains.get.return_value = self.fake_domain - mock_keystone.role_assignments.list.return_value = \ - [self.user_project_assignment] + self.role_data.role_name, + user=self.user_data.name, + project=self.project_data.project_id, + domain=self.domain_data.domain_name)) + self.assert_calls() + + def test_revoke_both_project_and_domain(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=[self.domain_data.domain_name]), + status_code=200, + json=self.domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url(resource='projects'), + status_code=200, + json={'projects': [ + self.project_data.json_response['project']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'user.id=%s' % self.user_data.user_id, + 'scope.project.id=%s' % self.project_data.project_id, + 'role.id=%s' % self.role_data.role_id]), + complete_qs=True, + status_code=200, + json={ + 'role_assignments': self._build_role_assignment_response( + role_id=self.role_data.role_id, + scope_type='project', + scope_id=self.project_data.project_id, + entity_type='user', + entity_id=self.user_data.user_id)}), + dict(method='DELETE', + uri=self.get_mock_url(resource='projects', + append=[self.project_data.project_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id]), + status_code=204) + ]) self.assertTrue(self.op_cloud.revoke_role( - self.fake_role['name'], - user=self.fake_user['name'], - project=self.fake_project['id'], - domain=self.fake_domain['name'])) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_grant_no_project_or_domain(self, mock_keystone, mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.users.list.return_value = [self.fake_user] - mock_keystone.projects.list.return_value = [] - mock_keystone.domains.get.return_value = None + self.role_data.role_name, + user=self.user_data.name, + project=self.project_data.project_id, + domain=self.domain_data.domain_name)) + self.assert_calls() + + def test_grant_no_project_or_domain(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=['user.id=%s' % self.user_data.user_id, + 'role.id=%s' % self.role_data.role_id]), + complete_qs=True, + status_code=200, + json={'role_assignments': []}) + ]) with testtools.ExpectedException( exc.OpenStackCloudException, 'Must specify either a domain or project' ): self.op_cloud.grant_role( - self.fake_role['name'], - user=self.fake_user['name']) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_revoke_no_project_or_domain(self, - mock_keystone, - mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.users.list.return_value = [self.fake_user] - mock_keystone.projects.list.return_value = [] - mock_keystone.domains.get.return_value = None - mock_keystone.role_assignments.list.return_value = \ - [self.user_project_assignment] + self.role_data.role_name, + user=self.user_data.name) + self.assert_calls() + + def test_revoke_no_project_or_domain(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=['user.id=%s' % self.user_data.user_id, + 'role.id=%s' % self.role_data.role_id]), + complete_qs=True, + status_code=200, + json={ + 'role_assignments': self._build_role_assignment_response( + role_id=self.role_data.role_id, + scope_type='project', + scope_id=self.project_data.project_id, + entity_type='user', + entity_id=self.user_data.user_id)}) + ]) with testtools.ExpectedException( exc.OpenStackCloudException, 'Must specify either a domain or project' ): self.op_cloud.revoke_role( - self.fake_role['name'], - user=self.fake_user['name']) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_grant_bad_domain_exception(self, - mock_keystone, - mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.domains.get.side_effect = Exception('test') + self.role_data.role_name, + user=self.user_data.name) + self.assert_calls() + + def test_grant_bad_domain_exception(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=['baddomain']), + status_code=404, + text='Could not find domain: baddomain') + ]) with testtools.ExpectedException( exc.OpenStackCloudException, - 'Failed to get domain baddomain \(Inner Exception: test\)' + 'Failed to get domain baddomain ' + '\(Inner Exception: Not Found \(HTTP 404\)\)' ): self.op_cloud.grant_role( - self.fake_role['name'], - user=self.fake_user['name'], + self.role_data.role_name, + user=self.user_data.name, domain='baddomain') + self.assert_calls() - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_revoke_bad_domain_exception(self, - mock_keystone, - mock_api_version): - mock_api_version.return_value = '3' - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.domains.get.side_effect = Exception('test') + def test_revoke_bad_domain_exception(self): + self._add_discovery_uri_call() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(resource='domains', + append=['baddomain']), + status_code=404, + text='Could not find domain: baddomain') + ]) with testtools.ExpectedException( exc.OpenStackCloudException, - 'Failed to get domain baddomain \(Inner Exception: test\)' + 'Failed to get domain baddomain ' + '\(Inner Exception: Not Found \(HTTP 404\)\)' ): self.op_cloud.revoke_role( - self.fake_role['name'], - user=self.fake_user['name'], + self.role_data.role_name, + user=self.user_data.name, domain='baddomain') + self.assert_calls() - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_grant_role_user_project_v2_wait(self, - mock_keystone, - mock_api_version): - mock_api_version.return_value = '2.0' - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.tenants.list.return_value = [self.fake_project] - mock_keystone.users.list.return_value = [self.fake_user] - mock_keystone.roles.roles_for_user.side_effect = [ - [], [], [self.fake_role]] - mock_keystone.roles.add_user_role.return_value = self.fake_role + def test_grant_role_user_project_v2_wait(self): + self.use_keystone_v2() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(base_url_append='OS-KSADM', + resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants'), + status_code=200, + json={'tenants': [ + self.project_data_v2.json_response['tenant']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', + self.user_data.user_id, + 'roles']), + status_code=200, + json={'roles': []}), + dict(method='PUT', + uri=self.get_mock_url( + base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', self.user_data.user_id, 'roles', + 'OS-KSADM', self.role_data.role_id]), + status_code=201), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', + self.user_data.user_id, + 'roles']), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + ]) self.assertTrue( self.op_cloud.grant_role( - self.fake_role['name'], - user=self.fake_user['name'], - project=self.fake_project['id'], + self.role_data.role_name, + user=self.user_data.name, + project=self.project_data.project_id, wait=True)) + self.assert_calls() - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_grant_role_user_project_v2_wait_exception(self, - mock_keystone, - mock_api_version): - mock_api_version.return_value = '2.0' - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.tenants.list.return_value = [self.fake_project] - mock_keystone.users.list.return_value = [self.fake_user] - mock_keystone.roles.roles_for_user.return_value = [] - mock_keystone.roles.add_user_role.return_value = self.fake_role + def test_grant_role_user_project_v2_wait_exception(self): + self.use_keystone_v2() with testtools.ExpectedException( exc.OpenStackCloudTimeout, 'Timeout waiting for role to be granted' ): - self.assertTrue(self.op_cloud.grant_role( - self.fake_role['name'], user=self.fake_user['name'], - project=self.fake_project['id'], wait=True, timeout=0.01)) - - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_revoke_role_user_project_v2_wait( - self, mock_keystone, mock_api_version): - mock_api_version.return_value = '2.0' - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.tenants.list.return_value = [self.fake_project] - mock_keystone.users.list.return_value = [self.fake_user] - mock_keystone.roles.roles_for_user.side_effect = [ - [self.fake_role], [self.fake_role], - []] - mock_keystone.roles.remove_user_role.return_value = self.fake_role + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(base_url_append='OS-KSADM', + resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants'), + status_code=200, + json={'tenants': [ + self.project_data_v2.json_response['tenant']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants', + append=[ + self.project_data_v2.project_id, + 'users', + self.user_data.user_id, + 'roles']), + status_code=200, + json={'roles': []}), + dict(method='PUT', + uri=self.get_mock_url( + base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', self.user_data.user_id, 'roles', + 'OS-KSADM', self.role_data.role_id]), + status_code=201), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants', + append=[ + self.project_data_v2.project_id, + 'users', + self.user_data.user_id, + 'roles']), + status_code=200, + json={'roles': []}), + ]) + self.assertTrue( + self.op_cloud.grant_role( + self.role_data.role_name, + user=self.user_data.name, + project=self.project_data.project_id, + wait=True, timeout=0.01)) + self.assert_calls(do_count=False) + + def test_revoke_role_user_project_v2_wait(self): + self.use_keystone_v2() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(base_url_append='OS-KSADM', + resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants'), + status_code=200, + json={ + 'tenants': [ + self.project_data_v2.json_response['tenant']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', + self.user_data.user_id, + 'roles']), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='DELETE', + uri=self.get_mock_url(base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', + self.user_data.user_id, + 'roles', 'OS-KSADM', + self.role_data.role_id]), + status_code=204), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', + self.user_data.user_id, + 'roles']), + status_code=200, + json={'roles': []}), + ]) self.assertTrue( self.op_cloud.revoke_role( - self.fake_role['name'], - user=self.fake_user['name'], - project=self.fake_project['id'], + self.role_data.role_name, + user=self.user_data.name, + project=self.project_data.project_id, wait=True)) + self.assert_calls(do_count=False) - @patch.object(occ.cloud_config.CloudConfig, 'get_api_version') - @patch.object(OperatorCloud, 'keystone_client') - def test_revoke_role_user_project_v2_wait_exception(self, - mock_keystone, - mock_api_version): - mock_api_version.return_value = '2.0' - mock_keystone.roles.list.return_value = [self.fake_role] - mock_keystone.tenants.list.return_value = [self.fake_project] - mock_keystone.users.list.return_value = [self.fake_user] - mock_keystone.roles.roles_for_user.return_value = [self.fake_role] - mock_keystone.roles.remove_user_role.return_value = self.fake_role + def test_revoke_role_user_project_v2_wait_exception(self): + self.use_keystone_v2() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(base_url_append='OS-KSADM', + resource='roles'), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, resource='users'), + status_code=200, + json={'users': [self.user_data.json_response['user']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants'), + status_code=200, + json={ + 'tenants': [ + self.project_data_v2.json_response['tenant']]}), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', + self.user_data.user_id, + 'roles']), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + dict(method='DELETE', + uri=self.get_mock_url(base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', + self.user_data.user_id, + 'roles', 'OS-KSADM', + self.role_data.role_id]), + status_code=204), + dict(method='GET', + uri=self.get_mock_url(base_url_append=None, + resource='tenants', + append=[self.project_data_v2.project_id, + 'users', + self.user_data.user_id, + 'roles']), + status_code=200, + json={'roles': [self.role_data.json_response['role']]}), + ]) with testtools.ExpectedException( exc.OpenStackCloudTimeout, 'Timeout waiting for role to be revoked' ): self.assertTrue(self.op_cloud.revoke_role( - self.fake_role['name'], user=self.fake_user['name'], - project=self.fake_project['id'], wait=True, timeout=0.01)) + self.role_data.role_name, user=self.user_data.name, + project=self.project_data.project_id, wait=True, timeout=0.01)) + self.assert_calls(do_count=False) From 944556e992eb21f060adb04cdf7093bc8ac4fbb9 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 30 Apr 2017 10:23:46 -0500 Subject: [PATCH 1480/3836] Remove two lines that are leftover and broken Not really sure how these crept in - they never would have made sense. Also not sure how this codepath _worked_ in the live test I used (with the results in IRC) - but they definitely break as they should when triggered in unit tests. Change-Id: Iddc69cbd325c7a3a25cde48a2b2bddb01826c0ff --- shade/exc.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/shade/exc.py b/shade/exc.py index 913a2f299..5a74834a1 100644 --- a/shade/exc.py +++ b/shade/exc.py @@ -91,8 +91,6 @@ class OpenStackCloudURINotFound(OpenStackCloudHTTPError): # Logic shamelessly stolen from requests def raise_from_response(response, error_message=None): msg = '' - if error_message: - msg = _ if 400 <= response.status_code < 500: source = "Client" elif 500 <= response.status_code < 600: From 478d4ba101b1c1f581ab48cb1bdf82917a928e52 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 30 Apr 2017 10:35:57 -0500 Subject: [PATCH 1481/3836] Move REST error_messages to error_message argument The with shade_exceptions is not doing what is expected here - since it passes OpenStackCloudException objects on unmodified. Instead, use the new error_message parameter to continue to give friendly error messages. Also - wow, we were mocking _compute_client for the nova extensions tests. I believe that was become those pre-dated our use of requests_mock. Anyway, that's super lame (and also broke because of this patch), so it's now fixed. Change-Id: Ia764cc76d724a8a732fa2b1209a138634a25a9e1 --- shade/openstackcloud.py | 87 ++++++++++++++++++---------------- shade/tests/unit/test_shade.py | 67 +++++++++++++++++++++----- 2 files changed, 101 insertions(+), 53 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 109fc4b48..e68bf0f45 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1454,9 +1454,10 @@ def has_service(self, service_key): def _nova_extensions(self): extensions = set() - with _utils.shade_exceptions("Error fetching extension list for nova"): - for extension in self._compute_client.get('/extensions'): - extensions.add(extension['alias']) + for extension in self._compute_client.get( + '/extensions', + error_message="Error fetching extension list for nova"): + extensions.add(extension['alias']) return extensions @@ -1765,23 +1766,25 @@ def list_flavors(self, get_extra=None): """ if get_extra is None: get_extra = self._extra_config['get_flavor_extra_specs'] - with _utils.shade_exceptions("Error fetching flavor list"): - flavors = self._normalize_flavors( - self._compute_client.get( - '/flavors/detail', params=dict(is_public='None'))) - - with _utils.shade_exceptions("Error fetching flavor extra specs"): - for flavor in flavors: - if not flavor.extra_specs and get_extra: - endpoint = "/flavors/{id}/os-extra_specs".format( - id=flavor.id) - try: - flavor.extra_specs = self._compute_client.get(endpoint) - except OpenStackCloudHTTPError as e: - flavor.extra_specs = {} - self.log.debug( - 'Fetching extra specs for flavor failed:' - ' %(msg)s', {'msg': str(e)}) + flavors = self._normalize_flavors( + self._compute_client.get( + '/flavors/detail', params=dict(is_public='None'), + error_message="Error fetching flavor list")) + + error_message = "Error fetching flavor extra specs" + for flavor in flavors: + if not flavor.extra_specs and get_extra: + endpoint = "/flavors/{id}/os-extra_specs".format( + id=flavor.id) + try: + flavor.extra_specs = self._compute_client.get( + endpoint, + error_message="Error fetching flavor extra specs") + except OpenStackCloudHTTPError as e: + flavor.extra_specs = {} + self.log.debug( + 'Fetching extra specs for flavor failed:' + ' %(msg)s', {'msg': str(e)}) return flavors @@ -1794,8 +1797,8 @@ def list_stacks(self): :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ - with _utils.shade_exceptions("Error fetching stack list"): - stacks = self._orchestration_client.get('/stacks') + stacks = self._orchestration_client.get( + '/stacks', error_message="Error fetching stack list") return self._normalize_stacks(stacks) def list_server_security_groups(self, server): @@ -2083,10 +2086,10 @@ def list_floating_ip_pools(self): raise OpenStackCloudUnavailableExtension( 'Floating IP pools extension is not available on target cloud') - with _utils.shade_exceptions("Error fetching floating IP pool list"): - return [ - {'name': p['name']} - for p in self._compute_client.get('os-floating-ip-pools')] + pools = self._compute_client.get( + 'os-floating-ip-pools', + error_message="Error fetching floating IP pool list") + return [{'name': p['name']} for p in pools] def _list_floating_ips(self, filters=None): if self._use_neutron_floating(): @@ -2903,15 +2906,15 @@ def get_stack(self, name_or_id, filters=None): def _search_one_stack(name_or_id=None, filters=None): # stack names are mandatory and enforced unique in the project # so a StackGet can always be used for name or ID. - with _utils.shade_exceptions("Error fetching stack"): - try: - stack = self._orchestration_client.get( - '/stacks/{name_or_id}'.format(name_or_id=name_or_id)) - # Treat DELETE_COMPLETE stacks as a NotFound - if stack['stack_status'] == 'DELETE_COMPLETE': - return [] - except OpenStackCloudURINotFound: + try: + stack = self._orchestration_client.get( + '/stacks/{name_or_id}'.format(name_or_id=name_or_id), + error_message="Error fetching stack") + # Treat DELETE_COMPLETE stacks as a NotFound + if stack['stack_status'] == 'DELETE_COMPLETE': return [] + except OpenStackCloudURINotFound: + return [] stack = self._normalize_stack(stack) return _utils._filter_list([stack], name_or_id, filters) @@ -3379,15 +3382,15 @@ def delete_image( image = self.get_image(name_or_id) if not image: return False - with _utils.shade_exceptions("Error in deleting image"): - self._image_client.delete( - '/images/{id}'.format(id=image.id)) - self.list_images.invalidate(self) + self._image_client.delete( + '/images/{id}'.format(id=image.id), + error_message="Error in deleting image") + self.list_images.invalidate(self) - # Task API means an image was uploaded to swift - if self.image_api_use_tasks and IMAGE_OBJECT_KEY in image: - (container, objname) = image[IMAGE_OBJECT_KEY].split('/', 1) - self.delete_object(container=container, name=objname) + # Task API means an image was uploaded to swift + if self.image_api_use_tasks and IMAGE_OBJECT_KEY in image: + (container, objname) = image[IMAGE_OBJECT_KEY].split('/', 1) + self.delete_object(container=container, name=objname) if wait: for count in _utils._iterate_timeout( diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index e9b4fed71..2d5941aa2 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -209,8 +209,7 @@ def test_iterate_timeout_timeout(self, mock_sleep): pass mock_sleep.assert_called_with(1.0) - @mock.patch.object(shade.OpenStackCloud, '_compute_client') - def test__nova_extensions(self, mock_compute): + def test__nova_extensions(self): body = [ { "updated": "2014-12-03T00:00:00Z", @@ -229,22 +228,33 @@ def test__nova_extensions(self, mock_compute): "description": "Disk Management Extension." }, ] - mock_compute.get.return_value = body + self.register_uris([ + dict(method='GET', + uri='{endpoint}/extensions'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json=dict(extensions=body)) + ]) extensions = self.cloud._nova_extensions() - mock_compute.get.assert_called_once_with('/extensions') self.assertEqual(set(['NMN', 'OS-DCF']), extensions) - @mock.patch.object(shade.OpenStackCloud, '_compute_client') - def test__nova_extensions_fails(self, mock_compute): - mock_compute.get.side_effect = Exception() + self.assert_calls() + + def test__nova_extensions_fails(self): + self.register_uris([ + dict(method='GET', + uri='{endpoint}/extensions'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + status_code=404), + ]) with testtools.ExpectedException( - exc.OpenStackCloudException, + exc.OpenStackCloudURINotFound, "Error fetching extension list for nova" ): self.cloud._nova_extensions() - @mock.patch.object(shade.OpenStackCloud, '_compute_client') - def test__has_nova_extension(self, mock_compute): + self.assert_calls() + + def test__has_nova_extension(self): body = [ { "updated": "2014-12-03T00:00:00Z", @@ -263,10 +273,45 @@ def test__has_nova_extension(self, mock_compute): "description": "Disk Management Extension." }, ] - mock_compute.get.return_value = body + self.register_uris([ + dict(method='GET', + uri='{endpoint}/extensions'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json=dict(extensions=body)) + ]) self.assertTrue(self.cloud._has_nova_extension('NMN')) + + self.assert_calls() + + def test__has_nova_extension_missing(self): + body = [ + { + "updated": "2014-12-03T00:00:00Z", + "name": "Multinic", + "links": [], + "namespace": "http://openstack.org/compute/ext/fake_xml", + "alias": "NMN", + "description": "Multiple network support." + }, + { + "updated": "2014-12-03T00:00:00Z", + "name": "DiskConfig", + "links": [], + "namespace": "http://openstack.org/compute/ext/fake_xml", + "alias": "OS-DCF", + "description": "Disk Management Extension." + }, + ] + self.register_uris([ + dict(method='GET', + uri='{endpoint}/extensions'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json=dict(extensions=body)) + ]) self.assertFalse(self.cloud._has_nova_extension('invalid')) + self.assert_calls() + def test_range_search(self): filters = {"key1": "min", "key2": "20"} retval = self.cloud.range_search(RANGE_DATA, filters) From 7abe4e9422d907f281c86aa09a48d8fabc3c48d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Thu, 27 Apr 2017 11:20:25 +0000 Subject: [PATCH 1482/3836] Replace neutronclient with REST API calls in router commands All router related commands to Neutron like list/create/update/delete/add_interface/remove_interface are now made via keystoneauth Change-Id: I81592285268030e4868ef6b522bc7937a31c756e --- shade/_tasks.py | 30 ------------------ shade/openstackcloud.py | 70 +++++++++++++++++++---------------------- 2 files changed, 32 insertions(+), 68 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index f6af97f9a..7eca87228 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -207,36 +207,6 @@ def main(self, client): return client.nova_client.keypairs.delete(**self.args) -class RouterList(task_manager.Task): - def main(self, client): - return client.neutron_client.list_routers() - - -class RouterCreate(task_manager.Task): - def main(self, client): - return client.neutron_client.create_router(**self.args) - - -class RouterUpdate(task_manager.Task): - def main(self, client): - return client.neutron_client.update_router(**self.args) - - -class RouterDelete(task_manager.Task): - def main(self, client): - return client.neutron_client.delete_router(**self.args) - - -class RouterAddInterface(task_manager.Task): - def main(self, client): - return client.neutron_client.add_interface_router(**self.args) - - -class RouterRemoveInterface(task_manager.Task): - def main(self, client): - client.neutron_client.remove_interface_router(**self.args) - - class NovaImageList(task_manager.Task): def main(self, client): return client.nova_client.images.list() diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 109fc4b48..cc481adda 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1654,9 +1654,9 @@ def list_routers(self, filters=None): # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} - with _utils.neutron_exceptions("Error fetching router list"): - return self.manager.submit_task( - _tasks.RouterList(**filters))['routers'] + return self._network_client.get( + "/routers.json", params=filters, + error_message="Error fetching router list") def list_subnets(self, filters=None): """List all available subnets. @@ -3059,18 +3059,18 @@ def add_router_interface(self, router, subnet_id=None, port_id=None): :raises: OpenStackCloudException on operation error. """ - body = {} + json_body = {} if subnet_id: - body['subnet_id'] = subnet_id + json_body['subnet_id'] = subnet_id if port_id: - body['port_id'] = port_id + json_body['port_id'] = port_id - with _utils.neutron_exceptions( - "Error attaching interface to router {0}".format(router['id']) - ): - return self.manager.submit_task( - _tasks.RouterAddInterface(router=router['id'], body=body) - ) + return self._network_client.put( + "/routers/{router_id}/add_router_interface.json".format( + router_id=router['id']), + json=json_body, + error_message="Error attaching interface to router {0}".format( + router['id'])) def remove_router_interface(self, router, subnet_id=None, port_id=None): """Detach a subnet from an internal router interface. @@ -3089,22 +3089,22 @@ def remove_router_interface(self, router, subnet_id=None, port_id=None): :raises: OpenStackCloudException on operation error. """ - body = {} + json_body = {} if subnet_id: - body['subnet_id'] = subnet_id + json_body['subnet_id'] = subnet_id if port_id: - body['port_id'] = port_id + json_body['port_id'] = port_id - if not body: + if not json_body: raise ValueError( "At least one of subnet_id or port_id must be supplied.") - with _utils.neutron_exceptions( - "Error detaching interface from router {0}".format(router['id']) - ): - return self.manager.submit_task( - _tasks.RouterRemoveInterface(router=router['id'], body=body) - ) + self._network_client.put( + "/routers/{router_id}/remove_router_interface.json".format( + router_id=router['id']), + json=json_body, + error_message="Error detaching interface from router {0}".format( + router['id'])) def list_router_interfaces(self, router, interface_type=None): """List all interfaces for a router. @@ -3182,11 +3182,9 @@ def create_router(self, name=None, admin_state_up=True, if ext_gw_info: router['external_gateway_info'] = ext_gw_info - with _utils.neutron_exceptions( - "Error creating router {0}".format(name)): - new_router = self.manager.submit_task( - _tasks.RouterCreate(body=dict(router=router))) - return new_router['router'] + return self._network_client.post( + "/routers.json", json={"router": router}, + error_message="Error creating router {0}".format(name)) def update_router(self, name_or_id, name=None, admin_state_up=None, ext_gateway_net_id=None, enable_snat=None, @@ -3233,13 +3231,10 @@ def update_router(self, name_or_id, name=None, admin_state_up=None, raise OpenStackCloudException( "Router %s not found." % name_or_id) - with _utils.neutron_exceptions( - "Error updating router {0}".format(name_or_id)): - new_router = self.manager.submit_task( - _tasks.RouterUpdate( - router=curr_router['id'], body=dict(router=router))) - - return new_router['router'] + return self._network_client.put( + "/routers/{router_id}.json".format(router_id=curr_router['id']), + json={"router": router}, + error_message="Error updating router {0}".format(name_or_id)) def delete_router(self, name_or_id): """Delete a logical router. @@ -3259,10 +3254,9 @@ def delete_router(self, name_or_id): self.log.debug("Router %s not found for deleting", name_or_id) return False - with _utils.neutron_exceptions( - "Error deleting router {0}".format(name_or_id)): - self.manager.submit_task( - _tasks.RouterDelete(router=router['id'])) + self._network_client.delete( + "/routers/{router_id}.json".format(router_id=router['id']), + error_message="Error deleting router {0}".format(name_or_id)) return True From f430b9483eabcbf9796e0ec6ca043c94159554be Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Wed, 26 Apr 2017 17:20:26 -0700 Subject: [PATCH 1483/3836] Refactor the test_create_volume_invalidates test Simplify the logic a little bit to make the switch to the REST API call easier. Change-Id: I8eee73f7d59d4def45b788be08c5170c1f6820c2 Signed-off-by: Rosario Di Somma --- shade/tests/unit/test_caching.py | 50 ++++++++++++++++---------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index e11d2ea5a..79879004c 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -248,53 +248,53 @@ def test_list_volumes_creating_invalidates(self): self.assert_calls() def test_create_volume_invalidates(self): - fake_volb4 = fakes.FakeVolume('volume1', 'available', - 'Volume 1 Display Name') - fake_volb4_dict = meta.obj_to_dict(fake_volb4) - fake_vol = fakes.FakeVolume('12345', 'creating', '') - fake_vol_dict = meta.obj_to_dict(fake_vol) - - def now_available(request, context): - fake_vol.status = 'available' - fake_vol_dict['status'] = 'available' - return {'volume': fake_vol_dict} + fake_volb4 = meta.obj_to_dict( + fakes.FakeVolume('volume1', 'available', '')) + _id = '12345' + fake_vol_creating = meta.obj_to_dict( + fakes.FakeVolume(_id, 'creating', '')) + fake_vol_avail = meta.obj_to_dict( + fakes.FakeVolume(_id, 'available', '')) def now_deleting(request, context): - fake_vol.status = 'deleting' - fake_vol_dict['status'] = 'deleting' + fake_vol_avail['status'] = 'deleting' self.register_uris([ dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', 'detail']), - json={'volumes': [fake_volb4_dict]}), + json={'volumes': [fake_volb4]}), dict(method='POST', uri=self.get_mock_url( 'volumev2', 'public', append=['volumes']), - json={'volume': fake_vol_dict}), + json={'volume': fake_vol_creating}), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', _id]), + json={'volume': [fake_volb4, fake_vol_creating]}), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', fake_vol.id]), - json=now_available), + 'volumev2', 'public', append=['volumes', 'detail']), + json={'volumes': [fake_volb4, fake_vol_creating]}), dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', 'detail']), - json={'volumes': [fake_volb4_dict, fake_vol_dict]}), + json={'volumes': [fake_volb4, fake_vol_avail]}), dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', 'detail']), - json={'volumes': [fake_volb4_dict, fake_vol_dict]}), + json={'volumes': [fake_volb4, fake_vol_avail]}), dict(method='DELETE', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', fake_vol.id]), + 'volumev2', 'public', append=['volumes', _id]), json=now_deleting), dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', 'detail']), - json={'volumes': [fake_volb4_dict]})]) + json={'volumes': [fake_volb4]})]) self.assertEqual( - [self.cloud._normalize_volume(fake_volb4_dict)], + [self.cloud._normalize_volume(fake_volb4)], self.cloud.list_volumes()) volume = dict(display_name='junk_vol', size=1, @@ -304,14 +304,14 @@ def now_deleting(request, context): # because the first volume was available and thus would already be # cached. self.assertEqual( - [self.cloud._normalize_volume(fake_volb4_dict), - self.cloud._normalize_volume(fake_vol_dict)], + [self.cloud._normalize_volume(fake_volb4), + self.cloud._normalize_volume(fake_vol_avail)], self.cloud.list_volumes()) - self.cloud.delete_volume(fake_vol.id) + self.cloud.delete_volume(_id) # And now delete and check same thing since list is cached as all # available self.assertEqual( - [self.cloud._normalize_volume(fake_volb4_dict)], + [self.cloud._normalize_volume(fake_volb4)], self.cloud.list_volumes()) self.assert_calls() From be9275178185feceaf95dd859afacec3979b8320 Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Sun, 30 Apr 2017 22:11:42 +0000 Subject: [PATCH 1484/3836] Use REST API for volume type_access and volume create Change-Id: Ibf3e16d7e2faab3ed9afe7e2b69216b59aceecdf Signed-off-by: Rosario Di Somma --- shade/_tasks.py | 27 --------------- shade/openstackcloud.py | 9 ++--- shade/operatorcloud.py | 38 +++++++++++----------- shade/tests/functional/test_volume_type.py | 10 +++--- shade/tests/unit/test_caching.py | 4 --- 5 files changed, 29 insertions(+), 59 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 7eca87228..ba6510062 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -212,33 +212,6 @@ def main(self, client): return client.nova_client.images.list() -class VolumeTypeList(task_manager.Task): - def main(self, client): - return client.cinder_client.volume_types.list() - - -class VolumeTypeAccessList(task_manager.Task): - def main(self, client): - return client.cinder_client.volume_type_access.list(**self.args) - - -class VolumeTypeAccessAdd(task_manager.Task): - def main(self, client): - return client.cinder_client.volume_type_access.add_project_access( - **self.args) - - -class VolumeTypeAccessRemove(task_manager.Task): - def main(self, client): - return client.cinder_client.volume_type_access.remove_project_access( - **self.args) - - -class VolumeCreate(task_manager.Task): - def main(self, client): - return client.cinder_client.volumes.create(**self.args) - - class VolumeDelete(task_manager.Task): def main(self, client): client.cinder_client.volumes.delete(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index cc481adda..8b3213b22 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1727,7 +1727,8 @@ def list_volume_types(self, get_extra=True): """ with _utils.shade_exceptions("Error fetching volume_type list"): return self._normalize_volume_types( - self.manager.submit_task(_tasks.VolumeTypeList())) + self._volume_client.get( + '/types', params=dict(is_public='None'))) @_utils.cache_on_arguments() def list_availability_zone_names(self, unavailable=False): @@ -3863,7 +3864,6 @@ def create_volume( :raises: OpenStackCloudTimeout if wait time exceeded. :raises: OpenStackCloudException on operation error. """ - if image: image_obj = self.get_image(image) if not image_obj: @@ -3873,9 +3873,10 @@ def create_volume( image=image)) kwargs['imageRef'] = image_obj['id'] kwargs = self._get_volume_kwargs(kwargs) + kwargs['size'] = size with _utils.shade_exceptions("Error in creating volume"): - volume = self.manager.submit_task(_tasks.VolumeCreate( - size=size, **kwargs)) + volume = self._volume_client.post( + '/volumes', json=dict(volume=kwargs)) self.list_volumes.invalidate(self) if volume['status'] == 'error': diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 6eeba0220..c23366b26 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -2014,9 +2014,9 @@ def get_volume_type_access(self, name_or_id): "Unable to get volume type access {name}".format( name=name_or_id)): return self._normalize_volume_type_accesses( - self.manager.submit_task( - _tasks.VolumeTypeAccessList(volume_type=volume_type)) - ) + self._volume_client.get( + '/types/{id}/os-volume-type-access'.format( + id=volume_type.id))) def add_volume_type_access(self, name_or_id, project_id): """Grant access on a volume_type to a project. @@ -2032,14 +2032,14 @@ def add_volume_type_access(self, name_or_id, project_id): if not volume_type: raise OpenStackCloudException( "VolumeType not found: %s" % name_or_id) - with _utils.shade_exceptions( - "Unable to authorize {project} " - "to use volume type {name}".format( - name=name_or_id, project=project_id - )): - self.manager.submit_task( - _tasks.VolumeTypeAccessAdd( - volume_type=volume_type, project=project_id)) + with _utils.shade_exceptions(): + payload = {'project': project_id} + self._volume_client.post( + '/types/{id}/action'.format(id=volume_type.id), + json=dict(addProjectAccess=payload), + error_message="Unable to authorize {project} " + "to use volume type {name}".format( + name=name_or_id, project=project_id)) def remove_volume_type_access(self, name_or_id, project_id): """Revoke access on a volume_type to a project. @@ -2053,14 +2053,14 @@ def remove_volume_type_access(self, name_or_id, project_id): if not volume_type: raise OpenStackCloudException( "VolumeType not found: %s" % name_or_id) - with _utils.shade_exceptions( - "Unable to revoke {project} " - "to use volume type {name}".format( - name=name_or_id, project=project_id - )): - self.manager.submit_task( - _tasks.VolumeTypeAccessRemove( - volume_type=volume_type, project=project_id)) + with _utils.shade_exceptions(): + payload = {'project': project_id} + self._volume_client.post( + '/types/{id}/action'.format(id=volume_type.id), + json=dict(removeProjectAccess=payload), + error_message="Unable to revoke {project} " + "to use volume type {name}".format( + name=name_or_id, project=project_id)) def set_compute_quotas(self, name_or_id, **kwargs): """ Set a quota in a project diff --git a/shade/tests/functional/test_volume_type.py b/shade/tests/functional/test_volume_type.py index b04284282..e1c5a66f3 100644 --- a/shade/tests/functional/test_volume_type.py +++ b/shade/tests/functional/test_volume_type.py @@ -19,7 +19,7 @@ Functional tests for `shade` block storage methods. """ import testtools -from shade.exc import OpenStackCloudException +from shade import exc from shade.tests.functional import base @@ -79,7 +79,7 @@ def test_add_volume_type_access_missing_project(self): def test_add_volume_type_access_missing_volume(self): with testtools.ExpectedException( - OpenStackCloudException, + exc.OpenStackCloudException, "VolumeType not found.*" ): self.operator_cloud.add_volume_type_access( @@ -88,7 +88,7 @@ def test_add_volume_type_access_missing_volume(self): def test_remove_volume_type_access_missing_volume(self): with testtools.ExpectedException( - OpenStackCloudException, + exc.OpenStackCloudException, "VolumeType not found.*" ): self.operator_cloud.remove_volume_type_access( @@ -97,7 +97,7 @@ def test_remove_volume_type_access_missing_volume(self): def test_add_volume_type_access_bad_project(self): with testtools.ExpectedException( - OpenStackCloudException, + exc.OpenStackCloudBadRequest, "Unable to authorize.*" ): self.operator_cloud.add_volume_type_access( @@ -106,7 +106,7 @@ def test_add_volume_type_access_bad_project(self): def test_remove_volume_type_access_missing_project(self): with testtools.ExpectedException( - OpenStackCloudException, + exc.OpenStackCloudURINotFound, "Unable to revoke.*" ): self.operator_cloud.remove_volume_type_access( diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 79879004c..f9e7a7e7a 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -268,10 +268,6 @@ def now_deleting(request, context): uri=self.get_mock_url( 'volumev2', 'public', append=['volumes']), json={'volume': fake_vol_creating}), - dict(method='GET', - uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', _id]), - json={'volume': [fake_volb4, fake_vol_creating]}), dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', 'detail']), From bcc7ea694ab78e0903c3a2075102afe3bb47eda2 Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Mon, 1 May 2017 01:42:49 +0000 Subject: [PATCH 1485/3836] Use REST API for volume delete and detach calls Change-Id: I0267899b4fb1dbb277ef969663ff82eadf465879 Signed-off-by: Rosario Di Somma --- shade/_tasks.py | 10 ---------- shade/openstackcloud.py | 19 ++++++++++--------- shade/tests/unit/test_volume.py | 2 +- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index ba6510062..424c36dc8 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -212,16 +212,6 @@ def main(self, client): return client.nova_client.images.list() -class VolumeDelete(task_manager.Task): - def main(self, client): - client.cinder_client.volumes.delete(**self.args) - - -class VolumeDetach(task_manager.Task): - def main(self, client): - client.nova_client.volumes.delete_server_volume(**self.args) - - class VolumeAttach(task_manager.Task): def main(self, client): return client.nova_client.volumes.create_server_volume(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 8b3213b22..3f3f5e08b 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3923,9 +3923,9 @@ def delete_volume(self, name_or_id=None, wait=True, timeout=None): with _utils.shade_exceptions("Error in deleting volume"): try: - self.manager.submit_task( - _tasks.VolumeDelete(volume=volume['id'])) - except cinder_exceptions.NotFound: + self._volume_client.delete( + 'volumes/{id}'.format(id=volume['id'])) + except OpenStackCloudURINotFound: self.log.debug( "Volume {id} not found when deleting. Ignoring.".format( id=volume['id'])) @@ -3987,12 +3987,13 @@ def detach_volume(self, server, volume, wait=True, timeout=None): :raises: OpenStackCloudException on operation error. """ - with _utils.shade_exceptions( - "Error detaching volume {volume} from server {server}".format( - volume=volume['id'], server=server['id'])): - self.manager.submit_task( - _tasks.VolumeDetach(attachment_id=volume['id'], - server_id=server['id'])) + with _utils.shade_exceptions(): + self._compute_client.delete( + 'servers/{server_id}/os-volume_attachments/{volume_id}'.format( + server_id=server['id'], volume_id=volume['id']), + error_message="Error detaching volume {volume} " + "from server {server}".format( + volume=volume['id'], server=server['id'])) if wait: for count in _utils._iterate_timeout( diff --git a/shade/tests/unit/test_volume.py b/shade/tests/unit/test_volume.py index 909073896..a02cd413b 100644 --- a/shade/tests/unit/test_volume.py +++ b/shade/tests/unit/test_volume.py @@ -189,7 +189,7 @@ def test_detach_volume_exception(self): 'os-volume_attachments', volume['id']]), status_code=404)]) with testtools.ExpectedException( - shade.OpenStackCloudException, + shade.OpenStackCloudURINotFound, "Error detaching volume %s from server %s" % ( volume['id'], server['id']) ): From 86a5f3bc89f10380b72410cf477c1da5e2b691dc Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 1 May 2017 13:40:57 +0000 Subject: [PATCH 1486/3836] Updated from global requirements Change-Id: Ie2e891d468301051030a79c0158228d3ab58d5f9 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8bd3069ec..91cf8a6f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,6 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 six>=1.9.0 # MIT stevedore>=1.20.0 # Apache-2.0 -os-client-config>=1.22.0 # Apache-2.0 +os-client-config>=1.27.0 # Apache-2.0 keystoneauth1>=2.18.0 # Apache-2.0 deprecation>=1.0 # Apache-2.0 From cbc233d0e70fb603004c16cace3698f9ef3bbe4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Mon, 1 May 2017 14:00:54 +0000 Subject: [PATCH 1487/3836] Replace neutronclient with REST API calls in security groups commands All security groups and security group rules related commands to Neutron like list/create/update/delete are now made via keystoneauth Change-Id: I875e8780ac28cbeb46c80f67629f98a86fd57160 --- shade/_tasks.py | 30 ------------------ shade/openstackcloud.py | 68 ++++++++++++++++------------------------- 2 files changed, 27 insertions(+), 71 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index ba6510062..27cbe8ddf 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -262,36 +262,6 @@ def main(self, client): return client.cinder_client.volume_snapshots.delete(**self.args) -class NeutronSecurityGroupList(task_manager.Task): - def main(self, client): - return client.neutron_client.list_security_groups(**self.args) - - -class NeutronSecurityGroupCreate(task_manager.Task): - def main(self, client): - return client.neutron_client.create_security_group(**self.args) - - -class NeutronSecurityGroupDelete(task_manager.Task): - def main(self, client): - return client.neutron_client.delete_security_group(**self.args) - - -class NeutronSecurityGroupUpdate(task_manager.Task): - def main(self, client): - return client.neutron_client.update_security_group(**self.args) - - -class NeutronSecurityGroupRuleCreate(task_manager.Task): - def main(self, client): - return client.neutron_client.create_security_group_rule(**self.args) - - -class NeutronSecurityGroupRuleDelete(task_manager.Task): - def main(self, client): - return client.neutron_client.delete_security_group_rule(**self.args) - - class NeutronFloatingIPList(task_manager.Task): def main(self, client): return client.neutron_client.list_floatingips(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 8b3213b22..ff55eb0fc 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1926,11 +1926,9 @@ def list_security_groups(self, filters=None): # Handle neutron security groups if self._use_neutron_secgroups(): # Neutron returns dicts, so no need to convert objects here. - with _utils.neutron_exceptions( - "Error fetching security group list"): - groups = self.manager.submit_task( - _tasks.NeutronSecurityGroupList(**filters) - )['security_groups'] + groups = self._network_client.get( + '/security-groups.json', params=filters, + error_message="Error fetching security group list") # Handle nova security groups else: @@ -6756,13 +6754,11 @@ def create_security_group(self, name, description): group = None if self._use_neutron_secgroups(): - with _utils.neutron_exceptions( - "Error creating security group {0}".format(name)): - group = self.manager.submit_task( - _tasks.NeutronSecurityGroupCreate( - body=dict(security_group=dict(name=name, - description=description)) - ))['security_group'] + group = self._network_client.post( + '/security-groups.json', + json={'security_group': + {'name': name, 'description': description}}, + error_message="Error creating security group {0}".format(name)) else: group = self._compute_client.post( @@ -6798,13 +6794,11 @@ def delete_security_group(self, name_or_id): return False if self._use_neutron_secgroups(): - with _utils.neutron_exceptions( - "Error deleting security group {0}".format(name_or_id)): - self.manager.submit_task( - _tasks.NeutronSecurityGroupDelete( - security_group=secgroup['id'] - ) - ) + self._network_client.delete( + '/security-groups/{sg_id}.json'.format(sg_id=secgroup['id']), + error_message="Error deleting security group {0}".format( + name_or_id) + ) return True else: @@ -6837,14 +6831,11 @@ def update_security_group(self, name_or_id, **kwargs): "Security group %s not found." % name_or_id) if self._use_neutron_secgroups(): - with _utils.neutron_exceptions( - "Error updating security group {0}".format(name_or_id)): - group = self.manager.submit_task( - _tasks.NeutronSecurityGroupUpdate( - security_group=group['id'], - body={'security_group': kwargs}) - )['security_group'] - + group = self._network_client.put( + '/security-groups/{sg_id}.json'.format(sg_id=group['id']), + json={'security_group': kwargs}, + error_message="Error updating security group {0}".format( + name_or_id)) else: for key in ('name', 'description'): kwargs.setdefault(key, group[key]) @@ -6931,13 +6922,11 @@ def create_security_group_rule(self, 'ethertype': ethertype } - with _utils.neutron_exceptions( - "Error creating security group rule"): - rule = self.manager.submit_task( - _tasks.NeutronSecurityGroupRuleCreate( - body={'security_group_rule': rule_def}) - ) - return self._normalize_secgroup_rule(rule['security_group_rule']) + rule = self._network_client.post( + '/security-group-rules.json', + json={'security_group_rule': rule_def}, + error_message="Error creating security group rule") + return self._normalize_secgroup_rule(rule) else: # NOTE: Neutron accepts None for protocol. Nova does not. @@ -6999,13 +6988,10 @@ def delete_security_group_rule(self, rule_id): if self._use_neutron_secgroups(): try: - with _utils.neutron_exceptions( - "Error deleting security group rule " - "{0}".format(rule_id)): - self.manager.submit_task( - _tasks.NeutronSecurityGroupRuleDelete( - security_group_rule=rule_id) - ) + self._network_client.delete( + '/security-group-rules/{sg_id}.json'.format(sg_id=rule_id), + error_message="Error deleting security group rule " + "{0}".format(rule_id)) except OpenStackCloudResourceNotFound: return False return True From 77bac2b9d03c1a7a3b53cf4ae388354ffc553568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Mon, 1 May 2017 11:27:08 +0000 Subject: [PATCH 1488/3836] Replace neutronclient with REST API calls in quotas commands All quotas related commands to Neutron like list/create/update/delete are now made via keystoneauth Change-Id: I5f0cb3e174c2f5453d2fe760bb39b2140fe1f201 --- shade/_tasks.py | 15 --------------- shade/operatorcloud.py | 24 +++++++++++++----------- 2 files changed, 13 insertions(+), 26 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index ba6510062..469866bd5 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -647,21 +647,6 @@ def main(self, client): return client.cinder_client.quotas.delete(**self.args) -class NeutronQuotasSet(task_manager.Task): - def main(self, client): - return client.neutron_client.update_quota(**self.args) - - -class NeutronQuotasGet(task_manager.Task): - def main(self, client): - return client.neutron_client.show_quota(**self.args)['quota'] - - -class NeutronQuotasDelete(task_manager.Task): - def main(self, client): - return client.neutron_client.delete_quota(**self.args) - - class NovaLimitsGet(task_manager.Task): def main(self, client): return client.nova_client.limits.get(**self.args).to_dict() diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index c23366b26..5b54da3ac 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -2222,11 +2222,11 @@ def set_network_quotas(self, name_or_id, **kwargs): if not proj: raise OpenStackCloudException("project does not exist") - body = {'quota': kwargs} - with _utils.neutron_exceptions("network client call failed"): - self.manager.submit_task( - _tasks.NeutronQuotasSet(tenant_id=proj.id, - body=body)) + self._network_client.put( + '/quotas/{project_id}.json'.format(project_id=proj.id), + json={'quota': kwargs}, + error_message=("Error setting Neutron's quota for " + "project {0}".format(proj.id))) def get_network_quotas(self, name_or_id): """ Get network quotas for a project @@ -2239,9 +2239,10 @@ def get_network_quotas(self, name_or_id): proj = self.get_project(name_or_id) if not proj: raise OpenStackCloudException("project does not exist") - with _utils.neutron_exceptions("network client call failed"): - return self.manager.submit_task( - _tasks.NeutronQuotasGet(tenant_id=proj.id)) + return self._network_client.get( + '/quotas/{project_id}.json'.format(project_id=proj.id), + error_message=("Error fetching Neutron's quota for " + "project {0}".format(proj.id))) def delete_network_quotas(self, name_or_id): """ Delete network quotas for a project @@ -2255,9 +2256,10 @@ def delete_network_quotas(self, name_or_id): proj = self.get_project(name_or_id) if not proj: raise OpenStackCloudException("project does not exist") - with _utils.neutron_exceptions("network client call failed"): - return self.manager.submit_task( - _tasks.NeutronQuotasDelete(tenant_id=proj.id)) + self._network_client.delete( + '/quotas/{project_id}.json'.format(project_id=proj.id), + error_message=("Error deleting Neutron's quota for " + "project {0}".format(proj.id))) def list_magnum_services(self): """List all Magnum services. From a2aad709fb76a46716311193868d5358c2555e3e Mon Sep 17 00:00:00 2001 From: Mike Perez Date: Tue, 2 May 2017 10:07:34 -0700 Subject: [PATCH 1489/3836] Removing unsed fake methods and classes These are being mocked otherways now. Change-Id: Ica242ff6a48508f616ca5dde90a77d588f8d2bdb --- shade/tests/fakes.py | 98 -------------------------------------------- 1 file changed, 98 deletions(-) diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index f153b4f85..a46fa1061 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -210,40 +210,6 @@ def make_fake_image( u'protected': False} -class FakeEndpoint(object): - def __init__(self, id, service_id, region, publicurl, internalurl=None, - adminurl=None): - self.id = id - self.service_id = service_id - self.region = region - self.publicurl = publicurl - self.internalurl = internalurl - self.adminurl = adminurl - - -class FakeEndpointv3(object): - def __init__(self, id, service_id, region, url, interface=None): - self.id = id - self.service_id = service_id - self.region = region - self.url = url - self.interface = interface - - -class FakeFlavor(object): - def __init__(self, id, name, ram, extra_specs=None): - self.id = id - self.name = name - self.ram = ram - # Leave it unset if we don't pass it in to test that normalize_ works - # but we also have to be able to pass one in to deal with mocks - if extra_specs: - self.extra_specs = extra_specs - - def get_keys(self): - return {} - - class FakeFloatingIP(object): def __init__(self, id, pool, ip, fixed_ip, instance_id): self.id = id @@ -253,18 +219,6 @@ def __init__(self, id, pool, ip, fixed_ip, instance_id): self.instance_id = instance_id -class FakeFloatingIPPool(object): - def __init__(self, id, name): - self.id = id - self.name = name - - -class FakeProject(object): - def __init__(self, id, domain_id=None): - self.id = id - self.domain_id = domain_id or 'default' - - class FakeServer(object): def __init__( self, id, name, status, addresses=None, @@ -304,27 +258,6 @@ def __init__(self, id, name, policies): self.policies = policies -class FakeService(object): - def __init__(self, id, name, type, service_type, description='', - enabled=True): - self.id = id - self.name = name - self.type = type - self.service_type = service_type - self.description = description - self.enabled = enabled - - -class FakeUser(object): - def __init__(self, id, email, name, domain_id=None, description=None): - self.id = id - self.email = email - self.name = name - self.description = description - if domain_id is not None: - self.domain_id = domain_id - - class FakeVolume(object): def __init__( self, id, status, name, attachments=[], @@ -404,43 +337,12 @@ def __init__(self, id, name, public_key): self.public_key = public_key -class FakeDomain(object): - def __init__(self, id, name, description, enabled): - self.id = id - self.name = name - self.description = description - self.enabled = enabled - - -class FakeRole(object): - def __init__(self, id, name): - self.id = id - self.name = name - - -class FakeGroup(object): - def __init__(self, id, name, description, domain_id=None): - self.id = id - self.name = name - self.description = description - self.domain_id = domain_id or 'default' - - class FakeHypervisor(object): def __init__(self, id, hostname): self.id = id self.hypervisor_hostname = hostname -class FakeStack(object): - def __init__(self, id, name, description=None, status='CREATE_COMPLETE'): - self.id = id - self.name = name - self.stack_name = name - self.stack_description = description - self.stack_status = status - - class FakeZone(object): def __init__(self, id, name, type_, email, description, ttl, masters): From f7b1eb5c60f83374fa8f5e3d5bad188a7565beeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Tue, 2 May 2017 20:33:34 +0000 Subject: [PATCH 1490/3836] Don't get ports info from unavailable neutron service In shade.meta._get_supplemental_addresses() Neutron API was called to get ports list even if cloud.has_service('neutron') returned False. Now it's done only if Neutron service is available. Change-Id: I30ff7a8d049ef0bac025ac21185deb50760144cf Closes-Bug: https://storyboard.openstack.org/?#!/story/2001011 --- shade/meta.py | 3 ++- shade/tests/unit/test_meta.py | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/shade/meta.py b/shade/meta.py index cfc01996e..87d4884ff 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -344,7 +344,8 @@ def _get_supplemental_addresses(cloud, server): try: # Don't bother doing this before the server is active, it's a waste # of an API call while polling for a server to come up - if cloud._has_floating_ips() and server['status'] == 'ACTIVE': + if (cloud.has_service('network') and cloud._has_floating_ips() and + server['status'] == 'ACTIVE'): for port in cloud.search_ports( filters=dict(device_id=server['id'])): for fip in cloud.search_floating_ips( diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 292a68f45..2f12fd11c 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -300,17 +300,19 @@ def test_get_server_private_ip(self): PRIVATE_V4, meta.get_server_private_ip(srv, self.cloud)) self.assert_calls() + @mock.patch.object(shade.OpenStackCloud, 'has_service') @mock.patch.object(shade.OpenStackCloud, 'get_volumes') @mock.patch.object(shade.OpenStackCloud, 'get_image_name') @mock.patch.object(shade.OpenStackCloud, 'get_flavor_name') def test_get_server_private_ip_devstack( self, mock_get_flavor_name, mock_get_image_name, - mock_get_volumes): + mock_get_volumes, mock_has_service): mock_get_image_name.return_value = 'cirros-0.3.4-x86_64-uec' mock_get_flavor_name.return_value = 'm1.tiny' mock_get_volumes.return_value = [] + mock_has_service.return_value = True self.register_uris([ dict(method='GET', @@ -461,16 +463,18 @@ def test_get_server_cloud_no_fips( self.assertEqual(PRIVATE_V4, srv['private_v4']) self.assert_calls() + @mock.patch.object(shade.OpenStackCloud, 'has_service') @mock.patch.object(shade.OpenStackCloud, 'get_volumes') @mock.patch.object(shade.OpenStackCloud, 'get_image_name') @mock.patch.object(shade.OpenStackCloud, 'get_flavor_name') def test_get_server_cloud_missing_fips( self, mock_get_flavor_name, mock_get_image_name, - mock_get_volumes): + mock_get_volumes, mock_has_service): mock_get_image_name.return_value = 'cirros-0.3.4-x86_64-uec' mock_get_flavor_name.return_value = 'm1.tiny' mock_get_volumes.return_value = [] + mock_has_service.return_value = True self.register_uris([ dict(method='GET', From 004722504b99edde1543cb62c24379e674e49ff6 Mon Sep 17 00:00:00 2001 From: Miguel Angel Ajo Date: Wed, 17 Aug 2016 16:23:59 +0200 Subject: [PATCH 1491/3836] Add direction field to QoS bandwidth limit. This patch enables the direction ('ingress'/'egress') field on the QoS bandwidth limit rule object and CRUD commands. Closes-Bug: 1614121 Depends-On: Ia13568879c2b6f80fb190ccafe7e19ca05b0c6a8 Change-Id: I90c412a5c8757b3ffe8abfc1165a70bdb8744702 --- openstack/network/v2/qos_bandwidth_limit_rule.py | 3 +-- .../network/v2/test_qos_bandwidth_limit_rule.py | 14 +++++++++----- .../network/v2/test_qos_bandwidth_limit_rule.py | 6 ++---- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/openstack/network/v2/qos_bandwidth_limit_rule.py b/openstack/network/v2/qos_bandwidth_limit_rule.py index 62f915648..ba5c32d8e 100644 --- a/openstack/network/v2/qos_bandwidth_limit_rule.py +++ b/openstack/network/v2/qos_bandwidth_limit_rule.py @@ -34,6 +34,5 @@ class QoSBandwidthLimitRule(resource.Resource): max_kbps = resource.Body('max_kbps') #: Maximum burst bandwidth in kbps. max_burst_kbps = resource.Body('max_burst_kbps') - # NOTE(ralonsoh): to be implemented in bug 1560961 #: Traffic direction from the tenant point of view ('egress', 'ingress'). - # direction = resource.prop('direction') + direction = resource.Body('direction') diff --git a/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py b/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py index 84e7aee86..6d8a9253e 100644 --- a/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py @@ -28,10 +28,8 @@ class TestQoSBandwidthLimitRule(base.BaseFunctionalTest): RULE_MAX_KBPS_NEW = 1800 RULE_MAX_BURST_KBPS = 1100 RULE_MAX_BURST_KBPS_NEW = 1300 - # NOTE(ralonsoh): to be implemented in bug 1560961. - # New checks must be added. - # RULE_DIRECTION = 'egress' - # RULE_DIRECTION_NEW = 'ingress' + RULE_DIRECTION = 'egress' + RULE_DIRECTION_NEW = 'ingress' @classmethod def setUpClass(cls): @@ -45,11 +43,13 @@ def setUpClass(cls): qos_rule = cls.conn.network.create_qos_bandwidth_limit_rule( cls.QOS_POLICY_ID, max_kbps=cls.RULE_MAX_KBPS, max_burst_kbps=cls.RULE_MAX_BURST_KBPS, + direction=cls.RULE_DIRECTION, ) assert isinstance(qos_rule, _qos_bandwidth_limit_rule.QoSBandwidthLimitRule) cls.assertIs(cls.RULE_MAX_KBPS, qos_rule.max_kbps) cls.assertIs(cls.RULE_MAX_BURST_KBPS, qos_rule.max_burst_kbps) + cls.assertIs(cls.RULE_DIRECTION, qos_rule.direction) cls.RULE_ID = qos_rule.id @classmethod @@ -68,6 +68,7 @@ def test_find(self): self.assertEqual(self.RULE_ID, sot.id) self.assertEqual(self.RULE_MAX_KBPS, sot.max_kbps) self.assertEqual(self.RULE_MAX_BURST_KBPS, sot.max_burst_kbps) + self.assertEqual(self.RULE_DIRECTION, sot.direction) def test_get(self): sot = self.conn.network.get_qos_bandwidth_limit_rule( @@ -77,6 +78,7 @@ def test_get(self): self.assertEqual(self.QOS_POLICY_ID, sot.qos_policy_id) self.assertEqual(self.RULE_MAX_KBPS, sot.max_kbps) self.assertEqual(self.RULE_MAX_BURST_KBPS, sot.max_burst_kbps) + self.assertEqual(self.RULE_DIRECTION, sot.direction) def test_list(self): rule_ids = [o.id for o in @@ -89,6 +91,8 @@ def test_update(self): self.RULE_ID, self.QOS_POLICY_ID, max_kbps=self.RULE_MAX_KBPS_NEW, - max_burst_kbps=self.RULE_MAX_BURST_KBPS_NEW) + max_burst_kbps=self.RULE_MAX_BURST_KBPS_NEW, + direction=self.RULE_DIRECTION_NEW) self.assertEqual(self.RULE_MAX_KBPS_NEW, sot.max_kbps) self.assertEqual(self.RULE_MAX_BURST_KBPS_NEW, sot.max_burst_kbps) + self.assertEqual(self.RULE_DIRECTION_NEW, sot.direction) diff --git a/openstack/tests/unit/network/v2/test_qos_bandwidth_limit_rule.py b/openstack/tests/unit/network/v2/test_qos_bandwidth_limit_rule.py index 56b7446de..f6fc9fd3c 100644 --- a/openstack/tests/unit/network/v2/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/unit/network/v2/test_qos_bandwidth_limit_rule.py @@ -20,8 +20,7 @@ 'qos_policy_id': 'qos-policy-' + uuid.uuid4().hex, 'max_kbps': 1500, 'max_burst_kbps': 1200, - # NOTE(ralonsoh): to be implemented in bug 1560961 - # 'direction': 'egress', + 'direction': 'egress', } @@ -47,5 +46,4 @@ def test_make_it(self): self.assertEqual(EXAMPLE['qos_policy_id'], sot.qos_policy_id) self.assertEqual(EXAMPLE['max_kbps'], sot.max_kbps) self.assertEqual(EXAMPLE['max_burst_kbps'], sot.max_burst_kbps) - # NOTE(ralonsoh): to be implemented in bug 1560961 - # self.assertEqual(EXAMPLE['direction'], sot.direction) + self.assertEqual(EXAMPLE['direction'], sot.direction) From f52448d8cb4af6ee46434f34c87368eca2f3544e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Tue, 2 May 2017 20:45:29 +0000 Subject: [PATCH 1492/3836] Replace neutronclient with REST API calls in ports commands All ports related commands to Neutron like list/create/update/delete are now made via keystoneauth Change-Id: I145b9347cf0be5e459bb025f9d92958176e6e7a2 --- shade/_tasks.py | 20 -------------------- shade/openstackcloud.py | 30 +++++++++++++++--------------- 2 files changed, 15 insertions(+), 35 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 424c36dc8..839abc7df 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -327,26 +327,6 @@ def main(self, client): return client.neutron_client.update_floatingip(**self.args) -class PortList(task_manager.Task): - def main(self, client): - return client.neutron_client.list_ports(**self.args) - - -class PortCreate(task_manager.Task): - def main(self, client): - return client.neutron_client.create_port(**self.args) - - -class PortUpdate(task_manager.Task): - def main(self, client): - return client.neutron_client.update_port(**self.args) - - -class PortDelete(task_manager.Task): - def main(self, client): - return client.neutron_client.delete_port(**self.args) - - class MachineCreate(task_manager.Task): def main(self, client): return client.ironic_client.node.create(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 3f3f5e08b..5d200819d 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1701,9 +1701,9 @@ def list_ports(self, filters=None): return self._ports def _list_ports(self, filters): - with _utils.neutron_exceptions("Error fetching port list"): - return self.manager.submit_task( - _tasks.PortList(**filters))['ports'] + return self._network_client.get( + "/ports.json", params=filters, + error_message="Error fetching port list") @_utils.cache_on_arguments(should_cache_fn=_no_pending_volumes) def list_volumes(self, cache=True): @@ -6648,10 +6648,11 @@ def create_port(self, network_id, **kwargs): """ kwargs['network_id'] = network_id - with _utils.neutron_exceptions( - "Error creating port for network {0}".format(network_id)): - return self.manager.submit_task( - _tasks.PortCreate(body={'port': kwargs}))['port'] + return self._network_client.post( + "/ports.json", json={'port': kwargs}, + error_message="Error creating port for network {0}".format( + network_id)) + @_utils.valid_kwargs('name', 'admin_state_up', 'fixed_ips', 'security_groups', 'allowed_address_pairs', @@ -6711,11 +6712,10 @@ def update_port(self, name_or_id, **kwargs): raise OpenStackCloudException( "failed to find port '{port}'".format(port=name_or_id)) - with _utils.neutron_exceptions( - "Error updating port {0}".format(name_or_id)): - return self.manager.submit_task( - _tasks.PortUpdate( - port=port['id'], body={'port': kwargs}))['port'] + return self._network_client.put( + "/ports/{port_id}.json".format(port_id=port['id']), + json={"port": kwargs}, + error_message="Error updating port {0}".format(name_or_id)) def delete_port(self, name_or_id): """Delete a port @@ -6731,9 +6731,9 @@ def delete_port(self, name_or_id): self.log.debug("Port %s not found for deleting", name_or_id) return False - with _utils.neutron_exceptions( - "Error deleting port {0}".format(name_or_id)): - self.manager.submit_task(_tasks.PortDelete(port=port['id'])) + self._network_client.delete( + "/ports/{port_id}.json".format(port_id=port['id']), + error_message="Error deleting port {0}".format(name_or_id)) return True def create_security_group(self, name, description): From 223e0d035c96fd8c53acde73c753b3135bcefda7 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 3 May 2017 12:23:17 +0000 Subject: [PATCH 1493/3836] Updated from global requirements Change-Id: I65bc8a31c61bca161cb0825b493cb88a50fa2b47 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 91cf8a6f5..b2a4bb5c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,5 +5,5 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 six>=1.9.0 # MIT stevedore>=1.20.0 # Apache-2.0 os-client-config>=1.27.0 # Apache-2.0 -keystoneauth1>=2.18.0 # Apache-2.0 +keystoneauth1>=2.20.0 # Apache-2.0 deprecation>=1.0 # Apache-2.0 From 70a74337a611c008afb02cc03c8b8512d7fa6c9e Mon Sep 17 00:00:00 2001 From: Akihiro Motoki Date: Wed, 3 May 2017 22:11:04 +0000 Subject: [PATCH 1494/3836] Specify alternate_id in network quota In network quota object, tenant_id is a unique key of the resource. alternate_id must be specified in the resource definition. Change-Id: Ia77a3711a08ee684fafcc76a8e1bb7d7f32ff1c1 Closes-Bug: #1687202 --- openstack/network/v2/quota.py | 2 +- openstack/tests/functional/network/v2/test_quota.py | 3 --- openstack/tests/unit/network/v2/test_quota.py | 8 ++++++++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/openstack/network/v2/quota.py b/openstack/network/v2/quota.py index a77f5aac4..c37e712ad 100644 --- a/openstack/network/v2/quota.py +++ b/openstack/network/v2/quota.py @@ -44,7 +44,7 @@ class Quota(resource.Resource): #: The maximum amount of ports you can create. *Type: int* ports = resource.Body('port', type=int) #: The ID of the project these quota values are for. - project_id = resource.Body('tenant_id') + project_id = resource.Body('tenant_id', alternate_id=True) #: The maximum amount of RBAC policies you can create. *Type: int* rbac_policies = resource.Body('rbac_policy', type=int) #: The maximum amount of routers you can create. *Type: int* diff --git a/openstack/tests/functional/network/v2/test_quota.py b/openstack/tests/functional/network/v2/test_quota.py index 14be76dde..9fa100453 100644 --- a/openstack/tests/functional/network/v2/test_quota.py +++ b/openstack/tests/functional/network/v2/test_quota.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import unittest - from openstack.tests.functional import base @@ -22,7 +20,6 @@ def test_list(self): self.assertIsNotNone(qot.project_id) self.assertIsNotNone(qot.networks) - @unittest.skip('bug/1687202') def test_set(self): attrs = {'networks': 123456789} for project_quota in self.conn.network.quotas(): diff --git a/openstack/tests/unit/network/v2/test_quota.py b/openstack/tests/unit/network/v2/test_quota.py index 26b18ca42..20f55effa 100644 --- a/openstack/tests/unit/network/v2/test_quota.py +++ b/openstack/tests/unit/network/v2/test_quota.py @@ -13,6 +13,7 @@ import testtools from openstack.network.v2 import quota +from openstack import resource2 as resource IDENTIFIER = 'IDENTIFIER' EXAMPLE = { @@ -73,6 +74,13 @@ def test_prepare_request(self): response = quota_obj._prepare_request() self.assertNotIn('id', response) + def test_alternate_id(self): + my_tenant_id = 'my-tenant-id' + body = {'tenant_id': my_tenant_id, 'network': 12345} + quota_obj = quota.Quota(**body) + self.assertEqual(my_tenant_id, + resource.Resource._get_id(quota_obj)) + class TestQuotaDefault(testtools.TestCase): From 3c62c90f45a62e6907f796340f25bde95ddc798c Mon Sep 17 00:00:00 2001 From: zengjianfang Date: Thu, 4 May 2017 15:12:06 +0800 Subject: [PATCH 1495/3836] Use https instead of http in cluster examples Change-Id: I674ec63232216729e953f6f5b11403b56eeb787d --- examples/cluster/policy.py | 2 +- examples/cluster/policy_type.py | 2 +- examples/cluster/profile.py | 2 +- examples/cluster/profile_type.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/cluster/policy.py b/examples/cluster/policy.py index 051b838db..0f37820d2 100644 --- a/examples/cluster/policy.py +++ b/examples/cluster/policy.py @@ -14,7 +14,7 @@ Managing policies in the Cluster service. For a full guide see -http://developer.openstack.org/sdks/python/openstacksdk/users/guides/cluster.html +https://developer.openstack.org/sdks/python/openstacksdk/users/guides/cluster.html """ diff --git a/examples/cluster/policy_type.py b/examples/cluster/policy_type.py index f3bb03692..447ecf265 100644 --- a/examples/cluster/policy_type.py +++ b/examples/cluster/policy_type.py @@ -14,7 +14,7 @@ Managing policy types in the Cluster service. For a full guide see -http://developer.openstack.org/sdks/python/openstacksdk/users/guides/cluster.html +https://developer.openstack.org/sdks/python/openstacksdk/users/guides/cluster.html """ diff --git a/examples/cluster/profile.py b/examples/cluster/profile.py index 6f1709fbb..0fad312c5 100644 --- a/examples/cluster/profile.py +++ b/examples/cluster/profile.py @@ -19,7 +19,7 @@ Managing profiles in the Cluster service. For a full guide see -http://developer.openstack.org/sdks/python/openstacksdk/users/guides/cluster.html +https://developer.openstack.org/sdks/python/openstacksdk/users/guides/cluster.html """ diff --git a/examples/cluster/profile_type.py b/examples/cluster/profile_type.py index e1d8ff7f7..8a3e99550 100644 --- a/examples/cluster/profile_type.py +++ b/examples/cluster/profile_type.py @@ -14,7 +14,7 @@ Managing profile types in the Cluster service. For a full guide see -http://developer.openstack.org/sdks/python/openstacksdk/users/guides/cluster.html +https://developer.openstack.org/sdks/python/openstacksdk/users/guides/cluster.html """ From 689d3bb9ddb69897238e48eb1dfb13763d2a3ec9 Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Tue, 2 May 2017 13:18:35 +0000 Subject: [PATCH 1496/3836] Use REST API for volume attach and volume backup calls * Also remove unneeded call to shade_exception in the detach_volume method. Change-Id: Ic8f63453ab11e39a28fbfe5027f95ba41c59fadc Signed-off-by: Rosario Di Somma --- shade/_tasks.py | 20 --------- shade/openstackcloud.py | 78 ++++++++++++++++++--------------- shade/tests/unit/test_volume.py | 2 +- 3 files changed, 44 insertions(+), 56 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 424c36dc8..36997724b 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -212,11 +212,6 @@ def main(self, client): return client.nova_client.images.list() -class VolumeAttach(task_manager.Task): - def main(self, client): - return client.nova_client.volumes.create_server_volume(**self.args) - - class VolumeSnapshotCreate(task_manager.Task): def main(self, client): return client.cinder_client.volume_snapshots.create(**self.args) @@ -232,21 +227,6 @@ def main(self, client): return client.cinder_client.volume_snapshots.list(**self.args) -class VolumeBackupList(task_manager.Task): - def main(self, client): - return client.cinder_client.backups.list(**self.args) - - -class VolumeBackupCreate(task_manager.Task): - def main(self, client): - return client.cinder_client.backups.create(**self.args) - - -class VolumeBackupDelete(task_manager.Task): - def main(self, client): - return client.cinder_client.backups.delete(**self.args) - - class VolumeSnapshotDelete(task_manager.Task): def main(self, client): return client.cinder_client.volume_snapshots.delete(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 3f3f5e08b..10c17c372 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3987,13 +3987,12 @@ def detach_volume(self, server, volume, wait=True, timeout=None): :raises: OpenStackCloudException on operation error. """ - with _utils.shade_exceptions(): - self._compute_client.delete( - 'servers/{server_id}/os-volume_attachments/{volume_id}'.format( - server_id=server['id'], volume_id=volume['id']), - error_message="Error detaching volume {volume} " - "from server {server}".format( - volume=volume['id'], server=server['id'])) + self._compute_client.delete( + '/servers/{server_id}/os-volume_attachments/{volume_id}'.format( + server_id=server['id'], volume_id=volume['id']), + error_message="Error detaching volume {volume} " + "from server {server}".format( + volume=volume['id'], server=server['id'])) if wait: for count in _utils._iterate_timeout( @@ -4052,14 +4051,16 @@ def attach_volume(self, server, volume, device=None, % (volume['id'], volume['status']) ) - with _utils.shade_exceptions( - "Error attaching volume {volume_id} to server " - "{server_id}".format(volume_id=volume['id'], - server_id=server['id'])): - vol_attachment = self.manager.submit_task( - _tasks.VolumeAttach(volume_id=volume['id'], - server_id=server['id'], - device=device)) + payload = {'volumeId': volume['id']} + if device: + payload['device'] = device + vol_attachment = self._compute_client.post( + '/servers/{server_id}/os-volume_attachments'.format( + server_id=server['id']), + json=dict(volumeAttachment=payload), + error_message="Error attaching volume {volume_id} to server " + "{server_id}".format(volume_id=volume['id'], + server_id=server['id'])) if wait: for count in _utils._iterate_timeout( @@ -4219,15 +4220,17 @@ def create_volume_backup(self, volume_id, name=None, description=None, :raises: OpenStackCloudTimeout if wait time exceeded. :raises: OpenStackCloudException on operation error. """ - with _utils.shade_exceptions( - "Error creating backup of volume {volume_id}".format( - volume_id=volume_id)): - backup = self.manager.submit_task( - _tasks.VolumeBackupCreate( - volume_id=volume_id, name=name, description=description, - force=force - ) - ) + payload = { + 'name': name, + 'volume_id': volume_id, + 'description': description, + 'force': force, + } + + backup = self._volume_client.post( + '/backups', json=dict(backup=payload), + error_message="Error creating backup of volume " + "{volume_id}".format(volume_id=volume_id)) if wait: backup_id = backup['id'] @@ -4284,11 +4287,10 @@ def list_volume_backups(self, detailed=True, search_opts=None): :returns: A list of volume backups ``munch.Munch``. """ - with _utils.shade_exceptions("Error getting a list of backups"): - return self._normalize_volume_backups( - self.manager.submit_task( - _tasks.VolumeBackupList( - detailed=detailed, search_opts=search_opts))) + endpoint = '/backups/detail' if detailed else '/backups' + return self._volume_client.get( + endpoint, params=search_opts, + error_message="Error getting a list of backups") def delete_volume_backup(self, name_or_id=None, force=False, wait=False, timeout=None): @@ -4309,12 +4311,18 @@ def delete_volume_backup(self, name_or_id=None, force=False, wait=False, if not volume_backup: return False - with _utils.shade_exceptions("Error in deleting volume backup"): - self.manager.submit_task( - _tasks.VolumeBackupDelete( - backup=volume_backup['id'], force=force - ) - ) + msg = "Error in deleting volume backup" + if force: + self._volume_client.post( + '/backups/{backup_id}/action'.format( + backup_id=volume_backup['id']), + json={'os-force_delete': None}, + error_message=msg) + else: + self._volume_client.delete( + '/backups/{backup_id}'.format( + backup_id=volume_backup['id']), + error_message=msg) if wait: msg = "Timeout waiting for the volume backup to be deleted." for count in _utils._iterate_timeout(timeout, msg): diff --git a/shade/tests/unit/test_volume.py b/shade/tests/unit/test_volume.py index a02cd413b..6cb7cb104 100644 --- a/shade/tests/unit/test_volume.py +++ b/shade/tests/unit/test_volume.py @@ -60,7 +60,7 @@ def test_attach_volume_exception(self): 'volumeId': vol['id']}}) )]) with testtools.ExpectedException( - shade.OpenStackCloudException, + shade.OpenStackCloudURINotFound, "Error attaching volume %s to server %s" % ( volume['id'], server['id']) ): From bc4a0c4066cda46b598537ff072f3e956d77a579 Mon Sep 17 00:00:00 2001 From: Rui Chen Date: Fri, 5 May 2017 14:39:05 +0800 Subject: [PATCH 1497/3836] Get endpoint versions with domain scope session Session.get_endpoint() failed when using domain scope authentcation, that cause by missing of project id in auth, that is regluar use case for admin to operate the resource (user, project, role and so on) of identity service with domain scope token. This patch fix the issue. Change-Id: I7f1f656918f6bfc1ea7333c49b97003022184d99 Closes-Bug: #1688241 --- openstack/session.py | 3 ++- openstack/tests/unit/test_session.py | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/openstack/session.py b/openstack/session.py index 3d3323cc9..f3a55e902 100644 --- a/openstack/session.py +++ b/openstack/session.py @@ -186,7 +186,8 @@ def _get_endpoint_versions(self, service_type, endpoint): # However, we do need to know that the project id was # previously there, so keep it. project_id = self.get_project_id() - project_id_location = parts.path.find(project_id) + # Domain scope token don't include project id + project_id_location = parts.path.find(project_id) if project_id else -1 if project_id_location > -1: usable_path = parts.path[slice(0, project_id_location)] needs_project_id = True diff --git a/openstack/tests/unit/test_session.py b/openstack/tests/unit/test_session.py index 528fe507d..82cdeeca0 100644 --- a/openstack/tests/unit/test_session.py +++ b/openstack/tests/unit/test_session.py @@ -328,6 +328,27 @@ def test__get_endpoint_versions_at_port(self): self.assertEqual(result, responses[0]) self.assertFalse(result.needs_project_id) + def test__get_endpoint_versions_with_domain_scope(self): + # This test covers a common case of services deployed under + # subdomains. Additionally, it covers the case of getting endpoint + # versions with domain scope token + sc_uri = "https://service.cloud.com/identity" + versions_uri = "https://service.cloud.com" + + sot = session.Session(None) + # Project id is None when domain scope session present + sot.get_project_id = mock.Mock(return_value=None) + + responses = [session.Session._Endpoint(versions_uri, "versions")] + sot._parse_versions_response = mock.Mock(side_effect=responses) + + result = sot._get_endpoint_versions("type", sc_uri) + + sot._parse_versions_response.assert_called_once_with(versions_uri) + self.assertEqual(result, responses[0]) + self.assertFalse(result.needs_project_id) + self.assertIsNone(result.project_id) + def test__parse_version(self): sot = session.Session(None) From 521480199cc6162b66ae58ab62e1115bef2faeb5 Mon Sep 17 00:00:00 2001 From: "Yuanbin.Chen" Date: Fri, 5 May 2017 12:17:01 +0800 Subject: [PATCH 1498/3836] Add 'service list' resource for senlin This proposes a 'service-list' resource for senlin clustering service. Change-Id: Ib8da9441a99f367a83729ef6871e7ab10845d384 Signed-off-by: Yuanbin.Chen --- doc/source/users/proxies/cluster.rst | 8 +++ openstack/cluster/v1/_proxy.py | 9 ++++ openstack/cluster/v1/service.py | 39 ++++++++++++++ openstack/tests/unit/cluster/v1/test_proxy.py | 6 +++ .../tests/unit/cluster/v1/test_service.py | 54 +++++++++++++++++++ 5 files changed, 116 insertions(+) create mode 100644 openstack/cluster/v1/service.py create mode 100644 openstack/tests/unit/cluster/v1/test_service.py diff --git a/doc/source/users/proxies/cluster.rst b/doc/source/users/proxies/cluster.rst index fe5c502ef..4d1d1e794 100644 --- a/doc/source/users/proxies/cluster.rst +++ b/doc/source/users/proxies/cluster.rst @@ -165,3 +165,11 @@ Helper Operations .. automethod:: openstack.cluster.v1._proxy.Proxy.wait_for_delete .. automethod:: openstack.cluster.v1._proxy.Proxy.wait_for_status + + +Service Operations +^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.cluster.v1._proxy.Proxy + + .. automethod:: openstack.cluster.v1._proxy.Proxy.services diff --git a/openstack/cluster/v1/_proxy.py b/openstack/cluster/v1/_proxy.py index 903caaaf2..cd8e600dd 100644 --- a/openstack/cluster/v1/_proxy.py +++ b/openstack/cluster/v1/_proxy.py @@ -22,6 +22,7 @@ from openstack.cluster.v1 import profile as _profile from openstack.cluster.v1 import profile_type as _profile_type from openstack.cluster.v1 import receiver as _receiver +from openstack.cluster.v1 import service as _service from openstack import proxy2 from openstack import resource2 from openstack import utils @@ -1083,3 +1084,11 @@ def wait_for_delete(self, resource, interval=2, wait=120): """ return resource2.wait_for_delete(self._session, resource, interval, wait) + + def services(self, **query): + """Get a generator of service. + + :returns: A generator of objects that are of type + :class:`~openstack.cluster.v1.service.Service` + """ + return self._list(_service.Service, paginated=False, **query) diff --git a/openstack/cluster/v1/service.py b/openstack/cluster/v1/service.py new file mode 100644 index 000000000..0bdfaa024 --- /dev/null +++ b/openstack/cluster/v1/service.py @@ -0,0 +1,39 @@ +# 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.cluster import cluster_service +from openstack import resource2 as resource + + +class Service(resource.Resource): + resource_key = 'service' + resources_key = 'services' + base_path = '/services' + service = cluster_service.ClusterService() + + # Capabilities + allow_list = True + + # Properties + #: Status of service + status = resource.Body('status') + #: State of service + state = resource.Body('state') + #: Name of service + binary = resource.Body('binary') + #: Disabled reason of service + disabled_reason = resource.Body('disabled_reason') + #: Host where service runs + host = resource.Body('host') + #: The timestamp the service was last updated. + #: *Type: datetime object parsed from ISO 8601 formatted string* + updated_at = resource.Body('updated_at') diff --git a/openstack/tests/unit/cluster/v1/test_proxy.py b/openstack/tests/unit/cluster/v1/test_proxy.py index 0e7992d34..c5acde57f 100644 --- a/openstack/tests/unit/cluster/v1/test_proxy.py +++ b/openstack/tests/unit/cluster/v1/test_proxy.py @@ -26,6 +26,7 @@ from openstack.cluster.v1 import profile from openstack.cluster.v1 import profile_type from openstack.cluster.v1 import receiver +from openstack.cluster.v1 import service from openstack import proxy2 as proxy_base from openstack.tests.unit import test_proxy_base2 @@ -210,6 +211,11 @@ def test_cluster_scale_in_with_obj(self): method_args=[mock_cluster, 5], expected_args=[5]) + def test_services(self): + self.verify_list(self.proxy.services, + service.Service, + paginated=False) + @mock.patch.object(proxy_base.BaseProxy, '_find') def test_cluster_resize(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') diff --git a/openstack/tests/unit/cluster/v1/test_service.py b/openstack/tests/unit/cluster/v1/test_service.py new file mode 100644 index 000000000..20b3b1b51 --- /dev/null +++ b/openstack/tests/unit/cluster/v1/test_service.py @@ -0,0 +1,54 @@ +# 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 testtools + +from openstack.cluster.v1 import service + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'binary': 'senlin-engine', + 'host': 'host1', + 'status': 'enabled', + 'state': 'up', + 'disabled_reason': None, + 'updated_at': '2016-10-10T12:46:36.000000', +} + + +class TestService(testtools.TestCase): + + def setUp(self): + super(TestService, self).setUp() + self.resp = mock.Mock() + self.resp.body = None + self.resp.json = mock.Mock(return_value=self.resp.body) + self.sess = mock.Mock() + self.sess.put = mock.Mock(return_value=self.resp) + + def test_basic(self): + sot = service.Service() + self.assertEqual('service', sot.resource_key) + self.assertEqual('services', sot.resources_key) + self.assertEqual('/services', sot.base_path) + self.assertEqual('clustering', sot.service.service_type) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = service.Service(**EXAMPLE) + self.assertEqual(EXAMPLE['host'], sot.host) + self.assertEqual(EXAMPLE['binary'], sot.binary) + self.assertEqual(EXAMPLE['status'], sot.status) + self.assertEqual(EXAMPLE['state'], sot.state) + self.assertEqual(EXAMPLE['disabled_reason'], sot.disabled_reason) + self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) From 24494880f8241fee38cf75afbe4b594cd4f6dd60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Fri, 5 May 2017 23:23:08 +0000 Subject: [PATCH 1499/3836] Remove neutronclient mocks from floating ips tests Neutronclient mock is replaced by mocking REST calls using base.RequestsMockTestCase class Change-Id: I5fcdd0c07d2ec3627e73598615940b555883612a --- shade/tests/unit/test_floating_ip_neutron.py | 438 ++++++++++++------- 1 file changed, 290 insertions(+), 148 deletions(-) diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 865987c52..baf7098cb 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -19,6 +19,7 @@ Tests Floating IP resource methods for Neutron """ +import copy from mock import patch import munch @@ -583,111 +584,163 @@ def test_auto_ip_pool_no_reuse(self): self.assert_calls() - @patch.object(OpenStackCloud, 'keystone_session') - @patch.object(OpenStackCloud, '_neutron_create_floating_ip') - @patch.object(OpenStackCloud, '_neutron_list_floating_ips') - @patch.object(OpenStackCloud, 'list_networks') - @patch.object(OpenStackCloud, 'list_subnets') @patch.object(OpenStackCloud, 'has_service') - def test_available_floating_ip_new( - self, mock_has_service, mock_list_subnets, mock_list_networks, - mock__neutron_list_floating_ips, - mock__neutron_create_floating_ip, mock_keystone_session): + def test_available_floating_ip_new(self, mock_has_service): mock_has_service.return_value = True - mock_list_networks.return_value = [self.mock_get_network_rep] - mock_list_subnets.return_value = [] - mock__neutron_list_floating_ips.return_value = [] - mock__neutron_create_floating_ip.return_value = \ - self.mock_floating_ip_new_rep['floatingip'] - mock_keystone_session.get_project_id.return_value = \ - '4969c491a3c74ee4af974e6d800c62df' + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': [self.mock_get_network_rep]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets.json']), + json={'subnets': []}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips.json']), + json={'floatingips': []}), + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips.json']), + validate=dict( + json={'floatingip': { + 'floating_network_id': 'my-network-id'}}), + json=self.mock_floating_ip_new_rep) + ]) ip = self.cloud.available_floating_ip(network='my-network') self.assertEqual( self.mock_floating_ip_new_rep['floatingip']['floating_ip_address'], ip['floating_ip_address']) + self.assert_calls() - @patch.object(OpenStackCloud, 'get_floating_ip') - @patch.object(OpenStackCloud, 'neutron_client') @patch.object(OpenStackCloud, 'has_service') - def test_delete_floating_ip_existing( - self, mock_has_service, mock_neutron_client, mock_get_floating_ip): + def test_delete_floating_ip_existing(self, mock_has_service): + fip_id = '2f245a7b-796b-4f26-9cf9-9e82d248fda7' mock_has_service.return_value = True fake_fip = { - 'id': '2f245a7b-796b-4f26-9cf9-9e82d248fda7', + 'id': fip_id, 'floating_ip_address': '172.99.106.167', 'status': 'ACTIVE', } + self.register_uris([ + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'floatingips/{0}.json'.format(fip_id)]), + json={}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips.json']), + json={'floatingips': [fake_fip]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'floatingips/{0}.json'.format(fip_id)]), + json={}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips.json']), + json={'floatingips': [fake_fip]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'floatingips/{0}.json'.format(fip_id)]), + json={}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips.json']), + json={'floatingips': []}), + ]) - mock_get_floating_ip.side_effect = [fake_fip, fake_fip, None] - mock_neutron_client.delete_floatingip.return_value = None - - ret = self.cloud.delete_floating_ip( - floating_ip_id='2f245a7b-796b-4f26-9cf9-9e82d248fda7', - retry=2) - - mock_neutron_client.delete_floatingip.assert_called_with( - floatingip='2f245a7b-796b-4f26-9cf9-9e82d248fda7' - ) - self.assertEqual(mock_get_floating_ip.call_count, 3) - self.assertTrue(ret) + self.assertTrue( + self.cloud.delete_floating_ip(floating_ip_id=fip_id, retry=2)) + self.assert_calls() - @patch.object(OpenStackCloud, 'get_floating_ip') - @patch.object(OpenStackCloud, 'neutron_client') @patch.object(OpenStackCloud, 'has_service') - def test_delete_floating_ip_existing_down( - self, mock_has_service, mock_neutron_client, mock_get_floating_ip): + def test_delete_floating_ip_existing_down(self, mock_has_service): + fip_id = '2f245a7b-796b-4f26-9cf9-9e82d248fda7' mock_has_service.return_value = True fake_fip = { - 'id': '2f245a7b-796b-4f26-9cf9-9e82d248fda7', + 'id': fip_id, 'floating_ip_address': '172.99.106.167', 'status': 'ACTIVE', } down_fip = { - 'id': '2f245a7b-796b-4f26-9cf9-9e82d248fda7', + 'id': fip_id, 'floating_ip_address': '172.99.106.167', 'status': 'DOWN', } + self.register_uris([ + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'floatingips/{0}.json'.format(fip_id)]), + json={}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips.json']), + json={'floatingips': [fake_fip]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'floatingips/{0}.json'.format(fip_id)]), + json={}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips.json']), + json={'floatingips': [down_fip]}), + ]) - mock_get_floating_ip.side_effect = [fake_fip, down_fip, None] - mock_neutron_client.delete_floatingip.return_value = None - - ret = self.cloud.delete_floating_ip( - floating_ip_id='2f245a7b-796b-4f26-9cf9-9e82d248fda7', - retry=2) - - mock_neutron_client.delete_floatingip.assert_called_with( - floatingip='2f245a7b-796b-4f26-9cf9-9e82d248fda7' - ) - self.assertEqual(mock_get_floating_ip.call_count, 2) - self.assertTrue(ret) + self.assertTrue( + self.cloud.delete_floating_ip(floating_ip_id=fip_id, retry=2)) + self.assert_calls() - @patch.object(OpenStackCloud, 'get_floating_ip') - @patch.object(OpenStackCloud, 'neutron_client') @patch.object(OpenStackCloud, 'has_service') - def test_delete_floating_ip_existing_no_delete( - self, mock_has_service, mock_neutron_client, mock_get_floating_ip): + def test_delete_floating_ip_existing_no_delete(self, mock_has_service): + fip_id = '2f245a7b-796b-4f26-9cf9-9e82d248fda7' mock_has_service.return_value = True fake_fip = { - 'id': '2f245a7b-796b-4f26-9cf9-9e82d248fda7', + 'id': fip_id, 'floating_ip_address': '172.99.106.167', 'status': 'ACTIVE', } - - mock_get_floating_ip.side_effect = [fake_fip, fake_fip, fake_fip] - mock_neutron_client.delete_floatingip.return_value = None - + self.register_uris([ + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'floatingips/{0}.json'.format(fip_id)]), + json={}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips.json']), + json={'floatingips': [fake_fip]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'floatingips/{0}.json'.format(fip_id)]), + json={}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips.json']), + json={'floatingips': [fake_fip]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'floatingips/{0}.json'.format(fip_id)]), + json={}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips.json']), + json={'floatingips': [fake_fip]}), + ]) self.assertRaises( exc.OpenStackCloudException, self.cloud.delete_floating_ip, - floating_ip_id='2f245a7b-796b-4f26-9cf9-9e82d248fda7', - retry=2) - - mock_neutron_client.delete_floatingip.assert_called_with( - floatingip='2f245a7b-796b-4f26-9cf9-9e82d248fda7' - ) - self.assertEqual(mock_get_floating_ip.call_count, 3) + floating_ip_id=fip_id, retry=2) + self.assert_calls() def test_delete_floating_ip_not_found(self): self.register_uris([ @@ -702,104 +755,170 @@ def test_delete_floating_ip_not_found(self): self.assertFalse(ret) self.assert_calls() - @patch.object(OpenStackCloud, 'search_ports') - @patch.object(OpenStackCloud, 'neutron_client') @patch.object(OpenStackCloud, 'has_service') - def test_attach_ip_to_server( - self, mock_has_service, mock_neutron_client, mock_search_ports): + def test_attach_ip_to_server(self, mock_has_service): + fip = self.mock_floating_ip_list_rep['floatingips'][0] + device_id = self.fake_server['id'] mock_has_service.return_value = True - mock_search_ports.return_value = self.mock_search_ports_rep - - mock_neutron_client.list_floatingips.return_value = \ - self.mock_floating_ip_list_rep + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json'], + qs_elements=["device_id={0}".format(device_id)]), + json={'ports': self.mock_search_ports_rep}), + dict(method='PUT', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'floatingips/{0}.json'.format( + fip['id'])]), + json={'floatingip': fip}, + validate=dict( + json={'floatingip': { + 'port_id': self.mock_search_ports_rep[0]['id'], + 'fixed_ip_address': self.mock_search_ports_rep[0][ + 'fixed_ips'][0]['ip_address']}})), + ]) self.cloud._attach_ip_to_server( server=self.fake_server, floating_ip=self.floating_ip) + self.assert_calls() - mock_neutron_client.update_floatingip.assert_called_with( - floatingip=self.mock_floating_ip_list_rep['floatingips'][0]['id'], - body={ - 'floatingip': { - 'port_id': self.mock_search_ports_rep[0]['id'], - 'fixed_ip_address': self.mock_search_ports_rep[0][ - 'fixed_ips'][0]['ip_address'] - } - } - ) - - @patch.object(OpenStackCloud, 'delete_floating_ip') @patch.object(OpenStackCloud, 'get_server') - @patch.object(OpenStackCloud, 'create_floating_ip') @patch.object(OpenStackCloud, 'has_service') - def test_add_ip_refresh_timeout( - self, mock_has_service, mock_create_floating_ip, - mock_get_server, mock_delete_floating_ip): + def test_add_ip_refresh_timeout(self, mock_has_service, mock_get_server): mock_has_service.return_value = True - - mock_create_floating_ip.return_value = self.floating_ip + device_id = self.fake_server['id'] mock_get_server.return_value = self.fake_server + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'networks.json']), + json={'networks': [self.mock_get_network_rep]}), + dict(method='GET', + uri='https://network.example.com/v2.0/subnets.json', + json={'subnets': []}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json'], + qs_elements=["device_id={0}".format(device_id)]), + json={'ports': self.mock_search_ports_rep}), + dict(method='POST', + uri='https://network.example.com/v2.0/floatingips.json', + json={'floatingip': self.floating_ip}, + validate=dict( + json={'floatingip': { + 'floating_network_id': 'my-network-id', + 'fixed_ip_address': self.mock_search_ports_rep[0][ + 'fixed_ips'][0]['ip_address'], + 'port_id': self.mock_search_ports_rep[0]['id']}})), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips.json']), + json={'floatingips': [self.floating_ip]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'floatingips/{0}.json'.format( + self.floating_ip['id'])]), + json={}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips.json']), + json={'floatingips': []}), + ]) + self.assertRaises( exc.OpenStackCloudTimeout, self.cloud._add_auto_ip, server=self.fake_server, wait=True, timeout=0.01, reuse=False) + self.assert_calls() - mock_delete_floating_ip.assert_called_once_with( - self.floating_ip['id']) - - @patch.object(OpenStackCloud, 'get_floating_ip') - @patch.object(OpenStackCloud, 'neutron_client') @patch.object(OpenStackCloud, 'has_service') - def test_detach_ip_from_server( - self, mock_has_service, mock_neutron_client, - mock_get_floating_ip): + def test_detach_ip_from_server(self, mock_has_service): + fip = self.mock_floating_ip_new_rep['floatingip'] + attached_fip = copy.copy(fip) + attached_fip['port_id'] = 'server-port-id' mock_has_service.return_value = True - mock_get_floating_ip.return_value = \ - self.cloud._normalize_floating_ips( - self.mock_floating_ip_list_rep['floatingips'])[0] - + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips.json']), + json={'floatingips': [attached_fip]}), + dict(method='PUT', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'floatingips/{0}.json'.format( + fip['id'])]), + json={'floatingip': fip}, + validate=dict( + json={'floatingip': {'port_id': None}})) + ]) self.cloud.detach_ip_from_server( server_id='server-id', - floating_ip_id='2f245a7b-796b-4f26-9cf9-9e82d248fda7') - - mock_neutron_client.update_floatingip.assert_called_with( - floatingip='2f245a7b-796b-4f26-9cf9-9e82d248fda7', - body={ - 'floatingip': { - 'port_id': None - } - } - ) + floating_ip_id=fip['id']) + self.assert_calls() - @patch.object(OpenStackCloud, '_attach_ip_to_server') - @patch.object(OpenStackCloud, 'available_floating_ip') @patch.object(OpenStackCloud, 'has_service') - def test_add_ip_from_pool( - self, mock_has_service, mock_available_floating_ip, - mock_attach_ip_to_server): + def test_add_ip_from_pool(self, mock_has_service): + network = self.mock_get_network_rep + fip = self.mock_floating_ip_new_rep['floatingip'] + fixed_ip = self.mock_search_ports_rep[0]['fixed_ips'][0]['ip_address'] + port_id = self.mock_search_ports_rep[0]['id'] mock_has_service.return_value = True - mock_available_floating_ip.return_value = \ - self.cloud._normalize_floating_ips([ - self.mock_floating_ip_new_rep['floatingip']])[0] - mock_attach_ip_to_server.return_value = self.fake_server + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': [network]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets.json']), + json={'subnets': []}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips.json']), + json={'floatingips': [fip]}), + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips.json']), + json={'floatingip': fip}, + validate=dict( + json={'floatingip': { + 'floating_network_id': network['id']}})), + dict(method="GET", + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json'], + qs_elements=[ + "device_id={0}".format(self.fake_server['id'])]), + json={'ports': self.mock_search_ports_rep}), + dict(method='PUT', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'floatingips/{0}.json'.format( + fip['id'])]), + json={'floatingip': fip}, + validate=dict( + json={'floatingip': { + 'fixed_ip_address': fixed_ip, + 'port_id': port_id}})), + ]) server = self.cloud._add_ip_from_pool( server=self.fake_server, - network='network-name', - fixed_address='192.0.2.129') + network=network['id'], + fixed_address=fixed_ip) self.assertEqual(server, self.fake_server) + self.assert_calls() - @patch.object(OpenStackCloud, 'delete_floating_ip') - @patch.object(OpenStackCloud, 'list_floating_ips') @patch.object(OpenStackCloud, '_use_neutron_floating') - def test_cleanup_floating_ips( - self, mock_use_neutron_floating, mock_list_floating_ips, - mock_delete_floating_ip): + def test_cleanup_floating_ips(self, mock_use_neutron_floating): mock_use_neutron_floating.return_value = True floating_ips = [{ "id": "this-is-a-floating-ip-id", @@ -807,7 +926,7 @@ def test_cleanup_floating_ips( "internal_network": None, "floating_ip_address": "203.0.113.29", "network": "this-is-a-net-or-pool-id", - "attached": False, + "port_id": None, "status": "ACTIVE" }, { "id": "this-is-an-attached-floating-ip-id", @@ -816,28 +935,51 @@ def test_cleanup_floating_ips( "floating_ip_address": "203.0.113.29", "network": "this-is-a-net-or-pool-id", "attached": True, + "port_id": "this-is-id-of-port-with-fip", "status": "ACTIVE" }] - - mock_list_floating_ips.return_value = floating_ips - + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips.json']), + json={'floatingips': floating_ips}), + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'floatingips/{0}.json'.format( + floating_ips[0]['id'])]), + json={}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips.json']), + json={'floatingips': [floating_ips[1]]}), + ]) self.cloud.delete_unattached_floating_ips() + self.assert_calls() - mock_delete_floating_ip.assert_called_once_with( - floating_ip_id='this-is-a-floating-ip-id', retry=1) - - @patch.object(OpenStackCloud, '_submit_create_fip') - @patch.object(OpenStackCloud, '_nat_destination_port') - @patch.object(OpenStackCloud, 'get_external_ipv4_networks') - def test_create_floating_ip_no_port( - self, mock_get_ext_nets, mock_nat_destination_port, - mock_submit_create_fip): - fake_port = dict(id='port-id') - mock_get_ext_nets.return_value = [self.mock_get_network_rep] - mock_nat_destination_port.return_value = (fake_port, '10.0.0.2') - mock_submit_create_fip.return_value = dict(port_id=None) + def test_create_floating_ip_no_port(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': [self.mock_get_network_rep]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets.json']), + json={'subnets': []}), + dict(method="GET", + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json'], + qs_elements=['device_id=some-server']), + json={'ports': [dict(id='port-id')]}), + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips.json']), + json={'floatingips': [dict(port_id=None)]}) + ]) self.assertRaises( exc.OpenStackCloudException, self.cloud._neutron_create_floating_ip, server=dict(id='some-server')) + self.assert_calls() From 4b75c85e184fed7f5bbad292c5d1f11bd076a926 Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Fri, 5 May 2017 23:40:01 +0000 Subject: [PATCH 1500/3836] Remove cinderclient mocks from snapshot tests Change-Id: Ib753f7db64cad35bc47fb6abf20bccf41202cd2e Signed-off-by: Rosario Di Somma --- shade/_normalize.py | 5 + .../tests/unit/test_create_volume_snapshot.py | 135 +++++++++++------- .../tests/unit/test_delete_volume_snapshot.py | 79 +++++----- 3 files changed, 134 insertions(+), 85 deletions(-) diff --git a/shade/_normalize.py b/shade/_normalize.py index 3b3d3c5cc..e28298540 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -687,6 +687,11 @@ def _normalize_volume(self, volume): # Discard noise self._remove_novaclient_artifacts(volume) + # TODO(rods) two lines below can be removed as soon as we move + # to the REST API calls + volume.pop('progress', None) + volume.pop('project_id', None) + volume_id = volume.pop('id') name = volume.pop('display_name', None) diff --git a/shade/tests/unit/test_create_volume_snapshot.py b/shade/tests/unit/test_create_volume_snapshot.py index e6fb267df..5672b6119 100644 --- a/shade/tests/unit/test_create_volume_snapshot.py +++ b/shade/tests/unit/test_create_volume_snapshot.py @@ -17,93 +17,122 @@ Tests for the `create_volume_snapshot` command. """ -from mock import patch -import shade from shade import exc from shade import meta from shade.tests import fakes from shade.tests.unit import base -class TestCreateVolumeSnapshot(base.TestCase): +class TestCreateVolumeSnapshot(base.RequestsMockTestCase): - @patch.object(shade.OpenStackCloud, 'cinder_client') - def test_create_volume_snapshot_wait(self, mock_cinder): + def test_create_volume_snapshot_wait(self): """ Test that create_volume_snapshot with a wait returns the volume snapshot when its status changes to "available". """ - build_snapshot = fakes.FakeVolumeSnapshot('1234', 'creating', + snapshot_id = '5678' + volume_id = '1234' + build_snapshot = fakes.FakeVolumeSnapshot(snapshot_id, 'creating', 'foo', 'derpysnapshot') - fake_snapshot = fakes.FakeVolumeSnapshot('1234', 'available', + build_snapshot_dict = meta.obj_to_dict(build_snapshot) + fake_snapshot = fakes.FakeVolumeSnapshot(snapshot_id, 'available', 'foo', 'derpysnapshot') - - mock_cinder.volume_snapshots.create.return_value = build_snapshot - mock_cinder.volume_snapshots.get.return_value = fake_snapshot - mock_cinder.volume_snapshots.list.return_value = [ - build_snapshot, fake_snapshot] + fake_snapshot_dict = meta.obj_to_dict(fake_snapshot) + + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'volumev2', 'public', append=['snapshots']), + json={'snapshot': build_snapshot_dict}, + validate=dict( + json={'snapshot': {'description': None, + 'force': False, + 'metadata': {}, + 'name': None, + 'volume_id': volume_id}})), + dict(method='GET', + uri=self.get_mock_url('volumev2', 'public', + append=['snapshots', snapshot_id]), + json={'snapshot': build_snapshot_dict}), + dict(method='GET', + uri=self.get_mock_url('volumev2', 'public', + append=['snapshots', snapshot_id]), + json={'snapshot': fake_snapshot_dict})]) self.assertEqual( - self.cloud._normalize_volume( - meta.obj_to_dict(fake_snapshot)), - self.cloud.create_volume_snapshot(volume_id='1234', wait=True) - ) - - mock_cinder.volume_snapshots.create.assert_called_with( - force=False, volume_id='1234' - ) - mock_cinder.volume_snapshots.get.assert_called_with( - snapshot_id=meta.obj_to_dict(build_snapshot)['id'] + self.cloud._normalize_volume(fake_snapshot_dict), + self.cloud.create_volume_snapshot(volume_id=volume_id, wait=True) ) + self.assert_calls() - @patch.object(shade.OpenStackCloud, 'cinder_client') - def test_create_volume_snapshot_with_timeout(self, mock_cinder): + def test_create_volume_snapshot_with_timeout(self): """ Test that a timeout while waiting for the volume snapshot to create raises an exception in create_volume_snapshot. """ - build_snapshot = fakes.FakeVolumeSnapshot('1234', 'creating', + snapshot_id = '5678' + volume_id = '1234' + build_snapshot = fakes.FakeVolumeSnapshot(snapshot_id, 'creating', 'foo', 'derpysnapshot') - - mock_cinder.volume_snapshots.create.return_value = build_snapshot - mock_cinder.volume_snapshots.get.return_value = build_snapshot - mock_cinder.volume_snapshots.list.return_value = [build_snapshot] + build_snapshot_dict = meta.obj_to_dict(build_snapshot) + + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'volumev2', 'public', append=['snapshots']), + json={'snapshot': build_snapshot_dict}, + validate=dict( + json={'snapshot': {'description': None, + 'force': False, + 'metadata': {}, + 'name': None, + 'volume_id': volume_id}})), + dict(method='GET', + uri=self.get_mock_url('volumev2', 'public', + append=['snapshots', snapshot_id]), + json={'snapshot': build_snapshot_dict})]) self.assertRaises( exc.OpenStackCloudTimeout, - self.cloud.create_volume_snapshot, volume_id='1234', + self.cloud.create_volume_snapshot, volume_id=volume_id, wait=True, timeout=0.01) - mock_cinder.volume_snapshots.create.assert_called_with( - force=False, volume_id='1234' - ) - mock_cinder.volume_snapshots.get.assert_called_with( - snapshot_id=meta.obj_to_dict(build_snapshot)['id'] - ) - - @patch.object(shade.OpenStackCloud, 'cinder_client') - def test_create_volume_snapshot_with_error(self, mock_cinder): + def test_create_volume_snapshot_with_error(self): """ Test that a error status while waiting for the volume snapshot to create raises an exception in create_volume_snapshot. """ - build_snapshot = fakes.FakeVolumeSnapshot('1234', 'creating', + snapshot_id = '5678' + volume_id = '1234' + build_snapshot = fakes.FakeVolumeSnapshot(snapshot_id, 'creating', 'bar', 'derpysnapshot') - error_snapshot = fakes.FakeVolumeSnapshot('1234', 'error', + build_snapshot_dict = meta.obj_to_dict(build_snapshot) + error_snapshot = fakes.FakeVolumeSnapshot(snapshot_id, 'error', 'blah', 'derpysnapshot') - - mock_cinder.volume_snapshots.create.return_value = build_snapshot - mock_cinder.volume_snapshots.get.return_value = error_snapshot - mock_cinder.volume_snapshots.list.return_value = [error_snapshot] + error_snapshot_dict = meta.obj_to_dict(error_snapshot) + + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'volumev2', 'public', append=['snapshots']), + json={'snapshot': build_snapshot_dict}, + validate=dict( + json={'snapshot': {'description': None, + 'force': False, + 'metadata': {}, + 'name': None, + 'volume_id': volume_id}})), + dict(method='GET', + uri=self.get_mock_url('volumev2', 'public', + append=['snapshots', snapshot_id]), + json={'snapshot': build_snapshot_dict}), + dict(method='GET', + uri=self.get_mock_url('volumev2', 'public', + append=['snapshots', snapshot_id]), + json={'snapshot': error_snapshot_dict})]) self.assertRaises( exc.OpenStackCloudException, - self.cloud.create_volume_snapshot, volume_id='1234', + self.cloud.create_volume_snapshot, volume_id=volume_id, wait=True, timeout=5) - - mock_cinder.volume_snapshots.create.assert_called_with( - force=False, volume_id='1234' - ) - mock_cinder.volume_snapshots.get.assert_called_with( - snapshot_id=meta.obj_to_dict(build_snapshot)['id'] - ) + self.assert_calls() diff --git a/shade/tests/unit/test_delete_volume_snapshot.py b/shade/tests/unit/test_delete_volume_snapshot.py index a9a663a28..49d25167e 100644 --- a/shade/tests/unit/test_delete_volume_snapshot.py +++ b/shade/tests/unit/test_delete_volume_snapshot.py @@ -17,69 +17,84 @@ Tests for the `delete_volume_snapshot` command. """ -from mock import patch -import shade from shade import exc +from shade import meta from shade.tests import fakes from shade.tests.unit import base -class TestDeleteVolumeSnapshot(base.TestCase): +class TestDeleteVolumeSnapshot(base.RequestsMockTestCase): - @patch.object(shade.OpenStackCloud, 'cinder_client') - def test_delete_volume_snapshot(self, mock_cinder): + def test_delete_volume_snapshot(self): """ Test that delete_volume_snapshot without a wait returns True instance when the volume snapshot deletes. """ fake_snapshot = fakes.FakeVolumeSnapshot('1234', 'available', 'foo', 'derpysnapshot') - - mock_cinder.volume_snapshots.list.return_value = [fake_snapshot] - - self.assertEqual( - True, - self.cloud.delete_volume_snapshot(name_or_id='1234', wait=False) - ) - - mock_cinder.volume_snapshots.list.assert_called_with(detailed=True, - search_opts=None) - - @patch.object(shade.OpenStackCloud, 'cinder_client') - def test_delete_volume_snapshot_with_error(self, mock_cinder): + fake_snapshot_dict = meta.obj_to_dict(fake_snapshot) + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['snapshots', 'detail']), + json={'snapshots': [fake_snapshot_dict]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['snapshots', fake_snapshot_dict['id']]))]) + + self.assertTrue( + self.cloud.delete_volume_snapshot(name_or_id='1234', wait=False)) + self.assert_calls() + + def test_delete_volume_snapshot_with_error(self): """ Test that a exception while deleting a volume snapshot will cause an OpenStackCloudException. """ fake_snapshot = fakes.FakeVolumeSnapshot('1234', 'available', 'foo', 'derpysnapshot') - - mock_cinder.volume_snapshots.delete.side_effect = Exception( - "exception") - mock_cinder.volume_snapshots.list.return_value = [fake_snapshot] + fake_snapshot_dict = meta.obj_to_dict(fake_snapshot) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['snapshots', 'detail']), + json={'snapshots': [fake_snapshot_dict]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['snapshots', fake_snapshot_dict['id']]), + status_code=404)]) self.assertRaises( exc.OpenStackCloudException, self.cloud.delete_volume_snapshot, name_or_id='1234') + self.assert_calls() - mock_cinder.volume_snapshots.delete.assert_called_with( - snapshot='1234') - - @patch.object(shade.OpenStackCloud, 'cinder_client') - def test_delete_volume_snapshot_with_timeout(self, mock_cinder): + def test_delete_volume_snapshot_with_timeout(self): """ Test that a timeout while waiting for the volume snapshot to delete raises an exception in delete_volume_snapshot. """ fake_snapshot = fakes.FakeVolumeSnapshot('1234', 'available', 'foo', 'derpysnapshot') - - mock_cinder.volume_snapshots.list.return_value = [fake_snapshot] + fake_snapshot_dict = meta.obj_to_dict(fake_snapshot) + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['snapshots', 'detail']), + json={'snapshots': [fake_snapshot_dict]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['snapshots', fake_snapshot_dict['id']]))]) self.assertRaises( exc.OpenStackCloudTimeout, self.cloud.delete_volume_snapshot, name_or_id='1234', wait=True, timeout=0.01) - - mock_cinder.volume_snapshots.list.assert_called_with(detailed=True, - search_opts=None) From f7f54d0ddbc8695f7efd9b8b27a95b04a686efc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Sat, 6 May 2017 15:16:37 +0000 Subject: [PATCH 1501/3836] Remove has_service mock from Neutron FIP tests As we switched FIP related tests to use request_mocks and Neutron is in service catalog in tests there is no need to mock has_service() method anymore. Additionally one useless mock of get_server() method was also removed. Change-Id: Idb6a8afbba7720548faefad1ab60b1bc582d1c10 --- shade/tests/unit/test_floating_ip_neutron.py | 40 +++++--------------- 1 file changed, 9 insertions(+), 31 deletions(-) diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index baf7098cb..384fe4290 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -20,12 +20,10 @@ """ import copy -from mock import patch import munch from shade import exc from shade import meta -from shade import OpenStackCloud from shade.tests import fakes from shade.tests.unit import base @@ -584,9 +582,7 @@ def test_auto_ip_pool_no_reuse(self): self.assert_calls() - @patch.object(OpenStackCloud, 'has_service') - def test_available_floating_ip_new(self, mock_has_service): - mock_has_service.return_value = True + def test_available_floating_ip_new(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( @@ -616,10 +612,8 @@ def test_available_floating_ip_new(self, mock_has_service): ip['floating_ip_address']) self.assert_calls() - @patch.object(OpenStackCloud, 'has_service') - def test_delete_floating_ip_existing(self, mock_has_service): + def test_delete_floating_ip_existing(self): fip_id = '2f245a7b-796b-4f26-9cf9-9e82d248fda7' - mock_has_service.return_value = True fake_fip = { 'id': fip_id, 'floating_ip_address': '172.99.106.167', @@ -659,10 +653,8 @@ def test_delete_floating_ip_existing(self, mock_has_service): self.cloud.delete_floating_ip(floating_ip_id=fip_id, retry=2)) self.assert_calls() - @patch.object(OpenStackCloud, 'has_service') - def test_delete_floating_ip_existing_down(self, mock_has_service): + def test_delete_floating_ip_existing_down(self): fip_id = '2f245a7b-796b-4f26-9cf9-9e82d248fda7' - mock_has_service.return_value = True fake_fip = { 'id': fip_id, 'floating_ip_address': '172.99.106.167', @@ -698,10 +690,8 @@ def test_delete_floating_ip_existing_down(self, mock_has_service): self.cloud.delete_floating_ip(floating_ip_id=fip_id, retry=2)) self.assert_calls() - @patch.object(OpenStackCloud, 'has_service') - def test_delete_floating_ip_existing_no_delete(self, mock_has_service): + def test_delete_floating_ip_existing_no_delete(self): fip_id = '2f245a7b-796b-4f26-9cf9-9e82d248fda7' - mock_has_service.return_value = True fake_fip = { 'id': fip_id, 'floating_ip_address': '172.99.106.167', @@ -755,11 +745,9 @@ def test_delete_floating_ip_not_found(self): self.assertFalse(ret) self.assert_calls() - @patch.object(OpenStackCloud, 'has_service') - def test_attach_ip_to_server(self, mock_has_service): + def test_attach_ip_to_server(self): fip = self.mock_floating_ip_list_rep['floatingips'][0] device_id = self.fake_server['id'] - mock_has_service.return_value = True self.register_uris([ dict(method='GET', @@ -785,12 +773,8 @@ def test_attach_ip_to_server(self, mock_has_service): floating_ip=self.floating_ip) self.assert_calls() - @patch.object(OpenStackCloud, 'get_server') - @patch.object(OpenStackCloud, 'has_service') - def test_add_ip_refresh_timeout(self, mock_has_service, mock_get_server): - mock_has_service.return_value = True + def test_add_ip_refresh_timeout(self): device_id = self.fake_server['id'] - mock_get_server.return_value = self.fake_server self.register_uris([ dict(method='GET', @@ -839,12 +823,10 @@ def test_add_ip_refresh_timeout(self, mock_has_service, mock_get_server): reuse=False) self.assert_calls() - @patch.object(OpenStackCloud, 'has_service') - def test_detach_ip_from_server(self, mock_has_service): + def test_detach_ip_from_server(self): fip = self.mock_floating_ip_new_rep['floatingip'] attached_fip = copy.copy(fip) attached_fip['port_id'] = 'server-port-id' - mock_has_service.return_value = True self.register_uris([ dict(method='GET', uri=self.get_mock_url( @@ -864,13 +846,11 @@ def test_detach_ip_from_server(self, mock_has_service): floating_ip_id=fip['id']) self.assert_calls() - @patch.object(OpenStackCloud, 'has_service') - def test_add_ip_from_pool(self, mock_has_service): + def test_add_ip_from_pool(self): network = self.mock_get_network_rep fip = self.mock_floating_ip_new_rep['floatingip'] fixed_ip = self.mock_search_ports_rep[0]['fixed_ips'][0]['ip_address'] port_id = self.mock_search_ports_rep[0]['id'] - mock_has_service.return_value = True self.register_uris([ dict(method='GET', uri=self.get_mock_url( @@ -917,9 +897,7 @@ def test_add_ip_from_pool(self, mock_has_service): self.assertEqual(server, self.fake_server) self.assert_calls() - @patch.object(OpenStackCloud, '_use_neutron_floating') - def test_cleanup_floating_ips(self, mock_use_neutron_floating): - mock_use_neutron_floating.return_value = True + def test_cleanup_floating_ips(self): floating_ips = [{ "id": "this-is-a-floating-ip-id", "fixed_ip_address": None, From b39045069fc2e562f4b7b1e36316628b0fc59e16 Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Sat, 6 May 2017 16:04:05 +0000 Subject: [PATCH 1502/3836] Add assert_calls check testing volume calls with timeout enabled Change-Id: I4203f0de012b3d84ece3ba7749568f39c92dcfeb Signed-off-by: Rosario Di Somma --- shade/tests/unit/test_create_volume_snapshot.py | 1 + shade/tests/unit/test_delete_volume_snapshot.py | 1 + 2 files changed, 2 insertions(+) diff --git a/shade/tests/unit/test_create_volume_snapshot.py b/shade/tests/unit/test_create_volume_snapshot.py index 5672b6119..8079ede69 100644 --- a/shade/tests/unit/test_create_volume_snapshot.py +++ b/shade/tests/unit/test_create_volume_snapshot.py @@ -96,6 +96,7 @@ def test_create_volume_snapshot_with_timeout(self): exc.OpenStackCloudTimeout, self.cloud.create_volume_snapshot, volume_id=volume_id, wait=True, timeout=0.01) + self.assert_calls(do_count=False) def test_create_volume_snapshot_with_error(self): """ diff --git a/shade/tests/unit/test_delete_volume_snapshot.py b/shade/tests/unit/test_delete_volume_snapshot.py index 49d25167e..355e08e01 100644 --- a/shade/tests/unit/test_delete_volume_snapshot.py +++ b/shade/tests/unit/test_delete_volume_snapshot.py @@ -98,3 +98,4 @@ def test_delete_volume_snapshot_with_timeout(self): exc.OpenStackCloudTimeout, self.cloud.delete_volume_snapshot, name_or_id='1234', wait=True, timeout=0.01) + self.assert_calls(do_count=False) From 81d39a9ea0265c72bb46238e8a9989538be735aa Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 6 May 2017 19:23:35 +0000 Subject: [PATCH 1503/3836] Updated from global requirements Change-Id: I3e7455e3f1bed53cbb3e9d64351d1536c82dc650 --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 7bbe8aea9..5caaf7e42 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ decorator>=3.4.0 # BSD jmespath>=0.9.0 # MIT jsonpatch>=1.1 # BSD ipaddress>=1.0.7;python_version<'3.3' # PSF -os-client-config>=1.22.0 # Apache-2.0 +os-client-config>=1.27.0 # Apache-2.0 # These two are here to prevent issues with version pin mismatches from our # client library transitive depends. # Babel can be removed when ironicclient is removed (because of openstackclient @@ -20,7 +20,7 @@ requestsexceptions>=1.2.0 # Apache-2.0 six>=1.9.0 # MIT futures>=3.0;python_version=='2.7' or python_version=='2.6' # BSD -keystoneauth1>=2.18.0 # Apache-2.0 +keystoneauth1>=2.20.0 # Apache-2.0 netifaces>=0.10.4 # MIT python-novaclient>=7.1.0 # Apache-2.0 python-keystoneclient>=3.8.0 # Apache-2.0 From 072be85da12914624f287f80dc9f57696b01e84c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Sun, 7 May 2017 11:06:42 +0000 Subject: [PATCH 1504/3836] Replace neutronclient with REST API calls in FIP commands All commands related to Neutron's floating IPs like list/create/attach/detach/delete are now made via keystoneauth Change-Id: Ifd739bbb2fe170cea2114816830d002c55ce2e45 --- shade/_tasks.py | 20 -------------------- shade/openstackcloud.py | 32 +++++++++++++++++++------------- 2 files changed, 19 insertions(+), 33 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 94b36c9a7..b73de51ea 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -232,31 +232,16 @@ def main(self, client): return client.cinder_client.volume_snapshots.delete(**self.args) -class NeutronFloatingIPList(task_manager.Task): - def main(self, client): - return client.neutron_client.list_floatingips(**self.args) - - class NovaFloatingIPList(task_manager.Task): def main(self, client): return client.nova_client.floating_ips.list() -class NeutronFloatingIPCreate(task_manager.Task): - def main(self, client): - return client.neutron_client.create_floatingip(**self.args) - - class NovaFloatingIPCreate(task_manager.Task): def main(self, client): return client.nova_client.floating_ips.create(**self.args) -class NeutronFloatingIPDelete(task_manager.Task): - def main(self, client): - return client.neutron_client.delete_floatingip(**self.args) - - class NovaFloatingIPDelete(task_manager.Task): def main(self, client): return client.nova_client.floating_ips.delete(**self.args) @@ -272,11 +257,6 @@ def main(self, client): return client.nova_client.servers.remove_floating_ip(**self.args) -class NeutronFloatingIPUpdate(task_manager.Task): - def main(self, client): - return client.neutron_client.update_floatingip(**self.args) - - class MachineCreate(task_manager.Task): def main(self, client): return client.ironic_client.node.create(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index f589baa09..133bc5894 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4615,9 +4615,9 @@ def create_floating_ip(self, network=None, server=None, def _submit_create_fip(self, kwargs): # Split into a method to aid in test mocking - return self._normalize_floating_ips( - [self.manager.submit_task(_tasks.NeutronFloatingIPCreate( - body={'floatingip': kwargs}))['floatingip']])[0] + fip = self._network_client.post( + "/floatingips.json", json={"floatingip": kwargs}) + return self._normalize_floating_ip(dict(fip)) def _neutron_create_floating_ip( self, network_name_or_id=None, server=None, @@ -4762,9 +4762,9 @@ def _delete_floating_ip(self, floating_ip_id): def _neutron_delete_floating_ip(self, floating_ip_id): try: - with _utils.neutron_exceptions("unable to delete floating IP"): - self.manager.submit_task( - _tasks.NeutronFloatingIPDelete(floatingip=floating_ip_id)) + self._network_client.delete( + "/floatingips/{fip_id}.json".format(fip_id=floating_ip_id), + error_message="unable to delete floating IP") except OpenStackCloudResourceNotFound: return False except Exception as e: @@ -4999,10 +4999,13 @@ def _neutron_attach_ip_to_server( if fixed_address is not None: floating_ip_args['fixed_ip_address'] = fixed_address - return self.manager.submit_task(_tasks.NeutronFloatingIPUpdate( - floatingip=floating_ip['id'], - body={'floatingip': floating_ip_args} - ))['floatingip'] + return self._network_client.put( + "/floatingips/{fip_id}.json".format(fip_id=floating_ip['id']), + json={'floatingip': floating_ip_args}, + error_message=("Error attaching IP {ip} to " + "server {server_id}".format( + ip=floating_ip['id'], + server_id=server['id']))) def _nova_attach_ip_to_server(self, server_id, floating_ip_id, fixed_address=None): @@ -5046,9 +5049,12 @@ def _neutron_detach_ip_from_server(self, server_id, floating_ip_id): f_ip = self.get_floating_ip(id=floating_ip_id) if f_ip is None or not f_ip['attached']: return False - self.manager.submit_task(_tasks.NeutronFloatingIPUpdate( - floatingip=floating_ip_id, - body={'floatingip': {'port_id': None}})) + self._network_client.put( + "/floatingips/{fip_id}.json".format(fip_id=floating_ip_id), + json={"floatingip": {"port_id": None}}, + error_message=("Error detaching IP {ip} from " + "server {server_id}".format( + ip=floating_ip_id, server_id=server_id))) return True From 55ef1369b63d5590e6b88b50d143cd259ae47aac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Sun, 7 May 2017 11:14:04 +0000 Subject: [PATCH 1505/3836] Enable neutron service in server create and rebuild tests Instead of mocking Neutron client calls to Neutron are now mocked by request_mocks in tests for create and rebuild servers. Change-Id: Ic90ad8f8468254a935e9e134348f599fa3079bba --- shade/tests/unit/test_create_server.py | 190 +++++++++++++++++++----- shade/tests/unit/test_rebuild_server.py | 75 ++++++---- 2 files changed, 199 insertions(+), 66 deletions(-) diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index d44f1d20a..4df7990db 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -27,32 +27,23 @@ class TestCreateServer(base.RequestsMockTestCase): - def setUp(self): - # This set of tests are not testing neutron, they're testing - # creating servers, but we do several network calls in service - # of a NORMAL create_server to find the default_network. Putting - # in all of the neutron mocks for that will make the tests harder - # to read. SO - we're going mock neutron into the off position - # and then turn it back on in the few tests that specifically do. - # Maybe we should reorg these into two classes - one with neutron - # mocked out - and one with it not mocked out - super(TestCreateServer, self).setUp() - self.has_neutron = False - - def fake_has_service(*args, **kwargs): - return self.has_neutron - self.cloud.has_service = fake_has_service - @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_create_server_with_create_exception(self, mock_nova): """ Test that an exception in the novaclient create raises an exception in create_server. """ + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': []}) + ]) mock_nova.servers.create.side_effect = Exception("exception") self.assertRaises( exc.OpenStackCloudException, self.cloud.create_server, 'server-name', {'id': 'image-id'}, {'id': 'flavor-id'}) + self.assert_calls() @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_create_server_with_get_exception(self, mock_nova): @@ -60,11 +51,18 @@ def test_create_server_with_get_exception(self, mock_nova): Test that an exception when attempting to get the server instance via the novaclient raises an exception in create_server. """ + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': []}) + ]) mock_nova.servers.create.return_value = mock.Mock(status="BUILD") mock_nova.servers.get.side_effect = Exception("exception") self.assertRaises( exc.OpenStackCloudException, self.cloud.create_server, 'server-name', {'id': 'image-id'}, {'id': 'flavor-id'}) + self.assert_calls() @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_create_server_with_server_error(self, mock_nova): @@ -74,11 +72,18 @@ def test_create_server_with_server_error(self, mock_nova): """ build_server = fakes.FakeServer('1234', '', 'BUILD') error_server = fakes.FakeServer('1234', '', 'ERROR') + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': []}) + ]) mock_nova.servers.create.return_value = build_server mock_nova.servers.get.return_value = error_server self.assertRaises( exc.OpenStackCloudException, self.cloud.create_server, 'server-name', {'id': 'image-id'}, {'id': 'flavor-id'}) + self.assert_calls() @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_create_server_wait_server_error(self, mock_nova): @@ -94,14 +99,29 @@ def test_create_server_wait_server_error(self, mock_nova): mock_nova.servers.create.return_value = build_server mock_nova.servers.get.return_value = build_server mock_nova.servers.list.side_effect = [[build_server], [error_server]] - # TODO(slaweq): change this to neutron floating ips and turn neutron - # back on for this test when you get to floating ips - mock_nova.floating_ips.list.return_value = [fake_floating_ip] + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': []}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json'], + qs_elements=['device_id=1234']), + json={'ports': []}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips.json']), + json={'floatingips': [fake_floating_ip]}) + ]) self.assertRaises( exc.OpenStackCloudException, self.cloud.create_server, 'server-name', dict(id='image-id'), dict(id='flavor-id'), wait=True) + # TODO(slaweq): change do_count to True when all nova mocks will be + # replaced with request_mocks also + self.assert_calls(do_count=False) @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_create_server_with_timeout(self, mock_nova): @@ -110,6 +130,12 @@ def test_create_server_with_timeout(self, mock_nova): exception in create_server. """ fake_server = fakes.FakeServer('1234', '', 'BUILD') + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': []}) + ]) mock_nova.servers.create.return_value = fake_server mock_nova.servers.get.return_value = fake_server mock_nova.servers.list.return_value = [fake_server] @@ -119,6 +145,7 @@ def test_create_server_with_timeout(self, mock_nova): 'server-name', dict(id='image-id'), dict(id='flavor-id'), wait=True, timeout=0.01) + self.assert_calls() @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_create_server_no_wait(self, mock_nova): @@ -132,9 +159,21 @@ def test_create_server_no_wait(self, mock_nova): '5678') mock_nova.servers.create.return_value = fake_server mock_nova.servers.get.return_value = fake_server - # TODO(slaweq): change this to neutron floating ips and turn neutron - # back on for this test when you get to floating ips - mock_nova.floating_ips.list.return_value = [fake_floating_ip] + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': []}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json'], + qs_elements=['device_id=1234']), + json={'ports': []}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips.json']), + json={'floatingips': [fake_floating_ip]}) + ]) self.assertEqual( self.cloud._normalize_server( meta.obj_to_dict(fake_server)), @@ -142,6 +181,9 @@ def test_create_server_no_wait(self, mock_nova): name='server-name', image=dict(id='image=id'), flavor=dict(id='flavor-id'))) + # TODO(slaweq): change do_count to True when all nova mocks will be + # replaced with request_mocks also + self.assert_calls(do_count=False) @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_create_server_with_admin_pass_no_wait(self, mock_nova): @@ -156,15 +198,30 @@ def test_create_server_with_admin_pass_no_wait(self, mock_nova): '5678') mock_nova.servers.create.return_value = fake_create_server mock_nova.servers.get.return_value = fake_server - # TODO(slaweq): change this to neutron floating ips and turn neutron - # back on for this test when you get to floating ips - mock_nova.floating_ips.list.return_value = [fake_floating_ip] + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': []}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json'], + qs_elements=['device_id=1234']), + json={'ports': []}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips.json']), + json={'floatingips': [fake_floating_ip]}) + ]) self.assertEqual( self.cloud._normalize_server( meta.obj_to_dict(fake_create_server)), self.cloud.create_server( name='server-name', image=dict(id='image=id'), flavor=dict(id='flavor-id'), admin_pass='ooBootheiX0edoh')) + # TODO(slaweq): change do_count to True when all nova mocks will be + # replaced with request_mocks also + self.assert_calls(do_count=False) @mock.patch.object(shade.OpenStackCloud, "wait_for_server") @mock.patch.object(shade.OpenStackCloud, "nova_client") @@ -173,6 +230,12 @@ def test_create_server_with_admin_pass_wait(self, mock_nova, mock_wait): Test that a server with an admin_pass passed returns the password """ fake_server = fakes.FakeServer('1234', '', 'BUILD') + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': []}) + ]) fake_server_with_pass = fakes.FakeServer('1234', '', 'BUILD', adminPass='ooBootheiX0edoh') @@ -196,6 +259,7 @@ def test_create_server_with_admin_pass_wait(self, mock_nova, mock_wait): self.cloud._normalize_server( meta.obj_to_dict(fake_server_with_pass)) ) + self.assert_calls() @mock.patch.object(shade.OpenStackCloud, "get_active_server") @mock.patch.object(shade.OpenStackCloud, "get_server") @@ -240,6 +304,12 @@ def test_create_server_wait(self, mock_nova, mock_wait): fake_server = {'id': 'fake_server_id', 'status': 'BUILDING'} mock_nova.servers.create.return_value = fake_server + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': []}) + ]) self.cloud.create_server( 'server-name', dict(id='image-id'), dict(id='flavor-id'), wait=True), @@ -249,6 +319,7 @@ def test_create_server_wait(self, mock_nova, mock_wait): ip_pool=None, reuse=True, timeout=180, nat_destination=None, ) + self.assert_calls() @mock.patch.object(shade.OpenStackCloud, 'add_ips_to_server') @mock.patch.object(shade.OpenStackCloud, 'nova_client') @@ -268,9 +339,21 @@ def test_create_server_no_addresses( mock_nova.servers.get.return_value = [build_server, None] mock_nova.servers.list.side_effect = [[build_server], [fake_server]] mock_nova.servers.delete.return_value = None - # TODO(slaweq): change this to neutron floating ips and turn neutron - # back on for this test when you get to floating ips - mock_nova.floating_ips.list.return_value = [fake_floating_ip] + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': []}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json'], + qs_elements=['device_id=1234']), + json={'ports': []}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips.json']), + json={'floatingips': [fake_floating_ip]}) + ]) mock_add_ips_to_server.return_value = fake_server self.cloud._SERVER_AGE = 0 @@ -278,23 +361,38 @@ def test_create_server_no_addresses( exc.OpenStackCloudException, self.cloud.create_server, 'server-name', {'id': 'image-id'}, {'id': 'flavor-id'}, wait=True) + # TODO(slaweq): change do_count to True when all nova mocks will be + # replaced with request_mocks also + self.assert_calls(do_count=False) @mock.patch('shade.OpenStackCloud.nova_client') - @mock.patch('shade.OpenStackCloud.get_network') - def test_create_server_network_with_no_nics(self, mock_get_network, - mock_nova): + def test_create_server_network_with_no_nics(self, mock_nova): """ Verify that if 'network' is supplied, and 'nics' is not, that we attempt to get the network for the server. """ - # TODO(slaweq): self.has_neutron = True should be added, the mock of - # get_network should be removed, and a register_uris containing - # a list with a network dict matching "network-name" should be used - # instead. + network = { + 'id': 'network-id', + 'name': 'network-name' + } + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': [network]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': [network]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets.json']), + json={'subnets': []}), + ]) self.cloud.create_server( 'server-name', dict(id='image-id'), dict(id='flavor-id'), network='network-name') - mock_get_network.assert_called_once_with(name_or_id='network-name') + self.assert_calls() @mock.patch('shade.OpenStackCloud.nova_client') @mock.patch('shade.OpenStackCloud.get_network') @@ -305,14 +403,24 @@ def test_create_server_network_with_empty_nics(self, Verify that if 'network' is supplied, along with an empty 'nics' list, it's treated the same as if 'nics' were not included. """ - # TODO(slaweq): self.has_neutron = True should be added, the mock of - # get_network should be removed, and a register_uris containing - # a list with a network dict matching "network-name" should be used - # instead. + network = { + 'id': 'network-id', + 'name': 'network-name' + } + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': [network]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets.json']), + json={'subnets': []}), + ]) self.cloud.create_server( 'server-name', dict(id='image-id'), dict(id='flavor-id'), network='network-name', nics=[]) - mock_get_network.assert_called_once_with(name_or_id='network-name') + self.assert_calls() @mock.patch.object(shade.OpenStackCloud, 'get_server_by_id') @mock.patch.object(shade.OpenStackCloud, 'get_image') diff --git a/shade/tests/unit/test_rebuild_server.py b/shade/tests/unit/test_rebuild_server.py index eb5b8b72e..f9d0714d6 100644 --- a/shade/tests/unit/test_rebuild_server.py +++ b/shade/tests/unit/test_rebuild_server.py @@ -30,22 +30,6 @@ class TestRebuildServer(base.RequestsMockTestCase): - def setUp(self): - # This set of tests are not testing neutron, they're testing - # rebuilding servers, but we do several network calls in service - # of a NORMAL rebuild to find the default_network. Putting - # in all of the neutron mocks for that will make the tests harder - # to read. SO - we're going mock neutron into the off position - # and then turn it back on in the few tests that specifically do. - # Maybe we should reorg these into two classes - one with neutron - # mocked out - and one with it not mocked out - super(TestRebuildServer, self).setUp() - self.has_neutron = False - - def fake_has_service(*args, **kwargs): - return self.has_neutron - self.cloud.has_service = fake_has_service - @mock.patch.object(OpenStackCloud, 'nova_client') def test_rebuild_server_rebuild_exception(self, mock_nova): """ @@ -69,12 +53,23 @@ def test_rebuild_server_server_error(self, mock_nova): '5678') mock_nova.servers.rebuild.return_value = rebuild_server mock_nova.servers.list.return_value = [error_server] - # TODO(slaweq): change this to neutron floating ips and turn neutron - # back on for this test when you get to floating ips - mock_nova.floating_ips.list.return_value = [fake_floating_ip] + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json'], + qs_elements=['device_id=1234']), + json={'ports': []}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips.json']), + json={'floatingips': [fake_floating_ip]}) + ]) self.assertRaises( exc.OpenStackCloudException, self.cloud.rebuild_server, "1234", "b", wait=True) + # TODO(slaweq): change do_count to True when all nova mocks will be + # replaced with request_mocks also + self.assert_calls(do_count=False) @mock.patch.object(OpenStackCloud, 'nova_client') def test_rebuild_server_timeout(self, mock_nova): @@ -128,15 +123,30 @@ def test_rebuild_server_with_admin_pass_wait(self, mock_nova): '5678') mock_nova.servers.rebuild.return_value = rebuild_server mock_nova.servers.list.return_value = [active_server] - # TODO(slaweq): change this to neutron floating ips and turn neutron - # back on for this test when you get to floating ips - mock_nova.floating_ips.list.return_value = [fake_floating_ip] + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json'], + qs_elements=['device_id=1234']), + json={'ports': []}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': []}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips.json']), + json={'floatingips': [fake_floating_ip]}) + ]) self.cloud.name = 'cloud-name' self.assertEqual( self.cloud._normalize_server( meta.obj_to_dict(ret_active_server)), self.cloud.rebuild_server( "1234", "b", wait=True, admin_pass='ooBootheiX0edoh')) + # TODO(slaweq): change do_count to True when all nova mocks will be + # replaced with request_mocks also + self.assert_calls(do_count=False) @mock.patch.object(OpenStackCloud, 'nova_client') def test_rebuild_server_wait(self, mock_nova): @@ -151,11 +161,26 @@ def test_rebuild_server_wait(self, mock_nova): '5678') mock_nova.servers.rebuild.return_value = rebuild_server mock_nova.servers.list.return_value = [active_server] - # TODO(slaweq): change this to neutron floating ips and turn neutron - # back on for this test when you get to floating ips - mock_nova.floating_ips.list.return_value = [fake_floating_ip] + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json'], + qs_elements=['device_id=1234']), + json={'ports': []}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': []}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips.json']), + json={'floatingips': [fake_floating_ip]}) + ]) self.cloud.name = 'cloud-name' self.assertEqual( self.cloud._normalize_server( meta.obj_to_dict(active_server)), self.cloud.rebuild_server("1234", "b", wait=True)) + # TODO(slaweq): change do_count to True when all nova mocks will be + # replaced with request_mocks also + self.assert_calls(do_count=False) From ad516fba6de1dd0a619083cd3ef1392f7070aaff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Sun, 7 May 2017 15:26:18 +0000 Subject: [PATCH 1506/3836] Remove usage of neutron_client from functional tests Neutron client was used in functional tests to add/remove router interface and to remove external_gw from router. Now shade can do such operations so shadeis used for that and neutron client is no longer needed there. Change-Id: Idf3e0329eef8c3ad6aebe7dd132f146d54f767cb --- shade/tests/functional/test_floating_ip.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/shade/tests/functional/test_floating_ip.py b/shade/tests/functional/test_floating_ip.py index 24ab87d1e..3e0d51fc1 100644 --- a/shade/tests/functional/test_floating_ip.py +++ b/shade/tests/functional/test_floating_ip.py @@ -37,8 +37,6 @@ class TestFloatingIP(base.BaseFunctionalTestCase): def setUp(self): super(TestFloatingIP, self).setUp() self.nova = self.user_cloud.nova_client - if self.user_cloud.has_service('network'): - self.neutron = self.user_cloud.neutron_client self.flavor = pick_flavor(self.nova.flavors.list()) if self.flavor is None: self.assertFalse('no sensible flavor available') @@ -59,20 +57,13 @@ def _cleanup_network(self): for r in self.user_cloud.list_routers(): try: if r['name'].startswith(self.new_item_name): - # ToDo: update_router currently won't allow removing - # external_gateway_info - router = { - 'external_gateway_info': None - } - self.neutron.update_router( - router=r['id'], body={'router': router}) - # ToDo: Shade currently doesn't have methods for this + self.user_cloud.update_router( + r['id'], ext_gateway_net_id=None) for s in self.user_cloud.list_subnets(): if s['name'].startswith(self.new_item_name): try: - self.neutron.remove_interface_router( - router=r['id'], - body={'subnet_id': s['id']}) + self.user_cloud.remove_router_interface( + r, subnet_id=s['id']) except Exception: pass self.user_cloud.delete_router(name_or_id=r['id']) @@ -165,9 +156,8 @@ def _setup_networks(self): name_or_id=self.test_router['id'], ext_gateway_net_id=ext_nets[0]['id']) # Attach the router to the internal subnet - self.neutron.add_interface_router( - router=self.test_router['id'], - body={'subnet_id': self.test_subnet['id']}) + self.user_cloud.add_router_interface( + self.test_router, subnet_id=self.test_subnet['id']) # Select the network for creating new servers self.nic = {'net-id': self.test_net['id']} From c43f1d81e3c02e6781684097407cbc5c18806c66 Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Sat, 6 May 2017 20:48:52 +0000 Subject: [PATCH 1507/3836] Use REST API for volume snapshot calls Change-Id: If6fda6bcc0455d086779d134e00417d72f1f4e92 Signed-off-by: Rosario Di Somma --- shade/_normalize.py | 4 -- shade/_tasks.py | 20 -------- shade/openstackcloud.py | 47 ++++++++----------- .../tests/unit/test_create_volume_snapshot.py | 24 +++------- 4 files changed, 26 insertions(+), 69 deletions(-) diff --git a/shade/_normalize.py b/shade/_normalize.py index e28298540..890903b97 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -687,10 +687,6 @@ def _normalize_volume(self, volume): # Discard noise self._remove_novaclient_artifacts(volume) - # TODO(rods) two lines below can be removed as soon as we move - # to the REST API calls - volume.pop('progress', None) - volume.pop('project_id', None) volume_id = volume.pop('id') diff --git a/shade/_tasks.py b/shade/_tasks.py index b73de51ea..74bda151d 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -212,26 +212,6 @@ def main(self, client): return client.nova_client.images.list() -class VolumeSnapshotCreate(task_manager.Task): - def main(self, client): - return client.cinder_client.volume_snapshots.create(**self.args) - - -class VolumeSnapshotGet(task_manager.Task): - def main(self, client): - return client.cinder_client.volume_snapshots.get(**self.args) - - -class VolumeSnapshotList(task_manager.Task): - def main(self, client): - return client.cinder_client.volume_snapshots.list(**self.args) - - -class VolumeSnapshotDelete(task_manager.Task): - def main(self, client): - return client.cinder_client.volume_snapshots.delete(**self.args) - - class NovaFloatingIPList(task_manager.Task): def main(self, client): return client.nova_client.floating_ips.list() diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 133bc5894..397f3f6a6 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4128,13 +4128,13 @@ def create_volume_snapshot(self, volume_id, force=False, """ kwargs = self._get_volume_kwargs(kwargs) - with _utils.shade_exceptions( - "Error creating snapshot of volume {volume_id}".format( - volume_id=volume_id)): - snapshot = self.manager.submit_task( - _tasks.VolumeSnapshotCreate( - volume_id=volume_id, force=force, - **kwargs)) + payload = {'volume_id': volume_id, 'force': force} + payload.update(kwargs) + snapshot = self._volume_client.post( + '/snapshots', + json=dict(snapshot=payload), + error_message="Error creating snapshot of volume " + "{volume_id}".format(volume_id=volume_id)) if wait: snapshot_id = snapshot['id'] @@ -4165,15 +4165,10 @@ def get_volume_snapshot_by_id(self, snapshot_id): param: snapshot_id: ID of the volume snapshot. """ - with _utils.shade_exceptions( - "Error getting snapshot {snapshot_id}".format( - snapshot_id=snapshot_id)): - snapshot = self.manager.submit_task( - _tasks.VolumeSnapshotGet( - snapshot_id=snapshot_id - ) - ) - + snapshot = self._volume_client.get( + '/snapshots/{snapshot_id}'.format(snapshot_id=snapshot_id), + error_message="Error getting snapshot " + "{snapshot_id}".format(snapshot_id=snapshot_id)) return self._normalize_volume(snapshot) def get_volume_snapshot(self, name_or_id, filters=None): @@ -4265,11 +4260,11 @@ def list_volume_snapshots(self, detailed=True, search_opts=None): :returns: A list of volume snapshots ``munch.Munch``. """ - with _utils.shade_exceptions("Error getting a list of snapshots"): - return self._normalize_volumes( - self.manager.submit_task( - _tasks.VolumeSnapshotList( - detailed=detailed, search_opts=search_opts))) + endpoint = '/snapshots/detail' if detailed else '/snapshots' + return self._volume_client.get( + endpoint, + params=search_opts, + error_message="Error getting a list of snapshots") def list_volume_backups(self, detailed=True, search_opts=None): """ @@ -4350,12 +4345,10 @@ def delete_volume_snapshot(self, name_or_id=None, wait=False, if not volumesnapshot: return False - with _utils.shade_exceptions("Error in deleting volume snapshot"): - self.manager.submit_task( - _tasks.VolumeSnapshotDelete( - snapshot=volumesnapshot['id'] - ) - ) + self._volume_client.delete( + '/snapshots/{snapshot_id}'.format( + snapshot_id=volumesnapshot['id']), + error_message="Error in deleting volume snapshot") if wait: for count in _utils._iterate_timeout( diff --git a/shade/tests/unit/test_create_volume_snapshot.py b/shade/tests/unit/test_create_volume_snapshot.py index 8079ede69..49882fdad 100644 --- a/shade/tests/unit/test_create_volume_snapshot.py +++ b/shade/tests/unit/test_create_volume_snapshot.py @@ -44,12 +44,8 @@ def test_create_volume_snapshot_wait(self): uri=self.get_mock_url( 'volumev2', 'public', append=['snapshots']), json={'snapshot': build_snapshot_dict}, - validate=dict( - json={'snapshot': {'description': None, - 'force': False, - 'metadata': {}, - 'name': None, - 'volume_id': volume_id}})), + validate=dict(json={ + 'snapshot': {'force': False, 'volume_id': '1234'}})), dict(method='GET', uri=self.get_mock_url('volumev2', 'public', append=['snapshots', snapshot_id]), @@ -81,12 +77,8 @@ def test_create_volume_snapshot_with_timeout(self): uri=self.get_mock_url( 'volumev2', 'public', append=['snapshots']), json={'snapshot': build_snapshot_dict}, - validate=dict( - json={'snapshot': {'description': None, - 'force': False, - 'metadata': {}, - 'name': None, - 'volume_id': volume_id}})), + validate=dict(json={ + 'snapshot': {'force': False, 'volume_id': '1234'}})), dict(method='GET', uri=self.get_mock_url('volumev2', 'public', append=['snapshots', snapshot_id]), @@ -117,12 +109,8 @@ def test_create_volume_snapshot_with_error(self): uri=self.get_mock_url( 'volumev2', 'public', append=['snapshots']), json={'snapshot': build_snapshot_dict}, - validate=dict( - json={'snapshot': {'description': None, - 'force': False, - 'metadata': {}, - 'name': None, - 'volume_id': volume_id}})), + validate=dict(json={ + 'snapshot': {'force': False, 'volume_id': '1234'}})), dict(method='GET', uri=self.get_mock_url('volumev2', 'public', append=['snapshots', snapshot_id]), From e00ff9c7194e7b62c34c2a13ac5d2e73ca67cf9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Sun, 7 May 2017 19:50:03 +0000 Subject: [PATCH 1508/3836] Fix Neutron floating IP test Test test_create_floating_ip_no_port was did wrong during transition from neutronclient mocks to request_mocks. This test expects raising error and error was raised but from wrong reason. Now test is using proper data and exception is raised with good reason. Change-Id: Ic965c66c2507fe4adbd42f159c11666199208b39 --- shade/openstackcloud.py | 2 +- shade/tests/unit/test_floating_ip_neutron.py | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 133bc5894..f8670ee40 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4617,7 +4617,7 @@ def _submit_create_fip(self, kwargs): # Split into a method to aid in test mocking fip = self._network_client.post( "/floatingips.json", json={"floatingip": kwargs}) - return self._normalize_floating_ip(dict(fip)) + return self._normalize_floating_ip(fip) def _neutron_create_floating_ip( self, network_name_or_id=None, server=None, diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 384fe4290..061e1edc6 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -936,6 +936,20 @@ def test_cleanup_floating_ips(self): self.assert_calls() def test_create_floating_ip_no_port(self): + server_port = { + "id": "port-id", + "device_id": "some-server", + 'fixed_ips': [ + { + 'subnet_id': 'subnet-id', + 'ip_address': '172.24.4.2' + } + ], + } + floating_ip = { + "id": "floating-ip-id", + "port_id": None + } self.register_uris([ dict(method='GET', uri=self.get_mock_url( @@ -949,11 +963,11 @@ def test_create_floating_ip_no_port(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'ports.json'], qs_elements=['device_id=some-server']), - json={'ports': [dict(id='port-id')]}), + json={'ports': [server_port]}), dict(method='POST', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'floatingips.json']), - json={'floatingips': [dict(port_id=None)]}) + json={'floatingip': floating_ip}) ]) self.assertRaises( From 1a8cb0aac20479309cca1da71d027aa23e047d64 Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Sun, 7 May 2017 17:30:50 +0000 Subject: [PATCH 1509/3836] Remove cinderclient mocks from quotas tests Change-Id: Ibce1d58309cf63a23ad8fb0e9ce89fb3b3de21fe Signed-off-by: Rosario Di Somma --- shade/tests/unit/test_quotas.py | 43 +++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/shade/tests/unit/test_quotas.py b/shade/tests/unit/test_quotas.py index 3cd35eb65..e733af6de 100644 --- a/shade/tests/unit/test_quotas.py +++ b/shade/tests/unit/test_quotas.py @@ -70,34 +70,47 @@ def test_delete_quotas(self, mock_nova): self.op_cloud.delete_compute_quotas, project) self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'cinder_client') - def test_cinder_update_quotas(self, mock_cinder): + def test_cinder_update_quotas(self): project = self.mock_for_keystone_projects(project_count=1, list_get=True)[0] + self.register_uris([ + dict(method='PUT', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['os-quota-sets', project.project_id]), + json=dict(quota_set={'volumes': 1}), + validate=dict( + json={'quota_set': { + 'volumes': 1, + 'tenant_id': project.project_id}}))]) self.op_cloud.set_volume_quotas(project.project_id, volumes=1) - - mock_cinder.quotas.update.assert_called_once_with( - volumes=1, tenant_id=project.project_id) self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'cinder_client') - def test_cinder_get_quotas(self, mock_cinder): + def test_cinder_get_quotas(self): project = self.mock_for_keystone_projects(project_count=1, list_get=True)[0] + # TODO(rods) `usage` defaults to False if not set but + # cinderclient is explicitly passing it. We'll have to do + # the same waiting to switch to the REST API call + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['os-quota-sets', project.project_id], + qs_elements=['usage=False']), + json=dict(quota_set={'snapshots': 10, 'volumes': 20}))]) self.op_cloud.get_volume_quotas(project.project_id) - - mock_cinder.quotas.get.assert_called_once_with( - tenant_id=project.project_id) self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'cinder_client') - def test_cinder_delete_quotas(self, mock_cinder): + def test_cinder_delete_quotas(self): project = self.mock_for_keystone_projects(project_count=1, list_get=True)[0] + self.register_uris([ + dict(method='DELETE', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['os-quota-sets', project.project_id]))]) self.op_cloud.delete_volume_quotas(project.project_id) - - mock_cinder.quotas.delete.assert_called_once_with( - tenant_id=project.project_id) self.assert_calls() def test_neutron_update_quotas(self): From 81239f6742a22415492fe65873f9606b6734173d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Sun, 7 May 2017 22:15:15 +0000 Subject: [PATCH 1510/3836] Remove neutronclient from shade's dependencies All calls to Neutron API are now done with REST calls made via keystoneauth1. Shade don't require python-neutronclient package for now. Change-Id: I0aa6f32b10986e99d6f86cad9640d9d95b5d2cef --- extras/install-tips.sh | 1 - requirements.txt | 1 - shade/_utils.py | 20 --- shade/openstackcloud.py | 276 ++++++++++++++++++++-------------------- 4 files changed, 139 insertions(+), 159 deletions(-) diff --git a/extras/install-tips.sh b/extras/install-tips.sh index 562270189..fe7a90780 100644 --- a/extras/install-tips.sh +++ b/extras/install-tips.sh @@ -19,7 +19,6 @@ for lib in \ python-novaclient \ python-keystoneclient \ python-cinderclient \ - python-neutronclient \ python-ironicclient \ python-designateclient \ keystoneauth diff --git a/requirements.txt b/requirements.txt index 5caaf7e42..08c0af534 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,7 +25,6 @@ netifaces>=0.10.4 # MIT python-novaclient>=7.1.0 # Apache-2.0 python-keystoneclient>=3.8.0 # Apache-2.0 python-cinderclient>=2.0.1 # Apache-2.0 -python-neutronclient>=5.1.0 # Apache-2.0 python-ironicclient>=1.11.0 # Apache-2.0 python-designateclient>=1.5.0 # Apache-2.0 diff --git a/shade/_utils.py b/shade/_utils.py index 82233f795..76b9f2ed3 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -25,7 +25,6 @@ import time from decorator import decorator -from neutronclient.common import exceptions as neutron_exc from novaclient import exceptions as nova_exc from shade import _log @@ -417,25 +416,6 @@ def invalidate(obj, *args, **kwargs): return _inner_cache_on_arguments -@contextlib.contextmanager -def neutron_exceptions(error_message): - try: - yield - except neutron_exc.NotFound as e: - raise exc.OpenStackCloudResourceNotFound( - "{msg}: {exc}".format(msg=error_message, exc=str(e))) - except neutron_exc.NeutronClientException as e: - if e.status_code == 404: - raise exc.OpenStackCloudURINotFound( - "{msg}: {exc}".format(msg=error_message, exc=str(e))) - else: - raise exc.OpenStackCloudException( - "{msg}: {exc}".format(msg=error_message, exc=str(e))) - except Exception as e: - raise exc.OpenStackCloudException( - "{msg}: {exc}".format(msg=error_message, exc=str(e))) - - @contextlib.contextmanager def shade_exceptions(error_message=None): """Context manager for dealing with shade exceptions. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index f8670ee40..3f500b84b 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1212,6 +1212,17 @@ def magnum_client(self): @property def neutron_client(self): + warnings.warn( + 'Using shade to get a neutron_client object is deprecated. If you' + ' need a raw neutronclient.Client object, please use' + ' make_legacy_client in os-client-config instead') + try: + import neutronclient # flake8: noqa + except ImportError: + self.log.error( + 'neutronclient is no longer a dependency of shade. You need to' + ' install python-neutronclient directly.') + raise if self._neutron_client is None: self._neutron_client = self._get_client('network') return self._neutron_client @@ -4475,47 +4486,46 @@ def _neutron_available_floating_ips( # tenant. This is the default behaviour of Nova project_id = self.current_project_id - with _utils.neutron_exceptions("unable to get available floating IPs"): - if network: - if isinstance(network, six.string_types): - network = [network] - - # Use given list to get first matching external network - floating_network_id = None - for net in network: - for ext_net in self.get_external_ipv4_floating_networks(): - if net in (ext_net['name'], ext_net['id']): - floating_network_id = ext_net['id'] - break - if floating_network_id: + if network: + if isinstance(network, six.string_types): + network = [network] + + # Use given list to get first matching external network + floating_network_id = None + for net in network: + for ext_net in self.get_external_ipv4_floating_networks(): + if net in (ext_net['name'], ext_net['id']): + floating_network_id = ext_net['id'] break + if floating_network_id: + break - if floating_network_id is None: - raise OpenStackCloudResourceNotFound( - "unable to find external network {net}".format( - net=network) - ) - else: - floating_network_id = self._get_floating_network_id() + if floating_network_id is None: + raise OpenStackCloudResourceNotFound( + "unable to find external network {net}".format( + net=network) + ) + else: + floating_network_id = self._get_floating_network_id() - filters = { - 'port': None, - 'network': floating_network_id, - 'location': {'project': {'id': project_id}}, - } + filters = { + 'port': None, + 'network': floating_network_id, + 'location': {'project': {'id': project_id}}, + } - floating_ips = self._list_floating_ips() - available_ips = _utils._filter_list( - floating_ips, name_or_id=None, filters=filters) - if available_ips: - return available_ips + floating_ips = self._list_floating_ips() + available_ips = _utils._filter_list( + floating_ips, name_or_id=None, filters=filters) + if available_ips: + return available_ips - # No available IP found or we didn't try - # allocate a new Floating IP - f_ip = self._neutron_create_floating_ip( - network_id=floating_network_id, server=server) + # No available IP found or we didn't try + # allocate a new Floating IP + f_ip = self._neutron_create_floating_ip( + network_id=floating_network_id, server=server) - return [f_ip] + return [f_ip] def _nova_available_floating_ips(self, pool=None): """Get available floating IPs from a floating IP pool. @@ -4624,76 +4634,74 @@ def _neutron_create_floating_ip( fixed_address=None, nat_destination=None, port=None, wait=False, timeout=60, network_id=None): - with _utils.neutron_exceptions( - "unable to create floating IP for net " - "{0}".format(network_name_or_id)): - if not network_id: - if network_name_or_id: - network = self.get_network(network_name_or_id) - if not network: - raise OpenStackCloudResourceNotFound( - "unable to find network for floating ips with ID " - "{0}".format(network_name_or_id)) - network_id = network['id'] - else: - network_id = self._get_floating_network_id() - kwargs = { - 'floating_network_id': network_id, - } - if not port: - if server: - (port_obj, fixed_ip_address) = self._nat_destination_port( - server, fixed_address=fixed_address, - nat_destination=nat_destination) - if port_obj: - port = port_obj['id'] - if fixed_ip_address: - kwargs['fixed_ip_address'] = fixed_ip_address - if port: - kwargs['port_id'] = port - - fip = self._submit_create_fip(kwargs) - fip_id = fip['id'] - - if port: - # The FIP is only going to become active in this context - # when we've attached it to something, which only occurs - # if we've provided a port as a parameter - if wait: + + if not network_id: + if network_name_or_id: + network = self.get_network(network_name_or_id) + if not network: + raise OpenStackCloudResourceNotFound( + "unable to find network for floating ips with ID " + "{0}".format(network_name_or_id)) + network_id = network['id'] + else: + network_id = self._get_floating_network_id() + kwargs = { + 'floating_network_id': network_id, + } + if not port: + if server: + (port_obj, fixed_ip_address) = self._nat_destination_port( + server, fixed_address=fixed_address, + nat_destination=nat_destination) + if port_obj: + port = port_obj['id'] + if fixed_ip_address: + kwargs['fixed_ip_address'] = fixed_ip_address + if port: + kwargs['port_id'] = port + + fip = self._submit_create_fip(kwargs) + fip_id = fip['id'] + + if port: + # The FIP is only going to become active in this context + # when we've attached it to something, which only occurs + # if we've provided a port as a parameter + if wait: + try: + for count in _utils._iterate_timeout( + timeout, + "Timeout waiting for the floating IP" + " to be ACTIVE", + wait=self._FLOAT_AGE): + fip = self.get_floating_ip(fip_id) + if fip and fip['status'] == 'ACTIVE': + break + except OpenStackCloudTimeout: + self.log.error( + "Timed out on floating ip %(fip)s becoming active." + " Deleting", {'fip': fip_id}) try: - for count in _utils._iterate_timeout( - timeout, - "Timeout waiting for the floating IP" - " to be ACTIVE", - wait=self._FLOAT_AGE): - fip = self.get_floating_ip(fip_id) - if fip and fip['status'] == 'ACTIVE': - break - except OpenStackCloudTimeout: + self.delete_floating_ip(fip_id) + except Exception as e: self.log.error( - "Timed out on floating ip %(fip)s becoming active." - " Deleting", {'fip': fip_id}) - try: - self.delete_floating_ip(fip_id) - except Exception as e: - self.log.error( - "FIP LEAK: Attempted to delete floating ip " - "%(fip)s but received %(exc)s exception: " - "%(err)s", {'fip': fip_id, 'exc': e.__class__, - 'err': str(e)}) - raise - if fip['port_id'] != port: - if server: - raise OpenStackCloudException( - "Attempted to create FIP on port {port} for server" - " {server} but FIP has port {port_id}".format( - port=port, port_id=fip['port_id'], - server=server['id'])) - else: - raise OpenStackCloudException( - "Attempted to create FIP on port {port}" - " but something went wrong".format(port=port)) - return fip + "FIP LEAK: Attempted to delete floating ip " + "%(fip)s but received %(exc)s exception: " + "%(err)s", {'fip': fip_id, 'exc': e.__class__, + 'err': str(e)}) + raise + if fip['port_id'] != port: + if server: + raise OpenStackCloudException( + "Attempted to create FIP on port {port} for server" + " {server} but FIP has port {port_id}".format( + port=port, port_id=fip['port_id'], + server=server['id'])) + else: + raise OpenStackCloudException( + "Attempted to create FIP on port {port}" + " but something went wrong".format(port=port)) + return fip def _nova_create_floating_ip(self, pool=None): with _utils.shade_exceptions( @@ -4982,30 +4990,27 @@ def _nat_destination_port( def _neutron_attach_ip_to_server( self, server, floating_ip, fixed_address=None, nat_destination=None): - with _utils.neutron_exceptions( - "unable to bind a floating ip to server " - "{0}".format(server['id'])): - # Find an available port - (port, fixed_address) = self._nat_destination_port( - server, fixed_address=fixed_address, - nat_destination=nat_destination) - if not port: - raise OpenStackCloudException( - "unable to find a port for server {0}".format( - server['id'])) + # Find an available port + (port, fixed_address) = self._nat_destination_port( + server, fixed_address=fixed_address, + nat_destination=nat_destination) + if not port: + raise OpenStackCloudException( + "unable to find a port for server {0}".format( + server['id'])) - floating_ip_args = {'port_id': port['id']} - if fixed_address is not None: - floating_ip_args['fixed_ip_address'] = fixed_address + floating_ip_args = {'port_id': port['id']} + if fixed_address is not None: + floating_ip_args['fixed_ip_address'] = fixed_address - return self._network_client.put( - "/floatingips/{fip_id}.json".format(fip_id=floating_ip['id']), - json={'floatingip': floating_ip_args}, - error_message=("Error attaching IP {ip} to " - "server {server_id}".format( - ip=floating_ip['id'], - server_id=server['id']))) + return self._network_client.put( + "/floatingips/{fip_id}.json".format(fip_id=floating_ip['id']), + json={'floatingip': floating_ip_args}, + error_message=("Error attaching IP {ip} to " + "server {server_id}".format( + ip=floating_ip['id'], + server_id=server['id']))) def _nova_attach_ip_to_server(self, server_id, floating_ip_id, fixed_address=None): @@ -5043,20 +5048,17 @@ def detach_ip_from_server(self, server_id, floating_ip_id): server_id=server_id, floating_ip_id=floating_ip_id) def _neutron_detach_ip_from_server(self, server_id, floating_ip_id): - with _utils.neutron_exceptions( - "unable to detach a floating ip from server " - "{0}".format(server_id)): - f_ip = self.get_floating_ip(id=floating_ip_id) - if f_ip is None or not f_ip['attached']: - return False - self._network_client.put( - "/floatingips/{fip_id}.json".format(fip_id=floating_ip_id), - json={"floatingip": {"port_id": None}}, - error_message=("Error detaching IP {ip} from " - "server {server_id}".format( - ip=floating_ip_id, server_id=server_id))) + f_ip = self.get_floating_ip(id=floating_ip_id) + if f_ip is None or not f_ip['attached']: + return False + self._network_client.put( + "/floatingips/{fip_id}.json".format(fip_id=floating_ip_id), + json={"floatingip": {"port_id": None}}, + error_message=("Error detaching IP {ip} from " + "server {server_id}".format( + ip=floating_ip_id, server_id=server_id))) - return True + return True def _nova_detach_ip_from_server(self, server_id, floating_ip_id): try: From 2f8285db7211b7b59f3ddb996f40a175990edf89 Mon Sep 17 00:00:00 2001 From: Christian Zunker Date: Fri, 5 May 2017 10:46:57 +0200 Subject: [PATCH 1511/3836] extend security_group and _rule with project id This allows admins to manage security groups and rules across projects. Story: #4193 Change-Id: Iaea4d14baec377f1885bd2e00fafcc889ca1ba94 --- shade/openstackcloud.py | 44 +++++++----- shade/tests/fakes.py | 3 +- shade/tests/unit/test_security_groups.py | 89 +++++++++++++++++++++++- 3 files changed, 115 insertions(+), 21 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index f589baa09..34b7c4c7a 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -6745,11 +6745,14 @@ def delete_port(self, name_or_id): error_message="Error deleting port {0}".format(name_or_id)) return True - def create_security_group(self, name, description): + def create_security_group(self, name, description, project_id=None): """Create a new security group :param string name: A name for the security group. :param string description: Describes the security group. + :param string project_id: + Specify the project ID this security group will be created + on (admin-only). :returns: A ``munch.Munch`` representing the new security group. @@ -6765,19 +6768,17 @@ def create_security_group(self, name, description): ) group = None + security_group_json = {'security_group': {'name': name, 'description': description}} + if project_id is not None: + security_group_json['security_group']['tenant_id'] = project_id if self._use_neutron_secgroups(): group = self._network_client.post( '/security-groups.json', - json={'security_group': - {'name': name, 'description': description}}, + json=security_group_json, error_message="Error creating security group {0}".format(name)) - else: group = self._compute_client.post( - '/os-security-groups', json={ - 'security_group': { - 'name': name, 'description': description} - }) + '/os-security-groups', json=security_group_json) return self._normalize_secgroup(group) def delete_security_group(self, name_or_id): @@ -6864,7 +6865,8 @@ def create_security_group_rule(self, remote_ip_prefix=None, remote_group_id=None, direction='ingress', - ethertype='IPv4'): + ethertype='IPv4', + project_id=None): """Create a new security group rule :param string secgroup_name_or_id: @@ -6902,6 +6904,9 @@ def create_security_group_rule(self, :param string ethertype: Must be IPv4 or IPv6, and addresses represented in CIDR must match the ingress or egress rules. + :param string project_id: + Specify the project ID this security group will be created + on (admin-only). :returns: A ``munch.Munch`` representing the new security group rule. @@ -6933,6 +6938,8 @@ def create_security_group_rule(self, 'direction': direction, 'ethertype': ethertype } + if project_id is not None: + rule_def['tenant_id'] = project_id rule = self._network_client.post( '/security-group-rules.json', @@ -6969,15 +6976,18 @@ def create_security_group_rule(self, port_range_min = 1 port_range_max = 65535 + security_group_rule_dict = dict(security_group_rule = dict( + parent_group_id=secgroup['id'], + ip_protocol=protocol, + from_port=port_range_min, + to_port=port_range_max, + cidr=remote_ip_prefix, + group_id=remote_group_id + )) + if project_id is not None: + security_group_rule_dict['security_group_rule']['tenant_id'] = project_id rule = self._compute_client.post( - '/os-security-group-rules', json=dict(security_group_rule=dict( - parent_group_id=secgroup['id'], - ip_protocol=protocol, - from_port=port_range_min, - to_port=port_range_max, - cidr=remote_ip_prefix, - group_id=remote_group_id - )) + '/os-security-group-rules', json=security_group_rule_dict ) return self._normalize_secgroup_rule(rule) diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index a46fa1061..4663330d4 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -311,10 +311,11 @@ def __init__(self, id, address, node_id): class FakeSecgroup(object): - def __init__(self, id, name, description='', rules=None): + def __init__(self, id, name, description='', project_id=None, rules=None): self.id = id self.name = name self.description = description + self.project_id = project_id self.rules = rules diff --git a/shade/tests/unit/test_security_groups.py b/shade/tests/unit/test_security_groups.py index e41186ea1..a09517d5c 100644 --- a/shade/tests/unit/test_security_groups.py +++ b/shade/tests/unit/test_security_groups.py @@ -188,6 +188,44 @@ def test_create_security_group_neutron(self): self.assert_calls() + def test_create_security_group_neutron_specific_tenant(self): + self.cloud.secgroup_source = 'neutron' + project_id = "861808a93da0484ea1767967c4df8a23" + group_name = self.getUniqueString() + group_desc = 'security group from' \ + ' test_create_security_group_neutron_specific_tenant' + new_group = meta.obj_to_dict( + fakes.FakeSecgroup( + id='2', + name=group_name, + description=group_desc, + project_id=project_id, + rules=[])) + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'security-groups.json']), + json={'security_group': new_group}, + validate=dict( + json={'security_group': { + 'name': group_name, + 'description': group_desc, + 'tenant_id': project_id + }})) + ]) + + r = self.cloud.create_security_group( + group_name, + group_desc, + project_id + ) + self.assertEqual(group_name, r['name']) + self.assertEqual(group_desc, r['description']) + self.assertEqual(project_id, r['tenant_id']) + + self.assert_calls() + def test_create_security_group_nova(self): group_name = self.getUniqueString() self.has_neutron = False @@ -206,7 +244,7 @@ def test_create_security_group_nova(self): validate=dict(json={ 'security_group': { 'name': group_name, - 'description': group_desc, + 'description': group_desc }})), ]) @@ -294,8 +332,8 @@ def test_create_security_group_rule_neutron(self): expected_new_rule = copy.copy(expected_args) expected_new_rule['id'] = '1234' - expected_new_rule['project_id'] = '' - expected_new_rule['tenant_id'] = expected_new_rule['project_id'] + expected_new_rule['tenant_id'] = '' + expected_new_rule['project_id'] = expected_new_rule['tenant_id'] self.register_uris([ dict(method='GET', @@ -319,6 +357,51 @@ def test_create_security_group_rule_neutron(self): self.assertEqual(expected_new_rule, new_rule) self.assert_calls() + def test_create_security_group_rule_neutron_specific_tenant(self): + self.cloud.secgroup_source = 'neutron' + args = dict( + port_range_min=-1, + port_range_max=40000, + protocol='tcp', + remote_ip_prefix='0.0.0.0/0', + remote_group_id='456', + direction='egress', + ethertype='IPv6', + project_id='861808a93da0484ea1767967c4df8a23' + ) + expected_args = copy.copy(args) + # For neutron, -1 port should be converted to None + expected_args['port_range_min'] = None + expected_args['security_group_id'] = neutron_grp_dict['id'] + expected_args['tenant_id'] = expected_args['project_id'] + expected_args.pop('project_id') + + expected_new_rule = copy.copy(expected_args) + expected_new_rule['id'] = '1234' + expected_new_rule['project_id'] = expected_new_rule['tenant_id'] + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'security-groups.json']), + json={'security_groups': [neutron_grp_dict]}), + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'security-group-rules.json']), + json={'security_group_rule': expected_new_rule}, + validate=dict(json={ + 'security_group_rule': expected_args})) + ]) + new_rule = self.cloud.create_security_group_rule( + secgroup_name_or_id=neutron_grp_dict['id'], ** args) + # NOTE(slaweq): don't check location and properties in new rule + new_rule.pop("location") + new_rule.pop("properties") + self.assertEqual(expected_new_rule, new_rule) + self.assert_calls() + def test_create_security_group_rule_nova(self): self.has_neutron = False self.cloud.secgroup_source = 'nova' From 347fe82c929bb9b8aba5aee504bbe3eaf1e8bf04 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 8 May 2017 07:20:53 -0400 Subject: [PATCH 1512/3836] Add helper method to fetch service catalog Grabbing the catalog is weird. OCC should help. Change-Id: I6e7176568311c1f0e644a8e8876f56c3e153d6e8 --- os_client_config/cloud_config.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 086092488..17cc801e2 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -225,6 +225,10 @@ def get_session(self): ('os-client-config', os_client_config.__version__)) return self._keystone_session + def get_service_catalog(self): + """Helper method to grab the service catalog.""" + return self._auth.get_access(self.get_session()).service_catalog + def get_session_client(self, service_key): """Return a prepped requests adapter for a given service. From b6926bb8e5e07b425809beb5773aa39b64feda98 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 8 May 2017 09:48:11 -0400 Subject: [PATCH 1513/3836] Add pprint and pformat helper methods Because of munch - using pprint directly is weird. Make a helper method that does what the user expects. Change-Id: I617b75034a59118c170895ad20b86fb8a1eab386 --- shade/_utils.py | 10 ++++++++++ shade/openstackcloud.py | 14 ++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/shade/_utils.py b/shade/_utils.py index 76b9f2ed3..340b63c08 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -95,6 +95,16 @@ def _make_unicode(input): return str(input) +def _dictify_resource(resource): + if isinstance(resource, list): + return [_dictify_resource(r) for r in resource] + else: + if hasattr(resource, 'toDict'): + return resource.toDict() + else: + return resource + + def _filter_list(data, name_or_id, filters): """Filter a list by name/ID and arbitrary meta data. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index aa845383c..8a5ca5457 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -528,6 +528,20 @@ def _volume_client(self): self._raw_clients['volume'] = self._get_raw_client('volume') return self._raw_clients['volume'] + def pprint(self, resource): + """Wrapper aroud pprint that groks munch objects""" + # import late since this is a utility function + import pprint + new_resource = _utils._dictify_resource(resource) + pprint.pprint(new_resource) + + def pformat(self, resource): + """Wrapper aroud pformat that groks munch objects""" + # import late since this is a utility function + import pprint + new_resource = _utils._dictify_resource(resource) + return pprint.pformat(new_resource) + @property def nova_client(self): if self._nova_client is None: From 7801de73ed57c015758ce7b2d69f53199f3410ae Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Mon, 8 May 2017 14:40:14 +0000 Subject: [PATCH 1514/3836] Use REST API for volume quotas calls Change-Id: I73c5448edbd5ba7bc36f7e709c128136916073fb Signed-off-by: Rosario Di Somma --- shade/_tasks.py | 15 --------------- shade/operatorcloud.py | 30 +++++++++++++----------------- shade/tests/unit/test_quotas.py | 6 +----- 3 files changed, 14 insertions(+), 37 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 74bda151d..ab66039fa 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -512,21 +512,6 @@ def main(self, client): return client.nova_client.usage.get(**self.args) -class CinderQuotasSet(task_manager.Task): - def main(self, client): - return client.cinder_client.quotas.update(**self.args) - - -class CinderQuotasGet(task_manager.Task): - def main(self, client): - return client.cinder_client.quotas.get(**self.args) - - -class CinderQuotasDelete(task_manager.Task): - def main(self, client): - return client.cinder_client.quotas.delete(**self.args) - - class NovaLimitsGet(task_manager.Task): def main(self, client): return client.nova_client.limits.get(**self.args).to_dict() diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 5b54da3ac..dd365d79e 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -16,7 +16,6 @@ from ironicclient import client as ironic_client from ironicclient import exceptions as ironic_exceptions from novaclient import exceptions as nova_exceptions -from cinderclient import exceptions as cinder_exceptions from shade.exc import * # noqa from shade import openstackcloud @@ -2166,12 +2165,11 @@ def set_volume_quotas(self, name_or_id, **kwargs): if not proj: raise OpenStackCloudException("project does not exist") - try: - self.manager.submit_task( - _tasks.CinderQuotasSet(tenant_id=proj.id, - **kwargs)) - except cinder_exceptions.BadRequest: - raise OpenStackCloudException("No valid quota or resource") + kwargs['tenant_id'] = proj.id + self._volume_client.put( + '/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id), + json={'quota_set': kwargs}, + error_message="No valid quota or resource") def get_volume_quotas(self, name_or_id): """ Get volume quotas for a project @@ -2184,11 +2182,10 @@ def get_volume_quotas(self, name_or_id): proj = self.get_project(name_or_id) if not proj: raise OpenStackCloudException("project does not exist") - try: - return self.manager.submit_task( - _tasks.CinderQuotasGet(tenant_id=proj.id)) - except cinder_exceptions.BadRequest: - raise OpenStackCloudException("cinder client call failed") + + return self._volume_client.get( + '/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id), + error_message="cinder client call failed") def delete_volume_quotas(self, name_or_id): """ Delete volume quotas for a project @@ -2202,11 +2199,10 @@ def delete_volume_quotas(self, name_or_id): proj = self.get_project(name_or_id) if not proj: raise OpenStackCloudException("project does not exist") - try: - return self.manager.submit_task( - _tasks.CinderQuotasDelete(tenant_id=proj.id)) - except cinder_exceptions.BadRequest: - raise OpenStackCloudException("cinder client call failed") + + return self._volume_client.delete( + '/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id), + error_message="cinder client call failed") def set_network_quotas(self, name_or_id, **kwargs): """ Set a network quota in a project diff --git a/shade/tests/unit/test_quotas.py b/shade/tests/unit/test_quotas.py index e733af6de..16032e2c5 100644 --- a/shade/tests/unit/test_quotas.py +++ b/shade/tests/unit/test_quotas.py @@ -89,15 +89,11 @@ def test_cinder_update_quotas(self): def test_cinder_get_quotas(self): project = self.mock_for_keystone_projects(project_count=1, list_get=True)[0] - # TODO(rods) `usage` defaults to False if not set but - # cinderclient is explicitly passing it. We'll have to do - # the same waiting to switch to the REST API call self.register_uris([ dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', - append=['os-quota-sets', project.project_id], - qs_elements=['usage=False']), + append=['os-quota-sets', project.project_id]), json=dict(quota_set={'snapshots': 10, 'volumes': 20}))]) self.op_cloud.get_volume_quotas(project.project_id) self.assert_calls() From fcaf06c3f2d1738590c2ff81a5de45c510a341df Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 7 May 2017 07:30:32 -0500 Subject: [PATCH 1515/3836] Add "Multi Cloud with Shade" presentation Check it out - when we write presentations in presentty, we can add them to our docs! First given at the 2017 Boston OpenStack Summit. Change-Id: Ia46a6f32b6b374974fba7b620b202f8eadc774a1 --- doc/source/examples/cleanup-servers.py | 13 + doc/source/examples/create-server-dict.py | 22 + .../examples/create-server-name-or-id.py | 25 + doc/source/examples/debug-logging.py | 6 + doc/source/examples/find-an-image.py | 7 + doc/source/examples/http-debug-logging.py | 6 + doc/source/examples/munch-dict-object.py | 7 + doc/source/examples/normalization.py | 7 + doc/source/examples/server-information.py | 23 + .../examples/service-conditional-overrides.py | 5 + doc/source/examples/service-conditionals.py | 6 + doc/source/examples/strict-mode.py | 8 + doc/source/examples/upload-large-object.py | 10 + doc/source/examples/upload-object.py | 10 + doc/source/examples/user-agent.py | 6 + doc/source/index.rst | 8 + doc/source/multi-cloud-demo.rst | 811 ++++++++++++++++++ 17 files changed, 980 insertions(+) create mode 100644 doc/source/examples/cleanup-servers.py create mode 100644 doc/source/examples/create-server-dict.py create mode 100644 doc/source/examples/create-server-name-or-id.py create mode 100644 doc/source/examples/debug-logging.py create mode 100644 doc/source/examples/find-an-image.py create mode 100644 doc/source/examples/http-debug-logging.py create mode 100644 doc/source/examples/munch-dict-object.py create mode 100644 doc/source/examples/normalization.py create mode 100644 doc/source/examples/server-information.py create mode 100644 doc/source/examples/service-conditional-overrides.py create mode 100644 doc/source/examples/service-conditionals.py create mode 100644 doc/source/examples/strict-mode.py create mode 100644 doc/source/examples/upload-large-object.py create mode 100644 doc/source/examples/upload-object.py create mode 100644 doc/source/examples/user-agent.py create mode 100644 doc/source/multi-cloud-demo.rst diff --git a/doc/source/examples/cleanup-servers.py b/doc/source/examples/cleanup-servers.py new file mode 100644 index 000000000..0e18ce228 --- /dev/null +++ b/doc/source/examples/cleanup-servers.py @@ -0,0 +1,13 @@ +import shade + +# Initialize and turn on debug logging +shade.simple_logging(debug=True) + +for cloud_name, region_name in [ + ('my-vexxhost', 'ca-ymq-1'), + ('my-citycloud', 'Buf1'), + ('my-internap', 'ams01')]: + # Initialize cloud + cloud = shade.openstack_cloud(cloud=cloud_name, region_name=region_name) + for server in cloud.search_servers('my-server'): + cloud.delete_server(server, wait=True, delete_ips=True) diff --git a/doc/source/examples/create-server-dict.py b/doc/source/examples/create-server-dict.py new file mode 100644 index 000000000..5fa9400ce --- /dev/null +++ b/doc/source/examples/create-server-dict.py @@ -0,0 +1,22 @@ +import shade + +# Initialize and turn on debug logging +shade.simple_logging(debug=True) + +for cloud_name, region_name, image, flavor_id in [ + ('my-vexxhost', 'ca-ymq-1', 'Ubuntu 16.04.1 LTS [2017-03-03]', + '5cf64088-893b-46b5-9bb1-ee020277635d'), + ('my-citycloud', 'Buf1', 'Ubuntu 16.04 Xenial Xerus', + '0dab10b5-42a2-438e-be7b-505741a7ffcc'), + ('my-internap', 'ams01', 'Ubuntu 16.04 LTS (Xenial Xerus)', + 'A1.4')]: + # Initialize cloud + cloud = shade.openstack_cloud(cloud=cloud_name, region_name=region_name) + + # Boot a server, wait for it to boot, and then do whatever is needed + # to get a public ip for it. + server = cloud.create_server( + 'my-server', image=image, flavor=dict(id=flavor_id), + wait=True, auto_ip=True) + # Delete it - this is a demo + cloud.delete_server(server, wait=True, delete_ips=True) diff --git a/doc/source/examples/create-server-name-or-id.py b/doc/source/examples/create-server-name-or-id.py new file mode 100644 index 000000000..66ab20e8d --- /dev/null +++ b/doc/source/examples/create-server-name-or-id.py @@ -0,0 +1,25 @@ +import shade + +# Initialize and turn on debug logging +shade.simple_logging(debug=True) + +for cloud_name, region_name, image, flavor in [ + ('my-vexxhost', 'ca-ymq-1', + 'Ubuntu 16.04.1 LTS [2017-03-03]', 'v1-standard-4'), + ('my-citycloud', 'Buf1', + 'Ubuntu 16.04 Xenial Xerus', '4C-4GB-100GB'), + ('my-internap', 'ams01', + 'Ubuntu 16.04 LTS (Xenial Xerus)', 'A1.4')]: + # Initialize cloud + cloud = shade.openstack_cloud(cloud=cloud_name, region_name=region_name) + cloud.delete_server('my-server', wait=True, delete_ips=True) + + # Boot a server, wait for it to boot, and then do whatever is needed + # to get a public ip for it. + server = cloud.create_server( + 'my-server', image=image, flavor=flavor, wait=True, auto_ip=True) + print(server.name) + print(server['name']) + cloud.pprint(server) + # Delete it - this is a demo + cloud.delete_server(server, wait=True, delete_ips=True) diff --git a/doc/source/examples/debug-logging.py b/doc/source/examples/debug-logging.py new file mode 100644 index 000000000..bf177f320 --- /dev/null +++ b/doc/source/examples/debug-logging.py @@ -0,0 +1,6 @@ +import shade +shade.simple_logging(debug=True) + +cloud = shade.openstack_cloud( + cloud='my-vexxhost', region_name='ca-ymq-1') +cloud.get_image('Ubuntu 16.04.1 LTS [2017-03-03]') diff --git a/doc/source/examples/find-an-image.py b/doc/source/examples/find-an-image.py new file mode 100644 index 000000000..b7bfdb483 --- /dev/null +++ b/doc/source/examples/find-an-image.py @@ -0,0 +1,7 @@ +import shade +shade.simple_logging() + +cloud = shade.openstack_cloud(cloud='fuga', region_name='cystack') +cloud.pprint([ + image for image in cloud.list_images() + if 'ubuntu' in image.name.lower()]) diff --git a/doc/source/examples/http-debug-logging.py b/doc/source/examples/http-debug-logging.py new file mode 100644 index 000000000..1a5b57f56 --- /dev/null +++ b/doc/source/examples/http-debug-logging.py @@ -0,0 +1,6 @@ +import shade +shade.simple_logging(http_debug=True) + +cloud = shade.openstack_cloud( + cloud='my-vexxhost', region_name='ca-ymq-1') +cloud.get_image('Ubuntu 16.04.1 LTS [2017-03-03]') diff --git a/doc/source/examples/munch-dict-object.py b/doc/source/examples/munch-dict-object.py new file mode 100644 index 000000000..b5430dd2c --- /dev/null +++ b/doc/source/examples/munch-dict-object.py @@ -0,0 +1,7 @@ +import shade +shade.simple_logging(debug=True) + +cloud = shade.openstack_cloud(cloud='ovh', region_name='SBG1') +image = cloud.get_image('Ubuntu 16.10') +print(image.name) +print(image['name']) diff --git a/doc/source/examples/normalization.py b/doc/source/examples/normalization.py new file mode 100644 index 000000000..220214f2f --- /dev/null +++ b/doc/source/examples/normalization.py @@ -0,0 +1,7 @@ +import shade +shade.simple_logging() + +cloud = shade.openstack_cloud(cloud='fuga', region_name='cystack') +image = cloud.get_image( + 'Ubuntu 16.04 LTS - Xenial Xerus - 64-bit - Fuga Cloud Based Image') +cloud.pprint(image) diff --git a/doc/source/examples/server-information.py b/doc/source/examples/server-information.py new file mode 100644 index 000000000..79f45ea53 --- /dev/null +++ b/doc/source/examples/server-information.py @@ -0,0 +1,23 @@ +import shade +shade.simple_logging(debug=True) + +cloud = shade.openstack_cloud(cloud='my-citycloud', region_name='Buf1') +try: + server = cloud.create_server( + 'my-server', image='Ubuntu 16.04 Xenial Xerus', + flavor=dict(id='0dab10b5-42a2-438e-be7b-505741a7ffcc'), + wait=True, auto_ip=True) + + print("\n\nFull Server\n\n") + cloud.pprint(server) + + print("\n\nTurn Detailed Off\n\n") + cloud.pprint(cloud.get_server('my-server', detailed=False)) + + print("\n\nBare Server\n\n") + cloud.pprint(cloud.get_server('my-server', bare=True)) + +finally: + # Delete it - this is a demo + cloud.delete_server(server, wait=True, delete_ips=True) + diff --git a/doc/source/examples/service-conditional-overrides.py b/doc/source/examples/service-conditional-overrides.py new file mode 100644 index 000000000..1b88f2033 --- /dev/null +++ b/doc/source/examples/service-conditional-overrides.py @@ -0,0 +1,5 @@ +import shade +shade.simple_logging(debug=True) + +cloud = shade.openstack_cloud(cloud='rax', region_name='DFW') +print(cloud.has_service('network')) diff --git a/doc/source/examples/service-conditionals.py b/doc/source/examples/service-conditionals.py new file mode 100644 index 000000000..2fa404c64 --- /dev/null +++ b/doc/source/examples/service-conditionals.py @@ -0,0 +1,6 @@ +import shade +shade.simple_logging(debug=True) + +cloud = shade.openstack_cloud(cloud='kiss', region_name='region1') +print(cloud.has_service('network')) +print(cloud.has_service('container-orchestration')) diff --git a/doc/source/examples/strict-mode.py b/doc/source/examples/strict-mode.py new file mode 100644 index 000000000..e67bfc8cd --- /dev/null +++ b/doc/source/examples/strict-mode.py @@ -0,0 +1,8 @@ +import shade +shade.simple_logging() + +cloud = shade.openstack_cloud( + cloud='fuga', region_name='cystack', strict=True) +image = cloud.get_image( + 'Ubuntu 16.04 LTS - Xenial Xerus - 64-bit - Fuga Cloud Based Image') +cloud.pprint(image) diff --git a/doc/source/examples/upload-large-object.py b/doc/source/examples/upload-large-object.py new file mode 100644 index 000000000..4a83728e5 --- /dev/null +++ b/doc/source/examples/upload-large-object.py @@ -0,0 +1,10 @@ +import shade +shade.simple_logging(debug=True) + +cloud = shade.openstack_cloud(cloud='ovh', region_name='SBG1') +cloud.create_object( + container='my-container', name='my-object', + filename='/home/mordred/briarcliff.sh3d', + segment_size=1000000) +cloud.delete_object('my-container', 'my-object') +cloud.delete_container('my-container') diff --git a/doc/source/examples/upload-object.py b/doc/source/examples/upload-object.py new file mode 100644 index 000000000..4a83728e5 --- /dev/null +++ b/doc/source/examples/upload-object.py @@ -0,0 +1,10 @@ +import shade +shade.simple_logging(debug=True) + +cloud = shade.openstack_cloud(cloud='ovh', region_name='SBG1') +cloud.create_object( + container='my-container', name='my-object', + filename='/home/mordred/briarcliff.sh3d', + segment_size=1000000) +cloud.delete_object('my-container', 'my-object') +cloud.delete_container('my-container') diff --git a/doc/source/examples/user-agent.py b/doc/source/examples/user-agent.py new file mode 100644 index 000000000..01aba7706 --- /dev/null +++ b/doc/source/examples/user-agent.py @@ -0,0 +1,6 @@ +import shade +shade.simple_logging(http_debug=True) + +cloud = shade.openstack_cloud( + cloud='datacentred', app_name='AmazingApp', app_version='1.0') +cloud.list_networks() diff --git a/doc/source/index.rst b/doc/source/index.rst index c298c6ac2..32ae8955e 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -21,6 +21,14 @@ Contents: .. include:: ../../README.rst +Presentations +============= + +.. toctree:: + :maxdepth: 1 + + multi-cloud-demo + Indices and tables ================== diff --git a/doc/source/multi-cloud-demo.rst b/doc/source/multi-cloud-demo.rst new file mode 100644 index 000000000..7b584487f --- /dev/null +++ b/doc/source/multi-cloud-demo.rst @@ -0,0 +1,811 @@ +================ +Multi-Cloud Demo +================ + +This document contains a presentation in `presentty`_ format. If you want to +walk through it like a presentation, install `presentty` and run: + +.. code:: bash + + presentty doc/source/multi-cloud-demo.rst + +The content is hopefully helpful even if it's not being narrated, so it's being +included in the `shade` docs. + +.. _presentty: https://pypi.python.org/pypi/presentty + +Using Multiple OpenStack Clouds Easily with Shade +================================================= + +Who am I? +========= + +Monty Taylor + +* OpenStack Infra Core +* irc: mordred +* twitter: @e_monty + +What are we going to talk about? +================================ + +`shade` + +* a task and end-user oriented Python library +* abstracts deployment differences +* designed for multi-cloud +* simple to use +* massive scale + + * optional advanced features to handle 20k servers a day + +* Initial logic/design extracted from nodepool +* Librified to re-use in Ansible + +shade is Free Software +====================== + +* https://git.openstack.org/cgit/openstack-infra/shade +* openstack-dev@lists.openstack.org +* #openstack-shade on freenode + +This talk is Free Software, too +=============================== + +* Written for presentty (https://pypi.python.org/pypi/presentty) +* doc/source/multi-cloud-demo.rst +* examples in doc/source/examples +* Paths subject to change- this is the first presentation in tree! + +Complete Example +================ + +.. code:: python + + import shade + + # Initialize and turn on debug logging + shade.simple_logging(debug=True) + + for cloud_name, region_name in [ + ('my-vexxhost', 'ca-ymq-1'), + ('my-citycloud', 'Buf1'), + ('my-internap', 'ams01')]: + # Initialize cloud + cloud = shade.openstack_cloud(cloud=cloud_name, region_name=region_name) + + # Upload an image to the cloud + image = cloud.create_image( + 'devuan-jessie', filename='devuan-jessie.qcow2', wait=True) + + # Find a flavor with at least 512M of RAM + flavor = cloud.get_flavor_by_ram(512) + + # Boot a server, wait for it to boot, and then do whatever is needed + # to get a public ip for it. + cloud.create_server( + 'my-server', image=image, flavor=flavor, wait=True, auto_ip=True) + +Let's Take a Few Steps Back +=========================== + +Multi-cloud is easy, but you need to know a few things. + +* Terminology +* Config +* Shade API + +Cloud Terminology +================= + +Let's define a few terms, so that we can use them with ease: + +* `cloud` - logically related collection of services +* `region` - completely independent subset of a given cloud +* `patron` - human who has an account +* `user` - account on a cloud +* `project` - logical collection of cloud resources +* `domain` - collection of users and projects + +Cloud Terminology Relationships +=============================== + +* A `cloud` has one or more `regions` +* A `patron` has one or more `users` +* A `patron` has one or more `projects` +* A `cloud` has one or more `domains` +* In a `cloud` with one `domain` it is named "default" +* Each `patron` may have their own `domain` +* Each `user` is in one `domain` +* Each `project` is in one `domain` +* A `user` has one or more `roles` on one or more `projects` + +HTTP Sessions +============= + +* HTTP interactions are authenticated via keystone +* Authenticating returns a `token` +* An authenticated HTTP Session is shared across a `region` + +Cloud Regions +============= + +A `cloud region` is the basic unit of REST interaction. + +* A `cloud` has a `service catalog` +* The `service catalog` is returned in the `token` +* The `service catalog` lists `endpoint` for each `service` in each `region` +* A `region` is completely autonomous + +Users, Projects and Domains +=========================== + +In clouds with multiple domains, project and user names are +only unique within a region. + +* Names require `domain` information for uniqueness. IDs do not. +* Providing `domain` information when not needed is fine. +* `project_name` requires `project_domain_name` or `project_domain_id` +* `project_id` does not +* `username` requires `user_domain_name` or `user_domain_id` +* `user_id` does not + +Confused Yet? +============= + +Don't worry - you don't have to deal with most of that. + +Auth per cloud, select per region +================================= + +In general, the thing you need to know is: + +* Configure authentication per `cloud` +* Select config to use by `cloud` and `region` + +clouds.yaml +=========== + +Information about the clouds you want to connect to is stored in a file +called `clouds.yaml`. + +`clouds.yaml` can be in your homedir: `~/.config/openstack/clouds.yaml` +or system-wide: `/etc/openstack/clouds.yaml`. + +Information in your homedir, if it exists, takes precedence. + +Full docs on `clouds.yaml` are at +https://docs.openstack.org/developer/os-client-config/ + +What about Mac and Windows? +=========================== + +`USER_CONFIG_DIR` is different on Linux, OSX and Windows. + +* Linux: `~/.config/openstack` +* OSX: `~/Library/Application Support/openstack` +* Windows: `C:\\Users\\USERNAME\\AppData\\Local\\OpenStack\\openstack` + +`SITE_CONFIG_DIR` is different on Linux, OSX and Windows. + +* Linux: `/etc/openstack` +* OSX: `/Library/Application Support/openstack` +* Windows: `C:\\ProgramData\\OpenStack\\openstack` + +Config Terminology +================== + +For multi-cloud, think of two types: + +* `profile` - Facts about the `cloud` that are true for everyone +* `cloud` - Information specific to a given `user` + +Apologies for the use of `cloud` twice. + +Environment Variables and Simple Usage +====================================== + +* Environment variables starting with `OS_` go into a cloud called `envvars` +* If you only have one cloud, you don't have to specify it +* `OS_CLOUD` and `OS_REGION_NAME` are default values for + `cloud` and `region_name` + +TOO MUCH TALKING - NOT ENOUGH CODE +================================== + +basic clouds.yaml for the example code +====================================== + +Simple example of a clouds.yaml + +* Config for a named `cloud` "my-citycloud" +* Reference a well-known "named" profile: `citycloud` +* `os-client-config` has a built-in list of profiles at + https://docs.openstack.org/developer/os-client-config/vendor-support.html +* Vendor profiles contain various advanced config +* `cloud` name can match `profile` name (using different names for clarity) + +.. code:: yaml + + clouds: + my-citycloud: + profile: citycloud + auth: + username: mordred + project_id: 65222a4d09ea4c68934fa1028c77f394 + user_domain_id: d0919bd5e8d74e49adf0e145807ffc38 + project_domain_id: d0919bd5e8d74e49adf0e145807ffc38 + +Where's the password? + +secure.yaml +=========== + +* Optional additional file just like `clouds.yaml` +* Values overlaid on `clouds.yaml` +* Useful if you want to protect secrets more stringently + +Example secure.yaml +=================== + +* No, my password isn't XXXXXXXX +* `cloud` name should match `clouds.yaml` +* Optional - I actually keep mine in my `clouds.yaml` + +.. code:: yaml + + clouds: + my-citycloud: + auth: + password: XXXXXXXX + +more clouds.yaml +================ + +More information can be provided. + +* Use v3 of the `identity` API - even if others are present +* Use `https://image-ca-ymq-1.vexxhost.net/v2` for `image` API + instead of what's in the catalog + +.. code:: yaml + + my-vexxhost: + identity_api_version: 3 + image_endpoint_override: https://image-ca-ymq-1.vexxhost.net/v2 + profile: vexxhost + auth: + user_domain_id: default + project_domain_id: default + project_name: d8af8a8f-a573-48e6-898a-af333b970a2d + username: 0b8c435b-cc4d-4e05-8a47-a2ada0539af1 + +Much more complex clouds.yaml example +===================================== + +* Not using a profile - all settings included +* In the `ams01` `region` there are two networks with undiscoverable qualities +* Each one are labeled here so choices can be made +* Any of the settings can be specific to a `region` if needed +* `region` settings override `cloud` settings +* `cloud` does not support `floating-ips` + +.. code:: yaml + + my-internap: + auth: + auth_url: https://identity.api.cloud.iweb.com + username: api-55f9a00fb2619 + project_name: inap-17037 + identity_api_version: 3 + floating_ip_source: None + regions: + - name: ams01 + values: + networks: + - name: inap-17037-WAN1654 + routes_externally: true + default_interface: true + - name: inap-17037-LAN3631 + routes_externally: false + +Complete Example Again +====================== + +.. code:: python + + import shade + + # Initialize and turn on debug logging + shade.simple_logging(debug=True) + + for cloud_name, region_name in [ + ('my-vexxhost', 'ca-ymq-1'), + ('my-citycloud', 'Buf1'), + ('my-internap', 'ams01')]: + # Initialize cloud + cloud = shade.openstack_cloud(cloud=cloud_name, region_name=region_name) + + # Upload an image to the cloud + image = cloud.create_image( + 'devuan-jessie', filename='devuan-jessie.qcow2', wait=True) + + # Find a flavor with at least 512M of RAM + flavor = cloud.get_flavor_by_ram(512) + + # Boot a server, wait for it to boot, and then do whatever is needed + # to get a public ip for it. + cloud.create_server( + 'my-server', image=image, flavor=flavor, wait=True, auto_ip=True) + +Step By Step +============ + +Import the library +================== + +.. code:: python + + import shade + +Logging +======= + +* `shade` uses standard python logging +* Special `shade.request_ids` logger for API request IDs +* `simple_logging` does easy defaults +* Squelches some meaningless warnings + + * `debug` + + * Logs shade loggers at debug level + * Includes `shade.request_ids` debug logging + + * `http_debug` Implies `debug`, turns on HTTP tracing + +.. code:: python + + # Initialize and turn on debug logging + shade.simple_logging(debug=True) + +Example with Debug Logging +========================== + +* doc/source/examples/debug-logging.py + +.. code:: python + + import shade + shade.simple_logging(debug=True) + + cloud = shade.openstack_cloud( + cloud='my-vexxhost', region_name='ca-ymq-1') + cloud.get_image('Ubuntu 16.04.1 LTS [2017-03-03]') + +Example with HTTP Debug Logging +=============================== + +* doc/source/examples/http-debug-logging.py + +.. code:: python + + import shade + shade.simple_logging(http_debug=True) + + cloud = shade.openstack_cloud( + cloud='my-vexxhost', region_name='ca-ymq-1') + cloud.get_image('Ubuntu 16.04.1 LTS [2017-03-03]') + +Cloud Regions +============= + +* `cloud` constructor needs `cloud` and `region_name` +* `shade.openstack_cloud` is a helper factory function + +.. code:: python + + for cloud_name, region_name in [ + ('my-vexxhost', 'ca-ymq-1'), + ('my-citycloud', 'Buf1'), + ('my-internap', 'ams01')]: + # Initialize cloud + cloud = shade.openstack_cloud(cloud=cloud_name, region_name=region_name) + +Upload an Image +=============== + +* Picks the correct upload mechanism +* **SUGGESTION** Always upload your own base images + +.. code:: python + + # Upload an image to the cloud + image = cloud.create_image( + 'devuan-jessie', filename='devuan-jessie.qcow2', wait=True) + +Always Upload an Image +====================== + +Ok. You don't have to. But, for multi-cloud... + +* Images with same content are named different on different clouds +* Images with same name on different clouds can have different content +* Upload your own to all clouds, both problems go away +* Download from OS vendor or build with `diskimage-builder` + +Find a flavor +============= + +* Flavors are all named differently on clouds +* Flavors can be found via RAM +* `get_flavor_by_ram` finds the smallest matching flavor + +.. code:: python + + # Find a flavor with at least 512M of RAM + flavor = cloud.get_flavor_by_ram(512) + +Create a server +=============== + +* my-vexxhost + + * Boot server + * Wait for `status==ACTIVE` + +* my-internap + + * Boot server on network `inap-17037-WAN1654` + * Wait for `status==ACTIVE` + +* my-citycloud + + * Boot server + * Wait for `status==ACTIVE` + * Find the `port` for the `fixed_ip` for `server` + * Create `floating-ip` on that `port` + * Wait for `floating-ip` to attach + +.. code:: python + + # Boot a server, wait for it to boot, and then do whatever is needed + # to get a public ip for it. + cloud.create_server( + 'my-server', image=image, flavor=flavor, wait=True, auto_ip=True) + +Wow. We didn't even deploy Wordpress! +===================================== + +Image and Flavor by Name or ID +============================== + +* Pass string to image/flavor +* Image/Flavor will be found by name or ID +* Common pattern +* doc/source/examples/create-server-name-or-id.py + +.. code:: python + + import shade + + # Initialize and turn on debug logging + shade.simple_logging(debug=True) + + for cloud_name, region_name, image, flavor in [ + ('my-vexxhost', 'ca-ymq-1', + 'Ubuntu 16.04.1 LTS [2017-03-03]', 'v1-standard-4'), + ('my-citycloud', 'Buf1', + 'Ubuntu 16.04 Xenial Xerus', '4C-4GB-100GB'), + ('my-internap', 'ams01', + 'Ubuntu 16.04 LTS (Xenial Xerus)', 'A1.4')]: + # Initialize cloud + cloud = shade.openstack_cloud(cloud=cloud_name, region_name=region_name) + + # Boot a server, wait for it to boot, and then do whatever is needed + # to get a public ip for it. + server = cloud.create_server( + 'my-server', image=image, flavor=flavor, wait=True, auto_ip=True) + print(server.name) + print(server['name']) + cloud.pprint(server) + # Delete it - this is a demo + cloud.delete_server(server, wait=True, delete_ips=True) + +cloud.pprint method was just added this morning +=============================================== + +Delete Servers +============== + +* `delete_ips` Delete any `floating_ips` the server may have + +.. code:: python + + cloud.delete_server('my-server', wait=True, delete_ips=True) + +Image and Flavor by Dict +======================== + +* Pass dict to image/flavor +* If you know if the value is Name or ID +* Common pattern +* doc/source/examples/create-server-dict.py + +.. code:: python + + import shade + + # Initialize and turn on debug logging + shade.simple_logging(debug=True) + + for cloud_name, region_name, image, flavor_id in [ + ('my-vexxhost', 'ca-ymq-1', 'Ubuntu 16.04.1 LTS [2017-03-03]', + '5cf64088-893b-46b5-9bb1-ee020277635d'), + ('my-citycloud', 'Buf1', 'Ubuntu 16.04 Xenial Xerus', + '0dab10b5-42a2-438e-be7b-505741a7ffcc'), + ('my-internap', 'ams01', 'Ubuntu 16.04 LTS (Xenial Xerus)', + 'A1.4')]: + # Initialize cloud + cloud = shade.openstack_cloud(cloud=cloud_name, region_name=region_name) + + # Boot a server, wait for it to boot, and then do whatever is needed + # to get a public ip for it. + server = cloud.create_server( + 'my-server', image=image, flavor=dict(id=flavor_id), + wait=True, auto_ip=True) + # Delete it - this is a demo + cloud.delete_server(server, wait=True, delete_ips=True) + +Munch Objects +============= + +* Behave like a dict and an object +* doc/source/examples/munch-dict-object.py + +.. code:: python + + import shade + shade.simple_logging(debug=True) + + cloud = shade.openstack_cloud(cloud='zetta', region_name='no-osl1') + image = cloud.get_image('Ubuntu 14.04 (AMD64) [Local Storage]') + print(image.name) + print(image['name']) + +API Organized by Logical Resource +================================= + +* list_servers +* search_servers +* get_server +* create_server +* delete_server +* update_server + +For other things, it's still {verb}_{noun} + +* attach_volume +* wait_for_server +* add_auto_ip + +Cleanup Script +============== + +* Sometimes my examples had bugs +* doc/source/examples/cleanup-servers.py + +.. code:: python + + import shade + + # Initialize and turn on debug logging + shade.simple_logging(debug=True) + + for cloud_name, region_name in [ + ('my-vexxhost', 'ca-ymq-1'), + ('my-citycloud', 'Buf1'), + ('my-internap', 'ams01')]: + # Initialize cloud + cloud = shade.openstack_cloud(cloud=cloud_name, region_name=region_name) + for server in cloud.search_servers('my-server'): + cloud.delete_server(server, wait=True, delete_ips=True) + +Normalization +============= + +* https://docs.openstack.org/developer/shade/model.html#image +* doc/source/examples/normalization.py + +.. code:: python + + import shade + shade.simple_logging() + + cloud = shade.openstack_cloud(cloud='fuga', region_name='cystack') + image = cloud.get_image( + 'Ubuntu 16.04 LTS - Xenial Xerus - 64-bit - Fuga Cloud Based Image') + cloud.pprint(image) + +Strict Normalized Results +========================= + +* Return only the declared model +* doc/source/examples/strict-mode.py + +.. code:: python + + import shade + shade.simple_logging() + + cloud = shade.openstack_cloud( + cloud='fuga', region_name='cystack', strict=True) + image = cloud.get_image( + 'Ubuntu 16.04 LTS - Xenial Xerus - 64-bit - Fuga Cloud Based Image') + cloud.pprint(image) + +How Did I Find the Image Name for the Last Example? +=================================================== + +* I often make stupid little utility scripts +* doc/source/examples/find-an-image.py + +.. code:: python + + import shade + shade.simple_logging() + + cloud = shade.openstack_cloud(cloud='fuga', region_name='cystack') + cloud.pprint([ + image for image in cloud.list_images() + if 'ubuntu' in image.name.lower()]) + +Added / Modified Information +============================ + +* Servers need more extra help +* Fetch addresses dict from neutron +* Figure out which IPs are good +* `detailed` - defaults to True, add everything +* `bare` - no extra calls - don't even fix broken things +* `bare` is still normalized +* doc/source/examples/server-information.py + +.. code:: python + + import shade + shade.simple_logging(debug=True) + + cloud = shade.openstack_cloud(cloud='my-citycloud', region_name='Buf1') + try: + server = cloud.create_server( + 'my-server', image='Ubuntu 16.04 Xenial Xerus', + flavor=dict(id='0dab10b5-42a2-438e-be7b-505741a7ffcc'), + wait=True, auto_ip=True) + + print("\n\nFull Server\n\n") + cloud.pprint(server) + + print("\n\nTurn Detailed Off\n\n") + cloud.pprint(cloud.get_server('my-server', detailed=False)) + + print("\n\nBare Server\n\n") + cloud.pprint(cloud.get_server('my-server', bare=True)) + + finally: + # Delete it - this is a demo + cloud.delete_server(server, wait=True, delete_ips=True) + +Exceptions +========== + +* All shade exceptions are subclasses of `OpenStackCloudException` +* Direct REST calls throw `OpenStackCloudHTTPError` +* `OpenStackCloudHTTPError` subclasses `OpenStackCloudException` + and `requests.exceptions.HTTPError` +* `OpenStackCloudURINotFound` for 404 +* `OpenStackCloudBadRequest` for 400 + +User Agent Info +=============== + +* Set `app_name` and `app_version` for User Agents +* (sssh ... `region_name` is optional if the cloud has one region) +* doc/source/examples/user-agent.py + +.. code:: python + + import shade + shade.simple_logging(http_debug=True) + + cloud = shade.openstack_cloud( + cloud='datacentred', app_name='AmazingApp', app_version='1.0') + cloud.list_networks() + +Uploading Large Objects +======================= + +* swift has a maximum object size +* Large Objects are uploaded specially +* shade figures this out and does it +* multi-threaded +* doc/source/examples/upload-object.py + +.. code:: python + + import shade + shade.simple_logging(debug=True) + + cloud = shade.openstack_cloud(cloud='ovh', region_name='SBG1') + cloud.create_object( + container='my-container', name='my-object', + filename='/home/mordred/briarcliff.sh3d') + cloud.delete_object('my-container', 'my-object') + cloud.delete_container('my-container') + +Uploading Large Objects +======================= + +* Default max_file_size is 5G +* This is a conference demo +* Let's force a segment_size +* One MILLION bytes +* doc/source/examples/upload-object.py + +.. code:: python + + import shade + shade.simple_logging(debug=True) + + cloud = shade.openstack_cloud(cloud='ovh', region_name='SBG1') + cloud.create_object( + container='my-container', name='my-object', + filename='/home/mordred/briarcliff.sh3d', + segment_size=1000000) + cloud.delete_object('my-container', 'my-object') + cloud.delete_container('my-container') + +Service Conditionals +==================== + +.. code:: python + + import shade + shade.simple_logging(debug=True) + + cloud = shade.openstack_cloud(cloud='kiss', region_name='region1') + print(cloud.has_service('network')) + print(cloud.has_service('container-orchestration')) + +Service Conditional Overrides +============================= + +* Sometimes clouds are weird and figuring that out won't work + +.. code:: python + + import shade + shade.simple_logging(debug=True) + + cloud = shade.openstack_cloud(cloud='rax', region_name='DFW') + print(cloud.has_service('network')) + +.. code:: yaml + + clouds: + rax: + profile: rackspace + auth: + username: mordred + project_id: 245018 + # This is already in profile: rackspace + has_network: false + +Coming Soon +=========== + +* Completion of RESTification +* Full version discovery support +* Multi-cloud facade layer +* Microversion support (talk tomorrow) +* Completion of caching tier (talk tomorrow) +* All of you helping hacking on shade!!! (we're friendly) From 61141ae9cff44f20e6ad7ce8dfd71b47ef088b2c Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Mon, 8 May 2017 19:23:12 +0000 Subject: [PATCH 1516/3836] Remove direct calls to cinderclient Change-Id: Iad89e4a0c13e28f545b2d164417b60ee39cb5b12 Signed-off-by: Rosario Di Somma --- shade/tests/functional/test_volume_type.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/shade/tests/functional/test_volume_type.py b/shade/tests/functional/test_volume_type.py index e1c5a66f3..03567837f 100644 --- a/shade/tests/functional/test_volume_type.py +++ b/shade/tests/functional/test_volume_type.py @@ -34,13 +34,18 @@ def setUp(self): super(TestVolumeType, self).setUp() if not self.user_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') - self.operator_cloud.cinder_client.volume_types.create( - 'test-volume-type', is_public=False) + volume_type = { + "name": 'test-volume-type', + "description": None, + "os-volume-type-access:is_public": False} + self.operator_cloud._volume_client.post( + '/types', json={'volume_type': volume_type}) def tearDown(self): ret = self.operator_cloud.get_volume_type('test-volume-type') if ret.get('id'): - self.operator_cloud.cinder_client.volume_types.delete(ret.id) + self.operator_cloud._volume_client.delete( + '/types/{volume_type_id}'.format(volume_type_id=ret.id)) super(TestVolumeType, self).tearDown() def test_list_volume_types(self): From d1d9ed7cd428e55a8e6b7f124a90659da739b69a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Tue, 9 May 2017 21:46:20 +0000 Subject: [PATCH 1517/3836] Remove designateclient mock from zones tests This patch adds also dns service to service catalog mock used in tests. Change-Id: Ic7ce778f58e9fd28951eb02067f6bbf86a14b0cd --- shade/tests/unit/fixtures/catalog-v2.json | 14 ++ shade/tests/unit/fixtures/catalog-v3.json | 15 +- shade/tests/unit/test_zone.py | 202 ++++++++++++++++------ 3 files changed, 175 insertions(+), 56 deletions(-) diff --git a/shade/tests/unit/fixtures/catalog-v2.json b/shade/tests/unit/fixtures/catalog-v2.json index 653ca1070..aa8744355 100644 --- a/shade/tests/unit/fixtures/catalog-v2.json +++ b/shade/tests/unit/fixtures/catalog-v2.json @@ -112,6 +112,20 @@ ], "type": "object-store", "name": "swift" + }, + { + "endpoints_links": [], + "endpoints": [ + { + "adminURL": "https://dns.example.com", + "region": "RegionOne", + "publicURL": "https://dns.example.com", + "internalURL": "https://dns.example.com", + "id": "652f0612744042bfbb8a8bb2c777a16d" + } + ], + "type": "dns", + "name": "designate" } ], "user": { diff --git a/shade/tests/unit/fixtures/catalog-v3.json b/shade/tests/unit/fixtures/catalog-v3.json index 0c372603e..c4bb48f6c 100644 --- a/shade/tests/unit/fixtures/catalog-v3.json +++ b/shade/tests/unit/fixtures/catalog-v3.json @@ -113,7 +113,7 @@ { "endpoints": [ { - "id": "4deb4d0504a044a395d4480741ba628c", + "id": "652f0612744042bfbb8a8bb2c777a16d", "interface": "public", "region": "RegionOne", "url": "https://orchestration.example.com/v1/1c36b64c840a42cd9e9b931a369337f0" @@ -122,6 +122,19 @@ "endpoints_links": [], "name": "heat", "type": "orchestration" + }, + { + "endpoints": [ + { + "id": "10c76ffd2b744a67950ed1365190d352", + "interface": "public", + "region": "RegionOne", + "url": "https://dns.example.com" + } + ], + "endpoints_links": [], + "name": "designate", + "type": "dns" } ], "expires_at": "9999-12-31T23:59:59Z", diff --git a/shade/tests/unit/test_zone.py b/shade/tests/unit/test_zone.py index 2049fcbf9..1fb3d0c91 100644 --- a/shade/tests/unit/test_zone.py +++ b/shade/tests/unit/test_zone.py @@ -10,86 +10,178 @@ # License for the specific language governing permissions and limitations # under the License. - -import mock +import copy import testtools import shade -from shade.tests import fakes from shade.tests.unit import base -zone_obj = fakes.FakeZone( - id='1', - name='example.net.', - type_='PRIMARY', - email='test@example.net', - description='Example zone', - ttl=3600, - masters=None -) +api_versions = { + "values": [{ + "id": "v1", + "links": [ + { + "href": "https://dns.example.com/v1", + "rel": "self" + } + ], + "status": "DEPRECATED" + }, { + "id": "v2", + "links": [ + { + "href": "https://dns.example.com/v2", + "rel": "self" + } + ], + "status": "CURRENT" + }] +} + +zone_dict = { + 'name': 'example.net.', + 'type': 'PRIMARY', + 'email': 'test@example.net', + 'description': 'Example zone', + 'ttl': 3600, +} +new_zone_dict = copy.copy(zone_dict) +new_zone_dict['id'] = '1' -class TestZone(base.TestCase): - def setUp(self): - super(TestZone, self).setUp() - self.cloud = shade.openstack_cloud(validate=False) +class TestZone(base.RequestsMockTestCase): - @mock.patch.object(shade.OpenStackCloud, 'designate_client') - def test_create_zone(self, mock_designate): - self.cloud.create_zone(name=zone_obj.name, zone_type=zone_obj.type_, - email=zone_obj.email, - description=zone_obj.description, - ttl=zone_obj.ttl, masters=zone_obj.masters) - mock_designate.zones.create.assert_called_once_with( - name=zone_obj.name, type_=zone_obj.type_.upper(), - email=zone_obj.email, description=zone_obj.description, - ttl=zone_obj.ttl, masters=zone_obj.masters - ) + def test_create_zone(self): + self.register_uris([ + dict(method='GET', + uri="https://dns.example.com/", json=api_versions), + dict(method='POST', + uri=self.get_mock_url( + 'dns', 'public', append=['zones']), + json=new_zone_dict, + validate=dict( + json=zone_dict)) + ]) + z = self.cloud.create_zone( + name=zone_dict['name'], + zone_type=zone_dict['type'], + email=zone_dict['email'], + description=zone_dict['description'], + ttl=zone_dict['ttl'], + masters=None) + self.assertEqual(new_zone_dict, z) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'designate_client') - def test_create_zone_exception(self, mock_designate): - mock_designate.zones.create.side_effect = Exception() + def test_create_zone_exception(self): + self.register_uris([ + dict(method='GET', + uri="https://dns.example.com/", json=api_versions), + dict(method='POST', + uri=self.get_mock_url( + 'dns', 'public', append=['zones']), + status_code=500) + ]) with testtools.ExpectedException( shade.OpenStackCloudException, "Unable to create zone example.net." ): self.cloud.create_zone('example.net.') + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'designate_client') - def test_update_zone(self, mock_designate): + def test_update_zone(self): new_ttl = 7200 - mock_designate.zones.list.return_value = [zone_obj] - self.cloud.update_zone('1', ttl=new_ttl) - mock_designate.zones.update.assert_called_once_with( - zone='1', values={'ttl': new_ttl} - ) + updated_zone = copy.copy(new_zone_dict) + updated_zone['ttl'] = new_ttl + self.register_uris([ + dict(method='GET', + uri="https://dns.example.com/", json=api_versions), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['zones']), + json={"zones": [new_zone_dict]}), + dict(method='GET', + uri="https://dns.example.com/", json=api_versions), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['zones'], + qs_elements=['name=1']), + json={'zones': [new_zone_dict]}), + dict(method='GET', + uri="https://dns.example.com/", + json=api_versions), + dict(method='PATCH', + uri=self.get_mock_url( + 'dns', 'public', append=['zones', '1']), + json=updated_zone, + validate=dict( + json={"ttl": new_ttl})) + ]) + z = self.cloud.update_zone('1', ttl=new_ttl) + self.assertEqual(updated_zone, z) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'designate_client') - def test_delete_zone(self, mock_designate): - mock_designate.zones.list.return_value = [zone_obj] - self.cloud.delete_zone('1') - mock_designate.zones.delete.assert_called_once_with( - zone='1' - ) + def test_delete_zone(self): + self.register_uris([ + dict(method='GET', + uri="https://dns.example.com/", json=api_versions), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['zones']), + json={"zones": [new_zone_dict]}), + dict(method='GET', + uri="https://dns.example.com/", json=api_versions), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['zones'], + qs_elements=['name=1']), + json={'zones': [new_zone_dict]}), + dict(method='GET', + uri="https://dns.example.com/", json=api_versions), + dict(method='DELETE', + uri=self.get_mock_url( + 'dns', 'public', append=['zones', '1']), + json=new_zone_dict) + ]) + self.assertTrue(self.cloud.delete_zone('1')) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'designate_client') - def test_get_zone_by_id(self, mock_designate): - mock_designate.zones.list.return_value = [zone_obj] + def test_get_zone_by_id(self): + self.register_uris([ + dict(method='GET', + uri="https://dns.example.com/", json=api_versions), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['zones']), + json={"zones": [new_zone_dict]}) + ]) zone = self.cloud.get_zone('1') - self.assertTrue(mock_designate.zones.list.called) self.assertEqual(zone['id'], '1') + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'designate_client') - def test_get_zone_by_name(self, mock_designate): - mock_designate.zones.list.return_value = [zone_obj] + def test_get_zone_by_name(self): + self.register_uris([ + dict(method='GET', + uri="https://dns.example.com/", json=api_versions), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['zones']), + json={"zones": [new_zone_dict]}) + ]) zone = self.cloud.get_zone('example.net.') - self.assertTrue(mock_designate.zones.list.called) self.assertEqual(zone['name'], 'example.net.') + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'designate_client') - def test_get_zone_not_found_returns_false(self, mock_designate): - mock_designate.zones.list.return_value = [] + def test_get_zone_not_found_returns_false(self): + self.register_uris([ + dict(method='GET', + uri="https://dns.example.com/", json=api_versions), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['zones']), + json={"zones": []}) + ]) zone = self.cloud.get_zone('nonexistingzone.net.') self.assertFalse(zone) + self.assert_calls() From ef35f02ea61a96ef025923cc3653bb67b4ef03ad Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 11 May 2017 21:08:38 -0400 Subject: [PATCH 1518/3836] Add super basic machine normalization In prep for ironic_client conversion to REST, make sure we're removing the novaclient artifacts. Change-Id: I8ef1c5e87492310821c1a0e8ab7e2144c6a10dbc --- shade/_normalize.py | 18 ++++++++++++++++++ shade/operatorcloud.py | 8 +++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/shade/_normalize.py b/shade/_normalize.py index e28298540..7d6443aa2 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -1014,3 +1014,21 @@ def _normalize_stack(self, stack): name=ret['name'], id=ret['id']) ret['properties'] = stack return ret + + def _normalize_machines(self, machines): + """Normalize Ironic Machines""" + ret = [] + for machine in machines: + ret.append(self._normalize_machine(machine)) + return ret + + def _normalize_machine(self, machine): + """Normalize Ironic Machine""" + machine = machine.copy() + + # Discard noise + self._remove_novaclient_artifacts(machine) + + # TODO(mordred) Normalize this resource + + return machine diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 5b54da3ac..760677b32 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -76,7 +76,8 @@ def get_nic_by_mac(self, mac): return None def list_machines(self): - return self.manager.submit_task(_tasks.MachineNodeList()) + return self._normalize_machines( + self.manager.submit_task(_tasks.MachineNodeList())) def get_machine(self, name_or_id): """Get Machine by name or uuid @@ -90,8 +91,9 @@ def get_machine(self, name_or_id): nodes are found. """ try: - return self.manager.submit_task( - _tasks.MachineNodeGet(node_id=name_or_id)) + return self._normalize_machine( + self.manager.submit_task( + _tasks.MachineNodeGet(node_id=name_or_id))) except ironic_exceptions.ClientException: return None From d3b653f23cef0cb0aef45e2a322a54e0626a7336 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 11 May 2017 21:09:47 -0400 Subject: [PATCH 1519/3836] Add 'public' as a default interface for get_mock_url The calls are almost always 'public'. Make it the default. Change-Id: I0923b79d88e839aa4331afb987d27a3200b3b01f --- shade/tests/unit/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index fa7ddf21f..1342650f8 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -175,7 +175,7 @@ def setUp(self, cloud_config_fixture='clouds.yaml'): self.use_keystone_v3() self.__register_uris_called = False - def get_mock_url(self, service_type, interface, resource=None, + def get_mock_url(self, service_type, interface='public', resource=None, append=None, base_url_append=None, qs_elements=None): endpoint_url = self.cloud.endpoint_for( From 3ed97257ec51c3bf42c39d5db142426f8c51dab8 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 12 May 2017 09:45:36 -0500 Subject: [PATCH 1520/3836] Make deprecated client helper method We had a bunch of cargo-culted code inside of the deprecated client factory methods. Make a helper method so that it's easy to mark one as deprecated correctly as we do. Change-Id: Iedc859876c4adc4ccdadd580c5b686e0bda798f0 --- shade/openstackcloud.py | 282 ++++++++++++++++++---------------------- 1 file changed, 127 insertions(+), 155 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 11af3ee0e..d6ba69f7a 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -13,6 +13,7 @@ import collections import functools import hashlib +import importlib import ipaddress import json import jsonpatch @@ -306,6 +307,7 @@ def invalidate(self): self._trove_client = None self._designate_client = None self._magnum_client = None + self._swift_client = None self._raw_clients = {} @@ -542,12 +544,137 @@ def pformat(self, resource): new_resource = _utils._dictify_resource(resource) return pprint.pformat(new_resource) + # LEGACY CLIENT IMPORTS + def _deprecated_import_check(self, module_name): + warnings.warn( + 'Using shade to get a {module_name} object is deprecated. If you' + ' need a {module_name} object, please use make_legacy_client in' + ' os-client-config instead'.format(module_name=module_name)) + try: + importlib.import_module(module_name) + except ImportError: + self.log.error( + '{module_name} is no longer a dependency of shade. You need to' + ' install python-{module_name} directly.'.format( + module_name=module_name)) + raise + + @property + def trove_client(self): + if self._trove_client is None: + _deprecated_import_check('troveclient') + self._trove_client = self._get_client('database') + return self._trove_client + + @property + def magnum_client(self): + if self._magnum_client is None: + _deprecated_import_check('magnumclient') + self._magnum_client = self._get_client('container-infra') + return self._magnum_client + + @property + def neutron_client(self): + if self._neutron_client is None: + _deprecated_import_check('neutronclient') + self._neutron_client = self._get_client('network') + return self._neutron_client + @property def nova_client(self): if self._nova_client is None: self._nova_client = self._get_client('compute', version='2.0') return self._nova_client + @property + def glance_client(self): + if self._glance_client is None: + _deprecated_import_check('glanceclient') + self._glance_client = self._get_client('image') + return self._glance_client + + @property + def heat_client(self): + if self._heat_client is None: + _deprecated_import_check('heatclient') + self._heat_client = self._get_client('orchestration') + return self._heat_client + + @property + def swift_client(self): + if self._swift_client is None: + _deprecated_import_check('swiftclient') + self._swift_client = self._get_client('object-store') + return self._swift_client + + def _get_swift_kwargs(self): + auth_version = self.cloud_config.get_api_version('identity') + auth_args = self.cloud_config.config.get('auth', {}) + os_options = {'auth_version': auth_version} + if auth_version == '2.0': + os_options['os_tenant_name'] = auth_args.get('project_name') + os_options['os_tenant_id'] = auth_args.get('project_id') + else: + os_options['os_project_name'] = auth_args.get('project_name') + os_options['os_project_id'] = auth_args.get('project_id') + + for key in ( + 'username', + 'password', + 'auth_url', + 'user_id', + 'project_domain_id', + 'project_domain_name', + 'user_domain_id', + 'user_domain_name'): + os_options['os_{key}'.format(key=key)] = auth_args.get(key) + return os_options + + @property + def swift_service(self): + # NOTE(mordred): Not using deprecated_client_check because the + # error message needs to be different + try: + import swiftclient.service + except ImportError: + self.log.error( + 'swiftclient is no longer a dependency of shade. You need to' + ' install python-swiftclient directly.') + with _utils.shade_exceptions("Error constructing " + "swift client"): + endpoint = self.get_session_endpoint( + service_key='object-store') + options = dict(os_auth_token=self.auth_token, + os_storage_url=endpoint, + os_region_name=self.region_name) + options.update(self._get_swift_kwargs()) + return swiftclient.service.SwiftService(options=options) + + @property + def cinder_client(self): + + # Import cinderclient late because importing it at the top level + # breaks logging for users of shade + import cinderclient.client # flake8: noqa + if self._cinder_client is None: + self._cinder_client = self._get_client('volume') + return self._cinder_client + + @property + def designate_client(self): + # Note: Explicit constructor is needed until occ 1.27.0 + import designateclient.client # flake8: noqa + if self._designate_client is None: + self._designate_client = self._get_client( + 'dns', designateclient.client.Client) + return self._designate_client + + @property + def keystone_client(self): + if self._keystone_client is None: + self._keystone_client = self._get_client('identity') + return self._keystone_client + @property def keystone_session(self): if self._keystone_session is None: @@ -561,12 +688,6 @@ def keystone_session(self): "Error authenticating to keystone: %s " % str(e)) return self._keystone_session - @property - def keystone_client(self): - if self._keystone_client is None: - self._keystone_client = self._get_client('identity') - return self._keystone_client - @property def _keystone_catalog(self): return self.keystone_session.auth.get_access( @@ -1090,40 +1211,6 @@ def remove_user_from_group(self, name_or_id, group_name_or_id): _tasks.UserRemoveFromGroup(user=user['id'], group=group['id']) ) - @property - def glance_client(self): - warnings.warn( - 'Using shade to get a glance_client object is deprecated. If you' - ' need a raw glanceclient.Client object, please use' - ' make_legacy_client in os-client-config instead') - try: - import glanceclient # flake8: noqa - except ImportError: - self.log.error( - 'glanceclient is no longer a dependency of shade. You need to' - ' install python-glanceclient directly.') - raise - if self._glance_client is None: - self._glance_client = self._get_client('image') - return self._glance_client - - @property - def heat_client(self): - warnings.warn( - 'Using shade to get a heat_client object is deprecated. If you' - ' need a raw heatclient.client.Client object, please use' - ' make_legacy_client in os-client-config instead') - try: - import heatclient # flake8: noqa - except ImportError: - self.log.error( - 'heatclient is no longer a dependency of shade. You need to' - ' install python-heatclient directly.') - raise - if self._heat_client is None: - self._heat_client = self._get_client('orchestration') - return self._heat_client - def get_template_contents( self, template_file=None, template_url=None, template_object=None, files=None): @@ -1135,121 +1222,6 @@ def get_template_contents( raise OpenStackCloudException( "Error in processing template files: %s" % str(e)) - @property - def swift_client(self): - warnings.warn( - 'Using shade to get a swift object is deprecated. If you' - ' need a raw swiftclient.Connection object, please use' - ' make_legacy_client in os-client-config instead') - try: - import swiftclient.client # flake8: noqa - except ImportError: - self.log.error( - 'swiftclient is no longer a dependency of shade. You need to' - ' install python-swiftclient directly.') - return self._get_client('object-store') - - def _get_swift_kwargs(self): - auth_version = self.cloud_config.get_api_version('identity') - auth_args = self.cloud_config.config.get('auth', {}) - os_options = {'auth_version': auth_version} - if auth_version == '2.0': - os_options['os_tenant_name'] = auth_args.get('project_name') - os_options['os_tenant_id'] = auth_args.get('project_id') - else: - os_options['os_project_name'] = auth_args.get('project_name') - os_options['os_project_id'] = auth_args.get('project_id') - - for key in ( - 'username', - 'password', - 'auth_url', - 'user_id', - 'project_domain_id', - 'project_domain_name', - 'user_domain_id', - 'user_domain_name'): - os_options['os_{key}'.format(key=key)] = auth_args.get(key) - return os_options - - @property - def swift_service(self): - warnings.warn( - 'Using shade to get a swift object is deprecated. If you' - ' need a raw swiftclient.service.SwiftService object, please use' - ' make_legacy_client in os-client-config instead') - try: - import swiftclient.service - except ImportError: - self.log.error( - 'swiftclient is no longer a dependency of shade. You need to' - ' install python-swiftclient directly.') - with _utils.shade_exceptions("Error constructing " - "swift client"): - endpoint = self.get_session_endpoint( - service_key='object-store') - options = dict(os_auth_token=self.auth_token, - os_storage_url=endpoint, - os_region_name=self.region_name) - options.update(self._get_swift_kwargs()) - return swiftclient.service.SwiftService(options=options) - - @property - def cinder_client(self): - - # Import cinderclient late because importing it at the top level - # breaks logging for users of shade - import cinderclient.client # flake8: noqa - if self._cinder_client is None: - self._cinder_client = self._get_client('volume') - return self._cinder_client - - @property - def trove_client(self): - warnings.warn( - 'Using shade to get a trove_client object is deprecated. If you' - ' need a raw troveclient.client.Client object, please use' - ' make_legacy_client in os-client-config instead') - if self._trove_client is None: - self._trove_client = self._get_client('database') - return self._trove_client - - @property - def magnum_client(self): - warnings.warn( - 'Using shade to get a magnum object is deprecated. If you' - ' need a raw magnumclient.client.Client object, please use' - ' make_legacy_client in os-client-config instead') - if self._magnum_client is None: - self._magnum_client = self._get_client('container-infra') - return self._magnum_client - - @property - def neutron_client(self): - warnings.warn( - 'Using shade to get a neutron_client object is deprecated. If you' - ' need a raw neutronclient.Client object, please use' - ' make_legacy_client in os-client-config instead') - try: - import neutronclient # flake8: noqa - except ImportError: - self.log.error( - 'neutronclient is no longer a dependency of shade. You need to' - ' install python-neutronclient directly.') - raise - if self._neutron_client is None: - self._neutron_client = self._get_client('network') - return self._neutron_client - - @property - def designate_client(self): - # Note: Explicit constructor is needed until occ 1.27.0 - import designateclient.client # flake8: noqa - if self._designate_client is None: - self._designate_client = self._get_client( - 'dns', designateclient.client.Client) - return self._designate_client - def create_stack( self, name, template_file=None, template_url=None, From 3f76b25798984cb3a4dabaf91ca5338782254465 Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Wed, 10 May 2017 14:48:05 +0000 Subject: [PATCH 1521/3836] Remove cinder client Change-Id: I585da9a60757da8020b8e1086ae8d8375a9ce364 Signed-off-by: Rosario Di Somma --- requirements.txt | 1 - shade/openstackcloud.py | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 08c0af534..6bc05760b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,7 +24,6 @@ keystoneauth1>=2.20.0 # Apache-2.0 netifaces>=0.10.4 # MIT python-novaclient>=7.1.0 # Apache-2.0 python-keystoneclient>=3.8.0 # Apache-2.0 -python-cinderclient>=2.0.1 # Apache-2.0 python-ironicclient>=1.11.0 # Apache-2.0 python-designateclient>=1.5.0 # Apache-2.0 diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index d6ba69f7a..0daacf313 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -31,7 +31,6 @@ import requestsexceptions from six.moves import urllib -import cinderclient.exceptions as cinder_exceptions import keystoneauth1.exceptions import novaclient.exceptions as nova_exceptions @@ -652,11 +651,8 @@ def swift_service(self): @property def cinder_client(self): - - # Import cinderclient late because importing it at the top level - # breaks logging for users of shade - import cinderclient.client # flake8: noqa if self._cinder_client is None: + _deprecated_import_check('cinderclient') self._cinder_client = self._get_client('volume') return self._cinder_client From 103bc13862639e6bfe942843c095230f3d6ddf89 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 12 May 2017 11:52:41 -0500 Subject: [PATCH 1522/3836] Fix pep8 errors that were lurking Something about how we were doing legacy client constructors was causing pep8 to fail open. The next patch fixes that, but these are the pep8 errors that snuck in while it wasn't working. Change-Id: I080fd1730fcbc4cd30860963a89dad6ccf3d4f2f --- shade/openstackcloud.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 0daacf313..251c385eb 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1765,7 +1765,6 @@ def list_flavors(self, get_extra=None): '/flavors/detail', params=dict(is_public='None'), error_message="Error fetching flavor list")) - error_message = "Error fetching flavor extra specs" for flavor in flavors: if not flavor.extra_specs and get_extra: endpoint = "/flavors/{id}/os-extra_specs".format( @@ -1885,22 +1884,24 @@ def remove_server_security_groups(self, server, security_groups): if not (server and security_groups): return False + ret = True + for sg in security_groups: try: self._compute_client.post( '/servers/%s/action' % server['id'], json={'removeSecurityGroup': {'name': sg.name}}) - except OpenStackCloudURINotFound as e: + except OpenStackCloudURINotFound: # NOTE(jamielennox): Is this ok? If we remove something that # isn't present should we just conclude job done or is that an # error? Nova returns ok if you try to add a group twice. self.log.debug( "The security group %s was not present on server %s so " "no action was performed", sg.name, server.name) + ret = False - return True - + return ret def list_security_groups(self, filters=None): """List all available security groups. @@ -3983,10 +3984,10 @@ def detach_volume(self, server, volume, wait=True, timeout=None): self._compute_client.delete( '/servers/{server_id}/os-volume_attachments/{volume_id}'.format( - server_id=server['id'], volume_id=volume['id']), - error_message="Error detaching volume {volume} " - "from server {server}".format( - volume=volume['id'], server=server['id'])) + server_id=server['id'], volume_id=volume['id']), + error_message=( + "Error detaching volume {volume} from server {server}".format( + volume=volume['id'], server=server['id']))) if wait: for count in _utils._iterate_timeout( @@ -6645,10 +6646,9 @@ def create_port(self, network_id, **kwargs): error_message="Error creating port for network {0}".format( network_id)) - @_utils.valid_kwargs('name', 'admin_state_up', 'fixed_ips', 'security_groups', 'allowed_address_pairs', - 'extra_dhcp_opts', 'device_owner','device_id') + 'extra_dhcp_opts', 'device_owner', 'device_id') def update_port(self, name_or_id, **kwargs): """Update a port @@ -6733,7 +6733,7 @@ def create_security_group(self, name, description, project_id=None): :param string name: A name for the security group. :param string description: Describes the security group. - :param string project_id: + :param string project_id: Specify the project ID this security group will be created on (admin-only). @@ -6751,7 +6751,10 @@ def create_security_group(self, name, description, project_id=None): ) group = None - security_group_json = {'security_group': {'name': name, 'description': description}} + security_group_json = { + 'security_group': { + 'name': name, 'description': description + }} if project_id is not None: security_group_json['security_group']['tenant_id'] = project_id if self._use_neutron_secgroups(): @@ -6959,7 +6962,7 @@ def create_security_group_rule(self, port_range_min = 1 port_range_max = 65535 - security_group_rule_dict = dict(security_group_rule = dict( + security_group_rule_dict = dict(security_group_rule=dict( parent_group_id=secgroup['id'], ip_protocol=protocol, from_port=port_range_min, @@ -6968,7 +6971,8 @@ def create_security_group_rule(self, group_id=remote_group_id )) if project_id is not None: - security_group_rule_dict['security_group_rule']['tenant_id'] = project_id + security_group_rule_dict[ + 'security_group_rule']['tenant_id'] = project_id rule = self._compute_client.post( '/os-security-group-rules', json=security_group_rule_dict ) From 7a3acde3ed8a68acc15596be2d297426ee357084 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 12 May 2017 12:05:50 -0500 Subject: [PATCH 1523/3836] Add ironicclient to constructors list Change-Id: I29db3c830759a80d8ea9f0d93a213b4bae4c8b59 --- os_client_config/constructors.json | 1 + 1 file changed, 1 insertion(+) diff --git a/os_client_config/constructors.json b/os_client_config/constructors.json index 2819bf380..9acb7cfb9 100644 --- a/os_client_config/constructors.json +++ b/os_client_config/constructors.json @@ -1,5 +1,6 @@ { "application-catalog": "muranoclient.client.Client", + "baremetal": "ironicclient.client.Client", "compute": "novaclient.client.Client", "container-infra": "magnumclient.client.Client", "database": "troveclient.client.Client", From 1c0a95ba8f9fda57e0a094c86393bae546f89f23 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 12 May 2017 10:58:46 -0500 Subject: [PATCH 1524/3836] Move legacy client constructors to mixin Remove them from the main class to make them slightly less obvious. Change-Id: I72b32d992c71777155c22b51ce4c7868e19d44c1 --- shade/_legacy_clients.py | 197 +++++++++++++++++++++++++++++++++++++++ shade/openstackcloud.py | 147 +---------------------------- shade/operatorcloud.py | 25 ----- 3 files changed, 202 insertions(+), 167 deletions(-) create mode 100644 shade/_legacy_clients.py diff --git a/shade/_legacy_clients.py b/shade/_legacy_clients.py new file mode 100644 index 000000000..3790d593b --- /dev/null +++ b/shade/_legacy_clients.py @@ -0,0 +1,197 @@ +# 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 importlib +import warnings + +from os_client_config import constructors + +from shade import _utils + + +class LegacyClientFactoryMixin(object): + """Mixin Class containing factory functions for legacy client objects. + + Methods in this class exist for backwards compatibility so will not go + away any time - but they are all things whose use is discouraged. They're + in a mixin to unclutter the main class file. + """ + + def _create_legacy_client( + self, client, service, deprecated=True, + module_name=None, **kwargs): + if client not in self._legacy_clients: + if deprecated: + self._deprecated_import_check(client) + if module_name: + constructors.get_constructor_mapping()[service] = module_name + self._legacy_clients[client] = self._get_client(service, **kwargs) + return self._legacy_clients[client] + + def _deprecated_import_check(self, client): + module_name = '{client}client' + warnings.warn( + 'Using shade to get a {module_name} object is deprecated. If you' + ' need a {module_name} object, please use make_legacy_client in' + ' os-client-config instead'.format(module_name=module_name)) + try: + importlib.import_module(module_name) + except ImportError: + self.log.error( + '{module_name} is no longer a dependency of shade. You need to' + ' install python-{module_name} directly.'.format( + module_name=module_name)) + raise + + @property + def trove_client(self): + return self._create_legacy_client('trove', 'database') + + @property + def magnum_client(self): + return self._create_legacy_client('magnum', 'container-infra') + + @property + def neutron_client(self): + return self._create_or_return_legacy_client('neutron', 'network') + + @property + def nova_client(self): + return self._create_legacy_client( + 'nova', 'compute', version='2.0', deprecated=False) + + @property + def glance_client(self): + return self._create_legacy_client('glance', 'image') + + @property + def heat_client(self): + return self._create_legacy_client('heat', 'orchestration') + + @property + def swift_client(self): + return self._create_legacy_client('swift', 'object-store') + + @property + def cinder_client(self): + return self._create_legacy_client('cinder', 'volume') + + @property + def designate_client(self): + return self._create_legacy_client('designate', 'dns', deprecated=False) + + @property + def keystone_client(self): + return self._create_legacy_client( + 'keystone', 'identity', deprecated=False) + + # Set the ironic API microversion to a known-good + # supported/tested with the contents of shade. + # + # NOTE(TheJulia): Defaulted to version 1.6 as the ironic + # state machine changes which will increment the version + # and break an automatic transition of an enrolled node + # to an available state. Locking the version is intended + # to utilize the original transition until shade supports + # calling for node inspection to allow the transition to + # take place automatically. + # NOTE(mordred): shade will handle microversions more + # directly in the REST layer. This microversion property + # will never change. When we implement REST, we should + # start at 1.6 since that's what we've been requesting + # via ironic_client + @property + def ironic_api_microversion(self): + # NOTE(mordred) Abuse _legacy_clients to only show + # this warning once + if 'ironic-microversion' not in self._legacy_clients: + warnings.warn( + 'shade is transitioning to direct REST calls which' + ' will handle microversions with no action needed' + ' on the part of the user. The ironic_api_microversion' + ' property is only used by the legacy ironic_client' + ' constructor and will never change. If you are using' + ' it for any reason, either switch to just using' + ' shade ironic-related API calls, or use os-client-config' + ' make_legacy_client directly and pass os_ironic_api_version' + ' to it as an argument. It is highly recommended to' + ' stop using this property.') + self._legacy_clients['ironic-microversion'] = True + return self._get_legacy_ironic_microversion() + + def _get_legacy_ironic_microversion(self): + return '1.6' + + @property + def ironic_client(self): + return self._create_legacy_client( + 'ironic', 'baremetal', deprecated=False, + module_name='ironicclient.client.Client', + os_ironic_api_version=self._get_legacy_ironic_microversion()) + + def _get_swift_kwargs(self): + auth_version = self.cloud_config.get_api_version('identity') + auth_args = self.cloud_config.config.get('auth', {}) + os_options = {'auth_version': auth_version} + if auth_version == '2.0': + os_options['os_tenant_name'] = auth_args.get('project_name') + os_options['os_tenant_id'] = auth_args.get('project_id') + else: + os_options['os_project_name'] = auth_args.get('project_name') + os_options['os_project_id'] = auth_args.get('project_id') + + for key in ( + 'username', + 'password', + 'auth_url', + 'user_id', + 'project_domain_id', + 'project_domain_name', + 'user_domain_id', + 'user_domain_name'): + os_options['os_{key}'.format(key=key)] = auth_args.get(key) + return os_options + + @property + def swift_service(self): + suppress_warning = 'swift-service' not in self._legacy_clients + return self.make_swift_service_object(suppress_warning) + + def make_swift_service(self, suppress_warning=False): + # NOTE(mordred): Not using helper functions because the + # error message needs to be different + if not suppress_warning: + warnings.warn( + 'Using shade to get a SwiftService object is deprecated. shade' + ' will automatically do the things SwiftServices does as part' + ' of the normal object resource calls. If you are having' + ' trouble using those such that you still need to use' + ' SwiftService, please file a bug with shade.' + ' If you understand the issues and want to make this warning' + ' go away, use cloud.make_swift_service(True) instead of' + ' cloud.swift_service') + # Abuse self._legacy_clients so that we only give the warning + # once. We don't cache SwiftService objects. + self._legacy_clients['swift-service'] = True + try: + import swiftclient.service + except ImportError: + self.log.error( + 'swiftclient is no longer a dependency of shade. You need to' + ' install python-swiftclient directly.') + with _utils.shade_exceptions("Error constructing SwiftService"): + endpoint = self.get_session_endpoint( + service_key='object-store') + options = dict(os_auth_token=self.auth_token, + os_storage_url=endpoint, + os_region_name=self.region_name) + options.update(self._get_swift_kwargs()) + return swiftclient.service.SwiftService(options=options) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 251c385eb..cb50a42b7 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -13,7 +13,6 @@ import collections import functools import hashlib -import importlib import ipaddress import json import jsonpatch @@ -40,6 +39,7 @@ from shade._heat import event_utils from shade._heat import template_utils from shade import _log +from shade import _legacy_clients from shade import _normalize from shade import meta from shade import task_manager @@ -92,7 +92,9 @@ def _no_pending_stacks(stacks): return True -class OpenStackCloud(_normalize.Normalizer): +class OpenStackCloud( + _normalize.Normalizer, + _legacy_clients.LegacyClientFactoryMixin): """Represent a connection to an OpenStack Cloud. OpenStackCloud is the entry point for all cloud operations, regardless @@ -296,18 +298,7 @@ def invalidate(self): self._keystone_session = None - self._cinder_client = None - self._glance_client = None - self._glance_endpoint = None - self._heat_client = None - self._keystone_client = None - self._neutron_client = None - self._nova_client = None - self._trove_client = None - self._designate_client = None - self._magnum_client = None - self._swift_client = None - + self._legacy_clients = {} self._raw_clients = {} self._local_ipv6 = _utils.localhost_supports_ipv6() @@ -543,134 +534,6 @@ def pformat(self, resource): new_resource = _utils._dictify_resource(resource) return pprint.pformat(new_resource) - # LEGACY CLIENT IMPORTS - def _deprecated_import_check(self, module_name): - warnings.warn( - 'Using shade to get a {module_name} object is deprecated. If you' - ' need a {module_name} object, please use make_legacy_client in' - ' os-client-config instead'.format(module_name=module_name)) - try: - importlib.import_module(module_name) - except ImportError: - self.log.error( - '{module_name} is no longer a dependency of shade. You need to' - ' install python-{module_name} directly.'.format( - module_name=module_name)) - raise - - @property - def trove_client(self): - if self._trove_client is None: - _deprecated_import_check('troveclient') - self._trove_client = self._get_client('database') - return self._trove_client - - @property - def magnum_client(self): - if self._magnum_client is None: - _deprecated_import_check('magnumclient') - self._magnum_client = self._get_client('container-infra') - return self._magnum_client - - @property - def neutron_client(self): - if self._neutron_client is None: - _deprecated_import_check('neutronclient') - self._neutron_client = self._get_client('network') - return self._neutron_client - - @property - def nova_client(self): - if self._nova_client is None: - self._nova_client = self._get_client('compute', version='2.0') - return self._nova_client - - @property - def glance_client(self): - if self._glance_client is None: - _deprecated_import_check('glanceclient') - self._glance_client = self._get_client('image') - return self._glance_client - - @property - def heat_client(self): - if self._heat_client is None: - _deprecated_import_check('heatclient') - self._heat_client = self._get_client('orchestration') - return self._heat_client - - @property - def swift_client(self): - if self._swift_client is None: - _deprecated_import_check('swiftclient') - self._swift_client = self._get_client('object-store') - return self._swift_client - - def _get_swift_kwargs(self): - auth_version = self.cloud_config.get_api_version('identity') - auth_args = self.cloud_config.config.get('auth', {}) - os_options = {'auth_version': auth_version} - if auth_version == '2.0': - os_options['os_tenant_name'] = auth_args.get('project_name') - os_options['os_tenant_id'] = auth_args.get('project_id') - else: - os_options['os_project_name'] = auth_args.get('project_name') - os_options['os_project_id'] = auth_args.get('project_id') - - for key in ( - 'username', - 'password', - 'auth_url', - 'user_id', - 'project_domain_id', - 'project_domain_name', - 'user_domain_id', - 'user_domain_name'): - os_options['os_{key}'.format(key=key)] = auth_args.get(key) - return os_options - - @property - def swift_service(self): - # NOTE(mordred): Not using deprecated_client_check because the - # error message needs to be different - try: - import swiftclient.service - except ImportError: - self.log.error( - 'swiftclient is no longer a dependency of shade. You need to' - ' install python-swiftclient directly.') - with _utils.shade_exceptions("Error constructing " - "swift client"): - endpoint = self.get_session_endpoint( - service_key='object-store') - options = dict(os_auth_token=self.auth_token, - os_storage_url=endpoint, - os_region_name=self.region_name) - options.update(self._get_swift_kwargs()) - return swiftclient.service.SwiftService(options=options) - - @property - def cinder_client(self): - if self._cinder_client is None: - _deprecated_import_check('cinderclient') - self._cinder_client = self._get_client('volume') - return self._cinder_client - - @property - def designate_client(self): - # Note: Explicit constructor is needed until occ 1.27.0 - import designateclient.client # flake8: noqa - if self._designate_client is None: - self._designate_client = self._get_client( - 'dns', designateclient.client.Client) - return self._designate_client - - @property - def keystone_client(self): - if self._keystone_client is None: - self._keystone_client = self._get_client('identity') - return self._keystone_client - @property def keystone_session(self): if self._keystone_session is None: diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index dd365d79e..d994081c5 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -13,7 +13,6 @@ import datetime import jsonpatch -from ironicclient import client as ironic_client from ironicclient import exceptions as ironic_exceptions from novaclient import exceptions as nova_exceptions @@ -32,30 +31,6 @@ class OperatorCloud(openstackcloud.OpenStackCloud): See the :class:`OpenStackCloud` class for a description of most options. """ - def __init__(self, *args, **kwargs): - super(OperatorCloud, self).__init__(*args, **kwargs) - self._ironic_client = None - - # Set the ironic API microversion to a known-good - # supported/tested with the contents of shade. - # - # Note(TheJulia): Defaulted to version 1.6 as the ironic - # state machine changes which will increment the version - # and break an automatic transition of an enrolled node - # to an available state. Locking the version is intended - # to utilize the original transition until shade supports - # calling for node inspection to allow the transition to - # take place automatically. - ironic_api_microversion = '1.6' - - @property - def ironic_client(self): - if self._ironic_client is None: - self._ironic_client = self._get_client( - 'baremetal', ironic_client.Client, - os_ironic_api_version=self.ironic_api_microversion) - return self._ironic_client - def list_nics(self): with _utils.shade_exceptions("Error fetching machine port list"): return self.manager.submit_task(_tasks.MachinePortList()) From 4493871824839782846c3825c754895876f8d08e Mon Sep 17 00:00:00 2001 From: Jordan Pittier Date: Wed, 3 May 2017 13:11:10 +0000 Subject: [PATCH 1525/3836] Revert "Use interface not endpoint_type for keystoneclient" This reverts commit 38e5eba621e48d74c05315da2b89e6c801f4c43f. This patch introduced a bug when using Keystone v2. With this patch, the following works: python -c "import os_client_config; print(os_client_config.make_client('identity', auth_url='http://localhost/identity_admin', username='admin', project_name='admin', password='testtest', identity_api_version='3').roles.list())" But changing identity_api_version from 3 to 2.0 raises an exception. Without this patch, both 3 and 2.0 works. Change-Id: I8d2ad71ff51a08af1166d36805b740ea272939ed --- os_client_config/cloud_config.py | 2 +- os_client_config/tests/test_cloud_config.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index fec350094..776ea8a17 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -367,7 +367,7 @@ def get_legacy_client( endpoint_override = self.get_endpoint(service_key) if not interface_key: - if service_key in ('image', 'key-manager', 'identity'): + if service_key in ('image', 'key-manager'): interface_key = 'interface' else: interface_key = 'endpoint_type' diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index 5df1fafa2..3ec7ecf88 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -550,7 +550,7 @@ def test_legacy_client_identity(self, mock_get_session_endpoint): mock_client.assert_called_with( version='2.0', endpoint='http://example.com/v2', - interface='admin', + endpoint_type='admin', endpoint_override=None, region_name='region-al', service_type='identity', @@ -570,7 +570,7 @@ def test_legacy_client_identity_v3(self, mock_get_session_endpoint): mock_client.assert_called_with( version='3', endpoint='http://example.com', - interface='admin', + endpoint_type='admin', endpoint_override=None, region_name='region-al', service_type='identity', From a483534123640eca2bcbf7c06920be3a2a01b090 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 13 May 2017 08:45:26 -0500 Subject: [PATCH 1526/3836] Revert "Revert "Use interface not endpoint_type for keystoneclient"" Unrevert the endpoint_type/interface patch. But this time around, put in a check for API version 2.0 and only apply the interface arg if it's for v3. This reverts commit 4493871824839782846c3825c754895876f8d08e. Change-Id: Ib347ec686d4d01788ee943c4c4f809aad06d9ccf --- os_client_config/cloud_config.py | 23 ++++++++++++--------- os_client_config/tests/test_cloud_config.py | 2 +- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 776ea8a17..89e070f39 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -366,12 +366,6 @@ def get_legacy_client( service_key, min_version=min_version, max_version=max_version) endpoint_override = self.get_endpoint(service_key) - if not interface_key: - if service_key in ('image', 'key-manager'): - interface_key = 'interface' - else: - interface_key = 'endpoint_type' - if service_key == 'object-store': constructor_kwargs = dict( session=self.get_session(), @@ -404,10 +398,6 @@ def get_legacy_client( if not endpoint_override: constructor_kwargs['endpoint_override'] = endpoint constructor_kwargs.update(kwargs) - if service_key == 'object-store': - constructor_kwargs['os_options'][interface_key] = interface - else: - constructor_kwargs[interface_key] = interface if pass_version_arg and service_key != 'object-store': if not version: version = self.get_api_version(service_key) @@ -451,6 +441,19 @@ def get_legacy_client( constructor_kwargs['username'] = None constructor_kwargs['password'] = None + if not interface_key: + if service_key in ('image', 'key-manager'): + interface_key = 'interface' + elif (service_key == 'identity' + and version and version.startswith('3')): + interface_key = 'interface' + else: + interface_key = 'endpoint_type' + if service_key == 'object-store': + constructor_kwargs['os_options'][interface_key] = interface + else: + constructor_kwargs[interface_key] = interface + return client_class(**constructor_kwargs) def get_cache_expiration_time(self): diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index 3ec7ecf88..813552639 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -570,7 +570,7 @@ def test_legacy_client_identity_v3(self, mock_get_session_endpoint): mock_client.assert_called_with( version='3', endpoint='http://example.com', - endpoint_type='admin', + interface='admin', endpoint_override=None, region_name='region-al', service_type='identity', From be0a0c99f3ebcb80e7a71226557bd2a96f75af73 Mon Sep 17 00:00:00 2001 From: Paul Belanger Date: Sat, 13 May 2017 15:10:17 -0400 Subject: [PATCH 1527/3836] Fix exception when using boot_from_volume for create_server It is possible for create_server to accept a dict for image, however _get_boot_from_volume_kwargs did expect that. This created the following exception: Traceback (most recent call last): File "/opt/stack/new/nodepool/nodepool/nodepool.py", line 358, in _run self._launchNode() File "/opt/stack/new/nodepool/nodepool/nodepool.py", line 290, in _launchNode volume_size=self._label.volume_size) File "/opt/stack/new/nodepool/nodepool/provider_manager.py", line 235, in createServer return self._client.create_server(wait=False, **create_args) File "", line 2, in create_server File "/opt/stack/new/nodepool-venv/local/lib/python2.7/site-packages/shade/_utils.py", line 393, in func_wrapper return func(*args, **kwargs) File "/opt/stack/new/nodepool-venv/local/lib/python2.7/site-packages/shade/openstackcloud.py", line 5517, in create_server volumes=volumes, kwargs=kwargs) File "/opt/stack/new/nodepool-venv/local/lib/python2.7/site-packages/shade/openstackcloud.py", line 5333, in _get_boot_from_volume_kwargs cloud=self.name, region=self.region_name)) OpenStackCloudException: Image {'id': u'1cb47019-08a4-4b9c-ae62-737b547648b6'} is not a valid image in devstack:RegionOne Now, check if image is a dictonary, like we did in create_server. Also add an ansible test for os_server so we additional code coverage. Change-Id: If58cd96b0b9ce4569120d60fbceb2c23b2f7641d Signed-off-by: Paul Belanger --- shade/openstackcloud.py | 7 +++++- .../tests/ansible/roles/server/tasks/main.yml | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 11af3ee0e..d0a157a79 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5311,6 +5311,11 @@ def _needs_floating_ip(self, server, nat_destination): def _get_boot_from_volume_kwargs( self, image, boot_from_volume, boot_volume, volume_size, terminate_volume, volumes, kwargs): + """Return block device mappings + + :param image: Image dict, name or id to boot with. + + """ if boot_volume or boot_from_volume or volumes: kwargs.setdefault('block_device_mapping_v2', []) else: @@ -5337,7 +5342,7 @@ def _get_boot_from_volume_kwargs( kwargs['image'] = None elif boot_from_volume: - if hasattr(image, 'id'): + if isinstance(image, dict): image_obj = image else: image_obj = self.get_image(image) diff --git a/shade/tests/ansible/roles/server/tasks/main.yml b/shade/tests/ansible/roles/server/tasks/main.yml index 64a2c111f..f25bc2ef6 100644 --- a/shade/tests/ansible/roles/server/tasks/main.yml +++ b/shade/tests/ansible/roles/server/tasks/main.yml @@ -66,3 +66,27 @@ state: absent name: "{{ server_name }}" wait: true + +- name: Create server from volume + os_server: + cloud: "{{ cloud }}" + state: present + name: "{{ server_name }}" + image: "{{ image }}" + flavor: "{{ flavor }}" + network: "{{ server_network }}" + auto_floating_ip: false + boot_from_volume: true + volume_size: 5 + terminate_volume: true + wait: true + register: server + +- debug: var=server + +- name: Delete server with volume + os_server: + cloud: "{{ cloud }}" + state: absent + name: "{{ server_name }}" + wait: true From f94880c6d835cda4cd33e5b55f7d81d3608b8f52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Sun, 14 May 2017 09:34:09 +0000 Subject: [PATCH 1528/3836] Move mocks of designate API discovery calls to base test class As API discovery call has to be mocked in each Designate's test case it's better to move it to base test class and reuse later. This commit moves it to tests.unit.base.RequestsMockTestCase that it can be added to mocks in setUp() method before each test. Change-Id: I3f00c2d51619d634e4e44f3ebf276949544c8575 --- shade/tests/unit/base.py | 15 +++++++++ shade/tests/unit/fixtures/dns.json | 22 +++++++++++++ shade/tests/unit/test_zone.py | 53 +++++------------------------- 3 files changed, 45 insertions(+), 45 deletions(-) create mode 100644 shade/tests/unit/fixtures/dns.json diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index fa7ddf21f..4bae60e78 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -476,6 +476,12 @@ def get_glance_discovery_mock_dict( return dict(method='GET', uri='https://image.example.com/', text=open(discovery_fixture, 'r').read()) + def get_designate_discovery_mock_dict(self): + discovery_fixture = os.path.join( + self.fixtures_directory, "dns.json") + return dict(method='GET', uri="https://dns.example.com/", + text=open(discovery_fixture, 'r').read()) + def use_glance(self, image_version_json='image-version.json'): # NOTE(notmorgan): This method is only meant to be used in "setUp" # where the ordering of the url being registered is tightly controlled @@ -485,6 +491,15 @@ def use_glance(self, image_version_json='image-version.json'): self.__do_register_uris([ self.get_glance_discovery_mock_dict(image_version_json)]) + def use_designate(self): + # NOTE(slaweq): This method is only meant to be used in "setUp" + # where the ordering of the url being registered is tightly controlled + # if the functionality of .use_designate is meant to be used during an + # actual test case, use .get_designate_discovery_mock and apply to the + # right location in the mock_uris when calling .register_uris + self.__do_register_uris([ + self.get_designate_discovery_mock_dict()]) + def register_uris(self, uri_mock_list=None): """Mock a list of URIs and responses via requests mock. diff --git a/shade/tests/unit/fixtures/dns.json b/shade/tests/unit/fixtures/dns.json new file mode 100644 index 000000000..d3bbfc5c0 --- /dev/null +++ b/shade/tests/unit/fixtures/dns.json @@ -0,0 +1,22 @@ +{ + "values": [{ + "id": "v1", + "links": [ + { + "href": "https://dns.example.com/v1", + "rel": "self" + } + ], + "status": "DEPRECATED" + }, { + "id": "v2", + "links": [ + { + "href": "https://dns.example.com/v2", + "rel": "self" + } + ], + "status": "CURRENT" + }] +} + diff --git a/shade/tests/unit/test_zone.py b/shade/tests/unit/test_zone.py index 1fb3d0c91..344bb0aae 100644 --- a/shade/tests/unit/test_zone.py +++ b/shade/tests/unit/test_zone.py @@ -17,28 +17,6 @@ from shade.tests.unit import base -api_versions = { - "values": [{ - "id": "v1", - "links": [ - { - "href": "https://dns.example.com/v1", - "rel": "self" - } - ], - "status": "DEPRECATED" - }, { - "id": "v2", - "links": [ - { - "href": "https://dns.example.com/v2", - "rel": "self" - } - ], - "status": "CURRENT" - }] -} - zone_dict = { 'name': 'example.net.', 'type': 'PRIMARY', @@ -53,10 +31,12 @@ class TestZone(base.RequestsMockTestCase): + def setUp(self): + super(TestZone, self).setUp() + self.use_designate() + def test_create_zone(self): self.register_uris([ - dict(method='GET', - uri="https://dns.example.com/", json=api_versions), dict(method='POST', uri=self.get_mock_url( 'dns', 'public', append=['zones']), @@ -76,8 +56,6 @@ def test_create_zone(self): def test_create_zone_exception(self): self.register_uris([ - dict(method='GET', - uri="https://dns.example.com/", json=api_versions), dict(method='POST', uri=self.get_mock_url( 'dns', 'public', append=['zones']), @@ -95,22 +73,17 @@ def test_update_zone(self): updated_zone = copy.copy(new_zone_dict) updated_zone['ttl'] = new_ttl self.register_uris([ - dict(method='GET', - uri="https://dns.example.com/", json=api_versions), dict(method='GET', uri=self.get_mock_url( 'dns', 'public', append=['zones']), json={"zones": [new_zone_dict]}), - dict(method='GET', - uri="https://dns.example.com/", json=api_versions), + self.get_designate_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'dns', 'public', append=['zones'], qs_elements=['name=1']), json={'zones': [new_zone_dict]}), - dict(method='GET', - uri="https://dns.example.com/", - json=api_versions), + self.get_designate_discovery_mock_dict(), dict(method='PATCH', uri=self.get_mock_url( 'dns', 'public', append=['zones', '1']), @@ -124,21 +97,17 @@ def test_update_zone(self): def test_delete_zone(self): self.register_uris([ - dict(method='GET', - uri="https://dns.example.com/", json=api_versions), dict(method='GET', uri=self.get_mock_url( 'dns', 'public', append=['zones']), json={"zones": [new_zone_dict]}), - dict(method='GET', - uri="https://dns.example.com/", json=api_versions), + self.get_designate_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'dns', 'public', append=['zones'], qs_elements=['name=1']), json={'zones': [new_zone_dict]}), - dict(method='GET', - uri="https://dns.example.com/", json=api_versions), + self.get_designate_discovery_mock_dict(), dict(method='DELETE', uri=self.get_mock_url( 'dns', 'public', append=['zones', '1']), @@ -149,8 +118,6 @@ def test_delete_zone(self): def test_get_zone_by_id(self): self.register_uris([ - dict(method='GET', - uri="https://dns.example.com/", json=api_versions), dict(method='GET', uri=self.get_mock_url( 'dns', 'public', append=['zones']), @@ -162,8 +129,6 @@ def test_get_zone_by_id(self): def test_get_zone_by_name(self): self.register_uris([ - dict(method='GET', - uri="https://dns.example.com/", json=api_versions), dict(method='GET', uri=self.get_mock_url( 'dns', 'public', append=['zones']), @@ -175,8 +140,6 @@ def test_get_zone_by_name(self): def test_get_zone_not_found_returns_false(self): self.register_uris([ - dict(method='GET', - uri="https://dns.example.com/", json=api_versions), dict(method='GET', uri=self.get_mock_url( 'dns', 'public', append=['zones']), From dc77e7d6c266d0278c13cdcaf5d46b2d07faeb96 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 11 May 2017 21:11:31 -0400 Subject: [PATCH 1529/3836] Convert first ironic_client test to REST Add the various plumbing and convert one test. Also, ironicclient prepends "v1" to all of its calls (joy) so there is an ugly transition related to version discovery coming with this one. For now just prepend it in our mock base class. When we convert calls, we'll have to prepend v1 at the call site until we're done with the transition. Then we can do a followup patch that'll do discovery and remove the v1 from the code. Gross. Change-Id: Icfd70267910d19a7da2ae8e9b9e4f24d2077b87d --- shade/openstackcloud.py | 8 ++++- shade/tests/fakes.py | 9 ++++++ shade/tests/unit/fixtures/catalog-v3.json | 13 ++++++++ shade/tests/unit/test_shade_operator.py | 38 +++++++++++++++++------ 4 files changed, 58 insertions(+), 10 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 8a5ca5457..e9001fa78 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -384,7 +384,13 @@ def _application_catalog_client(self): @property def _baremetal_client(self): if 'baremetal' not in self._raw_clients: - self._raw_clients['baremetal'] = self._get_raw_client('baremetal') + client = self._get_raw_client('baremetal') + # TODO(mordred) Fix this once we've migrated all the way to REST + # Don't bother with version discovery - there is only one version + # of ironic. This is what ironicclient does, fwiw. + client.endpoint_override = urllib.parse.urljoin( + client.get_endpoint(), 'v1') + self._raw_clients['baremetal'] = client return self._raw_clients['baremetal'] @property diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 4663330d4..c037e2f7f 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -20,6 +20,7 @@ import uuid from shade._heat import template_format +from shade import meta PROJECT_ID = '1c36b64c840a42cd9e9b931a369337f0' FLAVOR_ID = u'0c1d9008-f546-4608-9e8f-f8bdaec8dddd' @@ -210,6 +211,14 @@ def make_fake_image( u'protected': False} +def make_fake_machine(machine_name, machine_id=None): + if not machine_id: + machine_id = uuid.uuid4().hex + return meta.obj_to_dict(FakeMachine( + id=machine_id, + name=machine_name)) + + class FakeFloatingIP(object): def __init__(self, id, pool, ip, fixed_ip, instance_id): self.id = id diff --git a/shade/tests/unit/fixtures/catalog-v3.json b/shade/tests/unit/fixtures/catalog-v3.json index 0c372603e..c42ffe998 100644 --- a/shade/tests/unit/fixtures/catalog-v3.json +++ b/shade/tests/unit/fixtures/catalog-v3.json @@ -110,6 +110,19 @@ "name": "swift", "type": "object-store" }, + { + "endpoints": [ + { + "id": "4deb4d0504a044a395d4480741ba628c", + "interface": "public", + "region": "RegionOne", + "url": "https://bare-metal.example.com/" + } + ], + "endpoints_links": [], + "name": "ironic", + "type": "baremetal" + }, { "endpoints": [ { diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index 1c64b43f2..dd4b539a0 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -16,6 +16,7 @@ import mock import munch import testtools +import uuid import os_client_config as occ from os_client_config import cloud_config @@ -26,19 +27,38 @@ from shade.tests.unit import base -class TestShadeOperator(base.TestCase): +class TestShadeOperator(base.RequestsMockTestCase): + + def setUp(self): + super(TestShadeOperator, self).setUp() + self.machine_id = uuid.uuid4().hex + self.machine_name = self.getUniqueString('machine') + self.node = fakes.make_fake_machine( + machine_id=self.machine_id, + machine_name=self.machine_name) + + def get_ironic_mock_url(self, append=None, *args, **kwargs): + if append: + # TODO(mordred): Remove when we do version discovery + # properly everywhere + append.insert(0, 'v1') + return self.get_mock_url('baremetal', append=append, *args, **kwargs) def test_operator_cloud(self): self.assertIsInstance(self.op_cloud, shade.OperatorCloud) - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_get_machine(self, mock_client): - node = fakes.FakeMachine(id='00000000-0000-0000-0000-000000000000', - name='bigOlFaker') - mock_client.node.get.return_value = node - machine = self.op_cloud.get_machine('bigOlFaker') - mock_client.node.get.assert_called_with(node_id='bigOlFaker') - self.assertEqual(meta.obj_to_dict(node), machine) + def test_get_machine(self): + + self.register_uris([ + dict(method='GET', + uri=self.get_ironic_mock_url( + append=['nodes', self.machine_name]), + json=self.node), + ]) + machine = self.op_cloud.get_machine(self.machine_name) + self.assertEqual(self.node, machine) + + self.assert_calls() @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_get_machine_by_mac(self, mock_client): From a88d41e3026497cc98b0779d82fb05ee8ea4e720 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 27 Apr 2017 18:46:04 -0500 Subject: [PATCH 1530/3836] Fix tips jobs and convert Nova Floating IP calls The script to install the git versions of the libraries wasn't working because it wasn't installing over top of the versions already installed by pip. So by adding an uninstall first, we ensure that the git version is actually installed. Also, add a pbr freeze to the end so that we can verify, and some +x so that we can see the output of our scripts. And: novaclient 8.0 doesn't have these calls anymore. However, the tests we had on legacy clouds were erroneously installing the wrong version of novaclient so we did not catch it. Doh. This has to do the tests and the calls in the same patch because the fix to the gate job to install the correct verison of the library exposed the fact that we had be broken for these for a few minutes. Depends-On: I208e8c009d0438de19cd3eb08dc45ddebb45d3e9 Change-Id: I4fd882aeb8373b94c7f6b54d97b457042b324361 --- extras/install-tips.sh | 9 +- shade/_tasks.py | 25 -- shade/openstackcloud.py | 75 ++--- .../tests/functional/hooks/post_test_hook.sh | 3 +- shade/tests/functional/test_floating_ip.py | 4 +- shade/tests/unit/test_floating_ip_nova.py | 284 ++++++++++-------- tox.ini | 2 +- 7 files changed, 211 insertions(+), 191 deletions(-) diff --git a/extras/install-tips.sh b/extras/install-tips.sh index fe7a90780..e3ea482d5 100644 --- a/extras/install-tips.sh +++ b/extras/install-tips.sh @@ -15,16 +15,21 @@ # limitations under the License. for lib in \ - os-client-config \ python-novaclient \ python-keystoneclient \ python-cinderclient \ python-ironicclient \ python-designateclient \ + os-client-config \ keystoneauth do egg=$(echo $lib | tr '-' '_' | sed 's/python-//') if [ -d /opt/stack/new/$lib ] ; then - pip install -q -U -e "git+file:///opt/stack/new/$lib#egg=$egg" + tip_location="git+file:///opt/stack/new/$lib#egg=$egg" + echo "$(which pip) install -U -e $tip_location" + pip uninstall -y $lib + pip install -U -e $tip_location + else + echo "$lib not found in /opt/stack/new/$lib" fi done diff --git a/shade/_tasks.py b/shade/_tasks.py index ab66039fa..57e82fe5e 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -212,31 +212,6 @@ def main(self, client): return client.nova_client.images.list() -class NovaFloatingIPList(task_manager.Task): - def main(self, client): - return client.nova_client.floating_ips.list() - - -class NovaFloatingIPCreate(task_manager.Task): - def main(self, client): - return client.nova_client.floating_ips.create(**self.args) - - -class NovaFloatingIPDelete(task_manager.Task): - def main(self, client): - return client.nova_client.floating_ips.delete(**self.args) - - -class NovaFloatingIPAttach(task_manager.Task): - def main(self, client): - return client.nova_client.servers.add_floating_ip(**self.args) - - -class NovaFloatingIPDetach(task_manager.Task): - def main(self, client): - return client.nova_client.servers.remove_floating_ip(**self.args) - - class MachineCreate(task_manager.Task): def main(self, client): return client.ironic_client.node.create(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 3c5b43aad..4849d7c2e 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2012,8 +2012,7 @@ def _neutron_list_floating_ips(self, filters=None): return self._network_client.get('/floatingips.json', params=filters) def _nova_list_floating_ips(self): - with _utils.shade_exceptions("Error fetching floating IPs list"): - return self.manager.submit_task(_tasks.NovaFloatingIPList()) + return self._compute_client.get('/os-floating-ips') def use_external_network(self): return self._use_external_network @@ -4553,8 +4552,11 @@ def _nova_create_floating_ip(self, pool=None): "unable to find a floating ip pool") pool = pools[0]['name'] - pool_ip = self.manager.submit_task( - _tasks.NovaFloatingIPCreate(pool=pool)) + pool_ip = self._compute_client.post( + '/os-floating-ips', json=dict(pool=pool)) + # TODO(mordred) Remove this - it's just for compat + pool_ip = self._compute_client.get('/os-floating-ips/{id}'.format( + id=pool_ip['id'])) return pool_ip def delete_floating_ip(self, floating_ip_id, retry=1): @@ -4622,16 +4624,12 @@ def _neutron_delete_floating_ip(self, floating_ip_id): def _nova_delete_floating_ip(self, floating_ip_id): try: - self.manager.submit_task( - _tasks.NovaFloatingIPDelete(floating_ip=floating_ip_id)) - except nova_exceptions.NotFound: + self._compute_client.delete( + '/os-floating-ips/{id}'.format(id=floating_ip_id), + error_message='Unable to delete floating IP {fip_id}'.format( + fip_id=floating_ip_id)) + except OpenStackCloudURINotFound: return False - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Unable to delete floating IP ID {fip_id}: {msg}".format( - fip_id=floating_ip_id, msg=str(e))) return True def delete_unattached_floating_ips(self, retry=1): @@ -4853,13 +4851,22 @@ def _neutron_attach_ip_to_server( def _nova_attach_ip_to_server(self, server_id, floating_ip_id, fixed_address=None): - with _utils.shade_exceptions( - "Error attaching IP {ip} to instance {id}".format( - ip=floating_ip_id, id=server_id)): - f_ip = self.get_floating_ip(id=floating_ip_id) - return self.manager.submit_task(_tasks.NovaFloatingIPAttach( - server=server_id, address=f_ip['floating_ip_address'], - fixed_address=fixed_address)) + f_ip = self.get_floating_ip( + id=floating_ip_id) + if f_ip is None: + raise OpenStackCloudException( + "unable to find floating IP {0}".format(floating_ip_id)) + error_message = "Error attaching IP {ip} to instance {id}".format( + ip=floating_ip_id, id=server_id) + body = { + 'address': f_ip['floating_ip_address'] + } + if fixed_address: + body['fixed_address'] = fixed_address + return self._compute_client.post( + '/servers/{server_id}/action'.format(server_id=server_id), + json=dict(addFloatingIp=body), + error_message=error_message) def detach_ip_from_server(self, server_id, floating_ip_id): """Detach a floating IP from a server. @@ -4900,24 +4907,18 @@ def _neutron_detach_ip_from_server(self, server_id, floating_ip_id): return True def _nova_detach_ip_from_server(self, server_id, floating_ip_id): - try: - f_ip = self.get_floating_ip(id=floating_ip_id) - if f_ip is None: - raise OpenStackCloudException( - "unable to find floating IP {0}".format(floating_ip_id)) - self.manager.submit_task(_tasks.NovaFloatingIPDetach( - server=server_id, address=f_ip['floating_ip_address'])) - except nova_exceptions.Conflict as e: - self.log.debug( - "nova floating IP detach failed: %(msg)s", {'msg': str(e)}, - exc_info=True) - return False - except OpenStackCloudException: - raise - except Exception as e: + + f_ip = self.get_floating_ip(id=floating_ip_id) + if f_ip is None: raise OpenStackCloudException( - "Error detaching IP {ip} from instance {id}: {msg}".format( - ip=floating_ip_id, id=server_id, msg=str(e))) + "unable to find floating IP {0}".format(floating_ip_id)) + error_message = "Error detaching IP {ip} from instance {id}".format( + ip=floating_ip_id, id=server_id) + return self._compute_client.post( + '/servers/{server_id}/action'.format(server_id=server_id), + json=dict(removeFloatingIp=dict( + address=f_ip['floating_ip_address'])), + error_message=error_message) return True diff --git a/shade/tests/functional/hooks/post_test_hook.sh b/shade/tests/functional/hooks/post_test_hook.sh index 22ae99ec4..6be3fe42c 100755 --- a/shade/tests/functional/hooks/post_test_hook.sh +++ b/shade/tests/functional/hooks/post_test_hook.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash -x # 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 @@ -46,6 +46,7 @@ set +e sudo -E -H -u jenkins tox -e$tox_env EXIT_CODE=$? sudo testr last --subunit > $WORKSPACE/tempest.subunit +.tox/$tox_env/bin/pbr freeze set -e exit $EXIT_CODE diff --git a/shade/tests/functional/test_floating_ip.py b/shade/tests/functional/test_floating_ip.py index 3e0d51fc1..9046a8ef4 100644 --- a/shade/tests/functional/test_floating_ip.py +++ b/shade/tests/functional/test_floating_ip.py @@ -166,8 +166,8 @@ def _setup_networks(self): content.text_content(pprint.pformat( self.user_cloud.list_networks()))) else: - # ToDo: remove once we have list/get methods for nova networks - nets = self.user_cloud.nova_client.networks.list() + # Find network names for nova-net + nets = self.user_cloud._compute_client.get('/os-tenant-networks') self.addDetail( 'networks-nova', content.text_content(pprint.pformat( diff --git a/shade/tests/unit/test_floating_ip_nova.py b/shade/tests/unit/test_floating_ip_nova.py index adf6ed32d..cc0acd18d 100644 --- a/shade/tests/unit/test_floating_ip_nova.py +++ b/shade/tests/unit/test_floating_ip_nova.py @@ -19,22 +19,20 @@ Tests Floating IP resource methods for nova-network """ -from mock import patch -from novaclient import exceptions as n_exc - from shade import meta -from shade import OpenStackCloud from shade.tests import fakes from shade.tests.unit import base -def has_service_side_effect(s): - if s == 'network': - return False - return True +def get_fake_has_service(has_service): + def fake_has_service(s): + if s == 'network': + return False + return has_service(s) + return fake_has_service -class TestFloatingIP(base.TestCase): +class TestFloatingIP(base.RequestsMockTestCase): mock_floating_ip_list_rep = [ { 'fixed_ip': None, @@ -69,9 +67,6 @@ def assertAreInstances(self, elements, elem_type): def setUp(self): super(TestFloatingIP, self).setUp() - self.floating_ips = [ - fakes.FakeFloatingIP(**ip) for ip in self.mock_floating_ip_list_rep - ] self.fake_server = meta.obj_to_dict( fakes.FakeServer( @@ -83,172 +78,223 @@ def setUp(self): u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:ae:7d:42'}]})) - self.floating_ip = self.cloud._normalize_floating_ips( - meta.obj_list_to_dict(self.floating_ips))[0] + self.cloud.has_service = get_fake_has_service(self.cloud.has_service) - @patch.object(OpenStackCloud, 'nova_client') - @patch.object(OpenStackCloud, 'has_service') - def test_list_floating_ips(self, mock_has_service, mock_nova_client): - mock_has_service.side_effect = has_service_side_effect - mock_nova_client.floating_ips.list.return_value = self.floating_ips + def test_list_floating_ips(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url('compute', append=['os-floating-ips']), + json={'floating_ips': self.mock_floating_ip_list_rep}), + ]) floating_ips = self.cloud.list_floating_ips() - mock_nova_client.floating_ips.list.assert_called_with() self.assertIsInstance(floating_ips, list) self.assertEqual(3, len(floating_ips)) self.assertAreInstances(floating_ips, dict) - @patch.object(OpenStackCloud, 'nova_client') - @patch.object(OpenStackCloud, 'has_service') - def test_list_floating_ips_with_filters(self, mock_has_service, - mock_nova_client): - mock_has_service.side_effect = has_service_side_effect + self.assert_calls() + def test_list_floating_ips_with_filters(self): self.assertRaisesRegex( ValueError, "Nova-network don't support server-side", self.cloud.list_floating_ips, filters={'Foo': 42} ) - @patch.object(OpenStackCloud, 'nova_client') - @patch.object(OpenStackCloud, 'has_service') - def test_search_floating_ips(self, mock_has_service, mock_nova_client): - mock_has_service.side_effect = has_service_side_effect - mock_nova_client.floating_ips.list.return_value = self.floating_ips + def test_search_floating_ips(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url('compute', append=['os-floating-ips']), + json={'floating_ips': self.mock_floating_ip_list_rep}), + ]) floating_ips = self.cloud.search_floating_ips( filters={'attached': False}) - mock_nova_client.floating_ips.list.assert_called_with() self.assertIsInstance(floating_ips, list) self.assertEqual(2, len(floating_ips)) self.assertAreInstances(floating_ips, dict) - @patch.object(OpenStackCloud, 'nova_client') - @patch.object(OpenStackCloud, 'has_service') - def test_get_floating_ip(self, mock_has_service, mock_nova_client): - mock_has_service.side_effect = has_service_side_effect - mock_nova_client.floating_ips.list.return_value = self.floating_ips + self.assert_calls() + + def test_get_floating_ip(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url('compute', append=['os-floating-ips']), + json={'floating_ips': self.mock_floating_ip_list_rep}), + ]) floating_ip = self.cloud.get_floating_ip(id='29') - mock_nova_client.floating_ips.list.assert_called_with() self.assertIsInstance(floating_ip, dict) self.assertEqual('198.51.100.29', floating_ip['floating_ip_address']) - @patch.object(OpenStackCloud, 'nova_client') - @patch.object(OpenStackCloud, 'has_service') - def test_get_floating_ip_not_found( - self, mock_has_service, mock_nova_client): - mock_has_service.side_effect = has_service_side_effect - mock_nova_client.floating_ips.list.return_value = self.floating_ips + self.assert_calls() + + def test_get_floating_ip_not_found(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url('compute', append=['os-floating-ips']), + json={'floating_ips': self.mock_floating_ip_list_rep}), + ]) floating_ip = self.cloud.get_floating_ip(id='666') self.assertIsNone(floating_ip) - @patch.object(OpenStackCloud, 'nova_client') - @patch.object(OpenStackCloud, 'has_service') - def test_create_floating_ip(self, mock_has_service, mock_nova_client): - mock_has_service.side_effect = has_service_side_effect - mock_nova_client.floating_ips.create.return_value =\ - fakes.FakeFloatingIP(**self.mock_floating_ip_list_rep[1]) + self.assert_calls() + + def test_create_floating_ip(self): + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url('compute', append=['os-floating-ips']), + json={'floating_ip': self.mock_floating_ip_list_rep[1]}, + validate=dict( + json={'pool': 'nova'})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', + append=['os-floating-ips', '2']), + json={'floating_ips': self.mock_floating_ip_list_rep[1]}), + ]) self.cloud.create_floating_ip(network='nova') - mock_nova_client.floating_ips.create.assert_called_with(pool='nova') + self.assert_calls() - @patch.object(OpenStackCloud, '_nova_list_floating_ips') - @patch.object(OpenStackCloud, 'has_service') - def test_available_floating_ip_existing( - self, mock_has_service, mock__nova_list_floating_ips): - mock_has_service.side_effect = has_service_side_effect - mock__nova_list_floating_ips.return_value = \ - self.mock_floating_ip_list_rep[:1] + def test_available_floating_ip_existing(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url('compute', append=['os-floating-ips']), + json={'floating_ips': self.mock_floating_ip_list_rep[:1]}), + ]) ip = self.cloud.available_floating_ip(network='nova') self.assertEqual(self.mock_floating_ip_list_rep[0]['ip'], ip['floating_ip_address']) - - @patch.object(OpenStackCloud, 'nova_client') - @patch.object(OpenStackCloud, '_nova_list_floating_ips') - @patch.object(OpenStackCloud, 'has_service') - def test_available_floating_ip_new( - self, mock_has_service, mock__nova_list_floating_ips, - mock_nova_client): - mock_has_service.side_effect = has_service_side_effect - mock__nova_list_floating_ips.return_value = [] - mock_nova_client.floating_ips.create.return_value = \ - fakes.FakeFloatingIP(**self.mock_floating_ip_list_rep[0]) + self.assert_calls() + + def test_available_floating_ip_new(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url('compute', append=['os-floating-ips']), + json={'floating_ips': []}), + dict(method='POST', + uri=self.get_mock_url('compute', append=['os-floating-ips']), + json={'floating_ip': self.mock_floating_ip_list_rep[0]}, + validate=dict( + json={'pool': 'nova'})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', + append=['os-floating-ips', '1']), + json={'floating_ip': self.mock_floating_ip_list_rep[0]}), + ]) ip = self.cloud.available_floating_ip(network='nova') self.assertEqual(self.mock_floating_ip_list_rep[0]['ip'], ip['floating_ip_address']) + self.assert_calls() - @patch.object(OpenStackCloud, 'nova_client') - @patch.object(OpenStackCloud, 'has_service') - def test_delete_floating_ip_existing( - self, mock_has_service, mock_nova_client): - mock_has_service.side_effect = has_service_side_effect - mock_nova_client.floating_ips.delete.return_value = None + def test_delete_floating_ip_existing(self): + + self.register_uris([ + dict(method='DELETE', + uri=self.get_mock_url( + 'compute', + append=['os-floating-ips', 'a-wild-id-appears'])), + dict(method='GET', + uri=self.get_mock_url('compute', append=['os-floating-ips']), + json={'floating_ips': []}), + ]) ret = self.cloud.delete_floating_ip( floating_ip_id='a-wild-id-appears') - mock_nova_client.floating_ips.delete.assert_called_with( - floating_ip='a-wild-id-appears') self.assertTrue(ret) + self.assert_calls() - @patch.object(OpenStackCloud, 'nova_client') - @patch.object(OpenStackCloud, 'get_floating_ip') - @patch.object(OpenStackCloud, '_use_neutron_floating') - def test_delete_floating_ip_not_found( - self, mock_use_floating, mock_get_floating_ip, mock_nova_client): - mock_use_floating.return_value = False - mock_get_floating_ip.return_value = None - mock_nova_client.floating_ips.delete.side_effect = n_exc.NotFound( - code=404) + def test_delete_floating_ip_not_found(self): + self.register_uris([ + dict(method='DELETE', + uri=self.get_mock_url( + 'compute', + append=['os-floating-ips', 'a-wild-id-appears']), + status_code=404), + ]) ret = self.cloud.delete_floating_ip( floating_ip_id='a-wild-id-appears') self.assertFalse(ret) - - @patch.object(OpenStackCloud, 'nova_client') - @patch.object(OpenStackCloud, 'has_service') - def test_attach_ip_to_server(self, mock_has_service, mock_nova_client): - mock_has_service.side_effect = has_service_side_effect - mock_nova_client.floating_ips.list.return_value = self.floating_ips + self.assert_calls() + + def test_attach_ip_to_server(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url('compute', append=['os-floating-ips']), + json={'floating_ips': self.mock_floating_ip_list_rep}), + dict(method='POST', + uri=self.get_mock_url( + 'compute', + append=['servers', self.fake_server.id, 'action']), + validate=dict( + json={ + "addFloatingIp": { + "address": "203.0.113.1", + "fixed_address": "192.0.2.129", + }})), + ]) self.cloud._attach_ip_to_server( - server=self.fake_server, floating_ip=self.floating_ip, - fixed_address='192.0.2.129') - - mock_nova_client.servers.add_floating_ip.assert_called_with( - server='server-id', address='203.0.113.1', + server=self.fake_server, + floating_ip=self.cloud._normalize_floating_ip( + self.mock_floating_ip_list_rep[0]), fixed_address='192.0.2.129') - @patch.object(OpenStackCloud, 'nova_client') - @patch.object(OpenStackCloud, 'has_service') - def test_detach_ip_from_server(self, mock_has_service, mock_nova_client): - mock_has_service.side_effect = has_service_side_effect - mock_nova_client.floating_ips.list.return_value = [ - fakes.FakeFloatingIP(**ip) for ip in self.mock_floating_ip_list_rep - ] + self.assert_calls() + + def test_detach_ip_from_server(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url('compute', append=['os-floating-ips']), + json={'floating_ips': self.mock_floating_ip_list_rep}), + dict(method='POST', + uri=self.get_mock_url( + 'compute', + append=['servers', self.fake_server.id, 'action']), + validate=dict( + json={ + "removeFloatingIp": { + "address": "203.0.113.1", + }})), + ]) self.cloud.detach_ip_from_server( server_id='server-id', floating_ip_id=1) - - mock_nova_client.servers.remove_floating_ip.assert_called_with( - server='server-id', address='203.0.113.1') - - @patch.object(OpenStackCloud, 'nova_client') - @patch.object(OpenStackCloud, 'has_service') - def test_add_ip_from_pool(self, mock_has_service, mock_nova_client): - mock_has_service.side_effect = has_service_side_effect - mock_nova_client.floating_ips.list.return_value = self.floating_ips + self.assert_calls() + + def test_add_ip_from_pool(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url('compute', append=['os-floating-ips']), + json={'floating_ips': self.mock_floating_ip_list_rep}), + dict(method='GET', + uri=self.get_mock_url('compute', append=['os-floating-ips']), + json={'floating_ips': self.mock_floating_ip_list_rep}), + dict(method='POST', + uri=self.get_mock_url( + 'compute', + append=['servers', self.fake_server.id, 'action']), + validate=dict( + json={ + "addFloatingIp": { + "address": "203.0.113.1", + "fixed_address": "192.0.2.129", + }})), + ]) server = self.cloud._add_ip_from_pool( server=self.fake_server, @@ -256,16 +302,8 @@ def test_add_ip_from_pool(self, mock_has_service, mock_nova_client): fixed_address='192.0.2.129') self.assertEqual(server, self.fake_server) + self.assert_calls() - @patch.object(OpenStackCloud, 'delete_floating_ip') - @patch.object(OpenStackCloud, 'list_floating_ips') - @patch.object(OpenStackCloud, '_use_neutron_floating') - def test_cleanup_floating_ips( - self, mock_use_neutron_floating, mock_list_floating_ips, - mock_delete_floating_ip): - mock_use_neutron_floating.return_value = False - + def test_cleanup_floating_ips(self): + # This should not call anything because it's unsafe on nova. self.cloud.delete_unattached_floating_ips() - - mock_delete_floating_ip.assert_not_called() - mock_list_floating_ips.assert_not_called() diff --git a/tox.ini b/tox.ini index 1adb38ae6..8db2e9b7b 100644 --- a/tox.ini +++ b/tox.ini @@ -26,7 +26,7 @@ setenv = passenv = OS_* SHADE_* whitelist_externals = bash commands = - bash extras/install-tips.sh + bash -x {toxinidir}/extras/install-tips.sh python setup.py testr --slowest --testr-args='--concurrency=1 {posargs}' [testenv:pep8] From e1dfc7228da2729919b54a03825a5d1840718510 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 14 May 2017 07:53:12 -0500 Subject: [PATCH 1531/3836] Remove cinderclient from install-tips.sh It's removed from requirements - missed it in the install-tips helper script. Change-Id: If798251387bdb81da980ef9e8d9869f2b0738cbe --- extras/install-tips.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/extras/install-tips.sh b/extras/install-tips.sh index e3ea482d5..7af026d8b 100644 --- a/extras/install-tips.sh +++ b/extras/install-tips.sh @@ -17,7 +17,6 @@ for lib in \ python-novaclient \ python-keystoneclient \ - python-cinderclient \ python-ironicclient \ python-designateclient \ os-client-config \ From 40f66c70c3703e1ffd499562a99d724cffbe2c53 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 15 May 2017 10:46:21 -0500 Subject: [PATCH 1532/3836] Fix broken version discovery endpoints In the version discovery spec, it is noted that both schema and netloc on endpoints reported by version discovery documents are known to be able to be broken, but shade was only fixing scheme. Update the code to do the right thing. Also, add a test to ensure that this behavior occurs correctly. While we're in the tests for this, have the discovery call return a 300, which is what it returns. There are no bugs in this area, but 300 is correct so we should test it that way. Story: 2001027 Change-Id: Id04f6f033c9277e791b602f193d1eaf6ac73047f --- shade/openstackcloud.py | 2 +- shade/tests/unit/base.py | 1 + .../unit/fixtures/image-version-broken.json | 64 +++++++++++++++++++ shade/tests/unit/test_image.py | 23 +++++++ 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 shade/tests/unit/fixtures/image-version-broken.json diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 4849d7c2e..c92ca82fa 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -469,7 +469,7 @@ def _discover_image_endpoint(self, config_version, image_client): return urllib.parse.ParseResult( catalog_endpoint.scheme, - discovered_endpoint.netloc, + catalog_endpoint.netloc, discovered_endpoint.path, discovered_endpoint.params, discovered_endpoint.query, diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index b7d8e8588..1dd4cb2c2 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -474,6 +474,7 @@ def get_glance_discovery_mock_dict( discovery_fixture = os.path.join( self.fixtures_directory, image_version_json) return dict(method='GET', uri='https://image.example.com/', + status_code=300, text=open(discovery_fixture, 'r').read()) def get_designate_discovery_mock_dict(self): diff --git a/shade/tests/unit/fixtures/image-version-broken.json b/shade/tests/unit/fixtures/image-version-broken.json new file mode 100644 index 000000000..a130ca403 --- /dev/null +++ b/shade/tests/unit/fixtures/image-version-broken.json @@ -0,0 +1,64 @@ +{ + "versions": [ + { + "status": "CURRENT", + "id": "v2.3", + "links": [ + { + "href": "http://localhost/v2/", + "rel": "self" + } + ] + }, + { + "status": "SUPPORTED", + "id": "v2.2", + "links": [ + { + "href": "http://localhost/v2/", + "rel": "self" + } + ] + }, + { + "status": "SUPPORTED", + "id": "v2.1", + "links": [ + { + "href": "http://localhost/v2/", + "rel": "self" + } + ] + }, + { + "status": "SUPPORTED", + "id": "v2.0", + "links": [ + { + "href": "http://localhost/v2/", + "rel": "self" + } + ] + }, + { + "status": "SUPPORTED", + "id": "v1.1", + "links": [ + { + "href": "http://localhost/v1/", + "rel": "self" + } + ] + }, + { + "status": "SUPPORTED", + "id": "v1.0", + "links": [ + { + "href": "http://localhost/v1/", + "rel": "self" + } + ] + } + ] +} diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index ce206cf51..c19125577 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -738,3 +738,26 @@ def test_create_image_volume_duplicate(self): volume={'id': volume_id}, allow_duplicates=True) self.assert_calls() + + +class TestImageBrokenDiscovery(base.RequestsMockTestCase): + + def setUp(self): + super(TestImageBrokenDiscovery, self).setUp() + self.use_glance(image_version_json='image-version-broken.json') + + def test_url_fix(self): + # image-version-broken.json has both http urls and localhost as the + # host. This is testing that what is discovered is https, because + # that's what's in the catalog, and image.example.com for the same + # reason. + self.register_uris([ + dict(method='GET', + uri='https://image.example.com/v2/images', + json={'images': []}) + ]) + self.assertEqual([], self.cloud.list_images()) + self.assertEqual( + self.cloud._image_client.endpoint_override, + 'https://image.example.com/v2/') + self.assert_calls() From 9ecf600397aedd54236cbbb72bc8a080ea2f7a27 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 15 May 2017 11:14:27 -0500 Subject: [PATCH 1533/3836] Optimize the case of versioned image endpoint in catalog We have version discovery for glance. However, if someone asks for a version, and it's the version that they get from the catalog, we can skip doing a full discovery run. Do that. Change-Id: I2c0694ffc70bf2801de1fc187ba7b38e6a1b9d09 --- shade/openstackcloud.py | 14 ++++ shade/tests/unit/base.py | 4 +- .../fixtures/catalog-versioned-image.json | 71 +++++++++++++++++++ shade/tests/unit/test_image.py | 18 +++++ 4 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 shade/tests/unit/fixtures/catalog-versioned-image.json diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index c92ca82fa..8091d2169 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -418,8 +418,22 @@ def _raw_image_client(self): self._raw_clients['raw-image'] = image_client return self._raw_clients['raw-image'] + def _match_given_image_endpoint(self, given, version): + if given.endswith('/'): + given = given[:-1] + if given.split('/')[-1].startswith('v' + version[0]): + return True + return False + def _discover_image_endpoint(self, config_version, image_client): try: + # First - quick check to see if the endpoint in the catalog + # is a versioned endpoint that matches the version we requested. + # If it is, don't do any additoinal work. + catalog_endpoint = image_client.get_endpoint() + if self._match_given_image_endpoint( + catalog_endpoint, config_version): + return catalog_endpoint # Version discovery versions = image_client.get('/') api_version = None diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 1dd4cb2c2..c0d635d97 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -401,7 +401,7 @@ def _get_role_data(self, role_name=None): return _RoleData(role_id, role_name, {'role': response}, {'role': request}) - def use_keystone_v3(self): + def use_keystone_v3(self, catalog='catalog-v3.json'): self.adapter = self.useFixture(rm_fixture.Fixture()) self.calls = [] self._uri_registry.clear() @@ -413,7 +413,7 @@ def use_keystone_v3(self): headers={ 'X-Subject-Token': self.getUniqueString('KeystoneToken')}, text=open(os.path.join( - self.fixtures_directory, 'catalog-v3.json'), 'r').read() + self.fixtures_directory, catalog), 'r').read() ) ]) self._make_test_cloud(identity_api_version='3') diff --git a/shade/tests/unit/fixtures/catalog-versioned-image.json b/shade/tests/unit/fixtures/catalog-versioned-image.json new file mode 100644 index 000000000..cb33dd5c4 --- /dev/null +++ b/shade/tests/unit/fixtures/catalog-versioned-image.json @@ -0,0 +1,71 @@ +{ + "token": { + "audit_ids": [ + "Rvn7eHkiSeOwucBIPaKdYA" + ], + "catalog": [ + { + "endpoints": [ + { + "id": "5a64de3c4a614d8d8f8d1ba3dee5f45f", + "interface": "public", + "region": "RegionOne", + "url": "https://image.example.com/v2" + } + ], + "name": "glance", + "type": "image" + }, + { + "endpoints": [ + { + "id": "4deb4d0504a044a395d4480741ba628c", + "interface": "public", + "region": "RegionOne", + "url": "https://identity.example.com" + }, + { + "id": "012322eeedcd459edabb4933021112bc", + "interface": "admin", + "region": "RegionOne", + "url": "https://identity.example.com" + } + ], + "endpoints_links": [], + "name": "keystone", + "type": "identity" + } + ], + "expires_at": "9999-12-31T23:59:59Z", + "issued_at": "2016-12-17T14:25:05.000000Z", + "methods": [ + "password" + ], + "project": { + "domain": { + "id": "default", + "name": "default" + }, + "id": "1c36b64c840a42cd9e9b931a369337f0", + "name": "Default Project" + }, + "roles": [ + { + "id": "9fe2ff9ee4384b1894a90878d3e92bab", + "name": "_member_" + }, + { + "id": "37071fc082e14c2284c32a2761f71c63", + "name": "swiftoperator" + } + ], + "user": { + "domain": { + "id": "default", + "name": "default" + }, + "id": "c17534835f8f42bf98fc367e0bf35e09", + "name": "mordred" + } + } +} diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index c19125577..8f4bdc0c0 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -761,3 +761,21 @@ def test_url_fix(self): self.cloud._image_client.endpoint_override, 'https://image.example.com/v2/') self.assert_calls() + + +class TestImageDiscoveryOptimization(base.RequestsMockTestCase): + + def setUp(self): + super(TestImageDiscoveryOptimization, self).setUp() + self.use_keystone_v3(catalog='catalog-versioned-image.json') + + def test_version_discovery_skip(self): + self.cloud.cloud_config.config['image_api_version'] = '2' + + self.register_uris([ + dict(method='GET', + uri='https://image.example.com/v2/images', + json={'images': []}) + ]) + self.assertEqual([], self.cloud.list_images()) + self.assert_calls() From dbb42fc6ffec6dad484c1813639d5ce0643bd644 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 15 May 2017 15:42:19 -0500 Subject: [PATCH 1534/3836] Include error message from server if one exists Nova includes bodies in error responses that look like this: { "badRequest": { "message": "Invalid input for field/attribute fixed_address.", "code": 400 } } I recently had to look at HTTP debug logs to figure out a bug. Let's include that data in our exceptions already. Change-Id: I4b4b4702d772739b8f930ff6a3c1ad83987fed17 --- shade/exc.py | 46 ++++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/shade/exc.py b/shade/exc.py index 5a74834a1..c8c579ed6 100644 --- a/shade/exc.py +++ b/shade/exc.py @@ -96,27 +96,33 @@ def raise_from_response(response, error_message=None): elif 500 <= response.status_code < 600: source = "Server" else: - source = None - - if response.reason: - remote_error = "Error: {reason} for {url}".format( - reason=response.reason, - url=response.url) + return + + remote_error = "Error for url: {url}".format(url=response.url) + try: + details = response.json() + # Nova returns documents that look like + # {statusname: 'message': message, 'code': code} + if len(details.keys()) == 1: + detail_key = details.keys()[0] + detail_message = details[detail_key].get('message') + if detail_message: + remote_error += " {message}".format(message=detail_message) + except ValueError: + if response.reason: + remote_error += " {reason}".format(reason=response.reason) + + if error_message: + msg = '{error_message}. ({code}) {source} {remote_error}'.format( + error_message=error_message, + source=source, + code=response.status_code, + remote_error=remote_error) else: - remote_error = "Error for url: {url}".format(url=response.url) - - if source: - if error_message: - msg = '{error_message}. ({code}) {source} {remote_error}'.format( - error_message=error_message, - source=source, - code=response.status_code, - remote_error=remote_error) - else: - msg = '({code}) {source} {remote_error}'.format( - code=response.status_code, - source=source, - remote_error=remote_error) + msg = '({code}) {source} {remote_error}'.format( + code=response.status_code, + source=source, + remote_error=remote_error) # Special case 404 since we raised a specific one for neutron exceptions # before From f11edd7fdd6196ac4a74c33679efd5bbdba96e10 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 15 May 2017 16:00:18 -0500 Subject: [PATCH 1535/3836] Remove two unused nova tasks ServerConsoleGet and NovaImageList were both used but still in the file. Change-Id: I91a609b241eed497842a2a28ed41d2b1205913ca --- shade/_tasks.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 57e82fe5e..43b76770e 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -92,11 +92,6 @@ def main(self, client): return client.nova_client.servers.list_security_group(**self.args) -class ServerConsoleGet(task_manager.Task): - def main(self, client): - return client.nova_client.servers.get_console_output(**self.args) - - class ServerGet(task_manager.Task): def main(self, client): return client.nova_client.servers.get(**self.args) @@ -207,11 +202,6 @@ def main(self, client): return client.nova_client.keypairs.delete(**self.args) -class NovaImageList(task_manager.Task): - def main(self, client): - return client.nova_client.images.list() - - class MachineCreate(task_manager.Task): def main(self, client): return client.ironic_client.node.create(**self.args) From 4199976762d53583814b371f51672eb4a7a06a50 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 15 May 2017 16:09:37 -0500 Subject: [PATCH 1536/3836] Convert list_server_security_groups to REST The tests for this are already covered, the call just didn't get converted. Change-Id: I78f91dca0ec41207c433597bcf0fc6284bed73c7 --- shade/_tasks.py | 5 ----- shade/openstackcloud.py | 6 +++--- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 43b76770e..c350510b7 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -87,11 +87,6 @@ def main(self, client): return client.nova_client.servers.list(**self.args) -class ServerListSecurityGroups(task_manager.Task): - def main(self, client): - return client.nova_client.servers.list_security_group(**self.args) - - class ServerGet(task_manager.Task): def main(self, client): return client.nova_client.servers.get(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 8091d2169..6c1c91e1d 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1681,9 +1681,9 @@ def list_server_security_groups(self, server): if not self._has_secgroups(): return [] - with _utils.shade_exceptions(): - groups = self.manager.submit_task( - _tasks.ServerListSecurityGroups(server=server['id'])) + groups = self._compute_client.get( + '/servers/{server_id}/os-security-groups'.format( + server_id=server['id'])) return self._normalize_secgroups(groups) From 892b502251c08a930d62b146df5aac6f8bfe3f5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Wed, 10 May 2017 16:54:59 +0000 Subject: [PATCH 1537/3836] Remove designateclient mock from recordset tests Designate client mock is replaced by requests_mocks Change-Id: I839a4d83bb53214980fd87beb79454251335721b --- shade/tests/unit/test_recordset.py | 368 ++++++++++++++++++++++------- 1 file changed, 289 insertions(+), 79 deletions(-) diff --git a/shade/tests/unit/test_recordset.py b/shade/tests/unit/test_recordset.py index 484c82c03..975a70630 100644 --- a/shade/tests/unit/test_recordset.py +++ b/shade/tests/unit/test_recordset.py @@ -11,102 +11,312 @@ # under the License. -import mock +import copy import testtools import shade from shade.tests.unit import base -from shade.tests import fakes - -zone_obj = fakes.FakeZone( - id='1', - name='example.net.', - type_='PRIMARY', - email='test@example.net', - description='Example zone', - ttl=3600, - masters=None -) - -recordset_obj = fakes.FakeRecordset( - zone='1', - id='1', - name='www.example.net.', - type_='A', - description='Example zone', - ttl=3600, - records=['192.168.1.1'] -) - - -class TestRecordset(base.TestCase): + + +zone = { + 'id': '1', + 'name': 'example.net.', + 'type': 'PRIMARY', + 'email': 'test@example.net', + 'description': 'Example zone', + 'ttl': 3600, +} + +recordset = { + 'name': 'www.example.net.', + 'type': 'A', + 'description': 'Example zone', + 'ttl': 3600, + 'records': ['192.168.1.1'] +} +recordset_zone = '1' + +new_recordset = copy.copy(recordset) +new_recordset['id'] = '1' +new_recordset['zone'] = recordset_zone + + +class TestRecordset(base.RequestsMockTestCase): def setUp(self): super(TestRecordset, self).setUp() + self.use_designate() - @mock.patch.object(shade.OpenStackCloud, 'designate_client') - def test_create_recordset(self, mock_designate): - mock_designate.zones.list.return_value = [zone_obj] - self.cloud.create_recordset(zone=recordset_obj.zone, - name=recordset_obj.name, - recordset_type=recordset_obj.type_, - records=recordset_obj.records, - description=recordset_obj.description, - ttl=recordset_obj.ttl) - mock_designate.recordsets.create.assert_called_once_with( - zone=recordset_obj.zone, name=recordset_obj.name, - type_=recordset_obj.type_.upper(), - records=recordset_obj.records, - description=recordset_obj.description, - ttl=recordset_obj.ttl - ) - - @mock.patch.object(shade.OpenStackCloud, 'designate_client') - def test_create_recordset_exception(self, mock_designate): - mock_designate.zones.list.return_value = [zone_obj] - mock_designate.recordsets.create.side_effect = Exception() + def test_create_recordset(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['zones']), + json={"zones": [zone]}), + self.get_designate_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['zones'], + qs_elements=['name={0}'.format(zone['id'])]), + json={"zones": [zone]}), + self.get_designate_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['zones', zone['id']]), + json=zone), + self.get_designate_discovery_mock_dict(), + dict(method='POST', + uri=self.get_mock_url( + 'dns', 'public', + append=['zones', zone['id'], 'recordsets']), + json=new_recordset, + validate=dict(json=recordset)), + ]) + rs = self.cloud.create_recordset( + zone=recordset_zone, + name=recordset['name'], + recordset_type=recordset['type'], + records=recordset['records'], + description=recordset['description'], + ttl=recordset['ttl']) + self.assertEqual(new_recordset, rs) + self.assert_calls() + + def test_create_recordset_exception(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['zones']), + json={"zones": [zone]}), + self.get_designate_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['zones'], + qs_elements=['name={0}'.format(zone['id'])]), + json={"zones": [zone]}), + self.get_designate_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['zones', zone['id']]), + json=zone), + self.get_designate_discovery_mock_dict(), + dict(method='POST', + uri=self.get_mock_url( + 'dns', 'public', + append=['zones', zone['id'], 'recordsets']), + status_code=500, + validate=dict(json={ + 'name': 'www2.example.net.', + 'records': ['192.168.1.2'], + 'type': 'A'})), + ]) with testtools.ExpectedException( shade.OpenStackCloudException, "Unable to create recordset www2.example.net." ): self.cloud.create_recordset('1', 'www2.example.net.', 'a', ['192.168.1.2']) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'designate_client') - def test_update_recordset(self, mock_designate): + def test_update_recordset(self): new_ttl = 7200 - mock_designate.zones.list.return_value = [zone_obj] - mock_designate.recordsets.list.return_value = [recordset_obj] - self.cloud.update_recordset('1', '1', ttl=new_ttl) - mock_designate.recordsets.update.assert_called_once_with( - zone='1', recordset='1', values={'ttl': new_ttl} - ) - - @mock.patch.object(shade.OpenStackCloud, 'designate_client') - def test_delete_recordset(self, mock_designate): - mock_designate.zones.list.return_value = [zone_obj] - mock_designate.recordsets.list.return_value = [recordset_obj] - self.cloud.delete_recordset('1', '1') - mock_designate.recordsets.delete.assert_called_once_with( - zone='1', recordset='1' - ) - - @mock.patch.object(shade.OpenStackCloud, 'designate_client') - def test_get_recordset_by_id(self, mock_designate): - mock_designate.recordsets.get.return_value = recordset_obj + expected_recordset = { + 'name': recordset['name'], + 'records': recordset['records'], + 'type': recordset['type'] + } + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['zones']), + json={"zones": [zone]}), + self.get_designate_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['zones'], + qs_elements=['name={0}'.format(zone['id'])]), + json={"zones": [zone]}), + self.get_designate_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['zones'], + qs_elements=['name={0}'.format(zone['id'])]), + json={"zones": [zone]}), + self.get_designate_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', + append=['zones', zone['id'], 'recordsets'], + qs_elements=['name={0}'.format( + new_recordset['id'])]), + json={"recordsets": [new_recordset]}), + self.get_designate_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', + append=['zones', zone['id'], + 'recordsets', new_recordset['id']]), + json={"recordsets": [new_recordset]}), + self.get_designate_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['zones'], + qs_elements=['name={0}'.format(zone['id'])]), + json={"zones": [zone]}), + self.get_designate_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['zones'], + qs_elements=['name={0}'.format(zone['id'])]), + json={"zones": [zone]}), + self.get_designate_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', + append=['zones', zone['id'], 'recordsets'], + qs_elements=['name={0}'.format( + new_recordset['id'])]), + json={"recordsets": [new_recordset]}), + self.get_designate_discovery_mock_dict(), + dict(method='PUT', + uri=self.get_mock_url( + 'dns', 'public', + append=['zones', zone['id'], + 'recordsets', new_recordset['id']]), + json=expected_recordset, + validate=dict(json={'ttl': new_ttl})) + ]) + updated_rs = self.cloud.update_recordset('1', '1', ttl=new_ttl) + self.assertEqual(expected_recordset, updated_rs) + self.assert_calls() + + def test_delete_recordset(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['zones']), + json={"zones": [zone]}), + self.get_designate_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['zones'], + qs_elements=['name={0}'.format(zone['id'])]), + json={"zones": [zone]}), + self.get_designate_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['zones'], + qs_elements=['name={0}'.format(zone['id'])]), + json={"zones": [zone]}), + self.get_designate_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', + append=['zones', zone['id'], 'recordsets'], + qs_elements=['name={0}'.format( + new_recordset['id'])]), + json={"recordsets": [new_recordset]}), + self.get_designate_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', + append=['zones', zone['id'], + 'recordsets', new_recordset['id']]), + json={"recordsets": [new_recordset]}), + self.get_designate_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['zones'], + qs_elements=['name={0}'.format(zone['id'])]), + json={"zones": [zone]}), + self.get_designate_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['zones'], + qs_elements=['name={0}'.format(zone['id'])]), + json={"zones": [zone]}), + self.get_designate_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', + append=['zones', zone['id'], 'recordsets'], + qs_elements=['name={0}'.format( + new_recordset['id'])]), + json={"recordsets": [new_recordset]}), + self.get_designate_discovery_mock_dict(), + dict(method='DELETE', + uri=self.get_mock_url( + 'dns', 'public', + append=['zones', zone['id'], + 'recordsets', new_recordset['id']]), + json={}) + ]) + self.assertTrue(self.cloud.delete_recordset('1', '1')) + self.assert_calls() + + def _prepare_get_recordset_calls(self, zone_id, name_or_id): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['zones'], + qs_elements=['name={0}'.format(zone['id'])]), + json={"zones": [zone]}), + self.get_designate_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['zones'], + qs_elements=['name={0}'.format(zone['id'])]), + json={"zones": [zone]}), + self.get_designate_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', + append=['zones', zone['id'], 'recordsets'], + qs_elements=['name={0}'.format(name_or_id)]), + json={"recordsets": [new_recordset]}), + self.get_designate_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', + append=['zones', zone['id'], + 'recordsets', new_recordset['id']]), + json=new_recordset), + ]) + + def test_get_recordset_by_id(self): + recordset = self._prepare_get_recordset_calls('1', '1') recordset = self.cloud.get_recordset('1', '1') - self.assertTrue(mock_designate.recordsets.get.called) self.assertEqual(recordset['id'], '1') + self.assert_calls() + + def test_get_recordset_by_name(self): + self._prepare_get_recordset_calls('1', new_recordset['name']) + recordset = self.cloud.get_recordset('1', new_recordset['name']) + self.assertEqual(new_recordset['name'], recordset['name']) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'designate_client') - def test_get_recordset_by_name(self, mock_designate): - mock_designate.recordsets.get.return_value = recordset_obj - recordset = self.cloud.get_recordset('1', 'www.example.net.') - self.assertTrue(mock_designate.recordsets.get.called) - self.assertEqual(recordset['name'], 'www.example.net.') - - @mock.patch.object(shade.OpenStackCloud, 'designate_client') - def test_get_recordset_not_found_returns_false(self, mock_designate): - mock_designate.recordsets.get.return_value = None - recordset = self.cloud.get_recordset('1', 'www.nonexistingrecord.net.') + def test_get_recordset_not_found_returns_false(self): + recordset_name = "www.nonexistingrecord.net." + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['zones'], + qs_elements=['name={0}'.format(zone['id'])]), + json={"zones": [zone]}), + self.get_designate_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['zones'], + qs_elements=['name={0}'.format(zone['id'])]), + json={"zones": [zone]}), + self.get_designate_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', + append=['zones', zone['id'], 'recordsets'], + qs_elements=['name={0}'.format(recordset_name)]), + json=[]) + ]) + recordset = self.cloud.get_recordset('1', recordset_name) self.assertFalse(recordset) + self.assert_calls() From 5991a5e72214a8c7bd12e7478f91f4aaa3a05d2f Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 16 May 2017 12:18:08 +0000 Subject: [PATCH 1538/3836] Updated from global requirements Change-Id: I034286a50f55350269777e13331cda536f9c8b4f --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index d1c79ca35..f319a83bd 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,7 +3,7 @@ # process, which may cause wedges in the gate later. hacking<0.12,>=0.11.0 # Apache-2.0 -coverage>=4.0 # Apache-2.0 +coverage!=4.4,>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD mock>=2.0 # BSD python-subunit>=0.0.18 # Apache-2.0/BSD From 8235e0ce6cff00bfa84501865a3cf48511cdf2ec Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 16 May 2017 18:12:41 -0500 Subject: [PATCH 1539/3836] Keep a singleton to support multiple get_config calls We are destructive to os.environ in the OpenStackConfig constructor- so it really should only ever be called once. Make sure get_config does this. Change-Id: I279bdf68408a807ec18fba634df3769c9b8fc4dc Closes-Bug: #1691294 --- os_client_config/__init__.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/os_client_config/__init__.py b/os_client_config/__init__.py index 1f1266ce1..424652541 100644 --- a/os_client_config/__init__.py +++ b/os_client_config/__init__.py @@ -21,6 +21,7 @@ __version__ = pbr.version.VersionInfo('os_client_config').version_string() +_config = None def get_config( @@ -28,16 +29,18 @@ def get_config( app_name=None, app_version=None, **kwargs): load_yaml_config = kwargs.pop('load_yaml_config', True) - config = OpenStackConfig( - load_yaml_config=load_yaml_config, - app_name=app_name, app_version=app_version) + global _config + if not _config: + _config = OpenStackConfig( + load_yaml_config=load_yaml_config, + app_name=app_name, app_version=app_version) if options: - config.register_argparse_arguments(options, sys.argv, service_key) + _config.register_argparse_arguments(options, sys.argv, service_key) parsed_options = options.parse_known_args(sys.argv) else: parsed_options = None - return config.get_one_cloud(options=parsed_options, **kwargs) + return _config.get_one_cloud(options=parsed_options, **kwargs) def make_rest_client( From 750e0a98032c27e2db7d3fbce75ddb1d613dad10 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 15 May 2017 18:00:50 -0500 Subject: [PATCH 1540/3836] Remove direct uses of nova_client in functional tests We use nova_client in a few places for setup/teardown - but we're solid enough at this point to just use ourselves for these functions. We're _pretty_ good at deleting servers consistently. Change-Id: Ifd12488752c036e4453c308a1aa6573c57d52944 --- shade/tests/functional/test_floating_ip.py | 22 +++++++------------ shade/tests/functional/test_inventory.py | 25 +++++++++++----------- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/shade/tests/functional/test_floating_ip.py b/shade/tests/functional/test_floating_ip.py index 9046a8ef4..b0679135b 100644 --- a/shade/tests/functional/test_floating_ip.py +++ b/shade/tests/functional/test_floating_ip.py @@ -21,7 +21,6 @@ import pprint -from novaclient import exceptions as nova_exc from testtools import content from shade import _utils @@ -36,8 +35,8 @@ class TestFloatingIP(base.BaseFunctionalTestCase): def setUp(self): super(TestFloatingIP, self).setUp() - self.nova = self.user_cloud.nova_client - self.flavor = pick_flavor(self.nova.flavors.list()) + self.flavor = pick_flavor( + self.user_cloud.list_flavors(get_extra=False)) if self.flavor is None: self.assertFalse('no sensible flavor available') self.image = self.pick_image() @@ -96,18 +95,13 @@ def _cleanup_servers(self): exception_list = list() # Delete stale servers as well as server created for this test - for i in self.nova.servers.list(): + for i in self.user_cloud.list_servers(bare=True): if i.name.startswith(self.new_item_name): - self.nova.servers.delete(i) - for _ in _utils._iterate_timeout( - self.timeout, "Timeout deleting servers"): - try: - self.nova.servers.get(server=i) - except nova_exc.NotFound: - break - except Exception as e: - exception_list.append(str(e)) - continue + try: + self.user_cloud.delete_server(i, wait=True) + except Exception as e: + exception_list.append(str(e)) + continue if exception_list: # Raise an error: we must make users aware that something went diff --git a/shade/tests/functional/test_inventory.py b/shade/tests/functional/test_inventory.py index 2fd0e38e0..ce98625cf 100644 --- a/shade/tests/functional/test_inventory.py +++ b/shade/tests/functional/test_inventory.py @@ -31,21 +31,20 @@ def setUp(self): # This needs to use an admin account, otherwise a public IP # is not allocated from devstack. self.inventory = inventory.OpenStackInventory() - self.server_name = 'test_inventory_server' - self.nova = self.operator_cloud.nova_client - self.flavor = pick_flavor(self.nova.flavors.list()) + self.server_name = self.getUniqueString('inventory') + self.flavor = pick_flavor( + self.user_cloud.list_flavors(get_extra=False)) if self.flavor is None: self.assertTrue(False, 'no sensible flavor available') self.image = self.pick_image() - self.addCleanup(self._cleanup_servers) - self.operator_cloud.create_server( + self.addCleanup(self._cleanup_server) + server = self.operator_cloud.create_server( name=self.server_name, image=self.image, flavor=self.flavor, wait=True, auto_ip=True) + self.server_id = server['id'] - def _cleanup_servers(self): - for i in self.nova.servers.list(): - if i.name.startswith(self.server_name): - self.nova.servers.delete(i) + def _cleanup_server(self): + self.user_cloud.delete_server(self.server_id, wait=True) def _test_host_content(self, host): self.assertEqual(host['image']['id'], self.image.id) @@ -62,20 +61,20 @@ def _test_expanded_host_content(self, host): self.assertEqual(host['flavor']['name'], self.flavor.name) def test_get_host(self): - host = self.inventory.get_host(self.server_name) + host = self.inventory.get_host(self.server_id) self.assertIsNotNone(host) self.assertEqual(host['name'], self.server_name) self._test_host_content(host) self._test_expanded_host_content(host) host_found = False for host in self.inventory.list_hosts(): - if host['name'] == self.server_name: + if host['id'] == self.server_id: host_found = True self._test_host_content(host) self.assertTrue(host_found) def test_get_host_no_detail(self): - host = self.inventory.get_host(self.server_name, expand=False) + host = self.inventory.get_host(self.server_id, expand=False) self.assertIsNotNone(host) self.assertEqual(host['name'], self.server_name) @@ -88,7 +87,7 @@ def test_get_host_no_detail(self): host_found = False for host in self.inventory.list_hosts(expand=False): - if host['name'] == self.server_name: + if host['id'] == self.server_id: host_found = True self._test_host_content(host) self.assertTrue(host_found) From c19a797066348f44b8f478c60c28842083cfcc7f Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 17 May 2017 03:45:54 +0000 Subject: [PATCH 1541/3836] Updated from global requirements Change-Id: I245f8b4e10a57d133443683ba115274ce2332bfd --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index f319a83bd..2241f2af7 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,7 +9,7 @@ mock>=2.0 # BSD python-subunit>=0.0.18 # Apache-2.0/BSD oslosphinx>=4.7.0 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 -sphinx>=1.5.1 # BSD +sphinx!=1.6.1,>=1.5.1 # BSD testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=1.4.0 # MIT From b9872da5f0bc95ed0b0aa196517e28ebe0fb3842 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 17 May 2017 12:02:31 -0500 Subject: [PATCH 1542/3836] Make sure security_groups is always a list In https://github.com/ansible/ansible/issues/24675 we see that sometimes security_groups is coming back None. The data model in shade says it's a list, so make sure it is one. Change-Id: I6db45ba7c3d630812ea58353d3eaddbb98cb8c1f --- shade/_normalize.py | 4 +++- shade/meta.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/shade/_normalize.py b/shade/_normalize.py index e72378b7c..670944c19 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -39,7 +39,6 @@ 'private_v4', 'public_v4', 'public_v6', - 'security_groups', 'status', 'updated', 'user_id', @@ -441,6 +440,9 @@ def _normalize_server(self, server): short_key = key.split(':')[1] ret[short_key] = _pop_or_get(server, key, None, self.strict_mode) + # Protect against security_groups being None + ret['security_groups'] = server.pop('security_groups', None) or [] + for field in _SERVER_FIELDS: ret[field] = server.pop(field, None) ret['interface_ip'] = '' diff --git a/shade/meta.py b/shade/meta.py index 87d4884ff..25b65fd02 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -407,7 +407,7 @@ def expand_server_security_groups(cloud, server): groups = cloud.list_server_security_groups(server) except exc.OpenStackCloudException: groups = [] - server['security_groups'] = groups + server['security_groups'] = groups or [] def get_hostvars_from_server(cloud, server, mounts=None): From 66e7a7faf20a3c03aada4768446f053e541dad7e Mon Sep 17 00:00:00 2001 From: "Yuanbin.Chen" Date: Thu, 18 May 2017 22:21:17 +0800 Subject: [PATCH 1543/3836] Add compute support server migrate operation Implements: blueprint add-compute-migrate Change-Id: I429b7439d933715c10b56a2b686e401969c1602f Signed-off-by: Yuanbin.Chen --- doc/source/users/proxies/compute.rst | 1 + openstack/compute/v2/_proxy.py | 11 +++++++++++ openstack/compute/v2/server.py | 4 ++++ openstack/tests/unit/compute/v2/test_server.py | 12 ++++++++++++ 4 files changed, 28 insertions(+) diff --git a/doc/source/users/proxies/compute.rst b/doc/source/users/proxies/compute.rst index 5a5f25364..7c6573816 100644 --- a/doc/source/users/proxies/compute.rst +++ b/doc/source/users/proxies/compute.rst @@ -61,6 +61,7 @@ Starting, Stopping, etc. .. automethod:: openstack.compute.v2._proxy.Proxy.rescue_server .. automethod:: openstack.compute.v2._proxy.Proxy.unrescue_server .. automethod:: openstack.compute.v2._proxy.Proxy.evacuate_server + .. automethod:: openstack.compute.v2._proxy.Proxy.migrate_server Modifying a Server ****************** diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 6d342d91a..dbdfa6822 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -1244,3 +1244,14 @@ def volume_attachments(self, server): server_id = resource2.Resource._get_id(server) return self._list(_volume_attachment.VolumeAttachment, paginated=False, server_id=server_id) + + def migrate_server(self, server): + """Migrate a server from one host to another + + :param server: Either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance. + :returns: None + """ + + server = self._get_resource(_server.Server, server) + server.migrate(self._session) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 75c89a364..6618a0939 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -336,6 +336,10 @@ def unshelve(self, session): body = {"unshelve": None} self._action(session, body) + def migrate(self, session): + body = {"migrate": None} + self._action(session, body) + class ServerDetail(Server): base_path = '/servers/detail' diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 0c5109e36..0e60d050d 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -606,3 +606,15 @@ def test_unshelve(self): headers = {'Accept': ''} self.sess.post.assert_called_with( url, endpoint_filter=sot.service, json=body, headers=headers) + + def test_migrate(self): + sot = server.Server(**EXAMPLE) + + res = sot.migrate(self.sess) + + self.assertIsNone(res) + url = 'servers/IDENTIFIER/action' + body = {"migrate": None} + headers = {'Accept': ''} + self.sess.post.assert_called_with( + url, endpoint_filter=sot.service, json=body, headers=headers) From ee0112182a8e4a0c01546d472bb097f85ea2d8e3 Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Thu, 18 May 2017 15:14:49 +0000 Subject: [PATCH 1544/3836] Fix issue with list_volumes when pagination is used Change-Id: Iecf478d8c6352744bd58cfa437d34caf0f47021b Signed-off-by: Rosario Di Somma Fixes: bug 2001022 --- shade/_normalize.py | 5 +++++ shade/tests/unit/test_volume.py | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/shade/_normalize.py b/shade/_normalize.py index 670944c19..fcd973ff9 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -679,6 +679,11 @@ def _normalize_volumes(self, volumes): :returns: A list of normalized dicts. """ ret = [] + # With pagination we don't get a top-level container + # so we need to explicitly extract the list of volumes + if isinstance(volumes, dict): + volumes = volumes.get('volumes', []) + for volume in volumes: ret.append(self._normalize_volume(volume)) return ret diff --git a/shade/tests/unit/test_volume.py b/shade/tests/unit/test_volume.py index 6cb7cb104..78dbfb06b 100644 --- a/shade/tests/unit/test_volume.py +++ b/shade/tests/unit/test_volume.py @@ -278,3 +278,21 @@ def test_delete_volume_gone_away(self): status_code=404)]) self.assertFalse(self.cloud.delete_volume(volume['id'])) self.assert_calls() + + def test_list_volumes_with_pagination(self): + vol1 = meta.obj_to_dict(fakes.FakeVolume('01', 'available', 'vol1')) + vol2 = meta.obj_to_dict(fakes.FakeVolume('02', 'available', 'vol2')) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', 'detail']), + json={ + 'volumes': [vol1, vol2], + 'volumes_links': [ + {'href': 'https://volume.example.com/fake_url', + 'rel': 'next'}]})]) + self.assertEqual( + [self.cloud._normalize_volume(vol1), + self.cloud._normalize_volume(vol2)], + self.cloud.list_volumes()) + self.assert_calls() From 329b939241a218e09e03cb41ad4ac8f3bec3c2f4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 19 May 2017 09:44:11 -0500 Subject: [PATCH 1545/3836] Add ability to run any tox env in python3 To test this I ran the pep8 tox env with python3 set - and lo and behold there was a python3 mistake in a functional test. Whoops. Change-Id: Ia93a2a85e420d164d67abee479ad498b5e6d3167 --- shade/tests/functional/test_router.py | 15 ++++++++------- tox.ini | 1 + 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/shade/tests/functional/test_router.py b/shade/tests/functional/test_router.py index 6572c12ba..5c4ccde46 100644 --- a/shade/tests/functional/test_router.py +++ b/shade/tests/functional/test_router.py @@ -141,6 +141,7 @@ def test_create_router_project(self): def _create_and_verify_advanced_router(self, external_cidr, external_gateway_ip=None): + # external_cidr must be passed in as unicode (u'') # NOTE(Shrews): The arguments are needed because these tests # will run in parallel and we want to make sure that each test # is using different resources to prevent race conditions. @@ -153,7 +154,7 @@ def _create_and_verify_advanced_router(self, gateway_ip=external_gateway_ip ) - ip_net = ipaddress.IPv4Network(unicode(external_cidr)) + ip_net = ipaddress.IPv4Network(external_cidr) last_ip = str(list(ip_net.hosts())[-1]) router_name = self.router_prefix + '_create_advanced' @@ -191,11 +192,11 @@ def _create_and_verify_advanced_router(self, return router def test_create_router_advanced(self): - self._create_and_verify_advanced_router(external_cidr='10.2.2.0/24') + self._create_and_verify_advanced_router(external_cidr=u'10.2.2.0/24') def test_add_remove_router_interface(self): router = self._create_and_verify_advanced_router( - external_cidr='10.3.3.0/24') + external_cidr=u'10.3.3.0/24') net_name = self.network_prefix + '_intnet1' sub_name = self.subnet_prefix + '_intsub1' net = self.operator_cloud.create_network(name=net_name) @@ -221,7 +222,7 @@ def test_add_remove_router_interface(self): def test_list_router_interfaces(self): router = self._create_and_verify_advanced_router( - external_cidr='10.5.5.0/24') + external_cidr=u'10.5.5.0/24') net_name = self.network_prefix + '_intnet1' sub_name = self.subnet_prefix + '_intsub1' net = self.operator_cloud.create_network(name=net_name) @@ -256,7 +257,7 @@ def test_list_router_interfaces(self): def test_update_router_name(self): router = self._create_and_verify_advanced_router( - external_cidr='10.7.7.0/24') + external_cidr=u'10.7.7.0/24') new_name = self.router_prefix + '_update_name' updated = self.operator_cloud.update_router( @@ -277,7 +278,7 @@ def test_update_router_name(self): def test_update_router_admin_state(self): router = self._create_and_verify_advanced_router( - external_cidr='10.8.8.0/24') + external_cidr=u'10.8.8.0/24') updated = self.operator_cloud.update_router( router['id'], admin_state_up=True) @@ -299,7 +300,7 @@ def test_update_router_admin_state(self): def test_update_router_ext_gw_info(self): router = self._create_and_verify_advanced_router( - external_cidr='10.9.9.0/24') + external_cidr=u'10.9.9.0/24') # create a new subnet existing_net_id = router['external_gateway_info']['network_id'] diff --git a/tox.ini b/tox.ini index 8db2e9b7b..9bb35bbe1 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ skipsdist = True [testenv] usedevelop = True +basepython = {env:SHADE_TOX_PYTHON:python2} install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} setenv = VIRTUAL_ENV={envdir} From a87fde8ea7da4313a345445ec9c0c5cf9fd67444 Mon Sep 17 00:00:00 2001 From: Akihiro Motoki Date: Fri, 28 Apr 2017 23:07:17 +0000 Subject: [PATCH 1546/3836] Network tag support * set_tags() operation for network, subnet, port, subnetpool and router resource. Tag support is implemented as a mixin class as tag support for more resources is being planned. * Tag operation in the network proxy class * Tag related query parameters Tag support in neutron follows API-WG guideline. https://specs.openstack.org/openstack/api-wg/guidelines/tags.html In the API, four operations are defined: replace tags, add a tag, remove a tag, remove all tags, but we can do all operations by using only 'replace tags'. In addition, updating attributes of most network resources is an operation to replace an existing value to a new one, so I believe this applies to 'tags' attribute. Required for blueprint neutron-client-tag Needed-By: Iad59d052f46896d27d73c22d6d4bb3df889f2352 Change-Id: Ibaea97010d152f5491bb9d71b3f9b777ea7019dc --- doc/source/users/proxies/network.rst | 7 +++ openstack/network/v2/_proxy.py | 25 +++++++++++ openstack/network/v2/network.py | 7 ++- openstack/network/v2/port.py | 7 ++- openstack/network/v2/router.py | 7 ++- openstack/network/v2/subnet.py | 7 ++- openstack/network/v2/subnet_pool.py | 7 ++- openstack/network/v2/tag.py | 30 +++++++++++++ .../tests/unit/network/v2/test_network.py | 6 ++- openstack/tests/unit/network/v2/test_proxy.py | 17 +++++++ openstack/tests/unit/network/v2/test_tag.py | 44 +++++++++++++++++++ 11 files changed, 158 insertions(+), 6 deletions(-) create mode 100644 openstack/network/v2/tag.py create mode 100644 openstack/tests/unit/network/v2/test_tag.py diff --git a/doc/source/users/proxies/network.rst b/doc/source/users/proxies/network.rst index 690e87552..290e20105 100644 --- a/doc/source/users/proxies/network.rst +++ b/doc/source/users/proxies/network.rst @@ -332,6 +332,13 @@ Service Profile Operations .. automethod:: openstack.network.v2._proxy.Proxy.associate_flavor_with_service_profile .. automethod:: openstack.network.v2._proxy.Proxy.disassociate_flavor_from_service_profile +Tag Operations +^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + + .. automethod:: openstack.network.v2._proxy.Proxy.set_tags + VPN Operations ^^^^^^^^^^^^^^ diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index d0d3a936a..4ee8559aa 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import exceptions from openstack.network.v2 import address_scope as _address_scope from openstack.network.v2 import agent as _agent from openstack.network.v2 import auto_allocated_topology as \ @@ -2927,6 +2928,30 @@ def update_subnet_pool(self, subnet_pool, **attrs): """ return self._update(_subnet_pool.SubnetPool, subnet_pool, **attrs) + @staticmethod + def _check_tag_support(resource): + try: + # Check 'tags' attribute exists + resource.tags + except AttributeError: + raise exceptions.InvalidRequest( + '%s resource does not support tag' % + resource.__class__.__name__) + + def set_tags(self, resource, tags): + """Replace tags of a specified resource with specified tags + + :param resource: + :class:`~openstack.resource2.Resource` instance. + :param tags: New tags to be set. + :type tags: "list" + + :returns: The updated resource + :rtype: :class:`~openstack.resource2.Resource` + """ + self._check_tag_support(resource) + return resource.set_tags(self._session, tags) + def create_vpn_service(self, **attrs): """Create a new vpn service from attributes diff --git a/openstack/network/v2/network.py b/openstack/network/v2/network.py index 33a86e9a3..cbdd4518e 100644 --- a/openstack/network/v2/network.py +++ b/openstack/network/v2/network.py @@ -11,10 +11,11 @@ # under the License. from openstack.network import network_service +from openstack.network.v2 import tag from openstack import resource2 as resource -class Network(resource.Resource): +class Network(resource.Resource, tag.TagMixin): resource_key = 'network' resources_key = 'networks' base_path = '/networks' @@ -39,6 +40,7 @@ class Network(resource.Resource): provider_network_type='provider:network_type', provider_physical_network='provider:physical_network', provider_segmentation_id='provider:segmentation_id', + **tag.TagMixin._tag_query_parameters ) # Properties @@ -111,6 +113,9 @@ class Network(resource.Resource): updated_at = resource.Body('updated_at') #: Indicates the VLAN transparency mode of the network is_vlan_transparent = resource.Body('vlan_transparent', type=bool) + #: A list of assocaited tags + #: *Type: list of tag strings* + tags = resource.Body('tags', type=list) class DHCPAgentHostingNetwork(Network): diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index e98de3747..b6f94665d 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -11,10 +11,11 @@ # under the License. from openstack.network import network_service +from openstack.network.v2 import tag from openstack import resource2 as resource -class Port(resource.Resource): +class Port(resource.Resource, tag.TagMixin): resource_key = 'port' resources_key = 'ports' base_path = '/ports' @@ -34,6 +35,7 @@ class Port(resource.Resource): is_admin_state_up='admin_state_up', is_port_security_enabled='port_security_enabled', project_id='tenant_id', + **tag.TagMixin._tag_query_parameters ) # Properties @@ -127,3 +129,6 @@ class Port(resource.Resource): trunk_details = resource.Body('trunk_details', type=dict) #: Timestamp when the port was last updated. updated_at = resource.Body('updated_at') + #: A list of assocaited tags + #: *Type: list of tag strings* + tags = resource.Body('tags', type=list) diff --git a/openstack/network/v2/router.py b/openstack/network/v2/router.py index 3c21b113d..7fa58dbaf 100644 --- a/openstack/network/v2/router.py +++ b/openstack/network/v2/router.py @@ -11,11 +11,12 @@ # under the License. from openstack.network import network_service +from openstack.network.v2 import tag from openstack import resource2 as resource from openstack import utils -class Router(resource.Resource): +class Router(resource.Resource, tag.TagMixin): resource_key = 'router' resources_key = 'routers' base_path = '/routers' @@ -35,6 +36,7 @@ class Router(resource.Resource): is_distributed='distributed', is_ha='ha', project_id='tenant_id', + **tag.TagMixin._tag_query_parameters ) # Properties @@ -74,6 +76,9 @@ class Router(resource.Resource): status = resource.Body('status') #: Timestamp when the router was created. updated_at = resource.Body('updated_at') + #: A list of assocaited tags + #: *Type: list of tag strings* + tags = resource.Body('tags', type=list) def add_interface(self, session, **body): """Add an internal interface to a logical router. diff --git a/openstack/network/v2/subnet.py b/openstack/network/v2/subnet.py index 1466017b7..a9e95ad59 100644 --- a/openstack/network/v2/subnet.py +++ b/openstack/network/v2/subnet.py @@ -11,10 +11,11 @@ # under the License. from openstack.network import network_service +from openstack.network.v2 import tag from openstack import resource2 as resource -class Subnet(resource.Resource): +class Subnet(resource.Resource, tag.TagMixin): resource_key = 'subnet' resources_key = 'subnets' base_path = '/subnets' @@ -36,6 +37,7 @@ class Subnet(resource.Resource): project_id='tenant_id', subnet_pool_id='subnetpool_id', use_default_subnet_pool='use_default_subnetpool', + **tag.TagMixin._tag_query_parameters ) # Properties @@ -87,3 +89,6 @@ class Subnet(resource.Resource): 'use_default_subnetpool', type=bool ) + #: A list of assocaited tags + #: *Type: list of tag strings* + tags = resource.Body('tags', type=list) diff --git a/openstack/network/v2/subnet_pool.py b/openstack/network/v2/subnet_pool.py index 22b9f60a6..86a43590a 100644 --- a/openstack/network/v2/subnet_pool.py +++ b/openstack/network/v2/subnet_pool.py @@ -11,10 +11,11 @@ # under the License. from openstack.network import network_service +from openstack.network.v2 import tag from openstack import resource2 as resource -class SubnetPool(resource.Resource): +class SubnetPool(resource.Resource, tag.TagMixin): resource_key = 'subnetpool' resources_key = 'subnetpools' base_path = '/subnetpools' @@ -32,6 +33,7 @@ class SubnetPool(resource.Resource): 'name', is_shared='shared', project_id='tenant_id', + **tag.TagMixin._tag_query_parameters ) # Properties @@ -77,3 +79,6 @@ class SubnetPool(resource.Resource): revision_number = resource.Body('revision_number', type=int) #: Timestamp when the subnet pool was last updated. updated_at = resource.Body('updated_at') + #: A list of assocaited tags + #: *Type: list of tag strings* + tags = resource.Body('tags', type=list) diff --git a/openstack/network/v2/tag.py b/openstack/network/v2/tag.py new file mode 100644 index 000000000..b216e2ebc --- /dev/null +++ b/openstack/network/v2/tag.py @@ -0,0 +1,30 @@ +# 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 utils + + +class TagMixin(object): + + _tag_query_parameters = { + 'tags': 'tags', + 'any_tags': 'tags-any', + 'not_tags': 'not-tags', + 'not_any_tags': 'not-tags-any', + } + + def set_tags(self, session, tags): + url = utils.urljoin(self.base_path, self.id, 'tags') + session.put(url, endpoint_filter=self.service, + json={'tags': tags}) + self._body.attributes.update({'tags': tags}) + return self diff --git a/openstack/tests/unit/network/v2/test_network.py b/openstack/tests/unit/network/v2/test_network.py index fe90b0566..f8fc88b6b 100644 --- a/openstack/tests/unit/network/v2/test_network.py +++ b/openstack/tests/unit/network/v2/test_network.py @@ -110,7 +110,11 @@ def test_make_it(self): 'is_shared': 'shared', 'provider_network_type': 'provider:network_type', 'provider_physical_network': 'provider:physical_network', - 'provider_segmentation_id': 'provider:segmentation_id' + 'provider_segmentation_id': 'provider:segmentation_id', + 'tags': 'tags', + 'any_tags': 'tags-any', + 'not_tags': 'not-tags', + 'not_any_tags': 'not-tags-any', }, sot._query_mapping._mapping) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index ff11c8caf..06232cd23 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -14,6 +14,7 @@ import mock import uuid +from openstack import exceptions from openstack.network.v2 import _proxy from openstack.network.v2 import address_scope from openstack.network.v2 import agent @@ -1064,3 +1065,19 @@ def test_validate_topology(self): auto_allocated_topology.ValidateTopology], expected_kwargs={"project": mock.sentinel.project_id, "requires_id": False}) + + def test_set_tags(self): + x_network = network.Network.new(id='NETWORK_ID') + self._verify('openstack.network.v2.network.Network.set_tags', + self.proxy.set_tags, + method_args=[x_network, ['TAG1', 'TAG2']], + expected_args=[['TAG1', 'TAG2']], + expected_result=mock.sentinel.result_set_tags) + + @mock.patch('openstack.network.v2.network.Network.set_tags') + def test_set_tags_resource_without_tag_suport(self, mock_set_tags): + no_tag_resource = object() + self.assertRaises(exceptions.InvalidRequest, + self.proxy.set_tags, + no_tag_resource, ['TAG1', 'TAG2']) + self.assertEqual(0, mock_set_tags.call_count) diff --git a/openstack/tests/unit/network/v2/test_tag.py b/openstack/tests/unit/network/v2/test_tag.py new file mode 100644 index 000000000..b22ae87dc --- /dev/null +++ b/openstack/tests/unit/network/v2/test_tag.py @@ -0,0 +1,44 @@ +# 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 testtools + +from openstack.network.v2 import network + + +ID = 'IDENTIFIER' + + +class TestTag(testtools.TestCase): + + @staticmethod + def _create_resource(tags=None): + tags = tags or [] + return network.Network(id=ID, name='test-net', tags=tags) + + def test_tags_attribute(self): + net = self._create_resource() + self.assertTrue(hasattr(net, 'tags')) + self.assertIsInstance(net.tags, list) + + def test_set_tags(self): + net = self._create_resource() + sess = mock.Mock() + result = net.set_tags(sess, ['blue', 'green']) + # Check tags attribute is updated + self.assertEqual(['blue', 'green'], net.tags) + # Check the passed resource is returned + self.assertEqual(net, result) + url = 'networks/' + ID + '/tags' + sess.put.assert_called_once_with(url, endpoint_filter=net.service, + json={'tags': ['blue', 'green']}) From e3d43e7396d95d6cfaa4ca0113108cbef22225b6 Mon Sep 17 00:00:00 2001 From: Clark Boylan Date: Mon, 22 May 2017 12:10:24 -0700 Subject: [PATCH 1547/3836] Find private ip addr based on fip attachment In the case of multiple private ip addresses we previous returned the first private ip address found. That can cause problems if that IP address isn't valid. Since the IPs are private the only real validity check we can use is if the private IP address has a floating IP attaached. Update shade to return the private IP that is attached to a floating IP when there are multiple private IPs. Change-Id: I86807c5149dfad72948ff6832a6697b594d1f195 --- shade/meta.py | 40 +++++++++++++++++++++++++++++------ shade/tests/unit/test_meta.py | 35 ++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/shade/meta.py b/shade/meta.py index 25b65fd02..1a00be543 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -25,8 +25,8 @@ NON_CALLABLES = (six.string_types, bool, dict, int, float, list, type(None)) -def find_nova_addresses(addresses, ext_tag=None, key_name=None, version=4): - +def find_nova_interfaces(addresses, ext_tag=None, key_name=None, version=4, + mac_addr=None): ret = [] for (k, v) in iter(addresses.items()): if key_name is not None and k != key_name: @@ -48,12 +48,32 @@ def find_nova_addresses(addresses, ext_tag=None, key_name=None, version=4): # Type doesn't match, continue with next one continue - if interface_spec['version'] == version: - ret.append(interface_spec['addr']) + if mac_addr is not None: + if 'OS-EXT-IPS-MAC:mac_addr' not in interface_spec: + # mac_addr is specified, but this interface has no mac_addr + # We could actually return right away as this means that + # this cloud doesn't support OS-EXT-IPS-MAC. Nevertheless, + # it would be better to perform an explicit check. e.g.: + # cloud._has_nova_extension('OS-EXT-IPS-MAC') + # But this needs cloud to be passed to this function. + continue + elif interface_spec['OS-EXT-IPS-MAC:mac_addr'] != mac_addr: + # MAC doesn't match, continue with next one + continue + if interface_spec['version'] == version: + ret.append(interface_spec) return ret +def find_nova_addresses(addresses, ext_tag=None, key_name=None, version=4, + mac_addr=None): + interfaces = find_nova_interfaces(addresses, ext_tag, key_name, version, + mac_addr) + addrs = [i['addr'] for i in interfaces] + return addrs + + def get_server_ip(server, public=False, cloud_public=True, **kwargs): """Get an IP from the Nova addresses dict @@ -89,6 +109,13 @@ def get_server_private_ip(server, cloud=None): if cloud and not cloud.use_internal_network(): return None + # Try to get a floating IP interface. If we have one then return the + # private IP address associated with that floating IP for consistency. + fip_ints = find_nova_interfaces(server['addresses'], ext_tag='floating') + fip_mac = None + if fip_ints: + fip_mac = fip_ints[0].get('OS-EXT-IPS-MAC:mac_addr') + # Short circuit the ports/networks search below with a heavily cached # and possibly pre-configured network name if cloud: @@ -96,12 +123,13 @@ def get_server_private_ip(server, cloud=None): for int_net in int_nets: int_ip = get_server_ip( server, key_name=int_net['name'], - cloud_public=not cloud.private) + cloud_public=not cloud.private, + mac_addr=fip_mac) if int_ip is not None: return int_ip ip = get_server_ip( - server, ext_tag='fixed', key_name='private') + server, ext_tag='fixed', key_name='private', mac_addr=fip_mac) if ip: return ip diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 2f12fd11c..4ea2da865 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -300,6 +300,41 @@ def test_get_server_private_ip(self): PRIVATE_V4, meta.get_server_private_ip(srv, self.cloud)) self.assert_calls() + def test_get_server_multiple_private_ip(self): + self.register_uris([ + dict(method='GET', + uri='https://network.example.com/v2.0/networks.json', + json={'networks': [{ + 'id': 'test-net-id', + 'name': 'test-net'}]} + ), + dict(method='GET', + uri='https://network.example.com/v2.0/subnets.json', + json={'subnets': SUBNETS_WITH_NAT}) + ]) + + shared_mac = '11:22:33:44:55:66' + distinct_mac = '66:55:44:33:22:11' + srv = meta.obj_to_dict(fakes.FakeServer( + id='test-id', name='test-name', status='ACTIVE', + addresses={'test-net': [{'OS-EXT-IPS:type': 'fixed', + 'OS-EXT-IPS-MAC:mac_addr': distinct_mac, + 'addr': '10.0.0.100', + 'version': 4}, + {'OS-EXT-IPS:type': 'fixed', + 'OS-EXT-IPS-MAC:mac_addr': shared_mac, + 'addr': '10.0.0.101', + 'version': 4}], + 'public': [{'OS-EXT-IPS:type': 'floating', + 'OS-EXT-IPS-MAC:mac_addr': shared_mac, + 'addr': PUBLIC_V4, + 'version': 4}]} + )) + + self.assertEqual( + '10.0.0.101', meta.get_server_private_ip(srv, self.cloud)) + self.assert_calls() + @mock.patch.object(shade.OpenStackCloud, 'has_service') @mock.patch.object(shade.OpenStackCloud, 'get_volumes') @mock.patch.object(shade.OpenStackCloud, 'get_image_name') From 1cfc1a61dfe9c77c4f4805cc050fef8bd7f10536 Mon Sep 17 00:00:00 2001 From: chohoor Date: Tue, 23 May 2017 16:46:35 +0800 Subject: [PATCH 1548/3836] update params about cluster filter event This path replaced filter params about cluster filter event to api specification name rather than db table field name. at the same time, it not affect existing api that we use. Change-Id: I9527c3c17ace7c49057dbc645a769a4dc3f14cb8 --- openstack/cluster/v1/event.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openstack/cluster/v1/event.py b/openstack/cluster/v1/event.py index 008dc4794..d9b51fbb3 100644 --- a/openstack/cluster/v1/event.py +++ b/openstack/cluster/v1/event.py @@ -26,8 +26,9 @@ class Event(resource.Resource): allow_get = True _query_mapping = resource.QueryParameters( - 'oname', 'otype', 'oid', 'cluster_id', 'action', 'level', - 'sort', 'global_project') + 'cluster_id', 'action', 'level', 'sort', 'global_project', + obj_id='oid', obj_name='oname', obj_type='otype', + ) # Properties #: Timestamp string (in ISO8601 format) when the event was generated. From 8d34399714297d5eaa12ae7428bf93392e2b4108 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 23 May 2017 07:55:38 -0500 Subject: [PATCH 1549/3836] Fix get_compute_usage normalization problem When we normalize an empty usage report, start and end can be empty, so the pop() call fails. Don't fail on that. Also, stop forcing interface=admin in operator_cloud. We stopped adding this elsewhere already, because it turns out it's NOT actually a thing anyone should ever do by default. (It's an old keystone v2 thing - jamielennox says it should be avoided like the plague) Make operator_cloud consistent. (This will still honor interface=admin if it's in the clouds.yaml file) Change-Id: I432dc58d60fb56ce13332443435138354d66e9e1 --- shade/__init__.py | 2 -- shade/_normalize.py | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/shade/__init__.py b/shade/__init__.py index 615987fee..c2f69b711 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -108,8 +108,6 @@ def openstack_cloud( def operator_cloud( config=None, strict=False, app_name=None, app_version=None, **kwargs): - if 'interface' not in kwargs: - kwargs['interface'] = 'admin' if not config: config = _get_openstack_config(app_name, app_version) try: diff --git a/shade/_normalize.py b/shade/_normalize.py index fcd973ff9..32f156372 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -821,8 +821,8 @@ def _normalize_compute_usage(self, usage): 'total_server_groups_used', 'total_vcpus_usage'): ret[key] = usage.pop(key, 0) - ret['started_at'] = usage.pop('start') - ret['stopped_at'] = usage.pop('stop') + ret['started_at'] = usage.pop('start', None) + ret['stopped_at'] = usage.pop('stop', None) ret['server_usages'] = self._normalize_server_usages( usage.pop('server_usages', [])) ret['properties'] = usage From 12b07e12e2f6f43eb7dd9230d25acff2c6ac8343 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 23 May 2017 08:13:19 -0500 Subject: [PATCH 1550/3836] Fix get_compute_limits error message We're currently using name_or_id in the error message whether it's there or not. Change-Id: Ieaddc57e52f7c26b410ebe0178e2c30f48897769 --- shade/openstackcloud.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 7ad03662a..7d7be4d31 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1888,6 +1888,7 @@ def get_compute_limits(self, name_or_id=None): """ kwargs = {} project_id = None + error_msg = "Failed to get limits" if name_or_id: proj = self.get_project(name_or_id) @@ -1895,10 +1896,10 @@ def get_compute_limits(self, name_or_id=None): raise OpenStackCloudException("project does not exist") project_id = proj.id kwargs['tenant_id'] = project_id + error_msg = "{msg} for the project: {project} ".format( + msg=error_msg, project=name_or_id) - with _utils.shade_exceptions( - "Failed to get limits for the project: {} ".format( - name_or_id)): + with _utils.shade_exceptions(error_msg): # TODO(mordred) Before we convert this to REST, we need to add # in support for running calls with a different project context limits = self.manager.submit_task(_tasks.NovaLimitsGet(**kwargs)) From 072001a6289bee6594a97fe30dd5ab53bb1d9e99 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 23 May 2017 09:04:25 -0500 Subject: [PATCH 1551/3836] Allow a user to submit start and end time as strings get_compute_usage currently requires a user to provide a datetime object. That's actually fairly unfriendly. Also, it turns out if you happen to produce one of those with timezone offset information, Nova will return a 400 error. It's pretty easy to convert a datetime with timezone info to a datetime in UTC without it - so do that first. Now a user can submit a fully compliant ISO 8601 datetime with whatever info they want and we'll make it meet nova's standards. They can also supply a datetime if they prefer. Also - do date parsing before project get - to avoid an API call if the validation that doesn't need an API call fails. Finally, supply a default for 'start' of the first day OpenStack existed. Change-Id: I53989d47f0d24695c19c1023e35acb315bec9eea --- ...mpute-usage-defaults-5f5b2936f17ff400.yaml | 9 ++++ requirements.txt | 1 + shade/operatorcloud.py | 49 +++++++++++++++++-- 3 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/compute-usage-defaults-5f5b2936f17ff400.yaml diff --git a/releasenotes/notes/compute-usage-defaults-5f5b2936f17ff400.yaml b/releasenotes/notes/compute-usage-defaults-5f5b2936f17ff400.yaml new file mode 100644 index 000000000..7ca6b37f5 --- /dev/null +++ b/releasenotes/notes/compute-usage-defaults-5f5b2936f17ff400.yaml @@ -0,0 +1,9 @@ +--- +features: + - get_compute_usage now has a default value for the start + parameter of 2010-07-06. That was the date the OpenStack + project started. It's completely impossible for someone + to have Nova usage data that goes back further in time. + Also, both the start and end date parameters now also + accept strings which will be parsed and timezones will + be properly converted to UTC which is what Nova expects. diff --git a/requirements.txt b/requirements.txt index 6bc05760b..8472bc6d7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,6 +19,7 @@ requests!=2.12.2,!=2.13.0,>=2.10.0 # Apache-2.0 requestsexceptions>=1.2.0 # Apache-2.0 six>=1.9.0 # MIT futures>=3.0;python_version=='2.7' or python_version=='2.6' # BSD +iso8601>=0.1.11 # MIT keystoneauth1>=2.20.0 # Apache-2.0 netifaces>=0.10.4 # MIT diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index ded7bc909..0a109533c 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -11,6 +11,7 @@ # limitations under the License. import datetime +import iso8601 import jsonpatch from ironicclient import exceptions as ironic_exceptions @@ -2103,22 +2104,60 @@ def delete_compute_quotas(self, name_or_id): except nova_exceptions.BadRequest: raise OpenStackCloudException("nova client call failed") - def get_compute_usage(self, name_or_id, start, end=None): + def get_compute_usage(self, name_or_id, start=None, end=None): """ Get usage for a specific project :param name_or_id: project name or id - :param start: :class:`datetime.datetime` Start date in UTC - :param end: :class:`datetime.datetime` End date in UTC. Defaults to now + :param start: :class:`datetime.datetime` or string. Start date in UTC + Defaults to 2010-07-06T12:00:00Z (the date the OpenStack + project was started) + :param end: :class:`datetime.datetime` or string. End date in UTC. + Defaults to now :raises: OpenStackCloudException if it's not a valid project :returns: Munch object with the usage """ + def parse_date(date): + try: + return iso8601.parse_date(date) + except iso8601.iso8601.ParseError: + # Yes. This is an exception mask. However,iso8601 is an + # implementation detail - and the error message is actually + # less informative. + raise OpenStackCloudException( + "Date given, {date}, is invalid. Please pass in a date" + " string in ISO 8601 format -" + " YYYY-MM-DDTHH:MM:SS".format( + date=date)) + + def parse_datetime_for_nova(date): + # Must strip tzinfo from the date- it breaks Nova. Also, + # Nova is expecting this in UTC. If someone passes in an + # ISO8601 date string or a datetime with timzeone data attached, + # strip the timezone data but apply offset math first so that + # the user's well formed perfectly valid date will be used + # correctly. + offset = date.utcoffset() + if offset: + date = date - datetime.timedelta(hours=offset) + return date.replace(tzinfo=None) + + if not start: + start = parse_date('2010-07-06') + elif not isinstance(start, datetime.datetime): + start = parse_date(start) + if not end: + end = datetime.datetime.utcnow() + elif not isinstance(start, datetime.datetime): + end = parse_date(end) + + start = parse_datetime_for_nova(start) + end = parse_datetime_for_nova(end) + proj = self.get_project(name_or_id) if not proj: raise OpenStackCloudException("project does not exist: {}".format( name=proj.id)) - if not end: - end = datetime.datetime.now() with _utils.shade_exceptions( "Unable to get resources usage for project: {name}".format( From ba0e9451e664743dbc5d24307ca5a3829ee3e91b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 23 May 2017 16:05:15 -0500 Subject: [PATCH 1552/3836] Pick most recent rather than first fixed address If there are multiple fixed ips for a server after nat_destination filtering, we pick one arbitrarily. The thing we really want to do is pick the most recent one - since it's the one with the greatest chance of success. Removed a test that wasn't actually testing anything. Change-Id: I73fe5fe58269931ae9a7e52b79c3211d96e69a92 --- shade/openstackcloud.py | 70 +++++++++++--------- shade/tests/unit/test_floating_ip_common.py | 10 --- shade/tests/unit/test_floating_ip_neutron.py | 3 + 3 files changed, 43 insertions(+), 40 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 7d7be4d31..88e58d60d 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4779,9 +4779,7 @@ def _nat_destination_port( return (None, None) port = None if not fixed_address: - if len(ports) == 1: - port = ports[0] - else: + if len(ports) > 1: if nat_destination: nat_network = self.get_network(nat_destination) if not nat_network: @@ -4793,37 +4791,49 @@ def _nat_destination_port( else: nat_network = self.get_nat_destination() - if nat_network: - for maybe_port in ports: - if maybe_port['network_id'] == nat_network['id']: - port = maybe_port - if not port: - raise OpenStackCloudException( - 'No port on server {server} was found matching' - ' the network configured as the NAT destination' - ' {dest}. Please check your config'.format( - server=server['id'], dest=nat_network['name'])) - else: - port = ports[0] - warnings.warn( - 'During Floating IP creation, multiple private' - ' networks were found. {net} is being selected at' - ' random to be the destination of the NAT. If that' - ' is not what you want, please configure the' + if not nat_network: + raise OpenStackCloudException( + 'Multiple ports were found for server {server}' + ' but none of the networks are a valid NAT' + ' destination, so it is impossible to add a' + ' floating IP. If you have a network that is a valid' + ' destination for NAT and we could not find it,' + ' please file a bug. But also configure the' ' nat_destination property of the networks list in' ' your clouds.yaml file. If you do not have a' ' clouds.yaml file, please make one - your setup' - ' is complicated.'.format(net=port['network_id'])) + ' is complicated.'.format(server=server['id'])) - # Select the first available IPv4 address - for address in port.get('fixed_ips', list()): - try: - ip = ipaddress.ip_address(address['ip_address']) - except Exception: - continue - if ip.version == 4: - fixed_address = address['ip_address'] - return port, fixed_address + maybe_ports = [] + for maybe_port in ports: + if maybe_port['network_id'] == nat_network['id']: + maybe_ports.append(maybe_port) + if not maybe_ports: + raise OpenStackCloudException( + 'No port on server {server} was found matching' + ' your NAT destination network {dest}. Please ' + ' check your config'.format( + server=server['id'], dest=nat_network['name'])) + ports = maybe_ports + + # Select the most recent available IPv4 address + # To do this, sort the ports in reverse order by the created_at + # field which is a string containing an ISO DateTime (which + # thankfully sort properly) This way the most recent port created, + # if there are more than one, will be the arbitrary port we + # select. + for port in sorted( + ports, + key=operator.itemgetter('created_at'), + reverse=True): + for address in port.get('fixed_ips', list()): + try: + ip = ipaddress.ip_address(address['ip_address']) + except Exception: + continue + if ip.version == 4: + fixed_address = address['ip_address'] + return port, fixed_address raise OpenStackCloudException( "unable to find a free fixed IPv4 address for server " "{0}".format(server['id'])) diff --git a/shade/tests/unit/test_floating_ip_common.py b/shade/tests/unit/test_floating_ip_common.py index 5031e032d..d208afcb0 100644 --- a/shade/tests/unit/test_floating_ip_common.py +++ b/shade/tests/unit/test_floating_ip_common.py @@ -21,7 +21,6 @@ from mock import patch -from shade import exc from shade import meta from shade import OpenStackCloud from shade.tests.fakes import FakeServer @@ -225,12 +224,3 @@ def test_add_ips_to_server_auto_ip( mock_add_auto_ip.assert_called_with( server_dict, wait=False, timeout=60, reuse=True) - - @patch.object(OpenStackCloud, 'search_ports', return_value=[{}]) - def test_nat_destination_port_when_no_free_fixed_ip( - self, mock_search_ports): - server = {'id': 42} - self.assertRaisesRegexp( - exc.OpenStackCloudException, 'server 42$', - self.cloud._nat_destination_port, server - ) diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 061e1edc6..72e5c515f 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -20,6 +20,7 @@ """ import copy +import datetime import munch from shade import exc @@ -101,6 +102,7 @@ class TestFloatingIP(base.RequestsMockTestCase): 'status': 'ACTIVE', 'binding:host_id': 'devstack', 'name': 'first-port', + 'created_at': datetime.datetime.now().isoformat(), 'allowed_address_pairs': [], 'admin_state_up': True, 'network_id': '70c1db1f-b701-45bd-96e0-a313ee3430b3', @@ -939,6 +941,7 @@ def test_create_floating_ip_no_port(self): server_port = { "id": "port-id", "device_id": "some-server", + 'created_at': datetime.datetime.now().isoformat(), 'fixed_ips': [ { 'subnet_id': 'subnet-id', From d0e7c27cc3fa7dca1d9e958e03df52a00c2ff21b Mon Sep 17 00:00:00 2001 From: "Yuanbin.Chen" Date: Wed, 24 May 2017 10:06:34 +0800 Subject: [PATCH 1553/3836] Fix cluster action list filter This patch change action 'target' filter. Change-Id: I1a234ed9b0073c062a63127c45384d30d0bff3bd Signed-off-by: Yuanbin.Chen --- openstack/cluster/v1/action.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openstack/cluster/v1/action.py b/openstack/cluster/v1/action.py index 58691692e..255a34bc0 100644 --- a/openstack/cluster/v1/action.py +++ b/openstack/cluster/v1/action.py @@ -26,7 +26,8 @@ class Action(resource.Resource): allow_get = True _query_mapping = resource.QueryParameters( - 'name', 'target', 'action', 'status', 'sort', 'global_project') + 'name', 'action', 'status', 'sort', 'global_project', + target_id='target') # Properties #: Name of the action. From a4bcf38f5518579363bb5f33ef9b7c5651f44575 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 24 May 2017 09:37:11 -0500 Subject: [PATCH 1554/3836] Use catalog endpoint on any errors in image version discovery If we can't do version discovery for whatever reason, just use what's in the catalog. While we do that, try to detect what version it is so that we can at least take the correct actions. Change-Id: I2be381c1a0af9ee576f22b6c03b7a2e577503aed --- shade/openstackcloud.py | 83 +++++++++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 33 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 88e58d60d..87b0604e6 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -424,25 +424,33 @@ def _raw_image_client(self): self._raw_clients['raw-image'] = image_client return self._raw_clients['raw-image'] - def _match_given_image_endpoint(self, given, version): - if given.endswith('/'): - given = given[:-1] - if given.split('/')[-1].startswith('v' + version[0]): + def _detect_image_verison_from_url(self, image_url): + if image_url.endswith('/'): + image_url = image_url[:-1] + version = image_url.split('/')[-1] + if version.startswith('v'): + return version[1] + return None + + def _match_given_image_endpoint(self, image_url, version): + url_version = self._detect_image_verison_from_url(image_url) + if url_version and version == url_version: return True return False def _discover_image_endpoint(self, config_version, image_client): + # First - quick check to see if the endpoint in the catalog + # is a versioned endpoint that matches the version we requested. + # If it is, don't do any additoinal work. + catalog_endpoint = image_client.get_endpoint() + if self._match_given_image_endpoint( + catalog_endpoint, config_version): + return catalog_endpoint + api_version_id = None try: - # First - quick check to see if the endpoint in the catalog - # is a versioned endpoint that matches the version we requested. - # If it is, don't do any additoinal work. - catalog_endpoint = image_client.get_endpoint() - if self._match_given_image_endpoint( - catalog_endpoint, config_version): - return catalog_endpoint + api_version = None # Version discovery versions = image_client.get('/') - api_version = None if config_version.startswith('1'): api_version = [ version for version in versions @@ -455,36 +463,45 @@ def _discover_image_endpoint(self, config_version, image_client): if version['status'] == 'CURRENT'][0] image_url = api_version['links'][0]['href'] - # If we detect a different version that was configured, - # set the version in occ because we have logic elsewhere - # that is different depending on which version we're using - warning_msg = None - if (config_version.startswith('2') - and api_version['id'].startswith('v1')): - self.cloud_config.config['image_api_version'] = '1' - warning_msg = ( - 'image_api_version is 2 but only 1 is available.') - elif (config_version.startswith('1') - and api_version['id'].startswith('v2')): - self.cloud_config.config['image_api_version'] = '2' - warning_msg = ( - 'image_api_version is 1 but only 2 is available.') - if warning_msg: - self.log.debug(warning_msg) - warnings.warn(warning_msg) - except (keystoneauth1.exceptions.connection.ConnectFailure, - OpenStackCloudURINotFound) as e: + # Set the id to the first digit after the v + api_version_id = api_version['id'][1] + except Exception as e: # A 404 or a connection error is a likely thing to get # either with a misconfgured glance. or we've already # gotten a versioned endpoint from the catalog self.log.debug( "Glance version discovery failed, assuming endpoint in" " the catalog is already versioned. {e}".format(e=str(e))) - image_url = image_client.get_endpoint() + image_url = catalog_endpoint + api_version_id = self._detect_image_verison_from_url(image_url) + + if not api_version_id: + # We couldn't detect anything, assume config is correct and + # catalog is correct + return image_url + + # If we detect a different version that was configured, + # set the version in occ because we have logic elsewhere + # that is different depending on which version we're using + warning_msg = None + config_version_id = config_version[0] + if config_version_id != api_version_id: + self.cloud_config.config['image_api_version'] = api_version_id + warning_msg = ( + 'image_api_version is {config_version_id} but only' + ' {api_version_id} is available.'.format( + config_version_id=config_version_id, + api_version_id=api_version_id)) + if warning_msg: + self.log.debug(warning_msg) + warnings.warn(warning_msg) + + if catalog_endpoint == image_url: + return catalog_endpoint # Sometimes version discovery documents have broken endpoints, but # the catalog has good ones (what?) - catalog_endpoint = urllib.parse.urlparse(image_client.get_endpoint()) + catalog_endpoint = urllib.parse.urlparse(catalog_endpoint) discovered_endpoint = urllib.parse.urlparse(image_url) return urllib.parse.ParseResult( From b194a4d58a4eec557232a5d485b45da4a2382554 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 24 May 2017 11:40:58 -0500 Subject: [PATCH 1555/3836] Log cloud name on Connection retry issues It's hard in a complex environment like nodepool to know what to debug otherwise. Change-Id: I5885fc136c89a37ac405ce1d859a33d1c1f9cf1a --- shade/task_manager.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/shade/task_manager.py b/shade/task_manager.py index 3b0c281be..27207e6bc 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -107,9 +107,17 @@ def run(self, client): try: self.done(self.main(client)) except keystoneauth1.exceptions.RetriableConnectionFailure: - client.log.debug( - "Connection failure for %(name)s, retrying", - {'name': type(self).__name__}) + if client.region_name: + client.log.debug( + "Connection failure on %(cloud)s:%(region)s" + " for %(name)s, retrying", { + 'cloud': client.name, + 'region': client.region_name, + 'name': self.name}) + else: + client.log.debug( + "Connection failure on %(cloud)s for %(name)s," + " retrying", {'cloud': client.name, 'name': self.name}) self.done(self.main(client)) except Exception: raise From 9f393dcbc89390a08d5f20a44cbdeb96ae9be5e4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 24 May 2017 12:24:41 -0500 Subject: [PATCH 1556/3836] Add time reporting to Connection Retry message Change-Id: I38415e88fd3415022d003f710d95131a4dc314d8 --- shade/task_manager.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/shade/task_manager.py b/shade/task_manager.py index 27207e6bc..430a2585d 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -105,19 +105,25 @@ def run(self, client): try: # Retry one time if we get a retriable connection failure try: + # Keep time for connection retrying logging + start = time.time() self.done(self.main(client)) except keystoneauth1.exceptions.RetriableConnectionFailure: + end = time.time() + dt = end - start if client.region_name: client.log.debug( "Connection failure on %(cloud)s:%(region)s" - " for %(name)s, retrying", { - 'cloud': client.name, - 'region': client.region_name, - 'name': self.name}) + " for %(name)s after %(secs)s seconds, retrying", + {'cloud': client.name, + 'region': client.region_name, + 'secs': dt, + 'name': self.name}) else: client.log.debug( - "Connection failure on %(cloud)s for %(name)s," - " retrying", {'cloud': client.name, 'name': self.name}) + "Connection failure on %(cloud)s for %(name)s after" + " %(secs)s seconds, retrying", + {'cloud': client.name, 'name': self.name, 'secs': dt}) self.done(self.main(client)) except Exception: raise From 462fe8178bc10bbef0618f608eff94c826d9934d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 24 May 2017 12:34:52 -0500 Subject: [PATCH 1557/3836] Fix python3 issues in functional tests Change-Id: I86e28616403c36672603a6ad6cb0477722da4594 --- shade/exc.py | 5 +++-- shade/tests/functional/test_image.py | 10 +++++----- shade/tests/functional/test_stack.py | 11 ++++++----- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/shade/exc.py b/shade/exc.py index c8c579ed6..32023c42f 100644 --- a/shade/exc.py +++ b/shade/exc.py @@ -103,8 +103,9 @@ def raise_from_response(response, error_message=None): details = response.json() # Nova returns documents that look like # {statusname: 'message': message, 'code': code} - if len(details.keys()) == 1: - detail_key = details.keys()[0] + detail_keys = list(details.keys()) + if len(detail_keys) == 1: + detail_key = detail_keys[0] detail_message = details[detail_key].get('message') if detail_message: remote_error += " {message}".format(message=detail_message) diff --git a/shade/tests/functional/test_image.py b/shade/tests/functional/test_image.py index 7280bd36d..8de0b9fc4 100644 --- a/shade/tests/functional/test_image.py +++ b/shade/tests/functional/test_image.py @@ -31,7 +31,7 @@ def setUp(self): def test_create_image(self): test_image = tempfile.NamedTemporaryFile(delete=False) - test_image.write('\0' * 1024 * 1024) + test_image.write(b'\0' * 1024 * 1024) test_image.close() image_name = self.getUniqueString('image') try: @@ -49,7 +49,7 @@ def test_create_image(self): def test_download_image(self): test_image = tempfile.NamedTemporaryFile(delete=False) self.addCleanup(os.remove, test_image.name) - test_image.write('\0' * 1024 * 1024) + test_image.write(b'\0' * 1024 * 1024) test_image.close() image_name = self.getUniqueString('image') self.user_cloud.create_image( @@ -69,7 +69,7 @@ def test_download_image(self): def test_create_image_skip_duplicate(self): test_image = tempfile.NamedTemporaryFile(delete=False) - test_image.write('\0' * 1024 * 1024) + test_image.write(b'\0' * 1024 * 1024) test_image.close() image_name = self.getUniqueString('image') try: @@ -95,7 +95,7 @@ def test_create_image_skip_duplicate(self): def test_create_image_force_duplicate(self): test_image = tempfile.NamedTemporaryFile(delete=False) - test_image.write('\0' * 1024 * 1024) + test_image.write(b'\0' * 1024 * 1024) test_image.close() image_name = self.getUniqueString('image') first_image = None @@ -127,7 +127,7 @@ def test_create_image_force_duplicate(self): def test_create_image_update_properties(self): test_image = tempfile.NamedTemporaryFile(delete=False) - test_image.write('\0' * 1024 * 1024) + test_image.write(b'\0' * 1024 * 1024) test_image.close() image_name = self.getUniqueString('image') try: diff --git a/shade/tests/functional/test_stack.py b/shade/tests/functional/test_stack.py index 9416119ea..cd6d1fc20 100644 --- a/shade/tests/functional/test_stack.py +++ b/shade/tests/functional/test_stack.py @@ -85,7 +85,7 @@ def _cleanup_stack(self): def test_stack_validation(self): test_template = tempfile.NamedTemporaryFile(delete=False) - test_template.write(validate_template) + test_template.write(validate_template.encode('utf-8')) test_template.close() stack_name = self.getUniqueString('validate_template') self.assertRaises(exc.OpenStackCloudException, @@ -95,7 +95,7 @@ def test_stack_validation(self): def test_stack_simple(self): test_template = tempfile.NamedTemporaryFile(delete=False) - test_template.write(fakes.FAKE_TEMPLATE) + test_template.write(fakes.FAKE_TEMPLATE.encode('utf-8')) test_template.close() self.stack_name = self.getUniqueString('simple_stack') self.addCleanup(self._cleanup_stack) @@ -148,15 +148,16 @@ def test_stack_nested(self): test_template = tempfile.NamedTemporaryFile( suffix='.yaml', delete=False) - test_template.write(root_template) + test_template.write(root_template.encode('utf-8')) test_template.close() simple_tmpl = tempfile.NamedTemporaryFile(suffix='.yaml', delete=False) - simple_tmpl.write(fakes.FAKE_TEMPLATE) + simple_tmpl.write(fakes.FAKE_TEMPLATE.encode('utf-8')) simple_tmpl.close() env = tempfile.NamedTemporaryFile(suffix='.yaml', delete=False) - env.write(environment % simple_tmpl.name) + expanded_env = environment % simple_tmpl.name + env.write(expanded_env.encode('utf-8')) env.close() self.stack_name = self.getUniqueString('nested_stack') From aad00aa01e737b4b553c17eb22c4c5211f6767f5 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 24 May 2017 23:11:29 +0000 Subject: [PATCH 1558/3836] Updated from global requirements Change-Id: I1a52a58baccd8dd3b6f04bd8baa8d20654ce366d --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8472bc6d7..6a6bdab10 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ requests!=2.12.2,!=2.13.0,>=2.10.0 # Apache-2.0 requestsexceptions>=1.2.0 # Apache-2.0 six>=1.9.0 # MIT futures>=3.0;python_version=='2.7' or python_version=='2.6' # BSD -iso8601>=0.1.11 # MIT +iso8601>=0.1.11 # MIT keystoneauth1>=2.20.0 # Apache-2.0 netifaces>=0.10.4 # MIT From 990cfa3ce24d0a92c7578b52ae1fea02d69f7e87 Mon Sep 17 00:00:00 2001 From: Matthew Booth Date: Thu, 25 May 2017 13:26:48 +0100 Subject: [PATCH 1559/3836] Don't pop from os.environ It's rude to other users and subsequent callers. Change-Id: I7789f381c99311bfd1c1e0a9869cbacbc96b17d6 --- os_client_config/config.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/os_client_config/config.py b/os_client_config/config.py index 96d7f5355..1ed416cf5 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -106,9 +106,12 @@ def _get_os_environ(envvar_prefix=None): for k in environkeys: newkey = k.split('_', 1)[-1].lower() ret[newkey] = os.environ[k] - # If the only environ keys are cloud and region_name, don't return anything - # because they are cloud selectors - if set(environkeys) - set(['OS_CLOUD', 'OS_REGION_NAME']): + # If the only environ keys are selectors or behavior modification, don't + # return anything + selectors = set([ + 'OS_CLOUD', 'OS_REGION_NAME', + 'OS_CLIENT_CONFIG_FILE', 'OS_CLIENT_SECURE_FILE', 'OS_CLOUD_NAME']) + if set(environkeys) - selectors: return ret return None @@ -193,11 +196,11 @@ def __init__(self, config_files=None, vendor_files=None, self._secure_files = [] self._vendor_files = [] - config_file_override = os.environ.pop('OS_CLIENT_CONFIG_FILE', None) + config_file_override = os.environ.get('OS_CLIENT_CONFIG_FILE') if config_file_override: self._config_files.insert(0, config_file_override) - secure_file_override = os.environ.pop('OS_CLIENT_SECURE_FILE', None) + secure_file_override = os.environ.get('OS_CLIENT_SECURE_FILE') if secure_file_override: self._secure_files.insert(0, secure_file_override) @@ -226,12 +229,12 @@ def __init__(self, config_files=None, vendor_files=None, else: # Get the backwards compat value prefer_ipv6 = get_boolean( - os.environ.pop( + os.environ.get( 'OS_PREFER_IPV6', client_config.get( 'prefer_ipv6', client_config.get( 'prefer-ipv6', True)))) force_ipv4 = get_boolean( - os.environ.pop( + os.environ.get( 'OS_FORCE_IPV4', client_config.get( 'force_ipv4', client_config.get( 'broken-ipv6', False)))) @@ -243,7 +246,7 @@ def __init__(self, config_files=None, vendor_files=None, self.force_ipv4 = True # Next, process environment variables and add them to the mix - self.envvar_key = os.environ.pop('OS_CLOUD_NAME', 'envvars') + self.envvar_key = os.environ.get('OS_CLOUD_NAME', 'envvars') if self.envvar_key in self.cloud_config['clouds']: raise exceptions.OpenStackConfigException( '"{0}" defines a cloud named "{1}", but' @@ -251,9 +254,8 @@ def __init__(self, config_files=None, vendor_files=None, ' either your environment based cloud, or one of your' ' file-based clouds.'.format(self.config_filename, self.envvar_key)) - # Pull out OS_CLOUD so that if it's the only thing set, do not - # make an envvars cloud - self.default_cloud = os.environ.pop('OS_CLOUD', None) + + self.default_cloud = os.environ.get('OS_CLOUD') envvars = _get_os_environ(envvar_prefix=envvar_prefix) if envvars: From bf8774b80970357048e1d9353383a42ebe811c2c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 25 May 2017 09:29:30 -0500 Subject: [PATCH 1560/3836] Log specific error message from RetriableConnectionFailure There are more than one error from keystoneauth that can match RetriableConnectionFailure. We retry on all of them, but logging the whole message can be useful for debugging. Change-Id: I2fd3bf62a3a755281d34ff5d3624b835281604fb --- shade/task_manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shade/task_manager.py b/shade/task_manager.py index 430a2585d..d890b8a2c 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -108,10 +108,11 @@ def run(self, client): # Keep time for connection retrying logging start = time.time() self.done(self.main(client)) - except keystoneauth1.exceptions.RetriableConnectionFailure: + except keystoneauth1.exceptions.RetriableConnectionFailure as e: end = time.time() dt = end - start if client.region_name: + client.log.debug(str(e)) client.log.debug( "Connection failure on %(cloud)s:%(region)s" " for %(name)s after %(secs)s seconds, retrying", From 58d9b2a713544ed84a7abd68a019899d4588e47b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 25 May 2017 09:45:22 -0500 Subject: [PATCH 1561/3836] Add logging of non-standard error message documents Sometimes we get non-standard repsonses that include things like html errors. Log them to a special logger if they are there so that someone can opt in to seeing them. Change-Id: Ife1ce58b0484c610d6b43a9adb39113f387eff77 --- shade/__init__.py | 7 +++++++ shade/exc.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index c2f69b711..35e7ee98e 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -53,6 +53,13 @@ def simple_logging(debug=False, http_debug=False): log = _log.setup_logging('keystoneauth') log.addHandler(logging.StreamHandler()) log.setLevel(log_level) + # We only want extra shade HTTP tracing in http debug mode + log = _log.setup_logging('shade.http') + log.setLevel(log_level) + else: + # We only want extra shade HTTP tracing in http debug mode + log = _log.setup_logging('shade.http') + log.setLevel(logging.WARNING) # Simple case - we only care about request id log during debug log = _log.setup_logging('shade.request_ids') log.setLevel(log_level) diff --git a/shade/exc.py b/shade/exc.py index c8c579ed6..f504e44de 100644 --- a/shade/exc.py +++ b/shade/exc.py @@ -88,6 +88,32 @@ class OpenStackCloudURINotFound(OpenStackCloudHTTPError): OpenStackCloudResourceNotFound = OpenStackCloudURINotFound +def _log_response_extras(response): + # Sometimes we get weird HTML errors. This is usually from load balancers + # or other things. Log them to a special logger so that they can be + # toggled indepdently - and at debug level so that a person logging + # shade.* only gets them at debug. + if response.headers.get('content-type') != 'text/html': + return + try: + if int(response.headers.get('content-length', 0)) == 0: + return + except Exception: + return + logger = _log.setup_logging('shade.http') + if response.reason: + logger.debug( + "Non-standard error '{reason}' returned from {url}:".format( + reason=response.reason, + url=response.url)) + else: + logger.debug( + "Non-standard error returned from {url}:".format( + url=response.url)) + for response_line in response.text.split('\n'): + logger.debug(response_line) + + # Logic shamelessly stolen from requests def raise_from_response(response, error_message=None): msg = '' @@ -112,6 +138,8 @@ def raise_from_response(response, error_message=None): if response.reason: remote_error += " {reason}".format(reason=response.reason) + _log_response_extras(response) + if error_message: msg = '{error_message}. ({code}) {source} {remote_error}'.format( error_message=error_message, From f6c74f25f0bd6e8cd1341701f3bc135f46691fe7 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 25 May 2017 10:16:19 -0500 Subject: [PATCH 1562/3836] Set some logger names explicitly In the next patch we'll add documentation about what shade logs to what logging facilities. To make sure that those named facilities are maintained as part of the interface a consumer can count on, set non-root logger names explicitly. Also, this changes the logger name for the "waiting" messages from shade._utils to shade.iterate_timeout. _utils is an implemenation detail and such subject to change. The logger for it is not. Change-Id: I2ff413062c4cc3b49872fa8fd24e5daade063187 --- shade/_utils.py | 2 +- shade/exc.py | 2 +- shade/task_manager.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/shade/_utils.py b/shade/_utils.py index 340b63c08..265ff5afc 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -48,7 +48,7 @@ def _iterate_timeout(timeout, message, wait=2): with . """ - log = _log.setup_logging(__name__) + log = _log.setup_logging('shade.iterate_timeout') try: # None as a wait winds up flowing well in the per-resource cache diff --git a/shade/exc.py b/shade/exc.py index f504e44de..eade31f54 100644 --- a/shade/exc.py +++ b/shade/exc.py @@ -37,7 +37,7 @@ def __init__(self, message, extra_data=None, **kwargs): def log_error(self, logger=None): if not logger: - logger = _log.setup_logging(__name__) + logger = _log.setup_logging('shade.exc') if self.inner_exception and self.inner_exception[1]: logger.error(self.orig_message, exc_info=self.inner_exception) diff --git a/shade/task_manager.py b/shade/task_manager.py index d890b8a2c..9546653fb 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -226,7 +226,7 @@ def main(self, client): class TaskManager(object): - log = _log.setup_logging(__name__) + log = _log.setup_logging('shade.task_manager') def __init__( self, client, name, result_filter_cb=None, workers=5, **kwargs): From e167039f581b51995de0f9bdb8562b1b799d246b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 25 May 2017 10:21:23 -0500 Subject: [PATCH 1563/3836] Add novaclient interactions to http_debug While hopefully novaclient will be gone soon, it's here now. We set up novaclient logging in our unit tests so that we can see what's going on. Do the same in simple_logging at least until we've gotten rid of novaclient. Change-Id: I105af3acbe215fb8d0cdc3dd7e5670eb4379d668 --- shade/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/shade/__init__.py b/shade/__init__.py index 35e7ee98e..3d5af0401 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -53,6 +53,11 @@ def simple_logging(debug=False, http_debug=False): log = _log.setup_logging('keystoneauth') log.addHandler(logging.StreamHandler()) log.setLevel(log_level) + # Enable HTTP level tracing of novaclient + logger = logging.getLogger('novaclient') + log.addHandler(logging.StreamHandler()) + log.setLevel(log_level) + logger.propagate = False # We only want extra shade HTTP tracing in http debug mode log = _log.setup_logging('shade.http') log.setLevel(log_level) From 5d987eb31e8c620dd69fe11a3c3ce8458bb45332 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 25 May 2017 10:18:58 -0500 Subject: [PATCH 1564/3836] Add documentation about shade's use of logging shade logs to some specific named loggers for various things. They are defined and intentional, but are not documented. So let's document them. Change-Id: If52553e5478d4e2f8a56f5d899e93fd2b4fe3c2d --- doc/source/index.rst | 1 + doc/source/logging.rst | 98 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 doc/source/logging.rst diff --git a/doc/source/index.rst b/doc/source/index.rst index 32ae8955e..29f7a7253 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -13,6 +13,7 @@ Contents: installation usage + logging model contributing coding diff --git a/doc/source/logging.rst b/doc/source/logging.rst new file mode 100644 index 000000000..5915de9ec --- /dev/null +++ b/doc/source/logging.rst @@ -0,0 +1,98 @@ +======= +Logging +======= + +`shade` uses `Python Logging`_. As `shade` is a library, it does not +configure logging handlers automatically, expecting instead for that to be +the purview of the consuming application. + +Simple Usage +------------ + +For consumers who just want to get a basic logging setup without thinking +about it too deeply, there is a helper method. If used, it should be called +before any other `shade` functionality. + +.. code-block:: python + + import shade + shade.simple_logging() + +`shade.simple_logging` takes two optional boolean arguments: + +debug + Turns on debug logging. + +http_debug + Turns on debug logging as well as debug logging of the underlying HTTP calls. + +`shade.simple_logging` also sets up a few other loggers and squelches some +warnings or log messages that are otherwise uninteresting or unactionable by +a `shade` user. + +Advanced Usage +-------------- + +`shade` logs to a set of different named loggers. + +Most of the logging is set up to log to the root `shade` logger. There are +additional sub-loggers that are used at times, primarily so that a user can +decide to turn on or off a specific type of logging. They are listed below. + +shade.task_manager + `shade` uses a Task Manager to perform remote calls. The `shade.task_manager` + logger emits messages at the start and end of each Task announging what + it is going to run and then what it ran and how long it took. Logging + `shade.task_manager` is a good way to get a trace of external actions shade + is taking without full `HTTP Tracing`_. + +shade.request_ids + The `shade.request_ids` logger emits a log line at the end of each HTTP + interaction with the OpenStack Request ID associated with the interaction. + This can be be useful for tracking action taken on the server-side if one + does not want `HTTP Tracing`_. + +shade.exc + If `log_inner_exceptions` is set to True, `shade` will emit any wrapped + exception to the `shade.exc` logger. Wrapped exceptions are usually + considered implementation details, but can be useful for debugging problems. + +shade.iterate_timeout + When `shade` needs to poll a resource, it does so in a loop that waits + between iterations and ultimately timesout. The `shade.iterate_timeout` + logger emits messages for each iteration indicating it is waiting and for + how long. These can be useful to see for long running tasks so that one + can know things are not stuck, but can also be noisy. + +shade.http + `shade` will sometimes log additional information about HTTP interactions + to the `shade.http` logger. This can be verbose, as it sometimes logs + entire response bodies. + +shade.fnmatch + `shade` will try to use `fnmatch`_ on given `name_or_id` arguments. It's a + best effort attempt, so pattern misses are logged to `shade.fnmatch`. A user + may not be intending to use an fnmatch pattern - such as if they are trying + to find an image named ``Fedora 24 [official]``, so these messages are + logged separately. + +.. _fnmatch: https://pymotw.com/2/fnmatch/ + +HTTP Tracing +------------ + +HTTP Interactions are handled by `keystoneauth`. If you want to enable HTTP +tracing while using `shade` and are not using `shade.simple_logging`, +set the log level of the `keystoneauth` logger to `DEBUG`. + +Python Logging +-------------- + +Python logging is a standard feature of Python and is documented fully in the +Python Documentation, which varies by version of Python. + +For more information on Python Logging for Python v2, see +https://docs.python.org/2/library/logging.html. + +For more information on Python Logging for Python v3, see +https://docs.python.org/3/library/logging.html. From 92a19dfa2f545de5d56059e55a8380cad3246aee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Fri, 26 May 2017 20:57:41 +0000 Subject: [PATCH 1565/3836] Remove designateclient from commands related to zones All commands related to zones, like create/update/list/delete zone are now made using keystoneauth1 client and direct REST calls to cloud. Change-Id: Id3a0fc0aa55e62832fd6fb85cafda8d55023d01f --- shade/_tasks.py | 20 --------- shade/openstackcloud.py | 46 +++++++++++--------- shade/tests/unit/test_recordset.py | 38 +++++++++++------ shade/tests/unit/test_zone.py | 68 +++++++++++++++--------------- 4 files changed, 88 insertions(+), 84 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index c350510b7..6076affea 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -407,26 +407,6 @@ def main(self, client): return client.keystone_client.roles.roles_for_user(**self.args) -class ZoneList(task_manager.Task): - def main(self, client): - return client.designate_client.zones.list() - - -class ZoneCreate(task_manager.Task): - def main(self, client): - return client.designate_client.zones.create(**self.args) - - -class ZoneUpdate(task_manager.Task): - def main(self, client): - return client.designate_client.zones.update(**self.args) - - -class ZoneDelete(task_manager.Task): - def main(self, client): - return client.designate_client.zones.delete(**self.args) - - class RecordSetList(task_manager.Task): def main(self, client): return client.designate_client.recordsets.list(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 88e58d60d..d15166945 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -6916,8 +6916,9 @@ def list_zones(self): :returns: A list of zones dicts. """ - with _utils.shade_exceptions("Error fetching zones list"): - return self.manager.submit_task(_tasks.ZoneList()) + return self._dns_client.get( + "/v2/zones", + error_message="Error fetching zones list") def get_zone(self, name_or_id, filters=None): """Get a zone by name or ID. @@ -6936,7 +6937,7 @@ def get_zone(self, name_or_id, filters=None): def search_zones(self, name_or_id=None, filters=None): zones = self.list_zones() - return _utils._filter_list(zones, name_or_id, filters) + return _utils._filter_list(zones['zones'], name_or_id, filters) def create_zone(self, name, zone_type=None, email=None, description=None, ttl=None, masters=None): @@ -6965,11 +6966,23 @@ def create_zone(self, name, zone_type=None, email=None, description=None, "Invalid type %s, valid choices are PRIMARY or SECONDARY" % zone_type) - with _utils.shade_exceptions("Unable to create zone {name}".format( - name=name)): - return self.manager.submit_task(_tasks.ZoneCreate( - name=name, type_=zone_type, email=email, - description=description, ttl=ttl, masters=masters)) + zone = { + "name": name, + "email": email, + "description": description, + } + if ttl is not None: + zone["ttl"] = ttl + + if zone_type is not None: + zone["type"] = zone_type + + if masters is not None: + zone["masters"] = masters + + return self._dns_client.post( + "/v2/zones", json=zone, + error_message="Unable to create zone {name}".format(name=name)) @_utils.valid_kwargs('email', 'description', 'ttl', 'masters') def update_zone(self, name_or_id, **kwargs): @@ -6992,13 +7005,9 @@ def update_zone(self, name_or_id, **kwargs): raise OpenStackCloudException( "Zone %s not found." % name_or_id) - with _utils.shade_exceptions( - "Error updating zone {0}".format(name_or_id)): - new_zone = self.manager.submit_task( - _tasks.ZoneUpdate( - zone=zone['id'], values=kwargs)) - - return new_zone + return self._dns_client.patch( + "/v2/zones/{zone_id}".format(zone_id=zone['id']), json=kwargs, + error_message="Error updating zone {0}".format(name_or_id)) def delete_zone(self, name_or_id): """Delete a zone. @@ -7015,10 +7024,9 @@ def delete_zone(self, name_or_id): self.log.debug("Zone %s not found for deleting", name_or_id) return False - with _utils.shade_exceptions( - "Error deleting zone {0}".format(name_or_id)): - self.manager.submit_task( - _tasks.ZoneDelete(zone=zone['id'])) + return self._dns_client.delete( + "/v2/zones/{zone_id}".format(zone_id=zone['id']), + error_message="Error deleting zone {0}".format(name_or_id)) return True diff --git a/shade/tests/unit/test_recordset.py b/shade/tests/unit/test_recordset.py index 975a70630..f639170d5 100644 --- a/shade/tests/unit/test_recordset.py +++ b/shade/tests/unit/test_recordset.py @@ -43,16 +43,16 @@ class TestRecordset(base.RequestsMockTestCase): - def setUp(self): - super(TestRecordset, self).setUp() - self.use_designate() - def test_create_recordset(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'dns', 'public', append=['zones']), - json={"zones": [zone]}), + 'dns', 'public', append=['v2', 'zones']), + json={ + "zones": [zone], + "links": {}, + "metadata": { + 'total_count': 1}}), self.get_designate_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( @@ -86,8 +86,12 @@ def test_create_recordset_exception(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'dns', 'public', append=['zones']), - json={"zones": [zone]}), + 'dns', 'public', append=['v2', 'zones']), + json={ + "zones": [zone], + "links": {}, + "metadata": { + 'total_count': 1}}), self.get_designate_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( @@ -128,8 +132,12 @@ def test_update_recordset(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'dns', 'public', append=['zones']), - json={"zones": [zone]}), + 'dns', 'public', append=['v2', 'zones']), + json={ + "zones": [zone], + "links": {}, + "metadata": { + 'total_count': 1}}), self.get_designate_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( @@ -194,8 +202,12 @@ def test_delete_recordset(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'dns', 'public', append=['zones']), - json={"zones": [zone]}), + 'dns', 'public', append=['v2', 'zones']), + json={ + "zones": [zone], + "links": {}, + "metadata": { + 'total_count': 1}}), self.get_designate_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( @@ -256,6 +268,7 @@ def test_delete_recordset(self): def _prepare_get_recordset_calls(self, zone_id, name_or_id): self.register_uris([ + self.get_designate_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'dns', 'public', append=['zones'], @@ -298,6 +311,7 @@ def test_get_recordset_by_name(self): def test_get_recordset_not_found_returns_false(self): recordset_name = "www.nonexistingrecord.net." self.register_uris([ + self.get_designate_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'dns', 'public', append=['zones'], diff --git a/shade/tests/unit/test_zone.py b/shade/tests/unit/test_zone.py index 344bb0aae..3d481364c 100644 --- a/shade/tests/unit/test_zone.py +++ b/shade/tests/unit/test_zone.py @@ -31,15 +31,11 @@ class TestZone(base.RequestsMockTestCase): - def setUp(self): - super(TestZone, self).setUp() - self.use_designate() - def test_create_zone(self): self.register_uris([ dict(method='POST', uri=self.get_mock_url( - 'dns', 'public', append=['zones']), + 'dns', 'public', append=['v2', 'zones']), json=new_zone_dict, validate=dict( json=zone_dict)) @@ -58,11 +54,11 @@ def test_create_zone_exception(self): self.register_uris([ dict(method='POST', uri=self.get_mock_url( - 'dns', 'public', append=['zones']), + 'dns', 'public', append=['v2', 'zones']), status_code=500) ]) with testtools.ExpectedException( - shade.OpenStackCloudException, + shade.exc.OpenStackCloudHTTPError, "Unable to create zone example.net." ): self.cloud.create_zone('example.net.') @@ -75,18 +71,15 @@ def test_update_zone(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'dns', 'public', append=['zones']), - json={"zones": [new_zone_dict]}), - self.get_designate_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['zones'], - qs_elements=['name=1']), - json={'zones': [new_zone_dict]}), - self.get_designate_discovery_mock_dict(), + 'dns', 'public', append=['v2', 'zones']), + json={ + "zones": [new_zone_dict], + "links": {}, + "metadata": { + 'total_count': 1}}), dict(method='PATCH', uri=self.get_mock_url( - 'dns', 'public', append=['zones', '1']), + 'dns', 'public', append=['v2', 'zones', '1']), json=updated_zone, validate=dict( json={"ttl": new_ttl})) @@ -99,18 +92,15 @@ def test_delete_zone(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'dns', 'public', append=['zones']), - json={"zones": [new_zone_dict]}), - self.get_designate_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['zones'], - qs_elements=['name=1']), - json={'zones': [new_zone_dict]}), - self.get_designate_discovery_mock_dict(), + 'dns', 'public', append=['v2', 'zones']), + json={ + "zones": [new_zone_dict], + "links": {}, + "metadata": { + 'total_count': 1}}), dict(method='DELETE', uri=self.get_mock_url( - 'dns', 'public', append=['zones', '1']), + 'dns', 'public', append=['v2', 'zones', '1']), json=new_zone_dict) ]) self.assertTrue(self.cloud.delete_zone('1')) @@ -120,8 +110,12 @@ def test_get_zone_by_id(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'dns', 'public', append=['zones']), - json={"zones": [new_zone_dict]}) + 'dns', 'public', append=['v2', 'zones']), + json={ + "zones": [new_zone_dict], + "links": {}, + "metadata": { + 'total_count': 1}}) ]) zone = self.cloud.get_zone('1') self.assertEqual(zone['id'], '1') @@ -131,8 +125,12 @@ def test_get_zone_by_name(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'dns', 'public', append=['zones']), - json={"zones": [new_zone_dict]}) + 'dns', 'public', append=['v2', 'zones']), + json={ + "zones": [new_zone_dict], + "links": {}, + "metadata": { + 'total_count': 1}}) ]) zone = self.cloud.get_zone('example.net.') self.assertEqual(zone['name'], 'example.net.') @@ -142,8 +140,12 @@ def test_get_zone_not_found_returns_false(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'dns', 'public', append=['zones']), - json={"zones": []}) + 'dns', 'public', append=['v2', 'zones']), + json={ + "zones": [], + "links": {}, + "metadata": { + 'total_count': 1}}) ]) zone = self.cloud.get_zone('nonexistingzone.net.') self.assertFalse(zone) From 7d851893951a8c0932d9e5efc179b39ee6e1b8c5 Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Fri, 3 Feb 2017 16:51:10 +0000 Subject: [PATCH 1566/3836] Add new parameter "is_default" to Network QoS policy. Closes-Bug: #1639220 Depends-On: If5ff2b00fa828f93aa089e275ddbd1ff542b79d4 Change-Id: Ibe7b7881cb190bfd5582f35b6de51a8bc21135de --- openstack/network/v2/qos_policy.py | 8 ++++++-- openstack/tests/functional/network/v2/test_qos_policy.py | 3 +++ openstack/tests/unit/network/v2/test_qos_policy.py | 4 +++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/openstack/network/v2/qos_policy.py b/openstack/network/v2/qos_policy.py index 68a6295e4..f14de70aa 100644 --- a/openstack/network/v2/qos_policy.py +++ b/openstack/network/v2/qos_policy.py @@ -28,9 +28,9 @@ class QoSPolicy(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'name', 'description', + 'name', 'description', 'is_default', project_id='tenant_id', - is_shared='shared' + is_shared='shared', ) # Properties @@ -41,6 +41,10 @@ class QoSPolicy(resource.Resource): project_id = resource.Body('tenant_id') #: The QoS policy description. description = resource.Body('description') + #: Indicates whether this QoS policy is the default policy for this + #: project. + #: *Type: bool* + is_default = resource.Body('is_default', type=bool) #: Indicates whether this QoS policy is shared across all projects. #: *Type: bool* is_shared = resource.Body('shared', type=bool) diff --git a/openstack/tests/functional/network/v2/test_qos_policy.py b/openstack/tests/functional/network/v2/test_qos_policy.py index d9e164975..9643015b0 100644 --- a/openstack/tests/functional/network/v2/test_qos_policy.py +++ b/openstack/tests/functional/network/v2/test_qos_policy.py @@ -22,6 +22,7 @@ class TestQoSPolicy(base.BaseFunctionalTest): QOS_POLICY_NAME = uuid.uuid4().hex QOS_POLICY_NAME_UPDATED = uuid.uuid4().hex IS_SHARED = False + IS_DEFAULT = False RULES = [] QOS_POLICY_DESCRIPTION = "QoS policy description" @@ -32,6 +33,7 @@ def setUpClass(cls): description=cls.QOS_POLICY_DESCRIPTION, name=cls.QOS_POLICY_NAME, shared=cls.IS_SHARED, + is_default=cls.IS_DEFAULT, ) assert isinstance(qos, _qos_policy.QoSPolicy) cls.assertIs(cls.QOS_POLICY_NAME, qos.name) @@ -52,6 +54,7 @@ def test_get(self): self.assertEqual(self.IS_SHARED, sot.is_shared) self.assertEqual(self.RULES, sot.rules) self.assertEqual(self.QOS_POLICY_DESCRIPTION, sot.description) + self.assertEqual(self.IS_DEFAULT, sot.is_default) def test_list(self): names = [o.name for o in self.conn.network.qos_policies()] diff --git a/openstack/tests/unit/network/v2/test_qos_policy.py b/openstack/tests/unit/network/v2/test_qos_policy.py index d91633d37..7c5e6c4de 100644 --- a/openstack/tests/unit/network/v2/test_qos_policy.py +++ b/openstack/tests/unit/network/v2/test_qos_policy.py @@ -21,7 +21,8 @@ 'name': 'qos-policy-name', 'shared': True, 'tenant_id': '2', - 'rules': [uuid.uuid4().hex] + 'rules': [uuid.uuid4().hex], + 'is_default': False } @@ -47,3 +48,4 @@ def test_make_it(self): self.assertTrue(sot.is_shared) self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) self.assertEqual(EXAMPLE['rules'], sot.rules) + self.assertEqual(EXAMPLE['is_default'], sot.is_default) From 6a325df8b6423d004d91eae51a270d2ccfb8ec0a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 30 May 2017 09:52:03 -0500 Subject: [PATCH 1567/3836] Remove unused occ version tie We have a trap for old os-client-config versions so that we can use set_session_constructor. Problem is - we don't actually use set_session_constructor. So it's a bogus trap that's just making life hard. Also disable a part of test_update_user_password. Our keystone v3 devstack doesn't have a v2 endpoint anymore, and this went south. Change-Id: I1299c6d33f301a6b1deed848737bfb561129a031 --- shade/openstackcloud.py | 5 ----- shade/tests/functional/test_users.py | 9 +++++---- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 87b0604e6..e08758ab0 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -172,11 +172,6 @@ def __init__( self.manager = task_manager.TaskManager( name=':'.join([self.name, self.region_name]), client=self) - # Provide better error message for people with stale OCC - if cloud_config.set_session_constructor is None: - raise OpenStackCloudException( - "shade requires at least version 1.22.0 of os-client-config") - self._external_ipv4_names = cloud_config.get_external_ipv4_networks() self._internal_ipv4_names = cloud_config.get_internal_ipv4_networks() self._external_ipv6_names = cloud_config.get_external_ipv6_networks() diff --git a/shade/tests/functional/test_users.py b/shade/tests/functional/test_users.py index 772bdcc34..8a530424c 100644 --- a/shade/tests/functional/test_users.py +++ b/shade/tests/functional/test_users.py @@ -17,7 +17,6 @@ Functional tests for `shade` user methods. """ -from shade import operator_cloud from shade import OpenStackCloudException from shade.tests.functional import base @@ -127,9 +126,11 @@ def test_update_user_password(self): self.assertEqual(user_name, new_user['name']) self.assertEqual(user_email, new_user['email']) self.assertTrue(new_user['enabled']) - self.assertIsNotNone(operator_cloud( - username=user_name, password='new_secret', - auth_url=self.operator_cloud.auth['auth_url']).keystone_client) + # TODO(mordred) Add this back when we can figure out how to do it + # with the updated gate jobs + # self.assertIsNotNone(operator_cloud( + # user_id=user['id'], password='new_secret', + # auth_url=self.operator_cloud.auth['auth_url']).keystone_client) def test_users_and_groups(self): i_ver = self.operator_cloud.cloud_config.get_api_version('identity') From f0c981cfc4873b46710628a7de394e0a826cdd6a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 30 May 2017 08:48:19 -0500 Subject: [PATCH 1568/3836] Fix legacy clients helpers There is no method called _create_or_return_legacy_client. Also, you have to pass a variable to a format. Change-Id: I559416d29c34b6cfea944f405c8608714ad81508 --- shade/_legacy_clients.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shade/_legacy_clients.py b/shade/_legacy_clients.py index 3790d593b..f34261593 100644 --- a/shade/_legacy_clients.py +++ b/shade/_legacy_clients.py @@ -37,7 +37,7 @@ def _create_legacy_client( return self._legacy_clients[client] def _deprecated_import_check(self, client): - module_name = '{client}client' + module_name = '{client}client'.format(client=client) warnings.warn( 'Using shade to get a {module_name} object is deprecated. If you' ' need a {module_name} object, please use make_legacy_client in' @@ -61,7 +61,7 @@ def magnum_client(self): @property def neutron_client(self): - return self._create_or_return_legacy_client('neutron', 'network') + return self._create_legacy_client('neutron', 'network') @property def nova_client(self): From 014d3979cfca4b9229faba7ef5bbbfea8b17031f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 31 May 2017 07:10:57 -0500 Subject: [PATCH 1569/3836] Update test_user_update_password to overlay clouds.yaml Rather than adding a bunch of v2/v3 logic, just use the demo cloud entry in clouds.yaml but with the new user_id/password. To make sure this works, go ahead and assign the member role to the user, so that the project settings in the file will work too. Change-Id: I1adc271d60c0ded25c639962d0974242009c7c9b --- shade/tests/functional/test_users.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/shade/tests/functional/test_users.py b/shade/tests/functional/test_users.py index 8a530424c..4bc3b885c 100644 --- a/shade/tests/functional/test_users.py +++ b/shade/tests/functional/test_users.py @@ -17,6 +17,7 @@ Functional tests for `shade` user methods. """ +from shade import operator_cloud from shade import OpenStackCloudException from shade.tests.functional import base @@ -126,11 +127,14 @@ def test_update_user_password(self): self.assertEqual(user_name, new_user['name']) self.assertEqual(user_email, new_user['email']) self.assertTrue(new_user['enabled']) - # TODO(mordred) Add this back when we can figure out how to do it - # with the updated gate jobs - # self.assertIsNotNone(operator_cloud( - # user_id=user['id'], password='new_secret', - # auth_url=self.operator_cloud.auth['auth_url']).keystone_client) + self.assertTrue(self.operator_cloud.grant_role( + 'Member', user=user['id'], project='demo', wait=True)) + self.addCleanup( + self.operator_cloud.revoke_role, + 'Member', user=user['id'], project='demo', wait=True) + self.assertIsNotNone(operator_cloud( + cloud=self._demo_name, + username=user_name, password='new_secret').keystone_client) def test_users_and_groups(self): i_ver = self.operator_cloud.cloud_config.get_api_version('identity') From 755890ba413a67806ceb40a9d413c95cdd233f95 Mon Sep 17 00:00:00 2001 From: Sylvain Baubeau Date: Thu, 1 Jun 2017 16:10:27 +0200 Subject: [PATCH 1570/3836] Handle ports with no 'created_at' attribute Change-Id: I1ff4635365acd803878212787fa98c622c69a41b --- shade/openstackcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 66c59d727..b49e883e9 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4836,7 +4836,7 @@ def _nat_destination_port( # select. for port in sorted( ports, - key=operator.itemgetter('created_at'), + key=lambda p: p.get('created_at', 0), reverse=True): for address in port.get('fixed_ips', list()): try: From 1a5f7343fd462ad703b42046823c9ac33ee2df9f Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Mon, 22 May 2017 22:22:33 +0000 Subject: [PATCH 1571/3836] Add pagination for the list_volumes call This commit adds support for pagination when it is enforced by the cinder server because the length of the volumes list requested is longer then CONF.osapi_max_limit. Change-Id: Ibd234e521884d4de746cb002988a5a6540c9f043 Signed-off-by: Rosario Di Somma --- shade/_normalize.py | 5 ----- shade/openstackcloud.py | 26 ++++++++++++++++++++-- shade/tests/functional/test_volume.py | 20 +++++++++++++++++ shade/tests/unit/test_volume.py | 31 +++++++++++++++++++++++---- 4 files changed, 71 insertions(+), 11 deletions(-) diff --git a/shade/_normalize.py b/shade/_normalize.py index fcd973ff9..670944c19 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -679,11 +679,6 @@ def _normalize_volumes(self, volumes): :returns: A list of normalized dicts. """ ret = [] - # With pagination we don't get a top-level container - # so we need to explicitly extract the list of volumes - if isinstance(volumes, dict): - volumes = volumes.get('volumes', []) - for volume in volumes: ret.append(self._normalize_volume(volume)) return ret diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 7ad03662a..cb3900f0f 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1589,11 +1589,33 @@ def list_volumes(self, cache=True): :returns: A list of volume ``munch.Munch``. """ + def _list(response): + # NOTE(rods)The shade Adapter is removing the top-level + # container but not with pagination or in a few other + # circumstances, so `response` can be a list of Volumes, + # or a dict like {'volumes_list': [...], 'volume': [...]}. + # We need the type check to work around the issue until + # next commit where we'll move the top-level container + # removing from the adapter to the related call. + if isinstance(response, list): + volumes.extend(response) + if isinstance(response, dict): + volumes.extend(meta.obj_list_to_dict(response['volumes'])) + endpoint = None + if 'volumes_links' in response: + for l in response['volumes_links']: + if 'rel' in l and 'next' == l['rel']: + endpoint = l['href'] + break + if endpoint: + _list(self._volume_client.get(endpoint)) + if not cache: warnings.warn('cache argument to list_volumes is deprecated. Use ' 'invalidate instead.') - return self._normalize_volumes( - self._volume_client.get('/volumes/detail')) + volumes = [] + _list(self._volume_client.get('/volumes/detail')) + return self._normalize_volumes(volumes) @_utils.cache_on_arguments() def list_volume_types(self, get_extra=True): diff --git a/shade/tests/functional/test_volume.py b/shade/tests/functional/test_volume.py index e3707139c..8136da4fe 100644 --- a/shade/tests/functional/test_volume.py +++ b/shade/tests/functional/test_volume.py @@ -91,3 +91,23 @@ def cleanup(self, volume_name, snapshot_name=None, image_name=None): volume = self.user_cloud.get_volume(volume_name) if volume: self.user_cloud.delete_volume(volume_name, wait=True) + + def test_list_volumes_pagination(self): + '''Test pagination for list volumes functionality''' + volumes = [] + # the number of created volumes needs to be higher than + # CONF.osapi_max_limit but not higher than volume quotas for + # the test user in the tenant(default quotas is set to 10) + num_volumes = 8 + for i in range(num_volumes): + name = self.getUniqueString() + self.addCleanup(self.cleanup, name) + v = self.user_cloud.create_volume(display_name=name, size=1) + volumes.append(v) + result = [] + for i in self.user_cloud.list_volumes(): + if i['name'] and i['name'].startswith(self.id()): + result.append(i['id']) + self.assertEqual( + sorted([i['id'] for i in volumes]), + sorted(result)) diff --git a/shade/tests/unit/test_volume.py b/shade/tests/unit/test_volume.py index 78dbfb06b..8659bb3f6 100644 --- a/shade/tests/unit/test_volume.py +++ b/shade/tests/unit/test_volume.py @@ -285,12 +285,35 @@ def test_list_volumes_with_pagination(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', 'detail']), + 'volumev2', 'public', + append=['volumes', 'detail']), + json={ + 'volumes': [vol1], + 'volumes_links': [ + {'href': self.get_mock_url( + 'volumev2', 'public', + append=['volumes', 'detail'], + qs_elements=['marker=01']), + 'rel': 'next'}]}), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['volumes', 'detail'], + qs_elements=['marker=01']), json={ - 'volumes': [vol1, vol2], + 'volumes': [vol2], 'volumes_links': [ - {'href': 'https://volume.example.com/fake_url', - 'rel': 'next'}]})]) + {'href': self.get_mock_url( + 'volumev2', 'public', + append=['volumes', 'detail'], + qs_elements=['marker=02']), + 'rel': 'next'}]}), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['volumes', 'detail'], + qs_elements=['marker=02']), + json={'volumes': []})]) self.assertEqual( [self.cloud._normalize_volume(vol1), self.cloud._normalize_volume(vol2)], From f0fa7e71c00a8dbda25db704a16c4b46ae1c3f75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Thu, 1 Jun 2017 20:11:08 +0000 Subject: [PATCH 1572/3836] Remove designateclient from commands related to recordsets All commands related to recordsets, like create/update/list/get/delete recordset are now made using keystoneauth1 client and direct REST calls to cloud. Change-Id: I40339ec188b75668211d87d4410e5f9b8961424f --- shade/_tasks.py | 25 ---- shade/openstackcloud.py | 50 +++++--- shade/tests/unit/test_recordset.py | 182 ++++------------------------- 3 files changed, 53 insertions(+), 204 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 6076affea..4584de15d 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -407,31 +407,6 @@ def main(self, client): return client.keystone_client.roles.roles_for_user(**self.args) -class RecordSetList(task_manager.Task): - def main(self, client): - return client.designate_client.recordsets.list(**self.args) - - -class RecordSetGet(task_manager.Task): - def main(self, client): - return client.designate_client.recordsets.get(**self.args) - - -class RecordSetCreate(task_manager.Task): - def main(self, client): - return client.designate_client.recordsets.create(**self.args) - - -class RecordSetUpdate(task_manager.Task): - def main(self, client): - return client.designate_client.recordsets.update(**self.args) - - -class RecordSetDelete(task_manager.Task): - def main(self, client): - return client.designate_client.recordsets.delete(**self.args) - - class NovaQuotasSet(task_manager.Task): def main(self, client): return client.nova_client.quotas.update(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 66c59d727..25aa5a690 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -7050,8 +7050,9 @@ def list_recordsets(self, zone): :returns: A list of recordsets. """ - with _utils.shade_exceptions("Error fetching recordsets list"): - return self.manager.submit_task(_tasks.RecordSetList(zone=zone)) + return self._dns_client.get( + "/v2/zones/{zone_id}/recordsets".format(zone_id=zone), + error_message="Error fetching recordsets list") def get_recordset(self, zone, name_or_id): """Get a recordset by name or ID. @@ -7064,9 +7065,10 @@ def get_recordset(self, zone, name_or_id): """ try: - return self.manager.submit_task(_tasks.RecordSetGet( - zone=zone, - recordset=name_or_id)) + return self._dns_client.get( + "/v2/zones/{zone_id}/recordsets/{recordset_id}".format( + zone_id=zone, recordset_id=name_or_id), + error_message="Error fetching recordset") except Exception: return None @@ -7097,11 +7099,22 @@ def create_recordset(self, zone, name, recordset_type, records, # We capitalize the type in case the user sends in lowercase recordset_type = recordset_type.upper() - with _utils.shade_exceptions( - "Unable to create recordset {name}".format(name=name)): - return self.manager.submit_task(_tasks.RecordSetCreate( - zone=zone, name=name, type_=recordset_type, records=records, - description=description, ttl=ttl)) + body = { + 'name': name, + 'type': recordset_type, + 'records': records + } + + if description: + body['description'] = description + + if ttl: + body['ttl'] = ttl + + return self._dns_client.post( + "/v2/zones/{zone_id}/recordsets".format(zone_id=zone), + json=body, + error_message="Error creating recordset {name}".format(name=name)) @_utils.valid_kwargs('description', 'ttl', 'records') def update_recordset(self, zone, name_or_id, **kwargs): @@ -7127,11 +7140,10 @@ def update_recordset(self, zone, name_or_id, **kwargs): raise OpenStackCloudException( "Recordset %s not found." % name_or_id) - with _utils.shade_exceptions( - "Error updating recordset {0}".format(name_or_id)): - new_recordset = self.manager.submit_task( - _tasks.RecordSetUpdate( - zone=zone, recordset=name_or_id, values=kwargs)) + new_recordset = self._dns_client.put( + "/v2/zones/{zone_id}/recordsets/{recordset_id}".format( + zone_id=zone_obj['id'], recordset_id=name_or_id), json=kwargs, + error_message="Error updating recordset {0}".format(name_or_id)) return new_recordset @@ -7156,10 +7168,10 @@ def delete_recordset(self, zone, name_or_id): self.log.debug("Recordset %s not found for deleting", name_or_id) return False - with _utils.shade_exceptions( - "Error deleting recordset {0}".format(name_or_id)): - self.manager.submit_task( - _tasks.RecordSetDelete(zone=zone['id'], recordset=name_or_id)) + self._dns_client.delete( + "/v2/zones/{zone_id}/recordsets/{recordset_id}".format( + zone_id=zone['id'], recordset_id=name_or_id), + error_message="Error deleting recordset {0}".format(name_or_id)) return True diff --git a/shade/tests/unit/test_recordset.py b/shade/tests/unit/test_recordset.py index f639170d5..0583c0ed8 100644 --- a/shade/tests/unit/test_recordset.py +++ b/shade/tests/unit/test_recordset.py @@ -53,22 +53,10 @@ def test_create_recordset(self): "links": {}, "metadata": { 'total_count': 1}}), - self.get_designate_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['zones'], - qs_elements=['name={0}'.format(zone['id'])]), - json={"zones": [zone]}), - self.get_designate_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['zones', zone['id']]), - json=zone), - self.get_designate_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'dns', 'public', - append=['zones', zone['id'], 'recordsets']), + append=['v2', 'zones', zone['id'], 'recordsets']), json=new_recordset, validate=dict(json=recordset)), ]) @@ -92,22 +80,10 @@ def test_create_recordset_exception(self): "links": {}, "metadata": { 'total_count': 1}}), - self.get_designate_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['zones'], - qs_elements=['name={0}'.format(zone['id'])]), - json={"zones": [zone]}), - self.get_designate_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['zones', zone['id']]), - json=zone), - self.get_designate_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'dns', 'public', - append=['zones', zone['id'], 'recordsets']), + append=['v2', 'zones', zone['id'], 'recordsets']), status_code=500, validate=dict(json={ 'name': 'www2.example.net.', @@ -115,8 +91,8 @@ def test_create_recordset_exception(self): 'type': 'A'})), ]) with testtools.ExpectedException( - shade.OpenStackCloudException, - "Unable to create recordset www2.example.net." + shade.exc.OpenStackCloudHTTPError, + "Error creating recordset www2.example.net." ): self.cloud.create_recordset('1', 'www2.example.net.', 'a', ['192.168.1.2']) @@ -138,58 +114,16 @@ def test_update_recordset(self): "links": {}, "metadata": { 'total_count': 1}}), - self.get_designate_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['zones'], - qs_elements=['name={0}'.format(zone['id'])]), - json={"zones": [zone]}), - self.get_designate_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['zones'], - qs_elements=['name={0}'.format(zone['id'])]), - json={"zones": [zone]}), - self.get_designate_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', - append=['zones', zone['id'], 'recordsets'], - qs_elements=['name={0}'.format( - new_recordset['id'])]), - json={"recordsets": [new_recordset]}), - self.get_designate_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'dns', 'public', - append=['zones', zone['id'], + append=['v2', 'zones', zone['id'], 'recordsets', new_recordset['id']]), - json={"recordsets": [new_recordset]}), - self.get_designate_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['zones'], - qs_elements=['name={0}'.format(zone['id'])]), - json={"zones": [zone]}), - self.get_designate_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['zones'], - qs_elements=['name={0}'.format(zone['id'])]), - json={"zones": [zone]}), - self.get_designate_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', - append=['zones', zone['id'], 'recordsets'], - qs_elements=['name={0}'.format( - new_recordset['id'])]), - json={"recordsets": [new_recordset]}), - self.get_designate_discovery_mock_dict(), + json=new_recordset), dict(method='PUT', uri=self.get_mock_url( 'dns', 'public', - append=['zones', zone['id'], + append=['v2', 'zones', zone['id'], 'recordsets', new_recordset['id']]), json=expected_recordset, validate=dict(json={'ttl': new_ttl})) @@ -208,102 +142,43 @@ def test_delete_recordset(self): "links": {}, "metadata": { 'total_count': 1}}), - self.get_designate_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['zones'], - qs_elements=['name={0}'.format(zone['id'])]), - json={"zones": [zone]}), - self.get_designate_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['zones'], - qs_elements=['name={0}'.format(zone['id'])]), - json={"zones": [zone]}), - self.get_designate_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', - append=['zones', zone['id'], 'recordsets'], - qs_elements=['name={0}'.format( - new_recordset['id'])]), - json={"recordsets": [new_recordset]}), - self.get_designate_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'dns', 'public', - append=['zones', zone['id'], + append=['v2', 'zones', zone['id'], 'recordsets', new_recordset['id']]), - json={"recordsets": [new_recordset]}), - self.get_designate_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['zones'], - qs_elements=['name={0}'.format(zone['id'])]), - json={"zones": [zone]}), - self.get_designate_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['zones'], - qs_elements=['name={0}'.format(zone['id'])]), - json={"zones": [zone]}), - self.get_designate_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', - append=['zones', zone['id'], 'recordsets'], - qs_elements=['name={0}'.format( - new_recordset['id'])]), - json={"recordsets": [new_recordset]}), - self.get_designate_discovery_mock_dict(), + json=new_recordset), dict(method='DELETE', uri=self.get_mock_url( 'dns', 'public', - append=['zones', zone['id'], + append=['v2', 'zones', zone['id'], 'recordsets', new_recordset['id']]), json={}) ]) self.assertTrue(self.cloud.delete_recordset('1', '1')) self.assert_calls() - def _prepare_get_recordset_calls(self, zone_id, name_or_id): + def test_get_recordset_by_id(self): self.register_uris([ - self.get_designate_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['zones'], - qs_elements=['name={0}'.format(zone['id'])]), - json={"zones": [zone]}), - self.get_designate_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['zones'], - qs_elements=['name={0}'.format(zone['id'])]), - json={"zones": [zone]}), - self.get_designate_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'dns', 'public', - append=['zones', zone['id'], 'recordsets'], - qs_elements=['name={0}'.format(name_or_id)]), - json={"recordsets": [new_recordset]}), - self.get_designate_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', - append=['zones', zone['id'], - 'recordsets', new_recordset['id']]), + append=['v2', 'zones', '1', 'recordsets', '1']), json=new_recordset), ]) - - def test_get_recordset_by_id(self): - recordset = self._prepare_get_recordset_calls('1', '1') recordset = self.cloud.get_recordset('1', '1') self.assertEqual(recordset['id'], '1') self.assert_calls() def test_get_recordset_by_name(self): - self._prepare_get_recordset_calls('1', new_recordset['name']) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', + append=['v2', 'zones', '1', 'recordsets', + new_recordset['name']]), + json=new_recordset), + ]) recordset = self.cloud.get_recordset('1', new_recordset['name']) self.assertEqual(new_recordset['name'], recordset['name']) self.assert_calls() @@ -311,24 +186,11 @@ def test_get_recordset_by_name(self): def test_get_recordset_not_found_returns_false(self): recordset_name = "www.nonexistingrecord.net." self.register_uris([ - self.get_designate_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['zones'], - qs_elements=['name={0}'.format(zone['id'])]), - json={"zones": [zone]}), - self.get_designate_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['zones'], - qs_elements=['name={0}'.format(zone['id'])]), - json={"zones": [zone]}), - self.get_designate_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'dns', 'public', - append=['zones', zone['id'], 'recordsets'], - qs_elements=['name={0}'.format(recordset_name)]), + append=['v2', 'zones', '1', 'recordsets', + recordset_name]), json=[]) ]) recordset = self.cloud.get_recordset('1', recordset_name) From c97bac299751775294e8abc5f01d1bfb7bb681cf Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 1 Jun 2017 16:46:27 -0500 Subject: [PATCH 1573/3836] Do less work when deleting a server and floating ips When we delete a server with floating ips, we tell get_server to not fetch a bare server, which does the work to fill in the network info from neutron. Then we look in the server for the floating ip address and look up the port it goes with. This is not necessary. We can tell get_server to get us a bare server, then look up floating ips by device_id. Then just delete them. Change-Id: I5ec04dc2a356aa20cf561866e8f43f9e28b2db21 --- shade/openstackcloud.py | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index e08758ab0..dfa5900b5 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5617,7 +5617,7 @@ def delete_server( :raises: OpenStackCloudException on operation error. """ # If delete_ips is True, we need the server to not be bare. - server = self.get_server(name_or_id, bare=not delete_ips) + server = self.get_server(name_or_id, bare=True) if not server: return False @@ -5635,22 +5635,11 @@ def _delete_server( return False if delete_ips: - # Don't pass public=True because we're just deleting. Testing - # for connectivity is not useful. - floating_ip = meta.get_server_ip(server, ext_tag='floating') - if floating_ip: - ips = self.search_floating_ips(filters={ - 'floating_ip_address': floating_ip}) - if len(ips) != 1: - raise OpenStackCloudException( - "Tried to delete floating ip {floating_ip}" - " associated with server {id} but there was" - " an error finding it. Something is exceptionally" - " broken.".format( - floating_ip=floating_ip, - id=server['id'])) + ips = self.search_floating_ips(filters={ + 'device_id': server['id']}) + for ip in ips: deleted = self.delete_floating_ip( - ips[0]['id'], retry=delete_ip_retry) + ip['id'], retry=delete_ip_retry) if not deleted: raise OpenStackCloudException( "Tried to delete floating ip {floating_ip}" From dc1e255dba8b23fcb400785bf3519fa7bc48e552 Mon Sep 17 00:00:00 2001 From: "Yuanbin.Chen" Date: Fri, 2 Jun 2017 10:43:48 +0800 Subject: [PATCH 1574/3836] Add cluster support receiver update operation Implements: blueprint add-receiver-update Change-Id: Idde34c4476e4087b4e0d22cced2847e0ee7b98b7 Signed-off-by: Yuanbin.Chen --- doc/source/users/proxies/cluster.rst | 1 + openstack/cluster/v1/_proxy.py | 12 ++++++++++++ openstack/cluster/v1/receiver.py | 3 +++ openstack/tests/unit/cluster/v1/test_proxy.py | 3 +++ openstack/tests/unit/cluster/v1/test_receiver.py | 2 +- 5 files changed, 20 insertions(+), 1 deletion(-) diff --git a/doc/source/users/proxies/cluster.rst b/doc/source/users/proxies/cluster.rst index fe5c502ef..6cbba8e5f 100644 --- a/doc/source/users/proxies/cluster.rst +++ b/doc/source/users/proxies/cluster.rst @@ -134,6 +134,7 @@ Receiver Operations .. autoclass:: openstack.cluster.v1._proxy.Proxy .. automethod:: openstack.cluster.v1._proxy.Proxy.create_receiver + .. automethod:: openstack.cluster.v1._proxy.Proxy.update_receiver .. automethod:: openstack.cluster.v1._proxy.Proxy.delete_receiver .. automethod:: openstack.cluster.v1._proxy.Proxy.get_receiver .. automethod:: openstack.cluster.v1._proxy.Proxy.find_receiver diff --git a/openstack/cluster/v1/_proxy.py b/openstack/cluster/v1/_proxy.py index 903caaaf2..90ee87ab7 100644 --- a/openstack/cluster/v1/_proxy.py +++ b/openstack/cluster/v1/_proxy.py @@ -899,6 +899,18 @@ def create_receiver(self, **attrs): """ return self._create(_receiver.Receiver, **attrs) + def update_receiver(self, receiver, **attrs): + """Update a receiver. + + :param receiver: The value can be either the name or ID of a receiver + or a :class:`~openstack.cluster.v1.receiver.Receiver` instance. + :param attrs: The attributes to update on the receiver parameter. + Valid attribute names include ``name``, ``action`` and ``params``. + :returns: The updated receiver. + :rtype: :class:`~openstack.cluster.v1.receiver.Receiver` + """ + return self._update(_receiver.Receiver, receiver, **attrs) + def delete_receiver(self, receiver, ignore_missing=True): """Delete a receiver. diff --git a/openstack/cluster/v1/receiver.py b/openstack/cluster/v1/receiver.py index af9f1367f..d69400489 100644 --- a/openstack/cluster/v1/receiver.py +++ b/openstack/cluster/v1/receiver.py @@ -24,8 +24,11 @@ class Receiver(resource.Resource): allow_list = True allow_get = True allow_create = True + allow_update = True allow_delete = True + patch_update = True + _query_mapping = resource.QueryParameters( 'name', 'type', 'cluster_id', 'action', 'sort', 'global_project', user_id='user') diff --git a/openstack/tests/unit/cluster/v1/test_proxy.py b/openstack/tests/unit/cluster/v1/test_proxy.py index 0e7992d34..82246c8f6 100644 --- a/openstack/tests/unit/cluster/v1/test_proxy.py +++ b/openstack/tests/unit/cluster/v1/test_proxy.py @@ -461,6 +461,9 @@ def test_get_cluster_policy(self): def test_receiver_create(self): self.verify_create(self.proxy.create_receiver, receiver.Receiver) + def test_receiver_update(self): + self.verify_update(self.proxy.update_receiver, receiver.Receiver) + def test_receiver_delete(self): self.verify_delete(self.proxy.delete_receiver, receiver.Receiver, False) diff --git a/openstack/tests/unit/cluster/v1/test_receiver.py b/openstack/tests/unit/cluster/v1/test_receiver.py index b566c7976..bac16f006 100644 --- a/openstack/tests/unit/cluster/v1/test_receiver.py +++ b/openstack/tests/unit/cluster/v1/test_receiver.py @@ -53,7 +53,7 @@ def test_basic(self): self.assertEqual('clustering', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertTrue(sot.allow_update) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) From 10cb4f2d592908b74ca92cd1e61d21ab6b6dbab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Fri, 2 Jun 2017 16:48:03 +0000 Subject: [PATCH 1575/3836] Remove designate client from shade's dependencies All calls to Designate API are now done with REST calls made via keystoneauth1 Shade don't require python-designateclient package since now. Change-Id: I55393dc04ec9eeaba885fee849e6466f199be479 --- extras/install-tips.sh | 1 - requirements.txt | 3 --- shade/_legacy_clients.py | 2 +- shade/tests/unit/base.py | 15 --------------- 4 files changed, 1 insertion(+), 20 deletions(-) diff --git a/extras/install-tips.sh b/extras/install-tips.sh index 7af026d8b..50c2fd8da 100644 --- a/extras/install-tips.sh +++ b/extras/install-tips.sh @@ -18,7 +18,6 @@ for lib in \ python-novaclient \ python-keystoneclient \ python-ironicclient \ - python-designateclient \ os-client-config \ keystoneauth do diff --git a/requirements.txt b/requirements.txt index 6a6bdab10..10eaa12dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,8 +14,6 @@ os-client-config>=1.27.0 # Apache-2.0 # Babel can be removed when ironicclient is removed (because of openstackclient # transitive depend) Babel!=2.4.0,>=2.3.4 # BSD -# requests can be removed when designateclient is removed -requests!=2.12.2,!=2.13.0,>=2.10.0 # Apache-2.0 requestsexceptions>=1.2.0 # Apache-2.0 six>=1.9.0 # MIT futures>=3.0;python_version=='2.7' or python_version=='2.6' # BSD @@ -26,6 +24,5 @@ netifaces>=0.10.4 # MIT python-novaclient>=7.1.0 # Apache-2.0 python-keystoneclient>=3.8.0 # Apache-2.0 python-ironicclient>=1.11.0 # Apache-2.0 -python-designateclient>=1.5.0 # Apache-2.0 dogpile.cache>=0.6.2 # BSD diff --git a/shade/_legacy_clients.py b/shade/_legacy_clients.py index f34261593..cbf807bcd 100644 --- a/shade/_legacy_clients.py +++ b/shade/_legacy_clients.py @@ -86,7 +86,7 @@ def cinder_client(self): @property def designate_client(self): - return self._create_legacy_client('designate', 'dns', deprecated=False) + return self._create_legacy_client('designate', 'dns') @property def keystone_client(self): diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index c0d635d97..42ce66831 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -477,12 +477,6 @@ def get_glance_discovery_mock_dict( status_code=300, text=open(discovery_fixture, 'r').read()) - def get_designate_discovery_mock_dict(self): - discovery_fixture = os.path.join( - self.fixtures_directory, "dns.json") - return dict(method='GET', uri="https://dns.example.com/", - text=open(discovery_fixture, 'r').read()) - def use_glance(self, image_version_json='image-version.json'): # NOTE(notmorgan): This method is only meant to be used in "setUp" # where the ordering of the url being registered is tightly controlled @@ -492,15 +486,6 @@ def use_glance(self, image_version_json='image-version.json'): self.__do_register_uris([ self.get_glance_discovery_mock_dict(image_version_json)]) - def use_designate(self): - # NOTE(slaweq): This method is only meant to be used in "setUp" - # where the ordering of the url being registered is tightly controlled - # if the functionality of .use_designate is meant to be used during an - # actual test case, use .get_designate_discovery_mock and apply to the - # right location in the mock_uris when calling .register_uris - self.__do_register_uris([ - self.get_designate_discovery_mock_dict()]) - def register_uris(self, uri_mock_list=None): """Mock a list of URIs and responses via requests mock. From 593e15bbf3aa89b652b98a6dd4e6b06eaf30c466 Mon Sep 17 00:00:00 2001 From: Vu Cong Tuan Date: Sat, 3 Jun 2017 12:42:02 +0700 Subject: [PATCH 1576/3836] Replace assertRaisesRegexp with assertRaisesRegex assertRaisesRegexp was renamed to assertRaisesRegex in Py3.2 For more details, please check: https://docs.python.org/3/library/ unittest.html#unittest.TestCase.assertRaisesRegex Change-Id: I183178728c9a6c09a00d2a425d194308b2036997 Closes-Bug: #1436957 --- openstack/tests/unit/test_proxy.py | 16 ++++++++-------- openstack/tests/unit/test_proxy2.py | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index 311bd6439..ec4f68075 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -68,9 +68,9 @@ def test_notstrict_id(self): def test_strict_id(self): decorated = proxy._check_resource(strict=True)(self.sot.method) - self.assertRaisesRegexp(ValueError, "A Resource must be passed", - decorated, self.sot, resource.Resource, - "this-is-not-a-resource") + self.assertRaisesRegex(ValueError, "A Resource must be passed", + decorated, self.sot, resource.Resource, + "this-is-not-a-resource") def test_incorrect_resource(self): class OneType(resource.Resource): @@ -81,9 +81,9 @@ class AnotherType(resource.Resource): value = AnotherType() decorated = proxy._check_resource(strict=False)(self.sot.method) - self.assertRaisesRegexp(ValueError, - "Expected OneType but received AnotherType", - decorated, self.sot, OneType, value) + self.assertRaisesRegex(ValueError, + "Expected OneType but received AnotherType", + decorated, self.sot, OneType, value) class TestProxyDelete(testtools.TestCase): @@ -127,7 +127,7 @@ def test_delete_ResourceNotFound(self): self.res.delete.side_effect = exceptions.NotFoundException( message="test", http_status=404) - self.assertRaisesRegexp( + self.assertRaisesRegex( exceptions.ResourceNotFound, "No %s found for %s" % (DeleteableResource.__name__, self.res), self.sot._delete, DeleteableResource, self.res, @@ -239,7 +239,7 @@ def test_get_not_found(self): self.res.get.side_effect = exceptions.NotFoundException( message="test", http_status=404) - self.assertRaisesRegexp( + self.assertRaisesRegex( exceptions.ResourceNotFound, "No %s found for %s" % (RetrieveableResource.__name__, self.res), self.sot._get, RetrieveableResource, self.res) diff --git a/openstack/tests/unit/test_proxy2.py b/openstack/tests/unit/test_proxy2.py index 0e6dc5680..332012811 100644 --- a/openstack/tests/unit/test_proxy2.py +++ b/openstack/tests/unit/test_proxy2.py @@ -70,9 +70,9 @@ def test__check_resource_notstrict_id(self): def test__check_resource_strict_id(self): decorated = proxy2._check_resource(strict=True)(self.sot.method) - self.assertRaisesRegexp(ValueError, "A Resource must be passed", - decorated, self.sot, resource2.Resource, - "this-is-not-a-resource") + self.assertRaisesRegex(ValueError, "A Resource must be passed", + decorated, self.sot, resource2.Resource, + "this-is-not-a-resource") def test__check_resource_incorrect_resource(self): class OneType(resource2.Resource): @@ -83,9 +83,9 @@ class AnotherType(resource2.Resource): value = AnotherType() decorated = proxy2._check_resource(strict=False)(self.sot.method) - self.assertRaisesRegexp(ValueError, - "Expected OneType but received AnotherType", - decorated, self.sot, OneType, value) + self.assertRaisesRegex(ValueError, + "Expected OneType but received AnotherType", + decorated, self.sot, OneType, value) def test__get_uri_attribute_no_parent(self): class Child(resource2.Resource): @@ -199,7 +199,7 @@ def test_delete_ResourceNotFound(self): self.res.delete.side_effect = exceptions.NotFoundException( message="test", http_status=404) - self.assertRaisesRegexp( + self.assertRaisesRegex( exceptions.ResourceNotFound, "No %s found for %s" % (DeleteableResource.__name__, self.res), self.sot._delete, DeleteableResource, self.res, @@ -312,7 +312,7 @@ def test_get_not_found(self): self.res.get.side_effect = exceptions.NotFoundException( message="test", http_status=404) - self.assertRaisesRegexp( + self.assertRaisesRegex( exceptions.ResourceNotFound, "No %s found for %s" % (RetrieveableResource.__name__, self.res), self.sot._get, RetrieveableResource, self.res) From 751b0a0411941490e5d1be678e0a3ca4ad1b8db5 Mon Sep 17 00:00:00 2001 From: Jakub Jursa Date: Mon, 5 Jun 2017 15:16:50 +0200 Subject: [PATCH 1577/3836] Pass hints to Cinder scheduler in create_volume This commit adds support for passing 'scheduler_hints' as kwarg in create_volume. Change-Id: I85915fab03f2bff77a21802791f2f6a7579815f2 --- shade/openstackcloud.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 25aa5a690..c0c1c49f4 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3764,9 +3764,13 @@ def create_volume( kwargs['imageRef'] = image_obj['id'] kwargs = self._get_volume_kwargs(kwargs) kwargs['size'] = size + payload = dict(volume=kwargs) + if 'scheduler_hints' in kwargs: + payload['OS-SCH-HNT:scheduler_hints'] = kwargs.pop( + 'scheduler_hints', None) with _utils.shade_exceptions("Error in creating volume"): volume = self._volume_client.post( - '/volumes', json=dict(volume=kwargs)) + '/volumes', json=dict(payload)) self.list_volumes.invalidate(self) if volume['status'] == 'error': From a45848d5b97270685904653635b2bb4b248d7139 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 4 Jun 2017 10:34:46 -0500 Subject: [PATCH 1578/3836] Generalize version discovery for re-use This will eventually all push down into keystoneauth, but for now we need it in a few places. Let's just make one method rather than cutting and pasting it for each service. Change-Id: I190f3abfc403af337d671e8c420915c009964cf3 --- shade/openstackcloud.py | 133 +++++++++++++++++++++++----------------- 1 file changed, 76 insertions(+), 57 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 25aa5a690..9404422c7 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -419,85 +419,109 @@ def _raw_image_client(self): self._raw_clients['raw-image'] = image_client return self._raw_clients['raw-image'] - def _detect_image_verison_from_url(self, image_url): - if image_url.endswith('/'): - image_url = image_url[:-1] - version = image_url.split('/')[-1] - if version.startswith('v'): - return version[1] + def _get_version_from_endpoint(self, endpoint): + endpoint_parts = endpoint.rstrip('/').split('/') + if endpoint_parts[-1].endswith(self.current_project_id): + endpoint_parts.pop() + if endpoint_parts[-1].startswith('v'): + return endpoint_parts[-1][1] return None - def _match_given_image_endpoint(self, image_url, version): - url_version = self._detect_image_verison_from_url(image_url) - if url_version and version == url_version: + def _match_given_endpoint(self, given, version): + given_version = self._get_version_from_endpoint(given) + if given_version and version == given_version: return True return False - def _discover_image_endpoint(self, config_version, image_client): + def _discover_endpoint(self, service_type, version_required=False): + # If endpoint_override is set, do nothing + service_endpoint = self.cloud_config.get_endpoint(service_type) + if service_endpoint: + return service_endpoint + + client = self._get_raw_client(service_type) + config_version = self.cloud_config.get_api_version(service_type) + + # shade only groks major versions at the moment + if config_version: + config_version = config_version[0] + # First - quick check to see if the endpoint in the catalog # is a versioned endpoint that matches the version we requested. # If it is, don't do any additoinal work. - catalog_endpoint = image_client.get_endpoint() - if self._match_given_image_endpoint( + catalog_endpoint = client.get_endpoint() + if self._match_given_endpoint( catalog_endpoint, config_version): return catalog_endpoint - api_version_id = None + + # Ok, do version discovery + candidate_endpoints = None + version_key = '{service}_api_version'.format(service=service_type) try: - api_version = None - # Version discovery - versions = image_client.get('/') - if config_version.startswith('1'): - api_version = [ + versions = client.get('/') + if 'values' in versions: + versions = versions['values'] + if isinstance(versions, dict): + versions = [versions] + if config_version: + candidate_endpoints = [ version for version in versions - if version['id'] in ('v1.0', 'v1.1')] - if api_version: - api_version = api_version[0] - if not api_version: - api_version = [ + if version['id'][1] == config_version] + if not candidate_endpoints: + candidate_endpoints = [ version for version in versions - if version['status'] == 'CURRENT'][0] - - image_url = api_version['links'][0]['href'] - # Set the id to the first digit after the v - api_version_id = api_version['id'][1] - except Exception as e: + if version['status'] in ('CURRENT', 'stable')] + if not candidate_endpoints: + candidate_endpoints = versions + except (keystoneauth1.exceptions.connection.ConnectFailure, + OpenStackCloudURINotFound) as e: # A 404 or a connection error is a likely thing to get - # either with a misconfgured glance. or we've already + # either with a misconfgured service. or we've already # gotten a versioned endpoint from the catalog self.log.debug( - "Glance version discovery failed, assuming endpoint in" + "Version discovery failed, assuming endpoint in" " the catalog is already versioned. {e}".format(e=str(e))) - image_url = catalog_endpoint - api_version_id = self._detect_image_verison_from_url(image_url) - if not api_version_id: - # We couldn't detect anything, assume config is correct and - # catalog is correct - return image_url + if candidate_endpoints: + endpoint_description = candidate_endpoints[0] + service_endpoint = [ + link['href'] for link in endpoint_description['links'] + if link['rel'] == 'self'][0] + api_version = endpoint_description['id'][1] + else: + # Can't discover a version. Do best-attempt at inferring + # version from URL so that later logic can do its best + api_version = self._get_version_from_endpoint() + if not api_version: + if not config_version and version_required: + raise OpenStackCloudException( + "No version for {service_type} could be detected," + " and also {version_key} is not set. It is impossible" + " to continue without knowing what version this" + " service is.") + if not config_version: + return catalog_endpoint + api_version = config_version + service_endpoint = catalog_endpoint # If we detect a different version that was configured, # set the version in occ because we have logic elsewhere # that is different depending on which version we're using - warning_msg = None - config_version_id = config_version[0] - if config_version_id != api_version_id: - self.cloud_config.config['image_api_version'] = api_version_id + if config_version != api_version: warning_msg = ( - 'image_api_version is {config_version_id} but only' - ' {api_version_id} is available.'.format( - config_version_id=config_version_id, - api_version_id=api_version_id)) - if warning_msg: + '{version_key} is {config_version}' + ' but only {api_version} is available.'.format( + version_key=version_key, + config_version=config_version, + api_version=api_version)) self.log.debug(warning_msg) warnings.warn(warning_msg) - - if catalog_endpoint == image_url: - return catalog_endpoint + self.cloud_config.config[version_key] = api_version # Sometimes version discovery documents have broken endpoints, but # the catalog has good ones (what?) - catalog_endpoint = urllib.parse.urlparse(catalog_endpoint) - discovered_endpoint = urllib.parse.urlparse(image_url) + catalog_endpoint = urllib.parse.urlparse(client.get_endpoint()) + discovered_endpoint = urllib.parse.urlparse(service_endpoint) return urllib.parse.ParseResult( catalog_endpoint.scheme, @@ -510,13 +534,8 @@ def _discover_image_endpoint(self, config_version, image_client): @property def _image_client(self): if 'image' not in self._raw_clients: - # Get configured api version for downgrades - config_version = self.cloud_config.get_api_version('image') image_client = self._get_raw_client('image') - image_url = self.cloud_config.config.get('image_endpoint_override') - if not image_url: - image_url = self._discover_image_endpoint( - config_version, image_client) + image_url = self._discover_endpoint('image', version_required=True) image_client.endpoint_override = image_url self._raw_clients['image'] = image_client return self._raw_clients['image'] From 641073bba93e90e4373e08e7113d7de853355ab9 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 4 Jun 2017 23:30:01 -0500 Subject: [PATCH 1579/3836] Migrate dns to new discovery method Change-Id: I66cdb189a87e94df35fbdb720c48e59f24ca51cb --- shade/openstackcloud.py | 24 ++++++++++++++---------- shade/tests/unit/base.py | 15 +++++++++++++++ shade/tests/unit/test_recordset.py | 4 ++++ shade/tests/unit/test_zone.py | 4 ++++ 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 9404422c7..5f2cacfaf 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -403,7 +403,11 @@ def _database_client(self): @property def _dns_client(self): if 'dns' not in self._raw_clients: - self._raw_clients['dns'] = self._get_raw_client('dns') + dns_client = self._get_raw_client('dns') + dns_url = self._discover_endpoint( + 'dns', version_required=True) + dns_client.endpoint_override = dns_url + self._raw_clients['dns'] = dns_client return self._raw_clients['dns'] @property @@ -6948,7 +6952,7 @@ def list_zones(self): """ return self._dns_client.get( - "/v2/zones", + "/zones", error_message="Error fetching zones list") def get_zone(self, name_or_id, filters=None): @@ -7012,7 +7016,7 @@ def create_zone(self, name, zone_type=None, email=None, description=None, zone["masters"] = masters return self._dns_client.post( - "/v2/zones", json=zone, + "/zones", json=zone, error_message="Unable to create zone {name}".format(name=name)) @_utils.valid_kwargs('email', 'description', 'ttl', 'masters') @@ -7037,7 +7041,7 @@ def update_zone(self, name_or_id, **kwargs): "Zone %s not found." % name_or_id) return self._dns_client.patch( - "/v2/zones/{zone_id}".format(zone_id=zone['id']), json=kwargs, + "/zones/{zone_id}".format(zone_id=zone['id']), json=kwargs, error_message="Error updating zone {0}".format(name_or_id)) def delete_zone(self, name_or_id): @@ -7056,7 +7060,7 @@ def delete_zone(self, name_or_id): return False return self._dns_client.delete( - "/v2/zones/{zone_id}".format(zone_id=zone['id']), + "/zones/{zone_id}".format(zone_id=zone['id']), error_message="Error deleting zone {0}".format(name_or_id)) return True @@ -7070,7 +7074,7 @@ def list_recordsets(self, zone): """ return self._dns_client.get( - "/v2/zones/{zone_id}/recordsets".format(zone_id=zone), + "/zones/{zone_id}/recordsets".format(zone_id=zone), error_message="Error fetching recordsets list") def get_recordset(self, zone, name_or_id): @@ -7085,7 +7089,7 @@ def get_recordset(self, zone, name_or_id): """ try: return self._dns_client.get( - "/v2/zones/{zone_id}/recordsets/{recordset_id}".format( + "/zones/{zone_id}/recordsets/{recordset_id}".format( zone_id=zone, recordset_id=name_or_id), error_message="Error fetching recordset") except Exception: @@ -7131,7 +7135,7 @@ def create_recordset(self, zone, name, recordset_type, records, body['ttl'] = ttl return self._dns_client.post( - "/v2/zones/{zone_id}/recordsets".format(zone_id=zone), + "/zones/{zone_id}/recordsets".format(zone_id=zone), json=body, error_message="Error creating recordset {name}".format(name=name)) @@ -7160,7 +7164,7 @@ def update_recordset(self, zone, name_or_id, **kwargs): "Recordset %s not found." % name_or_id) new_recordset = self._dns_client.put( - "/v2/zones/{zone_id}/recordsets/{recordset_id}".format( + "/zones/{zone_id}/recordsets/{recordset_id}".format( zone_id=zone_obj['id'], recordset_id=name_or_id), json=kwargs, error_message="Error updating recordset {0}".format(name_or_id)) @@ -7188,7 +7192,7 @@ def delete_recordset(self, zone, name_or_id): return False self._dns_client.delete( - "/v2/zones/{zone_id}/recordsets/{recordset_id}".format( + "/zones/{zone_id}/recordsets/{recordset_id}".format( zone_id=zone['id'], recordset_id=name_or_id), error_message="Error deleting recordset {0}".format(name_or_id)) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 42ce66831..c0d635d97 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -477,6 +477,12 @@ def get_glance_discovery_mock_dict( status_code=300, text=open(discovery_fixture, 'r').read()) + def get_designate_discovery_mock_dict(self): + discovery_fixture = os.path.join( + self.fixtures_directory, "dns.json") + return dict(method='GET', uri="https://dns.example.com/", + text=open(discovery_fixture, 'r').read()) + def use_glance(self, image_version_json='image-version.json'): # NOTE(notmorgan): This method is only meant to be used in "setUp" # where the ordering of the url being registered is tightly controlled @@ -486,6 +492,15 @@ def use_glance(self, image_version_json='image-version.json'): self.__do_register_uris([ self.get_glance_discovery_mock_dict(image_version_json)]) + def use_designate(self): + # NOTE(slaweq): This method is only meant to be used in "setUp" + # where the ordering of the url being registered is tightly controlled + # if the functionality of .use_designate is meant to be used during an + # actual test case, use .get_designate_discovery_mock and apply to the + # right location in the mock_uris when calling .register_uris + self.__do_register_uris([ + self.get_designate_discovery_mock_dict()]) + def register_uris(self, uri_mock_list=None): """Mock a list of URIs and responses via requests mock. diff --git a/shade/tests/unit/test_recordset.py b/shade/tests/unit/test_recordset.py index 0583c0ed8..044499aa5 100644 --- a/shade/tests/unit/test_recordset.py +++ b/shade/tests/unit/test_recordset.py @@ -43,6 +43,10 @@ class TestRecordset(base.RequestsMockTestCase): + def setUp(self): + super(TestRecordset, self).setUp() + self.use_designate() + def test_create_recordset(self): self.register_uris([ dict(method='GET', diff --git a/shade/tests/unit/test_zone.py b/shade/tests/unit/test_zone.py index 3d481364c..960a81534 100644 --- a/shade/tests/unit/test_zone.py +++ b/shade/tests/unit/test_zone.py @@ -31,6 +31,10 @@ class TestZone(base.RequestsMockTestCase): + def setUp(self): + super(TestZone, self).setUp() + self.use_designate() + def test_create_zone(self): self.register_uris([ dict(method='POST', From 17d88b08280075e6b27e70a71c010595cf56eca2 Mon Sep 17 00:00:00 2001 From: Daniel Speichert Date: Tue, 6 Jun 2017 00:10:13 -0400 Subject: [PATCH 1580/3836] Fix py3 compatibility (dict.iteritems()) in object_store Change-Id: I50900ba74d8e3d1d913871f33921eb1111a1d983 Closes-Bug:#1696040 --- openstack/object_store/v1/obj.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openstack/object_store/v1/obj.py b/openstack/object_store/v1/obj.py index 1b93e8e84..c0fac8c6a 100644 --- a/openstack/object_store/v1/obj.py +++ b/openstack/object_store/v1/obj.py @@ -12,6 +12,7 @@ # under the License. import copy +import six from openstack.object_store import object_store_service from openstack.object_store.v1 import _base @@ -167,7 +168,7 @@ def set_metadata(self, session, metadata): # Filter out items with empty values so the create metadata behaviour # is the same as account and container filtered_metadata = \ - {key: value for key, value in metadata.iteritems() if value} + {key: value for key, value in six.iteritems(metadata) if value} # Get a copy of the original metadata so it doesn't get erased on POST # and update it with the new metadata values. From 076a407eac78d1d04efa65f5ad660da6f5a3c5de Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 6 Jun 2017 05:59:39 +0000 Subject: [PATCH 1581/3836] Updated from global requirements Change-Id: I5b98af79293aae245f2e37eb585ed87b9153c940 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 2241f2af7..5abe62d28 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -13,4 +13,4 @@ sphinx!=1.6.1,>=1.5.1 # BSD testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=1.4.0 # MIT -reno>=1.8.0 # Apache-2.0 +reno!=2.3.1,>=1.8.0 # Apache-2.0 From 07384cb8d4fe7fb153607f5062a9856fb8d576b3 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 6 Jun 2017 12:25:13 +0000 Subject: [PATCH 1582/3836] Updated from global requirements Change-Id: Ic7bb0f4928f225f47fec07385871fadc3d5f368b --- test-requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index e16cb83f3..70d58f1d5 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,7 +4,7 @@ hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 beautifulsoup4 # MIT -coverage>=4.0 # Apache-2.0 +coverage!=4.4,>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD mock>=2.0 # BSD python-subunit>=0.0.18 # Apache-2.0/BSD @@ -12,7 +12,7 @@ openstackdocstheme>=1.5.0 # Apache-2.0 os-testr>=0.8.0 # Apache-2.0 requests!=2.12.2,!=2.13.0,>=2.10.0 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 -sphinx>=1.5.1 # BSD +sphinx!=1.6.1,>=1.5.1 # BSD testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=1.4.0 # MIT From 96c24b1108f5eca3d679a6b17a29c475a433c2e5 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 6 Jun 2017 09:39:35 -0500 Subject: [PATCH 1583/3836] Use shade discovery for keystone Use the new discovery method for identity client. HOWEVER - also use it for ksc by setting an endpoint_override. This prevents us from having multiple competing discovery stacks while we transition from ksc to ksa. discovery is still less optimized. Our discovery isn't making use of the ksa discovery cache so it double discovers identity. We'll fix that next. Change-Id: Ieae669c1ea1155434a642dbc89f78f85a3563567 --- shade/_legacy_clients.py | 2 ++ shade/openstackcloud.py | 6 ++++++ shade/tests/unit/base.py | 21 ++++++--------------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/shade/_legacy_clients.py b/shade/_legacy_clients.py index cbf807bcd..715108b9d 100644 --- a/shade/_legacy_clients.py +++ b/shade/_legacy_clients.py @@ -90,6 +90,8 @@ def designate_client(self): @property def keystone_client(self): + # Trigger discovery from ksa + self._identity_client return self._create_legacy_client( 'keystone', 'identity', deprecated=False) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 5f2cacfaf..75706e7d9 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -413,6 +413,12 @@ def _dns_client(self): @property def _identity_client(self): if 'identity' not in self._raw_clients: + identity_client = self._get_raw_client('identity') + identity_url = self._discover_endpoint( + 'identity', version_required=True) + identity_client.endpoint_override = identity_url + self.cloud_config.config['identity_endpoint_override'] = \ + identity_url self._raw_clients['identity'] = self._get_raw_client('identity') return self._raw_clients['identity'] diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index c0d635d97..d12ea341c 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -17,7 +17,6 @@ import time import uuid -from distutils import version as du_version import fixtures import mock import os @@ -414,7 +413,7 @@ def use_keystone_v3(self, catalog='catalog-v3.json'): 'X-Subject-Token': self.getUniqueString('KeystoneToken')}, text=open(os.path.join( self.fixtures_directory, catalog), 'r').read() - ) + ), ]) self._make_test_cloud(identity_api_version='3') @@ -423,15 +422,6 @@ def use_keystone_v2(self): self.calls = [] self._uri_registry.clear() - # occ > 1.26.0 fixes keystoneclient construction. Unfortunately, it - # breaks our mocking of what keystoneclient does here. Since we're - # close to just getting rid of ksc anyway, just put in a version match - occ_version = du_version.StrictVersion(occ.__version__) - if occ_version > du_version.StrictVersion('1.26.0'): - endpoint_uri = 'https://identity.example.com/v2.0' - else: - endpoint_uri = 'https://identity.example.com/' - self.__do_register_uris([ dict(method='GET', uri='https://identity.example.com/', text=open(self.discovery_json, 'r').read()), @@ -439,10 +429,8 @@ def use_keystone_v2(self): text=open(os.path.join( self.fixtures_directory, 'catalog-v2.json'), 'r').read() ), - dict(method='GET', uri=endpoint_uri, + dict(method='GET', uri='https://identity.example.com/v2.0', text=open(self.discovery_json, 'r').read()), - dict(method='GET', uri='https://identity.example.com/', - text=open(self.discovery_json, 'r').read()) ]) self._make_test_cloud(cloud_name='_test_cloud_v2_', @@ -456,7 +444,10 @@ def _add_discovery_uri_call(self): # trips. self.__do_register_uris([ dict(method='GET', uri='https://identity.example.com/', - text=open(self.discovery_json, 'r').read())]) + text=open(self.discovery_json, 'r').read()), + dict(method='GET', uri='https://identity.example.com/v3/', + text=open(self.discovery_json, 'r').read()), + ]) def _make_test_cloud(self, cloud_name='_test_cloud_', **kwargs): test_cloud = os.environ.get('SHADE_OS_CLOUD', cloud_name) From 5a1a3d813b89e19f3f85da1a6ac363ae759c4f53 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 6 Jun 2017 10:40:22 -0500 Subject: [PATCH 1584/3836] Avoid keystoneclient making yet another discovery call Even with an endpoint_override set, keystoneclient will make one discovery call if the keystoneclient.client.Client constructor is used. Because we're not going to make that extra discovery call after we've already made a discovery call when we do ksa calls, directly instantiate the v2 or v3 objects ourselves. This lets us avoid the extra discovery call so that we can remove it from our test fixtures. The discovery call sequence now should be identical for ksa vs. ksc. There is still an inefficiency here due to our discovery not using ksa discovery. But we can optimize that later. Change-Id: If7f8625745f80334b9a78d742dc4a9250a4eb72c --- shade/_legacy_clients.py | 17 ++++++++++++++++- shade/openstackcloud.py | 2 +- shade/tests/unit/base.py | 4 ---- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/shade/_legacy_clients.py b/shade/_legacy_clients.py index 715108b9d..4b5ec1066 100644 --- a/shade/_legacy_clients.py +++ b/shade/_legacy_clients.py @@ -92,8 +92,23 @@ def designate_client(self): def keystone_client(self): # Trigger discovery from ksa self._identity_client + + # Skip broken discovery in ksc. We're good thanks. + from keystoneclient.v2_0 import client as v2_client + from keystoneclient.v3 import client as v3_client + if self.cloud_config.config['identity_api_version'] == '3': + client_class = v3_client + else: + client_class = v2_client + return self._create_legacy_client( - 'keystone', 'identity', deprecated=False) + 'keystone', 'identity', + client_class=client_class.Client, + deprecated=False, + endpoint=self.cloud_config.config[ + 'identity_endpoint_override'], + endpoint_override=self.cloud_config.config[ + 'identity_endpoint_override']) # Set the ironic API microversion to a known-good # supported/tested with the contents of shade. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 75706e7d9..fd230a94c 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -419,7 +419,7 @@ def _identity_client(self): identity_client.endpoint_override = identity_url self.cloud_config.config['identity_endpoint_override'] = \ identity_url - self._raw_clients['identity'] = self._get_raw_client('identity') + self._raw_clients['identity'] = identity_client return self._raw_clients['identity'] @property diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index d12ea341c..4115eebd5 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -429,8 +429,6 @@ def use_keystone_v2(self): text=open(os.path.join( self.fixtures_directory, 'catalog-v2.json'), 'r').read() ), - dict(method='GET', uri='https://identity.example.com/v2.0', - text=open(self.discovery_json, 'r').read()), ]) self._make_test_cloud(cloud_name='_test_cloud_v2_', @@ -445,8 +443,6 @@ def _add_discovery_uri_call(self): self.__do_register_uris([ dict(method='GET', uri='https://identity.example.com/', text=open(self.discovery_json, 'r').read()), - dict(method='GET', uri='https://identity.example.com/v3/', - text=open(self.discovery_json, 'r').read()), ]) def _make_test_cloud(self, cloud_name='_test_cloud_', **kwargs): From 752d01eb270c24a9058060be25cc51112669df25 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 6 Jun 2017 15:18:56 -0500 Subject: [PATCH 1585/3836] Add links to user list dict This is what keystone returns, so it's what our mock should return too. Change-Id: I7d4b428a48d3f4779994f4ac7d41744cf74d549d --- shade/tests/unit/test_users.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/shade/tests/unit/test_users.py b/shade/tests/unit/test_users.py index cb714537f..18b09f334 100644 --- a/shade/tests/unit/test_users.py +++ b/shade/tests/unit/test_users.py @@ -28,6 +28,19 @@ def _get_keystone_mock_url(self, resource, append=None, v3=True): service_type='identity', interface='admin', resource=resource, append=append, base_url_append=base_url_append) + def _get_user_list(self, user_data): + uri = self._get_keystone_mock_url(resource='users') + return { + 'users': [ + user_data.json_response['user'], + ], + 'links': { + 'self': uri, + 'previous': None, + 'next': None, + } + } + def test_create_user_v2(self): self.use_keystone_v2() @@ -99,7 +112,7 @@ def test_update_user_password_v2(self): # but is always chained together [keystoneclient oddity] # GET user info after user update dict(method='GET', uri=mock_users_uri, status_code=200, - json={'users': [user_data.json_response['user']]}), + json=self._get_user_list(user_data)), dict(method='GET', uri=mock_user_resource_uri, status_code=200, json=user_data.json_response), dict(method='PUT', @@ -145,7 +158,7 @@ def test_delete_user(self): dict(method='GET', uri=self._get_keystone_mock_url(resource='users'), status_code=200, - json={'users': [user_data.json_response['user']]}), + json=self._get_user_list(user_data)), dict(method='GET', uri=user_resource_uri, status_code=200, json=user_data.json_response), dict(method='DELETE', uri=user_resource_uri, status_code=204)]) @@ -170,7 +183,7 @@ def test_add_user_to_group(self): dict(method='GET', uri=self._get_keystone_mock_url(resource='users'), status_code=200, - json={'users': [user_data.json_response['user']]}), + json=self._get_user_list(user_data)), dict(method='GET', uri=self._get_keystone_mock_url(resource='groups'), status_code=200, @@ -192,7 +205,7 @@ def test_is_user_in_group(self): dict(method='GET', uri=self._get_keystone_mock_url(resource='users'), status_code=200, - json={'users': [user_data.json_response['user']]}), + json=self._get_user_list(user_data)), dict(method='GET', uri=self._get_keystone_mock_url(resource='groups'), status_code=200, @@ -215,7 +228,7 @@ def test_remove_user_from_group(self): self.register_uris([ dict(method='GET', uri=self._get_keystone_mock_url(resource='users'), - json={'users': [user_data.json_response['user']]}), + json=self._get_user_list(user_data)), dict(method='GET', uri=self._get_keystone_mock_url(resource='groups'), status_code=200, From ba36d445a71fb1c98168240a6a284e75154039f6 Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Wed, 24 May 2017 14:39:42 -0700 Subject: [PATCH 1586/3836] De-client-ify User Ops Remove keystoneclient from some user CRUD ops. Change-Id: I95ab2b58e2895c7735cc5a65dbf401bbd7715ccf --- shade/_tasks.py | 15 --------------- shade/openstackcloud.py | 27 +++++++++++++++++---------- 2 files changed, 17 insertions(+), 25 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 4584de15d..c62bf89d5 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -17,21 +17,11 @@ from shade import task_manager -class UserList(task_manager.Task): - def main(self, client): - return client.keystone_client.users.list() - - class UserCreate(task_manager.Task): def main(self, client): return client.keystone_client.users.create(**self.args) -class UserDelete(task_manager.Task): - def main(self, client): - return client.keystone_client.users.delete(**self.args) - - class UserUpdate(task_manager.Task): def main(self, client): return client.keystone_client.users.update(**self.args) @@ -42,11 +32,6 @@ def main(self, client): return client.keystone_client.users.update_password(**self.args) -class UserGet(task_manager.Task): - def main(self, client): - return client.keystone_client.users.get(**self.args) - - class UserAddToGroup(task_manager.Task): def main(self, client): return client.keystone_client.users.add_to_group(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index fd230a94c..db4e4408d 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -922,8 +922,9 @@ def list_users(self): :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ - with _utils.shade_exceptions("Failed to list users"): - users = self.manager.submit_task(_tasks.UserList()) + users = self._identity_client.get('/users') + if isinstance(users, dict): + users = users['users'] return _utils.normalize_users(users) def search_users(self, name_or_id=None, filters=None): @@ -967,10 +968,11 @@ def get_user_by_id(self, user_id, normalize=True): :returns: a single ``munch.Munch`` containing the user description """ - with _utils.shade_exceptions( - "Error getting user with ID {user_id}".format( - user_id=user_id)): - user = self.manager.submit_task(_tasks.UserGet(user=user_id)) + user = self._identity_client.get( + '/users/{user}'.format(user=user_id), + error_message="Error getting user with ID {user_id}".format( + user_id=user_id)) + if user and normalize: return _utils.normalize_users([user])[0] return user @@ -1036,6 +1038,8 @@ def create_user( return _utils.normalize_users([user])[0] def delete_user(self, name_or_id): + # TODO(mordred) Support name_or_id as dict to avoid any gets + # TODO(mordred) Why are we invalidating at the TOP? self.list_users.invalidate(self) user = self.get_user(name_or_id) if not user: @@ -1043,11 +1047,14 @@ def delete_user(self, name_or_id): "User {0} not found for deleting".format(name_or_id)) return False - # normalized dict won't work + # TODO(mordred) Extra GET only needed to support keystoneclient. + # Can be removed as a follow-on. user = self.get_user_by_id(user['id'], normalize=False) - with _utils.shade_exceptions("Error in deleting user {user}".format( - user=name_or_id)): - self.manager.submit_task(_tasks.UserDelete(user=user)) + self._identity_client.delete( + '/users/{user}'.format(user=user['id']), + error_message="Error in deleting user {user}".format( + user=name_or_id)) + self.list_users.invalidate(self) return True From 766d6227a39f485ae7fbcb29acd68036d0bd5bc7 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 6 Jun 2017 16:59:00 -0500 Subject: [PATCH 1587/3836] Use get_discovery from keystoneauth If we use the get_discovery call, we'll re-use the keystoneauth cache. This removes the last stupid extra discovery call. While doing this, noticed the dns version discovery was wrong, but we weren't noticing because adapter magic. Fix it. Change-Id: I01d07554d094fffd498f21a9291710c8eb0041f6 --- shade/openstackcloud.py | 93 +++++++++++++++--------- shade/tests/unit/base.py | 11 --- shade/tests/unit/fixtures/dns.json | 40 +++++----- shade/tests/unit/test_caching.py | 2 - shade/tests/unit/test_domains.py | 11 --- shade/tests/unit/test_endpoints.py | 5 -- shade/tests/unit/test_groups.py | 1 - shade/tests/unit/test_identity_roles.py | 9 --- shade/tests/unit/test_limits.py | 1 - shade/tests/unit/test_project.py | 8 -- shade/tests/unit/test_quotas.py | 1 - shade/tests/unit/test_role_assignment.py | 30 -------- shade/tests/unit/test_services.py | 6 -- shade/tests/unit/test_usage.py | 1 - shade/tests/unit/test_users.py | 6 -- 15 files changed, 79 insertions(+), 146 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index fd230a94c..3b24c4801 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -415,7 +415,7 @@ def _identity_client(self): if 'identity' not in self._raw_clients: identity_client = self._get_raw_client('identity') identity_url = self._discover_endpoint( - 'identity', version_required=True) + 'identity', version_required=True, versions=['3', '2']) identity_client.endpoint_override = identity_url self.cloud_config.config['identity_endpoint_override'] = \ identity_url @@ -429,13 +429,19 @@ def _raw_image_client(self): self._raw_clients['raw-image'] = image_client return self._raw_clients['raw-image'] + def _get_version_and_base_from_endpoint(self, endpoint): + url, version = endpoint.rstrip('/').rsplit('/', 1) + if version.endswith(self.current_project_id): + url, version = endpoint.rstrip('/').rsplit('/', 1) + if version.startswith('v'): + return url, version[1] + return "/".join([url, version]), None + def _get_version_from_endpoint(self, endpoint): - endpoint_parts = endpoint.rstrip('/').split('/') - if endpoint_parts[-1].endswith(self.current_project_id): - endpoint_parts.pop() - if endpoint_parts[-1].startswith('v'): - return endpoint_parts[-1][1] - return None + return self._get_version_and_base_from_endpoint(endpoint)[1] + + def _strip_version_from_endpoint(self, endpoint): + return self._get_version_and_base_from_endpoint(endpoint)[0] def _match_given_endpoint(self, given, version): given_version = self._get_version_from_endpoint(given) @@ -443,7 +449,9 @@ def _match_given_endpoint(self, given, version): return True return False - def _discover_endpoint(self, service_type, version_required=False): + def _discover_endpoint( + self, service_type, version_required=False, + versions=None): # If endpoint_override is set, do nothing service_endpoint = self.cloud_config.get_endpoint(service_type) if service_endpoint: @@ -452,56 +460,70 @@ def _discover_endpoint(self, service_type, version_required=False): client = self._get_raw_client(service_type) config_version = self.cloud_config.get_api_version(service_type) + if versions and config_version[0] not in versions: + raise OpenStackCloudException( + "Version {version} was requested for service {service_type}" + " but shade only understands how to handle versions" + " {versions}".format( + version=config_version, + service_type=service_type, + versions=versions)) + # shade only groks major versions at the moment if config_version: config_version = config_version[0] # First - quick check to see if the endpoint in the catalog # is a versioned endpoint that matches the version we requested. - # If it is, don't do any additoinal work. + # If it is, don't do any additional work. catalog_endpoint = client.get_endpoint() if self._match_given_endpoint( catalog_endpoint, config_version): return catalog_endpoint - # Ok, do version discovery - candidate_endpoints = None + if not versions and config_version: + versions = [config_version] + + candidate_endpoints = [] version_key = '{service}_api_version'.format(service=service_type) + + # Next, try built-in keystoneauth discovery so that we can take + # advantage of the discovery cache + base_url = self._strip_version_from_endpoint(catalog_endpoint) try: - versions = client.get('/') - if 'values' in versions: - versions = versions['values'] - if isinstance(versions, dict): - versions = [versions] + discovery = self.keystone_session.auth.get_discovery( + self.keystone_session, base_url) + + version_list = discovery.version_data(reverse=True) if config_version: - candidate_endpoints = [ - version for version in versions - if version['id'][1] == config_version] - if not candidate_endpoints: - candidate_endpoints = [ - version for version in versions - if version['status'] in ('CURRENT', 'stable')] + # If we have a specific version request, look for it first. + for version_data in version_list: + if str(version_data['version'][0]) == config_version: + candidate_endpoints.append(version_data) + if not candidate_endpoints: - candidate_endpoints = versions - except (keystoneauth1.exceptions.connection.ConnectFailure, - OpenStackCloudURINotFound) as e: - # A 404 or a connection error is a likely thing to get - # either with a misconfgured service. or we've already - # gotten a versioned endpoint from the catalog + # If we didn't find anything, look again, this time either + # for the range, or just grab everything if we don't have + # a range + if str(version_data['version'][0]) in versions: + candidate_endpoints.append(version_data) + elif not config_version and not versions: + candidate_endpoints.append(version_data) + + except keystoneauth1.exceptions.DiscoveryFailure as e: self.log.debug( "Version discovery failed, assuming endpoint in" " the catalog is already versioned. {e}".format(e=str(e))) if candidate_endpoints: + # If we got more than one, pick the highest endpoint_description = candidate_endpoints[0] - service_endpoint = [ - link['href'] for link in endpoint_description['links'] - if link['rel'] == 'self'][0] - api_version = endpoint_description['id'][1] + service_endpoint = endpoint_description['url'] + api_version = str(endpoint_description['version'][0]) else: # Can't discover a version. Do best-attempt at inferring # version from URL so that later logic can do its best - api_version = self._get_version_from_endpoint() + api_version = self._get_version_from_endpoint(catalog_endpoint) if not api_version: if not config_version and version_required: raise OpenStackCloudException( @@ -545,7 +567,8 @@ def _discover_endpoint(self, service_type, version_required=False): def _image_client(self): if 'image' not in self._raw_clients: image_client = self._get_raw_client('image') - image_url = self._discover_endpoint('image', version_required=True) + image_url = self._discover_endpoint( + 'image', version_required=True, versions=['2', '1']) image_client.endpoint_override = image_url self._raw_clients['image'] = image_client return self._raw_clients['image'] diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 4115eebd5..47bf1d9e2 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -434,17 +434,6 @@ def use_keystone_v2(self): self._make_test_cloud(cloud_name='_test_cloud_v2_', identity_api_version='2.0') - def _add_discovery_uri_call(self): - # NOTE(notmorgan): Temp workaround for transition to requests - # mock for cases keystoneclient is still mocked directly. This allows - # us to inject another call to discovery where needed in a test that - # no longer mocks out kyestoneclient and performs the extra round - # trips. - self.__do_register_uris([ - dict(method='GET', uri='https://identity.example.com/', - text=open(self.discovery_json, 'r').read()), - ]) - def _make_test_cloud(self, cloud_name='_test_cloud_', **kwargs): test_cloud = os.environ.get('SHADE_OS_CLOUD', cloud_name) self.cloud_config = self.config.get_one_cloud( diff --git a/shade/tests/unit/fixtures/dns.json b/shade/tests/unit/fixtures/dns.json index d3bbfc5c0..1fc8e86bd 100644 --- a/shade/tests/unit/fixtures/dns.json +++ b/shade/tests/unit/fixtures/dns.json @@ -1,22 +1,24 @@ { - "values": [{ - "id": "v1", - "links": [ - { - "href": "https://dns.example.com/v1", - "rel": "self" - } - ], - "status": "DEPRECATED" - }, { - "id": "v2", - "links": [ - { - "href": "https://dns.example.com/v2", - "rel": "self" - } - ], - "status": "CURRENT" - }] + "versions": { + "values": [{ + "id": "v1", + "links": [ + { + "href": "https://dns.example.com/v1", + "rel": "self" + } + ], + "status": "DEPRECATED" + }, { + "id": "v2", + "links": [ + { + "href": "https://dns.example.com/v2", + "rel": "self" + } + ], + "status": "CURRENT" + }] + } } diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index f9e7a7e7a..4a7f629aa 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -107,7 +107,6 @@ def test_openstack_cloud(self): self.assertIsInstance(self.cloud, shade.OpenStackCloud) def test_list_projects_v3(self): - self._add_discovery_uri_call() project_one = self._get_project_data() project_two = self._get_project_data() project_list = [project_one, project_two] @@ -312,7 +311,6 @@ def now_deleting(request, context): self.assert_calls() def test_list_users(self): - self._add_discovery_uri_call() user_data = self._get_user_data(email='test@example.com') self.register_uris([ dict(method='GET', diff --git a/shade/tests/unit/test_domains.py b/shade/tests/unit/test_domains.py index c63f045dd..90cf6874b 100644 --- a/shade/tests/unit/test_domains.py +++ b/shade/tests/unit/test_domains.py @@ -32,7 +32,6 @@ def get_mock_url(self, service_type='identity', append=append, base_url_append=base_url_append) def test_list_domains(self): - self._add_discovery_uri_call() domain_data = self._get_domain_data() self.register_uris([ dict(method='GET', uri=self.get_mock_url(), status_code=200, @@ -46,7 +45,6 @@ def test_list_domains(self): self.assert_calls() def test_get_domain(self): - self._add_discovery_uri_call() domain_data = self._get_domain_data() self.register_uris([ dict(method='GET', @@ -59,7 +57,6 @@ def test_get_domain(self): self.assert_calls() def test_get_domain_with_name_or_id(self): - self._add_discovery_uri_call() domain_data = self._get_domain_data() response = {'domains': [domain_data.json_response['domain']]} self.register_uris([ @@ -79,7 +76,6 @@ def test_get_domain_with_name_or_id(self): self.assert_calls() def test_create_domain(self): - self._add_discovery_uri_call() domain_data = self._get_domain_data(description=uuid.uuid4().hex, enabled=True) self.register_uris([ @@ -99,7 +95,6 @@ def test_create_domain(self): self.assert_calls() def test_create_domain_exception(self): - self._add_discovery_uri_call() with testtools.ExpectedException( shade.OpenStackCloudException, "Failed to create domain domain_name" @@ -110,7 +105,6 @@ def test_create_domain_exception(self): self.assert_calls() def test_delete_domain(self): - self._add_discovery_uri_call() domain_data = self._get_domain_data() new_resp = domain_data.json_response.copy() new_resp['domain']['enabled'] = False @@ -125,7 +119,6 @@ def test_delete_domain(self): self.assert_calls() def test_delete_domain_name_or_id(self): - self._add_discovery_uri_call() domain_data = self._get_domain_data() new_resp = domain_data.json_response.copy() new_resp['domain']['enabled'] = False @@ -148,7 +141,6 @@ def test_delete_domain_exception(self): # to update domain even though it is called via delete_domain. This # should be fixed in shade to catch either a failure on PATCH, # subsequent GET, or DELETE call(s). - self._add_discovery_uri_call() domain_data = self._get_domain_data() new_resp = domain_data.json_response.copy() new_resp['domain']['enabled'] = False @@ -167,7 +159,6 @@ def test_delete_domain_exception(self): self.assert_calls() def test_update_domain(self): - self._add_discovery_uri_call() domain_data = self._get_domain_data( description=self.getUniqueString('domainDesc')) domain_resource_uri = self.get_mock_url(append=[domain_data.domain_id]) @@ -188,7 +179,6 @@ def test_update_domain(self): self.assert_calls() def test_update_domain_name_or_id(self): - self._add_discovery_uri_call() domain_data = self._get_domain_data( description=self.getUniqueString('domainDesc')) domain_resource_uri = self.get_mock_url(append=[domain_data.domain_id]) @@ -211,7 +201,6 @@ def test_update_domain_name_or_id(self): self.assert_calls() def test_update_domain_exception(self): - self._add_discovery_uri_call() domain_data = self._get_domain_data( description=self.getUniqueString('domainDesc')) self.register_uris([ diff --git a/shade/tests/unit/test_endpoints.py b/shade/tests/unit/test_endpoints.py index 762341115..8dbdbc499 100644 --- a/shade/tests/unit/test_endpoints.py +++ b/shade/tests/unit/test_endpoints.py @@ -127,7 +127,6 @@ def test_create_endpoint_v2(self): self.assert_calls() def test_create_endpoint_v3(self): - self._add_discovery_uri_call() service_data = self._get_service_data() public_endpoint_data = self._get_endpoint_v3_data( service_id=service_data.service_id, interface='public', @@ -251,7 +250,6 @@ def test_update_endpoint_v2(self): self.op_cloud.update_endpoint, 'endpoint_id') def test_update_endpoint_v3(self): - self._add_discovery_uri_call() service_data = self._get_service_data() endpoint_data = self._get_endpoint_v3_data( service_id=service_data.service_id, interface='admin', @@ -289,7 +287,6 @@ def test_update_endpoint_v3(self): self.assert_calls() def test_list_endpoints(self): - self._add_discovery_uri_call() endpoints_data = [self._get_endpoint_v3_data() for e in range(1, 10)] self.register_uris([ dict(method='GET', @@ -317,7 +314,6 @@ def test_list_endpoints(self): self.assert_calls() def test_search_endpoints(self): - self._add_discovery_uri_call() endpoints_data = [self._get_endpoint_v3_data(region='region1') for e in range(0, 2)] endpoints_data.extend([self._get_endpoint_v3_data() @@ -378,7 +374,6 @@ def test_search_endpoints(self): self.assert_calls() def test_delete_endpoint(self): - self._add_discovery_uri_call() endpoint_data = self._get_endpoint_v3_data() self.register_uris([ dict(method='GET', diff --git a/shade/tests/unit/test_groups.py b/shade/tests/unit/test_groups.py index 80b5aa2af..a2525e2f1 100644 --- a/shade/tests/unit/test_groups.py +++ b/shade/tests/unit/test_groups.py @@ -18,7 +18,6 @@ class TestGroups(base.RequestsMockTestCase): def setUp(self, cloud_config_fixture='clouds.yaml'): super(TestGroups, self).setUp( cloud_config_fixture=cloud_config_fixture) - self._add_discovery_uri_call() self.addCleanup(self.assert_calls) def get_mock_url(self, service_type='identity', interface='admin', diff --git a/shade/tests/unit/test_identity_roles.py b/shade/tests/unit/test_identity_roles.py index d7f676c83..0767345e8 100644 --- a/shade/tests/unit/test_identity_roles.py +++ b/shade/tests/unit/test_identity_roles.py @@ -44,7 +44,6 @@ def get_mock_url(self, service_type='identity', interface='admin', qs_elements) def test_list_roles(self): - self._add_discovery_uri_call() role_data = self._get_role_data() self.register_uris([ dict(method='GET', @@ -56,7 +55,6 @@ def test_list_roles(self): self.assert_calls() def test_get_role_by_name(self): - self._add_discovery_uri_call() role_data = self._get_role_data() self.register_uris([ dict(method='GET', @@ -72,7 +70,6 @@ def test_get_role_by_name(self): self.assert_calls() def test_get_role_by_id(self): - self._add_discovery_uri_call() role_data = self._get_role_data() self.register_uris([ dict(method='GET', @@ -88,7 +85,6 @@ def test_get_role_by_id(self): self.assert_calls() def test_create_role(self): - self._add_discovery_uri_call() role_data = self._get_role_data() self.register_uris([ dict(method='POST', @@ -110,7 +106,6 @@ def test_create_role(self): self.assert_calls() def test_delete_role_by_id(self): - self._add_discovery_uri_call() role_data = self._get_role_data() self.register_uris([ dict(method='GET', @@ -126,7 +121,6 @@ def test_delete_role_by_id(self): self.assert_calls() def test_delete_role_by_name(self): - self._add_discovery_uri_call() role_data = self._get_role_data() self.register_uris([ dict(method='GET', @@ -142,7 +136,6 @@ def test_delete_role_by_name(self): self.assert_calls() def test_list_role_assignments(self): - self._add_discovery_uri_call() domain_data = self._get_domain_data() user_data = self._get_user_data(domain_id=domain_data.domain_id) group_data = self._get_group_data(domain_id=domain_data.domain_id) @@ -177,7 +170,6 @@ def test_list_role_assignments(self): matchers.Equals(project_data.project_id)) def test_list_role_assignments_filters(self): - self._add_discovery_uri_call() domain_data = self._get_domain_data() user_data = self._get_user_data(domain_id=domain_data.domain_id) role_data = self._get_role_data() @@ -208,7 +200,6 @@ def test_list_role_assignments_filters(self): self.assertThat(ret[0].domain, matchers.Equals(domain_data.domain_id)) def test_list_role_assignments_exception(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='role_assignments'), diff --git a/shade/tests/unit/test_limits.py b/shade/tests/unit/test_limits.py index 8ccb3adbd..6bc7ce710 100644 --- a/shade/tests/unit/test_limits.py +++ b/shade/tests/unit/test_limits.py @@ -25,7 +25,6 @@ def test_get_compute_limits(self, mock_nova): @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_other_get_compute_limits(self, mock_nova): - self._add_discovery_uri_call() project = self.mock_for_keystone_projects(project_count=1, list_get=True)[0] self.op_cloud.get_compute_limits(project.project_id) diff --git a/shade/tests/unit/test_project.py b/shade/tests/unit/test_project.py index 1751376d8..8c5bc678a 100644 --- a/shade/tests/unit/test_project.py +++ b/shade/tests/unit/test_project.py @@ -55,7 +55,6 @@ def test_create_project_v2(self): self.assert_calls() def test_create_project_v3(self,): - self._add_discovery_uri_call() project_data = self._get_project_data( description=self.getUniqueString('projectDesc')) self.register_uris([ @@ -107,7 +106,6 @@ def test_delete_project_v2(self): self.assert_calls() def test_delete_project_v3(self): - self._add_discovery_uri_call() project_data = self._get_project_data(v3=False) self.register_uris([ dict(method='GET', @@ -122,7 +120,6 @@ def test_delete_project_v3(self): self.assert_calls() def test_update_project_not_found(self): - self._add_discovery_uri_call() project_data = self._get_project_data() self.register_uris([ dict(method='GET', @@ -175,7 +172,6 @@ def test_update_project_v2(self): self.assert_calls() def test_update_project_v3(self): - self._add_discovery_uri_call() project_data = self._get_project_data( description=self.getUniqueString('projectDesc')) self.register_uris([ @@ -205,7 +201,6 @@ def test_update_project_v3(self): self.assert_calls() def test_list_projects_v3(self): - self._add_discovery_uri_call() project_data = self._get_project_data( description=self.getUniqueString('projectDesc')) self.register_uris([ @@ -223,7 +218,6 @@ def test_list_projects_v3(self): self.assert_calls() def test_list_projects_v3_kwarg(self): - self._add_discovery_uri_call() project_data = self._get_project_data( description=self.getUniqueString('projectDesc')) self.register_uris([ @@ -242,7 +236,6 @@ def test_list_projects_v3_kwarg(self): self.assert_calls() def test_list_projects_search_compat(self): - self._add_discovery_uri_call() project_data = self._get_project_data( description=self.getUniqueString('projectDesc')) self.register_uris([ @@ -258,7 +251,6 @@ def test_list_projects_search_compat(self): self.assert_calls() def test_list_projects_search_compat_v3(self): - self._add_discovery_uri_call() project_data = self._get_project_data( description=self.getUniqueString('projectDesc')) self.register_uris([ diff --git a/shade/tests/unit/test_quotas.py b/shade/tests/unit/test_quotas.py index 16032e2c5..cdf44f0b4 100644 --- a/shade/tests/unit/test_quotas.py +++ b/shade/tests/unit/test_quotas.py @@ -23,7 +23,6 @@ class TestQuotas(base.RequestsMockTestCase): def setUp(self, cloud_config_fixture='clouds.yaml'): super(TestQuotas, self).setUp( cloud_config_fixture=cloud_config_fixture) - self._add_discovery_uri_call() @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_update_quotas(self, mock_nova): diff --git a/shade/tests/unit/test_role_assignment.py b/shade/tests/unit/test_role_assignment.py index 324920467..782235966 100644 --- a/shade/tests/unit/test_role_assignment.py +++ b/shade/tests/unit/test_role_assignment.py @@ -361,7 +361,6 @@ def test_grant_role_user_project_v2_exists(self): self.assert_calls() def test_grant_role_user_project(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), @@ -437,7 +436,6 @@ def test_grant_role_user_project(self): self.assert_calls() def test_grant_role_user_project_exists(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), @@ -509,7 +507,6 @@ def test_grant_role_user_project_exists(self): self.assert_calls() def test_grant_role_group_project(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), @@ -583,7 +580,6 @@ def test_grant_role_group_project(self): self.assert_calls() def test_grant_role_group_project_exists(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), @@ -655,7 +651,6 @@ def test_grant_role_group_project_exists(self): self.assert_calls() def test_grant_role_user_domain(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), @@ -801,7 +796,6 @@ def test_grant_role_user_domain(self): self.assert_calls() def test_grant_role_user_domain_exists(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), @@ -939,7 +933,6 @@ def test_grant_role_user_domain_exists(self): self.assert_calls() def test_grant_role_group_domain(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), @@ -1085,7 +1078,6 @@ def test_grant_role_group_domain(self): self.assert_calls() def test_grant_role_group_domain_exists(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), @@ -1477,7 +1469,6 @@ def test_revoke_role_user_project_v2_exists(self): self.assert_calls() def test_revoke_role_user_project(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), @@ -1537,7 +1528,6 @@ def test_revoke_role_user_project(self): self.assert_calls() def test_revoke_role_user_project_exists(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), @@ -1623,7 +1613,6 @@ def test_revoke_role_user_project_exists(self): self.assert_calls() def test_revoke_role_group_project(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), @@ -1683,7 +1672,6 @@ def test_revoke_role_group_project(self): self.assert_calls() def test_revoke_role_group_project_exists(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), @@ -1769,7 +1757,6 @@ def test_revoke_role_group_project_exists(self): self.assert_calls() def test_revoke_role_user_domain(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), @@ -1883,7 +1870,6 @@ def test_revoke_role_user_domain(self): self.assert_calls() def test_revoke_role_user_domain_exists(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), @@ -2049,7 +2035,6 @@ def test_revoke_role_user_domain_exists(self): self.assert_calls() def test_revoke_role_group_domain(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), @@ -2163,7 +2148,6 @@ def test_revoke_role_group_domain(self): self.assert_calls() def test_revoke_role_group_domain_exists(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), @@ -2329,7 +2313,6 @@ def test_revoke_role_group_domain_exists(self): self.assert_calls() def test_grant_no_role(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), @@ -2348,7 +2331,6 @@ def test_grant_no_role(self): self.assert_calls() def test_revoke_no_role(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), @@ -2367,7 +2349,6 @@ def test_revoke_no_role(self): self.assert_calls() def test_grant_no_user_or_group_specified(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), @@ -2382,7 +2363,6 @@ def test_grant_no_user_or_group_specified(self): self.assert_calls() def test_revoke_no_user_or_group_specified(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), @@ -2397,7 +2377,6 @@ def test_revoke_no_user_or_group_specified(self): self.assert_calls() def test_grant_no_user_or_group(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), @@ -2418,7 +2397,6 @@ def test_grant_no_user_or_group(self): self.assert_calls() def test_revoke_no_user_or_group(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), @@ -2439,7 +2417,6 @@ def test_revoke_no_user_or_group(self): self.assert_calls() def test_grant_both_user_and_group(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), @@ -2465,7 +2442,6 @@ def test_grant_both_user_and_group(self): self.assert_calls() def test_revoke_both_user_and_group(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), @@ -2491,7 +2467,6 @@ def test_revoke_both_user_and_group(self): self.assert_calls() def test_grant_both_project_and_domain(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), @@ -2539,7 +2514,6 @@ def test_grant_both_project_and_domain(self): self.assert_calls() def test_revoke_both_project_and_domain(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), @@ -2592,7 +2566,6 @@ def test_revoke_both_project_and_domain(self): self.assert_calls() def test_grant_no_project_or_domain(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), @@ -2621,7 +2594,6 @@ def test_grant_no_project_or_domain(self): self.assert_calls() def test_revoke_no_project_or_domain(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), @@ -2656,7 +2628,6 @@ def test_revoke_no_project_or_domain(self): self.assert_calls() def test_grant_bad_domain_exception(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), @@ -2680,7 +2651,6 @@ def test_grant_bad_domain_exception(self): self.assert_calls() def test_revoke_bad_domain_exception(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self.get_mock_url(resource='roles'), diff --git a/shade/tests/unit/test_services.py b/shade/tests/unit/test_services.py index 0226cc2c7..d1ffe3852 100644 --- a/shade/tests/unit/test_services.py +++ b/shade/tests/unit/test_services.py @@ -67,7 +67,6 @@ def test_create_service_v2(self): self.assert_calls() def test_create_service_v3(self): - self._add_discovery_uri_call() service_data = self._get_service_data(name='a service', type='network', description='A test service') self.register_uris([ @@ -103,7 +102,6 @@ def test_update_service_v2(self): 'service_id', name='new name') def test_update_service_v3(self): - self._add_discovery_uri_call() service_data = self._get_service_data(name='a service', type='network', description='A test service') request = service_data.json_request.copy() @@ -134,7 +132,6 @@ def test_update_service_v3(self): self.assert_calls() def test_list_services(self): - self._add_discovery_uri_call() service_data = self._get_service_data() self.register_uris([ dict(method='GET', @@ -153,7 +150,6 @@ def test_list_services(self): self.assert_calls() def test_get_service(self): - self._add_discovery_uri_call() service_data = self._get_service_data() service2_data = self._get_service_data() self.register_uris([ @@ -201,7 +197,6 @@ def test_get_service(self): self.assert_calls() def test_search_services(self): - self._add_discovery_uri_call() service_data = self._get_service_data() service2_data = self._get_service_data(type=service_data.service_type) self.register_uris([ @@ -263,7 +258,6 @@ def test_search_services(self): self.assert_calls() def test_delete_service(self): - self._add_discovery_uri_call() service_data = self._get_service_data() self.register_uris([ dict(method='GET', diff --git a/shade/tests/unit/test_usage.py b/shade/tests/unit/test_usage.py index 10948e14f..8a7710a2c 100644 --- a/shade/tests/unit/test_usage.py +++ b/shade/tests/unit/test_usage.py @@ -22,7 +22,6 @@ class TestUsage(base.RequestsMockTestCase): @mock.patch.object(shade.OpenStackCloud, 'nova_client') def test_get_usage(self, mock_nova): - self._add_discovery_uri_call() project = self.mock_for_keystone_projects(project_count=1, list_get=True)[0] start = end = datetime.datetime.now() diff --git a/shade/tests/unit/test_users.py b/shade/tests/unit/test_users.py index 18b09f334..8ab9ac6d7 100644 --- a/shade/tests/unit/test_users.py +++ b/shade/tests/unit/test_users.py @@ -67,7 +67,6 @@ def test_create_user_v2(self): self.assert_calls() def test_create_user_v3(self): - self._add_discovery_uri_call() user_data = self._get_user_data( domain_id=uuid.uuid4().hex, description=self.getUniqueString('description')) @@ -149,7 +148,6 @@ def test_create_user_v3_no_domain(self): password=user_data.password) def test_delete_user(self): - self._add_discovery_uri_call() user_data = self._get_user_data(domain_id=uuid.uuid4().hex) user_resource_uri = self._get_keystone_mock_url( resource='users', append=[user_data.user_id]) @@ -167,7 +165,6 @@ def test_delete_user(self): self.assert_calls() def test_delete_user_not_found(self): - self._add_discovery_uri_call() self.register_uris([ dict(method='GET', uri=self._get_keystone_mock_url(resource='users'), @@ -175,7 +172,6 @@ def test_delete_user_not_found(self): self.assertFalse(self.op_cloud.delete_user(self.getUniqueString())) def test_add_user_to_group(self): - self._add_discovery_uri_call() user_data = self._get_user_data() group_data = self._get_group_data() @@ -197,7 +193,6 @@ def test_add_user_to_group(self): self.assert_calls() def test_is_user_in_group(self): - self._add_discovery_uri_call() user_data = self._get_user_data() group_data = self._get_group_data() @@ -221,7 +216,6 @@ def test_is_user_in_group(self): self.assert_calls() def test_remove_user_from_group(self): - self._add_discovery_uri_call() user_data = self._get_user_data() group_data = self._get_group_data() From a66826ee7bcdd9845167058e446a40e61b18d8f8 Mon Sep 17 00:00:00 2001 From: Vu Cong Tuan Date: Wed, 7 Jun 2017 10:38:31 +0700 Subject: [PATCH 1588/3836] Remove support for py34 The gating on python 3.4 is restricted to <= Mitaka. This is due to the change from Ubuntu Trusty to Xenial, where only python3.5 is available. There is no need to continue to keep these settings. Change-Id: If2aba189704c310ea4fcb805589e2732a525e992 --- setup.cfg | 1 - tox.ini | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 2c6b5a0a4..d006218f5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,7 +17,6 @@ classifier = Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 [files] diff --git a/tox.ini b/tox.ini index ecb18c79c..789758b43 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 1.6 -envlist = py35,py34,py27,pypy,pep8 +envlist = py35,py27,pypy,pep8 skipsdist = True [testenv] From d24bebb538296894c9dc0665e5dcec7cf1aa882f Mon Sep 17 00:00:00 2001 From: dineshbhor Date: Wed, 13 Apr 2016 13:27:04 +0000 Subject: [PATCH 1589/3836] Fix update_image unsupported media type Currently update_image returns unsupported media type because unexpected Content-Type (application/json) being passed to glance service. Provided expected Content-Type and body with patch operations(op). Used make_patch() method from jsonpatch library to create patch for updating image. Closes-Bug: #1455620 Change-Id: I3d77648d55b2870e40ff689b47574e68aa72d7f6 --- openstack/image/v2/_proxy.py | 3 +- openstack/image/v2/image.py | 16 +++++++++++ openstack/tests/unit/image/v2/test_image.py | 32 +++++++++++++++++++++ openstack/tests/unit/image/v2/test_proxy.py | 18 ++++++++++-- requirements.txt | 1 + 5 files changed, 67 insertions(+), 3 deletions(-) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index c886b305f..14907dbd5 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -158,7 +158,8 @@ def update_image(self, image, **attrs): :returns: The updated image :rtype: :class:`~openstack.image.v2.image.Image` """ - return self._update(_image.Image, image, **attrs) + img = self._get_resource(_image.Image, image) + return img.update(self._session, **attrs) def deactivate_image(self, image): """Deactivate an image diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index d3298c0d0..5694ce7f7 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -13,6 +13,8 @@ import hashlib import logging +import jsonpatch + from openstack import exceptions from openstack.image import image_service from openstack import resource2 @@ -283,3 +285,17 @@ def download(self, session, stream=False): "Unable to verify the integrity of image %s" % (self.id)) return resp.content + + def update(self, session, **attrs): + url = utils.urljoin(self.base_path, self.id) + headers = { + 'Content-Type': 'application/openstack-images-v2.1-json-patch', + 'Accept': '' + } + original = self.to_dict() + patch_string = jsonpatch.make_patch(original, attrs).to_string() + resp = session.patch(url, endpoint_filter=self.service, + data=patch_string, + headers=headers) + self._translate_response(resp, has_body=True) + return self diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index bebb4c535..2a3acf45f 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import json + import mock import testtools @@ -292,3 +294,33 @@ def test_download_stream(self): stream=True) self.assertEqual(rv, resp) + + def test_image_update(self): + sot = image.Image(**EXAMPLE) + # Let the translate pass through, that portion is tested elsewhere + sot._translate_response = mock.Mock() + + resp = mock.Mock() + resp.content = b"abc" + headers = { + 'Content-Type': 'application/openstack-images-v2.1-json-patch', + 'Accept': '', + } + resp.headers = headers + resp.status_code = 200 + self.sess.patch.return_value = resp + + value = ('[{"value": "fake_name", "op": "replace", "path": "/name"}, ' + '{"value": "fake_value", "op": "add", ' + '"path": "/new_property"}]') + fake_img = sot.to_dict() + fake_img['name'] = 'fake_name' + fake_img['new_property'] = 'fake_value' + + sot.update(self.sess, **fake_img) + url = 'images/' + IDENTIFIER + self.sess.patch.assert_called_once() + call = self.sess.patch.call_args + call_args, call_kwargs = call + self.assertEqual(url, call_args[0]) + self.assertEqual(json.loads(value), json.loads(call_kwargs['data'])) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index b8a1ee9d0..8bb060c77 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -16,8 +16,11 @@ from openstack.image.v2 import _proxy from openstack.image.v2 import image from openstack.image.v2 import member +from openstack.tests.unit.image.v2 import test_image as fake_image from openstack.tests.unit import test_proxy_base2 +EXAMPLE = fake_image.EXAMPLE + class TestImageProxy(test_proxy_base2.TestProxyBase): def setUp(self): @@ -53,8 +56,19 @@ def test_image_delete(self): def test_image_delete_ignore(self): self.verify_delete(self.proxy.delete_image, image.Image, True) - def test_image_update(self): - self.verify_update(self.proxy.update_image, image.Image) + @mock.patch("openstack.resource2.Resource._translate_response") + @mock.patch("openstack.proxy2.BaseProxy._get") + @mock.patch("openstack.image.v2.image.Image.update") + def test_image_update(self, mock_update_image, mock_get_image, + mock_transpose): + original_image = image.Image(**EXAMPLE) + mock_get_image.return_value = original_image + EXAMPLE['name'] = 'fake_name' + updated_image = image.Image(**EXAMPLE) + mock_update_image.return_value = updated_image.to_dict() + result = self.proxy.update_image(original_image, + **updated_image.to_dict()) + self.assertEqual('fake_name', result.get('name')) def test_image_get(self): self.verify_get(self.proxy.get_image, image.Image) diff --git a/requirements.txt b/requirements.txt index b2a4bb5c7..499232435 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ # 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 +jsonpatch>=1.1 # BSD six>=1.9.0 # MIT stevedore>=1.20.0 # Apache-2.0 os-client-config>=1.27.0 # Apache-2.0 From f01a7d43ca3423650e9d2e0b90eea0b61dfde92f Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Thu, 8 Jun 2017 13:40:35 +0000 Subject: [PATCH 1590/3836] Don't remove top-container element for volume REST API calls This is a first step in the process of removing the logic from the shade.Adapter that removes top-container element from the REST PAI response. The goal is to simplify the logic and move it into the REST API calls. Change-Id: I885ba0c8d1df8c6f33bd3e2e92d052a1614a917f Signed-off-by: Rosario Di Somma --- shade/_adapter.py | 9 ++++++++ shade/openstackcloud.py | 48 ++++++++++++++++++----------------------- shade/operatorcloud.py | 11 +++++----- 3 files changed, 36 insertions(+), 32 deletions(-) diff --git a/shade/_adapter.py b/shade/_adapter.py index f7a9241cb..be687cd5d 100644 --- a/shade/_adapter.py +++ b/shade/_adapter.py @@ -127,6 +127,15 @@ def _munch_response(self, response, result_key=None, error_message=None): except Exception: return self._log_request_id(response) + # Note(rods): this is just a temporary step needed until we + # don't update all the other REST API calls + if isinstance(result_json, dict): + for key in ['volumes', 'volume', 'volumeAttachment', 'backups', + 'volume_types', 'volume_type_access', 'snapshots']: + if key in result_json.keys(): + self._log_request_id(response) + return result_json + if isinstance(result_json, list): self._log_request_id(response) return meta.obj_list_to_dict(result_json) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 8ff34f13f..aa17cbece 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1630,26 +1630,15 @@ def list_volumes(self, cache=True): :returns: A list of volume ``munch.Munch``. """ - def _list(response): - # NOTE(rods)The shade Adapter is removing the top-level - # container but not with pagination or in a few other - # circumstances, so `response` can be a list of Volumes, - # or a dict like {'volumes_list': [...], 'volume': [...]}. - # We need the type check to work around the issue until - # next commit where we'll move the top-level container - # removing from the adapter to the related call. - if isinstance(response, list): - volumes.extend(response) - if isinstance(response, dict): - volumes.extend(meta.obj_list_to_dict(response['volumes'])) - endpoint = None - if 'volumes_links' in response: - for l in response['volumes_links']: - if 'rel' in l and 'next' == l['rel']: - endpoint = l['href'] - break - if endpoint: - _list(self._volume_client.get(endpoint)) + def _list(data): + volumes.extend(data.get('volumes', [])) + endpoint = None + for l in data.get('volumes_links', []): + if 'rel' in l and 'next' == l['rel']: + endpoint = l['href'] + break + if endpoint: + _list(self._volume_client.get(endpoint)) if not cache: warnings.warn('cache argument to list_volumes is deprecated. Use ' @@ -1666,9 +1655,10 @@ def list_volume_types(self, get_extra=True): """ with _utils.shade_exceptions("Error fetching volume_type list"): + data = self._volume_client.get( + '/types', params=dict(is_public='None')) return self._normalize_volume_types( - self._volume_client.get( - '/types', params=dict(is_public='None'))) + data.get('volume_types', [])) @_utils.cache_on_arguments() def list_availability_zone_names(self, unavailable=False): @@ -3820,8 +3810,9 @@ def create_volume( payload['OS-SCH-HNT:scheduler_hints'] = kwargs.pop( 'scheduler_hints', None) with _utils.shade_exceptions("Error in creating volume"): - volume = self._volume_client.post( + data = self._volume_client.post( '/volumes', json=dict(payload)) + volume = data.get('volume', {}) self.list_volumes.invalidate(self) if volume['status'] == 'error': @@ -3999,7 +3990,7 @@ def attach_volume(self, server, volume, device=None, payload = {'volumeId': volume['id']} if device: payload['device'] = device - vol_attachment = self._compute_client.post( + data = self._compute_client.post( '/servers/{server_id}/os-volume_attachments'.format( server_id=server['id']), json=dict(volumeAttachment=payload), @@ -4030,7 +4021,8 @@ def attach_volume(self, server, volume, device=None, raise OpenStackCloudException( "Error in attaching volume %s" % volume['id'] ) - return self._normalize_volume_attachment(vol_attachment) + return self._normalize_volume_attachment( + data.get('volumeAttachment', {})) def _get_volume_kwargs(self, kwargs): name = kwargs.pop('name', kwargs.pop('display_name', None)) @@ -4205,10 +4197,11 @@ def list_volume_snapshots(self, detailed=True, search_opts=None): """ endpoint = '/snapshots/detail' if detailed else '/snapshots' - return self._volume_client.get( + data = self._volume_client.get( endpoint, params=search_opts, error_message="Error getting a list of snapshots") + return data.get('snapshots', []) def list_volume_backups(self, detailed=True, search_opts=None): """ @@ -4228,9 +4221,10 @@ def list_volume_backups(self, detailed=True, search_opts=None): :returns: A list of volume backups ``munch.Munch``. """ endpoint = '/backups/detail' if detailed else '/backups' - return self._volume_client.get( + data = self._volume_client.get( endpoint, params=search_opts, error_message="Error getting a list of backups") + return data.get('backups', []) def delete_volume_backup(self, name_or_id=None, force=False, wait=False, timeout=None): diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 0a109533c..3553801d2 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1988,12 +1988,13 @@ def get_volume_type_access(self, name_or_id): "VolumeType not found: %s" % name_or_id) with _utils.shade_exceptions( - "Unable to get volume type access {name}".format( - name=name_or_id)): + "Unable to get volume type access {name}".format( + name=name_or_id)): + data = self._volume_client.get( + '/types/{id}/os-volume-type-access'.format( + id=volume_type.id)) return self._normalize_volume_type_accesses( - self._volume_client.get( - '/types/{id}/os-volume-type-access'.format( - id=volume_type.id))) + data.get('volume_type_access', [])) def add_volume_type_access(self, name_or_id, project_id): """Grant access on a volume_type to a project. From 40c4f3ca15f990572e3ebeb93f484b74ac9d392b Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Thu, 8 Jun 2017 17:56:32 +0000 Subject: [PATCH 1591/3836] Remove unneeded calls to shade_exceptions Change-Id: I3dce6e9a9e25e763e4daa2be0495521876a27fd4 Signed-off-by: Rosario Di Somma --- shade/openstackcloud.py | 20 +++++++++++--------- shade/operatorcloud.py | 14 ++++++-------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 695f04fb7..001cdbbfa 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1684,11 +1684,12 @@ def list_volume_types(self, get_extra=True): :returns: A list of volume ``munch.Munch``. """ - with _utils.shade_exceptions("Error fetching volume_type list"): - data = self._volume_client.get( - '/types', params=dict(is_public='None')) - return self._normalize_volume_types( - data.get('volume_types', [])) + data = self._volume_client.get( + '/types', + params=dict(is_public='None'), + error_message='Error fetching volume_type list') + return self._normalize_volume_types( + data.get('volume_types', [])) @_utils.cache_on_arguments() def list_availability_zone_names(self, unavailable=False): @@ -3839,10 +3840,11 @@ def create_volume( if 'scheduler_hints' in kwargs: payload['OS-SCH-HNT:scheduler_hints'] = kwargs.pop( 'scheduler_hints', None) - with _utils.shade_exceptions("Error in creating volume"): - data = self._volume_client.post( - '/volumes', json=dict(payload)) - volume = data.get('volume', {}) + data = self._volume_client.post( + '/volumes', + json=dict(payload), + error_message='Error in creating volume') + volume = data.get('volume', {}) self.list_volumes.invalidate(self) if volume['status'] == 'error': diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 3553801d2..973a35cd3 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1987,14 +1987,12 @@ def get_volume_type_access(self, name_or_id): raise OpenStackCloudException( "VolumeType not found: %s" % name_or_id) - with _utils.shade_exceptions( - "Unable to get volume type access {name}".format( - name=name_or_id)): - data = self._volume_client.get( - '/types/{id}/os-volume-type-access'.format( - id=volume_type.id)) - return self._normalize_volume_type_accesses( - data.get('volume_type_access', [])) + data = self._volume_client.get( + '/types/{id}/os-volume-type-access'.format(id=volume_type.id), + error_message="Unable to get volume type access" + " {name}".format(name=name_or_id)) + return self._normalize_volume_type_accesses( + data.get('volume_type_access', [])) def add_volume_type_access(self, name_or_id, project_id): """Grant access on a volume_type to a project. From 75ce9ad924c9315d747529e40788a9f4ccb35138 Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Fri, 9 Jun 2017 13:49:05 +0000 Subject: [PATCH 1592/3836] Convert data from raw clients to Munch objects Change-Id: Ib6fcb0b18e550c75248eaf00f4a305f5882e6cdf Signed-off-by: Rosario Di Somma --- shade/meta.py | 14 ++++++++++++++ shade/openstackcloud.py | 12 ++++++------ shade/operatorcloud.py | 3 ++- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/shade/meta.py b/shade/meta.py index 1a00be543..826df5158 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -568,3 +568,17 @@ def warlock_to_dict(obj): if isinstance(value, NON_CALLABLES) and not key.startswith('_'): obj_dict[key] = value return obj_dict + + +def get_and_munchify(key, data): + """Get the value associated to key and convert it. + + The value will be converted in a Munch object or a list of Munch objects + based on the type + """ + result = data.get(key, []) + if isinstance(result, list): + return obj_list_to_dict(result) + elif isinstance(result, dict): + return obj_to_dict(result) + return result diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 001cdbbfa..13343e843 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1661,7 +1661,7 @@ def list_volumes(self, cache=True): """ def _list(data): - volumes.extend(data.get('volumes', [])) + volumes.extend(meta.get_and_munchify('volumes', data)) endpoint = None for l in data.get('volumes_links', []): if 'rel' in l and 'next' == l['rel']: @@ -1689,7 +1689,7 @@ def list_volume_types(self, get_extra=True): params=dict(is_public='None'), error_message='Error fetching volume_type list') return self._normalize_volume_types( - data.get('volume_types', [])) + meta.get_and_munchify('volume_types', data)) @_utils.cache_on_arguments() def list_availability_zone_names(self, unavailable=False): @@ -3844,7 +3844,7 @@ def create_volume( '/volumes', json=dict(payload), error_message='Error in creating volume') - volume = data.get('volume', {}) + volume = meta.get_and_munchify('volume', data) self.list_volumes.invalidate(self) if volume['status'] == 'error': @@ -4054,7 +4054,7 @@ def attach_volume(self, server, volume, device=None, "Error in attaching volume %s" % volume['id'] ) return self._normalize_volume_attachment( - data.get('volumeAttachment', {})) + meta.get_and_munchify('volumeAttachment', data)) def _get_volume_kwargs(self, kwargs): name = kwargs.pop('name', kwargs.pop('display_name', None)) @@ -4233,7 +4233,7 @@ def list_volume_snapshots(self, detailed=True, search_opts=None): endpoint, params=search_opts, error_message="Error getting a list of snapshots") - return data.get('snapshots', []) + return meta.get_and_munchify('snapshots', data) def list_volume_backups(self, detailed=True, search_opts=None): """ @@ -4256,7 +4256,7 @@ def list_volume_backups(self, detailed=True, search_opts=None): data = self._volume_client.get( endpoint, params=search_opts, error_message="Error getting a list of backups") - return data.get('backups', []) + return meta.get_and_munchify('backups', data) def delete_volume_backup(self, name_or_id=None, force=False, wait=False, timeout=None): diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 973a35cd3..c0256661b 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -18,6 +18,7 @@ from novaclient import exceptions as nova_exceptions from shade.exc import * # noqa +from shade import meta from shade import openstackcloud from shade import _tasks from shade import _utils @@ -1992,7 +1993,7 @@ def get_volume_type_access(self, name_or_id): error_message="Unable to get volume type access" " {name}".format(name=name_or_id)) return self._normalize_volume_type_accesses( - data.get('volume_type_access', [])) + meta.get_and_munchify('volume_type_access', data)) def add_volume_type_access(self, name_or_id, project_id): """Grant access on a volume_type to a project. From 75ce1eae85c53cc517520d7208eb1091a9f7374d Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Thu, 8 Jun 2017 21:53:38 +0000 Subject: [PATCH 1593/3836] Don't remove top-container element for network REST API calls Change-Id: I2cde7add2bc0c85afbcfd03992dd500ce3ec7a8c Signed-off-by: Rosario Di Somma --- shade/_adapter.py | 5 +- shade/openstackcloud.py | 59 +++++++++++++--------- shade/tests/functional/test_floating_ip.py | 3 +- shade/tests/unit/test_floating_ip_nova.py | 2 +- 4 files changed, 42 insertions(+), 27 deletions(-) diff --git a/shade/_adapter.py b/shade/_adapter.py index be687cd5d..2bc3bbb5d 100644 --- a/shade/_adapter.py +++ b/shade/_adapter.py @@ -131,7 +131,10 @@ def _munch_response(self, response, result_key=None, error_message=None): # don't update all the other REST API calls if isinstance(result_json, dict): for key in ['volumes', 'volume', 'volumeAttachment', 'backups', - 'volume_types', 'volume_type_access', 'snapshots']: + 'volume_types', 'volume_type_access', 'snapshots', + 'network', 'networks', 'subnet', 'subnets', + 'router', 'routers', 'floatingip', 'floatingips', + 'floating_ip', 'floating_ips', 'port', 'ports']: if key in result_json.keys(): self._log_request_id(response) return result_json diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 13343e843..2d5de7a2b 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1590,7 +1590,8 @@ def list_networks(self, filters=None): # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} - return self._network_client.get("/networks.json", params=filters) + data = self._network_client.get("/networks.json", params=filters) + return meta.get_and_munchify('networks', data) def list_routers(self, filters=None): """List all available routers. @@ -1602,9 +1603,10 @@ def list_routers(self, filters=None): # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} - return self._network_client.get( + data = self._network_client.get( "/routers.json", params=filters, error_message="Error fetching router list") + return meta.get_and_munchify('routers', data) def list_subnets(self, filters=None): """List all available subnets. @@ -1616,7 +1618,8 @@ def list_subnets(self, filters=None): # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} - return self._network_client.get("/subnets.json", params=filters) + data = self._network_client.get("/subnets.json", params=filters) + return meta.get_and_munchify('subnets', data) def list_ports(self, filters=None): """List all available ports. @@ -1649,9 +1652,10 @@ def list_ports(self, filters=None): return self._ports def _list_ports(self, filters): - return self._network_client.get( + data = self._network_client.get( "/ports.json", params=filters, error_message="Error fetching port list") + return meta.get_and_munchify('ports', data) @_utils.cache_on_arguments(should_cache_fn=_no_pending_volumes) def list_volumes(self, cache=True): @@ -2114,10 +2118,12 @@ def list_floating_ips(self, filters=None): def _neutron_list_floating_ips(self, filters=None): if not filters: filters = {} - return self._network_client.get('/floatingips.json', params=filters) + data = self._network_client.get('/floatingips.json', params=filters) + return meta.get_and_munchify('floatingips', data) def _nova_list_floating_ips(self): - return self._compute_client.get('/os-floating-ips') + data = self._compute_client.get('/os-floating-ips') + return meta.get_and_munchify('floating_ips', data) def use_external_network(self): return self._use_external_network @@ -2960,13 +2966,12 @@ def create_network(self, name, shared=False, admin_state_up=True, if external: network['router:external'] = True - net = self._network_client.post("/networks.json", - json={'network': network}) + data = self._network_client.post("/networks.json", + json={'network': network}) # Reset cache so the new network is picked up self._reset_network_caches() - - return net + return meta.get_and_munchify('network', data) def delete_network(self, name_or_id): """Delete a network. @@ -3145,9 +3150,10 @@ def create_router(self, name=None, admin_state_up=True, if ext_gw_info: router['external_gateway_info'] = ext_gw_info - return self._network_client.post( + data = self._network_client.post( "/routers.json", json={"router": router}, error_message="Error creating router {0}".format(name)) + return meta.get_and_munchify('router', data) def update_router(self, name_or_id, name=None, admin_state_up=None, ext_gateway_net_id=None, enable_snat=None, @@ -3194,10 +3200,11 @@ def update_router(self, name_or_id, name=None, admin_state_up=None, raise OpenStackCloudException( "Router %s not found." % name_or_id) - return self._network_client.put( + data = self._network_client.put( "/routers/{router_id}.json".format(router_id=curr_router['id']), json={"router": router}, error_message="Error updating router {0}".format(name_or_id)) + return meta.get_and_munchify('router', data) def delete_router(self, name_or_id): """Delete a logical router. @@ -4577,9 +4584,10 @@ def create_floating_ip(self, network=None, server=None, def _submit_create_fip(self, kwargs): # Split into a method to aid in test mocking - fip = self._network_client.post( + data = self._network_client.post( "/floatingips.json", json={"floatingip": kwargs}) - return self._normalize_floating_ip(fip) + return self._normalize_floating_ip( + meta.get_and_munchify('floatingip', data)) def _neutron_create_floating_ip( self, network_name_or_id=None, server=None, @@ -4666,12 +4674,13 @@ def _nova_create_floating_ip(self, pool=None): "unable to find a floating ip pool") pool = pools[0]['name'] - pool_ip = self._compute_client.post( + data = self._compute_client.post( '/os-floating-ips', json=dict(pool=pool)) + pool_ip = meta.get_and_munchify('floating_ip', data) # TODO(mordred) Remove this - it's just for compat - pool_ip = self._compute_client.get('/os-floating-ips/{id}'.format( + data = self._compute_client.get('/os-floating-ips/{id}'.format( id=pool_ip['id'])) - return pool_ip + return meta.get_and_munchify('floating_ip', data) def delete_floating_ip(self, floating_ip_id, retry=1): """Deallocate a floating IP from a project. @@ -6450,10 +6459,10 @@ def create_subnet(self, network_name_or_id, cidr=None, ip_version=4, if use_default_subnetpool: subnet['use_default_subnetpool'] = True - new_subnet = self._network_client.post("/subnets.json", - json={"subnet": subnet}) + data = self._network_client.post("/subnets.json", + json={"subnet": subnet}) - return new_subnet + return meta.get_and_munchify('subnet', data) def delete_subnet(self, name_or_id): """Delete a subnet. @@ -6559,10 +6568,10 @@ def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, raise OpenStackCloudException( "Subnet %s not found." % name_or_id) - new_subnet = self._network_client.put( + data = self._network_client.put( "/subnets/{subnet_id}.json".format(subnet_id=curr_subnet['id']), json={"subnet": subnet}) - return new_subnet + return meta.get_and_munchify('subnet', data) @_utils.valid_kwargs('name', 'admin_state_up', 'mac_address', 'fixed_ips', 'subnet_id', 'ip_address', 'security_groups', @@ -6623,10 +6632,11 @@ def create_port(self, network_id, **kwargs): """ kwargs['network_id'] = network_id - return self._network_client.post( + data = self._network_client.post( "/ports.json", json={'port': kwargs}, error_message="Error creating port for network {0}".format( network_id)) + return meta.get_and_munchify('port', data) @_utils.valid_kwargs('name', 'admin_state_up', 'fixed_ips', 'security_groups', 'allowed_address_pairs', @@ -6686,10 +6696,11 @@ def update_port(self, name_or_id, **kwargs): raise OpenStackCloudException( "failed to find port '{port}'".format(port=name_or_id)) - return self._network_client.put( + data = self._network_client.put( "/ports/{port_id}.json".format(port_id=port['id']), json={"port": kwargs}, error_message="Error updating port {0}".format(name_or_id)) + return meta.get_and_munchify('port', data) def delete_port(self, name_or_id): """Delete a port diff --git a/shade/tests/functional/test_floating_ip.py b/shade/tests/functional/test_floating_ip.py index b0679135b..f739657fb 100644 --- a/shade/tests/functional/test_floating_ip.py +++ b/shade/tests/functional/test_floating_ip.py @@ -161,7 +161,8 @@ def _setup_networks(self): self.user_cloud.list_networks()))) else: # Find network names for nova-net - nets = self.user_cloud._compute_client.get('/os-tenant-networks') + data = self.user_cloud._compute_client.get('/os-tenant-networks') + nets = meta.get_and_munchify('networks', data) self.addDetail( 'networks-nova', content.text_content(pprint.pformat( diff --git a/shade/tests/unit/test_floating_ip_nova.py b/shade/tests/unit/test_floating_ip_nova.py index cc0acd18d..1ecf33238 100644 --- a/shade/tests/unit/test_floating_ip_nova.py +++ b/shade/tests/unit/test_floating_ip_nova.py @@ -155,7 +155,7 @@ def test_create_floating_ip(self): uri=self.get_mock_url( 'compute', append=['os-floating-ips', '2']), - json={'floating_ips': self.mock_floating_ip_list_rep[1]}), + json={'floating_ip': self.mock_floating_ip_list_rep[1]}), ]) self.cloud.create_floating_ip(network='nova') From 4fce8d91f401249c06db5a7d7b264588dcd547e4 Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Sat, 10 Jun 2017 16:07:38 +0000 Subject: [PATCH 1594/3836] Rename obj_to_dict and obj_list_to_dict Since we are actually to munch objects and not dict, let's rename them to obj_to_munch and obj_list_to_munch. Change-Id: Id4d77520237503bbfede89638675402d143fffcd Signed-off-by: Rosario Di Somma --- shade/_adapter.py | 6 +- shade/_utils.py | 10 +-- shade/meta.py | 20 ++++-- shade/openstackcloud.py | 4 +- shade/task_manager.py | 8 +-- shade/tests/fakes.py | 2 +- shade/tests/unit/test_caching.py | 28 ++++---- shade/tests/unit/test_create_server.py | 8 +-- .../tests/unit/test_create_volume_snapshot.py | 10 +-- .../tests/unit/test_delete_volume_snapshot.py | 6 +- shade/tests/unit/test_floating_ip_common.py | 14 ++-- shade/tests/unit/test_floating_ip_neutron.py | 2 +- shade/tests/unit/test_floating_ip_nova.py | 2 +- shade/tests/unit/test_image.py | 2 +- shade/tests/unit/test_inventory.py | 2 +- shade/tests/unit/test_keypair.py | 2 +- shade/tests/unit/test_meta.py | 72 +++++++++---------- shade/tests/unit/test_rebuild_server.py | 8 +-- shade/tests/unit/test_security_groups.py | 30 ++++---- shade/tests/unit/test_shade_operator.py | 4 +- shade/tests/unit/test_stack.py | 4 +- shade/tests/unit/test_volume.py | 28 ++++---- shade/tests/unit/test_volume_backups.py | 8 +-- 23 files changed, 143 insertions(+), 137 deletions(-) diff --git a/shade/_adapter.py b/shade/_adapter.py index be687cd5d..025103ce3 100644 --- a/shade/_adapter.py +++ b/shade/_adapter.py @@ -138,7 +138,7 @@ def _munch_response(self, response, result_key=None, error_message=None): if isinstance(result_json, list): self._log_request_id(response) - return meta.obj_list_to_dict(result_json) + return meta.obj_list_to_munch(result_json) result = None if isinstance(result_json, dict): @@ -158,9 +158,9 @@ def _munch_response(self, response, result_key=None, error_message=None): self._log_request_id(response, result) if isinstance(result, list): - return meta.obj_list_to_dict(result) + return meta.obj_list_to_munch(result) elif isinstance(result, dict): - return meta.obj_to_dict(result) + return meta.obj_to_munch(result) return result def request( diff --git a/shade/_utils.py b/shade/_utils.py index 265ff5afc..1e7e768db 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -249,7 +249,7 @@ def normalize_keystone_services(services): 'enabled': service['enabled'] } ret.append(new_service) - return meta.obj_list_to_dict(ret) + return meta.obj_list_to_munch(ret) def localhost_supports_ipv6(): @@ -277,7 +277,7 @@ def normalize_users(users): description=user.get('description') ) for user in users ] - return meta.obj_list_to_dict(ret) + return meta.obj_list_to_munch(ret) def normalize_domains(domains): @@ -289,7 +289,7 @@ def normalize_domains(domains): enabled=domain.get('enabled'), ) for domain in domains ] - return meta.obj_list_to_dict(ret) + return meta.obj_list_to_munch(ret) def normalize_groups(domains): @@ -302,7 +302,7 @@ def normalize_groups(domains): domain_id=domain.get('domain_id'), ) for domain in domains ] - return meta.obj_list_to_dict(ret) + return meta.obj_list_to_munch(ret) def normalize_role_assignments(assignments): @@ -363,7 +363,7 @@ def normalize_roles(roles): name=role.get('name'), ) for role in roles ] - return meta.obj_list_to_dict(ret) + return meta.obj_list_to_munch(ret) def normalize_flavor_accesses(flavor_accesses): diff --git a/shade/meta.py b/shade/meta.py index 826df5158..207de980f 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -505,7 +505,7 @@ def _log_request_id(obj, request_id): return obj -def obj_to_dict(obj): +def obj_to_munch(obj): """ Turn an object with attributes into a dict suitable for serializing. Some of the things that are returned in OpenStack are objects with @@ -517,7 +517,7 @@ def obj_to_dict(obj): if obj is None: return None elif isinstance(obj, munch.Munch) or hasattr(obj, 'mock_add_spec'): - # If we obj_to_dict twice, don't fail, just return the munch + # If we obj_to_munch twice, don't fail, just return the munch # Also, don't try to modify Mock objects - that way lies madness return obj elif isinstance(obj, dict): @@ -545,14 +545,20 @@ def obj_to_dict(obj): return instance -def obj_list_to_dict(obj_list): +obj_to_dict = obj_to_munch + + +def obj_list_to_munch(obj_list): """Enumerate through lists of objects and return lists of dictonaries. Some of the objects returned in OpenStack are actually lists of objects, and in order to expose the data structures as JSON, we need to facilitate the conversion to lists of dictonaries. """ - return [obj_to_dict(obj) for obj in obj_list] + return [obj_to_munch(obj) for obj in obj_list] + + +obj_list_to_dict = obj_list_to_munch def warlock_to_dict(obj): @@ -562,7 +568,7 @@ def warlock_to_dict(obj): # # glanceclient v2 uses warlock to construct its objects. Warlock does # deep black magic to attribute look up to support validation things that - # means we cannot use normal obj_to_dict + # means we cannot use normal obj_to_munch obj_dict = munch.Munch() for (key, value) in obj.items(): if isinstance(value, NON_CALLABLES) and not key.startswith('_'): @@ -578,7 +584,7 @@ def get_and_munchify(key, data): """ result = data.get(key, []) if isinstance(result, list): - return obj_list_to_dict(result) + return obj_list_to_munch(result) elif isinstance(result, dict): - return obj_to_dict(result) + return obj_to_munch(result) return result diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 13343e843..8324a9ee2 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2013,13 +2013,13 @@ def list_images(self, filter_deleted=True): # If this doesn't work - we just let the exception propagate response = self._compute_client.get('/images/detail') while 'next' in response: - image_list.extend(meta.obj_list_to_dict(response['images'])) + image_list.extend(meta.obj_list_to_munch(response['images'])) endpoint = response['next'] # Use the raw endpoint from the catalog not the one from # version discovery so that the next links will work right response = self._raw_image_client.get(endpoint) if 'images' in response: - image_list.extend(meta.obj_list_to_dict(response['images'])) + image_list.extend(meta.obj_list_to_munch(response['images'])) else: image_list.extend(response) diff --git a/shade/task_manager.py b/shade/task_manager.py index 9546653fb..885df8be5 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -143,9 +143,9 @@ def wait(self, raw=False): return self._result if _is_listlike(self._result): - return meta.obj_list_to_dict(self._result) + return meta.obj_list_to_munch(self._result) elif _is_objlike(self._result): - return meta.obj_to_dict(self._result) + return meta.obj_to_munch(self._result) else: return self._result @@ -184,10 +184,10 @@ def wait(self, raw=False): return self._result if _is_listlike(self._result): - return meta.obj_list_to_dict( + return meta.obj_list_to_munch( self._result, request_id=self._request_id) elif _is_objlike(self._result): - return meta.obj_to_dict(self._result, request_id=self._request_id) + return meta.obj_to_munch(self._result, request_id=self._request_id) return self._result diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index c037e2f7f..c79f784fe 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -214,7 +214,7 @@ def make_fake_image( def make_fake_machine(machine_name, machine_id=None): if not machine_id: machine_id = uuid.uuid4().hex - return meta.obj_to_dict(FakeMachine( + return meta.obj_to_munch(FakeMachine( id=machine_id, name=machine_name)) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 4a7f629aa..d4bcaffad 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -98,7 +98,7 @@ def setUp(self): cloud_config_fixture='clouds_cache.yaml') def _image_dict(self, fake_image): - return self.cloud._normalize_image(meta.obj_to_dict(fake_image)) + return self.cloud._normalize_image(meta.obj_to_munch(fake_image)) def _munch_images(self, fake_image): return self.cloud._normalize_images([fake_image]) @@ -127,18 +127,18 @@ def test_list_projects_v3(self): self.assertEqual( self.cloud._normalize_projects( - meta.obj_list_to_dict(first_response['projects'])), + meta.obj_list_to_munch(first_response['projects'])), self.cloud.list_projects()) self.assertEqual( self.cloud._normalize_projects( - meta.obj_list_to_dict(first_response['projects'])), + meta.obj_list_to_munch(first_response['projects'])), self.cloud.list_projects()) # invalidate the list_projects cache self.cloud.list_projects.invalidate(self.cloud) # ensure the new values are now retrieved self.assertEqual( self.cloud._normalize_projects( - meta.obj_list_to_dict(second_response['projects'])), + meta.obj_list_to_munch(second_response['projects'])), self.cloud.list_projects()) self.assert_calls() @@ -163,18 +163,18 @@ def test_list_projects_v2(self): self.assertEqual( self.cloud._normalize_projects( - meta.obj_list_to_dict(first_response['tenants'])), + meta.obj_list_to_munch(first_response['tenants'])), self.cloud.list_projects()) self.assertEqual( self.cloud._normalize_projects( - meta.obj_list_to_dict(first_response['tenants'])), + meta.obj_list_to_munch(first_response['tenants'])), self.cloud.list_projects()) # invalidate the list_projects cache self.cloud.list_projects.invalidate(self.cloud) # ensure the new values are now retrieved self.assertEqual( self.cloud._normalize_projects( - meta.obj_list_to_dict(second_response['tenants'])), + meta.obj_list_to_munch(second_response['tenants'])), self.cloud.list_projects()) self.assert_calls() @@ -194,10 +194,10 @@ def test_list_servers_no_herd(self, nova_mock): def test_list_volumes(self): fake_volume = fakes.FakeVolume('volume1', 'available', 'Volume 1 Display Name') - fake_volume_dict = meta.obj_to_dict(fake_volume) + fake_volume_dict = meta.obj_to_munch(fake_volume) fake_volume2 = fakes.FakeVolume('volume2', 'available', 'Volume 2 Display Name') - fake_volume2_dict = meta.obj_to_dict(fake_volume2) + fake_volume2_dict = meta.obj_to_munch(fake_volume2) self.register_uris([ dict(method='GET', uri=self.get_mock_url( @@ -224,10 +224,10 @@ def test_list_volumes(self): def test_list_volumes_creating_invalidates(self): fake_volume = fakes.FakeVolume('volume1', 'creating', 'Volume 1 Display Name') - fake_volume_dict = meta.obj_to_dict(fake_volume) + fake_volume_dict = meta.obj_to_munch(fake_volume) fake_volume2 = fakes.FakeVolume('volume2', 'available', 'Volume 2 Display Name') - fake_volume2_dict = meta.obj_to_dict(fake_volume2) + fake_volume2_dict = meta.obj_to_munch(fake_volume2) self.register_uris([ dict(method='GET', uri=self.get_mock_url( @@ -247,12 +247,12 @@ def test_list_volumes_creating_invalidates(self): self.assert_calls() def test_create_volume_invalidates(self): - fake_volb4 = meta.obj_to_dict( + fake_volb4 = meta.obj_to_munch( fakes.FakeVolume('volume1', 'available', '')) _id = '12345' - fake_vol_creating = meta.obj_to_dict( + fake_vol_creating = meta.obj_to_munch( fakes.FakeVolume(_id, 'creating', '')) - fake_vol_avail = meta.obj_to_dict( + fake_vol_avail = meta.obj_to_munch( fakes.FakeVolume(_id, 'available', '')) def now_deleting(request, context): diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index 4df7990db..ff64a1a41 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -176,7 +176,7 @@ def test_create_server_no_wait(self, mock_nova): ]) self.assertEqual( self.cloud._normalize_server( - meta.obj_to_dict(fake_server)), + meta.obj_to_munch(fake_server)), self.cloud.create_server( name='server-name', image=dict(id='image=id'), @@ -215,7 +215,7 @@ def test_create_server_with_admin_pass_no_wait(self, mock_nova): ]) self.assertEqual( self.cloud._normalize_server( - meta.obj_to_dict(fake_create_server)), + meta.obj_to_munch(fake_create_server)), self.cloud.create_server( name='server-name', image=dict(id='image=id'), flavor=dict(id='flavor-id'), admin_pass='ooBootheiX0edoh')) @@ -243,7 +243,7 @@ def test_create_server_with_admin_pass_wait(self, mock_nova, mock_wait): mock_nova.servers.get.return_value = fake_server # The wait returns non-password server mock_wait.return_value = self.cloud._normalize_server( - meta.obj_to_dict(fake_server)) + meta.obj_to_munch(fake_server)) server = self.cloud.create_server( name='server-name', image=dict(id='image-id'), @@ -257,7 +257,7 @@ def test_create_server_with_admin_pass_wait(self, mock_nova, mock_wait): self.assertEqual( server, self.cloud._normalize_server( - meta.obj_to_dict(fake_server_with_pass)) + meta.obj_to_munch(fake_server_with_pass)) ) self.assert_calls() diff --git a/shade/tests/unit/test_create_volume_snapshot.py b/shade/tests/unit/test_create_volume_snapshot.py index 49882fdad..2d1636f2f 100644 --- a/shade/tests/unit/test_create_volume_snapshot.py +++ b/shade/tests/unit/test_create_volume_snapshot.py @@ -34,10 +34,10 @@ def test_create_volume_snapshot_wait(self): volume_id = '1234' build_snapshot = fakes.FakeVolumeSnapshot(snapshot_id, 'creating', 'foo', 'derpysnapshot') - build_snapshot_dict = meta.obj_to_dict(build_snapshot) + build_snapshot_dict = meta.obj_to_munch(build_snapshot) fake_snapshot = fakes.FakeVolumeSnapshot(snapshot_id, 'available', 'foo', 'derpysnapshot') - fake_snapshot_dict = meta.obj_to_dict(fake_snapshot) + fake_snapshot_dict = meta.obj_to_munch(fake_snapshot) self.register_uris([ dict(method='POST', @@ -70,7 +70,7 @@ def test_create_volume_snapshot_with_timeout(self): volume_id = '1234' build_snapshot = fakes.FakeVolumeSnapshot(snapshot_id, 'creating', 'foo', 'derpysnapshot') - build_snapshot_dict = meta.obj_to_dict(build_snapshot) + build_snapshot_dict = meta.obj_to_munch(build_snapshot) self.register_uris([ dict(method='POST', @@ -99,10 +99,10 @@ def test_create_volume_snapshot_with_error(self): volume_id = '1234' build_snapshot = fakes.FakeVolumeSnapshot(snapshot_id, 'creating', 'bar', 'derpysnapshot') - build_snapshot_dict = meta.obj_to_dict(build_snapshot) + build_snapshot_dict = meta.obj_to_munch(build_snapshot) error_snapshot = fakes.FakeVolumeSnapshot(snapshot_id, 'error', 'blah', 'derpysnapshot') - error_snapshot_dict = meta.obj_to_dict(error_snapshot) + error_snapshot_dict = meta.obj_to_munch(error_snapshot) self.register_uris([ dict(method='POST', diff --git a/shade/tests/unit/test_delete_volume_snapshot.py b/shade/tests/unit/test_delete_volume_snapshot.py index 355e08e01..3d7920b6a 100644 --- a/shade/tests/unit/test_delete_volume_snapshot.py +++ b/shade/tests/unit/test_delete_volume_snapshot.py @@ -32,7 +32,7 @@ def test_delete_volume_snapshot(self): """ fake_snapshot = fakes.FakeVolumeSnapshot('1234', 'available', 'foo', 'derpysnapshot') - fake_snapshot_dict = meta.obj_to_dict(fake_snapshot) + fake_snapshot_dict = meta.obj_to_munch(fake_snapshot) self.register_uris([ dict(method='GET', @@ -56,7 +56,7 @@ def test_delete_volume_snapshot_with_error(self): """ fake_snapshot = fakes.FakeVolumeSnapshot('1234', 'available', 'foo', 'derpysnapshot') - fake_snapshot_dict = meta.obj_to_dict(fake_snapshot) + fake_snapshot_dict = meta.obj_to_munch(fake_snapshot) self.register_uris([ dict(method='GET', uri=self.get_mock_url( @@ -81,7 +81,7 @@ def test_delete_volume_snapshot_with_timeout(self): """ fake_snapshot = fakes.FakeVolumeSnapshot('1234', 'available', 'foo', 'derpysnapshot') - fake_snapshot_dict = meta.obj_to_dict(fake_snapshot) + fake_snapshot_dict = meta.obj_to_munch(fake_snapshot) self.register_uris([ dict(method='GET', diff --git a/shade/tests/unit/test_floating_ip_common.py b/shade/tests/unit/test_floating_ip_common.py index d208afcb0..4e813cf11 100644 --- a/shade/tests/unit/test_floating_ip_common.py +++ b/shade/tests/unit/test_floating_ip_common.py @@ -38,7 +38,7 @@ def test_add_auto_ip( server = FakeServer( id='server-id', name='test-server', status="ACTIVE", addresses={} ) - server_dict = meta.obj_to_dict(server) + server_dict = meta.obj_to_munch(server) floating_ip_dict = { "id": "this-is-a-floating-ip-id", "fixed_ip_address": None, @@ -64,7 +64,7 @@ def test_add_ips_to_server_pool( server = FakeServer( id='romeo', name='test-server', status="ACTIVE", addresses={} ) - server_dict = meta.obj_to_dict(server) + server_dict = meta.obj_to_munch(server) pool = 'nova' mock_nova_client.servers.get.return_value = server @@ -102,7 +102,7 @@ def test_add_ips_to_server_ipv6_only( } ) server_dict = meta.add_server_interfaces( - self.cloud, meta.obj_to_dict(server)) + self.cloud, meta.obj_to_munch(server)) new_server = self.cloud.add_ips_to_server(server=server_dict) mock_get_floating_ip.assert_not_called() @@ -143,7 +143,7 @@ def test_add_ips_to_server_rackspace( } ) server_dict = meta.add_server_interfaces( - self.cloud, meta.obj_to_dict(server)) + self.cloud, meta.obj_to_munch(server)) new_server = self.cloud.add_ips_to_server(server=server_dict) mock_get_floating_ip.assert_not_called() @@ -180,7 +180,7 @@ def test_add_ips_to_server_rackspace_local_ipv4( } ) server_dict = meta.add_server_interfaces( - self.cloud, meta.obj_to_dict(server)) + self.cloud, meta.obj_to_munch(server)) new_server = self.cloud.add_ips_to_server(server=server_dict) mock_get_floating_ip.assert_not_called() @@ -194,7 +194,7 @@ def test_add_ips_to_server_ip_list( server = FakeServer( id='server-id', name='test-server', status="ACTIVE", addresses={} ) - server_dict = meta.obj_to_dict(server) + server_dict = meta.obj_to_munch(server) ips = ['203.0.113.29', '172.24.4.229'] mock_nova_client.servers.get.return_value = server @@ -211,7 +211,7 @@ def test_add_ips_to_server_auto_ip( server = FakeServer( id='server-id', name='test-server', status="ACTIVE", addresses={} ) - server_dict = meta.obj_to_dict(server) + server_dict = meta.obj_to_munch(server) mock_nova_client.servers.get.return_value = server # TODO(mordred) REMOVE THIS MOCK WHEN THE NEXT PATCH LANDS diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 72e5c515f..5588bfcb3 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -136,7 +136,7 @@ def assertAreInstances(self, elements, elem_type): def setUp(self): super(TestFloatingIP, self).setUp() - self.fake_server = meta.obj_to_dict( + self.fake_server = meta.obj_to_munch( fakes.FakeServer( 'server-id', '', 'ACTIVE', addresses={u'test_pnztt_net': [{ diff --git a/shade/tests/unit/test_floating_ip_nova.py b/shade/tests/unit/test_floating_ip_nova.py index cc0acd18d..afbbbf5f4 100644 --- a/shade/tests/unit/test_floating_ip_nova.py +++ b/shade/tests/unit/test_floating_ip_nova.py @@ -68,7 +68,7 @@ def assertAreInstances(self, elements, elem_type): def setUp(self): super(TestFloatingIP, self).setUp() - self.fake_server = meta.obj_to_dict( + self.fake_server = meta.obj_to_munch( fakes.FakeServer( 'server-id', '', 'ACTIVE', addresses={u'test_pnztt_net': [{ diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index 8f4bdc0c0..4d16fc01d 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -301,7 +301,7 @@ def test_create_image_task(self): self.assert_calls() def _image_dict(self, fake_image): - return self.cloud._normalize_image(meta.obj_to_dict(fake_image)) + return self.cloud._normalize_image(meta.obj_to_munch(fake_image)) def _munch_images(self, fake_image): return self.cloud._normalize_images([fake_image]) diff --git a/shade/tests/unit/test_inventory.py b/shade/tests/unit/test_inventory.py index 34914fdb6..9a5632953 100644 --- a/shade/tests/unit/test_inventory.py +++ b/shade/tests/unit/test_inventory.py @@ -101,7 +101,7 @@ def test_list_hosts_no_detail(self, mock_cloud, mock_config): inv = inventory.OpenStackInventory() server = self.cloud._normalize_server( - meta.obj_to_dict(fakes.FakeServer( + meta.obj_to_munch(fakes.FakeServer( '1234', 'test', 'ACTIVE', addresses={}))) self.assertIsInstance(inv.clouds, list) self.assertEqual(1, len(inv.clouds)) diff --git a/shade/tests/unit/test_keypair.py b/shade/tests/unit/test_keypair.py index 2d8698554..8094d7d56 100644 --- a/shade/tests/unit/test_keypair.py +++ b/shade/tests/unit/test_keypair.py @@ -36,7 +36,7 @@ def test_create_keypair(self, mock_nova): mock_nova.keypairs.create.assert_called_once_with( name=keyname, public_key=pub_key ) - self.assertEqual(meta.obj_to_dict(key), new_key) + self.assertEqual(meta.obj_to_munch(key), new_key) @patch.object(shade.OpenStackCloud, 'nova_client') def test_create_keypair_exception(self, mock_nova): diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 4ea2da865..1b50dd37d 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -267,7 +267,7 @@ def test_find_nova_addresses_all(self): addrs, key_name='public', ext_tag='fixed', version=6)) def test_get_server_ip(self): - srv = meta.obj_to_dict(standard_fake_server) + srv = meta.obj_to_munch(standard_fake_server) self.assertEqual( PRIVATE_V4, meta.get_server_ip(srv, ext_tag='fixed')) self.assertEqual( @@ -286,7 +286,7 @@ def test_get_server_private_ip(self): json={'subnets': SUBNETS_WITH_NAT}) ]) - srv = meta.obj_to_dict(fakes.FakeServer( + srv = meta.obj_to_munch(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', addresses={'private': [{'OS-EXT-IPS:type': 'fixed', 'addr': PRIVATE_V4, @@ -315,7 +315,7 @@ def test_get_server_multiple_private_ip(self): shared_mac = '11:22:33:44:55:66' distinct_mac = '66:55:44:33:22:11' - srv = meta.obj_to_dict(fakes.FakeServer( + srv = meta.obj_to_munch(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', addresses={'test-net': [{'OS-EXT-IPS:type': 'fixed', 'OS-EXT-IPS-MAC:mac_addr': distinct_mac, @@ -383,7 +383,7 @@ def test_get_server_private_ip_devstack( json={'security_groups': []}) ]) - srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( + srv = self.cloud.get_openstack_vars(meta.obj_to_munch(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', flavor={u'id': u'1'}, image={ @@ -433,7 +433,7 @@ def test_get_server_private_ip_no_fip( json={'security_groups': []}) ]) - srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( + srv = self.cloud.get_openstack_vars(meta.obj_to_munch(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', flavor={u'id': u'1'}, image={ @@ -483,7 +483,7 @@ def test_get_server_cloud_no_fips( json={'security_groups': []}) ]) - srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( + srv = self.cloud.get_openstack_vars(meta.obj_to_munch(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', flavor={u'id': u'1'}, image={ @@ -551,7 +551,7 @@ def test_get_server_cloud_missing_fips( json={'security_groups': []}) ]) - srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( + srv = self.cloud.get_openstack_vars(meta.obj_to_munch(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', flavor={u'id': u'1'}, image={ @@ -588,7 +588,7 @@ def test_get_server_cloud_rackspace_v6( json={'security_groups': []}) ]) - srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( + srv = self.cloud.get_openstack_vars(meta.obj_to_munch(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', flavor={u'id': u'1'}, image={ @@ -647,7 +647,7 @@ def test_get_server_cloud_osic_split( json={'security_groups': []}) ]) - srv = self.cloud.get_openstack_vars(meta.obj_to_dict(fakes.FakeServer( + srv = self.cloud.get_openstack_vars(meta.obj_to_munch(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', flavor={u'id': u'1'}, image={ @@ -690,7 +690,7 @@ def test_get_server_external_ipv4_neutron(self): uri='https://network.example.com/v2.0/subnets.json', json={'subnets': SUBNETS_WITH_NAT}) ]) - srv = meta.obj_to_dict(fakes.FakeServer( + srv = meta.obj_to_munch(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', addresses={'test-net': [{ 'addr': PUBLIC_V4, @@ -717,7 +717,7 @@ def test_get_server_external_provider_ipv4_neutron(self): json={'subnets': SUBNETS_WITH_NAT}) ]) - srv = meta.obj_to_dict(fakes.FakeServer( + srv = meta.obj_to_munch(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', addresses={'test-net': [{ 'addr': PUBLIC_V4, @@ -744,7 +744,7 @@ def test_get_server_internal_provider_ipv4_neutron(self): uri='https://network.example.com/v2.0/subnets.json', json={'subnets': SUBNETS_WITH_NAT}) ]) - srv = meta.obj_to_dict(fakes.FakeServer( + srv = meta.obj_to_munch(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', addresses={'test-net': [{ 'addr': PRIVATE_V4, @@ -772,7 +772,7 @@ def test_get_server_external_none_ipv4_neutron(self): json={'subnets': SUBNETS_WITH_NAT}) ]) - srv = meta.obj_to_dict(fakes.FakeServer( + srv = meta.obj_to_munch(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', addresses={'test-net': [{ 'addr': PUBLIC_V4, @@ -784,7 +784,7 @@ def test_get_server_external_none_ipv4_neutron(self): self.assert_calls() def test_get_server_external_ipv4_neutron_accessIPv4(self): - srv = meta.obj_to_dict(fakes.FakeServer( + srv = meta.obj_to_munch(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', accessIPv4=PUBLIC_V4)) ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) @@ -792,7 +792,7 @@ def test_get_server_external_ipv4_neutron_accessIPv4(self): self.assertEqual(PUBLIC_V4, ip) def test_get_server_external_ipv4_neutron_accessIPv6(self): - srv = meta.obj_to_dict(fakes.FakeServer( + srv = meta.obj_to_munch(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', accessIPv6=PUBLIC_V6)) ip = meta.get_server_external_ipv6(server=srv) @@ -806,7 +806,7 @@ def test_get_server_external_ipv4_neutron_exception(self): uri='https://network.example.com/v2.0/networks.json', status_code=404)]) - srv = meta.obj_to_dict(fakes.FakeServer( + srv = meta.obj_to_munch(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', addresses={'public': [{'addr': PUBLIC_V4, 'version': 4}]} )) @@ -819,7 +819,7 @@ def test_get_server_external_ipv4_nova_public(self): # Testing Clouds w/o Neutron and a network named public self.cloud.cloud_config.config['has_network'] = False - srv = meta.obj_to_dict(fakes.FakeServer( + srv = meta.obj_to_munch(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', addresses={'public': [{'addr': PUBLIC_V4, 'version': 4}]})) ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) @@ -830,7 +830,7 @@ def test_get_server_external_ipv4_nova_none(self): # Testing Clouds w/o Neutron or a globally routable IP self.cloud.cloud_config.config['has_network'] = False - srv = meta.obj_to_dict(fakes.FakeServer( + srv = meta.obj_to_munch(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', addresses={'test-net': [{'addr': PRIVATE_V4}]})) ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) @@ -838,7 +838,7 @@ def test_get_server_external_ipv4_nova_none(self): self.assertIsNone(ip) def test_get_server_external_ipv6(self): - srv = meta.obj_to_dict(fakes.FakeServer( + srv = meta.obj_to_munch(fakes.FakeServer( id='test-id', name='test-name', status='ACTIVE', addresses={ 'test-net': [ @@ -866,12 +866,12 @@ def test_get_groups_from_server(self): 'test-name_test-region_test-az'], meta.get_groups_from_server( FakeCloud(), - meta.obj_to_dict(standard_fake_server), + meta.obj_to_munch(standard_fake_server), server_vars ) ) - def test_obj_list_to_dict(self): + def test_obj_list_to_munch(self): """Test conversion of a list of objects to a list of dictonaries""" class obj0(object): value = 0 @@ -880,7 +880,7 @@ class obj1(object): value = 1 list = [obj0, obj1] - new_list = meta.obj_list_to_dict(list) + new_list = meta.obj_list_to_munch(list) self.assertEqual(new_list[0]['value'], 0) self.assertEqual(new_list[1]['value'], 1) @@ -894,7 +894,7 @@ def test_get_security_groups(self, mock_list_server_security_groups.return_value = [ {'name': 'testgroup', 'id': '1'}] - server = meta.obj_to_dict(standard_fake_server) + server = meta.obj_to_munch(standard_fake_server) hostvars = meta.get_hostvars_from_server(FakeCloud(), server) mock_list_server_security_groups.assert_called_once_with(server) @@ -911,7 +911,7 @@ def test_basic_hostvars( hostvars = meta.get_hostvars_from_server( FakeCloud(), self.cloud._normalize_server( - meta.obj_to_dict(standard_fake_server))) + meta.obj_to_munch(standard_fake_server))) self.assertNotIn('links', hostvars) self.assertEqual(PRIVATE_V4, hostvars['private_v4']) self.assertEqual(PUBLIC_V4, hostvars['public_v4']) @@ -946,7 +946,7 @@ def test_ipv4_hostvars( fake_cloud = FakeCloud() fake_cloud.force_ipv4 = True hostvars = meta.get_hostvars_from_server( - fake_cloud, meta.obj_to_dict(standard_fake_server)) + fake_cloud, meta.obj_to_munch(standard_fake_server)) self.assertEqual(PUBLIC_V4, hostvars['interface_ip']) @mock.patch.object(shade.meta, 'get_server_external_ipv4') @@ -956,7 +956,7 @@ def test_private_interface_ip(self, mock_get_server_external_ipv4): cloud = FakeCloud() cloud.private = True hostvars = meta.get_hostvars_from_server( - cloud, meta.obj_to_dict(standard_fake_server)) + cloud, meta.obj_to_munch(standard_fake_server)) self.assertEqual(PRIVATE_V4, hostvars['interface_ip']) @mock.patch.object(shade.meta, 'get_server_external_ipv4') @@ -966,14 +966,14 @@ def test_image_string(self, mock_get_server_external_ipv4): server = standard_fake_server server.image = 'fake-image-id' hostvars = meta.get_hostvars_from_server( - FakeCloud(), meta.obj_to_dict(server)) + FakeCloud(), meta.obj_to_munch(server)) self.assertEqual('fake-image-id', hostvars['image']['id']) def test_az(self): server = standard_fake_server server.__dict__['OS-EXT-AZ:availability_zone'] = 'az1' - hostvars = self.cloud._normalize_server(meta.obj_to_dict(server)) + hostvars = self.cloud._normalize_server(meta.obj_to_munch(server)) self.assertEqual('az1', hostvars['az']) def test_current_location(self): @@ -1005,10 +1005,10 @@ def test_has_volume(self): status='available', name='Volume 1 Display Name', attachments=[{'device': '/dev/sda0'}]) - fake_volume_dict = meta.obj_to_dict(fake_volume) + fake_volume_dict = meta.obj_to_munch(fake_volume) mock_cloud.get_volumes.return_value = [fake_volume_dict] hostvars = meta.get_hostvars_from_server( - mock_cloud, meta.obj_to_dict(standard_fake_server)) + mock_cloud, meta.obj_to_munch(standard_fake_server)) self.assertEqual('volume1', hostvars['volumes'][0]['id']) self.assertEqual('/dev/sda0', hostvars['volumes'][0]['device']) @@ -1016,7 +1016,7 @@ def test_has_no_volume_service(self): fake_cloud = FakeCloud() fake_cloud.service_val = False hostvars = meta.get_hostvars_from_server( - fake_cloud, meta.obj_to_dict(standard_fake_server)) + fake_cloud, meta.obj_to_munch(standard_fake_server)) self.assertEqual([], hostvars['volumes']) def test_unknown_volume_exception(self): @@ -1032,12 +1032,12 @@ def side_effect(*args): FakeException, meta.get_hostvars_from_server, mock_cloud, - meta.obj_to_dict(standard_fake_server)) + meta.obj_to_munch(standard_fake_server)) - def test_obj_to_dict(self): + def test_obj_to_munch(self): cloud = FakeCloud() cloud.server = standard_fake_server - cloud_dict = meta.obj_to_dict(cloud) + cloud_dict = meta.obj_to_munch(cloud) self.assertEqual(FakeCloud.name, cloud_dict['name']) self.assertNotIn('_unused', cloud_dict) self.assertNotIn('get_flavor_name', cloud_dict) @@ -1045,11 +1045,11 @@ def test_obj_to_dict(self): self.assertTrue(hasattr(cloud_dict, 'name')) self.assertEqual(cloud_dict.name, cloud_dict['name']) - def test_obj_to_dict_subclass(self): + def test_obj_to_munch_subclass(self): class FakeObjDict(dict): additional = 1 obj = FakeObjDict(foo='bar') - obj_dict = meta.obj_to_dict(obj) + obj_dict = meta.obj_to_munch(obj) self.assertIn('additional', obj_dict) self.assertIn('foo', obj_dict) self.assertEqual(obj_dict['additional'], 1) diff --git a/shade/tests/unit/test_rebuild_server.py b/shade/tests/unit/test_rebuild_server.py index f9d0714d6..9000c4815 100644 --- a/shade/tests/unit/test_rebuild_server.py +++ b/shade/tests/unit/test_rebuild_server.py @@ -92,7 +92,7 @@ def test_rebuild_server_no_wait(self, mock_nova): """ rebuild_server = fakes.FakeServer('1234', '', 'REBUILD') mock_nova.servers.rebuild.return_value = rebuild_server - self.assertEqual(meta.obj_to_dict(rebuild_server), + self.assertEqual(meta.obj_to_munch(rebuild_server), self.cloud.rebuild_server("a", "b")) @mock.patch.object(OpenStackCloud, 'nova_client') @@ -104,7 +104,7 @@ def test_rebuild_server_with_admin_pass_no_wait(self, mock_nova): adminPass='ooBootheiX0edoh') mock_nova.servers.rebuild.return_value = rebuild_server self.assertEqual( - meta.obj_to_dict(rebuild_server), + meta.obj_to_munch(rebuild_server), self.cloud.rebuild_server( 'a', 'b', admin_pass='ooBootheiX0edoh')) @@ -141,7 +141,7 @@ def test_rebuild_server_with_admin_pass_wait(self, mock_nova): self.cloud.name = 'cloud-name' self.assertEqual( self.cloud._normalize_server( - meta.obj_to_dict(ret_active_server)), + meta.obj_to_munch(ret_active_server)), self.cloud.rebuild_server( "1234", "b", wait=True, admin_pass='ooBootheiX0edoh')) # TODO(slaweq): change do_count to True when all nova mocks will be @@ -179,7 +179,7 @@ def test_rebuild_server_wait(self, mock_nova): self.cloud.name = 'cloud-name' self.assertEqual( self.cloud._normalize_server( - meta.obj_to_dict(active_server)), + meta.obj_to_munch(active_server)), self.cloud.rebuild_server("1234", "b", wait=True)) # TODO(slaweq): change do_count to True when all nova mocks will be # replaced with request_mocks also diff --git a/shade/tests/unit/test_security_groups.py b/shade/tests/unit/test_security_groups.py index a09517d5c..cad328edf 100644 --- a/shade/tests/unit/test_security_groups.py +++ b/shade/tests/unit/test_security_groups.py @@ -20,7 +20,7 @@ # TODO(mordred): Make a fakes.make_fake_nova_security_group and a # fakes.make_fake_nova_security_group and remove all uses of -# meta.obj_to_dict here. Also, we have hardcoded id names - +# meta.obj_to_munch here. Also, we have hardcoded id names - # move those to using a getUniqueString() value. neutron_grp_obj = fakes.FakeSecgroup( @@ -47,8 +47,8 @@ # Neutron returns dicts instead of objects, so the dict versions should # be used as expected return values from neutron API methods. -neutron_grp_dict = meta.obj_to_dict(neutron_grp_obj) -nova_grp_dict = meta.obj_to_dict(nova_grp_obj) +neutron_grp_dict = meta.obj_to_munch(neutron_grp_obj) +nova_grp_dict = meta.obj_to_munch(nova_grp_obj) class TestSecurityGroups(base.RequestsMockTestCase): @@ -163,7 +163,7 @@ def test_create_security_group_neutron(self): self.cloud.secgroup_source = 'neutron' group_name = self.getUniqueString() group_desc = 'security group from test_create_security_group_neutron' - new_group = meta.obj_to_dict( + new_group = meta.obj_to_munch( fakes.FakeSecgroup( id='2', name=group_name, @@ -194,7 +194,7 @@ def test_create_security_group_neutron_specific_tenant(self): group_name = self.getUniqueString() group_desc = 'security group from' \ ' test_create_security_group_neutron_specific_tenant' - new_group = meta.obj_to_dict( + new_group = meta.obj_to_munch( fakes.FakeSecgroup( id='2', name=group_name, @@ -230,7 +230,7 @@ def test_create_security_group_nova(self): group_name = self.getUniqueString() self.has_neutron = False group_desc = 'security group from test_create_security_group_neutron' - new_group = meta.obj_to_dict( + new_group = meta.obj_to_munch( fakes.FakeSecgroup( id='2', name=group_name, @@ -266,7 +266,7 @@ def test_update_security_group_neutron(self): self.cloud.secgroup_source = 'neutron' new_name = self.getUniqueString() sg_id = neutron_grp_obj.id - update_return = meta.obj_to_dict(neutron_grp_obj) + update_return = meta.obj_to_munch(neutron_grp_obj) update_return['name'] = new_name self.register_uris([ dict(method='GET', @@ -291,7 +291,7 @@ def test_update_security_group_nova(self): new_name = self.getUniqueString() self.cloud.secgroup_source = 'nova' nova_return = [nova_grp_dict] - update_return = meta.obj_to_dict(nova_grp_obj) + update_return = meta.obj_to_munch(nova_grp_obj) update_return['name'] = new_name self.register_uris([ @@ -408,7 +408,7 @@ def test_create_security_group_rule_nova(self): nova_return = [nova_grp_dict] - new_rule = meta.obj_to_dict(fakes.FakeNovaSecgroupRule( + new_rule = meta.obj_to_munch(fakes.FakeNovaSecgroupRule( id='xyz', from_port=1, to_port=2000, ip_protocol='tcp', cidr='1.2.3.4/32')) @@ -448,7 +448,7 @@ def test_create_security_group_rule_nova_no_ports(self): nova_return = [nova_grp_dict] - new_rule = meta.obj_to_dict(fakes.FakeNovaSecgroupRule( + new_rule = meta.obj_to_munch(fakes.FakeNovaSecgroupRule( id='xyz', from_port=1, to_port=65535, ip_protocol='tcp', cidr='1.2.3.4/32')) @@ -627,7 +627,7 @@ def test_add_security_group_to_server_neutron(self): uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), - json={'servers': [meta.obj_to_dict(fake_server)]}), + json={'servers': [meta.obj_to_munch(fake_server)]}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', @@ -680,7 +680,7 @@ def test_remove_security_group_from_server_neutron(self): uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), - json={'servers': [meta.obj_to_dict(fake_server)]}), + json={'servers': [meta.obj_to_munch(fake_server)]}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', @@ -697,7 +697,7 @@ def test_remove_security_group_from_server_neutron(self): def test_add_bad_security_group_to_server_nova(self): # fake to get server by name, server-name must match - fake_server = meta.obj_to_dict( + fake_server = meta.obj_to_munch( fakes.FakeServer('1234', 'server-name', 'ACTIVE')) # use nova for secgroup list and return an existing fake @@ -734,7 +734,7 @@ def test_add_bad_security_group_to_server_neutron(self): uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), - json={'servers': [meta.obj_to_dict(fake_server)]}), + json={'servers': [meta.obj_to_munch(fake_server)]}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', @@ -747,7 +747,7 @@ def test_add_bad_security_group_to_server_neutron(self): def test_add_security_group_to_bad_server(self): # fake to get server by name, server-name must match - fake_server = meta.obj_to_dict( + fake_server = meta.obj_to_munch( fakes.FakeServer('1234', 'server-name', 'ACTIVE')) self.register_uris([ diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index dd4b539a0..fd3575e27 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -87,7 +87,7 @@ def test_list_machines(self, mock_client): mock_client.node.list.return_value = [m1] machines = self.op_cloud.list_machines() self.assertTrue(mock_client.node.list.called) - self.assertEqual(meta.obj_to_dict(m1), machines[0]) + self.assertEqual(meta.obj_to_munch(m1), machines[0]) @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_validate_node(self, mock_client): @@ -102,7 +102,7 @@ def test_list_nics(self, mock_client): port1 = fakes.FakeMachinePort(1, "aa:bb:cc:dd", "node1") port2 = fakes.FakeMachinePort(2, "dd:cc:bb:aa", "node2") port_list = [port1, port2] - port_dict_list = meta.obj_list_to_dict(port_list) + port_dict_list = meta.obj_list_to_munch(port_list) mock_client.port.list.return_value = port_list nics = self.op_cloud.list_nics() diff --git a/shade/tests/unit/test_stack.py b/shade/tests/unit/test_stack.py index 52cbde33d..cc83eb2ec 100644 --- a/shade/tests/unit/test_stack.py +++ b/shade/tests/unit/test_stack.py @@ -74,7 +74,7 @@ def test_search_stacks(self): ]) stacks = self.cloud.search_stacks() self.assertEqual( - self.cloud._normalize_stacks(meta.obj_list_to_dict(fake_stacks)), + self.cloud._normalize_stacks(meta.obj_list_to_munch(fake_stacks)), stacks) self.assert_calls() @@ -96,7 +96,7 @@ def test_search_stacks_filters(self): stacks = self.cloud.search_stacks(filters=filters) self.assertEqual( self.cloud._normalize_stacks( - meta.obj_list_to_dict(fake_stacks[1:])), + meta.obj_list_to_munch(fake_stacks[1:])), stacks) self.assert_calls() diff --git a/shade/tests/unit/test_volume.py b/shade/tests/unit/test_volume.py index 8659bb3f6..a0f9475a4 100644 --- a/shade/tests/unit/test_volume.py +++ b/shade/tests/unit/test_volume.py @@ -25,7 +25,7 @@ def test_attach_volume(self): server = dict(id='server001') vol = {'id': 'volume001', 'status': 'available', 'name': '', 'attachments': []} - volume = meta.obj_to_dict(fakes.FakeVolume(**vol)) + volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) rattach = {'server_id': server['id'], 'device': 'device001', 'volumeId': volume['id'], 'id': 'attachmentId'} self.register_uris([ @@ -47,7 +47,7 @@ def test_attach_volume_exception(self): server = dict(id='server001') vol = {'id': 'volume001', 'status': 'available', 'name': '', 'attachments': []} - volume = meta.obj_to_dict(fakes.FakeVolume(**vol)) + volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) self.register_uris([ dict(method='POST', uri=self.get_mock_url( @@ -71,11 +71,11 @@ def test_attach_volume_wait(self): server = dict(id='server001') vol = {'id': 'volume001', 'status': 'available', 'name': '', 'attachments': []} - volume = meta.obj_to_dict(fakes.FakeVolume(**vol)) + volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) vol['attachments'] = [{'server_id': server['id'], 'device': 'device001'}] vol['status'] = 'attached' - attached_volume = meta.obj_to_dict(fakes.FakeVolume(**vol)) + attached_volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) rattach = {'server_id': server['id'], 'device': 'device001', 'volumeId': volume['id'], 'id': 'attachmentId'} self.register_uris([ @@ -105,9 +105,9 @@ def test_attach_volume_wait_error(self): server = dict(id='server001') vol = {'id': 'volume001', 'status': 'available', 'name': '', 'attachments': []} - volume = meta.obj_to_dict(fakes.FakeVolume(**vol)) + volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) vol['status'] = 'error' - errored_volume = meta.obj_to_dict(fakes.FakeVolume(**vol)) + errored_volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) rattach = {'server_id': server['id'], 'device': 'device001', 'volumeId': volume['id'], 'id': 'attachmentId'} self.register_uris([ @@ -201,10 +201,10 @@ def test_detach_volume_wait(self): attachments = [{'server_id': 'server001', 'device': 'device001'}] vol = {'id': 'volume001', 'status': 'attached', 'name': '', 'attachments': attachments} - volume = meta.obj_to_dict(fakes.FakeVolume(**vol)) + volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) vol['status'] = 'available' vol['attachments'] = [] - avail_volume = meta.obj_to_dict(fakes.FakeVolume(**vol)) + avail_volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) self.register_uris([ dict(method='DELETE', uri=self.get_mock_url( @@ -223,10 +223,10 @@ def test_detach_volume_wait_error(self): attachments = [{'server_id': 'server001', 'device': 'device001'}] vol = {'id': 'volume001', 'status': 'attached', 'name': '', 'attachments': attachments} - volume = meta.obj_to_dict(fakes.FakeVolume(**vol)) + volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) vol['status'] = 'error' vol['attachments'] = [] - errored_volume = meta.obj_to_dict(fakes.FakeVolume(**vol)) + errored_volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) self.register_uris([ dict(method='DELETE', uri=self.get_mock_url( @@ -247,7 +247,7 @@ def test_detach_volume_wait_error(self): def test_delete_volume_deletes(self): vol = {'id': 'volume001', 'status': 'attached', 'name': '', 'attachments': []} - volume = meta.obj_to_dict(fakes.FakeVolume(**vol)) + volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) self.register_uris([ dict(method='GET', uri=self.get_mock_url( @@ -266,7 +266,7 @@ def test_delete_volume_deletes(self): def test_delete_volume_gone_away(self): vol = {'id': 'volume001', 'status': 'attached', 'name': '', 'attachments': []} - volume = meta.obj_to_dict(fakes.FakeVolume(**vol)) + volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) self.register_uris([ dict(method='GET', uri=self.get_mock_url( @@ -280,8 +280,8 @@ def test_delete_volume_gone_away(self): self.assert_calls() def test_list_volumes_with_pagination(self): - vol1 = meta.obj_to_dict(fakes.FakeVolume('01', 'available', 'vol1')) - vol2 = meta.obj_to_dict(fakes.FakeVolume('02', 'available', 'vol2')) + vol1 = meta.obj_to_munch(fakes.FakeVolume('01', 'available', 'vol1')) + vol2 = meta.obj_to_munch(fakes.FakeVolume('02', 'available', 'vol2')) self.register_uris([ dict(method='GET', uri=self.get_mock_url( diff --git a/shade/tests/unit/test_volume_backups.py b/shade/tests/unit/test_volume_backups.py index 17298937a..ca04c1d8c 100644 --- a/shade/tests/unit/test_volume_backups.py +++ b/shade/tests/unit/test_volume_backups.py @@ -28,7 +28,7 @@ def test_search_volume_backups(self): name, {'availability_zone': 'az1'}) self.assertEqual(len(result), 2) self.assertEqual( - meta.obj_list_to_dict([vol1, vol2]), + meta.obj_list_to_munch([vol1, vol2]), result) self.assert_calls() @@ -44,9 +44,9 @@ def test_get_volume_backup(self): json={"backups": [vol1, vol2, vol3]})]) result = self.cloud.get_volume_backup( name, {'availability_zone': 'az1'}) - result = meta.obj_to_dict(result) + result = meta.obj_to_munch(result) self.assertEqual( - meta.obj_to_dict(vol1), + meta.obj_to_munch(vol1), result) self.assert_calls() @@ -63,7 +63,7 @@ def test_list_volume_backups(self): result = self.cloud.list_volume_backups(True, search_opts) self.assertEqual(len(result), 1) self.assertEqual( - meta.obj_list_to_dict([backup]), + meta.obj_list_to_munch([backup]), result) self.assert_calls() From ebedf176c7e6e00cf4ae6110f2cb5ace5a8c66e9 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 10 Jun 2017 21:35:01 +0000 Subject: [PATCH 1595/3836] Updated from global requirements Change-Id: I363e82513bb504ee7691c9283ab9763c40471fb6 --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 10eaa12dd..8f25d2a85 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,9 +19,9 @@ six>=1.9.0 # MIT futures>=3.0;python_version=='2.7' or python_version=='2.6' # BSD iso8601>=0.1.11 # MIT -keystoneauth1>=2.20.0 # Apache-2.0 +keystoneauth1>=2.21.0 # Apache-2.0 netifaces>=0.10.4 # MIT -python-novaclient>=7.1.0 # Apache-2.0 +python-novaclient>=9.0.0 # Apache-2.0 python-keystoneclient>=3.8.0 # Apache-2.0 python-ironicclient>=1.11.0 # Apache-2.0 From 54334fd6f07bc9a90f13a2c634f0e52655452684 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 10 Jun 2017 21:48:52 +0000 Subject: [PATCH 1596/3836] Updated from global requirements Change-Id: I89f967e740452ac30bbab6abbb37987b7f936ffe --- requirements.txt | 2 +- test-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 499232435..51fbe5698 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,5 +6,5 @@ jsonpatch>=1.1 # BSD six>=1.9.0 # MIT stevedore>=1.20.0 # Apache-2.0 os-client-config>=1.27.0 # Apache-2.0 -keystoneauth1>=2.20.0 # Apache-2.0 +keystoneauth1>=2.21.0 # Apache-2.0 deprecation>=1.0 # Apache-2.0 diff --git a/test-requirements.txt b/test-requirements.txt index 70d58f1d5..872a54ce3 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,7 +10,7 @@ mock>=2.0 # BSD python-subunit>=0.0.18 # Apache-2.0/BSD openstackdocstheme>=1.5.0 # Apache-2.0 os-testr>=0.8.0 # Apache-2.0 -requests!=2.12.2,!=2.13.0,>=2.10.0 # Apache-2.0 +requests>=2.14.2 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 sphinx!=1.6.1,>=1.5.1 # BSD testrepository>=0.0.18 # Apache-2.0/BSD From 5d204c32696456c4789985e6da503a30b118c854 Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Mon, 12 Jun 2017 15:56:12 +0000 Subject: [PATCH 1597/3836] Don't remove top-container for stack and zone REST API calls Change-Id: I502c7a7bcae7a7efd6541d50cc9837c04d48cb63 Signed-off-by: Rosario Di Somma --- shade/_adapter.py | 3 ++- shade/_heat/event_utils.py | 5 ++++- shade/meta.py | 2 +- shade/openstackcloud.py | 19 ++++++++++++------- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/shade/_adapter.py b/shade/_adapter.py index 013e28431..df68b8643 100644 --- a/shade/_adapter.py +++ b/shade/_adapter.py @@ -134,7 +134,8 @@ def _munch_response(self, response, result_key=None, error_message=None): 'volume_types', 'volume_type_access', 'snapshots', 'network', 'networks', 'subnet', 'subnets', 'router', 'routers', 'floatingip', 'floatingips', - 'floating_ip', 'floating_ips', 'port', 'ports']: + 'floating_ip', 'floating_ips', 'port', 'ports', + 'stack', 'stacks', 'zones', 'events']: if key in result_json.keys(): self._log_request_id(response) return result_json diff --git a/shade/_heat/event_utils.py b/shade/_heat/event_utils.py index 8f63daef1..69c286220 100644 --- a/shade/_heat/event_utils.py +++ b/shade/_heat/event_utils.py @@ -15,6 +15,8 @@ import collections import time +from shade import meta + def get_events(cloud, stack_id, event_args, marker=None, limit=None): # TODO(mordred) FIX THIS ONCE assert_calls CAN HANDLE QUERY STRINGS @@ -27,9 +29,10 @@ def get_events(cloud, stack_id, event_args, marker=None, limit=None): if limit: event_args['limit'] = limit - events = cloud._orchestration_client.get( + data = cloud._orchestration_client.get( '/stacks/{id}/events'.format(id=stack_id), params=params) + events = meta.get_and_munchify('events', data) # Show which stack the event comes from (for nested events) for e in events: diff --git a/shade/meta.py b/shade/meta.py index 207de980f..85c0f4310 100644 --- a/shade/meta.py +++ b/shade/meta.py @@ -582,7 +582,7 @@ def get_and_munchify(key, data): The value will be converted in a Munch object or a list of Munch objects based on the type """ - result = data.get(key, []) + result = data.get(key, []) if key else data if isinstance(result, list): return obj_list_to_munch(result) elif isinstance(result, dict): diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 0182018e1..edd8f43e8 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1761,9 +1761,10 @@ def list_stacks(self): :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ - stacks = self._orchestration_client.get( + data = self._orchestration_client.get( '/stacks', error_message="Error fetching stack list") - return self._normalize_stacks(stacks) + return self._normalize_stacks( + meta.get_and_munchify('stacks', data)) def list_server_security_groups(self, server): """List all security groups associated with the given server. @@ -2873,9 +2874,10 @@ def _search_one_stack(name_or_id=None, filters=None): # stack names are mandatory and enforced unique in the project # so a StackGet can always be used for name or ID. try: - stack = self._orchestration_client.get( + data = self._orchestration_client.get( '/stacks/{name_or_id}'.format(name_or_id=name_or_id), error_message="Error fetching stack") + stack = meta.get_and_munchify('stack', data) # Treat DELETE_COMPLETE stacks as a NotFound if stack['stack_status'] == 'DELETE_COMPLETE': return [] @@ -7009,9 +7011,10 @@ def list_zones(self): :returns: A list of zones dicts. """ - return self._dns_client.get( + data = self._dns_client.get( "/zones", error_message="Error fetching zones list") + return meta.get_and_munchify('zones', data) def get_zone(self, name_or_id, filters=None): """Get a zone by name or ID. @@ -7030,7 +7033,7 @@ def get_zone(self, name_or_id, filters=None): def search_zones(self, name_or_id=None, filters=None): zones = self.list_zones() - return _utils._filter_list(zones['zones'], name_or_id, filters) + return _utils._filter_list(zones, name_or_id, filters) def create_zone(self, name, zone_type=None, email=None, description=None, ttl=None, masters=None): @@ -7073,9 +7076,10 @@ def create_zone(self, name, zone_type=None, email=None, description=None, if masters is not None: zone["masters"] = masters - return self._dns_client.post( + data = self._dns_client.post( "/zones", json=zone, error_message="Unable to create zone {name}".format(name=name)) + return meta.get_and_munchify(key=None, data=data) @_utils.valid_kwargs('email', 'description', 'ttl', 'masters') def update_zone(self, name_or_id, **kwargs): @@ -7098,9 +7102,10 @@ def update_zone(self, name_or_id, **kwargs): raise OpenStackCloudException( "Zone %s not found." % name_or_id) - return self._dns_client.patch( + data = self._dns_client.patch( "/zones/{zone_id}".format(zone_id=zone['id']), json=kwargs, error_message="Error updating zone {0}".format(name_or_id)) + return meta.get_and_munchify(key=None, data=data) def delete_zone(self, name_or_id): """Delete a zone. From db83c23be7d04debc3804906e12613a1616814cb Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Mon, 12 Jun 2017 18:03:33 -0400 Subject: [PATCH 1598/3836] Improve grant docs on when and how use domain arg Change-Id: I76184b229d48d8bd3268c803400c2ab9d3fcab17 --- shade/operatorcloud.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index c0256661b..74f4317d2 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1688,6 +1688,11 @@ def grant_role(self, name_or_id, user=None, group=None, :param bool wait: Wait for role to be granted :param int timeout: Timeout to wait for role to be granted + NOTE: domain is a required argument when the grant is on a project, + user or group specified by name. In that situation, they are all + considered to be in that domain. If different domains are in use + in the same role grant, it is required to specify those by ID. + NOTE: for wait and timeout, sometimes granting roles is not instantaneous for granting roles. From 2b48aed90d7f236b0a72dbc90698ae7aa30bdfcf Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Mon, 12 Jun 2017 23:28:13 +0000 Subject: [PATCH 1599/3836] Don't remove top-container element for sec group REST API calls Change-Id: I81cdf17fede6414bdbbd7c7ea0a7eb5efc8aced3 Signed-off-by: Rosario Di Somma --- shade/_adapter.py | 4 +++- shade/openstackcloud.py | 41 ++++++++++++++++++++++------------------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/shade/_adapter.py b/shade/_adapter.py index df68b8643..b7e70a328 100644 --- a/shade/_adapter.py +++ b/shade/_adapter.py @@ -135,7 +135,9 @@ def _munch_response(self, response, result_key=None, error_message=None): 'network', 'networks', 'subnet', 'subnets', 'router', 'routers', 'floatingip', 'floatingips', 'floating_ip', 'floating_ips', 'port', 'ports', - 'stack', 'stacks', 'zones', 'events']: + 'stack', 'stacks', 'zones', 'events', + 'security_group', 'security_groups', + 'security_group_rule', 'security_group_rules']: if key in result_json.keys(): self._log_request_id(response) return result_json diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index edd8f43e8..1bb6bdc34 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1776,11 +1776,11 @@ def list_server_security_groups(self, server): if not self._has_secgroups(): return [] - groups = self._compute_client.get( + data = self._compute_client.get( '/servers/{server_id}/os-security-groups'.format( server_id=server['id'])) - - return self._normalize_secgroups(groups) + return self._normalize_secgroups( + meta.get_and_munchify('security_groups', data)) def _get_server_security_groups(self, server, security_groups): if not self._has_secgroups(): @@ -1891,19 +1891,21 @@ def list_security_groups(self, filters=None): if not filters: filters = {} - groups = [] + data = [] # Handle neutron security groups if self._use_neutron_secgroups(): # Neutron returns dicts, so no need to convert objects here. - groups = self._network_client.get( + data = self._network_client.get( '/security-groups.json', params=filters, error_message="Error fetching security group list") + return meta.get_and_munchify('security_groups', data) # Handle nova security groups else: - groups = self._compute_client.get( + data = self._compute_client.get( '/os-security-groups', params=filters) - return self._normalize_secgroups(groups) + return self._normalize_secgroups( + meta.get_and_munchify('security_groups', data)) def list_servers(self, detailed=False, all_projects=False, bare=False): """List all available servers. @@ -6745,7 +6747,7 @@ def create_security_group(self, name, description, project_id=None): "Unavailable feature: security groups" ) - group = None + data = [] security_group_json = { 'security_group': { 'name': name, 'description': description @@ -6753,14 +6755,15 @@ def create_security_group(self, name, description, project_id=None): if project_id is not None: security_group_json['security_group']['tenant_id'] = project_id if self._use_neutron_secgroups(): - group = self._network_client.post( + data = self._network_client.post( '/security-groups.json', json=security_group_json, error_message="Error creating security group {0}".format(name)) else: - group = self._compute_client.post( + data = self._compute_client.post( '/os-security-groups', json=security_group_json) - return self._normalize_secgroup(group) + return self._normalize_secgroup( + meta.get_and_munchify('security_group', data)) def delete_security_group(self, name_or_id): """Delete a security group @@ -6825,7 +6828,7 @@ def update_security_group(self, name_or_id, **kwargs): "Security group %s not found." % name_or_id) if self._use_neutron_secgroups(): - group = self._network_client.put( + data = self._network_client.put( '/security-groups/{sg_id}.json'.format(sg_id=group['id']), json={'security_group': kwargs}, error_message="Error updating security group {0}".format( @@ -6833,10 +6836,11 @@ def update_security_group(self, name_or_id, **kwargs): else: for key in ('name', 'description'): kwargs.setdefault(key, group[key]) - group = self._compute_client.put( + data = self._compute_client.put( '/os-security-groups/{id}'.format(id=group['id']), json={'security-group': kwargs}) - return self._normalize_secgroup(group) + return self._normalize_secgroup( + meta.get_and_munchify('security_group', data)) def create_security_group_rule(self, secgroup_name_or_id, @@ -6922,12 +6926,10 @@ def create_security_group_rule(self, if project_id is not None: rule_def['tenant_id'] = project_id - rule = self._network_client.post( + data = self._network_client.post( '/security-group-rules.json', json={'security_group_rule': rule_def}, error_message="Error creating security group rule") - return self._normalize_secgroup_rule(rule) - else: # NOTE: Neutron accepts None for protocol. Nova does not. if protocol is None: @@ -6968,10 +6970,11 @@ def create_security_group_rule(self, if project_id is not None: security_group_rule_dict[ 'security_group_rule']['tenant_id'] = project_id - rule = self._compute_client.post( + data = self._compute_client.post( '/os-security-group-rules', json=security_group_rule_dict ) - return self._normalize_secgroup_rule(rule) + return self._normalize_secgroup_rule( + meta.get_and_munchify('security_group_rule', data)) def delete_security_group_rule(self, rule_id): """Delete a security group rule From 92370249542302cfd288b0843c61170659b819e6 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Mon, 12 Jun 2017 17:46:11 -0400 Subject: [PATCH 1600/3836] De-client-ify Project List Change-Id: I13cb90a46ca61716b04989574483cfb4b04fa6b1 --- shade/_normalize.py | 2 +- shade/_tasks.py | 5 ----- shade/openstackcloud.py | 15 ++++++++++++--- shade/tests/unit/test_role_assignment.py | 16 ++++++++++------ 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/shade/_normalize.py b/shade/_normalize.py index 2ab17a58e..7e4dcc2fe 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -63,7 +63,7 @@ _pushdown_fields = { 'project': [ - 'domain' + 'domain_id' ] } diff --git a/shade/_tasks.py b/shade/_tasks.py index c62bf89d5..f89c0d255 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -47,11 +47,6 @@ def main(self, client): return client.keystone_client.users.remove_from_group(**self.args) -class ProjectList(task_manager.Task): - def main(self, client): - return client._project_manager.list(**self.args) - - class ProjectCreate(task_manager.Task): def main(self, client): return client._project_manager.create(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 0182018e1..aa8baa7de 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -823,15 +823,24 @@ def list_projects(self, domain_id=None, name_or_id=None, filters=None): """ kwargs = dict( filters=filters, - domain=domain_id) + domain_id=domain_id) if self.cloud_config.get_api_version('identity') == '3': kwargs['obj_name'] = 'project' pushdown, filters = _normalize._split_filters(**kwargs) try: - projects = self._normalize_projects( - self.manager.submit_task(_tasks.ProjectList(**pushdown))) + if self.cloud_config.get_api_version('identity') == '3': + projects = self._identity_client.get('/projects', + params=pushdown) + if isinstance(projects, dict): + projects = projects['projects'] + else: + projects = self._identity_client.get('/tenants', + params=pushdown) + if isinstance(projects, dict): + projects = projects['tenants'] + projects = self._normalize_projects(projects) except Exception as e: self.log.debug("Failed to list projects", exc_info=True) raise OpenStackCloudException(str(e)) diff --git a/shade/tests/unit/test_role_assignment.py b/shade/tests/unit/test_role_assignment.py index 782235966..dd08b2bda 100644 --- a/shade/tests/unit/test_role_assignment.py +++ b/shade/tests/unit/test_role_assignment.py @@ -2482,10 +2482,12 @@ def test_grant_both_project_and_domain(self): status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', - uri=self.get_mock_url(resource='projects'), + uri=self.get_mock_url( + resource=('projects?domain_id=%s' % + self.domain_data.domain_id)), status_code=200, - json={'projects': [ - self.project_data.json_response['project']]}), + json={'projects': + [self.project_data.json_response['project']]}), dict(method='GET', uri=self.get_mock_url( resource='role_assignments', @@ -2529,10 +2531,12 @@ def test_revoke_both_project_and_domain(self): status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', - uri=self.get_mock_url(resource='projects'), + uri=self.get_mock_url( + resource=('projects?domain_id=%s' % + self.domain_data.domain_id)), status_code=200, - json={'projects': [ - self.project_data.json_response['project']]}), + json={'projects': + [self.project_data.json_response['project']]}), dict(method='GET', uri=self.get_mock_url( resource='role_assignments', From d3df09107ead9d690461e9166c420b28050baadb Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Mon, 12 Jun 2017 20:54:07 -0400 Subject: [PATCH 1601/3836] De-client-ify Project Delete Change-Id: I24522698dbb333edc7eec6087637a252c0f92ff1 --- shade/_tasks.py | 5 ----- shade/openstackcloud.py | 6 ++---- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index f89c0d255..b4e35c9c3 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -52,11 +52,6 @@ def main(self, client): return client._project_manager.create(**self.args) -class ProjectDelete(task_manager.Task): - def main(self, client): - return client._project_manager.delete(**self.args) - - class ProjectUpdate(task_manager.Task): def main(self, client): return client._project_manager.update(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index aa8baa7de..c1414e75f 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -936,12 +936,10 @@ def delete_project(self, name_or_id, domain_id=None): "Project %s not found for deleting", name_or_id) return False - params = {} if self.cloud_config.get_api_version('identity') == '3': - params['project'] = project['id'] + self._identity_client.delete('/projects/' + project['id']) else: - params['tenant'] = project['id'] - self.manager.submit_task(_tasks.ProjectDelete(**params)) + self._identity_client.delete('/tenants/' + project['id']) return True From 18f9b64ced1aa7d294d844976a7ddde8a58bafc0 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Mon, 12 Jun 2017 18:48:28 -0400 Subject: [PATCH 1602/3836] De-client-ify Project Create Change-Id: I4003cda72ac6f4bfc083722983159409673d3098 --- shade/_tasks.py | 5 ----- shade/openstackcloud.py | 37 +++++++++++++++++++++++++------- shade/tests/unit/test_project.py | 13 ++--------- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index b4e35c9c3..717b632fa 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -47,11 +47,6 @@ def main(self, client): return client.keystone_client.users.remove_from_group(**self.args) -class ProjectCreate(task_manager.Task): - def main(self, client): - return client._project_manager.create(**self.args) - - class ProjectUpdate(task_manager.Task): def main(self, client): return client._project_manager.update(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index c1414e75f..53eba43a1 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -728,6 +728,25 @@ def _get_project_param_dict(self, name_or_id): project_dict['tenant_id'] = project['id'] return project_dict + def _get_domain_id_param_dict(self, domain_id): + """Get a useable domain.""" + + # Keystone v3 requires domains for user and project creation. v2 does + # not. However, keystone v2 does not allow user creation by non-admin + # users, so we can throw an error to the user that does not need to + # mention api versions + if self.cloud_config.get_api_version('identity') == '3': + if not domain_id: + raise OpenStackCloudException( + "User or project creation requires an explicit" + " domain_id argument.") + else: + return {'domain_id': domain_id} + else: + return {} + + # TODO(samueldmq): Get rid of this method once create_user is migrated to + # REST and, consequently, _get_domain_id_param_dict is used instead def _get_domain_param_dict(self, domain_id): """Get a useable domain.""" @@ -901,16 +920,18 @@ def create_project( """Create a project.""" with _utils.shade_exceptions( "Error in creating project {project}".format(project=name)): - params = self._get_domain_param_dict(domain_id) + project_ref = self._get_domain_id_param_dict(domain_id) + project_ref.update({'name': name, + 'description': description, + 'enabled': enabled}) + if self.cloud_config.get_api_version('identity') == '3': - params['name'] = name + project = self._identity_client.post( + '/projects', json={'project': project_ref}) else: - params['tenant_name'] = name - - project = self._normalize_project( - self.manager.submit_task(_tasks.ProjectCreate( - project_name=name, description=description, - enabled=enabled, **params))) + project = self._identity_client.post( + '/tenants', json={'tenant': project_ref}) + project = self._normalize_project(project) self.list_projects.invalidate(self) return project diff --git a/shade/tests/unit/test_project.py b/shade/tests/unit/test_project.py index 8c5bc678a..7a0bc81f4 100644 --- a/shade/tests/unit/test_project.py +++ b/shade/tests/unit/test_project.py @@ -39,12 +39,7 @@ def test_create_project_v2(self): self.register_uris([ dict(method='POST', uri=self.get_mock_url(v3=False), status_code=200, json=project_data.json_response, - validate=project_data.json_request), - dict(method='GET', - uri=self.get_mock_url( - v3=False, append=[project_data.project_id]), - status_code=200, - json=project_data.json_response) + validate=project_data.json_request) ]) project = self.op_cloud.create_project( name=project_data.project_name, @@ -62,11 +57,7 @@ def test_create_project_v3(self,): uri=self.get_mock_url(), status_code=200, json=project_data.json_response, - validate=project_data.json_request), - dict(method='GET', - uri=self.get_mock_url(append=[project_data.project_id]), - status_code=200, - json=project_data.json_response) + validate=project_data.json_request) ]) project = self.op_cloud.create_project( name=project_data.project_name, From 081930d3a925e7f0c8ed6fe9cd9d95818e3eca8a Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Mon, 12 Jun 2017 21:04:55 -0400 Subject: [PATCH 1603/3836] De-client-ify Project Update Change-Id: I2c9c6ebe594268b1be78af7b1e25f110605a076d --- shade/_tasks.py | 5 ----- shade/openstackcloud.py | 24 +++++++++++++----------- shade/tests/unit/test_project.py | 12 ++---------- 3 files changed, 15 insertions(+), 26 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 717b632fa..b71bbd4bd 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -47,11 +47,6 @@ def main(self, client): return client.keystone_client.users.remove_from_group(**self.args) -class ProjectUpdate(task_manager.Task): - def main(self, client): - return client._project_manager.update(**self.args) - - class ServerList(task_manager.Task): def main(self, client): return client.nova_client.servers.list(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 53eba43a1..fd4c41f29 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -891,8 +891,9 @@ def get_project(self, name_or_id, filters=None, domain_id=None): return _utils._get_entity(self.search_projects, name_or_id, filters, domain_id=domain_id) - def update_project(self, name_or_id, description=None, enabled=True, - domain_id=None): + @_utils.valid_kwargs('description') + def update_project(self, name_or_id, enabled=True, domain_id=None, + **kwargs): with _utils.shade_exceptions( "Error in updating project {project}".format( project=name_or_id)): @@ -901,17 +902,18 @@ def update_project(self, name_or_id, description=None, enabled=True, raise OpenStackCloudException( "Project %s not found." % name_or_id) - params = {} + kwargs.update({'enabled': enabled}) + # NOTE(samueldmq): Current code only allow updates of description + # or enabled fields. + # FIXME(samueldmq): enable=True is the default, meaning it will + # enable a disabled project if you simply update other fields if self.cloud_config.get_api_version('identity') == '3': - params['project'] = proj['id'] + project = self._identity_client.patch( + '/projects/' + proj['id'], json={'project': kwargs}) else: - params['tenant_id'] = proj['id'] - - project = self._normalize_project( - self.manager.submit_task(_tasks.ProjectUpdate( - description=description, - enabled=enabled, - **params))) + project = self._identity_client.post( + '/tenants/' + proj['id'], json={'tenant': kwargs}) + project = self._normalize_project(project) self.list_projects.invalidate(self) return project diff --git a/shade/tests/unit/test_project.py b/shade/tests/unit/test_project.py index 7a0bc81f4..5ac7d4361 100644 --- a/shade/tests/unit/test_project.py +++ b/shade/tests/unit/test_project.py @@ -145,12 +145,7 @@ def test_update_project_v2(self): v3=False, append=[project_data.project_id]), status_code=200, json=project_data.json_response, - validate=project_data.json_request), - dict(method='GET', - uri=self.get_mock_url( - v3=False, append=[project_data.project_id]), - status_code=200, - json=project_data.json_response) + validate=project_data.json_request) ]) project = self.op_cloud.update_project( project_data.project_id, @@ -175,10 +170,7 @@ def test_update_project_v3(self): dict(method='PATCH', uri=self.get_mock_url(append=[project_data.project_id]), status_code=200, json=project_data.json_response, - validate=project_data.json_request), - dict(method='GET', - uri=self.get_mock_url(append=[project_data.project_id]), - status_code=200, json=project_data.json_response) + validate=project_data.json_request) ]) project = self.op_cloud.update_project( project_data.project_id, From e6573737638383ba7f7b0c4021b3ceb381cf2bc2 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 13 Jun 2017 15:25:01 -0500 Subject: [PATCH 1604/3836] Cleanup volumes in functional tests in parallel We spin up 8 volumes for pagination, but then we try to clean them up in series. That's bad for our timeout. Let's take a page from nodepool and just poll the list. Change-Id: Icda01fd21dca06e459fbdd3c1cbd31441b95cd7a --- shade/tests/functional/test_volume.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/shade/tests/functional/test_volume.py b/shade/tests/functional/test_volume.py index 8136da4fe..4972130a0 100644 --- a/shade/tests/functional/test_volume.py +++ b/shade/tests/functional/test_volume.py @@ -19,6 +19,7 @@ from testtools import content +from shade import _utils from shade.tests.functional import base @@ -77,7 +78,7 @@ def test_volume_to_image(self): self.user_cloud.delete_image(image_name, wait=True) self.user_cloud.delete_volume(volume_name, wait=True) - def cleanup(self, volume_name, snapshot_name=None, image_name=None): + def cleanup(self, volume, snapshot_name=None, image_name=None): # Need to delete snapshots before volumes if snapshot_name: snapshot = self.user_cloud.get_volume_snapshot(snapshot_name) @@ -88,9 +89,23 @@ def cleanup(self, volume_name, snapshot_name=None, image_name=None): image = self.user_cloud.get_image(image_name) if image: self.user_cloud.delete_image(image_name, wait=True) - volume = self.user_cloud.get_volume(volume_name) - if volume: - self.user_cloud.delete_volume(volume_name, wait=True) + if not isinstance(volume, list): + self.user_cloud.delete_volume(volume, wait=True) + else: + # We have more than one volume to clean up - submit all of the + # deletes without wait, then poll until none of them are found + # in the volume list anymore + for v in volume: + self.user_cloud.delete_volume(v, wait=False) + for count in _utils._iterate_timeout( + 180, "Timeout waiting for volume cleanup"): + found = False + for existing in self.user_cloud.list_volumes(): + for v in volume: + if v['id'] == existing['id']: + found = True + if not found: + break def test_list_volumes_pagination(self): '''Test pagination for list volumes functionality''' @@ -101,9 +116,9 @@ def test_list_volumes_pagination(self): num_volumes = 8 for i in range(num_volumes): name = self.getUniqueString() - self.addCleanup(self.cleanup, name) v = self.user_cloud.create_volume(display_name=name, size=1) volumes.append(v) + self.addCleanup(self.cleanup, volumes) result = [] for i in self.user_cloud.list_volumes(): if i['name'] and i['name'].startswith(self.id()): From e5389891214837b50c15987b4337a0f4678422f8 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 12 Jun 2017 17:28:37 -0500 Subject: [PATCH 1605/3836] Add boot from volume unit tests The boot from volume parameters are complex and we don't test them. Change-Id: Ia4b8db8792d8343d1bc1c5525e24065f67b4646c --- shade/tests/unit/test_create_server.py | 108 +++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index ff64a1a41..612473302 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -440,3 +440,111 @@ def test_create_server_get_flavor_image( mock_image.assert_called_once() self.assert_calls() + + def test_create_boot_attach_volume(self): + build_server = fakes.FakeServer('1234', '', 'BUILD') + active_server = fakes.FakeServer('1234', '', 'BUILD') + + vol = {'id': 'volume001', 'status': 'available', + 'name': '', 'attachments': []} + volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': []}), + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['os-volumes_boot']), + json={'server': meta.obj_to_munch(build_server).toDict()}, + validate=dict( + json={'server': { + u'flavorRef': 'flavor-id', + u'imageRef': 'image-id', + u'max_count': 1, + u'min_count': 1, + u'block_device_mapping_v2': [ + { + u'boot_index': 0, + u'delete_on_termination': True, + u'destination_type': u'local', + u'source_type': u'image', + u'uuid': u'image-id' + }, + { + u'boot_index': u'-1', + u'delete_on_termination': False, + u'destination_type': u'volume', + u'source_type': u'volume', + u'uuid': u'volume001' + } + ], + u'name': u'server-name'}})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234']), + json={'server': meta.obj_to_munch(active_server).toDict()}), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234']), + json={'server': meta.obj_to_munch(active_server).toDict()}), + ]) + + self.cloud.create_server( + name='server-name', + image=dict(id='image-id'), + flavor=dict(id='flavor-id'), + boot_from_volume=False, + volumes=[volume], + wait=False) + + self.assert_calls() + + def test_create_boot_from_volume_image_terminate(self): + build_server = fakes.FakeServer('1234', '', 'BUILD') + active_server = fakes.FakeServer('1234', '', 'BUILD') + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': []}), + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['os-volumes_boot']), + json={'server': meta.obj_to_munch(build_server).toDict()}, + validate=dict( + json={'server': { + u'flavorRef': 'flavor-id', + u'imageRef': '', + u'max_count': 1, + u'min_count': 1, + u'block_device_mapping_v2': [{ + u'boot_index': u'0', + u'delete_on_termination': True, + u'destination_type': u'volume', + u'source_type': u'image', + u'uuid': u'image-id', + u'volume_size': u'1'}], + u'name': u'server-name'}})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234']), + json={'server': meta.obj_to_munch(active_server).toDict()}), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234']), + json={'server': meta.obj_to_munch(active_server).toDict()}), + ]) + + self.cloud.create_server( + name='server-name', + image=dict(id='image-id'), + flavor=dict(id='flavor-id'), + boot_from_volume=True, + terminate_volume=True, + volume_size=1, + wait=False) + + self.assert_calls() From 9cb4b1dcf73de64dd0cc79bbbd9b4c32878c619e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 12 Jun 2017 12:00:34 -0500 Subject: [PATCH 1606/3836] Convert create_server mocks to request_mock Next patch will convert create_server. Note: this found a few bugs in the tests. Change-Id: Ia1c9d3a02e52b63756f61dbff77b90dc35e45d24 --- shade/_normalize.py | 3 + shade/openstackcloud.py | 6 +- shade/tests/unit/test_create_server.py | 433 +++++++++++++++++-------- 3 files changed, 311 insertions(+), 131 deletions(-) diff --git a/shade/_normalize.py b/shade/_normalize.py index 2ab17a58e..ddf27a2aa 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -445,6 +445,9 @@ def _normalize_server(self, server): for field in _SERVER_FIELDS: ret[field] = server.pop(field, None) + if not ret['networks']: + ret['networks'] = {} + ret['interface_ip'] = '' ret['properties'] = server.copy() diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 0182018e1..e91ea8b74 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5522,8 +5522,12 @@ def create_server( "Error in creating the server.") if wait: + # TODO(mordred) The normalize call here is just for easing + # novaclient transition (otherwise the HUMAN id stuff leaks into + # mocks. It can be removed when we don't use novaclient server = self.wait_for_server( - server, auto_ip=auto_ip, ips=ips, ip_pool=ip_pool, + self._normalize_server(server), + auto_ip=auto_ip, ips=ips, ip_pool=ip_pool, reuse=reuse_ips, timeout=timeout, nat_destination=nat_destination, ) diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index 612473302..f7c22426a 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -16,8 +16,10 @@ Tests for the `create_server` command. """ +import uuid import mock + import shade from shade import exc from shade import meta @@ -27,45 +29,44 @@ class TestCreateServer(base.RequestsMockTestCase): - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_create_server_with_create_exception(self, mock_nova): - """ - Test that an exception in the novaclient create raises an exception in - create_server. - """ - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), - json={'networks': []}) - ]) - mock_nova.servers.create.side_effect = Exception("exception") - self.assertRaises( - exc.OpenStackCloudException, self.cloud.create_server, - 'server-name', {'id': 'image-id'}, {'id': 'flavor-id'}) - self.assert_calls() - - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_create_server_with_get_exception(self, mock_nova): + def test_create_server_with_get_exception(self): """ Test that an exception when attempting to get the server instance via the novaclient raises an exception in create_server. """ + build_server = fakes.FakeServer('1234', '', 'BUILD') self.register_uris([ dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks.json']), - json={'networks': []}) + json={'networks': []}), + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers']), + json={'server': meta.obj_to_munch(build_server).toDict()}, + validate=dict( + json={'server': { + u'flavorRef': u'flavor-id', + u'imageRef': u'image-id', + u'max_count': 1, + u'min_count': 1, + u'name': u'server-name'}})), + # TODO(mordred) useless call made by novaclient + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234']), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234']), + status_code=404), ]) - mock_nova.servers.create.return_value = mock.Mock(status="BUILD") - mock_nova.servers.get.side_effect = Exception("exception") self.assertRaises( exc.OpenStackCloudException, self.cloud.create_server, 'server-name', {'id': 'image-id'}, {'id': 'flavor-id'}) self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_create_server_with_server_error(self, mock_nova): + def test_create_server_with_server_error(self): """ Test that a server error before we return or begin waiting for the server instance spawn raises an exception in create_server. @@ -76,55 +77,77 @@ def test_create_server_with_server_error(self, mock_nova): dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks.json']), - json={'networks': []}) + json={'networks': []}), + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers']), + json={'server': meta.obj_to_munch(build_server).toDict()}, + validate=dict( + json={'server': { + u'flavorRef': u'flavor-id', + u'imageRef': u'image-id', + u'max_count': 1, + u'min_count': 1, + u'name': u'server-name'}})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234']), + json={'server': meta.obj_to_munch(build_server).toDict()}), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234']), + json={'server': meta.obj_to_munch(error_server).toDict()}), ]) - mock_nova.servers.create.return_value = build_server - mock_nova.servers.get.return_value = error_server self.assertRaises( exc.OpenStackCloudException, self.cloud.create_server, 'server-name', {'id': 'image-id'}, {'id': 'flavor-id'}) self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_create_server_wait_server_error(self, mock_nova): + def test_create_server_wait_server_error(self): """ Test that a server error while waiting for the server to spawn raises an exception in create_server. """ build_server = fakes.FakeServer('1234', '', 'BUILD') error_server = fakes.FakeServer('1234', '', 'ERROR') - fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', - '1.1.1.1', '2.2.2.2', - '5678') - mock_nova.servers.create.return_value = build_server - mock_nova.servers.get.return_value = build_server - mock_nova.servers.list.side_effect = [[build_server], [error_server]] self.register_uris([ dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks.json']), json={'networks': []}), + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers']), + json={'server': meta.obj_to_munch(build_server).toDict()}, + validate=dict( + json={'server': { + u'flavorRef': u'flavor-id', + u'imageRef': u'image-id', + u'max_count': 1, + u'min_count': 1, + u'name': u'server-name'}})), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json'], - qs_elements=['device_id=1234']), - json={'ports': []}), + 'compute', 'public', append=['servers', '1234']), + json={'server': meta.obj_to_munch(build_server).toDict()}), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [meta.obj_to_munch(build_server).toDict()]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips.json']), - json={'floatingips': [fake_floating_ip]}) + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [meta.obj_to_munch(error_server).toDict()]}), ]) self.assertRaises( exc.OpenStackCloudException, self.cloud.create_server, 'server-name', dict(id='image-id'), dict(id='flavor-id'), wait=True) - # TODO(slaweq): change do_count to True when all nova mocks will be - # replaced with request_mocks also - self.assert_calls(do_count=False) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_create_server_with_timeout(self, mock_nova): + self.assert_calls() + + def test_create_server_with_timeout(self): """ Test that a timeout while waiting for the server to spawn raises an exception in create_server. @@ -134,113 +157,152 @@ def test_create_server_with_timeout(self, mock_nova): dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks.json']), - json={'networks': []}) + json={'networks': []}), + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers']), + json={'server': meta.obj_to_munch(fake_server).toDict()}, + validate=dict( + json={'server': { + u'flavorRef': u'flavor-id', + u'imageRef': u'image-id', + u'max_count': 1, + u'min_count': 1, + u'name': u'server-name'}})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234']), + json={'server': meta.obj_to_munch(fake_server).toDict()}), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [meta.obj_to_munch(fake_server).toDict()]}), ]) - mock_nova.servers.create.return_value = fake_server - mock_nova.servers.get.return_value = fake_server - mock_nova.servers.list.return_value = [fake_server] self.assertRaises( exc.OpenStackCloudTimeout, self.cloud.create_server, 'server-name', dict(id='image-id'), dict(id='flavor-id'), wait=True, timeout=0.01) - self.assert_calls() + # We poll at the end, so we don't know real counts + self.assert_calls(do_count=False) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_create_server_no_wait(self, mock_nova): + def test_create_server_no_wait(self): """ Test that create_server with no wait and no exception in the novaclient create call returns the server instance. """ fake_server = fakes.FakeServer('1234', '', 'BUILD') - fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', - '1.1.1.1', '2.2.2.2', - '5678') - mock_nova.servers.create.return_value = fake_server - mock_nova.servers.get.return_value = fake_server self.register_uris([ dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks.json']), json={'networks': []}), + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers']), + json={'server': meta.obj_to_munch(fake_server).toDict()}, + validate=dict( + json={'server': { + u'flavorRef': u'flavor-id', + u'imageRef': u'image-id', + u'max_count': 1, + u'min_count': 1, + u'name': u'server-name'}})), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json'], - qs_elements=['device_id=1234']), - json={'ports': []}), + 'compute', 'public', append=['servers', '1234']), + json={'server': meta.obj_to_munch(fake_server).toDict()}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips.json']), - json={'floatingips': [fake_floating_ip]}) + 'compute', 'public', append=['servers', '1234']), + json={'server': meta.obj_to_munch(fake_server).toDict()}), ]) self.assertEqual( self.cloud._normalize_server( meta.obj_to_munch(fake_server)), self.cloud.create_server( name='server-name', - image=dict(id='image=id'), + image=dict(id='image-id'), flavor=dict(id='flavor-id'))) - # TODO(slaweq): change do_count to True when all nova mocks will be - # replaced with request_mocks also - self.assert_calls(do_count=False) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_create_server_with_admin_pass_no_wait(self, mock_nova): + self.assert_calls() + + def test_create_server_with_admin_pass_no_wait(self): """ Test that a server with an admin_pass passed returns the password """ fake_server = fakes.FakeServer('1234', '', 'BUILD') fake_create_server = fakes.FakeServer('1234', '', 'BUILD', adminPass='ooBootheiX0edoh') - fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', - '1.1.1.1', '2.2.2.2', - '5678') - mock_nova.servers.create.return_value = fake_create_server - mock_nova.servers.get.return_value = fake_server self.register_uris([ dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks.json']), json={'networks': []}), + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers']), + json={'server': meta.obj_to_munch( + fake_create_server).toDict()}, + validate=dict( + json={'server': { + u'adminPass': 'ooBootheiX0edoh', + u'flavorRef': u'flavor-id', + u'imageRef': u'image-id', + u'max_count': 1, + u'min_count': 1, + u'name': u'server-name'}})), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json'], - qs_elements=['device_id=1234']), - json={'ports': []}), + 'compute', 'public', append=['servers', '1234']), + json={'server': meta.obj_to_munch(fake_server).toDict()}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips.json']), - json={'floatingips': [fake_floating_ip]}) + 'compute', 'public', append=['servers', '1234']), + json={'server': meta.obj_to_munch(fake_server).toDict()}), ]) self.assertEqual( self.cloud._normalize_server( meta.obj_to_munch(fake_create_server)), self.cloud.create_server( - name='server-name', image=dict(id='image=id'), + name='server-name', image=dict(id='image-id'), flavor=dict(id='flavor-id'), admin_pass='ooBootheiX0edoh')) - # TODO(slaweq): change do_count to True when all nova mocks will be - # replaced with request_mocks also - self.assert_calls(do_count=False) + + self.assert_calls() @mock.patch.object(shade.OpenStackCloud, "wait_for_server") - @mock.patch.object(shade.OpenStackCloud, "nova_client") - def test_create_server_with_admin_pass_wait(self, mock_nova, mock_wait): + def test_create_server_with_admin_pass_wait(self, mock_wait): """ Test that a server with an admin_pass passed returns the password """ fake_server = fakes.FakeServer('1234', '', 'BUILD') + fake_server_with_pass = fakes.FakeServer('1234', '', 'BUILD', + adminPass='ooBootheiX0edoh') self.register_uris([ dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks.json']), - json={'networks': []}) + json={'networks': []}), + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers']), + json={'server': meta.obj_to_munch( + fake_server_with_pass).toDict()}, + validate=dict( + json={'server': { + u'flavorRef': u'flavor-id', + u'imageRef': u'image-id', + u'max_count': 1, + u'min_count': 1, + u'adminPass': 'ooBootheiX0edoh', + u'name': u'server-name'}})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234']), + json={'server': meta.obj_to_munch(fake_server).toDict()}), ]) - fake_server_with_pass = fakes.FakeServer('1234', '', 'BUILD', - adminPass='ooBootheiX0edoh') - mock_nova.servers.create.return_value = fake_server - mock_nova.servers.get.return_value = fake_server # The wait returns non-password server mock_wait.return_value = self.cloud._normalize_server( meta.obj_to_munch(fake_server)) @@ -255,9 +317,9 @@ def test_create_server_with_admin_pass_wait(self, mock_nova, mock_wait): # Even with the wait, we should still get back a passworded server self.assertEqual( - server, + server['adminPass'], self.cloud._normalize_server( - meta.obj_to_munch(fake_server_with_pass)) + meta.obj_to_munch(fake_server_with_pass))['adminPass'] ) self.assert_calls() @@ -268,6 +330,7 @@ def test_wait_for_server(self, mock_get_server, mock_get_active_server): Test that waiting for a server returns the server instance when its status changes to "ACTIVE". """ + # TODO(mordred) Rework this to not mock methods building_server = {'id': 'fake_server_id', 'status': 'BUILDING'} active_server = {'id': 'fake_server_id', 'status': 'ACTIVE'} @@ -296,63 +359,96 @@ def test_wait_for_server(self, mock_get_server, mock_get_active_server): self.assertEqual('ACTIVE', server['status']) @mock.patch.object(shade.OpenStackCloud, 'wait_for_server') - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_create_server_wait(self, mock_nova, mock_wait): + def test_create_server_wait(self, mock_wait): """ Test that create_server with a wait actually does the wait. """ - fake_server = {'id': 'fake_server_id', 'status': 'BUILDING'} - mock_nova.servers.create.return_value = fake_server + # TODO(mordred) Make this a full proper response + fake_server = fakes.FakeServer('1234', '', 'BUILD') self.register_uris([ dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks.json']), - json={'networks': []}) + json={'networks': []}), + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers']), + json={'server': meta.obj_to_munch(fake_server).toDict()}, + validate=dict( + json={'server': { + u'flavorRef': u'flavor-id', + u'imageRef': u'image-id', + u'max_count': 1, + u'min_count': 1, + u'name': u'server-name'}})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234']), + json={'server': meta.obj_to_munch(fake_server).toDict()}), ]) self.cloud.create_server( 'server-name', dict(id='image-id'), dict(id='flavor-id'), wait=True), mock_wait.assert_called_once_with( - fake_server, auto_ip=True, ips=None, + self.cloud._normalize_server(meta.obj_to_munch(fake_server)), + auto_ip=True, ips=None, ip_pool=None, reuse=True, timeout=180, nat_destination=None, ) self.assert_calls() @mock.patch.object(shade.OpenStackCloud, 'add_ips_to_server') - @mock.patch.object(shade.OpenStackCloud, 'nova_client') @mock.patch('time.sleep') def test_create_server_no_addresses( - self, mock_sleep, mock_nova, mock_add_ips_to_server): + self, mock_sleep, mock_add_ips_to_server): """ Test that create_server with a wait throws an exception if the server doesn't have addresses. """ build_server = fakes.FakeServer('1234', '', 'BUILD') fake_server = fakes.FakeServer('1234', '', 'ACTIVE') - fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', - '1.1.1.1', '2.2.2.2', - '5678') - mock_nova.servers.create.return_value = build_server - mock_nova.servers.get.return_value = [build_server, None] - mock_nova.servers.list.side_effect = [[build_server], [fake_server]] - mock_nova.servers.delete.return_value = None self.register_uris([ dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks.json']), json={'networks': []}), + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers']), + json={'server': meta.obj_to_munch(build_server).toDict()}, + validate=dict( + json={'server': { + u'flavorRef': u'flavor-id', + u'imageRef': u'image-id', + u'max_count': 1, + u'min_count': 1, + u'name': u'server-name'}})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234']), + json={'server': meta.obj_to_munch(build_server).toDict()}), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [meta.obj_to_munch(build_server).toDict()]}), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [meta.obj_to_munch(fake_server).toDict()]}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'ports.json'], qs_elements=['device_id=1234']), json={'ports': []}), + dict(method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'])), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips.json']), - json={'floatingips': [fake_floating_ip]}) + 'compute', 'public', append=['servers', 'detail']), + json={'servers': []}), ]) mock_add_ips_to_server.return_value = fake_server self.cloud._SERVER_AGE = 0 @@ -361,16 +457,16 @@ def test_create_server_no_addresses( exc.OpenStackCloudException, self.cloud.create_server, 'server-name', {'id': 'image-id'}, {'id': 'flavor-id'}, wait=True) - # TODO(slaweq): change do_count to True when all nova mocks will be - # replaced with request_mocks also - self.assert_calls(do_count=False) - @mock.patch('shade.OpenStackCloud.nova_client') - def test_create_server_network_with_no_nics(self, mock_nova): + self.assert_calls() + + def test_create_server_network_with_no_nics(self): """ Verify that if 'network' is supplied, and 'nics' is not, that we attempt to get the network for the server. """ + build_server = fakes.FakeServer('1234', '', 'BUILD') + active_server = fakes.FakeServer('1234', '', 'ACTIVE') network = { 'id': 'network-id', 'name': 'network-name' @@ -380,6 +476,31 @@ def test_create_server_network_with_no_nics(self, mock_nova): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks.json']), json={'networks': [network]}), + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers']), + json={'server': meta.obj_to_munch(build_server).toDict()}, + validate=dict( + json={'server': { + u'flavorRef': u'flavor-id', + u'imageRef': u'image-id', + u'max_count': 1, + u'min_count': 1, + u'networks': [{u'uuid': u'network-id'}], + u'name': u'server-name'}})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234']), + json={'server': meta.obj_to_munch(build_server).toDict()}), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234']), + json={'server': meta.obj_to_munch(active_server).toDict()}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json'], + qs_elements=['device_id=1234']), + json={'ports': []}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks.json']), @@ -394,11 +515,7 @@ def test_create_server_network_with_no_nics(self, mock_nova): dict(id='image-id'), dict(id='flavor-id'), network='network-name') self.assert_calls() - @mock.patch('shade.OpenStackCloud.nova_client') - @mock.patch('shade.OpenStackCloud.get_network') - def test_create_server_network_with_empty_nics(self, - mock_get_network, - mock_nova): + def test_create_server_network_with_empty_nics(self): """ Verify that if 'network' is supplied, along with an empty 'nics' list, it's treated the same as if 'nics' were not included. @@ -407,7 +524,32 @@ def test_create_server_network_with_empty_nics(self, 'id': 'network-id', 'name': 'network-name' } + build_server = fakes.FakeServer('1234', '', 'BUILD') self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': [network]}), + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers']), + json={'server': meta.obj_to_munch(build_server).toDict()}, + validate=dict( + json={'server': { + u'flavorRef': u'flavor-id', + u'imageRef': u'image-id', + u'max_count': 1, + u'min_count': 1, + u'networks': [{u'uuid': u'network-id'}], + u'name': u'server-name'}})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234']), + json={'server': meta.obj_to_munch(build_server).toDict()}), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234']), + json={'server': meta.obj_to_munch(build_server).toDict()}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks.json']), @@ -422,22 +564,53 @@ def test_create_server_network_with_empty_nics(self, network='network-name', nics=[]) self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'get_server_by_id') - @mock.patch.object(shade.OpenStackCloud, 'get_image') - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_create_server_get_flavor_image( - self, mock_nova, mock_image, mock_get_server_by_id): + def test_create_server_get_flavor_image(self): + self.use_glance() + image_id = str(uuid.uuid4()) + fake_image_dict = fakes.make_fake_image(image_id=image_id) + fake_image_search_return = {'images': [fake_image_dict]} + + build_server = fakes.FakeServer('1234', '', 'BUILD') + active_server = fakes.FakeServer('1234', '', 'BUILD') self.register_uris([ dict(method='GET', - uri='{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'flavors': fakes.FAKE_FLAVOR_LIST})]) + uri='https://image.example.com/v2/images', + json=fake_image_search_return), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['flavors', 'detail'], + qs_elements=['is_public=None']), + json={'flavors': fakes.FAKE_FLAVOR_LIST}), + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers']), + json={'server': meta.obj_to_munch(build_server).toDict()}, + validate=dict( + json={'server': { + u'flavorRef': fakes.FLAVOR_ID, + u'imageRef': image_id, + u'max_count': 1, + u'min_count': 1, + u'networks': [{u'uuid': u'some-network'}], + u'name': u'server-name'}})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234']), + json={'server': meta.obj_to_munch(active_server).toDict()}), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234']), + json={'server': meta.obj_to_munch(active_server).toDict()}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': []}), + ]) self.cloud.create_server( - 'server-name', 'image-id', 'vanilla', - nics=[{'net-id': 'some-network'}]) - mock_image.assert_called_once() + 'server-name', image_id, 'vanilla', + nics=[{'net-id': 'some-network'}], wait=False) self.assert_calls() From 6b61f18ea20f622029939230730048e49e523fe6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 12 Jun 2017 15:34:25 -0500 Subject: [PATCH 1607/3836] Don't fetch extra_specs in functional tests We don't need to get these with every list_flavors and they make the logs harder to read. Change-Id: I642f6e0f05b13b67e95ed2cefdf7230c2cda5e5c --- shade/tests/functional/test_compute.py | 3 ++- shade/tests/functional/test_flavor.py | 4 ++-- shade/tests/functional/test_range_search.py | 24 ++++++++++----------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index 25e609e7f..0d198c95a 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -32,7 +32,8 @@ def setUp(self): self.TIMEOUT_SCALING_FACTOR = 1.5 super(TestCompute, self).setUp() - self.flavor = pick_flavor(self.user_cloud.list_flavors()) + self.flavor = pick_flavor( + self.user_cloud.list_flavors(get_extra=False)) if self.flavor is None: self.assertFalse('no sensible flavor available') self.image = self.pick_image() diff --git a/shade/tests/functional/test_flavor.py b/shade/tests/functional/test_flavor.py index 1e3590c05..d9a0baf4e 100644 --- a/shade/tests/functional/test_flavor.py +++ b/shade/tests/functional/test_flavor.py @@ -37,7 +37,7 @@ def setUp(self): def _cleanup_flavors(self): exception_list = list() - for f in self.operator_cloud.list_flavors(): + for f in self.operator_cloud.list_flavors(get_extra=False): if f['name'].startswith(self.new_item_name): try: self.operator_cloud.delete_flavor(f['id']) @@ -94,7 +94,7 @@ def test_list_flavors(self): self.operator_cloud.create_flavor(**public_kwargs) self.operator_cloud.create_flavor(**private_kwargs) - flavors = self.operator_cloud.list_flavors() + flavors = self.operator_cloud.list_flavors(get_extra=False) # Flavor list will include the standard devstack flavors. We just want # to make sure both of the flavors we just created are present. diff --git a/shade/tests/functional/test_range_search.py b/shade/tests/functional/test_range_search.py index 4b97f3c5e..dfcfbb712 100644 --- a/shade/tests/functional/test_range_search.py +++ b/shade/tests/functional/test_range_search.py @@ -30,13 +30,13 @@ def _filter_m1_flavors(self, results): return new_results def test_range_search_bad_range(self): - flavors = self.user_cloud.list_flavors() + flavors = self.user_cloud.list_flavors(get_extra=False) self.assertRaises( exc.OpenStackCloudException, self.user_cloud.range_search, flavors, {"ram": "<1a0"}) def test_range_search_exact(self): - flavors = self.user_cloud.list_flavors() + flavors = self.user_cloud.list_flavors(get_extra=False) result = self.user_cloud.range_search(flavors, {"ram": "4096"}) self.assertIsInstance(result, list) # should only be 1 m1 flavor with 4096 ram @@ -45,7 +45,7 @@ def test_range_search_exact(self): self.assertEqual("m1.medium", result[0]['name']) def test_range_search_min(self): - flavors = self.user_cloud.list_flavors() + flavors = self.user_cloud.list_flavors(get_extra=False) result = self.user_cloud.range_search(flavors, {"ram": "MIN"}) self.assertIsInstance(result, list) self.assertEqual(1, len(result)) @@ -53,14 +53,14 @@ def test_range_search_min(self): self.assertTrue(result[0]['name'] in ('cirros256', 'm1.tiny')) def test_range_search_max(self): - flavors = self.user_cloud.list_flavors() + flavors = self.user_cloud.list_flavors(get_extra=False) result = self.user_cloud.range_search(flavors, {"ram": "MAX"}) self.assertIsInstance(result, list) self.assertEqual(1, len(result)) self.assertEqual("m1.xlarge", result[0]['name']) def test_range_search_lt(self): - flavors = self.user_cloud.list_flavors() + flavors = self.user_cloud.list_flavors(get_extra=False) result = self.user_cloud.range_search(flavors, {"ram": "<1024"}) self.assertIsInstance(result, list) # should only be 1 m1 flavor with <1024 ram @@ -69,7 +69,7 @@ def test_range_search_lt(self): self.assertEqual("m1.tiny", result[0]['name']) def test_range_search_gt(self): - flavors = self.user_cloud.list_flavors() + flavors = self.user_cloud.list_flavors(get_extra=False) result = self.user_cloud.range_search(flavors, {"ram": ">4096"}) self.assertIsInstance(result, list) # should only be 2 m1 flavors with >4096 ram @@ -80,7 +80,7 @@ def test_range_search_gt(self): self.assertIn("m1.xlarge", flavor_names) def test_range_search_le(self): - flavors = self.user_cloud.list_flavors() + flavors = self.user_cloud.list_flavors(get_extra=False) result = self.user_cloud.range_search(flavors, {"ram": "<=4096"}) self.assertIsInstance(result, list) # should only be 3 m1 flavors with <=4096 ram @@ -92,7 +92,7 @@ def test_range_search_le(self): self.assertIn("m1.medium", flavor_names) def test_range_search_ge(self): - flavors = self.user_cloud.list_flavors() + flavors = self.user_cloud.list_flavors(get_extra=False) result = self.user_cloud.range_search(flavors, {"ram": ">=4096"}) self.assertIsInstance(result, list) # should only be 3 m1 flavors with >=4096 ram @@ -104,7 +104,7 @@ def test_range_search_ge(self): self.assertIn("m1.xlarge", flavor_names) def test_range_search_multi_1(self): - flavors = self.user_cloud.list_flavors() + flavors = self.user_cloud.list_flavors(get_extra=False) result = self.user_cloud.range_search( flavors, {"ram": "MIN", "vcpus": "MIN"}) self.assertIsInstance(result, list) @@ -113,7 +113,7 @@ def test_range_search_multi_1(self): self.assertTrue(result[0]['name'] in ('cirros256', 'm1.tiny')) def test_range_search_multi_2(self): - flavors = self.user_cloud.list_flavors() + flavors = self.user_cloud.list_flavors(get_extra=False) result = self.user_cloud.range_search( flavors, {"ram": "<1024", "vcpus": "MIN"}) self.assertIsInstance(result, list) @@ -123,7 +123,7 @@ def test_range_search_multi_2(self): self.assertIn("m1.tiny", flavor_names) def test_range_search_multi_3(self): - flavors = self.user_cloud.list_flavors() + flavors = self.user_cloud.list_flavors(get_extra=False) result = self.user_cloud.range_search( flavors, {"ram": ">=4096", "vcpus": "<6"}) self.assertIsInstance(result, list) @@ -134,7 +134,7 @@ def test_range_search_multi_3(self): self.assertIn("m1.large", flavor_names) def test_range_search_multi_4(self): - flavors = self.user_cloud.list_flavors() + flavors = self.user_cloud.list_flavors(get_extra=False) result = self.user_cloud.range_search( flavors, {"ram": ">=4096", "vcpus": "MAX"}) self.assertIsInstance(result, list) From 36b659eed403aea3ecf8666a68524b8e0f8ef494 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 12 Jun 2017 09:57:23 -0500 Subject: [PATCH 1608/3836] RESTify create_server In order to create passing tags to the nics parameter of create_server, we need to support microversions. In order to support microversions, we need to be using REST. Yolanda needs to pass tags to the nics parameter, so let's go ahead and transition create_server since we want to anyway. Tests are modified to remove an extra GET call that novaclient makes that we don't actually need. A follow up to this will add microversion discovery, and then will add support for 'tags' being in the nics list. Change-Id: I04c3b08d80cf496dabec8ce7000ca5fb679fe3b9 --- shade/_tasks.py | 5 -- shade/openstackcloud.py | 102 ++++++++++++++++++++----- shade/tests/unit/test_create_server.py | 59 +------------- 3 files changed, 82 insertions(+), 84 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index c62bf89d5..fcceeb10d 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -77,11 +77,6 @@ def main(self, client): return client.nova_client.servers.get(**self.args) -class ServerCreate(task_manager.Task): - def main(self, client): - return client.nova_client.servers.create(**self.args) - - class ServerDelete(task_manager.Task): def main(self, client): return client.nova_client.servers.delete(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index e91ea8b74..c73977182 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5282,6 +5282,8 @@ def _get_boot_from_volume_kwargs( :param image: Image dict, name or id to boot with. """ + # TODO(mordred) We're only testing this in functional tests. We need + # to add unit tests for this too. if boot_volume or boot_from_volume or volumes: kwargs.setdefault('block_device_mapping_v2', []) else: @@ -5305,7 +5307,7 @@ def _get_boot_from_volume_kwargs( 'source_type': 'volume', } kwargs['block_device_mapping_v2'].append(block_mapping) - kwargs['image'] = None + kwargs['imageRef'] = '' elif boot_from_volume: if isinstance(image, dict): @@ -5327,7 +5329,18 @@ def _get_boot_from_volume_kwargs( 'source_type': 'image', 'volume_size': volume_size, } - kwargs['image'] = None + kwargs['imageRef'] = '' + kwargs['block_device_mapping_v2'].append(block_mapping) + if volumes and kwargs['imageRef']: + # If we're attaching volumes on boot but booting from an image, + # we need to specify that in the BDM. + block_mapping = { + u'boot_index': 0, + u'delete_on_termination': True, + u'destination_type': u'local', + u'source_type': u'image', + u'uuid': kwargs['imageRef'], + } kwargs['block_device_mapping_v2'].append(block_mapping) for volume in volumes: volume_obj = self.get_volume(volume) @@ -5444,17 +5457,25 @@ def create_server( :returns: A ``munch.Munch`` representing the created server. :raises: OpenStackCloudException on operation error. """ - # nova cli calls this boot_volume. Let's be the same - - if volumes is None: - volumes = [] - - if root_volume and not boot_volume: - boot_volume = root_volume + # TODO(mordred) Add support for description starting in 2.19 - security_groups = kwargs.get('security_groups') + security_groups = kwargs.get('security_groups', []) if security_groups and not isinstance(kwargs['security_groups'], list): - kwargs['security_groups'] = [security_groups] + security_groups = [security_groups] + if security_groups: + kwargs['security_groups'] = [] + for group in security_groups: + kwargs['security_groups'].append(dict(name=group)) + for (desired, given) in ( + ('OS-DCF:diskConfig', 'disk_config'), + ('metadata', 'meta'), + ('user_data', 'userdata'), + ('adminPass', 'admin_pass')): + value = kwargs.pop(given, None) + if value: + kwargs[desired] = value + kwargs.setdefault('max_count', kwargs.get('max_count', 1)) + kwargs.setdefault('min_count', kwargs.get('min_count', 1)) if 'nics' in kwargs and not isinstance(kwargs['nics'], list): if isinstance(kwargs['nics'], dict): @@ -5464,6 +5485,7 @@ def create_server( raise OpenStackCloudException( 'nics parameter to create_server takes a list of dicts.' ' Got: {nics}'.format(nics=kwargs['nics'])) + if network and ('nics' not in kwargs or not kwargs['nics']): nics = [] if not isinstance(network, list): @@ -5487,15 +5509,50 @@ def create_server( if default_network: kwargs['nics'] = [{'net-id': default_network['id']}] + networks = [] + for nic in kwargs.pop('nics', []): + net = {} + if 'net-id' in nic: + # TODO(mordred) Make sure this is in uuid format + net['uuid'] = nic.pop('net-id') + # If there's a net-id, ignore net-name + nic.pop('net-name', None) + elif 'net-name' in nic: + nic_net = self.get_network(nic['net-name']) + if not nic_net: + raise OpenStackCloudException( + "Requested network {net} could not be found.".format( + net=nic['net-name'])) + net['uuid'] = nic_net['id'] + # TODO(mordred) Add support for tag if server supports microversion + # 2.32-2.36 or >= 2.42 + for key in ('port', 'fixed_ip'): + if key in nic: + net[key] = nic.pop(key) + if nic: + raise OpenStackCloudException( + "Additional unsupported keys given for server network" + " creation: {keys}".format(keys=nic.keys())) + networks.append(net) + if networks: + kwargs['networks'] = networks + if image: if isinstance(image, dict): - kwargs['image'] = image['id'] + kwargs['imageRef'] = image['id'] else: - kwargs['image'] = self.get_image(image) + kwargs['imageRef'] = self.get_image(image).id if flavor and isinstance(flavor, dict): - kwargs['flavor'] = flavor['id'] + kwargs['flavorRef'] = flavor['id'] else: - kwargs['flavor'] = self.get_flavor(flavor, get_extra=False) + kwargs['flavorRef'] = self.get_flavor(flavor, get_extra=False).id + + if volumes is None: + volumes = [] + + # nova cli calls this boot_volume. Let's be the same + if root_volume and not boot_volume: + boot_volume = root_volume kwargs = self._get_boot_from_volume_kwargs( image=image, boot_from_volume=boot_from_volume, @@ -5503,9 +5560,15 @@ def create_server( terminate_volume=terminate_volume, volumes=volumes, kwargs=kwargs) + kwargs['name'] = name + endpoint = '/servers' + # TODO(mordred) We're only testing this in functional tests. We need + # to add unit tests for this too. + if 'block_device_mapping_v2' in kwargs: + endpoint = '/os-volumes_boot' with _utils.shade_exceptions("Error in creating instance"): - server = self.manager.submit_task(_tasks.ServerCreate( - name=name, **kwargs)) + server = self._compute_client.post( + endpoint, json={'server': kwargs}) admin_pass = server.get('adminPass') or kwargs.get('admin_pass') if not wait: # This is a direct get task call to skip the list_servers @@ -5522,11 +5585,8 @@ def create_server( "Error in creating the server.") if wait: - # TODO(mordred) The normalize call here is just for easing - # novaclient transition (otherwise the HUMAN id stuff leaks into - # mocks. It can be removed when we don't use novaclient server = self.wait_for_server( - self._normalize_server(server), + server, auto_ip=auto_ip, ips=ips, ip_pool=ip_pool, reuse=reuse_ips, timeout=timeout, nat_destination=nat_destination, diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index f7c22426a..9dd813dde 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -51,11 +51,6 @@ def test_create_server_with_get_exception(self): u'max_count': 1, u'min_count': 1, u'name': u'server-name'}})), - # TODO(mordred) useless call made by novaclient - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - status_code=404), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), @@ -89,10 +84,6 @@ def test_create_server_with_server_error(self): u'max_count': 1, u'min_count': 1, u'name': u'server-name'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': meta.obj_to_munch(build_server).toDict()}), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), @@ -126,10 +117,6 @@ def test_create_server_wait_server_error(self): u'max_count': 1, u'min_count': 1, u'name': u'server-name'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': meta.obj_to_munch(build_server).toDict()}), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -169,10 +156,6 @@ def test_create_server_with_timeout(self): u'max_count': 1, u'min_count': 1, u'name': u'server-name'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': meta.obj_to_munch(fake_server).toDict()}), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -213,10 +196,6 @@ def test_create_server_no_wait(self): uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), json={'server': meta.obj_to_munch(fake_server).toDict()}), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': meta.obj_to_munch(fake_server).toDict()}), ]) self.assertEqual( self.cloud._normalize_server( @@ -257,10 +236,6 @@ def test_create_server_with_admin_pass_no_wait(self): uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), json={'server': meta.obj_to_munch(fake_server).toDict()}), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': meta.obj_to_munch(fake_server).toDict()}), ]) self.assertEqual( self.cloud._normalize_server( @@ -297,10 +272,6 @@ def test_create_server_with_admin_pass_wait(self, mock_wait): u'min_count': 1, u'adminPass': 'ooBootheiX0edoh', u'name': u'server-name'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': meta.obj_to_munch(fake_server).toDict()}), ]) # The wait returns non-password server @@ -382,17 +353,13 @@ def test_create_server_wait(self, mock_wait): u'max_count': 1, u'min_count': 1, u'name': u'server-name'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': meta.obj_to_munch(fake_server).toDict()}), ]) self.cloud.create_server( 'server-name', dict(id='image-id'), dict(id='flavor-id'), wait=True), mock_wait.assert_called_once_with( - self.cloud._normalize_server(meta.obj_to_munch(fake_server)), + meta.obj_to_munch(fake_server), auto_ip=True, ips=None, ip_pool=None, reuse=True, timeout=180, nat_destination=None, @@ -425,10 +392,6 @@ def test_create_server_no_addresses( u'max_count': 1, u'min_count': 1, u'name': u'server-name'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': meta.obj_to_munch(build_server).toDict()}), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -488,10 +451,6 @@ def test_create_server_network_with_no_nics(self): u'min_count': 1, u'networks': [{u'uuid': u'network-id'}], u'name': u'server-name'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': meta.obj_to_munch(build_server).toDict()}), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), @@ -546,10 +505,6 @@ def test_create_server_network_with_empty_nics(self): uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), json={'server': meta.obj_to_munch(build_server).toDict()}), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': meta.obj_to_munch(build_server).toDict()}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks.json']), @@ -598,10 +553,6 @@ def test_create_server_get_flavor_image(self): uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), json={'server': meta.obj_to_munch(active_server).toDict()}), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': meta.obj_to_munch(active_server).toDict()}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks.json']), @@ -658,10 +609,6 @@ def test_create_boot_attach_volume(self): uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), json={'server': meta.obj_to_munch(active_server).toDict()}), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': meta.obj_to_munch(active_server).toDict()}), ]) self.cloud.create_server( @@ -705,10 +652,6 @@ def test_create_boot_from_volume_image_terminate(self): uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), json={'server': meta.obj_to_munch(active_server).toDict()}), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': meta.obj_to_munch(active_server).toDict()}), ]) self.cloud.create_server( From c085c4c5f911133a9add0b50e63c1f6c85f3121f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 13 Jun 2017 06:56:55 -0500 Subject: [PATCH 1609/3836] Convert get_server_by_id Already did all the tests during create_server. Change-Id: I10ade7ff0bf682691e4842e639726195253e1300 --- shade/openstackcloud.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index c73977182..335806a27 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2731,7 +2731,7 @@ def _expand_server(self, server, detailed, bare): def get_server_by_id(self, id): return meta.add_server_interfaces(self, self._normalize_server( - self.manager.submit_task(_tasks.ServerGet(server=id)))) + self._compute_client.get('/servers/{id}'.format(id=id)))) def get_server_group(self, name_or_id=None, filters=None): """Get a server group by name or ID. @@ -5571,9 +5571,9 @@ def create_server( endpoint, json={'server': kwargs}) admin_pass = server.get('adminPass') or kwargs.get('admin_pass') if not wait: - # This is a direct get task call to skip the list_servers + # This is a direct get call to skip the list_servers # cache which has absolutely no chance of containing the - # new server + # new server. # Only do this if we're not going to wait for the server # to complete booting, because the only reason we do it # is to get a server record that is the return value from From 458b6e9c96c7a3e961f11be938718008cf644086 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 13 Jun 2017 07:53:15 -0500 Subject: [PATCH 1610/3836] Convert delete server mocks to requests_mock Change-Id: I1615aa825dffbddd33e5effc014415945a694e76 --- shade/tests/unit/test_delete_server.py | 188 +++++++++++++------------ 1 file changed, 96 insertions(+), 92 deletions(-) diff --git a/shade/tests/unit/test_delete_server.py b/shade/tests/unit/test_delete_server.py index 785d19d3e..a74432eff 100644 --- a/shade/tests/unit/test_delete_server.py +++ b/shade/tests/unit/test_delete_server.py @@ -17,123 +17,127 @@ Tests for the `delete_server` command. """ -import mock -from novaclient import exceptions as nova_exc - from shade import exc as shade_exc +from shade import meta from shade.tests import fakes from shade.tests.unit import base -class TestDeleteServer(base.TestCase): - novaclient_exceptions = (nova_exc.BadRequest, - nova_exc.Unauthorized, - nova_exc.Forbidden, - nova_exc.MethodNotAllowed, - nova_exc.Conflict, - nova_exc.OverLimit, - nova_exc.RateLimit, - nova_exc.HTTPNotImplemented) +class TestDeleteServer(base.RequestsMockTestCase): - @mock.patch('shade.OpenStackCloud.nova_client') - def test_delete_server(self, nova_mock): + def test_delete_server(self): """ - Test that novaclient server delete is called when wait=False + Test that server delete is called when wait=False """ server = fakes.FakeServer('1234', 'daffy', 'ACTIVE') - nova_mock.servers.list.return_value = [server] + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [meta.obj_to_munch(server).toDict()]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'])), + ]) self.assertTrue(self.cloud.delete_server('daffy', wait=False)) - nova_mock.servers.delete.assert_called_with(server=server.id) - @mock.patch('shade.OpenStackCloud.nova_client') - def test_delete_server_already_gone(self, nova_mock): + self.assert_calls() + + def test_delete_server_already_gone(self): """ Test that we return immediately when server is already gone """ - nova_mock.servers.list.return_value = [] + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': []}), + ]) self.assertFalse(self.cloud.delete_server('tweety', wait=False)) - self.assertFalse(nova_mock.servers.delete.called) - @mock.patch('shade.OpenStackCloud.nova_client') - def test_delete_server_already_gone_wait(self, nova_mock): + self.assert_calls() + + def test_delete_server_already_gone_wait(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': []}), + ]) self.assertFalse(self.cloud.delete_server('speedy', wait=True)) - self.assertFalse(nova_mock.servers.delete.called) + self.assert_calls() - @mock.patch('shade.OpenStackCloud.nova_client') - def test_delete_server_wait_for_notfound(self, nova_mock): + def test_delete_server_wait_for_deleted(self): """ - Test that delete_server waits for NotFound from novaclient + Test that delete_server waits for the server to be gone """ server = fakes.FakeServer('9999', 'wily', 'ACTIVE') - nova_mock.servers.list.return_value = [server] - - def _delete_wily(*args, **kwargs): - self.assertIn('server', kwargs) - self.assertEqual('9999', kwargs['server']) - nova_mock.servers.list.return_value = [] - - def _raise_notfound(*args, **kwargs): - self.assertIn('server', kwargs) - self.assertEqual('9999', kwargs['server']) - raise nova_exc.NotFound(code='404') - nova_mock.servers.get.side_effect = _raise_notfound - - nova_mock.servers.delete.side_effect = _delete_wily + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [meta.obj_to_munch(server).toDict()]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '9999'])), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [meta.obj_to_munch(server).toDict()]}), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': []}), + ]) self.assertTrue(self.cloud.delete_server('wily', wait=True)) - nova_mock.servers.delete.assert_called_with(server=server.id) - @mock.patch('shade.OpenStackCloud.nova_client') - def test_delete_server_fails(self, nova_mock): - """ - Test that delete_server wraps novaclient exceptions - """ - nova_mock.servers.list.return_value = [fakes.FakeServer('1212', - 'speedy', - 'ACTIVE')] - for fail in self.novaclient_exceptions: - - def _raise_fail(server): - raise fail(code=fail.http_status) - - nova_mock.servers.delete.side_effect = _raise_fail - exc = self.assertRaises(shade_exc.OpenStackCloudException, - self.cloud.delete_server, 'speedy', - wait=False) - # Note that message is deprecated from Exception, but not in - # the novaclient exceptions. - self.assertIn(fail.message, str(exc)) - - @mock.patch('shade.OpenStackCloud.nova_client') - def test_delete_server_get_fails(self, nova_mock): + self.assert_calls() + + def test_delete_server_fails(self): """ - Test that delete_server wraps novaclient exceptions on wait fails + Test that delete_server raises non-404 exceptions """ - nova_mock.servers.list.return_value = [fakes.FakeServer('2000', - 'yosemite', - 'ACTIVE')] - for fail in self.novaclient_exceptions: - - def _raise_fail(): - raise fail(code=fail.http_status) - - nova_mock.servers.list.side_effect = _raise_fail - exc = self.assertRaises(shade_exc.OpenStackCloudException, - self.cloud.delete_server, 'yosemite', - wait=True) - # Note that message is deprecated from Exception, but not in - # the novaclient exceptions. - self.assertIn(fail.message, str(exc)) - - @mock.patch('shade.OpenStackCloud.get_volume') - @mock.patch('shade.OpenStackCloud.nova_client') - def test_delete_server_no_cinder(self, nova_mock, cinder_mock): + server = fakes.FakeServer('1212', 'speedy', 'ACTIVE') + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [meta.obj_to_munch(server).toDict()]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1212']), + status_code=400), + ]) + + self.assertRaises( + shade_exc.OpenStackCloudException, + self.cloud.delete_server, 'speedy', + wait=False) + + self.assert_calls() + + def test_delete_server_no_cinder(self): """ - Test that novaclient server delete is called when wait=False + Test that deleting server works when cinder is not available """ + orig_has_service = self.cloud.has_service + + def fake_has_service(service_type): + if service_type == 'volume': + return False + return orig_has_service(service_type) + self.cloud.has_service = fake_has_service + server = fakes.FakeServer('1234', 'porky', 'ACTIVE') - nova_mock.servers.list.return_value = [server] - with mock.patch('shade.OpenStackCloud.has_service', - return_value=False): - self.assertTrue(self.cloud.delete_server('porky', wait=False)) - nova_mock.servers.delete.assert_called_with(server=server.id) - self.assertFalse(cinder_mock.called) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [meta.obj_to_munch(server).toDict()]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'])), + ]) + self.assertTrue(self.cloud.delete_server('porky', wait=False)) + + self.assert_calls() From a0abee2c5cd48501ea73a5e661a9acd15421ded3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 13 Jun 2017 07:53:27 -0500 Subject: [PATCH 1611/3836] Convert delete server calls to REST Also, remove the GetServer task which was missed earlier. Change-Id: Ic7e5bf011193efc842f68de6b038942af6f23b7d --- shade/_tasks.py | 10 ---------- shade/openstackcloud.py | 14 +++++++------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index fcceeb10d..a75a53117 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -72,16 +72,6 @@ def main(self, client): return client.nova_client.servers.list(**self.args) -class ServerGet(task_manager.Task): - def main(self, client): - return client.nova_client.servers.get(**self.args) - - -class ServerDelete(task_manager.Task): - def main(self, client): - return client.nova_client.servers.delete(**self.args) - - class ServerUpdate(task_manager.Task): def main(self, client): return client.nova_client.servers.update(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 335806a27..13d110faf 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5789,6 +5789,8 @@ def _delete_server( return False if delete_ips: + # TODO(mordred) Does the server have floating ips in its + # addresses dict? If not, skip this. ips = self.search_floating_ips(filters={ 'device_id': server['id']}) for ip in ips: @@ -5803,15 +5805,13 @@ def _delete_server( id=server['id'])) try: - self.manager.submit_task( - _tasks.ServerDelete(server=server['id'])) - except nova_exceptions.NotFound: + self._compute_client.delete( + '/servers/{id}'.format(id=server['id']), + error_message="Error in deleting server") + except OpenStackCloudURINotFound: return False - except OpenStackCloudException: + except Exception: raise - except Exception as e: - raise OpenStackCloudException( - "Error in deleting server: {0}".format(e)) if not wait: return True From 0d7fc5940a566cc21965ce68efaa0ef99edc593a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 13 Jun 2017 14:16:35 -0500 Subject: [PATCH 1612/3836] Update tests for server calls that aren't list Change-Id: I6abe198fc690fc04b35c0a90c3c3ee7a08cfdd08 --- shade/tests/unit/test_rebuild_server.py | 268 ++++++++++++++++-------- shade/tests/unit/test_update_server.py | 70 +++++-- 2 files changed, 225 insertions(+), 113 deletions(-) diff --git a/shade/tests/unit/test_rebuild_server.py b/shade/tests/unit/test_rebuild_server.py index 9000c4815..d7cc14d12 100644 --- a/shade/tests/unit/test_rebuild_server.py +++ b/shade/tests/unit/test_rebuild_server.py @@ -19,168 +19,250 @@ Tests for the `rebuild_server` command. """ -import mock +import uuid from shade import exc -from shade import meta -from shade import OpenStackCloud from shade.tests import fakes from shade.tests.unit import base class TestRebuildServer(base.RequestsMockTestCase): - @mock.patch.object(OpenStackCloud, 'nova_client') - def test_rebuild_server_rebuild_exception(self, mock_nova): + def setUp(self): + super(TestRebuildServer, self).setUp() + self.server_id = str(uuid.uuid4()) + self.server_name = self.getUniqueString('name') + self.fake_server = fakes.make_fake_server( + self.server_id, self.server_name) + self.rebuild_server = fakes.make_fake_server( + self.server_id, self.server_name, 'REBUILD') + self.error_server = fakes.make_fake_server( + self.server_id, self.server_name, 'ERROR') + + def test_rebuild_server_rebuild_exception(self): """ Test that an exception in the novaclient rebuild raises an exception in rebuild_server. """ - mock_nova.servers.rebuild.side_effect = Exception("exception") + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', self.server_id, 'action']), + status_code=400, + validate=dict( + json={ + 'rebuild': { + 'imageRef': 'a', + 'adminPass': 'b'}})), + ]) + self.assertRaises( - exc.OpenStackCloudException, self.cloud.rebuild_server, "a", "b") + exc.OpenStackCloudException, + self.cloud.rebuild_server, + self.fake_server['id'], "a", "b") + + self.assert_calls() - @mock.patch.object(OpenStackCloud, 'nova_client') - def test_rebuild_server_server_error(self, mock_nova): + def test_rebuild_server_server_error(self): """ Test that a server error while waiting for the server to rebuild raises an exception in rebuild_server. """ - rebuild_server = fakes.FakeServer('1234', '', 'REBUILD') - error_server = fakes.FakeServer('1234', '', 'ERROR') - fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', - '1.1.1.1', '2.2.2.2', - '5678') - mock_nova.servers.rebuild.return_value = rebuild_server - mock_nova.servers.list.return_value = [error_server] self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', self.server_id, 'action']), + json={'server': self.rebuild_server}, + validate=dict( + json={ + 'rebuild': { + 'imageRef': 'a'}})), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json'], - qs_elements=['device_id=1234']), - json={'ports': []}), + 'compute', 'public', append=['servers', self.server_id]), + json={'server': self.rebuild_server}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips.json']), - json={'floatingips': [fake_floating_ip]}) + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [self.error_server]}), ]) self.assertRaises( exc.OpenStackCloudException, - self.cloud.rebuild_server, "1234", "b", wait=True) - # TODO(slaweq): change do_count to True when all nova mocks will be - # replaced with request_mocks also - self.assert_calls(do_count=False) + self.cloud.rebuild_server, self.fake_server['id'], "a", wait=True) - @mock.patch.object(OpenStackCloud, 'nova_client') - def test_rebuild_server_timeout(self, mock_nova): + self.assert_calls() + + def test_rebuild_server_timeout(self): """ Test that a timeout while waiting for the server to rebuild raises an exception in rebuild_server. """ - rebuild_server = fakes.FakeServer('1234', '', 'REBUILD') - mock_nova.servers.rebuild.return_value = rebuild_server - mock_nova.servers.list.return_value = [rebuild_server] + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', self.server_id, 'action']), + json={'server': self.rebuild_server}, + validate=dict( + json={ + 'rebuild': { + 'imageRef': 'a'}})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', self.server_id]), + json={'server': self.rebuild_server}), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [self.rebuild_server]}), + ]) self.assertRaises( exc.OpenStackCloudTimeout, - self.cloud.rebuild_server, "a", "b", wait=True, timeout=0.001) + self.cloud.rebuild_server, + self.fake_server['id'], "a", wait=True, timeout=0.001) - @mock.patch.object(OpenStackCloud, 'nova_client') - def test_rebuild_server_no_wait(self, mock_nova): + self.assert_calls(do_count=False) + + def test_rebuild_server_no_wait(self): """ Test that rebuild_server with no wait and no exception in the novaclient rebuild call returns the server instance. """ - rebuild_server = fakes.FakeServer('1234', '', 'REBUILD') - mock_nova.servers.rebuild.return_value = rebuild_server - self.assertEqual(meta.obj_to_munch(rebuild_server), - self.cloud.rebuild_server("a", "b")) + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', self.server_id, 'action']), + json={'server': self.rebuild_server}, + validate=dict( + json={ + 'rebuild': { + 'imageRef': 'a'}})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', self.server_id]), + json={'server': self.rebuild_server}), + ]) + self.assertEqual( + self.rebuild_server['status'], + self.cloud.rebuild_server(self.fake_server['id'], "a")['status']) + + self.assert_calls() - @mock.patch.object(OpenStackCloud, 'nova_client') - def test_rebuild_server_with_admin_pass_no_wait(self, mock_nova): + def test_rebuild_server_with_admin_pass_no_wait(self): """ Test that a server with an admin_pass passed returns the password """ - rebuild_server = fakes.FakeServer('1234', '', 'REBUILD', - adminPass='ooBootheiX0edoh') - mock_nova.servers.rebuild.return_value = rebuild_server + password = self.getUniqueString('password') + rebuild_server = self.rebuild_server.copy() + rebuild_server['adminPass'] = password + + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', self.server_id, 'action']), + json={'server': rebuild_server}, + validate=dict( + json={ + 'rebuild': { + 'imageRef': 'a', + 'adminPass': password}})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', self.server_id]), + json={'server': self.rebuild_server}), + ]) self.assertEqual( - meta.obj_to_munch(rebuild_server), + password, self.cloud.rebuild_server( - 'a', 'b', admin_pass='ooBootheiX0edoh')) + self.fake_server['id'], 'a', + admin_pass=password)['adminPass']) + + self.assert_calls() - @mock.patch.object(OpenStackCloud, 'nova_client') - def test_rebuild_server_with_admin_pass_wait(self, mock_nova): + def test_rebuild_server_with_admin_pass_wait(self): """ Test that a server with an admin_pass passed returns the password """ - rebuild_server = fakes.FakeServer('1234', '', 'REBUILD', - adminPass='ooBootheiX0edoh') - active_server = fakes.FakeServer('1234', '', 'ACTIVE') - ret_active_server = fakes.FakeServer('1234', '', 'ACTIVE', - adminPass='ooBootheiX0edoh') - fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', - '1.1.1.1', '2.2.2.2', - '5678') - mock_nova.servers.rebuild.return_value = rebuild_server - mock_nova.servers.list.return_value = [active_server] + password = self.getUniqueString('password') + rebuild_server = self.rebuild_server.copy() + rebuild_server['adminPass'] = password + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', self.server_id, 'action']), + json={'server': rebuild_server}, + validate=dict( + json={ + 'rebuild': { + 'imageRef': 'a', + 'adminPass': password}})), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json'], - qs_elements=['device_id=1234']), - json={'ports': []}), + 'compute', 'public', append=['servers', self.server_id]), + json={'server': self.rebuild_server}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), - json={'networks': []}), + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [self.rebuild_server]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips.json']), - json={'floatingips': [fake_floating_ip]}) + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [self.fake_server]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': []}), ]) - self.cloud.name = 'cloud-name' + self.assertEqual( - self.cloud._normalize_server( - meta.obj_to_munch(ret_active_server)), + password, self.cloud.rebuild_server( - "1234", "b", wait=True, admin_pass='ooBootheiX0edoh')) - # TODO(slaweq): change do_count to True when all nova mocks will be - # replaced with request_mocks also - self.assert_calls(do_count=False) + self.fake_server['id'], 'a', + admin_pass=password, wait=True)['adminPass']) - @mock.patch.object(OpenStackCloud, 'nova_client') - def test_rebuild_server_wait(self, mock_nova): + self.assert_calls() + + def test_rebuild_server_wait(self): """ Test that rebuild_server with a wait returns the server instance when its status changes to "ACTIVE". """ - rebuild_server = fakes.FakeServer('1234', '', 'REBUILD') - active_server = fakes.FakeServer('1234', '', 'ACTIVE') - fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', - '1.1.1.1', '2.2.2.2', - '5678') - mock_nova.servers.rebuild.return_value = rebuild_server - mock_nova.servers.list.return_value = [active_server] self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', self.server_id, 'action']), + json={'server': self.rebuild_server}, + validate=dict( + json={ + 'rebuild': { + 'imageRef': 'a'}})), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json'], - qs_elements=['device_id=1234']), - json={'ports': []}), + 'compute', 'public', append=['servers', self.server_id]), + json={'server': self.rebuild_server}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), - json={'networks': []}), + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [self.rebuild_server]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips.json']), - json={'floatingips': [fake_floating_ip]}) + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [self.fake_server]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': []}), ]) - self.cloud.name = 'cloud-name' self.assertEqual( - self.cloud._normalize_server( - meta.obj_to_munch(active_server)), - self.cloud.rebuild_server("1234", "b", wait=True)) - # TODO(slaweq): change do_count to True when all nova mocks will be - # replaced with request_mocks also - self.assert_calls(do_count=False) + 'ACTIVE', + self.cloud.rebuild_server( + self.fake_server['id'], 'a', wait=True)['status']) + + self.assert_calls() diff --git a/shade/tests/unit/test_update_server.py b/shade/tests/unit/test_update_server.py index 6c503d33b..db30a7ec4 100644 --- a/shade/tests/unit/test_update_server.py +++ b/shade/tests/unit/test_update_server.py @@ -17,42 +17,72 @@ Tests for the `update_server` command. """ -import mock +import uuid -from shade import OpenStackCloud from shade.exc import OpenStackCloudException from shade.tests import fakes from shade.tests.unit import base -class TestUpdateServer(base.TestCase): +class TestUpdateServer(base.RequestsMockTestCase): - @mock.patch.object(OpenStackCloud, 'nova_client') - def test_update_server_with_update_exception(self, mock_nova): + def setUp(self): + super(TestUpdateServer, self).setUp() + self.server_id = str(uuid.uuid4()) + self.server_name = self.getUniqueString('name') + self.updated_server_name = self.getUniqueString('name2') + self.fake_server = fakes.make_fake_server( + self.server_id, self.server_name) + + def test_update_server_with_update_exception(self): """ Test that an exception in the novaclient update raises an exception in update_server. """ - mock_nova.servers.update.side_effect = Exception("exception") + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [self.fake_server]}), + dict(method='PUT', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', self.server_id]), + status_code=400, + validate=dict( + json={'server': {'name': self.updated_server_name}})), + ]) self.assertRaises( OpenStackCloudException, self.cloud.update_server, - 'server-name') + self.server_name, name=self.updated_server_name) + + self.assert_calls() - @mock.patch.object(OpenStackCloud, 'nova_client') - def test_update_server_name(self, mock_nova): + def test_update_server_name(self): """ Test that update_server updates the name without raising any exception """ - fake_server = fakes.FakeServer('1234', 'server-name', 'ACTIVE') - fake_update_server = fakes.FakeServer('1234', 'server-name2', - 'ACTIVE') - fake_floating_ip = fakes.FakeFloatingIP('1234', 'ippool', - '1.1.1.1', '2.2.2.2', - '5678') - mock_nova.servers.list.return_value = [fake_server] - mock_nova.servers.update.return_value = fake_update_server - mock_nova.floating_ips.list.return_value = [fake_floating_ip] + fake_update_server = fakes.make_fake_server( + self.server_id, self.updated_server_name) + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [self.fake_server]}), + dict(method='PUT', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', self.server_id]), + json={'server': fake_update_server}, + validate=dict( + json={'server': {'name': self.updated_server_name}})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', self.server_id]), + json={'server': fake_update_server}), + ]) self.assertEqual( - 'server-name2', + self.updated_server_name, self.cloud.update_server( - 'server-name', name='server-name2')['name']) + self.server_name, name=self.updated_server_name)['name']) + + self.assert_calls() From 3bb46e4f7fb01484fc1d855d58b3b02b644ea96e Mon Sep 17 00:00:00 2001 From: rajat29 Date: Wed, 14 Jun 2017 11:45:44 +0530 Subject: [PATCH 1613/3836] Replace six.iteritems() with .items() 1.As mentioned in [1], we should avoid usingg six.iteritems to achieve iterators. We can use dict.items instead, as it will return iterators in PY3 as well. And dict.items/keys will more readable. 2.In py2, the performance about list should be negligible, see the link [2]. [1] https://wiki.openstack.org/wiki/Python3 [2] http://lists.openstack.org/pipermail/openstack-dev/2015-June/066391.html Change-Id: Iffdaadabccd6314643f668147f486103378a55eb --- shade/_heat/template_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shade/_heat/template_utils.py b/shade/_heat/template_utils.py index 1b2aeb766..722e09a1d 100644 --- a/shade/_heat/template_utils.py +++ b/shade/_heat/template_utils.py @@ -115,7 +115,7 @@ def get_file_contents(from_data, files, base_url=None, is_object, object_request) if isinstance(from_data, dict): - for key, value in six.iteritems(from_data): + for key, value in from_data.items(): if ignore_if and ignore_if(key, value): continue @@ -307,7 +307,7 @@ def ignore_if(key, value): get_file_contents(rr, files, base_url, ignore_if, is_object=is_object, object_request=object_request) - for res_name, res_dict in six.iteritems(rr.get('resources', {})): + for res_name, res_dict in rr.get('resources', {}).items(): res_base_url = res_dict.get('base_url', base_url) get_file_contents( res_dict, files, res_base_url, ignore_if, From 19cddb559180ea22e26b7171f26ea692c32ee967 Mon Sep 17 00:00:00 2001 From: deepakmourya Date: Wed, 14 Jun 2017 11:54:08 +0530 Subject: [PATCH 1614/3836] Remove py34 and pypy in tox No gate check for py34 and pypy Change-Id: Idcc0dd485d94ab66e52e086c6d5cda268c5aea07 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 9bb35bbe1..e4aa17752 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 1.6 -envlist = py34,py35,py27,pypy,pep8 +envlist = py35,py27,pep8 skipsdist = True [testenv] From bdd048e76c36e0a3ae787c5dedfa06868abc6229 Mon Sep 17 00:00:00 2001 From: bhagyashris Date: Tue, 13 Jun 2017 18:15:25 +0530 Subject: [PATCH 1615/3836] Remove get_service method from compute If a get_service method is called with a valid service id, then it raises ResourceNotFound exception as this API is not implemented in nova. Removed this method and related test cases to cleanup the code. Closes-Bug: #1697687 Change-Id: I4086f4731fc653d721fe879b718b117327460933 --- doc/source/users/proxies/compute.rst | 1 - openstack/compute/v2/_proxy.py | 14 -------------- openstack/compute/v2/service.py | 1 - openstack/tests/unit/compute/v2/test_proxy.py | 4 ---- openstack/tests/unit/compute/v2/test_service.py | 2 +- 5 files changed, 1 insertion(+), 21 deletions(-) diff --git a/doc/source/users/proxies/compute.rst b/doc/source/users/proxies/compute.rst index 5a5f25364..3851966dc 100644 --- a/doc/source/users/proxies/compute.rst +++ b/doc/source/users/proxies/compute.rst @@ -103,7 +103,6 @@ Service Operations .. autoclass:: openstack.compute.v2._proxy.Proxy - .. automethod:: openstack.compute.v2._proxy.Proxy.get_service .. automethod:: openstack.compute.v2._proxy.Proxy.services .. automethod:: openstack.compute.v2._proxy.Proxy.enable_service .. automethod:: openstack.compute.v2._proxy.Proxy.disable_service diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 6d342d91a..5f248d692 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -1056,20 +1056,6 @@ def get_hypervisor(self, hypervisor): """ return self._get(_hypervisor.Hypervisor, hypervisor) - def get_service(self, service): - """Get a single service - - :param service: The value can be the ID of a serivce or a - :class:`~openstack.compute.v2.service.Service` - instance. - - :returns: - A :class:`~openstack.compute.v2.serivce.Service` object. - :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. - """ - return self._get(_service.Service, service) - def force_service_down(self, service, host, binary): """Force a service down diff --git a/openstack/compute/v2/service.py b/openstack/compute/v2/service.py index 9a48a3921..a36f67dff 100644 --- a/openstack/compute/v2/service.py +++ b/openstack/compute/v2/service.py @@ -23,7 +23,6 @@ class Service(resource2.Resource): service = compute_service.ComputeService() # capabilities - allow_get = True allow_list = True allow_update = True diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index f50112fcf..6beef34dd 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -471,10 +471,6 @@ def test_get_hypervisor(self): self.verify_get(self.proxy.get_hypervisor, hypervisor.Hypervisor) - def test_get_service(self): - self.verify_get(self.proxy.get_service, - service.Service) - def test_services(self): self.verify_list_no_kwargs(self.proxy.services, service.Service, diff --git a/openstack/tests/unit/compute/v2/test_service.py b/openstack/tests/unit/compute/v2/test_service.py index b993b5ac6..0872cf08e 100644 --- a/openstack/tests/unit/compute/v2/test_service.py +++ b/openstack/tests/unit/compute/v2/test_service.py @@ -42,9 +42,9 @@ def test_basic(self): self.assertEqual('services', sot.resources_key) self.assertEqual('/os-services', sot.base_path) self.assertEqual('compute', sot.service.service_type) - self.assertTrue(sot.allow_get) self.assertTrue(sot.allow_update) self.assertTrue(sot.allow_list) + self.assertFalse(sot.allow_get) def test_make_it(self): sot = service.Service(**EXAMPLE) From 6961772a4f34603049e0f74ce6f99bdcf5fe6a1a Mon Sep 17 00:00:00 2001 From: "Yuanbin.Chen" Date: Mon, 5 Jun 2017 23:52:39 +0800 Subject: [PATCH 1616/3836] Add compute support server backup operation Implements: blueprint add-compute-backup Change-Id: I8349cc5a16f39caa1d68db88b89313cf4d3ed416 Signed-off-by: Yuanbin.Chen --- doc/source/users/proxies/compute.rst | 1 + openstack/compute/v2/_proxy.py | 16 ++++++++++++++++ openstack/compute/v2/server.py | 10 ++++++++++ openstack/tests/unit/compute/v2/test_proxy.py | 6 ++++++ openstack/tests/unit/compute/v2/test_server.py | 13 +++++++++++++ 5 files changed, 46 insertions(+) diff --git a/doc/source/users/proxies/compute.rst b/doc/source/users/proxies/compute.rst index 5a5f25364..a19d2842b 100644 --- a/doc/source/users/proxies/compute.rst +++ b/doc/source/users/proxies/compute.rst @@ -29,6 +29,7 @@ Server Operations .. automethod:: openstack.compute.v2._proxy.Proxy.delete_server_metadata .. automethod:: openstack.compute.v2._proxy.Proxy.wait_for_server .. automethod:: openstack.compute.v2._proxy.Proxy.create_server_image + .. automethod:: openstack.compute.v2._proxy.Proxy.backup_server Network Actions *************** diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 6d342d91a..fa826ccef 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -620,6 +620,22 @@ def remove_floating_ip_from_server(self, server, address): server = self._get_resource(_server.Server, server) server.remove_floating_ip(self._session, address) + def backup_server(self, server, name, backup_type, rotation): + """Backup a server + + :param server: Either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance. + :param name: The name of the backup image. + :param backup_type: The type of the backup, for example, daily. + :param rotation: The rotation of the back up image, the oldest + image will be removed when image count exceed + the rotation count. + + :returns: None + """ + server = self._get_resource(_server.Server, server) + server.backup(self._session, name, backup_type, rotation) + def pause_server(self, server): """Pauses a server and changes its status to ``PAUSED``. diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 75c89a364..b45277ca1 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -274,6 +274,16 @@ def remove_floating_ip(self, session, address): body = {"removeFloatingIp": {"address": address}} self._action(session, body) + def backup(self, session, name, backup_type, rotation): + body = { + "createBackup": { + "name": name, + "backup_type": backup_type, + "rotation": rotation + } + } + self._action(session, body) + def pause(self, session): body = {"pause": None} self._action(session, body) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index f50112fcf..0e9ec135e 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -318,6 +318,12 @@ def test_remove_floating_ip_from_server(self): method_args=["value", "address"], expected_args=["address"]) + def test_server_backup(self): + self._verify("openstack.compute.v2.server.Server.backup", + self.proxy.backup_server, + method_args=["value", "name", "daily", 1], + expected_args=["name", "daily", 1]) + def test_server_pause(self): self._verify("openstack.compute.v2.server.Server.pause", self.proxy.pause_server, diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 0c5109e36..481683041 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -424,6 +424,19 @@ def test_remove_floating_ip(self): self.sess.post.assert_called_with( url, endpoint_filter=sot.service, json=body, headers=headers) + def test_backup(self): + sot = server.Server(**EXAMPLE) + + res = sot.backup(self.sess, "name", "daily", 1) + + self.assertIsNone(res) + url = 'servers/IDENTIFIER/action' + body = {"createBackup": {"name": "name", "backup_type": "daily", + "rotation": 1}} + headers = {'Accept': ''} + self.sess.post.assert_called_with( + url, endpoint_filter=sot.service, json=body, headers=headers) + def test_pause(self): sot = server.Server(**EXAMPLE) From 8d875a4211815bb400abb9caae7537c7223f27f7 Mon Sep 17 00:00:00 2001 From: Yuval Shalev Date: Thu, 25 May 2017 18:57:54 +0300 Subject: [PATCH 1617/3836] Added server console output method Change-Id: I12af4d8a4bc0df3f0377b6403f3ddef760b4615d --- doc/source/users/proxies/compute.rst | 1 + openstack/compute/v2/_proxy.py | 13 +++++++++++ openstack/compute/v2/server.py | 7 ++++++ openstack/tests/unit/compute/v2/test_proxy.py | 11 +++++++++ .../tests/unit/compute/v2/test_server.py | 23 +++++++++++++++++++ 5 files changed, 55 insertions(+) diff --git a/doc/source/users/proxies/compute.rst b/doc/source/users/proxies/compute.rst index 7c6573816..63dc2b4ec 100644 --- a/doc/source/users/proxies/compute.rst +++ b/doc/source/users/proxies/compute.rst @@ -62,6 +62,7 @@ Starting, Stopping, etc. .. automethod:: openstack.compute.v2._proxy.Proxy.unrescue_server .. automethod:: openstack.compute.v2._proxy.Proxy.evacuate_server .. automethod:: openstack.compute.v2._proxy.Proxy.migrate_server + .. automethod:: openstack.compute.v2._proxy.Proxy.get_server_console_output Modifying a Server ****************** diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index dbdfa6822..5a094078f 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -775,6 +775,19 @@ def unshelve_server(self, server): server = self._get_resource(_server.Server, server) server.unshelve(self._session) + def get_server_console_output(self, server, length=None): + """Return the console output for a server. + + :param server: Either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance. + :param length: Optional number of line to fetch from the end of console + log. All lines will be returned if this is not specified. + :returns: The console output as a dict. Control characters will be + escaped to create a valid JSON string. + """ + server = self._get_resource(_server.Server, server) + return server.get_console_output(self._session, length=length) + def wait_for_server(self, server, status='ACTIVE', failures=['ERROR'], interval=2, wait=120): return resource2.wait_for_status(self._session, server, status, diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 6618a0939..8955a5c31 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -340,6 +340,13 @@ def migrate(self, session): body = {"migrate": None} self._action(session, body) + def get_console_output(self, session, length=None): + body = {"os-getConsoleOutput": {}} + if length is not None: + body["os-getConsoleOutput"]["length"] = length + resp = self._action(session, body) + return resp.json() + class ServerDetail(Server): base_path = '/servers/detail' diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index f50112fcf..206b412dd 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -401,6 +401,17 @@ def test_server_unshelve(self): self.proxy.unshelve_server, method_args=["value"]) + def test_get_server_output(self): + self._verify("openstack.compute.v2.server.Server.get_console_output", + self.proxy.get_server_console_output, + method_args=["value"], + expected_kwargs={"length": None}) + + self._verify("openstack.compute.v2.server.Server.get_console_output", + self.proxy.get_server_console_output, + method_args=["value", 1], + expected_kwargs={"length": 1}) + def test_availability_zones(self): self.verify_list_no_kwargs(self.proxy.availability_zones, az.AvailabilityZone, diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 0e60d050d..44da49be2 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -615,6 +615,29 @@ def test_migrate(self): self.assertIsNone(res) url = 'servers/IDENTIFIER/action' body = {"migrate": None} + + headers = {'Accept': ''} + self.sess.post.assert_called_with( + url, endpoint_filter=sot.service, json=body, headers=headers) + + def test_get_console_output(self): + sot = server.Server(**EXAMPLE) + + res = sot.get_console_output(self.sess) + + self.assertIsNone(res) + url = 'servers/IDENTIFIER/action' + body = {'os-getConsoleOutput': {}} + headers = {'Accept': ''} + self.sess.post.assert_called_with( + url, endpoint_filter=sot.service, json=body, headers=headers) + + res = sot.get_console_output(self.sess, length=1) + + self.assertIsNone(res) + url = 'servers/IDENTIFIER/action' + body = {'os-getConsoleOutput': {'length': 1}} + headers = {'Accept': ''} self.sess.post.assert_called_with( url, endpoint_filter=sot.service, json=body, headers=headers) From 9353650d1d8325bc8836980f84fa14bbfd37d746 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 14 Jun 2017 06:28:12 -0500 Subject: [PATCH 1618/3836] Fix urljoin for neutron endpoint We just add a '/v2.0' to the catalog endpoint for neutron. However, if the catalog endpoint does not end in a /, this can turn https://domain.tld/neutron into https://domain.tld/v2.0 instead of https://domain.tld/neutron/v2.0 as intended. Change-Id: If16dc64dd9a4c9e525d042190632d0f4f0a4fb63 --- shade/openstackcloud.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 0182018e1..a04b6305f 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -579,8 +579,13 @@ def _network_client(self): client = self._get_raw_client('network') # Don't bother with version discovery - there is only one version # of neutron. This is what neutronclient does, fwiw. - client.endpoint_override = urllib.parse.urljoin( - client.get_endpoint(), 'v2.0') + endpoint = client.get_endpoint() + if not endpoint.rstrip().rsplit('/')[1] == 'v2.0': + if not endpoint.endswith('/'): + endpoint += '/' + endpoint = urllib.parse.urljoin( + endpoint, 'v2.0') + client.endpoint_override = endpoint self._raw_clients['network'] = client return self._raw_clients['network'] From 79462437058e37b83b8153531b5078ddc5a1edb8 Mon Sep 17 00:00:00 2001 From: "Yuanbin.Chen" Date: Thu, 1 Jun 2017 21:39:01 +0800 Subject: [PATCH 1619/3836] Add compute support server live migrate operation Implements: blueprint add-compute-migrate Change-Id: I16e40a946565a94a1ecff0ad522c7cc79bdb234e Signed-off-by: Yuanbin.Chen --- doc/source/users/proxies/compute.rst | 1 + openstack/compute/v2/_proxy.py | 14 +++++++++++++- openstack/compute/v2/server.py | 10 ++++++++++ openstack/tests/unit/compute/v2/test_proxy.py | 6 ++++++ .../tests/unit/compute/v2/test_server.py | 19 +++++++++++++++++++ 5 files changed, 49 insertions(+), 1 deletion(-) diff --git a/doc/source/users/proxies/compute.rst b/doc/source/users/proxies/compute.rst index 9fb50d01a..3d3b3cb13 100644 --- a/doc/source/users/proxies/compute.rst +++ b/doc/source/users/proxies/compute.rst @@ -64,6 +64,7 @@ Starting, Stopping, etc. .. automethod:: openstack.compute.v2._proxy.Proxy.evacuate_server .. automethod:: openstack.compute.v2._proxy.Proxy.migrate_server .. automethod:: openstack.compute.v2._proxy.Proxy.get_server_console_output + .. automethod:: openstack.compute.v2._proxy.Proxy.live_migrate_server Modifying a Server ****************** diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 626222987..9ee9899bc 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -1267,6 +1267,18 @@ def migrate_server(self, server): :class:`~openstack.compute.v2.server.Server` instance. :returns: None """ - server = self._get_resource(_server.Server, server) server.migrate(self._session) + + def live_migrate_server(self, server, host=None, force=False): + """Migrate a server from one host to target host + + :param server: Either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance. + :param host: The host to which to migrate the server + :param force: Force a live-migration by not verifying the provided + destination host by the scheduler. + :returns: None + """ + server = self._get_resource(_server.Server, server) + server.live_migrate(self._session, host, force) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 440cdc62d..574b72021 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -357,6 +357,16 @@ def get_console_output(self, session, length=None): resp = self._action(session, body) return resp.json() + def live_migrate(self, session, host, force): + body = { + "os-migrateLive": { + "host": host, + "block_migration": "auto", + "force": force + } + } + self._action(session, body) + class ServerDetail(Server): base_path = '/servers/detail' diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 9468a0f53..a608a261d 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -510,3 +510,9 @@ def test_force_service_down(self): self.proxy.force_service_down, method_args=["value", "host1", "nova-compute"], expected_args=["host1", "nova-compute"]) + + def test_live_migrate_server(self): + self._verify('openstack.compute.v2.server.Server.live_migrate', + self.proxy.live_migrate_server, + method_args=["value", "host1", "force"], + expected_args=["host1", "force"]) diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 3485eb8a8..80f5fa93e 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -654,3 +654,22 @@ def test_get_console_output(self): headers = {'Accept': ''} self.sess.post.assert_called_with( url, endpoint_filter=sot.service, json=body, headers=headers) + + def test_live_migrate(self): + sot = server.Server(**EXAMPLE) + + res = sot.live_migrate(self.sess, host='HOST2', force=False) + + self.assertIsNone(res) + url = 'servers/IDENTIFIER/action' + body = { + "os-migrateLive": { + "host": 'HOST2', + "block_migration": "auto", + "force": False + } + } + + headers = {'Accept': ''} + self.sess.post.assert_called_with( + url, endpoint_filter=sot.service, json=body, headers=headers) From f759fec584fad8c4d4a265abbeeded79636c766c Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Wed, 14 Jun 2017 13:05:30 -0400 Subject: [PATCH 1620/3836] Fix title in Network Agent resource doc This was previously causing the resource listing to call the agent docs the network.v2.network docs. Change-Id: I4d61840b5356cbe698ffb7d5c7c65f1808f4988f Closes: 1697966 --- doc/source/users/resources/network/v2/agent.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/users/resources/network/v2/agent.rst b/doc/source/users/resources/network/v2/agent.rst index f83856a62..1d99337dc 100644 --- a/doc/source/users/resources/network/v2/agent.rst +++ b/doc/source/users/resources/network/v2/agent.rst @@ -1,5 +1,5 @@ -openstack.network.v2.network -============================ +openstack.network.v2.agent +========================== .. automodule:: openstack.network.v2.agent From 4fa316b778a88c92e4fcfd57480631d45cc2df5e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 14 Jun 2017 14:43:13 -0500 Subject: [PATCH 1621/3836] Skip pagination test for now The pagination test, although awesome, is killing the cinder in the gate and we're seeing an unworkable increase in timeouts. Skip the test for now allowing us to work on fixing the test without killing everything else. Change-Id: If83abbad9398cb97ca7fdc1d3f6d2d5473343664 --- shade/tests/functional/test_volume.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shade/tests/functional/test_volume.py b/shade/tests/functional/test_volume.py index 4972130a0..59bca0145 100644 --- a/shade/tests/functional/test_volume.py +++ b/shade/tests/functional/test_volume.py @@ -109,6 +109,8 @@ def cleanup(self, volume, snapshot_name=None, image_name=None): def test_list_volumes_pagination(self): '''Test pagination for list volumes functionality''' + self.skipTest("Pagination test killing gate current") + volumes = [] # the number of created volumes needs to be higher than # CONF.osapi_max_limit but not higher than volume quotas for From 8ed5666501966ca8a2d9bfed8b9111f612b4dddf Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Wed, 14 Jun 2017 23:22:17 +0000 Subject: [PATCH 1622/3836] Increase timeout for volume tests Creating and deleting volumes is slow so tests like test_list_volumes_pagination are often timing out Change-Id: I87f59bdfb527326a2a90a03d524e38d632ea7377 Signed-off-by: Rosario Di Somma --- shade/tests/functional/test_volume.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/shade/tests/functional/test_volume.py b/shade/tests/functional/test_volume.py index 59bca0145..8a3d20c70 100644 --- a/shade/tests/functional/test_volume.py +++ b/shade/tests/functional/test_volume.py @@ -25,6 +25,9 @@ class TestVolume(base.BaseFunctionalTestCase): + # Creating and deleting volumes is slow + TIMEOUT_SCALING_FACTOR = 1.5 + def setUp(self): super(TestVolume, self).setUp() if not self.user_cloud.has_service('volume'): @@ -109,7 +112,6 @@ def cleanup(self, volume, snapshot_name=None, image_name=None): def test_list_volumes_pagination(self): '''Test pagination for list volumes functionality''' - self.skipTest("Pagination test killing gate current") volumes = [] # the number of created volumes needs to be higher than From 972dcfcdf71feca95d166e1d9c152fcc097567fd Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 13 Jun 2017 12:27:31 -0500 Subject: [PATCH 1623/3836] Migrate non-list server interactions to REST Change-Id: I999ea383909d01317f361ef55bee73db9fc1afbf --- shade/_tasks.py | 20 ------- shade/openstackcloud.py | 74 +++++++++++++++---------- shade/tests/functional/test_compute.py | 2 +- shade/tests/unit/test_rebuild_server.py | 24 -------- shade/tests/unit/test_update_server.py | 4 -- 5 files changed, 46 insertions(+), 78 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index a75a53117..054bfe6ae 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -72,26 +72,6 @@ def main(self, client): return client.nova_client.servers.list(**self.args) -class ServerUpdate(task_manager.Task): - def main(self, client): - return client.nova_client.servers.update(**self.args) - - -class ServerRebuild(task_manager.Task): - def main(self, client): - return client.nova_client.servers.rebuild(**self.args) - - -class ServerSetMetadata(task_manager.Task): - def main(self, client): - return client.nova_client.servers.set_meta(**self.args) - - -class ServerDeleteMetadata(task_manager.Task): - def main(self, client): - return client.nova_client.servers.delete_meta(**self.args) - - class ServerGroupList(task_manager.Task): def main(self, client): return client.nova_client.server_groups.list(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 13d110faf..e2c4926a9 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5680,11 +5680,20 @@ def get_active_server( def rebuild_server(self, server_id, image_id, admin_pass=None, detailed=False, bare=False, wait=False, timeout=180): - with _utils.shade_exceptions("Error in rebuilding instance"): - server = self.manager.submit_task(_tasks.ServerRebuild( - server=server_id, image=image_id, password=admin_pass)) + kwargs = {} + if image_id: + kwargs['imageRef'] = image_id + if admin_pass: + kwargs['adminPass'] = admin_pass + + server = self._compute_client.post( + '/servers/{server_id}/action'.format(server_id=server_id), + error_message="Error in rebuilding instance", + json={'rebuild': kwargs}) if not wait: - return server + # TODO(mordred) add expand server next + return self._normalize_server(server) + admin_pass = server.get('adminPass') or admin_pass for count in _utils._iterate_timeout( timeout, @@ -5720,16 +5729,15 @@ def set_server_metadata(self, name_or_id, metadata): :raises: OpenStackCloudException on operation error. """ - try: - self.manager.submit_task( - _tasks.ServerSetMetadata( - server=self.get_server(name_or_id, bare=True), - metadata=metadata)) - except OpenStackCloudException: - raise - except Exception as e: + server = self.get_server(name_or_id, bare=True) + if not server: raise OpenStackCloudException( - "Error updating metadata: {0}".format(e)) + 'Invalid Server {server}'.format(server=name_or_id)) + + self._compute_client.post( + '/servers/{server_id}/metadata'.format(server_id=server['id']), + json={'metadata': metadata}, + error_message='Error updating server metadata') def delete_server_metadata(self, name_or_id, metadata_keys): """Delete metadata from a server instance. @@ -5741,16 +5749,19 @@ def delete_server_metadata(self, name_or_id, metadata_keys): :raises: OpenStackCloudException on operation error. """ - try: - self.manager.submit_task( - _tasks.ServerDeleteMetadata( - server=self.get_server(name_or_id, bare=True), - keys=metadata_keys)) - except OpenStackCloudException: - raise - except Exception as e: + server = self.get_server(name_or_id, bare=True) + if not server: raise OpenStackCloudException( - "Error deleting metadata: {0}".format(e)) + 'Invalid Server {server}'.format(server=name_or_id)) + + for key in metadata_keys: + error_message = 'Error deleting metadata {key} on {server}'.format( + key=key, server=name_or_id) + self._compute_client.delete( + '/servers/{server_id}/metadata/{key}'.format( + server_id=server['id'], + key=key), + error_message=error_message) def delete_server( self, name_or_id, wait=False, timeout=180, delete_ips=False, @@ -5847,10 +5858,16 @@ def _delete_server( @_utils.valid_kwargs( 'name', 'description') - def update_server(self, name_or_id, **kwargs): + def update_server(self, name_or_id, detailed=False, bare=False, **kwargs): """Update a server. :param name_or_id: Name of the server to be updated. + :param detailed: Whether or not to add detailed additional information. + Defaults to False. + :param bare: Whether to skip adding any additional information to the + server record. Defaults to False, meaning the addresses + dict will be populated as needed from neutron. Setting + to True implies detailed = False. :name: New name for the server :description: New description for the server @@ -5863,12 +5880,11 @@ def update_server(self, name_or_id, **kwargs): raise OpenStackCloudException( "failed to find server '{server}'".format(server=name_or_id)) - with _utils.shade_exceptions( - "Error updating server {0}".format(name_or_id)): - # TODO(mordred) This is not sending back a normalized server - return self.manager.submit_task( - _tasks.ServerUpdate( - server=server['id'], **kwargs)) + return self._normalize_server( + self._compute_client.put( + '/servers/{server_id}'.format(server_id=server['id']), + error_message="Error updating server {0}".format(name_or_id), + json={'server': kwargs})) def create_server_group(self, name, policies): """Create a new server group. diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index 0d198c95a..c4c1e4be0 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -382,7 +382,7 @@ def test_set_and_delete_metadata(self): self.assertEqual(set(updated_server.metadata.items()), set([])) self.assertRaises( - exc.OpenStackCloudException, + exc.OpenStackCloudURINotFound, self.user_cloud.delete_server_metadata, self.server_name, ['key1']) diff --git a/shade/tests/unit/test_rebuild_server.py b/shade/tests/unit/test_rebuild_server.py index d7cc14d12..2f5c3c5fc 100644 --- a/shade/tests/unit/test_rebuild_server.py +++ b/shade/tests/unit/test_rebuild_server.py @@ -79,10 +79,6 @@ def test_rebuild_server_server_error(self): json={ 'rebuild': { 'imageRef': 'a'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', self.server_id]), - json={'server': self.rebuild_server}), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -109,10 +105,6 @@ def test_rebuild_server_timeout(self): json={ 'rebuild': { 'imageRef': 'a'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', self.server_id]), - json={'server': self.rebuild_server}), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -140,10 +132,6 @@ def test_rebuild_server_no_wait(self): json={ 'rebuild': { 'imageRef': 'a'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', self.server_id]), - json={'server': self.rebuild_server}), ]) self.assertEqual( self.rebuild_server['status'], @@ -170,10 +158,6 @@ def test_rebuild_server_with_admin_pass_no_wait(self): 'rebuild': { 'imageRef': 'a', 'adminPass': password}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', self.server_id]), - json={'server': self.rebuild_server}), ]) self.assertEqual( password, @@ -202,10 +186,6 @@ def test_rebuild_server_with_admin_pass_wait(self): 'rebuild': { 'imageRef': 'a', 'adminPass': password}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', self.server_id]), - json={'server': self.rebuild_server}), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -243,10 +223,6 @@ def test_rebuild_server_wait(self): json={ 'rebuild': { 'imageRef': 'a'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', self.server_id]), - json={'server': self.rebuild_server}), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), diff --git a/shade/tests/unit/test_update_server.py b/shade/tests/unit/test_update_server.py index db30a7ec4..5f85ccc37 100644 --- a/shade/tests/unit/test_update_server.py +++ b/shade/tests/unit/test_update_server.py @@ -75,10 +75,6 @@ def test_update_server_name(self): json={'server': fake_update_server}, validate=dict( json={'server': {'name': self.updated_server_name}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', self.server_id]), - json={'server': fake_update_server}), ]) self.assertEqual( self.updated_server_name, From 494d5d1e0fe2d600b43dd4ae9f7c9657a08bbc88 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 13 Jun 2017 14:57:39 -0500 Subject: [PATCH 1624/3836] Properly expand server dicts after rebuild and update Change-Id: I60568206c6b8900560ebd4f202aaaa5cd9f0d71e --- shade/openstackcloud.py | 9 +++++---- shade/tests/unit/test_rebuild_server.py | 8 ++++++++ shade/tests/unit/test_update_server.py | 4 ++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index e2c4926a9..99119b40f 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5691,8 +5691,8 @@ def rebuild_server(self, server_id, image_id, admin_pass=None, error_message="Error in rebuilding instance", json={'rebuild': kwargs}) if not wait: - # TODO(mordred) add expand server next - return self._normalize_server(server) + return self._expand_server( + self._normalize_server(server), bare=bare, detailed=detailed) admin_pass = server.get('adminPass') or admin_pass for count in _utils._iterate_timeout( @@ -5880,11 +5880,12 @@ def update_server(self, name_or_id, detailed=False, bare=False, **kwargs): raise OpenStackCloudException( "failed to find server '{server}'".format(server=name_or_id)) - return self._normalize_server( + return self._expand_server(self._normalize_server( self._compute_client.put( '/servers/{server_id}'.format(server_id=server['id']), error_message="Error updating server {0}".format(name_or_id), - json={'server': kwargs})) + json={'server': kwargs})), + bare=bare, detailed=detailed) def create_server_group(self, name, policies): """Create a new server group. diff --git a/shade/tests/unit/test_rebuild_server.py b/shade/tests/unit/test_rebuild_server.py index 2f5c3c5fc..dd588a26b 100644 --- a/shade/tests/unit/test_rebuild_server.py +++ b/shade/tests/unit/test_rebuild_server.py @@ -132,6 +132,10 @@ def test_rebuild_server_no_wait(self): json={ 'rebuild': { 'imageRef': 'a'}})), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': []}), ]) self.assertEqual( self.rebuild_server['status'], @@ -158,6 +162,10 @@ def test_rebuild_server_with_admin_pass_no_wait(self): 'rebuild': { 'imageRef': 'a', 'adminPass': password}})), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': []}), ]) self.assertEqual( password, diff --git a/shade/tests/unit/test_update_server.py b/shade/tests/unit/test_update_server.py index 5f85ccc37..ec50694be 100644 --- a/shade/tests/unit/test_update_server.py +++ b/shade/tests/unit/test_update_server.py @@ -75,6 +75,10 @@ def test_update_server_name(self): json={'server': fake_update_server}, validate=dict( json={'server': {'name': self.updated_server_name}})), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': []}), ]) self.assertEqual( self.updated_server_name, From daabd8cb9cded99ffeef9de34321bd9439d722b8 Mon Sep 17 00:00:00 2001 From: Markus Zoeller Date: Fri, 16 Jun 2017 11:21:41 +0200 Subject: [PATCH 1625/3836] docs: make the first example easier to understand The very first example of the usage of shade is easier to understand if there is also an example of the needed `clouds.yml` file. Without it, it is unclear what the meaning of the key `mordred` means. Change-Id: Iad3aba66b0c6344157da30f374e191d01e938b2b --- README.rst | 55 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/README.rst b/README.rst index f375f8670..fdfebc823 100644 --- a/README.rst +++ b/README.rst @@ -21,25 +21,48 @@ Example ======= Sometimes an example is nice. -:: - import shade +#. Create a ``clouds.yml`` file:: - # Initialize and turn on debug logging - shade.simple_logging(debug=True) + clouds: + mordred: + region_name: RegionOne + auth: + username: 'mordred' + password: XXXXXXX + project_name: 'shade' + auth_url: 'https://montytaylor-sjc.openstack.blueboxgrid.com:5001/v2.0' - # Initialize cloud - # Cloud configs are read with os-client-config - cloud = shade.openstack_cloud(cloud='mordred') + Please note: *os-client-config* will look for a file called ``clouds.yaml`` + in the following locations: - # Upload an image to the cloud - image = cloud.create_image( - 'ubuntu-trusty', filename='ubuntu-trusty.qcow2', wait=True) + * Current Directory + * ``~/.config/openstack`` + * ``/etc/openstack`` - # Find a flavor with at least 512M of RAM - flavor = cloud.get_flavor_by_ram(512) + More information at https://pypi.python.org/pypi/os-client-config + + +#. Create a server with *shade*, configured with the ``clouds.yml`` file:: + + import shade + + # Initialize and turn on debug logging + shade.simple_logging(debug=True) + + # Initialize cloud + # Cloud configs are read with os-client-config + cloud = shade.openstack_cloud(cloud='mordred') + + # Upload an image to the cloud + image = cloud.create_image( + 'ubuntu-trusty', filename='ubuntu-trusty.qcow2', wait=True) + + # Find a flavor with at least 512M of RAM + flavor = cloud.get_flavor_by_ram(512) + + # Boot a server, wait for it to boot, and then do whatever is needed + # to get a public ip for it. + cloud.create_server( + 'my-server', image=image, flavor=flavor, wait=True, auto_ip=True) - # Boot a server, wait for it to boot, and then do whatever is needed - # to get a public ip for it. - cloud.create_server( - 'my-server', image=image, flavor=flavor, wait=True, auto_ip=True) From 6b325282d3ab2f848de372a80def36b01b6ba6f4 Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Fri, 16 Jun 2017 18:13:15 +0000 Subject: [PATCH 1626/3836] Retry to fetch paginated volumes if we get 404 for next link When we get a volume list, it's possible for a volume to disappear causing the pagination to bork. When that happens we retry to get the list from scratch for 5 times. If all the attempts fail we just return what we found. Change-Id: Ia88d1d8a6b3558f7d5d364a9c78bbf834836d3f7 Signed-off-by: Rosario Di Somma --- shade/openstackcloud.py | 42 +++++++++++++-- shade/tests/unit/test_volume.py | 93 +++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 5 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index ccde5f2fd..6a9dcb642 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1700,21 +1700,53 @@ def list_volumes(self, cache=True): """ def _list(data): - volumes.extend(meta.get_and_munchify('volumes', data)) + volumes.extend(data.get('volumes', [])) endpoint = None for l in data.get('volumes_links', []): if 'rel' in l and 'next' == l['rel']: endpoint = l['href'] break if endpoint: - _list(self._volume_client.get(endpoint)) + try: + _list(self._volume_client.get(endpoint)) + except OpenStackCloudURINotFound: + # Catch and re-raise here because we are making recursive + # calls and we just have context for the log here + self.log.debug( + "While listing volumes, could not find next link" + " {link}.".format(link=data)) + raise if not cache: warnings.warn('cache argument to list_volumes is deprecated. Use ' 'invalidate instead.') - volumes = [] - _list(self._volume_client.get('/volumes/detail')) - return self._normalize_volumes(volumes) + + # Fetching paginated volumes can fails for several reasons, if + # something goes wrong we'll have to start fetching volumes from + # scratch + attempts = 5 + for _ in range(attempts): + volumes = [] + data = self._volume_client.get('/volumes/detail') + if 'volumes_links' not in data: + # no pagination needed + volumes.extend(data.get('volumes', [])) + break + + try: + _list(data) + break + except OpenStackCloudURINotFound: + pass + else: + self.log.debug( + "List volumes failed to retrieve all volumes after" + " {attempts} attempts. Returning what we found.".format( + attempts=attempts)) + # list volumes didn't complete succesfully so just return what + # we found + return self._normalize_volumes( + meta.get_and_munchify(key=None, data=volumes)) @_utils.cache_on_arguments() def list_volume_types(self, get_extra=True): diff --git a/shade/tests/unit/test_volume.py b/shade/tests/unit/test_volume.py index a0f9475a4..c4ff2aa10 100644 --- a/shade/tests/unit/test_volume.py +++ b/shade/tests/unit/test_volume.py @@ -319,3 +319,96 @@ def test_list_volumes_with_pagination(self): self.cloud._normalize_volume(vol2)], self.cloud.list_volumes()) self.assert_calls() + + def test_list_volumes_with_pagination_next_link_fails_once(self): + vol1 = meta.obj_to_munch(fakes.FakeVolume('01', 'available', 'vol1')) + vol2 = meta.obj_to_munch(fakes.FakeVolume('02', 'available', 'vol2')) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['volumes', 'detail']), + json={ + 'volumes': [vol1], + 'volumes_links': [ + {'href': self.get_mock_url( + 'volumev2', 'public', + append=['volumes', 'detail'], + qs_elements=['marker=01']), + 'rel': 'next'}]}), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['volumes', 'detail'], + qs_elements=['marker=01']), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['volumes', 'detail']), + json={ + 'volumes': [vol1], + 'volumes_links': [ + {'href': self.get_mock_url( + 'volumev2', 'public', + append=['volumes', 'detail'], + qs_elements=['marker=01']), + 'rel': 'next'}]}), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['volumes', 'detail'], + qs_elements=['marker=01']), + json={ + 'volumes': [vol2], + 'volumes_links': [ + {'href': self.get_mock_url( + 'volumev2', 'public', + append=['volumes', 'detail'], + qs_elements=['marker=02']), + 'rel': 'next'}]}), + + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['volumes', 'detail'], + qs_elements=['marker=02']), + json={'volumes': []})]) + self.assertEqual( + [self.cloud._normalize_volume(vol1), + self.cloud._normalize_volume(vol2)], + self.cloud.list_volumes()) + self.assert_calls() + + def test_list_volumes_with_pagination_next_link_fails_all_attempts(self): + vol1 = meta.obj_to_munch(fakes.FakeVolume('01', 'available', 'vol1')) + uris = [] + attempts = 5 + for i in range(attempts): + uris.extend([ + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['volumes', 'detail']), + json={ + 'volumes': [vol1], + 'volumes_links': [ + {'href': self.get_mock_url( + 'volumev2', 'public', + append=['volumes', 'detail'], + qs_elements=['marker=01']), + 'rel': 'next'}]}), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['volumes', 'detail'], + qs_elements=['marker=01']), + status_code=404)]) + self.register_uris(uris) + # Check that found volumes are returned even if pagination didn't + # complete because call to get next link 404'ed for all the allowed + # attempts + self.assertEqual( + [self.cloud._normalize_volume(vol1)], + self.cloud.list_volumes()) + self.assert_calls() From c431cc2c4bed6b8b181fd94b6d57098b32c68c83 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 14 Jun 2017 18:08:27 -0500 Subject: [PATCH 1627/3836] Add some release notes we forgot to add Change-Id: I05c31400e6f2e584a656c8ab422875be2d6f6bfe --- .../notes/multiple-updates-b48cc2f6db2e526d.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 releasenotes/notes/multiple-updates-b48cc2f6db2e526d.yaml diff --git a/releasenotes/notes/multiple-updates-b48cc2f6db2e526d.yaml b/releasenotes/notes/multiple-updates-b48cc2f6db2e526d.yaml new file mode 100644 index 000000000..5df3f6d51 --- /dev/null +++ b/releasenotes/notes/multiple-updates-b48cc2f6db2e526d.yaml @@ -0,0 +1,14 @@ +--- +features: + - Removed unneeded calls that were made when deleting servers with + floating ips. + - Added pagination support for volume listing. +upgrade: + - Removed designateclient as a dependency. All designate operations + are now performed with direct REST calls using keystoneauth + Adapter. + - Server creation calls are now done with direct REST calls. +fixes: + - Fixed a bug related to neutron endpoints that did not have trailing + slashes. + - Fixed issue with ports not having a created_at attribute. From 35980c1da8977db1cffb66975145ee5532a83fe8 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 13 Jun 2017 21:35:02 -0500 Subject: [PATCH 1628/3836] Break early from volume cleanup loop Change-Id: I540c4d860a60810d6ae6838ec558cc48cff67cf8 --- shade/tests/functional/test_volume.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shade/tests/functional/test_volume.py b/shade/tests/functional/test_volume.py index 8a3d20c70..c94577254 100644 --- a/shade/tests/functional/test_volume.py +++ b/shade/tests/functional/test_volume.py @@ -107,6 +107,9 @@ def cleanup(self, volume, snapshot_name=None, image_name=None): for v in volume: if v['id'] == existing['id']: found = True + break + if found: + break if not found: break From c5245648edcd2041d1df1f94f68c5d612fda6be9 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 15 Jun 2017 21:38:56 -0500 Subject: [PATCH 1629/3836] Remove some unused mocks Change-Id: I25c59933cd61eff31d02b3c763f6ebd5551d3231 --- shade/tests/unit/test_floating_ip_common.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/shade/tests/unit/test_floating_ip_common.py b/shade/tests/unit/test_floating_ip_common.py index 4e813cf11..d9860cf0d 100644 --- a/shade/tests/unit/test_floating_ip_common.py +++ b/shade/tests/unit/test_floating_ip_common.py @@ -57,18 +57,14 @@ def test_add_auto_ip( timeout=60, wait=False, server=server_dict, floating_ip=floating_ip_dict, skip_attach=False) - @patch.object(OpenStackCloud, 'nova_client') @patch.object(OpenStackCloud, '_add_ip_from_pool') - def test_add_ips_to_server_pool( - self, mock_add_ip_from_pool, mock_nova_client): + def test_add_ips_to_server_pool(self, mock_add_ip_from_pool): server = FakeServer( id='romeo', name='test-server', status="ACTIVE", addresses={} ) server_dict = meta.obj_to_munch(server) pool = 'nova' - mock_nova_client.servers.get.return_value = server - self.cloud.add_ips_to_server(server_dict, ip_pool=pool) mock_add_ip_from_pool.assert_called_with( @@ -187,16 +183,13 @@ def test_add_ips_to_server_rackspace_local_ipv4( mock_add_auto_ip.assert_not_called() self.assertEqual(new_server['interface_ip'], '104.130.246.91') - @patch.object(OpenStackCloud, 'nova_client') @patch.object(OpenStackCloud, 'add_ip_list') - def test_add_ips_to_server_ip_list( - self, mock_add_ip_list, mock_nova_client): + def test_add_ips_to_server_ip_list(self, mock_add_ip_list): server = FakeServer( id='server-id', name='test-server', status="ACTIVE", addresses={} ) server_dict = meta.obj_to_munch(server) ips = ['203.0.113.29', '172.24.4.229'] - mock_nova_client.servers.get.return_value = server self.cloud.add_ips_to_server(server_dict, ips=ips) @@ -204,16 +197,14 @@ def test_add_ips_to_server_ip_list( server_dict, ips, wait=False, timeout=60, fixed_address=None) @patch.object(OpenStackCloud, '_needs_floating_ip') - @patch.object(OpenStackCloud, 'nova_client') @patch.object(OpenStackCloud, '_add_auto_ip') def test_add_ips_to_server_auto_ip( - self, mock_add_auto_ip, mock_nova_client, mock_needs_floating_ip): + self, mock_add_auto_ip, mock_needs_floating_ip): server = FakeServer( id='server-id', name='test-server', status="ACTIVE", addresses={} ) server_dict = meta.obj_to_munch(server) - mock_nova_client.servers.get.return_value = server # TODO(mordred) REMOVE THIS MOCK WHEN THE NEXT PATCH LANDS # SERIOUSLY THIS TIME. NEXT PATCH - WHICH SHOULD ADD MOCKS FOR # list_ports AND list_networks AND list_subnets. BUT THAT WOULD From 83c8bf5a3439c8bf732604ca0de9098af312d6b0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 15 Jun 2017 21:50:52 -0500 Subject: [PATCH 1630/3836] Convert list servers tests to requests_mock Also, found a few others that had been missed before. Change-Id: I9dfbabd2b5318a59b91edf4d8a62ccc0ccb9baa0 --- shade/tests/unit/test_caching.py | 15 ++- shade/tests/unit/test_image_snapshot.py | 17 +-- .../tests/unit/test_server_delete_metadata.py | 66 +++++++--- shade/tests/unit/test_server_set_metadata.py | 69 +++++++--- shade/tests/unit/test_shade.py | 122 ++++++++---------- 5 files changed, 163 insertions(+), 126 deletions(-) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index d4bcaffad..72acd8300 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -178,18 +178,23 @@ def test_list_projects_v2(self): self.cloud.list_projects()) self.assert_calls() - @mock.patch('shade.OpenStackCloud.nova_client') - def test_list_servers_no_herd(self, nova_mock): + def test_list_servers_no_herd(self): self.cloud._SERVER_AGE = 2 - fake_server = fakes.FakeServer('1234', '', 'ACTIVE') - nova_mock.servers.list.return_value = [fake_server] + fake_server = fakes.make_fake_server('1234', 'name') + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [fake_server]}), + ]) with concurrent.futures.ThreadPoolExecutor(16) as pool: for i in range(16): pool.submit(lambda: self.cloud.list_servers(bare=True)) # It's possible to race-condition 16 threads all in the # single initial lock without a tiny sleep time.sleep(0.001) - self.assertEqual(1, nova_mock.servers.list.call_count) + + self.assert_calls() def test_list_volumes(self): fake_volume = fakes.FakeVolume('volume1', 'available', diff --git a/shade/tests/unit/test_image_snapshot.py b/shade/tests/unit/test_image_snapshot.py index 860cb19e1..1e3a854fa 100644 --- a/shade/tests/unit/test_image_snapshot.py +++ b/shade/tests/unit/test_image_snapshot.py @@ -25,6 +25,9 @@ def setUp(self): super(TestImageSnapshot, self).setUp() self.server_id = str(uuid.uuid4()) self.image_id = str(uuid.uuid4()) + self.server_name = self.getUniqueString('name') + self.fake_server = fakes.make_fake_server( + self.server_id, self.server_name) def test_create_image_snapshot_wait_until_active_never_active(self): snapshot_name = 'test-snapshot' @@ -97,17 +100,3 @@ def test_create_image_snapshot_wait_active(self): self.assertEqual(image['id'], self.image_id) self.assert_calls() - - def test_create_image_snapshot_bad_name_exception(self): - self.register_uris([ - dict( - method='POST', - uri='{endpoint}/servers/{server_id}/action'.format( - endpoint=fakes.COMPUTE_ENDPOINT, - server_id=self.server_id), - json=dict(servers=[])), - ]) - self.assertRaises( - exc.OpenStackCloudException, - self.cloud.create_image_snapshot, - 'test-snapshot', self.server_id) diff --git a/shade/tests/unit/test_server_delete_metadata.py b/shade/tests/unit/test_server_delete_metadata.py index b34cf507f..29fea9a8a 100644 --- a/shade/tests/unit/test_server_delete_metadata.py +++ b/shade/tests/unit/test_server_delete_metadata.py @@ -17,35 +17,59 @@ Tests for the `delete_server_metadata` command. """ -import mock +import uuid -from shade import OpenStackCloud -from shade.exc import OpenStackCloudException +from shade.exc import OpenStackCloudURINotFound +from shade.tests import fakes from shade.tests.unit import base -class TestServerDeleteMetadata(base.TestCase): +class TestServerDeleteMetadata(base.RequestsMockTestCase): - @mock.patch.object(OpenStackCloud, 'nova_client') - def test_server_delete_metadata_with_exception(self, mock_nova): + def setUp(self): + super(TestServerDeleteMetadata, self).setUp() + self.server_id = str(uuid.uuid4()) + self.server_name = self.getUniqueString('name') + self.fake_server = fakes.make_fake_server( + self.server_id, self.server_name) + + def test_server_delete_metadata_with_exception(self): """ - Test that a generic exception in the novaclient delete_meta raises - an exception in delete_server_metadata. + Test that a missing metadata throws an exception. """ - mock_nova.servers.delete_meta.side_effect = Exception("exception") + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [self.fake_server]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', self.fake_server['id'], + 'metadata', 'key']), + status_code=404), + ]) self.assertRaises( - OpenStackCloudException, self.cloud.delete_server_metadata, - {'id': 'server-id'}, ['key']) + OpenStackCloudURINotFound, self.cloud.delete_server_metadata, + self.server_name, ['key']) - @mock.patch.object(OpenStackCloud, 'nova_client') - def test_server_delete_metadata_with_exception_reraise(self, mock_nova): - """ - Test that an OpenStackCloudException exception gets re-raised - in delete_server_metadata. - """ - mock_nova.servers.delete_meta.side_effect = OpenStackCloudException("") + self.assert_calls() - self.assertRaises( - OpenStackCloudException, self.cloud.delete_server_metadata, - 'server-id', ['key']) + def test_server_delete_metadata(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [self.fake_server]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', self.fake_server['id'], + 'metadata', 'key']), + status_code=200), + ]) + + self.cloud.delete_server_metadata(self.server_id, ['key']) + + self.assert_calls() diff --git a/shade/tests/unit/test_server_set_metadata.py b/shade/tests/unit/test_server_set_metadata.py index a892cddaf..2af4787af 100644 --- a/shade/tests/unit/test_server_set_metadata.py +++ b/shade/tests/unit/test_server_set_metadata.py @@ -17,35 +17,62 @@ Tests for the `set_server_metadata` command. """ -import mock +import uuid -from shade import OpenStackCloud -from shade.exc import OpenStackCloudException +from shade.exc import OpenStackCloudBadRequest +from shade.tests import fakes from shade.tests.unit import base -class TestServerSetMetadata(base.TestCase): +class TestServerSetMetadata(base.RequestsMockTestCase): - @mock.patch.object(OpenStackCloud, 'nova_client') - def test_server_set_metadata_with_set_meta_exception(self, mock_nova): + def setUp(self): + super(TestServerSetMetadata, self).setUp() + self.server_id = str(uuid.uuid4()) + self.server_name = self.getUniqueString('name') + self.fake_server = fakes.make_fake_server( + self.server_id, self.server_name) + + def test_server_set_metadata_with_exception(self): """ - Test that a generic exception in the novaclient set_meta raises - an exception in set_server_metadata. + Test that a generic exception in the novaclient delete_meta raises + an exception in delete_server_metadata. """ - mock_nova.servers.set_meta.side_effect = Exception("exception") + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [self.fake_server]}), + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', self.fake_server['id'], + 'metadata']), + validate=dict(json={'metadata': {'meta': 'data'}}), + json={}, + status_code=400), + ]) self.assertRaises( - OpenStackCloudException, self.cloud.set_server_metadata, - {'id': 'server-id'}, {'meta': 'data'}) + OpenStackCloudBadRequest, self.cloud.set_server_metadata, + self.server_name, {'meta': 'data'}) - @mock.patch.object(OpenStackCloud, 'nova_client') - def test_server_set_metadata_with_exception_reraise(self, mock_nova): - """ - Test that an OpenStackCloudException exception gets re-raised - in set_server_metadata. - """ - mock_nova.servers.set_meta.side_effect = OpenStackCloudException("") + self.assert_calls() - self.assertRaises( - OpenStackCloudException, self.cloud.set_server_metadata, - 'server-id', {'meta': 'data'}) + def test_server_set_metadata(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [self.fake_server]}), + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', self.fake_server['id'], 'metadata']), + validate=dict(json={'metadata': {'meta': 'data'}}), + status_code=200), + ]) + + self.cloud.set_server_metadata(self.server_id, {'meta': 'data'}) + + self.assert_calls() diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 2d5941aa2..73b51f163 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -11,7 +11,7 @@ # under the License. import mock -import munch +import uuid import testtools @@ -68,32 +68,49 @@ def test_get_image_not_found(self, mock_search): r = self.cloud.get_image('doesNotExist') self.assertIsNone(r) - @mock.patch.object(shade.OpenStackCloud, '_expand_server') - @mock.patch.object(shade.OpenStackCloud, 'list_servers') - def test_get_server(self, mock_list, mock_expand): - server1 = dict(id='123', name='mickey') - server2 = dict(id='345', name='mouse') + def test_get_server(self): + server1 = fakes.make_fake_server('123', 'mickey') + server2 = fakes.make_fake_server('345', 'mouse') + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [server1, server2]}), + ]) - def expand_server(server, detailed, bare): - return server - mock_expand.side_effect = expand_server - mock_list.return_value = [server1, server2] r = self.cloud.get_server('mickey') self.assertIsNotNone(r) - self.assertDictEqual(server1, r) + self.assertEqual(server1['name'], r['name']) + + self.assert_calls() + + def test_get_server_not_found(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': []}), + ]) - @mock.patch.object(shade.OpenStackCloud, 'search_servers') - def test_get_server_not_found(self, mock_search): - mock_search.return_value = [] r = self.cloud.get_server('doesNotExist') self.assertIsNone(r) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_list_servers_exception(self, mock_client): - mock_client.servers.list.side_effect = Exception() + self.assert_calls() + + def test_list_servers_exception(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + status_code=400) + ]) + self.assertRaises(exc.OpenStackCloudException, self.cloud.list_servers) + self.assert_calls() + def test__neutron_exceptions_resource_not_found(self): self.register_uris([ dict(method='GET', @@ -116,64 +133,39 @@ def test__neutron_exceptions_url_not_found(self): self.cloud.list_networks) self.assert_calls() - @mock.patch.object(shade._tasks.ServerList, 'main') - @mock.patch('shade.meta.add_server_interfaces') - def test_list_servers(self, mock_add_srv_int, mock_serverlist): - '''This test verifies that calling list_servers results in a call - to the ServerList task.''' - server_obj = munch.Munch({'name': 'testserver', - 'id': '1', - 'flavor': {}, - 'addresses': {}, - 'accessIPv4': '', - 'accessIPv6': '', - 'image': ''}) - mock_serverlist.return_value = [server_obj] - mock_add_srv_int.side_effect = [server_obj] + def test_list_servers(self): + server_id = str(uuid.uuid4()) + server_name = self.getUniqueString('name') + fake_server = fakes.make_fake_server(server_id, server_name) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [fake_server]}), + ]) r = self.cloud.list_servers() self.assertEqual(1, len(r)) - self.assertEqual(1, mock_add_srv_int.call_count) - self.assertEqual('testserver', r[0]['name']) - - @mock.patch.object(shade._tasks.ServerList, 'main') - @mock.patch('shade.meta.get_hostvars_from_server') - def test_list_servers_detailed(self, - mock_get_hostvars_from_server, - mock_serverlist): - '''This test verifies that when list_servers is called with - `detailed=True` that it calls `get_hostvars_from_server` for each - server in the list.''' - mock_serverlist.return_value = [ - fakes.FakeServer('server1', '', 'ACTIVE'), - fakes.FakeServer('server2', '', 'ACTIVE'), - ] - mock_get_hostvars_from_server.side_effect = [ - {'name': 'server1', 'id': '1'}, - {'name': 'server2', 'id': '2'}, - ] - - r = self.cloud.list_servers(detailed=True) + self.assertEqual(server_name, r[0]['name']) - self.assertEqual(2, len(r)) - self.assertEqual(len(r), mock_get_hostvars_from_server.call_count) - self.assertEqual('server1', r[0]['name']) - self.assertEqual('server2', r[1]['name']) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_list_servers_all_projects(self, mock_nova_client): + def test_list_servers_all_projects(self): '''This test verifies that when list_servers is called with - `all_projects=True` that it passes `all_tenants=1` to novaclient.''' - mock_nova_client.servers.list.return_value = [ - fakes.FakeServer('server1', '', 'ACTIVE'), - fakes.FakeServer('server2', '', 'ACTIVE'), - ] + `all_projects=True` that it passes `all_tenants=True` to nova.''' + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'], + qs_elements=['all_tenants=True']), + complete_qs=True, + json={'servers': []}), + ]) self.cloud.list_servers(all_projects=True) - mock_nova_client.servers.list.assert_called_with( - search_opts={'all_tenants': True}) + self.assert_calls() def test_iterate_timeout_bad_wait(self): with testtools.ExpectedException( From 70365c9f65cc04b711f96a418aac3ae23dc2fbec Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 15 Jun 2017 22:41:08 -0500 Subject: [PATCH 1631/3836] Convert list_servers to REST Change-Id: Id3e60a9df87c3ca64fcabcb5c6d30b2b0bcb0b24 --- shade/_tasks.py | 5 ----- shade/openstackcloud.py | 29 +++++++++++++++-------------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index b82cec123..4bb761676 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -47,11 +47,6 @@ def main(self, client): return client.keystone_client.users.remove_from_group(**self.args) -class ServerList(task_manager.Task): - def main(self, client): - return client.nova_client.servers.list(**self.args) - - class ServerGroupList(task_manager.Task): def main(self, client): return client.nova_client.server_groups.list(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 6a9dcb642..63d8a0c4e 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2011,20 +2011,21 @@ def list_servers(self, detailed=False, all_projects=False, bare=False): return self._servers def _list_servers(self, detailed=False, all_projects=False, bare=False): - with _utils.shade_exceptions( - "Error fetching server list on {cloud}:{region}:".format( - cloud=self.name, - region=self.region_name)): - kwargs = {} - if all_projects: - kwargs['search_opts'] = {'all_tenants': True} - servers = self._normalize_servers( - self.manager.submit_task(_tasks.ServerList(**kwargs))) - - return [ - self._expand_server(server, detailed, bare) - for server in servers - ] + error_msg = "Error fetching server list on {cloud}:{region}:".format( + cloud=self.name, + region=self.region_name) + + params = {} + if all_projects: + params['all_tenants'] = True + servers = self._normalize_servers( + self._compute_client.get( + '/servers/detail', params=params, error_message=error_msg)) + + return [ + self._expand_server(server, detailed, bare) + for server in servers + ] def list_server_groups(self): """List all available server groups. From 587b41b0f6612c147fad93966369836b3e953a18 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 16 Jun 2017 18:10:34 -0500 Subject: [PATCH 1632/3836] Convert keypairs tests to requests_mock Change-Id: I85a9d19959e750995fed20a882eb8a9cae9add64 --- shade/tests/fakes.py | 11 +++ shade/tests/unit/test_keypair.py | 129 +++++++++++++++++++++---------- 2 files changed, 98 insertions(+), 42 deletions(-) diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index c79f784fe..7967df1dd 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -130,6 +130,17 @@ def make_fake_server(server_id, name, status='ACTIVE'): "config_drive": "True"} +def make_fake_keypair(name): + # Note: this is literally taken from: + # https://developer.openstack.org/api-ref/compute/ + return { + "fingerprint": "7e:eb:ab:24:ba:d1:e1:88:ae:9a:fb:66:53:df:d3:bd", + "name": name, + "type": "ssh", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCkF3MX59OrlBs3dH5CU7lNmvpbrgZxSpyGjlnE8Flkirnc/Up22lpjznoxqeoTAwTW034k7Dz6aYIrZGmQwe2TkE084yqvlj45Dkyoj95fW/sZacm0cZNuL69EObEGHdprfGJQajrpz22NQoCD8TFB8Wv+8om9NH9Le6s+WPe98WC77KLw8qgfQsbIey+JawPWl4O67ZdL5xrypuRjfIPWjgy/VH85IXg/Z/GONZ2nxHgSShMkwqSFECAC5L3PHB+0+/12M/iikdatFSVGjpuHvkLOs3oe7m6HlOfluSJ85BzLWBbvva93qkGmLg4ZAc8rPh2O+YIsBUHNLLMM/oQp Generated-by-Nova\n", # flake8: noqa + } + + def make_fake_stack(id, name, description=None, status='CREATE_COMPLETE'): return { 'creation_time': '2017-03-23T23:57:12Z', diff --git a/shade/tests/unit/test_keypair.py b/shade/tests/unit/test_keypair.py index 8094d7d56..8eb87d354 100644 --- a/shade/tests/unit/test_keypair.py +++ b/shade/tests/unit/test_keypair.py @@ -12,57 +12,102 @@ # limitations under the License. -import shade - -from mock import patch -from novaclient import exceptions as nova_exc - from shade import exc -from shade import meta from shade.tests import fakes from shade.tests.unit import base -class TestKeypair(base.TestCase): +class TestKeypair(base.RequestsMockTestCase): - @patch.object(shade.OpenStackCloud, 'nova_client') - def test_create_keypair(self, mock_nova): - keyname = 'my_keyname' - pub_key = 'ssh-rsa BLAH' - key = fakes.FakeKeypair('keyid', keyname, pub_key) - mock_nova.keypairs.create.return_value = key + def setUp(self): + super(TestKeypair, self).setUp() + self.keyname = self.getUniqueString('key') + self.key = fakes.make_fake_keypair(self.keyname) - new_key = self.cloud.create_keypair(keyname, pub_key) - mock_nova.keypairs.create.assert_called_once_with( - name=keyname, public_key=pub_key - ) - self.assertEqual(meta.obj_to_munch(key), new_key) + def test_create_keypair(self): + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['os-keypairs']), + json={'keypair': self.key}, + validate=dict(json={ + 'keypair': { + 'name': self.key['name'], + 'public_key': self.key['public_key']}})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', + append=['os-keypairs', self.keyname]), + json={'keypair': self.key}), + ]) - @patch.object(shade.OpenStackCloud, 'nova_client') - def test_create_keypair_exception(self, mock_nova): - mock_nova.keypairs.create.side_effect = Exception() - self.assertRaises(exc.OpenStackCloudException, - self.cloud.create_keypair, '', '') - - @patch.object(shade.OpenStackCloud, 'nova_client') - def test_delete_keypair(self, mock_nova): - self.assertTrue(self.cloud.delete_keypair('mykey')) - mock_nova.keypairs.delete.assert_called_once_with( - key='mykey' - ) - - @patch.object(shade.OpenStackCloud, 'nova_client') - def test_delete_keypair_not_found(self, mock_nova): - mock_nova.keypairs.delete.side_effect = nova_exc.NotFound('') - self.assertFalse(self.cloud.delete_keypair('invalid')) - - @patch.object(shade.OpenStackCloud, 'nova_client') - def test_list_keypairs(self, mock_nova): + new_key = self.cloud.create_keypair( + self.keyname, self.key['public_key']) + self.assertEqual(new_key['name'], self.keyname) + + self.assert_calls() + + def test_create_keypair_exception(self): + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['os-keypairs']), + status_code=400, + validate=dict(json={ + 'keypair': { + 'name': self.key['name'], + 'public_key': self.key['public_key']}})), + ]) + + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.create_keypair, + self.keyname, self.key['public_key']) + + self.assert_calls() + + def test_delete_keypair(self): + self.register_uris([ + dict(method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', + append=['os-keypairs', self.keyname]), + status_code=202), + ]) + self.assertTrue(self.cloud.delete_keypair(self.keyname)) + + self.assert_calls() + + def test_delete_keypair_not_found(self): + self.register_uris([ + dict(method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', + append=['os-keypairs', self.keyname]), + status_code=404), + ]) + self.assertFalse(self.cloud.delete_keypair(self.keyname)) + + self.assert_calls() + + def test_list_keypairs(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-keypairs']), + json={'keypairs': [self.key]}), + + ]) self.cloud.list_keypairs() - mock_nova.keypairs.list.assert_called_once_with() - @patch.object(shade.OpenStackCloud, 'nova_client') - def test_list_keypairs_exception(self, mock_nova): - mock_nova.keypairs.list.side_effect = Exception() + def test_list_keypairs_exception(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-keypairs']), + status_code=400), + + ]) self.assertRaises(exc.OpenStackCloudException, self.cloud.list_keypairs) + self.assert_calls() From 145a0ab7a3662615a9b5df5cad28b92dfb30884a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 20 Apr 2017 10:29:53 -0500 Subject: [PATCH 1633/3836] Add text about microversions We aren't doing anything with microversions yet, but since we just wrote down algorithms for version discovery, let's go ahead and talk about how microversions should work. Also, mention that it's important to fetch information about them as part of discovery. Change-Id: Iadd48cd53488240e33db83797a88af689b1497dc --- doc/source/index.rst | 1 + doc/source/microversions.rst | 75 ++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 doc/source/microversions.rst diff --git a/doc/source/index.rst b/doc/source/index.rst index 29f7a7253..1fa66381c 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -17,6 +17,7 @@ Contents: model contributing coding + microversions future releasenotes diff --git a/doc/source/microversions.rst b/doc/source/microversions.rst new file mode 100644 index 000000000..8e4142a90 --- /dev/null +++ b/doc/source/microversions.rst @@ -0,0 +1,75 @@ +============= +Microversions +============= + +As shade rolls out support for consuming microversions, it will do so on a +call by call basis as needed. Just like with major versions, shade should have +logic to handle each microversion for a given REST call it makes, with the +following rules in mind: + +* If an activity shade performs can be done differently or more efficiently + with a new microversion, the support should be added to shade. + +* shade should always attempt to use the latest microversion it is aware of + for a given call, unless a microversion removes important data. + +* Microversion selection should under no circumstances be exposed to the user, + except in the case of missing feature error messages. + +* If a feature is only exposed for a given microversion and cannot be simulated + for older clouds without that microversion, it is ok to add it to shade but + a clear error message should be given to the user that the given feature is + not available on their cloud. (A message such as "This cloud only supports + a maximum microversion of XXX for service YYY and this feature only exists + on clouds with microversion ZZZ. Please contact your cloud provider for + information about when this feature might be available") + +* When adding a feature to shade that only exists behind a new microversion, + every effort should be made to figure out how to provide the same + functionality if at all possible, even if doing so is inefficient. If an + inefficient workaround is employed, a warning should be provided to the + user. (the user's workaround to skip the inefficient behavior would be to + stop using that shade API call) + +* If shade is aware of logic for more than one microversion, it should always + attempt to use the latest version available for the service for that call. + +* Objects returned from shade should always go through normalization and thus + should always conform to shade's documented data model and should never look + different to the shade user regardless of the microversion used for the REST + call. + +* If a microversion adds new fields to an object, those fields should be + added to shade's data model contract for that object and the data should + either be filled in by performing additional REST calls if the data is + available that way, or the field should have a default value of None which + the user can be expected to test for when attempting to use the new value. + +* If a microversion removes fields from an object that are part of shade's + existing data model contract, care should be taken to not use the new + microversion for that call unless forced to by lack of availablity of the + old microversion on the cloud in question. In the case where an old + microversion is no longer available, care must be taken to either find the + data from another source and fill it in, or to put a value of None into the + field and document for the user that on some clouds the value may not exist. + +* If a microversion removes a field and the outcome is particularly intractable + and impossible to work around without fundamentally breaking shade's users, + an issue should be raised with the service team in question. Hopefully a + resolution can be found during the period while clouds still have the old + microversion. + +* As new calls or objects are added to shade, it is important to check in with + the service team in question on the expected stability of the object. If + there are known changes expected in the future, even if they may be a few + years off, shade should take care to not add committments to its data model + for those fields/features. It is ok for shade to not have something. + + ..note:: + shade does not currently have any sort of "experimental" opt-in API that + would allow a shade to expose things to a user that may not be supportable + under shade's normal compatibility contract. If a conflict arises in the + future where there is a strong desire for a feature but also a lack of + certainty about its stability over time, an experimental API may want to + be explored ... but concrete use cases should arise before such a thing + is started. From c23611a093290b0f8882a4995cb0ab04de13888b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 20 Apr 2017 10:33:14 -0500 Subject: [PATCH 1634/3836] Remove future document This is a write up of an idea from several years ago which is both long in the tooth and honestly never going to happen. Go ahead and remove it. Change-Id: I4377cfb24c468f2333ac470a2a58fa97cf35d4bd --- doc/source/future.rst | 176 ------------------------------------------ doc/source/index.rst | 1 - 2 files changed, 177 deletions(-) delete mode 100644 doc/source/future.rst diff --git a/doc/source/future.rst b/doc/source/future.rst deleted file mode 100644 index 61c1a4af8..000000000 --- a/doc/source/future.rst +++ /dev/null @@ -1,176 +0,0 @@ -************************ -Future Design Discussion -************************ - -This document discusses a new approach to the Shade library and how -we might wish for it to operate in a future, not-yet-developed version. -It presents a more object oriented approach, and design decisions that -we have learned and decided on while working on the current version. - -Object Design -============= - -Shade is a library for managing resources, not for operating APIs. As such, -it is the resource in question that is the primary object and not the service -that may or may not provide that resource, much as we may feel warm and fuzzy -to one of the services. - -Every resource at minimum has CRUD functions. Additionally, every resource -action should have a "do this task blocking" or "request that the cloud start -this action and give me a way to check its status" The creation and deletion -of Resources will be handled by a ResourceManager that is attached to the Cloud -:: - - class Cloud: - ResourceManager server - servers = server - ResourceManager floating_ip - floating_ips = floating_ip - ResourceManager image - images = image - ResourceManager role - roles = role - ResourceManager volume - volumes = volume - -getting, listing and searching ------------------------------- - -In addition to creating a resource, there are different ways of getting your -hands on a resource. A `get`, a `list` and a `search`. - -`list` has the simplest semantics - it takes no parameters and simply returns -a list of all of the resources that exist. - -`search` takes a set of parameters to match against and returns a list of -resources that match the parameters given. If no resources match, it returns -an empty list. - -`get` takes the same set of parameters that `search` takes, but will only ever -return a single matching resource or None. If multiple resources are matched, -an exception will be raised. - -:: - - class ResourceManager: - def get -> Resource - def list -> List - def search -> List - def create -> Resource - -Cloud and ResourceManager interface -=================================== - -All ResourceManagers should accept a cache object passed in to their constructor -and should additionally pass that cache object to all Resource constructors. -The top-level cloud should create the cache object, then pass it to each of -the ResourceManagers when it creates them. - -Client connection objects should exist and be managed at the Cloud level. A -backreference to the OpenStack cloud should be passed to every resource manager -so that ResourceManagers can get hold of the ones they need. For instance, -an Image ResourceManager would potentially need access to both the glance_client -and the swift_client. - -:: - - class ResourceManager - def __init__(self, cache, cloud) - class ServerManager(ResourceManager) - class OpenStackCloud - def __init__(self): - self.cache = dogpile.cache() - self.server = ServerManager(self.cache, self) - self.servers = self.server - -Any resources that have an association action - such as servers and -floating_ips, should carry reciprocal methods on each resource with absolutely -no difference in behavior. - -:: - - class Server(Resource): - def connect_floating_ip: - class FloatingIp(Resource): - def connect_server: - -Resource objects should have all of the accessor methods you'd expect, as well -as any other interesting rollup methods or actions. For instance, since -a keystone User can be enabled or disabled, one should expect that there -would be an enable() and a disable() method, and that those methods will -immediately operate the necessary REST apis. However, if you need to make 80 -changes to a Resource, 80 REST calls may or may not be silly, so there should -also be a generic update() method which can be used to request the minimal -amount of REST calls needed to update the attributes to the requested values. - -Resource objects should all have a to_dict method which will return a plain -flat dictionary of their attributes. - -:: - - class Resource: - def update(**new_values) -> Resource - def delete -> None, throws on error - -Readiness ---------- - -`create`, `get`, and `attach` can return resources that are not yet ready. Each -method should take a `wait` and a `timeout` parameter, that will cause the -request for the resource to block until it is ready. However, the user may -want to poll themselves. Each resource should have an `is_ready` method which -will return True when the resource is ready. The `wait` method then can -actually be implemented in the base Resource class as an iterate timeout -loop around calls to `is_ready`. Every Resource should also have an -`is_failed` and an `is_deleted` method. - -Optional Behavior ------------------ - -Not all clouds expose all features. For instance, some clouds do not have -floating ips. Additionally, some clouds may have the feature but the user -account does not, which is effectively the same thing. -This should be handled in several ways: - -If the user explicitly requests a resource that they do not have access to, -an error should be raised. For instance, if a user tries to create a floating -ip on a cloud that does not expose that feature to them, shade should throw -a "Your cloud does not let you do that" error. - -If the resource concept can be can be serviced by multiple possible services, -shade should transparently try all of them. The discovery method should use -the dogpile.cache mechanism so that it can be avoided on subsequent tries. For -instance, if the user says "please upload this image", shade should figure -out which sequence of actions need to be performed and should get the job done. - -If the resource isn't present on some clouds, but the overall concept the -resource represents is, a different resource should present the concept. For -instance, while some clouds do not have floating ips, if what the user wants -is "a server with an IP" - then the fact that one needs to request a floating -ip on some clouds is a detail, and the right thing for that to be is a quality -of a server and managed by the server resource. A floating ip resource should -really only be directly manipulated by the user if they were doing something -very floating-ip specific, such as moving a floating ip from one server to -another. - -In short, it should be considered a MASSIVE bug in shade if the shade user -ever has to have in their own code "if cloud.has_capability("X") do_thing -else do_other_thing" - since that construct conveys some resource that shade -should really be able to model. - -Functional Interface -==================== - -shade should also provide a functional mapping to the object interface that -does not expose the object interface at all. For instance, for a resource type -`server`, one could expect the following. - -:: - - class OpenStackCloud: - def create_server - return self.server.create().to_dict() - def get_server - return self.server.get().to_dict() - def update_server - return self.server.get().update().to_dict() diff --git a/doc/source/index.rst b/doc/source/index.rst index 1fa66381c..be106d6e2 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -18,7 +18,6 @@ Contents: contributing coding microversions - future releasenotes .. include:: ../../README.rst From caa69b4117ebe5f75d45adc805f67035d1d55855 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 17 Jun 2017 08:46:23 -0500 Subject: [PATCH 1635/3836] Add normalization and functional tests for keypairs Keypairs didn't have functional tests, although they do have ansible tests. Also, there was no normalization. Add both. Change-Id: Ib6fab25cf4c88e5f9d224e831a8b5f297b263aea --- doc/source/model.rst | 20 ++++++++ shade/_normalize.py | 49 +++++++++++++++++++ shade/openstackcloud.py | 10 ++-- shade/tests/fakes.py | 5 +- shade/tests/functional/test_keypairs.py | 62 +++++++++++++++++++++++++ shade/tests/unit/test_keypair.py | 8 ++-- 6 files changed, 146 insertions(+), 8 deletions(-) create mode 100644 shade/tests/functional/test_keypairs.py diff --git a/doc/source/model.rst b/doc/source/model.rst index 70818f586..3293b0306 100644 --- a/doc/source/model.rst +++ b/doc/source/model.rst @@ -130,6 +130,26 @@ A Glance Image. tags=list(), properties=dict()) + +Keypair +------- + +A keypair for a Nova Server. + +.. code-block:: python + + Keypair = dict( + location=Location(), + name=str(), + id=str(), + public_key=str(), + fingerprint=str(), + type=str(), + user_id=str(), + private_key=str() or None + properties=dict()) + + Security Group -------------- diff --git a/shade/_normalize.py b/shade/_normalize.py index b7630f8ed..825a20d25 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -12,6 +12,7 @@ # 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 datetime import munch import six @@ -44,6 +45,21 @@ 'user_id', ) +_KEYPAIR_FIELDS = ( + 'fingerprint', + 'name', + 'private_key', + 'public_key', + 'user_id', +) + +_KEYPAIR_USELESS_FIELDS = ( + 'deleted', + 'deleted_at', + 'id', + 'updated_at', +) + _COMPUTE_LIMITS_FIELDS = ( ('maxPersonality', 'max_personality'), ('maxPersonalitySize', 'max_personality_size'), @@ -203,6 +219,39 @@ def _normalize_flavor(self, flavor): return new_flavor + def _normalize_keypairs(self, keypairs): + """Normalize Nova Keypairs""" + ret = [] + for keypair in keypairs: + ret.append(self._normalize_keypair(keypair)) + return ret + + def _normalize_keypair(self, keypair): + """Normalize Ironic Machine""" + + new_keypair = munch.Munch() + keypair = keypair.copy() + + # Discard noise + self._remove_novaclient_artifacts(keypair) + + new_keypair['location'] = self.current_location + for key in _KEYPAIR_FIELDS: + new_keypair[key] = keypair.pop(key, None) + # These are completely meaningless fields + for key in _KEYPAIR_USELESS_FIELDS: + keypair.pop(key, None) + new_keypair['type'] = keypair.pop('type', 'ssh') + # created_at isn't returned from the keypair creation. (what?) + new_keypair['created_at'] = keypair.pop( + 'created_at', datetime.datetime.now().isoformat()) + # Don't even get me started on this + new_keypair['id'] = new_keypair['name'] + + new_keypair['properties'] = keypair.copy() + + return new_keypair + def _normalize_images(self, images): ret = [] for image in images: diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 63d8a0c4e..d7563971c 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1613,7 +1613,8 @@ def list_keypairs(self): """ with _utils.shade_exceptions("Error fetching keypair list"): - return self.manager.submit_task(_tasks.KeypairList()) + return self._normalize_keypairs( + self.manager.submit_task(_tasks.KeypairList())) def list_networks(self, filters=None): """List all available networks. @@ -2959,7 +2960,7 @@ def _search_one_stack(name_or_id=None, filters=None): return _utils._get_entity( _search_one_stack, name_or_id, filters) - def create_keypair(self, name, public_key): + def create_keypair(self, name, public_key=None): """Create a new keypair. :param name: Name of the keypair being created. @@ -2969,8 +2970,9 @@ def create_keypair(self, name, public_key): """ with _utils.shade_exceptions("Unable to create keypair {name}".format( name=name)): - return self.manager.submit_task(_tasks.KeypairCreate( - name=name, public_key=public_key)) + return self._normalize_keypair( + self.manager.submit_task(_tasks.KeypairCreate( + name=name, public_key=public_key))) def delete_keypair(self, name): """Delete a keypair. diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 7967df1dd..d6ab32d50 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -17,6 +17,7 @@ Fakes used for testing """ +import datetime import uuid from shade._heat import template_format @@ -31,6 +32,7 @@ p=PROJECT_ID) NO_MD5 = '93b885adfe0da089cdf634904fd59f71' NO_SHA256 = '6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d' +FAKE_PUBLIC_KEY = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCkF3MX59OrlBs3dH5CU7lNmvpbrgZxSpyGjlnE8Flkirnc/Up22lpjznoxqeoTAwTW034k7Dz6aYIrZGmQwe2TkE084yqvlj45Dkyoj95fW/sZacm0cZNuL69EObEGHdprfGJQajrpz22NQoCD8TFB8Wv+8om9NH9Le6s+WPe98WC77KLw8qgfQsbIey+JawPWl4O67ZdL5xrypuRjfIPWjgy/VH85IXg/Z/GONZ2nxHgSShMkwqSFECAC5L3PHB+0+/12M/iikdatFSVGjpuHvkLOs3oe7m6HlOfluSJ85BzLWBbvva93qkGmLg4ZAc8rPh2O+YIsBUHNLLMM/oQp Generated-by-Nova\n" # flake8: noqa def make_fake_flavor(flavor_id, name, ram=100, disk=1600, vcpus=24): @@ -137,7 +139,8 @@ def make_fake_keypair(name): "fingerprint": "7e:eb:ab:24:ba:d1:e1:88:ae:9a:fb:66:53:df:d3:bd", "name": name, "type": "ssh", - "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCkF3MX59OrlBs3dH5CU7lNmvpbrgZxSpyGjlnE8Flkirnc/Up22lpjznoxqeoTAwTW034k7Dz6aYIrZGmQwe2TkE084yqvlj45Dkyoj95fW/sZacm0cZNuL69EObEGHdprfGJQajrpz22NQoCD8TFB8Wv+8om9NH9Le6s+WPe98WC77KLw8qgfQsbIey+JawPWl4O67ZdL5xrypuRjfIPWjgy/VH85IXg/Z/GONZ2nxHgSShMkwqSFECAC5L3PHB+0+/12M/iikdatFSVGjpuHvkLOs3oe7m6HlOfluSJ85BzLWBbvva93qkGmLg4ZAc8rPh2O+YIsBUHNLLMM/oQp Generated-by-Nova\n", # flake8: noqa + "public_key": FAKE_PUBLIC_KEY, + "created_at": datetime.datetime.now().isoformat(), } diff --git a/shade/tests/functional/test_keypairs.py b/shade/tests/functional/test_keypairs.py new file mode 100644 index 000000000..35a159e1c --- /dev/null +++ b/shade/tests/functional/test_keypairs.py @@ -0,0 +1,62 @@ +# 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_keypairs +---------------------------------- + +Functional tests for `shade` keypairs methods +""" +from shade.tests import fakes +from shade.tests.functional import base + + +class TestKeypairs(base.BaseFunctionalTestCase): + + def test_create_and_delete(self): + '''Test creating and deleting keypairs functionality''' + name = self.getUniqueString('keypair') + self.addCleanup(self.user_cloud.delete_keypair, name) + keypair = self.user_cloud.create_keypair(name=name) + self.assertEqual(keypair['name'], name) + self.assertIsNotNone(keypair['public_key']) + self.assertIsNotNone(keypair['private_key']) + self.assertIsNotNone(keypair['fingerprint']) + self.assertEqual(keypair['type'], 'ssh') + + keypairs = self.user_cloud.list_keypairs() + self.assertIn(name, [k['name'] for k in keypairs]) + + self.user_cloud.delete_keypair(name) + + keypairs = self.user_cloud.list_keypairs() + self.assertNotIn(name, [k['name'] for k in keypairs]) + + def test_create_and_delete_with_key(self): + '''Test creating and deleting keypairs functionality''' + name = self.getUniqueString('keypair') + self.addCleanup(self.user_cloud.delete_keypair, name) + keypair = self.user_cloud.create_keypair( + name=name, public_key=fakes.FAKE_PUBLIC_KEY) + self.assertEqual(keypair['name'], name) + self.assertIsNotNone(keypair['public_key']) + self.assertIsNone(keypair['private_key']) + self.assertIsNotNone(keypair['fingerprint']) + self.assertEqual(keypair['type'], 'ssh') + + keypairs = self.user_cloud.list_keypairs() + self.assertIn(name, [k['name'] for k in keypairs]) + + self.user_cloud.delete_keypair(name) + + keypairs = self.user_cloud.list_keypairs() + self.assertNotIn(name, [k['name'] for k in keypairs]) diff --git a/shade/tests/unit/test_keypair.py b/shade/tests/unit/test_keypair.py index 8eb87d354..55c3f94f3 100644 --- a/shade/tests/unit/test_keypair.py +++ b/shade/tests/unit/test_keypair.py @@ -43,7 +43,7 @@ def test_create_keypair(self): new_key = self.cloud.create_keypair( self.keyname, self.key['public_key']) - self.assertEqual(new_key['name'], self.keyname) + self.assertEqual(new_key, self.cloud._normalize_keypair(self.key)) self.assert_calls() @@ -95,10 +95,12 @@ def test_list_keypairs(self): dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['os-keypairs']), - json={'keypairs': [self.key]}), + json={'keypairs': [{'keypair': self.key}]}), ]) - self.cloud.list_keypairs() + keypairs = self.cloud.list_keypairs() + self.assertEqual(keypairs, self.cloud._normalize_keypairs([self.key])) + self.assert_calls() def test_list_keypairs_exception(self): self.register_uris([ From b9a2c296e618d6ea07f8e91d995cc29885fe63c0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 16 Jun 2017 18:23:07 -0500 Subject: [PATCH 1636/3836] Convert keypairs calls to REST Change-Id: Iea7c8267e87c5b5beb83e9315f41a61ae714005f --- shade/_tasks.py | 15 --------------- shade/openstackcloud.py | 32 ++++++++++++++++---------------- shade/tests/unit/test_keypair.py | 5 ----- 3 files changed, 16 insertions(+), 36 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 4bb761676..1449ac021 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -107,21 +107,6 @@ def main(self, client): return client.nova_client.aggregates.set_metadata(**self.args) -class KeypairList(task_manager.Task): - def main(self, client): - return client.nova_client.keypairs.list() - - -class KeypairCreate(task_manager.Task): - def main(self, client): - return client.nova_client.keypairs.create(**self.args) - - -class KeypairDelete(task_manager.Task): - def main(self, client): - return client.nova_client.keypairs.delete(**self.args) - - class MachineCreate(task_manager.Task): def main(self, client): return client.ironic_client.node.create(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index d7563971c..277821764 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -31,7 +31,6 @@ from six.moves import urllib import keystoneauth1.exceptions -import novaclient.exceptions as nova_exceptions import shade from shade.exc import * # noqa @@ -1612,9 +1611,10 @@ def list_keypairs(self): :returns: A list of ``munch.Munch`` containing keypair info. """ - with _utils.shade_exceptions("Error fetching keypair list"): - return self._normalize_keypairs( - self.manager.submit_task(_tasks.KeypairList())) + return self._normalize_keypairs([ + k['keypair'] for k in self._compute_client.get( + '/os-keypairs', + error_message="Error fetching keypair list")]) def list_networks(self, filters=None): """List all available networks. @@ -2968,11 +2968,15 @@ def create_keypair(self, name, public_key=None): :raises: OpenStackCloudException on operation error. """ - with _utils.shade_exceptions("Unable to create keypair {name}".format( - name=name)): - return self._normalize_keypair( - self.manager.submit_task(_tasks.KeypairCreate( - name=name, public_key=public_key))) + keypair = { + 'name': name, + } + if public_key: + keypair['public_key'] = public_key + return self._normalize_keypair(self._compute_client.post( + '/os-keypairs', + json={'keypair': keypair}, + error_message="Unable to create keypair {name}".format(name=name))) def delete_keypair(self, name): """Delete a keypair. @@ -2984,15 +2988,11 @@ def delete_keypair(self, name): :raises: OpenStackCloudException on operation error. """ try: - self.manager.submit_task(_tasks.KeypairDelete(key=name)) - except nova_exceptions.NotFound: + self._compute_client.delete('/os-keypairs/{name}'.format( + name=name)) + except OpenStackCloudURINotFound: self.log.debug("Keypair %s not found for deleting", name) return False - except OpenStackCloudException: - raise - except Exception as e: - raise OpenStackCloudException( - "Unable to delete keypair %s: %s" % (name, e)) return True def create_network(self, name, shared=False, admin_state_up=True, diff --git a/shade/tests/unit/test_keypair.py b/shade/tests/unit/test_keypair.py index 55c3f94f3..2fa3e3ae1 100644 --- a/shade/tests/unit/test_keypair.py +++ b/shade/tests/unit/test_keypair.py @@ -34,11 +34,6 @@ def test_create_keypair(self): 'keypair': { 'name': self.key['name'], 'public_key': self.key['public_key']}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', - append=['os-keypairs', self.keyname]), - json={'keypair': self.key}), ]) new_key = self.cloud.create_keypair( From 7cd4ef000153c7c4b8b3a43910ccb2bc4439c6c5 Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Sun, 18 Jun 2017 00:47:12 +0000 Subject: [PATCH 1637/3836] Don't remove top-container element for user and project REST API calls Change-Id: I5d2ca9426f629014069f98ed9521556392c9d71d Signed-off-by: Rosario Di Somma --- shade/_adapter.py | 4 +++- shade/openstackcloud.py | 49 +++++++++++++++++++---------------------- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/shade/_adapter.py b/shade/_adapter.py index b7e70a328..408d09456 100644 --- a/shade/_adapter.py +++ b/shade/_adapter.py @@ -137,7 +137,9 @@ def _munch_response(self, response, result_key=None, error_message=None): 'floating_ip', 'floating_ips', 'port', 'ports', 'stack', 'stacks', 'zones', 'events', 'security_group', 'security_groups', - 'security_group_rule', 'security_group_rules']: + 'security_group_rule', 'security_group_rules', + 'users', 'user', 'projects', 'tenants', + 'project', 'tenant']: if key in result_json.keys(): self._log_request_id(response) return result_json diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 6a9dcb642..dcaa62e56 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -854,17 +854,12 @@ def list_projects(self, domain_id=None, name_or_id=None, filters=None): pushdown, filters = _normalize._split_filters(**kwargs) try: - if self.cloud_config.get_api_version('identity') == '3': - projects = self._identity_client.get('/projects', - params=pushdown) - if isinstance(projects, dict): - projects = projects['projects'] - else: - projects = self._identity_client.get('/tenants', - params=pushdown) - if isinstance(projects, dict): - projects = projects['tenants'] - projects = self._normalize_projects(projects) + api_version = self.cloud_config.get_api_version('identity') + key = 'projects' if api_version == '3' else 'tenants' + data = self._identity_client.get( + '/{endpoint}'.format(endpoint=key), params=pushdown) + projects = self._normalize_projects( + meta.get_and_munchify(key, data)) except Exception as e: self.log.debug("Failed to list projects", exc_info=True) raise OpenStackCloudException(str(e)) @@ -913,11 +908,13 @@ def update_project(self, name_or_id, enabled=True, domain_id=None, # FIXME(samueldmq): enable=True is the default, meaning it will # enable a disabled project if you simply update other fields if self.cloud_config.get_api_version('identity') == '3': - project = self._identity_client.patch( + data = self._identity_client.patch( '/projects/' + proj['id'], json={'project': kwargs}) + project = meta.get_and_munchify('project', data) else: - project = self._identity_client.post( + data = self._identity_client.post( '/tenants/' + proj['id'], json={'tenant': kwargs}) + project = meta.get_and_munchify('tenant', data) project = self._normalize_project(project) self.list_projects.invalidate(self) return project @@ -931,14 +928,14 @@ def create_project( project_ref.update({'name': name, 'description': description, 'enabled': enabled}) - + endpoint, key = ('tenants', 'tenant') if self.cloud_config.get_api_version('identity') == '3': - project = self._identity_client.post( - '/projects', json={'project': project_ref}) - else: - project = self._identity_client.post( - '/tenants', json={'tenant': project_ref}) - project = self._normalize_project(project) + endpoint, key = ('projects', 'project') + data = self._identity_client.post( + '/{endpoint}'.format(endpoint=endpoint), + json={key: project_ref}) + project = self._normalize_project( + meta.get_and_munchify(key, data)) self.list_projects.invalidate(self) return project @@ -980,10 +977,9 @@ def list_users(self): :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ - users = self._identity_client.get('/users') - if isinstance(users, dict): - users = users['users'] - return _utils.normalize_users(users) + data = self._identity_client.get('/users') + return _utils.normalize_users( + meta.get_and_munchify('users', data)) def search_users(self, name_or_id=None, filters=None): """Search users. @@ -1026,13 +1022,14 @@ def get_user_by_id(self, user_id, normalize=True): :returns: a single ``munch.Munch`` containing the user description """ - user = self._identity_client.get( + data = self._identity_client.get( '/users/{user}'.format(user=user_id), error_message="Error getting user with ID {user_id}".format( user_id=user_id)) + user = meta.get_and_munchify('user', data) if user and normalize: - return _utils.normalize_users([user])[0] + user = _utils.normalize_users(user) return user # NOTE(Shrews): Keystone v2 supports updating only name, email and enabled. From fb956cce7b528eebc7332377d76c156cddcbd6ed Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 18 Jun 2017 08:19:21 -0500 Subject: [PATCH 1638/3836] Remove use of FakeServer from tests FakeServer is a Fake of a novaclient object. Use actual json instead. Speaking of - transform the dict through json dump/load so that it's unicode where it's supposed to be. This makes diffs easier to make. Change-Id: I45c1136078f604c73c1fea54a9eb52bbbc8c69b5 --- shade/tests/fakes.py | 103 ++++++-------- shade/tests/unit/test_create_server.py | 138 +++++++++---------- shade/tests/unit/test_delete_server.py | 19 ++- shade/tests/unit/test_floating_ip_common.py | 50 +++---- shade/tests/unit/test_floating_ip_neutron.py | 18 ++- shade/tests/unit/test_floating_ip_nova.py | 24 ++-- shade/tests/unit/test_inventory.py | 5 +- shade/tests/unit/test_meta.py | 128 +++++++++-------- shade/tests/unit/test_security_groups.py | 12 +- 9 files changed, 226 insertions(+), 271 deletions(-) diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index d6ab32d50..8c1ff1543 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -18,6 +18,7 @@ """ import datetime +import json import uuid from shade._heat import template_format @@ -83,33 +84,45 @@ def make_fake_flavor(flavor_id, name, ram=100, disk=1600, vcpus=24): FAKE_TEMPLATE_CONTENT = template_format.parse(FAKE_TEMPLATE) -def make_fake_server(server_id, name, status='ACTIVE'): - return { +def make_fake_server( + server_id, name, status='ACTIVE', admin_pass=None, + addresses=None, image=None, flavor=None): + if addresses is None: + if status == 'ACTIVE': + addresses = { + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:df:b0:8d", + "version": 6, + "addr": "fddb:b018:307:0:f816:3eff:fedf:b08d", + "OS-EXT-IPS:type": "fixed"}, + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:df:b0:8d", + "version": 4, + "addr": "10.1.0.9", + "OS-EXT-IPS:type": "fixed"}, + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:df:b0:8d", + "version": 4, + "addr": "172.24.5.5", + "OS-EXT-IPS:type": "floating"}]} + else: + addresses = {} + if image is None: + image = {"id": "217f3ab1-03e0-4450-bf27-63d52b421e9e", + "links": []} + if flavor is None: + flavor = {"id": "64", + "links": []} + + server = { "OS-EXT-STS:task_state": None, - "addresses": { - "private": [ - { - "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:df:b0:8d", - "version": 6, - "addr": "fddb:b018:307:0:f816:3eff:fedf:b08d", - "OS-EXT-IPS:type": "fixed"}, - { - "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:df:b0:8d", - "version": 4, - "addr": "10.1.0.9", - "OS-EXT-IPS:type": "fixed"}, - { - "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:df:b0:8d", - "version": 4, - "addr": "172.24.5.5", - "OS-EXT-IPS:type": "floating"}]}, + "addresses": addresses, "links": [], - "image": {"id": "217f3ab1-03e0-4450-bf27-63d52b421e9e", - "links": []}, + "image": image, "OS-EXT-STS:vm_state": "active", "OS-SRV-USG:launched_at": "2017-03-23T23:57:38.000000", - "flavor": {"id": "64", - "links": []}, + "flavor": flavor, "id": server_id, "security_groups": [{"name": "default"}], "user_id": "9c119f4beaaa438792ce89387362b3ad", @@ -127,9 +140,12 @@ def make_fake_server(server_id, name, status='ACTIVE'): "key_name": None, "name": name, "created": "2017-03-23T23:57:12Z", - "tenant_id": "fdbf563e9d474696b35667254e65b45b", + "tenant_id": PROJECT_ID, "os-extended-volumes:volumes_attached": [], "config_drive": "True"} + if admin_pass: + server['adminPass'] = admin_pass + return json.loads(json.dumps(server)) def make_fake_keypair(name): @@ -242,38 +258,6 @@ def __init__(self, id, pool, ip, fixed_ip, instance_id): self.instance_id = instance_id -class FakeServer(object): - def __init__( - self, id, name, status, addresses=None, - accessIPv4='', accessIPv6='', private_v4='', - private_v6='', public_v4='', public_v6='', - interface_ip='', - flavor=None, image=None, adminPass=None, - metadata=None): - self.id = id - self.name = name - self.status = status - if not addresses: - self.addresses = {} - else: - self.addresses = addresses - if not flavor: - flavor = {} - self.flavor = flavor - if not image: - image = {} - self.image = image - self.accessIPv4 = accessIPv4 - self.accessIPv6 = accessIPv6 - self.private_v4 = private_v4 - self.public_v4 = public_v4 - self.private_v6 = private_v6 - self.public_v6 = public_v6 - self.adminPass = adminPass - self.metadata = metadata - self.interface_ip = interface_ip - - class FakeServerGroup(object): def __init__(self, id, name, policies): self.id = id @@ -354,13 +338,6 @@ def __init__(self, id, from_port=None, to_port=None, ip_protocol=None, self.parent_group_id = parent_group_id -class FakeKeypair(object): - def __init__(self, id, name, public_key): - self.id = id - self.name = name - self.public_key = public_key - - class FakeHypervisor(object): def __init__(self, id, hostname): self.id = id diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index 9dd813dde..4ab000aa3 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -34,7 +34,7 @@ def test_create_server_with_get_exception(self): Test that an exception when attempting to get the server instance via the novaclient raises an exception in create_server. """ - build_server = fakes.FakeServer('1234', '', 'BUILD') + build_server = fakes.make_fake_server('1234', '', 'BUILD') self.register_uris([ dict(method='GET', uri=self.get_mock_url( @@ -43,7 +43,7 @@ def test_create_server_with_get_exception(self): dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), - json={'server': meta.obj_to_munch(build_server).toDict()}, + json={'server': build_server}, validate=dict( json={'server': { u'flavorRef': u'flavor-id', @@ -66,8 +66,8 @@ def test_create_server_with_server_error(self): Test that a server error before we return or begin waiting for the server instance spawn raises an exception in create_server. """ - build_server = fakes.FakeServer('1234', '', 'BUILD') - error_server = fakes.FakeServer('1234', '', 'ERROR') + build_server = fakes.make_fake_server('1234', '', 'BUILD') + error_server = fakes.make_fake_server('1234', '', 'ERROR') self.register_uris([ dict(method='GET', uri=self.get_mock_url( @@ -76,7 +76,7 @@ def test_create_server_with_server_error(self): dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), - json={'server': meta.obj_to_munch(build_server).toDict()}, + json={'server': build_server}, validate=dict( json={'server': { u'flavorRef': u'flavor-id', @@ -87,7 +87,7 @@ def test_create_server_with_server_error(self): dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), - json={'server': meta.obj_to_munch(error_server).toDict()}), + json={'server': error_server}), ]) self.assertRaises( exc.OpenStackCloudException, self.cloud.create_server, @@ -99,8 +99,8 @@ def test_create_server_wait_server_error(self): Test that a server error while waiting for the server to spawn raises an exception in create_server. """ - build_server = fakes.FakeServer('1234', '', 'BUILD') - error_server = fakes.FakeServer('1234', '', 'ERROR') + build_server = fakes.make_fake_server('1234', '', 'BUILD') + error_server = fakes.make_fake_server('1234', '', 'ERROR') self.register_uris([ dict(method='GET', uri=self.get_mock_url( @@ -109,7 +109,7 @@ def test_create_server_wait_server_error(self): dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), - json={'server': meta.obj_to_munch(build_server).toDict()}, + json={'server': build_server}, validate=dict( json={'server': { u'flavorRef': u'flavor-id', @@ -120,11 +120,11 @@ def test_create_server_wait_server_error(self): dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), - json={'servers': [meta.obj_to_munch(build_server).toDict()]}), + json={'servers': [build_server]}), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), - json={'servers': [meta.obj_to_munch(error_server).toDict()]}), + json={'servers': [error_server]}), ]) self.assertRaises( exc.OpenStackCloudException, @@ -139,7 +139,7 @@ def test_create_server_with_timeout(self): Test that a timeout while waiting for the server to spawn raises an exception in create_server. """ - fake_server = fakes.FakeServer('1234', '', 'BUILD') + fake_server = fakes.make_fake_server('1234', '', 'BUILD') self.register_uris([ dict(method='GET', uri=self.get_mock_url( @@ -148,7 +148,7 @@ def test_create_server_with_timeout(self): dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), - json={'server': meta.obj_to_munch(fake_server).toDict()}, + json={'server': fake_server}, validate=dict( json={'server': { u'flavorRef': u'flavor-id', @@ -159,7 +159,7 @@ def test_create_server_with_timeout(self): dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), - json={'servers': [meta.obj_to_munch(fake_server).toDict()]}), + json={'servers': [fake_server]}), ]) self.assertRaises( exc.OpenStackCloudTimeout, @@ -175,7 +175,7 @@ def test_create_server_no_wait(self): Test that create_server with no wait and no exception in the novaclient create call returns the server instance. """ - fake_server = fakes.FakeServer('1234', '', 'BUILD') + fake_server = fakes.make_fake_server('1234', '', 'BUILD') self.register_uris([ dict(method='GET', uri=self.get_mock_url( @@ -184,7 +184,7 @@ def test_create_server_no_wait(self): dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), - json={'server': meta.obj_to_munch(fake_server).toDict()}, + json={'server': fake_server}, validate=dict( json={'server': { u'flavorRef': u'flavor-id', @@ -195,11 +195,12 @@ def test_create_server_no_wait(self): dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), - json={'server': meta.obj_to_munch(fake_server).toDict()}), + json={'server': fake_server}), ]) + normalized = self.cloud._expand_server( + self.cloud._normalize_server(fake_server), False, False) self.assertEqual( - self.cloud._normalize_server( - meta.obj_to_munch(fake_server)), + normalized, self.cloud.create_server( name='server-name', image=dict(id='image-id'), @@ -211,9 +212,10 @@ def test_create_server_with_admin_pass_no_wait(self): """ Test that a server with an admin_pass passed returns the password """ - fake_server = fakes.FakeServer('1234', '', 'BUILD') - fake_create_server = fakes.FakeServer('1234', '', 'BUILD', - adminPass='ooBootheiX0edoh') + admin_pass = self.getUniqueString('password') + fake_server = fakes.make_fake_server('1234', '', 'BUILD') + fake_create_server = fakes.make_fake_server( + '1234', '', 'BUILD', admin_pass=admin_pass) self.register_uris([ dict(method='GET', uri=self.get_mock_url( @@ -222,11 +224,10 @@ def test_create_server_with_admin_pass_no_wait(self): dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), - json={'server': meta.obj_to_munch( - fake_create_server).toDict()}, + json={'server': fake_create_server}, validate=dict( json={'server': { - u'adminPass': 'ooBootheiX0edoh', + u'adminPass': admin_pass, u'flavorRef': u'flavor-id', u'imageRef': u'image-id', u'max_count': 1, @@ -235,14 +236,14 @@ def test_create_server_with_admin_pass_no_wait(self): dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), - json={'server': meta.obj_to_munch(fake_server).toDict()}), + json={'server': fake_server}), ]) self.assertEqual( - self.cloud._normalize_server( - meta.obj_to_munch(fake_create_server)), + self.cloud._normalize_server(fake_create_server)['adminPass'], self.cloud.create_server( name='server-name', image=dict(id='image-id'), - flavor=dict(id='flavor-id'), admin_pass='ooBootheiX0edoh')) + flavor=dict(id='flavor-id'), + admin_pass=admin_pass)['adminPass']) self.assert_calls() @@ -251,9 +252,10 @@ def test_create_server_with_admin_pass_wait(self, mock_wait): """ Test that a server with an admin_pass passed returns the password """ - fake_server = fakes.FakeServer('1234', '', 'BUILD') - fake_server_with_pass = fakes.FakeServer('1234', '', 'BUILD', - adminPass='ooBootheiX0edoh') + admin_pass = self.getUniqueString('password') + fake_server = fakes.make_fake_server('1234', '', 'BUILD') + fake_server_with_pass = fakes.make_fake_server( + '1234', '', 'BUILD', admin_pass=admin_pass) self.register_uris([ dict(method='GET', uri=self.get_mock_url( @@ -262,26 +264,24 @@ def test_create_server_with_admin_pass_wait(self, mock_wait): dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), - json={'server': meta.obj_to_munch( - fake_server_with_pass).toDict()}, + json={'server': fake_server_with_pass}, validate=dict( json={'server': { u'flavorRef': u'flavor-id', u'imageRef': u'image-id', u'max_count': 1, u'min_count': 1, - u'adminPass': 'ooBootheiX0edoh', + u'adminPass': admin_pass, u'name': u'server-name'}})), ]) # The wait returns non-password server - mock_wait.return_value = self.cloud._normalize_server( - meta.obj_to_munch(fake_server)) + mock_wait.return_value = self.cloud._normalize_server(fake_server) server = self.cloud.create_server( name='server-name', image=dict(id='image-id'), flavor=dict(id='flavor-id'), - admin_pass='ooBootheiX0edoh', wait=True) + admin_pass=admin_pass, wait=True) # Assert that we did wait self.assertTrue(mock_wait.called) @@ -289,8 +289,7 @@ def test_create_server_with_admin_pass_wait(self, mock_wait): # Even with the wait, we should still get back a passworded server self.assertEqual( server['adminPass'], - self.cloud._normalize_server( - meta.obj_to_munch(fake_server_with_pass))['adminPass'] + self.cloud._normalize_server(fake_server_with_pass)['adminPass'] ) self.assert_calls() @@ -335,7 +334,7 @@ def test_create_server_wait(self, mock_wait): Test that create_server with a wait actually does the wait. """ # TODO(mordred) Make this a full proper response - fake_server = fakes.FakeServer('1234', '', 'BUILD') + fake_server = fakes.make_fake_server('1234', '', 'BUILD') self.register_uris([ dict(method='GET', @@ -345,7 +344,7 @@ def test_create_server_wait(self, mock_wait): dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), - json={'server': meta.obj_to_munch(fake_server).toDict()}, + json={'server': fake_server}, validate=dict( json={'server': { u'flavorRef': u'flavor-id', @@ -359,7 +358,7 @@ def test_create_server_wait(self, mock_wait): dict(id='image-id'), dict(id='flavor-id'), wait=True), mock_wait.assert_called_once_with( - meta.obj_to_munch(fake_server), + fake_server, auto_ip=True, ips=None, ip_pool=None, reuse=True, timeout=180, nat_destination=None, @@ -374,8 +373,9 @@ def test_create_server_no_addresses( Test that create_server with a wait throws an exception if the server doesn't have addresses. """ - build_server = fakes.FakeServer('1234', '', 'BUILD') - fake_server = fakes.FakeServer('1234', '', 'ACTIVE') + build_server = fakes.make_fake_server('1234', '', 'BUILD') + fake_server = fakes.make_fake_server( + '1234', '', 'ACTIVE', addresses={}) self.register_uris([ dict(method='GET', uri=self.get_mock_url( @@ -384,7 +384,7 @@ def test_create_server_no_addresses( dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), - json={'server': meta.obj_to_munch(build_server).toDict()}, + json={'server': build_server}, validate=dict( json={'server': { u'flavorRef': u'flavor-id', @@ -395,11 +395,11 @@ def test_create_server_no_addresses( dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), - json={'servers': [meta.obj_to_munch(build_server).toDict()]}), + json={'servers': [build_server]}), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), - json={'servers': [meta.obj_to_munch(fake_server).toDict()]}), + json={'servers': [fake_server]}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'ports.json'], @@ -428,8 +428,7 @@ def test_create_server_network_with_no_nics(self): Verify that if 'network' is supplied, and 'nics' is not, that we attempt to get the network for the server. """ - build_server = fakes.FakeServer('1234', '', 'BUILD') - active_server = fakes.FakeServer('1234', '', 'ACTIVE') + build_server = fakes.make_fake_server('1234', '', 'BUILD') network = { 'id': 'network-id', 'name': 'network-name' @@ -442,7 +441,7 @@ def test_create_server_network_with_no_nics(self): dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), - json={'server': meta.obj_to_munch(build_server).toDict()}, + json={'server': build_server}, validate=dict( json={'server': { u'flavorRef': u'flavor-id', @@ -454,12 +453,7 @@ def test_create_server_network_with_no_nics(self): dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), - json={'server': meta.obj_to_munch(active_server).toDict()}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json'], - qs_elements=['device_id=1234']), - json={'ports': []}), + json={'server': build_server}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks.json']), @@ -483,7 +477,7 @@ def test_create_server_network_with_empty_nics(self): 'id': 'network-id', 'name': 'network-name' } - build_server = fakes.FakeServer('1234', '', 'BUILD') + build_server = fakes.make_fake_server('1234', '', 'BUILD') self.register_uris([ dict(method='GET', uri=self.get_mock_url( @@ -492,7 +486,7 @@ def test_create_server_network_with_empty_nics(self): dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), - json={'server': meta.obj_to_munch(build_server).toDict()}, + json={'server': build_server}, validate=dict( json={'server': { u'flavorRef': u'flavor-id', @@ -504,7 +498,7 @@ def test_create_server_network_with_empty_nics(self): dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), - json={'server': meta.obj_to_munch(build_server).toDict()}), + json={'server': build_server}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks.json']), @@ -525,8 +519,8 @@ def test_create_server_get_flavor_image(self): fake_image_dict = fakes.make_fake_image(image_id=image_id) fake_image_search_return = {'images': [fake_image_dict]} - build_server = fakes.FakeServer('1234', '', 'BUILD') - active_server = fakes.FakeServer('1234', '', 'BUILD') + build_server = fakes.make_fake_server('1234', '', 'BUILD') + active_server = fakes.make_fake_server('1234', '', 'BUILD') self.register_uris([ dict(method='GET', @@ -540,7 +534,7 @@ def test_create_server_get_flavor_image(self): dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), - json={'server': meta.obj_to_munch(build_server).toDict()}, + json={'server': build_server}, validate=dict( json={'server': { u'flavorRef': fakes.FLAVOR_ID, @@ -552,7 +546,7 @@ def test_create_server_get_flavor_image(self): dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), - json={'server': meta.obj_to_munch(active_server).toDict()}), + json={'server': active_server}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks.json']), @@ -566,8 +560,8 @@ def test_create_server_get_flavor_image(self): self.assert_calls() def test_create_boot_attach_volume(self): - build_server = fakes.FakeServer('1234', '', 'BUILD') - active_server = fakes.FakeServer('1234', '', 'BUILD') + build_server = fakes.make_fake_server('1234', '', 'BUILD') + active_server = fakes.make_fake_server('1234', '', 'BUILD') vol = {'id': 'volume001', 'status': 'available', 'name': '', 'attachments': []} @@ -581,7 +575,7 @@ def test_create_boot_attach_volume(self): dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['os-volumes_boot']), - json={'server': meta.obj_to_munch(build_server).toDict()}, + json={'server': build_server}, validate=dict( json={'server': { u'flavorRef': 'flavor-id', @@ -608,7 +602,7 @@ def test_create_boot_attach_volume(self): dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), - json={'server': meta.obj_to_munch(active_server).toDict()}), + json={'server': active_server}), ]) self.cloud.create_server( @@ -622,8 +616,8 @@ def test_create_boot_attach_volume(self): self.assert_calls() def test_create_boot_from_volume_image_terminate(self): - build_server = fakes.FakeServer('1234', '', 'BUILD') - active_server = fakes.FakeServer('1234', '', 'BUILD') + build_server = fakes.make_fake_server('1234', '', 'BUILD') + active_server = fakes.make_fake_server('1234', '', 'BUILD') self.register_uris([ dict(method='GET', @@ -633,7 +627,7 @@ def test_create_boot_from_volume_image_terminate(self): dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['os-volumes_boot']), - json={'server': meta.obj_to_munch(build_server).toDict()}, + json={'server': build_server}, validate=dict( json={'server': { u'flavorRef': 'flavor-id', @@ -651,7 +645,7 @@ def test_create_boot_from_volume_image_terminate(self): dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), - json={'server': meta.obj_to_munch(active_server).toDict()}), + json={'server': active_server}), ]) self.cloud.create_server( diff --git a/shade/tests/unit/test_delete_server.py b/shade/tests/unit/test_delete_server.py index a74432eff..bcbe0497b 100644 --- a/shade/tests/unit/test_delete_server.py +++ b/shade/tests/unit/test_delete_server.py @@ -18,7 +18,6 @@ """ from shade import exc as shade_exc -from shade import meta from shade.tests import fakes from shade.tests.unit import base @@ -29,12 +28,12 @@ def test_delete_server(self): """ Test that server delete is called when wait=False """ - server = fakes.FakeServer('1234', 'daffy', 'ACTIVE') + server = fakes.make_fake_server('1234', 'daffy', 'ACTIVE') self.register_uris([ dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), - json={'servers': [meta.obj_to_munch(server).toDict()]}), + json={'servers': [server]}), dict(method='DELETE', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234'])), @@ -71,19 +70,19 @@ def test_delete_server_wait_for_deleted(self): """ Test that delete_server waits for the server to be gone """ - server = fakes.FakeServer('9999', 'wily', 'ACTIVE') + server = fakes.make_fake_server('9999', 'wily', 'ACTIVE') self.register_uris([ dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), - json={'servers': [meta.obj_to_munch(server).toDict()]}), + json={'servers': [server]}), dict(method='DELETE', uri=self.get_mock_url( 'compute', 'public', append=['servers', '9999'])), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), - json={'servers': [meta.obj_to_munch(server).toDict()]}), + json={'servers': [server]}), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -97,12 +96,12 @@ def test_delete_server_fails(self): """ Test that delete_server raises non-404 exceptions """ - server = fakes.FakeServer('1212', 'speedy', 'ACTIVE') + server = fakes.make_fake_server('1212', 'speedy', 'ACTIVE') self.register_uris([ dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), - json={'servers': [meta.obj_to_munch(server).toDict()]}), + json={'servers': [server]}), dict(method='DELETE', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1212']), @@ -128,12 +127,12 @@ def fake_has_service(service_type): return orig_has_service(service_type) self.cloud.has_service = fake_has_service - server = fakes.FakeServer('1234', 'porky', 'ACTIVE') + server = fakes.make_fake_server('1234', 'porky', 'ACTIVE') self.register_uris([ dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), - json={'servers': [meta.obj_to_munch(server).toDict()]}), + json={'servers': [server]}), dict(method='DELETE', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234'])), diff --git a/shade/tests/unit/test_floating_ip_common.py b/shade/tests/unit/test_floating_ip_common.py index d9860cf0d..638294aef 100644 --- a/shade/tests/unit/test_floating_ip_common.py +++ b/shade/tests/unit/test_floating_ip_common.py @@ -23,7 +23,7 @@ from shade import meta from shade import OpenStackCloud -from shade.tests.fakes import FakeServer +from shade.tests import fakes from shade.tests.unit import base @@ -35,10 +35,10 @@ class TestFloatingIP(base.TestCase): def test_add_auto_ip( self, mock_available_floating_ip, mock_attach_ip_to_server, mock_get_floating_ip): - server = FakeServer( - id='server-id', name='test-server', status="ACTIVE", addresses={} + server_dict = fakes.make_fake_server( + server_id='server-id', name='test-server', status="ACTIVE", + addresses={} ) - server_dict = meta.obj_to_munch(server) floating_ip_dict = { "id": "this-is-a-floating-ip-id", "fixed_ip_address": None, @@ -59,10 +59,9 @@ def test_add_auto_ip( @patch.object(OpenStackCloud, '_add_ip_from_pool') def test_add_ips_to_server_pool(self, mock_add_ip_from_pool): - server = FakeServer( - id='romeo', name='test-server', status="ACTIVE", addresses={} - ) - server_dict = meta.obj_to_munch(server) + server_dict = fakes.make_fake_server( + server_id='romeo', name='test-server', status="ACTIVE", + addresses={}) pool = 'nova' self.cloud.add_ips_to_server(server_dict, ip_pool=pool) @@ -82,8 +81,8 @@ def test_add_ips_to_server_ipv6_only( self.cloud.force_ipv4 = False self.cloud._local_ipv6 = True mock_has_service.return_value = False - server = FakeServer( - id='server-id', name='test-server', status="ACTIVE", + server = fakes.make_fake_server( + server_id='server-id', name='test-server', status="ACTIVE", addresses={ 'private': [{ 'addr': "10.223.160.141", @@ -97,8 +96,7 @@ def test_add_ips_to_server_ipv6_only( }] } ) - server_dict = meta.add_server_interfaces( - self.cloud, meta.obj_to_munch(server)) + server_dict = meta.add_server_interfaces(self.cloud, server) new_server = self.cloud.add_ips_to_server(server=server_dict) mock_get_floating_ip.assert_not_called() @@ -122,8 +120,8 @@ def test_add_ips_to_server_rackspace( self.cloud.force_ipv4 = False self.cloud._local_ipv6 = True mock_has_service.return_value = False - server = FakeServer( - id='server-id', name='test-server', status="ACTIVE", + server = fakes.make_fake_server( + server_id='server-id', name='test-server', status="ACTIVE", addresses={ 'private': [{ 'addr': "10.223.160.141", @@ -138,8 +136,7 @@ def test_add_ips_to_server_rackspace( }] } ) - server_dict = meta.add_server_interfaces( - self.cloud, meta.obj_to_munch(server)) + server_dict = meta.add_server_interfaces(self.cloud, server) new_server = self.cloud.add_ips_to_server(server=server_dict) mock_get_floating_ip.assert_not_called() @@ -159,8 +156,8 @@ def test_add_ips_to_server_rackspace_local_ipv4( self.cloud.force_ipv4 = False self.cloud._local_ipv6 = False mock_has_service.return_value = False - server = FakeServer( - id='server-id', name='test-server', status="ACTIVE", + server = fakes.make_fake_server( + server_id='server-id', name='test-server', status="ACTIVE", addresses={ 'private': [{ 'addr': "10.223.160.141", @@ -175,8 +172,7 @@ def test_add_ips_to_server_rackspace_local_ipv4( }] } ) - server_dict = meta.add_server_interfaces( - self.cloud, meta.obj_to_munch(server)) + server_dict = meta.add_server_interfaces(self.cloud, server) new_server = self.cloud.add_ips_to_server(server=server_dict) mock_get_floating_ip.assert_not_called() @@ -185,10 +181,9 @@ def test_add_ips_to_server_rackspace_local_ipv4( @patch.object(OpenStackCloud, 'add_ip_list') def test_add_ips_to_server_ip_list(self, mock_add_ip_list): - server = FakeServer( - id='server-id', name='test-server', status="ACTIVE", addresses={} - ) - server_dict = meta.obj_to_munch(server) + server_dict = fakes.make_fake_server( + server_id='server-id', name='test-server', status="ACTIVE", + addresses={}) ips = ['203.0.113.29', '172.24.4.229'] self.cloud.add_ips_to_server(server_dict, ips=ips) @@ -200,10 +195,9 @@ def test_add_ips_to_server_ip_list(self, mock_add_ip_list): @patch.object(OpenStackCloud, '_add_auto_ip') def test_add_ips_to_server_auto_ip( self, mock_add_auto_ip, mock_needs_floating_ip): - server = FakeServer( - id='server-id', name='test-server', status="ACTIVE", addresses={} - ) - server_dict = meta.obj_to_munch(server) + server_dict = fakes.make_fake_server( + server_id='server-id', name='test-server', status="ACTIVE", + addresses={}) # TODO(mordred) REMOVE THIS MOCK WHEN THE NEXT PATCH LANDS # SERIOUSLY THIS TIME. NEXT PATCH - WHICH SHOULD ADD MOCKS FOR diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 5588bfcb3..555f47289 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -24,7 +24,6 @@ import munch from shade import exc -from shade import meta from shade.tests import fakes from shade.tests.unit import base @@ -136,15 +135,14 @@ def assertAreInstances(self, elements, elem_type): def setUp(self): super(TestFloatingIP, self).setUp() - self.fake_server = meta.obj_to_munch( - fakes.FakeServer( - 'server-id', '', 'ACTIVE', - addresses={u'test_pnztt_net': [{ - u'OS-EXT-IPS:type': u'fixed', - u'addr': '192.0.2.129', - u'version': 4, - u'OS-EXT-IPS-MAC:mac_addr': - u'fa:16:3e:ae:7d:42'}]})) + self.fake_server = fakes.make_fake_server( + 'server-id', '', 'ACTIVE', + addresses={u'test_pnztt_net': [{ + u'OS-EXT-IPS:type': u'fixed', + u'addr': '192.0.2.129', + u'version': 4, + u'OS-EXT-IPS-MAC:mac_addr': + u'fa:16:3e:ae:7d:42'}]}) self.floating_ip = self.cloud._normalize_floating_ips( self.mock_floating_ip_list_rep['floatingips'])[0] diff --git a/shade/tests/unit/test_floating_ip_nova.py b/shade/tests/unit/test_floating_ip_nova.py index 6e39f1c79..c86321b40 100644 --- a/shade/tests/unit/test_floating_ip_nova.py +++ b/shade/tests/unit/test_floating_ip_nova.py @@ -19,7 +19,6 @@ Tests Floating IP resource methods for nova-network """ -from shade import meta from shade.tests import fakes from shade.tests.unit import base @@ -68,15 +67,14 @@ def assertAreInstances(self, elements, elem_type): def setUp(self): super(TestFloatingIP, self).setUp() - self.fake_server = meta.obj_to_munch( - fakes.FakeServer( - 'server-id', '', 'ACTIVE', - addresses={u'test_pnztt_net': [{ - u'OS-EXT-IPS:type': u'fixed', - u'addr': '192.0.2.129', - u'version': 4, - u'OS-EXT-IPS-MAC:mac_addr': - u'fa:16:3e:ae:7d:42'}]})) + self.fake_server = fakes.make_fake_server( + 'server-id', '', 'ACTIVE', + addresses={u'test_pnztt_net': [{ + u'OS-EXT-IPS:type': u'fixed', + u'addr': '192.0.2.129', + u'version': 4, + u'OS-EXT-IPS-MAC:mac_addr': + u'fa:16:3e:ae:7d:42'}]}) self.cloud.has_service = get_fake_has_service(self.cloud.has_service) @@ -239,7 +237,7 @@ def test_attach_ip_to_server(self): dict(method='POST', uri=self.get_mock_url( 'compute', - append=['servers', self.fake_server.id, 'action']), + append=['servers', self.fake_server['id'], 'action']), validate=dict( json={ "addFloatingIp": { @@ -264,7 +262,7 @@ def test_detach_ip_from_server(self): dict(method='POST', uri=self.get_mock_url( 'compute', - append=['servers', self.fake_server.id, 'action']), + append=['servers', self.fake_server['id'], 'action']), validate=dict( json={ "removeFloatingIp": { @@ -287,7 +285,7 @@ def test_add_ip_from_pool(self): dict(method='POST', uri=self.get_mock_url( 'compute', - append=['servers', self.fake_server.id, 'action']), + append=['servers', self.fake_server['id'], 'action']), validate=dict( json={ "addFloatingIp": { diff --git a/shade/tests/unit/test_inventory.py b/shade/tests/unit/test_inventory.py index 9a5632953..84846b231 100644 --- a/shade/tests/unit/test_inventory.py +++ b/shade/tests/unit/test_inventory.py @@ -18,7 +18,6 @@ from shade import exc from shade import inventory -from shade import meta from shade.tests import fakes from shade.tests.unit import base @@ -101,8 +100,8 @@ def test_list_hosts_no_detail(self, mock_cloud, mock_config): inv = inventory.OpenStackInventory() server = self.cloud._normalize_server( - meta.obj_to_munch(fakes.FakeServer( - '1234', 'test', 'ACTIVE', addresses={}))) + fakes.make_fake_server( + '1234', 'test', 'ACTIVE', addresses={})) self.assertIsInstance(inv.clouds, list) self.assertEqual(1, len(inv.clouds)) inv.clouds[0].list_servers.return_value = [server] diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 1b50dd37d..65c1eca57 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -75,11 +75,10 @@ def list_server_security_groups(self, server): def get_default_network(self): return None -standard_fake_server = fakes.FakeServer( - id='test-id-0', +standard_fake_server = fakes.make_fake_server( + server_id='test-id-0', name='test-id-0', status='ACTIVE', - metadata={'group': 'test-group'}, addresses={'private': [{'OS-EXT-IPS:type': 'fixed', 'addr': PRIVATE_V4, 'version': 4}], @@ -88,9 +87,8 @@ def get_default_network(self): 'version': 4}]}, flavor={'id': '101'}, image={'id': '471c2475-da2f-47ac-aba5-cb4aa3d546f5'}, - accessIPv4='', - accessIPv6='', ) +standard_fake_server['metadata'] = {'group': 'test-group'} SUBNETS_WITH_NAT = [ { @@ -286,15 +284,15 @@ def test_get_server_private_ip(self): json={'subnets': SUBNETS_WITH_NAT}) ]) - srv = meta.obj_to_munch(fakes.FakeServer( - id='test-id', name='test-name', status='ACTIVE', + srv = fakes.make_fake_server( + server_id='test-id', name='test-name', status='ACTIVE', addresses={'private': [{'OS-EXT-IPS:type': 'fixed', 'addr': PRIVATE_V4, 'version': 4}], 'public': [{'OS-EXT-IPS:type': 'floating', 'addr': PUBLIC_V4, 'version': 4}]} - )) + ) self.assertEqual( PRIVATE_V4, meta.get_server_private_ip(srv, self.cloud)) @@ -315,8 +313,8 @@ def test_get_server_multiple_private_ip(self): shared_mac = '11:22:33:44:55:66' distinct_mac = '66:55:44:33:22:11' - srv = meta.obj_to_munch(fakes.FakeServer( - id='test-id', name='test-name', status='ACTIVE', + srv = fakes.make_fake_server( + server_id='test-id', name='test-name', status='ACTIVE', addresses={'test-net': [{'OS-EXT-IPS:type': 'fixed', 'OS-EXT-IPS-MAC:mac_addr': distinct_mac, 'addr': '10.0.0.100', @@ -329,7 +327,7 @@ def test_get_server_multiple_private_ip(self): 'OS-EXT-IPS-MAC:mac_addr': shared_mac, 'addr': PUBLIC_V4, 'version': 4}]} - )) + ) self.assertEqual( '10.0.0.101', meta.get_server_private_ip(srv, self.cloud)) @@ -383,8 +381,8 @@ def test_get_server_private_ip_devstack( json={'security_groups': []}) ]) - srv = self.cloud.get_openstack_vars(meta.obj_to_munch(fakes.FakeServer( - id='test-id', name='test-name', status='ACTIVE', + srv = self.cloud.get_openstack_vars(fakes.make_fake_server( + server_id='test-id', name='test-name', status='ACTIVE', flavor={u'id': u'1'}, image={ 'name': u'cirros-0.3.4-x86_64-uec', @@ -395,7 +393,7 @@ def test_get_server_private_ip_devstack( u'version': 4, u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:ae:7d:42' }]} - ))) + )) self.assertEqual(PRIVATE_V4, srv['private_v4']) self.assert_calls() @@ -433,8 +431,8 @@ def test_get_server_private_ip_no_fip( json={'security_groups': []}) ]) - srv = self.cloud.get_openstack_vars(meta.obj_to_munch(fakes.FakeServer( - id='test-id', name='test-name', status='ACTIVE', + srv = self.cloud.get_openstack_vars(fakes.make_fake_server( + server_id='test-id', name='test-name', status='ACTIVE', flavor={u'id': u'1'}, image={ 'name': u'cirros-0.3.4-x86_64-uec', @@ -445,7 +443,7 @@ def test_get_server_private_ip_no_fip( u'version': 4, u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:ae:7d:42' }]} - ))) + )) self.assertEqual(PRIVATE_V4, srv['private_v4']) self.assert_calls() @@ -483,8 +481,8 @@ def test_get_server_cloud_no_fips( json={'security_groups': []}) ]) - srv = self.cloud.get_openstack_vars(meta.obj_to_munch(fakes.FakeServer( - id='test-id', name='test-name', status='ACTIVE', + srv = self.cloud.get_openstack_vars(fakes.make_fake_server( + server_id='test-id', name='test-name', status='ACTIVE', flavor={u'id': u'1'}, image={ 'name': u'cirros-0.3.4-x86_64-uec', @@ -493,7 +491,7 @@ def test_get_server_cloud_no_fips( u'addr': PRIVATE_V4, u'version': 4, }]} - ))) + )) self.assertEqual(PRIVATE_V4, srv['private_v4']) self.assert_calls() @@ -551,8 +549,8 @@ def test_get_server_cloud_missing_fips( json={'security_groups': []}) ]) - srv = self.cloud.get_openstack_vars(meta.obj_to_munch(fakes.FakeServer( - id='test-id', name='test-name', status='ACTIVE', + srv = self.cloud.get_openstack_vars(fakes.make_fake_server( + server_id='test-id', name='test-name', status='ACTIVE', flavor={u'id': u'1'}, image={ 'name': u'cirros-0.3.4-x86_64-uec', @@ -562,7 +560,7 @@ def test_get_server_cloud_missing_fips( u'version': 4, 'OS-EXT-IPS-MAC:mac_addr': 'fa:16:3e:ae:7d:42', }]} - ))) + )) self.assertEqual(PUBLIC_V4, srv['public_v4']) self.assert_calls() @@ -588,8 +586,8 @@ def test_get_server_cloud_rackspace_v6( json={'security_groups': []}) ]) - srv = self.cloud.get_openstack_vars(meta.obj_to_munch(fakes.FakeServer( - id='test-id', name='test-name', status='ACTIVE', + srv = self.cloud.get_openstack_vars(fakes.make_fake_server( + server_id='test-id', name='test-name', status='ACTIVE', flavor={u'id': u'1'}, image={ 'name': u'cirros-0.3.4-x86_64-uec', @@ -607,7 +605,7 @@ def test_get_server_cloud_rackspace_v6( 'version': 6 }] } - ))) + )) self.assertEqual("10.223.160.141", srv['private_v4']) self.assertEqual("104.130.246.91", srv['public_v4']) @@ -647,8 +645,8 @@ def test_get_server_cloud_osic_split( json={'security_groups': []}) ]) - srv = self.cloud.get_openstack_vars(meta.obj_to_munch(fakes.FakeServer( - id='test-id', name='test-name', status='ACTIVE', + srv = self.cloud.get_openstack_vars(fakes.make_fake_server( + server_id='test-id', name='test-name', status='ACTIVE', flavor={u'id': u'1'}, image={ 'name': u'cirros-0.3.4-x86_64-uec', @@ -666,7 +664,7 @@ def test_get_server_cloud_osic_split( 'version': 6 }] } - ))) + )) self.assertEqual("10.223.160.141", srv['private_v4']) self.assertEqual("104.130.246.91", srv['public_v4']) @@ -690,12 +688,12 @@ def test_get_server_external_ipv4_neutron(self): uri='https://network.example.com/v2.0/subnets.json', json={'subnets': SUBNETS_WITH_NAT}) ]) - srv = meta.obj_to_munch(fakes.FakeServer( - id='test-id', name='test-name', status='ACTIVE', + srv = fakes.make_fake_server( + server_id='test-id', name='test-name', status='ACTIVE', addresses={'test-net': [{ 'addr': PUBLIC_V4, 'version': 4}]}, - )) + ) ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) self.assertEqual(PUBLIC_V4, ip) @@ -717,12 +715,12 @@ def test_get_server_external_provider_ipv4_neutron(self): json={'subnets': SUBNETS_WITH_NAT}) ]) - srv = meta.obj_to_munch(fakes.FakeServer( - id='test-id', name='test-name', status='ACTIVE', + srv = fakes.make_fake_server( + server_id='test-id', name='test-name', status='ACTIVE', addresses={'test-net': [{ 'addr': PUBLIC_V4, 'version': 4}]}, - )) + ) ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) self.assertEqual(PUBLIC_V4, ip) @@ -744,12 +742,12 @@ def test_get_server_internal_provider_ipv4_neutron(self): uri='https://network.example.com/v2.0/subnets.json', json={'subnets': SUBNETS_WITH_NAT}) ]) - srv = meta.obj_to_munch(fakes.FakeServer( - id='test-id', name='test-name', status='ACTIVE', + srv = fakes.make_fake_server( + server_id='test-id', name='test-name', status='ACTIVE', addresses={'test-net': [{ 'addr': PRIVATE_V4, 'version': 4}]}, - )) + ) self.assertIsNone( meta.get_server_external_ipv4(cloud=self.cloud, server=srv)) int_ip = meta.get_server_private_ip(cloud=self.cloud, server=srv) @@ -772,29 +770,29 @@ def test_get_server_external_none_ipv4_neutron(self): json={'subnets': SUBNETS_WITH_NAT}) ]) - srv = meta.obj_to_munch(fakes.FakeServer( - id='test-id', name='test-name', status='ACTIVE', + srv = fakes.make_fake_server( + server_id='test-id', name='test-name', status='ACTIVE', addresses={'test-net': [{ 'addr': PUBLIC_V4, 'version': 4}]}, - )) + ) ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) self.assertEqual(None, ip) self.assert_calls() def test_get_server_external_ipv4_neutron_accessIPv4(self): - srv = meta.obj_to_munch(fakes.FakeServer( - id='test-id', name='test-name', status='ACTIVE', - accessIPv4=PUBLIC_V4)) + srv = fakes.make_fake_server( + server_id='test-id', name='test-name', status='ACTIVE') + srv['accessIPv4'] = PUBLIC_V4 ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) self.assertEqual(PUBLIC_V4, ip) def test_get_server_external_ipv4_neutron_accessIPv6(self): - srv = meta.obj_to_munch(fakes.FakeServer( - id='test-id', name='test-name', status='ACTIVE', - accessIPv6=PUBLIC_V6)) + srv = fakes.make_fake_server( + server_id='test-id', name='test-name', status='ACTIVE') + srv['accessIPv6'] = PUBLIC_V6 ip = meta.get_server_external_ipv6(server=srv) self.assertEqual(PUBLIC_V6, ip) @@ -806,10 +804,10 @@ def test_get_server_external_ipv4_neutron_exception(self): uri='https://network.example.com/v2.0/networks.json', status_code=404)]) - srv = meta.obj_to_munch(fakes.FakeServer( - id='test-id', name='test-name', status='ACTIVE', + srv = fakes.make_fake_server( + server_id='test-id', name='test-name', status='ACTIVE', addresses={'public': [{'addr': PUBLIC_V4, 'version': 4}]} - )) + ) ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) self.assertEqual(PUBLIC_V4, ip) @@ -819,9 +817,9 @@ def test_get_server_external_ipv4_nova_public(self): # Testing Clouds w/o Neutron and a network named public self.cloud.cloud_config.config['has_network'] = False - srv = meta.obj_to_munch(fakes.FakeServer( - id='test-id', name='test-name', status='ACTIVE', - addresses={'public': [{'addr': PUBLIC_V4, 'version': 4}]})) + srv = fakes.make_fake_server( + server_id='test-id', name='test-name', status='ACTIVE', + addresses={'public': [{'addr': PUBLIC_V4, 'version': 4}]}) ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) self.assertEqual(PUBLIC_V4, ip) @@ -830,23 +828,23 @@ def test_get_server_external_ipv4_nova_none(self): # Testing Clouds w/o Neutron or a globally routable IP self.cloud.cloud_config.config['has_network'] = False - srv = meta.obj_to_munch(fakes.FakeServer( - id='test-id', name='test-name', status='ACTIVE', - addresses={'test-net': [{'addr': PRIVATE_V4}]})) + srv = fakes.make_fake_server( + server_id='test-id', name='test-name', status='ACTIVE', + addresses={'test-net': [{'addr': PRIVATE_V4}]}) ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) self.assertIsNone(ip) def test_get_server_external_ipv6(self): - srv = meta.obj_to_munch(fakes.FakeServer( - id='test-id', name='test-name', status='ACTIVE', + srv = fakes.make_fake_server( + server_id='test-id', name='test-name', status='ACTIVE', addresses={ 'test-net': [ {'addr': PUBLIC_V4, 'version': 4}, {'addr': PUBLIC_V6, 'version': 6} ] } - )) + ) ip = meta.get_server_external_ipv6(srv) self.assertEqual(PUBLIC_V6, ip) @@ -925,10 +923,10 @@ def test_basic_hostvars( self.assertEqual('admin', hostvars['location']['project']['name']) self.assertEqual("test-image-name", hostvars['image']['name']) self.assertEqual( - standard_fake_server.image['id'], hostvars['image']['id']) + standard_fake_server['image']['id'], hostvars['image']['id']) self.assertNotIn('links', hostvars['image']) self.assertEqual( - standard_fake_server.flavor['id'], hostvars['flavor']['id']) + standard_fake_server['flavor']['id'], hostvars['flavor']['id']) self.assertEqual("test-flavor-name", hostvars['flavor']['name']) self.assertNotIn('links', hostvars['flavor']) # test having volumes @@ -964,14 +962,14 @@ def test_image_string(self, mock_get_server_external_ipv4): mock_get_server_external_ipv4.return_value = PUBLIC_V4 server = standard_fake_server - server.image = 'fake-image-id' + server['image'] = 'fake-image-id' hostvars = meta.get_hostvars_from_server( FakeCloud(), meta.obj_to_munch(server)) self.assertEqual('fake-image-id', hostvars['image']['id']) def test_az(self): server = standard_fake_server - server.__dict__['OS-EXT-AZ:availability_zone'] = 'az1' + server['OS-EXT-AZ:availability_zone'] = 'az1' hostvars = self.cloud._normalize_server(meta.obj_to_munch(server)) self.assertEqual('az1', hostvars['az']) @@ -1036,12 +1034,12 @@ def side_effect(*args): def test_obj_to_munch(self): cloud = FakeCloud() - cloud.server = standard_fake_server + cloud.subcloud = FakeCloud() cloud_dict = meta.obj_to_munch(cloud) self.assertEqual(FakeCloud.name, cloud_dict['name']) self.assertNotIn('_unused', cloud_dict) self.assertNotIn('get_flavor_name', cloud_dict) - self.assertNotIn('server', cloud_dict) + self.assertNotIn('subcloud', cloud_dict) self.assertTrue(hasattr(cloud_dict, 'name')) self.assertEqual(cloud_dict.name, cloud_dict['name']) diff --git a/shade/tests/unit/test_security_groups.py b/shade/tests/unit/test_security_groups.py index cad328edf..d41628f8d 100644 --- a/shade/tests/unit/test_security_groups.py +++ b/shade/tests/unit/test_security_groups.py @@ -617,7 +617,7 @@ def test_add_security_group_to_server_nova(self): def test_add_security_group_to_server_neutron(self): # fake to get server by name, server-name must match - fake_server = fakes.FakeServer('1234', 'server-name', 'ACTIVE') + fake_server = fakes.make_fake_server('1234', 'server-name', 'ACTIVE') # use neutron for secgroup list and return an existing fake self.cloud.secgroup_source = 'neutron' @@ -669,7 +669,7 @@ def test_remove_security_group_from_server_nova(self): def test_remove_security_group_from_server_neutron(self): # fake to get server by name, server-name must match - fake_server = fakes.FakeServer('1234', 'server-name', 'ACTIVE') + fake_server = fakes.make_fake_server('1234', 'server-name', 'ACTIVE') # use neutron for secgroup list and return an existing fake self.cloud.secgroup_source = 'neutron' @@ -697,8 +697,7 @@ def test_remove_security_group_from_server_neutron(self): def test_add_bad_security_group_to_server_nova(self): # fake to get server by name, server-name must match - fake_server = meta.obj_to_munch( - fakes.FakeServer('1234', 'server-name', 'ACTIVE')) + fake_server = fakes.make_fake_server('1234', 'server-name', 'ACTIVE') # use nova for secgroup list and return an existing fake self.has_neutron = False @@ -724,7 +723,7 @@ def test_add_bad_security_group_to_server_nova(self): def test_add_bad_security_group_to_server_neutron(self): # fake to get server by name, server-name must match - fake_server = fakes.FakeServer('1234', 'server-name', 'ACTIVE') + fake_server = fakes.make_fake_server('1234', 'server-name', 'ACTIVE') # use neutron for secgroup list and return an existing fake self.cloud.secgroup_source = 'neutron' @@ -747,8 +746,7 @@ def test_add_bad_security_group_to_server_neutron(self): def test_add_security_group_to_bad_server(self): # fake to get server by name, server-name must match - fake_server = meta.obj_to_munch( - fakes.FakeServer('1234', 'server-name', 'ACTIVE')) + fake_server = fakes.make_fake_server('1234', 'server-name', 'ACTIVE') self.register_uris([ dict( From 611ce3dca1bf7b320ab075b4f05294c1f24fd4e3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 18 Jun 2017 10:05:19 -0500 Subject: [PATCH 1639/3836] Convert FakeSecGroup to dict Change-Id: I0e0e1b87b0b32e8c015ba1f9944faf3b0e0738c1 --- shade/tests/fakes.py | 46 +++++++++++-- shade/tests/unit/test_security_groups.py | 87 ++++++++++-------------- 2 files changed, 75 insertions(+), 58 deletions(-) diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 8c1ff1543..f9996bab9 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -317,13 +317,45 @@ def __init__(self, id, address, node_id): self.node_id = node_id -class FakeSecgroup(object): - def __init__(self, id, name, description='', project_id=None, rules=None): - self.id = id - self.name = name - self.description = description - self.project_id = project_id - self.rules = rules +def make_fake_neutron_security_group( + id, name, description, rules, project_id=None): + if not rules: + rules = [] + if not project_id: + project_id = PROJECT_ID + return json.loads(json.dumps({ + 'id': id, + 'name': name, + 'description': description, + 'project_id': project_id, + 'tenant_id': project_id, + 'security_group_rules': rules, + })) + + +def make_fake_nova_security_group_rule( + id, from_port, to_port, ip_protocol, cidr): + return json.loads(json.dumps({ + 'id': id, + 'from_port': int(from_port), + 'to_port': int(to_port), + 'ip_protcol': 'tcp', + 'ip_range': { + 'cidr': cidr + } + })) + + +def make_fake_nova_security_group(id, name, description, rules): + if not rules: + rules = [] + return json.loads(json.dumps({ + 'id': id, + 'name': name, + 'description': description, + 'tenant_id': PROJECT_ID, + 'rules': rules, + })) class FakeNovaSecgroupRule(object): diff --git a/shade/tests/unit/test_security_groups.py b/shade/tests/unit/test_security_groups.py index d41628f8d..a4d4f6934 100644 --- a/shade/tests/unit/test_security_groups.py +++ b/shade/tests/unit/test_security_groups.py @@ -14,16 +14,12 @@ import copy import shade -from shade import meta from shade.tests.unit import base from shade.tests import fakes -# TODO(mordred): Make a fakes.make_fake_nova_security_group and a -# fakes.make_fake_nova_security_group and remove all uses of -# meta.obj_to_munch here. Also, we have hardcoded id names - -# move those to using a getUniqueString() value. +# TODO(mordred): Move id and name to using a getUniqueString() value -neutron_grp_obj = fakes.FakeSecgroup( +neutron_grp_dict = fakes.make_fake_neutron_security_group( id='1', name='neutron-sec-group', description='Test Neutron security group', @@ -34,23 +30,18 @@ ) -nova_grp_obj = fakes.FakeSecgroup( +nova_grp_dict = fakes.make_fake_nova_security_group( id='2', name='nova-sec-group', description='Test Nova security group #1', rules=[ - dict(id='2', from_port=8000, to_port=8001, ip_protocol='tcp', - ip_range=dict(cidr='0.0.0.0/0'), parent_group_id=None) + fakes.make_fake_nova_security_group_rule( + id='2', from_port=8000, to_port=8001, ip_protocol='tcp', + cidr='0.0.0.0/0'), ] ) -# Neutron returns dicts instead of objects, so the dict versions should -# be used as expected return values from neutron API methods. -neutron_grp_dict = meta.obj_to_munch(neutron_grp_obj) -nova_grp_dict = meta.obj_to_munch(nova_grp_obj) - - class TestSecurityGroups(base.RequestsMockTestCase): def setUp(self): @@ -162,13 +153,12 @@ def test_delete_security_group_none(self): def test_create_security_group_neutron(self): self.cloud.secgroup_source = 'neutron' group_name = self.getUniqueString() - group_desc = 'security group from test_create_security_group_neutron' - new_group = meta.obj_to_munch( - fakes.FakeSecgroup( - id='2', - name=group_name, - description=group_desc, - rules=[])) + group_desc = self.getUniqueString('description') + new_group = fakes.make_fake_neutron_security_group( + id='2', + name=group_name, + description=group_desc, + rules=[]) self.register_uris([ dict(method='POST', uri=self.get_mock_url( @@ -194,13 +184,12 @@ def test_create_security_group_neutron_specific_tenant(self): group_name = self.getUniqueString() group_desc = 'security group from' \ ' test_create_security_group_neutron_specific_tenant' - new_group = meta.obj_to_munch( - fakes.FakeSecgroup( - id='2', - name=group_name, - description=group_desc, - project_id=project_id, - rules=[])) + new_group = fakes.make_fake_neutron_security_group( + id='2', + name=group_name, + description=group_desc, + project_id=project_id, + rules=[]) self.register_uris([ dict(method='POST', uri=self.get_mock_url( @@ -229,13 +218,12 @@ def test_create_security_group_neutron_specific_tenant(self): def test_create_security_group_nova(self): group_name = self.getUniqueString() self.has_neutron = False - group_desc = 'security group from test_create_security_group_neutron' - new_group = meta.obj_to_munch( - fakes.FakeSecgroup( - id='2', - name=group_name, - description=group_desc, - rules=[])) + group_desc = self.getUniqueString('description') + new_group = fakes.make_fake_nova_security_group( + id='2', + name=group_name, + description=group_desc, + rules=[]) self.register_uris([ dict(method='POST', uri='{endpoint}/os-security-groups'.format( @@ -265,8 +253,8 @@ def test_create_security_group_none(self): def test_update_security_group_neutron(self): self.cloud.secgroup_source = 'neutron' new_name = self.getUniqueString() - sg_id = neutron_grp_obj.id - update_return = meta.obj_to_munch(neutron_grp_obj) + sg_id = neutron_grp_dict['id'] + update_return = neutron_grp_dict.copy() update_return['name'] = new_name self.register_uris([ dict(method='GET', @@ -282,7 +270,7 @@ def test_update_security_group_neutron(self): validate=dict(json={ 'security_group': {'name': new_name}})) ]) - r = self.cloud.update_security_group(neutron_grp_obj.id, name=new_name) + r = self.cloud.update_security_group(sg_id, name=new_name) self.assertEqual(r['name'], new_name) self.assert_calls() @@ -291,7 +279,7 @@ def test_update_security_group_nova(self): new_name = self.getUniqueString() self.cloud.secgroup_source = 'nova' nova_return = [nova_grp_dict] - update_return = meta.obj_to_munch(nova_grp_obj) + update_return = nova_grp_dict.copy() update_return['name'] = new_name self.register_uris([ @@ -305,7 +293,8 @@ def test_update_security_group_nova(self): json={'security_group': update_return}), ]) - r = self.cloud.update_security_group(nova_grp_obj.id, name=new_name) + r = self.cloud.update_security_group( + nova_grp_dict['id'], name=new_name) self.assertEqual(r['name'], new_name) self.assert_calls() @@ -408,9 +397,9 @@ def test_create_security_group_rule_nova(self): nova_return = [nova_grp_dict] - new_rule = meta.obj_to_munch(fakes.FakeNovaSecgroupRule( + new_rule = fakes.make_fake_nova_security_group_rule( id='xyz', from_port=1, to_port=2000, ip_protocol='tcp', - cidr='1.2.3.4/32')) + cidr='1.2.3.4/32') self.register_uris([ dict(method='GET', @@ -442,16 +431,12 @@ def test_create_security_group_rule_nova_no_ports(self): self.has_neutron = False self.cloud.secgroup_source = 'nova' - new_rule = fakes.FakeNovaSecgroupRule( + new_rule = fakes.make_fake_nova_security_group_rule( id='xyz', from_port=1, to_port=65535, ip_protocol='tcp', cidr='1.2.3.4/32') nova_return = [nova_grp_dict] - new_rule = meta.obj_to_munch(fakes.FakeNovaSecgroupRule( - id='xyz', from_port=1, to_port=65535, ip_protocol='tcp', - cidr='1.2.3.4/32')) - self.register_uris([ dict(method='GET', uri='{endpoint}/os-security-groups'.format( @@ -627,7 +612,7 @@ def test_add_security_group_to_server_neutron(self): uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), - json={'servers': [meta.obj_to_munch(fake_server)]}), + json={'servers': [fake_server]}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', @@ -680,7 +665,7 @@ def test_remove_security_group_from_server_neutron(self): uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), - json={'servers': [meta.obj_to_munch(fake_server)]}), + json={'servers': [fake_server]}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', @@ -733,7 +718,7 @@ def test_add_bad_security_group_to_server_neutron(self): uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), - json={'servers': [meta.obj_to_munch(fake_server)]}), + json={'servers': [fake_server]}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', From 3aec23cfd2a0477693e6684a97bfb3abbd3cd937 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 18 Jun 2017 10:34:34 -0500 Subject: [PATCH 1640/3836] Convert server group tests to requests_mock Change-Id: Ic4b137073e0c662d26cac254611708b3f31734e0 --- shade/tests/fakes.py | 13 +++-- shade/tests/unit/test_server_group.py | 74 ++++++++++++++++++--------- 2 files changed, 58 insertions(+), 29 deletions(-) diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index f9996bab9..7f0290ed2 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -258,11 +258,14 @@ def __init__(self, id, pool, ip, fixed_ip, instance_id): self.instance_id = instance_id -class FakeServerGroup(object): - def __init__(self, id, name, policies): - self.id = id - self.name = name - self.policies = policies +def make_fake_server_group(id, name, policies): + return json.loads(json.dumps({ + 'id': id, + 'name': name, + 'policies': policies, + 'members': [], + 'metadata': {}, + })) class FakeVolume(object): diff --git a/shade/tests/unit/test_server_group.py b/shade/tests/unit/test_server_group.py index 87ae9d9b5..31bf05e40 100644 --- a/shade/tests/unit/test_server_group.py +++ b/shade/tests/unit/test_server_group.py @@ -11,32 +11,58 @@ # under the License. -import mock +import uuid -import shade from shade.tests.unit import base from shade.tests import fakes -class TestServerGroup(base.TestCase): - - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_create_server_group(self, mock_nova): - server_group_name = 'my-server-group' - self.cloud.create_server_group(name=server_group_name, - policies=['affinity']) - - mock_nova.server_groups.create.assert_called_once_with( - name=server_group_name, policies=['affinity'] - ) - - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_delete_server_group(self, mock_nova): - mock_nova.server_groups.list.return_value = [ - fakes.FakeServerGroup('1234', 'name', ['affinity']) - ] - self.assertTrue(self.cloud.delete_server_group('1234')) - mock_nova.server_groups.list.assert_called_once_with() - mock_nova.server_groups.delete.assert_called_once_with( - id='1234' - ) +class TestServerGroup(base.RequestsMockTestCase): + + def setUp(self): + super(TestServerGroup, self).setUp() + self.group_id = uuid.uuid4().hex + self.group_name = self.getUniqueString('server-group') + self.policies = ['affinity'] + self.fake_group = fakes.make_fake_server_group( + self.group_id, self.group_name, self.policies) + + def test_create_server_group(self): + + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['os-server-groups']), + json={'server_group': self.fake_group}, + validate=dict( + json={'server_group': { + 'name': self.group_name, + 'policies': self.policies, + }})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', + append=['os-server-groups', self.group_id],), + json={'server_group': self.fake_group}), + ]) + + self.cloud.create_server_group(name=self.group_name, + policies=self.policies) + + self.assert_calls() + + def test_delete_server_group(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-server-groups']), + json={'server_groups': [self.fake_group]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', + append=['os-server-groups', self.group_id]), + json={'server_groups': [self.fake_group]}), + ]) + self.assertTrue(self.cloud.delete_server_group(self.group_name)) + + self.assert_calls() From 0ad08858a543d67f66e6576c49e7988c53038a28 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 18 Jun 2017 10:41:20 -0500 Subject: [PATCH 1641/3836] Convert Server Groups to REST Change-Id: If779c02521d0cf07d43f19ff9a11666838e4ac3b --- shade/_tasks.py | 20 -------------------- shade/openstackcloud.py | 26 +++++++++++++++----------- shade/tests/unit/test_server_group.py | 5 ----- 3 files changed, 15 insertions(+), 36 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 1449ac021..054a0a3cb 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -47,26 +47,6 @@ def main(self, client): return client.keystone_client.users.remove_from_group(**self.args) -class ServerGroupList(task_manager.Task): - def main(self, client): - return client.nova_client.server_groups.list(**self.args) - - -class ServerGroupGet(task_manager.Task): - def main(self, client): - return client.nova_client.server_groups.get(**self.args) - - -class ServerGroupCreate(task_manager.Task): - def main(self, client): - return client.nova_client.server_groups.create(**self.args) - - -class ServerGroupDelete(task_manager.Task): - def main(self, client): - return client.nova_client.server_groups.delete(**self.args) - - class HypervisorList(task_manager.Task): def main(self, client): return client.nova_client.hypervisors.list(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 277821764..e847236fa 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2034,8 +2034,9 @@ def list_server_groups(self): :returns: A list of server group dicts. """ - with _utils.shade_exceptions("Error fetching server group list"): - return self.manager.submit_task(_tasks.ServerGroupList()) + return self._compute_client.get( + '/os-server-groups', + error_message="Error fetching server group list") def get_compute_limits(self, name_or_id=None): """ Get compute limits for a project @@ -5971,11 +5972,14 @@ def create_server_group(self, name, policies): :raises: OpenStackCloudException on operation error. """ - with _utils.shade_exceptions( - "Unable to create server group {name}".format( - name=name)): - return self.manager.submit_task(_tasks.ServerGroupCreate( - name=name, policies=policies)) + return self._compute_client.post( + '/os-server-groups', + json={ + 'server_group': { + 'name': name, + 'policies': policies}}, + error_message="Unable to create server group {name}".format( + name=name)) def delete_server_group(self, name_or_id): """Delete a server group. @@ -5992,10 +5996,10 @@ def delete_server_group(self, name_or_id): name_or_id) return False - with _utils.shade_exceptions( - "Error deleting server group {name}".format(name=name_or_id)): - self.manager.submit_task( - _tasks.ServerGroupDelete(id=server_group['id'])) + self._compute_client.delete( + '/os-server-groups/{id}'.format(id=server_group['id']), + error_message="Error deleting server group {name}".format( + name=name_or_id)) return True diff --git a/shade/tests/unit/test_server_group.py b/shade/tests/unit/test_server_group.py index 31bf05e40..7e2ee2b1f 100644 --- a/shade/tests/unit/test_server_group.py +++ b/shade/tests/unit/test_server_group.py @@ -39,11 +39,6 @@ def test_create_server_group(self): 'name': self.group_name, 'policies': self.policies, }})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', - append=['os-server-groups', self.group_id],), - json={'server_group': self.fake_group}), ]) self.cloud.create_server_group(name=self.group_name, From d69b81fe00627b527a8c999bd9ce7ac9ac689d30 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 18 Jun 2017 10:50:53 -0500 Subject: [PATCH 1642/3836] Convert hypervisor test to requests_mock Change-Id: Ie9acb8582ab52580c90b3511acf5e28eed2f9abf --- shade/tests/fakes.py | 44 +++++++++++++++++++++++++ shade/tests/unit/test_shade_operator.py | 20 +++++++---- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 7f0290ed2..df3336995 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -268,6 +268,50 @@ def make_fake_server_group(id, name, policies): })) +def make_fake_hypervisor(id, name): + return json.loads(json.dumps({ + 'id': id, + 'hypervisor_hostname': name, + 'state': 'up', + 'status': 'enabled', + "cpu_info": { + "arch": "x86_64", + "model": "Nehalem", + "vendor": "Intel", + "features": [ + "pge", + "clflush" + ], + "topology": { + "cores": 1, + "threads": 1, + "sockets": 4 + } + }, + "current_workload": 0, + "status": "enabled", + "state": "up", + "disk_available_least": 0, + "host_ip": "1.1.1.1", + "free_disk_gb": 1028, + "free_ram_mb": 7680, + "hypervisor_type": "fake", + "hypervisor_version": 1000, + "local_gb": 1028, + "local_gb_used": 0, + "memory_mb": 8192, + "memory_mb_used": 512, + "running_vms": 0, + "service": { + "host": "host1", + "id": 7, + "disabled_reason": None + }, + "vcpus": 1, + "vcpus_used": 0 + })) + + class FakeVolume(object): def __init__( self, id, status, name, attachments=[], diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index fd3575e27..43b27b95e 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -1192,17 +1192,23 @@ def test_has_service_yes(self, get_session_mock): get_session_mock.return_value = session_mock self.assertTrue(self.op_cloud.has_service("image")) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_list_hypervisors(self, mock_nova): + def test_list_hypervisors(self): '''This test verifies that calling list_hypervisors results in a call to nova client.''' - mock_nova.hypervisors.list.return_value = [ - fakes.FakeHypervisor('1', 'testserver1'), - fakes.FakeHypervisor('2', 'testserver2'), - ] + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-hypervisors', 'detail']), + json={'hypervisors': [ + fakes.make_fake_hypervisor('1', 'testserver1'), + fakes.make_fake_hypervisor('2', 'testserver2'), + ]}), + ]) r = self.op_cloud.list_hypervisors() - mock_nova.hypervisors.list.assert_called_once_with() + self.assertEqual(2, len(r)) self.assertEqual('testserver1', r[0]['hypervisor_hostname']) self.assertEqual('testserver2', r[1]['hypervisor_hostname']) + + self.assert_calls() From b1faf5bdd12fdd2cb08c0f08e05223f103171dcd Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 18 Jun 2017 10:53:59 -0500 Subject: [PATCH 1643/3836] Convert hypervisor list to REST Change-Id: Ie08b2e2e9ef0b27e9faa27d38934e97c74c76305 --- shade/_tasks.py | 5 ----- shade/operatorcloud.py | 5 +++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 054a0a3cb..40e834deb 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -47,11 +47,6 @@ def main(self, client): return client.keystone_client.users.remove_from_group(**self.args) -class HypervisorList(task_manager.Task): - def main(self, client): - return client.nova_client.hypervisors.list(**self.args) - - class AggregateList(task_manager.Task): def main(self, client): return client.nova_client.aggregates.list(**self.args) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 74f4317d2..efef19a93 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1812,8 +1812,9 @@ def list_hypervisors(self): :returns: A list of hypervisor ``munch.Munch``. """ - with _utils.shade_exceptions("Error fetching hypervisor list"): - return self.manager.submit_task(_tasks.HypervisorList()) + return self._compute_client.get( + '/os-hypervisors/detail', + error_message="Error fetching hypervisor list") def search_aggregates(self, name_or_id=None, filters=None): """Seach host aggregates. From f1778968de1125f48165efb1694be4c1a951aab9 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 18 Jun 2017 11:51:07 -0500 Subject: [PATCH 1644/3836] Convert host aggregate tests to requests_mock Change-Id: Ia2f5ef2798c57c88a0b3735db3381e1fbc20d9fa --- shade/tests/fakes.py | 31 ++-- shade/tests/unit/test_aggregate.py | 276 ++++++++++++++++++++--------- 2 files changed, 208 insertions(+), 99 deletions(-) diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index df3336995..f2f4d1549 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -447,15 +447,22 @@ def __init__(self, zone, id, name, type_, description, self.records = records -class FakeAggregate(object): - def __init__(self, id, name, availability_zone=None, metadata=None, - hosts=None): - self.id = id - self.name = name - self.availability_zone = availability_zone - if not metadata: - metadata = {} - self.metadata = metadata - if not hosts: - hosts = [] - self.hosts = hosts +def make_fake_aggregate(id, name, availability_zone='nova', + metadata=None, hosts=None): + if not metadata: + metadata = {} + if not hosts: + hosts = [] + return json.loads(json.dumps({ + "availability_zone": availability_zone, + "created_at": datetime.datetime.now().isoformat(), + "deleted": False, + "deleted_at": None, + "hosts": hosts, + "id": int(id), + "metadata": { + "availability_zone": availability_zone, + }, + "name": name, + "updated_at": None, + })) diff --git a/shade/tests/unit/test_aggregate.py b/shade/tests/unit/test_aggregate.py index d739e2cd3..16806e2b6 100644 --- a/shade/tests/unit/test_aggregate.py +++ b/shade/tests/unit/test_aggregate.py @@ -10,101 +10,203 @@ # License for the specific language governing permissions and limitations # under the License. - -import mock - -import shade from shade.tests.unit import base from shade.tests import fakes -class TestAggregate(base.TestCase): +class TestAggregate(base.RequestsMockTestCase): + + def setUp(self): + super(TestAggregate, self).setUp() + self.aggregate_name = self.getUniqueString('aggregate') + self.fake_aggregate = fakes.make_fake_aggregate(1, self.aggregate_name) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_create_aggregate(self, mock_nova): - aggregate_name = 'aggr1' - self.op_cloud.create_aggregate(name=aggregate_name) + def test_create_aggregate(self): + create_aggregate = self.fake_aggregate.copy() + del create_aggregate['metadata'] + del create_aggregate['hosts'] - mock_nova.aggregates.create.assert_called_once_with( - name=aggregate_name, availability_zone=None - ) + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates']), + json={'aggregate': create_aggregate}, + validate=dict(json={ + 'aggregate': { + 'name': self.aggregate_name, + 'availability_zone': None, + }})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', + append=['os-aggregates', '1']), + json={'aggregate': self.fake_aggregate}), + ]) + self.op_cloud.create_aggregate(name=self.aggregate_name) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_create_aggregate_with_az(self, mock_nova): - aggregate_name = 'aggr1' + self.assert_calls() + + def test_create_aggregate_with_az(self): availability_zone = 'az1' + az_aggregate = fakes.make_fake_aggregate( + 1, self.aggregate_name, availability_zone=availability_zone) + + create_aggregate = az_aggregate.copy() + del create_aggregate['metadata'] + del create_aggregate['hosts'] + + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates']), + json={'aggregate': create_aggregate}, + validate=dict(json={ + 'aggregate': { + 'name': self.aggregate_name, + 'availability_zone': availability_zone, + }})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', + append=['os-aggregates', '1']), + json={'aggregate': az_aggregate}), + ]) + self.op_cloud.create_aggregate( - name=aggregate_name, availability_zone=availability_zone) - - mock_nova.aggregates.create.assert_called_once_with( - name=aggregate_name, availability_zone=availability_zone - ) - - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_delete_aggregate(self, mock_nova): - mock_nova.aggregates.list.return_value = [ - fakes.FakeAggregate('1234', 'name') - ] - self.assertTrue(self.op_cloud.delete_aggregate('1234')) - mock_nova.aggregates.list.assert_called_once_with() - mock_nova.aggregates.delete.assert_called_once_with( - aggregate='1234' - ) - - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_update_aggregate_set_az(self, mock_nova): - mock_nova.aggregates.list.return_value = [ - fakes.FakeAggregate('1234', 'name') - ] - self.op_cloud.update_aggregate('1234', availability_zone='az') - mock_nova.aggregates.update.assert_called_once_with( - aggregate='1234', - values={'availability_zone': 'az'}, - ) - - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_update_aggregate_unset_az(self, mock_nova): - mock_nova.aggregates.list.return_value = [ - fakes.FakeAggregate('1234', 'name', availability_zone='az') - ] - self.op_cloud.update_aggregate('1234', availability_zone=None) - mock_nova.aggregates.update.assert_called_once_with( - aggregate='1234', - values={'availability_zone': None}, - ) - - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_set_aggregate_metadata(self, mock_nova): - metadata = {'key', 'value'} - mock_nova.aggregates.list.return_value = [ - fakes.FakeAggregate('1234', 'name') - ] - self.op_cloud.set_aggregate_metadata('1234', metadata) - mock_nova.aggregates.set_metadata.assert_called_once_with( - aggregate='1234', - metadata=metadata - ) - - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_add_host_to_aggregate(self, mock_nova): + name=self.aggregate_name, availability_zone=availability_zone) + + self.assert_calls() + + def test_delete_aggregate(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates']), + json={'aggregates': [self.fake_aggregate]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates', '1'])), + ]) + + self.assertTrue(self.op_cloud.delete_aggregate('1')) + + self.assert_calls() + + def test_update_aggregate_set_az(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates']), + json={'aggregates': [self.fake_aggregate]}), + dict(method='PUT', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates', '1']), + json={'aggregate': self.fake_aggregate}, + validate=dict( + json={ + 'aggregate': { + 'availability_zone': 'az', + }})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates', '1']), + json={'aggregate': self.fake_aggregate}), + ]) + + self.op_cloud.update_aggregate(1, availability_zone='az') + + self.assert_calls() + + def test_update_aggregate_unset_az(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates']), + json={'aggregates': [self.fake_aggregate]}), + dict(method='PUT', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates', '1']), + json={'aggregate': self.fake_aggregate}, + validate=dict( + json={ + 'aggregate': { + 'availability_zone': None, + }})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates', '1']), + json={'aggregate': self.fake_aggregate}), + ]) + + self.op_cloud.update_aggregate(1, availability_zone=None) + + self.assert_calls() + + def test_set_aggregate_metadata(self): + metadata = {'key': 'value'} + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates']), + json={'aggregates': [self.fake_aggregate]}), + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', + append=['os-aggregates', '1', 'action']), + json={'aggregate': self.fake_aggregate}, + validate=dict( + json={'set_metadata': {'metadata': metadata}})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates', '1']), + json={'aggregate': self.fake_aggregate}), + ]) + self.op_cloud.set_aggregate_metadata('1', metadata) + + self.assert_calls() + + def test_add_host_to_aggregate(self): hostname = 'host1' - mock_nova.aggregates.list.return_value = [ - fakes.FakeAggregate('1234', 'name') - ] - self.op_cloud.add_host_to_aggregate('1234', hostname) - mock_nova.aggregates.add_host.assert_called_once_with( - aggregate='1234', - host=hostname - ) - - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_remove_host_from_aggregate(self, mock_nova): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates']), + json={'aggregates': [self.fake_aggregate]}), + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', + append=['os-aggregates', '1', 'action']), + json={'aggregate': self.fake_aggregate}, + validate=dict( + json={'add_host': {'host': hostname}})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates', '1']), + json={'aggregate': self.fake_aggregate}), + ]) + self.op_cloud.add_host_to_aggregate('1', hostname) + + self.assert_calls() + + def test_remove_host_from_aggregate(self): hostname = 'host1' - mock_nova.aggregates.list.return_value = [ - fakes.FakeAggregate('1234', 'name', hosts=[hostname]) - ] - self.op_cloud.remove_host_from_aggregate('1234', hostname) - mock_nova.aggregates.remove_host.assert_called_once_with( - aggregate='1234', - host=hostname - ) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates']), + json={'aggregates': [self.fake_aggregate]}), + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', + append=['os-aggregates', '1', 'action']), + json={'aggregate': self.fake_aggregate}, + validate=dict( + json={'remove_host': {'host': hostname}})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates', '1']), + json={'aggregate': self.fake_aggregate}), + ]) + self.op_cloud.remove_host_from_aggregate('1', hostname) + + self.assert_calls() From a2ec277bf5f6ee63b7396894975a104401dfd284 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 18 Jun 2017 12:09:45 -0500 Subject: [PATCH 1645/3836] Convert host aggregates calls to REST Change-Id: Ic250e904e1a51d659c2599591e2b4274ebc5d1c0 --- shade/_tasks.py | 35 -------------- shade/operatorcloud.py | 74 +++++++++++++++++------------- shade/tests/unit/test_aggregate.py | 30 ------------ 3 files changed, 41 insertions(+), 98 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 40e834deb..7b3d53f4f 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -47,41 +47,6 @@ def main(self, client): return client.keystone_client.users.remove_from_group(**self.args) -class AggregateList(task_manager.Task): - def main(self, client): - return client.nova_client.aggregates.list(**self.args) - - -class AggregateCreate(task_manager.Task): - def main(self, client): - return client.nova_client.aggregates.create(**self.args) - - -class AggregateUpdate(task_manager.Task): - def main(self, client): - return client.nova_client.aggregates.update(**self.args) - - -class AggregateDelete(task_manager.Task): - def main(self, client): - return client.nova_client.aggregates.delete(**self.args) - - -class AggregateAddHost(task_manager.Task): - def main(self, client): - return client.nova_client.aggregates.add_host(**self.args) - - -class AggregateRemoveHost(task_manager.Task): - def main(self, client): - return client.nova_client.aggregates.remove_host(**self.args) - - -class AggregateSetMetadata(task_manager.Task): - def main(self, client): - return client.nova_client.aggregates.set_metadata(**self.args) - - class MachineCreate(task_manager.Task): def main(self, client): return client.ironic_client.node.create(**self.args) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index efef19a93..3b039b8d7 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1836,8 +1836,9 @@ def list_aggregates(self): :returns: A list of aggregate dicts. """ - with _utils.shade_exceptions("Error fetching aggregate list"): - return self.manager.submit_task(_tasks.AggregateList()) + return self._compute_client.get( + '/os-aggregates', + error_message="Error fetching aggregate list") def get_aggregate(self, name_or_id, filters=None): """Get an aggregate by name or ID. @@ -1870,11 +1871,14 @@ def create_aggregate(self, name, availability_zone=None): :raises: OpenStackCloudException on operation error. """ - with _utils.shade_exceptions( - "Unable to create host aggregate {name}".format( - name=name)): - return self.manager.submit_task(_tasks.AggregateCreate( - name=name, availability_zone=availability_zone)) + return self._compute_client.post( + '/os-aggregates', + json={'aggregate': { + 'name': name, + 'availability_zone': availability_zone + }}, + error_message="Unable to create host aggregate {name}".format( + name=name)) @_utils.valid_kwargs('name', 'availability_zone') def update_aggregate(self, name_or_id, **kwargs): @@ -1893,13 +1897,11 @@ def update_aggregate(self, name_or_id, **kwargs): raise OpenStackCloudException( "Host aggregate %s not found." % name_or_id) - with _utils.shade_exceptions( - "Error updating aggregate {name}".format(name=name_or_id)): - new_aggregate = self.manager.submit_task( - _tasks.AggregateUpdate( - aggregate=aggregate['id'], values=kwargs)) - - return new_aggregate + return self._compute_client.put( + '/os-aggregates/{id}'.format(id=aggregate['id']), + json={'aggregate': kwargs}, + error_message="Error updating aggregate {name}".format( + name=name_or_id)) def delete_aggregate(self, name_or_id): """Delete a host aggregate. @@ -1915,10 +1917,10 @@ def delete_aggregate(self, name_or_id): self.log.debug("Aggregate %s not found for deleting", name_or_id) return False - with _utils.shade_exceptions( - "Error deleting aggregate {name}".format(name=name_or_id)): - self.manager.submit_task( - _tasks.AggregateDelete(aggregate=aggregate['id'])) + return self._compute_client.delete( + '/os-aggregates/{id}'.format(id=aggregate['id']), + error_message="Error deleting aggregate {name}".format( + name=name_or_id)) return True @@ -1938,11 +1940,13 @@ def set_aggregate_metadata(self, name_or_id, metadata): raise OpenStackCloudException( "Host aggregate %s not found." % name_or_id) - with _utils.shade_exceptions( - "Unable to set metadata for host aggregate {name}".format( - name=name_or_id)): - return self.manager.submit_task(_tasks.AggregateSetMetadata( - aggregate=aggregate['id'], metadata=metadata)) + err_msg = "Unable to set metadata for host aggregate {name}".format( + name=name_or_id) + + return self._compute_client.post( + '/os-aggregates/{id}/action'.format(id=aggregate['id']), + json={'set_metadata': {'metadata': metadata}}, + error_message=err_msg) def add_host_to_aggregate(self, name_or_id, host_name): """Add a host to an aggregate. @@ -1957,11 +1961,13 @@ def add_host_to_aggregate(self, name_or_id, host_name): raise OpenStackCloudException( "Host aggregate %s not found." % name_or_id) - with _utils.shade_exceptions( - "Unable to add host {host} to aggregate {name}".format( - name=name_or_id, host=host_name)): - return self.manager.submit_task(_tasks.AggregateAddHost( - aggregate=aggregate['id'], host=host_name)) + err_msg = "Unable to add host {host} to aggregate {name}".format( + host=host_name, name=name_or_id) + + return self._compute_client.post( + '/os-aggregates/{id}/action'.format(id=aggregate['id']), + json={'add_host': {'host': host_name}}, + error_message=err_msg) def remove_host_from_aggregate(self, name_or_id, host_name): """Remove a host from an aggregate. @@ -1976,11 +1982,13 @@ def remove_host_from_aggregate(self, name_or_id, host_name): raise OpenStackCloudException( "Host aggregate %s not found." % name_or_id) - with _utils.shade_exceptions( - "Unable to remove host {host} from aggregate {name}".format( - name=name_or_id, host=host_name)): - return self.manager.submit_task(_tasks.AggregateRemoveHost( - aggregate=aggregate['id'], host=host_name)) + err_msg = "Unable to remove host {host} to aggregate {name}".format( + host=host_name, name=name_or_id) + + return self._compute_client.post( + '/os-aggregates/{id}/action'.format(id=aggregate['id']), + json={'remove_host': {'host': host_name}}, + error_message=err_msg) def get_volume_type_access(self, name_or_id): """Return a list of volume_type_access. diff --git a/shade/tests/unit/test_aggregate.py b/shade/tests/unit/test_aggregate.py index 16806e2b6..1d832dc4c 100644 --- a/shade/tests/unit/test_aggregate.py +++ b/shade/tests/unit/test_aggregate.py @@ -36,11 +36,6 @@ def test_create_aggregate(self): 'name': self.aggregate_name, 'availability_zone': None, }})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', - append=['os-aggregates', '1']), - json={'aggregate': self.fake_aggregate}), ]) self.op_cloud.create_aggregate(name=self.aggregate_name) @@ -65,11 +60,6 @@ def test_create_aggregate_with_az(self): 'name': self.aggregate_name, 'availability_zone': availability_zone, }})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', - append=['os-aggregates', '1']), - json={'aggregate': az_aggregate}), ]) self.op_cloud.create_aggregate( @@ -107,10 +97,6 @@ def test_update_aggregate_set_az(self): 'aggregate': { 'availability_zone': 'az', }})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['os-aggregates', '1']), - json={'aggregate': self.fake_aggregate}), ]) self.op_cloud.update_aggregate(1, availability_zone='az') @@ -132,10 +118,6 @@ def test_update_aggregate_unset_az(self): 'aggregate': { 'availability_zone': None, }})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['os-aggregates', '1']), - json={'aggregate': self.fake_aggregate}), ]) self.op_cloud.update_aggregate(1, availability_zone=None) @@ -156,10 +138,6 @@ def test_set_aggregate_metadata(self): json={'aggregate': self.fake_aggregate}, validate=dict( json={'set_metadata': {'metadata': metadata}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['os-aggregates', '1']), - json={'aggregate': self.fake_aggregate}), ]) self.op_cloud.set_aggregate_metadata('1', metadata) @@ -179,10 +157,6 @@ def test_add_host_to_aggregate(self): json={'aggregate': self.fake_aggregate}, validate=dict( json={'add_host': {'host': hostname}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['os-aggregates', '1']), - json={'aggregate': self.fake_aggregate}), ]) self.op_cloud.add_host_to_aggregate('1', hostname) @@ -202,10 +176,6 @@ def test_remove_host_from_aggregate(self): json={'aggregate': self.fake_aggregate}, validate=dict( json={'remove_host': {'host': hostname}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['os-aggregates', '1']), - json={'aggregate': self.fake_aggregate}), ]) self.op_cloud.remove_host_from_aggregate('1', hostname) From 56244f54103c076444a1e8793fa78b6753350961 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 18 Jun 2017 12:22:27 -0500 Subject: [PATCH 1646/3836] Convert remaining nova tests to requests_mock Change-Id: Iff4341f2c83493c901f5e18570f82b0b5f7f3ad5 --- shade/tests/unit/test_limits.py | 79 +++++++++++++++++++++++---- shade/tests/unit/test_quotas.py | 94 +++++++++++++++++++++++---------- shade/tests/unit/test_usage.py | 45 +++++++++++++--- 3 files changed, 174 insertions(+), 44 deletions(-) diff --git a/shade/tests/unit/test_limits.py b/shade/tests/unit/test_limits.py index 6bc7ce710..3eeb9fc0f 100644 --- a/shade/tests/unit/test_limits.py +++ b/shade/tests/unit/test_limits.py @@ -9,26 +9,87 @@ # 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 shade from shade.tests.unit import base class TestLimits(base.RequestsMockTestCase): - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_get_compute_limits(self, mock_nova): + def test_get_compute_limits(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['limits']), + json={ + "limits": { + "absolute": { + "maxImageMeta": 128, + "maxPersonality": 5, + "maxPersonalitySize": 10240, + "maxSecurityGroupRules": 20, + "maxSecurityGroups": 10, + "maxServerMeta": 128, + "maxTotalCores": 20, + "maxTotalFloatingIps": 10, + "maxTotalInstances": 10, + "maxTotalKeypairs": 100, + "maxTotalRAMSize": 51200, + "maxServerGroups": 10, + "maxServerGroupMembers": 10, + "totalCoresUsed": 0, + "totalInstancesUsed": 0, + "totalRAMUsed": 0, + "totalSecurityGroupsUsed": 0, + "totalFloatingIpsUsed": 0, + "totalServerGroupsUsed": 0 + }, + "rate": [] + } + }), + ]) + self.cloud.get_compute_limits() - mock_nova.limits.get.assert_called_once_with() + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_other_get_compute_limits(self, mock_nova): + def test_other_get_compute_limits(self): project = self.mock_for_keystone_projects(project_count=1, list_get=True)[0] + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['limits'], + qs_elements=[ + 'tenant_id={id}'.format(id=project.project_id) + ]), + json={ + "limits": { + "absolute": { + "maxImageMeta": 128, + "maxPersonality": 5, + "maxPersonalitySize": 10240, + "maxSecurityGroupRules": 20, + "maxSecurityGroups": 10, + "maxServerMeta": 128, + "maxTotalCores": 20, + "maxTotalFloatingIps": 10, + "maxTotalInstances": 10, + "maxTotalKeypairs": 100, + "maxTotalRAMSize": 51200, + "maxServerGroups": 10, + "maxServerGroupMembers": 10, + "totalCoresUsed": 0, + "totalInstancesUsed": 0, + "totalRAMUsed": 0, + "totalSecurityGroupsUsed": 0, + "totalFloatingIpsUsed": 0, + "totalServerGroupsUsed": 0 + }, + "rate": [] + } + }), + ]) + self.op_cloud.get_compute_limits(project.project_id) - mock_nova.limits.get.assert_called_once_with( - tenant_id=project.project_id) self.assert_calls() diff --git a/shade/tests/unit/test_quotas.py b/shade/tests/unit/test_quotas.py index cdf44f0b4..1a9768398 100644 --- a/shade/tests/unit/test_quotas.py +++ b/shade/tests/unit/test_quotas.py @@ -10,63 +10,99 @@ # License for the specific language governing permissions and limitations # under the License. - -import mock -from novaclient import exceptions as nova_exceptions - -import shade from shade import exc from shade.tests.unit import base +fake_quota_set = { + "cores": 20, + "fixed_ips": -1, + "floating_ips": 10, + "injected_file_content_bytes": 10240, + "injected_file_path_bytes": 255, + "injected_files": 5, + "instances": 10, + "key_pairs": 100, + "metadata_items": 128, + "ram": 51200, + "security_group_rules": 20, + "security_groups": 45, + "server_groups": 10, + "server_group_members": 10 +} + class TestQuotas(base.RequestsMockTestCase): def setUp(self, cloud_config_fixture='clouds.yaml'): super(TestQuotas, self).setUp( cloud_config_fixture=cloud_config_fixture) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_update_quotas(self, mock_nova): + def test_update_quotas(self): project = self.mock_for_keystone_projects(project_count=1, list_get=True)[0] - # re-mock the list-get as the call to set_compute_quotas when - # bad-request is raised, still calls out to get the project data. - self.mock_for_keystone_projects(project=project, list_get=True) + + self.register_uris([ + dict(method='PUT', + uri=self.get_mock_url( + 'compute', 'public', + append=['os-quota-sets', project.project_id]), + json={'quota_set': fake_quota_set}, + validate=dict( + json={ + 'quota_set': { + 'cores': 1, + 'force': True + }})), + ]) self.op_cloud.set_compute_quotas(project.project_id, cores=1) - mock_nova.quotas.update.assert_called_once_with( - cores=1, force=True, tenant_id=project.project_id) + self.assert_calls() + + def test_update_quotas_bad_request(self): + project = self.mock_for_keystone_projects(project_count=1, + list_get=True)[0] + + self.register_uris([ + dict(method='PUT', + uri=self.get_mock_url( + 'compute', 'public', + append=['os-quota-sets', project.project_id]), + status_code=400), + ]) - mock_nova.quotas.update.side_effect = nova_exceptions.BadRequest(400) self.assertRaises(exc.OpenStackCloudException, - self.op_cloud.set_compute_quotas, project) + self.op_cloud.set_compute_quotas, project.project_id) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_get_quotas(self, mock_nova): + def test_get_quotas(self): project = self.mock_for_keystone_projects(project_count=1, list_get=True)[0] + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', + append=['os-quota-sets', project.project_id]), + json={'quota_set': fake_quota_set}), + ]) + self.op_cloud.get_compute_quotas(project.project_id) - mock_nova.quotas.get.assert_called_once_with( - tenant_id=project.project_id) + self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_delete_quotas(self, mock_nova): + def test_delete_quotas(self): project = self.mock_for_keystone_projects(project_count=1, list_get=True)[0] - # re-mock the list-get as the call to set_delete_compute_quotas when - # bad-request is raised, still calls out to get the project data. - self.mock_for_keystone_projects(project=project, list_get=True) - self.op_cloud.delete_compute_quotas(project.project_id) + self.register_uris([ + dict(method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', + append=['os-quota-sets', project.project_id])), + ]) - mock_nova.quotas.delete.assert_called_once_with( - tenant_id=project.project_id) + self.op_cloud.delete_compute_quotas(project.project_id) - mock_nova.quotas.delete.side_effect = nova_exceptions.BadRequest(400) - self.assertRaises(exc.OpenStackCloudException, - self.op_cloud.delete_compute_quotas, project) self.assert_calls() def test_cinder_update_quotas(self): diff --git a/shade/tests/unit/test_usage.py b/shade/tests/unit/test_usage.py index 8a7710a2c..9643f9ba2 100644 --- a/shade/tests/unit/test_usage.py +++ b/shade/tests/unit/test_usage.py @@ -12,21 +12,54 @@ # License for the specific language governing permissions and limitations # under the License. import datetime -import mock +import uuid -import shade from shade.tests.unit import base class TestUsage(base.RequestsMockTestCase): - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - def test_get_usage(self, mock_nova): + def test_get_usage(self): project = self.mock_for_keystone_projects(project_count=1, list_get=True)[0] start = end = datetime.datetime.now() + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', + append=['os-simple-tenant-usage', project.project_id], + qs_elements=[ + 'start={now}'.format(now=start.isoformat()), + 'end={now}'.format(now=end.isoformat()), + ]), + json={"tenant_usage": { + "server_usages": [ + { + "ended_at": None, + "flavor": "m1.tiny", + "hours": 1.0, + "instance_id": uuid.uuid4().hex, + "local_gb": 1, + "memory_mb": 512, + "name": "instance-2", + "started_at": "2012-10-08T20:10:44.541277", + "state": "active", + "tenant_id": "6f70656e737461636b20342065766572", + "uptime": 3600, + "vcpus": 1 + } + ], + "start": "2012-10-08T20:10:44.587336", + "stop": "2012-10-08T21:10:44.587336", + "tenant_id": "6f70656e737461636b20342065766572", + "total_hours": 1.0, + "total_local_gb_usage": 1.0, + "total_memory_mb_usage": 512.0, + "total_vcpus_usage": 1.0 + }}) + ]) + self.op_cloud.get_compute_usage(project.project_id, start, end) - mock_nova.usage.get.assert_called_once_with( - start=start, end=end, tenant_id=project.project_id) self.assert_calls() From 56524c16a82f0b75cb9e576eb74982d8b19fb105 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 18 Jun 2017 12:50:24 -0500 Subject: [PATCH 1647/3836] Translate final nova calls to REST Change-Id: I89e0e59ec1ed6a81843da61bd3fce49d57da7c17 --- shade/_tasks.py | 25 ------------------------- shade/openstackcloud.py | 9 +++------ shade/operatorcloud.py | 37 ++++++++++++++----------------------- 3 files changed, 17 insertions(+), 54 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 7b3d53f4f..c944347a4 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -255,28 +255,3 @@ def main(self, client): class RolesForUser(task_manager.Task): def main(self, client): return client.keystone_client.roles.roles_for_user(**self.args) - - -class NovaQuotasSet(task_manager.Task): - def main(self, client): - return client.nova_client.quotas.update(**self.args) - - -class NovaQuotasGet(task_manager.Task): - def main(self, client): - return client.nova_client.quotas.get(**self.args) - - -class NovaQuotasDelete(task_manager.Task): - def main(self, client): - return client.nova_client.quotas.delete(**self.args) - - -class NovaUsageGet(task_manager.Task): - def main(self, client): - return client.nova_client.usage.get(**self.args) - - -class NovaLimitsGet(task_manager.Task): - def main(self, client): - return client.nova_client.limits.get(**self.args).to_dict() diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index e847236fa..12bd1784d 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2047,7 +2047,7 @@ def get_compute_limits(self, name_or_id=None): :returns: Munch object with the limits """ - kwargs = {} + params = {} project_id = None error_msg = "Failed to get limits" if name_or_id: @@ -2056,14 +2056,11 @@ def get_compute_limits(self, name_or_id=None): if not proj: raise OpenStackCloudException("project does not exist") project_id = proj.id - kwargs['tenant_id'] = project_id + params['tenant_id'] = project_id error_msg = "{msg} for the project: {project} ".format( msg=error_msg, project=name_or_id) - with _utils.shade_exceptions(error_msg): - # TODO(mordred) Before we convert this to REST, we need to add - # in support for running calls with a different project context - limits = self.manager.submit_task(_tasks.NovaLimitsGet(**kwargs)) + limits = self._compute_client.get('/limits', params=params) return self._normalize_compute_limits(limits, project_id=project_id) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 3b039b8d7..2ded0fb07 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -15,7 +15,6 @@ import jsonpatch from ironicclient import exceptions as ironic_exceptions -from novaclient import exceptions as nova_exceptions from shade.exc import * # noqa from shade import meta @@ -2075,13 +2074,11 @@ def set_compute_quotas(self, name_or_id, **kwargs): # volume_quotas = {key: val for key, val in kwargs.items() # if key in quota.VOLUME_QUOTAS} - try: - self.manager.submit_task( - _tasks.NovaQuotasSet(tenant_id=proj.id, - force=True, - **kwargs)) - except nova_exceptions.BadRequest: - raise OpenStackCloudException("No valid quota or resource") + kwargs['force'] = True + self._compute_client.put( + '/os-quota-sets/{project}'.format(project=proj.id), + json={'quota_set': kwargs}, + error_message="No valid quota or resource") def get_compute_quotas(self, name_or_id): """ Get quota for a project @@ -2094,11 +2091,8 @@ def get_compute_quotas(self, name_or_id): proj = self.get_project(name_or_id) if not proj: raise OpenStackCloudException("project does not exist") - try: - return self.manager.submit_task( - _tasks.NovaQuotasGet(tenant_id=proj.id)) - except nova_exceptions.BadRequest: - raise OpenStackCloudException("nova client call failed") + return self._compute_client.get( + '/os-quota-sets/{project}'.format(project=proj.id)) def delete_compute_quotas(self, name_or_id): """ Delete quota for a project @@ -2112,11 +2106,8 @@ def delete_compute_quotas(self, name_or_id): proj = self.get_project(name_or_id) if not proj: raise OpenStackCloudException("project does not exist") - try: - return self.manager.submit_task( - _tasks.NovaQuotasDelete(tenant_id=proj.id)) - except nova_exceptions.BadRequest: - raise OpenStackCloudException("nova client call failed") + return self._compute_client.delete( + '/os-quota-sets/{project}'.format(project=proj.id)) def get_compute_usage(self, name_or_id, start=None, end=None): """ Get usage for a specific project @@ -2173,11 +2164,11 @@ def parse_datetime_for_nova(date): raise OpenStackCloudException("project does not exist: {}".format( name=proj.id)) - with _utils.shade_exceptions( - "Unable to get resources usage for project: {name}".format( - name=proj.id)): - usage = self.manager.submit_task( - _tasks.NovaUsageGet(tenant_id=proj.id, start=start, end=end)) + usage = self._compute_client.get( + '/os-simple-tenant-usage/{project}'.format(project=proj.id), + params=dict(start=start.isoformat(), end=end.isoformat()), + error_message="Unable to get usage for project: {name}".format( + name=proj.id)) return self._normalize_compute_usage(usage) From 3aee4f5e6ea3ec4a49442c905a4bd43b1e7b13a9 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 18 Jun 2017 12:54:05 -0500 Subject: [PATCH 1648/3836] Remove novaclient from shade's dependencies All calls to nova are now done via REST. This means we can remove the dependency. Only two more to go ... Change-Id: I01a0afef5986b7452fd73e04c48568ebb9817681 --- extras/install-tips.sh | 1 - releasenotes/notes/remove-novaclient-3f8d4db20d5f9582.yaml | 5 +++++ requirements.txt | 1 - shade/__init__.py | 5 ----- shade/_legacy_clients.py | 3 +-- shade/_utils.py | 5 ----- shade/tests/base.py | 6 ------ shade/tests/unit/test_create_server.py | 6 +++--- shade/tests/unit/test_rebuild_server.py | 4 ++-- shade/tests/unit/test_server_set_metadata.py | 4 ---- shade/tests/unit/test_update_server.py | 2 +- 11 files changed, 12 insertions(+), 30 deletions(-) create mode 100644 releasenotes/notes/remove-novaclient-3f8d4db20d5f9582.yaml diff --git a/extras/install-tips.sh b/extras/install-tips.sh index 50c2fd8da..d96773ac4 100644 --- a/extras/install-tips.sh +++ b/extras/install-tips.sh @@ -15,7 +15,6 @@ # limitations under the License. for lib in \ - python-novaclient \ python-keystoneclient \ python-ironicclient \ os-client-config \ diff --git a/releasenotes/notes/remove-novaclient-3f8d4db20d5f9582.yaml b/releasenotes/notes/remove-novaclient-3f8d4db20d5f9582.yaml new file mode 100644 index 000000000..bd0ffbf4a --- /dev/null +++ b/releasenotes/notes/remove-novaclient-3f8d4db20d5f9582.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - All Nova interactions are done via direct REST calls. + python-novaclient is no longer a direct dependency of + shade. diff --git a/requirements.txt b/requirements.txt index 8f25d2a85..fbf6c0081 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,7 +21,6 @@ iso8601>=0.1.11 # MIT keystoneauth1>=2.21.0 # Apache-2.0 netifaces>=0.10.4 # MIT -python-novaclient>=9.0.0 # Apache-2.0 python-keystoneclient>=3.8.0 # Apache-2.0 python-ironicclient>=1.11.0 # Apache-2.0 diff --git a/shade/__init__.py b/shade/__init__.py index 3d5af0401..35e7ee98e 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -53,11 +53,6 @@ def simple_logging(debug=False, http_debug=False): log = _log.setup_logging('keystoneauth') log.addHandler(logging.StreamHandler()) log.setLevel(log_level) - # Enable HTTP level tracing of novaclient - logger = logging.getLogger('novaclient') - log.addHandler(logging.StreamHandler()) - log.setLevel(log_level) - logger.propagate = False # We only want extra shade HTTP tracing in http debug mode log = _log.setup_logging('shade.http') log.setLevel(log_level) diff --git a/shade/_legacy_clients.py b/shade/_legacy_clients.py index 4b5ec1066..c18eea1a4 100644 --- a/shade/_legacy_clients.py +++ b/shade/_legacy_clients.py @@ -65,8 +65,7 @@ def neutron_client(self): @property def nova_client(self): - return self._create_legacy_client( - 'nova', 'compute', version='2.0', deprecated=False) + return self._create_legacy_client('nova', 'compute', version='2.0') @property def glance_client(self): diff --git a/shade/_utils.py b/shade/_utils.py index 1e7e768db..bf8abd6f7 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -25,7 +25,6 @@ import time from decorator import decorator -from novaclient import exceptions as nova_exc from shade import _log from shade import exc @@ -445,10 +444,6 @@ def shade_exceptions(error_message=None): yield except exc.OpenStackCloudException: raise - except nova_exc.BadRequest as e: - if error_message is None: - error_message = str(e) - raise exc.OpenStackCloudBadRequest(error_message) except Exception as e: if error_message is None: error_message = str(e) diff --git a/shade/tests/base.py b/shade/tests/base.py index 73d1e86bb..81e6450bc 100644 --- a/shade/tests/base.py +++ b/shade/tests/base.py @@ -76,12 +76,6 @@ def setUp(self): logger.addHandler(handler) logger.propagate = False - # Enable HTTP level tracing - logger = logging.getLogger('novaclient') - logger.setLevel(logging.DEBUG) - logger.addHandler(handler) - logger.propagate = False - def assertEqual(self, first, second, *args, **kwargs): '''Munch aware wrapper''' if isinstance(first, munch.Munch): diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index 4ab000aa3..ed1dab4b8 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -31,8 +31,8 @@ class TestCreateServer(base.RequestsMockTestCase): def test_create_server_with_get_exception(self): """ - Test that an exception when attempting to get the server instance via - the novaclient raises an exception in create_server. + Test that a bad status code when attempting to get the server instance + raises an exception in create_server. """ build_server = fakes.make_fake_server('1234', '', 'BUILD') self.register_uris([ @@ -173,7 +173,7 @@ def test_create_server_with_timeout(self): def test_create_server_no_wait(self): """ Test that create_server with no wait and no exception in the - novaclient create call returns the server instance. + create call returns the server instance. """ fake_server = fakes.make_fake_server('1234', '', 'BUILD') self.register_uris([ diff --git a/shade/tests/unit/test_rebuild_server.py b/shade/tests/unit/test_rebuild_server.py index dd588a26b..aae725245 100644 --- a/shade/tests/unit/test_rebuild_server.py +++ b/shade/tests/unit/test_rebuild_server.py @@ -41,7 +41,7 @@ def setUp(self): def test_rebuild_server_rebuild_exception(self): """ - Test that an exception in the novaclient rebuild raises an exception in + Test that an exception in the rebuild raises an exception in rebuild_server. """ self.register_uris([ @@ -120,7 +120,7 @@ def test_rebuild_server_timeout(self): def test_rebuild_server_no_wait(self): """ Test that rebuild_server with no wait and no exception in the - novaclient rebuild call returns the server instance. + rebuild call returns the server instance. """ self.register_uris([ dict(method='POST', diff --git a/shade/tests/unit/test_server_set_metadata.py b/shade/tests/unit/test_server_set_metadata.py index 2af4787af..0b32ae6e8 100644 --- a/shade/tests/unit/test_server_set_metadata.py +++ b/shade/tests/unit/test_server_set_metadata.py @@ -34,10 +34,6 @@ def setUp(self): self.server_id, self.server_name) def test_server_set_metadata_with_exception(self): - """ - Test that a generic exception in the novaclient delete_meta raises - an exception in delete_server_metadata. - """ self.register_uris([ dict(method='GET', uri=self.get_mock_url( diff --git a/shade/tests/unit/test_update_server.py b/shade/tests/unit/test_update_server.py index ec50694be..2a94c13c1 100644 --- a/shade/tests/unit/test_update_server.py +++ b/shade/tests/unit/test_update_server.py @@ -36,7 +36,7 @@ def setUp(self): def test_update_server_with_update_exception(self): """ - Test that an exception in the novaclient update raises an exception in + Test that an exception in the update raises an exception in update_server. """ self.register_uris([ From 5512e46933b894d447c4b19917e1cd1cfebae6f7 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 19 Jun 2017 08:17:43 -0500 Subject: [PATCH 1649/3836] base64 encode user_data sent to create server user_data is base64 encoded, which novaclient did for us previously. Since we no longer use novaclient, we need to base64 encode it ourselves. Change-Id: Idf2320f43ba81b3ddc3b1c0016c7f872b5c5319b --- shade/openstackcloud.py | 20 ++++++++++++- shade/tests/unit/test_create_server.py | 40 ++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 12bd1784d..c2dc6c8cd 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -10,6 +10,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import base64 import collections import functools import hashlib @@ -5434,6 +5435,20 @@ def _get_boot_from_volume_kwargs( self.list_volumes.invalidate(self) return kwargs + def _encode_server_userdata(self, userdata): + if hasattr(userdata, 'read'): + userdata = userdata.read() + + if not isinstance(userdata, six.binary_type): + # If the userdata passed in is bytes, just send it unmodified + if not isinstance(text, six.string_types): + raise TypeError("%s can't be encoded" % type(text)) + # If it's not bytes, make it bytes + userdata = userdata.encode('utf-8', 'strict') + + # Once we have base64 bytes, make them into a utf-8 string for REST + return base64.b64encode(userdata).decode('utf-8') + @_utils.valid_kwargs( 'meta', 'files', 'userdata', 'reservation_id', 'return_raw', 'min_count', @@ -5538,10 +5553,13 @@ def create_server( kwargs['security_groups'] = [] for group in security_groups: kwargs['security_groups'].append(dict(name=group)) + if 'userdata' in kwargs: + user_data = kwargs.pop('userdata') + if user_data: + kwargs['user_data'] = self._encode_server_userdata(user_data) for (desired, given) in ( ('OS-DCF:diskConfig', 'disk_config'), ('metadata', 'meta'), - ('user_data', 'userdata'), ('adminPass', 'admin_pass')): value = kwargs.pop(given, None) if value: diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index ed1dab4b8..a4023f556 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -16,6 +16,7 @@ Tests for the `create_server` command. """ +import base64 import uuid import mock @@ -293,6 +294,45 @@ def test_create_server_with_admin_pass_wait(self, mock_wait): ) self.assert_calls() + def test_create_server_user_data_base64(self): + """ + Test that a server passed user-data sends it base64 encoded. + """ + user_data = self.getUniqueString('user_data') + user_data_b64 = base64.b64encode(user_data).decode('utf-8') + fake_server = fakes.make_fake_server('1234', '', 'BUILD') + fake_server['user_data'] = user_data + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': []}), + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers']), + json={'server': fake_server}, + validate=dict( + json={'server': { + u'flavorRef': u'flavor-id', + u'imageRef': u'image-id', + u'max_count': 1, + u'min_count': 1, + u'user_data': user_data_b64, + u'name': u'server-name'}})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234']), + json={'server': fake_server}), + ]) + + self.cloud.create_server( + name='server-name', image=dict(id='image-id'), + flavor=dict(id='flavor-id'), + userdata=user_data, wait=False) + + self.assert_calls() + @mock.patch.object(shade.OpenStackCloud, "get_active_server") @mock.patch.object(shade.OpenStackCloud, "get_server") def test_wait_for_server(self, mock_get_server, mock_get_active_server): From 410a4558db80874dd34352ebc2c761b18404f0a7 Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Mon, 19 Jun 2017 19:37:27 +0000 Subject: [PATCH 1650/3836] Don't remove top-container element for server REST API calls Change-Id: Ic44e99b95a546bbc62562634340f14033ae1e346 Signed-off-by: Rosario Di Somma --- shade/_adapter.py | 2 +- shade/openstackcloud.py | 30 +++++++++++++++++------------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/shade/_adapter.py b/shade/_adapter.py index 408d09456..7bbb5330a 100644 --- a/shade/_adapter.py +++ b/shade/_adapter.py @@ -139,7 +139,7 @@ def _munch_response(self, response, result_key=None, error_message=None): 'security_group', 'security_groups', 'security_group_rule', 'security_group_rules', 'users', 'user', 'projects', 'tenants', - 'project', 'tenant']: + 'project', 'tenant', 'servers', 'server']: if key in result_json.keys(): self._log_request_id(response) return result_json diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index b2e419155..715610bf6 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2016,10 +2016,10 @@ def _list_servers(self, detailed=False, all_projects=False, bare=False): params = {} if all_projects: params['all_tenants'] = True + data = self._compute_client.get( + '/servers/detail', params=params, error_message=error_msg) servers = self._normalize_servers( - self._compute_client.get( - '/servers/detail', params=params, error_message=error_msg)) - + meta.get_and_munchify('servers', data)) return [ self._expand_server(server, detailed, bare) for server in servers @@ -2800,8 +2800,9 @@ def _expand_server(self, server, detailed, bare): return meta.add_server_interfaces(self, server) def get_server_by_id(self, id): - return meta.add_server_interfaces(self, self._normalize_server( - self._compute_client.get('/servers/{id}'.format(id=id)))) + data = self._compute_client.get('/servers/{id}'.format(id=id)) + server = meta.get_and_munchify('server', data) + return meta.add_server_interfaces(self, self._normalize_server(server)) def get_server_group(self, name_or_id=None, filters=None): """Get a server group by name or ID. @@ -5639,8 +5640,9 @@ def create_server( if 'block_device_mapping_v2' in kwargs: endpoint = '/os-volumes_boot' with _utils.shade_exceptions("Error in creating instance"): - server = self._compute_client.post( + data = self._compute_client.post( endpoint, json={'server': kwargs}) + server = meta.get_and_munchify('server', data) admin_pass = server.get('adminPass') or kwargs.get('admin_pass') if not wait: # This is a direct get call to skip the list_servers @@ -5758,10 +5760,11 @@ def rebuild_server(self, server_id, image_id, admin_pass=None, if admin_pass: kwargs['adminPass'] = admin_pass - server = self._compute_client.post( + data = self._compute_client.post( '/servers/{server_id}/action'.format(server_id=server_id), error_message="Error in rebuilding instance", json={'rebuild': kwargs}) + server = meta.get_and_munchify('server', data) if not wait: return self._expand_server( self._normalize_server(server), bare=bare, detailed=detailed) @@ -5952,12 +5955,13 @@ def update_server(self, name_or_id, detailed=False, bare=False, **kwargs): raise OpenStackCloudException( "failed to find server '{server}'".format(server=name_or_id)) - return self._expand_server(self._normalize_server( - self._compute_client.put( - '/servers/{server_id}'.format(server_id=server['id']), - error_message="Error updating server {0}".format(name_or_id), - json={'server': kwargs})), - bare=bare, detailed=detailed) + data = self._compute_client.put( + '/servers/{server_id}'.format(server_id=server['id']), + error_message="Error updating server {0}".format(name_or_id), + json={'server': kwargs}) + server = self._normalize_server( + meta.get_and_munchify('server', data)) + return self._expand_server(server, bare=bare, detailed=detailed) def create_server_group(self, name, policies): """Create a new server group. From 10beaf9c43a19220b61b7e979f6e46f0056410c5 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 20 Jun 2017 07:42:59 -0500 Subject: [PATCH 1651/3836] Return an empty list on FIP listing failure We don't want a 404 - we want an empty list. Change-Id: Ia30077d4093b224485b9953c5ad9d2d7c811ec50 --- shade/openstackcloud.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index e181190b3..ef3d67a46 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2190,7 +2190,10 @@ def _neutron_list_floating_ips(self, filters=None): return meta.get_and_munchify('floatingips', data) def _nova_list_floating_ips(self): - data = self._compute_client.get('/os-floating-ips') + try: + data = self._compute_client.get('/os-floating-ips') + except OpenStackCloudURINotFound: + return [] return meta.get_and_munchify('floating_ips', data) def use_external_network(self): From 2486e09b67c20254f780a1f2cee941c4345553d1 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 20 Jun 2017 07:44:30 -0500 Subject: [PATCH 1652/3836] Don't try to delete fips on non-fip clouds There is no need to even go through the logic. We have a flag for this. Change-Id: I103a2f346344d7a1eed5c54b58d279f04a1f76a3 --- shade/openstackcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index ef3d67a46..77cfbeb55 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5889,7 +5889,7 @@ def _delete_server( if not server: return False - if delete_ips: + if delete_ips and self._has_floating_ips(): # TODO(mordred) Does the server have floating ips in its # addresses dict? If not, skip this. ips = self.search_floating_ips(filters={ From 45e689942676f52f5a405262b41c0d60a0d681ab Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 20 Jun 2017 14:47:24 -0500 Subject: [PATCH 1653/3836] Only search for floating ips if the server has them We have a server dict, which has an addresses dict. If the addresses dict does not list any floating ips, then there is no need to spend the API call to look for the fip ids needed to do the delete. Change-Id: Iaf7583fe29bd197a59ae2728fd52d8de2ac1d411 --- shade/openstackcloud.py | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index e0a5d37c5..a657f4662 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5886,6 +5886,26 @@ def delete_server( server, wait=wait, timeout=timeout, delete_ips=delete_ips, delete_ip_retry=delete_ip_retry) + def _delete_server_floating_ips(self, server, delete_ip_retry): + # Does the server have floating ips in its + # addresses dict? If not, skip this. + server_floats = meta.find_nova_interfaces( + server['addresses'], ext_tag='floating') + if not server_floats: + return + ips = self.search_floating_ips(filters={ + 'device_id': server['id']}) + for ip in ips: + deleted = self.delete_floating_ip( + ip['id'], retry=delete_ip_retry) + if not deleted: + raise OpenStackCloudException( + "Tried to delete floating ip {floating_ip}" + " associated with server {id} but there was" + " an error deleting it. Not deleting server.".format( + floating_ip=ip['floating_ip_address'], + id=server['id'])) + def _delete_server( self, server, wait=False, timeout=180, delete_ips=False, delete_ip_retry=1): @@ -5893,20 +5913,7 @@ def _delete_server( return False if delete_ips and self._has_floating_ips(): - # TODO(mordred) Does the server have floating ips in its - # addresses dict? If not, skip this. - ips = self.search_floating_ips(filters={ - 'device_id': server['id']}) - for ip in ips: - deleted = self.delete_floating_ip( - ip['id'], retry=delete_ip_retry) - if not deleted: - raise OpenStackCloudException( - "Tried to delete floating ip {floating_ip}" - " associated with server {id} but there was" - " an error deleting it. Not deleting server.".format( - floating_ip=floating_ip, - id=server['id'])) + self._delete_server_floating_ips(server, delete_ip_retry) try: self._compute_client.delete( From 6334250493624aa56a7062c394442026dea5dfca Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 20 Jun 2017 14:37:49 -0500 Subject: [PATCH 1654/3836] Don't fail hard on 404 from neutron FIP listing We do a filtered FIP listing from _delete_servers, which has exposed our list_floating_ips method as dying hard when it can't find a match. That's pretty inconsistent with the rest of the API - return a [] in that case but keep logging the big error message. The user should really mark the cloud has floating_ip_source: None if they want the error message to go away - which is indicated in the error message. Change-Id: I76493e7cc004831c1a0620de9fbe004fdbc39e45 --- shade/openstackcloud.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index e0a5d37c5..236df1d9d 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -59,6 +59,7 @@ DEFAULT_SERVER_AGE = 5 DEFAULT_PORT_AGE = 5 DEFAULT_FLOAT_AGE = 5 +_OCC_DOC_URL = "https://docs.openstack.org/developer/os-client-config" OBJECT_CONTAINER_ACLS = { @@ -2131,15 +2132,32 @@ def _list_floating_ips(self, filters=None): self._neutron_list_floating_ips(filters)) except OpenStackCloudURINotFound as e: # Nova-network don't support server-side floating ips - # filtering, so it's safer to die hard than to fallback to Nova - # which may return more results that expected. + # filtering, so it's safer to return and empty list than + # to fallback to Nova which may return more results that + # expected. if filters: self.log.error( - "Something went wrong talking to neutron API. Can't " - "fallback to Nova since it doesn't support server-side" - " filtering when listing floating ips." + "Neutron returned NotFound for floating IPs, which" + " means this cloud doesn't have neutron floating ips." + " shade can't fallback to trying Nova since nova" + " doesn't support server-side filtering when listing" + " floating ips and filters were given. If you do not" + " think shade should be attempting to list floating" + " ips on neutron, it is possible to control the" + " behavior by setting floating_ip_source to 'nova' or" + " None for cloud: %(cloud)s. If you are not already" + " using clouds.yaml to configure settings for your" + " cloud(s), and you want to configure this setting," + " you will need a clouds.yaml file. For more" + " information, please see %(doc_url)s", { + 'cloud': self.name, + 'doc_url': _OCC_DOC_URL, + } ) - raise + # We can't fallback to nova because we push-down filters. + # We got a 404 which means neutron doesn't exist. If the + # user + return [] self.log.debug( "Something went wrong talking to neutron API: " "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) From 16cd2cc88b19130769b6dd5b7f3c3ce8752bdb9d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 20 Jun 2017 23:49:03 +0100 Subject: [PATCH 1655/3836] Fix config_drive, scheduler_hints and key_name in create_server We don't want to pass these if they're None. Turns out - we don't want to pass them if they're False either. Change-Id: I421b4e537892deead1866ddc979565e86fbcb211 --- .../fix-config-drive-a148b7589f7e1022.yaml | 6 ++ shade/openstackcloud.py | 4 + shade/tests/functional/test_compute.py | 38 +++++++++ shade/tests/unit/test_create_server.py | 77 +++++++++++++++++++ 4 files changed, 125 insertions(+) create mode 100644 releasenotes/notes/fix-config-drive-a148b7589f7e1022.yaml diff --git a/releasenotes/notes/fix-config-drive-a148b7589f7e1022.yaml b/releasenotes/notes/fix-config-drive-a148b7589f7e1022.yaml new file mode 100644 index 000000000..cd08b87cf --- /dev/null +++ b/releasenotes/notes/fix-config-drive-a148b7589f7e1022.yaml @@ -0,0 +1,6 @@ +--- +issues: + - Fixed an issue where nodepool could cause config_drive + to be passed explicitly as None, which was getting directly + passed through to the JSON. Also fix the same logic for key_name + and scheduler_hints while we're in there. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index a657f4662..3efef0832 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5560,11 +5560,15 @@ def create_server( kwargs['user_data'] = self._encode_server_userdata(user_data) for (desired, given) in ( ('OS-DCF:diskConfig', 'disk_config'), + ('os:scheduler_hints', 'scheduler_hints'), + ('config_drive', 'config_drive'), + ('key_name', 'key_name'), ('metadata', 'meta'), ('adminPass', 'admin_pass')): value = kwargs.pop(given, None) if value: kwargs[desired] = value + kwargs.setdefault('max_count', kwargs.get('max_count', 1)) kwargs.setdefault('min_count', kwargs.get('min_count', 1)) diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index c4c1e4be0..45883c782 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -84,6 +84,44 @@ def test_attach_detach_volume(self): self.assertTrue(vol_attachment[key]) # assert string is not empty self.assertIsNone(self.user_cloud.detach_volume(server, volume)) + def test_create_and_delete_server_with_config_drive(self): + self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) + server = self.user_cloud.create_server( + name=self.server_name, + image=self.image, + flavor=self.flavor, + config_drive=True, + wait=True) + self.assertEqual(self.server_name, server['name']) + self.assertEqual(self.image.id, server['image']['id']) + self.assertEqual(self.flavor.id, server['flavor']['id']) + self.assertEqual(True, server['has_config_drive']) + self.assertIsNotNone(server['adminPass']) + self.assertTrue( + self.user_cloud.delete_server(self.server_name, wait=True)) + self.assertIsNone(self.user_cloud.get_server(self.server_name)) + + def test_create_and_delete_server_with_config_drive_none(self): + # check that we're not sending invalid values for config_drive + # if it's passed in explicitly as None - which nodepool does if it's + # not set in the config + self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) + server = self.user_cloud.create_server( + name=self.server_name, + image=self.image, + flavor=self.flavor, + config_drive=None, + wait=True) + self.assertEqual(self.server_name, server['name']) + self.assertEqual(self.image.id, server['image']['id']) + self.assertEqual(self.flavor.id, server['flavor']['id']) + self.assertEqual(False, server['has_config_drive']) + self.assertIsNotNone(server['adminPass']) + self.assertTrue( + self.user_cloud.delete_server( + self.server_name, wait=True)) + self.assertIsNone(self.user_cloud.get_server(self.server_name)) + def test_list_all_servers(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) server = self.user_cloud.create_server( diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index a4023f556..308c20cfc 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -209,6 +209,83 @@ def test_create_server_no_wait(self): self.assert_calls() + def test_create_server_config_drive(self): + """ + Test that config_drive gets passed in properly + """ + fake_server = fakes.make_fake_server('1234', '', 'BUILD') + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': []}), + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers']), + json={'server': fake_server}, + validate=dict( + json={'server': { + u'flavorRef': u'flavor-id', + u'imageRef': u'image-id', + u'config_drive': True, + u'max_count': 1, + u'min_count': 1, + u'name': u'server-name'}})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234']), + json={'server': fake_server}), + ]) + normalized = self.cloud._expand_server( + self.cloud._normalize_server(fake_server), False, False) + self.assertEqual( + normalized, + self.cloud.create_server( + name='server-name', + image=dict(id='image-id'), + flavor=dict(id='flavor-id'), + config_drive=True)) + + self.assert_calls() + + def test_create_server_config_drive_none(self): + """ + Test that config_drive gets not passed in properly + """ + fake_server = fakes.make_fake_server('1234', '', 'BUILD') + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': []}), + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers']), + json={'server': fake_server}, + validate=dict( + json={'server': { + u'flavorRef': u'flavor-id', + u'imageRef': u'image-id', + u'max_count': 1, + u'min_count': 1, + u'name': u'server-name'}})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234']), + json={'server': fake_server}), + ]) + normalized = self.cloud._expand_server( + self.cloud._normalize_server(fake_server), False, False) + self.assertEqual( + normalized, + self.cloud.create_server( + name='server-name', + image=dict(id='image-id'), + flavor=dict(id='flavor-id'), + config_drive=None)) + + self.assert_calls() + def test_create_server_with_admin_pass_no_wait(self): """ Test that a server with an admin_pass passed returns the password From 74522a1a224a5379649645ad57e916fe003638bd Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 20 Jun 2017 16:10:01 -0500 Subject: [PATCH 1656/3836] Fix delete_ips on delete_server and add tests This codepath was basically completely untested and essentially completely broken. Fix the break, but also add tests for it. Also add tests for config_drive while we're in there. Change-Id: I9f44474000213bab9e08266a376e41fecc4fbc1f --- .../fix-delete-ips-1d4eebf7bc4d4733.yaml | 6 + shade/openstackcloud.py | 17 ++- shade/tests/functional/test_compute.py | 17 +++ shade/tests/unit/test_delete_server.py | 123 ++++++++++++++++++ 4 files changed, 158 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/fix-delete-ips-1d4eebf7bc4d4733.yaml diff --git a/releasenotes/notes/fix-delete-ips-1d4eebf7bc4d4733.yaml b/releasenotes/notes/fix-delete-ips-1d4eebf7bc4d4733.yaml new file mode 100644 index 000000000..7d8199dee --- /dev/null +++ b/releasenotes/notes/fix-delete-ips-1d4eebf7bc4d4733.yaml @@ -0,0 +1,6 @@ +--- +issues: + - Fixed the logic in delete_ips and added regression + tests to cover it. The old logic was incorrectly looking + for floating ips using port syntax. It was also not + swallowing errors when it should. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 3efef0832..ef818235c 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5895,11 +5895,18 @@ def _delete_server_floating_ips(self, server, delete_ip_retry): # addresses dict? If not, skip this. server_floats = meta.find_nova_interfaces( server['addresses'], ext_tag='floating') - if not server_floats: - return - ips = self.search_floating_ips(filters={ - 'device_id': server['id']}) - for ip in ips: + for fip in server_floats: + try: + ip = self.get_floating_ip(id=None, filters={ + 'floating_ip_address': fip['addr']}) + except OpenStackCloudURINotFound: + # We're deleting. If it doesn't exist - awesome + # NOTE(mordred) If the cloud is a nova FIP cloud but + # floating_ip_source is set to neutron, this + # can lead to a FIP leak. + continue + if not ip: + continue deleted = self.delete_floating_ip( ip['id'], retry=delete_ip_retry) if not deleted: diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index 45883c782..d52dfedf7 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -71,6 +71,23 @@ def test_create_and_delete_server(self): self.user_cloud.delete_server(self.server_name, wait=True)) self.assertIsNone(self.user_cloud.get_server(self.server_name)) + def test_create_and_delete_server_auto_ip_delete_ips(self): + self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) + server = self.user_cloud.create_server( + name=self.server_name, + image=self.image, + flavor=self.flavor, + auto_ip=True, + wait=True) + self.assertEqual(self.server_name, server['name']) + self.assertEqual(self.image.id, server['image']['id']) + self.assertEqual(self.flavor.id, server['flavor']['id']) + self.assertIsNotNone(server['adminPass']) + self.assertTrue( + self.user_cloud.delete_server( + self.server_name, wait=True, delete_ips=True)) + self.assertIsNone(self.user_cloud.get_server(self.server_name)) + def test_attach_detach_volume(self): server_name = self.getUniqueString() self.addCleanup(self._cleanup_servers_and_volumes, server_name) diff --git a/shade/tests/unit/test_delete_server.py b/shade/tests/unit/test_delete_server.py index bcbe0497b..0e558e509 100644 --- a/shade/tests/unit/test_delete_server.py +++ b/shade/tests/unit/test_delete_server.py @@ -16,6 +16,7 @@ Tests for the `delete_server` command. """ +import uuid from shade import exc as shade_exc from shade.tests import fakes @@ -140,3 +141,125 @@ def fake_has_service(service_type): self.assertTrue(self.cloud.delete_server('porky', wait=False)) self.assert_calls() + + def test_delete_server_delete_ips(self): + """ + Test that deleting server and fips works + """ + server = fakes.make_fake_server('1234', 'porky', 'ACTIVE') + fip_id = uuid.uuid4().hex + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [server]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips.json'], + qs_elements=['floating_ip_address=172.24.5.5']), + complete_qs=True, + json={'floatingips': [{ + 'router_id': 'd23abc8d-2991-4a55-ba98-2aaea84cc72f', + 'tenant_id': '4969c491a3c74ee4af974e6d800c62de', + 'floating_network_id': '376da547-b977-4cfe-9cba7', + 'fixed_ip_address': '10.0.0.4', + 'floating_ip_address': '172.24.5.5', + 'port_id': 'ce705c24-c1ef-408a-bda3-7bbd946164ac', + 'id': fip_id, + 'status': 'ACTIVE'}]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'floatingips', + '{fip_id}.json'.format(fip_id=fip_id)])), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips.json']), + complete_qs=True, + json={'floatingips': []}), + dict(method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'])), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': []}), + ]) + self.assertTrue(self.cloud.delete_server( + 'porky', wait=True, delete_ips=True)) + + self.assert_calls() + + def test_delete_server_delete_ips_bad_neutron(self): + """ + Test that deleting server with a borked neutron doesn't bork + """ + server = fakes.make_fake_server('1234', 'porky', 'ACTIVE') + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [server]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips.json'], + qs_elements=['floating_ip_address=172.24.5.5']), + complete_qs=True, + status_code=404), + dict(method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'])), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': []}), + ]) + self.assertTrue(self.cloud.delete_server( + 'porky', wait=True, delete_ips=True)) + + self.assert_calls() + + def test_delete_server_delete_fips_nova(self): + """ + Test that deleting server with a borked neutron doesn't bork + """ + self.cloud._floating_ip_source = 'nova' + server = fakes.make_fake_server('1234', 'porky', 'ACTIVE') + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [server]}), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-floating-ips']), + json={'floating_ips': [ + { + 'fixed_ip': None, + 'id': 1, + 'instance_id': None, + 'ip': '172.24.5.5', + 'pool': 'nova' + }]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', append=['os-floating-ips', '1'])), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-floating-ips']), + json={'floating_ips': []}), + dict(method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'])), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': []}), + ]) + self.assertTrue(self.cloud.delete_server( + 'porky', wait=True, delete_ips=True)) + + self.assert_calls() From bc942953bc4a39c7696624dab6b59d06ad4f3298 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 21 Jun 2017 02:15:12 +0100 Subject: [PATCH 1657/3836] Fix image normalization when image has properties property In the somewhat pathological case of an image having a property named properties already that contains a non-dict value, image normalizaiton becomes quite unhappy. Fix it. Change-Id: I2ce4dd4e3b7a4a9b46ff2bf699d2c8517e1ce7ce Story: 2001073 Task: 4697 --- ...properties-key-conflict-2161ca1faaad6731.yaml | 4 ++++ shade/_normalize.py | 9 ++++++++- shade/tests/unit/test_image.py | 16 ++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/fix-properties-key-conflict-2161ca1faaad6731.yaml diff --git a/releasenotes/notes/fix-properties-key-conflict-2161ca1faaad6731.yaml b/releasenotes/notes/fix-properties-key-conflict-2161ca1faaad6731.yaml new file mode 100644 index 000000000..d681f93ca --- /dev/null +++ b/releasenotes/notes/fix-properties-key-conflict-2161ca1faaad6731.yaml @@ -0,0 +1,4 @@ +--- +issues: + - Images in the cloud with a string property named "properties" + caused image normalization to bomb. diff --git a/shade/_normalize.py b/shade/_normalize.py index 825a20d25..b8e242b56 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -268,7 +268,13 @@ def _normalize_image(self, image): # Discard noise self._remove_novaclient_artifacts(image) + # If someone made a property called "properties" that contains a + # string (this has happened at least one time in the wild), the + # the rest of the normalization here goes belly up. properties = image.pop('properties', {}) + if not isinstance(properties, dict): + properties = {'properties': properties} + visibility = image.pop('visibility', None) protected = _to_bool(image.pop('protected', False)) @@ -318,7 +324,8 @@ def _normalize_image(self, image): # Backwards compat with glance if not self.strict_mode: for key, val in properties.items(): - new_image[key] = val + if key != 'properties': + new_image[key] = val new_image['protected'] = protected new_image['metadata'] = properties new_image['created'] = new_image['created_at'] diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index 4d16fc01d..0b2032bce 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -136,6 +136,22 @@ def test_list_images(self): self.cloud.list_images()) self.assert_calls() + def test_list_images_string_properties(self): + image_dict = self.fake_image_dict.copy() + image_dict['properties'] = 'list,of,properties' + self.register_uris([ + dict(method='GET', uri='https://image.example.com/v2/images', + json={'images': [image_dict]}), + ]) + images = self.cloud.list_images() + self.assertEqual( + self.cloud._normalize_images([image_dict]), + images) + self.assertEqual( + images[0]['properties']['properties'], + 'list,of,properties') + self.assert_calls() + def test_list_images_paginated(self): marker = str(uuid.uuid4()) self.register_uris([ From 14216dc5c9b7ab338d264d96b02db6c1f902b634 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 21 Jun 2017 02:39:00 +0100 Subject: [PATCH 1658/3836] Remove a direct mocking of _image_client Change-Id: I21505fc5b0b043f9c32c3ee568b48db9c32ff23e --- shade/tests/unit/test_caching.py | 33 +++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 72acd8300..0f5600ff0 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -459,26 +459,29 @@ def test_list_flavors(self): self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, '_image_client') - def test_list_images(self, mock_image_client): - mock_image_client.get.return_value = [] - self.assertEqual([], self.cloud.list_images()) + def test_list_images(self): + + self.use_glance() + fake_image = fakes.make_fake_image(image_id='42') - fake_image = munch.Munch( - id='42', status='success', name='42 name', - container_format='bare', - disk_format='qcow2', - properties={ - 'owner_specified.shade.md5': mock.ANY, - 'owner_specified.shade.sha256': mock.ANY, - 'owner_specified.shade.object': 'images/42 name', - 'is_public': False}) - mock_image_client.get.return_value = [fake_image] + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url('image', 'public', + append=['v2', 'images']), + json={'images': []}), + dict(method='GET', + uri=self.get_mock_url('image', 'public', + append=['v2', 'images']), + json={'images': [fake_image]}), + ]) + + self.assertEqual([], self.cloud.list_images()) self.assertEqual([], self.cloud.list_images()) self.cloud.list_images.invalidate(self.cloud) self.assertEqual( self._munch_images(fake_image), self.cloud.list_images()) - mock_image_client.get.assert_called_with('/images') + + self.assert_calls() @mock.patch.object(shade.OpenStackCloud, '_image_client') def test_list_images_ignores_unsteady_status(self, mock_image_client): From 1069d44810c57aaa3191ec77dc766001c7bd6327 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 21 Jun 2017 04:20:35 +0100 Subject: [PATCH 1659/3836] Fix mismatch between port and port-id for REST call The old novaclient parameter is port-id, but the rest wants port. We should really stop having so much logic in os_server. Story: 2001075 Task: 4700 Change-Id: Ia16f8effca705e04b01fee5c2e899d90d084acb4 --- shade/openstackcloud.py | 2 ++ shade/tests/unit/test_create_server.py | 36 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index ef818235c..3a487da97 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5624,6 +5624,8 @@ def create_server( for key in ('port', 'fixed_ip'): if key in nic: net[key] = nic.pop(key) + if 'port-id' in nic: + net['port'] = nic.pop('port-id') if nic: raise OpenStackCloudException( "Additional unsupported keys given for server network" diff --git a/shade/tests/unit/test_create_server.py b/shade/tests/unit/test_create_server.py index 308c20cfc..0e2855e7c 100644 --- a/shade/tests/unit/test_create_server.py +++ b/shade/tests/unit/test_create_server.py @@ -676,6 +676,42 @@ def test_create_server_get_flavor_image(self): self.assert_calls() + def test_create_server_nics_port_id(self): + '''Verify port-id in nics input turns into port in REST.''' + build_server = fakes.make_fake_server('1234', '', 'BUILD') + active_server = fakes.make_fake_server('1234', '', 'BUILD') + image_id = uuid.uuid4().hex + port_id = uuid.uuid4().hex + + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers']), + json={'server': build_server}, + validate=dict( + json={'server': { + u'flavorRef': fakes.FLAVOR_ID, + u'imageRef': image_id, + u'max_count': 1, + u'min_count': 1, + u'networks': [{u'port': port_id}], + u'name': u'server-name'}})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234']), + json={'server': active_server}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': []}), + ]) + + self.cloud.create_server( + 'server-name', dict(id=image_id), dict(id=fakes.FLAVOR_ID), + nics=[{'port-id': port_id}], wait=False) + + self.assert_calls() + def test_create_boot_attach_volume(self): build_server = fakes.make_fake_server('1234', '', 'BUILD') active_server = fakes.make_fake_server('1234', '', 'BUILD') From 95f44f171c614cb0bb1650eb0062baea5c96f25d Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Thu, 22 Jun 2017 15:15:52 -0400 Subject: [PATCH 1660/3836] rearrange existing documentation to follow the new standard layout Depends-On: Ia750cb049c0f53a234ea70ce1f2bbbb7a2aa9454 Change-Id: Ib68812eda4ab0ce09c6d438f9f236111d61ef38b Signed-off-by: Doug Hellmann --- README.rst | 481 +----------------- .../index.rst} | 2 +- doc/source/index.rst | 28 +- .../{installation.rst => install/index.rst} | 0 .../index.rst} | 0 doc/source/user/configuration.rst | 303 +++++++++++ doc/source/user/index.rst | 12 + doc/source/{ => user}/network-config.rst | 0 doc/source/{ => user}/releasenotes.rst | 0 doc/source/user/using.rst | 182 +++++++ doc/source/{ => user}/vendor-support.rst | 0 11 files changed, 520 insertions(+), 488 deletions(-) rename doc/source/{contributing.rst => contributor/index.rst} (50%) rename doc/source/{installation.rst => install/index.rst} (100%) rename doc/source/{api-reference.rst => reference/index.rst} (100%) create mode 100644 doc/source/user/configuration.rst create mode 100644 doc/source/user/index.rst rename doc/source/{ => user}/network-config.rst (100%) rename doc/source/{ => user}/releasenotes.rst (100%) create mode 100644 doc/source/user/using.rst rename doc/source/{ => user}/vendor-support.rst (100%) diff --git a/README.rst b/README.rst index 67aa91a34..35ff07b43 100644 --- a/README.rst +++ b/README.rst @@ -16,489 +16,10 @@ have to know extra info to use OpenStack * If you have environment variables, you will get a cloud named `envvars` * If you have neither, you will get a cloud named `defaults` with base defaults -Environment Variables ---------------------- - -`os-client-config` honors all of the normal `OS_*` variables. It does not -provide backwards compatibility to service-specific variables such as -`NOVA_USERNAME`. - -If you have OpenStack environment variables set, `os-client-config` will produce -a cloud config object named `envvars` containing your values from the -environment. If you don't like the name `envvars`, that's ok, you can override -it by setting `OS_CLOUD_NAME`. - -Service specific settings, like the nova service type, are set with the -default service type as a prefix. For instance, to set a special service_type -for trove set - -.. code-block:: bash - - export OS_DATABASE_SERVICE_TYPE=rax:database - -Config Files ------------- - -`os-client-config` will look for a file called `clouds.yaml` in the following -locations: - -* Current Directory -* ~/.config/openstack -* /etc/openstack - -The first file found wins. - -You can also set the environment variable `OS_CLIENT_CONFIG_FILE` to an -absolute path of a file to look for and that location will be inserted at the -front of the file search list. - -The keys are all of the keys you'd expect from `OS_*` - except lower case -and without the OS prefix. So, region name is set with `region_name`. - -Service specific settings, like the nova service type, are set with the -default service type as a prefix. For instance, to set a special service_type -for trove (because you're using Rackspace) set: - -.. code-block:: yaml - - database_service_type: 'rax:database' - - -Site Specific File Locations -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In addition to `~/.config/openstack` and `/etc/openstack` - some platforms -have other locations they like to put things. `os-client-config` will also -look in an OS specific config dir - -* `USER_CONFIG_DIR` -* `SITE_CONFIG_DIR` - -`USER_CONFIG_DIR` is different on Linux, OSX and Windows. - -* Linux: `~/.config/openstack` -* OSX: `~/Library/Application Support/openstack` -* Windows: `C:\\Users\\USERNAME\\AppData\\Local\\OpenStack\\openstack` - -`SITE_CONFIG_DIR` is different on Linux, OSX and Windows. - -* Linux: `/etc/openstack` -* OSX: `/Library/Application Support/openstack` -* Windows: `C:\\ProgramData\\OpenStack\\openstack` - -An example config file is probably helpful: - -.. code-block:: yaml - - clouds: - mtvexx: - profile: vexxhost - auth: - username: mordred@inaugust.com - password: XXXXXXXXX - project_name: mordred@inaugust.com - region_name: ca-ymq-1 - dns_api_version: 1 - mordred: - region_name: RegionOne - auth: - username: 'mordred' - password: XXXXXXX - project_name: 'shade' - auth_url: 'https://montytaylor-sjc.openstack.blueboxgrid.com:5001/v2.0' - infra: - profile: rackspace - auth: - username: openstackci - password: XXXXXXXX - project_id: 610275 - regions: - - DFW - - ORD - - IAD - -You may note a few things. First, since `auth_url` settings are silly -and embarrassingly ugly, known cloud vendor profile information is included and -may be referenced by name. One of the benefits of that is that `auth_url` -isn't the only thing the vendor defaults contain. For instance, since -Rackspace lists `rax:database` as the service type for trove, `os-client-config` -knows that so that you don't have to. In case the cloud vendor profile is not -available, you can provide one called `clouds-public.yaml`, following the same -location rules previously mentioned for the config files. - -`regions` can be a list of regions. When you call `get_all_clouds`, -you'll get a cloud config object for each cloud/region combo. - -As seen with `dns_service_type`, any setting that makes sense to be per-service, -like `service_type` or `endpoint` or `api_version` can be set by prefixing -the setting with the default service type. That might strike you funny when -setting `service_type` and it does me too - but that's just the world we live -in. - -Auth Settings -------------- - -Keystone has auth plugins - which means it's not possible to know ahead of time -which auth settings are needed. `os-client-config` sets the default plugin type -to `password`, which is what things all were before plugins came about. In -order to facilitate validation of values, all of the parameters that exist -as a result of a chosen plugin need to go into the auth dict. For password -auth, this includes `auth_url`, `username` and `password` as well as anything -related to domains, projects and trusts. - -Splitting Secrets ------------------ - -In some scenarios, such as configuration management controlled environments, -it might be easier to have secrets in one file and non-secrets in another. -This is fully supported via an optional file `secure.yaml` which follows all -the same location rules as `clouds.yaml`. It can contain anything you put -in `clouds.yaml` and will take precedence over anything in the `clouds.yaml` -file. - -.. code-block:: yaml - - # clouds.yaml - clouds: - internap: - profile: internap - auth: - username: api-55f9a00fb2619 - project_name: inap-17037 - regions: - - ams01 - - nyj01 - # secure.yaml - clouds: - internap: - auth: - password: XXXXXXXXXXXXXXXXX - -SSL Settings ------------- - -When the access to a cloud is done via a secure connection, `os-client-config` -will always verify the SSL cert by default. This can be disabled by setting -`verify` to `False`. In case the cert is signed by an unknown CA, a specific -cacert can be provided via `cacert`. **WARNING:** `verify` will always have -precedence over `cacert`, so when setting a CA cert but disabling `verify`, the -cloud cert will never be validated. - -Client certs are also configurable. `cert` will be the client cert file -location. In case the cert key is not included within the client cert file, -its file location needs to be set via `key`. - -.. code-block:: yaml - - # clouds.yaml - clouds: - secure: - auth: ... - key: /home/myhome/client-cert.key - cert: /home/myhome/client-cert.crt - cacert: /home/myhome/ca.crt - insecure: - auth: ... - verify: False - -Cache Settings --------------- - -Accessing a cloud is often expensive, so it's quite common to want to do some -client-side caching of those operations. To facilitate that, `os-client-config` -understands passing through cache settings to dogpile.cache, with the following -behaviors: - -* Listing no config settings means you get a null cache. -* `cache.expiration_time` and nothing else gets you memory cache. -* Otherwise, `cache.class` and `cache.arguments` are passed in - -Different cloud behaviors are also differently expensive to deal with. If you -want to get really crazy and tweak stuff, you can specify different expiration -times on a per-resource basis by passing values, in seconds to an expiration -mapping keyed on the singular name of the resource. A value of `-1` indicates -that the resource should never expire. - -`os-client-config` does not actually cache anything itself, but it collects -and presents the cache information so that your various applications that -are connecting to OpenStack can share a cache should you desire. - -.. code-block:: yaml - - cache: - class: dogpile.cache.pylibmc - expiration_time: 3600 - arguments: - url: - - 127.0.0.1 - expiration: - server: 5 - flavor: -1 - clouds: - mtvexx: - profile: vexxhost - auth: - username: mordred@inaugust.com - password: XXXXXXXXX - project_name: mordred@inaugust.com - region_name: ca-ymq-1 - dns_api_version: 1 - - -IPv6 ----- - -IPv6 is the future, and you should always use it if your cloud supports it and -if your local network supports it. Both of those are easily detectable and all -friendly software should do the right thing. However, sometimes you might -exist in a location where you have an IPv6 stack, but something evil has -caused it to not actually function. In that case, there is a config option -you can set to unbreak you `force_ipv4`, or `OS_FORCE_IPV4` boolean -environment variable. - -.. code-block:: yaml - - client: - force_ipv4: true - clouds: - mtvexx: - profile: vexxhost - auth: - username: mordred@inaugust.com - password: XXXXXXXXX - project_name: mordred@inaugust.com - region_name: ca-ymq-1 - dns_api_version: 1 - monty: - profile: rax - auth: - username: mordred@inaugust.com - password: XXXXXXXXX - project_name: mordred@inaugust.com - region_name: DFW - -The above snippet will tell client programs to prefer returning an IPv4 -address. - -Per-region settings -------------------- - -Sometimes you have a cloud provider that has config that is common to the -cloud, but also with some things you might want to express on a per-region -basis. For instance, Internap provides a public and private network specific -to the user in each region, and putting the values of those networks into -config can make consuming programs more efficient. - -To support this, the region list can actually be a list of dicts, and any -setting that can be set at the cloud level can be overridden for that -region. - -.. code-block:: yaml - - clouds: - internap: - profile: internap - auth: - password: XXXXXXXXXXXXXXXXX - username: api-55f9a00fb2619 - project_name: inap-17037 - regions: - - name: ams01 - values: - networks: - - name: inap-17037-WAN1654 - routes_externally: true - - name: inap-17037-LAN6745 - - name: nyj01 - values: - networks: - - name: inap-17037-WAN1654 - routes_externally: true - - name: inap-17037-LAN6745 - -Usage ------ - -The simplest and least useful thing you can do is: - -.. code-block:: python - - python -m os_client_config.config - -Which will print out whatever if finds for your config. If you want to use -it from python, which is much more likely what you want to do, things like: - -Get a named cloud. - -.. code-block:: python - - import os_client_config - - cloud_config = os_client_config.OpenStackConfig().get_one_cloud( - 'internap', region_name='ams01') - print(cloud_config.name, cloud_config.region, cloud_config.config) - -Or, get all of the clouds. - -.. code-block:: python - - import os_client_config - - cloud_config = os_client_config.OpenStackConfig().get_all_clouds() - for cloud in cloud_config: - print(cloud.name, cloud.region, cloud.config) - -argparse --------- - -If you're using os-client-config from a program that wants to process -command line options, there is a registration function to register the -arguments that both os-client-config and keystoneauth know how to deal -with - as well as a consumption argument. - -.. code-block:: python - - import argparse - import sys - - import os_client_config - - cloud_config = os_client_config.OpenStackConfig() - parser = argparse.ArgumentParser() - cloud_config.register_argparse_arguments(parser, sys.argv) - - options = parser.parse_args() - - cloud = cloud_config.get_one_cloud(argparse=options) - -Constructing OpenStack SDK object ---------------------------------- - -If what you want to do is get an OpenStack SDK Connection and you want it to -do all the normal things related to clouds.yaml, `OS_` environment variables, -a helper function is provided. The following will get you a fully configured -`openstacksdk` instance. - -.. code-block:: python - - import os_client_config - - sdk = os_client_config.make_sdk() - -If you want to do the same thing but on a named cloud. - -.. code-block:: python - - import os_client_config - - sdk = os_client_config.make_sdk(cloud='mtvexx') - -If you want to do the same thing but also support command line parsing. - -.. code-block:: python - - import argparse - - import os_client_config - - sdk = os_client_config.make_sdk(options=argparse.ArgumentParser()) - -It should be noted that OpenStack SDK has ways to construct itself that allow -for additional flexibility. If the helper function here does not meet your -needs, you should see the `from_config` method of -`openstack.connection.Connection `_ - -Constructing shade objects --------------------------- - -If what you want to do is get a -`shade `_ OpenStackCloud object, a -helper function that honors clouds.yaml and `OS_` environment variables is -provided. The following will get you a fully configured `OpenStackCloud` -instance. - -.. code-block:: python - - import os_client_config - - cloud = os_client_config.make_shade() - -If you want to do the same thing but on a named cloud. - -.. code-block:: python - - import os_client_config - - cloud = os_client_config.make_shade(cloud='mtvexx') - -If you want to do the same thing but also support command line parsing. - -.. code-block:: python - - import argparse - - import os_client_config - - cloud = os_client_config.make_shade(options=argparse.ArgumentParser()) - -Constructing REST API Clients ------------------------------ - -What if you want to make direct REST calls via a Session interface? You're -in luck. A similar interface is available as with `openstacksdk` and `shade`. -The main difference is that you need to specify which service you want to -talk to and `make_rest_client` will return you a keystoneauth Session object -that is mounted on the endpoint for the service you're looking for. - -.. code-block:: python - - import os_client_config - - session = os_client_config.make_rest_client('compute', cloud='vexxhost') - - response = session.get('/servers') - server_list = response.json()['servers'] - -Constructing Legacy Client objects ----------------------------------- - -If you want get an old-style Client object from a python-\*client library, -and you want it to do all the normal things related to clouds.yaml, `OS_` -environment variables, a helper function is also provided. The following -will get you a fully configured `novaclient` instance. - -.. code-block:: python - - import os_client_config - - nova = os_client_config.make_client('compute') - -If you want to do the same thing but on a named cloud. - -.. code-block:: python - - import os_client_config - - nova = os_client_config.make_client('compute', cloud='mtvexx') - -If you want to do the same thing but also support command line parsing. - -.. code-block:: python - - import argparse - - import os_client_config - - nova = os_client_config.make_client( - 'compute', options=argparse.ArgumentParser()) - -If you want to get fancier than that in your python, then the rest of the -API is available to you. But often times, you just want to do the one thing. - Source ------ * Free software: Apache license -* Documentation: http://docs.openstack.org/developer/os-client-config +* Documentation: http://docs.openstack.org/os-client-config/latest * Source: http://git.openstack.org/cgit/openstack/os-client-config * Bugs: http://bugs.launchpad.net/os-client-config diff --git a/doc/source/contributing.rst b/doc/source/contributor/index.rst similarity index 50% rename from doc/source/contributing.rst rename to doc/source/contributor/index.rst index ed77c1262..2aa070771 100644 --- a/doc/source/contributing.rst +++ b/doc/source/contributor/index.rst @@ -1,4 +1,4 @@ ============ Contributing ============ -.. include:: ../../CONTRIBUTING.rst \ No newline at end of file +.. include:: ../../../CONTRIBUTING.rst diff --git a/doc/source/index.rst b/doc/source/index.rst index f7263c9c3..5a407adcc 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -1,14 +1,28 @@ -.. include:: ../../README.rst +================ +os-client-config +================ + +.. image:: http://governance.openstack.org/badges/os-client-config.svg + :target: http://governance.openstack.org/reference/tags/index.html + +`os-client-config` is a library for collecting client configuration for +using an OpenStack cloud in a consistent and comprehensive manner. It +will find cloud config for as few as 1 cloud and as many as you want to +put in a config file. It will read environment variables and config files, +and it also contains some vendor specific default values so that you don't +have to know extra info to use OpenStack + +* If you have a config file, you will get the clouds listed in it +* If you have environment variables, you will get a cloud named `envvars` +* If you have neither, you will get a cloud named `defaults` with base defaults .. toctree:: :maxdepth: 2 - vendor-support - contributing - installation - network-config - api-reference - releasenotes + install/index + user/index + reference/index + contributor/index Indices and tables ================== diff --git a/doc/source/installation.rst b/doc/source/install/index.rst similarity index 100% rename from doc/source/installation.rst rename to doc/source/install/index.rst diff --git a/doc/source/api-reference.rst b/doc/source/reference/index.rst similarity index 100% rename from doc/source/api-reference.rst rename to doc/source/reference/index.rst diff --git a/doc/source/user/configuration.rst b/doc/source/user/configuration.rst new file mode 100644 index 000000000..df0b26659 --- /dev/null +++ b/doc/source/user/configuration.rst @@ -0,0 +1,303 @@ +=========================================== + Configuring os-client-config Applications +=========================================== + +Environment Variables +--------------------- + +`os-client-config` honors all of the normal `OS_*` variables. It does not +provide backwards compatibility to service-specific variables such as +`NOVA_USERNAME`. + +If you have OpenStack environment variables set, `os-client-config` will produce +a cloud config object named `envvars` containing your values from the +environment. If you don't like the name `envvars`, that's ok, you can override +it by setting `OS_CLOUD_NAME`. + +Service specific settings, like the nova service type, are set with the +default service type as a prefix. For instance, to set a special service_type +for trove set + +.. code-block:: bash + + export OS_DATABASE_SERVICE_TYPE=rax:database + +Config Files +------------ + +`os-client-config` will look for a file called `clouds.yaml` in the following +locations: + +* Current Directory +* ~/.config/openstack +* /etc/openstack + +The first file found wins. + +You can also set the environment variable `OS_CLIENT_CONFIG_FILE` to an +absolute path of a file to look for and that location will be inserted at the +front of the file search list. + +The keys are all of the keys you'd expect from `OS_*` - except lower case +and without the OS prefix. So, region name is set with `region_name`. + +Service specific settings, like the nova service type, are set with the +default service type as a prefix. For instance, to set a special service_type +for trove (because you're using Rackspace) set: + +.. code-block:: yaml + + database_service_type: 'rax:database' + + +Site Specific File Locations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In addition to `~/.config/openstack` and `/etc/openstack` - some platforms +have other locations they like to put things. `os-client-config` will also +look in an OS specific config dir + +* `USER_CONFIG_DIR` +* `SITE_CONFIG_DIR` + +`USER_CONFIG_DIR` is different on Linux, OSX and Windows. + +* Linux: `~/.config/openstack` +* OSX: `~/Library/Application Support/openstack` +* Windows: `C:\\Users\\USERNAME\\AppData\\Local\\OpenStack\\openstack` + +`SITE_CONFIG_DIR` is different on Linux, OSX and Windows. + +* Linux: `/etc/openstack` +* OSX: `/Library/Application Support/openstack` +* Windows: `C:\\ProgramData\\OpenStack\\openstack` + +An example config file is probably helpful: + +.. code-block:: yaml + + clouds: + mtvexx: + profile: vexxhost + auth: + username: mordred@inaugust.com + password: XXXXXXXXX + project_name: mordred@inaugust.com + region_name: ca-ymq-1 + dns_api_version: 1 + mordred: + region_name: RegionOne + auth: + username: 'mordred' + password: XXXXXXX + project_name: 'shade' + auth_url: 'https://montytaylor-sjc.openstack.blueboxgrid.com:5001/v2.0' + infra: + profile: rackspace + auth: + username: openstackci + password: XXXXXXXX + project_id: 610275 + regions: + - DFW + - ORD + - IAD + +You may note a few things. First, since `auth_url` settings are silly +and embarrassingly ugly, known cloud vendor profile information is included and +may be referenced by name. One of the benefits of that is that `auth_url` +isn't the only thing the vendor defaults contain. For instance, since +Rackspace lists `rax:database` as the service type for trove, `os-client-config` +knows that so that you don't have to. In case the cloud vendor profile is not +available, you can provide one called `clouds-public.yaml`, following the same +location rules previously mentioned for the config files. + +`regions` can be a list of regions. When you call `get_all_clouds`, +you'll get a cloud config object for each cloud/region combo. + +As seen with `dns_service_type`, any setting that makes sense to be per-service, +like `service_type` or `endpoint` or `api_version` can be set by prefixing +the setting with the default service type. That might strike you funny when +setting `service_type` and it does me too - but that's just the world we live +in. + +Auth Settings +------------- + +Keystone has auth plugins - which means it's not possible to know ahead of time +which auth settings are needed. `os-client-config` sets the default plugin type +to `password`, which is what things all were before plugins came about. In +order to facilitate validation of values, all of the parameters that exist +as a result of a chosen plugin need to go into the auth dict. For password +auth, this includes `auth_url`, `username` and `password` as well as anything +related to domains, projects and trusts. + +Splitting Secrets +----------------- + +In some scenarios, such as configuration management controlled environments, +it might be easier to have secrets in one file and non-secrets in another. +This is fully supported via an optional file `secure.yaml` which follows all +the same location rules as `clouds.yaml`. It can contain anything you put +in `clouds.yaml` and will take precedence over anything in the `clouds.yaml` +file. + +.. code-block:: yaml + + # clouds.yaml + clouds: + internap: + profile: internap + auth: + username: api-55f9a00fb2619 + project_name: inap-17037 + regions: + - ams01 + - nyj01 + # secure.yaml + clouds: + internap: + auth: + password: XXXXXXXXXXXXXXXXX + +SSL Settings +------------ + +When the access to a cloud is done via a secure connection, `os-client-config` +will always verify the SSL cert by default. This can be disabled by setting +`verify` to `False`. In case the cert is signed by an unknown CA, a specific +cacert can be provided via `cacert`. **WARNING:** `verify` will always have +precedence over `cacert`, so when setting a CA cert but disabling `verify`, the +cloud cert will never be validated. + +Client certs are also configurable. `cert` will be the client cert file +location. In case the cert key is not included within the client cert file, +its file location needs to be set via `key`. + +.. code-block:: yaml + + # clouds.yaml + clouds: + secure: + auth: ... + key: /home/myhome/client-cert.key + cert: /home/myhome/client-cert.crt + cacert: /home/myhome/ca.crt + insecure: + auth: ... + verify: False + +Cache Settings +-------------- + +Accessing a cloud is often expensive, so it's quite common to want to do some +client-side caching of those operations. To facilitate that, `os-client-config` +understands passing through cache settings to dogpile.cache, with the following +behaviors: + +* Listing no config settings means you get a null cache. +* `cache.expiration_time` and nothing else gets you memory cache. +* Otherwise, `cache.class` and `cache.arguments` are passed in + +Different cloud behaviors are also differently expensive to deal with. If you +want to get really crazy and tweak stuff, you can specify different expiration +times on a per-resource basis by passing values, in seconds to an expiration +mapping keyed on the singular name of the resource. A value of `-1` indicates +that the resource should never expire. + +`os-client-config` does not actually cache anything itself, but it collects +and presents the cache information so that your various applications that +are connecting to OpenStack can share a cache should you desire. + +.. code-block:: yaml + + cache: + class: dogpile.cache.pylibmc + expiration_time: 3600 + arguments: + url: + - 127.0.0.1 + expiration: + server: 5 + flavor: -1 + clouds: + mtvexx: + profile: vexxhost + auth: + username: mordred@inaugust.com + password: XXXXXXXXX + project_name: mordred@inaugust.com + region_name: ca-ymq-1 + dns_api_version: 1 + + +IPv6 +---- + +IPv6 is the future, and you should always use it if your cloud supports it and +if your local network supports it. Both of those are easily detectable and all +friendly software should do the right thing. However, sometimes you might +exist in a location where you have an IPv6 stack, but something evil has +caused it to not actually function. In that case, there is a config option +you can set to unbreak you `force_ipv4`, or `OS_FORCE_IPV4` boolean +environment variable. + +.. code-block:: yaml + + client: + force_ipv4: true + clouds: + mtvexx: + profile: vexxhost + auth: + username: mordred@inaugust.com + password: XXXXXXXXX + project_name: mordred@inaugust.com + region_name: ca-ymq-1 + dns_api_version: 1 + monty: + profile: rax + auth: + username: mordred@inaugust.com + password: XXXXXXXXX + project_name: mordred@inaugust.com + region_name: DFW + +The above snippet will tell client programs to prefer returning an IPv4 +address. + +Per-region settings +------------------- + +Sometimes you have a cloud provider that has config that is common to the +cloud, but also with some things you might want to express on a per-region +basis. For instance, Internap provides a public and private network specific +to the user in each region, and putting the values of those networks into +config can make consuming programs more efficient. + +To support this, the region list can actually be a list of dicts, and any +setting that can be set at the cloud level can be overridden for that +region. + +.. code-block:: yaml + + clouds: + internap: + profile: internap + auth: + password: XXXXXXXXXXXXXXXXX + username: api-55f9a00fb2619 + project_name: inap-17037 + regions: + - name: ams01 + values: + networks: + - name: inap-17037-WAN1654 + routes_externally: true + - name: inap-17037-LAN6745 + - name: nyj01 + values: + networks: + - name: inap-17037-WAN1654 + routes_externally: true + - name: inap-17037-LAN6745 diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst new file mode 100644 index 000000000..ec31c102c --- /dev/null +++ b/doc/source/user/index.rst @@ -0,0 +1,12 @@ +======================== + Using os-client-config +======================== + +.. toctree:: + :maxdepth: 2 + + configuration + using + vendor-support + network-config + releasenotes diff --git a/doc/source/network-config.rst b/doc/source/user/network-config.rst similarity index 100% rename from doc/source/network-config.rst rename to doc/source/user/network-config.rst diff --git a/doc/source/releasenotes.rst b/doc/source/user/releasenotes.rst similarity index 100% rename from doc/source/releasenotes.rst rename to doc/source/user/releasenotes.rst diff --git a/doc/source/user/using.rst b/doc/source/user/using.rst new file mode 100644 index 000000000..7d1d34eae --- /dev/null +++ b/doc/source/user/using.rst @@ -0,0 +1,182 @@ +========================================== + Using os-client-config in an Application +========================================== + +Usage +----- + +The simplest and least useful thing you can do is: + +.. code-block:: python + + python -m os_client_config.config + +Which will print out whatever if finds for your config. If you want to use +it from python, which is much more likely what you want to do, things like: + +Get a named cloud. + +.. code-block:: python + + import os_client_config + + cloud_config = os_client_config.OpenStackConfig().get_one_cloud( + 'internap', region_name='ams01') + print(cloud_config.name, cloud_config.region, cloud_config.config) + +Or, get all of the clouds. + +.. code-block:: python + + import os_client_config + + cloud_config = os_client_config.OpenStackConfig().get_all_clouds() + for cloud in cloud_config: + print(cloud.name, cloud.region, cloud.config) + +argparse +-------- + +If you're using os-client-config from a program that wants to process +command line options, there is a registration function to register the +arguments that both os-client-config and keystoneauth know how to deal +with - as well as a consumption argument. + +.. code-block:: python + + import argparse + import sys + + import os_client_config + + cloud_config = os_client_config.OpenStackConfig() + parser = argparse.ArgumentParser() + cloud_config.register_argparse_arguments(parser, sys.argv) + + options = parser.parse_args() + + cloud = cloud_config.get_one_cloud(argparse=options) + +Constructing OpenStack SDK object +--------------------------------- + +If what you want to do is get an OpenStack SDK Connection and you want it to +do all the normal things related to clouds.yaml, `OS_` environment variables, +a helper function is provided. The following will get you a fully configured +`openstacksdk` instance. + +.. code-block:: python + + import os_client_config + + sdk = os_client_config.make_sdk() + +If you want to do the same thing but on a named cloud. + +.. code-block:: python + + import os_client_config + + sdk = os_client_config.make_sdk(cloud='mtvexx') + +If you want to do the same thing but also support command line parsing. + +.. code-block:: python + + import argparse + + import os_client_config + + sdk = os_client_config.make_sdk(options=argparse.ArgumentParser()) + +It should be noted that OpenStack SDK has ways to construct itself that allow +for additional flexibility. If the helper function here does not meet your +needs, you should see the `from_config` method of +`openstack.connection.Connection `_ + +Constructing shade objects +-------------------------- + +If what you want to do is get a +`shade `_ OpenStackCloud object, a +helper function that honors clouds.yaml and `OS_` environment variables is +provided. The following will get you a fully configured `OpenStackCloud` +instance. + +.. code-block:: python + + import os_client_config + + cloud = os_client_config.make_shade() + +If you want to do the same thing but on a named cloud. + +.. code-block:: python + + import os_client_config + + cloud = os_client_config.make_shade(cloud='mtvexx') + +If you want to do the same thing but also support command line parsing. + +.. code-block:: python + + import argparse + + import os_client_config + + cloud = os_client_config.make_shade(options=argparse.ArgumentParser()) + +Constructing REST API Clients +----------------------------- + +What if you want to make direct REST calls via a Session interface? You're +in luck. A similar interface is available as with `openstacksdk` and `shade`. +The main difference is that you need to specify which service you want to +talk to and `make_rest_client` will return you a keystoneauth Session object +that is mounted on the endpoint for the service you're looking for. + +.. code-block:: python + + import os_client_config + + session = os_client_config.make_rest_client('compute', cloud='vexxhost') + + response = session.get('/servers') + server_list = response.json()['servers'] + +Constructing Legacy Client objects +---------------------------------- + +If you want get an old-style Client object from a python-\*client library, +and you want it to do all the normal things related to clouds.yaml, `OS_` +environment variables, a helper function is also provided. The following +will get you a fully configured `novaclient` instance. + +.. code-block:: python + + import os_client_config + + nova = os_client_config.make_client('compute') + +If you want to do the same thing but on a named cloud. + +.. code-block:: python + + import os_client_config + + nova = os_client_config.make_client('compute', cloud='mtvexx') + +If you want to do the same thing but also support command line parsing. + +.. code-block:: python + + import argparse + + import os_client_config + + nova = os_client_config.make_client( + 'compute', options=argparse.ArgumentParser()) + +If you want to get fancier than that in your python, then the rest of the +API is available to you. But often times, you just want to do the one thing. diff --git a/doc/source/vendor-support.rst b/doc/source/user/vendor-support.rst similarity index 100% rename from doc/source/vendor-support.rst rename to doc/source/user/vendor-support.rst From 412f0fdd8503d4a5b67d5ec55021fd0a9f452984 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Thu, 22 Jun 2017 15:17:54 -0400 Subject: [PATCH 1661/3836] turn on warning-is-error in documentation build Change-Id: I18cdecec84f8dd5f11741ac1ffc35630f7eb64b8 Signed-off-by: Doug Hellmann --- os_client_config/config.py | 1 + setup.cfg | 1 + 2 files changed, 2 insertions(+) diff --git a/os_client_config/config.py b/os_client_config/config.py index 1ed416cf5..eb415f702 100644 --- a/os_client_config/config.py +++ b/os_client_config/config.py @@ -661,6 +661,7 @@ def register_argparse_arguments(self, parser, argv, service_keys=None): the keystoneauth Auth Plugin Options and os-cloud. Also, peek in the argv to see if all of the auth plugin options should be registered or merely the ones already configured. + :param argparse.ArgumentParser: parser to attach argparse options to :param list argv: the arguments provided to the application :param string service_keys: Service or list of services this argparse diff --git a/setup.cfg b/setup.cfg index b87bd6ab3..19548fb75 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,6 +26,7 @@ packages = source-dir = doc/source build-dir = doc/build all_files = 1 +warning-is-error = 1 [upload_sphinx] upload-dir = doc/build/html From 30c8729f782c0c13ca872260085d5b7b7c37df61 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Thu, 22 Jun 2017 15:26:16 -0400 Subject: [PATCH 1662/3836] switch from oslosphinx to openstackdocstheme Change-Id: Ie45909df0b5a118d0200a1ee71277f4dbfe41d08 Signed-off-by: Doug Hellmann --- doc/source/conf.py | 5 ++++- releasenotes/source/conf.py | 5 +++-- test-requirements.txt | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 208517c86..82f27c36a 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -15,6 +15,8 @@ import os import sys +import openstackdocstheme + sys.path.insert(0, os.path.abspath('../..')) # -- General configuration ---------------------------------------------------- @@ -23,7 +25,6 @@ extensions = [ 'sphinx.ext.autodoc', #'sphinx.ext.intersphinx', - 'oslosphinx', 'reno.sphinxext' ] @@ -58,6 +59,8 @@ # html_theme_path = ["."] # html_theme = '_theme' # html_static_path = ['static'] +html_theme = 'openstackdocs' +html_theme_path = [openstackdocstheme.get_html_theme_path()] # Output file base name for HTML help builder. htmlhelp_basename = '%sdoc' % project diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py index e33ee8e57..1d0365645 100644 --- a/releasenotes/source/conf.py +++ b/releasenotes/source/conf.py @@ -21,12 +21,12 @@ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' +import openstackdocstheme # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'oslosphinx', 'reno.sphinxext', ] @@ -100,7 +100,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +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 @@ -109,6 +109,7 @@ # 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". diff --git a/test-requirements.txt b/test-requirements.txt index e71d004cd..5fc33b015 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -13,7 +13,7 @@ mock>=1.2 python-glanceclient>=0.18.0 python-subunit>=0.0.18 sphinx>=1.5.1 # BSD -oslosphinx>=4.7.0 # Apache-2.0 +openstackdocstheme>=1.5.0 # Apache-2.0 oslotest>=1.5.1,<1.6.0 # Apache-2.0 reno>=0.1.1 # Apache2 testrepository>=0.0.18 From f4668b7c6f0a2578ae6a96516e02ec1ff754c1b4 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Fri, 23 Jun 2017 10:48:51 -0400 Subject: [PATCH 1663/3836] Project update to change enabled only when provided The project update call considers enabled=True as the default, causing the project to always become enabled unless enabled=False is passed explicitly. This patch removes that default and only touches the enable field when it is explicitly provided as enabled=False or enabled=True. Closes-Bug: #2001080 Change-Id: I0a3b926b42be0321d06ebc370e4f51eba4150a50 --- .../notes/bug-2001080-de52ead3c5466792.yaml | 10 ++++++++++ shade/openstackcloud.py | 8 +++----- shade/tests/functional/test_project.py | 18 ++++++++++++++++-- 3 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/bug-2001080-de52ead3c5466792.yaml diff --git a/releasenotes/notes/bug-2001080-de52ead3c5466792.yaml b/releasenotes/notes/bug-2001080-de52ead3c5466792.yaml new file mode 100644 index 000000000..2b8b3c319 --- /dev/null +++ b/releasenotes/notes/bug-2001080-de52ead3c5466792.yaml @@ -0,0 +1,10 @@ +--- +prelude: > + Fixed a bug where a project was always enabled upon update, unless + ``enabled=False`` is passed explicitly. +fixes: + - | + [`bug 2001080 `_] + Project update will only update the enabled field of projects when + ``enabled=True`` or ``enabled=False`` is passed explicitly. The previous + behavior had ``enabled=True`` as the default. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 06879089f..084dc3ffc 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -893,7 +893,7 @@ def get_project(self, name_or_id, filters=None, domain_id=None): domain_id=domain_id) @_utils.valid_kwargs('description') - def update_project(self, name_or_id, enabled=True, domain_id=None, + def update_project(self, name_or_id, enabled=None, domain_id=None, **kwargs): with _utils.shade_exceptions( "Error in updating project {project}".format( @@ -902,12 +902,10 @@ def update_project(self, name_or_id, enabled=True, domain_id=None, if not proj: raise OpenStackCloudException( "Project %s not found." % name_or_id) - - kwargs.update({'enabled': enabled}) + if enabled is not None: + kwargs.update({'enabled': enabled}) # NOTE(samueldmq): Current code only allow updates of description # or enabled fields. - # FIXME(samueldmq): enable=True is the default, meaning it will - # enable a disabled project if you simply update other fields if self.cloud_config.get_api_version('identity') == '3': data = self._identity_client.patch( '/projects/' + proj['id'], json={'project': kwargs}) diff --git a/shade/tests/functional/test_project.py b/shade/tests/functional/test_project.py index 9ffd4150f..25d59e6db 100644 --- a/shade/tests/functional/test_project.py +++ b/shade/tests/functional/test_project.py @@ -67,18 +67,32 @@ def test_update_project(self): params = { 'name': project_name, 'description': 'test_update_project', + 'enabled': True } if self.identity_version == '3': params['domain_id'] = \ self.operator_cloud.get_domain('default')['id'] project = self.operator_cloud.create_project(**params) - updated_project = self.operator_cloud.update_project(project_name, - description='new') + updated_project = self.operator_cloud.update_project( + project_name, enabled=False, description='new') self.assertIsNotNone(updated_project) self.assertEqual(project['id'], updated_project['id']) self.assertEqual(project['name'], updated_project['name']) self.assertEqual(updated_project['description'], 'new') + self.assertTrue(project['enabled']) + self.assertFalse(updated_project['enabled']) + + # Revert the description and verify the project is still disabled + updated_project = self.operator_cloud.update_project( + project_name, description=params['description']) + self.assertIsNotNone(updated_project) + self.assertEqual(project['id'], updated_project['id']) + self.assertEqual(project['name'], updated_project['name']) + self.assertEqual(project['description'], + updated_project['description']) + self.assertTrue(project['enabled']) + self.assertFalse(updated_project['enabled']) def test_delete_project(self): project_name = self.new_project_name + '_delete' From 08a15468d3dc20e34ac5a4f61a0c3bdd9afbec54 Mon Sep 17 00:00:00 2001 From: Yi Zhao Date: Thu, 22 Jun 2017 16:35:19 +0800 Subject: [PATCH 1664/3836] Add query filters for find_network Change-Id: Iec5fc8ebfe58d9bcd1bd06ba2fd64845d33b8c53 Closes-Bug: #1699703 --- openstack/network/v2/_proxy.py | 166 ++++++++++++------ .../functional/network/v2/test_network.py | 13 ++ openstack/tests/unit/network/v2/test_proxy.py | 9 + 3 files changed, 134 insertions(+), 54 deletions(-) diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 4ee8559aa..288522089 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -84,7 +84,7 @@ def delete_address_scope(self, address_scope, ignore_missing=True): self._delete(_address_scope.AddressScope, address_scope, ignore_missing=ignore_missing) - def find_address_scope(self, name_or_id, ignore_missing=True): + def find_address_scope(self, name_or_id, ignore_missing=True, **args): """Find a single address scope :param name_or_id: The name or ID of an address scope. @@ -93,11 +93,13 @@ def find_address_scope(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.address_scope.AddressScope` or None """ return self._find(_address_scope.AddressScope, name_or_id, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, **args) def get_address_scope(self, address_scope): """Get a single address scope @@ -324,7 +326,7 @@ def availability_zones(self, **query): """ return self._list(availability_zone.AvailabilityZone, paginated=False) - def find_extension(self, name_or_id, ignore_missing=True): + def find_extension(self, name_or_id, ignore_missing=True, **args): """Find a single extension :param name_or_id: The name or ID of a extension. @@ -333,11 +335,13 @@ def find_extension(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.extension.Extension` or None """ return self._find(extension.Extension, name_or_id, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, **args) def extensions(self, **query): """Return a generator of extensions @@ -379,7 +383,7 @@ def delete_flavor(self, flavor, ignore_missing=True): """ self._delete(_flavor.Flavor, flavor, ignore_missing=ignore_missing) - def find_flavor(self, name_or_id, ignore_missing=True): + def find_flavor(self, name_or_id, ignore_missing=True, **args): """Find a single network service flavor :param name_or_id: The name or ID of a flavor. @@ -388,10 +392,12 @@ def find_flavor(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.flavor.Flavor` or None """ return self._find(_flavor.Flavor, name_or_id, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, **args) def get_flavor(self, flavor): """Get a single network service flavor @@ -511,7 +517,7 @@ def find_available_ip(self): """ return _floating_ip.FloatingIP.find_available(self._session) - def find_ip(self, name_or_id, ignore_missing=True): + def find_ip(self, name_or_id, ignore_missing=True, **args): """Find a single IP :param name_or_id: The name or ID of an IP. @@ -520,11 +526,13 @@ def find_ip(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.floating_ip.FloatingIP` or None """ return self._find(_floating_ip.FloatingIP, name_or_id, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, **args) def get_ip(self, floating_ip): """Get a single floating ip @@ -608,7 +616,7 @@ def delete_health_monitor(self, health_monitor, ignore_missing=True): self._delete(_health_monitor.HealthMonitor, health_monitor, ignore_missing=ignore_missing) - def find_health_monitor(self, name_or_id, ignore_missing=True): + def find_health_monitor(self, name_or_id, ignore_missing=True, **args): """Find a single health monitor :param name_or_id: The name or ID of a health monitor. @@ -617,11 +625,13 @@ def find_health_monitor(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.health_monitor. HealthMonitor` or None """ return self._find(_health_monitor.HealthMonitor, - name_or_id, ignore_missing=ignore_missing) + name_or_id, ignore_missing=ignore_missing, **args) def get_health_monitor(self, health_monitor): """Get a single health monitor @@ -709,7 +719,7 @@ def delete_listener(self, listener, ignore_missing=True): self._delete(_listener.Listener, listener, ignore_missing=ignore_missing) - def find_listener(self, name_or_id, ignore_missing=True): + def find_listener(self, name_or_id, ignore_missing=True, **args): """Find a single listener :param name_or_id: The name or ID of a listener. @@ -718,10 +728,12 @@ def find_listener(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.listener.Listener` or None """ return self._find(_listener.Listener, name_or_id, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, **args) def get_listener(self, listener): """Get a single listener @@ -802,7 +814,7 @@ def delete_load_balancer(self, load_balancer, ignore_missing=True): self._delete(_load_balancer.LoadBalancer, load_balancer, ignore_missing=ignore_missing) - def find_load_balancer(self, name_or_id, ignore_missing=True): + def find_load_balancer(self, name_or_id, ignore_missing=True, **args): """Find a single load balancer :param name_or_id: The name or ID of a load balancer. @@ -811,11 +823,13 @@ def find_load_balancer(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.load_balancer.LoadBalancer` or None """ return self._find(_load_balancer.LoadBalancer, name_or_id, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, **args) def get_load_balancer(self, load_balancer): """Get a single load balancer @@ -887,7 +901,7 @@ def delete_metering_label(self, metering_label, ignore_missing=True): self._delete(_metering_label.MeteringLabel, metering_label, ignore_missing=ignore_missing) - def find_metering_label(self, name_or_id, ignore_missing=True): + def find_metering_label(self, name_or_id, ignore_missing=True, **args): """Find a single metering label :param name_or_id: The name or ID of a metering label. @@ -896,11 +910,13 @@ def find_metering_label(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.metering_label. MeteringLabel` or None """ return self._find(_metering_label.MeteringLabel, name_or_id, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, **args) def get_metering_label(self, metering_label): """Get a single metering label @@ -983,7 +999,8 @@ def delete_metering_label_rule(self, metering_label_rule, self._delete(_metering_label_rule.MeteringLabelRule, metering_label_rule, ignore_missing=ignore_missing) - def find_metering_label_rule(self, name_or_id, ignore_missing=True): + def find_metering_label_rule(self, name_or_id, ignore_missing=True, + **args): """Find a single metering label rule :param name_or_id: The name or ID of a metering label rule. @@ -992,11 +1009,13 @@ def find_metering_label_rule(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.metering_label_rule. MeteringLabelRule` or None """ return self._find(_metering_label_rule.MeteringLabelRule, name_or_id, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, **args) def get_metering_label_rule(self, metering_label_rule): """Get a single metering label rule @@ -1082,7 +1101,7 @@ def delete_network(self, network, ignore_missing=True): """ self._delete(_network.Network, network, ignore_missing=ignore_missing) - def find_network(self, name_or_id, ignore_missing=True): + def find_network(self, name_or_id, ignore_missing=True, **args): """Find a single network :param name_or_id: The name or ID of a network. @@ -1091,10 +1110,12 @@ def find_network(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.network.Network` or None """ return self._find(_network.Network, name_or_id, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, **args) def get_network(self, network): """Get a single network @@ -1150,7 +1171,8 @@ def update_network(self, network, **attrs): """ return self._update(_network.Network, network, **attrs) - def find_network_ip_availability(self, name_or_id, ignore_missing=True): + def find_network_ip_availability(self, name_or_id, ignore_missing=True, + **args): """Find IP availability of a network :param name_or_id: The name or ID of a network. @@ -1159,11 +1181,13 @@ def find_network_ip_availability(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.network_ip_availability. NetworkIPAvailability` or None """ return self._find(network_ip_availability.NetworkIPAvailability, - name_or_id, ignore_missing=ignore_missing) + name_or_id, ignore_missing=ignore_missing, **args) def get_network_ip_availability(self, network): """Get IP availability of a network @@ -1227,7 +1251,7 @@ def delete_pool(self, pool, ignore_missing=True): """ self._delete(_pool.Pool, pool, ignore_missing=ignore_missing) - def find_pool(self, name_or_id, ignore_missing=True): + def find_pool(self, name_or_id, ignore_missing=True, **args): """Find a single pool :param name_or_id: The name or ID of a pool. @@ -1236,10 +1260,12 @@ def find_pool(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.pool.Pool` or None """ return self._find(_pool.Pool, name_or_id, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, **args) def get_pool(self, pool): """Get a single pool @@ -1330,7 +1356,7 @@ def delete_pool_member(self, pool_member, pool, ignore_missing=True): self._delete(_pool_member.PoolMember, pool_member, ignore_missing=ignore_missing, pool_id=poolobj.id) - def find_pool_member(self, name_or_id, pool, ignore_missing=True): + def find_pool_member(self, name_or_id, pool, ignore_missing=True, **args): """Find a single pool member :param str name_or_id: The name or ID of a pool member. @@ -1342,12 +1368,15 @@ def find_pool_member(self, name_or_id, pool, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.pool_member.PoolMember` or None """ poolobj = self._get_resource(_pool.Pool, pool) return self._find(_pool_member.PoolMember, name_or_id, - ignore_missing=ignore_missing, pool_id=poolobj.id) + ignore_missing=ignore_missing, pool_id=poolobj.id, + **args) def get_pool_member(self, pool_member, pool): """Get a single pool member @@ -1441,7 +1470,7 @@ def delete_port(self, port, ignore_missing=True): """ self._delete(_port.Port, port, ignore_missing=ignore_missing) - def find_port(self, name_or_id, ignore_missing=True): + def find_port(self, name_or_id, ignore_missing=True, **args): """Find a single port :param name_or_id: The name or ID of a port. @@ -1450,10 +1479,12 @@ def find_port(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.port.Port` or None """ return self._find(_port.Port, name_or_id, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, **args) def get_port(self, port): """Get a single port @@ -1566,7 +1597,7 @@ def delete_qos_bandwidth_limit_rule(self, qos_rule, qos_policy, qos_policy_id=policy.id) def find_qos_bandwidth_limit_rule(self, qos_rule_id, qos_policy, - ignore_missing=True): + ignore_missing=True, **args): """Find a bandwidth limit rule :param qos_rule_id: The ID of a bandwidth limit rule. @@ -1578,13 +1609,15 @@ def find_qos_bandwidth_limit_rule(self, qos_rule_id, qos_policy, raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.qos_bandwidth_limit_rule. QoSBandwidthLimitRule` or None """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._find(_qos_bandwidth_limit_rule.QoSBandwidthLimitRule, qos_rule_id, ignore_missing=ignore_missing, - qos_policy_id=policy.id) + qos_policy_id=policy.id, **args) def get_qos_bandwidth_limit_rule(self, qos_rule, qos_policy): """Get a single bandwidth limit rule @@ -1688,7 +1721,7 @@ def delete_qos_dscp_marking_rule(self, qos_rule, qos_policy, qos_policy_id=policy.id) def find_qos_dscp_marking_rule(self, qos_rule_id, qos_policy, - ignore_missing=True): + ignore_missing=True, **args): """Find a QoS DSCP marking rule :param qos_rule_id: The ID of a QoS DSCP marking rule. @@ -1700,13 +1733,15 @@ def find_qos_dscp_marking_rule(self, qos_rule_id, qos_policy, raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.qos_dscp_marking_rule. QoSDSCPMarkingRule` or None """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._find(_qos_dscp_marking_rule.QoSDSCPMarkingRule, qos_rule_id, ignore_missing=ignore_missing, - qos_policy_id=policy.id) + qos_policy_id=policy.id, **args) def get_qos_dscp_marking_rule(self, qos_rule, qos_policy): """Get a single QoS DSCP marking rule @@ -1808,7 +1843,7 @@ def delete_qos_minimum_bandwidth_rule(self, qos_rule, qos_policy, qos_policy_id=policy.id) def find_qos_minimum_bandwidth_rule(self, qos_rule_id, qos_policy, - ignore_missing=True): + ignore_missing=True, **args): """Find a minimum bandwidth rule :param qos_rule_id: The ID of a minimum bandwidth rule. @@ -1820,13 +1855,15 @@ def find_qos_minimum_bandwidth_rule(self, qos_rule_id, qos_policy, raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.qos_minimum_bandwidth_rule. QoSMinimumBandwidthRule` or None """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._find(_qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, qos_rule_id, ignore_missing=ignore_missing, - qos_policy_id=policy.id) + qos_policy_id=policy.id, **args) def get_qos_minimum_bandwidth_rule(self, qos_rule, qos_policy): """Get a single minimum bandwidth rule @@ -1916,7 +1953,7 @@ def delete_qos_policy(self, qos_policy, ignore_missing=True): self._delete(_qos_policy.QoSPolicy, qos_policy, ignore_missing=ignore_missing) - def find_qos_policy(self, name_or_id, ignore_missing=True): + def find_qos_policy(self, name_or_id, ignore_missing=True, **args): """Find a single QoS policy :param name_or_id: The name or ID of a QoS policy. @@ -1925,11 +1962,13 @@ def find_qos_policy(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.qos_policy.QoSPolicy` or None """ return self._find(_qos_policy.QoSPolicy, name_or_id, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, **args) def get_qos_policy(self, qos_policy): """Get a single QoS policy @@ -2089,7 +2128,7 @@ def delete_rbac_policy(self, rbac_policy, ignore_missing=True): self._delete(_rbac_policy.RBACPolicy, rbac_policy, ignore_missing=ignore_missing) - def find_rbac_policy(self, rbac_policy, ignore_missing=True): + def find_rbac_policy(self, rbac_policy, ignore_missing=True, **args): """Find a single RBAC policy :param rbac_policy: The ID of a RBAC policy. @@ -2098,11 +2137,13 @@ def find_rbac_policy(self, rbac_policy, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.rbac_policy.RBACPolicy` or None """ return self._find(_rbac_policy.RBACPolicy, rbac_policy, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, **args) def get_rbac_policy(self, rbac_policy): """Get a single RBAC policy @@ -2174,7 +2215,7 @@ def delete_router(self, router, ignore_missing=True): """ self._delete(_router.Router, router, ignore_missing=ignore_missing) - def find_router(self, name_or_id, ignore_missing=True): + def find_router(self, name_or_id, ignore_missing=True, **args): """Find a single router :param name_or_id: The name or ID of a router. @@ -2183,10 +2224,12 @@ def find_router(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.router.Router` or None """ return self._find(_router.Router, name_or_id, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, **args) def get_router(self, router): """Get a single router @@ -2382,7 +2425,7 @@ def delete_security_group(self, security_group, ignore_missing=True): self._delete(_security_group.SecurityGroup, security_group, ignore_missing=ignore_missing) - def find_security_group(self, name_or_id, ignore_missing=True): + def find_security_group(self, name_or_id, ignore_missing=True, **args): """Find a single security group :param name_or_id: The name or ID of a security group. @@ -2391,11 +2434,13 @@ def find_security_group(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.security_group. SecurityGroup` or None """ return self._find(_security_group.SecurityGroup, name_or_id, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, **args) def get_security_group(self, security_group): """Get a single security group @@ -2504,7 +2549,8 @@ def delete_security_group_rule(self, security_group_rule, self._delete(_security_group_rule.SecurityGroupRule, security_group_rule, ignore_missing=ignore_missing) - def find_security_group_rule(self, name_or_id, ignore_missing=True): + def find_security_group_rule(self, name_or_id, ignore_missing=True, + **args): """Find a single security group rule :param str name_or_id: The ID of a security group rule. @@ -2513,11 +2559,13 @@ def find_security_group_rule(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.security_group_rule. SecurityGroupRule` or None """ return self._find(_security_group_rule.SecurityGroupRule, - name_or_id, ignore_missing=ignore_missing) + name_or_id, ignore_missing=ignore_missing, **args) def get_security_group_rule(self, security_group_rule): """Get a single security group rule @@ -2586,7 +2634,7 @@ def delete_segment(self, segment, ignore_missing=True): """ self._delete(_segment.Segment, segment, ignore_missing=ignore_missing) - def find_segment(self, name_or_id, ignore_missing=True): + def find_segment(self, name_or_id, ignore_missing=True, **args): """Find a single segment :param name_or_id: The name or ID of a segment. @@ -2595,10 +2643,12 @@ def find_segment(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.segment.Segment` or None """ return self._find(_segment.Segment, name_or_id, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, **args) def get_segment(self, segment): """Get a single segment @@ -2690,7 +2740,7 @@ def delete_service_profile(self, service_profile, ignore_missing=True): self._delete(_service_profile.ServiceProfile, service_profile, ignore_missing=ignore_missing) - def find_service_profile(self, name_or_id, ignore_missing=True): + def find_service_profile(self, name_or_id, ignore_missing=True, **args): """Find a single network service flavor profile :param name_or_id: The name or ID of a service profile. @@ -2699,11 +2749,13 @@ def find_service_profile(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.service_profile .ServiceProfile` or None """ return self._find(_service_profile.ServiceProfile, name_or_id, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, **args) def get_service_profile(self, service_profile): """Get a single network service flavor profile @@ -2778,7 +2830,7 @@ def delete_subnet(self, subnet, ignore_missing=True): """ self._delete(_subnet.Subnet, subnet, ignore_missing=ignore_missing) - def find_subnet(self, name_or_id, ignore_missing=True): + def find_subnet(self, name_or_id, ignore_missing=True, **args): """Find a single subnet :param name_or_id: The name or ID of a subnet. @@ -2787,10 +2839,12 @@ def find_subnet(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.subnet.Subnet` or None """ return self._find(_subnet.Subnet, name_or_id, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, **args) def get_subnet(self, subnet): """Get a single subnet @@ -2869,7 +2923,7 @@ def delete_subnet_pool(self, subnet_pool, ignore_missing=True): self._delete(_subnet_pool.SubnetPool, subnet_pool, ignore_missing=ignore_missing) - def find_subnet_pool(self, name_or_id, ignore_missing=True): + def find_subnet_pool(self, name_or_id, ignore_missing=True, **args): """Find a single subnet pool :param name_or_id: The name or ID of a subnet pool. @@ -2878,11 +2932,13 @@ def find_subnet_pool(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.subnet_pool.SubnetPool` or None """ return self._find(_subnet_pool.SubnetPool, name_or_id, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, **args) def get_subnet_pool(self, subnet_pool): """Get a single subnet pool @@ -2981,7 +3037,7 @@ def delete_vpn_service(self, vpn_service, ignore_missing=True): self._delete(_vpn_service.VPNService, vpn_service, ignore_missing=ignore_missing) - def find_vpn_service(self, name_or_id, ignore_missing=True): + def find_vpn_service(self, name_or_id, ignore_missing=True, **args): """Find a single vpn service :param name_or_id: The name or ID of a vpn service. @@ -2990,11 +3046,13 @@ def find_vpn_service(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.vpn_service.VPNService` or None """ return self._find(_vpn_service.VPNService, name_or_id, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, **args) def get_vpn_service(self, vpn_service): """Get a single vpn service diff --git a/openstack/tests/functional/network/v2/test_network.py b/openstack/tests/functional/network/v2/test_network.py index 6a959d71f..5f0230a0e 100644 --- a/openstack/tests/functional/network/v2/test_network.py +++ b/openstack/tests/functional/network/v2/test_network.py @@ -60,6 +60,19 @@ def test_find(self): sot = self.conn.network.find_network(self.NAME) self.assertEqual(self.ID, sot.id) + def test_find_with_filter(self): + project_id_1 = "1" + project_id_2 = "2" + sot1 = self.conn.network.create_network(name=self.NAME, + project_id=project_id_1) + sot2 = self.conn.network.create_network(name=self.NAME, + project_id=project_id_2) + sot = self.conn.network.find_network(self.NAME, + project_id=project_id_1) + self.assertEqual(project_id_1, sot.project_id) + self.conn.network.delete_network(sot1.id) + self.conn.network.delete_network(sot2.id) + def test_get(self): sot = self.conn.network.get_network(self.ID) self.assertEqual(self.NAME, sot.name) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 06232cd23..eccd2dcdc 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -315,6 +315,15 @@ def test_network_delete_ignore(self): def test_network_find(self): self.verify_find(self.proxy.find_network, network.Network) + def test_network_find_with_filter(self): + self._verify2('openstack.proxy2.BaseProxy._find', + self.proxy.find_network, + method_args=["net1"], + method_kwargs={"project_id": "1"}, + expected_args=[network.Network, "net1"], + expected_kwargs={"project_id": "1", + "ignore_missing": True}) + def test_network_get(self): self.verify_get(self.proxy.get_network, network.Network) From c016f15867d53b7054e8d8561dd5de4d995adc3c Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Mon, 26 Jun 2017 23:20:09 +0000 Subject: [PATCH 1665/3836] Don't remove top-container element for flavors and clusters Change-Id: Ie48b477a35f448bfddd561bec68f30a5eb452e97 Signed-off-by: Rosario Di Somma --- shade/_adapter.py | 3 ++- shade/openstackcloud.py | 15 +++++++++------ shade/operatorcloud.py | 5 +++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/shade/_adapter.py b/shade/_adapter.py index 7bbb5330a..6a67c1c0a 100644 --- a/shade/_adapter.py +++ b/shade/_adapter.py @@ -139,7 +139,8 @@ def _munch_response(self, response, result_key=None, error_message=None): 'security_group', 'security_groups', 'security_group_rule', 'security_group_rules', 'users', 'user', 'projects', 'tenants', - 'project', 'tenant', 'servers', 'server']: + 'project', 'tenant', 'servers', 'server', + 'flavor', 'flavors', 'baymodels']: if key in result_json.keys(): self._log_request_id(response) return result_json diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 06879089f..b7b0e98bb 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1798,19 +1798,21 @@ def list_flavors(self, get_extra=None): """ if get_extra is None: get_extra = self._extra_config['get_flavor_extra_specs'] + data = self._compute_client.get( + '/flavors/detail', params=dict(is_public='None'), + error_message="Error fetching flavor list") flavors = self._normalize_flavors( - self._compute_client.get( - '/flavors/detail', params=dict(is_public='None'), - error_message="Error fetching flavor list")) + meta.get_and_munchify('flavors', data)) for flavor in flavors: if not flavor.extra_specs and get_extra: endpoint = "/flavors/{id}/os-extra_specs".format( id=flavor.id) try: - flavor.extra_specs = self._compute_client.get( + extra_specs = self._compute_client.get( endpoint, error_message="Error fetching flavor extra specs") + flavor.extra_specs = extra_specs except OpenStackCloudHTTPError as e: flavor.extra_specs = {} self.log.debug( @@ -7489,9 +7491,10 @@ def list_cluster_templates(self, detail=False): the OpenStack API call. """ with _utils.shade_exceptions("Error fetching cluster template list"): - cluster_templates = self._container_infra_client.get( + data = self._container_infra_client.get( '/baymodels/detail') - return self._normalize_cluster_templates(cluster_templates) + return self._normalize_cluster_templates( + meta.get_and_munchify('baymodels', data)) list_baymodels = list_cluster_templates def search_cluster_templates( diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 2ded0fb07..51e4af830 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1496,11 +1496,12 @@ def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", } if flavorid == 'auto': payload['id'] = None - flavor = self._compute_client.post( + data = self._compute_client.post( '/flavors', json=dict(flavor=payload)) - return self._normalize_flavor(flavor) + return self._normalize_flavor( + meta.get_and_munchify('flavor', data)) def delete_flavor(self, name_or_id): """Delete a flavor From 70d8cc39df1093a69618ba1d1a6eb41ef1842baa Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 27 Jun 2017 12:07:54 +0000 Subject: [PATCH 1666/3836] Updated from global requirements Change-Id: Id6df6784e14fa6dfd4543ab54f9c2e01c87c69cf --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 5abe62d28..25a1ef26b 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,7 +9,7 @@ mock>=2.0 # BSD python-subunit>=0.0.18 # Apache-2.0/BSD oslosphinx>=4.7.0 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 -sphinx!=1.6.1,>=1.5.1 # BSD +sphinx>=1.6.2 # BSD testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=1.4.0 # MIT From d922253958d9d6f7dc2c89e8574c2dde1caacdeb Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 27 Jun 2017 12:22:24 +0000 Subject: [PATCH 1667/3836] Updated from global requirements Change-Id: Ie558b341237a1e095d14dc275fe0569e4b6a6e78 --- test-requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 872a54ce3..872accec7 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,11 +8,11 @@ coverage!=4.4,>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD mock>=2.0 # BSD python-subunit>=0.0.18 # Apache-2.0/BSD -openstackdocstheme>=1.5.0 # Apache-2.0 +openstackdocstheme>=1.11.0 # Apache-2.0 os-testr>=0.8.0 # Apache-2.0 requests>=2.14.2 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 -sphinx!=1.6.1,>=1.5.1 # BSD +sphinx>=1.6.2 # BSD testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=1.4.0 # MIT From 071e3429103a027fb579399907f4976b894ffd12 Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Tue, 27 Jun 2017 22:15:42 +0000 Subject: [PATCH 1668/3836] Don't remove top-container element for flavor, zones and server groups Change-Id: If85a133be034b27253bd6ddd36cf8a9224ab1673 Signed-off-by: Rosario Di Somma --- shade/_adapter.py | 4 +++- shade/openstackcloud.py | 9 ++++++--- shade/operatorcloud.py | 17 +++++++++++------ 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/shade/_adapter.py b/shade/_adapter.py index 6a67c1c0a..9abd9d3f1 100644 --- a/shade/_adapter.py +++ b/shade/_adapter.py @@ -140,7 +140,9 @@ def _munch_response(self, response, result_key=None, error_message=None): 'security_group_rule', 'security_group_rules', 'users', 'user', 'projects', 'tenants', 'project', 'tenant', 'servers', 'server', - 'flavor', 'flavors', 'baymodels']: + 'flavor', 'flavors', 'baymodels', 'aggregate', + 'aggregates', 'availabilityZoneInfo', + 'flavor_access', 'output', 'server_groups']: if key in result_json.keys(): self._log_request_id(response) return result_json diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index b7b0e98bb..7743763bd 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1773,12 +1773,13 @@ def list_availability_zone_names(self, unavailable=False): list could not be fetched. """ try: - zones = self._compute_client.get('/os-availability-zone') + data = self._compute_client.get('/os-availability-zone') except OpenStackCloudHTTPError: self.log.debug( "Availability zone list could not be fetched", exc_info=True) return [] + zones = meta.get_and_munchify('availabilityZoneInfo', data) ret = [] for zone in zones: if zone['zoneState']['available'] or unavailable: @@ -2035,9 +2036,10 @@ def list_server_groups(self): :returns: A list of server group dicts. """ - return self._compute_client.get( + data = self._compute_client.get( '/os-server-groups', error_message="Error fetching server group list") + return meta.get_and_munchify('server_groups', data) def get_compute_limits(self, name_or_id=None): """ Get compute limits for a project @@ -2771,9 +2773,10 @@ def get_server_console(self, server, length=None): "Console log requested for invalid server") try: - return self._compute_client.post( + data = self._compute_client.post( '/servers/{server}/action'.format(server=server['id']), json={'os-getConsoleOutput': {'length': length}}) + return meta.get_and_munchify('output', data) except OpenStackCloudBadRequest: return "" diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 51e4af830..dd4ac631a 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1606,9 +1606,10 @@ def list_flavor_access(self, flavor_id): with _utils.shade_exceptions("Error trying to list access from " "flavor ID {flavor}".format( flavor=flavor_id)): - projects = self._compute_client.get( + data = self._compute_client.get( '/flavors/{id}/os-flavor-access'.format(id=flavor_id)) - return _utils.normalize_flavor_accesses(projects) + return _utils.normalize_flavor_accesses( + meta.get_and_munchify('flavor_access', data)) def create_role(self, name): """Create a Keystone role. @@ -1836,9 +1837,10 @@ def list_aggregates(self): :returns: A list of aggregate dicts. """ - return self._compute_client.get( + data = self._compute_client.get( '/os-aggregates', error_message="Error fetching aggregate list") + return meta.get_and_munchify('aggregates', data) def get_aggregate(self, name_or_id, filters=None): """Get an aggregate by name or ID. @@ -1871,7 +1873,7 @@ def create_aggregate(self, name, availability_zone=None): :raises: OpenStackCloudException on operation error. """ - return self._compute_client.post( + data = self._compute_client.post( '/os-aggregates', json={'aggregate': { 'name': name, @@ -1879,6 +1881,7 @@ def create_aggregate(self, name, availability_zone=None): }}, error_message="Unable to create host aggregate {name}".format( name=name)) + return meta.get_and_munchify('aggregate', data) @_utils.valid_kwargs('name', 'availability_zone') def update_aggregate(self, name_or_id, **kwargs): @@ -1897,11 +1900,12 @@ def update_aggregate(self, name_or_id, **kwargs): raise OpenStackCloudException( "Host aggregate %s not found." % name_or_id) - return self._compute_client.put( + data = self._compute_client.put( '/os-aggregates/{id}'.format(id=aggregate['id']), json={'aggregate': kwargs}, error_message="Error updating aggregate {name}".format( name=name_or_id)) + return meta.get_and_munchify('aggregate', data) def delete_aggregate(self, name_or_id): """Delete a host aggregate. @@ -1943,10 +1947,11 @@ def set_aggregate_metadata(self, name_or_id, metadata): err_msg = "Unable to set metadata for host aggregate {name}".format( name=name_or_id) - return self._compute_client.post( + data = self._compute_client.post( '/os-aggregates/{id}/action'.format(id=aggregate['id']), json={'set_metadata': {'metadata': metadata}}, error_message=err_msg) + return meta.get_and_munchify('aggregate', data) def add_host_to_aggregate(self, name_or_id, host_name): """Add a host to an aggregate. From 8eebf74bfb6e495cc144d66d05e12478ba1f1acb Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Wed, 28 Jun 2017 06:50:40 -0400 Subject: [PATCH 1669/3836] Remove dead links about OpenStack RC file Change-Id: Ifdd09714cf5d55c6a85b09f2103e7186b00395a1 Closes-Bug: 1700885 --- doc/source/users/guides/connect.rst | 3 --- doc/source/users/guides/connect_from_config.rst | 3 --- 2 files changed, 6 deletions(-) diff --git a/doc/source/users/guides/connect.rst b/doc/source/users/guides/connect.rst index 80267b80e..6c9937d9c 100644 --- a/doc/source/users/guides/connect.rst +++ b/doc/source/users/guides/connect.rst @@ -8,9 +8,6 @@ created in 3 ways, using the class itself, a file, or environment variables. If this is your first time using the SDK, we recommend simply using the class itself as illustrated below. -.. note:: To get your credentials - `Download the OpenStack RC file `_. - Create Connection ----------------- diff --git a/doc/source/users/guides/connect_from_config.rst b/doc/source/users/guides/connect_from_config.rst index 2e5769ac2..ca99426d3 100644 --- a/doc/source/users/guides/connect_from_config.rst +++ b/doc/source/users/guides/connect_from_config.rst @@ -9,9 +9,6 @@ environment variables as illustrated below. The SDK uses `os-client-config `_ to handle the configuration. -.. note:: To get your credentials - `Download the OpenStack RC file `_. - Create Connection From A File ----------------------------- From 1213ccb00f706680a963a9d4cdad756a9b51d31e Mon Sep 17 00:00:00 2001 From: Kiran_totad Date: Thu, 29 Jun 2017 10:07:30 +0530 Subject: [PATCH 1670/3836] Replace six.iteritems() with .items() 1.As mentioned in [1], we should avoid using six.iteritems to achieve iterators. We can use dict.items instead, as it will return iterators in PY3 as well. And dict.items/keys will more readable. 2.In py2, the performance about list should be negligible, see the link [2]. [1] https://wiki.openstack.org/wiki/Python3 [2] http://lists.openstack.org/pipermail/openstack-dev/2015-June/066391.html Change-Id: If5ab2f298e887a90cd43530e3fccc0294412f5c9 --- openstack/object_store/v1/obj.py | 3 +-- openstack/profile.py | 3 +-- openstack/telemetry/v2/capability.py | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/openstack/object_store/v1/obj.py b/openstack/object_store/v1/obj.py index c0fac8c6a..1be77ec16 100644 --- a/openstack/object_store/v1/obj.py +++ b/openstack/object_store/v1/obj.py @@ -12,7 +12,6 @@ # under the License. import copy -import six from openstack.object_store import object_store_service from openstack.object_store.v1 import _base @@ -168,7 +167,7 @@ def set_metadata(self, session, metadata): # Filter out items with empty values so the create metadata behaviour # is the same as account and container filtered_metadata = \ - {key: value for key, value in six.iteritems(metadata) if value} + {key: value for key, value in metadata.items() if value} # Get a copy of the original metadata so it doesn't get erased on POST # and update it with the new metadata values. diff --git a/openstack/profile.py b/openstack/profile.py index d82b88a6d..69994fafc 100644 --- a/openstack/profile.py +++ b/openstack/profile.py @@ -53,7 +53,6 @@ import copy import logging -import six from openstack.bare_metal import bare_metal_service from openstack.block_store import block_store_service @@ -166,7 +165,7 @@ def _setter(self, service, attr, value): def get_services(self): """Get a list of all the known services.""" services = [] - for name, service in six.iteritems(self._services): + for name, service in self._services.items(): services.append(service) return services diff --git a/openstack/telemetry/v2/capability.py b/openstack/telemetry/v2/capability.py index cfe11571a..be93db841 100644 --- a/openstack/telemetry/v2/capability.py +++ b/openstack/telemetry/v2/capability.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import six from openstack import resource2 as resource from openstack.telemetry import telemetry_service @@ -34,5 +33,5 @@ def list(cls, session, paginated=False, **params): resp = session.get(cls.base_path, endpoint_filter=cls.service, params=params) resp = resp.json() - for key, value in six.iteritems(resp['api']): + for key, value in resp['api'].items(): yield cls.existing(id=key, enabled=value) From f74902b0b9f82e221b1ae1af7f254dc2918f96b8 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Fri, 23 Jun 2017 14:53:54 -0400 Subject: [PATCH 1671/3836] use openstackdocstheme html context Set some of the new config values and enable openstackdocstheme as an extension so it will inject values into the page context as it writes each documentation page. This ensures the pages link to the right bug tracker, etc. Change-Id: Id9cc61e81aa43f4b69883d338090716005477d0a Signed-off-by: Doug Hellmann --- doc/source/conf.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 82f27c36a..cbd9888d1 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -25,9 +25,16 @@ extensions = [ 'sphinx.ext.autodoc', #'sphinx.ext.intersphinx', - 'reno.sphinxext' + 'reno.sphinxext', + 'openstackdocstheme', ] +# openstackdocstheme options +repository_name = 'openstack/os-client-config' +bug_project = 'os-client-config' +bug_tag = '' +html_last_updated_fmt = '%Y-%m-%d %H:%M' + # autodoc generation is a bit aggressive and a nuisance when doing heavy # text edit cycles. # execute "export SPHINX_DEBUG=1" in your terminal to disable From 20b2f1f0c0402f28b6dde1e8ff0c5416edb6d21e Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Fri, 30 Jun 2017 10:07:41 -0400 Subject: [PATCH 1672/3836] reorganize docs using the new standard layout Move the docs around a little to allow the new templated docs.o.o site link to things like the user and install guides in the expected location. Change-Id: I7f3b625c04aa6cd2a7ebe5f2ce4a398cf464b1cc Signed-off-by: Doug Hellmann --- doc/source/contributing.rst | 1 - doc/source/{ => contributor}/coding.rst | 2 +- doc/source/contributor/contributing.rst | 1 + doc/source/contributor/index.rst | 9 +++++++++ doc/source/index.rst | 20 ++++--------------- .../{installation.rst => install/index.rst} | 0 .../index.rst} | 0 .../{ => user}/examples/cleanup-servers.py | 0 .../{ => user}/examples/create-server-dict.py | 0 .../examples/create-server-name-or-id.py | 0 .../{ => user}/examples/debug-logging.py | 0 .../{ => user}/examples/find-an-image.py | 0 .../{ => user}/examples/http-debug-logging.py | 0 .../{ => user}/examples/munch-dict-object.py | 0 .../{ => user}/examples/normalization.py | 0 .../{ => user}/examples/server-information.py | 0 .../examples/service-conditional-overrides.py | 0 .../examples/service-conditionals.py | 0 doc/source/{ => user}/examples/strict-mode.py | 0 .../examples/upload-large-object.py | 0 .../{ => user}/examples/upload-object.py | 0 doc/source/{ => user}/examples/user-agent.py | 0 doc/source/user/index.rst | 19 ++++++++++++++++++ doc/source/{ => user}/logging.rst | 0 doc/source/{ => user}/microversions.rst | 0 doc/source/{ => user}/model.rst | 0 doc/source/{ => user}/multi-cloud-demo.rst | 2 +- doc/source/{ => user}/usage.rst | 0 28 files changed, 35 insertions(+), 19 deletions(-) delete mode 100644 doc/source/contributing.rst rename doc/source/{ => contributor}/coding.rst (98%) create mode 100644 doc/source/contributor/contributing.rst create mode 100644 doc/source/contributor/index.rst rename doc/source/{installation.rst => install/index.rst} (100%) rename doc/source/{releasenotes.rst => releasenotes/index.rst} (100%) rename doc/source/{ => user}/examples/cleanup-servers.py (100%) rename doc/source/{ => user}/examples/create-server-dict.py (100%) rename doc/source/{ => user}/examples/create-server-name-or-id.py (100%) rename doc/source/{ => user}/examples/debug-logging.py (100%) rename doc/source/{ => user}/examples/find-an-image.py (100%) rename doc/source/{ => user}/examples/http-debug-logging.py (100%) rename doc/source/{ => user}/examples/munch-dict-object.py (100%) rename doc/source/{ => user}/examples/normalization.py (100%) rename doc/source/{ => user}/examples/server-information.py (100%) rename doc/source/{ => user}/examples/service-conditional-overrides.py (100%) rename doc/source/{ => user}/examples/service-conditionals.py (100%) rename doc/source/{ => user}/examples/strict-mode.py (100%) rename doc/source/{ => user}/examples/upload-large-object.py (100%) rename doc/source/{ => user}/examples/upload-object.py (100%) rename doc/source/{ => user}/examples/user-agent.py (100%) create mode 100644 doc/source/user/index.rst rename doc/source/{ => user}/logging.rst (100%) rename doc/source/{ => user}/microversions.rst (100%) rename doc/source/{ => user}/model.rst (100%) rename doc/source/{ => user}/multi-cloud-demo.rst (99%) rename doc/source/{ => user}/usage.rst (100%) diff --git a/doc/source/contributing.rst b/doc/source/contributing.rst deleted file mode 100644 index 8cb3146fe..000000000 --- a/doc/source/contributing.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../../CONTRIBUTING.rst \ No newline at end of file diff --git a/doc/source/coding.rst b/doc/source/contributor/coding.rst similarity index 98% rename from doc/source/coding.rst rename to doc/source/contributor/coding.rst index 778089e6d..739b319fa 100644 --- a/doc/source/coding.rst +++ b/doc/source/contributor/coding.rst @@ -73,7 +73,7 @@ All objects should be normalized. It is shade's purpose in life to make OpenStack consistent for end users, and this means not trusting the clouds to return consistent objects. There should be a normalize function in `shade/_normalize.py` that is applied to objects before returning them to -the user. See :doc:`model` for further details on object model requirements. +the user. See :doc:`../user/model` for further details on object model requirements. Fields should not be in the normalization contract if we cannot commit to providing them to all users. diff --git a/doc/source/contributor/contributing.rst b/doc/source/contributor/contributing.rst new file mode 100644 index 000000000..b1cd2f37d --- /dev/null +++ b/doc/source/contributor/contributing.rst @@ -0,0 +1 @@ +.. include:: ../../../CONTRIBUTING.rst diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst new file mode 100644 index 000000000..b032c3728 --- /dev/null +++ b/doc/source/contributor/index.rst @@ -0,0 +1,9 @@ +========================= + Shade Contributor Guide +========================= + +.. toctree:: + :maxdepth: 2 + + contributing + coding diff --git a/doc/source/index.rst b/doc/source/index.rst index be106d6e2..850bcff37 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -11,25 +11,13 @@ Contents: .. toctree:: :maxdepth: 2 - installation - usage - logging - model - contributing - coding - microversions - releasenotes + install/index + user/index + contributor/index + releasenotes/index .. include:: ../../README.rst -Presentations -============= - -.. toctree:: - :maxdepth: 1 - - multi-cloud-demo - Indices and tables ================== diff --git a/doc/source/installation.rst b/doc/source/install/index.rst similarity index 100% rename from doc/source/installation.rst rename to doc/source/install/index.rst diff --git a/doc/source/releasenotes.rst b/doc/source/releasenotes/index.rst similarity index 100% rename from doc/source/releasenotes.rst rename to doc/source/releasenotes/index.rst diff --git a/doc/source/examples/cleanup-servers.py b/doc/source/user/examples/cleanup-servers.py similarity index 100% rename from doc/source/examples/cleanup-servers.py rename to doc/source/user/examples/cleanup-servers.py diff --git a/doc/source/examples/create-server-dict.py b/doc/source/user/examples/create-server-dict.py similarity index 100% rename from doc/source/examples/create-server-dict.py rename to doc/source/user/examples/create-server-dict.py diff --git a/doc/source/examples/create-server-name-or-id.py b/doc/source/user/examples/create-server-name-or-id.py similarity index 100% rename from doc/source/examples/create-server-name-or-id.py rename to doc/source/user/examples/create-server-name-or-id.py diff --git a/doc/source/examples/debug-logging.py b/doc/source/user/examples/debug-logging.py similarity index 100% rename from doc/source/examples/debug-logging.py rename to doc/source/user/examples/debug-logging.py diff --git a/doc/source/examples/find-an-image.py b/doc/source/user/examples/find-an-image.py similarity index 100% rename from doc/source/examples/find-an-image.py rename to doc/source/user/examples/find-an-image.py diff --git a/doc/source/examples/http-debug-logging.py b/doc/source/user/examples/http-debug-logging.py similarity index 100% rename from doc/source/examples/http-debug-logging.py rename to doc/source/user/examples/http-debug-logging.py diff --git a/doc/source/examples/munch-dict-object.py b/doc/source/user/examples/munch-dict-object.py similarity index 100% rename from doc/source/examples/munch-dict-object.py rename to doc/source/user/examples/munch-dict-object.py diff --git a/doc/source/examples/normalization.py b/doc/source/user/examples/normalization.py similarity index 100% rename from doc/source/examples/normalization.py rename to doc/source/user/examples/normalization.py diff --git a/doc/source/examples/server-information.py b/doc/source/user/examples/server-information.py similarity index 100% rename from doc/source/examples/server-information.py rename to doc/source/user/examples/server-information.py diff --git a/doc/source/examples/service-conditional-overrides.py b/doc/source/user/examples/service-conditional-overrides.py similarity index 100% rename from doc/source/examples/service-conditional-overrides.py rename to doc/source/user/examples/service-conditional-overrides.py diff --git a/doc/source/examples/service-conditionals.py b/doc/source/user/examples/service-conditionals.py similarity index 100% rename from doc/source/examples/service-conditionals.py rename to doc/source/user/examples/service-conditionals.py diff --git a/doc/source/examples/strict-mode.py b/doc/source/user/examples/strict-mode.py similarity index 100% rename from doc/source/examples/strict-mode.py rename to doc/source/user/examples/strict-mode.py diff --git a/doc/source/examples/upload-large-object.py b/doc/source/user/examples/upload-large-object.py similarity index 100% rename from doc/source/examples/upload-large-object.py rename to doc/source/user/examples/upload-large-object.py diff --git a/doc/source/examples/upload-object.py b/doc/source/user/examples/upload-object.py similarity index 100% rename from doc/source/examples/upload-object.py rename to doc/source/user/examples/upload-object.py diff --git a/doc/source/examples/user-agent.py b/doc/source/user/examples/user-agent.py similarity index 100% rename from doc/source/examples/user-agent.py rename to doc/source/user/examples/user-agent.py diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst new file mode 100644 index 000000000..0e576834a --- /dev/null +++ b/doc/source/user/index.rst @@ -0,0 +1,19 @@ +================== + Shade User Guide +================== + +.. toctree:: + :maxdepth: 2 + + usage + logging + model + microversions + +Presentations +============= + +.. toctree:: + :maxdepth: 1 + + multi-cloud-demo diff --git a/doc/source/logging.rst b/doc/source/user/logging.rst similarity index 100% rename from doc/source/logging.rst rename to doc/source/user/logging.rst diff --git a/doc/source/microversions.rst b/doc/source/user/microversions.rst similarity index 100% rename from doc/source/microversions.rst rename to doc/source/user/microversions.rst diff --git a/doc/source/model.rst b/doc/source/user/model.rst similarity index 100% rename from doc/source/model.rst rename to doc/source/user/model.rst diff --git a/doc/source/multi-cloud-demo.rst b/doc/source/user/multi-cloud-demo.rst similarity index 99% rename from doc/source/multi-cloud-demo.rst rename to doc/source/user/multi-cloud-demo.rst index 7b584487f..fdd9dcb1b 100644 --- a/doc/source/multi-cloud-demo.rst +++ b/doc/source/user/multi-cloud-demo.rst @@ -7,7 +7,7 @@ walk through it like a presentation, install `presentty` and run: .. code:: bash - presentty doc/source/multi-cloud-demo.rst + presentty doc/source/user/multi-cloud-demo.rst The content is hopefully helpful even if it's not being narrated, so it's being included in the `shade` docs. diff --git a/doc/source/usage.rst b/doc/source/user/usage.rst similarity index 100% rename from doc/source/usage.rst rename to doc/source/user/usage.rst From 65ce1a22896e52ff59a23a393e3bc4227f55f006 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Fri, 30 Jun 2017 10:14:38 -0400 Subject: [PATCH 1673/3836] switch from oslosphinx to openstackdocstheme Change-Id: I83c5856a49cdfd912eb2d62314848a4cc4905fe1 Signed-off-by: Doug Hellmann --- doc/source/conf.py | 9 ++++++++- releasenotes/source/conf.py | 10 ++++++++-- test-requirements.txt | 2 +- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index e55e85b19..aaf41b319 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -5,10 +5,17 @@ extensions = [ 'sphinx.ext.autodoc', - 'oslosphinx', + 'openstackdocstheme', 'reno.sphinxext' ] +# openstackdocstheme options +repository_name = 'openstack-infra/shade' +bug_project = '760' +bug_tag = '' +html_last_updated_fmt = '%Y-%m-%d %H:%M' +html_theme = 'openstackdocs' + # The suffix of source filenames. source_suffix = '.rst' diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py index 1abd91cc2..8e1265344 100644 --- a/releasenotes/source/conf.py +++ b/releasenotes/source/conf.py @@ -38,10 +38,16 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'oslosphinx', + 'openstackdocstheme', 'reno.sphinxext', ] +# openstackdocstheme options +repository_name = 'openstack-infra/shade' +bug_project = '760' +bug_tag = '' +html_last_updated_fmt = '%Y-%m-%d %H:%M' + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -112,7 +118,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +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 diff --git a/test-requirements.txt b/test-requirements.txt index 25a1ef26b..c69d2a2f5 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,7 +7,7 @@ coverage!=4.4,>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD mock>=2.0 # BSD python-subunit>=0.0.18 # Apache-2.0/BSD -oslosphinx>=4.7.0 # Apache-2.0 +openstackdocstheme>=1.11.0 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 sphinx>=1.6.2 # BSD testrepository>=0.0.18 # Apache-2.0/BSD From 1e1c6ac1d8c1bc9e994bef3610d07ed3b44b8ff9 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Tue, 27 Jun 2017 14:39:01 -0400 Subject: [PATCH 1674/3836] De-client-ify Domain Create Change-Id: I09426fd4fd374bdb64f42f3b31862dd3b898310d --- shade/_adapter.py | 2 +- shade/_tasks.py | 5 ----- shade/operatorcloud.py | 22 +++++++++++----------- shade/tests/unit/test_domains.py | 14 +++++++------- 4 files changed, 19 insertions(+), 24 deletions(-) diff --git a/shade/_adapter.py b/shade/_adapter.py index 9abd9d3f1..0893c92b4 100644 --- a/shade/_adapter.py +++ b/shade/_adapter.py @@ -142,7 +142,7 @@ def _munch_response(self, response, result_key=None, error_message=None): 'project', 'tenant', 'servers', 'server', 'flavor', 'flavors', 'baymodels', 'aggregate', 'aggregates', 'availabilityZoneInfo', - 'flavor_access', 'output', 'server_groups']: + 'flavor_access', 'output', 'server_groups', 'domain']: if key in result_json.keys(): self._log_request_id(response) return result_json diff --git a/shade/_tasks.py b/shade/_tasks.py index c944347a4..3d596ccdc 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -167,11 +167,6 @@ def main(self, client): return client.keystone_client.endpoints.delete(**self.args) -class DomainCreate(task_manager.Task): - def main(self, client): - return client.keystone_client.domains.create(**self.args) - - class DomainList(task_manager.Task): def main(self, client): return client.keystone_client.domains.list(**self.args) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index dd4ac631a..618e72bfd 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1077,24 +1077,24 @@ def delete_endpoint(self, id): return True - def create_domain( - self, name, description=None, enabled=True): - """Create a Keystone domain. + def create_domain(self, name, description=None, enabled=True): + """Create a domain. :param name: The name of the domain. :param description: A description of the domain. :param enabled: Is the domain enabled or not (default True). - :returns: a ``munch.Munch`` containing the domain description + :returns: a ``munch.Munch`` containing the domain representation. - :raise OpenStackCloudException: if the domain cannot be created + :raise OpenStackCloudException: if the domain cannot be created. """ - with _utils.shade_exceptions("Failed to create domain {name}".format( - name=name)): - domain = self.manager.submit_task(_tasks.DomainCreate( - name=name, - description=description, - enabled=enabled)) + domain_ref = {'name': name, 'enabled': enabled} + if description is not None: + domain_ref['description'] = description + msg = 'Failed to create domain {name}'.format(name=name) + data = self._identity_client.post( + '/domains', json={'domain': domain_ref}, error_message=msg) + domain = meta.get_and_munchify('domain', data) return _utils.normalize_domains([domain])[0] def update_domain( diff --git a/shade/tests/unit/test_domains.py b/shade/tests/unit/test_domains.py index 90cf6874b..a952d34a4 100644 --- a/shade/tests/unit/test_domains.py +++ b/shade/tests/unit/test_domains.py @@ -81,11 +81,7 @@ def test_create_domain(self): self.register_uris([ dict(method='POST', uri=self.get_mock_url(), status_code=200, json=domain_data.json_response, - validate=dict(json=domain_data.json_request)), - dict(method='GET', - uri=self.get_mock_url(append=[domain_data.domain_id]), - status_code=200, - json=domain_data.json_request)]) + validate=dict(json=domain_data.json_request))]) domain = self.op_cloud.create_domain( domain_data.domain_name, domain_data.description) self.assertThat(domain.id, matchers.Equals(domain_data.domain_id)) @@ -95,12 +91,16 @@ def test_create_domain(self): self.assert_calls() def test_create_domain_exception(self): + domain_data = self._get_domain_data(domain_name='domain_name', + enabled=True) with testtools.ExpectedException( - shade.OpenStackCloudException, + shade.OpenStackCloudBadRequest, "Failed to create domain domain_name" ): self.register_uris([ - dict(method='POST', uri=self.get_mock_url(), status_code=409)]) + dict(method='POST', uri=self.get_mock_url(), status_code=400, + json=domain_data.json_response, + validate=dict(json=domain_data.json_request))]) self.op_cloud.create_domain('domain_name') self.assert_calls() From 790fffd37a4fa1ab584c40c0abeb56b7373d53df Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Tue, 27 Jun 2017 19:31:00 -0400 Subject: [PATCH 1675/3836] De-client-ify Domain Update and Delete Change-Id: I56e661ca38e9a94ab43ccc3c0cc45a42976eac3d --- shade/_tasks.py | 10 ---------- shade/operatorcloud.py | 30 +++++++++++++++++------------- shade/tests/unit/test_domains.py | 22 +++++++--------------- 3 files changed, 24 insertions(+), 38 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 3d596ccdc..ba433ad70 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -177,16 +177,6 @@ def main(self, client): return client.keystone_client.domains.get(**self.args) -class DomainUpdate(task_manager.Task): - def main(self, client): - return client.keystone_client.domains.update(**self.args) - - -class DomainDelete(task_manager.Task): - def main(self, client): - return client.keystone_client.domains.delete(**self.args) - - class GroupList(task_manager.Task): def main(self, client): return client.keystone_client.groups.list() diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 618e72bfd..4b2246023 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1112,15 +1112,20 @@ def update_domain( ) domain_id = dom['id'] - with _utils.shade_exceptions( - "Error in updating domain {domain}".format(domain=domain_id)): - domain = self.manager.submit_task(_tasks.DomainUpdate( - domain=domain_id, name=name, description=description, - enabled=enabled)) + domain_ref = {} + domain_ref.update({'name': name} if name else {}) + domain_ref.update({'description': description} if description else {}) + domain_ref.update({'enabled': enabled} if enabled is not None else {}) + + error_msg = "Error in updating domain {id}".format(id=domain_id) + data = self._identity_client.patch( + '/domains/{id}'.format(id=domain_id), + json={'domain': domain_ref}, error_message=error_msg) + domain = meta.get_and_munchify('domain', data) return _utils.normalize_domains([domain])[0] def delete_domain(self, domain_id=None, name_or_id=None): - """Delete a Keystone domain. + """Delete a domain. :param domain_id: ID of the domain to delete. :param name_or_id: Name or ID of the domain to delete. @@ -1135,19 +1140,18 @@ def delete_domain(self, domain_id=None, name_or_id=None): raise OpenStackCloudException( "You must pass either domain_id or name_or_id value" ) - dom = self.get_domain(None, name_or_id) + dom = self.get_domain(name_or_id=name_or_id) if dom is None: self.log.debug( "Domain %s not found for deleting", name_or_id) return False domain_id = dom['id'] - with _utils.shade_exceptions( - "Failed to delete domain {id}".format(id=domain_id)): - # Deleting a domain is expensive, so disabling it first increases - # the changes of success - domain = self.update_domain(domain_id, enabled=False) - self.manager.submit_task(_tasks.DomainDelete(domain=domain['id'])) + # A domain must be disabled before deleting + self.update_domain(domain_id, enabled=False) + error_msg = "Failed to delete domain {id}".format(id=domain_id) + self._identity_client.delete('/domains/{id}'.format(id=domain_id), + error_message=error_msg) return True diff --git a/shade/tests/unit/test_domains.py b/shade/tests/unit/test_domains.py index a952d34a4..64d2a0289 100644 --- a/shade/tests/unit/test_domains.py +++ b/shade/tests/unit/test_domains.py @@ -112,8 +112,6 @@ def test_delete_domain(self): self.register_uris([ dict(method='PATCH', uri=domain_resource_uri, status_code=200, json=new_resp, validate={'domain': {'enabled': False}}), - dict(method='GET', uri=domain_resource_uri, status_code=200, - json=new_resp), dict(method='DELETE', uri=domain_resource_uri, status_code=204)]) self.op_cloud.delete_domain(domain_data.domain_id) self.assert_calls() @@ -129,8 +127,6 @@ def test_delete_domain_name_or_id(self): json={'domains': [domain_data.json_response['domain']]}), dict(method='PATCH', uri=domain_resource_uri, status_code=200, json=new_resp, validate={'domain': {'enabled': False}}), - dict(method='GET', uri=domain_resource_uri, status_code=200, - json=new_resp), dict(method='DELETE', uri=domain_resource_uri, status_code=204)]) self.op_cloud.delete_domain(name_or_id=domain_data.domain_id) self.assert_calls() @@ -148,11 +144,9 @@ def test_delete_domain_exception(self): self.register_uris([ dict(method='PATCH', uri=domain_resource_uri, status_code=200, json=new_resp, validate={'domain': {'enabled': False}}), - dict(method='GET', uri=domain_resource_uri, status_code=200, - json=new_resp), dict(method='DELETE', uri=domain_resource_uri, status_code=404)]) with testtools.ExpectedException( - shade.OpenStackCloudException, + shade.OpenStackCloudURINotFound, "Failed to delete domain %s" % domain_data.domain_id ): self.op_cloud.delete_domain(domain_data.domain_id) @@ -165,9 +159,7 @@ def test_update_domain(self): self.register_uris([ dict(method='PATCH', uri=domain_resource_uri, status_code=200, json=domain_data.json_response, - validate=dict(json=domain_data.json_request)), - dict(method='GET', uri=domain_resource_uri, status_code=200, - json=domain_data.json_response)]) + validate=dict(json=domain_data.json_request))]) domain = self.op_cloud.update_domain( domain_data.domain_id, name=domain_data.domain_name, @@ -187,9 +179,7 @@ def test_update_domain_name_or_id(self): json={'domains': [domain_data.json_response['domain']]}), dict(method='PATCH', uri=domain_resource_uri, status_code=200, json=domain_data.json_response, - validate=dict(json=domain_data.json_request)), - dict(method='GET', uri=domain_resource_uri, status_code=200, - json=domain_data.json_response)]) + validate=dict(json=domain_data.json_request))]) domain = self.op_cloud.update_domain( name_or_id=domain_data.domain_id, name=domain_data.domain_name, @@ -206,9 +196,11 @@ def test_update_domain_exception(self): self.register_uris([ dict(method='PATCH', uri=self.get_mock_url(append=[domain_data.domain_id]), - status_code=409)]) + status_code=409, + json=domain_data.json_response, + validate=dict(json={'domain': {'enabled': False}}))]) with testtools.ExpectedException( - shade.OpenStackCloudException, + shade.OpenStackCloudHTTPError, "Error in updating domain %s" % domain_data.domain_id ): self.op_cloud.delete_domain(domain_data.domain_id) From 67b053c7b1a640de98bc5d243d8db3db227a2dac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Sun, 2 Jul 2017 20:49:34 +0000 Subject: [PATCH 1676/3836] Add Neutron QoS policies commands Create/Update/List/Get/Delete QoS policies is now possible to do with shade. Change-Id: I127463dff9f73307aaac7ebb6927ef23599d3682 --- shade/_adapter.py | 1 + shade/openstackcloud.py | 152 ++++++++++++++++++++++++++++ shade/tests/unit/test_qos_policy.py | 148 +++++++++++++++++++++++++++ 3 files changed, 301 insertions(+) create mode 100644 shade/tests/unit/test_qos_policy.py diff --git a/shade/_adapter.py b/shade/_adapter.py index 9abd9d3f1..ff84ce324 100644 --- a/shade/_adapter.py +++ b/shade/_adapter.py @@ -135,6 +135,7 @@ def _munch_response(self, response, result_key=None, error_message=None): 'network', 'networks', 'subnet', 'subnets', 'router', 'routers', 'floatingip', 'floatingips', 'floating_ip', 'floating_ips', 'port', 'ports', + 'policy', 'policies', 'stack', 'stacks', 'zones', 'events', 'security_group', 'security_groups', 'security_group_rule', 'security_group_rules', diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index b221684ac..fd85bde0d 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1513,6 +1513,21 @@ def search_ports(self, name_or_id=None, filters=None): ports = self.list_ports(pushdown_filters) return _utils._filter_list(ports, name_or_id, filters) + def search_qos_policies(self, name_or_id=None, filters=None): + """Search QoS policies + + :param name_or_id: Name or ID of the desired policy. + :param filters: a dict containing additional filters to use. e.g. + {'shared': True} + + :returns: a list of ``munch.Munch`` containing the network description. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + OpenStack API call. + """ + policies = self.list_qos_policies(filters) + return _utils._filter_list(policies, name_or_id, filters) + def search_volumes(self, name_or_id=None, filters=None): volumes = self.list_volumes() return _utils._filter_list( @@ -1690,6 +1705,21 @@ def _list_ports(self, filters): error_message="Error fetching port list") return meta.get_and_munchify('ports', data) + def list_qos_policies(self, filters=None): + """List all available QoS policies. + + :param filters: (optional) dict of filter conditions to push down + :returns: A list of policies ``munch.Munch``. + + """ + # Translate None from search interface to empty {} for kwargs below + if not filters: + filters = {} + data = self._network_client.get( + "/qos/policies.json", params=filters, + error_message="Error fetching QoS policies list") + return meta.get_and_munchify('policies', data) + @_utils.cache_on_arguments(should_cache_fn=_no_pending_volumes) def list_volumes(self, cache=True): """List all available volumes. @@ -2642,6 +2672,32 @@ def get_port(self, name_or_id, filters=None): """ return _utils._get_entity(self.search_ports, name_or_id, filters) + def get_qos_policy(self, name_or_id, filters=None): + """Get a QoS policy by name or ID. + + :param name_or_id: Name or ID of the policy. + :param filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: A policy ``munch.Munch`` or None if no matching network is + found. + + """ + return _utils._get_entity( + self.search_qos_policies, name_or_id, filters) + def get_volume(self, name_or_id, filters=None): """Get a volume by name or ID. @@ -3090,6 +3146,102 @@ def delete_network(self, name_or_id): return True + def create_qos_policy(self, name=None, description=None, shared=None, + default=None, project_id=None): + """Create a QoS policy. + + :param string name: Name of the QoS policy being created. + :param string description: Description of created QoS policy. + :param bool shared: Set the QoS policy as shared. + :param bool default: Set the QoS policy as default for project. + :param string project_id: Specify the project ID this QoS policy + will be created on (admin-only). + + :returns: The QoS policy object. + :raises: OpenStackCloudException on operation error. + """ + policy = {} + if name: + policy['name'] = name + if description: + policy['description'] = description + if shared is not None: + policy['shared'] = shared + # TODO(slaweq): this should be used only if proper API extension is + # available in Neutron + if default is not None: + policy['is_default'] = default + if project_id: + policy['project_id'] = project_id + + data = self._network_client.post("/qos/policies.json", + json={'policy': policy}) + return meta.get_and_munchify('policy', data) + + def update_qos_policy(self, name_or_id, policy_name=None, + description=None, shared=None, default=None): + """Update an existing QoS policy. + + :param string name_or_id: + Name or ID of the QoS policy to update. + :param string policy_name: + The new name of the QoS policy. + :param string description: + The new description of the QoS policy. + :param bool shared: + If True, the QoS policy will be set as shared. + :param bool default: + If True, the QoS policy will be set as default for project. + + :returns: The updated QoS policy object. + :raises: OpenStackCloudException on operation error. + """ + policy = {} + if policy_name: + policy['name'] = policy_name + if description: + policy['description'] = description + if shared is not None: + policy['shared'] = shared + # TODO(slaweq): this should be used only if proper API extension is + # available in Neutron + if default is not None: + policy['is_default'] = default + + if not policy: + self.log.debug("No QoS policy data to update") + return + + curr_policy = self.get_qos_policy(name_or_id) + if not curr_policy: + raise OpenStackCloudException( + "QoS policy %s not found." % name_or_id) + + data = self._network_client.put( + "/qos/policies/{policy_id}.json".format( + policy_id=curr_policy['id']), + json={'policy': policy}) + return meta.get_and_munchify('policy', data) + + def delete_qos_policy(self, name_or_id): + """Delete a QoS policy. + + :param name_or_id: Name or ID of the policy being deleted. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException on operation error. + """ + policy = self.get_qos_policy(name_or_id) + if not policy: + self.log.debug("QoS policy %s not found for deleting", name_or_id) + return False + + self._network_client.delete( + "/qos/policies/{policy_id}.json".format(policy_id=policy['id'])) + + return True + def _build_external_gateway_info(self, ext_gateway_net_id, enable_snat, ext_fixed_ips): info = {} diff --git a/shade/tests/unit/test_qos_policy.py b/shade/tests/unit/test_qos_policy.py new file mode 100644 index 000000000..c26f4e32e --- /dev/null +++ b/shade/tests/unit/test_qos_policy.py @@ -0,0 +1,148 @@ +# Copyright 2017 OVH SAS +# 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 copy + +from shade import exc +from shade.tests.unit import base + + +class TestQosPolicy(base.RequestsMockTestCase): + + policy_name = 'qos test policy' + policy_id = '881d1bb7-a663-44c0-8f9f-ee2765b74486' + project_id = 'c88fc89f-5121-4a4c-87fd-496b5af864e9' + + mock_policy = { + 'id': policy_id, + 'name': policy_name, + 'description': '', + 'rules': [], + 'project_id': project_id, + 'tenant_id': project_id, + 'shared': False, + 'is_default': False + } + + def test_get_qos_policy(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': [self.mock_policy]}) + ]) + r = self.cloud.get_qos_policy(self.policy_name) + self.assertIsNotNone(r) + self.assertDictEqual(self.mock_policy, r) + self.assert_calls() + + def test_create_qos_policy(self): + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policy': self.mock_policy}) + ]) + policy = self.cloud.create_qos_policy( + name=self.policy_name, project_id=self.project_id) + self.assertDictEqual(self.mock_policy, policy) + self.assert_calls() + + def test_delete_qos_policy(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': [self.mock_policy]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies', + '%s.json' % self.policy_id]), + json={}) + ]) + self.assertTrue(self.cloud.delete_qos_policy(self.policy_name)) + self.assert_calls() + + def test_delete_qos_policy_not_found(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': []}) + ]) + self.assertFalse(self.cloud.delete_qos_policy('goofy')) + self.assert_calls() + + def test_delete_qos_policy_multiple_found(self): + policy1 = dict(id='123', name=self.policy_name) + policy2 = dict(id='456', name=self.policy_name) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': [policy1, policy2]}) + ]) + self.assertRaises(exc.OpenStackCloudException, + self.cloud.delete_qos_policy, + self.policy_name) + self.assert_calls() + + def test_delete_qos_policy_multiple_using_id(self): + policy1 = self.mock_policy + policy2 = dict(id='456', name=self.policy_name) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': [policy1, policy2]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies', + '%s.json' % self.policy_id]), + json={}) + ]) + self.assertTrue(self.cloud.delete_qos_policy(policy1['id'])) + self.assert_calls() + + def test_update_qos_policy(self): + expected_policy = copy.copy(self.mock_policy) + expected_policy['name'] = 'goofy' + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': [self.mock_policy]}), + dict(method='PUT', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies', + '%s.json' % self.policy_id]), + json={'policy': expected_policy}, + validate=dict( + json={'policy': {'name': 'goofy'}})) + ]) + policy = self.cloud.update_qos_policy( + self.policy_id, policy_name='goofy') + self.assertDictEqual(expected_policy, policy) + self.assert_calls() From e3753627042e53ecc9086736ed47ecf4b1e623ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Tue, 4 Jul 2017 22:08:04 +0000 Subject: [PATCH 1677/3836] Add searching for Neutron API extensions Shade client can now check if required API extension is available in Neutron. Change-Id: I9ffe431b23ac8586d327489347abaa40b0c1d775 --- shade/openstackcloud.py | 14 +++++ shade/tests/unit/test_shade.py | 98 ++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index fd85bde0d..73325df04 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1446,6 +1446,20 @@ def search_keypairs(self, name_or_id=None, filters=None): keypairs = self.list_keypairs() return _utils._filter_list(keypairs, name_or_id, filters) + @_utils.cache_on_arguments() + def _neutron_extensions(self): + extensions = set() + + for extension in self._network_client.get( + '/extensions.json', + error_message="Error fetching extension list for neutron"): + extensions.add(extension['alias']) + + return extensions + + def _has_neutron_extension(self, extension_alias): + return extension_alias in self._neutron_extensions() + def search_networks(self, name_or_id=None, filters=None): """Search networks diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 73b51f163..65e398de4 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -304,6 +304,104 @@ def test__has_nova_extension_missing(self): self.assert_calls() + def test__neutron_extensions(self): + body = [ + { + "updated": "2014-06-1T10:00:00-00:00", + "name": "Distributed Virtual Router", + "links": [], + "alias": "dvr", + "description": + "Enables configuration of Distributed Virtual Routers." + }, + { + "updated": "2013-07-23T10:00:00-00:00", + "name": "Allowed Address Pairs", + "links": [], + "alias": "allowed-address-pairs", + "description": "Provides allowed address pairs" + }, + ] + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json=dict(extensions=body)) + ]) + extensions = self.cloud._neutron_extensions() + self.assertEqual(set(['dvr', 'allowed-address-pairs']), extensions) + + self.assert_calls() + + def test__neutron_extensions_fails(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + status_code=404) + ]) + with testtools.ExpectedException( + exc.OpenStackCloudURINotFound, + "Error fetching extension list for neutron" + ): + self.cloud._neutron_extensions() + + self.assert_calls() + + def test__has_neutron_extension(self): + body = [ + { + "updated": "2014-06-1T10:00:00-00:00", + "name": "Distributed Virtual Router", + "links": [], + "alias": "dvr", + "description": + "Enables configuration of Distributed Virtual Routers." + }, + { + "updated": "2013-07-23T10:00:00-00:00", + "name": "Allowed Address Pairs", + "links": [], + "alias": "allowed-address-pairs", + "description": "Provides allowed address pairs" + }, + ] + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json=dict(extensions=body)) + ]) + self.assertTrue(self.cloud._has_neutron_extension('dvr')) + self.assert_calls() + + def test__has_neutron_extension_missing(self): + body = [ + { + "updated": "2014-06-1T10:00:00-00:00", + "name": "Distributed Virtual Router", + "links": [], + "alias": "dvr", + "description": + "Enables configuration of Distributed Virtual Routers." + }, + { + "updated": "2013-07-23T10:00:00-00:00", + "name": "Allowed Address Pairs", + "links": [], + "alias": "allowed-address-pairs", + "description": "Provides allowed address pairs" + }, + ] + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json=dict(extensions=body)) + ]) + self.assertFalse(self.cloud._has_neutron_extension('invalid')) + self.assert_calls() + def test_range_search(self): filters = {"key1": "min", "key2": "20"} retval = self.cloud.range_search(RANGE_DATA, filters) From ab2c4b314fc0a58c138dd2798a947be1fd957208 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 5 Jul 2017 08:56:49 -0500 Subject: [PATCH 1678/3836] Use the right variable name in userdata encoding The variable is "userdata" not "text". Change-Id: I1fecdf088f3aba744b5060e36dcfe62674772d1b --- shade/openstackcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index fd85bde0d..3ae105e7a 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -5615,7 +5615,7 @@ def _encode_server_userdata(self, userdata): if not isinstance(userdata, six.binary_type): # If the userdata passed in is bytes, just send it unmodified - if not isinstance(text, six.string_types): + if not isinstance(userdata, six.string_types): raise TypeError("%s can't be encoded" % type(text)) # If it's not bytes, make it bytes userdata = userdata.encode('utf-8', 'strict') From ecd470db33bd1458a346f95355b111882073d43f Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Wed, 14 Jun 2017 21:37:00 -0400 Subject: [PATCH 1679/3836] De-client-ify User Create Change-Id: I29b8502883f6ce58e6d2dcfb72df38d6989c18f9 --- shade/openstackcloud.py | 66 +++++++++----------------- shade/tests/unit/base.py | 3 +- shade/tests/unit/test_caching.py | 4 +- shade/tests/unit/test_domain_params.py | 8 ++-- shade/tests/unit/test_users.py | 12 +---- 5 files changed, 31 insertions(+), 62 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 73325df04..01df143a1 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -722,38 +722,19 @@ def _project_manager(self): return self.keystone_client.tenants return self.keystone_client.projects - def _get_project_param_dict(self, name_or_id): - project_dict = dict() + def _get_project_id_param_dict(self, name_or_id): if name_or_id: project = self.get_project(name_or_id) if not project: - return project_dict + return {} if self.cloud_config.get_api_version('identity') == '3': - project_dict['default_project'] = project['id'] + return {'default_project_id': project['id']} else: - project_dict['tenant_id'] = project['id'] - return project_dict - - def _get_domain_id_param_dict(self, domain_id): - """Get a useable domain.""" - - # Keystone v3 requires domains for user and project creation. v2 does - # not. However, keystone v2 does not allow user creation by non-admin - # users, so we can throw an error to the user that does not need to - # mention api versions - if self.cloud_config.get_api_version('identity') == '3': - if not domain_id: - raise OpenStackCloudException( - "User or project creation requires an explicit" - " domain_id argument.") - else: - return {'domain_id': domain_id} + return {'tenant_id': project['id']} else: return {} - # TODO(samueldmq): Get rid of this method once create_user is migrated to - # REST and, consequently, _get_domain_id_param_dict is used instead - def _get_domain_param_dict(self, domain_id): + def _get_domain_id_param_dict(self, domain_id): """Get a useable domain.""" # Keystone v3 requires domains for user and project creation. v2 does @@ -766,7 +747,7 @@ def _get_domain_param_dict(self, domain_id): "User or project creation requires an explicit" " domain_id argument.") else: - return {'domain': domain_id} + return {'domain_id': domain_id} else: return {} @@ -777,8 +758,8 @@ def _get_identity_params(self, domain_id=None, project=None): pass project or tenant_id or domain or nothing in a sane manner. """ ret = {} - ret.update(self._get_domain_param_dict(domain_id)) - ret.update(self._get_project_param_dict(project)) + ret.update(self._get_domain_id_param_dict(domain_id)) + ret.update(self._get_project_id_param_dict(project)) return ret def range_search(self, data, filters): @@ -1071,23 +1052,20 @@ def create_user( self, name, password=None, email=None, default_project=None, enabled=True, domain_id=None, description=None): """Create a user.""" - with _utils.shade_exceptions("Error in creating user {user}".format( - user=name)): - identity_params = self._get_identity_params( - domain_id, default_project) - if self.cloud_config.get_api_version('identity') != '3': - if description is not None: - self.log.info( - "description parameter is not supported on Keystone v2" - ) - user = self.manager.submit_task(_tasks.UserCreate( - name=name, password=password, email=email, - enabled=enabled, **identity_params)) - else: - user = self.manager.submit_task(_tasks.UserCreate( - name=name, password=password, email=email, - enabled=enabled, description=description, - **identity_params)) + params = self._get_identity_params(domain_id, default_project) + params.update({'name': name, 'password': password, 'email': email, + 'enabled': enabled}) + if self.cloud_config.get_api_version('identity') == '3': + params['description'] = description + elif description is not None: + self.log.info( + "description parameter is not supported on Keystone v2") + + error_msg = "Error in creating user {user}".format(user=name) + data = self._identity_client.post('/users', json={'user': params}, + error_message=error_msg) + user = meta.get_and_munchify('user', data) + self.list_users.invalidate(self) return _utils.normalize_users([user])[0] diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 47bf1d9e2..3523152dc 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -290,11 +290,12 @@ def _get_user_data(self, name=None, password=None, **kwargs): user_id = uuid.uuid4().hex response = {'name': name, 'id': user_id} - request = {'name': name, 'password': password, 'tenantId': None} + request = {'name': name, 'password': password} if kwargs.get('domain_id'): kwargs['domain_id'] = uuid.UUID(kwargs['domain_id']).hex response['domain_id'] = kwargs.pop('domain_id') + request['domain_id'] = response['domain_id'] response['email'] = kwargs.pop('email', None) request['email'] = response['email'] diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 0f5600ff0..7fdb49dd9 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -338,7 +338,7 @@ def test_modify_user_invalidates_cache(self): user_data = self._get_user_data(email='test@example.com') new_resp = {'user': user_data.json_response['user'].copy()} - new_resp['user']['email'] = 'Nope@Nope.Nope' + new_resp['user']['email'] = 'updated@example.com' new_req = {'user': {'email': new_resp['user']['email']}} mock_users_url = self.get_mock_url( @@ -364,8 +364,6 @@ def test_modify_user_invalidates_cache(self): dict(method='POST', uri=mock_users_url, status_code=200, json=user_data.json_response, validate=user_data.json_request), - dict(method='GET', uri=mock_user_resource_url, status_code=200, - json=user_data.json_response), # List Users Call dict(method='GET', uri=mock_users_url, status_code=200, json=users_list_resp), diff --git a/shade/tests/unit/test_domain_params.py b/shade/tests/unit/test_domain_params.py index 930f2f4a0..1ff5586fc 100644 --- a/shade/tests/unit/test_domain_params.py +++ b/shade/tests/unit/test_domain_params.py @@ -29,10 +29,10 @@ def test_identity_params_v3(self, mock_get_project, mock_api_version): mock_api_version.return_value = '3' ret = self.cloud._get_identity_params(domain_id='5678', project='bar') - self.assertIn('default_project', ret) - self.assertEqual(ret['default_project'], 1234) - self.assertIn('domain', ret) - self.assertEqual(ret['domain'], '5678') + self.assertIn('default_project_id', ret) + self.assertEqual(ret['default_project_id'], 1234) + self.assertIn('domain_id', ret) + self.assertEqual(ret['domain_id'], '5678') @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @mock.patch.object(shade.OpenStackCloud, 'get_project') diff --git a/shade/tests/unit/test_users.py b/shade/tests/unit/test_users.py index 8ab9ac6d7..97e5affab 100644 --- a/shade/tests/unit/test_users.py +++ b/shade/tests/unit/test_users.py @@ -51,11 +51,7 @@ def test_create_user_v2(self): uri=self._get_keystone_mock_url(resource='users', v3=False), status_code=200, json=user_data.json_response, - validate=user_data.json_request), - dict(method='GET', - uri=self._get_keystone_mock_url(resource='users', v3=False, - append=[user_data.user_id]), - status_code=200, json=user_data.json_response)]) + validate=dict(json=user_data.json_request))]) user = self.op_cloud.create_user( name=user_data.name, email=user_data.email, @@ -75,11 +71,7 @@ def test_create_user_v3(self): dict(method='POST', uri=self._get_keystone_mock_url(resource='users'), status_code=200, json=user_data.json_response, - validate=user_data.json_request), - dict(method='GET', - uri=self._get_keystone_mock_url( - resource='users', append=[user_data.user_id]), - status_code=200, json=user_data.json_response)]) + validate=dict(json=user_data.json_request))]) user = self.op_cloud.create_user( name=user_data.name, email=user_data.email, From cac429f4a9fe940fc03dc0f06c8f79e64f95d2df Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Tue, 27 Jun 2017 19:38:53 -0400 Subject: [PATCH 1680/3836] De-client-ify Domain List Change-Id: I08332dc617fe2f77ddefa6dded86fe964b4fd1e0 --- shade/_adapter.py | 3 ++- shade/operatorcloud.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/shade/_adapter.py b/shade/_adapter.py index 544f9976a..639242077 100644 --- a/shade/_adapter.py +++ b/shade/_adapter.py @@ -143,7 +143,8 @@ def _munch_response(self, response, result_key=None, error_message=None): 'project', 'tenant', 'servers', 'server', 'flavor', 'flavors', 'baymodels', 'aggregate', 'aggregates', 'availabilityZoneInfo', - 'flavor_access', 'output', 'server_groups', 'domain']: + 'flavor_access', 'output', 'server_groups', 'domain', + 'domains']: if key in result_json.keys(): self._log_request_id(response) return result_json diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 4b2246023..1f8769006 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1163,9 +1163,9 @@ def list_domains(self): :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ - with _utils.shade_exceptions("Failed to list domains"): - domains = self.manager.submit_task(_tasks.DomainList()) - return _utils.normalize_domains(domains) + data = self._identity_client.get( + '/domains', error_message="Failed to list domains") + return _utils.normalize_domains(meta.get_and_munchify('domains', data)) def search_domains(self, filters=None, name_or_id=None): """Search Keystone domains. From 1bd19cca5cab0792c906bb1417c771ee960e10ca Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Tue, 27 Jun 2017 20:39:42 -0400 Subject: [PATCH 1681/3836] De-client-ify Domain Get Change-Id: I43dab8b2cbc9f3f9f2e34fdd7d0cec915d3abdb8 --- shade/_tasks.py | 5 ----- shade/operatorcloud.py | 11 +++++------ shade/tests/unit/test_role_assignment.py | 10 ++++------ 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index ba433ad70..2f164f5ee 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -172,11 +172,6 @@ def main(self, client): return client.keystone_client.domains.list(**self.args) -class DomainGet(task_manager.Task): - def main(self, client): - return client.keystone_client.domains.get(**self.args) - - class GroupList(task_manager.Task): def main(self, client): return client.keystone_client.groups.list() diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 1f8769006..1e9fec4a3 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1215,12 +1215,11 @@ def get_domain(self, domain_id=None, name_or_id=None, filters=None): if domain_id is None: return _utils._get_entity(self.search_domains, filters, name_or_id) else: - with _utils.shade_exceptions( - "Failed to get domain " - "{domain_id}".format(domain_id=domain_id) - ): - domain = self.manager.submit_task( - _tasks.DomainGet(domain=domain_id)) + error_msg = 'Failed to get domain {id}'.format(id=domain_id) + data = self._identity_client.get( + '/domains/{id}'.format(id=domain_id), + error_message=error_msg) + domain = meta.get_and_munchify('domain', data) return _utils.normalize_domains([domain])[0] @_utils.cache_on_arguments() diff --git a/shade/tests/unit/test_role_assignment.py b/shade/tests/unit/test_role_assignment.py index dd08b2bda..3aedd0fd9 100644 --- a/shade/tests/unit/test_role_assignment.py +++ b/shade/tests/unit/test_role_assignment.py @@ -2644,9 +2644,8 @@ def test_grant_bad_domain_exception(self): text='Could not find domain: baddomain') ]) with testtools.ExpectedException( - exc.OpenStackCloudException, - 'Failed to get domain baddomain ' - '\(Inner Exception: Not Found \(HTTP 404\)\)' + exc.OpenStackCloudURINotFound, + 'Failed to get domain baddomain' ): self.op_cloud.grant_role( self.role_data.role_name, @@ -2667,9 +2666,8 @@ def test_revoke_bad_domain_exception(self): text='Could not find domain: baddomain') ]) with testtools.ExpectedException( - exc.OpenStackCloudException, - 'Failed to get domain baddomain ' - '\(Inner Exception: Not Found \(HTTP 404\)\)' + exc.OpenStackCloudURINotFound, + 'Failed to get domain baddomain' ): self.op_cloud.revoke_role( self.role_data.role_name, From 29dd83d4e0fedcc9ae36576ff3293ac4d2252d27 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Tue, 27 Jun 2017 20:54:02 -0400 Subject: [PATCH 1682/3836] De-client-ify Domain Search Change-Id: If3e2d677cbc8f936144ae1e433eb3f23540e90eb --- shade/_tasks.py | 5 ----- shade/operatorcloud.py | 9 +++------ 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 2f164f5ee..28d8f9a57 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -167,11 +167,6 @@ def main(self, client): return client.keystone_client.endpoints.delete(**self.args) -class DomainList(task_manager.Task): - def main(self, client): - return client.keystone_client.domains.list(**self.args) - - class GroupList(task_manager.Task): def main(self, client): return client.keystone_client.groups.list() diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 1e9fec4a3..c01a1ce37 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1155,7 +1155,7 @@ def delete_domain(self, domain_id=None, name_or_id=None): return True - def list_domains(self): + def list_domains(self, **filters): """List Keystone domains. :returns: a list of ``munch.Munch`` containing the domain description. @@ -1164,7 +1164,7 @@ def list_domains(self): the openstack API call. """ data = self._identity_client.get( - '/domains', error_message="Failed to list domains") + '/domains', params=filters, error_message="Failed to list domains") return _utils.normalize_domains(meta.get_and_munchify('domains', data)) def search_domains(self, filters=None, name_or_id=None): @@ -1189,10 +1189,7 @@ def search_domains(self, filters=None, name_or_id=None): domains = self.list_domains() return _utils._filter_list(domains, name_or_id, filters) else: - with _utils.shade_exceptions("Failed to list domains"): - domains = self.manager.submit_task( - _tasks.DomainList(**filters)) - return _utils.normalize_domains(domains) + return self.list_domains(**filters) def get_domain(self, domain_id=None, name_or_id=None, filters=None): """Get exactly one Keystone domain. From 4446ba9920315ff57b8bd39bc2fdfaeec02d056b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Thu, 6 Jul 2017 17:46:14 +0000 Subject: [PATCH 1683/3836] Add validation of required QoS extensions in Neutron Before operations on QoS policies shade can now check if required extensions (qos and qos-default) are available in cloud. Change-Id: I81727e25b72580d748757ead2c9a38708160d6c5 --- shade/openstackcloud.py | 28 ++++- shade/tests/unit/test_qos_policy.py | 174 ++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+), 6 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 73325df04..e4dfb611c 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1726,6 +1726,9 @@ def list_qos_policies(self, filters=None): :returns: A list of policies ``munch.Munch``. """ + if not self._has_neutron_extension('qos'): + raise OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} @@ -3174,6 +3177,9 @@ def create_qos_policy(self, name=None, description=None, shared=None, :returns: The QoS policy object. :raises: OpenStackCloudException on operation error. """ + if not self._has_neutron_extension('qos'): + raise OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') policy = {} if name: policy['name'] = name @@ -3181,10 +3187,12 @@ def create_qos_policy(self, name=None, description=None, shared=None, policy['description'] = description if shared is not None: policy['shared'] = shared - # TODO(slaweq): this should be used only if proper API extension is - # available in Neutron if default is not None: - policy['is_default'] = default + if self._has_neutron_extension('qos-default'): + policy['is_default'] = default + else: + self.log.debug("'qos-default' extension is not available on " + "target cloud") if project_id: policy['project_id'] = project_id @@ -3210,6 +3218,9 @@ def update_qos_policy(self, name_or_id, policy_name=None, :returns: The updated QoS policy object. :raises: OpenStackCloudException on operation error. """ + if not self._has_neutron_extension('qos'): + raise OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') policy = {} if policy_name: policy['name'] = policy_name @@ -3217,10 +3228,12 @@ def update_qos_policy(self, name_or_id, policy_name=None, policy['description'] = description if shared is not None: policy['shared'] = shared - # TODO(slaweq): this should be used only if proper API extension is - # available in Neutron if default is not None: - policy['is_default'] = default + if self._has_neutron_extension('qos-default'): + policy['is_default'] = default + else: + self.log.debug("'qos-default' extension is not available on " + "target cloud") if not policy: self.log.debug("No QoS policy data to update") @@ -3246,6 +3259,9 @@ def delete_qos_policy(self, name_or_id): :raises: OpenStackCloudException on operation error. """ + if not self._has_neutron_extension('qos'): + raise OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') policy = self.get_qos_policy(name_or_id) if not policy: self.log.debug("QoS policy %s not found for deleting", name_or_id) diff --git a/shade/tests/unit/test_qos_policy.py b/shade/tests/unit/test_qos_policy.py index c26f4e32e..de33b0fea 100644 --- a/shade/tests/unit/test_qos_policy.py +++ b/shade/tests/unit/test_qos_policy.py @@ -36,8 +36,30 @@ class TestQosPolicy(base.RequestsMockTestCase): 'is_default': False } + qos_extension = { + "updated": "2015-06-08T10:00:00-00:00", + "name": "Quality of Service", + "links": [], + "alias": "qos", + "description": "The Quality of Service extension." + } + + qos_default_extension = { + "updated": "2017-041-06T10:00:00-00:00", + "name": "QoS default policy", + "links": [], + "alias": "qos-default", + "description": "Expose the QoS default policy per project" + } + + enabled_neutron_extensions = [qos_extension, qos_default_extension] + def test_get_qos_policy(self): self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', @@ -49,8 +71,24 @@ def test_get_qos_policy(self): self.assertDictEqual(self.mock_policy, r) self.assert_calls() + def test_get_qos_policy_no_qos_extension(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': []}) + ]) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.get_qos_policy, self.policy_name) + self.assert_calls() + def test_create_qos_policy(self): self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), dict(method='POST', uri=self.get_mock_url( 'network', 'public', @@ -62,8 +100,53 @@ def test_create_qos_policy(self): self.assertDictEqual(self.mock_policy, policy) self.assert_calls() + def test_create_qos_policy_no_qos_extension(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': []}) + ]) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.create_qos_policy, name=self.policy_name) + self.assert_calls() + + def test_create_qos_policy_no_qos_default_extension(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': [self.qos_extension]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': [self.qos_extension]}), + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policy': self.mock_policy}, + validate=dict( + json={'policy': { + 'name': self.policy_name, + 'project_id': self.project_id}})) + ]) + policy = self.cloud.create_qos_policy( + name=self.policy_name, project_id=self.project_id, default=True) + self.assertDictEqual(self.mock_policy, policy) + self.assert_calls() + def test_delete_qos_policy(self): self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', @@ -79,8 +162,28 @@ def test_delete_qos_policy(self): self.assertTrue(self.cloud.delete_qos_policy(self.policy_name)) self.assert_calls() + def test_delete_qos_policy_no_qos_extension(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': []}) + ]) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.delete_qos_policy, self.policy_name) + self.assert_calls() + def test_delete_qos_policy_not_found(self): self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', @@ -94,6 +197,14 @@ def test_delete_qos_policy_multiple_found(self): policy1 = dict(id='123', name=self.policy_name) policy2 = dict(id='456', name=self.policy_name) self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', @@ -109,6 +220,14 @@ def test_delete_qos_policy_multiple_using_id(self): policy1 = self.mock_policy policy2 = dict(id='456', name=self.policy_name) self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', @@ -128,6 +247,14 @@ def test_update_qos_policy(self): expected_policy = copy.copy(self.mock_policy) expected_policy['name'] = 'goofy' self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', @@ -146,3 +273,50 @@ def test_update_qos_policy(self): self.policy_id, policy_name='goofy') self.assertDictEqual(expected_policy, policy) self.assert_calls() + + def test_update_qos_policy_no_qos_extension(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': []}) + ]) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.update_qos_policy, self.policy_id, policy_name="goofy") + self.assert_calls() + + def test_update_qos_policy_no_qos_default_extension(self): + expected_policy = copy.copy(self.mock_policy) + expected_policy['name'] = 'goofy' + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': [self.qos_extension]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': [self.qos_extension]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': [self.qos_extension]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': [self.mock_policy]}), + dict(method='PUT', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies', + '%s.json' % self.policy_id]), + json={'policy': expected_policy}, + validate=dict( + json={'policy': {'name': "goofy"}})) + ]) + policy = self.cloud.update_qos_policy( + self.policy_id, policy_name='goofy', default=True) + self.assertDictEqual(expected_policy, policy) + self.assert_calls() From af0dbbd86f671c4c33841a11a63698ace1e4869b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Thu, 6 Jul 2017 21:16:58 +0000 Subject: [PATCH 1684/3836] Add support for list available QoS rule types Neutron with QoS plugin enabled can return list of rule types which are available in current cloud deployment. This patch adds support for listing such available rule types in shade. Change-Id: I005b34b129bfe8e1ac4deb6dbeefad2ffe5ce023 --- shade/_adapter.py | 2 +- shade/openstackcloud.py | 19 ++++++++ shade/tests/unit/test_qos_rule_type.py | 66 ++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 shade/tests/unit/test_qos_rule_type.py diff --git a/shade/_adapter.py b/shade/_adapter.py index 544f9976a..2a09713fb 100644 --- a/shade/_adapter.py +++ b/shade/_adapter.py @@ -135,7 +135,7 @@ def _munch_response(self, response, result_key=None, error_message=None): 'network', 'networks', 'subnet', 'subnets', 'router', 'routers', 'floatingip', 'floatingips', 'floating_ip', 'floating_ips', 'port', 'ports', - 'policy', 'policies', + 'rule_types', 'policy', 'policies', 'stack', 'stacks', 'zones', 'events', 'security_group', 'security_groups', 'security_group_rule', 'security_group_rules', diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 73325df04..aed03b50f 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1719,6 +1719,25 @@ def _list_ports(self, filters): error_message="Error fetching port list") return meta.get_and_munchify('ports', data) + def list_qos_rule_types(self, filters=None): + """List all available QoS rule types. + + :param filters: (optional) dict of filter conditions to push down + :returns: A list of rule types ``munch.Munch``. + + """ + if not self._has_neutron_extension('qos'): + raise OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + # Translate None from search interface to empty {} for kwargs below + if not filters: + filters = {} + data = self._network_client.get( + "/qos/rule-types.json", params=filters, + error_message="Error fetching QoS rule types list") + return meta.get_and_munchify('rule_types', data) + def list_qos_policies(self, filters=None): """List all available QoS policies. diff --git a/shade/tests/unit/test_qos_rule_type.py b/shade/tests/unit/test_qos_rule_type.py new file mode 100644 index 000000000..d0142db1f --- /dev/null +++ b/shade/tests/unit/test_qos_rule_type.py @@ -0,0 +1,66 @@ +# Copyright 2017 OVH SAS +# 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 shade import exc +from shade.tests.unit import base + + +class TestQosRuleType(base.RequestsMockTestCase): + + qos_extension = { + "updated": "2015-06-08T10:00:00-00:00", + "name": "Quality of Service", + "links": [], + "alias": "qos", + "description": "The Quality of Service extension." + } + + mock_rule_type_bandwidth_limit = { + 'type': 'bandwidth_limit' + } + + mock_rule_type_dscp_marking = { + 'type': 'dscp_marking' + } + + mock_rule_types = [ + mock_rule_type_bandwidth_limit, mock_rule_type_dscp_marking] + + def test_list_qos_rule_types(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': [self.qos_extension]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'rule-types.json']), + json={'rule_types': self.mock_rule_types}) + ]) + rule_types = self.cloud.list_qos_rule_types() + self.assertEqual(self.mock_rule_types, rule_types) + self.assert_calls() + + def test_list_qos_rule_types_no_qos_extension(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': []}) + ]) + self.assertRaises(exc.OpenStackCloudException, + self.cloud.list_qos_rule_types) + self.assert_calls() From 54d6e1d1847b0bd18e4ef1276be2b7044ab5b8f8 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 7 Jul 2017 12:10:06 -0500 Subject: [PATCH 1685/3836] Add flag to include all images in image list Recent Glance v2 hides images that are shared but not accepted. The way to see them is to send member_status=all, which is a bit low-level for a shade user to need to know. Add a show_all to list_images that defaults to False so that behavior doesn't change. Don't add it to search_images, because at the search_images level the user has the ability to pass in whatever filters they want. Change-Id: Ida2ea943168f5be56a60a94576bdcc6c8e1a9d24 --- ...show-all-images-flag-352748b6c3d99f3f.yaml | 9 +++ shade/openstackcloud.py | 13 +++- shade/tests/unit/test_image.py | 65 +++++++++++++++++-- 3 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/add-show-all-images-flag-352748b6c3d99f3f.yaml diff --git a/releasenotes/notes/add-show-all-images-flag-352748b6c3d99f3f.yaml b/releasenotes/notes/add-show-all-images-flag-352748b6c3d99f3f.yaml new file mode 100644 index 000000000..98c320b26 --- /dev/null +++ b/releasenotes/notes/add-show-all-images-flag-352748b6c3d99f3f.yaml @@ -0,0 +1,9 @@ +--- +features: + - Added flag "show_all" to list_images. The behavior of + Glance v2 to only show shared images if they have been + accepted by the user can be confusing, and the only way + to change it is to use search_images(filters=dict(member_status='all')) + which isn't terribly obvious. "show_all=True" will set + that flag, as well as disabling the filtering of images + in "deleted" state. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 73325df04..be5bf29b0 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2110,22 +2110,31 @@ def get_compute_limits(self, name_or_id=None): return self._normalize_compute_limits(limits, project_id=project_id) @_utils.cache_on_arguments(should_cache_fn=_no_pending_images) - def list_images(self, filter_deleted=True): + def list_images(self, filter_deleted=True, show_all=False): """Get available images. :param filter_deleted: Control whether deleted images are returned. + :param show_all: Show all images, including images that are shared + but not accepted. (By default in glance v2 shared image that + have not been accepted are not shown) show_all will override the + value of filter_deleted to False. :returns: A list of glance images. """ + if show_all: + filter_deleted = False # First, try to actually get images from glance, it's more efficient images = [] + params = {} image_list = [] try: if self.cloud_config.get_api_version('image') == '2': endpoint = '/images' + if show_all: + params['member_status'] = 'all' else: endpoint = '/images/detail' - response = self._image_client.get(endpoint) + response = self._image_client.get(endpoint, params=params) except keystoneauth1.exceptions.catalog.EndpointNotFound: # We didn't have glance, let's try nova diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index 0b2032bce..ba07d8918 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -12,6 +12,9 @@ # License for the specific language governing permissions and limitations # under the License. +# TODO(mordred) There are mocks of the image_client in here that are not +# using requests_mock. Erradicate them. + import operator import tempfile import uuid @@ -136,6 +139,58 @@ def test_list_images(self): self.cloud.list_images()) self.assert_calls() + def test_list_images_show_all(self): + self.register_uris([ + dict(method='GET', + uri='https://image.example.com/v2/images?member_status=all', + json=self.fake_search_return) + ]) + self.assertEqual( + self.cloud._normalize_images([self.fake_image_dict]), + self.cloud.list_images(show_all=True)) + self.assert_calls() + + def test_list_images_show_all_deleted(self): + deleted_image = self.fake_image_dict.copy() + deleted_image['status'] = 'deleted' + self.register_uris([ + dict(method='GET', + uri='https://image.example.com/v2/images?member_status=all', + json={'images': [self.fake_image_dict, deleted_image]}) + ]) + self.assertEqual( + self.cloud._normalize_images([ + self.fake_image_dict, deleted_image]), + self.cloud.list_images(show_all=True)) + self.assert_calls() + + def test_list_images_no_filter_deleted(self): + deleted_image = self.fake_image_dict.copy() + deleted_image['status'] = 'deleted' + self.register_uris([ + dict(method='GET', + uri='https://image.example.com/v2/images', + json={'images': [self.fake_image_dict, deleted_image]}) + ]) + self.assertEqual( + self.cloud._normalize_images([ + self.fake_image_dict, deleted_image]), + self.cloud.list_images(filter_deleted=False)) + self.assert_calls() + + def test_list_images_filter_deleted(self): + deleted_image = self.fake_image_dict.copy() + deleted_image['status'] = 'deleted' + self.register_uris([ + dict(method='GET', + uri='https://image.example.com/v2/images', + json={'images': [self.fake_image_dict, deleted_image]}) + ]) + self.assertEqual( + self.cloud._normalize_images([self.fake_image_dict]), + self.cloud.list_images()) + self.assert_calls() + def test_list_images_string_properties(self): image_dict = self.fake_image_dict.copy() image_dict['properties'] = 'list,of,properties' @@ -362,7 +417,7 @@ def test_create_image_put_v1(self, mock_image_client, mock_api_version): 'x-image-meta-checksum': mock.ANY, 'x-glance-registry-purge-props': 'false' }) - mock_image_client.get.assert_called_with('/images/detail') + mock_image_client.get.assert_called_with('/images/detail', params={}) self.assertEqual( self._munch_images(ret), self.cloud.list_images()) @@ -431,7 +486,7 @@ def test_update_image_no_patch(self, mock_image_client, mock_api_version): self.cloud.update_image_properties( image=self._image_dict(ret), **{'owner_specified.shade.object': 'images/42 name'}) - mock_image_client.get.assert_called_with('/images') + mock_image_client.get.assert_called_with('/images', params={}) mock_image_client.patch.assert_not_called() @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') @@ -516,7 +571,7 @@ def test_create_image_put_user_int( '/images/42/file', headers={'Content-Type': 'application/octet-stream'}, data=mock.ANY) - mock_image_client.get.assert_called_with('/images') + mock_image_client.get.assert_called_with('/images', params={}) self.assertEqual( self._munch_images(ret), self.cloud.list_images()) @@ -545,7 +600,7 @@ def test_create_image_put_meta_int( ret['status'] = 'success' mock_image_client.get.return_value = [ret] mock_image_client.post.return_value = ret - mock_image_client.get.assert_called_with('/images') + mock_image_client.get.assert_called_with('/images', params={}) self.assertEqual( self._munch_images(ret), self.cloud.list_images()) @@ -613,7 +668,7 @@ def test_create_image_put_user_prop( mock_image_client.post.return_value = ret self._call_create_image( '42 name', min_disk='0', min_ram=0, properties={'int_v': 12345}) - mock_image_client.get.assert_called_with('/images') + mock_image_client.get.assert_called_with('/images', params={}) self.assertEqual( self._munch_images(ret), self.cloud.list_images()) From 10e6fbe44f53a0beff2a2e4d2243b3c2e7f96d8c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 28 Jun 2017 12:38:05 -0500 Subject: [PATCH 1686/3836] Make sure we pass propert dicts to validate In register_uris, validate takes a dict that matches the dict one normally passes to requests_mock - that is, it can take json, headers, etc. Unfortunately, our fixture doesn't error when one passes a normal dict to it thinking you're passing a json dict for it to validate. This fixes the call sites - the associated tests need to be fixed. Change-Id: I16f265fe41219be0f3b031eb8be26f740a65d665 --- shade/operatorcloud.py | 14 +++++++++----- shade/tests/unit/base.py | 8 +++++++- shade/tests/unit/test_caching.py | 9 ++++++--- shade/tests/unit/test_domains.py | 9 ++++++--- shade/tests/unit/test_endpoints.py | 24 +++++++++++++++--------- shade/tests/unit/test_groups.py | 6 ++++-- shade/tests/unit/test_project.py | 18 ++++++++++++++---- shade/tests/unit/test_security_groups.py | 11 +++++++---- shade/tests/unit/test_services.py | 11 ++++++++--- shade/tests/unit/test_users.py | 6 ++++-- 10 files changed, 80 insertions(+), 36 deletions(-) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index c01a1ce37..4d302e1ff 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -929,17 +929,21 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, urlkwargs['interface'] = interface endpoint_args.append(urlkwargs) else: - expected_endpoints = {'public': public_url, - 'internal': internal_url, - 'admin': admin_url} + # NOTE(notmorgan): This is done as a list of tuples to ensure we + # have a deterministic order we try and create the endpoint + # elements. This is done mostly so that it is possible to test the + # requests themselves. + expected_endpoints = [('public', public_url), + ('internal', internal_url), + ('admin', admin_url)] if self.cloud_config.get_api_version('identity').startswith('2'): urlkwargs = {} - for interface, url in expected_endpoints.items(): + for interface, url in expected_endpoints: if url: urlkwargs['{}url'.format(interface)] = url endpoint_args.append(urlkwargs) else: - for interface, url in expected_endpoints.items(): + for interface, url in expected_endpoints: if url: urlkwargs = {} urlkwargs['url'] = url diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 3523152dc..9da294d85 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -259,9 +259,11 @@ def _get_project_data(self, project_name=None, enabled=None, response['enabled'] = enabled request['enabled'] = enabled response.setdefault('enabled', True) + request.setdefault('enabled', True) if description: response['description'] = description request['description'] = description + request.setdefault('description', None) if v3: project_key = 'project' else: @@ -275,7 +277,7 @@ def _get_group_data(self, name=None, domain_id=None, description=None): name = name or self.getUniqueString('groupname') domain_id = uuid.UUID(domain_id or uuid.uuid4().hex).hex response = {'id': group_id, 'name': name, 'domain_id': domain_id} - request = {'name': name} + request = {'name': name, 'domain_id': domain_id} if description is not None: response['description'] = description request['description'] = description @@ -374,6 +376,8 @@ def _get_endpoint_v2_data(self, service_id=None, region=None, response = {'id': endpoint_id, 'service_id': service_id, 'region': region} v3_endpoints = {} + request = response.copy() + request.pop('id') if admin_url: response['adminURL'] = admin_url v3_endpoints['admin'] = self._get_endpoint_v3_data( @@ -388,6 +392,8 @@ def _get_endpoint_v2_data(self, service_id=None, region=None, service_id, region, public_url, interface='public') request = response.copy() request.pop('id') + for u in ('publicURL', 'internalURL', 'adminURL'): + request[u.lower()] = request.pop(u, None) return _EndpointDataV2(endpoint_id, service_id, region, public_url, internal_url, admin_url, v3_endpoints, {'endpoint': response}, {'endpoint': request}) diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 7fdb49dd9..11c01f5c1 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -338,7 +338,7 @@ def test_modify_user_invalidates_cache(self): user_data = self._get_user_data(email='test@example.com') new_resp = {'user': user_data.json_response['user'].copy()} - new_resp['user']['email'] = 'updated@example.com' + new_resp['user']['email'] = 'Nope@Nope.Nope' new_req = {'user': {'email': new_resp['user']['email']}} mock_users_url = self.get_mock_url( @@ -355,6 +355,9 @@ def test_modify_user_invalidates_cache(self): users_list_resp = {'users': [user_data.json_response['user']]} updated_users_list_resp = {'users': [new_resp['user']]} + # Password is None in the original create below + user_data.json_request['user']['password'] = None + uris_to_mock = [ # Inital User List is Empty dict(method='GET', uri=mock_users_url, status_code=200, @@ -363,7 +366,7 @@ def test_modify_user_invalidates_cache(self): # GET to get the user data after POST dict(method='POST', uri=mock_users_url, status_code=200, json=user_data.json_response, - validate=user_data.json_request), + validate=dict(json=user_data.json_request)), # List Users Call dict(method='GET', uri=mock_users_url, status_code=200, json=users_list_resp), @@ -376,7 +379,7 @@ def test_modify_user_invalidates_cache(self): dict(method='GET', uri=mock_user_resource_url, status_code=200, json=user_data.json_response), dict(method='PUT', uri=mock_user_resource_url, status_code=200, - json=new_resp, validate=new_req), + json=new_resp, validate=dict(json=new_req)), dict(method='GET', uri=mock_user_resource_url, status_code=200, json=new_resp), # List Users Call diff --git a/shade/tests/unit/test_domains.py b/shade/tests/unit/test_domains.py index 64d2a0289..b1996c1c5 100644 --- a/shade/tests/unit/test_domains.py +++ b/shade/tests/unit/test_domains.py @@ -111,7 +111,8 @@ def test_delete_domain(self): domain_resource_uri = self.get_mock_url(append=[domain_data.domain_id]) self.register_uris([ dict(method='PATCH', uri=domain_resource_uri, status_code=200, - json=new_resp, validate={'domain': {'enabled': False}}), + json=new_resp, + validate=dict(json={'domain': {'enabled': False}})), dict(method='DELETE', uri=domain_resource_uri, status_code=204)]) self.op_cloud.delete_domain(domain_data.domain_id) self.assert_calls() @@ -126,7 +127,8 @@ def test_delete_domain_name_or_id(self): dict(method='GET', uri=self.get_mock_url(), status_code=200, json={'domains': [domain_data.json_response['domain']]}), dict(method='PATCH', uri=domain_resource_uri, status_code=200, - json=new_resp, validate={'domain': {'enabled': False}}), + json=new_resp, + validate=dict(json={'domain': {'enabled': False}})), dict(method='DELETE', uri=domain_resource_uri, status_code=204)]) self.op_cloud.delete_domain(name_or_id=domain_data.domain_id) self.assert_calls() @@ -143,7 +145,8 @@ def test_delete_domain_exception(self): domain_resource_uri = self.get_mock_url(append=[domain_data.domain_id]) self.register_uris([ dict(method='PATCH', uri=domain_resource_uri, status_code=200, - json=new_resp, validate={'domain': {'enabled': False}}), + json=new_resp, + validate=dict(json={'domain': {'enabled': False}})), dict(method='DELETE', uri=domain_resource_uri, status_code=404)]) with testtools.ExpectedException( shade.OpenStackCloudURINotFound, diff --git a/shade/tests/unit/test_endpoints.py b/shade/tests/unit/test_endpoints.py index 8dbdbc499..2c14ea679 100644 --- a/shade/tests/unit/test_endpoints.py +++ b/shade/tests/unit/test_endpoints.py @@ -44,7 +44,9 @@ def test_create_endpoint_v2(self): service_data.service_id, public_url=self._dummy_url(), internal_url=self._dummy_url(), admin_url=self._dummy_url()) other_endpoint_data = self._get_endpoint_v2_data( - service_data.service_id, public_url=self._dummy_url()) + service_data.service_id, region=endpoint_data.region, + public_url=endpoint_data.public_url) + # correct the keys self.register_uris([ dict(method='GET', @@ -57,7 +59,7 @@ def test_create_endpoint_v2(self): uri=self.get_mock_url(base_url_append=None), status_code=200, json=endpoint_data.json_response, - validate=endpoint_data.json_request), + validate=dict(json=endpoint_data.json_request)), dict(method='GET', uri=self.get_mock_url( resource='services', base_url_append='OS-KSADM'), @@ -77,7 +79,7 @@ def test_create_endpoint_v2(self): uri=self.get_mock_url(base_url_append=None), status_code=200, json=other_endpoint_data.json_response, - validate=other_endpoint_data.json_request) + validate=dict(json=other_endpoint_data.json_request)) ]) endpoints = self.op_cloud.create_endpoint( @@ -151,7 +153,8 @@ def test_create_endpoint_v3(self): uri=self.get_mock_url(), status_code=200, json=public_endpoint_data_disabled.json_response, - validate=public_endpoint_data_disabled.json_request), + validate=dict( + json=public_endpoint_data_disabled.json_request)), dict(method='GET', uri=self.get_mock_url( append=[public_endpoint_data_disabled.endpoint_id]), @@ -166,7 +169,7 @@ def test_create_endpoint_v3(self): uri=self.get_mock_url(), status_code=200, json=public_endpoint_data.json_response, - validate=public_endpoint_data.json_request), + validate=dict(json=public_endpoint_data.json_request)), dict(method='GET', uri=self.get_mock_url( append=[public_endpoint_data.endpoint_id]), @@ -176,7 +179,7 @@ def test_create_endpoint_v3(self): uri=self.get_mock_url(), status_code=200, json=internal_endpoint_data.json_response, - validate=internal_endpoint_data.json_request), + validate=dict(json=internal_endpoint_data.json_request)), dict(method='GET', uri=self.get_mock_url( append=[internal_endpoint_data.endpoint_id]), @@ -186,7 +189,7 @@ def test_create_endpoint_v3(self): uri=self.get_mock_url(), status_code=200, json=admin_endpoint_data.json_response, - validate=admin_endpoint_data.json_request), + validate=dict(json=admin_endpoint_data.json_request)), dict(method='GET', uri=self.get_mock_url( append=[admin_endpoint_data.endpoint_id]), @@ -251,15 +254,18 @@ def test_update_endpoint_v2(self): def test_update_endpoint_v3(self): service_data = self._get_service_data() + dummy_url = self._dummy_url() endpoint_data = self._get_endpoint_v3_data( service_id=service_data.service_id, interface='admin', enabled=False) + reference_request = endpoint_data.json_request.copy() + reference_request['endpoint']['url'] = dummy_url self.register_uris([ dict(method='PATCH', uri=self.get_mock_url(append=[endpoint_data.endpoint_id]), status_code=200, json=endpoint_data.json_response, - validate=endpoint_data.json_request), + validate=dict(json=reference_request)), dict(method='GET', uri=self.get_mock_url(append=[endpoint_data.endpoint_id]), status_code=200, @@ -269,7 +275,7 @@ def test_update_endpoint_v3(self): endpoint_data.endpoint_id, service_name_or_id=service_data.service_id, region=endpoint_data.region, - url=self._dummy_url(), + url=dummy_url, interface=endpoint_data.interface, enabled=False ) diff --git a/shade/tests/unit/test_groups.py b/shade/tests/unit/test_groups.py index a2525e2f1..4f9dec447 100644 --- a/shade/tests/unit/test_groups.py +++ b/shade/tests/unit/test_groups.py @@ -72,7 +72,7 @@ def test_create_group(self): uri=self.get_mock_url(), status_code=200, json=group_data.json_response, - validate=group_data.json_request), + validate=dict(json=group_data.json_request)), dict(method='GET', uri=self.get_mock_url(append=[group_data.group_id]), status_code=200, @@ -84,6 +84,8 @@ def test_create_group(self): def test_update_group(self): group_data = self._get_group_data() + # Domain ID is not sent + group_data.json_request['group'].pop('domain_id') self.register_uris([ dict(method='GET', uri=self.get_mock_url(), @@ -93,7 +95,7 @@ def test_update_group(self): uri=self.get_mock_url(append=[group_data.group_id]), status_code=200, json=group_data.json_response, - validate=group_data.json_request), + validate=dict(json=group_data.json_request)), dict(method='GET', uri=self.get_mock_url(append=[group_data.group_id]), status_code=200, diff --git a/shade/tests/unit/test_project.py b/shade/tests/unit/test_project.py index 5ac7d4361..f836bc0d2 100644 --- a/shade/tests/unit/test_project.py +++ b/shade/tests/unit/test_project.py @@ -39,7 +39,7 @@ def test_create_project_v2(self): self.register_uris([ dict(method='POST', uri=self.get_mock_url(v3=False), status_code=200, json=project_data.json_response, - validate=project_data.json_request) + validate=dict(json=project_data.json_request)) ]) project = self.op_cloud.create_project( name=project_data.project_name, @@ -52,12 +52,14 @@ def test_create_project_v2(self): def test_create_project_v3(self,): project_data = self._get_project_data( description=self.getUniqueString('projectDesc')) + reference_req = project_data.json_request.copy() + reference_req['project']['enabled'] = True self.register_uris([ dict(method='POST', uri=self.get_mock_url(), status_code=200, json=project_data.json_response, - validate=project_data.json_request) + validate=dict(json=reference_req)) ]) project = self.op_cloud.create_project( name=project_data.project_name, @@ -135,6 +137,9 @@ def test_update_project_v2(self): project_data = self._get_project_data( v3=False, description=self.getUniqueString('projectDesc')) + # remove elements that are not updated in this test. + project_data.json_request['tenant'].pop('name') + project_data.json_request['tenant'].pop('enabled') self.register_uris([ dict(method='GET', uri=self.get_mock_url(v3=False), @@ -145,7 +150,7 @@ def test_update_project_v2(self): v3=False, append=[project_data.project_id]), status_code=200, json=project_data.json_response, - validate=project_data.json_request) + validate=dict(json=project_data.json_request)) ]) project = self.op_cloud.update_project( project_data.project_id, @@ -160,6 +165,11 @@ def test_update_project_v2(self): def test_update_project_v3(self): project_data = self._get_project_data( description=self.getUniqueString('projectDesc')) + reference_req = project_data.json_request.copy() + # Remove elements not actually sent in the update + reference_req['project'].pop('domain_id') + reference_req['project'].pop('name') + reference_req['project'].pop('enabled') self.register_uris([ dict(method='GET', uri=self.get_mock_url( @@ -170,7 +180,7 @@ def test_update_project_v3(self): dict(method='PATCH', uri=self.get_mock_url(append=[project_data.project_id]), status_code=200, json=project_data.json_response, - validate=project_data.json_request) + validate=dict(json=reference_req)) ]) project = self.op_cloud.update_project( project_data.project_id, diff --git a/shade/tests/unit/test_security_groups.py b/shade/tests/unit/test_security_groups.py index a4d4f6934..ca9db074d 100644 --- a/shade/tests/unit/test_security_groups.py +++ b/shade/tests/unit/test_security_groups.py @@ -588,7 +588,8 @@ def test_add_security_group_to_server_nova(self): dict( method='POST', uri='%s/servers/%s/action' % (fakes.COMPUTE_ENDPOINT, '1234'), - validate={'addSecurityGroup': {'name': 'nova-sec-group'}}, + validate=dict( + json={'addSecurityGroup': {'name': 'nova-sec-group'}}), status_code=202, ), ]) @@ -620,7 +621,8 @@ def test_add_security_group_to_server_neutron(self): json={'security_groups': [neutron_grp_dict]}), dict(method='POST', uri='%s/servers/%s/action' % (fakes.COMPUTE_ENDPOINT, '1234'), - validate={'addSecurityGroup': {'name': 'neutron-sec-group'}}, + validate=dict( + json={'addSecurityGroup': {'name': 'neutron-sec-group'}}), status_code=202), ]) @@ -642,7 +644,8 @@ def test_remove_security_group_from_server_nova(self): dict( method='POST', uri='%s/servers/%s/action' % (fakes.COMPUTE_ENDPOINT, '1234'), - validate={'removeSecurityGroup': {'name': 'nova-sec-group'}}, + validate=dict( + json={'removeSecurityGroup': {'name': 'nova-sec-group'}}), ), ]) @@ -673,7 +676,7 @@ def test_remove_security_group_from_server_neutron(self): json={'security_groups': [neutron_grp_dict]}), dict(method='POST', uri='%s/servers/%s/action' % (fakes.COMPUTE_ENDPOINT, '1234'), - validate=validate), + validate=dict(json=validate)), ]) self.assertTrue(self.cloud.remove_server_security_groups( diff --git a/shade/tests/unit/test_services.py b/shade/tests/unit/test_services.py index d1ffe3852..cbd01bf55 100644 --- a/shade/tests/unit/test_services.py +++ b/shade/tests/unit/test_services.py @@ -40,12 +40,14 @@ def test_create_service_v2(self): self.use_keystone_v2() service_data = self._get_service_data(name='a service', type='network', description='A test service') + reference_req = service_data.json_request.copy() + reference_req.pop('enabled') self.register_uris([ dict(method='POST', uri=self.get_mock_url(base_url_append='OS-KSADM'), status_code=200, json=service_data.json_response_v2, - validate=service_data.json_request), + validate=dict(json={'OS-KSADM:service': reference_req})), dict(method='GET', uri=self.get_mock_url(base_url_append='OS-KSADM', append=[service_data.service_id]), @@ -74,7 +76,7 @@ def test_create_service_v3(self): uri=self.get_mock_url(), status_code=200, json=service_data.json_response_v3, - validate=service_data.json_request), + validate=dict(json={'service': service_data.json_request})), dict(method='GET', uri=self.get_mock_url(append=[service_data.service_id]), status_code=200, @@ -108,12 +110,15 @@ def test_update_service_v3(self): request['enabled'] = False resp = service_data.json_response_v3.copy() resp['enabled'] = False + request.pop('description') + request.pop('name') + request.pop('type') self.register_uris([ dict(method='PATCH', uri=self.get_mock_url(append=[service_data.service_id]), status_code=200, json=resp, - validate=request), + validate=dict(json={'service': request})), dict(method='GET', uri=self.get_mock_url(append=[service_data.service_id]), status_code=200, diff --git a/shade/tests/unit/test_users.py b/shade/tests/unit/test_users.py index 97e5affab..e1da45625 100644 --- a/shade/tests/unit/test_users.py +++ b/shade/tests/unit/test_users.py @@ -51,7 +51,8 @@ def test_create_user_v2(self): uri=self._get_keystone_mock_url(resource='users', v3=False), status_code=200, json=user_data.json_response, - validate=dict(json=user_data.json_request))]) + validate=dict(json=user_data.json_request)), + ]) user = self.op_cloud.create_user( name=user_data.name, email=user_data.email, @@ -71,7 +72,8 @@ def test_create_user_v3(self): dict(method='POST', uri=self._get_keystone_mock_url(resource='users'), status_code=200, json=user_data.json_response, - validate=dict(json=user_data.json_request))]) + validate=dict(json=user_data.json_request)), + ]) user = self.op_cloud.create_user( name=user_data.name, email=user_data.email, From d1eea7a720bb14f6781ec5abadbc27fafdf85e1e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 28 Jun 2017 12:52:40 -0500 Subject: [PATCH 1687/3836] Make sure we don't fail open on bad input to validate Passing arbitrary dicts to validate does not validate that that's what we're sending to the API. Put in a validation check that the keys we're sending are the ones validate does something about. It's possible there is another param to validate we're not listing. If we hit a test that needs it, we can always add it. Change-Id: I75b24f4f640b7cf6ffebff494e1627569d74755a --- shade/tests/unit/base.py | 6 ++++++ shade/tests/unit/test_identity_roles.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index 9da294d85..bbbcab7b0 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -535,6 +535,12 @@ def __do_register_uris(self, uri_mock_list=None): key = '{method}|{uri}|{params}'.format( method=method, uri=uri, params=kw_params) validate = to_mock.pop('validate', {}) + valid_keys = set(['json', 'headers', 'params']) + invalid_keys = set(validate.keys()) - valid_keys + if invalid_keys: + raise TypeError( + "Invalid values passed to validate: {keys}".format( + keys=invalid_keys)) headers = structures.CaseInsensitiveDict(to_mock.pop('headers', {})) if 'content-type' not in headers: diff --git a/shade/tests/unit/test_identity_roles.py b/shade/tests/unit/test_identity_roles.py index 0767345e8..05db5ba3f 100644 --- a/shade/tests/unit/test_identity_roles.py +++ b/shade/tests/unit/test_identity_roles.py @@ -91,7 +91,7 @@ def test_create_role(self): uri=self.get_mock_url(), status_code=200, json=role_data.json_response, - validate=role_data.json_request), + validate=dict(json=role_data.json_request)), dict(method='GET', uri=self.get_mock_url(append=[role_data.role_id]), status_code=200, From 20c1e82fa7c639a9f88fcd1cbcc57226ca6729c1 Mon Sep 17 00:00:00 2001 From: Abhijeet Kasurde Date: Sun, 9 Jul 2017 21:48:22 +0530 Subject: [PATCH 1688/3836] Remove hard-coding of timeout from API This patch adds a fix for removing hardcoding of timeout variable in _upload_image_put method by adding method argument timeout. Change-Id: I01ac7d590bfc4aea1adf571bd56d52048723d570 Signed-off-by: Abhijeet Kasurde --- shade/openstackcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 295cce78b..88a694f57 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3946,7 +3946,7 @@ def _upload_image_put( return image try: for count in _utils._iterate_timeout( - 60, + timeout, "Timeout waiting for the image to finish."): image_obj = self.get_image(image.id) if image_obj and image_obj.status not in ('queued', 'saving'): From a9687a82fc14e3e9f6ce9925d27bf5699308914d Mon Sep 17 00:00:00 2001 From: Lee Yarwood Date: Mon, 10 Jul 2017 14:41:49 +0100 Subject: [PATCH 1689/3836] Add debug to tox environment The oslotest package distributes a shell file that may be used to assist in debugging python code. The shell file uses testtools, and supports debugging with pdb. To enable debugging, run tox with the debug environment. Below are the following ways to run it. * tox -e debug module * tox -e debug module.test_class * tox -e debug module.test_class.test_method Change-Id: If0b06dcf094682401c4b09dd72493c678ea2a6b0 --- test-requirements.txt | 1 + tox.ini | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/test-requirements.txt b/test-requirements.txt index c69d2a2f5..16c7d8ccf 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,6 +8,7 @@ fixtures>=3.0.0 # Apache-2.0/BSD mock>=2.0 # BSD python-subunit>=0.0.18 # Apache-2.0/BSD openstackdocstheme>=1.11.0 # Apache-2.0 +oslotest>=1.10.0 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 sphinx>=1.6.2 # BSD testrepository>=0.0.18 # Apache-2.0/BSD diff --git a/tox.ini b/tox.ini index e4aa17752..7ae628f90 100644 --- a/tox.ini +++ b/tox.ini @@ -36,6 +36,12 @@ commands = flake8 shade [testenv:venv] commands = {posargs} +[testenv:debug] +whitelist_externals = find +commands = + find . -type f -name "*.pyc" -delete + oslo_debug_helper {posargs} + [testenv:cover] commands = python setup.py testr --coverage --testr-args='{posargs}' From 9f78c543498c3c419418d3161ffe8bf5ea3d4253 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Mon, 10 Jul 2017 11:38:10 -0400 Subject: [PATCH 1690/3836] De-client-ify Service Create Change-Id: Ifc71043cbb40d2888cd9c8341f135e832c553d00 --- shade/_adapter.py | 2 +- shade/_tasks.py | 5 ----- shade/operatorcloud.py | 16 ++++++++-------- shade/tests/unit/test_services.py | 13 ++----------- 4 files changed, 11 insertions(+), 25 deletions(-) diff --git a/shade/_adapter.py b/shade/_adapter.py index 3dd4b5799..5089b6975 100644 --- a/shade/_adapter.py +++ b/shade/_adapter.py @@ -144,7 +144,7 @@ def _munch_response(self, response, result_key=None, error_message=None): 'flavor', 'flavors', 'baymodels', 'aggregate', 'aggregates', 'availabilityZoneInfo', 'flavor_access', 'output', 'server_groups', 'domain', - 'domains']: + 'domains', 'service', 'OS-KSADM:service']: if key in result_json.keys(): self._log_request_id(response) return result_json diff --git a/shade/_tasks.py b/shade/_tasks.py index 28d8f9a57..e197e8307 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -127,11 +127,6 @@ def main(self, client): return client.ironic_client.node.set_provision_state(**self.args) -class ServiceCreate(task_manager.Task): - def main(self, client): - return client.keystone_client.services.create(**self.args) - - class ServiceList(task_manager.Task): def main(self, client): return client.keystone_client.services.list() diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 4d302e1ff..a33575e8a 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -756,18 +756,18 @@ def create_service(self, name, enabled=True, **kwargs): # TODO(mordred) When this changes to REST, force interface=admin # in the adapter call if self.cloud_config.get_api_version('identity').startswith('2'): - kwargs['service_type'] = type_ or service_type + url, key = '/OS-KSADM/services', 'OS-KSADM:service' + kwargs['type'] = type_ or service_type else: + url, key = '/services', 'service' kwargs['type'] = type_ or service_type kwargs['enabled'] = enabled + kwargs['name'] = name - with _utils.shade_exceptions( - "Failed to create service {name}".format(name=name) - ): - service = self.manager.submit_task( - _tasks.ServiceCreate(name=name, **kwargs) - ) - + msg = 'Failed to create service {name}'.format(name=name) + data = self._identity_client.post( + url, json={key: kwargs}, error_message=msg) + service = meta.get_and_munchify(key, data) return _utils.normalize_keystone_services([service])[0] @_utils.valid_kwargs('name', 'enabled', 'type', 'service_type', diff --git a/shade/tests/unit/test_services.py b/shade/tests/unit/test_services.py index cbd01bf55..585184e6f 100644 --- a/shade/tests/unit/test_services.py +++ b/shade/tests/unit/test_services.py @@ -47,12 +47,7 @@ def test_create_service_v2(self): uri=self.get_mock_url(base_url_append='OS-KSADM'), status_code=200, json=service_data.json_response_v2, - validate=dict(json={'OS-KSADM:service': reference_req})), - dict(method='GET', - uri=self.get_mock_url(base_url_append='OS-KSADM', - append=[service_data.service_id]), - status_code=200, - json=service_data.json_response_v2) + validate=dict(json={'OS-KSADM:service': reference_req})) ]) service = self.op_cloud.create_service( @@ -76,11 +71,7 @@ def test_create_service_v3(self): uri=self.get_mock_url(), status_code=200, json=service_data.json_response_v3, - validate=dict(json={'service': service_data.json_request})), - dict(method='GET', - uri=self.get_mock_url(append=[service_data.service_id]), - status_code=200, - json=service_data.json_response_v3) + validate=dict(json={'service': service_data.json_request})) ]) service = self.op_cloud.create_service( From 806378f4f37f5dd421535b7961d705b0bc7f071a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Mon, 10 Jul 2017 20:45:37 +0000 Subject: [PATCH 1691/3836] Add Neutron QoS bandwidth limit rule commands Create/Update/List/Get/Delete QoS bandwidth limit rules is now possible to do with shade. Change-Id: Ife015dd3f9584df901462b3998a4d775374073cf --- shade/_adapter.py | 1 + shade/openstackcloud.py | 214 ++++++++++ .../unit/test_qos_bandwidth_limit_rule.py | 397 ++++++++++++++++++ 3 files changed, 612 insertions(+) create mode 100644 shade/tests/unit/test_qos_bandwidth_limit_rule.py diff --git a/shade/_adapter.py b/shade/_adapter.py index 3dd4b5799..cb4822d65 100644 --- a/shade/_adapter.py +++ b/shade/_adapter.py @@ -136,6 +136,7 @@ def _munch_response(self, response, result_key=None, error_message=None): 'router', 'routers', 'floatingip', 'floatingips', 'floating_ip', 'floating_ips', 'port', 'ports', 'rule_types', 'policy', 'policies', + 'bandwidth_limit_rule', 'bandwidth_limit_rules', 'stack', 'stacks', 'zones', 'events', 'security_group', 'security_groups', 'security_group_rule', 'security_group_rules', diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 295cce78b..25307d29f 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3269,6 +3269,220 @@ def delete_qos_policy(self, name_or_id): return True + def search_qos_bandwidth_limit_rules(self, policy_name_or_id, rule_id=None, + filters=None): + """Search QoS bandwidth limit rules + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rules should be associated. + :param string rule_id: ID of searched rule. + :param filters: a dict containing additional filters to use. e.g. + {'max_kbps': 1000} + + :returns: a list of ``munch.Munch`` containing the bandwidth limit + rule descriptions. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + OpenStack API call. + """ + rules = self.list_qos_bandwidth_limit_rules(policy_name_or_id, filters) + return _utils._filter_list(rules, rule_id, filters) + + def list_qos_bandwidth_limit_rules(self, policy_name_or_id, filters=None): + """List all available QoS bandwith limit rules. + + :param string policy_name_or_id: Name or ID of the QoS policy from + from rules should be listed. + :param filters: (optional) dict of filter conditions to push down + :returns: A list of ``munch.Munch`` containing rule info. + + :raises: ``OpenStackCloudResourceNotFound`` if QoS policy will not be + found. + """ + if not self._has_neutron_extension('qos'): + raise OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + # Translate None from search interface to empty {} for kwargs below + if not filters: + filters = {} + + data = self._network_client.get( + "/qos/policies/{policy_id}/bandwidth_limit_rules.json".format( + policy_id=policy['id']), + params=filters, + error_message="Error fetching QoS bandwith limit rules from " + "{policy}".format(policy=policy['id'])) + return meta.get_and_munchify('bandwidth_limit_rules', data) + + def get_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): + """Get a QoS bandwidth limit rule by name or ID. + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rule should be associated. + :param rule_id: ID of the rule. + + :returns: A bandwidth limit rule ``munch.Munch`` or None if + no matching rule is found. + + """ + if not self._has_neutron_extension('qos'): + raise OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + data = self._network_client.get( + "/qos/policies/{policy_id}/bandwidth_limit_rules/{rule_id}.json". + format(policy_id=policy['id'], rule_id=rule_id), + error_message="Error fetching QoS bandwith limit rule {rule_id} " + "from {policy}".format(rule_id=rule_id, + policy=policy['id'])) + return meta.get_and_munchify('bandwidth_limit_rule', data) + + def create_qos_bandwidth_limit_rule(self, policy_name_or_id, max_kbps=None, + max_burst_kbps=None, direction=None): + """Create a QoS bandwidth limit rule. + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rule should be associated. + :param int max_kbps: Maximum bandwidth limit value + (in kilobits per second). + :param int max_burst_kbps: Maximum burst value (in kilobits). + :param string direction: Ingress or egress. + The direction in which the traffic will be limited. + + :returns: The QoS bandwidth limit rule. + :raises: OpenStackCloudException on operation error. + """ + if not self._has_neutron_extension('qos'): + raise OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + rule = {} + if max_kbps: + rule['max_kbps'] = max_kbps + if max_burst_kbps: + rule['max_burst_kbps'] = max_burst_kbps + if direction is not None: + if self._has_neutron_extension('qos-bw-limit-direction'): + rule['direction'] = direction + else: + self.log.debug( + "'qos-bw-limit-direction' extension is not available on " + "target cloud") + + data = self._network_client.post( + "/qos/policies/{policy_id}/bandwidth_limit_rules".format( + policy_id=policy['id']), + json={'bandwidth_limit_rule': rule}) + return meta.get_and_munchify('bandwidth_limit_rule', data) + + def update_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id, + max_kbps=None, max_burst_kbps=None, + direction=None): + """Update a QoS bandwidth limit rule. + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rule is associated. + :param string rule_id: ID of rule to update. + :param int max_kbps: Maximum bandwidth limit value + (in kilobits per second). + :param int max_burst_kbps: Maximum burst value (in kilobits). + :param string direction: Ingress or egress. + The direction in which the traffic will be limited. + + :returns: The updated QoS bandwidth limit rule. + :raises: OpenStackCloudException on operation error. + """ + if not self._has_neutron_extension('qos'): + raise OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + rule = {} + if max_kbps: + rule['max_kbps'] = max_kbps + if max_burst_kbps: + rule['max_burst_kbps'] = max_burst_kbps + if direction is not None: + if self._has_neutron_extension('qos-bw-limit-direction'): + rule['direction'] = direction + else: + self.log.debug( + "'qos-bw-limit-direction' extension is not available on " + "target cloud") + if not rule: + self.log.debug("No QoS bandwidth limit rule data to update") + return + + curr_rule = self.get_qos_bandwidth_limit_rule( + policy_name_or_id, rule_id) + if not curr_rule: + raise OpenStackCloudException( + "QoS bandwidth_limit_rule {rule_id} not found in policy " + "{policy_id}".format(rule_id=rule_id, + policy_id=policy['id'])) + + data = self._network_client.put( + "/qos/policies/{policy_id}/bandwidth_limit_rules/{rule_id}.json". + format(policy_id=policy['id'], rule_id=rule_id), + json={'bandwidth_limit_rule': rule}) + return meta.get_and_munchify('bandwidth_limit_rule', data) + + def delete_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): + """Delete a QoS bandwidth limit rule. + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rule is associated. + :param string rule_id: ID of rule to update. + + :raises: OpenStackCloudException on operation error. + """ + if not self._has_neutron_extension('qos'): + raise OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + try: + self._network_client.delete( + "/qos/policies/{policy}/bandwidth_limit_rules/{rule}.json". + format(policy=policy['id'], rule=rule_id)) + except OpenStackCloudURINotFound: + self.log.debug( + "QoS bandwidth limit rule {rule_id} not found in policy " + "{policy_id}. Ignoring.".format(rule_id=rule_id, + policy_id=policy['id'])) + return False + + return True + def _build_external_gateway_info(self, ext_gateway_net_id, enable_snat, ext_fixed_ips): info = {} diff --git a/shade/tests/unit/test_qos_bandwidth_limit_rule.py b/shade/tests/unit/test_qos_bandwidth_limit_rule.py new file mode 100644 index 000000000..53858496e --- /dev/null +++ b/shade/tests/unit/test_qos_bandwidth_limit_rule.py @@ -0,0 +1,397 @@ +# Copyright 2017 OVH SAS +# 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 copy + +from shade import exc +from shade.tests.unit import base + + +class TestQosBandwidthLimitRule(base.RequestsMockTestCase): + + policy_name = 'qos test policy' + policy_id = '881d1bb7-a663-44c0-8f9f-ee2765b74486' + project_id = 'c88fc89f-5121-4a4c-87fd-496b5af864e9' + + rule_id = 'ed1a2b05-0ad7-45d7-873f-008b575a02b3' + rule_max_kbps = 1000 + rule_max_burst = 100 + + mock_policy = { + 'id': policy_id, + 'name': policy_name, + 'description': '', + 'rules': [], + 'project_id': project_id, + 'tenant_id': project_id, + 'shared': False, + 'is_default': False + } + + mock_rule = { + 'id': rule_id, + 'max_kbps': rule_max_kbps, + 'max_burst_kbps': rule_max_burst, + 'direction': 'egress' + } + + qos_extension = { + "updated": "2015-06-08T10:00:00-00:00", + "name": "Quality of Service", + "links": [], + "alias": "qos", + "description": "The Quality of Service extension." + } + + qos_bw_limit_direction_extension = { + "updated": "2017-04-10T10:00:00-00:00", + "name": "Direction for QoS bandwidth limit rule", + "links": [], + "alias": "qos-bw-limit-direction", + "description": ("Allow to configure QoS bandwidth limit rule with " + "specific direction: ingress or egress") + } + + enabled_neutron_extensions = [qos_extension, + qos_bw_limit_direction_extension] + + def test_get_qos_bandwidth_limit_rule(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': [self.mock_policy]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_id, + 'bandwidth_limit_rules', + '%s.json' % self.rule_id]), + json={'bandwidth_limit_rule': self.mock_rule}) + ]) + r = self.cloud.get_qos_bandwidth_limit_rule(self.policy_name, + self.rule_id) + self.assertDictEqual(self.mock_rule, r) + self.assert_calls() + + def test_get_qos_bandwidth_limit_rule_no_qos_policy_found(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': []}) + ]) + self.assertRaises( + exc.OpenStackCloudResourceNotFound, + self.cloud.get_qos_bandwidth_limit_rule, + self.policy_name, self.rule_id) + self.assert_calls() + + def test_get_qos_bandwidth_limit_rule_no_qos_extension(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': []}) + ]) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.get_qos_bandwidth_limit_rule, + self.policy_name, self.rule_id) + self.assert_calls() + + def test_create_qos_bandwidth_limit_rule(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': [self.mock_policy]}), + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_id, + 'bandwidth_limit_rules']), + json={'bandwidth_limit_rule': self.mock_rule}) + ]) + rule = self.cloud.create_qos_bandwidth_limit_rule( + self.policy_name, max_kbps=self.rule_max_kbps) + self.assertDictEqual(self.mock_rule, rule) + self.assert_calls() + + def test_create_qos_bandwidth_limit_rule_no_qos_extension(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': []}) + ]) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.create_qos_bandwidth_limit_rule, self.policy_name, + max_kbps=100) + self.assert_calls() + + def test_create_qos_bandwidth_limit_rule_no_qos_direction_extension(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': [self.qos_extension]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': [self.qos_extension]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': [self.mock_policy]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': [self.qos_extension]}), + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_id, + 'bandwidth_limit_rules']), + json={'bandwidth_limit_rule': self.mock_rule}) + ]) + rule = self.cloud.create_qos_bandwidth_limit_rule( + self.policy_name, max_kbps=self.rule_max_kbps, direction="ingress") + self.assertDictEqual(self.mock_rule, rule) + self.assert_calls() + + def test_update_qos_bandwidth_limit_rule(self): + expected_rule = copy.copy(self.mock_rule) + expected_rule['max_kbps'] = self.rule_max_kbps + 100 + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': [self.mock_policy]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': [self.mock_policy]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_id, + 'bandwidth_limit_rules', + '%s.json' % self.rule_id]), + json={'bandwidth_limit_rule': self.mock_rule}), + dict(method='PUT', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_id, + 'bandwidth_limit_rules', + '%s.json' % self.rule_id]), + json={'bandwidth_limit_rule': expected_rule}, + validate=dict( + json={'bandwidth_limit_rule': { + 'max_kbps': self.rule_max_kbps + 100}})) + ]) + rule = self.cloud.update_qos_bandwidth_limit_rule( + self.policy_id, self.rule_id, max_kbps=self.rule_max_kbps + 100) + self.assertDictEqual(expected_rule, rule) + self.assert_calls() + + def test_update_qos_bandwidth_limit_rule_no_qos_extension(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': []}) + ]) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.update_qos_bandwidth_limit_rule, + self.policy_id, self.rule_id, max_kbps=2000) + self.assert_calls() + + def test_update_qos_bandwidth_limit_rule_no_qos_direction_extension(self): + expected_rule = copy.copy(self.mock_rule) + expected_rule['direction'] = self.rule_max_kbps + 100 + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': [self.qos_extension]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': [self.qos_extension]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': [self.mock_policy]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': [self.qos_extension]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': [self.qos_extension]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': [self.qos_extension]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': [self.mock_policy]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_id, + 'bandwidth_limit_rules', + '%s.json' % self.rule_id]), + json={'bandwidth_limit_rule': self.mock_rule}), + dict(method='PUT', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_id, + 'bandwidth_limit_rules', + '%s.json' % self.rule_id]), + json={'bandwidth_limit_rule': expected_rule}, + validate=dict( + json={'bandwidth_limit_rule': { + 'max_kbps': self.rule_max_kbps + 100}})) + ]) + rule = self.cloud.update_qos_bandwidth_limit_rule( + self.policy_id, self.rule_id, max_kbps=self.rule_max_kbps + 100, + direction="ingress") + # Even if there was attempt to change direction to 'ingress' it should + # be not changed in returned rule + self.assertDictEqual(expected_rule, rule) + self.assert_calls() + + def test_delete_qos_bandwidth_limit_rule(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': [self.mock_policy]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_id, + 'bandwidth_limit_rules', + '%s.json' % self.rule_id]), + json={}) + ]) + self.assertTrue( + self.cloud.delete_qos_bandwidth_limit_rule( + self.policy_name, self.rule_id)) + self.assert_calls() + + def test_delete_qos_bandwidth_limit_rule_no_qos_extension(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': []}) + ]) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.delete_qos_bandwidth_limit_rule, + self.policy_name, self.rule_id) + self.assert_calls() + + def test_delete_qos_bandwidth_limit_rule_not_found(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': [self.mock_policy]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_id, + 'bandwidth_limit_rules', + '%s.json' % self.rule_id]), + status_code=404) + ]) + self.assertFalse( + self.cloud.delete_qos_bandwidth_limit_rule( + self.policy_name, self.rule_id)) + self.assert_calls() From a134c10ea443ff74aa558aa7e83a651083d0d910 Mon Sep 17 00:00:00 2001 From: Sorin Sbarnea Date: Tue, 11 Jul 2017 12:23:46 +0100 Subject: [PATCH 1692/3836] Added useful links to README Fixes #2001116 Change-Id: I43c313fffe8ff0c0e8aa583adce7676872ce357b --- README.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.rst b/README.rst index fdfebc823..0364dcfc4 100644 --- a/README.rst +++ b/README.rst @@ -66,3 +66,12 @@ Sometimes an example is nice. cloud.create_server( 'my-server', image=image, flavor=flavor, wait=True, auto_ip=True) + +Links +===== + +* `Issue Tracker `_ +* `Code Review `_ +* `Documentation `_ +* `PyPI `_ +* `Mailing list `_ From 4a81b26b09daf9eee82b6a319f39134e5f3f5277 Mon Sep 17 00:00:00 2001 From: liyi Date: Tue, 11 Jul 2017 17:11:09 +0800 Subject: [PATCH 1693/3836] Unify style of 'domain' field The output of CLI need unify style of 'domain' filed. Change-Id: I556db6060af4ec480f50ae253eb652b5c42eec27 Partial-Bug: #1703545 --- openstack/cluster/v1/action.py | 2 ++ openstack/cluster/v1/node.py | 2 ++ openstack/cluster/v1/policy.py | 2 ++ openstack/cluster/v1/profile.py | 2 ++ openstack/tests/unit/cluster/v1/test_action.py | 2 ++ openstack/tests/unit/cluster/v1/test_node.py | 2 ++ openstack/tests/unit/cluster/v1/test_policy.py | 2 ++ openstack/tests/unit/cluster/v1/test_profile.py | 2 ++ 8 files changed, 16 insertions(+) diff --git a/openstack/cluster/v1/action.py b/openstack/cluster/v1/action.py index 255a34bc0..a7db8aa90 100644 --- a/openstack/cluster/v1/action.py +++ b/openstack/cluster/v1/action.py @@ -44,6 +44,8 @@ class Action(resource.Resource): user_id = resource.Body('user') #: The ID of the project this profile belongs to. project_id = resource.Body('project') + #: The domain ID of the action. + domain_id = resource.Body('domain') #: Interval in seconds between two consecutive executions. interval = resource.Body('interval') #: The time the action was started. diff --git a/openstack/cluster/v1/node.py b/openstack/cluster/v1/node.py index 3b8bfa261..3c10dcc75 100644 --- a/openstack/cluster/v1/node.py +++ b/openstack/cluster/v1/node.py @@ -44,6 +44,8 @@ class Node(resource.Resource): cluster_id = resource.Body('cluster_id') #: The ID of the profile used by this node. profile_id = resource.Body('profile_id') + #: The domain ID of the node. + domain_id = resource.Body('domain') #: The ID of the user who created this node. user_id = resource.Body('user') #: The ID of the project this node belongs to. diff --git a/openstack/cluster/v1/policy.py b/openstack/cluster/v1/policy.py index 7b2e67bd3..0137220b3 100644 --- a/openstack/cluster/v1/policy.py +++ b/openstack/cluster/v1/policy.py @@ -39,6 +39,8 @@ class Policy(resource.Resource): type = resource.Body('type') #: The ID of the project this policy belongs to. project_id = resource.Body('project') + # The domain ID of the policy. + domain_id = resource.Body('domain') #: The ID of the user who created this policy. user_id = resource.Body('user') #: The timestamp when the policy is created. diff --git a/openstack/cluster/v1/profile.py b/openstack/cluster/v1/profile.py index 7688e07cf..711b302f2 100644 --- a/openstack/cluster/v1/profile.py +++ b/openstack/cluster/v1/profile.py @@ -39,6 +39,8 @@ class Profile(resource.Resource): type = resource.Body('type') #: The ID of the project this profile belongs to. project_id = resource.Body('project') + #: The domain ID of the profile. + domain_id = resource.Body('domain') #: The ID of the user who created this profile. user_id = resource.Body('user') #: The spec of the profile. diff --git a/openstack/tests/unit/cluster/v1/test_action.py b/openstack/tests/unit/cluster/v1/test_action.py index 0cf72a380..017779520 100644 --- a/openstack/tests/unit/cluster/v1/test_action.py +++ b/openstack/tests/unit/cluster/v1/test_action.py @@ -27,6 +27,7 @@ 'owner': None, 'user': '3747afc360b64702a53bdd64dc1b8976', 'project': '42d9e9663331431f97b75e25136307ff', + 'domain': '204ccccd267b40aea871750116b5b184', 'interval': -1, 'start_time': 1453414055.48672, 'end_time': 1453414055.48672, @@ -66,6 +67,7 @@ def test_instantiate(self): self.assertEqual(FAKE['owner'], sot.owner_id) self.assertEqual(FAKE['user'], sot.user_id) self.assertEqual(FAKE['project'], sot.project_id) + self.assertEqual(FAKE['domain'], sot.domain_id) self.assertEqual(FAKE['interval'], sot.interval) self.assertEqual(FAKE['start_time'], sot.start_at) self.assertEqual(FAKE['end_time'], sot.end_at) diff --git a/openstack/tests/unit/cluster/v1/test_node.py b/openstack/tests/unit/cluster/v1/test_node.py index aefffd037..cdf938968 100644 --- a/openstack/tests/unit/cluster/v1/test_node.py +++ b/openstack/tests/unit/cluster/v1/test_node.py @@ -25,6 +25,7 @@ 'metadata': {'key1': 'value1'}, 'name': FAKE_NAME, 'profile_id': 'myserver', + 'domain': '204ccccd267b40aea871750116b5b184', 'user': '3747afc360b64702a53bdd64dc1b8976', 'project': '42d9e9663331431f97b75e25136307ff', 'index': 1, @@ -57,6 +58,7 @@ def test_instantiate(self): self.assertEqual(FAKE['cluster_id'], sot.cluster_id) self.assertEqual(FAKE['user'], sot.user_id) self.assertEqual(FAKE['project'], sot.project_id) + self.assertEqual(FAKE['domain'], sot.domain_id) self.assertEqual(FAKE['name'], sot.name) self.assertEqual(FAKE['index'], sot.index) self.assertEqual(FAKE['role'], sot.role) diff --git a/openstack/tests/unit/cluster/v1/test_policy.py b/openstack/tests/unit/cluster/v1/test_policy.py index 3c2be2043..7a9b18fab 100644 --- a/openstack/tests/unit/cluster/v1/test_policy.py +++ b/openstack/tests/unit/cluster/v1/test_policy.py @@ -32,6 +32,7 @@ } }, 'project': '42d9e9663331431f97b75e25136307ff', + 'domain': '204ccccd267b40aea871750116b5b184', 'user': '3747afc360b64702a53bdd64dc1b8976', 'type': 'senlin.policy.deletion-1.0', 'created_at': '2015-10-10T12:46:36.000000', @@ -63,6 +64,7 @@ def test_instantiate(self): self.assertEqual(FAKE['name'], sot.name) self.assertEqual(FAKE['spec'], sot.spec) self.assertEqual(FAKE['project'], sot.project_id) + self.assertEqual(FAKE['domain'], sot.domain_id) self.assertEqual(FAKE['user'], sot.user_id) self.assertEqual(FAKE['data'], sot.data) self.assertEqual(FAKE['created_at'], sot.created_at) diff --git a/openstack/tests/unit/cluster/v1/test_profile.py b/openstack/tests/unit/cluster/v1/test_profile.py index e371ed1b4..7a65bf6f3 100644 --- a/openstack/tests/unit/cluster/v1/test_profile.py +++ b/openstack/tests/unit/cluster/v1/test_profile.py @@ -33,6 +33,7 @@ } }, 'project': '42d9e9663331431f97b75e25136307ff', + 'domain': '204ccccd267b40aea871750116b5b184', 'user': '3747afc360b64702a53bdd64dc1b8976', 'type': 'os.nova.server', 'created_at': '2015-10-10T12:46:36.000000', @@ -65,6 +66,7 @@ def test_instantiate(self): self.assertEqual(FAKE['metadata'], sot.metadata) self.assertEqual(FAKE['spec'], sot.spec) self.assertEqual(FAKE['project'], sot.project_id) + self.assertEqual(FAKE['domain'], sot.domain_id) self.assertEqual(FAKE['user'], sot.user_id) self.assertEqual(FAKE['type'], sot.type) self.assertEqual(FAKE['created_at'], sot.created_at) From 262061afd05e9fd097e11f7b073a7dd0e3f44627 Mon Sep 17 00:00:00 2001 From: Akihiro Motoki Date: Wed, 12 Jul 2017 06:43:50 +0000 Subject: [PATCH 1694/3836] Improve doc formatting a bit After migrating to openstackdocstheme, some shade document became not easy to read. This commit fixes them a bit. - openstackdocstheme assumee only one title per page. Use a different level of title for the title. Otherwise, titles with the same level are not shown. - Release notes page has a lot of sections. It leads to a long TOC in the user guide index page. Use maxdepth=1 explicitly for the release notes. - Add a link to a simple example to usage.rst. It helps users who access the user guide directly. Change-Id: If51afa471505296b502bed3288cc9bcf30a69ba3 --- README.rst | 2 ++ doc/source/index.rst | 6 ++++++ doc/source/user/usage.rst | 2 ++ 3 files changed, 10 insertions(+) diff --git a/README.rst b/README.rst index 0364dcfc4..e4c4e8a66 100644 --- a/README.rst +++ b/README.rst @@ -17,6 +17,8 @@ library, and adding logic and features that the OpenStack Infra team had developed to run client applications at scale, it turned out that we'd written nine-tenths of what we'd need to have a standalone library. +.. _example: + Example ======= diff --git a/doc/source/index.rst b/doc/source/index.rst index 850bcff37..5235e1664 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -3,6 +3,7 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. +================================= Welcome to shade's documentation! ================================= @@ -14,6 +15,11 @@ Contents: install/index user/index contributor/index + +.. releasenotes contains a lot of sections, toctree with maxdepth 1 is used. +.. toctree:: + :maxdepth: 1 + releasenotes/index .. include:: ../../README.rst diff --git a/doc/source/user/usage.rst b/doc/source/user/usage.rst index d5348c389..fe37d74cb 100644 --- a/doc/source/user/usage.rst +++ b/doc/source/user/usage.rst @@ -6,6 +6,8 @@ To use shade in a project:: import shade +For a simple example, see :ref:`example`. + .. note:: API methods that return a description of an OpenStack resource (e.g., server instance, image, volume, etc.) do so using a `munch.Munch` object From dadce7b74045d460e5c0073e52746832b0786796 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 14 Jul 2017 05:08:29 +0000 Subject: [PATCH 1695/3836] Updated from global requirements Change-Id: I6358a54b79de12f9b37aff759d7587b2c8d014bb --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 872accec7..318d0b148 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,7 +3,7 @@ # process, which may cause wedges in the gate later. hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 -beautifulsoup4 # MIT +beautifulsoup4>=4.6.0 # MIT coverage!=4.4,>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD mock>=2.0 # BSD From d946438a343bac1f0c31a6a14ed6333d4db166f7 Mon Sep 17 00:00:00 2001 From: tengqm Date: Mon, 17 Jul 2017 21:31:10 -0400 Subject: [PATCH 1696/3836] Support to node-adopt and node-adopt-preview This adds support to adopt and adopt-preview API added to Senlin API (microversion 1.7) Change-Id: Ic3a11a094ba36d6875953cf10da11aae563e10a4 --- doc/source/users/proxies/cluster.rst | 1 + openstack/cluster/v1/_proxy.py | 32 +++++++++++++++++++ openstack/cluster/v1/node.py | 18 +++++++++++ openstack/tests/unit/cluster/v1/test_node.py | 28 ++++++++++++++++ openstack/tests/unit/cluster/v1/test_proxy.py | 22 +++++++++++++ 5 files changed, 101 insertions(+) diff --git a/doc/source/users/proxies/cluster.rst b/doc/source/users/proxies/cluster.rst index 9951478f0..db7eec004 100644 --- a/doc/source/users/proxies/cluster.rst +++ b/doc/source/users/proxies/cluster.rst @@ -125,6 +125,7 @@ Node Operations .. automethod:: openstack.cluster.v1._proxy.Proxy.recover_node .. automethod:: openstack.cluster.v1._proxy.Proxy.perform_operation_on_node + .. automethod:: openstack.cluster.v1._proxy.Proxy.adopt_node .. automethod:: openstack.cluster.v1._proxy.Proxy.node_operation diff --git a/openstack/cluster/v1/_proxy.py b/openstack/cluster/v1/_proxy.py index 44778a625..29ef4b6a7 100644 --- a/openstack/cluster/v1/_proxy.py +++ b/openstack/cluster/v1/_proxy.py @@ -722,6 +722,38 @@ def recover_node(self, node, **params): obj = self._get_resource(_node.Node, node) return obj.recover(self._session, **params) + def adopt_node(self, preview=False, **attrs): + """Adopting an existing resource as a node. + + :param preview: A boolean indicating whether this is a "preview" + operation which means only the profile to be used is returned + rather than creating a node object using that profile. + :param dict attrs: Keyword parameters for node adoption. Valid + parameters include: + + * type: (Required) A string containing the profile type and + version to be used for node adoption. For example, + ``os.nova.sever-1.0``. + * identity: (Required) A string including the name or ID of an + OpenStack resource to be adopted as a Senlin node. + * name: (Optional) The name of of node to be created. Omitting + this parameter will have the node named automatically. + * snapshot: (Optional) A boolean indicating whether a snapshot + of the target resource should be created if possible. Default + is False. + * metadata: (Optional) A dictionary of arbitrary key-value pairs + to be associated with the adopted node. + * overrides: (Optional) A dictionary of key-value pairs to be used + to override attributes derived from the target resource. + + :returns: The result of node adoption. If `preview` is set to False + (default), returns a :class:`~openstack.cluster.v1.node.Node` + object, otherwise a Dict is returned containing the profile to + be used for the new node. + """ + node = self._get_resource(_node.Node, None) + return node.adopt(self._session, preview=preview, **attrs) + @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", details="Use perform_operation_on_node instead") def node_operation(self, node, operation, **params): diff --git a/openstack/cluster/v1/node.py b/openstack/cluster/v1/node.py index 3b8bfa261..fe442d0a9 100644 --- a/openstack/cluster/v1/node.py +++ b/openstack/cluster/v1/node.py @@ -125,6 +125,24 @@ def op(self, session, operation, **params): json={operation: params}) return resp.json() + def adopt(self, session, preview=False, **params): + """Adopt a node for management. + + :param session: A session object used for sending request. + :param preview: A boolean indicating whether the adoption is a + preview. A "preview" does not create the node object. + :param dict params: A dict providing the details of a node to be + adopted. + """ + path = "adopt-preview" if preview else "adopt" + url = utils.urljoin(self.base_path, path) + resp = session.post(url, endpoint_filter=self.service, json=params) + if preview: + return resp.json() + + self._translate_response(resp) + return self + class NodeDetail(Node): base_path = '/nodes/%(node_id)s?show_details=True' diff --git a/openstack/tests/unit/cluster/v1/test_node.py b/openstack/tests/unit/cluster/v1/test_node.py index aefffd037..a62258232 100644 --- a/openstack/tests/unit/cluster/v1/test_node.py +++ b/openstack/tests/unit/cluster/v1/test_node.py @@ -104,6 +104,34 @@ def test_operation(self): sess.post.assert_called_once_with(url, endpoint_filter=sot.service, json={'dance': {'style': 'tango'}}) + def test_adopt_preview(self): + sot = node.Node.new() + resp = mock.Mock() + resp.headers = {} + resp.json = mock.Mock(return_value={"foo": "bar"}) + sess = mock.Mock() + sess.post = mock.Mock(return_value=resp) + + res = sot.adopt(sess, True, param="value") + self.assertEqual({"foo": "bar"}, res) + sess.post.assert_called_once_with("nodes/adopt-preview", + endpoint_filter=sot.service, + json={"param": "value"}) + + def test_adopt(self): + sot = node.Node.new() + resp = mock.Mock() + resp.headers = {} + resp.json = mock.Mock(return_value={"foo": "bar"}) + sess = mock.Mock() + sess.post = mock.Mock(return_value=resp) + + res = sot.adopt(sess, False, param="value") + self.assertEqual(sot, res) + sess.post.assert_called_once_with("nodes/adopt", + endpoint_filter=sot.service, + json={"param": "value"}) + class TestNodeDetail(testtools.TestCase): diff --git a/openstack/tests/unit/cluster/v1/test_proxy.py b/openstack/tests/unit/cluster/v1/test_proxy.py index 3609b59f1..ec6b55c35 100644 --- a/openstack/tests/unit/cluster/v1/test_proxy.py +++ b/openstack/tests/unit/cluster/v1/test_proxy.py @@ -391,6 +391,28 @@ def test_node_recover(self, mock_get): method_args=["FAKE_NODE"]) mock_get.assert_called_once_with(node.Node, "FAKE_NODE") + @mock.patch.object(proxy_base.BaseProxy, '_get_resource') + def test_node_adopt(self, mock_get): + mock_node = node.Node.new() + mock_get.return_value = mock_node + self._verify("openstack.cluster.v1.node.Node.adopt", + self.proxy.adopt_node, + method_kwargs={"preview": False, "foo": "bar"}, + expected_kwargs={"preview": False, "foo": "bar"}) + + mock_get.assert_called_once_with(node.Node, None) + + @mock.patch.object(proxy_base.BaseProxy, '_get_resource') + def test_node_adopt_preview(self, mock_get): + mock_node = node.Node.new() + mock_get.return_value = mock_node + self._verify("openstack.cluster.v1.node.Node.adopt", + self.proxy.adopt_node, + method_kwargs={"preview": True, "foo": "bar"}, + expected_kwargs={"preview": True, "foo": "bar"}) + + mock_get.assert_called_once_with(node.Node, None) + @deprecation.fail_if_not_removed @mock.patch.object(proxy_base.BaseProxy, '_get_resource') def test_node_operation(self, mock_get): From 4413f46983c8a589bee88e9b9eac00690cbd5668 Mon Sep 17 00:00:00 2001 From: Ankur Gupta Date: Thu, 6 Apr 2017 20:20:59 -0500 Subject: [PATCH 1697/3836] Update load_balancer for v2 API This patch updates the load_balancer.v2.load_balancer for the current state of the API It also updates the post_test_hook script to accept an argument to restrict the functional tests run. Since the load-balancer tests can take a significant time to run, this allows us to run a parallel gate job for the load-balancer functional tests. Co-Authored-By: Michael Johnson Change-Id: I6c526cd8225a929d3d5984da10b6478312461e6f --- doc/source/enforcer.py | 1 + doc/source/users/index.rst | 1 + doc/source/users/proxies/load_balancer_v2.rst | 5 +- .../users/resources/load_balancer/index.rst | 7 +++ .../load_balancer/v2/load_balancer.rst | 13 ++++ .../load_balancer/load_balancer_service.py | 4 +- openstack/load_balancer/v2/_proxy.py | 14 +++++ openstack/load_balancer/v2/load_balancer.py | 25 ++++++-- openstack/session.py | 5 ++ .../tests/functional/load_balancer/base.py | 63 +++++++++++++++++++ .../load_balancer/v2/test_load_balancer.py | 35 ++++++++--- .../unit/load_balancer/test_load_balancer.py | 42 ++++++++----- .../test_load_balancer_service.py | 4 +- .../tests/unit/load_balancer/test_proxy.py | 4 ++ .../tests/unit/load_balancer/test_version.py | 2 +- openstack/tests/unit/test_profile.py | 4 +- post_test_hook.sh | 6 +- 17 files changed, 197 insertions(+), 38 deletions(-) create mode 100644 doc/source/users/resources/load_balancer/index.rst create mode 100644 doc/source/users/resources/load_balancer/v2/load_balancer.rst create mode 100644 openstack/tests/functional/load_balancer/base.py diff --git a/doc/source/enforcer.py b/doc/source/enforcer.py index 8f653b099..40740e033 100644 --- a/doc/source/enforcer.py +++ b/doc/source/enforcer.py @@ -36,6 +36,7 @@ def get_proxy_methods(): "openstack.image.v1._proxy", "openstack.image.v2._proxy", "openstack.key_manager.v1._proxy", + "openstack.load_balancer.v2._proxy", "openstack.message.v1._proxy", "openstack.message.v2._proxy", "openstack.metric.v1._proxy", diff --git a/doc/source/users/index.rst b/doc/source/users/index.rst index 2209a451d..c06b98577 100644 --- a/doc/source/users/index.rst +++ b/doc/source/users/index.rst @@ -114,6 +114,7 @@ The following services have exposed *Resource* classes. Identity Image Key Management + Load Balancer Metric Network Orchestration diff --git a/doc/source/users/proxies/load_balancer_v2.rst b/doc/source/users/proxies/load_balancer_v2.rst index 220c4f708..ef566a563 100644 --- a/doc/source/users/proxies/load_balancer_v2.rst +++ b/doc/source/users/proxies/load_balancer_v2.rst @@ -16,7 +16,8 @@ Load Balancer Operations .. autoclass:: openstack.load_balancer.v2._proxy.Proxy .. automethod:: openstack.load_balancer.v2._proxy.Proxy.create_load_balancer - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_load_balancer - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.load_balancers .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_load_balancer .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_load_balancer + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_load_balancer + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.load_balancers + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_load_balancer diff --git a/doc/source/users/resources/load_balancer/index.rst b/doc/source/users/resources/load_balancer/index.rst new file mode 100644 index 000000000..61838c730 --- /dev/null +++ b/doc/source/users/resources/load_balancer/index.rst @@ -0,0 +1,7 @@ +Load Balancer Resources +======================= + +.. toctree:: + :maxdepth: 1 + + v2/load_balancer diff --git a/doc/source/users/resources/load_balancer/v2/load_balancer.rst b/doc/source/users/resources/load_balancer/v2/load_balancer.rst new file mode 100644 index 000000000..fd55e4668 --- /dev/null +++ b/doc/source/users/resources/load_balancer/v2/load_balancer.rst @@ -0,0 +1,13 @@ +openstack.load_balancer.v2.load_balancer +======================================== + +.. automodule:: openstack.load_balancer.v2.load_balancer + +The LoadBalancer Class +---------------------- + +The ``LoadBalancer`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.load_balancer.v2.load_balancer.LoadBalancer + :members: + diff --git a/openstack/load_balancer/load_balancer_service.py b/openstack/load_balancer/load_balancer_service.py index 1ff22b22f..3c645e6f6 100644 --- a/openstack/load_balancer/load_balancer_service.py +++ b/openstack/load_balancer/load_balancer_service.py @@ -16,11 +16,11 @@ class LoadBalancerService(service_filter.ServiceFilter): """The load balancer service.""" - valid_versions = [service_filter.ValidVersion('v2')] + valid_versions = [service_filter.ValidVersion('v2', 'v2.0')] def __init__(self, version=None): """Create a load balancer service.""" super(LoadBalancerService, self).__init__( - service_type='load_balancer', + service_type='load-balancer', version=version ) diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index 224fddb57..4533cb664 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -80,3 +80,17 @@ def find_load_balancer(self, name_or_id, ignore_missing=True): """ return self._find(_lb.LoadBalancer, name_or_id, ignore_missing=ignore_missing) + + def update_load_balancer(self, load_balancer, **attrs): + """Update a load balancer + + :param load_balancer: The load_balancer can be either the name or a + :class:`~openstack.load_balancer.v2.load_balancer.LoadBalancer` + instance + :param dict attrs: The attributes to update on the load balancer + represented by ``load_balancer``. + + :returns: The updated load_balancer + :rtype: :class:`~openstack.load_balancer.v2.load_balancer.LoadBalancer` + """ + return self._update(_lb.LoadBalancer, load_balancer, **attrs) diff --git a/openstack/load_balancer/v2/load_balancer.py b/openstack/load_balancer/v2/load_balancer.py index 509f0ae80..7c444376a 100644 --- a/openstack/load_balancer/v2/load_balancer.py +++ b/openstack/load_balancer/v2/load_balancer.py @@ -17,22 +17,31 @@ class LoadBalancer(resource.Resource): resource_key = 'loadbalancer' resources_key = 'loadbalancers' - base_path = '/loadbalancers' + base_path = '/v2.0/lbaas/loadbalancers' service = lb_service.LoadBalancerService() # capabilities allow_create = True - allow_list = True allow_get = True + allow_update = True allow_delete = True + allow_list = True + + _query_mapping = resource.QueryParameters( + 'description', 'flavor', 'name', 'project_id', 'provider', + 'vip_address', 'vip_network_id', 'vip_port_id', 'vip_subnet_id', + is_admin_state_up='admin_state_up' + ) #: Properties + #: The administrative state of the load balancer *Type: bool* + is_admin_state_up = resource.Body('admin_state_up', type=bool) #: Timestamp when the load balancer was created created_at = resource.Body('created_at') #: The load balancer description description = resource.Body('description') - #: The administrative state of the load balancer *Type: bool* - is_admin_state_up = resource.Body('admin_state_up', type=bool) + #: The load balancer flavor + flavor = resource.Body('flavor') #: List of listeners associated with this load balancer listeners = resource.Body('listeners', type=list) #: The load balancer name @@ -43,13 +52,17 @@ class LoadBalancer(resource.Resource): pools = resource.Body('pools', type=list) #: The ID of the project this load balancer is associated with. project_id = resource.Body('project_id') + #: Provider name for the load balancer. + provider = resource.Body('provider') #: The provisioning status of this load balancer provisioning_status = resource.Body('provisioning_status') + #: Timestamp when the load balancer was last updated + updated_at = resource.Body('updated_at') #: VIP address of load balancer vip_address = resource.Body('vip_address') + #: VIP netowrk ID + vip_network_id = resource.Body('vip_network_id') #: VIP port ID vip_port_id = resource.Body('vip_port_id') #: VIP subnet ID vip_subnet_id = resource.Body('vip_subnet_id') - #: Timestamp when the load balancer was last updated - updated_at = resource.Body('updated_at') diff --git a/openstack/session.py b/openstack/session.py index f3a55e902..d989d1172 100644 --- a/openstack/session.py +++ b/openstack/session.py @@ -330,6 +330,11 @@ def get_endpoint(self, auth=None, interface=None, service_type=None, self.endpoint_cache[key] = sc_endpoint return sc_endpoint + # We just want what is returned from catalog + if service_type == "load-balancer": + self.endpoint_cache[key] = sc_endpoint + return sc_endpoint + endpoint = self._get_endpoint_versions(service_type, sc_endpoint) profile_version = self._parse_version(filt.version) diff --git a/openstack/tests/functional/load_balancer/base.py b/openstack/tests/functional/load_balancer/base.py new file mode 100644 index 000000000..09f96f093 --- /dev/null +++ b/openstack/tests/functional/load_balancer/base.py @@ -0,0 +1,63 @@ +# Copyright 2017 Rackspace, US 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 time + +from openstack import exceptions +from openstack.tests.functional import base + + +class BaseLBFunctionalTest(base.BaseFunctionalTest): + + @classmethod + def setUpClass(cls): + super(BaseLBFunctionalTest, cls).setUpClass() + + @classmethod + def lb_wait_for_status(cls, lb, status, failures, interval=1, wait=120): + """Wait for load balancer to be in a particular provisioning status. + + :param lb: The load balancer to wait on to reach the status. + :type lb: :class:`~openstack.load_blanacer.v2.load_balancer + :param status: Desired status of the resource. + :param list failures: Statuses that would indicate the transition + failed such as 'ERROR'. + :param interval: Number of seconds to wait between checks. + :param wait: Maximum number of seconds to wait for transition. + Note, most actions should easily finish in 120 seconds, + but for load balancer create slow hosts can take up to + ten minutes for nova to fully boot a VM. + :return: None + :raises: :class:`~openstack.exceptions.ResourceTimeout` transition + to status failed to occur in wait seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` resource + transitioned to one of the failure states. + """ + + total_sleep = 0 + if failures is None: + failures = [] + + while total_sleep < wait: + lb = cls.conn.load_balancer.get_load_balancer(lb.id) + if lb.provisioning_status == status: + return None + if lb.provisioning_status in failures: + msg = ("Load Balancer %s transitioned to failure state %s" % + (lb.id, lb.provisioning_status)) + raise exceptions.ResourceFailure(msg) + time.sleep(interval) + total_sleep += interval + msg = "Timeout waiting for Load Balancer %s to transition to %s" % ( + lb.id, status) + raise exceptions.ResourceTimeout(msg) diff --git a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py index acf953498..a6b126651 100644 --- a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py +++ b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py @@ -15,30 +15,43 @@ from openstack.load_balancer.v2 import load_balancer from openstack.tests.functional import base +from openstack.tests.functional.load_balancer import base as lb_base -@unittest.skipUnless(base.service_exists(service_type='load_balancer'), - 'Load-balancing service does not exist') -class TestLoadBalancer(base.BaseFunctionalTest): +@unittest.skipUnless(base.service_exists(service_type='load-balancer'), + 'Load-balancer service does not exist') +class TestLoadBalancer(lb_base.BaseLBFunctionalTest): NAME = uuid.uuid4().hex ID = None - VIP_SUBNET_ID = uuid.uuid4().hex + VIP_SUBNET_ID = None + PROJECT_ID = None + UPDATE_NAME = uuid.uuid4().hex @classmethod def setUpClass(cls): super(TestLoadBalancer, cls).setUpClass() + subnets = list(cls.conn.network.subnets()) + cls.VIP_SUBNET_ID = subnets[0].id + cls.PROJECT_ID = cls.conn.session.get_project_id() test_lb = cls.conn.load_balancer.create_load_balancer( - name=cls.NAME, vip_subnet_id=cls.VIP_SUBNET_ID) + name=cls.NAME, vip_subnet_id=cls.VIP_SUBNET_ID, + project_id=cls.PROJECT_ID) assert isinstance(test_lb, load_balancer.LoadBalancer) cls.assertIs(cls.NAME, test_lb.name) + # Wait for the LB to go ACTIVE. On non-virtualization enabled hosts + # it can take nova up to ten minutes to boot a VM. + cls.lb_wait_for_status(test_lb, status='ACTIVE', + failures=['ERROR'], interval=1, wait=600) cls.ID = test_lb.id @classmethod def tearDownClass(cls): - test_lb = cls.conn.load_balancer.delete_load_balancer( + test_lb = cls.conn.load_balancer.get_load_balancer(cls.ID) + cls.lb_wait_for_status(test_lb, status='ACTIVE', + failures=['ERROR']) + cls.conn.load_balancer.delete_load_balancer( cls.ID, ignore_missing=False) - cls.assertIs(None, test_lb) def test_find(self): test_lb = self.conn.load_balancer.find_load_balancer(self.NAME) @@ -53,3 +66,11 @@ def test_get(self): def test_list(self): names = [lb.name for lb in self.conn.load_balancer.load_balancers()] self.assertIn(self.NAME, names) + + def test_update(self): + update_lb = self.conn.load_balancer.update_load_balancer( + self.ID, name=self.UPDATE_NAME) + self.lb_wait_for_status(update_lb, status='ACTIVE', + failures=['ERROR']) + test_lb = self.conn.load_balancer.get_load_balancer(self.ID) + self.assertEqual(self.UPDATE_NAME, test_lb.name) diff --git a/openstack/tests/unit/load_balancer/test_load_balancer.py b/openstack/tests/unit/load_balancer/test_load_balancer.py index 0e832ab82..98196b312 100644 --- a/openstack/tests/unit/load_balancer/test_load_balancer.py +++ b/openstack/tests/unit/load_balancer/test_load_balancer.py @@ -11,24 +11,29 @@ # under the License. import testtools +import uuid from openstack.load_balancer.v2 import load_balancer IDENTIFIER = 'IDENTIFIER' EXAMPLE = { 'admin_state_up': True, - 'created_at': '3', + 'created_at': '2017-07-17T12:14:57.233772', 'description': 'fake_description', + 'flavor': uuid.uuid4(), 'id': IDENTIFIER, - 'listeners': [{'id', '4'}], + 'listeners': [{'id', uuid.uuid4()}], 'name': 'test_load_balancer', - 'operating_status': '6', - 'provisioning_status': '7', - 'project_id': '8', - 'vip_address': '9', - 'vip_subnet_id': '10', - 'vip_port_id': '11', - 'pools': [{'id', '13'}], + 'operating_status': 'ONLINE', + 'pools': [{'id', uuid.uuid4()}], + 'project_id': uuid.uuid4(), + 'provider': 'fake_provider', + 'provisioning_status': 'ACTIVE', + 'updated_at': '2017-07-17T12:16:57.233772', + 'vip_address': '192.0.2.5', + 'vip_network_id': uuid.uuid4(), + 'vip_port_id': uuid.uuid4(), + 'vip_subnet_id': uuid.uuid4(), } @@ -38,13 +43,15 @@ def test_basic(self): test_load_balancer = load_balancer.LoadBalancer() self.assertEqual('loadbalancer', test_load_balancer.resource_key) self.assertEqual('loadbalancers', test_load_balancer.resources_key) - self.assertEqual('/loadbalancers', test_load_balancer.base_path) - self.assertEqual('load_balancer', + self.assertEqual('/v2.0/lbaas/loadbalancers', + test_load_balancer.base_path) + self.assertEqual('load-balancer', test_load_balancer.service.service_type) self.assertTrue(test_load_balancer.allow_create) self.assertTrue(test_load_balancer.allow_get) self.assertTrue(test_load_balancer.allow_delete) self.assertTrue(test_load_balancer.allow_list) + self.assertTrue(test_load_balancer.allow_update) def test_make_it(self): test_load_balancer = load_balancer.LoadBalancer(**EXAMPLE) @@ -52,18 +59,23 @@ def test_make_it(self): self.assertEqual(EXAMPLE['created_at'], test_load_balancer.created_at), self.assertEqual(EXAMPLE['description'], test_load_balancer.description) + self.assertEqual(EXAMPLE['flavor'], test_load_balancer.flavor) self.assertEqual(EXAMPLE['id'], test_load_balancer.id) self.assertEqual(EXAMPLE['listeners'], test_load_balancer.listeners) self.assertEqual(EXAMPLE['name'], test_load_balancer.name) self.assertEqual(EXAMPLE['operating_status'], test_load_balancer.operating_status) + self.assertEqual(EXAMPLE['pools'], test_load_balancer.pools) + self.assertEqual(EXAMPLE['project_id'], test_load_balancer.project_id) + self.assertEqual(EXAMPLE['provider'], test_load_balancer.provider) self.assertEqual(EXAMPLE['provisioning_status'], test_load_balancer.provisioning_status) - self.assertEqual(EXAMPLE['project_id'], test_load_balancer.project_id) + self.assertEqual(EXAMPLE['updated_at'], test_load_balancer.updated_at), self.assertEqual(EXAMPLE['vip_address'], test_load_balancer.vip_address) - self.assertEqual(EXAMPLE['vip_subnet_id'], - test_load_balancer.vip_subnet_id) + self.assertEqual(EXAMPLE['vip_network_id'], + test_load_balancer.vip_network_id) self.assertEqual(EXAMPLE['vip_port_id'], test_load_balancer.vip_port_id) - self.assertEqual(EXAMPLE['pools'], test_load_balancer.pools) + self.assertEqual(EXAMPLE['vip_subnet_id'], + test_load_balancer.vip_subnet_id) diff --git a/openstack/tests/unit/load_balancer/test_load_balancer_service.py b/openstack/tests/unit/load_balancer/test_load_balancer_service.py index dd11ae55f..6ad82dfc5 100644 --- a/openstack/tests/unit/load_balancer/test_load_balancer_service.py +++ b/openstack/tests/unit/load_balancer/test_load_balancer_service.py @@ -19,10 +19,10 @@ class TestLoadBalancingService(testtools.TestCase): def test_service(self): sot = lb_service.LoadBalancerService() - self.assertEqual('load_balancer', sot.service_type) + self.assertEqual('load-balancer', sot.service_type) self.assertEqual('public', sot.interface) self.assertIsNone(sot.region) self.assertIsNone(sot.service_name) self.assertEqual(1, len(sot.valid_versions)) self.assertEqual('v2', sot.valid_versions[0].module) - self.assertEqual('v2', sot.valid_versions[0].path) + self.assertEqual('v2.0', sot.valid_versions[0].path) diff --git a/openstack/tests/unit/load_balancer/test_proxy.py b/openstack/tests/unit/load_balancer/test_proxy.py index 97cdec134..267ea0a98 100644 --- a/openstack/tests/unit/load_balancer/test_proxy.py +++ b/openstack/tests/unit/load_balancer/test_proxy.py @@ -40,3 +40,7 @@ def test_load_balancer_delete(self): def test_load_balancer_find(self): self.verify_find(self.proxy.find_load_balancer, lb.LoadBalancer) + + def test_load_balancer_update(self): + self.verify_update(self.proxy.update_load_balancer, + lb.LoadBalancer) diff --git a/openstack/tests/unit/load_balancer/test_version.py b/openstack/tests/unit/load_balancer/test_version.py index 1b77eda26..8461c4e51 100644 --- a/openstack/tests/unit/load_balancer/test_version.py +++ b/openstack/tests/unit/load_balancer/test_version.py @@ -29,7 +29,7 @@ def test_basic(self): self.assertEqual('version', sot.resource_key) self.assertEqual('versions', sot.resources_key) self.assertEqual('/', sot.base_path) - self.assertEqual('load_balancer', sot.service.service_type) + self.assertEqual('load-balancer', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_get) self.assertFalse(sot.allow_update) diff --git a/openstack/tests/unit/test_profile.py b/openstack/tests/unit/test_profile.py index 272918171..c11d05f2a 100644 --- a/openstack/tests/unit/test_profile.py +++ b/openstack/tests/unit/test_profile.py @@ -27,7 +27,7 @@ def test_init(self): 'identity', 'image', 'key-manager', - 'load_balancer', + 'load-balancer', 'messaging', 'metering', 'network', @@ -46,7 +46,7 @@ def test_default_versions(self): self.assertEqual('v1', prof.get_filter('database').version) self.assertEqual('v3', prof.get_filter('identity').version) self.assertEqual('v2', prof.get_filter('image').version) - self.assertEqual('v2', prof.get_filter('load_balancer').version) + self.assertEqual('v2', prof.get_filter('load-balancer').version) self.assertEqual('v2', prof.get_filter('network').version) self.assertEqual('v1', prof.get_filter('object-store').version) self.assertEqual('v1', prof.get_filter('orchestration').version) diff --git a/post_test_hook.sh b/post_test_hook.sh index 567dd1f7d..13421d423 100755 --- a/post_test_hook.sh +++ b/post_test_hook.sh @@ -14,7 +14,11 @@ cat /etc/openstack/clouds.yaml cd ${DIR} echo '=functional==============================================' -tox -e functional +if [[ -n "$1" ]]; then + tox -e functional -- $1 +else + tox -e functional +fi FUNCTIONAL_RESULT=\$? echo '=examples================================================' tox -e examples From 3b2c2957e20c1ff8521b410a33d0229291ebad13 Mon Sep 17 00:00:00 2001 From: liyi Date: Wed, 19 Jul 2017 16:52:59 +0800 Subject: [PATCH 1698/3836] Add config param for cluster object Change-Id: I5d36bcba12fa4fdbcb8d4474b0e0351adfb1b304 --- openstack/cluster/v1/cluster.py | 2 ++ openstack/tests/unit/cluster/v1/test_cluster.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/openstack/cluster/v1/cluster.py b/openstack/cluster/v1/cluster.py index 988c77d2c..a1d44ac7d 100644 --- a/openstack/cluster/v1/cluster.py +++ b/openstack/cluster/v1/cluster.py @@ -66,6 +66,8 @@ class Cluster(resource.Resource): status = resource.Body('status') #: A string describing the reason why the cluster in current status. status_reason = resource.Body('status_reason') + #: A dictionary configuration for cluster. + config = resource.Body('config', type=dict) #: A collection of key-value pairs that are attached to the cluster. metadata = resource.Body('metadata', type=dict) #: A dictionary with some runtime data associated with the cluster. diff --git a/openstack/tests/unit/cluster/v1/test_cluster.py b/openstack/tests/unit/cluster/v1/test_cluster.py index 9ffb2bc80..5c1de631f 100644 --- a/openstack/tests/unit/cluster/v1/test_cluster.py +++ b/openstack/tests/unit/cluster/v1/test_cluster.py @@ -21,6 +21,7 @@ FAKE = { 'id': 'IDENTIFIER', + 'config': {'key1': 'value1', 'key2': 'value2'}, 'desired_capacity': 1, 'max_size': 3, 'min_size': 0, @@ -93,6 +94,7 @@ def test_instantiate(self): self.assertEqual(FAKE['max_size'], sot.max_size) self.assertEqual(FAKE['desired_capacity'], sot.desired_capacity) + self.assertEqual(FAKE['config'], sot.config) self.assertEqual(FAKE['timeout'], sot.timeout) self.assertEqual(FAKE['metadata'], sot.metadata) From bf5cd53cbf79f35d4a08987bfc432e9a56489a01 Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Sat, 15 Jul 2017 22:37:38 +0000 Subject: [PATCH 1699/3836] Don't remove top-container element in the adapter Change-Id: I0b375b5c6f3575216cbecc59285a6980fbe14314 Signed-off-by: Rosario Di Somma --- shade/_adapter.py | 53 +---------- shade/openstackcloud.py | 198 ++++++++++++++++++++++------------------ shade/operatorcloud.py | 21 +++-- 3 files changed, 124 insertions(+), 148 deletions(-) diff --git a/shade/_adapter.py b/shade/_adapter.py index 49a5d0360..6293c2533 100644 --- a/shade/_adapter.py +++ b/shade/_adapter.py @@ -20,7 +20,6 @@ from shade import _log from shade import exc -from shade import meta from shade import task_manager @@ -124,58 +123,10 @@ def _munch_response(self, response, result_key=None, error_message=None): try: result_json = response.json() + self._log_request_id(response, result_json) except Exception: return self._log_request_id(response) - - # Note(rods): this is just a temporary step needed until we - # don't update all the other REST API calls - if isinstance(result_json, dict): - for key in ['volumes', 'volume', 'volumeAttachment', 'backups', - 'volume_types', 'volume_type_access', 'snapshots', - 'network', 'networks', 'subnet', 'subnets', - 'router', 'routers', 'floatingip', 'floatingips', - 'floating_ip', 'floating_ips', 'port', 'ports', - 'rule_types', 'policy', 'policies', - 'bandwidth_limit_rule', 'bandwidth_limit_rules', - 'stack', 'stacks', 'zones', 'events', - 'security_group', 'security_groups', - 'security_group_rule', 'security_group_rules', - 'users', 'user', 'projects', 'tenants', - 'project', 'tenant', 'servers', 'server', - 'flavor', 'flavors', 'baymodels', 'aggregate', - 'aggregates', 'availabilityZoneInfo', - 'flavor_access', 'output', 'server_groups', 'domain', - 'domains', 'service', 'OS-KSADM:service']: - if key in result_json.keys(): - self._log_request_id(response) - return result_json - - if isinstance(result_json, list): - self._log_request_id(response) - return meta.obj_list_to_munch(result_json) - - result = None - if isinstance(result_json, dict): - # Wrap the keys() call in list() because in python3 keys returns - # a "dict_keys" iterator-like object rather than a list - json_keys = list(result_json.keys()) - if len(json_keys) > 1 and result_key: - result = result_json[result_key] - elif len(json_keys) == 1: - result = result_json[json_keys[0]] - if result is None: - # Passthrough the whole body - sometimes (hi glance) things - # come through without a top-level container. Also, sometimes - # you need to deal with pagination - result = result_json - - self._log_request_id(response, result) - - if isinstance(result, list): - return meta.obj_list_to_munch(result) - elif isinstance(result, dict): - return meta.obj_to_munch(result) - return result + return result_json def request( self, url, method, run_async=False, error_message=None, diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index a947a933a..6fe69de18 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -809,6 +809,16 @@ def range_search(self, data, filters): return filtered + def _get_and_munchify(self, key, data): + """Wrapper around meta.get_and_munchify. + + Some of the methods expect a `meta` attribute to be passed in as + part of the method signature. In this methods the meta param is + overriding the meta module making the call to meta.get_and_munchify + to fail. + """ + return meta.get_and_munchify(key, data) + @_utils.cache_on_arguments() def list_projects(self, domain_id=None, name_or_id=None, filters=None): """List projects. @@ -841,7 +851,7 @@ def list_projects(self, domain_id=None, name_or_id=None, filters=None): data = self._identity_client.get( '/{endpoint}'.format(endpoint=key), params=pushdown) projects = self._normalize_projects( - meta.get_and_munchify(key, data)) + self._get_and_munchify(key, data)) except Exception as e: self.log.debug("Failed to list projects", exc_info=True) raise OpenStackCloudException(str(e)) @@ -890,11 +900,11 @@ def update_project(self, name_or_id, enabled=None, domain_id=None, if self.cloud_config.get_api_version('identity') == '3': data = self._identity_client.patch( '/projects/' + proj['id'], json={'project': kwargs}) - project = meta.get_and_munchify('project', data) + project = self._get_and_munchify('project', data) else: data = self._identity_client.post( '/tenants/' + proj['id'], json={'tenant': kwargs}) - project = meta.get_and_munchify('tenant', data) + project = self._get_and_munchify('tenant', data) project = self._normalize_project(project) self.list_projects.invalidate(self) return project @@ -915,7 +925,7 @@ def create_project( '/{endpoint}'.format(endpoint=endpoint), json={key: project_ref}) project = self._normalize_project( - meta.get_and_munchify(key, data)) + self._get_and_munchify(key, data)) self.list_projects.invalidate(self) return project @@ -959,7 +969,7 @@ def list_users(self): """ data = self._identity_client.get('/users') return _utils.normalize_users( - meta.get_and_munchify('users', data)) + self._get_and_munchify('users', data)) def search_users(self, name_or_id=None, filters=None): """Search users. @@ -1007,7 +1017,7 @@ def get_user_by_id(self, user_id, normalize=True): error_message="Error getting user with ID {user_id}".format( user_id=user_id)) - user = meta.get_and_munchify('user', data) + user = self._get_and_munchify('user', data) if user and normalize: user = _utils.normalize_users(user) return user @@ -1064,7 +1074,7 @@ def create_user( error_msg = "Error in creating user {user}".format(user=name) data = self._identity_client.post('/users', json={'user': params}, error_message=error_msg) - user = meta.get_and_munchify('user', data) + user = self._get_and_munchify('user', data) self.list_users.invalidate(self) return _utils.normalize_users([user])[0] @@ -1409,12 +1419,11 @@ def has_service(self, service_key): @_utils.cache_on_arguments() def _nova_extensions(self): extensions = set() - - for extension in self._compute_client.get( - '/extensions', - error_message="Error fetching extension list for nova"): + data = self._compute_client.get( + '/extensions', + error_message="Error fetching extension list for nova") + for extension in self._get_and_munchify('extensions', data): extensions.add(extension['alias']) - return extensions def _has_nova_extension(self, extension_name): @@ -1427,12 +1436,11 @@ def search_keypairs(self, name_or_id=None, filters=None): @_utils.cache_on_arguments() def _neutron_extensions(self): extensions = set() - - for extension in self._network_client.get( - '/extensions.json', - error_message="Error fetching extension list for neutron"): + data = self._network_client.get( + '/extensions.json', + error_message="Error fetching extension list for neutron") + for extension in self._get_and_munchify('extensions', data): extensions.add(extension['alias']) - return extensions def _has_neutron_extension(self, extension_alias): @@ -1615,10 +1623,11 @@ def list_keypairs(self): :returns: A list of ``munch.Munch`` containing keypair info. """ + data = self._compute_client.get( + '/os-keypairs', + error_message="Error fetching keypair list") return self._normalize_keypairs([ - k['keypair'] for k in self._compute_client.get( - '/os-keypairs', - error_message="Error fetching keypair list")]) + k['keypair'] for k in self._get_and_munchify('keypairs', data)]) def list_networks(self, filters=None): """List all available networks. @@ -1631,7 +1640,7 @@ def list_networks(self, filters=None): if not filters: filters = {} data = self._network_client.get("/networks.json", params=filters) - return meta.get_and_munchify('networks', data) + return self._get_and_munchify('networks', data) def list_routers(self, filters=None): """List all available routers. @@ -1646,7 +1655,7 @@ def list_routers(self, filters=None): data = self._network_client.get( "/routers.json", params=filters, error_message="Error fetching router list") - return meta.get_and_munchify('routers', data) + return self._get_and_munchify('routers', data) def list_subnets(self, filters=None): """List all available subnets. @@ -1659,7 +1668,7 @@ def list_subnets(self, filters=None): if not filters: filters = {} data = self._network_client.get("/subnets.json", params=filters) - return meta.get_and_munchify('subnets', data) + return self._get_and_munchify('subnets', data) def list_ports(self, filters=None): """List all available ports. @@ -1695,7 +1704,7 @@ def _list_ports(self, filters): data = self._network_client.get( "/ports.json", params=filters, error_message="Error fetching port list") - return meta.get_and_munchify('ports', data) + return self._get_and_munchify('ports', data) def list_qos_rule_types(self, filters=None): """List all available QoS rule types. @@ -1714,7 +1723,7 @@ def list_qos_rule_types(self, filters=None): data = self._network_client.get( "/qos/rule-types.json", params=filters, error_message="Error fetching QoS rule types list") - return meta.get_and_munchify('rule_types', data) + return self._get_and_munchify('rule_types', data) def list_qos_policies(self, filters=None): """List all available QoS policies. @@ -1732,7 +1741,7 @@ def list_qos_policies(self, filters=None): data = self._network_client.get( "/qos/policies.json", params=filters, error_message="Error fetching QoS policies list") - return meta.get_and_munchify('policies', data) + return self._get_and_munchify('policies', data) @_utils.cache_on_arguments(should_cache_fn=_no_pending_volumes) def list_volumes(self, cache=True): @@ -1788,7 +1797,7 @@ def _list(data): # list volumes didn't complete succesfully so just return what # we found return self._normalize_volumes( - meta.get_and_munchify(key=None, data=volumes)) + self._get_and_munchify(key=None, data=volumes)) @_utils.cache_on_arguments() def list_volume_types(self, get_extra=True): @@ -1802,7 +1811,7 @@ def list_volume_types(self, get_extra=True): params=dict(is_public='None'), error_message='Error fetching volume_type list') return self._normalize_volume_types( - meta.get_and_munchify('volume_types', data)) + self._get_and_munchify('volume_types', data)) @_utils.cache_on_arguments() def list_availability_zone_names(self, unavailable=False): @@ -1821,7 +1830,7 @@ def list_availability_zone_names(self, unavailable=False): "Availability zone list could not be fetched", exc_info=True) return [] - zones = meta.get_and_munchify('availabilityZoneInfo', data) + zones = self._get_and_munchify('availabilityZoneInfo', data) ret = [] for zone in zones: if zone['zoneState']['available'] or unavailable: @@ -1845,17 +1854,18 @@ def list_flavors(self, get_extra=None): '/flavors/detail', params=dict(is_public='None'), error_message="Error fetching flavor list") flavors = self._normalize_flavors( - meta.get_and_munchify('flavors', data)) + self._get_and_munchify('flavors', data)) for flavor in flavors: if not flavor.extra_specs and get_extra: endpoint = "/flavors/{id}/os-extra_specs".format( id=flavor.id) try: - extra_specs = self._compute_client.get( + data = self._compute_client.get( endpoint, error_message="Error fetching flavor extra specs") - flavor.extra_specs = extra_specs + flavor.extra_specs = self._get_and_munchify( + 'extra_specs', data) except OpenStackCloudHTTPError as e: flavor.extra_specs = {} self.log.debug( @@ -1876,7 +1886,7 @@ def list_stacks(self): data = self._orchestration_client.get( '/stacks', error_message="Error fetching stack list") return self._normalize_stacks( - meta.get_and_munchify('stacks', data)) + self._get_and_munchify('stacks', data)) def list_server_security_groups(self, server): """List all security groups associated with the given server. @@ -1892,7 +1902,7 @@ def list_server_security_groups(self, server): '/servers/{server_id}/os-security-groups'.format( server_id=server['id'])) return self._normalize_secgroups( - meta.get_and_munchify('security_groups', data)) + self._get_and_munchify('security_groups', data)) def _get_server_security_groups(self, server, security_groups): if not self._has_secgroups(): @@ -2010,14 +2020,14 @@ def list_security_groups(self, filters=None): data = self._network_client.get( '/security-groups.json', params=filters, error_message="Error fetching security group list") - return meta.get_and_munchify('security_groups', data) + return self._get_and_munchify('security_groups', data) # Handle nova security groups else: data = self._compute_client.get( '/os-security-groups', params=filters) return self._normalize_secgroups( - meta.get_and_munchify('security_groups', data)) + self._get_and_munchify('security_groups', data)) def list_servers(self, detailed=False, all_projects=False, bare=False): """List all available servers. @@ -2066,7 +2076,7 @@ def _list_servers(self, detailed=False, all_projects=False, bare=False): data = self._compute_client.get( '/servers/detail', params=params, error_message=error_msg) servers = self._normalize_servers( - meta.get_and_munchify('servers', data)) + self._get_and_munchify('servers', data)) return [ self._expand_server(server, detailed, bare) for server in servers @@ -2081,7 +2091,7 @@ def list_server_groups(self): data = self._compute_client.get( '/os-server-groups', error_message="Error fetching server group list") - return meta.get_and_munchify('server_groups', data) + return self._get_and_munchify('server_groups', data) def get_compute_limits(self, name_or_id=None): """ Get compute limits for a project @@ -2105,8 +2115,8 @@ def get_compute_limits(self, name_or_id=None): error_msg = "{msg} for the project: {project} ".format( msg=error_msg, project=name_or_id) - limits = self._compute_client.get('/limits', params=params) - + data = self._compute_client.get('/limits', params=params) + limits = self._get_and_munchify('limits', data) return self._normalize_compute_limits(limits, project_id=project_id) @_utils.cache_on_arguments(should_cache_fn=_no_pending_images) @@ -2175,9 +2185,10 @@ def list_floating_ip_pools(self): raise OpenStackCloudUnavailableExtension( 'Floating IP pools extension is not available on target cloud') - pools = self._compute_client.get( + data = self._compute_client.get( 'os-floating-ip-pools', error_message="Error fetching floating IP pool list") + pools = self._get_and_munchify('floating_ip_pools', data) return [{'name': p['name']} for p in pools] def _list_floating_ips(self, filters=None): @@ -2260,14 +2271,14 @@ def _neutron_list_floating_ips(self, filters=None): if not filters: filters = {} data = self._network_client.get('/floatingips.json', params=filters) - return meta.get_and_munchify('floatingips', data) + return self._get_and_munchify('floatingips', data) def _nova_list_floating_ips(self): try: data = self._compute_client.get('/os-floating-ips') except OpenStackCloudURINotFound: return [] - return meta.get_and_munchify('floating_ips', data) + return self._get_and_munchify('floating_ips', data) def use_external_network(self): return self._use_external_network @@ -2853,7 +2864,7 @@ def get_server_console(self, server, length=None): data = self._compute_client.post( '/servers/{server}/action'.format(server=server['id']), json={'os-getConsoleOutput': {'length': length}}) - return meta.get_and_munchify('output', data) + return self._get_and_munchify('output', data) except OpenStackCloudBadRequest: return "" @@ -2902,7 +2913,7 @@ def _expand_server(self, server, detailed, bare): def get_server_by_id(self, id): data = self._compute_client.get('/servers/{id}'.format(id=id)) - server = meta.get_and_munchify('server', data) + server = self._get_and_munchify('server', data) return meta.add_server_interfaces(self, self._normalize_server(server)) def get_server_group(self, name_or_id=None, filters=None): @@ -3048,7 +3059,7 @@ def _search_one_stack(name_or_id=None, filters=None): data = self._orchestration_client.get( '/stacks/{name_or_id}'.format(name_or_id=name_or_id), error_message="Error fetching stack") - stack = meta.get_and_munchify('stack', data) + stack = self._get_and_munchify('stack', data) # Treat DELETE_COMPLETE stacks as a NotFound if stack['stack_status'] == 'DELETE_COMPLETE': return [] @@ -3073,10 +3084,12 @@ def create_keypair(self, name, public_key=None): } if public_key: keypair['public_key'] = public_key - return self._normalize_keypair(self._compute_client.post( + data = self._compute_client.post( '/os-keypairs', json={'keypair': keypair}, - error_message="Unable to create keypair {name}".format(name=name))) + error_message="Unable to create keypair {name}".format(name=name)) + return self._normalize_keypair( + self._get_and_munchify('keypair', data)) def delete_keypair(self, name): """Delete a keypair. @@ -3145,7 +3158,7 @@ def create_network(self, name, shared=False, admin_state_up=True, # Reset cache so the new network is picked up self._reset_network_caches() - return meta.get_and_munchify('network', data) + return self._get_and_munchify('network', data) def delete_network(self, name_or_id): """Delete a network. @@ -3204,7 +3217,7 @@ def create_qos_policy(self, name=None, description=None, shared=None, data = self._network_client.post("/qos/policies.json", json={'policy': policy}) - return meta.get_and_munchify('policy', data) + return self._get_and_munchify('policy', data) def update_qos_policy(self, name_or_id, policy_name=None, description=None, shared=None, default=None): @@ -3254,7 +3267,7 @@ def update_qos_policy(self, name_or_id, policy_name=None, "/qos/policies/{policy_id}.json".format( policy_id=curr_policy['id']), json={'policy': policy}) - return meta.get_and_munchify('policy', data) + return self._get_and_munchify('policy', data) def delete_qos_policy(self, name_or_id): """Delete a QoS policy. @@ -3328,7 +3341,7 @@ def list_qos_bandwidth_limit_rules(self, policy_name_or_id, filters=None): params=filters, error_message="Error fetching QoS bandwith limit rules from " "{policy}".format(policy=policy['id'])) - return meta.get_and_munchify('bandwidth_limit_rules', data) + return self._get_and_munchify('bandwidth_limit_rules', data) def get_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): """Get a QoS bandwidth limit rule by name or ID. @@ -3357,7 +3370,7 @@ def get_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): error_message="Error fetching QoS bandwith limit rule {rule_id} " "from {policy}".format(rule_id=rule_id, policy=policy['id'])) - return meta.get_and_munchify('bandwidth_limit_rule', data) + return self._get_and_munchify('bandwidth_limit_rule', data) def create_qos_bandwidth_limit_rule(self, policy_name_or_id, max_kbps=None, max_burst_kbps=None, direction=None): @@ -3401,7 +3414,7 @@ def create_qos_bandwidth_limit_rule(self, policy_name_or_id, max_kbps=None, "/qos/policies/{policy_id}/bandwidth_limit_rules".format( policy_id=policy['id']), json={'bandwidth_limit_rule': rule}) - return meta.get_and_munchify('bandwidth_limit_rule', data) + return self._get_and_munchify('bandwidth_limit_rule', data) def update_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id, max_kbps=None, max_burst_kbps=None, @@ -3458,7 +3471,7 @@ def update_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id, "/qos/policies/{policy_id}/bandwidth_limit_rules/{rule_id}.json". format(policy_id=policy['id'], rule_id=rule_id), json={'bandwidth_limit_rule': rule}) - return meta.get_and_munchify('bandwidth_limit_rule', data) + return self._get_and_munchify('bandwidth_limit_rule', data) def delete_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): """Delete a QoS bandwidth limit rule. @@ -3650,7 +3663,7 @@ def create_router(self, name=None, admin_state_up=True, data = self._network_client.post( "/routers.json", json={"router": router}, error_message="Error creating router {0}".format(name)) - return meta.get_and_munchify('router', data) + return self._get_and_munchify('router', data) def update_router(self, name_or_id, name=None, admin_state_up=None, ext_gateway_net_id=None, enable_snat=None, @@ -3701,7 +3714,7 @@ def update_router(self, name_or_id, name=None, admin_state_up=None, "/routers/{router_id}.json".format(router_id=curr_router['id']), json={"router": router}, error_message="Error updating router {0}".format(name_or_id)) - return meta.get_and_munchify('router', data) + return self._get_and_munchify('router', data) def delete_router(self, name_or_id): """Delete a logical router. @@ -4068,7 +4081,7 @@ def _make_v2_image_params(self, meta, properties): def _upload_image_from_volume( self, name, volume_id, allow_duplicates, container_format, disk_format, wait, timeout): - response = self._volume_client.post( + data = self._volume_client.post( '/volumes/{id}/action'.format(id=volume_id), json={ 'os-volume_upload_image': { @@ -4076,6 +4089,8 @@ def _upload_image_from_volume( 'image_name': name, 'container_format': container_format, 'disk_format': disk_format}}) + response = self._get_and_munchify('os-volume_upload_image', data) + if not wait: return self.get_image(response['image_id']) try: @@ -4092,13 +4107,13 @@ def _upload_image_from_volume( raise def _upload_image_put_v2(self, name, image_data, meta, **image_kwargs): - properties = image_kwargs.pop('properties', {}) image_kwargs.update(self._make_v2_image_params(meta, properties)) image_kwargs['name'] = name - image = self._image_client.post('/images', json=image_kwargs) + data = self._image_client.post('/images', json=image_kwargs) + image = self._get_and_munchify(key=None, data=data) try: self._image_client.put( @@ -4201,7 +4216,8 @@ def _upload_image_task( import_from='{container}/{name}'.format( container=container, name=name), image_properties=dict(name=name))) - glance_task = self._image_client.post('/tasks', json=task_args) + data = self._image_client.post('/tasks', json=task_args) + glance_task = self._get_and_munchify(key=None, data=data) self.list_images.invalidate(self) if wait: start = time.time() @@ -4211,8 +4227,9 @@ def _upload_image_task( "Timeout waiting for the image to import."): try: if image_id is None: - status = self._image_client.get( + data = self._image_client.get( '/tasks/{id}'.format(id=glance_task.id)) + status = self._get_and_munchify('images', data=data) except OpenStackCloudHTTPError as e: if e.response.status_code == 503: # Clear the exception so that it doesn't linger @@ -4348,7 +4365,7 @@ def create_volume( '/volumes', json=dict(payload), error_message='Error in creating volume') - volume = meta.get_and_munchify('volume', data) + volume = self._get_and_munchify('volume', data) self.list_volumes.invalidate(self) if volume['status'] == 'error': @@ -4558,7 +4575,7 @@ def attach_volume(self, server, volume, device=None, "Error in attaching volume %s" % volume['id'] ) return self._normalize_volume_attachment( - meta.get_and_munchify('volumeAttachment', data)) + self._get_and_munchify('volumeAttachment', data)) def _get_volume_kwargs(self, kwargs): name = kwargs.pop('name', kwargs.pop('display_name', None)) @@ -4602,12 +4619,12 @@ def create_volume_snapshot(self, volume_id, force=False, kwargs = self._get_volume_kwargs(kwargs) payload = {'volume_id': volume_id, 'force': force} payload.update(kwargs) - snapshot = self._volume_client.post( + data = self._volume_client.post( '/snapshots', json=dict(snapshot=payload), error_message="Error creating snapshot of volume " "{volume_id}".format(volume_id=volume_id)) - + snapshot = self._get_and_munchify('snapshot', data) if wait: snapshot_id = snapshot['id'] for count in _utils._iterate_timeout( @@ -4637,11 +4654,12 @@ def get_volume_snapshot_by_id(self, snapshot_id): param: snapshot_id: ID of the volume snapshot. """ - snapshot = self._volume_client.get( + data = self._volume_client.get( '/snapshots/{snapshot_id}'.format(snapshot_id=snapshot_id), error_message="Error getting snapshot " "{snapshot_id}".format(snapshot_id=snapshot_id)) - return self._normalize_volume(snapshot) + return self._normalize_volume( + self._get_and_munchify('snapshot', data)) def get_volume_snapshot(self, name_or_id, filters=None): """Get a volume by name or ID. @@ -4695,10 +4713,11 @@ def create_volume_backup(self, volume_id, name=None, description=None, 'force': force, } - backup = self._volume_client.post( + data = self._volume_client.post( '/backups', json=dict(backup=payload), error_message="Error creating backup of volume " "{volume_id}".format(volume_id=volume_id)) + backup = self._get_and_munchify('backup', data) if wait: backup_id = backup['id'] @@ -4737,7 +4756,7 @@ def list_volume_snapshots(self, detailed=True, search_opts=None): endpoint, params=search_opts, error_message="Error getting a list of snapshots") - return meta.get_and_munchify('snapshots', data) + return self._get_and_munchify('snapshots', data) def list_volume_backups(self, detailed=True, search_opts=None): """ @@ -4760,7 +4779,7 @@ def list_volume_backups(self, detailed=True, search_opts=None): data = self._volume_client.get( endpoint, params=search_opts, error_message="Error getting a list of backups") - return meta.get_and_munchify('backups', data) + return self._get_and_munchify('backups', data) def delete_volume_backup(self, name_or_id=None, force=False, wait=False, timeout=None): @@ -5084,7 +5103,7 @@ def _submit_create_fip(self, kwargs): data = self._network_client.post( "/floatingips.json", json={"floatingip": kwargs}) return self._normalize_floating_ip( - meta.get_and_munchify('floatingip', data)) + self._get_and_munchify('floatingip', data)) def _neutron_create_floating_ip( self, network_name_or_id=None, server=None, @@ -5173,11 +5192,11 @@ def _nova_create_floating_ip(self, pool=None): data = self._compute_client.post( '/os-floating-ips', json=dict(pool=pool)) - pool_ip = meta.get_and_munchify('floating_ip', data) + pool_ip = self._get_and_munchify('floating_ip', data) # TODO(mordred) Remove this - it's just for compat data = self._compute_client.get('/os-floating-ips/{id}'.format( id=pool_ip['id'])) - return meta.get_and_munchify('floating_ip', data) + return self._get_and_munchify('floating_ip', data) def delete_floating_ip(self, floating_ip_id, retry=1): """Deallocate a floating IP from a project. @@ -6089,7 +6108,7 @@ def create_server( with _utils.shade_exceptions("Error in creating instance"): data = self._compute_client.post( endpoint, json={'server': kwargs}) - server = meta.get_and_munchify('server', data) + server = self._get_and_munchify('server', data) admin_pass = server.get('adminPass') or kwargs.get('admin_pass') if not wait: # This is a direct get call to skip the list_servers @@ -6211,7 +6230,7 @@ def rebuild_server(self, server_id, image_id, admin_pass=None, '/servers/{server_id}/action'.format(server_id=server_id), error_message="Error in rebuilding instance", json={'rebuild': kwargs}) - server = meta.get_and_munchify('server', data) + server = self._get_and_munchify('server', data) if not wait: return self._expand_server( self._normalize_server(server), bare=bare, detailed=detailed) @@ -6421,7 +6440,7 @@ def update_server(self, name_or_id, detailed=False, bare=False, **kwargs): error_message="Error updating server {0}".format(name_or_id), json={'server': kwargs}) server = self._normalize_server( - meta.get_and_munchify('server', data)) + self._get_and_munchify('server', data)) return self._expand_server(server, bare=bare, detailed=detailed) def create_server_group(self, name, policies): @@ -6434,7 +6453,7 @@ def create_server_group(self, name, policies): :raises: OpenStackCloudException on operation error. """ - return self._compute_client.post( + data = self._compute_client.post( '/os-server-groups', json={ 'server_group': { @@ -6442,6 +6461,7 @@ def create_server_group(self, name, policies): 'policies': policies}}, error_message="Unable to create server group {name}".format( name=name)) + return self._get_and_munchify('server_group', data) def delete_server_group(self, name_or_id): """Delete a server group. @@ -7083,7 +7103,7 @@ def create_subnet(self, network_name_or_id, cidr=None, ip_version=4, data = self._network_client.post("/subnets.json", json={"subnet": subnet}) - return meta.get_and_munchify('subnet', data) + return self._get_and_munchify('subnet', data) def delete_subnet(self, name_or_id): """Delete a subnet. @@ -7192,7 +7212,7 @@ def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, data = self._network_client.put( "/subnets/{subnet_id}.json".format(subnet_id=curr_subnet['id']), json={"subnet": subnet}) - return meta.get_and_munchify('subnet', data) + return self._get_and_munchify('subnet', data) @_utils.valid_kwargs('name', 'admin_state_up', 'mac_address', 'fixed_ips', 'subnet_id', 'ip_address', 'security_groups', @@ -7257,7 +7277,7 @@ def create_port(self, network_id, **kwargs): "/ports.json", json={'port': kwargs}, error_message="Error creating port for network {0}".format( network_id)) - return meta.get_and_munchify('port', data) + return self._get_and_munchify('port', data) @_utils.valid_kwargs('name', 'admin_state_up', 'fixed_ips', 'security_groups', 'allowed_address_pairs', @@ -7321,7 +7341,7 @@ def update_port(self, name_or_id, **kwargs): "/ports/{port_id}.json".format(port_id=port['id']), json={"port": kwargs}, error_message="Error updating port {0}".format(name_or_id)) - return meta.get_and_munchify('port', data) + return self._get_and_munchify('port', data) def delete_port(self, name_or_id): """Delete a port @@ -7380,7 +7400,7 @@ def create_security_group(self, name, description, project_id=None): data = self._compute_client.post( '/os-security-groups', json=security_group_json) return self._normalize_secgroup( - meta.get_and_munchify('security_group', data)) + self._get_and_munchify('security_group', data)) def delete_security_group(self, name_or_id): """Delete a security group @@ -7457,7 +7477,7 @@ def update_security_group(self, name_or_id, **kwargs): '/os-security-groups/{id}'.format(id=group['id']), json={'security-group': kwargs}) return self._normalize_secgroup( - meta.get_and_munchify('security_group', data)) + self._get_and_munchify('security_group', data)) def create_security_group_rule(self, secgroup_name_or_id, @@ -7591,7 +7611,7 @@ def create_security_group_rule(self, '/os-security-group-rules', json=security_group_rule_dict ) return self._normalize_secgroup_rule( - meta.get_and_munchify('security_group_rule', data)) + self._get_and_munchify('security_group_rule', data)) def delete_security_group_rule(self, rule_id): """Delete a security group rule @@ -7634,7 +7654,7 @@ def list_zones(self): data = self._dns_client.get( "/zones", error_message="Error fetching zones list") - return meta.get_and_munchify('zones', data) + return self._get_and_munchify('zones', data) def get_zone(self, name_or_id, filters=None): """Get a zone by name or ID. @@ -7699,7 +7719,7 @@ def create_zone(self, name, zone_type=None, email=None, description=None, data = self._dns_client.post( "/zones", json=zone, error_message="Unable to create zone {name}".format(name=name)) - return meta.get_and_munchify(key=None, data=data) + return self._get_and_munchify(key=None, data=data) @_utils.valid_kwargs('email', 'description', 'ttl', 'masters') def update_zone(self, name_or_id, **kwargs): @@ -7725,7 +7745,7 @@ def update_zone(self, name_or_id, **kwargs): data = self._dns_client.patch( "/zones/{zone_id}".format(zone_id=zone['id']), json=kwargs, error_message="Error updating zone {0}".format(name_or_id)) - return meta.get_and_munchify(key=None, data=data) + return self._get_and_munchify(key=None, data=data) def delete_zone(self, name_or_id): """Delete a zone. @@ -7897,7 +7917,7 @@ def list_cluster_templates(self, detail=False): data = self._container_infra_client.get( '/baymodels/detail') return self._normalize_cluster_templates( - meta.get_and_munchify('baymodels', data)) + self._get_and_munchify('baymodels', data)) list_baymodels = list_cluster_templates def search_cluster_templates( diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index a33575e8a..4cd7227e1 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1817,9 +1817,10 @@ def list_hypervisors(self): :returns: A list of hypervisor ``munch.Munch``. """ - return self._compute_client.get( + data = self._compute_client.get( '/os-hypervisors/detail', error_message="Error fetching hypervisor list") + return meta.get_and_munchify('hypervisors', data) def search_aggregates(self, name_or_id=None, filters=None): """Seach host aggregates. @@ -2101,8 +2102,9 @@ def get_compute_quotas(self, name_or_id): proj = self.get_project(name_or_id) if not proj: raise OpenStackCloudException("project does not exist") - return self._compute_client.get( + data = self._compute_client.get( '/os-quota-sets/{project}'.format(project=proj.id)) + return meta.get_and_munchify('quota_set', data) def delete_compute_quotas(self, name_or_id): """ Delete quota for a project @@ -2174,13 +2176,13 @@ def parse_datetime_for_nova(date): raise OpenStackCloudException("project does not exist: {}".format( name=proj.id)) - usage = self._compute_client.get( + data = self._compute_client.get( '/os-simple-tenant-usage/{project}'.format(project=proj.id), params=dict(start=start.isoformat(), end=end.isoformat()), error_message="Unable to get usage for project: {name}".format( name=proj.id)) - - return self._normalize_compute_usage(usage) + return self._normalize_compute_usage( + meta.get_and_munchify('tenant_usage', data)) def set_volume_quotas(self, name_or_id, **kwargs): """ Set a volume quota in a project @@ -2214,9 +2216,10 @@ def get_volume_quotas(self, name_or_id): if not proj: raise OpenStackCloudException("project does not exist") - return self._volume_client.get( + data = self._volume_client.get( '/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id), error_message="cinder client call failed") + return meta.get_and_munchify('quota_set', data) def delete_volume_quotas(self, name_or_id): """ Delete volume quotas for a project @@ -2266,10 +2269,11 @@ def get_network_quotas(self, name_or_id): proj = self.get_project(name_or_id) if not proj: raise OpenStackCloudException("project does not exist") - return self._network_client.get( + data = self._network_client.get( '/quotas/{project_id}.json'.format(project_id=proj.id), error_message=("Error fetching Neutron's quota for " "project {0}".format(proj.id))) + return meta.get_and_munchify('quota', data) def delete_network_quotas(self, name_or_id): """ Delete network quotas for a project @@ -2295,5 +2299,6 @@ def list_magnum_services(self): :raises: OpenStackCloudException on operation error. """ with _utils.shade_exceptions("Error fetching Magnum services list"): + data = self._container_infra_client.get('/mservices') return self._normalize_magnum_services( - self._container_infra_client.get('/mservices')) + meta.get_and_munchify('mservices', data)) From 67accb5b099eeab26d402d33073dd09bcedeb9b6 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 19 Jul 2017 17:59:42 +0000 Subject: [PATCH 1700/3836] Updated from global requirements Change-Id: I96229428c485ca9ea9af79c3b5d5dbb43840f169 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fbf6c0081..d7ba2b1a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,6 +22,6 @@ iso8601>=0.1.11 # MIT keystoneauth1>=2.21.0 # Apache-2.0 netifaces>=0.10.4 # MIT python-keystoneclient>=3.8.0 # Apache-2.0 -python-ironicclient>=1.11.0 # Apache-2.0 +python-ironicclient>=1.14.0 # Apache-2.0 dogpile.cache>=0.6.2 # BSD From 34a088ee5f4cd95270dc747f8a0ac00142b13ddc Mon Sep 17 00:00:00 2001 From: jonnary Date: Wed, 19 Jul 2017 23:24:57 +0800 Subject: [PATCH 1701/3836] Fix comment in services function A generator should be a complex number. Change-Id: I3c8f626b083ebc2ca5f9aa3bf0099e30283d0ddc --- openstack/cluster/v1/_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/cluster/v1/_proxy.py b/openstack/cluster/v1/_proxy.py index 29ef4b6a7..7bcff9f7e 100644 --- a/openstack/cluster/v1/_proxy.py +++ b/openstack/cluster/v1/_proxy.py @@ -1130,7 +1130,7 @@ def wait_for_delete(self, resource, interval=2, wait=120): wait) def services(self, **query): - """Get a generator of service. + """Get a generator of services. :returns: A generator of objects that are of type :class:`~openstack.cluster.v1.service.Service` From 9ca78367fd49763c20d2dbb92def2819f366fe80 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Wed, 19 Jul 2017 06:01:32 -0400 Subject: [PATCH 1702/3836] De-client-ify Group Create Change-Id: Id7674f3aacdfa1448c243fe0e3932dc5f303e9ba --- shade/_tasks.py | 5 ----- shade/operatorcloud.py | 30 +++++++++++++++--------------- shade/tests/unit/test_groups.py | 6 +----- 3 files changed, 16 insertions(+), 25 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index e197e8307..27b5847aa 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -167,11 +167,6 @@ def main(self, client): return client.keystone_client.groups.list() -class GroupCreate(task_manager.Task): - def main(self, client): - return client.keystone_client.groups.create(**self.args) - - class GroupDelete(task_manager.Task): def main(self, client): return client.keystone_client.groups.delete(**self.args) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 4cd7227e1..ad511e282 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1275,22 +1275,22 @@ def create_group(self, name, description, domain=None): :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ - with _utils.shade_exceptions( - "Error creating group {group}".format(group=name) - ): - domain_id = None - if domain: - dom = self.get_domain(domain) - if not dom: - raise OpenStackCloudException( - "Creating group {group} failed: Invalid domain " - "{domain}".format(group=name, domain=domain) - ) - domain_id = dom['id'] + group_ref = {'name': name} + if description: + group_ref['description'] = description + if domain: + dom = self.get_domain(domain) + if not dom: + raise OpenStackCloudException( + "Creating group {group} failed: Invalid domain " + "{domain}".format(group=name, domain=domain) + ) + group_ref['domain_id'] = dom['id'] - group = self.manager.submit_task(_tasks.GroupCreate( - name=name, description=description, domain=domain_id) - ) + error_msg = "Error creating group {group}".format(group=name) + data = self._identity_client.post( + '/groups', json={'group': group_ref}, error_message=error_msg) + group = self._get_and_munchify('group', data) self.list_groups.invalidate(self) return _utils.normalize_groups([group])[0] diff --git a/shade/tests/unit/test_groups.py b/shade/tests/unit/test_groups.py index 4f9dec447..ed7761fee 100644 --- a/shade/tests/unit/test_groups.py +++ b/shade/tests/unit/test_groups.py @@ -72,11 +72,7 @@ def test_create_group(self): uri=self.get_mock_url(), status_code=200, json=group_data.json_response, - validate=dict(json=group_data.json_request)), - dict(method='GET', - uri=self.get_mock_url(append=[group_data.group_id]), - status_code=200, - json=group_data.json_response) + validate=dict(json=group_data.json_request)) ]) self.op_cloud.create_group( name=group_data.group_name, description=group_data.description, From 57f5c896e088c7d69a13fb26d536c8fb391c39cd Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Wed, 19 Jul 2017 06:07:06 -0400 Subject: [PATCH 1703/3836] De-client-ify Group List Change-Id: If2b1f2aa064e33629307f457b94ba52b2dc02230 --- shade/_tasks.py | 5 ----- shade/operatorcloud.py | 6 +++--- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 27b5847aa..ce48df7b8 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -162,11 +162,6 @@ def main(self, client): return client.keystone_client.endpoints.delete(**self.args) -class GroupList(task_manager.Task): - def main(self, client): - return client.keystone_client.groups.list() - - class GroupDelete(task_manager.Task): def main(self, client): return client.keystone_client.groups.delete(**self.args) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index ad511e282..c45ac61ef 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1232,9 +1232,9 @@ def list_groups(self): :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ - with _utils.shade_exceptions("Failed to list groups"): - groups = self.manager.submit_task(_tasks.GroupList()) - return _utils.normalize_groups(groups) + data = self._identity_client.get( + '/groups', error_message="Failed to list groups") + return _utils.normalize_groups(self._get_and_munchify('groups', data)) def search_groups(self, name_or_id=None, filters=None): """Search Keystone groups. From b7f90dc661636c5ea50f4d18188d45e2d71953f6 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Wed, 19 Jul 2017 06:26:03 -0400 Subject: [PATCH 1704/3836] De-client-ify Group Update Change-Id: Iff584891a07a619b57a1700351a7fe5f19ca8a57 --- shade/_tasks.py | 5 ----- shade/operatorcloud.py | 15 ++++++++++----- shade/tests/unit/test_groups.py | 6 +----- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index ce48df7b8..28c026cbe 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -167,11 +167,6 @@ def main(self, client): return client.keystone_client.groups.delete(**self.args) -class GroupUpdate(task_manager.Task): - def main(self, client): - return client.keystone_client.groups.update(**self.args) - - class RoleList(task_manager.Task): def main(self, client): return client.keystone_client.roles.list() diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index c45ac61ef..a3bf44e00 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1312,12 +1312,17 @@ def update_group(self, name_or_id, name=None, description=None): "Group {0} not found for updating".format(name_or_id) ) - with _utils.shade_exceptions( - "Unable to update group {name}".format(name=name_or_id) - ): - group = self.manager.submit_task(_tasks.GroupUpdate( - group=group['id'], name=name, description=description)) + group_ref = {} + if name: + group_ref['name'] = name + if description: + group_ref['description'] = description + error_msg = "Unable to update group {name}".format(name=name_or_id) + data = self._identity_client.patch( + '/groups/{id}'.format(id=group['id']), + json={'group': group_ref}, error_message=error_msg) + group = self._get_and_munchify('group', data) self.list_groups.invalidate(self) return _utils.normalize_groups([group])[0] diff --git a/shade/tests/unit/test_groups.py b/shade/tests/unit/test_groups.py index ed7761fee..63db99723 100644 --- a/shade/tests/unit/test_groups.py +++ b/shade/tests/unit/test_groups.py @@ -91,11 +91,7 @@ def test_update_group(self): uri=self.get_mock_url(append=[group_data.group_id]), status_code=200, json=group_data.json_response, - validate=dict(json=group_data.json_request)), - dict(method='GET', - uri=self.get_mock_url(append=[group_data.group_id]), - status_code=200, - json=group_data.json_response) + validate=dict(json=group_data.json_request)) ]) self.op_cloud.update_group(group_data.group_id, group_data.group_name, group_data.description) From 9b2e01d962bb1cfb3717c943a564a8d9fef48419 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Wed, 19 Jul 2017 06:33:15 -0400 Subject: [PATCH 1705/3836] De-client-ify Group Delete Change-Id: I07785bcee5cd312293ba9a3eed9aec8820378332 --- shade/_tasks.py | 5 ----- shade/operatorcloud.py | 7 +++---- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 28c026cbe..e5d227565 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -162,11 +162,6 @@ def main(self, client): return client.keystone_client.endpoints.delete(**self.args) -class GroupDelete(task_manager.Task): - def main(self, client): - return client.keystone_client.groups.delete(**self.args) - - class RoleList(task_manager.Task): def main(self, client): return client.keystone_client.roles.list() diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index a3bf44e00..3b305915d 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1342,10 +1342,9 @@ def delete_group(self, name_or_id): "Group %s not found for deleting", name_or_id) return False - with _utils.shade_exceptions( - "Unable to delete group {name}".format(name=name_or_id) - ): - self.manager.submit_task(_tasks.GroupDelete(group=group['id'])) + error_msg = "Unable to delete group {name}".format(name=name_or_id) + self._identity_client.delete('/groups/{id}'.format(id=group['id']), + error_message=error_msg) self.list_groups.invalidate(self) return True From 8034894d81e05d8a790098c02805e9931f48718d Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Mon, 10 Jul 2017 13:28:00 -0400 Subject: [PATCH 1706/3836] De-client-ify Role Create Change-Id: I0b01b10907939cc93adfee9e53109893abfe62b1 --- shade/_tasks.py | 5 ----- shade/operatorcloud.py | 12 +++++++----- shade/tests/unit/test_identity_roles.py | 6 +----- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index e5d227565..890d46524 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -167,11 +167,6 @@ def main(self, client): return client.keystone_client.roles.list() -class RoleCreate(task_manager.Task): - def main(self, client): - return client.keystone_client.roles.create(**self.args) - - class RoleDelete(task_manager.Task): def main(self, client): return client.keystone_client.roles.delete(**self.args) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 3b305915d..d02a5cd63 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1628,11 +1628,13 @@ def create_role(self, name): :raise OpenStackCloudException: if the role cannot be created """ - with _utils.shade_exceptions(): - role = self.manager.submit_task( - _tasks.RoleCreate(name=name) - ) - return role + v2 = self.cloud_config.get_api_version('identity').startswith('2') + url = '/OS-KSADM/roles' if v2 else '/roles' + msg = 'Failed to create role {name}'.format(name=name) + data = self._identity_client.post( + url, json={'role': {'name': name}}, error_message=msg) + role = self._get_and_munchify('role', data) + return _utils.normalize_roles([role])[0] def delete_role(self, name_or_id): """Delete a Keystone role. diff --git a/shade/tests/unit/test_identity_roles.py b/shade/tests/unit/test_identity_roles.py index 05db5ba3f..b7e448fb4 100644 --- a/shade/tests/unit/test_identity_roles.py +++ b/shade/tests/unit/test_identity_roles.py @@ -91,11 +91,7 @@ def test_create_role(self): uri=self.get_mock_url(), status_code=200, json=role_data.json_response, - validate=dict(json=role_data.json_request)), - dict(method='GET', - uri=self.get_mock_url(append=[role_data.role_id]), - status_code=200, - json=role_data.json_response) + validate=dict(json=role_data.json_request)) ]) role = self.op_cloud.create_role(role_data.role_name) From 2d777b911f1e26ee2d7002040099c6769e177f84 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Mon, 10 Jul 2017 14:45:49 -0400 Subject: [PATCH 1707/3836] De-client-ify Role List Change-Id: I3e5a093f0d3b09b897afda7ed793a18a5732513a --- shade/_tasks.py | 5 ----- shade/operatorcloud.py | 9 +++++---- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 890d46524..ab9c07dcd 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -162,11 +162,6 @@ def main(self, client): return client.keystone_client.endpoints.delete(**self.args) -class RoleList(task_manager.Task): - def main(self, client): - return client.keystone_client.roles.list() - - class RoleDelete(task_manager.Task): def main(self, client): return client.keystone_client.roles.delete(**self.args) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index d02a5cd63..fdecd7d3b 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1357,10 +1357,11 @@ def list_roles(self): :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ - with _utils.shade_exceptions(): - roles = self.manager.submit_task(_tasks.RoleList()) - - return roles + v2 = self.cloud_config.get_api_version('identity').startswith('2') + url = '/OS-KSADM/roles' if v2 else '/roles' + data = self._identity_client.get( + url, error_message="Failed to list roles") + return _utils.normalize_roles(self._get_and_munchify('roles', data)) def search_roles(self, name_or_id=None, filters=None): """Seach Keystone roles. From e171aa4f6aeb34a830db1839ce11578b8adae379 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Wed, 19 Jul 2017 07:26:42 -0400 Subject: [PATCH 1708/3836] De-client-ify Role Delete Change-Id: I97c4e7a2cd9e6e3de9b5d03dc5b5476a9e595108 --- shade/_tasks.py | 5 ----- shade/operatorcloud.py | 8 +++++--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index ab9c07dcd..1573304b3 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -162,11 +162,6 @@ def main(self, client): return client.keystone_client.endpoints.delete(**self.args) -class RoleDelete(task_manager.Task): - def main(self, client): - return client.keystone_client.roles.delete(**self.args) - - class RoleAddUser(task_manager.Task): def main(self, client): return client.keystone_client.roles.add_user_role(**self.args) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index fdecd7d3b..0606e2e7a 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1653,9 +1653,11 @@ def delete_role(self, name_or_id): "Role %s not found for deleting", name_or_id) return False - with _utils.shade_exceptions("Unable to delete role {name}".format( - name=name_or_id)): - self.manager.submit_task(_tasks.RoleDelete(role=role['id'])) + v2 = self.cloud_config.get_api_version('identity').startswith('2') + url = '{preffix}/{id}'.format( + preffix='/OS-KSADM/roles' if v2 else '/roles', id=role['id']) + error_msg = "Unable to delete role {name}".format(name=name_or_id) + self._identity_client.delete(url, error_message=error_msg) return True From d9091fb8ad63210fbdf72084d2d4a98305a45a1d Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Thu, 20 Jul 2017 06:48:38 -0400 Subject: [PATCH 1709/3836] Consolidate the use of self._get_and_munchify Replace meta.get_and_munchify with self._get_and_munchify in operatorcloud.py Change-Id: I411a1d740ea96bd7a1ac9c3ae67d8d518bf619f7 --- shade/openstackcloud.py | 2 +- shade/operatorcloud.py | 38 +++++++++++++++++++------------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 6fe69de18..c85ac0c34 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -813,7 +813,7 @@ def _get_and_munchify(self, key, data): """Wrapper around meta.get_and_munchify. Some of the methods expect a `meta` attribute to be passed in as - part of the method signature. In this methods the meta param is + part of the method signature. In those methods the meta param is overriding the meta module making the call to meta.get_and_munchify to fail. """ diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 0606e2e7a..7cbb49c91 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -17,7 +17,6 @@ from ironicclient import exceptions as ironic_exceptions from shade.exc import * # noqa -from shade import meta from shade import openstackcloud from shade import _tasks from shade import _utils @@ -767,7 +766,7 @@ def create_service(self, name, enabled=True, **kwargs): msg = 'Failed to create service {name}'.format(name=name) data = self._identity_client.post( url, json={key: kwargs}, error_message=msg) - service = meta.get_and_munchify(key, data) + service = self._get_and_munchify(key, data) return _utils.normalize_keystone_services([service])[0] @_utils.valid_kwargs('name', 'enabled', 'type', 'service_type', @@ -1098,7 +1097,7 @@ def create_domain(self, name, description=None, enabled=True): msg = 'Failed to create domain {name}'.format(name=name) data = self._identity_client.post( '/domains', json={'domain': domain_ref}, error_message=msg) - domain = meta.get_and_munchify('domain', data) + domain = self._get_and_munchify('domain', data) return _utils.normalize_domains([domain])[0] def update_domain( @@ -1125,7 +1124,7 @@ def update_domain( data = self._identity_client.patch( '/domains/{id}'.format(id=domain_id), json={'domain': domain_ref}, error_message=error_msg) - domain = meta.get_and_munchify('domain', data) + domain = self._get_and_munchify('domain', data) return _utils.normalize_domains([domain])[0] def delete_domain(self, domain_id=None, name_or_id=None): @@ -1169,7 +1168,8 @@ def list_domains(self, **filters): """ data = self._identity_client.get( '/domains', params=filters, error_message="Failed to list domains") - return _utils.normalize_domains(meta.get_and_munchify('domains', data)) + domains = self._get_and_munchify('domains', data) + return _utils.normalize_domains(domains) def search_domains(self, filters=None, name_or_id=None): """Search Keystone domains. @@ -1220,7 +1220,7 @@ def get_domain(self, domain_id=None, name_or_id=None, filters=None): data = self._identity_client.get( '/domains/{id}'.format(id=domain_id), error_message=error_msg) - domain = meta.get_and_munchify('domain', data) + domain = self._get_and_munchify('domain', data) return _utils.normalize_domains([domain])[0] @_utils.cache_on_arguments() @@ -1510,7 +1510,7 @@ def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", json=dict(flavor=payload)) return self._normalize_flavor( - meta.get_and_munchify('flavor', data)) + self._get_and_munchify('flavor', data)) def delete_flavor(self, name_or_id): """Delete a flavor @@ -1618,7 +1618,7 @@ def list_flavor_access(self, flavor_id): data = self._compute_client.get( '/flavors/{id}/os-flavor-access'.format(id=flavor_id)) return _utils.normalize_flavor_accesses( - meta.get_and_munchify('flavor_access', data)) + self._get_and_munchify('flavor_access', data)) def create_role(self, name): """Create a Keystone role. @@ -1829,7 +1829,7 @@ def list_hypervisors(self): data = self._compute_client.get( '/os-hypervisors/detail', error_message="Error fetching hypervisor list") - return meta.get_and_munchify('hypervisors', data) + return self._get_and_munchify('hypervisors', data) def search_aggregates(self, name_or_id=None, filters=None): """Seach host aggregates. @@ -1854,7 +1854,7 @@ def list_aggregates(self): data = self._compute_client.get( '/os-aggregates', error_message="Error fetching aggregate list") - return meta.get_and_munchify('aggregates', data) + return self._get_and_munchify('aggregates', data) def get_aggregate(self, name_or_id, filters=None): """Get an aggregate by name or ID. @@ -1895,7 +1895,7 @@ def create_aggregate(self, name, availability_zone=None): }}, error_message="Unable to create host aggregate {name}".format( name=name)) - return meta.get_and_munchify('aggregate', data) + return self._get_and_munchify('aggregate', data) @_utils.valid_kwargs('name', 'availability_zone') def update_aggregate(self, name_or_id, **kwargs): @@ -1919,7 +1919,7 @@ def update_aggregate(self, name_or_id, **kwargs): json={'aggregate': kwargs}, error_message="Error updating aggregate {name}".format( name=name_or_id)) - return meta.get_and_munchify('aggregate', data) + return self._get_and_munchify('aggregate', data) def delete_aggregate(self, name_or_id): """Delete a host aggregate. @@ -1965,7 +1965,7 @@ def set_aggregate_metadata(self, name_or_id, metadata): '/os-aggregates/{id}/action'.format(id=aggregate['id']), json={'set_metadata': {'metadata': metadata}}, error_message=err_msg) - return meta.get_and_munchify('aggregate', data) + return self._get_and_munchify('aggregate', data) def add_host_to_aggregate(self, name_or_id, host_name): """Add a host to an aggregate. @@ -2026,7 +2026,7 @@ def get_volume_type_access(self, name_or_id): error_message="Unable to get volume type access" " {name}".format(name=name_or_id)) return self._normalize_volume_type_accesses( - meta.get_and_munchify('volume_type_access', data)) + self._get_and_munchify('volume_type_access', data)) def add_volume_type_access(self, name_or_id, project_id): """Grant access on a volume_type to a project. @@ -2113,7 +2113,7 @@ def get_compute_quotas(self, name_or_id): raise OpenStackCloudException("project does not exist") data = self._compute_client.get( '/os-quota-sets/{project}'.format(project=proj.id)) - return meta.get_and_munchify('quota_set', data) + return self._get_and_munchify('quota_set', data) def delete_compute_quotas(self, name_or_id): """ Delete quota for a project @@ -2191,7 +2191,7 @@ def parse_datetime_for_nova(date): error_message="Unable to get usage for project: {name}".format( name=proj.id)) return self._normalize_compute_usage( - meta.get_and_munchify('tenant_usage', data)) + self._get_and_munchify('tenant_usage', data)) def set_volume_quotas(self, name_or_id, **kwargs): """ Set a volume quota in a project @@ -2228,7 +2228,7 @@ def get_volume_quotas(self, name_or_id): data = self._volume_client.get( '/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id), error_message="cinder client call failed") - return meta.get_and_munchify('quota_set', data) + return self._get_and_munchify('quota_set', data) def delete_volume_quotas(self, name_or_id): """ Delete volume quotas for a project @@ -2282,7 +2282,7 @@ def get_network_quotas(self, name_or_id): '/quotas/{project_id}.json'.format(project_id=proj.id), error_message=("Error fetching Neutron's quota for " "project {0}".format(proj.id))) - return meta.get_and_munchify('quota', data) + return self._get_and_munchify('quota', data) def delete_network_quotas(self, name_or_id): """ Delete network quotas for a project @@ -2310,4 +2310,4 @@ def list_magnum_services(self): with _utils.shade_exceptions("Error fetching Magnum services list"): data = self._container_infra_client.get('/mservices') return self._normalize_magnum_services( - meta.get_and_munchify('mservices', data)) + self._get_and_munchify('mservices', data)) From 06b390ad7c8971a8ce5bf51a05eea40b48b83d94 Mon Sep 17 00:00:00 2001 From: rajat29 Date: Thu, 20 Jul 2017 19:05:26 +0530 Subject: [PATCH 1710/3836] Replace six.itervalues with dict.values() We should avoid using six.itervalues to achieve iterators. We can use dict.values instead, as it willreturn iterators in PY3 as well. And dict.items/values will more readable. Change-Id: I81fcf7cfdbd24784039ec2aca6a0c1f503a76502 --- shade/_heat/template_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/_heat/template_utils.py b/shade/_heat/template_utils.py index 722e09a1d..3653daaf6 100644 --- a/shade/_heat/template_utils.py +++ b/shade/_heat/template_utils.py @@ -107,7 +107,7 @@ def get_file_contents(from_data, files, base_url=None, if recurse_if and recurse_if(from_data): if isinstance(from_data, dict): - recurse_data = six.itervalues(from_data) + recurse_data = from_data.values() else: recurse_data = from_data for value in recurse_data: From 1b51cd83de845e4e4a5abbad22c6c94cd7a3f0f8 Mon Sep 17 00:00:00 2001 From: rajat29 Date: Fri, 21 Jul 2017 12:26:43 +0530 Subject: [PATCH 1711/3836] Update the documentation link for doc migration Change-Id: Idd4909d94c07220425fbdce938c133d890c4be86 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 4f3139ec0..3f3300b3d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,7 @@ description-file = README.rst author = OpenStack author-email = openstack-dev@lists.openstack.org -home-page = http://docs.openstack.org/developer/shade/ +home-page = http://docs.openstack.org/shade/latest classifier = Environment :: OpenStack Intended Audience :: Information Technology From 2e3ad0ff7ecee51bdf0f03840b05ff892be34fe5 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 22 Jul 2017 16:39:09 +0000 Subject: [PATCH 1712/3836] Updated from global requirements Change-Id: I1bec8faaed866642da53ac23f3889cc44c6eaf3b --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 51fbe5698..3e9962313 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,6 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 jsonpatch>=1.1 # BSD six>=1.9.0 # MIT stevedore>=1.20.0 # Apache-2.0 -os-client-config>=1.27.0 # Apache-2.0 -keystoneauth1>=2.21.0 # Apache-2.0 +os-client-config>=1.28.0 # Apache-2.0 +keystoneauth1!=3.0.0,>=2.21.0 # Apache-2.0 deprecation>=1.0 # Apache-2.0 From 52583776f3c0b0c493d33ac607282ec7c182753e Mon Sep 17 00:00:00 2001 From: Hangdong Zhang Date: Mon, 24 Jul 2017 11:48:03 +0800 Subject: [PATCH 1713/3836] Update external links which have moved The developer docs seem to have moved so this updates any references to docs.openstack.org/developer Change-Id: Ie622b9514dfffc0bbf5ed553f38f0b736d698a64 --- HACKING.rst | 2 +- doc/source/contributors/local.conf | 2 +- doc/source/contributors/testing.rst | 4 ++-- openstack/image/v2/image.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/HACKING.rst b/HACKING.rst index bfb22b7fa..e7627c519 100644 --- a/HACKING.rst +++ b/HACKING.rst @@ -1,4 +1,4 @@ python-openstacksdk Style Commandments ====================================== -Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/ +Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/ diff --git a/doc/source/contributors/local.conf b/doc/source/contributors/local.conf index 4aaa56b09..79106deac 100644 --- a/doc/source/contributors/local.conf +++ b/doc/source/contributors/local.conf @@ -57,7 +57,7 @@ enable_service h-api-cw # Automatically download and register a VM image that Heat can launch # For more information on Heat and DevStack see -# http://docs.openstack.org/developer/heat/getting_started/on_devstack.html +# https://docs.openstack.org/heat/latest/getting_started/on_devstack.html IMAGE_URL_SITE="http://download.fedoraproject.org" IMAGE_URL_PATH="/pub/fedora/linux/releases/25/CloudImages/x86_64/images/" IMAGE_URL_FILE="Fedora-Cloud-Base-25-1.3.x86_64.qcow2" diff --git a/doc/source/contributors/testing.rst b/doc/source/contributors/testing.rst index dc884a213..32d7419e4 100644 --- a/doc/source/contributors/testing.rst +++ b/doc/source/contributors/testing.rst @@ -41,7 +41,7 @@ The functional tests assume that you have a public or private OpenStack cloud that you can run the tests against. The tests must be able to be run against public clouds but first and foremost they must be run against OpenStack. In practice, this means that the tests should initially be run against a stable -branch of `DevStack `_. +branch of `DevStack `_. DevStack ******** @@ -111,7 +111,7 @@ Examples Tests Similar to the functional tests, the examples tests assume that you have a public or private OpenStack cloud that you can run the tests against. In practice, this means that the tests should initially be run against a stable -branch of `DevStack `_. +branch of `DevStack `_. And like the functional tests, the examples tests connect to an OpenStack cloud using `os-client-config `_. See the functional tests instructions for information on setting up DevStack and diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index 5694ce7f7..61f970fad 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -124,7 +124,7 @@ class Image(resource2.Resource): metadata = resource2.Body('metadata', type=dict) # Additional Image Properties - # http://docs.openstack.org/developer/glance/common-image-properties.html + # https://docs.openstack.org/glance/latest/user/common-image-properties.html # http://docs.openstack.org/cli-reference/glance-property-keys.html #: The CPU architecture that must be supported by the hypervisor. architecture = resource2.Body("architecture") From 623593a6933da69000967abd7b6d3e2cf7cec954 Mon Sep 17 00:00:00 2001 From: Dirk Mueller Date: Sat, 15 Jul 2017 20:26:43 +0200 Subject: [PATCH 1714/3836] Manually sync with g-r Change-Id: I4298bb7c2d66632b716b0dbeae64c9dca2b3434d --- requirements.txt | 8 ++++---- setup.py | 11 +++++++++-- test-requirements.txt | 30 +++++++++++++++--------------- 3 files changed, 28 insertions(+), 21 deletions(-) mode change 100755 => 100644 setup.py diff --git a/requirements.txt b/requirements.txt index 1531be808..6d1ee01aa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ # 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. -PyYAML>=3.1.0 -appdirs>=1.3.0 -keystoneauth1>=2.1.0 -requestsexceptions>=1.1.1 # Apache-2.0 +PyYAML>=3.10.0 # MIT +appdirs>=1.3.0 # MIT License +keystoneauth1>=3.0.1 # Apache-2.0 +requestsexceptions>=1.2.0 # Apache-2.0 diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 index 70c2b3f32..566d84432 --- a/setup.py +++ b/setup.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +16,14 @@ # 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'], + setup_requires=['pbr>=2.0.0'], pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt index 5fc33b015..9739a1696 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,20 +2,20 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. -hacking>=0.12.0,!=0.13.0,<0.14 # Apache-2.0 +hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 -coverage>=3.6 +coverage!=4.4,>=4.0 # Apache-2.0 docutils>=0.11 # OSI-Approved Open Source, Public Domain -extras -fixtures>=0.3.14 -jsonschema>=2.0.0,<3.0.0,!=2.5.0 -mock>=1.2 -python-glanceclient>=0.18.0 -python-subunit>=0.0.18 -sphinx>=1.5.1 # BSD -openstackdocstheme>=1.5.0 # Apache-2.0 -oslotest>=1.5.1,<1.6.0 # Apache-2.0 -reno>=0.1.1 # Apache2 -testrepository>=0.0.18 -testscenarios>=0.4 -testtools>=0.9.36,!=1.2.0 +extras # MIT +fixtures>=3.0.0 # Apache-2.0/BSD +jsonschema!=2.5.0,<3.0.0,>=2.0.0 # MIT +mock>=2.0 # BSD +python-glanceclient>=2.7.0 # Apache-2.0 +python-subunit>=0.0.18 # Apache-2.0/BSD +sphinx>=1.6.2 # BSD +openstackdocstheme>=1.11.0 # Apache-2.0 +oslotest>=1.10.0 # Apache-2.0 +reno!=2.3.1,>=1.8.0 # Apache-2.0 +testrepository>=0.0.18 # Apache-2.0/BSD +testscenarios>=0.4 # Apache-2.0/BSD +testtools>=1.4.0 # MIT From f66f86207361d733faef7fc4c4712ae828eb4821 Mon Sep 17 00:00:00 2001 From: Paul Belanger Date: Fri, 14 Jul 2017 15:37:46 -0400 Subject: [PATCH 1715/3836] Initial commit of zuulv3 jobs Here we are adding shade to zuulv3.o.o to aid in testing. Add UPPER_CONSTRAINTS_FILE to tox whitelist of variables. Change-Id: I4bc3bd751a80308df3b2114f95886bd104516dd2 Depends-On: Ib9f0a787998f34414c8072074113d29cdb8cdb59 Signed-off-by: Paul Belanger --- .zuul.yaml | 5 +++++ tox.ini | 1 + 2 files changed, 6 insertions(+) create mode 100644 .zuul.yaml diff --git a/.zuul.yaml b/.zuul.yaml new file mode 100644 index 000000000..f55ae2e2d --- /dev/null +++ b/.zuul.yaml @@ -0,0 +1,5 @@ +- project: + name: openstack-infra/shade + check: + jobs: + - tox-py35-constraints diff --git a/tox.ini b/tox.ini index e4aa17752..9df877edd 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,7 @@ skipsdist = True [testenv] usedevelop = True basepython = {env:SHADE_TOX_PYTHON:python2} +passenv = UPPER_CONSTRAINTS_FILE install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} setenv = VIRTUAL_ENV={envdir} From a4fdee57310f9bca644e84e43787f04c5996ab98 Mon Sep 17 00:00:00 2001 From: Lee Yarwood Date: Mon, 10 Jul 2017 14:15:31 +0100 Subject: [PATCH 1716/3836] router: Ignore L3 HA ports when listing interfaces L3 HA ports do not correspond to a router interface and should be ignored when listing these interfaces. Change-Id: I7fb5add36af09cc22084c8c034bf9f3cd0fbb442 --- shade/openstackcloud.py | 37 ++++++++++-------------------- shade/tests/unit/test_router.py | 40 +++++++++++---------------------- 2 files changed, 25 insertions(+), 52 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index c85ac0c34..481498769 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3594,33 +3594,20 @@ def list_router_interfaces(self, router, interface_type=None): :returns: A list of port ``munch.Munch`` objects. """ - ports = self.search_ports(filters={'device_id': router['id']}) + # Find only router interface and gateway ports, ignore L3 HA ports etc. + router_interfaces = self.search_ports(filters={ + 'device_id': router['id'], + 'device_owner': 'network:router_interface'}) + router_gateways = self.search_ports(filters={ + 'device_id': router['id'], + 'device_owner': 'network:router_gateway'}) + ports = router_interfaces + router_gateways if interface_type: - filtered_ports = [] - if (router.get('external_gateway_info') and - 'external_fixed_ips' in router['external_gateway_info']): - ext_fixed = \ - router['external_gateway_info']['external_fixed_ips'] - else: - ext_fixed = [] - - # Compare the subnets (subnet_id, ip_address) on the ports with - # the subnets making up the router external gateway. Those ports - # that match are the external interfaces, and those that don't - # are internal. - for port in ports: - matched_ext = False - for port_subnet in port['fixed_ips']: - for router_external_subnet in ext_fixed: - if port_subnet == router_external_subnet: - matched_ext = True - if interface_type == 'internal' and not matched_ext: - filtered_ports.append(port) - elif interface_type == 'external' and matched_ext: - filtered_ports.append(port) - return filtered_ports - + if interface_type == 'internal': + return router_interfaces + if interface_type == 'external': + return router_gateways return ports def create_router(self, name=None, admin_state_up=True, diff --git a/shade/tests/unit/test_router.py b/shade/tests/unit/test_router.py index f9030e4df..778d0dadc 100644 --- a/shade/tests/unit/test_router.py +++ b/shade/tests/unit/test_router.py @@ -266,7 +266,8 @@ def _test_list_router_interfaces(self, router, interface_type, 'subnet_id': 'internal_subnet_id', 'ip_address': "10.0.0.1" }], - 'device_id': self.router_id + 'device_id': self.router_id, + 'device_owner': 'network:router_interface' } external_port = { 'id': 'external_port_id', @@ -274,7 +275,8 @@ def _test_list_router_interfaces(self, router, interface_type, 'subnet_id': 'external_subnet_id', 'ip_address': "1.2.3.4" }], - 'device_id': self.router_id + 'device_id': self.router_id, + 'device_owner': 'network:router_gateway' } if expected_result is None: if interface_type == "internal": @@ -288,36 +290,20 @@ def _test_list_router_interfaces(self, router, interface_type, dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'ports.json'], - qs_elements=["device_id=%s" % self.router_id]), - json={'ports': [internal_port, external_port]}) + qs_elements=["device_id=%s" % self.router_id, + "device_owner=network:router_interface"]), + json={'ports': [internal_port]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json'], + qs_elements=["device_id=%s" % self.router_id, + "device_owner=network:router_gateway"]), + json={'ports': [external_port]}) ]) ret = self.cloud.list_router_interfaces(router, interface_type) self.assertEqual(expected_result, ret) self.assert_calls() - def test_list_router_interfaces_no_gw(self): - """ - If a router does not have external_gateway_info, do not fail. - """ - router = { - 'id': self.router_id - } - self._test_list_router_interfaces(router, - interface_type="external", - expected_result=[]) - - def test_list_router_interfaces_gw_none(self): - """ - If a router does have external_gateway_info set to None, do not fail. - """ - router = { - 'id': self.router_id, - 'external_gateway_info': None - } - self._test_list_router_interfaces(router, - interface_type="external", - expected_result=[]) - def test_list_router_interfaces_all(self): router = { 'id': self.router_id, From 85d8adaec6ac976a4c95f0796eaaca2ff15d4f67 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 27 Jul 2017 20:19:00 +0000 Subject: [PATCH 1717/3836] Updated from global requirements Change-Id: I2da0ea1c58865c47396b001f1aa66c54ece57bfb --- requirements.txt | 4 ++-- test-requirements.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index d7ba2b1a6..6935bc3fb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ decorator>=3.4.0 # BSD jmespath>=0.9.0 # MIT jsonpatch>=1.1 # BSD ipaddress>=1.0.7;python_version<'3.3' # PSF -os-client-config>=1.27.0 # Apache-2.0 +os-client-config>=1.28.0 # Apache-2.0 # These two are here to prevent issues with version pin mismatches from our # client library transitive depends. # Babel can be removed when ironicclient is removed (because of openstackclient @@ -19,7 +19,7 @@ six>=1.9.0 # MIT futures>=3.0;python_version=='2.7' or python_version=='2.6' # BSD iso8601>=0.1.11 # MIT -keystoneauth1>=2.21.0 # Apache-2.0 +keystoneauth1>=3.1.0 # Apache-2.0 netifaces>=0.10.4 # MIT python-keystoneclient>=3.8.0 # Apache-2.0 python-ironicclient>=1.14.0 # Apache-2.0 diff --git a/test-requirements.txt b/test-requirements.txt index 16c7d8ccf..67f0acfa3 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,7 +7,7 @@ coverage!=4.4,>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD mock>=2.0 # BSD python-subunit>=0.0.18 # Apache-2.0/BSD -openstackdocstheme>=1.11.0 # Apache-2.0 +openstackdocstheme>=1.16.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 sphinx>=1.6.2 # BSD From 1d4c124bb1c94ce475ca494b86dd69b09cbae0b4 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 27 Jul 2017 20:30:31 +0000 Subject: [PATCH 1718/3836] Updated from global requirements Change-Id: I611163aecdc4810e6fd1d7e47e60171d72db0ea3 --- requirements.txt | 2 +- test-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 6d1ee01aa..b4387d3ba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,5 @@ # process, which may cause wedges in the gate later. PyYAML>=3.10.0 # MIT appdirs>=1.3.0 # MIT License -keystoneauth1>=3.0.1 # Apache-2.0 +keystoneauth1>=3.1.0 # Apache-2.0 requestsexceptions>=1.2.0 # Apache-2.0 diff --git a/test-requirements.txt b/test-requirements.txt index 9739a1696..3c44d7f02 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -13,7 +13,7 @@ mock>=2.0 # BSD python-glanceclient>=2.7.0 # Apache-2.0 python-subunit>=0.0.18 # Apache-2.0/BSD sphinx>=1.6.2 # BSD -openstackdocstheme>=1.11.0 # Apache-2.0 +openstackdocstheme>=1.16.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 reno!=2.3.1,>=1.8.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD From 1c98ea5e2d2f79d921db3e8da234da70fa24ccd2 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 27 Jul 2017 20:33:19 +0000 Subject: [PATCH 1719/3836] Updated from global requirements Change-Id: Ifff5f947c99ef3e8d41dc4982a5b864fd4bf4684 --- requirements.txt | 2 +- test-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 3e9962313..f9aea340e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,5 +6,5 @@ jsonpatch>=1.1 # BSD six>=1.9.0 # MIT stevedore>=1.20.0 # Apache-2.0 os-client-config>=1.28.0 # Apache-2.0 -keystoneauth1!=3.0.0,>=2.21.0 # Apache-2.0 +keystoneauth1>=3.1.0 # Apache-2.0 deprecation>=1.0 # Apache-2.0 diff --git a/test-requirements.txt b/test-requirements.txt index 318d0b148..9c9ff8bdc 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,7 +8,7 @@ coverage!=4.4,>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD mock>=2.0 # BSD python-subunit>=0.0.18 # Apache-2.0/BSD -openstackdocstheme>=1.11.0 # Apache-2.0 +openstackdocstheme>=1.16.0 # Apache-2.0 os-testr>=0.8.0 # Apache-2.0 requests>=2.14.2 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 From 1521c59999ad9ef8acfd3e1486a4b9f7903242c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Sun, 16 Jul 2017 08:29:15 +0000 Subject: [PATCH 1720/3836] Add Neutron QoS dscp marking rule commands Create/Update/List/Get/Delete QoS DSCP marking rules is now possible to do with shade. Change-Id: I3b233c28c28cfa27e2d15599e34bae60bad83d75 --- shade/openstackcloud.py | 186 +++++++++++ .../tests/unit/test_qos_dscp_marking_rule.py | 294 ++++++++++++++++++ 2 files changed, 480 insertions(+) create mode 100644 shade/tests/unit/test_qos_dscp_marking_rule.py diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index c85ac0c34..bd3e5829e 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3505,6 +3505,192 @@ def delete_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): return True + def search_qos_dscp_marking_rules(self, policy_name_or_id, rule_id=None, + filters=None): + """Search QoS DSCP marking rules + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rules should be associated. + :param string rule_id: ID of searched rule. + :param filters: a dict containing additional filters to use. e.g. + {'dscp_mark': 32} + + :returns: a list of ``munch.Munch`` containing the dscp marking + rule descriptions. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + OpenStack API call. + """ + rules = self.list_qos_dscp_marking_rules(policy_name_or_id, filters) + return _utils._filter_list(rules, rule_id, filters) + + def list_qos_dscp_marking_rules(self, policy_name_or_id, filters=None): + """List all available QoS DSCP marking rules. + + :param string policy_name_or_id: Name or ID of the QoS policy from + from rules should be listed. + :param filters: (optional) dict of filter conditions to push down + :returns: A list of ``munch.Munch`` containing rule info. + + :raises: ``OpenStackCloudResourceNotFound`` if QoS policy will not be + found. + """ + if not self._has_neutron_extension('qos'): + raise OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + # Translate None from search interface to empty {} for kwargs below + if not filters: + filters = {} + + data = self._network_client.get( + "/qos/policies/{policy_id}/dscp_marking_rules.json".format( + policy_id=policy['id']), + params=filters, + error_message="Error fetching QoS DSCP marking rules from " + "{policy}".format(policy=policy['id'])) + return meta.get_and_munchify('dscp_marking_rules', data) + + def get_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): + """Get a QoS DSCP marking rule by name or ID. + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rule should be associated. + :param rule_id: ID of the rule. + + :returns: A bandwidth limit rule ``munch.Munch`` or None if + no matching rule is found. + + """ + if not self._has_neutron_extension('qos'): + raise OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + data = self._network_client.get( + "/qos/policies/{policy_id}/dscp_marking_rules/{rule_id}.json". + format(policy_id=policy['id'], rule_id=rule_id), + error_message="Error fetching QoS DSCP marking rule {rule_id} " + "from {policy}".format(rule_id=rule_id, + policy=policy['id'])) + return meta.get_and_munchify('dscp_marking_rule', data) + + def create_qos_dscp_marking_rule(self, policy_name_or_id, dscp_mark=None): + """Create a QoS DSCP marking rule. + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rule should be associated. + :param int dscp_mark: DSCP mark value + + :returns: The QoS DSCP marking rule. + :raises: OpenStackCloudException on operation error. + """ + if not self._has_neutron_extension('qos'): + raise OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + rule = {} + if dscp_mark: + rule['dscp_mark'] = dscp_mark + + data = self._network_client.post( + "/qos/policies/{policy_id}/dscp_marking_rules".format( + policy_id=policy['id']), + json={'dscp_marking_rule': rule}) + return meta.get_and_munchify('dscp_marking_rule', data) + + def update_qos_dscp_marking_rule(self, policy_name_or_id, rule_id, + dscp_mark=None): + """Update a QoS DSCP marking rule. + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rule is associated. + :param string rule_id: ID of rule to update. + :param int dscp_mark: DSCP mark value + + :returns: The updated QoS bandwidth limit rule. + :raises: OpenStackCloudException on operation error. + """ + if not self._has_neutron_extension('qos'): + raise OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + rule = {} + if dscp_mark: + rule['dscp_mark'] = dscp_mark + if not rule: + self.log.debug("No QoS DSCP marking rule data to update") + return + + curr_rule = self.get_qos_dscp_marking_rule( + policy_name_or_id, rule_id) + if not curr_rule: + raise OpenStackCloudException( + "QoS dscp_marking_rule {rule_id} not found in policy " + "{policy_id}".format(rule_id=rule_id, + policy_id=policy['id'])) + + data = self._network_client.put( + "/qos/policies/{policy_id}/dscp_marking_rules/{rule_id}.json". + format(policy_id=policy['id'], rule_id=rule_id), + json={'dscp_marking_rule': rule}) + return meta.get_and_munchify('dscp_marking_rule', data) + + def delete_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): + """Delete a QoS DSCP marking rule. + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rule is associated. + :param string rule_id: ID of rule to update. + + :raises: OpenStackCloudException on operation error. + """ + if not self._has_neutron_extension('qos'): + raise OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + try: + self._network_client.delete( + "/qos/policies/{policy}/dscp_marking_rules/{rule}.json". + format(policy=policy['id'], rule=rule_id)) + except OpenStackCloudURINotFound: + self.log.debug( + "QoS DSCP marking rule {rule_id} not found in policy " + "{policy_id}. Ignoring.".format(rule_id=rule_id, + policy_id=policy['id'])) + return False + + return True + def _build_external_gateway_info(self, ext_gateway_net_id, enable_snat, ext_fixed_ips): info = {} diff --git a/shade/tests/unit/test_qos_dscp_marking_rule.py b/shade/tests/unit/test_qos_dscp_marking_rule.py new file mode 100644 index 000000000..ccb60e15f --- /dev/null +++ b/shade/tests/unit/test_qos_dscp_marking_rule.py @@ -0,0 +1,294 @@ +# Copyright 2017 OVH SAS +# 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 copy + +from shade import exc +from shade.tests.unit import base + + +class TestQosDscpMarkingRule(base.RequestsMockTestCase): + + policy_name = 'qos test policy' + policy_id = '881d1bb7-a663-44c0-8f9f-ee2765b74486' + project_id = 'c88fc89f-5121-4a4c-87fd-496b5af864e9' + + rule_id = 'ed1a2b05-0ad7-45d7-873f-008b575a02b3' + rule_dscp_mark = 32 + + mock_policy = { + 'id': policy_id, + 'name': policy_name, + 'description': '', + 'rules': [], + 'project_id': project_id, + 'tenant_id': project_id, + 'shared': False, + 'is_default': False + } + + mock_rule = { + 'id': rule_id, + 'dscp_mark': rule_dscp_mark, + } + + qos_extension = { + "updated": "2015-06-08T10:00:00-00:00", + "name": "Quality of Service", + "links": [], + "alias": "qos", + "description": "The Quality of Service extension." + } + + enabled_neutron_extensions = [qos_extension] + + def test_get_qos_dscp_marking_rule(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': [self.mock_policy]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_id, + 'dscp_marking_rules', + '%s.json' % self.rule_id]), + json={'dscp_marking_rule': self.mock_rule}) + ]) + r = self.cloud.get_qos_dscp_marking_rule(self.policy_name, + self.rule_id) + self.assertDictEqual(self.mock_rule, r) + self.assert_calls() + + def test_get_qos_dscp_marking_rule_no_qos_policy_found(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': []}) + ]) + self.assertRaises( + exc.OpenStackCloudResourceNotFound, + self.cloud.get_qos_dscp_marking_rule, + self.policy_name, self.rule_id) + self.assert_calls() + + def test_get_qos_dscp_marking_rule_no_qos_extension(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': []}) + ]) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.get_qos_dscp_marking_rule, + self.policy_name, self.rule_id) + self.assert_calls() + + def test_create_qos_dscp_marking_rule(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': [self.mock_policy]}), + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_id, + 'dscp_marking_rules']), + json={'dscp_marking_rule': self.mock_rule}) + ]) + rule = self.cloud.create_qos_dscp_marking_rule( + self.policy_name, dscp_mark=self.rule_dscp_mark) + self.assertDictEqual(self.mock_rule, rule) + self.assert_calls() + + def test_create_qos_dscp_marking_rule_no_qos_extension(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': []}) + ]) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.create_qos_dscp_marking_rule, self.policy_name, + dscp_mark=16) + self.assert_calls() + + def test_update_qos_dscp_marking_rule(self): + new_dscp_mark_value = 16 + expected_rule = copy.copy(self.mock_rule) + expected_rule['dscp_mark'] = new_dscp_mark_value + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': [self.mock_policy]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': [self.mock_policy]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_id, + 'dscp_marking_rules', + '%s.json' % self.rule_id]), + json={'dscp_marking_rule': self.mock_rule}), + dict(method='PUT', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_id, + 'dscp_marking_rules', + '%s.json' % self.rule_id]), + json={'dscp_marking_rule': expected_rule}, + validate=dict( + json={'dscp_marking_rule': { + 'dscp_mark': new_dscp_mark_value}})) + ]) + rule = self.cloud.update_qos_dscp_marking_rule( + self.policy_id, self.rule_id, dscp_mark=new_dscp_mark_value) + self.assertDictEqual(expected_rule, rule) + self.assert_calls() + + def test_update_qos_dscp_marking_rule_no_qos_extension(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': []}) + ]) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.update_qos_dscp_marking_rule, + self.policy_id, self.rule_id, dscp_mark=8) + self.assert_calls() + + def test_delete_qos_dscp_marking_rule(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': [self.mock_policy]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_id, + 'dscp_marking_rules', + '%s.json' % self.rule_id]), + json={}) + ]) + self.assertTrue( + self.cloud.delete_qos_dscp_marking_rule( + self.policy_name, self.rule_id)) + self.assert_calls() + + def test_delete_qos_dscp_marking_rule_no_qos_extension(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': []}) + ]) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.delete_qos_dscp_marking_rule, + self.policy_name, self.rule_id) + self.assert_calls() + + def test_delete_qos_dscp_marking_rule_not_found(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': [self.mock_policy]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_id, + 'dscp_marking_rules', + '%s.json' % self.rule_id]), + status_code=404) + ]) + self.assertFalse( + self.cloud.delete_qos_dscp_marking_rule( + self.policy_name, self.rule_id)) + self.assert_calls() From 8c8f365a9f143f2159339584f15a48bdf185b63d Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 28 Jul 2017 21:04:06 +0000 Subject: [PATCH 1721/3836] Update reno for stable/pike Change-Id: Ie69ff1e5a5d3cf3a762d6915c5d596de4f919931 --- releasenotes/source/index.rst | 1 + releasenotes/source/pike.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/pike.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 22609515d..405f263a0 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -9,6 +9,7 @@ Contents :maxdepth: 2 unreleased + pike ocata newton mitaka diff --git a/releasenotes/source/pike.rst b/releasenotes/source/pike.rst new file mode 100644 index 000000000..e43bfc0ce --- /dev/null +++ b/releasenotes/source/pike.rst @@ -0,0 +1,6 @@ +=================================== + Pike Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/pike From 420e7454de7a2812f3938e84ee074b479620bd46 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 28 Jul 2017 21:08:43 +0000 Subject: [PATCH 1722/3836] Update reno for stable/pike Change-Id: I03e74702143f0d8ba248c12aba2c179d6cbaafdd --- releasenotes/source/index.rst | 1 + releasenotes/source/pike.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/pike.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 353e35068..74d2b566c 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -7,3 +7,4 @@ mainline unreleased + pike diff --git a/releasenotes/source/pike.rst b/releasenotes/source/pike.rst new file mode 100644 index 000000000..e43bfc0ce --- /dev/null +++ b/releasenotes/source/pike.rst @@ -0,0 +1,6 @@ +=================================== + Pike Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/pike From 99865c9f77b9825498d60c7895c9784ffdaf70f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Mon, 31 Jul 2017 20:46:32 +0000 Subject: [PATCH 1723/3836] Add Neutron QoS minimum bandwidth rule commands Create/Update/List/Get/Delete QoS minimum bandwidth rules is now possible to do with shade. Change-Id: Ibe2b28d15a9a8de58e9605330e241328c1cd242b --- shade/openstackcloud.py | 198 ++++++++++++ .../unit/test_qos_minimum_bandwidth_rule.py | 294 ++++++++++++++++++ 2 files changed, 492 insertions(+) create mode 100644 shade/tests/unit/test_qos_minimum_bandwidth_rule.py diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index bd3e5829e..5ad67526a 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3691,6 +3691,204 @@ def delete_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): return True + def search_qos_minimum_bandwidth_rules(self, policy_name_or_id, + rule_id=None, filters=None): + """Search QoS minimum bandwidth rules + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rules should be associated. + :param string rule_id: ID of searched rule. + :param filters: a dict containing additional filters to use. e.g. + {'min_kbps': 1000} + + :returns: a list of ``munch.Munch`` containing the bandwidth limit + rule descriptions. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + OpenStack API call. + """ + rules = self.list_qos_minimum_bandwidth_rules( + policy_name_or_id, filters) + return _utils._filter_list(rules, rule_id, filters) + + def list_qos_minimum_bandwidth_rules(self, policy_name_or_id, + filters=None): + """List all available QoS minimum bandwith rules. + + :param string policy_name_or_id: Name or ID of the QoS policy from + from rules should be listed. + :param filters: (optional) dict of filter conditions to push down + :returns: A list of ``munch.Munch`` containing rule info. + + :raises: ``OpenStackCloudResourceNotFound`` if QoS policy will not be + found. + """ + if not self._has_neutron_extension('qos'): + raise OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + # Translate None from search interface to empty {} for kwargs below + if not filters: + filters = {} + + data = self._network_client.get( + "/qos/policies/{policy_id}/minimum_bandwidth_rules.json".format( + policy_id=policy['id']), + params=filters, + error_message="Error fetching QoS minimum bandwith rules from " + "{policy}".format(policy=policy['id'])) + return self._get_and_munchify('minimum_bandwidth_rules', data) + + def get_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): + """Get a QoS minimum bandwidth rule by name or ID. + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rule should be associated. + :param rule_id: ID of the rule. + + :returns: A bandwidth limit rule ``munch.Munch`` or None if + no matching rule is found. + + """ + if not self._has_neutron_extension('qos'): + raise OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + data = self._network_client.get( + "/qos/policies/{policy_id}/minimum_bandwidth_rules/{rule_id}.json". + format(policy_id=policy['id'], rule_id=rule_id), + error_message="Error fetching QoS minimum_bandwith rule {rule_id} " + "from {policy}".format(rule_id=rule_id, + policy=policy['id'])) + return self._get_and_munchify('minimum_bandwidth_rule', data) + + def create_qos_minimum_bandwidth_rule(self, policy_name_or_id, + min_kbps=None, direction=None): + """Create a QoS minimum bandwidth limit rule. + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rule should be associated. + :param int min_kbps: Minimum bandwidth value (in kilobits per second). + :param string direction: Ingress or egress. + The direction in which the traffic will be available. + + :returns: The QoS minimum bandwidth rule. + :raises: OpenStackCloudException on operation error. + """ + if not self._has_neutron_extension('qos'): + raise OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + rule = {} + if min_kbps: + rule['min_kbps'] = min_kbps + if direction: + rule['direction'] = direction + + data = self._network_client.post( + "/qos/policies/{policy_id}/minimum_bandwidth_rules".format( + policy_id=policy['id']), + json={'minimum_bandwidth_rule': rule}) + return self._get_and_munchify('minimum_bandwidth_rule', data) + + def update_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id, + min_kbps=None, direction=None): + """Update a QoS minimum bandwidth rule. + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rule is associated. + :param string rule_id: ID of rule to update. + :param int min_kbps: Minimum bandwidth value (in kilobits per second). + :param string direction: Ingress or egress. + The direction in which the traffic will be available. + + :returns: The updated QoS minimum bandwidth rule. + :raises: OpenStackCloudException on operation error. + """ + if not self._has_neutron_extension('qos'): + raise OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + rule = {} + if min_kbps: + rule['min_kbps'] = min_kbps + if direction: + rule['direction'] = direction + + if not rule: + self.log.debug("No QoS minimum bandwidth rule data to update") + return + + curr_rule = self.get_qos_minimum_bandwidth_rule( + policy_name_or_id, rule_id) + if not curr_rule: + raise OpenStackCloudException( + "QoS minimum_bandwidth_rule {rule_id} not found in policy " + "{policy_id}".format(rule_id=rule_id, + policy_id=policy['id'])) + + data = self._network_client.put( + "/qos/policies/{policy_id}/minimum_bandwidth_rules/{rule_id}.json". + format(policy_id=policy['id'], rule_id=rule_id), + json={'minimum_bandwidth_rule': rule}) + return self._get_and_munchify('minimum_bandwidth_rule', data) + + def delete_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): + """Delete a QoS minimum bandwidth rule. + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rule is associated. + :param string rule_id: ID of rule to delete. + + :raises: OpenStackCloudException on operation error. + """ + if not self._has_neutron_extension('qos'): + raise OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + try: + self._network_client.delete( + "/qos/policies/{policy}/minimum_bandwidth_rules/{rule}.json". + format(policy=policy['id'], rule=rule_id)) + except OpenStackCloudURINotFound: + self.log.debug( + "QoS minimum bandwidth rule {rule_id} not found in policy " + "{policy_id}. Ignoring.".format(rule_id=rule_id, + policy_id=policy['id'])) + return False + + return True + def _build_external_gateway_info(self, ext_gateway_net_id, enable_snat, ext_fixed_ips): info = {} diff --git a/shade/tests/unit/test_qos_minimum_bandwidth_rule.py b/shade/tests/unit/test_qos_minimum_bandwidth_rule.py new file mode 100644 index 000000000..6afdc44bb --- /dev/null +++ b/shade/tests/unit/test_qos_minimum_bandwidth_rule.py @@ -0,0 +1,294 @@ +# Copyright 2017 OVH SAS +# 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 copy + +from shade import exc +from shade.tests.unit import base + + +class TestQosMinimumBandwidthRule(base.RequestsMockTestCase): + + policy_name = 'qos test policy' + policy_id = '881d1bb7-a663-44c0-8f9f-ee2765b74486' + project_id = 'c88fc89f-5121-4a4c-87fd-496b5af864e9' + + rule_id = 'ed1a2b05-0ad7-45d7-873f-008b575a02b3' + rule_min_kbps = 1000 + + mock_policy = { + 'id': policy_id, + 'name': policy_name, + 'description': '', + 'rules': [], + 'project_id': project_id, + 'tenant_id': project_id, + 'shared': False, + 'is_default': False + } + + mock_rule = { + 'id': rule_id, + 'min_kbps': rule_min_kbps, + 'direction': 'egress' + } + + qos_extension = { + "updated": "2015-06-08T10:00:00-00:00", + "name": "Quality of Service", + "links": [], + "alias": "qos", + "description": "The Quality of Service extension." + } + + enabled_neutron_extensions = [qos_extension] + + def test_get_qos_minimum_bandwidth_rule(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': [self.mock_policy]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_id, + 'minimum_bandwidth_rules', + '%s.json' % self.rule_id]), + json={'minimum_bandwidth_rule': self.mock_rule}) + ]) + r = self.cloud.get_qos_minimum_bandwidth_rule(self.policy_name, + self.rule_id) + self.assertDictEqual(self.mock_rule, r) + self.assert_calls() + + def test_get_qos_minimum_bandwidth_rule_no_qos_policy_found(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': []}) + ]) + self.assertRaises( + exc.OpenStackCloudResourceNotFound, + self.cloud.get_qos_minimum_bandwidth_rule, + self.policy_name, self.rule_id) + self.assert_calls() + + def test_get_qos_minimum_bandwidth_rule_no_qos_extension(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': []}) + ]) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.get_qos_minimum_bandwidth_rule, + self.policy_name, self.rule_id) + self.assert_calls() + + def test_create_qos_minimum_bandwidth_rule(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': [self.mock_policy]}), + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_id, + 'minimum_bandwidth_rules']), + json={'minimum_bandwidth_rule': self.mock_rule}) + ]) + rule = self.cloud.create_qos_minimum_bandwidth_rule( + self.policy_name, min_kbps=self.rule_min_kbps) + self.assertDictEqual(self.mock_rule, rule) + self.assert_calls() + + def test_create_qos_minimum_bandwidth_rule_no_qos_extension(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': []}) + ]) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.create_qos_minimum_bandwidth_rule, self.policy_name, + min_kbps=100) + self.assert_calls() + + def test_update_qos_minimum_bandwidth_rule(self): + expected_rule = copy.copy(self.mock_rule) + expected_rule['min_kbps'] = self.rule_min_kbps + 100 + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': [self.mock_policy]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': [self.mock_policy]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_id, + 'minimum_bandwidth_rules', + '%s.json' % self.rule_id]), + json={'minimum_bandwidth_rule': self.mock_rule}), + dict(method='PUT', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_id, + 'minimum_bandwidth_rules', + '%s.json' % self.rule_id]), + json={'minimum_bandwidth_rule': expected_rule}, + validate=dict( + json={'minimum_bandwidth_rule': { + 'min_kbps': self.rule_min_kbps + 100}})) + ]) + rule = self.cloud.update_qos_minimum_bandwidth_rule( + self.policy_id, self.rule_id, min_kbps=self.rule_min_kbps + 100) + self.assertDictEqual(expected_rule, rule) + self.assert_calls() + + def test_update_qos_minimum_bandwidth_rule_no_qos_extension(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': []}) + ]) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.update_qos_minimum_bandwidth_rule, + self.policy_id, self.rule_id, min_kbps=2000) + self.assert_calls() + + def test_delete_qos_minimum_bandwidth_rule(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': [self.mock_policy]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_id, + 'minimum_bandwidth_rules', + '%s.json' % self.rule_id]), + json={}) + ]) + self.assertTrue( + self.cloud.delete_qos_minimum_bandwidth_rule( + self.policy_name, self.rule_id)) + self.assert_calls() + + def test_delete_qos_minimum_bandwidth_rule_no_qos_extension(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': []}) + ]) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.delete_qos_minimum_bandwidth_rule, + self.policy_name, self.rule_id) + self.assert_calls() + + def test_delete_qos_minimum_bandwidth_rule_not_found(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies.json']), + json={'policies': [self.mock_policy]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_id, + 'minimum_bandwidth_rules', + '%s.json' % self.rule_id]), + status_code=404) + ]) + self.assertFalse( + self.cloud.delete_qos_minimum_bandwidth_rule( + self.policy_name, self.rule_id)) + self.assert_calls() From 05b0fe25ccb82b69a986a1ebda3b65506c73b5d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9la=20Vancsics?= Date: Tue, 1 Aug 2017 10:17:58 +0200 Subject: [PATCH 1724/3836] Use more specific asserts in tests Instead of assertTrue and assertFalse use more specific asserts. They are compatible with Python 2.7[1] and 3.5[2] [1]: https://docs.python.org/2.7/library/unittest.html [2]: https://docs.python.org/3.5/library/unittest.html Change-Id: Ie2e117935e12b9fb56c0b6070924786f9693421e --- .../functional/test_cluster_templates.py | 2 +- shade/tests/functional/test_compute.py | 8 ++++---- shade/tests/functional/test_flavor.py | 2 +- .../tests/functional/test_floating_ip_pool.py | 2 +- shade/tests/functional/test_identity.py | 20 +++++++++---------- shade/tests/functional/test_port.py | 6 +++--- shade/tests/functional/test_range_search.py | 4 ++-- shade/tests/unit/test_meta.py | 2 +- shade/tests/unit/test_shade_operator.py | 14 ++++++------- 9 files changed, 30 insertions(+), 30 deletions(-) diff --git a/shade/tests/functional/test_cluster_templates.py b/shade/tests/functional/test_cluster_templates.py index 634858d97..b19178c88 100644 --- a/shade/tests/functional/test_cluster_templates.py +++ b/shade/tests/functional/test_cluster_templates.py @@ -93,7 +93,7 @@ def test_cluster_templates(self): self.ct['uuid'], 'replace', tls_disabled=True) self.assertEqual( cluster_template_update['uuid'], self.ct['uuid']) - self.assertEqual(cluster_template_update['tls_disabled'], True) + self.assertTrue(cluster_template_update['tls_disabled']) # Test we can delete and get True returned cluster_template_delete = self.user_cloud.delete_cluster_template( diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index d52dfedf7..938365b73 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -112,7 +112,7 @@ def test_create_and_delete_server_with_config_drive(self): self.assertEqual(self.server_name, server['name']) self.assertEqual(self.image.id, server['image']['id']) self.assertEqual(self.flavor.id, server['flavor']['id']) - self.assertEqual(True, server['has_config_drive']) + self.assertTrue(server['has_config_drive']) self.assertIsNotNone(server['adminPass']) self.assertTrue( self.user_cloud.delete_server(self.server_name, wait=True)) @@ -132,7 +132,7 @@ def test_create_and_delete_server_with_config_drive_none(self): self.assertEqual(self.server_name, server['name']) self.assertEqual(self.image.id, server['image']['id']) self.assertEqual(self.flavor.id, server['flavor']['id']) - self.assertEqual(False, server['has_config_drive']) + self.assertFalse(server['has_config_drive']) self.assertIsNotNone(server['adminPass']) self.assertTrue( self.user_cloud.delete_server( @@ -272,7 +272,7 @@ def test_create_boot_from_volume_image(self): volume = self.user_cloud.get_volume(volume_id) self.assertIsNotNone(volume) self.assertEqual(volume['name'], volume['display_name']) - self.assertEqual(True, volume['bootable']) + self.assertTrue(volume['bootable']) self.assertEqual(server['id'], volume['attachments'][0]['server_id']) self.assertTrue(self.user_cloud.delete_server(server.id, wait=True)) self._wait_for_detach(volume.id) @@ -336,7 +336,7 @@ def test_create_boot_from_volume_preexisting(self): volume = self.user_cloud.get_volume(volume_id) self.assertIsNotNone(volume) self.assertEqual(volume['name'], volume['display_name']) - self.assertEqual(True, volume['bootable']) + self.assertTrue(volume['bootable']) self.assertEqual([], volume['attachments']) self._wait_for_detach(volume.id) self.assertTrue(self.user_cloud.delete_volume(volume_id)) diff --git a/shade/tests/functional/test_flavor.py b/shade/tests/functional/test_flavor.py index d9a0baf4e..e149e0118 100644 --- a/shade/tests/functional/test_flavor.py +++ b/shade/tests/functional/test_flavor.py @@ -72,7 +72,7 @@ def test_create_flavor(self): self.assertEqual(5, flavor['ephemeral']) self.assertIn('is_public', flavor) self.assertIn('os-flavor-access:is_public', flavor) - self.assertEqual(True, flavor['is_public']) + self.assertTrue(flavor['is_public']) for key in flavor_kwargs.keys(): self.assertIn(key, flavor) diff --git a/shade/tests/functional/test_floating_ip_pool.py b/shade/tests/functional/test_floating_ip_pool.py index a6b98e08c..c4ee5a0f6 100644 --- a/shade/tests/functional/test_floating_ip_pool.py +++ b/shade/tests/functional/test_floating_ip_pool.py @@ -47,4 +47,4 @@ def test_list_floating_ip_pools(self): self.assertFalse('no floating-ip pool available') for pool in pools: - self.assertTrue('name' in pool) + self.assertIn('name', pool) diff --git a/shade/tests/functional/test_identity.py b/shade/tests/functional/test_identity.py index 655407a9e..56413f7db 100644 --- a/shade/tests/functional/test_identity.py +++ b/shade/tests/functional/test_identity.py @@ -125,7 +125,7 @@ def test_list_role_assignments(self): self.skipTest("Identity service does not support role assignments") assignments = self.operator_cloud.list_role_assignments() self.assertIsInstance(assignments, list) - self.assertTrue(len(assignments) > 0) + self.assertGreater(len(assignments), 0) def test_list_role_assignments_v2(self): user = self.operator_cloud.get_user('demo') @@ -133,7 +133,7 @@ def test_list_role_assignments_v2(self): assignments = self.operator_cloud.list_role_assignments( filters={'user': user['id'], 'project': project['id']}) self.assertIsInstance(assignments, list) - self.assertTrue(len(assignments) > 0) + self.assertGreater(len(assignments), 0) def test_grant_revoke_role_user_project(self): user_name = self.user_prefix + '_user_project' @@ -151,7 +151,7 @@ def test_grant_revoke_role_user_project(self): 'project': self.operator_cloud.get_project('demo')['id'] }) self.assertIsInstance(assignments, list) - self.assertTrue(len(assignments) == 1) + self.assertEqual(1, len(assignments)) self.assertTrue(self.operator_cloud.revoke_role( role_name, user=user['id'], project='demo', wait=True)) assignments = self.operator_cloud.list_role_assignments({ @@ -160,7 +160,7 @@ def test_grant_revoke_role_user_project(self): 'project': self.operator_cloud.get_project('demo')['id'] }) self.assertIsInstance(assignments, list) - self.assertTrue(len(assignments) == 0) + self.assertEqual(0, len(assignments)) def test_grant_revoke_role_group_project(self): if self.identity_version in ('2', '2.0'): @@ -180,7 +180,7 @@ def test_grant_revoke_role_group_project(self): 'project': self.operator_cloud.get_project('demo')['id'] }) self.assertIsInstance(assignments, list) - self.assertTrue(len(assignments) == 1) + self.assertEqual(1, len(assignments)) self.assertTrue(self.operator_cloud.revoke_role( role_name, group=group['id'], project='demo')) assignments = self.operator_cloud.list_role_assignments({ @@ -189,7 +189,7 @@ def test_grant_revoke_role_group_project(self): 'project': self.operator_cloud.get_project('demo')['id'] }) self.assertIsInstance(assignments, list) - self.assertTrue(len(assignments) == 0) + self.assertEqual(0, len(assignments)) def test_grant_revoke_role_user_domain(self): if self.identity_version in ('2', '2.0'): @@ -209,7 +209,7 @@ def test_grant_revoke_role_user_domain(self): 'domain': self.operator_cloud.get_domain('default')['id'] }) self.assertIsInstance(assignments, list) - self.assertTrue(len(assignments) == 1) + self.assertEqual(1, len(assignments)) self.assertTrue(self.operator_cloud.revoke_role( role_name, user=user['id'], domain='default')) assignments = self.operator_cloud.list_role_assignments({ @@ -218,7 +218,7 @@ def test_grant_revoke_role_user_domain(self): 'domain': self.operator_cloud.get_domain('default')['id'] }) self.assertIsInstance(assignments, list) - self.assertTrue(len(assignments) == 0) + self.assertEqual(0, len(assignments)) def test_grant_revoke_role_group_domain(self): if self.identity_version in ('2', '2.0'): @@ -238,7 +238,7 @@ def test_grant_revoke_role_group_domain(self): 'domain': self.operator_cloud.get_domain('default')['id'] }) self.assertIsInstance(assignments, list) - self.assertTrue(len(assignments) == 1) + self.assertEqual(1, len(assignments)) self.assertTrue(self.operator_cloud.revoke_role( role_name, group=group['id'], domain='default')) assignments = self.operator_cloud.list_role_assignments({ @@ -247,4 +247,4 @@ def test_grant_revoke_role_group_domain(self): 'domain': self.operator_cloud.get_domain('default')['id'] }) self.assertIsInstance(assignments, list) - self.assertTrue(len(assignments) == 0) + self.assertEqual(0, len(assignments)) diff --git a/shade/tests/functional/test_port.py b/shade/tests/functional/test_port.py index 31eedfdf9..6331a75d3 100644 --- a/shade/tests/functional/test_port.py +++ b/shade/tests/functional/test_port.py @@ -69,7 +69,7 @@ def test_create_port(self): port = self.operator_cloud.create_port( network_id=networks[0]['id'], name=port_name) self.assertIsInstance(port, dict) - self.assertTrue('id' in port) + self.assertIn('id', port) self.assertEqual(port.get('name'), port_name) def test_get_port(self): @@ -82,7 +82,7 @@ def test_get_port(self): port = self.operator_cloud.create_port( network_id=networks[0]['id'], name=port_name) self.assertIsInstance(port, dict) - self.assertTrue('id' in port) + self.assertIn('id', port) self.assertEqual(port.get('name'), port_name) updated_port = self.operator_cloud.get_port(name_or_id=port['id']) @@ -121,7 +121,7 @@ def test_delete_port(self): port = self.operator_cloud.create_port( network_id=networks[0]['id'], name=port_name) self.assertIsInstance(port, dict) - self.assertTrue('id' in port) + self.assertIn('id', port) self.assertEqual(port.get('name'), port_name) updated_port = self.operator_cloud.get_port(name_or_id=port['id']) diff --git a/shade/tests/functional/test_range_search.py b/shade/tests/functional/test_range_search.py index dfcfbb712..15bf277df 100644 --- a/shade/tests/functional/test_range_search.py +++ b/shade/tests/functional/test_range_search.py @@ -50,7 +50,7 @@ def test_range_search_min(self): self.assertIsInstance(result, list) self.assertEqual(1, len(result)) # older devstack does not have cirros256 - self.assertTrue(result[0]['name'] in ('cirros256', 'm1.tiny')) + self.assertIn(result[0]['name'], ('cirros256', 'm1.tiny')) def test_range_search_max(self): flavors = self.user_cloud.list_flavors(get_extra=False) @@ -110,7 +110,7 @@ def test_range_search_multi_1(self): self.assertIsInstance(result, list) self.assertEqual(1, len(result)) # older devstack does not have cirros256 - self.assertTrue(result[0]['name'] in ('cirros256', 'm1.tiny')) + self.assertIn(result[0]['name'], ('cirros256', 'm1.tiny')) def test_range_search_multi_2(self): flavors = self.user_cloud.list_flavors(get_extra=False) diff --git a/shade/tests/unit/test_meta.py b/shade/tests/unit/test_meta.py index 65c1eca57..ae76ce877 100644 --- a/shade/tests/unit/test_meta.py +++ b/shade/tests/unit/test_meta.py @@ -778,7 +778,7 @@ def test_get_server_external_none_ipv4_neutron(self): ) ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) - self.assertEqual(None, ip) + self.assertIsNone(ip) self.assert_calls() def test_get_server_external_ipv4_neutron_accessIPv4(self): diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index 43b27b95e..f14333a5c 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -815,7 +815,7 @@ def test_set_machine_power_on(self, mock_client): mock_client.node.set_power_state.return_value = None node_id = 'node01' return_value = self.op_cloud.set_machine_power_on(node_id) - self.assertEqual(None, return_value) + self.assertIsNone(return_value) mock_client.node.set_power_state.assert_called_with( node_id='node01', state='on') @@ -825,7 +825,7 @@ def test_set_machine_power_off(self, mock_client): mock_client.node.set_power_state.return_value = None node_id = 'node01' return_value = self.op_cloud.set_machine_power_off(node_id) - self.assertEqual(None, return_value) + self.assertIsNone(return_value) mock_client.node.set_power_state.assert_called_with( node_id='node01', state='off') @@ -835,7 +835,7 @@ def test_set_machine_power_reboot(self, mock_client): mock_client.node.set_power_state.return_value = None node_id = 'node01' return_value = self.op_cloud.set_machine_power_reboot(node_id) - self.assertEqual(None, return_value) + self.assertIsNone(return_value) mock_client.node.set_power_state.assert_called_with( node_id='node01', state='reboot') @@ -1008,7 +1008,7 @@ def test_activate_node(self, mock_timeout, mock_client): return_value = self.op_cloud.activate_node( node_id, configdrive='http://127.0.0.1/file.iso') - self.assertEqual(None, return_value) + self.assertIsNone(return_value) mock_client.node.set_provision_state.assert_called_with( node_uuid='node02', state='active', @@ -1035,7 +1035,7 @@ class available_node_state(object): configdrive='http://127.0.0.1/file.iso', wait=True, timeout=2) - self.assertEqual(None, return_value) + self.assertIsNone(return_value) mock_client.node.set_provision_state.assert_called_with( node_uuid='node04', state='active', @@ -1049,7 +1049,7 @@ def test_deactivate_node(self, mock_timeout, mock_client): node_id = 'node03' return_value = self.op_cloud.deactivate_node( node_id, wait=False) - self.assertEqual(None, return_value) + self.assertIsNone(return_value) mock_client.node.set_provision_state.assert_called_with( node_uuid='node03', state='deleted', @@ -1073,7 +1073,7 @@ class deactivated_node_state(object): node_id = 'node03' return_value = self.op_cloud.deactivate_node( node_id, wait=True, timeout=2) - self.assertEqual(None, return_value) + self.assertIsNone(return_value) mock_client.node.set_provision_state.assert_called_with( node_uuid='node03', state='deleted', From 737f9de574b57f33d748368415df9822a48b44c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Tue, 1 Aug 2017 21:09:42 +0000 Subject: [PATCH 1725/3836] Add support for get details of available QoS rule type Neutron with QoS plugin enabled can return details about details of support specific rule type in existing cloud. This patch adds support for getting such details in shade. Change-Id: I95d10f67f9d1c83eb9adb17127d9d7bba766ad6e --- shade/openstackcloud.py | 24 +++++++ shade/tests/unit/test_qos_rule_type.py | 87 +++++++++++++++++++++++++- 2 files changed, 109 insertions(+), 2 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 0ba9bc131..a77e053fd 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1725,6 +1725,30 @@ def list_qos_rule_types(self, filters=None): error_message="Error fetching QoS rule types list") return self._get_and_munchify('rule_types', data) + def get_qos_rule_type_details(self, rule_type, filters=None): + """Get a QoS rule type details by rule type name. + + :param string rule_type: Name of the QoS rule type. + + :returns: A rule type details ``munch.Munch`` or None if + no matching rule type is found. + + """ + if not self._has_neutron_extension('qos'): + raise OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + if not self._has_neutron_extension('qos-rule-type-details'): + raise OpenStackCloudUnavailableExtension( + 'qos-rule-type-details extension is not available ' + 'on target cloud') + + data = self._network_client.get( + "/qos/rule-types/{rule_type}.json".format(rule_type=rule_type), + error_message="Error fetching QoS details of {rule_type} " + "rule type".format(rule_type=rule_type)) + return self._get_and_munchify('rule_type', data) + def list_qos_policies(self, filters=None): """List all available QoS policies. diff --git a/shade/tests/unit/test_qos_rule_type.py b/shade/tests/unit/test_qos_rule_type.py index d0142db1f..fd23e411c 100644 --- a/shade/tests/unit/test_qos_rule_type.py +++ b/shade/tests/unit/test_qos_rule_type.py @@ -19,6 +19,8 @@ class TestQosRuleType(base.RequestsMockTestCase): + rule_type_name = "bandwidth_limit" + qos_extension = { "updated": "2015-06-08T10:00:00-00:00", "name": "Quality of Service", @@ -26,18 +28,44 @@ class TestQosRuleType(base.RequestsMockTestCase): "alias": "qos", "description": "The Quality of Service extension." } + qos_rule_type_details_extension = { + "updated": "2017-06-22T10:00:00-00:00", + "name": "Details of QoS rule types", + "links": [], + "alias": "qos-rule-type-details", + "description": ("Expose details about QoS rule types supported by " + "loaded backend drivers") + } mock_rule_type_bandwidth_limit = { 'type': 'bandwidth_limit' } - mock_rule_type_dscp_marking = { 'type': 'dscp_marking' } - mock_rule_types = [ mock_rule_type_bandwidth_limit, mock_rule_type_dscp_marking] + mock_rule_type_details = { + 'drivers': [{ + 'name': 'linuxbridge', + 'supported_parameters': [{ + 'parameter_values': {'start': 0, 'end': 2147483647}, + 'parameter_type': 'range', + 'parameter_name': u'max_kbps' + }, { + 'parameter_values': ['ingress', 'egress'], + 'parameter_type': 'choices', + 'parameter_name': u'direction' + }, { + 'parameter_values': {'start': 0, 'end': 2147483647}, + 'parameter_type': 'range', + 'parameter_name': 'max_burst_kbps' + }] + }], + 'type': rule_type_name + } + def test_list_qos_rule_types(self): self.register_uris([ dict(method='GET', @@ -64,3 +92,58 @@ def test_list_qos_rule_types_no_qos_extension(self): self.assertRaises(exc.OpenStackCloudException, self.cloud.list_qos_rule_types) self.assert_calls() + + def test_get_qos_rule_type_details(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': [ + self.qos_extension, + self.qos_rule_type_details_extension]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': [ + self.qos_extension, + self.qos_rule_type_details_extension]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'rule-types', + '%s.json' % self.rule_type_name]), + json={'rule_type': self.mock_rule_type_details}) + ]) + self.assertEqual( + self.mock_rule_type_details, + self.cloud.get_qos_rule_type_details(self.rule_type_name) + ) + self.assert_calls() + + def test_get_qos_rule_type_details_no_qos_extension(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': []}) + ]) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.get_qos_rule_type_details, self.rule_type_name) + self.assert_calls() + + def test_get_qos_rule_type_details_no_qos_details_extension(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': [self.qos_extension]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': [self.qos_extension]}) + ]) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.get_qos_rule_type_details, self.rule_type_name) + self.assert_calls() From 2d547522572962760f2c7081e795a40694296f51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Tue, 1 Aug 2017 21:41:56 +0000 Subject: [PATCH 1726/3836] Use valid_kwargs decorator in QoS related functions This patch switches all QoS related functions to create/update policies and rules to use _utils.valid_kwargs() decorator instead of validating manually if each argument is None or not None. Change-Id: Ieea34d653e60a97963f7030424ea0f3138c169c2 --- shade/openstackcloud.py | 123 ++++++++++------------------ shade/tests/unit/test_qos_policy.py | 6 +- 2 files changed, 46 insertions(+), 83 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 0ba9bc131..f82ce7e6b 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3182,8 +3182,9 @@ def delete_network(self, name_or_id): return True - def create_qos_policy(self, name=None, description=None, shared=None, - default=None, project_id=None): + @_utils.valid_kwargs("name", "description", "shared", "default", + "project_id") + def create_qos_policy(self, **kwargs): """Create a QoS policy. :param string name: Name of the QoS policy being created. @@ -3199,28 +3200,22 @@ def create_qos_policy(self, name=None, description=None, shared=None, if not self._has_neutron_extension('qos'): raise OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') - policy = {} - if name: - policy['name'] = name - if description: - policy['description'] = description - if shared is not None: - policy['shared'] = shared + + default = kwargs.pop("default", None) if default is not None: if self._has_neutron_extension('qos-default'): - policy['is_default'] = default + kwargs['is_default'] = default else: self.log.debug("'qos-default' extension is not available on " "target cloud") - if project_id: - policy['project_id'] = project_id data = self._network_client.post("/qos/policies.json", - json={'policy': policy}) + json={'policy': kwargs}) return self._get_and_munchify('policy', data) - def update_qos_policy(self, name_or_id, policy_name=None, - description=None, shared=None, default=None): + @_utils.valid_kwargs("name", "description", "shared", "default", + "project_id") + def update_qos_policy(self, name_or_id, **kwargs): """Update an existing QoS policy. :param string name_or_id: @@ -3240,21 +3235,16 @@ def update_qos_policy(self, name_or_id, policy_name=None, if not self._has_neutron_extension('qos'): raise OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') - policy = {} - if policy_name: - policy['name'] = policy_name - if description: - policy['description'] = description - if shared is not None: - policy['shared'] = shared + + default = kwargs.pop("default", None) if default is not None: if self._has_neutron_extension('qos-default'): - policy['is_default'] = default + kwargs['is_default'] = default else: self.log.debug("'qos-default' extension is not available on " "target cloud") - if not policy: + if not kwargs: self.log.debug("No QoS policy data to update") return @@ -3266,7 +3256,7 @@ def update_qos_policy(self, name_or_id, policy_name=None, data = self._network_client.put( "/qos/policies/{policy_id}.json".format( policy_id=curr_policy['id']), - json={'policy': policy}) + json={'policy': kwargs}) return self._get_and_munchify('policy', data) def delete_qos_policy(self, name_or_id): @@ -3372,8 +3362,8 @@ def get_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): policy=policy['id'])) return self._get_and_munchify('bandwidth_limit_rule', data) - def create_qos_bandwidth_limit_rule(self, policy_name_or_id, max_kbps=None, - max_burst_kbps=None, direction=None): + @_utils.valid_kwargs("max_kbps", "max_burst_kbps", "direction") + def create_qos_bandwidth_limit_rule(self, policy_name_or_id, **kwargs): """Create a QoS bandwidth limit rule. :param string policy_name_or_id: Name or ID of the QoS policy to which @@ -3397,15 +3387,9 @@ def create_qos_bandwidth_limit_rule(self, policy_name_or_id, max_kbps=None, "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) - rule = {} - if max_kbps: - rule['max_kbps'] = max_kbps - if max_burst_kbps: - rule['max_burst_kbps'] = max_burst_kbps - if direction is not None: - if self._has_neutron_extension('qos-bw-limit-direction'): - rule['direction'] = direction - else: + if kwargs.get("direction") is not None: + if not self._has_neutron_extension('qos-bw-limit-direction'): + kwargs.pop("direction") self.log.debug( "'qos-bw-limit-direction' extension is not available on " "target cloud") @@ -3413,12 +3397,12 @@ def create_qos_bandwidth_limit_rule(self, policy_name_or_id, max_kbps=None, data = self._network_client.post( "/qos/policies/{policy_id}/bandwidth_limit_rules".format( policy_id=policy['id']), - json={'bandwidth_limit_rule': rule}) + json={'bandwidth_limit_rule': kwargs}) return self._get_and_munchify('bandwidth_limit_rule', data) + @_utils.valid_kwargs("max_kbps", "max_burst_kbps", "direction") def update_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id, - max_kbps=None, max_burst_kbps=None, - direction=None): + **kwargs): """Update a QoS bandwidth limit rule. :param string policy_name_or_id: Name or ID of the QoS policy to which @@ -3443,19 +3427,14 @@ def update_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id, "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) - rule = {} - if max_kbps: - rule['max_kbps'] = max_kbps - if max_burst_kbps: - rule['max_burst_kbps'] = max_burst_kbps - if direction is not None: - if self._has_neutron_extension('qos-bw-limit-direction'): - rule['direction'] = direction - else: + if kwargs.get("direction") is not None: + if not self._has_neutron_extension('qos-bw-limit-direction'): + kwargs.pop("direction") self.log.debug( "'qos-bw-limit-direction' extension is not available on " "target cloud") - if not rule: + + if not kwargs: self.log.debug("No QoS bandwidth limit rule data to update") return @@ -3470,7 +3449,7 @@ def update_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id, data = self._network_client.put( "/qos/policies/{policy_id}/bandwidth_limit_rules/{rule_id}.json". format(policy_id=policy['id'], rule_id=rule_id), - json={'bandwidth_limit_rule': rule}) + json={'bandwidth_limit_rule': kwargs}) return self._get_and_munchify('bandwidth_limit_rule', data) def delete_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): @@ -3586,7 +3565,8 @@ def get_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): policy=policy['id'])) return meta.get_and_munchify('dscp_marking_rule', data) - def create_qos_dscp_marking_rule(self, policy_name_or_id, dscp_mark=None): + @_utils.valid_kwargs("dscp_mark") + def create_qos_dscp_marking_rule(self, policy_name_or_id, **kwargs): """Create a QoS DSCP marking rule. :param string policy_name_or_id: Name or ID of the QoS policy to which @@ -3606,18 +3586,15 @@ def create_qos_dscp_marking_rule(self, policy_name_or_id, dscp_mark=None): "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) - rule = {} - if dscp_mark: - rule['dscp_mark'] = dscp_mark - data = self._network_client.post( "/qos/policies/{policy_id}/dscp_marking_rules".format( policy_id=policy['id']), - json={'dscp_marking_rule': rule}) + json={'dscp_marking_rule': kwargs}) return meta.get_and_munchify('dscp_marking_rule', data) + @_utils.valid_kwargs("dscp_mark") def update_qos_dscp_marking_rule(self, policy_name_or_id, rule_id, - dscp_mark=None): + **kwargs): """Update a QoS DSCP marking rule. :param string policy_name_or_id: Name or ID of the QoS policy to which @@ -3638,10 +3615,7 @@ def update_qos_dscp_marking_rule(self, policy_name_or_id, rule_id, "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) - rule = {} - if dscp_mark: - rule['dscp_mark'] = dscp_mark - if not rule: + if not kwargs: self.log.debug("No QoS DSCP marking rule data to update") return @@ -3656,7 +3630,7 @@ def update_qos_dscp_marking_rule(self, policy_name_or_id, rule_id, data = self._network_client.put( "/qos/policies/{policy_id}/dscp_marking_rules/{rule_id}.json". format(policy_id=policy['id'], rule_id=rule_id), - json={'dscp_marking_rule': rule}) + json={'dscp_marking_rule': kwargs}) return meta.get_and_munchify('dscp_marking_rule', data) def delete_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): @@ -3774,8 +3748,8 @@ def get_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): policy=policy['id'])) return self._get_and_munchify('minimum_bandwidth_rule', data) - def create_qos_minimum_bandwidth_rule(self, policy_name_or_id, - min_kbps=None, direction=None): + @_utils.valid_kwargs("min_kbps", "direction") + def create_qos_minimum_bandwidth_rule(self, policy_name_or_id, **kwargs): """Create a QoS minimum bandwidth limit rule. :param string policy_name_or_id: Name or ID of the QoS policy to which @@ -3797,20 +3771,15 @@ def create_qos_minimum_bandwidth_rule(self, policy_name_or_id, "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) - rule = {} - if min_kbps: - rule['min_kbps'] = min_kbps - if direction: - rule['direction'] = direction - data = self._network_client.post( "/qos/policies/{policy_id}/minimum_bandwidth_rules".format( policy_id=policy['id']), - json={'minimum_bandwidth_rule': rule}) + json={'minimum_bandwidth_rule': kwargs}) return self._get_and_munchify('minimum_bandwidth_rule', data) + @_utils.valid_kwargs("min_kbps", "direction") def update_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id, - min_kbps=None, direction=None): + **kwargs): """Update a QoS minimum bandwidth rule. :param string policy_name_or_id: Name or ID of the QoS policy to which @@ -3833,13 +3802,7 @@ def update_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id, "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) - rule = {} - if min_kbps: - rule['min_kbps'] = min_kbps - if direction: - rule['direction'] = direction - - if not rule: + if not kwargs: self.log.debug("No QoS minimum bandwidth rule data to update") return @@ -3854,7 +3817,7 @@ def update_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id, data = self._network_client.put( "/qos/policies/{policy_id}/minimum_bandwidth_rules/{rule_id}.json". format(policy_id=policy['id'], rule_id=rule_id), - json={'minimum_bandwidth_rule': rule}) + json={'minimum_bandwidth_rule': kwargs}) return self._get_and_munchify('minimum_bandwidth_rule', data) def delete_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): diff --git a/shade/tests/unit/test_qos_policy.py b/shade/tests/unit/test_qos_policy.py index de33b0fea..a0b327626 100644 --- a/shade/tests/unit/test_qos_policy.py +++ b/shade/tests/unit/test_qos_policy.py @@ -270,7 +270,7 @@ def test_update_qos_policy(self): json={'policy': {'name': 'goofy'}})) ]) policy = self.cloud.update_qos_policy( - self.policy_id, policy_name='goofy') + self.policy_id, name='goofy') self.assertDictEqual(expected_policy, policy) self.assert_calls() @@ -283,7 +283,7 @@ def test_update_qos_policy_no_qos_extension(self): ]) self.assertRaises( exc.OpenStackCloudException, - self.cloud.update_qos_policy, self.policy_id, policy_name="goofy") + self.cloud.update_qos_policy, self.policy_id, name="goofy") self.assert_calls() def test_update_qos_policy_no_qos_default_extension(self): @@ -317,6 +317,6 @@ def test_update_qos_policy_no_qos_default_extension(self): json={'policy': {'name': "goofy"}})) ]) policy = self.cloud.update_qos_policy( - self.policy_id, policy_name='goofy', default=True) + self.policy_id, name='goofy', default=True) self.assertDictEqual(expected_policy, policy) self.assert_calls() From 697cf58c826c56ee1599d23ec1271933e8aae54b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Fri, 4 Aug 2017 19:19:58 +0000 Subject: [PATCH 1727/3836] Make QoS rules required parameters to be not optional Some of QoS rules parameters, like max_kbps for bandwidth limit rule, dscp_mark for dscp marking rule or min_kbps for minimum bandwidth are not optional on Neutron's side so it shouldn't be optional in shade client. This commit changes them to be required in create methods. Change-Id: I6919690d5fe024698dc76bf9808ec8ada80d2ae8 --- shade/openstackcloud.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index a0260c7a6..53b6b631c 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3386,8 +3386,9 @@ def get_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): policy=policy['id'])) return self._get_and_munchify('bandwidth_limit_rule', data) - @_utils.valid_kwargs("max_kbps", "max_burst_kbps", "direction") - def create_qos_bandwidth_limit_rule(self, policy_name_or_id, **kwargs): + @_utils.valid_kwargs("max_burst_kbps", "direction") + def create_qos_bandwidth_limit_rule(self, policy_name_or_id, max_kbps, + **kwargs): """Create a QoS bandwidth limit rule. :param string policy_name_or_id: Name or ID of the QoS policy to which @@ -3418,6 +3419,7 @@ def create_qos_bandwidth_limit_rule(self, policy_name_or_id, **kwargs): "'qos-bw-limit-direction' extension is not available on " "target cloud") + kwargs['max_kbps'] = max_kbps data = self._network_client.post( "/qos/policies/{policy_id}/bandwidth_limit_rules".format( policy_id=policy['id']), @@ -3589,8 +3591,7 @@ def get_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): policy=policy['id'])) return meta.get_and_munchify('dscp_marking_rule', data) - @_utils.valid_kwargs("dscp_mark") - def create_qos_dscp_marking_rule(self, policy_name_or_id, **kwargs): + def create_qos_dscp_marking_rule(self, policy_name_or_id, dscp_mark): """Create a QoS DSCP marking rule. :param string policy_name_or_id: Name or ID of the QoS policy to which @@ -3610,10 +3611,13 @@ def create_qos_dscp_marking_rule(self, policy_name_or_id, **kwargs): "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) + body = { + 'dscp_mark': dscp_mark + } data = self._network_client.post( "/qos/policies/{policy_id}/dscp_marking_rules".format( policy_id=policy['id']), - json={'dscp_marking_rule': kwargs}) + json={'dscp_marking_rule': body}) return meta.get_and_munchify('dscp_marking_rule', data) @_utils.valid_kwargs("dscp_mark") @@ -3772,8 +3776,9 @@ def get_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): policy=policy['id'])) return self._get_and_munchify('minimum_bandwidth_rule', data) - @_utils.valid_kwargs("min_kbps", "direction") - def create_qos_minimum_bandwidth_rule(self, policy_name_or_id, **kwargs): + @_utils.valid_kwargs("direction") + def create_qos_minimum_bandwidth_rule(self, policy_name_or_id, min_kbps, + **kwargs): """Create a QoS minimum bandwidth limit rule. :param string policy_name_or_id: Name or ID of the QoS policy to which @@ -3795,6 +3800,7 @@ def create_qos_minimum_bandwidth_rule(self, policy_name_or_id, **kwargs): "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) + kwargs['min_kbps'] = min_kbps data = self._network_client.post( "/qos/policies/{policy_id}/minimum_bandwidth_rules".format( policy_id=policy['id']), From eed1cbb8cdbb9a1214277b5d38a4b75123ec14ef Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Sat, 5 Aug 2017 18:52:47 +0200 Subject: [PATCH 1728/3836] Remove OSIC OSIC has been decommissioned, remove the now useless vendor data. Change-Id: I57c6043018e96c0069c7db777b9f585cb7d535e7 Related-Change: I2d1b0710e875bd1ebc305fb5b184b68bf18f2ef7 --- os_client_config/vendors/osic.json | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 os_client_config/vendors/osic.json diff --git a/os_client_config/vendors/osic.json b/os_client_config/vendors/osic.json deleted file mode 100644 index 484d7111b..000000000 --- a/os_client_config/vendors/osic.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "osic", - "profile": { - "auth": { - "auth_url": "https://cloud1.osic.org:5000" - }, - "regions": [ - "RegionOne" - ] - } -} From 240e2594b969fee06be5e5a38e414e6a12d5ba6e Mon Sep 17 00:00:00 2001 From: lingyongxu Date: Mon, 7 Aug 2017 15:10:02 +0800 Subject: [PATCH 1729/3836] Update the documentation link for doc migration This patch is proposed according to the Direction 10 of doc migration(https://etherpad.openstack.org/p/doc-migration-tracking). Change-Id: Ida458338d353cbd6cc0162263db25f533b0bd9fd --- HACKING.rst | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/HACKING.rst b/HACKING.rst index c995c5c92..aab08e34e 100644 --- a/HACKING.rst +++ b/HACKING.rst @@ -1,4 +1,4 @@ os-client-config Style Commandments =============================================== -Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/ \ No newline at end of file +Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest diff --git a/setup.cfg b/setup.cfg index 19548fb75..7149cdf8e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,7 @@ description-file = README.rst author = OpenStack author-email = openstack-dev@lists.openstack.org -home-page = http://docs.openstack.org/developer/os-client-config/ +home-page = https://docs.openstack.org/os-client-config/latest classifier = Environment :: OpenStack Intended Audience :: Information Technology From d597ee271e1085e10f89fa9632491b3647ed95b1 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Mon, 7 Aug 2017 15:37:37 -0700 Subject: [PATCH 1730/3836] Update globals safely The right way to update these globals is to use a lock and ensure that nobody else is updating them at the same time. Also update a temporary dictionary before setting the global one so that nobody sees partial updates to the global one. This should help fix the thread-safety of shade (and other tooling built ontop of this library). Change-Id: Ie0e0369d98ba6a01edcbf447378a786eec3f13f9 --- os_client_config/constructors.py | 14 +++++++++++--- os_client_config/defaults.py | 21 ++++++++++++++++----- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/os_client_config/constructors.py b/os_client_config/constructors.py index e88ac92d6..579bb2d5e 100644 --- a/os_client_config/constructors.py +++ b/os_client_config/constructors.py @@ -14,15 +14,23 @@ import json import os +import threading _json_path = os.path.join( os.path.dirname(os.path.realpath(__file__)), 'constructors.json') _class_mapping = None +_class_mapping_lock = threading.Lock() def get_constructor_mapping(): global _class_mapping - if not _class_mapping: + if _class_mapping is not None: + return _class_mapping.copy() + with _class_mapping_lock: + if _class_mapping is not None: + return _class_mapping.copy() + tmp_class_mapping = {} with open(_json_path, 'r') as json_file: - _class_mapping = json.load(json_file) - return _class_mapping + tmp_class_mapping.update(json.load(json_file)) + _class_mapping = tmp_class_mapping + return tmp_class_mapping.copy() diff --git a/os_client_config/defaults.py b/os_client_config/defaults.py index c10358a10..1231cce92 100644 --- a/os_client_config/defaults.py +++ b/os_client_config/defaults.py @@ -14,19 +14,30 @@ import json import os +import threading _json_path = os.path.join( os.path.dirname(os.path.realpath(__file__)), 'defaults.json') _defaults = None +_defaults_lock = threading.Lock() def get_defaults(): global _defaults - if not _defaults: + if _defaults is not None: + return _defaults.copy() + with _defaults_lock: + if _defaults is not None: + # Did someone else just finish filling it? + return _defaults.copy() # Python language specific defaults # These are defaults related to use of python libraries, they are # not qualities of a cloud. - _defaults = dict( + # + # NOTE(harlowja): update a in-memory dict, before updating + # the global one so that other callers of get_defaults do not + # see the partially filled one. + tmp_defaults = dict( api_timeout=None, verify=True, cacert=None, @@ -36,6 +47,6 @@ def get_defaults(): with open(_json_path, 'r') as json_file: updates = json.load(json_file) if updates is not None: - _defaults.update(updates) - - return _defaults.copy() + tmp_defaults.update(updates) + _defaults = tmp_defaults + return tmp_defaults.copy() From 20ebacce1493e00410aeac4fbe0460dbc8b39817 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 10 Aug 2017 15:56:20 -0500 Subject: [PATCH 1731/3836] Make get_server_console tests more resilient The current tests can fail if the cloud just doesn't return real content. That's not what we want to test though - we want to test that the shade method calls the remote method and gets a result. Change-Id: I2238598c07d5ec51bd11ab019cf3f666ee2eaf7d --- shade/openstackcloud.py | 9 ++++++--- shade/tests/functional/test_compute.py | 19 +++++++------------ 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index a0260c7a6..8e17c371b 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2885,12 +2885,15 @@ def get_server_console(self, server, length=None): "Console log requested for invalid server") try: + return self._get_server_console_output(server['id'], length) + except OpenStackCloudBadRequest: + return "" + + def _get_server_console_output(self, server_id, length=None): data = self._compute_client.post( - '/servers/{server}/action'.format(server=server['id']), + '/servers/{server_id}/action'.format(server_id=server_id), json={'os-getConsoleOutput': {'length': length}}) return self._get_and_munchify('output', data) - except OpenStackCloudBadRequest: - return "" def get_server( self, name_or_id=None, filters=None, detailed=False, bare=False): diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index 938365b73..2f5191cb8 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -183,12 +183,11 @@ def test_get_server_console(self): image=self.image, flavor=self.flavor, wait=True) - for _ in _utils._iterate_timeout( - 5, "Did not get more than 0 lines in the console log"): - log = self.user_cloud.get_server_console(server=server) - self.assertTrue(isinstance(log, six.string_types)) - if len(log) > 0: - break + # _get_server_console_output does not trap HTTP exceptions, so this + # returning a string tests that the call is correct. Testing that + # the cloud returns actual data in the output is out of scope. + log = self.user_cloud._get_server_console_output(server_id=server.id) + self.assertTrue(isinstance(log, six.string_types)) def test_get_server_console_name_or_id(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) @@ -197,12 +196,8 @@ def test_get_server_console_name_or_id(self): image=self.image, flavor=self.flavor, wait=True) - for _ in _utils._iterate_timeout( - 5, "Did not get more than 0 lines in the console log"): - log = self.user_cloud.get_server_console(server=self.server_name) - self.assertTrue(isinstance(log, six.string_types)) - if len(log) > 0: - break + log = self.user_cloud.get_server_console(server=self.server_name) + self.assertTrue(isinstance(log, six.string_types)) def test_list_availability_zone_names(self): self.assertEqual( From 4e5d46df7661051db37b48d84ffc5c096be63a55 Mon Sep 17 00:00:00 2001 From: Rui Chen Date: Wed, 9 Aug 2017 18:02:37 +0800 Subject: [PATCH 1732/3836] Support to get resource by id For finding specified resource, we list all the resources in get_xxx(name_or_id) method, then find the result in list result loop, that might have poor performance and slow response when there are many resources in a project. OpenStack API support to get resource by id directly, and part of them had been implemented in Shade, like: get_server_by_id(), get_user_by_id(), get_volume_snapshot_by_id. The patch aims to support more: - flavor - image - volume - network - subnet - port - floatingip - security group - ... Change-Id: Icb0af21c2d7e8bda07c072cc6098269304b7ab88 Closes-Bug: #1709577 --- shade/openstackcloud.py | 153 ++++++++++++++++++ shade/tests/functional/test_flavor.py | 2 +- shade/tests/functional/test_floating_ip.py | 7 + shade/tests/functional/test_image.py | 20 +++ shade/tests/functional/test_network.py | 15 ++ shade/tests/functional/test_port.py | 19 +++ .../tests/functional/test_security_groups.py | 7 + shade/tests/functional/test_volume.py | 3 + shade/tests/unit/test_flavors.py | 20 +++ shade/tests/unit/test_floating_ip_neutron.py | 23 +++ shade/tests/unit/test_floating_ip_nova.py | 14 ++ shade/tests/unit/test_image.py | 12 ++ shade/tests/unit/test_network.py | 14 ++ shade/tests/unit/test_port.py | 15 ++ shade/tests/unit/test_security_groups.py | 33 ++++ shade/tests/unit/test_subnet.py | 14 ++ shade/tests/unit/test_volume.py | 15 ++ 17 files changed, 385 insertions(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 53b6b631c..ff956353d 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -2660,6 +2660,20 @@ def get_network(self, name_or_id, filters=None): """ return _utils._get_entity(self.search_networks, name_or_id, filters) + def get_network_by_id(self, id): + """ Get a network by ID + + :param id: ID of the network. + :returns: A network ``munch.Munch``. + """ + data = self._network_client.get( + '/networks/{id}'.format(id=id), + error_message="Error getting network with ID {id}".format(id=id) + ) + network = self._get_and_munchify('network', data) + + return network + def get_router(self, name_or_id, filters=None): """Get a router by name or ID. @@ -2706,6 +2720,20 @@ def get_subnet(self, name_or_id, filters=None): """ return _utils._get_entity(self.search_subnets, name_or_id, filters) + def get_subnet_by_id(self, id): + """ Get a subnet by ID + + :param id: ID of the subnet. + :returns: A subnet ``munch.Munch``. + """ + data = self._network_client.get( + '/subnets/{id}'.format(id=id), + error_message="Error getting subnet with ID {id}".format(id=id) + ) + subnet = self._get_and_munchify('subnet', data) + + return subnet + def get_port(self, name_or_id, filters=None): """Get a port by name or ID. @@ -2730,6 +2758,20 @@ def get_port(self, name_or_id, filters=None): """ return _utils._get_entity(self.search_ports, name_or_id, filters) + def get_port_by_id(self, id): + """ Get a port by ID + + :param id: ID of the port. + :returns: A port ``munch.Munch``. + """ + data = self._network_client.get( + '/ports/{id}'.format(id=id), + error_message="Error getting port with ID {id}".format(id=id) + ) + port = self._get_and_munchify('port', data) + + return port + def get_qos_policy(self, name_or_id, filters=None): """Get a QoS policy by name or ID. @@ -2781,6 +2823,21 @@ def get_volume(self, name_or_id, filters=None): """ return _utils._get_entity(self.search_volumes, name_or_id, filters) + def get_volume_by_id(self, id): + """ Get a volume by ID + + :param id: ID of the volume. + :returns: A volume ``munch.Munch``. + """ + data = self._volume_client.get( + '/volumes/{id}'.format(id=id), + error_message="Error getting volume with ID {id}".format(id=id) + ) + volume = self._normalize_volume( + self._get_and_munchify('volume', data)) + + return volume + def get_volume_type(self, name_or_id, filters=None): """Get a volume type by name or ID. @@ -2837,6 +2894,42 @@ def get_flavor(self, name_or_id, filters=None, get_extra=True): self.search_flavors, get_extra=get_extra) return _utils._get_entity(search_func, name_or_id, filters) + def get_flavor_by_id(self, id, get_extra=True): + """ Get a flavor by ID + + :param id: ID of the flavor. + :param get_extra: + Whether or not the list_flavors call should get the extra flavor + specs. + :returns: A flavor ``munch.Munch``. + """ + data = self._compute_client.get( + '/flavors/{id}'.format(id=id), + error_message="Error getting flavor with ID {id}".format(id=id) + ) + flavor = self._normalize_flavor( + self._get_and_munchify('flavor', data)) + + if get_extra is None: + get_extra = self._extra_config['get_flavor_extra_specs'] + + if not flavor.extra_specs and get_extra: + endpoint = "/flavors/{id}/os-extra_specs".format( + id=flavor.id) + try: + data = self._compute_client.get( + endpoint, + error_message="Error fetching flavor extra specs") + flavor.extra_specs = self._get_and_munchify( + 'extra_specs', data) + except OpenStackCloudHTTPError as e: + flavor.extra_specs = {} + self.log.debug( + 'Fetching extra specs for flavor failed:' + ' %(msg)s', {'msg': str(e)}) + + return flavor + def get_security_group(self, name_or_id, filters=None): """Get a security group by name or ID. @@ -2863,6 +2956,29 @@ def get_security_group(self, name_or_id, filters=None): return _utils._get_entity( self.search_security_groups, name_or_id, filters) + def get_security_group_by_id(self, id): + """ Get a security group by ID + + :param id: ID of the security group. + :returns: A security group ``munch.Munch``. + """ + if not self._has_secgroups(): + raise OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + error_message = ("Error getting security group with" + " ID {id}".format(id=id)) + if self._use_neutron_secgroups(): + data = self._network_client.get( + '/security-groups/{id}'.format(id=id), + error_message=error_message) + else: + data = self._compute_client.get( + '/os-security-groups/{id}'.format(id=id), + error_message=error_message) + return self._normalize_secgroup( + self._get_and_munchify('security_group', data)) + def get_server_console(self, server, length=None): """Get the console log for a server. @@ -2988,6 +3104,22 @@ def get_image(self, name_or_id, filters=None): """ return _utils._get_entity(self.search_images, name_or_id, filters) + def get_image_by_id(self, id): + """ Get a image by ID + + :param id: ID of the image. + :returns: An image ``munch.Munch``. + """ + data = self._image_client.get( + '/images/{id}'.format(id=id), + error_message="Error getting image with ID {id}".format(id=id) + ) + key = 'image' if 'image' in data else None + image = self._normalize_image( + self._get_and_munchify(key, data)) + + return image + def download_image( self, name_or_id, output_path=None, output_file=None, chunk_size=1024): @@ -3063,6 +3195,27 @@ def get_floating_ip(self, id, filters=None): """ return _utils._get_entity(self.search_floating_ips, id, filters) + def get_floating_ip_by_id(self, id): + """ Get a floating ip by ID + + :param id: ID of the floating ip. + :returns: A floating ip ``munch.Munch``. + """ + error_message = "Error getting floating ip with ID {id}".format(id=id) + + if self._use_neutron_floating(): + data = self._network_client.get( + '/floatingips/{id}'.format(id=id), + error_message=error_message) + return self._normalize_floating_ip( + self._get_and_munchify('floatingip', data)) + else: + data = self._compute_client.get( + '/os-floating-ips/{id}'.format(id=id), + error_message=error_message) + return self._normalize_floating_ip( + self._get_and_munchify('floating_ip', data)) + def get_stack(self, name_or_id, filters=None): """Get exactly one stack. diff --git a/shade/tests/functional/test_flavor.py b/shade/tests/functional/test_flavor.py index e149e0118..71d176f72 100644 --- a/shade/tests/functional/test_flavor.py +++ b/shade/tests/functional/test_flavor.py @@ -165,7 +165,7 @@ def test_set_unset_flavor_specs(self): # Unset the 'foo' value self.operator_cloud.unset_flavor_specs(mod_flavor['id'], ['foo']) - mod_flavor = self.operator_cloud.get_flavor(new_flavor['id']) + mod_flavor = self.operator_cloud.get_flavor_by_id(new_flavor['id']) # Verify 'foo' is unset and 'bar' is still set self.assertEqual({'bar': 'bbb'}, mod_flavor['extra_specs']) diff --git a/shade/tests/functional/test_floating_ip.py b/shade/tests/functional/test_floating_ip.py index f739657fb..e74a23f5a 100644 --- a/shade/tests/functional/test_floating_ip.py +++ b/shade/tests/functional/test_floating_ip.py @@ -275,3 +275,10 @@ def test_search_floating_ips(self): [fip.id for fip in self.user_cloud.search_floating_ips( filters={"attached": True})] ) + + def test_get_floating_ip_by_id(self): + fip_user = self.user_cloud.create_floating_ip() + self.addCleanup(self.user_cloud.delete_floating_ip, fip_user.id) + + ret_fip = self.user_cloud.get_floating_ip_by_id(fip_user.id) + self.assertEqual(fip_user, ret_fip) diff --git a/shade/tests/functional/test_image.py b/shade/tests/functional/test_image.py index 8de0b9fc4..960214ba1 100644 --- a/shade/tests/functional/test_image.py +++ b/shade/tests/functional/test_image.py @@ -148,3 +148,23 @@ def test_create_image_update_properties(self): self.assertEqual(image.properties['foo'], 'bar') finally: self.user_cloud.delete_image(image_name, wait=True) + + def test_get_image_by_id(self): + test_image = tempfile.NamedTemporaryFile(delete=False) + test_image.write(b'\0' * 1024 * 1024) + test_image.close() + image_name = self.getUniqueString('image') + try: + image = self.user_cloud.create_image( + name=image_name, + filename=test_image.name, + disk_format='raw', + container_format='bare', + min_disk=10, + min_ram=1024, + wait=True) + image = self.user_cloud.get_image_by_id(image.id) + self.assertEqual(image_name, image.name) + self.assertEqual('raw', image.disk_format) + finally: + self.user_cloud.delete_image(image_name, wait=True) diff --git a/shade/tests/functional/test_network.py b/shade/tests/functional/test_network.py index a50692747..14e5776d5 100644 --- a/shade/tests/functional/test_network.py +++ b/shade/tests/functional/test_network.py @@ -50,6 +50,21 @@ def test_create_network_basic(self): self.assertFalse(net1['router:external']) self.assertTrue(net1['admin_state_up']) + def test_get_network_by_id(self): + net1 = self.operator_cloud.create_network(name=self.network_name) + self.assertIn('id', net1) + self.assertEqual(self.network_name, net1['name']) + self.assertFalse(net1['shared']) + self.assertFalse(net1['router:external']) + self.assertTrue(net1['admin_state_up']) + + ret_net1 = self.operator_cloud.get_network_by_id(net1.id) + self.assertIn('id', ret_net1) + self.assertEqual(self.network_name, ret_net1['name']) + self.assertFalse(ret_net1['shared']) + self.assertFalse(ret_net1['router:external']) + self.assertTrue(ret_net1['admin_state_up']) + def test_create_network_advanced(self): net1 = self.operator_cloud.create_network( name=self.network_name, diff --git a/shade/tests/functional/test_port.py b/shade/tests/functional/test_port.py index 6331a75d3..a824a2fe3 100644 --- a/shade/tests/functional/test_port.py +++ b/shade/tests/functional/test_port.py @@ -91,6 +91,25 @@ def test_get_port(self): del updated_port['extra_dhcp_opts'] self.assertEqual(port, updated_port) + def test_get_port_by_id(self): + port_name = self.new_port_name + '_get_by_id' + + networks = self.operator_cloud.list_networks() + if not networks: + self.assertFalse('no sensible network available') + + port = self.operator_cloud.create_port( + network_id=networks[0]['id'], name=port_name) + self.assertIsInstance(port, dict) + self.assertIn('id', port) + self.assertEqual(port.get('name'), port_name) + + updated_port = self.operator_cloud.get_port_by_id(port['id']) + # extra_dhcp_opts is added later by Neutron... + if 'extra_dhcp_opts' in updated_port and 'extra_dhcp_opts' not in port: + del updated_port['extra_dhcp_opts'] + self.assertEqual(port, updated_port) + def test_update_port(self): port_name = self.new_port_name + '_update' new_port_name = port_name + '_new' diff --git a/shade/tests/functional/test_security_groups.py b/shade/tests/functional/test_security_groups.py index 97819c7a0..de9c05f99 100644 --- a/shade/tests/functional/test_security_groups.py +++ b/shade/tests/functional/test_security_groups.py @@ -50,3 +50,10 @@ def test_create_list_security_groups(self): sg_list = self.operator_cloud.list_security_groups( filters={'all_tenants': 1}) self.assertIn(sg1['id'], [sg['id'] for sg in sg_list]) + + def test_get_security_group_by_id(self): + sg = self.user_cloud.create_security_group(name='sg', description='sg') + self.addCleanup(self.user_cloud.delete_security_group, sg['id']) + + ret_sg = self.user_cloud.get_security_group_by_id(sg['id']) + self.assertEqual(sg, ret_sg) diff --git a/shade/tests/functional/test_volume.py b/shade/tests/functional/test_volume.py index c94577254..bd6302fc8 100644 --- a/shade/tests/functional/test_volume.py +++ b/shade/tests/functional/test_volume.py @@ -46,6 +46,9 @@ def test_volumes(self): display_name=snapshot_name ) + ret_volume = self.user_cloud.get_volume_by_id(volume['id']) + self.assertEqual(volume['id'], ret_volume['id']) + volume_ids = [v['id'] for v in self.user_cloud.list_volumes()] self.assertIn(volume['id'], volume_ids) diff --git a/shade/tests/unit/test_flavors.py b/shade/tests/unit/test_flavors.py index a01de4028..5fe7b7abf 100644 --- a/shade/tests/unit/test_flavors.py +++ b/shade/tests/unit/test_flavors.py @@ -236,3 +236,23 @@ def test_list_flavor_access(self): ]) self.op_cloud.list_flavor_access('vanilla') self.assert_calls() + + def test_get_flavor_by_id(self): + flavor_uri = '{endpoint}/flavors/1'.format( + endpoint=fakes.COMPUTE_ENDPOINT) + flavor_extra_uri = '{endpoint}/flavors/1/os-extra_specs'.format( + endpoint=fakes.COMPUTE_ENDPOINT) + flavor_json = {'flavor': fakes.make_fake_flavor('1', 'vanilla')} + flavor_extra_json = {'extra_specs': {'name': 'test'}} + + self.register_uris([ + dict(method='GET', uri=flavor_uri, json=flavor_json), + dict(method='GET', uri=flavor_extra_uri, json=flavor_extra_json), + ]) + + flavor1 = self.cloud.get_flavor_by_id('1') + self.assertEqual('1', flavor1['id']) + self.assertEqual({'name': 'test'}, flavor1.extra_specs) + flavor2 = self.cloud.get_flavor_by_id('1', get_extra=False) + self.assertEqual('1', flavor2['id']) + self.assertEqual({}, flavor2.extra_specs) diff --git a/shade/tests/unit/test_floating_ip_neutron.py b/shade/tests/unit/test_floating_ip_neutron.py index 555f47289..9ab437fda 100644 --- a/shade/tests/unit/test_floating_ip_neutron.py +++ b/shade/tests/unit/test_floating_ip_neutron.py @@ -235,6 +235,29 @@ def test_get_floating_ip_not_found(self): self.assertIsNone(floating_ip) self.assert_calls() + def test_get_floating_ip_by_id(self): + fid = self.mock_floating_ip_new_rep['floatingip']['id'] + self.register_uris([ + dict(method='GET', + uri='https://network.example.com/v2.0/floatingips/' + '{id}'.format(id=fid), + json=self.mock_floating_ip_new_rep)]) + + floating_ip = self.cloud.get_floating_ip_by_id(id=fid) + + self.assertIsInstance(floating_ip, dict) + self.assertEqual('172.24.4.229', floating_ip['floating_ip_address']) + self.assertEqual( + self.mock_floating_ip_new_rep['floatingip']['tenant_id'], + floating_ip['project_id'] + ) + self.assertEqual( + self.mock_floating_ip_new_rep['floatingip']['tenant_id'], + floating_ip['tenant_id'] + ) + self.assertIn('location', floating_ip) + self.assert_calls() + def test_create_floating_ip(self): self.register_uris([ dict(method='GET', diff --git a/shade/tests/unit/test_floating_ip_nova.py b/shade/tests/unit/test_floating_ip_nova.py index c86321b40..492d142ce 100644 --- a/shade/tests/unit/test_floating_ip_nova.py +++ b/shade/tests/unit/test_floating_ip_nova.py @@ -142,6 +142,20 @@ def test_get_floating_ip_not_found(self): self.assert_calls() + def test_get_floating_ip_by_id(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url('compute', append=['os-floating-ips', + '1']), + json={'floating_ip': self.mock_floating_ip_list_rep[0]}), + ]) + + floating_ip = self.cloud.get_floating_ip_by_id(id='1') + + self.assertIsInstance(floating_ip, dict) + self.assertEqual('203.0.113.1', floating_ip['floating_ip_address']) + self.assert_calls() + def test_create_floating_ip(self): self.register_uris([ dict(method='POST', diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index ba07d8918..54e56ab61 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -672,6 +672,18 @@ def test_create_image_put_user_prop( self.assertEqual( self._munch_images(ret), self.cloud.list_images()) + def test_get_image_by_id(self): + self.register_uris([ + dict(method='GET', + uri='https://image.example.com/v2/images/{id}'.format( + id=self.image_id), + json=self.fake_image_dict) + ]) + self.assertEqual( + self.cloud._normalize_image(self.fake_image_dict), + self.cloud.get_image_by_id(self.image_id)) + self.assert_calls() + class TestImageV1Only(base.RequestsMockTestCase): diff --git a/shade/tests/unit/test_network.py b/shade/tests/unit/test_network.py index 5893d8b4e..96af721bf 100644 --- a/shade/tests/unit/test_network.py +++ b/shade/tests/unit/test_network.py @@ -234,3 +234,17 @@ def test_delete_network_exception(self): self.assertRaises(shade.OpenStackCloudException, self.cloud.delete_network, network_name) self.assert_calls() + + def test_get_network_by_id(self): + network_id = "test-net-id" + network_name = "network" + network = {'id': network_id, 'name': network_name} + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'networks', "%s" % network_id]), + json={'network': network}) + ]) + self.assertTrue(self.cloud.get_network_by_id(network_id)) + self.assert_calls() diff --git a/shade/tests/unit/test_port.py b/shade/tests/unit/test_port.py index 4fd2ad4e0..4ef52a2e3 100644 --- a/shade/tests/unit/test_port.py +++ b/shade/tests/unit/test_port.py @@ -346,3 +346,18 @@ def test_delete_subnet_multiple_using_id(self): ]) self.assertTrue(self.cloud.delete_port(name_or_id=port1['id'])) self.assert_calls() + + def test_get_port_by_id(self): + fake_port = dict(id='123', name='456') + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', + 'ports', + fake_port['id']]), + json={'port': fake_port}) + ]) + r = self.cloud.get_port_by_id(fake_port['id']) + self.assertIsNotNone(r) + self.assertDictEqual(fake_port, r) + self.assert_calls() diff --git a/shade/tests/unit/test_security_groups.py b/shade/tests/unit/test_security_groups.py index ca9db074d..e3485c8fa 100644 --- a/shade/tests/unit/test_security_groups.py +++ b/shade/tests/unit/test_security_groups.py @@ -749,3 +749,36 @@ def test_add_security_group_to_bad_server(self): self.assertFalse(ret) self.assert_calls() + + def test_get_security_group_by_id_neutron(self): + self.cloud.secgroup_source = 'neutron' + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', + 'security-groups', + neutron_grp_dict['id']]), + json={'security_group': neutron_grp_dict}) + ]) + ret_sg = self.cloud.get_security_group_by_id(neutron_grp_dict['id']) + self.assertEqual(neutron_grp_dict['id'], ret_sg['id']) + self.assertEqual(neutron_grp_dict['name'], ret_sg['name']) + self.assertEqual(neutron_grp_dict['description'], + ret_sg['description']) + self.assert_calls() + + def test_get_security_group_by_id_nova(self): + self.register_uris([ + dict(method='GET', + uri='{endpoint}/os-security-groups/{id}'.format( + endpoint=fakes.COMPUTE_ENDPOINT, + id=nova_grp_dict['id']), + json={'security_group': nova_grp_dict}), + ]) + self.cloud.secgroup_source = 'nova' + self.has_neutron = False + ret_sg = self.cloud.get_security_group_by_id(nova_grp_dict['id']) + self.assertEqual(nova_grp_dict['id'], ret_sg['id']) + self.assertEqual(nova_grp_dict['name'], ret_sg['name']) + self.assert_calls() diff --git a/shade/tests/unit/test_subnet.py b/shade/tests/unit/test_subnet.py index 815c4af42..d52c7dee0 100644 --- a/shade/tests/unit/test_subnet.py +++ b/shade/tests/unit/test_subnet.py @@ -69,6 +69,20 @@ def test_get_subnet(self): self.assertDictEqual(self.mock_subnet_rep, r) self.assert_calls() + def test_get_subnet_by_id(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', + 'subnets', + self.subnet_id]), + json={'subnet': self.mock_subnet_rep}) + ]) + r = self.cloud.get_subnet_by_id(self.subnet_id) + self.assertIsNotNone(r) + self.assertDictEqual(self.mock_subnet_rep, r) + self.assert_calls() + def test_create_subnet(self): pool = [{'start': '192.168.199.2', 'end': '192.168.199.254'}] dns = ['8.8.8.8'] diff --git a/shade/tests/unit/test_volume.py b/shade/tests/unit/test_volume.py index c4ff2aa10..f00875055 100644 --- a/shade/tests/unit/test_volume.py +++ b/shade/tests/unit/test_volume.py @@ -412,3 +412,18 @@ def test_list_volumes_with_pagination_next_link_fails_all_attempts(self): [self.cloud._normalize_volume(vol1)], self.cloud.list_volumes()) self.assert_calls() + + def test_get_volume_by_id(self): + vol1 = meta.obj_to_munch(fakes.FakeVolume('01', 'available', 'vol1')) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['volumes', '01']), + json={'volume': vol1} + ) + ]) + self.assertEqual( + self.cloud._normalize_volume(vol1), + self.cloud.get_volume_by_id('01')) + self.assert_calls() From 164501c71570eac15c7be55d0400c400e502bc8b Mon Sep 17 00:00:00 2001 From: Sean Handley Date: Fri, 11 Aug 2017 14:12:12 +0100 Subject: [PATCH 1733/3836] DataCentred supports Keystone V3 and Glance V2. Change-Id: Ia8c656e2c6b97c877f5028fef8a94a2c41909bc5 --- os_client_config/vendors/datacentred.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/os_client_config/vendors/datacentred.json b/os_client_config/vendors/datacentred.json index 2be4a5863..e67d3da72 100644 --- a/os_client_config/vendors/datacentred.json +++ b/os_client_config/vendors/datacentred.json @@ -5,7 +5,7 @@ "auth_url": "https://compute.datacentred.io:5000" }, "region-name": "sal01", - "identity_api_version": "2", - "image_api_version": "1" + "identity_api_version": "3", + "image_api_version": "2" } } From 15a83dab9a8fa68f983903a65fb656104379da5d Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 12 Aug 2017 11:50:13 +0000 Subject: [PATCH 1734/3836] Updated from global requirements Change-Id: Iad6eba535f48d0f09e5507db32399623f63a4f88 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 3c44d7f02..cb252f268 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,7 +10,7 @@ extras # MIT fixtures>=3.0.0 # Apache-2.0/BSD jsonschema!=2.5.0,<3.0.0,>=2.0.0 # MIT mock>=2.0 # BSD -python-glanceclient>=2.7.0 # Apache-2.0 +python-glanceclient>=2.8.0 # Apache-2.0 python-subunit>=0.0.18 # Apache-2.0/BSD sphinx>=1.6.2 # BSD openstackdocstheme>=1.16.0 # Apache-2.0 From d8ddbcf749f695288a16f3488e12022c62a620e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Sun, 6 Aug 2017 15:20:37 +0000 Subject: [PATCH 1735/3836] Add functional tests for Neutron QoS policies and rules This patch adds functional tests for create/list/update/delete QoS policies and rules. Change-Id: Iebff3309423bea1fa871612c5e824940c13926c1 --- .../test_qos_bandwidth_limit_rule.py | 106 ++++++++++++++++++ .../functional/test_qos_dscp_marking_rule.py | 75 +++++++++++++ .../test_qos_minimum_bandwidth_rule.py | 75 +++++++++++++ shade/tests/functional/test_qos_policy.py | 95 ++++++++++++++++ 4 files changed, 351 insertions(+) create mode 100644 shade/tests/functional/test_qos_bandwidth_limit_rule.py create mode 100644 shade/tests/functional/test_qos_dscp_marking_rule.py create mode 100644 shade/tests/functional/test_qos_minimum_bandwidth_rule.py create mode 100644 shade/tests/functional/test_qos_policy.py diff --git a/shade/tests/functional/test_qos_bandwidth_limit_rule.py b/shade/tests/functional/test_qos_bandwidth_limit_rule.py new file mode 100644 index 000000000..3bec8ced2 --- /dev/null +++ b/shade/tests/functional/test_qos_bandwidth_limit_rule.py @@ -0,0 +1,106 @@ +# Copyright 2017 OVH SAS +# 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_qos_bandwidth_limit_rule +---------------------------------- + +Functional tests for `shade`QoS bandwidth limit methods. +""" + +from shade.exc import OpenStackCloudException +from shade.tests.functional import base + + +class TestQosBandwidthLimitRule(base.BaseFunctionalTestCase): + def setUp(self): + super(TestQosBandwidthLimitRule, self).setUp() + if not self.operator_cloud.has_service('network'): + self.skipTest('Network service not supported by cloud') + if not self.operator_cloud._has_neutron_extension('qos'): + self.skipTest('QoS network extension not supported by cloud') + + policy_name = self.getUniqueString('qos_policy') + self.policy = self.operator_cloud.create_qos_policy(name=policy_name) + + self.addCleanup(self._cleanup_qos_policy) + + def _cleanup_qos_policy(self): + try: + self.operator_cloud.delete_qos_policy(self.policy['id']) + except Exception as e: + raise OpenStackCloudException(e) + + def test_qos_bandwidth_limit_rule_lifecycle(self): + max_kbps = 1500 + max_burst_kbps = 500 + updated_max_kbps = 2000 + + # Create bw limit rule + rule = self.operator_cloud.create_qos_bandwidth_limit_rule( + self.policy['id'], + max_kbps=max_kbps, + max_burst_kbps=max_burst_kbps) + self.assertIn('id', rule) + self.assertEqual(max_kbps, rule['max_kbps']) + self.assertEqual(max_burst_kbps, rule['max_burst_kbps']) + + # Now try to update rule + updated_rule = self.operator_cloud.update_qos_bandwidth_limit_rule( + self.policy['id'], + rule['id'], + max_kbps=updated_max_kbps) + self.assertIn('id', updated_rule) + self.assertEqual(updated_max_kbps, updated_rule['max_kbps']) + self.assertEqual(max_burst_kbps, updated_rule['max_burst_kbps']) + + # List rules from policy + policy_rules = self.operator_cloud.list_qos_bandwidth_limit_rules( + self.policy['id']) + self.assertEqual([updated_rule], policy_rules) + + # Delete rule + self.operator_cloud.delete_qos_bandwidth_limit_rule( + self.policy['id'], updated_rule['id']) + + # Check if there is no rules in policy + policy_rules = self.operator_cloud.list_qos_bandwidth_limit_rules( + self.policy['id']) + self.assertEqual([], policy_rules) + + def test_create_qos_bandwidth_limit_rule_direction(self): + if not self.operator_cloud._has_neutron_extension( + 'qos-bw-limit-direction'): + self.skipTest("'qos-bw-limit-direction' network extension " + "not supported by cloud") + max_kbps = 1500 + direction = "ingress" + updated_direction = "egress" + + # Create bw limit rule + rule = self.operator_cloud.create_qos_bandwidth_limit_rule( + self.policy['id'], + max_kbps=max_kbps, + direction=direction) + self.assertIn('id', rule) + self.assertEqual(max_kbps, rule['max_kbps']) + self.assertEqual(direction, rule['direction']) + + # Now try to update direction in rule + updated_rule = self.operator_cloud.update_qos_bandwidth_limit_rule( + self.policy['id'], + rule['id'], + direction=updated_direction) + self.assertIn('id', updated_rule) + self.assertEqual(max_kbps, updated_rule['max_kbps']) + self.assertEqual(updated_direction, updated_rule['direction']) diff --git a/shade/tests/functional/test_qos_dscp_marking_rule.py b/shade/tests/functional/test_qos_dscp_marking_rule.py new file mode 100644 index 000000000..3640fe4e9 --- /dev/null +++ b/shade/tests/functional/test_qos_dscp_marking_rule.py @@ -0,0 +1,75 @@ +# Copyright 2017 OVH SAS +# 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_qos_dscp_marking_rule +---------------------------------- + +Functional tests for `shade`QoS DSCP marking rule methods. +""" + +from shade.exc import OpenStackCloudException +from shade.tests.functional import base + + +class TestQosDscpMarkingRule(base.BaseFunctionalTestCase): + def setUp(self): + super(TestQosDscpMarkingRule, self).setUp() + if not self.operator_cloud.has_service('network'): + self.skipTest('Network service not supported by cloud') + if not self.operator_cloud._has_neutron_extension('qos'): + self.skipTest('QoS network extension not supported by cloud') + + policy_name = self.getUniqueString('qos_policy') + self.policy = self.operator_cloud.create_qos_policy(name=policy_name) + + self.addCleanup(self._cleanup_qos_policy) + + def _cleanup_qos_policy(self): + try: + self.operator_cloud.delete_qos_policy(self.policy['id']) + except Exception as e: + raise OpenStackCloudException(e) + + def test_qos_dscp_marking_rule_lifecycle(self): + dscp_mark = 16 + updated_dscp_mark = 32 + + # Create DSCP marking rule + rule = self.operator_cloud.create_qos_dscp_marking_rule( + self.policy['id'], + dscp_mark=dscp_mark) + self.assertIn('id', rule) + self.assertEqual(dscp_mark, rule['dscp_mark']) + + # Now try to update rule + updated_rule = self.operator_cloud.update_qos_dscp_marking_rule( + self.policy['id'], + rule['id'], + dscp_mark=updated_dscp_mark) + self.assertIn('id', updated_rule) + self.assertEqual(updated_dscp_mark, updated_rule['dscp_mark']) + + # List rules from policy + policy_rules = self.operator_cloud.list_qos_dscp_marking_rules( + self.policy['id']) + self.assertEqual([updated_rule], policy_rules) + + # Delete rule + self.operator_cloud.delete_qos_dscp_marking_rule( + self.policy['id'], updated_rule['id']) + + # Check if there is no rules in policy + policy_rules = self.operator_cloud.list_qos_dscp_marking_rules( + self.policy['id']) + self.assertEqual([], policy_rules) diff --git a/shade/tests/functional/test_qos_minimum_bandwidth_rule.py b/shade/tests/functional/test_qos_minimum_bandwidth_rule.py new file mode 100644 index 000000000..ad5291f10 --- /dev/null +++ b/shade/tests/functional/test_qos_minimum_bandwidth_rule.py @@ -0,0 +1,75 @@ +# Copyright 2017 OVH SAS +# 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_qos_minumum_bandwidth_rule +---------------------------------- + +Functional tests for `shade`QoS minimum bandwidth methods. +""" + +from shade.exc import OpenStackCloudException +from shade.tests.functional import base + + +class TestQosMinimumBandwidthRule(base.BaseFunctionalTestCase): + def setUp(self): + super(TestQosMinimumBandwidthRule, self).setUp() + if not self.operator_cloud.has_service('network'): + self.skipTest('Network service not supported by cloud') + if not self.operator_cloud._has_neutron_extension('qos'): + self.skipTest('QoS network extension not supported by cloud') + + policy_name = self.getUniqueString('qos_policy') + self.policy = self.operator_cloud.create_qos_policy(name=policy_name) + + self.addCleanup(self._cleanup_qos_policy) + + def _cleanup_qos_policy(self): + try: + self.operator_cloud.delete_qos_policy(self.policy['id']) + except Exception as e: + raise OpenStackCloudException(e) + + def test_qos_minimum_bandwidth_rule_lifecycle(self): + min_kbps = 1500 + updated_min_kbps = 2000 + + # Create min bw rule + rule = self.operator_cloud.create_qos_minimum_bandwidth_rule( + self.policy['id'], + min_kbps=min_kbps) + self.assertIn('id', rule) + self.assertEqual(min_kbps, rule['min_kbps']) + + # Now try to update rule + updated_rule = self.operator_cloud.update_qos_minimum_bandwidth_rule( + self.policy['id'], + rule['id'], + min_kbps=updated_min_kbps) + self.assertIn('id', updated_rule) + self.assertEqual(updated_min_kbps, updated_rule['min_kbps']) + + # List rules from policy + policy_rules = self.operator_cloud.list_qos_minimum_bandwidth_rules( + self.policy['id']) + self.assertEqual([updated_rule], policy_rules) + + # Delete rule + self.operator_cloud.delete_qos_minimum_bandwidth_rule( + self.policy['id'], updated_rule['id']) + + # Check if there is no rules in policy + policy_rules = self.operator_cloud.list_qos_minimum_bandwidth_rules( + self.policy['id']) + self.assertEqual([], policy_rules) diff --git a/shade/tests/functional/test_qos_policy.py b/shade/tests/functional/test_qos_policy.py new file mode 100644 index 000000000..ba418337d --- /dev/null +++ b/shade/tests/functional/test_qos_policy.py @@ -0,0 +1,95 @@ +# Copyright 2017 OVH SAS +# 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_qos_policy +---------------------------------- + +Functional tests for `shade`QoS policies methods. +""" + +from shade.exc import OpenStackCloudException +from shade.tests.functional import base + + +class TestQosPolicy(base.BaseFunctionalTestCase): + def setUp(self): + super(TestQosPolicy, self).setUp() + if not self.operator_cloud.has_service('network'): + self.skipTest('Network service not supported by cloud') + if not self.operator_cloud._has_neutron_extension('qos'): + self.skipTest('QoS network extension not supported by cloud') + self.policy_name = self.getUniqueString('qos_policy') + self.addCleanup(self._cleanup_policies) + + def _cleanup_policies(self): + exception_list = list() + for policy in self.operator_cloud.list_qos_policies(): + if policy['name'].startswith(self.policy_name): + try: + self.operator_cloud.delete_qos_policy(policy['id']) + except Exception as e: + exception_list.append(str(e)) + continue + + if exception_list: + raise OpenStackCloudException('\n'.join(exception_list)) + + def test_create_qos_policy_basic(self): + policy = self.operator_cloud.create_qos_policy(name=self.policy_name) + self.assertIn('id', policy) + self.assertEqual(self.policy_name, policy['name']) + self.assertFalse(policy['shared']) + self.assertFalse(policy['is_default']) + + def test_create_qos_policy_shared(self): + policy = self.operator_cloud.create_qos_policy( + name=self.policy_name, shared=True) + self.assertIn('id', policy) + self.assertEqual(self.policy_name, policy['name']) + self.assertTrue(policy['shared']) + self.assertFalse(policy['is_default']) + + def test_create_qos_policy_default(self): + if not self.operator_cloud._has_neutron_extension('qos-default'): + self.skipTest("'qos-default' network extension not supported " + "by cloud") + policy = self.operator_cloud.create_qos_policy( + name=self.policy_name, default=True) + self.assertIn('id', policy) + self.assertEqual(self.policy_name, policy['name']) + self.assertFalse(policy['shared']) + self.assertTrue(policy['is_default']) + + def test_update_qos_policy(self): + policy = self.operator_cloud.create_qos_policy(name=self.policy_name) + self.assertEqual(self.policy_name, policy['name']) + self.assertFalse(policy['shared']) + self.assertFalse(policy['is_default']) + + updated_policy = self.operator_cloud.update_qos_policy( + policy['id'], shared=True, default=True) + self.assertEqual(self.policy_name, updated_policy['name']) + self.assertTrue(updated_policy['shared']) + self.assertTrue(updated_policy['is_default']) + + def test_list_qos_policies_filtered(self): + policy1 = self.operator_cloud.create_qos_policy(name=self.policy_name) + self.assertIsNotNone(policy1) + policy2 = self.operator_cloud.create_qos_policy( + name=self.policy_name + 'other') + self.assertIsNotNone(policy2) + match = self.operator_cloud.list_qos_policies( + filters=dict(name=self.policy_name)) + self.assertEqual(1, len(match)) + self.assertEqual(policy1['name'], match[0]['name']) From 2b1baefb4babcc69f8295dfda18c9babe0887d2e Mon Sep 17 00:00:00 2001 From: liyi Date: Mon, 14 Aug 2017 21:33:10 +0800 Subject: [PATCH 1736/3836] Support node-adopt/preview CLI The preview API only accept few parameters, so the adopt API and adopt-preview API can't use the same parameters. Change-Id: Iea193672f33826551a370516c98ab4c950c71c1c Partial-Bug: #1710620 --- openstack/cluster/v1/node.py | 15 +++++++++++++-- openstack/tests/unit/cluster/v1/test_node.py | 10 ++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/openstack/cluster/v1/node.py b/openstack/cluster/v1/node.py index cd067ba00..a0305a82c 100644 --- a/openstack/cluster/v1/node.py +++ b/openstack/cluster/v1/node.py @@ -136,9 +136,20 @@ def adopt(self, session, preview=False, **params): :param dict params: A dict providing the details of a node to be adopted. """ - path = "adopt-preview" if preview else "adopt" + if preview: + path = 'adopt-preview' + attrs = { + 'identity': params.get('identity'), + 'overrides': params.get('overrides'), + 'type': params.get('type'), + 'snapshot': params.get('snapshot') + } + else: + path = 'adopt' + attrs = params + url = utils.urljoin(self.base_path, path) - resp = session.post(url, endpoint_filter=self.service, json=params) + resp = session.post(url, endpoint_filter=self.service, json=attrs) if preview: return resp.json() diff --git a/openstack/tests/unit/cluster/v1/test_node.py b/openstack/tests/unit/cluster/v1/test_node.py index 9ffa543eb..4566387d4 100644 --- a/openstack/tests/unit/cluster/v1/test_node.py +++ b/openstack/tests/unit/cluster/v1/test_node.py @@ -114,11 +114,17 @@ def test_adopt_preview(self): sess = mock.Mock() sess.post = mock.Mock(return_value=resp) - res = sot.adopt(sess, True, param="value") + attrs = { + 'identity': 'fake-resource-id', + 'overrides': {}, + 'type': 'os.nova.server-1.0', + 'snapshot': False + } + res = sot.adopt(sess, True, **attrs) self.assertEqual({"foo": "bar"}, res) sess.post.assert_called_once_with("nodes/adopt-preview", endpoint_filter=sot.service, - json={"param": "value"}) + json=attrs) def test_adopt(self): sot = node.Node.new() From 7466aaedfa202dc416fbb02b2a672fb8d94c4f92 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Mon, 14 Aug 2017 13:26:46 -0400 Subject: [PATCH 1737/3836] Consolidate client version checks in an utility method Change-Id: I8da6f940a5983ce38a4a5bdc054f203088c51c48 --- shade/openstackcloud.py | 42 +++++++++++++++++++++++------------------ shade/operatorcloud.py | 35 +++++++++++++++++----------------- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 53b6b631c..4e8bd54d9 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -362,6 +362,10 @@ def _get_raw_client(self, service_key): region_name=self.cloud_config.region, shade_logger=self.log) + def _is_client_version(self, client, version): + api_version = self.cloud_config.get_api_version(client) + return api_version.startswith(str(version)) + @property def _application_catalog_client(self): if 'application-catalog' not in self._raw_clients: @@ -718,7 +722,7 @@ def _project_manager(self): # Keystone v2 calls this attribute tenants # Keystone v3 calls it projects # Yay for usable APIs! - if self.cloud_config.get_api_version('identity').startswith('2'): + if self._is_client_version('identity', 2): return self.keystone_client.tenants return self.keystone_client.projects @@ -727,7 +731,7 @@ def _get_project_id_param_dict(self, name_or_id): project = self.get_project(name_or_id) if not project: return {} - if self.cloud_config.get_api_version('identity') == '3': + if self._is_client_version('identity', 3): return {'default_project_id': project['id']} else: return {'tenant_id': project['id']} @@ -741,7 +745,7 @@ def _get_domain_id_param_dict(self, domain_id): # not. However, keystone v2 does not allow user creation by non-admin # users, so we can throw an error to the user that does not need to # mention api versions - if self.cloud_config.get_api_version('identity') == '3': + if self._is_client_version('identity', 3): if not domain_id: raise OpenStackCloudException( "User or project creation requires an explicit" @@ -840,14 +844,16 @@ def list_projects(self, domain_id=None, name_or_id=None, filters=None): kwargs = dict( filters=filters, domain_id=domain_id) - if self.cloud_config.get_api_version('identity') == '3': + if self._is_client_version('identity', 3): kwargs['obj_name'] = 'project' pushdown, filters = _normalize._split_filters(**kwargs) try: - api_version = self.cloud_config.get_api_version('identity') - key = 'projects' if api_version == '3' else 'tenants' + if self._is_client_version('identity', 3): + key = 'projects' + else: + key = 'tenants' data = self._identity_client.get( '/{endpoint}'.format(endpoint=key), params=pushdown) projects = self._normalize_projects( @@ -897,7 +903,7 @@ def update_project(self, name_or_id, enabled=None, domain_id=None, kwargs.update({'enabled': enabled}) # NOTE(samueldmq): Current code only allow updates of description # or enabled fields. - if self.cloud_config.get_api_version('identity') == '3': + if self._is_client_version('identity', 3): data = self._identity_client.patch( '/projects/' + proj['id'], json={'project': kwargs}) project = self._get_and_munchify('project', data) @@ -919,7 +925,7 @@ def create_project( 'description': description, 'enabled': enabled}) endpoint, key = ('tenants', 'tenant') - if self.cloud_config.get_api_version('identity') == '3': + if self._is_client_version('identity', 3): endpoint, key = ('projects', 'project') data = self._identity_client.post( '/{endpoint}'.format(endpoint=endpoint), @@ -951,7 +957,7 @@ def delete_project(self, name_or_id, domain_id=None): "Project %s not found for deleting", name_or_id) return False - if self.cloud_config.get_api_version('identity') == '3': + if self._is_client_version('identity', 3): self._identity_client.delete('/projects/' + project['id']) else: self._identity_client.delete('/tenants/' + project['id']) @@ -1034,7 +1040,7 @@ def update_user(self, name_or_id, **kwargs): # TODO(mordred) When this changes to REST, force interface=admin # in the adapter call if it's an admin force call (and figure out how # to make that disctinction) - if self.cloud_config.get_api_version('identity') != '3': + if not self._is_client_version('identity', 3): # Do not pass v3 args to a v2 keystone. kwargs.pop('domain_id', None) kwargs.pop('description', None) @@ -1065,7 +1071,7 @@ def create_user( params = self._get_identity_params(domain_id, default_project) params.update({'name': name, 'password': password, 'email': email, 'enabled': enabled}) - if self.cloud_config.get_api_version('identity') == '3': + if self._is_client_version('identity', 3): params['description'] = description elif description is not None: self.log.info( @@ -2161,7 +2167,7 @@ def list_images(self, filter_deleted=True, show_all=False): params = {} image_list = [] try: - if self.cloud_config.get_api_version('image') == '2': + if self._is_client_version('image', 2): endpoint = '/images' if show_all: params['member_status'] = 'all' @@ -3020,7 +3026,7 @@ def download_image( if len(image) == 0: raise OpenStackCloudResourceNotFound( "No images with name or ID %s were found" % name_or_id, None) - if self.cloud_config.get_api_version('image') == '2': + if self._is_client_version('image', 2): endpoint = '/images/{id}/file'.format(id=image[0]['id']) else: endpoint = '/images/{id}'.format(id=image[0]['id']) @@ -4388,7 +4394,7 @@ def create_image( # boolean. Glance v2 takes "visibility". If the user gives us # is_public, we know what they mean. If they give us visibility, they # know that they mean. - if self.cloud_config.get_api_version('image') == '2': + if self._is_client_version('image', 2): if 'is_public' in kwargs: is_public = kwargs.pop('is_public') if is_public: @@ -4537,7 +4543,7 @@ def _upload_image_put( self, name, filename, meta, wait, timeout, **image_kwargs): image_data = open(filename, 'rb') # Because reasons and crying bunnies - if self.cloud_config.get_api_version('image') == '2': + if self._is_client_version('image', 2): image = self._upload_image_put_v2( name, image_data, meta, **image_kwargs) else: @@ -4652,7 +4658,7 @@ def update_image_properties( img_props[k] = v # This makes me want to die inside - if self.cloud_config.get_api_version('image') == '2': + if self._is_client_version('image', 2): return self._update_image_properties_v2(image, meta, img_props) else: return self._update_image_properties_v1(image, meta, img_props) @@ -4946,12 +4952,12 @@ def _get_volume_kwargs(self, kwargs): description = kwargs.pop('description', kwargs.pop('display_description', None)) if name: - if self.cloud_config.get_api_version('volume').startswith('2'): + if self._is_client_version('volume', 2): kwargs['name'] = name else: kwargs['display_name'] = name if description: - if self.cloud_config.get_api_version('volume').startswith('2'): + if self._is_client_version('volume', 2): kwargs['description'] = description else: kwargs['display_description'] = description diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 7cbb49c91..4099a4b8a 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -754,7 +754,7 @@ def create_service(self, name, enabled=True, **kwargs): # TODO(mordred) When this changes to REST, force interface=admin # in the adapter call - if self.cloud_config.get_api_version('identity').startswith('2'): + if self._is_client_version('identity', 2): url, key = '/OS-KSADM/services', 'OS-KSADM:service' kwargs['type'] = type_ or service_type else: @@ -773,7 +773,7 @@ def create_service(self, name, enabled=True, **kwargs): 'description') def update_service(self, name_or_id, **kwargs): # NOTE(SamYaple): Service updates are only available on v3 api - if self.cloud_config.get_api_version('identity').startswith('2'): + if self._is_client_version('identity', 2): raise OpenStackCloudUnavailableFeature( 'Unavailable Feature: Service update requires Identity v3' ) @@ -860,7 +860,7 @@ def delete_service(self, name_or_id): self.log.debug("Service %s not found for deleting", name_or_id) return False - if self.cloud_config.get_api_version('identity').startswith('2'): + if self._is_client_version('identity', 2): service_kwargs = {'id': service['id']} else: service_kwargs = {'service': service['id']} @@ -913,7 +913,7 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, endpoint_args = [] if url: urlkwargs = {} - if self.cloud_config.get_api_version('identity').startswith('2'): + if self._is_client_version('identity', 2): if interface != 'public': raise OpenStackCloudException( "Error adding endpoint for service {service}." @@ -935,7 +935,7 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, expected_endpoints = [('public', public_url), ('internal', internal_url), ('admin', admin_url)] - if self.cloud_config.get_api_version('identity').startswith('2'): + if self._is_client_version('identity', 2): urlkwargs = {} for interface, url in expected_endpoints: if url: @@ -949,7 +949,7 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, urlkwargs['interface'] = interface endpoint_args.append(urlkwargs) - if self.cloud_config.get_api_version('identity').startswith('2'): + if self._is_client_version('identity', 2): kwargs['service_id'] = service['id'] # Keystone v2 requires 'region' arg even if it is None kwargs['region'] = region @@ -978,7 +978,7 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, 'region') def update_endpoint(self, endpoint_id, **kwargs): # NOTE(SamYaple): Endpoint updates are only available on v3 api - if self.cloud_config.get_api_version('identity').startswith('2'): + if self._is_client_version('identity', 2): raise OpenStackCloudUnavailableFeature( 'Unavailable Feature: Endpoint update' ) @@ -1070,7 +1070,7 @@ def delete_endpoint(self, id): # TODO(mordred) When this changes to REST, force interface=admin # in the adapter call - if self.cloud_config.get_api_version('identity').startswith('2'): + if self._is_client_version('identity', 2): endpoint_kwargs = {'id': endpoint['id']} else: endpoint_kwargs = {'endpoint': endpoint['id']} @@ -1357,7 +1357,7 @@ def list_roles(self): :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ - v2 = self.cloud_config.get_api_version('identity').startswith('2') + v2 = self._is_client_version('identity', 2) url = '/OS-KSADM/roles' if v2 else '/roles' data = self._identity_client.get( url, error_message="Failed to list roles") @@ -1459,7 +1459,7 @@ def list_role_assignments(self, filters=None): if not filters: filters = {} - if self.cloud_config.get_api_version('identity').startswith('2'): + if self._is_client_version('identity', 2): if filters.get('project') is None or filters.get('user') is None: raise OpenStackCloudException( "Must provide project and user for keystone v2" @@ -1629,7 +1629,7 @@ def create_role(self, name): :raise OpenStackCloudException: if the role cannot be created """ - v2 = self.cloud_config.get_api_version('identity').startswith('2') + v2 = self._is_client_version('identity', 2) url = '/OS-KSADM/roles' if v2 else '/roles' msg = 'Failed to create role {name}'.format(name=name) data = self._identity_client.post( @@ -1653,7 +1653,7 @@ def delete_role(self, name_or_id): "Role %s not found for deleting", name_or_id) return False - v2 = self.cloud_config.get_api_version('identity').startswith('2') + v2 = self._is_client_version('identity', 2) url = '{preffix}/{id}'.format( preffix='/OS-KSADM/roles' if v2 else '/roles', id=role['id']) error_msg = "Unable to delete role {name}".format(name=name_or_id) @@ -1669,8 +1669,7 @@ def _get_grant_revoke_params(self, role, user=None, group=None, data = {'role': role.id} # domain and group not available in keystone v2.0 - keystone_version = self.cloud_config.get_api_version('identity') - is_keystone_v2 = keystone_version.startswith('2') + is_keystone_v2 = self._is_client_version('identity', 2) filters = {} if not is_keystone_v2 and domain: @@ -1729,7 +1728,7 @@ def grant_role(self, name_or_id, user=None, group=None, if data.get('user') is None and data.get('group') is None: raise OpenStackCloudException( 'Must specify either a user or a group') - if self.cloud_config.get_api_version('identity').startswith('2') and \ + if self._is_client_version('identity', 2) and \ data.get('project') is None: raise OpenStackCloudException( 'Must specify project for keystone v2') @@ -1741,7 +1740,7 @@ def grant_role(self, name_or_id, user=None, group=None, with _utils.shade_exceptions( "Error granting access to role: {0}".format( data)): - if self.cloud_config.get_api_version('identity').startswith('2'): + if self._is_client_version('identity', 2): data['tenant'] = data.pop('project') self.manager.submit_task(_tasks.RoleAddUser(**data)) else: @@ -1792,7 +1791,7 @@ def revoke_role(self, name_or_id, user=None, group=None, if data.get('user') is None and data.get('group') is None: raise OpenStackCloudException( 'Must specify either a user or a group') - if self.cloud_config.get_api_version('identity').startswith('2') and \ + if self._is_client_version('identity', 2) and \ data.get('project') is None: raise OpenStackCloudException( 'Must specify project for keystone v2') @@ -1804,7 +1803,7 @@ def revoke_role(self, name_or_id, user=None, group=None, with _utils.shade_exceptions( "Error revoking access to role: {0}".format( data)): - if self.cloud_config.get_api_version('identity').startswith('2'): + if self._is_client_version('identity', 2): data['tenant'] = data.pop('project') self.manager.submit_task(_tasks.RoleRemoveUser(**data)) else: From 31d5f9ecde663b4eb4bf20694196bf1efe6b00a1 Mon Sep 17 00:00:00 2001 From: Nakul Dahiwade Date: Wed, 29 Mar 2017 21:30:46 +0000 Subject: [PATCH 1738/3836] Introduce Listener for Octavia (load balancing) Adds SDK support for load-balancer listeners. Co-Authored-By: Michael Johnson Change-Id: I50af4a1d95b18c50a0684b546cc5ed48aecf8cca --- doc/source/users/proxies/load_balancer_v2.rst | 12 +++ .../users/resources/load_balancer/index.rst | 1 + .../resources/load_balancer/v2/listener.rst | 12 +++ .../load_balancer/v2/load_balancer.rst | 1 - openstack/load_balancer/v2/_proxy.py | 82 ++++++++++++++ openstack/load_balancer/v2/listener.py | 81 ++++++++++++++ openstack/load_balancer/v2/load_balancer.py | 1 + .../load_balancer/v2/test_load_balancer.py | 101 ++++++++++++++---- .../tests/unit/load_balancer/test_listener.py | 84 +++++++++++++++ .../tests/unit/load_balancer/test_proxy.py | 26 +++++ 10 files changed, 380 insertions(+), 21 deletions(-) create mode 100644 doc/source/users/resources/load_balancer/v2/listener.rst create mode 100644 openstack/load_balancer/v2/listener.py create mode 100644 openstack/tests/unit/load_balancer/test_listener.py diff --git a/doc/source/users/proxies/load_balancer_v2.rst b/doc/source/users/proxies/load_balancer_v2.rst index ef566a563..1f9c9b5ff 100644 --- a/doc/source/users/proxies/load_balancer_v2.rst +++ b/doc/source/users/proxies/load_balancer_v2.rst @@ -21,3 +21,15 @@ Load Balancer Operations .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_load_balancer .. automethod:: openstack.load_balancer.v2._proxy.Proxy.load_balancers .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_load_balancer + +Listener Operations +^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.load_balancer.v2._proxy.Proxy + + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.create_listener + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_listener + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_listener + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_listener + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.listeners + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_listener diff --git a/doc/source/users/resources/load_balancer/index.rst b/doc/source/users/resources/load_balancer/index.rst index 61838c730..d542175f3 100644 --- a/doc/source/users/resources/load_balancer/index.rst +++ b/doc/source/users/resources/load_balancer/index.rst @@ -5,3 +5,4 @@ Load Balancer Resources :maxdepth: 1 v2/load_balancer + v2/listener diff --git a/doc/source/users/resources/load_balancer/v2/listener.rst b/doc/source/users/resources/load_balancer/v2/listener.rst new file mode 100644 index 000000000..099caf7d0 --- /dev/null +++ b/doc/source/users/resources/load_balancer/v2/listener.rst @@ -0,0 +1,12 @@ +openstack.load_balancer.v2.listener +=================================== + +.. automodule:: openstack.load_balancer.v2.listener + +The Listener Class +------------------ + +The ``Listener`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.load_balancer.v2.listener.Listener + :members: diff --git a/doc/source/users/resources/load_balancer/v2/load_balancer.rst b/doc/source/users/resources/load_balancer/v2/load_balancer.rst index fd55e4668..0622c7cb4 100644 --- a/doc/source/users/resources/load_balancer/v2/load_balancer.rst +++ b/doc/source/users/resources/load_balancer/v2/load_balancer.rst @@ -10,4 +10,3 @@ The ``LoadBalancer`` class inherits from :class:`~openstack.resource.Resource`. .. autoclass:: openstack.load_balancer.v2.load_balancer.LoadBalancer :members: - diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index 4533cb664..796c209f2 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.load_balancer.v2 import listener as _listener from openstack.load_balancer.v2 import load_balancer as _lb from openstack import proxy2 @@ -94,3 +95,84 @@ def update_load_balancer(self, load_balancer, **attrs): :rtype: :class:`~openstack.load_balancer.v2.load_balancer.LoadBalancer` """ return self._update(_lb.LoadBalancer, load_balancer, **attrs) + + def create_listener(self, **attrs): + """Create a new listener from attributes + + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.load_balancer.v2.listener.Listener`, + comprised of the properties on the Listener class. + + :returns: The results of listener creation + :rtype: :class:`~openstack.load_balancer.v2.listener.Listener` + """ + return self._create(_listener.Listener, **attrs) + + def delete_listener(self, listener, ignore_missing=True): + """Delete a listener + + :param listener: The value can be either the ID of a listner or a + :class:`~openstack.load_balancer.v2.listener.Listener` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the listner does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent listener. + + :returns: ``None`` + """ + self._delete(_listener.Listener, listener, + ignore_missing=ignore_missing) + + def find_listener(self, name_or_id, ignore_missing=True): + """Find a single listener + + :param name_or_id: The name or ID of a listener. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + + :returns: One :class:`~openstack.load_balancer.v2.listener.Listener` + or None + """ + return self._find(_listener.Listener, name_or_id, + ignore_missing=ignore_missing) + + def get_listener(self, listener): + """Get a single listener + + :param listener: The value can be the ID of a listener or a + :class:`~openstack.load_balancer.v2.listener.Listener` + instance. + + :returns: One :class:`~openstack.load_balancer.v2.listener.Listener` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_listener.Listener, listener) + + def listeners(self, **query): + """Return a generator of listeners + + :param dict query: Optional query parameters to be sent to limit + the resources being returned. Valid parameters are: + :returns: A generator of listener objects + :rtype: :class:`~openstack.load_balancer.v2.listener.Listener` + """ + return self._list(_listener.Listener, paginated=True, **query) + + def update_listener(self, listener, **attrs): + """Update a listener + + :param listener: Either the id of a listener or a + :class:`~openstack.load_balancer.v2.listener.Listener` + instance. + :param dict attrs: The attributes to update on the listener + represented by ``listener``. + + :returns: The updated listener + :rtype: :class:`~openstack.load_balancer.v2.listener.Listener` + """ + return self._update(_listener.Listener, listener, **attrs) diff --git a/openstack/load_balancer/v2/listener.py b/openstack/load_balancer/v2/listener.py new file mode 100644 index 000000000..2612ff7ba --- /dev/null +++ b/openstack/load_balancer/v2/listener.py @@ -0,0 +1,81 @@ +# 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.load_balancer import load_balancer_service as lb_service +from openstack import resource2 as resource + + +class Listener(resource.Resource): + resource_key = 'listener' + resources_key = 'listeners' + base_path = '/v2.0/lbaas/listeners' + service = lb_service.LoadBalancerService() + + # capabilities + allow_create = True + allow_get = True + allow_update = True + allow_delete = True + allow_list = True + + _query_mapping = resource.QueryParameters( + 'connection_limit', 'default_pool_id', 'default_tls_container_ref', + 'description', 'name', 'project_id', 'protocol', 'protocol_port', + 'created_at', 'updated_at', 'provisioning_status', 'operating_status', + 'sni_container_refs', 'insert_headers', 'load_balancer_id', + is_admin_state_up='admin_state_up', + ) + + # Properties + #: The maximum number of connections permitted for this load balancer. + #: Default is infinite. + connection_limit = resource.Body('connection_limit') + #: Timestamp when the listener was created. + created_at = resource.Body('created_at') + #: Default pool to which the requests will be routed. + default_pool = resource.Body('default_pool') + #: ID of default pool. Must have compatible protocol with listener. + default_pool_id = resource.Body('default_pool_id') + #: A reference to a container of TLS secrets. + default_tls_container_ref = resource.Body('default_tls_container_ref') + #: Description for the listener. + description = resource.Body('description') + #: Dictionary of additional headers insertion into HTTP header. + insert_headers = resource.Body('insert_headers', type=dict) + #: The administrative state of the listener, which is up + #: ``True`` or down ``False``. *Type: bool* + is_admin_state_up = resource.Body('admin_state_up', type=bool) + #: List of l7policies associated with this listener. + l7_policies = resource.Body('l7policies', type=list) + #: The ID of the parent load balancer. + load_balancer_id = resource.Body('loadbalancer_id') + #: List of load balancers associated with this listener. + #: *Type: list of dicts which contain the load balancer IDs* + load_balancers = resource.Body('loadbalancers', type=list) + #: Name of the listener + name = resource.Body('name') + #: Operating status of the listener. + operating_status = resource.Body('operating_status') + #: The ID of the project this listener is associated with. + project_id = resource.Body('project_id') + #: The protocol of the listener, which is TCP, HTTP, HTTPS + #: or TERMINATED_HTTPS. + protocol = resource.Body('protocol') + #: Port the listener will listen to, e.g. 80. + protocol_port = resource.Body('protocol_port', type=int) + #: The provisioning status of this listener. + provisioning_status = resource.Body('provisioning_status') + #: A list of references to TLS secrets. + #: *Type: list* + sni_container_refs = resource.Body('sni_container_refs') + #: Timestamp when the listener was last updated. + updated_at = resource.Body('updated_at') diff --git a/openstack/load_balancer/v2/load_balancer.py b/openstack/load_balancer/v2/load_balancer.py index 7c444376a..1e2afa733 100644 --- a/openstack/load_balancer/v2/load_balancer.py +++ b/openstack/load_balancer/v2/load_balancer.py @@ -30,6 +30,7 @@ class LoadBalancer(resource.Resource): _query_mapping = resource.QueryParameters( 'description', 'flavor', 'name', 'project_id', 'provider', 'vip_address', 'vip_network_id', 'vip_port_id', 'vip_subnet_id', + 'provisioning_status', 'operating_status', is_admin_state_up='admin_state_up' ) diff --git a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py index a6b126651..c7523af78 100644 --- a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py +++ b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py @@ -13,6 +13,7 @@ import unittest import uuid +from openstack.load_balancer.v2 import listener from openstack.load_balancer.v2 import load_balancer from openstack.tests.functional import base from openstack.tests.functional.load_balancer import base as lb_base @@ -22,12 +23,19 @@ 'Load-balancer service does not exist') class TestLoadBalancer(lb_base.BaseLBFunctionalTest): - NAME = uuid.uuid4().hex - ID = None + LB_NAME = uuid.uuid4().hex + LISTENER_NAME = uuid.uuid4().hex + UPDATE_NAME = uuid.uuid4().hex + LB_ID = None + LISTENER_ID = None VIP_SUBNET_ID = None PROJECT_ID = None - UPDATE_NAME = uuid.uuid4().hex + PROTOCOL = 'HTTP' + PROTOCOL_PORT = 80 + # Note: Creating load balancers can be slow on some hosts due to nova + # instance boot times (up to ten minutes) so we are consolidating + # all of our functional tests here to reduce test runtime. @classmethod def setUpClass(cls): super(TestLoadBalancer, cls).setUpClass() @@ -35,42 +43,95 @@ def setUpClass(cls): cls.VIP_SUBNET_ID = subnets[0].id cls.PROJECT_ID = cls.conn.session.get_project_id() test_lb = cls.conn.load_balancer.create_load_balancer( - name=cls.NAME, vip_subnet_id=cls.VIP_SUBNET_ID, + name=cls.LB_NAME, vip_subnet_id=cls.VIP_SUBNET_ID, project_id=cls.PROJECT_ID) assert isinstance(test_lb, load_balancer.LoadBalancer) - cls.assertIs(cls.NAME, test_lb.name) + cls.assertIs(cls.LB_NAME, test_lb.name) # Wait for the LB to go ACTIVE. On non-virtualization enabled hosts # it can take nova up to ten minutes to boot a VM. cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR'], interval=1, wait=600) - cls.ID = test_lb.id + cls.LB_ID = test_lb.id + + test_listener = cls.conn.load_balancer.create_listener( + name=cls.LISTENER_NAME, protocol=cls.PROTOCOL, + protocol_port=cls.PROTOCOL_PORT, loadbalancer_id=cls.LB_ID) + assert isinstance(test_listener, listener.Listener) + cls.assertIs(cls.LISTENER_NAME, test_listener.name) + cls.LISTENER_ID = test_listener.id + cls.lb_wait_for_status(test_lb, status='ACTIVE', + failures=['ERROR']) @classmethod def tearDownClass(cls): - test_lb = cls.conn.load_balancer.get_load_balancer(cls.ID) + test_lb = cls.conn.load_balancer.get_load_balancer(cls.LB_ID) + cls.lb_wait_for_status(test_lb, status='ACTIVE', + failures=['ERROR']) + cls.conn.load_balancer.delete_listener(cls.LISTENER_ID, + ignore_missing=False) cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) cls.conn.load_balancer.delete_load_balancer( - cls.ID, ignore_missing=False) + cls.LB_ID, ignore_missing=False) - def test_find(self): - test_lb = self.conn.load_balancer.find_load_balancer(self.NAME) - self.assertEqual(self.ID, test_lb.id) + def test_lb_find(self): + test_lb = self.conn.load_balancer.find_load_balancer(self.LB_NAME) + self.assertEqual(self.LB_ID, test_lb.id) - def test_get(self): - test_lb = self.conn.load_balancer.get_load_balancer(self.ID) - self.assertEqual(self.NAME, test_lb.name) - self.assertEqual(self.ID, test_lb.id) + def test_lb_get(self): + test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) + self.assertEqual(self.LB_NAME, test_lb.name) + self.assertEqual(self.LB_ID, test_lb.id) self.assertEqual(self.VIP_SUBNET_ID, test_lb.vip_subnet_id) - def test_list(self): + def test_lb_list(self): names = [lb.name for lb in self.conn.load_balancer.load_balancers()] - self.assertIn(self.NAME, names) + self.assertIn(self.LB_NAME, names) - def test_update(self): + def test_lb_update(self): update_lb = self.conn.load_balancer.update_load_balancer( - self.ID, name=self.UPDATE_NAME) + self.LB_ID, name=self.UPDATE_NAME) self.lb_wait_for_status(update_lb, status='ACTIVE', failures=['ERROR']) - test_lb = self.conn.load_balancer.get_load_balancer(self.ID) + test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) self.assertEqual(self.UPDATE_NAME, test_lb.name) + + update_lb = self.conn.load_balancer.update_load_balancer( + self.LB_ID, name=self.LB_NAME) + self.lb_wait_for_status(update_lb, status='ACTIVE', + failures=['ERROR']) + test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) + self.assertEqual(self.LB_NAME, test_lb.name) + + def test_listener_find(self): + test_listener = self.conn.load_balancer.find_listener( + self.LISTENER_NAME) + self.assertEqual(self.LISTENER_ID, test_listener.id) + + def test_listener_get(self): + test_listener = self.conn.load_balancer.get_listener(self.LISTENER_ID) + self.assertEqual(self.LISTENER_NAME, test_listener.name) + self.assertEqual(self.LISTENER_ID, test_listener.id) + self.assertEqual(self.PROTOCOL, test_listener.protocol) + self.assertEqual(self.PROTOCOL_PORT, test_listener.protocol_port) + + def test_listener_list(self): + names = [ls.name for ls in self.conn.load_balancer.listeners()] + self.assertIn(self.LISTENER_NAME, names) + + def test_listener_update(self): + test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) + + self.conn.load_balancer.update_listener( + self.LISTENER_ID, name=self.UPDATE_NAME) + self.lb_wait_for_status(test_lb, status='ACTIVE', + failures=['ERROR']) + test_listener = self.conn.load_balancer.get_listener(self.LISTENER_ID) + self.assertEqual(self.UPDATE_NAME, test_listener.name) + + self.conn.load_balancer.update_listener( + self.LISTENER_ID, name=self.LISTENER_NAME) + self.lb_wait_for_status(test_lb, status='ACTIVE', + failures=['ERROR']) + test_listener = self.conn.load_balancer.get_listener(self.LISTENER_ID) + self.assertEqual(self.LISTENER_NAME, test_listener.name) diff --git a/openstack/tests/unit/load_balancer/test_listener.py b/openstack/tests/unit/load_balancer/test_listener.py new file mode 100644 index 000000000..84fddf61c --- /dev/null +++ b/openstack/tests/unit/load_balancer/test_listener.py @@ -0,0 +1,84 @@ +# 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 testtools +import uuid + +from openstack.load_balancer.v2 import listener + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'admin_state_up': True, + 'connection_limit': '2', + 'default_pool_id': uuid.uuid4(), + 'description': 'test description', + 'id': IDENTIFIER, + 'insert_headers': {"X-Forwarded-For": "true"}, + 'l7policies': [{'id': uuid.uuid4()}], + 'loadbalancers': [{'id': uuid.uuid4()}], + 'name': 'test_listener', + 'project_id': uuid.uuid4(), + 'protocol': 'TEST_PROTOCOL', + 'protocol_port': 10, + 'default_tls_container_ref': ('http://198.51.100.10:9311/v1/containers/' + 'a570068c-d295-4780-91d4-3046a325db51'), + 'sni_container_refs': [], + 'created_at': '2017-07-17T12:14:57.233772', + 'updated_at': '2017-07-17T12:16:57.233772', + 'operating_status': 'ONLINE', + 'provisioning_status': 'ACTIVE', +} + + +class TestListener(testtools.TestCase): + + def test_basic(self): + test_listener = listener.Listener() + self.assertEqual('listener', test_listener.resource_key) + self.assertEqual('listeners', test_listener.resources_key) + self.assertEqual('/v2.0/lbaas/listeners', test_listener.base_path) + self.assertEqual('load-balancer', test_listener.service.service_type) + self.assertTrue(test_listener.allow_create) + self.assertTrue(test_listener.allow_get) + self.assertTrue(test_listener.allow_update) + self.assertTrue(test_listener.allow_delete) + self.assertTrue(test_listener.allow_list) + + def test_make_it(self): + test_listener = listener.Listener(**EXAMPLE) + self.assertTrue(test_listener.is_admin_state_up) + self.assertEqual(EXAMPLE['connection_limit'], + test_listener.connection_limit) + self.assertEqual(EXAMPLE['default_pool_id'], + test_listener.default_pool_id) + self.assertEqual(EXAMPLE['description'], test_listener.description) + self.assertEqual(EXAMPLE['id'], test_listener.id) + self.assertEqual(EXAMPLE['insert_headers'], + test_listener.insert_headers) + self.assertEqual(EXAMPLE['l7policies'], + test_listener.l7_policies) + self.assertEqual(EXAMPLE['loadbalancers'], + test_listener.load_balancers) + self.assertEqual(EXAMPLE['name'], test_listener.name) + self.assertEqual(EXAMPLE['project_id'], test_listener.project_id) + self.assertEqual(EXAMPLE['protocol'], test_listener.protocol) + self.assertEqual(EXAMPLE['protocol_port'], test_listener.protocol_port) + self.assertEqual(EXAMPLE['default_tls_container_ref'], + test_listener.default_tls_container_ref) + self.assertEqual(EXAMPLE['sni_container_refs'], + test_listener.sni_container_refs) + self.assertEqual(EXAMPLE['created_at'], test_listener.created_at) + self.assertEqual(EXAMPLE['updated_at'], test_listener.updated_at) + self.assertEqual(EXAMPLE['provisioning_status'], + test_listener.provisioning_status) + self.assertEqual(EXAMPLE['operating_status'], + test_listener.operating_status) diff --git a/openstack/tests/unit/load_balancer/test_proxy.py b/openstack/tests/unit/load_balancer/test_proxy.py index 267ea0a98..325fb8d8f 100644 --- a/openstack/tests/unit/load_balancer/test_proxy.py +++ b/openstack/tests/unit/load_balancer/test_proxy.py @@ -11,6 +11,7 @@ # under the License. from openstack.load_balancer.v2 import _proxy +from openstack.load_balancer.v2 import listener from openstack.load_balancer.v2 import load_balancer as lb from openstack.tests.unit import test_proxy_base2 @@ -44,3 +45,28 @@ def test_load_balancer_find(self): def test_load_balancer_update(self): self.verify_update(self.proxy.update_load_balancer, lb.LoadBalancer) + + def test_listeners(self): + self.verify_list(self.proxy.listeners, + listener.Listener, + paginated=True) + + def test_listener_get(self): + self.verify_get(self.proxy.get_listener, + listener.Listener) + + def test_listener_create(self): + self.verify_create(self.proxy.create_listener, + listener.Listener) + + def test_listener_delete(self): + self.verify_delete(self.proxy.delete_listener, + listener.Listener, True) + + def test_listener_find(self): + self.verify_find(self.proxy.find_listener, + listener.Listener) + + def test_listener_update(self): + self.verify_update(self.proxy.update_listener, + listener.Listener) From 7164a24f2136ed507e638ac109dfcec3f8d2b86a Mon Sep 17 00:00:00 2001 From: Shashank Kumar Shankar Date: Thu, 23 Mar 2017 13:32:24 -0500 Subject: [PATCH 1739/3836] Introduce Pool for Octavia (load balancing) This patch introduces Resource for Pool. Co-Authored-By: Michael Johnson Change-Id: I5a701cdf80aaea0a597c83da3ad37baddf29da81 --- doc/source/users/proxies/load_balancer_v2.rst | 12 +++ .../users/resources/load_balancer/index.rst | 1 + .../users/resources/load_balancer/v2/pool.rst | 12 +++ openstack/load_balancer/v2/_proxy.py | 80 +++++++++++++++++ openstack/load_balancer/v2/pool.py | 71 ++++++++++++++++ .../load_balancer/v2/test_load_balancer.py | 53 +++++++++++- .../tests/unit/load_balancer/test_pool.py | 85 +++++++++++++++++++ .../tests/unit/load_balancer/test_proxy.py | 26 ++++++ 8 files changed, 336 insertions(+), 4 deletions(-) create mode 100644 doc/source/users/resources/load_balancer/v2/pool.rst create mode 100644 openstack/load_balancer/v2/pool.py create mode 100644 openstack/tests/unit/load_balancer/test_pool.py diff --git a/doc/source/users/proxies/load_balancer_v2.rst b/doc/source/users/proxies/load_balancer_v2.rst index 1f9c9b5ff..c7a3bbb4a 100644 --- a/doc/source/users/proxies/load_balancer_v2.rst +++ b/doc/source/users/proxies/load_balancer_v2.rst @@ -33,3 +33,15 @@ Listener Operations .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_listener .. automethod:: openstack.load_balancer.v2._proxy.Proxy.listeners .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_listener + +Pool Operations +^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.load_balancer.v2._proxy.Proxy + + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.create_pool + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_pool + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_pool + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_pool + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.pools + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_pool diff --git a/doc/source/users/resources/load_balancer/index.rst b/doc/source/users/resources/load_balancer/index.rst index d542175f3..784ec547c 100644 --- a/doc/source/users/resources/load_balancer/index.rst +++ b/doc/source/users/resources/load_balancer/index.rst @@ -6,3 +6,4 @@ Load Balancer Resources v2/load_balancer v2/listener + v2/pool diff --git a/doc/source/users/resources/load_balancer/v2/pool.rst b/doc/source/users/resources/load_balancer/v2/pool.rst new file mode 100644 index 000000000..19c04e3f1 --- /dev/null +++ b/doc/source/users/resources/load_balancer/v2/pool.rst @@ -0,0 +1,12 @@ +openstack.load_balancer.v2.pool +=============================== + +.. automodule:: openstack.load_balancer.v2.pool + +The Pool Class +------------------ + +The ``Pool`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.load_balancer.v2.pool.Pool + :members: diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index 796c209f2..db199604f 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -12,6 +12,7 @@ from openstack.load_balancer.v2 import listener as _listener from openstack.load_balancer.v2 import load_balancer as _lb +from openstack.load_balancer.v2 import pool as _pool from openstack import proxy2 @@ -176,3 +177,82 @@ def update_listener(self, listener, **attrs): :rtype: :class:`~openstack.load_balancer.v2.listener.Listener` """ return self._update(_listener.Listener, listener, **attrs) + + def create_pool(self, **attrs): + """Create a new pool from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.load_balancer.v2. + pool.Pool`, + comprised of the properties on the + Pool class. + + :returns: The results of Pool creation + :rtype: :class:`~openstack.load_balancer.v2.pool.Pool` + """ + return self._create(_pool.Pool, **attrs) + + def get_pool(self, *attrs): + """Get a pool + + :param pool: Value is + :class:`~openstack.load_balancer.v2.pool.Pool` + instance. + + :returns: One + :class:`~openstack.load_balancer.v2.pool.Pool` + """ + return self._get(_pool.Pool, *attrs) + + def pools(self, **query): + """Retrieve a generator of pools + + :returns: A generator of Pool instances + """ + return self._list(_pool.Pool, paginated=True, **query) + + def delete_pool(self, pool, ignore_missing=True): + """Delete a pool + + :param pool: The pool is a + :class:`~openstack.load_balancer.v2.pool.Pool` + instance + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the pool does not exist. + When set to ``True``, no exception will be set when attempting to + delete a nonexistent pool. + + :returns: ``None`` + """ + return self._delete(_pool.Pool, pool, + ignore_missing=ignore_missing) + + def find_pool(self, name_or_id, ignore_missing=True): + """Find a single pool + + :param name_or_id: The name or ID of a pool + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the pool does not exist. + When set to ``True``, no exception will be set when attempting + to delete a nonexistent pool. + + :returns: ``None`` + """ + return self._find(_pool.Pool, name_or_id, + ignore_missing=ignore_missing) + + def update_pool(self, pool, **attrs): + """Update a pool + + :param pool: Either the id of a pool or a + :class:`~openstack.load_balancer.v2.pool.Pool` + instance. + :param dict attrs: The attributes to update on the pool + represented by ``pool``. + + :returns: The updated pool + :rtype: :class:`~openstack.load_balancer.v2.pool.Pool` + """ + return self._update(_pool.Pool, pool, **attrs) diff --git a/openstack/load_balancer/v2/pool.py b/openstack/load_balancer/v2/pool.py new file mode 100644 index 000000000..6d72681e6 --- /dev/null +++ b/openstack/load_balancer/v2/pool.py @@ -0,0 +1,71 @@ +# 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.load_balancer import load_balancer_service as lb_service +from openstack import resource2 as resource + + +class Pool(resource.Resource): + resource_key = 'pool' + resources_key = 'pools' + base_path = '/v2.0/lbaas/pools' + service = lb_service.LoadBalancerService() + + # capabilities + allow_create = True + allow_list = True + allow_get = True + allow_delete = True + allow_update = True + + _query_mapping = resource.QueryParameters( + 'health_monitor_id', 'lb_algorithm', 'listener_id', 'loadbalancer_id', + 'description', 'name', 'project_id', 'protocol', + 'created_at', 'updated_at', 'provisioning_status', 'operating_status', + is_admin_state_up='admin_state_up' + ) + + #: Properties + #: Timestamp when the pool was created + created_at = resource.Body('created_at') + #: Description for the pool. + description = resource.Body('description') + #: Health Monitor ID + health_monitor_id = resource.Body('healthmonitor_id') + #: The administrative state of the pool *Type: bool* + is_admin_state_up = resource.Body('admin_state_up', type=bool) + #: The loadbalancing algorithm used in the pool + lb_algorithm = resource.Body('lb_algorithm') + #: ID of listener associated with this pool + listener_id = resource.Body('listener_id') + #: List of listeners associated with this pool + listeners = resource.Body('listeners', type=list) + #: ID of load balancer associated with this pool + loadbalancer_id = resource.Body('loadbalancer_id') + #: List of loadbalancers associated with this pool + loadbalancers = resource.Body('loadbalancers', type=list) + #: Members associated with this pool + members = resource.Body('members', type=list) + #: The pool name + name = resource.Body('name') + #: Operating status of the pool + operating_status = resource.Body('operating_status') + #: The ID of the project + project_id = resource.Body('project_id') + #: The protocol of the pool + protocol = resource.Body('protocol') + #: Provisioning status of the pool + provisioning_status = resource.Body('provisioning_status') + #: A JSON object specifying the session persistence for the pool. + session_persistence = resource.Body('session_persistence', type=dict) + #: Timestamp when the pool was updated + updated_at = resource.Body('updated_at') diff --git a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py index c7523af78..34335609c 100644 --- a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py +++ b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py @@ -15,6 +15,7 @@ from openstack.load_balancer.v2 import listener from openstack.load_balancer.v2 import load_balancer +from openstack.load_balancer.v2 import pool from openstack.tests.functional import base from openstack.tests.functional.load_balancer import base as lb_base @@ -25,13 +26,16 @@ class TestLoadBalancer(lb_base.BaseLBFunctionalTest): LB_NAME = uuid.uuid4().hex LISTENER_NAME = uuid.uuid4().hex + POOL_NAME = uuid.uuid4().hex UPDATE_NAME = uuid.uuid4().hex LB_ID = None LISTENER_ID = None + POOL_ID = None VIP_SUBNET_ID = None PROJECT_ID = None PROTOCOL = 'HTTP' PROTOCOL_PORT = 80 + LB_ALGORITHM = 'ROUND_ROBIN' # Note: Creating load balancers can be slow on some hosts due to nova # instance boot times (up to ten minutes) so we are consolidating @@ -62,15 +66,27 @@ def setUpClass(cls): cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + test_pool = cls.conn.load_balancer.create_pool( + name=cls.POOL_NAME, protocol=cls.PROTOCOL, + lb_algorithm=cls.LB_ALGORITHM, listener_id=cls.LISTENER_ID) + assert isinstance(test_pool, pool.Pool) + cls.assertIs(cls.POOL_NAME, test_pool.name) + cls.POOL_ID = test_pool.id + cls.lb_wait_for_status(test_lb, status='ACTIVE', + failures=['ERROR']) + @classmethod def tearDownClass(cls): test_lb = cls.conn.load_balancer.get_load_balancer(cls.LB_ID) - cls.lb_wait_for_status(test_lb, status='ACTIVE', - failures=['ERROR']) + cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + + cls.conn.load_balancer.delete_pool(cls.POOL_ID, ignore_missing=False) + cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + cls.conn.load_balancer.delete_listener(cls.LISTENER_ID, ignore_missing=False) - cls.lb_wait_for_status(test_lb, status='ACTIVE', - failures=['ERROR']) + cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + cls.conn.load_balancer.delete_load_balancer( cls.LB_ID, ignore_missing=False) @@ -135,3 +151,32 @@ def test_listener_update(self): failures=['ERROR']) test_listener = self.conn.load_balancer.get_listener(self.LISTENER_ID) self.assertEqual(self.LISTENER_NAME, test_listener.name) + + def test_pool_find(self): + test_pool = self.conn.load_balancer.find_pool(self.POOL_NAME) + self.assertEqual(self.POOL_ID, test_pool.id) + + def test_pool_get(self): + test_pool = self.conn.load_balancer.get_pool(self.POOL_ID) + self.assertEqual(self.POOL_NAME, test_pool.name) + self.assertEqual(self.POOL_ID, test_pool.id) + self.assertEqual(self.PROTOCOL, test_pool.protocol) + + def test_pool_list(self): + names = [pool.name for pool in self.conn.load_balancer.pools()] + self.assertIn(self.POOL_NAME, names) + + def test_pool_update(self): + test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) + + self.conn.load_balancer.update_pool(self.POOL_ID, + name=self.UPDATE_NAME) + self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + test_pool = self.conn.load_balancer.get_pool(self.POOL_ID) + self.assertEqual(self.UPDATE_NAME, test_pool.name) + + self.conn.load_balancer.update_pool(self.POOL_ID, + name=self.POOL_NAME) + self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + test_pool = self.conn.load_balancer.get_pool(self.POOL_ID) + self.assertEqual(self.POOL_NAME, test_pool.name) diff --git a/openstack/tests/unit/load_balancer/test_pool.py b/openstack/tests/unit/load_balancer/test_pool.py new file mode 100644 index 000000000..2000fb012 --- /dev/null +++ b/openstack/tests/unit/load_balancer/test_pool.py @@ -0,0 +1,85 @@ +# 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 testtools +import uuid + +from openstack.load_balancer.v2 import pool + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'name': 'test_pool', + 'description': 'fake_description', + 'admin_state_up': True, + 'provisioning_status': 'ACTIVE', + 'operating_status': 'ONLINE', + 'protocol': 'HTTP', + 'listener_id': uuid.uuid4(), + 'loadbalancer_id': uuid.uuid4(), + 'lb_algorithm': 'ROUND_ROBIN', + 'session_persistence': {"type": "SOURCE_IP"}, + 'project_id': uuid.uuid4(), + 'loadbalancers': [{'id': uuid.uuid4()}], + 'listeners': [{'id': uuid.uuid4()}], + 'created_at': '2017-07-17T12:14:57.233772', + 'updated_at': '2017-07-17T12:16:57.233772', + 'health_monitor': 'healthmonitor', + 'health_monitor_id': uuid.uuid4(), + 'members': [{'id': uuid.uuid4()}] +} + + +class TestPool(testtools.TestCase): + + def test_basic(self): + test_pool = pool.Pool() + self.assertEqual('pool', test_pool.resource_key) + self.assertEqual('pools', test_pool.resources_key) + self.assertEqual('/v2.0/lbaas/pools', test_pool.base_path) + self.assertEqual('load-balancer', + test_pool.service.service_type) + self.assertTrue(test_pool.allow_create) + self.assertTrue(test_pool.allow_get) + self.assertTrue(test_pool.allow_delete) + self.assertTrue(test_pool.allow_list) + self.assertTrue(test_pool.allow_update) + + def test_make_it(self): + test_pool = pool.Pool(**EXAMPLE) + self.assertEqual(EXAMPLE['name'], test_pool.name), + self.assertEqual(EXAMPLE['description'], + test_pool.description) + self.assertEqual(EXAMPLE['admin_state_up'], + test_pool.is_admin_state_up) + self.assertEqual(EXAMPLE['provisioning_status'], + test_pool.provisioning_status) + self.assertEqual(EXAMPLE['protocol'], test_pool.protocol) + self.assertEqual(EXAMPLE['operating_status'], + test_pool.operating_status) + self.assertEqual(EXAMPLE['listener_id'], test_pool.listener_id) + self.assertEqual(EXAMPLE['loadbalancer_id'], + test_pool.loadbalancer_id) + self.assertEqual(EXAMPLE['lb_algorithm'], + test_pool.lb_algorithm) + self.assertEqual(EXAMPLE['session_persistence'], + test_pool.session_persistence) + self.assertEqual(EXAMPLE['project_id'], + test_pool.project_id) + self.assertEqual(EXAMPLE['loadbalancers'], + test_pool.loadbalancers) + self.assertEqual(EXAMPLE['listeners'], + test_pool.listeners) + self.assertEqual(EXAMPLE['created_at'], test_pool.created_at) + self.assertEqual(EXAMPLE['updated_at'], test_pool.updated_at) + self.assertEqual(EXAMPLE['health_monitor_id'], + test_pool.health_monitor_id) + self.assertEqual(EXAMPLE['members'], test_pool.members) diff --git a/openstack/tests/unit/load_balancer/test_proxy.py b/openstack/tests/unit/load_balancer/test_proxy.py index 325fb8d8f..7471ea5df 100644 --- a/openstack/tests/unit/load_balancer/test_proxy.py +++ b/openstack/tests/unit/load_balancer/test_proxy.py @@ -13,6 +13,7 @@ from openstack.load_balancer.v2 import _proxy from openstack.load_balancer.v2 import listener from openstack.load_balancer.v2 import load_balancer as lb +from openstack.load_balancer.v2 import pool from openstack.tests.unit import test_proxy_base2 @@ -70,3 +71,28 @@ def test_listener_find(self): def test_listener_update(self): self.verify_update(self.proxy.update_listener, listener.Listener) + + def test_pools(self): + self.verify_list(self.proxy.pools, + pool.Pool, + paginated=True) + + def test_pool_get(self): + self.verify_get(self.proxy.get_pool, + pool.Pool) + + def test_pool_create(self): + self.verify_create(self.proxy.create_pool, + pool.Pool) + + def test_pool_delete(self): + self.verify_delete(self.proxy.delete_pool, + pool.Pool, True) + + def test_pool_find(self): + self.verify_find(self.proxy.find_pool, + pool.Pool) + + def test_pool_update(self): + self.verify_update(self.proxy.update_pool, + pool.Pool) From bc4c4e6c35b65e797a582c42455f3ab40b5501bf Mon Sep 17 00:00:00 2001 From: liyi Date: Tue, 15 Aug 2017 16:36:08 +0800 Subject: [PATCH 1740/3836] Fix stack_file function return body The function return response object, but not body. Change-Id: I45a4f2a2092bb1a7d15830c469e5696f8d8fc565 Closes-Bug: #1710829 --- openstack/orchestration/v1/stack_files.py | 3 ++- .../unit/orchestration/v1/test_stack_files.py | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/openstack/orchestration/v1/stack_files.py b/openstack/orchestration/v1/stack_files.py index b1fb24574..db23a4771 100644 --- a/openstack/orchestration/v1/stack_files.py +++ b/openstack/orchestration/v1/stack_files.py @@ -36,4 +36,5 @@ def get(self, session): # The stack files response contains a map of filenames and file # contents. request = self._prepare_request(requires_id=False) - return session.get(request.uri, endpoint_filter=self.service) + resp = session.get(request.uri, endpoint_filter=self.service) + return resp.json() diff --git a/openstack/tests/unit/orchestration/v1/test_stack_files.py b/openstack/tests/unit/orchestration/v1/test_stack_files.py index 58d8def54..989121a14 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack_files.py +++ b/openstack/tests/unit/orchestration/v1/test_stack_files.py @@ -10,9 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +import mock import testtools from openstack.orchestration.v1 import stack_files as sf +from openstack import resource2 as resource FAKE = { 'stack_id': 'ID', @@ -35,3 +37,25 @@ def test_make_it(self): sot = sf.StackFiles(**FAKE) self.assertEqual(FAKE['stack_id'], sot.stack_id) self.assertEqual(FAKE['stack_name'], sot.stack_name) + + @mock.patch.object(resource.Resource, '_prepare_request') + def test_get(self, mock_prepare_request): + resp = mock.Mock() + resp.json = mock.Mock(return_value={'file': 'file-content'}) + + sess = mock.Mock() + sess.get = mock.Mock(return_value=resp) + + sot = sf.StackFiles(**FAKE) + sot.service = mock.Mock() + + req = mock.MagicMock() + req.uri = ('/stacks/%(stack_name)s/%(stack_id)s/files' % + {'stack_name': FAKE['stack_name'], + 'stack_id': FAKE['stack_id']}) + mock_prepare_request.return_value = req + + files = sot.get(sess) + + sess.get.assert_called_once_with(req.uri, endpoint_filter=sot.service) + self.assertEqual({'file': 'file-content'}, files) From e72a6cf6da2456c888d925b849ea544bba92f357 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Wed, 16 Aug 2017 20:31:27 +0000 Subject: [PATCH 1741/3836] Don't determine local IPv6 support if force_ip4=True In case when user set in config to force usage of IPv4 shade will not try to check if IPv6 is locally supported or not. Change-Id: Id12df5de0de58bbe427301c8a32d4c622dfbcc99 Related-Bug: #2001001 --- shade/openstackcloud.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index fd5eca380..935139bbc 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -297,7 +297,8 @@ def invalidate(self): self._legacy_clients = {} self._raw_clients = {} - self._local_ipv6 = _utils.localhost_supports_ipv6() + self._local_ipv6 = ( + _utils.localhost_supports_ipv6() if not self.force_ipv4 else False) self.cloud_config = cloud_config From 89e07d905a94525a0813e6e467da9f5cd94a1c8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Wed, 16 Aug 2017 20:36:01 +0000 Subject: [PATCH 1742/3836] Fix determining if IPv6 is supported when it's disabled On systems where IPv6 is completly disabled there can be AttributeError when shade tries to determine if IPv6 is supported or not on local system. Now it's fixed by returning False always when AF_INET6 isn't available in netifaces. Change-Id: I45138bd42cb63ff59b6aed85f33da11aab43aaba Related-Bug: #2001001 --- shade/_utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/shade/_utils.py b/shade/_utils.py index bf8abd6f7..bd57a3fdf 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -259,7 +259,10 @@ def localhost_supports_ipv6(): IPv6 connectivity. """ - return netifaces.AF_INET6 in netifaces.gateways()['default'] + try: + return netifaces.AF_INET6 in netifaces.gateways()['default'] + except AttributeError: + return False def normalize_users(users): From f6b01f86b23d8d502a9141bbd6bcdd9fc2733561 Mon Sep 17 00:00:00 2001 From: Nakul Dahiwade Date: Wed, 29 Mar 2017 16:58:30 +0000 Subject: [PATCH 1743/3836] Introduce Member for Octavia (load balancing) This patch introduces Resource for Member. Co-Authored-By: Michael Johnson Change-Id: Ie325270096bff313f11cabe2cba7129dd02acd6f --- doc/source/users/proxies/load_balancer_v2.rst | 12 ++ .../users/resources/load_balancer/index.rst | 1 + .../resources/load_balancer/v2/member.rst | 12 ++ openstack/load_balancer/v2/_proxy.py | 112 ++++++++++++++++++ openstack/load_balancer/v2/member.py | 69 +++++++++++ .../load_balancer/v2/test_load_balancer.py | 54 +++++++++ .../tests/unit/load_balancer/test_member.py | 62 ++++++++++ .../tests/unit/load_balancer/test_proxy.py | 47 ++++++++ 8 files changed, 369 insertions(+) create mode 100644 doc/source/users/resources/load_balancer/v2/member.rst create mode 100644 openstack/load_balancer/v2/member.py create mode 100644 openstack/tests/unit/load_balancer/test_member.py diff --git a/doc/source/users/proxies/load_balancer_v2.rst b/doc/source/users/proxies/load_balancer_v2.rst index c7a3bbb4a..0c728c07d 100644 --- a/doc/source/users/proxies/load_balancer_v2.rst +++ b/doc/source/users/proxies/load_balancer_v2.rst @@ -45,3 +45,15 @@ Pool Operations .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_pool .. automethod:: openstack.load_balancer.v2._proxy.Proxy.pools .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_pool + +Member Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.load_balancer.v2._proxy.Proxy + + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.create_member + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_member + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_member + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_member + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.members + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_member diff --git a/doc/source/users/resources/load_balancer/index.rst b/doc/source/users/resources/load_balancer/index.rst index 784ec547c..e007d7af7 100644 --- a/doc/source/users/resources/load_balancer/index.rst +++ b/doc/source/users/resources/load_balancer/index.rst @@ -7,3 +7,4 @@ Load Balancer Resources v2/load_balancer v2/listener v2/pool + v2/member diff --git a/doc/source/users/resources/load_balancer/v2/member.rst b/doc/source/users/resources/load_balancer/v2/member.rst new file mode 100644 index 000000000..1fff01f07 --- /dev/null +++ b/doc/source/users/resources/load_balancer/v2/member.rst @@ -0,0 +1,12 @@ +openstack.load_balancer.v2.member +================================= + +.. automodule:: openstack.load_balancer.v2.member + +The Member Class +---------------- + +The ``Member`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.load_balancer.v2.member.Member + :members: diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index db199604f..2f27c786e 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -12,6 +12,7 @@ from openstack.load_balancer.v2 import listener as _listener from openstack.load_balancer.v2 import load_balancer as _lb +from openstack.load_balancer.v2 import member as _member from openstack.load_balancer.v2 import pool as _pool from openstack import proxy2 @@ -256,3 +257,114 @@ def update_pool(self, pool, **attrs): :rtype: :class:`~openstack.load_balancer.v2.pool.Pool` """ return self._update(_pool.Pool, pool, **attrs) + + def create_member(self, pool, **attrs): + """Create a new member from attributes + + :param pool: The pool can be either the ID of a pool or a + :class:`~openstack.load_balancer.v2.pool.Pool` instance + that the member will be created in. + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.load_balancer.v2.member.Member`, + comprised of the properties on the Member class. + + :returns: The results of member creation + :rtype: :class:`~openstack.load_balancer.v2.member.Member` + """ + poolobj = self._get_resource(_pool.Pool, pool) + return self._create(_member.Member, pool_id=poolobj.id, + **attrs) + + def delete_member(self, member, pool, ignore_missing=True): + """Delete a member + + :param member: + The member can be either the ID of a member or a + :class:`~openstack.load_balancer.v2.member.Member` instance. + :param pool: The pool can be either the ID of a pool or a + :class:`~openstack.load_balancer.v2.pool.Pool` instance + that the member belongs to. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the member does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent member. + + :returns: ``None`` + """ + poolobj = self._get_resource(_pool.Pool, pool) + self._delete(_member.Member, member, + ignore_missing=ignore_missing, pool_id=poolobj.id) + + def find_member(self, name_or_id, pool, ignore_missing=True): + """Find a single member + + :param str name_or_id: The name or ID of a member. + :param pool: The pool can be either the ID of a pool or a + :class:`~openstack.load_balancer.v2.pool.Pool` instance + that the member belongs to. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + + :returns: One :class:`~openstack.load_balancer.v2.member.Member` + or None + """ + poolobj = self._get_resource(_pool.Pool, pool) + return self._find(_member.Member, name_or_id, + ignore_missing=ignore_missing, pool_id=poolobj.id) + + def get_member(self, member, pool): + """Get a single member + + :param member: The member can be the ID of a member or a + :class:`~openstack.load_balancer.v2.member.Member` + instance. + :param pool: The pool can be either the ID of a pool or a + :class:`~openstack.load_balancer.v2.pool.Pool` instance + that the member belongs to. + + :returns: One :class:`~openstack.load_balancer.v2.member.Member` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + poolobj = self._get_resource(_pool.Pool, pool) + return self._get(_member.Member, member, + pool_id=poolobj.id) + + def members(self, pool, **query): + """Return a generator of members + + :param pool: The pool can be either the ID of a pool or a + :class:`~openstack.load_balancer.v2.pool.Pool` instance + that the member belongs to. + :param dict query: Optional query parameters to be sent to limit + the resources being returned. Valid parameters are: + + :returns: A generator of member objects + :rtype: :class:`~openstack.load_balancer.v2.member.Member` + """ + poolobj = self._get_resource(_pool.Pool, pool) + return self._list(_member.Member, paginated=True, + pool_id=poolobj.id, **query) + + def update_member(self, member, pool, **attrs): + """Update a member + + :param member: Either the ID of a member or a + :class:`~openstack.load_balancer.v2.member.Member` + instance. + :param pool: The pool can be either the ID of a pool or a + :class:`~openstack.load_balancer.v2.pool.Pool` instance + that the member belongs to. + :param dict attrs: The attributes to update on the member + represented by ``member``. + + :returns: The updated member + :rtype: :class:`~openstack.load_balancer.v2.member.Member` + """ + poolobj = self._get_resource(_pool.Pool, pool) + return self._update(_member.Member, member, + pool_id=poolobj.id, **attrs) diff --git a/openstack/load_balancer/v2/member.py b/openstack/load_balancer/v2/member.py new file mode 100644 index 000000000..f40b4dd83 --- /dev/null +++ b/openstack/load_balancer/v2/member.py @@ -0,0 +1,69 @@ +# 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.load_balancer import load_balancer_service as lb_service +from openstack import resource2 as resource + + +class Member(resource.Resource): + resource_key = 'member' + resources_key = 'members' + base_path = '/v2.0/lbaas/pools/%(pool_id)s/members' + service = lb_service.LoadBalancerService() + + # capabilities + allow_create = True + allow_get = True + allow_update = True + allow_delete = True + allow_list = True + + _query_mapping = resource.QueryParameters( + 'address', 'name', 'protocol_port', 'subnet_id', 'weight', + 'created_at', 'updated_at', 'provisioning_status', 'operating_status', + 'project_id', 'monitor_address', 'monitor_port', + is_admin_state_up='admin_state_up', + ) + + # Properties + #: The IP address of the member. + address = resource.Body('address') + #: Timestamp when the member was created. + created_at = resource.Body('created_at') + #: The administrative state of the member, which is up ``True`` or + #: down ``False``. *Type: bool* + is_admin_state_up = resource.Body('admin_state_up', type=bool) + #: IP address used to monitor this member + monitor_address = resource.Body('monitor_address') + #: Port used to monitor this member + monitor_port = resource.Body('monitor_port', type=int) + #: Name of the member. + name = resource.Body('name') + #: Operating status of the member. + operating_status = resource.Body('operating_status') + #: The ID of the owning pool. + pool_id = resource.URI('pool_id') + #: The provisioning status of this member. + provisioning_status = resource.Body('provisioning_status') + #: The ID of the project this member is associated with. + project_id = resource.Body('project_id') + #: The port on which the application is hosted. + protocol_port = resource.Body('protocol_port', type=int) + #: Subnet ID in which to access this member. + subnet_id = resource.Body('subnet_id') + #: Timestamp when the member was last updated. + updated_at = resource.Body('updated_at') + #: A positive integer value that indicates the relative portion of traffic + #: that this member should receive from the pool. For example, a member + #: with a weight of 10 receives five times as much traffic as a member + #: with weight of 2. + weight = resource.Body('weight', type=int) diff --git a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py index 34335609c..9b47ad694 100644 --- a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py +++ b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py @@ -15,6 +15,7 @@ from openstack.load_balancer.v2 import listener from openstack.load_balancer.v2 import load_balancer +from openstack.load_balancer.v2 import member from openstack.load_balancer.v2 import pool from openstack.tests.functional import base from openstack.tests.functional.load_balancer import base as lb_base @@ -26,16 +27,20 @@ class TestLoadBalancer(lb_base.BaseLBFunctionalTest): LB_NAME = uuid.uuid4().hex LISTENER_NAME = uuid.uuid4().hex + MEMBER_NAME = uuid.uuid4().hex POOL_NAME = uuid.uuid4().hex UPDATE_NAME = uuid.uuid4().hex LB_ID = None LISTENER_ID = None + MEMBER_ID = None POOL_ID = None VIP_SUBNET_ID = None PROJECT_ID = None PROTOCOL = 'HTTP' PROTOCOL_PORT = 80 LB_ALGORITHM = 'ROUND_ROBIN' + MEMBER_ADDRESS = '192.0.2.16' + WEIGHT = 10 # Note: Creating load balancers can be slow on some hosts due to nova # instance boot times (up to ten minutes) so we are consolidating @@ -75,11 +80,24 @@ def setUpClass(cls): cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + test_member = cls.conn.load_balancer.create_member( + pool=cls.POOL_ID, name=cls.MEMBER_NAME, address=cls.MEMBER_ADDRESS, + protocol_port=cls.PROTOCOL_PORT, weight=cls.WEIGHT) + assert isinstance(test_member, member.Member) + cls.assertIs(cls.MEMBER_NAME, test_member.name) + cls.MEMBER_ID = test_member.id + cls.lb_wait_for_status(test_lb, status='ACTIVE', + failures=['ERROR']) + @classmethod def tearDownClass(cls): test_lb = cls.conn.load_balancer.get_load_balancer(cls.LB_ID) cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + cls.conn.load_balancer.delete_member( + cls.MEMBER_ID, cls.POOL_ID, ignore_missing=False) + cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + cls.conn.load_balancer.delete_pool(cls.POOL_ID, ignore_missing=False) cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) @@ -180,3 +198,39 @@ def test_pool_update(self): self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) test_pool = self.conn.load_balancer.get_pool(self.POOL_ID) self.assertEqual(self.POOL_NAME, test_pool.name) + + def test_member_find(self): + test_member = self.conn.load_balancer.find_member(self.MEMBER_NAME, + self.POOL_ID) + self.assertEqual(self.MEMBER_ID, test_member.id) + + def test_member_get(self): + test_member = self.conn.load_balancer.get_member(self.MEMBER_ID, + self.POOL_ID) + self.assertEqual(self.MEMBER_NAME, test_member.name) + self.assertEqual(self.MEMBER_ID, test_member.id) + self.assertEqual(self.MEMBER_ADDRESS, test_member.address) + self.assertEqual(self.PROTOCOL_PORT, test_member.protocol_port) + self.assertEqual(self.WEIGHT, test_member.weight) + + def test_member_list(self): + names = [mb.name for mb in self.conn.load_balancer.members( + self.POOL_ID)] + self.assertIn(self.MEMBER_NAME, names) + + def test_member_update(self): + test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) + + self.conn.load_balancer.update_member(self.MEMBER_ID, self.POOL_ID, + name=self.UPDATE_NAME) + self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + test_member = self.conn.load_balancer.get_member(self.MEMBER_ID, + self.POOL_ID) + self.assertEqual(self.UPDATE_NAME, test_member.name) + + self.conn.load_balancer.update_member(self.MEMBER_ID, self.POOL_ID, + name=self.MEMBER_NAME) + self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + test_member = self.conn.load_balancer.get_member(self.MEMBER_ID, + self.POOL_ID) + self.assertEqual(self.MEMBER_NAME, test_member.name) diff --git a/openstack/tests/unit/load_balancer/test_member.py b/openstack/tests/unit/load_balancer/test_member.py new file mode 100644 index 000000000..64446dfe9 --- /dev/null +++ b/openstack/tests/unit/load_balancer/test_member.py @@ -0,0 +1,62 @@ +# 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 testtools +import uuid + +from openstack.load_balancer.v2 import member + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'address': '192.0.2.16', + 'admin_state_up': True, + 'id': IDENTIFIER, + 'monitor_address': '192.0.2.17', + 'monitor_port': 9, + 'name': 'test_member', + 'pool_id': uuid.uuid4(), + 'project_id': uuid.uuid4(), + 'protocol_port': 5, + 'subnet_id': uuid.uuid4(), + 'weight': 7, +} + + +class TestPoolMember(testtools.TestCase): + + def test_basic(self): + test_member = member.Member() + self.assertEqual('member', test_member.resource_key) + self.assertEqual('members', test_member.resources_key) + self.assertEqual('/v2.0/lbaas/pools/%(pool_id)s/members', + test_member.base_path) + self.assertEqual('load-balancer', test_member.service.service_type) + self.assertTrue(test_member.allow_create) + self.assertTrue(test_member.allow_get) + self.assertTrue(test_member.allow_update) + self.assertTrue(test_member.allow_delete) + self.assertTrue(test_member.allow_list) + + def test_make_it(self): + test_member = member.Member(**EXAMPLE) + self.assertEqual(EXAMPLE['address'], test_member.address) + self.assertTrue(test_member.is_admin_state_up) + self.assertEqual(EXAMPLE['id'], test_member.id) + self.assertEqual(EXAMPLE['monitor_address'], + test_member.monitor_address) + self.assertEqual(EXAMPLE['monitor_port'], test_member.monitor_port) + self.assertEqual(EXAMPLE['name'], test_member.name) + self.assertEqual(EXAMPLE['pool_id'], test_member.pool_id) + self.assertEqual(EXAMPLE['project_id'], test_member.project_id) + self.assertEqual(EXAMPLE['protocol_port'], test_member.protocol_port) + self.assertEqual(EXAMPLE['subnet_id'], test_member.subnet_id) + self.assertEqual(EXAMPLE['weight'], test_member.weight) diff --git a/openstack/tests/unit/load_balancer/test_proxy.py b/openstack/tests/unit/load_balancer/test_proxy.py index 7471ea5df..5c4b1d554 100644 --- a/openstack/tests/unit/load_balancer/test_proxy.py +++ b/openstack/tests/unit/load_balancer/test_proxy.py @@ -10,14 +10,20 @@ # License for the specific language governing permissions and limitations # under the License. +import uuid + from openstack.load_balancer.v2 import _proxy from openstack.load_balancer.v2 import listener from openstack.load_balancer.v2 import load_balancer as lb +from openstack.load_balancer.v2 import member from openstack.load_balancer.v2 import pool from openstack.tests.unit import test_proxy_base2 class TestLoadBalancerProxy(test_proxy_base2.TestProxyBase): + + POOL_ID = uuid.uuid4() + def setUp(self): super(TestLoadBalancerProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) @@ -96,3 +102,44 @@ def test_pool_find(self): def test_pool_update(self): self.verify_update(self.proxy.update_pool, pool.Pool) + + def test_members(self): + self.verify_list(self.proxy.members, + member.Member, + paginated=True, + method_kwargs={'pool': self.POOL_ID}, + expected_kwargs={'pool_id': self.POOL_ID}) + + def test_member_get(self): + self.verify_get(self.proxy.get_member, + member.Member, + method_kwargs={'pool': self.POOL_ID}, + expected_kwargs={'pool_id': self.POOL_ID}) + + def test_member_create(self): + self.verify_create(self.proxy.create_member, + member.Member, + method_kwargs={'pool': self.POOL_ID}, + expected_kwargs={'pool_id': self.POOL_ID}) + + def test_member_delete(self): + self.verify_delete(self.proxy.delete_member, + member.Member, + True, + method_kwargs={'pool': self.POOL_ID}, + expected_kwargs={'pool_id': self.POOL_ID}) + + def test_member_find(self): + self._verify2('openstack.proxy2.BaseProxy._find', + self.proxy.find_member, + method_args=["MEMBER", self.POOL_ID], + expected_args=[member.Member, "MEMBER"], + expected_kwargs={"pool_id": self.POOL_ID, + "ignore_missing": True}) + + def test_member_update(self): + self._verify2('openstack.proxy2.BaseProxy._update', + self.proxy.update_member, + method_args=["MEMBER", self.POOL_ID], + expected_args=[member.Member, "MEMBER"], + expected_kwargs={"pool_id": self.POOL_ID}) From d6ce9b719b6cca9d957b8b13b933ad1a6c6ffc1e Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 18 Aug 2017 04:52:15 +0000 Subject: [PATCH 1744/3836] Updated from global requirements Change-Id: Ic16abb7dbc290d7ae0da1f185c7375a7471d67dc --- test-requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 9c9ff8bdc..c82640c82 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -6,12 +6,12 @@ hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 beautifulsoup4>=4.6.0 # MIT coverage!=4.4,>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD -mock>=2.0 # BSD +mock>=2.0.0 # BSD python-subunit>=0.0.18 # Apache-2.0/BSD openstackdocstheme>=1.16.0 # Apache-2.0 os-testr>=0.8.0 # Apache-2.0 requests>=2.14.2 # Apache-2.0 -requests-mock>=1.1 # Apache-2.0 +requests-mock>=1.1.0 # Apache-2.0 sphinx>=1.6.2 # BSD testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD From 5a7da1d9d97a1beca65c9572084b0dccc12a298e Mon Sep 17 00:00:00 2001 From: TingtingYu Date: Fri, 18 Aug 2017 16:26:43 +0800 Subject: [PATCH 1745/3836] fix the bug that cannot create a pool by openstacksdk When I create a pool with a listener id or a loadbalance id, there is an error with the message "Pool must be created with a loadbalancer or listener." Then I find the code has no the parameter that "loadbalancer_id". I modify the pool class, I add new lines load_balancer_id = resource.Body('loadbalancer_id'),listener_id = resource.Body('listener_id'), then successfully create a pool. Change-Id: I37f041de8284c23fdb3291d932548aa1a1a9f00a Closes-Bug: 1711528 --- openstack/network/v2/pool.py | 7 ++++++- openstack/tests/unit/network/v2/test_pool.py | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/openstack/network/v2/pool.py b/openstack/network/v2/pool.py index 27888d89a..328d51afa 100644 --- a/openstack/network/v2/pool.py +++ b/openstack/network/v2/pool.py @@ -29,9 +29,10 @@ class Pool(resource.Resource): _query_mapping = resource.QueryParameters( 'description', 'lb_algorithm', 'name', - 'protocol', 'provider', 'subnet_id', 'virtual_ip_id', + 'protocol', 'provider', 'subnet_id', 'virtual_ip_id', 'listener_id', is_admin_state_up='admin_state_up', project_id='tenant_id', + load_balancer_id='loadbalancer_id', ) # Properties @@ -51,9 +52,13 @@ class Pool(resource.Resource): #: List of associated listeners. #: *Type: list of dicts which contain the listener IDs* listener_ids = resource.Body('listeners', type=list) + #: ID of listener associated with this pool + listener_id = resource.Body('listener_id') #: List of associated load balancers. #: *Type: list of dicts which contain the load balancer IDs* load_balancer_ids = resource.Body('loadbalancers', type=list) + #: ID of load balancer associated with this pool + load_balancer_id = resource.Body('loadbalancer_id') #: List of members that belong to the pool. #: *Type: list of dicts which contain the member IDs* member_ids = resource.Body('members', type=list) diff --git a/openstack/tests/unit/network/v2/test_pool.py b/openstack/tests/unit/network/v2/test_pool.py index e8e3c8849..fb580812d 100644 --- a/openstack/tests/unit/network/v2/test_pool.py +++ b/openstack/tests/unit/network/v2/test_pool.py @@ -23,6 +23,7 @@ 'id': IDENTIFIER, 'lb_algorithm': '5', 'listeners': [{'id': '6'}], + 'listener_id': '6', 'members': [{'id': '7'}], 'name': '8', 'tenant_id': '9', @@ -33,6 +34,7 @@ 'status_description': '14', 'subnet_id': '15', 'loadbalancers': [{'id': '16'}], + 'loadbalancer_id': '16', 'vip_id': '17', } @@ -61,6 +63,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['lb_algorithm'], sot.lb_algorithm) self.assertEqual(EXAMPLE['listeners'], sot.listener_ids) + self.assertEqual(EXAMPLE['listener_id'], sot.listener_id) self.assertEqual(EXAMPLE['members'], sot.member_ids) self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) @@ -72,4 +75,5 @@ def test_make_it(self): self.assertEqual(EXAMPLE['status_description'], sot.status_description) self.assertEqual(EXAMPLE['subnet_id'], sot.subnet_id) self.assertEqual(EXAMPLE['loadbalancers'], sot.load_balancer_ids) + self.assertEqual(EXAMPLE['loadbalancer_id'], sot.load_balancer_id) self.assertEqual(EXAMPLE['vip_id'], sot.virtual_ip_id) From b5af1ae9367db42a50fa926f0f0bcd3f2ea56e9a Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 18 Aug 2017 11:39:22 +0000 Subject: [PATCH 1746/3836] Updated from global requirements Change-Id: I805c30c0a522c03721a97118594030b9c8dfcd51 --- requirements.txt | 2 +- test-requirements.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index b4387d3ba..8f0e21321 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ # 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. -PyYAML>=3.10.0 # MIT +PyYAML>=3.10 # MIT appdirs>=1.3.0 # MIT License keystoneauth1>=3.1.0 # Apache-2.0 requestsexceptions>=1.2.0 # Apache-2.0 diff --git a/test-requirements.txt b/test-requirements.txt index cb252f268..9a3518a43 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,13 +9,13 @@ docutils>=0.11 # OSI-Approved Open Source, Public Domain extras # MIT fixtures>=3.0.0 # Apache-2.0/BSD jsonschema!=2.5.0,<3.0.0,>=2.0.0 # MIT -mock>=2.0 # BSD +mock>=2.0.0 # BSD python-glanceclient>=2.8.0 # Apache-2.0 python-subunit>=0.0.18 # Apache-2.0/BSD sphinx>=1.6.2 # BSD openstackdocstheme>=1.16.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 -reno!=2.3.1,>=1.8.0 # Apache-2.0 +reno>=2.5.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=1.4.0 # MIT From a045f9741fa3068d7eaac17b903bca676f69e240 Mon Sep 17 00:00:00 2001 From: TingtingYu Date: Fri, 18 Aug 2017 17:26:05 +0800 Subject: [PATCH 1747/3836] Add required pool_id property to HealthMonitor When I create a healthmonitor with a pool id by openstack sdk, there is an error with the message "Failed to parse request. Required attribute 'pool_id' not specified", Then I find the code has no the parameter that "pool_id". I modify the healthmonitor class, I add a new line pool_id = resource.Body('pool_id'), then successfully create a healthmonitor. Change-Id: I95901152849341862333f1b3694c6265c43879ce Closes-Bug: 1711546 --- openstack/network/v2/health_monitor.py | 2 ++ openstack/tests/unit/network/v2/test_health_monitor.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/openstack/network/v2/health_monitor.py b/openstack/network/v2/health_monitor.py index 1a636fe1b..74be84183 100644 --- a/openstack/network/v2/health_monitor.py +++ b/openstack/network/v2/health_monitor.py @@ -51,6 +51,8 @@ class HealthMonitor(resource.Resource): #: List of pools associated with this health monitor #: *Type: list of dicts which contain the pool IDs* pool_ids = resource.Body('pools', type=list) + #: The ID of the pool associated with this health monitor + pool_id = resource.Body('pool_id') #: The ID of the project this health monitor is associated with. project_id = resource.Body('tenant_id') #: The maximum number of seconds for a monitor to wait for a diff --git a/openstack/tests/unit/network/v2/test_health_monitor.py b/openstack/tests/unit/network/v2/test_health_monitor.py index b5d89fae4..1198a102e 100644 --- a/openstack/tests/unit/network/v2/test_health_monitor.py +++ b/openstack/tests/unit/network/v2/test_health_monitor.py @@ -23,6 +23,7 @@ 'id': IDENTIFIER, 'max_retries': '6', 'pools': [{'id': '7'}], + 'pool_id': '7', 'tenant_id': '8', 'timeout': '9', 'type': '10', @@ -54,6 +55,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['max_retries'], sot.max_retries) self.assertEqual(EXAMPLE['pools'], sot.pool_ids) + self.assertEqual(EXAMPLE['pool_id'], sot.pool_id) self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) self.assertEqual(EXAMPLE['timeout'], sot.timeout) self.assertEqual(EXAMPLE['type'], sot.type) From 616abc354ea71e1f525592e559786f27168a1c00 Mon Sep 17 00:00:00 2001 From: Sindhu Devale Date: Tue, 4 Apr 2017 16:10:54 +0000 Subject: [PATCH 1748/3836] Introduce Health Monitor for Octavia Adds the health monitor resources for Octavia. Co-Authored-By: Michael Johnson Change-Id: I0c7232d166673563fabb9d9eba74c5849ce9e3cd --- doc/source/users/proxies/load_balancer_v2.rst | 12 ++ .../users/resources/load_balancer/index.rst | 1 + .../load_balancer/v2/health_monitor.rst | 12 ++ openstack/load_balancer/v2/_proxy.py | 103 ++++++++++++++++++ openstack/load_balancer/v2/health_monitor.py | 71 ++++++++++++ .../load_balancer/v2/test_load_balancer.py | 52 +++++++++ .../unit/load_balancer/test_health_monitor.py | 73 +++++++++++++ .../tests/unit/load_balancer/test_proxy.py | 26 +++++ 8 files changed, 350 insertions(+) create mode 100644 doc/source/users/resources/load_balancer/v2/health_monitor.rst create mode 100644 openstack/load_balancer/v2/health_monitor.py create mode 100644 openstack/tests/unit/load_balancer/test_health_monitor.py diff --git a/doc/source/users/proxies/load_balancer_v2.rst b/doc/source/users/proxies/load_balancer_v2.rst index 0c728c07d..4a72d8e69 100644 --- a/doc/source/users/proxies/load_balancer_v2.rst +++ b/doc/source/users/proxies/load_balancer_v2.rst @@ -57,3 +57,15 @@ Member Operations .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_member .. automethod:: openstack.load_balancer.v2._proxy.Proxy.members .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_member + +Health Monitor Operations +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.load_balancer.v2._proxy.Proxy + + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.create_health_monitor + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_health_monitor + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_health_monitor + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_health_monitor + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.health_monitors + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_health_monitor diff --git a/doc/source/users/resources/load_balancer/index.rst b/doc/source/users/resources/load_balancer/index.rst index e007d7af7..ff81573d9 100644 --- a/doc/source/users/resources/load_balancer/index.rst +++ b/doc/source/users/resources/load_balancer/index.rst @@ -8,3 +8,4 @@ Load Balancer Resources v2/listener v2/pool v2/member + v2/health_monitor diff --git a/doc/source/users/resources/load_balancer/v2/health_monitor.rst b/doc/source/users/resources/load_balancer/v2/health_monitor.rst new file mode 100644 index 000000000..c5143e0df --- /dev/null +++ b/doc/source/users/resources/load_balancer/v2/health_monitor.rst @@ -0,0 +1,12 @@ +openstack.load_balancer.v2.health_monitor +========================================= + +.. automodule:: openstack.load_balancer.v2.health_monitor + +The HealthMonitor Class +----------------------- + +The ``HealthMonitor`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.load_balancer.v2.health_monitor.HealthMonitor + :members: diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index 2f27c786e..87ea33a65 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.load_balancer.v2 import health_monitor as _hm from openstack.load_balancer.v2 import listener as _listener from openstack.load_balancer.v2 import load_balancer as _lb from openstack.load_balancer.v2 import member as _member @@ -368,3 +369,105 @@ def update_member(self, member, pool, **attrs): poolobj = self._get_resource(_pool.Pool, pool) return self._update(_member.Member, member, pool_id=poolobj.id, **attrs) + + def find_health_monitor(self, name_or_id, ignore_missing=True): + """Find a single health monitor + + :param name_or_id: The name or ID of a health monitor + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the health monitor does not exist. + When set to ``True``, no exception will be set when attempting + to find a nonexistent health monitor. + + :returns: The + :class:`openstack.load_balancer.v2.healthmonitor.HealthMonitor` + object matching the given name or id or None if nothing matches. + + :raises: :class:`openstack.exceptions.DuplicateResource` if more + than one resource is found for this request. + :raises: :class:`openstack.exceptions.ResourceNotFound` if nothing + is found and ignore_missing is ``False``. + """ + return self._find(_hm.HealthMonitor, name_or_id, + ignore_missing=ignore_missing) + + def create_health_monitor(self, **attrs): + """Create a new health monitor from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.load_balancer.v2. + healthmonitor.HealthMonitor`, + comprised of the properties on the + HealthMonitor class. + + :returns: The results of HealthMonitor creation + :rtype: :class:`~openstack.load_balancer.v2. + healthmonitor.HealthMonitor` + """ + + return self._create(_hm.HealthMonitor, **attrs) + + def get_health_monitor(self, healthmonitor): + """Get a health monitor + + :param healthmonitor: The value can be the ID of a health monitor or + :class:`~openstack.load_balancer.v2.healthmonitor.HealthMonitor` + instance. + + :returns: One health monitor + :rtype: :class:`~openstack.load_balancer.v2. + healthmonitor.HealthMonitor` + """ + return self._get(_hm.HealthMonitor, healthmonitor) + + def health_monitors(self, **query): + """Retrieve a generator of health monitors + + :param dict query: Optional query parameters to be sent to limit + the resources being returned. Valid parameters are: + 'name', 'created_at', 'updated_at', 'delay', + 'expected_codes', 'http_method', 'max_retries', + 'max_retries_down', 'pool_id', + 'provisioning_status', 'operating_status', + 'timeout', 'project_id', 'type', 'url_path', + 'is_admin_state_up'. + + :returns: A generator of health monitor instances + """ + return self._list(_hm.HealthMonitor, paginated=True, **query) + + def delete_health_monitor(self, healthmonitor, ignore_missing=True): + """Delete a health monitor + + :param healthmonitor: The healthmonitor can be either the ID of the + health monitor or a + :class:`~openstack.load_balancer.v2.healthmonitor.HealthMonitor` + instance + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the healthmonitor does not exist. + When set to ``True``, no exception will be set when attempting to + delete a nonexistent healthmonitor. + + :returns: ``None`` + """ + return self._delete(_hm.HealthMonitor, healthmonitor, + ignore_missing=ignore_missing) + + def update_health_monitor(self, healthmonitor, **attrs): + """Update a health monitor + + :param healthmonitor: The healthmonitor can be either the ID of the + health monitor or a + :class:`~openstack.load_balancer.v2.healthmonitor.HealthMonitor` + instance + :param dict attrs: The attributes to update on the health monitor + represented by ``healthmonitor``. + + :returns: The updated health monitor + :rtype: :class:`~openstack.load_balancer.v2. + healthmonitor.HealthMonitor` + """ + return self._update(_hm.HealthMonitor, healthmonitor, + **attrs) diff --git a/openstack/load_balancer/v2/health_monitor.py b/openstack/load_balancer/v2/health_monitor.py new file mode 100644 index 000000000..da8820980 --- /dev/null +++ b/openstack/load_balancer/v2/health_monitor.py @@ -0,0 +1,71 @@ +# 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.load_balancer import load_balancer_service as lb_service +from openstack import resource2 as resource + + +class HealthMonitor(resource.Resource): + resource_key = 'healthmonitor' + resources_key = 'healthmonitors' + base_path = '/v2.0/lbaas/healthmonitors' + service = lb_service.LoadBalancerService() + + # capabilities + allow_create = True + allow_list = True + allow_get = True + allow_delete = True + allow_update = True + + _query_mapping = resource.QueryParameters( + 'name', 'created_at', 'updated_at', 'delay', 'expected_codes', + 'http_method', 'max_retries', 'max_retries_down', 'pool_id', + 'provisioning_status', 'operating_status', 'timeout', + 'project_id', 'type', 'url_path', is_admin_state_up='admin_state_up', + ) + + #: Properties + #: Timestamp when the health monitor was created. + created_at = resource.Body('created_at') + #: The time, in seconds, between sending probes to members. + delay = resource.Body('delay', type=int) + #: The expected http status codes to get from a successful health check + expected_codes = resource.Body('expected_codes') + #: The HTTP method that the monitor uses for requests + http_method = resource.Body('http_method') + #: The administrative state of the health monitor *Type: bool* + is_admin_state_up = resource.Body('admin_state_up', type=bool) + #: The number of successful checks before changing the operating status + #: of the member to ONLINE. + max_retries = resource.Body('max_retries', type=int) + #: The number of allowed check failures before changing the operating + #: status of the member to ERROR. + max_retries_down = resource.Body('max_retries_down', type=int) + #: The health monitor name + name = resource.Body('name') + #: Operating status of the member. + operating_status = resource.Body('operating_status') + #: The ID of the associated Pool + pool_id = resource.Body('pool_id') + #: The ID of the project + project_id = resource.Body('project_id') + #: The provisioning status of this member. + provisioning_status = resource.Body('provisioning_status') + #: The time, in seconds, after which a health check times out + timeout = resource.Body('timeout', type=int) + #: The type of health monitor + type = resource.Body('type') + #: Timestamp when the member was last updated. + updated_at = resource.Body('updated_at') + #: The HTTP path of the request to test the health of a member + url_path = resource.Body('url_path') diff --git a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py index 9b47ad694..3b0223f65 100644 --- a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py +++ b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py @@ -13,6 +13,7 @@ import unittest import uuid +from openstack.load_balancer.v2 import health_monitor from openstack.load_balancer.v2 import listener from openstack.load_balancer.v2 import load_balancer from openstack.load_balancer.v2 import member @@ -25,11 +26,13 @@ 'Load-balancer service does not exist') class TestLoadBalancer(lb_base.BaseLBFunctionalTest): + HM_NAME = uuid.uuid4().hex LB_NAME = uuid.uuid4().hex LISTENER_NAME = uuid.uuid4().hex MEMBER_NAME = uuid.uuid4().hex POOL_NAME = uuid.uuid4().hex UPDATE_NAME = uuid.uuid4().hex + HM_ID = None LB_ID = None LISTENER_ID = None MEMBER_ID = None @@ -41,6 +44,10 @@ class TestLoadBalancer(lb_base.BaseLBFunctionalTest): LB_ALGORITHM = 'ROUND_ROBIN' MEMBER_ADDRESS = '192.0.2.16' WEIGHT = 10 + DELAY = 2 + TIMEOUT = 1 + MAX_RETRY = 3 + HM_TYPE = 'HTTP' # Note: Creating load balancers can be slow on some hosts due to nova # instance boot times (up to ten minutes) so we are consolidating @@ -89,11 +96,24 @@ def setUpClass(cls): cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + test_hm = cls.conn.load_balancer.create_health_monitor( + pool_id=cls.POOL_ID, name=cls.HM_NAME, delay=cls.DELAY, + timeout=cls.TIMEOUT, max_retries=cls.MAX_RETRY, type=cls.HM_TYPE) + assert isinstance(test_hm, health_monitor.HealthMonitor) + cls.assertIs(cls.HM_NAME, test_hm.name) + cls.HM_ID = test_hm.id + cls.lb_wait_for_status(test_lb, status='ACTIVE', + failures=['ERROR']) + @classmethod def tearDownClass(cls): test_lb = cls.conn.load_balancer.get_load_balancer(cls.LB_ID) cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + cls.conn.load_balancer.delete_health_monitor( + cls.HM_ID, ignore_missing=False) + cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + cls.conn.load_balancer.delete_member( cls.MEMBER_ID, cls.POOL_ID, ignore_missing=False) cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) @@ -234,3 +254,35 @@ def test_member_update(self): test_member = self.conn.load_balancer.get_member(self.MEMBER_ID, self.POOL_ID) self.assertEqual(self.MEMBER_NAME, test_member.name) + + def test_health_monitor_find(self): + test_hm = self.conn.load_balancer.find_health_monitor(self.HM_NAME) + self.assertEqual(self.HM_ID, test_hm.id) + + def test_health_monitor_get(self): + test_hm = self.conn.load_balancer.get_health_monitor(self.HM_ID) + self.assertEqual(self.HM_NAME, test_hm.name) + self.assertEqual(self.HM_ID, test_hm.id) + self.assertEqual(self.DELAY, test_hm.delay) + self.assertEqual(self.TIMEOUT, test_hm.timeout) + self.assertEqual(self.MAX_RETRY, test_hm.max_retries) + self.assertEqual(self.HM_TYPE, test_hm.type) + + def test_health_monitor_list(self): + names = [hm.name for hm in self.conn.load_balancer.health_monitors()] + self.assertIn(self.HM_NAME, names) + + def test_health_monitor_update(self): + test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) + + self.conn.load_balancer.update_health_monitor(self.HM_ID, + name=self.UPDATE_NAME) + self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + test_hm = self.conn.load_balancer.get_health_monitor(self.HM_ID) + self.assertEqual(self.UPDATE_NAME, test_hm.name) + + self.conn.load_balancer.update_health_monitor(self.HM_ID, + name=self.HM_NAME) + self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + test_hm = self.conn.load_balancer.get_health_monitor(self.HM_ID) + self.assertEqual(self.HM_NAME, test_hm.name) diff --git a/openstack/tests/unit/load_balancer/test_health_monitor.py b/openstack/tests/unit/load_balancer/test_health_monitor.py new file mode 100644 index 000000000..b5cf0a24b --- /dev/null +++ b/openstack/tests/unit/load_balancer/test_health_monitor.py @@ -0,0 +1,73 @@ +# Copyright 2017 Rackspace, US 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 testtools +import uuid + +from openstack.load_balancer.v2 import health_monitor + +EXAMPLE = { + 'admin_state_up': True, + 'created_at': '2017-07-17T12:14:57.233772', + 'delay': 10, + 'expected_codes': '200, 202', + 'http_method': 'HEAD', + 'id': uuid.uuid4(), + 'max_retries': 2, + 'max_retries_down': 3, + 'name': 'test_health_monitor', + 'operating_status': 'ONLINE', + 'pool_id': uuid.uuid4(), + 'project_id': uuid.uuid4(), + 'provisioning_status': 'ACTIVE', + 'timeout': 4, + 'type': 'HTTP', + 'updated_at': '2017-07-17T12:16:57.233772', + 'url_path': '/health_page.html' +} + + +class TestPoolHealthMonitor(testtools.TestCase): + + def test_basic(self): + test_hm = health_monitor.HealthMonitor() + self.assertEqual('healthmonitor', test_hm.resource_key) + self.assertEqual('healthmonitors', test_hm.resources_key) + self.assertEqual('/v2.0/lbaas/healthmonitors', test_hm.base_path) + self.assertEqual('load-balancer', test_hm.service.service_type) + self.assertTrue(test_hm.allow_create) + self.assertTrue(test_hm.allow_get) + self.assertTrue(test_hm.allow_update) + self.assertTrue(test_hm.allow_delete) + self.assertTrue(test_hm.allow_list) + + def test_make_it(self): + test_hm = health_monitor.HealthMonitor(**EXAMPLE) + self.assertTrue(test_hm.is_admin_state_up) + self.assertEqual(EXAMPLE['created_at'], test_hm.created_at) + self.assertEqual(EXAMPLE['delay'], test_hm.delay) + self.assertEqual(EXAMPLE['expected_codes'], test_hm.expected_codes) + self.assertEqual(EXAMPLE['http_method'], test_hm.http_method) + self.assertEqual(EXAMPLE['id'], test_hm.id) + self.assertEqual(EXAMPLE['max_retries'], test_hm.max_retries) + self.assertEqual(EXAMPLE['max_retries_down'], test_hm.max_retries_down) + self.assertEqual(EXAMPLE['name'], test_hm.name) + self.assertEqual(EXAMPLE['operating_status'], test_hm.operating_status) + self.assertEqual(EXAMPLE['pool_id'], test_hm.pool_id) + self.assertEqual(EXAMPLE['project_id'], test_hm.project_id) + self.assertEqual(EXAMPLE['provisioning_status'], + test_hm.provisioning_status) + self.assertEqual(EXAMPLE['timeout'], test_hm.timeout) + self.assertEqual(EXAMPLE['type'], test_hm.type) + self.assertEqual(EXAMPLE['updated_at'], test_hm.updated_at) + self.assertEqual(EXAMPLE['url_path'], test_hm.url_path) diff --git a/openstack/tests/unit/load_balancer/test_proxy.py b/openstack/tests/unit/load_balancer/test_proxy.py index 5c4b1d554..f09bb4820 100644 --- a/openstack/tests/unit/load_balancer/test_proxy.py +++ b/openstack/tests/unit/load_balancer/test_proxy.py @@ -13,6 +13,7 @@ import uuid from openstack.load_balancer.v2 import _proxy +from openstack.load_balancer.v2 import health_monitor from openstack.load_balancer.v2 import listener from openstack.load_balancer.v2 import load_balancer as lb from openstack.load_balancer.v2 import member @@ -143,3 +144,28 @@ def test_member_update(self): method_args=["MEMBER", self.POOL_ID], expected_args=[member.Member, "MEMBER"], expected_kwargs={"pool_id": self.POOL_ID}) + + def test_health_monitors(self): + self.verify_list(self.proxy.health_monitors, + health_monitor.HealthMonitor, + paginated=True) + + def test_health_monitor_get(self): + self.verify_get(self.proxy.get_health_monitor, + health_monitor.HealthMonitor) + + def test_health_monitor_create(self): + self.verify_create(self.proxy.create_health_monitor, + health_monitor.HealthMonitor) + + def test_health_monitor_delete(self): + self.verify_delete(self.proxy.delete_health_monitor, + health_monitor.HealthMonitor, True) + + def test_health_monitor_find(self): + self.verify_find(self.proxy.find_health_monitor, + health_monitor.HealthMonitor) + + def test_health_monitor_update(self): + self.verify_update(self.proxy.update_health_monitor, + health_monitor.HealthMonitor) From 20cc034f65a7591c85bfd3c31204c4004375800c Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 18 Aug 2017 13:29:21 +0000 Subject: [PATCH 1749/3836] Updated from global requirements Change-Id: I00bdd8c84248ed6f8df6c2b5e593dfe21acc07ec --- test-requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 67f0acfa3..e7edec71a 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,13 +5,13 @@ hacking<0.12,>=0.11.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD -mock>=2.0 # BSD +mock>=2.0.0 # BSD python-subunit>=0.0.18 # Apache-2.0/BSD openstackdocstheme>=1.16.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 -requests-mock>=1.1 # Apache-2.0 +requests-mock>=1.1.0 # Apache-2.0 sphinx>=1.6.2 # BSD testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=1.4.0 # MIT -reno!=2.3.1,>=1.8.0 # Apache-2.0 +reno>=2.5.0 # Apache-2.0 From 9a4bd69aa9e7f8fa5aedb61470d44ee5873ce871 Mon Sep 17 00:00:00 2001 From: Nakul Dahiwade Date: Thu, 30 Mar 2017 20:50:48 +0000 Subject: [PATCH 1750/3836] Introduce L7Policy for Octavia (load balancing) Co-Authored-By: Michael Johnson Change-Id: Ibf1d239f8eb3175224c91ca70b41e1f5c988d08d --- doc/source/users/proxies/load_balancer_v2.rst | 12 +++ .../users/resources/load_balancer/index.rst | 1 + .../resources/load_balancer/v2/l7_policy.rst | 12 +++ openstack/load_balancer/v2/_proxy.py | 83 +++++++++++++++++++ openstack/load_balancer/v2/l7_policy.py | 64 ++++++++++++++ .../load_balancer/v2/test_load_balancer.py | 51 ++++++++++++ .../tests/unit/load_balancer/test_l7policy.py | 70 ++++++++++++++++ .../tests/unit/load_balancer/test_proxy.py | 26 ++++++ 8 files changed, 319 insertions(+) create mode 100644 doc/source/users/resources/load_balancer/v2/l7_policy.rst create mode 100644 openstack/load_balancer/v2/l7_policy.py create mode 100644 openstack/tests/unit/load_balancer/test_l7policy.py diff --git a/doc/source/users/proxies/load_balancer_v2.rst b/doc/source/users/proxies/load_balancer_v2.rst index 4a72d8e69..25e37d748 100644 --- a/doc/source/users/proxies/load_balancer_v2.rst +++ b/doc/source/users/proxies/load_balancer_v2.rst @@ -69,3 +69,15 @@ Health Monitor Operations .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_health_monitor .. automethod:: openstack.load_balancer.v2._proxy.Proxy.health_monitors .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_health_monitor + +L7 Policy Operations +^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.load_balancer.v2._proxy.Proxy + + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.create_l7_policy + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_l7_policy + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_l7_policy + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_l7_policy + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.l7_policies + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_l7_policy diff --git a/doc/source/users/resources/load_balancer/index.rst b/doc/source/users/resources/load_balancer/index.rst index ff81573d9..4fa8beb61 100644 --- a/doc/source/users/resources/load_balancer/index.rst +++ b/doc/source/users/resources/load_balancer/index.rst @@ -9,3 +9,4 @@ Load Balancer Resources v2/pool v2/member v2/health_monitor + v2/l7_policy diff --git a/doc/source/users/resources/load_balancer/v2/l7_policy.rst b/doc/source/users/resources/load_balancer/v2/l7_policy.rst new file mode 100644 index 000000000..2a5e6f01c --- /dev/null +++ b/doc/source/users/resources/load_balancer/v2/l7_policy.rst @@ -0,0 +1,12 @@ +openstack.load_balancer.v2.l7_policy +==================================== + +.. automodule:: openstack.load_balancer.v2.l7_policy + +The L7Policy Class +------------------ + +The ``L7Policy`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.load_balancer.v2.l7_policy.L7Policy + :members: diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index 87ea33a65..296527654 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -11,6 +11,7 @@ # under the License. from openstack.load_balancer.v2 import health_monitor as _hm +from openstack.load_balancer.v2 import l7_policy as _l7policy from openstack.load_balancer.v2 import listener as _listener from openstack.load_balancer.v2 import load_balancer as _lb from openstack.load_balancer.v2 import member as _member @@ -471,3 +472,85 @@ def update_health_monitor(self, healthmonitor, **attrs): """ return self._update(_hm.HealthMonitor, healthmonitor, **attrs) + + def create_l7_policy(self, **attrs): + """Create a new l7policy from attributes + + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.load_balancer.v2.l7_policy.L7Policy`, + comprised of the properties on the L7Policy class. + + :returns: The results of l7policy creation + :rtype: :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` + """ + return self._create(_l7policy.L7Policy, **attrs) + + def delete_l7_policy(self, l7_policy, ignore_missing=True): + """Delete a l7policy + + :param l7policy: The value can be either the ID of a l7policy or a + :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the l7policy does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent l7policy. + + :returns: ``None`` + """ + self._delete(_l7policy.L7Policy, l7_policy, + ignore_missing=ignore_missing) + + def find_l7_policy(self, name_or_id, ignore_missing=True): + """Find a single l7policy + + :param name_or_id: The name or ID of a l7policy. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + + :returns: One :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` + or None + """ + return self._find(_l7policy.L7Policy, name_or_id, + ignore_missing=ignore_missing) + + def get_l7_policy(self, l7_policy): + """Get a single l7policy + + :param l7policy: The value can be the ID of a l7policy or a + :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` + instance. + + :returns: One :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_l7policy.L7Policy, l7_policy) + + def l7_policies(self, **query): + """Return a generator of l7policies + + :param dict query: Optional query parameters to be sent to limit + the resources being returned. Valid parameters are: + + :returns: A generator of l7policy objects + :rtype: :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` + """ + return self._list(_l7policy.L7Policy, paginated=True, **query) + + def update_l7_policy(self, l7_policy, **attrs): + """Update a l7policy + + :param l7policy: Either the id of a l7policy or a + :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` + instance. + :param dict attrs: The attributes to update on the l7policy + represented by ``l7policy``. + + :returns: The updated l7policy + :rtype: :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` + """ + return self._update(_l7policy.L7Policy, l7_policy, **attrs) diff --git a/openstack/load_balancer/v2/l7_policy.py b/openstack/load_balancer/v2/l7_policy.py new file mode 100644 index 000000000..2c700702a --- /dev/null +++ b/openstack/load_balancer/v2/l7_policy.py @@ -0,0 +1,64 @@ +# 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.load_balancer import load_balancer_service as lb_service +from openstack import resource2 as resource + + +class L7Policy(resource.Resource): + resource_key = 'l7policy' + resources_key = 'l7policies' + base_path = '/v2.0/lbaas/l7policies' + service = lb_service.LoadBalancerService() + + # capabilities + allow_create = True + allow_list = True + allow_get = True + allow_update = True + allow_delete = True + + _query_mapping = resource.QueryParameters( + 'action', 'description', 'listener_id', 'name', 'position', + 'redirect_pool_id', 'redirect_url', 'provisioning_status', + 'operating_status', is_admin_state_up='admin_state_up', + ) + + #: Properties + #: The action to be taken l7policy is matched + action = resource.Body('action') + #: Timestamp when the L7 policy was created. + created_at = resource.Body('created_at') + #: The l7policy description + description = resource.Body('description') + #: The administrative state of the l7policy *Type: bool* + is_admin_state_up = resource.Body('admin_state_up', type=bool) + #: The ID of the listener associated with this l7policy + listener_id = resource.Body('listener_id') + #: The l7policy name + name = resource.Body('name') + #: Operating status of the member. + operating_status = resource.Body('operating_status') + #: Sequence number of this l7policy + position = resource.Body('position', type=int) + #: The ID of the project this l7policy is associated with. + project_id = resource.Body('project_id') + #: The provisioning status of this l7policy + provisioning_status = resource.Body('provisioning_status') + #: The ID of the pool to which the requests will be redirected + redirect_pool_id = resource.Body('redirect_pool_id') + #: The URL to which the requests should be redirected + redirect_url = resource.Body('redirect_url') + #: The list of L7Rules associated with the l7policy + rules = resource.Body('rules', type=list) + #: Timestamp when the member was last updated. + updated_at = resource.Body('updated_at') diff --git a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py index 3b0223f65..410a690e1 100644 --- a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py +++ b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py @@ -14,6 +14,7 @@ import uuid from openstack.load_balancer.v2 import health_monitor +from openstack.load_balancer.v2 import l7_policy from openstack.load_balancer.v2 import listener from openstack.load_balancer.v2 import load_balancer from openstack.load_balancer.v2 import member @@ -27,12 +28,14 @@ class TestLoadBalancer(lb_base.BaseLBFunctionalTest): HM_NAME = uuid.uuid4().hex + L7POLICY_NAME = uuid.uuid4().hex LB_NAME = uuid.uuid4().hex LISTENER_NAME = uuid.uuid4().hex MEMBER_NAME = uuid.uuid4().hex POOL_NAME = uuid.uuid4().hex UPDATE_NAME = uuid.uuid4().hex HM_ID = None + L7POLICY_ID = None LB_ID = None LISTENER_ID = None MEMBER_ID = None @@ -48,6 +51,8 @@ class TestLoadBalancer(lb_base.BaseLBFunctionalTest): TIMEOUT = 1 MAX_RETRY = 3 HM_TYPE = 'HTTP' + ACTION = 'REDIRECT_TO_URL' + REDIRECT_URL = 'http://www.example.com' # Note: Creating load balancers can be slow on some hosts due to nova # instance boot times (up to ten minutes) so we are consolidating @@ -105,11 +110,24 @@ def setUpClass(cls): cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + test_l7policy = cls.conn.load_balancer.create_l7_policy( + listener_id=cls.LISTENER_ID, name=cls.L7POLICY_NAME, + action=cls.ACTION, redirect_url=cls.REDIRECT_URL) + assert isinstance(test_l7policy, l7_policy.L7Policy) + cls.assertIs(cls.L7POLICY_NAME, test_l7policy.name) + cls.L7POLICY_ID = test_l7policy.id + cls.lb_wait_for_status(test_lb, status='ACTIVE', + failures=['ERROR']) + @classmethod def tearDownClass(cls): test_lb = cls.conn.load_balancer.get_load_balancer(cls.LB_ID) cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + cls.conn.load_balancer.delete_l7_policy( + cls.L7POLICY_ID, ignore_missing=False) + cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + cls.conn.load_balancer.delete_health_monitor( cls.HM_ID, ignore_missing=False) cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) @@ -286,3 +304,36 @@ def test_health_monitor_update(self): self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) test_hm = self.conn.load_balancer.get_health_monitor(self.HM_ID) self.assertEqual(self.HM_NAME, test_hm.name) + + def test_l7_policy_find(self): + test_l7_policy = self.conn.load_balancer.find_l7_policy( + self.L7POLICY_NAME) + self.assertEqual(self.L7POLICY_ID, test_l7_policy.id) + + def test_l7_policy_get(self): + test_l7_policy = self.conn.load_balancer.get_l7_policy( + self.L7POLICY_ID) + self.assertEqual(self.L7POLICY_NAME, test_l7_policy.name) + self.assertEqual(self.L7POLICY_ID, test_l7_policy.id) + self.assertEqual(self.ACTION, test_l7_policy.action) + + def test_l7_policy_list(self): + names = [l7.name for l7 in self.conn.load_balancer.l7_policies()] + self.assertIn(self.L7POLICY_NAME, names) + + def test_l7_policy_update(self): + test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) + + self.conn.load_balancer.update_l7_policy( + self.L7POLICY_ID, name=self.UPDATE_NAME) + self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + test_l7_policy = self.conn.load_balancer.get_l7_policy( + self.L7POLICY_ID) + self.assertEqual(self.UPDATE_NAME, test_l7_policy.name) + + self.conn.load_balancer.update_l7_policy(self.L7POLICY_ID, + name=self.L7POLICY_NAME) + self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + test_l7_policy = self.conn.load_balancer.get_l7_policy( + self.L7POLICY_ID) + self.assertEqual(self.L7POLICY_NAME, test_l7_policy.name) diff --git a/openstack/tests/unit/load_balancer/test_l7policy.py b/openstack/tests/unit/load_balancer/test_l7policy.py new file mode 100644 index 000000000..74742b303 --- /dev/null +++ b/openstack/tests/unit/load_balancer/test_l7policy.py @@ -0,0 +1,70 @@ +# 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 testtools +import uuid + +from openstack.load_balancer.v2 import l7_policy + +EXAMPLE = { + 'action': 'REJECT', + 'admin_state_up': True, + 'created_at': '2017-07-17T12:14:57.233772', + 'description': 'test_description', + 'id': uuid.uuid4(), + 'listener_id': uuid.uuid4(), + 'name': 'test_l7_policy', + 'operating_status': 'ONLINE', + 'position': 7, + 'project_id': uuid.uuid4(), + 'provisioning_status': 'ACTIVE', + 'redirect_pool_id': uuid.uuid4(), + 'redirect_url': '/test_url', + 'rules': [{'id': uuid.uuid4()}], + 'updated_at': '2017-07-17T12:16:57.233772', +} + + +class TestL7Policy(testtools.TestCase): + + def test_basic(self): + test_l7_policy = l7_policy.L7Policy() + self.assertEqual('l7policy', test_l7_policy.resource_key) + self.assertEqual('l7policies', test_l7_policy.resources_key) + self.assertEqual('/v2.0/lbaas/l7policies', test_l7_policy.base_path) + self.assertEqual('load-balancer', test_l7_policy.service.service_type) + self.assertTrue(test_l7_policy.allow_create) + self.assertTrue(test_l7_policy.allow_get) + self.assertTrue(test_l7_policy.allow_update) + self.assertTrue(test_l7_policy.allow_delete) + self.assertTrue(test_l7_policy.allow_list) + + def test_make_it(self): + test_l7_policy = l7_policy.L7Policy(**EXAMPLE) + self.assertTrue(test_l7_policy.is_admin_state_up) + self.assertEqual(EXAMPLE['action'], test_l7_policy.action) + self.assertEqual(EXAMPLE['created_at'], test_l7_policy.created_at) + self.assertEqual(EXAMPLE['description'], test_l7_policy.description) + self.assertEqual(EXAMPLE['id'], test_l7_policy.id) + self.assertEqual(EXAMPLE['listener_id'], test_l7_policy.listener_id) + self.assertEqual(EXAMPLE['name'], test_l7_policy.name) + self.assertEqual(EXAMPLE['operating_status'], + test_l7_policy.operating_status) + self.assertEqual(EXAMPLE['position'], test_l7_policy.position) + self.assertEqual(EXAMPLE['project_id'], test_l7_policy.project_id) + self.assertEqual(EXAMPLE['provisioning_status'], + test_l7_policy.provisioning_status) + self.assertEqual(EXAMPLE['redirect_pool_id'], + test_l7_policy.redirect_pool_id) + self.assertEqual(EXAMPLE['redirect_url'], test_l7_policy.redirect_url) + self.assertEqual(EXAMPLE['rules'], test_l7_policy.rules) + self.assertEqual(EXAMPLE['updated_at'], test_l7_policy.updated_at) diff --git a/openstack/tests/unit/load_balancer/test_proxy.py b/openstack/tests/unit/load_balancer/test_proxy.py index f09bb4820..32baa2d01 100644 --- a/openstack/tests/unit/load_balancer/test_proxy.py +++ b/openstack/tests/unit/load_balancer/test_proxy.py @@ -14,6 +14,7 @@ from openstack.load_balancer.v2 import _proxy from openstack.load_balancer.v2 import health_monitor +from openstack.load_balancer.v2 import l7_policy from openstack.load_balancer.v2 import listener from openstack.load_balancer.v2 import load_balancer as lb from openstack.load_balancer.v2 import member @@ -169,3 +170,28 @@ def test_health_monitor_find(self): def test_health_monitor_update(self): self.verify_update(self.proxy.update_health_monitor, health_monitor.HealthMonitor) + + def test_l7_policies(self): + self.verify_list(self.proxy.l7_policies, + l7_policy.L7Policy, + paginated=True) + + def test_l7_policy_get(self): + self.verify_get(self.proxy.get_l7_policy, + l7_policy.L7Policy) + + def test_l7_policy_create(self): + self.verify_create(self.proxy.create_l7_policy, + l7_policy.L7Policy) + + def test_l7_policy_delete(self): + self.verify_delete(self.proxy.delete_l7_policy, + l7_policy.L7Policy, True) + + def test_l7_policy_find(self): + self.verify_find(self.proxy.find_l7_policy, + l7_policy.L7Policy) + + def test_l7_policy_update(self): + self.verify_update(self.proxy.update_l7_policy, + l7_policy.L7Policy) From 317cfe6e69966769a90f94403929c1825d6d1cde Mon Sep 17 00:00:00 2001 From: Nakul Dahiwade Date: Mon, 3 Apr 2017 16:46:31 +0000 Subject: [PATCH 1751/3836] Introduce L7Rule for Octavia (load balancing) Co-Authored-By: Michael Johnson Change-Id: I19d2ec8e2f71527c4c7a1f4df043f5b28a5355c9 --- doc/source/users/proxies/load_balancer_v2.rst | 12 ++ .../users/resources/load_balancer/index.rst | 1 + .../resources/load_balancer/v2/l7_rule.rst | 12 ++ openstack/load_balancer/v2/_proxy.py | 119 +++++++++++++++++- openstack/load_balancer/v2/l7_rule.py | 61 +++++++++ .../load_balancer/v2/test_load_balancer.py | 52 ++++++++ .../tests/unit/load_balancer/test_l7rule.py | 66 ++++++++++ .../tests/unit/load_balancer/test_proxy.py | 43 +++++++ 8 files changed, 363 insertions(+), 3 deletions(-) create mode 100644 doc/source/users/resources/load_balancer/v2/l7_rule.rst create mode 100644 openstack/load_balancer/v2/l7_rule.py create mode 100644 openstack/tests/unit/load_balancer/test_l7rule.py diff --git a/doc/source/users/proxies/load_balancer_v2.rst b/doc/source/users/proxies/load_balancer_v2.rst index 25e37d748..4ff99ffef 100644 --- a/doc/source/users/proxies/load_balancer_v2.rst +++ b/doc/source/users/proxies/load_balancer_v2.rst @@ -81,3 +81,15 @@ L7 Policy Operations .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_l7_policy .. automethod:: openstack.load_balancer.v2._proxy.Proxy.l7_policies .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_l7_policy + +L7 Rule Operations +^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.load_balancer.v2._proxy.Proxy + + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.create_l7_rule + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_l7_rule + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_l7_rule + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_l7_rule + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.l7_rules + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_l7_rule diff --git a/doc/source/users/resources/load_balancer/index.rst b/doc/source/users/resources/load_balancer/index.rst index 4fa8beb61..873f6d130 100644 --- a/doc/source/users/resources/load_balancer/index.rst +++ b/doc/source/users/resources/load_balancer/index.rst @@ -10,3 +10,4 @@ Load Balancer Resources v2/member v2/health_monitor v2/l7_policy + v2/l7_rule diff --git a/doc/source/users/resources/load_balancer/v2/l7_rule.rst b/doc/source/users/resources/load_balancer/v2/l7_rule.rst new file mode 100644 index 000000000..2b1f471af --- /dev/null +++ b/doc/source/users/resources/load_balancer/v2/l7_rule.rst @@ -0,0 +1,12 @@ +openstack.load_balancer.v2.l7_rule +==================================== + +.. automodule:: openstack.load_balancer.v2.l7_rule + +The L7Rule Class +------------------ + +The ``L7Rule`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.load_balancer.v2.l7_rule.L7Rule + :members: diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index 296527654..1129dab6a 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -12,6 +12,7 @@ from openstack.load_balancer.v2 import health_monitor as _hm from openstack.load_balancer.v2 import l7_policy as _l7policy +from openstack.load_balancer.v2 import l7_rule as _l7rule from openstack.load_balancer.v2 import listener as _listener from openstack.load_balancer.v2 import load_balancer as _lb from openstack.load_balancer.v2 import member as _member @@ -488,7 +489,7 @@ def create_l7_policy(self, **attrs): def delete_l7_policy(self, l7_policy, ignore_missing=True): """Delete a l7policy - :param l7policy: The value can be either the ID of a l7policy or a + :param l7_policy: The value can be either the ID of a l7policy or a :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be @@ -520,7 +521,7 @@ def find_l7_policy(self, name_or_id, ignore_missing=True): def get_l7_policy(self, l7_policy): """Get a single l7policy - :param l7policy: The value can be the ID of a l7policy or a + :param l7_policy: The value can be the ID of a l7policy or a :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` instance. @@ -544,7 +545,7 @@ def l7_policies(self, **query): def update_l7_policy(self, l7_policy, **attrs): """Update a l7policy - :param l7policy: Either the id of a l7policy or a + :param l7_policy: Either the id of a l7policy or a :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` instance. :param dict attrs: The attributes to update on the l7policy @@ -554,3 +555,115 @@ def update_l7_policy(self, l7_policy, **attrs): :rtype: :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` """ return self._update(_l7policy.L7Policy, l7_policy, **attrs) + + def create_l7_rule(self, l7_policy, **attrs): + """Create a new l7rule from attributes + + :param l7_policy: The l7_policy can be either the ID of a l7policy or + :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` + instance that the l7rule will be created in. + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.load_balancer.v2.l7_rule.L7Rule`, + comprised of the properties on the L7Rule class. + + :returns: The results of l7rule creation + :rtype: :class:`~openstack.load_balancer.v2.l7_rule.L7Rule` + """ + l7policyobj = self._get_resource(_l7policy.L7Policy, l7_policy) + return self._create(_l7rule.L7Rule, l7policy_id=l7policyobj.id, + **attrs) + + def delete_l7_rule(self, l7rule, l7_policy, ignore_missing=True): + """Delete a l7rule + + :param l7rule: + The l7rule can be either the ID of a l7rule or a + :class:`~openstack.load_balancer.v2.l7_rule.L7Rule` instance. + :param l7_policy: The l7_policy can be either the ID of a l7policy or + :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` + instance that the l7rule belongs to. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the l7rule does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent l7rule. + + :returns: ``None`` + """ + l7policyobj = self._get_resource(_l7policy.L7Policy, l7_policy) + self._delete(_l7rule.L7Rule, l7rule, + ignore_missing=ignore_missing, l7policy_id=l7policyobj.id) + + def find_l7_rule(self, name_or_id, l7_policy, ignore_missing=True): + """Find a single l7rule + + :param str name_or_id: The name or ID of a l7rule. + :param l7_policy: The l7_policy can be either the ID of a l7policy or + :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` + instance that the l7rule belongs to. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + + :returns: One :class:`~openstack.load_balancer.v2.l7_rule.L7Rule` + or None + """ + l7policyobj = self._get_resource(_l7policy.L7Policy, l7_policy) + return self._find(_l7rule.L7Rule, name_or_id, + ignore_missing=ignore_missing, + l7policy_id=l7policyobj.id) + + def get_l7_rule(self, l7rule, l7_policy): + """Get a single l7rule + + :param l7rule: The l7rule can be the ID of a l7rule or a + :class:`~openstack.load_balancer.v2.l7_rule.L7Rule` + instance. + :param l7_policy: The l7_policy can be either the ID of a l7policy or + :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` + instance that the l7rule belongs to. + + :returns: One :class:`~openstack.load_balancer.v2.l7_rule.L7Rule` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + l7policyobj = self._get_resource(_l7policy.L7Policy, l7_policy) + return self._get(_l7rule.L7Rule, l7rule, + l7policy_id=l7policyobj.id) + + def l7_rules(self, l7_policy, **query): + """Return a generator of l7rules + + :param l7_policy: The l7_policy can be either the ID of a l7_policy or + :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` + instance that the l7rule belongs to. + :param dict query: Optional query parameters to be sent to limit + the resources being returned. Valid parameters are: + + :returns: A generator of l7rule objects + :rtype: :class:`~openstack.load_balancer.v2.l7_rule.L7Rule` + """ + l7policyobj = self._get_resource(_l7policy.L7Policy, l7_policy) + return self._list(_l7rule.L7Rule, paginated=True, + l7policy_id=l7policyobj.id, **query) + + def update_l7_rule(self, l7rule, l7_policy, **attrs): + """Update a l7rule + + :param l7rule: Either the ID of a l7rule or a + :class:`~openstack.load_balancer.v2.l7_rule.L7Rule` + instance. + :param l7_policy: The l7_policy can be either the ID of a l7policy or + :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` + instance that the l7rule belongs to. + :param dict attrs: The attributes to update on the l7rule + represented by ``l7rule``. + + :returns: The updated l7rule + :rtype: :class:`~openstack.load_balancer.v2.l7_rule.L7Rule` + """ + l7policyobj = self._get_resource(_l7policy.L7Policy, l7_policy) + return self._update(_l7rule.L7Rule, l7rule, + l7policy_id=l7policyobj.id, **attrs) diff --git a/openstack/load_balancer/v2/l7_rule.py b/openstack/load_balancer/v2/l7_rule.py new file mode 100644 index 000000000..41dd6c5f0 --- /dev/null +++ b/openstack/load_balancer/v2/l7_rule.py @@ -0,0 +1,61 @@ +# 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.load_balancer import load_balancer_service as lb_service +from openstack import resource2 as resource + + +class L7Rule(resource.Resource): + resource_key = 'l7rule' + resources_key = 'l7rules' + base_path = '/l7policies/%(l7_policy_id)s/rules' + service = lb_service.LoadBalancerService() + + # capabilities + allow_create = True + allow_list = True + allow_get = True + allow_update = True + allow_delete = True + + _query_mapping = resource.QueryParameters( + 'compare_type', 'created_at', 'invert', 'key', 'l7_policy_id', + 'project_id', 'provisioning_status', 'type', 'updated_at', 'value', + 'operating_status', is_admin_state_up='admin_state_up', + ) + + #: Properties + #: The administrative state of the l7policy *Type: bool* + is_admin_state_up = resource.Body('admin_state_up', type=bool) + #: comparison type to be used with the value in this L7 rule. + compare_type = resource.Body('compare_type') + #: Timestamp when the L7 rule was created. + created_at = resource.Body('created_at') + #: inverts the logic of the rule if True + # (ie. perform a logical NOT on the rule) + invert = resource.Body('invert', type=bool) + #: The key to use for the comparison. + key = resource.Body('key') + #: The ID of the associated l7 policy + l7_policy_id = resource.Body('l7policy_id') + #: The operating status of this l7rule + operating_status = resource.Body('operating_status') + #: The ID of the project this l7policy is associated with. + project_id = resource.Body('project_id') + #: The provisioning status of this l7policy + provisioning_status = resource.Body('provisioning_status') + #: The type of L7 rule + type = resource.Body('type') + #: Timestamp when the L7 rule was updated. + updated_at = resource.Body('updated_at') + #: value to be compared with + value = resource.Body('value') diff --git a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py index 410a690e1..8832d0a25 100644 --- a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py +++ b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py @@ -15,6 +15,7 @@ from openstack.load_balancer.v2 import health_monitor from openstack.load_balancer.v2 import l7_policy +from openstack.load_balancer.v2 import l7_rule from openstack.load_balancer.v2 import listener from openstack.load_balancer.v2 import load_balancer from openstack.load_balancer.v2 import member @@ -53,6 +54,9 @@ class TestLoadBalancer(lb_base.BaseLBFunctionalTest): HM_TYPE = 'HTTP' ACTION = 'REDIRECT_TO_URL' REDIRECT_URL = 'http://www.example.com' + COMPARE_TYPE = 'CONTAINS' + L7RULE_TYPE = 'HOST_NAME' + L7RULE_VALUE = 'example' # Note: Creating load balancers can be slow on some hosts due to nova # instance boot times (up to ten minutes) so we are consolidating @@ -119,11 +123,24 @@ def setUpClass(cls): cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + test_l7rule = cls.conn.load_balancer.create_l7_rule( + l7_policy=cls.L7POLICY_ID, compare_type=cls.COMPARE_TYPE, + type=cls.L7RULE_TYPE, value=cls.L7RULE_VALUE) + assert isinstance(test_l7rule, l7_rule.L7Rule) + cls.assertIs(cls.COMPARE_TYPE, test_l7rule.compare_type) + cls.L7RULE_ID = test_l7rule.id + cls.lb_wait_for_status(test_lb, status='ACTIVE', + failures=['ERROR']) + @classmethod def tearDownClass(cls): test_lb = cls.conn.load_balancer.get_load_balancer(cls.LB_ID) cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + cls.conn.load_balancer.delete_l7_rule( + cls.L7RULE_ID, ignore_missing=False) + cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + cls.conn.load_balancer.delete_l7_policy( cls.L7POLICY_ID, ignore_missing=False) cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) @@ -337,3 +354,38 @@ def test_l7_policy_update(self): test_l7_policy = self.conn.load_balancer.get_l7_policy( self.L7POLICY_ID) self.assertEqual(self.L7POLICY_NAME, test_l7_policy.name) + + def test_l7_rule_find(self): + test_l7_rule = self.conn.load_balancer.find_l7_rule( + self.L7RULE_ID) + self.assertEqual(self.L7RULE_ID, test_l7_rule.id) + self.assertEqual(self.L7RULE_TYPE, test_l7_rule.type) + + def test_l7_rule_get(self): + test_l7_rule = self.conn.load_balancer.get_l7_rule( + self.L7RULE_ID) + self.assertEqual(self.L7RULE_ID, test_l7_rule.id) + self.assertEqual(self.COMPARE_TYPE, test_l7_rule.compare_type) + self.assertEqual(self.L7RULE_TYPE, test_l7_rule.type) + self.assertEqual(self.L7RULE_VALUE, test_l7_rule.value) + + def test_l7_rule_list(self): + ids = [l7.id for l7 in self.conn.load_balancer.l7_rules()] + self.assertIn(self.L7RULE_ID, ids) + + def test_l7_rule_update(self): + test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) + + self.conn.load_balancer.update_l7_rule( + self.L7RULE_ID, value=self.UPDATE_NAME) + self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + test_l7_rule = self.conn.load_balancer.get_l7_rule( + self.L7RULE_ID) + self.assertEqual(self.UPDATE_NAME, test_l7_rule.value) + + self.conn.load_balancer.update_l7_policy(self.L7POLICY_ID, + value=self.L7RULE_VALUE) + self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + test_l7_rule = self.conn.load_balancer.get_l7_rule( + self.L7RULE_ID) + self.assertEqual(self.L7RULE_VALUE, test_l7_rule.value) diff --git a/openstack/tests/unit/load_balancer/test_l7rule.py b/openstack/tests/unit/load_balancer/test_l7rule.py new file mode 100644 index 000000000..7e99af377 --- /dev/null +++ b/openstack/tests/unit/load_balancer/test_l7rule.py @@ -0,0 +1,66 @@ +# 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 testtools +import uuid + +from openstack.load_balancer.v2 import l7_rule + +EXAMPLE = { + 'admin_state_up': True, + 'compare_type': 'REGEX', + 'created_at': '2017-08-17T12:14:57.233772', + 'id': uuid.uuid4(), + 'invert': False, + 'key': 'my_cookie', + 'l7_policy_id': uuid.uuid4(), + 'operating_status': 'ONLINE', + 'project_id': uuid.uuid4(), + 'provisioning_status': 'ACTIVE', + 'type': 'COOKIE', + 'updated_at': '2017-08-17T12:16:57.233772', + 'value': 'chocolate' +} + + +class TestL7Rule(testtools.TestCase): + + def test_basic(self): + test_l7rule = l7_rule.L7Rule() + self.assertEqual('l7rule', test_l7rule.resource_key) + self.assertEqual('l7rules', test_l7rule.resources_key) + self.assertEqual('/l7policies/%(l7_policy_id)s/rules', + test_l7rule.base_path) + self.assertEqual('load-balancer', test_l7rule.service.service_type) + self.assertTrue(test_l7rule.allow_create) + self.assertTrue(test_l7rule.allow_get) + self.assertTrue(test_l7rule.allow_update) + self.assertTrue(test_l7rule.allow_delete) + self.assertTrue(test_l7rule.allow_list) + + def test_make_it(self): + test_l7rule = l7_rule.L7Rule(**EXAMPLE) + self.assertTrue(test_l7rule.is_admin_state_up) + self.assertEqual(EXAMPLE['compare_type'], test_l7rule.compare_type) + self.assertEqual(EXAMPLE['created_at'], test_l7rule.created_at) + self.assertEqual(EXAMPLE['id'], test_l7rule.id) + self.assertEqual(EXAMPLE['invert'], test_l7rule.invert) + self.assertEqual(EXAMPLE['key'], test_l7rule.key) + self.assertEqual(EXAMPLE['l7_policy_id'], test_l7rule.l7_policy_id) + self.assertEqual(EXAMPLE['operating_status'], + test_l7rule.operating_status) + self.assertEqual(EXAMPLE['project_id'], test_l7rule.project_id) + self.assertEqual(EXAMPLE['provisioning_status'], + test_l7rule.provisioning_status) + self.assertEqual(EXAMPLE['type'], test_l7rule.type) + self.assertEqual(EXAMPLE['updated_at'], test_l7rule.updated_at) + self.assertEqual(EXAMPLE['value'], test_l7rule.value) diff --git a/openstack/tests/unit/load_balancer/test_proxy.py b/openstack/tests/unit/load_balancer/test_proxy.py index 32baa2d01..4db9ad239 100644 --- a/openstack/tests/unit/load_balancer/test_proxy.py +++ b/openstack/tests/unit/load_balancer/test_proxy.py @@ -15,6 +15,7 @@ from openstack.load_balancer.v2 import _proxy from openstack.load_balancer.v2 import health_monitor from openstack.load_balancer.v2 import l7_policy +from openstack.load_balancer.v2 import l7_rule from openstack.load_balancer.v2 import listener from openstack.load_balancer.v2 import load_balancer as lb from openstack.load_balancer.v2 import member @@ -25,6 +26,7 @@ class TestLoadBalancerProxy(test_proxy_base2.TestProxyBase): POOL_ID = uuid.uuid4() + L7_POLICY_ID = uuid.uuid4() def setUp(self): super(TestLoadBalancerProxy, self).setUp() @@ -195,3 +197,44 @@ def test_l7_policy_find(self): def test_l7_policy_update(self): self.verify_update(self.proxy.update_l7_policy, l7_policy.L7Policy) + + def test_l7_rules(self): + self.verify_list(self.proxy.l7_rules, + l7_rule.L7Rule, + paginated=True, + method_kwargs={'l7_policy': self.L7_POLICY_ID}, + expected_kwargs={'l7policy_id': self.L7_POLICY_ID}) + + def test_l7_rule_get(self): + self.verify_get(self.proxy.get_l7_rule, + l7_rule.L7Rule, + method_kwargs={'l7_policy': self.L7_POLICY_ID}, + expected_kwargs={'l7policy_id': self.L7_POLICY_ID}) + + def test_l7_rule_create(self): + self.verify_create(self.proxy.create_l7_rule, + l7_rule.L7Rule, + method_kwargs={'l7_policy': self.L7_POLICY_ID}, + expected_kwargs={'l7policy_id': self.L7_POLICY_ID}) + + def test_l7_rule_delete(self): + self.verify_delete(self.proxy.delete_l7_rule, + l7_rule.L7Rule, + True, + method_kwargs={'l7_policy': self.L7_POLICY_ID}, + expected_kwargs={'l7policy_id': self.L7_POLICY_ID}) + + def test_l7_rule_find(self): + self._verify2('openstack.proxy2.BaseProxy._find', + self.proxy.find_l7_rule, + method_args=["RULE", self.L7_POLICY_ID], + expected_args=[l7_rule.L7Rule, "RULE"], + expected_kwargs={"l7policy_id": self.L7_POLICY_ID, + "ignore_missing": True}) + + def test_l7_rule_update(self): + self._verify2('openstack.proxy2.BaseProxy._update', + self.proxy.update_l7_rule, + method_args=["RULE", self.L7_POLICY_ID], + expected_args=[l7_rule.L7Rule, "RULE"], + expected_kwargs={"l7policy_id": self.L7_POLICY_ID}) From 73924e988fd2c05ce8b74e5f526c71becc43e250 Mon Sep 17 00:00:00 2001 From: TingtingYu Date: Mon, 14 Aug 2017 14:08:20 +0800 Subject: [PATCH 1752/3836] fix the bug that cannot create a listener by openstacksdk When creating a listener with a loadbalance id by openstacksdk,there have an error with the message "Listener must be created with a loadbalancer or pool." I guess the reason is that I give the parameter "loadbalancer_id" but it be ignored in the code. I modify the listener class, I add a new line load_balancer_id = resource.Body('loadbalancer_id'), then successfully create a listener. Change-Id: Id05fcdc0be8f50304a974c01d1aada4eaa4053cd Closes-Bug: 1708780 --- openstack/network/v2/listener.py | 2 ++ openstack/tests/unit/network/v2/test_listener.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/openstack/network/v2/listener.py b/openstack/network/v2/listener.py index 39bf8b1c3..509367e91 100644 --- a/openstack/network/v2/listener.py +++ b/openstack/network/v2/listener.py @@ -49,6 +49,8 @@ class Listener(resource.Resource): #: List of load balancers associated with this listener. #: *Type: list of dicts which contain the load balancer IDs* load_balancer_ids = resource.Body('loadbalancers') + #: The ID of the load balancer associated with this listener. + load_balancer_id = resource.Body('loadbalancer_id') #: Name of the listener name = resource.Body('name') #: The ID of the project this listener is associated with. diff --git a/openstack/tests/unit/network/v2/test_listener.py b/openstack/tests/unit/network/v2/test_listener.py index 34c91d824..4eb550e0e 100644 --- a/openstack/tests/unit/network/v2/test_listener.py +++ b/openstack/tests/unit/network/v2/test_listener.py @@ -22,6 +22,7 @@ 'description': '4', 'id': IDENTIFIER, 'loadbalancers': [{'id': '6'}], + 'loadbalancer_id': '6', 'name': '7', 'project_id': '8', 'protocol': '9', @@ -53,6 +54,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['description'], sot.description) self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['loadbalancers'], sot.load_balancer_ids) + self.assertEqual(EXAMPLE['loadbalancer_id'], sot.load_balancer_id) self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['protocol'], sot.protocol) From 3cfaa4c63b0ff54f45da59ee48fcb9e5ea57dde5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Thu, 17 Aug 2017 21:07:54 +0000 Subject: [PATCH 1753/3836] Add option to force delete cinder volume Cinder's API gives possibility to force delete volume which is in state 'deleting' or 'error deleting'. This patch adds support for such force delete of volume to shade. Change-Id: Iffede494e5af250a6bfea98e1fbfd8014eff0b02 --- shade/openstackcloud.py | 14 +++++++++++--- shade/tests/unit/test_volume.py | 21 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index fd5eca380..e90a436f5 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4762,12 +4762,15 @@ def create_volume( return self._normalize_volume(volume) - def delete_volume(self, name_or_id=None, wait=True, timeout=None): + def delete_volume(self, name_or_id=None, wait=True, timeout=None, + force=False): """Delete a volume. :param name_or_id: Name or unique ID of the volume. :param wait: If true, waits for volume to be deleted. :param timeout: Seconds to wait for volume deletion. None is forever. + :param force: Force delete volume even if the volume is in deleting + or error_deleting state. :raises: OpenStackCloudTimeout if wait time exceeded. :raises: OpenStackCloudException on operation error. @@ -4785,8 +4788,13 @@ def delete_volume(self, name_or_id=None, wait=True, timeout=None): with _utils.shade_exceptions("Error in deleting volume"): try: - self._volume_client.delete( - 'volumes/{id}'.format(id=volume['id'])) + if force: + self._volume_client.post( + 'volumes/{id}/action'.format(id=volume['id']), + json={'os-force_delete': None}) + else: + self._volume_client.delete( + 'volumes/{id}'.format(id=volume['id'])) except OpenStackCloudURINotFound: self.log.debug( "Volume {id} not found when deleting. Ignoring.".format( diff --git a/shade/tests/unit/test_volume.py b/shade/tests/unit/test_volume.py index c4ff2aa10..bade4bd46 100644 --- a/shade/tests/unit/test_volume.py +++ b/shade/tests/unit/test_volume.py @@ -279,6 +279,27 @@ def test_delete_volume_gone_away(self): self.assertFalse(self.cloud.delete_volume(volume['id'])) self.assert_calls() + def test_delete_volume_force(self): + vol = {'id': 'volume001', 'status': 'attached', + 'name': '', 'attachments': []} + volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', 'detail']), + json={'volumes': [volume]}), + dict(method='POST', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['volumes', volume.id, 'action']), + json={'os-force_delete': None}), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', 'detail']), + json={'volumes': []})]) + self.assertTrue(self.cloud.delete_volume(volume['id'], force=True)) + self.assert_calls() + def test_list_volumes_with_pagination(self): vol1 = meta.obj_to_munch(fakes.FakeVolume('01', 'available', 'vol1')) vol2 = meta.obj_to_munch(fakes.FakeVolume('02', 'available', 'vol2')) From 0eecb4ba7179f8b6eeeb9d7f58eb3d307acca506 Mon Sep 17 00:00:00 2001 From: lidong Date: Tue, 22 Aug 2017 16:07:26 +0800 Subject: [PATCH 1754/3836] Update links in README Change http://developer.openstack.org/sdks/python/openstacksdk/ to https://developer.openstack.org/sdks/python/openstacksdk/ in README.rst Change-Id: I19b2a8aeb625c682acf3ab23730af0c8556886b1 --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index a75f448a6..36ad09d38 100644 --- a/README.rst +++ b/README.rst @@ -28,7 +28,7 @@ Documentation ------------- Documentation is available at -http://developer.openstack.org/sdks/python/openstacksdk/ +https://developer.openstack.org/sdks/python/openstacksdk/ License ------- From ab5091eb745ed97b966dc6a1fcdba33af73d935f Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Tue, 22 Aug 2017 14:52:08 -0700 Subject: [PATCH 1755/3836] Fix octavia l7rules The l7rules patch had a bad rebase and ended up getting merged in a broken state as indicated by the functional test gate failure (non-voting). This patch fixes this issues from the bad rebase and allows the tests to pass. Change-Id: I4b2270f32c467e18d6ec203f20692685453687cd --- openstack/load_balancer/v2/_proxy.py | 14 ++++----- openstack/load_balancer/v2/l7_rule.py | 14 ++++----- .../load_balancer/v2/test_load_balancer.py | 29 ++++++++++--------- .../tests/unit/load_balancer/test_l7rule.py | 8 ++--- .../tests/unit/load_balancer/test_proxy.py | 12 ++++---- 5 files changed, 40 insertions(+), 37 deletions(-) diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index 1129dab6a..575cbcc4d 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -570,7 +570,7 @@ def create_l7_rule(self, l7_policy, **attrs): :rtype: :class:`~openstack.load_balancer.v2.l7_rule.L7Rule` """ l7policyobj = self._get_resource(_l7policy.L7Policy, l7_policy) - return self._create(_l7rule.L7Rule, l7policy_id=l7policyobj.id, + return self._create(_l7rule.L7Rule, l7_policy_id=l7policyobj.id, **attrs) def delete_l7_rule(self, l7rule, l7_policy, ignore_missing=True): @@ -591,8 +591,8 @@ def delete_l7_rule(self, l7rule, l7_policy, ignore_missing=True): :returns: ``None`` """ l7policyobj = self._get_resource(_l7policy.L7Policy, l7_policy) - self._delete(_l7rule.L7Rule, l7rule, - ignore_missing=ignore_missing, l7policy_id=l7policyobj.id) + self._delete(_l7rule.L7Rule, l7rule, ignore_missing=ignore_missing, + l7_policy_id=l7policyobj.id) def find_l7_rule(self, name_or_id, l7_policy, ignore_missing=True): """Find a single l7rule @@ -613,7 +613,7 @@ def find_l7_rule(self, name_or_id, l7_policy, ignore_missing=True): l7policyobj = self._get_resource(_l7policy.L7Policy, l7_policy) return self._find(_l7rule.L7Rule, name_or_id, ignore_missing=ignore_missing, - l7policy_id=l7policyobj.id) + l7_policy_id=l7policyobj.id) def get_l7_rule(self, l7rule, l7_policy): """Get a single l7rule @@ -631,7 +631,7 @@ def get_l7_rule(self, l7rule, l7_policy): """ l7policyobj = self._get_resource(_l7policy.L7Policy, l7_policy) return self._get(_l7rule.L7Rule, l7rule, - l7policy_id=l7policyobj.id) + l7_policy_id=l7policyobj.id) def l7_rules(self, l7_policy, **query): """Return a generator of l7rules @@ -647,7 +647,7 @@ def l7_rules(self, l7_policy, **query): """ l7policyobj = self._get_resource(_l7policy.L7Policy, l7_policy) return self._list(_l7rule.L7Rule, paginated=True, - l7policy_id=l7policyobj.id, **query) + l7_policy_id=l7policyobj.id, **query) def update_l7_rule(self, l7rule, l7_policy, **attrs): """Update a l7rule @@ -666,4 +666,4 @@ def update_l7_rule(self, l7rule, l7_policy, **attrs): """ l7policyobj = self._get_resource(_l7policy.L7Policy, l7_policy) return self._update(_l7rule.L7Rule, l7rule, - l7policy_id=l7policyobj.id, **attrs) + l7_policy_id=l7policyobj.id, **attrs) diff --git a/openstack/load_balancer/v2/l7_rule.py b/openstack/load_balancer/v2/l7_rule.py index 41dd6c5f0..f9dca5432 100644 --- a/openstack/load_balancer/v2/l7_rule.py +++ b/openstack/load_balancer/v2/l7_rule.py @@ -15,9 +15,9 @@ class L7Rule(resource.Resource): - resource_key = 'l7rule' - resources_key = 'l7rules' - base_path = '/l7policies/%(l7_policy_id)s/rules' + resource_key = 'rule' + resources_key = 'rules' + base_path = '/v2.0/lbaas/l7policies/%(l7_policy_id)s/rules' service = lb_service.LoadBalancerService() # capabilities @@ -29,8 +29,8 @@ class L7Rule(resource.Resource): _query_mapping = resource.QueryParameters( 'compare_type', 'created_at', 'invert', 'key', 'l7_policy_id', - 'project_id', 'provisioning_status', 'type', 'updated_at', 'value', - 'operating_status', is_admin_state_up='admin_state_up', + 'project_id', 'provisioning_status', 'type', 'updated_at', + 'rule_value', 'operating_status', is_admin_state_up='admin_state_up', ) #: Properties @@ -46,7 +46,7 @@ class L7Rule(resource.Resource): #: The key to use for the comparison. key = resource.Body('key') #: The ID of the associated l7 policy - l7_policy_id = resource.Body('l7policy_id') + l7_policy_id = resource.URI('l7_policy_id') #: The operating status of this l7rule operating_status = resource.Body('operating_status') #: The ID of the project this l7policy is associated with. @@ -58,4 +58,4 @@ class L7Rule(resource.Resource): #: Timestamp when the L7 rule was updated. updated_at = resource.Body('updated_at') #: value to be compared with - value = resource.Body('value') + rule_value = resource.Body('value') diff --git a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py index 8832d0a25..1bf8b6013 100644 --- a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py +++ b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py @@ -138,7 +138,7 @@ def tearDownClass(cls): cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) cls.conn.load_balancer.delete_l7_rule( - cls.L7RULE_ID, ignore_missing=False) + cls.L7RULE_ID, l7_policy=cls.L7POLICY_ID, ignore_missing=False) cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) cls.conn.load_balancer.delete_l7_policy( @@ -357,35 +357,38 @@ def test_l7_policy_update(self): def test_l7_rule_find(self): test_l7_rule = self.conn.load_balancer.find_l7_rule( - self.L7RULE_ID) + self.L7RULE_ID, self.L7POLICY_ID) self.assertEqual(self.L7RULE_ID, test_l7_rule.id) self.assertEqual(self.L7RULE_TYPE, test_l7_rule.type) def test_l7_rule_get(self): test_l7_rule = self.conn.load_balancer.get_l7_rule( - self.L7RULE_ID) + self.L7RULE_ID, l7_policy=self.L7POLICY_ID) self.assertEqual(self.L7RULE_ID, test_l7_rule.id) self.assertEqual(self.COMPARE_TYPE, test_l7_rule.compare_type) self.assertEqual(self.L7RULE_TYPE, test_l7_rule.type) - self.assertEqual(self.L7RULE_VALUE, test_l7_rule.value) + self.assertEqual(self.L7RULE_VALUE, test_l7_rule.rule_value) def test_l7_rule_list(self): - ids = [l7.id for l7 in self.conn.load_balancer.l7_rules()] + ids = [l7.id for l7 in self.conn.load_balancer.l7_rules( + l7_policy=self.L7POLICY_ID)] self.assertIn(self.L7RULE_ID, ids) def test_l7_rule_update(self): test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) - self.conn.load_balancer.update_l7_rule( - self.L7RULE_ID, value=self.UPDATE_NAME) + self.conn.load_balancer.update_l7_rule(self.L7RULE_ID, + l7_policy=self.L7POLICY_ID, + rule_value=self.UPDATE_NAME) self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) test_l7_rule = self.conn.load_balancer.get_l7_rule( - self.L7RULE_ID) - self.assertEqual(self.UPDATE_NAME, test_l7_rule.value) + self.L7RULE_ID, l7_policy=self.L7POLICY_ID) + self.assertEqual(self.UPDATE_NAME, test_l7_rule.rule_value) - self.conn.load_balancer.update_l7_policy(self.L7POLICY_ID, - value=self.L7RULE_VALUE) + self.conn.load_balancer.update_l7_rule(self.L7RULE_ID, + l7_policy=self.L7POLICY_ID, + rule_value=self.L7RULE_VALUE) self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) test_l7_rule = self.conn.load_balancer.get_l7_rule( - self.L7RULE_ID) - self.assertEqual(self.L7RULE_VALUE, test_l7_rule.value) + self.L7RULE_ID, l7_policy=self.L7POLICY_ID,) + self.assertEqual(self.L7RULE_VALUE, test_l7_rule.rule_value) diff --git a/openstack/tests/unit/load_balancer/test_l7rule.py b/openstack/tests/unit/load_balancer/test_l7rule.py index 7e99af377..fd2e3cf81 100644 --- a/openstack/tests/unit/load_balancer/test_l7rule.py +++ b/openstack/tests/unit/load_balancer/test_l7rule.py @@ -36,9 +36,9 @@ class TestL7Rule(testtools.TestCase): def test_basic(self): test_l7rule = l7_rule.L7Rule() - self.assertEqual('l7rule', test_l7rule.resource_key) - self.assertEqual('l7rules', test_l7rule.resources_key) - self.assertEqual('/l7policies/%(l7_policy_id)s/rules', + self.assertEqual('rule', test_l7rule.resource_key) + self.assertEqual('rules', test_l7rule.resources_key) + self.assertEqual('/v2.0/lbaas/l7policies/%(l7_policy_id)s/rules', test_l7rule.base_path) self.assertEqual('load-balancer', test_l7rule.service.service_type) self.assertTrue(test_l7rule.allow_create) @@ -63,4 +63,4 @@ def test_make_it(self): test_l7rule.provisioning_status) self.assertEqual(EXAMPLE['type'], test_l7rule.type) self.assertEqual(EXAMPLE['updated_at'], test_l7rule.updated_at) - self.assertEqual(EXAMPLE['value'], test_l7rule.value) + self.assertEqual(EXAMPLE['value'], test_l7rule.rule_value) diff --git a/openstack/tests/unit/load_balancer/test_proxy.py b/openstack/tests/unit/load_balancer/test_proxy.py index 4db9ad239..f3690d827 100644 --- a/openstack/tests/unit/load_balancer/test_proxy.py +++ b/openstack/tests/unit/load_balancer/test_proxy.py @@ -203,33 +203,33 @@ def test_l7_rules(self): l7_rule.L7Rule, paginated=True, method_kwargs={'l7_policy': self.L7_POLICY_ID}, - expected_kwargs={'l7policy_id': self.L7_POLICY_ID}) + expected_kwargs={'l7_policy_id': self.L7_POLICY_ID}) def test_l7_rule_get(self): self.verify_get(self.proxy.get_l7_rule, l7_rule.L7Rule, method_kwargs={'l7_policy': self.L7_POLICY_ID}, - expected_kwargs={'l7policy_id': self.L7_POLICY_ID}) + expected_kwargs={'l7_policy_id': self.L7_POLICY_ID}) def test_l7_rule_create(self): self.verify_create(self.proxy.create_l7_rule, l7_rule.L7Rule, method_kwargs={'l7_policy': self.L7_POLICY_ID}, - expected_kwargs={'l7policy_id': self.L7_POLICY_ID}) + expected_kwargs={'l7_policy_id': self.L7_POLICY_ID}) def test_l7_rule_delete(self): self.verify_delete(self.proxy.delete_l7_rule, l7_rule.L7Rule, True, method_kwargs={'l7_policy': self.L7_POLICY_ID}, - expected_kwargs={'l7policy_id': self.L7_POLICY_ID}) + expected_kwargs={'l7_policy_id': self.L7_POLICY_ID}) def test_l7_rule_find(self): self._verify2('openstack.proxy2.BaseProxy._find', self.proxy.find_l7_rule, method_args=["RULE", self.L7_POLICY_ID], expected_args=[l7_rule.L7Rule, "RULE"], - expected_kwargs={"l7policy_id": self.L7_POLICY_ID, + expected_kwargs={"l7_policy_id": self.L7_POLICY_ID, "ignore_missing": True}) def test_l7_rule_update(self): @@ -237,4 +237,4 @@ def test_l7_rule_update(self): self.proxy.update_l7_rule, method_args=["RULE", self.L7_POLICY_ID], expected_args=[l7_rule.L7Rule, "RULE"], - expected_kwargs={"l7policy_id": self.L7_POLICY_ID}) + expected_kwargs={"l7_policy_id": self.L7_POLICY_ID}) From 4e7bd7f965e1cbcdfc1a1ae45aee2682f2bc9014 Mon Sep 17 00:00:00 2001 From: lidong Date: Wed, 23 Aug 2017 09:35:23 +0800 Subject: [PATCH 1756/3836] Fix some typos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Correct some spelling errors in files : doc/source/users/guides/object_store.rst. doc/source/users/resources/cluster/v1/receiver.rst. openstack/compute/v2/_proxy.py. openstack/network/v2/_proxy.py. Change-Id: I798d11e181c61a117a910eac09ef10d9935a577c --- doc/source/users/guides/object_store.rst | 2 +- doc/source/users/resources/cluster/v1/receiver.rst | 4 ++-- openstack/compute/v2/_proxy.py | 2 +- openstack/network/v2/_proxy.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/source/users/guides/object_store.rst b/doc/source/users/guides/object_store.rst index da8c57e2c..094bac5e9 100644 --- a/doc/source/users/guides/object_store.rst +++ b/doc/source/users/guides/object_store.rst @@ -197,7 +197,7 @@ The metadata attributes to be set can be found on the We set the :attr:`~openstack.object_store.obj.Object.delete_after` value to 500 seconds, causing the object to be deleted in 300 seconds, or five minutes. That attribute corresponds to the ``X-Delete-After`` -header value, which you can see is returned when we retreive the updated +header value, which you can see is returned when we retrieve the updated metadata. :: >>> conn.object_store.get_object_metadata(ob) diff --git a/doc/source/users/resources/cluster/v1/receiver.rst b/doc/source/users/resources/cluster/v1/receiver.rst index 8d757d55d..dc23eb5c8 100644 --- a/doc/source/users/resources/cluster/v1/receiver.rst +++ b/doc/source/users/resources/cluster/v1/receiver.rst @@ -3,10 +3,10 @@ openstack.cluster.v1.receiver .. automodule:: openstack.cluster.v1.receiver -The Reciever Class +The Receiver Class ------------------ -The ``Reciever`` class inherits from :class:`~openstack.resource.Resource`. +The ``Receiver`` class inherits from :class:`~openstack.resource.Resource`. .. autoclass:: openstack.cluster.v1.receiver.Receiver :members: diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 9ee9899bc..a6bd829ef 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -335,7 +335,7 @@ def delete_server(self, server, ignore_missing=True, force=False): When set to ``True``, no exception will be set when attempting to delete a nonexistent server :param bool force: When set to ``True``, the server deletion will be - forced immediatly. + forced immediately. :returns: ``None`` """ diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 4ee8559aa..df86e9740 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -2814,7 +2814,7 @@ def subnets(self, **query): * ``description``: The subnet description * ``gateway_ip``: Subnet gateway IP address * ``ip_version``: Subnet IP address version - * ``ipv6_address_mode``: The IPv6 adress mode + * ``ipv6_address_mode``: The IPv6 address mode * ``ipv6_ra_mode``: The IPv6 router advertisement mode * ``is_dhcp_enabled``: Subnet has DHCP enabled (boolean) * ``name``: Subnet name From f18dc29eb09da2b26922022b4284edfb95e5217c Mon Sep 17 00:00:00 2001 From: liuxiaoyang Date: Wed, 23 Aug 2017 09:31:43 +0800 Subject: [PATCH 1757/3836] Add doc8 rule and check doc/source files doc8 is a linter for documents and used in openstack-manuals. It is better to enforce document linters for simple checking. This change is to add doc8 in tox file and fix line too long in some files. The current rules are as bellow: - invalid rst format - D000 - lines should not be longer than 79 characters - D001 - RST exception: line with no whitespace except in the beginning - RST exception: lines with http or https urls - RST exception: literal blocks - RST exception: rst target directives - no trailing whitespace - D002 - no tabulation for indentation - D003 - no carriage returns (use unix newlines) - D004 - no newline at end of file - D005 Change-Id: I5b409fbfd95e05921310c6ecf4afea0220fb0bf0 --- doc/source/contributors/index.rst | 6 +++--- doc/source/contributors/testing.rst | 7 ++++--- doc/source/users/guides/compute.rst | 2 +- doc/source/users/guides/logging.rst | 3 ++- doc/source/users/guides/network.rst | 20 ++++++++++--------- .../users/resources/compute/v2/limits.rst | 3 ++- .../resources/compute/v2/server_interface.rst | 3 ++- .../load_balancer/v2/health_monitor.rst | 3 ++- .../resources/metric/v1/archive_policy.rst | 3 ++- .../network/v2/auto_allocated_topology.rst | 3 ++- .../network/v2/availability_zone.rst | 3 ++- .../resources/network/v2/health_monitor.rst | 3 ++- .../resources/network/v2/metering_label.rst | 3 ++- .../network/v2/metering_label_rule.rst | 3 ++- .../network/v2/network_ip_availability.rst | 3 ++- .../network/v2/qos_bandwidth_limit_rule.rst | 3 ++- .../network/v2/qos_dscp_marking_rule.rst | 3 ++- .../network/v2/qos_minimum_bandwidth_rule.rst | 3 ++- .../resources/network/v2/rbac_policy.rst | 2 +- .../resources/network/v2/security_group.rst | 3 ++- .../network/v2/security_group_rule.rst | 3 ++- .../resources/network/v2/service_profile.rst | 3 ++- .../resources/network/v2/service_provider.rst | 3 ++- test-requirements.txt | 1 + tox.ini | 9 ++++++++- 25 files changed, 65 insertions(+), 36 deletions(-) diff --git a/doc/source/contributors/index.rst b/doc/source/contributors/index.rst index 8b246a245..67c5b261f 100644 --- a/doc/source/contributors/index.rst +++ b/doc/source/contributors/index.rst @@ -50,9 +50,9 @@ Testing The project contains three test packages, one for unit tests, one for functional tests and one for examples tests. The ``openstack.tests.unit`` -package tests the SDK's features in isolation. The ``openstack.tests.functional`` -and ``openstack.tests.examples`` packages test the SDK's features and examples -against an OpenStack cloud. +package tests the SDK's features in isolation. The +``openstack.tests.functional`` and ``openstack.tests.examples`` packages test +the SDK's features and examples against an OpenStack cloud. .. toctree:: diff --git a/doc/source/contributors/testing.rst b/doc/source/contributors/testing.rst index 32d7419e4..0998abe24 100644 --- a/doc/source/contributors/testing.rst +++ b/doc/source/contributors/testing.rst @@ -76,7 +76,8 @@ under ``clouds`` must be named ``test_cloud``. .. literalinclude:: clouds.yaml :language: yaml -Replace ``xxx.xxx.xxx.xxx`` with the IP address or FQDN of your DevStack instance. +Replace ``xxx.xxx.xxx.xxx`` with the IP address or FQDN of your DevStack +instance. You can also create a ``~/.config/openstack/clouds.yaml`` file for your DevStack cloud environment using the following commands. Replace @@ -114,8 +115,8 @@ practice, this means that the tests should initially be run against a stable branch of `DevStack `_. And like the functional tests, the examples tests connect to an OpenStack cloud using `os-client-config `_. -See the functional tests instructions for information on setting up DevStack and -os-client-config. +See the functional tests instructions for information on setting up DevStack +and os-client-config. Run *** diff --git a/doc/source/users/guides/compute.rst b/doc/source/users/guides/compute.rst index cc06fa28c..3954d584b 100644 --- a/doc/source/users/guides/compute.rst +++ b/doc/source/users/guides/compute.rst @@ -86,4 +86,4 @@ Full example: `compute resource create`_ .. _compute resource list: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/compute/list.py .. _network resource list: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/network/list.py .. _compute resource create: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/compute/create.py -.. _public–key cryptography: https://en.wikipedia.org/wiki/Public-key_cryptography \ No newline at end of file +.. _public–key cryptography: https://en.wikipedia.org/wiki/Public-key_cryptography diff --git a/doc/source/users/guides/logging.rst b/doc/source/users/guides/logging.rst index 623916fad..00014e6b7 100644 --- a/doc/source/users/guides/logging.rst +++ b/doc/source/users/guides/logging.rst @@ -26,7 +26,8 @@ To log debug and higher messages:: utils.enable_logging(debug=True, stream=sys.stdout) The ``path`` parameter controls the location of a log file. If set, this -parameter will send log messages to a file using a :py:class:`~logging.FileHandler`. +parameter will send log messages to a file using a +:py:class:`~logging.FileHandler`. To log messages to a file called ``openstack.log``:: diff --git a/doc/source/users/guides/network.rst b/doc/source/users/guides/network.rst index ae9a28b69..b403c4a6e 100644 --- a/doc/source/users/guides/network.rst +++ b/doc/source/users/guides/network.rst @@ -14,11 +14,12 @@ List Networks ------------- A **network** is an isolated `Layer 2 `_ -networking segment. There are two types of networks, project and provider networks. -Project networks are fully isolated and are not shared with other projects. Provider -networks map to existing physical networks in the data center and provide external -network access for servers. Only an OpenStack administrator can create provider -networks. Networks can be connected via routers. +networking segment. There are two types of networks, project and provider +networks. Project networks are fully isolated and are not shared with other +projects. Provider networks map to existing physical networks in the data +center and provide external network access for servers. Only an OpenStack +administrator can create provider networks. Networks can be connected via +routers. .. literalinclude:: ../examples/network/list.py :pyobject: list_networks @@ -66,10 +67,11 @@ Full example: `network resource list`_ List Routers ------------ -A **router** is a logical component that forwards data packets between networks. -It also provides `Layer 3 `_ and -`NAT `_ forwarding to -provide external network access for servers on project networks. +A **router** is a logical component that forwards data packets between +networks. It also provides +`Layer 3 `_ and +`NAT `_ +forwarding to provide external network access for servers on project networks. .. literalinclude:: ../examples/network/list.py :pyobject: list_routers diff --git a/doc/source/users/resources/compute/v2/limits.rst b/doc/source/users/resources/compute/v2/limits.rst index 090988bb9..f30ff2ffc 100644 --- a/doc/source/users/resources/compute/v2/limits.rst +++ b/doc/source/users/resources/compute/v2/limits.rst @@ -14,7 +14,8 @@ The ``Limits`` class inherits from :class:`~openstack.resource.Resource`. The AbsoluteLimits Class ------------------------ -The ``AbsoluteLimits`` class inherits from :class:`~openstack.resource.Resource`. +The ``AbsoluteLimits`` class inherits from +:class:`~openstack.resource.Resource`. .. autoclass:: openstack.compute.v2.limits.AbsoluteLimits :members: diff --git a/doc/source/users/resources/compute/v2/server_interface.rst b/doc/source/users/resources/compute/v2/server_interface.rst index e67d9964a..922606fb4 100644 --- a/doc/source/users/resources/compute/v2/server_interface.rst +++ b/doc/source/users/resources/compute/v2/server_interface.rst @@ -6,7 +6,8 @@ openstack.compute.v2.server_interface The ServerInterface Class ------------------------- -The ``ServerInterface`` class inherits from :class:`~openstack.resource.Resource`. +The ``ServerInterface`` class inherits from +:class:`~openstack.resource.Resource`. .. autoclass:: openstack.compute.v2.server_interface.ServerInterface :members: diff --git a/doc/source/users/resources/load_balancer/v2/health_monitor.rst b/doc/source/users/resources/load_balancer/v2/health_monitor.rst index c5143e0df..7f90f4261 100644 --- a/doc/source/users/resources/load_balancer/v2/health_monitor.rst +++ b/doc/source/users/resources/load_balancer/v2/health_monitor.rst @@ -6,7 +6,8 @@ openstack.load_balancer.v2.health_monitor The HealthMonitor Class ----------------------- -The ``HealthMonitor`` class inherits from :class:`~openstack.resource.Resource`. +The ``HealthMonitor`` class inherits from +:class:`~openstack.resource.Resource`. .. autoclass:: openstack.load_balancer.v2.health_monitor.HealthMonitor :members: diff --git a/doc/source/users/resources/metric/v1/archive_policy.rst b/doc/source/users/resources/metric/v1/archive_policy.rst index 903aaa4f0..7358e795e 100644 --- a/doc/source/users/resources/metric/v1/archive_policy.rst +++ b/doc/source/users/resources/metric/v1/archive_policy.rst @@ -6,7 +6,8 @@ openstack.metric.v1.archive_policy The ArchivePolicy Class ----------------------- -The ``ArchivePolicy`` class inherits from :class:`~openstack.resource.Resource`. +The ``ArchivePolicy`` class inherits from +:class:`~openstack.resource.Resource`. .. autoclass:: openstack.metric.v1.archive_policy.ArchivePolicy :members: diff --git a/doc/source/users/resources/network/v2/auto_allocated_topology.rst b/doc/source/users/resources/network/v2/auto_allocated_topology.rst index f27241c50..a3700d087 100644 --- a/doc/source/users/resources/network/v2/auto_allocated_topology.rst +++ b/doc/source/users/resources/network/v2/auto_allocated_topology.rst @@ -6,7 +6,8 @@ openstack.network.v2.auto_allocated_topology The Auto Allocated Topology Class --------------------------------- -The ``Auto Allocated Toplogy`` class inherits from :class:`~openstack.resource.Resource`. +The ``Auto Allocated Toplogy`` class inherits from +:class:`~openstack.resource.Resource`. .. autoclass:: openstack.network.v2.auto_allocated_topology.AutoAllocatedTopology :members: diff --git a/doc/source/users/resources/network/v2/availability_zone.rst b/doc/source/users/resources/network/v2/availability_zone.rst index a33f2b65e..da386ce6d 100644 --- a/doc/source/users/resources/network/v2/availability_zone.rst +++ b/doc/source/users/resources/network/v2/availability_zone.rst @@ -6,7 +6,8 @@ openstack.network.v2.availability_zone The AvailabilityZone Class -------------------------- -The ``AvailabilityZone`` class inherits from :class:`~openstack.resource.Resource`. +The ``AvailabilityZone`` class inherits from +:class:`~openstack.resource.Resource`. .. autoclass:: openstack.network.v2.availability_zone.AvailabilityZone :members: diff --git a/doc/source/users/resources/network/v2/health_monitor.rst b/doc/source/users/resources/network/v2/health_monitor.rst index 63a029cd1..76cf6a7e8 100644 --- a/doc/source/users/resources/network/v2/health_monitor.rst +++ b/doc/source/users/resources/network/v2/health_monitor.rst @@ -6,7 +6,8 @@ openstack.network.v2.health_monitor The HealthMonitor Class ----------------------- -The ``HealthMonitor`` class inherits from :class:`~openstack.resource.Resource`. +The ``HealthMonitor`` class inherits from +:class:`~openstack.resource.Resource`. .. autoclass:: openstack.network.v2.health_monitor.HealthMonitor :members: diff --git a/doc/source/users/resources/network/v2/metering_label.rst b/doc/source/users/resources/network/v2/metering_label.rst index dacb3801b..d07f02512 100644 --- a/doc/source/users/resources/network/v2/metering_label.rst +++ b/doc/source/users/resources/network/v2/metering_label.rst @@ -6,7 +6,8 @@ openstack.network.v2.metering_label The MeteringLabel Class ----------------------- -The ``MeteringLabel`` class inherits from :class:`~openstack.resource.Resource`. +The ``MeteringLabel`` class inherits from +:class:`~openstack.resource.Resource`. .. autoclass:: openstack.network.v2.metering_label.MeteringLabel :members: diff --git a/doc/source/users/resources/network/v2/metering_label_rule.rst b/doc/source/users/resources/network/v2/metering_label_rule.rst index 390884933..9bcf7840c 100644 --- a/doc/source/users/resources/network/v2/metering_label_rule.rst +++ b/doc/source/users/resources/network/v2/metering_label_rule.rst @@ -6,7 +6,8 @@ openstack.network.v2.metering_label_rule The MeteringLabelRule Class --------------------------- -The ``MeteringLabelRule`` class inherits from :class:`~openstack.resource.Resource`. +The ``MeteringLabelRule`` class inherits from +:class:`~openstack.resource.Resource`. .. autoclass:: openstack.network.v2.metering_label_rule.MeteringLabelRule :members: diff --git a/doc/source/users/resources/network/v2/network_ip_availability.rst b/doc/source/users/resources/network/v2/network_ip_availability.rst index 3cbbd9f27..900f2461b 100644 --- a/doc/source/users/resources/network/v2/network_ip_availability.rst +++ b/doc/source/users/resources/network/v2/network_ip_availability.rst @@ -6,7 +6,8 @@ openstack.network.v2.network_ip_availability The NetworkIPAvailability Class ------------------------------- -The ``NetworkIPAvailability`` class inherits from :class:`~openstack.resource.Resource`. +The ``NetworkIPAvailability`` class inherits from +:class:`~openstack.resource.Resource`. .. autoclass:: openstack.network.v2.network_ip_availability.NetworkIPAvailability :members: diff --git a/doc/source/users/resources/network/v2/qos_bandwidth_limit_rule.rst b/doc/source/users/resources/network/v2/qos_bandwidth_limit_rule.rst index 115a55de5..98d904027 100644 --- a/doc/source/users/resources/network/v2/qos_bandwidth_limit_rule.rst +++ b/doc/source/users/resources/network/v2/qos_bandwidth_limit_rule.rst @@ -6,7 +6,8 @@ openstack.network.v2.qos_bandwidth_limit_rule The QoSBandwidthLimitRule Class ------------------------------- -The ``QoSBandwidthLimitRule`` class inherits from :class:`~openstack.resource.Resource`. +The ``QoSBandwidthLimitRule`` class inherits from +:class:`~openstack.resource.Resource`. .. autoclass:: openstack.network.v2.qos_bandwidth_limit_rule.QoSBandwidthLimitRule :members: diff --git a/doc/source/users/resources/network/v2/qos_dscp_marking_rule.rst b/doc/source/users/resources/network/v2/qos_dscp_marking_rule.rst index 6d2cf9a45..c00023642 100644 --- a/doc/source/users/resources/network/v2/qos_dscp_marking_rule.rst +++ b/doc/source/users/resources/network/v2/qos_dscp_marking_rule.rst @@ -6,7 +6,8 @@ openstack.network.v2.qos_dscp_marking_rule The QoSDSCPMarkingRule Class ---------------------------- -The ``QoSDSCPMarkingRule`` class inherits from :class:`~openstack.resource.Resource`. +The ``QoSDSCPMarkingRule`` class inherits from +:class:`~openstack.resource.Resource`. .. autoclass:: openstack.network.v2.qos_dscp_marking_rule.QoSDSCPMarkingRule :members: diff --git a/doc/source/users/resources/network/v2/qos_minimum_bandwidth_rule.rst b/doc/source/users/resources/network/v2/qos_minimum_bandwidth_rule.rst index 6ba004b4d..05c52768b 100644 --- a/doc/source/users/resources/network/v2/qos_minimum_bandwidth_rule.rst +++ b/doc/source/users/resources/network/v2/qos_minimum_bandwidth_rule.rst @@ -6,7 +6,8 @@ openstack.network.v2.qos_minimum_bandwidth_rule The QoSMinimumBandwidthRule Class --------------------------------- -The ``QoSMinimumBandwidthRule`` class inherits from :class:`~openstack.resource.Resource`. +The ``QoSMinimumBandwidthRule`` class inherits from +:class:`~openstack.resource.Resource`. .. autoclass:: openstack.network.v2.qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule :members: diff --git a/doc/source/users/resources/network/v2/rbac_policy.rst b/doc/source/users/resources/network/v2/rbac_policy.rst index daf76007a..062da1d7a 100644 --- a/doc/source/users/resources/network/v2/rbac_policy.rst +++ b/doc/source/users/resources/network/v2/rbac_policy.rst @@ -9,4 +9,4 @@ The RBACPolicy Class The ``RBACPolicy`` class inherits from :class:`~openstack.resource.Resource`. .. autoclass:: openstack.network.v2.rbac_policy.RBACPolicy - :members: \ No newline at end of file + :members: diff --git a/doc/source/users/resources/network/v2/security_group.rst b/doc/source/users/resources/network/v2/security_group.rst index 5d4788436..2d0c860a9 100644 --- a/doc/source/users/resources/network/v2/security_group.rst +++ b/doc/source/users/resources/network/v2/security_group.rst @@ -6,7 +6,8 @@ openstack.network.v2.security_group The SecurityGroup Class ----------------------- -The ``SecurityGroup`` class inherits from :class:`~openstack.resource.Resource`. +The ``SecurityGroup`` class inherits from +:class:`~openstack.resource.Resource`. .. autoclass:: openstack.network.v2.security_group.SecurityGroup :members: diff --git a/doc/source/users/resources/network/v2/security_group_rule.rst b/doc/source/users/resources/network/v2/security_group_rule.rst index 245720053..8af566ebc 100644 --- a/doc/source/users/resources/network/v2/security_group_rule.rst +++ b/doc/source/users/resources/network/v2/security_group_rule.rst @@ -6,7 +6,8 @@ openstack.network.v2.security_group_rule The SecurityGroupRule Class --------------------------- -The ``SecurityGroupRule`` class inherits from :class:`~openstack.resource.Resource`. +The ``SecurityGroupRule`` class inherits from +:class:`~openstack.resource.Resource`. .. autoclass:: openstack.network.v2.security_group_rule.SecurityGroupRule :members: diff --git a/doc/source/users/resources/network/v2/service_profile.rst b/doc/source/users/resources/network/v2/service_profile.rst index c1c013028..5d66c4473 100644 --- a/doc/source/users/resources/network/v2/service_profile.rst +++ b/doc/source/users/resources/network/v2/service_profile.rst @@ -6,7 +6,8 @@ openstack.network.v2.service_profile The ServiceProfile Class ------------------------ -The ``ServiceProfile`` class inherits from :class:`~openstack.resource.Resource`. +The ``ServiceProfile`` class inherits from +:class:`~openstack.resource.Resource`. .. autoclass:: openstack.network.v2.service_profile.ServiceProfile :members: diff --git a/doc/source/users/resources/network/v2/service_provider.rst b/doc/source/users/resources/network/v2/service_provider.rst index e9d667848..02f912dec 100644 --- a/doc/source/users/resources/network/v2/service_provider.rst +++ b/doc/source/users/resources/network/v2/service_provider.rst @@ -6,7 +6,8 @@ openstack.network.v2.service_provider The Service Provider Class -------------------------- -The ``Service Provider`` class inherits from :class:`~openstack.resource.Resource`. +The ``Service Provider`` class inherits from +:class:`~openstack.resource.Resource`. .. autoclass:: openstack.network.v2.service_provider.ServiceProvider :members: diff --git a/test-requirements.txt b/test-requirements.txt index c82640c82..cb5107be4 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,6 +5,7 @@ hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 beautifulsoup4>=4.6.0 # MIT coverage!=4.4,>=4.0 # Apache-2.0 +doc8 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD mock>=2.0.0 # BSD python-subunit>=0.0.18 # Apache-2.0/BSD diff --git a/tox.ini b/tox.ini index 789758b43..ac1e695a9 100644 --- a/tox.ini +++ b/tox.ini @@ -41,9 +41,16 @@ commands = {posargs} commands = python setup.py test --coverage --coverage-package-name=openstack --testr-args='{posargs}' [testenv:docs] -commands = python setup.py build_sphinx +commands = + doc8 doc/source + python setup.py build_sphinx [flake8] ignore=D100,D101,D102,D103,D104,D105,D200,D202,D204,D205,D211,D301,D400,D401 show-source = True exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build + +[doc8] +extensions = .rst, .yaml +# Maximal line length should be 80. +max-line-length = 80 From bda65e7265247b2d20d8d18d0cbf344bee119425 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Mon, 10 Jul 2017 12:19:42 -0400 Subject: [PATCH 1758/3836] De-client-ify Service List Change-Id: I22149f4dd2d63b0f3b809015f4e9633f20a0a1d0 --- shade/_tasks.py | 5 ----- shade/operatorcloud.py | 12 ++++++++---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 1573304b3..6822ab656 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -127,11 +127,6 @@ def main(self, client): return client.ironic_client.node.set_provision_state(**self.args) -class ServiceList(task_manager.Task): - def main(self, client): - return client.keystone_client.services.list() - - class ServiceUpdate(task_manager.Task): def main(self, client): return client.keystone_client.services.update(**self.args) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 4099a4b8a..cd8585825 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -805,10 +805,14 @@ def list_services(self): :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call. """ - # TODO(mordred) When this changes to REST, force interface=admin - # in the adapter call - with _utils.shade_exceptions(): - services = self.manager.submit_task(_tasks.ServiceList()) + if self.cloud_config.get_api_version('identity').startswith('2'): + url, key = '/OS-KSADM/services', 'OS-KSADM:services' + else: + url, key = '/services', 'services' + data = self._identity_client.get( + url, endpoint_filter={'interface': 'admin'}, + error_message="Failed to list services") + services = self._get_and_munchify(key, data) return _utils.normalize_keystone_services(services) def search_services(self, name_or_id=None, filters=None): From b05aede84bbcb3707be1c4da0c596844ae1f2a60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Wed, 23 Aug 2017 21:34:04 +0000 Subject: [PATCH 1759/3836] Fix cleaning of Cinder volumes in functional tests There is some issue, probably with Cinder which cause sometimes problem with removing of volumes. Such volumes stuck in "deleting" state in Cinder. This patch changes cleanup method from functional Cinder test that it will not raise exception (and make test fails) if volumes will not be cleaned properly. In such case cleaning will try to delete volume once again with "force=True" and will move forward if it also fails. This change shouldn't cause any new problem with reached volumes quota limits becasue before this patch such volumes were also not removed properly. Change-Id: I984f863b55b46fe6eba4a7ec31434ee4d2baa16f --- shade/tests/functional/test_volume.py | 28 ++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/shade/tests/functional/test_volume.py b/shade/tests/functional/test_volume.py index c94577254..579d7d53d 100644 --- a/shade/tests/functional/test_volume.py +++ b/shade/tests/functional/test_volume.py @@ -20,6 +20,7 @@ from testtools import content from shade import _utils +from shade import exc from shade.tests.functional import base @@ -100,18 +101,27 @@ def cleanup(self, volume, snapshot_name=None, image_name=None): # in the volume list anymore for v in volume: self.user_cloud.delete_volume(v, wait=False) - for count in _utils._iterate_timeout( - 180, "Timeout waiting for volume cleanup"): - found = False + try: + for count in _utils._iterate_timeout( + 180, "Timeout waiting for volume cleanup"): + found = False + for existing in self.user_cloud.list_volumes(): + for v in volume: + if v['id'] == existing['id']: + found = True + break + if found: + break + if not found: + break + except exc.OpenStackCloudTimeout: + # NOTE(slaweq): ups, some volumes are still not removed + # so we should try to force delete it once again and move + # forward for existing in self.user_cloud.list_volumes(): for v in volume: if v['id'] == existing['id']: - found = True - break - if found: - break - if not found: - break + self.operator_cloud.delete_volume(v, force=True) def test_list_volumes_pagination(self): '''Test pagination for list volumes functionality''' From 4cd71a139db8745d63b12389ed2029e1e762072c Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Mon, 10 Jul 2017 12:12:06 -0400 Subject: [PATCH 1760/3836] De-client-ify Service Update Change-Id: I161b680c6f165d298538ec4e13264deeb92a8453 --- shade/_tasks.py | 5 ----- shade/operatorcloud.py | 18 ++++++++++-------- shade/tests/unit/test_services.py | 10 +++++----- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 6822ab656..c1801dd59 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -127,11 +127,6 @@ def main(self, client): return client.ironic_client.node.set_provision_state(**self.args) -class ServiceUpdate(task_manager.Task): - def main(self, client): - return client.keystone_client.services.update(**self.args) - - class ServiceDelete(task_manager.Task): def main(self, client): return client.keystone_client.services.delete(**self.args) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index cd8585825..9db206fff 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -778,8 +778,6 @@ def update_service(self, name_or_id, **kwargs): 'Unavailable Feature: Service update requires Identity v3' ) - # TODO(mordred) When this changes to REST, force interface=admin - # in the adapter call # NOTE(SamYaple): Keystone v3 only accepts 'type' but shade accepts # both 'type' and 'service_type' with a preference # towards 'type' @@ -788,13 +786,17 @@ def update_service(self, name_or_id, **kwargs): if type_ or service_type: kwargs['type'] = type_ or service_type - with _utils.shade_exceptions( - "Error in updating service {service}".format(service=name_or_id) - ): - service = self.manager.submit_task( - _tasks.ServiceUpdate(service=name_or_id, **kwargs) - ) + if self._is_client_version('identity', 2): + url, key = '/OS-KSADM/services', 'OS-KSADM:service' + else: + url, key = '/services', 'service' + service = self.get_service(name_or_id) + msg = 'Error in updating service {service}'.format(service=name_or_id) + data = self._identity_client.patch( + '{url}/{id}'.format(url=url, id=service['id']), json={key: kwargs}, + endpoint_filter={'interface': 'admin'}, error_message=msg) + service = self._get_and_munchify(key, data) return _utils.normalize_keystone_services([service])[0] def list_services(self): diff --git a/shade/tests/unit/test_services.py b/shade/tests/unit/test_services.py index 585184e6f..6ff05a40c 100644 --- a/shade/tests/unit/test_services.py +++ b/shade/tests/unit/test_services.py @@ -105,15 +105,15 @@ def test_update_service_v3(self): request.pop('name') request.pop('type') self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'services': [resp['service']]}), dict(method='PATCH', uri=self.get_mock_url(append=[service_data.service_id]), status_code=200, json=resp, - validate=dict(json={'service': request})), - dict(method='GET', - uri=self.get_mock_url(append=[service_data.service_id]), - status_code=200, - json=resp), + validate=dict(json={'service': request})) ]) service = self.op_cloud.update_service(service_data.service_id, From 522f51cdf60ed214f07430d0fe01e1638654322c Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Mon, 10 Jul 2017 12:53:35 -0400 Subject: [PATCH 1761/3836] De-client-ify Service Delete Change-Id: I9a034bf240450574dfcc6ab8074db708002d0659 --- shade/_tasks.py | 5 ----- shade/operatorcloud.py | 14 +++++++------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index c1801dd59..6c5e962dc 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -127,11 +127,6 @@ def main(self, client): return client.ironic_client.node.set_provision_state(**self.args) -class ServiceDelete(task_manager.Task): - def main(self, client): - return client.keystone_client.services.delete(**self.args) - - class EndpointCreate(task_manager.Task): def main(self, client): return client.keystone_client.endpoints.create(**self.args) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 9db206fff..b4e365384 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -867,14 +867,14 @@ def delete_service(self, name_or_id): return False if self._is_client_version('identity', 2): - service_kwargs = {'id': service['id']} + url = '/OS-KSADM/services' else: - service_kwargs = {'service': service['id']} - # TODO(mordred) When this changes to REST, force interface=admin - # in the adapter call - with _utils.shade_exceptions("Failed to delete service {id}".format( - id=service['id'])): - self.manager.submit_task(_tasks.ServiceDelete(**service_kwargs)) + url = '/services' + + error_msg = 'Failed to delete service {id}'.format(id=service['id']) + self._identity_client.delete( + '{url}/{id}'.format(url=url, id=service['id']), + endpoint_filter={'interface': 'admin'}, error_message=error_msg) return True From 7fb83a668d73b53e84d8994a802b1e6b14ecde32 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Thu, 24 Aug 2017 06:33:45 -0300 Subject: [PATCH 1762/3836] Switch to _is_client_version in list_services Change-Id: Ie7e77c4a766d01557222decdab48fb206086e57f --- shade/operatorcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index b4e365384..8fec1b313 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -807,7 +807,7 @@ def list_services(self): :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call. """ - if self.cloud_config.get_api_version('identity').startswith('2'): + if self._is_client_version('identity', 2): url, key = '/OS-KSADM/services', 'OS-KSADM:services' else: url, key = '/services', 'services' From c9028ee6cf4e6cc5f439a35c7f3c36e71c15eeb3 Mon Sep 17 00:00:00 2001 From: Jens Harbott Date: Thu, 24 Aug 2017 12:28:51 +0000 Subject: [PATCH 1763/3836] Allow filtering network ports by fixed_ips The patch in [1] introduced filtering network ports by fixed-ip in the openstackclient, but it doesn't really work because the filter isn't accepted in the sdk. Add that option and add an unit test for the filter list. [1] https://review.openstack.org/388575 Change-Id: I6cbb7f1aaff8ad12234f68ddb3fe564c556ece54 Closes-Bug: 1712527 --- openstack/network/v2/port.py | 2 +- openstack/tests/unit/network/v2/test_port.py | 22 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index b6f94665d..1356df49d 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -30,7 +30,7 @@ class Port(resource.Resource, tag.TagMixin): # NOTE: we skip query on list or datetime fields for now _query_mapping = resource.QueryParameters( - 'description', 'device_id', 'device_owner', 'ip_address', + 'description', 'device_id', 'device_owner', 'fixed_ips', 'ip_address', 'mac_address', 'name', 'network_id', 'status', 'subnet_id', is_admin_state_up='admin_state_up', is_port_security_enabled='port_security_enabled', diff --git a/openstack/tests/unit/network/v2/test_port.py b/openstack/tests/unit/network/v2/test_port.py index dd931a765..2bfbcdac3 100644 --- a/openstack/tests/unit/network/v2/test_port.py +++ b/openstack/tests/unit/network/v2/test_port.py @@ -71,6 +71,28 @@ def test_basic(self): self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) + self.assertDictEqual({"description": "description", + "device_id": "device_id", + "device_owner": "device_owner", + "fixed_ips": "fixed_ips", + "ip_address": "ip_address", + "mac_address": "mac_address", + "name": "name", + "network_id": "network_id", + "status": "status", + "subnet_id": "subnet_id", + "is_admin_state_up": "admin_state_up", + "is_port_security_enabled": + "port_security_enabled", + "project_id": "tenant_id", + "limit": "limit", + "marker": "marker", + "any_tags": "tags-any", + "not_any_tags": "not-tags-any", + "not_tags": "not-tags", + "tags": "tags"}, + sot._query_mapping._mapping) + def test_make_it(self): sot = port.Port(**EXAMPLE) self.assertTrue(sot.is_admin_state_up) From 96f86ee6bb3fb8fc32837ba4c57ebc1d165cef4b Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 25 Aug 2017 14:01:12 +0800 Subject: [PATCH 1764/3836] Add parameter_groups and conditions params for StackTemplate Change-Id: I333be10bcc5b6ddbf27bbda57fd27de24e7438d9 --- openstack/orchestration/v1/stack_template.py | 11 +++++++++++ .../unit/orchestration/v1/test_stack_template.py | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/openstack/orchestration/v1/stack_template.py b/openstack/orchestration/v1/stack_template.py index 35b2db4cb..7dace49a5 100644 --- a/openstack/orchestration/v1/stack_template.py +++ b/openstack/orchestration/v1/stack_template.py @@ -42,3 +42,14 @@ class StackTemplate(resource.Resource): #: Key and value pairs that contain definition of resources in the #: template resources = resource.Body('resources', type=dict) + # List parameters grouped. + parameter_groups = resource.Body('parameter_groups', type=list) + # Restrict conditions. + conditions = resource.Body('conditions', type=dict) + + def to_dict(self): + mapping = super(StackTemplate, self).to_dict() + mapping.pop('location') + mapping.pop('id') + mapping.pop('name') + return mapping diff --git a/openstack/tests/unit/orchestration/v1/test_stack_template.py b/openstack/tests/unit/orchestration/v1/test_stack_template.py index 592d644ee..7f927081e 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack_template.py +++ b/openstack/tests/unit/orchestration/v1/test_stack_template.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import copy import testtools from openstack.orchestration.v1 import stack_template @@ -53,3 +54,13 @@ def test_make_it(self): self.assertEqual(FAKE['outputs'], sot.outputs) self.assertEqual(FAKE['parameters'], sot.parameters) self.assertEqual(FAKE['resources'], sot.resources) + + def test_to_dict(self): + fake_sot = copy.deepcopy(FAKE) + fake_sot['parameter_groups'] = [{ + "description": "server parameters", + "parameters": ["key_name", "image_id"], + "label": "server_parameters"}] + fake_sot['conditions'] = {"cd1": True} + sot = stack_template.StackTemplate(**fake_sot) + self.assertEqual(fake_sot, sot.to_dict()) From 68f0947ba09baeb545474f0bbb808262613439d6 Mon Sep 17 00:00:00 2001 From: Sam Yaple Date: Fri, 25 Aug 2017 21:17:51 -0400 Subject: [PATCH 1765/3836] Fix switched params Fixes a reversal of params in get_domain() Change-Id: I3f6e763a2378f52954c9d835a233867fb46f3e56 --- shade/operatorcloud.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index cd8585825..941ebac83 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1218,6 +1218,12 @@ def get_domain(self, domain_id=None, name_or_id=None, filters=None): the openstack API call. """ if domain_id is None: + # NOTE(SamYaple): search_domains() has filters and name_or_id + # in the wrong positional order which prevents _get_entity from + # being able to return quickly if passing a domain object so we + # duplicate that logic here + if hasattr(name_or_id, 'id'): + return name_or_id return _utils._get_entity(self.search_domains, filters, name_or_id) else: error_msg = 'Failed to get domain {id}'.format(id=domain_id) From 98885734d274294b08cf4033441c2ace8e80246d Mon Sep 17 00:00:00 2001 From: dommgifer Date: Tue, 29 Aug 2017 15:10:35 +0800 Subject: [PATCH 1766/3836] Connection doc add arguments Connection doc add "project_domain_name" and "user_domain_name" attribute. Change-Id: I56b855ee184dee7964e0d93d26ba8aa81273b69b Close-bug: #1622523 --- openstack/connection.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openstack/connection.py b/openstack/connection.py index 6f42d35d4..aa0d5a0ea 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -34,6 +34,8 @@ auth_args = { 'auth_url': 'http://172.20.1.108:5000/v3', 'project_name': 'admin', + 'user_domain_name': 'default', + 'project_domain_name': 'default', 'username': 'admin', 'password': 'admin', } From 1935528ed750b6f3d3d0be6c95a61d511d63076c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Tue, 29 Aug 2017 21:34:45 +0200 Subject: [PATCH 1767/3836] Fix handling timeouts in volume functional tests cleanup This patch adds handle for fixture.TimeoutException in same way as it was done for OpenStackCloudTimeout. It also changes cleanup function that it will not wait for delete of volumes if volume was stuck in "deletion" state. Delete volume with "force=True" will now be call with "wait=False" Change-Id: Ie6a8c7aecaf36b07247b61296d68afd923280440 --- shade/tests/functional/test_volume.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/shade/tests/functional/test_volume.py b/shade/tests/functional/test_volume.py index 59f818547..fe4dd6c6f 100644 --- a/shade/tests/functional/test_volume.py +++ b/shade/tests/functional/test_volume.py @@ -17,6 +17,7 @@ Functional tests for `shade` block storage methods. """ +from fixtures import TimeoutException from testtools import content from shade import _utils @@ -117,14 +118,15 @@ def cleanup(self, volume, snapshot_name=None, image_name=None): break if not found: break - except exc.OpenStackCloudTimeout: + except (exc.OpenStackCloudTimeout, TimeoutException): # NOTE(slaweq): ups, some volumes are still not removed # so we should try to force delete it once again and move # forward for existing in self.user_cloud.list_volumes(): for v in volume: if v['id'] == existing['id']: - self.operator_cloud.delete_volume(v, force=True) + self.operator_cloud.delete_volume( + v, wait=False, force=True) def test_list_volumes_pagination(self): '''Test pagination for list volumes functionality''' From 589b765574d457d2ed9f87b4fd41cf38b4634d1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Tue, 29 Aug 2017 21:58:40 +0200 Subject: [PATCH 1768/3836] Add handling timeout in servers cleanup function In functional tests in method which cleans server and volumes attached to it, there was sometimes reached timeout, probably because one of resources stuck in "deletion" state for longer than 180 seconds. This patch adds handling such timeout and in such case, try to call delete of server and each volume but without waiting for deletion to be completed and with "force" option in case of volumes. Change-Id: I7a8caf2e89348ae0931a7e7e30ce2a83c4d12e5a --- shade/tests/functional/test_compute.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index 2f5191cb8..da6c315cb 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -17,6 +17,7 @@ Functional tests for `shade` compute methods. """ +from fixtures import TimeoutException import six from shade import exc @@ -51,10 +52,19 @@ def _cleanup_servers_and_volumes(self, server_name): if not server: return volumes = self.user_cloud.get_volumes(server) - self.user_cloud.delete_server(server.name, wait=True) - for volume in volumes: - if volume.status != 'deleting': - self.user_cloud.delete_volume(volume.id, wait=True) + try: + self.user_cloud.delete_server(server.name, wait=True) + for volume in volumes: + if volume.status != 'deleting': + self.user_cloud.delete_volume(volume.id, wait=True) + except (exc.OpenStackCloudTimeout, TimeoutException): + # Ups, some timeout occured during process of deletion server + # or volumes, so now we will try to call delete each of them + # once again and we will try to live with it + self.user_cloud.delete_server(server.name) + for volume in volumes: + self.operator_cloud.delete_volume( + volume.id, wait=False, force=True) def test_create_and_delete_server(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) From 4695f5c1b6960c475632e80eccce63bb4d8c39f9 Mon Sep 17 00:00:00 2001 From: Sam Yaple Date: Thu, 24 Aug 2017 14:04:03 -0400 Subject: [PATCH 1769/3836] Add domain_id to groups When groups are hosted in ldap you must query the domain explictly to list the groups Change-Id: I5be672c3eae5e013525cc7c0a4d73f9166f379ba --- shade/operatorcloud.py | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index cd8585825..3adc25a8c 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1227,45 +1227,54 @@ def get_domain(self, domain_id=None, name_or_id=None, filters=None): domain = self._get_and_munchify('domain', data) return _utils.normalize_domains([domain])[0] + @_utils.valid_kwargs('domain_id') @_utils.cache_on_arguments() - def list_groups(self): + def list_groups(self, **kwargs): """List Keystone Groups. + :param domain_id: domain id. + :returns: A list of ``munch.Munch`` containing the group description. :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ data = self._identity_client.get( - '/groups', error_message="Failed to list groups") + '/groups', params=kwargs, error_message="Failed to list groups") return _utils.normalize_groups(self._get_and_munchify('groups', data)) - def search_groups(self, name_or_id=None, filters=None): + @_utils.valid_kwargs('domain_id') + def search_groups(self, name_or_id=None, filters=None, **kwargs): """Search Keystone groups. :param name: Group name or id. :param filters: A dict containing additional filters to use. + :param domain_id: domain id. :returns: A list of ``munch.Munch`` containing the group description. :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ - groups = self.list_groups() - return _utils._filter_list(groups, name_or_id, filters) + groups = self.list_groups(**kwargs) + return _utils._filter_list(groups, name_or_id, filters, + **kwargs) - def get_group(self, name_or_id, filters=None): + @_utils.valid_kwargs('domain_id') + def get_group(self, name_or_id, filters=None, **kwargs): """Get exactly one Keystone group. :param id: Group name or id. :param filters: A dict containing additional filters to use. + :param domain_id: domain id. :returns: A ``munch.Munch`` containing the group description. :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ - return _utils._get_entity(self.search_groups, name_or_id, filters) + return _utils._get_entity(self.search_groups, name_or_id, filters, + **kwargs) def create_group(self, name, description, domain=None): """Create a group. @@ -1298,11 +1307,14 @@ def create_group(self, name, description, domain=None): self.list_groups.invalidate(self) return _utils.normalize_groups([group])[0] - def update_group(self, name_or_id, name=None, description=None): + @_utils.valid_kwargs('domain_id') + def update_group(self, name_or_id, name=None, description=None, + **kwargs): """Update an existing group :param string name: New group name. :param string description: New group description. + :param domain_id: domain id. :returns: A ``munch.Munch`` containing the group description. @@ -1310,7 +1322,7 @@ def update_group(self, name_or_id, name=None, description=None): the openstack API call. """ self.list_groups.invalidate(self) - group = self.get_group(name_or_id) + group = self.get_group(name_or_id, **kwargs) if group is None: raise OpenStackCloudException( "Group {0} not found for updating".format(name_or_id) @@ -1330,17 +1342,19 @@ def update_group(self, name_or_id, name=None, description=None): self.list_groups.invalidate(self) return _utils.normalize_groups([group])[0] - def delete_group(self, name_or_id): + @_utils.valid_kwargs('domain_id') + def delete_group(self, name_or_id, **kwargs): """Delete a group :param name_or_id: ID or name of the group to delete. + :param domain_id: domain id. :returns: True if delete succeeded, False otherwise. :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ - group = self.get_group(name_or_id) + group = self.get_group(name_or_id, **kwargs) if group is None: self.log.debug( "Group %s not found for deleting", name_or_id) From 8887b483e193cebc5b4a8df12b3325ec4e8938c5 Mon Sep 17 00:00:00 2001 From: Sam Yaple Date: Thu, 24 Aug 2017 12:34:49 -0400 Subject: [PATCH 1770/3836] Support domain_id for user operations When using ldap you cannot list all users across all domains. You must explicitly declare the domain you want to list. Change-Id: I83d8e313a86a527ea4ccf83cb8c329a7123c2943 --- shade/openstackcloud.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 55c69768d..9fd9be757 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -965,23 +965,28 @@ def delete_project(self, name_or_id, domain_id=None): return True + @_utils.valid_kwargs('domain_id') @_utils.cache_on_arguments() - def list_users(self): + def list_users(self, **kwargs): """List users. + :param domain_id: Domain ID. (v3) + :returns: a list of ``munch.Munch`` containing the user description. :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ - data = self._identity_client.get('/users') + data = self._identity_client.get('/users', params=kwargs) return _utils.normalize_users( self._get_and_munchify('users', data)) - def search_users(self, name_or_id=None, filters=None): + @_utils.valid_kwargs('domain_id') + def search_users(self, name_or_id=None, filters=None, **kwargs): """Search users. :param string name_or_id: user name or ID. + :param domain_id: Domain ID. (v3) :param filters: a dict containing additional filters to use. OR A string containing a jmespath expression for further filtering. @@ -992,13 +997,15 @@ def search_users(self, name_or_id=None, filters=None): :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ - users = self.list_users() + users = self.list_users(**kwargs) return _utils._filter_list(users, name_or_id, filters) - def get_user(self, name_or_id, filters=None): + @_utils.valid_kwargs('domain_id') + def get_user(self, name_or_id, filters=None, **kwargs): """Get exactly one user. :param string name_or_id: user name or ID. + :param domain_id: Domain ID. (v3) :param filters: a dict containing additional filters to use. OR A string containing a jmespath expression for further filtering. @@ -1009,7 +1016,8 @@ def get_user(self, name_or_id, filters=None): :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ - return _utils._get_entity(self.search_users, name_or_id, filters) + return _utils._get_entity(self.search_users, name_or_id, filters, + **kwargs) def get_user_by_id(self, user_id, normalize=True): """Get a user by ID. @@ -1034,7 +1042,10 @@ def get_user_by_id(self, user_id, normalize=True): 'description', 'default_project') def update_user(self, name_or_id, **kwargs): self.list_users.invalidate(self) - user = self.get_user(name_or_id) + user_kwargs = {} + if 'domain_id' in kwargs and kwargs['domain_id']: + user_kwargs['domain_id'] = kwargs['domain_id'] + user = self.get_user(name_or_id, **user_kwargs) # normalized dict won't work kwargs['user'] = self.get_user_by_id(user['id'], normalize=False) @@ -1086,11 +1097,11 @@ def create_user( self.list_users.invalidate(self) return _utils.normalize_users([user])[0] - def delete_user(self, name_or_id): - # TODO(mordred) Support name_or_id as dict to avoid any gets + @_utils.valid_kwargs('domain_id') + def delete_user(self, name_or_id, **kwargs): # TODO(mordred) Why are we invalidating at the TOP? self.list_users.invalidate(self) - user = self.get_user(name_or_id) + user = self.get_user(name_or_id, **kwargs) if not user: self.log.debug( "User {0} not found for deleting".format(name_or_id)) From 78a1aa3b931b2ad7fd2820292ba4060eb6feb043 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 31 Aug 2017 08:38:33 -0500 Subject: [PATCH 1771/3836] Add tox_install.sh to deal with upper-constraints shade is in global requirements and upper-constraints now, which means we need to use the tox_install.sh script so that constraints don't break install of shade itself. Change-Id: Id286b634046519e34ac2c01461391d5bc61cd919 --- tools/tox_install.sh | 30 ++++++++++++++++++++++++++++++ tox.ini | 10 +++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) create mode 100755 tools/tox_install.sh diff --git a/tools/tox_install.sh b/tools/tox_install.sh new file mode 100755 index 000000000..43468e450 --- /dev/null +++ b/tools/tox_install.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +# Client constraint file contains this client version pin that is in conflict +# with installing the client from source. We should remove the version pin in +# the constraints file before applying it for from-source installation. + +CONSTRAINTS_FILE=$1 +shift 1 + +set -e + +# NOTE(tonyb): Place this in the tox enviroment's log dir so it will get +# published to logs.openstack.org for easy debugging. +localfile="$VIRTUAL_ENV/log/upper-constraints.txt" + +if [[ $CONSTRAINTS_FILE != http* ]]; then + CONSTRAINTS_FILE=file://$CONSTRAINTS_FILE +fi +# NOTE(tonyb): need to add curl to bindep.txt if the project supports bindep +curl $CONSTRAINTS_FILE --insecure --progress-bar --output $localfile + +pip install -c$localfile openstack-requirements + +# This is the main purpose of the script: Allow local installation of +# the current repo. It is listed in constraints file and thus any +# install will be constrained and we need to unconstrain it. +edit-constraints $localfile -- $CLIENT_NAME + +pip install -c$localfile -U $* +exit $? diff --git a/tox.ini b/tox.ini index e2421549e..688c837a4 100644 --- a/tox.ini +++ b/tox.ini @@ -7,25 +7,29 @@ skipsdist = True usedevelop = True basepython = {env:SHADE_TOX_PYTHON:python2} passenv = UPPER_CONSTRAINTS_FILE -install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} +install_command = {toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} setenv = VIRTUAL_ENV={envdir} LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=C + BRANCH_NAME=master + CLIENT_NAME=shade deps = -r{toxinidir}/test-requirements.txt commands = python setup.py testr --slowest --testr-args='{posargs}' [testenv:functional] setenv = + {[testenv]setenv} OS_TEST_PATH = ./shade/tests/functional -passenv = OS_* SHADE_* +passenv = OS_* SHADE_* UPPER_CONSTRAINTS_FILE commands = python setup.py testr --slowest --testr-args='--concurrency=1 {posargs}' [testenv:functional-tips] setenv = + {[testenv]setenv} OS_TEST_PATH = ./shade/tests/functional -passenv = OS_* SHADE_* +passenv = {OS_* SHADE_* UPPER_CONSTRAINTS_FILE whitelist_externals = bash commands = bash -x {toxinidir}/extras/install-tips.sh From 8dc051ce3f0a885f2bfab748ba412e5b0239cc5d Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 1 Sep 2017 12:34:00 +0000 Subject: [PATCH 1772/3836] Updated from global requirements Change-Id: I2b75a24df0ad5779ac9b2f0a87453ffcc49b3dbe --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6935bc3fb..a6ac179e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ six>=1.9.0 # MIT futures>=3.0;python_version=='2.7' or python_version=='2.6' # BSD iso8601>=0.1.11 # MIT -keystoneauth1>=3.1.0 # Apache-2.0 +keystoneauth1>=3.2.0 # Apache-2.0 netifaces>=0.10.4 # MIT python-keystoneclient>=3.8.0 # Apache-2.0 python-ironicclient>=1.14.0 # Apache-2.0 From 371c3ebc0ee28c5e6c0d942c6efc8b16553b80db Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 1 Sep 2017 12:44:43 +0000 Subject: [PATCH 1773/3836] Updated from global requirements Change-Id: I7b1217cd50a3e3c89edcb5bda1d22cded90e9b4b --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8f0e21321..6c609105e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,5 @@ # process, which may cause wedges in the gate later. PyYAML>=3.10 # MIT appdirs>=1.3.0 # MIT License -keystoneauth1>=3.1.0 # Apache-2.0 +keystoneauth1>=3.2.0 # Apache-2.0 requestsexceptions>=1.2.0 # Apache-2.0 From 913cb48be1cb8a3be5d277e00ab4a8fa7a36ba8d Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 1 Sep 2017 12:47:16 +0000 Subject: [PATCH 1774/3836] Updated from global requirements Change-Id: Id527edfd5dd2ee8ad38857bca132d8a6fb483ac6 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f9aea340e..293ff6354 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,5 +6,5 @@ jsonpatch>=1.1 # BSD six>=1.9.0 # MIT stevedore>=1.20.0 # Apache-2.0 os-client-config>=1.28.0 # Apache-2.0 -keystoneauth1>=3.1.0 # Apache-2.0 +keystoneauth1>=3.2.0 # Apache-2.0 deprecation>=1.0 # Apache-2.0 From 469cc5ae49ef4cfcaee302b95ce81dccf9faa945 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 1 Sep 2017 08:13:22 -0500 Subject: [PATCH 1775/3836] Fix typo in tox.ini Change-Id: I320460d9226f3f4dba782047f83466c0dbbd7e56 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 688c837a4..f6fd3f783 100644 --- a/tox.ini +++ b/tox.ini @@ -29,7 +29,7 @@ commands = python setup.py testr --slowest --testr-args='--concurrency=1 {posarg setenv = {[testenv]setenv} OS_TEST_PATH = ./shade/tests/functional -passenv = {OS_* SHADE_* UPPER_CONSTRAINTS_FILE +passenv = OS_* SHADE_* UPPER_CONSTRAINTS_FILE whitelist_externals = bash commands = bash -x {toxinidir}/extras/install-tips.sh From 8b47d15fd5a3f67072e82ca8fc4928f57d9c96c8 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 14 Aug 2017 10:05:19 -0500 Subject: [PATCH 1776/3836] Use new keystoneauth version discovery We can remove a ton of our logic. We still need SOME because we have a different fallback strategy that isn't appropriate for keystoneauth generally. There are a couple of tests removed, since they were testing shade code that was removed. Also it's worth noting that there are tests in test_domain_params and test_image that are mocking the client rather than mocking the requests and we should fix those. Co-authored-by: Samuel de Medeiros Queiroz Change-Id: I78019717cdee79cab43b0d11e737327aa281fd03 --- .../version-discovery-a501c4e9e9869f77.yaml | 13 + shade/_adapter.py | 6 + shade/_legacy_clients.py | 6 +- shade/openstackcloud.py | 262 +++++++----------- .../fixtures/catalog-versioned-image.json | 71 ----- shade/tests/unit/test_domain_params.py | 31 ++- shade/tests/unit/test_image.py | 108 +++----- 7 files changed, 177 insertions(+), 320 deletions(-) create mode 100644 releasenotes/notes/version-discovery-a501c4e9e9869f77.yaml delete mode 100644 shade/tests/unit/fixtures/catalog-versioned-image.json diff --git a/releasenotes/notes/version-discovery-a501c4e9e9869f77.yaml b/releasenotes/notes/version-discovery-a501c4e9e9869f77.yaml new file mode 100644 index 000000000..c55792fe8 --- /dev/null +++ b/releasenotes/notes/version-discovery-a501c4e9e9869f77.yaml @@ -0,0 +1,13 @@ +--- +features: + - Version discovery is now done via the keystoneauth + library. shade still has one behavioral difference + from default keystoneauth behavior, which is that + shade will use a version it understands if it can + find one even if the user has requested a different + version. This change opens the door for shade to + start being able to consume API microversions as + needed. +upgrade: + - keystoneauth version 3.2.0 or higher is required + because of version discovery. diff --git a/shade/_adapter.py b/shade/_adapter.py index 6293c2533..bd88d6fd6 100644 --- a/shade/_adapter.py +++ b/shade/_adapter.py @@ -156,3 +156,9 @@ def main(self, client): return response else: return self._munch_response(response, error_message=error_message) + + def _version_matches(self, version): + api_version = self.get_api_major_version() + if api_version: + return api_version[0] == version + return False diff --git a/shade/_legacy_clients.py b/shade/_legacy_clients.py index c18eea1a4..b5ae535aa 100644 --- a/shade/_legacy_clients.py +++ b/shade/_legacy_clients.py @@ -104,10 +104,8 @@ def keystone_client(self): 'keystone', 'identity', client_class=client_class.Client, deprecated=False, - endpoint=self.cloud_config.config[ - 'identity_endpoint_override'], - endpoint_override=self.cloud_config.config[ - 'identity_endpoint_override']) + endpoint=self._identity_client.get_endpoint(), + endpoint_override=self._identity_client.get_endpoint()) # Set the ironic API microversion to a known-good # supported/tested with the contents of shade. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 4e8bd54d9..8f02b5e5a 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -17,6 +17,7 @@ import ipaddress import json import jsonpatch +import keystoneauth1.session import operator import os import os_client_config @@ -352,19 +353,108 @@ def _get_client( service=service_key)) return client - def _get_raw_client(self, service_key): - return _adapter.ShadeAdapter( + def _get_major_version_id(self, version): + if isinstance(version, int): + return version + elif isinstance(version, six.string_types + (tuple,)): + return int(version[0]) + return version + + def _get_versioned_client( + self, service_type, min_version=None, max_version=None): + config_version = self.cloud_config.get_api_version(service_type) + config_major = self._get_major_version_id(config_version) + max_major = self._get_major_version_id(max_version) + min_major = self._get_major_version_id(min_version) + # NOTE(mordred) The shade logic for versions is slightly different + # than the ksa Adapter constructor logic. shade knows the versions + # it knows, and uses them when it detects them. However, if a user + # requests a version, and it's not found, and a different one shade + # does know about it found, that's a warning in shade. + if config_version: + if min_major and config_major < min_major: + raise OpenStackCloudException( + "Version {config_version} requested for {service_type}" + " but shade understands a minimum of {min_version}".format( + config_version=config_version, + service_type=service_type, + min_version=min_version)) + elif max_major and config_major > max_major: + raise OpenStackCloudException( + "Version {config_version} requested for {service_type}" + " but shade understands a maximum of {max_version}".format( + config_version=config_version, + service_type=service_type, + max_version=max_version)) + request_min_version = config_version + request_max_version = '{version}.latest'.format( + version=config_major) + adapter = _adapter.ShadeAdapter( + session=self.keystone_session, + manager=self.manager, + service_type=self.cloud_config.get_service_type(service_type), + service_name=self.cloud_config.get_service_name(service_type), + interface=self.cloud_config.get_interface(service_type), + endpoint_override=self.cloud_config.get_endpoint(service_type), + region_name=self.cloud_config.region, + min_version=request_min_version, + max_version=request_max_version, + shade_logger=self.log) + if adapter.get_endpoint(): + return adapter + + adapter = _adapter.ShadeAdapter( + session=self.keystone_session, manager=self.manager, + service_type=self.cloud_config.get_service_type(service_type), + service_name=self.cloud_config.get_service_name(service_type), + interface=self.cloud_config.get_interface(service_type), + endpoint_override=self.cloud_config.get_endpoint(service_type), + region_name=self.cloud_config.region, + min_version=min_version, + max_version=max_version, + shade_logger=self.log) + + # data.api_version can be None if no version was detected, such + # as with neutron + api_version = adapter.get_api_major_version() + api_major = self._get_major_version_id(api_version) + + # If we detect a different version that was configured, warn the user. + # shade still knows what to do - but if the user gave us an explicit + # version and we couldn't find it, they may want to investigate. + if api_version and (api_major != config_major): + warning_msg = ( + '{service_type} is configured for {config_version}' + ' but only {api_version} is available. shade is happy' + ' with this version, but if you were trying to force an' + ' override, that did not happen. You may want to check' + ' your cloud, or remove the version specification from' + ' your config.'.format( + service_type=service_type, + config_version=config_version, + api_version='.'.join([str(f) for f in api_version]))) + self.log.debug(warning_msg) + warnings.warn(warning_msg) + return adapter + + def _get_raw_client( + self, service_type, api_version=None, endpoint_override=None): + return _adapter.ShadeAdapter( session=self.keystone_session, - service_type=self.cloud_config.get_service_type(service_key), - service_name=self.cloud_config.get_service_name(service_key), - interface=self.cloud_config.get_interface(service_key), + manager=self.manager, + service_type=self.cloud_config.get_service_type(service_type), + service_name=self.cloud_config.get_service_name(service_type), + interface=self.cloud_config.get_interface(service_type), + endpoint_override=self.cloud_config.get_endpoint( + service_type) or endpoint_override, region_name=self.cloud_config.region, shade_logger=self.log) def _is_client_version(self, client, version): - api_version = self.cloud_config.get_api_version(client) - return api_version.startswith(str(version)) + client_name = '_{client}_client'.format(client=client) + client = getattr(self, client_name) + return client._version_matches(version) @property def _application_catalog_client(self): @@ -408,23 +498,16 @@ def _database_client(self): @property def _dns_client(self): if 'dns' not in self._raw_clients: - dns_client = self._get_raw_client('dns') - dns_url = self._discover_endpoint( - 'dns', version_required=True) - dns_client.endpoint_override = dns_url + dns_client = self._get_versioned_client( + 'dns', min_version=2, max_version='2.latest') self._raw_clients['dns'] = dns_client return self._raw_clients['dns'] @property def _identity_client(self): if 'identity' not in self._raw_clients: - identity_client = self._get_raw_client('identity') - identity_url = self._discover_endpoint( - 'identity', version_required=True, versions=['3', '2']) - identity_client.endpoint_override = identity_url - self.cloud_config.config['identity_endpoint_override'] = \ - identity_url - self._raw_clients['identity'] = identity_client + self._raw_clients['identity'] = self._get_versioned_client( + 'identity', min_version=2, max_version='3.latest') return self._raw_clients['identity'] @property @@ -434,154 +517,19 @@ def _raw_image_client(self): self._raw_clients['raw-image'] = image_client return self._raw_clients['raw-image'] - def _get_version_and_base_from_endpoint(self, endpoint): - url, version = endpoint.rstrip('/').rsplit('/', 1) - if version.endswith(self.current_project_id): - url, version = endpoint.rstrip('/').rsplit('/', 1) - if version.startswith('v'): - return url, version[1] - return "/".join([url, version]), None - - def _get_version_from_endpoint(self, endpoint): - return self._get_version_and_base_from_endpoint(endpoint)[1] - - def _strip_version_from_endpoint(self, endpoint): - return self._get_version_and_base_from_endpoint(endpoint)[0] - - def _match_given_endpoint(self, given, version): - given_version = self._get_version_from_endpoint(given) - if given_version and version == given_version: - return True - return False - - def _discover_endpoint( - self, service_type, version_required=False, - versions=None): - # If endpoint_override is set, do nothing - service_endpoint = self.cloud_config.get_endpoint(service_type) - if service_endpoint: - return service_endpoint - - client = self._get_raw_client(service_type) - config_version = self.cloud_config.get_api_version(service_type) - - if versions and config_version[0] not in versions: - raise OpenStackCloudException( - "Version {version} was requested for service {service_type}" - " but shade only understands how to handle versions" - " {versions}".format( - version=config_version, - service_type=service_type, - versions=versions)) - - # shade only groks major versions at the moment - if config_version: - config_version = config_version[0] - - # First - quick check to see if the endpoint in the catalog - # is a versioned endpoint that matches the version we requested. - # If it is, don't do any additional work. - catalog_endpoint = client.get_endpoint() - if self._match_given_endpoint( - catalog_endpoint, config_version): - return catalog_endpoint - - if not versions and config_version: - versions = [config_version] - - candidate_endpoints = [] - version_key = '{service}_api_version'.format(service=service_type) - - # Next, try built-in keystoneauth discovery so that we can take - # advantage of the discovery cache - base_url = self._strip_version_from_endpoint(catalog_endpoint) - try: - discovery = self.keystone_session.auth.get_discovery( - self.keystone_session, base_url) - - version_list = discovery.version_data(reverse=True) - if config_version: - # If we have a specific version request, look for it first. - for version_data in version_list: - if str(version_data['version'][0]) == config_version: - candidate_endpoints.append(version_data) - - if not candidate_endpoints: - # If we didn't find anything, look again, this time either - # for the range, or just grab everything if we don't have - # a range - if str(version_data['version'][0]) in versions: - candidate_endpoints.append(version_data) - elif not config_version and not versions: - candidate_endpoints.append(version_data) - - except keystoneauth1.exceptions.DiscoveryFailure as e: - self.log.debug( - "Version discovery failed, assuming endpoint in" - " the catalog is already versioned. {e}".format(e=str(e))) - - if candidate_endpoints: - # If we got more than one, pick the highest - endpoint_description = candidate_endpoints[0] - service_endpoint = endpoint_description['url'] - api_version = str(endpoint_description['version'][0]) - else: - # Can't discover a version. Do best-attempt at inferring - # version from URL so that later logic can do its best - api_version = self._get_version_from_endpoint(catalog_endpoint) - if not api_version: - if not config_version and version_required: - raise OpenStackCloudException( - "No version for {service_type} could be detected," - " and also {version_key} is not set. It is impossible" - " to continue without knowing what version this" - " service is.") - if not config_version: - return catalog_endpoint - api_version = config_version - service_endpoint = catalog_endpoint - - # If we detect a different version that was configured, - # set the version in occ because we have logic elsewhere - # that is different depending on which version we're using - if config_version != api_version: - warning_msg = ( - '{version_key} is {config_version}' - ' but only {api_version} is available.'.format( - version_key=version_key, - config_version=config_version, - api_version=api_version)) - self.log.debug(warning_msg) - warnings.warn(warning_msg) - self.cloud_config.config[version_key] = api_version - - # Sometimes version discovery documents have broken endpoints, but - # the catalog has good ones (what?) - catalog_endpoint = urllib.parse.urlparse(client.get_endpoint()) - discovered_endpoint = urllib.parse.urlparse(service_endpoint) - - return urllib.parse.ParseResult( - catalog_endpoint.scheme, - catalog_endpoint.netloc, - discovered_endpoint.path, - discovered_endpoint.params, - discovered_endpoint.query, - discovered_endpoint.fragment).geturl() - @property def _image_client(self): if 'image' not in self._raw_clients: - image_client = self._get_raw_client('image') - image_url = self._discover_endpoint( - 'image', version_required=True, versions=['2', '1']) - image_client.endpoint_override = image_url - self._raw_clients['image'] = image_client + self._raw_clients['image'] = self._get_versioned_client( + 'image', min_version=1, max_version='2.latest') return self._raw_clients['image'] @property def _network_client(self): if 'network' not in self._raw_clients: client = self._get_raw_client('network') + # TODO(mordred) I don't care if this is what neutronclient does, + # fix this. # Don't bother with version discovery - there is only one version # of neutron. This is what neutronclient does, fwiw. endpoint = client.get_endpoint() diff --git a/shade/tests/unit/fixtures/catalog-versioned-image.json b/shade/tests/unit/fixtures/catalog-versioned-image.json deleted file mode 100644 index cb33dd5c4..000000000 --- a/shade/tests/unit/fixtures/catalog-versioned-image.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "token": { - "audit_ids": [ - "Rvn7eHkiSeOwucBIPaKdYA" - ], - "catalog": [ - { - "endpoints": [ - { - "id": "5a64de3c4a614d8d8f8d1ba3dee5f45f", - "interface": "public", - "region": "RegionOne", - "url": "https://image.example.com/v2" - } - ], - "name": "glance", - "type": "image" - }, - { - "endpoints": [ - { - "id": "4deb4d0504a044a395d4480741ba628c", - "interface": "public", - "region": "RegionOne", - "url": "https://identity.example.com" - }, - { - "id": "012322eeedcd459edabb4933021112bc", - "interface": "admin", - "region": "RegionOne", - "url": "https://identity.example.com" - } - ], - "endpoints_links": [], - "name": "keystone", - "type": "identity" - } - ], - "expires_at": "9999-12-31T23:59:59Z", - "issued_at": "2016-12-17T14:25:05.000000Z", - "methods": [ - "password" - ], - "project": { - "domain": { - "id": "default", - "name": "default" - }, - "id": "1c36b64c840a42cd9e9b931a369337f0", - "name": "Default Project" - }, - "roles": [ - { - "id": "9fe2ff9ee4384b1894a90878d3e92bab", - "name": "_member_" - }, - { - "id": "37071fc082e14c2284c32a2761f71c63", - "name": "swiftoperator" - } - ], - "user": { - "domain": { - "id": "default", - "name": "default" - }, - "id": "c17534835f8f42bf98fc367e0bf35e09", - "name": "mordred" - } - } -} diff --git a/shade/tests/unit/test_domain_params.py b/shade/tests/unit/test_domain_params.py index 1ff5586fc..62bfcc51f 100644 --- a/shade/tests/unit/test_domain_params.py +++ b/shade/tests/unit/test_domain_params.py @@ -11,7 +11,6 @@ # under the License. import mock -import os_client_config as occ import munch @@ -22,11 +21,12 @@ class TestDomainParams(base.TestCase): - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_is_client_version') @mock.patch.object(shade.OpenStackCloud, 'get_project') - def test_identity_params_v3(self, mock_get_project, mock_api_version): + def test_identity_params_v3(self, mock_get_project, + mock_is_client_version): mock_get_project.return_value = munch.Munch(id=1234) - mock_api_version.return_value = '3' + mock_is_client_version.return_value = True ret = self.cloud._get_identity_params(domain_id='5678', project='bar') self.assertIn('default_project_id', ret) @@ -34,39 +34,40 @@ def test_identity_params_v3(self, mock_get_project, mock_api_version): self.assertIn('domain_id', ret) self.assertEqual(ret['domain_id'], '5678') - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_is_client_version') @mock.patch.object(shade.OpenStackCloud, 'get_project') def test_identity_params_v3_no_domain( - self, mock_get_project, mock_api_version): + self, mock_get_project, mock_is_client_version): mock_get_project.return_value = munch.Munch(id=1234) - mock_api_version.return_value = '3' + mock_is_client_version.return_value = True self.assertRaises( exc.OpenStackCloudException, self.cloud._get_identity_params, domain_id=None, project='bar') - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_is_client_version') @mock.patch.object(shade.OpenStackCloud, 'get_project') - def test_identity_params_v2(self, mock_get_project, mock_api_version): + def test_identity_params_v2(self, mock_get_project, + mock_is_client_version): mock_get_project.return_value = munch.Munch(id=1234) - mock_api_version.return_value = '2' + mock_is_client_version.return_value = False ret = self.cloud._get_identity_params(domain_id='foo', project='bar') self.assertIn('tenant_id', ret) self.assertEqual(ret['tenant_id'], 1234) self.assertNotIn('domain', ret) - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_is_client_version') @mock.patch.object(shade.OpenStackCloud, 'get_project') def test_identity_params_v2_no_domain(self, mock_get_project, - mock_api_version): + mock_is_client_version): mock_get_project.return_value = munch.Munch(id=1234) - mock_api_version.return_value = '2' + mock_is_client_version.return_value = False ret = self.cloud._get_identity_params(domain_id=None, project='bar') - api_calls = [mock.call('identity'), mock.call('identity')] - mock_api_version.assert_has_calls(api_calls) + api_calls = [mock.call('identity', 3), mock.call('identity', 3)] + mock_is_client_version.assert_has_calls(api_calls) self.assertIn('tenant_id', ret) self.assertEqual(ret['tenant_id'], 1234) self.assertNotIn('domain', ret) diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index ba07d8918..f0be4b990 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -21,7 +21,6 @@ import mock import munch -import os_client_config as occ import six import shade @@ -385,10 +384,12 @@ def _call_create_image(self, name, **kwargs): name, imagefile.name, wait=True, timeout=1, is_public=False, **kwargs) - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_is_client_version') @mock.patch.object(shade.OpenStackCloud, '_image_client') - def test_create_image_put_v1(self, mock_image_client, mock_api_version): - mock_api_version.return_value = '1' + def test_create_image_put_v1( + self, mock_image_client, mock_is_client_version): + # TODO(mordred) Fix this to use requests_mock + mock_is_client_version.return_value = False mock_image_client.get.return_value = [] self.assertEqual([], self.cloud.list_images()) @@ -421,11 +422,11 @@ def test_create_image_put_v1(self, mock_image_client, mock_api_version): self.assertEqual( self._munch_images(ret), self.cloud.list_images()) - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_is_client_version') @mock.patch.object(shade.OpenStackCloud, '_image_client') def test_create_image_put_v1_bad_delete( - self, mock_image_client, mock_api_version): - mock_api_version.return_value = '1' + self, mock_image_client, mock_is_client_version): + mock_is_client_version.return_value = False mock_image_client.get.return_value = [] self.assertEqual([], self.cloud.list_images()) @@ -459,10 +460,11 @@ def test_create_image_put_v1_bad_delete( }) mock_image_client.delete.assert_called_with('/images/42') - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_is_client_version') @mock.patch.object(shade.OpenStackCloud, '_image_client') - def test_update_image_no_patch(self, mock_image_client, mock_api_version): - mock_api_version.return_value = '2' + def test_update_image_no_patch( + self, mock_image_client, mock_is_client_version): + mock_is_client_version.return_value = True self.cloud.image_api_use_tasks = False mock_image_client.get.return_value = [] @@ -489,11 +491,11 @@ def test_update_image_no_patch(self, mock_image_client, mock_api_version): mock_image_client.get.assert_called_with('/images', params={}) mock_image_client.patch.assert_not_called() - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_is_client_version') @mock.patch.object(shade.OpenStackCloud, '_image_client') def test_create_image_put_v2_bad_delete( - self, mock_image_client, mock_api_version): - mock_api_version.return_value = '2' + self, mock_image_client, mock_is_client_version): + mock_is_client_version.return_value = True self.cloud.image_api_use_tasks = False mock_image_client.get.return_value = [] @@ -528,11 +530,11 @@ def test_create_image_put_v2_bad_delete( data=mock.ANY) mock_image_client.delete.assert_called_with('/images/42') - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_is_client_version') @mock.patch.object(shade.OpenStackCloud, '_image_client') def test_create_image_put_bad_int( - self, mock_image_client, mock_api_version): - mock_api_version.return_value = '2' + self, mock_image_client, mock_is_client_version): + mock_is_client_version.return_value = True self.cloud.image_api_use_tasks = False self.assertRaises( @@ -540,11 +542,11 @@ def test_create_image_put_bad_int( self._call_create_image, '42 name', min_disk='fish', min_ram=0) mock_image_client.post.assert_not_called() - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_is_client_version') @mock.patch.object(shade.OpenStackCloud, '_image_client') def test_create_image_put_user_int( - self, mock_image_client, mock_api_version): - mock_api_version.return_value = '2' + self, mock_image_client, mock_is_client_version): + mock_is_client_version.return_value = True self.cloud.image_api_use_tasks = False args = {'name': '42 name', @@ -575,11 +577,11 @@ def test_create_image_put_user_int( self.assertEqual( self._munch_images(ret), self.cloud.list_images()) - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_is_client_version') @mock.patch.object(shade.OpenStackCloud, '_image_client') def test_create_image_put_meta_int( - self, mock_image_client, mock_api_version): - mock_api_version.return_value = '2' + self, mock_image_client, mock_is_client_version): + mock_is_client_version.return_value = True self.cloud.image_api_use_tasks = False mock_image_client.get.return_value = [] @@ -604,11 +606,11 @@ def test_create_image_put_meta_int( self.assertEqual( self._munch_images(ret), self.cloud.list_images()) - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_is_client_version') @mock.patch.object(shade.OpenStackCloud, '_image_client') def test_create_image_put_protected( - self, mock_image_client, mock_api_version): - mock_api_version.return_value = '2' + self, mock_image_client, mock_is_client_version): + mock_is_client_version.return_value = True self.cloud.image_api_use_tasks = False mock_image_client.get.return_value = [] @@ -642,11 +644,11 @@ def test_create_image_put_protected( headers={'Content-Type': 'application/octet-stream'}) self.assertEqual(self._munch_images(ret), self.cloud.list_images()) - @mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version') + @mock.patch.object(shade.OpenStackCloud, '_is_client_version') @mock.patch.object(shade.OpenStackCloud, '_image_client') def test_create_image_put_user_prop( - self, mock_image_client, mock_api_version): - mock_api_version.return_value = '2' + self, mock_image_client, mock_is_client_version): + mock_is_client_version.return_value = True self.cloud.image_api_use_tasks = False mock_image_client.get.return_value = [] @@ -687,8 +689,7 @@ def test_config_v1(self): self.assertEqual( 'https://image.example.com/v1/', self.cloud._image_client.get_endpoint()) - self.assertEqual( - '1', self.cloud_config.get_api_version('image')) + self.assertTrue(self.cloud._is_client_version('image', 1)) def test_config_v2(self): self.cloud.cloud_config.config['image_api_version'] = '2' @@ -697,8 +698,7 @@ def test_config_v2(self): self.assertEqual( 'https://image.example.com/v1/', self.cloud._image_client.get_endpoint()) - self.assertEqual( - '1', self.cloud_config.get_api_version('image')) + self.assertFalse(self.cloud._is_client_version('image', 2)) class TestImageV2Only(base.RequestsMockTestCase): @@ -714,8 +714,7 @@ def test_config_v1(self): self.assertEqual( 'https://image.example.com/v2/', self.cloud._image_client.get_endpoint()) - self.assertEqual( - '2', self.cloud_config.get_api_version('image')) + self.assertTrue(self.cloud._is_client_version('image', 2)) def test_config_v2(self): self.cloud.cloud_config.config['image_api_version'] = '2' @@ -724,26 +723,7 @@ def test_config_v2(self): self.assertEqual( 'https://image.example.com/v2/', self.cloud._image_client.get_endpoint()) - self.assertEqual( - '2', self.cloud_config.get_api_version('image')) - - -class TestImageVersionDiscovery(BaseTestImage): - - def test_version_discovery_skip(self): - self.cloud.cloud_config.config['image_endpoint_override'] = \ - 'https://image.example.com/v2/override' - - self.register_uris([ - dict(method='GET', - uri='https://image.example.com/v2/override/images', - json={'images': []}) - ]) - self.assertEqual([], self.cloud.list_images()) - self.assertEqual( - self.cloud._image_client.endpoint_override, - 'https://image.example.com/v2/override') - self.assert_calls() + self.assertTrue(self.cloud._is_client_version('image', 2)) class TestImageVolume(BaseTestImage): @@ -829,24 +809,6 @@ def test_url_fix(self): ]) self.assertEqual([], self.cloud.list_images()) self.assertEqual( - self.cloud._image_client.endpoint_override, + self.cloud._image_client.get_endpoint(), 'https://image.example.com/v2/') self.assert_calls() - - -class TestImageDiscoveryOptimization(base.RequestsMockTestCase): - - def setUp(self): - super(TestImageDiscoveryOptimization, self).setUp() - self.use_keystone_v3(catalog='catalog-versioned-image.json') - - def test_version_discovery_skip(self): - self.cloud.cloud_config.config['image_api_version'] = '2' - - self.register_uris([ - dict(method='GET', - uri='https://image.example.com/v2/images', - json={'images': []}) - ]) - self.assertEqual([], self.cloud.list_images()) - self.assert_calls() From ca0103a0a2e4f2772895f904b52e02cd6cd2ccc4 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Wed, 30 Aug 2017 16:56:06 -0300 Subject: [PATCH 1777/3836] De-client-ify User Update Change-Id: I2f0917eed4464944b4552972baca838be8e6866e --- shade/_tasks.py | 10 ---------- shade/openstackcloud.py | 32 +++++++++++++++++++------------- shade/tests/unit/test_caching.py | 4 ---- shade/tests/unit/test_users.py | 4 +--- 4 files changed, 20 insertions(+), 30 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 6c5e962dc..2334c032b 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -17,16 +17,6 @@ from shade import task_manager -class UserCreate(task_manager.Task): - def main(self, client): - return client.keystone_client.users.create(**self.args) - - -class UserUpdate(task_manager.Task): - def main(self, client): - return client.keystone_client.users.update(**self.args) - - class UserPasswordUpdate(task_manager.Task): def main(self, client): return client.keystone_client.users.update_password(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 9fd9be757..20ab4a0fd 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1046,13 +1046,11 @@ def update_user(self, name_or_id, **kwargs): if 'domain_id' in kwargs and kwargs['domain_id']: user_kwargs['domain_id'] = kwargs['domain_id'] user = self.get_user(name_or_id, **user_kwargs) - # normalized dict won't work - kwargs['user'] = self.get_user_by_id(user['id'], normalize=False) # TODO(mordred) When this changes to REST, force interface=admin # in the adapter call if it's an admin force call (and figure out how # to make that disctinction) - if not self._is_client_version('identity', 3): + if self._is_client_version('identity', 2): # Do not pass v3 args to a v2 keystone. kwargs.pop('domain_id', None) kwargs.pop('description', None) @@ -1062,17 +1060,25 @@ def update_user(self, name_or_id, **kwargs): with _utils.shade_exceptions( "Error updating password for {user}".format( user=name_or_id)): + # normalized dict won't work user = self.manager.submit_task(_tasks.UserPasswordUpdate( - user=kwargs['user'], password=password)) - elif 'domain_id' in kwargs: - # The incoming parameter is domain_id in order to match the - # parameter name in create_user(), but UserUpdate() needs it - # to be domain. - kwargs['domain'] = kwargs.pop('domain_id') - - with _utils.shade_exceptions("Error in updating user {user}".format( - user=name_or_id)): - user = self.manager.submit_task(_tasks.UserUpdate(**kwargs)) + user=self.get_user_by_id(user['id'], normalize=False), + password=password)) + + # Identity v2.0 implements PUT. v3 PATCH. Both work as PATCH. + data = self._identity_client.put( + '/users/{user}'.format(user=user['id']), json={'user': kwargs}, + error_message="Error in updating user {}".format(name_or_id)) + else: + # NOTE(samueldmq): now this is a REST call and domain_id is dropped + # if None. keystoneclient drops keys with None values. + if 'domain_id' in kwargs and kwargs['domain_id'] is None: + del kwargs['domain_id'] + data = self._identity_client.patch( + '/users/{user}'.format(user=user['id']), json={'user': kwargs}, + error_message="Error in updating user {}".format(name_or_id)) + + user = self._get_and_munchify('user', data) self.list_users.invalidate(self) return _utils.normalize_users([user])[0] diff --git a/shade/tests/unit/test_caching.py b/shade/tests/unit/test_caching.py index 11c01f5c1..97fe4bb10 100644 --- a/shade/tests/unit/test_caching.py +++ b/shade/tests/unit/test_caching.py @@ -376,12 +376,8 @@ def test_modify_user_invalidates_cache(self): # Get updated user dict(method='GET', uri=mock_users_url, status_code=200, json=users_list_resp), - dict(method='GET', uri=mock_user_resource_url, status_code=200, - json=user_data.json_response), dict(method='PUT', uri=mock_user_resource_url, status_code=200, json=new_resp, validate=dict(json=new_req)), - dict(method='GET', uri=mock_user_resource_url, status_code=200, - json=new_resp), # List Users Call dict(method='GET', uri=mock_users_url, status_code=200, json=updated_users_list_resp), diff --git a/shade/tests/unit/test_users.py b/shade/tests/unit/test_users.py index e1da45625..4923946a6 100644 --- a/shade/tests/unit/test_users.py +++ b/shade/tests/unit/test_users.py @@ -119,9 +119,7 @@ def test_update_user_password_v2(self): json=user_data.json_response), dict(method='PUT', uri=mock_user_resource_uri, status_code=200, json=user_data.json_response, - validate=dict(json={'user': {}})), - dict(method='GET', uri=mock_user_resource_uri, status_code=200, - json=user_data.json_response)]) + validate=dict(json={'user': {}}))]) user = self.op_cloud.update_user( user_data.user_id, password=user_data.password) From 149a9fbc7841d70b9f9db1ab109eb3060a12bfff Mon Sep 17 00:00:00 2001 From: Rosario Di Somma Date: Thu, 17 Aug 2017 12:58:03 +0000 Subject: [PATCH 1778/3836] Use direct calls to get__by_id This commit adds a `use_direct_get` flag to the OpenStackCloud object. The goal is to enable direct calls to the get__by_id methods when a UUID is passed and default to the `list` and `search` calls otherwise. Change-Id: I6aebfe7cb40adace0568d8f131e64d6555736712 Signed-off-by: Rosario Di Somma --- shade/_utils.py | 63 +++++++++++++++++++++++++++------ shade/inventory.py | 5 +-- shade/openstackcloud.py | 48 ++++++++++++------------- shade/operatorcloud.py | 13 ++++--- shade/tests/unit/test__utils.py | 60 +++++++++++++++++++++++++++++++ 5 files changed, 145 insertions(+), 44 deletions(-) diff --git a/shade/_utils.py b/shade/_utils.py index bd57a3fdf..37939e5b2 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -23,6 +23,7 @@ import sre_constants import sys import time +import uuid from decorator import decorator @@ -196,12 +197,15 @@ def _dict_filter(f, d): return filtered -def _get_entity(func, name_or_id, filters, **kwargs): +def _get_entity(cloud, resource, name_or_id, filters, **kwargs): """Return a single entity from the list returned by a given method. - :param callable func: - A function that takes `name_or_id` and `filters` as parameters - and returns a list of entities to filter. + :param object cloud: + The controller class (Example: the main OpenStackCloud object) . + :param string or callable resource: + The string that identifies the resource to use to lookup the + get_<>_by_id or search_s methods(Example: network) + or a callable to invoke. :param string name_or_id: The name or ID of the entity being filtered or a dict :param filters: @@ -210,20 +214,33 @@ def _get_entity(func, name_or_id, filters, **kwargs): A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" """ + # Sometimes in the control flow of shade, we already have an object # fetched. Rather than then needing to pull the name or id out of that # object, pass it in here and rely on caching to prevent us from making # an additional call, it's simple enough to test to see if we got an # object and just short-circuit return it. + if hasattr(name_or_id, 'id'): return name_or_id - entities = func(name_or_id, filters, **kwargs) - if not entities: - return None - if len(entities) > 1: - raise exc.OpenStackCloudException( - "Multiple matches found for %s" % name_or_id) - return entities[0] + + # If a uuid is passed short-circuit it calling the + # get__by_id method + if getattr(cloud, 'use_direct_get', False) and _is_uuid_like(name_or_id): + get_resource = getattr(cloud, 'get_%s_by_id' % resource, None) + if get_resource: + return get_resource(name_or_id) + + search = resource if callable(resource) else getattr( + cloud, 'search_%ss' % resource, None) + if search: + entities = search(name_or_id, filters, **kwargs) + if entities: + if len(entities) > 1: + raise exc.OpenStackCloudException( + "Multiple matches found for %s" % name_or_id) + return entities[0] + return None def normalize_keystone_services(services): @@ -670,3 +687,27 @@ def read(self, size=-1): def reset(self): self._file.seek(self.offset, 0) + + +def _format_uuid_string(string): + return (string.replace('urn:', '') + .replace('uuid:', '') + .strip('{}') + .replace('-', '') + .lower()) + + +def _is_uuid_like(val): + """Returns validation of a value as a UUID. + + :param val: Value to verify + :type val: string + :returns: bool + + .. versionchanged:: 1.1.1 + Support non-lowercase UUIDs. + """ + try: + return str(uuid.UUID(val)).replace('-', '') == _format_uuid_string(val) + except (TypeError, ValueError, AttributeError): + return False diff --git a/shade/inventory.py b/shade/inventory.py index 101682a18..2490e93bf 100644 --- a/shade/inventory.py +++ b/shade/inventory.py @@ -27,7 +27,8 @@ class OpenStackInventory(object): def __init__( self, config_files=None, refresh=False, private=False, - config_key=None, config_defaults=None, cloud=None): + config_key=None, config_defaults=None, cloud=None, + use_direct_get=False): if config_files is None: config_files = [] config = os_client_config.config.OpenStackConfig( @@ -82,4 +83,4 @@ def get_host(self, name_or_id, filters=None, expand=True): func = self.search_hosts else: func = functools.partial(self.search_hosts, expand=False) - return _utils._get_entity(func, name_or_id, filters) + return _utils._get_entity(self, func, name_or_id, filters) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 211768239..53e55a624 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1,4 +1,4 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); +# Licensed under the Apache License, Version 3.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # @@ -138,6 +138,7 @@ def __init__( strict=False, app_name=None, app_version=None, + use_direct_get=False, **kwargs): if log_inner_exceptions: @@ -211,6 +212,7 @@ def __init__( warnings.filterwarnings('ignore', category=category) self._disable_warnings = {} + self.use_direct_get = use_direct_get self._servers = None self._servers_time = 0 @@ -835,7 +837,7 @@ def get_project(self, name_or_id, filters=None, domain_id=None): :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ - return _utils._get_entity(self.search_projects, name_or_id, filters, + return _utils._get_entity(self, 'project', name_or_id, filters, domain_id=domain_id) @_utils.valid_kwargs('description') @@ -964,8 +966,7 @@ def get_user(self, name_or_id, filters=None, **kwargs): :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ - return _utils._get_entity(self.search_users, name_or_id, filters, - **kwargs) + return _utils._get_entity(self, 'user', name_or_id, filters, **kwargs) def get_user_by_id(self, user_id, normalize=True): """Get a user by ID. @@ -2599,7 +2600,7 @@ def get_keypair(self, name_or_id, filters=None): :returns: A keypair ``munch.Munch`` or None if no matching keypair is found. """ - return _utils._get_entity(self.search_keypairs, name_or_id, filters) + return _utils._get_entity(self, 'keypair', name_or_id, filters) def get_network(self, name_or_id, filters=None): """Get a network by name or ID. @@ -2624,7 +2625,7 @@ def get_network(self, name_or_id, filters=None): found. """ - return _utils._get_entity(self.search_networks, name_or_id, filters) + return _utils._get_entity(self, 'network', name_or_id, filters) def get_network_by_id(self, id): """ Get a network by ID @@ -2663,7 +2664,7 @@ def get_router(self, name_or_id, filters=None): found. """ - return _utils._get_entity(self.search_routers, name_or_id, filters) + return _utils._get_entity(self, 'router', name_or_id, filters) def get_subnet(self, name_or_id, filters=None): """Get a subnet by name or ID. @@ -2684,7 +2685,7 @@ def get_subnet(self, name_or_id, filters=None): found. """ - return _utils._get_entity(self.search_subnets, name_or_id, filters) + return _utils._get_entity(self, 'subnet', name_or_id, filters) def get_subnet_by_id(self, id): """ Get a subnet by ID @@ -2722,7 +2723,7 @@ def get_port(self, name_or_id, filters=None): :returns: A port ``munch.Munch`` or None if no matching port is found. """ - return _utils._get_entity(self.search_ports, name_or_id, filters) + return _utils._get_entity(self, 'port', name_or_id, filters) def get_port_by_id(self, id): """ Get a port by ID @@ -2762,7 +2763,7 @@ def get_qos_policy(self, name_or_id, filters=None): """ return _utils._get_entity( - self.search_qos_policies, name_or_id, filters) + self, 'qos_policie', name_or_id, filters) def get_volume(self, name_or_id, filters=None): """Get a volume by name or ID. @@ -2787,7 +2788,7 @@ def get_volume(self, name_or_id, filters=None): found. """ - return _utils._get_entity(self.search_volumes, name_or_id, filters) + return _utils._get_entity(self, 'volume', name_or_id, filters) def get_volume_by_id(self, id): """ Get a volume by ID @@ -2828,7 +2829,7 @@ def get_volume_type(self, name_or_id, filters=None): """ return _utils._get_entity( - self.search_volume_types, name_or_id, filters) + self, 'volume_type', name_or_id, filters) def get_flavor(self, name_or_id, filters=None, get_extra=True): """Get a flavor by name or ID. @@ -2858,7 +2859,7 @@ def get_flavor(self, name_or_id, filters=None, get_extra=True): """ search_func = functools.partial( self.search_flavors, get_extra=get_extra) - return _utils._get_entity(search_func, name_or_id, filters) + return _utils._get_entity(self, search_func, name_or_id, filters) def get_flavor_by_id(self, id, get_extra=True): """ Get a flavor by ID @@ -2920,7 +2921,7 @@ def get_security_group(self, name_or_id, filters=None): """ return _utils._get_entity( - self.search_security_groups, name_or_id, filters) + self, 'security_group', name_or_id, filters) def get_security_group_by_id(self, id): """ Get a security group by ID @@ -3009,7 +3010,7 @@ def get_server( """ searchfunc = functools.partial(self.search_servers, detailed=detailed, bare=True) - server = _utils._get_entity(searchfunc, name_or_id, filters) + server = _utils._get_entity(self, searchfunc, name_or_id, filters) return self._expand_server(server, detailed, bare) def _expand_server(self, server, detailed, bare): @@ -3045,7 +3046,7 @@ def get_server_group(self, name_or_id=None, filters=None): is found. """ - return _utils._get_entity(self.search_server_groups, name_or_id, + return _utils._get_entity(self, 'server_group', name_or_id, filters) def get_image(self, name_or_id, filters=None): @@ -3071,7 +3072,7 @@ def get_image(self, name_or_id, filters=None): is found """ - return _utils._get_entity(self.search_images, name_or_id, filters) + return _utils._get_entity(self, 'image', name_or_id, filters) def get_image_by_id(self, id): """ Get a image by ID @@ -3162,7 +3163,7 @@ def get_floating_ip(self, id, filters=None): IP is found. """ - return _utils._get_entity(self.search_floating_ips, id, filters) + return _utils._get_entity(self, 'floating_ip', id, filters) def get_floating_ip_by_id(self, id): """ Get a floating ip by ID @@ -3215,7 +3216,7 @@ def _search_one_stack(name_or_id=None, filters=None): return _utils._filter_list([stack], name_or_id, filters) return _utils._get_entity( - _search_one_stack, name_or_id, filters) + self, _search_one_stack, name_or_id, filters) def create_keypair(self, name, public_key=None): """Create a new keypair. @@ -5177,7 +5178,7 @@ def get_volume_snapshot(self, name_or_id, filters=None): :returns: A volume ``munch.Munch`` or None if no matching volume is found. """ - return _utils._get_entity(self.search_volume_snapshots, name_or_id, + return _utils._get_entity(self, 'volume_snapshot', name_or_id, filters) def create_volume_backup(self, volume_id, name=None, description=None, @@ -5236,7 +5237,7 @@ def get_volume_backup(self, name_or_id, filters=None): :returns: A backup ``munch.Munch`` or None if no matching backup is found. """ - return _utils._get_entity(self.search_volume_backups, name_or_id, + return _utils._get_entity(self, 'volume_backup', name_or_id, filters) def list_volume_snapshots(self, detailed=True, search_opts=None): @@ -6482,7 +6483,6 @@ def create_server( :raises: OpenStackCloudException on operation error. """ # TODO(mordred) Add support for description starting in 2.19 - security_groups = kwargs.get('security_groups', []) if security_groups and not isinstance(kwargs['security_groups'], list): security_groups = [security_groups] @@ -8163,7 +8163,7 @@ def get_zone(self, name_or_id, filters=None): :returns: A zone dict or None if no matching zone is found. """ - return _utils._get_entity(self.search_zones, name_or_id, filters) + return _utils._get_entity(self, 'zone', name_or_id, filters) def search_zones(self, name_or_id=None, filters=None): zones = self.list_zones() @@ -8455,7 +8455,7 @@ def get_cluster_template(self, name_or_id, filters=None, detail=False): :returns: A cluster template dict or None if no matching cluster template is found. """ - return _utils._get_entity(self.search_cluster_templates, name_or_id, + return _utils._get_entity(self, 'cluster_template', name_or_id, filters=filters, detail=detail) get_baymodel = get_cluster_template diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 1d74ee32d..6fac151d4 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -849,7 +849,7 @@ def get_service(self, name_or_id, filters=None): :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call or if multiple matches are found. """ - return _utils._get_entity(self.search_services, name_or_id, filters) + return _utils._get_entity(self, 'service', name_or_id, filters) def delete_service(self, name_or_id): """Delete a Keystone service. @@ -1057,7 +1057,7 @@ def get_endpoint(self, id, filters=None): - internal_url: (optional) - admin_url: (optional) """ - return _utils._get_entity(self.search_endpoints, id, filters) + return _utils._get_entity(self, 'endpoint', id, filters) def delete_endpoint(self, id): """Delete a Keystone endpoint. @@ -1226,7 +1226,7 @@ def get_domain(self, domain_id=None, name_or_id=None, filters=None): # duplicate that logic here if hasattr(name_or_id, 'id'): return name_or_id - return _utils._get_entity(self.search_domains, filters, name_or_id) + return _utils._get_entity(self, 'domain', filters, name_or_id) else: error_msg = 'Failed to get domain {id}'.format(id=domain_id) data = self._identity_client.get( @@ -1281,8 +1281,7 @@ def get_group(self, name_or_id, filters=None, **kwargs): :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ - return _utils._get_entity(self.search_groups, name_or_id, filters, - **kwargs) + return _utils._get_entity(self, 'group', name_or_id, filters, **kwargs) def create_group(self, name, description, domain=None): """Create a group. @@ -1424,7 +1423,7 @@ def get_role(self, name_or_id, filters=None): :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ - return _utils._get_entity(self.search_roles, name_or_id, filters) + return _utils._get_entity(self, 'role', name_or_id, filters) def _keystone_v2_role_assignments(self, user, project=None, role=None, **kwargs): @@ -1900,7 +1899,7 @@ def get_aggregate(self, name_or_id, filters=None): found. """ - return _utils._get_entity(self.search_aggregates, name_or_id, filters) + return _utils._get_entity(self, 'aggregate', name_or_id, filters) def create_aggregate(self, name, availability_zone=None): """Create a new host aggregate. diff --git a/shade/tests/unit/test__utils.py b/shade/tests/unit/test__utils.py index 24da072f2..1c453f426 100644 --- a/shade/tests/unit/test__utils.py +++ b/shade/tests/unit/test__utils.py @@ -15,7 +15,9 @@ import random import string import tempfile +from uuid import uuid4 +import mock import testtools from shade import _utils @@ -318,3 +320,61 @@ def test_file_segment(self): name) segment_content += segment.read() self.assertEqual(content, segment_content) + + def test_get_entity_pass_object(self): + obj = mock.Mock(id=uuid4().hex) + self.cloud.use_direct_get = True + self.assertEqual(obj, _utils._get_entity(self.cloud, '', obj, {})) + + def test_get_entity_no_use_direct_get(self): + # test we are defaulting to the search_ methods + # if the use_direct_get flag is set to False(default). + uuid = uuid4().hex + resource = 'network' + func = 'search_%ss' % resource + filters = {} + with mock.patch.object(self.cloud, func) as search: + _utils._get_entity(self.cloud, resource, uuid, filters) + search.assert_called_once_with(uuid, filters) + + def test_get_entity_no_uuid_like(self): + # test we are defaulting to the search_ methods + # if the name_or_id param is a name(string) but not a uuid. + self.cloud.use_direct_get = True + name = 'name_no_uuid' + resource = 'network' + func = 'search_%ss' % resource + filters = {} + with mock.patch.object(self.cloud, func) as search: + _utils._get_entity(self.cloud, resource, name, filters) + search.assert_called_once_with(name, filters) + + def test_get_entity_pass_uuid(self): + uuid = uuid4().hex + self.cloud.use_direct_get = True + resources = ['flavor', 'image', 'volume', 'network', + 'subnet', 'port', 'floating_ip', 'security_group'] + for r in resources: + f = 'get_%s_by_id' % r + with mock.patch.object(self.cloud, f) as get: + _utils._get_entity(self.cloud, r, uuid, {}) + get.assert_called_once_with(uuid) + + def test_get_entity_pass_search_methods(self): + self.cloud.use_direct_get = True + resources = ['flavor', 'image', 'volume', 'network', + 'subnet', 'port', 'floating_ip', 'security_group'] + filters = {} + name = 'name_no_uuid' + for r in resources: + f = 'search_%ss' % r + with mock.patch.object(self.cloud, f) as search: + _utils._get_entity(self.cloud, r, name, {}) + search.assert_called_once_with(name, filters) + + def test_get_entity_get_and_search(self): + resources = ['flavor', 'image', 'volume', 'network', + 'subnet', 'port', 'floating_ip', 'security_group'] + for r in resources: + self.assertTrue(hasattr(self.cloud, 'get_%s_by_id' % r)) + self.assertTrue(hasattr(self.cloud, 'search_%ss' % r)) From b82a659f7958d39de62e34558987086e6f02bebf Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Wed, 30 Aug 2017 18:29:15 -0300 Subject: [PATCH 1779/3836] De-client-ify Add User to Group Change-Id: If80267260c6db0440b36ac2e8ce74691eb1948da --- shade/_tasks.py | 5 ----- shade/openstackcloud.py | 12 +++++------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 2334c032b..a7f880767 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -22,11 +22,6 @@ def main(self, client): return client.keystone_client.users.update_password(**self.args) -class UserAddToGroup(task_manager.Task): - def main(self, client): - return client.keystone_client.users.add_to_group(**self.args) - - class UserCheckInGroup(task_manager.Task): def main(self, client): return client.keystone_client.users.check_in_group(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 450e113fe..8cad97446 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1096,13 +1096,11 @@ def add_user_to_group(self, name_or_id, group_name_or_id): """ user, group = self._get_user_and_group(name_or_id, group_name_or_id) - with _utils.shade_exceptions( - "Error adding user {user} to group {group}".format( - user=name_or_id, group=group_name_or_id) - ): - self.manager.submit_task( - _tasks.UserAddToGroup(user=user['id'], group=group['id']) - ) + error_msg = "Error adding user {user} to group {group}".format( + user=name_or_id, group=group_name_or_id) + self._identity_client.put( + '/groups/{g}/users/{u}'.format(g=group['id'], u=user['id']), + error_message=error_msg) def is_user_in_group(self, name_or_id, group_name_or_id): """Check to see if a user is in a group. From 30e0fbcfd77286fa25b50061637976cd94c2710f Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Wed, 30 Aug 2017 18:46:01 -0300 Subject: [PATCH 1780/3836] De-client-ify Check User in Group Change-Id: Ia4ff42cf25b3ccad54b4870cde5a5a7e7c6acad6 --- shade/_tasks.py | 5 ----- shade/openstackcloud.py | 14 ++++++-------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index a7f880767..1025ab7b7 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -22,11 +22,6 @@ def main(self, client): return client.keystone_client.users.update_password(**self.args) -class UserCheckInGroup(task_manager.Task): - def main(self, client): - return client.keystone_client.users.check_in_group(**self.args) - - class UserRemoveFromGroup(task_manager.Task): def main(self, client): return client.keystone_client.users.remove_from_group(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 8cad97446..3785b654f 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1116,15 +1116,13 @@ def is_user_in_group(self, name_or_id, group_name_or_id): user, group = self._get_user_and_group(name_or_id, group_name_or_id) try: - return self.manager.submit_task( - _tasks.UserCheckInGroup(user=user['id'], group=group['id']) - ) - except keystoneauth1.exceptions.http.NotFound: - # Because the keystone API returns either True or raises an - # exception, which is awesome. + self._identity_client.head( + '/groups/{g}/users/{u}'.format(g=group['id'], u=user['id'])) + return True + except OpenStackCloudURINotFound: + # NOTE(samueldmq): knowing this URI exists, let's interpret this as + # user not found in group rather than URI not found. return False - except OpenStackCloudException: - raise except Exception as e: raise OpenStackCloudException( "Error adding user {user} to group {group}: {err}".format( From be9d046925ecc204bef5a69212b123ce9c3a1039 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Tue, 5 Sep 2017 00:22:10 +0000 Subject: [PATCH 1781/3836] Correct baremetal fake data model In preparation for rewriting tests to use mock_requests, we should need to correct the object naming to match what is defined by the baremetal API. Change-Id: I55c4c895e9fd1f09992bcc9059b7ca132af977cf --- shade/tests/fakes.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index f2f4d1549..9ff4c8457 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -347,7 +347,7 @@ class FakeMachine(object): def __init__(self, id, name=None, driver=None, driver_info=None, chassis_uuid=None, instance_info=None, instance_uuid=None, properties=None): - self.id = id + self.uuid = id self.name = name self.driver = driver self.driver_info = driver_info @@ -359,10 +359,9 @@ def __init__(self, id, name=None, driver=None, driver_info=None, class FakeMachinePort(object): def __init__(self, id, address, node_id): - self.id = id + self.uuid = id self.address = address - self.node_id = node_id - + self.node_uuid = node_id def make_fake_neutron_security_group( id, name, description, rules, project_id=None): From e42192e8393f01e825a8b8f5628f7813ae49c4bb Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Wed, 30 Aug 2017 18:50:42 -0300 Subject: [PATCH 1782/3836] De-client-ify Remove User from Group Change-Id: Ica1cc5e472cfc14f9afeb05db5fa82cfd8ece500 --- shade/_tasks.py | 5 ----- shade/openstackcloud.py | 12 +++++------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 1025ab7b7..b6b319048 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -22,11 +22,6 @@ def main(self, client): return client.keystone_client.users.update_password(**self.args) -class UserRemoveFromGroup(task_manager.Task): - def main(self, client): - return client.keystone_client.users.remove_from_group(**self.args) - - class MachineCreate(task_manager.Task): def main(self, client): return client.ironic_client.node.create(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 3785b654f..64651d318 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1140,13 +1140,11 @@ def remove_user_from_group(self, name_or_id, group_name_or_id): """ user, group = self._get_user_and_group(name_or_id, group_name_or_id) - with _utils.shade_exceptions( - "Error removing user {user} from group {group}".format( - user=name_or_id, group=group_name_or_id) - ): - self.manager.submit_task( - _tasks.UserRemoveFromGroup(user=user['id'], group=group['id']) - ) + error_msg = "Error removing user {user} from group {group}".format( + user=name_or_id, group=group_name_or_id) + self._identity_client.delete( + '/groups/{g}/users/{u}'.format(g=group['id'], u=user['id']), + error_message=error_msg) def get_template_contents( self, template_file=None, template_url=None, From e76896046dcafe3832beb19927621225e9bdecb9 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Mon, 4 Sep 2017 11:01:27 -0300 Subject: [PATCH 1783/3836] Remove improper exc handling in is_user_in_group Since we are already catching the expected exceptions for that operation there is no need to catch Exception in that method. Also, the message was saying "error in adding user to group", but that method is just checking ... Change-Id: I3b6fb28773a6c52b223bca4964dba4ccc27f9787 --- shade/openstackcloud.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 64651d318..93a5595aa 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1123,11 +1123,6 @@ def is_user_in_group(self, name_or_id, group_name_or_id): # NOTE(samueldmq): knowing this URI exists, let's interpret this as # user not found in group rather than URI not found. return False - except Exception as e: - raise OpenStackCloudException( - "Error adding user {user} to group {group}: {err}".format( - user=name_or_id, group=group_name_or_id, err=str(e)) - ) def remove_user_from_group(self, name_or_id, group_name_or_id): """Remove a user from a group. From 4de4fb4a5aeb25e05f85d4696d60cd2794084348 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 5 Sep 2017 09:46:41 -0500 Subject: [PATCH 1784/3836] Begin converting baremetal node tests Shamelessly based upon pre-existing requests mock code in shade, with the attempt to replace the existing baremetal client mock testing. This updates the way we're creating the ironic_client so that it always does discovery, which _baremetal_client will always have to do because of how the versioned discovery document is in ironic. It also puts in a workaround for a bug that was found in keystoneauth's new version discovery that we should eventually take out. Change-Id: I0e811d69c3cba50f164cb2a3a1302b88fba51308 --- shade/_legacy_clients.py | 41 ++++++++++++++ shade/openstackcloud.py | 15 +++-- shade/tests/unit/base.py | 29 ++++++++++ shade/tests/unit/fixtures/baremetal.json | 30 ++++++++++ shade/tests/unit/test_baremetal_node.py | 59 ++++++++++++++++++++ shade/tests/unit/test_operator_noauth.py | 71 ++++++++++++++---------- shade/tests/unit/test_shade_operator.py | 20 ------- 7 files changed, 211 insertions(+), 54 deletions(-) create mode 100644 shade/tests/unit/fixtures/baremetal.json create mode 100644 shade/tests/unit/test_baremetal_node.py diff --git a/shade/_legacy_clients.py b/shade/_legacy_clients.py index b5ae535aa..e67b35d3d 100644 --- a/shade/_legacy_clients.py +++ b/shade/_legacy_clients.py @@ -12,9 +12,11 @@ import importlib import warnings +from keystoneauth1 import plugin from os_client_config import constructors from shade import _utils +from shade import exc class LegacyClientFactoryMixin(object): @@ -144,8 +146,47 @@ def ironic_api_microversion(self): def _get_legacy_ironic_microversion(self): return '1.6' + def _join_ksa_version(self, version): + return ".".join([str(x) for x in version]) + @property def ironic_client(self): + # Trigger discovery from ksa. This will make ironicclient and + # keystoneauth1.adapter.Adapter code paths both go through discovery. + # ironicclient does its own magic with discovery, so we won't + # pass an endpoint_override here like we do for keystoneclient. + # Just so it's not wasted though, make sure we can handle the + # min microversion we need. + needed = self._get_legacy_ironic_microversion() + + # TODO(mordred) Bug in ksa - don't do microversion matching for + # auth_type = admin_token. Remove this if when the fix lands. + if (hasattr(plugin.BaseAuthPlugin, 'get_endpoint_data') or + self.cloud_config.config['auth_type'] not in ( + 'admin_token', 'none')): + # TODO(mordred) once we're on REST properly, we need a better + # method for matching requested and available microversion + endpoint_data = self._baremetal_client.get_endpoint_data() + if not endpoint_data.min_microversion: + raise exc.OpenStackCloudException( + "shade needs an ironic that supports microversions") + if endpoint_data.min_microversion[1] > int(needed[-1]): + raise exc.OpenStackCloudException( + "shade needs an ironic that supports microversion {needed}" + " but the ironic found has a minimum microversion" + " of {found}".format( + needed=needed, + found=self._join_ksa_version( + endpoint_data.min_microversion))) + if endpoint_data.max_microversion[1] < int(needed[-1]): + raise exc.OpenStackCloudException( + "shade needs an ironic that supports microversion {needed}" + " but the ironic found has a maximum microversion" + " of {found}".format( + needed=needed, + found=self._join_ksa_version( + endpoint_data.max_microversion))) + return self._create_legacy_client( 'ironic', 'baremetal', deprecated=False, module_name='ironicclient.client.Client', diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 3785b654f..d711ec71c 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -418,7 +418,8 @@ def _get_versioned_client( # data.api_version can be None if no version was detected, such # as with neutron - api_version = adapter.get_api_major_version() + api_version = adapter.get_api_major_version( + endpoint_override=self.cloud_config.get_endpoint(service_type)) api_major = self._get_major_version_id(api_version) # If we detect a different version that was configured, warn the user. @@ -468,11 +469,13 @@ def _application_catalog_client(self): def _baremetal_client(self): if 'baremetal' not in self._raw_clients: client = self._get_raw_client('baremetal') - # TODO(mordred) Fix this once we've migrated all the way to REST - # Don't bother with version discovery - there is only one version - # of ironic. This is what ironicclient does, fwiw. - client.endpoint_override = urllib.parse.urljoin( - client.get_endpoint(), 'v1') + # Do this to force version discovery. We need to do that, because + # the endpoint-override trick we do for neutron because + # ironicclient just appends a /v1 won't work and will break + # keystoneauth - because ironic's versioned discovery endpoint + # is non-compliant and doesn't return an actual version dict. + client = self._get_versioned_client( + 'baremetal', min_version=1, max_version='1.latest') self._raw_clients['baremetal'] = client return self._raw_clients['baremetal'] diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index bbbcab7b0..be24b1731 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -466,6 +466,12 @@ def get_designate_discovery_mock_dict(self): return dict(method='GET', uri="https://dns.example.com/", text=open(discovery_fixture, 'r').read()) + def get_ironic_discovery_mock_dict(self): + discovery_fixture = os.path.join( + self.fixtures_directory, "baremetal.json") + return dict(method='GET', uri="https://bare-metal.example.com/", + text=open(discovery_fixture, 'r').read()) + def use_glance(self, image_version_json='image-version.json'): # NOTE(notmorgan): This method is only meant to be used in "setUp" # where the ordering of the url being registered is tightly controlled @@ -484,6 +490,15 @@ def use_designate(self): self.__do_register_uris([ self.get_designate_discovery_mock_dict()]) + def use_ironic(self): + # NOTE(TheJulia): This method is only meant to be used in "setUp" + # where the ordering of the url being registered is tightly controlled + # if the functionality of .use_ironic is meant to be used during an + # actual test case, use .get_ironic_discovery_mock and apply to the + # right location in the mock_uris when calling .register_uris + self.__do_register_uris([ + self.get_ironic_discovery_mock_dict()]) + def register_uris(self, uri_mock_list=None): """Mock a list of URIs and responses via requests mock. @@ -613,3 +628,17 @@ def assert_calls(self, stop_after=None, do_count=True): if do_count: self.assertEqual( len(self.calls), len(self.adapter.request_history)) + + +class IronicTestCase(RequestsMockTestCase): + + def setUp(self): + super(IronicTestCase, self).setUp() + self.use_ironic() + self.uuid = str(uuid.uuid4()) + self.name = self.getUniqueString('name') + + def get_mock_url(self, resource=None, append=None, qs_elements=None): + return super(IronicTestCase, self).get_mock_url( + service_type='baremetal', interface='public', resource=resource, + append=append, base_url_append='v1', qs_elements=qs_elements) diff --git a/shade/tests/unit/fixtures/baremetal.json b/shade/tests/unit/fixtures/baremetal.json new file mode 100644 index 000000000..fa0a9e7a7 --- /dev/null +++ b/shade/tests/unit/fixtures/baremetal.json @@ -0,0 +1,30 @@ +{ + "default_version": { + "id": "v1", + "links": [ + { + "href": "https://bare-metal.example.com/v1/", + "rel": "self" + } + ], + "min_version": "1.1", + "status": "CURRENT", + "version": "1.33" + }, + "description": "Ironic is an OpenStack project which aims to provision baremetal machines.", + "name": "OpenStack Ironic API", + "versions": [ + { + "id": "v1", + "links": [ + { + "href": "https://bare-metal.example.com/v1/", + "rel": "self" + } + ], + "min_version": "1.1", + "status": "CURRENT", + "version": "1.33" + } + ] +} diff --git a/shade/tests/unit/test_baremetal_node.py b/shade/tests/unit/test_baremetal_node.py new file mode 100644 index 000000000..2ae1af632 --- /dev/null +++ b/shade/tests/unit/test_baremetal_node.py @@ -0,0 +1,59 @@ +# 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_baremetal_node +---------------------------------- + +Tests for baremetal node related operations +""" + +import uuid + +from shade.tests import fakes +from shade.tests.unit import base + + +class TestBaremetalNode(base.IronicTestCase): + + def setUp(self): + super(TestBaremetalNode, self).setUp() + self.fake_baremetal_node = fakes.make_fake_machine( + self.name, self.uuid) + + def test_list_machines(self): + fake_baremetal_two = fakes.make_fake_machine('two', str(uuid.uuid4())) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='nodes'), + json={'nodes': [self.fake_baremetal_node, + fake_baremetal_two]}), + ]) + + machines = self.op_cloud.list_machines() + self.assertEqual(2, len(machines)) + self.assertEqual(self.fake_baremetal_node, machines[0]) + self.assert_calls() + + def test_get_machine(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + ]) + + machine = self.op_cloud.get_machine(self.fake_baremetal_node['uuid']) + self.assertEqual(machine['uuid'], + self.fake_baremetal_node['uuid']) + self.assert_calls() diff --git a/shade/tests/unit/test_operator_noauth.py b/shade/tests/unit/test_operator_noauth.py index 37b760f3f..e489d2e84 100644 --- a/shade/tests/unit/test_operator_noauth.py +++ b/shade/tests/unit/test_operator_noauth.py @@ -12,15 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -import mock +from keystoneauth1 import plugin -import ironicclient -from os_client_config import cloud_config import shade -from shade.tests import base +from shade.tests.unit import base -class TestShadeOperatorNoAuth(base.TestCase): +class TestShadeOperatorNoAuth(base.RequestsMockTestCase): def setUp(self): """Setup Noauth OperatorCloud tests @@ -28,32 +26,49 @@ def setUp(self): URL in the auth data. This is permits testing of the basic mechanism that enables Ironic noauth mode to be utilized with Shade. + + Uses base.RequestsMockTestCase instead of IronicTestCase because + we need to do completely different things with discovery. """ super(TestShadeOperatorNoAuth, self).setUp() - self.cloud_noauth = shade.operator_cloud( - auth_type='admin_token', - auth=dict(endpoint="http://localhost:6385"), - validate=False, - ) - - @mock.patch.object(cloud_config.CloudConfig, 'get_session') - @mock.patch.object(ironicclient.client, 'Client') - def test_ironic_noauth_selection_using_a_task( - self, mock_client, get_session_mock): + # By clearing the URI registry, we remove all calls to a keystone + # catalog or getting a token + self._uri_registry.clear() + # TODO(mordred) Remove this if with next KSA release + if hasattr(plugin.BaseAuthPlugin, 'get_endpoint_data'): + self.use_ironic() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + service_type='baremetal', base_url_append='v1', + resource='nodes'), + json={'nodes': []}), + ]) + + def test_ironic_noauth_none_auth_type(self): """Test noauth selection for Ironic in OperatorCloud - Utilize a task to trigger the client connection attempt - and evaluate if get_session_endpoint was called while the client - was still called. + The new way of doing this is with the keystoneauth none plugin. + """ + self.cloud_noauth = shade.operator_cloud( + auth_type='none', + baremetal_endpoint_override="https://bare-metal.example.com") + + self.cloud_noauth.list_machines() - We want session_endpoint to be called because we're storing the - endpoint in a noauth token Session object now. + self.assert_calls() + + def test_ironic_noauth_admin_token_auth_type(self): + """Test noauth selection for Ironic in OperatorCloud + + The old way of doing this was to abuse admin_token. """ - session_mock = mock.Mock() - session_mock.get_endpoint.return_value = None - session_mock.get_token.return_value = 'yankee' - get_session_mock.return_value = session_mock - - self.cloud_noauth.patch_machine('name', {}) - self.assertTrue(get_session_mock.called) - self.assertTrue(mock_client.called) + self.cloud_noauth = shade.operator_cloud( + auth_type='admin_token', + auth=dict( + endpoint='https://bare-metal.example.com', + token='ignored')) + + self.cloud_noauth.list_machines() + + self.assert_calls() diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index f14333a5c..fec16a594 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -37,29 +37,9 @@ def setUp(self): machine_id=self.machine_id, machine_name=self.machine_name) - def get_ironic_mock_url(self, append=None, *args, **kwargs): - if append: - # TODO(mordred): Remove when we do version discovery - # properly everywhere - append.insert(0, 'v1') - return self.get_mock_url('baremetal', append=append, *args, **kwargs) - def test_operator_cloud(self): self.assertIsInstance(self.op_cloud, shade.OperatorCloud) - def test_get_machine(self): - - self.register_uris([ - dict(method='GET', - uri=self.get_ironic_mock_url( - append=['nodes', self.machine_name]), - json=self.node), - ]) - machine = self.op_cloud.get_machine(self.machine_name) - self.assertEqual(self.node, machine) - - self.assert_calls() - @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_get_machine_by_mac(self, mock_client): class port_value(object): From 76caad4d420abe16895b54d40fa89462397f8240 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Tue, 5 Sep 2017 14:14:09 -0300 Subject: [PATCH 1785/3836] De-client-ify User Password Update Change-Id: Idfafc845e44c5a2eb9c6f334a1a4d7eb83c7ce2c --- shade/_tasks.py | 5 ----- shade/openstackcloud.py | 11 ++++++----- shade/tests/unit/test_users.py | 7 ------- 3 files changed, 6 insertions(+), 17 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index b6b319048..8b53b1816 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -17,11 +17,6 @@ from shade import task_manager -class UserPasswordUpdate(task_manager.Task): - def main(self, client): - return client.keystone_client.users.update_password(**self.args) - - class MachineCreate(task_manager.Task): def main(self, client): return client.ironic_client.node.create(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 93a5595aa..5cdfcacba 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -44,7 +44,6 @@ from shade import _normalize from shade import meta from shade import task_manager -from shade import _tasks from shade import _utils OBJECT_MD5_KEY = 'x-object-meta-x-shade-md5' @@ -1008,10 +1007,12 @@ def update_user(self, name_or_id, **kwargs): with _utils.shade_exceptions( "Error updating password for {user}".format( user=name_or_id)): - # normalized dict won't work - user = self.manager.submit_task(_tasks.UserPasswordUpdate( - user=self.get_user_by_id(user['id'], normalize=False), - password=password)) + error_msg = "Error updating password for user {}".format( + name_or_id) + data = self._identity_client.put( + '/users/{u}/OS-KSADM/password'.format(u=user['id']), + json={'user': {'password': password}}, + error_message=error_msg) # Identity v2.0 implements PUT. v3 PATCH. Both work as PATCH. data = self._identity_client.put( diff --git a/shade/tests/unit/test_users.py b/shade/tests/unit/test_users.py index 4923946a6..8f15c864a 100644 --- a/shade/tests/unit/test_users.py +++ b/shade/tests/unit/test_users.py @@ -98,16 +98,11 @@ def test_update_user_password_v2(self): self.register_uris([ # GET list to find user id - # GET user info with user_id from list # PUT user with password update - # GET user info with id after update # PUT empty update (password change is different than update) # but is always chained together [keystoneclient oddity] - # GET user info after user update dict(method='GET', uri=mock_users_uri, status_code=200, json=self._get_user_list(user_data)), - dict(method='GET', uri=mock_user_resource_uri, status_code=200, - json=user_data.json_response), dict(method='PUT', uri=self._get_keystone_mock_url( resource='users', v3=False, @@ -115,8 +110,6 @@ def test_update_user_password_v2(self): status_code=200, json=user_data.json_response, validate=dict( json={'user': {'password': user_data.password}})), - dict(method='GET', uri=mock_user_resource_uri, status_code=200, - json=user_data.json_response), dict(method='PUT', uri=mock_user_resource_uri, status_code=200, json=user_data.json_response, validate=dict(json={'user': {}}))]) From a4f94bf4def73704ff4947f9874845bca314239d Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Tue, 5 Sep 2017 14:51:32 -0300 Subject: [PATCH 1786/3836] De-client-ify Endpoint Delete Change-Id: Ieea67d733a20af6ecb7fde88d6af85f8b0cf2c18 --- shade/_tasks.py | 5 ----- shade/operatorcloud.py | 16 +++++++--------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 8b53b1816..4cbc3d84e 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -112,11 +112,6 @@ def main(self, client): return client.keystone_client.endpoints.list() -class EndpointDelete(task_manager.Task): - def main(self, client): - return client.keystone_client.endpoints.delete(**self.args) - - class RoleAddUser(task_manager.Task): def main(self, client): return client.keystone_client.roles.add_user_role(**self.args) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 1d74ee32d..65af936e2 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1074,15 +1074,13 @@ def delete_endpoint(self, id): self.log.debug("Endpoint %s not found for deleting", id) return False - # TODO(mordred) When this changes to REST, force interface=admin - # in the adapter call - if self._is_client_version('identity', 2): - endpoint_kwargs = {'id': endpoint['id']} - else: - endpoint_kwargs = {'endpoint': endpoint['id']} - with _utils.shade_exceptions("Failed to delete endpoint {id}".format( - id=id)): - self.manager.submit_task(_tasks.EndpointDelete(**endpoint_kwargs)) + # Force admin interface if v2.0 is in use + v2 = self._is_client_version('identity', 2) + kwargs = {'endpoint_filter': {'interface': 'admin'}} if v2 else {} + + error_msg = "Failed to delete endpoint {id}".format(id=id) + self._identity_client.delete('/endpoints/{id}'.format(id=id), + error_message=error_msg, **kwargs) return True From 8d310b7818798d828d0fac8c90553bccb26228d9 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Tue, 5 Sep 2017 20:59:55 -0300 Subject: [PATCH 1787/3836] De-client-ify Role Grant and Revoke Change-Id: Ie0a9748132ef7f417aabf976be21ec2d32085c8b --- shade/_tasks.py | 20 ----------- shade/operatorcloud.py | 75 +++++++++++++++++++++++++++++------------- 2 files changed, 53 insertions(+), 42 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 4cbc3d84e..ec7e3b6e6 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -112,26 +112,6 @@ def main(self, client): return client.keystone_client.endpoints.list() -class RoleAddUser(task_manager.Task): - def main(self, client): - return client.keystone_client.roles.add_user_role(**self.args) - - -class RoleGrantUser(task_manager.Task): - def main(self, client): - return client.keystone_client.roles.grant(**self.args) - - -class RoleRemoveUser(task_manager.Task): - def main(self, client): - return client.keystone_client.roles.remove_user_role(**self.args) - - -class RoleRevokeUser(task_manager.Task): - def main(self, client): - return client.keystone_client.roles.revoke(**self.args) - - class RoleAssignmentList(task_manager.Task): def main(self, client): return client.keystone_client.role_assignments.list(**self.args) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 65af936e2..1bbd2ea87 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1731,7 +1731,7 @@ def grant_role(self, name_or_id, user=None, group=None, in the same role grant, it is required to specify those by ID. NOTE: for wait and timeout, sometimes granting roles is not - instantaneous for granting roles. + instantaneous. NOTE: project is required for keystone v2 @@ -1761,17 +1761,32 @@ def grant_role(self, name_or_id, user=None, group=None, self.log.debug('Assignment already exists') return False - with _utils.shade_exceptions( - "Error granting access to role: {0}".format( - data)): - if self._is_client_version('identity', 2): - data['tenant'] = data.pop('project') - self.manager.submit_task(_tasks.RoleAddUser(**data)) + error_msg = "Error granting access to role: {0}".format(data) + if self._is_client_version('identity', 2): + # For v2.0, only tenant/project assignment is supported + url = "/tenants/{t}/users/{u}/roles/OS-KSADM/{r}".format( + t=data['project']['id'], u=data['user']['id'], r=data['role']) + + self._identity_client.put(url, error_message=error_msg, + endpoint_filter={'interface': 'admin'}) + else: + if data.get('project') is None and data.get('domain') is None: + raise OpenStackCloudException( + 'Must specify either a domain or project') + + # For v3, figure out the assignment type and build the URL + if data.get('domain'): + url = "/domains/{}".format(data['domain']) else: - if data.get('project') is None and data.get('domain') is None: - raise OpenStackCloudException( - 'Must specify either a domain or project') - self.manager.submit_task(_tasks.RoleGrantUser(**data)) + url = "/projects/{}".format(data['project']['id']) + if data.get('group'): + url += "/groups/{}".format(data['group']['id']) + else: + url += "/users/{}".format(data['user']['id']) + url += "/roles/{}".format(data.get('role')) + + self._identity_client.put(url, error_message=error_msg) + if wait: for count in _utils._iterate_timeout( timeout, @@ -1793,7 +1808,7 @@ def revoke_role(self, name_or_id, user=None, group=None, :param int timeout: Timeout to wait for role to be revoked NOTE: for wait and timeout, sometimes revoking roles is not - instantaneous for revoking roles. + instantaneous. NOTE: project is required for keystone v2 @@ -1824,17 +1839,33 @@ def revoke_role(self, name_or_id, user=None, group=None, self.log.debug('Assignment does not exist') return False - with _utils.shade_exceptions( - "Error revoking access to role: {0}".format( - data)): - if self._is_client_version('identity', 2): - data['tenant'] = data.pop('project') - self.manager.submit_task(_tasks.RoleRemoveUser(**data)) + error_msg = "Error revoking access to role: {0}".format(data) + if self._is_client_version('identity', 2): + # For v2.0, only tenant/project assignment is supported + url = "/tenants/{t}/users/{u}/roles/OS-KSADM/{r}".format( + t=data['project']['id'], u=data['user']['id'], r=data['role']) + + self._identity_client.delete( + url, error_message=error_msg, + endpoint_filter={'interface': 'admin'}) + else: + if data.get('project') is None and data.get('domain') is None: + raise OpenStackCloudException( + 'Must specify either a domain or project') + + # For v3, figure out the assignment type and build the URL + if data.get('domain'): + url = "/domains/{}".format(data['domain']) else: - if data.get('project') is None and data.get('domain') is None: - raise OpenStackCloudException( - 'Must specify either a domain or project') - self.manager.submit_task(_tasks.RoleRevokeUser(**data)) + url = "/projects/{}".format(data['project']['id']) + if data.get('group'): + url += "/groups/{}".format(data['group']['id']) + else: + url += "/users/{}".format(data['user']['id']) + url += "/roles/{}".format(data.get('role')) + + self._identity_client.delete(url, error_message=error_msg) + if wait: for count in _utils._iterate_timeout( timeout, From eb28fa57510267b63d9a8647bb12e31d94539430 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Wed, 6 Sep 2017 06:46:52 -0300 Subject: [PATCH 1788/3836] De-client-ify List Roles for User in v2.0 Change-Id: Ic652a38ba86594540ac4a4f5d944a8cd627d60f5 --- shade/_tasks.py | 5 ----- shade/operatorcloud.py | 22 ++++++++++++++++++---- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index ec7e3b6e6..f85cf6de1 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -115,8 +115,3 @@ def main(self, client): class RoleAssignmentList(task_manager.Task): def main(self, client): return client.keystone_client.role_assignments.list(**self.args) - - -class RolesForUser(task_manager.Task): - def main(self, client): - return client.keystone_client.roles.roles_for_user(**self.args) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 1bbd2ea87..1d363747e 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -13,6 +13,7 @@ import datetime import iso8601 import jsonpatch +import munch from ironicclient import exceptions as ironic_exceptions @@ -1426,10 +1427,13 @@ def get_role(self, name_or_id, filters=None): def _keystone_v2_role_assignments(self, user, project=None, role=None, **kwargs): - with _utils.shade_exceptions("Failed to list role assignments"): - roles = self.manager.submit_task( - _tasks.RolesForUser(user=user, tenant=project) - ) + data = self._identity_client.get( + "/tenants/{tenant}/users/{user}/roles".format( + tenant=project, user=user), + error_message="Failed to list role assignments") + + roles = self._get_and_munchify('roles', data) + ret = [] for tmprole in roles: if role is not None and role != tmprole.id: @@ -1483,6 +1487,16 @@ def list_role_assignments(self, filters=None): if not filters: filters = {} + # NOTE(samueldmq): the docs above say filters are *IDs*, though if + # munch.Munch objects are passed, this still works for backwards + # compatibility as keystoneclient allows either IDs or objects to be + # passed in. + # TODO(samueldmq): fix the docs above to advertise munch.Munch objects + # can be provided as parameters too + for k, v in filters.items(): + if isinstance(v, munch.Munch): + filters[k] = v['id'] + if self._is_client_version('identity', 2): if filters.get('project') is None or filters.get('user') is None: raise OpenStackCloudException( From 179430c9868321bbc91f12376f44c4e1433fa7fb Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Wed, 6 Sep 2017 07:14:38 -0300 Subject: [PATCH 1789/3836] De-client-ify Endpoint List Change-Id: If711f71ec05a03e5613cf25849c7b6a68f23eb55 --- shade/_tasks.py | 5 ----- shade/operatorcloud.py | 19 +++++++++++-------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index f85cf6de1..8f58eae6e 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -107,11 +107,6 @@ def main(self, client): return client.keystone_client.endpoints.update(**self.args) -class EndpointList(task_manager.Task): - def main(self, client): - return client.keystone_client.endpoints.list() - - class RoleAssignmentList(task_manager.Task): def main(self, client): return client.keystone_client.role_assignments.list(**self.args) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 1d363747e..58f0bed80 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1011,14 +1011,13 @@ def list_endpoints(self): :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ - # NOTE(SamYaple): With keystone v3 we can filter directly via the - # the keystone api, but since the return of all the endpoints even in - # large environments is small, we can continue to filter in shade just - # like the v2 api. - # TODO(mordred) When this changes to REST, force interface=admin - # in the adapter call - with _utils.shade_exceptions("Failed to list endpoints"): - endpoints = self.manager.submit_task(_tasks.EndpointList()) + # Force admin interface if v2.0 is in use + v2 = self._is_client_version('identity', 2) + kwargs = {'endpoint_filter': {'interface': 'admin'}} if v2 else {} + + data = self._identity_client.get( + '/endpoints', error_message="Failed to list endpoints", **kwargs) + endpoints = self._get_and_munchify('endpoints', data) return endpoints @@ -1040,6 +1039,10 @@ def search_endpoints(self, id=None, filters=None): :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ + # NOTE(SamYaple): With keystone v3 we can filter directly via the + # the keystone api, but since the return of all the endpoints even in + # large environments is small, we can continue to filter in shade just + # like the v2 api. endpoints = self.list_endpoints() return _utils._filter_list(endpoints, id, filters) From 8afeaf60b164120c240b54fbbb36ccf5b73ca299 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Wed, 6 Sep 2017 10:53:43 -0300 Subject: [PATCH 1790/3836] De-client-ify List Role Assignments Change-Id: Ief3fd56ccab977399c71cd603e42b52296e2a002 --- shade/_tasks.py | 5 ---- shade/operatorcloud.py | 35 ++++++++++++++++++++++--- shade/tests/unit/test_identity_roles.py | 2 +- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 8f58eae6e..420540bec 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -105,8 +105,3 @@ def main(self, client): class EndpointUpdate(task_manager.Task): def main(self, client): return client.keystone_client.endpoints.update(**self.args) - - -class RoleAssignmentList(task_manager.Task): - def main(self, client): - return client.keystone_client.role_assignments.list(**self.args) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 58f0bed80..f0aee9a2a 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1456,6 +1456,30 @@ def _keystone_v2_role_assignments(self, user, project=None, }) return ret + def _keystone_v3_role_assignments(self, **filters): + # NOTE(samueldmq): different parameters have different representation + # patterns as query parameters in the call to the list role assignments + # API. The code below handles each set of patterns separately and + # renames the parameters names accordingly, ignoring 'effective', + # 'include_names' and 'include_subtree' whose do not need any renaming. + for k in ('group', 'role', 'user'): + if k in filters: + filters[k + '.id'] = filters[k] + del filters[k] + for k in ('project', 'domain'): + if k in filters: + filters['scope.' + k + '.id'] = filters[k] + del filters[k] + if 'os_inherit_extension_inherited_to' in filters: + filters['scope.OS-INHERIT:inherited_to'] = ( + filters['os_inherit_extension_inherited_to']) + del filters['os_inherit_extension_inherited_to'] + + data = self._identity_client.get( + '/role_assignments', params=filters, + error_message="Failed to list role assignments") + return self._get_and_munchify('role_assignments', data) + def list_role_assignments(self, filters=None): """List Keystone role assignments @@ -1487,6 +1511,11 @@ def list_role_assignments(self, filters=None): :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ + # NOTE(samueldmq): although 'include_names' is a valid query parameter + # in the keystone v3 list role assignments API, it would have NO effect + # on shade due to normalization. It is not documented as an acceptable + # filter in the docs above per design! + if not filters: filters = {} @@ -1507,10 +1536,8 @@ def list_role_assignments(self, filters=None): ) assignments = self._keystone_v2_role_assignments(**filters) else: - with _utils.shade_exceptions("Failed to list role assignments"): - assignments = self.manager.submit_task( - _tasks.RoleAssignmentList(**filters) - ) + assignments = self._keystone_v3_role_assignments(**filters) + return _utils.normalize_role_assignments(assignments) def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", diff --git a/shade/tests/unit/test_identity_roles.py b/shade/tests/unit/test_identity_roles.py index b7e448fb4..3ef75ce53 100644 --- a/shade/tests/unit/test_identity_roles.py +++ b/shade/tests/unit/test_identity_roles.py @@ -202,7 +202,7 @@ def test_list_role_assignments_exception(self): status_code=403) ]) with testtools.ExpectedException( - shade.OpenStackCloudException, + shade.exc.OpenStackCloudHTTPError, "Failed to list role assignments" ): self.op_cloud.list_role_assignments() From 8cc1483cc153254b54d96dd615dd174e4dc3c6c5 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Wed, 6 Sep 2017 13:55:26 -0300 Subject: [PATCH 1791/3836] De-client-ify Endpoint Update Change-Id: I823414cbee3b8f38fcbddd16271ff61005eaf35d --- shade/_tasks.py | 5 ----- shade/operatorcloud.py | 14 +++++--------- shade/tests/unit/test_endpoints.py | 6 +----- 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index 420540bec..a826ac92b 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -100,8 +100,3 @@ def main(self, client): class EndpointCreate(task_manager.Task): def main(self, client): return client.keystone_client.endpoints.create(**self.args) - - -class EndpointUpdate(task_manager.Task): - def main(self, client): - return client.keystone_client.endpoints.update(**self.args) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index f0aee9a2a..c5f4d4e92 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -992,16 +992,12 @@ def update_endpoint(self, endpoint_id, **kwargs): service_name_or_id = kwargs.pop('service_name_or_id', None) if service_name_or_id is not None: - kwargs['service'] = service_name_or_id + kwargs['service_id'] = service_name_or_id - # TODO(mordred) When this changes to REST, force interface=admin - # in the adapter call - with _utils.shade_exceptions( - "Failed to update endpoint {}".format(endpoint_id) - ): - return self.manager.submit_task(_tasks.EndpointUpdate( - endpoint=endpoint_id, **kwargs - )) + data = self._identity_client.patch( + '/endpoints/{}'.format(endpoint_id), json={'endpoint': kwargs}, + error_message="Failed to update endpoint {}".format(endpoint_id)) + return self._get_and_munchify('endpoint', data) def list_endpoints(self): """List Keystone endpoints. diff --git a/shade/tests/unit/test_endpoints.py b/shade/tests/unit/test_endpoints.py index 2c14ea679..c1ab3f5fd 100644 --- a/shade/tests/unit/test_endpoints.py +++ b/shade/tests/unit/test_endpoints.py @@ -265,11 +265,7 @@ def test_update_endpoint_v3(self): uri=self.get_mock_url(append=[endpoint_data.endpoint_id]), status_code=200, json=endpoint_data.json_response, - validate=dict(json=reference_request)), - dict(method='GET', - uri=self.get_mock_url(append=[endpoint_data.endpoint_id]), - status_code=200, - json=endpoint_data.json_response) + validate=dict(json=reference_request)) ]) endpoint = self.op_cloud.update_endpoint( endpoint_data.endpoint_id, From e8d37f8281192838fe5ca1b1dc86b4a69b44f061 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Thu, 7 Sep 2017 06:10:27 +0000 Subject: [PATCH 1792/3836] Migrate additional machine tests Migrated a number of simpler machine tests to using rest calls, while removing the mocked client versions of the tests. Added additional patch_machine tests, which raised the fact that patch method was not normalizing the data being returned. Added normalization since those fields are not normally returned by get_machine. Change-Id: I1c5f4ebd06fff40da45dbcc0d6ea0f7b108ce8fa --- shade/operatorcloud.py | 9 +- shade/tests/unit/test_baremetal_node.py | 250 +++++++++++++++++++ shade/tests/unit/test_shade_operator.py | 307 ------------------------ 3 files changed, 255 insertions(+), 311 deletions(-) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 6fac151d4..5bec9b3da 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -378,10 +378,11 @@ def patch_machine(self, name_or_id, patch): "Error updating machine via patch operation on node " "{node}".format(node=name_or_id) ): - return self.manager.submit_task( - _tasks.MachinePatch(node_id=name_or_id, - patch=patch, - http_method='PATCH')) + return self._normalize_machine( + self.manager.submit_task( + _tasks.MachinePatch(node_id=name_or_id, + patch=patch, + http_method='PATCH'))) def update_machine(self, name_or_id, chassis_uuid=None, driver=None, driver_info=None, name=None, instance_info=None, diff --git a/shade/tests/unit/test_baremetal_node.py b/shade/tests/unit/test_baremetal_node.py index 2ae1af632..6238a2da2 100644 --- a/shade/tests/unit/test_baremetal_node.py +++ b/shade/tests/unit/test_baremetal_node.py @@ -57,3 +57,253 @@ def test_get_machine(self): self.assertEqual(machine['uuid'], self.fake_baremetal_node['uuid']) self.assert_calls() + + def test_get_machine_by_mac(self): + mac_address = '00:01:02:03:04:05' + url_address = 'detail?address=%s' % mac_address + node_uuid = self.fake_baremetal_node['uuid'] + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + resource='ports', + append=[url_address]), + json={'ports': [{'address': mac_address, + 'node_uuid': node_uuid}]}), + dict(method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + ]) + + machine = self.op_cloud.get_machine_by_mac(mac_address) + self.assertEqual(machine['uuid'], + self.fake_baremetal_node['uuid']) + self.assert_calls() + + def test_validate_node(self): + # NOTE(TheJulia): Note: These are only the interfaces + # that are validated, and both must be true for an + # exception to not be raised. + # This should be fixed at some point, as some interfaces + # are important in some cases and should be validated, + # such as storage. + validate_return = { + 'deploy': { + 'result': True, + }, + 'power': { + 'result': True, + }, + 'foo': { + 'result': False, + }} + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'validate']), + json=validate_return), + ]) + self.op_cloud.validate_node(self.fake_baremetal_node['uuid']) + + self.assert_calls() + + # FIXME(TheJulia): So, this doesn't presently fail, but should fail. + # Placing the test here, so we can sort out the issue in the actual + # method later. + # def test_validate_node_raises_exception(self): + # validate_return = { + # 'deploy': { + # 'result': False, + # 'reason': 'error!', + # }, + # 'power': { + # 'result': False, + # 'reason': 'meow!', + # }, + # 'foo': { + # 'result': True + # }} + # self.register_uris([ + # dict(method='GET', + # uri=self.get_mock_url( + # resource='nodes', + # append=[self.fake_baremetal_node['uuid'], + # 'validate']), + # json=validate_return), + # ]) + # self.assertRaises( + # Exception, + # self.op_cloud.validate_node, + # self.fake_baremetal_node['uuid']) + # + # self.assert_calls() + + def test_patch_machine(self): + test_patch = [{ + 'op': 'remove', + 'path': '/instance_info'}] + self.fake_baremetal_node['instance_info'] = {} + self.register_uris([ + dict(method='PATCH', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node, + validate=dict(json=test_patch)), + ]) + self.op_cloud.patch_machine(self.fake_baremetal_node['uuid'], + test_patch) + + self.assert_calls() + + def test_set_node_instance_info(self): + test_patch = [{ + 'op': 'add', + 'path': '/foo', + 'value': 'bar'}] + self.register_uris([ + dict(method='PATCH', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node, + validate=dict(json=test_patch)), + ]) + self.op_cloud.set_node_instance_info( + self.fake_baremetal_node['uuid'], test_patch) + + self.assert_calls() + + def test_purge_node_instance_info(self): + test_patch = [{ + 'op': 'remove', + 'path': '/instance_info'}] + self.fake_baremetal_node['instance_info'] = {} + self.register_uris([ + dict(method='PATCH', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node, + validate=dict(json=test_patch)), + ]) + self.op_cloud.purge_node_instance_info( + self.fake_baremetal_node['uuid']) + + self.assert_calls() + + def _test_update_machine(self, fake_node, field_name, changed=True): + # The model has evolved over time, create the field if + # we don't already have it. + if field_name not in fake_node: + fake_node[field_name] = None + value_to_send = fake_node[field_name] + if changed: + value_to_send = 'meow' + uris = [dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[fake_node['uuid']]), + json=fake_node), + ] + if changed: + test_patch = [{ + 'op': 'replace', + 'path': '/' + field_name, + 'value': 'meow'}] + uris.append( + dict( + method='PATCH', + uri=self.get_mock_url( + resource='nodes', + append=[fake_node['uuid']]), + json=fake_node, + validate=dict(json=test_patch))) + + self.register_uris(uris) + + call_args = {field_name: value_to_send} + update_dict = self.op_cloud.update_machine( + fake_node['uuid'], **call_args) + + if not changed: + self.assertIsNone(update_dict['changes']) + self.assertDictEqual(fake_node, update_dict['node']) + + self.assert_calls() + + def test_update_machine_patch_name(self): + self._test_update_machine(self.fake_baremetal_node, + 'name', False) + + def test_update_machine_patch_chassis_uuid(self): + self._test_update_machine(self.fake_baremetal_node, + 'chassis_uuid', False) + + def test_update_machine_patch_driver(self): + self._test_update_machine(self.fake_baremetal_node, + 'driver', False) + + def test_update_machine_patch_driver_info(self): + self._test_update_machine(self.fake_baremetal_node, + 'driver_info', False) + + def test_update_machine_patch_instance_info(self): + self._test_update_machine(self.fake_baremetal_node, + 'instance_info', False) + + def test_update_machine_patch_instance_uuid(self): + self._test_update_machine(self.fake_baremetal_node, + 'instance_uuid', False) + + def test_update_machine_patch_properties(self): + self._test_update_machine(self.fake_baremetal_node, + 'properties', False) + + def test_update_machine_patch_update_name(self): + self._test_update_machine(self.fake_baremetal_node, + 'name', True) + + def test_update_machine_patch_update_chassis_uuid(self): + self._test_update_machine(self.fake_baremetal_node, + 'chassis_uuid', True) + + def test_update_machine_patch_update_driver(self): + self._test_update_machine(self.fake_baremetal_node, + 'driver', True) + + def test_update_machine_patch_update_driver_info(self): + self._test_update_machine(self.fake_baremetal_node, + 'driver_info', True) + + def test_update_machine_patch_update_instance_info(self): + self._test_update_machine(self.fake_baremetal_node, + 'instance_info', True) + + def test_update_machine_patch_update_instance_uuid(self): + self._test_update_machine(self.fake_baremetal_node, + 'instance_uuid', True) + + def test_update_machine_patch_update_properties(self): + self._test_update_machine(self.fake_baremetal_node, + 'properties', True) + + def test_update_machine_patch_no_action(self): + self.register_uris([dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + ]) + # NOTE(TheJulia): This is just testing mechanics. + update_dict = self.op_cloud.update_machine( + self.fake_baremetal_node['uuid']) + self.assertIsNone(update_dict['changes']) + self.assertDictEqual(self.fake_baremetal_node, update_dict['node']) + + self.assert_calls() diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index fec16a594..960836099 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -40,43 +40,6 @@ def setUp(self): def test_operator_cloud(self): self.assertIsInstance(self.op_cloud, shade.OperatorCloud) - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_get_machine_by_mac(self, mock_client): - class port_value(object): - node_uuid = '00000000-0000-0000-0000-000000000000' - address = '00:00:00:00:00:00' - - class node_value(object): - uuid = '00000000-0000-0000-0000-000000000000' - - expected_value = dict( - uuid='00000000-0000-0000-0000-000000000000') - - mock_client.port.get_by_address.return_value = port_value - mock_client.node.get.return_value = node_value - machine = self.op_cloud.get_machine_by_mac('00:00:00:00:00:00') - mock_client.port.get_by_address.assert_called_with( - address='00:00:00:00:00:00') - mock_client.node.get.assert_called_with( - node_id='00000000-0000-0000-0000-000000000000') - self.assertEqual(machine, expected_value) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_list_machines(self, mock_client): - m1 = fakes.FakeMachine(1, 'fake_machine1') - mock_client.node.list.return_value = [m1] - machines = self.op_cloud.list_machines() - self.assertTrue(mock_client.node.list.called) - self.assertEqual(meta.obj_to_munch(m1), machines[0]) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_validate_node(self, mock_client): - node_uuid = '123' - self.op_cloud.validate_node(node_uuid) - mock_client.node.validate.assert_called_once_with( - node_uuid=node_uuid - ) - @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_list_nics(self, mock_client): port1 = fakes.FakeMachinePort(1, "aa:bb:cc:dd", "node1") @@ -108,258 +71,6 @@ def test_list_nics_for_machine_failure(self, mock_client): self.assertRaises(exc.OpenStackCloudException, self.op_cloud.list_nics_for_machine, None) - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_patch_machine(self, mock_client): - node_id = 'node01' - patch = [] - patch.append({'op': 'remove', 'path': '/instance_info'}) - self.op_cloud.patch_machine(node_id, patch) - self.assertTrue(mock_client.node.update.called) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - @mock.patch.object(shade.OperatorCloud, 'patch_machine') - def test_update_machine_patch_no_action(self, mock_patch, mock_client): - class client_return_value(object): - uuid = '00000000-0000-0000-0000-000000000000' - name = 'node01' - - expected_machine = dict( - uuid='00000000-0000-0000-0000-000000000000', - name='node01' - ) - mock_client.node.get.return_value = client_return_value - - update_dict = self.op_cloud.update_machine('node01') - self.assertIsNone(update_dict['changes']) - self.assertFalse(mock_patch.called) - self.assertDictEqual(expected_machine, update_dict['node']) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - @mock.patch.object(shade.OperatorCloud, 'patch_machine') - def test_update_machine_patch_no_action_name(self, mock_patch, - mock_client): - class client_return_value(object): - uuid = '00000000-0000-0000-0000-000000000000' - name = 'node01' - - expected_machine = dict( - uuid='00000000-0000-0000-0000-000000000000', - name='node01' - ) - mock_client.node.get.return_value = client_return_value - - update_dict = self.op_cloud.update_machine('node01', name='node01') - self.assertIsNone(update_dict['changes']) - self.assertFalse(mock_patch.called) - self.assertDictEqual(expected_machine, update_dict['node']) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - @mock.patch.object(shade.OperatorCloud, 'patch_machine') - def test_update_machine_patch_action_name(self, mock_patch, - mock_client): - class client_return_value(object): - uuid = '00000000-0000-0000-0000-000000000000' - name = 'evil' - - expected_patch = [dict(op='replace', path='/name', value='good')] - - mock_client.node.get.return_value = client_return_value - - update_dict = self.op_cloud.update_machine('evil', name='good') - self.assertIsNotNone(update_dict['changes']) - self.assertEqual('/name', update_dict['changes'][0]) - self.assertTrue(mock_patch.called) - mock_patch.assert_called_with( - '00000000-0000-0000-0000-000000000000', - expected_patch) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - @mock.patch.object(shade.OperatorCloud, 'patch_machine') - def test_update_machine_patch_update_name(self, mock_patch, - mock_client): - class client_return_value(object): - uuid = '00000000-0000-0000-0000-000000000000' - name = 'evil' - - expected_patch = [dict(op='replace', path='/name', value='good')] - - mock_client.node.get.return_value = client_return_value - - update_dict = self.op_cloud.update_machine('evil', name='good') - self.assertIsNotNone(update_dict['changes']) - self.assertEqual('/name', update_dict['changes'][0]) - self.assertTrue(mock_patch.called) - mock_patch.assert_called_with( - '00000000-0000-0000-0000-000000000000', - expected_patch) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - @mock.patch.object(shade.OperatorCloud, 'patch_machine') - def test_update_machine_patch_update_chassis_uuid(self, mock_patch, - mock_client): - class client_return_value(object): - uuid = '00000000-0000-0000-0000-000000000000' - chassis_uuid = None - - expected_patch = [ - dict( - op='replace', - path='/chassis_uuid', - value='00000000-0000-0000-0000-000000000001' - )] - - mock_client.node.get.return_value = client_return_value - - update_dict = self.op_cloud.update_machine( - '00000000-0000-0000-0000-000000000000', - chassis_uuid='00000000-0000-0000-0000-000000000001') - self.assertIsNotNone(update_dict['changes']) - self.assertEqual('/chassis_uuid', update_dict['changes'][0]) - self.assertTrue(mock_patch.called) - mock_patch.assert_called_with( - '00000000-0000-0000-0000-000000000000', - expected_patch) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - @mock.patch.object(shade.OperatorCloud, 'patch_machine') - def test_update_machine_patch_update_driver(self, mock_patch, - mock_client): - class client_return_value(object): - uuid = '00000000-0000-0000-0000-000000000000' - driver = None - - expected_patch = [ - dict( - op='replace', - path='/driver', - value='fake' - )] - - mock_client.node.get.return_value = client_return_value - - update_dict = self.op_cloud.update_machine( - '00000000-0000-0000-0000-000000000000', - driver='fake' - ) - self.assertIsNotNone(update_dict['changes']) - self.assertEqual('/driver', update_dict['changes'][0]) - self.assertTrue(mock_patch.called) - mock_patch.assert_called_with( - '00000000-0000-0000-0000-000000000000', - expected_patch) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - @mock.patch.object(shade.OperatorCloud, 'patch_machine') - def test_update_machine_patch_update_driver_info(self, mock_patch, - mock_client): - class client_return_value(object): - uuid = '00000000-0000-0000-0000-000000000000' - driver_info = None - - expected_patch = [ - dict( - op='replace', - path='/driver_info', - value=dict(var='fake') - )] - - mock_client.node.get.return_value = client_return_value - - update_dict = self.op_cloud.update_machine( - '00000000-0000-0000-0000-000000000000', - driver_info=dict(var="fake") - ) - self.assertIsNotNone(update_dict['changes']) - self.assertEqual('/driver_info', update_dict['changes'][0]) - self.assertTrue(mock_patch.called) - mock_patch.assert_called_with( - '00000000-0000-0000-0000-000000000000', - expected_patch) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - @mock.patch.object(shade.OperatorCloud, 'patch_machine') - def test_update_machine_patch_update_instance_info(self, mock_patch, - mock_client): - class client_return_value(object): - uuid = '00000000-0000-0000-0000-000000000000' - instance_info = None - - expected_patch = [ - dict( - op='replace', - path='/instance_info', - value=dict(var='fake') - )] - - mock_client.node.get.return_value = client_return_value - - update_dict = self.op_cloud.update_machine( - '00000000-0000-0000-0000-000000000000', - instance_info=dict(var="fake") - ) - self.assertIsNotNone(update_dict['changes']) - self.assertEqual('/instance_info', update_dict['changes'][0]) - self.assertTrue(mock_patch.called) - mock_patch.assert_called_with( - '00000000-0000-0000-0000-000000000000', - expected_patch) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - @mock.patch.object(shade.OperatorCloud, 'patch_machine') - def test_update_machine_patch_update_instance_uuid(self, mock_patch, - mock_client): - class client_return_value(object): - uuid = '00000000-0000-0000-0000-000000000000' - instance_uuid = None - - expected_patch = [ - dict( - op='replace', - path='/instance_uuid', - value='00000000-0000-0000-0000-000000000002' - )] - - mock_client.node.get.return_value = client_return_value - - update_dict = self.op_cloud.update_machine( - '00000000-0000-0000-0000-000000000000', - instance_uuid='00000000-0000-0000-0000-000000000002' - ) - self.assertIsNotNone(update_dict['changes']) - self.assertEqual('/instance_uuid', update_dict['changes'][0]) - self.assertTrue(mock_patch.called) - mock_patch.assert_called_with( - '00000000-0000-0000-0000-000000000000', - expected_patch) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - @mock.patch.object(shade.OperatorCloud, 'patch_machine') - def test_update_machine_patch_update_properties(self, mock_patch, - mock_client): - class client_return_value(object): - uuid = '00000000-0000-0000-0000-000000000000' - properties = None - - expected_patch = [ - dict( - op='replace', - path='/properties', - value=dict(var='fake') - )] - - mock_client.node.get.return_value = client_return_value - - update_dict = self.op_cloud.update_machine( - '00000000-0000-0000-0000-000000000000', - properties=dict(var="fake") - ) - self.assertIsNotNone(update_dict['changes']) - self.assertEqual('/properties', update_dict['changes'][0]) - self.assertTrue(mock_patch.called) - mock_patch.assert_called_with( - '00000000-0000-0000-0000-000000000000', - expected_patch) - @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_inspect_machine_fail_active(self, mock_client): @@ -1060,24 +771,6 @@ class deactivated_node_state(object): configdrive=None) self.assertEqual(mock_client.node.get.call_count, 2) - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_set_node_instance_info(self, mock_client): - uuid = 'aaa' - patch = [{'op': 'add', 'foo': 'bar'}] - self.op_cloud.set_node_instance_info(uuid, patch) - mock_client.node.update.assert_called_with( - node_id=uuid, patch=patch - ) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_purge_node_instance_info(self, mock_client): - uuid = 'aaa' - expected_patch = [{'op': 'remove', 'path': '/instance_info'}] - self.op_cloud.purge_node_instance_info(uuid) - mock_client.node.update.assert_called_with( - node_id=uuid, patch=expected_patch - ) - @mock.patch.object(shade.OpenStackCloud, '_image_client') def test_get_image_name(self, mock_client): From e2dc28f8d3b430d2953ccecdac2a1f6cc7604d5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Thu, 7 Sep 2017 19:49:29 +0000 Subject: [PATCH 1793/3836] Add getting of QoS rule type details Neutron API now supports getting details of supported QoS rule type. This patch adds support for this feature to SDK. Change-Id: I448b5d4f8e4ef42eafe50d9d6c63d0be666f98fc Related-Bug: #1686035 --- doc/source/users/proxies/network.rst | 2 ++ openstack/network/v2/_proxy.py | 30 +++++++++++++++++++ openstack/network/v2/qos_rule_type.py | 6 ++-- .../network/v2/test_qos_rule_type.py | 12 ++++++++ openstack/tests/unit/network/v2/test_proxy.py | 8 +++++ .../unit/network/v2/test_qos_rule_type.py | 19 +++++++++++- 6 files changed, 74 insertions(+), 3 deletions(-) diff --git a/doc/source/users/proxies/network.rst b/doc/source/users/proxies/network.rst index 290e20105..20f895772 100644 --- a/doc/source/users/proxies/network.rst +++ b/doc/source/users/proxies/network.rst @@ -167,6 +167,8 @@ QoS Operations .. automethod:: openstack.network.v2._proxy.Proxy.get_qos_policy .. automethod:: openstack.network.v2._proxy.Proxy.find_qos_policy .. automethod:: openstack.network.v2._proxy.Proxy.qos_policies + .. automethod:: openstack.network.v2._proxy.Proxy.get_qos_rule_type + .. automethod:: openstack.network.v2._proxy.Proxy.find_qos_rule_type .. automethod:: openstack.network.v2._proxy.Proxy.qos_rule_types .. automethod:: openstack.network.v2._proxy.Proxy.create_qos_minimum_bandwidth_rule diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index df86e9740..e65cbf05f 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -1974,6 +1974,36 @@ def update_qos_policy(self, qos_policy, **attrs): """ return self._update(_qos_policy.QoSPolicy, qos_policy, **attrs) + def find_qos_rule_type(self, rule_type_name, ignore_missing=True): + """Find a single QoS rule type details + + :param rule_type_name: The name of a QoS rule type. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :returns: One :class:`~openstack.network.v2.qos_rule_type.QoSRuleType` + or None + """ + return self._find(_qos_rule_type.QoSRuleType, rule_type_name, + ignore_missing=ignore_missing) + + def get_qos_rule_type(self, qos_rule_type): + """Get details about single QoS rule type + + :param qos_rule_type: The value can be the name of a QoS policy + rule type or a + :class:`~openstack.network.v2. + qos_rule_type.QoSRuleType` + instance. + + :returns: One :class:`~openstack.network.v2.qos_rule_type.QoSRuleType` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_qos_rule_type.QoSRuleType, qos_rule_type) + def qos_rule_types(self, **query): """Return a generator of QoS rule types diff --git a/openstack/network/v2/qos_rule_type.py b/openstack/network/v2/qos_rule_type.py index 2f33f59b8..ee2812c17 100644 --- a/openstack/network/v2/qos_rule_type.py +++ b/openstack/network/v2/qos_rule_type.py @@ -22,13 +22,15 @@ class QoSRuleType(resource.Resource): # capabilities allow_create = False - allow_get = False + allow_get = True allow_update = False allow_delete = False allow_list = True - _query_mapping = resource.QueryParameters('type') + _query_mapping = resource.QueryParameters('type', 'drivers') # Properties #: QoS rule type name. type = resource.Body('type') + #: List of QoS backend drivers supporting this QoS rule type + drivers = resource.Body('drivers') diff --git a/openstack/tests/functional/network/v2/test_qos_rule_type.py b/openstack/tests/functional/network/v2/test_qos_rule_type.py index 96c545da3..d60907851 100644 --- a/openstack/tests/functional/network/v2/test_qos_rule_type.py +++ b/openstack/tests/functional/network/v2/test_qos_rule_type.py @@ -17,6 +17,18 @@ class TestQoSRuleType(base.BaseFunctionalTest): + QOS_RULE_TYPE = "bandwidth_limit" + + def test_find(self): + sot = self.conn.network.find_qos_rule_type(self.QOS_RULE_TYPE) + self.assertEqual(self.QOS_RULE_TYPE, sot.type) + self.assertIsInstance(sot.drivers, list) + + def test_get(self): + sot = self.conn.network.get_qos_rule_type(self.QOS_RULE_TYPE) + self.assertEqual(self.QOS_RULE_TYPE, sot.type) + self.assertIsInstance(sot.drivers, list) + def test_list(self): rule_types = list(self.conn.network.qos_rule_types()) self.assertGreater(len(rule_types), 0) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 06232cd23..8681c9435 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -665,6 +665,14 @@ def test_qos_policies(self): def test_qos_policy_update(self): self.verify_update(self.proxy.update_qos_policy, qos_policy.QoSPolicy) + def test_qos_rule_type_find(self): + self.verify_find(self.proxy.find_qos_rule_type, + qos_rule_type.QoSRuleType) + + def test_qos_rule_type_get(self): + self.verify_get(self.proxy.get_qos_rule_type, + qos_rule_type.QoSRuleType) + def test_qos_rule_types(self): self.verify_list(self.proxy.qos_rule_types, qos_rule_type.QoSRuleType, paginated=False) diff --git a/openstack/tests/unit/network/v2/test_qos_rule_type.py b/openstack/tests/unit/network/v2/test_qos_rule_type.py index 59b2f6bf0..f0dc06fee 100644 --- a/openstack/tests/unit/network/v2/test_qos_rule_type.py +++ b/openstack/tests/unit/network/v2/test_qos_rule_type.py @@ -16,6 +16,22 @@ EXAMPLE = { 'type': 'bandwidth_limit', + 'drivers': [{ + 'name': 'openvswitch', + 'supported_parameters': [{ + 'parameter_values': {'start': 0, 'end': 2147483647}, + 'parameter_type': 'range', + 'parameter_name': 'max_kbps' + }, { + 'parameter_values': ['ingress', 'egress'], + 'parameter_type': 'choices', + 'parameter_name': 'direction' + }, { + 'parameter_values': {'start': 0, 'end': 2147483647}, + 'parameter_type': 'range', + 'parameter_name': 'max_burst_kbps' + }] + }] } @@ -28,7 +44,7 @@ def test_basic(self): self.assertEqual('/qos/rule-types', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) + self.assertTrue(sot.allow_get) self.assertFalse(sot.allow_update) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) @@ -36,3 +52,4 @@ def test_basic(self): def test_make_it(self): sot = qos_rule_type.QoSRuleType(**EXAMPLE) self.assertEqual(EXAMPLE['type'], sot.type) + self.assertEqual(EXAMPLE['drivers'], sot.drivers) From c821c977b937285d16adff7ba7ee154931852fdb Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Fri, 8 Sep 2017 04:05:56 +0000 Subject: [PATCH 1794/3836] Migrate machine inspection tests to requests_mock Change-Id: I53bfeff53d569a8075b40e4fb1087357f7eb4da0 --- shade/tests/unit/test_baremetal_node.py | 291 ++++++++++++++++++++++++ shade/tests/unit/test_shade_operator.py | 174 -------------- 2 files changed, 291 insertions(+), 174 deletions(-) diff --git a/shade/tests/unit/test_baremetal_node.py b/shade/tests/unit/test_baremetal_node.py index 6238a2da2..f33aa0658 100644 --- a/shade/tests/unit/test_baremetal_node.py +++ b/shade/tests/unit/test_baremetal_node.py @@ -19,6 +19,7 @@ import uuid +from shade import exc from shade.tests import fakes from shade.tests.unit import base @@ -195,6 +196,296 @@ def test_purge_node_instance_info(self): self.assert_calls() + def test_inspect_machine_fail_active(self): + self.fake_baremetal_node['provision_state'] = 'active' + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + ]) + self.assertRaises( + exc.OpenStackCloudException, + self.op_cloud.inspect_machine, + self.fake_baremetal_node['uuid'], + wait=True, + timeout=1) + + self.assert_calls() + + def test_inspect_machine_failed(self): + inspecting_node = self.fake_baremetal_node.copy() + self.fake_baremetal_node['provision_state'] = 'inspect failed' + self.fake_baremetal_node['last_error'] = 'kaboom!' + inspecting_node['provision_state'] = 'inspecting' + self.register_uris([ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'provision']), + validate=dict(json={'target': 'inspect'})), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=inspecting_node) + ]) + + self.op_cloud.inspect_machine(self.fake_baremetal_node['uuid']) + + self.assert_calls() + + def test_inspect_machine_manageable(self): + self.fake_baremetal_node['provision_state'] = 'manageable' + inspecting_node = self.fake_baremetal_node.copy() + inspecting_node['provision_state'] = 'inspecting' + self.register_uris([ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'provision']), + validate=dict(json={'target': 'inspect'})), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=inspecting_node), + ]) + self.op_cloud.inspect_machine(self.fake_baremetal_node['uuid']) + + self.assert_calls() + + def test_inspect_machine_available(self): + available_node = self.fake_baremetal_node.copy() + available_node['provision_state'] = 'available' + manageable_node = self.fake_baremetal_node.copy() + manageable_node['provision_state'] = 'manageable' + + self.register_uris([ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=available_node), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'provision']), + validate=dict(json={'target': 'manage'})), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=manageable_node), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'provision']), + validate=dict(json={'target': 'inspect'})), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=manageable_node), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'provision']), + validate=dict(json={'target': 'provide'})), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=available_node), + ]) + self.op_cloud.inspect_machine(self.fake_baremetal_node['uuid']) + + self.assert_calls() + + def test_inspect_machine_available_wait(self): + available_node = self.fake_baremetal_node.copy() + available_node['provision_state'] = 'available' + manageable_node = self.fake_baremetal_node.copy() + manageable_node['provision_state'] = 'manageable' + inspecting_node = self.fake_baremetal_node.copy() + inspecting_node['provision_state'] = 'inspecting' + + self.register_uris([ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=available_node), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'provision']), + validate=dict(json={'target': 'manage'})), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=available_node), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=manageable_node), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'provision']), + validate=dict(json={'target': 'inspect'})), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=inspecting_node), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=manageable_node), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'provision']), + validate=dict(json={'target': 'provide'})), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=available_node), + ]) + self.op_cloud.inspect_machine(self.fake_baremetal_node['uuid'], + wait=True, timeout=1) + + self.assert_calls() + + def test_inspect_machine_wait(self): + self.fake_baremetal_node['provision_state'] = 'manageable' + inspecting_node = self.fake_baremetal_node.copy() + inspecting_node['provision_state'] = 'inspecting' + self.register_uris([ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'provision']), + validate=dict(json={'target': 'inspect'})), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=inspecting_node), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=inspecting_node), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + ]) + self.op_cloud.inspect_machine(self.fake_baremetal_node['uuid'], + wait=True, timeout=1) + + self.assert_calls() + + def test_inspect_machine_inspect_failed(self): + self.fake_baremetal_node['provision_state'] = 'manageable' + inspecting_node = self.fake_baremetal_node.copy() + inspecting_node['provision_state'] = 'inspecting' + inspect_fail_node = self.fake_baremetal_node.copy() + inspect_fail_node['provision_state'] = 'inspect failed' + inspect_fail_node['last_error'] = 'Earth Imploded' + self.register_uris([ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'provision']), + validate=dict(json={'target': 'inspect'})), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=inspecting_node), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=inspect_fail_node), + ]) + self.assertRaises(exc.OpenStackCloudException, + self.op_cloud.inspect_machine, + self.fake_baremetal_node['uuid'], + wait=True, timeout=1) + + self.assert_calls() + def _test_update_machine(self, fake_node, field_name, changed=True): # The model has evolved over time, create the field if # we don't already have it. diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index 960836099..2e363cdb6 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -71,180 +71,6 @@ def test_list_nics_for_machine_failure(self, mock_client): self.assertRaises(exc.OpenStackCloudException, self.op_cloud.list_nics_for_machine, None) - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_inspect_machine_fail_active(self, mock_client): - - machine_uuid = '00000000-0000-0000-0000-000000000000' - - class active_machine(object): - uuid = machine_uuid - provision_state = "active" - - mock_client.node.get.return_value = active_machine - self.assertRaises( - shade.OpenStackCloudException, - self.op_cloud.inspect_machine, - machine_uuid, - wait=True, - timeout=1) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_inspect_machine_failed(self, mock_client): - - machine_uuid = '00000000-0000-0000-0000-000000000000' - - class inspect_failed_machine(object): - uuid = machine_uuid - provision_state = "inspect failed" - last_error = "kaboom" - - mock_client.node.get.return_value = inspect_failed_machine - self.op_cloud.inspect_machine(machine_uuid) - self.assertTrue(mock_client.node.set_provision_state.called) - self.assertEqual( - mock_client.node.set_provision_state.call_count, 1) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_inspect_machine_managable(self, mock_client): - - machine_uuid = '00000000-0000-0000-0000-000000000000' - - class manageable_machine(object): - uuid = machine_uuid - provision_state = "manageable" - - mock_client.node.get.return_value = manageable_machine - self.op_cloud.inspect_machine(machine_uuid) - self.assertEqual( - mock_client.node.set_provision_state.call_count, 1) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_inspect_machine_available(self, mock_client): - - machine_uuid = '00000000-0000-0000-0000-000000000000' - - class available_machine(object): - uuid = machine_uuid - provision_state = "available" - - class manageable_machine(object): - uuid = machine_uuid - provision_state = "manageable" - - class inspecting_machine(object): - uuid = machine_uuid - provision_state = "inspecting" - - mock_client.node.get.side_effect = iter([ - available_machine, - available_machine, - manageable_machine, - manageable_machine, - inspecting_machine]) - self.op_cloud.inspect_machine(machine_uuid) - self.assertTrue(mock_client.node.set_provision_state.called) - self.assertEqual( - mock_client.node.set_provision_state.call_count, 3) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_inspect_machine_available_wait(self, mock_client): - - machine_uuid = '00000000-0000-0000-0000-000000000000' - - class available_machine(object): - uuid = machine_uuid - provision_state = "available" - - class manageable_machine(object): - uuid = machine_uuid - provision_state = "manageable" - - class inspecting_machine(object): - uuid = machine_uuid - provision_state = "inspecting" - - mock_client.node.get.side_effect = iter([ - available_machine, - available_machine, - manageable_machine, - inspecting_machine, - manageable_machine, - available_machine, - available_machine]) - expected_return_value = dict( - uuid=machine_uuid, - provision_state="available" - ) - - return_value = self.op_cloud.inspect_machine( - machine_uuid, wait=True, timeout=1) - self.assertTrue(mock_client.node.set_provision_state.called) - self.assertEqual( - mock_client.node.set_provision_state.call_count, 3) - self.assertDictEqual(expected_return_value, return_value) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_inspect_machine_wait(self, mock_client): - - machine_uuid = '00000000-0000-0000-0000-000000000000' - - class manageable_machine(object): - uuid = machine_uuid - provision_state = "manageable" - - class inspecting_machine(object): - uuid = machine_uuid - provision_state = "inspecting" - - expected_return_value = dict( - uuid=machine_uuid, - provision_state="manageable" - ) - mock_client.node.get.side_effect = iter([ - manageable_machine, - inspecting_machine, - inspecting_machine, - manageable_machine, - manageable_machine]) - - return_value = self.op_cloud.inspect_machine( - machine_uuid, wait=True, timeout=1) - self.assertDictEqual(expected_return_value, return_value) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_inspect_machine_inspect_failed(self, mock_client): - - machine_uuid = '00000000-0000-0000-0000-000000000000' - - class manageable_machine(object): - uuid = machine_uuid - provision_state = "manageable" - last_error = None - - class inspecting_machine(object): - uuid = machine_uuid - provision_state = "inspecting" - last_error = None - - class inspect_failed_machine(object): - uuid = machine_uuid - provision_state = "inspect failed" - last_error = "kaboom" - - mock_client.node.get.side_effect = iter([ - manageable_machine, - inspecting_machine, - inspect_failed_machine]) - self.assertRaises( - shade.OpenStackCloudException, - self.op_cloud.inspect_machine, - machine_uuid, - wait=True, - timeout=1) - self.assertEqual( - mock_client.node.set_provision_state.call_count, 1) - self.assertEqual(mock_client.node.get.call_count, 3) - @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_register_machine(self, mock_client): class fake_node(object): From b817f47782b05416c7b14ae67d0786b1e380d134 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Fri, 8 Sep 2017 18:53:52 +0000 Subject: [PATCH 1795/3836] Migrate machine tests related to state transitions Migrating the machine tests where state transitions occur, such as turning power on/off, maintenance mode, hardware inspection, and general provision state changes such as activating a node and deactivating a node. Change-Id: I88663fecb79247d5473c47847f9c7405f447a80d --- shade/tests/unit/test_baremetal_node.py | 332 ++++++++++++++++++++++++ shade/tests/unit/test_shade_operator.py | 300 --------------------- 2 files changed, 332 insertions(+), 300 deletions(-) diff --git a/shade/tests/unit/test_baremetal_node.py b/shade/tests/unit/test_baremetal_node.py index f33aa0658..9aa9b5d03 100644 --- a/shade/tests/unit/test_baremetal_node.py +++ b/shade/tests/unit/test_baremetal_node.py @@ -486,6 +486,338 @@ def test_inspect_machine_inspect_failed(self): self.assert_calls() + def test_set_machine_maintenace_state(self): + self.register_uris([ + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'maintenance']), + validate=dict(json={'reason': 'no reason'})), + ]) + self.op_cloud.set_machine_maintenance_state( + self.fake_baremetal_node['uuid'], True, reason='no reason') + + self.assert_calls() + + def test_set_machine_maintenace_state_false(self): + self.register_uris([ + dict( + method='DELETE', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'maintenance'])), + ]) + self.op_cloud.set_machine_maintenance_state( + self.fake_baremetal_node['uuid'], False) + + self.assert_calls + + def test_remove_machine_from_maintenance(self): + self.register_uris([ + dict( + method='DELETE', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'maintenance'])), + ]) + self.op_cloud.remove_machine_from_maintenance( + self.fake_baremetal_node['uuid']) + + self.assert_calls() + + def test_set_machine_power_on(self): + self.register_uris([ + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'power']), + validate=dict(json={'target': 'power on'})), + ]) + return_value = self.op_cloud.set_machine_power_on( + self.fake_baremetal_node['uuid']) + self.assertIsNone(return_value) + + self.assert_calls() + + def test_set_machine_power_off(self): + self.register_uris([ + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'power']), + validate=dict(json={'target': 'power off'})), + ]) + return_value = self.op_cloud.set_machine_power_off( + self.fake_baremetal_node['uuid']) + self.assertIsNone(return_value) + + self.assert_calls() + + def test_set_machine_power_reboot(self): + self.register_uris([ + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'power']), + validate=dict(json={'target': 'rebooting'})), + ]) + return_value = self.op_cloud.set_machine_power_reboot( + self.fake_baremetal_node['uuid']) + self.assertIsNone(return_value) + + self.assert_calls() + + def test_set_machine_power_reboot_failure(self): + self.register_uris([ + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'power']), + status_code=400, + json={'error': 'invalid'}, + validate=dict(json={'target': 'rebooting'})), + ]) + self.assertRaises(exc.OpenStackCloudException, + self.op_cloud.set_machine_power_reboot, + self.fake_baremetal_node['uuid']) + + self.assert_calls() + + def test_node_set_provision_state(self): + deploy_node = self.fake_baremetal_node.copy() + deploy_node['provision_state'] = 'deploying' + active_node = self.fake_baremetal_node.copy() + active_node['provision_state'] = 'active' + self.register_uris([ + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'provision']), + validate=dict(json={'target': 'active', + 'configdrive': 'http://host/file'})), + dict(method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + ]) + self.op_cloud.node_set_provision_state( + self.fake_baremetal_node['uuid'], + 'active', + configdrive='http://host/file') + + self.assert_calls() + + def test_node_set_provision_state_wait_timeout(self): + deploy_node = self.fake_baremetal_node.copy() + deploy_node['provision_state'] = 'deploying' + active_node = self.fake_baremetal_node.copy() + active_node['provision_state'] = 'active' + self.fake_baremetal_node['provision_state'] = 'available' + self.register_uris([ + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'provision']), + validate=dict(json={'target': 'active'})), + dict(method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + dict(method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=deploy_node), + dict(method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=active_node), + ]) + return_value = self.op_cloud.node_set_provision_state( + self.fake_baremetal_node['uuid'], + 'active', + wait=True) + + self.assertEqual(active_node, return_value) + self.assert_calls() + + def test_node_set_provision_state_wait_timeout_fails(self): + # Intentionally time out. + self.register_uris([ + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'provision']), + validate=dict(json={'target': 'active'})), + dict(method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + ]) + + self.assertRaises( + exc.OpenStackCloudException, + self.op_cloud.node_set_provision_state, + self.fake_baremetal_node['uuid'], + 'active', + wait=True, + timeout=0.001) + + self.assert_calls() + + def test_node_set_provision_state_wait_success(self): + self.fake_baremetal_node['provision_state'] = 'active' + self.register_uris([ + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'provision']), + validate=dict(json={'target': 'active'})), + dict(method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + ]) + + return_value = self.op_cloud.node_set_provision_state( + self.fake_baremetal_node['uuid'], + 'active', + wait=True) + + self.assertEqual(self.fake_baremetal_node, return_value) + self.assert_calls() + + def test_node_set_provision_state_wait_failure_cases(self): + self.fake_baremetal_node['provision_state'] = 'foo failed' + self.register_uris([ + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'provision']), + validate=dict(json={'target': 'active'})), + dict(method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + ]) + + self.assertRaises( + exc.OpenStackCloudException, + self.op_cloud.node_set_provision_state, + self.fake_baremetal_node['uuid'], + 'active', + wait=True, + timeout=300) + + self.assert_calls() + + def test_node_set_provision_state_wait_provide(self): + self.fake_baremetal_node['provision_state'] = 'manageable' + available_node = self.fake_baremetal_node.copy() + available_node['provision_state'] = 'available' + self.register_uris([ + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'provision']), + validate=dict(json={'target': 'provide'})), + dict(method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + dict(method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=available_node), + ]) + return_value = self.op_cloud.node_set_provision_state( + self.fake_baremetal_node['uuid'], + 'provide', + wait=True) + + self.assertEqual(available_node, return_value) + self.assert_calls() + + def test_activate_node(self): + self.fake_baremetal_node['provision_state'] = 'active' + self.register_uris([ + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'provision']), + validate=dict(json={'target': 'active', + 'configdrive': 'http://host/file'})), + dict(method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + ]) + return_value = self.op_cloud.activate_node( + self.fake_baremetal_node['uuid'], + configdrive='http://host/file', + wait=True) + + self.assertIsNone(return_value) + self.assert_calls() + + def test_deactivate_node(self): + self.fake_baremetal_node['provision_state'] = 'available' + self.register_uris([ + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'provision']), + validate=dict(json={'target': 'deleted'})), + dict(method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + ]) + return_value = self.op_cloud.deactivate_node( + self.fake_baremetal_node['uuid'], + wait=True) + + self.assertIsNone(return_value) + self.assert_calls() + def _test_update_machine(self, fake_node, field_name, changed=True): # The model has evolved over time, create the field if # we don't already have it. diff --git a/shade/tests/unit/test_shade_operator.py b/shade/tests/unit/test_shade_operator.py index 2e363cdb6..7528178db 100644 --- a/shade/tests/unit/test_shade_operator.py +++ b/shade/tests/unit/test_shade_operator.py @@ -297,306 +297,6 @@ class fake_node(object): self.assertTrue(mock_client.port.get_by_address.called) self.assertTrue(mock_client.node.get.called) - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_set_machine_maintenace_state(self, mock_client): - mock_client.node.set_maintenance.return_value = None - node_id = 'node01' - reason = 'no reason' - self.op_cloud.set_machine_maintenance_state( - node_id, True, reason=reason) - mock_client.node.set_maintenance.assert_called_with( - node_id='node01', - state='true', - maint_reason='no reason') - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_set_machine_maintenace_state_false(self, mock_client): - mock_client.node.set_maintenance.return_value = None - node_id = 'node01' - self.op_cloud.set_machine_maintenance_state(node_id, False) - mock_client.node.set_maintenance.assert_called_with( - node_id='node01', - state='false') - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_remove_machine_from_maintenance(self, mock_client): - mock_client.node.set_maintenance.return_value = None - node_id = 'node01' - self.op_cloud.remove_machine_from_maintenance(node_id) - mock_client.node.set_maintenance.assert_called_with( - node_id='node01', - state='false') - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_set_machine_power_on(self, mock_client): - mock_client.node.set_power_state.return_value = None - node_id = 'node01' - return_value = self.op_cloud.set_machine_power_on(node_id) - self.assertIsNone(return_value) - mock_client.node.set_power_state.assert_called_with( - node_id='node01', - state='on') - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_set_machine_power_off(self, mock_client): - mock_client.node.set_power_state.return_value = None - node_id = 'node01' - return_value = self.op_cloud.set_machine_power_off(node_id) - self.assertIsNone(return_value) - mock_client.node.set_power_state.assert_called_with( - node_id='node01', - state='off') - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_set_machine_power_reboot(self, mock_client): - mock_client.node.set_power_state.return_value = None - node_id = 'node01' - return_value = self.op_cloud.set_machine_power_reboot(node_id) - self.assertIsNone(return_value) - mock_client.node.set_power_state.assert_called_with( - node_id='node01', - state='reboot') - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_set_machine_power_reboot_failure(self, mock_client): - mock_client.node.set_power_state.return_value = 'failure' - self.assertRaises(shade.OpenStackCloudException, - self.op_cloud.set_machine_power_reboot, - 'node01') - mock_client.node.set_power_state.assert_called_with( - node_id='node01', - state='reboot') - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_node_set_provision_state(self, mock_client): - - class active_node_state(object): - provision_state = "active" - - active_return_value = dict( - provision_state="active") - - mock_client.node.set_provision_state.return_value = None - mock_client.node.get.return_value = active_node_state - node_id = 'node01' - return_value = self.op_cloud.node_set_provision_state( - node_id, - 'active', - configdrive='http://127.0.0.1/file.iso') - self.assertEqual(active_return_value, return_value) - mock_client.node.set_provision_state.assert_called_with( - node_uuid='node01', - state='active', - configdrive='http://127.0.0.1/file.iso') - self.assertTrue(mock_client.node.get.called) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_node_set_provision_state_wait_timeout(self, mock_client): - class deploying_node_state(object): - provision_state = "deploying" - - class active_node_state(object): - provision_state = "active" - - class managable_node_state(object): - provision_state = "managable" - - class available_node_state(object): - provision_state = "available" - - active_return_value = dict( - provision_state="active") - mock_client.node.get.return_value = active_node_state - mock_client.node.set_provision_state.return_value = None - node_id = 'node01' - return_value = self.op_cloud.node_set_provision_state( - node_id, - 'active', - configdrive='http://127.0.0.1/file.iso', - wait=True) - - self.assertEqual(active_return_value, return_value) - mock_client.node.set_provision_state.assert_called_with( - node_uuid='node01', - state='active', - configdrive='http://127.0.0.1/file.iso') - self.assertTrue(mock_client.node.get.called) - mock_client.mock_reset() - mock_client.node.get.return_value = deploying_node_state - self.assertRaises( - shade.OpenStackCloudException, - self.op_cloud.node_set_provision_state, - node_id, - 'active', - configdrive='http://127.0.0.1/file.iso', - wait=True, - timeout=0.001) - self.assertTrue(mock_client.node.get.called) - mock_client.node.set_provision_state.assert_called_with( - node_uuid='node01', - state='active', - configdrive='http://127.0.0.1/file.iso') - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_node_set_provision_state_wait_failure(self, mock_client): - class active_node_state(object): - provision_state = "active" - - class deploy_failed_node_state(object): - provision_state = "deploy failed" - - class clean_failed_node_state(object): - provision_state = "clean failed" - - active_return_value = dict( - provision_state="active") - mock_client.node.get.return_value = active_node_state - mock_client.node.set_provision_state.return_value = None - node_id = 'node01' - return_value = self.op_cloud.node_set_provision_state( - node_id, - 'active', - configdrive='http://127.0.0.1/file.iso', - wait=True) - - self.assertEqual(active_return_value, return_value) - mock_client.node.set_provision_state.assert_called_with( - node_uuid='node01', - state='active', - configdrive='http://127.0.0.1/file.iso') - self.assertTrue(mock_client.node.get.called) - mock_client.mock_reset() - mock_client.node.get.return_value = deploy_failed_node_state - self.assertRaises( - shade.OpenStackCloudException, - self.op_cloud.node_set_provision_state, - node_id, - 'active', - configdrive='http://127.0.0.1/file.iso', - wait=True, - timeout=300) - self.assertTrue(mock_client.node.get.called) - mock_client.node.set_provision_state.assert_called_with( - node_uuid='node01', - state='active', - configdrive='http://127.0.0.1/file.iso') - mock_client.mock_reset() - mock_client.node.get.return_value = clean_failed_node_state - self.assertRaises( - shade.OpenStackCloudException, - self.op_cloud.node_set_provision_state, - node_id, - 'deleted', - wait=True, - timeout=300) - self.assertTrue(mock_client.node.get.called) - mock_client.node.set_provision_state.assert_called_with( - node_uuid='node01', - state='deleted', - configdrive=None) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_node_set_provision_state_wait_provide(self, mock_client): - - class managable_node_state(object): - provision_state = "managable" - - class available_node_state(object): - provision_state = "available" - - node_provide_return_value = dict( - provision_state="available") - - mock_client.node.get.side_effect = iter([ - managable_node_state, - available_node_state]) - return_value = self.op_cloud.node_set_provision_state( - 'test_node', - 'provide', - wait=True) - self.assertEqual(mock_client.node.get.call_count, 2) - self.assertDictEqual(node_provide_return_value, return_value) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - @mock.patch.object(shade._utils, '_iterate_timeout') - def test_activate_node(self, mock_timeout, mock_client): - mock_client.node.set_provision_state.return_value = None - node_id = 'node02' - return_value = self.op_cloud.activate_node( - node_id, - configdrive='http://127.0.0.1/file.iso') - self.assertIsNone(return_value) - mock_client.node.set_provision_state.assert_called_with( - node_uuid='node02', - state='active', - configdrive='http://127.0.0.1/file.iso') - self.assertFalse(mock_timeout.called) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_activate_node_timeout(self, mock_client): - - class active_node_state(object): - provision_state = 'active' - - class available_node_state(object): - provision_state = 'available' - - mock_client.node.get.side_effect = iter([ - available_node_state, - active_node_state]) - - mock_client.node.set_provision_state.return_value = None - node_id = 'node04' - return_value = self.op_cloud.activate_node( - node_id, - configdrive='http://127.0.0.1/file.iso', - wait=True, - timeout=2) - self.assertIsNone(return_value) - mock_client.node.set_provision_state.assert_called_with( - node_uuid='node04', - state='active', - configdrive='http://127.0.0.1/file.iso') - self.assertEqual(mock_client.node.get.call_count, 2) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - @mock.patch.object(shade._utils, '_iterate_timeout') - def test_deactivate_node(self, mock_timeout, mock_client): - mock_client.node.set_provision_state.return_value = None - node_id = 'node03' - return_value = self.op_cloud.deactivate_node( - node_id, wait=False) - self.assertIsNone(return_value) - mock_client.node.set_provision_state.assert_called_with( - node_uuid='node03', - state='deleted', - configdrive=None) - self.assertFalse(mock_timeout.called) - - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - def test_deactivate_node_timeout(self, mock_client): - - class active_node_state(object): - provision_state = 'active' - - class deactivated_node_state(object): - provision_state = 'available' - - mock_client.node.get.side_effect = iter([ - active_node_state, - deactivated_node_state]) - - mock_client.node.set_provision_state.return_value = None - node_id = 'node03' - return_value = self.op_cloud.deactivate_node( - node_id, wait=True, timeout=2) - self.assertIsNone(return_value) - mock_client.node.set_provision_state.assert_called_with( - node_uuid='node03', - state='deleted', - configdrive=None) - self.assertEqual(mock_client.node.get.call_count, 2) - @mock.patch.object(shade.OpenStackCloud, '_image_client') def test_get_image_name(self, mock_client): From 73ef1f549c922759ba6c74f2bb6c0f1a4c72e6c2 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 4 Aug 2017 15:21:38 -0400 Subject: [PATCH 1796/3836] Switch to using stestr stestr is a fork of testrepository, designed specifically to concentrate on being a dedicated test runner. [1] The testrepository project is basically not active anymore and has several long standing bugs and useablility issues. stestr is still actively maintained and fixes a large number of those issues with testrepository. [1] https://github.com/mtreinish/stestr Change-Id: I9b8049fd2c78d8f90aed5a8b35c0a9b40ad8c463 --- .gitignore | 1 + .stestr.conf | 3 ++ .testr.conf | 7 ---- .../tests/functional/hooks/post_test_hook.sh | 2 +- test-requirements.txt | 2 +- tox.ini | 42 ++++++++++--------- 6 files changed, 29 insertions(+), 28 deletions(-) create mode 100644 .stestr.conf delete mode 100644 .testr.conf diff --git a/.gitignore b/.gitignore index 09132e5f7..1f0139551 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ cover .tox nosetests.xml .testrepository +.stestr # Translations *.mo diff --git a/.stestr.conf b/.stestr.conf new file mode 100644 index 000000000..d90a44466 --- /dev/null +++ b/.stestr.conf @@ -0,0 +1,3 @@ +[DEFAULT] +test_path=./shade/tests/unit +top_dir=./ diff --git a/.testr.conf b/.testr.conf deleted file mode 100644 index 3ce7ec2a9..000000000 --- a/.testr.conf +++ /dev/null @@ -1,7 +0,0 @@ -[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:-./shade/tests/unit} $LISTOPT $IDOPTION -test_id_option=--load-list $IDFILE -test_list_option=--list diff --git a/shade/tests/functional/hooks/post_test_hook.sh b/shade/tests/functional/hooks/post_test_hook.sh index 6be3fe42c..52037cd38 100755 --- a/shade/tests/functional/hooks/post_test_hook.sh +++ b/shade/tests/functional/hooks/post_test_hook.sh @@ -45,7 +45,7 @@ echo "Running shade functional test suite" set +e sudo -E -H -u jenkins tox -e$tox_env EXIT_CODE=$? -sudo testr last --subunit > $WORKSPACE/tempest.subunit +sudo stestr last --subunit > $WORKSPACE/tempest.subunit .tox/$tox_env/bin/pbr freeze set -e diff --git a/test-requirements.txt b/test-requirements.txt index e7edec71a..f726c02f2 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -11,7 +11,7 @@ openstackdocstheme>=1.16.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 requests-mock>=1.1.0 # Apache-2.0 sphinx>=1.6.2 # BSD -testrepository>=0.0.18 # Apache-2.0/BSD +stestr>=1.0.0 # Apache-2.0 testscenarios>=0.4 # Apache-2.0/BSD testtools>=1.4.0 # MIT reno>=2.5.0 # Apache-2.0 diff --git a/tox.ini b/tox.ini index f6fd3f783..52a9274c1 100644 --- a/tox.ini +++ b/tox.ini @@ -9,31 +9,28 @@ basepython = {env:SHADE_TOX_PYTHON:python2} passenv = UPPER_CONSTRAINTS_FILE install_command = {toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} setenv = - VIRTUAL_ENV={envdir} - LANG=en_US.UTF-8 - LANGUAGE=en_US:en - LC_ALL=C - BRANCH_NAME=master - CLIENT_NAME=shade + VIRTUAL_ENV={envdir} + LANG=en_US.UTF-8 + LANGUAGE=en_US:en + LC_ALL=C + BRANCH_NAME=master + CLIENT_NAME=shade deps = -r{toxinidir}/test-requirements.txt -commands = python setup.py testr --slowest --testr-args='{posargs}' +commands = stestr run {posargs} + stestr slowest [testenv:functional] -setenv = - {[testenv]setenv} - OS_TEST_PATH = ./shade/tests/functional passenv = OS_* SHADE_* UPPER_CONSTRAINTS_FILE -commands = python setup.py testr --slowest --testr-args='--concurrency=1 {posargs}' +commands = stestr --test-path ./shade/tests/functional run --serial {posargs} + stestr slowest [testenv:functional-tips] -setenv = - {[testenv]setenv} - OS_TEST_PATH = ./shade/tests/functional passenv = OS_* SHADE_* UPPER_CONSTRAINTS_FILE whitelist_externals = bash commands = - bash -x {toxinidir}/extras/install-tips.sh - python setup.py testr --slowest --testr-args='--concurrency=1 {posargs}' + bash -x {toxinidir}/extras/install-tips.sh + stestr --test-path ./shade/tests/functional run --serial {posargs} + stestr slowest [testenv:pep8] commands = flake8 shade @@ -44,11 +41,18 @@ commands = {posargs} [testenv:debug] whitelist_externals = find commands = - find . -type f -name "*.pyc" -delete - oslo_debug_helper {posargs} + find . -type f -name "*.pyc" -delete + oslo_debug_helper {posargs} [testenv:cover] -commands = python setup.py testr --coverage --testr-args='{posargs}' +setenv = + {[testenv]setenv} + PYTHON=coverage run --source shade --parallel-mode +commands = + stestr run {posargs} + coverage combine + coverage html -d cover + coverage xml -o cover/coverage.xml [testenv:ansible] # Need to pass some env vars for the Ansible playbooks From ff709954362cf37c21bbe5ede8c4d4ed81d64f48 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 2 Sep 2017 15:24:58 -0500 Subject: [PATCH 1797/3836] Switch to normal tox-py35 job Change-Id: Id307bd15b0c1b43ce7fba15ac5230c7d26b2a5bc --- .zuul.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index f55ae2e2d..edf97c5a3 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -2,4 +2,4 @@ name: openstack-infra/shade check: jobs: - - tox-py35-constraints + - tox-py35 From 960a27447c3c18ba9a68dfe91e01cc629cd3b16f Mon Sep 17 00:00:00 2001 From: Paul Belanger Date: Fri, 1 Sep 2017 17:43:47 -0400 Subject: [PATCH 1798/3836] Add openstack-doc-build to shade Validate shade is able to build docs using openstack-doc-build with constraints. Change-Id: If419a417d717fe4d5a6da452adf65f63bb8d93e6 Depends-On: Ib998f83135a4fcb796a205e52c435ab29b7cab7b Signed-off-by: Paul Belanger --- .zuul.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.zuul.yaml b/.zuul.yaml index edf97c5a3..382c32273 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -2,4 +2,5 @@ name: openstack-infra/shade check: jobs: + - openstack-doc-build - tox-py35 From 8daf38fb6ace805383fae63ac60b500756decd0a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 7 Sep 2017 07:46:06 -0500 Subject: [PATCH 1799/3836] Convert test_baremetal_machine_patch to testscenarios We have a helper library that handles this sort of thing. Change-Id: I58f50663ab9c026918088845cceecf78ff15d8c8 --- shade/tests/unit/test_baremetal_node.py | 151 ++++++++++-------------- 1 file changed, 65 insertions(+), 86 deletions(-) diff --git a/shade/tests/unit/test_baremetal_node.py b/shade/tests/unit/test_baremetal_node.py index 9aa9b5d03..d76b37597 100644 --- a/shade/tests/unit/test_baremetal_node.py +++ b/shade/tests/unit/test_baremetal_node.py @@ -19,6 +19,8 @@ import uuid +from testscenarios import load_tests_apply_scenarios as load_tests # noqa + from shade import exc from shade.tests import fakes from shade.tests.unit import base @@ -818,115 +820,92 @@ def test_deactivate_node(self): self.assertIsNone(return_value) self.assert_calls() - def _test_update_machine(self, fake_node, field_name, changed=True): + def test_update_machine_patch_no_action(self): + self.register_uris([dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + ]) + # NOTE(TheJulia): This is just testing mechanics. + update_dict = self.op_cloud.update_machine( + self.fake_baremetal_node['uuid']) + self.assertIsNone(update_dict['changes']) + self.assertDictEqual(self.fake_baremetal_node, update_dict['node']) + + self.assert_calls() + + +class TestUpdateMachinePatch(base.IronicTestCase): + # NOTE(TheJulia): As appears, and mordred describes, + # this class utilizes black magic, which ultimately + # results in additional test runs being executed with + # the scenario name appended. Useful for lots of + # variables that need to be tested. + + def setUp(self): + super(TestUpdateMachinePatch, self).setUp() + self.fake_baremetal_node = fakes.make_fake_machine( + self.name, self.uuid) + + def test_update_machine_patch(self): # The model has evolved over time, create the field if # we don't already have it. - if field_name not in fake_node: - fake_node[field_name] = None - value_to_send = fake_node[field_name] - if changed: + if self.field_name not in self.fake_baremetal_node: + self.fake_baremetal_node[self.field_name] = None + value_to_send = self.fake_baremetal_node[self.field_name] + if self.changed: value_to_send = 'meow' uris = [dict( method='GET', uri=self.get_mock_url( resource='nodes', - append=[fake_node['uuid']]), - json=fake_node), + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), ] - if changed: + if self.changed: test_patch = [{ 'op': 'replace', - 'path': '/' + field_name, + 'path': '/' + self.field_name, 'value': 'meow'}] uris.append( dict( method='PATCH', uri=self.get_mock_url( resource='nodes', - append=[fake_node['uuid']]), - json=fake_node, + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node, validate=dict(json=test_patch))) self.register_uris(uris) - call_args = {field_name: value_to_send} + call_args = {self.field_name: value_to_send} update_dict = self.op_cloud.update_machine( - fake_node['uuid'], **call_args) + self.fake_baremetal_node['uuid'], **call_args) - if not changed: + if not self.changed: self.assertIsNone(update_dict['changes']) - self.assertDictEqual(fake_node, update_dict['node']) - - self.assert_calls() - - def test_update_machine_patch_name(self): - self._test_update_machine(self.fake_baremetal_node, - 'name', False) - - def test_update_machine_patch_chassis_uuid(self): - self._test_update_machine(self.fake_baremetal_node, - 'chassis_uuid', False) - - def test_update_machine_patch_driver(self): - self._test_update_machine(self.fake_baremetal_node, - 'driver', False) - - def test_update_machine_patch_driver_info(self): - self._test_update_machine(self.fake_baremetal_node, - 'driver_info', False) - - def test_update_machine_patch_instance_info(self): - self._test_update_machine(self.fake_baremetal_node, - 'instance_info', False) - - def test_update_machine_patch_instance_uuid(self): - self._test_update_machine(self.fake_baremetal_node, - 'instance_uuid', False) - - def test_update_machine_patch_properties(self): - self._test_update_machine(self.fake_baremetal_node, - 'properties', False) - - def test_update_machine_patch_update_name(self): - self._test_update_machine(self.fake_baremetal_node, - 'name', True) - - def test_update_machine_patch_update_chassis_uuid(self): - self._test_update_machine(self.fake_baremetal_node, - 'chassis_uuid', True) - - def test_update_machine_patch_update_driver(self): - self._test_update_machine(self.fake_baremetal_node, - 'driver', True) - - def test_update_machine_patch_update_driver_info(self): - self._test_update_machine(self.fake_baremetal_node, - 'driver_info', True) - - def test_update_machine_patch_update_instance_info(self): - self._test_update_machine(self.fake_baremetal_node, - 'instance_info', True) - - def test_update_machine_patch_update_instance_uuid(self): - self._test_update_machine(self.fake_baremetal_node, - 'instance_uuid', True) - - def test_update_machine_patch_update_properties(self): - self._test_update_machine(self.fake_baremetal_node, - 'properties', True) - - def test_update_machine_patch_no_action(self): - self.register_uris([dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - ]) - # NOTE(TheJulia): This is just testing mechanics. - update_dict = self.op_cloud.update_machine( - self.fake_baremetal_node['uuid']) - self.assertIsNone(update_dict['changes']) self.assertDictEqual(self.fake_baremetal_node, update_dict['node']) self.assert_calls() + + scenarios = [ + ('chassis_uuid', dict(field_name='chassis_uuid', changed=False)), + ('chassis_uuid_changed', + dict(field_name='chassis_uuid', changed=True)), + ('driver', dict(field_name='driver', changed=False)), + ('driver_changed', dict(field_name='driver', changed=True)), + ('driver_info', dict(field_name='driver_info', changed=False)), + ('driver_info_changed', dict(field_name='driver_info', changed=True)), + ('instance_info', dict(field_name='instance_info', changed=False)), + ('instance_info_changed', + dict(field_name='instance_info', changed=True)), + ('instance_uuid', dict(field_name='instance_uuid', changed=False)), + ('instance_uuid_changed', + dict(field_name='instance_uuid', changed=True)), + ('name', dict(field_name='name', changed=False)), + ('name_changed', dict(field_name='name', changed=True)), + ('properties', dict(field_name='properties', changed=False)), + ('properties_changed', dict(field_name='properties', changed=True)) + ] From 605301f99af38d388af3b0500e734c8afc2528a8 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 8 Sep 2017 18:45:22 -0500 Subject: [PATCH 1800/3836] Switch to constraints version of tox job Change-Id: I4c874df5b0c7f6d28b4dea6b25b32aba95cf6db9 --- .zuul.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index 382c32273..c5ab67446 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -3,4 +3,4 @@ check: jobs: - openstack-doc-build - - tox-py35 + - openstack-tox-py35 From f3e62250438c347424969fafe3b90a04ecf62890 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Sun, 10 Sep 2017 10:17:55 -0300 Subject: [PATCH 1801/3836] Reorganize endpoint create code Group the code for v2.0 in a single if statement. Idem for v3 Change-Id: I12f9a571b33b38e9e0e75fe11c877cca4905e78c --- shade/operatorcloud.py | 91 ++++++++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 34 deletions(-) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index c5f4d4e92..0d00f0405 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -918,9 +918,9 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, endpoints = [] endpoint_args = [] - if url: - urlkwargs = {} - if self._is_client_version('identity', 2): + if self._is_client_version('identity', 2): + if url: + urlkwargs = {} if interface != 'public': raise OpenStackCloudException( "Error adding endpoint for service {service}." @@ -930,25 +930,53 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, " url and interface".format( service=service_name_or_id)) urlkwargs['{}url'.format(interface)] = url + endpoint_args.append(urlkwargs) else: - urlkwargs['url'] = url - urlkwargs['interface'] = interface - endpoint_args.append(urlkwargs) - else: - # NOTE(notmorgan): This is done as a list of tuples to ensure we - # have a deterministic order we try and create the endpoint - # elements. This is done mostly so that it is possible to test the - # requests themselves. - expected_endpoints = [('public', public_url), - ('internal', internal_url), - ('admin', admin_url)] - if self._is_client_version('identity', 2): + # NOTE(notmorgan): This is done as a list of tuples to ensure + # we have a deterministic order we try and create the endpoint + # elements. This is done mostly so that it is possible to test + # the requests themselves. + expected_endpoints = [('public', public_url), + ('internal', internal_url), + ('admin', admin_url)] urlkwargs = {} for interface, url in expected_endpoints: if url: urlkwargs['{}url'.format(interface)] = url endpoint_args.append(urlkwargs) + + kwargs['service_id'] = service['id'] + # Keystone v2 requires 'region' arg even if it is None + kwargs['region'] = region + + # TODO(mordred) When this changes to REST, force interface=admin + # in the adapter call + with _utils.shade_exceptions( + "Failed to create endpoint for service" + " {service}".format(service=service['name']) + ): + for args in endpoint_args: + # NOTE(SamYaple): Add shared kwargs to endpoint args + args.update(kwargs) + endpoint = self.manager.submit_task( + _tasks.EndpointCreate(**args) + ) + endpoints.append(endpoint) + return endpoints + else: + if url: + urlkwargs = {} + urlkwargs['url'] = url + urlkwargs['interface'] = interface + endpoint_args.append(urlkwargs) else: + # NOTE(notmorgan): This is done as a list of tuples to ensure + # we have a deterministic order we try and create the endpoint + # elements. This is done mostly so that it is possible to test + # the requests themselves. + expected_endpoints = [('public', public_url), + ('internal', internal_url), + ('admin', admin_url)] for interface, url in expected_endpoints: if url: urlkwargs = {} @@ -956,30 +984,25 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, urlkwargs['interface'] = interface endpoint_args.append(urlkwargs) - if self._is_client_version('identity', 2): - kwargs['service_id'] = service['id'] - # Keystone v2 requires 'region' arg even if it is None - kwargs['region'] = region - else: kwargs['service'] = service['id'] kwargs['enabled'] = enabled if region is not None: kwargs['region'] = region - # TODO(mordred) When this changes to REST, force interface=admin - # in the adapter call - with _utils.shade_exceptions( - "Failed to create endpoint for service" - " {service}".format(service=service['name']) - ): - for args in endpoint_args: - # NOTE(SamYaple): Add shared kwargs to endpoint args - args.update(kwargs) - endpoint = self.manager.submit_task( - _tasks.EndpointCreate(**args) - ) - endpoints.append(endpoint) - return endpoints + # TODO(mordred) When this changes to REST, force interface=admin + # in the adapter call + with _utils.shade_exceptions( + "Failed to create endpoint for service" + " {service}".format(service=service['name']) + ): + for args in endpoint_args: + # NOTE(SamYaple): Add shared kwargs to endpoint args + args.update(kwargs) + endpoint = self.manager.submit_task( + _tasks.EndpointCreate(**args) + ) + endpoints.append(endpoint) + return endpoints @_utils.valid_kwargs('enabled', 'service_name_or_id', 'url', 'interface', 'region') From 941e179e535a2a4ba0df8387abe2d4391644b36c Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Sun, 10 Sep 2017 10:45:44 -0300 Subject: [PATCH 1802/3836] Refactor the create endpoint code The code is rather complex, this patches improves its readability Change-Id: I0b79217b89fdd636cbf8da241cd7e1af24f07371 --- shade/operatorcloud.py | 98 ++++++++++++++++++------------------------ 1 file changed, 42 insertions(+), 56 deletions(-) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 0d00f0405..8a7f6ff9c 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -916,11 +916,9 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, raise OpenStackCloudException("service {service} not found".format( service=service_name_or_id)) - endpoints = [] - endpoint_args = [] if self._is_client_version('identity', 2): if url: - urlkwargs = {} + # v2.0 in use, v3-like arguments, one endpoint created if interface != 'public': raise OpenStackCloudException( "Error adding endpoint for service {service}." @@ -929,25 +927,20 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, " internal_url, admin_url parameters instead of" " url and interface".format( service=service_name_or_id)) - urlkwargs['{}url'.format(interface)] = url - endpoint_args.append(urlkwargs) + endpoint_args = {'publicurl': url} else: - # NOTE(notmorgan): This is done as a list of tuples to ensure - # we have a deterministic order we try and create the endpoint - # elements. This is done mostly so that it is possible to test - # the requests themselves. - expected_endpoints = [('public', public_url), - ('internal', internal_url), - ('admin', admin_url)] - urlkwargs = {} - for interface, url in expected_endpoints: - if url: - urlkwargs['{}url'.format(interface)] = url - endpoint_args.append(urlkwargs) - - kwargs['service_id'] = service['id'] - # Keystone v2 requires 'region' arg even if it is None - kwargs['region'] = region + # v2.0 in use, v2.0-like arguments, one endpoint created + endpoint_args = {} + if public_url: + endpoint_args.update({'publicurl': public_url}) + if internal_url: + endpoint_args.update({'internalurl': internal_url}) + if admin_url: + endpoint_args.update({'adminurl': admin_url}) + + # keystone v2.0 requires 'region' arg even if it is None + endpoint_args.update( + {'service_id': service['id'], 'region': region}) # TODO(mordred) When this changes to REST, force interface=admin # in the adapter call @@ -955,49 +948,42 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, "Failed to create endpoint for service" " {service}".format(service=service['name']) ): - for args in endpoint_args: - # NOTE(SamYaple): Add shared kwargs to endpoint args - args.update(kwargs) - endpoint = self.manager.submit_task( - _tasks.EndpointCreate(**args) - ) - endpoints.append(endpoint) - return endpoints + endpoint = self.manager.submit_task( + _tasks.EndpointCreate(**endpoint_args) + ) + return [endpoint] else: + endpoints_args = [] if url: - urlkwargs = {} - urlkwargs['url'] = url - urlkwargs['interface'] = interface - endpoint_args.append(urlkwargs) + # v3 in use, v3-like arguments, one endpoint created + endpoints_args.append( + {'url': url, 'interface': interface, + 'service': service['id'], 'enabled': enabled, + 'region': region}) else: - # NOTE(notmorgan): This is done as a list of tuples to ensure - # we have a deterministic order we try and create the endpoint - # elements. This is done mostly so that it is possible to test - # the requests themselves. - expected_endpoints = [('public', public_url), - ('internal', internal_url), - ('admin', admin_url)] - for interface, url in expected_endpoints: - if url: - urlkwargs = {} - urlkwargs['url'] = url - urlkwargs['interface'] = interface - endpoint_args.append(urlkwargs) - - kwargs['service'] = service['id'] - kwargs['enabled'] = enabled - if region is not None: - kwargs['region'] = region + # v3 in use, v2.0-like arguments, one endpoint created for each + # interface url provided + endpoint_args = {'region': region, 'enabled': enabled, + 'service': service['id']} + if public_url: + endpoint_args.update({'url': public_url, + 'interface': 'public'}) + endpoints_args.append(endpoint_args.copy()) + if internal_url: + endpoint_args.update({'url': internal_url, + 'interface': 'internal'}) + endpoints_args.append(endpoint_args.copy()) + if admin_url: + endpoint_args.update({'url': admin_url, + 'interface': 'admin'}) + endpoints_args.append(endpoint_args.copy()) - # TODO(mordred) When this changes to REST, force interface=admin - # in the adapter call with _utils.shade_exceptions( "Failed to create endpoint for service" " {service}".format(service=service['name']) ): - for args in endpoint_args: - # NOTE(SamYaple): Add shared kwargs to endpoint args - args.update(kwargs) + endpoints = [] + for args in endpoints_args: endpoint = self.manager.submit_task( _tasks.EndpointCreate(**args) ) From d0ca641f8e42893cffcb98bb960cf4d98941f0a4 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Sun, 10 Sep 2017 10:45:44 -0300 Subject: [PATCH 1803/3836] De-client-ify Endpoint Create Change-Id: I916dea906886955e8c7d074d5d1d69f29ff5bdf3 --- shade/operatorcloud.py | 40 +++++++++++++----------------- shade/tests/unit/base.py | 3 ++- shade/tests/unit/test_endpoints.py | 20 --------------- 3 files changed, 19 insertions(+), 44 deletions(-) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 8a7f6ff9c..c8bc21381 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -942,29 +942,25 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, endpoint_args.update( {'service_id': service['id'], 'region': region}) - # TODO(mordred) When this changes to REST, force interface=admin - # in the adapter call - with _utils.shade_exceptions( - "Failed to create endpoint for service" - " {service}".format(service=service['name']) - ): - endpoint = self.manager.submit_task( - _tasks.EndpointCreate(**endpoint_args) - ) - return [endpoint] + data = self._identity_client.post( + '/endpoints', json={'endpoint': endpoint_args}, + endpoint_filter={'interface': 'admin'}, + error_message=("Failed to create endpoint for service" + " {service}".format(service=service['name']))) + return [self._get_and_munchify('endpoint', data)] else: endpoints_args = [] if url: # v3 in use, v3-like arguments, one endpoint created endpoints_args.append( {'url': url, 'interface': interface, - 'service': service['id'], 'enabled': enabled, + 'service_id': service['id'], 'enabled': enabled, 'region': region}) else: # v3 in use, v2.0-like arguments, one endpoint created for each # interface url provided endpoint_args = {'region': region, 'enabled': enabled, - 'service': service['id']} + 'service_id': service['id']} if public_url: endpoint_args.update({'url': public_url, 'interface': 'public'}) @@ -978,17 +974,15 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, 'interface': 'admin'}) endpoints_args.append(endpoint_args.copy()) - with _utils.shade_exceptions( - "Failed to create endpoint for service" - " {service}".format(service=service['name']) - ): - endpoints = [] - for args in endpoints_args: - endpoint = self.manager.submit_task( - _tasks.EndpointCreate(**args) - ) - endpoints.append(endpoint) - return endpoints + endpoints = [] + error_msg = ("Failed to create endpoint for service" + " {service}".format(service=service['name'])) + for args in endpoints_args: + data = self._identity_client.post( + '/endpoints', json={'endpoint': args}, + error_message=error_msg) + endpoints.append(self._get_and_munchify('endpoint', data)) + return endpoints @_utils.valid_kwargs('enabled', 'service_name_or_id', 'url', 'interface', 'region') diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index bbbcab7b0..28b531321 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -393,7 +393,8 @@ def _get_endpoint_v2_data(self, service_id=None, region=None, request = response.copy() request.pop('id') for u in ('publicURL', 'internalURL', 'adminURL'): - request[u.lower()] = request.pop(u, None) + if request.get(u): + request[u.lower()] = request.pop(u) return _EndpointDataV2(endpoint_id, service_id, region, public_url, internal_url, admin_url, v3_endpoints, {'endpoint': response}, {'endpoint': request}) diff --git a/shade/tests/unit/test_endpoints.py b/shade/tests/unit/test_endpoints.py index c1ab3f5fd..3884cad8d 100644 --- a/shade/tests/unit/test_endpoints.py +++ b/shade/tests/unit/test_endpoints.py @@ -155,11 +155,6 @@ def test_create_endpoint_v3(self): json=public_endpoint_data_disabled.json_response, validate=dict( json=public_endpoint_data_disabled.json_request)), - dict(method='GET', - uri=self.get_mock_url( - append=[public_endpoint_data_disabled.endpoint_id]), - status_code=200, - json=public_endpoint_data_disabled.json_response), dict(method='GET', uri=self.get_mock_url(resource='services'), status_code=200, @@ -170,31 +165,16 @@ def test_create_endpoint_v3(self): status_code=200, json=public_endpoint_data.json_response, validate=dict(json=public_endpoint_data.json_request)), - dict(method='GET', - uri=self.get_mock_url( - append=[public_endpoint_data.endpoint_id]), - status_code=200, - json=public_endpoint_data.json_response), dict(method='POST', uri=self.get_mock_url(), status_code=200, json=internal_endpoint_data.json_response, validate=dict(json=internal_endpoint_data.json_request)), - dict(method='GET', - uri=self.get_mock_url( - append=[internal_endpoint_data.endpoint_id]), - status_code=200, - json=internal_endpoint_data.json_response), dict(method='POST', uri=self.get_mock_url(), status_code=200, json=admin_endpoint_data.json_response, validate=dict(json=admin_endpoint_data.json_request)), - dict(method='GET', - uri=self.get_mock_url( - append=[admin_endpoint_data.endpoint_id]), - status_code=200, - json=admin_endpoint_data.json_response), ]) endpoints = self.op_cloud.create_endpoint( From 97b98c97e779a29587e2155bbd6b1b7f7056e746 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Mon, 11 Sep 2017 14:37:32 -0300 Subject: [PATCH 1804/3836] Remove keystoneclient dependency Since all the keystone client calls have been moved to direct REST calls to keystone server, the dependency can be removed. If someone is getting a legacy keystone client from shade directly, they will be warned to 'pip install keystoneclient' themselves. Change-Id: I653dead3992be09c003aab911dda7cbf892cc9c1 --- requirements.txt | 1 - shade/_legacy_clients.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index a6ac179e0..105793a20 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,7 +21,6 @@ iso8601>=0.1.11 # MIT keystoneauth1>=3.2.0 # Apache-2.0 netifaces>=0.10.4 # MIT -python-keystoneclient>=3.8.0 # Apache-2.0 python-ironicclient>=1.14.0 # Apache-2.0 dogpile.cache>=0.6.2 # BSD diff --git a/shade/_legacy_clients.py b/shade/_legacy_clients.py index b5ae535aa..b41835f45 100644 --- a/shade/_legacy_clients.py +++ b/shade/_legacy_clients.py @@ -103,7 +103,7 @@ def keystone_client(self): return self._create_legacy_client( 'keystone', 'identity', client_class=client_class.Client, - deprecated=False, + deprecated=True, endpoint=self._identity_client.get_endpoint(), endpoint_override=self._identity_client.get_endpoint()) From 25ef7bf24446905e3a33083202acaeec58cb04dd Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 11 Sep 2017 21:34:25 +0000 Subject: [PATCH 1805/3836] Updated from global requirements Change-Id: I3833b1f450b969e54a9869af9effe2c82a4b4a83 --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index a6ac179e0..01369e3f8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,8 +6,8 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 munch>=2.1.0 # MIT decorator>=3.4.0 # BSD jmespath>=0.9.0 # MIT -jsonpatch>=1.1 # BSD -ipaddress>=1.0.7;python_version<'3.3' # PSF +jsonpatch>=1.16 # BSD +ipaddress>=1.0.16;python_version<'3.3' # PSF os-client-config>=1.28.0 # Apache-2.0 # These two are here to prevent issues with version pin mismatches from our # client library transitive depends. From 1e5dbb59ceb1ce460a21d9ada865cca0aeb91ca1 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 11 Sep 2017 21:49:24 +0000 Subject: [PATCH 1806/3836] Updated from global requirements Change-Id: I4567ef7ac70b85d5542cd2e4ad1fd5c375c03e5a --- requirements.txt | 2 +- test-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 293ff6354..b4d574ed5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # 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 -jsonpatch>=1.1 # BSD +jsonpatch>=1.16 # BSD six>=1.9.0 # MIT stevedore>=1.20.0 # Apache-2.0 os-client-config>=1.28.0 # Apache-2.0 diff --git a/test-requirements.txt b/test-requirements.txt index cb5107be4..877a7bc5b 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,7 +10,7 @@ fixtures>=3.0.0 # Apache-2.0/BSD mock>=2.0.0 # BSD python-subunit>=0.0.18 # Apache-2.0/BSD openstackdocstheme>=1.16.0 # Apache-2.0 -os-testr>=0.8.0 # Apache-2.0 +os-testr>=0.8.2 # Apache-2.0 requests>=2.14.2 # Apache-2.0 requests-mock>=1.1.0 # Apache-2.0 sphinx>=1.6.2 # BSD From ec797b62cc7404db3a70d466e2d80cc00b673b39 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 4 Sep 2017 09:01:42 -0500 Subject: [PATCH 1807/3836] Remove use of legacy keystone client in functional tests We use it here to verify the new user account information works. We can do the same thing by just grabbing a catalog. Change-Id: I55c981a29f4386a53472a88c1618b390f4b64a08 --- shade/tests/functional/test_users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shade/tests/functional/test_users.py b/shade/tests/functional/test_users.py index 4bc3b885c..985853854 100644 --- a/shade/tests/functional/test_users.py +++ b/shade/tests/functional/test_users.py @@ -134,7 +134,7 @@ def test_update_user_password(self): 'Member', user=user['id'], project='demo', wait=True) self.assertIsNotNone(operator_cloud( cloud=self._demo_name, - username=user_name, password='new_secret').keystone_client) + username=user_name, password='new_secret').service_catalog) def test_users_and_groups(self): i_ver = self.operator_cloud.cloud_config.get_api_version('identity') From e1cafe3aa22809b34515340b05452ba8457dbd96 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 12 Sep 2017 13:12:43 -0600 Subject: [PATCH 1808/3836] Remove EndpointCreate and _project_manager They're not used anymore. Change-Id: I9858bbd4d753d16c113e7df70a5f316ccbce6493 --- shade/_tasks.py | 5 ----- shade/openstackcloud.py | 9 --------- 2 files changed, 14 deletions(-) diff --git a/shade/_tasks.py b/shade/_tasks.py index a826ac92b..a60d27139 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -95,8 +95,3 @@ def main(self, client): class MachineSetProvision(task_manager.Task): def main(self, client): return client.ironic_client.node.set_provision_state(**self.args) - - -class EndpointCreate(task_manager.Task): - def main(self, client): - return client.keystone_client.endpoints.create(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index a253b8f02..7727dddd6 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -670,15 +670,6 @@ def _get_current_location(self, project_id=None, zone=None): project=self._get_project_info(project_id), ) - @property - def _project_manager(self): - # Keystone v2 calls this attribute tenants - # Keystone v3 calls it projects - # Yay for usable APIs! - if self._is_client_version('identity', 2): - return self.keystone_client.tenants - return self.keystone_client.projects - def _get_project_id_param_dict(self, name_or_id): if name_or_id: project = self.get_project(name_or_id) From f545e8ebeafbfa56a1c337ab1c369e6f0be2adf0 Mon Sep 17 00:00:00 2001 From: Sam Yaple Date: Tue, 12 Sep 2017 17:33:36 -0400 Subject: [PATCH 1809/3836] Fix search_groups Change-Id: Ibb79be4ea42994b481ff484d5ae2b0aa0a97f4db Related-Id: I5be672c3eae5e013525cc7c0a4d73f9166f379ba --- shade/operatorcloud.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 961a2e30d..53ba8cd66 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1267,8 +1267,7 @@ def search_groups(self, name_or_id=None, filters=None, **kwargs): the openstack API call. """ groups = self.list_groups(**kwargs) - return _utils._filter_list(groups, name_or_id, filters, - **kwargs) + return _utils._filter_list(groups, name_or_id, filters) @_utils.valid_kwargs('domain_id') def get_group(self, name_or_id, filters=None, **kwargs): From 9de98c4d6fa3db03366278933f08706b392bca76 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 13 Sep 2017 00:15:34 +0000 Subject: [PATCH 1810/3836] Updated from global requirements Change-Id: I6aa99cd8ff4394461a056110aa1b9d1310e79a53 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 877a7bc5b..0ca6a907d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,7 +10,7 @@ fixtures>=3.0.0 # Apache-2.0/BSD mock>=2.0.0 # BSD python-subunit>=0.0.18 # Apache-2.0/BSD openstackdocstheme>=1.16.0 # Apache-2.0 -os-testr>=0.8.2 # Apache-2.0 +os-testr>=1.0.0 # Apache-2.0 requests>=2.14.2 # Apache-2.0 requests-mock>=1.1.0 # Apache-2.0 sphinx>=1.6.2 # BSD From 35ae661b2c6b8890d6535bd485129238e6730e4c Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 13 Sep 2017 12:48:14 +0000 Subject: [PATCH 1811/3836] Updated from global requirements Change-Id: I17ac8b33f1db658ea5571ac5e954c68bf3fa3ca7 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index f726c02f2..b0b3d5028 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,7 +7,7 @@ coverage!=4.4,>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD mock>=2.0.0 # BSD python-subunit>=0.0.18 # Apache-2.0/BSD -openstackdocstheme>=1.16.0 # Apache-2.0 +openstackdocstheme>=1.17.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 requests-mock>=1.1.0 # Apache-2.0 sphinx>=1.6.2 # BSD From 3fb4fec1d6ab2c72f72997533792fbe5d05936de Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 13 Sep 2017 13:00:03 +0000 Subject: [PATCH 1812/3836] Updated from global requirements Change-Id: I6ecb4e80d2944bf592a2cbd41695643bc49f832d --- test-requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 9a3518a43..bbe53373d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,12 +8,12 @@ coverage!=4.4,>=4.0 # Apache-2.0 docutils>=0.11 # OSI-Approved Open Source, Public Domain extras # MIT fixtures>=3.0.0 # Apache-2.0/BSD -jsonschema!=2.5.0,<3.0.0,>=2.0.0 # MIT +jsonschema<3.0.0,>=2.6.0 # MIT mock>=2.0.0 # BSD python-glanceclient>=2.8.0 # Apache-2.0 python-subunit>=0.0.18 # Apache-2.0/BSD sphinx>=1.6.2 # BSD -openstackdocstheme>=1.16.0 # Apache-2.0 +openstackdocstheme>=1.17.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 reno>=2.5.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD From 8396fd548fe8960526b3e0d697a57e90cd92140a Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 13 Sep 2017 13:03:04 +0000 Subject: [PATCH 1813/3836] Updated from global requirements Change-Id: Ie6497aa5d9d3bea2ed7db264787462aae6831c83 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 0ca6a907d..d5be1f433 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,7 +9,7 @@ doc8 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD mock>=2.0.0 # BSD python-subunit>=0.0.18 # Apache-2.0/BSD -openstackdocstheme>=1.16.0 # Apache-2.0 +openstackdocstheme>=1.17.0 # Apache-2.0 os-testr>=1.0.0 # Apache-2.0 requests>=2.14.2 # Apache-2.0 requests-mock>=1.1.0 # Apache-2.0 From f29d04dcf903fc6ac10f276badaff3edf3f09b7a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 9 Sep 2017 11:01:53 -0500 Subject: [PATCH 1814/3836] Add pypi and doc publication templates Change-Id: Iaa2b3742d04135a6da35f42f4a787937b9bc81eb Depends-On: I01ea35061ba92c2e466c6269c61e640461d6da99 Depends-On: I4f1aea66fcbb623f554ded4d7ce63ff4c1168e45 --- .zuul.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.zuul.yaml b/.zuul.yaml index 382c32273..dcbbb046d 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -1,5 +1,8 @@ - project: name: openstack-infra/shade + templates: + - publish-to-pypi + - publish-openstack-python-docs check: jobs: - openstack-doc-build From 482b313b10149bfcb67f5c059da17f08426f7a7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Thu, 14 Sep 2017 12:02:02 -0600 Subject: [PATCH 1815/3836] Add support for network quota details command Neutron recently introduced API call to get details about current usage of quota by given project. This patch adds support for this command in Shade. Related-Bug: #1716043 Change-Id: I74682c6b2a287f54acef91686e7281f45cbe9684 --- shade/operatorcloud.py | 10 ++++- shade/tests/functional/test_quotas.py | 9 +++++ shade/tests/unit/test_quotas.py | 54 +++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 53ba8cd66..da6d07411 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -2363,10 +2363,12 @@ def set_network_quotas(self, name_or_id, **kwargs): error_message=("Error setting Neutron's quota for " "project {0}".format(proj.id))) - def get_network_quotas(self, name_or_id): + def get_network_quotas(self, name_or_id, details=False): """ Get network quotas for a project :param name_or_id: project name or id + :param details: if set to True it will return details about usage + of quotas by given project :raises: OpenStackCloudException if it's not a valid project :returns: Munch object with the quotas @@ -2374,8 +2376,12 @@ def get_network_quotas(self, name_or_id): proj = self.get_project(name_or_id) if not proj: raise OpenStackCloudException("project does not exist") + url = '/quotas/{project_id}'.format(project_id=proj.id) + if details: + url = url + "/details" + url = url + ".json" data = self._network_client.get( - '/quotas/{project_id}.json'.format(project_id=proj.id), + url, error_message=("Error fetching Neutron's quota for " "project {0}".format(proj.id))) return self._get_and_munchify('quota', data) diff --git a/shade/tests/functional/test_quotas.py b/shade/tests/functional/test_quotas.py index 26ffc755c..dd2816873 100644 --- a/shade/tests/functional/test_quotas.py +++ b/shade/tests/functional/test_quotas.py @@ -75,3 +75,12 @@ def test_quotas(self): self.assertEqual( network, self.operator_cloud.get_network_quotas('demo')['network']) + + def test_get_quotas_details(self): + expected_keys = ['limit', 'used', 'reserved'] + '''Test getting details about quota usage''' + quota_details = self.operator_cloud.get_network_quotas( + 'demo', details=True) + for quota_values in quota_details.values(): + for expected_key in expected_keys: + self.assertTrue(expected_key in quota_values.keys()) diff --git a/shade/tests/unit/test_quotas.py b/shade/tests/unit/test_quotas.py index 1a9768398..12efecbd3 100644 --- a/shade/tests/unit/test_quotas.py +++ b/shade/tests/unit/test_quotas.py @@ -186,6 +186,60 @@ def test_neutron_get_quotas(self): self.assertDictEqual(quota, received_quota) self.assert_calls() + def test_neutron_get_quotas_details(self): + quota_details = { + 'subnet': { + 'limit': 100, + 'used': 7, + 'reserved': 0}, + 'network': { + 'limit': 100, + 'used': 6, + 'reserved': 0}, + 'floatingip': { + 'limit': 50, + 'used': 0, + 'reserved': 0}, + 'subnetpool': { + 'limit': -1, + 'used': 2, + 'reserved': 0}, + 'security_group_rule': { + 'limit': 100, + 'used': 4, + 'reserved': 0}, + 'security_group': { + 'limit': 10, + 'used': 1, + 'reserved': 0}, + 'router': { + 'limit': 10, + 'used': 2, + 'reserved': 0}, + 'rbac_policy': { + 'limit': 10, + 'used': 2, + 'reserved': 0}, + 'port': { + 'limit': 500, + 'used': 7, + 'reserved': 0} + } + project = self.mock_for_keystone_projects(project_count=1, + list_get=True)[0] + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'quotas', + '%s/details.json' % project.project_id]), + json={'quota': quota_details}) + ]) + received_quota_details = self.op_cloud.get_network_quotas( + project.project_id, details=True) + self.assertDictEqual(quota_details, received_quota_details) + self.assert_calls() + def test_neutron_delete_quotas(self): project = self.mock_for_keystone_projects(project_count=1, list_get=True)[0] From 6f72637fb6356d39adee82a2718118156ae4a6bf Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 14 Sep 2017 16:57:42 -0500 Subject: [PATCH 1816/3836] Updates for stestr Change-Id: I344cd6ce38d8db8fe24e1611c9c61e1ffa1b586d --- .gitignore | 1 + .stestr.conf | 3 +++ tox.ini | 3 +++ 3 files changed, 7 insertions(+) create mode 100644 .stestr.conf diff --git a/.gitignore b/.gitignore index 218bf6a45..c24b89b8a 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ cover .coverage .tox nosetests.xml +.stestr/ .testrepository # Translations diff --git a/.stestr.conf b/.stestr.conf new file mode 100644 index 000000000..792382206 --- /dev/null +++ b/.stestr.conf @@ -0,0 +1,3 @@ +[DEFAULT] +test_path=. +top_dir=./ diff --git a/tox.ini b/tox.ini index bf0fd8bf9..57feb6d0b 100644 --- a/tox.ini +++ b/tox.ini @@ -12,6 +12,9 @@ setenv = VIRTUAL_ENV={envdir} BRANCH_NAME=master CLIENT_NAME=os-client-config + OS_STDOUT_CAPTURE=1 + OS_STDERR_CAPTURE=1 + OS_TEST_TIMEOUT=60 deps = -r{toxinidir}/test-requirements.txt commands = python setup.py testr --slowest --testr-args='{posargs}' From 2c23196f23be244cd747c1e26c5a55c203f3e7be Mon Sep 17 00:00:00 2001 From: Paul Belanger Date: Fri, 15 Sep 2017 12:52:57 -0400 Subject: [PATCH 1817/3836] Stop using openstack-doc-build We are in the process of moving openstack-doc-build to project-config, stop running for now. Change-Id: I409929a61044b397eedebe730b43b7897816141f Signed-off-by: Paul Belanger --- .zuul.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index 74568efd3..eb219b914 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -5,5 +5,4 @@ - publish-openstack-python-docs check: jobs: - - openstack-doc-build - openstack-tox-py35 From 2c24e204a6d75313447298bdcd7c458fa5107ba6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 15 Sep 2017 10:55:43 -0600 Subject: [PATCH 1818/3836] Record server.id in server creation exception If an error happens in the initial creation, no id is returned to the server can't be cleaned up. Change-Id: Ieb818dcf9ded062f016a3f96342f320dc203edd0 --- .../notes/server-create-error-id-66c698c7e633fb8b.yaml | 4 ++++ shade/exc.py | 10 ++++++++++ shade/openstackcloud.py | 4 ++-- 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/server-create-error-id-66c698c7e633fb8b.yaml diff --git a/releasenotes/notes/server-create-error-id-66c698c7e633fb8b.yaml b/releasenotes/notes/server-create-error-id-66c698c7e633fb8b.yaml new file mode 100644 index 000000000..673c7dcb8 --- /dev/null +++ b/releasenotes/notes/server-create-error-id-66c698c7e633fb8b.yaml @@ -0,0 +1,4 @@ +--- +features: + - server creation errors now include the server id in the + Exception to allow people to clean up. diff --git a/shade/exc.py b/shade/exc.py index 3d29af490..cda469f9c 100644 --- a/shade/exc.py +++ b/shade/exc.py @@ -54,6 +54,16 @@ def __str__(self): return message +class OpenStackCloudCreateException(OpenStackCloudException): + + def __init__(self, resource, resource_id, extra_data=None, **kwargs): + super(OpenStackCloudCreateException, self).__init__( + message="Error creating {resource}: {resource_id}".format( + resource=resource, resource_id=resource_id), + extra_data=extra_data, **kwargs) + self.resource_id = resource_id + + class OpenStackCloudTimeout(OpenStackCloudException): pass diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 7727dddd6..781a06efd 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -6605,8 +6605,8 @@ def create_server( # going to do the wait loop below, this is a waste of a call server = self.get_server_by_id(server.id) if server.status == 'ERROR': - raise OpenStackCloudException( - "Error in creating the server.") + raise OpenStackCloudCreationException( + resource='server', resource_id=server.id) if wait: server = self.wait_for_server( From eafc8bed564d41f2a0c6a050e7a301241309fa59 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 16 Sep 2017 13:13:25 -0500 Subject: [PATCH 1819/3836] Fix requires_floating_ip This isn't a required piece of the config, so it might be unset. Use get instead of []. Change-Id: I1bbbcb4ac63a4f6d4399c0fa8881c21264a03e4b --- os_client_config/cloud_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 9c6b5a5c4..d8b1e262c 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -505,7 +505,7 @@ def requires_floating_ip(self): """ if self.config['floating_ip_source'] == "None": return False - return self.config['requires_floating_ip'] + return self.config.get('requires_floating_ip') def get_external_networks(self): """Get list of network names for external networks.""" From 94ace709e2bf860567a0920c3f836263432862ea Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 16 Sep 2017 23:21:20 +0000 Subject: [PATCH 1820/3836] Updated from global requirements Change-Id: I1bbda934cc65d508f1cece8c5adc714e9f464707 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index bbe53373d..73d982263 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -6,7 +6,7 @@ hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 docutils>=0.11 # OSI-Approved Open Source, Public Domain -extras # MIT +extras>=0.0.3 # MIT fixtures>=3.0.0 # Apache-2.0/BSD jsonschema<3.0.0,>=2.6.0 # MIT mock>=2.0.0 # BSD From 521d967139d7c41e172a1412ad5f3009b85a5037 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 16 Sep 2017 23:23:58 +0000 Subject: [PATCH 1821/3836] Updated from global requirements Change-Id: I7af6ff374f2b6df62892d2e2938f97c091128113 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index d5be1f433..c9153ad55 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,7 +5,7 @@ hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 beautifulsoup4>=4.6.0 # MIT coverage!=4.4,>=4.0 # Apache-2.0 -doc8 # Apache-2.0 +doc8>=0.6.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD mock>=2.0.0 # BSD python-subunit>=0.0.18 # Apache-2.0/BSD From 52048c350cb64f7de7773b8006de1e9fb9b2a034 Mon Sep 17 00:00:00 2001 From: lidong Date: Wed, 20 Sep 2017 10:07:08 +0800 Subject: [PATCH 1822/3836] Update links in CONTRIBUTING.rst Use https instead of http for docs links Change-Id: If77bd194dc95edecda557863d4a7f2bb95b0eb3a --- CONTRIBUTING.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 8fee79414..6fe312571 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,16 +1,16 @@ If you would like to contribute to the development of OpenStack, you must follow the steps in this page: - http://docs.openstack.org/infra/manual/developers.html + https://docs.openstack.org/infra/manual/developers.html Once those steps have been completed, changes to OpenStack should be submitted for review via the Gerrit tool, following the workflow documented at: - http://docs.openstack.org/infra/manual/developers.html#development-workflow + https://docs.openstack.org/infra/manual/developers.html#development-workflow Pull requests submitted through GitHub will be ignored. Bugs should be filed on Launchpad, not GitHub: - https://bugs.launchpad.net/python-openstacksdk \ No newline at end of file + https://bugs.launchpad.net/python-openstacksdk From 6fefc92c9b1d957dbebe3cfa1df1004069f635d9 Mon Sep 17 00:00:00 2001 From: liyi Date: Wed, 20 Sep 2017 11:24:11 +0800 Subject: [PATCH 1823/3836] Remove 'conditions' section in heat stack template When version is less than '2016-10-14', we will drop 'conditions' section in stack template. Change-Id: I3aeb54c46f36572b06deb58645b994407d5c2cca Closes-Bug: #1718330 --- openstack/orchestration/v1/stack_template.py | 5 +++- .../orchestration/v1/test_stack_template.py | 26 ++++++++++++++++--- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/openstack/orchestration/v1/stack_template.py b/openstack/orchestration/v1/stack_template.py index 7dace49a5..8dfe21574 100644 --- a/openstack/orchestration/v1/stack_template.py +++ b/openstack/orchestration/v1/stack_template.py @@ -44,7 +44,7 @@ class StackTemplate(resource.Resource): resources = resource.Body('resources', type=dict) # List parameters grouped. parameter_groups = resource.Body('parameter_groups', type=list) - # Restrict conditions. + # Restrict conditions which supported since '2016-10-14'. conditions = resource.Body('conditions', type=dict) def to_dict(self): @@ -52,4 +52,7 @@ def to_dict(self): mapping.pop('location') mapping.pop('id') mapping.pop('name') + if self.heat_template_version < '2016-10-14': + mapping.pop('conditions') + return mapping diff --git a/openstack/tests/unit/orchestration/v1/test_stack_template.py b/openstack/tests/unit/orchestration/v1/test_stack_template.py index 7f927081e..29cf5437d 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack_template.py +++ b/openstack/tests/unit/orchestration/v1/test_stack_template.py @@ -29,6 +29,7 @@ 'type': 'ResourceType' } }, + 'conditions': {'cd1': True}, 'outputs': { 'key1': 'value1' } @@ -54,6 +55,7 @@ def test_make_it(self): self.assertEqual(FAKE['outputs'], sot.outputs) self.assertEqual(FAKE['parameters'], sot.parameters) self.assertEqual(FAKE['resources'], sot.resources) + self.assertEqual(FAKE['conditions'], sot.conditions) def test_to_dict(self): fake_sot = copy.deepcopy(FAKE) @@ -61,6 +63,24 @@ def test_to_dict(self): "description": "server parameters", "parameters": ["key_name", "image_id"], "label": "server_parameters"}] - fake_sot['conditions'] = {"cd1": True} - sot = stack_template.StackTemplate(**fake_sot) - self.assertEqual(fake_sot, sot.to_dict()) + + for temp_version in ['2016-10-14', '2017-02-24', '2017-02-24', + '2017-09-01', '2018-03-02', 'newton', + 'ocata', 'pike', 'queens']: + fake_sot['heat_template_version'] = temp_version + sot = stack_template.StackTemplate(**fake_sot) + self.assertEqual(fake_sot, sot.to_dict()) + + def test_to_dict_without_conditions(self): + fake_sot = copy.deepcopy(FAKE) + fake_sot['parameter_groups'] = [{ + "description": "server parameters", + "parameters": ["key_name", "image_id"], + "label": "server_parameters"}] + fake_sot.pop('conditions') + + for temp_version in ['2013-05-23', '2014-10-16', '2015-04-30', + '2015-10-15', '2016-04-08']: + fake_sot['heat_template_version'] = temp_version + sot = stack_template.StackTemplate(**fake_sot) + self.assertEqual(fake_sot, sot.to_dict()) From a3cd1f0faff25b6b43a80e8357b134ccd7437f99 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 20 Sep 2017 10:29:27 -0500 Subject: [PATCH 1824/3836] Merge shade and occ releasenotes Change-Id: I4d25c654ee3c298de03907b14161096a59d098db --- os-client-config/releasenotes/source/conf.py | 265 ------------------ .../releasenotes/source/index.rst | 21 -- .../releasenotes/source/unreleased.rst | 5 - ...add-jmespath-support-f47b7a503dbbfda1.yaml | 0 ...d-list_flavor_access-e038253e953e6586.yaml | 0 .../add-server-console-078ed2696e5b04d9.yaml | 0 ...show-all-images-flag-352748b6c3d99f3f.yaml | 0 ...cription_create_user-0ddc9a0ef4da840d.yaml | 0 ...e_recordsets_support-69af0a6b317073e7.yaml | 0 ...ignate_zones_support-35fa9b8b09995b43.yaml | 0 ...st_aggregate_support-471623faf45ec3c3.yaml | 0 ...num_baymodel_support-e35e5aab0b14ff75.yaml | 0 ...num_services_support-3d95f9dcc60b5573.yaml | 0 ...server_group_support-dfa472e3dae7d34d.yaml | 0 .../add_update_server-8761059d6de7e68b.yaml | 0 .../add_update_service-28e590a7a7524053.yaml | 0 ...il-cluster-templates-3eb4b5744ba327ac.yaml | 0 .../notes/bug-2001080-de52ead3c5466792.yaml | 0 ...cache-in-use-volumes-c7fa8bb378106fe3.yaml | 0 ...tch-up-release-notes-e385fad34e9f3d6e.yaml | 0 ...ach-vol-return-value-4834a1f78392abb1.yaml | 0 ...lume_backups_support-6f7ceab440853833.yaml | 0 .../cinderv2-norm-fix-037189c60b43089f.yaml | 0 ...cloud-profile-status-e0d29b5e2f10e95c.yaml | 0 .../compute-quotas-b07a0f24dfac8444.yaml | 0 ...mpute-usage-defaults-5f5b2936f17ff400.yaml | 0 .../config-flavor-specs-ca712e17971482b6.yaml | 0 .../create-stack-fix-12dbb59a48ac7442.yaml | 0 ...e_server_network_fix-c4a56b31d2850a4b.yaml | 0 .../create_service_norm-319a97433d68fa6a.yaml | 0 .../notes/data-model-cf50d86982646370.yaml | 0 ...delete-image-objects-9d4b4e0fff36a23f.yaml | 0 .../delete-obj-return-a3ecf0415b7a2989.yaml | 0 .../delete_project-399f9b3107014dde.yaml | 0 ...perations_name_or_id-baba4cac5b67234d.yaml | 0 .../dual-stack-networks-8a81941c97d28deb.yaml | 0 ...ndpoint-from-catalog-bad36cb0409a4e6a.yaml | 0 ...-not-attribute-error-49484d0fdc61f75d.yaml | 0 ...ture-server-metadata-50caf18cec532160.yaml | 0 .../notes/fip_timeout-035c4bb3ff92fa1f.yaml | 0 ...ith-old-keystoneauth-66e11ee9d008b962.yaml | 0 .../fix-config-drive-a148b7589f7e1022.yaml | 0 .../fix-delete-ips-1d4eebf7bc4d4733.yaml | 0 .../fix-list-networks-a592725df64c306e.yaml | 0 .../fix-missing-futures-a0617a1c1ce6e659.yaml | 0 ...perties-key-conflict-2161ca1faaad6731.yaml | 0 ...ix-supplemental-fips-c9cd58aac12eb30e.yaml | 0 .../fix-update-domain-af47b066ac52eb7f.yaml | 0 .../fixed-magnum-type-7406f0a60525f858.yaml | 0 .../notes/flavor_fix-a53c6b326dc34a2c.yaml | 0 .../fnmatch-name-or-id-f658fe26f84086c8.yaml | 0 .../notes/get-limits-c383c512f8e01873.yaml | 0 .../notes/get-usage-72d249ff790d1b8f.yaml | 0 .../get_object_api-968483adb016bce1.yaml | 0 ...nce-image-pagination-0b4dfef22b25852b.yaml | 0 ...t-revoke-assignments-231d3f9596a1ae75.yaml | 0 ...image-flavor-by-name-54865b00ebbf1004.yaml | 0 .../image-from-volume-9acf7379f5995b5b.yaml | 0 ...nfer-secgroup-source-58d840aaf1a1f485.yaml | 0 .../ironic-microversion-ba5b0f36f11196a6.yaml | 0 .../less-file-hashing-d2497337da5acbef.yaml | 0 .../notes/list-az-names-a38c277d1192471b.yaml | 0 ...ignments-keystone-v2-b127b12b4860f50c.yaml | 0 ...servers-all-projects-349e6dc665ba2e8d.yaml | 0 .../notes/load-yaml-3177efca78e5c67a.yaml | 0 .../log-request-ids-37507cb6eed9a7da.yaml | 0 .../notes/magic-fixes-dca4ae4dac2441a8.yaml | 0 .../make-rest-client-dd3d365632a26fa0.yaml | 0 ...metadata_easier.yaml-e9751723e002e06f.yaml | 0 ...ade-os-client-config-29878734ad643e33.yaml | 4 + .../meta-passthrough-d695bff4f9366b65.yaml | 0 ...n-max-legacy-version-301242466ddefa93.yaml | 0 .../multiple-updates-b48cc2f6db2e526d.yaml | 0 .../notes/net_provider-dd64b697476b7094.yaml | 0 .../notes/network-list-e6e9dafdd8446263.yaml | 0 .../network-quotas-b98cce9ffeffdbf4.yaml | 0 ...-floating-attributes-213cdf5681d337e1.yaml | 0 .../no-more-troveclient-0a4739c21432ac63.yaml | 0 ...orm_role_assignments-a13f41768e62d40c.yaml | 0 .../normalize-images-1331bea7bfffa36a.yaml | 0 .../nova-flavor-to-rest-0a5757e35714a690.yaml | 0 ...ova-old-microversion-5e4b8e239ba44096.yaml | 0 .../option-precedence-1fecab21fdfb2c33.yaml | 0 .../remove-magnumclient-875b3e513f98f57c.yaml | 0 .../remove-novaclient-3f8d4db20d5f9582.yaml | 0 ...removed-glanceclient-105c7fba9481b9be.yaml | 0 .../removed-swiftclient-aff22bfaeee5f59f.yaml | 0 .../notes/router_ext_gw-b86582317bca8b39.yaml | 0 .../notes/sdk-helper-41f8d815cfbcfb00.yaml | 0 ...rver-create-error-id-66c698c7e633fb8b.yaml | 0 ...rver-security-groups-840ab28c04f359de.yaml | 0 ...service_enabled_flag-c917b305d3f2e8fd.yaml | 0 .../session-client-b581a6e5d18c8f04.yaml | 0 .../notes/shade-helper-568f8cb372eef6d9.yaml | 0 .../notes/stack-update-5886e91fd6e423bf.yaml | 0 .../started-using-reno-242e2b0cd27f9480.yaml | 0 .../stream-to-file-91f48d6dcea399c6.yaml | 0 .../notes/strict-mode-d493abc0c3e87945.yaml | 0 .../swift-upload-lock-d18f3d42b3a0719a.yaml | 0 .../update_endpoint-f87c1f42d0c0d1ef.yaml | 0 .../use-interface-ip-c5cb3e7c91150096.yaml | 0 .../vendor-updates-f11184ba56bb27cf.yaml | 0 .../version-discovery-a501c4e9e9869f77.yaml | 0 .../notes/volume-quotas-5b674ee8c1f71eb6.yaml | 0 .../notes/volume-types-a07a14ae668e7dd2.yaml | 0 ...it-on-image-snapshot-27cd2eacab2fabd8.yaml | 0 .../wait_for_server-8dc8446b7c673d36.yaml | 0 ...ound-transitive-deps-1e7a214f3256b77e.yaml | 0 .../source/_static/.placeholder | 0 .../source/_templates/.placeholder | 0 .../source/conf.py | 0 .../source/index.rst | 0 .../source/mainline.rst | 0 .../source/mitaka.rst | 0 .../source/newton.rst | 0 .../source/ocata.rst | 0 .../source/pike.rst | 0 .../source/unreleased.rst | 0 .../started-using-reno-242e2b0cd27f9480.yaml | 3 - .../releasenotes/source/_static/.placeholder | 0 .../source/_templates/.placeholder | 0 shade/releasenotes/source/pike.rst | 6 - 122 files changed, 4 insertions(+), 300 deletions(-) delete mode 100644 os-client-config/releasenotes/source/conf.py delete mode 100644 os-client-config/releasenotes/source/index.rst delete mode 100644 os-client-config/releasenotes/source/unreleased.rst rename {shade/releasenotes => releasenotes}/notes/add-jmespath-support-f47b7a503dbbfda1.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/add-list_flavor_access-e038253e953e6586.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/add-server-console-078ed2696e5b04d9.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/add-show-all-images-flag-352748b6c3d99f3f.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/add_description_create_user-0ddc9a0ef4da840d.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/add_designate_recordsets_support-69af0a6b317073e7.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/add_designate_zones_support-35fa9b8b09995b43.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/add_host_aggregate_support-471623faf45ec3c3.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/add_magnum_baymodel_support-e35e5aab0b14ff75.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/add_magnum_services_support-3d95f9dcc60b5573.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/add_server_group_support-dfa472e3dae7d34d.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/add_update_server-8761059d6de7e68b.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/add_update_service-28e590a7a7524053.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/always-detail-cluster-templates-3eb4b5744ba327ac.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/bug-2001080-de52ead3c5466792.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/cache-in-use-volumes-c7fa8bb378106fe3.yaml (100%) rename {os-client-config/releasenotes => releasenotes}/notes/catch-up-release-notes-e385fad34e9f3d6e.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/change-attach-vol-return-value-4834a1f78392abb1.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/cinder_volume_backups_support-6f7ceab440853833.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/cinderv2-norm-fix-037189c60b43089f.yaml (100%) rename {os-client-config/releasenotes => releasenotes}/notes/cloud-profile-status-e0d29b5e2f10e95c.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/compute-quotas-b07a0f24dfac8444.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/compute-usage-defaults-5f5b2936f17ff400.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/config-flavor-specs-ca712e17971482b6.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/create-stack-fix-12dbb59a48ac7442.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/create_server_network_fix-c4a56b31d2850a4b.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/create_service_norm-319a97433d68fa6a.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/data-model-cf50d86982646370.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/delete-image-objects-9d4b4e0fff36a23f.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/delete-obj-return-a3ecf0415b7a2989.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/delete_project-399f9b3107014dde.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/domain_operations_name_or_id-baba4cac5b67234d.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/dual-stack-networks-8a81941c97d28deb.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/endpoint-from-catalog-bad36cb0409a4e6a.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/false-not-attribute-error-49484d0fdc61f75d.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/feature-server-metadata-50caf18cec532160.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/fip_timeout-035c4bb3ff92fa1f.yaml (100%) rename {os-client-config/releasenotes => releasenotes}/notes/fix-compat-with-old-keystoneauth-66e11ee9d008b962.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/fix-config-drive-a148b7589f7e1022.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/fix-delete-ips-1d4eebf7bc4d4733.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/fix-list-networks-a592725df64c306e.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/fix-missing-futures-a0617a1c1ce6e659.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/fix-properties-key-conflict-2161ca1faaad6731.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/fix-supplemental-fips-c9cd58aac12eb30e.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/fix-update-domain-af47b066ac52eb7f.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/fixed-magnum-type-7406f0a60525f858.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/flavor_fix-a53c6b326dc34a2c.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/fnmatch-name-or-id-f658fe26f84086c8.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/get-limits-c383c512f8e01873.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/get-usage-72d249ff790d1b8f.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/get_object_api-968483adb016bce1.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/glance-image-pagination-0b4dfef22b25852b.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/grant-revoke-assignments-231d3f9596a1ae75.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/image-flavor-by-name-54865b00ebbf1004.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/image-from-volume-9acf7379f5995b5b.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/infer-secgroup-source-58d840aaf1a1f485.yaml (100%) rename {os-client-config/releasenotes => releasenotes}/notes/ironic-microversion-ba5b0f36f11196a6.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/less-file-hashing-d2497337da5acbef.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/list-az-names-a38c277d1192471b.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/list-role-assignments-keystone-v2-b127b12b4860f50c.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/list-servers-all-projects-349e6dc665ba2e8d.yaml (100%) rename {os-client-config/releasenotes => releasenotes}/notes/load-yaml-3177efca78e5c67a.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/log-request-ids-37507cb6eed9a7da.yaml (100%) rename {os-client-config/releasenotes => releasenotes}/notes/magic-fixes-dca4ae4dac2441a8.yaml (100%) rename {os-client-config/releasenotes => releasenotes}/notes/make-rest-client-dd3d365632a26fa0.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/make_object_metadata_easier.yaml-e9751723e002e06f.yaml (100%) create mode 100644 releasenotes/notes/merge-shade-os-client-config-29878734ad643e33.yaml rename {shade/releasenotes => releasenotes}/notes/meta-passthrough-d695bff4f9366b65.yaml (100%) rename {os-client-config/releasenotes => releasenotes}/notes/min-max-legacy-version-301242466ddefa93.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/multiple-updates-b48cc2f6db2e526d.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/net_provider-dd64b697476b7094.yaml (100%) rename {os-client-config/releasenotes => releasenotes}/notes/network-list-e6e9dafdd8446263.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/network-quotas-b98cce9ffeffdbf4.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/new-floating-attributes-213cdf5681d337e1.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/no-more-troveclient-0a4739c21432ac63.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/norm_role_assignments-a13f41768e62d40c.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/normalize-images-1331bea7bfffa36a.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/nova-flavor-to-rest-0a5757e35714a690.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/nova-old-microversion-5e4b8e239ba44096.yaml (100%) rename {os-client-config/releasenotes => releasenotes}/notes/option-precedence-1fecab21fdfb2c33.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/remove-magnumclient-875b3e513f98f57c.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/remove-novaclient-3f8d4db20d5f9582.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/removed-glanceclient-105c7fba9481b9be.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/removed-swiftclient-aff22bfaeee5f59f.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/router_ext_gw-b86582317bca8b39.yaml (100%) rename {os-client-config/releasenotes => releasenotes}/notes/sdk-helper-41f8d815cfbcfb00.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/server-create-error-id-66c698c7e633fb8b.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/server-security-groups-840ab28c04f359de.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/service_enabled_flag-c917b305d3f2e8fd.yaml (100%) rename {os-client-config/releasenotes => releasenotes}/notes/session-client-b581a6e5d18c8f04.yaml (100%) rename {os-client-config/releasenotes => releasenotes}/notes/shade-helper-568f8cb372eef6d9.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/stack-update-5886e91fd6e423bf.yaml (100%) rename {os-client-config/releasenotes => releasenotes}/notes/started-using-reno-242e2b0cd27f9480.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/stream-to-file-91f48d6dcea399c6.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/strict-mode-d493abc0c3e87945.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/swift-upload-lock-d18f3d42b3a0719a.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/update_endpoint-f87c1f42d0c0d1ef.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/use-interface-ip-c5cb3e7c91150096.yaml (100%) rename {os-client-config/releasenotes => releasenotes}/notes/vendor-updates-f11184ba56bb27cf.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/version-discovery-a501c4e9e9869f77.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/volume-quotas-5b674ee8c1f71eb6.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/volume-types-a07a14ae668e7dd2.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/wait-on-image-snapshot-27cd2eacab2fabd8.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/wait_for_server-8dc8446b7c673d36.yaml (100%) rename {shade/releasenotes => releasenotes}/notes/workaround-transitive-deps-1e7a214f3256b77e.yaml (100%) rename {os-client-config/releasenotes => releasenotes}/source/_static/.placeholder (100%) rename {os-client-config/releasenotes => releasenotes}/source/_templates/.placeholder (100%) rename {shade/releasenotes => releasenotes}/source/conf.py (100%) rename {shade/releasenotes => releasenotes}/source/index.rst (100%) rename {shade/releasenotes => releasenotes}/source/mainline.rst (100%) rename {os-client-config/releasenotes => releasenotes}/source/mitaka.rst (100%) rename {os-client-config/releasenotes => releasenotes}/source/newton.rst (100%) rename {os-client-config/releasenotes => releasenotes}/source/ocata.rst (100%) rename {os-client-config/releasenotes => releasenotes}/source/pike.rst (100%) rename {shade/releasenotes => releasenotes}/source/unreleased.rst (100%) delete mode 100644 shade/releasenotes/notes/started-using-reno-242e2b0cd27f9480.yaml delete mode 100644 shade/releasenotes/source/_static/.placeholder delete mode 100644 shade/releasenotes/source/_templates/.placeholder delete mode 100644 shade/releasenotes/source/pike.rst diff --git a/os-client-config/releasenotes/source/conf.py b/os-client-config/releasenotes/source/conf.py deleted file mode 100644 index 1d0365645..000000000 --- a/os-client-config/releasenotes/source/conf.py +++ /dev/null @@ -1,265 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Os-Client-Config Release Notes documentation build configuration file, created by -# sphinx-quickstart on Thu Nov 5 11:50:32 2015. -# -# 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. -#sys.path.insert(0, os.path.abspath('.')) - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' -import openstackdocstheme - -# 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', -] - -# 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'os-client-config Release Notes' -copyright = u'2015, os-client-config developers' - -# 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. -# -import pbr.version -occ_version = pbr.version.VersionInfo('os-client-config') -# The short X.Y version. -version = occ_version.canonical_version_string() -# The full version, including alpha/beta/rc tags. -release = occ_version.version_string_with_vcs() - -# 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 = [] - -# 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 '', 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 = 'OCCReleaseNotesdoc' - - -# -- 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', 'OCCReleaseNotes.tex', u'os-client-config Release Notes Documentation', - u'os-client-config 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', 'occreleasenotes', u'os-client-config Release Notes Documentation', - [u'os-client-config 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', 'OCCReleaseNotes', u'os-client-config Release Notes Documentation', - u'os-client-config developers', 'OCCReleaseNotes', - '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' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False - -# -- Options for Internationalization output ------------------------------ -locale_dirs = ['locale/'] diff --git a/os-client-config/releasenotes/source/index.rst b/os-client-config/releasenotes/source/index.rst deleted file mode 100644 index 405f263a0..000000000 --- a/os-client-config/releasenotes/source/index.rst +++ /dev/null @@ -1,21 +0,0 @@ -================================ - os-client-config Release Notes -================================ - -Contents -======== - -.. toctree:: - :maxdepth: 2 - - unreleased - pike - ocata - newton - mitaka - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`search` diff --git a/os-client-config/releasenotes/source/unreleased.rst b/os-client-config/releasenotes/source/unreleased.rst deleted file mode 100644 index 875030f9d..000000000 --- a/os-client-config/releasenotes/source/unreleased.rst +++ /dev/null @@ -1,5 +0,0 @@ -============================ -Current Series Release Notes -============================ - -.. release-notes:: diff --git a/shade/releasenotes/notes/add-jmespath-support-f47b7a503dbbfda1.yaml b/releasenotes/notes/add-jmespath-support-f47b7a503dbbfda1.yaml similarity index 100% rename from shade/releasenotes/notes/add-jmespath-support-f47b7a503dbbfda1.yaml rename to releasenotes/notes/add-jmespath-support-f47b7a503dbbfda1.yaml diff --git a/shade/releasenotes/notes/add-list_flavor_access-e038253e953e6586.yaml b/releasenotes/notes/add-list_flavor_access-e038253e953e6586.yaml similarity index 100% rename from shade/releasenotes/notes/add-list_flavor_access-e038253e953e6586.yaml rename to releasenotes/notes/add-list_flavor_access-e038253e953e6586.yaml diff --git a/shade/releasenotes/notes/add-server-console-078ed2696e5b04d9.yaml b/releasenotes/notes/add-server-console-078ed2696e5b04d9.yaml similarity index 100% rename from shade/releasenotes/notes/add-server-console-078ed2696e5b04d9.yaml rename to releasenotes/notes/add-server-console-078ed2696e5b04d9.yaml diff --git a/shade/releasenotes/notes/add-show-all-images-flag-352748b6c3d99f3f.yaml b/releasenotes/notes/add-show-all-images-flag-352748b6c3d99f3f.yaml similarity index 100% rename from shade/releasenotes/notes/add-show-all-images-flag-352748b6c3d99f3f.yaml rename to releasenotes/notes/add-show-all-images-flag-352748b6c3d99f3f.yaml diff --git a/shade/releasenotes/notes/add_description_create_user-0ddc9a0ef4da840d.yaml b/releasenotes/notes/add_description_create_user-0ddc9a0ef4da840d.yaml similarity index 100% rename from shade/releasenotes/notes/add_description_create_user-0ddc9a0ef4da840d.yaml rename to releasenotes/notes/add_description_create_user-0ddc9a0ef4da840d.yaml diff --git a/shade/releasenotes/notes/add_designate_recordsets_support-69af0a6b317073e7.yaml b/releasenotes/notes/add_designate_recordsets_support-69af0a6b317073e7.yaml similarity index 100% rename from shade/releasenotes/notes/add_designate_recordsets_support-69af0a6b317073e7.yaml rename to releasenotes/notes/add_designate_recordsets_support-69af0a6b317073e7.yaml diff --git a/shade/releasenotes/notes/add_designate_zones_support-35fa9b8b09995b43.yaml b/releasenotes/notes/add_designate_zones_support-35fa9b8b09995b43.yaml similarity index 100% rename from shade/releasenotes/notes/add_designate_zones_support-35fa9b8b09995b43.yaml rename to releasenotes/notes/add_designate_zones_support-35fa9b8b09995b43.yaml diff --git a/shade/releasenotes/notes/add_host_aggregate_support-471623faf45ec3c3.yaml b/releasenotes/notes/add_host_aggregate_support-471623faf45ec3c3.yaml similarity index 100% rename from shade/releasenotes/notes/add_host_aggregate_support-471623faf45ec3c3.yaml rename to releasenotes/notes/add_host_aggregate_support-471623faf45ec3c3.yaml diff --git a/shade/releasenotes/notes/add_magnum_baymodel_support-e35e5aab0b14ff75.yaml b/releasenotes/notes/add_magnum_baymodel_support-e35e5aab0b14ff75.yaml similarity index 100% rename from shade/releasenotes/notes/add_magnum_baymodel_support-e35e5aab0b14ff75.yaml rename to releasenotes/notes/add_magnum_baymodel_support-e35e5aab0b14ff75.yaml diff --git a/shade/releasenotes/notes/add_magnum_services_support-3d95f9dcc60b5573.yaml b/releasenotes/notes/add_magnum_services_support-3d95f9dcc60b5573.yaml similarity index 100% rename from shade/releasenotes/notes/add_magnum_services_support-3d95f9dcc60b5573.yaml rename to releasenotes/notes/add_magnum_services_support-3d95f9dcc60b5573.yaml diff --git a/shade/releasenotes/notes/add_server_group_support-dfa472e3dae7d34d.yaml b/releasenotes/notes/add_server_group_support-dfa472e3dae7d34d.yaml similarity index 100% rename from shade/releasenotes/notes/add_server_group_support-dfa472e3dae7d34d.yaml rename to releasenotes/notes/add_server_group_support-dfa472e3dae7d34d.yaml diff --git a/shade/releasenotes/notes/add_update_server-8761059d6de7e68b.yaml b/releasenotes/notes/add_update_server-8761059d6de7e68b.yaml similarity index 100% rename from shade/releasenotes/notes/add_update_server-8761059d6de7e68b.yaml rename to releasenotes/notes/add_update_server-8761059d6de7e68b.yaml diff --git a/shade/releasenotes/notes/add_update_service-28e590a7a7524053.yaml b/releasenotes/notes/add_update_service-28e590a7a7524053.yaml similarity index 100% rename from shade/releasenotes/notes/add_update_service-28e590a7a7524053.yaml rename to releasenotes/notes/add_update_service-28e590a7a7524053.yaml diff --git a/shade/releasenotes/notes/always-detail-cluster-templates-3eb4b5744ba327ac.yaml b/releasenotes/notes/always-detail-cluster-templates-3eb4b5744ba327ac.yaml similarity index 100% rename from shade/releasenotes/notes/always-detail-cluster-templates-3eb4b5744ba327ac.yaml rename to releasenotes/notes/always-detail-cluster-templates-3eb4b5744ba327ac.yaml diff --git a/shade/releasenotes/notes/bug-2001080-de52ead3c5466792.yaml b/releasenotes/notes/bug-2001080-de52ead3c5466792.yaml similarity index 100% rename from shade/releasenotes/notes/bug-2001080-de52ead3c5466792.yaml rename to releasenotes/notes/bug-2001080-de52ead3c5466792.yaml diff --git a/shade/releasenotes/notes/cache-in-use-volumes-c7fa8bb378106fe3.yaml b/releasenotes/notes/cache-in-use-volumes-c7fa8bb378106fe3.yaml similarity index 100% rename from shade/releasenotes/notes/cache-in-use-volumes-c7fa8bb378106fe3.yaml rename to releasenotes/notes/cache-in-use-volumes-c7fa8bb378106fe3.yaml diff --git a/os-client-config/releasenotes/notes/catch-up-release-notes-e385fad34e9f3d6e.yaml b/releasenotes/notes/catch-up-release-notes-e385fad34e9f3d6e.yaml similarity index 100% rename from os-client-config/releasenotes/notes/catch-up-release-notes-e385fad34e9f3d6e.yaml rename to releasenotes/notes/catch-up-release-notes-e385fad34e9f3d6e.yaml diff --git a/shade/releasenotes/notes/change-attach-vol-return-value-4834a1f78392abb1.yaml b/releasenotes/notes/change-attach-vol-return-value-4834a1f78392abb1.yaml similarity index 100% rename from shade/releasenotes/notes/change-attach-vol-return-value-4834a1f78392abb1.yaml rename to releasenotes/notes/change-attach-vol-return-value-4834a1f78392abb1.yaml diff --git a/shade/releasenotes/notes/cinder_volume_backups_support-6f7ceab440853833.yaml b/releasenotes/notes/cinder_volume_backups_support-6f7ceab440853833.yaml similarity index 100% rename from shade/releasenotes/notes/cinder_volume_backups_support-6f7ceab440853833.yaml rename to releasenotes/notes/cinder_volume_backups_support-6f7ceab440853833.yaml diff --git a/shade/releasenotes/notes/cinderv2-norm-fix-037189c60b43089f.yaml b/releasenotes/notes/cinderv2-norm-fix-037189c60b43089f.yaml similarity index 100% rename from shade/releasenotes/notes/cinderv2-norm-fix-037189c60b43089f.yaml rename to releasenotes/notes/cinderv2-norm-fix-037189c60b43089f.yaml diff --git a/os-client-config/releasenotes/notes/cloud-profile-status-e0d29b5e2f10e95c.yaml b/releasenotes/notes/cloud-profile-status-e0d29b5e2f10e95c.yaml similarity index 100% rename from os-client-config/releasenotes/notes/cloud-profile-status-e0d29b5e2f10e95c.yaml rename to releasenotes/notes/cloud-profile-status-e0d29b5e2f10e95c.yaml diff --git a/shade/releasenotes/notes/compute-quotas-b07a0f24dfac8444.yaml b/releasenotes/notes/compute-quotas-b07a0f24dfac8444.yaml similarity index 100% rename from shade/releasenotes/notes/compute-quotas-b07a0f24dfac8444.yaml rename to releasenotes/notes/compute-quotas-b07a0f24dfac8444.yaml diff --git a/shade/releasenotes/notes/compute-usage-defaults-5f5b2936f17ff400.yaml b/releasenotes/notes/compute-usage-defaults-5f5b2936f17ff400.yaml similarity index 100% rename from shade/releasenotes/notes/compute-usage-defaults-5f5b2936f17ff400.yaml rename to releasenotes/notes/compute-usage-defaults-5f5b2936f17ff400.yaml diff --git a/shade/releasenotes/notes/config-flavor-specs-ca712e17971482b6.yaml b/releasenotes/notes/config-flavor-specs-ca712e17971482b6.yaml similarity index 100% rename from shade/releasenotes/notes/config-flavor-specs-ca712e17971482b6.yaml rename to releasenotes/notes/config-flavor-specs-ca712e17971482b6.yaml diff --git a/shade/releasenotes/notes/create-stack-fix-12dbb59a48ac7442.yaml b/releasenotes/notes/create-stack-fix-12dbb59a48ac7442.yaml similarity index 100% rename from shade/releasenotes/notes/create-stack-fix-12dbb59a48ac7442.yaml rename to releasenotes/notes/create-stack-fix-12dbb59a48ac7442.yaml diff --git a/shade/releasenotes/notes/create_server_network_fix-c4a56b31d2850a4b.yaml b/releasenotes/notes/create_server_network_fix-c4a56b31d2850a4b.yaml similarity index 100% rename from shade/releasenotes/notes/create_server_network_fix-c4a56b31d2850a4b.yaml rename to releasenotes/notes/create_server_network_fix-c4a56b31d2850a4b.yaml diff --git a/shade/releasenotes/notes/create_service_norm-319a97433d68fa6a.yaml b/releasenotes/notes/create_service_norm-319a97433d68fa6a.yaml similarity index 100% rename from shade/releasenotes/notes/create_service_norm-319a97433d68fa6a.yaml rename to releasenotes/notes/create_service_norm-319a97433d68fa6a.yaml diff --git a/shade/releasenotes/notes/data-model-cf50d86982646370.yaml b/releasenotes/notes/data-model-cf50d86982646370.yaml similarity index 100% rename from shade/releasenotes/notes/data-model-cf50d86982646370.yaml rename to releasenotes/notes/data-model-cf50d86982646370.yaml diff --git a/shade/releasenotes/notes/delete-image-objects-9d4b4e0fff36a23f.yaml b/releasenotes/notes/delete-image-objects-9d4b4e0fff36a23f.yaml similarity index 100% rename from shade/releasenotes/notes/delete-image-objects-9d4b4e0fff36a23f.yaml rename to releasenotes/notes/delete-image-objects-9d4b4e0fff36a23f.yaml diff --git a/shade/releasenotes/notes/delete-obj-return-a3ecf0415b7a2989.yaml b/releasenotes/notes/delete-obj-return-a3ecf0415b7a2989.yaml similarity index 100% rename from shade/releasenotes/notes/delete-obj-return-a3ecf0415b7a2989.yaml rename to releasenotes/notes/delete-obj-return-a3ecf0415b7a2989.yaml diff --git a/shade/releasenotes/notes/delete_project-399f9b3107014dde.yaml b/releasenotes/notes/delete_project-399f9b3107014dde.yaml similarity index 100% rename from shade/releasenotes/notes/delete_project-399f9b3107014dde.yaml rename to releasenotes/notes/delete_project-399f9b3107014dde.yaml diff --git a/shade/releasenotes/notes/domain_operations_name_or_id-baba4cac5b67234d.yaml b/releasenotes/notes/domain_operations_name_or_id-baba4cac5b67234d.yaml similarity index 100% rename from shade/releasenotes/notes/domain_operations_name_or_id-baba4cac5b67234d.yaml rename to releasenotes/notes/domain_operations_name_or_id-baba4cac5b67234d.yaml diff --git a/shade/releasenotes/notes/dual-stack-networks-8a81941c97d28deb.yaml b/releasenotes/notes/dual-stack-networks-8a81941c97d28deb.yaml similarity index 100% rename from shade/releasenotes/notes/dual-stack-networks-8a81941c97d28deb.yaml rename to releasenotes/notes/dual-stack-networks-8a81941c97d28deb.yaml diff --git a/shade/releasenotes/notes/endpoint-from-catalog-bad36cb0409a4e6a.yaml b/releasenotes/notes/endpoint-from-catalog-bad36cb0409a4e6a.yaml similarity index 100% rename from shade/releasenotes/notes/endpoint-from-catalog-bad36cb0409a4e6a.yaml rename to releasenotes/notes/endpoint-from-catalog-bad36cb0409a4e6a.yaml diff --git a/shade/releasenotes/notes/false-not-attribute-error-49484d0fdc61f75d.yaml b/releasenotes/notes/false-not-attribute-error-49484d0fdc61f75d.yaml similarity index 100% rename from shade/releasenotes/notes/false-not-attribute-error-49484d0fdc61f75d.yaml rename to releasenotes/notes/false-not-attribute-error-49484d0fdc61f75d.yaml diff --git a/shade/releasenotes/notes/feature-server-metadata-50caf18cec532160.yaml b/releasenotes/notes/feature-server-metadata-50caf18cec532160.yaml similarity index 100% rename from shade/releasenotes/notes/feature-server-metadata-50caf18cec532160.yaml rename to releasenotes/notes/feature-server-metadata-50caf18cec532160.yaml diff --git a/shade/releasenotes/notes/fip_timeout-035c4bb3ff92fa1f.yaml b/releasenotes/notes/fip_timeout-035c4bb3ff92fa1f.yaml similarity index 100% rename from shade/releasenotes/notes/fip_timeout-035c4bb3ff92fa1f.yaml rename to releasenotes/notes/fip_timeout-035c4bb3ff92fa1f.yaml diff --git a/os-client-config/releasenotes/notes/fix-compat-with-old-keystoneauth-66e11ee9d008b962.yaml b/releasenotes/notes/fix-compat-with-old-keystoneauth-66e11ee9d008b962.yaml similarity index 100% rename from os-client-config/releasenotes/notes/fix-compat-with-old-keystoneauth-66e11ee9d008b962.yaml rename to releasenotes/notes/fix-compat-with-old-keystoneauth-66e11ee9d008b962.yaml diff --git a/shade/releasenotes/notes/fix-config-drive-a148b7589f7e1022.yaml b/releasenotes/notes/fix-config-drive-a148b7589f7e1022.yaml similarity index 100% rename from shade/releasenotes/notes/fix-config-drive-a148b7589f7e1022.yaml rename to releasenotes/notes/fix-config-drive-a148b7589f7e1022.yaml diff --git a/shade/releasenotes/notes/fix-delete-ips-1d4eebf7bc4d4733.yaml b/releasenotes/notes/fix-delete-ips-1d4eebf7bc4d4733.yaml similarity index 100% rename from shade/releasenotes/notes/fix-delete-ips-1d4eebf7bc4d4733.yaml rename to releasenotes/notes/fix-delete-ips-1d4eebf7bc4d4733.yaml diff --git a/shade/releasenotes/notes/fix-list-networks-a592725df64c306e.yaml b/releasenotes/notes/fix-list-networks-a592725df64c306e.yaml similarity index 100% rename from shade/releasenotes/notes/fix-list-networks-a592725df64c306e.yaml rename to releasenotes/notes/fix-list-networks-a592725df64c306e.yaml diff --git a/shade/releasenotes/notes/fix-missing-futures-a0617a1c1ce6e659.yaml b/releasenotes/notes/fix-missing-futures-a0617a1c1ce6e659.yaml similarity index 100% rename from shade/releasenotes/notes/fix-missing-futures-a0617a1c1ce6e659.yaml rename to releasenotes/notes/fix-missing-futures-a0617a1c1ce6e659.yaml diff --git a/shade/releasenotes/notes/fix-properties-key-conflict-2161ca1faaad6731.yaml b/releasenotes/notes/fix-properties-key-conflict-2161ca1faaad6731.yaml similarity index 100% rename from shade/releasenotes/notes/fix-properties-key-conflict-2161ca1faaad6731.yaml rename to releasenotes/notes/fix-properties-key-conflict-2161ca1faaad6731.yaml diff --git a/shade/releasenotes/notes/fix-supplemental-fips-c9cd58aac12eb30e.yaml b/releasenotes/notes/fix-supplemental-fips-c9cd58aac12eb30e.yaml similarity index 100% rename from shade/releasenotes/notes/fix-supplemental-fips-c9cd58aac12eb30e.yaml rename to releasenotes/notes/fix-supplemental-fips-c9cd58aac12eb30e.yaml diff --git a/shade/releasenotes/notes/fix-update-domain-af47b066ac52eb7f.yaml b/releasenotes/notes/fix-update-domain-af47b066ac52eb7f.yaml similarity index 100% rename from shade/releasenotes/notes/fix-update-domain-af47b066ac52eb7f.yaml rename to releasenotes/notes/fix-update-domain-af47b066ac52eb7f.yaml diff --git a/shade/releasenotes/notes/fixed-magnum-type-7406f0a60525f858.yaml b/releasenotes/notes/fixed-magnum-type-7406f0a60525f858.yaml similarity index 100% rename from shade/releasenotes/notes/fixed-magnum-type-7406f0a60525f858.yaml rename to releasenotes/notes/fixed-magnum-type-7406f0a60525f858.yaml diff --git a/shade/releasenotes/notes/flavor_fix-a53c6b326dc34a2c.yaml b/releasenotes/notes/flavor_fix-a53c6b326dc34a2c.yaml similarity index 100% rename from shade/releasenotes/notes/flavor_fix-a53c6b326dc34a2c.yaml rename to releasenotes/notes/flavor_fix-a53c6b326dc34a2c.yaml diff --git a/shade/releasenotes/notes/fnmatch-name-or-id-f658fe26f84086c8.yaml b/releasenotes/notes/fnmatch-name-or-id-f658fe26f84086c8.yaml similarity index 100% rename from shade/releasenotes/notes/fnmatch-name-or-id-f658fe26f84086c8.yaml rename to releasenotes/notes/fnmatch-name-or-id-f658fe26f84086c8.yaml diff --git a/shade/releasenotes/notes/get-limits-c383c512f8e01873.yaml b/releasenotes/notes/get-limits-c383c512f8e01873.yaml similarity index 100% rename from shade/releasenotes/notes/get-limits-c383c512f8e01873.yaml rename to releasenotes/notes/get-limits-c383c512f8e01873.yaml diff --git a/shade/releasenotes/notes/get-usage-72d249ff790d1b8f.yaml b/releasenotes/notes/get-usage-72d249ff790d1b8f.yaml similarity index 100% rename from shade/releasenotes/notes/get-usage-72d249ff790d1b8f.yaml rename to releasenotes/notes/get-usage-72d249ff790d1b8f.yaml diff --git a/shade/releasenotes/notes/get_object_api-968483adb016bce1.yaml b/releasenotes/notes/get_object_api-968483adb016bce1.yaml similarity index 100% rename from shade/releasenotes/notes/get_object_api-968483adb016bce1.yaml rename to releasenotes/notes/get_object_api-968483adb016bce1.yaml diff --git a/shade/releasenotes/notes/glance-image-pagination-0b4dfef22b25852b.yaml b/releasenotes/notes/glance-image-pagination-0b4dfef22b25852b.yaml similarity index 100% rename from shade/releasenotes/notes/glance-image-pagination-0b4dfef22b25852b.yaml rename to releasenotes/notes/glance-image-pagination-0b4dfef22b25852b.yaml diff --git a/shade/releasenotes/notes/grant-revoke-assignments-231d3f9596a1ae75.yaml b/releasenotes/notes/grant-revoke-assignments-231d3f9596a1ae75.yaml similarity index 100% rename from shade/releasenotes/notes/grant-revoke-assignments-231d3f9596a1ae75.yaml rename to releasenotes/notes/grant-revoke-assignments-231d3f9596a1ae75.yaml diff --git a/shade/releasenotes/notes/image-flavor-by-name-54865b00ebbf1004.yaml b/releasenotes/notes/image-flavor-by-name-54865b00ebbf1004.yaml similarity index 100% rename from shade/releasenotes/notes/image-flavor-by-name-54865b00ebbf1004.yaml rename to releasenotes/notes/image-flavor-by-name-54865b00ebbf1004.yaml diff --git a/shade/releasenotes/notes/image-from-volume-9acf7379f5995b5b.yaml b/releasenotes/notes/image-from-volume-9acf7379f5995b5b.yaml similarity index 100% rename from shade/releasenotes/notes/image-from-volume-9acf7379f5995b5b.yaml rename to releasenotes/notes/image-from-volume-9acf7379f5995b5b.yaml diff --git a/shade/releasenotes/notes/infer-secgroup-source-58d840aaf1a1f485.yaml b/releasenotes/notes/infer-secgroup-source-58d840aaf1a1f485.yaml similarity index 100% rename from shade/releasenotes/notes/infer-secgroup-source-58d840aaf1a1f485.yaml rename to releasenotes/notes/infer-secgroup-source-58d840aaf1a1f485.yaml diff --git a/os-client-config/releasenotes/notes/ironic-microversion-ba5b0f36f11196a6.yaml b/releasenotes/notes/ironic-microversion-ba5b0f36f11196a6.yaml similarity index 100% rename from os-client-config/releasenotes/notes/ironic-microversion-ba5b0f36f11196a6.yaml rename to releasenotes/notes/ironic-microversion-ba5b0f36f11196a6.yaml diff --git a/shade/releasenotes/notes/less-file-hashing-d2497337da5acbef.yaml b/releasenotes/notes/less-file-hashing-d2497337da5acbef.yaml similarity index 100% rename from shade/releasenotes/notes/less-file-hashing-d2497337da5acbef.yaml rename to releasenotes/notes/less-file-hashing-d2497337da5acbef.yaml diff --git a/shade/releasenotes/notes/list-az-names-a38c277d1192471b.yaml b/releasenotes/notes/list-az-names-a38c277d1192471b.yaml similarity index 100% rename from shade/releasenotes/notes/list-az-names-a38c277d1192471b.yaml rename to releasenotes/notes/list-az-names-a38c277d1192471b.yaml diff --git a/shade/releasenotes/notes/list-role-assignments-keystone-v2-b127b12b4860f50c.yaml b/releasenotes/notes/list-role-assignments-keystone-v2-b127b12b4860f50c.yaml similarity index 100% rename from shade/releasenotes/notes/list-role-assignments-keystone-v2-b127b12b4860f50c.yaml rename to releasenotes/notes/list-role-assignments-keystone-v2-b127b12b4860f50c.yaml diff --git a/shade/releasenotes/notes/list-servers-all-projects-349e6dc665ba2e8d.yaml b/releasenotes/notes/list-servers-all-projects-349e6dc665ba2e8d.yaml similarity index 100% rename from shade/releasenotes/notes/list-servers-all-projects-349e6dc665ba2e8d.yaml rename to releasenotes/notes/list-servers-all-projects-349e6dc665ba2e8d.yaml diff --git a/os-client-config/releasenotes/notes/load-yaml-3177efca78e5c67a.yaml b/releasenotes/notes/load-yaml-3177efca78e5c67a.yaml similarity index 100% rename from os-client-config/releasenotes/notes/load-yaml-3177efca78e5c67a.yaml rename to releasenotes/notes/load-yaml-3177efca78e5c67a.yaml diff --git a/shade/releasenotes/notes/log-request-ids-37507cb6eed9a7da.yaml b/releasenotes/notes/log-request-ids-37507cb6eed9a7da.yaml similarity index 100% rename from shade/releasenotes/notes/log-request-ids-37507cb6eed9a7da.yaml rename to releasenotes/notes/log-request-ids-37507cb6eed9a7da.yaml diff --git a/os-client-config/releasenotes/notes/magic-fixes-dca4ae4dac2441a8.yaml b/releasenotes/notes/magic-fixes-dca4ae4dac2441a8.yaml similarity index 100% rename from os-client-config/releasenotes/notes/magic-fixes-dca4ae4dac2441a8.yaml rename to releasenotes/notes/magic-fixes-dca4ae4dac2441a8.yaml diff --git a/os-client-config/releasenotes/notes/make-rest-client-dd3d365632a26fa0.yaml b/releasenotes/notes/make-rest-client-dd3d365632a26fa0.yaml similarity index 100% rename from os-client-config/releasenotes/notes/make-rest-client-dd3d365632a26fa0.yaml rename to releasenotes/notes/make-rest-client-dd3d365632a26fa0.yaml diff --git a/shade/releasenotes/notes/make_object_metadata_easier.yaml-e9751723e002e06f.yaml b/releasenotes/notes/make_object_metadata_easier.yaml-e9751723e002e06f.yaml similarity index 100% rename from shade/releasenotes/notes/make_object_metadata_easier.yaml-e9751723e002e06f.yaml rename to releasenotes/notes/make_object_metadata_easier.yaml-e9751723e002e06f.yaml diff --git a/releasenotes/notes/merge-shade-os-client-config-29878734ad643e33.yaml b/releasenotes/notes/merge-shade-os-client-config-29878734ad643e33.yaml new file mode 100644 index 000000000..f7718aabb --- /dev/null +++ b/releasenotes/notes/merge-shade-os-client-config-29878734ad643e33.yaml @@ -0,0 +1,4 @@ +--- +other: + - The shade and os-client-config libraries have been + merged into python-openstacksdk. diff --git a/shade/releasenotes/notes/meta-passthrough-d695bff4f9366b65.yaml b/releasenotes/notes/meta-passthrough-d695bff4f9366b65.yaml similarity index 100% rename from shade/releasenotes/notes/meta-passthrough-d695bff4f9366b65.yaml rename to releasenotes/notes/meta-passthrough-d695bff4f9366b65.yaml diff --git a/os-client-config/releasenotes/notes/min-max-legacy-version-301242466ddefa93.yaml b/releasenotes/notes/min-max-legacy-version-301242466ddefa93.yaml similarity index 100% rename from os-client-config/releasenotes/notes/min-max-legacy-version-301242466ddefa93.yaml rename to releasenotes/notes/min-max-legacy-version-301242466ddefa93.yaml diff --git a/shade/releasenotes/notes/multiple-updates-b48cc2f6db2e526d.yaml b/releasenotes/notes/multiple-updates-b48cc2f6db2e526d.yaml similarity index 100% rename from shade/releasenotes/notes/multiple-updates-b48cc2f6db2e526d.yaml rename to releasenotes/notes/multiple-updates-b48cc2f6db2e526d.yaml diff --git a/shade/releasenotes/notes/net_provider-dd64b697476b7094.yaml b/releasenotes/notes/net_provider-dd64b697476b7094.yaml similarity index 100% rename from shade/releasenotes/notes/net_provider-dd64b697476b7094.yaml rename to releasenotes/notes/net_provider-dd64b697476b7094.yaml diff --git a/os-client-config/releasenotes/notes/network-list-e6e9dafdd8446263.yaml b/releasenotes/notes/network-list-e6e9dafdd8446263.yaml similarity index 100% rename from os-client-config/releasenotes/notes/network-list-e6e9dafdd8446263.yaml rename to releasenotes/notes/network-list-e6e9dafdd8446263.yaml diff --git a/shade/releasenotes/notes/network-quotas-b98cce9ffeffdbf4.yaml b/releasenotes/notes/network-quotas-b98cce9ffeffdbf4.yaml similarity index 100% rename from shade/releasenotes/notes/network-quotas-b98cce9ffeffdbf4.yaml rename to releasenotes/notes/network-quotas-b98cce9ffeffdbf4.yaml diff --git a/shade/releasenotes/notes/new-floating-attributes-213cdf5681d337e1.yaml b/releasenotes/notes/new-floating-attributes-213cdf5681d337e1.yaml similarity index 100% rename from shade/releasenotes/notes/new-floating-attributes-213cdf5681d337e1.yaml rename to releasenotes/notes/new-floating-attributes-213cdf5681d337e1.yaml diff --git a/shade/releasenotes/notes/no-more-troveclient-0a4739c21432ac63.yaml b/releasenotes/notes/no-more-troveclient-0a4739c21432ac63.yaml similarity index 100% rename from shade/releasenotes/notes/no-more-troveclient-0a4739c21432ac63.yaml rename to releasenotes/notes/no-more-troveclient-0a4739c21432ac63.yaml diff --git a/shade/releasenotes/notes/norm_role_assignments-a13f41768e62d40c.yaml b/releasenotes/notes/norm_role_assignments-a13f41768e62d40c.yaml similarity index 100% rename from shade/releasenotes/notes/norm_role_assignments-a13f41768e62d40c.yaml rename to releasenotes/notes/norm_role_assignments-a13f41768e62d40c.yaml diff --git a/shade/releasenotes/notes/normalize-images-1331bea7bfffa36a.yaml b/releasenotes/notes/normalize-images-1331bea7bfffa36a.yaml similarity index 100% rename from shade/releasenotes/notes/normalize-images-1331bea7bfffa36a.yaml rename to releasenotes/notes/normalize-images-1331bea7bfffa36a.yaml diff --git a/shade/releasenotes/notes/nova-flavor-to-rest-0a5757e35714a690.yaml b/releasenotes/notes/nova-flavor-to-rest-0a5757e35714a690.yaml similarity index 100% rename from shade/releasenotes/notes/nova-flavor-to-rest-0a5757e35714a690.yaml rename to releasenotes/notes/nova-flavor-to-rest-0a5757e35714a690.yaml diff --git a/shade/releasenotes/notes/nova-old-microversion-5e4b8e239ba44096.yaml b/releasenotes/notes/nova-old-microversion-5e4b8e239ba44096.yaml similarity index 100% rename from shade/releasenotes/notes/nova-old-microversion-5e4b8e239ba44096.yaml rename to releasenotes/notes/nova-old-microversion-5e4b8e239ba44096.yaml diff --git a/os-client-config/releasenotes/notes/option-precedence-1fecab21fdfb2c33.yaml b/releasenotes/notes/option-precedence-1fecab21fdfb2c33.yaml similarity index 100% rename from os-client-config/releasenotes/notes/option-precedence-1fecab21fdfb2c33.yaml rename to releasenotes/notes/option-precedence-1fecab21fdfb2c33.yaml diff --git a/shade/releasenotes/notes/remove-magnumclient-875b3e513f98f57c.yaml b/releasenotes/notes/remove-magnumclient-875b3e513f98f57c.yaml similarity index 100% rename from shade/releasenotes/notes/remove-magnumclient-875b3e513f98f57c.yaml rename to releasenotes/notes/remove-magnumclient-875b3e513f98f57c.yaml diff --git a/shade/releasenotes/notes/remove-novaclient-3f8d4db20d5f9582.yaml b/releasenotes/notes/remove-novaclient-3f8d4db20d5f9582.yaml similarity index 100% rename from shade/releasenotes/notes/remove-novaclient-3f8d4db20d5f9582.yaml rename to releasenotes/notes/remove-novaclient-3f8d4db20d5f9582.yaml diff --git a/shade/releasenotes/notes/removed-glanceclient-105c7fba9481b9be.yaml b/releasenotes/notes/removed-glanceclient-105c7fba9481b9be.yaml similarity index 100% rename from shade/releasenotes/notes/removed-glanceclient-105c7fba9481b9be.yaml rename to releasenotes/notes/removed-glanceclient-105c7fba9481b9be.yaml diff --git a/shade/releasenotes/notes/removed-swiftclient-aff22bfaeee5f59f.yaml b/releasenotes/notes/removed-swiftclient-aff22bfaeee5f59f.yaml similarity index 100% rename from shade/releasenotes/notes/removed-swiftclient-aff22bfaeee5f59f.yaml rename to releasenotes/notes/removed-swiftclient-aff22bfaeee5f59f.yaml diff --git a/shade/releasenotes/notes/router_ext_gw-b86582317bca8b39.yaml b/releasenotes/notes/router_ext_gw-b86582317bca8b39.yaml similarity index 100% rename from shade/releasenotes/notes/router_ext_gw-b86582317bca8b39.yaml rename to releasenotes/notes/router_ext_gw-b86582317bca8b39.yaml diff --git a/os-client-config/releasenotes/notes/sdk-helper-41f8d815cfbcfb00.yaml b/releasenotes/notes/sdk-helper-41f8d815cfbcfb00.yaml similarity index 100% rename from os-client-config/releasenotes/notes/sdk-helper-41f8d815cfbcfb00.yaml rename to releasenotes/notes/sdk-helper-41f8d815cfbcfb00.yaml diff --git a/shade/releasenotes/notes/server-create-error-id-66c698c7e633fb8b.yaml b/releasenotes/notes/server-create-error-id-66c698c7e633fb8b.yaml similarity index 100% rename from shade/releasenotes/notes/server-create-error-id-66c698c7e633fb8b.yaml rename to releasenotes/notes/server-create-error-id-66c698c7e633fb8b.yaml diff --git a/shade/releasenotes/notes/server-security-groups-840ab28c04f359de.yaml b/releasenotes/notes/server-security-groups-840ab28c04f359de.yaml similarity index 100% rename from shade/releasenotes/notes/server-security-groups-840ab28c04f359de.yaml rename to releasenotes/notes/server-security-groups-840ab28c04f359de.yaml diff --git a/shade/releasenotes/notes/service_enabled_flag-c917b305d3f2e8fd.yaml b/releasenotes/notes/service_enabled_flag-c917b305d3f2e8fd.yaml similarity index 100% rename from shade/releasenotes/notes/service_enabled_flag-c917b305d3f2e8fd.yaml rename to releasenotes/notes/service_enabled_flag-c917b305d3f2e8fd.yaml diff --git a/os-client-config/releasenotes/notes/session-client-b581a6e5d18c8f04.yaml b/releasenotes/notes/session-client-b581a6e5d18c8f04.yaml similarity index 100% rename from os-client-config/releasenotes/notes/session-client-b581a6e5d18c8f04.yaml rename to releasenotes/notes/session-client-b581a6e5d18c8f04.yaml diff --git a/os-client-config/releasenotes/notes/shade-helper-568f8cb372eef6d9.yaml b/releasenotes/notes/shade-helper-568f8cb372eef6d9.yaml similarity index 100% rename from os-client-config/releasenotes/notes/shade-helper-568f8cb372eef6d9.yaml rename to releasenotes/notes/shade-helper-568f8cb372eef6d9.yaml diff --git a/shade/releasenotes/notes/stack-update-5886e91fd6e423bf.yaml b/releasenotes/notes/stack-update-5886e91fd6e423bf.yaml similarity index 100% rename from shade/releasenotes/notes/stack-update-5886e91fd6e423bf.yaml rename to releasenotes/notes/stack-update-5886e91fd6e423bf.yaml diff --git a/os-client-config/releasenotes/notes/started-using-reno-242e2b0cd27f9480.yaml b/releasenotes/notes/started-using-reno-242e2b0cd27f9480.yaml similarity index 100% rename from os-client-config/releasenotes/notes/started-using-reno-242e2b0cd27f9480.yaml rename to releasenotes/notes/started-using-reno-242e2b0cd27f9480.yaml diff --git a/shade/releasenotes/notes/stream-to-file-91f48d6dcea399c6.yaml b/releasenotes/notes/stream-to-file-91f48d6dcea399c6.yaml similarity index 100% rename from shade/releasenotes/notes/stream-to-file-91f48d6dcea399c6.yaml rename to releasenotes/notes/stream-to-file-91f48d6dcea399c6.yaml diff --git a/shade/releasenotes/notes/strict-mode-d493abc0c3e87945.yaml b/releasenotes/notes/strict-mode-d493abc0c3e87945.yaml similarity index 100% rename from shade/releasenotes/notes/strict-mode-d493abc0c3e87945.yaml rename to releasenotes/notes/strict-mode-d493abc0c3e87945.yaml diff --git a/shade/releasenotes/notes/swift-upload-lock-d18f3d42b3a0719a.yaml b/releasenotes/notes/swift-upload-lock-d18f3d42b3a0719a.yaml similarity index 100% rename from shade/releasenotes/notes/swift-upload-lock-d18f3d42b3a0719a.yaml rename to releasenotes/notes/swift-upload-lock-d18f3d42b3a0719a.yaml diff --git a/shade/releasenotes/notes/update_endpoint-f87c1f42d0c0d1ef.yaml b/releasenotes/notes/update_endpoint-f87c1f42d0c0d1ef.yaml similarity index 100% rename from shade/releasenotes/notes/update_endpoint-f87c1f42d0c0d1ef.yaml rename to releasenotes/notes/update_endpoint-f87c1f42d0c0d1ef.yaml diff --git a/shade/releasenotes/notes/use-interface-ip-c5cb3e7c91150096.yaml b/releasenotes/notes/use-interface-ip-c5cb3e7c91150096.yaml similarity index 100% rename from shade/releasenotes/notes/use-interface-ip-c5cb3e7c91150096.yaml rename to releasenotes/notes/use-interface-ip-c5cb3e7c91150096.yaml diff --git a/os-client-config/releasenotes/notes/vendor-updates-f11184ba56bb27cf.yaml b/releasenotes/notes/vendor-updates-f11184ba56bb27cf.yaml similarity index 100% rename from os-client-config/releasenotes/notes/vendor-updates-f11184ba56bb27cf.yaml rename to releasenotes/notes/vendor-updates-f11184ba56bb27cf.yaml diff --git a/shade/releasenotes/notes/version-discovery-a501c4e9e9869f77.yaml b/releasenotes/notes/version-discovery-a501c4e9e9869f77.yaml similarity index 100% rename from shade/releasenotes/notes/version-discovery-a501c4e9e9869f77.yaml rename to releasenotes/notes/version-discovery-a501c4e9e9869f77.yaml diff --git a/shade/releasenotes/notes/volume-quotas-5b674ee8c1f71eb6.yaml b/releasenotes/notes/volume-quotas-5b674ee8c1f71eb6.yaml similarity index 100% rename from shade/releasenotes/notes/volume-quotas-5b674ee8c1f71eb6.yaml rename to releasenotes/notes/volume-quotas-5b674ee8c1f71eb6.yaml diff --git a/shade/releasenotes/notes/volume-types-a07a14ae668e7dd2.yaml b/releasenotes/notes/volume-types-a07a14ae668e7dd2.yaml similarity index 100% rename from shade/releasenotes/notes/volume-types-a07a14ae668e7dd2.yaml rename to releasenotes/notes/volume-types-a07a14ae668e7dd2.yaml diff --git a/shade/releasenotes/notes/wait-on-image-snapshot-27cd2eacab2fabd8.yaml b/releasenotes/notes/wait-on-image-snapshot-27cd2eacab2fabd8.yaml similarity index 100% rename from shade/releasenotes/notes/wait-on-image-snapshot-27cd2eacab2fabd8.yaml rename to releasenotes/notes/wait-on-image-snapshot-27cd2eacab2fabd8.yaml diff --git a/shade/releasenotes/notes/wait_for_server-8dc8446b7c673d36.yaml b/releasenotes/notes/wait_for_server-8dc8446b7c673d36.yaml similarity index 100% rename from shade/releasenotes/notes/wait_for_server-8dc8446b7c673d36.yaml rename to releasenotes/notes/wait_for_server-8dc8446b7c673d36.yaml diff --git a/shade/releasenotes/notes/workaround-transitive-deps-1e7a214f3256b77e.yaml b/releasenotes/notes/workaround-transitive-deps-1e7a214f3256b77e.yaml similarity index 100% rename from shade/releasenotes/notes/workaround-transitive-deps-1e7a214f3256b77e.yaml rename to releasenotes/notes/workaround-transitive-deps-1e7a214f3256b77e.yaml diff --git a/os-client-config/releasenotes/source/_static/.placeholder b/releasenotes/source/_static/.placeholder similarity index 100% rename from os-client-config/releasenotes/source/_static/.placeholder rename to releasenotes/source/_static/.placeholder diff --git a/os-client-config/releasenotes/source/_templates/.placeholder b/releasenotes/source/_templates/.placeholder similarity index 100% rename from os-client-config/releasenotes/source/_templates/.placeholder rename to releasenotes/source/_templates/.placeholder diff --git a/shade/releasenotes/source/conf.py b/releasenotes/source/conf.py similarity index 100% rename from shade/releasenotes/source/conf.py rename to releasenotes/source/conf.py diff --git a/shade/releasenotes/source/index.rst b/releasenotes/source/index.rst similarity index 100% rename from shade/releasenotes/source/index.rst rename to releasenotes/source/index.rst diff --git a/shade/releasenotes/source/mainline.rst b/releasenotes/source/mainline.rst similarity index 100% rename from shade/releasenotes/source/mainline.rst rename to releasenotes/source/mainline.rst diff --git a/os-client-config/releasenotes/source/mitaka.rst b/releasenotes/source/mitaka.rst similarity index 100% rename from os-client-config/releasenotes/source/mitaka.rst rename to releasenotes/source/mitaka.rst diff --git a/os-client-config/releasenotes/source/newton.rst b/releasenotes/source/newton.rst similarity index 100% rename from os-client-config/releasenotes/source/newton.rst rename to releasenotes/source/newton.rst diff --git a/os-client-config/releasenotes/source/ocata.rst b/releasenotes/source/ocata.rst similarity index 100% rename from os-client-config/releasenotes/source/ocata.rst rename to releasenotes/source/ocata.rst diff --git a/os-client-config/releasenotes/source/pike.rst b/releasenotes/source/pike.rst similarity index 100% rename from os-client-config/releasenotes/source/pike.rst rename to releasenotes/source/pike.rst diff --git a/shade/releasenotes/source/unreleased.rst b/releasenotes/source/unreleased.rst similarity index 100% rename from shade/releasenotes/source/unreleased.rst rename to releasenotes/source/unreleased.rst diff --git a/shade/releasenotes/notes/started-using-reno-242e2b0cd27f9480.yaml b/shade/releasenotes/notes/started-using-reno-242e2b0cd27f9480.yaml deleted file mode 100644 index d7cfb5145..000000000 --- a/shade/releasenotes/notes/started-using-reno-242e2b0cd27f9480.yaml +++ /dev/null @@ -1,3 +0,0 @@ ---- -other: -- Started using reno for release notes. diff --git a/shade/releasenotes/source/_static/.placeholder b/shade/releasenotes/source/_static/.placeholder deleted file mode 100644 index e69de29bb..000000000 diff --git a/shade/releasenotes/source/_templates/.placeholder b/shade/releasenotes/source/_templates/.placeholder deleted file mode 100644 index e69de29bb..000000000 diff --git a/shade/releasenotes/source/pike.rst b/shade/releasenotes/source/pike.rst deleted file mode 100644 index e43bfc0ce..000000000 --- a/shade/releasenotes/source/pike.rst +++ /dev/null @@ -1,6 +0,0 @@ -=================================== - Pike Series Release Notes -=================================== - -.. release-notes:: - :branch: stable/pike From aae4528eeb706a9868b0ea588b325da3fcd69853 Mon Sep 17 00:00:00 2001 From: zhangyangyang Date: Thu, 21 Sep 2017 23:26:19 +0800 Subject: [PATCH 1825/3836] Cleanup test-requirements python-subunit is not used directly anywhere and it is dependency of both testrepository and os-testr (probably was used by some tox wrapper script before) Change-Id: Ifd32c3add54e8cc752908be79242b8f3dcc145f2 --- test-requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index c9153ad55..40cb35668 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,7 +8,6 @@ coverage!=4.4,>=4.0 # Apache-2.0 doc8>=0.6.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD mock>=2.0.0 # BSD -python-subunit>=0.0.18 # Apache-2.0/BSD openstackdocstheme>=1.17.0 # Apache-2.0 os-testr>=1.0.0 # Apache-2.0 requests>=2.14.2 # Apache-2.0 From 689d9e872475f0981508f19e0058ab065bca62d5 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Sun, 1 Oct 2017 07:30:57 +0200 Subject: [PATCH 1826/3836] Consume publish-openstack-sphinx-docs It's been updated in openstack-zuul-jobs, consume the new thing here. Change-Id: Ieede8432bc9c41eb786923ffa94cbcbecc89a5ec Needed-By: Ia5ecbdb48d3c425a2a15945b4f2e620080b7b3d5 Depends-On: I2b75479fc925822c13fd375bff66926e7766b912 --- .zuul.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index eb219b914..395b00584 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -2,7 +2,7 @@ name: openstack-infra/shade templates: - publish-to-pypi - - publish-openstack-python-docs + - publish-openstack-sphinx-docs check: jobs: - openstack-tox-py35 From 5eead744927fd18fafc98e438f45639cd133e275 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Thu, 14 Sep 2017 16:27:37 +0000 Subject: [PATCH 1827/3836] Add support for network quota details command Neutron recently introduced API call to get details about current usage of quota by given project. This patch adds support for this command in OpenstackSDK. Partially-Implements: blueprint quota-counts Related-Bug: #1716043 Change-Id: I707b491e8562495cc83c394aabdd3b5717bf67f9 --- openstack/network/v2/_proxy.py | 12 ++++- openstack/network/v2/quota.py | 44 +++++++++++++++++++ .../tests/functional/network/v2/test_quota.py | 8 ++++ openstack/tests/unit/network/v2/test_proxy.py | 13 ++++++ 4 files changed, 75 insertions(+), 2 deletions(-) diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index df86e9740..695fdd5df 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -2004,19 +2004,27 @@ def delete_quota(self, quota, ignore_missing=True): """ self._delete(_quota.Quota, quota, ignore_missing=ignore_missing) - def get_quota(self, quota): + def get_quota(self, quota, details=False): """Get a quota :param quota: The value can be the ID of a quota or a :class:`~openstack.network.v2.quota.Quota` instance. The ID of a quota is the same as the project ID for the quota. + :param details: If set to True, details about quota usage will + be returned. :returns: One :class:`~openstack.network.v2.quota.Quota` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get(_quota.Quota, quota) + if details: + quota_obj = self._get_resource(_quota.Quota, quota) + quota = self._get(_quota.QuotaDetails, project=quota_obj.id, + requires_id=False) + else: + quota = self._get(_quota.Quota, quota) + return quota def get_quota_default(self, quota): """Get a default quota diff --git a/openstack/network/v2/quota.py b/openstack/network/v2/quota.py index c37e712ad..283242088 100644 --- a/openstack/network/v2/quota.py +++ b/openstack/network/v2/quota.py @@ -82,3 +82,47 @@ class QuotaDefault(Quota): # Properties #: The ID of the project. project = resource.URI('project') + + +class QuotaDetails(Quota): + base_path = '/quotas/%(project)s/details' + + # capabilities + allow_retrieve = True + allow_update = False + allow_delete = False + allow_list = False + + # Properties + #: The ID of the project. + project = resource.URI('project') + #: The maximum amount of floating IPs you can have. *Type: dict* + floating_ips = resource.Body('floatingip', type=dict) + #: The maximum amount of health monitors you can create. *Type: dict* + health_monitors = resource.Body('healthmonitor', type=dict) + #: The maximum amount of listeners you can create. *Type: dict* + listeners = resource.Body('listener', type=dict) + #: The maximum amount of load balancers you can create. *Type: dict* + load_balancers = resource.Body('loadbalancer', type=dict) + #: The maximum amount of L7 policies you can create. *Type: dict* + l7_policies = resource.Body('l7policy', type=dict) + #: The maximum amount of networks you can create. *Type: dict* + networks = resource.Body('network', type=dict) + #: The maximum amount of pools you can create. *Type: dict* + pools = resource.Body('pool', type=dict) + #: The maximum amount of ports you can create. *Type: dict* + ports = resource.Body('port', type=dict) + #: The ID of the project these quota values are for. + project_id = resource.Body('tenant_id', alternate_id=True) + #: The maximum amount of RBAC policies you can create. *Type: dict* + rbac_policies = resource.Body('rbac_policy', type=dict) + #: The maximum amount of routers you can create. *Type: int* + routers = resource.Body('router', type=dict) + #: The maximum amount of subnets you can create. *Type: dict* + subnets = resource.Body('subnet', type=dict) + #: The maximum amount of subnet pools you can create. *Type: dict* + subnet_pools = resource.Body('subnetpool', type=dict) + #: The maximum amount of security group rules you can create. *Type: dict* + security_group_rules = resource.Body('security_group_rule', type=dict) + #: The maximum amount of security groups you can create. *Type: dict* + security_groups = resource.Body('security_group', type=dict) diff --git a/openstack/tests/functional/network/v2/test_quota.py b/openstack/tests/functional/network/v2/test_quota.py index 9fa100453..23dfa9774 100644 --- a/openstack/tests/functional/network/v2/test_quota.py +++ b/openstack/tests/functional/network/v2/test_quota.py @@ -20,6 +20,14 @@ def test_list(self): self.assertIsNotNone(qot.project_id) self.assertIsNotNone(qot.networks) + def test_list_details(self): + expected_keys = ['limit', 'used', 'reserved'] + project_id = self.conn.session.get_project_id() + quota_details = self.conn.network.get_quota(project_id, details=True) + for details in quota_details._body.attributes.values(): + for expected_key in expected_keys: + self.assertTrue(expected_key in details.keys()) + def test_set(self): attrs = {'networks': 123456789} for project_quota in self.conn.network.quotas(): diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 06232cd23..cedfeb186 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -678,6 +678,19 @@ def test_quota_delete_ignore(self): def test_quota_get(self): self.verify_get(self.proxy.get_quota, quota.Quota) + @mock.patch.object(proxy_base2.BaseProxy, "_get_resource") + def test_quota_get_details(self, mock_get): + fake_quota = mock.Mock(project_id='PROJECT') + mock_get.return_value = fake_quota + self._verify2("openstack.proxy2.BaseProxy._get", + self.proxy.get_quota, + method_args=['QUOTA_ID'], + method_kwargs={'details': True}, + expected_args=[quota.QuotaDetails], + expected_kwargs={'project': fake_quota.id, + 'requires_id': False}) + mock_get.assert_called_once_with(quota.Quota, 'QUOTA_ID') + @mock.patch.object(proxy_base2.BaseProxy, "_get_resource") def test_quota_default_get(self, mock_get): fake_quota = mock.Mock(project_id='PROJECT') From 65293358a0d3cd7055987b18e16b9be5e8a0261b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 4 Oct 2017 12:39:43 -0500 Subject: [PATCH 1828/3836] Move shade and os-client-config python content Make shade be openstack.cloud and os-client-config be openstack.config. Change-Id: I1d28be82a72cc1a30d6be306257fc1f0736ed604 --- examples/connect.py | 13 +- {shade/shade => openstack/cloud}/__init__.py | 21 +- {shade/shade => openstack/cloud}/_adapter.py | 10 +- .../cloud/_heat}/__init__.py | 0 .../cloud}/_heat/environment_format.py | 2 +- .../cloud}/_heat/event_utils.py | 2 +- .../cloud}/_heat/template_format.py | 0 .../cloud}/_heat/template_utils.py | 8 +- .../shade => openstack/cloud}/_heat/utils.py | 2 +- .../cloud}/_log.py | 0 .../shade => openstack/cloud}/_normalize.py | 4 + {shade/shade => openstack/cloud}/_tasks.py | 2 +- {shade/shade => openstack/cloud}/_utils.py | 16 +- .../_heat => openstack/cloud/cmd}/__init__.py | 0 .../cloud}/cmd/inventory.py | 10 +- {shade/shade => openstack/cloud}/exc.py | 8 +- {shade/shade => openstack/cloud}/inventory.py | 14 +- {shade/shade => openstack/cloud}/meta.py | 6 +- .../cloud}/openstackcloud.py | 83 +++--- .../cloud}/operatorcloud.py | 8 +- .../shade => openstack/cloud}/task_manager.py | 8 +- .../cmd => openstack/cloud/tests}/__init__.py | 0 .../cloud}/tests/ansible/README.txt | 0 .../tests/ansible/hooks/post_test_hook.sh | 0 .../tests/ansible/roles/auth/tasks/main.yml | 0 .../roles/client_config/tasks/main.yml | 0 .../tests/ansible/roles/group/tasks/main.yml | 0 .../tests/ansible/roles/group/vars/main.yml | 0 .../tests/ansible/roles/image/tasks/main.yml | 0 .../tests/ansible/roles/image/vars/main.yml | 0 .../ansible/roles/keypair/tasks/main.yml | 0 .../tests/ansible/roles/keypair/vars/main.yml | 0 .../roles/keystone_domain/tasks/main.yml | 0 .../roles/keystone_domain/vars/main.yml | 0 .../roles/keystone_role/tasks/main.yml | 0 .../ansible/roles/keystone_role/vars/main.yml | 0 .../ansible/roles/network/tasks/main.yml | 0 .../tests/ansible/roles/network/vars/main.yml | 0 .../ansible/roles/nova_flavor/tasks/main.yml | 0 .../tests/ansible/roles/object/tasks/main.yml | 0 .../tests/ansible/roles/port/tasks/main.yml | 0 .../tests/ansible/roles/port/vars/main.yml | 0 .../tests/ansible/roles/router/tasks/main.yml | 0 .../tests/ansible/roles/router/vars/main.yml | 0 .../roles/security_group/tasks/main.yml | 0 .../roles/security_group/vars/main.yml | 0 .../tests/ansible/roles/server/tasks/main.yml | 0 .../tests/ansible/roles/server/vars/main.yaml | 0 .../tests/ansible/roles/subnet/tasks/main.yml | 0 .../tests/ansible/roles/subnet/vars/main.yml | 0 .../tests/ansible/roles/user/tasks/main.yml | 0 .../ansible/roles/user_group/tasks/main.yml | 0 .../tests/ansible/roles/volume/tasks/main.yml | 0 .../cloud}/tests/ansible/run.yml | 0 .../shade => openstack/cloud}/tests/base.py | 0 .../shade => openstack/cloud}/tests/fakes.py | 10 +- .../cloud/tests/functional}/__init__.py | 0 .../cloud}/tests/functional/base.py | 10 +- .../tests/functional/hooks/post_test_hook.sh | 0 .../cloud}/tests/functional/test_aggregate.py | 2 +- .../functional/test_cluster_templates.py | 6 +- .../cloud}/tests/functional/test_compute.py | 8 +- .../cloud}/tests/functional/test_devstack.py | 2 +- .../cloud}/tests/functional/test_domain.py | 7 +- .../cloud}/tests/functional/test_endpoints.py | 6 +- .../cloud}/tests/functional/test_flavor.py | 4 +- .../tests/functional/test_floating_ip.py | 10 +- .../tests/functional/test_floating_ip_pool.py | 2 +- .../cloud}/tests/functional/test_groups.py | 7 +- .../cloud}/tests/functional/test_identity.py | 4 +- .../cloud}/tests/functional/test_image.py | 2 +- .../cloud}/tests/functional/test_inventory.py | 6 +- .../cloud}/tests/functional/test_keypairs.py | 4 +- .../cloud}/tests/functional/test_limits.py | 2 +- .../tests/functional/test_magnum_services.py | 2 +- .../cloud}/tests/functional/test_network.py | 4 +- .../cloud}/tests/functional/test_object.py | 4 +- .../cloud}/tests/functional/test_port.py | 4 +- .../cloud}/tests/functional/test_project.py | 4 +- .../test_qos_bandwidth_limit_rule.py | 4 +- .../functional/test_qos_dscp_marking_rule.py | 4 +- .../test_qos_minimum_bandwidth_rule.py | 4 +- .../tests/functional/test_qos_policy.py | 4 +- .../cloud}/tests/functional/test_quotas.py | 2 +- .../tests/functional/test_range_search.py | 4 +- .../cloud}/tests/functional/test_recordset.py | 2 +- .../cloud}/tests/functional/test_router.py | 4 +- .../tests/functional/test_security_groups.py | 2 +- .../tests/functional/test_server_group.py | 2 +- .../cloud}/tests/functional/test_services.py | 6 +- .../cloud}/tests/functional/test_stack.py | 6 +- .../cloud}/tests/functional/test_usage.py | 2 +- .../cloud}/tests/functional/test_users.py | 6 +- .../cloud}/tests/functional/test_volume.py | 6 +- .../tests/functional/test_volume_backup.py | 2 +- .../tests/functional/test_volume_type.py | 4 +- .../cloud}/tests/functional/test_zone.py | 2 +- .../cloud}/tests/functional/util.py | 0 .../cloud/tests/unit}/__init__.py | 0 .../cloud}/tests/unit/base.py | 19 +- .../cloud}/tests/unit/fixtures/baremetal.json | 0 .../tests/unit/fixtures/catalog-v2.json | 0 .../tests/unit/fixtures/catalog-v3.json | 0 .../tests/unit/fixtures/clouds/clouds.yaml | 0 .../unit/fixtures/clouds/clouds_cache.yaml | 0 .../cloud}/tests/unit/fixtures/discovery.json | 0 .../cloud}/tests/unit/fixtures/dns.json | 0 .../unit/fixtures/image-version-broken.json | 0 .../tests/unit/fixtures/image-version-v1.json | 0 .../tests/unit/fixtures/image-version-v2.json | 0 .../tests/unit/fixtures/image-version.json | 0 .../cloud}/tests/unit/test__adapter.py | 4 +- .../cloud}/tests/unit/test__utils.py | 6 +- .../cloud}/tests/unit/test_aggregate.py | 4 +- .../tests/unit/test_availability_zones.py | 4 +- .../cloud}/tests/unit/test_baremetal_node.py | 6 +- .../cloud}/tests/unit/test_caching.py | 20 +- .../tests/unit/test_cluster_templates.py | 7 +- .../cloud}/tests/unit/test_create_server.py | 20 +- .../tests/unit/test_create_volume_snapshot.py | 8 +- .../cloud}/tests/unit/test_delete_server.py | 6 +- .../tests/unit/test_delete_volume_snapshot.py | 8 +- .../cloud}/tests/unit/test_domain_params.py | 22 +- .../cloud}/tests/unit/test_domains.py | 10 +- .../cloud}/tests/unit/test_endpoints.py | 6 +- .../cloud}/tests/unit/test_flavors.py | 10 +- .../tests/unit/test_floating_ip_common.py | 8 +- .../tests/unit/test_floating_ip_neutron.py | 6 +- .../tests/unit/test_floating_ip_nova.py | 4 +- .../tests/unit/test_floating_ip_pool.py | 6 +- .../cloud}/tests/unit/test_groups.py | 2 +- .../cloud}/tests/unit/test_identity_roles.py | 10 +- .../cloud}/tests/unit/test_image.py | 137 +++++----- .../cloud}/tests/unit/test_image_snapshot.py | 6 +- .../cloud}/tests/unit/test_inventory.py | 24 +- .../cloud}/tests/unit/test_keypair.py | 6 +- .../cloud}/tests/unit/test_limits.py | 2 +- .../cloud}/tests/unit/test_magnum_services.py | 2 +- .../cloud}/tests/unit/test_meta.py | 60 ++--- .../cloud}/tests/unit/test_network.py | 8 +- .../cloud}/tests/unit/test_normalize.py | 2 +- .../cloud}/tests/unit/test_object.py | 33 +-- .../cloud}/tests/unit/test_operator_noauth.py | 8 +- .../cloud}/tests/unit/test_port.py | 4 +- .../cloud}/tests/unit/test_project.py | 10 +- .../unit/test_qos_bandwidth_limit_rule.py | 4 +- .../tests/unit/test_qos_dscp_marking_rule.py | 4 +- .../unit/test_qos_minimum_bandwidth_rule.py | 4 +- .../cloud}/tests/unit/test_qos_policy.py | 4 +- .../cloud}/tests/unit/test_qos_rule_type.py | 4 +- .../cloud}/tests/unit/test_quotas.py | 4 +- .../cloud}/tests/unit/test_rebuild_server.py | 6 +- .../cloud}/tests/unit/test_recordset.py | 6 +- .../cloud}/tests/unit/test_role_assignment.py | 4 +- .../cloud}/tests/unit/test_router.py | 4 +- .../cloud}/tests/unit/test_security_groups.py | 18 +- .../cloud}/tests/unit/test_server_console.py | 4 +- .../tests/unit/test_server_delete_metadata.py | 6 +- .../cloud}/tests/unit/test_server_group.py | 4 +- .../tests/unit/test_server_set_metadata.py | 6 +- .../cloud}/tests/unit/test_services.py | 6 +- .../cloud}/tests/unit/test_shade.py | 16 +- .../cloud}/tests/unit/test_shade_operator.py | 54 ++-- .../cloud}/tests/unit/test_stack.py | 20 +- .../cloud}/tests/unit/test_subnet.py | 4 +- .../cloud}/tests/unit/test_task_manager.py | 4 +- .../cloud}/tests/unit/test_update_server.py | 6 +- .../cloud}/tests/unit/test_usage.py | 2 +- .../cloud}/tests/unit/test_users.py | 6 +- .../cloud}/tests/unit/test_volume.py | 20 +- .../cloud}/tests/unit/test_volume_access.py | 9 +- .../cloud}/tests/unit/test_volume_backups.py | 4 +- .../cloud}/tests/unit/test_zone.py | 6 +- .../config}/__init__.py | 6 +- {shade/shade => openstack/config}/_log.py | 0 .../config}/cloud_config.py | 2 +- .../config}/config.py | 0 .../config}/constructors.json | 0 .../config}/constructors.py | 0 .../config}/defaults.json | 0 .../config}/defaults.py | 0 .../config}/exceptions.py | 0 .../config}/schema.json | 0 .../config/tests}/__init__.py | 0 .../config}/tests/base.py | 2 +- .../config}/tests/test_cloud_config.py | 0 .../config}/tests/test_config.py | 0 .../config}/tests/test_environ.py | 0 .../config}/tests/test_init.py | 2 +- .../config}/tests/test_json.py | 0 .../config}/vendor-schema.json | 0 .../config}/vendors/__init__.py | 0 .../config}/vendors/auro.json | 0 .../config}/vendors/bluebox.json | 0 .../config}/vendors/catalyst.json | 0 .../config}/vendors/citycloud.json | 0 .../config}/vendors/conoha.json | 0 .../config}/vendors/datacentred.json | 0 .../config}/vendors/dreamcompute.json | 0 .../config}/vendors/dreamhost.json | 0 .../config}/vendors/elastx.json | 0 .../config}/vendors/entercloudsuite.json | 0 .../config}/vendors/fuga.json | 0 .../config}/vendors/ibmcloud.json | 0 .../config}/vendors/internap.json | 0 .../config}/vendors/otc.json | 0 .../config}/vendors/ovh.json | 0 .../config}/vendors/rackspace.json | 0 .../config}/vendors/switchengines.json | 0 .../config}/vendors/ultimum.json | 0 .../config}/vendors/unitedstack.json | 0 .../config}/vendors/vexxhost.json | 0 .../config}/vendors/zetta.json | 0 openstack/connection.py | 2 +- openstack/tests/functional/base.py | 2 +- openstack/tests/unit/test_connection.py | 2 +- os-client-config/doc/source/user/using.rst | 26 +- os-client-config/tools/keystone_version.py | 2 +- os-client-config/tools/nova_version.py | 2 +- .../log-request-ids-37507cb6eed9a7da.yaml | 2 +- .../remove-novaclient-3f8d4db20d5f9582.yaml | 2 +- setup.cfg | 5 + shade/.gitreview | 2 +- shade/HACKING.rst | 2 +- shade/README.rst | 6 +- shade/doc/source/contributor/coding.rst | 4 +- .../source/user/examples/cleanup-servers.py | 6 +- .../user/examples/create-server-dict.py | 6 +- .../user/examples/create-server-name-or-id.py | 6 +- .../doc/source/user/examples/debug-logging.py | 6 +- .../doc/source/user/examples/find-an-image.py | 6 +- .../user/examples/http-debug-logging.py | 6 +- .../source/user/examples/munch-dict-object.py | 6 +- .../doc/source/user/examples/normalization.py | 6 +- .../user/examples/server-information.py | 6 +- .../examples/service-conditional-overrides.py | 6 +- .../user/examples/service-conditionals.py | 6 +- shade/doc/source/user/examples/strict-mode.py | 6 +- .../user/examples/upload-large-object.py | 6 +- .../doc/source/user/examples/upload-object.py | 6 +- shade/doc/source/user/examples/user-agent.py | 6 +- shade/doc/source/user/logging.rst | 36 +-- shade/doc/source/user/microversions.rst | 2 +- shade/doc/source/user/multi-cloud-demo.rst | 114 ++++---- shade/doc/source/user/usage.rst | 6 +- shade/setup.cfg | 4 - shade/shade/_legacy_clients.py | 252 ------------------ tox.ini | 11 +- 248 files changed, 751 insertions(+), 965 deletions(-) rename {shade/shade => openstack/cloud}/__init__.py (88%) rename {shade/shade => openstack/cloud}/_adapter.py (95%) rename {os-client-config/os_client_config/tests => openstack/cloud/_heat}/__init__.py (100%) rename {shade/shade => openstack/cloud}/_heat/environment_format.py (97%) rename {shade/shade => openstack/cloud}/_heat/event_utils.py (99%) rename {shade/shade => openstack/cloud}/_heat/template_format.py (100%) rename {shade/shade => openstack/cloud}/_heat/template_utils.py (98%) rename {shade/shade => openstack/cloud}/_heat/utils.py (98%) rename {os-client-config/os_client_config => openstack/cloud}/_log.py (100%) rename {shade/shade => openstack/cloud}/_normalize.py (99%) rename {shade/shade => openstack/cloud}/_tasks.py (98%) rename {shade/shade => openstack/cloud}/_utils.py (98%) rename {shade/shade/_heat => openstack/cloud/cmd}/__init__.py (100%) rename {shade/shade => openstack/cloud}/cmd/inventory.py (90%) rename {shade/shade => openstack/cloud}/exc.py (96%) rename {shade/shade => openstack/cloud}/inventory.py (88%) rename {shade/shade => openstack/cloud}/meta.py (99%) rename {shade/shade => openstack/cloud}/openstackcloud.py (99%) rename {shade/shade => openstack/cloud}/operatorcloud.py (99%) rename {shade/shade => openstack/cloud}/task_manager.py (98%) rename {shade/shade/cmd => openstack/cloud/tests}/__init__.py (100%) rename {shade/shade => openstack/cloud}/tests/ansible/README.txt (100%) rename {shade/shade => openstack/cloud}/tests/ansible/hooks/post_test_hook.sh (100%) rename {shade/shade => openstack/cloud}/tests/ansible/roles/auth/tasks/main.yml (100%) rename {shade/shade => openstack/cloud}/tests/ansible/roles/client_config/tasks/main.yml (100%) rename {shade/shade => openstack/cloud}/tests/ansible/roles/group/tasks/main.yml (100%) rename {shade/shade => openstack/cloud}/tests/ansible/roles/group/vars/main.yml (100%) rename {shade/shade => openstack/cloud}/tests/ansible/roles/image/tasks/main.yml (100%) rename {shade/shade => openstack/cloud}/tests/ansible/roles/image/vars/main.yml (100%) rename {shade/shade => openstack/cloud}/tests/ansible/roles/keypair/tasks/main.yml (100%) rename {shade/shade => openstack/cloud}/tests/ansible/roles/keypair/vars/main.yml (100%) rename {shade/shade => openstack/cloud}/tests/ansible/roles/keystone_domain/tasks/main.yml (100%) rename {shade/shade => openstack/cloud}/tests/ansible/roles/keystone_domain/vars/main.yml (100%) rename {shade/shade => openstack/cloud}/tests/ansible/roles/keystone_role/tasks/main.yml (100%) rename {shade/shade => openstack/cloud}/tests/ansible/roles/keystone_role/vars/main.yml (100%) rename {shade/shade => openstack/cloud}/tests/ansible/roles/network/tasks/main.yml (100%) rename {shade/shade => openstack/cloud}/tests/ansible/roles/network/vars/main.yml (100%) rename {shade/shade => openstack/cloud}/tests/ansible/roles/nova_flavor/tasks/main.yml (100%) rename {shade/shade => openstack/cloud}/tests/ansible/roles/object/tasks/main.yml (100%) rename {shade/shade => openstack/cloud}/tests/ansible/roles/port/tasks/main.yml (100%) rename {shade/shade => openstack/cloud}/tests/ansible/roles/port/vars/main.yml (100%) rename {shade/shade => openstack/cloud}/tests/ansible/roles/router/tasks/main.yml (100%) rename {shade/shade => openstack/cloud}/tests/ansible/roles/router/vars/main.yml (100%) rename {shade/shade => openstack/cloud}/tests/ansible/roles/security_group/tasks/main.yml (100%) rename {shade/shade => openstack/cloud}/tests/ansible/roles/security_group/vars/main.yml (100%) rename {shade/shade => openstack/cloud}/tests/ansible/roles/server/tasks/main.yml (100%) rename {shade/shade => openstack/cloud}/tests/ansible/roles/server/vars/main.yaml (100%) rename {shade/shade => openstack/cloud}/tests/ansible/roles/subnet/tasks/main.yml (100%) rename {shade/shade => openstack/cloud}/tests/ansible/roles/subnet/vars/main.yml (100%) rename {shade/shade => openstack/cloud}/tests/ansible/roles/user/tasks/main.yml (100%) rename {shade/shade => openstack/cloud}/tests/ansible/roles/user_group/tasks/main.yml (100%) rename {shade/shade => openstack/cloud}/tests/ansible/roles/volume/tasks/main.yml (100%) rename {shade/shade => openstack/cloud}/tests/ansible/run.yml (100%) rename {shade/shade => openstack/cloud}/tests/base.py (100%) rename {shade/shade => openstack/cloud}/tests/fakes.py (98%) rename {shade/shade/tests => openstack/cloud/tests/functional}/__init__.py (100%) rename {shade/shade => openstack/cloud}/tests/functional/base.py (93%) rename {shade/shade => openstack/cloud}/tests/functional/hooks/post_test_hook.sh (100%) rename {shade/shade => openstack/cloud}/tests/functional/test_aggregate.py (97%) rename {shade/shade => openstack/cloud}/tests/functional/test_cluster_templates.py (95%) rename {shade/shade => openstack/cloud}/tests/functional/test_compute.py (99%) rename {shade/shade => openstack/cloud}/tests/functional/test_devstack.py (97%) rename {shade/shade => openstack/cloud}/tests/functional/test_domain.py (96%) rename {shade/shade => openstack/cloud}/tests/functional/test_endpoints.py (97%) rename {shade/shade => openstack/cloud}/tests/functional/test_flavor.py (98%) rename {shade/shade => openstack/cloud}/tests/functional/test_floating_ip.py (97%) rename {shade/shade => openstack/cloud}/tests/functional/test_floating_ip_pool.py (97%) rename {shade/shade => openstack/cloud}/tests/functional/test_groups.py (95%) rename {shade/shade => openstack/cloud}/tests/functional/test_identity.py (99%) rename {shade/shade => openstack/cloud}/tests/functional/test_image.py (99%) rename {shade/shade => openstack/cloud}/tests/functional/test_inventory.py (95%) rename {shade/shade => openstack/cloud}/tests/functional/test_keypairs.py (96%) rename {shade/shade => openstack/cloud}/tests/functional/test_limits.py (96%) rename {shade/shade => openstack/cloud}/tests/functional/test_magnum_services.py (96%) rename {shade/shade => openstack/cloud}/tests/functional/test_network.py (97%) rename {shade/shade => openstack/cloud}/tests/functional/test_object.py (98%) rename {shade/shade => openstack/cloud}/tests/functional/test_port.py (98%) rename {shade/shade => openstack/cloud}/tests/functional/test_project.py (97%) rename {shade/shade => openstack/cloud}/tests/functional/test_qos_bandwidth_limit_rule.py (97%) rename {shade/shade => openstack/cloud}/tests/functional/test_qos_dscp_marking_rule.py (96%) rename {shade/shade => openstack/cloud}/tests/functional/test_qos_minimum_bandwidth_rule.py (96%) rename {shade/shade => openstack/cloud}/tests/functional/test_qos_policy.py (97%) rename {shade/shade => openstack/cloud}/tests/functional/test_quotas.py (98%) rename {shade/shade => openstack/cloud}/tests/functional/test_range_search.py (98%) rename {shade/shade => openstack/cloud}/tests/functional/test_recordset.py (98%) rename {shade/shade => openstack/cloud}/tests/functional/test_router.py (99%) rename {shade/shade => openstack/cloud}/tests/functional/test_security_groups.py (98%) rename {shade/shade => openstack/cloud}/tests/functional/test_server_group.py (96%) rename {shade/shade => openstack/cloud}/tests/functional/test_services.py (96%) rename {shade/shade => openstack/cloud}/tests/functional/test_stack.py (97%) rename {shade/shade => openstack/cloud}/tests/functional/test_usage.py (96%) rename {shade/shade => openstack/cloud}/tests/functional/test_users.py (97%) rename {shade/shade => openstack/cloud}/tests/functional/test_volume.py (98%) rename {shade/shade => openstack/cloud}/tests/functional/test_volume_backup.py (98%) rename {shade/shade => openstack/cloud}/tests/functional/test_volume_type.py (98%) rename {shade/shade => openstack/cloud}/tests/functional/test_zone.py (98%) rename {shade/shade => openstack/cloud}/tests/functional/util.py (100%) rename {shade/shade/tests/functional => openstack/cloud/tests/unit}/__init__.py (100%) rename {shade/shade => openstack/cloud}/tests/unit/base.py (98%) rename {shade/shade => openstack/cloud}/tests/unit/fixtures/baremetal.json (100%) rename {shade/shade => openstack/cloud}/tests/unit/fixtures/catalog-v2.json (100%) rename {shade/shade => openstack/cloud}/tests/unit/fixtures/catalog-v3.json (100%) rename {shade/shade => openstack/cloud}/tests/unit/fixtures/clouds/clouds.yaml (100%) rename {shade/shade => openstack/cloud}/tests/unit/fixtures/clouds/clouds_cache.yaml (100%) rename {shade/shade => openstack/cloud}/tests/unit/fixtures/discovery.json (100%) rename {shade/shade => openstack/cloud}/tests/unit/fixtures/dns.json (100%) rename {shade/shade => openstack/cloud}/tests/unit/fixtures/image-version-broken.json (100%) rename {shade/shade => openstack/cloud}/tests/unit/fixtures/image-version-v1.json (100%) rename {shade/shade => openstack/cloud}/tests/unit/fixtures/image-version-v2.json (100%) rename {shade/shade => openstack/cloud}/tests/unit/fixtures/image-version.json (100%) rename {shade/shade => openstack/cloud}/tests/unit/test__adapter.py (94%) rename {shade/shade => openstack/cloud}/tests/unit/test__utils.py (99%) rename {shade/shade => openstack/cloud}/tests/unit/test_aggregate.py (98%) rename {shade/shade => openstack/cloud}/tests/unit/test_availability_zones.py (96%) rename {shade/shade => openstack/cloud}/tests/unit/test_baremetal_node.py (99%) rename {shade/shade => openstack/cloud}/tests/unit/test_caching.py (97%) rename {shade/shade => openstack/cloud}/tests/unit/test_cluster_templates.py (97%) rename {shade/shade => openstack/cloud}/tests/unit/test_create_server.py (98%) rename {shade/shade => openstack/cloud}/tests/unit/test_create_volume_snapshot.py (97%) rename {shade/shade => openstack/cloud}/tests/unit/test_delete_server.py (98%) rename {shade/shade => openstack/cloud}/tests/unit/test_delete_volume_snapshot.py (96%) rename {shade/shade => openstack/cloud}/tests/unit/test_domain_params.py (78%) rename {shade/shade => openstack/cloud}/tests/unit/test_domains.py (97%) rename {shade/shade => openstack/cloud}/tests/unit/test_endpoints.py (98%) rename {shade/shade => openstack/cloud}/tests/unit/test_flavors.py (97%) rename {shade/shade => openstack/cloud}/tests/unit/test_floating_ip_common.py (98%) rename {shade/shade => openstack/cloud}/tests/unit/test_floating_ip_neutron.py (99%) rename {shade/shade => openstack/cloud}/tests/unit/test_floating_ip_nova.py (99%) rename {shade/shade => openstack/cloud}/tests/unit/test_floating_ip_pool.py (95%) rename {shade/shade => openstack/cloud}/tests/unit/test_groups.py (98%) rename {shade/shade => openstack/cloud}/tests/unit/test_identity_roles.py (97%) rename {shade/shade => openstack/cloud}/tests/unit/test_image.py (87%) rename {shade/shade => openstack/cloud}/tests/unit/test_image_snapshot.py (97%) rename {shade/shade => openstack/cloud}/tests/unit/test_inventory.py (90%) rename {shade/shade => openstack/cloud}/tests/unit/test_keypair.py (96%) rename {shade/shade => openstack/cloud}/tests/unit/test_limits.py (98%) rename {shade/shade => openstack/cloud}/tests/unit/test_magnum_services.py (96%) rename {shade/shade => openstack/cloud}/tests/unit/test_meta.py (95%) rename {shade/shade => openstack/cloud}/tests/unit/test_network.py (98%) rename {shade/shade => openstack/cloud}/tests/unit/test_normalize.py (99%) rename {shade/shade => openstack/cloud}/tests/unit/test_object.py (97%) rename {shade/shade => openstack/cloud}/tests/unit/test_operator_noauth.py (93%) rename {shade/shade => openstack/cloud}/tests/unit/test_port.py (99%) rename {shade/shade => openstack/cloud}/tests/unit/test_project.py (98%) rename {shade/shade => openstack/cloud}/tests/unit/test_qos_bandwidth_limit_rule.py (99%) rename {shade/shade => openstack/cloud}/tests/unit/test_qos_dscp_marking_rule.py (99%) rename {shade/shade => openstack/cloud}/tests/unit/test_qos_minimum_bandwidth_rule.py (99%) rename {shade/shade => openstack/cloud}/tests/unit/test_qos_policy.py (99%) rename {shade/shade => openstack/cloud}/tests/unit/test_qos_rule_type.py (98%) rename {shade/shade => openstack/cloud}/tests/unit/test_quotas.py (99%) rename {shade/shade => openstack/cloud}/tests/unit/test_rebuild_server.py (98%) rename {shade/shade => openstack/cloud}/tests/unit/test_recordset.py (98%) rename {shade/shade => openstack/cloud}/tests/unit/test_role_assignment.py (99%) rename {shade/shade => openstack/cloud}/tests/unit/test_router.py (99%) rename {shade/shade => openstack/cloud}/tests/unit/test_security_groups.py (98%) rename {shade/shade => openstack/cloud}/tests/unit/test_server_console.py (97%) rename {shade/shade => openstack/cloud}/tests/unit/test_server_delete_metadata.py (94%) rename {shade/shade => openstack/cloud}/tests/unit/test_server_group.py (96%) rename {shade/shade => openstack/cloud}/tests/unit/test_server_set_metadata.py (94%) rename {shade/shade => openstack/cloud}/tests/unit/test_services.py (98%) rename {shade/shade => openstack/cloud}/tests/unit/test_shade.py (97%) rename {shade/shade => openstack/cloud}/tests/unit/test_shade_operator.py (90%) rename {shade/shade => openstack/cloud}/tests/unit/test_stack.py (97%) rename {shade/shade => openstack/cloud}/tests/unit/test_subnet.py (99%) rename {shade/shade => openstack/cloud}/tests/unit/test_task_manager.py (97%) rename {shade/shade => openstack/cloud}/tests/unit/test_update_server.py (95%) rename {shade/shade => openstack/cloud}/tests/unit/test_usage.py (98%) rename {shade/shade => openstack/cloud}/tests/unit/test_users.py (98%) rename {shade/shade => openstack/cloud}/tests/unit/test_volume.py (97%) rename {shade/shade => openstack/cloud}/tests/unit/test_volume_access.py (97%) rename {shade/shade => openstack/cloud}/tests/unit/test_volume_backups.py (98%) rename {shade/shade => openstack/cloud}/tests/unit/test_zone.py (97%) rename {os-client-config/os_client_config => openstack/config}/__init__.py (96%) rename {shade/shade => openstack/config}/_log.py (100%) rename {os-client-config/os_client_config => openstack/config}/cloud_config.py (99%) rename {os-client-config/os_client_config => openstack/config}/config.py (100%) rename {os-client-config/os_client_config => openstack/config}/constructors.json (100%) rename {os-client-config/os_client_config => openstack/config}/constructors.py (100%) rename {os-client-config/os_client_config => openstack/config}/defaults.json (100%) rename {os-client-config/os_client_config => openstack/config}/defaults.py (100%) rename {os-client-config/os_client_config => openstack/config}/exceptions.py (100%) rename {os-client-config/os_client_config => openstack/config}/schema.json (100%) rename {shade/shade/tests/unit => openstack/config/tests}/__init__.py (100%) rename {os-client-config/os_client_config => openstack/config}/tests/base.py (99%) rename {os-client-config/os_client_config => openstack/config}/tests/test_cloud_config.py (100%) rename {os-client-config/os_client_config => openstack/config}/tests/test_config.py (100%) rename {os-client-config/os_client_config => openstack/config}/tests/test_environ.py (100%) rename {os-client-config/os_client_config => openstack/config}/tests/test_init.py (96%) rename {os-client-config/os_client_config => openstack/config}/tests/test_json.py (100%) rename {os-client-config/os_client_config => openstack/config}/vendor-schema.json (100%) rename {os-client-config/os_client_config => openstack/config}/vendors/__init__.py (100%) rename {os-client-config/os_client_config => openstack/config}/vendors/auro.json (100%) rename {os-client-config/os_client_config => openstack/config}/vendors/bluebox.json (100%) rename {os-client-config/os_client_config => openstack/config}/vendors/catalyst.json (100%) rename {os-client-config/os_client_config => openstack/config}/vendors/citycloud.json (100%) rename {os-client-config/os_client_config => openstack/config}/vendors/conoha.json (100%) rename {os-client-config/os_client_config => openstack/config}/vendors/datacentred.json (100%) rename {os-client-config/os_client_config => openstack/config}/vendors/dreamcompute.json (100%) rename {os-client-config/os_client_config => openstack/config}/vendors/dreamhost.json (100%) rename {os-client-config/os_client_config => openstack/config}/vendors/elastx.json (100%) rename {os-client-config/os_client_config => openstack/config}/vendors/entercloudsuite.json (100%) rename {os-client-config/os_client_config => openstack/config}/vendors/fuga.json (100%) rename {os-client-config/os_client_config => openstack/config}/vendors/ibmcloud.json (100%) rename {os-client-config/os_client_config => openstack/config}/vendors/internap.json (100%) rename {os-client-config/os_client_config => openstack/config}/vendors/otc.json (100%) rename {os-client-config/os_client_config => openstack/config}/vendors/ovh.json (100%) rename {os-client-config/os_client_config => openstack/config}/vendors/rackspace.json (100%) rename {os-client-config/os_client_config => openstack/config}/vendors/switchengines.json (100%) rename {os-client-config/os_client_config => openstack/config}/vendors/ultimum.json (100%) rename {os-client-config/os_client_config => openstack/config}/vendors/unitedstack.json (100%) rename {os-client-config/os_client_config => openstack/config}/vendors/vexxhost.json (100%) rename {os-client-config/os_client_config => openstack/config}/vendors/zetta.json (100%) delete mode 100644 shade/shade/_legacy_clients.py diff --git a/examples/connect.py b/examples/connect.py index 07216ae16..7db375ca5 100644 --- a/examples/connect.py +++ b/examples/connect.py @@ -19,8 +19,7 @@ import argparse import os -import os_client_config - +from openstack import config as occ from openstack import connection from openstack import profile from openstack import utils @@ -49,8 +48,8 @@ def _get_resource_value(resource_key, default): except KeyError: return default -occ = os_client_config.OpenStackConfig() -cloud = occ.get_one_cloud(TEST_CLOUD) +config = occ.OpenStackConfig() +cloud = config.get_one_cloud(TEST_CLOUD) SERVER_NAME = 'openstacksdk-example' IMAGE_NAME = _get_resource_value('image_name', 'cirros-0.3.5-x86_64-disk') @@ -68,14 +67,14 @@ def _get_resource_value(resource_key, default): def create_connection_from_config(): opts = Opts(cloud_name=TEST_CLOUD) - occ = os_client_config.OpenStackConfig() - cloud = occ.get_one_cloud(opts.cloud) + config = occ.OpenStackConfig() + cloud = config.get_one_cloud(opts.cloud) return connection.from_config(cloud_config=cloud, options=opts) def create_connection_from_args(): parser = argparse.ArgumentParser() - config = os_client_config.OpenStackConfig() + config = occ.OpenStackConfig() config.register_argparse_arguments(parser, sys.argv[1:]) args = parser.parse_args() return connection.from_config(options=args) diff --git a/shade/shade/__init__.py b/openstack/cloud/__init__.py similarity index 88% rename from shade/shade/__init__.py rename to openstack/cloud/__init__.py index 35e7ee98e..d727aa4c9 100644 --- a/shade/shade/__init__.py +++ b/openstack/cloud/__init__.py @@ -16,16 +16,16 @@ import warnings import keystoneauth1.exceptions -import os_client_config import pbr.version import requestsexceptions -from shade.exc import * # noqa -from shade.openstackcloud import OpenStackCloud -from shade.operatorcloud import OperatorCloud -from shade import _log +import openstack.config as os_client_config +from openstack.cloud.exc import * # noqa +from openstack.cloud.openstackcloud import OpenStackCloud +from openstack.cloud.operatorcloud import OperatorCloud +from openstack.cloud import _log -__version__ = pbr.version.VersionInfo('shade').version_string() +__version__ = pbr.version.VersionInfo('openstack').version_string() if requestsexceptions.SubjectAltNameWarning: warnings.filterwarnings( @@ -54,16 +54,13 @@ def simple_logging(debug=False, http_debug=False): log.addHandler(logging.StreamHandler()) log.setLevel(log_level) # We only want extra shade HTTP tracing in http debug mode - log = _log.setup_logging('shade.http') + log = _log.setup_logging('openstack.cloud.http') log.setLevel(log_level) else: # We only want extra shade HTTP tracing in http debug mode - log = _log.setup_logging('shade.http') + log = _log.setup_logging('openstack.cloud.http') log.setLevel(logging.WARNING) - # Simple case - we only care about request id log during debug - log = _log.setup_logging('shade.request_ids') - log.setLevel(log_level) - log = _log.setup_logging('shade') + log = _log.setup_logging('openstack.cloud') log.addHandler(logging.StreamHandler()) log.setLevel(log_level) # Suppress warning about keystoneauth loggers diff --git a/shade/shade/_adapter.py b/openstack/cloud/_adapter.py similarity index 95% rename from shade/shade/_adapter.py rename to openstack/cloud/_adapter.py index bd88d6fd6..354389c92 100644 --- a/shade/shade/_adapter.py +++ b/openstack/cloud/_adapter.py @@ -18,9 +18,9 @@ from keystoneauth1 import adapter from six.moves import urllib -from shade import _log -from shade import exc -from shade import task_manager +from openstack.cloud import _log +from openstack.cloud import exc +from openstack.cloud import task_manager def extract_name(url): @@ -81,13 +81,15 @@ def extract_name(url): return [part for part in name_parts if part] +# TODO(shade) This adapter should go away in favor of the work merging +# adapter with openstack.proxy. class ShadeAdapter(adapter.Adapter): def __init__(self, shade_logger, manager, *args, **kwargs): super(ShadeAdapter, self).__init__(*args, **kwargs) self.shade_logger = shade_logger self.manager = manager - self.request_log = _log.setup_logging('shade.request_ids') + self.request_log = _log.setup_logging('openstack.cloud.request_ids') def _log_request_id(self, response, obj=None): # Log the request id and object id in a specific logger. This way diff --git a/os-client-config/os_client_config/tests/__init__.py b/openstack/cloud/_heat/__init__.py similarity index 100% rename from os-client-config/os_client_config/tests/__init__.py rename to openstack/cloud/_heat/__init__.py diff --git a/shade/shade/_heat/environment_format.py b/openstack/cloud/_heat/environment_format.py similarity index 97% rename from shade/shade/_heat/environment_format.py rename to openstack/cloud/_heat/environment_format.py index 56bc2c1c0..ac60715ae 100644 --- a/shade/shade/_heat/environment_format.py +++ b/openstack/cloud/_heat/environment_format.py @@ -12,7 +12,7 @@ import yaml -from shade._heat import template_format +from openstack.cloud._heat import template_format SECTIONS = ( diff --git a/shade/shade/_heat/event_utils.py b/openstack/cloud/_heat/event_utils.py similarity index 99% rename from shade/shade/_heat/event_utils.py rename to openstack/cloud/_heat/event_utils.py index 69c286220..bceec38af 100644 --- a/shade/shade/_heat/event_utils.py +++ b/openstack/cloud/_heat/event_utils.py @@ -15,7 +15,7 @@ import collections import time -from shade import meta +from openstack.cloud import meta def get_events(cloud, stack_id, event_args, marker=None, limit=None): diff --git a/shade/shade/_heat/template_format.py b/openstack/cloud/_heat/template_format.py similarity index 100% rename from shade/shade/_heat/template_format.py rename to openstack/cloud/_heat/template_format.py diff --git a/shade/shade/_heat/template_utils.py b/openstack/cloud/_heat/template_utils.py similarity index 98% rename from shade/shade/_heat/template_utils.py rename to openstack/cloud/_heat/template_utils.py index 3653daaf6..c56b76ea5 100644 --- a/shade/shade/_heat/template_utils.py +++ b/openstack/cloud/_heat/template_utils.py @@ -18,10 +18,10 @@ from six.moves.urllib import parse from six.moves.urllib import request -from shade._heat import environment_format -from shade._heat import template_format -from shade._heat import utils -from shade import exc +from openstack.cloud._heat import environment_format +from openstack.cloud._heat import template_format +from openstack.cloud._heat import utils +from openstack.cloud import exc def get_template_contents(template_file=None, template_url=None, diff --git a/shade/shade/_heat/utils.py b/openstack/cloud/_heat/utils.py similarity index 98% rename from shade/shade/_heat/utils.py rename to openstack/cloud/_heat/utils.py index 24cb0b071..c916c8b63 100644 --- a/shade/shade/_heat/utils.py +++ b/openstack/cloud/_heat/utils.py @@ -20,7 +20,7 @@ from six.moves.urllib import parse from six.moves.urllib import request -from shade import exc +from openstack.cloud import exc def base_url_for_url(url): diff --git a/os-client-config/os_client_config/_log.py b/openstack/cloud/_log.py similarity index 100% rename from os-client-config/os_client_config/_log.py rename to openstack/cloud/_log.py diff --git a/shade/shade/_normalize.py b/openstack/cloud/_normalize.py similarity index 99% rename from shade/shade/_normalize.py rename to openstack/cloud/_normalize.py index b8e242b56..cf80627bc 100644 --- a/shade/shade/_normalize.py +++ b/openstack/cloud/_normalize.py @@ -12,6 +12,10 @@ # 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. + +# TODO(shade) The normalize functions here should get merged in to +# the sdk resource objects. + import datetime import munch import six diff --git a/shade/shade/_tasks.py b/openstack/cloud/_tasks.py similarity index 98% rename from shade/shade/_tasks.py rename to openstack/cloud/_tasks.py index a60d27139..294fae3ba 100644 --- a/shade/shade/_tasks.py +++ b/openstack/cloud/_tasks.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from shade import task_manager +from openstack.cloud import task_manager class MachineCreate(task_manager.Task): diff --git a/shade/shade/_utils.py b/openstack/cloud/_utils.py similarity index 98% rename from shade/shade/_utils.py rename to openstack/cloud/_utils.py index 37939e5b2..f38034213 100644 --- a/shade/shade/_utils.py +++ b/openstack/cloud/_utils.py @@ -27,9 +27,9 @@ from decorator import decorator -from shade import _log -from shade import exc -from shade import meta +from openstack.cloud import _log +from openstack.cloud import exc +from openstack.cloud import meta _decorated_methods = [] @@ -48,7 +48,7 @@ def _iterate_timeout(timeout, message, wait=2): with . """ - log = _log.setup_logging('shade.iterate_timeout') + log = _log.setup_logging('openstack.cloud.iterate_timeout') try: # None as a wait winds up flowing well in the per-resource cache @@ -128,10 +128,10 @@ def _filter_list(data, name_or_id, filters): OR A string containing a jmespath expression for further filtering. """ - # The logger is shade.fmmatch to allow a user/operator to configure logging - # not to communicate about fnmatch misses (they shouldn't be too spammy, - # but one never knows) - log = _log.setup_logging('shade.fnmatch') + # The logger is openstack.cloud.fmmatch to allow a user/operator to + # configure logging not to communicate about fnmatch misses + # (they shouldn't be too spammy, but one never knows) + log = _log.setup_logging('openstack.cloud.fnmatch') if name_or_id: # name_or_id might already be unicode name_or_id = _make_unicode(name_or_id) diff --git a/shade/shade/_heat/__init__.py b/openstack/cloud/cmd/__init__.py similarity index 100% rename from shade/shade/_heat/__init__.py rename to openstack/cloud/cmd/__init__.py diff --git a/shade/shade/cmd/inventory.py b/openstack/cloud/cmd/inventory.py similarity index 90% rename from shade/shade/cmd/inventory.py rename to openstack/cloud/cmd/inventory.py index 26d615299..c5bc8cf26 100755 --- a/shade/shade/cmd/inventory.py +++ b/openstack/cloud/cmd/inventory.py @@ -18,8 +18,8 @@ import sys import yaml -import shade -import shade.inventory +import openstack.cloud +import openstack.cloud.inventory def output_format_dict(data, use_yaml): @@ -51,8 +51,8 @@ def parse_args(): def main(): args = parse_args() try: - shade.simple_logging(debug=args.debug) - inventory = shade.inventory.OpenStackInventory( + openstack.cloud.simple_logging(debug=args.debug) + inventory = openstack.cloud.inventory.OpenStackInventory( refresh=args.refresh, private=args.private, cloud=args.cloud) if args.list: @@ -60,7 +60,7 @@ def main(): elif args.host: output = inventory.get_host(args.host) print(output_format_dict(output, args.yaml)) - except shade.OpenStackCloudException as e: + except openstack.cloud.OpenStackCloudException as e: sys.stderr.write(e.message + '\n') sys.exit(1) sys.exit(0) diff --git a/shade/shade/exc.py b/openstack/cloud/exc.py similarity index 96% rename from shade/shade/exc.py rename to openstack/cloud/exc.py index cda469f9c..9262a500f 100644 --- a/shade/shade/exc.py +++ b/openstack/cloud/exc.py @@ -17,7 +17,7 @@ import munch from requests import exceptions as _rex -from shade import _log +from openstack.cloud import _log class OpenStackCloudException(Exception): @@ -37,7 +37,7 @@ def __init__(self, message, extra_data=None, **kwargs): def log_error(self, logger=None): if not logger: - logger = _log.setup_logging('shade.exc') + logger = _log.setup_logging('openstack.cloud.exc') if self.inner_exception and self.inner_exception[1]: logger.error(self.orig_message, exc_info=self.inner_exception) @@ -102,7 +102,7 @@ def _log_response_extras(response): # Sometimes we get weird HTML errors. This is usually from load balancers # or other things. Log them to a special logger so that they can be # toggled indepdently - and at debug level so that a person logging - # shade.* only gets them at debug. + # openstack.cloud.* only gets them at debug. if response.headers.get('content-type') != 'text/html': return try: @@ -110,7 +110,7 @@ def _log_response_extras(response): return except Exception: return - logger = _log.setup_logging('shade.http') + logger = _log.setup_logging('openstack.cloud.http') if response.reason: logger.debug( "Non-standard error '{reason}' returned from {url}:".format( diff --git a/shade/shade/inventory.py b/openstack/cloud/inventory.py similarity index 88% rename from shade/shade/inventory.py rename to openstack/cloud/inventory.py index 2490e93bf..d8b6a9f93 100644 --- a/shade/shade/inventory.py +++ b/openstack/cloud/inventory.py @@ -14,10 +14,10 @@ import functools -import os_client_config +import openstack.config as os_client_config -import shade -from shade import _utils +import openstack.cloud +from openstack.cloud import _utils class OpenStackInventory(object): @@ -38,17 +38,17 @@ def __init__( if cloud is None: self.clouds = [ - shade.OpenStackCloud(cloud_config=cloud_config) + openstack.cloud.OpenStackCloud(cloud_config=cloud_config) for cloud_config in config.get_all_clouds() ] else: try: self.clouds = [ - shade.OpenStackCloud( + openstack.cloud.OpenStackCloud( cloud_config=config.get_one_cloud(cloud)) ] except os_client_config.exceptions.OpenStackConfigException as e: - raise shade.OpenStackCloudException(e) + raise openstack.cloud.OpenStackCloudException(e) if private: for cloud in self.clouds: @@ -67,7 +67,7 @@ def list_hosts(self, expand=True, fail_on_cloud_config=True): # Cycle on servers for server in cloud.list_servers(detailed=expand): hostvars.append(server) - except shade.OpenStackCloudException: + except openstack.cloud.OpenStackCloudException: # Don't fail on one particular cloud as others may work if fail_on_cloud_config: raise diff --git a/shade/shade/meta.py b/openstack/cloud/meta.py similarity index 99% rename from shade/shade/meta.py rename to openstack/cloud/meta.py index 85c0f4310..089fb3229 100644 --- a/shade/shade/meta.py +++ b/openstack/cloud/meta.py @@ -18,8 +18,8 @@ import six import socket -from shade import _log -from shade import exc +from openstack.cloud import _log +from openstack.cloud import exc NON_CALLABLES = (six.string_types, bool, dict, int, float, list, type(None)) @@ -490,7 +490,7 @@ def _log_request_id(obj, request_id): if request_id: # Log the request id and object id in a specific logger. This way # someone can turn it on if they're interested in this kind of tracing. - log = _log.setup_logging('shade.request_ids') + log = _log.setup_logging('openstack.cloud.request_ids') obj_id = None if isinstance(obj, dict): obj_id = obj.get('id', obj.get('uuid')) diff --git a/shade/shade/openstackcloud.py b/openstack/cloud/openstackcloud.py similarity index 99% rename from shade/shade/openstackcloud.py rename to openstack/cloud/openstackcloud.py index 781a06efd..7244b791c 100644 --- a/shade/shade/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -20,8 +20,8 @@ import keystoneauth1.session import operator import os -import os_client_config -import os_client_config.defaults +import openstack.config +import openstack.config.defaults import six import threading import time @@ -34,23 +34,28 @@ import keystoneauth1.exceptions -import shade -from shade.exc import * # noqa -from shade import _adapter -from shade._heat import event_utils -from shade._heat import template_utils -from shade import _log -from shade import _legacy_clients -from shade import _normalize -from shade import meta -from shade import task_manager -from shade import _utils - -OBJECT_MD5_KEY = 'x-object-meta-x-shade-md5' -OBJECT_SHA256_KEY = 'x-object-meta-x-shade-sha256' -IMAGE_MD5_KEY = 'owner_specified.shade.md5' -IMAGE_SHA256_KEY = 'owner_specified.shade.sha256' -IMAGE_OBJECT_KEY = 'owner_specified.shade.object' +import openstack.cloud +from openstack.cloud.exc import * # noqa +from openstack.cloud import _adapter +from openstack.cloud._heat import event_utils +from openstack.cloud._heat import template_utils +from openstack.cloud import _log +from openstack.cloud import _normalize +from openstack.cloud import meta +from openstack.cloud import task_manager +from openstack.cloud import _utils + +# TODO(shade) shade keys were x-object-meta-x-shade-md5 - we need to add those +# to freshness checks so that a shade->sdk transition doens't +# result in a re-upload +OBJECT_MD5_KEY = 'x-object-meta-x-openstack-md5' +OBJECT_SHA256_KEY = 'x-object-meta-x-openstack-sha256' +# TODO(shade) shade keys were owner_specified.shade.md5 - we need to add those +# to freshness checks so that a shade->sdk transition doens't +# result in a re-upload +IMAGE_MD5_KEY = 'owner_specified.openstack.md5' +IMAGE_SHA256_KEY = 'owner_specified.openstack.sha256' +IMAGE_OBJECT_KEY = 'owner_specified.openstack.object' # Rackspace returns this for intermittent import errors IMAGE_ERROR_396 = "Image cannot be imported. Error code: '396'" DEFAULT_OBJECT_SEGMENT_SIZE = 1073741824 # 1GB @@ -93,9 +98,7 @@ def _no_pending_stacks(stacks): return True -class OpenStackCloud( - _normalize.Normalizer, - _legacy_clients.LegacyClientFactoryMixin): +class OpenStackCloud(_normalize.Normalizer): """Represent a connection to an OpenStack Cloud. OpenStackCloud is the entry point for all cloud operations, regardless @@ -119,7 +122,7 @@ class OpenStackCloud( emitted to the error log. This flag will enable that behavior. :param bool strict: Only return documented attributes for each resource - as per the shade Data Model contract. (Default False) + as per the Data Model contract. (Default False) :param app_name: Name of the application to be appended to the user-agent string. Optional, defaults to None. :param app_version: Version of the application to be appended to the @@ -143,10 +146,10 @@ def __init__( if log_inner_exceptions: OpenStackCloudException.log_inner_exceptions = True - self.log = _log.setup_logging('shade') + self.log = _log.setup_logging('openstack.cloud') if not cloud_config: - config = os_client_config.OpenStackConfig( + config = openstack.config.OpenStackConfig( app_name=app_name, app_version=app_version) cloud_config = config.get_one_cloud(**kwargs) @@ -161,8 +164,8 @@ def __init__( self.secgroup_source = cloud_config.config['secgroup_source'] self.force_ipv4 = cloud_config.force_ipv4 self.strict_mode = strict - # TODO(mordred) When os-client-config adds a "get_client_settings()" - # method to CloudConfig - remove this. + # TODO(shade) The openstack.cloud default for get_flavor_extra_specs + # should be changed and this should be removed completely self._extra_config = cloud_config._openstack_config.get_extra_config( 'shade', { 'get_flavor_extra_specs': True, @@ -296,7 +299,6 @@ def invalidate(self): self._keystone_session = None - self._legacy_clients = {} self._raw_clients = {} self._local_ipv6 = ( @@ -368,11 +370,17 @@ def _get_versioned_client( config_major = self._get_major_version_id(config_version) max_major = self._get_major_version_id(max_version) min_major = self._get_major_version_id(min_version) - # NOTE(mordred) The shade logic for versions is slightly different - # than the ksa Adapter constructor logic. shade knows the versions - # it knows, and uses them when it detects them. However, if a user - # requests a version, and it's not found, and a different one shade - # does know about it found, that's a warning in shade. + # TODO(shade) This should be replaced with use of Connection. However, + # we need to find a sane way to deal with this additional + # logic - or we need to give up on it. If we give up on it, + # we need to make sure we can still support it in the shade + # compat layer. + # NOTE(mordred) This logic for versions is slightly different + # than the ksa Adapter constructor logic. openstack.cloud knows the + # versions it knows, and uses them when it detects them. However, if + # a user requests a version, and it's not found, and a different one + # openstack.cloud does know about is found, that's a warning in + # openstack.cloud. if config_version: if min_major and config_major < min_major: raise OpenStackCloudException( @@ -384,7 +392,8 @@ def _get_versioned_client( elif max_major and config_major > max_major: raise OpenStackCloudException( "Version {config_version} requested for {service_type}" - " but shade understands a maximum of {max_version}".format( + " but openstack.cloud understands a maximum of" + " {max_version}".format( config_version=config_version, service_type=service_type, max_version=max_version)) @@ -441,6 +450,8 @@ def _get_versioned_client( warnings.warn(warning_msg) return adapter + # TODO(shade) This should be replaced with using openstack Connection + # object. def _get_raw_client( self, service_type, api_version=None, endpoint_override=None): return _adapter.ShadeAdapter( @@ -588,7 +599,7 @@ def keystone_session(self): self._keystone_session = self.cloud_config.get_session() if hasattr(self._keystone_session, 'additional_user_agent'): self._keystone_session.additional_user_agent.append( - ('shade', shade.__version__)) + ('shade', openstack.cloud.__version__)) except Exception as e: raise OpenStackCloudException( "Error authenticating to keystone: %s " % str(e)) @@ -1825,7 +1836,7 @@ def list_flavors(self, get_extra=None): :param get_extra: Whether or not to fetch extra specs for each flavor. Defaults to True. Default behavior value can be overridden in clouds.yaml by setting - shade.get_extra_specs to False. + openstack.cloud.get_extra_specs to False. :returns: A list of flavor ``munch.Munch``. """ diff --git a/shade/shade/operatorcloud.py b/openstack/cloud/operatorcloud.py similarity index 99% rename from shade/shade/operatorcloud.py rename to openstack/cloud/operatorcloud.py index da6d07411..d2f2a9464 100644 --- a/shade/shade/operatorcloud.py +++ b/openstack/cloud/operatorcloud.py @@ -17,10 +17,10 @@ from ironicclient import exceptions as ironic_exceptions -from shade.exc import * # noqa -from shade import openstackcloud -from shade import _tasks -from shade import _utils +from openstack.cloud.exc import * # noqa +from openstack.cloud import openstackcloud +from openstack.cloud import _tasks +from openstack.cloud import _utils class OperatorCloud(openstackcloud.OpenStackCloud): diff --git a/shade/shade/task_manager.py b/openstack/cloud/task_manager.py similarity index 98% rename from shade/shade/task_manager.py rename to openstack/cloud/task_manager.py index 885df8be5..39db58575 100644 --- a/shade/shade/task_manager.py +++ b/openstack/cloud/task_manager.py @@ -25,9 +25,9 @@ import simplejson import six -from shade import _log -from shade import exc -from shade import meta +from openstack.cloud import _log +from openstack.cloud import exc +from openstack.cloud import meta def _is_listlike(obj): @@ -226,7 +226,7 @@ def main(self, client): class TaskManager(object): - log = _log.setup_logging('shade.task_manager') + log = _log.setup_logging('openstack.cloud.task_manager') def __init__( self, client, name, result_filter_cb=None, workers=5, **kwargs): diff --git a/shade/shade/cmd/__init__.py b/openstack/cloud/tests/__init__.py similarity index 100% rename from shade/shade/cmd/__init__.py rename to openstack/cloud/tests/__init__.py diff --git a/shade/shade/tests/ansible/README.txt b/openstack/cloud/tests/ansible/README.txt similarity index 100% rename from shade/shade/tests/ansible/README.txt rename to openstack/cloud/tests/ansible/README.txt diff --git a/shade/shade/tests/ansible/hooks/post_test_hook.sh b/openstack/cloud/tests/ansible/hooks/post_test_hook.sh similarity index 100% rename from shade/shade/tests/ansible/hooks/post_test_hook.sh rename to openstack/cloud/tests/ansible/hooks/post_test_hook.sh diff --git a/shade/shade/tests/ansible/roles/auth/tasks/main.yml b/openstack/cloud/tests/ansible/roles/auth/tasks/main.yml similarity index 100% rename from shade/shade/tests/ansible/roles/auth/tasks/main.yml rename to openstack/cloud/tests/ansible/roles/auth/tasks/main.yml diff --git a/shade/shade/tests/ansible/roles/client_config/tasks/main.yml b/openstack/cloud/tests/ansible/roles/client_config/tasks/main.yml similarity index 100% rename from shade/shade/tests/ansible/roles/client_config/tasks/main.yml rename to openstack/cloud/tests/ansible/roles/client_config/tasks/main.yml diff --git a/shade/shade/tests/ansible/roles/group/tasks/main.yml b/openstack/cloud/tests/ansible/roles/group/tasks/main.yml similarity index 100% rename from shade/shade/tests/ansible/roles/group/tasks/main.yml rename to openstack/cloud/tests/ansible/roles/group/tasks/main.yml diff --git a/shade/shade/tests/ansible/roles/group/vars/main.yml b/openstack/cloud/tests/ansible/roles/group/vars/main.yml similarity index 100% rename from shade/shade/tests/ansible/roles/group/vars/main.yml rename to openstack/cloud/tests/ansible/roles/group/vars/main.yml diff --git a/shade/shade/tests/ansible/roles/image/tasks/main.yml b/openstack/cloud/tests/ansible/roles/image/tasks/main.yml similarity index 100% rename from shade/shade/tests/ansible/roles/image/tasks/main.yml rename to openstack/cloud/tests/ansible/roles/image/tasks/main.yml diff --git a/shade/shade/tests/ansible/roles/image/vars/main.yml b/openstack/cloud/tests/ansible/roles/image/vars/main.yml similarity index 100% rename from shade/shade/tests/ansible/roles/image/vars/main.yml rename to openstack/cloud/tests/ansible/roles/image/vars/main.yml diff --git a/shade/shade/tests/ansible/roles/keypair/tasks/main.yml b/openstack/cloud/tests/ansible/roles/keypair/tasks/main.yml similarity index 100% rename from shade/shade/tests/ansible/roles/keypair/tasks/main.yml rename to openstack/cloud/tests/ansible/roles/keypair/tasks/main.yml diff --git a/shade/shade/tests/ansible/roles/keypair/vars/main.yml b/openstack/cloud/tests/ansible/roles/keypair/vars/main.yml similarity index 100% rename from shade/shade/tests/ansible/roles/keypair/vars/main.yml rename to openstack/cloud/tests/ansible/roles/keypair/vars/main.yml diff --git a/shade/shade/tests/ansible/roles/keystone_domain/tasks/main.yml b/openstack/cloud/tests/ansible/roles/keystone_domain/tasks/main.yml similarity index 100% rename from shade/shade/tests/ansible/roles/keystone_domain/tasks/main.yml rename to openstack/cloud/tests/ansible/roles/keystone_domain/tasks/main.yml diff --git a/shade/shade/tests/ansible/roles/keystone_domain/vars/main.yml b/openstack/cloud/tests/ansible/roles/keystone_domain/vars/main.yml similarity index 100% rename from shade/shade/tests/ansible/roles/keystone_domain/vars/main.yml rename to openstack/cloud/tests/ansible/roles/keystone_domain/vars/main.yml diff --git a/shade/shade/tests/ansible/roles/keystone_role/tasks/main.yml b/openstack/cloud/tests/ansible/roles/keystone_role/tasks/main.yml similarity index 100% rename from shade/shade/tests/ansible/roles/keystone_role/tasks/main.yml rename to openstack/cloud/tests/ansible/roles/keystone_role/tasks/main.yml diff --git a/shade/shade/tests/ansible/roles/keystone_role/vars/main.yml b/openstack/cloud/tests/ansible/roles/keystone_role/vars/main.yml similarity index 100% rename from shade/shade/tests/ansible/roles/keystone_role/vars/main.yml rename to openstack/cloud/tests/ansible/roles/keystone_role/vars/main.yml diff --git a/shade/shade/tests/ansible/roles/network/tasks/main.yml b/openstack/cloud/tests/ansible/roles/network/tasks/main.yml similarity index 100% rename from shade/shade/tests/ansible/roles/network/tasks/main.yml rename to openstack/cloud/tests/ansible/roles/network/tasks/main.yml diff --git a/shade/shade/tests/ansible/roles/network/vars/main.yml b/openstack/cloud/tests/ansible/roles/network/vars/main.yml similarity index 100% rename from shade/shade/tests/ansible/roles/network/vars/main.yml rename to openstack/cloud/tests/ansible/roles/network/vars/main.yml diff --git a/shade/shade/tests/ansible/roles/nova_flavor/tasks/main.yml b/openstack/cloud/tests/ansible/roles/nova_flavor/tasks/main.yml similarity index 100% rename from shade/shade/tests/ansible/roles/nova_flavor/tasks/main.yml rename to openstack/cloud/tests/ansible/roles/nova_flavor/tasks/main.yml diff --git a/shade/shade/tests/ansible/roles/object/tasks/main.yml b/openstack/cloud/tests/ansible/roles/object/tasks/main.yml similarity index 100% rename from shade/shade/tests/ansible/roles/object/tasks/main.yml rename to openstack/cloud/tests/ansible/roles/object/tasks/main.yml diff --git a/shade/shade/tests/ansible/roles/port/tasks/main.yml b/openstack/cloud/tests/ansible/roles/port/tasks/main.yml similarity index 100% rename from shade/shade/tests/ansible/roles/port/tasks/main.yml rename to openstack/cloud/tests/ansible/roles/port/tasks/main.yml diff --git a/shade/shade/tests/ansible/roles/port/vars/main.yml b/openstack/cloud/tests/ansible/roles/port/vars/main.yml similarity index 100% rename from shade/shade/tests/ansible/roles/port/vars/main.yml rename to openstack/cloud/tests/ansible/roles/port/vars/main.yml diff --git a/shade/shade/tests/ansible/roles/router/tasks/main.yml b/openstack/cloud/tests/ansible/roles/router/tasks/main.yml similarity index 100% rename from shade/shade/tests/ansible/roles/router/tasks/main.yml rename to openstack/cloud/tests/ansible/roles/router/tasks/main.yml diff --git a/shade/shade/tests/ansible/roles/router/vars/main.yml b/openstack/cloud/tests/ansible/roles/router/vars/main.yml similarity index 100% rename from shade/shade/tests/ansible/roles/router/vars/main.yml rename to openstack/cloud/tests/ansible/roles/router/vars/main.yml diff --git a/shade/shade/tests/ansible/roles/security_group/tasks/main.yml b/openstack/cloud/tests/ansible/roles/security_group/tasks/main.yml similarity index 100% rename from shade/shade/tests/ansible/roles/security_group/tasks/main.yml rename to openstack/cloud/tests/ansible/roles/security_group/tasks/main.yml diff --git a/shade/shade/tests/ansible/roles/security_group/vars/main.yml b/openstack/cloud/tests/ansible/roles/security_group/vars/main.yml similarity index 100% rename from shade/shade/tests/ansible/roles/security_group/vars/main.yml rename to openstack/cloud/tests/ansible/roles/security_group/vars/main.yml diff --git a/shade/shade/tests/ansible/roles/server/tasks/main.yml b/openstack/cloud/tests/ansible/roles/server/tasks/main.yml similarity index 100% rename from shade/shade/tests/ansible/roles/server/tasks/main.yml rename to openstack/cloud/tests/ansible/roles/server/tasks/main.yml diff --git a/shade/shade/tests/ansible/roles/server/vars/main.yaml b/openstack/cloud/tests/ansible/roles/server/vars/main.yaml similarity index 100% rename from shade/shade/tests/ansible/roles/server/vars/main.yaml rename to openstack/cloud/tests/ansible/roles/server/vars/main.yaml diff --git a/shade/shade/tests/ansible/roles/subnet/tasks/main.yml b/openstack/cloud/tests/ansible/roles/subnet/tasks/main.yml similarity index 100% rename from shade/shade/tests/ansible/roles/subnet/tasks/main.yml rename to openstack/cloud/tests/ansible/roles/subnet/tasks/main.yml diff --git a/shade/shade/tests/ansible/roles/subnet/vars/main.yml b/openstack/cloud/tests/ansible/roles/subnet/vars/main.yml similarity index 100% rename from shade/shade/tests/ansible/roles/subnet/vars/main.yml rename to openstack/cloud/tests/ansible/roles/subnet/vars/main.yml diff --git a/shade/shade/tests/ansible/roles/user/tasks/main.yml b/openstack/cloud/tests/ansible/roles/user/tasks/main.yml similarity index 100% rename from shade/shade/tests/ansible/roles/user/tasks/main.yml rename to openstack/cloud/tests/ansible/roles/user/tasks/main.yml diff --git a/shade/shade/tests/ansible/roles/user_group/tasks/main.yml b/openstack/cloud/tests/ansible/roles/user_group/tasks/main.yml similarity index 100% rename from shade/shade/tests/ansible/roles/user_group/tasks/main.yml rename to openstack/cloud/tests/ansible/roles/user_group/tasks/main.yml diff --git a/shade/shade/tests/ansible/roles/volume/tasks/main.yml b/openstack/cloud/tests/ansible/roles/volume/tasks/main.yml similarity index 100% rename from shade/shade/tests/ansible/roles/volume/tasks/main.yml rename to openstack/cloud/tests/ansible/roles/volume/tasks/main.yml diff --git a/shade/shade/tests/ansible/run.yml b/openstack/cloud/tests/ansible/run.yml similarity index 100% rename from shade/shade/tests/ansible/run.yml rename to openstack/cloud/tests/ansible/run.yml diff --git a/shade/shade/tests/base.py b/openstack/cloud/tests/base.py similarity index 100% rename from shade/shade/tests/base.py rename to openstack/cloud/tests/base.py diff --git a/shade/shade/tests/fakes.py b/openstack/cloud/tests/fakes.py similarity index 98% rename from shade/shade/tests/fakes.py rename to openstack/cloud/tests/fakes.py index 9ff4c8457..e3ff72934 100644 --- a/shade/shade/tests/fakes.py +++ b/openstack/cloud/tests/fakes.py @@ -21,8 +21,8 @@ import json import uuid -from shade._heat import template_format -from shade import meta +from openstack.cloud._heat import template_format +from openstack.cloud import meta PROJECT_ID = '1c36b64c840a42cd9e9b931a369337f0' FLAVOR_ID = u'0c1d9008-f546-4608-9e8f-f8bdaec8dddd' @@ -235,9 +235,9 @@ def make_fake_image( u'name': u'fake_image', u'checksum': u'ee36e35a297980dee1b514de9803ec6d', u'created_at': u'2016-02-10T05:03:11Z', - u'owner_specified.shade.md5': NO_MD5, - u'owner_specified.shade.sha256': NO_SHA256, - u'owner_specified.shade.object': 'images/fake_image', + u'owner_specified.openstack.cloud.md5': NO_MD5, + u'owner_specified.openstack.cloud.sha256': NO_SHA256, + u'owner_specified.openstack.cloud.object': 'images/fake_image', u'protected': False} diff --git a/shade/shade/tests/__init__.py b/openstack/cloud/tests/functional/__init__.py similarity index 100% rename from shade/shade/tests/__init__.py rename to openstack/cloud/tests/functional/__init__.py diff --git a/shade/shade/tests/functional/base.py b/openstack/cloud/tests/functional/base.py similarity index 93% rename from shade/shade/tests/functional/base.py rename to openstack/cloud/tests/functional/base.py index f2fc32525..32c07d7da 100644 --- a/shade/shade/tests/functional/base.py +++ b/openstack/cloud/tests/functional/base.py @@ -12,10 +12,10 @@ import os -import os_client_config as occ +import openstack.config as occ -import shade -from shade.tests import base +import openstack.cloud +from openstack.cloud.tests import base class BaseFunctionalTestCase(base.TestCase): @@ -36,14 +36,14 @@ def setUp(self): def _set_user_cloud(self, **kwargs): user_config = self.config.get_one_cloud( cloud=self._demo_name, **kwargs) - self.user_cloud = shade.OpenStackCloud( + self.user_cloud = openstack.cloud.OpenStackCloud( cloud_config=user_config, log_inner_exceptions=True) def _set_operator_cloud(self, **kwargs): operator_config = self.config.get_one_cloud( cloud=self._op_name, **kwargs) - self.operator_cloud = shade.OperatorCloud( + self.operator_cloud = openstack.cloud.OperatorCloud( cloud_config=operator_config, log_inner_exceptions=True) diff --git a/shade/shade/tests/functional/hooks/post_test_hook.sh b/openstack/cloud/tests/functional/hooks/post_test_hook.sh similarity index 100% rename from shade/shade/tests/functional/hooks/post_test_hook.sh rename to openstack/cloud/tests/functional/hooks/post_test_hook.sh diff --git a/shade/shade/tests/functional/test_aggregate.py b/openstack/cloud/tests/functional/test_aggregate.py similarity index 97% rename from shade/shade/tests/functional/test_aggregate.py rename to openstack/cloud/tests/functional/test_aggregate.py index db41afd5a..83fe24de6 100644 --- a/shade/shade/tests/functional/test_aggregate.py +++ b/openstack/cloud/tests/functional/test_aggregate.py @@ -17,7 +17,7 @@ Functional tests for `shade` aggregate resource. """ -from shade.tests.functional import base +from openstack.cloud.tests.functional import base class TestAggregate(base.BaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_cluster_templates.py b/openstack/cloud/tests/functional/test_cluster_templates.py similarity index 95% rename from shade/shade/tests/functional/test_cluster_templates.py rename to openstack/cloud/tests/functional/test_cluster_templates.py index b19178c88..cb2ff2634 100644 --- a/shade/shade/tests/functional/test_cluster_templates.py +++ b/openstack/cloud/tests/functional/test_cluster_templates.py @@ -19,7 +19,7 @@ from testtools import content -from shade.tests.functional import base +from openstack.cloud.tests.functional import base import os import subprocess @@ -56,7 +56,7 @@ def test_cluster_templates(self): '%s/id_rsa_shade' % ssh_directory]) # add keypair to nova - with open('%s/id_rsa_shade.pub' % ssh_directory) as f: + with open('%s/id_rsa_openstack.cloud.pub' % ssh_directory) as f: key_content = f.read() self.user_cloud.create_keypair('testkey', key_content) @@ -110,4 +110,4 @@ def cleanup(self, name): # delete keypair self.user_cloud.delete_keypair('testkey') os.unlink('/tmp/.ssh/id_rsa_shade') - os.unlink('/tmp/.ssh/id_rsa_shade.pub') + os.unlink('/tmp/.ssh/id_rsa_openstack.cloud.pub') diff --git a/shade/shade/tests/functional/test_compute.py b/openstack/cloud/tests/functional/test_compute.py similarity index 99% rename from shade/shade/tests/functional/test_compute.py rename to openstack/cloud/tests/functional/test_compute.py index da6c315cb..3c4525410 100644 --- a/shade/shade/tests/functional/test_compute.py +++ b/openstack/cloud/tests/functional/test_compute.py @@ -20,10 +20,10 @@ from fixtures import TimeoutException import six -from shade import exc -from shade.tests.functional import base -from shade.tests.functional.util import pick_flavor -from shade import _utils +from openstack.cloud import exc +from openstack.cloud.tests.functional import base +from openstack.cloud.tests.functional.util import pick_flavor +from openstack.cloud import _utils class TestCompute(base.BaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_devstack.py b/openstack/cloud/tests/functional/test_devstack.py similarity index 97% rename from shade/shade/tests/functional/test_devstack.py rename to openstack/cloud/tests/functional/test_devstack.py index d14392007..b48ba5d8c 100644 --- a/shade/shade/tests/functional/test_devstack.py +++ b/openstack/cloud/tests/functional/test_devstack.py @@ -24,7 +24,7 @@ from testscenarios import load_tests_apply_scenarios as load_tests # noqa -from shade.tests.functional import base +from openstack.cloud.tests.functional import base class TestDevstack(base.BaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_domain.py b/openstack/cloud/tests/functional/test_domain.py similarity index 96% rename from shade/shade/tests/functional/test_domain.py rename to openstack/cloud/tests/functional/test_domain.py index 4d5ca94e5..42ba09aa7 100644 --- a/shade/shade/tests/functional/test_domain.py +++ b/openstack/cloud/tests/functional/test_domain.py @@ -19,8 +19,8 @@ Functional tests for `shade` keystone domain resource. """ -import shade -from shade.tests.functional import base +import openstack.cloud +from openstack.cloud.tests.functional import base class TestDomain(base.BaseFunctionalTestCase): @@ -46,7 +46,8 @@ def _cleanup_domains(self): if exception_list: # Raise an error: we must make users aware that something went # wrong - raise shade.OpenStackCloudException('\n'.join(exception_list)) + raise openstack.cloud.OpenStackCloudException( + '\n'.join(exception_list)) def test_search_domains(self): domain_name = self.domain_prefix + '_search' diff --git a/shade/shade/tests/functional/test_endpoints.py b/openstack/cloud/tests/functional/test_endpoints.py similarity index 97% rename from shade/shade/tests/functional/test_endpoints.py rename to openstack/cloud/tests/functional/test_endpoints.py index da39cb33b..7f32312fe 100644 --- a/shade/shade/tests/functional/test_endpoints.py +++ b/openstack/cloud/tests/functional/test_endpoints.py @@ -24,9 +24,9 @@ import string import random -from shade.exc import OpenStackCloudException -from shade.exc import OpenStackCloudUnavailableFeature -from shade.tests.functional import base +from openstack.cloud.exc import OpenStackCloudException +from openstack.cloud.exc import OpenStackCloudUnavailableFeature +from openstack.cloud.tests.functional import base class TestEndpoints(base.KeystoneBaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_flavor.py b/openstack/cloud/tests/functional/test_flavor.py similarity index 98% rename from shade/shade/tests/functional/test_flavor.py rename to openstack/cloud/tests/functional/test_flavor.py index 71d176f72..3aae4b43b 100644 --- a/shade/shade/tests/functional/test_flavor.py +++ b/openstack/cloud/tests/functional/test_flavor.py @@ -21,8 +21,8 @@ Functional tests for `shade` flavor resource. """ -from shade.exc import OpenStackCloudException -from shade.tests.functional import base +from openstack.cloud.exc import OpenStackCloudException +from openstack.cloud.tests.functional import base class TestFlavor(base.BaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_floating_ip.py b/openstack/cloud/tests/functional/test_floating_ip.py similarity index 97% rename from shade/shade/tests/functional/test_floating_ip.py rename to openstack/cloud/tests/functional/test_floating_ip.py index e74a23f5a..917b8bda9 100644 --- a/shade/shade/tests/functional/test_floating_ip.py +++ b/openstack/cloud/tests/functional/test_floating_ip.py @@ -23,11 +23,11 @@ from testtools import content -from shade import _utils -from shade import meta -from shade.exc import OpenStackCloudException -from shade.tests.functional import base -from shade.tests.functional.util import pick_flavor +from openstack.cloud import _utils +from openstack.cloud import meta +from openstack.cloud.exc import OpenStackCloudException +from openstack.cloud.tests.functional import base +from openstack.cloud.tests.functional.util import pick_flavor class TestFloatingIP(base.BaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_floating_ip_pool.py b/openstack/cloud/tests/functional/test_floating_ip_pool.py similarity index 97% rename from shade/shade/tests/functional/test_floating_ip_pool.py rename to openstack/cloud/tests/functional/test_floating_ip_pool.py index c4ee5a0f6..987e83ba4 100644 --- a/shade/shade/tests/functional/test_floating_ip_pool.py +++ b/openstack/cloud/tests/functional/test_floating_ip_pool.py @@ -19,7 +19,7 @@ Functional tests for floating IP pool resource (managed by nova) """ -from shade.tests.functional import base +from openstack.cloud.tests.functional import base # When using nova-network, floating IP pools are created with nova-manage diff --git a/shade/shade/tests/functional/test_groups.py b/openstack/cloud/tests/functional/test_groups.py similarity index 95% rename from shade/shade/tests/functional/test_groups.py rename to openstack/cloud/tests/functional/test_groups.py index cabfeb52a..e48e8b851 100644 --- a/shade/shade/tests/functional/test_groups.py +++ b/openstack/cloud/tests/functional/test_groups.py @@ -19,8 +19,8 @@ Functional tests for `shade` keystone group resource. """ -import shade -from shade.tests.functional import base +import openstack.cloud +from openstack.cloud.tests.functional import base class TestGroup(base.BaseFunctionalTestCase): @@ -46,7 +46,8 @@ def _cleanup_groups(self): if exception_list: # Raise an error: we must make users aware that something went # wrong - raise shade.OpenStackCloudException('\n'.join(exception_list)) + raise openstack.cloud.OpenStackCloudException( + '\n'.join(exception_list)) def test_create_group(self): group_name = self.group_prefix + '_create' diff --git a/shade/shade/tests/functional/test_identity.py b/openstack/cloud/tests/functional/test_identity.py similarity index 99% rename from shade/shade/tests/functional/test_identity.py rename to openstack/cloud/tests/functional/test_identity.py index 56413f7db..2636f1dbe 100644 --- a/shade/shade/tests/functional/test_identity.py +++ b/openstack/cloud/tests/functional/test_identity.py @@ -20,8 +20,8 @@ import random import string -from shade import OpenStackCloudException -from shade.tests.functional import base +from openstack.cloud import OpenStackCloudException +from openstack.cloud.tests.functional import base class TestIdentity(base.KeystoneBaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_image.py b/openstack/cloud/tests/functional/test_image.py similarity index 99% rename from shade/shade/tests/functional/test_image.py rename to openstack/cloud/tests/functional/test_image.py index 960214ba1..26a48ea3c 100644 --- a/shade/shade/tests/functional/test_image.py +++ b/openstack/cloud/tests/functional/test_image.py @@ -21,7 +21,7 @@ import os import tempfile -from shade.tests.functional import base +from openstack.cloud.tests.functional import base class TestImage(base.BaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_inventory.py b/openstack/cloud/tests/functional/test_inventory.py similarity index 95% rename from shade/shade/tests/functional/test_inventory.py rename to openstack/cloud/tests/functional/test_inventory.py index ce98625cf..9e5143cab 100644 --- a/shade/shade/tests/functional/test_inventory.py +++ b/openstack/cloud/tests/functional/test_inventory.py @@ -19,10 +19,10 @@ Functional tests for `shade` inventory methods. """ -from shade import inventory +from openstack.cloud import inventory -from shade.tests.functional import base -from shade.tests.functional.util import pick_flavor +from openstack.cloud.tests.functional import base +from openstack.cloud.tests.functional.util import pick_flavor class TestInventory(base.BaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_keypairs.py b/openstack/cloud/tests/functional/test_keypairs.py similarity index 96% rename from shade/shade/tests/functional/test_keypairs.py rename to openstack/cloud/tests/functional/test_keypairs.py index 35a159e1c..f990f6367 100644 --- a/shade/shade/tests/functional/test_keypairs.py +++ b/openstack/cloud/tests/functional/test_keypairs.py @@ -16,8 +16,8 @@ Functional tests for `shade` keypairs methods """ -from shade.tests import fakes -from shade.tests.functional import base +from openstack.cloud.tests import fakes +from openstack.cloud.tests.functional import base class TestKeypairs(base.BaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_limits.py b/openstack/cloud/tests/functional/test_limits.py similarity index 96% rename from shade/shade/tests/functional/test_limits.py rename to openstack/cloud/tests/functional/test_limits.py index 015dafec0..2aae80434 100644 --- a/shade/shade/tests/functional/test_limits.py +++ b/openstack/cloud/tests/functional/test_limits.py @@ -16,7 +16,7 @@ Functional tests for `shade` limits method """ -from shade.tests.functional import base +from openstack.cloud.tests.functional import base class TestUsage(base.BaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_magnum_services.py b/openstack/cloud/tests/functional/test_magnum_services.py similarity index 96% rename from shade/shade/tests/functional/test_magnum_services.py rename to openstack/cloud/tests/functional/test_magnum_services.py index d77468618..2690b910f 100644 --- a/shade/shade/tests/functional/test_magnum_services.py +++ b/openstack/cloud/tests/functional/test_magnum_services.py @@ -17,7 +17,7 @@ Functional tests for `shade` services method. """ -from shade.tests.functional import base +from openstack.cloud.tests.functional import base class TestMagnumServices(base.BaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_network.py b/openstack/cloud/tests/functional/test_network.py similarity index 97% rename from shade/shade/tests/functional/test_network.py rename to openstack/cloud/tests/functional/test_network.py index 14e5776d5..8a4b5ee1a 100644 --- a/shade/shade/tests/functional/test_network.py +++ b/openstack/cloud/tests/functional/test_network.py @@ -17,8 +17,8 @@ Functional tests for `shade` network methods. """ -from shade.exc import OpenStackCloudException -from shade.tests.functional import base +from openstack.cloud.exc import OpenStackCloudException +from openstack.cloud.tests.functional import base class TestNetwork(base.BaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_object.py b/openstack/cloud/tests/functional/test_object.py similarity index 98% rename from shade/shade/tests/functional/test_object.py rename to openstack/cloud/tests/functional/test_object.py index a13b27ac3..17d914b8f 100644 --- a/shade/shade/tests/functional/test_object.py +++ b/openstack/cloud/tests/functional/test_object.py @@ -23,8 +23,8 @@ from testtools import content -from shade import exc -from shade.tests.functional import base +from openstack.cloud import exc +from openstack.cloud.tests.functional import base class TestObject(base.BaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_port.py b/openstack/cloud/tests/functional/test_port.py similarity index 98% rename from shade/shade/tests/functional/test_port.py rename to openstack/cloud/tests/functional/test_port.py index a824a2fe3..1cea9e2cb 100644 --- a/shade/shade/tests/functional/test_port.py +++ b/openstack/cloud/tests/functional/test_port.py @@ -24,8 +24,8 @@ import string import random -from shade.exc import OpenStackCloudException -from shade.tests.functional import base +from openstack.cloud.exc import OpenStackCloudException +from openstack.cloud.tests.functional import base class TestPort(base.BaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_project.py b/openstack/cloud/tests/functional/test_project.py similarity index 97% rename from shade/shade/tests/functional/test_project.py rename to openstack/cloud/tests/functional/test_project.py index 25d59e6db..1ba501d72 100644 --- a/shade/shade/tests/functional/test_project.py +++ b/openstack/cloud/tests/functional/test_project.py @@ -21,8 +21,8 @@ Functional tests for `shade` project resource. """ -from shade.exc import OpenStackCloudException -from shade.tests.functional import base +from openstack.cloud.exc import OpenStackCloudException +from openstack.cloud.tests.functional import base class TestProject(base.KeystoneBaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_qos_bandwidth_limit_rule.py b/openstack/cloud/tests/functional/test_qos_bandwidth_limit_rule.py similarity index 97% rename from shade/shade/tests/functional/test_qos_bandwidth_limit_rule.py rename to openstack/cloud/tests/functional/test_qos_bandwidth_limit_rule.py index 3bec8ced2..f1b449b22 100644 --- a/shade/shade/tests/functional/test_qos_bandwidth_limit_rule.py +++ b/openstack/cloud/tests/functional/test_qos_bandwidth_limit_rule.py @@ -18,8 +18,8 @@ Functional tests for `shade`QoS bandwidth limit methods. """ -from shade.exc import OpenStackCloudException -from shade.tests.functional import base +from openstack.cloud.exc import OpenStackCloudException +from openstack.cloud.tests.functional import base class TestQosBandwidthLimitRule(base.BaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_qos_dscp_marking_rule.py b/openstack/cloud/tests/functional/test_qos_dscp_marking_rule.py similarity index 96% rename from shade/shade/tests/functional/test_qos_dscp_marking_rule.py rename to openstack/cloud/tests/functional/test_qos_dscp_marking_rule.py index 3640fe4e9..b326f126f 100644 --- a/shade/shade/tests/functional/test_qos_dscp_marking_rule.py +++ b/openstack/cloud/tests/functional/test_qos_dscp_marking_rule.py @@ -18,8 +18,8 @@ Functional tests for `shade`QoS DSCP marking rule methods. """ -from shade.exc import OpenStackCloudException -from shade.tests.functional import base +from openstack.cloud.exc import OpenStackCloudException +from openstack.cloud.tests.functional import base class TestQosDscpMarkingRule(base.BaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_qos_minimum_bandwidth_rule.py b/openstack/cloud/tests/functional/test_qos_minimum_bandwidth_rule.py similarity index 96% rename from shade/shade/tests/functional/test_qos_minimum_bandwidth_rule.py rename to openstack/cloud/tests/functional/test_qos_minimum_bandwidth_rule.py index ad5291f10..a0492b262 100644 --- a/shade/shade/tests/functional/test_qos_minimum_bandwidth_rule.py +++ b/openstack/cloud/tests/functional/test_qos_minimum_bandwidth_rule.py @@ -18,8 +18,8 @@ Functional tests for `shade`QoS minimum bandwidth methods. """ -from shade.exc import OpenStackCloudException -from shade.tests.functional import base +from openstack.cloud.exc import OpenStackCloudException +from openstack.cloud.tests.functional import base class TestQosMinimumBandwidthRule(base.BaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_qos_policy.py b/openstack/cloud/tests/functional/test_qos_policy.py similarity index 97% rename from shade/shade/tests/functional/test_qos_policy.py rename to openstack/cloud/tests/functional/test_qos_policy.py index ba418337d..670fe38ad 100644 --- a/shade/shade/tests/functional/test_qos_policy.py +++ b/openstack/cloud/tests/functional/test_qos_policy.py @@ -18,8 +18,8 @@ Functional tests for `shade`QoS policies methods. """ -from shade.exc import OpenStackCloudException -from shade.tests.functional import base +from openstack.cloud.exc import OpenStackCloudException +from openstack.cloud.tests.functional import base class TestQosPolicy(base.BaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_quotas.py b/openstack/cloud/tests/functional/test_quotas.py similarity index 98% rename from shade/shade/tests/functional/test_quotas.py rename to openstack/cloud/tests/functional/test_quotas.py index dd2816873..3abc2cd0c 100644 --- a/shade/shade/tests/functional/test_quotas.py +++ b/openstack/cloud/tests/functional/test_quotas.py @@ -17,7 +17,7 @@ Functional tests for `shade` quotas methods. """ -from shade.tests.functional import base +from openstack.cloud.tests.functional import base class TestComputeQuotas(base.BaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_range_search.py b/openstack/cloud/tests/functional/test_range_search.py similarity index 98% rename from shade/shade/tests/functional/test_range_search.py rename to openstack/cloud/tests/functional/test_range_search.py index 15bf277df..d7089269d 100644 --- a/shade/shade/tests/functional/test_range_search.py +++ b/openstack/cloud/tests/functional/test_range_search.py @@ -15,8 +15,8 @@ # limitations under the License. -from shade import exc -from shade.tests.functional import base +from openstack.cloud import exc +from openstack.cloud.tests.functional import base class TestRangeSearch(base.BaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_recordset.py b/openstack/cloud/tests/functional/test_recordset.py similarity index 98% rename from shade/shade/tests/functional/test_recordset.py rename to openstack/cloud/tests/functional/test_recordset.py index 98e209f83..53f98eef7 100644 --- a/shade/shade/tests/functional/test_recordset.py +++ b/openstack/cloud/tests/functional/test_recordset.py @@ -19,7 +19,7 @@ from testtools import content -from shade.tests.functional import base +from openstack.cloud.tests.functional import base class TestRecordset(base.BaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_router.py b/openstack/cloud/tests/functional/test_router.py similarity index 99% rename from shade/shade/tests/functional/test_router.py rename to openstack/cloud/tests/functional/test_router.py index 5c4ccde46..0259347cb 100644 --- a/shade/shade/tests/functional/test_router.py +++ b/openstack/cloud/tests/functional/test_router.py @@ -19,8 +19,8 @@ import ipaddress -from shade.exc import OpenStackCloudException -from shade.tests.functional import base +from openstack.cloud.exc import OpenStackCloudException +from openstack.cloud.tests.functional import base EXPECTED_TOPLEVEL_FIELDS = ( diff --git a/shade/shade/tests/functional/test_security_groups.py b/openstack/cloud/tests/functional/test_security_groups.py similarity index 98% rename from shade/shade/tests/functional/test_security_groups.py rename to openstack/cloud/tests/functional/test_security_groups.py index de9c05f99..23cc006dd 100644 --- a/shade/shade/tests/functional/test_security_groups.py +++ b/openstack/cloud/tests/functional/test_security_groups.py @@ -17,7 +17,7 @@ Functional tests for `shade` security_groups resource. """ -from shade.tests.functional import base +from openstack.cloud.tests.functional import base class TestSecurityGroups(base.BaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_server_group.py b/openstack/cloud/tests/functional/test_server_group.py similarity index 96% rename from shade/shade/tests/functional/test_server_group.py rename to openstack/cloud/tests/functional/test_server_group.py index e4e7f8ecf..4dc32a4cd 100644 --- a/shade/shade/tests/functional/test_server_group.py +++ b/openstack/cloud/tests/functional/test_server_group.py @@ -17,7 +17,7 @@ Functional tests for `shade` server_group resource. """ -from shade.tests.functional import base +from openstack.cloud.tests.functional import base class TestServerGroup(base.BaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_services.py b/openstack/cloud/tests/functional/test_services.py similarity index 96% rename from shade/shade/tests/functional/test_services.py rename to openstack/cloud/tests/functional/test_services.py index 92920d53f..8c5191490 100644 --- a/shade/shade/tests/functional/test_services.py +++ b/openstack/cloud/tests/functional/test_services.py @@ -24,9 +24,9 @@ import string import random -from shade.exc import OpenStackCloudException -from shade.exc import OpenStackCloudUnavailableFeature -from shade.tests.functional import base +from openstack.cloud.exc import OpenStackCloudException +from openstack.cloud.exc import OpenStackCloudUnavailableFeature +from openstack.cloud.tests.functional import base class TestServices(base.KeystoneBaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_stack.py b/openstack/cloud/tests/functional/test_stack.py similarity index 97% rename from shade/shade/tests/functional/test_stack.py rename to openstack/cloud/tests/functional/test_stack.py index cd6d1fc20..eb3164561 100644 --- a/shade/shade/tests/functional/test_stack.py +++ b/openstack/cloud/tests/functional/test_stack.py @@ -19,9 +19,9 @@ import tempfile -from shade import exc -from shade.tests import fakes -from shade.tests.functional import base +from openstack.cloud import exc +from openstack.cloud.tests import fakes +from openstack.cloud.tests.functional import base simple_template = '''heat_template_version: 2014-10-16 parameters: diff --git a/shade/shade/tests/functional/test_usage.py b/openstack/cloud/tests/functional/test_usage.py similarity index 96% rename from shade/shade/tests/functional/test_usage.py rename to openstack/cloud/tests/functional/test_usage.py index ae36ab11d..0f5a3bcdc 100644 --- a/shade/shade/tests/functional/test_usage.py +++ b/openstack/cloud/tests/functional/test_usage.py @@ -20,7 +20,7 @@ """ import datetime -from shade.tests.functional import base +from openstack.cloud.tests.functional import base class TestUsage(base.BaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_users.py b/openstack/cloud/tests/functional/test_users.py similarity index 97% rename from shade/shade/tests/functional/test_users.py rename to openstack/cloud/tests/functional/test_users.py index 985853854..2a147fb33 100644 --- a/shade/shade/tests/functional/test_users.py +++ b/openstack/cloud/tests/functional/test_users.py @@ -17,9 +17,9 @@ Functional tests for `shade` user methods. """ -from shade import operator_cloud -from shade import OpenStackCloudException -from shade.tests.functional import base +from openstack.cloud import operator_cloud +from openstack.cloud import OpenStackCloudException +from openstack.cloud.tests.functional import base class TestUsers(base.KeystoneBaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_volume.py b/openstack/cloud/tests/functional/test_volume.py similarity index 98% rename from shade/shade/tests/functional/test_volume.py rename to openstack/cloud/tests/functional/test_volume.py index fe4dd6c6f..3cc3898c9 100644 --- a/shade/shade/tests/functional/test_volume.py +++ b/openstack/cloud/tests/functional/test_volume.py @@ -20,9 +20,9 @@ from fixtures import TimeoutException from testtools import content -from shade import _utils -from shade import exc -from shade.tests.functional import base +from openstack.cloud import _utils +from openstack.cloud import exc +from openstack.cloud.tests.functional import base class TestVolume(base.BaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_volume_backup.py b/openstack/cloud/tests/functional/test_volume_backup.py similarity index 98% rename from shade/shade/tests/functional/test_volume_backup.py rename to openstack/cloud/tests/functional/test_volume_backup.py index 3a7d4bff9..dcbc76fb3 100644 --- a/shade/shade/tests/functional/test_volume_backup.py +++ b/openstack/cloud/tests/functional/test_volume_backup.py @@ -9,7 +9,7 @@ # 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 shade.tests.functional import base +from openstack.cloud.tests.functional import base class TestVolume(base.BaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_volume_type.py b/openstack/cloud/tests/functional/test_volume_type.py similarity index 98% rename from shade/shade/tests/functional/test_volume_type.py rename to openstack/cloud/tests/functional/test_volume_type.py index 03567837f..bc09a7413 100644 --- a/shade/shade/tests/functional/test_volume_type.py +++ b/openstack/cloud/tests/functional/test_volume_type.py @@ -19,8 +19,8 @@ Functional tests for `shade` block storage methods. """ import testtools -from shade import exc -from shade.tests.functional import base +from openstack.cloud import exc +from openstack.cloud.tests.functional import base class TestVolumeType(base.BaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/test_zone.py b/openstack/cloud/tests/functional/test_zone.py similarity index 98% rename from shade/shade/tests/functional/test_zone.py rename to openstack/cloud/tests/functional/test_zone.py index 3a90d9b78..e8ade0aeb 100644 --- a/shade/shade/tests/functional/test_zone.py +++ b/openstack/cloud/tests/functional/test_zone.py @@ -19,7 +19,7 @@ from testtools import content -from shade.tests.functional import base +from openstack.cloud.tests.functional import base class TestZone(base.BaseFunctionalTestCase): diff --git a/shade/shade/tests/functional/util.py b/openstack/cloud/tests/functional/util.py similarity index 100% rename from shade/shade/tests/functional/util.py rename to openstack/cloud/tests/functional/util.py diff --git a/shade/shade/tests/functional/__init__.py b/openstack/cloud/tests/unit/__init__.py similarity index 100% rename from shade/shade/tests/functional/__init__.py rename to openstack/cloud/tests/unit/__init__.py diff --git a/shade/shade/tests/unit/base.py b/openstack/cloud/tests/unit/base.py similarity index 98% rename from shade/shade/tests/unit/base.py rename to openstack/cloud/tests/unit/base.py index 95f731e36..ffeefe019 100644 --- a/shade/shade/tests/unit/base.py +++ b/openstack/cloud/tests/unit/base.py @@ -20,14 +20,14 @@ import fixtures import mock import os -import os_client_config as occ +import openstack.config as occ from requests import structures from requests_mock.contrib import fixture as rm_fixture from six.moves import urllib import tempfile -import shade.openstackcloud -from shade.tests import base +import openstack.cloud.openstackcloud +from openstack.cloud.tests import base _ProjectData = collections.namedtuple( @@ -125,14 +125,14 @@ def _nosleep(seconds): secure_files=['non-existant']) self.cloud_config = self.config.get_one_cloud( cloud=test_cloud, validate=False) - self.cloud = shade.OpenStackCloud( + self.cloud = openstack.cloud.OpenStackCloud( cloud_config=self.cloud_config, log_inner_exceptions=True) - self.strict_cloud = shade.OpenStackCloud( + self.strict_cloud = openstack.cloud.OpenStackCloud( cloud_config=self.cloud_config, log_inner_exceptions=True, strict=True) - self.op_cloud = shade.OperatorCloud( + self.op_cloud = openstack.cloud.OperatorCloud( cloud_config=self.cloud_config, log_inner_exceptions=True) @@ -161,7 +161,8 @@ def setUp(self, cloud_config_fixture='clouds.yaml'): # assert_calls (and calling assert_calls every test case that uses # it on cleanup). Subclassing here could be 100% eliminated in the # future allowing any class to simply - # self.useFixture(shade.RequestsMockFixture) and get all the benefits. + # self.useFixture(openstack.cloud.RequestsMockFixture) and get all + # the benefits. # NOTE(notmorgan): use an ordered dict here to ensure we preserve the # order in which items are added to the uri_registry. This makes @@ -446,10 +447,10 @@ def _make_test_cloud(self, cloud_name='_test_cloud_', **kwargs): test_cloud = os.environ.get('SHADE_OS_CLOUD', cloud_name) self.cloud_config = self.config.get_one_cloud( cloud=test_cloud, validate=True, **kwargs) - self.cloud = shade.OpenStackCloud( + self.cloud = openstack.cloud.OpenStackCloud( cloud_config=self.cloud_config, log_inner_exceptions=True) - self.op_cloud = shade.OperatorCloud( + self.op_cloud = openstack.cloud.OperatorCloud( cloud_config=self.cloud_config, log_inner_exceptions=True) diff --git a/shade/shade/tests/unit/fixtures/baremetal.json b/openstack/cloud/tests/unit/fixtures/baremetal.json similarity index 100% rename from shade/shade/tests/unit/fixtures/baremetal.json rename to openstack/cloud/tests/unit/fixtures/baremetal.json diff --git a/shade/shade/tests/unit/fixtures/catalog-v2.json b/openstack/cloud/tests/unit/fixtures/catalog-v2.json similarity index 100% rename from shade/shade/tests/unit/fixtures/catalog-v2.json rename to openstack/cloud/tests/unit/fixtures/catalog-v2.json diff --git a/shade/shade/tests/unit/fixtures/catalog-v3.json b/openstack/cloud/tests/unit/fixtures/catalog-v3.json similarity index 100% rename from shade/shade/tests/unit/fixtures/catalog-v3.json rename to openstack/cloud/tests/unit/fixtures/catalog-v3.json diff --git a/shade/shade/tests/unit/fixtures/clouds/clouds.yaml b/openstack/cloud/tests/unit/fixtures/clouds/clouds.yaml similarity index 100% rename from shade/shade/tests/unit/fixtures/clouds/clouds.yaml rename to openstack/cloud/tests/unit/fixtures/clouds/clouds.yaml diff --git a/shade/shade/tests/unit/fixtures/clouds/clouds_cache.yaml b/openstack/cloud/tests/unit/fixtures/clouds/clouds_cache.yaml similarity index 100% rename from shade/shade/tests/unit/fixtures/clouds/clouds_cache.yaml rename to openstack/cloud/tests/unit/fixtures/clouds/clouds_cache.yaml diff --git a/shade/shade/tests/unit/fixtures/discovery.json b/openstack/cloud/tests/unit/fixtures/discovery.json similarity index 100% rename from shade/shade/tests/unit/fixtures/discovery.json rename to openstack/cloud/tests/unit/fixtures/discovery.json diff --git a/shade/shade/tests/unit/fixtures/dns.json b/openstack/cloud/tests/unit/fixtures/dns.json similarity index 100% rename from shade/shade/tests/unit/fixtures/dns.json rename to openstack/cloud/tests/unit/fixtures/dns.json diff --git a/shade/shade/tests/unit/fixtures/image-version-broken.json b/openstack/cloud/tests/unit/fixtures/image-version-broken.json similarity index 100% rename from shade/shade/tests/unit/fixtures/image-version-broken.json rename to openstack/cloud/tests/unit/fixtures/image-version-broken.json diff --git a/shade/shade/tests/unit/fixtures/image-version-v1.json b/openstack/cloud/tests/unit/fixtures/image-version-v1.json similarity index 100% rename from shade/shade/tests/unit/fixtures/image-version-v1.json rename to openstack/cloud/tests/unit/fixtures/image-version-v1.json diff --git a/shade/shade/tests/unit/fixtures/image-version-v2.json b/openstack/cloud/tests/unit/fixtures/image-version-v2.json similarity index 100% rename from shade/shade/tests/unit/fixtures/image-version-v2.json rename to openstack/cloud/tests/unit/fixtures/image-version-v2.json diff --git a/shade/shade/tests/unit/fixtures/image-version.json b/openstack/cloud/tests/unit/fixtures/image-version.json similarity index 100% rename from shade/shade/tests/unit/fixtures/image-version.json rename to openstack/cloud/tests/unit/fixtures/image-version.json diff --git a/shade/shade/tests/unit/test__adapter.py b/openstack/cloud/tests/unit/test__adapter.py similarity index 94% rename from shade/shade/tests/unit/test__adapter.py rename to openstack/cloud/tests/unit/test__adapter.py index 68063d65f..57d87a762 100644 --- a/shade/shade/tests/unit/test__adapter.py +++ b/openstack/cloud/tests/unit/test__adapter.py @@ -12,8 +12,8 @@ from testscenarios import load_tests_apply_scenarios as load_tests # noqa -from shade import _adapter -from shade.tests.unit import base +from openstack.cloud import _adapter +from openstack.cloud.tests.unit import base class TestExtractName(base.TestCase): diff --git a/shade/shade/tests/unit/test__utils.py b/openstack/cloud/tests/unit/test__utils.py similarity index 99% rename from shade/shade/tests/unit/test__utils.py rename to openstack/cloud/tests/unit/test__utils.py index 1c453f426..d2e243060 100644 --- a/shade/shade/tests/unit/test__utils.py +++ b/openstack/cloud/tests/unit/test__utils.py @@ -20,9 +20,9 @@ import mock import testtools -from shade import _utils -from shade import exc -from shade.tests.unit import base +from openstack.cloud import _utils +from openstack.cloud import exc +from openstack.cloud.tests.unit import base RANGE_DATA = [ diff --git a/shade/shade/tests/unit/test_aggregate.py b/openstack/cloud/tests/unit/test_aggregate.py similarity index 98% rename from shade/shade/tests/unit/test_aggregate.py rename to openstack/cloud/tests/unit/test_aggregate.py index 1d832dc4c..9878ad64a 100644 --- a/shade/shade/tests/unit/test_aggregate.py +++ b/openstack/cloud/tests/unit/test_aggregate.py @@ -10,8 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from shade.tests.unit import base -from shade.tests import fakes +from openstack.cloud.tests.unit import base +from openstack.cloud.tests import fakes class TestAggregate(base.RequestsMockTestCase): diff --git a/shade/shade/tests/unit/test_availability_zones.py b/openstack/cloud/tests/unit/test_availability_zones.py similarity index 96% rename from shade/shade/tests/unit/test_availability_zones.py rename to openstack/cloud/tests/unit/test_availability_zones.py index fbbd8cc99..7a6e0a1ab 100644 --- a/shade/shade/tests/unit/test_availability_zones.py +++ b/openstack/cloud/tests/unit/test_availability_zones.py @@ -12,8 +12,8 @@ # under the License. -from shade.tests.unit import base -from shade.tests import fakes +from openstack.cloud.tests.unit import base +from openstack.cloud.tests import fakes _fake_zone_list = { diff --git a/shade/shade/tests/unit/test_baremetal_node.py b/openstack/cloud/tests/unit/test_baremetal_node.py similarity index 99% rename from shade/shade/tests/unit/test_baremetal_node.py rename to openstack/cloud/tests/unit/test_baremetal_node.py index d76b37597..a906013d5 100644 --- a/shade/shade/tests/unit/test_baremetal_node.py +++ b/openstack/cloud/tests/unit/test_baremetal_node.py @@ -21,9 +21,9 @@ from testscenarios import load_tests_apply_scenarios as load_tests # noqa -from shade import exc -from shade.tests import fakes -from shade.tests.unit import base +from openstack.cloud import exc +from openstack.cloud.tests import fakes +from openstack.cloud.tests.unit import base class TestBaremetalNode(base.IronicTestCase): diff --git a/shade/shade/tests/unit/test_caching.py b/openstack/cloud/tests/unit/test_caching.py similarity index 97% rename from shade/shade/tests/unit/test_caching.py rename to openstack/cloud/tests/unit/test_caching.py index 97fe4bb10..4264202a8 100644 --- a/shade/shade/tests/unit/test_caching.py +++ b/openstack/cloud/tests/unit/test_caching.py @@ -16,11 +16,11 @@ import munch import testtools -import shade.openstackcloud -from shade import exc -from shade import meta -from shade.tests import fakes -from shade.tests.unit import base +import openstack.cloud.openstackcloud +from openstack.cloud import exc +from openstack.cloud import meta +from openstack.cloud.tests import fakes +from openstack.cloud.tests.unit import base # Mock out the gettext function so that the task schema can be copypasta @@ -104,7 +104,7 @@ def _munch_images(self, fake_image): return self.cloud._normalize_images([fake_image]) def test_openstack_cloud(self): - self.assertIsInstance(self.cloud, shade.OpenStackCloud) + self.assertIsInstance(self.cloud, openstack.cloud.OpenStackCloud) def test_list_projects_v3(self): project_one = self._get_project_data() @@ -480,7 +480,7 @@ def test_list_images(self): self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, '_image_client') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') def test_list_images_ignores_unsteady_status(self, mock_image_client): steady_image = munch.Munch(id='68', name='Jagr', status='active') for status in ('queued', 'saving', 'pending_delete'): @@ -500,7 +500,7 @@ def test_list_images_ignores_unsteady_status(self, mock_image_client): self._image_dict(steady_image)], self.cloud.list_images()) - @mock.patch.object(shade.OpenStackCloud, '_image_client') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') def test_list_images_caches_steady_status(self, mock_image_client): steady_image = munch.Munch(id='91', name='Federov', status='active') first_image = None @@ -523,7 +523,7 @@ def test_list_images_caches_steady_status(self, mock_image_client): self._munch_images(first_image), self.cloud.list_images()) - @mock.patch.object(shade.OpenStackCloud, '_image_client') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') def test_cache_no_cloud_name(self, mock_image_client): self.cloud.name = None @@ -557,5 +557,5 @@ def setUp(self): def test_get_auth_bogus(self): with testtools.ExpectedException(exc.OpenStackCloudException): - shade.openstack_cloud( + openstack.cloud.openstack_cloud( cloud='_bogus_test_', config=self.config) diff --git a/shade/shade/tests/unit/test_cluster_templates.py b/openstack/cloud/tests/unit/test_cluster_templates.py similarity index 97% rename from shade/shade/tests/unit/test_cluster_templates.py rename to openstack/cloud/tests/unit/test_cluster_templates.py index dbab19bdb..88915927d 100644 --- a/shade/shade/tests/unit/test_cluster_templates.py +++ b/openstack/cloud/tests/unit/test_cluster_templates.py @@ -13,9 +13,9 @@ import munch -import shade +import openstack.cloud import testtools -from shade.tests.unit import base +from openstack.cloud.tests.unit import base cluster_template_obj = munch.Munch( @@ -151,7 +151,8 @@ def test_create_cluster_template_exception(self): # OpenStackCloudException - but for some reason testtools will not # match the more specific HTTPError, even though it's a subclass # of OpenStackCloudException. - with testtools.ExpectedException(shade.OpenStackCloudHTTPError): + with testtools.ExpectedException( + openstack.cloud.OpenStackCloudHTTPError): self.cloud.create_cluster_template('fake-cluster-template') self.assert_calls() diff --git a/shade/shade/tests/unit/test_create_server.py b/openstack/cloud/tests/unit/test_create_server.py similarity index 98% rename from shade/shade/tests/unit/test_create_server.py rename to openstack/cloud/tests/unit/test_create_server.py index 0e2855e7c..f85b9727e 100644 --- a/shade/shade/tests/unit/test_create_server.py +++ b/openstack/cloud/tests/unit/test_create_server.py @@ -21,11 +21,11 @@ import mock -import shade -from shade import exc -from shade import meta -from shade.tests import fakes -from shade.tests.unit import base +import openstack.cloud +from openstack.cloud import exc +from openstack.cloud import meta +from openstack.cloud.tests import fakes +from openstack.cloud.tests.unit import base class TestCreateServer(base.RequestsMockTestCase): @@ -325,7 +325,7 @@ def test_create_server_with_admin_pass_no_wait(self): self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, "wait_for_server") + @mock.patch.object(openstack.cloud.OpenStackCloud, "wait_for_server") def test_create_server_with_admin_pass_wait(self, mock_wait): """ Test that a server with an admin_pass passed returns the password @@ -410,8 +410,8 @@ def test_create_server_user_data_base64(self): self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, "get_active_server") - @mock.patch.object(shade.OpenStackCloud, "get_server") + @mock.patch.object(openstack.cloud.OpenStackCloud, "get_active_server") + @mock.patch.object(openstack.cloud.OpenStackCloud, "get_server") def test_wait_for_server(self, mock_get_server, mock_get_active_server): """ Test that waiting for a server returns the server instance when @@ -445,7 +445,7 @@ def test_wait_for_server(self, mock_get_server, mock_get_active_server): self.assertEqual('ACTIVE', server['status']) - @mock.patch.object(shade.OpenStackCloud, 'wait_for_server') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'wait_for_server') def test_create_server_wait(self, mock_wait): """ Test that create_server with a wait actually does the wait. @@ -482,7 +482,7 @@ def test_create_server_wait(self, mock_wait): ) self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'add_ips_to_server') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'add_ips_to_server') @mock.patch('time.sleep') def test_create_server_no_addresses( self, mock_sleep, mock_add_ips_to_server): diff --git a/shade/shade/tests/unit/test_create_volume_snapshot.py b/openstack/cloud/tests/unit/test_create_volume_snapshot.py similarity index 97% rename from shade/shade/tests/unit/test_create_volume_snapshot.py rename to openstack/cloud/tests/unit/test_create_volume_snapshot.py index 2d1636f2f..0f85b81f0 100644 --- a/shade/shade/tests/unit/test_create_volume_snapshot.py +++ b/openstack/cloud/tests/unit/test_create_volume_snapshot.py @@ -17,10 +17,10 @@ Tests for the `create_volume_snapshot` command. """ -from shade import exc -from shade import meta -from shade.tests import fakes -from shade.tests.unit import base +from openstack.cloud import exc +from openstack.cloud import meta +from openstack.cloud.tests import fakes +from openstack.cloud.tests.unit import base class TestCreateVolumeSnapshot(base.RequestsMockTestCase): diff --git a/shade/shade/tests/unit/test_delete_server.py b/openstack/cloud/tests/unit/test_delete_server.py similarity index 98% rename from shade/shade/tests/unit/test_delete_server.py rename to openstack/cloud/tests/unit/test_delete_server.py index 0e558e509..8f8cc9d5d 100644 --- a/shade/shade/tests/unit/test_delete_server.py +++ b/openstack/cloud/tests/unit/test_delete_server.py @@ -18,9 +18,9 @@ """ import uuid -from shade import exc as shade_exc -from shade.tests import fakes -from shade.tests.unit import base +from openstack.cloud import exc as shade_exc +from openstack.cloud.tests import fakes +from openstack.cloud.tests.unit import base class TestDeleteServer(base.RequestsMockTestCase): diff --git a/shade/shade/tests/unit/test_delete_volume_snapshot.py b/openstack/cloud/tests/unit/test_delete_volume_snapshot.py similarity index 96% rename from shade/shade/tests/unit/test_delete_volume_snapshot.py rename to openstack/cloud/tests/unit/test_delete_volume_snapshot.py index 3d7920b6a..c267c1d80 100644 --- a/shade/shade/tests/unit/test_delete_volume_snapshot.py +++ b/openstack/cloud/tests/unit/test_delete_volume_snapshot.py @@ -17,10 +17,10 @@ Tests for the `delete_volume_snapshot` command. """ -from shade import exc -from shade import meta -from shade.tests import fakes -from shade.tests.unit import base +from openstack.cloud import exc +from openstack.cloud import meta +from openstack.cloud.tests import fakes +from openstack.cloud.tests.unit import base class TestDeleteVolumeSnapshot(base.RequestsMockTestCase): diff --git a/shade/shade/tests/unit/test_domain_params.py b/openstack/cloud/tests/unit/test_domain_params.py similarity index 78% rename from shade/shade/tests/unit/test_domain_params.py rename to openstack/cloud/tests/unit/test_domain_params.py index 62bfcc51f..a687704a0 100644 --- a/shade/shade/tests/unit/test_domain_params.py +++ b/openstack/cloud/tests/unit/test_domain_params.py @@ -14,15 +14,15 @@ import munch -import shade -from shade import exc -from shade.tests.unit import base +import openstack.cloud +from openstack.cloud import exc +from openstack.cloud.tests.unit import base class TestDomainParams(base.TestCase): - @mock.patch.object(shade.OpenStackCloud, '_is_client_version') - @mock.patch.object(shade.OpenStackCloud, 'get_project') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_project') def test_identity_params_v3(self, mock_get_project, mock_is_client_version): mock_get_project.return_value = munch.Munch(id=1234) @@ -34,8 +34,8 @@ def test_identity_params_v3(self, mock_get_project, self.assertIn('domain_id', ret) self.assertEqual(ret['domain_id'], '5678') - @mock.patch.object(shade.OpenStackCloud, '_is_client_version') - @mock.patch.object(shade.OpenStackCloud, 'get_project') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_project') def test_identity_params_v3_no_domain( self, mock_get_project, mock_is_client_version): mock_get_project.return_value = munch.Munch(id=1234) @@ -46,8 +46,8 @@ def test_identity_params_v3_no_domain( self.cloud._get_identity_params, domain_id=None, project='bar') - @mock.patch.object(shade.OpenStackCloud, '_is_client_version') - @mock.patch.object(shade.OpenStackCloud, 'get_project') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_project') def test_identity_params_v2(self, mock_get_project, mock_is_client_version): mock_get_project.return_value = munch.Munch(id=1234) @@ -58,8 +58,8 @@ def test_identity_params_v2(self, mock_get_project, self.assertEqual(ret['tenant_id'], 1234) self.assertNotIn('domain', ret) - @mock.patch.object(shade.OpenStackCloud, '_is_client_version') - @mock.patch.object(shade.OpenStackCloud, 'get_project') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_project') def test_identity_params_v2_no_domain(self, mock_get_project, mock_is_client_version): mock_get_project.return_value = munch.Munch(id=1234) diff --git a/shade/shade/tests/unit/test_domains.py b/openstack/cloud/tests/unit/test_domains.py similarity index 97% rename from shade/shade/tests/unit/test_domains.py rename to openstack/cloud/tests/unit/test_domains.py index b1996c1c5..e15e132e4 100644 --- a/shade/shade/tests/unit/test_domains.py +++ b/openstack/cloud/tests/unit/test_domains.py @@ -18,8 +18,8 @@ import testtools from testtools import matchers -import shade -from shade.tests.unit import base +import openstack.cloud +from openstack.cloud.tests.unit import base class TestDomains(base.RequestsMockTestCase): @@ -94,7 +94,7 @@ def test_create_domain_exception(self): domain_data = self._get_domain_data(domain_name='domain_name', enabled=True) with testtools.ExpectedException( - shade.OpenStackCloudBadRequest, + openstack.cloud.OpenStackCloudBadRequest, "Failed to create domain domain_name" ): self.register_uris([ @@ -149,7 +149,7 @@ def test_delete_domain_exception(self): validate=dict(json={'domain': {'enabled': False}})), dict(method='DELETE', uri=domain_resource_uri, status_code=404)]) with testtools.ExpectedException( - shade.OpenStackCloudURINotFound, + openstack.cloud.OpenStackCloudURINotFound, "Failed to delete domain %s" % domain_data.domain_id ): self.op_cloud.delete_domain(domain_data.domain_id) @@ -203,7 +203,7 @@ def test_update_domain_exception(self): json=domain_data.json_response, validate=dict(json={'domain': {'enabled': False}}))]) with testtools.ExpectedException( - shade.OpenStackCloudHTTPError, + openstack.cloud.OpenStackCloudHTTPError, "Error in updating domain %s" % domain_data.domain_id ): self.op_cloud.delete_domain(domain_data.domain_id) diff --git a/shade/shade/tests/unit/test_endpoints.py b/openstack/cloud/tests/unit/test_endpoints.py similarity index 98% rename from shade/shade/tests/unit/test_endpoints.py rename to openstack/cloud/tests/unit/test_endpoints.py index 3884cad8d..3bd3196f0 100644 --- a/shade/shade/tests/unit/test_endpoints.py +++ b/openstack/cloud/tests/unit/test_endpoints.py @@ -21,9 +21,9 @@ import uuid -from shade.exc import OpenStackCloudException -from shade.exc import OpenStackCloudUnavailableFeature -from shade.tests.unit import base +from openstack.cloud.exc import OpenStackCloudException +from openstack.cloud.exc import OpenStackCloudUnavailableFeature +from openstack.cloud.tests.unit import base from testtools import matchers diff --git a/shade/shade/tests/unit/test_flavors.py b/openstack/cloud/tests/unit/test_flavors.py similarity index 97% rename from shade/shade/tests/unit/test_flavors.py rename to openstack/cloud/tests/unit/test_flavors.py index 5fe7b7abf..7342e65c6 100644 --- a/shade/shade/tests/unit/test_flavors.py +++ b/openstack/cloud/tests/unit/test_flavors.py @@ -11,9 +11,9 @@ # under the License. -import shade -from shade.tests import fakes -from shade.tests.unit import base +import openstack.cloud +from openstack.cloud.tests import fakes +from openstack.cloud.tests.unit import base class TestFlavors(base.RequestsMockTestCase): @@ -78,7 +78,7 @@ def test_delete_flavor_exception(self): endpoint=fakes.FAKE_FLAVOR_LIST, id=fakes.FLAVOR_ID), status_code=503)]) - self.assertRaises(shade.OpenStackCloudException, + self.assertRaises(openstack.cloud.OpenStackCloudException, self.op_cloud.delete_flavor, 'vanilla') def test_list_flavors(self): @@ -153,7 +153,7 @@ def test_get_flavor_by_ram_not_found(self): endpoint=fakes.COMPUTE_ENDPOINT), json={'flavors': []})]) self.assertRaises( - shade.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, self.cloud.get_flavor_by_ram, ram=100) diff --git a/shade/shade/tests/unit/test_floating_ip_common.py b/openstack/cloud/tests/unit/test_floating_ip_common.py similarity index 98% rename from shade/shade/tests/unit/test_floating_ip_common.py rename to openstack/cloud/tests/unit/test_floating_ip_common.py index 638294aef..cf2b55c86 100644 --- a/shade/shade/tests/unit/test_floating_ip_common.py +++ b/openstack/cloud/tests/unit/test_floating_ip_common.py @@ -21,10 +21,10 @@ from mock import patch -from shade import meta -from shade import OpenStackCloud -from shade.tests import fakes -from shade.tests.unit import base +from openstack.cloud import meta +from openstack.cloud import OpenStackCloud +from openstack.cloud.tests import fakes +from openstack.cloud.tests.unit import base class TestFloatingIP(base.TestCase): diff --git a/shade/shade/tests/unit/test_floating_ip_neutron.py b/openstack/cloud/tests/unit/test_floating_ip_neutron.py similarity index 99% rename from shade/shade/tests/unit/test_floating_ip_neutron.py rename to openstack/cloud/tests/unit/test_floating_ip_neutron.py index 9ab437fda..000f0d10a 100644 --- a/shade/shade/tests/unit/test_floating_ip_neutron.py +++ b/openstack/cloud/tests/unit/test_floating_ip_neutron.py @@ -23,9 +23,9 @@ import datetime import munch -from shade import exc -from shade.tests import fakes -from shade.tests.unit import base +from openstack.cloud import exc +from openstack.cloud.tests import fakes +from openstack.cloud.tests.unit import base class TestFloatingIP(base.RequestsMockTestCase): diff --git a/shade/shade/tests/unit/test_floating_ip_nova.py b/openstack/cloud/tests/unit/test_floating_ip_nova.py similarity index 99% rename from shade/shade/tests/unit/test_floating_ip_nova.py rename to openstack/cloud/tests/unit/test_floating_ip_nova.py index 492d142ce..29a93f2a6 100644 --- a/shade/shade/tests/unit/test_floating_ip_nova.py +++ b/openstack/cloud/tests/unit/test_floating_ip_nova.py @@ -19,8 +19,8 @@ Tests Floating IP resource methods for nova-network """ -from shade.tests import fakes -from shade.tests.unit import base +from openstack.cloud.tests import fakes +from openstack.cloud.tests.unit import base def get_fake_has_service(has_service): diff --git a/shade/shade/tests/unit/test_floating_ip_pool.py b/openstack/cloud/tests/unit/test_floating_ip_pool.py similarity index 95% rename from shade/shade/tests/unit/test_floating_ip_pool.py rename to openstack/cloud/tests/unit/test_floating_ip_pool.py index 6ebdd9597..01e757d1a 100644 --- a/shade/shade/tests/unit/test_floating_ip_pool.py +++ b/openstack/cloud/tests/unit/test_floating_ip_pool.py @@ -19,9 +19,9 @@ Test floating IP pool resource (managed by nova) """ -from shade import OpenStackCloudException -from shade.tests.unit import base -from shade.tests import fakes +from openstack.cloud import OpenStackCloudException +from openstack.cloud.tests.unit import base +from openstack.cloud.tests import fakes class TestFloatingIPPool(base.RequestsMockTestCase): diff --git a/shade/shade/tests/unit/test_groups.py b/openstack/cloud/tests/unit/test_groups.py similarity index 98% rename from shade/shade/tests/unit/test_groups.py rename to openstack/cloud/tests/unit/test_groups.py index 63db99723..1e96cd088 100644 --- a/shade/shade/tests/unit/test_groups.py +++ b/openstack/cloud/tests/unit/test_groups.py @@ -11,7 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from shade.tests.unit import base +from openstack.cloud.tests.unit import base class TestGroups(base.RequestsMockTestCase): diff --git a/shade/shade/tests/unit/test_identity_roles.py b/openstack/cloud/tests/unit/test_identity_roles.py similarity index 97% rename from shade/shade/tests/unit/test_identity_roles.py rename to openstack/cloud/tests/unit/test_identity_roles.py index 3ef75ce53..7660982cb 100644 --- a/shade/shade/tests/unit/test_identity_roles.py +++ b/openstack/cloud/tests/unit/test_identity_roles.py @@ -13,8 +13,8 @@ import testtools -import shade -from shade.tests.unit import base +import openstack.cloud +from openstack.cloud.tests.unit import base from testtools import matchers @@ -202,7 +202,7 @@ def test_list_role_assignments_exception(self): status_code=403) ]) with testtools.ExpectedException( - shade.exc.OpenStackCloudHTTPError, + openstack.cloud.exc.OpenStackCloudHTTPError, "Failed to list role assignments" ): self.op_cloud.list_role_assignments() @@ -268,7 +268,7 @@ def test_list_role_assignments_keystone_v2_with_role(self): def test_list_role_assignments_exception_v2(self): self.use_keystone_v2() with testtools.ExpectedException( - shade.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, "Must provide project and user for keystone v2" ): self.op_cloud.list_role_assignments() @@ -277,7 +277,7 @@ def test_list_role_assignments_exception_v2(self): def test_list_role_assignments_exception_v2_no_project(self): self.use_keystone_v2() with testtools.ExpectedException( - shade.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, "Must provide project and user for keystone v2" ): self.op_cloud.list_role_assignments(filters={'user': '12345'}) diff --git a/shade/shade/tests/unit/test_image.py b/openstack/cloud/tests/unit/test_image.py similarity index 87% rename from shade/shade/tests/unit/test_image.py rename to openstack/cloud/tests/unit/test_image.py index 515ca22e5..39a610167 100644 --- a/shade/shade/tests/unit/test_image.py +++ b/openstack/cloud/tests/unit/test_image.py @@ -23,11 +23,11 @@ import munch import six -import shade -from shade import exc -from shade import meta -from shade.tests import fakes -from shade.tests.unit import base +import openstack.cloud +from openstack.cloud import exc +from openstack.cloud import meta +from openstack.cloud.tests import fakes +from openstack.cloud.tests.unit import base CINDER_URL = 'https://volume.example.com/v2/1c36b64c840a42cd9e9b931a369337f0' @@ -233,13 +233,15 @@ def test_create_image_put_v2(self): dict(method='POST', uri='https://image.example.com/v2/images', json=self.fake_image_dict, validate=dict( - json={u'container_format': u'bare', - u'disk_format': u'qcow2', - u'name': u'fake_image', - u'owner_specified.shade.md5': fakes.NO_MD5, - u'owner_specified.shade.object': u'images/fake_image', # noqa - u'owner_specified.shade.sha256': fakes.NO_SHA256, - u'visibility': u'private'}) + json={ + u'container_format': u'bare', + u'disk_format': u'qcow2', + u'name': u'fake_image', + u'owner_specified.openstack.md5': fakes.NO_MD5, + u'owner_specified.openstack.object': + u'images/fake_image', + u'owner_specified.openstack.sha256': fakes.NO_SHA256, + u'visibility': u'private'}) ), dict(method='PUT', uri='https://image.example.com/v2/images/{id}/file'.format( @@ -273,9 +275,9 @@ def test_create_image_task(self): ) image_no_checksums = self.fake_image_dict.copy() - del(image_no_checksums['owner_specified.shade.md5']) - del(image_no_checksums['owner_specified.shade.sha256']) - del(image_no_checksums['owner_specified.shade.object']) + del(image_no_checksums['owner_specified.openstack.md5']) + del(image_no_checksums['owner_specified.openstack.sha256']) + del(image_no_checksums['owner_specified.openstack.object']) self.register_uris([ dict(method='GET', uri='https://image.example.com/v2/images', @@ -346,15 +348,16 @@ def test_create_image_task(self): uri='https://image.example.com/v2/images/{id}'.format( id=self.image_id), validate=dict( - json=sorted([{u'op': u'add', - u'value': '{container}/{object}'.format( - container=container_name, - object=image_name), - u'path': u'/owner_specified.shade.object'}, - {u'op': u'add', u'value': fakes.NO_MD5, - u'path': u'/owner_specified.shade.md5'}, - {u'op': u'add', u'value': fakes.NO_SHA256, - u'path': u'/owner_specified.shade.sha256'}], + json=sorted([ + {u'op': u'add', + u'value': '{container}/{object}'.format( + container=container_name, + object=image_name), + u'path': u'/owner_specified.openstack.object'}, + {u'op': u'add', u'value': fakes.NO_MD5, + u'path': u'/owner_specified.openstack.md5'}, + {u'op': u'add', u'value': fakes.NO_SHA256, + u'path': u'/owner_specified.openstack.sha256'}], key=operator.itemgetter('value')), headers={ 'Content-Type': @@ -384,8 +387,8 @@ def _call_create_image(self, name, **kwargs): name, imagefile.name, wait=True, timeout=1, is_public=False, **kwargs) - @mock.patch.object(shade.OpenStackCloud, '_is_client_version') - @mock.patch.object(shade.OpenStackCloud, '_image_client') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') def test_create_image_put_v1( self, mock_image_client, mock_is_client_version): # TODO(mordred) Fix this to use requests_mock @@ -396,9 +399,9 @@ def test_create_image_put_v1( args = {'name': '42 name', 'container_format': 'bare', 'disk_format': 'qcow2', 'properties': { - 'owner_specified.shade.md5': mock.ANY, - 'owner_specified.shade.sha256': mock.ANY, - 'owner_specified.shade.object': 'images/42 name', + 'owner_specified.openstack.md5': mock.ANY, + 'owner_specified.openstack.sha256': mock.ANY, + 'owner_specified.openstack.object': 'images/42 name', 'is_public': False}} ret = munch.Munch(args.copy()) ret['id'] = '42' @@ -422,8 +425,8 @@ def test_create_image_put_v1( self.assertEqual( self._munch_images(ret), self.cloud.list_images()) - @mock.patch.object(shade.OpenStackCloud, '_is_client_version') - @mock.patch.object(shade.OpenStackCloud, '_image_client') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') def test_create_image_put_v1_bad_delete( self, mock_image_client, mock_is_client_version): mock_is_client_version.return_value = False @@ -433,9 +436,9 @@ def test_create_image_put_v1_bad_delete( args = {'name': '42 name', 'container_format': 'bare', 'disk_format': 'qcow2', 'properties': { - 'owner_specified.shade.md5': mock.ANY, - 'owner_specified.shade.sha256': mock.ANY, - 'owner_specified.shade.object': 'images/42 name', + 'owner_specified.openstack.md5': mock.ANY, + 'owner_specified.openstack.sha256': mock.ANY, + 'owner_specified.openstack.object': 'images/42 name', 'is_public': False}} ret = munch.Munch(args.copy()) ret['id'] = '42' @@ -460,8 +463,8 @@ def test_create_image_put_v1_bad_delete( }) mock_image_client.delete.assert_called_with('/images/42') - @mock.patch.object(shade.OpenStackCloud, '_is_client_version') - @mock.patch.object(shade.OpenStackCloud, '_image_client') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') def test_update_image_no_patch( self, mock_image_client, mock_is_client_version): mock_is_client_version.return_value = True @@ -472,9 +475,9 @@ def test_update_image_no_patch( args = {'name': '42 name', 'container_format': 'bare', 'disk_format': 'qcow2', - 'owner_specified.shade.md5': mock.ANY, - 'owner_specified.shade.sha256': mock.ANY, - 'owner_specified.shade.object': 'images/42 name', + 'owner_specified.openstack.md5': mock.ANY, + 'owner_specified.openstack.sha256': mock.ANY, + 'owner_specified.openstack.object': 'images/42 name', 'visibility': 'private', 'min_disk': 0, 'min_ram': 0} ret = munch.Munch(args.copy()) @@ -487,12 +490,12 @@ def test_update_image_no_patch( ] self.cloud.update_image_properties( image=self._image_dict(ret), - **{'owner_specified.shade.object': 'images/42 name'}) + **{'owner_specified.openstack.object': 'images/42 name'}) mock_image_client.get.assert_called_with('/images', params={}) mock_image_client.patch.assert_not_called() - @mock.patch.object(shade.OpenStackCloud, '_is_client_version') - @mock.patch.object(shade.OpenStackCloud, '_image_client') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') def test_create_image_put_v2_bad_delete( self, mock_image_client, mock_is_client_version): mock_is_client_version.return_value = True @@ -503,9 +506,9 @@ def test_create_image_put_v2_bad_delete( args = {'name': '42 name', 'container_format': 'bare', 'disk_format': 'qcow2', - 'owner_specified.shade.md5': mock.ANY, - 'owner_specified.shade.sha256': mock.ANY, - 'owner_specified.shade.object': 'images/42 name', + 'owner_specified.openstack.md5': mock.ANY, + 'owner_specified.openstack.sha256': mock.ANY, + 'owner_specified.openstack.object': 'images/42 name', 'visibility': 'private', 'min_disk': 0, 'min_ram': 0} ret = munch.Munch(args.copy()) @@ -530,8 +533,8 @@ def test_create_image_put_v2_bad_delete( data=mock.ANY) mock_image_client.delete.assert_called_with('/images/42') - @mock.patch.object(shade.OpenStackCloud, '_is_client_version') - @mock.patch.object(shade.OpenStackCloud, '_image_client') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') def test_create_image_put_bad_int( self, mock_image_client, mock_is_client_version): mock_is_client_version.return_value = True @@ -542,8 +545,8 @@ def test_create_image_put_bad_int( self._call_create_image, '42 name', min_disk='fish', min_ram=0) mock_image_client.post.assert_not_called() - @mock.patch.object(shade.OpenStackCloud, '_is_client_version') - @mock.patch.object(shade.OpenStackCloud, '_image_client') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') def test_create_image_put_user_int( self, mock_image_client, mock_is_client_version): mock_is_client_version.return_value = True @@ -551,9 +554,9 @@ def test_create_image_put_user_int( args = {'name': '42 name', 'container_format': 'bare', 'disk_format': u'qcow2', - 'owner_specified.shade.md5': mock.ANY, - 'owner_specified.shade.sha256': mock.ANY, - 'owner_specified.shade.object': 'images/42 name', + 'owner_specified.openstack.md5': mock.ANY, + 'owner_specified.openstack.sha256': mock.ANY, + 'owner_specified.openstack.object': 'images/42 name', 'int_v': '12345', 'visibility': 'private', 'min_disk': 0, 'min_ram': 0} @@ -577,8 +580,8 @@ def test_create_image_put_user_int( self.assertEqual( self._munch_images(ret), self.cloud.list_images()) - @mock.patch.object(shade.OpenStackCloud, '_is_client_version') - @mock.patch.object(shade.OpenStackCloud, '_image_client') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') def test_create_image_put_meta_int( self, mock_image_client, mock_is_client_version): mock_is_client_version.return_value = True @@ -591,9 +594,9 @@ def test_create_image_put_meta_int( '42 name', min_disk='0', min_ram=0, meta={'int_v': 12345}) args = {'name': '42 name', 'container_format': 'bare', 'disk_format': u'qcow2', - 'owner_specified.shade.md5': mock.ANY, - 'owner_specified.shade.sha256': mock.ANY, - 'owner_specified.shade.object': 'images/42 name', + 'owner_specified.openstack.md5': mock.ANY, + 'owner_specified.openstack.sha256': mock.ANY, + 'owner_specified.openstack.object': 'images/42 name', 'int_v': 12345, 'visibility': 'private', 'min_disk': 0, 'min_ram': 0} @@ -606,8 +609,8 @@ def test_create_image_put_meta_int( self.assertEqual( self._munch_images(ret), self.cloud.list_images()) - @mock.patch.object(shade.OpenStackCloud, '_is_client_version') - @mock.patch.object(shade.OpenStackCloud, '_image_client') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') def test_create_image_put_protected( self, mock_image_client, mock_is_client_version): mock_is_client_version.return_value = True @@ -618,9 +621,9 @@ def test_create_image_put_protected( args = {'name': '42 name', 'container_format': 'bare', 'disk_format': u'qcow2', - 'owner_specified.shade.md5': mock.ANY, - 'owner_specified.shade.sha256': mock.ANY, - 'owner_specified.shade.object': 'images/42 name', + 'owner_specified.openstack.md5': mock.ANY, + 'owner_specified.openstack.sha256': mock.ANY, + 'owner_specified.openstack.object': 'images/42 name', 'protected': False, 'int_v': '12345', 'visibility': 'private', @@ -644,8 +647,8 @@ def test_create_image_put_protected( headers={'Content-Type': 'application/octet-stream'}) self.assertEqual(self._munch_images(ret), self.cloud.list_images()) - @mock.patch.object(shade.OpenStackCloud, '_is_client_version') - @mock.patch.object(shade.OpenStackCloud, '_image_client') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') def test_create_image_put_user_prop( self, mock_image_client, mock_is_client_version): mock_is_client_version.return_value = True @@ -656,9 +659,9 @@ def test_create_image_put_user_prop( args = {'name': '42 name', 'container_format': 'bare', 'disk_format': u'qcow2', - 'owner_specified.shade.md5': mock.ANY, - 'owner_specified.shade.sha256': mock.ANY, - 'owner_specified.shade.object': 'images/42 name', + 'owner_specified.openstack.md5': mock.ANY, + 'owner_specified.openstack.sha256': mock.ANY, + 'owner_specified.openstack.object': 'images/42 name', 'int_v': '12345', 'xenapi_use_agent': 'False', 'visibility': 'private', diff --git a/shade/shade/tests/unit/test_image_snapshot.py b/openstack/cloud/tests/unit/test_image_snapshot.py similarity index 97% rename from shade/shade/tests/unit/test_image_snapshot.py rename to openstack/cloud/tests/unit/test_image_snapshot.py index 1e3a854fa..87da1281f 100644 --- a/shade/shade/tests/unit/test_image_snapshot.py +++ b/openstack/cloud/tests/unit/test_image_snapshot.py @@ -14,9 +14,9 @@ import uuid -from shade import exc -from shade.tests import fakes -from shade.tests.unit import base +from openstack.cloud import exc +from openstack.cloud.tests import fakes +from openstack.cloud.tests.unit import base class TestImageSnapshot(base.RequestsMockTestCase): diff --git a/shade/shade/tests/unit/test_inventory.py b/openstack/cloud/tests/unit/test_inventory.py similarity index 90% rename from shade/shade/tests/unit/test_inventory.py rename to openstack/cloud/tests/unit/test_inventory.py index 84846b231..aaac355d2 100644 --- a/shade/shade/tests/unit/test_inventory.py +++ b/openstack/cloud/tests/unit/test_inventory.py @@ -12,14 +12,14 @@ import mock -import os_client_config +import openstack.config as os_client_config from os_client_config import exceptions as occ_exc -from shade import exc -from shade import inventory -from shade.tests import fakes -from shade.tests.unit import base +from openstack.cloud import exc +from openstack.cloud import inventory +from openstack.cloud.tests import fakes +from openstack.cloud.tests.unit import base class TestInventory(base.TestCase): @@ -28,7 +28,7 @@ def setUp(self): super(TestInventory, self).setUp() @mock.patch("os_client_config.config.OpenStackConfig") - @mock.patch("shade.OpenStackCloud") + @mock.patch("openstack.cloud.OpenStackCloud") def test__init(self, mock_cloud, mock_config): mock_config.return_value.get_all_clouds.return_value = [{}] @@ -42,7 +42,7 @@ def test__init(self, mock_cloud, mock_config): self.assertTrue(mock_config.return_value.get_all_clouds.called) @mock.patch("os_client_config.config.OpenStackConfig") - @mock.patch("shade.OpenStackCloud") + @mock.patch("openstack.cloud.OpenStackCloud") def test__init_one_cloud(self, mock_cloud, mock_config): mock_config.return_value.get_one_cloud.return_value = [{}] @@ -58,7 +58,7 @@ def test__init_one_cloud(self, mock_cloud, mock_config): 'supercloud') @mock.patch("os_client_config.config.OpenStackConfig") - @mock.patch("shade.OpenStackCloud") + @mock.patch("openstack.cloud.OpenStackCloud") def test__raise_exception_on_no_cloud(self, mock_cloud, mock_config): """ Test that when os-client-config can't find a named cloud, a @@ -74,7 +74,7 @@ def test__raise_exception_on_no_cloud(self, mock_cloud, mock_config): 'supercloud') @mock.patch("os_client_config.config.OpenStackConfig") - @mock.patch("shade.OpenStackCloud") + @mock.patch("openstack.cloud.OpenStackCloud") def test_list_hosts(self, mock_cloud, mock_config): mock_config.return_value.get_all_clouds.return_value = [{}] @@ -93,7 +93,7 @@ def test_list_hosts(self, mock_cloud, mock_config): self.assertEqual([server], ret) @mock.patch("os_client_config.config.OpenStackConfig") - @mock.patch("shade.OpenStackCloud") + @mock.patch("openstack.cloud.OpenStackCloud") def test_list_hosts_no_detail(self, mock_cloud, mock_config): mock_config.return_value.get_all_clouds.return_value = [{}] @@ -112,7 +112,7 @@ def test_list_hosts_no_detail(self, mock_cloud, mock_config): self.assertFalse(inv.clouds[0].get_openstack_vars.called) @mock.patch("os_client_config.config.OpenStackConfig") - @mock.patch("shade.OpenStackCloud") + @mock.patch("openstack.cloud.OpenStackCloud") def test_search_hosts(self, mock_cloud, mock_config): mock_config.return_value.get_all_clouds.return_value = [{}] @@ -128,7 +128,7 @@ def test_search_hosts(self, mock_cloud, mock_config): self.assertEqual([server], ret) @mock.patch("os_client_config.config.OpenStackConfig") - @mock.patch("shade.OpenStackCloud") + @mock.patch("openstack.cloud.OpenStackCloud") def test_get_host(self, mock_cloud, mock_config): mock_config.return_value.get_all_clouds.return_value = [{}] diff --git a/shade/shade/tests/unit/test_keypair.py b/openstack/cloud/tests/unit/test_keypair.py similarity index 96% rename from shade/shade/tests/unit/test_keypair.py rename to openstack/cloud/tests/unit/test_keypair.py index 2fa3e3ae1..f1c288938 100644 --- a/shade/shade/tests/unit/test_keypair.py +++ b/openstack/cloud/tests/unit/test_keypair.py @@ -12,9 +12,9 @@ # limitations under the License. -from shade import exc -from shade.tests import fakes -from shade.tests.unit import base +from openstack.cloud import exc +from openstack.cloud.tests import fakes +from openstack.cloud.tests.unit import base class TestKeypair(base.RequestsMockTestCase): diff --git a/shade/shade/tests/unit/test_limits.py b/openstack/cloud/tests/unit/test_limits.py similarity index 98% rename from shade/shade/tests/unit/test_limits.py rename to openstack/cloud/tests/unit/test_limits.py index 3eeb9fc0f..0db11e58c 100644 --- a/shade/shade/tests/unit/test_limits.py +++ b/openstack/cloud/tests/unit/test_limits.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from shade.tests.unit import base +from openstack.cloud.tests.unit import base class TestLimits(base.RequestsMockTestCase): diff --git a/shade/shade/tests/unit/test_magnum_services.py b/openstack/cloud/tests/unit/test_magnum_services.py similarity index 96% rename from shade/shade/tests/unit/test_magnum_services.py rename to openstack/cloud/tests/unit/test_magnum_services.py index 24499f1fb..83153f365 100644 --- a/shade/shade/tests/unit/test_magnum_services.py +++ b/openstack/cloud/tests/unit/test_magnum_services.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from shade.tests.unit import base +from openstack.cloud.tests.unit import base magnum_service_obj = dict( diff --git a/shade/shade/tests/unit/test_meta.py b/openstack/cloud/tests/unit/test_meta.py similarity index 95% rename from shade/shade/tests/unit/test_meta.py rename to openstack/cloud/tests/unit/test_meta.py index ae76ce877..b32d3518f 100644 --- a/shade/shade/tests/unit/test_meta.py +++ b/openstack/cloud/tests/unit/test_meta.py @@ -14,10 +14,10 @@ import mock -import shade -from shade import meta -from shade.tests import fakes -from shade.tests.unit import base +import openstack.cloud +from openstack.cloud import meta +from openstack.cloud.tests import fakes +from openstack.cloud.tests.unit import base PRIVATE_V4 = '198.51.100.3' PUBLIC_V4 = '192.0.2.99' @@ -333,10 +333,10 @@ def test_get_server_multiple_private_ip(self): '10.0.0.101', meta.get_server_private_ip(srv, self.cloud)) self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'has_service') - @mock.patch.object(shade.OpenStackCloud, 'get_volumes') - @mock.patch.object(shade.OpenStackCloud, 'get_image_name') - @mock.patch.object(shade.OpenStackCloud, 'get_flavor_name') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'has_service') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_volumes') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_image_name') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_flavor_name') def test_get_server_private_ip_devstack( self, mock_get_flavor_name, mock_get_image_name, @@ -398,9 +398,9 @@ def test_get_server_private_ip_devstack( self.assertEqual(PRIVATE_V4, srv['private_v4']) self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'get_volumes') - @mock.patch.object(shade.OpenStackCloud, 'get_image_name') - @mock.patch.object(shade.OpenStackCloud, 'get_flavor_name') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_volumes') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_image_name') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_flavor_name') def test_get_server_private_ip_no_fip( self, mock_get_flavor_name, mock_get_image_name, @@ -448,9 +448,9 @@ def test_get_server_private_ip_no_fip( self.assertEqual(PRIVATE_V4, srv['private_v4']) self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'get_volumes') - @mock.patch.object(shade.OpenStackCloud, 'get_image_name') - @mock.patch.object(shade.OpenStackCloud, 'get_flavor_name') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_volumes') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_image_name') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_flavor_name') def test_get_server_cloud_no_fips( self, mock_get_flavor_name, mock_get_image_name, @@ -496,10 +496,10 @@ def test_get_server_cloud_no_fips( self.assertEqual(PRIVATE_V4, srv['private_v4']) self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'has_service') - @mock.patch.object(shade.OpenStackCloud, 'get_volumes') - @mock.patch.object(shade.OpenStackCloud, 'get_image_name') - @mock.patch.object(shade.OpenStackCloud, 'get_flavor_name') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'has_service') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_volumes') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_image_name') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_flavor_name') def test_get_server_cloud_missing_fips( self, mock_get_flavor_name, mock_get_image_name, @@ -565,9 +565,9 @@ def test_get_server_cloud_missing_fips( self.assertEqual(PUBLIC_V4, srv['public_v4']) self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'get_volumes') - @mock.patch.object(shade.OpenStackCloud, 'get_image_name') - @mock.patch.object(shade.OpenStackCloud, 'get_flavor_name') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_volumes') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_image_name') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_flavor_name') def test_get_server_cloud_rackspace_v6( self, mock_get_flavor_name, mock_get_image_name, mock_get_volumes): @@ -615,9 +615,9 @@ def test_get_server_cloud_rackspace_v6( "2001:4800:7819:103:be76:4eff:fe05:8525", srv['interface_ip']) self.assert_calls() - @mock.patch.object(shade.OpenStackCloud, 'get_volumes') - @mock.patch.object(shade.OpenStackCloud, 'get_image_name') - @mock.patch.object(shade.OpenStackCloud, 'get_flavor_name') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_volumes') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_image_name') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_flavor_name') def test_get_server_cloud_osic_split( self, mock_get_flavor_name, mock_get_image_name, mock_get_volumes): @@ -899,8 +899,8 @@ def test_get_security_groups(self, self.assertEqual('testgroup', hostvars['security_groups'][0]['name']) - @mock.patch.object(shade.meta, 'get_server_external_ipv6') - @mock.patch.object(shade.meta, 'get_server_external_ipv4') + @mock.patch.object(openstack.cloud.meta, 'get_server_external_ipv6') + @mock.patch.object(openstack.cloud.meta, 'get_server_external_ipv4') def test_basic_hostvars( self, mock_get_server_external_ipv4, mock_get_server_external_ipv6): @@ -933,8 +933,8 @@ def test_basic_hostvars( # test volume exception self.assertEqual([], hostvars['volumes']) - @mock.patch.object(shade.meta, 'get_server_external_ipv6') - @mock.patch.object(shade.meta, 'get_server_external_ipv4') + @mock.patch.object(openstack.cloud.meta, 'get_server_external_ipv6') + @mock.patch.object(openstack.cloud.meta, 'get_server_external_ipv4') def test_ipv4_hostvars( self, mock_get_server_external_ipv4, mock_get_server_external_ipv6): @@ -947,7 +947,7 @@ def test_ipv4_hostvars( fake_cloud, meta.obj_to_munch(standard_fake_server)) self.assertEqual(PUBLIC_V4, hostvars['interface_ip']) - @mock.patch.object(shade.meta, 'get_server_external_ipv4') + @mock.patch.object(openstack.cloud.meta, 'get_server_external_ipv4') def test_private_interface_ip(self, mock_get_server_external_ipv4): mock_get_server_external_ipv4.return_value = PUBLIC_V4 @@ -957,7 +957,7 @@ def test_private_interface_ip(self, mock_get_server_external_ipv4): cloud, meta.obj_to_munch(standard_fake_server)) self.assertEqual(PRIVATE_V4, hostvars['interface_ip']) - @mock.patch.object(shade.meta, 'get_server_external_ipv4') + @mock.patch.object(openstack.cloud.meta, 'get_server_external_ipv4') def test_image_string(self, mock_get_server_external_ipv4): mock_get_server_external_ipv4.return_value = PUBLIC_V4 diff --git a/shade/shade/tests/unit/test_network.py b/openstack/cloud/tests/unit/test_network.py similarity index 98% rename from shade/shade/tests/unit/test_network.py rename to openstack/cloud/tests/unit/test_network.py index 96af721bf..922a2c9b4 100644 --- a/shade/shade/tests/unit/test_network.py +++ b/openstack/cloud/tests/unit/test_network.py @@ -13,8 +13,8 @@ import copy import testtools -import shade -from shade.tests.unit import base +import openstack.cloud +from openstack.cloud.tests.unit import base class TestNetwork(base.RequestsMockTestCase): @@ -183,7 +183,7 @@ def test_create_network_provider_ignored_value(self): def test_create_network_provider_wrong_type(self): provider_opts = "invalid" with testtools.ExpectedException( - shade.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, "Parameter 'provider' must be a dict" ): self.cloud.create_network("netname", provider=provider_opts) @@ -231,7 +231,7 @@ def test_delete_network_exception(self): append=['v2.0', 'networks', "%s.json" % network_id]), status_code=503) ]) - self.assertRaises(shade.OpenStackCloudException, + self.assertRaises(openstack.cloud.OpenStackCloudException, self.cloud.delete_network, network_name) self.assert_calls() diff --git a/shade/shade/tests/unit/test_normalize.py b/openstack/cloud/tests/unit/test_normalize.py similarity index 99% rename from shade/shade/tests/unit/test_normalize.py rename to openstack/cloud/tests/unit/test_normalize.py index 0be594022..f0e37f8e3 100644 --- a/shade/shade/tests/unit/test_normalize.py +++ b/openstack/cloud/tests/unit/test_normalize.py @@ -12,7 +12,7 @@ import mock -from shade.tests.unit import base +from openstack.cloud.tests.unit import base RAW_SERVER_DICT = { 'HUMAN_ID': True, diff --git a/shade/shade/tests/unit/test_object.py b/openstack/cloud/tests/unit/test_object.py similarity index 97% rename from shade/shade/tests/unit/test_object.py rename to openstack/cloud/tests/unit/test_object.py index 54fb65b6e..d627c2dbb 100644 --- a/shade/shade/tests/unit/test_object.py +++ b/openstack/cloud/tests/unit/test_object.py @@ -16,10 +16,10 @@ import testtools -import shade -import shade.openstackcloud -from shade import exc -from shade.tests.unit import base +import openstack.cloud +import openstack.cloud.openstackcloud as oc_oc +from openstack.cloud import exc +from openstack.cloud.tests.unit import base class BaseTestObject(base.RequestsMockTestCase): @@ -82,7 +82,7 @@ def test_create_container_public(self): validate=dict( headers={ 'x-container-read': - shade.openstackcloud.OBJECT_CONTAINER_ACLS[ + oc_oc.OBJECT_CONTAINER_ACLS[ 'public']})), dict(method='HEAD', uri=self.container_endpoint, headers={ @@ -144,14 +144,14 @@ def test_delete_container_error(self): dict(method='DELETE', uri=self.container_endpoint, status_code=409)]) self.assertRaises( - shade.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, self.cloud.delete_container, self.container) self.assert_calls() def test_update_container(self): headers = { 'x-container-read': - shade.openstackcloud.OBJECT_CONTAINER_ACLS['public']} + oc_oc.OBJECT_CONTAINER_ACLS['public']} self.register_uris([ dict(method='POST', uri=self.container_endpoint, status_code=204, @@ -171,7 +171,7 @@ def test_update_container_error(self): dict(method='POST', uri=self.container_endpoint, status_code=409)]) self.assertRaises( - shade.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, self.cloud.update_container, self.container, dict(foo='bar')) self.assert_calls() @@ -182,7 +182,7 @@ def test_set_container_access_public(self): validate=dict( headers={ 'x-container-read': - shade.openstackcloud.OBJECT_CONTAINER_ACLS[ + oc_oc.OBJECT_CONTAINER_ACLS[ 'public']}))]) self.cloud.set_container_access(self.container, 'public') @@ -196,7 +196,7 @@ def test_set_container_access_private(self): validate=dict( headers={ 'x-container-read': - shade.openstackcloud.OBJECT_CONTAINER_ACLS[ + oc_oc.OBJECT_CONTAINER_ACLS[ 'private']}))]) self.cloud.set_container_access(self.container, 'private') @@ -205,7 +205,7 @@ def test_set_container_access_private(self): def test_set_container_access_invalid(self): self.assertRaises( - shade.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, self.cloud.set_container_access, self.container, 'invalid') def test_get_container_access(self): @@ -213,7 +213,7 @@ def test_get_container_access(self): dict(method='HEAD', uri=self.container_endpoint, headers={ 'x-container-read': - str(shade.openstackcloud.OBJECT_CONTAINER_ACLS[ + str(oc_oc.OBJECT_CONTAINER_ACLS[ 'public'])})]) access = self.cloud.get_container_access(self.container) self.assertEqual('public', access) @@ -360,7 +360,7 @@ def test_get_object_exception(self): status_code=416)]) self.assertRaises( - shade.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, self.cloud.get_object, self.container, self.object) @@ -385,7 +385,7 @@ def test_get_object_segment_size_http_404(self): self.register_uris([ dict(method='GET', uri='https://object-store.example.com/info', status_code=404, reason='Not Found')]) - self.assertEqual(shade.openstackcloud.DEFAULT_OBJECT_SEGMENT_SIZE, + self.assertEqual(oc_oc.DEFAULT_OBJECT_SEGMENT_SIZE, self.cloud.get_object_segment_size(None)) self.assert_calls() @@ -393,8 +393,9 @@ def test_get_object_segment_size_http_412(self): self.register_uris([ dict(method='GET', uri='https://object-store.example.com/info', status_code=412, reason='Precondition failed')]) - self.assertEqual(shade.openstackcloud.DEFAULT_OBJECT_SEGMENT_SIZE, - self.cloud.get_object_segment_size(None)) + self.assertEqual( + oc_oc.DEFAULT_OBJECT_SEGMENT_SIZE, + self.cloud.get_object_segment_size(None)) self.assert_calls() diff --git a/shade/shade/tests/unit/test_operator_noauth.py b/openstack/cloud/tests/unit/test_operator_noauth.py similarity index 93% rename from shade/shade/tests/unit/test_operator_noauth.py rename to openstack/cloud/tests/unit/test_operator_noauth.py index e489d2e84..fcc4bd0c2 100644 --- a/shade/shade/tests/unit/test_operator_noauth.py +++ b/openstack/cloud/tests/unit/test_operator_noauth.py @@ -14,8 +14,8 @@ from keystoneauth1 import plugin -import shade -from shade.tests.unit import base +import openstack.cloud +from openstack.cloud.tests.unit import base class TestShadeOperatorNoAuth(base.RequestsMockTestCase): @@ -50,7 +50,7 @@ def test_ironic_noauth_none_auth_type(self): The new way of doing this is with the keystoneauth none plugin. """ - self.cloud_noauth = shade.operator_cloud( + self.cloud_noauth = openstack.cloud.operator_cloud( auth_type='none', baremetal_endpoint_override="https://bare-metal.example.com") @@ -63,7 +63,7 @@ def test_ironic_noauth_admin_token_auth_type(self): The old way of doing this was to abuse admin_token. """ - self.cloud_noauth = shade.operator_cloud( + self.cloud_noauth = openstack.cloud.operator_cloud( auth_type='admin_token', auth=dict( endpoint='https://bare-metal.example.com', diff --git a/shade/shade/tests/unit/test_port.py b/openstack/cloud/tests/unit/test_port.py similarity index 99% rename from shade/shade/tests/unit/test_port.py rename to openstack/cloud/tests/unit/test_port.py index 4ef52a2e3..b98571b83 100644 --- a/shade/shade/tests/unit/test_port.py +++ b/openstack/cloud/tests/unit/test_port.py @@ -19,8 +19,8 @@ Test port resource (managed by neutron) """ -from shade.exc import OpenStackCloudException -from shade.tests.unit import base +from openstack.cloud.exc import OpenStackCloudException +from openstack.cloud.tests.unit import base class TestPort(base.RequestsMockTestCase): diff --git a/shade/shade/tests/unit/test_project.py b/openstack/cloud/tests/unit/test_project.py similarity index 98% rename from shade/shade/tests/unit/test_project.py rename to openstack/cloud/tests/unit/test_project.py index f836bc0d2..fc9ec19b3 100644 --- a/shade/shade/tests/unit/test_project.py +++ b/openstack/cloud/tests/unit/test_project.py @@ -13,9 +13,9 @@ import testtools from testtools import matchers -import shade -import shade._utils -from shade.tests.unit import base +import openstack.cloud +import openstack.cloud._utils +from openstack.cloud.tests.unit import base class TestProject(base.RequestsMockTestCase): @@ -76,7 +76,7 @@ def test_create_project_v3(self,): def test_create_project_v3_no_domain(self): with testtools.ExpectedException( - shade.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, "User or project creation requires an explicit" " domain_id argument." ): @@ -126,7 +126,7 @@ def test_update_project_not_found(self): # shade will raise an attribute error instead of the proper # project not found exception. with testtools.ExpectedException( - shade.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, "Project %s not found." % project_data.project_id ): self.op_cloud.update_project(project_data.project_id) diff --git a/shade/shade/tests/unit/test_qos_bandwidth_limit_rule.py b/openstack/cloud/tests/unit/test_qos_bandwidth_limit_rule.py similarity index 99% rename from shade/shade/tests/unit/test_qos_bandwidth_limit_rule.py rename to openstack/cloud/tests/unit/test_qos_bandwidth_limit_rule.py index 53858496e..a09a618cb 100644 --- a/shade/shade/tests/unit/test_qos_bandwidth_limit_rule.py +++ b/openstack/cloud/tests/unit/test_qos_bandwidth_limit_rule.py @@ -15,8 +15,8 @@ import copy -from shade import exc -from shade.tests.unit import base +from openstack.cloud import exc +from openstack.cloud.tests.unit import base class TestQosBandwidthLimitRule(base.RequestsMockTestCase): diff --git a/shade/shade/tests/unit/test_qos_dscp_marking_rule.py b/openstack/cloud/tests/unit/test_qos_dscp_marking_rule.py similarity index 99% rename from shade/shade/tests/unit/test_qos_dscp_marking_rule.py rename to openstack/cloud/tests/unit/test_qos_dscp_marking_rule.py index ccb60e15f..56234cdc6 100644 --- a/shade/shade/tests/unit/test_qos_dscp_marking_rule.py +++ b/openstack/cloud/tests/unit/test_qos_dscp_marking_rule.py @@ -15,8 +15,8 @@ import copy -from shade import exc -from shade.tests.unit import base +from openstack.cloud import exc +from openstack.cloud.tests.unit import base class TestQosDscpMarkingRule(base.RequestsMockTestCase): diff --git a/shade/shade/tests/unit/test_qos_minimum_bandwidth_rule.py b/openstack/cloud/tests/unit/test_qos_minimum_bandwidth_rule.py similarity index 99% rename from shade/shade/tests/unit/test_qos_minimum_bandwidth_rule.py rename to openstack/cloud/tests/unit/test_qos_minimum_bandwidth_rule.py index 6afdc44bb..0b1c220fb 100644 --- a/shade/shade/tests/unit/test_qos_minimum_bandwidth_rule.py +++ b/openstack/cloud/tests/unit/test_qos_minimum_bandwidth_rule.py @@ -15,8 +15,8 @@ import copy -from shade import exc -from shade.tests.unit import base +from openstack.cloud import exc +from openstack.cloud.tests.unit import base class TestQosMinimumBandwidthRule(base.RequestsMockTestCase): diff --git a/shade/shade/tests/unit/test_qos_policy.py b/openstack/cloud/tests/unit/test_qos_policy.py similarity index 99% rename from shade/shade/tests/unit/test_qos_policy.py rename to openstack/cloud/tests/unit/test_qos_policy.py index a0b327626..3998f7c6b 100644 --- a/shade/shade/tests/unit/test_qos_policy.py +++ b/openstack/cloud/tests/unit/test_qos_policy.py @@ -15,8 +15,8 @@ import copy -from shade import exc -from shade.tests.unit import base +from openstack.cloud import exc +from openstack.cloud.tests.unit import base class TestQosPolicy(base.RequestsMockTestCase): diff --git a/shade/shade/tests/unit/test_qos_rule_type.py b/openstack/cloud/tests/unit/test_qos_rule_type.py similarity index 98% rename from shade/shade/tests/unit/test_qos_rule_type.py rename to openstack/cloud/tests/unit/test_qos_rule_type.py index fd23e411c..6590788c8 100644 --- a/shade/shade/tests/unit/test_qos_rule_type.py +++ b/openstack/cloud/tests/unit/test_qos_rule_type.py @@ -13,8 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from shade import exc -from shade.tests.unit import base +from openstack.cloud import exc +from openstack.cloud.tests.unit import base class TestQosRuleType(base.RequestsMockTestCase): diff --git a/shade/shade/tests/unit/test_quotas.py b/openstack/cloud/tests/unit/test_quotas.py similarity index 99% rename from shade/shade/tests/unit/test_quotas.py rename to openstack/cloud/tests/unit/test_quotas.py index 12efecbd3..a6c29ea6c 100644 --- a/shade/shade/tests/unit/test_quotas.py +++ b/openstack/cloud/tests/unit/test_quotas.py @@ -10,8 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from shade import exc -from shade.tests.unit import base +from openstack.cloud import exc +from openstack.cloud.tests.unit import base fake_quota_set = { "cores": 20, diff --git a/shade/shade/tests/unit/test_rebuild_server.py b/openstack/cloud/tests/unit/test_rebuild_server.py similarity index 98% rename from shade/shade/tests/unit/test_rebuild_server.py rename to openstack/cloud/tests/unit/test_rebuild_server.py index aae725245..c54701d77 100644 --- a/shade/shade/tests/unit/test_rebuild_server.py +++ b/openstack/cloud/tests/unit/test_rebuild_server.py @@ -21,9 +21,9 @@ import uuid -from shade import exc -from shade.tests import fakes -from shade.tests.unit import base +from openstack.cloud import exc +from openstack.cloud.tests import fakes +from openstack.cloud.tests.unit import base class TestRebuildServer(base.RequestsMockTestCase): diff --git a/shade/shade/tests/unit/test_recordset.py b/openstack/cloud/tests/unit/test_recordset.py similarity index 98% rename from shade/shade/tests/unit/test_recordset.py rename to openstack/cloud/tests/unit/test_recordset.py index 044499aa5..055f0de3d 100644 --- a/shade/shade/tests/unit/test_recordset.py +++ b/openstack/cloud/tests/unit/test_recordset.py @@ -14,8 +14,8 @@ import copy import testtools -import shade -from shade.tests.unit import base +import openstack.cloud +from openstack.cloud.tests.unit import base zone = { @@ -95,7 +95,7 @@ def test_create_recordset_exception(self): 'type': 'A'})), ]) with testtools.ExpectedException( - shade.exc.OpenStackCloudHTTPError, + openstack.cloud.exc.OpenStackCloudHTTPError, "Error creating recordset www2.example.net." ): self.cloud.create_recordset('1', 'www2.example.net.', diff --git a/shade/shade/tests/unit/test_role_assignment.py b/openstack/cloud/tests/unit/test_role_assignment.py similarity index 99% rename from shade/shade/tests/unit/test_role_assignment.py rename to openstack/cloud/tests/unit/test_role_assignment.py index 3aedd0fd9..257f4b288 100644 --- a/shade/shade/tests/unit/test_role_assignment.py +++ b/openstack/cloud/tests/unit/test_role_assignment.py @@ -11,8 +11,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from shade import exc -from shade.tests.unit import base +from openstack.cloud import exc +from openstack.cloud.tests.unit import base import testtools from testtools import matchers diff --git a/shade/shade/tests/unit/test_router.py b/openstack/cloud/tests/unit/test_router.py similarity index 99% rename from shade/shade/tests/unit/test_router.py rename to openstack/cloud/tests/unit/test_router.py index 778d0dadc..bbc266235 100644 --- a/shade/shade/tests/unit/test_router.py +++ b/openstack/cloud/tests/unit/test_router.py @@ -15,8 +15,8 @@ import copy -from shade import exc -from shade.tests.unit import base +from openstack.cloud import exc +from openstack.cloud.tests.unit import base class TestRouter(base.RequestsMockTestCase): diff --git a/shade/shade/tests/unit/test_security_groups.py b/openstack/cloud/tests/unit/test_security_groups.py similarity index 98% rename from shade/shade/tests/unit/test_security_groups.py rename to openstack/cloud/tests/unit/test_security_groups.py index e3485c8fa..64357575b 100644 --- a/shade/shade/tests/unit/test_security_groups.py +++ b/openstack/cloud/tests/unit/test_security_groups.py @@ -13,9 +13,9 @@ import copy -import shade -from shade.tests.unit import base -from shade.tests import fakes +import openstack.cloud +from openstack.cloud.tests.unit import base +from openstack.cloud.tests import fakes # TODO(mordred): Move id and name to using a getUniqueString() value @@ -83,7 +83,7 @@ def test_list_security_groups_none(self): self.cloud.secgroup_source = None self.has_neutron = False - self.assertRaises(shade.OpenStackCloudUnavailableFeature, + self.assertRaises(openstack.cloud.OpenStackCloudUnavailableFeature, self.cloud.list_security_groups) def test_delete_security_group_neutron(self): @@ -146,7 +146,7 @@ def test_delete_security_group_nova_not_found(self): def test_delete_security_group_none(self): self.cloud.secgroup_source = None - self.assertRaises(shade.OpenStackCloudUnavailableFeature, + self.assertRaises(openstack.cloud.OpenStackCloudUnavailableFeature, self.cloud.delete_security_group, 'doesNotExist') @@ -246,7 +246,7 @@ def test_create_security_group_nova(self): def test_create_security_group_none(self): self.cloud.secgroup_source = None self.has_neutron = False - self.assertRaises(shade.OpenStackCloudUnavailableFeature, + self.assertRaises(openstack.cloud.OpenStackCloudUnavailableFeature, self.cloud.create_security_group, '', '') @@ -465,7 +465,7 @@ def test_create_security_group_rule_nova_no_ports(self): def test_create_security_group_rule_none(self): self.has_neutron = False self.cloud.secgroup_source = None - self.assertRaises(shade.OpenStackCloudUnavailableFeature, + self.assertRaises(openstack.cloud.OpenStackCloudUnavailableFeature, self.cloud.create_security_group_rule, '') @@ -498,7 +498,7 @@ def test_delete_security_group_rule_nova(self): def test_delete_security_group_rule_none(self): self.has_neutron = False self.cloud.secgroup_source = None - self.assertRaises(shade.OpenStackCloudUnavailableFeature, + self.assertRaises(openstack.cloud.OpenStackCloudUnavailableFeature, self.cloud.delete_security_group_rule, '') @@ -538,7 +538,7 @@ def test_nova_egress_security_group_rule(self): endpoint=fakes.COMPUTE_ENDPOINT), json={'security_groups': [nova_grp_dict]}), ]) - self.assertRaises(shade.OpenStackCloudException, + self.assertRaises(openstack.cloud.OpenStackCloudException, self.cloud.create_security_group_rule, secgroup_name_or_id='nova-sec-group', direction='egress') diff --git a/shade/shade/tests/unit/test_server_console.py b/openstack/cloud/tests/unit/test_server_console.py similarity index 97% rename from shade/shade/tests/unit/test_server_console.py rename to openstack/cloud/tests/unit/test_server_console.py index bf534aa2c..5b8e78a11 100644 --- a/shade/shade/tests/unit/test_server_console.py +++ b/openstack/cloud/tests/unit/test_server_console.py @@ -13,8 +13,8 @@ import uuid -from shade.tests.unit import base -from shade.tests import fakes +from openstack.cloud.tests.unit import base +from openstack.cloud.tests import fakes class TestServerConsole(base.RequestsMockTestCase): diff --git a/shade/shade/tests/unit/test_server_delete_metadata.py b/openstack/cloud/tests/unit/test_server_delete_metadata.py similarity index 94% rename from shade/shade/tests/unit/test_server_delete_metadata.py rename to openstack/cloud/tests/unit/test_server_delete_metadata.py index 29fea9a8a..e9264432a 100644 --- a/shade/shade/tests/unit/test_server_delete_metadata.py +++ b/openstack/cloud/tests/unit/test_server_delete_metadata.py @@ -19,9 +19,9 @@ import uuid -from shade.exc import OpenStackCloudURINotFound -from shade.tests import fakes -from shade.tests.unit import base +from openstack.cloud.exc import OpenStackCloudURINotFound +from openstack.cloud.tests import fakes +from openstack.cloud.tests.unit import base class TestServerDeleteMetadata(base.RequestsMockTestCase): diff --git a/shade/shade/tests/unit/test_server_group.py b/openstack/cloud/tests/unit/test_server_group.py similarity index 96% rename from shade/shade/tests/unit/test_server_group.py rename to openstack/cloud/tests/unit/test_server_group.py index 7e2ee2b1f..943739ffd 100644 --- a/shade/shade/tests/unit/test_server_group.py +++ b/openstack/cloud/tests/unit/test_server_group.py @@ -13,8 +13,8 @@ import uuid -from shade.tests.unit import base -from shade.tests import fakes +from openstack.cloud.tests.unit import base +from openstack.cloud.tests import fakes class TestServerGroup(base.RequestsMockTestCase): diff --git a/shade/shade/tests/unit/test_server_set_metadata.py b/openstack/cloud/tests/unit/test_server_set_metadata.py similarity index 94% rename from shade/shade/tests/unit/test_server_set_metadata.py rename to openstack/cloud/tests/unit/test_server_set_metadata.py index 0b32ae6e8..c3a507821 100644 --- a/shade/shade/tests/unit/test_server_set_metadata.py +++ b/openstack/cloud/tests/unit/test_server_set_metadata.py @@ -19,9 +19,9 @@ import uuid -from shade.exc import OpenStackCloudBadRequest -from shade.tests import fakes -from shade.tests.unit import base +from openstack.cloud.exc import OpenStackCloudBadRequest +from openstack.cloud.tests import fakes +from openstack.cloud.tests.unit import base class TestServerSetMetadata(base.RequestsMockTestCase): diff --git a/shade/shade/tests/unit/test_services.py b/openstack/cloud/tests/unit/test_services.py similarity index 98% rename from shade/shade/tests/unit/test_services.py rename to openstack/cloud/tests/unit/test_services.py index 6ff05a40c..df261d444 100644 --- a/shade/shade/tests/unit/test_services.py +++ b/openstack/cloud/tests/unit/test_services.py @@ -19,9 +19,9 @@ Tests Keystone services commands. """ -from shade import OpenStackCloudException -from shade.exc import OpenStackCloudUnavailableFeature -from shade.tests.unit import base +from openstack.cloud import OpenStackCloudException +from openstack.cloud.exc import OpenStackCloudUnavailableFeature +from openstack.cloud.tests.unit import base from testtools import matchers diff --git a/shade/shade/tests/unit/test_shade.py b/openstack/cloud/tests/unit/test_shade.py similarity index 97% rename from shade/shade/tests/unit/test_shade.py rename to openstack/cloud/tests/unit/test_shade.py index 65e398de4..3a6988e6a 100644 --- a/shade/shade/tests/unit/test_shade.py +++ b/openstack/cloud/tests/unit/test_shade.py @@ -15,11 +15,11 @@ import testtools -import shade -from shade import _utils -from shade import exc -from shade.tests import fakes -from shade.tests.unit import base +import openstack.cloud +from openstack.cloud import _utils +from openstack.cloud import exc +from openstack.cloud.tests import fakes +from openstack.cloud.tests.unit import base RANGE_DATA = [ @@ -52,9 +52,9 @@ def fake_has_service(*args, **kwargs): self.cloud.has_service = fake_has_service def test_openstack_cloud(self): - self.assertIsInstance(self.cloud, shade.OpenStackCloud) + self.assertIsInstance(self.cloud, openstack.cloud.OpenStackCloud) - @mock.patch.object(shade.OpenStackCloud, 'search_images') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'search_images') def test_get_images(self, mock_search): image1 = dict(id='123', name='mickey') mock_search.return_value = [image1] @@ -62,7 +62,7 @@ def test_get_images(self, mock_search): self.assertIsNotNone(r) self.assertDictEqual(image1, r) - @mock.patch.object(shade.OpenStackCloud, 'search_images') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'search_images') def test_get_image_not_found(self, mock_search): mock_search.return_value = [] r = self.cloud.get_image('doesNotExist') diff --git a/shade/shade/tests/unit/test_shade_operator.py b/openstack/cloud/tests/unit/test_shade_operator.py similarity index 90% rename from shade/shade/tests/unit/test_shade_operator.py rename to openstack/cloud/tests/unit/test_shade_operator.py index 7528178db..5396e705f 100644 --- a/shade/shade/tests/unit/test_shade_operator.py +++ b/openstack/cloud/tests/unit/test_shade_operator.py @@ -18,13 +18,13 @@ import testtools import uuid -import os_client_config as occ +import openstack.config as occ from os_client_config import cloud_config -import shade -from shade import exc -from shade import meta -from shade.tests import fakes -from shade.tests.unit import base +import openstack.cloud +from openstack.cloud import exc +from openstack.cloud import meta +from openstack.cloud.tests import fakes +from openstack.cloud.tests.unit import base class TestShadeOperator(base.RequestsMockTestCase): @@ -38,9 +38,9 @@ def setUp(self): machine_name=self.machine_name) def test_operator_cloud(self): - self.assertIsInstance(self.op_cloud, shade.OperatorCloud) + self.assertIsInstance(self.op_cloud, openstack.cloud.OperatorCloud) - @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(openstack.cloud.OperatorCloud, 'ironic_client') def test_list_nics(self, mock_client): port1 = fakes.FakeMachinePort(1, "aa:bb:cc:dd", "node1") port2 = fakes.FakeMachinePort(2, "dd:cc:bb:aa", "node2") @@ -53,25 +53,25 @@ def test_list_nics(self, mock_client): self.assertTrue(mock_client.port.list.called) self.assertEqual(port_dict_list, nics) - @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(openstack.cloud.OperatorCloud, 'ironic_client') def test_list_nics_failure(self, mock_client): mock_client.port.list.side_effect = Exception() self.assertRaises(exc.OpenStackCloudException, self.op_cloud.list_nics) - @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(openstack.cloud.OperatorCloud, 'ironic_client') def test_list_nics_for_machine(self, mock_client): mock_client.node.list_ports.return_value = [] self.op_cloud.list_nics_for_machine("123") mock_client.node.list_ports.assert_called_with(node_id="123") - @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(openstack.cloud.OperatorCloud, 'ironic_client') def test_list_nics_for_machine_failure(self, mock_client): mock_client.node.list_ports.side_effect = Exception() self.assertRaises(exc.OpenStackCloudException, self.op_cloud.list_nics_for_machine, None) - @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(openstack.cloud.OperatorCloud, 'ironic_client') def test_register_machine(self, mock_client): class fake_node(object): uuid = "00000000-0000-0000-0000-000000000000" @@ -94,8 +94,9 @@ class fake_node(object): self.assertTrue(mock_client.port.create.called) self.assertFalse(mock_client.node.get.called) - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - @mock.patch.object(shade.OperatorCloud, 'node_set_provision_state') + @mock.patch.object(openstack.cloud.OperatorCloud, 'ironic_client') + @mock.patch.object(openstack.cloud.OperatorCloud, + 'node_set_provision_state') def test_register_machine_enroll( self, mock_set_state, @@ -168,17 +169,18 @@ class fake_node_post_enroll_failure(object): fake_node_post_manage, fake_node_post_enroll_failure]) self.assertRaises( - shade.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, self.op_cloud.register_machine, nics) self.assertRaises( - shade.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, self.op_cloud.register_machine, nics, wait=True) - @mock.patch.object(shade.OperatorCloud, 'ironic_client') - @mock.patch.object(shade.OperatorCloud, 'node_set_provision_state') + @mock.patch.object(openstack.cloud.OperatorCloud, 'ironic_client') + @mock.patch.object(openstack.cloud.OperatorCloud, + 'node_set_provision_state') def test_register_machine_enroll_timeout( self, mock_set_state, @@ -195,7 +197,7 @@ class fake_node_init_state(object): mock_client.node.create.return_value = fake_node_init_state nics = [{'mac': '00:00:00:00:00:00'}] self.assertRaises( - shade.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, self.op_cloud.register_machine, nics, lock_timeout=0.001) @@ -205,7 +207,7 @@ class fake_node_init_state(object): mock_client.node.get.reset_mock() mock_client.node.create.reset_mock() self.assertRaises( - shade.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, self.op_cloud.register_machine, nics, wait=True, @@ -214,7 +216,7 @@ class fake_node_init_state(object): self.assertTrue(mock_client.port.create.called) self.assertTrue(mock_client.node.get.called) - @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(openstack.cloud.OperatorCloud, 'ironic_client') def test_register_machine_port_create_failed(self, mock_client): class fake_node(object): uuid = "00000000-0000-0000-0000-000000000000" @@ -233,7 +235,7 @@ class fake_node(object): self.assertTrue(mock_client.port.create.called) self.assertTrue(mock_client.node.delete.called) - @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(openstack.cloud.OperatorCloud, 'ironic_client') def test_unregister_machine(self, mock_client): class fake_node(object): provision_state = 'available' @@ -255,7 +257,7 @@ class fake_port(object): mock_client.port.delete.assert_called_with( port_id='00000000-0000-0000-0000-000000000001') - @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(openstack.cloud.OperatorCloud, 'ironic_client') def test_unregister_machine_unavailable(self, mock_client): invalid_states = ['active', 'cleaning', 'clean wait', 'clean failed'] nics = [{'mac': '00:00:00:00:00:00'}] @@ -277,7 +279,7 @@ class fake_node(object): mock_client.node.reset_mock() mock_client.node.reset_mock() - @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(openstack.cloud.OperatorCloud, 'ironic_client') def test_unregister_machine_timeout(self, mock_client): class fake_node(object): provision_state = 'available' @@ -297,7 +299,7 @@ class fake_node(object): self.assertTrue(mock_client.port.get_by_address.called) self.assertTrue(mock_client.node.get.called) - @mock.patch.object(shade.OpenStackCloud, '_image_client') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') def test_get_image_name(self, mock_client): fake_image = munch.Munch( @@ -308,7 +310,7 @@ def test_get_image_name(self, mock_client): self.assertEqual('22 name', self.op_cloud.get_image_name('22')) self.assertEqual('22 name', self.op_cloud.get_image_name('22 name')) - @mock.patch.object(shade.OpenStackCloud, '_image_client') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') def test_get_image_id(self, mock_client): fake_image = munch.Munch( diff --git a/shade/shade/tests/unit/test_stack.py b/openstack/cloud/tests/unit/test_stack.py similarity index 97% rename from shade/shade/tests/unit/test_stack.py rename to openstack/cloud/tests/unit/test_stack.py index cc83eb2ec..6915d6479 100644 --- a/shade/shade/tests/unit/test_stack.py +++ b/openstack/cloud/tests/unit/test_stack.py @@ -14,10 +14,10 @@ import tempfile import testtools -import shade -from shade import meta -from shade.tests import fakes -from shade.tests.unit import base +import openstack.cloud +from openstack.cloud import meta +from openstack.cloud.tests import fakes +from openstack.cloud.tests.unit import base class TestStack(base.RequestsMockTestCase): @@ -55,7 +55,8 @@ def test_list_stacks_exception(self): endpoint=fakes.ORCHESTRATION_ENDPOINT), status_code=404) ]) - with testtools.ExpectedException(shade.OpenStackCloudURINotFound): + with testtools.ExpectedException( + openstack.cloud.OpenStackCloudURINotFound): self.cloud.list_stacks() self.assert_calls() @@ -107,7 +108,8 @@ def test_search_stacks_exception(self): endpoint=fakes.ORCHESTRATION_ENDPOINT), status_code=404) ]) - with testtools.ExpectedException(shade.OpenStackCloudURINotFound): + with testtools.ExpectedException( + openstack.cloud.OpenStackCloudURINotFound): self.cloud.search_stacks() def test_delete_stack(self): @@ -167,7 +169,8 @@ def test_delete_stack_exception(self): status_code=400, reason="ouch"), ]) - with testtools.ExpectedException(shade.OpenStackCloudBadRequest): + with testtools.ExpectedException( + openstack.cloud.OpenStackCloudBadRequest): self.cloud.delete_stack(self.stack_id) self.assert_calls() @@ -283,7 +286,8 @@ def test_delete_stack_wait_failed(self): json={"stack": failed_stack}), ]) - with testtools.ExpectedException(shade.OpenStackCloudException): + with testtools.ExpectedException( + openstack.cloud.OpenStackCloudException): self.cloud.delete_stack(self.stack_id, wait=True) self.assert_calls() diff --git a/shade/shade/tests/unit/test_subnet.py b/openstack/cloud/tests/unit/test_subnet.py similarity index 99% rename from shade/shade/tests/unit/test_subnet.py rename to openstack/cloud/tests/unit/test_subnet.py index d52c7dee0..5e875055f 100644 --- a/shade/shade/tests/unit/test_subnet.py +++ b/openstack/cloud/tests/unit/test_subnet.py @@ -16,8 +16,8 @@ import copy import testtools -from shade import exc -from shade.tests.unit import base +from openstack.cloud import exc +from openstack.cloud.tests.unit import base class TestSubnet(base.RequestsMockTestCase): diff --git a/shade/shade/tests/unit/test_task_manager.py b/openstack/cloud/tests/unit/test_task_manager.py similarity index 97% rename from shade/shade/tests/unit/test_task_manager.py rename to openstack/cloud/tests/unit/test_task_manager.py index 1a416ee44..8d6583e49 100644 --- a/shade/shade/tests/unit/test_task_manager.py +++ b/openstack/cloud/tests/unit/test_task_manager.py @@ -16,8 +16,8 @@ import concurrent.futures import mock -from shade import task_manager -from shade.tests.unit import base +from openstack.cloud import task_manager +from openstack.cloud.tests.unit import base class TestException(Exception): diff --git a/shade/shade/tests/unit/test_update_server.py b/openstack/cloud/tests/unit/test_update_server.py similarity index 95% rename from shade/shade/tests/unit/test_update_server.py rename to openstack/cloud/tests/unit/test_update_server.py index 2a94c13c1..4fef35566 100644 --- a/shade/shade/tests/unit/test_update_server.py +++ b/openstack/cloud/tests/unit/test_update_server.py @@ -19,9 +19,9 @@ import uuid -from shade.exc import OpenStackCloudException -from shade.tests import fakes -from shade.tests.unit import base +from openstack.cloud.exc import OpenStackCloudException +from openstack.cloud.tests import fakes +from openstack.cloud.tests.unit import base class TestUpdateServer(base.RequestsMockTestCase): diff --git a/shade/shade/tests/unit/test_usage.py b/openstack/cloud/tests/unit/test_usage.py similarity index 98% rename from shade/shade/tests/unit/test_usage.py rename to openstack/cloud/tests/unit/test_usage.py index 9643f9ba2..d51530ab6 100644 --- a/shade/shade/tests/unit/test_usage.py +++ b/openstack/cloud/tests/unit/test_usage.py @@ -14,7 +14,7 @@ import datetime import uuid -from shade.tests.unit import base +from openstack.cloud.tests.unit import base class TestUsage(base.RequestsMockTestCase): diff --git a/shade/shade/tests/unit/test_users.py b/openstack/cloud/tests/unit/test_users.py similarity index 98% rename from shade/shade/tests/unit/test_users.py rename to openstack/cloud/tests/unit/test_users.py index 8f15c864a..b3ab63d23 100644 --- a/shade/shade/tests/unit/test_users.py +++ b/openstack/cloud/tests/unit/test_users.py @@ -14,8 +14,8 @@ import testtools -import shade -from shade.tests.unit import base +import openstack.cloud +from openstack.cloud.tests.unit import base class TestUsers(base.RequestsMockTestCase): @@ -124,7 +124,7 @@ def test_create_user_v3_no_domain(self): user_data = self._get_user_data(domain_id=uuid.uuid4().hex, email='test@example.com') with testtools.ExpectedException( - shade.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, "User or project creation requires an explicit" " domain_id argument." ): diff --git a/shade/shade/tests/unit/test_volume.py b/openstack/cloud/tests/unit/test_volume.py similarity index 97% rename from shade/shade/tests/unit/test_volume.py rename to openstack/cloud/tests/unit/test_volume.py index 5983c6af8..fc495b97d 100644 --- a/shade/shade/tests/unit/test_volume.py +++ b/openstack/cloud/tests/unit/test_volume.py @@ -13,10 +13,10 @@ import testtools -import shade -from shade import meta -from shade.tests import fakes -from shade.tests.unit import base +import openstack.cloud +from openstack.cloud import meta +from openstack.cloud.tests import fakes +from openstack.cloud.tests.unit import base class TestVolume(base.RequestsMockTestCase): @@ -60,7 +60,7 @@ def test_attach_volume_exception(self): 'volumeId': vol['id']}}) )]) with testtools.ExpectedException( - shade.OpenStackCloudURINotFound, + openstack.cloud.OpenStackCloudURINotFound, "Error attaching volume %s to server %s" % ( volume['id'], server['id']) ): @@ -126,7 +126,7 @@ def test_attach_volume_wait_error(self): json={'volumes': [errored_volume]})]) with testtools.ExpectedException( - shade.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, "Error in attaching volume %s" % errored_volume['id'] ): self.cloud.attach_volume(server, volume) @@ -137,7 +137,7 @@ def test_attach_volume_not_available(self): volume = dict(id='volume001', status='error', attachments=[]) with testtools.ExpectedException( - shade.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, "Volume %s is not available. Status is '%s'" % ( volume['id'], volume['status']) ): @@ -153,7 +153,7 @@ def test_attach_volume_already_attached(self): ]) with testtools.ExpectedException( - shade.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, "Volume %s already attached to server %s on device %s" % ( volume['id'], server['id'], device_id) ): @@ -189,7 +189,7 @@ def test_detach_volume_exception(self): 'os-volume_attachments', volume['id']]), status_code=404)]) with testtools.ExpectedException( - shade.OpenStackCloudURINotFound, + openstack.cloud.OpenStackCloudURINotFound, "Error detaching volume %s from server %s" % ( volume['id'], server['id']) ): @@ -238,7 +238,7 @@ def test_detach_volume_wait_error(self): 'volumev2', 'public', append=['volumes', 'detail']), json={'volumes': [errored_volume]})]) with testtools.ExpectedException( - shade.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, "Error in detaching volume %s" % errored_volume['id'] ): self.cloud.detach_volume(server, volume) diff --git a/shade/shade/tests/unit/test_volume_access.py b/openstack/cloud/tests/unit/test_volume_access.py similarity index 97% rename from shade/shade/tests/unit/test_volume_access.py rename to openstack/cloud/tests/unit/test_volume_access.py index 39ee64ead..785e5695e 100644 --- a/shade/shade/tests/unit/test_volume_access.py +++ b/openstack/cloud/tests/unit/test_volume_access.py @@ -15,8 +15,8 @@ import testtools -import shade -from shade.tests.unit import base +import openstack.cloud +from openstack.cloud.tests.unit import base class TestVolumeAccess(base.RequestsMockTestCase): @@ -186,8 +186,9 @@ def test_add_volume_type_access_missing(self): append=['types'], qs_elements=['is_public=None']), json={'volume_types': [volume_type]})]) - with testtools.ExpectedException(shade.OpenStackCloudException, - "VolumeType not found: MISSING"): + with testtools.ExpectedException( + openstack.cloud.OpenStackCloudException, + "VolumeType not found: MISSING"): self.op_cloud.add_volume_type_access( "MISSING", project_001['project_id']) self.assert_calls() diff --git a/shade/shade/tests/unit/test_volume_backups.py b/openstack/cloud/tests/unit/test_volume_backups.py similarity index 98% rename from shade/shade/tests/unit/test_volume_backups.py rename to openstack/cloud/tests/unit/test_volume_backups.py index ca04c1d8c..f32beef6e 100644 --- a/shade/shade/tests/unit/test_volume_backups.py +++ b/openstack/cloud/tests/unit/test_volume_backups.py @@ -9,8 +9,8 @@ # 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 shade import meta -from shade.tests.unit import base +from openstack.cloud import meta +from openstack.cloud.tests.unit import base class TestVolumeBackups(base.RequestsMockTestCase): diff --git a/shade/shade/tests/unit/test_zone.py b/openstack/cloud/tests/unit/test_zone.py similarity index 97% rename from shade/shade/tests/unit/test_zone.py rename to openstack/cloud/tests/unit/test_zone.py index 960a81534..1d317880c 100644 --- a/shade/shade/tests/unit/test_zone.py +++ b/openstack/cloud/tests/unit/test_zone.py @@ -13,8 +13,8 @@ import copy import testtools -import shade -from shade.tests.unit import base +import openstack.cloud +from openstack.cloud.tests.unit import base zone_dict = { @@ -62,7 +62,7 @@ def test_create_zone_exception(self): status_code=500) ]) with testtools.ExpectedException( - shade.exc.OpenStackCloudHTTPError, + openstack.cloud.exc.OpenStackCloudHTTPError, "Unable to create zone example.net." ): self.cloud.create_zone('example.net.') diff --git a/os-client-config/os_client_config/__init__.py b/openstack/config/__init__.py similarity index 96% rename from os-client-config/os_client_config/__init__.py rename to openstack/config/__init__.py index 424652541..8cf0c9427 100644 --- a/os-client-config/os_client_config/__init__.py +++ b/openstack/config/__init__.py @@ -104,8 +104,8 @@ def make_shade(options=None, **kwargs): A mechanism that matches make_sdk, make_client and make_rest_client. - :rtype: :class:`~shade.OpenStackCloud` + :rtype: :class:`~openstack.cloud.OpenStackCloud` """ - import shade + import openstack.cloud cloud = get_config(options=options, **kwargs) - return shade.OpenStackCloud(cloud_config=cloud, **kwargs) + return openstack.cloud.OpenStackCloud(cloud_config=cloud, **kwargs) diff --git a/shade/shade/_log.py b/openstack/config/_log.py similarity index 100% rename from shade/shade/_log.py rename to openstack/config/_log.py diff --git a/os-client-config/os_client_config/cloud_config.py b/openstack/config/cloud_config.py similarity index 99% rename from os-client-config/os_client_config/cloud_config.py rename to openstack/config/cloud_config.py index d8b1e262c..29acc5499 100644 --- a/os-client-config/os_client_config/cloud_config.py +++ b/openstack/config/cloud_config.py @@ -21,7 +21,7 @@ from keystoneauth1 import session import requestsexceptions -import os_client_config +import openstack.config as os_client_config from os_client_config import _log from os_client_config import constructors from os_client_config import exceptions diff --git a/os-client-config/os_client_config/config.py b/openstack/config/config.py similarity index 100% rename from os-client-config/os_client_config/config.py rename to openstack/config/config.py diff --git a/os-client-config/os_client_config/constructors.json b/openstack/config/constructors.json similarity index 100% rename from os-client-config/os_client_config/constructors.json rename to openstack/config/constructors.json diff --git a/os-client-config/os_client_config/constructors.py b/openstack/config/constructors.py similarity index 100% rename from os-client-config/os_client_config/constructors.py rename to openstack/config/constructors.py diff --git a/os-client-config/os_client_config/defaults.json b/openstack/config/defaults.json similarity index 100% rename from os-client-config/os_client_config/defaults.json rename to openstack/config/defaults.json diff --git a/os-client-config/os_client_config/defaults.py b/openstack/config/defaults.py similarity index 100% rename from os-client-config/os_client_config/defaults.py rename to openstack/config/defaults.py diff --git a/os-client-config/os_client_config/exceptions.py b/openstack/config/exceptions.py similarity index 100% rename from os-client-config/os_client_config/exceptions.py rename to openstack/config/exceptions.py diff --git a/os-client-config/os_client_config/schema.json b/openstack/config/schema.json similarity index 100% rename from os-client-config/os_client_config/schema.json rename to openstack/config/schema.json diff --git a/shade/shade/tests/unit/__init__.py b/openstack/config/tests/__init__.py similarity index 100% rename from shade/shade/tests/unit/__init__.py rename to openstack/config/tests/__init__.py diff --git a/os-client-config/os_client_config/tests/base.py b/openstack/config/tests/base.py similarity index 99% rename from os-client-config/os_client_config/tests/base.py rename to openstack/config/tests/base.py index 9710782d4..e66dea14d 100644 --- a/os-client-config/os_client_config/tests/base.py +++ b/openstack/config/tests/base.py @@ -121,7 +121,7 @@ 'name': 'split-no-default', 'routes_ipv6_externally': False, 'routes_ipv4_externally': True, - }], + }], 'region_name': 'test-region', }, '_test_cloud_regions': { diff --git a/os-client-config/os_client_config/tests/test_cloud_config.py b/openstack/config/tests/test_cloud_config.py similarity index 100% rename from os-client-config/os_client_config/tests/test_cloud_config.py rename to openstack/config/tests/test_cloud_config.py diff --git a/os-client-config/os_client_config/tests/test_config.py b/openstack/config/tests/test_config.py similarity index 100% rename from os-client-config/os_client_config/tests/test_config.py rename to openstack/config/tests/test_config.py diff --git a/os-client-config/os_client_config/tests/test_environ.py b/openstack/config/tests/test_environ.py similarity index 100% rename from os-client-config/os_client_config/tests/test_environ.py rename to openstack/config/tests/test_environ.py diff --git a/os-client-config/os_client_config/tests/test_init.py b/openstack/config/tests/test_init.py similarity index 96% rename from os-client-config/os_client_config/tests/test_init.py rename to openstack/config/tests/test_init.py index 5b4fab998..6c766a2ef 100644 --- a/os-client-config/os_client_config/tests/test_init.py +++ b/openstack/config/tests/test_init.py @@ -12,7 +12,7 @@ import argparse -import os_client_config +import openstack.config as os_client_config from os_client_config.tests import base diff --git a/os-client-config/os_client_config/tests/test_json.py b/openstack/config/tests/test_json.py similarity index 100% rename from os-client-config/os_client_config/tests/test_json.py rename to openstack/config/tests/test_json.py diff --git a/os-client-config/os_client_config/vendor-schema.json b/openstack/config/vendor-schema.json similarity index 100% rename from os-client-config/os_client_config/vendor-schema.json rename to openstack/config/vendor-schema.json diff --git a/os-client-config/os_client_config/vendors/__init__.py b/openstack/config/vendors/__init__.py similarity index 100% rename from os-client-config/os_client_config/vendors/__init__.py rename to openstack/config/vendors/__init__.py diff --git a/os-client-config/os_client_config/vendors/auro.json b/openstack/config/vendors/auro.json similarity index 100% rename from os-client-config/os_client_config/vendors/auro.json rename to openstack/config/vendors/auro.json diff --git a/os-client-config/os_client_config/vendors/bluebox.json b/openstack/config/vendors/bluebox.json similarity index 100% rename from os-client-config/os_client_config/vendors/bluebox.json rename to openstack/config/vendors/bluebox.json diff --git a/os-client-config/os_client_config/vendors/catalyst.json b/openstack/config/vendors/catalyst.json similarity index 100% rename from os-client-config/os_client_config/vendors/catalyst.json rename to openstack/config/vendors/catalyst.json diff --git a/os-client-config/os_client_config/vendors/citycloud.json b/openstack/config/vendors/citycloud.json similarity index 100% rename from os-client-config/os_client_config/vendors/citycloud.json rename to openstack/config/vendors/citycloud.json diff --git a/os-client-config/os_client_config/vendors/conoha.json b/openstack/config/vendors/conoha.json similarity index 100% rename from os-client-config/os_client_config/vendors/conoha.json rename to openstack/config/vendors/conoha.json diff --git a/os-client-config/os_client_config/vendors/datacentred.json b/openstack/config/vendors/datacentred.json similarity index 100% rename from os-client-config/os_client_config/vendors/datacentred.json rename to openstack/config/vendors/datacentred.json diff --git a/os-client-config/os_client_config/vendors/dreamcompute.json b/openstack/config/vendors/dreamcompute.json similarity index 100% rename from os-client-config/os_client_config/vendors/dreamcompute.json rename to openstack/config/vendors/dreamcompute.json diff --git a/os-client-config/os_client_config/vendors/dreamhost.json b/openstack/config/vendors/dreamhost.json similarity index 100% rename from os-client-config/os_client_config/vendors/dreamhost.json rename to openstack/config/vendors/dreamhost.json diff --git a/os-client-config/os_client_config/vendors/elastx.json b/openstack/config/vendors/elastx.json similarity index 100% rename from os-client-config/os_client_config/vendors/elastx.json rename to openstack/config/vendors/elastx.json diff --git a/os-client-config/os_client_config/vendors/entercloudsuite.json b/openstack/config/vendors/entercloudsuite.json similarity index 100% rename from os-client-config/os_client_config/vendors/entercloudsuite.json rename to openstack/config/vendors/entercloudsuite.json diff --git a/os-client-config/os_client_config/vendors/fuga.json b/openstack/config/vendors/fuga.json similarity index 100% rename from os-client-config/os_client_config/vendors/fuga.json rename to openstack/config/vendors/fuga.json diff --git a/os-client-config/os_client_config/vendors/ibmcloud.json b/openstack/config/vendors/ibmcloud.json similarity index 100% rename from os-client-config/os_client_config/vendors/ibmcloud.json rename to openstack/config/vendors/ibmcloud.json diff --git a/os-client-config/os_client_config/vendors/internap.json b/openstack/config/vendors/internap.json similarity index 100% rename from os-client-config/os_client_config/vendors/internap.json rename to openstack/config/vendors/internap.json diff --git a/os-client-config/os_client_config/vendors/otc.json b/openstack/config/vendors/otc.json similarity index 100% rename from os-client-config/os_client_config/vendors/otc.json rename to openstack/config/vendors/otc.json diff --git a/os-client-config/os_client_config/vendors/ovh.json b/openstack/config/vendors/ovh.json similarity index 100% rename from os-client-config/os_client_config/vendors/ovh.json rename to openstack/config/vendors/ovh.json diff --git a/os-client-config/os_client_config/vendors/rackspace.json b/openstack/config/vendors/rackspace.json similarity index 100% rename from os-client-config/os_client_config/vendors/rackspace.json rename to openstack/config/vendors/rackspace.json diff --git a/os-client-config/os_client_config/vendors/switchengines.json b/openstack/config/vendors/switchengines.json similarity index 100% rename from os-client-config/os_client_config/vendors/switchengines.json rename to openstack/config/vendors/switchengines.json diff --git a/os-client-config/os_client_config/vendors/ultimum.json b/openstack/config/vendors/ultimum.json similarity index 100% rename from os-client-config/os_client_config/vendors/ultimum.json rename to openstack/config/vendors/ultimum.json diff --git a/os-client-config/os_client_config/vendors/unitedstack.json b/openstack/config/vendors/unitedstack.json similarity index 100% rename from os-client-config/os_client_config/vendors/unitedstack.json rename to openstack/config/vendors/unitedstack.json diff --git a/os-client-config/os_client_config/vendors/vexxhost.json b/openstack/config/vendors/vexxhost.json similarity index 100% rename from os-client-config/os_client_config/vendors/vexxhost.json rename to openstack/config/vendors/vexxhost.json diff --git a/os-client-config/os_client_config/vendors/zetta.json b/openstack/config/vendors/zetta.json similarity index 100% rename from os-client-config/os_client_config/vendors/zetta.json rename to openstack/config/vendors/zetta.json diff --git a/openstack/connection.py b/openstack/connection.py index 6f42d35d4..9743f6f84 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -61,7 +61,7 @@ import sys from keystoneauth1.loading import base as ksa_loader -import os_client_config +import openstack.config as os_client_config from openstack import exceptions from openstack import profile as _profile diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index 9eb4eba4d..27bb895d3 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -11,7 +11,7 @@ # under the License. import os -import os_client_config +import openstack.config as os_client_config import time import unittest diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index 229dbb728..eb5b03da5 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -15,7 +15,7 @@ import fixtures from keystoneauth1 import session as ksa_session import mock -import os_client_config +import openstack.config as os_client_config from openstack import connection from openstack import exceptions diff --git a/os-client-config/doc/source/user/using.rst b/os-client-config/doc/source/user/using.rst index 7d1d34eae..827bb4af2 100644 --- a/os-client-config/doc/source/user/using.rst +++ b/os-client-config/doc/source/user/using.rst @@ -18,7 +18,7 @@ Get a named cloud. .. code-block:: python - import os_client_config + import openstack.config as os_client_config cloud_config = os_client_config.OpenStackConfig().get_one_cloud( 'internap', region_name='ams01') @@ -28,7 +28,7 @@ Or, get all of the clouds. .. code-block:: python - import os_client_config + import openstack.config as os_client_config cloud_config = os_client_config.OpenStackConfig().get_all_clouds() for cloud in cloud_config: @@ -47,7 +47,7 @@ with - as well as a consumption argument. import argparse import sys - import os_client_config + import openstack.config as os_client_config cloud_config = os_client_config.OpenStackConfig() parser = argparse.ArgumentParser() @@ -67,7 +67,7 @@ a helper function is provided. The following will get you a fully configured .. code-block:: python - import os_client_config + import openstack.config as os_client_config sdk = os_client_config.make_sdk() @@ -75,7 +75,7 @@ If you want to do the same thing but on a named cloud. .. code-block:: python - import os_client_config + import openstack.config as os_client_config sdk = os_client_config.make_sdk(cloud='mtvexx') @@ -85,7 +85,7 @@ If you want to do the same thing but also support command line parsing. import argparse - import os_client_config + import openstack.config as os_client_config sdk = os_client_config.make_sdk(options=argparse.ArgumentParser()) @@ -105,7 +105,7 @@ instance. .. code-block:: python - import os_client_config + import openstack.config as os_client_config cloud = os_client_config.make_shade() @@ -113,7 +113,7 @@ If you want to do the same thing but on a named cloud. .. code-block:: python - import os_client_config + import openstack.config as os_client_config cloud = os_client_config.make_shade(cloud='mtvexx') @@ -123,7 +123,7 @@ If you want to do the same thing but also support command line parsing. import argparse - import os_client_config + import openstack.config as os_client_config cloud = os_client_config.make_shade(options=argparse.ArgumentParser()) @@ -138,7 +138,7 @@ that is mounted on the endpoint for the service you're looking for. .. code-block:: python - import os_client_config + import openstack.config as os_client_config session = os_client_config.make_rest_client('compute', cloud='vexxhost') @@ -155,7 +155,7 @@ will get you a fully configured `novaclient` instance. .. code-block:: python - import os_client_config + import openstack.config as os_client_config nova = os_client_config.make_client('compute') @@ -163,7 +163,7 @@ If you want to do the same thing but on a named cloud. .. code-block:: python - import os_client_config + import openstack.config as os_client_config nova = os_client_config.make_client('compute', cloud='mtvexx') @@ -173,7 +173,7 @@ If you want to do the same thing but also support command line parsing. import argparse - import os_client_config + import openstack.config as os_client_config nova = os_client_config.make_client( 'compute', options=argparse.ArgumentParser()) diff --git a/os-client-config/tools/keystone_version.py b/os-client-config/tools/keystone_version.py index a81bdabf2..a4a00d27f 100644 --- a/os-client-config/tools/keystone_version.py +++ b/os-client-config/tools/keystone_version.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os_client_config +import openstack.config as os_client_config import pprint import sys import urlparse diff --git a/os-client-config/tools/nova_version.py b/os-client-config/tools/nova_version.py index 20603db41..e173ef6d1 100644 --- a/os-client-config/tools/nova_version.py +++ b/os-client-config/tools/nova_version.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os_client_config +import openstack.config as os_client_config ran = [] for cloud in os_client_config.OpenStackConfig().get_all_clouds(): diff --git a/releasenotes/notes/log-request-ids-37507cb6eed9a7da.yaml b/releasenotes/notes/log-request-ids-37507cb6eed9a7da.yaml index 8dbb75491..6c81b7756 100644 --- a/releasenotes/notes/log-request-ids-37507cb6eed9a7da.yaml +++ b/releasenotes/notes/log-request-ids-37507cb6eed9a7da.yaml @@ -2,4 +2,4 @@ other: - The contents of x-openstack-request-id are no longer added to object returned. Instead, they are logged to - a logger named 'shade.request_ids'. + a logger named 'openstack.cloud.request_ids'. diff --git a/releasenotes/notes/remove-novaclient-3f8d4db20d5f9582.yaml b/releasenotes/notes/remove-novaclient-3f8d4db20d5f9582.yaml index bd0ffbf4a..27db18cb9 100644 --- a/releasenotes/notes/remove-novaclient-3f8d4db20d5f9582.yaml +++ b/releasenotes/notes/remove-novaclient-3f8d4db20d5f9582.yaml @@ -2,4 +2,4 @@ upgrade: - All Nova interactions are done via direct REST calls. python-novaclient is no longer a direct dependency of - shade. + openstack.cloud. diff --git a/setup.cfg b/setup.cfg index d006218f5..11118defb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,6 +23,11 @@ classifier = packages = openstack +# TODO(mordred) Move this to an OSC command before 1.0 +[entry_points] +console_scripts = + openstack-inventory = openstack.cloud.cmd.inventory:main + [build_sphinx] source-dir = doc/source build-dir = doc/build diff --git a/shade/.gitreview b/shade/.gitreview index 35d93597c..650bebe84 100644 --- a/shade/.gitreview +++ b/shade/.gitreview @@ -1,4 +1,4 @@ [gerrit] host=review.openstack.org port=29418 -project=openstack-infra/shade.git +project=openstack-infra/openstack.cloud.git diff --git a/shade/HACKING.rst b/shade/HACKING.rst index 82e34ce37..9c8af4d35 100644 --- a/shade/HACKING.rst +++ b/shade/HACKING.rst @@ -44,6 +44,6 @@ Unit Tests Unit tests should be virtually instant. If a unit test takes more than 1 second to run, it is a bad unit test. Honestly, 1 second is too slow. -All unit test classes should subclass `shade.tests.unit.base.BaseTestCase`. The +All unit test classes should subclass `openstack.cloud.tests.unit.base.BaseTestCase`. The base TestCase class takes care of properly creating `OpenStackCloud` objects in a way that protects against local environment. diff --git a/shade/README.rst b/shade/README.rst index e4c4e8a66..c43a16699 100644 --- a/shade/README.rst +++ b/shade/README.rst @@ -47,14 +47,14 @@ Sometimes an example is nice. #. Create a server with *shade*, configured with the ``clouds.yml`` file:: - import shade + import openstack.cloud # Initialize and turn on debug logging - shade.simple_logging(debug=True) + openstack.cloud.simple_logging(debug=True) # Initialize cloud # Cloud configs are read with os-client-config - cloud = shade.openstack_cloud(cloud='mordred') + cloud = openstack.cloud.openstack_cloud(cloud='mordred') # Upload an image to the cloud image = cloud.create_image( diff --git a/shade/doc/source/contributor/coding.rst b/shade/doc/source/contributor/coding.rst index 739b319fa..851c18b1e 100644 --- a/shade/doc/source/contributor/coding.rst +++ b/shade/doc/source/contributor/coding.rst @@ -67,7 +67,7 @@ Returned Resources ================== Complex objects returned to the caller must be a `munch.Munch` type. The -`shade._adapter.Adapter` class makes resources into `munch.Munch`. +`openstack.cloud._adapter.Adapter` class makes resources into `munch.Munch`. All objects should be normalized. It is shade's purpose in life to make OpenStack consistent for end users, and this means not trusting the clouds @@ -79,7 +79,7 @@ Fields should not be in the normalization contract if we cannot commit to providing them to all users. Fields should be renamed in normalization to be consistent with -the rest of shade. For instance, nothing in shade exposes the legacy OpenStack +the rest of openstack.cloud. For instance, nothing in shade exposes the legacy OpenStack concept of "tenant" to a user, but instead uses "project" even if the cloud uses tenant. diff --git a/shade/doc/source/user/examples/cleanup-servers.py b/shade/doc/source/user/examples/cleanup-servers.py index 0e18ce228..832dfbc45 100644 --- a/shade/doc/source/user/examples/cleanup-servers.py +++ b/shade/doc/source/user/examples/cleanup-servers.py @@ -1,13 +1,13 @@ -import shade +import openstack.cloud # Initialize and turn on debug logging -shade.simple_logging(debug=True) +openstack.cloud.simple_logging(debug=True) for cloud_name, region_name in [ ('my-vexxhost', 'ca-ymq-1'), ('my-citycloud', 'Buf1'), ('my-internap', 'ams01')]: # Initialize cloud - cloud = shade.openstack_cloud(cloud=cloud_name, region_name=region_name) + cloud = openstack.cloud.openstack_cloud(cloud=cloud_name, region_name=region_name) for server in cloud.search_servers('my-server'): cloud.delete_server(server, wait=True, delete_ips=True) diff --git a/shade/doc/source/user/examples/create-server-dict.py b/shade/doc/source/user/examples/create-server-dict.py index 5fa9400ce..6c3b969a8 100644 --- a/shade/doc/source/user/examples/create-server-dict.py +++ b/shade/doc/source/user/examples/create-server-dict.py @@ -1,7 +1,7 @@ -import shade +import openstack.cloud # Initialize and turn on debug logging -shade.simple_logging(debug=True) +openstack.cloud.simple_logging(debug=True) for cloud_name, region_name, image, flavor_id in [ ('my-vexxhost', 'ca-ymq-1', 'Ubuntu 16.04.1 LTS [2017-03-03]', @@ -11,7 +11,7 @@ ('my-internap', 'ams01', 'Ubuntu 16.04 LTS (Xenial Xerus)', 'A1.4')]: # Initialize cloud - cloud = shade.openstack_cloud(cloud=cloud_name, region_name=region_name) + cloud = openstack.cloud.openstack_cloud(cloud=cloud_name, region_name=region_name) # Boot a server, wait for it to boot, and then do whatever is needed # to get a public ip for it. diff --git a/shade/doc/source/user/examples/create-server-name-or-id.py b/shade/doc/source/user/examples/create-server-name-or-id.py index 66ab20e8d..10c151eb6 100644 --- a/shade/doc/source/user/examples/create-server-name-or-id.py +++ b/shade/doc/source/user/examples/create-server-name-or-id.py @@ -1,7 +1,7 @@ -import shade +import openstack.cloud # Initialize and turn on debug logging -shade.simple_logging(debug=True) +openstack.cloud.simple_logging(debug=True) for cloud_name, region_name, image, flavor in [ ('my-vexxhost', 'ca-ymq-1', @@ -11,7 +11,7 @@ ('my-internap', 'ams01', 'Ubuntu 16.04 LTS (Xenial Xerus)', 'A1.4')]: # Initialize cloud - cloud = shade.openstack_cloud(cloud=cloud_name, region_name=region_name) + cloud = openstack.cloud.openstack_cloud(cloud=cloud_name, region_name=region_name) cloud.delete_server('my-server', wait=True, delete_ips=True) # Boot a server, wait for it to boot, and then do whatever is needed diff --git a/shade/doc/source/user/examples/debug-logging.py b/shade/doc/source/user/examples/debug-logging.py index bf177f320..14806e4ef 100644 --- a/shade/doc/source/user/examples/debug-logging.py +++ b/shade/doc/source/user/examples/debug-logging.py @@ -1,6 +1,6 @@ -import shade -shade.simple_logging(debug=True) +import openstack.cloud +openstack.cloud.simple_logging(debug=True) -cloud = shade.openstack_cloud( +cloud = openstack.cloud.openstack_cloud( cloud='my-vexxhost', region_name='ca-ymq-1') cloud.get_image('Ubuntu 16.04.1 LTS [2017-03-03]') diff --git a/shade/doc/source/user/examples/find-an-image.py b/shade/doc/source/user/examples/find-an-image.py index b7bfdb483..3b92b0356 100644 --- a/shade/doc/source/user/examples/find-an-image.py +++ b/shade/doc/source/user/examples/find-an-image.py @@ -1,7 +1,7 @@ -import shade -shade.simple_logging() +import openstack.cloud +openstack.cloud.simple_logging() -cloud = shade.openstack_cloud(cloud='fuga', region_name='cystack') +cloud = openstack.cloud.openstack_cloud(cloud='fuga', region_name='cystack') cloud.pprint([ image for image in cloud.list_images() if 'ubuntu' in image.name.lower()]) diff --git a/shade/doc/source/user/examples/http-debug-logging.py b/shade/doc/source/user/examples/http-debug-logging.py index 1a5b57f56..22a2b746c 100644 --- a/shade/doc/source/user/examples/http-debug-logging.py +++ b/shade/doc/source/user/examples/http-debug-logging.py @@ -1,6 +1,6 @@ -import shade -shade.simple_logging(http_debug=True) +import openstack.cloud +openstack.cloud.simple_logging(http_debug=True) -cloud = shade.openstack_cloud( +cloud = openstack.cloud.openstack_cloud( cloud='my-vexxhost', region_name='ca-ymq-1') cloud.get_image('Ubuntu 16.04.1 LTS [2017-03-03]') diff --git a/shade/doc/source/user/examples/munch-dict-object.py b/shade/doc/source/user/examples/munch-dict-object.py index b5430dd2c..212cb0956 100644 --- a/shade/doc/source/user/examples/munch-dict-object.py +++ b/shade/doc/source/user/examples/munch-dict-object.py @@ -1,7 +1,7 @@ -import shade -shade.simple_logging(debug=True) +import openstack.cloud +openstack.cloud.simple_logging(debug=True) -cloud = shade.openstack_cloud(cloud='ovh', region_name='SBG1') +cloud = openstack.cloud.openstack_cloud(cloud='ovh', region_name='SBG1') image = cloud.get_image('Ubuntu 16.10') print(image.name) print(image['name']) diff --git a/shade/doc/source/user/examples/normalization.py b/shade/doc/source/user/examples/normalization.py index 220214f2f..374b88577 100644 --- a/shade/doc/source/user/examples/normalization.py +++ b/shade/doc/source/user/examples/normalization.py @@ -1,7 +1,7 @@ -import shade -shade.simple_logging() +import openstack.cloud +openstack.cloud.simple_logging() -cloud = shade.openstack_cloud(cloud='fuga', region_name='cystack') +cloud = openstack.cloud.openstack_cloud(cloud='fuga', region_name='cystack') image = cloud.get_image( 'Ubuntu 16.04 LTS - Xenial Xerus - 64-bit - Fuga Cloud Based Image') cloud.pprint(image) diff --git a/shade/doc/source/user/examples/server-information.py b/shade/doc/source/user/examples/server-information.py index 79f45ea53..f896d287d 100644 --- a/shade/doc/source/user/examples/server-information.py +++ b/shade/doc/source/user/examples/server-information.py @@ -1,7 +1,7 @@ -import shade -shade.simple_logging(debug=True) +import openstack.cloud +openstack.cloud.simple_logging(debug=True) -cloud = shade.openstack_cloud(cloud='my-citycloud', region_name='Buf1') +cloud = openstack.cloud.openstack_cloud(cloud='my-citycloud', region_name='Buf1') try: server = cloud.create_server( 'my-server', image='Ubuntu 16.04 Xenial Xerus', diff --git a/shade/doc/source/user/examples/service-conditional-overrides.py b/shade/doc/source/user/examples/service-conditional-overrides.py index 1b88f2033..62800630f 100644 --- a/shade/doc/source/user/examples/service-conditional-overrides.py +++ b/shade/doc/source/user/examples/service-conditional-overrides.py @@ -1,5 +1,5 @@ -import shade -shade.simple_logging(debug=True) +import openstack.cloud +openstack.cloud.simple_logging(debug=True) -cloud = shade.openstack_cloud(cloud='rax', region_name='DFW') +cloud = openstack.cloud.openstack_cloud(cloud='rax', region_name='DFW') print(cloud.has_service('network')) diff --git a/shade/doc/source/user/examples/service-conditionals.py b/shade/doc/source/user/examples/service-conditionals.py index 2fa404c64..90953c7c8 100644 --- a/shade/doc/source/user/examples/service-conditionals.py +++ b/shade/doc/source/user/examples/service-conditionals.py @@ -1,6 +1,6 @@ -import shade -shade.simple_logging(debug=True) +import openstack.cloud +openstack.cloud.simple_logging(debug=True) -cloud = shade.openstack_cloud(cloud='kiss', region_name='region1') +cloud = openstack.cloud.openstack_cloud(cloud='kiss', region_name='region1') print(cloud.has_service('network')) print(cloud.has_service('container-orchestration')) diff --git a/shade/doc/source/user/examples/strict-mode.py b/shade/doc/source/user/examples/strict-mode.py index e67bfc8cd..4ecbf0f2a 100644 --- a/shade/doc/source/user/examples/strict-mode.py +++ b/shade/doc/source/user/examples/strict-mode.py @@ -1,7 +1,7 @@ -import shade -shade.simple_logging() +import openstack.cloud +openstack.cloud.simple_logging() -cloud = shade.openstack_cloud( +cloud = openstack.cloud.openstack_cloud( cloud='fuga', region_name='cystack', strict=True) image = cloud.get_image( 'Ubuntu 16.04 LTS - Xenial Xerus - 64-bit - Fuga Cloud Based Image') diff --git a/shade/doc/source/user/examples/upload-large-object.py b/shade/doc/source/user/examples/upload-large-object.py index 4a83728e5..10aa13cd0 100644 --- a/shade/doc/source/user/examples/upload-large-object.py +++ b/shade/doc/source/user/examples/upload-large-object.py @@ -1,7 +1,7 @@ -import shade -shade.simple_logging(debug=True) +import openstack.cloud +openstack.cloud.simple_logging(debug=True) -cloud = shade.openstack_cloud(cloud='ovh', region_name='SBG1') +cloud = openstack.cloud.openstack_cloud(cloud='ovh', region_name='SBG1') cloud.create_object( container='my-container', name='my-object', filename='/home/mordred/briarcliff.sh3d', diff --git a/shade/doc/source/user/examples/upload-object.py b/shade/doc/source/user/examples/upload-object.py index 4a83728e5..10aa13cd0 100644 --- a/shade/doc/source/user/examples/upload-object.py +++ b/shade/doc/source/user/examples/upload-object.py @@ -1,7 +1,7 @@ -import shade -shade.simple_logging(debug=True) +import openstack.cloud +openstack.cloud.simple_logging(debug=True) -cloud = shade.openstack_cloud(cloud='ovh', region_name='SBG1') +cloud = openstack.cloud.openstack_cloud(cloud='ovh', region_name='SBG1') cloud.create_object( container='my-container', name='my-object', filename='/home/mordred/briarcliff.sh3d', diff --git a/shade/doc/source/user/examples/user-agent.py b/shade/doc/source/user/examples/user-agent.py index 01aba7706..f65052854 100644 --- a/shade/doc/source/user/examples/user-agent.py +++ b/shade/doc/source/user/examples/user-agent.py @@ -1,6 +1,6 @@ -import shade -shade.simple_logging(http_debug=True) +import openstack.cloud +openstack.cloud.simple_logging(http_debug=True) -cloud = shade.openstack_cloud( +cloud = openstack.cloud.openstack_cloud( cloud='datacentred', app_name='AmazingApp', app_version='1.0') cloud.list_networks() diff --git a/shade/doc/source/user/logging.rst b/shade/doc/source/user/logging.rst index 5915de9ec..12f606a03 100644 --- a/shade/doc/source/user/logging.rst +++ b/shade/doc/source/user/logging.rst @@ -15,10 +15,10 @@ before any other `shade` functionality. .. code-block:: python - import shade - shade.simple_logging() + import openstack.cloud + openstack.cloud.simple_logging() -`shade.simple_logging` takes two optional boolean arguments: +`openstack.cloud.simple_logging` takes two optional boolean arguments: debug Turns on debug logging. @@ -26,7 +26,7 @@ debug http_debug Turns on debug logging as well as debug logging of the underlying HTTP calls. -`shade.simple_logging` also sets up a few other loggers and squelches some +`openstack.cloud.simple_logging` also sets up a few other loggers and squelches some warnings or log messages that are otherwise uninteresting or unactionable by a `shade` user. @@ -39,39 +39,39 @@ Most of the logging is set up to log to the root `shade` logger. There are additional sub-loggers that are used at times, primarily so that a user can decide to turn on or off a specific type of logging. They are listed below. -shade.task_manager - `shade` uses a Task Manager to perform remote calls. The `shade.task_manager` +openstack.cloud.task_manager + `shade` uses a Task Manager to perform remote calls. The `openstack.cloud.task_manager` logger emits messages at the start and end of each Task announging what it is going to run and then what it ran and how long it took. Logging - `shade.task_manager` is a good way to get a trace of external actions shade + `openstack.cloud.task_manager` is a good way to get a trace of external actions shade is taking without full `HTTP Tracing`_. -shade.request_ids - The `shade.request_ids` logger emits a log line at the end of each HTTP +openstack.cloud.request_ids + The `openstack.cloud.request_ids` logger emits a log line at the end of each HTTP interaction with the OpenStack Request ID associated with the interaction. This can be be useful for tracking action taken on the server-side if one does not want `HTTP Tracing`_. -shade.exc +openstack.cloud.exc If `log_inner_exceptions` is set to True, `shade` will emit any wrapped - exception to the `shade.exc` logger. Wrapped exceptions are usually + exception to the `openstack.cloud.exc` logger. Wrapped exceptions are usually considered implementation details, but can be useful for debugging problems. -shade.iterate_timeout +openstack.cloud.iterate_timeout When `shade` needs to poll a resource, it does so in a loop that waits - between iterations and ultimately timesout. The `shade.iterate_timeout` + between iterations and ultimately timesout. The `openstack.cloud.iterate_timeout` logger emits messages for each iteration indicating it is waiting and for how long. These can be useful to see for long running tasks so that one can know things are not stuck, but can also be noisy. -shade.http +openstack.cloud.http `shade` will sometimes log additional information about HTTP interactions - to the `shade.http` logger. This can be verbose, as it sometimes logs + to the `openstack.cloud.http` logger. This can be verbose, as it sometimes logs entire response bodies. -shade.fnmatch +openstack.cloud.fnmatch `shade` will try to use `fnmatch`_ on given `name_or_id` arguments. It's a - best effort attempt, so pattern misses are logged to `shade.fnmatch`. A user + best effort attempt, so pattern misses are logged to `openstack.cloud.fnmatch`. A user may not be intending to use an fnmatch pattern - such as if they are trying to find an image named ``Fedora 24 [official]``, so these messages are logged separately. @@ -82,7 +82,7 @@ HTTP Tracing ------------ HTTP Interactions are handled by `keystoneauth`. If you want to enable HTTP -tracing while using `shade` and are not using `shade.simple_logging`, +tracing while using `shade` and are not using `openstack.cloud.simple_logging`, set the log level of the `keystoneauth` logger to `DEBUG`. Python Logging diff --git a/shade/doc/source/user/microversions.rst b/shade/doc/source/user/microversions.rst index 8e4142a90..ce821dace 100644 --- a/shade/doc/source/user/microversions.rst +++ b/shade/doc/source/user/microversions.rst @@ -8,7 +8,7 @@ logic to handle each microversion for a given REST call it makes, with the following rules in mind: * If an activity shade performs can be done differently or more efficiently - with a new microversion, the support should be added to shade. + with a new microversion, the support should be added to openstack.cloud. * shade should always attempt to use the latest microversion it is aware of for a given call, unless a microversion removes important data. diff --git a/shade/doc/source/user/multi-cloud-demo.rst b/shade/doc/source/user/multi-cloud-demo.rst index fdd9dcb1b..449f16650 100644 --- a/shade/doc/source/user/multi-cloud-demo.rst +++ b/shade/doc/source/user/multi-cloud-demo.rst @@ -62,17 +62,17 @@ Complete Example .. code:: python - import shade + import openstack.cloud # Initialize and turn on debug logging - shade.simple_logging(debug=True) + openstack.cloud.simple_logging(debug=True) for cloud_name, region_name in [ ('my-vexxhost', 'ca-ymq-1'), ('my-citycloud', 'Buf1'), ('my-internap', 'ams01')]: # Initialize cloud - cloud = shade.openstack_cloud(cloud=cloud_name, region_name=region_name) + cloud = openstack.cloud.openstack_cloud(cloud=cloud_name, region_name=region_name) # Upload an image to the cloud image = cloud.create_image( @@ -314,17 +314,17 @@ Complete Example Again .. code:: python - import shade + import openstack.cloud # Initialize and turn on debug logging - shade.simple_logging(debug=True) + openstack.cloud.simple_logging(debug=True) for cloud_name, region_name in [ ('my-vexxhost', 'ca-ymq-1'), ('my-citycloud', 'Buf1'), ('my-internap', 'ams01')]: # Initialize cloud - cloud = shade.openstack_cloud(cloud=cloud_name, region_name=region_name) + cloud = openstack.cloud.openstack_cloud(cloud=cloud_name, region_name=region_name) # Upload an image to the cloud image = cloud.create_image( @@ -346,27 +346,27 @@ Import the library .. code:: python - import shade + import openstack.cloud Logging ======= * `shade` uses standard python logging -* Special `shade.request_ids` logger for API request IDs +* Special `openstack.cloud.request_ids` logger for API request IDs * `simple_logging` does easy defaults * Squelches some meaningless warnings * `debug` * Logs shade loggers at debug level - * Includes `shade.request_ids` debug logging + * Includes `openstack.cloud.request_ids` debug logging * `http_debug` Implies `debug`, turns on HTTP tracing .. code:: python # Initialize and turn on debug logging - shade.simple_logging(debug=True) + openstack.cloud.simple_logging(debug=True) Example with Debug Logging ========================== @@ -375,10 +375,10 @@ Example with Debug Logging .. code:: python - import shade - shade.simple_logging(debug=True) + import openstack.cloud + openstack.cloud.simple_logging(debug=True) - cloud = shade.openstack_cloud( + cloud = openstack.cloud.openstack_cloud( cloud='my-vexxhost', region_name='ca-ymq-1') cloud.get_image('Ubuntu 16.04.1 LTS [2017-03-03]') @@ -389,10 +389,10 @@ Example with HTTP Debug Logging .. code:: python - import shade - shade.simple_logging(http_debug=True) + import openstack.cloud + openstack.cloud.simple_logging(http_debug=True) - cloud = shade.openstack_cloud( + cloud = openstack.cloud.openstack_cloud( cloud='my-vexxhost', region_name='ca-ymq-1') cloud.get_image('Ubuntu 16.04.1 LTS [2017-03-03]') @@ -400,7 +400,7 @@ Cloud Regions ============= * `cloud` constructor needs `cloud` and `region_name` -* `shade.openstack_cloud` is a helper factory function +* `openstack.cloud.openstack_cloud` is a helper factory function .. code:: python @@ -409,7 +409,7 @@ Cloud Regions ('my-citycloud', 'Buf1'), ('my-internap', 'ams01')]: # Initialize cloud - cloud = shade.openstack_cloud(cloud=cloud_name, region_name=region_name) + cloud = openstack.cloud.openstack_cloud(cloud=cloud_name, region_name=region_name) Upload an Image =============== @@ -486,10 +486,10 @@ Image and Flavor by Name or ID .. code:: python - import shade + import openstack.cloud # Initialize and turn on debug logging - shade.simple_logging(debug=True) + openstack.cloud.simple_logging(debug=True) for cloud_name, region_name, image, flavor in [ ('my-vexxhost', 'ca-ymq-1', @@ -499,7 +499,7 @@ Image and Flavor by Name or ID ('my-internap', 'ams01', 'Ubuntu 16.04 LTS (Xenial Xerus)', 'A1.4')]: # Initialize cloud - cloud = shade.openstack_cloud(cloud=cloud_name, region_name=region_name) + cloud = openstack.cloud.openstack_cloud(cloud=cloud_name, region_name=region_name) # Boot a server, wait for it to boot, and then do whatever is needed # to get a public ip for it. @@ -533,10 +533,10 @@ Image and Flavor by Dict .. code:: python - import shade + import openstack.cloud # Initialize and turn on debug logging - shade.simple_logging(debug=True) + openstack.cloud.simple_logging(debug=True) for cloud_name, region_name, image, flavor_id in [ ('my-vexxhost', 'ca-ymq-1', 'Ubuntu 16.04.1 LTS [2017-03-03]', @@ -546,7 +546,7 @@ Image and Flavor by Dict ('my-internap', 'ams01', 'Ubuntu 16.04 LTS (Xenial Xerus)', 'A1.4')]: # Initialize cloud - cloud = shade.openstack_cloud(cloud=cloud_name, region_name=region_name) + cloud = openstack.cloud.openstack_cloud(cloud=cloud_name, region_name=region_name) # Boot a server, wait for it to boot, and then do whatever is needed # to get a public ip for it. @@ -564,10 +564,10 @@ Munch Objects .. code:: python - import shade - shade.simple_logging(debug=True) + import openstack.cloud + openstack.cloud.simple_logging(debug=True) - cloud = shade.openstack_cloud(cloud='zetta', region_name='no-osl1') + cloud = openstack.cloud.openstack_cloud(cloud='zetta', region_name='no-osl1') image = cloud.get_image('Ubuntu 14.04 (AMD64) [Local Storage]') print(image.name) print(image['name']) @@ -596,17 +596,17 @@ Cleanup Script .. code:: python - import shade + import openstack.cloud # Initialize and turn on debug logging - shade.simple_logging(debug=True) + openstack.cloud.simple_logging(debug=True) for cloud_name, region_name in [ ('my-vexxhost', 'ca-ymq-1'), ('my-citycloud', 'Buf1'), ('my-internap', 'ams01')]: # Initialize cloud - cloud = shade.openstack_cloud(cloud=cloud_name, region_name=region_name) + cloud = openstack.cloud.openstack_cloud(cloud=cloud_name, region_name=region_name) for server in cloud.search_servers('my-server'): cloud.delete_server(server, wait=True, delete_ips=True) @@ -618,10 +618,10 @@ Normalization .. code:: python - import shade - shade.simple_logging() + import openstack.cloud + openstack.cloud.simple_logging() - cloud = shade.openstack_cloud(cloud='fuga', region_name='cystack') + cloud = openstack.cloud.openstack_cloud(cloud='fuga', region_name='cystack') image = cloud.get_image( 'Ubuntu 16.04 LTS - Xenial Xerus - 64-bit - Fuga Cloud Based Image') cloud.pprint(image) @@ -634,10 +634,10 @@ Strict Normalized Results .. code:: python - import shade - shade.simple_logging() + import openstack.cloud + openstack.cloud.simple_logging() - cloud = shade.openstack_cloud( + cloud = openstack.cloud.openstack_cloud( cloud='fuga', region_name='cystack', strict=True) image = cloud.get_image( 'Ubuntu 16.04 LTS - Xenial Xerus - 64-bit - Fuga Cloud Based Image') @@ -651,10 +651,10 @@ How Did I Find the Image Name for the Last Example? .. code:: python - import shade - shade.simple_logging() + import openstack.cloud + openstack.cloud.simple_logging() - cloud = shade.openstack_cloud(cloud='fuga', region_name='cystack') + cloud = openstack.cloud.openstack_cloud(cloud='fuga', region_name='cystack') cloud.pprint([ image for image in cloud.list_images() if 'ubuntu' in image.name.lower()]) @@ -672,10 +672,10 @@ Added / Modified Information .. code:: python - import shade - shade.simple_logging(debug=True) + import openstack.cloud + openstack.cloud.simple_logging(debug=True) - cloud = shade.openstack_cloud(cloud='my-citycloud', region_name='Buf1') + cloud = openstack.cloud.openstack_cloud(cloud='my-citycloud', region_name='Buf1') try: server = cloud.create_server( 'my-server', image='Ubuntu 16.04 Xenial Xerus', @@ -714,10 +714,10 @@ User Agent Info .. code:: python - import shade - shade.simple_logging(http_debug=True) + import openstack.cloud + openstack.cloud.simple_logging(http_debug=True) - cloud = shade.openstack_cloud( + cloud = openstack.cloud.openstack_cloud( cloud='datacentred', app_name='AmazingApp', app_version='1.0') cloud.list_networks() @@ -732,10 +732,10 @@ Uploading Large Objects .. code:: python - import shade - shade.simple_logging(debug=True) + import openstack.cloud + openstack.cloud.simple_logging(debug=True) - cloud = shade.openstack_cloud(cloud='ovh', region_name='SBG1') + cloud = openstack.cloud.openstack_cloud(cloud='ovh', region_name='SBG1') cloud.create_object( container='my-container', name='my-object', filename='/home/mordred/briarcliff.sh3d') @@ -753,10 +753,10 @@ Uploading Large Objects .. code:: python - import shade - shade.simple_logging(debug=True) + import openstack.cloud + openstack.cloud.simple_logging(debug=True) - cloud = shade.openstack_cloud(cloud='ovh', region_name='SBG1') + cloud = openstack.cloud.openstack_cloud(cloud='ovh', region_name='SBG1') cloud.create_object( container='my-container', name='my-object', filename='/home/mordred/briarcliff.sh3d', @@ -769,10 +769,10 @@ Service Conditionals .. code:: python - import shade - shade.simple_logging(debug=True) + import openstack.cloud + openstack.cloud.simple_logging(debug=True) - cloud = shade.openstack_cloud(cloud='kiss', region_name='region1') + cloud = openstack.cloud.openstack_cloud(cloud='kiss', region_name='region1') print(cloud.has_service('network')) print(cloud.has_service('container-orchestration')) @@ -783,10 +783,10 @@ Service Conditional Overrides .. code:: python - import shade - shade.simple_logging(debug=True) + import openstack.cloud + openstack.cloud.simple_logging(debug=True) - cloud = shade.openstack_cloud(cloud='rax', region_name='DFW') + cloud = openstack.cloud.openstack_cloud(cloud='rax', region_name='DFW') print(cloud.has_service('network')) .. code:: yaml diff --git a/shade/doc/source/user/usage.rst b/shade/doc/source/user/usage.rst index fe37d74cb..e66c89a04 100644 --- a/shade/doc/source/user/usage.rst +++ b/shade/doc/source/user/usage.rst @@ -4,7 +4,7 @@ Usage To use shade in a project:: - import shade + import openstack.cloud For a simple example, see :ref:`example`. @@ -15,8 +15,8 @@ For a simple example, see :ref:`example`. objects can be accessed using either dictionary or object notation (e.g., ``server.id``, ``image.name`` and ``server['id']``, ``image['name']``) -.. autoclass:: shade.OpenStackCloud +.. autoclass:: openstack.cloud.OpenStackCloud :members: -.. autoclass:: shade.OperatorCloud +.. autoclass:: openstack.cloud.OperatorCloud :members: diff --git a/shade/setup.cfg b/shade/setup.cfg index 3f3300b3d..0ad4c36c8 100644 --- a/shade/setup.cfg +++ b/shade/setup.cfg @@ -18,10 +18,6 @@ classifier = Programming Language :: Python :: 3 Programming Language :: Python :: 3.4 -[entry_points] -console_scripts = - shade-inventory = shade.cmd.inventory:main - [build_sphinx] source-dir = doc/source build-dir = doc/build diff --git a/shade/shade/_legacy_clients.py b/shade/shade/_legacy_clients.py deleted file mode 100644 index fa6f51776..000000000 --- a/shade/shade/_legacy_clients.py +++ /dev/null @@ -1,252 +0,0 @@ -# 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 importlib -import warnings - -from keystoneauth1 import plugin -from os_client_config import constructors - -from shade import _utils -from shade import exc - - -class LegacyClientFactoryMixin(object): - """Mixin Class containing factory functions for legacy client objects. - - Methods in this class exist for backwards compatibility so will not go - away any time - but they are all things whose use is discouraged. They're - in a mixin to unclutter the main class file. - """ - - def _create_legacy_client( - self, client, service, deprecated=True, - module_name=None, **kwargs): - if client not in self._legacy_clients: - if deprecated: - self._deprecated_import_check(client) - if module_name: - constructors.get_constructor_mapping()[service] = module_name - self._legacy_clients[client] = self._get_client(service, **kwargs) - return self._legacy_clients[client] - - def _deprecated_import_check(self, client): - module_name = '{client}client'.format(client=client) - warnings.warn( - 'Using shade to get a {module_name} object is deprecated. If you' - ' need a {module_name} object, please use make_legacy_client in' - ' os-client-config instead'.format(module_name=module_name)) - try: - importlib.import_module(module_name) - except ImportError: - self.log.error( - '{module_name} is no longer a dependency of shade. You need to' - ' install python-{module_name} directly.'.format( - module_name=module_name)) - raise - - @property - def trove_client(self): - return self._create_legacy_client('trove', 'database') - - @property - def magnum_client(self): - return self._create_legacy_client('magnum', 'container-infra') - - @property - def neutron_client(self): - return self._create_legacy_client('neutron', 'network') - - @property - def nova_client(self): - return self._create_legacy_client('nova', 'compute', version='2.0') - - @property - def glance_client(self): - return self._create_legacy_client('glance', 'image') - - @property - def heat_client(self): - return self._create_legacy_client('heat', 'orchestration') - - @property - def swift_client(self): - return self._create_legacy_client('swift', 'object-store') - - @property - def cinder_client(self): - return self._create_legacy_client('cinder', 'volume') - - @property - def designate_client(self): - return self._create_legacy_client('designate', 'dns') - - @property - def keystone_client(self): - # Trigger discovery from ksa - self._identity_client - - # Skip broken discovery in ksc. We're good thanks. - from keystoneclient.v2_0 import client as v2_client - from keystoneclient.v3 import client as v3_client - if self.cloud_config.config['identity_api_version'] == '3': - client_class = v3_client - else: - client_class = v2_client - - return self._create_legacy_client( - 'keystone', 'identity', - client_class=client_class.Client, - deprecated=True, - endpoint=self._identity_client.get_endpoint(), - endpoint_override=self._identity_client.get_endpoint()) - - # Set the ironic API microversion to a known-good - # supported/tested with the contents of shade. - # - # NOTE(TheJulia): Defaulted to version 1.6 as the ironic - # state machine changes which will increment the version - # and break an automatic transition of an enrolled node - # to an available state. Locking the version is intended - # to utilize the original transition until shade supports - # calling for node inspection to allow the transition to - # take place automatically. - # NOTE(mordred): shade will handle microversions more - # directly in the REST layer. This microversion property - # will never change. When we implement REST, we should - # start at 1.6 since that's what we've been requesting - # via ironic_client - @property - def ironic_api_microversion(self): - # NOTE(mordred) Abuse _legacy_clients to only show - # this warning once - if 'ironic-microversion' not in self._legacy_clients: - warnings.warn( - 'shade is transitioning to direct REST calls which' - ' will handle microversions with no action needed' - ' on the part of the user. The ironic_api_microversion' - ' property is only used by the legacy ironic_client' - ' constructor and will never change. If you are using' - ' it for any reason, either switch to just using' - ' shade ironic-related API calls, or use os-client-config' - ' make_legacy_client directly and pass os_ironic_api_version' - ' to it as an argument. It is highly recommended to' - ' stop using this property.') - self._legacy_clients['ironic-microversion'] = True - return self._get_legacy_ironic_microversion() - - def _get_legacy_ironic_microversion(self): - return '1.6' - - def _join_ksa_version(self, version): - return ".".join([str(x) for x in version]) - - @property - def ironic_client(self): - # Trigger discovery from ksa. This will make ironicclient and - # keystoneauth1.adapter.Adapter code paths both go through discovery. - # ironicclient does its own magic with discovery, so we won't - # pass an endpoint_override here like we do for keystoneclient. - # Just so it's not wasted though, make sure we can handle the - # min microversion we need. - needed = self._get_legacy_ironic_microversion() - - # TODO(mordred) Bug in ksa - don't do microversion matching for - # auth_type = admin_token. Remove this if when the fix lands. - if (hasattr(plugin.BaseAuthPlugin, 'get_endpoint_data') or - self.cloud_config.config['auth_type'] not in ( - 'admin_token', 'none')): - # TODO(mordred) once we're on REST properly, we need a better - # method for matching requested and available microversion - endpoint_data = self._baremetal_client.get_endpoint_data() - if not endpoint_data.min_microversion: - raise exc.OpenStackCloudException( - "shade needs an ironic that supports microversions") - if endpoint_data.min_microversion[1] > int(needed[-1]): - raise exc.OpenStackCloudException( - "shade needs an ironic that supports microversion {needed}" - " but the ironic found has a minimum microversion" - " of {found}".format( - needed=needed, - found=self._join_ksa_version( - endpoint_data.min_microversion))) - if endpoint_data.max_microversion[1] < int(needed[-1]): - raise exc.OpenStackCloudException( - "shade needs an ironic that supports microversion {needed}" - " but the ironic found has a maximum microversion" - " of {found}".format( - needed=needed, - found=self._join_ksa_version( - endpoint_data.max_microversion))) - - return self._create_legacy_client( - 'ironic', 'baremetal', deprecated=False, - module_name='ironicclient.client.Client', - os_ironic_api_version=self._get_legacy_ironic_microversion()) - - def _get_swift_kwargs(self): - auth_version = self.cloud_config.get_api_version('identity') - auth_args = self.cloud_config.config.get('auth', {}) - os_options = {'auth_version': auth_version} - if auth_version == '2.0': - os_options['os_tenant_name'] = auth_args.get('project_name') - os_options['os_tenant_id'] = auth_args.get('project_id') - else: - os_options['os_project_name'] = auth_args.get('project_name') - os_options['os_project_id'] = auth_args.get('project_id') - - for key in ( - 'username', - 'password', - 'auth_url', - 'user_id', - 'project_domain_id', - 'project_domain_name', - 'user_domain_id', - 'user_domain_name'): - os_options['os_{key}'.format(key=key)] = auth_args.get(key) - return os_options - - @property - def swift_service(self): - suppress_warning = 'swift-service' not in self._legacy_clients - return self.make_swift_service_object(suppress_warning) - - def make_swift_service(self, suppress_warning=False): - # NOTE(mordred): Not using helper functions because the - # error message needs to be different - if not suppress_warning: - warnings.warn( - 'Using shade to get a SwiftService object is deprecated. shade' - ' will automatically do the things SwiftServices does as part' - ' of the normal object resource calls. If you are having' - ' trouble using those such that you still need to use' - ' SwiftService, please file a bug with shade.' - ' If you understand the issues and want to make this warning' - ' go away, use cloud.make_swift_service(True) instead of' - ' cloud.swift_service') - # Abuse self._legacy_clients so that we only give the warning - # once. We don't cache SwiftService objects. - self._legacy_clients['swift-service'] = True - try: - import swiftclient.service - except ImportError: - self.log.error( - 'swiftclient is no longer a dependency of shade. You need to' - ' install python-swiftclient directly.') - with _utils.shade_exceptions("Error constructing SwiftService"): - endpoint = self.get_session_endpoint( - service_key='object-store') - options = dict(os_auth_token=self.auth_token, - os_storage_url=endpoint, - os_region_name=self.region_name) - options.update(self._get_swift_kwargs()) - return swiftclient.service.SwiftService(options=options) diff --git a/tox.ini b/tox.ini index 789758b43..b6ab412d3 100644 --- a/tox.ini +++ b/tox.ini @@ -44,6 +44,15 @@ commands = python setup.py test --coverage --coverage-package-name=openstack --t commands = python setup.py build_sphinx [flake8] -ignore=D100,D101,D102,D103,D104,D105,D200,D202,D204,D205,D211,D301,D400,D401 +# The following are ignored on purpose. It's not super worth it to fix them. +# However, if you feel strongly about it, patches will be accepted to fix them +# if they fix ALL of the occurances of one and only one of them. +# H103 Is about the Apache license. It's strangely strict about the use of +# single vs double quotes in the license text. If someone decides to fix +# this, please be sure to preseve all copyright lines. +# H306 Is about alphabetical imports - there's a lot to fix. +# H4 Are about docstrings and there's just a huge pile of pre-existing issues. +# D* Came from sdk, unknown why they're skipped. +ignore = H103,H306,H4,D100,D101,D102,D103,D104,D105,D200,D202,D204,D205,D211,D301,D400,D401 show-source = True exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build From a4ee1a3f098e97f0e6bdc0f3d20c3021d06cb343 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 4 Oct 2017 13:06:22 -0500 Subject: [PATCH 1829/3836] Merge tox, tests and other support files Change-Id: I5a4759e36089f1f4fab0c75412c94d051d8b16a7 --- .gitignore | 1 + .mailmap | 5 +- .stestr.conf | 3 + .testr.conf | 8 - CONTRIBUTING.rst | 47 +- HACKING.rst | 51 +- README.rst | 137 +++- shade/bindep.txt => bindep.txt | 0 devstack/plugin.sh | 54 ++ doc/source/conf.py | 34 +- .../{contributors => contributor}/clouds.yaml | 0 .../doc => doc}/source/contributor/coding.rst | 61 +- .../source/contributor/contributing.rst | 0 .../create/examples/resource/fake.py | 0 .../create/examples/resource/fake_service.py | 0 .../create/resource.rst | 0 .../{contributors => contributor}/index.rst | 19 + .../{contributors => contributor}/layout.rst | 0 .../{contributors => contributor}/layout.txt | 0 .../{contributors => contributor}/local.conf | 0 .../{contributors => contributor}/setup.rst | 0 .../{contributors => contributor}/testing.rst | 0 doc/source/history.rst | 1 - doc/source/index.rst | 10 +- doc/source/install/index.rst | 12 + doc/source/releasenotes.rst | 6 + .../source/user/config}/configuration.rst | 0 .../user => doc/source/user/config}/index.rst | 2 +- .../source/user/config}/network-config.rst | 0 .../source/user/config/reference.rst | 0 .../user => doc/source/user/config}/using.rst | 0 .../source/user/config}/vendor-support.rst | 0 .../source/user/examples/cleanup-servers.py | 0 .../user/examples/create-server-dict.py | 0 .../user/examples/create-server-name-or-id.py | 0 .../source/user/examples/debug-logging.py | 0 .../source/user/examples/find-an-image.py | 0 .../user/examples/http-debug-logging.py | 0 .../source/user/examples/munch-dict-object.py | 0 .../source/user/examples/normalization.py | 0 .../user/examples/server-information.py | 0 .../examples/service-conditional-overrides.py | 0 .../user/examples/service-conditionals.py | 0 .../source/user/examples/strict-mode.py | 0 .../user/examples/upload-large-object.py | 0 .../source/user/examples/upload-object.py | 0 .../source/user/examples/user-agent.py | 0 {shade/doc => doc}/source/user/index.rst | 1 + {shade/doc => doc}/source/user/logging.rst | 0 .../doc => doc}/source/user/microversions.rst | 0 {shade/doc => doc}/source/user/model.rst | 0 .../source/user/multi-cloud-demo.rst | 0 {shade/doc => doc}/source/user/usage.rst | 0 {shade/extras => extras}/delete-network.sh | 0 {shade/extras => extras}/install-tips.sh | 0 {shade/extras => extras}/run-ansible-tests.sh | 4 +- openstack/cloud/tests/unit/base.py | 646 ------------------ .../{cloud => }/tests/ansible/README.txt | 0 .../tests/ansible/hooks/post_test_hook.sh | 10 +- .../tests/ansible/roles/auth/tasks/main.yml | 0 .../roles/client_config/tasks/main.yml | 0 .../tests/ansible/roles/group/tasks/main.yml | 0 .../tests/ansible/roles/group/vars/main.yml | 0 .../tests/ansible/roles/image/tasks/main.yml | 0 .../tests/ansible/roles/image/vars/main.yml | 0 .../ansible/roles/keypair/tasks/main.yml | 0 .../tests/ansible/roles/keypair/vars/main.yml | 0 .../roles/keystone_domain/tasks/main.yml | 0 .../roles/keystone_domain/vars/main.yml | 0 .../roles/keystone_role/tasks/main.yml | 0 .../ansible/roles/keystone_role/vars/main.yml | 0 .../ansible/roles/network/tasks/main.yml | 0 .../tests/ansible/roles/network/vars/main.yml | 0 .../ansible/roles/nova_flavor/tasks/main.yml | 0 .../tests/ansible/roles/object/tasks/main.yml | 0 .../tests/ansible/roles/port/tasks/main.yml | 0 .../tests/ansible/roles/port/vars/main.yml | 0 .../tests/ansible/roles/router/tasks/main.yml | 0 .../tests/ansible/roles/router/vars/main.yml | 0 .../roles/security_group/tasks/main.yml | 0 .../roles/security_group/vars/main.yml | 0 .../tests/ansible/roles/server/tasks/main.yml | 0 .../tests/ansible/roles/server/vars/main.yaml | 0 .../tests/ansible/roles/subnet/tasks/main.yml | 0 .../tests/ansible/roles/subnet/vars/main.yml | 0 .../tests/ansible/roles/user/tasks/main.yml | 0 .../ansible/roles/user_group/tasks/main.yml | 0 .../tests/ansible/roles/volume/tasks/main.yml | 0 openstack/{cloud => }/tests/ansible/run.yml | 0 openstack/{cloud => }/tests/base.py | 0 openstack/{cloud => }/tests/fakes.py | 0 .../functional/cloud}/__init__.py | 0 .../functional/cloud}/base.py | 10 +- .../functional/cloud}/hooks/post_test_hook.sh | 10 +- .../functional/cloud}/test_aggregate.py | 0 .../cloud}/test_cluster_templates.py | 0 .../functional/cloud}/test_compute.py | 0 .../functional/cloud}/test_devstack.py | 5 +- .../functional/cloud}/test_domain.py | 0 .../functional/cloud}/test_endpoints.py | 0 .../functional/cloud}/test_flavor.py | 0 .../functional/cloud}/test_floating_ip.py | 0 .../cloud}/test_floating_ip_pool.py | 0 .../functional/cloud}/test_groups.py | 0 .../functional/cloud}/test_identity.py | 0 .../functional/cloud}/test_image.py | 0 .../functional/cloud}/test_inventory.py | 0 .../functional/cloud}/test_keypairs.py | 0 .../functional/cloud}/test_limits.py | 0 .../functional/cloud}/test_magnum_services.py | 0 .../functional/cloud}/test_network.py | 0 .../functional/cloud}/test_object.py | 0 .../functional/cloud}/test_port.py | 0 .../functional/cloud}/test_project.py | 0 .../cloud}/test_qos_bandwidth_limit_rule.py | 0 .../cloud}/test_qos_dscp_marking_rule.py | 0 .../cloud}/test_qos_minimum_bandwidth_rule.py | 0 .../functional/cloud}/test_qos_policy.py | 0 .../functional/cloud}/test_quotas.py | 0 .../functional/cloud}/test_range_search.py | 0 .../functional/cloud}/test_recordset.py | 0 .../functional/cloud}/test_router.py | 0 .../functional/cloud}/test_security_groups.py | 0 .../functional/cloud}/test_server_group.py | 0 .../functional/cloud}/test_services.py | 0 .../functional/cloud}/test_stack.py | 0 .../functional/cloud}/test_usage.py | 0 .../functional/cloud}/test_users.py | 0 .../functional/cloud}/test_volume.py | 0 .../functional/cloud}/test_volume_backup.py | 0 .../functional/cloud}/test_volume_type.py | 0 .../functional/cloud}/test_zone.py | 0 .../functional/cloud}/util.py | 2 +- openstack/tests/unit/base.py | 642 ++++++++++++++++- .../unit => tests/unit/cloud}/__init__.py | 0 .../unit/cloud}/test__adapter.py | 0 .../unit => tests/unit/cloud}/test__utils.py | 0 .../unit/cloud}/test_aggregate.py | 0 .../unit/cloud}/test_availability_zones.py | 0 .../unit/cloud}/test_baremetal_node.py | 0 .../unit => tests/unit/cloud}/test_caching.py | 0 .../unit/cloud}/test_cluster_templates.py | 0 .../unit/cloud}/test_create_server.py | 0 .../cloud}/test_create_volume_snapshot.py | 0 .../unit/cloud}/test_delete_server.py | 0 .../cloud}/test_delete_volume_snapshot.py | 0 .../unit/cloud}/test_domain_params.py | 0 .../unit => tests/unit/cloud}/test_domains.py | 0 .../unit/cloud}/test_endpoints.py | 0 .../unit => tests/unit/cloud}/test_flavors.py | 0 .../unit/cloud}/test_floating_ip_common.py | 0 .../unit/cloud}/test_floating_ip_neutron.py | 0 .../unit/cloud}/test_floating_ip_nova.py | 0 .../unit/cloud}/test_floating_ip_pool.py | 0 .../unit => tests/unit/cloud}/test_groups.py | 0 .../unit/cloud}/test_identity_roles.py | 0 .../unit => tests/unit/cloud}/test_image.py | 0 .../unit/cloud}/test_image_snapshot.py | 0 .../unit/cloud}/test_inventory.py | 0 .../unit => tests/unit/cloud}/test_keypair.py | 0 .../unit => tests/unit/cloud}/test_limits.py | 0 .../unit/cloud}/test_magnum_services.py | 0 .../unit => tests/unit/cloud}/test_meta.py | 0 .../unit => tests/unit/cloud}/test_network.py | 0 .../unit/cloud}/test_normalize.py | 0 .../unit => tests/unit/cloud}/test_object.py | 0 .../unit/cloud}/test_operator_noauth.py | 0 .../unit => tests/unit/cloud}/test_port.py | 0 .../unit => tests/unit/cloud}/test_project.py | 0 .../cloud}/test_qos_bandwidth_limit_rule.py | 0 .../unit/cloud}/test_qos_dscp_marking_rule.py | 0 .../cloud}/test_qos_minimum_bandwidth_rule.py | 0 .../unit/cloud}/test_qos_policy.py | 0 .../unit/cloud}/test_qos_rule_type.py | 0 .../unit => tests/unit/cloud}/test_quotas.py | 0 .../unit/cloud}/test_rebuild_server.py | 0 .../unit/cloud}/test_recordset.py | 0 .../unit/cloud}/test_role_assignment.py | 0 .../unit => tests/unit/cloud}/test_router.py | 0 .../unit/cloud}/test_security_groups.py | 0 .../unit/cloud}/test_server_console.py | 0 .../cloud}/test_server_delete_metadata.py | 0 .../unit/cloud}/test_server_group.py | 0 .../unit/cloud}/test_server_set_metadata.py | 0 .../unit/cloud}/test_services.py | 0 .../unit => tests/unit/cloud}/test_shade.py | 0 .../unit/cloud}/test_shade_operator.py | 0 .../unit => tests/unit/cloud}/test_stack.py | 0 .../unit => tests/unit/cloud}/test_subnet.py | 0 .../unit/cloud}/test_task_manager.py | 0 .../unit/cloud}/test_update_server.py | 0 .../unit => tests/unit/cloud}/test_usage.py | 0 .../unit => tests/unit/cloud}/test_users.py | 0 .../unit => tests/unit/cloud}/test_volume.py | 0 .../unit/cloud}/test_volume_access.py | 0 .../unit/cloud}/test_volume_backups.py | 0 .../unit => tests/unit/cloud}/test_zone.py | 0 .../tests/unit/fixtures/baremetal.json | 0 .../tests/unit/fixtures/catalog-v2.json | 0 .../tests/unit/fixtures/catalog-v3.json | 0 .../tests/unit/fixtures/clouds/clouds.yaml | 0 .../unit/fixtures/clouds/clouds_cache.yaml | 0 .../tests/unit/fixtures/discovery.json | 0 .../{cloud => }/tests/unit/fixtures/dns.json | 0 .../unit/fixtures/image-version-broken.json | 0 .../tests/unit/fixtures/image-version-v1.json | 0 .../tests/unit/fixtures/image-version-v2.json | 0 .../tests/unit/fixtures/image-version.json | 0 os-client-config/.coveragerc | 7 - os-client-config/.gitignore | 55 -- os-client-config/.gitreview | 4 - os-client-config/.mailmap | 3 - os-client-config/.stestr.conf | 3 - os-client-config/.testr.conf | 7 - os-client-config/CONTRIBUTING.rst | 16 - os-client-config/HACKING.rst | 4 - os-client-config/LICENSE | 175 ----- os-client-config/README.rst | 25 - os-client-config/doc/source/conf.py | 86 --- .../doc/source/contributor/index.rst | 4 - os-client-config/doc/source/index.rst | 32 - os-client-config/doc/source/install/index.rst | 12 - .../doc/source/user/releasenotes.rst | 6 - os-client-config/requirements.txt | 7 - os-client-config/setup.cfg | 35 - os-client-config/setup.py | 29 - os-client-config/test-requirements.txt | 21 - os-client-config/tox.ini | 45 -- releasenotes/source/conf.py | 8 +- requirements.txt | 18 +- setup.cfg | 1 - shade/.coveragerc | 7 - shade/.gitignore | 54 -- shade/.gitreview | 4 - shade/.mailmap | 4 - shade/.stestr.conf | 3 - shade/.zuul.yaml | 8 - shade/CONTRIBUTING.rst | 44 -- shade/HACKING.rst | 49 -- shade/LICENSE | 175 ----- shade/MANIFEST.in | 6 - shade/README.rst | 79 --- shade/devstack/plugin.sh | 54 -- shade/doc/source/conf.py | 47 -- shade/doc/source/contributor/index.rst | 9 - shade/doc/source/index.rst | 31 - shade/doc/source/install/index.rst | 12 - shade/doc/source/releasenotes/index.rst | 5 - shade/requirements.txt | 26 - shade/setup.cfg | 28 - shade/setup.py | 29 - shade/test-requirements.txt | 17 - shade/tools/tox_install.sh | 30 - shade/tox.ini | 80 --- test-requirements.txt | 12 +- .../tools => tools}/keystone_version.py | 0 .../tools => tools}/nova_version.py | 0 .../tools => tools}/tox_install.sh | 0 tox.ini | 61 +- 259 files changed, 1052 insertions(+), 2206 deletions(-) create mode 100644 .stestr.conf delete mode 100644 .testr.conf rename shade/bindep.txt => bindep.txt (100%) create mode 100644 devstack/plugin.sh rename doc/source/{contributors => contributor}/clouds.yaml (100%) rename {shade/doc => doc}/source/contributor/coding.rst (70%) rename {shade/doc => doc}/source/contributor/contributing.rst (100%) rename doc/source/{contributors => contributor}/create/examples/resource/fake.py (100%) rename doc/source/{contributors => contributor}/create/examples/resource/fake_service.py (100%) rename doc/source/{contributors => contributor}/create/resource.rst (100%) rename doc/source/{contributors => contributor}/index.rst (89%) rename doc/source/{contributors => contributor}/layout.rst (100%) rename doc/source/{contributors => contributor}/layout.txt (100%) rename doc/source/{contributors => contributor}/local.conf (100%) rename doc/source/{contributors => contributor}/setup.rst (100%) rename doc/source/{contributors => contributor}/testing.rst (100%) delete mode 100644 doc/source/history.rst create mode 100644 doc/source/install/index.rst create mode 100644 doc/source/releasenotes.rst rename {os-client-config/doc/source/user => doc/source/user/config}/configuration.rst (100%) rename {os-client-config/doc/source/user => doc/source/user/config}/index.rst (91%) rename {os-client-config/doc/source/user => doc/source/user/config}/network-config.rst (100%) rename os-client-config/doc/source/reference/index.rst => doc/source/user/config/reference.rst (100%) rename {os-client-config/doc/source/user => doc/source/user/config}/using.rst (100%) rename {os-client-config/doc/source/user => doc/source/user/config}/vendor-support.rst (100%) rename {shade/doc => doc}/source/user/examples/cleanup-servers.py (100%) rename {shade/doc => doc}/source/user/examples/create-server-dict.py (100%) rename {shade/doc => doc}/source/user/examples/create-server-name-or-id.py (100%) rename {shade/doc => doc}/source/user/examples/debug-logging.py (100%) rename {shade/doc => doc}/source/user/examples/find-an-image.py (100%) rename {shade/doc => doc}/source/user/examples/http-debug-logging.py (100%) rename {shade/doc => doc}/source/user/examples/munch-dict-object.py (100%) rename {shade/doc => doc}/source/user/examples/normalization.py (100%) rename {shade/doc => doc}/source/user/examples/server-information.py (100%) rename {shade/doc => doc}/source/user/examples/service-conditional-overrides.py (100%) rename {shade/doc => doc}/source/user/examples/service-conditionals.py (100%) rename {shade/doc => doc}/source/user/examples/strict-mode.py (100%) rename {shade/doc => doc}/source/user/examples/upload-large-object.py (100%) rename {shade/doc => doc}/source/user/examples/upload-object.py (100%) rename {shade/doc => doc}/source/user/examples/user-agent.py (100%) rename {shade/doc => doc}/source/user/index.rst (95%) rename {shade/doc => doc}/source/user/logging.rst (100%) rename {shade/doc => doc}/source/user/microversions.rst (100%) rename {shade/doc => doc}/source/user/model.rst (100%) rename {shade/doc => doc}/source/user/multi-cloud-demo.rst (100%) rename {shade/doc => doc}/source/user/usage.rst (100%) rename {shade/extras => extras}/delete-network.sh (100%) rename {shade/extras => extras}/install-tips.sh (100%) rename {shade/extras => extras}/run-ansible-tests.sh (92%) delete mode 100644 openstack/cloud/tests/unit/base.py rename openstack/{cloud => }/tests/ansible/README.txt (100%) rename openstack/{cloud => }/tests/ansible/hooks/post_test_hook.sh (81%) rename openstack/{cloud => }/tests/ansible/roles/auth/tasks/main.yml (100%) rename openstack/{cloud => }/tests/ansible/roles/client_config/tasks/main.yml (100%) rename openstack/{cloud => }/tests/ansible/roles/group/tasks/main.yml (100%) rename openstack/{cloud => }/tests/ansible/roles/group/vars/main.yml (100%) rename openstack/{cloud => }/tests/ansible/roles/image/tasks/main.yml (100%) rename openstack/{cloud => }/tests/ansible/roles/image/vars/main.yml (100%) rename openstack/{cloud => }/tests/ansible/roles/keypair/tasks/main.yml (100%) rename openstack/{cloud => }/tests/ansible/roles/keypair/vars/main.yml (100%) rename openstack/{cloud => }/tests/ansible/roles/keystone_domain/tasks/main.yml (100%) rename openstack/{cloud => }/tests/ansible/roles/keystone_domain/vars/main.yml (100%) rename openstack/{cloud => }/tests/ansible/roles/keystone_role/tasks/main.yml (100%) rename openstack/{cloud => }/tests/ansible/roles/keystone_role/vars/main.yml (100%) rename openstack/{cloud => }/tests/ansible/roles/network/tasks/main.yml (100%) rename openstack/{cloud => }/tests/ansible/roles/network/vars/main.yml (100%) rename openstack/{cloud => }/tests/ansible/roles/nova_flavor/tasks/main.yml (100%) rename openstack/{cloud => }/tests/ansible/roles/object/tasks/main.yml (100%) rename openstack/{cloud => }/tests/ansible/roles/port/tasks/main.yml (100%) rename openstack/{cloud => }/tests/ansible/roles/port/vars/main.yml (100%) rename openstack/{cloud => }/tests/ansible/roles/router/tasks/main.yml (100%) rename openstack/{cloud => }/tests/ansible/roles/router/vars/main.yml (100%) rename openstack/{cloud => }/tests/ansible/roles/security_group/tasks/main.yml (100%) rename openstack/{cloud => }/tests/ansible/roles/security_group/vars/main.yml (100%) rename openstack/{cloud => }/tests/ansible/roles/server/tasks/main.yml (100%) rename openstack/{cloud => }/tests/ansible/roles/server/vars/main.yaml (100%) rename openstack/{cloud => }/tests/ansible/roles/subnet/tasks/main.yml (100%) rename openstack/{cloud => }/tests/ansible/roles/subnet/vars/main.yml (100%) rename openstack/{cloud => }/tests/ansible/roles/user/tasks/main.yml (100%) rename openstack/{cloud => }/tests/ansible/roles/user_group/tasks/main.yml (100%) rename openstack/{cloud => }/tests/ansible/roles/volume/tasks/main.yml (100%) rename openstack/{cloud => }/tests/ansible/run.yml (100%) rename openstack/{cloud => }/tests/base.py (100%) rename openstack/{cloud => }/tests/fakes.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/__init__.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/base.py (90%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/hooks/post_test_hook.sh (88%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_aggregate.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_cluster_templates.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_compute.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_devstack.py (89%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_domain.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_endpoints.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_flavor.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_floating_ip.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_floating_ip_pool.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_groups.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_identity.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_image.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_inventory.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_keypairs.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_limits.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_magnum_services.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_network.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_object.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_port.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_project.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_qos_bandwidth_limit_rule.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_qos_dscp_marking_rule.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_qos_minimum_bandwidth_rule.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_qos_policy.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_quotas.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_range_search.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_recordset.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_router.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_security_groups.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_server_group.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_services.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_stack.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_usage.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_users.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_volume.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_volume_backup.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_volume_type.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/test_zone.py (100%) rename openstack/{cloud/tests/functional => tests/functional/cloud}/util.py (95%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/__init__.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test__adapter.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test__utils.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_aggregate.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_availability_zones.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_baremetal_node.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_caching.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_cluster_templates.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_create_server.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_create_volume_snapshot.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_delete_server.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_delete_volume_snapshot.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_domain_params.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_domains.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_endpoints.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_flavors.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_floating_ip_common.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_floating_ip_neutron.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_floating_ip_nova.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_floating_ip_pool.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_groups.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_identity_roles.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_image.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_image_snapshot.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_inventory.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_keypair.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_limits.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_magnum_services.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_meta.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_network.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_normalize.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_object.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_operator_noauth.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_port.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_project.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_qos_bandwidth_limit_rule.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_qos_dscp_marking_rule.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_qos_minimum_bandwidth_rule.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_qos_policy.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_qos_rule_type.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_quotas.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_rebuild_server.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_recordset.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_role_assignment.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_router.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_security_groups.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_server_console.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_server_delete_metadata.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_server_group.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_server_set_metadata.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_services.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_shade.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_shade_operator.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_stack.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_subnet.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_task_manager.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_update_server.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_usage.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_users.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_volume.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_volume_access.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_volume_backups.py (100%) rename openstack/{cloud/tests/unit => tests/unit/cloud}/test_zone.py (100%) rename openstack/{cloud => }/tests/unit/fixtures/baremetal.json (100%) rename openstack/{cloud => }/tests/unit/fixtures/catalog-v2.json (100%) rename openstack/{cloud => }/tests/unit/fixtures/catalog-v3.json (100%) rename openstack/{cloud => }/tests/unit/fixtures/clouds/clouds.yaml (100%) rename openstack/{cloud => }/tests/unit/fixtures/clouds/clouds_cache.yaml (100%) rename openstack/{cloud => }/tests/unit/fixtures/discovery.json (100%) rename openstack/{cloud => }/tests/unit/fixtures/dns.json (100%) rename openstack/{cloud => }/tests/unit/fixtures/image-version-broken.json (100%) rename openstack/{cloud => }/tests/unit/fixtures/image-version-v1.json (100%) rename openstack/{cloud => }/tests/unit/fixtures/image-version-v2.json (100%) rename openstack/{cloud => }/tests/unit/fixtures/image-version.json (100%) delete mode 100644 os-client-config/.coveragerc delete mode 100644 os-client-config/.gitignore delete mode 100644 os-client-config/.gitreview delete mode 100644 os-client-config/.mailmap delete mode 100644 os-client-config/.stestr.conf delete mode 100644 os-client-config/.testr.conf delete mode 100644 os-client-config/CONTRIBUTING.rst delete mode 100644 os-client-config/HACKING.rst delete mode 100644 os-client-config/LICENSE delete mode 100644 os-client-config/README.rst delete mode 100755 os-client-config/doc/source/conf.py delete mode 100644 os-client-config/doc/source/contributor/index.rst delete mode 100644 os-client-config/doc/source/index.rst delete mode 100644 os-client-config/doc/source/install/index.rst delete mode 100644 os-client-config/doc/source/user/releasenotes.rst delete mode 100644 os-client-config/requirements.txt delete mode 100644 os-client-config/setup.cfg delete mode 100644 os-client-config/setup.py delete mode 100644 os-client-config/test-requirements.txt delete mode 100644 os-client-config/tox.ini delete mode 100644 shade/.coveragerc delete mode 100644 shade/.gitignore delete mode 100644 shade/.gitreview delete mode 100644 shade/.mailmap delete mode 100644 shade/.stestr.conf delete mode 100644 shade/.zuul.yaml delete mode 100644 shade/CONTRIBUTING.rst delete mode 100644 shade/HACKING.rst delete mode 100644 shade/LICENSE delete mode 100644 shade/MANIFEST.in delete mode 100644 shade/README.rst delete mode 100644 shade/devstack/plugin.sh delete mode 100755 shade/doc/source/conf.py delete mode 100644 shade/doc/source/contributor/index.rst delete mode 100644 shade/doc/source/index.rst delete mode 100644 shade/doc/source/install/index.rst delete mode 100644 shade/doc/source/releasenotes/index.rst delete mode 100644 shade/requirements.txt delete mode 100644 shade/setup.cfg delete mode 100644 shade/setup.py delete mode 100644 shade/test-requirements.txt delete mode 100755 shade/tools/tox_install.sh delete mode 100644 shade/tox.ini rename {os-client-config/tools => tools}/keystone_version.py (100%) rename {os-client-config/tools => tools}/nova_version.py (100%) rename {os-client-config/tools => tools}/tox_install.sh (100%) diff --git a/.gitignore b/.gitignore index f8b6eb209..70c3c4095 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ cover/* .tox nosetests.xml .testrepository +.stestr # Translations *.mo diff --git a/.mailmap b/.mailmap index cc92f17b8..c7b4804d7 100644 --- a/.mailmap +++ b/.mailmap @@ -1,3 +1,6 @@ # Format is: # -# \ No newline at end of file +# + + + diff --git a/.stestr.conf b/.stestr.conf new file mode 100644 index 000000000..9ac1db7de --- /dev/null +++ b/.stestr.conf @@ -0,0 +1,3 @@ +[DEFAULT] +test_path=./openstack/cloud/tests/unit +top_dir=./ diff --git a/.testr.conf b/.testr.conf deleted file mode 100644 index 499b7a370..000000000 --- a/.testr.conf +++ /dev/null @@ -1,8 +0,0 @@ -[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:-./openstack/tests/unit} $LISTOPT $IDOPTION -test_id_option=--load-list $IDFILE -test_list_option=--list -group_regex=([^\.]+\.)+ diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 8fee79414..e74b8dc19 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,16 +1,45 @@ -If you would like to contribute to the development of OpenStack, -you must follow the steps in this page: +.. _contributing: - http://docs.openstack.org/infra/manual/developers.html +=================================== +Contributing to python-openstacksdk +=================================== -Once those steps have been completed, changes to OpenStack -should be submitted for review via the Gerrit tool, following -the workflow documented at: +If you're interested in contributing to the python-openstacksdk project, +the following will help get you started. - http://docs.openstack.org/infra/manual/developers.html#development-workflow +Contributor License Agreement +----------------------------- +.. index:: + single: license; agreement + +In order to contribute to the python-openstacksdk project, you need to have +signed OpenStack's contributor's agreement. + +Please read `DeveloperWorkflow`_ before sending your first patch for review. Pull requests submitted through GitHub will be ignored. -Bugs should be filed on Launchpad, not GitHub: +.. seealso:: + + * http://wiki.openstack.org/HowToContribute + * http://wiki.openstack.org/CLA + +.. _DeveloperWorkflow: http://docs.openstack.org/infra/manual/developers.html#development-workflow + +Project Hosting Details +------------------------- + +Project Documentation + http://docs.openstack.org/sdks/python/openstacksdk/ + +Bug tracker + http://storyboard.openstack.org + +Mailing list (prefix subjects with ``[sdk]`` for faster responses) + http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-dev + +Code Hosting + https://git.openstack.org/cgit/openstack/python-openstacksdk - https://bugs.launchpad.net/python-openstacksdk \ No newline at end of file +Code Review + https://review.openstack.org/#/q/status:open+project:openstack/python-openstacksdk,n,z diff --git a/HACKING.rst b/HACKING.rst index e7627c519..c9a0b9682 100644 --- a/HACKING.rst +++ b/HACKING.rst @@ -1,4 +1,49 @@ -python-openstacksdk Style Commandments -====================================== +openstacksdk Style Commandments +=============================== -Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/ +Read the OpenStack Style Commandments +http://docs.openstack.org/developer/hacking/ + +Indentation +----------- + +PEP-8 allows for 'visual' indentation. Do not use it. Visual indentation looks +like this: + +.. code-block:: python + + return_value = self.some_method(arg1, arg1, + arg3, arg4) + +Visual indentation makes refactoring the code base unneccesarily hard. + +Instead of visual indentation, use this: + +.. code-block:: python + + return_value = self.some_method( + arg1, arg1, arg3, arg4) + +That way, if some_method ever needs to be renamed, the only line that needs +to be touched is the line with some_method. Additionaly, if you need to +line break at the top of a block, please indent the continuation line +an additional 4 spaces, like this: + +.. code-block:: python + + for val in self.some_method( + arg1, arg1, arg3, arg4): + self.do_something_awesome() + +Neither of these are 'mandated' by PEP-8. However, they are prevailing styles +within this code base. + +Unit Tests +---------- + +Unit tests should be virtually instant. If a unit test takes more than 1 second +to run, it is a bad unit test. Honestly, 1 second is too slow. + +All unit test classes should subclass `openstack.cloud.tests.unit.base.BaseTestCase`. The +base TestCase class takes care of properly creating `OpenStackCloud` objects +in a way that protects against local environment. diff --git a/README.rst b/README.rst index a75f448a6..88575d3b6 100644 --- a/README.rst +++ b/README.rst @@ -1,36 +1,119 @@ -OpenStack Python SDK -==================== +openstacksdk +============ -The ``python-openstacksdk`` is a collection of libraries for building -applications to work with OpenStack clouds. The project aims to provide -a consistent and complete set of interactions with OpenStack's many -services, along with complete documentation, examples, and tools. +openstacksdk is a client library for for building applications to work +with OpenStack clouds. The project aims to provide a consistent and +complete set of interactions with OpenStack's many services, along with +complete documentation, examples, and tools. -This SDK is under active development, and in the interests of providing -a high-quality interface, the APIs provided in this release may differ -from those provided in future release. +It also contains a simple interface layer. Clouds can do many things, but +there are probably only about 10 of them that most people care about with any +regularity. If you want to do complicated things, the per-service oriented +portions of the SDK are for you. However, if what you want is to be able to +write an application that talks to clouds no matter what crazy choices the +deployer has made in an attempt to be more hipster than their self-entitled +narcissist peers, then the ``openstack.cloud`` layer is for you. -Usage ------ +A Brief History +--------------- -The following example simply connects to an OpenStack cloud and lists -the containers in the Object Store service.:: +openstacksdk started its life as three different libraries: shade, +os-client-config and python-openstacksdk. - from openstack import connection - conn = connection.Connection(auth_url="http://openstack:5000/v3", - project_name="big_project", - username="SDK_user", - password="Super5ecretPassw0rd") - for container in conn.object_store.containers(): - print(container.name) +``shade`` started its life as some code inside of OpenStack Infra's nodepool +project, and as some code inside of Ansible. Ansible had a bunch of different +OpenStack related modules, and there was a ton of duplicated code. Eventually, +between refactoring that duplication into an internal library, and adding logic +and features that the OpenStack Infra team had developed to run client +applications at scale, it turned out that we'd written nine-tenths of what we'd +need to have a standalone library. -Documentation -------------- +``os-client-config`` was a library for collecting client configuration for +using an OpenStack cloud in a consistent and comprehensive manner. +In parallel, the python-openstacksdk team was working on a library to expose +the OpenStack APIs to developers in a consistent and predictable manner. After +a while it became clear that there was value in both a high-level layer that +contains business logic, a lower-level SDK that exposes services and their +resources as Python objects, and also to be able to make direct REST calls +when needed with a properly configured Session or Adapter from python-requests. +This led to the merger of the three projects. -Documentation is available at -http://developer.openstack.org/sdks/python/openstacksdk/ +The contents of the shade library have been moved into ``openstack.cloud`` +and os-client-config has been moved in to ``openstack.config``. The next +release of shade will be a thin compatibility layer that subclasses the objects +from ``openstack.cloud`` and provides different argument defaults where needed +for compat. Similarly the next release of os-client-config will be a compat +layer shim around ``openstack.config``. -License -------- +openstack.config +================ -Apache 2.0 +``openstack.config`` will find cloud configuration for as few as 1 clouds and +as many as you want to put in a config file. It will read environment variables +and config files, and it also contains some vendor specific default values so +that you don't have to know extra info to use OpenStack + +* If you have a config file, you will get the clouds listed in it +* If you have environment variables, you will get a cloud named `envvars` +* If you have neither, you will get a cloud named `defaults` with base defaults + +Sometimes an example is nice. + +Create a ``clouds.yaml`` file: + +.. code-block:: yaml + + clouds: + mordred: + region_name: Dallas + auth: + username: 'mordred' + password: XXXXXXX + project_name: 'shade' + auth_url: 'https://identity.example.com' + +Please note: ``openstack.config`` will look for a file called ``clouds.yaml`` +in the following locations: + +* Current Directory +* ``~/.config/openstack`` +* ``/etc/openstack`` + +More information at https://developer.openstack.org/sdks/python/openstacksdk/users/config + +openstack.cloud +=============== + +Create a server using objects configured with the ``clouds.yaml`` file: + +.. code-block:: python + + import openstack.cloud + + # Initialize and turn on debug logging + openstack.cloud.simple_logging(debug=True) + + # Initialize cloud + # Cloud configs are read with openstack.config + cloud = openstack.cloud.openstack_cloud(cloud='mordred') + + # Upload an image to the cloud + image = cloud.create_image( + 'ubuntu-trusty', filename='ubuntu-trusty.qcow2', wait=True) + + # Find a flavor with at least 512M of RAM + flavor = cloud.get_flavor_by_ram(512) + + # Boot a server, wait for it to boot, and then do whatever is needed + # to get a public ip for it. + cloud.create_server( + 'my-server', image=image, flavor=flavor, wait=True, auto_ip=True) + +Links +===== + +* `Issue Tracker `_ +* `Code Review `_ +* `Documentation `_ +* `PyPI `_ +* `Mailing list `_ diff --git a/shade/bindep.txt b/bindep.txt similarity index 100% rename from shade/bindep.txt rename to bindep.txt diff --git a/devstack/plugin.sh b/devstack/plugin.sh new file mode 100644 index 000000000..4a710af2f --- /dev/null +++ b/devstack/plugin.sh @@ -0,0 +1,54 @@ +# Install and configure **openstacksdk** library in devstack +# +# To enable openstacksdk in devstack add an entry to local.conf that looks like +# +# [[local|localrc]] +# enable_plugin openstacksdk git://git.openstack.org/openstack/python-openstacksdk + +function preinstall_openstacksdk { + : +} + +function install_openstacksdk { + if use_library_from_git "python-openstacksdk"; then + # don't clone, it'll be done by the plugin install + setup_dev_lib "python-openstacksdk" + else + pip_install "python-openstacksdk" + fi +} + +function configure_openstacksdk { + : +} + +function initialize_openstacksdk { + : +} + +function unstack_openstacksdk { + : +} + +function clean_openstacksdk { + : +} + +# This is the main for plugin.sh +if [[ "$1" == "stack" && "$2" == "pre-install" ]]; then + preinstall_openstacksdk +elif [[ "$1" == "stack" && "$2" == "install" ]]; then + install_openstacksdk +elif [[ "$1" == "stack" && "$2" == "post-config" ]]; then + configure_openstacksdk +elif [[ "$1" == "stack" && "$2" == "extra" ]]; then + initialize_openstacksdk +fi + +if [[ "$1" == "unstack" ]]; then + unstack_openstacksdk +fi + +if [[ "$1" == "clean" ]]; then + clean_openstacksdk +fi diff --git a/doc/source/conf.py b/doc/source/conf.py index 6c4cbcf16..31351b480 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -19,16 +19,24 @@ sys.path.insert(0, os.path.abspath('../..')) sys.path.insert(0, os.path.abspath('.')) + # -- General configuration ---------------------------------------------------- # 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.intersphinx', + 'openstackdocstheme', 'enforcer' ] +# openstackdocstheme options +repository_name = 'openstack/python-openstacksdk' +bug_project = '760' +bug_tag = '' +html_last_updated_fmt = '%Y-%m-%d %H:%M' +html_theme = 'openstackdocs' + # When True, this will raise an exception that kills sphinx-build. enforcer_warnings_as_errors = True @@ -47,18 +55,7 @@ # General information about the project. project = u'python-openstacksdk' -copyright = u'2015, OpenStack Foundation' - -# 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" and "release" are used by the "log-a-bug" feature -# -# The short X.Y version. -version = '1.0' -# The full version, including alpha/beta/rc tags. -release = '1.0' +copyright = u'2017, Various members of the OpenStack Foundation' # A few variables have to be set for the log-a-bug feature. # giturl: The location of conf.py on Git. Must be set manually. @@ -101,13 +98,6 @@ # -- 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' - -# Add any paths that contain custom themes here, relative to this directory. -html_theme_path = [openstackdocstheme.get_html_theme_path()] - # Don't let openstackdocstheme insert TOCs automatically. theme_include_auto_toc = False @@ -124,9 +114,5 @@ u'OpenStack Foundation', 'manual'), ] -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/3/': None, - 'http://docs.python-requests.org/en/master/': None} - # Include both the class and __init__ docstrings when describing the class autoclass_content = "both" diff --git a/doc/source/contributors/clouds.yaml b/doc/source/contributor/clouds.yaml similarity index 100% rename from doc/source/contributors/clouds.yaml rename to doc/source/contributor/clouds.yaml diff --git a/shade/doc/source/contributor/coding.rst b/doc/source/contributor/coding.rst similarity index 70% rename from shade/doc/source/contributor/coding.rst rename to doc/source/contributor/coding.rst index 851c18b1e..1cb45b9cc 100644 --- a/shade/doc/source/contributor/coding.rst +++ b/doc/source/contributor/coding.rst @@ -1,24 +1,24 @@ -******************************** -Shade Developer Coding Standards -******************************** +======================================== +OpenStack SDK Developer Coding Standards +======================================== In the beginning, there were no guidelines. And it was good. But that didn't last long. As more and more people added more and more code, we realized that we needed a set of coding standards to make sure that -the shade API at least *attempted* to display some form of consistency. +the openstacksdk API at least *attempted* to display some form of consistency. Thus, these coding standards/guidelines were developed. Note that not -all of shade adheres to these standards just yet. Some older code has +all of openstacksdk adheres to these standards just yet. Some older code has not been updated because we need to maintain backward compatibility. Some of it just hasn't been changed yet. But be clear, all new code *must* adhere to these guidelines. -Below are the patterns that we expect Shade developers to follow. +Below are the patterns that we expect openstacksdk developers to follow. Release Notes ============= -Shade uses `reno `_ for +openstacksdk uses `reno `_ for managing its release notes. A new release note should be added to your contribution anytime you add new API calls, fix significant bugs, add new functionality or parameters to existing API calls, or make any @@ -29,8 +29,17 @@ It is *not* necessary to add release notes for minor fixes, such as correction of documentation typos, minor code cleanup or reorganization, or any other change that a user would not notice through normal usage. -API Methods -=========== +Exceptions +========== + +Exceptions should NEVER be wrapped and re-raised inside of a new exception. +This removes important debug information from the user. All of the exceptions +should be raised correctly the first time. + +openstack.cloud API Methods +=========================== + +The `openstack.cloud` layer has some specific rules: - When an API call acts on a resource that has both a unique ID and a name, that API call should accept either identifier with a name_or_id @@ -50,21 +59,8 @@ API Methods - Deleting a resource should return True if the delete succeeded, or False if the resource was not found. -Exceptions -========== - -All underlying client exceptions must be captured and converted to an -`OpenStackCloudException` or one of its derivatives. - -REST Calls -============ - -All interactions with the cloud should be done with direct REST using -the appropriate `keystoneauth1.adapter.Adapter`. See Glance and Swift -calls for examples. - Returned Resources -================== +------------------ Complex objects returned to the caller must be a `munch.Munch` type. The `openstack.cloud._adapter.Adapter` class makes resources into `munch.Munch`. @@ -72,19 +68,20 @@ Complex objects returned to the caller must be a `munch.Munch` type. The All objects should be normalized. It is shade's purpose in life to make OpenStack consistent for end users, and this means not trusting the clouds to return consistent objects. There should be a normalize function in -`shade/_normalize.py` that is applied to objects before returning them to -the user. See :doc:`../user/model` for further details on object model requirements. +`openstack/cloud/_normalize.py` that is applied to objects before returning +them to the user. See :doc:`../user/model` for further details on object model +requirements. Fields should not be in the normalization contract if we cannot commit to providing them to all users. Fields should be renamed in normalization to be consistent with -the rest of openstack.cloud. For instance, nothing in shade exposes the legacy OpenStack -concept of "tenant" to a user, but instead uses "project" even if the -cloud uses tenant. +the rest of `openstack.cloud`. For instance, nothing in `openstack.cloud` +exposes the legacy OpenStack concept of "tenant" to a user, but instead uses +"project" even if the cloud in question uses tenant. Nova vs. Neutron -================ +---------------- - Recognize that not all cloud providers support Neutron, so never assume it will be present. If a task can be handled by either @@ -101,8 +98,10 @@ Tests - New API methods *must* have unit tests! - New unit tests should only mock at the REST layer using `requests_mock`. - Any mocking of shade itself or of legacy client libraries should be - considered legacy and to be avoided. + Any mocking of openstacksdk itself should be considered legacy and to be + avoided. Exceptions to this rule can be made when attempting to test the + internals of a logical shim where the inputs and output of the method aren't + actually impacted by remote content. - Functional tests should be added, when possible. diff --git a/shade/doc/source/contributor/contributing.rst b/doc/source/contributor/contributing.rst similarity index 100% rename from shade/doc/source/contributor/contributing.rst rename to doc/source/contributor/contributing.rst diff --git a/doc/source/contributors/create/examples/resource/fake.py b/doc/source/contributor/create/examples/resource/fake.py similarity index 100% rename from doc/source/contributors/create/examples/resource/fake.py rename to doc/source/contributor/create/examples/resource/fake.py diff --git a/doc/source/contributors/create/examples/resource/fake_service.py b/doc/source/contributor/create/examples/resource/fake_service.py similarity index 100% rename from doc/source/contributors/create/examples/resource/fake_service.py rename to doc/source/contributor/create/examples/resource/fake_service.py diff --git a/doc/source/contributors/create/resource.rst b/doc/source/contributor/create/resource.rst similarity index 100% rename from doc/source/contributors/create/resource.rst rename to doc/source/contributor/create/resource.rst diff --git a/doc/source/contributors/index.rst b/doc/source/contributor/index.rst similarity index 89% rename from doc/source/contributors/index.rst rename to doc/source/contributor/index.rst index 8b246a245..93e1884f0 100644 --- a/doc/source/contributors/index.rst +++ b/doc/source/contributor/index.rst @@ -13,6 +13,14 @@ software development kit for the programs which make up the OpenStack community. It is a set of Python-based libraries, documentation, examples, and tools released under the Apache 2 license. +Contribution Mechanics +---------------------- + +.. toctree:: + :maxdepth: 2 + + contributing + Contacting the Developers ------------------------- @@ -33,6 +41,17 @@ mailing list fields questions of all types on OpenStack. Using the ``[python-openstacksdk]`` filter to begin your email subject will ensure that the message gets to SDK developers. +Coding Standards +---------------- + +We are a bit stricter than usual in the coding standards department. It's a +good idea to read through the :doc:`coding ` section. + +.. toctree:: + :maxdepth: 2 + + coding + Development Environment ----------------------- diff --git a/doc/source/contributors/layout.rst b/doc/source/contributor/layout.rst similarity index 100% rename from doc/source/contributors/layout.rst rename to doc/source/contributor/layout.rst diff --git a/doc/source/contributors/layout.txt b/doc/source/contributor/layout.txt similarity index 100% rename from doc/source/contributors/layout.txt rename to doc/source/contributor/layout.txt diff --git a/doc/source/contributors/local.conf b/doc/source/contributor/local.conf similarity index 100% rename from doc/source/contributors/local.conf rename to doc/source/contributor/local.conf diff --git a/doc/source/contributors/setup.rst b/doc/source/contributor/setup.rst similarity index 100% rename from doc/source/contributors/setup.rst rename to doc/source/contributor/setup.rst diff --git a/doc/source/contributors/testing.rst b/doc/source/contributor/testing.rst similarity index 100% rename from doc/source/contributors/testing.rst rename to doc/source/contributor/testing.rst diff --git a/doc/source/history.rst b/doc/source/history.rst deleted file mode 100644 index 69ed4fe6c..000000000 --- a/doc/source/history.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../../ChangeLog diff --git a/doc/source/index.rst b/doc/source/index.rst index e2c2bb526..bf7edc804 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -13,6 +13,10 @@ For Users :maxdepth: 2 users/index + install/index + user/index + +.. TODO(shade) merge users/index and user/index into user/index For Contributors ---------------- @@ -20,7 +24,9 @@ For Contributors .. toctree:: :maxdepth: 2 - contributors/index + contributor/index + +.. include:: ../../README.rst General Information ------------------- @@ -31,4 +37,4 @@ General information about the SDK including a glossary and release history. :maxdepth: 1 Glossary of Terms - Release History + Release Notes diff --git a/doc/source/install/index.rst b/doc/source/install/index.rst new file mode 100644 index 000000000..5b06c9812 --- /dev/null +++ b/doc/source/install/index.rst @@ -0,0 +1,12 @@ +============ +Installation +============ + +At the command line:: + + $ pip install python-openstacksdk + +Or, if you have virtualenv wrapper installed:: + + $ mkvirtualenv python-openstacksdk + $ pip install python-openstacksdk diff --git a/doc/source/releasenotes.rst b/doc/source/releasenotes.rst new file mode 100644 index 000000000..a61e4d3e3 --- /dev/null +++ b/doc/source/releasenotes.rst @@ -0,0 +1,6 @@ +============= +Release Notes +============= + +Release notes for `python-openstacksdk` can be found at +http://docs.openstack.org/releasenotes/python-openstacksdk/ diff --git a/os-client-config/doc/source/user/configuration.rst b/doc/source/user/config/configuration.rst similarity index 100% rename from os-client-config/doc/source/user/configuration.rst rename to doc/source/user/config/configuration.rst diff --git a/os-client-config/doc/source/user/index.rst b/doc/source/user/config/index.rst similarity index 91% rename from os-client-config/doc/source/user/index.rst rename to doc/source/user/config/index.rst index ec31c102c..d09b28351 100644 --- a/os-client-config/doc/source/user/index.rst +++ b/doc/source/user/config/index.rst @@ -9,4 +9,4 @@ using vendor-support network-config - releasenotes + reference diff --git a/os-client-config/doc/source/user/network-config.rst b/doc/source/user/config/network-config.rst similarity index 100% rename from os-client-config/doc/source/user/network-config.rst rename to doc/source/user/config/network-config.rst diff --git a/os-client-config/doc/source/reference/index.rst b/doc/source/user/config/reference.rst similarity index 100% rename from os-client-config/doc/source/reference/index.rst rename to doc/source/user/config/reference.rst diff --git a/os-client-config/doc/source/user/using.rst b/doc/source/user/config/using.rst similarity index 100% rename from os-client-config/doc/source/user/using.rst rename to doc/source/user/config/using.rst diff --git a/os-client-config/doc/source/user/vendor-support.rst b/doc/source/user/config/vendor-support.rst similarity index 100% rename from os-client-config/doc/source/user/vendor-support.rst rename to doc/source/user/config/vendor-support.rst diff --git a/shade/doc/source/user/examples/cleanup-servers.py b/doc/source/user/examples/cleanup-servers.py similarity index 100% rename from shade/doc/source/user/examples/cleanup-servers.py rename to doc/source/user/examples/cleanup-servers.py diff --git a/shade/doc/source/user/examples/create-server-dict.py b/doc/source/user/examples/create-server-dict.py similarity index 100% rename from shade/doc/source/user/examples/create-server-dict.py rename to doc/source/user/examples/create-server-dict.py diff --git a/shade/doc/source/user/examples/create-server-name-or-id.py b/doc/source/user/examples/create-server-name-or-id.py similarity index 100% rename from shade/doc/source/user/examples/create-server-name-or-id.py rename to doc/source/user/examples/create-server-name-or-id.py diff --git a/shade/doc/source/user/examples/debug-logging.py b/doc/source/user/examples/debug-logging.py similarity index 100% rename from shade/doc/source/user/examples/debug-logging.py rename to doc/source/user/examples/debug-logging.py diff --git a/shade/doc/source/user/examples/find-an-image.py b/doc/source/user/examples/find-an-image.py similarity index 100% rename from shade/doc/source/user/examples/find-an-image.py rename to doc/source/user/examples/find-an-image.py diff --git a/shade/doc/source/user/examples/http-debug-logging.py b/doc/source/user/examples/http-debug-logging.py similarity index 100% rename from shade/doc/source/user/examples/http-debug-logging.py rename to doc/source/user/examples/http-debug-logging.py diff --git a/shade/doc/source/user/examples/munch-dict-object.py b/doc/source/user/examples/munch-dict-object.py similarity index 100% rename from shade/doc/source/user/examples/munch-dict-object.py rename to doc/source/user/examples/munch-dict-object.py diff --git a/shade/doc/source/user/examples/normalization.py b/doc/source/user/examples/normalization.py similarity index 100% rename from shade/doc/source/user/examples/normalization.py rename to doc/source/user/examples/normalization.py diff --git a/shade/doc/source/user/examples/server-information.py b/doc/source/user/examples/server-information.py similarity index 100% rename from shade/doc/source/user/examples/server-information.py rename to doc/source/user/examples/server-information.py diff --git a/shade/doc/source/user/examples/service-conditional-overrides.py b/doc/source/user/examples/service-conditional-overrides.py similarity index 100% rename from shade/doc/source/user/examples/service-conditional-overrides.py rename to doc/source/user/examples/service-conditional-overrides.py diff --git a/shade/doc/source/user/examples/service-conditionals.py b/doc/source/user/examples/service-conditionals.py similarity index 100% rename from shade/doc/source/user/examples/service-conditionals.py rename to doc/source/user/examples/service-conditionals.py diff --git a/shade/doc/source/user/examples/strict-mode.py b/doc/source/user/examples/strict-mode.py similarity index 100% rename from shade/doc/source/user/examples/strict-mode.py rename to doc/source/user/examples/strict-mode.py diff --git a/shade/doc/source/user/examples/upload-large-object.py b/doc/source/user/examples/upload-large-object.py similarity index 100% rename from shade/doc/source/user/examples/upload-large-object.py rename to doc/source/user/examples/upload-large-object.py diff --git a/shade/doc/source/user/examples/upload-object.py b/doc/source/user/examples/upload-object.py similarity index 100% rename from shade/doc/source/user/examples/upload-object.py rename to doc/source/user/examples/upload-object.py diff --git a/shade/doc/source/user/examples/user-agent.py b/doc/source/user/examples/user-agent.py similarity index 100% rename from shade/doc/source/user/examples/user-agent.py rename to doc/source/user/examples/user-agent.py diff --git a/shade/doc/source/user/index.rst b/doc/source/user/index.rst similarity index 95% rename from shade/doc/source/user/index.rst rename to doc/source/user/index.rst index 0e576834a..26f35a7af 100644 --- a/shade/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -5,6 +5,7 @@ .. toctree:: :maxdepth: 2 + config usage logging model diff --git a/shade/doc/source/user/logging.rst b/doc/source/user/logging.rst similarity index 100% rename from shade/doc/source/user/logging.rst rename to doc/source/user/logging.rst diff --git a/shade/doc/source/user/microversions.rst b/doc/source/user/microversions.rst similarity index 100% rename from shade/doc/source/user/microversions.rst rename to doc/source/user/microversions.rst diff --git a/shade/doc/source/user/model.rst b/doc/source/user/model.rst similarity index 100% rename from shade/doc/source/user/model.rst rename to doc/source/user/model.rst diff --git a/shade/doc/source/user/multi-cloud-demo.rst b/doc/source/user/multi-cloud-demo.rst similarity index 100% rename from shade/doc/source/user/multi-cloud-demo.rst rename to doc/source/user/multi-cloud-demo.rst diff --git a/shade/doc/source/user/usage.rst b/doc/source/user/usage.rst similarity index 100% rename from shade/doc/source/user/usage.rst rename to doc/source/user/usage.rst diff --git a/shade/extras/delete-network.sh b/extras/delete-network.sh similarity index 100% rename from shade/extras/delete-network.sh rename to extras/delete-network.sh diff --git a/shade/extras/install-tips.sh b/extras/install-tips.sh similarity index 100% rename from shade/extras/install-tips.sh rename to extras/install-tips.sh diff --git a/shade/extras/run-ansible-tests.sh b/extras/run-ansible-tests.sh similarity index 92% rename from shade/extras/run-ansible-tests.sh rename to extras/run-ansible-tests.sh index 37573b29d..bda6007c9 100755 --- a/shade/extras/run-ansible-tests.sh +++ b/extras/run-ansible-tests.sh @@ -66,7 +66,7 @@ then echo "Using existing Ansible source repo" else echo "Installing Ansible source repo at $ENVDIR" - git clone --recursive git://github.com/ansible/ansible.git ${ENVDIR}/ansible + git clone --recursive https://github.com/ansible/ansible.git ${ENVDIR}/ansible fi source $ENVDIR/ansible/hacking/env-setup else @@ -91,4 +91,4 @@ then exit 1 fi -ansible-playbook -vvv ./shade/tests/ansible/run.yml -e "cloud=${CLOUD} image=${IMAGE}" ${tag_opt} +ansible-playbook -vvv ./openstack/tests/ansible/run.yml -e "cloud=${CLOUD} image=${IMAGE}" ${tag_opt} diff --git a/openstack/cloud/tests/unit/base.py b/openstack/cloud/tests/unit/base.py deleted file mode 100644 index ffeefe019..000000000 --- a/openstack/cloud/tests/unit/base.py +++ /dev/null @@ -1,646 +0,0 @@ -# Copyright 2010-2011 OpenStack Foundation -# 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. - -import collections -import time -import uuid - -import fixtures -import mock -import os -import openstack.config as occ -from requests import structures -from requests_mock.contrib import fixture as rm_fixture -from six.moves import urllib -import tempfile - -import openstack.cloud.openstackcloud -from openstack.cloud.tests import base - - -_ProjectData = collections.namedtuple( - 'ProjectData', - 'project_id, project_name, enabled, domain_id, description, ' - 'json_response, json_request') - - -_UserData = collections.namedtuple( - 'UserData', - 'user_id, password, name, email, description, domain_id, enabled, ' - 'json_response, json_request') - - -_GroupData = collections.namedtuple( - 'GroupData', - 'group_id, group_name, domain_id, description, json_response, ' - 'json_request') - - -_DomainData = collections.namedtuple( - 'DomainData', - 'domain_id, domain_name, description, json_response, ' - 'json_request') - - -_ServiceData = collections.namedtuple( - 'Servicedata', - 'service_id, service_name, service_type, description, enabled, ' - 'json_response_v3, json_response_v2, json_request') - - -_EndpointDataV3 = collections.namedtuple( - 'EndpointData', - 'endpoint_id, service_id, interface, region, url, enabled, ' - 'json_response, json_request') - - -_EndpointDataV2 = collections.namedtuple( - 'EndpointData', - 'endpoint_id, service_id, region, public_url, internal_url, ' - 'admin_url, v3_endpoint_list, json_response, ' - 'json_request') - - -# NOTE(notmorgan): Shade does not support domain-specific roles -# This should eventually be fixed if it becomes a main-stream feature. -_RoleData = collections.namedtuple( - 'RoleData', - 'role_id, role_name, json_response, json_request') - - -class BaseTestCase(base.TestCase): - - def setUp(self, cloud_config_fixture='clouds.yaml'): - """Run before each test method to initialize test environment.""" - - super(BaseTestCase, self).setUp() - - # Sleeps are for real testing, but unit tests shouldn't need them - realsleep = time.sleep - - def _nosleep(seconds): - return realsleep(seconds * 0.0001) - - self.sleep_fixture = self.useFixture(fixtures.MonkeyPatch( - 'time.sleep', - _nosleep)) - self.fixtures_directory = 'shade/tests/unit/fixtures' - - # Isolate os-client-config from test environment - config = tempfile.NamedTemporaryFile(delete=False) - cloud_path = '%s/clouds/%s' % (self.fixtures_directory, - cloud_config_fixture) - with open(cloud_path, 'rb') as f: - content = f.read() - config.write(content) - config.close() - - vendor = tempfile.NamedTemporaryFile(delete=False) - vendor.write(b'{}') - vendor.close() - - # set record mode depending on environment - record_mode = os.environ.get('BETAMAX_RECORD_FIXTURES', False) - if record_mode: - self.record_fixtures = 'new_episodes' - else: - self.record_fixtures = None - - test_cloud = os.environ.get('SHADE_OS_CLOUD', '_test_cloud_') - self.config = occ.OpenStackConfig( - config_files=[config.name], - vendor_files=[vendor.name], - secure_files=['non-existant']) - self.cloud_config = self.config.get_one_cloud( - cloud=test_cloud, validate=False) - self.cloud = openstack.cloud.OpenStackCloud( - cloud_config=self.cloud_config, - log_inner_exceptions=True) - self.strict_cloud = openstack.cloud.OpenStackCloud( - cloud_config=self.cloud_config, - log_inner_exceptions=True, - strict=True) - self.op_cloud = openstack.cloud.OperatorCloud( - cloud_config=self.cloud_config, - log_inner_exceptions=True) - - -class TestCase(BaseTestCase): - - def setUp(self, cloud_config_fixture='clouds.yaml'): - - super(TestCase, self).setUp(cloud_config_fixture=cloud_config_fixture) - self.session_fixture = self.useFixture(fixtures.MonkeyPatch( - 'os_client_config.cloud_config.CloudConfig.get_session', - mock.Mock())) - - -class RequestsMockTestCase(BaseTestCase): - - def setUp(self, cloud_config_fixture='clouds.yaml'): - - super(RequestsMockTestCase, self).setUp( - cloud_config_fixture=cloud_config_fixture) - - # FIXME(notmorgan): Convert the uri_registry, discovery.json, and - # use of keystone_v3/v2 to a proper fixtures.Fixture. For now this - # is acceptable, but eventually this should become it's own fixture - # that encapsulates the registry, registering the URIs, and - # assert_calls (and calling assert_calls every test case that uses - # it on cleanup). Subclassing here could be 100% eliminated in the - # future allowing any class to simply - # self.useFixture(openstack.cloud.RequestsMockFixture) and get all - # the benefits. - - # NOTE(notmorgan): use an ordered dict here to ensure we preserve the - # order in which items are added to the uri_registry. This makes - # the behavior more consistent when dealing with ensuring the - # requests_mock uri/query_string matchers are ordered and parse the - # request in the correct orders. - self._uri_registry = collections.OrderedDict() - self.discovery_json = os.path.join( - self.fixtures_directory, 'discovery.json') - self.use_keystone_v3() - self.__register_uris_called = False - - def get_mock_url(self, service_type, interface='public', resource=None, - append=None, base_url_append=None, - qs_elements=None): - endpoint_url = self.cloud.endpoint_for( - service_type=service_type, interface=interface) - # Strip trailing slashes, so as not to produce double-slashes below - if endpoint_url.endswith('/'): - endpoint_url = endpoint_url[:-1] - to_join = [endpoint_url] - qs = '' - if base_url_append: - to_join.append(base_url_append) - if resource: - to_join.append(resource) - to_join.extend(append or []) - if qs_elements is not None: - qs = '?%s' % '&'.join(qs_elements) - return '%(uri)s%(qs)s' % {'uri': '/'.join(to_join), 'qs': qs} - - def mock_for_keystone_projects(self, project=None, v3=True, - list_get=False, id_get=False, - project_list=None, project_count=None): - if project: - assert not (project_list or project_count) - elif project_list: - assert not (project or project_count) - elif project_count: - assert not (project or project_list) - else: - raise Exception('Must specify a project, project_list, ' - 'or project_count') - assert list_get or id_get - - base_url_append = 'v3' if v3 else None - if project: - project_list = [project] - elif project_count: - # Generate multiple projects - project_list = [self._get_project_data(v3=v3) - for c in range(0, project_count)] - uri_mock_list = [] - if list_get: - uri_mock_list.append( - dict(method='GET', - uri=self.get_mock_url( - service_type='identity', - interface='admin', - resource='projects', - base_url_append=base_url_append), - status_code=200, - json={'projects': [p.json_response['project'] - for p in project_list]}) - ) - if id_get: - for p in project_list: - uri_mock_list.append( - dict(method='GET', - uri=self.get_mock_url( - service_type='identity', - interface='admin', - resource='projects', - append=[p.project_id], - base_url_append=base_url_append), - status_code=200, - json=p.json_response) - ) - self.__do_register_uris(uri_mock_list) - return project_list - - def _get_project_data(self, project_name=None, enabled=None, - domain_id=None, description=None, v3=True, - project_id=None): - project_name = project_name or self.getUniqueString('projectName') - project_id = uuid.UUID(project_id or uuid.uuid4().hex).hex - response = {'id': project_id, 'name': project_name} - request = {'name': project_name} - domain_id = (domain_id or uuid.uuid4().hex) if v3 else None - if domain_id: - request['domain_id'] = domain_id - response['domain_id'] = domain_id - if enabled is not None: - enabled = bool(enabled) - response['enabled'] = enabled - request['enabled'] = enabled - response.setdefault('enabled', True) - request.setdefault('enabled', True) - if description: - response['description'] = description - request['description'] = description - request.setdefault('description', None) - if v3: - project_key = 'project' - else: - project_key = 'tenant' - return _ProjectData(project_id, project_name, enabled, domain_id, - description, {project_key: response}, - {project_key: request}) - - def _get_group_data(self, name=None, domain_id=None, description=None): - group_id = uuid.uuid4().hex - name = name or self.getUniqueString('groupname') - domain_id = uuid.UUID(domain_id or uuid.uuid4().hex).hex - response = {'id': group_id, 'name': name, 'domain_id': domain_id} - request = {'name': name, 'domain_id': domain_id} - if description is not None: - response['description'] = description - request['description'] = description - - return _GroupData(group_id, name, domain_id, description, - {'group': response}, {'group': request}) - - def _get_user_data(self, name=None, password=None, **kwargs): - - name = name or self.getUniqueString('username') - password = password or self.getUniqueString('user_password') - user_id = uuid.uuid4().hex - - response = {'name': name, 'id': user_id} - request = {'name': name, 'password': password} - - if kwargs.get('domain_id'): - kwargs['domain_id'] = uuid.UUID(kwargs['domain_id']).hex - response['domain_id'] = kwargs.pop('domain_id') - request['domain_id'] = response['domain_id'] - - response['email'] = kwargs.pop('email', None) - request['email'] = response['email'] - - response['enabled'] = kwargs.pop('enabled', True) - request['enabled'] = response['enabled'] - - response['description'] = kwargs.pop('description', None) - if response['description']: - request['description'] = response['description'] - - self.assertIs(0, len(kwargs), message='extra key-word args received ' - 'on _get_user_data') - - return _UserData(user_id, password, name, response['email'], - response['description'], response.get('domain_id'), - response.get('enabled'), {'user': response}, - {'user': request}) - - def _get_domain_data(self, domain_name=None, description=None, - enabled=None): - domain_id = uuid.uuid4().hex - domain_name = domain_name or self.getUniqueString('domainName') - response = {'id': domain_id, 'name': domain_name} - request = {'name': domain_name} - if enabled is not None: - request['enabled'] = bool(enabled) - response['enabled'] = bool(enabled) - if description: - response['description'] = description - request['description'] = description - response.setdefault('enabled', True) - return _DomainData(domain_id, domain_name, description, - {'domain': response}, {'domain': request}) - - def _get_service_data(self, type=None, name=None, description=None, - enabled=True): - service_id = uuid.uuid4().hex - name = name or uuid.uuid4().hex - type = type or uuid.uuid4().hex - - response = {'id': service_id, 'name': name, 'type': type, - 'enabled': enabled} - if description is not None: - response['description'] = description - request = response.copy() - request.pop('id') - return _ServiceData(service_id, name, type, description, enabled, - {'service': response}, - {'OS-KSADM:service': response}, request) - - def _get_endpoint_v3_data(self, service_id=None, region=None, - url=None, interface=None, enabled=True): - endpoint_id = uuid.uuid4().hex - service_id = service_id or uuid.uuid4().hex - region = region or uuid.uuid4().hex - url = url or 'https://example.com/' - interface = interface or uuid.uuid4().hex - - response = {'id': endpoint_id, 'service_id': service_id, - 'region': region, 'interface': interface, - 'url': url, 'enabled': enabled} - request = response.copy() - request.pop('id') - response['region_id'] = response['region'] - return _EndpointDataV3(endpoint_id, service_id, interface, region, - url, enabled, {'endpoint': response}, - {'endpoint': request}) - - def _get_endpoint_v2_data(self, service_id=None, region=None, - public_url=None, admin_url=None, - internal_url=None): - endpoint_id = uuid.uuid4().hex - service_id = service_id or uuid.uuid4().hex - region = region or uuid.uuid4().hex - response = {'id': endpoint_id, 'service_id': service_id, - 'region': region} - v3_endpoints = {} - request = response.copy() - request.pop('id') - if admin_url: - response['adminURL'] = admin_url - v3_endpoints['admin'] = self._get_endpoint_v3_data( - service_id, region, public_url, interface='admin') - if internal_url: - response['internalURL'] = internal_url - v3_endpoints['internal'] = self._get_endpoint_v3_data( - service_id, region, internal_url, interface='internal') - if public_url: - response['publicURL'] = public_url - v3_endpoints['public'] = self._get_endpoint_v3_data( - service_id, region, public_url, interface='public') - request = response.copy() - request.pop('id') - for u in ('publicURL', 'internalURL', 'adminURL'): - if request.get(u): - request[u.lower()] = request.pop(u) - return _EndpointDataV2(endpoint_id, service_id, region, public_url, - internal_url, admin_url, v3_endpoints, - {'endpoint': response}, {'endpoint': request}) - - def _get_role_data(self, role_name=None): - role_id = uuid.uuid4().hex - role_name = role_name or uuid.uuid4().hex - request = {'name': role_name} - response = request.copy() - response['id'] = role_id - return _RoleData(role_id, role_name, {'role': response}, - {'role': request}) - - def use_keystone_v3(self, catalog='catalog-v3.json'): - self.adapter = self.useFixture(rm_fixture.Fixture()) - self.calls = [] - self._uri_registry.clear() - self.__do_register_uris([ - dict(method='GET', uri='https://identity.example.com/', - text=open(self.discovery_json, 'r').read()), - dict(method='POST', - uri='https://identity.example.com/v3/auth/tokens', - headers={ - 'X-Subject-Token': self.getUniqueString('KeystoneToken')}, - text=open(os.path.join( - self.fixtures_directory, catalog), 'r').read() - ), - ]) - self._make_test_cloud(identity_api_version='3') - - def use_keystone_v2(self): - self.adapter = self.useFixture(rm_fixture.Fixture()) - self.calls = [] - self._uri_registry.clear() - - self.__do_register_uris([ - dict(method='GET', uri='https://identity.example.com/', - text=open(self.discovery_json, 'r').read()), - dict(method='POST', uri='https://identity.example.com/v2.0/tokens', - text=open(os.path.join( - self.fixtures_directory, 'catalog-v2.json'), 'r').read() - ), - ]) - - self._make_test_cloud(cloud_name='_test_cloud_v2_', - identity_api_version='2.0') - - def _make_test_cloud(self, cloud_name='_test_cloud_', **kwargs): - test_cloud = os.environ.get('SHADE_OS_CLOUD', cloud_name) - self.cloud_config = self.config.get_one_cloud( - cloud=test_cloud, validate=True, **kwargs) - self.cloud = openstack.cloud.OpenStackCloud( - cloud_config=self.cloud_config, - log_inner_exceptions=True) - self.op_cloud = openstack.cloud.OperatorCloud( - cloud_config=self.cloud_config, - log_inner_exceptions=True) - - def get_glance_discovery_mock_dict( - self, image_version_json='image-version.json'): - discovery_fixture = os.path.join( - self.fixtures_directory, image_version_json) - return dict(method='GET', uri='https://image.example.com/', - status_code=300, - text=open(discovery_fixture, 'r').read()) - - def get_designate_discovery_mock_dict(self): - discovery_fixture = os.path.join( - self.fixtures_directory, "dns.json") - return dict(method='GET', uri="https://dns.example.com/", - text=open(discovery_fixture, 'r').read()) - - def get_ironic_discovery_mock_dict(self): - discovery_fixture = os.path.join( - self.fixtures_directory, "baremetal.json") - return dict(method='GET', uri="https://bare-metal.example.com/", - text=open(discovery_fixture, 'r').read()) - - def use_glance(self, image_version_json='image-version.json'): - # NOTE(notmorgan): This method is only meant to be used in "setUp" - # where the ordering of the url being registered is tightly controlled - # if the functionality of .use_glance is meant to be used during an - # actual test case, use .get_glance_discovery_mock and apply to the - # right location in the mock_uris when calling .register_uris - self.__do_register_uris([ - self.get_glance_discovery_mock_dict(image_version_json)]) - - def use_designate(self): - # NOTE(slaweq): This method is only meant to be used in "setUp" - # where the ordering of the url being registered is tightly controlled - # if the functionality of .use_designate is meant to be used during an - # actual test case, use .get_designate_discovery_mock and apply to the - # right location in the mock_uris when calling .register_uris - self.__do_register_uris([ - self.get_designate_discovery_mock_dict()]) - - def use_ironic(self): - # NOTE(TheJulia): This method is only meant to be used in "setUp" - # where the ordering of the url being registered is tightly controlled - # if the functionality of .use_ironic is meant to be used during an - # actual test case, use .get_ironic_discovery_mock and apply to the - # right location in the mock_uris when calling .register_uris - self.__do_register_uris([ - self.get_ironic_discovery_mock_dict()]) - - def register_uris(self, uri_mock_list=None): - """Mock a list of URIs and responses via requests mock. - - This method may be called only once per test-case to avoid odd - and difficult to debug interactions. Discovery and Auth request mocking - happens separately from this method. - - :param uri_mock_list: List of dictionaries that template out what is - passed to requests_mock fixture's `register_uri`. - Format is: - {'method': , - 'uri': , - ... - } - - Common keys to pass in the dictionary: - * json: the json response (dict) - * status_code: the HTTP status (int) - * validate: The request body (dict) to - validate with assert_calls - all key-word arguments that are valid to send to - requests_mock are supported. - - This list should be in the order in which calls - are made. When `assert_calls` is executed, order - here will be validated. Duplicate URIs and - Methods are allowed and will be collapsed into a - single matcher. Each response will be returned - in order as the URI+Method is hit. - :type uri_mock_list: list - :return: None - """ - assert not self.__register_uris_called - self.__do_register_uris(uri_mock_list or []) - self.__register_uris_called = True - - def __do_register_uris(self, uri_mock_list=None): - for to_mock in uri_mock_list: - kw_params = {k: to_mock.pop(k) - for k in ('request_headers', 'complete_qs', - '_real_http') - if k in to_mock} - - method = to_mock.pop('method') - uri = to_mock.pop('uri') - # NOTE(notmorgan): make sure the delimiter is non-url-safe, in this - # case "|" is used so that the split can be a bit easier on - # maintainers of this code. - key = '{method}|{uri}|{params}'.format( - method=method, uri=uri, params=kw_params) - validate = to_mock.pop('validate', {}) - valid_keys = set(['json', 'headers', 'params']) - invalid_keys = set(validate.keys()) - valid_keys - if invalid_keys: - raise TypeError( - "Invalid values passed to validate: {keys}".format( - keys=invalid_keys)) - headers = structures.CaseInsensitiveDict(to_mock.pop('headers', - {})) - if 'content-type' not in headers: - headers[u'content-type'] = 'application/json' - - to_mock['headers'] = headers - - self.calls += [ - dict( - method=method, - url=uri, **validate) - ] - self._uri_registry.setdefault( - key, {'response_list': [], 'kw_params': kw_params}) - if self._uri_registry[key]['kw_params'] != kw_params: - raise AssertionError( - 'PROGRAMMING ERROR: key-word-params ' - 'should be part of the uri_key and cannot change, ' - 'it will affect the matcher in requests_mock. ' - '%(old)r != %(new)r' % - {'old': self._uri_registry[key]['kw_params'], - 'new': kw_params}) - self._uri_registry[key]['response_list'].append(to_mock) - - for mocked, params in self._uri_registry.items(): - mock_method, mock_uri, _ignored = mocked.split('|', 2) - self.adapter.register_uri( - mock_method, mock_uri, params['response_list'], - **params['kw_params']) - - def assert_calls(self, stop_after=None, do_count=True): - for (x, (call, history)) in enumerate( - zip(self.calls, self.adapter.request_history)): - if stop_after and x > stop_after: - break - - call_uri_parts = urllib.parse.urlparse(call['url']) - history_uri_parts = urllib.parse.urlparse(history.url) - self.assertEqual( - (call['method'], call_uri_parts.scheme, call_uri_parts.netloc, - call_uri_parts.path, call_uri_parts.params, - urllib.parse.parse_qs(call_uri_parts.query)), - (history.method, history_uri_parts.scheme, - history_uri_parts.netloc, history_uri_parts.path, - history_uri_parts.params, - urllib.parse.parse_qs(history_uri_parts.query)), - ('REST mismatch on call %(index)d. Expected %(call)r. ' - 'Got %(history)r). ' - 'NOTE: query string order differences wont cause mismatch' % - { - 'index': x, - 'call': '{method} {url}'.format(method=call['method'], - url=call['url']), - 'history': '{method} {url}'.format( - method=history.method, - url=history.url)}) - ) - if 'json' in call: - self.assertEqual( - call['json'], history.json(), - 'json content mismatch in call {index}'.format(index=x)) - # headers in a call isn't exhaustive - it's checking to make sure - # a specific header or headers are there, not that they are the - # only headers - if 'headers' in call: - for key, value in call['headers'].items(): - self.assertEqual( - value, history.headers[key], - 'header mismatch in call {index}'.format(index=x)) - if do_count: - self.assertEqual( - len(self.calls), len(self.adapter.request_history)) - - -class IronicTestCase(RequestsMockTestCase): - - def setUp(self): - super(IronicTestCase, self).setUp() - self.use_ironic() - self.uuid = str(uuid.uuid4()) - self.name = self.getUniqueString('name') - - def get_mock_url(self, resource=None, append=None, qs_elements=None): - return super(IronicTestCase, self).get_mock_url( - service_type='baremetal', interface='public', resource=resource, - append=append, base_url_append='v1', qs_elements=qs_elements) diff --git a/openstack/cloud/tests/ansible/README.txt b/openstack/tests/ansible/README.txt similarity index 100% rename from openstack/cloud/tests/ansible/README.txt rename to openstack/tests/ansible/README.txt diff --git a/openstack/cloud/tests/ansible/hooks/post_test_hook.sh b/openstack/tests/ansible/hooks/post_test_hook.sh similarity index 81% rename from openstack/cloud/tests/ansible/hooks/post_test_hook.sh rename to openstack/tests/ansible/hooks/post_test_hook.sh index 0c2bbcfd2..6b511719a 100755 --- a/openstack/cloud/tests/ansible/hooks/post_test_hook.sh +++ b/openstack/tests/ansible/hooks/post_test_hook.sh @@ -12,14 +12,16 @@ # License for the specific language governing permissions and limitations # under the License. -export SHADE_DIR="$BASE/new/shade" +# TODO(shade) Rework for Zuul v3 -cd $SHADE_DIR -sudo chown -R jenkins:stack $SHADE_DIR +export OPENSTACKSDK_DIR="$BASE/new/python-openstacksdk" + +cd $OPENSTACKSDK_DIR +sudo chown -R jenkins:stack $OPENSTACKSDK_DIR echo "Running shade Ansible test suite" -if [ ${SHADE_ANSIBLE_DEV:-0} -eq 1 ] +if [ ${OPENSTACKSDK_ANSIBLE_DEV:-0} -eq 1 ] then # Use the upstream development version of Ansible set +e diff --git a/openstack/cloud/tests/ansible/roles/auth/tasks/main.yml b/openstack/tests/ansible/roles/auth/tasks/main.yml similarity index 100% rename from openstack/cloud/tests/ansible/roles/auth/tasks/main.yml rename to openstack/tests/ansible/roles/auth/tasks/main.yml diff --git a/openstack/cloud/tests/ansible/roles/client_config/tasks/main.yml b/openstack/tests/ansible/roles/client_config/tasks/main.yml similarity index 100% rename from openstack/cloud/tests/ansible/roles/client_config/tasks/main.yml rename to openstack/tests/ansible/roles/client_config/tasks/main.yml diff --git a/openstack/cloud/tests/ansible/roles/group/tasks/main.yml b/openstack/tests/ansible/roles/group/tasks/main.yml similarity index 100% rename from openstack/cloud/tests/ansible/roles/group/tasks/main.yml rename to openstack/tests/ansible/roles/group/tasks/main.yml diff --git a/openstack/cloud/tests/ansible/roles/group/vars/main.yml b/openstack/tests/ansible/roles/group/vars/main.yml similarity index 100% rename from openstack/cloud/tests/ansible/roles/group/vars/main.yml rename to openstack/tests/ansible/roles/group/vars/main.yml diff --git a/openstack/cloud/tests/ansible/roles/image/tasks/main.yml b/openstack/tests/ansible/roles/image/tasks/main.yml similarity index 100% rename from openstack/cloud/tests/ansible/roles/image/tasks/main.yml rename to openstack/tests/ansible/roles/image/tasks/main.yml diff --git a/openstack/cloud/tests/ansible/roles/image/vars/main.yml b/openstack/tests/ansible/roles/image/vars/main.yml similarity index 100% rename from openstack/cloud/tests/ansible/roles/image/vars/main.yml rename to openstack/tests/ansible/roles/image/vars/main.yml diff --git a/openstack/cloud/tests/ansible/roles/keypair/tasks/main.yml b/openstack/tests/ansible/roles/keypair/tasks/main.yml similarity index 100% rename from openstack/cloud/tests/ansible/roles/keypair/tasks/main.yml rename to openstack/tests/ansible/roles/keypair/tasks/main.yml diff --git a/openstack/cloud/tests/ansible/roles/keypair/vars/main.yml b/openstack/tests/ansible/roles/keypair/vars/main.yml similarity index 100% rename from openstack/cloud/tests/ansible/roles/keypair/vars/main.yml rename to openstack/tests/ansible/roles/keypair/vars/main.yml diff --git a/openstack/cloud/tests/ansible/roles/keystone_domain/tasks/main.yml b/openstack/tests/ansible/roles/keystone_domain/tasks/main.yml similarity index 100% rename from openstack/cloud/tests/ansible/roles/keystone_domain/tasks/main.yml rename to openstack/tests/ansible/roles/keystone_domain/tasks/main.yml diff --git a/openstack/cloud/tests/ansible/roles/keystone_domain/vars/main.yml b/openstack/tests/ansible/roles/keystone_domain/vars/main.yml similarity index 100% rename from openstack/cloud/tests/ansible/roles/keystone_domain/vars/main.yml rename to openstack/tests/ansible/roles/keystone_domain/vars/main.yml diff --git a/openstack/cloud/tests/ansible/roles/keystone_role/tasks/main.yml b/openstack/tests/ansible/roles/keystone_role/tasks/main.yml similarity index 100% rename from openstack/cloud/tests/ansible/roles/keystone_role/tasks/main.yml rename to openstack/tests/ansible/roles/keystone_role/tasks/main.yml diff --git a/openstack/cloud/tests/ansible/roles/keystone_role/vars/main.yml b/openstack/tests/ansible/roles/keystone_role/vars/main.yml similarity index 100% rename from openstack/cloud/tests/ansible/roles/keystone_role/vars/main.yml rename to openstack/tests/ansible/roles/keystone_role/vars/main.yml diff --git a/openstack/cloud/tests/ansible/roles/network/tasks/main.yml b/openstack/tests/ansible/roles/network/tasks/main.yml similarity index 100% rename from openstack/cloud/tests/ansible/roles/network/tasks/main.yml rename to openstack/tests/ansible/roles/network/tasks/main.yml diff --git a/openstack/cloud/tests/ansible/roles/network/vars/main.yml b/openstack/tests/ansible/roles/network/vars/main.yml similarity index 100% rename from openstack/cloud/tests/ansible/roles/network/vars/main.yml rename to openstack/tests/ansible/roles/network/vars/main.yml diff --git a/openstack/cloud/tests/ansible/roles/nova_flavor/tasks/main.yml b/openstack/tests/ansible/roles/nova_flavor/tasks/main.yml similarity index 100% rename from openstack/cloud/tests/ansible/roles/nova_flavor/tasks/main.yml rename to openstack/tests/ansible/roles/nova_flavor/tasks/main.yml diff --git a/openstack/cloud/tests/ansible/roles/object/tasks/main.yml b/openstack/tests/ansible/roles/object/tasks/main.yml similarity index 100% rename from openstack/cloud/tests/ansible/roles/object/tasks/main.yml rename to openstack/tests/ansible/roles/object/tasks/main.yml diff --git a/openstack/cloud/tests/ansible/roles/port/tasks/main.yml b/openstack/tests/ansible/roles/port/tasks/main.yml similarity index 100% rename from openstack/cloud/tests/ansible/roles/port/tasks/main.yml rename to openstack/tests/ansible/roles/port/tasks/main.yml diff --git a/openstack/cloud/tests/ansible/roles/port/vars/main.yml b/openstack/tests/ansible/roles/port/vars/main.yml similarity index 100% rename from openstack/cloud/tests/ansible/roles/port/vars/main.yml rename to openstack/tests/ansible/roles/port/vars/main.yml diff --git a/openstack/cloud/tests/ansible/roles/router/tasks/main.yml b/openstack/tests/ansible/roles/router/tasks/main.yml similarity index 100% rename from openstack/cloud/tests/ansible/roles/router/tasks/main.yml rename to openstack/tests/ansible/roles/router/tasks/main.yml diff --git a/openstack/cloud/tests/ansible/roles/router/vars/main.yml b/openstack/tests/ansible/roles/router/vars/main.yml similarity index 100% rename from openstack/cloud/tests/ansible/roles/router/vars/main.yml rename to openstack/tests/ansible/roles/router/vars/main.yml diff --git a/openstack/cloud/tests/ansible/roles/security_group/tasks/main.yml b/openstack/tests/ansible/roles/security_group/tasks/main.yml similarity index 100% rename from openstack/cloud/tests/ansible/roles/security_group/tasks/main.yml rename to openstack/tests/ansible/roles/security_group/tasks/main.yml diff --git a/openstack/cloud/tests/ansible/roles/security_group/vars/main.yml b/openstack/tests/ansible/roles/security_group/vars/main.yml similarity index 100% rename from openstack/cloud/tests/ansible/roles/security_group/vars/main.yml rename to openstack/tests/ansible/roles/security_group/vars/main.yml diff --git a/openstack/cloud/tests/ansible/roles/server/tasks/main.yml b/openstack/tests/ansible/roles/server/tasks/main.yml similarity index 100% rename from openstack/cloud/tests/ansible/roles/server/tasks/main.yml rename to openstack/tests/ansible/roles/server/tasks/main.yml diff --git a/openstack/cloud/tests/ansible/roles/server/vars/main.yaml b/openstack/tests/ansible/roles/server/vars/main.yaml similarity index 100% rename from openstack/cloud/tests/ansible/roles/server/vars/main.yaml rename to openstack/tests/ansible/roles/server/vars/main.yaml diff --git a/openstack/cloud/tests/ansible/roles/subnet/tasks/main.yml b/openstack/tests/ansible/roles/subnet/tasks/main.yml similarity index 100% rename from openstack/cloud/tests/ansible/roles/subnet/tasks/main.yml rename to openstack/tests/ansible/roles/subnet/tasks/main.yml diff --git a/openstack/cloud/tests/ansible/roles/subnet/vars/main.yml b/openstack/tests/ansible/roles/subnet/vars/main.yml similarity index 100% rename from openstack/cloud/tests/ansible/roles/subnet/vars/main.yml rename to openstack/tests/ansible/roles/subnet/vars/main.yml diff --git a/openstack/cloud/tests/ansible/roles/user/tasks/main.yml b/openstack/tests/ansible/roles/user/tasks/main.yml similarity index 100% rename from openstack/cloud/tests/ansible/roles/user/tasks/main.yml rename to openstack/tests/ansible/roles/user/tasks/main.yml diff --git a/openstack/cloud/tests/ansible/roles/user_group/tasks/main.yml b/openstack/tests/ansible/roles/user_group/tasks/main.yml similarity index 100% rename from openstack/cloud/tests/ansible/roles/user_group/tasks/main.yml rename to openstack/tests/ansible/roles/user_group/tasks/main.yml diff --git a/openstack/cloud/tests/ansible/roles/volume/tasks/main.yml b/openstack/tests/ansible/roles/volume/tasks/main.yml similarity index 100% rename from openstack/cloud/tests/ansible/roles/volume/tasks/main.yml rename to openstack/tests/ansible/roles/volume/tasks/main.yml diff --git a/openstack/cloud/tests/ansible/run.yml b/openstack/tests/ansible/run.yml similarity index 100% rename from openstack/cloud/tests/ansible/run.yml rename to openstack/tests/ansible/run.yml diff --git a/openstack/cloud/tests/base.py b/openstack/tests/base.py similarity index 100% rename from openstack/cloud/tests/base.py rename to openstack/tests/base.py diff --git a/openstack/cloud/tests/fakes.py b/openstack/tests/fakes.py similarity index 100% rename from openstack/cloud/tests/fakes.py rename to openstack/tests/fakes.py diff --git a/openstack/cloud/tests/functional/__init__.py b/openstack/tests/functional/cloud/__init__.py similarity index 100% rename from openstack/cloud/tests/functional/__init__.py rename to openstack/tests/functional/cloud/__init__.py diff --git a/openstack/cloud/tests/functional/base.py b/openstack/tests/functional/cloud/base.py similarity index 90% rename from openstack/cloud/tests/functional/base.py rename to openstack/tests/functional/cloud/base.py index 32c07d7da..192a96b6e 100644 --- a/openstack/cloud/tests/functional/base.py +++ b/openstack/tests/functional/cloud/base.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +# TODO(shade) Merge this with openstack.tests.functional.base + import os import openstack.config as occ @@ -22,9 +24,9 @@ class BaseFunctionalTestCase(base.TestCase): def setUp(self): super(BaseFunctionalTestCase, self).setUp() - self._demo_name = os.environ.get('SHADE_DEMO_CLOUD', 'devstack') + self._demo_name = os.environ.get('OPENSTACKSDK_DEMO_CLOUD', 'devstack') self._op_name = os.environ.get( - 'SHADE_OPERATOR_CLOUD', 'devstack-admin') + 'OPENSTACKSDK_OPERATOR_CLOUD', 'devstack-admin') self.config = occ.OpenStackConfig() self._set_user_cloud() @@ -51,7 +53,7 @@ def pick_image(self): images = self.user_cloud.list_images() self.add_info_on_exception('images', images) - image_name = os.environ.get('SHADE_IMAGE') + image_name = os.environ.get('OPENSTACKSDK_IMAGE') if image_name: for image in images: if image.name == image_name: @@ -80,7 +82,7 @@ class KeystoneBaseFunctionalTestCase(BaseFunctionalTestCase): def setUp(self): super(KeystoneBaseFunctionalTestCase, self).setUp() - use_keystone_v2 = os.environ.get('SHADE_USE_KEYSTONE_V2', False) + use_keystone_v2 = os.environ.get('OPENSTACKSDK_USE_KEYSTONE_V2', False) if use_keystone_v2: # keystone v2 has special behavior for the admin # interface and some of the operations, so make a new cloud diff --git a/openstack/cloud/tests/functional/hooks/post_test_hook.sh b/openstack/tests/functional/cloud/hooks/post_test_hook.sh similarity index 88% rename from openstack/cloud/tests/functional/hooks/post_test_hook.sh rename to openstack/tests/functional/cloud/hooks/post_test_hook.sh index 52037cd38..8092a6114 100755 --- a/openstack/cloud/tests/functional/hooks/post_test_hook.sh +++ b/openstack/tests/functional/cloud/hooks/post_test_hook.sh @@ -12,10 +12,12 @@ # License for the specific language governing permissions and limitations # under the License. -export SHADE_DIR="$BASE/new/shade" +# TODO(shade) Rework for zuul v3 -cd $SHADE_DIR -sudo chown -R jenkins:stack $SHADE_DIR +export OPENSTACKSDK_DIR="$BASE/new/shade" + +cd $OPENSTACKSDK_DIR +sudo chown -R jenkins:stack $OPENSTACKSDK_DIR CLOUDS_YAML=/etc/openstack/clouds.yaml @@ -30,7 +32,7 @@ fi # Devstack runs both keystone v2 and v3. An environment variable is set # within the shade keystone v2 job that tells us which version we should # test against. -if [ ${SHADE_USE_KEYSTONE_V2:-0} -eq 1 ] +if [ ${OPENSTACKSDK_USE_KEYSTONE_V2:-0} -eq 1 ] then sudo sed -ie "s/identity_api_version: '3'/identity_api_version: '2.0'/g" $CLOUDS_YAML sudo sed -ie '/^.*domain_id.*$/d' $CLOUDS_YAML diff --git a/openstack/cloud/tests/functional/test_aggregate.py b/openstack/tests/functional/cloud/test_aggregate.py similarity index 100% rename from openstack/cloud/tests/functional/test_aggregate.py rename to openstack/tests/functional/cloud/test_aggregate.py diff --git a/openstack/cloud/tests/functional/test_cluster_templates.py b/openstack/tests/functional/cloud/test_cluster_templates.py similarity index 100% rename from openstack/cloud/tests/functional/test_cluster_templates.py rename to openstack/tests/functional/cloud/test_cluster_templates.py diff --git a/openstack/cloud/tests/functional/test_compute.py b/openstack/tests/functional/cloud/test_compute.py similarity index 100% rename from openstack/cloud/tests/functional/test_compute.py rename to openstack/tests/functional/cloud/test_compute.py diff --git a/openstack/cloud/tests/functional/test_devstack.py b/openstack/tests/functional/cloud/test_devstack.py similarity index 89% rename from openstack/cloud/tests/functional/test_devstack.py rename to openstack/tests/functional/cloud/test_devstack.py index b48ba5d8c..c2a132bfe 100644 --- a/openstack/cloud/tests/functional/test_devstack.py +++ b/openstack/tests/functional/cloud/test_devstack.py @@ -38,14 +38,15 @@ class TestDevstack(base.BaseFunctionalTestCase): ] def test_has_service(self): - if os.environ.get('SHADE_HAS_{env}'.format(env=self.env), '0') == '1': + if os.environ.get( + 'OPENSTACKSDK_HAS_{env}'.format(env=self.env), '0') == '1': self.assertTrue(self.user_cloud.has_service(self.service)) class TestKeystoneVersion(base.BaseFunctionalTestCase): def test_keystone_version(self): - use_keystone_v2 = os.environ.get('SHADE_USE_KEYSTONE_V2', False) + use_keystone_v2 = os.environ.get('OPENSTACKSDK_USE_KEYSTONE_V2', False) if use_keystone_v2 and use_keystone_v2 != '0': self.assertEqual('2.0', self.identity_version) else: diff --git a/openstack/cloud/tests/functional/test_domain.py b/openstack/tests/functional/cloud/test_domain.py similarity index 100% rename from openstack/cloud/tests/functional/test_domain.py rename to openstack/tests/functional/cloud/test_domain.py diff --git a/openstack/cloud/tests/functional/test_endpoints.py b/openstack/tests/functional/cloud/test_endpoints.py similarity index 100% rename from openstack/cloud/tests/functional/test_endpoints.py rename to openstack/tests/functional/cloud/test_endpoints.py diff --git a/openstack/cloud/tests/functional/test_flavor.py b/openstack/tests/functional/cloud/test_flavor.py similarity index 100% rename from openstack/cloud/tests/functional/test_flavor.py rename to openstack/tests/functional/cloud/test_flavor.py diff --git a/openstack/cloud/tests/functional/test_floating_ip.py b/openstack/tests/functional/cloud/test_floating_ip.py similarity index 100% rename from openstack/cloud/tests/functional/test_floating_ip.py rename to openstack/tests/functional/cloud/test_floating_ip.py diff --git a/openstack/cloud/tests/functional/test_floating_ip_pool.py b/openstack/tests/functional/cloud/test_floating_ip_pool.py similarity index 100% rename from openstack/cloud/tests/functional/test_floating_ip_pool.py rename to openstack/tests/functional/cloud/test_floating_ip_pool.py diff --git a/openstack/cloud/tests/functional/test_groups.py b/openstack/tests/functional/cloud/test_groups.py similarity index 100% rename from openstack/cloud/tests/functional/test_groups.py rename to openstack/tests/functional/cloud/test_groups.py diff --git a/openstack/cloud/tests/functional/test_identity.py b/openstack/tests/functional/cloud/test_identity.py similarity index 100% rename from openstack/cloud/tests/functional/test_identity.py rename to openstack/tests/functional/cloud/test_identity.py diff --git a/openstack/cloud/tests/functional/test_image.py b/openstack/tests/functional/cloud/test_image.py similarity index 100% rename from openstack/cloud/tests/functional/test_image.py rename to openstack/tests/functional/cloud/test_image.py diff --git a/openstack/cloud/tests/functional/test_inventory.py b/openstack/tests/functional/cloud/test_inventory.py similarity index 100% rename from openstack/cloud/tests/functional/test_inventory.py rename to openstack/tests/functional/cloud/test_inventory.py diff --git a/openstack/cloud/tests/functional/test_keypairs.py b/openstack/tests/functional/cloud/test_keypairs.py similarity index 100% rename from openstack/cloud/tests/functional/test_keypairs.py rename to openstack/tests/functional/cloud/test_keypairs.py diff --git a/openstack/cloud/tests/functional/test_limits.py b/openstack/tests/functional/cloud/test_limits.py similarity index 100% rename from openstack/cloud/tests/functional/test_limits.py rename to openstack/tests/functional/cloud/test_limits.py diff --git a/openstack/cloud/tests/functional/test_magnum_services.py b/openstack/tests/functional/cloud/test_magnum_services.py similarity index 100% rename from openstack/cloud/tests/functional/test_magnum_services.py rename to openstack/tests/functional/cloud/test_magnum_services.py diff --git a/openstack/cloud/tests/functional/test_network.py b/openstack/tests/functional/cloud/test_network.py similarity index 100% rename from openstack/cloud/tests/functional/test_network.py rename to openstack/tests/functional/cloud/test_network.py diff --git a/openstack/cloud/tests/functional/test_object.py b/openstack/tests/functional/cloud/test_object.py similarity index 100% rename from openstack/cloud/tests/functional/test_object.py rename to openstack/tests/functional/cloud/test_object.py diff --git a/openstack/cloud/tests/functional/test_port.py b/openstack/tests/functional/cloud/test_port.py similarity index 100% rename from openstack/cloud/tests/functional/test_port.py rename to openstack/tests/functional/cloud/test_port.py diff --git a/openstack/cloud/tests/functional/test_project.py b/openstack/tests/functional/cloud/test_project.py similarity index 100% rename from openstack/cloud/tests/functional/test_project.py rename to openstack/tests/functional/cloud/test_project.py diff --git a/openstack/cloud/tests/functional/test_qos_bandwidth_limit_rule.py b/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py similarity index 100% rename from openstack/cloud/tests/functional/test_qos_bandwidth_limit_rule.py rename to openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py diff --git a/openstack/cloud/tests/functional/test_qos_dscp_marking_rule.py b/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py similarity index 100% rename from openstack/cloud/tests/functional/test_qos_dscp_marking_rule.py rename to openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py diff --git a/openstack/cloud/tests/functional/test_qos_minimum_bandwidth_rule.py b/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py similarity index 100% rename from openstack/cloud/tests/functional/test_qos_minimum_bandwidth_rule.py rename to openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py diff --git a/openstack/cloud/tests/functional/test_qos_policy.py b/openstack/tests/functional/cloud/test_qos_policy.py similarity index 100% rename from openstack/cloud/tests/functional/test_qos_policy.py rename to openstack/tests/functional/cloud/test_qos_policy.py diff --git a/openstack/cloud/tests/functional/test_quotas.py b/openstack/tests/functional/cloud/test_quotas.py similarity index 100% rename from openstack/cloud/tests/functional/test_quotas.py rename to openstack/tests/functional/cloud/test_quotas.py diff --git a/openstack/cloud/tests/functional/test_range_search.py b/openstack/tests/functional/cloud/test_range_search.py similarity index 100% rename from openstack/cloud/tests/functional/test_range_search.py rename to openstack/tests/functional/cloud/test_range_search.py diff --git a/openstack/cloud/tests/functional/test_recordset.py b/openstack/tests/functional/cloud/test_recordset.py similarity index 100% rename from openstack/cloud/tests/functional/test_recordset.py rename to openstack/tests/functional/cloud/test_recordset.py diff --git a/openstack/cloud/tests/functional/test_router.py b/openstack/tests/functional/cloud/test_router.py similarity index 100% rename from openstack/cloud/tests/functional/test_router.py rename to openstack/tests/functional/cloud/test_router.py diff --git a/openstack/cloud/tests/functional/test_security_groups.py b/openstack/tests/functional/cloud/test_security_groups.py similarity index 100% rename from openstack/cloud/tests/functional/test_security_groups.py rename to openstack/tests/functional/cloud/test_security_groups.py diff --git a/openstack/cloud/tests/functional/test_server_group.py b/openstack/tests/functional/cloud/test_server_group.py similarity index 100% rename from openstack/cloud/tests/functional/test_server_group.py rename to openstack/tests/functional/cloud/test_server_group.py diff --git a/openstack/cloud/tests/functional/test_services.py b/openstack/tests/functional/cloud/test_services.py similarity index 100% rename from openstack/cloud/tests/functional/test_services.py rename to openstack/tests/functional/cloud/test_services.py diff --git a/openstack/cloud/tests/functional/test_stack.py b/openstack/tests/functional/cloud/test_stack.py similarity index 100% rename from openstack/cloud/tests/functional/test_stack.py rename to openstack/tests/functional/cloud/test_stack.py diff --git a/openstack/cloud/tests/functional/test_usage.py b/openstack/tests/functional/cloud/test_usage.py similarity index 100% rename from openstack/cloud/tests/functional/test_usage.py rename to openstack/tests/functional/cloud/test_usage.py diff --git a/openstack/cloud/tests/functional/test_users.py b/openstack/tests/functional/cloud/test_users.py similarity index 100% rename from openstack/cloud/tests/functional/test_users.py rename to openstack/tests/functional/cloud/test_users.py diff --git a/openstack/cloud/tests/functional/test_volume.py b/openstack/tests/functional/cloud/test_volume.py similarity index 100% rename from openstack/cloud/tests/functional/test_volume.py rename to openstack/tests/functional/cloud/test_volume.py diff --git a/openstack/cloud/tests/functional/test_volume_backup.py b/openstack/tests/functional/cloud/test_volume_backup.py similarity index 100% rename from openstack/cloud/tests/functional/test_volume_backup.py rename to openstack/tests/functional/cloud/test_volume_backup.py diff --git a/openstack/cloud/tests/functional/test_volume_type.py b/openstack/tests/functional/cloud/test_volume_type.py similarity index 100% rename from openstack/cloud/tests/functional/test_volume_type.py rename to openstack/tests/functional/cloud/test_volume_type.py diff --git a/openstack/cloud/tests/functional/test_zone.py b/openstack/tests/functional/cloud/test_zone.py similarity index 100% rename from openstack/cloud/tests/functional/test_zone.py rename to openstack/tests/functional/cloud/test_zone.py diff --git a/openstack/cloud/tests/functional/util.py b/openstack/tests/functional/cloud/util.py similarity index 95% rename from openstack/cloud/tests/functional/util.py rename to openstack/tests/functional/cloud/util.py index 180f08f76..fef67190f 100644 --- a/openstack/cloud/tests/functional/util.py +++ b/openstack/tests/functional/cloud/util.py @@ -24,7 +24,7 @@ def pick_flavor(flavors): """Given a flavor list pick the smallest one.""" # Enable running functional tests against rax - which requires # performance flavors be used for boot from volume - flavor_name = os.environ.get('SHADE_FLAVOR') + flavor_name = os.environ.get('OPENSTACKSDK_FLAVOR') if flavor_name: for flavor in flavors: if flavor.name == flavor_name: diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index eae41c16d..87f128da7 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -13,39 +13,627 @@ # License for the specific language governing permissions and limitations # under the License. -import os +import collections +import time +import uuid import fixtures -import testtools +import mock +import os +import openstack.config as occ +from requests import structures +from requests_mock.contrib import fixture as rm_fixture +from six.moves import urllib +import tempfile -_TRUE_VALUES = ('true', '1', 'yes') +import openstack.cloud.openstackcloud +from openstack.cloud.tests import base -class TestCase(testtools.TestCase): +_ProjectData = collections.namedtuple( + 'ProjectData', + 'project_id, project_name, enabled, domain_id, description, ' + 'json_response, json_request') - """Test case base class for all unit tests.""" - def setUp(self): +_UserData = collections.namedtuple( + 'UserData', + 'user_id, password, name, email, description, domain_id, enabled, ' + 'json_response, json_request') + + +_GroupData = collections.namedtuple( + 'GroupData', + 'group_id, group_name, domain_id, description, json_response, ' + 'json_request') + + +_DomainData = collections.namedtuple( + 'DomainData', + 'domain_id, domain_name, description, json_response, ' + 'json_request') + + +_ServiceData = collections.namedtuple( + 'Servicedata', + 'service_id, service_name, service_type, description, enabled, ' + 'json_response_v3, json_response_v2, json_request') + + +_EndpointDataV3 = collections.namedtuple( + 'EndpointData', + 'endpoint_id, service_id, interface, region, url, enabled, ' + 'json_response, json_request') + + +_EndpointDataV2 = collections.namedtuple( + 'EndpointData', + 'endpoint_id, service_id, region, public_url, internal_url, ' + 'admin_url, v3_endpoint_list, json_response, ' + 'json_request') + + +# NOTE(notmorgan): Shade does not support domain-specific roles +# This should eventually be fixed if it becomes a main-stream feature. +_RoleData = collections.namedtuple( + 'RoleData', + 'role_id, role_name, json_response, json_request') + + +class BaseTestCase(base.TestCase): + + def setUp(self, cloud_config_fixture='clouds.yaml'): """Run before each test method to initialize test environment.""" - super(TestCase, self).setUp() - test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0) - try: - test_timeout = int(test_timeout) - except ValueError: - # If timeout value is invalid do not set a timeout. - test_timeout = 0 - if test_timeout > 0: - self.useFixture(fixtures.Timeout(test_timeout, gentle=True)) - - self.useFixture(fixtures.NestedTempfile()) - self.useFixture(fixtures.TempHomeDir()) - - if os.environ.get('OS_STDOUT_CAPTURE') in _TRUE_VALUES: - stdout = self.useFixture(fixtures.StringStream('stdout')).stream - self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout)) - if os.environ.get('OS_STDERR_CAPTURE') in _TRUE_VALUES: - stderr = self.useFixture(fixtures.StringStream('stderr')).stream - self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) - - self.log_fixture = self.useFixture(fixtures.FakeLogger()) + super(BaseTestCase, self).setUp() + + # Sleeps are for real testing, but unit tests shouldn't need them + realsleep = time.sleep + + def _nosleep(seconds): + return realsleep(seconds * 0.0001) + + self.sleep_fixture = self.useFixture(fixtures.MonkeyPatch( + 'time.sleep', + _nosleep)) + self.fixtures_directory = 'openstack/tests/unit/fixtures' + + # Isolate os-client-config from test environment + config = tempfile.NamedTemporaryFile(delete=False) + cloud_path = '%s/clouds/%s' % (self.fixtures_directory, + cloud_config_fixture) + with open(cloud_path, 'rb') as f: + content = f.read() + config.write(content) + config.close() + + vendor = tempfile.NamedTemporaryFile(delete=False) + vendor.write(b'{}') + vendor.close() + + test_cloud = os.environ.get('OPENSTACKSDK_OS_CLOUD', '_test_cloud_') + self.config = occ.OpenStackConfig( + config_files=[config.name], + vendor_files=[vendor.name], + secure_files=['non-existant']) + self.cloud_config = self.config.get_one_cloud( + cloud=test_cloud, validate=False) + self.cloud = openstack.cloud.OpenStackCloud( + cloud_config=self.cloud_config, + log_inner_exceptions=True) + self.strict_cloud = openstack.cloud.OpenStackCloud( + cloud_config=self.cloud_config, + log_inner_exceptions=True, + strict=True) + self.op_cloud = openstack.cloud.OperatorCloud( + cloud_config=self.cloud_config, + log_inner_exceptions=True) + + +class TestCase(BaseTestCase): + + def setUp(self, cloud_config_fixture='clouds.yaml'): + + super(TestCase, self).setUp(cloud_config_fixture=cloud_config_fixture) + self.session_fixture = self.useFixture(fixtures.MonkeyPatch( + 'os_client_config.cloud_config.CloudConfig.get_session', + mock.Mock())) + + +class RequestsMockTestCase(BaseTestCase): + + def setUp(self, cloud_config_fixture='clouds.yaml'): + + super(RequestsMockTestCase, self).setUp( + cloud_config_fixture=cloud_config_fixture) + + # FIXME(notmorgan): Convert the uri_registry, discovery.json, and + # use of keystone_v3/v2 to a proper fixtures.Fixture. For now this + # is acceptable, but eventually this should become it's own fixture + # that encapsulates the registry, registering the URIs, and + # assert_calls (and calling assert_calls every test case that uses + # it on cleanup). Subclassing here could be 100% eliminated in the + # future allowing any class to simply + # self.useFixture(openstack.cloud.RequestsMockFixture) and get all + # the benefits. + + # NOTE(notmorgan): use an ordered dict here to ensure we preserve the + # order in which items are added to the uri_registry. This makes + # the behavior more consistent when dealing with ensuring the + # requests_mock uri/query_string matchers are ordered and parse the + # request in the correct orders. + self._uri_registry = collections.OrderedDict() + self.discovery_json = os.path.join( + self.fixtures_directory, 'discovery.json') + self.use_keystone_v3() + self.__register_uris_called = False + + def get_mock_url(self, service_type, interface='public', resource=None, + append=None, base_url_append=None, + qs_elements=None): + endpoint_url = self.cloud.endpoint_for( + service_type=service_type, interface=interface) + # Strip trailing slashes, so as not to produce double-slashes below + if endpoint_url.endswith('/'): + endpoint_url = endpoint_url[:-1] + to_join = [endpoint_url] + qs = '' + if base_url_append: + to_join.append(base_url_append) + if resource: + to_join.append(resource) + to_join.extend(append or []) + if qs_elements is not None: + qs = '?%s' % '&'.join(qs_elements) + return '%(uri)s%(qs)s' % {'uri': '/'.join(to_join), 'qs': qs} + + def mock_for_keystone_projects(self, project=None, v3=True, + list_get=False, id_get=False, + project_list=None, project_count=None): + if project: + assert not (project_list or project_count) + elif project_list: + assert not (project or project_count) + elif project_count: + assert not (project or project_list) + else: + raise Exception('Must specify a project, project_list, ' + 'or project_count') + assert list_get or id_get + + base_url_append = 'v3' if v3 else None + if project: + project_list = [project] + elif project_count: + # Generate multiple projects + project_list = [self._get_project_data(v3=v3) + for c in range(0, project_count)] + uri_mock_list = [] + if list_get: + uri_mock_list.append( + dict(method='GET', + uri=self.get_mock_url( + service_type='identity', + interface='admin', + resource='projects', + base_url_append=base_url_append), + status_code=200, + json={'projects': [p.json_response['project'] + for p in project_list]}) + ) + if id_get: + for p in project_list: + uri_mock_list.append( + dict(method='GET', + uri=self.get_mock_url( + service_type='identity', + interface='admin', + resource='projects', + append=[p.project_id], + base_url_append=base_url_append), + status_code=200, + json=p.json_response) + ) + self.__do_register_uris(uri_mock_list) + return project_list + + def _get_project_data(self, project_name=None, enabled=None, + domain_id=None, description=None, v3=True, + project_id=None): + project_name = project_name or self.getUniqueString('projectName') + project_id = uuid.UUID(project_id or uuid.uuid4().hex).hex + response = {'id': project_id, 'name': project_name} + request = {'name': project_name} + domain_id = (domain_id or uuid.uuid4().hex) if v3 else None + if domain_id: + request['domain_id'] = domain_id + response['domain_id'] = domain_id + if enabled is not None: + enabled = bool(enabled) + response['enabled'] = enabled + request['enabled'] = enabled + response.setdefault('enabled', True) + request.setdefault('enabled', True) + if description: + response['description'] = description + request['description'] = description + request.setdefault('description', None) + if v3: + project_key = 'project' + else: + project_key = 'tenant' + return _ProjectData(project_id, project_name, enabled, domain_id, + description, {project_key: response}, + {project_key: request}) + + def _get_group_data(self, name=None, domain_id=None, description=None): + group_id = uuid.uuid4().hex + name = name or self.getUniqueString('groupname') + domain_id = uuid.UUID(domain_id or uuid.uuid4().hex).hex + response = {'id': group_id, 'name': name, 'domain_id': domain_id} + request = {'name': name, 'domain_id': domain_id} + if description is not None: + response['description'] = description + request['description'] = description + + return _GroupData(group_id, name, domain_id, description, + {'group': response}, {'group': request}) + + def _get_user_data(self, name=None, password=None, **kwargs): + + name = name or self.getUniqueString('username') + password = password or self.getUniqueString('user_password') + user_id = uuid.uuid4().hex + + response = {'name': name, 'id': user_id} + request = {'name': name, 'password': password} + + if kwargs.get('domain_id'): + kwargs['domain_id'] = uuid.UUID(kwargs['domain_id']).hex + response['domain_id'] = kwargs.pop('domain_id') + request['domain_id'] = response['domain_id'] + + response['email'] = kwargs.pop('email', None) + request['email'] = response['email'] + + response['enabled'] = kwargs.pop('enabled', True) + request['enabled'] = response['enabled'] + + response['description'] = kwargs.pop('description', None) + if response['description']: + request['description'] = response['description'] + + self.assertIs(0, len(kwargs), message='extra key-word args received ' + 'on _get_user_data') + + return _UserData(user_id, password, name, response['email'], + response['description'], response.get('domain_id'), + response.get('enabled'), {'user': response}, + {'user': request}) + + def _get_domain_data(self, domain_name=None, description=None, + enabled=None): + domain_id = uuid.uuid4().hex + domain_name = domain_name or self.getUniqueString('domainName') + response = {'id': domain_id, 'name': domain_name} + request = {'name': domain_name} + if enabled is not None: + request['enabled'] = bool(enabled) + response['enabled'] = bool(enabled) + if description: + response['description'] = description + request['description'] = description + response.setdefault('enabled', True) + return _DomainData(domain_id, domain_name, description, + {'domain': response}, {'domain': request}) + + def _get_service_data(self, type=None, name=None, description=None, + enabled=True): + service_id = uuid.uuid4().hex + name = name or uuid.uuid4().hex + type = type or uuid.uuid4().hex + + response = {'id': service_id, 'name': name, 'type': type, + 'enabled': enabled} + if description is not None: + response['description'] = description + request = response.copy() + request.pop('id') + return _ServiceData(service_id, name, type, description, enabled, + {'service': response}, + {'OS-KSADM:service': response}, request) + + def _get_endpoint_v3_data(self, service_id=None, region=None, + url=None, interface=None, enabled=True): + endpoint_id = uuid.uuid4().hex + service_id = service_id or uuid.uuid4().hex + region = region or uuid.uuid4().hex + url = url or 'https://example.com/' + interface = interface or uuid.uuid4().hex + + response = {'id': endpoint_id, 'service_id': service_id, + 'region': region, 'interface': interface, + 'url': url, 'enabled': enabled} + request = response.copy() + request.pop('id') + response['region_id'] = response['region'] + return _EndpointDataV3(endpoint_id, service_id, interface, region, + url, enabled, {'endpoint': response}, + {'endpoint': request}) + + def _get_endpoint_v2_data(self, service_id=None, region=None, + public_url=None, admin_url=None, + internal_url=None): + endpoint_id = uuid.uuid4().hex + service_id = service_id or uuid.uuid4().hex + region = region or uuid.uuid4().hex + response = {'id': endpoint_id, 'service_id': service_id, + 'region': region} + v3_endpoints = {} + request = response.copy() + request.pop('id') + if admin_url: + response['adminURL'] = admin_url + v3_endpoints['admin'] = self._get_endpoint_v3_data( + service_id, region, public_url, interface='admin') + if internal_url: + response['internalURL'] = internal_url + v3_endpoints['internal'] = self._get_endpoint_v3_data( + service_id, region, internal_url, interface='internal') + if public_url: + response['publicURL'] = public_url + v3_endpoints['public'] = self._get_endpoint_v3_data( + service_id, region, public_url, interface='public') + request = response.copy() + request.pop('id') + for u in ('publicURL', 'internalURL', 'adminURL'): + if request.get(u): + request[u.lower()] = request.pop(u) + return _EndpointDataV2(endpoint_id, service_id, region, public_url, + internal_url, admin_url, v3_endpoints, + {'endpoint': response}, {'endpoint': request}) + + def _get_role_data(self, role_name=None): + role_id = uuid.uuid4().hex + role_name = role_name or uuid.uuid4().hex + request = {'name': role_name} + response = request.copy() + response['id'] = role_id + return _RoleData(role_id, role_name, {'role': response}, + {'role': request}) + + def use_keystone_v3(self, catalog='catalog-v3.json'): + self.adapter = self.useFixture(rm_fixture.Fixture()) + self.calls = [] + self._uri_registry.clear() + self.__do_register_uris([ + dict(method='GET', uri='https://identity.example.com/', + text=open(self.discovery_json, 'r').read()), + dict(method='POST', + uri='https://identity.example.com/v3/auth/tokens', + headers={ + 'X-Subject-Token': self.getUniqueString('KeystoneToken')}, + text=open(os.path.join( + self.fixtures_directory, catalog), 'r').read() + ), + ]) + self._make_test_cloud(identity_api_version='3') + + def use_keystone_v2(self): + self.adapter = self.useFixture(rm_fixture.Fixture()) + self.calls = [] + self._uri_registry.clear() + + self.__do_register_uris([ + dict(method='GET', uri='https://identity.example.com/', + text=open(self.discovery_json, 'r').read()), + dict(method='POST', uri='https://identity.example.com/v2.0/tokens', + text=open(os.path.join( + self.fixtures_directory, 'catalog-v2.json'), 'r').read() + ), + ]) + + self._make_test_cloud(cloud_name='_test_cloud_v2_', + identity_api_version='2.0') + + def _make_test_cloud(self, cloud_name='_test_cloud_', **kwargs): + test_cloud = os.environ.get('OPENSTACKSDK_OS_CLOUD', cloud_name) + self.cloud_config = self.config.get_one_cloud( + cloud=test_cloud, validate=True, **kwargs) + self.cloud = openstack.cloud.OpenStackCloud( + cloud_config=self.cloud_config, + log_inner_exceptions=True) + self.op_cloud = openstack.cloud.OperatorCloud( + cloud_config=self.cloud_config, + log_inner_exceptions=True) + + def get_glance_discovery_mock_dict( + self, image_version_json='image-version.json'): + discovery_fixture = os.path.join( + self.fixtures_directory, image_version_json) + return dict(method='GET', uri='https://image.example.com/', + status_code=300, + text=open(discovery_fixture, 'r').read()) + + def get_designate_discovery_mock_dict(self): + discovery_fixture = os.path.join( + self.fixtures_directory, "dns.json") + return dict(method='GET', uri="https://dns.example.com/", + text=open(discovery_fixture, 'r').read()) + + def get_ironic_discovery_mock_dict(self): + discovery_fixture = os.path.join( + self.fixtures_directory, "baremetal.json") + return dict(method='GET', uri="https://bare-metal.example.com/", + text=open(discovery_fixture, 'r').read()) + + def use_glance(self, image_version_json='image-version.json'): + # NOTE(notmorgan): This method is only meant to be used in "setUp" + # where the ordering of the url being registered is tightly controlled + # if the functionality of .use_glance is meant to be used during an + # actual test case, use .get_glance_discovery_mock and apply to the + # right location in the mock_uris when calling .register_uris + self.__do_register_uris([ + self.get_glance_discovery_mock_dict(image_version_json)]) + + def use_designate(self): + # NOTE(slaweq): This method is only meant to be used in "setUp" + # where the ordering of the url being registered is tightly controlled + # if the functionality of .use_designate is meant to be used during an + # actual test case, use .get_designate_discovery_mock and apply to the + # right location in the mock_uris when calling .register_uris + self.__do_register_uris([ + self.get_designate_discovery_mock_dict()]) + + def use_ironic(self): + # NOTE(TheJulia): This method is only meant to be used in "setUp" + # where the ordering of the url being registered is tightly controlled + # if the functionality of .use_ironic is meant to be used during an + # actual test case, use .get_ironic_discovery_mock and apply to the + # right location in the mock_uris when calling .register_uris + self.__do_register_uris([ + self.get_ironic_discovery_mock_dict()]) + + def register_uris(self, uri_mock_list=None): + """Mock a list of URIs and responses via requests mock. + + This method may be called only once per test-case to avoid odd + and difficult to debug interactions. Discovery and Auth request mocking + happens separately from this method. + + :param uri_mock_list: List of dictionaries that template out what is + passed to requests_mock fixture's `register_uri`. + Format is: + {'method': , + 'uri': , + ... + } + + Common keys to pass in the dictionary: + * json: the json response (dict) + * status_code: the HTTP status (int) + * validate: The request body (dict) to + validate with assert_calls + all key-word arguments that are valid to send to + requests_mock are supported. + + This list should be in the order in which calls + are made. When `assert_calls` is executed, order + here will be validated. Duplicate URIs and + Methods are allowed and will be collapsed into a + single matcher. Each response will be returned + in order as the URI+Method is hit. + :type uri_mock_list: list + :return: None + """ + assert not self.__register_uris_called + self.__do_register_uris(uri_mock_list or []) + self.__register_uris_called = True + + def __do_register_uris(self, uri_mock_list=None): + for to_mock in uri_mock_list: + kw_params = {k: to_mock.pop(k) + for k in ('request_headers', 'complete_qs', + '_real_http') + if k in to_mock} + + method = to_mock.pop('method') + uri = to_mock.pop('uri') + # NOTE(notmorgan): make sure the delimiter is non-url-safe, in this + # case "|" is used so that the split can be a bit easier on + # maintainers of this code. + key = '{method}|{uri}|{params}'.format( + method=method, uri=uri, params=kw_params) + validate = to_mock.pop('validate', {}) + valid_keys = set(['json', 'headers', 'params']) + invalid_keys = set(validate.keys()) - valid_keys + if invalid_keys: + raise TypeError( + "Invalid values passed to validate: {keys}".format( + keys=invalid_keys)) + headers = structures.CaseInsensitiveDict(to_mock.pop('headers', + {})) + if 'content-type' not in headers: + headers[u'content-type'] = 'application/json' + + to_mock['headers'] = headers + + self.calls += [ + dict( + method=method, + url=uri, **validate) + ] + self._uri_registry.setdefault( + key, {'response_list': [], 'kw_params': kw_params}) + if self._uri_registry[key]['kw_params'] != kw_params: + raise AssertionError( + 'PROGRAMMING ERROR: key-word-params ' + 'should be part of the uri_key and cannot change, ' + 'it will affect the matcher in requests_mock. ' + '%(old)r != %(new)r' % + {'old': self._uri_registry[key]['kw_params'], + 'new': kw_params}) + self._uri_registry[key]['response_list'].append(to_mock) + + for mocked, params in self._uri_registry.items(): + mock_method, mock_uri, _ignored = mocked.split('|', 2) + self.adapter.register_uri( + mock_method, mock_uri, params['response_list'], + **params['kw_params']) + + def assert_calls(self, stop_after=None, do_count=True): + for (x, (call, history)) in enumerate( + zip(self.calls, self.adapter.request_history)): + if stop_after and x > stop_after: + break + + call_uri_parts = urllib.parse.urlparse(call['url']) + history_uri_parts = urllib.parse.urlparse(history.url) + self.assertEqual( + (call['method'], call_uri_parts.scheme, call_uri_parts.netloc, + call_uri_parts.path, call_uri_parts.params, + urllib.parse.parse_qs(call_uri_parts.query)), + (history.method, history_uri_parts.scheme, + history_uri_parts.netloc, history_uri_parts.path, + history_uri_parts.params, + urllib.parse.parse_qs(history_uri_parts.query)), + ('REST mismatch on call %(index)d. Expected %(call)r. ' + 'Got %(history)r). ' + 'NOTE: query string order differences wont cause mismatch' % + { + 'index': x, + 'call': '{method} {url}'.format(method=call['method'], + url=call['url']), + 'history': '{method} {url}'.format( + method=history.method, + url=history.url)}) + ) + if 'json' in call: + self.assertEqual( + call['json'], history.json(), + 'json content mismatch in call {index}'.format(index=x)) + # headers in a call isn't exhaustive - it's checking to make sure + # a specific header or headers are there, not that they are the + # only headers + if 'headers' in call: + for key, value in call['headers'].items(): + self.assertEqual( + value, history.headers[key], + 'header mismatch in call {index}'.format(index=x)) + if do_count: + self.assertEqual( + len(self.calls), len(self.adapter.request_history)) + + +class IronicTestCase(RequestsMockTestCase): + + def setUp(self): + super(IronicTestCase, self).setUp() + self.use_ironic() + self.uuid = str(uuid.uuid4()) + self.name = self.getUniqueString('name') + + def get_mock_url(self, resource=None, append=None, qs_elements=None): + return super(IronicTestCase, self).get_mock_url( + service_type='baremetal', interface='public', resource=resource, + append=append, base_url_append='v1', qs_elements=qs_elements) diff --git a/openstack/cloud/tests/unit/__init__.py b/openstack/tests/unit/cloud/__init__.py similarity index 100% rename from openstack/cloud/tests/unit/__init__.py rename to openstack/tests/unit/cloud/__init__.py diff --git a/openstack/cloud/tests/unit/test__adapter.py b/openstack/tests/unit/cloud/test__adapter.py similarity index 100% rename from openstack/cloud/tests/unit/test__adapter.py rename to openstack/tests/unit/cloud/test__adapter.py diff --git a/openstack/cloud/tests/unit/test__utils.py b/openstack/tests/unit/cloud/test__utils.py similarity index 100% rename from openstack/cloud/tests/unit/test__utils.py rename to openstack/tests/unit/cloud/test__utils.py diff --git a/openstack/cloud/tests/unit/test_aggregate.py b/openstack/tests/unit/cloud/test_aggregate.py similarity index 100% rename from openstack/cloud/tests/unit/test_aggregate.py rename to openstack/tests/unit/cloud/test_aggregate.py diff --git a/openstack/cloud/tests/unit/test_availability_zones.py b/openstack/tests/unit/cloud/test_availability_zones.py similarity index 100% rename from openstack/cloud/tests/unit/test_availability_zones.py rename to openstack/tests/unit/cloud/test_availability_zones.py diff --git a/openstack/cloud/tests/unit/test_baremetal_node.py b/openstack/tests/unit/cloud/test_baremetal_node.py similarity index 100% rename from openstack/cloud/tests/unit/test_baremetal_node.py rename to openstack/tests/unit/cloud/test_baremetal_node.py diff --git a/openstack/cloud/tests/unit/test_caching.py b/openstack/tests/unit/cloud/test_caching.py similarity index 100% rename from openstack/cloud/tests/unit/test_caching.py rename to openstack/tests/unit/cloud/test_caching.py diff --git a/openstack/cloud/tests/unit/test_cluster_templates.py b/openstack/tests/unit/cloud/test_cluster_templates.py similarity index 100% rename from openstack/cloud/tests/unit/test_cluster_templates.py rename to openstack/tests/unit/cloud/test_cluster_templates.py diff --git a/openstack/cloud/tests/unit/test_create_server.py b/openstack/tests/unit/cloud/test_create_server.py similarity index 100% rename from openstack/cloud/tests/unit/test_create_server.py rename to openstack/tests/unit/cloud/test_create_server.py diff --git a/openstack/cloud/tests/unit/test_create_volume_snapshot.py b/openstack/tests/unit/cloud/test_create_volume_snapshot.py similarity index 100% rename from openstack/cloud/tests/unit/test_create_volume_snapshot.py rename to openstack/tests/unit/cloud/test_create_volume_snapshot.py diff --git a/openstack/cloud/tests/unit/test_delete_server.py b/openstack/tests/unit/cloud/test_delete_server.py similarity index 100% rename from openstack/cloud/tests/unit/test_delete_server.py rename to openstack/tests/unit/cloud/test_delete_server.py diff --git a/openstack/cloud/tests/unit/test_delete_volume_snapshot.py b/openstack/tests/unit/cloud/test_delete_volume_snapshot.py similarity index 100% rename from openstack/cloud/tests/unit/test_delete_volume_snapshot.py rename to openstack/tests/unit/cloud/test_delete_volume_snapshot.py diff --git a/openstack/cloud/tests/unit/test_domain_params.py b/openstack/tests/unit/cloud/test_domain_params.py similarity index 100% rename from openstack/cloud/tests/unit/test_domain_params.py rename to openstack/tests/unit/cloud/test_domain_params.py diff --git a/openstack/cloud/tests/unit/test_domains.py b/openstack/tests/unit/cloud/test_domains.py similarity index 100% rename from openstack/cloud/tests/unit/test_domains.py rename to openstack/tests/unit/cloud/test_domains.py diff --git a/openstack/cloud/tests/unit/test_endpoints.py b/openstack/tests/unit/cloud/test_endpoints.py similarity index 100% rename from openstack/cloud/tests/unit/test_endpoints.py rename to openstack/tests/unit/cloud/test_endpoints.py diff --git a/openstack/cloud/tests/unit/test_flavors.py b/openstack/tests/unit/cloud/test_flavors.py similarity index 100% rename from openstack/cloud/tests/unit/test_flavors.py rename to openstack/tests/unit/cloud/test_flavors.py diff --git a/openstack/cloud/tests/unit/test_floating_ip_common.py b/openstack/tests/unit/cloud/test_floating_ip_common.py similarity index 100% rename from openstack/cloud/tests/unit/test_floating_ip_common.py rename to openstack/tests/unit/cloud/test_floating_ip_common.py diff --git a/openstack/cloud/tests/unit/test_floating_ip_neutron.py b/openstack/tests/unit/cloud/test_floating_ip_neutron.py similarity index 100% rename from openstack/cloud/tests/unit/test_floating_ip_neutron.py rename to openstack/tests/unit/cloud/test_floating_ip_neutron.py diff --git a/openstack/cloud/tests/unit/test_floating_ip_nova.py b/openstack/tests/unit/cloud/test_floating_ip_nova.py similarity index 100% rename from openstack/cloud/tests/unit/test_floating_ip_nova.py rename to openstack/tests/unit/cloud/test_floating_ip_nova.py diff --git a/openstack/cloud/tests/unit/test_floating_ip_pool.py b/openstack/tests/unit/cloud/test_floating_ip_pool.py similarity index 100% rename from openstack/cloud/tests/unit/test_floating_ip_pool.py rename to openstack/tests/unit/cloud/test_floating_ip_pool.py diff --git a/openstack/cloud/tests/unit/test_groups.py b/openstack/tests/unit/cloud/test_groups.py similarity index 100% rename from openstack/cloud/tests/unit/test_groups.py rename to openstack/tests/unit/cloud/test_groups.py diff --git a/openstack/cloud/tests/unit/test_identity_roles.py b/openstack/tests/unit/cloud/test_identity_roles.py similarity index 100% rename from openstack/cloud/tests/unit/test_identity_roles.py rename to openstack/tests/unit/cloud/test_identity_roles.py diff --git a/openstack/cloud/tests/unit/test_image.py b/openstack/tests/unit/cloud/test_image.py similarity index 100% rename from openstack/cloud/tests/unit/test_image.py rename to openstack/tests/unit/cloud/test_image.py diff --git a/openstack/cloud/tests/unit/test_image_snapshot.py b/openstack/tests/unit/cloud/test_image_snapshot.py similarity index 100% rename from openstack/cloud/tests/unit/test_image_snapshot.py rename to openstack/tests/unit/cloud/test_image_snapshot.py diff --git a/openstack/cloud/tests/unit/test_inventory.py b/openstack/tests/unit/cloud/test_inventory.py similarity index 100% rename from openstack/cloud/tests/unit/test_inventory.py rename to openstack/tests/unit/cloud/test_inventory.py diff --git a/openstack/cloud/tests/unit/test_keypair.py b/openstack/tests/unit/cloud/test_keypair.py similarity index 100% rename from openstack/cloud/tests/unit/test_keypair.py rename to openstack/tests/unit/cloud/test_keypair.py diff --git a/openstack/cloud/tests/unit/test_limits.py b/openstack/tests/unit/cloud/test_limits.py similarity index 100% rename from openstack/cloud/tests/unit/test_limits.py rename to openstack/tests/unit/cloud/test_limits.py diff --git a/openstack/cloud/tests/unit/test_magnum_services.py b/openstack/tests/unit/cloud/test_magnum_services.py similarity index 100% rename from openstack/cloud/tests/unit/test_magnum_services.py rename to openstack/tests/unit/cloud/test_magnum_services.py diff --git a/openstack/cloud/tests/unit/test_meta.py b/openstack/tests/unit/cloud/test_meta.py similarity index 100% rename from openstack/cloud/tests/unit/test_meta.py rename to openstack/tests/unit/cloud/test_meta.py diff --git a/openstack/cloud/tests/unit/test_network.py b/openstack/tests/unit/cloud/test_network.py similarity index 100% rename from openstack/cloud/tests/unit/test_network.py rename to openstack/tests/unit/cloud/test_network.py diff --git a/openstack/cloud/tests/unit/test_normalize.py b/openstack/tests/unit/cloud/test_normalize.py similarity index 100% rename from openstack/cloud/tests/unit/test_normalize.py rename to openstack/tests/unit/cloud/test_normalize.py diff --git a/openstack/cloud/tests/unit/test_object.py b/openstack/tests/unit/cloud/test_object.py similarity index 100% rename from openstack/cloud/tests/unit/test_object.py rename to openstack/tests/unit/cloud/test_object.py diff --git a/openstack/cloud/tests/unit/test_operator_noauth.py b/openstack/tests/unit/cloud/test_operator_noauth.py similarity index 100% rename from openstack/cloud/tests/unit/test_operator_noauth.py rename to openstack/tests/unit/cloud/test_operator_noauth.py diff --git a/openstack/cloud/tests/unit/test_port.py b/openstack/tests/unit/cloud/test_port.py similarity index 100% rename from openstack/cloud/tests/unit/test_port.py rename to openstack/tests/unit/cloud/test_port.py diff --git a/openstack/cloud/tests/unit/test_project.py b/openstack/tests/unit/cloud/test_project.py similarity index 100% rename from openstack/cloud/tests/unit/test_project.py rename to openstack/tests/unit/cloud/test_project.py diff --git a/openstack/cloud/tests/unit/test_qos_bandwidth_limit_rule.py b/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py similarity index 100% rename from openstack/cloud/tests/unit/test_qos_bandwidth_limit_rule.py rename to openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py diff --git a/openstack/cloud/tests/unit/test_qos_dscp_marking_rule.py b/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py similarity index 100% rename from openstack/cloud/tests/unit/test_qos_dscp_marking_rule.py rename to openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py diff --git a/openstack/cloud/tests/unit/test_qos_minimum_bandwidth_rule.py b/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py similarity index 100% rename from openstack/cloud/tests/unit/test_qos_minimum_bandwidth_rule.py rename to openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py diff --git a/openstack/cloud/tests/unit/test_qos_policy.py b/openstack/tests/unit/cloud/test_qos_policy.py similarity index 100% rename from openstack/cloud/tests/unit/test_qos_policy.py rename to openstack/tests/unit/cloud/test_qos_policy.py diff --git a/openstack/cloud/tests/unit/test_qos_rule_type.py b/openstack/tests/unit/cloud/test_qos_rule_type.py similarity index 100% rename from openstack/cloud/tests/unit/test_qos_rule_type.py rename to openstack/tests/unit/cloud/test_qos_rule_type.py diff --git a/openstack/cloud/tests/unit/test_quotas.py b/openstack/tests/unit/cloud/test_quotas.py similarity index 100% rename from openstack/cloud/tests/unit/test_quotas.py rename to openstack/tests/unit/cloud/test_quotas.py diff --git a/openstack/cloud/tests/unit/test_rebuild_server.py b/openstack/tests/unit/cloud/test_rebuild_server.py similarity index 100% rename from openstack/cloud/tests/unit/test_rebuild_server.py rename to openstack/tests/unit/cloud/test_rebuild_server.py diff --git a/openstack/cloud/tests/unit/test_recordset.py b/openstack/tests/unit/cloud/test_recordset.py similarity index 100% rename from openstack/cloud/tests/unit/test_recordset.py rename to openstack/tests/unit/cloud/test_recordset.py diff --git a/openstack/cloud/tests/unit/test_role_assignment.py b/openstack/tests/unit/cloud/test_role_assignment.py similarity index 100% rename from openstack/cloud/tests/unit/test_role_assignment.py rename to openstack/tests/unit/cloud/test_role_assignment.py diff --git a/openstack/cloud/tests/unit/test_router.py b/openstack/tests/unit/cloud/test_router.py similarity index 100% rename from openstack/cloud/tests/unit/test_router.py rename to openstack/tests/unit/cloud/test_router.py diff --git a/openstack/cloud/tests/unit/test_security_groups.py b/openstack/tests/unit/cloud/test_security_groups.py similarity index 100% rename from openstack/cloud/tests/unit/test_security_groups.py rename to openstack/tests/unit/cloud/test_security_groups.py diff --git a/openstack/cloud/tests/unit/test_server_console.py b/openstack/tests/unit/cloud/test_server_console.py similarity index 100% rename from openstack/cloud/tests/unit/test_server_console.py rename to openstack/tests/unit/cloud/test_server_console.py diff --git a/openstack/cloud/tests/unit/test_server_delete_metadata.py b/openstack/tests/unit/cloud/test_server_delete_metadata.py similarity index 100% rename from openstack/cloud/tests/unit/test_server_delete_metadata.py rename to openstack/tests/unit/cloud/test_server_delete_metadata.py diff --git a/openstack/cloud/tests/unit/test_server_group.py b/openstack/tests/unit/cloud/test_server_group.py similarity index 100% rename from openstack/cloud/tests/unit/test_server_group.py rename to openstack/tests/unit/cloud/test_server_group.py diff --git a/openstack/cloud/tests/unit/test_server_set_metadata.py b/openstack/tests/unit/cloud/test_server_set_metadata.py similarity index 100% rename from openstack/cloud/tests/unit/test_server_set_metadata.py rename to openstack/tests/unit/cloud/test_server_set_metadata.py diff --git a/openstack/cloud/tests/unit/test_services.py b/openstack/tests/unit/cloud/test_services.py similarity index 100% rename from openstack/cloud/tests/unit/test_services.py rename to openstack/tests/unit/cloud/test_services.py diff --git a/openstack/cloud/tests/unit/test_shade.py b/openstack/tests/unit/cloud/test_shade.py similarity index 100% rename from openstack/cloud/tests/unit/test_shade.py rename to openstack/tests/unit/cloud/test_shade.py diff --git a/openstack/cloud/tests/unit/test_shade_operator.py b/openstack/tests/unit/cloud/test_shade_operator.py similarity index 100% rename from openstack/cloud/tests/unit/test_shade_operator.py rename to openstack/tests/unit/cloud/test_shade_operator.py diff --git a/openstack/cloud/tests/unit/test_stack.py b/openstack/tests/unit/cloud/test_stack.py similarity index 100% rename from openstack/cloud/tests/unit/test_stack.py rename to openstack/tests/unit/cloud/test_stack.py diff --git a/openstack/cloud/tests/unit/test_subnet.py b/openstack/tests/unit/cloud/test_subnet.py similarity index 100% rename from openstack/cloud/tests/unit/test_subnet.py rename to openstack/tests/unit/cloud/test_subnet.py diff --git a/openstack/cloud/tests/unit/test_task_manager.py b/openstack/tests/unit/cloud/test_task_manager.py similarity index 100% rename from openstack/cloud/tests/unit/test_task_manager.py rename to openstack/tests/unit/cloud/test_task_manager.py diff --git a/openstack/cloud/tests/unit/test_update_server.py b/openstack/tests/unit/cloud/test_update_server.py similarity index 100% rename from openstack/cloud/tests/unit/test_update_server.py rename to openstack/tests/unit/cloud/test_update_server.py diff --git a/openstack/cloud/tests/unit/test_usage.py b/openstack/tests/unit/cloud/test_usage.py similarity index 100% rename from openstack/cloud/tests/unit/test_usage.py rename to openstack/tests/unit/cloud/test_usage.py diff --git a/openstack/cloud/tests/unit/test_users.py b/openstack/tests/unit/cloud/test_users.py similarity index 100% rename from openstack/cloud/tests/unit/test_users.py rename to openstack/tests/unit/cloud/test_users.py diff --git a/openstack/cloud/tests/unit/test_volume.py b/openstack/tests/unit/cloud/test_volume.py similarity index 100% rename from openstack/cloud/tests/unit/test_volume.py rename to openstack/tests/unit/cloud/test_volume.py diff --git a/openstack/cloud/tests/unit/test_volume_access.py b/openstack/tests/unit/cloud/test_volume_access.py similarity index 100% rename from openstack/cloud/tests/unit/test_volume_access.py rename to openstack/tests/unit/cloud/test_volume_access.py diff --git a/openstack/cloud/tests/unit/test_volume_backups.py b/openstack/tests/unit/cloud/test_volume_backups.py similarity index 100% rename from openstack/cloud/tests/unit/test_volume_backups.py rename to openstack/tests/unit/cloud/test_volume_backups.py diff --git a/openstack/cloud/tests/unit/test_zone.py b/openstack/tests/unit/cloud/test_zone.py similarity index 100% rename from openstack/cloud/tests/unit/test_zone.py rename to openstack/tests/unit/cloud/test_zone.py diff --git a/openstack/cloud/tests/unit/fixtures/baremetal.json b/openstack/tests/unit/fixtures/baremetal.json similarity index 100% rename from openstack/cloud/tests/unit/fixtures/baremetal.json rename to openstack/tests/unit/fixtures/baremetal.json diff --git a/openstack/cloud/tests/unit/fixtures/catalog-v2.json b/openstack/tests/unit/fixtures/catalog-v2.json similarity index 100% rename from openstack/cloud/tests/unit/fixtures/catalog-v2.json rename to openstack/tests/unit/fixtures/catalog-v2.json diff --git a/openstack/cloud/tests/unit/fixtures/catalog-v3.json b/openstack/tests/unit/fixtures/catalog-v3.json similarity index 100% rename from openstack/cloud/tests/unit/fixtures/catalog-v3.json rename to openstack/tests/unit/fixtures/catalog-v3.json diff --git a/openstack/cloud/tests/unit/fixtures/clouds/clouds.yaml b/openstack/tests/unit/fixtures/clouds/clouds.yaml similarity index 100% rename from openstack/cloud/tests/unit/fixtures/clouds/clouds.yaml rename to openstack/tests/unit/fixtures/clouds/clouds.yaml diff --git a/openstack/cloud/tests/unit/fixtures/clouds/clouds_cache.yaml b/openstack/tests/unit/fixtures/clouds/clouds_cache.yaml similarity index 100% rename from openstack/cloud/tests/unit/fixtures/clouds/clouds_cache.yaml rename to openstack/tests/unit/fixtures/clouds/clouds_cache.yaml diff --git a/openstack/cloud/tests/unit/fixtures/discovery.json b/openstack/tests/unit/fixtures/discovery.json similarity index 100% rename from openstack/cloud/tests/unit/fixtures/discovery.json rename to openstack/tests/unit/fixtures/discovery.json diff --git a/openstack/cloud/tests/unit/fixtures/dns.json b/openstack/tests/unit/fixtures/dns.json similarity index 100% rename from openstack/cloud/tests/unit/fixtures/dns.json rename to openstack/tests/unit/fixtures/dns.json diff --git a/openstack/cloud/tests/unit/fixtures/image-version-broken.json b/openstack/tests/unit/fixtures/image-version-broken.json similarity index 100% rename from openstack/cloud/tests/unit/fixtures/image-version-broken.json rename to openstack/tests/unit/fixtures/image-version-broken.json diff --git a/openstack/cloud/tests/unit/fixtures/image-version-v1.json b/openstack/tests/unit/fixtures/image-version-v1.json similarity index 100% rename from openstack/cloud/tests/unit/fixtures/image-version-v1.json rename to openstack/tests/unit/fixtures/image-version-v1.json diff --git a/openstack/cloud/tests/unit/fixtures/image-version-v2.json b/openstack/tests/unit/fixtures/image-version-v2.json similarity index 100% rename from openstack/cloud/tests/unit/fixtures/image-version-v2.json rename to openstack/tests/unit/fixtures/image-version-v2.json diff --git a/openstack/cloud/tests/unit/fixtures/image-version.json b/openstack/tests/unit/fixtures/image-version.json similarity index 100% rename from openstack/cloud/tests/unit/fixtures/image-version.json rename to openstack/tests/unit/fixtures/image-version.json diff --git a/os-client-config/.coveragerc b/os-client-config/.coveragerc deleted file mode 100644 index 3c1292222..000000000 --- a/os-client-config/.coveragerc +++ /dev/null @@ -1,7 +0,0 @@ -[run] -branch = True -source = os_client_config -omit = os_client_config/tests/*,os_client_config/openstack/* - -[report] -ignore_errors = True diff --git a/os-client-config/.gitignore b/os-client-config/.gitignore deleted file mode 100644 index c24b89b8a..000000000 --- a/os-client-config/.gitignore +++ /dev/null @@ -1,55 +0,0 @@ -*.py[cod] -.venv - -# C extensions -*.so - -# Packages -*.egg -*.egg-info -dist -build -eggs -parts -bin -var -sdist -develop-eggs -.installed.cfg -lib -lib64 - -# Installer logs -pip-log.txt - -# Unit test / coverage reports -cover -.coverage -.tox -nosetests.xml -.stestr/ -.testrepository - -# Translations -*.mo - -# Mr Developer -.mr.developer.cfg -.project -.pydevproject - -# Complexity -output/*.html -output/*/index.html - -# Sphinx -doc/build - -# pbr generates these -AUTHORS -ChangeLog - -# Editors -*~ -.*.swp -.*sw? diff --git a/os-client-config/.gitreview b/os-client-config/.gitreview deleted file mode 100644 index 5ba7eddc1..000000000 --- a/os-client-config/.gitreview +++ /dev/null @@ -1,4 +0,0 @@ -[gerrit] -host=review.openstack.org -port=29418 -project=openstack/os-client-config.git diff --git a/os-client-config/.mailmap b/os-client-config/.mailmap deleted file mode 100644 index cc92f17b8..000000000 --- a/os-client-config/.mailmap +++ /dev/null @@ -1,3 +0,0 @@ -# Format is: -# -# \ No newline at end of file diff --git a/os-client-config/.stestr.conf b/os-client-config/.stestr.conf deleted file mode 100644 index 792382206..000000000 --- a/os-client-config/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=. -top_dir=./ diff --git a/os-client-config/.testr.conf b/os-client-config/.testr.conf deleted file mode 100644 index fb622677a..000000000 --- a/os-client-config/.testr.conf +++ /dev/null @@ -1,7 +0,0 @@ -[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 ./ . $LISTOPT $IDOPTION -test_id_option=--load-list $IDFILE -test_list_option=--list \ No newline at end of file diff --git a/os-client-config/CONTRIBUTING.rst b/os-client-config/CONTRIBUTING.rst deleted file mode 100644 index 1990ecf25..000000000 --- a/os-client-config/CONTRIBUTING.rst +++ /dev/null @@ -1,16 +0,0 @@ -If you would like to contribute to the development of OpenStack, -you must follow the steps in this page: - - http://docs.openstack.org/infra/manual/developers.html - -Once those steps have been completed, changes to OpenStack -should be submitted for review via the Gerrit tool, following -the workflow documented at: - - http://docs.openstack.org/infra/manual/developers.html#development-workflow - -Pull requests submitted through GitHub will be ignored. - -Bugs should be filed on Launchpad, not GitHub: - - https://bugs.launchpad.net/os-client-config \ No newline at end of file diff --git a/os-client-config/HACKING.rst b/os-client-config/HACKING.rst deleted file mode 100644 index aab08e34e..000000000 --- a/os-client-config/HACKING.rst +++ /dev/null @@ -1,4 +0,0 @@ -os-client-config Style Commandments -=============================================== - -Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest diff --git a/os-client-config/LICENSE b/os-client-config/LICENSE deleted file mode 100644 index 67db85882..000000000 --- a/os-client-config/LICENSE +++ /dev/null @@ -1,175 +0,0 @@ - - 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. diff --git a/os-client-config/README.rst b/os-client-config/README.rst deleted file mode 100644 index 35ff07b43..000000000 --- a/os-client-config/README.rst +++ /dev/null @@ -1,25 +0,0 @@ -================ -os-client-config -================ - -.. image:: http://governance.openstack.org/badges/os-client-config.svg - :target: http://governance.openstack.org/reference/tags/index.html - -`os-client-config` is a library for collecting client configuration for -using an OpenStack cloud in a consistent and comprehensive manner. It -will find cloud config for as few as 1 cloud and as many as you want to -put in a config file. It will read environment variables and config files, -and it also contains some vendor specific default values so that you don't -have to know extra info to use OpenStack - -* If you have a config file, you will get the clouds listed in it -* If you have environment variables, you will get a cloud named `envvars` -* If you have neither, you will get a cloud named `defaults` with base defaults - -Source ------- - -* Free software: Apache license -* Documentation: http://docs.openstack.org/os-client-config/latest -* Source: http://git.openstack.org/cgit/openstack/os-client-config -* Bugs: http://bugs.launchpad.net/os-client-config diff --git a/os-client-config/doc/source/conf.py b/os-client-config/doc/source/conf.py deleted file mode 100755 index cbd9888d1..000000000 --- a/os-client-config/doc/source/conf.py +++ /dev/null @@ -1,86 +0,0 @@ -# -*- 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. - -import os -import sys - -import openstackdocstheme - -sys.path.insert(0, os.path.abspath('../..')) -# -- General configuration ---------------------------------------------------- - -# 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.intersphinx', - 'reno.sphinxext', - 'openstackdocstheme', -] - -# openstackdocstheme options -repository_name = 'openstack/os-client-config' -bug_project = 'os-client-config' -bug_tag = '' -html_last_updated_fmt = '%Y-%m-%d %H:%M' - -# autodoc generation is a bit aggressive and a nuisance when doing heavy -# text edit cycles. -# execute "export SPHINX_DEBUG=1" in your terminal to disable - -# The suffix of source filenames. -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'os-client-config' -copyright = u'2015, various OpenStack developers' - -# 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 - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# -- Options for HTML output -------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. Major themes that come with -# Sphinx are currently 'default' and 'sphinxdoc'. -# html_theme_path = ["."] -# html_theme = '_theme' -# html_static_path = ['static'] -html_theme = 'openstackdocs' -html_theme_path = [openstackdocstheme.get_html_theme_path()] - -# Output file base name for HTML help builder. -htmlhelp_basename = '%sdoc' % project - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass -# [howto/manual]). -latex_documents = [ - ('index', - '%s.tex' % project, - u'%s Documentation' % project, - u'OpenStack Foundation', 'manual'), -] - -# Example configuration for intersphinx: refer to the Python standard library. -#intersphinx_mapping = {'http://docs.python.org/': None} diff --git a/os-client-config/doc/source/contributor/index.rst b/os-client-config/doc/source/contributor/index.rst deleted file mode 100644 index 2aa070771..000000000 --- a/os-client-config/doc/source/contributor/index.rst +++ /dev/null @@ -1,4 +0,0 @@ -============ -Contributing -============ -.. include:: ../../../CONTRIBUTING.rst diff --git a/os-client-config/doc/source/index.rst b/os-client-config/doc/source/index.rst deleted file mode 100644 index 5a407adcc..000000000 --- a/os-client-config/doc/source/index.rst +++ /dev/null @@ -1,32 +0,0 @@ -================ -os-client-config -================ - -.. image:: http://governance.openstack.org/badges/os-client-config.svg - :target: http://governance.openstack.org/reference/tags/index.html - -`os-client-config` is a library for collecting client configuration for -using an OpenStack cloud in a consistent and comprehensive manner. It -will find cloud config for as few as 1 cloud and as many as you want to -put in a config file. It will read environment variables and config files, -and it also contains some vendor specific default values so that you don't -have to know extra info to use OpenStack - -* If you have a config file, you will get the clouds listed in it -* If you have environment variables, you will get a cloud named `envvars` -* If you have neither, you will get a cloud named `defaults` with base defaults - -.. toctree:: - :maxdepth: 2 - - install/index - user/index - reference/index - contributor/index - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/os-client-config/doc/source/install/index.rst b/os-client-config/doc/source/install/index.rst deleted file mode 100644 index 48bbc2f20..000000000 --- a/os-client-config/doc/source/install/index.rst +++ /dev/null @@ -1,12 +0,0 @@ -============ -Installation -============ - -At the command line:: - - $ pip install os-client-config - -Or, if you have virtualenvwrapper installed:: - - $ mkvirtualenv os-client-config - $ pip install os-client-config \ No newline at end of file diff --git a/os-client-config/doc/source/user/releasenotes.rst b/os-client-config/doc/source/user/releasenotes.rst deleted file mode 100644 index 9f41b7e14..000000000 --- a/os-client-config/doc/source/user/releasenotes.rst +++ /dev/null @@ -1,6 +0,0 @@ -============= -Release Notes -============= - -Release notes for `os-client-config` can be found at -http://docs.openstack.org/releasenotes/os-client-config/ diff --git a/os-client-config/requirements.txt b/os-client-config/requirements.txt deleted file mode 100644 index 6c609105e..000000000 --- a/os-client-config/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -# 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. -PyYAML>=3.10 # MIT -appdirs>=1.3.0 # MIT License -keystoneauth1>=3.2.0 # Apache-2.0 -requestsexceptions>=1.2.0 # Apache-2.0 diff --git a/os-client-config/setup.cfg b/os-client-config/setup.cfg deleted file mode 100644 index 7149cdf8e..000000000 --- a/os-client-config/setup.cfg +++ /dev/null @@ -1,35 +0,0 @@ -[metadata] -name = os-client-config -summary = OpenStack Client Configuation Library -description-file = - README.rst -author = OpenStack -author-email = openstack-dev@lists.openstack.org -home-page = https://docs.openstack.org/os-client-config/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 :: 2 - Programming Language :: Python :: 2.7 - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - -[files] -packages = - os_client_config - -[build_sphinx] -source-dir = doc/source -build-dir = doc/build -all_files = 1 -warning-is-error = 1 - -[upload_sphinx] -upload-dir = doc/build/html - -[wheel] -universal = 1 diff --git a/os-client-config/setup.py b/os-client-config/setup.py deleted file mode 100644 index 566d84432..000000000 --- a/os-client-config/setup.py +++ /dev/null @@ -1,29 +0,0 @@ -# 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) diff --git a/os-client-config/test-requirements.txt b/os-client-config/test-requirements.txt deleted file mode 100644 index 73d982263..000000000 --- a/os-client-config/test-requirements.txt +++ /dev/null @@ -1,21 +0,0 @@ -# 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.13.0,<0.14,>=0.12.0 # Apache-2.0 - -coverage!=4.4,>=4.0 # Apache-2.0 -docutils>=0.11 # OSI-Approved Open Source, Public Domain -extras>=0.0.3 # MIT -fixtures>=3.0.0 # Apache-2.0/BSD -jsonschema<3.0.0,>=2.6.0 # MIT -mock>=2.0.0 # BSD -python-glanceclient>=2.8.0 # Apache-2.0 -python-subunit>=0.0.18 # Apache-2.0/BSD -sphinx>=1.6.2 # BSD -openstackdocstheme>=1.17.0 # Apache-2.0 -oslotest>=1.10.0 # Apache-2.0 -reno>=2.5.0 # Apache-2.0 -testrepository>=0.0.18 # Apache-2.0/BSD -testscenarios>=0.4 # Apache-2.0/BSD -testtools>=1.4.0 # MIT diff --git a/os-client-config/tox.ini b/os-client-config/tox.ini deleted file mode 100644 index 57feb6d0b..000000000 --- a/os-client-config/tox.ini +++ /dev/null @@ -1,45 +0,0 @@ -[tox] -minversion = 1.6 -envlist = py35,py27,pypy,pep8 -skipsdist = True - -[testenv] -usedevelop = True -passenv = ZUUL_CACHE_DIR - REQUIREMENTS_PIP_LOCATION -install_command = {toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} -setenv = - VIRTUAL_ENV={envdir} - BRANCH_NAME=master - CLIENT_NAME=os-client-config - OS_STDOUT_CAPTURE=1 - OS_STDERR_CAPTURE=1 - OS_TEST_TIMEOUT=60 -deps = -r{toxinidir}/test-requirements.txt -commands = python setup.py testr --slowest --testr-args='{posargs}' - -[testenv:pep8] -commands = flake8 - -[testenv:venv] -commands = {posargs} - -[testenv:cover] -commands = python setup.py test --coverage --coverage-package-name=os_client_config --testr-args='{posargs}' - -[testenv:docs] -deps = - {[testenv]deps} - readme -commands = - python setup.py build_sphinx - python setup.py check -r -s - -[testenv:releasenotes] -commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html - -[flake8] -show-source = True -builtins = _ -exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,releasenotes/source/conf.py - diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py index 8e1265344..1a6db74cd 100644 --- a/releasenotes/source/conf.py +++ b/releasenotes/source/conf.py @@ -43,7 +43,7 @@ ] # openstackdocstheme options -repository_name = 'openstack-infra/shade' +repository_name = 'openstack/python-openstacksdk' bug_project = '760' bug_tag = '' html_last_updated_fmt = '%Y-%m-%d %H:%M' @@ -61,15 +61,15 @@ master_doc = 'index' # General information about the project. -project = u'Shade Release Notes' -copyright = u'2017, Shade Developers' +project = u'OpenStack SDK Release Notes' +copyright = u'2017, Various members of the OpenStack Foundation' # 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. # import pbr.version -version_info = pbr.version.VersionInfo('shade') +version_info = pbr.version.VersionInfo('openstacksdk') # The full version, including alpha/beta/rc tags. release = version_info.version_string_with_vcs() # The short X.Y version. diff --git a/requirements.txt b/requirements.txt index f9aea340e..1a7ad95d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,9 +2,21 @@ # 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 -jsonpatch>=1.1 # BSD +PyYAML>=3.10 # MIT +appdirs>=1.3.0 # MIT License +requestsexceptions>=1.2.0 # Apache-2.0 +jsonpatch>=1.16 # BSD six>=1.9.0 # MIT stevedore>=1.20.0 # Apache-2.0 -os-client-config>=1.28.0 # Apache-2.0 -keystoneauth1>=3.1.0 # Apache-2.0 +keystoneauth1>=3.2.0 # Apache-2.0 deprecation>=1.0 # Apache-2.0 + +munch>=2.1.0 # MIT +decorator>=3.4.0 # BSD +jmespath>=0.9.0 # MIT +ipaddress>=1.0.16;python_version<'3.3' # PSF +futures>=3.0;python_version=='2.7' or python_version=='2.6' # BSD +iso8601>=0.1.11 # MIT +netifaces>=0.10.4 # MIT + +dogpile.cache>=0.6.2 # BSD diff --git a/setup.cfg b/setup.cfg index 11118defb..37e19d4bb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,7 +16,6 @@ classifier = Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 - Programming Language :: Python :: 3.3 Programming Language :: Python :: 3.5 [files] diff --git a/shade/.coveragerc b/shade/.coveragerc deleted file mode 100644 index ca361e59b..000000000 --- a/shade/.coveragerc +++ /dev/null @@ -1,7 +0,0 @@ -[run] -branch = True -source = shade -omit = shade/tests/* - -[report] -ignore_errors = True diff --git a/shade/.gitignore b/shade/.gitignore deleted file mode 100644 index 1f0139551..000000000 --- a/shade/.gitignore +++ /dev/null @@ -1,54 +0,0 @@ -*.py[cod] - -# C extensions -*.so - -# Packages -.eggs -*.egg -*.egg-info -dist -build -eggs -parts -bin -var -sdist -develop-eggs -.installed.cfg -lib -lib64 - -# Installer logs -pip-log.txt - -# Unit test / coverage reports -.coverage -cover -.tox -nosetests.xml -.testrepository -.stestr - -# Translations -*.mo - -# Mr Developer -.mr.developer.cfg -.project -.pydevproject - -# Complexity -output/*.html -output/*/index.html - -# Sphinx -doc/build - -# pbr generates these -AUTHORS -ChangeLog - -# Editors -*~ -.*.swp diff --git a/shade/.gitreview b/shade/.gitreview deleted file mode 100644 index 650bebe84..000000000 --- a/shade/.gitreview +++ /dev/null @@ -1,4 +0,0 @@ -[gerrit] -host=review.openstack.org -port=29418 -project=openstack-infra/openstack.cloud.git diff --git a/shade/.mailmap b/shade/.mailmap deleted file mode 100644 index 4d8361ef0..000000000 --- a/shade/.mailmap +++ /dev/null @@ -1,4 +0,0 @@ -# Format is: -# -# - diff --git a/shade/.stestr.conf b/shade/.stestr.conf deleted file mode 100644 index d90a44466..000000000 --- a/shade/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./shade/tests/unit -top_dir=./ diff --git a/shade/.zuul.yaml b/shade/.zuul.yaml deleted file mode 100644 index eb219b914..000000000 --- a/shade/.zuul.yaml +++ /dev/null @@ -1,8 +0,0 @@ -- project: - name: openstack-infra/shade - templates: - - publish-to-pypi - - publish-openstack-python-docs - check: - jobs: - - openstack-tox-py35 diff --git a/shade/CONTRIBUTING.rst b/shade/CONTRIBUTING.rst deleted file mode 100644 index 798b5b5af..000000000 --- a/shade/CONTRIBUTING.rst +++ /dev/null @@ -1,44 +0,0 @@ -.. _contributing: - -===================== -Contributing to shade -===================== - -If you're interested in contributing to the shade project, -the following will help get you started. - -Contributor License Agreement ------------------------------ - -.. index:: - single: license; agreement - -In order to contribute to the shade project, you need to have -signed OpenStack's contributor's agreement. - -.. seealso:: - - * http://wiki.openstack.org/HowToContribute - * http://wiki.openstack.org/CLA - -Project Hosting Details -------------------------- - -Project Documentation - http://docs.openstack.org/infra/shade/ - -Bug tracker - http://storyboard.openstack.org - -Mailing list (prefix subjects with ``[shade]`` for faster responses) - http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-infra - -Code Hosting - https://git.openstack.org/cgit/openstack-infra/shade - -Code Review - https://review.openstack.org/#/q/status:open+project:openstack-infra/shade,n,z - - Please read `GerritWorkflow`_ before sending your first patch for review. - -.. _GerritWorkflow: https://wiki.openstack.org/wiki/GerritWorkflow diff --git a/shade/HACKING.rst b/shade/HACKING.rst deleted file mode 100644 index 9c8af4d35..000000000 --- a/shade/HACKING.rst +++ /dev/null @@ -1,49 +0,0 @@ -shade Style Commandments -======================== - -Read the OpenStack Style Commandments -http://docs.openstack.org/developer/hacking/ - -Indentation ------------ - -PEP-8 allows for 'visual' indentation. Do not use it. Visual indentation looks -like this: - -.. code-block:: python - - return_value = self.some_method(arg1, arg1, - arg3, arg4) - -Visual indentation makes refactoring the code base unneccesarily hard. - -Instead of visual indentation, use this: - -.. code-block:: python - - return_value = self.some_method( - arg1, arg1, arg3, arg4) - -That way, if some_method ever needs to be renamed, the only line that needs -to be touched is the line with some_method. Additionaly, if you need to -line break at the top of a block, please indent the continuation line -an additional 4 spaces, like this: - -.. code-block:: python - - for val in self.some_method( - arg1, arg1, arg3, arg4): - self.do_something_awesome() - -Neither of these are 'mandated' by PEP-8. However, they are prevailing styles -within this code base. - -Unit Tests ----------- - -Unit tests should be virtually instant. If a unit test takes more than 1 second -to run, it is a bad unit test. Honestly, 1 second is too slow. - -All unit test classes should subclass `openstack.cloud.tests.unit.base.BaseTestCase`. The -base TestCase class takes care of properly creating `OpenStackCloud` objects -in a way that protects against local environment. diff --git a/shade/LICENSE b/shade/LICENSE deleted file mode 100644 index 67db85882..000000000 --- a/shade/LICENSE +++ /dev/null @@ -1,175 +0,0 @@ - - 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. diff --git a/shade/MANIFEST.in b/shade/MANIFEST.in deleted file mode 100644 index 90f8a7aef..000000000 --- a/shade/MANIFEST.in +++ /dev/null @@ -1,6 +0,0 @@ -include AUTHORS -include ChangeLog -exclude .gitignore -exclude .gitreview - -global-exclude *.pyc \ No newline at end of file diff --git a/shade/README.rst b/shade/README.rst deleted file mode 100644 index c43a16699..000000000 --- a/shade/README.rst +++ /dev/null @@ -1,79 +0,0 @@ -Introduction -============ - -shade is a simple client library for interacting with OpenStack clouds. The -key word here is *simple*. Clouds can do many many many things - but there are -probably only about 10 of them that most people care about with any -regularity. If you want to do complicated things, you should probably use -the lower level client libraries - or even the REST API directly. However, -if what you want is to be able to write an application that talks to clouds -no matter what crazy choices the deployer has made in an attempt to be -more hipster than their self-entitled narcissist peers, then shade is for you. - -shade started its life as some code inside of ansible. ansible has a bunch -of different OpenStack related modules, and there was a ton of duplicated -code. Eventually, between refactoring that duplication into an internal -library, and adding logic and features that the OpenStack Infra team had -developed to run client applications at scale, it turned out that we'd written -nine-tenths of what we'd need to have a standalone library. - -.. _example: - -Example -======= - -Sometimes an example is nice. - -#. Create a ``clouds.yml`` file:: - - clouds: - mordred: - region_name: RegionOne - auth: - username: 'mordred' - password: XXXXXXX - project_name: 'shade' - auth_url: 'https://montytaylor-sjc.openstack.blueboxgrid.com:5001/v2.0' - - Please note: *os-client-config* will look for a file called ``clouds.yaml`` - in the following locations: - - * Current Directory - * ``~/.config/openstack`` - * ``/etc/openstack`` - - More information at https://pypi.python.org/pypi/os-client-config - - -#. Create a server with *shade*, configured with the ``clouds.yml`` file:: - - import openstack.cloud - - # Initialize and turn on debug logging - openstack.cloud.simple_logging(debug=True) - - # Initialize cloud - # Cloud configs are read with os-client-config - cloud = openstack.cloud.openstack_cloud(cloud='mordred') - - # Upload an image to the cloud - image = cloud.create_image( - 'ubuntu-trusty', filename='ubuntu-trusty.qcow2', wait=True) - - # Find a flavor with at least 512M of RAM - flavor = cloud.get_flavor_by_ram(512) - - # Boot a server, wait for it to boot, and then do whatever is needed - # to get a public ip for it. - cloud.create_server( - 'my-server', image=image, flavor=flavor, wait=True, auto_ip=True) - - -Links -===== - -* `Issue Tracker `_ -* `Code Review `_ -* `Documentation `_ -* `PyPI `_ -* `Mailing list `_ diff --git a/shade/devstack/plugin.sh b/shade/devstack/plugin.sh deleted file mode 100644 index 59c130be2..000000000 --- a/shade/devstack/plugin.sh +++ /dev/null @@ -1,54 +0,0 @@ -# Install and configure **shade** library in devstack -# -# To enable shade in devstack add an entry to local.conf that looks like -# -# [[local|localrc]] -# enable_plugin shade git://git.openstack.org/openstack-infra/shade - -function preinstall_shade { - : -} - -function install_shade { - if use_library_from_git "shade"; then - # don't clone, it'll be done by the plugin install - setup_dev_lib "shade" - else - pip_install "shade" - fi -} - -function configure_shade { - : -} - -function initialize_shade { - : -} - -function unstack_shade { - : -} - -function clean_shade { - : -} - -# This is the main for plugin.sh -if [[ "$1" == "stack" && "$2" == "pre-install" ]]; then - preinstall_shade -elif [[ "$1" == "stack" && "$2" == "install" ]]; then - install_shade -elif [[ "$1" == "stack" && "$2" == "post-config" ]]; then - configure_shade -elif [[ "$1" == "stack" && "$2" == "extra" ]]; then - initialize_shade -fi - -if [[ "$1" == "unstack" ]]; then - unstack_shade -fi - -if [[ "$1" == "clean" ]]; then - clean_shade -fi diff --git a/shade/doc/source/conf.py b/shade/doc/source/conf.py deleted file mode 100755 index aaf41b319..000000000 --- a/shade/doc/source/conf.py +++ /dev/null @@ -1,47 +0,0 @@ -import os -import sys - -sys.path.insert(0, os.path.abspath('../..')) - -extensions = [ - 'sphinx.ext.autodoc', - 'openstackdocstheme', - 'reno.sphinxext' -] - -# openstackdocstheme options -repository_name = 'openstack-infra/shade' -bug_project = '760' -bug_tag = '' -html_last_updated_fmt = '%Y-%m-%d %H:%M' -html_theme = 'openstackdocs' - -# The suffix of source filenames. -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'shade' -copyright = u'2014 Hewlett-Packard Development Company, L.P.' - -# 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 - -# Output file base name for HTML help builder. -htmlhelp_basename = '%sdoc' % project - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass -# [howto/manual]). -latex_documents = [ - ('index', - '%s.tex' % project, - u'%s Documentation' % project, - u'Monty Taylor', 'manual'), -] diff --git a/shade/doc/source/contributor/index.rst b/shade/doc/source/contributor/index.rst deleted file mode 100644 index b032c3728..000000000 --- a/shade/doc/source/contributor/index.rst +++ /dev/null @@ -1,9 +0,0 @@ -========================= - Shade Contributor Guide -========================= - -.. toctree:: - :maxdepth: 2 - - contributing - coding diff --git a/shade/doc/source/index.rst b/shade/doc/source/index.rst deleted file mode 100644 index 5235e1664..000000000 --- a/shade/doc/source/index.rst +++ /dev/null @@ -1,31 +0,0 @@ -.. shade documentation master file, created by - sphinx-quickstart on Tue Jul 9 22:26:36 2013. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -================================= -Welcome to shade's documentation! -================================= - -Contents: - -.. toctree:: - :maxdepth: 2 - - install/index - user/index - contributor/index - -.. releasenotes contains a lot of sections, toctree with maxdepth 1 is used. -.. toctree:: - :maxdepth: 1 - - releasenotes/index - -.. include:: ../../README.rst - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`search` diff --git a/shade/doc/source/install/index.rst b/shade/doc/source/install/index.rst deleted file mode 100644 index 9699e779e..000000000 --- a/shade/doc/source/install/index.rst +++ /dev/null @@ -1,12 +0,0 @@ -============ -Installation -============ - -At the command line:: - - $ pip install shade - -Or, if you have virtualenv wrapper installed:: - - $ mkvirtualenv shade - $ pip install shade diff --git a/shade/doc/source/releasenotes/index.rst b/shade/doc/source/releasenotes/index.rst deleted file mode 100644 index 2a4bceb4e..000000000 --- a/shade/doc/source/releasenotes/index.rst +++ /dev/null @@ -1,5 +0,0 @@ -============= -Release Notes -============= - -.. release-notes:: diff --git a/shade/requirements.txt b/shade/requirements.txt deleted file mode 100644 index 5a3916192..000000000 --- a/shade/requirements.txt +++ /dev/null @@ -1,26 +0,0 @@ -# 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 - -munch>=2.1.0 # MIT -decorator>=3.4.0 # BSD -jmespath>=0.9.0 # MIT -jsonpatch>=1.16 # BSD -ipaddress>=1.0.16;python_version<'3.3' # PSF -os-client-config>=1.28.0 # Apache-2.0 -# These two are here to prevent issues with version pin mismatches from our -# client library transitive depends. -# Babel can be removed when ironicclient is removed (because of openstackclient -# transitive depend) -Babel!=2.4.0,>=2.3.4 # BSD -requestsexceptions>=1.2.0 # Apache-2.0 -six>=1.9.0 # MIT -futures>=3.0;python_version=='2.7' or python_version=='2.6' # BSD -iso8601>=0.1.11 # MIT - -keystoneauth1>=3.2.0 # Apache-2.0 -netifaces>=0.10.4 # MIT -python-ironicclient>=1.14.0 # Apache-2.0 - -dogpile.cache>=0.6.2 # BSD diff --git a/shade/setup.cfg b/shade/setup.cfg deleted file mode 100644 index 0ad4c36c8..000000000 --- a/shade/setup.cfg +++ /dev/null @@ -1,28 +0,0 @@ -[metadata] -name = shade -summary = Simple client library for interacting with OpenStack clouds -description-file = - README.rst -author = OpenStack -author-email = openstack-dev@lists.openstack.org -home-page = http://docs.openstack.org/shade/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 :: 2 - Programming Language :: Python :: 2.7 - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 - -[build_sphinx] -source-dir = doc/source -build-dir = doc/build -all_files = 1 -warning-is-error = 1 - -[upload_sphinx] -upload-dir = doc/build/html diff --git a/shade/setup.py b/shade/setup.py deleted file mode 100644 index 566d84432..000000000 --- a/shade/setup.py +++ /dev/null @@ -1,29 +0,0 @@ -# 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) diff --git a/shade/test-requirements.txt b/shade/test-requirements.txt deleted file mode 100644 index b0b3d5028..000000000 --- a/shade/test-requirements.txt +++ /dev/null @@ -1,17 +0,0 @@ -# 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.12,>=0.11.0 # Apache-2.0 - -coverage!=4.4,>=4.0 # Apache-2.0 -fixtures>=3.0.0 # Apache-2.0/BSD -mock>=2.0.0 # BSD -python-subunit>=0.0.18 # Apache-2.0/BSD -openstackdocstheme>=1.17.0 # Apache-2.0 -oslotest>=1.10.0 # Apache-2.0 -requests-mock>=1.1.0 # Apache-2.0 -sphinx>=1.6.2 # BSD -stestr>=1.0.0 # Apache-2.0 -testscenarios>=0.4 # Apache-2.0/BSD -testtools>=1.4.0 # MIT -reno>=2.5.0 # Apache-2.0 diff --git a/shade/tools/tox_install.sh b/shade/tools/tox_install.sh deleted file mode 100755 index 43468e450..000000000 --- a/shade/tools/tox_install.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash - -# Client constraint file contains this client version pin that is in conflict -# with installing the client from source. We should remove the version pin in -# the constraints file before applying it for from-source installation. - -CONSTRAINTS_FILE=$1 -shift 1 - -set -e - -# NOTE(tonyb): Place this in the tox enviroment's log dir so it will get -# published to logs.openstack.org for easy debugging. -localfile="$VIRTUAL_ENV/log/upper-constraints.txt" - -if [[ $CONSTRAINTS_FILE != http* ]]; then - CONSTRAINTS_FILE=file://$CONSTRAINTS_FILE -fi -# NOTE(tonyb): need to add curl to bindep.txt if the project supports bindep -curl $CONSTRAINTS_FILE --insecure --progress-bar --output $localfile - -pip install -c$localfile openstack-requirements - -# This is the main purpose of the script: Allow local installation of -# the current repo. It is listed in constraints file and thus any -# install will be constrained and we need to unconstrain it. -edit-constraints $localfile -- $CLIENT_NAME - -pip install -c$localfile -U $* -exit $? diff --git a/shade/tox.ini b/shade/tox.ini deleted file mode 100644 index 52a9274c1..000000000 --- a/shade/tox.ini +++ /dev/null @@ -1,80 +0,0 @@ -[tox] -minversion = 1.6 -envlist = py35,py27,pep8 -skipsdist = True - -[testenv] -usedevelop = True -basepython = {env:SHADE_TOX_PYTHON:python2} -passenv = UPPER_CONSTRAINTS_FILE -install_command = {toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} -setenv = - VIRTUAL_ENV={envdir} - LANG=en_US.UTF-8 - LANGUAGE=en_US:en - LC_ALL=C - BRANCH_NAME=master - CLIENT_NAME=shade -deps = -r{toxinidir}/test-requirements.txt -commands = stestr run {posargs} - stestr slowest - -[testenv:functional] -passenv = OS_* SHADE_* UPPER_CONSTRAINTS_FILE -commands = stestr --test-path ./shade/tests/functional run --serial {posargs} - stestr slowest - -[testenv:functional-tips] -passenv = OS_* SHADE_* UPPER_CONSTRAINTS_FILE -whitelist_externals = bash -commands = - bash -x {toxinidir}/extras/install-tips.sh - stestr --test-path ./shade/tests/functional run --serial {posargs} - stestr slowest - -[testenv:pep8] -commands = flake8 shade - -[testenv:venv] -commands = {posargs} - -[testenv:debug] -whitelist_externals = find -commands = - find . -type f -name "*.pyc" -delete - oslo_debug_helper {posargs} - -[testenv:cover] -setenv = - {[testenv]setenv} - PYTHON=coverage run --source shade --parallel-mode -commands = - stestr run {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - -[testenv:ansible] -# Need to pass some env vars for the Ansible playbooks -passenv = HOME USER -commands = {toxinidir}/extras/run-ansible-tests.sh -e {envdir} {posargs} - -[testenv:docs] -skip_install = True -deps = -r{toxinidir}/test-requirements.txt -commands = python setup.py build_sphinx - -[testenv:releasenotes] -commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html - -[flake8] -# The following are ignored on purpose - please do not submit patches to "fix" -# without first verifying with a core that fixing them is non-disruptive. -# H103 Is about the Apache license. It's strangely strict about the use of -# single vs double quotes in the license text. Fixing is not worth it -# H306 Is about alphabetical imports - there's a lot to fix -# H4 Are about docstrings - and there's just too many of them to fix -ignore = H103,H306,H4 -show-source = True -builtins = _ -exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build diff --git a/test-requirements.txt b/test-requirements.txt index 9c9ff8bdc..a4cd6f449 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,14 +5,18 @@ hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 beautifulsoup4>=4.6.0 # MIT coverage!=4.4,>=4.0 # Apache-2.0 +docutils>=0.11 # OSI-Approved Open Source, Public Domain +extras>=0.0.3 # MIT fixtures>=3.0.0 # Apache-2.0/BSD +jsonschema<3.0.0,>=2.6.0 # MIT mock>=2.0 # BSD python-subunit>=0.0.18 # Apache-2.0/BSD -openstackdocstheme>=1.16.0 # Apache-2.0 -os-testr>=0.8.0 # Apache-2.0 -requests>=2.14.2 # Apache-2.0 -requests-mock>=1.1 # Apache-2.0 +openstackdocstheme>=1.17.0 # Apache-2.0 +oslotest>=1.10.0 # Apache-2.0 +reno>=2.5.0 # Apache-2.0 +requests-mock>=1.1.0 # Apache-2.0 sphinx>=1.6.2 # BSD +stestr>=1.0.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=1.4.0 # MIT diff --git a/os-client-config/tools/keystone_version.py b/tools/keystone_version.py similarity index 100% rename from os-client-config/tools/keystone_version.py rename to tools/keystone_version.py diff --git a/os-client-config/tools/nova_version.py b/tools/nova_version.py similarity index 100% rename from os-client-config/tools/nova_version.py rename to tools/nova_version.py diff --git a/os-client-config/tools/tox_install.sh b/tools/tox_install.sh similarity index 100% rename from os-client-config/tools/tox_install.sh rename to tools/tox_install.sh diff --git a/tox.ini b/tox.ini index b6ab412d3..7661e9f33 100644 --- a/tox.ini +++ b/tox.ini @@ -5,29 +5,29 @@ skipsdist = True [testenv] usedevelop = True -install_command = pip install -U {opts} {packages} +basepython = {env:OPENSTACKSDK_TOX_PYTHON:python2} +passenv = UPPER_CONSTRAINTS_FILE +install_command = {toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} setenv = - VIRTUAL_ENV={envdir} + VIRTUAL_ENV={envdir} + LANG=en_US.UTF-8 + LANGUAGE=en_US:en + LC_ALL=C + BRANCH_NAME=master + CLIENT_NAME=openstacksdk deps = -r{toxinidir}/test-requirements.txt -commands = ostestr {posargs} +commands = stestr run posargs} + stestr slowest [testenv:examples] -setenv = OS_TEST_PATH=./openstack/tests/examples -passenv = OS_* - -[functionalbase] -setenv = OS_TEST_PATH=./openstack/tests/functional -passenv = OS_* +passenv = OS_* OPENSTACKSDK_* UPPER_CONSTRAINTS_FILE +commands = stestr --test-path ./openstack/tests/examples run {posargs} + stestr slowest [testenv:functional] -basepython = python2.7 -setenv = {[functionalbase]setenv} -passenv = {[functionalbase]passenv} - -[testenv:functional3] -basepython = python3.4 -setenv = {[functionalbase]setenv} -passenv = {[functionalbase]passenv} +passenv = OS_* OPENSTACKSDK_* UPPER_CONSTRAINTS_FILE +commands = stestr --test-path ./openstack/tests/functional run --serial {posargs} + stestr slowest [testenv:pep8] commands = flake8 @@ -35,14 +35,35 @@ commands = flake8 [testenv:venv] commands = {posargs} -; If this fails for you, you may be running an old version of tox. -; Run 'pip install tox' to install a newer version of tox. +[testenv:debug] +whitelist_externals = find +commands = + find . -type f -name "*.pyc" -delete + oslo_debug_helper {posargs} + [testenv:cover] -commands = python setup.py test --coverage --coverage-package-name=openstack --testr-args='{posargs}' +setenv = + {[testenv]setenv} + PYTHON=coverage run --source shade --parallel-mode +commands = + stestr run {posargs} + coverage combine + coverage html -d cover + coverage xml -o cover/coverage.xml + +[testenv:ansible] +# Need to pass some env vars for the Ansible playbooks +passenv = HOME USER +commands = {toxinidir}/extras/run-ansible-tests.sh -e {envdir} {posargs} [testenv:docs] +skip_install = True +deps = -r{toxinidir}/test-requirements.txt commands = python setup.py build_sphinx +[testenv:releasenotes] +commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html + [flake8] # The following are ignored on purpose. It's not super worth it to fix them. # However, if you feel strongly about it, patches will be accepted to fix them From 1fcc3e7aee4529ea7c2fc834add553592234801c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 28 Sep 2017 08:29:12 -0500 Subject: [PATCH 1830/3836] Temporarily disable volume and os_image functional tests There is a config issue with the shade devstack cinder tests. We'll fix it post v3 rollout, but for now there are some other patches that need to progress. There is also a bug in upstream ansible that snuck in (yay for assymetric gating) A fix is in but hasn't been released yet. Change-Id: Ifacccecfeb112497b78feff908c0e681d6c012cf --- shade/tests/ansible/run.yml | 4 +++- shade/tests/functional/test_compute.py | 6 ++++++ shade/tests/functional/test_volume.py | 1 + shade/tests/functional/test_volume_backup.py | 1 + 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/shade/tests/ansible/run.yml b/shade/tests/ansible/run.yml index 27ad8af0f..9340ccd06 100644 --- a/shade/tests/ansible/run.yml +++ b/shade/tests/ansible/run.yml @@ -7,7 +7,9 @@ - { role: auth, tags: auth } - { role: client_config, tags: client_config } - { role: group, tags: group } - - { role: image, tags: image } + # TODO(mordred) Reenable this once the fixed os_image winds up in an + # upstream ansible release. + # - { role: image, tags: image } - { role: keypair, tags: keypair } - { role: keystone_domain, tags: keystone_domain } - { role: keystone_role, tags: keystone_role } diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index da6c315cb..c5b0eb6ca 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -99,6 +99,7 @@ def test_create_and_delete_server_auto_ip_delete_ips(self): self.assertIsNone(self.user_cloud.get_server(self.server_name)) def test_attach_detach_volume(self): + self.skipTest('Volume functional tests temporarily disabled') server_name = self.getUniqueString() self.addCleanup(self._cleanup_servers_and_volumes, server_name) server = self.user_cloud.create_server( @@ -263,6 +264,7 @@ def _assert_volume_attach(self, server, volume_id=None, image=''): return volume_id def test_create_boot_from_volume_image(self): + self.skipTest('Volume functional tests temporarily disabled') if not self.user_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) @@ -300,6 +302,7 @@ def _wait_for_detach(self, volume_id): return def test_create_terminate_volume_image(self): + self.skipTest('Volume functional tests temporarily disabled') if not self.user_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) @@ -322,6 +325,7 @@ def test_create_terminate_volume_image(self): self.assertIsNone(self.user_cloud.get_server(self.server_name)) def test_create_boot_from_volume_preexisting(self): + self.skipTest('Volume functional tests temporarily disabled') if not self.user_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) @@ -349,6 +353,7 @@ def test_create_boot_from_volume_preexisting(self): self.assertIsNone(self.user_cloud.get_volume(volume_id)) def test_create_boot_attach_volume(self): + self.skipTest('Volume functional tests temporarily disabled') if not self.user_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) @@ -376,6 +381,7 @@ def test_create_boot_attach_volume(self): self.assertIsNone(self.user_cloud.get_volume(volume_id)) def test_create_boot_from_volume_preexisting_terminate(self): + self.skipTest('Volume functional tests temporarily disabled') if not self.user_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) diff --git a/shade/tests/functional/test_volume.py b/shade/tests/functional/test_volume.py index fe4dd6c6f..5425b7146 100644 --- a/shade/tests/functional/test_volume.py +++ b/shade/tests/functional/test_volume.py @@ -32,6 +32,7 @@ class TestVolume(base.BaseFunctionalTestCase): def setUp(self): super(TestVolume, self).setUp() + self.skipTest('Volume functional tests temporarily disabled') if not self.user_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') diff --git a/shade/tests/functional/test_volume_backup.py b/shade/tests/functional/test_volume_backup.py index 3a7d4bff9..8d2f66fc8 100644 --- a/shade/tests/functional/test_volume_backup.py +++ b/shade/tests/functional/test_volume_backup.py @@ -18,6 +18,7 @@ class TestVolume(base.BaseFunctionalTestCase): def setUp(self): super(TestVolume, self).setUp() + self.skipTest('Volume functional tests temporarily disabled') if not self.user_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') From e7ade19fbb3e39a0bcb43fac8ed445972de94e03 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 12 Oct 2017 11:52:06 -0500 Subject: [PATCH 1831/3836] Fix image task uploads These are accidentally working in production in a very inefficient manner for nodepool. Change-Id: I6cf4502d96ac0ac98a8723ed436b0859dfc60325 --- shade/openstackcloud.py | 13 ++++++------- shade/tests/unit/test_image.py | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 781a06efd..ab4bcd28b 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4704,9 +4704,8 @@ def _upload_image_task( "Timeout waiting for the image to import."): try: if image_id is None: - data = self._image_client.get( + status = self._image_client.get( '/tasks/{id}'.format(id=glance_task.id)) - status = self._get_and_munchify('images', data=data) except OpenStackCloudHTTPError as e: if e.response.status_code == 503: # Clear the exception so that it doesn't linger @@ -4716,8 +4715,8 @@ def _upload_image_task( continue raise - if status.status == 'success': - image_id = status.result['image_id'] + if status['status'] == 'success': + image_id = status['result']['image_id'] try: image = self.get_image(image_id) except OpenStackCloudHTTPError as e: @@ -4736,15 +4735,15 @@ def _upload_image_task( "Image Task %s imported %s in %s", glance_task.id, image_id, (time.time() - start)) return self.get_image(image_id) - if status.status == 'failure': - if status.message == IMAGE_ERROR_396: + elif status['status'] == 'failure': + if status['message'] == IMAGE_ERROR_396: glance_task = self._image_client.post( '/tasks', data=task_args) self.list_images.invalidate(self) else: raise OpenStackCloudException( "Image creation failed: {message}".format( - message=status.message), + message=status['message']), extra_data=status) else: return glance_task diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index 515ca22e5..2ee6a85b9 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -339,7 +339,7 @@ def test_create_image_task(self): dict(method='GET', uri='https://image.example.com/v2/tasks/{id}'.format( id=task_id), - json={'images': args}), + json=args), dict(method='GET', uri='https://image.example.com/v2/images', json={'images': [image_no_checksums]}), dict(method='PATCH', From fd9c2b57299d92c6092a966a79cdf480f523c92b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 11 Oct 2017 12:09:56 -0500 Subject: [PATCH 1832/3836] Add group parameter to create_server Server groups are a user-facing feature and can be requested via scheduler hints. While that already exists it's not the world's cleanest user interface, so add a specific parameter which will set the right thing into the scheduler hints dict. Change-Id: Idb28779ed1fde341acab2116b510fce349f74b50 --- ...boot-on-server-group-a80e51850db24b3d.yaml | 4 ++++ shade/openstackcloud.py | 20 ++++++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/boot-on-server-group-a80e51850db24b3d.yaml diff --git a/releasenotes/notes/boot-on-server-group-a80e51850db24b3d.yaml b/releasenotes/notes/boot-on-server-group-a80e51850db24b3d.yaml new file mode 100644 index 000000000..4f4a39c23 --- /dev/null +++ b/releasenotes/notes/boot-on-server-group-a80e51850db24b3d.yaml @@ -0,0 +1,4 @@ +--- +features: + - Added ``group`` parameter to create_server to allow + booting a server into a specific server group. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index ab4bcd28b..806713c28 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -6390,6 +6390,7 @@ def create_server( wait=False, timeout=180, reuse_ips=True, network=None, boot_from_volume=False, volume_size='50', boot_volume=None, volumes=None, nat_destination=None, + group=None, **kwargs): """Create a virtual server instance. @@ -6468,6 +6469,10 @@ def create_server( be attached to, if it's not possible to infer from the cloud's configuration. (Optional, defaults to None) + :param group: ServerGroup dict, name or id to boot the server in. + If a group is provided in both scheduler_hints and in + the group param, the group param will win. + (Optional, defaults to None) :returns: A ``munch.Munch`` representing the created server. :raises: OpenStackCloudException on operation error. """ @@ -6477,15 +6482,14 @@ def create_server( security_groups = [security_groups] if security_groups: kwargs['security_groups'] = [] - for group in security_groups: - kwargs['security_groups'].append(dict(name=group)) + for sec_group in security_groups: + kwargs['security_groups'].append(dict(name=sec_group)) if 'userdata' in kwargs: user_data = kwargs.pop('userdata') if user_data: kwargs['user_data'] = self._encode_server_userdata(user_data) for (desired, given) in ( ('OS-DCF:diskConfig', 'disk_config'), - ('os:scheduler_hints', 'scheduler_hints'), ('config_drive', 'config_drive'), ('key_name', 'key_name'), ('metadata', 'meta'), @@ -6494,6 +6498,16 @@ def create_server( if value: kwargs[desired] = value + hints = kwargs.pop('scheduler_hints', {}) + if group: + group_obj = self.get_server_group(group) + if not group_obj: + raise OpenStackCloudException( + "Server Group {group} was requested but was not found" + " on the cloud".format(group=group)) + hints['group'] = group_obj['id'] + if hints: + kwargs['os:scheduler_hints'] = hints kwargs.setdefault('max_count', kwargs.get('max_count', 1)) kwargs.setdefault('min_count', kwargs.get('min_count', 1)) From b653090489a0517ec2b06e13cd7849e29a4ad374 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 11 Oct 2017 08:58:40 -0500 Subject: [PATCH 1833/3836] Image should be optional If someone is booting from volume it is not necessary for them to specify an image. Unfortunately we chose name, image, flavor as the order for positional arguments, even though name and flavor are the only two that are actually required. Make the image and flavor options both optional - but then add a check for flavor, since it is required. Add in a check to ensure image is given if boot_volume is not given. Change-Id: I0362838dbcf35745ccf8369d41e80df95d9611f5 --- shade/openstackcloud.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 806713c28..ca167dd41 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -6384,7 +6384,7 @@ def _encode_server_userdata(self, userdata): 'block_device_mapping_v2', 'nics', 'scheduler_hints', 'config_drive', 'admin_pass', 'disk_config') def create_server( - self, name, image, flavor, + self, name, image=None, flavor=None, auto_ip=True, ips=None, ip_pool=None, root_volume=None, terminate_volume=False, wait=False, timeout=180, reuse_ips=True, @@ -6395,7 +6395,8 @@ def create_server( """Create a virtual server instance. :param name: Something to name the server. - :param image: Image dict, name or ID to boot with. + :param image: Image dict, name or ID to boot with. image is required + unless boot_volume is given. :param flavor: Flavor dict, name or ID to boot onto. :param auto_ip: Whether to take actions to find a routable IP for the server. (defaults to True) @@ -6476,6 +6477,14 @@ def create_server( :returns: A ``munch.Munch`` representing the created server. :raises: OpenStackCloudException on operation error. """ + # TODO(shade) Image is optional but flavor is not - yet flavor comes + # after image in the argument list. Doh. + if not flavor: + raise TypeError( + "create_server() missing 1 required argument: 'flavor'") + if not image and not boot_volume: + raise TypeError( + "create_server() requires either 'image' or 'boot_volume'") # TODO(mordred) Add support for description starting in 2.19 security_groups = kwargs.get('security_groups', []) if security_groups and not isinstance(kwargs['security_groups'], list): @@ -6578,7 +6587,7 @@ def create_server( kwargs['imageRef'] = image['id'] else: kwargs['imageRef'] = self.get_image(image).id - if flavor and isinstance(flavor, dict): + if isinstance(flavor, dict): kwargs['flavorRef'] = flavor['id'] else: kwargs['flavorRef'] = self.get_flavor(flavor, get_extra=False).id From 8cda430e8b81ef991f22b84c49e24eda0bc2f4bd Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 11 Sep 2017 08:26:34 -0600 Subject: [PATCH 1834/3836] Add method to set bootable flag on volumes If a person wants to create a bootable volume not from an image, they need to set a flag, which is done with this action call. Change-Id: I765eb97501a5ba9e54325c8c56573bb7311deb72 --- .../set-bootable-volume-454a7a41e7e77d08.yaml | 4 + shade/openstackcloud.py | 36 +++++++- shade/tests/unit/test_volume.py | 89 ++++++++++++++++++- 3 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/set-bootable-volume-454a7a41e7e77d08.yaml diff --git a/releasenotes/notes/set-bootable-volume-454a7a41e7e77d08.yaml b/releasenotes/notes/set-bootable-volume-454a7a41e7e77d08.yaml new file mode 100644 index 000000000..c7d84fe03 --- /dev/null +++ b/releasenotes/notes/set-bootable-volume-454a7a41e7e77d08.yaml @@ -0,0 +1,4 @@ +--- +features: + - Added a ``set_volume_bootable`` call to allow toggling the bootable state + of a volume. diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index ca167dd41..39012bc72 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -4806,7 +4806,7 @@ def _update_image_properties_v1(self, image, meta, properties): def create_volume( self, size, - wait=True, timeout=None, image=None, **kwargs): + wait=True, timeout=None, image=None, bootable=None, **kwargs): """Create a volume. :param size: Size, in GB of the volume to create. @@ -4816,6 +4816,8 @@ def create_volume( :param timeout: Seconds to wait for volume creation. None is forever. :param image: (optional) Image name, ID or object from which to create the volume + :param bootable: (optional) Make this volume bootable. If set, wait + will also be set to true. :param kwargs: Keyword arguments as expected for cinder client. :returns: The created volume object. @@ -4823,6 +4825,9 @@ def create_volume( :raises: OpenStackCloudTimeout if wait time exceeded. :raises: OpenStackCloudException on operation error. """ + if bootable is not None: + wait = True + if image: image_obj = self.get_image(image) if not image_obj: @@ -4858,6 +4863,10 @@ def create_volume( continue if volume['status'] == 'available': + if bootable is not None: + self.set_volume_bootable(volume, bootable=bootable) + # no need to re-fetch to update the flag, just set it. + volume['bootable'] = bootable return volume if volume['status'] == 'error': @@ -4865,6 +4874,31 @@ def create_volume( return self._normalize_volume(volume) + def set_volume_bootable(self, name_or_id, bootable=True): + """Set a volume's bootable flag. + + :param name_or_id: Name, unique ID of the volume or a volume dict. + :param bool bootable: Whether the volume should be bootable. + (Defaults to True) + + :raises: OpenStackCloudTimeout if wait time exceeded. + :raises: OpenStackCloudException on operation error. + """ + + volume = self.get_volume(name_or_id) + + if not volume: + raise OpenStackCloudException( + "Volume {name_or_id} does not exist".format( + name_or_id=name_or_id)) + + self._volume_client.post( + 'volumes/{id}/action'.format(id=volume['id']), + json={'os-set_bootable': {'bootable': bootable}}, + error_message="Error setting bootable on volume {volume}".format( + volume=volume['id']) + ) + def delete_volume(self, name_or_id=None, wait=True, timeout=None, force=False): """Delete a volume. diff --git a/shade/tests/unit/test_volume.py b/shade/tests/unit/test_volume.py index 5983c6af8..40d838881 100644 --- a/shade/tests/unit/test_volume.py +++ b/shade/tests/unit/test_volume.py @@ -292,7 +292,8 @@ def test_delete_volume_force(self): uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', volume.id, 'action']), - json={'os-force_delete': None}), + validate=dict( + json={'os-force_delete': None})), dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', 'detail']), @@ -300,6 +301,42 @@ def test_delete_volume_force(self): self.assertTrue(self.cloud.delete_volume(volume['id'], force=True)) self.assert_calls() + def test_set_volume_bootable(self): + vol = {'id': 'volume001', 'status': 'attached', + 'name': '', 'attachments': []} + volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', 'detail']), + json={'volumes': [volume]}), + dict(method='POST', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['volumes', volume.id, 'action']), + json={'os-set_bootable': {'bootable': True}}), + ]) + self.cloud.set_volume_bootable(volume['id']) + self.assert_calls() + + def test_set_volume_bootable_false(self): + vol = {'id': 'volume001', 'status': 'attached', + 'name': '', 'attachments': []} + volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', 'detail']), + json={'volumes': [volume]}), + dict(method='POST', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['volumes', volume.id, 'action']), + json={'os-set_bootable': {'bootable': False}}), + ]) + self.cloud.set_volume_bootable(volume['id']) + self.assert_calls() + def test_list_volumes_with_pagination(self): vol1 = meta.obj_to_munch(fakes.FakeVolume('01', 'available', 'vol1')) vol2 = meta.obj_to_munch(fakes.FakeVolume('02', 'available', 'vol2')) @@ -448,3 +485,53 @@ def test_get_volume_by_id(self): self.cloud._normalize_volume(vol1), self.cloud.get_volume_by_id('01')) self.assert_calls() + + def test_create_volume(self): + vol1 = meta.obj_to_munch(fakes.FakeVolume('01', 'available', 'vol1')) + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes']), + json={'volume': vol1}, + validate=dict(json={ + 'volume': { + 'size': 50, + 'name': 'vol1', + }})), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['volumes', 'detail']), + json={'volumes': [vol1]}), + ]) + + self.cloud.create_volume(50, name='vol1') + self.assert_calls() + + def test_create_bootable_volume(self): + vol1 = meta.obj_to_munch(fakes.FakeVolume('01', 'available', 'vol1')) + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes']), + json={'volume': vol1}, + validate=dict(json={ + 'volume': { + 'size': 50, + 'name': 'vol1', + }})), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['volumes', 'detail']), + json={'volumes': [vol1]}), + dict(method='POST', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['volumes', '01', 'action']), + validate=dict( + json={'os-set_bootable': {'bootable': True}})), + ]) + + self.cloud.create_volume(50, name='vol1', bootable=True) + self.assert_calls() From 835d6555c3a7e2f870a55a57e165a588e61b1c6c Mon Sep 17 00:00:00 2001 From: Sam Yaple Date: Wed, 23 Aug 2017 22:51:15 -0400 Subject: [PATCH 1835/3836] Allow domain_id for roles Roles can be domain specific but there is no current way for shade to allow us to set that value. Additionally the role name can be updated so this add update_role() Change-Id: I3279f17cb8871e91fcc3aa3bd18ae8457a0016bb --- shade/_utils.py | 1 + shade/operatorcloud.py | 61 +++++++++++++++++++++---- shade/tests/unit/test_identity_roles.py | 24 ++++++++++ 3 files changed, 76 insertions(+), 10 deletions(-) diff --git a/shade/_utils.py b/shade/_utils.py index 37939e5b2..4838fc7e7 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -378,6 +378,7 @@ def normalize_roles(roles): """Normalize Identity roles.""" ret = [ dict( + domain_id=role.get('domain_id'), id=role.get('id'), name=role.get('name'), ) for role in roles diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index da6d07411..248bcd91f 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1375,9 +1375,12 @@ def delete_group(self, name_or_id, **kwargs): self.list_groups.invalidate(self) return True - def list_roles(self): + @_utils.valid_kwargs('domain_id') + def list_roles(self, **kwargs): """List Keystone roles. + :param domain_id: domain id for listing roles (v3) + :returns: a list of ``munch.Munch`` containing the role description. :raises: ``OpenStackCloudException``: if something goes wrong during @@ -1386,14 +1389,16 @@ def list_roles(self): v2 = self._is_client_version('identity', 2) url = '/OS-KSADM/roles' if v2 else '/roles' data = self._identity_client.get( - url, error_message="Failed to list roles") + url, params=kwargs, error_message="Failed to list roles") return _utils.normalize_roles(self._get_and_munchify('roles', data)) - def search_roles(self, name_or_id=None, filters=None): + @_utils.valid_kwargs('domain_id') + def search_roles(self, name_or_id=None, filters=None, **kwargs): """Seach Keystone roles. :param string name: role name or id. :param dict filters: a dict containing additional filters to use. + :param domain_id: domain id (v3) :returns: a list of ``munch.Munch`` containing the role description. Each ``munch.Munch`` contains the following attributes:: @@ -1405,14 +1410,16 @@ def search_roles(self, name_or_id=None, filters=None): :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ - roles = self.list_roles() + roles = self.list_roles(**kwargs) return _utils._filter_list(roles, name_or_id, filters) - def get_role(self, name_or_id, filters=None): + @_utils.valid_kwargs('domain_id') + def get_role(self, name_or_id, filters=None, **kwargs): """Get exactly one Keystone role. :param id: role name or id. :param filters: a dict containing additional filters to use. + :param domain_id: domain id (v3) :returns: a single ``munch.Munch`` containing the role description. Each ``munch.Munch`` contains the following attributes:: @@ -1424,7 +1431,7 @@ def get_role(self, name_or_id, filters=None): :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ - return _utils._get_entity(self, 'role', name_or_id, filters) + return _utils._get_entity(self, 'role', name_or_id, filters, **kwargs) def _keystone_v2_role_assignments(self, user, project=None, role=None, **kwargs): @@ -1686,10 +1693,12 @@ def list_flavor_access(self, flavor_id): return _utils.normalize_flavor_accesses( self._get_and_munchify('flavor_access', data)) - def create_role(self, name): + @_utils.valid_kwargs('domain_id') + def create_role(self, name, **kwargs): """Create a Keystone role. :param string name: The name of the role. + :param domain_id: domain id (v3) :returns: a ``munch.Munch`` containing the role description @@ -1697,23 +1706,55 @@ def create_role(self, name): """ v2 = self._is_client_version('identity', 2) url = '/OS-KSADM/roles' if v2 else '/roles' + kwargs['name'] = name msg = 'Failed to create role {name}'.format(name=name) data = self._identity_client.post( - url, json={'role': {'name': name}}, error_message=msg) + url, json={'role': kwargs}, error_message=msg) role = self._get_and_munchify('role', data) return _utils.normalize_roles([role])[0] - def delete_role(self, name_or_id): + @_utils.valid_kwargs('domain_id') + def update_role(self, name_or_id, name, **kwargs): + """Update a Keystone role. + + :param name_or_id: Name or id of the role to update + :param string name: The new role name + :param domain_id: domain id + + :returns: a ``munch.Munch`` containing the role description + + :raise OpenStackCloudException: if the role cannot be created + """ + if self._is_client_version('identity', 2): + raise OpenStackCloudUnavailableFeature( + 'Unavailable Feature: Role update requires Identity v3' + ) + kwargs['name_or_id'] = name_or_id + role = self.get_role(**kwargs) + if role is None: + self.log.debug( + "Role %s not found for updating", name_or_id) + return False + msg = 'Failed to update role {name}'.format(name=name_or_id) + json_kwargs = {'role_id': role.id, 'role': {'name': name}} + data = self._identity_client.patch('/roles', error_message=msg, + json=json_kwargs) + role = self._get_and_munchify('role', data) + return _utils.normalize_roles([role])[0] + + @_utils.valid_kwargs('domain_id') + def delete_role(self, name_or_id, **kwargs): """Delete a Keystone role. :param string id: Name or id of the role to delete. + :param domain_id: domain id (v3) :returns: True if delete succeeded, False otherwise. :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call. """ - role = self.get_role(name_or_id) + role = self.get_role(name_or_id, **kwargs) if role is None: self.log.debug( "Role %s not found for deleting", name_or_id) diff --git a/shade/tests/unit/test_identity_roles.py b/shade/tests/unit/test_identity_roles.py index 3ef75ce53..890449ae1 100644 --- a/shade/tests/unit/test_identity_roles.py +++ b/shade/tests/unit/test_identity_roles.py @@ -101,6 +101,30 @@ def test_create_role(self): self.assertThat(role.id, matchers.Equals(role_data.role_id)) self.assert_calls() + def test_update_role(self): + role_data = self._get_role_data() + req = {'role_id': role_data.role_id, + 'role': {'name': role_data.role_name}} + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'roles': [role_data.json_response['role']]}), + dict(method='PATCH', + uri=self.get_mock_url(), + status_code=200, + json=role_data.json_response, + validate=dict(json=req)) + ]) + + role = self.op_cloud.update_role(role_data.role_id, + role_data.role_name) + + self.assertIsNotNone(role) + self.assertThat(role.name, matchers.Equals(role_data.role_name)) + self.assertThat(role.id, matchers.Equals(role_data.role_id)) + self.assert_calls() + def test_delete_role_by_id(self): role_data = self._get_role_data() self.register_uris([ From c39d98ccafa690cc88d0ed3da1eb37aefc52a3b0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 1 Sep 2017 11:09:37 -0500 Subject: [PATCH 1836/3836] Move role normalization to normalize.py Location has specific semantics for identity resources. Add a method to get a projectless location. Add domain_id to project since all of the identity resources have it already, but keep the parent-project semantics already in place for project. Change-Id: Ife37833baabf58d9e329071acb4187842815c7d2 --- doc/source/user/model.rst | 84 ++++++++++++++++++++++++++------------- shade/_normalize.py | 27 +++++++++---- shade/_utils.py | 12 ------ shade/openstackcloud.py | 12 ++++++ shade/operatorcloud.py | 6 +-- 5 files changed, 91 insertions(+), 50 deletions(-) diff --git a/doc/source/user/model.rst b/doc/source/user/model.rst index 3293b0306..ebfc3af97 100644 --- a/doc/source/user/model.rst +++ b/doc/source/user/model.rst @@ -65,6 +65,9 @@ If all of the project information is None, then domain_name=str() or None)) +Resources +========= + Flavor ------ @@ -324,34 +327,6 @@ A Floating IP from Neutron or Nova revision_number=int() or None, properties=dict()) -Project -------- - -A Project from Keystone (or a tenant if Keystone v2) - -Location information for Project has some specific semantics. - -If the project has a parent project, that will be in location.project.id, -and if it doesn't that should be None. If the Project is associated with -a domain that will be in location.project.domain_id regardless of the current -user's token scope. location.project.name and location.project.domain_name -will always be None. Finally, location.region_name will always be None as -Projects are global to a cloud. If a deployer happens to deploy OpenStack -in such a way that users and projects are not shared amongst regions, that -necessitates treating each of those regions as separate clouds from shade's -POV. - -.. code-block:: python - - Project = dict( - location=Location(), - id=str(), - name=str(), - description=str(), - is_enabled=bool(), - is_domain=bool(), - properties=dict()) - Volume ------ @@ -502,3 +477,56 @@ A Stack from Heat tempate_description=str(), timeout_mins=int(), properties=dict()) + +Identity Resources +================== + +Identity Resources are slightly different. + +They are global to a cloud, so location.availability_zone and +location.region_name and will always be None. If a deployer happens to deploy +OpenStack in such a way that users and projects are not shared amongst regions, +that necessitates treating each of those regions as separate clouds from +shade's POV. + +The Identity Resources that are not Project do not exist within a Project, +so all of the values in ``location.project`` will be None. + +Project +------- + +A Project from Keystone (or a tenant if Keystone v2) + +Location information for Project has some additional specific semantics. +If the project has a parent project, that will be in ``location.project.id``, +and if it doesn't that should be ``None``. + +If the Project is associated with a domain that will be in +``location.project.domain_id`` in addition to the normal ``domain_id`` +regardless of the current user's token scope. + +.. code-block:: python + + Project = dict( + location=Location(), + id=str(), + name=str(), + description=str(), + is_enabled=bool(), + is_domain=bool(), + domain_id=str(), + properties=dict()) + +Role +---- + +A Role from Keystone + +.. code-block:: python + + Project = dict( + location=Location(), + id=str(), + name=str(), + domain_id=str(), + properties=dict()) diff --git a/shade/_normalize.py b/shade/_normalize.py index b8e242b56..83783c776 100644 --- a/shade/_normalize.py +++ b/shade/_normalize.py @@ -643,19 +643,14 @@ def _normalize_project(self, project): description = project.pop('description', '') is_enabled = project.pop('enabled', True) - # Projects are global - strip region - location = self._get_current_location(project_id=project_id) - location['region_name'] = None - # v3 additions domain_id = project.pop('domain_id', 'default') parent_id = project.pop('parent_id', None) is_domain = project.pop('is_domain', False) # Projects have a special relationship with location + location = self._get_identity_location() location['project']['domain_id'] = domain_id - location['project']['domain_name'] = None - location['project']['name'] = None location['project']['id'] = parent_id ret = munch.Munch( @@ -665,13 +660,13 @@ def _normalize_project(self, project): description=description, is_enabled=is_enabled, is_domain=is_domain, + domain_id=domain_id, properties=project.copy() ) # Backwards compat if not self.strict_mode: ret['enabled'] = is_enabled - ret['domain_id'] = domain_id ret['parent_id'] = parent_id for key, val in ret['properties'].items(): ret.setdefault(key, val) @@ -1089,3 +1084,21 @@ def _normalize_machine(self, machine): # TODO(mordred) Normalize this resource return machine + + def _normalize_roles(self, roles): + """Normalize Keystone roles""" + ret = [] + for role in roles: + ret.append(self._normalize_role(role)) + return ret + + def _normalize_role(self, role): + """Normalize Identity roles.""" + + return munch.Munch( + id=role.get('id'), + name=role.get('name'), + domain_id=role.get('domain_id'), + location=self._get_identity_location(), + properties={}, + ) diff --git a/shade/_utils.py b/shade/_utils.py index 4838fc7e7..750f07ab8 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -374,18 +374,6 @@ def normalize_role_assignments(assignments): return new_assignments -def normalize_roles(roles): - """Normalize Identity roles.""" - ret = [ - dict( - domain_id=role.get('domain_id'), - id=role.get('id'), - name=role.get('name'), - ) for role in roles - ] - return meta.obj_list_to_munch(ret) - - def normalize_flavor_accesses(flavor_accesses): """Normalize Flavor access list.""" return [munch.Munch( diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 39012bc72..531a66ba3 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -670,6 +670,18 @@ def _get_current_location(self, project_id=None, zone=None): project=self._get_project_info(project_id), ) + def _get_identity_location(self): + '''Identity resources do not exist inside of projects.''' + return munch.Munch( + cloud=self.name, + region_name=None, + zone=None, + project=munch.Munch( + id=None, + name=None, + domain_id=None, + domain_name=None)) + def _get_project_id_param_dict(self, name_or_id): if name_or_id: project = self.get_project(name_or_id) diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 248bcd91f..3270a8f98 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1390,7 +1390,7 @@ def list_roles(self, **kwargs): url = '/OS-KSADM/roles' if v2 else '/roles' data = self._identity_client.get( url, params=kwargs, error_message="Failed to list roles") - return _utils.normalize_roles(self._get_and_munchify('roles', data)) + return self._normalize_roles(self._get_and_munchify('roles', data)) @_utils.valid_kwargs('domain_id') def search_roles(self, name_or_id=None, filters=None, **kwargs): @@ -1711,7 +1711,7 @@ def create_role(self, name, **kwargs): data = self._identity_client.post( url, json={'role': kwargs}, error_message=msg) role = self._get_and_munchify('role', data) - return _utils.normalize_roles([role])[0] + return self._normalize_role(role) @_utils.valid_kwargs('domain_id') def update_role(self, name_or_id, name, **kwargs): @@ -1740,7 +1740,7 @@ def update_role(self, name_or_id, name, **kwargs): data = self._identity_client.patch('/roles', error_message=msg, json=json_kwargs) role = self._get_and_munchify('role', data) - return _utils.normalize_roles([role])[0] + return self._normalize_role(role) @_utils.valid_kwargs('domain_id') def delete_role(self, name_or_id, **kwargs): From d25b80eaf3d75d71bf085632354d4156e20edf27 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 7 Nov 2017 10:31:45 +1100 Subject: [PATCH 1837/3836] Add jobs for Zuul v3 These are copies of the shade jobs, but they should work with this stack. The changes that are not the tests are changes needed to make the tests work. Change-Id: I9f223c4a9ac8dc2570b8698284512e1aa834970a --- .zuul.yaml | 237 ++++++++++++++++++ openstack/tests/base.py | 3 + .../tests/functional/cloud/test_aggregate.py | 2 +- .../cloud/test_cluster_templates.py | 2 +- .../tests/functional/cloud/test_compute.py | 24 +- .../tests/functional/cloud/test_devstack.py | 2 +- .../tests/functional/cloud/test_domain.py | 2 +- .../tests/functional/cloud/test_endpoints.py | 2 +- .../tests/functional/cloud/test_flavor.py | 2 +- .../functional/cloud/test_floating_ip.py | 4 +- .../functional/cloud/test_floating_ip_pool.py | 2 +- .../tests/functional/cloud/test_groups.py | 2 +- .../tests/functional/cloud/test_identity.py | 2 +- .../tests/functional/cloud/test_image.py | 2 +- .../tests/functional/cloud/test_inventory.py | 4 +- .../tests/functional/cloud/test_keypairs.py | 2 +- .../tests/functional/cloud/test_limits.py | 2 +- .../functional/cloud/test_magnum_services.py | 2 +- .../tests/functional/cloud/test_network.py | 2 +- .../tests/functional/cloud/test_object.py | 2 +- openstack/tests/functional/cloud/test_port.py | 2 +- .../tests/functional/cloud/test_project.py | 2 +- .../cloud/test_qos_bandwidth_limit_rule.py | 2 +- .../cloud/test_qos_dscp_marking_rule.py | 2 +- .../cloud/test_qos_minimum_bandwidth_rule.py | 2 +- .../tests/functional/cloud/test_qos_policy.py | 2 +- .../tests/functional/cloud/test_quotas.py | 2 +- .../functional/cloud/test_range_search.py | 2 +- .../tests/functional/cloud/test_recordset.py | 2 +- .../tests/functional/cloud/test_router.py | 2 +- .../functional/cloud/test_security_groups.py | 2 +- .../functional/cloud/test_server_group.py | 2 +- .../tests/functional/cloud/test_services.py | 2 +- .../tests/functional/cloud/test_stack.py | 2 +- .../tests/functional/cloud/test_usage.py | 37 --- .../tests/functional/cloud/test_users.py | 4 +- .../tests/functional/cloud/test_volume.py | 2 +- .../functional/cloud/test_volume_backup.py | 2 +- .../functional/cloud/test_volume_type.py | 2 +- openstack/tests/functional/cloud/test_zone.py | 2 +- .../functional/object_store/v1/test_obj.py | 5 +- playbooks/devstack/legacy-git.yaml | 11 + playbooks/devstack/post.yaml | 4 + playbooks/devstack/pre.yaml | 10 + playbooks/devstack/run.yaml | 3 + test-requirements.txt | 2 + 46 files changed, 334 insertions(+), 80 deletions(-) create mode 100644 .zuul.yaml delete mode 100644 openstack/tests/functional/cloud/test_usage.py create mode 100644 playbooks/devstack/legacy-git.yaml create mode 100644 playbooks/devstack/post.yaml create mode 100644 playbooks/devstack/pre.yaml create mode 100644 playbooks/devstack/run.yaml diff --git a/.zuul.yaml b/.zuul.yaml new file mode 100644 index 000000000..88bf27266 --- /dev/null +++ b/.zuul.yaml @@ -0,0 +1,237 @@ +# TODO(shade) Add job that enables ceilometer + +- job: + name: openstacksdk-tox-py27-tips + parent: openstack-tox-py27 + description: | + Run tox python 27 unittests against master of important libs + vars: + tox_install_siblings: true + # openstacksdk in required-projects so that os-client-config + # and keystoneauth can add the job as well + required-projects: + - openstack-infra/shade + - openstack/keystoneauth + - openstack/os-client-config + - openstack/python-openstacksdk + +- job: + name: openstacksdk-tox-py35-tips + parent: openstack-tox-py35 + description: | + Run tox python 35 unittests against master of important libs + vars: + tox_install_siblings: true + # openstacksdk in required-projects so that osc and keystoneauth + # can add the job as well + required-projects: + - openstack-infra/shade + - openstack/keystoneauth + - openstack/os-client-config + - openstack/python-openstacksdk + +- project-template: + name: openstacksdk-tox-tips + check: + jobs: + - openstacksdk-tox-py27-tips + - openstacksdk-tox-py35-tips + gate: + jobs: + - openstacksdk-tox-py27-tips + - openstacksdk-tox-py35-tips + +- job: + name: openstacksdk-functional-devstack-base + parent: devstack + description: | + Base job for devstack-based functional tests + pre-run: playbooks/devstack/pre.yaml + run: playbooks/devstack/run.yaml + post-run: playbooks/devstack/post.yaml + required-projects: + # These jobs will DTRT when openstacksdk triggers them, but we want to + # make sure stable branches of openstacksdk never get cloned by other + # people, since stable branches of openstacksdk are, well, not actually + # things. + - name: openstack-infra/shade + override-branch: master + - name: openstack/python-openstacksdk + override-branch: master + - name: openstack/os-client-config + override-branch: master + - name: openstack/heat + - name: openstack/swift + roles: + - zuul: openstack-infra/devstack + timeout: 9000 + vars: + devstack_localrc: + SWIFT_HASH: '1234123412341234' + devstack_local_conf: + post-config: + $CINDER_CONF: + DEFAULT: + osapi_max_limit: 6 + devstack_services: + ceilometer-acentral: false + ceilometer-acompute: false + ceilometer-alarm-evaluator: false + ceilometer-alarm-notifier: false + ceilometer-anotification: false + ceilometer-api: false + ceilometer-collector: false + horizon: false + s-account: true + s-container: true + s-object: true + s-proxy: true + devstack_plugins: + heat: https://git.openstack.org/openstack/heat + tox_environment: + # Do we really need to set this? It's cargo culted + PYTHONUNBUFFERED: 'true' + # Is there a way we can query the localconf variable to get these + # rather than setting them explicitly? + OPENSTACKSDK_HAS_DESIGNATE: 0 + OPENSTACKSDK_HAS_HEAT: 1 + OPENSTACKSDK_HAS_MAGNUM: 0 + OPENSTACKSDK_HAS_NEUTRON: 1 + OPENSTACKSDK_HAS_SWIFT: 1 + tox_install_siblings: false + tox_envlist: functional + zuul_work_dir: src/git.openstack.org/openstack/python-openstacksdk + +- job: + name: openstacksdk-functional-devstack-legacy + parent: openstacksdk-functional-devstack-base + description: | + Run openstacksdk functional tests against a legacy devstack + voting: false + vars: + devstack_localrc: + ENABLE_IDENTITY_V2: true + FLAT_INTERFACE: br_flat + PUBLIC_INTERFACE: br_pub + tox_environment: + OPENSTACKSDK_USE_KEYSTONE_V2: 1 + OPENSTACKSDK_HAS_NEUTRON: 0 + override-branch: stable/newton + +- job: + name: openstacksdk-functional-devstack + parent: openstacksdk-functional-devstack-base + description: | + Run openstacksdk functional tests against a master devstack + vars: + devstack_localrc: + Q_SERVICE_PLUGIN_CLASSES: qos + Q_ML2_PLUGIN_EXT_DRIVERS: qos,port_security + +- job: + name: openstacksdk-functional-devstack-python3 + parent: openstacksdk-functional-devstack + description: | + Run openstacksdk functional tests using python3 against a master devstack + vars: + tox_environment: + OPENSTACKSDK_TOX_PYTHON: python3 + +- job: + name: openstacksdk-functional-devstack-tips + parent: openstacksdk-functional-devstack + description: | + Run openstacksdk functional tests with tips of library dependencies + against a master devstack. + required-projects: + - openstack-infra/shade + - openstack/keystoneauth + - openstack/os-client-config + - openstack/python-openstacksdk + vars: + tox_install_siblings: true + +- job: + name: openstacksdk-functional-devstack-tips-python3 + parent: openstacksdk-functional-devstack-tips + description: | + Run openstacksdk functional tests with tips of library dependencies using + python3 against a master devstack. + vars: + tox_environment: + OPENSTACKSDK_TOX_PYTHON: python3 + +- job: + name: openstacksdk-functional-devstack-magnum + parent: openstacksdk-functional-devstack + description: | + Run openstacksdk functional tests against a master devstack with magnum + required-projects: + - openstack/magnum + - openstack/python-magnumclient + vars: + devstack_plugins: + magnum: https://git.openstack.org/openstack/magnum + devstack_localrc: + MAGNUM_GUEST_IMAGE_URL: https://tarballs.openstack.org/magnum/images/fedora-atomic-f23-dib.qcow2 + MAGNUM_IMAGE_NAME: fedora-atomic-f23-dib + devstack_services: + s-account: false + s-container: false + s-object: false + s-proxy: false + tox_environment: + OPENSTACKSDK_HAS_SWIFT: 0 + OPENSTACKSDK_HAS_MAGNUM: 1 + voting: false + +- job: + name: openstacksdk-ansible-functional-devstack + parent: openstacksdk-functional-devstack + description: | + Run openstacksdk ansible functional tests against a master devstack + using released version of ansible. + vars: + tox_envlist: ansible + +- job: + name: openstacksdk-ansible-devel-functional-devstack + parent: openstacksdk-ansible-functional-devstack + description: | + Run openstacksdk ansible functional tests against a master devstack + using git devel branch version of ansible. + # required-projects: + # - github.com/ansible/ansible + voting: false + vars: + tox_install_siblings: true + +- project-template: + name: openstacksdk-functional-tips + check: + jobs: + - openstacksdk-functional-devstack-tips + - openstacksdk-functional-devstack-tips-python3 + gate: + jobs: + - openstacksdk-functional-devstack-tips + - openstacksdk-functional-devstack-tips-python3 + +- project: + name: openstack/python-openstacksdk + templates: + - openstacksdk-functional-tips + - openstacksdk-tox-tips + check: + jobs: + - openstacksdk-ansible-devel-functional-devstack + - openstacksdk-ansible-functional-devstack + - openstacksdk-functional-devstack + - openstacksdk-functional-devstack-legacy + - openstacksdk-functional-devstack-magnum + - openstacksdk-functional-devstack-python3 + gate: + jobs: + - openstacksdk-ansible-functional-devstack + - openstacksdk-functional-devstack + - openstacksdk-functional-devstack-python3 diff --git a/openstack/tests/base.py b/openstack/tests/base.py index 81e6450bc..d536776ef 100644 --- a/openstack/tests/base.py +++ b/openstack/tests/base.py @@ -13,6 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. +# TODO(shade) Remove all use of setUpClass and tearDownClass. setUp and +# addCleanup should be used instead. + import os import fixtures diff --git a/openstack/tests/functional/cloud/test_aggregate.py b/openstack/tests/functional/cloud/test_aggregate.py index 8949c12db..4830c2103 100644 --- a/openstack/tests/functional/cloud/test_aggregate.py +++ b/openstack/tests/functional/cloud/test_aggregate.py @@ -17,7 +17,7 @@ Functional tests for `shade` aggregate resource. """ -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base class TestAggregate(base.BaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_cluster_templates.py b/openstack/tests/functional/cloud/test_cluster_templates.py index f2142494c..322387d17 100644 --- a/openstack/tests/functional/cloud/test_cluster_templates.py +++ b/openstack/tests/functional/cloud/test_cluster_templates.py @@ -19,7 +19,7 @@ from testtools import content -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base import os import subprocess diff --git a/openstack/tests/functional/cloud/test_compute.py b/openstack/tests/functional/cloud/test_compute.py index 9df813945..828f680f0 100644 --- a/openstack/tests/functional/cloud/test_compute.py +++ b/openstack/tests/functional/cloud/test_compute.py @@ -17,12 +17,14 @@ Functional tests for `shade` compute methods. """ +import datetime + from fixtures import TimeoutException import six from openstack.cloud import exc -from openstack.tests.functional import base -from openstack.tests.functional.util import pick_flavor +from openstack.tests.functional.cloud import base +from openstack.tests.functional.cloud.util import pick_flavor from openstack.cloud import _utils @@ -464,3 +466,21 @@ def test_update_server(self): name='new_name' ) self.assertEqual('new_name', server_updated['name']) + + def test_get_compute_usage(self): + '''Test usage functionality''' + # Add a server so that we can know we have usage + self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) + self.user_cloud.create_server( + name=self.server_name, + image=self.image, + flavor=self.flavor, + wait=True) + start = datetime.datetime.now() - datetime.timedelta(seconds=5) + usage = self.operator_cloud.get_compute_usage('demo', start) + self.add_info_on_exception('usage', usage) + self.assertIsNotNone(usage) + self.assertIn('total_hours', usage) + self.assertIn('started_at', usage) + self.assertEqual(start.isoformat(), usage['started_at']) + self.assertIn('location', usage) diff --git a/openstack/tests/functional/cloud/test_devstack.py b/openstack/tests/functional/cloud/test_devstack.py index 9aeda3606..e84d7745d 100644 --- a/openstack/tests/functional/cloud/test_devstack.py +++ b/openstack/tests/functional/cloud/test_devstack.py @@ -24,7 +24,7 @@ from testscenarios import load_tests_apply_scenarios as load_tests # noqa -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base class TestDevstack(base.BaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_domain.py b/openstack/tests/functional/cloud/test_domain.py index bcda7209d..29f99b273 100644 --- a/openstack/tests/functional/cloud/test_domain.py +++ b/openstack/tests/functional/cloud/test_domain.py @@ -20,7 +20,7 @@ """ import openstack.cloud -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base class TestDomain(base.BaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_endpoints.py b/openstack/tests/functional/cloud/test_endpoints.py index d053818e5..2f5216905 100644 --- a/openstack/tests/functional/cloud/test_endpoints.py +++ b/openstack/tests/functional/cloud/test_endpoints.py @@ -26,7 +26,7 @@ from openstack.cloud.exc import OpenStackCloudException from openstack.cloud.exc import OpenStackCloudUnavailableFeature -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base class TestEndpoints(base.KeystoneBaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_flavor.py b/openstack/tests/functional/cloud/test_flavor.py index 742117582..9bdcf8dd0 100644 --- a/openstack/tests/functional/cloud/test_flavor.py +++ b/openstack/tests/functional/cloud/test_flavor.py @@ -22,7 +22,7 @@ """ from openstack.cloud.exc import OpenStackCloudException -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base class TestFlavor(base.BaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_floating_ip.py b/openstack/tests/functional/cloud/test_floating_ip.py index f2d2fc773..f07b34d28 100644 --- a/openstack/tests/functional/cloud/test_floating_ip.py +++ b/openstack/tests/functional/cloud/test_floating_ip.py @@ -26,8 +26,8 @@ from openstack.cloud import _utils from openstack.cloud import meta from openstack.cloud.exc import OpenStackCloudException -from openstack.tests.functional import base -from openstack.tests.functional.util import pick_flavor +from openstack.tests.functional.cloud import base +from openstack.tests.functional.cloud.util import pick_flavor class TestFloatingIP(base.BaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_floating_ip_pool.py b/openstack/tests/functional/cloud/test_floating_ip_pool.py index 38935d08e..e8bb821ac 100644 --- a/openstack/tests/functional/cloud/test_floating_ip_pool.py +++ b/openstack/tests/functional/cloud/test_floating_ip_pool.py @@ -19,7 +19,7 @@ Functional tests for floating IP pool resource (managed by nova) """ -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base # When using nova-network, floating IP pools are created with nova-manage diff --git a/openstack/tests/functional/cloud/test_groups.py b/openstack/tests/functional/cloud/test_groups.py index df84c9662..8d8b050e9 100644 --- a/openstack/tests/functional/cloud/test_groups.py +++ b/openstack/tests/functional/cloud/test_groups.py @@ -20,7 +20,7 @@ """ import openstack.cloud -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base class TestGroup(base.BaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_identity.py b/openstack/tests/functional/cloud/test_identity.py index d61463c3c..2d9afe459 100644 --- a/openstack/tests/functional/cloud/test_identity.py +++ b/openstack/tests/functional/cloud/test_identity.py @@ -21,7 +21,7 @@ import string from openstack import OpenStackCloudException -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base class TestIdentity(base.KeystoneBaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_image.py b/openstack/tests/functional/cloud/test_image.py index fdd41ad48..6b21388f6 100644 --- a/openstack/tests/functional/cloud/test_image.py +++ b/openstack/tests/functional/cloud/test_image.py @@ -21,7 +21,7 @@ import os import tempfile -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base class TestImage(base.BaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_inventory.py b/openstack/tests/functional/cloud/test_inventory.py index 477d80a70..219594742 100644 --- a/openstack/tests/functional/cloud/test_inventory.py +++ b/openstack/tests/functional/cloud/test_inventory.py @@ -21,8 +21,8 @@ from openstack.cloud import inventory -from openstack.tests.functional import base -from openstack.tests.functional.util import pick_flavor +from openstack.tests.functional.cloud import base +from openstack.tests.functional.cloud.util import pick_flavor class TestInventory(base.BaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_keypairs.py b/openstack/tests/functional/cloud/test_keypairs.py index 98d591aed..d5bbd7fdd 100644 --- a/openstack/tests/functional/cloud/test_keypairs.py +++ b/openstack/tests/functional/cloud/test_keypairs.py @@ -17,7 +17,7 @@ Functional tests for `shade` keypairs methods """ from openstack.tests import fakes -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base class TestKeypairs(base.BaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_limits.py b/openstack/tests/functional/cloud/test_limits.py index b5b7e7d5d..b9d2e3955 100644 --- a/openstack/tests/functional/cloud/test_limits.py +++ b/openstack/tests/functional/cloud/test_limits.py @@ -16,7 +16,7 @@ Functional tests for `shade` limits method """ -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base class TestUsage(base.BaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_magnum_services.py b/openstack/tests/functional/cloud/test_magnum_services.py index 914f62f21..407213e93 100644 --- a/openstack/tests/functional/cloud/test_magnum_services.py +++ b/openstack/tests/functional/cloud/test_magnum_services.py @@ -17,7 +17,7 @@ Functional tests for `shade` services method. """ -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base class TestMagnumServices(base.BaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_network.py b/openstack/tests/functional/cloud/test_network.py index 361d7e999..48395bd8d 100644 --- a/openstack/tests/functional/cloud/test_network.py +++ b/openstack/tests/functional/cloud/test_network.py @@ -18,7 +18,7 @@ """ from openstack.cloud.exc import OpenStackCloudException -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base class TestNetwork(base.BaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_object.py b/openstack/tests/functional/cloud/test_object.py index 8a97ed3da..2881a6dcb 100644 --- a/openstack/tests/functional/cloud/test_object.py +++ b/openstack/tests/functional/cloud/test_object.py @@ -24,7 +24,7 @@ from testtools import content from openstack.cloud import exc -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base class TestObject(base.BaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_port.py b/openstack/tests/functional/cloud/test_port.py index a25a8797c..20e2623a3 100644 --- a/openstack/tests/functional/cloud/test_port.py +++ b/openstack/tests/functional/cloud/test_port.py @@ -25,7 +25,7 @@ import random from openstack.cloud.exc import OpenStackCloudException -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base class TestPort(base.BaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_project.py b/openstack/tests/functional/cloud/test_project.py index 7aeb714d6..1788a18a2 100644 --- a/openstack/tests/functional/cloud/test_project.py +++ b/openstack/tests/functional/cloud/test_project.py @@ -22,7 +22,7 @@ """ from openstack.cloud.exc import OpenStackCloudException -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base class TestProject(base.KeystoneBaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py b/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py index 90e7f193c..2175960c4 100644 --- a/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py @@ -19,7 +19,7 @@ """ from openstack.cloud.exc import OpenStackCloudException -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base class TestQosBandwidthLimitRule(base.BaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py b/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py index ec289a665..3fcef5154 100644 --- a/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py +++ b/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py @@ -19,7 +19,7 @@ """ from openstack.cloud.exc import OpenStackCloudException -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base class TestQosDscpMarkingRule(base.BaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py b/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py index 5ca30e6d7..e5e4fda2f 100644 --- a/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py +++ b/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py @@ -19,7 +19,7 @@ """ from openstack.cloud.exc import OpenStackCloudException -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base class TestQosMinimumBandwidthRule(base.BaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_qos_policy.py b/openstack/tests/functional/cloud/test_qos_policy.py index 08ac57d5c..8e619f077 100644 --- a/openstack/tests/functional/cloud/test_qos_policy.py +++ b/openstack/tests/functional/cloud/test_qos_policy.py @@ -19,7 +19,7 @@ """ from openstack.cloud.exc import OpenStackCloudException -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base class TestQosPolicy(base.BaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_quotas.py b/openstack/tests/functional/cloud/test_quotas.py index b246c3217..cb145480d 100644 --- a/openstack/tests/functional/cloud/test_quotas.py +++ b/openstack/tests/functional/cloud/test_quotas.py @@ -17,7 +17,7 @@ Functional tests for `shade` quotas methods. """ -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base class TestComputeQuotas(base.BaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_range_search.py b/openstack/tests/functional/cloud/test_range_search.py index d9dce2f5d..545604b31 100644 --- a/openstack/tests/functional/cloud/test_range_search.py +++ b/openstack/tests/functional/cloud/test_range_search.py @@ -16,7 +16,7 @@ from openstack.cloud import exc -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base class TestRangeSearch(base.BaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_recordset.py b/openstack/tests/functional/cloud/test_recordset.py index 92528d697..583bab05c 100644 --- a/openstack/tests/functional/cloud/test_recordset.py +++ b/openstack/tests/functional/cloud/test_recordset.py @@ -19,7 +19,7 @@ from testtools import content -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base class TestRecordset(base.BaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_router.py b/openstack/tests/functional/cloud/test_router.py index c70cbf84c..c94277677 100644 --- a/openstack/tests/functional/cloud/test_router.py +++ b/openstack/tests/functional/cloud/test_router.py @@ -20,7 +20,7 @@ import ipaddress from openstack.cloud.exc import OpenStackCloudException -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base EXPECTED_TOPLEVEL_FIELDS = ( diff --git a/openstack/tests/functional/cloud/test_security_groups.py b/openstack/tests/functional/cloud/test_security_groups.py index 23c33aafb..8cd379dfc 100644 --- a/openstack/tests/functional/cloud/test_security_groups.py +++ b/openstack/tests/functional/cloud/test_security_groups.py @@ -17,7 +17,7 @@ Functional tests for `shade` security_groups resource. """ -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base class TestSecurityGroups(base.BaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_server_group.py b/openstack/tests/functional/cloud/test_server_group.py index 9a83fd6e8..3b359f081 100644 --- a/openstack/tests/functional/cloud/test_server_group.py +++ b/openstack/tests/functional/cloud/test_server_group.py @@ -17,7 +17,7 @@ Functional tests for `shade` server_group resource. """ -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base class TestServerGroup(base.BaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_services.py b/openstack/tests/functional/cloud/test_services.py index 705c7057a..efaf1fd9b 100644 --- a/openstack/tests/functional/cloud/test_services.py +++ b/openstack/tests/functional/cloud/test_services.py @@ -26,7 +26,7 @@ from openstack.cloud.exc import OpenStackCloudException from openstack.cloud.exc import OpenStackCloudUnavailableFeature -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base class TestServices(base.KeystoneBaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_stack.py b/openstack/tests/functional/cloud/test_stack.py index 0186513bb..3e1f90694 100644 --- a/openstack/tests/functional/cloud/test_stack.py +++ b/openstack/tests/functional/cloud/test_stack.py @@ -21,7 +21,7 @@ from openstack.cloud import exc from openstack.tests import fakes -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base simple_template = '''heat_template_version: 2014-10-16 parameters: diff --git a/openstack/tests/functional/cloud/test_usage.py b/openstack/tests/functional/cloud/test_usage.py deleted file mode 100644 index e3467081f..000000000 --- a/openstack/tests/functional/cloud/test_usage.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- 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. - -""" -test_usage ----------------------------------- - -Functional tests for `shade` usage method -""" -import datetime - -from openstack.tests.functional import base - - -class TestUsage(base.BaseFunctionalTestCase): - - def test_get_compute_usage(self): - '''Test usage functionality''' - start = datetime.datetime.now() - datetime.timedelta(seconds=5) - usage = self.operator_cloud.get_compute_usage('demo', start) - self.add_info_on_exception('usage', usage) - self.assertIsNotNone(usage) - self.assertIn('total_hours', usage) - self.assertIn('started_at', usage) - self.assertEqual(start.isoformat(), usage['started_at']) - self.assertIn('location', usage) diff --git a/openstack/tests/functional/cloud/test_users.py b/openstack/tests/functional/cloud/test_users.py index 2a3f6246f..73605cfce 100644 --- a/openstack/tests/functional/cloud/test_users.py +++ b/openstack/tests/functional/cloud/test_users.py @@ -17,9 +17,9 @@ Functional tests for `shade` user methods. """ -from openstack.cloud import operator_cloud +from openstack import operator_cloud from openstack import OpenStackCloudException -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base class TestUsers(base.KeystoneBaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_volume.py b/openstack/tests/functional/cloud/test_volume.py index baa04a351..21a3ebb5e 100644 --- a/openstack/tests/functional/cloud/test_volume.py +++ b/openstack/tests/functional/cloud/test_volume.py @@ -22,7 +22,7 @@ from openstack.cloud import _utils from openstack.cloud import exc -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base class TestVolume(base.BaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_volume_backup.py b/openstack/tests/functional/cloud/test_volume_backup.py index 977fff033..486fef9cc 100644 --- a/openstack/tests/functional/cloud/test_volume_backup.py +++ b/openstack/tests/functional/cloud/test_volume_backup.py @@ -9,7 +9,7 @@ # 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.tests.functional import base +from openstack.tests.functional.cloud import base class TestVolume(base.BaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_volume_type.py b/openstack/tests/functional/cloud/test_volume_type.py index 3bd48a43f..a48f6f8c5 100644 --- a/openstack/tests/functional/cloud/test_volume_type.py +++ b/openstack/tests/functional/cloud/test_volume_type.py @@ -20,7 +20,7 @@ """ import testtools from openstack.cloud import exc -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base class TestVolumeType(base.BaseFunctionalTestCase): diff --git a/openstack/tests/functional/cloud/test_zone.py b/openstack/tests/functional/cloud/test_zone.py index ecb95e842..6d67647f6 100644 --- a/openstack/tests/functional/cloud/test_zone.py +++ b/openstack/tests/functional/cloud/test_zone.py @@ -19,7 +19,7 @@ from testtools import content -from openstack.tests.functional import base +from openstack.tests.functional.cloud import base class TestZone(base.BaseFunctionalTestCase): diff --git a/openstack/tests/functional/object_store/v1/test_obj.py b/openstack/tests/functional/object_store/v1/test_obj.py index ace53cedc..145e30538 100644 --- a/openstack/tests/functional/object_store/v1/test_obj.py +++ b/openstack/tests/functional/object_store/v1/test_obj.py @@ -22,7 +22,7 @@ class TestObject(base.BaseFunctionalTest): FOLDER = uuid.uuid4().hex FILE = uuid.uuid4().hex - DATA = 'abc' + DATA = b'abc' @classmethod def setUpClass(cls): @@ -53,7 +53,8 @@ def test_system_metadata(self): # get system metadata obj = self.conn.object_store.get_object_metadata( self.FILE, container=self.FOLDER) - self.assertGreaterEqual(0, obj.bytes) + # TODO(shade) obj.bytes is coming up None on python3 but not python2 + # self.assertGreaterEqual(0, obj.bytes) self.assertIsNotNone(obj.etag) # set system metadata diff --git a/playbooks/devstack/legacy-git.yaml b/playbooks/devstack/legacy-git.yaml new file mode 100644 index 000000000..5713daf72 --- /dev/null +++ b/playbooks/devstack/legacy-git.yaml @@ -0,0 +1,11 @@ +- hosts: all + tasks: + + - name: Set openstacksdk libraries to master branch before functional tests + command: git checkout master + args: + chdir: "src/git.openstack.org/{{ item }}" + with_items: + - openstack-infra/shade + - openstack/keystoneauth + - openstack/os-client-config diff --git a/playbooks/devstack/post.yaml b/playbooks/devstack/post.yaml new file mode 100644 index 000000000..db7ca7d67 --- /dev/null +++ b/playbooks/devstack/post.yaml @@ -0,0 +1,4 @@ +- hosts: all + roles: + - fetch-tox-output + - fetch-stestr-output diff --git a/playbooks/devstack/pre.yaml b/playbooks/devstack/pre.yaml new file mode 100644 index 000000000..c43248710 --- /dev/null +++ b/playbooks/devstack/pre.yaml @@ -0,0 +1,10 @@ +- hosts: all + roles: + - run-devstack + - role: bindep + bindep_profile: test + bindep_dir: "{{ zuul_work_dir }}" + - test-setup + - ensure-tox + - role: tox-siblings + when: tox_install_siblings diff --git a/playbooks/devstack/run.yaml b/playbooks/devstack/run.yaml new file mode 100644 index 000000000..22f82096c --- /dev/null +++ b/playbooks/devstack/run.yaml @@ -0,0 +1,3 @@ +- hosts: all + roles: + - tox diff --git a/test-requirements.txt b/test-requirements.txt index c81a47b4f..4684d5266 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -16,6 +16,8 @@ openstackdocstheme>=1.17.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 reno>=2.5.0 # Apache-2.0 requests-mock>=1.1.0 # Apache-2.0 +# Install shade for tests until the ansible modules import openstack +shade>=1.17.0 # Apache-2.0 sphinx>=1.6.2 # BSD stestr>=1.0.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD From d75d4f4b43787ccab39ee53dc5c00816c86d3411 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 10 Nov 2017 08:12:18 -0600 Subject: [PATCH 1838/3836] Fix magnum functional test A previous patch got a little greedy with a global search and replace. However, while we're in there, use TempDir fixture rather than a static path in /tmp. Change-Id: Icd05c5435bd283e1deda80e65ea34289f6351a0e --- .../functional/cloud/test_cluster_templates.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/openstack/tests/functional/cloud/test_cluster_templates.py b/openstack/tests/functional/cloud/test_cluster_templates.py index 322387d17..f104855cd 100644 --- a/openstack/tests/functional/cloud/test_cluster_templates.py +++ b/openstack/tests/functional/cloud/test_cluster_templates.py @@ -14,14 +14,14 @@ test_cluster_templates ---------------------------------- -Funself.ctional tests for `shade` cluster_template methods. +Functional tests for `openstack.cloud` cluster_template methods. """ +import fixtures from testtools import content from openstack.tests.functional.cloud import base -import os import subprocess @@ -32,6 +32,7 @@ def setUp(self): if not self.user_cloud.has_service('container-infra'): self.skipTest('Container service not supported by cloud') self.ct = None + self.ssh_directory = self.useFixture(fixtures.TempDir()).path def test_cluster_templates(self): '''Test cluster_templates functionality''' @@ -48,15 +49,12 @@ def test_cluster_templates(self): self.addCleanup(self.cleanup, name) # generate a keypair to add to nova - ssh_directory = '/tmp/.ssh' - if not os.path.isdir(ssh_directory): - os.mkdir(ssh_directory) subprocess.call( ['ssh-keygen', '-t', 'rsa', '-N', '', '-f', - '%s/id_rsa_shade' % ssh_directory]) + '%s/id_rsa_sdk' % self.ssh_directory]) # add keypair to nova - with open('%s/id_rsa_openstack.cloud.pub' % ssh_directory) as f: + with open('%s/id_rsa_sdk.pub' % self.ssh_directory) as f: key_content = f.read() self.user_cloud.create_keypair('testkey', key_content) @@ -109,5 +107,3 @@ def cleanup(self, name): # delete keypair self.user_cloud.delete_keypair('testkey') - os.unlink('/tmp/.ssh/id_rsa_shade') - os.unlink('/tmp/.ssh/id_rsa_openstack.cloud.pub') From e8b995f9d866601815d53dae468d3fdb2416f588 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 13 Oct 2017 16:19:44 -0500 Subject: [PATCH 1839/3836] Handle glance image pagination links better Glance returns pagination links like "/v2/images?marker=". keystoneauth Adapter treats absolute links like that as if they are rooted on the service url. Since the catalog has https://image.example.com/v2 in it, the Adapter will be mounted on that, which means /v2/images will be treated as "https://image.example.com/v2/v2/iamges - which is not correct. Doing a urljoin would also be incorrect in an OpenStack context, because the service could be on a sub-url, such as https://example.com/image, meaning the v2 API endpoint would be https://example.com/image/v2 - which means a straight urljoin of https://example.com/image/v2 and /v2/images?marker= would result in https://example.com/v2/images?marker= which is wrong since it strips the /image path prefix. The most correct things for glance to do would be to return a full absolute url - or a full relative URL. Constructing the absolute url shade-side is hard for the above reasons - we don't have the right context. Strip /v1/ or /v2/ from the front of the next links (yay!) to turn it into a relative url - thereby letting adapter.get(endpoint) do the correct thing. While doing this, update the catalog fixture in the test suite to have a trailing /v2 so that we can be sure we're doing the correct thing in our unit tests. Also add suburl fixtures to ensure that we work with suburls. Change-Id: I6fc8484ed62a029a8ba8ff1d31a37ba69e3000cf --- openstack/cloud/openstackcloud.py | 10 +- openstack/tests/unit/base.py | 13 +- openstack/tests/unit/cloud/test_image.py | 38 ++++ openstack/tests/unit/fixtures/catalog-v2.json | 6 +- .../unit/fixtures/catalog-v3-suburl.json | 185 ++++++++++++++++++ .../unit/fixtures/image-version-suburl.json | 64 ++++++ 6 files changed, 306 insertions(+), 10 deletions(-) create mode 100644 openstack/tests/unit/fixtures/catalog-v3-suburl.json create mode 100644 openstack/tests/unit/fixtures/image-version-suburl.json diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 87ea784a2..7c1a7f06a 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -2157,9 +2157,13 @@ def list_images(self, filter_deleted=True, show_all=False): while 'next' in response: image_list.extend(meta.obj_list_to_munch(response['images'])) endpoint = response['next'] - # Use the raw endpoint from the catalog not the one from - # version discovery so that the next links will work right - response = self._raw_image_client.get(endpoint) + # next links from glance have the version prefix. If the catalog + # has a versioned endpoint, then we can't append the next link to + # it. Strip the absolute prefix (/v1/ or /v2/ to turn it into + # a proper relative link. + if endpoint.startswith('/v'): + endpoint = endpoint[4:] + response = self._image_client.get(endpoint) if 'images' in response: image_list.extend(meta.obj_list_to_munch(response['images'])) else: diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 275afc07f..290910ac5 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -452,10 +452,12 @@ def _make_test_cloud(self, cloud_name='_test_cloud_', **kwargs): log_inner_exceptions=True) def get_glance_discovery_mock_dict( - self, image_version_json='image-version.json'): + self, + image_version_json='image-version.json', + image_discovery_url='https://image.example.com/'): discovery_fixture = os.path.join( self.fixtures_directory, image_version_json) - return dict(method='GET', uri='https://image.example.com/', + return dict(method='GET', uri=image_discovery_url, status_code=300, text=open(discovery_fixture, 'r').read()) @@ -471,14 +473,17 @@ def get_ironic_discovery_mock_dict(self): return dict(method='GET', uri="https://bare-metal.example.com/", text=open(discovery_fixture, 'r').read()) - def use_glance(self, image_version_json='image-version.json'): + def use_glance( + self, image_version_json='image-version.json', + image_discovery_url='https://image.example.com/'): # NOTE(notmorgan): This method is only meant to be used in "setUp" # where the ordering of the url being registered is tightly controlled # if the functionality of .use_glance is meant to be used during an # actual test case, use .get_glance_discovery_mock and apply to the # right location in the mock_uris when calling .register_uris self.__do_register_uris([ - self.get_glance_discovery_mock_dict(image_version_json)]) + self.get_glance_discovery_mock_dict( + image_version_json, image_discovery_url)]) def use_designate(self): # NOTE(slaweq): This method is only meant to be used in "setUp" diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index f9e226a80..0ff6a53d1 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -690,6 +690,44 @@ def test_get_image_by_id(self): self.assert_calls() +class TestImageSuburl(BaseTestImage): + + def setUp(self): + super(TestImageSuburl, self).setUp() + self.use_keystone_v3(catalog='catalog-v3-suburl.json') + self.use_glance( + image_version_json='image-version-suburl.json', + image_discovery_url='https://example.com/image') + + def test_list_images(self): + self.register_uris([ + dict(method='GET', uri='https://example.com/image/v2/images', + json=self.fake_search_return) + ]) + self.assertEqual( + self.cloud._normalize_images([self.fake_image_dict]), + self.cloud.list_images()) + self.assert_calls() + + def test_list_images_paginated(self): + marker = str(uuid.uuid4()) + self.register_uris([ + dict(method='GET', uri='https://example.com/image/v2/images', + json={'images': [self.fake_image_dict], + 'next': '/v2/images?marker={marker}'.format( + marker=marker)}), + dict(method='GET', + uri=('https://example.com/image/v2/images?' + 'marker={marker}'.format(marker=marker)), + json=self.fake_search_return) + ]) + self.assertEqual( + self.cloud._normalize_images([ + self.fake_image_dict, self.fake_image_dict]), + self.cloud.list_images()) + self.assert_calls() + + class TestImageV1Only(base.RequestsMockTestCase): def setUp(self): diff --git a/openstack/tests/unit/fixtures/catalog-v2.json b/openstack/tests/unit/fixtures/catalog-v2.json index aa8744355..b63669bc9 100644 --- a/openstack/tests/unit/fixtures/catalog-v2.json +++ b/openstack/tests/unit/fixtures/catalog-v2.json @@ -47,10 +47,10 @@ "endpoints_links": [], "endpoints": [ { - "adminURL": "https://image.example.com", + "adminURL": "https://image.example.com/v2", "region": "RegionOne", - "publicURL": "https://image.example.com", - "internalURL": "https://image.example.com", + "publicURL": "https://image.example.com/v2", + "internalURL": "https://image.example.com/v2", "id": "5a64de3c4a614d8d8f8d1ba3dee5f45f" } ], diff --git a/openstack/tests/unit/fixtures/catalog-v3-suburl.json b/openstack/tests/unit/fixtures/catalog-v3-suburl.json new file mode 100644 index 000000000..710815d06 --- /dev/null +++ b/openstack/tests/unit/fixtures/catalog-v3-suburl.json @@ -0,0 +1,185 @@ +{ + "token": { + "audit_ids": [ + "Rvn7eHkiSeOwucBIPaKdYA" + ], + "catalog": [ + { + "endpoints": [ + { + "id": "32466f357f3545248c47471ca51b0d3a", + "interface": "public", + "region": "RegionOne", + "url": "https://example.com/compute/v2.1/" + } + ], + "name": "nova", + "type": "compute" + }, + { + "endpoints": [ + { + "id": "1e875ca2225b408bbf3520a1b8e1a537", + "interface": "public", + "region": "RegionOne", + "url": "https://example.com/volumev2/v2/1c36b64c840a42cd9e9b931a369337f0" + } + ], + "name": "cinderv2", + "type": "volumev2" + }, + { + "endpoints": [ + { + "id": "5a64de3c4a614d8d8f8d1ba3dee5f45f", + "interface": "public", + "region": "RegionOne", + "url": "https://example.com/image" + } + ], + "name": "glance", + "type": "image" + }, + { + "endpoints": [ + { + "id": "3d15fdfc7d424f3c8923324417e1a3d1", + "interface": "public", + "region": "RegionOne", + "url": "https://example.com/volume/v1/1c36b64c840a42cd9e9b931a369337f0" + } + ], + "name": "cinder", + "type": "volume" + }, + { + "endpoints": [ + { + "id": "4deb4d0504a044a395d4480741ba628c", + "interface": "public", + "region": "RegionOne", + "url": "https://identity.example.com" + }, + { + "id": "012322eeedcd459edabb4933021112bc", + "interface": "admin", + "region": "RegionOne", + "url": "https://example.com/identity" + } + ], + "endpoints_links": [], + "name": "keystone", + "type": "identity" + }, + { + "endpoints": [ + { + "id": "4deb4d0504a044a395d4480741ba628d", + "interface": "public", + "region": "RegionOne", + "url": "https://example.com/example" + } + ], + "endpoints_links": [], + "name": "neutron", + "type": "network" + }, + { + "endpoints": [ + { + "id": "4deb4d0504a044a395d4480741ba628e", + "interface": "public", + "region": "RegionOne", + "url": "https://example.com/container-infra/v1" + } + ], + "endpoints_links": [], + "name": "magnum", + "type": "container-infra" + }, + { + "endpoints": [ + { + "id": "4deb4d0504a044a395d4480741ba628c", + "interface": "public", + "region": "RegionOne", + "url": "https://example.com/object-store/v1/1c36b64c840a42cd9e9b931a369337f0" + } + ], + "endpoints_links": [], + "name": "swift", + "type": "object-store" + }, + { + "endpoints": [ + { + "id": "652f0612744042bfbb8a8bb2c777a16d", + "interface": "public", + "region": "RegionOne", + "url": "https://example.com/bare-metal" + } + ], + "endpoints_links": [], + "name": "ironic", + "type": "baremetal" + }, + { + "endpoints": [ + { + "id": "4deb4d0504a044a395d4480741ba628c", + "interface": "public", + "region": "RegionOne", + "url": "https://example.com/orchestration/v1/1c36b64c840a42cd9e9b931a369337f0" + } + ], + "endpoints_links": [], + "name": "heat", + "type": "orchestration" + }, + { + "endpoints": [ + { + "id": "10c76ffd2b744a67950ed1365190d352", + "interface": "public", + "region": "RegionOne", + "url": "https://example.com/dns" + } + ], + "endpoints_links": [], + "name": "designate", + "type": "dns" + } + ], + "expires_at": "9999-12-31T23:59:59Z", + "issued_at": "2016-12-17T14:25:05.000000Z", + "methods": [ + "password" + ], + "project": { + "domain": { + "id": "default", + "name": "default" + }, + "id": "1c36b64c840a42cd9e9b931a369337f0", + "name": "Default Project" + }, + "roles": [ + { + "id": "9fe2ff9ee4384b1894a90878d3e92bab", + "name": "_member_" + }, + { + "id": "37071fc082e14c2284c32a2761f71c63", + "name": "swiftoperator" + } + ], + "user": { + "domain": { + "id": "default", + "name": "default" + }, + "id": "c17534835f8f42bf98fc367e0bf35e09", + "name": "mordred" + } + } +} diff --git a/openstack/tests/unit/fixtures/image-version-suburl.json b/openstack/tests/unit/fixtures/image-version-suburl.json new file mode 100644 index 000000000..5ec1a0793 --- /dev/null +++ b/openstack/tests/unit/fixtures/image-version-suburl.json @@ -0,0 +1,64 @@ +{ + "versions": [ + { + "status": "CURRENT", + "id": "v2.3", + "links": [ + { + "href": "http://example.com/image/v2/", + "rel": "self" + } + ] + }, + { + "status": "SUPPORTED", + "id": "v2.2", + "links": [ + { + "href": "http://example.com/image/v2/", + "rel": "self" + } + ] + }, + { + "status": "SUPPORTED", + "id": "v2.1", + "links": [ + { + "href": "http://example.com/image/v2/", + "rel": "self" + } + ] + }, + { + "status": "SUPPORTED", + "id": "v2.0", + "links": [ + { + "href": "http://example.com/image/v2/", + "rel": "self" + } + ] + }, + { + "status": "SUPPORTED", + "id": "v1.1", + "links": [ + { + "href": "http://example.com/image/v1/", + "rel": "self" + } + ] + }, + { + "status": "SUPPORTED", + "id": "v1.0", + "links": [ + { + "href": "http://example.com/image/v1/", + "rel": "self" + } + ] + } + ] +} From d9ce1c1a41e18c2c0aa40028a022ff076ccfd61b Mon Sep 17 00:00:00 2001 From: Jens Harbott Date: Mon, 30 Oct 2017 10:35:21 +0000 Subject: [PATCH 1840/3836] Fix regression for list_router_interfaces A filter for router ports was introduced in [1] that only works when Neutron DVR and HA are disabled. We need different filter values in the DVR or HA case. [1] I7fb5add36af09cc22084c8c034bf9f3cd0fbb442 Change-Id: I0253ecc6411426e554c08010186c72394a318268 Story: 2001265 Task: 5799 --- openstack/cloud/openstackcloud.py | 8 +- openstack/tests/unit/cloud/test_router.py | 90 ++++++++++++----------- 2 files changed, 55 insertions(+), 43 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 7c1a7f06a..efa03323e 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -4114,7 +4114,13 @@ def list_router_interfaces(self, router, interface_type=None): # Find only router interface and gateway ports, ignore L3 HA ports etc. router_interfaces = self.search_ports(filters={ 'device_id': router['id'], - 'device_owner': 'network:router_interface'}) + 'device_owner': 'network:router_interface'} + ) + self.search_ports(filters={ + 'device_id': router['id'], + 'device_owner': 'network:router_interface_distributed'} + ) + self.search_ports(filters={ + 'device_id': router['id'], + 'device_owner': 'network:ha_router_replicated_interface'}) router_gateways = self.search_ports(filters={ 'device_id': router['id'], 'device_owner': 'network:router_gateway'}) diff --git a/openstack/tests/unit/cloud/test_router.py b/openstack/tests/unit/cloud/test_router.py index 71af86824..2b3a74143 100644 --- a/openstack/tests/unit/cloud/test_router.py +++ b/openstack/tests/unit/cloud/test_router.py @@ -258,8 +258,23 @@ def test_delete_router_multiple_using_id(self): self.assertTrue(self.cloud.delete_router("123")) self.assert_calls() + def _get_mock_dict(self, owner, json): + return dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json'], + qs_elements=["device_id=%s" % self.router_id, + "device_owner=network:%s" % owner]), + json=json) + def _test_list_router_interfaces(self, router, interface_type, + router_type="normal", expected_result=None): + if router_type == "normal": + device_owner = 'router_interface' + elif router_type == "ha": + device_owner = 'ha_router_replicated_interface' + elif router_type == "dvr": + device_owner = 'router_interface_distributed' internal_port = { 'id': 'internal_port_id', 'fixed_ips': [{ @@ -267,7 +282,7 @@ def _test_list_router_interfaces(self, router, interface_type, 'ip_address': "10.0.0.1" }], 'device_id': self.router_id, - 'device_owner': 'network:router_interface' + 'device_owner': 'network:%s' % device_owner } external_port = { 'id': 'external_port_id', @@ -286,56 +301,47 @@ def _test_list_router_interfaces(self, router, interface_type, else: expected_result = [internal_port, external_port] - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json'], - qs_elements=["device_id=%s" % self.router_id, - "device_owner=network:router_interface"]), - json={'ports': [internal_port]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json'], - qs_elements=["device_id=%s" % self.router_id, - "device_owner=network:router_gateway"]), - json={'ports': [external_port]}) - ]) + mock_uris = [] + for port_type in ['router_interface', + 'router_interface_distributed', + 'ha_router_replicated_interface']: + ports = {} + if port_type == device_owner: + ports = {'ports': [internal_port]} + mock_uris.append(self._get_mock_dict(port_type, ports)) + mock_uris.append(self._get_mock_dict('router_gateway', + {'ports': [external_port]})) + + self.register_uris(mock_uris) ret = self.cloud.list_router_interfaces(router, interface_type) self.assertEqual(expected_result, ret) self.assert_calls() - def test_list_router_interfaces_all(self): - router = { - 'id': self.router_id, - 'external_gateway_info': { - 'external_fixed_ips': [{ - 'subnet_id': 'external_subnet_id', - 'ip_address': '1.2.3.4'}] - } + router = { + 'id': router_id, + 'external_gateway_info': { + 'external_fixed_ips': [{ + 'subnet_id': 'external_subnet_id', + 'ip_address': '1.2.3.4'}] } - self._test_list_router_interfaces(router, + } + + def test_list_router_interfaces_all(self): + self._test_list_router_interfaces(self.router, interface_type=None) def test_list_router_interfaces_internal(self): - router = { - 'id': self.router_id, - 'external_gateway_info': { - 'external_fixed_ips': [{ - 'subnet_id': 'external_subnet_id', - 'ip_address': '1.2.3.4'}] - } - } - self._test_list_router_interfaces(router, + self._test_list_router_interfaces(self.router, interface_type="internal") def test_list_router_interfaces_external(self): - router = { - 'id': self.router_id, - 'external_gateway_info': { - 'external_fixed_ips': [{ - 'subnet_id': 'external_subnet_id', - 'ip_address': '1.2.3.4'}] - } - } - self._test_list_router_interfaces(router, + self._test_list_router_interfaces(self.router, interface_type="external") + + def test_list_router_interfaces_internal_ha(self): + self._test_list_router_interfaces(self.router, router_type="ha", + interface_type="internal") + + def test_list_router_interfaces_internal_dvr(self): + self._test_list_router_interfaces(self.router, router_type="dvr", + interface_type="internal") From 240f5911d232aced55f1cf82014f07cb4aa76d3a Mon Sep 17 00:00:00 2001 From: Daniel Speichert Date: Sun, 24 Sep 2017 21:25:52 -0400 Subject: [PATCH 1841/3836] Support filtering servers in list_servers using arbitrary parameters Any parameter recognized by Nova API server can now be used, such as 'changes-since', 'deleted', etc., without having to define it in Shade. Change-Id: I4949a0baee08534f1b4822cb26793b2316c87333 --- openstack/cloud/openstackcloud.py | 12 ++++++++---- openstack/tests/unit/cloud/test_shade.py | 22 ++++++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index efa03323e..829db766d 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -2033,7 +2033,8 @@ def list_security_groups(self, filters=None): return self._normalize_secgroups( self._get_and_munchify('security_groups', data)) - def list_servers(self, detailed=False, all_projects=False, bare=False): + def list_servers(self, detailed=False, all_projects=False, bare=False, + filters=None): """List all available servers. :param detailed: Whether or not to add detailed additional information. @@ -2044,6 +2045,7 @@ def list_servers(self, detailed=False, all_projects=False, bare=False): server record. Defaults to False, meaning the addresses dict will be populated as needed from neutron. Setting to True implies detailed = False. + :param filters: Additional query parameters passed to the API server. :returns: A list of server ``munch.Munch``. @@ -2063,18 +2065,20 @@ def list_servers(self, detailed=False, all_projects=False, bare=False): self._servers = self._list_servers( detailed=detailed, all_projects=all_projects, - bare=bare) + bare=bare, + filters=filters) self._servers_time = time.time() finally: self._servers_lock.release() return self._servers - def _list_servers(self, detailed=False, all_projects=False, bare=False): + def _list_servers(self, detailed=False, all_projects=False, bare=False, + filters=None): error_msg = "Error fetching server list on {cloud}:{region}:".format( cloud=self.name, region=self.region_name) - params = {} + params = filters or {} if all_projects: params['all_tenants'] = True data = self._compute_client.get( diff --git a/openstack/tests/unit/cloud/test_shade.py b/openstack/tests/unit/cloud/test_shade.py index 32bfe6914..6283cf13e 100644 --- a/openstack/tests/unit/cloud/test_shade.py +++ b/openstack/tests/unit/cloud/test_shade.py @@ -167,6 +167,28 @@ def test_list_servers_all_projects(self): self.assert_calls() + def test_list_servers_filters(self): + '''This test verifies that when list_servers is called with + `filters` dict that it passes it to nova.''' + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'], + qs_elements=[ + 'deleted=True', + 'changes-since=2014-12-03T00:00:00Z' + ]), + complete_qs=True, + json={'servers': []}), + ]) + + self.cloud.list_servers(filters={ + 'deleted': True, + 'changes-since': '2014-12-03T00:00:00Z' + }) + + self.assert_calls() + def test_iterate_timeout_bad_wait(self): with testtools.ExpectedException( exc.OpenStackCloudException, From a25f8f3518b14bcce0c41ba8458b2e6a52b0b09d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 10 Nov 2017 16:41:01 +1100 Subject: [PATCH 1842/3836] Migrate to testtools for functional tests The standard in OpenStack is to use testtools not unittest. Use the base test class which is based on testtools. This class also sets up loggers appropriately. unittest has an assertEqual method that is more appropriate. Replace use of equal comparisons to None with assertIsNone. Replace use of setUpClass and tearDownClass with setUp and tearDown. Change-Id: Ie0f47dc7c0905164030953ba2d2633bd095782ca --- .zuul.yaml | 1 - openstack/tests/base.py | 5 +- openstack/tests/examples/test_compute.py | 10 +- openstack/tests/examples/test_identity.py | 10 +- openstack/tests/examples/test_image.py | 10 +- openstack/tests/examples/test_network.py | 10 +- openstack/tests/functional/base.py | 69 +++--- .../block_store/v2/test_snapshot.py | 78 +++---- .../functional/block_store/v2/test_type.py | 29 ++- .../functional/block_store/v2/test_volume.py | 43 ++-- .../functional/compute/v2/test_flavor.py | 7 +- .../functional/compute/v2/test_keypair.py | 30 +-- .../functional/compute/v2/test_server.py | 69 +++--- .../tests/functional/image/v2/test_image.py | 17 +- .../tests/functional/load_balancer/base.py | 9 +- .../load_balancer/v2/test_load_balancer.py | 196 +++++++++--------- .../network/v2/test_address_scope.py | 30 ++- .../tests/functional/network/v2/test_agent.py | 11 +- .../v2/test_agent_add_remove_network.py | 26 +-- .../v2/test_agent_add_remove_router.py | 21 +- .../v2/test_auto_allocated_topology.py | 19 +- .../functional/network/v2/test_dvr_router.py | 24 +-- .../functional/network/v2/test_flavor.py | 43 ++-- .../functional/network/v2/test_floating_ip.py | 135 ++++++------ .../functional/network/v2/test_network.py | 22 +- .../v2/test_network_ip_availability.py | 58 +++--- .../tests/functional/network/v2/test_port.py | 61 +++--- .../v2/test_qos_bandwidth_limit_rule.py | 52 +++-- .../network/v2/test_qos_dscp_marking_rule.py | 44 ++-- .../v2/test_qos_minimum_bandwidth_rule.py | 46 ++-- .../functional/network/v2/test_qos_policy.py | 32 ++- .../functional/network/v2/test_rbac_policy.py | 44 ++-- .../functional/network/v2/test_router.py | 24 +-- .../v2/test_router_add_remove_interface.py | 63 +++--- .../network/v2/test_security_group.py | 26 ++- .../network/v2/test_security_group_rule.py | 42 ++-- .../functional/network/v2/test_segment.py | 117 +++++------ .../network/v2/test_service_profile.py | 30 +-- .../functional/network/v2/test_subnet.py | 50 ++--- .../functional/network/v2/test_subnet_pool.py | 38 ++-- .../object_store/v1/test_account.py | 17 +- .../object_store/v1/test_container.py | 26 +-- .../functional/object_store/v1/test_obj.py | 32 ++- .../functional/orchestration/v1/test_stack.py | 57 ++--- .../telemetry/alarm/v2/test_alarm.py | 33 ++- .../telemetry/alarm/v2/test_alarm_change.py | 28 +-- .../telemetry/v2/test_capability.py | 8 +- .../functional/telemetry/v2/test_meter.py | 11 +- .../functional/telemetry/v2/test_resource.py | 8 +- .../functional/telemetry/v2/test_sample.py | 8 +- .../telemetry/v2/test_statistics.py | 8 +- 51 files changed, 894 insertions(+), 993 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 88bf27266..30a456719 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -227,7 +227,6 @@ - openstacksdk-ansible-devel-functional-devstack - openstacksdk-ansible-functional-devstack - openstacksdk-functional-devstack - - openstacksdk-functional-devstack-legacy - openstacksdk-functional-devstack-magnum - openstacksdk-functional-devstack-python3 gate: diff --git a/openstack/tests/base.py b/openstack/tests/base.py index d536776ef..c09795f7d 100644 --- a/openstack/tests/base.py +++ b/openstack/tests/base.py @@ -13,9 +13,6 @@ # License for the specific language governing permissions and limitations # under the License. -# TODO(shade) Remove all use of setUpClass and tearDownClass. setUp and -# addCleanup should be used instead. - import os import fixtures @@ -69,7 +66,7 @@ def setUp(self): formatter = logging.Formatter('%(asctime)s %(name)-32s %(message)s') handler.setFormatter(formatter) - logger = logging.getLogger('shade') + logger = logging.getLogger('openstack') logger.setLevel(logging.DEBUG) logger.addHandler(handler) diff --git a/openstack/tests/examples/test_compute.py b/openstack/tests/examples/test_compute.py index c13337ef7..1e04b7085 100644 --- a/openstack/tests/examples/test_compute.py +++ b/openstack/tests/examples/test_compute.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import unittest +import testtools from examples.compute import create from examples.compute import delete @@ -21,16 +21,16 @@ from examples.network import list as network_list -class TestCompute(unittest.TestCase): +class TestCompute(testtools.TestCase): """Test the compute examples The purpose of these tests is to ensure the examples run without erring out. """ - @classmethod - def setUpClass(cls): - cls.conn = connect.create_connection_from_config() + def setUp(self): + super(TestCompute, self).setUp() + self.conn = connect.create_connection_from_config() def test_compute(self): compute_list.list_servers(self.conn) diff --git a/openstack/tests/examples/test_identity.py b/openstack/tests/examples/test_identity.py index f55dbe53d..29fc95fdd 100644 --- a/openstack/tests/examples/test_identity.py +++ b/openstack/tests/examples/test_identity.py @@ -10,22 +10,22 @@ # License for the specific language governing permissions and limitations # under the License. -import unittest +import testtools from examples import connect from examples.identity import list as identity_list -class TestIdentity(unittest.TestCase): +class TestIdentity(testtools.TestCase): """Test the identity examples The purpose of these tests is to ensure the examples run without erring out. """ - @classmethod - def setUpClass(cls): - cls.conn = connect.create_connection_from_config() + def setUp(self): + super(TestIdentity, self).setUp() + self.conn = connect.create_connection_from_config() def test_identity(self): identity_list.list_users(self.conn) diff --git a/openstack/tests/examples/test_image.py b/openstack/tests/examples/test_image.py index db027e9b4..0c1ec1054 100644 --- a/openstack/tests/examples/test_image.py +++ b/openstack/tests/examples/test_image.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import unittest +import testtools from examples import connect from examples.image import create as image_create @@ -18,16 +18,16 @@ from examples.image import list as image_list -class TestImage(unittest.TestCase): +class TestImage(testtools.TestCase): """Test the image examples The purpose of these tests is to ensure the examples run without erring out. """ - @classmethod - def setUpClass(cls): - cls.conn = connect.create_connection_from_config() + def setUp(self): + super(TestImage, self).setUp() + self.conn = connect.create_connection_from_config() def test_image(self): image_list.list_images(self.conn) diff --git a/openstack/tests/examples/test_network.py b/openstack/tests/examples/test_network.py index 0e09f39b7..ee8646536 100644 --- a/openstack/tests/examples/test_network.py +++ b/openstack/tests/examples/test_network.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import unittest +import testtools from examples import connect from examples.network import create as network_create @@ -19,16 +19,16 @@ from examples.network import list as network_list -class TestNetwork(unittest.TestCase): +class TestNetwork(testtools.TestCase): """Test the network examples The purpose of these tests is to ensure the examples run without erring out. """ - @classmethod - def setUpClass(cls): - cls.conn = connect.create_connection_from_config() + def setUp(self): + super(TestNetwork, self).setUp() + self.conn = connect.create_connection_from_config() def test_network(self): network_list.list_networks(self.conn) diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index eb968f35d..77c76b35e 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -12,11 +12,10 @@ import os import openstack.config -import time -import unittest from keystoneauth1 import exceptions as _exceptions from openstack import connection +from openstack.tests import base #: Defines the OpenStack Client Config (OCC) cloud key in your OCC config @@ -46,38 +45,34 @@ def _get_resource_value(resource_key, default): FLAVOR_NAME = _get_resource_value('flavor_name', 'm1.small') -def service_exists(**kwargs): - """Decorator function to check whether a service exists - - Usage: - @unittest.skipUnless(base.service_exists(service_type="metering"), - "Metering service does not exist") - class TestMeter(base.BaseFunctionalTest): - ... - - :param kwargs: The kwargs needed to filter an endpoint. - :returns: True if the service exists, otherwise False. - """ - try: - conn = connection.from_config(cloud_name=TEST_CLOUD) - conn.session.get_endpoint(**kwargs) - - return True - except _exceptions.EndpointNotFound: - return False - - -class BaseFunctionalTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.conn = connection.from_config(cloud_name=TEST_CLOUD) - - @classmethod - def assertIs(cls, expected, actual): - if expected != actual: - raise Exception(expected + ' != ' + actual) - - @classmethod - def linger_for_delete(cls): - time.sleep(40) +class BaseFunctionalTest(base.TestCase): + + def setUp(self): + super(BaseFunctionalTest, self).setUp() + self.conn = connection.from_config(cloud_name=TEST_CLOUD) + + def addEmptyCleanup(self, func, *args, **kwargs): + def cleanup(): + result = func(*args, **kwargs) + self.assertIsNone(result) + self.addCleanup(cleanup) + + # TODO(shade) Replace this with call to conn.has_service when we've merged + # the shade methods into Connection. + def require_service(self, service_type, **kwargs): + """Method to check whether a service exists + + Usage: + class TestMeter(base.BaseFunctionalTest): + ... + def setUp(self): + super(TestMeter, self).setUp() + self.require_service('metering') + + :returns: True if the service exists, otherwise False. + """ + try: + self.conn.session.get_endpoint(service_type=service_type, **kwargs) + except _exceptions.EndpointNotFound: + self.skipTest('Service {service_type} not found in cloud'.format( + service_type=service_type)) diff --git a/openstack/tests/functional/block_store/v2/test_snapshot.py b/openstack/tests/functional/block_store/v2/test_snapshot.py index fd4747648..f6b254fa5 100644 --- a/openstack/tests/functional/block_store/v2/test_snapshot.py +++ b/openstack/tests/functional/block_store/v2/test_snapshot.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid from openstack.block_store.v2 import snapshot as _snapshot from openstack.block_store.v2 import volume as _volume @@ -19,49 +18,50 @@ class TestSnapshot(base.BaseFunctionalTest): - SNAPSHOT_NAME = uuid.uuid4().hex - SNAPSHOT_ID = None - VOLUME_NAME = uuid.uuid4().hex - VOLUME_ID = None + def setUp(self): + super(TestSnapshot, self).setUp() - @classmethod - def setUpClass(cls): - super(TestSnapshot, cls).setUpClass() - volume = cls.conn.block_store.create_volume( - name=cls.VOLUME_NAME, + self.SNAPSHOT_NAME = self.getUniqueString() + self.SNAPSHOT_ID = None + self.VOLUME_NAME = self.getUniqueString() + self.VOLUME_ID = None + + volume = self.conn.block_store.create_volume( + name=self.VOLUME_NAME, size=1) - cls.conn.block_store.wait_for_status(volume, - status='available', - failures=['error'], - interval=2, - wait=120) + self.conn.block_store.wait_for_status( + volume, + status='available', + failures=['error'], + interval=2, + wait=120) assert isinstance(volume, _volume.Volume) - cls.assertIs(cls.VOLUME_NAME, volume.name) - cls.VOLUME_ID = volume.id - snapshot = cls.conn.block_store.create_snapshot( - name=cls.SNAPSHOT_NAME, - volume_id=cls.VOLUME_ID) - cls.conn.block_store.wait_for_status(snapshot, - status='available', - failures=['error'], - interval=2, - wait=120) + self.assertEqual(self.VOLUME_NAME, volume.name) + self.VOLUME_ID = volume.id + snapshot = self.conn.block_store.create_snapshot( + name=self.SNAPSHOT_NAME, + volume_id=self.VOLUME_ID) + self.conn.block_store.wait_for_status( + snapshot, + status='available', + failures=['error'], + interval=2, + wait=120) assert isinstance(snapshot, _snapshot.Snapshot) - cls.assertIs(cls.SNAPSHOT_NAME, snapshot.name) - cls.SNAPSHOT_ID = snapshot.id + self.assertEqual(self.SNAPSHOT_NAME, snapshot.name) + self.SNAPSHOT_ID = snapshot.id - @classmethod - def tearDownClass(cls): - snapshot = cls.conn.block_store.get_snapshot(cls.SNAPSHOT_ID) - sot = cls.conn.block_store.delete_snapshot(snapshot, - ignore_missing=False) - cls.conn.block_store.wait_for_delete(snapshot, - interval=2, - wait=120) - cls.assertIs(None, sot) - sot = cls.conn.block_store.delete_volume(cls.VOLUME_ID, - ignore_missing=False) - cls.assertIs(None, sot) + def tearDown(self): + snapshot = self.conn.block_store.get_snapshot(self.SNAPSHOT_ID) + sot = self.conn.block_store.delete_snapshot( + snapshot, ignore_missing=False) + self.conn.block_store.wait_for_delete( + snapshot, interval=2, wait=120) + self.assertIsNone(sot) + sot = self.conn.block_store.delete_volume( + self.VOLUME_ID, ignore_missing=False) + self.assertIsNone(sot) + super(TestSnapshot, self).tearDown() def test_get(self): sot = self.conn.block_store.get_snapshot(self.SNAPSHOT_ID) diff --git a/openstack/tests/functional/block_store/v2/test_type.py b/openstack/tests/functional/block_store/v2/test_type.py index 428389adb..6ae0e93ea 100644 --- a/openstack/tests/functional/block_store/v2/test_type.py +++ b/openstack/tests/functional/block_store/v2/test_type.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid from openstack.block_store.v2 import type as _type from openstack.tests.functional import base @@ -18,22 +17,22 @@ class TestType(base.BaseFunctionalTest): - TYPE_NAME = uuid.uuid4().hex - TYPE_ID = None + def setUp(self): + super(TestType, self).setUp() - @classmethod - def setUpClass(cls): - super(TestType, cls).setUpClass() - sot = cls.conn.block_store.create_type(name=cls.TYPE_NAME) + self.TYPE_NAME = self.getUniqueString() + self.TYPE_ID = None + + sot = self.conn.block_store.create_type(name=self.TYPE_NAME) assert isinstance(sot, _type.Type) - cls.assertIs(cls.TYPE_NAME, sot.name) - cls.TYPE_ID = sot.id - - @classmethod - def tearDownClass(cls): - sot = cls.conn.block_store.delete_type(cls.TYPE_ID, - ignore_missing=False) - cls.assertIs(None, sot) + self.assertEqual(self.TYPE_NAME, sot.name) + self.TYPE_ID = sot.id + + def tearDown(self): + sot = self.conn.block_store.delete_type( + self.TYPE_ID, ignore_missing=False) + self.assertIsNone(sot) + super(TestType, self).tearDown() def test_get(self): sot = self.conn.block_store.get_type(self.TYPE_ID) diff --git a/openstack/tests/functional/block_store/v2/test_volume.py b/openstack/tests/functional/block_store/v2/test_volume.py index c8d70ba52..66b02880d 100644 --- a/openstack/tests/functional/block_store/v2/test_volume.py +++ b/openstack/tests/functional/block_store/v2/test_volume.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid from openstack.block_store.v2 import volume as _volume from openstack.tests.functional import base @@ -18,29 +17,31 @@ class TestVolume(base.BaseFunctionalTest): - VOLUME_NAME = uuid.uuid4().hex - VOLUME_ID = None + def setUp(self): + super(TestVolume, self).setUp() - @classmethod - def setUpClass(cls): - super(TestVolume, cls).setUpClass() - volume = cls.conn.block_store.create_volume( - name=cls.VOLUME_NAME, + self.VOLUME_NAME = self.getUniqueString() + self.VOLUME_ID = None + + volume = self.conn.block_store.create_volume( + name=self.VOLUME_NAME, size=1) - cls.conn.block_store.wait_for_status(volume, - status='available', - failures=['error'], - interval=2, - wait=120) + self.conn.block_store.wait_for_status( + volume, + status='available', + failures=['error'], + interval=2, + wait=120) assert isinstance(volume, _volume.Volume) - cls.assertIs(cls.VOLUME_NAME, volume.name) - cls.VOLUME_ID = volume.id - - @classmethod - def tearDownClass(cls): - sot = cls.conn.block_store.delete_volume(cls.VOLUME_ID, - ignore_missing=False) - cls.assertIs(None, sot) + self.assertEqual(self.VOLUME_NAME, volume.name) + self.VOLUME_ID = volume.id + + def tearDown(self): + sot = self.conn.block_store.delete_volume( + self.VOLUME_ID, + ignore_missing=False) + self.assertIsNone(sot) + super(TestVolume, self).tearDown() def test_get(self): sot = self.conn.block_store.get_volume(self.VOLUME_ID) diff --git a/openstack/tests/functional/compute/v2/test_flavor.py b/openstack/tests/functional/compute/v2/test_flavor.py index ec3f0bed5..8785ae1b4 100644 --- a/openstack/tests/functional/compute/v2/test_flavor.py +++ b/openstack/tests/functional/compute/v2/test_flavor.py @@ -18,11 +18,10 @@ class TestFlavor(base.BaseFunctionalTest): - @classmethod - def setUpClass(cls): - super(TestFlavor, cls).setUpClass() + def setUp(self): + super(TestFlavor, self).setUp() - cls.one_flavor = list(cls.conn.compute.flavors())[0] + self.one_flavor = list(self.conn.compute.flavors())[0] def test_flavors(self): flavors = list(self.conn.compute.flavors()) diff --git a/openstack/tests/functional/compute/v2/test_keypair.py b/openstack/tests/functional/compute/v2/test_keypair.py index 3b2f2f63e..1909ae42d 100644 --- a/openstack/tests/functional/compute/v2/test_keypair.py +++ b/openstack/tests/functional/compute/v2/test_keypair.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid from openstack.compute.v2 import keypair from openstack.tests.functional import base @@ -18,22 +17,23 @@ class TestKeypair(base.BaseFunctionalTest): - NAME = uuid.uuid4().hex - ID = None + def setUp(self): + super(TestKeypair, self).setUp() - @classmethod - def setUpClass(cls): - super(TestKeypair, cls).setUpClass() - sot = cls.conn.compute.create_keypair(name=cls.NAME) + # Keypairs can't have .'s in the name. Because why? + self.NAME = self.getUniqueString().split('.')[-1] + self.ID = None + + sot = self.conn.compute.create_keypair(name=self.NAME) assert isinstance(sot, keypair.Keypair) - cls.assertIs(cls.NAME, sot.name) - cls._keypair = sot - cls.ID = sot.id - - @classmethod - def tearDownClass(cls): - sot = cls.conn.compute.delete_keypair(cls._keypair) - cls.assertIs(None, sot) + self.assertEqual(self.NAME, sot.name) + self._keypair = sot + self.ID = sot.id + + def tearDown(self): + sot = self.conn.compute.delete_keypair(self._keypair) + self.assertIsNone(sot) + super(TestKeypair, self).tearDown() def test_find(self): sot = self.conn.compute.find_keypair(self.NAME) diff --git a/openstack/tests/functional/compute/v2/test_server.py b/openstack/tests/functional/compute/v2/test_server.py index 795e8180d..65883bbf7 100644 --- a/openstack/tests/functional/compute/v2/test_server.py +++ b/openstack/tests/functional/compute/v2/test_server.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid +import time from openstack.compute.v2 import server from openstack.tests.functional import base @@ -19,44 +19,41 @@ class TestServer(base.BaseFunctionalTest): - NAME = uuid.uuid4().hex - server = None - network = None - subnet = None - cidr = '10.99.99.0/16' - - @classmethod - def setUpClass(cls): - super(TestServer, cls).setUpClass() - flavor = cls.conn.compute.find_flavor(base.FLAVOR_NAME, - ignore_missing=False) - image = cls.conn.compute.find_image(base.IMAGE_NAME, - ignore_missing=False) - cls.network, cls.subnet = test_network.create_network(cls.conn, - cls.NAME, - cls.cidr) - if not cls.network: - # We can't call TestCase.fail from within the setUpClass - # classmethod, but we need to raise some exception in order - # to get this setup to fail and thusly fail the entire class. - raise Exception("Unable to create network for TestServer") - - sot = cls.conn.compute.create_server( - name=cls.NAME, flavor_id=flavor.id, image_id=image.id, - networks=[{"uuid": cls.network.id}]) - cls.conn.compute.wait_for_server(sot) + def setUp(self): + super(TestServer, self).setUp() + self.NAME = self.getUniqueString() + self.server = None + self.network = None + self.subnet = None + self.cidr = '10.99.99.0/16' + + flavor = self.conn.compute.find_flavor(base.FLAVOR_NAME, + ignore_missing=False) + image = self.conn.compute.find_image(base.IMAGE_NAME, + ignore_missing=False) + self.network, self.subnet = test_network.create_network( + self.conn, + self.NAME, + self.cidr) + self.assertIsNotNone(self.network) + + sot = self.conn.compute.create_server( + name=self.NAME, flavor_id=flavor.id, image_id=image.id, + networks=[{"uuid": self.network.id}]) + self.conn.compute.wait_for_server(sot) assert isinstance(sot, server.Server) - cls.assertIs(cls.NAME, sot.name) - cls.server = sot + self.assertEqual(self.NAME, sot.name) + self.server = sot - @classmethod - def tearDownClass(cls): - sot = cls.conn.compute.delete_server(cls.server.id) - cls.assertIs(None, sot) + def tearDown(self): + sot = self.conn.compute.delete_server(self.server.id) + self.assertIsNone(sot) # Need to wait for the stack to go away before network delete - cls.conn.compute.wait_for_delete(cls.server) - cls.linger_for_delete() - test_network.delete_network(cls.conn, cls.network, cls.subnet) + self.conn.compute.wait_for_delete(self.server) + # TODO(shade) sleeping in tests is bad mmkay? + time.sleep(40) + test_network.delete_network(self.conn, self.network, self.subnet) + super(TestServer, self).tearDown() def test_find(self): sot = self.conn.compute.find_server(self.NAME) diff --git a/openstack/tests/functional/image/v2/test_image.py b/openstack/tests/functional/image/v2/test_image.py index 3008097a5..54e4f6662 100644 --- a/openstack/tests/functional/image/v2/test_image.py +++ b/openstack/tests/functional/image/v2/test_image.py @@ -22,23 +22,20 @@ class ImageOpts(object): def __init__(self): self.image_api_version = '2' - @classmethod - def setUpClass(cls): - opts = cls.ImageOpts() - cls.conn = connection.from_config(cloud_name=base.TEST_CLOUD, - options=opts) + def setUp(self): + super(TestImage, self).setUp() + opts = self.ImageOpts() + self.conn = connection.from_config( + cloud_name=base.TEST_CLOUD, options=opts) - cls.img = cls.conn.image.upload_image( + self.img = self.conn.image.upload_image( name=TEST_IMAGE_NAME, disk_format='raw', container_format='bare', properties='{"description": "This is not an image"}', data=open('CONTRIBUTING.rst', 'r') ) - - @classmethod - def tearDownClass(cls): - cls.conn.image.delete_image(cls.img) + self.addCleanup(self.conn.image.delete_image, self.img) def test_get_image(self): img2 = self.conn.image.get_image(self.img) diff --git a/openstack/tests/functional/load_balancer/base.py b/openstack/tests/functional/load_balancer/base.py index 09f96f093..975cd19cc 100644 --- a/openstack/tests/functional/load_balancer/base.py +++ b/openstack/tests/functional/load_balancer/base.py @@ -19,12 +19,7 @@ class BaseLBFunctionalTest(base.BaseFunctionalTest): - @classmethod - def setUpClass(cls): - super(BaseLBFunctionalTest, cls).setUpClass() - - @classmethod - def lb_wait_for_status(cls, lb, status, failures, interval=1, wait=120): + def lb_wait_for_status(self, lb, status, failures, interval=1, wait=120): """Wait for load balancer to be in a particular provisioning status. :param lb: The load balancer to wait on to reach the status. @@ -49,7 +44,7 @@ def lb_wait_for_status(cls, lb, status, failures, interval=1, wait=120): failures = [] while total_sleep < wait: - lb = cls.conn.load_balancer.get_load_balancer(lb.id) + lb = self.conn.load_balancer.get_load_balancer(lb.id) if lb.provisioning_status == status: return None if lb.provisioning_status in failures: diff --git a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py index 1bf8b6013..a78229e8d 100644 --- a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py +++ b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py @@ -10,9 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import unittest -import uuid - from openstack.load_balancer.v2 import health_monitor from openstack.load_balancer.v2 import l7_policy from openstack.load_balancer.v2 import l7_rule @@ -20,21 +17,11 @@ from openstack.load_balancer.v2 import load_balancer from openstack.load_balancer.v2 import member from openstack.load_balancer.v2 import pool -from openstack.tests.functional import base from openstack.tests.functional.load_balancer import base as lb_base -@unittest.skipUnless(base.service_exists(service_type='load-balancer'), - 'Load-balancer service does not exist') class TestLoadBalancer(lb_base.BaseLBFunctionalTest): - HM_NAME = uuid.uuid4().hex - L7POLICY_NAME = uuid.uuid4().hex - LB_NAME = uuid.uuid4().hex - LISTENER_NAME = uuid.uuid4().hex - MEMBER_NAME = uuid.uuid4().hex - POOL_NAME = uuid.uuid4().hex - UPDATE_NAME = uuid.uuid4().hex HM_ID = None L7POLICY_ID = None LB_ID = None @@ -58,110 +45,121 @@ class TestLoadBalancer(lb_base.BaseLBFunctionalTest): L7RULE_TYPE = 'HOST_NAME' L7RULE_VALUE = 'example' - # Note: Creating load balancers can be slow on some hosts due to nova - # instance boot times (up to ten minutes) so we are consolidating - # all of our functional tests here to reduce test runtime. - @classmethod - def setUpClass(cls): - super(TestLoadBalancer, cls).setUpClass() - subnets = list(cls.conn.network.subnets()) - cls.VIP_SUBNET_ID = subnets[0].id - cls.PROJECT_ID = cls.conn.session.get_project_id() - test_lb = cls.conn.load_balancer.create_load_balancer( - name=cls.LB_NAME, vip_subnet_id=cls.VIP_SUBNET_ID, - project_id=cls.PROJECT_ID) + # TODO(shade): Creating load balancers can be slow on some hosts due to + # nova instance boot times (up to ten minutes). This used to + # use setUpClass, but that's a whole other pile of bad, so + # we may need to engineer something pleasing here. + def setUp(self): + super(TestLoadBalancer, self).setUp() + self.require_service('load-balancer') + + self.HM_NAME = self.getUniqueString() + self.L7POLICY_NAME = self.getUniqueString() + self.LB_NAME = self.getUniqueString() + self.LISTENER_NAME = self.getUniqueString() + self.MEMBER_NAME = self.getUniqueString() + self.POOL_NAME = self.getUniqueString() + self.UPDATE_NAME = self.getUniqueString() + subnets = list(self.conn.network.subnets()) + self.VIP_SUBNET_ID = subnets[0].id + self.PROJECT_ID = self.conn.session.get_project_id() + test_lb = self.conn.load_balancer.create_load_balancer( + name=self.LB_NAME, vip_subnet_id=self.VIP_SUBNET_ID, + project_id=self.PROJECT_ID) assert isinstance(test_lb, load_balancer.LoadBalancer) - cls.assertIs(cls.LB_NAME, test_lb.name) + self.assertEqual(self.LB_NAME, test_lb.name) # Wait for the LB to go ACTIVE. On non-virtualization enabled hosts # it can take nova up to ten minutes to boot a VM. - cls.lb_wait_for_status(test_lb, status='ACTIVE', - failures=['ERROR'], interval=1, wait=600) - cls.LB_ID = test_lb.id + self.lb_wait_for_status(test_lb, status='ACTIVE', + failures=['ERROR'], interval=1, wait=600) + self.LB_ID = test_lb.id - test_listener = cls.conn.load_balancer.create_listener( - name=cls.LISTENER_NAME, protocol=cls.PROTOCOL, - protocol_port=cls.PROTOCOL_PORT, loadbalancer_id=cls.LB_ID) + test_listener = self.conn.load_balancer.create_listener( + name=self.LISTENER_NAME, protocol=self.PROTOCOL, + protocol_port=self.PROTOCOL_PORT, loadbalancer_id=self.LB_ID) assert isinstance(test_listener, listener.Listener) - cls.assertIs(cls.LISTENER_NAME, test_listener.name) - cls.LISTENER_ID = test_listener.id - cls.lb_wait_for_status(test_lb, status='ACTIVE', - failures=['ERROR']) - - test_pool = cls.conn.load_balancer.create_pool( - name=cls.POOL_NAME, protocol=cls.PROTOCOL, - lb_algorithm=cls.LB_ALGORITHM, listener_id=cls.LISTENER_ID) + self.assertEqual(self.LISTENER_NAME, test_listener.name) + self.LISTENER_ID = test_listener.id + self.lb_wait_for_status(test_lb, status='ACTIVE', + failures=['ERROR']) + + test_pool = self.conn.load_balancer.create_pool( + name=self.POOL_NAME, protocol=self.PROTOCOL, + lb_algorithm=self.LB_ALGORITHM, listener_id=self.LISTENER_ID) assert isinstance(test_pool, pool.Pool) - cls.assertIs(cls.POOL_NAME, test_pool.name) - cls.POOL_ID = test_pool.id - cls.lb_wait_for_status(test_lb, status='ACTIVE', - failures=['ERROR']) - - test_member = cls.conn.load_balancer.create_member( - pool=cls.POOL_ID, name=cls.MEMBER_NAME, address=cls.MEMBER_ADDRESS, - protocol_port=cls.PROTOCOL_PORT, weight=cls.WEIGHT) + self.assertEqual(self.POOL_NAME, test_pool.name) + self.POOL_ID = test_pool.id + self.lb_wait_for_status(test_lb, status='ACTIVE', + failures=['ERROR']) + + test_member = self.conn.load_balancer.create_member( + pool=self.POOL_ID, name=self.MEMBER_NAME, + address=self.MEMBER_ADDRESS, + protocol_port=self.PROTOCOL_PORT, weight=self.WEIGHT) assert isinstance(test_member, member.Member) - cls.assertIs(cls.MEMBER_NAME, test_member.name) - cls.MEMBER_ID = test_member.id - cls.lb_wait_for_status(test_lb, status='ACTIVE', - failures=['ERROR']) - - test_hm = cls.conn.load_balancer.create_health_monitor( - pool_id=cls.POOL_ID, name=cls.HM_NAME, delay=cls.DELAY, - timeout=cls.TIMEOUT, max_retries=cls.MAX_RETRY, type=cls.HM_TYPE) + self.assertEqual(self.MEMBER_NAME, test_member.name) + self.MEMBER_ID = test_member.id + self.lb_wait_for_status(test_lb, status='ACTIVE', + failures=['ERROR']) + + test_hm = self.conn.load_balancer.create_health_monitor( + pool_id=self.POOL_ID, name=self.HM_NAME, delay=self.DELAY, + timeout=self.TIMEOUT, max_retries=self.MAX_RETRY, + type=self.HM_TYPE) assert isinstance(test_hm, health_monitor.HealthMonitor) - cls.assertIs(cls.HM_NAME, test_hm.name) - cls.HM_ID = test_hm.id - cls.lb_wait_for_status(test_lb, status='ACTIVE', - failures=['ERROR']) - - test_l7policy = cls.conn.load_balancer.create_l7_policy( - listener_id=cls.LISTENER_ID, name=cls.L7POLICY_NAME, - action=cls.ACTION, redirect_url=cls.REDIRECT_URL) + self.assertEqual(self.HM_NAME, test_hm.name) + self.HM_ID = test_hm.id + self.lb_wait_for_status(test_lb, status='ACTIVE', + failures=['ERROR']) + + test_l7policy = self.conn.load_balancer.create_l7_policy( + listener_id=self.LISTENER_ID, name=self.L7POLICY_NAME, + action=self.ACTION, redirect_url=self.REDIRECT_URL) assert isinstance(test_l7policy, l7_policy.L7Policy) - cls.assertIs(cls.L7POLICY_NAME, test_l7policy.name) - cls.L7POLICY_ID = test_l7policy.id - cls.lb_wait_for_status(test_lb, status='ACTIVE', - failures=['ERROR']) - - test_l7rule = cls.conn.load_balancer.create_l7_rule( - l7_policy=cls.L7POLICY_ID, compare_type=cls.COMPARE_TYPE, - type=cls.L7RULE_TYPE, value=cls.L7RULE_VALUE) + self.assertEqual(self.L7POLICY_NAME, test_l7policy.name) + self.L7POLICY_ID = test_l7policy.id + self.lb_wait_for_status(test_lb, status='ACTIVE', + failures=['ERROR']) + + test_l7rule = self.conn.load_balancer.create_l7_rule( + l7_policy=self.L7POLICY_ID, compare_type=self.COMPARE_TYPE, + type=self.L7RULE_TYPE, value=self.L7RULE_VALUE) assert isinstance(test_l7rule, l7_rule.L7Rule) - cls.assertIs(cls.COMPARE_TYPE, test_l7rule.compare_type) - cls.L7RULE_ID = test_l7rule.id - cls.lb_wait_for_status(test_lb, status='ACTIVE', - failures=['ERROR']) + self.assertEqual(self.COMPARE_TYPE, test_l7rule.compare_type) + self.L7RULE_ID = test_l7rule.id + self.lb_wait_for_status(test_lb, status='ACTIVE', + failures=['ERROR']) - @classmethod - def tearDownClass(cls): - test_lb = cls.conn.load_balancer.get_load_balancer(cls.LB_ID) - cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + def tearDown(self): + test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) + self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) - cls.conn.load_balancer.delete_l7_rule( - cls.L7RULE_ID, l7_policy=cls.L7POLICY_ID, ignore_missing=False) - cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + self.conn.load_balancer.delete_l7_rule( + self.L7RULE_ID, l7_policy=self.L7POLICY_ID, ignore_missing=False) + self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) - cls.conn.load_balancer.delete_l7_policy( - cls.L7POLICY_ID, ignore_missing=False) - cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + self.conn.load_balancer.delete_l7_policy( + self.L7POLICY_ID, ignore_missing=False) + self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) - cls.conn.load_balancer.delete_health_monitor( - cls.HM_ID, ignore_missing=False) - cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + self.conn.load_balancer.delete_health_monitor( + self.HM_ID, ignore_missing=False) + self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) - cls.conn.load_balancer.delete_member( - cls.MEMBER_ID, cls.POOL_ID, ignore_missing=False) - cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + self.conn.load_balancer.delete_member( + self.MEMBER_ID, self.POOL_ID, ignore_missing=False) + self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) - cls.conn.load_balancer.delete_pool(cls.POOL_ID, ignore_missing=False) - cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + self.conn.load_balancer.delete_pool(self.POOL_ID, ignore_missing=False) + self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) - cls.conn.load_balancer.delete_listener(cls.LISTENER_ID, - ignore_missing=False) - cls.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + self.conn.load_balancer.delete_listener(self.LISTENER_ID, + ignore_missing=False) + self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) - cls.conn.load_balancer.delete_load_balancer( - cls.LB_ID, ignore_missing=False) + self.conn.load_balancer.delete_load_balancer( + self.LB_ID, ignore_missing=False) + super(TestLoadBalancer, self).tearDown() def test_lb_find(self): test_lb = self.conn.load_balancer.find_load_balancer(self.LB_NAME) diff --git a/openstack/tests/functional/network/v2/test_address_scope.py b/openstack/tests/functional/network/v2/test_address_scope.py index 223f15732..c67a5caec 100644 --- a/openstack/tests/functional/network/v2/test_address_scope.py +++ b/openstack/tests/functional/network/v2/test_address_scope.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid from openstack.network.v2 import address_scope as _address_scope from openstack.tests.functional import base @@ -19,27 +18,26 @@ class TestAddressScope(base.BaseFunctionalTest): ADDRESS_SCOPE_ID = None - ADDRESS_SCOPE_NAME = uuid.uuid4().hex - ADDRESS_SCOPE_NAME_UPDATED = uuid.uuid4().hex IS_SHARED = False IP_VERSION = 4 - @classmethod - def setUpClass(cls): - super(TestAddressScope, cls).setUpClass() - address_scope = cls.conn.network.create_address_scope( - ip_version=cls.IP_VERSION, - name=cls.ADDRESS_SCOPE_NAME, - shared=cls.IS_SHARED, + def setUp(self): + super(TestAddressScope, self).setUp() + self.ADDRESS_SCOPE_NAME = self.getUniqueString() + self.ADDRESS_SCOPE_NAME_UPDATED = self.getUniqueString() + address_scope = self.conn.network.create_address_scope( + ip_version=self.IP_VERSION, + name=self.ADDRESS_SCOPE_NAME, + shared=self.IS_SHARED, ) assert isinstance(address_scope, _address_scope.AddressScope) - cls.assertIs(cls.ADDRESS_SCOPE_NAME, address_scope.name) - cls.ADDRESS_SCOPE_ID = address_scope.id + self.assertEqual(self.ADDRESS_SCOPE_NAME, address_scope.name) + self.ADDRESS_SCOPE_ID = address_scope.id - @classmethod - def tearDownClass(cls): - sot = cls.conn.network.delete_address_scope(cls.ADDRESS_SCOPE_ID) - cls.assertIs(None, sot) + def tearDown(self): + sot = self.conn.network.delete_address_scope(self.ADDRESS_SCOPE_ID) + self.assertIsNone(sot) + super(TestAddressScope, self).tearDown() def test_find(self): sot = self.conn.network.find_address_scope(self.ADDRESS_SCOPE_NAME) diff --git a/openstack/tests/functional/network/v2/test_agent.py b/openstack/tests/functional/network/v2/test_agent.py index 2cc96525b..da1d3ce91 100644 --- a/openstack/tests/functional/network/v2/test_agent.py +++ b/openstack/tests/functional/network/v2/test_agent.py @@ -28,12 +28,11 @@ def validate_uuid(self, s): return False return True - @classmethod - def setUpClass(cls): - super(TestAgent, cls).setUpClass() - agent_list = list(cls.conn.network.agents()) - cls.AGENT = agent_list[0] - assert isinstance(cls.AGENT, agent.Agent) + def setUp(self): + super(TestAgent, self).setUp() + agent_list = list(self.conn.network.agents()) + self.AGENT = agent_list[0] + assert isinstance(self.AGENT, agent.Agent) def test_list(self): agent_list = list(self.conn.network.agents()) diff --git a/openstack/tests/functional/network/v2/test_agent_add_remove_network.py b/openstack/tests/functional/network/v2/test_agent_add_remove_network.py index cae3dddb6..e1ee63d93 100644 --- a/openstack/tests/functional/network/v2/test_agent_add_remove_network.py +++ b/openstack/tests/functional/network/v2/test_agent_add_remove_network.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid from openstack.network.v2 import network from openstack.tests.functional import base @@ -18,34 +17,29 @@ class TestAgentNetworks(base.BaseFunctionalTest): - NETWORK_NAME = 'network-' + uuid.uuid4().hex NETWORK_ID = None AGENT = None AGENT_ID = None - @classmethod - def setUpClass(cls): - super(TestAgentNetworks, cls).setUpClass() + def setUp(self): + super(TestAgentNetworks, self).setUp() - net = cls.conn.network.create_network(name=cls.NETWORK_NAME) + self.NETWORK_NAME = self.getUniqueString('network') + net = self.conn.network.create_network(name=self.NETWORK_NAME) + self.addCleanup(self.conn.network.delete_network, net.id) assert isinstance(net, network.Network) - cls.NETWORK_ID = net.id - agent_list = list(cls.conn.network.agents()) + self.NETWORK_ID = net.id + agent_list = list(self.conn.network.agents()) agents = [agent for agent in agent_list if agent.agent_type == 'DHCP agent'] - cls.AGENT = agents[0] - cls.AGENT_ID = cls.AGENT.id + self.AGENT = agents[0] + self.AGENT_ID = self.AGENT.id - @classmethod - def tearDownClass(cls): - cls.conn.network.delete_network(cls.NETWORK_ID) - - def test_add_agent_to_network(self): + def test_add_remove_agent(self): net = self.AGENT.add_agent_to_network(self.conn.session, network_id=self.NETWORK_ID) self._verify_add(net) - def test_remove_agent_from_network(self): net = self.AGENT.remove_agent_from_network(self.conn.session, network_id=self.NETWORK_ID) self._verify_remove(net) diff --git a/openstack/tests/functional/network/v2/test_agent_add_remove_router.py b/openstack/tests/functional/network/v2/test_agent_add_remove_router.py index ee3b43467..f22eab354 100644 --- a/openstack/tests/functional/network/v2/test_agent_add_remove_router.py +++ b/openstack/tests/functional/network/v2/test_agent_add_remove_router.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid from openstack.network.v2 import router from openstack.tests.functional import base @@ -18,24 +17,20 @@ class TestAgentRouters(base.BaseFunctionalTest): - ROUTER_NAME = 'router-name-' + uuid.uuid4().hex ROUTER = None AGENT = None - @classmethod - def setUpClass(cls): - super(TestAgentRouters, cls).setUpClass() + def setUp(self): + super(TestAgentRouters, self).setUp() - cls.ROUTER = cls.conn.network.create_router(name=cls.ROUTER_NAME) - assert isinstance(cls.ROUTER, router.Router) - agent_list = list(cls.conn.network.agents()) + self.ROUTER_NAME = 'router-name-' + self.getUniqueString('router-name') + self.ROUTER = self.conn.network.create_router(name=self.ROUTER_NAME) + self.addCleanup(self.conn.network.delete_router, self.ROUTER) + assert isinstance(self.ROUTER, router.Router) + agent_list = list(self.conn.network.agents()) agents = [agent for agent in agent_list if agent.agent_type == 'L3 agent'] - cls.AGENT = agents[0] - - @classmethod - def tearDownClass(cls): - cls.conn.network.delete_router(cls.ROUTER) + self.AGENT = agents[0] def test_add_router_to_agent(self): self.conn.network.add_router_to_agent(self.AGENT, self.ROUTER) diff --git a/openstack/tests/functional/network/v2/test_auto_allocated_topology.py b/openstack/tests/functional/network/v2/test_auto_allocated_topology.py index 629957b80..a2850e2c4 100644 --- a/openstack/tests/functional/network/v2/test_auto_allocated_topology.py +++ b/openstack/tests/functional/network/v2/test_auto_allocated_topology.py @@ -19,16 +19,15 @@ class TestAutoAllocatedTopology(base.BaseFunctionalTest): NETWORK_ID = None PROJECT_ID = None - @classmethod - def setUpClass(cls): - super(TestAutoAllocatedTopology, cls).setUpClass() - projects = [o.project_id for o in cls.conn.network.networks()] - cls.PROJECT_ID = projects[0] - - @classmethod - def tearDownClass(cls): - res = cls.conn.network.delete_auto_allocated_topology(cls.PROJECT_ID) - cls.assertIs(None, res) + def setUp(self): + super(TestAutoAllocatedTopology, self).setUp() + projects = [o.project_id for o in self.conn.network.networks()] + self.PROJECT_ID = projects[0] + + def tearDown(self): + res = self.conn.network.delete_auto_allocated_topology(self.PROJECT_ID) + self.assertIsNone(res) + super(TestAutoAllocatedTopology, self).tearDown() def test_dry_run_option_pass(self): # Dry run will only pass if there is a public network diff --git a/openstack/tests/functional/network/v2/test_dvr_router.py b/openstack/tests/functional/network/v2/test_dvr_router.py index e93231f10..20b479c50 100644 --- a/openstack/tests/functional/network/v2/test_dvr_router.py +++ b/openstack/tests/functional/network/v2/test_dvr_router.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid from openstack.network.v2 import router from openstack.tests.functional import base @@ -18,22 +17,21 @@ class TestDVRRouter(base.BaseFunctionalTest): - NAME = uuid.uuid4().hex - UPDATE_NAME = uuid.uuid4().hex ID = None - @classmethod - def setUpClass(cls): - super(TestDVRRouter, cls).setUpClass() - sot = cls.conn.network.create_router(name=cls.NAME, distributed=True) + def setUp(self): + super(TestDVRRouter, self).setUp() + self.NAME = self.getUniqueString() + self.UPDATE_NAME = self.getUniqueString() + sot = self.conn.network.create_router(name=self.NAME, distributed=True) assert isinstance(sot, router.Router) - cls.assertIs(cls.NAME, sot.name) - cls.ID = sot.id + self.assertEqual(self.NAME, sot.name) + self.ID = sot.id - @classmethod - def tearDownClass(cls): - sot = cls.conn.network.delete_router(cls.ID, ignore_missing=False) - cls.assertIs(None, sot) + def tearDown(self): + sot = self.conn.network.delete_router(self.ID, ignore_missing=False) + self.assertIsNone(sot) + super(TestDVRRouter, self).tearDown() def test_find(self): sot = self.conn.network.find_router(self.NAME) diff --git a/openstack/tests/functional/network/v2/test_flavor.py b/openstack/tests/functional/network/v2/test_flavor.py index ad486e9e0..24d0f126f 100644 --- a/openstack/tests/functional/network/v2/test_flavor.py +++ b/openstack/tests/functional/network/v2/test_flavor.py @@ -10,15 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid - from openstack.network.v2 import flavor from openstack.tests.functional import base class TestFlavor(base.BaseFunctionalTest): - FLAVOR_NAME = uuid.uuid4().hex UPDATE_NAME = "UPDATED-NAME" SERVICE_TYPE = "FLAVORS" ID = None @@ -26,29 +23,30 @@ class TestFlavor(base.BaseFunctionalTest): SERVICE_PROFILE_DESCRIPTION = "DESCRIPTION" METAINFO = "FlAVOR_PROFILE_METAINFO" - @classmethod - def setUpClass(cls): - super(TestFlavor, cls).setUpClass() - flavors = cls.conn.network.create_flavor(name=cls.FLAVOR_NAME, - service_type=cls.SERVICE_TYPE) + def setUp(self): + super(TestFlavor, self).setUp() + self.FLAVOR_NAME = self.getUniqueString('flavor') + flavors = self.conn.network.create_flavor( + name=self.FLAVOR_NAME, + service_type=self.SERVICE_TYPE) assert isinstance(flavors, flavor.Flavor) - cls.assertIs(cls.FLAVOR_NAME, flavors.name) - cls.assertIs(cls.SERVICE_TYPE, flavors.service_type) + self.assertEqual(self.FLAVOR_NAME, flavors.name) + self.assertEqual(self.SERVICE_TYPE, flavors.service_type) - cls.ID = flavors.id + self.ID = flavors.id - cls.service_profiles = cls.conn.network.create_service_profile( - description=cls.SERVICE_PROFILE_DESCRIPTION, - metainfo=cls.METAINFO,) + self.service_profiles = self.conn.network.create_service_profile( + description=self.SERVICE_PROFILE_DESCRIPTION, + metainfo=self.METAINFO,) - @classmethod - def tearDownClass(cls): - flavors = cls.conn.network.delete_flavor(cls.ID, ignore_missing=True) - cls.assertIs(None, flavors) + def tearDown(self): + flavors = self.conn.network.delete_flavor(self.ID, ignore_missing=True) + self.assertIsNone(flavors) - service_profiles = cls.conn.network.delete_service_profile( - cls.ID, ignore_missing=True) - cls.assertIs(None, service_profiles) + service_profiles = self.conn.network.delete_service_profile( + self.ID, ignore_missing=True) + self.assertIsNone(service_profiles) + super(TestFlavor, self).tearDown() def test_find(self): flavors = self.conn.network.find_flavor(self.FLAVOR_NAME) @@ -68,13 +66,12 @@ def test_update(self): name=self.UPDATE_NAME) self.assertEqual(self.UPDATE_NAME, flavor.name) - def test_associate_flavor_with_service_profile(self): + def test_associate_disassociate_flavor_with_service_profile(self): response = \ self.conn.network.associate_flavor_with_service_profile( self.ID, self.service_profiles.id) self.assertIsNotNone(response) - def test_disassociate_flavor_from_service_profile(self): response = \ self.conn.network.disassociate_flavor_from_service_profile( self.ID, self.service_profiles.id) diff --git a/openstack/tests/functional/network/v2/test_floating_ip.py b/openstack/tests/functional/network/v2/test_floating_ip.py index b2274ce56..bf8405309 100644 --- a/openstack/tests/functional/network/v2/test_floating_ip.py +++ b/openstack/tests/functional/network/v2/test_floating_ip.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid from openstack.network.v2 import floating_ip from openstack.network.v2 import network @@ -22,11 +21,6 @@ class TestFloatingIP(base.BaseFunctionalTest): - ROT_NAME = uuid.uuid4().hex - EXT_NET_NAME = uuid.uuid4().hex - EXT_SUB_NAME = uuid.uuid4().hex - INT_NET_NAME = uuid.uuid4().hex - INT_SUB_NAME = uuid.uuid4().hex IPV4 = 4 EXT_CIDR = "10.100.0.0/24" INT_CIDR = "10.101.0.0/24" @@ -38,84 +32,89 @@ class TestFloatingIP(base.BaseFunctionalTest): PORT_ID = None FIP = None - @classmethod - def setUpClass(cls): - super(TestFloatingIP, cls).setUpClass() + def setUp(self): + super(TestFloatingIP, self).setUp() + self.ROT_NAME = self.getUniqueString() + self.EXT_NET_NAME = self.getUniqueString() + self.EXT_SUB_NAME = self.getUniqueString() + self.INT_NET_NAME = self.getUniqueString() + self.INT_SUB_NAME = self.getUniqueString() # Create Exeternal Network args = {'router:external': True} - net = cls._create_network(cls.EXT_NET_NAME, **args) - cls.EXT_NET_ID = net.id - sub = cls._create_subnet(cls.EXT_SUB_NAME, cls.EXT_NET_ID, - cls.EXT_CIDR) - cls.EXT_SUB_ID = sub.id + net = self._create_network(self.EXT_NET_NAME, **args) + self.EXT_NET_ID = net.id + sub = self._create_subnet( + self.EXT_SUB_NAME, self.EXT_NET_ID, self.EXT_CIDR) + self.EXT_SUB_ID = sub.id # Create Internal Network - net = cls._create_network(cls.INT_NET_NAME) - cls.INT_NET_ID = net.id - sub = cls._create_subnet(cls.INT_SUB_NAME, cls.INT_NET_ID, - cls.INT_CIDR) - cls.INT_SUB_ID = sub.id + net = self._create_network(self.INT_NET_NAME) + self.INT_NET_ID = net.id + sub = self._create_subnet( + self.INT_SUB_NAME, self.INT_NET_ID, self.INT_CIDR) + self.INT_SUB_ID = sub.id # Create Router - args = {'external_gateway_info': {'network_id': cls.EXT_NET_ID}} - sot = cls.conn.network.create_router(name=cls.ROT_NAME, **args) + args = {'external_gateway_info': {'network_id': self.EXT_NET_ID}} + sot = self.conn.network.create_router(name=self.ROT_NAME, **args) assert isinstance(sot, router.Router) - cls.assertIs(cls.ROT_NAME, sot.name) - cls.ROT_ID = sot.id - cls.ROT = sot + self.assertEqual(self.ROT_NAME, sot.name) + self.ROT_ID = sot.id + self.ROT = sot # Add Router's Interface to Internal Network - sot = cls.ROT.add_interface(cls.conn.session, subnet_id=cls.INT_SUB_ID) - cls.assertIs(sot['subnet_id'], cls.INT_SUB_ID) + sot = self.ROT.add_interface( + self.conn.session, subnet_id=self.INT_SUB_ID) + self.assertEqual(sot['subnet_id'], self.INT_SUB_ID) # Create Port in Internal Network - prt = cls.conn.network.create_port(network_id=cls.INT_NET_ID) + prt = self.conn.network.create_port(network_id=self.INT_NET_ID) assert isinstance(prt, port.Port) - cls.PORT_ID = prt.id + self.PORT_ID = prt.id # Create Floating IP. - fip = cls.conn.network.create_ip(floating_network_id=cls.EXT_NET_ID) + fip = self.conn.network.create_ip(floating_network_id=self.EXT_NET_ID) assert isinstance(fip, floating_ip.FloatingIP) - cls.FIP = fip + self.FIP = fip - @classmethod - def tearDownClass(cls): - sot = cls.conn.network.delete_ip(cls.FIP.id, ignore_missing=False) - cls.assertIs(None, sot) - sot = cls.conn.network.delete_port(cls.PORT_ID, ignore_missing=False) - cls.assertIs(None, sot) - sot = cls.ROT.remove_interface(cls.conn.session, - subnet_id=cls.INT_SUB_ID) - cls.assertIs(sot['subnet_id'], cls.INT_SUB_ID) - sot = cls.conn.network.delete_router(cls.ROT_ID, ignore_missing=False) - cls.assertIs(None, sot) - sot = cls.conn.network.delete_subnet(cls.EXT_SUB_ID, - ignore_missing=False) - cls.assertIs(None, sot) - sot = cls.conn.network.delete_network(cls.EXT_NET_ID, - ignore_missing=False) - cls.assertIs(None, sot) - sot = cls.conn.network.delete_subnet(cls.INT_SUB_ID, - ignore_missing=False) - cls.assertIs(None, sot) - sot = cls.conn.network.delete_network(cls.INT_NET_ID, - ignore_missing=False) - cls.assertIs(None, sot) + def tearDown(self): + sot = self.conn.network.delete_ip(self.FIP.id, ignore_missing=False) + self.assertIsNone(sot) + sot = self.conn.network.delete_port(self.PORT_ID, ignore_missing=False) + self.assertIsNone(sot) + sot = self.ROT.remove_interface( + self.conn.session, subnet_id=self.INT_SUB_ID) + self.assertEqual(sot['subnet_id'], self.INT_SUB_ID) + sot = self.conn.network.delete_router( + self.ROT_ID, ignore_missing=False) + self.assertIsNone(sot) + sot = self.conn.network.delete_subnet( + self.EXT_SUB_ID, ignore_missing=False) + self.assertIsNone(sot) + sot = self.conn.network.delete_network( + self.EXT_NET_ID, ignore_missing=False) + self.assertIsNone(sot) + sot = self.conn.network.delete_subnet( + self.INT_SUB_ID, ignore_missing=False) + self.assertIsNone(sot) + sot = self.conn.network.delete_network( + self.INT_NET_ID, ignore_missing=False) + self.assertIsNone(sot) + super(TestFloatingIP, self).tearDown() - @classmethod - def _create_network(cls, name, **args): - cls.name = name - net = cls.conn.network.create_network(name=name, **args) + def _create_network(self, name, **args): + self.name = name + net = self.conn.network.create_network(name=name, **args) assert isinstance(net, network.Network) - cls.assertIs(cls.name, net.name) + self.assertEqual(self.name, net.name) return net - @classmethod - def _create_subnet(cls, name, net_id, cidr): - cls.name = name - cls.net_id = net_id - cls.cidr = cidr - sub = cls.conn.network.create_subnet(name=cls.name, - ip_version=cls.IPV4, - network_id=cls.net_id, - cidr=cls.cidr) + def _create_subnet(self, name, net_id, cidr): + self.name = name + self.net_id = net_id + self.cidr = cidr + sub = self.conn.network.create_subnet( + name=self.name, + ip_version=self.IPV4, + network_id=self.net_id, + cidr=self.cidr) assert isinstance(sub, subnet.Subnet) - cls.assertIs(cls.name, sub.name) + self.assertEqual(self.name, sub.name) return sub def test_find_by_id(self): diff --git a/openstack/tests/functional/network/v2/test_network.py b/openstack/tests/functional/network/v2/test_network.py index 6a959d71f..de27fd407 100644 --- a/openstack/tests/functional/network/v2/test_network.py +++ b/openstack/tests/functional/network/v2/test_network.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid from openstack.network.v2 import network from openstack.tests.functional import base @@ -40,21 +39,20 @@ def delete_network(conn, network, subnet): class TestNetwork(base.BaseFunctionalTest): - NAME = uuid.uuid4().hex ID = None - @classmethod - def setUpClass(cls): - super(TestNetwork, cls).setUpClass() - sot = cls.conn.network.create_network(name=cls.NAME) + def setUp(self): + super(TestNetwork, self).setUp() + self.NAME = self.getUniqueString() + sot = self.conn.network.create_network(name=self.NAME) assert isinstance(sot, network.Network) - cls.assertIs(cls.NAME, sot.name) - cls.ID = sot.id + self.assertEqual(self.NAME, sot.name) + self.ID = sot.id - @classmethod - def tearDownClass(cls): - sot = cls.conn.network.delete_network(cls.ID, ignore_missing=False) - cls.assertIs(None, sot) + def tearDown(self): + sot = self.conn.network.delete_network(self.ID, ignore_missing=False) + self.assertIsNone(sot) + super(TestNetwork, self).tearDown() def test_find(self): sot = self.conn.network.find_network(self.NAME) diff --git a/openstack/tests/functional/network/v2/test_network_ip_availability.py b/openstack/tests/functional/network/v2/test_network_ip_availability.py index cfd814b64..6f7eda4bc 100644 --- a/openstack/tests/functional/network/v2/test_network_ip_availability.py +++ b/openstack/tests/functional/network/v2/test_network_ip_availability.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid from openstack.network.v2 import network from openstack.network.v2 import port @@ -20,44 +19,45 @@ class TestNetworkIPAvailability(base.BaseFunctionalTest): - NET_NAME = uuid.uuid4().hex - SUB_NAME = uuid.uuid4().hex - PORT_NAME = uuid.uuid4().hex - UPDATE_NAME = uuid.uuid4().hex IPV4 = 4 CIDR = "10.100.0.0/24" NET_ID = None SUB_ID = None PORT_ID = None - @classmethod - def setUpClass(cls): - super(TestNetworkIPAvailability, cls).setUpClass() - net = cls.conn.network.create_network(name=cls.NET_NAME) + def setUp(self): + super(TestNetworkIPAvailability, self).setUp() + self.NET_NAME = self.getUniqueString() + self.SUB_NAME = self.getUniqueString() + self.PORT_NAME = self.getUniqueString() + self.UPDATE_NAME = self.getUniqueString() + net = self.conn.network.create_network(name=self.NET_NAME) assert isinstance(net, network.Network) - cls.assertIs(cls.NET_NAME, net.name) - cls.NET_ID = net.id - sub = cls.conn.network.create_subnet(name=cls.SUB_NAME, - ip_version=cls.IPV4, - network_id=cls.NET_ID, - cidr=cls.CIDR) + self.assertEqual(self.NET_NAME, net.name) + self.NET_ID = net.id + sub = self.conn.network.create_subnet( + name=self.SUB_NAME, + ip_version=self.IPV4, + network_id=self.NET_ID, + cidr=self.CIDR) assert isinstance(sub, subnet.Subnet) - cls.assertIs(cls.SUB_NAME, sub.name) - cls.SUB_ID = sub.id - prt = cls.conn.network.create_port(name=cls.PORT_NAME, - network_id=cls.NET_ID) + self.assertEqual(self.SUB_NAME, sub.name) + self.SUB_ID = sub.id + prt = self.conn.network.create_port( + name=self.PORT_NAME, + network_id=self.NET_ID) assert isinstance(prt, port.Port) - cls.assertIs(cls.PORT_NAME, prt.name) - cls.PORT_ID = prt.id + self.assertEqual(self.PORT_NAME, prt.name) + self.PORT_ID = prt.id - @classmethod - def tearDownClass(cls): - sot = cls.conn.network.delete_port(cls.PORT_ID) - cls.assertIs(None, sot) - sot = cls.conn.network.delete_subnet(cls.SUB_ID) - cls.assertIs(None, sot) - sot = cls.conn.network.delete_network(cls.NET_ID) - cls.assertIs(None, sot) + def tearDown(self): + sot = self.conn.network.delete_port(self.PORT_ID) + self.assertIsNone(sot) + sot = self.conn.network.delete_subnet(self.SUB_ID) + self.assertIsNone(sot) + sot = self.conn.network.delete_network(self.NET_ID) + self.assertIsNone(sot) + super(TestNetworkIPAvailability, self).tearDown() def test_find(self): sot = self.conn.network.find_network_ip_availability(self.NET_ID) diff --git a/openstack/tests/functional/network/v2/test_port.py b/openstack/tests/functional/network/v2/test_port.py index 7409c7940..7f5514669 100644 --- a/openstack/tests/functional/network/v2/test_port.py +++ b/openstack/tests/functional/network/v2/test_port.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid from openstack.network.v2 import network from openstack.network.v2 import port @@ -20,44 +19,48 @@ class TestPort(base.BaseFunctionalTest): - NET_NAME = uuid.uuid4().hex - SUB_NAME = uuid.uuid4().hex - PORT_NAME = uuid.uuid4().hex - UPDATE_NAME = uuid.uuid4().hex IPV4 = 4 CIDR = "10.100.0.0/24" NET_ID = None SUB_ID = None PORT_ID = None - @classmethod - def setUpClass(cls): - super(TestPort, cls).setUpClass() - net = cls.conn.network.create_network(name=cls.NET_NAME) + def setUp(self): + super(TestPort, self).setUp() + self.NET_NAME = self.getUniqueString() + self.SUB_NAME = self.getUniqueString() + self.PORT_NAME = self.getUniqueString() + self.UPDATE_NAME = self.getUniqueString() + net = self.conn.network.create_network(name=self.NET_NAME) assert isinstance(net, network.Network) - cls.assertIs(cls.NET_NAME, net.name) - cls.NET_ID = net.id - sub = cls.conn.network.create_subnet(name=cls.SUB_NAME, - ip_version=cls.IPV4, - network_id=cls.NET_ID, - cidr=cls.CIDR) + self.assertEqual(self.NET_NAME, net.name) + self.NET_ID = net.id + sub = self.conn.network.create_subnet( + name=self.SUB_NAME, + ip_version=self.IPV4, + network_id=self.NET_ID, + cidr=self.CIDR) assert isinstance(sub, subnet.Subnet) - cls.assertIs(cls.SUB_NAME, sub.name) - cls.SUB_ID = sub.id - prt = cls.conn.network.create_port(name=cls.PORT_NAME, - network_id=cls.NET_ID) + self.assertEqual(self.SUB_NAME, sub.name) + self.SUB_ID = sub.id + prt = self.conn.network.create_port( + name=self.PORT_NAME, + network_id=self.NET_ID) assert isinstance(prt, port.Port) - cls.assertIs(cls.PORT_NAME, prt.name) - cls.PORT_ID = prt.id + self.assertEqual(self.PORT_NAME, prt.name) + self.PORT_ID = prt.id - @classmethod - def tearDownClass(cls): - sot = cls.conn.network.delete_port(cls.PORT_ID, ignore_missing=False) - cls.assertIs(None, sot) - sot = cls.conn.network.delete_subnet(cls.SUB_ID, ignore_missing=False) - cls.assertIs(None, sot) - sot = cls.conn.network.delete_network(cls.NET_ID, ignore_missing=False) - cls.assertIs(None, sot) + def tearDown(self): + sot = self.conn.network.delete_port( + self.PORT_ID, ignore_missing=False) + self.assertIsNone(sot) + sot = self.conn.network.delete_subnet( + self.SUB_ID, ignore_missing=False) + self.assertIsNone(sot) + sot = self.conn.network.delete_network( + self.NET_ID, ignore_missing=False) + self.assertIsNone(sot) + super(TestPort, self).tearDown() def test_find(self): sot = self.conn.network.find_port(self.PORT_NAME) diff --git a/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py b/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py index 6d8a9253e..5302067fa 100644 --- a/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid from openstack.network.v2 import (qos_bandwidth_limit_rule as _qos_bandwidth_limit_rule) @@ -20,10 +19,8 @@ class TestQoSBandwidthLimitRule(base.BaseFunctionalTest): QOS_POLICY_ID = None - QOS_POLICY_NAME = uuid.uuid4().hex QOS_IS_SHARED = False QOS_POLICY_DESCRIPTION = "QoS policy description" - RULE_ID = uuid.uuid4().hex RULE_MAX_KBPS = 1500 RULE_MAX_KBPS_NEW = 1800 RULE_MAX_BURST_KBPS = 1100 @@ -31,35 +28,36 @@ class TestQoSBandwidthLimitRule(base.BaseFunctionalTest): RULE_DIRECTION = 'egress' RULE_DIRECTION_NEW = 'ingress' - @classmethod - def setUpClass(cls): - super(TestQoSBandwidthLimitRule, cls).setUpClass() - qos_policy = cls.conn.network.create_qos_policy( - description=cls.QOS_POLICY_DESCRIPTION, - name=cls.QOS_POLICY_NAME, - shared=cls.QOS_IS_SHARED, + def setUp(self): + super(TestQoSBandwidthLimitRule, self).setUp() + self.QOS_POLICY_NAME = self.getUniqueString() + self.RULE_ID = self.getUniqueString() + qos_policy = self.conn.network.create_qos_policy( + description=self.QOS_POLICY_DESCRIPTION, + name=self.QOS_POLICY_NAME, + shared=self.QOS_IS_SHARED, ) - cls.QOS_POLICY_ID = qos_policy.id - qos_rule = cls.conn.network.create_qos_bandwidth_limit_rule( - cls.QOS_POLICY_ID, max_kbps=cls.RULE_MAX_KBPS, - max_burst_kbps=cls.RULE_MAX_BURST_KBPS, - direction=cls.RULE_DIRECTION, + self.QOS_POLICY_ID = qos_policy.id + qos_rule = self.conn.network.create_qos_bandwidth_limit_rule( + self.QOS_POLICY_ID, max_kbps=self.RULE_MAX_KBPS, + max_burst_kbps=self.RULE_MAX_BURST_KBPS, + direction=self.RULE_DIRECTION, ) assert isinstance(qos_rule, _qos_bandwidth_limit_rule.QoSBandwidthLimitRule) - cls.assertIs(cls.RULE_MAX_KBPS, qos_rule.max_kbps) - cls.assertIs(cls.RULE_MAX_BURST_KBPS, qos_rule.max_burst_kbps) - cls.assertIs(cls.RULE_DIRECTION, qos_rule.direction) - cls.RULE_ID = qos_rule.id + self.assertEqual(self.RULE_MAX_KBPS, qos_rule.max_kbps) + self.assertEqual(self.RULE_MAX_BURST_KBPS, qos_rule.max_burst_kbps) + self.assertEqual(self.RULE_DIRECTION, qos_rule.direction) + self.RULE_ID = qos_rule.id - @classmethod - def tearDownClass(cls): - rule = cls.conn.network.delete_qos_minimum_bandwidth_rule( - cls.RULE_ID, - cls.QOS_POLICY_ID) - qos_policy = cls.conn.network.delete_qos_policy(cls.QOS_POLICY_ID) - cls.assertIs(None, rule) - cls.assertIs(None, qos_policy) + def tearDown(self): + rule = self.conn.network.delete_qos_minimum_bandwidth_rule( + self.RULE_ID, + self.QOS_POLICY_ID) + qos_policy = self.conn.network.delete_qos_policy(self.QOS_POLICY_ID) + self.assertIsNone(rule) + self.assertIsNone(qos_policy) + super(TestQoSBandwidthLimitRule, self).tearDown() def test_find(self): sot = self.conn.network.find_qos_bandwidth_limit_rule( diff --git a/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py b/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py index 3930587c0..61690d31d 100644 --- a/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py +++ b/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid from openstack.network.v2 import (qos_dscp_marking_rule as _qos_dscp_marking_rule) @@ -20,37 +19,36 @@ class TestQoSDSCPMarkingRule(base.BaseFunctionalTest): QOS_POLICY_ID = None - QOS_POLICY_NAME = uuid.uuid4().hex QOS_IS_SHARED = False QOS_POLICY_DESCRIPTION = "QoS policy description" - RULE_ID = uuid.uuid4().hex RULE_DSCP_MARK = 36 RULE_DSCP_MARK_NEW = 40 - @classmethod - def setUpClass(cls): - super(TestQoSDSCPMarkingRule, cls).setUpClass() - qos_policy = cls.conn.network.create_qos_policy( - description=cls.QOS_POLICY_DESCRIPTION, - name=cls.QOS_POLICY_NAME, - shared=cls.QOS_IS_SHARED, + def setUp(self): + super(TestQoSDSCPMarkingRule, self).setUp() + self.QOS_POLICY_NAME = self.getUniqueString() + self.RULE_ID = self.getUniqueString() + qos_policy = self.conn.network.create_qos_policy( + description=self.QOS_POLICY_DESCRIPTION, + name=self.QOS_POLICY_NAME, + shared=self.QOS_IS_SHARED, ) - cls.QOS_POLICY_ID = qos_policy.id - qos_rule = cls.conn.network.create_qos_dscp_marking_rule( - cls.QOS_POLICY_ID, dscp_mark=cls.RULE_DSCP_MARK, + self.QOS_POLICY_ID = qos_policy.id + qos_rule = self.conn.network.create_qos_dscp_marking_rule( + self.QOS_POLICY_ID, dscp_mark=self.RULE_DSCP_MARK, ) assert isinstance(qos_rule, _qos_dscp_marking_rule.QoSDSCPMarkingRule) - cls.assertIs(cls.RULE_DSCP_MARK, qos_rule.dscp_mark) - cls.RULE_ID = qos_rule.id + self.assertEqual(self.RULE_DSCP_MARK, qos_rule.dscp_mark) + self.RULE_ID = qos_rule.id - @classmethod - def tearDownClass(cls): - rule = cls.conn.network.delete_qos_minimum_bandwidth_rule( - cls.RULE_ID, - cls.QOS_POLICY_ID) - qos_policy = cls.conn.network.delete_qos_policy(cls.QOS_POLICY_ID) - cls.assertIs(None, rule) - cls.assertIs(None, qos_policy) + def tearDown(self): + rule = self.conn.network.delete_qos_minimum_bandwidth_rule( + self.RULE_ID, + self.QOS_POLICY_ID) + qos_policy = self.conn.network.delete_qos_policy(self.QOS_POLICY_ID) + self.assertIsNone(rule) + self.assertIsNone(qos_policy) + super(TestQoSDSCPMarkingRule, self).tearDown() def test_find(self): sot = self.conn.network.find_qos_dscp_marking_rule( diff --git a/openstack/tests/functional/network/v2/test_qos_minimum_bandwidth_rule.py b/openstack/tests/functional/network/v2/test_qos_minimum_bandwidth_rule.py index 035121ca9..57138c22f 100644 --- a/openstack/tests/functional/network/v2/test_qos_minimum_bandwidth_rule.py +++ b/openstack/tests/functional/network/v2/test_qos_minimum_bandwidth_rule.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid from openstack.network.v2 import (qos_minimum_bandwidth_rule as _qos_minimum_bandwidth_rule) @@ -20,7 +19,6 @@ class TestQoSMinimumBandwidthRule(base.BaseFunctionalTest): QOS_POLICY_ID = None - QOS_POLICY_NAME = uuid.uuid4().hex QOS_IS_SHARED = False QOS_POLICY_DESCRIPTION = "QoS policy description" RULE_ID = None @@ -28,33 +26,33 @@ class TestQoSMinimumBandwidthRule(base.BaseFunctionalTest): RULE_MIN_KBPS_NEW = 1800 RULE_DIRECTION = 'egress' - @classmethod - def setUpClass(cls): - super(TestQoSMinimumBandwidthRule, cls).setUpClass() - qos_policy = cls.conn.network.create_qos_policy( - description=cls.QOS_POLICY_DESCRIPTION, - name=cls.QOS_POLICY_NAME, - shared=cls.QOS_IS_SHARED, + def setUp(self): + super(TestQoSMinimumBandwidthRule, self).setUp() + self.QOS_POLICY_NAME = self.getUniqueString() + qos_policy = self.conn.network.create_qos_policy( + description=self.QOS_POLICY_DESCRIPTION, + name=self.QOS_POLICY_NAME, + shared=self.QOS_IS_SHARED, ) - cls.QOS_POLICY_ID = qos_policy.id - qos_min_bw_rule = cls.conn.network.create_qos_minimum_bandwidth_rule( - cls.QOS_POLICY_ID, direction=cls.RULE_DIRECTION, - min_kbps=cls.RULE_MIN_KBPS, + self.QOS_POLICY_ID = qos_policy.id + qos_min_bw_rule = self.conn.network.create_qos_minimum_bandwidth_rule( + self.QOS_POLICY_ID, direction=self.RULE_DIRECTION, + min_kbps=self.RULE_MIN_KBPS, ) assert isinstance(qos_min_bw_rule, _qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule) - cls.assertIs(cls.RULE_MIN_KBPS, qos_min_bw_rule.min_kbps) - cls.assertIs(cls.RULE_DIRECTION, qos_min_bw_rule.direction) - cls.RULE_ID = qos_min_bw_rule.id + self.assertEqual(self.RULE_MIN_KBPS, qos_min_bw_rule.min_kbps) + self.assertEqual(self.RULE_DIRECTION, qos_min_bw_rule.direction) + self.RULE_ID = qos_min_bw_rule.id - @classmethod - def tearDownClass(cls): - rule = cls.conn.network.delete_qos_minimum_bandwidth_rule( - cls.RULE_ID, - cls.QOS_POLICY_ID) - qos_policy = cls.conn.network.delete_qos_policy(cls.QOS_POLICY_ID) - cls.assertIs(None, rule) - cls.assertIs(None, qos_policy) + def tearDown(self): + rule = self.conn.network.delete_qos_minimum_bandwidth_rule( + self.RULE_ID, + self.QOS_POLICY_ID) + qos_policy = self.conn.network.delete_qos_policy(self.QOS_POLICY_ID) + self.assertIsNone(rule) + self.assertIsNone(qos_policy) + super(TestQoSMinimumBandwidthRule, self).tearDown() def test_find(self): sot = self.conn.network.find_qos_minimum_bandwidth_rule( diff --git a/openstack/tests/functional/network/v2/test_qos_policy.py b/openstack/tests/functional/network/v2/test_qos_policy.py index 9643015b0..abe85de2b 100644 --- a/openstack/tests/functional/network/v2/test_qos_policy.py +++ b/openstack/tests/functional/network/v2/test_qos_policy.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid from openstack.network.v2 import qos_policy as _qos_policy from openstack.tests.functional import base @@ -19,30 +18,29 @@ class TestQoSPolicy(base.BaseFunctionalTest): QOS_POLICY_ID = None - QOS_POLICY_NAME = uuid.uuid4().hex - QOS_POLICY_NAME_UPDATED = uuid.uuid4().hex IS_SHARED = False IS_DEFAULT = False RULES = [] QOS_POLICY_DESCRIPTION = "QoS policy description" - @classmethod - def setUpClass(cls): - super(TestQoSPolicy, cls).setUpClass() - qos = cls.conn.network.create_qos_policy( - description=cls.QOS_POLICY_DESCRIPTION, - name=cls.QOS_POLICY_NAME, - shared=cls.IS_SHARED, - is_default=cls.IS_DEFAULT, + def setUp(self): + super(TestQoSPolicy, self).setUp() + self.QOS_POLICY_NAME = self.getUniqueString() + self.QOS_POLICY_NAME_UPDATED = self.getUniqueString() + qos = self.conn.network.create_qos_policy( + description=self.QOS_POLICY_DESCRIPTION, + name=self.QOS_POLICY_NAME, + shared=self.IS_SHARED, + is_default=self.IS_DEFAULT, ) assert isinstance(qos, _qos_policy.QoSPolicy) - cls.assertIs(cls.QOS_POLICY_NAME, qos.name) - cls.QOS_POLICY_ID = qos.id + self.assertEqual(self.QOS_POLICY_NAME, qos.name) + self.QOS_POLICY_ID = qos.id - @classmethod - def tearDownClass(cls): - sot = cls.conn.network.delete_qos_policy(cls.QOS_POLICY_ID) - cls.assertIs(None, sot) + def tearDown(self): + sot = self.conn.network.delete_qos_policy(self.QOS_POLICY_ID) + self.assertIsNone(sot) + super(TestQoSPolicy, self).tearDown() def test_find(self): sot = self.conn.network.find_qos_policy(self.QOS_POLICY_NAME) diff --git a/openstack/tests/functional/network/v2/test_rbac_policy.py b/openstack/tests/functional/network/v2/test_rbac_policy.py index 1c28b886b..b99e73c14 100644 --- a/openstack/tests/functional/network/v2/test_rbac_policy.py +++ b/openstack/tests/functional/network/v2/test_rbac_policy.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid from openstack.network.v2 import network from openstack.network.v2 import rbac_policy @@ -19,37 +18,38 @@ class TestRBACPolicy(base.BaseFunctionalTest): - NET_NAME = 'net-' + uuid.uuid4().hex - UPDATE_NAME = uuid.uuid4().hex ACTION = 'access_as_shared' OBJ_TYPE = 'network' TARGET_TENANT_ID = '*' NET_ID = None ID = None - @classmethod - def setUpClass(cls): - super(TestRBACPolicy, cls).setUpClass() - net = cls.conn.network.create_network(name=cls.NET_NAME) + def setUp(self): + super(TestRBACPolicy, self).setUp() + self.NET_NAME = self.getUniqueString('net') + self.UPDATE_NAME = self.getUniqueString() + net = self.conn.network.create_network(name=self.NET_NAME) assert isinstance(net, network.Network) - cls.NET_ID = net.id + self.NET_ID = net.id - sot = cls.conn.network.\ - create_rbac_policy(action=cls.ACTION, - object_type=cls.OBJ_TYPE, - target_tenant=cls.TARGET_TENANT_ID, - object_id=cls.NET_ID) + sot = self.conn.network.create_rbac_policy( + action=self.ACTION, + object_type=self.OBJ_TYPE, + target_tenant=self.TARGET_TENANT_ID, + object_id=self.NET_ID) assert isinstance(sot, rbac_policy.RBACPolicy) - cls.ID = sot.id + self.ID = sot.id - @classmethod - def tearDownClass(cls): - sot = cls.conn.network.delete_rbac_policy(cls.ID, - ignore_missing=False) - cls.assertIs(None, sot) - sot = cls.conn.network.delete_network(cls.NET_ID, - ignore_missing=False) - cls.assertIs(None, sot) + def tearDown(self): + sot = self.conn.network.delete_rbac_policy( + self.ID, + ignore_missing=False) + self.assertIsNone(sot) + sot = self.conn.network.delete_network( + self.NET_ID, + ignore_missing=False) + self.assertIsNone(sot) + super(TestRBACPolicy, self).tearDown() def test_find(self): sot = self.conn.network.find_rbac_policy(self.ID) diff --git a/openstack/tests/functional/network/v2/test_router.py b/openstack/tests/functional/network/v2/test_router.py index 1102c5da1..a42bcf686 100644 --- a/openstack/tests/functional/network/v2/test_router.py +++ b/openstack/tests/functional/network/v2/test_router.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid from openstack.network.v2 import router from openstack.tests.functional import base @@ -18,22 +17,21 @@ class TestRouter(base.BaseFunctionalTest): - NAME = uuid.uuid4().hex - UPDATE_NAME = uuid.uuid4().hex ID = None - @classmethod - def setUpClass(cls): - super(TestRouter, cls).setUpClass() - sot = cls.conn.network.create_router(name=cls.NAME) + def setUp(self): + super(TestRouter, self).setUp() + self.NAME = self.getUniqueString() + self.UPDATE_NAME = self.getUniqueString() + sot = self.conn.network.create_router(name=self.NAME) assert isinstance(sot, router.Router) - cls.assertIs(cls.NAME, sot.name) - cls.ID = sot.id + self.assertEqual(self.NAME, sot.name) + self.ID = sot.id - @classmethod - def tearDownClass(cls): - sot = cls.conn.network.delete_router(cls.ID, ignore_missing=False) - cls.assertIs(None, sot) + def tearDown(self): + sot = self.conn.network.delete_router(self.ID, ignore_missing=False) + self.assertIsNone(sot) + super(TestRouter, self).tearDown() def test_find(self): sot = self.conn.network.find_router(self.NAME) diff --git a/openstack/tests/functional/network/v2/test_router_add_remove_interface.py b/openstack/tests/functional/network/v2/test_router_add_remove_interface.py index 27072dd33..663400dfa 100644 --- a/openstack/tests/functional/network/v2/test_router_add_remove_interface.py +++ b/openstack/tests/functional/network/v2/test_router_add_remove_interface.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid from openstack.network.v2 import network from openstack.network.v2 import router @@ -20,9 +19,6 @@ class TestRouterInterface(base.BaseFunctionalTest): - ROUTER_NAME = uuid.uuid4().hex - NET_NAME = uuid.uuid4().hex - SUB_NAME = uuid.uuid4().hex CIDR = "10.100.0.0/16" IPV4 = 4 ROUTER_ID = None @@ -30,42 +26,45 @@ class TestRouterInterface(base.BaseFunctionalTest): SUB_ID = None ROT = None - @classmethod - def setUpClass(cls): - super(TestRouterInterface, cls).setUpClass() - sot = cls.conn.network.create_router(name=cls.ROUTER_NAME) + def setUp(self): + super(TestRouterInterface, self).setUp() + self.ROUTER_NAME = self.getUniqueString() + self.NET_NAME = self.getUniqueString() + self.SUB_NAME = self.getUniqueString() + sot = self.conn.network.create_router(name=self.ROUTER_NAME) assert isinstance(sot, router.Router) - cls.assertIs(cls.ROUTER_NAME, sot.name) - net = cls.conn.network.create_network(name=cls.NET_NAME) + self.assertEqual(self.ROUTER_NAME, sot.name) + net = self.conn.network.create_network(name=self.NET_NAME) assert isinstance(net, network.Network) - cls.assertIs(cls.NET_NAME, net.name) - sub = cls.conn.network.create_subnet(name=cls.SUB_NAME, - ip_version=cls.IPV4, - network_id=net.id, - cidr=cls.CIDR) + self.assertEqual(self.NET_NAME, net.name) + sub = self.conn.network.create_subnet( + name=self.SUB_NAME, + ip_version=self.IPV4, + network_id=net.id, + cidr=self.CIDR) assert isinstance(sub, subnet.Subnet) - cls.assertIs(cls.SUB_NAME, sub.name) - cls.ROUTER_ID = sot.id - cls.ROT = sot - cls.NET_ID = net.id - cls.SUB_ID = sub.id + self.assertEqual(self.SUB_NAME, sub.name) + self.ROUTER_ID = sot.id + self.ROT = sot + self.NET_ID = net.id + self.SUB_ID = sub.id - @classmethod - def tearDownClass(cls): - sot = cls.conn.network.delete_router(cls.ROUTER_ID, - ignore_missing=False) - cls.assertIs(None, sot) - sot = cls.conn.network.delete_subnet(cls.SUB_ID, ignore_missing=False) - cls.assertIs(None, sot) - sot = cls.conn.network.delete_network(cls.NET_ID, ignore_missing=False) - cls.assertIs(None, sot) + def tearDown(self): + sot = self.conn.network.delete_router( + self.ROUTER_ID, ignore_missing=False) + self.assertIsNone(sot) + sot = self.conn.network.delete_subnet( + self.SUB_ID, ignore_missing=False) + self.assertIsNone(sot) + sot = self.conn.network.delete_network( + self.NET_ID, ignore_missing=False) + self.assertIsNone(sot) + super(TestRouterInterface, self).tearDown() - def test_router_add_interface(self): + def test_router_add_remove_interface(self): iface = self.ROT.add_interface(self.conn.session, subnet_id=self.SUB_ID) self._verification(iface) - - def test_router_remove_interface(self): iface = self.ROT.remove_interface(self.conn.session, subnet_id=self.SUB_ID) self._verification(iface) diff --git a/openstack/tests/functional/network/v2/test_security_group.py b/openstack/tests/functional/network/v2/test_security_group.py index cc9fa114e..95120d03d 100644 --- a/openstack/tests/functional/network/v2/test_security_group.py +++ b/openstack/tests/functional/network/v2/test_security_group.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid from openstack.network.v2 import security_group from openstack.tests.functional import base @@ -18,22 +17,21 @@ class TestSecurityGroup(base.BaseFunctionalTest): - NAME = uuid.uuid4().hex ID = None - @classmethod - def setUpClass(cls): - super(TestSecurityGroup, cls).setUpClass() - sot = cls.conn.network.create_security_group(name=cls.NAME) + def setUp(self): + super(TestSecurityGroup, self).setUp() + self.NAME = self.getUniqueString() + sot = self.conn.network.create_security_group(name=self.NAME) assert isinstance(sot, security_group.SecurityGroup) - cls.assertIs(cls.NAME, sot.name) - cls.ID = sot.id - - @classmethod - def tearDownClass(cls): - sot = cls.conn.network.delete_security_group(cls.ID, - ignore_missing=False) - cls.assertIs(None, sot) + self.assertEqual(self.NAME, sot.name) + self.ID = sot.id + + def tearDown(self): + sot = self.conn.network.delete_security_group( + self.ID, ignore_missing=False) + self.assertIsNone(sot) + super(TestSecurityGroup, self).tearDown() def test_find(self): sot = self.conn.network.find_security_group(self.NAME) diff --git a/openstack/tests/functional/network/v2/test_security_group_rule.py b/openstack/tests/functional/network/v2/test_security_group_rule.py index f114fc4e4..544587183 100644 --- a/openstack/tests/functional/network/v2/test_security_group_rule.py +++ b/openstack/tests/functional/network/v2/test_security_group_rule.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid from openstack.network.v2 import security_group from openstack.network.v2 import security_group_rule @@ -19,7 +18,6 @@ class TestSecurityGroupRule(base.BaseFunctionalTest): - NAME = uuid.uuid4().hex IPV4 = 'IPv4' PROTO = 'tcp' PORT = 22 @@ -27,29 +25,29 @@ class TestSecurityGroupRule(base.BaseFunctionalTest): ID = None RULE_ID = None - @classmethod - def setUpClass(cls): - super(TestSecurityGroupRule, cls).setUpClass() - sot = cls.conn.network.create_security_group(name=cls.NAME) + def setUp(self): + super(TestSecurityGroupRule, self).setUp() + self.NAME = self.getUniqueString() + sot = self.conn.network.create_security_group(name=self.NAME) assert isinstance(sot, security_group.SecurityGroup) - cls.assertIs(cls.NAME, sot.name) - cls.ID = sot.id - rul = cls.conn.network.create_security_group_rule( - direction=cls.DIR, ethertype=cls.IPV4, - port_range_max=cls.PORT, port_range_min=cls.PORT, - protocol=cls.PROTO, security_group_id=cls.ID) + self.assertEqual(self.NAME, sot.name) + self.ID = sot.id + rul = self.conn.network.create_security_group_rule( + direction=self.DIR, ethertype=self.IPV4, + port_range_max=self.PORT, port_range_min=self.PORT, + protocol=self.PROTO, security_group_id=self.ID) assert isinstance(rul, security_group_rule.SecurityGroupRule) - cls.assertIs(cls.ID, rul.security_group_id) - cls.RULE_ID = rul.id + self.assertEqual(self.ID, rul.security_group_id) + self.RULE_ID = rul.id - @classmethod - def tearDownClass(cls): - sot = cls.conn.network.delete_security_group_rule(cls.RULE_ID, - ignore_missing=False) - cls.assertIs(None, sot) - sot = cls.conn.network.delete_security_group(cls.ID, - ignore_missing=False) - cls.assertIs(None, sot) + def tearDown(self): + sot = self.conn.network.delete_security_group_rule( + self.RULE_ID, ignore_missing=False) + self.assertIsNone(sot) + sot = self.conn.network.delete_security_group( + self.ID, ignore_missing=False) + self.assertIsNone(sot) + super(TestSecurityGroupRule, self).tearDown() def test_find(self): sot = self.conn.network.find_security_group_rule(self.RULE_ID) diff --git a/openstack/tests/functional/network/v2/test_segment.py b/openstack/tests/functional/network/v2/test_segment.py index 54200ded0..88499ed9e 100644 --- a/openstack/tests/functional/network/v2/test_segment.py +++ b/openstack/tests/functional/network/v2/test_segment.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid from openstack.network.v2 import network from openstack.network.v2 import segment @@ -19,7 +18,6 @@ class TestSegment(base.BaseFunctionalTest): - NETWORK_NAME = uuid.uuid4().hex NETWORK_TYPE = None PHYSICAL_NETWORK = None SEGMENTATION_ID = None @@ -27,88 +25,75 @@ class TestSegment(base.BaseFunctionalTest): SEGMENT_ID = None SEGMENT_EXTENSION = None - @classmethod - def setUpClass(cls): - super(TestSegment, cls).setUpClass() + def setUp(self): + super(TestSegment, self).setUp() + self.NETWORK_NAME = self.getUniqueString() # NOTE(rtheis): The segment extension is not yet enabled by default. # Skip the tests if not enabled. - cls.SEGMENT_EXTENSION = cls.conn.network.find_extension('segment') + if not self.conn.network.find_extension('segment'): + self.skipTest('Segment extension disabled') # Create a network to hold the segment. - net = cls.conn.network.create_network(name=cls.NETWORK_NAME) + net = self.conn.network.create_network(name=self.NETWORK_NAME) assert isinstance(net, network.Network) - cls.assertIs(cls.NETWORK_NAME, net.name) - cls.NETWORK_ID = net.id + self.assertEqual(self.NETWORK_NAME, net.name) + self.NETWORK_ID = net.id - if cls.SEGMENT_EXTENSION: + if self.SEGMENT_EXTENSION: # Get the segment for the network. - for seg in cls.conn.network.segments(): + for seg in self.conn.network.segments(): assert isinstance(seg, segment.Segment) - if cls.NETWORK_ID == seg.network_id: - cls.NETWORK_TYPE = seg.network_type - cls.PHYSICAL_NETWORK = seg.physical_network - cls.SEGMENTATION_ID = seg.segmentation_id - cls.SEGMENT_ID = seg.id + if self.NETWORK_ID == seg.network_id: + self.NETWORK_TYPE = seg.network_type + self.PHYSICAL_NETWORK = seg.physical_network + self.SEGMENTATION_ID = seg.segmentation_id + self.SEGMENT_ID = seg.id break - @classmethod - def tearDownClass(cls): - sot = cls.conn.network.delete_network(cls.NETWORK_ID, - ignore_missing=False) - cls.assertIs(None, sot) + def tearDown(self): + sot = self.conn.network.delete_network( + self.NETWORK_ID, + ignore_missing=False) + self.assertIsNone(sot) + super(TestSegment, self).tearDown() def test_create_delete(self): - if self.SEGMENT_EXTENSION: - sot = self.conn.network.create_segment( - description='test description', - name='test name', - network_id=self.NETWORK_ID, - network_type='geneve', - segmentation_id=2055, - ) - self.assertIsInstance(sot, segment.Segment) - del_sot = self.conn.network.delete_segment(sot.id) - self.assertEqual('test description', sot.description) - self.assertEqual('test name', sot.name) - self.assertEqual(self.NETWORK_ID, sot.network_id) - self.assertEqual('geneve', sot.network_type) - self.assertIsNone(sot.physical_network) - self.assertEqual(2055, sot.segmentation_id) - self.assertIsNone(del_sot) - else: - self.skipTest('Segment extension disabled') + sot = self.conn.network.create_segment( + description='test description', + name='test name', + network_id=self.NETWORK_ID, + network_type='geneve', + segmentation_id=2055, + ) + self.assertIsInstance(sot, segment.Segment) + del_sot = self.conn.network.delete_segment(sot.id) + self.assertEqual('test description', sot.description) + self.assertEqual('test name', sot.name) + self.assertEqual(self.NETWORK_ID, sot.network_id) + self.assertEqual('geneve', sot.network_type) + self.assertIsNone(sot.physical_network) + self.assertEqual(2055, sot.segmentation_id) + self.assertIsNone(del_sot) def test_find(self): - if self.SEGMENT_EXTENSION: - sot = self.conn.network.find_segment(self.SEGMENT_ID) - self.assertEqual(self.SEGMENT_ID, sot.id) - else: - self.skipTest('Segment extension disabled') + sot = self.conn.network.find_segment(self.SEGMENT_ID) + self.assertEqual(self.SEGMENT_ID, sot.id) def test_get(self): - if self.SEGMENT_EXTENSION: - sot = self.conn.network.get_segment(self.SEGMENT_ID) - self.assertEqual(self.SEGMENT_ID, sot.id) - self.assertIsNone(sot.name) - self.assertEqual(self.NETWORK_ID, sot.network_id) - self.assertEqual(self.NETWORK_TYPE, sot.network_type) - self.assertEqual(self.PHYSICAL_NETWORK, sot.physical_network) - self.assertEqual(self.SEGMENTATION_ID, sot.segmentation_id) - else: - self.skipTest('Segment extension disabled') + sot = self.conn.network.get_segment(self.SEGMENT_ID) + self.assertEqual(self.SEGMENT_ID, sot.id) + self.assertIsNone(sot.name) + self.assertEqual(self.NETWORK_ID, sot.network_id) + self.assertEqual(self.NETWORK_TYPE, sot.network_type) + self.assertEqual(self.PHYSICAL_NETWORK, sot.physical_network) + self.assertEqual(self.SEGMENTATION_ID, sot.segmentation_id) def test_list(self): - if self.SEGMENT_EXTENSION: - ids = [o.id for o in self.conn.network.segments(name=None)] - self.assertIn(self.SEGMENT_ID, ids) - else: - self.skipTest('Segment extension disabled') + ids = [o.id for o in self.conn.network.segments(name=None)] + self.assertIn(self.SEGMENT_ID, ids) def test_update(self): - if self.SEGMENT_EXTENSION: - sot = self.conn.network.update_segment(self.SEGMENT_ID, - description='update') - self.assertEqual('update', sot.description) - else: - self.skipTest('Segment extension disabled') + sot = self.conn.network.update_segment(self.SEGMENT_ID, + description='update') + self.assertEqual('update', sot.description) diff --git a/openstack/tests/functional/network/v2/test_service_profile.py b/openstack/tests/functional/network/v2/test_service_profile.py index 9b9fd73f8..221d9b6bd 100644 --- a/openstack/tests/functional/network/v2/test_service_profile.py +++ b/openstack/tests/functional/network/v2/test_service_profile.py @@ -21,25 +21,25 @@ class TestServiceProfile(base.BaseFunctionalTest): METAINFO = "FlAVOR_PROFILE_METAINFO" ID = None - @classmethod - def setUpClass(cls): - super(TestServiceProfile, cls).setUpClass() - service_profiles = cls.conn.network.create_service_profile( - description=cls.SERVICE_PROFILE_DESCRIPTION, - metainfo=cls.METAINFO,) + def setUp(self): + super(TestServiceProfile, self).setUp() + service_profiles = self.conn.network.create_service_profile( + description=self.SERVICE_PROFILE_DESCRIPTION, + metainfo=self.METAINFO,) assert isinstance(service_profiles, _service_profile.ServiceProfile) - cls.assertIs(cls.SERVICE_PROFILE_DESCRIPTION, - service_profiles.description) - cls.assertIs(cls.METAINFO, service_profiles.meta_info) + self.assertEqual( + self.SERVICE_PROFILE_DESCRIPTION, + service_profiles.description) + self.assertEqual(self.METAINFO, service_profiles.meta_info) - cls.ID = service_profiles.id + self.ID = service_profiles.id - @classmethod - def tearDownClass(cls): - service_profiles = cls.conn.network.delete_service_profile( - cls.ID, + def tearDown(self): + service_profiles = self.conn.network.delete_service_profile( + self.ID, ignore_missing=True) - cls.assertIs(None, service_profiles) + self.assertIsNone(service_profiles) + super(TestServiceProfile, self).tearDown() def test_find(self): service_profiles = self.conn.network.find_service_profile( diff --git a/openstack/tests/functional/network/v2/test_subnet.py b/openstack/tests/functional/network/v2/test_subnet.py index eb56a2bd9..c6ad4f986 100644 --- a/openstack/tests/functional/network/v2/test_subnet.py +++ b/openstack/tests/functional/network/v2/test_subnet.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid from openstack.network.v2 import network from openstack.network.v2 import subnet @@ -19,9 +18,6 @@ class TestSubnet(base.BaseFunctionalTest): - NET_NAME = uuid.uuid4().hex - SUB_NAME = uuid.uuid4().hex - UPDATE_NAME = uuid.uuid4().hex IPV4 = 4 CIDR = "10.100.0.0/24" DNS_SERVERS = ["8.8.4.4", "8.8.8.8"] @@ -30,30 +26,34 @@ class TestSubnet(base.BaseFunctionalTest): NET_ID = None SUB_ID = None - @classmethod - def setUpClass(cls): - super(TestSubnet, cls).setUpClass() - net = cls.conn.network.create_network(name=cls.NET_NAME) + def setUp(self): + super(TestSubnet, self).setUp() + self.NET_NAME = self.getUniqueString() + self.SUB_NAME = self.getUniqueString() + self.UPDATE_NAME = self.getUniqueString() + net = self.conn.network.create_network(name=self.NET_NAME) assert isinstance(net, network.Network) - cls.assertIs(cls.NET_NAME, net.name) - cls.NET_ID = net.id - sub = cls.conn.network.create_subnet(name=cls.SUB_NAME, - ip_version=cls.IPV4, - network_id=cls.NET_ID, - cidr=cls.CIDR, - dns_nameservers=cls.DNS_SERVERS, - allocation_pools=cls.POOL, - host_routes=cls.ROUTES) + self.assertEqual(self.NET_NAME, net.name) + self.NET_ID = net.id + sub = self.conn.network.create_subnet( + name=self.SUB_NAME, + ip_version=self.IPV4, + network_id=self.NET_ID, + cidr=self.CIDR, + dns_nameservers=self.DNS_SERVERS, + allocation_pools=self.POOL, + host_routes=self.ROUTES) assert isinstance(sub, subnet.Subnet) - cls.assertIs(cls.SUB_NAME, sub.name) - cls.SUB_ID = sub.id + self.assertEqual(self.SUB_NAME, sub.name) + self.SUB_ID = sub.id - @classmethod - def tearDownClass(cls): - sot = cls.conn.network.delete_subnet(cls.SUB_ID) - cls.assertIs(None, sot) - sot = cls.conn.network.delete_network(cls.NET_ID, ignore_missing=False) - cls.assertIs(None, sot) + def tearDown(self): + sot = self.conn.network.delete_subnet(self.SUB_ID) + self.assertIsNone(sot) + sot = self.conn.network.delete_network( + self.NET_ID, ignore_missing=False) + self.assertIsNone(sot) + super(TestSubnet, self).tearDown() def test_find(self): sot = self.conn.network.find_subnet(self.SUB_NAME) diff --git a/openstack/tests/functional/network/v2/test_subnet_pool.py b/openstack/tests/functional/network/v2/test_subnet_pool.py index c06d850b9..ebb4cd708 100644 --- a/openstack/tests/functional/network/v2/test_subnet_pool.py +++ b/openstack/tests/functional/network/v2/test_subnet_pool.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid from openstack.network.v2 import subnet_pool as _subnet_pool from openstack.tests.functional import base @@ -18,8 +17,6 @@ class TestSubnetPool(base.BaseFunctionalTest): - SUBNET_POOL_NAME = uuid.uuid4().hex - SUBNET_POOL_NAME_UPDATED = uuid.uuid4().hex SUBNET_POOL_ID = None MINIMUM_PREFIX_LENGTH = 8 DEFAULT_PREFIX_LENGTH = 24 @@ -29,25 +26,26 @@ class TestSubnetPool(base.BaseFunctionalTest): IP_VERSION = 4 PREFIXES = ['10.100.0.0/24', '10.101.0.0/24'] - @classmethod - def setUpClass(cls): - super(TestSubnetPool, cls).setUpClass() - subnet_pool = cls.conn.network.create_subnet_pool( - name=cls.SUBNET_POOL_NAME, - min_prefixlen=cls.MINIMUM_PREFIX_LENGTH, - default_prefixlen=cls.DEFAULT_PREFIX_LENGTH, - max_prefixlen=cls.MAXIMUM_PREFIX_LENGTH, - default_quota=cls.DEFAULT_QUOTA, - shared=cls.IS_SHARED, - prefixes=cls.PREFIXES) + def setUp(self): + super(TestSubnetPool, self).setUp() + self.SUBNET_POOL_NAME = self.getUniqueString() + self.SUBNET_POOL_NAME_UPDATED = self.getUniqueString() + subnet_pool = self.conn.network.create_subnet_pool( + name=self.SUBNET_POOL_NAME, + min_prefixlen=self.MINIMUM_PREFIX_LENGTH, + default_prefixlen=self.DEFAULT_PREFIX_LENGTH, + max_prefixlen=self.MAXIMUM_PREFIX_LENGTH, + default_quota=self.DEFAULT_QUOTA, + shared=self.IS_SHARED, + prefixes=self.PREFIXES) assert isinstance(subnet_pool, _subnet_pool.SubnetPool) - cls.assertIs(cls.SUBNET_POOL_NAME, subnet_pool.name) - cls.SUBNET_POOL_ID = subnet_pool.id + self.assertEqual(self.SUBNET_POOL_NAME, subnet_pool.name) + self.SUBNET_POOL_ID = subnet_pool.id - @classmethod - def tearDownClass(cls): - sot = cls.conn.network.delete_subnet_pool(cls.SUBNET_POOL_ID) - cls.assertIs(None, sot) + def tearDown(self): + sot = self.conn.network.delete_subnet_pool(self.SUBNET_POOL_ID) + self.assertIsNone(sot) + super(TestSubnetPool, self).tearDown() def test_find(self): sot = self.conn.network.find_subnet_pool(self.SUBNET_POOL_NAME) diff --git a/openstack/tests/functional/object_store/v1/test_account.py b/openstack/tests/functional/object_store/v1/test_account.py index fe258db88..a71e63a4e 100644 --- a/openstack/tests/functional/object_store/v1/test_account.py +++ b/openstack/tests/functional/object_store/v1/test_account.py @@ -10,20 +10,19 @@ # License for the specific language governing permissions and limitations # under the License. -import unittest - from openstack.tests.functional import base -@unittest.skipUnless(base.service_exists(service_type='object-store'), - 'Object Storage service does not exist') class TestAccount(base.BaseFunctionalTest): - @classmethod - def tearDownClass(cls): - super(TestAccount, cls).tearDownClass() - account = cls.conn.object_store.get_account_metadata() - cls.conn.object_store.delete_account_metadata(account.metadata.keys()) + def setUp(self): + super(TestAccount, self).setUp() + self.require_service('object-store') + + def tearDown(self): + account = self.conn.object_store.get_account_metadata() + self.conn.object_store.delete_account_metadata(account.metadata.keys()) + super(TestAccount, self).tearDown() def test_system_metadata(self): account = self.conn.object_store.get_account_metadata() diff --git a/openstack/tests/functional/object_store/v1/test_container.py b/openstack/tests/functional/object_store/v1/test_container.py index 0a6dc754f..25e0f7ad5 100644 --- a/openstack/tests/functional/object_store/v1/test_container.py +++ b/openstack/tests/functional/object_store/v1/test_container.py @@ -10,31 +10,23 @@ # License for the specific language governing permissions and limitations # under the License. -import unittest -import uuid - from openstack.object_store.v1 import container as _container from openstack.tests.functional import base -@unittest.skipUnless(base.service_exists(service_type='object-store'), - 'Object Storage service does not exist') class TestContainer(base.BaseFunctionalTest): - NAME = uuid.uuid4().hex + def setUp(self): + super(TestContainer, self).setUp() + self.require_service('object-store') - @classmethod - def setUpClass(cls): - super(TestContainer, cls).setUpClass() - container = cls.conn.object_store.create_container(name=cls.NAME) + self.NAME = self.getUniqueString() + container = self.conn.object_store.create_container(name=self.NAME) + self.addEmptyCleanup( + self.conn.object_store.delete_container, + self.NAME, ignore_missing=False) assert isinstance(container, _container.Container) - cls.assertIs(cls.NAME, container.name) - - @classmethod - def tearDownClass(cls): - result = cls.conn.object_store.delete_container(cls.NAME, - ignore_missing=False) - cls.assertIs(None, result) + self.assertEqual(self.NAME, container.name) def test_list(self): names = [o.name for o in self.conn.object_store.containers()] diff --git a/openstack/tests/functional/object_store/v1/test_obj.py b/openstack/tests/functional/object_store/v1/test_obj.py index 145e30538..534b0dd22 100644 --- a/openstack/tests/functional/object_store/v1/test_obj.py +++ b/openstack/tests/functional/object_store/v1/test_obj.py @@ -10,32 +10,26 @@ # License for the specific language governing permissions and limitations # under the License. -import unittest -import uuid - from openstack.tests.functional import base -@unittest.skipUnless(base.service_exists(service_type='object-store'), - 'Object Storage service does not exist') class TestObject(base.BaseFunctionalTest): - FOLDER = uuid.uuid4().hex - FILE = uuid.uuid4().hex DATA = b'abc' - @classmethod - def setUpClass(cls): - super(TestObject, cls).setUpClass() - cls.conn.object_store.create_container(name=cls.FOLDER) - cls.sot = cls.conn.object_store.upload_object( - container=cls.FOLDER, name=cls.FILE, data=cls.DATA) - - @classmethod - def tearDownClass(cls): - super(TestObject, cls).tearDownClass() - cls.conn.object_store.delete_object(cls.sot, ignore_missing=False) - cls.conn.object_store.delete_container(cls.FOLDER) + def setUp(self): + super(TestObject, self).setUp() + self.require_service('object-store') + + self.FOLDER = self.getUniqueString() + self.FILE = self.getUniqueString() + self.conn.object_store.create_container(name=self.FOLDER) + self.addCleanup(self.conn.object_store.delete_container, self.FOLDER) + self.sot = self.conn.object_store.upload_object( + container=self.FOLDER, name=self.FILE, data=self.DATA) + self.addEmptyCleanup( + self.conn.object_store.delete_object, self.sot, + ignore_missing=False) def test_list(self): names = [o.name for o diff --git a/openstack/tests/functional/orchestration/v1/test_stack.py b/openstack/tests/functional/orchestration/v1/test_stack.py index 73dc19339..4ccd14dee 100644 --- a/openstack/tests/functional/orchestration/v1/test_stack.py +++ b/openstack/tests/functional/orchestration/v1/test_stack.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import time import unittest from openstack import exceptions @@ -19,8 +20,6 @@ @unittest.skip("bug/1525005") -@unittest.skipUnless(base.service_exists(service_type='orchestration'), - 'Orchestration service does not exist') class TestStack(base.BaseFunctionalTest): NAME = 'test_stack' @@ -29,48 +28,50 @@ class TestStack(base.BaseFunctionalTest): subnet = None cidr = '10.99.99.0/16' - @classmethod - def setUpClass(cls): - super(TestStack, cls).setUpClass() - if cls.conn.compute.find_keypair(cls.NAME) is None: - cls.conn.compute.create_keypair(name=cls.NAME) - image = next(cls.conn.image.images()) + def setUp(self): + super(TestStack, self).setUp() + self.require_service('orchestration') + + if self.conn.compute.find_keypair(self.NAME) is None: + self.conn.compute.create_keypair(name=self.NAME) + image = next(self.conn.image.images()) tname = "openstack/tests/functional/orchestration/v1/hello_world.yaml" with open(tname) as f: template = f.read() - cls.network, cls.subnet = test_network.create_network(cls.conn, - cls.NAME, - cls.cidr) + self.network, self.subnet = test_network.create_network( + self.conn, + self.NAME, + self.cidr) parameters = { 'image': image.id, - 'key_name': cls.NAME, - 'network': cls.network.id, + 'key_name': self.NAME, + 'network': self.network.id, } - sot = cls.conn.orchestration.create_stack( - name=cls.NAME, + sot = self.conn.orchestration.create_stack( + name=self.NAME, parameters=parameters, template=template, ) assert isinstance(sot, stack.Stack) - cls.assertIs(True, (sot.id is not None)) - cls.stack = sot - cls.assertIs(cls.NAME, sot.name) - cls.conn.orchestration.wait_for_status( + self.assertEqual(True, (sot.id is not None)) + self.stack = sot + self.assertEqual(self.NAME, sot.name) + self.conn.orchestration.wait_for_status( sot, status='CREATE_COMPLETE', failures=['CREATE_FAILED']) - @classmethod - def tearDownClass(cls): - super(TestStack, cls).tearDownClass() - cls.conn.orchestration.delete_stack(cls.stack, ignore_missing=False) - cls.conn.compute.delete_keypair(cls.NAME) + def tearDown(self): + self.conn.orchestration.delete_stack(self.stack, ignore_missing=False) + self.conn.compute.delete_keypair(self.NAME) # Need to wait for the stack to go away before network delete try: - cls.conn.orchestration.wait_for_status( - cls.stack, 'DELETE_COMPLETE') + self.conn.orchestration.wait_for_status( + self.stack, 'DELETE_COMPLETE') except exceptions.NotFoundException: pass - cls.linger_for_delete() - test_network.delete_network(cls.conn, cls.network, cls.subnet) + # TODO(shade) sleeping in tests is bad mmkay? + time.sleep(40) + test_network.delete_network(self.conn, self.network, self.subnet) + super(TestStack, self).tearDown() def test_list(self): names = [o.name for o in self.conn.orchestration.stacks()] diff --git a/openstack/tests/functional/telemetry/alarm/v2/test_alarm.py b/openstack/tests/functional/telemetry/alarm/v2/test_alarm.py index 3c6c0dd90..38a18ea5f 100644 --- a/openstack/tests/functional/telemetry/alarm/v2/test_alarm.py +++ b/openstack/tests/functional/telemetry/alarm/v2/test_alarm.py @@ -11,28 +11,25 @@ # under the License. import unittest -import uuid from openstack.telemetry.alarm.v2 import alarm from openstack.tests.functional import base @unittest.skip("bug/1524468") -@unittest.skipUnless(base.service_exists(service_type="alarming"), - "Alarming service does not exist") -@unittest.skipUnless(base.service_exists(service_type="metering"), - "Metering service does not exist") class TestAlarm(base.BaseFunctionalTest): - NAME = uuid.uuid4().hex ID = None - @classmethod - def setUpClass(cls): - super(TestAlarm, cls).setUpClass() - meter = next(cls.conn.telemetry.meters()) - sot = cls.conn.alarm.create_alarm( - name=cls.NAME, + def setUp(self): + super(TestAlarm, self).setUp() + self.require_service('alarming') + self.require_service('metering') + + self.NAME = self.getUniqueString() + meter = next(self.conn.telemetry.meters()) + sot = self.conn.alarm.create_alarm( + name=self.NAME, type='threshold', threshold_rule={ 'meter_name': meter.name, @@ -40,13 +37,13 @@ def setUpClass(cls): }, ) assert isinstance(sot, alarm.Alarm) - cls.assertIs(cls.NAME, sot.name) - cls.ID = sot.id + self.assertEqual(self.NAME, sot.name) + self.ID = sot.id - @classmethod - def tearDownClass(cls): - sot = cls.conn.alarm.delete_alarm(cls.ID, ignore_missing=False) - cls.assertIs(None, sot) + def tearDown(self): + sot = self.conn.alarm.delete_alarm(self.ID, ignore_missing=False) + self.assertIsNone(sot) + super(TestAlarm, self).tearDown() def test_get(self): sot = self.conn.alarm.get_alarm(self.ID) diff --git a/openstack/tests/functional/telemetry/alarm/v2/test_alarm_change.py b/openstack/tests/functional/telemetry/alarm/v2/test_alarm_change.py index f1b94d419..311944e40 100644 --- a/openstack/tests/functional/telemetry/alarm/v2/test_alarm_change.py +++ b/openstack/tests/functional/telemetry/alarm/v2/test_alarm_change.py @@ -11,38 +11,32 @@ # under the License. import unittest -import uuid from openstack.tests.functional import base @unittest.skip("bug/1524468") -@unittest.skipUnless(base.service_exists(service_type="metering"), - "Metering service does not exist") -@unittest.skipUnless(base.service_exists(service_type="alarming"), - "Alarming service does not exist") class TestAlarmChange(base.BaseFunctionalTest): - NAME = uuid.uuid4().hex alarm = None - @classmethod - def setUpClass(cls): - super(TestAlarmChange, cls).setUpClass() - meter = next(cls.conn.telemetry.meters()) - alarm = cls.conn.alarm.create_alarm( - name=cls.NAME, + def setUp(self): + super(TestAlarmChange, self).setUp() + self.require_service('alarming') + self.require_service('metering') + + self.NAME = self.getUniqueString() + meter = next(self.conn.telemetry.meters()) + self.alarm = self.conn.alarm.create_alarm( + name=self.NAME, type='threshold', threshold_rule={ 'meter_name': meter.name, 'threshold': 1.1, }, ) - cls.alarm = alarm - - @classmethod - def tearDownClass(cls): - cls.conn.alarm.delete_alarm(cls.alarm, ignore_missing=False) + self.addCleanup( + self.conn.alarm.delete_alarm, self.alarm, ignore_missing=False) def test_list(self): change = next(self.conn.alarm.alarm_changes(self.alarm)) diff --git a/openstack/tests/functional/telemetry/v2/test_capability.py b/openstack/tests/functional/telemetry/v2/test_capability.py index 8db9d4e60..4e4f93835 100644 --- a/openstack/tests/functional/telemetry/v2/test_capability.py +++ b/openstack/tests/functional/telemetry/v2/test_capability.py @@ -10,15 +10,15 @@ # License for the specific language governing permissions and limitations # under the License. -import unittest - from openstack.tests.functional import base -@unittest.skipUnless(base.service_exists(service_type="metering"), - "Metering service does not exist") class TestCapability(base.BaseFunctionalTest): + def setUp(self): + super(TestCapability, self).setUp() + self.require_service('metering') + def test_list(self): ids = [o.id for o in self.conn.telemetry.capabilities()] self.assertIn('resources:query:simple', ids) diff --git a/openstack/tests/functional/telemetry/v2/test_meter.py b/openstack/tests/functional/telemetry/v2/test_meter.py index c06495ef6..26fe8a4e8 100644 --- a/openstack/tests/functional/telemetry/v2/test_meter.py +++ b/openstack/tests/functional/telemetry/v2/test_meter.py @@ -10,21 +10,20 @@ # License for the specific language governing permissions and limitations # under the License. -import unittest -import uuid - from openstack.tests.functional import base -@unittest.skipUnless(base.service_exists(service_type="metering"), - "Metering service does not exist") class TestMeter(base.BaseFunctionalTest): + def setUp(self): + super(TestMeter, self).setUp() + self.require_service('metering') + def test_list(self): # TODO(thowe): Remove this in favor of create_meter call. # Since we do not have a create meter method at the moment # make sure there is some data in there - name = uuid.uuid4().hex + name = self.getUniqueString() tainer = self.conn.object_store.create_container(name=name) self.conn.object_store.delete_container(tainer) diff --git a/openstack/tests/functional/telemetry/v2/test_resource.py b/openstack/tests/functional/telemetry/v2/test_resource.py index 0ec768a53..28f8a15bc 100644 --- a/openstack/tests/functional/telemetry/v2/test_resource.py +++ b/openstack/tests/functional/telemetry/v2/test_resource.py @@ -10,15 +10,15 @@ # License for the specific language governing permissions and limitations # under the License. -import unittest - from openstack.tests.functional import base -@unittest.skipUnless(base.service_exists(service_type="metering"), - "Metering service does not exist") class TestResource(base.BaseFunctionalTest): + def setUp(self): + super(TestResource, self).setUp() + self.require_service('metering') + def test_list(self): ids = [o.resource_id for o in self.conn.telemetry.resources()] self.assertNotEqual(0, len(ids)) diff --git a/openstack/tests/functional/telemetry/v2/test_sample.py b/openstack/tests/functional/telemetry/v2/test_sample.py index 49c209ec5..cfa3692d8 100644 --- a/openstack/tests/functional/telemetry/v2/test_sample.py +++ b/openstack/tests/functional/telemetry/v2/test_sample.py @@ -10,16 +10,16 @@ # License for the specific language governing permissions and limitations # under the License. -import unittest - from openstack.telemetry.v2 import sample from openstack.tests.functional import base -@unittest.skipUnless(base.service_exists(service_type="metering"), - "Metering service does not exist") class TestSample(base.BaseFunctionalTest): + def setUp(self): + super(TestSample, self).setUp() + self.require_service('metering') + def test_list(self): for meter in self.conn.telemetry.meters(): for sot in self.conn.telemetry.samples(meter): diff --git a/openstack/tests/functional/telemetry/v2/test_statistics.py b/openstack/tests/functional/telemetry/v2/test_statistics.py index 7db971357..349070a0f 100644 --- a/openstack/tests/functional/telemetry/v2/test_statistics.py +++ b/openstack/tests/functional/telemetry/v2/test_statistics.py @@ -10,15 +10,15 @@ # License for the specific language governing permissions and limitations # under the License. -import unittest - from openstack.tests.functional import base -@unittest.skipUnless(base.service_exists(service_type="metering"), - "Metering service does not exist") class TestStatistics(base.BaseFunctionalTest): + def setUp(self): + super(TestStatistics, self).setUp() + self.require_service('metering') + def test_list(self): for met in self.conn.telemetry.meters(): for stat in self.conn.telemetry.statistics(met): From 4bad718783ccd760cac0a97ce194f391c3ac63c5 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 11 Aug 2017 15:58:38 -0500 Subject: [PATCH 1843/3836] Rework config and rest layers This is a large and invasive change to the underlying guts. Most casual use should not notice a difference, but advanced users, especially those using the Profile or Authenticator interfaces or making use of pluggable providers will be broken. The overall intent is to align directly on top of the mechanisms that came from os-client-config for config and to use keystoneauth1's Adapter interface to make use of the canonical implementations of such things as service and version discovery. The end goal is that openstacksdk provides the REST interaction layer for python-openstackclient, shade, Ansible and nodepool. Replace profile with openstack.config os-client-config is used by shade and python-openstackclient to read and process configuration. openstacksdk also can use the os-client-config interface, but translates it internally into the Profile object. As os-client-config has been injested into openstack.config, remove Profile and just use the config classes. Make proxy subclass of adapter This gives every service a generic passthrough for REST calls, which means we can map unknown service-type values to a generic proxy. Strip endpoint_filter We're passing Adapters around, not sessions. Doing so means that self.service and endpoint_filter have become unnecessary. Rename _Request.uri to _Request.url This is a stepping-stone to replacing _Request with requests.Request and using requests.Session.prepare_request inside of _prepare_request. Rename service proxy instances to match their official service-type. Aliases are kept for the old versions, but make the canonical versions match the official name. Rename bare_metal to baremetal Rename cluster to clustering Rename block_store to block_storage Rename telemetry to meter Create generic proxies for all services in STA Every service listed in service types authority is an OpenStack service. Even if we don't know about it in SDK, we should at the very least have a low-level Adapter for it so that people can use REST calls while waiting on the SDK to add higher-level constructs. The pypy jobs are happily green. Run them as voting rather than non-voting. Add syntatic sugar alias for making connections Typing: import openstack.connection conn = openstack.connection.Connection(cloud='example') is annoying. This allows: import openstack conn = openstack.connect(cloud='example') Use task_manager and Adapter from shade As a stepping-stone towards shade and sdk codepaths being rationalized, we need to get SDK using the Adapter from shade that submits requests into the TaskManager. For normal operation this is a passthrough/no-op sort of thing, but it's essential for high-volume consumers such as nodepool. This exposes a bunch of places in tests where we're mocking a bit too deeply. We should go back through and fix all of those via requests_mock, but that's WAY too much for today. This was a 'for later' task, but it turns out that the move to Adapter was causing exceptions to be thrown that were not the exceptions that were intended to be caught in the SDK layer, which was causing functional tests of things like GET operations to fail. So it became a today task. Change-Id: I7b46e263a76d84573bdfbbece57b1048764ed939 --- .zuul.yaml | 1 + doc/source/contributor/layout.rst | 23 +- doc/source/contributor/layout.txt | 1 - doc/source/enforcer.py | 10 +- doc/source/user/config/configuration.rst | 4 + .../guides/{bare_metal.rst => baremetal.rst} | 4 +- .../{block_store.rst => block_storage.rst} | 6 +- doc/source/users/guides/cluster.rst | 36 -- doc/source/users/guides/clustering.rst | 37 ++ .../guides/{cluster => clustering}/action.rst | 0 .../{cluster => clustering}/cluster.rst | 0 .../guides/{cluster => clustering}/event.rst | 0 .../guides/{cluster => clustering}/node.rst | 0 .../guides/{cluster => clustering}/policy.rst | 14 +- .../{cluster => clustering}/policy_type.rst | 6 +- .../{cluster => clustering}/profile.rst | 14 +- .../{cluster => clustering}/profile_type.rst | 6 +- .../{cluster => clustering}/receiver.rst | 0 doc/source/users/guides/connect.rst | 29 +- .../users/guides/{telemetry.rst => meter.rst} | 4 +- doc/source/users/index.rst | 37 +- doc/source/users/profile.rst | 9 - doc/source/users/proxies/bare_metal.rst | 76 --- doc/source/users/proxies/baremetal.rst | 76 +++ doc/source/users/proxies/block_storage.rst | 43 ++ doc/source/users/proxies/block_store.rst | 43 -- doc/source/users/proxies/cluster.rst | 177 ------- doc/source/users/proxies/clustering.rst | 177 +++++++ doc/source/users/proxies/meter.rst | 85 ++++ doc/source/users/proxies/telemetry.rst | 85 ---- .../{bare_metal => baremetal}/index.rst | 2 +- .../{bare_metal => baremetal}/v1/chassis.rst | 6 +- .../{bare_metal => baremetal}/v1/driver.rst | 6 +- .../{bare_metal => baremetal}/v1/node.rst | 6 +- .../{bare_metal => baremetal}/v1/port.rst | 6 +- .../v1/port_group.rst | 6 +- .../{block_store => block_storage}/index.rst | 4 +- .../resources/block_storage/v2/snapshot.rst | 21 + .../users/resources/block_storage/v2/type.rst | 13 + .../resources/block_storage/v2/volume.rst | 21 + .../resources/block_store/v2/snapshot.rst | 21 - .../users/resources/block_store/v2/type.rst | 13 - .../users/resources/block_store/v2/volume.rst | 21 - .../users/resources/cluster/v1/action.rst | 12 - .../users/resources/cluster/v1/build_info.rst | 12 - .../users/resources/cluster/v1/cluster.rst | 12 - .../resources/cluster/v1/cluster_policy.rst | 13 - .../users/resources/cluster/v1/event.rst | 12 - .../users/resources/cluster/v1/node.rst | 12 - .../users/resources/cluster/v1/policy.rst | 12 - .../resources/cluster/v1/policy_type.rst | 12 - .../users/resources/cluster/v1/profile.rst | 12 - .../resources/cluster/v1/profile_type.rst | 12 - .../users/resources/cluster/v1/receiver.rst | 12 - .../{cluster => clustering}/index.rst | 0 .../users/resources/clustering/v1/action.rst | 12 + .../resources/clustering/v1/build_info.rst | 12 + .../users/resources/clustering/v1/cluster.rst | 12 + .../clustering/v1/cluster_policy.rst | 13 + .../users/resources/clustering/v1/event.rst | 12 + .../users/resources/clustering/v1/node.rst | 12 + .../users/resources/clustering/v1/policy.rst | 12 + .../resources/clustering/v1/policy_type.rst | 12 + .../users/resources/clustering/v1/profile.rst | 12 + .../resources/clustering/v1/profile_type.rst | 12 + .../resources/clustering/v1/receiver.rst | 12 + .../resources/{telemetry => meter}/index.rst | 2 +- .../{telemetry => meter}/v2/capability.rst | 6 +- .../{telemetry => meter}/v2/meter.rst | 6 +- .../{telemetry => meter}/v2/resource.rst | 6 +- .../{telemetry => meter}/v2/sample.rst | 6 +- .../{telemetry => meter}/v2/statistics.rst | 6 +- doc/source/users/session.rst | 10 - examples/{cluster => clustering}/__init__.py | 0 examples/{cluster => clustering}/policy.py | 0 .../{cluster => clustering}/policy_type.py | 0 examples/{cluster => clustering}/profile.py | 0 .../{cluster => clustering}/profile_type.py | 2 +- examples/connect.py | 23 +- openstack/__init__.py | 6 + .../{bare_metal => baremetal}/__init__.py | 0 .../baremetal_service.py} | 4 +- .../{bare_metal => baremetal}/v1/__init__.py | 0 .../{bare_metal => baremetal}/v1/_proxy.py | 104 ++--- .../{bare_metal => baremetal}/v1/chassis.py | 4 +- .../{bare_metal => baremetal}/v1/driver.py | 4 +- .../{bare_metal => baremetal}/v1/node.py | 6 +- .../{bare_metal => baremetal}/v1/port.py | 4 +- .../v1/port_group.py | 4 +- .../{bare_metal => baremetal}/version.py | 6 +- .../__init__.py | 0 .../block_storage_service.py} | 11 +- .../v2/__init__.py | 0 .../v2/_proxy.py | 14 +- .../v2/snapshot.py | 4 +- .../{block_store => block_storage}/v2/type.py | 4 +- .../v2/volume.py | 4 +- openstack/cloud/_adapter.py | 123 +++-- openstack/cloud/exc.py | 137 +----- openstack/cloud/openstackcloud.py | 17 +- openstack/cloud/task_manager.py | 299 ++++-------- openstack/{cluster => clustering}/__init__.py | 0 .../clustering_service.py} | 8 +- .../{cluster => clustering}/v1/__init__.py | 0 .../{cluster => clustering}/v1/_proxy.py | 243 +++++----- .../{cluster => clustering}/v1/action.py | 4 +- .../{cluster => clustering}/v1/build_info.py | 4 +- .../{cluster => clustering}/v1/cluster.py | 8 +- .../v1/cluster_attr.py | 4 +- .../v1/cluster_policy.py | 4 +- openstack/{cluster => clustering}/v1/event.py | 4 +- openstack/{cluster => clustering}/v1/node.py | 10 +- .../{cluster => clustering}/v1/policy.py | 4 +- .../{cluster => clustering}/v1/policy_type.py | 4 +- .../{cluster => clustering}/v1/profile.py | 4 +- .../v1/profile_type.py | 4 +- .../{cluster => clustering}/v1/receiver.py | 4 +- .../{cluster => clustering}/v1/service.py | 4 +- openstack/{cluster => clustering}/version.py | 6 +- openstack/compute/v2/_proxy.py | 84 ++-- openstack/compute/v2/keypair.py | 2 +- openstack/compute/v2/limits.py | 6 +- openstack/compute/v2/metadata.py | 2 +- openstack/compute/v2/server.py | 2 +- openstack/compute/v2/server_ip.py | 2 +- openstack/compute/v2/service.py | 2 +- openstack/config/cloud_config.py | 10 +- openstack/connection.py | 388 +++++++++------- openstack/database/v1/instance.py | 14 +- openstack/exceptions.py | 130 ++++-- openstack/identity/v2/extension.py | 2 +- openstack/identity/v3/domain.py | 12 +- openstack/identity/v3/project.py | 12 +- openstack/identity/version.py | 2 +- openstack/image/v2/_proxy.py | 14 +- openstack/image/v2/image.py | 12 +- openstack/key_manager/v1/secret.py | 6 +- openstack/message/v1/_proxy.py | 6 +- openstack/message/v1/claim.py | 2 +- openstack/message/v1/message.py | 4 +- openstack/message/v1/queue.py | 2 +- openstack/message/v2/_proxy.py | 2 +- openstack/message/v2/claim.py | 8 +- openstack/message/v2/message.py | 10 +- openstack/message/v2/queue.py | 8 +- openstack/message/v2/subscription.py | 8 +- openstack/{telemetry => meter}/__init__.py | 0 .../{telemetry => meter}/alarm/__init__.py | 0 .../alarm/alarm_service.py | 0 .../{telemetry => meter}/alarm/v2/__init__.py | 0 .../{telemetry => meter}/alarm/v2/_proxy.py | 26 +- .../{telemetry => meter}/alarm/v2/alarm.py | 6 +- .../alarm/v2/alarm_change.py | 4 +- .../meter_service.py} | 10 +- openstack/{telemetry => meter}/v2/__init__.py | 0 openstack/{telemetry => meter}/v2/_proxy.py | 34 +- .../{telemetry => meter}/v2/capability.py | 6 +- openstack/{telemetry => meter}/v2/meter.py | 4 +- openstack/{telemetry => meter}/v2/resource.py | 4 +- openstack/{telemetry => meter}/v2/sample.py | 4 +- .../{telemetry => meter}/v2/statistics.py | 6 +- openstack/module_loader.py | 29 -- openstack/network/v2/_proxy.py | 34 +- openstack/network/v2/agent.py | 8 +- openstack/network/v2/flavor.py | 4 +- openstack/network/v2/router.py | 16 +- openstack/network/v2/tag.py | 2 +- openstack/object_store/v1/_base.py | 8 +- openstack/object_store/v1/_proxy.py | 16 +- openstack/object_store/v1/container.py | 6 +- openstack/object_store/v1/obj.py | 11 +- openstack/orchestration/v1/_proxy.py | 6 +- openstack/orchestration/v1/stack.py | 2 +- openstack/orchestration/v1/stack_files.py | 2 +- openstack/orchestration/v1/template.py | 2 +- openstack/profile.py | 35 +- openstack/proxy.py | 49 +- openstack/proxy2.py | 49 +- openstack/resource.py | 66 +-- openstack/resource2.py | 57 ++- openstack/service_filter.py | 2 - openstack/session.py | 352 -------------- .../__init__.py | 0 .../v2/__init__.py | 0 .../v2/test_snapshot.py | 22 +- .../v2/test_type.py | 8 +- .../v2/test_volume.py | 10 +- .../{telemetry => meter}/__init__.py | 0 .../{telemetry => meter}/alarm/__init__.py | 0 .../{telemetry => meter}/alarm/v2/__init__.py | 0 .../alarm/v2/test_alarm.py | 4 +- .../alarm/v2/test_alarm_change.py | 2 +- .../{telemetry => meter}/v2/__init__.py | 0 .../v2/test_capability.py | 2 +- .../{telemetry => meter}/v2/test_meter.py | 2 +- .../{telemetry => meter}/v2/test_resource.py | 2 +- .../{telemetry => meter}/v2/test_sample.py | 6 +- .../v2/test_statistics.py | 4 +- .../v2/test_agent_add_remove_network.py | 4 +- .../functional/network/v2/test_floating_ip.py | 4 +- .../v2/test_router_add_remove_interface.py | 4 +- .../{bare_metal => baremetal}/__init__.py | 0 .../test_baremetal_service.py} | 6 +- .../{bare_metal => baremetal}/test_version.py | 2 +- .../{bare_metal => baremetal}/v1/__init__.py | 0 .../v1/test_chassis.py | 2 +- .../v1/test_driver.py | 2 +- .../{bare_metal => baremetal}/v1/test_node.py | 2 +- .../{bare_metal => baremetal}/v1/test_port.py | 2 +- .../v1/test_port_group.py | 2 +- .../v1/test_proxy.py | 16 +- openstack/tests/unit/base.py | 16 + .../__init__.py | 0 .../test_block_storage_service.py} | 6 +- .../v2/__init__.py | 0 .../v2/test_proxy.py | 8 +- .../v2/test_snapshot.py | 2 +- .../v2/test_type.py | 2 +- .../v2/test_volume.py | 2 +- openstack/tests/unit/cloud/test__adapter.py | 2 +- .../tests/unit/cloud/test_role_assignment.py | 2 + .../tests/unit/cloud/test_task_manager.py | 21 +- .../unit/cluster/test_cluster_service.py | 6 +- openstack/tests/unit/cluster/test_version.py | 2 +- .../tests/unit/cluster/v1/test_action.py | 2 +- .../tests/unit/cluster/v1/test_build_info.py | 2 +- .../tests/unit/cluster/v1/test_cluster.py | 28 +- .../unit/cluster/v1/test_cluster_attr.py | 2 +- .../unit/cluster/v1/test_cluster_policy.py | 2 +- openstack/tests/unit/cluster/v1/test_event.py | 2 +- openstack/tests/unit/cluster/v1/test_node.py | 11 +- .../tests/unit/cluster/v1/test_policy.py | 2 +- .../tests/unit/cluster/v1/test_policy_type.py | 2 +- .../tests/unit/cluster/v1/test_profile.py | 2 +- .../unit/cluster/v1/test_profile_type.py | 2 +- openstack/tests/unit/cluster/v1/test_proxy.py | 88 ++-- .../tests/unit/cluster/v1/test_receiver.py | 2 +- .../tests/unit/cluster/v1/test_service.py | 2 +- .../tests/unit/compute/v2/test_metadata.py | 9 +- openstack/tests/unit/compute/v2/test_proxy.py | 6 +- .../tests/unit/compute/v2/test_server.py | 76 +-- .../tests/unit/compute/v2/test_service.py | 8 +- .../tests/unit/database/v1/test_instance.py | 10 +- openstack/tests/unit/image/v2/test_image.py | 76 +-- openstack/tests/unit/image/v2/test_proxy.py | 2 +- .../tests/unit/key_manager/v1/test_secret.py | 7 +- openstack/tests/unit/message/v1/test_claim.py | 2 +- .../tests/unit/message/v1/test_message.py | 4 +- openstack/tests/unit/message/v1/test_proxy.py | 6 +- openstack/tests/unit/message/v1/test_queue.py | 2 +- openstack/tests/unit/message/v2/test_claim.py | 18 +- .../tests/unit/message/v2/test_message.py | 14 +- openstack/tests/unit/message/v2/test_queue.py | 12 +- .../unit/message/v2/test_subscription.py | 12 +- .../unit/{telemetry => meter}/__init__.py | 0 .../{telemetry => meter}/alarm/__init__.py | 0 .../alarm/test_alarm_service.py | 2 +- .../{telemetry => meter}/alarm/v2/__init__.py | 0 .../alarm/v2/test_alarm.py | 6 +- .../alarm/v2/test_alarm_change.py | 2 +- .../alarm/v2/test_proxy.py | 6 +- .../test_meter_service.py} | 6 +- .../unit/{telemetry => meter}/v2/__init__.py | 0 .../v2/test_capability.py | 2 +- .../{telemetry => meter}/v2/test_meter.py | 2 +- .../{telemetry => meter}/v2/test_proxy.py | 16 +- .../{telemetry => meter}/v2/test_resource.py | 2 +- .../{telemetry => meter}/v2/test_sample.py | 2 +- .../v2/test_statistics.py | 4 +- openstack/tests/unit/network/v2/test_agent.py | 8 +- .../tests/unit/network/v2/test_flavor.py | 4 +- .../tests/unit/network/v2/test_floating_ip.py | 1 - .../tests/unit/network/v2/test_router.py | 12 +- openstack/tests/unit/network/v2/test_tag.py | 2 +- .../unit/object_store/v1/test_container.py | 4 +- .../tests/unit/object_store/v1/test_obj.py | 7 +- .../tests/unit/orchestration/v1/test_proxy.py | 13 +- .../tests/unit/orchestration/v1/test_stack.py | 4 +- .../unit/orchestration/v1/test_stack_files.py | 4 +- .../unit/orchestration/v1/test_template.py | 8 +- openstack/tests/unit/test_connection.py | 178 ++----- openstack/tests/unit/test_exceptions.py | 2 +- openstack/tests/unit/test_profile.py | 106 ----- openstack/tests/unit/test_proxy.py | 45 +- openstack/tests/unit/test_proxy2.py | 47 +- openstack/tests/unit/test_proxy_base.py | 6 +- openstack/tests/unit/test_proxy_base2.py | 6 +- openstack/tests/unit/test_resource.py | 67 +-- openstack/tests/unit/test_resource2.py | 77 ++- openstack/tests/unit/test_session.py | 437 ------------------ openstack/utils.py | 14 + openstack/workflow/v2/execution.py | 3 +- openstack/workflow/v2/workflow.py | 3 +- .../removed-profile-437f3038025b0fb3.yaml | 8 + .../renamed-bare-metal-b1cdbc52af14e042.yaml | 4 + .../renamed-block-store-bc5e0a7315bfeb67.yaml | 4 + .../renamed-cluster-743da6d321fffcba.yaml | 4 + .../renamed-telemetry-c08ae3e72afca24f.yaml | 4 + requirements.txt | 2 +- 299 files changed, 2343 insertions(+), 3549 deletions(-) rename doc/source/users/guides/{bare_metal.rst => baremetal.rst} (70%) rename doc/source/users/guides/{block_store.rst => block_storage.rst} (59%) delete mode 100644 doc/source/users/guides/cluster.rst create mode 100644 doc/source/users/guides/clustering.rst rename doc/source/users/guides/{cluster => clustering}/action.rst (100%) rename doc/source/users/guides/{cluster => clustering}/cluster.rst (100%) rename doc/source/users/guides/{cluster => clustering}/event.rst (100%) rename doc/source/users/guides/{cluster => clustering}/node.rst (100%) rename doc/source/users/guides/{cluster => clustering}/policy.rst (86%) rename doc/source/users/guides/{cluster => clustering}/policy_type.rst (87%) rename doc/source/users/guides/{cluster => clustering}/profile.rst (86%) rename doc/source/users/guides/{cluster => clustering}/profile_type.rst (86%) rename doc/source/users/guides/{cluster => clustering}/receiver.rst (100%) rename doc/source/users/guides/{telemetry.rst => meter.rst} (73%) delete mode 100644 doc/source/users/profile.rst delete mode 100644 doc/source/users/proxies/bare_metal.rst create mode 100644 doc/source/users/proxies/baremetal.rst create mode 100644 doc/source/users/proxies/block_storage.rst delete mode 100644 doc/source/users/proxies/block_store.rst delete mode 100644 doc/source/users/proxies/cluster.rst create mode 100644 doc/source/users/proxies/clustering.rst create mode 100644 doc/source/users/proxies/meter.rst delete mode 100644 doc/source/users/proxies/telemetry.rst rename doc/source/users/resources/{bare_metal => baremetal}/index.rst (85%) rename doc/source/users/resources/{bare_metal => baremetal}/v1/chassis.rst (54%) rename doc/source/users/resources/{bare_metal => baremetal}/v1/driver.rst (54%) rename doc/source/users/resources/{bare_metal => baremetal}/v1/node.rst (54%) rename doc/source/users/resources/{bare_metal => baremetal}/v1/port.rst (54%) rename doc/source/users/resources/{bare_metal => baremetal}/v1/port_group.rst (53%) rename doc/source/users/resources/{block_store => block_storage}/index.rst (59%) create mode 100644 doc/source/users/resources/block_storage/v2/snapshot.rst create mode 100644 doc/source/users/resources/block_storage/v2/type.rst create mode 100644 doc/source/users/resources/block_storage/v2/volume.rst delete mode 100644 doc/source/users/resources/block_store/v2/snapshot.rst delete mode 100644 doc/source/users/resources/block_store/v2/type.rst delete mode 100644 doc/source/users/resources/block_store/v2/volume.rst delete mode 100644 doc/source/users/resources/cluster/v1/action.rst delete mode 100644 doc/source/users/resources/cluster/v1/build_info.rst delete mode 100644 doc/source/users/resources/cluster/v1/cluster.rst delete mode 100644 doc/source/users/resources/cluster/v1/cluster_policy.rst delete mode 100644 doc/source/users/resources/cluster/v1/event.rst delete mode 100644 doc/source/users/resources/cluster/v1/node.rst delete mode 100644 doc/source/users/resources/cluster/v1/policy.rst delete mode 100644 doc/source/users/resources/cluster/v1/policy_type.rst delete mode 100644 doc/source/users/resources/cluster/v1/profile.rst delete mode 100644 doc/source/users/resources/cluster/v1/profile_type.rst delete mode 100644 doc/source/users/resources/cluster/v1/receiver.rst rename doc/source/users/resources/{cluster => clustering}/index.rst (100%) create mode 100644 doc/source/users/resources/clustering/v1/action.rst create mode 100644 doc/source/users/resources/clustering/v1/build_info.rst create mode 100644 doc/source/users/resources/clustering/v1/cluster.rst create mode 100644 doc/source/users/resources/clustering/v1/cluster_policy.rst create mode 100644 doc/source/users/resources/clustering/v1/event.rst create mode 100644 doc/source/users/resources/clustering/v1/node.rst create mode 100644 doc/source/users/resources/clustering/v1/policy.rst create mode 100644 doc/source/users/resources/clustering/v1/policy_type.rst create mode 100644 doc/source/users/resources/clustering/v1/profile.rst create mode 100644 doc/source/users/resources/clustering/v1/profile_type.rst create mode 100644 doc/source/users/resources/clustering/v1/receiver.rst rename doc/source/users/resources/{telemetry => meter}/index.rst (86%) rename doc/source/users/resources/{telemetry => meter}/v2/capability.rst (54%) rename doc/source/users/resources/{telemetry => meter}/v2/meter.rst (55%) rename doc/source/users/resources/{telemetry => meter}/v2/resource.rst (54%) rename doc/source/users/resources/{telemetry => meter}/v2/sample.rst (54%) rename doc/source/users/resources/{telemetry => meter}/v2/statistics.rst (54%) delete mode 100644 doc/source/users/session.rst rename examples/{cluster => clustering}/__init__.py (100%) rename examples/{cluster => clustering}/policy.py (100%) rename examples/{cluster => clustering}/policy_type.py (100%) rename examples/{cluster => clustering}/profile.py (100%) rename examples/{cluster => clustering}/profile_type.py (97%) rename openstack/{bare_metal => baremetal}/__init__.py (100%) rename openstack/{bare_metal/bare_metal_service.py => baremetal/baremetal_service.py} (87%) rename openstack/{bare_metal => baremetal}/v1/__init__.py (100%) rename openstack/{bare_metal => baremetal}/v1/_proxy.py (88%) rename openstack/{bare_metal => baremetal}/v1/chassis.py (94%) rename openstack/{bare_metal => baremetal}/v1/driver.py (92%) rename openstack/{bare_metal => baremetal}/v1/node.py (97%) rename openstack/{bare_metal => baremetal}/v1/port.py (96%) rename openstack/{bare_metal => baremetal}/v1/port_group.py (96%) rename openstack/{bare_metal => baremetal}/version.py (83%) rename openstack/{block_store => block_storage}/__init__.py (100%) rename openstack/{block_store/block_store_service.py => block_storage/block_storage_service.py} (66%) rename openstack/{block_store => block_storage}/v2/__init__.py (100%) rename openstack/{block_store => block_storage}/v2/_proxy.py (93%) rename openstack/{block_store => block_storage}/v2/snapshot.py (95%) rename openstack/{block_store => block_storage}/v2/type.py (90%) rename openstack/{block_store => block_storage}/v2/volume.py (97%) rename openstack/{cluster => clustering}/__init__.py (100%) rename openstack/{cluster/cluster_service.py => clustering/clustering_service.py} (81%) rename openstack/{cluster => clustering}/v1/__init__.py (100%) rename openstack/{cluster => clustering}/v1/_proxy.py (84%) rename openstack/{cluster => clustering}/v1/action.py (96%) rename openstack/{cluster => clustering}/v1/build_info.py (89%) rename openstack/{cluster => clustering}/v1/cluster.py (96%) rename openstack/{cluster => clustering}/v1/cluster_attr.py (91%) rename openstack/{cluster => clustering}/v1/cluster_policy.py (93%) rename openstack/{cluster => clustering}/v1/event.py (95%) rename openstack/{cluster => clustering}/v1/node.py (95%) rename openstack/{cluster => clustering}/v1/policy.py (95%) rename openstack/{cluster => clustering}/v1/policy_type.py (91%) rename openstack/{cluster => clustering}/v1/profile.py (95%) rename openstack/{cluster => clustering}/v1/profile_type.py (91%) rename openstack/{cluster => clustering}/v1/receiver.py (95%) rename openstack/{cluster => clustering}/v1/service.py (92%) rename openstack/{cluster => clustering}/version.py (83%) rename openstack/{telemetry => meter}/__init__.py (100%) rename openstack/{telemetry => meter}/alarm/__init__.py (100%) rename openstack/{telemetry => meter}/alarm/alarm_service.py (100%) rename openstack/{telemetry => meter}/alarm/v2/__init__.py (100%) rename openstack/{telemetry => meter}/alarm/v2/_proxy.py (83%) rename openstack/{telemetry => meter}/alarm/v2/alarm.py (94%) rename openstack/{telemetry => meter}/alarm/v2/alarm_change.py (93%) rename openstack/{telemetry/telemetry_service.py => meter/meter_service.py} (72%) rename openstack/{telemetry => meter}/v2/__init__.py (100%) rename openstack/{telemetry => meter}/v2/_proxy.py (84%) rename openstack/{telemetry => meter}/v2/capability.py (87%) rename openstack/{telemetry => meter}/v2/meter.py (93%) rename openstack/{telemetry => meter}/v2/resource.py (94%) rename openstack/{telemetry => meter}/v2/sample.py (95%) rename openstack/{telemetry => meter}/v2/statistics.py (93%) delete mode 100644 openstack/module_loader.py delete mode 100644 openstack/session.py rename openstack/tests/functional/{block_store => block_storage}/__init__.py (100%) rename openstack/tests/functional/{block_store => block_storage}/v2/__init__.py (100%) rename openstack/tests/functional/{block_store => block_storage}/v2/test_snapshot.py (75%) rename openstack/tests/functional/{block_store => block_storage}/v2/test_type.py (82%) rename openstack/tests/functional/{block_store => block_storage}/v2/test_volume.py (82%) rename openstack/tests/functional/{telemetry => meter}/__init__.py (100%) rename openstack/tests/functional/{telemetry => meter}/alarm/__init__.py (100%) rename openstack/tests/functional/{telemetry => meter}/alarm/v2/__init__.py (100%) rename openstack/tests/functional/{telemetry => meter}/alarm/v2/test_alarm.py (94%) rename openstack/tests/functional/{telemetry => meter}/alarm/v2/test_alarm_change.py (96%) rename openstack/tests/functional/{telemetry => meter}/v2/__init__.py (100%) rename openstack/tests/functional/{telemetry => meter}/v2/test_capability.py (93%) rename openstack/tests/functional/{telemetry => meter}/v2/test_meter.py (94%) rename openstack/tests/functional/{telemetry => meter}/v2/test_resource.py (91%) rename openstack/tests/functional/{telemetry => meter}/v2/test_sample.py (84%) rename openstack/tests/functional/{telemetry => meter}/v2/test_statistics.py (88%) rename openstack/tests/unit/{bare_metal => baremetal}/__init__.py (100%) rename openstack/tests/unit/{bare_metal/test_bare_metal_service.py => baremetal/test_baremetal_service.py} (86%) rename openstack/tests/unit/{bare_metal => baremetal}/test_version.py (97%) rename openstack/tests/unit/{bare_metal => baremetal}/v1/__init__.py (100%) rename openstack/tests/unit/{bare_metal => baremetal}/v1/test_chassis.py (98%) rename openstack/tests/unit/{bare_metal => baremetal}/v1/test_driver.py (97%) rename openstack/tests/unit/{bare_metal => baremetal}/v1/test_node.py (99%) rename openstack/tests/unit/{bare_metal => baremetal}/v1/test_port.py (98%) rename openstack/tests/unit/{bare_metal => baremetal}/v1/test_port_group.py (98%) rename openstack/tests/unit/{bare_metal => baremetal}/v1/test_proxy.py (94%) rename openstack/tests/unit/{block_store => block_storage}/__init__.py (100%) rename openstack/tests/unit/{block_store/test_block_store_service.py => block_storage/test_block_storage_service.py} (85%) rename openstack/tests/unit/{block_store => block_storage}/v2/__init__.py (100%) rename openstack/tests/unit/{block_store => block_storage}/v2/test_proxy.py (94%) rename openstack/tests/unit/{block_store => block_storage}/v2/test_snapshot.py (98%) rename openstack/tests/unit/{block_store => block_storage}/v2/test_type.py (97%) rename openstack/tests/unit/{block_store => block_storage}/v2/test_volume.py (99%) rename openstack/tests/unit/{telemetry => meter}/__init__.py (100%) rename openstack/tests/unit/{telemetry => meter}/alarm/__init__.py (100%) rename openstack/tests/unit/{telemetry => meter}/alarm/test_alarm_service.py (95%) rename openstack/tests/unit/{telemetry => meter}/alarm/v2/__init__.py (100%) rename openstack/tests/unit/{telemetry => meter}/alarm/v2/test_alarm.py (95%) rename openstack/tests/unit/{telemetry => meter}/alarm/v2/test_alarm_change.py (98%) rename openstack/tests/unit/{telemetry => meter}/alarm/v2/test_proxy.py (92%) rename openstack/tests/unit/{telemetry/test_telemetry_service.py => meter/test_meter_service.py} (86%) rename openstack/tests/unit/{telemetry => meter}/v2/__init__.py (100%) rename openstack/tests/unit/{telemetry => meter}/v2/test_capability.py (98%) rename openstack/tests/unit/{telemetry => meter}/v2/test_meter.py (97%) rename openstack/tests/unit/{telemetry => meter}/v2/test_proxy.py (85%) rename openstack/tests/unit/{telemetry => meter}/v2/test_resource.py (98%) rename openstack/tests/unit/{telemetry => meter}/v2/test_sample.py (98%) rename openstack/tests/unit/{telemetry => meter}/v2/test_statistics.py (96%) delete mode 100644 openstack/tests/unit/test_profile.py delete mode 100644 openstack/tests/unit/test_session.py create mode 100644 releasenotes/notes/removed-profile-437f3038025b0fb3.yaml create mode 100644 releasenotes/notes/renamed-bare-metal-b1cdbc52af14e042.yaml create mode 100644 releasenotes/notes/renamed-block-store-bc5e0a7315bfeb67.yaml create mode 100644 releasenotes/notes/renamed-cluster-743da6d321fffcba.yaml create mode 100644 releasenotes/notes/renamed-telemetry-c08ae3e72afca24f.yaml diff --git a/.zuul.yaml b/.zuul.yaml index 30a456719..1309cc7b0 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -220,6 +220,7 @@ - project: name: openstack/python-openstacksdk templates: + - openstack-pypy-jobs - openstacksdk-functional-tips - openstacksdk-tox-tips check: diff --git a/doc/source/contributor/layout.rst b/doc/source/contributor/layout.rst index c221bc793..2c9d70395 100644 --- a/doc/source/contributor/layout.rst +++ b/doc/source/contributor/layout.rst @@ -5,15 +5,6 @@ The following diagram shows how the project is laid out. .. literalinclude:: layout.txt -Session -------- - -The :class:`openstack.session.Session` manages an authenticator, -transport, and user profile. It exposes methods corresponding to -HTTP verbs, and injects your authentication token into a request, -determines any service preferences callers may have set, gets the endpoint -from the authenticator, and sends the request out through the transport. - Resource -------- @@ -26,7 +17,7 @@ service's ``https://openstack:1234/v2/servers`` resource. The base ``Resource`` contains methods to support the typical `CRUD `_ operations supported by REST APIs, and handles the construction of URLs -and calling the appropriate HTTP verb on the given ``Session``. +and calling the appropriate HTTP verb on the given ``Adapter``. Values sent to or returned from the service are implemented as attributes on the ``Resource`` subclass with type :class:`openstack.resource.prop`. @@ -63,10 +54,10 @@ Each service implements a ``Proxy`` class, within the ``openstack//vX/_proxy.py`` module. For example, the v2 compute service's ``Proxy`` exists in ``openstack/compute/v2/_proxy.py``. -This ``Proxy`` class manages a :class:`~openstack.sessions.Session` and +This ``Proxy`` class contains a :class:`~keystoneauth1.adapter.Adapter` and provides a higher-level interface for users to work with via a :class:`~openstack.connection.Connection` instance. Rather than requiring -users to maintain their own session and work with lower-level +users to maintain their own ``Adapter`` and work with lower-level :class:`~openstack.resource.Resource` objects, the ``Proxy`` interface offers a place to make things easier for the caller. @@ -77,7 +68,7 @@ Each ``Proxy`` class implements methods which act on the underlying return flavor.Flavor.list(self.session, **params) This method is operating on the ``openstack.compute.v2.flavor.Flavor.list`` -method. For the time being, it simply passes on the ``Session`` maintained +method. For the time being, it simply passes on the ``Adapter`` maintained by the ``Proxy``, and returns what the underlying ``Resource.list`` method does. @@ -88,9 +79,9 @@ way which will apply nicely across all of the services. Connection ---------- -The :class:`openstack.connection.Connection` class builds atop a ``Session`` -object, and provides a higher level interface constructed of ``Proxy`` -objects from each of the services. +The :class:`openstack.connection.Connection` class builds atop a +:class:`os_client_config.config.CloudConfig` object, and provides a higher +level interface constructed of ``Proxy`` objects from each of the services. The ``Connection`` class' primary purpose is to act as a high-level interface to this SDK, managing the lower level connecton bits and exposing the diff --git a/doc/source/contributor/layout.txt b/doc/source/contributor/layout.txt index 2dd7121d5..eeffbac87 100644 --- a/doc/source/contributor/layout.txt +++ b/doc/source/contributor/layout.txt @@ -1,7 +1,6 @@ openstack/ connection.py resource.py - session.py compute/ compute_service.py v2/ diff --git a/doc/source/enforcer.py b/doc/source/enforcer.py index 04bea4335..ff705e9f3 100644 --- a/doc/source/enforcer.py +++ b/doc/source/enforcer.py @@ -26,9 +26,9 @@ class EnforcementError(errors.SphinxError): def get_proxy_methods(): """Return a set of public names on all proxies""" - names = ["openstack.bare_metal.v1._proxy", - "openstack.block_store.v2._proxy", - "openstack.cluster.v1._proxy", + names = ["openstack.baremetal.v1._proxy", + "openstack.clustering.v1._proxy", + "openstack.block_storage.v2._proxy", "openstack.compute.v2._proxy", "openstack.database.v1._proxy", "openstack.identity.v2._proxy", @@ -43,8 +43,8 @@ def get_proxy_methods(): "openstack.network.v2._proxy", "openstack.object_store.v1._proxy", "openstack.orchestration.v1._proxy", - "openstack.telemetry.v2._proxy", - "openstack.telemetry.alarm.v2._proxy", + "openstack.meter.v2._proxy", + "openstack.meter.alarm.v2._proxy", "openstack.workflow.v2._proxy"] modules = (importlib.import_module(name) for name in names) diff --git a/doc/source/user/config/configuration.rst b/doc/source/user/config/configuration.rst index df0b26659..0282cf219 100644 --- a/doc/source/user/config/configuration.rst +++ b/doc/source/user/config/configuration.rst @@ -2,6 +2,8 @@ Configuring os-client-config Applications =========================================== +.. _config-environment-variables: + Environment Variables --------------------- @@ -22,6 +24,8 @@ for trove set export OS_DATABASE_SERVICE_TYPE=rax:database +.. _config-clouds-yaml: + Config Files ------------ diff --git a/doc/source/users/guides/bare_metal.rst b/doc/source/users/guides/baremetal.rst similarity index 70% rename from doc/source/users/guides/bare_metal.rst rename to doc/source/users/guides/baremetal.rst index 10e96561c..81421ea99 100644 --- a/doc/source/users/guides/bare_metal.rst +++ b/doc/source/users/guides/baremetal.rst @@ -1,7 +1,7 @@ -Using OpenStack Bare Metal +Using OpenStack Baremetal =========================== -Before working with the Bare Metal service, you'll need to create a +Before working with the Baremetal service, you'll need to create a connection to your OpenStack cloud by following the :doc:`connect` user guide. This will provide you with the ``conn`` variable used in the examples below. diff --git a/doc/source/users/guides/block_store.rst b/doc/source/users/guides/block_storage.rst similarity index 59% rename from doc/source/users/guides/block_store.rst rename to doc/source/users/guides/block_storage.rst index bb67a4eed..8f2661d09 100644 --- a/doc/source/users/guides/block_store.rst +++ b/doc/source/users/guides/block_storage.rst @@ -1,7 +1,7 @@ -Using OpenStack Block Store -=========================== +Using OpenStack Block Storage +============================= -Before working with the Block Store service, you'll need to create a +Before working with the Block Storage service, you'll need to create a connection to your OpenStack cloud by following the :doc:`connect` user guide. This will provide you with the ``conn`` variable used in the examples below. diff --git a/doc/source/users/guides/cluster.rst b/doc/source/users/guides/cluster.rst deleted file mode 100644 index d0a0474b8..000000000 --- a/doc/source/users/guides/cluster.rst +++ /dev/null @@ -1,36 +0,0 @@ -.. - 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. - - -======================= -Using OpenStack Cluster -======================= - -Before working with the Cluster service, you'll need to create a connection -to your OpenStack cloud by following the :doc:`connect` user guide. This will -provide you with the ``conn`` variable used by all examples in this guide. - -The primary abstractions/resources of the Cluster service are: - -.. toctree:: - :maxdepth: 1 - - Profile Type - Profile - Cluster - Node - Policy Type - Policy - Receiver - Action - Event diff --git a/doc/source/users/guides/clustering.rst b/doc/source/users/guides/clustering.rst new file mode 100644 index 000000000..c0543fd5b --- /dev/null +++ b/doc/source/users/guides/clustering.rst @@ -0,0 +1,37 @@ +.. + 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. + + +================================ +Using OpenStack Clustering +================================ + +Before working with the Clustering service, you'll need to create a +connection to your OpenStack cloud by following the :doc:`connect` user guide. +This will provide you with the ``conn`` variable used by all examples in this +guide. + +The primary abstractions/resources of the Clustering service are: + +.. toctree:: + :maxdepth: 1 + + Profile Type + Profile + Cluster + Node + Policy Type + Policy + Receiver + Action + Event diff --git a/doc/source/users/guides/cluster/action.rst b/doc/source/users/guides/clustering/action.rst similarity index 100% rename from doc/source/users/guides/cluster/action.rst rename to doc/source/users/guides/clustering/action.rst diff --git a/doc/source/users/guides/cluster/cluster.rst b/doc/source/users/guides/clustering/cluster.rst similarity index 100% rename from doc/source/users/guides/cluster/cluster.rst rename to doc/source/users/guides/clustering/cluster.rst diff --git a/doc/source/users/guides/cluster/event.rst b/doc/source/users/guides/clustering/event.rst similarity index 100% rename from doc/source/users/guides/cluster/event.rst rename to doc/source/users/guides/clustering/event.rst diff --git a/doc/source/users/guides/cluster/node.rst b/doc/source/users/guides/clustering/node.rst similarity index 100% rename from doc/source/users/guides/cluster/node.rst rename to doc/source/users/guides/clustering/node.rst diff --git a/doc/source/users/guides/cluster/policy.rst b/doc/source/users/guides/clustering/policy.rst similarity index 86% rename from doc/source/users/guides/cluster/policy.rst rename to doc/source/users/guides/clustering/policy.rst index c0995840c..07a8de9e9 100644 --- a/doc/source/users/guides/cluster/policy.rst +++ b/doc/source/users/guides/clustering/policy.rst @@ -26,7 +26,7 @@ List Policies To examine the list of policies: -.. literalinclude:: ../../examples/cluster/policy.py +.. literalinclude:: ../../examples/clustering/policy.py :pyobject: list_policies When listing policies, you can specify the sorting option using the ``sort`` @@ -42,7 +42,7 @@ Create Policy When creating a policy, you will provide a dictionary with keys and values according to the policy type referenced. -.. literalinclude:: ../../examples/cluster/policy.py +.. literalinclude:: ../../examples/clustering/policy.py :pyobject: create_policy Optionally, you can specify a ``metadata`` keyword argument that contains some @@ -56,7 +56,7 @@ Find Policy To find a policy based on its name or ID: -.. literalinclude:: ../../examples/cluster/policy.py +.. literalinclude:: ../../examples/clustering/policy.py :pyobject: find_policy Full example: `manage policy`_ @@ -67,7 +67,7 @@ Get Policy To get a policy based on its name or ID: -.. literalinclude:: ../../examples/cluster/policy.py +.. literalinclude:: ../../examples/clustering/policy.py :pyobject: get_policy Full example: `manage policy`_ @@ -79,7 +79,7 @@ Update Policy After a policy is created, most of its properties are immutable. Still, you can update a policy's ``name`` and/or ``metadata``. -.. literalinclude:: ../../examples/cluster/policy.py +.. literalinclude:: ../../examples/clustering/policy.py :pyobject: update_policy The Cluster service doesn't allow updating the ``spec`` of a policy. The only @@ -95,8 +95,8 @@ A policy can be deleted after creation, provided that it is not referenced by any active clusters or nodes. If you attempt to delete a policy that is still in use, you will get an error message. -.. literalinclude:: ../../examples/cluster/policy.py +.. literalinclude:: ../../examples/clustering/policy.py :pyobject: delete_policy -.. _manage policy: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/cluster/policy.py +.. _manage policy: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/clustering/policy.py diff --git a/doc/source/users/guides/cluster/policy_type.rst b/doc/source/users/guides/clustering/policy_type.rst similarity index 87% rename from doc/source/users/guides/cluster/policy_type.rst rename to doc/source/users/guides/clustering/policy_type.rst index 346ed7838..ceea4aefe 100644 --- a/doc/source/users/guides/cluster/policy_type.rst +++ b/doc/source/users/guides/clustering/policy_type.rst @@ -25,7 +25,7 @@ List Policy Types To examine the known policy types: -.. literalinclude:: ../../examples/cluster/policy_type.py +.. literalinclude:: ../../examples/clustering/policy_type.py :pyobject: list_policy_types Full example: `manage policy type`_ @@ -37,9 +37,9 @@ Get Policy Type To retrieve the details about a policy type, you need to provide the name of it. -.. literalinclude:: ../../examples/cluster/policy_type.py +.. literalinclude:: ../../examples/clustering/policy_type.py :pyobject: get_policy_type Full example: `manage policy type`_ -.. _manage policy type: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/cluster/policy_type.py +.. _manage policy type: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/clustering/policy_type.py diff --git a/doc/source/users/guides/cluster/profile.rst b/doc/source/users/guides/clustering/profile.rst similarity index 86% rename from doc/source/users/guides/cluster/profile.rst rename to doc/source/users/guides/clustering/profile.rst index ad3f5e5be..1c8d7f96a 100644 --- a/doc/source/users/guides/cluster/profile.rst +++ b/doc/source/users/guides/clustering/profile.rst @@ -26,7 +26,7 @@ List Profiles To examine the list of profiles: -.. literalinclude:: ../../examples/cluster/profile.py +.. literalinclude:: ../../examples/clustering/profile.py :pyobject: list_profiles When listing profiles, you can specify the sorting option using the ``sort`` @@ -42,7 +42,7 @@ Create Profile When creating a profile, you will provide a dictionary with keys and values specified according to the profile type referenced. -.. literalinclude:: ../../examples/cluster/profile.py +.. literalinclude:: ../../examples/clustering/profile.py :pyobject: create_profile Optionally, you can specify a ``metadata`` keyword argument that contains some @@ -56,7 +56,7 @@ Find Profile To find a profile based on its name or ID: -.. literalinclude:: ../../examples/cluster/profile.py +.. literalinclude:: ../../examples/clustering/profile.py :pyobject: find_profile The Cluster service doesn't allow updating the ``spec`` of a profile. The only @@ -70,7 +70,7 @@ Get Profile To get a profile based on its name or ID: -.. literalinclude:: ../../examples/cluster/profile.py +.. literalinclude:: ../../examples/clustering/profile.py :pyobject: get_profile Full example: `manage profile`_ @@ -82,7 +82,7 @@ Update Profile After a profile is created, most of its properties are immutable. Still, you can update a profile's ``name`` and/or ``metadata``. -.. literalinclude:: ../../examples/cluster/profile.py +.. literalinclude:: ../../examples/clustering/profile.py :pyobject: update_profile The Cluster service doesn't allow updating the ``spec`` of a profile. The only @@ -98,8 +98,8 @@ A profile can be deleted after creation, provided that it is not referenced by any active clusters or nodes. If you attempt to delete a profile that is still in use, you will get an error message. -.. literalinclude:: ../../examples/cluster/profile.py +.. literalinclude:: ../../examples/clustering/profile.py :pyobject: delete_profile -.. _manage profile: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/cluster/profile.py +.. _manage profile: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/clustering/profile.py diff --git a/doc/source/users/guides/cluster/profile_type.rst b/doc/source/users/guides/clustering/profile_type.rst similarity index 86% rename from doc/source/users/guides/cluster/profile_type.rst rename to doc/source/users/guides/clustering/profile_type.rst index 45183bf0a..b8a3fae35 100644 --- a/doc/source/users/guides/cluster/profile_type.rst +++ b/doc/source/users/guides/clustering/profile_type.rst @@ -25,7 +25,7 @@ List Profile Types To examine the known profile types: -.. literalinclude:: ../../examples/cluster/profile_type.py +.. literalinclude:: ../../examples/clustering/profile_type.py :pyobject: list_profile_types Full example: `manage profile type`_ @@ -36,9 +36,9 @@ Get Profile Type To get the details about a profile type, you need to provide the name of it. -.. literalinclude:: ../../examples/cluster/profile_type.py +.. literalinclude:: ../../examples/clustering/profile_type.py :pyobject: get_profile_type Full example: `manage profile type`_ -.. _manage profile type: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/cluster/profile_type.py +.. _manage profile type: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/clustering/profile_type.py diff --git a/doc/source/users/guides/cluster/receiver.rst b/doc/source/users/guides/clustering/receiver.rst similarity index 100% rename from doc/source/users/guides/cluster/receiver.rst rename to doc/source/users/guides/clustering/receiver.rst diff --git a/doc/source/users/guides/connect.rst b/doc/source/users/guides/connect.rst index 6c9937d9c..51be68bd6 100644 --- a/doc/source/users/guides/connect.rst +++ b/doc/source/users/guides/connect.rst @@ -4,29 +4,20 @@ Connect In order to work with an OpenStack cloud you first need to create a :class:`~openstack.connection.Connection` to it using your credentials. A :class:`~openstack.connection.Connection` can be -created in 3 ways, using the class itself, a file, or environment variables. -If this is your first time using the SDK, we recommend simply using the -class itself as illustrated below. +created in 3 ways, using the class itself, :ref:`config-clouds-yaml`, or +:ref:`config-environment-variables`. It is recommended to always use +:ref:`config-clouds-yaml` as the same config can be used across tools and +languages. Create Connection ----------------- -To create a connection you need a :class:`~openstack.profile.Profile` and a -:class:`~openstack.connection.Connection`. +To create a :class:`~openstack.connection.Connection` instance, use the +:func:`~openstack.connect` factory function. .. literalinclude:: ../examples/connect.py :pyobject: create_connection -The :class:`~openstack.profile.Profile` sets your preferences for each -service. You will pass it the region of the OpenStack cloud that this -connection will use. - -The :class:`~openstack.connection.Connection` is a context for a connection -to an OpenStack cloud. You will primarily use it to set the -:class:`~openstack.profile.Profile` and authentication information. You can -also set the ``user_agent`` to something that describes your application -(e.g. ``my-web-app/1.3.4``). - Full example at `connect.py `_ .. note:: To enable logging, see the :doc:`logging` user guide. @@ -37,5 +28,9 @@ Now that you can create a connection, continue with the :ref:`user_guides` to work with an OpenStack service. As an alternative to creating a :class:`~openstack.connection.Connection` -using the class itself, you can connect using a file or environment -variables. See the :doc:`connect_from_config` user guide. +using :ref:config-clouds-yaml, you can connect using +`config-environment-variables`. + +.. TODO(shade) Update the text here and consolidate with the old + os-client-config docs so that we have a single and consistent explanation + of the envvars cloud, etc. diff --git a/doc/source/users/guides/telemetry.rst b/doc/source/users/guides/meter.rst similarity index 73% rename from doc/source/users/guides/telemetry.rst rename to doc/source/users/guides/meter.rst index cf7acae04..a6850e282 100644 --- a/doc/source/users/guides/telemetry.rst +++ b/doc/source/users/guides/meter.rst @@ -1,10 +1,10 @@ -Using OpenStack Telemetry +Using OpenStack Meter ========================= .. caution:: BETA: This API is a work in progress and is subject to change. -Before working with the Telemetry service, you'll need to create a connection +Before working with the Meter service, you'll need to create a connection to your OpenStack cloud by following the :doc:`connect` user guide. This will provide you with the ``conn`` variable used in the examples below. diff --git a/doc/source/users/index.rst b/doc/source/users/index.rst index c06b98577..218d172ef 100644 --- a/doc/source/users/index.rst +++ b/doc/source/users/index.rst @@ -28,19 +28,19 @@ approach, this is where you'll want to begin. Connect to an OpenStack Cloud Connect to an OpenStack Cloud Using a Config File Logging - Bare Metal - Block Store - Cluster + Baremetal + Block Storage + Clustering Compute Database Identity Image Key Manager Message + Meter Network Object Store Orchestration - Telemetry API Documentation ----------------- @@ -54,26 +54,26 @@ interface is the layer upon which the *Connection* is built, with Connection Interface ******************** -A *Connection* instance maintains your session, authentication, transport, -and profile, providing you with a set of higher-level interfaces to work -with OpenStack services. +A *Connection* instance maintains your cloud config, session and authentication +information providing you with a set of higher-level interfaces to work with +OpenStack services. .. toctree:: :maxdepth: 1 connection - profile Once you have a *Connection* instance, the following services may be exposed -to you. Your user profile determine the full set of exposed services, -but listed below are the ones provided by this SDK by default. +to you. The combination of your ``CloudConfig`` and the catalog of the cloud +in question control which services are exposed, but listed below are the ones +provided by the SDK. .. toctree:: :maxdepth: 1 - Bare Metal - Block Store - Cluster + Baremetal + Block Storage + Clustering Compute Database Identity v2 @@ -85,10 +85,10 @@ but listed below are the ones provided by this SDK by default. Message v1 Message v2 Network + Meter Metric Object Store Orchestration - Telemetry Workflow Resource Interface @@ -106,20 +106,20 @@ The following services have exposed *Resource* classes. .. toctree:: :maxdepth: 1 - Bare Metal - Block Store - Cluster + Baremetal + Block Storage + Clustering Compute Database Identity Image Key Management Load Balancer + Meter Metric Network Orchestration Object Store - Telemetry Workflow Low-Level Classes @@ -133,7 +133,6 @@ can be customized. .. toctree:: :maxdepth: 1 - session resource resource2 service_filter diff --git a/doc/source/users/profile.rst b/doc/source/users/profile.rst deleted file mode 100644 index 195a1848c..000000000 --- a/doc/source/users/profile.rst +++ /dev/null @@ -1,9 +0,0 @@ -Profile -======= -.. automodule:: openstack.profile - -Profile Object --------------- - -.. autoclass:: openstack.profile.Profile - :members: diff --git a/doc/source/users/proxies/bare_metal.rst b/doc/source/users/proxies/bare_metal.rst deleted file mode 100644 index 8317b86be..000000000 --- a/doc/source/users/proxies/bare_metal.rst +++ /dev/null @@ -1,76 +0,0 @@ -Bare Metal API -============== - -For details on how to use bare_metal, see :doc:`/users/guides/bare_metal` - -.. automodule:: openstack.bare_metal.v1._proxy - -The BareMetal Class --------------------- - -The bare_metal high-level interface is available through the ``bare_metal`` -member of a :class:`~openstack.connection.Connection` object. -The ``bare_metal`` member will only be added if the service is detected. - -Node Operations -^^^^^^^^^^^^^^^ -.. autoclass:: openstack.bare_metal.v1._proxy.Proxy - - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.create_node - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.update_node - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.delete_node - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.get_node - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.find_node - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.nodes - -Port Operations -^^^^^^^^^^^^^^^ -.. autoclass:: openstack.bare_metal.v1._proxy.Proxy - - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.create_port - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.update_port - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.delete_port - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.get_port - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.find_port - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.ports - -Port Group Operations -^^^^^^^^^^^^^^^^^^^^^ -.. autoclass:: openstack.bare_metal.v1._proxy.Proxy - - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.create_port_group - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.update_port_group - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.delete_port_group - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.get_port_group - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.find_port_group - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.port_groups - -Driver Operations -^^^^^^^^^^^^^^^^^ -.. autoclass:: openstack.bare_metal.v1._proxy.Proxy - - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.drivers - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.get_driver - -Chassis Operations -^^^^^^^^^^^^^^^^^^ -.. autoclass:: openstack.bare_metal.v1._proxy.Proxy - - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.create_chassis - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.update_chassis - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.delete_chassis - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.get_chassis - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.find_chassis - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.chassis - -Deprecated Methods -^^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.bare_metal.v1._proxy.Proxy - - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.create_portgroup - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.update_portgroup - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.delete_portgroup - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.get_portgroup - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.find_portgroup - .. automethod:: openstack.bare_metal.v1._proxy.Proxy.portgroups diff --git a/doc/source/users/proxies/baremetal.rst b/doc/source/users/proxies/baremetal.rst new file mode 100644 index 000000000..5ca777b2a --- /dev/null +++ b/doc/source/users/proxies/baremetal.rst @@ -0,0 +1,76 @@ +Baremetal API +============== + +For details on how to use baremetal, see :doc:`/users/guides/baremetal` + +.. automodule:: openstack.baremetal.v1._proxy + +The Baremetal Class +-------------------- + +The baremetal high-level interface is available through the ``baremetal`` +member of a :class:`~openstack.connection.Connection` object. +The ``baremetal`` member will only be added if the service is detected. + +Node Operations +^^^^^^^^^^^^^^^ +.. autoclass:: openstack.baremetal.v1._proxy.Proxy + + .. automethod:: openstack.baremetal.v1._proxy.Proxy.create_node + .. automethod:: openstack.baremetal.v1._proxy.Proxy.update_node + .. automethod:: openstack.baremetal.v1._proxy.Proxy.delete_node + .. automethod:: openstack.baremetal.v1._proxy.Proxy.get_node + .. automethod:: openstack.baremetal.v1._proxy.Proxy.find_node + .. automethod:: openstack.baremetal.v1._proxy.Proxy.nodes + +Port Operations +^^^^^^^^^^^^^^^ +.. autoclass:: openstack.baremetal.v1._proxy.Proxy + + .. automethod:: openstack.baremetal.v1._proxy.Proxy.create_port + .. automethod:: openstack.baremetal.v1._proxy.Proxy.update_port + .. automethod:: openstack.baremetal.v1._proxy.Proxy.delete_port + .. automethod:: openstack.baremetal.v1._proxy.Proxy.get_port + .. automethod:: openstack.baremetal.v1._proxy.Proxy.find_port + .. automethod:: openstack.baremetal.v1._proxy.Proxy.ports + +Port Group Operations +^^^^^^^^^^^^^^^^^^^^^ +.. autoclass:: openstack.baremetal.v1._proxy.Proxy + + .. automethod:: openstack.baremetal.v1._proxy.Proxy.create_port_group + .. automethod:: openstack.baremetal.v1._proxy.Proxy.update_port_group + .. automethod:: openstack.baremetal.v1._proxy.Proxy.delete_port_group + .. automethod:: openstack.baremetal.v1._proxy.Proxy.get_port_group + .. automethod:: openstack.baremetal.v1._proxy.Proxy.find_port_group + .. automethod:: openstack.baremetal.v1._proxy.Proxy.port_groups + +Driver Operations +^^^^^^^^^^^^^^^^^ +.. autoclass:: openstack.baremetal.v1._proxy.Proxy + + .. automethod:: openstack.baremetal.v1._proxy.Proxy.drivers + .. automethod:: openstack.baremetal.v1._proxy.Proxy.get_driver + +Chassis Operations +^^^^^^^^^^^^^^^^^^ +.. autoclass:: openstack.baremetal.v1._proxy.Proxy + + .. automethod:: openstack.baremetal.v1._proxy.Proxy.create_chassis + .. automethod:: openstack.baremetal.v1._proxy.Proxy.update_chassis + .. automethod:: openstack.baremetal.v1._proxy.Proxy.delete_chassis + .. automethod:: openstack.baremetal.v1._proxy.Proxy.get_chassis + .. automethod:: openstack.baremetal.v1._proxy.Proxy.find_chassis + .. automethod:: openstack.baremetal.v1._proxy.Proxy.chassis + +Deprecated Methods +^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.baremetal.v1._proxy.Proxy + + .. automethod:: openstack.baremetal.v1._proxy.Proxy.create_portgroup + .. automethod:: openstack.baremetal.v1._proxy.Proxy.update_portgroup + .. automethod:: openstack.baremetal.v1._proxy.Proxy.delete_portgroup + .. automethod:: openstack.baremetal.v1._proxy.Proxy.get_portgroup + .. automethod:: openstack.baremetal.v1._proxy.Proxy.find_portgroup + .. automethod:: openstack.baremetal.v1._proxy.Proxy.portgroups diff --git a/doc/source/users/proxies/block_storage.rst b/doc/source/users/proxies/block_storage.rst new file mode 100644 index 000000000..460624151 --- /dev/null +++ b/doc/source/users/proxies/block_storage.rst @@ -0,0 +1,43 @@ +Block Storage API +================= + +For details on how to use block_storage, see :doc:`/users/guides/block_storage` + +.. automodule:: openstack.block_storage.v2._proxy + +The BlockStorage Class +---------------------- + +The block_storage high-level interface is available through the +``block_storage`` member of a :class:`~openstack.connection.Connection` object. +The ``block_storage`` member will only be added if the service is detected. + +Volume Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v2._proxy.Proxy + + .. automethod:: openstack.block_storage.v2._proxy.Proxy.create_volume + .. automethod:: openstack.block_storage.v2._proxy.Proxy.delete_volume + .. automethod:: openstack.block_storage.v2._proxy.Proxy.get_volume + .. automethod:: openstack.block_storage.v2._proxy.Proxy.volumes + +Type Operations +^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v2._proxy.Proxy + + .. automethod:: openstack.block_storage.v2._proxy.Proxy.create_type + .. automethod:: openstack.block_storage.v2._proxy.Proxy.delete_type + .. automethod:: openstack.block_storage.v2._proxy.Proxy.get_type + .. automethod:: openstack.block_storage.v2._proxy.Proxy.types + +Snapshot Operations +^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v2._proxy.Proxy + + .. automethod:: openstack.block_storage.v2._proxy.Proxy.create_snapshot + .. automethod:: openstack.block_storage.v2._proxy.Proxy.delete_snapshot + .. automethod:: openstack.block_storage.v2._proxy.Proxy.get_snapshot + .. automethod:: openstack.block_storage.v2._proxy.Proxy.snapshots diff --git a/doc/source/users/proxies/block_store.rst b/doc/source/users/proxies/block_store.rst deleted file mode 100644 index 92c12cd15..000000000 --- a/doc/source/users/proxies/block_store.rst +++ /dev/null @@ -1,43 +0,0 @@ -Block Store API -=============== - -For details on how to use block_store, see :doc:`/users/guides/block_store` - -.. automodule:: openstack.block_store.v2._proxy - -The BlockStore Class --------------------- - -The block_store high-level interface is available through the ``block_store`` -member of a :class:`~openstack.connection.Connection` object. -The ``block_store`` member will only be added if the service is detected. - -Volume Operations -^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.block_store.v2._proxy.Proxy - - .. automethod:: openstack.block_store.v2._proxy.Proxy.create_volume - .. automethod:: openstack.block_store.v2._proxy.Proxy.delete_volume - .. automethod:: openstack.block_store.v2._proxy.Proxy.get_volume - .. automethod:: openstack.block_store.v2._proxy.Proxy.volumes - -Type Operations -^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.block_store.v2._proxy.Proxy - - .. automethod:: openstack.block_store.v2._proxy.Proxy.create_type - .. automethod:: openstack.block_store.v2._proxy.Proxy.delete_type - .. automethod:: openstack.block_store.v2._proxy.Proxy.get_type - .. automethod:: openstack.block_store.v2._proxy.Proxy.types - -Snapshot Operations -^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.block_store.v2._proxy.Proxy - - .. automethod:: openstack.block_store.v2._proxy.Proxy.create_snapshot - .. automethod:: openstack.block_store.v2._proxy.Proxy.delete_snapshot - .. automethod:: openstack.block_store.v2._proxy.Proxy.get_snapshot - .. automethod:: openstack.block_store.v2._proxy.Proxy.snapshots diff --git a/doc/source/users/proxies/cluster.rst b/doc/source/users/proxies/cluster.rst deleted file mode 100644 index db7eec004..000000000 --- a/doc/source/users/proxies/cluster.rst +++ /dev/null @@ -1,177 +0,0 @@ -Cluster API -=========== - -.. automodule:: openstack.cluster.v1._proxy - -The Cluster Class ------------------ - -The cluster high-level interface is available through the ``cluster`` -member of a :class:`~openstack.connection.Connection` object. The -``cluster`` member will only be added if the service is detected. - - -Build Info Operations -^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.cluster.v1._proxy.Proxy - - .. automethod:: openstack.cluster.v1._proxy.Proxy.get_build_info - - -Profile Type Operations -^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.cluster.v1._proxy.Proxy - - .. automethod:: openstack.cluster.v1._proxy.Proxy.profile_types - .. automethod:: openstack.cluster.v1._proxy.Proxy.get_profile_type - - -Profile Operations -^^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.cluster.v1._proxy.Proxy - - .. automethod:: openstack.cluster.v1._proxy.Proxy.create_profile - .. automethod:: openstack.cluster.v1._proxy.Proxy.update_profile - .. automethod:: openstack.cluster.v1._proxy.Proxy.delete_profile - .. automethod:: openstack.cluster.v1._proxy.Proxy.get_profile - .. automethod:: openstack.cluster.v1._proxy.Proxy.find_profile - .. automethod:: openstack.cluster.v1._proxy.Proxy.profiles - - .. automethod:: openstack.cluster.v1._proxy.Proxy.validate_profile - - -Policy Type Operations -^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.cluster.v1._proxy.Proxy - - .. automethod:: openstack.cluster.v1._proxy.Proxy.policy_types - .. automethod:: openstack.cluster.v1._proxy.Proxy.get_policy_type - - -Policy Operations -^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.cluster.v1._proxy.Proxy - - .. automethod:: openstack.cluster.v1._proxy.Proxy.create_policy - .. automethod:: openstack.cluster.v1._proxy.Proxy.update_policy - .. automethod:: openstack.cluster.v1._proxy.Proxy.delete_policy - .. automethod:: openstack.cluster.v1._proxy.Proxy.get_policy - .. automethod:: openstack.cluster.v1._proxy.Proxy.find_policy - .. automethod:: openstack.cluster.v1._proxy.Proxy.policies - - .. automethod:: openstack.cluster.v1._proxy.Proxy.validate_policy - - -Cluster Operations -^^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.cluster.v1._proxy.Proxy - - .. automethod:: openstack.cluster.v1._proxy.Proxy.create_cluster - .. automethod:: openstack.cluster.v1._proxy.Proxy.update_cluster - .. automethod:: openstack.cluster.v1._proxy.Proxy.delete_cluster - .. automethod:: openstack.cluster.v1._proxy.Proxy.get_cluster - .. automethod:: openstack.cluster.v1._proxy.Proxy.find_cluster - .. automethod:: openstack.cluster.v1._proxy.Proxy.clusters - - .. automethod:: openstack.cluster.v1._proxy.Proxy.check_cluster - .. automethod:: openstack.cluster.v1._proxy.Proxy.recover_cluster - .. automethod:: openstack.cluster.v1._proxy.Proxy.resize_cluster - .. automethod:: openstack.cluster.v1._proxy.Proxy.scale_in_cluster - .. automethod:: openstack.cluster.v1._proxy.Proxy.scale_out_cluster - .. automethod:: openstack.cluster.v1._proxy.Proxy.collect_cluster_attrs - .. automethod:: openstack.cluster.v1._proxy.Proxy.perform_operation_on_cluster - - .. automethod:: openstack.cluster.v1._proxy.Proxy.add_nodes_to_cluster - .. automethod:: openstack.cluster.v1._proxy.Proxy.remove_nodes_from_cluster - .. automethod:: openstack.cluster.v1._proxy.Proxy.replace_nodes_in_cluster - - .. automethod:: openstack.cluster.v1._proxy.Proxy.attach_policy_to_cluster - .. automethod:: openstack.cluster.v1._proxy.Proxy.update_cluster_policy - .. automethod:: openstack.cluster.v1._proxy.Proxy.detach_policy_from_cluster - .. automethod:: openstack.cluster.v1._proxy.Proxy.get_cluster_policy - .. automethod:: openstack.cluster.v1._proxy.Proxy.cluster_policies - - .. automethod:: openstack.cluster.v1._proxy.Proxy.cluster_add_nodes - .. automethod:: openstack.cluster.v1._proxy.Proxy.cluster_attach_policy - .. automethod:: openstack.cluster.v1._proxy.Proxy.cluster_del_nodes - .. automethod:: openstack.cluster.v1._proxy.Proxy.cluster_detach_policy - .. automethod:: openstack.cluster.v1._proxy.Proxy.cluster_operation - .. automethod:: openstack.cluster.v1._proxy.Proxy.cluster_replace_nodes - .. automethod:: openstack.cluster.v1._proxy.Proxy.cluster_resize - .. automethod:: openstack.cluster.v1._proxy.Proxy.cluster_scale_in - .. automethod:: openstack.cluster.v1._proxy.Proxy.cluster_scale_out - .. automethod:: openstack.cluster.v1._proxy.Proxy.cluster_update_policy - - -Node Operations -^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.cluster.v1._proxy.Proxy - - .. automethod:: openstack.cluster.v1._proxy.Proxy.create_node - .. automethod:: openstack.cluster.v1._proxy.Proxy.update_node - .. automethod:: openstack.cluster.v1._proxy.Proxy.delete_node - .. automethod:: openstack.cluster.v1._proxy.Proxy.get_node - .. automethod:: openstack.cluster.v1._proxy.Proxy.find_node - .. automethod:: openstack.cluster.v1._proxy.Proxy.nodes - - .. automethod:: openstack.cluster.v1._proxy.Proxy.check_node - .. automethod:: openstack.cluster.v1._proxy.Proxy.recover_node - .. automethod:: openstack.cluster.v1._proxy.Proxy.perform_operation_on_node - - .. automethod:: openstack.cluster.v1._proxy.Proxy.adopt_node - .. automethod:: openstack.cluster.v1._proxy.Proxy.node_operation - - -Receiver Operations -^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.cluster.v1._proxy.Proxy - - .. automethod:: openstack.cluster.v1._proxy.Proxy.create_receiver - .. automethod:: openstack.cluster.v1._proxy.Proxy.update_receiver - .. automethod:: openstack.cluster.v1._proxy.Proxy.delete_receiver - .. automethod:: openstack.cluster.v1._proxy.Proxy.get_receiver - .. automethod:: openstack.cluster.v1._proxy.Proxy.find_receiver - .. automethod:: openstack.cluster.v1._proxy.Proxy.receivers - - -Action Operations -^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.cluster.v1._proxy.Proxy - - .. automethod:: openstack.cluster.v1._proxy.Proxy.get_action - .. automethod:: openstack.cluster.v1._proxy.Proxy.actions - - -Event Operations -^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.cluster.v1._proxy.Proxy - - .. automethod:: openstack.cluster.v1._proxy.Proxy.get_event - .. automethod:: openstack.cluster.v1._proxy.Proxy.events - - -Helper Operations -^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.cluster.v1._proxy.Proxy - - .. automethod:: openstack.cluster.v1._proxy.Proxy.wait_for_delete - .. automethod:: openstack.cluster.v1._proxy.Proxy.wait_for_status - - -Service Operations -^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.cluster.v1._proxy.Proxy - - .. automethod:: openstack.cluster.v1._proxy.Proxy.services diff --git a/doc/source/users/proxies/clustering.rst b/doc/source/users/proxies/clustering.rst new file mode 100644 index 000000000..ff8b5c380 --- /dev/null +++ b/doc/source/users/proxies/clustering.rst @@ -0,0 +1,177 @@ +Cluster API +=========== + +.. automodule:: openstack.clustering.v1._proxy + +The Cluster Class +----------------- + +The cluster high-level interface is available through the ``cluster`` +member of a :class:`~openstack.connection.Connection` object. The +``cluster`` member will only be added if the service is detected. + + +Build Info Operations +^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.clustering.v1._proxy.Proxy + + .. automethod:: openstack.clustering.v1._proxy.Proxy.get_build_info + + +Profile Type Operations +^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.clustering.v1._proxy.Proxy + + .. automethod:: openstack.clustering.v1._proxy.Proxy.profile_types + .. automethod:: openstack.clustering.v1._proxy.Proxy.get_profile_type + + +Profile Operations +^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.clustering.v1._proxy.Proxy + + .. automethod:: openstack.clustering.v1._proxy.Proxy.create_profile + .. automethod:: openstack.clustering.v1._proxy.Proxy.update_profile + .. automethod:: openstack.clustering.v1._proxy.Proxy.delete_profile + .. automethod:: openstack.clustering.v1._proxy.Proxy.get_profile + .. automethod:: openstack.clustering.v1._proxy.Proxy.find_profile + .. automethod:: openstack.clustering.v1._proxy.Proxy.profiles + + .. automethod:: openstack.clustering.v1._proxy.Proxy.validate_profile + + +Policy Type Operations +^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.clustering.v1._proxy.Proxy + + .. automethod:: openstack.clustering.v1._proxy.Proxy.policy_types + .. automethod:: openstack.clustering.v1._proxy.Proxy.get_policy_type + + +Policy Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.clustering.v1._proxy.Proxy + + .. automethod:: openstack.clustering.v1._proxy.Proxy.create_policy + .. automethod:: openstack.clustering.v1._proxy.Proxy.update_policy + .. automethod:: openstack.clustering.v1._proxy.Proxy.delete_policy + .. automethod:: openstack.clustering.v1._proxy.Proxy.get_policy + .. automethod:: openstack.clustering.v1._proxy.Proxy.find_policy + .. automethod:: openstack.clustering.v1._proxy.Proxy.policies + + .. automethod:: openstack.clustering.v1._proxy.Proxy.validate_policy + + +Cluster Operations +^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.clustering.v1._proxy.Proxy + + .. automethod:: openstack.clustering.v1._proxy.Proxy.create_cluster + .. automethod:: openstack.clustering.v1._proxy.Proxy.update_cluster + .. automethod:: openstack.clustering.v1._proxy.Proxy.delete_cluster + .. automethod:: openstack.clustering.v1._proxy.Proxy.get_cluster + .. automethod:: openstack.clustering.v1._proxy.Proxy.find_cluster + .. automethod:: openstack.clustering.v1._proxy.Proxy.clusters + + .. automethod:: openstack.clustering.v1._proxy.Proxy.check_cluster + .. automethod:: openstack.clustering.v1._proxy.Proxy.recover_cluster + .. automethod:: openstack.clustering.v1._proxy.Proxy.resize_cluster + .. automethod:: openstack.clustering.v1._proxy.Proxy.scale_in_cluster + .. automethod:: openstack.clustering.v1._proxy.Proxy.scale_out_cluster + .. automethod:: openstack.clustering.v1._proxy.Proxy.collect_cluster_attrs + .. automethod:: openstack.clustering.v1._proxy.Proxy.perform_operation_on_cluster + + .. automethod:: openstack.clustering.v1._proxy.Proxy.add_nodes_to_cluster + .. automethod:: openstack.clustering.v1._proxy.Proxy.remove_nodes_from_cluster + .. automethod:: openstack.clustering.v1._proxy.Proxy.replace_nodes_in_cluster + + .. automethod:: openstack.clustering.v1._proxy.Proxy.attach_policy_to_cluster + .. automethod:: openstack.clustering.v1._proxy.Proxy.update_cluster_policy + .. automethod:: openstack.clustering.v1._proxy.Proxy.detach_policy_from_cluster + .. automethod:: openstack.clustering.v1._proxy.Proxy.get_cluster_policy + .. automethod:: openstack.clustering.v1._proxy.Proxy.cluster_policies + + .. automethod:: openstack.clustering.v1._proxy.Proxy.cluster_add_nodes + .. automethod:: openstack.clustering.v1._proxy.Proxy.cluster_attach_policy + .. automethod:: openstack.clustering.v1._proxy.Proxy.cluster_del_nodes + .. automethod:: openstack.clustering.v1._proxy.Proxy.cluster_detach_policy + .. automethod:: openstack.clustering.v1._proxy.Proxy.cluster_operation + .. automethod:: openstack.clustering.v1._proxy.Proxy.cluster_replace_nodes + .. automethod:: openstack.clustering.v1._proxy.Proxy.cluster_resize + .. automethod:: openstack.clustering.v1._proxy.Proxy.cluster_scale_in + .. automethod:: openstack.clustering.v1._proxy.Proxy.cluster_scale_out + .. automethod:: openstack.clustering.v1._proxy.Proxy.cluster_update_policy + + +Node Operations +^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.clustering.v1._proxy.Proxy + + .. automethod:: openstack.clustering.v1._proxy.Proxy.create_node + .. automethod:: openstack.clustering.v1._proxy.Proxy.update_node + .. automethod:: openstack.clustering.v1._proxy.Proxy.delete_node + .. automethod:: openstack.clustering.v1._proxy.Proxy.get_node + .. automethod:: openstack.clustering.v1._proxy.Proxy.find_node + .. automethod:: openstack.clustering.v1._proxy.Proxy.nodes + + .. automethod:: openstack.clustering.v1._proxy.Proxy.check_node + .. automethod:: openstack.clustering.v1._proxy.Proxy.recover_node + .. automethod:: openstack.clustering.v1._proxy.Proxy.perform_operation_on_node + + .. automethod:: openstack.clustering.v1._proxy.Proxy.adopt_node + .. automethod:: openstack.clustering.v1._proxy.Proxy.node_operation + + +Receiver Operations +^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.clustering.v1._proxy.Proxy + + .. automethod:: openstack.clustering.v1._proxy.Proxy.create_receiver + .. automethod:: openstack.clustering.v1._proxy.Proxy.update_receiver + .. automethod:: openstack.clustering.v1._proxy.Proxy.delete_receiver + .. automethod:: openstack.clustering.v1._proxy.Proxy.get_receiver + .. automethod:: openstack.clustering.v1._proxy.Proxy.find_receiver + .. automethod:: openstack.clustering.v1._proxy.Proxy.receivers + + +Action Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.clustering.v1._proxy.Proxy + + .. automethod:: openstack.clustering.v1._proxy.Proxy.get_action + .. automethod:: openstack.clustering.v1._proxy.Proxy.actions + + +Event Operations +^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.clustering.v1._proxy.Proxy + + .. automethod:: openstack.clustering.v1._proxy.Proxy.get_event + .. automethod:: openstack.clustering.v1._proxy.Proxy.events + + +Helper Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.clustering.v1._proxy.Proxy + + .. automethod:: openstack.clustering.v1._proxy.Proxy.wait_for_delete + .. automethod:: openstack.clustering.v1._proxy.Proxy.wait_for_status + + +Service Operations +^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.clustering.v1._proxy.Proxy + + .. automethod:: openstack.clustering.v1._proxy.Proxy.services diff --git a/doc/source/users/proxies/meter.rst b/doc/source/users/proxies/meter.rst new file mode 100644 index 000000000..a08e6f211 --- /dev/null +++ b/doc/source/users/proxies/meter.rst @@ -0,0 +1,85 @@ +Meter API +============= + +.. caution:: + BETA: This API is a work in progress and is subject to change. + +For details on how to use meter, see :doc:`/users/guides/meter` + +.. automodule:: openstack.meter.v2._proxy + +The Meter Class +------------------- + +The meter high-level interface is available through the ``meter`` +member of a :class:`~openstack.connection.Connection` object. The +``meter`` member will only be added if the service is detected. + +Sample Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.meter.v2._proxy.Proxy + + .. automethod:: openstack.meter.v2._proxy.Proxy.find_sample + .. automethod:: openstack.meter.v2._proxy.Proxy.samples + +Statistic Operations +^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.meter.v2._proxy.Proxy + + .. automethod:: openstack.meter.v2._proxy.Proxy.find_statistics + .. automethod:: openstack.meter.v2._proxy.Proxy.statistics + +Resource Operations +^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.meter.v2._proxy.Proxy + + .. automethod:: openstack.meter.v2._proxy.Proxy.get_resource + .. automethod:: openstack.meter.v2._proxy.Proxy.find_resource + .. automethod:: openstack.meter.v2._proxy.Proxy.resources + +Meter Operations +^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.meter.v2._proxy.Proxy + + .. automethod:: openstack.meter.v2._proxy.Proxy.find_meter + .. automethod:: openstack.meter.v2._proxy.Proxy.meters + +Capability Operations +^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.meter.v2._proxy.Proxy + + .. automethod:: openstack.meter.v2._proxy.Proxy.find_capability + .. automethod:: openstack.meter.v2._proxy.Proxy.capabilities + +The Alarm Class +--------------- + +The alarm high-level interface is available through the ``meter.alarm`` +member of a :class:`~openstack.connection.Connection` object. The +``meter.alarm`` member will only be added if the service is detected. + +Alarm Operations +^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.meter.alarm.v2._proxy.Proxy + + .. automethod:: openstack.meter.alarm.v2._proxy.Proxy.create_alarm + .. automethod:: openstack.meter.alarm.v2._proxy.Proxy.update_alarm + .. automethod:: openstack.meter.alarm.v2._proxy.Proxy.delete_alarm + .. automethod:: openstack.meter.alarm.v2._proxy.Proxy.get_alarm + .. automethod:: openstack.meter.alarm.v2._proxy.Proxy.find_alarm + .. automethod:: openstack.meter.alarm.v2._proxy.Proxy.alarms + + +Alarm Change Operations +^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.meter.alarm.v2._proxy.Proxy + + .. automethod:: openstack.meter.alarm.v2._proxy.Proxy.find_alarm_change + .. automethod:: openstack.meter.alarm.v2._proxy.Proxy.alarm_changes diff --git a/doc/source/users/proxies/telemetry.rst b/doc/source/users/proxies/telemetry.rst deleted file mode 100644 index fc29a13a4..000000000 --- a/doc/source/users/proxies/telemetry.rst +++ /dev/null @@ -1,85 +0,0 @@ -Telemetry API -============= - -.. caution:: - BETA: This API is a work in progress and is subject to change. - -For details on how to use telemetry, see :doc:`/users/guides/telemetry` - -.. automodule:: openstack.telemetry.v2._proxy - -The Telemetry Class -------------------- - -The telemetry high-level interface is available through the ``telemetry`` -member of a :class:`~openstack.connection.Connection` object. The -``telemetry`` member will only be added if the service is detected. - -Sample Operations -^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.telemetry.v2._proxy.Proxy - - .. automethod:: openstack.telemetry.v2._proxy.Proxy.find_sample - .. automethod:: openstack.telemetry.v2._proxy.Proxy.samples - -Statistic Operations -^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.telemetry.v2._proxy.Proxy - - .. automethod:: openstack.telemetry.v2._proxy.Proxy.find_statistics - .. automethod:: openstack.telemetry.v2._proxy.Proxy.statistics - -Resource Operations -^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.telemetry.v2._proxy.Proxy - - .. automethod:: openstack.telemetry.v2._proxy.Proxy.get_resource - .. automethod:: openstack.telemetry.v2._proxy.Proxy.find_resource - .. automethod:: openstack.telemetry.v2._proxy.Proxy.resources - -Meter Operations -^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.telemetry.v2._proxy.Proxy - - .. automethod:: openstack.telemetry.v2._proxy.Proxy.find_meter - .. automethod:: openstack.telemetry.v2._proxy.Proxy.meters - -Capability Operations -^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.telemetry.v2._proxy.Proxy - - .. automethod:: openstack.telemetry.v2._proxy.Proxy.find_capability - .. automethod:: openstack.telemetry.v2._proxy.Proxy.capabilities - -The Alarm Class ---------------- - -The alarm high-level interface is available through the ``telemetry.alarm`` -member of a :class:`~openstack.connection.Connection` object. The -``telemetry.alarm`` member will only be added if the service is detected. - -Alarm Operations -^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.telemetry.alarm.v2._proxy.Proxy - - .. automethod:: openstack.telemetry.alarm.v2._proxy.Proxy.create_alarm - .. automethod:: openstack.telemetry.alarm.v2._proxy.Proxy.update_alarm - .. automethod:: openstack.telemetry.alarm.v2._proxy.Proxy.delete_alarm - .. automethod:: openstack.telemetry.alarm.v2._proxy.Proxy.get_alarm - .. automethod:: openstack.telemetry.alarm.v2._proxy.Proxy.find_alarm - .. automethod:: openstack.telemetry.alarm.v2._proxy.Proxy.alarms - - -Alarm Change Operations -^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.telemetry.alarm.v2._proxy.Proxy - - .. automethod:: openstack.telemetry.alarm.v2._proxy.Proxy.find_alarm_change - .. automethod:: openstack.telemetry.alarm.v2._proxy.Proxy.alarm_changes diff --git a/doc/source/users/resources/bare_metal/index.rst b/doc/source/users/resources/baremetal/index.rst similarity index 85% rename from doc/source/users/resources/bare_metal/index.rst rename to doc/source/users/resources/baremetal/index.rst index 348a6e4f6..c8025248b 100644 --- a/doc/source/users/resources/bare_metal/index.rst +++ b/doc/source/users/resources/baremetal/index.rst @@ -1,4 +1,4 @@ -Bare Metal Resources +Baremetal Resources ===================== .. toctree:: diff --git a/doc/source/users/resources/bare_metal/v1/chassis.rst b/doc/source/users/resources/baremetal/v1/chassis.rst similarity index 54% rename from doc/source/users/resources/bare_metal/v1/chassis.rst rename to doc/source/users/resources/baremetal/v1/chassis.rst index 303896378..8db33407a 100644 --- a/doc/source/users/resources/bare_metal/v1/chassis.rst +++ b/doc/source/users/resources/baremetal/v1/chassis.rst @@ -1,12 +1,12 @@ -openstack.bare_metal.v1.chassis +openstack.baremetal.v1.chassis =============================== -.. automodule:: openstack.bare_metal.v1.chassis +.. automodule:: openstack.baremetal.v1.chassis The Chassis Class ----------------- The ``Chassis`` class inherits from :class:`~openstack.resource.Resource`. -.. autoclass:: openstack.bare_metal.v1.chassis.Chassis +.. autoclass:: openstack.baremetal.v1.chassis.Chassis :members: diff --git a/doc/source/users/resources/bare_metal/v1/driver.rst b/doc/source/users/resources/baremetal/v1/driver.rst similarity index 54% rename from doc/source/users/resources/bare_metal/v1/driver.rst rename to doc/source/users/resources/baremetal/v1/driver.rst index d45379e38..980a067bc 100644 --- a/doc/source/users/resources/bare_metal/v1/driver.rst +++ b/doc/source/users/resources/baremetal/v1/driver.rst @@ -1,12 +1,12 @@ -openstack.bare_metal.v1.driver +openstack.baremetal.v1.driver ============================== -.. automodule:: openstack.bare_metal.v1.driver +.. automodule:: openstack.baremetal.v1.driver The Driver Class ---------------- The ``Driver`` class inherits from :class:`~openstack.resource.Resource`. -.. autoclass:: openstack.bare_metal.v1.driver.Driver +.. autoclass:: openstack.baremetal.v1.driver.Driver :members: diff --git a/doc/source/users/resources/bare_metal/v1/node.rst b/doc/source/users/resources/baremetal/v1/node.rst similarity index 54% rename from doc/source/users/resources/bare_metal/v1/node.rst rename to doc/source/users/resources/baremetal/v1/node.rst index 7900c5598..323c8db6e 100644 --- a/doc/source/users/resources/bare_metal/v1/node.rst +++ b/doc/source/users/resources/baremetal/v1/node.rst @@ -1,12 +1,12 @@ -openstack.bare_metal.v1.Node +openstack.baremetal.v1.Node ============================ -.. automodule:: openstack.bare_metal.v1.node +.. automodule:: openstack.baremetal.v1.node The Node Class -------------- The ``Node`` class inherits from :class:`~openstack.resource.Resource`. -.. autoclass:: openstack.bare_metal.v1.node.Node +.. autoclass:: openstack.baremetal.v1.node.Node :members: diff --git a/doc/source/users/resources/bare_metal/v1/port.rst b/doc/source/users/resources/baremetal/v1/port.rst similarity index 54% rename from doc/source/users/resources/bare_metal/v1/port.rst rename to doc/source/users/resources/baremetal/v1/port.rst index b0ed31b47..34f1ab9a3 100644 --- a/doc/source/users/resources/bare_metal/v1/port.rst +++ b/doc/source/users/resources/baremetal/v1/port.rst @@ -1,12 +1,12 @@ -openstack.bare_metal.v1.port +openstack.baremetal.v1.port ============================ -.. automodule:: openstack.bare_metal.v1.port +.. automodule:: openstack.baremetal.v1.port The Port Class -------------- The ``Port`` class inherits from :class:`~openstack.resource.Resource`. -.. autoclass:: openstack.bare_metal.v1.port.Port +.. autoclass:: openstack.baremetal.v1.port.Port :members: diff --git a/doc/source/users/resources/bare_metal/v1/port_group.rst b/doc/source/users/resources/baremetal/v1/port_group.rst similarity index 53% rename from doc/source/users/resources/bare_metal/v1/port_group.rst rename to doc/source/users/resources/baremetal/v1/port_group.rst index 3feb4e2f4..8867dc16d 100644 --- a/doc/source/users/resources/bare_metal/v1/port_group.rst +++ b/doc/source/users/resources/baremetal/v1/port_group.rst @@ -1,12 +1,12 @@ -openstack.bare_metal.v1.port_group +openstack.baremetal.v1.port_group ================================== -.. automodule:: openstack.bare_metal.v1.port_group +.. automodule:: openstack.baremetal.v1.port_group The PortGroup Class ------------------- The ``PortGroup`` class inherits from :class:`~openstack.resource.Resource`. -.. autoclass:: openstack.bare_metal.v1.port_group.PortGroup +.. autoclass:: openstack.baremetal.v1.port_group.PortGroup :members: diff --git a/doc/source/users/resources/block_store/index.rst b/doc/source/users/resources/block_storage/index.rst similarity index 59% rename from doc/source/users/resources/block_store/index.rst rename to doc/source/users/resources/block_storage/index.rst index ba0be3c1f..e4a249416 100644 --- a/doc/source/users/resources/block_store/index.rst +++ b/doc/source/users/resources/block_storage/index.rst @@ -1,5 +1,5 @@ -Block Store Resources -===================== +Block Storage Resources +======================= .. toctree:: :maxdepth: 1 diff --git a/doc/source/users/resources/block_storage/v2/snapshot.rst b/doc/source/users/resources/block_storage/v2/snapshot.rst new file mode 100644 index 000000000..5b2eea172 --- /dev/null +++ b/doc/source/users/resources/block_storage/v2/snapshot.rst @@ -0,0 +1,21 @@ +openstack.block_storage.v2.snapshot +=================================== + +.. automodule:: openstack.block_storage.v2.snapshot + +The Snapshot Class +------------------ + +The ``Snapshot`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v2.snapshot.Snapshot + :members: + +The SnapshotDetail Class +------------------------ + +The ``SnapshotDetail`` class inherits from +:class:`~openstack.block_storage.v2.snapshot.Snapshot`. + +.. autoclass:: openstack.block_storage.v2.snapshot.SnapshotDetail + :members: diff --git a/doc/source/users/resources/block_storage/v2/type.rst b/doc/source/users/resources/block_storage/v2/type.rst new file mode 100644 index 000000000..963f235db --- /dev/null +++ b/doc/source/users/resources/block_storage/v2/type.rst @@ -0,0 +1,13 @@ +openstack.block_storage.v2.type +=============================== + +.. automodule:: openstack.block_storage.v2.type + +The Type Class +-------------- + +The ``Type`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v2.type.Type + :members: + diff --git a/doc/source/users/resources/block_storage/v2/volume.rst b/doc/source/users/resources/block_storage/v2/volume.rst new file mode 100644 index 000000000..499f585ae --- /dev/null +++ b/doc/source/users/resources/block_storage/v2/volume.rst @@ -0,0 +1,21 @@ +openstack.block_storage.v2.volume +================================= + +.. automodule:: openstack.block_storage.v2.volume + +The Volume Class +---------------- + +The ``Volume`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v2.volume.Volume + :members: + +The VolumeDetail Class +---------------------- + +The ``VolumeDetail`` class inherits from +:class:`~openstack.block_storage.v2.volume.Volume`. + +.. autoclass:: openstack.block_storage.v2.volume.VolumeDetail + :members: diff --git a/doc/source/users/resources/block_store/v2/snapshot.rst b/doc/source/users/resources/block_store/v2/snapshot.rst deleted file mode 100644 index e742ffcf8..000000000 --- a/doc/source/users/resources/block_store/v2/snapshot.rst +++ /dev/null @@ -1,21 +0,0 @@ -openstack.block_store.v2.snapshot -================================= - -.. automodule:: openstack.block_store.v2.snapshot - -The Snapshot Class ------------------- - -The ``Snapshot`` class inherits from :class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.block_store.v2.snapshot.Snapshot - :members: - -The SnapshotDetail Class ------------------------- - -The ``SnapshotDetail`` class inherits from -:class:`~openstack.block_store.v2.snapshot.Snapshot`. - -.. autoclass:: openstack.block_store.v2.snapshot.SnapshotDetail - :members: diff --git a/doc/source/users/resources/block_store/v2/type.rst b/doc/source/users/resources/block_store/v2/type.rst deleted file mode 100644 index fcab77fe2..000000000 --- a/doc/source/users/resources/block_store/v2/type.rst +++ /dev/null @@ -1,13 +0,0 @@ -openstack.block_store.v2.type -============================= - -.. automodule:: openstack.block_store.v2.type - -The Type Class --------------- - -The ``Type`` class inherits from :class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.block_store.v2.type.Type - :members: - diff --git a/doc/source/users/resources/block_store/v2/volume.rst b/doc/source/users/resources/block_store/v2/volume.rst deleted file mode 100644 index 115452535..000000000 --- a/doc/source/users/resources/block_store/v2/volume.rst +++ /dev/null @@ -1,21 +0,0 @@ -openstack.block_store.v2.volume -=============================== - -.. automodule:: openstack.block_store.v2.volume - -The Volume Class ----------------- - -The ``Volume`` class inherits from :class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.block_store.v2.volume.Volume - :members: - -The VolumeDetail Class ----------------------- - -The ``VolumeDetail`` class inherits from -:class:`~openstack.block_store.v2.volume.Volume`. - -.. autoclass:: openstack.block_store.v2.volume.VolumeDetail - :members: diff --git a/doc/source/users/resources/cluster/v1/action.rst b/doc/source/users/resources/cluster/v1/action.rst deleted file mode 100644 index f75deb424..000000000 --- a/doc/source/users/resources/cluster/v1/action.rst +++ /dev/null @@ -1,12 +0,0 @@ -openstack.cluster.v1.action -=========================== - -.. automodule:: openstack.cluster.v1.action - -The Action Class ----------------- - -The ``Action`` class inherits from :class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.cluster.v1.action.Action - :members: diff --git a/doc/source/users/resources/cluster/v1/build_info.rst b/doc/source/users/resources/cluster/v1/build_info.rst deleted file mode 100644 index 8534e1f7a..000000000 --- a/doc/source/users/resources/cluster/v1/build_info.rst +++ /dev/null @@ -1,12 +0,0 @@ -openstack.cluster.v1.build_info -=============================== - -.. automodule:: openstack.cluster.v1.build_info - -The BuildInfo Class -------------------- - -The ``BuildInfo`` class inherits from :class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.cluster.v1.build_info.BuildInfo - :members: diff --git a/doc/source/users/resources/cluster/v1/cluster.rst b/doc/source/users/resources/cluster/v1/cluster.rst deleted file mode 100644 index a54ce6cf5..000000000 --- a/doc/source/users/resources/cluster/v1/cluster.rst +++ /dev/null @@ -1,12 +0,0 @@ -openstack.cluster.v1.Cluster -============================ - -.. automodule:: openstack.cluster.v1.cluster - -The Cluster Class ------------------ - -The ``Cluster`` class inherits from :class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.cluster.v1.cluster.Cluster - :members: diff --git a/doc/source/users/resources/cluster/v1/cluster_policy.rst b/doc/source/users/resources/cluster/v1/cluster_policy.rst deleted file mode 100644 index d3a55d541..000000000 --- a/doc/source/users/resources/cluster/v1/cluster_policy.rst +++ /dev/null @@ -1,13 +0,0 @@ -openstack.cluster.v1.cluster_policy -=================================== - -.. automodule:: openstack.cluster.v1.cluster_policy - -The ClusterPolicy Class ------------------------ - -The ``ClusterPolicy`` class inherits from -:class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.cluster.v1.cluster_policy.ClusterPolicy - :members: diff --git a/doc/source/users/resources/cluster/v1/event.rst b/doc/source/users/resources/cluster/v1/event.rst deleted file mode 100644 index 29678062a..000000000 --- a/doc/source/users/resources/cluster/v1/event.rst +++ /dev/null @@ -1,12 +0,0 @@ -openstack.cluster.v1.event -========================== - -.. automodule:: openstack.cluster.v1.event - -The Event Class ---------------- - -The ``Event`` class inherits from :class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.cluster.v1.event.Event - :members: diff --git a/doc/source/users/resources/cluster/v1/node.rst b/doc/source/users/resources/cluster/v1/node.rst deleted file mode 100644 index 74f11f350..000000000 --- a/doc/source/users/resources/cluster/v1/node.rst +++ /dev/null @@ -1,12 +0,0 @@ -openstack.cluster.v1.Node -========================= - -.. automodule:: openstack.cluster.v1.node - -The Node Class --------------- - -The ``Node`` class inherits from :class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.cluster.v1.node.Node - :members: diff --git a/doc/source/users/resources/cluster/v1/policy.rst b/doc/source/users/resources/cluster/v1/policy.rst deleted file mode 100644 index 0fe59378c..000000000 --- a/doc/source/users/resources/cluster/v1/policy.rst +++ /dev/null @@ -1,12 +0,0 @@ -openstack.cluster.v1.policy -=========================== - -.. automodule:: openstack.cluster.v1.policy - -The Policy Class ----------------- - -The ``Policy`` class inherits from :class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.cluster.v1.policy.Policy - :members: diff --git a/doc/source/users/resources/cluster/v1/policy_type.rst b/doc/source/users/resources/cluster/v1/policy_type.rst deleted file mode 100644 index ee74b3101..000000000 --- a/doc/source/users/resources/cluster/v1/policy_type.rst +++ /dev/null @@ -1,12 +0,0 @@ -openstack.cluster.v1.policy_type -================================ - -.. automodule:: openstack.cluster.v1.policy_type - -The PolicyType Class --------------------- - -The ``PolicyType`` class inherits from :class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.cluster.v1.policy_type.PolicyType - :members: diff --git a/doc/source/users/resources/cluster/v1/profile.rst b/doc/source/users/resources/cluster/v1/profile.rst deleted file mode 100644 index bdf782dcb..000000000 --- a/doc/source/users/resources/cluster/v1/profile.rst +++ /dev/null @@ -1,12 +0,0 @@ -openstack.cluster.v1.profile -============================ - -.. automodule:: openstack.cluster.v1.profile - -The Profile Class ------------------ - -The ``Profile`` class inherits from :class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.cluster.v1.profile.Profile - :members: diff --git a/doc/source/users/resources/cluster/v1/profile_type.rst b/doc/source/users/resources/cluster/v1/profile_type.rst deleted file mode 100644 index 48c007f0f..000000000 --- a/doc/source/users/resources/cluster/v1/profile_type.rst +++ /dev/null @@ -1,12 +0,0 @@ -openstack.cluster.v1.profile_type -================================= - -.. automodule:: openstack.cluster.v1.profile_type - -The ProfileType Class ---------------------- - -The ``ProfileType`` class inherits from :class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.cluster.v1.profile_type.ProfileType - :members: diff --git a/doc/source/users/resources/cluster/v1/receiver.rst b/doc/source/users/resources/cluster/v1/receiver.rst deleted file mode 100644 index dc23eb5c8..000000000 --- a/doc/source/users/resources/cluster/v1/receiver.rst +++ /dev/null @@ -1,12 +0,0 @@ -openstack.cluster.v1.receiver -============================= - -.. automodule:: openstack.cluster.v1.receiver - -The Receiver Class ------------------- - -The ``Receiver`` class inherits from :class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.cluster.v1.receiver.Receiver - :members: diff --git a/doc/source/users/resources/cluster/index.rst b/doc/source/users/resources/clustering/index.rst similarity index 100% rename from doc/source/users/resources/cluster/index.rst rename to doc/source/users/resources/clustering/index.rst diff --git a/doc/source/users/resources/clustering/v1/action.rst b/doc/source/users/resources/clustering/v1/action.rst new file mode 100644 index 000000000..a12aa284b --- /dev/null +++ b/doc/source/users/resources/clustering/v1/action.rst @@ -0,0 +1,12 @@ +openstack.clustering.v1.action +============================== + +.. automodule:: openstack.clustering.v1.action + +The Action Class +---------------- + +The ``Action`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.clustering.v1.action.Action + :members: diff --git a/doc/source/users/resources/clustering/v1/build_info.rst b/doc/source/users/resources/clustering/v1/build_info.rst new file mode 100644 index 000000000..d84754f27 --- /dev/null +++ b/doc/source/users/resources/clustering/v1/build_info.rst @@ -0,0 +1,12 @@ +openstack.clustering.v1.build_info +================================== + +.. automodule:: openstack.clustering.v1.build_info + +The BuildInfo Class +------------------- + +The ``BuildInfo`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.clustering.v1.build_info.BuildInfo + :members: diff --git a/doc/source/users/resources/clustering/v1/cluster.rst b/doc/source/users/resources/clustering/v1/cluster.rst new file mode 100644 index 000000000..43e8a6d51 --- /dev/null +++ b/doc/source/users/resources/clustering/v1/cluster.rst @@ -0,0 +1,12 @@ +openstack.clustering.v1.Cluster +===================================== + +.. automodule:: openstack.clustering.v1.cluster + +The Cluster Class +----------------- + +The ``Cluster`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.clustering.v1.cluster.Cluster + :members: diff --git a/doc/source/users/resources/clustering/v1/cluster_policy.rst b/doc/source/users/resources/clustering/v1/cluster_policy.rst new file mode 100644 index 000000000..58ae94374 --- /dev/null +++ b/doc/source/users/resources/clustering/v1/cluster_policy.rst @@ -0,0 +1,13 @@ +openstack.clustering.v1.cluster_policy +====================================== + +.. automodule:: openstack.clustering.v1.cluster_policy + +The ClusterPolicy Class +----------------------- + +The ``ClusterPolicy`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.clustering.v1.cluster_policy.ClusterPolicy + :members: diff --git a/doc/source/users/resources/clustering/v1/event.rst b/doc/source/users/resources/clustering/v1/event.rst new file mode 100644 index 000000000..decc992e2 --- /dev/null +++ b/doc/source/users/resources/clustering/v1/event.rst @@ -0,0 +1,12 @@ +openstack.clustering.v1.event +============================= + +.. automodule:: openstack.clustering.v1.event + +The Event Class +--------------- + +The ``Event`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.clustering.v1.event.Event + :members: diff --git a/doc/source/users/resources/clustering/v1/node.rst b/doc/source/users/resources/clustering/v1/node.rst new file mode 100644 index 000000000..3cab1ec47 --- /dev/null +++ b/doc/source/users/resources/clustering/v1/node.rst @@ -0,0 +1,12 @@ +openstack.clustering.v1.Node +============================ + +.. automodule:: openstack.clustering.v1.node + +The Node Class +-------------- + +The ``Node`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.clustering.v1.node.Node + :members: diff --git a/doc/source/users/resources/clustering/v1/policy.rst b/doc/source/users/resources/clustering/v1/policy.rst new file mode 100644 index 000000000..00b832ed0 --- /dev/null +++ b/doc/source/users/resources/clustering/v1/policy.rst @@ -0,0 +1,12 @@ +openstack.clustering.v1.policy +============================== + +.. automodule:: openstack.clustering.v1.policy + +The Policy Class +---------------- + +The ``Policy`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.clustering.v1.policy.Policy + :members: diff --git a/doc/source/users/resources/clustering/v1/policy_type.rst b/doc/source/users/resources/clustering/v1/policy_type.rst new file mode 100644 index 000000000..ad665f9ed --- /dev/null +++ b/doc/source/users/resources/clustering/v1/policy_type.rst @@ -0,0 +1,12 @@ +openstack.clustering.v1.policy_type +=================================== + +.. automodule:: openstack.clustering.v1.policy_type + +The PolicyType Class +-------------------- + +The ``PolicyType`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.clustering.v1.policy_type.PolicyType + :members: diff --git a/doc/source/users/resources/clustering/v1/profile.rst b/doc/source/users/resources/clustering/v1/profile.rst new file mode 100644 index 000000000..c114e0c36 --- /dev/null +++ b/doc/source/users/resources/clustering/v1/profile.rst @@ -0,0 +1,12 @@ +openstack.clustering.v1.profile +=============================== + +.. automodule:: openstack.clustering.v1.profile + +The Profile Class +----------------- + +The ``Profile`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.clustering.v1.profile.Profile + :members: diff --git a/doc/source/users/resources/clustering/v1/profile_type.rst b/doc/source/users/resources/clustering/v1/profile_type.rst new file mode 100644 index 000000000..d8534c0d9 --- /dev/null +++ b/doc/source/users/resources/clustering/v1/profile_type.rst @@ -0,0 +1,12 @@ +openstack.clustering.v1.profile_type +==================================== + +.. automodule:: openstack.clustering.v1.profile_type + +The ProfileType Class +--------------------- + +The ``ProfileType`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.clustering.v1.profile_type.ProfileType + :members: diff --git a/doc/source/users/resources/clustering/v1/receiver.rst b/doc/source/users/resources/clustering/v1/receiver.rst new file mode 100644 index 000000000..9cdd4e5d7 --- /dev/null +++ b/doc/source/users/resources/clustering/v1/receiver.rst @@ -0,0 +1,12 @@ +openstack.clustering.v1.receiver +================================ + +.. automodule:: openstack.clustering.v1.receiver + +The Receiver Class +------------------ + +The ``Receiver`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.clustering.v1.receiver.Receiver + :members: diff --git a/doc/source/users/resources/telemetry/index.rst b/doc/source/users/resources/meter/index.rst similarity index 86% rename from doc/source/users/resources/telemetry/index.rst rename to doc/source/users/resources/meter/index.rst index b429949d6..9efbaf68b 100644 --- a/doc/source/users/resources/telemetry/index.rst +++ b/doc/source/users/resources/meter/index.rst @@ -1,4 +1,4 @@ -Telemetry Resources +Meter Resources =================== .. toctree:: diff --git a/doc/source/users/resources/telemetry/v2/capability.rst b/doc/source/users/resources/meter/v2/capability.rst similarity index 54% rename from doc/source/users/resources/telemetry/v2/capability.rst rename to doc/source/users/resources/meter/v2/capability.rst index c17edd501..b710e907a 100644 --- a/doc/source/users/resources/telemetry/v2/capability.rst +++ b/doc/source/users/resources/meter/v2/capability.rst @@ -1,12 +1,12 @@ -openstack.telemetry.v2.capability +openstack.meter.v2.capability ================================= -.. automodule:: openstack.telemetry.v2.capability +.. automodule:: openstack.meter.v2.capability The Capability Class -------------------- The ``Capability`` class inherits from :class:`~openstack.resource.Resource`. -.. autoclass:: openstack.telemetry.v2.capability.Capability +.. autoclass:: openstack.meter.v2.capability.Capability :members: diff --git a/doc/source/users/resources/telemetry/v2/meter.rst b/doc/source/users/resources/meter/v2/meter.rst similarity index 55% rename from doc/source/users/resources/telemetry/v2/meter.rst rename to doc/source/users/resources/meter/v2/meter.rst index b38bc5b6b..4953ecdaa 100644 --- a/doc/source/users/resources/telemetry/v2/meter.rst +++ b/doc/source/users/resources/meter/v2/meter.rst @@ -1,12 +1,12 @@ -openstack.telemetry.v2.meter +openstack.meter.v2.meter ============================ -.. automodule:: openstack.telemetry.v2.meter +.. automodule:: openstack.meter.v2.meter The Meter Class ---------------- The ``Meter`` class inherits from :class:`~openstack.resource.Resource`. -.. autoclass:: openstack.telemetry.v2.meter.Meter +.. autoclass:: openstack.meter.v2.meter.Meter :members: diff --git a/doc/source/users/resources/telemetry/v2/resource.rst b/doc/source/users/resources/meter/v2/resource.rst similarity index 54% rename from doc/source/users/resources/telemetry/v2/resource.rst rename to doc/source/users/resources/meter/v2/resource.rst index f3b33887d..c2d8c0fdb 100644 --- a/doc/source/users/resources/telemetry/v2/resource.rst +++ b/doc/source/users/resources/meter/v2/resource.rst @@ -1,12 +1,12 @@ -openstack.telemetry.v2.resource +openstack.meter.v2.resource =============================== -.. automodule:: openstack.telemetry.v2.resource +.. automodule:: openstack.meter.v2.resource The Resource Class ------------------ The ``Resource`` class inherits from :class:`~openstack.resource.Resource`. -.. autoclass:: openstack.telemetry.v2.resource.Resource +.. autoclass:: openstack.meter.v2.resource.Resource :members: diff --git a/doc/source/users/resources/telemetry/v2/sample.rst b/doc/source/users/resources/meter/v2/sample.rst similarity index 54% rename from doc/source/users/resources/telemetry/v2/sample.rst rename to doc/source/users/resources/meter/v2/sample.rst index f2430df27..d5573dca6 100644 --- a/doc/source/users/resources/telemetry/v2/sample.rst +++ b/doc/source/users/resources/meter/v2/sample.rst @@ -1,12 +1,12 @@ -openstack.telemetry.v2.sample +openstack.meter.v2.sample ============================= -.. automodule:: openstack.telemetry.v2.sample +.. automodule:: openstack.meter.v2.sample The Sample Class ---------------- The ``Sample`` class inherits from :class:`~openstack.resource.Resource`. -.. autoclass:: openstack.telemetry.v2.sample.Sample +.. autoclass:: openstack.meter.v2.sample.Sample :members: diff --git a/doc/source/users/resources/telemetry/v2/statistics.rst b/doc/source/users/resources/meter/v2/statistics.rst similarity index 54% rename from doc/source/users/resources/telemetry/v2/statistics.rst rename to doc/source/users/resources/meter/v2/statistics.rst index 7e1e6b8a5..14661beb9 100644 --- a/doc/source/users/resources/telemetry/v2/statistics.rst +++ b/doc/source/users/resources/meter/v2/statistics.rst @@ -1,12 +1,12 @@ -openstack.telemetry.v2.statistics +openstack.meter.v2.statistics ================================= -.. automodule:: openstack.telemetry.v2.statistics +.. automodule:: openstack.meter.v2.statistics The Statistics Class -------------------- The ``Statistics`` class inherits from :class:`~openstack.resource.Resource`. -.. autoclass:: openstack.telemetry.v2.statistics.Statistics +.. autoclass:: openstack.meter.v2.statistics.Statistics :members: diff --git a/doc/source/users/session.rst b/doc/source/users/session.rst deleted file mode 100644 index 44ac576b2..000000000 --- a/doc/source/users/session.rst +++ /dev/null @@ -1,10 +0,0 @@ -Session -======= - -.. automodule:: openstack.session - -Session Object --------------- - -.. autoclass:: openstack.session.Session - :members: diff --git a/examples/cluster/__init__.py b/examples/clustering/__init__.py similarity index 100% rename from examples/cluster/__init__.py rename to examples/clustering/__init__.py diff --git a/examples/cluster/policy.py b/examples/clustering/policy.py similarity index 100% rename from examples/cluster/policy.py rename to examples/clustering/policy.py diff --git a/examples/cluster/policy_type.py b/examples/clustering/policy_type.py similarity index 100% rename from examples/cluster/policy_type.py rename to examples/clustering/policy_type.py diff --git a/examples/cluster/profile.py b/examples/clustering/profile.py similarity index 100% rename from examples/cluster/profile.py rename to examples/clustering/profile.py diff --git a/examples/cluster/profile_type.py b/examples/clustering/profile_type.py similarity index 97% rename from examples/cluster/profile_type.py rename to examples/clustering/profile_type.py index 8a3e99550..2856e4e0f 100644 --- a/examples/cluster/profile_type.py +++ b/examples/clustering/profile_type.py @@ -14,7 +14,7 @@ Managing profile types in the Cluster service. For a full guide see -https://developer.openstack.org/sdks/python/openstacksdk/users/guides/cluster.html +https://developer.openstack.org/sdks/python/openstacksdk/users/guides/clustering.html """ diff --git a/examples/connect.py b/examples/connect.py index 7db375ca5..cb819a7a4 100644 --- a/examples/connect.py +++ b/examples/connect.py @@ -19,9 +19,8 @@ import argparse import os +import openstack from openstack import config as occ -from openstack import connection -from openstack import profile from openstack import utils import sys @@ -49,7 +48,7 @@ def _get_resource_value(resource_key, default): return default config = occ.OpenStackConfig() -cloud = config.get_one_cloud(TEST_CLOUD) +cloud = openstack.connect(cloud=TEST_CLOUD) SERVER_NAME = 'openstacksdk-example' IMAGE_NAME = _get_resource_value('image_name', 'cirros-0.3.5-x86_64-disk') @@ -66,10 +65,7 @@ def _get_resource_value(resource_key, default): def create_connection_from_config(): - opts = Opts(cloud_name=TEST_CLOUD) - config = occ.OpenStackConfig() - cloud = config.get_one_cloud(opts.cloud) - return connection.from_config(cloud_config=cloud, options=opts) + return openstack.connect(cloud=TEST_CLOUD) def create_connection_from_args(): @@ -77,18 +73,17 @@ def create_connection_from_args(): config = occ.OpenStackConfig() config.register_argparse_arguments(parser, sys.argv[1:]) args = parser.parse_args() - return connection.from_config(options=args) + return openstack.connect(config=config.get_one_cloud(argparse=args)) def create_connection(auth_url, region, project_name, username, password): - prof = profile.Profile() - prof.set_region(profile.Profile.ALL, region) - return connection.Connection( - profile=prof, - user_agent='examples', + return openstack.connect( auth_url=auth_url, project_name=project_name, username=username, - password=password + password=password, + region_name=region, + app_name='examples', + app_version='1.0', ) diff --git a/openstack/__init__.py b/openstack/__init__.py index a03c06908..a34937014 100644 --- a/openstack/__init__.py +++ b/openstack/__init__.py @@ -23,6 +23,7 @@ from openstack.cloud.exc import * # noqa from openstack.cloud.openstackcloud import OpenStackCloud from openstack.cloud.operatorcloud import OperatorCloud +import openstack.connection __version__ = pbr.version.VersionInfo('openstacksdk').version_string() @@ -130,3 +131,8 @@ def operator_cloud( raise OpenStackCloudException( "Invalid cloud configuration: {exc}".format(exc=str(e))) return OperatorCloud(cloud_config=cloud_config, strict=strict) + + +def connect(self, *args, **kwargs): + """Create a `openstack.connection.Connection`.""" + return openstack.connection.Connection(*args, **kwargs) diff --git a/openstack/bare_metal/__init__.py b/openstack/baremetal/__init__.py similarity index 100% rename from openstack/bare_metal/__init__.py rename to openstack/baremetal/__init__.py diff --git a/openstack/bare_metal/bare_metal_service.py b/openstack/baremetal/baremetal_service.py similarity index 87% rename from openstack/bare_metal/bare_metal_service.py rename to openstack/baremetal/baremetal_service.py index 71c3bca59..9853be081 100644 --- a/openstack/bare_metal/bare_metal_service.py +++ b/openstack/baremetal/baremetal_service.py @@ -13,12 +13,12 @@ from openstack import service_filter -class BareMetalService(service_filter.ServiceFilter): +class BaremetalService(service_filter.ServiceFilter): """The bare metal service.""" valid_versions = [service_filter.ValidVersion('v1')] def __init__(self, version=None): """Create a bare metal service.""" - super(BareMetalService, self).__init__(service_type='baremetal', + super(BaremetalService, self).__init__(service_type='baremetal', version=version) diff --git a/openstack/bare_metal/v1/__init__.py b/openstack/baremetal/v1/__init__.py similarity index 100% rename from openstack/bare_metal/v1/__init__.py rename to openstack/baremetal/v1/__init__.py diff --git a/openstack/bare_metal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py similarity index 88% rename from openstack/bare_metal/v1/_proxy.py rename to openstack/baremetal/v1/_proxy.py index 0c3cccfb0..0e68cf357 100644 --- a/openstack/bare_metal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -10,11 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.bare_metal.v1 import chassis as _chassis -from openstack.bare_metal.v1 import driver as _driver -from openstack.bare_metal.v1 import node as _node -from openstack.bare_metal.v1 import port as _port -from openstack.bare_metal.v1 import port_group as _portgroup +from openstack.baremetal.v1 import chassis as _chassis +from openstack.baremetal.v1 import driver as _driver +from openstack.baremetal.v1 import node as _node +from openstack.baremetal.v1 import port as _port +from openstack.baremetal.v1 import port_group as _portgroup from openstack import proxy2 from openstack import utils @@ -61,11 +61,11 @@ def create_chassis(self, **attrs): """Create a new chassis from attributes. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.bare_metal.v1.chassis.Chassis`, it comprised + :class:`~openstack.baremetal.v1.chassis.Chassis`, it comprised of the properties on the ``Chassis`` class. :returns: The results of chassis creation. - :rtype: :class:`~openstack.bare_metal.v1.chassis.Chassis`. + :rtype: :class:`~openstack.baremetal.v1.chassis.Chassis`. """ return self._create(_chassis.Chassis, **attrs) @@ -77,7 +77,7 @@ def find_chassis(self, name_or_id, ignore_missing=True): :class:`~openstack.exceptions.ResourceNotFound` will be raised when the chassis does not exist. When set to `True``, None will be returned when attempting to find a nonexistent chassis. - :returns: One :class:`~openstack.bare_metal.v1.chassis.Chassis` object + :returns: One :class:`~openstack.baremetal.v1.chassis.Chassis` object or None. """ return self._find(_chassis.Chassis, name_or_id, @@ -87,9 +87,9 @@ def get_chassis(self, chassis): """Get a specific chassis. :param chassis: The value can be the name or ID of a chassis or a - :class:`~openstack.bare_metal.v1.chassis.Chassis` instance. + :class:`~openstack.baremetal.v1.chassis.Chassis` instance. - :returns: One :class:`~openstack.bare_metal.v1.chassis.Chassis` + :returns: One :class:`~openstack.baremetal.v1.chassis.Chassis` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no chassis matching the name or ID could be found. """ @@ -99,12 +99,12 @@ def update_chassis(self, chassis, **attrs): """Update a chassis. :param chassis: Either the name or the ID of a chassis, or an instance - of :class:`~openstack.bare_metal.v1.chassis.Chassis`. + of :class:`~openstack.baremetal.v1.chassis.Chassis`. :param dict attrs: The attributes to update on the chassis represented by the ``chassis`` parameter. :returns: The updated chassis. - :rtype: :class:`~openstack.bare_metal.v1.chassis.Chassis` + :rtype: :class:`~openstack.baremetal.v1.chassis.Chassis` """ return self._update(_chassis.Chassis, chassis, **attrs) @@ -112,7 +112,7 @@ def delete_chassis(self, chassis, ignore_missing=True): """Delete a chassis. :param chassis: The value can be either the name or ID of a chassis or - a :class:`~openstack.bare_metal.v1.chassis.Chassis` instance. + a :class:`~openstack.baremetal.v1.chassis.Chassis` instance. :param bool ignore_missing: When set to ``False``, an exception :class:`~openstack.exceptions.ResourceNotFound` will be raised when the chassis could not be found. When set to ``True``, no @@ -120,7 +120,7 @@ def delete_chassis(self, chassis, ignore_missing=True): chassis. :returns: The instance of the chassis which was deleted. - :rtype: :class:`~openstack.bare_metal.v1.chassis.Chassis`. + :rtype: :class:`~openstack.baremetal.v1.chassis.Chassis`. """ return self._delete(_chassis.Chassis, chassis, ignore_missing=ignore_missing) @@ -136,9 +136,9 @@ def get_driver(self, driver): """Get a specific driver. :param driver: The value can be the name of a driver or a - :class:`~openstack.bare_metal.v1.driver.Driver` instance. + :class:`~openstack.baremetal.v1.driver.Driver` instance. - :returns: One :class:`~openstack.bare_metal.v1.driver.Driver` + :returns: One :class:`~openstack.baremetal.v1.driver.Driver` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no driver matching the name could be found. """ @@ -193,11 +193,11 @@ def create_node(self, **attrs): """Create a new node from attributes. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.bare_metal.v1.node.Node`, it comprised + :class:`~openstack.baremetal.v1.node.Node`, it comprised of the properties on the ``Node`` class. :returns: The results of node creation. - :rtype: :class:`~openstack.bare_metal.v1.node.Node`. + :rtype: :class:`~openstack.baremetal.v1.node.Node`. """ return self._create(_node.Node, **attrs) @@ -209,7 +209,7 @@ def find_node(self, name_or_id, ignore_missing=True): :class:`~openstack.exceptions.ResourceNotFound` will be raised when the node does not exist. When set to `True``, None will be returned when attempting to find a nonexistent node. - :returns: One :class:`~openstack.bare_metal.v1.node.Node` object + :returns: One :class:`~openstack.baremetal.v1.node.Node` object or None. """ return self._find(_node.Node, name_or_id, @@ -219,9 +219,9 @@ def get_node(self, node): """Get a specific node. :param node: The value can be the name or ID of a chassis or a - :class:`~openstack.bare_metal.v1.node.Node` instance. + :class:`~openstack.baremetal.v1.node.Node` instance. - :returns: One :class:`~openstack.bare_metal.v1.node.Node` + :returns: One :class:`~openstack.baremetal.v1.node.Node` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no node matching the name or ID could be found. """ @@ -231,12 +231,12 @@ def update_node(self, node, **attrs): """Update a node. :param chassis: Either the name or the ID of a node or an instance - of :class:`~openstack.bare_metal.v1.node.Node`. + of :class:`~openstack.baremetal.v1.node.Node`. :param dict attrs: The attributes to update on the node represented by the ``node`` parameter. :returns: The updated node. - :rtype: :class:`~openstack.bare_metal.v1.node.Node` + :rtype: :class:`~openstack.baremetal.v1.node.Node` """ return self._update(_node.Node, node, **attrs) @@ -244,7 +244,7 @@ def delete_node(self, node, ignore_missing=True): """Delete a node. :param node: The value can be either the name or ID of a node or - a :class:`~openstack.bare_metal.v1.node.Node` instance. + a :class:`~openstack.baremetal.v1.node.Node` instance. :param bool ignore_missing: When set to ``False``, an exception :class:`~openstack.exceptions.ResourceNotFound` will be raised when the node could not be found. When set to ``True``, no @@ -252,7 +252,7 @@ def delete_node(self, node, ignore_missing=True): node. :returns: The instance of the node which was deleted. - :rtype: :class:`~openstack.bare_metal.v1.node.Node`. + :rtype: :class:`~openstack.baremetal.v1.node.Node`. """ return self._delete(_node.Node, node, ignore_missing=ignore_missing) @@ -306,11 +306,11 @@ def create_port(self, **attrs): """Create a new port from attributes. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.bare_metal.v1.port.Port`, it comprises of the + :class:`~openstack.baremetal.v1.port.Port`, it comprises of the properties on the ``Port`` class. :returns: The results of port creation. - :rtype: :class:`~openstack.bare_metal.v1.port.Port`. + :rtype: :class:`~openstack.baremetal.v1.port.Port`. """ return self._create(_port.Port, **attrs) @@ -322,7 +322,7 @@ def find_port(self, name_or_id, ignore_missing=True): :class:`~openstack.exceptions.ResourceNotFound` will be raised when the port does not exist. When set to `True``, None will be returned when attempting to find a nonexistent port. - :returns: One :class:`~openstack.bare_metal.v1.port.Port` object + :returns: One :class:`~openstack.baremetal.v1.port.Port` object or None. """ return self._find(_port.Port, name_or_id, @@ -332,7 +332,7 @@ def get_port(self, port, **query): """Get a specific port. :param port: The value can be the name or ID of a chassis or a - :class:`~openstack.bare_metal.v1.port.Port` instance. + :class:`~openstack.baremetal.v1.port.Port` instance. :param dict query: Optional query parameters to be sent to restrict the port properties returned. Available parameters include: @@ -340,7 +340,7 @@ def get_port(self, port, **query): in the response. This may lead to some performance gain because other fields of the resource are not refreshed. - :returns: One :class:`~openstack.bare_metal.v1.port.Port` + :returns: One :class:`~openstack.baremetal.v1.port.Port` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no port matching the name or ID could be found. """ @@ -350,12 +350,12 @@ def update_port(self, port, **attrs): """Update a port. :param chassis: Either the name or the ID of a port or an instance - of :class:`~openstack.bare_metal.v1.port.Port`. + of :class:`~openstack.baremetal.v1.port.Port`. :param dict attrs: The attributes to update on the port represented by the ``port`` parameter. :returns: The updated port. - :rtype: :class:`~openstack.bare_metal.v1.port.Port` + :rtype: :class:`~openstack.baremetal.v1.port.Port` """ return self._update(_port.Port, port, **attrs) @@ -363,7 +363,7 @@ def delete_port(self, port, ignore_missing=True): """Delete a port. :param port: The value can be either the name or ID of a port or - a :class:`~openstack.bare_metal.v1.port.Port` instance. + a :class:`~openstack.baremetal.v1.port.Port` instance. :param bool ignore_missing: When set to ``False``, an exception :class:`~openstack.exceptions.ResourceNotFound` will be raised when the port could not be found. When set to ``True``, no @@ -371,7 +371,7 @@ def delete_port(self, port, ignore_missing=True): port. :returns: The instance of the port which was deleted. - :rtype: :class:`~openstack.bare_metal.v1.port.Port`. + :rtype: :class:`~openstack.baremetal.v1.port.Port`. """ return self._delete(_port.Port, port, ignore_missing=ignore_missing) @@ -462,11 +462,11 @@ def create_portgroup(self, **attrs): """Create a new port group from attributes. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.bare_metal.v1.port_group.PortGroup`, it + :class:`~openstack.baremetal.v1.port_group.PortGroup`, it comprises of the properties on the ``PortGroup`` class. :returns: The results of portgroup creation. - :rtype: :class:`~openstack.bare_metal.v1.port_group.PortGroup`. + :rtype: :class:`~openstack.baremetal.v1.port_group.PortGroup`. """ return self.create_port_group(**attrs) @@ -474,11 +474,11 @@ def create_port_group(self, **attrs): """Create a new portgroup from attributes. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.bare_metal.v1.port_group.PortGroup`, it + :class:`~openstack.baremetal.v1.port_group.PortGroup`, it comprises of the properties on the ``PortGroup`` class. :returns: The results of portgroup creation. - :rtype: :class:`~openstack.bare_metal.v1.port_group.PortGroup`. + :rtype: :class:`~openstack.baremetal.v1.port_group.PortGroup`. """ return self._create(_portgroup.PortGroup, **attrs) @@ -492,7 +492,7 @@ def find_portgroup(self, name_or_id, ignore_missing=True): :class:`~openstack.exceptions.ResourceNotFound` will be raised when the port group does not exist. When set to `True``, None will be returned when attempting to find a nonexistent port group. - :returns: One :class:`~openstack.bare_metal.v1.port_group.PortGroup` + :returns: One :class:`~openstack.baremetal.v1.port_group.PortGroup` object or None. """ return self.find_port_group(name_or_id, ignore_missing=ignore_missing) @@ -505,7 +505,7 @@ def find_port_group(self, name_or_id, ignore_missing=True): :class:`~openstack.exceptions.ResourceNotFound` will be raised when the port group does not exist. When set to `True``, None will be returned when attempting to find a nonexistent port group. - :returns: One :class:`~openstack.bare_metal.v1.port_group.PortGroup` + :returns: One :class:`~openstack.baremetal.v1.port_group.PortGroup` object or None. """ return self._find(_portgroup.PortGroup, name_or_id, @@ -517,7 +517,7 @@ def get_portgroup(self, portgroup, **query): """Get a specific port group. :param portgroup: The value can be the name or ID of a chassis or a - :class:`~openstack.bare_metal.v1.port_group.PortGroup` instance. + :class:`~openstack.baremetal.v1.port_group.PortGroup` instance. :param dict query: Optional query parameters to be sent to restrict the portgroup properties returned. Available parameters include: @@ -525,7 +525,7 @@ def get_portgroup(self, portgroup, **query): in the response. This may lead to some performance gain because other fields of the resource are not refreshed. - :returns: One :class:`~openstack.bare_metal.v1.port_group.PortGroup` + :returns: One :class:`~openstack.baremetal.v1.port_group.PortGroup` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no port group matching the name or ID could be found. """ @@ -535,7 +535,7 @@ def get_port_group(self, port_group, **query): """Get a specific port group. :param port_group: The value can be the name or ID of a chassis or a - :class:`~openstack.bare_metal.v1.port_group.PortGroup` instance. + :class:`~openstack.baremetal.v1.port_group.PortGroup` instance. :param dict query: Optional query parameters to be sent to restrict the port group properties returned. Available parameters include: @@ -543,7 +543,7 @@ def get_port_group(self, port_group, **query): in the response. This may lead to some performance gain because other fields of the resource are not refreshed. - :returns: One :class:`~openstack.bare_metal.v1.port_group.PortGroup` + :returns: One :class:`~openstack.baremetal.v1.port_group.PortGroup` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no port group matching the name or ID could be found. """ @@ -556,12 +556,12 @@ def update_portgroup(self, portgroup, **attrs): :param chassis: Either the name or the ID of a port group or an instance of - :class:`~openstack.bare_metal.v1.port_group.PortGroup`. + :class:`~openstack.baremetal.v1.port_group.PortGroup`. :param dict attrs: The attributes to update on the port group represented by the ``portgroup`` parameter. :returns: The updated port group. - :rtype: :class:`~openstack.bare_metal.v1.port_group.PortGroup` + :rtype: :class:`~openstack.baremetal.v1.port_group.PortGroup` """ return self.update_port_group(portgroup, **attrs) @@ -570,12 +570,12 @@ def update_port_group(self, port_group, **attrs): :param chassis: Either the name or the ID of a port group or an instance of - :class:`~openstack.bare_metal.v1.port_group.PortGroup`. + :class:`~openstack.baremetal.v1.port_group.PortGroup`. :param dict attrs: The attributes to update on the port group represented by the ``port_group`` parameter. :returns: The updated port group. - :rtype: :class:`~openstack.bare_metal.v1.port_group.PortGroup` + :rtype: :class:`~openstack.baremetal.v1.port_group.PortGroup` """ return self._update(_portgroup.PortGroup, port_group, **attrs) @@ -586,7 +586,7 @@ def delete_portgroup(self, portgroup, ignore_missing=True): :param portgroup: The value can be either the name or ID of a port group or a - :class:`~openstack.bare_metal.v1.port_group.PortGroup` + :class:`~openstack.baremetal.v1.port_group.PortGroup` instance. :param bool ignore_missing: When set to ``False``, an exception :class:`~openstack.exceptions.ResourceNotFound` will be raised @@ -595,7 +595,7 @@ def delete_portgroup(self, portgroup, ignore_missing=True): port group. :returns: The instance of the port group which was deleted. - :rtype: :class:`~openstack.bare_metal.v1.port_group.PortGroup`. + :rtype: :class:`~openstack.baremetal.v1.port_group.PortGroup`. """ return self.delete_port_group(portgroup, ignore_missing=ignore_missing) @@ -604,7 +604,7 @@ def delete_port_group(self, port_group, ignore_missing=True): :param port_group: The value can be either the name or ID of a port group or a - :class:`~openstack.bare_metal.v1.port_group.PortGroup` + :class:`~openstack.baremetal.v1.port_group.PortGroup` instance. :param bool ignore_missing: When set to ``False``, an exception :class:`~openstack.exceptions.ResourceNotFound` will be raised @@ -613,7 +613,7 @@ def delete_port_group(self, port_group, ignore_missing=True): port group. :returns: The instance of the port group which was deleted. - :rtype: :class:`~openstack.bare_metal.v1.port_group.PortGroup`. + :rtype: :class:`~openstack.baremetal.v1.port_group.PortGroup`. """ return self._delete(_portgroup.PortGroup, port_group, ignore_missing=ignore_missing) diff --git a/openstack/bare_metal/v1/chassis.py b/openstack/baremetal/v1/chassis.py similarity index 94% rename from openstack/bare_metal/v1/chassis.py rename to openstack/baremetal/v1/chassis.py index 7440e2f46..a8ba9746a 100644 --- a/openstack/bare_metal/v1/chassis.py +++ b/openstack/baremetal/v1/chassis.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.bare_metal import bare_metal_service +from openstack.baremetal import baremetal_service from openstack import resource2 as resource @@ -18,7 +18,7 @@ class Chassis(resource.Resource): resources_key = 'chassis' base_path = '/chassis' - service = bare_metal_service.BareMetalService() + service = baremetal_service.BaremetalService() # capabilities allow_create = True diff --git a/openstack/bare_metal/v1/driver.py b/openstack/baremetal/v1/driver.py similarity index 92% rename from openstack/bare_metal/v1/driver.py rename to openstack/baremetal/v1/driver.py index fcf6229b3..8421bdb54 100644 --- a/openstack/bare_metal/v1/driver.py +++ b/openstack/baremetal/v1/driver.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.bare_metal import bare_metal_service +from openstack.baremetal import baremetal_service from openstack import resource2 as resource @@ -18,7 +18,7 @@ class Driver(resource.Resource): resources_key = 'drivers' base_path = '/drivers' - service = bare_metal_service.BareMetalService() + service = baremetal_service.BaremetalService() # capabilities allow_create = False diff --git a/openstack/bare_metal/v1/node.py b/openstack/baremetal/v1/node.py similarity index 97% rename from openstack/bare_metal/v1/node.py rename to openstack/baremetal/v1/node.py index 8d920e66f..c1b6266ed 100644 --- a/openstack/bare_metal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.bare_metal import bare_metal_service +from openstack.baremetal import baremetal_service from openstack import resource2 as resource @@ -18,7 +18,7 @@ class Node(resource.Resource): resources_key = 'nodes' base_path = '/nodes' - service = bare_metal_service.BareMetalService() + service = baremetal_service.BaremetalService() # capabilities allow_create = True @@ -45,7 +45,7 @@ class Node(resource.Resource): driver = resource.Body("driver") #: All the metadata required by the driver to manage this node. List of #: fields varies between drivers, and can be retrieved from the - #: :class:`openstack.bare_metal.v1.driver.Driver` resource. + #: :class:`openstack.baremetal.v1.driver.Driver` resource. driver_info = resource.Body("driver_info", type=dict) #: Internal metadata set and stored by node's driver. This is read-only. driver_internal_info = resource.Body("driver_internal_info", type=dict) diff --git a/openstack/bare_metal/v1/port.py b/openstack/baremetal/v1/port.py similarity index 96% rename from openstack/bare_metal/v1/port.py rename to openstack/baremetal/v1/port.py index 99aa0ffe1..a3f86594c 100644 --- a/openstack/bare_metal/v1/port.py +++ b/openstack/baremetal/v1/port.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.bare_metal import bare_metal_service +from openstack.baremetal import baremetal_service from openstack import resource2 as resource @@ -18,7 +18,7 @@ class Port(resource.Resource): resources_key = 'ports' base_path = '/ports' - service = bare_metal_service.BareMetalService() + service = baremetal_service.BaremetalService() # capabilities allow_create = True diff --git a/openstack/bare_metal/v1/port_group.py b/openstack/baremetal/v1/port_group.py similarity index 96% rename from openstack/bare_metal/v1/port_group.py rename to openstack/baremetal/v1/port_group.py index 7cec820e9..b2db7467f 100644 --- a/openstack/bare_metal/v1/port_group.py +++ b/openstack/baremetal/v1/port_group.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.bare_metal import bare_metal_service +from openstack.baremetal import baremetal_service from openstack import resource2 as resource @@ -18,7 +18,7 @@ class PortGroup(resource.Resource): resources_key = 'portgroups' base_path = '/portgroups' - service = bare_metal_service.BareMetalService() + service = baremetal_service.BaremetalService() # capabilities allow_create = True diff --git a/openstack/bare_metal/version.py b/openstack/baremetal/version.py similarity index 83% rename from openstack/bare_metal/version.py rename to openstack/baremetal/version.py index f98f3b1f4..51d0e85bf 100644 --- a/openstack/bare_metal/version.py +++ b/openstack/baremetal/version.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.bare_metal import bare_metal_service +from openstack.baremetal import baremetal_service from openstack import resource2 @@ -18,8 +18,8 @@ class Version(resource2.Resource): resource_key = 'version' resources_key = 'versions' base_path = '/' - service = bare_metal_service.BareMetalService( - version=bare_metal_service.BareMetalService.UNVERSIONED + service = baremetal_service.BaremetalService( + version=baremetal_service.BaremetalService.UNVERSIONED ) # Capabilities diff --git a/openstack/block_store/__init__.py b/openstack/block_storage/__init__.py similarity index 100% rename from openstack/block_store/__init__.py rename to openstack/block_storage/__init__.py diff --git a/openstack/block_store/block_store_service.py b/openstack/block_storage/block_storage_service.py similarity index 66% rename from openstack/block_store/block_store_service.py rename to openstack/block_storage/block_storage_service.py index fa133a6a7..7192544ff 100644 --- a/openstack/block_store/block_store_service.py +++ b/openstack/block_storage/block_storage_service.py @@ -13,13 +13,12 @@ from openstack import service_filter -class BlockStoreService(service_filter.ServiceFilter): - """The block store service.""" +class BlockStorageService(service_filter.ServiceFilter): + """The block storage service.""" valid_versions = [service_filter.ValidVersion('v2')] def __init__(self, version=None): - """Create a block store service.""" - super(BlockStoreService, self).__init__(service_type='volume', - version=version, - requires_project_id=True) + """Create a block storage service.""" + super(BlockStorageService, self).__init__( + service_type='volume', version=version, requires_project_id=True) diff --git a/openstack/block_store/v2/__init__.py b/openstack/block_storage/v2/__init__.py similarity index 100% rename from openstack/block_store/v2/__init__.py rename to openstack/block_storage/v2/__init__.py diff --git a/openstack/block_store/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py similarity index 93% rename from openstack/block_store/v2/_proxy.py rename to openstack/block_storage/v2/_proxy.py index 1d82eae3a..202a2d8a0 100644 --- a/openstack/block_store/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.block_store.v2 import snapshot as _snapshot -from openstack.block_store.v2 import type as _type -from openstack.block_store.v2 import volume as _volume +from openstack.block_storage.v2 import snapshot as _snapshot +from openstack.block_storage.v2 import type as _type +from openstack.block_storage.v2 import volume as _volume from openstack import proxy2 @@ -35,9 +35,9 @@ def snapshots(self, details=True, **query): """Retrieve a generator of snapshots :param bool details: When set to ``False`` - :class:`~openstack.block_store.v2.snapshot.Snapshot` + :class:`~openstack.block_storage.v2.snapshot.Snapshot` objects will be returned. The default, ``True``, will cause - :class:`~openstack.block_store.v2.snapshot.SnapshotDetail` + :class:`~openstack.block_storage.v2.snapshot.SnapshotDetail` objects to be returned. :param kwargs \*\*query: Optional query parameters to be sent to limit the snapshots being returned. Available parameters include: @@ -144,9 +144,9 @@ def volumes(self, details=True, **query): """Retrieve a generator of volumes :param bool details: When set to ``False`` - :class:`~openstack.block_store.v2.volume.Volume` objects + :class:`~openstack.block_storage.v2.volume.Volume` objects will be returned. The default, ``True``, will cause - :class:`~openstack.block_store.v2.volume.VolumeDetail` + :class:`~openstack.block_storage.v2.volume.VolumeDetail` objects to be returned. :param kwargs \*\*query: Optional query parameters to be sent to limit the volumes being returned. Available parameters include: diff --git a/openstack/block_store/v2/snapshot.py b/openstack/block_storage/v2/snapshot.py similarity index 95% rename from openstack/block_store/v2/snapshot.py rename to openstack/block_storage/v2/snapshot.py index ae2755c26..9d5b3dae6 100644 --- a/openstack/block_store/v2/snapshot.py +++ b/openstack/block_storage/v2/snapshot.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.block_store import block_store_service +from openstack.block_storage import block_storage_service from openstack import format from openstack import resource2 @@ -19,7 +19,7 @@ class Snapshot(resource2.Resource): resource_key = "snapshot" resources_key = "snapshots" base_path = "/snapshots" - service = block_store_service.BlockStoreService() + service = block_storage_service.BlockStorageService() _query_mapping = resource2.QueryParameters('all_tenants', 'name', 'status', 'volume_id') diff --git a/openstack/block_store/v2/type.py b/openstack/block_storage/v2/type.py similarity index 90% rename from openstack/block_store/v2/type.py rename to openstack/block_storage/v2/type.py index a39466a5a..7b477aee4 100644 --- a/openstack/block_store/v2/type.py +++ b/openstack/block_storage/v2/type.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.block_store import block_store_service +from openstack.block_storage import block_storage_service from openstack import resource2 @@ -18,7 +18,7 @@ class Type(resource2.Resource): resource_key = "volume_type" resources_key = "volume_types" base_path = "/types" - service = block_store_service.BlockStoreService() + service = block_storage_service.BlockStorageService() # capabilities allow_get = True diff --git a/openstack/block_store/v2/volume.py b/openstack/block_storage/v2/volume.py similarity index 97% rename from openstack/block_store/v2/volume.py rename to openstack/block_storage/v2/volume.py index a20ddd621..82ef84071 100644 --- a/openstack/block_store/v2/volume.py +++ b/openstack/block_storage/v2/volume.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.block_store import block_store_service +from openstack.block_storage import block_storage_service from openstack import format from openstack import resource2 @@ -19,7 +19,7 @@ class Volume(resource2.Resource): resource_key = "volume" resources_key = "volumes" base_path = "/volumes" - service = block_store_service.BlockStoreService() + service = block_storage_service.BlockStorageService() _query_mapping = resource2.QueryParameters('all_tenants', 'name', 'status', 'project_id') diff --git a/openstack/cloud/_adapter.py b/openstack/cloud/_adapter.py index 2929a195b..8807329f3 100644 --- a/openstack/cloud/_adapter.py +++ b/openstack/cloud/_adapter.py @@ -12,18 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -''' Wrapper around keystoneauth Session to wrap calls in TaskManager ''' +''' Wrapper around keystoneauth Adapter to wrap calls in TaskManager ''' import functools -from keystoneauth1 import adapter from six.moves import urllib -from openstack import _log -from openstack.cloud import exc -from openstack.cloud import task_manager +from keystoneauth1 import adapter + +from openstack.cloud import task_manager as _task_manager +from openstack import exceptions -def extract_name(url): +def _extract_name(url): '''Produce a key name to use in logging/metrics from the URL path. We want to be able to logic/metric sane general things, so we pull @@ -81,86 +81,67 @@ def extract_name(url): return [part for part in name_parts if part] -# TODO(shade) This adapter should go away in favor of the work merging -# adapter with openstack.proxy. -class ShadeAdapter(adapter.Adapter): +class OpenStackSDKAdapter(adapter.Adapter): + """Wrapper around keystoneauth1.adapter.Adapter. - def __init__(self, shade_logger, manager, *args, **kwargs): - super(ShadeAdapter, self).__init__(*args, **kwargs) - self.shade_logger = shade_logger - self.manager = manager - self.request_log = _log.setup_logging('openstack.cloud.request_ids') + Uses task_manager to run tasks rather than executing them directly. + This allows using the nodepool MultiThreaded Rate Limiting TaskManager. + """ - def _log_request_id(self, response, obj=None): - # Log the request id and object id in a specific logger. This way - # someone can turn it on if they're interested in this kind of tracing. - request_id = response.headers.get('x-openstack-request-id') - if not request_id: - return response - tmpl = "{meth} call to {service} for {url} used request id {req}" - kwargs = dict( - meth=response.request.method, - service=self.service_type, - url=response.request.url, - req=request_id) - - if isinstance(obj, dict): - obj_id = obj.get('id', obj.get('uuid')) - if obj_id: - kwargs['obj_id'] = obj_id - tmpl += " returning object {obj_id}" - self.request_log.debug(tmpl.format(**kwargs)) - return response - - def _munch_response(self, response, result_key=None, error_message=None): - exc.raise_from_response(response, error_message=error_message) + def __init__(self, session=None, task_manager=None, *args, **kwargs): + super(OpenStackSDKAdapter, self).__init__( + session=session, *args, **kwargs) + if not task_manager: + task_manager = _task_manager.TaskManager(name=self.service_type) - if not response.content: - # This doens't have any content - return self._log_request_id(response) - - # Some REST calls do not return json content. Don't decode it. - if 'application/json' not in response.headers.get('Content-Type'): - return self._log_request_id(response) - - try: - result_json = response.json() - self._log_request_id(response, result_json) - except Exception: - return self._log_request_id(response) - return result_json + self.task_manager = task_manager def request( self, url, method, run_async=False, error_message=None, - *args, **kwargs): - name_parts = extract_name(url) + raise_exc=False, connect_retries=1, *args, **kwargs): + name_parts = _extract_name(url) name = '.'.join([self.service_type, method] + name_parts) - class_name = "".join([ - part.lower().capitalize() for part in name.split('.')]) request_method = functools.partial( - super(ShadeAdapter, self).request, url, method) + super(OpenStackSDKAdapter, self).request, url, method) - class RequestTask(task_manager.BaseTask): + return self.task_manager.submit_function( + request_method, run_async=run_async, name=name, + connect_retries=connect_retries, raise_exc=raise_exc, + **kwargs) + + def _version_matches(self, version): + api_version = self.get_api_major_version() + if api_version: + return api_version[0] == version + return False - def __init__(self, **kw): - super(RequestTask, self).__init__(**kw) - self.name = name - self.__class__.__name__ = str(class_name) - self.run_async = run_async - def main(self, client): - self.args.setdefault('raise_exc', False) - return request_method(**self.args) +class ShadeAdapter(OpenStackSDKAdapter): + """Wrapper for shade methods that expect json unpacking.""" - response = self.manager.submit_task(RequestTask(**kwargs)) + def request(self, url, method, + run_async=False, error_message=None, **kwargs): + response = super(ShadeAdapter, self).request( + url, method, run_async=run_async, **kwargs) if run_async: return response else: return self._munch_response(response, error_message=error_message) - def _version_matches(self, version): - api_version = self.get_api_major_version() - if api_version: - return api_version[0] == version - return False + def _munch_response(self, response, result_key=None, error_message=None): + exceptions.raise_from_response(response, error_message=error_message) + + if not response.content: + # This doens't have any content + return response + + # Some REST calls do not return json content. Don't decode it. + if 'application/json' not in response.headers.get('Content-Type'): + return response + + try: + result_json = response.json() + except Exception: + return response + return result_json diff --git a/openstack/cloud/exc.py b/openstack/cloud/exc.py index 7635c2bfc..c02de2d77 100644 --- a/openstack/cloud/exc.py +++ b/openstack/cloud/exc.py @@ -12,46 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -import sys +from openstack import exceptions -import munch -from requests import exceptions as _rex - -from openstack import _log - - -class OpenStackCloudException(Exception): - - log_inner_exceptions = False - - def __init__(self, message, extra_data=None, **kwargs): - args = [message] - if extra_data: - if isinstance(extra_data, munch.Munch): - extra_data = extra_data.toDict() - args.append("Extra: {0}".format(str(extra_data))) - super(OpenStackCloudException, self).__init__(*args, **kwargs) - self.extra_data = extra_data - self.inner_exception = sys.exc_info() - self.orig_message = message - - def log_error(self, logger=None): - if not logger: - logger = _log.setup_logging('openstack.cloud.exc') - if self.inner_exception and self.inner_exception[1]: - logger.error(self.orig_message, exc_info=self.inner_exception) - - def __str__(self): - message = Exception.__str__(self) - if (self.inner_exception and self.inner_exception[1] - and not self.orig_message.endswith( - str(self.inner_exception[1]))): - message = "%s (Inner Exception: %s)" % ( - message, - str(self.inner_exception[1])) - if self.log_inner_exceptions: - self.log_error() - return message +OpenStackCloudException = exceptions.SDKException class OpenStackCloudCreateException(OpenStackCloudException): @@ -76,98 +39,8 @@ class OpenStackCloudUnavailableFeature(OpenStackCloudException): pass -class OpenStackCloudHTTPError(OpenStackCloudException, _rex.HTTPError): - - def __init__(self, *args, **kwargs): - OpenStackCloudException.__init__(self, *args, **kwargs) - _rex.HTTPError.__init__(self, *args, **kwargs) - - -class OpenStackCloudBadRequest(OpenStackCloudHTTPError): - """There is something wrong with the request payload. - - Possible reasons can include malformed json or invalid values to parameters - such as flavorRef to a server create. - """ - - -class OpenStackCloudURINotFound(OpenStackCloudHTTPError): - pass - # Backwards compat +OpenStackCloudHTTPError = exceptions.HttpException +OpenStackCloudBadRequest = exceptions.BadRequestException +OpenStackCloudURINotFound = exceptions.NotFoundException OpenStackCloudResourceNotFound = OpenStackCloudURINotFound - - -def _log_response_extras(response): - # Sometimes we get weird HTML errors. This is usually from load balancers - # or other things. Log them to a special logger so that they can be - # toggled indepdently - and at debug level so that a person logging - # openstack.cloud.* only gets them at debug. - if response.headers.get('content-type') != 'text/html': - return - try: - if int(response.headers.get('content-length', 0)) == 0: - return - except Exception: - return - logger = _log.setup_logging('openstack.cloud.http') - if response.reason: - logger.debug( - "Non-standard error '{reason}' returned from {url}:".format( - reason=response.reason, - url=response.url)) - else: - logger.debug( - "Non-standard error returned from {url}:".format( - url=response.url)) - for response_line in response.text.split('\n'): - logger.debug(response_line) - - -# Logic shamelessly stolen from requests -def raise_from_response(response, error_message=None): - msg = '' - if 400 <= response.status_code < 500: - source = "Client" - elif 500 <= response.status_code < 600: - source = "Server" - else: - return - - remote_error = "Error for url: {url}".format(url=response.url) - try: - details = response.json() - # Nova returns documents that look like - # {statusname: 'message': message, 'code': code} - detail_keys = list(details.keys()) - if len(detail_keys) == 1: - detail_key = detail_keys[0] - detail_message = details[detail_key].get('message') - if detail_message: - remote_error += " {message}".format(message=detail_message) - except ValueError: - if response.reason: - remote_error += " {reason}".format(reason=response.reason) - - _log_response_extras(response) - - if error_message: - msg = '{error_message}. ({code}) {source} {remote_error}'.format( - error_message=error_message, - source=source, - code=response.status_code, - remote_error=remote_error) - else: - msg = '({code}) {source} {remote_error}'.format( - code=response.status_code, - source=source, - remote_error=remote_error) - - # Special case 404 since we raised a specific one for neutron exceptions - # before - if response.status_code == 404: - raise OpenStackCloudURINotFound(msg, response=response) - elif response.status_code == 400: - raise OpenStackCloudBadRequest(msg, response=response) - if msg: - raise OpenStackCloudHTTPError(msg, response=response) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 829db766d..a2e3dff7f 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -175,7 +175,7 @@ def __init__( self.manager = manager else: self.manager = task_manager.TaskManager( - name=':'.join([self.name, self.region_name]), client=self) + name=':'.join([self.name, self.region_name])) self._external_ipv4_names = cloud_config.get_external_ipv4_networks() self._internal_ipv4_names = cloud_config.get_internal_ipv4_networks() @@ -402,29 +402,27 @@ def _get_versioned_client( version=config_major) adapter = _adapter.ShadeAdapter( session=self.keystone_session, - manager=self.manager, + task_manager=self.manager, service_type=self.cloud_config.get_service_type(service_type), service_name=self.cloud_config.get_service_name(service_type), interface=self.cloud_config.get_interface(service_type), endpoint_override=self.cloud_config.get_endpoint(service_type), region_name=self.cloud_config.region, min_version=request_min_version, - max_version=request_max_version, - shade_logger=self.log) + max_version=request_max_version) if adapter.get_endpoint(): return adapter adapter = _adapter.ShadeAdapter( session=self.keystone_session, - manager=self.manager, + task_manager=self.manager, service_type=self.cloud_config.get_service_type(service_type), service_name=self.cloud_config.get_service_name(service_type), interface=self.cloud_config.get_interface(service_type), endpoint_override=self.cloud_config.get_endpoint(service_type), region_name=self.cloud_config.region, min_version=min_version, - max_version=max_version, - shade_logger=self.log) + max_version=max_version) # data.api_version can be None if no version was detected, such # as with neutron @@ -456,14 +454,13 @@ def _get_raw_client( self, service_type, api_version=None, endpoint_override=None): return _adapter.ShadeAdapter( session=self.keystone_session, - manager=self.manager, + task_manager=self.manager, service_type=self.cloud_config.get_service_type(service_type), service_name=self.cloud_config.get_service_name(service_type), interface=self.cloud_config.get_interface(service_type), endpoint_override=self.cloud_config.get_endpoint( service_type) or endpoint_override, - region_name=self.cloud_config.region, - shade_logger=self.log) + region_name=self.cloud_config.region) def _is_client_version(self, client, version): client_name = '_{client}_client'.format(client=client) diff --git a/openstack/cloud/task_manager.py b/openstack/cloud/task_manager.py index 358e9eed2..b015ac85f 100644 --- a/openstack/cloud/task_manager.py +++ b/openstack/cloud/task_manager.py @@ -14,44 +14,22 @@ # See the License for the specific language governing permissions and # limitations under the License. -import abc import concurrent.futures import sys import threading import time -import types import keystoneauth1.exceptions import six -from openstack import _log -from openstack.cloud import exc -from openstack.cloud import meta +from openstack import exceptions +from openstack import utils +_log = utils.setup_logging(__name__) -def _is_listlike(obj): - # NOTE(Shrews): Since the client API might decide to subclass one - # of these result types, we use isinstance() here instead of type(). - return ( - isinstance(obj, list) or - isinstance(obj, types.GeneratorType)) - -def _is_objlike(obj): - # NOTE(Shrews): Since the client API might decide to subclass one - # of these result types, we use isinstance() here instead of type(). - return ( - not isinstance(obj, bool) and - not isinstance(obj, int) and - not isinstance(obj, float) and - not isinstance(obj, six.string_types) and - not isinstance(obj, set) and - not isinstance(obj, tuple)) - - -@six.add_metaclass(abc.ABCMeta) -class BaseTask(object): - """Represent a task to be performed on an OpenStack Cloud. +class Task(object): + """Represent a remote task to be performed on an OpenStack Cloud. Some consumers need to inject things like rate-limiting or auditing around each external REST interaction. Task provides an interface @@ -67,19 +45,24 @@ class BaseTask(object): the main payload at execution time. """ - def __init__(self, **kw): + def __init__(self, main=None, name=None, run_async=False, *args, **kwargs): self._exception = None self._traceback = None self._result = None self._response = None self._finished = threading.Event() - self.run_async = False - self.args = kw - self.name = type(self).__name__ + self._main = main + self._run_async = run_async + self.args = args + self.kwargs = kwargs + self.name = name or type(self).__name__ + + def main(self): + return self._main(*self.args, **self.kwargs) - @abc.abstractmethod - def main(self, client): - """ Override this method with the actual workload to be performed """ + @property + def run_async(self): + return self._run_async def done(self, result): self._result = result @@ -99,212 +82,104 @@ def wait(self, raw=False): return self._result - def run(self, client): - self._client = client + def run(self): try: # Retry one time if we get a retriable connection failure try: - # Keep time for connection retrying logging - start = time.time() - self.done(self.main(client)) + self.done(self.main()) except keystoneauth1.exceptions.RetriableConnectionFailure as e: - end = time.time() - dt = end - start - if client.region_name: - client.log.debug(str(e)) - client.log.debug( - "Connection failure on %(cloud)s:%(region)s" - " for %(name)s after %(secs)s seconds, retrying", - {'cloud': client.name, - 'region': client.region_name, - 'secs': dt, - 'name': self.name}) - else: - client.log.debug( - "Connection failure on %(cloud)s for %(name)s after" - " %(secs)s seconds, retrying", - {'cloud': client.name, 'name': self.name, 'secs': dt}) - self.done(self.main(client)) - except Exception: - raise + self.done(self.main()) except Exception as e: self.exception(e, sys.exc_info()[2]) -class Task(BaseTask): - """ Shade specific additions to the BaseTask Interface. """ - - def wait(self, raw=False): - super(Task, self).wait() - - if raw: - # Do NOT convert the result. - return self._result - - if _is_listlike(self._result): - return meta.obj_list_to_munch(self._result) - elif _is_objlike(self._result): - return meta.obj_to_munch(self._result) - else: - return self._result - - -class RequestTask(BaseTask): - """ Extensions to the Shade Tasks to handle raw requests """ - - # It's totally legit for calls to not return things - result_key = None - - # keystoneauth1 throws keystoneauth1.exceptions.http.HttpError on !200 - def done(self, result): - self._response = result - - try: - result_json = self._response.json() - except ValueError as e: - result_json = self._response.text - self._client.log.debug( - 'Could not decode json in response: %(e)s', {'e': str(e)}) - self._client.log.debug(result_json) - - if self.result_key: - self._result = result_json[self.result_key] - else: - self._result = result_json - - self._request_id = self._response.headers.get('x-openstack-request-id') - self._finished.set() - - def wait(self, raw=False): - super(RequestTask, self).wait() - - if raw: - # Do NOT convert the result. - return self._result - - if _is_listlike(self._result): - return meta.obj_list_to_munch( - self._result, request_id=self._request_id) - elif _is_objlike(self._result): - return meta.obj_to_munch(self._result, request_id=self._request_id) - return self._result - - -def _result_filter_cb(result): - return result - - -def generate_task_class(method, name, result_filter_cb): - if name is None: - if callable(method): - name = method.__name__ - else: - name = method - - class RunTask(Task): - def __init__(self, **kw): - super(RunTask, self).__init__(**kw) - self.name = name - self._method = method - - def wait(self, raw=False): - super(RunTask, self).wait() - - if raw: - # Do NOT convert the result. - return self._result - return result_filter_cb(self._result) - - def main(self, client): - if callable(self._method): - return method(**self.args) - else: - meth = getattr(client, self._method) - return meth(**self.args) - return RunTask - - class TaskManager(object): - log = _log.setup_logging('openstack.cloud.task_manager') - def __init__( - self, client, name, result_filter_cb=None, workers=5, **kwargs): + def __init__(self, name, log=_log, workers=5, **kwargs): self.name = name - self._client = client - self._executor = concurrent.futures.ThreadPoolExecutor( - max_workers=workers) - if not result_filter_cb: - self._result_filter_cb = _result_filter_cb - else: - self._result_filter_cb = result_filter_cb + self._executor = None + self._log = log + self._workers = workers - def set_client(self, client): - self._client = client + @property + def executor(self): + if not self._executor: + self._executor = concurrent.futures.ThreadPoolExecutor( + max_workers=self._workers) + return self._executor def stop(self): """ This is a direct action passthrough TaskManager """ - self._executor.shutdown(wait=True) + if self._executor: + self._executor.shutdown() def run(self): """ This is a direct action passthrough TaskManager """ pass - def submit_task(self, task, raw=False): + def submit_task(self, task): """Submit and execute the given task. :param task: The task to execute. :param bool raw: If True, return the raw result as received from the underlying client call. """ - return self.run_task(task=task, raw=raw) - - def _run_task_async(self, task, raw=False): - self.log.debug( - "Manager %s submitting task %s", self.name, task.name) - return self._executor.submit(self._run_task, task, raw=raw) + return self.run_task(task=task) - def run_task(self, task, raw=False): - if hasattr(task, 'run_async') and task.run_async: - return self._run_task_async(task, raw=raw) - else: - return self._run_task(task, raw=raw) + def submit_function(self, method, name=None, *args, **kwargs): + """ Allows submitting an arbitrary method for work. - def _run_task(self, task, raw=False): - self.log.debug( - "Manager %s running task %s", self.name, task.name) - start = time.time() - task.run(self._client) - end = time.time() - dt = end - start - self.log.debug( - "Manager %s ran task %s in %ss", self.name, task.name, dt) + :param method: Callable to run in the TaskManager. + :param str name: Name to use for the generated Task object. + :param args: positional arguments to pass to the method when it runs. + :param kwargs: keyword arguments to pass to the method when it runs. + """ + task = Task(main=method, name=name, *args, **kwargs) + return self.submit_task(task) - self.post_run_task(dt, task) + def submit_function_async(self, method, name=None, *args, **kwargs): + """ Allows submitting an arbitrary method for async work scheduling. - return task.wait(raw) + :param method: Callable to run in the TaskManager. + :param str name: Name to use for the generated Task object. + :param args: positional arguments to pass to the method when it runs. + :param kwargs: keyword arguments to pass to the method when it runs. + """ + task = Task(method=method, name=name, run_async=True, **kwargs) + return self.submit_task(task) - def post_run_task(self, elasped_time, task): - pass + def pre_run_task(self, task): + self._log.debug( + "Manager %s running task %s", self.name, task.name) - # Backwards compatibility - submitTask = submit_task + def run_task(self, task): + if task.run_async: + return self._run_task_async(task) + else: + return self._run_task(task) - def submit_function( - self, method, name=None, result_filter_cb=None, **kwargs): - """ Allows submitting an arbitrary method for work. + def post_run_task(self, elapsed_time, task): + self._log.debug( + "Manager %s ran task %s in %ss", + self.name, task.name, elapsed_time) - :param method: Method to run in the TaskManager. Can be either the - name of a method to find on self.client, or a callable. - """ - if not result_filter_cb: - result_filter_cb = self._result_filter_cb + def _run_task_async(self, task): + self._log.debug( + "Manager %s submitting task %s", self.name, task.name) + return self.executor.submit(self._run_task, task) - task_class = generate_task_class(method, name, result_filter_cb) + def _run_task(self, task): + self.pre_run_task(task) + start = time.time() + task.run() + end = time.time() + dt = end - start + self.post_run_task(dt, task) - return self._executor.submit_task(task_class(**kwargs)) + return task.wait() -def wait_for_futures(futures, raise_on_error=True, log=None): +def wait_for_futures(futures, raise_on_error=True, log=_log): '''Collect results or failures from a list of running future tasks.''' results = [] @@ -314,21 +189,15 @@ def wait_for_futures(futures, raise_on_error=True, log=None): for completed in concurrent.futures.as_completed(futures): try: result = completed.result() - # We have to do this here because munch_response doesn't - # get called on async job results - exc.raise_from_response(result) + exceptions.raise_from_response(result) results.append(result) except (keystoneauth1.exceptions.RetriableConnectionFailure, - exc.OpenStackCloudException) as e: - if log: - log.debug( - "Exception processing async task: {e}".format( - e=str(e)), - exc_info=True) - # If we get an exception, put the result into a list so we - # can try again + exceptions.HttpException) as e: + log.exception( + "Exception processing async task: {e}".format(e=str(e))) if raise_on_error: raise - else: - retries.append(result) + # If we get an exception, put the result into a list so we + # can try again + retries.append(result) return results, retries diff --git a/openstack/cluster/__init__.py b/openstack/clustering/__init__.py similarity index 100% rename from openstack/cluster/__init__.py rename to openstack/clustering/__init__.py diff --git a/openstack/cluster/cluster_service.py b/openstack/clustering/clustering_service.py similarity index 81% rename from openstack/cluster/cluster_service.py rename to openstack/clustering/clustering_service.py index 7b6eb5d1b..0d5e57e4a 100644 --- a/openstack/cluster/cluster_service.py +++ b/openstack/clustering/clustering_service.py @@ -13,15 +13,15 @@ from openstack import service_filter -class ClusterService(service_filter.ServiceFilter): - """The cluster service.""" +class ClusteringService(service_filter.ServiceFilter): + """The clustering service.""" valid_versions = [service_filter.ValidVersion('v1')] UNVERSIONED = None def __init__(self, version=None): - """Create a cluster service.""" - super(ClusterService, self).__init__( + """Create a clustering service.""" + super(ClusteringService, self).__init__( service_type='clustering', version=version ) diff --git a/openstack/cluster/v1/__init__.py b/openstack/clustering/v1/__init__.py similarity index 100% rename from openstack/cluster/v1/__init__.py rename to openstack/clustering/v1/__init__.py diff --git a/openstack/cluster/v1/_proxy.py b/openstack/clustering/v1/_proxy.py similarity index 84% rename from openstack/cluster/v1/_proxy.py rename to openstack/clustering/v1/_proxy.py index 7bcff9f7e..01602607d 100644 --- a/openstack/cluster/v1/_proxy.py +++ b/openstack/clustering/v1/_proxy.py @@ -10,19 +10,19 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.cluster.v1 import action as _action -from openstack.cluster.v1 import build_info -from openstack.cluster.v1 import cluster as _cluster -from openstack.cluster.v1 import cluster_attr as _cluster_attr -from openstack.cluster.v1 import cluster_policy as _cluster_policy -from openstack.cluster.v1 import event as _event -from openstack.cluster.v1 import node as _node -from openstack.cluster.v1 import policy as _policy -from openstack.cluster.v1 import policy_type as _policy_type -from openstack.cluster.v1 import profile as _profile -from openstack.cluster.v1 import profile_type as _profile_type -from openstack.cluster.v1 import receiver as _receiver -from openstack.cluster.v1 import service as _service +from openstack.clustering.v1 import action as _action +from openstack.clustering.v1 import build_info +from openstack.clustering.v1 import cluster as _cluster +from openstack.clustering.v1 import cluster_attr as _cluster_attr +from openstack.clustering.v1 import cluster_policy as _cluster_policy +from openstack.clustering.v1 import event as _event +from openstack.clustering.v1 import node as _node +from openstack.clustering.v1 import policy as _policy +from openstack.clustering.v1 import policy_type as _policy_type +from openstack.clustering.v1 import profile as _profile +from openstack.clustering.v1 import profile_type as _profile_type +from openstack.clustering.v1 import receiver as _receiver +from openstack.clustering.v1 import service as _service from openstack import proxy2 from openstack import resource2 from openstack import utils @@ -41,7 +41,7 @@ def profile_types(self, **query): """Get a generator of profile types. :returns: A generator of objects that are of type - :class:`~openstack.cluster.v1.profile_type.ProfileType` + :class:`~openstack.clustering.v1.profile_type.ProfileType` """ return self._list(_profile_type.ProfileType, paginated=False, **query) @@ -49,9 +49,9 @@ def get_profile_type(self, profile_type): """Get the details about a profile_type. :param name: The name of the profile_type to retrieve or an object of - :class:`~openstack.cluster.v1.profile_type.ProfileType`. + :class:`~openstack.clustering.v1.profile_type.ProfileType`. - :returns: A :class:`~openstack.cluster.v1.profile_type.ProfileType` + :returns: A :class:`~openstack.clustering.v1.profile_type.ProfileType` object. :raises: :class:`~openstack.exceptions.ResourceNotFound` when no profile_type matching the name could be found. @@ -62,7 +62,7 @@ def policy_types(self, **query): """Get a generator of policy types. :returns: A generator of objects that are of type - :class:`~openstack.cluster.v1.policy_type.PolicyType` + :class:`~openstack.clustering.v1.policy_type.PolicyType` """ return self._list(_policy_type.PolicyType, paginated=False, **query) @@ -70,9 +70,9 @@ def get_policy_type(self, policy_type): """Get the details about a policy_type. :param policy_type: The name of a poicy_type or an object of - :class:`~openstack.cluster.v1.policy_type.PolicyType`. + :class:`~openstack.clustering.v1.policy_type.PolicyType`. - :returns: A :class:`~openstack.cluster.v1.policy_type.PolicyType` + :returns: A :class:`~openstack.clustering.v1.policy_type.PolicyType` object. :raises: :class:`~openstack.exceptions.ResourceNotFound` when no policy_type matching the name could be found. @@ -83,11 +83,11 @@ def create_profile(self, **attrs): """Create a new profile from attributes. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.cluster.v1.profile.Profile`, it is comprised + :class:`~openstack.clustering.v1.profile.Profile`, it is comprised of the properties on the Profile class. :returns: The results of profile creation. - :rtype: :class:`~openstack.cluster.v1.profile.Profile`. + :rtype: :class:`~openstack.clustering.v1.profile.Profile`. """ return self._create(_profile.Profile, **attrs) @@ -95,7 +95,7 @@ def delete_profile(self, profile, ignore_missing=True): """Delete a profile. :param profile: The value can be either the name or ID of a profile or - a :class:`~openstack.cluster.v1.profile.Profile` instance. + a :class:`~openstack.clustering.v1.profile.Profile` instance. :param bool ignore_missing: When set to ``False``, an exception :class:`~openstack.exceptions.ResourceNotFound` will be raised when the profile could not be found. When set to ``True``, no exception @@ -114,7 +114,7 @@ def find_profile(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :returns: One :class:`~openstack.cluster.v1.profile.Profile` object + :returns: One :class:`~openstack.clustering.v1.profile.Profile` object or None """ return self._find(_profile.Profile, name_or_id, @@ -124,9 +124,9 @@ def get_profile(self, profile): """Get a single profile. :param profile: The value can be the name or ID of a profile or a - :class:`~openstack.cluster.v1.profile.Profile` instance. + :class:`~openstack.clustering.v1.profile.Profile` instance. - :returns: One :class:`~openstack.cluster.v1.profile.Profile` + :returns: One :class:`~openstack.clustering.v1.profile.Profile` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no profile matching the criteria could be found. """ @@ -163,12 +163,12 @@ def update_profile(self, profile, **attrs): """Update a profile. :param profile: Either the name or the ID of the profile, or an - instance of :class:`~openstack.cluster.v1.profile.Profile`. + instance of :class:`~openstack.clustering.v1.profile.Profile`. :param attrs: The attributes to update on the profile represented by the ``value`` parameter. :returns: The updated profile. - :rtype: :class:`~openstack.cluster.v1.profile.Profile` + :rtype: :class:`~openstack.clustering.v1.profile.Profile` """ return self._update(_profile.Profile, profile, **attrs) @@ -176,11 +176,11 @@ def validate_profile(self, **attrs): """Validate a profile spec. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.cluster.v1.profile.ProfileValidate`, it is + :class:`~openstack.clustering.v1.profile.ProfileValidate`, it is comprised of the properties on the Profile class. :returns: The results of profile validation. - :rtype: :class:`~openstack.cluster.v1.profile.ProfileValidate`. + :rtype: :class:`~openstack.clustering.v1.profile.ProfileValidate`. """ return self._create(_profile.ProfileValidate, **attrs) @@ -188,11 +188,11 @@ def create_cluster(self, **attrs): """Create a new cluster from attributes. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.cluster.v1.cluster.Cluster`, it is comprised + :class:`~openstack.clustering.v1.cluster.Cluster`, it is comprised of the properties on the Cluster class. :returns: The results of cluster creation. - :rtype: :class:`~openstack.cluster.v1.cluster.Cluster`. + :rtype: :class:`~openstack.clustering.v1.cluster.Cluster`. """ return self._create(_cluster.Cluster, **attrs) @@ -200,14 +200,14 @@ def delete_cluster(self, cluster, ignore_missing=True): """Delete a cluster. :param cluster: The value can be either the name or ID of a cluster or - a :class:`~openstack.cluster.v1.cluster.Cluster` instance. + a :class:`~openstack.clustering.v1.cluster.Cluster` instance. :param bool ignore_missing: When set to ``False``, an exception :class:`~openstack.exceptions.ResourceNotFound` will be raised when the cluster could not be found. When set to ``True``, no exception will be raised when attempting to delete a non-existent cluster. :returns: The instance of the Cluster which was deleted. - :rtype: :class:`~openstack.cluster.v1.cluster.Cluster`. + :rtype: :class:`~openstack.clustering.v1.cluster.Cluster`. """ return self._delete(_cluster.Cluster, cluster, ignore_missing=ignore_missing) @@ -221,7 +221,7 @@ def find_cluster(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :returns: One :class:`~openstack.cluster.v1.cluster.Cluster` object + :returns: One :class:`~openstack.clustering.v1.cluster.Cluster` object or None """ return self._find(_cluster.Cluster, name_or_id, @@ -231,9 +231,9 @@ def get_cluster(self, cluster): """Get a single cluster. :param cluster: The value can be the name or ID of a cluster or a - :class:`~openstack.cluster.v1.cluster.Cluster` instance. + :class:`~openstack.clustering.v1.cluster.Cluster` instance. - :returns: One :class:`~openstack.cluster.v1.cluster.Cluster` + :returns: One :class:`~openstack.clustering.v1.cluster.Cluster` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no cluster matching the criteria could be found. """ @@ -268,12 +268,12 @@ def update_cluster(self, cluster, **attrs): """Update a cluster. :param cluster: Either the name or the ID of the cluster, or an - instance of :class:`~openstack.cluster.v1.cluster.Cluster`. + instance of :class:`~openstack.clustering.v1.cluster.Cluster`. :param attrs: The attributes to update on the cluster represented by the ``cluster`` parameter. :returns: The updated cluster. - :rtype: :class:`~openstack.cluster.v1.cluster.Cluster` + :rtype: :class:`~openstack.clustering.v1.cluster.Cluster` """ return self._update(_cluster.Cluster, cluster, **attrs) @@ -283,7 +283,7 @@ def cluster_add_nodes(self, cluster, nodes): """Add nodes to a cluster. :param cluster: Either the name or the ID of the cluster, or an - instance of :class:`~openstack.cluster.v1.cluster.Cluster`. + instance of :class:`~openstack.clustering.v1.cluster.Cluster`. :param nodes: List of nodes to be added to the cluster. :returns: A dict containing the action initiated by this operation. """ @@ -293,7 +293,7 @@ def add_nodes_to_cluster(self, cluster, nodes): """Add nodes to a cluster. :param cluster: Either the name or the ID of the cluster, or an - instance of :class:`~openstack.cluster.v1.cluster.Cluster`. + instance of :class:`~openstack.clustering.v1.cluster.Cluster`. :param nodes: List of nodes to be added to the cluster. :returns: A dict containing the action initiated by this operation. """ @@ -301,7 +301,7 @@ def add_nodes_to_cluster(self, cluster, nodes): obj = cluster else: obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) - return obj.add_nodes(self._session, nodes) + return obj.add_nodes(self, nodes) @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", details="Use remove_nodes_from_cluster instead") @@ -309,7 +309,7 @@ def cluster_del_nodes(self, cluster, nodes, **params): """Remove nodes from a cluster. :param cluster: Either the name or the ID of the cluster, or an - instance of :class:`~openstack.cluster.v1.cluster.Cluster`. + instance of :class:`~openstack.clustering.v1.cluster.Cluster`. :param nodes: List of nodes to be removed from the cluster. :param kwargs \*\*params: Optional query parameters to be sent to restrict the nodes to be returned. Available parameters include: @@ -324,7 +324,7 @@ def remove_nodes_from_cluster(self, cluster, nodes, **params): """Remove nodes from a cluster. :param cluster: Either the name or the ID of the cluster, or an - instance of :class:`~openstack.cluster.v1.cluster.Cluster`. + instance of :class:`~openstack.clustering.v1.cluster.Cluster`. :param nodes: List of nodes to be removed from the cluster. :param kwargs \*\*params: Optional query parameters to be sent to restrict the nodes to be returned. Available parameters include: @@ -337,7 +337,7 @@ def remove_nodes_from_cluster(self, cluster, nodes, **params): obj = cluster else: obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) - return obj.del_nodes(self._session, nodes, **params) + return obj.del_nodes(self, nodes, **params) @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", details="Use replace_nodes_in_cluster instead") @@ -345,7 +345,7 @@ def cluster_replace_nodes(self, cluster, nodes): """Replace the nodes in a cluster with specified nodes. :param cluster: Either the name or the ID of the cluster, or an - instance of :class:`~openstack.cluster.v1.cluster.Cluster`. + instance of :class:`~openstack.clustering.v1.cluster.Cluster`. :param nodes: List of nodes to be deleted/added to the cluster. :returns: A dict containing the action initiated by this operation. """ @@ -355,7 +355,7 @@ def replace_nodes_in_cluster(self, cluster, nodes): """Replace the nodes in a cluster with specified nodes. :param cluster: Either the name or the ID of the cluster, or an - instance of :class:`~openstack.cluster.v1.cluster.Cluster`. + instance of :class:`~openstack.clustering.v1.cluster.Cluster`. :param nodes: List of nodes to be deleted/added to the cluster. :returns: A dict containing the action initiated by this operation. """ @@ -363,7 +363,7 @@ def replace_nodes_in_cluster(self, cluster, nodes): obj = cluster else: obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) - return obj.replace_nodes(self._session, nodes) + return obj.replace_nodes(self, nodes) @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", details="Use scale_out_cluster instead") @@ -371,7 +371,7 @@ def cluster_scale_out(self, cluster, count=None): """Inflate the size of a cluster. :param cluster: Either the name or the ID of the cluster, or an - instance of :class:`~openstack.cluster.v1.cluster.Cluster`. + instance of :class:`~openstack.clustering.v1.cluster.Cluster`. :param count: Optional parameter specifying the number of nodes to be added. :returns: A dict containing the action initiated by this operation. @@ -382,7 +382,7 @@ def scale_out_cluster(self, cluster, count=None): """Inflate the size of a cluster. :param cluster: Either the name or the ID of the cluster, or an - instance of :class:`~openstack.cluster.v1.cluster.Cluster`. + instance of :class:`~openstack.clustering.v1.cluster.Cluster`. :param count: Optional parameter specifying the number of nodes to be added. :returns: A dict containing the action initiated by this operation. @@ -391,7 +391,7 @@ def scale_out_cluster(self, cluster, count=None): obj = cluster else: obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) - return obj.scale_out(self._session, count) + return obj.scale_out(self, count) @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", details="Use scale_in_cluster instead") @@ -399,7 +399,7 @@ def cluster_scale_in(self, cluster, count=None): """Shrink the size of a cluster. :param cluster: Either the name or the ID of the cluster, or an - instance of :class:`~openstack.cluster.v1.cluster.Cluster`. + instance of :class:`~openstack.clustering.v1.cluster.Cluster`. :param count: Optional parameter specifying the number of nodes to be removed. :returns: A dict containing the action initiated by this operation. @@ -410,7 +410,7 @@ def scale_in_cluster(self, cluster, count=None): """Shrink the size of a cluster. :param cluster: Either the name or the ID of the cluster, or an - instance of :class:`~openstack.cluster.v1.cluster.Cluster`. + instance of :class:`~openstack.clustering.v1.cluster.Cluster`. :param count: Optional parameter specifying the number of nodes to be removed. :returns: A dict containing the action initiated by this operation. @@ -419,7 +419,7 @@ def scale_in_cluster(self, cluster, count=None): obj = cluster else: obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) - return obj.scale_in(self._session, count) + return obj.scale_in(self, count) @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", details="Use resize_cluster instead") @@ -427,7 +427,7 @@ def cluster_resize(self, cluster, **params): """Resize of cluster. :param cluster: Either the name or the ID of the cluster, or an - instance of :class:`~openstack.cluster.v1.cluster.Cluster`. + instance of :class:`~openstack.clustering.v1.cluster.Cluster`. :param dict \*\*params: A dictionary providing the parameters for the resize action. :returns: A dict containing the action initiated by this operation. @@ -438,7 +438,7 @@ def resize_cluster(self, cluster, **params): """Resize of cluster. :param cluster: Either the name or the ID of the cluster, or an - instance of :class:`~openstack.cluster.v1.cluster.Cluster`. + instance of :class:`~openstack.clustering.v1.cluster.Cluster`. :param dict \*\*params: A dictionary providing the parameters for the resize action. :returns: A dict containing the action initiated by this operation. @@ -447,7 +447,7 @@ def resize_cluster(self, cluster, **params): obj = cluster else: obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) - return obj.resize(self._session, **params) + return obj.resize(self, **params) @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", details="Use attach_policy_to_cluster instead") @@ -455,7 +455,7 @@ def cluster_attach_policy(self, cluster, policy, **params): """Attach a policy to a cluster. :param cluster: Either the name or the ID of the cluster, or an - instance of :class:`~openstack.cluster.v1.cluster.Cluster`. + instance of :class:`~openstack.clustering.v1.cluster.Cluster`. :param policy: Either the name or the ID of a policy. :param dict \*\*params: A dictionary containing the properties for the policy to be attached. @@ -467,7 +467,7 @@ def attach_policy_to_cluster(self, cluster, policy, **params): """Attach a policy to a cluster. :param cluster: Either the name or the ID of the cluster, or an - instance of :class:`~openstack.cluster.v1.cluster.Cluster`. + instance of :class:`~openstack.clustering.v1.cluster.Cluster`. :param policy: Either the name or the ID of a policy. :param dict \*\*params: A dictionary containing the properties for the policy to be attached. @@ -477,7 +477,7 @@ def attach_policy_to_cluster(self, cluster, policy, **params): obj = cluster else: obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) - return obj.policy_attach(self._session, policy, **params) + return obj.policy_attach(self, policy, **params) @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", details="Use detach_policy_from_cluster instead") @@ -485,7 +485,7 @@ def cluster_detach_policy(self, cluster, policy): """Attach a policy to a cluster. :param cluster: Either the name or the ID of the cluster, or an - instance of :class:`~openstack.cluster.v1.cluster.Cluster`. + instance of :class:`~openstack.clustering.v1.cluster.Cluster`. :param policy: Either the name or the ID of a policy. :returns: A dict containing the action initiated by this operation. """ @@ -495,7 +495,7 @@ def detach_policy_from_cluster(self, cluster, policy): """Detach a policy from a cluster. :param cluster: Either the name or the ID of the cluster, or an - instance of :class:`~openstack.cluster.v1.cluster.Cluster`. + instance of :class:`~openstack.clustering.v1.cluster.Cluster`. :param policy: Either the name or the ID of a policy. :returns: A dict containing the action initiated by this operation. """ @@ -503,7 +503,7 @@ def detach_policy_from_cluster(self, cluster, policy): obj = cluster else: obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) - return obj.policy_detach(self._session, policy) + return obj.policy_detach(self, policy) @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", details="Use update_cluster_policy instead") @@ -511,7 +511,7 @@ def cluster_update_policy(self, cluster, policy, **params): """Change properties of a policy which is bound to the cluster. :param cluster: Either the name or the ID of the cluster, or an - instance of :class:`~openstack.cluster.v1.cluster.Cluster`. + instance of :class:`~openstack.clustering.v1.cluster.Cluster`. :param policy: Either the name or the ID of a policy. :param dict \*\*params: A dictionary containing the new properties for the policy. @@ -523,7 +523,7 @@ def update_cluster_policy(self, cluster, policy, **params): """Change properties of a policy which is bound to the cluster. :param cluster: Either the name or the ID of the cluster, or an - instance of :class:`~openstack.cluster.v1.cluster.Cluster`. + instance of :class:`~openstack.clustering.v1.cluster.Cluster`. :param policy: Either the name or the ID of a policy. :param dict \*\*params: A dictionary containing the new properties for the policy. @@ -533,13 +533,13 @@ def update_cluster_policy(self, cluster, policy, **params): obj = cluster else: obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) - return obj.policy_update(self._session, policy, **params) + return obj.policy_update(self, policy, **params) def collect_cluster_attrs(self, cluster, path): """Collect attribute values across a cluster. :param cluster: The value can be either the ID of a cluster or a - :class:`~openstack.cluster.v1.cluster.Cluster` instance. + :class:`~openstack.clustering.v1.cluster.Cluster` instance. :param path: A Json path string specifying the attribute to collect. :returns: A dictionary containing the list of attribute values. @@ -551,27 +551,27 @@ def check_cluster(self, cluster, **params): """Check a cluster. :param cluster: The value can be either the ID of a cluster or a - :class:`~openstack.cluster.v1.cluster.Cluster` instance. + :class:`~openstack.clustering.v1.cluster.Cluster` instance. :param dict params: A dictionary providing the parameters for the check action. :returns: A dictionary containing the action ID. """ obj = self._get_resource(_cluster.Cluster, cluster) - return obj.check(self._session, **params) + return obj.check(self, **params) def recover_cluster(self, cluster, **params): """recover a cluster. :param cluster: The value can be either the ID of a cluster or a - :class:`~openstack.cluster.v1.cluster.Cluster` instance. + :class:`~openstack.clustering.v1.cluster.Cluster` instance. :param dict params: A dictionary providing the parameters for the recover action. :returns: A dictionary containing the action ID. """ obj = self._get_resource(_cluster.Cluster, cluster) - return obj.recover(self._session, **params) + return obj.recover(self, **params) @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", details="Use perform_operation_on_cluster instead") @@ -579,7 +579,7 @@ def cluster_operation(self, cluster, operation, **params): """Perform an operation on the specified cluster. :param cluster: The value can be either the ID of a cluster or a - :class:`~openstack.cluster.v1.cluster.Cluster` instance. + :class:`~openstack.clustering.v1.cluster.Cluster` instance. :param operation: A string specifying the operation to be performed. :param dict params: A dictionary providing the parameters for the operation. @@ -592,7 +592,7 @@ def perform_operation_on_cluster(self, cluster, operation, **params): """Perform an operation on the specified cluster. :param cluster: The value can be either the ID of a cluster or a - :class:`~openstack.cluster.v1.cluster.Cluster` instance. + :class:`~openstack.clustering.v1.cluster.Cluster` instance. :param operation: A string specifying the operation to be performed. :param dict params: A dictionary providing the parameters for the operation. @@ -600,17 +600,17 @@ def perform_operation_on_cluster(self, cluster, operation, **params): :returns: A dictionary containing the action ID. """ obj = self._get_resource(_cluster.Cluster, cluster) - return obj.op(self._session, operation, **params) + return obj.op(self, operation, **params) def create_node(self, **attrs): """Create a new node from attributes. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.cluster.v1.node.Node`, it is comprised + :class:`~openstack.clustering.v1.node.Node`, it is comprised of the properties on the ``Node`` class. :returns: The results of node creation. - :rtype: :class:`~openstack.cluster.v1.node.Node`. + :rtype: :class:`~openstack.clustering.v1.node.Node`. """ return self._create(_node.Node, **attrs) @@ -618,14 +618,14 @@ def delete_node(self, node, ignore_missing=True): """Delete a node. :param node: The value can be either the name or ID of a node or a - :class:`~openstack.cluster.v1.node.Node` instance. + :class:`~openstack.clustering.v1.node.Node` instance. :param bool ignore_missing: When set to ``False``, an exception :class:`~openstack.exceptions.ResourceNotFound` will be raised when the node could not be found. When set to ``True``, no exception will be raised when attempting to delete a non-existent node. :returns: The instance of the Node which was deleted. - :rtype: :class:`~openstack.cluster.v1.node.Node`. + :rtype: :class:`~openstack.clustering.v1.node.Node`. """ return self._delete(_node.Node, node, ignore_missing=ignore_missing) @@ -633,7 +633,8 @@ def find_node(self, name_or_id, ignore_missing=True): """Find a single node. :param str name_or_id: The name or ID of a node. - :returns: One :class:`~openstack.cluster.v1.node.Node` object or None. + :returns: One :class:`~openstack.clustering.v1.node.Node` object + or None. """ return self._find(_node.Node, name_or_id, ignore_missing=ignore_missing) @@ -642,11 +643,11 @@ def get_node(self, node, details=False): """Get a single node. :param node: The value can be the name or ID of a node or a - :class:`~openstack.cluster.v1.node.Node` instance. + :class:`~openstack.clustering.v1.node.Node` instance. :param details: An optional argument that indicates whether the server should return more details when retrieving the node data. - :returns: One :class:`~openstack.cluster.v1.node.Node` + :returns: One :class:`~openstack.clustering.v1.node.Node` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no node matching the name or ID could be found. """ @@ -688,12 +689,12 @@ def update_node(self, node, **attrs): """Update a node. :param node: Either the name or the ID of the node, or an instance - of :class:`~openstack.cluster.v1.node.Node`. + of :class:`~openstack.clustering.v1.node.Node`. :param attrs: The attributes to update on the node represented by the ``node`` parameter. :returns: The updated node. - :rtype: :class:`~openstack.cluster.v1.node.Node` + :rtype: :class:`~openstack.clustering.v1.node.Node` """ return self._update(_node.Node, node, **attrs) @@ -701,26 +702,26 @@ def check_node(self, node, **params): """Check the health of the specified node. :param node: The value can be either the ID of a node or a - :class:`~openstack.cluster.v1.node.Node` instance. + :class:`~openstack.clustering.v1.node.Node` instance. :param dict params: A dictionary providing the parametes to the check action. :returns: A dictionary containing the action ID. """ obj = self._get_resource(_node.Node, node) - return obj.check(self._session, **params) + return obj.check(self, **params) def recover_node(self, node, **params): """Recover the specified node into healthy status. :param node: The value can be either the ID of a node or a - :class:`~openstack.cluster.v1.node.Node` instance. + :class:`~openstack.clustering.v1.node.Node` instance. :param dict params: A dict supplying parameters to the recover action. :returns: A dictionary containing the action ID. """ obj = self._get_resource(_node.Node, node) - return obj.recover(self._session, **params) + return obj.recover(self, **params) def adopt_node(self, preview=False, **attrs): """Adopting an existing resource as a node. @@ -747,12 +748,12 @@ def adopt_node(self, preview=False, **attrs): to override attributes derived from the target resource. :returns: The result of node adoption. If `preview` is set to False - (default), returns a :class:`~openstack.cluster.v1.node.Node` + (default), returns a :class:`~openstack.clustering.v1.node.Node` object, otherwise a Dict is returned containing the profile to be used for the new node. """ node = self._get_resource(_node.Node, None) - return node.adopt(self._session, preview=preview, **attrs) + return node.adopt(self, preview=preview, **attrs) @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", details="Use perform_operation_on_node instead") @@ -760,7 +761,7 @@ def node_operation(self, node, operation, **params): """Perform an operation on the specified node. :param cluster: The value can be either the ID of a node or a - :class:`~openstack.cluster.v1.node.Node` instance. + :class:`~openstack.clustering.v1.node.Node` instance. :param operation: A string specifying the operation to be performed. :param dict params: A dictionary providing the parameters for the operation. @@ -773,7 +774,7 @@ def perform_operation_on_node(self, node, operation, **params): """Perform an operation on the specified node. :param cluster: The value can be either the ID of a node or a - :class:`~openstack.cluster.v1.node.Node` instance. + :class:`~openstack.clustering.v1.node.Node` instance. :param operation: A string specifying the operation to be performed. :param dict params: A dictionary providing the parameters for the operation. @@ -781,17 +782,17 @@ def perform_operation_on_node(self, node, operation, **params): :returns: A dictionary containing the action ID. """ obj = self._get_resource(_node.Node, node) - return obj.op(self._session, operation, **params) + return obj.op(self, operation, **params) def create_policy(self, **attrs): """Create a new policy from attributes. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.cluster.v1.policy.Policy`, it is comprised + :class:`~openstack.clustering.v1.policy.Policy`, it is comprised of the properties on the ``Policy`` class. :returns: The results of policy creation. - :rtype: :class:`~openstack.cluster.v1.policy.Policy`. + :rtype: :class:`~openstack.clustering.v1.policy.Policy`. """ return self._create(_policy.Policy, **attrs) @@ -799,7 +800,7 @@ def delete_policy(self, policy, ignore_missing=True): """Delete a policy. :param policy: The value can be either the name or ID of a policy or a - :class:`~openstack.cluster.v1.policy.Policy` instance. + :class:`~openstack.clustering.v1.policy.Policy` instance. :param bool ignore_missing: When set to ``False``, an exception :class:`~openstack.exceptions.ResourceNotFound` will be raised when the policy could not be found. When set to ``True``, no exception @@ -819,7 +820,7 @@ def find_policy(self, name_or_id, ignore_missing=True): When set to ``True``, None will be returned when attempting to find a nonexistent policy. :returns: A policy object or None. - :rtype: :class:`~openstack.cluster.v1.policy.Policy` + :rtype: :class:`~openstack.clustering.v1.policy.Policy` """ return self._find(_policy.Policy, name_or_id, ignore_missing=ignore_missing) @@ -828,10 +829,10 @@ def get_policy(self, policy): """Get a single policy. :param policy: The value can be the name or ID of a policy or a - :class:`~openstack.cluster.v1.policy.Policy` instance. + :class:`~openstack.clustering.v1.policy.Policy` instance. :returns: A policy object. - :rtype: :class:`~openstack.cluster.v1.policy.Policy` + :rtype: :class:`~openstack.clustering.v1.policy.Policy` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no policy matching the criteria could be found. """ @@ -866,12 +867,12 @@ def update_policy(self, policy, **attrs): """Update a policy. :param policy: Either the name or the ID of a policy, or an instance - of :class:`~openstack.cluster.v1.policy.Policy`. + of :class:`~openstack.clustering.v1.policy.Policy`. :param attrs: The attributes to update on the policy represented by the ``value`` parameter. :returns: The updated policy. - :rtype: :class:`~openstack.cluster.v1.policy.Policy` + :rtype: :class:`~openstack.clustering.v1.policy.Policy` """ return self._update(_policy.Policy, policy, **attrs) @@ -879,11 +880,11 @@ def validate_policy(self, **attrs): """Validate a policy spec. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.cluster.v1.policy.PolicyValidate`, it is + :class:`~openstack.clustering.v1.policy.PolicyValidate`, it is comprised of the properties on the Policy class. :returns: The results of Policy validation. - :rtype: :class:`~openstack.cluster.v1.policy.PolicyValidate`. + :rtype: :class:`~openstack.clustering.v1.policy.PolicyValidate`. """ return self._create(_policy.PolicyValidate, **attrs) @@ -891,7 +892,7 @@ def cluster_policies(self, cluster, **query): """Retrieve a generator of cluster-policy bindings. :param cluster: The value can be the name or ID of a cluster or a - :class:`~openstack.cluster.v1.cluster.Cluster` instance. + :class:`~openstack.clustering.v1.cluster.Cluster` instance. :param kwargs \*\*query: Optional query parameters to be sent to restrict the policies to be returned. Available parameters include: @@ -908,12 +909,12 @@ def get_cluster_policy(self, cluster_policy, cluster): :param cluster_policy: The value can be the name or ID of a policy or a - :class:`~openstack.cluster.v1.policy.Policy` instance. + :class:`~openstack.clustering.v1.policy.Policy` instance. :param cluster: The value can be the name or ID of a cluster or a - :class:`~openstack.cluster.v1.cluster.Cluster` instance. + :class:`~openstack.clustering.v1.cluster.Cluster` instance. :returns: a cluster-policy binding object. - :rtype: :class:`~openstack.cluster.v1.cluster_policy.CLusterPolicy` + :rtype: :class:`~openstack.clustering.v1.cluster_policy.CLusterPolicy` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no cluster-policy binding matching the criteria could be found. """ @@ -924,11 +925,11 @@ def create_receiver(self, **attrs): """Create a new receiver from attributes. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.cluster.v1.receiver.Receiver`, it is comprised - of the properties on the Receiver class. + :class:`~openstack.clustering.v1.receiver.Receiver`, it is + comprised of the properties on the Receiver class. :returns: The results of receiver creation. - :rtype: :class:`~openstack.cluster.v1.receiver.Receiver`. + :rtype: :class:`~openstack.clustering.v1.receiver.Receiver`. """ return self._create(_receiver.Receiver, **attrs) @@ -936,11 +937,11 @@ def update_receiver(self, receiver, **attrs): """Update a receiver. :param receiver: The value can be either the name or ID of a receiver - or a :class:`~openstack.cluster.v1.receiver.Receiver` instance. + or a :class:`~openstack.clustering.v1.receiver.Receiver` instance. :param attrs: The attributes to update on the receiver parameter. Valid attribute names include ``name``, ``action`` and ``params``. :returns: The updated receiver. - :rtype: :class:`~openstack.cluster.v1.receiver.Receiver` + :rtype: :class:`~openstack.clustering.v1.receiver.Receiver` """ return self._update(_receiver.Receiver, receiver, **attrs) @@ -948,7 +949,7 @@ def delete_receiver(self, receiver, ignore_missing=True): """Delete a receiver. :param receiver: The value can be either the name or ID of a receiver - or a :class:`~openstack.cluster.v1.receiver.Receiver` instance. + or a :class:`~openstack.clustering.v1.receiver.Receiver` instance. :param bool ignore_missing: When set to ``False``, an exception :class:`~openstack.exceptions.ResourceNotFound` will be raised when the receiver could not be found. When set to ``True``, no exception @@ -969,7 +970,7 @@ def find_receiver(self, name_or_id, ignore_missing=True): set to ``True``, None will be returned when attempting to find a nonexistent receiver. :returns: A receiver object or None. - :rtype: :class:`~openstack.cluster.v1.receiver.Receiver` + :rtype: :class:`~openstack.clustering.v1.receiver.Receiver` """ return self._find(_receiver.Receiver, name_or_id, ignore_missing=ignore_missing) @@ -978,10 +979,10 @@ def get_receiver(self, receiver): """Get a single receiver. :param receiver: The value can be the name or ID of a receiver or a - :class:`~openstack.cluster.v1.receiver.Receiver` instance. + :class:`~openstack.clustering.v1.receiver.Receiver` instance. :returns: A receiver object. - :rtype: :class:`~openstack.cluster.v1.receiver.Receiver` + :rtype: :class:`~openstack.clustering.v1.receiver.Receiver` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no receiver matching the criteria could be found. """ @@ -1011,10 +1012,10 @@ def get_action(self, action): """Get a single action. :param action: The value can be the name or ID of an action or a - :class:`~openstack.cluster.v1.action.Action` instance. + :class:`~openstack.clustering.v1.action.Action` instance. :returns: an action object. - :rtype: :class:`~openstack.cluster.v1.action.Action` + :rtype: :class:`~openstack.clustering.v1.action.Action` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no action matching the criteria could be found. """ @@ -1049,10 +1050,10 @@ def get_event(self, event): """Get a single event. :param event: The value can be the name or ID of an event or a - :class:`~openstack.cluster.v1.event.Event` instance. + :class:`~openstack.clustering.v1.event.Event` instance. :returns: an event object. - :rtype: :class:`~openstack.cluster.v1.event.Event` + :rtype: :class:`~openstack.clustering.v1.event.Event` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no event matching the criteria could be found. """ @@ -1110,7 +1111,7 @@ def wait_for_status(self, resource, status, failures=[], interval=2, :raises: :class:`~AttributeError` if the resource does not have a ``status`` attribute. """ - return resource2.wait_for_status(self._session, resource, status, + return resource2.wait_for_status(self, resource, status, failures, interval, wait) def wait_for_delete(self, resource, interval=2, wait=120): @@ -1126,13 +1127,13 @@ def wait_for_delete(self, resource, interval=2, wait=120): :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition to delete failed to occur in the specified seconds. """ - return resource2.wait_for_delete(self._session, resource, interval, + return resource2.wait_for_delete(self, resource, interval, wait) def services(self, **query): """Get a generator of services. :returns: A generator of objects that are of type - :class:`~openstack.cluster.v1.service.Service` + :class:`~openstack.clustering.v1.service.Service` """ return self._list(_service.Service, paginated=False, **query) diff --git a/openstack/cluster/v1/action.py b/openstack/clustering/v1/action.py similarity index 96% rename from openstack/cluster/v1/action.py rename to openstack/clustering/v1/action.py index a7db8aa90..46609f8a9 100644 --- a/openstack/cluster/v1/action.py +++ b/openstack/clustering/v1/action.py @@ -11,7 +11,7 @@ # under the License. -from openstack.cluster import cluster_service +from openstack.clustering import clustering_service from openstack import resource2 as resource @@ -19,7 +19,7 @@ class Action(resource.Resource): resource_key = 'action' resources_key = 'actions' base_path = '/actions' - service = cluster_service.ClusterService() + service = clustering_service.ClusteringService() # Capabilities allow_list = True diff --git a/openstack/cluster/v1/build_info.py b/openstack/clustering/v1/build_info.py similarity index 89% rename from openstack/cluster/v1/build_info.py rename to openstack/clustering/v1/build_info.py index 78ac642a1..e666050eb 100644 --- a/openstack/cluster/v1/build_info.py +++ b/openstack/clustering/v1/build_info.py @@ -10,14 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.cluster import cluster_service +from openstack.clustering import clustering_service from openstack import resource2 as resource class BuildInfo(resource.Resource): base_path = '/build-info' resource_key = 'build_info' - service = cluster_service.ClusterService() + service = clustering_service.ClusteringService() # Capabilities allow_get = True diff --git a/openstack/cluster/v1/cluster.py b/openstack/clustering/v1/cluster.py similarity index 96% rename from openstack/cluster/v1/cluster.py rename to openstack/clustering/v1/cluster.py index a1d44ac7d..7563d0a07 100644 --- a/openstack/cluster/v1/cluster.py +++ b/openstack/clustering/v1/cluster.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.cluster import cluster_service +from openstack.clustering import clustering_service from openstack import resource2 as resource from openstack import utils @@ -19,7 +19,7 @@ class Cluster(resource.Resource): resource_key = 'cluster' resources_key = 'clusters' base_path = '/clusters' - service = cluster_service.ClusterService() + service = clustering_service.ClusteringService() # capabilities allow_create = True @@ -83,7 +83,7 @@ class Cluster(resource.Resource): def action(self, session, body): url = utils.urljoin(self.base_path, self._get_id(self), 'actions') - resp = session.post(url, endpoint_filter=self.service, json=body) + resp = session.post(url, json=body) return resp.json() def add_nodes(self, session, nodes): @@ -178,6 +178,6 @@ def op(self, session, operation, **params): :returns: A dictionary containing the action ID. """ url = utils.urljoin(self.base_path, self.id, 'ops') - resp = session.post(url, endpoint_filter=self.service, + resp = session.post(url, json={operation: params}) return resp.json() diff --git a/openstack/cluster/v1/cluster_attr.py b/openstack/clustering/v1/cluster_attr.py similarity index 91% rename from openstack/cluster/v1/cluster_attr.py rename to openstack/clustering/v1/cluster_attr.py index 1755b100f..353728503 100644 --- a/openstack/cluster/v1/cluster_attr.py +++ b/openstack/clustering/v1/cluster_attr.py @@ -10,14 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.cluster import cluster_service +from openstack.clustering import clustering_service from openstack import resource2 as resource class ClusterAttr(resource.Resource): resources_key = 'cluster_attributes' base_path = '/clusters/%(cluster_id)s/attrs/%(path)s' - service = cluster_service.ClusterService() + service = clustering_service.ClusteringService() # capabilities allow_list = True diff --git a/openstack/cluster/v1/cluster_policy.py b/openstack/clustering/v1/cluster_policy.py similarity index 93% rename from openstack/cluster/v1/cluster_policy.py rename to openstack/clustering/v1/cluster_policy.py index 377e3d46b..6d48ece91 100644 --- a/openstack/cluster/v1/cluster_policy.py +++ b/openstack/clustering/v1/cluster_policy.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.cluster import cluster_service +from openstack.clustering import clustering_service from openstack import resource2 as resource @@ -18,7 +18,7 @@ class ClusterPolicy(resource.Resource): resource_key = 'cluster_policy' resources_key = 'cluster_policies' base_path = '/clusters/%(cluster_id)s/policies' - service = cluster_service.ClusterService() + service = clustering_service.ClusteringService() # Capabilities allow_list = True diff --git a/openstack/cluster/v1/event.py b/openstack/clustering/v1/event.py similarity index 95% rename from openstack/cluster/v1/event.py rename to openstack/clustering/v1/event.py index d9b51fbb3..c248a5179 100644 --- a/openstack/cluster/v1/event.py +++ b/openstack/clustering/v1/event.py @@ -11,7 +11,7 @@ # under the License. -from openstack.cluster import cluster_service +from openstack.clustering import clustering_service from openstack import resource2 as resource @@ -19,7 +19,7 @@ class Event(resource.Resource): resource_key = 'event' resources_key = 'events' base_path = '/events' - service = cluster_service.ClusterService() + service = clustering_service.ClusteringService() # Capabilities allow_list = True diff --git a/openstack/cluster/v1/node.py b/openstack/clustering/v1/node.py similarity index 95% rename from openstack/cluster/v1/node.py rename to openstack/clustering/v1/node.py index a0305a82c..1e3208ad8 100644 --- a/openstack/cluster/v1/node.py +++ b/openstack/clustering/v1/node.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.cluster import cluster_service +from openstack.clustering import clustering_service from openstack import resource2 as resource from openstack import utils @@ -19,7 +19,7 @@ class Node(resource.Resource): resource_key = 'node' resources_key = 'nodes' base_path = '/nodes' - service = cluster_service.ClusterService() + service = clustering_service.ClusteringService() # capabilities allow_create = True @@ -88,7 +88,7 @@ def _action(self, session, body): :param body: The body of action to be sent. """ url = utils.urljoin(self.base_path, self.id, 'actions') - resp = session.post(url, endpoint_filter=self.service, json=body) + resp = session.post(url, json=body) return resp.json() def check(self, session, **params): @@ -123,7 +123,7 @@ def op(self, session, operation, **params): :returns: A dictionary containing the action ID. """ url = utils.urljoin(self.base_path, self.id, 'ops') - resp = session.post(url, endpoint_filter=self.service, + resp = session.post(url, json={operation: params}) return resp.json() @@ -149,7 +149,7 @@ def adopt(self, session, preview=False, **params): attrs = params url = utils.urljoin(self.base_path, path) - resp = session.post(url, endpoint_filter=self.service, json=attrs) + resp = session.post(url, json=attrs) if preview: return resp.json() diff --git a/openstack/cluster/v1/policy.py b/openstack/clustering/v1/policy.py similarity index 95% rename from openstack/cluster/v1/policy.py rename to openstack/clustering/v1/policy.py index 0137220b3..a547c7f61 100644 --- a/openstack/cluster/v1/policy.py +++ b/openstack/clustering/v1/policy.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.cluster import cluster_service +from openstack.clustering import clustering_service from openstack import resource2 as resource @@ -18,7 +18,7 @@ class Policy(resource.Resource): resource_key = 'policy' resources_key = 'policies' base_path = '/policies' - service = cluster_service.ClusterService() + service = clustering_service.ClusteringService() # Capabilities allow_list = True diff --git a/openstack/cluster/v1/policy_type.py b/openstack/clustering/v1/policy_type.py similarity index 91% rename from openstack/cluster/v1/policy_type.py rename to openstack/clustering/v1/policy_type.py index aefcffec6..e63edf007 100644 --- a/openstack/cluster/v1/policy_type.py +++ b/openstack/clustering/v1/policy_type.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.cluster import cluster_service +from openstack.clustering import clustering_service from openstack import resource2 as resource @@ -18,7 +18,7 @@ class PolicyType(resource.Resource): resource_key = 'policy_type' resources_key = 'policy_types' base_path = '/policy-types' - service = cluster_service.ClusterService() + service = clustering_service.ClusteringService() # Capabilities allow_list = True diff --git a/openstack/cluster/v1/profile.py b/openstack/clustering/v1/profile.py similarity index 95% rename from openstack/cluster/v1/profile.py rename to openstack/clustering/v1/profile.py index 711b302f2..6420d8615 100644 --- a/openstack/cluster/v1/profile.py +++ b/openstack/clustering/v1/profile.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.cluster import cluster_service +from openstack.clustering import clustering_service from openstack import resource2 as resource @@ -18,7 +18,7 @@ class Profile(resource.Resource): resource_key = 'profile' resources_key = 'profiles' base_path = '/profiles' - service = cluster_service.ClusterService() + service = clustering_service.ClusteringService() # capabilities allow_create = True diff --git a/openstack/cluster/v1/profile_type.py b/openstack/clustering/v1/profile_type.py similarity index 91% rename from openstack/cluster/v1/profile_type.py rename to openstack/clustering/v1/profile_type.py index 3b79297aa..be04686ea 100644 --- a/openstack/cluster/v1/profile_type.py +++ b/openstack/clustering/v1/profile_type.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.cluster import cluster_service +from openstack.clustering import clustering_service from openstack import resource2 as resource @@ -18,7 +18,7 @@ class ProfileType(resource.Resource): resource_key = 'profile_type' resources_key = 'profile_types' base_path = '/profile-types' - service = cluster_service.ClusterService() + service = clustering_service.ClusteringService() # Capabilities allow_list = True diff --git a/openstack/cluster/v1/receiver.py b/openstack/clustering/v1/receiver.py similarity index 95% rename from openstack/cluster/v1/receiver.py rename to openstack/clustering/v1/receiver.py index d69400489..1437fce35 100644 --- a/openstack/cluster/v1/receiver.py +++ b/openstack/clustering/v1/receiver.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.cluster import cluster_service +from openstack.clustering import clustering_service from openstack import resource2 as resource @@ -18,7 +18,7 @@ class Receiver(resource.Resource): resource_key = 'receiver' resources_key = 'receivers' base_path = '/receivers' - service = cluster_service.ClusterService() + service = clustering_service.ClusteringService() # Capabilities allow_list = True diff --git a/openstack/cluster/v1/service.py b/openstack/clustering/v1/service.py similarity index 92% rename from openstack/cluster/v1/service.py rename to openstack/clustering/v1/service.py index 0bdfaa024..dde009087 100644 --- a/openstack/cluster/v1/service.py +++ b/openstack/clustering/v1/service.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.cluster import cluster_service +from openstack.clustering import clustering_service from openstack import resource2 as resource @@ -18,7 +18,7 @@ class Service(resource.Resource): resource_key = 'service' resources_key = 'services' base_path = '/services' - service = cluster_service.ClusterService() + service = clustering_service.ClusteringService() # Capabilities allow_list = True diff --git a/openstack/cluster/version.py b/openstack/clustering/version.py similarity index 83% rename from openstack/cluster/version.py rename to openstack/clustering/version.py index 44549b60f..c08894012 100644 --- a/openstack/cluster/version.py +++ b/openstack/clustering/version.py @@ -11,7 +11,7 @@ # under the License. -from openstack.cluster import cluster_service +from openstack.clustering import clustering_service from openstack import resource2 as resource @@ -19,8 +19,8 @@ class Version(resource.Resource): resource_key = 'version' resources_key = 'versions' base_path = '/' - service = cluster_service.ClusterService( - version=cluster_service.ClusterService.UNVERSIONED + service = clustering_service.ClusteringService( + version=clustering_service.ClusteringService.UNVERSIONED ) # capabilities diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index a6bd829ef..9ba0ef6e4 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -198,7 +198,7 @@ def get_image_metadata(self, image): :rtype: :class:`~openstack.compute.v2.image.Image` """ res = self._get_base_resource(image, _image.Image) - metadata = res.get_metadata(self._session) + metadata = res.get_metadata(self) result = _image.Image.existing(id=res.id, metadata=metadata) return result @@ -219,7 +219,7 @@ def set_image_metadata(self, image, **metadata): :rtype: :class:`~openstack.compute.v2.image.Image` """ res = self._get_base_resource(image, _image.Image) - metadata = res.set_metadata(self._session, **metadata) + metadata = res.set_metadata(self, **metadata) result = _image.Image.existing(id=res.id, metadata=metadata) return result @@ -237,7 +237,7 @@ def delete_image_metadata(self, image, keys): :rtype: ``None`` """ res = self._get_base_resource(image, _image.Image) - return res.delete_metadata(self._session, keys) + return res.delete_metadata(self, keys) def create_keypair(self, **attrs): """Create a new keypair from attributes @@ -341,7 +341,7 @@ def delete_server(self, server, ignore_missing=True, force=False): """ if force: server = self._get_resource(_server.Server, server) - server.force_delete(self._session) + server.force_delete(self) else: self._delete(_server.Server, server, ignore_missing=ignore_missing) @@ -434,7 +434,7 @@ def change_server_password(self, server, new_password): :returns: None """ server = self._get_resource(_server.Server, server) - server.change_password(self._session, new_password) + server.change_password(self, new_password) def reset_server_state(self, server, state): """Reset the state of server @@ -447,7 +447,7 @@ def reset_server_state(self, server, state): :returns: None """ res = self._get_base_resource(server, _server.Server) - res.reset_state(self._session, state) + res.reset_state(self, state) def reboot_server(self, server, reboot_type): """Reboot a server @@ -460,7 +460,7 @@ def reboot_server(self, server, reboot_type): :returns: None """ server = self._get_resource(_server.Server, server) - server.reboot(self._session, reboot_type) + server.reboot(self, reboot_type) def rebuild_server(self, server, name, admin_password, **attrs): """Rebuild a server @@ -488,7 +488,7 @@ def rebuild_server(self, server, name, admin_password, **attrs): instance. """ server = self._get_resource(_server.Server, server) - return server.rebuild(self._session, name, admin_password, **attrs) + return server.rebuild(self, name, admin_password, **attrs) def resize_server(self, server, flavor): """Resize a server @@ -502,7 +502,7 @@ def resize_server(self, server, flavor): """ server = self._get_resource(_server.Server, server) flavor_id = resource2.Resource._get_id(flavor) - server.resize(self._session, flavor_id) + server.resize(self, flavor_id) def confirm_server_resize(self, server): """Confirm a server resize @@ -513,7 +513,7 @@ def confirm_server_resize(self, server): :returns: None """ server = self._get_resource(_server.Server, server) - server.confirm_resize(self._session) + server.confirm_resize(self) def revert_server_resize(self, server): """Revert a server resize @@ -524,7 +524,7 @@ def revert_server_resize(self, server): :returns: None """ server = self._get_resource(_server.Server, server) - server.revert_resize(self._session) + server.revert_resize(self) def create_server_image(self, server, name, metadata=None): """Create an image from a server @@ -537,7 +537,7 @@ def create_server_image(self, server, name, metadata=None): :returns: None """ server = self._get_resource(_server.Server, server) - server.create_image(self._session, name, metadata) + server.create_image(self, name, metadata) def add_security_group_to_server(self, server, security_group): """Add a security group to a server @@ -552,7 +552,7 @@ def add_security_group_to_server(self, server, security_group): """ server = self._get_resource(_server.Server, server) security_group_id = resource2.Resource._get_id(security_group) - server.add_security_group(self._session, security_group_id) + server.add_security_group(self, security_group_id) def remove_security_group_from_server(self, server, security_group): """Remove a security group from a server @@ -567,7 +567,7 @@ def remove_security_group_from_server(self, server, security_group): """ server = self._get_resource(_server.Server, server) security_group_id = resource2.Resource._get_id(security_group) - server.remove_security_group(self._session, security_group_id) + server.remove_security_group(self, security_group_id) def add_fixed_ip_to_server(self, server, network_id): """Adds a fixed IP address to a server instance. @@ -579,7 +579,7 @@ def add_fixed_ip_to_server(self, server, network_id): :returns: None """ server = self._get_resource(_server.Server, server) - server.add_fixed_ip(self._session, network_id) + server.add_fixed_ip(self, network_id) def remove_fixed_ip_from_server(self, server, address): """Removes a fixed IP address from a server instance. @@ -591,7 +591,7 @@ def remove_fixed_ip_from_server(self, server, address): :returns: None """ server = self._get_resource(_server.Server, server) - server.remove_fixed_ip(self._session, address) + server.remove_fixed_ip(self, address) def add_floating_ip_to_server(self, server, address, fixed_address=None): """Adds a floating IP address to a server instance. @@ -605,7 +605,7 @@ def add_floating_ip_to_server(self, server, address, fixed_address=None): :returns: None """ server = self._get_resource(_server.Server, server) - server.add_floating_ip(self._session, address, + server.add_floating_ip(self, address, fixed_address=fixed_address) def remove_floating_ip_from_server(self, server, address): @@ -618,7 +618,7 @@ def remove_floating_ip_from_server(self, server, address): :returns: None """ server = self._get_resource(_server.Server, server) - server.remove_floating_ip(self._session, address) + server.remove_floating_ip(self, address) def backup_server(self, server, name, backup_type, rotation): """Backup a server @@ -634,7 +634,7 @@ def backup_server(self, server, name, backup_type, rotation): :returns: None """ server = self._get_resource(_server.Server, server) - server.backup(self._session, name, backup_type, rotation) + server.backup(self, name, backup_type, rotation) def pause_server(self, server): """Pauses a server and changes its status to ``PAUSED``. @@ -644,7 +644,7 @@ def pause_server(self, server): :returns: None """ server = self._get_resource(_server.Server, server) - server.pause(self._session) + server.pause(self) def unpause_server(self, server): """Unpauses a paused server and changes its status to ``ACTIVE``. @@ -654,7 +654,7 @@ def unpause_server(self, server): :returns: None """ server = self._get_resource(_server.Server, server) - server.unpause(self._session) + server.unpause(self) def suspend_server(self, server): """Suspends a server and changes its status to ``SUSPENDED``. @@ -664,7 +664,7 @@ def suspend_server(self, server): :returns: None """ server = self._get_resource(_server.Server, server) - server.suspend(self._session) + server.suspend(self) def resume_server(self, server): """Resumes a suspended server and changes its status to ``ACTIVE``. @@ -674,7 +674,7 @@ def resume_server(self, server): :returns: None """ server = self._get_resource(_server.Server, server) - server.resume(self._session) + server.resume(self) def lock_server(self, server): """Locks a server. @@ -684,7 +684,7 @@ def lock_server(self, server): :returns: None """ server = self._get_resource(_server.Server, server) - server.lock(self._session) + server.lock(self) def unlock_server(self, server): """Unlocks a locked server. @@ -694,7 +694,7 @@ def unlock_server(self, server): :returns: None """ server = self._get_resource(_server.Server, server) - server.unlock(self._session) + server.unlock(self) def rescue_server(self, server, admin_pass=None, image_ref=None): """Puts a server in rescue mode and changes it status to ``RESCUE``. @@ -711,7 +711,7 @@ def rescue_server(self, server, admin_pass=None, image_ref=None): :returns: None """ server = self._get_resource(_server.Server, server) - server.rescue(self._session, admin_pass=admin_pass, + server.rescue(self, admin_pass=admin_pass, image_ref=image_ref) def unrescue_server(self, server): @@ -722,7 +722,7 @@ def unrescue_server(self, server): :returns: None """ server = self._get_resource(_server.Server, server) - server.unrescue(self._session) + server.unrescue(self) def evacuate_server(self, server, host=None, admin_pass=None, force=None): """Evacuates a server from a failed host to a new host. @@ -739,7 +739,7 @@ def evacuate_server(self, server, host=None, admin_pass=None, force=None): :returns: None """ server = self._get_resource(_server.Server, server) - server.evacuate(self._session, host=host, admin_pass=admin_pass, + server.evacuate(self, host=host, admin_pass=admin_pass, force=force) def start_server(self, server): @@ -750,7 +750,7 @@ def start_server(self, server): :returns: None """ server = self._get_resource(_server.Server, server) - server.start(self._session) + server.start(self) def stop_server(self, server): """Stops a running server and changes its state to ``SHUTOFF``. @@ -760,7 +760,7 @@ def stop_server(self, server): :returns: None """ server = self._get_resource(_server.Server, server) - server.stop(self._session) + server.stop(self) def shelve_server(self, server): """Shelves a server. @@ -775,7 +775,7 @@ def shelve_server(self, server): :returns: None """ server = self._get_resource(_server.Server, server) - server.shelve(self._session) + server.shelve(self) def unshelve_server(self, server): """Unselves or restores a shelved server. @@ -789,7 +789,7 @@ def unshelve_server(self, server): :returns: None """ server = self._get_resource(_server.Server, server) - server.unshelve(self._session) + server.unshelve(self) def get_server_console_output(self, server, length=None): """Return the console output for a server. @@ -802,11 +802,11 @@ def get_server_console_output(self, server, length=None): escaped to create a valid JSON string. """ server = self._get_resource(_server.Server, server) - return server.get_console_output(self._session, length=length) + return server.get_console_output(self, length=length) def wait_for_server(self, server, status='ACTIVE', failures=['ERROR'], interval=2, wait=120): - return resource2.wait_for_status(self._session, server, status, + return resource2.wait_for_status(self, server, status, failures, interval, wait) def create_server_interface(self, server, **attrs): @@ -938,7 +938,7 @@ def get_server_metadata(self, server): :rtype: :class:`~openstack.compute.v2.server.Server` """ res = self._get_base_resource(server, _server.Server) - metadata = res.get_metadata(self._session) + metadata = res.get_metadata(self) result = _server.Server.existing(id=res.id, metadata=metadata) return result @@ -959,7 +959,7 @@ def set_server_metadata(self, server, **metadata): :rtype: :class:`~openstack.compute.v2.server.Server` """ res = self._get_base_resource(server, _server.Server) - metadata = res.set_metadata(self._session, **metadata) + metadata = res.set_metadata(self, **metadata) result = _server.Server.existing(id=res.id, metadata=metadata) return result @@ -977,7 +977,7 @@ def delete_server_metadata(self, server, keys): :rtype: ``None`` """ res = self._get_base_resource(server, _server.Server) - return res.delete_metadata(self._session, keys) + return res.delete_metadata(self, keys) def create_server_group(self, **attrs): """Create a new server group from attributes @@ -1096,7 +1096,7 @@ def force_service_down(self, service, host, binary): :returns: None """ service = self._get_resource(_service.Service, service) - service.force_down(self._session, host, binary) + service.force_down(self, host, binary) def disable_service(self, service, host, binary, disabled_reason=None): """Disable a service @@ -1110,7 +1110,7 @@ def disable_service(self, service, host, binary, disabled_reason=None): :returns: None """ service = self._get_resource(_service.Service, service) - service.disable(self._session, + service.disable(self, host, binary, disabled_reason) @@ -1126,7 +1126,7 @@ def enable_service(self, service, host, binary): :returns: None """ service = self._get_resource(_service.Service, service) - service.enable(self._session, host, binary) + service.enable(self, host, binary) def services(self): """Return a generator of service @@ -1268,7 +1268,7 @@ def migrate_server(self, server): :returns: None """ server = self._get_resource(_server.Server, server) - server.migrate(self._session) + server.migrate(self) def live_migrate_server(self, server, host=None, force=False): """Migrate a server from one host to target host @@ -1281,4 +1281,4 @@ def live_migrate_server(self, server, host=None, force=False): :returns: None """ server = self._get_resource(_server.Server, server) - server.live_migrate(self._session, host, force) + server.live_migrate(self, host, force) diff --git a/openstack/compute/v2/keypair.py b/openstack/compute/v2/keypair.py index 26580068e..b1fd2e4cb 100644 --- a/openstack/compute/v2/keypair.py +++ b/openstack/compute/v2/keypair.py @@ -47,7 +47,7 @@ class Keypair(resource2.Resource): @classmethod def list(cls, session, paginated=False): - resp = session.get(cls.base_path, endpoint_filter=cls.service, + resp = session.get(cls.base_path, headers={"Accept": "application/json"}) resp = resp.json() resp = resp[cls.resources_key] diff --git a/openstack/compute/v2/limits.py b/openstack/compute/v2/limits.py index 113e2e85e..1e04f4d89 100644 --- a/openstack/compute/v2/limits.py +++ b/openstack/compute/v2/limits.py @@ -76,18 +76,18 @@ class Limits(resource2.Resource): absolute = resource2.Body("absolute", type=AbsoluteLimits) rate = resource2.Body("rate", type=list) - def get(self, session, requires_id=False): + def get(self, session, requires_id=False, error_message=None): """Get the Limits resource. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :returns: A Limits instance :rtype: :class:`~openstack.compute.v2.limits.Limits` """ request = self._prepare_request(requires_id=False, prepend_key=False) - response = session.get(request.uri, endpoint_filter=self.service) + response = session.get(request.url, error_message=error_message) body = response.json() body = body[self.resource_key] diff --git a/openstack/compute/v2/metadata.py b/openstack/compute/v2/metadata.py index e611fd98b..5fbb7e2c5 100644 --- a/openstack/compute/v2/metadata.py +++ b/openstack/compute/v2/metadata.py @@ -37,7 +37,7 @@ def _metadata(self, method, key=None, clear=False, delete=False, else: url = utils.urljoin(base, self.id, "metadata") - kwargs = {"endpoint_filter": self.service} + kwargs = {} if metadata or clear: # 'meta' is the key for singular modifications. # 'metadata' is the key for mass modifications. diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 574b72021..01ef2ba65 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -178,7 +178,7 @@ def _action(self, session, body): url = utils.urljoin(Server.base_path, self.id, 'action') headers = {'Accept': ''} return session.post( - url, endpoint_filter=self.service, json=body, headers=headers) + url, json=body, headers=headers) def change_password(self, session, new_password): """Change the administrator password to the given password.""" diff --git a/openstack/compute/v2/server_ip.py b/openstack/compute/v2/server_ip.py index bc90d645d..2f4ed9542 100644 --- a/openstack/compute/v2/server_ip.py +++ b/openstack/compute/v2/server_ip.py @@ -41,7 +41,7 @@ def list(cls, session, paginated=False, server_id=None, if network_label is not None: url = utils.urljoin(url, network_label) - resp = session.get(url, endpoint_filter=cls.service) + resp = session.get(url,) resp = resp.json() if network_label is None: diff --git a/openstack/compute/v2/service.py b/openstack/compute/v2/service.py index a36f67dff..ece512703 100644 --- a/openstack/compute/v2/service.py +++ b/openstack/compute/v2/service.py @@ -44,7 +44,7 @@ class Service(resource2.Resource): def _action(self, session, action, body): url = utils.urljoin(Service.base_path, action) - return session.put(url, endpoint_filter=self.service, json=body) + return session.put(url, json=body) def force_down(self, session, host, binary): """Force a service down.""" diff --git a/openstack/config/cloud_config.py b/openstack/config/cloud_config.py index 404769f69..7f655a396 100644 --- a/openstack/config/cloud_config.py +++ b/openstack/config/cloud_config.py @@ -166,10 +166,12 @@ def get_service_type(self, service_type): # still work. # What's even more amazing is that they did it AGAIN with cinder v3 # And then I learned that mistral copied it. - if service_type == 'volume': - if self.get_api_version(service_type).startswith('2'): + # TODO(shade) This should get removed when we have os-service-types + # alias support landed in keystoneauth. + if service_type in ('volume', 'block-storage'): + if self.get_api_version('volume').startswith('2'): service_type = 'volumev2' - elif self.get_api_version(service_type).startswith('3'): + elif self.get_api_version('volume').startswith('3'): service_type = 'volumev3' elif service_type == 'workflow': if self.get_api_version(service_type).startswith('2'): @@ -255,7 +257,7 @@ def get_session_client(self, service_key): service_type=self.get_service_type(service_key), service_name=self.get_service_name(service_key), interface=self.get_interface(service_key), - region_name=self.region) + region_name=self.get_region_name(service_key)) def _get_highest_endpoint(self, service_types, kwargs): session = self.get_session() diff --git a/openstack/connection.py b/openstack/connection.py index d83b77daa..beb071f2e 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -13,22 +13,37 @@ """ The :class:`~openstack.connection.Connection` class is the primary interface to the Python SDK it maintains a context for a connection to a cloud provider. -The connection has an attribute to access each supported service. The service -attributes are created dynamically based on user profiles and the service -catalog. +The connection has an attribute to access each supported service. Examples -------- At a minimum, the :class:`~openstack.connection.Connection` class needs to be -created with an authenticator or the parameters to build one. +created with a config or the parameters to build one. Create a connection ~~~~~~~~~~~~~~~~~~~ -The following example constructor uses the identity authenticator using -username and password. The default settings for the transport are used -by this connection.:: +The preferred way to create a connection is to manage named configuration +settings in your clouds.yaml file and refer to them by name.:: + + from openstack import connection + + conn = connection.Connection(cloud='example', region_name='earth1') + +If you already have an :class:`~openstack.config.cloud_config.CloudConfig` +you can pass it in instead.:: + + from openstack import connection + import openstack.config + + config = openstack.config.OpenStackConfig.get_one_cloud( + cloud='example', region_name='earth') + conn = connection.Connection(config=config) + +It's also possible to pass in parameters directly if needed. The following +example constructor uses the default identity password auth +plugin and provides a username and password.:: from openstack import connection auth_args = { @@ -42,45 +57,47 @@ List ~~~~ -Services are accessed through an attribute named after the service. A list -of all the projects is retrieved in this manner:: +Services are accessed through an attribute named after the service's official +service-type. A list of all the projects is retrieved in this manner:: projects = conn.identity.list_projects() Find or create ~~~~~~~~~~~~~~ -If you wanted to make sure you had a network named 'jenkins', you would first +If you wanted to make sure you had a network named 'zuul', you would first try to find it and if that fails, you would create it:: - network = conn.network.find_network("jenkins") + network = conn.network.find_network("zuul") if network is None: - network = conn.network.create_network({"name": "jenkins"}) + network = conn.network.create_network({"name": "zuul"}) """ +import importlib import logging import sys -from keystoneauth1.loading import base as ksa_loader -import openstack.config +import keystoneauth1.exceptions +import os_service_types +from openstack.cloud import task_manager +import openstack.config +from openstack.config import cloud_config from openstack import exceptions -from openstack import profile as _profile from openstack import proxy from openstack import proxy2 -from openstack import session as _session from openstack import utils _logger = logging.getLogger(__name__) def from_config(cloud_name=None, cloud_config=None, options=None): - """Create a Connection using os-client-config + """Create a Connection using openstack.config :param str cloud_name: Use the `cloud_name` configuration details when creating the Connection instance. :param cloud_config: An instance of `openstack.config.loader.OpenStackConfig` - as returned from the os-client-config library. + as returned from openstack.config. If no `config` is provided, `openstack.config.OpenStackConfig` will be called, and the provided `cloud_name` will be used in @@ -95,177 +112,205 @@ def from_config(cloud_name=None, cloud_config=None, options=None): :rtype: :class:`~openstack.connection.Connection` """ - # TODO(thowe): I proposed that service name defaults to None in OCC - defaults = {} - prof = _profile.Profile() - services = [service.service_type for service in prof.get_services()] - for service in services: - defaults[service + '_service_name'] = None - # TODO(thowe): default is 2 which turns into v2 which doesn't work - # this stuff needs to be fixed where we keep version and path separated. - defaults['network_api_version'] = 'v2.0' if cloud_config is None: - occ = openstack.config.OpenStackConfig(override_defaults=defaults) + occ = openstack.config.OpenStackConfig() cloud_config = occ.get_one_cloud(cloud=cloud_name, argparse=options) if cloud_config.debug: utils.enable_logging(True, stream=sys.stdout) - # TODO(mordred) we need to add service_type setting to openstacksdk. - # Some clouds have type overridden as well as name. - services = [service.service_type for service in prof.get_services()] - for service in cloud_config.get_services(): - if service in services: - version = cloud_config.get_api_version(service) - if version: - version = str(version) - if not version.startswith("v"): - version = "v" + version - prof.set_version(service, version) - name = cloud_config.get_service_name(service) - if name: - prof.set_name(service, name) - interface = cloud_config.get_interface(service) - if interface: - prof.set_interface(service, interface) - - region = cloud_config.get_region_name(service) - if region: - for service in services: - prof.set_region(service, region) - - # Auth - auth = cloud_config.config['auth'] - # TODO(thowe) We should be using auth_type - auth['auth_plugin'] = cloud_config.config['auth_type'] - if 'cacert' in auth: - auth['verify'] = auth.pop('cacert') - if 'cacert' in cloud_config.config: - auth['verify'] = cloud_config.config['cacert'] - insecure = cloud_config.config.get('insecure', False) - if insecure: - auth['verify'] = False - - cert = cloud_config.config.get('cert') - if cert: - key = cloud_config.config.get('key') - auth['cert'] = (cert, key) if key else cert - - return Connection(profile=prof, **auth) + return Connection(config=cloud_config) class Connection(object): - def __init__(self, session=None, authenticator=None, profile=None, - verify=True, cert=None, user_agent=None, - auth_plugin="password", - **auth_args): - """Create a context for a connection to a cloud provider. - - A connection needs a transport and an authenticator. The user may pass - in a transport and authenticator they want to use or they may pass in - the parameters to create a transport and authenticator. The connection - creates a - :class:`~openstack.session.Session` which uses the profile - and authenticator to perform HTTP requests. - + def __init__(self, cloud=None, config=None, session=None, + app_name=None, app_version=None, + # TODO(shade) Remove these once we've shifted + # python-openstackclient to not use the profile interface. + authenticator=None, profile=None, + **kwargs): + """Create a connection to a cloud. + + A connection needs information about how to connect, how to + authenticate and how to select the appropriate services to use. + + The recommended way to provide this information is by referencing + a named cloud config from an existing `clouds.yaml` file. The cloud + name ``envvars`` may be used to consume a cloud configured via ``OS_`` + environment variables. + + A pre-existing :class:`~openstack.config.cloud_config.CloudConfig` + object can be passed in lieu of a cloud name, for cases where the user + already has a fully formed CloudConfig and just wants to use it. + + Similarly, if for some reason the user already has a + :class:`~keystoneauth1.session.Session` and wants to use it, it may be + passed in. + + :param str cloud: Name of the cloud from config to use. + :param config: CloudConfig object representing the config for the + region of the cloud in question. + :type config: :class:`~openstack.config.cloud_config.CloudConfig` :param session: A session object compatible with - :class:`~openstack.session.Session`. - :type session: :class:`~openstack.session.Session` - :param authenticator: An authenticator derived from the base - authenticator plugin that was previously created. Two common - authentication identity plugins are - :class:`identity_v2 ` and - :class:`identity_v3 `. - If this parameter is not passed in, the connection will create an - authenticator. - :type authenticator: :class:`~openstack.auth.base.BaseAuthPlugin` - :param profile: If the user has any special profiles such as the - service name, region, version or interface, they may be provided - in the profile object. If no profiles are provided, the - services that appear first in the service catalog will be used. - :type profile: :class:`~openstack.profile.Profile` - :param bool verify: If a transport is not provided to the connection, - this parameter will be used to create a transport. If ``verify`` - is set to true, which is the default, the SSL cert will be - verified. It can also be set to a CA_BUNDLE path. - :param cert: If a transport is not provided to the connection then this - parameter will be used to create a transport. `cert` allows to - provide a client certificate file path or a tuple with client - certificate and key paths. - :type cert: str or tuple - :param str user_agent: If a transport is not provided to the - connection, this parameter will be used when creating a transport. - The value given here will be prepended to the default, which is - specified in :attr:`~openstack.transport.USER_AGENT`. - The resulting ``user_agent`` value is used for the ``User-Agent`` - HTTP header. - :param str auth_plugin: The name of authentication plugin to use. - The default value is ``password``. - :param auth_args: The rest of the parameters provided are assumed to be - authentication arguments that are used by the authentication - plugin. + :class:`~keystoneauth1.session.Session`. + :type session: :class:`~keystoneauth1.session.Session` + :param str app_name: Name of the application to be added to User Agent. + :param str app_version: Version of the application to be added to + User Agent. + :param authenticator: DEPRECATED. Only exists for short-term backwards + compatibility for python-openstackclient while we + transition. + :param profile: DEPRECATED. Only exists for short-term backwards + compatibility for python-openstackclient while we + transition. + :param kwargs: If a config is not provided, the rest of the parameters + provided are assumed to be arguments to be passed to the + CloudConfig contructor. """ - self.profile = profile if profile else _profile.Profile() + self.config = config + self.service_type_manager = os_service_types.ServiceTypes() + + if not self.config: + openstack_config = openstack.config.OpenStackConfig( + app_name=app_name, app_version=app_version, + load_yaml_config=profile is None) + if profile: + # TODO(shade) Remove this once we've shifted + # python-openstackclient to not use the profile interface. + self.config = self._get_config_from_profile( + openstack_config, profile, authenticator, **kwargs) + else: + self.config = openstack_config.get_one_cloud( + cloud=cloud, validate=session is None, **kwargs) + + self.task_manager = task_manager.TaskManager( + name=':'.join([self.config.name, self.config.region])) + if session: - # Make sure it is the right kind of session. A keystoneauth1 - # session would work in some ways but show strange errors in - # others. E.g. a Resource.find would work with an id but fail when - # given a name because it attempts to catch - # openstack.exceptions.NotFoundException to signal that a search by - # ID failed before trying a search by name, but with a - # keystoneauth1 session the lookup by ID raises - # keystoneauth1.exceptions.NotFound instead. We need to ensure our - # Session class gets used so that our implementation of various - # methods always works as we expect. - if not isinstance(session, _session.Session): - raise exceptions.SDKException( - 'Session instance is from %s but must be from %s' % - (session.__module__, _session.__name__)) - self.session = session - else: - self.authenticator = self._create_authenticator(authenticator, - auth_plugin, - **auth_args) - self.session = _session.Session( - self.profile, auth=self.authenticator, verify=verify, - cert=cert, user_agent=user_agent) + # TODO(mordred) Expose constructor option for this in OCC + self.config._keystone_session = session + + self.session = self.config.get_session() self._open() - def _create_authenticator(self, authenticator, auth_plugin, **args): - if authenticator: - return authenticator - # TODO(thowe): Jamie was suggesting we should support other - # ways of loading the plugin - loader = ksa_loader.get_plugin_loader(auth_plugin) - load_args = {} - for opt in loader.get_options(): - if args.get(opt.dest): - load_args[opt.dest] = args[opt.dest] - return loader.load_from_options(**load_args) + def _get_config_from_profile( + self, openstack_config, profile, authenticator, **kwargs): + """Get openstack.config objects from legacy profile.""" + # TODO(shade) Remove this once we've shifted python-openstackclient + # to not use the profile interface. + config = openstack_config.get_one_cloud( + cloud='defaults', validate=False, **kwargs) + config._auth = authenticator + + for service in profile.get_services(): + service_type = service.service_type + if service.interface: + key = cloud_config._make_key('interface', service_type) + config.config[key] = service.interface + if service.region: + key = cloud_config._make_key('region_name', service_type) + config.config[key] = service.region + if service.version: + version = service.version + if version.startswith('v'): + version = version[1:] + key = cloud_config._make_key('api_version', service_type) + config.config[key] = service.version + return config def _open(self): - """Open the connection. - - NOTE(thowe): Have this set up some lazy loader instead. - """ - for service in self.profile.get_services(): - self._load(service) - - def _load(self, service): - attr_name = service.get_service_module() - module = service.get_module() + "._proxy" - try: - __import__(module) - proxy_class = getattr(sys.modules[module], "Proxy") + """Open the connection. """ + for service in self.service_type_manager.services: + self._load(service['service_type']) + # TODO(mordred) openstacksdk has support for the metric service + # which is not in service-types-authority. What do we do about that? + self._load('metric') + + def _load(self, service_type): + service = self._get_service(service_type) + + if service: + module_name = service.get_module() + "._proxy" + module = importlib.import_module(module_name) + proxy_class = getattr(module, "Proxy") if not (issubclass(proxy_class, proxy.BaseProxy) or issubclass(proxy_class, proxy2.BaseProxy)): raise TypeError("%s.Proxy must inherit from BaseProxy" % proxy_class.__module__) - setattr(self, attr_name, proxy_class(self.session)) - except Exception as e: - _logger.warn("Unable to load %s: %s" % (module, e)) + else: + # If we don't have a proxy, just instantiate BaseProxy so that + # we get an adapter. + proxy_class = proxy2.BaseProxy + + proxy_object = proxy_class( + session=self.config.get_session(), + task_manager=self.task_manager, + allow_version_hack=True, + service_type=self.config.get_service_type(service_type), + service_name=self.config.get_service_name(service_type), + interface=self.config.get_interface(service_type), + region_name=self.config.region, + version=self.config.get_api_version(service_type) + ) + all_types = self.service_type_manager.get_all_types(service_type) + # Register the proxy class with every known alias + for attr_name in [name.replace('-', '_') for name in all_types]: + setattr(self, attr_name, proxy_object) + + def _get_all_types(self, service_type): + # We make connection attributes for all official real type names + # and aliases. Three services have names they were called by in + # openstacksdk that are not covered by Service Types Authority aliases. + # Include them here - but take heed, no additional values should ever + # be added to this list. + # that were only used in openstacksdk resource naming. + LOCAL_ALIASES = { + 'baremetal': 'bare_metal', + 'block_storage': 'block_store', + 'clustering': 'cluster', + } + all_types = self.service_type_manager.get_all_types(service_type) + if service_type in LOCAL_ALIASES: + all_types.append(LOCAL_ALIASES[service_type]) + return all_types + + def _get_service(self, official_service_type): + service_class = None + for service_type in self._get_all_types(official_service_type): + service_class = self._find_service_class(service_type) + if service_class: + break + if not service_class: + return None + # TODO(mordred) Replace this with proper discovery + version_string = self.config.get_api_version(official_service_type) + version = None + if version_string: + version = 'v{version}'.format(version=version_string[0]) + return service_class(version=version) + + def _find_service_class(self, service_type): + package_name = 'openstack.{service_type}'.format( + service_type=service_type).replace('-', '_') + module_name = service_type.replace('-', '_') + '_service' + class_name = ''.join( + [part.capitalize() for part in module_name.split('_')]) + try: + import_name = '.'.join([package_name, module_name]) + service_module = importlib.import_module(import_name) + except ImportError: + return None + service_class = getattr(service_module, class_name, None) + if not service_class: + _logger.warn( + 'Unable to find class {class_name} in module for service' + ' for service {service_type}'.format( + class_name=class_name, + service_type=service_type)) + return None + return service_class def authorize(self): """Authorize this Connection @@ -278,9 +323,10 @@ def authorize(self): :raises: :class:`~openstack.exceptions.HttpException` if the authorization fails due to reasons like the credentials - provided are unable to be authorized or the `auth_plugin` + provided are unable to be authorized or the `auth_type` argument is missing, etc. """ - headers = self.session.get_auth_headers() - - return headers.get('X-Auth-Token') if headers else None + try: + return self.session.get_token() + except keystoneauth1.exceptions.ClientException as e: + raise exceptions.raise_from_response(e.response) diff --git a/openstack/database/v1/instance.py b/openstack/database/v1/instance.py index 97660a981..30d03cc9a 100644 --- a/openstack/database/v1/instance.py +++ b/openstack/database/v1/instance.py @@ -60,12 +60,12 @@ def enable_root_user(self, session): and provides the user with a generated root password. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :returns: A dictionary with keys ``name`` and ``password`` specifying the login credentials. """ url = utils.urljoin(self.base_path, self.id, 'root') - resp = session.post(url, endpoint_filter=self.service) + resp = session.post(url,) return resp.json()['user'] def is_root_enabled(self, session): @@ -74,12 +74,12 @@ def is_root_enabled(self, session): Determine if root is enabled on this particular instance. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :returns: ``True`` if root user is enabled for a specified database instance or ``False`` otherwise. """ url = utils.urljoin(self.base_path, self.id, 'root') - resp = session.get(url, endpoint_filter=self.service) + resp = session.get(url,) return resp.json()['rootEnabled'] def restart(self, session): @@ -89,7 +89,7 @@ def restart(self, session): """ body = {'restart': {}} url = utils.urljoin(self.base_path, self.id, 'action') - session.post(url, endpoint_filter=self.service, json=body) + session.post(url, json=body) def resize(self, session, flavor_reference): """Resize the database instance @@ -98,7 +98,7 @@ def resize(self, session, flavor_reference): """ body = {'resize': {'flavorRef': flavor_reference}} url = utils.urljoin(self.base_path, self.id, 'action') - session.post(url, endpoint_filter=self.service, json=body) + session.post(url, json=body) def resize_volume(self, session, volume_size): """Resize the volume attached to the instance @@ -107,4 +107,4 @@ def resize_volume(self, session, volume_size): """ body = {'resize': {'volume': volume_size}} url = utils.urljoin(self.base_path, self.id, 'action') - session.post(url, endpoint_filter=self.service, json=body) + session.post(url, json=body) diff --git a/openstack/exceptions.py b/openstack/exceptions.py index 069f020d1..d56c92edc 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -18,15 +18,17 @@ import re +from requests import exceptions as _rex import six class SDKException(Exception): """The base exception class for all exceptions this library raises.""" - def __init__(self, message=None, cause=None): + def __init__(self, message=None, extra_data=None): self.message = self.__class__.__name__ if message is None else message - self.cause = cause + self.extra_data = extra_data super(SDKException, self).__init__(self.message) +OpenStackCloudException = SDKException class EndpointNotFound(SDKException): @@ -50,24 +52,53 @@ def __init__(self, message=None): super(InvalidRequest, self).__init__(message) -class HttpException(SDKException): - - def __init__(self, message=None, details=None, response=None, - request_id=None, url=None, method=None, - http_status=None, cause=None): - super(HttpException, self).__init__(message=message, cause=cause) +class HttpException(SDKException, _rex.HTTPError): + + def __init__(self, message='Error', response=None, + http_status=None, + details=None, request_id=None): + # TODO(shade) Remove http_status parameter and the ability for response + # to be None once we're not mocking Session everywhere. + if not message: + if response: + message = "{name}: {code}".format( + name=self.__class__.__name__, + code=response.status_code) + else: + message = "{name}: Unknown error".format( + name=self.__class__.__name__) + + # Call directly rather than via super to control parameters + SDKException.__init__(self, message=message) + _rex.HTTPError.__init__(self, message, response=response) + + if response: + self.request_id = response.headers.get('x-openstack-request-id') + self.status_code = response.status_code + else: + self.request_id = None + self.status_code = http_status self.details = details - self.response = response - self.request_id = request_id - self.url = url - self.method = method - self.http_status = http_status + self.url = self.request and self.request.url or None + self.method = self.request and self.request.method or None + self.source = "Server" + if self.status_code is not None and (400 <= self.status_code < 500): + self.source = "Client" def __unicode__(self): - msg = self.__class__.__name__ + ": " + self.message + if not self.url and not self.url: + return super(HttpException, self).__str__() + if self.url: + remote_error = "{source} Error for url: {url}".format( + source=self.source, url=self.url) + if self.details: + remote_error += ', ' if self.details: - msg += ", " + six.text_type(self.details) - return msg + remote_error += six.text_type(self.details) + + return "{message}: {remote_error}".format( + message=super(HttpException, self).__str__(), + remote_error=remote_error) def __str__(self): return self.__unicode__() @@ -78,6 +109,11 @@ class NotFoundException(HttpException): pass +class BadRequestException(HttpException): + """HTTP 400 Bad Request.""" + pass + + class MethodNotSupported(SDKException): """The resource does not support this operation type.""" def __init__(self, resource, method): @@ -112,37 +148,49 @@ class ResourceFailure(SDKException): pass -def from_exception(exc): - """Return an instance of an HTTPException based on httplib response.""" - if exc.response.status_code == 404: +def raise_from_response(response, error_message=None): + """Raise an instance of an HTTPException based on keystoneauth response.""" + if response.status_code < 400: + return + + if response.status_code == 404: cls = NotFoundException + elif response.status_code == 400: + cls = BadRequestException else: cls = HttpException - resp = exc.response - details = resp.text - resp_body = resp.content - content_type = resp.headers.get('content-type', '') - if resp_body and 'application/json' in content_type: + details = None + content_type = response.headers.get('content-type', '') + if response.content and 'application/json' in content_type: # Iterate over the nested objects to retrieve "message" attribute. - messages = [obj.get('message') for obj in resp.json().values() - if isinstance(obj, dict)] - # Join all of the messages together nicely and filter out any objects - # that don't have a "message" attr. - details = '\n'.join(msg for msg in messages if msg) + # TODO(shade) Add exception handling for times when the content type + # is lying. - elif resp_body and 'text/html' in content_type: + try: + content = response.json() + messages = [obj.get('message') for obj in content.values() + if isinstance(obj, dict)] + # Join all of the messages together nicely and filter out any + # objects that don't have a "message" attr. + details = '\n'.join(msg for msg in messages if msg) + except Exception: + details = response.text + elif response.content and 'text/html' in content_type: # Split the lines, strip whitespace and inline HTML from the response. details = [re.sub(r'<.+?>', '', i.strip()) - for i in details.splitlines()] - details = [msg for msg in details if msg] - # Remove duplicates from the list. - details_temp = [] - for detail in details: - if detail not in details_temp: - details_temp.append(detail) + for i in response.text.splitlines()] + details = list(set([msg for msg in details if msg])) # Return joined string separated by colons. - details = ': '.join(details_temp) - return cls(details=details, message=exc.message, response=exc.response, - request_id=exc.request_id, url=exc.url, method=exc.method, - http_status=exc.http_status, cause=exc) + details = ': '.join(details) + if not details and response.reason: + details = response.reason + else: + details = response.text + + raise cls(message=error_message, response=response, details=details) + + +class ArgumentDeprecationWarning(Warning): + """A deprecated argument has been provided.""" + pass diff --git a/openstack/identity/v2/extension.py b/openstack/identity/v2/extension.py index b4160aad4..e8ba4fcfd 100644 --- a/openstack/identity/v2/extension.py +++ b/openstack/identity/v2/extension.py @@ -46,7 +46,7 @@ class Extension(resource.Resource): @classmethod def list(cls, session, paginated=False, **params): - resp = session.get(cls.base_path, endpoint_filter=cls.service, + resp = session.get(cls.base_path, params=params) resp = resp.json() for data in resp[cls.resources_key]['values']: diff --git a/openstack/identity/v3/domain.py b/openstack/identity/v3/domain.py index 42e57649d..f358e4913 100644 --- a/openstack/identity/v3/domain.py +++ b/openstack/identity/v3/domain.py @@ -49,7 +49,7 @@ def assign_role_to_user(self, session, user, role): """Assign role to user on domain""" url = utils.urljoin(self.base_path, self.id, 'users', user.id, 'roles', role.id) - resp = session.put(url, endpoint_filter=self.service) + resp = session.put(url,) if resp.status_code == 204: return True return False @@ -58,7 +58,7 @@ def validate_user_has_role(self, session, user, role): """Validates that a user has a role on a domain""" url = utils.urljoin(self.base_path, self.id, 'users', user.id, 'roles', role.id) - resp = session.head(url, endpoint_filter=self.service) + resp = session.head(url,) if resp.status_code == 201: return True return False @@ -67,7 +67,7 @@ def unassign_role_from_user(self, session, user, role): """Unassigns a role from a user on a domain""" url = utils.urljoin(self.base_path, self.id, 'users', user.id, 'roles', role.id) - resp = session.delete(url, endpoint_filter=self.service) + resp = session.delete(url,) if resp.status_code == 204: return True return False @@ -76,7 +76,7 @@ def assign_role_to_group(self, session, group, role): """Assign role to group on domain""" url = utils.urljoin(self.base_path, self.id, 'groups', group.id, 'roles', role.id) - resp = session.put(url, endpoint_filter=self.service) + resp = session.put(url,) if resp.status_code == 204: return True return False @@ -85,7 +85,7 @@ def validate_group_has_role(self, session, group, role): """Validates that a group has a role on a domain""" url = utils.urljoin(self.base_path, self.id, 'groups', group.id, 'roles', role.id) - resp = session.head(url, endpoint_filter=self.service) + resp = session.head(url,) if resp.status_code == 201: return True return False @@ -94,7 +94,7 @@ def unassign_role_from_group(self, session, group, role): """Unassigns a role from a group on a domain""" url = utils.urljoin(self.base_path, self.id, 'groups', group.id, 'roles', role.id) - resp = session.delete(url, endpoint_filter=self.service) + resp = session.delete(url,) if resp.status_code == 204: return True return False diff --git a/openstack/identity/v3/project.py b/openstack/identity/v3/project.py index 3a1ab714c..ab28e359f 100644 --- a/openstack/identity/v3/project.py +++ b/openstack/identity/v3/project.py @@ -56,7 +56,7 @@ def assign_role_to_user(self, session, user, role): """Assign role to user on project""" url = utils.urljoin(self.base_path, self.id, 'users', user.id, 'roles', role.id) - resp = session.put(url, endpoint_filter=self.service) + resp = session.put(url,) if resp.status_code == 204: return True return False @@ -65,7 +65,7 @@ def validate_user_has_role(self, session, user, role): """Validates that a user has a role on a project""" url = utils.urljoin(self.base_path, self.id, 'users', user.id, 'roles', role.id) - resp = session.head(url, endpoint_filter=self.service) + resp = session.head(url,) if resp.status_code == 201: return True return False @@ -74,7 +74,7 @@ def unassign_role_from_user(self, session, user, role): """Unassigns a role from a user on a project""" url = utils.urljoin(self.base_path, self.id, 'users', user.id, 'roles', role.id) - resp = session.delete(url, endpoint_filter=self.service) + resp = session.delete(url,) if resp.status_code == 204: return True return False @@ -83,7 +83,7 @@ def assign_role_to_group(self, session, group, role): """Assign role to group on project""" url = utils.urljoin(self.base_path, self.id, 'groups', group.id, 'roles', role.id) - resp = session.put(url, endpoint_filter=self.service) + resp = session.put(url,) if resp.status_code == 204: return True return False @@ -92,7 +92,7 @@ def validate_group_has_role(self, session, group, role): """Validates that a group has a role on a project""" url = utils.urljoin(self.base_path, self.id, 'groups', group.id, 'roles', role.id) - resp = session.head(url, endpoint_filter=self.service) + resp = session.head(url,) if resp.status_code == 201: return True return False @@ -101,7 +101,7 @@ def unassign_role_from_group(self, session, group, role): """Unassigns a role from a group on a project""" url = utils.urljoin(self.base_path, self.id, 'groups', group.id, 'roles', role.id) - resp = session.delete(url, endpoint_filter=self.service) + resp = session.delete(url,) if resp.status_code == 204: return True return False diff --git a/openstack/identity/version.py b/openstack/identity/version.py index 4564cab9d..1f8442a68 100644 --- a/openstack/identity/version.py +++ b/openstack/identity/version.py @@ -32,7 +32,7 @@ class Version(resource.Resource): @classmethod def list(cls, session, paginated=False, **params): - resp = session.get(cls.base_path, endpoint_filter=cls.service, + resp = session.get(cls.base_path, params=params) resp = resp.json() for data in resp[cls.resources_key]['values']: diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 9de368a7c..55d7db916 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -56,7 +56,7 @@ def upload_image(self, container_format=None, disk_format=None, # return anything anyway. Otherwise this blocks while uploading # significant amounts of image data. img.data = data - img.upload(self._session) + img.upload(self) return img @@ -92,7 +92,7 @@ def download_image(self, image, stream=False): """ image = self._get_resource(_image.Image, image) - return image.download(self._session, stream=stream) + return image.download(self, stream=stream) def delete_image(self, image, ignore_missing=True): """Delete an image @@ -158,7 +158,7 @@ def update_image(self, image, **attrs): :rtype: :class:`~openstack.image.v2.image.Image` """ img = self._get_resource(_image.Image, image) - return img.update(self._session, **attrs) + return img.update(self, **attrs) def deactivate_image(self, image): """Deactivate an image @@ -169,7 +169,7 @@ def deactivate_image(self, image): :returns: None """ image = self._get_resource(_image.Image, image) - image.deactivate(self._session) + image.deactivate(self) def reactivate_image(self, image): """Deactivate an image @@ -180,7 +180,7 @@ def reactivate_image(self, image): :returns: None """ image = self._get_resource(_image.Image, image) - image.reactivate(self._session) + image.reactivate(self) def add_tag(self, image, tag): """Add a tag to an image @@ -193,7 +193,7 @@ def add_tag(self, image, tag): :returns: None """ image = self._get_resource(_image.Image, image) - image.add_tag(self._session, tag) + image.add_tag(self, tag) def remove_tag(self, image, tag): """Remove a tag to an image @@ -206,7 +206,7 @@ def remove_tag(self, image, tag): :returns: None """ image = self._get_resource(_image.Image, image) - image.remove_tag(self._session, tag) + image.remove_tag(self, tag) def add_member(self, image, **attrs): """Create a new member from attributes diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index 61f970fad..79af09ea0 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -214,7 +214,7 @@ class Image(resource2.Resource): def _action(self, session, action): """Call an action on an image ID.""" url = utils.urljoin(self.base_path, self.id, 'actions', action) - return session.post(url, endpoint_filter=self.service) + return session.post(url,) def deactivate(self, session): """Deactivate an image @@ -234,17 +234,17 @@ def reactivate(self, session): def add_tag(self, session, tag): """Add a tag to an image""" url = utils.urljoin(self.base_path, self.id, 'tags', tag) - session.put(url, endpoint_filter=self.service) + session.put(url,) def remove_tag(self, session, tag): """Remove a tag from an image""" url = utils.urljoin(self.base_path, self.id, 'tags', tag) - session.delete(url, endpoint_filter=self.service) + session.delete(url,) def upload(self, session): """Upload data into an existing image""" url = utils.urljoin(self.base_path, self.id, 'file') - session.put(url, endpoint_filter=self.service, data=self.data, + session.put(url, data=self.data, headers={"Content-Type": "application/octet-stream", "Accept": ""}) @@ -253,7 +253,7 @@ def download(self, session, stream=False): # TODO(briancurtin): This method should probably offload the get # operation into another thread or something of that nature. url = utils.urljoin(self.base_path, self.id, 'file') - resp = session.get(url, endpoint_filter=self.service, stream=stream) + resp = session.get(url, stream=stream) # See the following bug report for details on why the checksum # code may sometimes depend on a second GET call. @@ -294,7 +294,7 @@ def update(self, session, **attrs): } original = self.to_dict() patch_string = jsonpatch.make_patch(original, attrs).to_string() - resp = session.patch(url, endpoint_filter=self.service, + resp = session.patch(url, data=patch_string, headers=headers) self._translate_response(resp, has_body=True) diff --git a/openstack/key_manager/v1/secret.py b/openstack/key_manager/v1/secret.py index 603b09ef0..de11b28e7 100644 --- a/openstack/key_manager/v1/secret.py +++ b/openstack/key_manager/v1/secret.py @@ -81,8 +81,7 @@ class Secret(resource2.Resource): def get(self, session, requires_id=True): request = self._prepare_request(requires_id=requires_id) - response = session.get(request.uri, - endpoint_filter=self.service).json() + response = session.get(request.url).json() content_type = None if self.payload_content_type is not None: @@ -93,8 +92,7 @@ def get(self, session, requires_id=True): # Only try to get the payload if a content type has been explicitly # specified or if one was found in the metadata response if content_type is not None: - payload = session.get(utils.urljoin(request.uri, "payload"), - endpoint_filter=self.service, + payload = session.get(utils.urljoin(request.url, "payload"), headers={"Accept": content_type}) response["payload"] = payload.text diff --git a/openstack/message/v1/_proxy.py b/openstack/message/v1/_proxy.py index bdd25ef04..86a3af367 100644 --- a/openstack/message/v1/_proxy.py +++ b/openstack/message/v1/_proxy.py @@ -64,7 +64,7 @@ def create_messages(self, values): :class:`~openstack.message.v1.message.Message` objects that were created. """ - return message.Message.create_messages(self._session, values) + return message.Message.create_messages(self, values) @utils.deprecated(deprecated_in="0.9.16", removed_in="0.9.17", details="Message v1 is deprecated since 2014. Use v2.") @@ -78,7 +78,7 @@ def claim_messages(self, value): :class:`~openstack.message.v1.message.Message` objects that were claimed. """ - return claim.Claim.claim_messages(self._session, value) + return claim.Claim.claim_messages(self, value) @utils.deprecated(deprecated_in="0.9.16", removed_in="0.9.17", details="Message v1 is deprecated since 2014. Use v2.") @@ -90,4 +90,4 @@ def delete_message(self, value): :returns: ``None`` """ - message.Message.delete_by_id(self._session, value) + message.Message.delete_by_id(self, value) diff --git a/openstack/message/v1/claim.py b/openstack/message/v1/claim.py index 5b8c25f61..d993a443f 100644 --- a/openstack/message/v1/claim.py +++ b/openstack/message/v1/claim.py @@ -60,7 +60,7 @@ def claim_messages(cls, session, claim): body = [] try: - resp = session.post(url, endpoint_filter=cls.service, + resp = session.post(url, headers=headers, data=json.dumps(claim, cls=ClaimEncoder), params=params) diff --git a/openstack/message/v1/message.py b/openstack/message/v1/message.py index 4650032ee..6d6f703f0 100644 --- a/openstack/message/v1/message.py +++ b/openstack/message/v1/message.py @@ -69,7 +69,7 @@ def create_messages(cls, session, messages): url = cls._get_url({'queue_name': messages[0].queue_name}) headers = {'Client-ID': messages[0].client_id} - resp = session.post(url, endpoint_filter=cls.service, headers=headers, + resp = session.post(url, headers=headers, data=json.dumps(messages, cls=MessageEncoder)) resp = resp.json() @@ -99,7 +99,7 @@ def delete_by_id(cls, session, message, path_args=None): 'Client-ID': message.client_id, 'Accept': '', } - session.delete(url, endpoint_filter=cls.service, headers=headers) + session.delete(url, headers=headers) class MessageEncoder(json.JSONEncoder): diff --git a/openstack/message/v1/queue.py b/openstack/message/v1/queue.py index dd10420fa..034ce3c2a 100644 --- a/openstack/message/v1/queue.py +++ b/openstack/message/v1/queue.py @@ -30,5 +30,5 @@ class Queue(resource.Resource): def create_by_id(cls, session, attrs, resource_id=None, path_args=None): url = cls._get_url(path_args, resource_id) headers = {'Accept': ''} - session.put(url, endpoint_filter=cls.service, headers=headers) + session.put(url, headers=headers) return {cls.id_attribute: resource_id} diff --git a/openstack/message/v2/_proxy.py b/openstack/message/v2/_proxy.py index 7652ec6c7..948277278 100644 --- a/openstack/message/v2/_proxy.py +++ b/openstack/message/v2/_proxy.py @@ -87,7 +87,7 @@ def post_message(self, queue_name, messages): """ message = self._get_resource(_message.Message, None, queue_name=queue_name) - return message.post(self._session, messages) + return message.post(self, messages) def messages(self, queue_name, **query): """Retrieve a generator of messages diff --git a/openstack/message/v2/claim.py b/openstack/message/v2/claim.py index 135858726..6125418de 100644 --- a/openstack/message/v2/claim.py +++ b/openstack/message/v2/claim.py @@ -71,7 +71,7 @@ def create(self, session, prepend_key=False): "X-PROJECT-ID": self.project_id or session.get_project_id() } request.headers.update(headers) - response = session.post(request.uri, endpoint_filter=self.service, + response = session.post(request.url, json=request.body, headers=request.headers) # For case no message was claimed successfully, 204 No Content @@ -90,7 +90,7 @@ def get(self, session, requires_id=True): } request.headers.update(headers) - response = session.get(request.uri, endpoint_filter=self.service, + response = session.get(request.url, headers=request.headers) self._translate_response(response) @@ -104,7 +104,7 @@ def update(self, session, prepend_key=False, has_body=False): } request.headers.update(headers) - session.patch(request.uri, endpoint_filter=self.service, + session.patch(request.url, json=request.body, headers=request.headers) return self @@ -117,7 +117,7 @@ def delete(self, session): } request.headers.update(headers) - response = session.delete(request.uri, endpoint_filter=self.service, + response = session.delete(request.url, headers=request.headers) self._translate_response(response, has_body=False) diff --git a/openstack/message/v2/message.py b/openstack/message/v2/message.py index 668f69c06..a889f14d2 100644 --- a/openstack/message/v2/message.py +++ b/openstack/message/v2/message.py @@ -63,7 +63,7 @@ def post(self, session, messages): } request.headers.update(headers) request.body = {'messages': messages} - response = session.post(request.uri, endpoint_filter=self.service, + response = session.post(request.url, json=request.body, headers=request.headers) return response.json()['resources'] @@ -86,7 +86,7 @@ def list(cls, session, paginated=True, **params): query_params = cls._query_mapping._transpose(params) while more_data: - resp = session.get(uri, endpoint_filter=cls.service, + resp = session.get(uri, headers=headers, params=query_params) resp = resp.json() resp = resp[cls.resources_key] @@ -117,7 +117,7 @@ def get(self, session, requires_id=True): } request.headers.update(headers) - response = session.get(request.uri, endpoint_filter=self.service, + response = session.get(request.url, headers=headers) self._translate_response(response) @@ -135,8 +135,8 @@ def delete(self, session): # parameter when deleting a message that has been claimed, we # rebuild the request URI if claim_id is not None. if self.claim_id: - request.uri += '?claim_id=%s' % self.claim_id - response = session.delete(request.uri, endpoint_filter=self.service, + request.url += '?claim_id=%s' % self.claim_id + response = session.delete(request.url, headers=headers) self._translate_response(response, has_body=False) diff --git a/openstack/message/v2/queue.py b/openstack/message/v2/queue.py index 49f79f6e1..7be063789 100644 --- a/openstack/message/v2/queue.py +++ b/openstack/message/v2/queue.py @@ -60,7 +60,7 @@ def create(self, session, prepend_key=True): "X-PROJECT-ID": self.project_id or session.get_project_id() } request.headers.update(headers) - response = session.put(request.uri, endpoint_filter=self.service, + response = session.put(request.url, json=request.body, headers=request.headers) self._translate_response(response, has_body=False) @@ -84,7 +84,7 @@ def list(cls, session, paginated=False, **params): } while more_data: - resp = session.get(uri, endpoint_filter=cls.service, + resp = session.get(uri, headers=headers, params=query_params) resp = resp.json() resp = resp[cls.resources_key] @@ -114,7 +114,7 @@ def get(self, session, requires_id=True): "X-PROJECT-ID": self.project_id or session.get_project_id() } request.headers.update(headers) - response = session.get(request.uri, endpoint_filter=self.service, + response = session.get(request.url, headers=headers) self._translate_response(response) @@ -127,7 +127,7 @@ def delete(self, session): "X-PROJECT-ID": self.project_id or session.get_project_id() } request.headers.update(headers) - response = session.delete(request.uri, endpoint_filter=self.service, + response = session.delete(request.url, headers=headers) self._translate_response(response, has_body=False) diff --git a/openstack/message/v2/subscription.py b/openstack/message/v2/subscription.py index d5acdb059..e95329fe2 100644 --- a/openstack/message/v2/subscription.py +++ b/openstack/message/v2/subscription.py @@ -68,7 +68,7 @@ def create(self, session, prepend_key=True): "X-PROJECT-ID": self.project_id or session.get_project_id() } request.headers.update(headers) - response = session.post(request.uri, endpoint_filter=self.service, + response = session.post(request.url, json=request.body, headers=request.headers) self._translate_response(response) @@ -92,7 +92,7 @@ def list(cls, session, paginated=True, **params): query_params = cls._query_mapping._transpose(params) while more_data: - resp = session.get(uri, endpoint_filter=cls.service, + resp = session.get(uri, headers=headers, params=query_params) resp = resp.json() resp = resp[cls.resources_key] @@ -123,7 +123,7 @@ def get(self, session, requires_id=True): } request.headers.update(headers) - response = session.get(request.uri, endpoint_filter=self.service, + response = session.get(request.url, headers=request.headers) self._translate_response(response) @@ -137,7 +137,7 @@ def delete(self, session): } request.headers.update(headers) - response = session.delete(request.uri, endpoint_filter=self.service, + response = session.delete(request.url, headers=request.headers) self._translate_response(response, has_body=False) diff --git a/openstack/telemetry/__init__.py b/openstack/meter/__init__.py similarity index 100% rename from openstack/telemetry/__init__.py rename to openstack/meter/__init__.py diff --git a/openstack/telemetry/alarm/__init__.py b/openstack/meter/alarm/__init__.py similarity index 100% rename from openstack/telemetry/alarm/__init__.py rename to openstack/meter/alarm/__init__.py diff --git a/openstack/telemetry/alarm/alarm_service.py b/openstack/meter/alarm/alarm_service.py similarity index 100% rename from openstack/telemetry/alarm/alarm_service.py rename to openstack/meter/alarm/alarm_service.py diff --git a/openstack/telemetry/alarm/v2/__init__.py b/openstack/meter/alarm/v2/__init__.py similarity index 100% rename from openstack/telemetry/alarm/v2/__init__.py rename to openstack/meter/alarm/v2/__init__.py diff --git a/openstack/telemetry/alarm/v2/_proxy.py b/openstack/meter/alarm/v2/_proxy.py similarity index 83% rename from openstack/telemetry/alarm/v2/_proxy.py rename to openstack/meter/alarm/v2/_proxy.py index abadcd268..32b9e5795 100644 --- a/openstack/telemetry/alarm/v2/_proxy.py +++ b/openstack/meter/alarm/v2/_proxy.py @@ -11,8 +11,8 @@ # under the License. from openstack import proxy -from openstack.telemetry.alarm.v2 import alarm as _alarm -from openstack.telemetry.alarm.v2 import alarm_change as _alarm_change +from openstack.meter.alarm.v2 import alarm as _alarm +from openstack.meter.alarm.v2 import alarm_change as _alarm_change class Proxy(proxy.BaseProxy): @@ -22,11 +22,11 @@ def create_alarm(self, **attrs): """Create a new alarm from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.telemetry.v2.alarm.Alarm`, + a :class:`~openstack.meter.v2.alarm.Alarm`, comprised of the properties on the Alarm class. :returns: The results of alarm creation - :rtype: :class:`~openstack.telemetry.v2.alarm.Alarm` + :rtype: :class:`~openstack.meter.v2.alarm.Alarm` """ return self._create(_alarm.Alarm, **attrs) @@ -34,7 +34,7 @@ def delete_alarm(self, alarm, ignore_missing=True): """Delete an alarm :param alarm: The value can be either the ID of an alarm or a - :class:`~openstack.telemetry.v2.alarm.Alarm` instance. + :class:`~openstack.meter.v2.alarm.Alarm` instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the alarm does not exist. @@ -54,7 +54,7 @@ def find_alarm(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :returns: One :class:`~openstack.telemetry.v2.alarm.Alarm` or None + :returns: One :class:`~openstack.meter.v2.alarm.Alarm` or None """ return self._find(_alarm.Alarm, name_or_id, ignore_missing=ignore_missing) @@ -63,9 +63,9 @@ def get_alarm(self, alarm): """Get a single alarm :param alarm: The value can be the ID of an alarm or a - :class:`~openstack.telemetry.v2.alarm.Alarm` instance. + :class:`~openstack.meter.v2.alarm.Alarm` instance. - :returns: One :class:`~openstack.telemetry.v2.alarm.Alarm` + :returns: One :class:`~openstack.meter.v2.alarm.Alarm` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ @@ -78,7 +78,7 @@ def alarms(self, **query): the resources being returned. :returns: A generator of alarm objects - :rtype: :class:`~openstack.telemetry.v2.alarm.Alarm` + :rtype: :class:`~openstack.meter.v2.alarm.Alarm` """ # TODO(Qiming): Check the alarm service API docs/code to verify if # the parameters need a change. @@ -88,12 +88,12 @@ def update_alarm(self, alarm, **attrs): """Update a alarm :param alarm: Either the id of a alarm or a - :class:`~openstack.telemetry.v2.alarm.Alarm` instance. + :class:`~openstack.meter.v2.alarm.Alarm` instance. :attrs kwargs: The attributes to update on the alarm represented by ``value``. :returns: The updated alarm - :rtype: :class:`~openstack.telemetry.v2.alarm.Alarm` + :rtype: :class:`~openstack.meter.v2.alarm.Alarm` """ return self._update(_alarm.Alarm, alarm, **attrs) @@ -106,7 +106,7 @@ def find_alarm_change(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :returns: One :class:`~openstack.telemetry.v2.alarm_change.AlarmChange` + :returns: One :class:`~openstack.meter.v2.alarm_change.AlarmChange` or None """ return self._find(_alarm_change.AlarmChange, name_or_id, @@ -120,7 +120,7 @@ def alarm_changes(self, alarm, **query): the resources being returned. :returns: A generator of alarm change objects - :rtype: :class:`~openstack.telemetry.v2.alarm_change.AlarmChange` + :rtype: :class:`~openstack.meter.v2.alarm_change.AlarmChange` """ # TODO(Qiming): Check the alarm service API docs/code to verify if # the parameters need a change. diff --git a/openstack/telemetry/alarm/v2/alarm.py b/openstack/meter/alarm/v2/alarm.py similarity index 94% rename from openstack/telemetry/alarm/v2/alarm.py rename to openstack/meter/alarm/v2/alarm.py index 095d44aba..181a9b254 100644 --- a/openstack/telemetry/alarm/v2/alarm.py +++ b/openstack/meter/alarm/v2/alarm.py @@ -11,7 +11,7 @@ # under the License. from openstack import resource -from openstack.telemetry.alarm import alarm_service +from openstack.meter.alarm import alarm_service from openstack import utils @@ -76,7 +76,7 @@ def change_state(self, session, next_state): ``insufficient data``. """ url = utils.urljoin(self.base_path, self.id, 'state') - resp = session.put(url, endpoint_filter=self.service, json=next_state) + resp = session.put(url, json=next_state) return resp.json() def check_state(self, session): @@ -85,7 +85,7 @@ def check_state(self, session): The properties of the alarm are not modified. """ url = utils.urljoin(self.base_path, self.id, 'state') - resp = session.get(url, endpoint_filter=self.service) + resp = session.get(url,) resp = resp.json() current_state = resp.replace('\"', '') return current_state diff --git a/openstack/telemetry/alarm/v2/alarm_change.py b/openstack/meter/alarm/v2/alarm_change.py similarity index 93% rename from openstack/telemetry/alarm/v2/alarm_change.py rename to openstack/meter/alarm/v2/alarm_change.py index ecc3e42d0..a4dcdd624 100644 --- a/openstack/telemetry/alarm/v2/alarm_change.py +++ b/openstack/meter/alarm/v2/alarm_change.py @@ -11,7 +11,7 @@ # under the License. from openstack import resource -from openstack.telemetry.alarm import alarm_service +from openstack.meter.alarm import alarm_service class AlarmChange(resource.Resource): @@ -47,6 +47,6 @@ class AlarmChange(resource.Resource): def list(cls, session, limit=None, marker=None, path_args=None, paginated=False, **params): url = cls._get_url(path_args) - resp = session.get(url, endpoint_filter=cls.service, params=params) + resp = session.get(url, params=params) for item in resp.json(): yield cls.existing(**item) diff --git a/openstack/telemetry/telemetry_service.py b/openstack/meter/meter_service.py similarity index 72% rename from openstack/telemetry/telemetry_service.py rename to openstack/meter/meter_service.py index 6b0a9cc92..d84bffb67 100644 --- a/openstack/telemetry/telemetry_service.py +++ b/openstack/meter/meter_service.py @@ -13,12 +13,12 @@ from openstack import service_filter -class TelemetryService(service_filter.ServiceFilter): - """The telemetry service.""" +class MeterService(service_filter.ServiceFilter): + """The meter service.""" valid_versions = [service_filter.ValidVersion('v2')] def __init__(self, version=None): - """Create a telemetry service.""" - super(TelemetryService, self).__init__(service_type='metering', - version=version) + """Create a meter service.""" + super(MeterService, self).__init__(service_type='metering', + version=version) diff --git a/openstack/telemetry/v2/__init__.py b/openstack/meter/v2/__init__.py similarity index 100% rename from openstack/telemetry/v2/__init__.py rename to openstack/meter/v2/__init__.py diff --git a/openstack/telemetry/v2/_proxy.py b/openstack/meter/v2/_proxy.py similarity index 84% rename from openstack/telemetry/v2/_proxy.py rename to openstack/meter/v2/_proxy.py index 054becca3..dcf4b108c 100644 --- a/openstack/telemetry/v2/_proxy.py +++ b/openstack/meter/v2/_proxy.py @@ -11,11 +11,11 @@ # under the License. from openstack import proxy2 -from openstack.telemetry.v2 import capability -from openstack.telemetry.v2 import meter as _meter -from openstack.telemetry.v2 import resource as _resource -from openstack.telemetry.v2 import sample -from openstack.telemetry.v2 import statistics +from openstack.meter.v2 import capability +from openstack.meter.v2 import meter as _meter +from openstack.meter.v2 import resource as _resource +from openstack.meter.v2 import sample +from openstack.meter.v2 import statistics class Proxy(proxy2.BaseProxy): @@ -30,7 +30,7 @@ def find_capability(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :returns: One :class:`~openstack.telemetry.v2.capability.Capability` + :returns: One :class:`~openstack.meter.v2.capability.Capability` or None """ return self._find(capability.Capability, name_or_id, @@ -43,7 +43,7 @@ def capabilities(self, **query): the resources being returned. :returns: A generator of capability objects - :rtype: :class:`~openstack.telemetry.v2.capability.Capability` + :rtype: :class:`~openstack.meter.v2.capability.Capability` """ return self._list(capability.Capability, paginated=False, **query) @@ -56,7 +56,7 @@ def find_meter(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :returns: One :class:`~openstack.telemetry.v2.meter.Meter` or None + :returns: One :class:`~openstack.meter.v2.meter.Meter` or None """ return self._find(_meter.Meter, name_or_id, ignore_missing=ignore_missing) @@ -68,7 +68,7 @@ def meters(self, **query): the resources being returned. :returns: A generator of meter objects - :rtype: :class:`~openstack.telemetry.v2.meter.Meter` + :rtype: :class:`~openstack.meter.v2.meter.Meter` """ return self._list(_meter.Meter, paginated=False, **query) @@ -81,7 +81,7 @@ def find_resource(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :returns: One :class:`~openstack.telemetry.v2.resource.Resource` or + :returns: One :class:`~openstack.meter.v2.resource.Resource` or None """ return self._find(_resource.Resource, name_or_id, @@ -91,10 +91,10 @@ def get_resource(self, resource): """Get a single resource :param resource: The value can be the ID of a resource or a - :class:`~openstack.telemetry.v2.resource.Resource` + :class:`~openstack.meter.v2.resource.Resource` instance. - :returns: One :class:`~openstack.telemetry.v2.resource.Resource` + :returns: One :class:`~openstack.meter.v2.resource.Resource` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ @@ -107,7 +107,7 @@ def resources(self, **query): the resources being returned. :returns: A generator of resource objects - :rtype: :class:`~openstack.telemetry.v2.resource.Resource` + :rtype: :class:`~openstack.meter.v2.resource.Resource` """ return self._list(_resource.Resource, paginated=False, **query) @@ -120,7 +120,7 @@ def find_sample(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :returns: One :class:`~openstack.telemetry.v2.sample.Sample` or None + :returns: One :class:`~openstack.meter.v2.sample.Sample` or None """ return self._find(sample.Sample, name_or_id, ignore_missing=ignore_missing) @@ -133,7 +133,7 @@ def samples(self, meter, **query): the resources being returned. :returns: A generator of sample objects - :rtype: :class:`~openstack.telemetry.v2.sample.Sample` + :rtype: :class:`~openstack.meter.v2.sample.Sample` """ return self._list(sample.Sample, paginated=False, counter_name=meter, **query) @@ -147,7 +147,7 @@ def find_statistics(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :returns: One :class:`~openstack.telemetry.v2.statistics.Statistics` + :returns: One :class:`~openstack.meter.v2.statistics.Statistics` or None """ return self._find(statistics.Statistics, name_or_id, @@ -161,7 +161,7 @@ def statistics(self, meter, **query): the resources being returned. :returns: A generator of statistics objects - :rtype: :class:`~openstack.telemetry.v2.statistics.Statistics` + :rtype: :class:`~openstack.meter.v2.statistics.Statistics` """ return self._list(statistics.Statistics, paginated=False, meter_name=meter, **query) diff --git a/openstack/telemetry/v2/capability.py b/openstack/meter/v2/capability.py similarity index 87% rename from openstack/telemetry/v2/capability.py rename to openstack/meter/v2/capability.py index be93db841..cdef31970 100644 --- a/openstack/telemetry/v2/capability.py +++ b/openstack/meter/v2/capability.py @@ -12,7 +12,7 @@ from openstack import resource2 as resource -from openstack.telemetry import telemetry_service +from openstack.meter import meter_service class Capability(resource.Resource): @@ -20,7 +20,7 @@ class Capability(resource.Resource): resource_key = 'capability' resources_key = 'capabilities' base_path = '/capabilities' - service = telemetry_service.TelemetryService() + service = meter_service.MeterService() # Supported Operations allow_list = True @@ -30,7 +30,7 @@ class Capability(resource.Resource): @classmethod def list(cls, session, paginated=False, **params): - resp = session.get(cls.base_path, endpoint_filter=cls.service, + resp = session.get(cls.base_path, params=params) resp = resp.json() for key, value in resp['api'].items(): diff --git a/openstack/telemetry/v2/meter.py b/openstack/meter/v2/meter.py similarity index 93% rename from openstack/telemetry/v2/meter.py rename to openstack/meter/v2/meter.py index 9e7e9504e..b716252e9 100644 --- a/openstack/telemetry/v2/meter.py +++ b/openstack/meter/v2/meter.py @@ -11,14 +11,14 @@ # under the License. from openstack import resource2 as resource -from openstack.telemetry import telemetry_service +from openstack.meter import meter_service class Meter(resource.Resource): """.. caution:: This API is a work in progress and is subject to change.""" resource_key = 'meter' base_path = '/meters' - service = telemetry_service.TelemetryService() + service = meter_service.MeterService() # Supported Operations allow_list = True diff --git a/openstack/telemetry/v2/resource.py b/openstack/meter/v2/resource.py similarity index 94% rename from openstack/telemetry/v2/resource.py rename to openstack/meter/v2/resource.py index f359a391b..7e9f560ea 100644 --- a/openstack/telemetry/v2/resource.py +++ b/openstack/meter/v2/resource.py @@ -11,13 +11,13 @@ # under the License. from openstack import resource2 as resource -from openstack.telemetry import telemetry_service +from openstack.meter import meter_service class Resource(resource.Resource): """.. caution:: This API is a work in progress and is subject to change.""" base_path = '/resources' - service = telemetry_service.TelemetryService() + service = meter_service.MeterService() # Supported Operations allow_get = True diff --git a/openstack/telemetry/v2/sample.py b/openstack/meter/v2/sample.py similarity index 95% rename from openstack/telemetry/v2/sample.py rename to openstack/meter/v2/sample.py index 0ff47f287..d4838e44d 100644 --- a/openstack/telemetry/v2/sample.py +++ b/openstack/meter/v2/sample.py @@ -11,13 +11,13 @@ # under the License. from openstack import resource2 as resource -from openstack.telemetry import telemetry_service +from openstack.meter import meter_service class Sample(resource.Resource): """.. caution:: This API is a work in progress and is subject to change.""" base_path = '/meters/%(counter_name)s' - service = telemetry_service.TelemetryService() + service = meter_service.MeterService() # Supported Operations allow_get = True diff --git a/openstack/telemetry/v2/statistics.py b/openstack/meter/v2/statistics.py similarity index 93% rename from openstack/telemetry/v2/statistics.py rename to openstack/meter/v2/statistics.py index a295d5147..08a9e03b8 100644 --- a/openstack/telemetry/v2/statistics.py +++ b/openstack/meter/v2/statistics.py @@ -11,14 +11,14 @@ # under the License. from openstack import resource2 as resource -from openstack.telemetry import telemetry_service +from openstack.meter import meter_service class Statistics(resource.Resource): """.. caution:: This API is a work in progress and is subject to change.""" resource_key = 'statistics' base_path = '/meters/%(meter_name)s/statistics' - service = telemetry_service.TelemetryService() + service = meter_service.MeterService() # Supported Operations allow_list = True @@ -57,6 +57,6 @@ class Statistics(resource.Resource): @classmethod def list(cls, session, paginated=False, **params): url = cls.base_path % {'meter_name': params.pop('meter_name')} - resp = session.get(url, endpoint_filter=cls.service, params=params) + resp = session.get(url, params=params) for stat in resp.json(): yield cls.existing(**stat) diff --git a/openstack/module_loader.py b/openstack/module_loader.py deleted file mode 100644 index faf271acd..000000000 --- a/openstack/module_loader.py +++ /dev/null @@ -1,29 +0,0 @@ -# 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. - -""" -Load various modules for authorization and eventually services. -""" -from stevedore import extension - - -def load_service_plugins(namespace): - service_plugins = extension.ExtensionManager( - namespace=namespace, - invoke_on_load=True, - ) - services = {} - for service in service_plugins: - service = service.obj - service.interface = None - services[service.service_type] = service - return services diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 3678c2b21..3f031b112 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -227,7 +227,7 @@ def add_dhcp_agent_to_network(self, agent, network): """ network = self._get_resource(_network.Network, network) agent = self._get_resource(_agent.Agent, agent) - return agent.add_agent_to_network(self._session, network.id) + return agent.add_agent_to_network(self, network.id) def remove_dhcp_agent_from_network(self, agent, network): """Remove a DHCP Agent from a network @@ -239,7 +239,7 @@ def remove_dhcp_agent_from_network(self, agent, network): """ network = self._get_resource(_network.Network, network) agent = self._get_resource(_agent.Agent, agent) - return agent.remove_agent_from_network(self._session, network.id) + return agent.remove_agent_from_network(self, network.id) def network_hosting_dhcp_agents(self, network, **query): """A generator of DHCP agents hosted on a network. @@ -267,7 +267,7 @@ def get_auto_allocated_topology(self, project=None): # If project option is not given, grab project id from session if project is None: - project = self._session.get_project_id() + project = self.get_project_id() return self._get(_auto_allocated_topology.AutoAllocatedTopology, project) @@ -288,7 +288,7 @@ def delete_auto_allocated_topology(self, project=None, # If project option is not given, grab project id from session if project is None: - project = self._session.get_project_id() + project = self.get_project_id() self._delete(_auto_allocated_topology.AutoAllocatedTopology, project, ignore_missing=ignore_missing) @@ -305,7 +305,7 @@ def validate_auto_allocated_topology(self, project=None): # If project option is not given, grab project id from session if project is None: - project = self._session.get_project_id() + project = self.get_project_id() return self._get(_auto_allocated_topology.ValidateTopology, project=project, requires_id=False) @@ -453,7 +453,7 @@ def associate_flavor_with_service_profile(self, flavor, service_profile): service_profile = self._get_resource( _service_profile.ServiceProfile, service_profile) return flavor.associate_flavor_with_service_profile( - self._session, service_profile.id) + self, service_profile.id) def disassociate_flavor_from_service_profile( self, flavor, service_profile): @@ -472,7 +472,7 @@ def disassociate_flavor_from_service_profile( service_profile = self._get_resource( _service_profile.ServiceProfile, service_profile) return flavor.disassociate_flavor_from_service_profile( - self._session, service_profile.id) + self, service_profile.id) def create_ip(self, **attrs): """Create a new floating ip from attributes @@ -509,7 +509,7 @@ def find_available_ip(self): :returns: One :class:`~openstack.network.v2.floating_ip.FloatingIP` or None """ - return _floating_ip.FloatingIP.find_available(self._session) + return _floating_ip.FloatingIP.find_available(self) def find_ip(self, name_or_id, ignore_missing=True): """Find a single IP @@ -1506,11 +1506,11 @@ def update_port(self, port, **attrs): def add_ip_to_port(self, port, ip): ip['port_id'] = port.id - return ip.update(self._session) + return ip.update(self) def remove_ip_from_port(self, ip): ip['port_id'] = None - return ip.update(self._session) + return ip.update(self) def get_subnet_ports(self, subnet_id): result = [] @@ -2288,7 +2288,7 @@ def add_interface_to_router(self, router, subnet_id=None, port_id=None): else: body = {'subnet_id': subnet_id} router = self._get_resource(_router.Router, router) - return router.add_interface(self._session, **body) + return router.add_interface(self, **body) def remove_interface_from_router(self, router, subnet_id=None, port_id=None): @@ -2308,7 +2308,7 @@ def remove_interface_from_router(self, router, subnet_id=None, else: body = {'subnet_id': subnet_id} router = self._get_resource(_router.Router, router) - return router.remove_interface(self._session, **body) + return router.remove_interface(self, **body) def add_gateway_to_router(self, router, **body): """Add Gateway to a router @@ -2320,7 +2320,7 @@ def add_gateway_to_router(self, router, **body): :rtype: :class: `~openstack.network.v2.router.Router` """ router = self._get_resource(_router.Router, router) - return router.add_gateway(self._session, **body) + return router.add_gateway(self, **body) def remove_gateway_from_router(self, router, **body): """Remove Gateway from a router @@ -2332,7 +2332,7 @@ def remove_gateway_from_router(self, router, **body): :rtype: :class: `~openstack.network.v2.router.Router` """ router = self._get_resource(_router.Router, router) - return router.remove_gateway(self._session, **body) + return router.remove_gateway(self, **body) def routers_hosting_l3_agents(self, router, **query): """Return a generator of L3 agent hosting a router @@ -2375,7 +2375,7 @@ def add_router_to_agent(self, agent, router): """ agent = self._get_resource(_agent.Agent, agent) router = self._get_resource(_router.Router, router) - return agent.add_router_to_agent(self._session, router.id) + return agent.add_router_to_agent(self, router.id) def remove_router_from_agent(self, agent, router): """Remove router from L3 agent @@ -2388,7 +2388,7 @@ def remove_router_from_agent(self, agent, router): """ agent = self._get_resource(_agent.Agent, agent) router = self._get_resource(_router.Router, router) - return agent.remove_router_from_agent(self._session, router.id) + return agent.remove_router_from_agent(self, router.id) def create_security_group(self, **attrs): """Create a new security group from attributes @@ -2988,7 +2988,7 @@ def set_tags(self, resource, tags): :rtype: :class:`~openstack.resource2.Resource` """ self._check_tag_support(resource) - return resource.set_tags(self._session, tags) + return resource.set_tags(self, tags) def create_vpn_service(self, **attrs): """Create a new vpn service from attributes diff --git a/openstack/network/v2/agent.py b/openstack/network/v2/agent.py index bb58ed415..0259d2b4e 100644 --- a/openstack/network/v2/agent.py +++ b/openstack/network/v2/agent.py @@ -70,25 +70,25 @@ class Agent(resource.Resource): def add_agent_to_network(self, session, network_id): body = {'network_id': network_id} url = utils.urljoin(self.base_path, self.id, 'dhcp-networks') - resp = session.post(url, endpoint_filter=self.service, json=body) + resp = session.post(url, json=body) return resp.json() def remove_agent_from_network(self, session, network_id): body = {'network_id': network_id} url = utils.urljoin(self.base_path, self.id, 'dhcp-networks', network_id) - session.delete(url, endpoint_filter=self.service, json=body) + session.delete(url, json=body) def add_router_to_agent(self, session, router): body = {'router_id': router} url = utils.urljoin(self.base_path, self.id, 'l3-routers') - resp = session.post(url, endpoint_filter=self.service, json=body) + resp = session.post(url, json=body) return resp.json() def remove_router_from_agent(self, session, router): body = {'router_id': router} url = utils.urljoin(self.base_path, self.id, 'l3-routers', router) - session.delete(url, endpoint_filter=self.service, json=body) + session.delete(url, json=body) class NetworkHostingDHCPAgent(Agent): diff --git a/openstack/network/v2/flavor.py b/openstack/network/v2/flavor.py index 46e9579a2..273d132f5 100644 --- a/openstack/network/v2/flavor.py +++ b/openstack/network/v2/flavor.py @@ -48,7 +48,7 @@ def associate_flavor_with_service_profile( flavor_id = self.id url = utils.urljoin(self.base_path, flavor_id, 'service_profiles') body = {"service_profile": {"id": service_profile_id}} - resp = session.post(url, endpoint_filter=self.service, json=body) + resp = session.post(url, json=body) return resp.json() def disassociate_flavor_from_service_profile( @@ -56,5 +56,5 @@ def disassociate_flavor_from_service_profile( flavor_id = self.id url = utils.urljoin( self.base_path, flavor_id, 'service_profiles', service_profile_id) - session.delete(url, endpoint_filter=self.service) + session.delete(url,) return None diff --git a/openstack/network/v2/router.py b/openstack/network/v2/router.py index 7fa58dbaf..2573dc62a 100644 --- a/openstack/network/v2/router.py +++ b/openstack/network/v2/router.py @@ -84,54 +84,54 @@ def add_interface(self, session, **body): """Add an internal interface to a logical router. :param session: The session to communicate through. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :param dict body: The body requested to be updated on the router :returns: The body of the response as a dictionary. """ url = utils.urljoin(self.base_path, self.id, 'add_router_interface') - resp = session.put(url, endpoint_filter=self.service, json=body) + resp = session.put(url, json=body) return resp.json() def remove_interface(self, session, **body): """Remove an internal interface from a logical router. :param session: The session to communicate through. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :param dict body: The body requested to be updated on the router :returns: The body of the response as a dictionary. """ url = utils.urljoin(self.base_path, self.id, 'remove_router_interface') - resp = session.put(url, endpoint_filter=self.service, json=body) + resp = session.put(url, json=body) return resp.json() def add_gateway(self, session, **body): """Add an external gateway to a logical router. :param session: The session to communicate through. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :param dict body: The body requested to be updated on the router :returns: The body of the response as a dictionary. """ url = utils.urljoin(self.base_path, self.id, 'add_gateway_router') - resp = session.put(url, endpoint_filter=self.service, json=body) + resp = session.put(url, json=body) return resp.json() def remove_gateway(self, session, **body): """Remove an external gateway from a logical router. :param session: The session to communicate through. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :param dict body: The body requested to be updated on the router :returns: The body of the response as a dictionary. """ url = utils.urljoin(self.base_path, self.id, 'remove_gateway_router') - resp = session.put(url, endpoint_filter=self.service, json=body) + resp = session.put(url, json=body) return resp.json() diff --git a/openstack/network/v2/tag.py b/openstack/network/v2/tag.py index b216e2ebc..83ed190f3 100644 --- a/openstack/network/v2/tag.py +++ b/openstack/network/v2/tag.py @@ -24,7 +24,7 @@ class TagMixin(object): def set_tags(self, session, tags): url = utils.urljoin(self.base_path, self.id, 'tags') - session.put(url, endpoint_filter=self.service, + session.put(url, json={'tags': tags}) self._body.attributes.update({'tags': tags}) return self diff --git a/openstack/object_store/v1/_base.py b/openstack/object_store/v1/_base.py index 17df93555..cd99503fc 100644 --- a/openstack/object_store/v1/_base.py +++ b/openstack/object_store/v1/_base.py @@ -36,13 +36,13 @@ def _calculate_headers(self, metadata): def set_metadata(self, session, metadata): url = self._get_url(self, self.id) - session.post(url, endpoint_filter=self.service, + session.post(url, headers=self._calculate_headers(metadata)) def delete_metadata(self, session, keys): url = self._get_url(self, self.id) headers = {key: '' for key in keys} - session.post(url, endpoint_filter=self.service, + session.post(url, headers=self._calculate_headers(headers)) def _set_metadata(self): @@ -69,7 +69,7 @@ def update_by_id(cls, session, resource_id, attrs, path_args=None): """Update a Resource with the given attributes. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :param resource_id: This resource's identifier, if needed by the request. The default is ``None``. :param dict attrs: The attributes to be sent in the body @@ -82,5 +82,5 @@ class but is ignored for this method. url = cls._get_url(None, resource_id) headers = attrs.get(resource.HEADERS, dict()) headers['Accept'] = '' - return session.post(url, endpoint_filter=cls.service, + return session.post(url, headers=headers).headers diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index 78a48902a..6258eab53 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -35,7 +35,7 @@ def set_account_metadata(self, **metadata): by the user. """ account = self._get_resource(_account.Account, None) - account.set_metadata(self._session, metadata) + account.set_metadata(self, metadata) def delete_account_metadata(self, keys): """Delete metadata for this account. @@ -43,7 +43,7 @@ def delete_account_metadata(self, keys): :param keys: The keys of metadata to be deleted. """ account = self._get_resource(_account.Account, None) - account.delete_metadata(self._session, keys) + account.delete_metadata(self, keys) def containers(self, **query): """Obtain Container objects for this account. @@ -54,7 +54,7 @@ def containers(self, **query): :rtype: A generator of :class:`~openstack.object_store.v1.container.Container` objects. """ - return _container.Container.list(self._session, **query) + return _container.Container.list(self, **query) def create_container(self, **attrs): """Create a new container from attributes @@ -121,7 +121,7 @@ def set_container_metadata(self, container, **metadata): - `sync_key` """ res = self._get_resource(_container.Container, container) - res.set_metadata(self._session, metadata) + res.set_metadata(self, metadata) def delete_container_metadata(self, container, keys): """Delete metadata for a container. @@ -132,7 +132,7 @@ def delete_container_metadata(self, container, keys): :param keys: The keys of metadata to be deleted. """ res = self._get_resource(_container.Container, container) - res.delete_metadata(self._session, keys) + res.delete_metadata(self, keys) def objects(self, container, **query): """Return a generator that yields the Container's objects. @@ -149,7 +149,7 @@ def objects(self, container, **query): """ container = _container.Container.from_id(container) - objs = _obj.Object.list(self._session, + objs = _obj.Object.list(self, path_args={"container": container.name}, **query) for obj in objs: @@ -300,7 +300,7 @@ def set_object_metadata(self, obj, container=None, **metadata): container_name = self._get_container_name(obj, container) res = self._get_resource(_obj.Object, obj, path_args={"container": container_name}) - res.set_metadata(self._session, metadata) + res.set_metadata(self, metadata) def delete_object_metadata(self, obj, container=None, keys=None): """Delete metadata for an object. @@ -315,4 +315,4 @@ def delete_object_metadata(self, obj, container=None, keys=None): container_name = self._get_container_name(obj, container) res = self._get_resource(_obj.Object, obj, path_args={"container": container_name}) - res.delete_metadata(self._session, keys) + res.delete_metadata(self, keys) diff --git a/openstack/object_store/v1/container.py b/openstack/object_store/v1/container.py index 678846d6e..dd440b929 100644 --- a/openstack/object_store/v1/container.py +++ b/openstack/object_store/v1/container.py @@ -97,7 +97,7 @@ def create_by_id(cls, session, attrs, resource_id=None): """Create a Resource from its attributes. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :param dict attrs: The attributes to be sent in the body of the request. :param resource_id: This resource's identifier, if needed by @@ -108,14 +108,14 @@ def create_by_id(cls, session, attrs, resource_id=None): url = cls._get_url(None, resource_id) headers = attrs.get(resource.HEADERS, dict()) headers['Accept'] = '' - return session.put(url, endpoint_filter=cls.service, + return session.put(url, headers=headers).headers def create(self, session): """Create a Resource from this instance. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :return: This instance. """ diff --git a/openstack/object_store/v1/obj.py b/openstack/object_store/v1/obj.py index 1be77ec16..f18ff6275 100644 --- a/openstack/object_store/v1/obj.py +++ b/openstack/object_store/v1/obj.py @@ -206,13 +206,14 @@ def delete_metadata(self, session, keys): del(metadata[key]) url = self._get_url(self, self.id) - session.post(url, endpoint_filter=self.service, + session.post(url, headers=self._calculate_headers(metadata)) - def get(self, session, include_headers=False, args=None): + def get(self, session, include_headers=False, args=None, + error_message=None): url = self._get_url(self, self.id) headers = {'Accept': 'bytes'} - resp = session.get(url, endpoint_filter=self.service, headers=headers) + resp = session.get(url, headers=headers, error_message=error_message) resp = resp.content self._set_metadata() return resp @@ -223,11 +224,11 @@ def create(self, session): headers = self.get_headers() headers['Accept'] = '' if self.data is not None: - resp = session.put(url, endpoint_filter=self.service, + resp = session.put(url, data=self.data, headers=headers).headers else: - resp = session.post(url, endpoint_filter=self.service, data=None, + resp = session.post(url, data=None, headers=headers).headers self.set_headers(resp) return self diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index fad49ccf5..0ce2f1ec5 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -125,7 +125,7 @@ def check_stack(self, stack): else: stk_obj = _stack.Stack.existing(id=stack) - stk_obj.check(self._session) + stk_obj.check(self) def get_stack_template(self, stack): """Get template used by a stack @@ -184,7 +184,7 @@ def get_stack_files(self, stack): stk = self._find(_stack.Stack, stack, ignore_missing=False) obj = _stack_files.StackFiles(stack_name=stk.name, stack_id=stk.id) - return obj.get(self._session) + return obj.get(self) def resources(self, stack, **query): """Return a generator of resources @@ -357,6 +357,6 @@ def validate_template(self, template, environment=None, template_url=None, "'template_url' must be specified when template is None") tmpl = _template.Template.new() - return tmpl.validate(self._session, template, environment=environment, + return tmpl.validate(self, template, environment=environment, template_url=template_url, ignore_errors=ignore_errors) diff --git a/openstack/orchestration/v1/stack.py b/openstack/orchestration/v1/stack.py index 5445d11fc..9172be71e 100644 --- a/openstack/orchestration/v1/stack.py +++ b/openstack/orchestration/v1/stack.py @@ -90,7 +90,7 @@ def update(self, session): def _action(self, session, body): """Perform stack actions""" url = utils.urljoin(self.base_path, self._get_id(self), 'actions') - resp = session.post(url, endpoint_filter=self.service, json=body) + resp = session.post(url, json=body) return resp.json() def check(self, session): diff --git a/openstack/orchestration/v1/stack_files.py b/openstack/orchestration/v1/stack_files.py index db23a4771..cb16dcd6e 100644 --- a/openstack/orchestration/v1/stack_files.py +++ b/openstack/orchestration/v1/stack_files.py @@ -36,5 +36,5 @@ def get(self, session): # The stack files response contains a map of filenames and file # contents. request = self._prepare_request(requires_id=False) - resp = session.get(request.uri, endpoint_filter=self.service) + resp = session.get(request.url) return resp.json() diff --git a/openstack/orchestration/v1/template.py b/openstack/orchestration/v1/template.py index 5c8227261..7e2c5eab2 100644 --- a/openstack/orchestration/v1/template.py +++ b/openstack/orchestration/v1/template.py @@ -47,6 +47,6 @@ def validate(self, session, template, environment=None, template_url=None, qry = parse.urlencode({'ignore_errors': ignore_errors}) url = '?'.join([url, qry]) - resp = session.post(url, endpoint_filter=self.service, json=body) + resp = session.post(url, json=body) self._translate_response(resp) return self diff --git a/openstack/profile.py b/openstack/profile.py index 69994fafc..d70c05afa 100644 --- a/openstack/profile.py +++ b/openstack/profile.py @@ -54,9 +54,9 @@ import copy import logging -from openstack.bare_metal import bare_metal_service -from openstack.block_store import block_store_service -from openstack.cluster import cluster_service +from openstack.baremetal import baremetal_service +from openstack.block_storage import block_storage_service +from openstack.clustering import clustering_service from openstack.compute import compute_service from openstack.database import database_service from openstack import exceptions @@ -65,12 +65,11 @@ from openstack.key_manager import key_manager_service from openstack.load_balancer import load_balancer_service as lb_service from openstack.message import message_service -from openstack import module_loader +from openstack.meter.alarm import alarm_service +from openstack.meter import meter_service from openstack.network import network_service from openstack.object_store import object_store_service from openstack.orchestration import orchestration_service -from openstack.telemetry.alarm import alarm_service -from openstack.telemetry import telemetry_service from openstack.workflow import workflow_service _logger = logging.getLogger(__name__) @@ -94,9 +93,10 @@ def __init__(self, plugins=None): self._services = {} self._add_service(alarm_service.AlarmService(version="v2")) - self._add_service(bare_metal_service.BareMetalService(version="v1")) - self._add_service(block_store_service.BlockStoreService(version="v2")) - self._add_service(cluster_service.ClusterService(version="v1")) + self._add_service(baremetal_service.BaremetalService(version="v1")) + self._add_service( + block_storage_service.BlockStorageService(version="v2")) + self._add_service(clustering_service.ClusteringService(version="v1")) self._add_service(compute_service.ComputeService(version="v2")) self._add_service(database_service.DatabaseService(version="v1")) self._add_service(identity_service.IdentityService(version="v3")) @@ -104,17 +104,14 @@ def __init__(self, plugins=None): self._add_service(key_manager_service.KeyManagerService(version="v1")) self._add_service(lb_service.LoadBalancerService(version="v2")) self._add_service(message_service.MessageService(version="v1")) + self._add_service(meter_service.MeterService(version="v2")) self._add_service(network_service.NetworkService(version="v2")) self._add_service( object_store_service.ObjectStoreService(version="v1")) self._add_service( orchestration_service.OrchestrationService(version="v1")) - self._add_service(telemetry_service.TelemetryService(version="v2")) self._add_service(workflow_service.WorkflowService(version="v2")) - if plugins: - for plugin in plugins: - self._load_plugin(plugin) self.service_keys = sorted(self._services.keys()) def __repr__(self): @@ -124,18 +121,6 @@ def _add_service(self, serv): serv.interface = None self._services[serv.service_type] = serv - def _load_plugin(self, namespace): - """Load a service plugin. - - :param str namespace: Entry point namespace - """ - services = module_loader.load_service_plugins(namespace) - for service_type in services: - if service_type in self._services: - _logger.debug("Overriding %s with %s", service_type, - services[service_type]) - self._add_service(services[service_type]) - def get_filter(self, service): """Get a service preference. diff --git a/openstack/proxy.py b/openstack/proxy.py index cbe8616ee..24123f377 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -9,7 +9,7 @@ # 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.cloud import _adapter from openstack import exceptions from openstack import resource @@ -38,10 +38,7 @@ def check(self, expected, actual=None, *args, **kwargs): return wrap -class BaseProxy(object): - - def __init__(self, session): - self._session = session +class BaseProxy(_adapter.OpenStackSDKAdapter): def _get_resource(self, resource_type, value, path_args=None): """Get a resource object to work on @@ -87,7 +84,7 @@ def _find(self, resource_type, name_or_id, path_args=None, :returns: An instance of ``resource_type`` or None """ - return resource_type.find(self._session, name_or_id, + return resource_type.find(self, name_or_id, path_args=path_args, ignore_missing=ignore_missing) @@ -122,18 +119,15 @@ def _delete(self, resource_type, value, path_args=None, res = self._get_resource(resource_type, value, path_args) try: - rv = res.delete(self._session) - except exceptions.NotFoundException as e: + rv = res.delete( + self, + error_message="No {resource_type} found for {value}".format( + resource_type=resource_type.__name__, value=value)) + except exceptions.NotFoundException: if ignore_missing: return None else: - # Reraise with a more specific type and message - raise exceptions.ResourceNotFound( - message="No %s found for %s" % - (resource_type.__name__, value), - details=e.details, response=e.response, - request_id=e.request_id, url=e.url, method=e.method, - http_status=e.http_status, cause=e.cause) + raise return rv @@ -157,7 +151,7 @@ def _update(self, resource_type, value, path_args=None, **attrs): """ res = self._get_resource(resource_type, value, path_args) res.update_attrs(attrs) - return res.update(self._session) + return res.update(self) def _create(self, resource_type, path_args=None, **attrs): """Create a resource from attributes @@ -176,7 +170,7 @@ def _create(self, resource_type, path_args=None, **attrs): res = resource_type.new(**attrs) if path_args is not None: res.update_attrs(path_args) - return res.create(self._session) + return res.create(self) @_check_resource(strict=False) def _get(self, resource_type, value=None, path_args=None, args=None): @@ -197,15 +191,10 @@ def _get(self, resource_type, value=None, path_args=None, args=None): """ res = self._get_resource(resource_type, value, path_args) - try: - return res.get(self._session, args=args) - except exceptions.NotFoundException as e: - raise exceptions.ResourceNotFound( - message="No %s found for %s" % - (resource_type.__name__, value), - details=e.details, response=e.response, - request_id=e.request_id, url=e.url, method=e.method, - http_status=e.http_status, cause=e.cause) + return res.get( + self, args=args, + error_message='No {resource} found for {value}'.format( + resource=resource_type.__name__, value=value)) def _list(self, resource_type, value=None, paginated=False, path_args=None, **query): @@ -235,7 +224,7 @@ def _list(self, resource_type, value=None, paginated=False, res = self._get_resource(resource_type, value, path_args) query = res.convert_ids(query) - return res.list(self._session, path_args=path_args, + return res.list(self, path_args=path_args, paginated=paginated, params=query) def _head(self, resource_type, value=None, path_args=None): @@ -255,7 +244,7 @@ def _head(self, resource_type, value=None, path_args=None): """ res = self._get_resource(resource_type, value, path_args) - return res.head(self._session) + return res.head(self) def wait_for_status(self, value, status, failures=[], interval=2, wait=120): @@ -278,7 +267,7 @@ def wait_for_status(self, value, status, failures=[], interval=2, :raises: :class:`~AttributeError` if the resource does not have a status attribute """ - return resource.wait_for_status(self._session, value, status, + return resource.wait_for_status(self, value, status, failures, interval, wait) def wait_for_delete(self, value, interval=2, wait=120): @@ -293,4 +282,4 @@ def wait_for_delete(self, value, interval=2, wait=120): :raises: :class:`~openstack.exceptions.ResourceTimeout` transition to delete failed to occur in wait seconds. """ - return resource.wait_for_delete(self._session, value, interval, wait) + return resource.wait_for_delete(self, value, interval, wait) diff --git a/openstack/proxy2.py b/openstack/proxy2.py index 0a7fcb373..739c8510d 100644 --- a/openstack/proxy2.py +++ b/openstack/proxy2.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.cloud import _adapter from openstack import exceptions from openstack import resource2 from openstack import utils @@ -39,10 +40,7 @@ def check(self, expected, actual=None, *args, **kwargs): return wrap -class BaseProxy(object): - - def __init__(self, session): - self._session = session +class BaseProxy(_adapter.OpenStackSDKAdapter): def _get_resource(self, resource_type, value, **attrs): """Get a resource object to work on @@ -101,7 +99,7 @@ def _find(self, resource_type, name_or_id, ignore_missing=True, :returns: An instance of ``resource_type`` or None """ - return resource_type.find(self._session, name_or_id, + return resource_type.find(self, name_or_id, ignore_missing=ignore_missing, **attrs) @@ -136,18 +134,14 @@ def _delete(self, resource_type, value, ignore_missing=True, **attrs): res = self._get_resource(resource_type, value, **attrs) try: - rv = res.delete(self._session) - except exceptions.NotFoundException as e: + rv = res.delete( + self, + error_message="No {resource_type} found for {value}".format( + resource_type=resource_type.__name__, value=value)) + except exceptions.NotFoundException: if ignore_missing: return None - else: - # Reraise with a more specific type and message - raise exceptions.ResourceNotFound( - message="No %s found for %s" % - (resource_type.__name__, value), - details=e.details, response=e.response, - request_id=e.request_id, url=e.url, method=e.method, - http_status=e.http_status, cause=e.cause) + raise return rv @@ -171,7 +165,7 @@ def _update(self, resource_type, value, **attrs): :rtype: :class:`~openstack.resource2.Resource` """ res = self._get_resource(resource_type, value, **attrs) - return res.update(self._session) + return res.update(self) def _create(self, resource_type, **attrs): """Create a resource from attributes @@ -191,7 +185,7 @@ def _create(self, resource_type, **attrs): :rtype: :class:`~openstack.resource2.Resource` """ res = resource_type.new(**attrs) - return res.create(self._session) + return res.create(self) @_check_resource(strict=False) def _get(self, resource_type, value=None, requires_id=True, **attrs): @@ -214,15 +208,10 @@ def _get(self, resource_type, value=None, requires_id=True, **attrs): """ res = self._get_resource(resource_type, value, **attrs) - try: - return res.get(self._session, requires_id=requires_id) - except exceptions.NotFoundException as e: - raise exceptions.ResourceNotFound( - message="No %s found for %s" % - (resource_type.__name__, value), - details=e.details, response=e.response, - request_id=e.request_id, url=e.url, method=e.method, - http_status=e.http_status, cause=e.cause) + return res.get( + self, requires_id=requires_id, + error_message="No {resource_type} found for {value}".format( + resource_type=resource_type.__name__, value=value)) def _list(self, resource_type, value=None, paginated=False, **attrs): """List a resource @@ -248,7 +237,7 @@ def _list(self, resource_type, value=None, paginated=False, **attrs): the ``resource_type``. """ res = self._get_resource(resource_type, value, **attrs) - return res.list(self._session, paginated=paginated, **attrs) + return res.list(self, paginated=paginated, **attrs) def _head(self, resource_type, value=None, **attrs): """Retrieve a resource's header @@ -268,7 +257,7 @@ def _head(self, resource_type, value=None, **attrs): :rtype: :class:`~openstack.resource2.Resource` """ res = self._get_resource(resource_type, value, **attrs) - return res.head(self._session) + return res.head(self) @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", details=("This is no longer a part of the proxy base, " @@ -296,7 +285,7 @@ def wait_for_status(self, value, status, failures=[], interval=2, :raises: :class:`~AttributeError` if the resource does not have a status attribute """ - return resource2.wait_for_status(self._session, value, status, + return resource2.wait_for_status(self, value, status, failures, interval, wait) @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", @@ -316,4 +305,4 @@ def wait_for_delete(self, value, interval=2, wait=120): :raises: :class:`~openstack.exceptions.ResourceTimeout` transition to delete failed to occur in wait seconds. """ - return resource2.wait_for_delete(self._session, value, interval, wait) + return resource2.wait_for_delete(self, value, interval, wait) diff --git a/openstack/resource.py b/openstack/resource.py index 2bc559e71..c9c0671f7 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -427,7 +427,8 @@ def is_dirty(self): def _reset_dirty(self): self._dirty = set() - def _update_attrs_from_response(self, resp, include_headers=False): + def _update_attrs_from_response(self, resp, include_headers=False, + error_message=None): resp_headers = resp.pop(HEADERS, None) self._attrs.update(resp) self.update_attrs(self._attrs) @@ -529,7 +530,7 @@ def create_by_id(cls, session, attrs, resource_id=None, path_args=None): """Create a remote resource from its attributes. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :param dict attrs: The attributes to be sent in the body of the request. :param resource_id: This resource's identifier, if needed by @@ -556,9 +557,9 @@ def create_by_id(cls, session, attrs, resource_id=None, path_args=None): if headers: args[HEADERS] = headers if resource_id: - resp = session.put(url, endpoint_filter=cls.service, **args) + resp = session.put(url, **args) else: - resp = session.post(url, endpoint_filter=cls.service, **args) + resp = session.post(url, **args) resp_headers = resp.headers resp = resp.json() @@ -573,7 +574,7 @@ def create(self, session): """Create a remote resource from this instance. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if @@ -590,7 +591,7 @@ def get_data_by_id(cls, session, resource_id, path_args=None, args=None, """Get the attributes of a remote resource from an id. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :param resource_id: This resource's identifier, if needed by the request. :param dict path_args: A dictionary of arguments to construct @@ -612,7 +613,8 @@ def get_data_by_id(cls, session, resource_id, path_args=None, args=None, url = cls._get_url(path_args, resource_id) if args: url = '?'.join([url, url_parse.urlencode(args)]) - response = session.get(url, endpoint_filter=cls.service) + response = session.get(url,) + exceptions.raise_from_response(response) body = response.json() if cls.resource_key: @@ -629,7 +631,7 @@ def get_by_id(cls, session, resource_id, path_args=None, """Get an object representing a remote resource from an id. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :param resource_id: This resource's identifier, if needed by the request. :param dict path_args: A dictionary of arguments to construct @@ -648,11 +650,12 @@ def get_by_id(cls, session, resource_id, path_args=None, include_headers=include_headers) return cls.existing(**body) - def get(self, session, include_headers=False, args=None): + def get(self, session, include_headers=False, + args=None, error_message=None): """Get the remote resource associated with this instance. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :param bool include_headers: ``True`` if header data should be included in the response body, ``False`` if not. @@ -673,7 +676,7 @@ def head_data_by_id(cls, session, resource_id, path_args=None): """Get a dictionary representing the headers of a remote resource. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :param resource_id: This resource's identifier, if needed by the request. :param dict path_args: A dictionary of arguments to construct @@ -690,7 +693,7 @@ def head_data_by_id(cls, session, resource_id, path_args=None): url = cls._get_url(path_args, resource_id) headers = {'Accept': ''} - resp = session.head(url, endpoint_filter=cls.service, headers=headers) + resp = session.head(url, headers=headers) return {HEADERS: resp.headers} @@ -699,7 +702,7 @@ def head_by_id(cls, session, resource_id, path_args=None): """Get an object representing the headers of a remote resource. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :param resource_id: This resource's identifier, if needed by the request. :param dict path_args: A dictionary of arguments to construct @@ -717,7 +720,7 @@ def head(self, session): """Get the remote resource headers associated with this instance. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if @@ -733,7 +736,7 @@ def update_by_id(cls, session, resource_id, attrs, path_args=None): """Update a remote resource with the given attributes. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :param resource_id: This resource's identifier, if needed by the request. :param dict attrs: The attributes to be sent in the body @@ -762,9 +765,9 @@ def update_by_id(cls, session, resource_id, attrs, path_args=None): if headers: args[HEADERS] = headers if cls.patch_update: - resp = session.patch(url, endpoint_filter=cls.service, **args) + resp = session.patch(url, **args) else: - resp = session.put(url, endpoint_filter=cls.service, **args) + resp = session.put(url, **args) resp_headers = resp.headers resp = resp.json() @@ -779,7 +782,7 @@ def update(self, session): """Update the remote resource associated with this instance. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if @@ -802,11 +805,12 @@ def update(self, session): return self @classmethod - def delete_by_id(cls, session, resource_id, path_args=None): + def delete_by_id(cls, session, resource_id, path_args=None, + error_message=None): """Delete a remote resource with the given id. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :param resource_id: This resource's identifier, if needed by the request. :param dict path_args: A dictionary of arguments to construct @@ -822,19 +826,21 @@ def delete_by_id(cls, session, resource_id, path_args=None): url = cls._get_url(path_args, resource_id) headers = {'Accept': ''} - session.delete(url, endpoint_filter=cls.service, headers=headers) + response = session.delete(url, headers=headers) + exceptions.raise_from_response(response, error_message=error_message) - def delete(self, session): + def delete(self, session, error_message=None): """Delete the remote resource associated with this instance. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :return: ``None`` :raises: :exc:`~openstack.exceptions.MethodNotSupported` if :data:`Resource.allow_update` is not set to ``True``. """ - self.delete_by_id(session, self.id, path_args=self) + self.delete_by_id(session, self.id, path_args=self, + error_message=error_message) @classmethod def list(cls, session, path_args=None, paginated=False, params=None): @@ -844,7 +850,7 @@ def list(cls, session, path_args=None, paginated=False, params=None): params for response filtering. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :param dict path_args: A dictionary of arguments to construct a compound URL. See `How path_args are used`_ for details. @@ -855,7 +861,7 @@ def list(cls, session, path_args=None, paginated=False, params=None): page of data will be returned regardless of the API's support of pagination.** :param dict params: Query parameters to be passed into the underlying - :meth:`~openstack.session.Session.get` method. + :meth:`~keystoneauth1.adapter.Adapter.get` method. Values that the server may support include `limit` and `marker`. @@ -871,7 +877,7 @@ def list(cls, session, path_args=None, paginated=False, params=None): url = cls._get_url(path_args) headers = {'Accept': 'application/json'} while more_data: - resp = session.get(url, endpoint_filter=cls.service, + resp = session.get(url, headers=headers, params=params) resp = resp.json() if cls.resources_key: @@ -903,7 +909,7 @@ def find(cls, session, name_or_id, path_args=None, ignore_missing=True): """Find a resource by its name or id. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :param name_or_id: This resource's identifier, if needed by the request. The default is ``None``. :param dict path_args: A dictionary of arguments to construct @@ -969,7 +975,7 @@ def wait_for_status(session, resource, status, failures, interval, wait): """Wait for the resource to be in a particular status. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :param resource: The resource to wait on to reach the status. The resource must have a status attribute. :type resource: :class:`~openstack.resource.Resource` @@ -1012,7 +1018,7 @@ def wait_for_delete(session, resource, interval, wait): """Wait for the resource to be deleted. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :param resource: The resource to wait on to be deleted. :type resource: :class:`~openstack.resource.Resource` :param interval: Number of seconds to wait between checks. diff --git a/openstack/resource2.py b/openstack/resource2.py index 7d8b0403c..0f411f25b 100644 --- a/openstack/resource2.py +++ b/openstack/resource2.py @@ -172,8 +172,8 @@ def clean(self): class _Request(object): """Prepared components that go into a KSA request""" - def __init__(self, uri, body, headers): - self.uri = uri + def __init__(self, url, body, headers): + self.url = url self.body = body self.headers = headers @@ -537,7 +537,7 @@ def _filter_component(self, component, mapping): """ return {k: v for k, v in component.items() if k in mapping.values()} - def _translate_response(self, response, has_body=True): + def _translate_response(self, response, has_body=True, error_message=None): """Given a KSA response, inflate this instance with its data DELETE operations don't return a body, so only try to work @@ -546,6 +546,7 @@ def _translate_response(self, response, has_body=True): This method updates attributes that correspond to headers and body on this instance and clears the dirty set. """ + exceptions.raise_from_response(response, error_message=error_message) if has_body: body = response.json() if self.resource_key and self.resource_key in body: @@ -564,7 +565,7 @@ def create(self, session, prepend_key=True): """Create a remote resource based on this instance. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :param prepend_key: A boolean indicating whether the resource_key should be prepended in a resource creation request. Default to True. @@ -579,22 +580,22 @@ def create(self, session, prepend_key=True): if self.put_create: request = self._prepare_request(requires_id=True, prepend_key=prepend_key) - response = session.put(request.uri, endpoint_filter=self.service, + response = session.put(request.url, json=request.body, headers=request.headers) else: request = self._prepare_request(requires_id=False, prepend_key=prepend_key) - response = session.post(request.uri, endpoint_filter=self.service, + response = session.post(request.url, json=request.body, headers=request.headers) self._translate_response(response) return self - def get(self, session, requires_id=True): + def get(self, session, requires_id=True, error_message=None): """Get a remote resource based on this instance. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :param boolean requires_id: A boolean indicating whether resource ID should be part of the requested URI. :return: This :class:`Resource` instance. @@ -605,16 +606,19 @@ def get(self, session, requires_id=True): raise exceptions.MethodNotSupported(self, "get") request = self._prepare_request(requires_id=requires_id) - response = session.get(request.uri, endpoint_filter=self.service) + response = session.get(request.url) + kwargs = {} + if error_message: + kwargs['error_message'] = error_message - self._translate_response(response) + self._translate_response(response, **kwargs) return self def head(self, session): """Get headers from a remote resource based on this instance. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if @@ -625,7 +629,7 @@ def head(self, session): request = self._prepare_request() - response = session.head(request.uri, endpoint_filter=self.service, + response = session.head(request.url, headers={"Accept": ""}) self._translate_response(response) @@ -635,7 +639,7 @@ def update(self, session, prepend_key=True, has_body=True): """Update the remote resource based on this instance. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :param prepend_key: A boolean indicating whether the resource_key should be prepended in a resource update request. Default to True. @@ -657,21 +661,21 @@ def update(self, session, prepend_key=True, has_body=True): request = self._prepare_request(prepend_key=prepend_key) if self.patch_update: - response = session.patch(request.uri, endpoint_filter=self.service, + response = session.patch(request.url, json=request.body, headers=request.headers) else: - response = session.put(request.uri, endpoint_filter=self.service, + response = session.put(request.url, json=request.body, headers=request.headers) self._translate_response(response, has_body=has_body) return self - def delete(self, session): + def delete(self, session, error_message=None): """Delete the remote resource based on this instance. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if @@ -682,10 +686,13 @@ def delete(self, session): request = self._prepare_request() - response = session.delete(request.uri, endpoint_filter=self.service, + response = session.delete(request.url, headers={"Accept": ""}) + kwargs = {} + if error_message: + kwargs['error_message'] = error_message - self._translate_response(response, has_body=False) + self._translate_response(response, has_body=False, **kwargs) return self @classmethod @@ -696,7 +703,7 @@ def list(cls, session, paginated=False, **params): params for response filtering. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :param bool paginated: ``True`` if a GET to this resource returns a paginated series of responses, or ``False`` if a GET returns only one page of data. @@ -707,7 +714,7 @@ def list(cls, session, paginated=False, **params): :meth:`~openstack.resource2.QueryParamter._transpose` method to find if any of them match expected query parameters to be sent in the *params* argument to - :meth:`~openstack.session.Session.get`. They are additionally + :meth:`~keystoneauth1.adapter.Adapter.get`. They are additionally checked against the :data:`~openstack.resource2.Resource.base_path` format string to see if any path fragments need to be filled in by the contents @@ -725,7 +732,7 @@ def list(cls, session, paginated=False, **params): uri = cls.base_path % params while more_data: - resp = session.get(uri, endpoint_filter=cls.service, + resp = session.get(uri, headers={"Accept": "application/json"}, params=query_params) resp = resp.json() @@ -785,7 +792,7 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): """Find a resource by its name or id. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :param name_or_id: This resource's identifier, if needed by the request. The default is ``None``. :param bool ignore_missing: When set to ``False`` @@ -828,7 +835,7 @@ def wait_for_status(session, resource, status, failures, interval, wait): """Wait for the resource to be in a particular status. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :param resource: The resource to wait on to reach the status. The resource must have a status attribute. :type resource: :class:`~openstack.resource.Resource` @@ -871,7 +878,7 @@ def wait_for_delete(session, resource, interval, wait): """Wait for the resource to be deleted. :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` + :type session: :class:`~keystoneauth1.adapter.Adapter` :param resource: The resource to wait on to be deleted. :type resource: :class:`~openstack.resource.Resource` :param interval: Number of seconds to wait between checks. diff --git a/openstack/service_filter.py b/openstack/service_filter.py index 95bc2aa12..a0f60e3b6 100644 --- a/openstack/service_filter.py +++ b/openstack/service_filter.py @@ -16,8 +16,6 @@ :class:`~openstack.resource.Resource` has a service identifier to associate the resource with a service. An example of a service identifier would be ``openstack.compute.compute_service.ComputeService``. -The preferences are stored in the -:class:`~openstack.profile.Profile` object. The service preference and the service identifier are joined to create a filter to match a service. diff --git a/openstack/session.py b/openstack/session.py deleted file mode 100644 index d989d1172..000000000 --- a/openstack/session.py +++ /dev/null @@ -1,352 +0,0 @@ -# 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. - -""" -The :class:`~openstack.session.Session` overrides -:class:`~keystoneauth1.session.Session` to provide end point filtering and -mapping KSA exceptions to SDK exceptions. - -""" -from collections import namedtuple -import logging - -try: - from itertools import accumulate -except ImportError: - # itertools.accumulate was added to Python 3.2, and since we have to - # support Python 2 for some reason, we include this equivalent from - # the 3.x docs. While it's stated that it's a rough equivalent, it's - # good enough for the purposes we're using it for. - # https://docs.python.org/dev/library/itertools.html#itertools.accumulate - def accumulate(iterable, func=None): - """Return running totals""" - # accumulate([1,2,3,4,5]) --> 1 3 6 10 15 - # accumulate([1,2,3,4,5], operator.mul) --> 1 2 6 24 120 - it = iter(iterable) - try: - total = next(it) - except StopIteration: - return - yield total - for element in it: - total = func(total, element) - yield total - -from keystoneauth1 import exceptions as _exceptions -from keystoneauth1 import session as _session - -from openstack import exceptions -from openstack import utils -from openstack import version as openstack_version - -from six.moves.urllib import parse - -DEFAULT_USER_AGENT = "openstacksdk/%s" % openstack_version.__version__ -API_REQUEST_HEADER = "openstack-api-version" - -Version = namedtuple("Version", ["major", "minor"]) - -_logger = logging.getLogger(__name__) - - -def map_exceptions(func): - def map_exceptions_wrapper(*args, **kwargs): - try: - return func(*args, **kwargs) - except _exceptions.HttpError as e: - raise exceptions.from_exception(e) - except _exceptions.ClientException as e: - raise exceptions.SDKException(message=e.message, cause=e) - - return map_exceptions_wrapper - - -class Session(_session.Session): - - def __init__(self, profile, user_agent=None, **kwargs): - """Create a new Keystone auth session with a profile. - - :param profile: If the user has any special profiles such as the - service name, region, version or interface, they may be provided - in the profile object. If no profiles are provided, the - services that appear first in the service catalog will be used. - :param user_agent: A User-Agent header string to use for the - request. If not provided, a default of - :attr:`~openstack.session.DEFAULT_USER_AGENT` - is used, which contains the openstacksdk version - When a non-None value is passed, it will be - prepended to the default. - :type profile: :class:`~openstack.profile.Profile` - """ - if user_agent is not None: - self.user_agent = "%s %s" % (user_agent, DEFAULT_USER_AGENT) - else: - self.user_agent = DEFAULT_USER_AGENT - - self.profile = profile - api_version_header = self._get_api_requests() - self.endpoint_cache = {} - - super(Session, self).__init__(user_agent=self.user_agent, - additional_headers=api_version_header, - **kwargs) - - def _get_api_requests(self): - """Get API micro-version requests. - - :param profile: A profile object that contains customizations about - service name, region, version, interface or - api_version. - :return: A standard header string if there is any specialization in - API microversion, or None if no such request exists. - """ - if self.profile is None: - return None - - req = [] - for svc in self.profile.get_services(): - if svc.service_type and svc.api_version: - req.append(" ".join([svc.service_type, svc.api_version])) - if req: - return {API_REQUEST_HEADER: ",".join(req)} - - return None - - class _Endpoint(object): - - def __init__(self, uri, versions, - needs_project_id=False, project_id=None): - self.uri = uri - self.versions = versions - self.needs_project_id = needs_project_id - self.project_id = project_id - - def __eq__(self, other): - return all([self.uri == other.uri, - self.versions == other.versions, - self.needs_project_id == other.needs_project_id, - self.project_id == other.project_id]) - - def _parse_versions_response(self, uri): - """Look for a "versions" JSON response at `uri` - - Return versions if we get them, otherwise return None. - """ - _logger.debug("Looking for versions at %s", uri) - - try: - response = self.get(uri) - except exceptions.HttpException: - return None - - try: - response_body = response.json() - except Exception: - # This could raise a number of things, all of which are bad. - # ValueError, JSONDecodeError, etc. Rather than pick and choose - # a bunch of things that might happen, catch 'em all. - return None - - if "versions" in response_body: - versions = response_body["versions"] - # Normalize the version response. Identity nests the versions - # a level deeper than others, inside of a "values" dictionary. - if "values" in versions: - versions = versions["values"] - return self._Endpoint(uri, versions) - - return None - - def _get_endpoint_versions(self, service_type, endpoint): - """Get available endpoints from the remote service - - Take the endpoint that the Service Catalog gives us as a base - and then work from there. In most cases, the path-less 'root' - of the URI is the base of the service which contains the versions. - In other cases, we need to discover it by trying the paths that - eminate from that root. Generally this is achieved in one roundtrip - request/response, but depending on how the service is installed, - it may require multiple requests. - """ - parts = parse.urlparse(endpoint) - - just_root = "://".join([parts.scheme, parts.netloc]) - - # If we need to try using a portion of the parts, - # the project id won't be one worth asking for so remove it. - # However, we do need to know that the project id was - # previously there, so keep it. - project_id = self.get_project_id() - # Domain scope token don't include project id - project_id_location = parts.path.find(project_id) if project_id else -1 - if project_id_location > -1: - usable_path = parts.path[slice(0, project_id_location)] - needs_project_id = True - else: - usable_path = parts.path - needs_project_id = False - - # Generate a series of paths that might contain our version - # information. This will build successively longer paths from - # the split, so /nova/v2 would return "", "/nova", - # "/nova/v2" out of it. Based on what we've normally seen, - # the match will be found early on within those. - paths = accumulate(usable_path.split("/"), - func=lambda *fragments: "/".join(fragments)) - - result = None - - # If we have paths, try them from the root outwards. - # NOTE: Both the body of the for loop and the else clause - # cover the request for `just_root`. The else clause is explicit - # in only testing it because there are no path parts. In the for - # loop, it gets requested in the first iteration. - for path in paths: - response = self._parse_versions_response(just_root + path) - if response is not None: - result = response - break - else: - # If we didn't have paths, root is all we can do anyway. - response = self._parse_versions_response(just_root) - if response is not None: - result = response - - if result is not None: - if needs_project_id: - result.needs_project_id = True - result.project_id = project_id - - return result - - raise exceptions.EndpointNotFound( - "Unable to parse endpoints for %s" % service_type) - - def _parse_version(self, version): - """Parse the version and return major and minor components - - If the version was given with a leading "v", e.g., "v3", strip - that off to just numerals. - """ - version_num = version[version.find("v") + 1:] - components = version_num.split(".") - if len(components) == 1: - # The minor version of a v2 ends up being -1 so that we can - # loop through versions taking the highest available match - # while also working around a direct match for 2.0. - rv = Version(int(components[0]), -1) - elif len(components) == 2: - rv = Version(*[int(component) for component in components]) - else: - raise ValueError("Unable to parse version string %s" % version) - - return rv - - def _get_version_match(self, endpoint, profile_version, service_type): - """Return the best matching version - - Look through each version trying to find the best match for - the version specified in this profile. - * The best match will only ever be found within the same - major version, meaning a v2 profile will never match if - only v3 is available on the server. - * The search for the best match is fuzzy if needed. - * If the profile specifies v2 and the server has - v2.0, v2.1, and v2.2, the match will be v2.2. - * When an exact major/minor is specified, e.g., v2.0, - it will only match v2.0. - """ - - match_version = None - - for version in endpoint.versions: - api_version = self._parse_version(version["id"]) - if profile_version.major != api_version.major: - continue - - if profile_version.minor <= api_version.minor: - for link in version["links"]: - if link["rel"] == "self": - resp_link = link['href'] - match_version = parse.urlsplit(resp_link).path - - # Only break out of the loop on an exact match, - # otherwise keep trying. - if profile_version.minor == api_version.minor: - break - - if match_version is None: - raise exceptions.EndpointNotFound( - "Unable to determine endpoint for %s" % service_type) - - # Make sure the root endpoint has no overlap with match_version - root_parts = parse.urlsplit(endpoint.uri) - match_version = match_version.replace(root_parts.path, "", 1) - match = utils.urljoin(endpoint.uri, match_version) - - # For services that require the project id in the request URI, - # add them in here. - if endpoint.needs_project_id: - match = utils.urljoin(match, endpoint.project_id) - - return match - - def get_endpoint(self, auth=None, interface=None, service_type=None, - **kwargs): - """Override get endpoint to automate endpoint filtering - - This method uses the service catalog to find the root URI of - each service and then gets all available versions directly - from the service, not from the service catalog. - - Endpoints are cached per service type and interface combination - so that they're only requested from the remote service once - per instance of this class. - """ - key = (service_type, interface) - if key in self.endpoint_cache: - return self.endpoint_cache[key] - - filt = self.profile.get_filter(service_type) - if filt.interface is None: - filt.interface = interface - sc_endpoint = super(Session, self).get_endpoint(auth, - **filt.get_filter()) - - # Object Storage is, of course, different. Just use what we get - # back from the service catalog as not only does it not offer - # a list of supported versions, it appends an "AUTH_" prefix to - # the project id so we'd have to special case that as well. - if service_type == "object-store": - self.endpoint_cache[key] = sc_endpoint - return sc_endpoint - - # We just want what is returned from catalog - if service_type == "load-balancer": - self.endpoint_cache[key] = sc_endpoint - return sc_endpoint - - endpoint = self._get_endpoint_versions(service_type, sc_endpoint) - - profile_version = self._parse_version(filt.version) - match = self._get_version_match(endpoint, profile_version, - service_type) - - _logger.debug("Using %s as %s %s endpoint", - match, interface, service_type) - - self.endpoint_cache[key] = match - return match - - @map_exceptions - def request(self, *args, **kwargs): - return super(Session, self).request(*args, **kwargs) diff --git a/openstack/tests/functional/block_store/__init__.py b/openstack/tests/functional/block_storage/__init__.py similarity index 100% rename from openstack/tests/functional/block_store/__init__.py rename to openstack/tests/functional/block_storage/__init__.py diff --git a/openstack/tests/functional/block_store/v2/__init__.py b/openstack/tests/functional/block_storage/v2/__init__.py similarity index 100% rename from openstack/tests/functional/block_store/v2/__init__.py rename to openstack/tests/functional/block_storage/v2/__init__.py diff --git a/openstack/tests/functional/block_store/v2/test_snapshot.py b/openstack/tests/functional/block_storage/v2/test_snapshot.py similarity index 75% rename from openstack/tests/functional/block_store/v2/test_snapshot.py rename to openstack/tests/functional/block_storage/v2/test_snapshot.py index f6b254fa5..af898488d 100644 --- a/openstack/tests/functional/block_store/v2/test_snapshot.py +++ b/openstack/tests/functional/block_storage/v2/test_snapshot.py @@ -11,8 +11,8 @@ # under the License. -from openstack.block_store.v2 import snapshot as _snapshot -from openstack.block_store.v2 import volume as _volume +from openstack.block_storage.v2 import snapshot as _snapshot +from openstack.block_storage.v2 import volume as _volume from openstack.tests.functional import base @@ -26,10 +26,10 @@ def setUp(self): self.VOLUME_NAME = self.getUniqueString() self.VOLUME_ID = None - volume = self.conn.block_store.create_volume( + volume = self.conn.block_storage.create_volume( name=self.VOLUME_NAME, size=1) - self.conn.block_store.wait_for_status( + self.conn.block_storage.wait_for_status( volume, status='available', failures=['error'], @@ -38,10 +38,10 @@ def setUp(self): assert isinstance(volume, _volume.Volume) self.assertEqual(self.VOLUME_NAME, volume.name) self.VOLUME_ID = volume.id - snapshot = self.conn.block_store.create_snapshot( + snapshot = self.conn.block_storage.create_snapshot( name=self.SNAPSHOT_NAME, volume_id=self.VOLUME_ID) - self.conn.block_store.wait_for_status( + self.conn.block_storage.wait_for_status( snapshot, status='available', failures=['error'], @@ -52,17 +52,17 @@ def setUp(self): self.SNAPSHOT_ID = snapshot.id def tearDown(self): - snapshot = self.conn.block_store.get_snapshot(self.SNAPSHOT_ID) - sot = self.conn.block_store.delete_snapshot( + snapshot = self.conn.block_storage.get_snapshot(self.SNAPSHOT_ID) + sot = self.conn.block_storage.delete_snapshot( snapshot, ignore_missing=False) - self.conn.block_store.wait_for_delete( + self.conn.block_storage.wait_for_delete( snapshot, interval=2, wait=120) self.assertIsNone(sot) - sot = self.conn.block_store.delete_volume( + sot = self.conn.block_storage.delete_volume( self.VOLUME_ID, ignore_missing=False) self.assertIsNone(sot) super(TestSnapshot, self).tearDown() def test_get(self): - sot = self.conn.block_store.get_snapshot(self.SNAPSHOT_ID) + sot = self.conn.block_storage.get_snapshot(self.SNAPSHOT_ID) self.assertEqual(self.SNAPSHOT_NAME, sot.name) diff --git a/openstack/tests/functional/block_store/v2/test_type.py b/openstack/tests/functional/block_storage/v2/test_type.py similarity index 82% rename from openstack/tests/functional/block_store/v2/test_type.py rename to openstack/tests/functional/block_storage/v2/test_type.py index 6ae0e93ea..46d2b8720 100644 --- a/openstack/tests/functional/block_store/v2/test_type.py +++ b/openstack/tests/functional/block_storage/v2/test_type.py @@ -11,7 +11,7 @@ # under the License. -from openstack.block_store.v2 import type as _type +from openstack.block_storage.v2 import type as _type from openstack.tests.functional import base @@ -23,17 +23,17 @@ def setUp(self): self.TYPE_NAME = self.getUniqueString() self.TYPE_ID = None - sot = self.conn.block_store.create_type(name=self.TYPE_NAME) + sot = self.conn.block_storage.create_type(name=self.TYPE_NAME) assert isinstance(sot, _type.Type) self.assertEqual(self.TYPE_NAME, sot.name) self.TYPE_ID = sot.id def tearDown(self): - sot = self.conn.block_store.delete_type( + sot = self.conn.block_storage.delete_type( self.TYPE_ID, ignore_missing=False) self.assertIsNone(sot) super(TestType, self).tearDown() def test_get(self): - sot = self.conn.block_store.get_type(self.TYPE_ID) + sot = self.conn.block_storage.get_type(self.TYPE_ID) self.assertEqual(self.TYPE_NAME, sot.name) diff --git a/openstack/tests/functional/block_store/v2/test_volume.py b/openstack/tests/functional/block_storage/v2/test_volume.py similarity index 82% rename from openstack/tests/functional/block_store/v2/test_volume.py rename to openstack/tests/functional/block_storage/v2/test_volume.py index 66b02880d..803d858b0 100644 --- a/openstack/tests/functional/block_store/v2/test_volume.py +++ b/openstack/tests/functional/block_storage/v2/test_volume.py @@ -11,7 +11,7 @@ # under the License. -from openstack.block_store.v2 import volume as _volume +from openstack.block_storage.v2 import volume as _volume from openstack.tests.functional import base @@ -23,10 +23,10 @@ def setUp(self): self.VOLUME_NAME = self.getUniqueString() self.VOLUME_ID = None - volume = self.conn.block_store.create_volume( + volume = self.conn.block_storage.create_volume( name=self.VOLUME_NAME, size=1) - self.conn.block_store.wait_for_status( + self.conn.block_storage.wait_for_status( volume, status='available', failures=['error'], @@ -37,12 +37,12 @@ def setUp(self): self.VOLUME_ID = volume.id def tearDown(self): - sot = self.conn.block_store.delete_volume( + sot = self.conn.block_storage.delete_volume( self.VOLUME_ID, ignore_missing=False) self.assertIsNone(sot) super(TestVolume, self).tearDown() def test_get(self): - sot = self.conn.block_store.get_volume(self.VOLUME_ID) + sot = self.conn.block_storage.get_volume(self.VOLUME_ID) self.assertEqual(self.VOLUME_NAME, sot.name) diff --git a/openstack/tests/functional/telemetry/__init__.py b/openstack/tests/functional/meter/__init__.py similarity index 100% rename from openstack/tests/functional/telemetry/__init__.py rename to openstack/tests/functional/meter/__init__.py diff --git a/openstack/tests/functional/telemetry/alarm/__init__.py b/openstack/tests/functional/meter/alarm/__init__.py similarity index 100% rename from openstack/tests/functional/telemetry/alarm/__init__.py rename to openstack/tests/functional/meter/alarm/__init__.py diff --git a/openstack/tests/functional/telemetry/alarm/v2/__init__.py b/openstack/tests/functional/meter/alarm/v2/__init__.py similarity index 100% rename from openstack/tests/functional/telemetry/alarm/v2/__init__.py rename to openstack/tests/functional/meter/alarm/v2/__init__.py diff --git a/openstack/tests/functional/telemetry/alarm/v2/test_alarm.py b/openstack/tests/functional/meter/alarm/v2/test_alarm.py similarity index 94% rename from openstack/tests/functional/telemetry/alarm/v2/test_alarm.py rename to openstack/tests/functional/meter/alarm/v2/test_alarm.py index 38a18ea5f..34cdf2703 100644 --- a/openstack/tests/functional/telemetry/alarm/v2/test_alarm.py +++ b/openstack/tests/functional/meter/alarm/v2/test_alarm.py @@ -12,7 +12,7 @@ import unittest -from openstack.telemetry.alarm.v2 import alarm +from openstack.meter.alarm.v2 import alarm from openstack.tests.functional import base @@ -27,7 +27,7 @@ def setUp(self): self.require_service('metering') self.NAME = self.getUniqueString() - meter = next(self.conn.telemetry.meters()) + meter = next(self.conn.meter.meters()) sot = self.conn.alarm.create_alarm( name=self.NAME, type='threshold', diff --git a/openstack/tests/functional/telemetry/alarm/v2/test_alarm_change.py b/openstack/tests/functional/meter/alarm/v2/test_alarm_change.py similarity index 96% rename from openstack/tests/functional/telemetry/alarm/v2/test_alarm_change.py rename to openstack/tests/functional/meter/alarm/v2/test_alarm_change.py index 311944e40..f8bd5dffe 100644 --- a/openstack/tests/functional/telemetry/alarm/v2/test_alarm_change.py +++ b/openstack/tests/functional/meter/alarm/v2/test_alarm_change.py @@ -26,7 +26,7 @@ def setUp(self): self.require_service('metering') self.NAME = self.getUniqueString() - meter = next(self.conn.telemetry.meters()) + meter = next(self.conn.meter.meters()) self.alarm = self.conn.alarm.create_alarm( name=self.NAME, type='threshold', diff --git a/openstack/tests/functional/telemetry/v2/__init__.py b/openstack/tests/functional/meter/v2/__init__.py similarity index 100% rename from openstack/tests/functional/telemetry/v2/__init__.py rename to openstack/tests/functional/meter/v2/__init__.py diff --git a/openstack/tests/functional/telemetry/v2/test_capability.py b/openstack/tests/functional/meter/v2/test_capability.py similarity index 93% rename from openstack/tests/functional/telemetry/v2/test_capability.py rename to openstack/tests/functional/meter/v2/test_capability.py index 4e4f93835..991fc3c11 100644 --- a/openstack/tests/functional/telemetry/v2/test_capability.py +++ b/openstack/tests/functional/meter/v2/test_capability.py @@ -20,7 +20,7 @@ def setUp(self): self.require_service('metering') def test_list(self): - ids = [o.id for o in self.conn.telemetry.capabilities()] + ids = [o.id for o in self.conn.meter.capabilities()] self.assertIn('resources:query:simple', ids) self.assertIn('meters:query:simple', ids) self.assertIn('statistics:query:simple', ids) diff --git a/openstack/tests/functional/telemetry/v2/test_meter.py b/openstack/tests/functional/meter/v2/test_meter.py similarity index 94% rename from openstack/tests/functional/telemetry/v2/test_meter.py rename to openstack/tests/functional/meter/v2/test_meter.py index 26fe8a4e8..913bf4b73 100644 --- a/openstack/tests/functional/telemetry/v2/test_meter.py +++ b/openstack/tests/functional/meter/v2/test_meter.py @@ -27,5 +27,5 @@ def test_list(self): tainer = self.conn.object_store.create_container(name=name) self.conn.object_store.delete_container(tainer) - names = set([o.name for o in self.conn.telemetry.meters()]) + names = set([o.name for o in self.conn.meter.meters()]) self.assertIn('storage.objects.incoming.bytes', names) diff --git a/openstack/tests/functional/telemetry/v2/test_resource.py b/openstack/tests/functional/meter/v2/test_resource.py similarity index 91% rename from openstack/tests/functional/telemetry/v2/test_resource.py rename to openstack/tests/functional/meter/v2/test_resource.py index 28f8a15bc..37c2bfbe7 100644 --- a/openstack/tests/functional/telemetry/v2/test_resource.py +++ b/openstack/tests/functional/meter/v2/test_resource.py @@ -20,5 +20,5 @@ def setUp(self): self.require_service('metering') def test_list(self): - ids = [o.resource_id for o in self.conn.telemetry.resources()] + ids = [o.resource_id for o in self.conn.meter.resources()] self.assertNotEqual(0, len(ids)) diff --git a/openstack/tests/functional/telemetry/v2/test_sample.py b/openstack/tests/functional/meter/v2/test_sample.py similarity index 84% rename from openstack/tests/functional/telemetry/v2/test_sample.py rename to openstack/tests/functional/meter/v2/test_sample.py index cfa3692d8..83adca7ab 100644 --- a/openstack/tests/functional/telemetry/v2/test_sample.py +++ b/openstack/tests/functional/meter/v2/test_sample.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.telemetry.v2 import sample +from openstack.meter.v2 import sample from openstack.tests.functional import base @@ -21,6 +21,6 @@ def setUp(self): self.require_service('metering') def test_list(self): - for meter in self.conn.telemetry.meters(): - for sot in self.conn.telemetry.samples(meter): + for meter in self.conn.meter.meters(): + for sot in self.conn.meter.samples(meter): assert isinstance(sot, sample.Sample) diff --git a/openstack/tests/functional/telemetry/v2/test_statistics.py b/openstack/tests/functional/meter/v2/test_statistics.py similarity index 88% rename from openstack/tests/functional/telemetry/v2/test_statistics.py rename to openstack/tests/functional/meter/v2/test_statistics.py index 349070a0f..85cd47bdd 100644 --- a/openstack/tests/functional/telemetry/v2/test_statistics.py +++ b/openstack/tests/functional/meter/v2/test_statistics.py @@ -20,7 +20,7 @@ def setUp(self): self.require_service('metering') def test_list(self): - for met in self.conn.telemetry.meters(): - for stat in self.conn.telemetry.statistics(met): + for met in self.conn.meter.meters(): + for stat in self.conn.meter.statistics(met): self.assertTrue(stat.period_end_at is not None) break diff --git a/openstack/tests/functional/network/v2/test_agent_add_remove_network.py b/openstack/tests/functional/network/v2/test_agent_add_remove_network.py index e1ee63d93..6cd7837fa 100644 --- a/openstack/tests/functional/network/v2/test_agent_add_remove_network.py +++ b/openstack/tests/functional/network/v2/test_agent_add_remove_network.py @@ -36,11 +36,11 @@ def setUp(self): self.AGENT_ID = self.AGENT.id def test_add_remove_agent(self): - net = self.AGENT.add_agent_to_network(self.conn.session, + net = self.AGENT.add_agent_to_network(self.conn.network, network_id=self.NETWORK_ID) self._verify_add(net) - net = self.AGENT.remove_agent_from_network(self.conn.session, + net = self.AGENT.remove_agent_from_network(self.conn.network, network_id=self.NETWORK_ID) self._verify_remove(net) diff --git a/openstack/tests/functional/network/v2/test_floating_ip.py b/openstack/tests/functional/network/v2/test_floating_ip.py index bf8405309..8e0fb5afa 100644 --- a/openstack/tests/functional/network/v2/test_floating_ip.py +++ b/openstack/tests/functional/network/v2/test_floating_ip.py @@ -61,7 +61,7 @@ def setUp(self): self.ROT = sot # Add Router's Interface to Internal Network sot = self.ROT.add_interface( - self.conn.session, subnet_id=self.INT_SUB_ID) + self.conn.network, subnet_id=self.INT_SUB_ID) self.assertEqual(sot['subnet_id'], self.INT_SUB_ID) # Create Port in Internal Network prt = self.conn.network.create_port(network_id=self.INT_NET_ID) @@ -78,7 +78,7 @@ def tearDown(self): sot = self.conn.network.delete_port(self.PORT_ID, ignore_missing=False) self.assertIsNone(sot) sot = self.ROT.remove_interface( - self.conn.session, subnet_id=self.INT_SUB_ID) + self.conn.network, subnet_id=self.INT_SUB_ID) self.assertEqual(sot['subnet_id'], self.INT_SUB_ID) sot = self.conn.network.delete_router( self.ROT_ID, ignore_missing=False) diff --git a/openstack/tests/functional/network/v2/test_router_add_remove_interface.py b/openstack/tests/functional/network/v2/test_router_add_remove_interface.py index 663400dfa..a300368a0 100644 --- a/openstack/tests/functional/network/v2/test_router_add_remove_interface.py +++ b/openstack/tests/functional/network/v2/test_router_add_remove_interface.py @@ -62,10 +62,10 @@ def tearDown(self): super(TestRouterInterface, self).tearDown() def test_router_add_remove_interface(self): - iface = self.ROT.add_interface(self.conn.session, + iface = self.ROT.add_interface(self.conn.network, subnet_id=self.SUB_ID) self._verification(iface) - iface = self.ROT.remove_interface(self.conn.session, + iface = self.ROT.remove_interface(self.conn.network, subnet_id=self.SUB_ID) self._verification(iface) diff --git a/openstack/tests/unit/bare_metal/__init__.py b/openstack/tests/unit/baremetal/__init__.py similarity index 100% rename from openstack/tests/unit/bare_metal/__init__.py rename to openstack/tests/unit/baremetal/__init__.py diff --git a/openstack/tests/unit/bare_metal/test_bare_metal_service.py b/openstack/tests/unit/baremetal/test_baremetal_service.py similarity index 86% rename from openstack/tests/unit/bare_metal/test_bare_metal_service.py rename to openstack/tests/unit/baremetal/test_baremetal_service.py index 0a7f00581..e808b6ef2 100644 --- a/openstack/tests/unit/bare_metal/test_bare_metal_service.py +++ b/openstack/tests/unit/baremetal/test_baremetal_service.py @@ -12,13 +12,13 @@ import testtools -from openstack.bare_metal import bare_metal_service +from openstack.baremetal import baremetal_service -class TestBareMetalService(testtools.TestCase): +class TestBaremetalService(testtools.TestCase): def test_service(self): - sot = bare_metal_service.BareMetalService() + sot = baremetal_service.BaremetalService() self.assertEqual('baremetal', sot.service_type) self.assertEqual('public', sot.interface) self.assertIsNone(sot.region) diff --git a/openstack/tests/unit/bare_metal/test_version.py b/openstack/tests/unit/baremetal/test_version.py similarity index 97% rename from openstack/tests/unit/bare_metal/test_version.py rename to openstack/tests/unit/baremetal/test_version.py index 38aff7927..7466730a9 100644 --- a/openstack/tests/unit/bare_metal/test_version.py +++ b/openstack/tests/unit/baremetal/test_version.py @@ -12,7 +12,7 @@ import testtools -from openstack.bare_metal import version +from openstack.baremetal import version IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/bare_metal/v1/__init__.py b/openstack/tests/unit/baremetal/v1/__init__.py similarity index 100% rename from openstack/tests/unit/bare_metal/v1/__init__.py rename to openstack/tests/unit/baremetal/v1/__init__.py diff --git a/openstack/tests/unit/bare_metal/v1/test_chassis.py b/openstack/tests/unit/baremetal/v1/test_chassis.py similarity index 98% rename from openstack/tests/unit/bare_metal/v1/test_chassis.py rename to openstack/tests/unit/baremetal/v1/test_chassis.py index eab6c7a9a..b9dfe82a5 100644 --- a/openstack/tests/unit/bare_metal/v1/test_chassis.py +++ b/openstack/tests/unit/baremetal/v1/test_chassis.py @@ -12,7 +12,7 @@ import testtools -from openstack.bare_metal.v1 import chassis +from openstack.baremetal.v1 import chassis FAKE = { "created_at": "2016-08-18T22:28:48.165105+00:00", diff --git a/openstack/tests/unit/bare_metal/v1/test_driver.py b/openstack/tests/unit/baremetal/v1/test_driver.py similarity index 97% rename from openstack/tests/unit/bare_metal/v1/test_driver.py rename to openstack/tests/unit/baremetal/v1/test_driver.py index 36ce7dc1f..c360e0eba 100644 --- a/openstack/tests/unit/bare_metal/v1/test_driver.py +++ b/openstack/tests/unit/baremetal/v1/test_driver.py @@ -12,7 +12,7 @@ import testtools -from openstack.bare_metal.v1 import driver +from openstack.baremetal.v1 import driver FAKE = { "hosts": [ diff --git a/openstack/tests/unit/bare_metal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py similarity index 99% rename from openstack/tests/unit/bare_metal/v1/test_node.py rename to openstack/tests/unit/baremetal/v1/test_node.py index ddbf2512a..05cff3e48 100644 --- a/openstack/tests/unit/bare_metal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -12,7 +12,7 @@ import testtools -from openstack.bare_metal.v1 import node +from openstack.baremetal.v1 import node # NOTE: Sample data from api-ref doc FAKE = { diff --git a/openstack/tests/unit/bare_metal/v1/test_port.py b/openstack/tests/unit/baremetal/v1/test_port.py similarity index 98% rename from openstack/tests/unit/bare_metal/v1/test_port.py rename to openstack/tests/unit/baremetal/v1/test_port.py index c58b6f93a..29d4ad4d5 100644 --- a/openstack/tests/unit/bare_metal/v1/test_port.py +++ b/openstack/tests/unit/baremetal/v1/test_port.py @@ -12,7 +12,7 @@ import testtools -from openstack.bare_metal.v1 import port +from openstack.baremetal.v1 import port FAKE = { "address": "11:11:11:11:11:11", diff --git a/openstack/tests/unit/bare_metal/v1/test_port_group.py b/openstack/tests/unit/baremetal/v1/test_port_group.py similarity index 98% rename from openstack/tests/unit/bare_metal/v1/test_port_group.py rename to openstack/tests/unit/baremetal/v1/test_port_group.py index ea1cfd06e..b2c6ed147 100644 --- a/openstack/tests/unit/bare_metal/v1/test_port_group.py +++ b/openstack/tests/unit/baremetal/v1/test_port_group.py @@ -12,7 +12,7 @@ import testtools -from openstack.bare_metal.v1 import port_group +from openstack.baremetal.v1 import port_group FAKE = { "address": "11:11:11:11:11:11", diff --git a/openstack/tests/unit/bare_metal/v1/test_proxy.py b/openstack/tests/unit/baremetal/v1/test_proxy.py similarity index 94% rename from openstack/tests/unit/bare_metal/v1/test_proxy.py rename to openstack/tests/unit/baremetal/v1/test_proxy.py index 34212a6a0..d1bb49c16 100644 --- a/openstack/tests/unit/bare_metal/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal/v1/test_proxy.py @@ -12,19 +12,19 @@ import deprecation -from openstack.bare_metal.v1 import _proxy -from openstack.bare_metal.v1 import chassis -from openstack.bare_metal.v1 import driver -from openstack.bare_metal.v1 import node -from openstack.bare_metal.v1 import port -from openstack.bare_metal.v1 import port_group +from openstack.baremetal.v1 import _proxy +from openstack.baremetal.v1 import chassis +from openstack.baremetal.v1 import driver +from openstack.baremetal.v1 import node +from openstack.baremetal.v1 import port +from openstack.baremetal.v1 import port_group from openstack.tests.unit import test_proxy_base2 -class TestBareMetalProxy(test_proxy_base2.TestProxyBase): +class TestBaremetalProxy(test_proxy_base2.TestProxyBase): def setUp(self): - super(TestBareMetalProxy, self).setUp() + super(TestBaremetalProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) def test_drivers(self): diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 290910ac5..4f474e0f9 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -27,6 +27,7 @@ import tempfile import openstack +import openstack.connection from openstack.tests import base @@ -406,6 +407,19 @@ def _get_role_data(self, role_name=None): return _RoleData(role_id, role_name, {'role': response}, {'role': request}) + def use_broken_keystone(self): + self.adapter = self.useFixture(rm_fixture.Fixture()) + self.calls = [] + self._uri_registry.clear() + self.__do_register_uris([ + dict(method='GET', uri='https://identity.example.com/', + text=open(self.discovery_json, 'r').read()), + dict(method='POST', + uri='https://identity.example.com/v3/auth/tokens', + status_code=400), + ]) + self._make_test_cloud(identity_api_version='3') + def use_keystone_v3(self, catalog='catalog-v3.json'): self.adapter = self.useFixture(rm_fixture.Fixture()) self.calls = [] @@ -444,6 +458,8 @@ def _make_test_cloud(self, cloud_name='_test_cloud_', **kwargs): test_cloud = os.environ.get('OPENSTACKSDK_OS_CLOUD', cloud_name) self.cloud_config = self.config.get_one_cloud( cloud=test_cloud, validate=True, **kwargs) + self.conn = openstack.connection.Connection( + config=self.cloud_config) self.cloud = openstack.OpenStackCloud( cloud_config=self.cloud_config, log_inner_exceptions=True) diff --git a/openstack/tests/unit/block_store/__init__.py b/openstack/tests/unit/block_storage/__init__.py similarity index 100% rename from openstack/tests/unit/block_store/__init__.py rename to openstack/tests/unit/block_storage/__init__.py diff --git a/openstack/tests/unit/block_store/test_block_store_service.py b/openstack/tests/unit/block_storage/test_block_storage_service.py similarity index 85% rename from openstack/tests/unit/block_store/test_block_store_service.py rename to openstack/tests/unit/block_storage/test_block_storage_service.py index f02d12ac5..fd4ea444e 100644 --- a/openstack/tests/unit/block_store/test_block_store_service.py +++ b/openstack/tests/unit/block_storage/test_block_storage_service.py @@ -12,13 +12,13 @@ import testtools -from openstack.block_store import block_store_service +from openstack.block_storage import block_storage_service -class TestBlockStoreService(testtools.TestCase): +class TestBlockStorageService(testtools.TestCase): def test_service(self): - sot = block_store_service.BlockStoreService() + sot = block_storage_service.BlockStorageService() self.assertEqual("volume", sot.service_type) self.assertEqual("public", sot.interface) self.assertIsNone(sot.region) diff --git a/openstack/tests/unit/block_store/v2/__init__.py b/openstack/tests/unit/block_storage/v2/__init__.py similarity index 100% rename from openstack/tests/unit/block_store/v2/__init__.py rename to openstack/tests/unit/block_storage/v2/__init__.py diff --git a/openstack/tests/unit/block_store/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py similarity index 94% rename from openstack/tests/unit/block_store/v2/test_proxy.py rename to openstack/tests/unit/block_storage/v2/test_proxy.py index 4af3b4d81..431b5f48a 100644 --- a/openstack/tests/unit/block_store/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -10,10 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.block_store.v2 import _proxy -from openstack.block_store.v2 import snapshot -from openstack.block_store.v2 import type -from openstack.block_store.v2 import volume +from openstack.block_storage.v2 import _proxy +from openstack.block_storage.v2 import snapshot +from openstack.block_storage.v2 import type +from openstack.block_storage.v2 import volume from openstack.tests.unit import test_proxy_base2 diff --git a/openstack/tests/unit/block_store/v2/test_snapshot.py b/openstack/tests/unit/block_storage/v2/test_snapshot.py similarity index 98% rename from openstack/tests/unit/block_store/v2/test_snapshot.py rename to openstack/tests/unit/block_storage/v2/test_snapshot.py index 79388489a..6e542bd17 100644 --- a/openstack/tests/unit/block_store/v2/test_snapshot.py +++ b/openstack/tests/unit/block_storage/v2/test_snapshot.py @@ -12,7 +12,7 @@ import testtools -from openstack.block_store.v2 import snapshot +from openstack.block_storage.v2 import snapshot FAKE_ID = "ffa9bc5e-1172-4021-acaf-cdcd78a9584d" diff --git a/openstack/tests/unit/block_store/v2/test_type.py b/openstack/tests/unit/block_storage/v2/test_type.py similarity index 97% rename from openstack/tests/unit/block_store/v2/test_type.py rename to openstack/tests/unit/block_storage/v2/test_type.py index d841b35ca..b44c39dbe 100644 --- a/openstack/tests/unit/block_store/v2/test_type.py +++ b/openstack/tests/unit/block_storage/v2/test_type.py @@ -12,7 +12,7 @@ import testtools -from openstack.block_store.v2 import type +from openstack.block_storage.v2 import type FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff" TYPE = { diff --git a/openstack/tests/unit/block_store/v2/test_volume.py b/openstack/tests/unit/block_storage/v2/test_volume.py similarity index 99% rename from openstack/tests/unit/block_store/v2/test_volume.py rename to openstack/tests/unit/block_storage/v2/test_volume.py index 825cb910f..70b0ce8e8 100644 --- a/openstack/tests/unit/block_store/v2/test_volume.py +++ b/openstack/tests/unit/block_storage/v2/test_volume.py @@ -14,7 +14,7 @@ import testtools -from openstack.block_store.v2 import volume +from openstack.block_storage.v2 import volume FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff" diff --git a/openstack/tests/unit/cloud/test__adapter.py b/openstack/tests/unit/cloud/test__adapter.py index 9722b02c8..aaca16854 100644 --- a/openstack/tests/unit/cloud/test__adapter.py +++ b/openstack/tests/unit/cloud/test__adapter.py @@ -34,5 +34,5 @@ class TestExtractName(base.TestCase): def test_extract_name(self): - results = _adapter.extract_name(self.url) + results = _adapter._extract_name(self.url) self.assertEqual(self.parts, results) diff --git a/openstack/tests/unit/cloud/test_role_assignment.py b/openstack/tests/unit/cloud/test_role_assignment.py index 5ab23d2fc..dd7fa5bfb 100644 --- a/openstack/tests/unit/cloud/test_role_assignment.py +++ b/openstack/tests/unit/cloud/test_role_assignment.py @@ -2641,6 +2641,7 @@ def test_grant_bad_domain_exception(self): uri=self.get_mock_url(resource='domains', append=['baddomain']), status_code=404, + headers={'Content-Type': 'text/plain'}, text='Could not find domain: baddomain') ]) with testtools.ExpectedException( @@ -2663,6 +2664,7 @@ def test_revoke_bad_domain_exception(self): uri=self.get_mock_url(resource='domains', append=['baddomain']), status_code=404, + headers={'Content-Type': 'text/plain'}, text='Could not find domain: baddomain') ]) with testtools.ExpectedException( diff --git a/openstack/tests/unit/cloud/test_task_manager.py b/openstack/tests/unit/cloud/test_task_manager.py index cad3da4a7..f78d5bb4e 100644 --- a/openstack/tests/unit/cloud/test_task_manager.py +++ b/openstack/tests/unit/cloud/test_task_manager.py @@ -25,46 +25,45 @@ class TestException(Exception): class TaskTest(task_manager.Task): - def main(self, client): + def main(self): raise TestException("This is a test exception") class TaskTestGenerator(task_manager.Task): - def main(self, client): + def main(self): yield 1 class TaskTestInt(task_manager.Task): - def main(self, client): + def main(self): return int(1) class TaskTestFloat(task_manager.Task): - def main(self, client): + def main(self): return float(2.0) class TaskTestStr(task_manager.Task): - def main(self, client): + def main(self): return "test" class TaskTestBool(task_manager.Task): - def main(self, client): + def main(self): return True class TaskTestSet(task_manager.Task): - def main(self, client): + def main(self): return set([1, 2]) class TaskTestAsync(task_manager.Task): def __init__(self): - super(task_manager.Task, self).__init__() - self.run_async = True + super(TaskTestAsync, self).__init__(run_async=True) - def main(self, client): + def main(self): pass @@ -72,7 +71,7 @@ class TestTaskManager(base.TestCase): def setUp(self): super(TestTaskManager, self).setUp() - self.manager = task_manager.TaskManager(name='test', client=self) + self.manager = task_manager.TaskManager(name='test') def test_wait_re_raise(self): """Test that Exceptions thrown in a Task is reraised correctly diff --git a/openstack/tests/unit/cluster/test_cluster_service.py b/openstack/tests/unit/cluster/test_cluster_service.py index 0d7532a60..4c828bd66 100644 --- a/openstack/tests/unit/cluster/test_cluster_service.py +++ b/openstack/tests/unit/cluster/test_cluster_service.py @@ -12,13 +12,13 @@ import testtools -from openstack.cluster import cluster_service +from openstack.clustering import clustering_service -class TestClusterService(testtools.TestCase): +class TestClusteringService(testtools.TestCase): def test_service(self): - sot = cluster_service.ClusterService() + sot = clustering_service.ClusteringService() self.assertEqual('clustering', sot.service_type) self.assertEqual('public', sot.interface) self.assertIsNone(sot.region) diff --git a/openstack/tests/unit/cluster/test_version.py b/openstack/tests/unit/cluster/test_version.py index c9b0a5bd6..30646a5c7 100644 --- a/openstack/tests/unit/cluster/test_version.py +++ b/openstack/tests/unit/cluster/test_version.py @@ -12,7 +12,7 @@ import testtools -from openstack.cluster import version +from openstack.clustering import version IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/cluster/v1/test_action.py b/openstack/tests/unit/cluster/v1/test_action.py index 017779520..b12eaac98 100644 --- a/openstack/tests/unit/cluster/v1/test_action.py +++ b/openstack/tests/unit/cluster/v1/test_action.py @@ -12,7 +12,7 @@ import testtools -from openstack.cluster.v1 import action +from openstack.clustering.v1 import action FAKE_ID = '633bd3c6-520b-420f-8e6a-dc2a47022b53' diff --git a/openstack/tests/unit/cluster/v1/test_build_info.py b/openstack/tests/unit/cluster/v1/test_build_info.py index ab695c027..080d0b15b 100644 --- a/openstack/tests/unit/cluster/v1/test_build_info.py +++ b/openstack/tests/unit/cluster/v1/test_build_info.py @@ -12,7 +12,7 @@ import testtools -from openstack.cluster.v1 import build_info +from openstack.clustering.v1 import build_info FAKE = { diff --git a/openstack/tests/unit/cluster/v1/test_cluster.py b/openstack/tests/unit/cluster/v1/test_cluster.py index 5c1de631f..31f34cab7 100644 --- a/openstack/tests/unit/cluster/v1/test_cluster.py +++ b/openstack/tests/unit/cluster/v1/test_cluster.py @@ -13,7 +13,7 @@ import mock import testtools -from openstack.cluster.v1 import cluster +from openstack.clustering.v1 import cluster FAKE_ID = '092d0955-2645-461a-b8fa-6a44655cdb2c' @@ -122,7 +122,7 @@ def test_scale_in(self): self.assertEqual('', sot.scale_in(sess, 3)) url = 'clusters/%s/actions' % sot.id body = {'scale_in': {'count': 3}} - sess.post.assert_called_once_with(url, endpoint_filter=sot.service, + sess.post.assert_called_once_with(url, json=body) def test_scale_out(self): @@ -135,7 +135,7 @@ def test_scale_out(self): self.assertEqual('', sot.scale_out(sess, 3)) url = 'clusters/%s/actions' % sot.id body = {'scale_out': {'count': 3}} - sess.post.assert_called_once_with(url, endpoint_filter=sot.service, + sess.post.assert_called_once_with(url, json=body) def test_resize(self): @@ -148,7 +148,7 @@ def test_resize(self): self.assertEqual('', sot.resize(sess, foo='bar', zoo=5)) url = 'clusters/%s/actions' % sot.id body = {'resize': {'foo': 'bar', 'zoo': 5}} - sess.post.assert_called_once_with(url, endpoint_filter=sot.service, + sess.post.assert_called_once_with(url, json=body) def test_add_nodes(self): @@ -161,7 +161,7 @@ def test_add_nodes(self): self.assertEqual('', sot.add_nodes(sess, ['node-33'])) url = 'clusters/%s/actions' % sot.id body = {'add_nodes': {'nodes': ['node-33']}} - sess.post.assert_called_once_with(url, endpoint_filter=sot.service, + sess.post.assert_called_once_with(url, json=body) def test_del_nodes(self): @@ -174,7 +174,7 @@ def test_del_nodes(self): self.assertEqual('', sot.del_nodes(sess, ['node-11'])) url = 'clusters/%s/actions' % sot.id body = {'del_nodes': {'nodes': ['node-11']}} - sess.post.assert_called_once_with(url, endpoint_filter=sot.service, + sess.post.assert_called_once_with(url, json=body) def test_del_nodes_with_params(self): @@ -195,7 +195,7 @@ def test_del_nodes_with_params(self): 'destroy_after_deletion': True, } } - sess.post.assert_called_once_with(url, endpoint_filter=sot.service, + sess.post.assert_called_once_with(url, json=body) def test_replace_nodes(self): @@ -208,7 +208,7 @@ def test_replace_nodes(self): self.assertEqual('', sot.replace_nodes(sess, {'node-22': 'node-44'})) url = 'clusters/%s/actions' % sot.id body = {'replace_nodes': {'nodes': {'node-22': 'node-44'}}} - sess.post.assert_called_once_with(url, endpoint_filter=sot.service, + sess.post.assert_called_once_with(url, json=body) def test_policy_attach(self): @@ -230,7 +230,7 @@ def test_policy_attach(self): 'enabled': True, } } - sess.post.assert_called_once_with(url, endpoint_filter=sot.service, + sess.post.assert_called_once_with(url, json=body) def test_policy_detach(self): @@ -244,7 +244,7 @@ def test_policy_detach(self): url = 'clusters/%s/actions' % sot.id body = {'policy_detach': {'policy_id': 'POLICY'}} - sess.post.assert_called_once_with(url, endpoint_filter=sot.service, + sess.post.assert_called_once_with(url, json=body) def test_policy_update(self): @@ -266,7 +266,7 @@ def test_policy_update(self): 'enabled': False } } - sess.post.assert_called_once_with(url, endpoint_filter=sot.service, + sess.post.assert_called_once_with(url, json=body) def test_check(self): @@ -279,7 +279,7 @@ def test_check(self): self.assertEqual('', sot.check(sess)) url = 'clusters/%s/actions' % sot.id body = {'check': {}} - sess.post.assert_called_once_with(url, endpoint_filter=sot.service, + sess.post.assert_called_once_with(url, json=body) def test_recover(self): @@ -292,7 +292,7 @@ def test_recover(self): self.assertEqual('', sot.recover(sess)) url = 'clusters/%s/actions' % sot.id body = {'recover': {}} - sess.post.assert_called_once_with(url, endpoint_filter=sot.service, + sess.post.assert_called_once_with(url, json=body) def test_operation(self): @@ -305,5 +305,5 @@ def test_operation(self): self.assertEqual('', sot.op(sess, 'dance', style='tango')) url = 'clusters/%s/ops' % sot.id body = {'dance': {'style': 'tango'}} - sess.post.assert_called_once_with(url, endpoint_filter=sot.service, + sess.post.assert_called_once_with(url, json=body) diff --git a/openstack/tests/unit/cluster/v1/test_cluster_attr.py b/openstack/tests/unit/cluster/v1/test_cluster_attr.py index cded9ef87..3d7181682 100644 --- a/openstack/tests/unit/cluster/v1/test_cluster_attr.py +++ b/openstack/tests/unit/cluster/v1/test_cluster_attr.py @@ -12,7 +12,7 @@ import testtools -from openstack.cluster.v1 import cluster_attr as ca +from openstack.clustering.v1 import cluster_attr as ca FAKE = { diff --git a/openstack/tests/unit/cluster/v1/test_cluster_policy.py b/openstack/tests/unit/cluster/v1/test_cluster_policy.py index a4126977c..e84990e12 100644 --- a/openstack/tests/unit/cluster/v1/test_cluster_policy.py +++ b/openstack/tests/unit/cluster/v1/test_cluster_policy.py @@ -12,7 +12,7 @@ import testtools -from openstack.cluster.v1 import cluster_policy +from openstack.clustering.v1 import cluster_policy FAKE = { diff --git a/openstack/tests/unit/cluster/v1/test_event.py b/openstack/tests/unit/cluster/v1/test_event.py index 0d482b97e..82a76593a 100644 --- a/openstack/tests/unit/cluster/v1/test_event.py +++ b/openstack/tests/unit/cluster/v1/test_event.py @@ -12,7 +12,7 @@ import testtools -from openstack.cluster.v1 import event +from openstack.clustering.v1 import event FAKE = { diff --git a/openstack/tests/unit/cluster/v1/test_node.py b/openstack/tests/unit/cluster/v1/test_node.py index 4566387d4..3aa3b26ae 100644 --- a/openstack/tests/unit/cluster/v1/test_node.py +++ b/openstack/tests/unit/cluster/v1/test_node.py @@ -13,7 +13,7 @@ import mock import testtools -from openstack.cluster.v1 import node +from openstack.clustering.v1 import node FAKE_ID = '123d0955-0099-aabb-b8fa-6a44655ceeff' @@ -78,7 +78,7 @@ def test_check(self): self.assertEqual('', sot.check(sess)) url = 'nodes/%s/actions' % sot.id body = {'check': {}} - sess.post.assert_called_once_with(url, endpoint_filter=sot.service, + sess.post.assert_called_once_with(url, json=body) def test_recover(self): @@ -91,7 +91,7 @@ def test_recover(self): self.assertEqual('', sot.recover(sess)) url = 'nodes/%s/actions' % sot.id body = {'recover': {}} - sess.post.assert_called_once_with(url, endpoint_filter=sot.service, + sess.post.assert_called_once_with(url, json=body) def test_operation(self): @@ -103,7 +103,7 @@ def test_operation(self): sess.post = mock.Mock(return_value=resp) self.assertEqual('', sot.op(sess, 'dance', style='tango')) url = 'nodes/%s/ops' % sot.id - sess.post.assert_called_once_with(url, endpoint_filter=sot.service, + sess.post.assert_called_once_with(url, json={'dance': {'style': 'tango'}}) def test_adopt_preview(self): @@ -123,7 +123,6 @@ def test_adopt_preview(self): res = sot.adopt(sess, True, **attrs) self.assertEqual({"foo": "bar"}, res) sess.post.assert_called_once_with("nodes/adopt-preview", - endpoint_filter=sot.service, json=attrs) def test_adopt(self): @@ -131,13 +130,13 @@ def test_adopt(self): resp = mock.Mock() resp.headers = {} resp.json = mock.Mock(return_value={"foo": "bar"}) + resp.status_code = 200 sess = mock.Mock() sess.post = mock.Mock(return_value=resp) res = sot.adopt(sess, False, param="value") self.assertEqual(sot, res) sess.post.assert_called_once_with("nodes/adopt", - endpoint_filter=sot.service, json={"param": "value"}) diff --git a/openstack/tests/unit/cluster/v1/test_policy.py b/openstack/tests/unit/cluster/v1/test_policy.py index 7a9b18fab..a238c08cf 100644 --- a/openstack/tests/unit/cluster/v1/test_policy.py +++ b/openstack/tests/unit/cluster/v1/test_policy.py @@ -12,7 +12,7 @@ import testtools -from openstack.cluster.v1 import policy +from openstack.clustering.v1 import policy FAKE_ID = 'ac5415bd-f522-4160-8be0-f8853e4bc332' diff --git a/openstack/tests/unit/cluster/v1/test_policy_type.py b/openstack/tests/unit/cluster/v1/test_policy_type.py index fd20733a3..7ce8f0507 100644 --- a/openstack/tests/unit/cluster/v1/test_policy_type.py +++ b/openstack/tests/unit/cluster/v1/test_policy_type.py @@ -12,7 +12,7 @@ import testtools -from openstack.cluster.v1 import policy_type +from openstack.clustering.v1 import policy_type FAKE = { diff --git a/openstack/tests/unit/cluster/v1/test_profile.py b/openstack/tests/unit/cluster/v1/test_profile.py index 7a65bf6f3..700382466 100644 --- a/openstack/tests/unit/cluster/v1/test_profile.py +++ b/openstack/tests/unit/cluster/v1/test_profile.py @@ -12,7 +12,7 @@ import testtools -from openstack.cluster.v1 import profile +from openstack.clustering.v1 import profile FAKE_ID = '9b127538-a675-4271-ab9b-f24f54cfe173' diff --git a/openstack/tests/unit/cluster/v1/test_profile_type.py b/openstack/tests/unit/cluster/v1/test_profile_type.py index d494a8e55..88816e2c6 100644 --- a/openstack/tests/unit/cluster/v1/test_profile_type.py +++ b/openstack/tests/unit/cluster/v1/test_profile_type.py @@ -12,7 +12,7 @@ import testtools -from openstack.cluster.v1 import profile_type +from openstack.clustering.v1 import profile_type FAKE = { diff --git a/openstack/tests/unit/cluster/v1/test_proxy.py b/openstack/tests/unit/cluster/v1/test_proxy.py index ec6b55c35..905a46eae 100644 --- a/openstack/tests/unit/cluster/v1/test_proxy.py +++ b/openstack/tests/unit/cluster/v1/test_proxy.py @@ -13,20 +13,20 @@ import deprecation import mock -from openstack.cluster.v1 import _proxy -from openstack.cluster.v1 import action -from openstack.cluster.v1 import build_info -from openstack.cluster.v1 import cluster -from openstack.cluster.v1 import cluster_attr -from openstack.cluster.v1 import cluster_policy -from openstack.cluster.v1 import event -from openstack.cluster.v1 import node -from openstack.cluster.v1 import policy -from openstack.cluster.v1 import policy_type -from openstack.cluster.v1 import profile -from openstack.cluster.v1 import profile_type -from openstack.cluster.v1 import receiver -from openstack.cluster.v1 import service +from openstack.clustering.v1 import _proxy +from openstack.clustering.v1 import action +from openstack.clustering.v1 import build_info +from openstack.clustering.v1 import cluster +from openstack.clustering.v1 import cluster_attr +from openstack.clustering.v1 import cluster_policy +from openstack.clustering.v1 import event +from openstack.clustering.v1 import node +from openstack.clustering.v1 import policy +from openstack.clustering.v1 import policy_type +from openstack.clustering.v1 import profile +from openstack.clustering.v1 import profile_type +from openstack.clustering.v1 import receiver +from openstack.clustering.v1 import service from openstack import proxy2 as proxy_base from openstack.tests.unit import test_proxy_base2 @@ -114,7 +114,7 @@ def test_cluster_update(self): def test_cluster_add_nodes(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_find.return_value = mock_cluster - self._verify("openstack.cluster.v1.cluster.Cluster.add_nodes", + self._verify("openstack.clustering.v1.cluster.Cluster.add_nodes", self.proxy.cluster_add_nodes, method_args=["FAKE_CLUSTER", ["node1"]], expected_args=[["node1"]]) @@ -124,7 +124,7 @@ def test_cluster_add_nodes(self, mock_find): @deprecation.fail_if_not_removed def test_cluster_add_nodes_with_obj(self): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') - self._verify("openstack.cluster.v1.cluster.Cluster.add_nodes", + self._verify("openstack.clustering.v1.cluster.Cluster.add_nodes", self.proxy.cluster_add_nodes, method_args=[mock_cluster, ["node1"]], expected_args=[["node1"]]) @@ -134,7 +134,7 @@ def test_cluster_add_nodes_with_obj(self): def test_cluster_del_nodes(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_find.return_value = mock_cluster - self._verify("openstack.cluster.v1.cluster.Cluster.del_nodes", + self._verify("openstack.clustering.v1.cluster.Cluster.del_nodes", self.proxy.cluster_del_nodes, method_args=["FAKE_CLUSTER", ["node1"]], expected_args=[["node1"]]) @@ -144,7 +144,7 @@ def test_cluster_del_nodes(self, mock_find): @deprecation.fail_if_not_removed def test_cluster_del_nodes_with_obj(self): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') - self._verify("openstack.cluster.v1.cluster.Cluster.del_nodes", + self._verify("openstack.clustering.v1.cluster.Cluster.del_nodes", self.proxy.cluster_del_nodes, method_args=[mock_cluster, ["node1"]], method_kwargs={"key": "value"}, @@ -156,7 +156,7 @@ def test_cluster_del_nodes_with_obj(self): def test_cluster_replace_nodes(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_find.return_value = mock_cluster - self._verify("openstack.cluster.v1.cluster.Cluster.replace_nodes", + self._verify("openstack.clustering.v1.cluster.Cluster.replace_nodes", self.proxy.cluster_replace_nodes, method_args=["FAKE_CLUSTER", {"node1": "node2"}], expected_args=[{"node1": "node2"}]) @@ -166,7 +166,7 @@ def test_cluster_replace_nodes(self, mock_find): @deprecation.fail_if_not_removed def test_cluster_replace_nodes_with_obj(self): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') - self._verify("openstack.cluster.v1.cluster.Cluster.replace_nodes", + self._verify("openstack.clustering.v1.cluster.Cluster.replace_nodes", self.proxy.cluster_replace_nodes, method_args=[mock_cluster, {"node1": "node2"}], expected_args=[{"node1": "node2"}]) @@ -176,7 +176,7 @@ def test_cluster_replace_nodes_with_obj(self): def test_cluster_scale_out(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_find.return_value = mock_cluster - self._verify("openstack.cluster.v1.cluster.Cluster.scale_out", + self._verify("openstack.clustering.v1.cluster.Cluster.scale_out", self.proxy.cluster_scale_out, method_args=["FAKE_CLUSTER", 3], expected_args=[3]) @@ -186,7 +186,7 @@ def test_cluster_scale_out(self, mock_find): @deprecation.fail_if_not_removed def test_cluster_scale_out_with_obj(self): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') - self._verify("openstack.cluster.v1.cluster.Cluster.scale_out", + self._verify("openstack.clustering.v1.cluster.Cluster.scale_out", self.proxy.cluster_scale_out, method_args=[mock_cluster, 5], expected_args=[5]) @@ -196,7 +196,7 @@ def test_cluster_scale_out_with_obj(self): def test_cluster_scale_in(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_find.return_value = mock_cluster - self._verify("openstack.cluster.v1.cluster.Cluster.scale_in", + self._verify("openstack.clustering.v1.cluster.Cluster.scale_in", self.proxy.cluster_scale_in, method_args=["FAKE_CLUSTER", 3], expected_args=[3]) @@ -206,7 +206,7 @@ def test_cluster_scale_in(self, mock_find): @deprecation.fail_if_not_removed def test_cluster_scale_in_with_obj(self): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') - self._verify("openstack.cluster.v1.cluster.Cluster.scale_in", + self._verify("openstack.clustering.v1.cluster.Cluster.scale_in", self.proxy.cluster_scale_in, method_args=[mock_cluster, 5], expected_args=[5]) @@ -220,7 +220,7 @@ def test_services(self): def test_cluster_resize(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_find.return_value = mock_cluster - self._verify("openstack.cluster.v1.cluster.Cluster.resize", + self._verify("openstack.clustering.v1.cluster.Cluster.resize", self.proxy.cluster_resize, method_args=["FAKE_CLUSTER"], method_kwargs={'k1': 'v1', 'k2': 'v2'}, @@ -230,7 +230,7 @@ def test_cluster_resize(self, mock_find): def test_cluster_resize_with_obj(self): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') - self._verify("openstack.cluster.v1.cluster.Cluster.resize", + self._verify("openstack.clustering.v1.cluster.Cluster.resize", self.proxy.cluster_resize, method_args=[mock_cluster], method_kwargs={'k1': 'v1', 'k2': 'v2'}, @@ -241,7 +241,7 @@ def test_cluster_resize_with_obj(self): def test_cluster_attach_policy(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_find.return_value = mock_cluster - self._verify("openstack.cluster.v1.cluster.Cluster.policy_attach", + self._verify("openstack.clustering.v1.cluster.Cluster.policy_attach", self.proxy.cluster_attach_policy, method_args=["FAKE_CLUSTER", "FAKE_POLICY"], method_kwargs={"k1": "v1", "k2": "v2"}, @@ -253,7 +253,7 @@ def test_cluster_attach_policy(self, mock_find): @deprecation.fail_if_not_removed def test_cluster_attach_policy_with_obj(self): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') - self._verify("openstack.cluster.v1.cluster.Cluster.policy_attach", + self._verify("openstack.clustering.v1.cluster.Cluster.policy_attach", self.proxy.cluster_attach_policy, method_args=[mock_cluster, "FAKE_POLICY"], method_kwargs={"k1": "v1", "k2": "v2"}, @@ -265,7 +265,7 @@ def test_cluster_attach_policy_with_obj(self): def test_cluster_detach_policy(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_find.return_value = mock_cluster - self._verify("openstack.cluster.v1.cluster.Cluster.policy_detach", + self._verify("openstack.clustering.v1.cluster.Cluster.policy_detach", self.proxy.cluster_detach_policy, method_args=["FAKE_CLUSTER", "FAKE_POLICY"], expected_args=["FAKE_POLICY"]) @@ -275,7 +275,7 @@ def test_cluster_detach_policy(self, mock_find): @deprecation.fail_if_not_removed def test_cluster_detach_policy_with_obj(self): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') - self._verify("openstack.cluster.v1.cluster.Cluster.policy_detach", + self._verify("openstack.clustering.v1.cluster.Cluster.policy_detach", self.proxy.cluster_detach_policy, method_args=[mock_cluster, "FAKE_POLICY"], expected_args=["FAKE_POLICY"]) @@ -285,7 +285,7 @@ def test_cluster_detach_policy_with_obj(self): def test_cluster_update_policy(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_find.return_value = mock_cluster - self._verify("openstack.cluster.v1.cluster.Cluster.policy_update", + self._verify("openstack.clustering.v1.cluster.Cluster.policy_update", self.proxy.cluster_update_policy, method_args=["FAKE_CLUSTER", "FAKE_POLICY"], method_kwargs={"k1": "v1", "k2": "v2"}, @@ -297,7 +297,7 @@ def test_cluster_update_policy(self, mock_find): @deprecation.fail_if_not_removed def test_cluster_update_policy_with_obj(self): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') - self._verify("openstack.cluster.v1.cluster.Cluster.policy_update", + self._verify("openstack.clustering.v1.cluster.Cluster.policy_update", self.proxy.cluster_update_policy, method_args=[mock_cluster, "FAKE_POLICY"], method_kwargs={"k1": "v1", "k2": "v2"}, @@ -315,7 +315,7 @@ def test_collect_cluster_attrs(self): def test_cluster_check(self, mock_get): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_get.return_value = mock_cluster - self._verify("openstack.cluster.v1.cluster.Cluster.check", + self._verify("openstack.clustering.v1.cluster.Cluster.check", self.proxy.check_cluster, method_args=["FAKE_CLUSTER"]) mock_get.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER") @@ -324,7 +324,7 @@ def test_cluster_check(self, mock_get): def test_cluster_recover(self, mock_get): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_get.return_value = mock_cluster - self._verify("openstack.cluster.v1.cluster.Cluster.recover", + self._verify("openstack.clustering.v1.cluster.Cluster.recover", self.proxy.recover_cluster, method_args=["FAKE_CLUSTER"]) mock_get.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER") @@ -334,7 +334,7 @@ def test_cluster_recover(self, mock_get): def test_cluster_operation(self, mock_get): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_get.return_value = mock_cluster - self._verify("openstack.cluster.v1.cluster.Cluster.op", + self._verify("openstack.clustering.v1.cluster.Cluster.op", self.proxy.cluster_operation, method_args=["FAKE_CLUSTER", "dance"], expected_args=["dance"]) @@ -377,7 +377,7 @@ def test_node_update(self): def test_node_check(self, mock_get): mock_node = node.Node.new(id='FAKE_NODE') mock_get.return_value = mock_node - self._verify("openstack.cluster.v1.node.Node.check", + self._verify("openstack.clustering.v1.node.Node.check", self.proxy.check_node, method_args=["FAKE_NODE"]) mock_get.assert_called_once_with(node.Node, "FAKE_NODE") @@ -386,7 +386,7 @@ def test_node_check(self, mock_get): def test_node_recover(self, mock_get): mock_node = node.Node.new(id='FAKE_NODE') mock_get.return_value = mock_node - self._verify("openstack.cluster.v1.node.Node.recover", + self._verify("openstack.clustering.v1.node.Node.recover", self.proxy.recover_node, method_args=["FAKE_NODE"]) mock_get.assert_called_once_with(node.Node, "FAKE_NODE") @@ -395,7 +395,7 @@ def test_node_recover(self, mock_get): def test_node_adopt(self, mock_get): mock_node = node.Node.new() mock_get.return_value = mock_node - self._verify("openstack.cluster.v1.node.Node.adopt", + self._verify("openstack.clustering.v1.node.Node.adopt", self.proxy.adopt_node, method_kwargs={"preview": False, "foo": "bar"}, expected_kwargs={"preview": False, "foo": "bar"}) @@ -406,7 +406,7 @@ def test_node_adopt(self, mock_get): def test_node_adopt_preview(self, mock_get): mock_node = node.Node.new() mock_get.return_value = mock_node - self._verify("openstack.cluster.v1.node.Node.adopt", + self._verify("openstack.clustering.v1.node.Node.adopt", self.proxy.adopt_node, method_kwargs={"preview": True, "foo": "bar"}, expected_kwargs={"preview": True, "foo": "bar"}) @@ -418,7 +418,7 @@ def test_node_adopt_preview(self, mock_get): def test_node_operation(self, mock_get): mock_node = node.Node.new(id='FAKE_CLUSTER') mock_get.return_value = mock_node - self._verify("openstack.cluster.v1.node.Node.op", + self._verify("openstack.clustering.v1.node.Node.op", self.proxy.node_operation, method_args=["FAKE_NODE", "dance"], expected_args=["dance"]) @@ -536,7 +536,7 @@ def test_wait_for(self, mock_wait): self.proxy.wait_for_status(mock_resource, 'ACTIVE') - mock_wait.assert_called_once_with(self.session, mock_resource, + mock_wait.assert_called_once_with(self.proxy, mock_resource, 'ACTIVE', [], 2, 120) @mock.patch("openstack.resource2.wait_for_status") @@ -546,7 +546,7 @@ def test_wait_for_params(self, mock_wait): self.proxy.wait_for_status(mock_resource, 'ACTIVE', ['ERROR'], 1, 2) - mock_wait.assert_called_once_with(self.session, mock_resource, + mock_wait.assert_called_once_with(self.proxy, mock_resource, 'ACTIVE', ['ERROR'], 1, 2) @mock.patch("openstack.resource2.wait_for_delete") @@ -556,7 +556,7 @@ def test_wait_for_delete(self, mock_wait): self.proxy.wait_for_delete(mock_resource) - mock_wait.assert_called_once_with(self.session, mock_resource, 2, 120) + mock_wait.assert_called_once_with(self.proxy, mock_resource, 2, 120) @mock.patch("openstack.resource2.wait_for_delete") def test_wait_for_delete_params(self, mock_wait): @@ -565,4 +565,4 @@ def test_wait_for_delete_params(self, mock_wait): self.proxy.wait_for_delete(mock_resource, 1, 2) - mock_wait.assert_called_once_with(self.session, mock_resource, 1, 2) + mock_wait.assert_called_once_with(self.proxy, mock_resource, 1, 2) diff --git a/openstack/tests/unit/cluster/v1/test_receiver.py b/openstack/tests/unit/cluster/v1/test_receiver.py index bac16f006..39875b4ab 100644 --- a/openstack/tests/unit/cluster/v1/test_receiver.py +++ b/openstack/tests/unit/cluster/v1/test_receiver.py @@ -12,7 +12,7 @@ import testtools -from openstack.cluster.v1 import receiver +from openstack.clustering.v1 import receiver FAKE_ID = 'ae63a10b-4a90-452c-aef1-113a0b255ee3' diff --git a/openstack/tests/unit/cluster/v1/test_service.py b/openstack/tests/unit/cluster/v1/test_service.py index 20b3b1b51..cc94f42b4 100644 --- a/openstack/tests/unit/cluster/v1/test_service.py +++ b/openstack/tests/unit/cluster/v1/test_service.py @@ -13,7 +13,7 @@ import mock import testtools -from openstack.cluster.v1 import service +from openstack.clustering.v1 import service IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/compute/v2/test_metadata.py b/openstack/tests/unit/compute/v2/test_metadata.py index 458f931b1..8f2c4da64 100644 --- a/openstack/tests/unit/compute/v2/test_metadata.py +++ b/openstack/tests/unit/compute/v2/test_metadata.py @@ -48,9 +48,9 @@ def _test_get_all_metadata(self, sot): result = sot.get_metadata(sess) self.assertEqual(result, self.metadata_result["metadata"]) - sess.get.assert_called_once_with("servers/IDENTIFIER/metadata", - headers={}, - endpoint_filter=sot.service) + sess.get.assert_called_once_with( + "servers/IDENTIFIER/metadata", + headers={}) def test_set_metadata(self): response = mock.Mock() @@ -66,7 +66,6 @@ def test_set_metadata(self): self.assertEqual(result, self.metadata_result["metadata"]) sess.post.assert_called_once_with("servers/IDENTIFIER/metadata", - endpoint_filter=sot.service, headers={}, json={"metadata": set_meta}) @@ -83,4 +82,4 @@ def test_delete_metadata(self): sess.delete.assert_called_once_with( "servers/IDENTIFIER/metadata/" + key, headers={"Accept": ""}, - endpoint_filter=sot.service) + ) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index a608a261d..39a971e6d 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -428,7 +428,7 @@ def test_get_all_server_metadata(self): self.proxy.get_server_metadata, method_args=["value"], method_result=server.Server(id="value", metadata={}), - expected_args=[self.session], + expected_args=[self.proxy], expected_result={}) def test_set_server_metadata(self): @@ -440,7 +440,7 @@ def test_set_server_metadata(self): method_kwargs=kwargs, method_result=server.Server.existing(id=id, metadata=kwargs), - expected_args=[self.session], + expected_args=[self.proxy], expected_kwargs=kwargs, expected_result=kwargs) @@ -449,7 +449,7 @@ def test_delete_server_metadata(self): self.proxy.delete_server_metadata, expected_result=None, method_args=["value", "key"], - expected_args=[self.session, "key"]) + expected_args=[self.proxy, "key"]) def test_server_group_create(self): self.verify_create(self.proxy.create_server_group, diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 80f5fa93e..601ec5f62 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -193,7 +193,7 @@ def test_change_password(self): body = {"changePassword": {"adminPass": "a"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_reboot(self): sot = server.Server(**EXAMPLE) @@ -204,7 +204,7 @@ def test_reboot(self): body = {"reboot": {"type": "HARD"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_force_delete(self): sot = server.Server(**EXAMPLE) @@ -215,7 +215,7 @@ def test_force_delete(self): body = {'forceDelete': None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_rebuild(self): sot = server.Server(**EXAMPLE) @@ -246,7 +246,7 @@ def test_rebuild(self): } headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_rebuild_minimal(self): sot = server.Server(**EXAMPLE) @@ -270,7 +270,7 @@ def test_rebuild_minimal(self): } headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_resize(self): sot = server.Server(**EXAMPLE) @@ -281,7 +281,7 @@ def test_resize(self): body = {"resize": {"flavorRef": "2"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_confirm_resize(self): sot = server.Server(**EXAMPLE) @@ -292,7 +292,7 @@ def test_confirm_resize(self): body = {"confirmResize": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_revert_resize(self): sot = server.Server(**EXAMPLE) @@ -303,7 +303,7 @@ def test_revert_resize(self): body = {"revertResize": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_create_image(self): sot = server.Server(**EXAMPLE) @@ -316,7 +316,7 @@ def test_create_image(self): body = {"createImage": {'name': name, 'metadata': metadata}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_create_image_minimal(self): sot = server.Server(**EXAMPLE) @@ -328,7 +328,7 @@ def test_create_image_minimal(self): body = {"createImage": {'name': name}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=dict(sot.service), json=body, headers=headers) + url, json=body, headers=headers) def test_add_security_group(self): sot = server.Server(**EXAMPLE) @@ -339,7 +339,7 @@ def test_add_security_group(self): body = {"addSecurityGroup": {"name": "group"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_remove_security_group(self): sot = server.Server(**EXAMPLE) @@ -350,7 +350,7 @@ def test_remove_security_group(self): body = {"removeSecurityGroup": {"name": "group"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_reset_state(self): sot = server.Server(**EXAMPLE) @@ -361,7 +361,7 @@ def test_reset_state(self): body = {"os-resetState": {"state": 'active'}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_add_fixed_ip(self): sot = server.Server(**EXAMPLE) @@ -373,7 +373,7 @@ def test_add_fixed_ip(self): body = {"addFixedIp": {"networkId": "NETWORK-ID"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_remove_fixed_ip(self): sot = server.Server(**EXAMPLE) @@ -385,7 +385,7 @@ def test_remove_fixed_ip(self): body = {"removeFixedIp": {"address": "ADDRESS"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_add_floating_ip(self): sot = server.Server(**EXAMPLE) @@ -397,7 +397,7 @@ def test_add_floating_ip(self): body = {"addFloatingIp": {"address": "FLOATING-IP"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_add_floating_ip_with_fixed_addr(self): sot = server.Server(**EXAMPLE) @@ -410,7 +410,7 @@ def test_add_floating_ip_with_fixed_addr(self): "fixed_address": "FIXED-ADDR"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_remove_floating_ip(self): sot = server.Server(**EXAMPLE) @@ -422,7 +422,7 @@ def test_remove_floating_ip(self): body = {"removeFloatingIp": {"address": "I-AM-FLOATING"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_backup(self): sot = server.Server(**EXAMPLE) @@ -435,7 +435,7 @@ def test_backup(self): "rotation": 1}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_pause(self): sot = server.Server(**EXAMPLE) @@ -447,7 +447,7 @@ def test_pause(self): body = {"pause": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_unpause(self): sot = server.Server(**EXAMPLE) @@ -459,7 +459,7 @@ def test_unpause(self): body = {"unpause": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_suspend(self): sot = server.Server(**EXAMPLE) @@ -471,7 +471,7 @@ def test_suspend(self): body = {"suspend": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_resume(self): sot = server.Server(**EXAMPLE) @@ -483,7 +483,7 @@ def test_resume(self): body = {"resume": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_lock(self): sot = server.Server(**EXAMPLE) @@ -495,7 +495,7 @@ def test_lock(self): body = {"lock": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_unlock(self): sot = server.Server(**EXAMPLE) @@ -507,7 +507,7 @@ def test_unlock(self): body = {"unlock": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_rescue(self): sot = server.Server(**EXAMPLE) @@ -519,7 +519,7 @@ def test_rescue(self): body = {"rescue": {}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_rescue_with_options(self): sot = server.Server(**EXAMPLE) @@ -532,7 +532,7 @@ def test_rescue_with_options(self): 'rescue_image_ref': 'IMG-ID'}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_unrescue(self): sot = server.Server(**EXAMPLE) @@ -544,7 +544,7 @@ def test_unrescue(self): body = {"unrescue": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_evacuate(self): sot = server.Server(**EXAMPLE) @@ -556,7 +556,7 @@ def test_evacuate(self): body = {"evacuate": {}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_evacuate_with_options(self): sot = server.Server(**EXAMPLE) @@ -570,7 +570,7 @@ def test_evacuate_with_options(self): 'force': True}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_start(self): sot = server.Server(**EXAMPLE) @@ -582,7 +582,7 @@ def test_start(self): body = {"os-start": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_stop(self): sot = server.Server(**EXAMPLE) @@ -594,7 +594,7 @@ def test_stop(self): body = {"os-stop": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_shelve(self): sot = server.Server(**EXAMPLE) @@ -606,7 +606,7 @@ def test_shelve(self): body = {"shelve": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_unshelve(self): sot = server.Server(**EXAMPLE) @@ -618,7 +618,7 @@ def test_unshelve(self): body = {"unshelve": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_migrate(self): sot = server.Server(**EXAMPLE) @@ -631,7 +631,7 @@ def test_migrate(self): headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_get_console_output(self): sot = server.Server(**EXAMPLE) @@ -643,7 +643,7 @@ def test_get_console_output(self): body = {'os-getConsoleOutput': {}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) res = sot.get_console_output(self.sess, length=1) @@ -653,7 +653,7 @@ def test_get_console_output(self): headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) def test_live_migrate(self): sot = server.Server(**EXAMPLE) @@ -672,4 +672,4 @@ def test_live_migrate(self): headers = {'Accept': ''} self.sess.post.assert_called_with( - url, endpoint_filter=sot.service, json=body, headers=headers) + url, json=body, headers=headers) diff --git a/openstack/tests/unit/compute/v2/test_service.py b/openstack/tests/unit/compute/v2/test_service.py index 0872cf08e..3a1e4ca40 100644 --- a/openstack/tests/unit/compute/v2/test_service.py +++ b/openstack/tests/unit/compute/v2/test_service.py @@ -68,7 +68,7 @@ def test_force_down(self): 'forced_down': True, } self.sess.put.assert_called_with( - url, endpoint_filter=sot.service, json=body) + url, json=body) def test_enable(self): sot = service.Service(**EXAMPLE) @@ -82,7 +82,7 @@ def test_enable(self): 'host': 'host1', } self.sess.put.assert_called_with( - url, endpoint_filter=sot.service, json=body) + url, json=body) def test_disable(self): sot = service.Service(**EXAMPLE) @@ -96,7 +96,7 @@ def test_disable(self): 'host': 'host1', } self.sess.put.assert_called_with( - url, endpoint_filter=sot.service, json=body) + url, json=body) def test_disable_with_reason(self): sot = service.Service(**EXAMPLE) @@ -113,4 +113,4 @@ def test_disable_with_reason(self): 'disabled_reason': reason } self.sess.put.assert_called_with( - url, endpoint_filter=sot.service, json=body) + url, json=body) diff --git a/openstack/tests/unit/database/v1/test_instance.py b/openstack/tests/unit/database/v1/test_instance.py index a13bed7b3..116f63b7c 100644 --- a/openstack/tests/unit/database/v1/test_instance.py +++ b/openstack/tests/unit/database/v1/test_instance.py @@ -70,7 +70,7 @@ def test_enable_root_user(self): self.assertEqual(response.body['user'], sot.enable_root_user(sess)) url = ("instances/%s/root" % IDENTIFIER) - sess.post.assert_called_with(url, endpoint_filter=sot.service) + sess.post.assert_called_with(url,) def test_is_root_enabled(self): sot = instance.Instance(**EXAMPLE) @@ -83,7 +83,7 @@ def test_is_root_enabled(self): self.assertTrue(sot.is_root_enabled(sess)) url = ("instances/%s/root" % IDENTIFIER) - sess.get.assert_called_with(url, endpoint_filter=sot.service) + sess.get.assert_called_with(url,) def test_action_restart(self): sot = instance.Instance(**EXAMPLE) @@ -96,7 +96,7 @@ def test_action_restart(self): url = ("instances/%s/action" % IDENTIFIER) body = {'restart': {}} - sess.post.assert_called_with(url, endpoint_filter=sot.service, + sess.post.assert_called_with(url, json=body) def test_action_resize(self): @@ -111,7 +111,7 @@ def test_action_resize(self): url = ("instances/%s/action" % IDENTIFIER) body = {'resize': {'flavorRef': flavor}} - sess.post.assert_called_with(url, endpoint_filter=sot.service, + sess.post.assert_called_with(url, json=body) def test_action_resize_volume(self): @@ -126,5 +126,5 @@ def test_action_resize_volume(self): url = ("instances/%s/action" % IDENTIFIER) body = {'resize': {'volume': size}} - sess.post.assert_called_with(url, endpoint_filter=sot.service, + sess.post.assert_called_with(url, json=body) diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 2a3acf45f..5c0399f5b 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -13,6 +13,7 @@ import json import mock +import requests import testtools from openstack import exceptions @@ -79,6 +80,18 @@ } +class FakeResponse(object): + def __init__(self, response, status_code=200, headers=None): + self.body = response + self.content = response + self.status_code = status_code + headers = headers if headers else {'content-type': 'application/json'} + self.headers = requests.structures.CaseInsensitiveDict(headers) + + def json(self): + return self.body + + class TestImage(testtools.TestCase): def setUp(self): @@ -166,14 +179,14 @@ def test_deactivate(self): self.assertIsNone(sot.deactivate(self.sess)) self.sess.post.assert_called_with( 'images/IDENTIFIER/actions/deactivate', - endpoint_filter=sot.service) + ) def test_reactivate(self): sot = image.Image(**EXAMPLE) self.assertIsNone(sot.reactivate(self.sess)) self.sess.post.assert_called_with( 'images/IDENTIFIER/actions/reactivate', - endpoint_filter=sot.service) + ) def test_add_tag(self): sot = image.Image(**EXAMPLE) @@ -182,7 +195,7 @@ def test_add_tag(self): self.assertIsNone(sot.add_tag(self.sess, tag)) self.sess.put.assert_called_with( 'images/IDENTIFIER/tags/%s' % tag, - endpoint_filter=sot.service) + ) def test_remove_tag(self): sot = image.Image(**EXAMPLE) @@ -191,14 +204,13 @@ def test_remove_tag(self): self.assertIsNone(sot.remove_tag(self.sess, tag)) self.sess.delete.assert_called_with( 'images/IDENTIFIER/tags/%s' % tag, - endpoint_filter=sot.service) + ) def test_upload(self): sot = image.Image(**EXAMPLE) self.assertIsNone(sot.upload(self.sess)) self.sess.put.assert_called_with('images/IDENTIFIER/file', - endpoint_filter=sot.service, data=sot.data, headers={"Content-Type": "application/octet-stream", @@ -207,14 +219,14 @@ def test_upload(self): def test_download_checksum_match(self): sot = image.Image(**EXAMPLE) - resp = mock.Mock() - resp.content = b"abc" - resp.headers = {"Content-MD5": "900150983cd24fb0d6963f7d28e17f72"} + resp = FakeResponse( + b"abc", + headers={"Content-MD5": "900150983cd24fb0d6963f7d28e17f72", + "Content-Type": "application/octet-stream"}) self.sess.get.return_value = resp rv = sot.download(self.sess) self.sess.get.assert_called_with('images/IDENTIFIER/file', - endpoint_filter=sot.service, stream=False) self.assertEqual(rv, resp.content) @@ -222,9 +234,10 @@ def test_download_checksum_match(self): def test_download_checksum_mismatch(self): sot = image.Image(**EXAMPLE) - resp = mock.Mock() - resp.content = b"abc" - resp.headers = {"Content-MD5": "the wrong checksum"} + resp = FakeResponse( + b"abc", + headers={"Content-MD5": "the wrong checksum", + "Content-Type": "application/octet-stream"}) self.sess.get.return_value = resp self.assertRaises(exceptions.InvalidResponse, sot.download, self.sess) @@ -232,35 +245,29 @@ def test_download_checksum_mismatch(self): def test_download_no_checksum_header(self): sot = image.Image(**EXAMPLE) - resp1 = mock.Mock() - resp1.content = b"abc" - resp1.headers = {"no_checksum_here": ""} + resp1 = FakeResponse( + b"abc", headers={"Content-Type": "application/octet-stream"}) - resp2 = mock.Mock() - resp2.json = mock.Mock( - return_value={"checksum": "900150983cd24fb0d6963f7d28e17f72"}) - resp2.headers = {"": ""} + resp2 = FakeResponse( + {"checksum": "900150983cd24fb0d6963f7d28e17f72"}) self.sess.get.side_effect = [resp1, resp2] rv = sot.download(self.sess) self.sess.get.assert_has_calls( - [mock.call('images/IDENTIFIER/file', endpoint_filter=sot.service, + [mock.call('images/IDENTIFIER/file', stream=False), - mock.call('images/IDENTIFIER', endpoint_filter=sot.service)]) + mock.call('images/IDENTIFIER',)]) self.assertEqual(rv, resp1.content) def test_download_no_checksum_at_all2(self): sot = image.Image(**EXAMPLE) - resp1 = mock.Mock() - resp1.content = b"abc" - resp1.headers = {"no_checksum_here": ""} + resp1 = FakeResponse( + b"abc", headers={"Content-Type": "application/octet-stream"}) - resp2 = mock.Mock() - resp2.json = mock.Mock(return_value={"checksum": None}) - resp2.headers = {"": ""} + resp2 = FakeResponse({"checksum": None}) self.sess.get.side_effect = [resp1, resp2] @@ -274,24 +281,23 @@ def test_download_no_checksum_at_all2(self): log.records[0].msg) self.sess.get.assert_has_calls( - [mock.call('images/IDENTIFIER/file', endpoint_filter=sot.service, + [mock.call('images/IDENTIFIER/file', stream=False), - mock.call('images/IDENTIFIER', endpoint_filter=sot.service)]) + mock.call('images/IDENTIFIER',)]) self.assertEqual(rv, resp1.content) def test_download_stream(self): sot = image.Image(**EXAMPLE) - resp = mock.Mock() - resp.content = b"abc" - resp.headers = {"Content-MD5": "900150983cd24fb0d6963f7d28e17f72"} + resp = FakeResponse( + b"abc", + headers={"Content-MD5": "900150983cd24fb0d6963f7d28e17f72", + "Content-Type": "application/octet-stream"}) self.sess.get.return_value = resp rv = sot.download(self.sess, stream=True) - self.sess.get.assert_called_with('images/IDENTIFIER/file', - endpoint_filter=sot.service, - stream=True) + self.sess.get.assert_called_with('images/IDENTIFIER/file', stream=True) self.assertEqual(rv, resp) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 8bb060c77..0fafcd927 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -47,7 +47,7 @@ def test_image_create(self): container_format="x", disk_format="y", name="z") - created_image.upload.assert_called_with(self.session) + created_image.upload.assert_called_with(self.proxy) self.assertEqual(rv, created_image) def test_image_delete(self): diff --git a/openstack/tests/unit/key_manager/v1/test_secret.py b/openstack/tests/unit/key_manager/v1/test_secret.py index 9e103ecbf..31814bee8 100644 --- a/openstack/tests/unit/key_manager/v1/test_secret.py +++ b/openstack/tests/unit/key_manager/v1/test_secret.py @@ -94,8 +94,7 @@ def test_get_no_payload(self): sot.get(sess) - sess.get.assert_called_once_with("secrets/id", - endpoint_filter=sot.service) + sess.get.assert_called_once_with("secrets/id") def _test_payload(self, sot, metadata, content_type): content_type = "some/type" @@ -114,8 +113,8 @@ def _test_payload(self, sot, metadata, content_type): rv = sot.get(sess) sess.get.assert_has_calls( - [mock.call("secrets/id", endpoint_filter=sot.service), - mock.call("secrets/id/payload", endpoint_filter=sot.service, + [mock.call("secrets/id",), + mock.call("secrets/id/payload", headers={"Accept": content_type})]) self.assertEqual(rv.payload, payload) diff --git a/openstack/tests/unit/message/v1/test_claim.py b/openstack/tests/unit/message/v1/test_claim.py index cbfeb9f9c..f6490a3d0 100644 --- a/openstack/tests/unit/message/v1/test_claim.py +++ b/openstack/tests/unit/message/v1/test_claim.py @@ -63,7 +63,7 @@ def test_create(self): url = '/queues/%s/claims' % QUEUE sess.post.assert_called_with( - url, endpoint_filter=sot.service, + url, headers={'Client-ID': CLIENT}, params=None, data=json.dumps(FAKE, cls=claim.ClaimEncoder)) diff --git a/openstack/tests/unit/message/v1/test_message.py b/openstack/tests/unit/message/v1/test_message.py index 6fe5d26ad..e3cd59a7f 100644 --- a/openstack/tests/unit/message/v1/test_message.py +++ b/openstack/tests/unit/message/v1/test_message.py @@ -60,7 +60,7 @@ def test_create(self): url = '/queues/%s/messages' % QUEUE sess.post.assert_called_with( - url, endpoint_filter=sot.service, + url, headers={'Client-ID': CLIENT}, data=mock.ANY) @@ -81,5 +81,5 @@ def test_delete(self): url = '/queues/%s/messages/1234' % QUEUE sess.delete.assert_called_with( - url, endpoint_filter=sot.service, + url, headers={'Client-ID': CLIENT, 'Accept': ''}) diff --git a/openstack/tests/unit/message/v1/test_proxy.py b/openstack/tests/unit/message/v1/test_proxy.py index 8436f024f..6f3c4a5ee 100644 --- a/openstack/tests/unit/message/v1/test_proxy.py +++ b/openstack/tests/unit/message/v1/test_proxy.py @@ -39,17 +39,17 @@ def test_messages_create(self): self.proxy.create_messages, expected_result="result", method_args=[[]], - expected_args=[self.session, []]) + expected_args=[self.proxy, []]) def test_messages_claim(self): self._verify2("openstack.message.v1.claim.Claim.claim_messages", self.proxy.claim_messages, expected_result="result", method_args=[claim.Claim], - expected_args=[self.session, claim.Claim]) + expected_args=[self.proxy, claim.Claim]) def test_message_delete(self): self._verify2("openstack.message.v1.message.Message.delete_by_id", self.proxy.delete_message, method_args=[message.Message], - expected_args=[self.session, message.Message]) + expected_args=[self.proxy, message.Message]) diff --git a/openstack/tests/unit/message/v1/test_queue.py b/openstack/tests/unit/message/v1/test_queue.py index 81b07df7f..0e62b0fec 100644 --- a/openstack/tests/unit/message/v1/test_queue.py +++ b/openstack/tests/unit/message/v1/test_queue.py @@ -49,7 +49,7 @@ def test_create(self): url = 'queues/%s' % FAKE_NAME headers = {'Accept': ''} - sess.put.assert_called_with(url, endpoint_filter=sot.service, + sess.put.assert_called_with(url, headers=headers) self.assertEqual(FAKE_NAME, sot.id) self.assertEqual(FAKE_NAME, sot.name) diff --git a/openstack/tests/unit/message/v2/test_claim.py b/openstack/tests/unit/message/v2/test_claim.py index 945829148..649f3ca51 100644 --- a/openstack/tests/unit/message/v2/test_claim.py +++ b/openstack/tests/unit/message/v2/test_claim.py @@ -81,7 +81,7 @@ def test_create_204_resp(self, mock_uuid): url = "/queues/%(queue)s/claims" % {"queue": FAKE.pop("queue_name")} headers = {"Client-ID": "NEW_CLIENT_ID", "X-PROJECT-ID": "NEW_PROJECT_ID"} - sess.post.assert_called_once_with(url, endpoint_filter=sot.service, + sess.post.assert_called_once_with(url, headers=headers, json=FAKE) sess.get_project_id.assert_called_once_with() self.assertEqual(sot, res) @@ -103,7 +103,7 @@ def test_create_non_204_resp(self, mock_uuid): url = "/queues/%(queue)s/claims" % {"queue": FAKE.pop("queue_name")} headers = {"Client-ID": "NEW_CLIENT_ID", "X-PROJECT-ID": "NEW_PROJECT_ID"} - sess.post.assert_called_once_with(url, endpoint_filter=sot.service, + sess.post.assert_called_once_with(url, headers=headers, json=FAKE) sess.get_project_id.assert_called_once_with() self.assertEqual(sot, res) @@ -123,7 +123,7 @@ def test_create_client_id_project_id_exist(self): url = "/queues/%(queue)s/claims" % {"queue": FAKE.pop("queue_name")} headers = {"Client-ID": FAKE.pop("client_id"), "X-PROJECT-ID": FAKE.pop("project_id")} - sess.post.assert_called_once_with(url, endpoint_filter=sot.service, + sess.post.assert_called_once_with(url, headers=headers, json=FAKE) self.assertEqual(sot, res) @@ -143,7 +143,7 @@ def test_get(self, mock_uuid): "queue": FAKE1["queue_name"], "claim": FAKE1["id"]} headers = {"Client-ID": "NEW_CLIENT_ID", "X-PROJECT-ID": "NEW_PROJECT_ID"} - sess.get.assert_called_with(url, endpoint_filter=sot.service, + sess.get.assert_called_with(url, headers=headers) sess.get_project_id.assert_called_once_with() sot._translate_response.assert_called_once_with(resp) @@ -162,7 +162,7 @@ def test_get_client_id_project_id_exist(self): "queue": FAKE2["queue_name"], "claim": FAKE2["id"]} headers = {"Client-ID": "OLD_CLIENT_ID", "X-PROJECT-ID": "OLD_PROJECT_ID"} - sess.get.assert_called_with(url, endpoint_filter=sot.service, + sess.get.assert_called_with(url, headers=headers) sot._translate_response.assert_called_once_with(resp) self.assertEqual(sot, res) @@ -183,7 +183,7 @@ def test_update(self, mock_uuid): "queue": FAKE.pop("queue_name"), "claim": FAKE["id"]} headers = {"Client-ID": "NEW_CLIENT_ID", "X-PROJECT-ID": "NEW_PROJECT_ID"} - sess.patch.assert_called_with(url, endpoint_filter=sot.service, + sess.patch.assert_called_with(url, headers=headers, json=FAKE) sess.get_project_id.assert_called_once_with() self.assertEqual(sot, res) @@ -201,7 +201,7 @@ def test_update_client_id_project_id_exist(self): "queue": FAKE.pop("queue_name"), "claim": FAKE["id"]} headers = {"Client-ID": FAKE.pop("client_id"), "X-PROJECT-ID": FAKE.pop("project_id")} - sess.patch.assert_called_with(url, endpoint_filter=sot.service, + sess.patch.assert_called_with(url, headers=headers, json=FAKE) self.assertEqual(sot, res) @@ -221,7 +221,7 @@ def test_delete(self, mock_uuid): "queue": FAKE1["queue_name"], "claim": FAKE1["id"]} headers = {"Client-ID": "NEW_CLIENT_ID", "X-PROJECT-ID": "NEW_PROJECT_ID"} - sess.delete.assert_called_with(url, endpoint_filter=sot.service, + sess.delete.assert_called_with(url, headers=headers) sess.get_project_id.assert_called_once_with() sot._translate_response.assert_called_once_with(resp, has_body=False) @@ -239,6 +239,6 @@ def test_delete_client_id_project_id_exist(self): "queue": FAKE2["queue_name"], "claim": FAKE2["id"]} headers = {"Client-ID": "OLD_CLIENT_ID", "X-PROJECT-ID": "OLD_PROJECT_ID"} - sess.delete.assert_called_with(url, endpoint_filter=sot.service, + sess.delete.assert_called_with(url, headers=headers) sot._translate_response.assert_called_once_with(resp, has_body=False) diff --git a/openstack/tests/unit/message/v2/test_message.py b/openstack/tests/unit/message/v2/test_message.py index d7541f5bc..9a095b1ef 100644 --- a/openstack/tests/unit/message/v2/test_message.py +++ b/openstack/tests/unit/message/v2/test_message.py @@ -98,7 +98,7 @@ def test_post(self, mock_uuid): url = '/queues/%(queue)s/messages' % {'queue': FAKE1['queue_name']} headers = {'Client-ID': 'NEW_CLIENT_ID', 'X-PROJECT-ID': 'NEW_PROJECT_ID'} - sess.post.assert_called_once_with(url, endpoint_filter=sot.service, + sess.post.assert_called_once_with(url, headers=headers, json={'messages': messages}) sess.get_project_id.assert_called_once_with() @@ -131,7 +131,7 @@ def test_post_client_id_project_id_exist(self): url = '/queues/%(queue)s/messages' % {'queue': FAKE2['queue_name']} headers = {'Client-ID': 'OLD_CLIENT_ID', 'X-PROJECT-ID': 'OLD_PROJECT_ID'} - sess.post.assert_called_once_with(url, endpoint_filter=sot.service, + sess.post.assert_called_once_with(url, headers=headers, json={'messages': messages}) resp.json.assert_called_once_with() @@ -153,7 +153,7 @@ def test_get(self, mock_uuid): 'queue': FAKE1['queue_name'], 'message': FAKE1['id']} headers = {'Client-ID': 'NEW_CLIENT_ID', 'X-PROJECT-ID': 'NEW_PROJECT_ID'} - sess.get.assert_called_with(url, endpoint_filter=sot.service, + sess.get.assert_called_with(url, headers=headers) sess.get_project_id.assert_called_once_with() sot._translate_response.assert_called_once_with(resp) @@ -175,7 +175,7 @@ def test_get_client_id_project_id_exist(self): res = sot.get(sess) headers = {'Client-ID': 'OLD_CLIENT_ID', 'X-PROJECT-ID': 'OLD_PROJECT_ID'} - sess.get.assert_called_with(url, endpoint_filter=sot.service, + sess.get.assert_called_with(url, headers=headers) sot._translate_response.assert_called_once_with(resp) self.assertEqual(sot, res) @@ -197,7 +197,7 @@ def test_delete_unclaimed(self, mock_uuid): 'queue': FAKE1['queue_name'], 'message': FAKE1['id']} headers = {'Client-ID': 'NEW_CLIENT_ID', 'X-PROJECT-ID': 'NEW_PROJECT_ID'} - sess.delete.assert_called_with(url, endpoint_filter=sot.service, + sess.delete.assert_called_with(url, headers=headers) sess.get_project_id.assert_called_once_with() sot._translate_response.assert_called_once_with(resp, has_body=False) @@ -220,7 +220,7 @@ def test_delete_claimed(self, mock_uuid): 'cid': 'CLAIM_ID'} headers = {'Client-ID': 'NEW_CLIENT_ID', 'X-PROJECT-ID': 'NEW_PROJECT_ID'} - sess.delete.assert_called_with(url, endpoint_filter=sot.service, + sess.delete.assert_called_with(url, headers=headers) sess.get_project_id.assert_called_once_with() sot._translate_response.assert_called_once_with(resp, has_body=False) @@ -239,6 +239,6 @@ def test_delete_client_id_project_id_exist(self): 'queue': FAKE2['queue_name'], 'message': FAKE2['id']} headers = {'Client-ID': 'OLD_CLIENT_ID', 'X-PROJECT-ID': 'OLD_PROJECT_ID'} - sess.delete.assert_called_with(url, endpoint_filter=sot.service, + sess.delete.assert_called_with(url, headers=headers) sot._translate_response.assert_called_once_with(resp, has_body=False) diff --git a/openstack/tests/unit/message/v2/test_queue.py b/openstack/tests/unit/message/v2/test_queue.py index 566510c67..83acc66b4 100644 --- a/openstack/tests/unit/message/v2/test_queue.py +++ b/openstack/tests/unit/message/v2/test_queue.py @@ -73,7 +73,7 @@ def test_create(self, mock_uuid): url = 'queues/%s' % FAKE1['name'] headers = {'Client-ID': 'NEW_CLIENT_ID', 'X-PROJECT-ID': 'NEW_PROJECT_ID'} - sess.put.assert_called_with(url, endpoint_filter=sot.service, + sess.put.assert_called_with(url, headers=headers, json=FAKE1) sess.get_project_id.assert_called_once_with() sot._translate_response.assert_called_once_with(resp, has_body=False) @@ -91,7 +91,7 @@ def test_create_client_id_project_id_exist(self): url = 'queues/%s' % FAKE2['name'] headers = {'Client-ID': 'OLD_CLIENT_ID', 'X-PROJECT-ID': 'OLD_PROJECT_ID'} - sess.put.assert_called_with(url, endpoint_filter=sot.service, + sess.put.assert_called_with(url, headers=headers, json=FAKE1) sot._translate_response.assert_called_once_with(resp, has_body=False) self.assertEqual(sot, res) @@ -111,7 +111,7 @@ def test_get(self, mock_uuid): url = 'queues/%s' % FAKE1['name'] headers = {'Client-ID': 'NEW_CLIENT_ID', 'X-PROJECT-ID': 'NEW_PROJECT_ID'} - sess.get.assert_called_with(url, endpoint_filter=sot.service, + sess.get.assert_called_with(url, headers=headers) sess.get_project_id.assert_called_once_with() sot._translate_response.assert_called_once_with(resp) @@ -129,7 +129,7 @@ def test_get_client_id_project_id_exist(self): url = 'queues/%s' % FAKE2['name'] headers = {'Client-ID': 'OLD_CLIENT_ID', 'X-PROJECT-ID': 'OLD_PROJECT_ID'} - sess.get.assert_called_with(url, endpoint_filter=sot.service, + sess.get.assert_called_with(url, headers=headers) sot._translate_response.assert_called_once_with(resp) self.assertEqual(sot, res) @@ -149,7 +149,7 @@ def test_delete(self, mock_uuid): url = 'queues/%s' % FAKE1['name'] headers = {'Client-ID': 'NEW_CLIENT_ID', 'X-PROJECT-ID': 'NEW_PROJECT_ID'} - sess.delete.assert_called_with(url, endpoint_filter=sot.service, + sess.delete.assert_called_with(url, headers=headers) sess.get_project_id.assert_called_once_with() sot._translate_response.assert_called_once_with(resp, has_body=False) @@ -166,6 +166,6 @@ def test_delete_client_id_project_id_exist(self): url = 'queues/%s' % FAKE2['name'] headers = {'Client-ID': 'OLD_CLIENT_ID', 'X-PROJECT-ID': 'OLD_PROJECT_ID'} - sess.delete.assert_called_with(url, endpoint_filter=sot.service, + sess.delete.assert_called_with(url, headers=headers) sot._translate_response.assert_called_once_with(resp, has_body=False) diff --git a/openstack/tests/unit/message/v2/test_subscription.py b/openstack/tests/unit/message/v2/test_subscription.py index 47c165fc9..9c9c9e510 100644 --- a/openstack/tests/unit/message/v2/test_subscription.py +++ b/openstack/tests/unit/message/v2/test_subscription.py @@ -89,7 +89,7 @@ def test_create(self, mock_uuid): "queue": FAKE.pop("queue_name")} headers = {"Client-ID": "NEW_CLIENT_ID", "X-PROJECT-ID": "NEW_PROJECT_ID"} - sess.post.assert_called_once_with(url, endpoint_filter=sot.service, + sess.post.assert_called_once_with(url, headers=headers, json=FAKE) sess.get_project_id.assert_called_once_with() self.assertEqual(sot, res) @@ -108,7 +108,7 @@ def test_create_client_id_project_id_exist(self): "queue": FAKE.pop("queue_name")} headers = {"Client-ID": FAKE.pop("client_id"), "X-PROJECT-ID": FAKE.pop("project_id")} - sess.post.assert_called_once_with(url, endpoint_filter=sot.service, + sess.post.assert_called_once_with(url, headers=headers, json=FAKE) self.assertEqual(sot, res) @@ -128,7 +128,7 @@ def test_get(self, mock_uuid): "queue": FAKE1["queue_name"], "subscription": FAKE1["id"]} headers = {"Client-ID": "NEW_CLIENT_ID", "X-PROJECT-ID": "NEW_PROJECT_ID"} - sess.get.assert_called_with(url, endpoint_filter=sot.service, + sess.get.assert_called_with(url, headers=headers) sess.get_project_id.assert_called_once_with() sot._translate_response.assert_called_once_with(resp) @@ -147,7 +147,7 @@ def test_get_client_id_project_id_exist(self): "queue": FAKE2["queue_name"], "subscription": FAKE2["id"]} headers = {"Client-ID": "OLD_CLIENT_ID", "X-PROJECT-ID": "OLD_PROJECT_ID"} - sess.get.assert_called_with(url, endpoint_filter=sot.service, + sess.get.assert_called_with(url, headers=headers) sot._translate_response.assert_called_once_with(resp) self.assertEqual(sot, res) @@ -168,7 +168,7 @@ def test_delete(self, mock_uuid): "queue": FAKE1["queue_name"], "subscription": FAKE1["id"]} headers = {"Client-ID": "NEW_CLIENT_ID", "X-PROJECT-ID": "NEW_PROJECT_ID"} - sess.delete.assert_called_with(url, endpoint_filter=sot.service, + sess.delete.assert_called_with(url, headers=headers) sess.get_project_id.assert_called_once_with() sot._translate_response.assert_called_once_with(resp, has_body=False) @@ -186,6 +186,6 @@ def test_delete_client_id_project_id_exist(self): "queue": FAKE2["queue_name"], "subscription": FAKE2["id"]} headers = {"Client-ID": "OLD_CLIENT_ID", "X-PROJECT-ID": "OLD_PROJECT_ID"} - sess.delete.assert_called_with(url, endpoint_filter=sot.service, + sess.delete.assert_called_with(url, headers=headers) sot._translate_response.assert_called_once_with(resp, has_body=False) diff --git a/openstack/tests/unit/telemetry/__init__.py b/openstack/tests/unit/meter/__init__.py similarity index 100% rename from openstack/tests/unit/telemetry/__init__.py rename to openstack/tests/unit/meter/__init__.py diff --git a/openstack/tests/unit/telemetry/alarm/__init__.py b/openstack/tests/unit/meter/alarm/__init__.py similarity index 100% rename from openstack/tests/unit/telemetry/alarm/__init__.py rename to openstack/tests/unit/meter/alarm/__init__.py diff --git a/openstack/tests/unit/telemetry/alarm/test_alarm_service.py b/openstack/tests/unit/meter/alarm/test_alarm_service.py similarity index 95% rename from openstack/tests/unit/telemetry/alarm/test_alarm_service.py rename to openstack/tests/unit/meter/alarm/test_alarm_service.py index 8106e961f..3f6ffea85 100644 --- a/openstack/tests/unit/telemetry/alarm/test_alarm_service.py +++ b/openstack/tests/unit/meter/alarm/test_alarm_service.py @@ -12,7 +12,7 @@ import testtools -from openstack.telemetry.alarm import alarm_service +from openstack.meter.alarm import alarm_service class TestAlarmService(testtools.TestCase): diff --git a/openstack/tests/unit/telemetry/alarm/v2/__init__.py b/openstack/tests/unit/meter/alarm/v2/__init__.py similarity index 100% rename from openstack/tests/unit/telemetry/alarm/v2/__init__.py rename to openstack/tests/unit/meter/alarm/v2/__init__.py diff --git a/openstack/tests/unit/telemetry/alarm/v2/test_alarm.py b/openstack/tests/unit/meter/alarm/v2/test_alarm.py similarity index 95% rename from openstack/tests/unit/telemetry/alarm/v2/test_alarm.py rename to openstack/tests/unit/meter/alarm/v2/test_alarm.py index cd0cb1af5..ea35e4a92 100644 --- a/openstack/tests/unit/telemetry/alarm/v2/test_alarm.py +++ b/openstack/tests/unit/meter/alarm/v2/test_alarm.py @@ -13,7 +13,7 @@ import mock import testtools -from openstack.telemetry.alarm.v2 import alarm +from openstack.meter.alarm.v2 import alarm IDENTIFIER = 'IDENTIFIER' EXAMPLE = { @@ -96,12 +96,12 @@ def test_check_status(self): sot.check_state(self.sess) url = 'alarms/IDENTIFIER/state' - self.sess.get.assert_called_with(url, endpoint_filter=sot.service) + self.sess.get.assert_called_with(url,) def test_change_status(self): sot = alarm.Alarm(EXAMPLE) self.assertEqual(self.resp.body, sot.change_state(self.sess, 'alarm')) url = 'alarms/IDENTIFIER/state' - self.sess.put.assert_called_with(url, endpoint_filter=sot.service, + self.sess.put.assert_called_with(url, json='alarm') diff --git a/openstack/tests/unit/telemetry/alarm/v2/test_alarm_change.py b/openstack/tests/unit/meter/alarm/v2/test_alarm_change.py similarity index 98% rename from openstack/tests/unit/telemetry/alarm/v2/test_alarm_change.py rename to openstack/tests/unit/meter/alarm/v2/test_alarm_change.py index bd272440e..84326f4cb 100644 --- a/openstack/tests/unit/telemetry/alarm/v2/test_alarm_change.py +++ b/openstack/tests/unit/meter/alarm/v2/test_alarm_change.py @@ -13,7 +13,7 @@ import mock import testtools -from openstack.telemetry.alarm.v2 import alarm_change +from openstack.meter.alarm.v2 import alarm_change IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/telemetry/alarm/v2/test_proxy.py b/openstack/tests/unit/meter/alarm/v2/test_proxy.py similarity index 92% rename from openstack/tests/unit/telemetry/alarm/v2/test_proxy.py rename to openstack/tests/unit/meter/alarm/v2/test_proxy.py index 343db8916..fc839e52e 100644 --- a/openstack/tests/unit/telemetry/alarm/v2/test_proxy.py +++ b/openstack/tests/unit/meter/alarm/v2/test_proxy.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.telemetry.alarm.v2 import _proxy -from openstack.telemetry.alarm.v2 import alarm -from openstack.telemetry.alarm.v2 import alarm_change +from openstack.meter.alarm.v2 import _proxy +from openstack.meter.alarm.v2 import alarm +from openstack.meter.alarm.v2 import alarm_change from openstack.tests.unit import test_proxy_base diff --git a/openstack/tests/unit/telemetry/test_telemetry_service.py b/openstack/tests/unit/meter/test_meter_service.py similarity index 86% rename from openstack/tests/unit/telemetry/test_telemetry_service.py rename to openstack/tests/unit/meter/test_meter_service.py index acc7da94c..330c6f1d0 100644 --- a/openstack/tests/unit/telemetry/test_telemetry_service.py +++ b/openstack/tests/unit/meter/test_meter_service.py @@ -12,13 +12,13 @@ import testtools -from openstack.telemetry import telemetry_service +from openstack.meter import meter_service -class TestTelemetryService(testtools.TestCase): +class TestMeterService(testtools.TestCase): def test_service(self): - sot = telemetry_service.TelemetryService() + sot = meter_service.MeterService() self.assertEqual('metering', sot.service_type) self.assertEqual('public', sot.interface) self.assertIsNone(sot.region) diff --git a/openstack/tests/unit/telemetry/v2/__init__.py b/openstack/tests/unit/meter/v2/__init__.py similarity index 100% rename from openstack/tests/unit/telemetry/v2/__init__.py rename to openstack/tests/unit/meter/v2/__init__.py diff --git a/openstack/tests/unit/telemetry/v2/test_capability.py b/openstack/tests/unit/meter/v2/test_capability.py similarity index 98% rename from openstack/tests/unit/telemetry/v2/test_capability.py rename to openstack/tests/unit/meter/v2/test_capability.py index 8d37c8ae6..0cde8b56c 100644 --- a/openstack/tests/unit/telemetry/v2/test_capability.py +++ b/openstack/tests/unit/meter/v2/test_capability.py @@ -13,7 +13,7 @@ import mock import testtools -from openstack.telemetry.v2 import capability +from openstack.meter.v2 import capability EXAMPLE = { "id": "123", diff --git a/openstack/tests/unit/telemetry/v2/test_meter.py b/openstack/tests/unit/meter/v2/test_meter.py similarity index 97% rename from openstack/tests/unit/telemetry/v2/test_meter.py rename to openstack/tests/unit/meter/v2/test_meter.py index 83cdb8943..dced7f809 100644 --- a/openstack/tests/unit/telemetry/v2/test_meter.py +++ b/openstack/tests/unit/meter/v2/test_meter.py @@ -12,7 +12,7 @@ import testtools -from openstack.telemetry.v2 import meter +from openstack.meter.v2 import meter IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/telemetry/v2/test_proxy.py b/openstack/tests/unit/meter/v2/test_proxy.py similarity index 85% rename from openstack/tests/unit/telemetry/v2/test_proxy.py rename to openstack/tests/unit/meter/v2/test_proxy.py index 974dd8f01..76883e973 100644 --- a/openstack/tests/unit/telemetry/v2/test_proxy.py +++ b/openstack/tests/unit/meter/v2/test_proxy.py @@ -10,18 +10,18 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.telemetry.v2 import _proxy -from openstack.telemetry.v2 import capability -from openstack.telemetry.v2 import meter -from openstack.telemetry.v2 import resource -from openstack.telemetry.v2 import sample -from openstack.telemetry.v2 import statistics +from openstack.meter.v2 import _proxy +from openstack.meter.v2 import capability +from openstack.meter.v2 import meter +from openstack.meter.v2 import resource +from openstack.meter.v2 import sample +from openstack.meter.v2 import statistics from openstack.tests.unit import test_proxy_base2 -class TestTelemetryProxy(test_proxy_base2.TestProxyBase): +class TestMeterProxy(test_proxy_base2.TestProxyBase): def setUp(self): - super(TestTelemetryProxy, self).setUp() + super(TestMeterProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) def test_capability_find(self): diff --git a/openstack/tests/unit/telemetry/v2/test_resource.py b/openstack/tests/unit/meter/v2/test_resource.py similarity index 98% rename from openstack/tests/unit/telemetry/v2/test_resource.py rename to openstack/tests/unit/meter/v2/test_resource.py index 62051830c..78bb937fa 100644 --- a/openstack/tests/unit/telemetry/v2/test_resource.py +++ b/openstack/tests/unit/meter/v2/test_resource.py @@ -12,7 +12,7 @@ import testtools -from openstack.telemetry.v2 import resource +from openstack.meter.v2 import resource IDENTIFIER = 'IDENTIFIER' LINKS = [{'href': 'first_uri', 'rel': 'label 1', }, diff --git a/openstack/tests/unit/telemetry/v2/test_sample.py b/openstack/tests/unit/meter/v2/test_sample.py similarity index 98% rename from openstack/tests/unit/telemetry/v2/test_sample.py rename to openstack/tests/unit/meter/v2/test_sample.py index 9c31427d9..ee2588512 100644 --- a/openstack/tests/unit/telemetry/v2/test_sample.py +++ b/openstack/tests/unit/meter/v2/test_sample.py @@ -13,7 +13,7 @@ import mock import testtools -from openstack.telemetry.v2 import sample +from openstack.meter.v2 import sample SAMPLE = { 'sample_id': '0', diff --git a/openstack/tests/unit/telemetry/v2/test_statistics.py b/openstack/tests/unit/meter/v2/test_statistics.py similarity index 96% rename from openstack/tests/unit/telemetry/v2/test_statistics.py rename to openstack/tests/unit/meter/v2/test_statistics.py index 71ea61b3e..8fd46f573 100644 --- a/openstack/tests/unit/telemetry/v2/test_statistics.py +++ b/openstack/tests/unit/meter/v2/test_statistics.py @@ -13,7 +13,7 @@ import mock import testtools -from openstack.telemetry.v2 import statistics +from openstack.meter.v2 import statistics EXAMPLE = { 'aggregate': '1', @@ -74,7 +74,7 @@ def test_list(self): url = '/meters/example/statistics' stat = next(reply) - sess.get.assert_called_with(url, endpoint_filter=stat.service, + sess.get.assert_called_with(url, params={}) self.assertEqual(EXAMPLE['aggregate'], stat.aggregate) self.assertEqual(EXAMPLE['avg'], stat.avg) diff --git a/openstack/tests/unit/network/v2/test_agent.py b/openstack/tests/unit/network/v2/test_agent.py index 6eddcb27e..80bbbbd47 100644 --- a/openstack/tests/unit/network/v2/test_agent.py +++ b/openstack/tests/unit/network/v2/test_agent.py @@ -78,7 +78,7 @@ def test_add_agent_to_network(self): self.assertEqual(response.body, net.add_agent_to_network(sess, **body)) url = 'agents/IDENTIFIER/dhcp-networks' - sess.post.assert_called_with(url, endpoint_filter=net.service, + sess.post.assert_called_with(url, json=body) def test_remove_agent_from_network(self): @@ -90,7 +90,7 @@ def test_remove_agent_from_network(self): body = {'network_id': {}} sess.delete.assert_called_with('agents/IDENTIFIER/dhcp-networks/', - endpoint_filter=net.service, json=body) + json=body) def test_add_router_to_agent(self): # Add router to agent @@ -105,7 +105,7 @@ def test_add_router_to_agent(self): sot.add_router_to_agent(sess, router_id)) body = {'router_id': router_id} url = 'agents/IDENTIFIER/l3-routers' - sess.post.assert_called_with(url, endpoint_filter=sot.service, + sess.post.assert_called_with(url, json=body) def test_remove_router_from_agent(self): @@ -117,7 +117,7 @@ def test_remove_router_from_agent(self): body = {'router_id': {}} sess.delete.assert_called_with('agents/IDENTIFIER/l3-routers/', - endpoint_filter=sot.service, json=body) + json=body) class TestNetworkHostingDHCPAgent(testtools.TestCase): diff --git a/openstack/tests/unit/network/v2/test_flavor.py b/openstack/tests/unit/network/v2/test_flavor.py index 9bf70d5a9..236511f20 100644 --- a/openstack/tests/unit/network/v2/test_flavor.py +++ b/openstack/tests/unit/network/v2/test_flavor.py @@ -76,7 +76,7 @@ def test_associate_flavor_with_service_profile(self): sess, '1')) url = 'flavors/IDENTIFIER/service_profiles' - sess.post.assert_called_with(url, endpoint_filter=flav.service, + sess.post.assert_called_with(url, json=response.body) def test_disassociate_flavor_from_service_profile(self): @@ -91,4 +91,4 @@ def test_disassociate_flavor_from_service_profile(self): sess, '1')) url = 'flavors/IDENTIFIER/service_profiles/1' - sess.delete.assert_called_with(url, endpoint_filter=flav.service) + sess.delete.assert_called_with(url,) diff --git a/openstack/tests/unit/network/v2/test_floating_ip.py b/openstack/tests/unit/network/v2/test_floating_ip.py index 4673233a0..a2448ac68 100644 --- a/openstack/tests/unit/network/v2/test_floating_ip.py +++ b/openstack/tests/unit/network/v2/test_floating_ip.py @@ -77,7 +77,6 @@ def test_find_available(self): self.assertEqual('one', result.id) mock_session.get.assert_called_with( floating_ip.FloatingIP.base_path, - endpoint_filter=floating_ip.FloatingIP.service, headers={'Accept': 'application/json'}, params={'port_id': ''}) diff --git a/openstack/tests/unit/network/v2/test_router.py b/openstack/tests/unit/network/v2/test_router.py index 5fb87f16c..8cf9359ca 100644 --- a/openstack/tests/unit/network/v2/test_router.py +++ b/openstack/tests/unit/network/v2/test_router.py @@ -125,7 +125,7 @@ def test_add_interface_subnet(self): self.assertEqual(response.body, sot.add_interface(sess, **body)) url = 'routers/IDENTIFIER/add_router_interface' - sess.put.assert_called_with(url, endpoint_filter=sot.service, + sess.put.assert_called_with(url, json=body) def test_add_interface_port(self): @@ -141,7 +141,7 @@ def test_add_interface_port(self): self.assertEqual(response.body, sot.add_interface(sess, **body)) url = 'routers/IDENTIFIER/add_router_interface' - sess.put.assert_called_with(url, endpoint_filter=sot.service, + sess.put.assert_called_with(url, json=body) def test_remove_interface_subnet(self): @@ -156,7 +156,7 @@ def test_remove_interface_subnet(self): self.assertEqual(response.body, sot.remove_interface(sess, **body)) url = 'routers/IDENTIFIER/remove_router_interface' - sess.put.assert_called_with(url, endpoint_filter=sot.service, + sess.put.assert_called_with(url, json=body) def test_remove_interface_port(self): @@ -171,7 +171,7 @@ def test_remove_interface_port(self): self.assertEqual(response.body, sot.remove_interface(sess, **body)) url = 'routers/IDENTIFIER/remove_router_interface' - sess.put.assert_called_with(url, endpoint_filter=sot.service, + sess.put.assert_called_with(url, json=body) def test_add_router_gateway(self): @@ -186,7 +186,7 @@ def test_add_router_gateway(self): self.assertEqual(response.body, sot.add_gateway(sess, **body)) url = 'routers/IDENTIFIER/add_gateway_router' - sess.put.assert_called_with(url, endpoint_filter=sot.service, + sess.put.assert_called_with(url, json=body) def test_remove_router_gateway(self): @@ -201,7 +201,7 @@ def test_remove_router_gateway(self): self.assertEqual(response.body, sot.remove_gateway(sess, **body)) url = 'routers/IDENTIFIER/remove_gateway_router' - sess.put.assert_called_with(url, endpoint_filter=sot.service, + sess.put.assert_called_with(url, json=body) diff --git a/openstack/tests/unit/network/v2/test_tag.py b/openstack/tests/unit/network/v2/test_tag.py index b22ae87dc..102247457 100644 --- a/openstack/tests/unit/network/v2/test_tag.py +++ b/openstack/tests/unit/network/v2/test_tag.py @@ -40,5 +40,5 @@ def test_set_tags(self): # Check the passed resource is returned self.assertEqual(net, result) url = 'networks/' + ID + '/tags' - sess.put.assert_called_once_with(url, endpoint_filter=net.service, + sess.put.assert_called_once_with(url, json={'tags': ['blue', 'green']}) diff --git a/openstack/tests/unit/object_store/v1/test_container.py b/openstack/tests/unit/object_store/v1/test_container.py index 587c7e470..c18c49421 100644 --- a/openstack/tests/unit/object_store/v1/test_container.py +++ b/openstack/tests/unit/object_store/v1/test_container.py @@ -143,7 +143,7 @@ def _test_create_update(self, sot, sot_call, sess_method): sot_call(self.sess) url = "/%s" % CONTAINER_NAME - sess_method.assert_called_with(url, endpoint_filter=sot.service, + sess_method.assert_called_with(url, headers=headers) def test_create(self): @@ -159,7 +159,7 @@ def _test_no_headers(self, sot, sot_call, sess_method): sot.create(self.sess) url = "/%s" % CONTAINER_NAME headers = {'Accept': ''} - self.sess.put.assert_called_with(url, endpoint_filter=sot.service, + self.sess.put.assert_called_with(url, headers=headers) def test_create_no_headers(self): diff --git a/openstack/tests/unit/object_store/v1/test_obj.py b/openstack/tests/unit/object_store/v1/test_obj.py index 47a3b95ad..f3dfb793f 100644 --- a/openstack/tests/unit/object_store/v1/test_obj.py +++ b/openstack/tests/unit/object_store/v1/test_obj.py @@ -113,8 +113,9 @@ def test_get(self): # "if-match": {"who": "what"} # } headers = {'Accept': 'bytes'} - self.sess.get.assert_called_with(url, endpoint_filter=sot.service, - headers=headers) + self.sess.get.assert_called_with(url, + headers=headers, + error_message=None) self.assertEqual(self.resp.content, rv) def _test_create(self, method, data, accept): @@ -126,7 +127,7 @@ def _test_create(self, method, data, accept): rv = sot.create(self.sess) url = "%s/%s" % (CONTAINER_NAME, OBJECT_NAME) - method.assert_called_with(url, endpoint_filter=sot.service, data=data, + method.assert_called_with(url, data=data, headers=headers) self.assertEqual(self.resp.headers, rv.get_headers()) diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index 9628198a7..3289f5656 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -64,7 +64,7 @@ def test_check_stack_with_stack_object(self, mock_check): res = self.proxy.check_stack(stk) self.assertIsNone(res) - mock_check.assert_called_once_with(self.proxy._session) + mock_check.assert_called_once_with(self.proxy) @mock.patch.object(stack.Stack, 'existing') def test_check_stack_with_stack_ID(self, mock_stack): @@ -75,7 +75,7 @@ def test_check_stack_with_stack_ID(self, mock_stack): self.assertIsNone(res) mock_stack.assert_called_once_with(id='FAKE_ID') - stk.check.assert_called_once_with(self.proxy._session) + stk.check.assert_called_once_with(self.proxy) @mock.patch.object(stack.Stack, 'find') def test_get_stack_environment_with_stack_identity(self, mock_find): @@ -121,7 +121,7 @@ def test_get_stack_files_with_stack_identity(self, mock_find, mock_get): self.assertEqual({'file': 'content'}, res) mock_find.assert_called_once_with(mock.ANY, 'IDENTITY', ignore_missing=False) - mock_get.assert_called_once_with(self.proxy._session) + mock_get.assert_called_once_with(self.proxy) @mock.patch.object(stack_files.StackFiles, 'get') def test_get_stack_files_with_stack_object(self, mock_get): @@ -133,7 +133,7 @@ def test_get_stack_files_with_stack_object(self, mock_get): res = self.proxy.get_stack_files(stk) self.assertEqual({'file': 'content'}, res) - mock_get.assert_called_once_with(self.proxy._session) + mock_get.assert_called_once_with(self.proxy) @mock.patch.object(stack.Stack, 'find') def test_get_stack_template_with_stack_identity(self, mock_find): @@ -202,8 +202,7 @@ def test_resources_stack_not_found(self, mock_list, mock_find): ex = self.assertRaises(exceptions.ResourceNotFound, self.proxy.resources, stack_name) - self.assertEqual('ResourceNotFound: No stack found for test_stack', - six.text_type(ex)) + self.assertEqual('No stack found for test_stack', six.text_type(ex)) def test_create_software_config(self): self.verify_create(self.proxy.create_software_config, @@ -254,7 +253,7 @@ def test_validate_template(self, mock_validate): res = self.proxy.validate_template(tmpl, env, tmpl_url, ignore_errors) mock_validate.assert_called_once_with( - self.proxy._session, tmpl, environment=env, template_url=tmpl_url, + self.proxy, tmpl, environment=env, template_url=tmpl_url, ignore_errors=ignore_errors) self.assertEqual(mock_validate.return_value, res) diff --git a/openstack/tests/unit/orchestration/v1/test_stack.py b/openstack/tests/unit/orchestration/v1/test_stack.py index 4ca0d2bad..fc91d720d 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack.py +++ b/openstack/tests/unit/orchestration/v1/test_stack.py @@ -132,9 +132,9 @@ def test_get(self, mock_get): self.assertEqual(normal_stack, sot.get(sess)) ex = self.assertRaises(exceptions.NotFoundException, sot.get, sess) - self.assertEqual('NotFoundException: oops', six.text_type(ex)) + self.assertEqual('oops', six.text_type(ex)) ex = self.assertRaises(exceptions.NotFoundException, sot.get, sess) - self.assertEqual('NotFoundException: No stack found for %s' % FAKE_ID, + self.assertEqual('No stack found for %s' % FAKE_ID, six.text_type(ex)) diff --git a/openstack/tests/unit/orchestration/v1/test_stack_files.py b/openstack/tests/unit/orchestration/v1/test_stack_files.py index 989121a14..fe2d47c4e 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack_files.py +++ b/openstack/tests/unit/orchestration/v1/test_stack_files.py @@ -50,12 +50,12 @@ def test_get(self, mock_prepare_request): sot.service = mock.Mock() req = mock.MagicMock() - req.uri = ('/stacks/%(stack_name)s/%(stack_id)s/files' % + req.url = ('/stacks/%(stack_name)s/%(stack_id)s/files' % {'stack_name': FAKE['stack_name'], 'stack_id': FAKE['stack_id']}) mock_prepare_request.return_value = req files = sot.get(sess) - sess.get.assert_called_once_with(req.uri, endpoint_filter=sot.service) + sess.get.assert_called_once_with(req.url) self.assertEqual({'file': 'file-content'}, files) diff --git a/openstack/tests/unit/orchestration/v1/test_template.py b/openstack/tests/unit/orchestration/v1/test_template.py index c92b0ae79..24d7a369d 100644 --- a/openstack/tests/unit/orchestration/v1/test_template.py +++ b/openstack/tests/unit/orchestration/v1/test_template.py @@ -58,7 +58,7 @@ def test_validate(self, mock_translate): sot.validate(sess, tmpl) sess.post.assert_called_once_with( - '/validate', endpoint_filter=sot.service, json=body) + '/validate', json=body) mock_translate.assert_called_once_with(sess.post.return_value) @mock.patch.object(resource.Resource, '_translate_response') @@ -72,7 +72,7 @@ def test_validate_with_env(self, mock_translate): sot.validate(sess, tmpl, environment=env) sess.post.assert_called_once_with( - '/validate', endpoint_filter=sot.service, json=body) + '/validate', json=body) mock_translate.assert_called_once_with(sess.post.return_value) @mock.patch.object(resource.Resource, '_translate_response') @@ -85,7 +85,7 @@ def test_validate_with_template_url(self, mock_translate): sot.validate(sess, None, template_url=template_url) sess.post.assert_called_once_with( - '/validate', endpoint_filter=sot.service, json=body) + '/validate', json=body) mock_translate.assert_called_once_with(sess.post.return_value) @mock.patch.object(resource.Resource, '_translate_response') @@ -99,5 +99,5 @@ def test_validate_with_ignore_errors(self, mock_translate): sess.post.assert_called_once_with( '/validate?ignore_errors=123%2C456', - endpoint_filter=sot.service, json=body) + json=body) mock_translate.assert_called_once_with(sess.post.return_value) diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index 500c067a2..5a87366c2 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -13,14 +13,11 @@ import os import fixtures -from keystoneauth1 import session as ksa_session +from keystoneauth1 import session import mock -import openstack.config from openstack import connection -from openstack import exceptions -from openstack import profile -from openstack import session +import openstack.config from openstack.tests.unit import base @@ -46,7 +43,7 @@ password: {password} project_name: {project} cacert: {cacert} - insecure: True + verify: False cacert: auth: auth_url: {auth_url} @@ -54,92 +51,46 @@ password: {password} project_name: {project} cacert: {cacert} - insecure: False """.format(auth_url=CONFIG_AUTH_URL, username=CONFIG_USERNAME, password=CONFIG_PASSWORD, project=CONFIG_PROJECT, cacert=CONFIG_CACERT) -class TestConnection(base.TestCase): - @mock.patch("openstack.session.Session") - def test_other_parameters(self, mock_session_init): - mock_session_init.return_value = mock_session_init - mock_profile = mock.Mock() - mock_profile.get_services = mock.Mock(return_value=[]) - conn = connection.Connection(profile=mock_profile, authenticator='2', - verify=True, cert='cert', user_agent='1') - args = {'auth': '2', 'user_agent': '1', 'verify': True, 'cert': 'cert'} - mock_session_init.assert_called_with(mock_profile, **args) - self.assertEqual(mock_session_init, conn.session) +class TestConnection(base.RequestsMockTestCase): + + def setUp(self): + super(TestConnection, self).setUp() + # Create a temporary directory where our test config will live + # and insert it into the search path via OS_CLIENT_CONFIG_FILE. + config_dir = self.useFixture(fixtures.TempDir()).path + config_path = os.path.join(config_dir, "clouds.yaml") + + with open(config_path, "w") as conf: + conf.write(CLOUD_CONFIG) + + self.useFixture(fixtures.EnvironmentVariable( + "OS_CLIENT_CONFIG_FILE", config_path)) + + def test_other_parameters(self): + conn = connection.Connection(cloud='sample', cert='cert') + self.assertEqual(conn.session.cert, 'cert') def test_session_provided(self): mock_session = mock.Mock(spec=session.Session) - mock_profile = mock.Mock() - mock_profile.get_services = mock.Mock(return_value=[]) - conn = connection.Connection(session=mock_session, - profile=mock_profile, - user_agent='1') + conn = connection.Connection(session=mock_session, cert='cert') self.assertEqual(mock_session, conn.session) - def test_ksa_session_provided(self): - mock_session = mock.Mock(spec=ksa_session.Session) - mock_profile = mock.Mock() - mock_profile.get_services = mock.Mock(return_value=[]) - self.assertRaises(exceptions.SDKException, connection.Connection, - session=mock_session, profile=mock_profile, - user_agent='1') - - @mock.patch("keystoneauth1.loading.base.get_plugin_loader") - def test_create_authenticator(self, mock_get_plugin): - mock_plugin = mock.Mock() - mock_loader = mock.Mock() - mock_options = [ - mock.Mock(dest="auth_url"), - mock.Mock(dest="password"), - mock.Mock(dest="username"), - ] - mock_loader.get_options = mock.Mock(return_value=mock_options) - mock_loader.load_from_options = mock.Mock(return_value=mock_plugin) - mock_get_plugin.return_value = mock_loader - auth_args = { - 'auth_url': '0', - 'username': '1', - 'password': '2', - } - conn = connection.Connection(auth_plugin='v2password', **auth_args) - mock_get_plugin.assert_called_with('v2password') - mock_loader.load_from_options.assert_called_with(**auth_args) - self.assertEqual(mock_plugin, conn.authenticator) - - @mock.patch("keystoneauth1.loading.base.get_plugin_loader") - def test_default_plugin(self, mock_get_plugin): - connection.Connection() - self.assertTrue(mock_get_plugin.called) - self.assertEqual(mock_get_plugin.call_args, mock.call("password")) - - @mock.patch("keystoneauth1.loading.base.get_plugin_loader") - def test_pass_authenticator(self, mock_get_plugin): - mock_plugin = mock.Mock() - mock_get_plugin.return_value = None - conn = connection.Connection(authenticator=mock_plugin) - self.assertFalse(mock_get_plugin.called) - self.assertEqual(mock_plugin, conn.authenticator) - def test_create_session(self): - auth = mock.Mock() - prof = profile.Profile() - conn = connection.Connection(authenticator=auth, profile=prof) - self.assertEqual(auth, conn.authenticator) - self.assertEqual(prof, conn.profile) - self.assertEqual('openstack.telemetry.alarm.v2._proxy', + conn = connection.Connection(cloud='sample') + self.assertEqual('openstack.proxy2', conn.alarm.__class__.__module__) - self.assertEqual('openstack.cluster.v1._proxy', - conn.cluster.__class__.__module__) + self.assertEqual('openstack.clustering.v1._proxy', + conn.clustering.__class__.__module__) self.assertEqual('openstack.compute.v2._proxy', conn.compute.__class__.__module__) self.assertEqual('openstack.database.v1._proxy', conn.database.__class__.__module__) - self.assertEqual('openstack.identity.v3._proxy', + self.assertEqual('openstack.identity.v2._proxy', conn.identity.__class__.__module__) self.assertEqual('openstack.image.v2._proxy', conn.image.__class__.__module__) @@ -151,56 +102,38 @@ def test_create_session(self): conn.load_balancer.__class__.__module__) self.assertEqual('openstack.orchestration.v1._proxy', conn.orchestration.__class__.__module__) - self.assertEqual('openstack.telemetry.v2._proxy', - conn.telemetry.__class__.__module__) + self.assertEqual('openstack.meter.v2._proxy', + conn.meter.__class__.__module__) self.assertEqual('openstack.workflow.v2._proxy', conn.workflow.__class__.__module__) - def _prepare_test_config(self): - # Create a temporary directory where our test config will live - # and insert it into the search path via OS_CLIENT_CONFIG_FILE. - config_dir = self.useFixture(fixtures.TempDir()).path - config_path = os.path.join(config_dir, "clouds.yaml") - - with open(config_path, "w") as conf: - conf.write(CLOUD_CONFIG) - - self.useFixture(fixtures.EnvironmentVariable( - "OS_CLIENT_CONFIG_FILE", config_path)) - def test_from_config_given_data(self): - self._prepare_test_config() - data = openstack.config.OpenStackConfig().get_one_cloud("sample") sot = connection.from_config(cloud_config=data) self.assertEqual(CONFIG_USERNAME, - sot.authenticator._username) + sot.config.config['auth']['username']) self.assertEqual(CONFIG_PASSWORD, - sot.authenticator._password) + sot.config.config['auth']['password']) self.assertEqual(CONFIG_AUTH_URL, - sot.authenticator.auth_url) + sot.config.config['auth']['auth_url']) self.assertEqual(CONFIG_PROJECT, - sot.authenticator._project_name) + sot.config.config['auth']['project_name']) def test_from_config_given_name(self): - self._prepare_test_config() - sot = connection.from_config(cloud_name="sample") self.assertEqual(CONFIG_USERNAME, - sot.authenticator._username) + sot.config.config['auth']['username']) self.assertEqual(CONFIG_PASSWORD, - sot.authenticator._password) + sot.config.config['auth']['password']) self.assertEqual(CONFIG_AUTH_URL, - sot.authenticator.auth_url) + sot.config.config['auth']['auth_url']) self.assertEqual(CONFIG_PROJECT, - sot.authenticator._project_name) + sot.config.config['auth']['project_name']) def test_from_config_given_options(self): - self._prepare_test_config() - version = "100" class Opts(object): @@ -208,37 +141,24 @@ class Opts(object): sot = connection.from_config(cloud_name="sample", options=Opts) - pref = sot.session.profile.get_filter("compute") - - # NOTE: Along the way, the `v` prefix gets added so we can build - # up URLs with it. - self.assertEqual("v" + version, pref.version) + self.assertEqual(version, sot.compute.version) def test_from_config_verify(self): - self._prepare_test_config() - sot = connection.from_config(cloud_name="insecure") self.assertFalse(sot.session.verify) sot = connection.from_config(cloud_name="cacert") self.assertEqual(CONFIG_CACERT, sot.session.verify) + +class TestAuthorize(base.RequestsMockTestCase): + def test_authorize_works(self): - fake_session = mock.Mock(spec=session.Session) - fake_headers = {'X-Auth-Token': 'FAKE_TOKEN'} - fake_session.get_auth_headers.return_value = fake_headers - - sot = connection.Connection(session=fake_session, - authenticator=mock.Mock()) - res = sot.authorize() - self.assertEqual('FAKE_TOKEN', res) - - def test_authorize_silent_failure(self): - fake_session = mock.Mock(spec=session.Session) - fake_session.get_auth_headers.return_value = None - fake_session.__module__ = 'openstack.session' - - sot = connection.Connection(session=fake_session, - authenticator=mock.Mock()) - res = sot.authorize() - self.assertIsNone(res) + res = self.conn.authorize() + self.assertEqual('KeystoneToken-1', res) + + def test_authorize_failure(self): + self.use_broken_keystone() + + self.assertRaises(openstack.exceptions.HttpException, + self.conn.authorize) diff --git a/openstack/tests/unit/test_exceptions.py b/openstack/tests/unit/test_exceptions.py index 54a95b1fb..9deb5bab2 100644 --- a/openstack/tests/unit/test_exceptions.py +++ b/openstack/tests/unit/test_exceptions.py @@ -54,4 +54,4 @@ def test_http_status(self): http_status=http_status) self.assertEqual(self.message, exc.message) - self.assertEqual(http_status, exc.http_status) + self.assertEqual(http_status, exc.status_code) diff --git a/openstack/tests/unit/test_profile.py b/openstack/tests/unit/test_profile.py deleted file mode 100644 index c11d05f2a..000000000 --- a/openstack/tests/unit/test_profile.py +++ /dev/null @@ -1,106 +0,0 @@ -# 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 import profile -from openstack.tests.unit import base - - -class TestProfile(base.TestCase): - def test_init(self): - prof = profile.Profile() - expected = [ - 'alarming', - 'baremetal', - 'clustering', - 'compute', - 'database', - 'identity', - 'image', - 'key-manager', - 'load-balancer', - 'messaging', - 'metering', - 'network', - 'object-store', - 'orchestration', - 'volume', - 'workflowv2', - ] - self.assertEqual(expected, prof.service_keys) - - def test_default_versions(self): - prof = profile.Profile() - self.assertEqual('v1', prof.get_filter('baremetal').version) - self.assertEqual('v1', prof.get_filter('clustering').version) - self.assertEqual('v2', prof.get_filter('compute').version) - self.assertEqual('v1', prof.get_filter('database').version) - self.assertEqual('v3', prof.get_filter('identity').version) - self.assertEqual('v2', prof.get_filter('image').version) - self.assertEqual('v2', prof.get_filter('load-balancer').version) - self.assertEqual('v2', prof.get_filter('network').version) - self.assertEqual('v1', prof.get_filter('object-store').version) - self.assertEqual('v1', prof.get_filter('orchestration').version) - self.assertEqual('v1', prof.get_filter('key-manager').version) - self.assertEqual('v2', prof.get_filter('metering').version) - self.assertEqual('v2', prof.get_filter('volume').version) - self.assertEqual('v1', prof.get_filter('messaging').version) - - def test_set(self): - prof = profile.Profile() - prof.set_version('alarming', 'v2') - self.assertEqual('v2', prof.get_filter('alarming').version) - prof.set_version('baremetal', 'v1') - self.assertEqual('v1', prof.get_filter('baremetal').version) - prof.set_version('clustering', 'v1') - self.assertEqual('v1', prof.get_filter('clustering').version) - prof.set_version('compute', 'v2') - self.assertEqual('v2', prof.get_filter('compute').version) - prof.set_version('database', 'v3') - self.assertEqual('v3', prof.get_filter('database').version) - prof.set_version('identity', 'v4') - self.assertEqual('v4', prof.get_filter('identity').version) - prof.set_version('image', 'v5') - self.assertEqual('v5', prof.get_filter('image').version) - prof.set_version('metering', 'v6') - self.assertEqual('v6', prof.get_filter('metering').version) - prof.set_version('network', 'v7') - self.assertEqual('v7', prof.get_filter('network').version) - prof.set_version('object-store', 'v8') - self.assertEqual('v8', prof.get_filter('object-store').version) - prof.set_version('orchestration', 'v9') - self.assertEqual('v9', prof.get_filter('orchestration').version) - - def test_set_version_bad_service(self): - prof = profile.Profile() - self.assertRaises(exceptions.SDKException, prof.set_version, 'bogus', - 'v2') - - def test_set_api_version(self): - # This tests that api_version is effective after explicit setting, or - # else it defaults to None. - prof = profile.Profile() - prof.set_api_version('clustering', '1.2') - svc = prof.get_filter('clustering') - self.assertEqual('1.2', svc.api_version) - svc = prof.get_filter('compute') - self.assertIsNone(svc.api_version) - - def test_set_all(self): - prof = profile.Profile() - prof.set_name(prof.ALL, 'fee') - prof.set_region(prof.ALL, 'fie') - prof.set_interface(prof.ALL, 'public') - for service in prof.service_keys: - self.assertEqual('fee', prof.get_filter(service).service_name) - self.assertEqual('fie', prof.get_filter(service).region) - self.assertEqual('public', prof.get_filter(service).interface) diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index ec4f68075..6a9e9761c 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -103,11 +103,11 @@ def setUp(self): def test_delete(self): self.sot._delete(DeleteableResource, self.res) - self.res.delete.assert_called_with(self.session) + self.res.delete.assert_called_with(self.sot, error_message=mock.ANY) self.sot._delete(DeleteableResource, self.fake_id) DeleteableResource.existing.assert_called_with(id=self.fake_id) - self.res.delete.assert_called_with(self.session) + self.res.delete.assert_called_with(self.sot, error_message=mock.ANY) # Delete generally doesn't return anything, so we will normally # swallow any return from within a service's proxy, but make sure @@ -123,13 +123,12 @@ def test_delete_ignore_missing(self): rv = self.sot._delete(DeleteableResource, self.fake_id) self.assertIsNone(rv) - def test_delete_ResourceNotFound(self): + def test_delete_NotFound(self): self.res.delete.side_effect = exceptions.NotFoundException( message="test", http_status=404) self.assertRaisesRegex( - exceptions.ResourceNotFound, - "No %s found for %s" % (DeleteableResource.__name__, self.res), + exceptions.NotFoundException, "test", self.sot._delete, DeleteableResource, self.res, ignore_missing=False) @@ -166,7 +165,7 @@ def _test_update(self, value): self.assertEqual(rv, self.fake_result) self.res.update_attrs.assert_called_once_with(self.attrs) - self.res.update.assert_called_once_with(self.session) + self.res.update.assert_called_once_with(self.sot) def test_update_resource(self): self._test_update(self.res) @@ -196,7 +195,7 @@ def test_create_attributes(self): self.assertEqual(rv, self.fake_result) CreateableResource.new.assert_called_once_with(**attrs) - self.res.create.assert_called_once_with(self.session) + self.res.create.assert_called_once_with(self.sot) class TestProxyGet(testtools.TestCase): @@ -219,29 +218,35 @@ def setUp(self): def test_get_resource(self): rv = self.sot._get(RetrieveableResource, self.res) - self.res.get.assert_called_with(self.session, args=None) + self.res.get.assert_called_with(self.sot, args=None, + error_message=mock.ANY) self.assertEqual(rv, self.fake_result) def test_get_resource_with_args(self): rv = self.sot._get(RetrieveableResource, self.res, args={'K': 'V'}) - self.res.get.assert_called_with(self.session, args={'K': 'V'}) + self.res.get.assert_called_with( + self.sot, args={'K': 'V'}, + error_message='No RetrieveableResource found for {res}'.format( + res=str(self.res))) self.assertEqual(rv, self.fake_result) def test_get_id(self): rv = self.sot._get(RetrieveableResource, self.fake_id) RetrieveableResource.existing.assert_called_with(id=self.fake_id) - self.res.get.assert_called_with(self.session, args=None) + self.res.get.assert_called_with(self.sot, args=None, + error_message=mock.ANY) self.assertEqual(rv, self.fake_result) def test_get_not_found(self): self.res.get.side_effect = exceptions.NotFoundException( message="test", http_status=404) + # TODO(shade) The mock here does not mock the right things, so we're + # not testing the actual exception mechanism. self.assertRaisesRegex( - exceptions.ResourceNotFound, - "No %s found for %s" % (RetrieveableResource.__name__, self.res), + exceptions.NotFoundException, "test", self.sot._get, RetrieveableResource, self.res) @@ -270,7 +275,7 @@ def _test_list(self, path_args, paginated, **query): self.assertEqual(self.fake_response, rv) ListableResource.list.assert_called_once_with( - self.session, path_args=path_args, paginated=paginated, + self.sot, path_args=path_args, paginated=paginated, params={'a': self.fake_a, 'b': self.fake_b}) def test_list_paginated(self): @@ -300,14 +305,14 @@ def setUp(self): def test_head_resource(self): rv = self.sot._head(HeadableResource, self.res) - self.res.head.assert_called_with(self.session) + self.res.head.assert_called_with(self.sot) self.assertEqual(rv, self.fake_result) def test_head_id(self): rv = self.sot._head(HeadableResource, self.fake_id) HeadableResource.existing.assert_called_with(id=self.fake_id) - self.res.head.assert_called_with(self.session) + self.res.head.assert_called_with(self.sot) self.assertEqual(rv, self.fake_result) def test_head_no_value(self): @@ -318,7 +323,7 @@ def test_head_no_value(self): self.sot._head(MockHeadResource) MockHeadResource.assert_called_with() - instance.head.assert_called_with(self.session) + instance.head.assert_called_with(self.sot) @mock.patch("openstack.resource.wait_for_status") def test_wait_for(self, mock_wait): @@ -326,7 +331,7 @@ def test_wait_for(self, mock_wait): mock_wait.return_value = mock_resource self.sot.wait_for_status(mock_resource, 'ACTIVE') mock_wait.assert_called_once_with( - self.session, mock_resource, 'ACTIVE', [], 2, 120) + self.sot, mock_resource, 'ACTIVE', [], 2, 120) @mock.patch("openstack.resource.wait_for_status") def test_wait_for_params(self, mock_wait): @@ -334,7 +339,7 @@ def test_wait_for_params(self, mock_wait): mock_wait.return_value = mock_resource self.sot.wait_for_status(mock_resource, 'ACTIVE', ['ERROR'], 1, 2) mock_wait.assert_called_once_with( - self.session, mock_resource, 'ACTIVE', ['ERROR'], 1, 2) + self.sot, mock_resource, 'ACTIVE', ['ERROR'], 1, 2) @mock.patch("openstack.resource.wait_for_delete") def test_wait_for_delete(self, mock_wait): @@ -342,7 +347,7 @@ def test_wait_for_delete(self, mock_wait): mock_wait.return_value = mock_resource self.sot.wait_for_delete(mock_resource) mock_wait.assert_called_once_with( - self.session, mock_resource, 2, 120) + self.sot, mock_resource, 2, 120) @mock.patch("openstack.resource.wait_for_delete") def test_wait_for_delete_params(self, mock_wait): @@ -350,4 +355,4 @@ def test_wait_for_delete_params(self, mock_wait): mock_wait.return_value = mock_resource self.sot.wait_for_delete(mock_resource, 1, 2) mock_wait.assert_called_once_with( - self.session, mock_resource, 1, 2) + self.sot, mock_resource, 1, 2) diff --git a/openstack/tests/unit/test_proxy2.py b/openstack/tests/unit/test_proxy2.py index 332012811..6c9832d63 100644 --- a/openstack/tests/unit/test_proxy2.py +++ b/openstack/tests/unit/test_proxy2.py @@ -175,11 +175,11 @@ def setUp(self): def test_delete(self): self.sot._delete(DeleteableResource, self.res) - self.res.delete.assert_called_with(self.session) + self.res.delete.assert_called_with(self.sot, error_message=mock.ANY) self.sot._delete(DeleteableResource, self.fake_id) DeleteableResource.new.assert_called_with(id=self.fake_id) - self.res.delete.assert_called_with(self.session) + self.res.delete.assert_called_with(self.sot, error_message=mock.ANY) # Delete generally doesn't return anything, so we will normally # swallow any return from within a service's proxy, but make sure @@ -195,13 +195,14 @@ def test_delete_ignore_missing(self): rv = self.sot._delete(DeleteableResource, self.fake_id) self.assertIsNone(rv) - def test_delete_ResourceNotFound(self): + def test_delete_NotFound(self): self.res.delete.side_effect = exceptions.NotFoundException( message="test", http_status=404) self.assertRaisesRegex( - exceptions.ResourceNotFound, - "No %s found for %s" % (DeleteableResource.__name__, self.res), + exceptions.NotFoundException, + # TODO(shade) The mocks here are hiding the thing we want to test. + "test", self.sot._delete, DeleteableResource, self.res, ignore_missing=False) @@ -237,13 +238,13 @@ def test_update_resource(self): self.assertEqual(rv, self.fake_result) self.res._update.assert_called_once_with(**self.attrs) - self.res.update.assert_called_once_with(self.session) + self.res.update.assert_called_once_with(self.sot) def test_update_id(self): rv = self.sot._update(UpdateableResource, self.fake_id, **self.attrs) self.assertEqual(rv, self.fake_result) - self.res.update.assert_called_once_with(self.session) + self.res.update.assert_called_once_with(self.sot) class TestProxyCreate(testtools.TestCase): @@ -267,7 +268,7 @@ def test_create_attributes(self): self.assertEqual(rv, self.fake_result) CreateableResource.new.assert_called_once_with(**attrs) - self.res.create.assert_called_once_with(self.session) + self.res.create.assert_called_once_with(self.sot) class TestProxyGet(testtools.TestCase): @@ -290,7 +291,8 @@ def setUp(self): def test_get_resource(self): rv = self.sot._get(RetrieveableResource, self.res) - self.res.get.assert_called_with(self.session, requires_id=True) + self.res.get.assert_called_with(self.sot, requires_id=True, + error_message=mock.ANY) self.assertEqual(rv, self.fake_result) def test_get_resource_with_args(self): @@ -298,14 +300,16 @@ def test_get_resource_with_args(self): rv = self.sot._get(RetrieveableResource, self.res, **args) self.res._update.assert_called_once_with(**args) - self.res.get.assert_called_with(self.session, requires_id=True) + self.res.get.assert_called_with(self.sot, requires_id=True, + error_message=mock.ANY) self.assertEqual(rv, self.fake_result) def test_get_id(self): rv = self.sot._get(RetrieveableResource, self.fake_id) RetrieveableResource.new.assert_called_with(id=self.fake_id) - self.res.get.assert_called_with(self.session, requires_id=True) + self.res.get.assert_called_with(self.sot, requires_id=True, + error_message=mock.ANY) self.assertEqual(rv, self.fake_result) def test_get_not_found(self): @@ -313,9 +317,8 @@ def test_get_not_found(self): message="test", http_status=404) self.assertRaisesRegex( - exceptions.ResourceNotFound, - "No %s found for %s" % (RetrieveableResource.__name__, self.res), - self.sot._get, RetrieveableResource, self.res) + exceptions.NotFoundException, + "test", self.sot._get, RetrieveableResource, self.res) class TestProxyList(testtools.TestCase): @@ -337,7 +340,7 @@ def _test_list(self, paginated): self.assertEqual(self.fake_response, rv) ListableResource.list.assert_called_once_with( - self.session, paginated=paginated, **self.args) + self.sot, paginated=paginated, **self.args) def test_list_paginated(self): self._test_list(True) @@ -366,14 +369,14 @@ def setUp(self): def test_head_resource(self): rv = self.sot._head(HeadableResource, self.res) - self.res.head.assert_called_with(self.session) + self.res.head.assert_called_with(self.sot) self.assertEqual(rv, self.fake_result) def test_head_id(self): rv = self.sot._head(HeadableResource, self.fake_id) HeadableResource.new.assert_called_with(id=self.fake_id) - self.res.head.assert_called_with(self.session) + self.res.head.assert_called_with(self.sot) self.assertEqual(rv, self.fake_result) @@ -391,7 +394,7 @@ def test_wait_for(self, mock_wait): mock_wait.return_value = mock_resource self.sot.wait_for_status(mock_resource, 'ACTIVE') mock_wait.assert_called_once_with( - self.session, mock_resource, 'ACTIVE', [], 2, 120) + self.sot, mock_resource, 'ACTIVE', [], 2, 120) @mock.patch("openstack.resource2.wait_for_status") def test_wait_for_params(self, mock_wait): @@ -399,20 +402,18 @@ def test_wait_for_params(self, mock_wait): mock_wait.return_value = mock_resource self.sot.wait_for_status(mock_resource, 'ACTIVE', ['ERROR'], 1, 2) mock_wait.assert_called_once_with( - self.session, mock_resource, 'ACTIVE', ['ERROR'], 1, 2) + self.sot, mock_resource, 'ACTIVE', ['ERROR'], 1, 2) @mock.patch("openstack.resource2.wait_for_delete") def test_wait_for_delete(self, mock_wait): mock_resource = mock.Mock() mock_wait.return_value = mock_resource self.sot.wait_for_delete(mock_resource) - mock_wait.assert_called_once_with( - self.session, mock_resource, 2, 120) + mock_wait.assert_called_once_with(self.sot, mock_resource, 2, 120) @mock.patch("openstack.resource2.wait_for_delete") def test_wait_for_delete_params(self, mock_wait): mock_resource = mock.Mock() mock_wait.return_value = mock_resource self.sot.wait_for_delete(mock_resource, 1, 2) - mock_wait.assert_called_once_with( - self.session, mock_resource, 1, 2) + mock_wait.assert_called_once_with(self.sot, mock_resource, 1, 2) diff --git a/openstack/tests/unit/test_proxy_base.py b/openstack/tests/unit/test_proxy_base.py index 348422d82..38cb1dd52 100644 --- a/openstack/tests/unit/test_proxy_base.py +++ b/openstack/tests/unit/test_proxy_base.py @@ -43,11 +43,11 @@ def _verify(self, mock_method, test_method, self.assertEqual(expected_result, test_method(*method_args, **method_kwargs)) - mocked.assert_called_with(self.session, + mocked.assert_called_with(test_method.__self__, *expected_args, **expected_kwargs) else: self.assertEqual(expected_result, test_method()) - mocked.assert_called_with(self.session) + mocked.assert_called_with(test_method.__self__) # NOTE(briancurtin): This is a duplicate version of _verify that is # temporarily here while we shift APIs. The difference is that @@ -79,7 +79,7 @@ def _verify2(self, mock_method, test_method, mocked.assert_called_with(*expected_args, **expected_kwargs) else: self.assertEqual(expected_result, test_method()) - mocked.assert_called_with(self.session) + mocked.assert_called_with(test_method.__self__) def verify_create(self, test_method, resource_type, mock_method="openstack.proxy.BaseProxy._create", diff --git a/openstack/tests/unit/test_proxy_base2.py b/openstack/tests/unit/test_proxy_base2.py index f984de659..27e1c150d 100644 --- a/openstack/tests/unit/test_proxy_base2.py +++ b/openstack/tests/unit/test_proxy_base2.py @@ -43,11 +43,11 @@ def _verify(self, mock_method, test_method, self.assertEqual(expected_result, test_method(*method_args, **method_kwargs)) - mocked.assert_called_with(self.session, + mocked.assert_called_with(test_method.__self__, *expected_args, **expected_kwargs) else: self.assertEqual(expected_result, test_method()) - mocked.assert_called_with(self.session) + mocked.assert_called_with(test_method.__self__) # NOTE(briancurtin): This is a duplicate version of _verify that is # temporarily here while we shift APIs. The difference is that @@ -79,7 +79,7 @@ def _verify2(self, mock_method, test_method, mocked.assert_called_with(*expected_args, **expected_kwargs) else: self.assertEqual(expected_result, test_method()) - mocked.assert_called_with(self.session) + mocked.assert_called_with(test_method.__self__) def verify_create(self, test_method, resource_type, mock_method="openstack.proxy2.BaseProxy._create", diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 87f400924..2364920b2 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -269,7 +269,6 @@ def test_create_update_headers(self): sot.create(sess) headers = {'guitar': 'johnny', 'bass': 'deedee'} sess.post.assert_called_with(HeaderTests.Test.base_path, - endpoint_filter=HeaderTests.Test.service, headers=headers, json={}) @@ -278,7 +277,6 @@ def test_create_update_headers(self): headers = {'guitar': 'johnny', 'bass': 'cj'} sot.update(sess) sess.put.assert_called_with('ramones/1', - endpoint_filter=HeaderTests.Test.service, headers=headers, json={}) @@ -296,8 +294,7 @@ def assertCalledURL(self, method, url): self.assertEqual(method.call_args[0][0], url) def test_empty_id(self): - resp = mock.Mock() - resp.json = mock.Mock(return_value=fake_body) + resp = FakeResponse(fake_body) self.session.get.return_value = resp obj = FakeResource.new(**fake_arguments) @@ -372,7 +369,6 @@ class FakeResource2(FakeResource): resp = FakeResource2.create_by_id(sess, attrs) self.assertEqual(expected_resp, resp) sess.post.assert_called_with(FakeResource2.base_path, - endpoint_filter=FakeResource2.service, json=json_body) r_id = "my_id" @@ -380,14 +376,12 @@ class FakeResource2(FakeResource): self.assertEqual(response_value, resp) sess.put.assert_called_with( utils.urljoin(FakeResource2.base_path, r_id), - endpoint_filter=FakeResource2.service, json=json_body) path_args = {"parent_name": "my_name"} resp = FakeResource2.create_by_id(sess, attrs, path_args=path_args) self.assertEqual(response_value, resp) sess.post.assert_called_with(FakeResource2.base_path % path_args, - endpoint_filter=FakeResource2.service, json=json_body) resp = FakeResource2.create_by_id(sess, attrs, resource_id=r_id, @@ -395,7 +389,6 @@ class FakeResource2(FakeResource): self.assertEqual(response_value, resp) sess.put.assert_called_with( utils.urljoin(FakeResource2.base_path % path_args, r_id), - endpoint_filter=FakeResource2.service, json=json_body) def test_create_without_resource_key(self): @@ -432,8 +425,7 @@ class FakeResource2(FakeResource): resource_key = key service = "my_service" - response = mock.Mock() - response.json = mock.Mock(return_value=response_body) + response = FakeResponse(response_body) sess = mock.Mock() sess.get = mock.Mock(return_value=response) @@ -443,7 +435,7 @@ class FakeResource2(FakeResource): self.assertEqual(response_value, resp) sess.get.assert_called_with( utils.urljoin(FakeResource2.base_path, r_id), - endpoint_filter=FakeResource2.service) + ) path_args = {"parent_name": "my_name"} resp = FakeResource2.get_data_by_id(sess, resource_id=r_id, @@ -451,7 +443,7 @@ class FakeResource2(FakeResource): self.assertEqual(response_value, resp) sess.get.assert_called_with( utils.urljoin(FakeResource2.base_path % path_args, r_id), - endpoint_filter=FakeResource2.service) + ) def test_get_data_without_resource_key(self): key = None @@ -482,7 +474,6 @@ class FakeResource2(FakeResource): headers = {'Accept': ''} sess.head.assert_called_with( utils.urljoin(FakeResource2.base_path, r_id), - endpoint_filter=FakeResource2.service, headers=headers) path_args = {"parent_name": "my_name"} @@ -492,7 +483,6 @@ class FakeResource2(FakeResource): headers = {'Accept': ''} sess.head.assert_called_with( utils.urljoin(FakeResource2.base_path % path_args, r_id), - endpoint_filter=FakeResource2.service, headers=headers) def test_head_data_without_resource_key(self): @@ -528,7 +518,6 @@ class FakeResource2(FakeResource): self.assertEqual(expected_resp, resp) sess.patch.assert_called_with( utils.urljoin(FakeResource2.base_path, r_id), - endpoint_filter=FakeResource2.service, json=json_body) path_args = {"parent_name": "my_name"} @@ -537,7 +526,6 @@ class FakeResource2(FakeResource): self.assertEqual(expected_resp, resp) sess.patch.assert_called_with( utils.urljoin(FakeResource2.base_path % path_args, r_id), - endpoint_filter=FakeResource2.service, json=json_body) def test_update_without_resource_key(self): @@ -574,7 +562,7 @@ class FakeResource2(FakeResource): service = "my_service" sess = mock.Mock() - sess.delete = mock.Mock(return_value=None) + sess.delete = mock.Mock(return_value=FakeResponse({})) r_id = "my_id" resp = FakeResource2.delete_by_id(sess, r_id) @@ -582,7 +570,6 @@ class FakeResource2(FakeResource): headers = {'Accept': ''} sess.delete.assert_called_with( utils.urljoin(FakeResource2.base_path, r_id), - endpoint_filter=FakeResource2.service, headers=headers) path_args = {"parent_name": "my_name"} @@ -591,13 +578,10 @@ class FakeResource2(FakeResource): headers = {'Accept': ''} sess.delete.assert_called_with( utils.urljoin(FakeResource2.base_path % path_args, r_id), - endpoint_filter=FakeResource2.service, headers=headers) def test_create(self): - resp = mock.Mock() - resp.json = mock.Mock(return_value=fake_body) - resp.headers = {'location': 'foo'} + resp = FakeResponse(fake_body, headers={'location': 'foo'}) self.session.post = mock.Mock(return_value=resp) # Create resource with subset of attributes in order to @@ -639,9 +623,7 @@ def test_create(self): self.assertEqual('foo', obj.location) def test_get(self): - resp = mock.Mock() - resp.json = mock.Mock(return_value=fake_body) - resp.headers = {'location': 'foo'} + resp = FakeResponse(fake_body, headers={'location': 'foo'}) self.session.get = mock.Mock(return_value=resp) # Create resource with subset of attributes in order to @@ -676,8 +658,7 @@ def test_get(self): self.assertIsNone(obj.location) def test_get_by_id(self): - resp = mock.Mock() - resp.json = mock.Mock(return_value=fake_body) + resp = FakeResponse(fake_body) self.session.get = mock.Mock(return_value=resp) obj = FakeResource.get_by_id(self.session, fake_id, @@ -703,8 +684,7 @@ def test_get_by_id_with_headers(self): headers = {"header1": header1, "header2": header2} - resp = mock.Mock(headers=headers) - resp.json = mock.Mock(return_value=fake_body) + resp = FakeResponse(fake_body, headers=headers) self.session.get = mock.Mock(return_value=resp) class FakeResource2(FakeResource): @@ -737,7 +717,7 @@ class FakeResource2(FakeResource): header1 = resource.header("header1") header2 = resource.header("header2") - resp = mock.Mock(headers={"header1": "one", "header2": "two"}) + resp = FakeResponse(None, headers={"header1": "one", "header2": "two"}) self.session.head = mock.Mock(return_value=resp) obj = FakeResource2.head_by_id(self.session, fake_id, @@ -757,9 +737,7 @@ def test_patch_update(self): class FakeResourcePatch(FakeResource): patch_update = True - resp = mock.Mock() - resp.json = mock.Mock(return_value=fake_body) - resp.headers = {'location': 'foo'} + resp = FakeResponse(fake_body, headers={'location': 'foo'}) self.session.patch = mock.Mock(return_value=resp) # Create resource with subset of attributes in order to @@ -807,9 +785,7 @@ class FakeResourcePut(FakeResource): # This is False by default, but explicit for this test. patch_update = False - resp = mock.Mock() - resp.json = mock.Mock(return_value=fake_body) - resp.headers = {'location': 'foo'} + resp = FakeResponse(fake_body, headers={'location': 'foo'}) self.session.put = mock.Mock(return_value=resp) # Create resource with subset of attributes in order to @@ -869,6 +845,7 @@ def test_update_no_id_attribute(self): def test_delete(self): obj = FakeResource({"id": fake_id, "parent_name": fake_parent}) + self.session.delete.return_value = FakeResponse({}) obj.delete(self.session) self.assertCalledURL(self.session.delete, @@ -919,8 +896,7 @@ def test_list_non_keyed_resource(self): def _test_list_call_count(self, paginated): # Test that we've only made one call to receive all data results = [fake_data.copy(), fake_data.copy(), fake_data.copy()] - resp = mock.Mock() - resp.json = mock.Mock(return_value={fake_resources: results}) + resp = FakeResponse({fake_resources: results}) attrs = {"get.return_value": resp} session = mock.Mock(**attrs) @@ -1315,8 +1291,7 @@ def fake_call(*args, **kwargs): json.dumps(attrs) except TypeError as e: self.fail("Unable to serialize _attrs: %s" % e) - resp = mock.Mock() - resp.json = mock.Mock(return_value=attrs) + resp = FakeResponse(attrs) return resp session = mock.Mock() @@ -1335,8 +1310,11 @@ def test_update_serializes_resource_types(self): class FakeResponse(object): - def __init__(self, response): + def __init__(self, response, status_code=200, headers=None): self.body = response + self.status_code = status_code + headers = headers if headers else {'content-type': 'application/json'} + self.headers = requests.structures.CaseInsensitiveDict(headers) def json(self): return self.body @@ -1378,7 +1356,7 @@ def test_id(self): self.assertEqual(self.PROP, result.prop) path = "fakes/" + fake_parent + "/data/" + self.ID - self.mock_get.assert_any_call(path, endpoint_filter=None) + self.mock_get.assert_any_call(path,) def test_id_no_retrieve(self): self.mock_get.side_effect = [ @@ -1423,7 +1401,7 @@ def test_id_attribute_find(self): p = {'ip_address': "127.0.0.1"} path = fake_path + "?limit=2" - self.mock_get.called_once_with(path, params=p, endpoint_filter=None) + self.mock_get.called_once_with(path, params=p,) def test_nada(self): self.mock_get.side_effect = [ @@ -1520,7 +1498,8 @@ def test_wait_for_delete(self): sot.get = mock.Mock() sot.get.side_effect = [ sot, - exceptions.NotFoundException()] + exceptions.NotFoundException( + 'not found', FakeResponse({}, status_code=404))] self.assertEqual(sot, resource.wait_for_delete(sess, sot, 1, 2)) diff --git a/openstack/tests/unit/test_resource2.py b/openstack/tests/unit/test_resource2.py index c4055f531..8f72dbfdd 100644 --- a/openstack/tests/unit/test_resource2.py +++ b/openstack/tests/unit/test_resource2.py @@ -12,16 +12,28 @@ import itertools +from keystoneauth1 import session import mock +import requests import six from openstack import exceptions from openstack import format from openstack import resource2 -from openstack import session from openstack.tests.unit import base +class FakeResponse(object): + def __init__(self, response, status_code=200, headers=None): + self.body = response + self.status_code = status_code + headers = headers if headers else {'content-type': 'application/json'} + self.headers = requests.structures.CaseInsensitiveDict(headers) + + def json(self): + return self.body + + class TestComponent(base.TestCase): class ExampleComponent(resource2._BaseComponent): @@ -337,7 +349,7 @@ def test_create(self): sot = resource2._Request(uri, body, headers) - self.assertEqual(uri, sot.uri) + self.assertEqual(uri, sot.url) self.assertEqual(body, sot.body) self.assertEqual(headers, sot.headers) @@ -791,7 +803,7 @@ class Test(resource2.Resource): result = sot._prepare_request(requires_id=True) - self.assertEqual("something/id", result.uri) + self.assertEqual("something/id", result.url) self.assertEqual({"x": body_value, "id": the_id}, result.body) self.assertEqual({"y": header_value}, result.headers) @@ -817,7 +829,7 @@ class Test(resource2.Resource): result = sot._prepare_request(requires_id=False, prepend_key=True) - self.assertEqual("/something", result.uri) + self.assertEqual("/something", result.url) self.assertEqual({key: {"x": body_value}}, result.body) self.assertEqual({"y": header_value}, result.headers) @@ -840,8 +852,7 @@ def test__translate_response_no_body(self): class Test(resource2.Resource): attr = resource2.Header("attr") - response = mock.Mock() - response.headers = dict() + response = FakeResponse({}) sot = Test() sot._filter_component = mock.Mock(return_value={"attr": "value"}) @@ -856,9 +867,7 @@ class Test(resource2.Resource): attr = resource2.Body("attr") body = {"attr": "value"} - response = mock.Mock() - response.headers = dict() - response.json.return_value = body + response = FakeResponse(body) sot = Test() sot._filter_component = mock.Mock(side_effect=[body, dict()]) @@ -877,9 +886,7 @@ class Test(resource2.Resource): attr = resource2.Body("attr") body = {"attr": "value"} - response = mock.Mock() - response.headers = dict() - response.json.return_value = {key: body} + response = FakeResponse({key: body}) sot = Test() sot._filter_component = mock.Mock(side_effect=[body, dict()]) @@ -941,11 +948,11 @@ class Test(resource2.Resource): self.test_class = Test self.request = mock.Mock(spec=resource2._Request) - self.request.uri = "uri" + self.request.url = "uri" self.request.body = "body" self.request.headers = "headers" - self.response = mock.Mock() + self.response = FakeResponse({}) self.sot = Test(id="id") self.sot._prepare_request = mock.Mock(return_value=self.request) @@ -972,13 +979,11 @@ def _test_create(self, cls, requires_id=False, prepend_key=False): requires_id=requires_id, prepend_key=prepend_key) if requires_id: self.session.put.assert_called_once_with( - self.request.uri, - endpoint_filter=self.service_name, + self.request.url, json=self.request.body, headers=self.request.headers) else: self.session.post.assert_called_once_with( - self.request.uri, - endpoint_filter=self.service_name, + self.request.url, json=self.request.body, headers=self.request.headers) sot._translate_response.assert_called_once_with(self.response) @@ -1007,7 +1012,7 @@ def test_get(self): self.sot._prepare_request.assert_called_once_with(requires_id=True) self.session.get.assert_called_once_with( - self.request.uri, endpoint_filter=self.service_name) + self.request.url,) self.sot._translate_response.assert_called_once_with(self.response) self.assertEqual(result, self.sot) @@ -1017,7 +1022,7 @@ def test_get_not_requires_id(self): self.sot._prepare_request.assert_called_once_with(requires_id=False) self.session.get.assert_called_once_with( - self.request.uri, endpoint_filter=self.service_name) + self.request.url,) self.sot._translate_response.assert_called_once_with(self.response) self.assertEqual(result, self.sot) @@ -1027,8 +1032,7 @@ def test_head(self): self.sot._prepare_request.assert_called_once_with() self.session.head.assert_called_once_with( - self.request.uri, - endpoint_filter=self.service_name, + self.request.url, headers={"Accept": ""}) self.sot._translate_response.assert_called_once_with(self.response) @@ -1050,13 +1054,11 @@ def _test_update(self, patch_update=False, prepend_key=True, if patch_update: self.session.patch.assert_called_once_with( - self.request.uri, - endpoint_filter=self.service_name, + self.request.url, json=self.request.body, headers=self.request.headers) else: self.session.put.assert_called_once_with( - self.request.uri, - endpoint_filter=self.service_name, + self.request.url, json=self.request.body, headers=self.request.headers) self.sot._translate_response.assert_called_once_with( @@ -1083,8 +1085,7 @@ def test_delete(self): self.sot._prepare_request.assert_called_once_with() self.session.delete.assert_called_once_with( - self.request.uri, - endpoint_filter=self.service_name, + self.request.url, headers={"Accept": ""}) self.sot._translate_response.assert_called_once_with( @@ -1095,8 +1096,7 @@ def test_delete(self): # the generator. Wrap calls to self.sot.list in a `list` # and then test the results as a list of responses. def test_list_empty_response(self): - mock_response = mock.Mock() - mock_response.json.return_value = [] + mock_response = FakeResponse([]) self.session.get.return_value = mock_response @@ -1104,7 +1104,6 @@ def test_list_empty_response(self): self.session.get.assert_called_once_with( self.base_path, - endpoint_filter=self.service_name, headers={"Accept": "application/json"}, params={}) @@ -1142,7 +1141,6 @@ def test_list_one_page_response_not_paginated(self): self.session.get.assert_called_once_with( self.base_path, - endpoint_filter=self.service_name, headers={"Accept": "application/json"}, params={}) @@ -1168,7 +1166,6 @@ class Test(self.test_class): self.session.get.assert_called_once_with( self.base_path, - endpoint_filter=self.service_name, headers={"Accept": "application/json"}, params={}) @@ -1242,7 +1239,6 @@ def test_list_multi_page_response_paginated(self): self.assertEqual(result0.id, ids[0]) self.session.get.assert_called_with( self.base_path, - endpoint_filter=self.service_name, headers={"Accept": "application/json"}, params={}) @@ -1250,14 +1246,12 @@ def test_list_multi_page_response_paginated(self): self.assertEqual(result1.id, ids[1]) self.session.get.assert_called_with( self.base_path, - endpoint_filter=self.service_name, headers={"Accept": "application/json"}, params={"limit": 1, "marker": 1}) self.assertRaises(StopIteration, next, results) self.session.get.assert_called_with( self.base_path, - endpoint_filter=self.service_name, headers={"Accept": "application/json"}, params={"limit": 1, "marker": 2}) @@ -1284,7 +1278,6 @@ def test_list_multi_page_early_termination(self): self.assertEqual(result1.id, ids[1]) self.session.get.assert_called_with( self.base_path, - endpoint_filter=self.service_name, headers={"Accept": "application/json"}, params={}) @@ -1293,7 +1286,6 @@ def test_list_multi_page_early_termination(self): self.assertEqual(result2.id, ids[2]) self.session.get.assert_called_with( self.base_path, - endpoint_filter=self.service_name, headers={"Accept": "application/json"}, params={"limit": 2, "marker": 2}) @@ -1315,7 +1307,8 @@ class Base(resource2.Resource): @classmethod def existing(cls, **kwargs): - raise exceptions.NotFoundException + raise exceptions.NotFoundException( + 'Not Found', response=mock.Mock()) @classmethod def list(cls, session): @@ -1490,8 +1483,12 @@ class TestWaitForDelete(base.TestCase): @mock.patch("time.sleep", return_value=None) def test_success(self, mock_sleep): + response = mock.Mock() + response.headers = {} resource = mock.Mock() - resource.get.side_effect = [None, None, exceptions.NotFoundException] + resource.get.side_effect = [ + None, None, + exceptions.NotFoundException('Not Found', response)] result = resource2.wait_for_delete("session", resource, 1, 3) diff --git a/openstack/tests/unit/test_session.py b/openstack/tests/unit/test_session.py deleted file mode 100644 index 82cdeeca0..000000000 --- a/openstack/tests/unit/test_session.py +++ /dev/null @@ -1,437 +0,0 @@ -# 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 testtools - -from keystoneauth1 import exceptions as _exceptions - -from openstack import exceptions -from openstack import profile -from openstack import session -from openstack import utils - -HTML_MSG = """ - - 404 Entity Not Found - - -

404 Entity Not Found

- Entity could not be found -

- -""" - - -class TestSession(testtools.TestCase): - - def test_init_user_agent_none(self): - sot = session.Session(None) - self.assertTrue(sot.user_agent.startswith("openstacksdk")) - - def test_init_user_agent_set(self): - sot = session.Session(None, user_agent="testing/123") - self.assertTrue(sot.user_agent.startswith("testing/123 openstacksdk")) - - def test_init_with_single_api_request(self): - prof = profile.Profile() - prof.set_api_version('clustering', '1.2') - - sot = session.Session(prof) - - # The assertion acutally tests the property assigned in parent class - self.assertEqual({'openstack-api-version': 'clustering 1.2'}, - sot.additional_headers) - - def test_init_with_multi_api_requests(self): - prof = profile.Profile() - prof.set_api_version('clustering', '1.2') - prof.set_api_version('compute', '2.15') - - sot = session.Session(prof) - - versions = sot.additional_headers['openstack-api-version'] - requests = [req.strip() for req in versions.split(',')] - self.assertIn('clustering 1.2', requests) - self.assertIn('compute 2.15', requests) - - def test_init_with_no_api_requests(self): - prof = profile.Profile() - - sot = session.Session(prof) - - self.assertEqual({}, sot.additional_headers) - - def _assert_map_exceptions(self, expected_exc, ksa_exc, func): - os_exc = self.assertRaises( - expected_exc, session.map_exceptions(func)) - self.assertIsInstance(os_exc, expected_exc) - self.assertEqual(ksa_exc.message, os_exc.message) - self.assertEqual(ksa_exc.http_status, os_exc.http_status) - self.assertEqual(ksa_exc, os_exc.cause) - return os_exc - - def test_map_exceptions_not_found_exception(self): - response = mock.Mock() - response_body = {'NotFoundException': { - 'message': 'Resource not found'}} - response.json = mock.Mock(return_value=response_body) - response.headers = {"content-type": "application/json"} - response.status_code = 404 - ksa_exc = _exceptions.HttpError( - message="test", http_status=404, response=response) - func = mock.Mock(side_effect=ksa_exc) - os_exc = self._assert_map_exceptions( - exceptions.NotFoundException, ksa_exc, func) - self.assertEqual('Resource not found', os_exc.details) - - def test_map_exceptions_http_exception(self): - response = mock.Mock() - response_body = {'HTTPBadRequest': { - 'message': 'request is invalid'}} - response.json = mock.Mock(return_value=response_body) - response.headers = {"content-type": "application/json"} - response.status_code = 400 - ksa_exc = _exceptions.HttpError( - message="test", http_status=400, response=response) - func = mock.Mock(side_effect=ksa_exc) - - os_exc = self._assert_map_exceptions( - exceptions.HttpException, ksa_exc, func) - self.assertEqual('request is invalid', os_exc.details) - - def test_map_exceptions_http_exception_handle_json(self): - mock_resp = mock.Mock() - mock_resp.status_code = 413 - mock_resp.json.return_value = { - "overLimit": { - "message": "OverLimit413...", - "retryAt": "2017-01-03T13:33:06Z" - }, - "overLimitRetry": { - "message": "OverLimit Retry...", - "retryAt": "2017-01-03T13:33:06Z" - } - } - mock_resp.headers = { - "content-type": "application/json" - } - ksa_exc = _exceptions.HttpError( - message="test", http_status=413, response=mock_resp) - func = mock.Mock(side_effect=ksa_exc) - - os_exc = self._assert_map_exceptions( - exceptions.HttpException, ksa_exc, func) - # It's not sure that which 'message' will be first so exact checking is - # difficult here. It can be 'OverLimit413...\nOverLimit Retry...' or - # it can be 'OverLimit Retry...\nOverLimit413...'. - self.assertIn('OverLimit413...', os_exc.details) - self.assertIn('OverLimit Retry...', os_exc.details) - - def test_map_exceptions_http_exception_handle_json_1(self): - # A test for json containing non-dict values - mock_resp = mock.Mock() - mock_resp.status_code = 404 - mock_resp.json.return_value = { - "code": 404, - "error": { - "message": "resource not found", - }, - } - mock_resp.headers = { - "content-type": "application/json" - } - ksa_exc = _exceptions.HttpError(message="test", http_status=404, - response=mock_resp) - func = mock.Mock(side_effect=ksa_exc) - - os_exc = self._assert_map_exceptions( - exceptions.HttpException, ksa_exc, func) - self.assertIn('not found', os_exc.details) - - def test_map_exceptions_notfound_exception_handle_html(self): - mock_resp = mock.Mock() - mock_resp.status_code = 404 - mock_resp.text = HTML_MSG - mock_resp.headers = { - "content-type": "text/html" - } - ksa_exc = _exceptions.HttpError( - message="test", http_status=404, response=mock_resp) - func = mock.Mock(side_effect=ksa_exc) - - os_exc = self._assert_map_exceptions( - exceptions.NotFoundException, ksa_exc, func) - self.assertEqual('404 Entity Not Found: Entity could not be found', - os_exc.details) - - def test_map_exceptions_notfound_exception_handle_other_content_type(self): - mock_resp = mock.Mock() - mock_resp.status_code = 404 - fake_text = ("{'UnknownException': {'message': " - "'UnknownException occurred...'}}") - mock_resp.text = fake_text - mock_resp.headers = { - "content-type": 'application/octet-stream' - } - ksa_exc = _exceptions.HttpError( - message="test", http_status=404, response=mock_resp) - func = mock.Mock(side_effect=ksa_exc) - - os_exc = self._assert_map_exceptions( - exceptions.NotFoundException, ksa_exc, func) - self.assertEqual(fake_text, os_exc.details) - - def test_map_exceptions_sdk_exception_1(self): - ksa_exc = _exceptions.ClientException() - func = mock.Mock(side_effect=ksa_exc) - - os_exc = self.assertRaises( - exceptions.SDKException, session.map_exceptions(func)) - self.assertIsInstance(os_exc, exceptions.SDKException) - self.assertEqual(ksa_exc, os_exc.cause) - - def test_map_exceptions_sdk_exception_2(self): - ksa_exc = _exceptions.VersionNotAvailable() - func = mock.Mock(side_effect=ksa_exc) - - os_exc = self.assertRaises( - exceptions.SDKException, session.map_exceptions(func)) - self.assertIsInstance(os_exc, exceptions.SDKException) - self.assertEqual(ksa_exc, os_exc.cause) - - def test__parse_versions_response_exception(self): - uri = "http://www.openstack.org" - level = "DEBUG" - sot = session.Session(None) - sot.get = mock.Mock(side_effect=exceptions.NotFoundException) - - with self.assertLogs(logger=session.__name__, level=level) as log: - self.assertIsNone(sot._parse_versions_response(uri)) - - self.assertEqual(len(log.output), 1, - "Too many warnings were logged") - self.assertEqual( - log.output[0], - "%s:%s:Looking for versions at %s" % (level, session.__name__, - uri)) - - def test__parse_versions_response_no_json(self): - sot = session.Session(None) - retval = mock.Mock() - retval.json = mock.Mock(side_effect=ValueError) - sot.get = mock.Mock(return_value=retval) - - self.assertIsNone(sot._parse_versions_response("test")) - - def test__parse_versions_response_no_versions(self): - sot = session.Session(None) - retval = mock.Mock() - retval.json = mock.Mock(return_value={"no_versions_here": "blarga"}) - sot.get = mock.Mock(return_value=retval) - - self.assertIsNone(sot._parse_versions_response("test")) - - def test__parse_versions_response_with_versions(self): - uri = "http://openstack.org" - versions = [1, 2, 3] - - sot = session.Session(None) - retval = mock.Mock() - retval.json = mock.Mock(return_value={"versions": versions}) - sot.get = mock.Mock(return_value=retval) - - expected = session.Session._Endpoint(uri, versions) - self.assertEqual(expected, sot._parse_versions_response(uri)) - - def test__parse_versions_response_with_nested_versions(self): - uri = "http://openstack.org" - versions = [1, 2, 3] - - sot = session.Session(None) - retval = mock.Mock() - retval.json = mock.Mock(return_value={"versions": - {"values": versions}}) - sot.get = mock.Mock(return_value=retval) - - expected = session.Session._Endpoint(uri, versions) - self.assertEqual(expected, sot._parse_versions_response(uri)) - - def test__get_endpoint_versions_at_subdomain(self): - # This test covers a common case of services deployed under - # subdomains. Additionally, it covers the case of a service - # deployed at the root, which will be the first request made - # for versions. - sc_uri = "https://service.cloud.com/v1/" - versions_uri = "https://service.cloud.com" - - sot = session.Session(None) - sot.get_project_id = mock.Mock(return_value="project_id") - - responses = [session.Session._Endpoint(versions_uri, "versions")] - sot._parse_versions_response = mock.Mock(side_effect=responses) - - result = sot._get_endpoint_versions("type", sc_uri) - - sot._parse_versions_response.assert_called_once_with(versions_uri) - self.assertEqual(result, responses[0]) - self.assertFalse(result.needs_project_id) - - def test__get_endpoint_versions_at_path(self): - # This test covers a common case of services deployed under - # a path. Additionally, it covers the case of a service - # deployed at a path deeper than the root, which will mean - # more than one request will need to be made. - sc_uri = "https://cloud.com/api/service/v2/project_id" - versions_uri = "https://cloud.com/api/service" - - sot = session.Session(None) - sot.get_project_id = mock.Mock(return_value="project_id") - - responses = [None, None, - session.Session._Endpoint(versions_uri, "versions")] - sot._parse_versions_response = mock.Mock(side_effect=responses) - - result = sot._get_endpoint_versions("type", sc_uri) - - sot._parse_versions_response.assert_has_calls( - [mock.call("https://cloud.com"), - mock.call("https://cloud.com/api"), - mock.call(versions_uri)]) - self.assertEqual(result, responses[2]) - self.assertTrue(result.needs_project_id) - - def test__get_endpoint_versions_at_port(self): - # This test covers a common case of services deployed under - # a port. - sc_uri = "https://cloud.com:1234/v3" - versions_uri = "https://cloud.com:1234" - - sot = session.Session(None) - sot.get_project_id = mock.Mock(return_value="project_id") - - responses = [session.Session._Endpoint(versions_uri, "versions")] - sot._parse_versions_response = mock.Mock(side_effect=responses) - - result = sot._get_endpoint_versions("type", sc_uri) - - sot._parse_versions_response.assert_called_once_with(versions_uri) - self.assertEqual(result, responses[0]) - self.assertFalse(result.needs_project_id) - - def test__get_endpoint_versions_with_domain_scope(self): - # This test covers a common case of services deployed under - # subdomains. Additionally, it covers the case of getting endpoint - # versions with domain scope token - sc_uri = "https://service.cloud.com/identity" - versions_uri = "https://service.cloud.com" - - sot = session.Session(None) - # Project id is None when domain scope session present - sot.get_project_id = mock.Mock(return_value=None) - - responses = [session.Session._Endpoint(versions_uri, "versions")] - sot._parse_versions_response = mock.Mock(side_effect=responses) - - result = sot._get_endpoint_versions("type", sc_uri) - - sot._parse_versions_response.assert_called_once_with(versions_uri) - self.assertEqual(result, responses[0]) - self.assertFalse(result.needs_project_id) - self.assertIsNone(result.project_id) - - def test__parse_version(self): - sot = session.Session(None) - - self.assertEqual(sot._parse_version("2"), (2, -1)) - self.assertEqual(sot._parse_version("v2"), (2, -1)) - self.assertEqual(sot._parse_version("v2.1"), (2, 1)) - self.assertRaises(ValueError, sot._parse_version, "lol") - - def test__get_version_match_none(self): - sot = session.Session(None) - - endpoint = session.Session._Endpoint("root", []) - self.assertRaises( - exceptions.EndpointNotFound, - sot._get_version_match, endpoint, None, "service") - - def test__get_version_match_fuzzy(self): - match = "http://devstack/v2.1" - root_endpoint = "http://devstack" - versions = [{"id": "v2.0", - "links": [{"href": "http://devstack/v2/", - "rel": "self"}]}, - {"id": "v2.1", - "links": [{"href": match, - "rel": "self"}]}] - - sot = session.Session(None) - - endpoint = session.Session._Endpoint(root_endpoint, versions) - # Look for a v2 match, which we internally denote as a minor - # version of -1 so we can find the highest matching minor. - rv = sot._get_version_match(endpoint, session.Version(2, -1), - "service") - self.assertEqual(rv, match) - - def test__get_version_match_exact(self): - match = "http://devstack/v2" - root_endpoint = "http://devstack" - versions = [{"id": "v2.0", - "links": [{"href": match, - "rel": "self"}]}, - {"id": "v2.1", - "links": [{"href": "http://devstack/v2.1/", - "rel": "self"}]}] - - sot = session.Session(None) - endpoint = session.Session._Endpoint(root_endpoint, versions) - rv = sot._get_version_match(endpoint, session.Version(2, 0), - "service") - self.assertEqual(rv, match) - - def test__get_version_match_fragment(self): - root = "http://cloud.net" - match = "/v2" - versions = [{"id": "v2.0", "links": [{"href": match, "rel": "self"}]}] - - sot = session.Session(None) - endpoint = session.Session._Endpoint(root, versions) - rv = sot._get_version_match(endpoint, session.Version(2, 0), "service") - self.assertEqual(rv, root + match) - - def test__get_version_match_project_id(self): - match = "http://devstack/v2" - root_endpoint = "http://devstack" - project_id = "asdf123" - versions = [{"id": "v2.0", "links": [{"href": match, "rel": "self"}]}] - - sot = session.Session(None) - sot.get_project_id = mock.Mock(return_value=project_id) - endpoint = session.Session._Endpoint(root_endpoint, versions, - project_id=project_id, - needs_project_id=True) - rv = sot._get_version_match(endpoint, session.Version(2, 0), - "service") - match_endpoint = utils.urljoin(match, project_id) - self.assertEqual(rv, match_endpoint) - - def test_get_endpoint_cached(self): - sot = session.Session(None) - service_type = "compute" - interface = "public" - endpoint = "the world wide web" - - sot.endpoint_cache[(service_type, interface)] = endpoint - rv = sot.get_endpoint(service_type=service_type, interface=interface) - self.assertEqual(rv, endpoint) diff --git a/openstack/utils.py b/openstack/utils.py index 5aef9ba5f..f14c87e3b 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -44,6 +44,20 @@ def deprecated(deprecated_in=None, removed_in=None, details=details) +class NullHandler(logging.Handler): + def emit(self, record): + pass + + +def setup_logging(name): + '''Get a logging.Logger and make sure there is at least a NullHandler.''' + log = logging.getLogger(name) + if len(log.handlers) == 0: + h = NullHandler() + log.addHandler(h) + return log + + def enable_logging(debug=False, path=None, stream=None): """Enable logging to a file at path and/or a console stream. diff --git a/openstack/workflow/v2/execution.py b/openstack/workflow/v2/execution.py index d54160f1e..9877e2bc2 100644 --- a/openstack/workflow/v2/execution.py +++ b/openstack/workflow/v2/execution.py @@ -57,8 +57,7 @@ def create(self, session, prepend_key=True): prepend_key=prepend_key) request_body = request.body["execution"] - response = session.post(request.uri, - endpoint_filter=self.service, + response = session.post(request.url, json=request_body, headers=request.headers) diff --git a/openstack/workflow/v2/workflow.py b/openstack/workflow/v2/workflow.py index 6624c87b3..73f04b48e 100644 --- a/openstack/workflow/v2/workflow.py +++ b/openstack/workflow/v2/workflow.py @@ -60,11 +60,10 @@ def create(self, session, prepend_key=True): } scope = "?scope=%s" % self.scope - uri = request.uri + scope + uri = request.url + scope request.headers.update(headers) response = session.post(uri, - endpoint_filter=self.service, json=None, headers=request.headers, **kwargs) diff --git a/releasenotes/notes/removed-profile-437f3038025b0fb3.yaml b/releasenotes/notes/removed-profile-437f3038025b0fb3.yaml new file mode 100644 index 000000000..ce8a383b3 --- /dev/null +++ b/releasenotes/notes/removed-profile-437f3038025b0fb3.yaml @@ -0,0 +1,8 @@ +--- +upgrade: + - The Profile object has been replaced with the use of + CloudConfig objects from openstack.config. + - The openstacksdk specific Session object has been removed. + - Proxy objects are now subclasses of + keystoneauth1.adapter.Adapter. + - REST interactions all go through TaskManager now. diff --git a/releasenotes/notes/renamed-bare-metal-b1cdbc52af14e042.yaml b/releasenotes/notes/renamed-bare-metal-b1cdbc52af14e042.yaml new file mode 100644 index 000000000..d39f1a1cb --- /dev/null +++ b/releasenotes/notes/renamed-bare-metal-b1cdbc52af14e042.yaml @@ -0,0 +1,4 @@ +--- +upgrade: + - Renamed bare-metal to baremetal to align with the official + service type. diff --git a/releasenotes/notes/renamed-block-store-bc5e0a7315bfeb67.yaml b/releasenotes/notes/renamed-block-store-bc5e0a7315bfeb67.yaml new file mode 100644 index 000000000..3d5a5d34c --- /dev/null +++ b/releasenotes/notes/renamed-block-store-bc5e0a7315bfeb67.yaml @@ -0,0 +1,4 @@ +--- +upgrade: + - The block_store service object has been renamed to block_storage to + align the API with the official service types. diff --git a/releasenotes/notes/renamed-cluster-743da6d321fffcba.yaml b/releasenotes/notes/renamed-cluster-743da6d321fffcba.yaml new file mode 100644 index 000000000..0796d92b8 --- /dev/null +++ b/releasenotes/notes/renamed-cluster-743da6d321fffcba.yaml @@ -0,0 +1,4 @@ +--- +upgrade: + - Renamed cluster to clustering to align with the official + service type. diff --git a/releasenotes/notes/renamed-telemetry-c08ae3e72afca24f.yaml b/releasenotes/notes/renamed-telemetry-c08ae3e72afca24f.yaml new file mode 100644 index 000000000..5929d4f55 --- /dev/null +++ b/releasenotes/notes/renamed-telemetry-c08ae3e72afca24f.yaml @@ -0,0 +1,4 @@ +--- +upgrade: + - Renamed telemetry to meter to align with the official + service type. diff --git a/requirements.txt b/requirements.txt index bb087c608..c1b23e695 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ appdirs>=1.3.0 # MIT License requestsexceptions>=1.2.0 # Apache-2.0 jsonpatch>=1.16 # BSD six>=1.9.0 # MIT -stevedore>=1.20.0 # Apache-2.0 +os-service-types>=1.1.0 # Apache-2.0 keystoneauth1>=3.2.0 # Apache-2.0 deprecation>=1.0 # Apache-2.0 From c2de39de8b83611ed1020999c2b78228753c6f0c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 14 Nov 2017 15:50:43 -0600 Subject: [PATCH 1844/3836] Move task_manager and adapter up a level from cloud They've been integrated with the rest of things. Move them to better indicate that. Also, delete some dead code that was noticed while updating the docs. Change-Id: Ie92c3d74edc08d62aa2bd55da689a400740bab6e --- doc/source/contributor/coding.rst | 2 +- doc/source/user/logging.rst | 8 +------- openstack/{cloud => }/_adapter.py | 2 +- openstack/cloud/_tasks.py | 2 +- openstack/cloud/meta.py | 19 ------------------- openstack/cloud/openstackcloud.py | 4 ++-- openstack/connection.py | 2 +- openstack/proxy.py | 2 +- openstack/proxy2.py | 2 +- openstack/{cloud => }/task_manager.py | 0 .../tests/unit/cloud/test_task_manager.py | 2 +- .../tests/unit/{cloud => }/test__adapter.py | 2 +- 12 files changed, 11 insertions(+), 36 deletions(-) rename openstack/{cloud => }/_adapter.py (98%) rename openstack/{cloud => }/task_manager.py (100%) rename openstack/tests/unit/{cloud => }/test__adapter.py (97%) diff --git a/doc/source/contributor/coding.rst b/doc/source/contributor/coding.rst index 1cb45b9cc..7ee1f557b 100644 --- a/doc/source/contributor/coding.rst +++ b/doc/source/contributor/coding.rst @@ -63,7 +63,7 @@ Returned Resources ------------------ Complex objects returned to the caller must be a `munch.Munch` type. The -`openstack.cloud._adapter.Adapter` class makes resources into `munch.Munch`. +`openstack._adapter.ShadeAdapter` class makes resources into `munch.Munch`. All objects should be normalized. It is shade's purpose in life to make OpenStack consistent for end users, and this means not trusting the clouds diff --git a/doc/source/user/logging.rst b/doc/source/user/logging.rst index 0d6276e2e..57bf99f61 100644 --- a/doc/source/user/logging.rst +++ b/doc/source/user/logging.rst @@ -44,7 +44,7 @@ There are additional sub-loggers that are used at times, primarily so that a user can decide to turn on or off a specific type of logging. They are listed below. -openstack.cloud.task_manager +openstack.task_manager `openstack.cloud` uses a Task Manager to perform remote calls. The `openstack.cloud.task_manager` logger emits messages at the start and end of each Task announcing what it is going to run and then what it ran and @@ -52,12 +52,6 @@ openstack.cloud.task_manager get a trace of external actions `openstack.cloud` is taking without full `HTTP Tracing`_. -openstack.cloud.request_ids - The `openstack.cloud.request_ids` logger emits a log line at the end of each - HTTP interaction with the OpenStack Request ID associated with the - interaction. This can be be useful for tracking action taken on the - server-side if one does not want `HTTP Tracing`_. - openstack.cloud.exc If `log_inner_exceptions` is set to True, `shade` will emit any wrapped exception to the `openstack.cloud.exc` logger. Wrapped exceptions are usually diff --git a/openstack/cloud/_adapter.py b/openstack/_adapter.py similarity index 98% rename from openstack/cloud/_adapter.py rename to openstack/_adapter.py index 8807329f3..4a375b10e 100644 --- a/openstack/cloud/_adapter.py +++ b/openstack/_adapter.py @@ -19,8 +19,8 @@ from keystoneauth1 import adapter -from openstack.cloud import task_manager as _task_manager from openstack import exceptions +from openstack import task_manager as _task_manager def _extract_name(url): diff --git a/openstack/cloud/_tasks.py b/openstack/cloud/_tasks.py index 294fae3ba..3ad6b8592 100644 --- a/openstack/cloud/_tasks.py +++ b/openstack/cloud/_tasks.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from openstack.cloud import task_manager +from openstack import task_manager class MachineCreate(task_manager.Task): diff --git a/openstack/cloud/meta.py b/openstack/cloud/meta.py index 001eb616b..957fea6a8 100644 --- a/openstack/cloud/meta.py +++ b/openstack/cloud/meta.py @@ -486,25 +486,6 @@ def get_hostvars_from_server(cloud, server, mounts=None): return server_vars -def _log_request_id(obj, request_id): - if request_id: - # Log the request id and object id in a specific logger. This way - # someone can turn it on if they're interested in this kind of tracing. - log = _log.setup_logging('openstack.cloud.request_ids') - obj_id = None - if isinstance(obj, dict): - obj_id = obj.get('id', obj.get('uuid')) - if obj_id: - log.debug("Retrieved object %(id)s. Request ID %(request_id)s", - {'id': obj.get('id', obj.get('uuid')), - 'request_id': request_id}) - else: - log.debug("Retrieved a response. Request ID %(request_id)s", - {'request_id': request_id}) - - return obj - - def obj_to_munch(obj): """ Turn an object with attributes into a dict suitable for serializing. diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index a2e3dff7f..b9ed640d1 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -35,15 +35,15 @@ import keystoneauth1.exceptions import openstack +from openstack import _adapter from openstack import _log from openstack.cloud.exc import * # noqa -from openstack.cloud import _adapter from openstack.cloud._heat import event_utils from openstack.cloud._heat import template_utils from openstack.cloud import _normalize from openstack.cloud import meta -from openstack.cloud import task_manager from openstack.cloud import _utils +from openstack import task_manager # TODO(shade) shade keys were x-object-meta-x-sdk-md5 - we need to add those # to freshness checks so that a shade->sdk transition doens't diff --git a/openstack/connection.py b/openstack/connection.py index beb071f2e..90631a727 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -79,12 +79,12 @@ import keystoneauth1.exceptions import os_service_types -from openstack.cloud import task_manager import openstack.config from openstack.config import cloud_config from openstack import exceptions from openstack import proxy from openstack import proxy2 +from openstack import task_manager from openstack import utils _logger = logging.getLogger(__name__) diff --git a/openstack/proxy.py b/openstack/proxy.py index 24123f377..f2f286ce5 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -9,7 +9,7 @@ # 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.cloud import _adapter +from openstack import _adapter from openstack import exceptions from openstack import resource diff --git a/openstack/proxy2.py b/openstack/proxy2.py index 739c8510d..c6295699f 100644 --- a/openstack/proxy2.py +++ b/openstack/proxy2.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.cloud import _adapter +from openstack import _adapter from openstack import exceptions from openstack import resource2 from openstack import utils diff --git a/openstack/cloud/task_manager.py b/openstack/task_manager.py similarity index 100% rename from openstack/cloud/task_manager.py rename to openstack/task_manager.py diff --git a/openstack/tests/unit/cloud/test_task_manager.py b/openstack/tests/unit/cloud/test_task_manager.py index f78d5bb4e..64e521487 100644 --- a/openstack/tests/unit/cloud/test_task_manager.py +++ b/openstack/tests/unit/cloud/test_task_manager.py @@ -16,7 +16,7 @@ import concurrent.futures import mock -from openstack.cloud import task_manager +from openstack import task_manager from openstack.tests.unit import base diff --git a/openstack/tests/unit/cloud/test__adapter.py b/openstack/tests/unit/test__adapter.py similarity index 97% rename from openstack/tests/unit/cloud/test__adapter.py rename to openstack/tests/unit/test__adapter.py index aaca16854..67cbbf744 100644 --- a/openstack/tests/unit/cloud/test__adapter.py +++ b/openstack/tests/unit/test__adapter.py @@ -12,7 +12,7 @@ from testscenarios import load_tests_apply_scenarios as load_tests # noqa -from openstack.cloud import _adapter +from openstack import _adapter from openstack.tests.unit import base From c469caedc7886133cbc8dd6b8538594e9a3aa60b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 11 Nov 2017 09:25:06 -0600 Subject: [PATCH 1845/3836] Add notes about moving forward As we look at this stack, having a summary and then a path forward seemed like a good idea. Change-Id: Ie43bf0e1f911a452c3ad101e804a8cb03a84ca36 --- SHADE-MERGE-TODO.rst | 120 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 SHADE-MERGE-TODO.rst diff --git a/SHADE-MERGE-TODO.rst b/SHADE-MERGE-TODO.rst new file mode 100644 index 000000000..61cc4e1a8 --- /dev/null +++ b/SHADE-MERGE-TODO.rst @@ -0,0 +1,120 @@ +Tasks Needed for merging shade and openstacksdk +=============================================== + +A large portion of the important things have already been done in the stack +leading up to this change. For reference, those are: + +* shade and os-client-config library content have been merged into the tree. +* Use official service-type names from Service Types Authority via + os-service-types to refer to services and proxies. +* Automatically also add properties to the connection for every known alias + for each service-type. +* Made openstack.proxy.Proxy a subclass of keystoneauth1.adapter.Adapter. + Removed local logic that duplicates keystoneauth logic. This means every + proxy also has direct REST primitives available. For example: + + .. code-block:: python + + connection = connection.Connection() + servers = connection.compute.list_servers() + server_response = connection.compute.get('/servers') + +* Removed the Profile object in favor of openstack.config. +* Removed the Session object in favor of using keystoneauth. +* Plumbed Proxy use of Adapter through the Adapter subclass from shade that + uses the TaskManager to run REST calls. + +Next steps +========== + +* Finish migrating to Resource2 and Proxy2, rename them to Resource and Proxy. +* Rename self.session and session parameter in all usage in proxy and resource + to self.adapter. +* Migrate unit tests to requests-mock instead of mocking python calls to + session. +* Consider removing ServiceFilter and the various Service objects if an + acceptable plan can be found for using discovery. +* Replace _prepare_request with requests.Session.prepare_request. + +Service Proxies +--------------- + +* Authenticate at Connection() creation time. Having done that, use the + catalog in the token to determine which service proxies to add to the + Connection object. +* Filter the above service list from the token by has_service() from + openstack.config. +* Add a has_service method to Connection which will BASICALLY just be + hasattr(self, 'service') - but will look nicer. +* Consider adding magic to Connection for every service that a given cloud + DOESN'T have that will throw an exception on any attribute access that is + "cloud doesn't have service blah" rather than simply Attribute Not Found. + The SDK has a python api regardless of the services remotely, it would be + nice if trimming the existing attribute list wouldn't make it impossible for + someone to validate their code correctness. It's also possible that instead + of not having services, we always mount proxy objects for every service, but + we mount a "NotFound" proxy for each service that isn't there. +* Since openstacksdk uses version discovery now, there is always a good path + to "the" version of a given service. However, a cloud may have more than one. + Attach the discovered service proxy to connection as today under the service + type name. Add a property to each service proxy for each version the SDK + knows about. For instance: + + .. code-block:: python + + connection = openstack.Connection() + connection.volume # openstack.volume.v3._proxy + connection.volume.v2 # openstack.volume.v2._proxy + connection.volume.v3 # openstack.volume.v3._proxy + + Those versioned proxies should be done as Adapters with min and max version + set explicitly. This should allow a common pattern for people to write code + that just wants to use the discovered or configured service, or who want to + attempt to use a specific version of the API if they know what they're doing + and at the very least wind up with a properly configured Adapter they can + make rest calls on. Because: + + .. code-block:: python + + connection = openstack.Connection() + connection.dns.v2.get('/zones') + + should always work on an OpenStack cloud with designate even if the SDK + authors don't know anything about Designate and haven't added Resource or + Proxy explicitly for it. +* Decide what todo about non-OpenStack services. Do we add base Proxy + properties to Connection for every service we find in the catalog regardless + of official/non-official? If so, do we let someone pass a dict of + service-type, Proxy to connection that would let the provide a local service + we don't know about? If we do that- we should disallow passing in overrides + for services we DO know about to discourage people writing local tools that + have different Compute behavior, for instance. + +Microversions +------------- + +* keystoneauth.adapter.Adapter knows how to send microversion headers, and + get_endpoint_data knows how to fetch supported ranges. As microversion + support is added to calls, it needs to be on a per-request basis. This + has implications to both Resource and Proxy, as cloud payloads for data + mapping can be different on a per-microversion basis. + +shade integration +----------------- + +* Add support for shade expressing normalization model/contract into Resource. +* Make a plan for normalization supporting shade users continuing + to get shade normalized resource Munch objects from shade API calls, sdk + proxy/resource users getting SDK objects, and both of them being able to opt + in to "strict" normalization at Connection constructor time. Perhaps making + Resource subclass Munch would allow mixed use? Needs investigation. +* Investigate auto-generating the bulk of shade's API based on introspection of + SDK objects, leaving only the code with extra special logic in shade itself. +* Rationalize openstack.util.enable_logging and shade.simple_logging. + +caching +------- + +* Make a plan for caching that can work with shade's batched-access/client-side + rate-limiting, per-resource configurable caching and direct get models. It + may want to actually live in keystoneauth. From 43eff7bb9bf5ae969f9452cf4799dc56cca0ed12 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 15 Nov 2017 12:40:48 -0600 Subject: [PATCH 1846/3836] Remove bogus and unneeded role from job definition We get the devstack roles for free since we're using the devstack job as a parent. Also, the repo location was incorrect anyway. Change-Id: I75842ff0bb28ffed7336cf77ea9535ece9e67dc3 --- .zuul.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 1309cc7b0..cad7ab651 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -62,8 +62,6 @@ override-branch: master - name: openstack/heat - name: openstack/swift - roles: - - zuul: openstack-infra/devstack timeout: 9000 vars: devstack_localrc: From 3070a6e1ccd224911bd30dc52d5bbe32f9f6195b Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Tue, 10 Oct 2017 13:54:26 +0100 Subject: [PATCH 1847/3836] Avoid default mutable values in arguments Mutable values shouldn't be used as default values in function arguments [1]. [1] http://docs.python-guide.org/en/latest/writing/gotchas/ Change-Id: I25774e634d1489059225af545f780d8a285decef --- openstack/clustering/v1/_proxy.py | 3 ++- openstack/proxy.py | 3 ++- openstack/proxy2.py | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/openstack/clustering/v1/_proxy.py b/openstack/clustering/v1/_proxy.py index 01602607d..95e444b85 100644 --- a/openstack/clustering/v1/_proxy.py +++ b/openstack/clustering/v1/_proxy.py @@ -1089,7 +1089,7 @@ def events(self, **query): """ return self._list(_event.Event, paginated=True, **query) - def wait_for_status(self, resource, status, failures=[], interval=2, + def wait_for_status(self, resource, status, failures=None, interval=2, wait=120): """Wait for a resource to be in a particular status. @@ -1111,6 +1111,7 @@ def wait_for_status(self, resource, status, failures=[], interval=2, :raises: :class:`~AttributeError` if the resource does not have a ``status`` attribute. """ + failures = [] if failures is None else failures return resource2.wait_for_status(self, resource, status, failures, interval, wait) diff --git a/openstack/proxy.py b/openstack/proxy.py index 24123f377..58537c0c8 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -246,7 +246,7 @@ def _head(self, resource_type, value=None, path_args=None): return res.head(self) - def wait_for_status(self, value, status, failures=[], interval=2, + def wait_for_status(self, value, status, failures=None, interval=2, wait=120): """Wait for a resource to be in a particular status. @@ -267,6 +267,7 @@ def wait_for_status(self, value, status, failures=[], interval=2, :raises: :class:`~AttributeError` if the resource does not have a status attribute """ + failures = [] if failures is None else failures return resource.wait_for_status(self, value, status, failures, interval, wait) diff --git a/openstack/proxy2.py b/openstack/proxy2.py index 739c8510d..f63358f84 100644 --- a/openstack/proxy2.py +++ b/openstack/proxy2.py @@ -264,7 +264,7 @@ def _head(self, resource_type, value=None, **attrs): "service-specific subclasses should expose " "this as needed. See resource2.wait_for_status " "for this behavior")) - def wait_for_status(self, value, status, failures=[], interval=2, + def wait_for_status(self, value, status, failures=None, interval=2, wait=120): """Wait for a resource to be in a particular status. @@ -285,6 +285,7 @@ def wait_for_status(self, value, status, failures=[], interval=2, :raises: :class:`~AttributeError` if the resource does not have a status attribute """ + failures = [] if failures is None else failures return resource2.wait_for_status(self, value, status, failures, interval, wait) From 2dfe498df28cb0bc28c5bc386ad2653c6b058ddc Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 16 Nov 2017 11:26:05 +0000 Subject: [PATCH 1848/3836] Updated from global requirements Change-Id: I3142490cf4b7660ccb59ba7f06f87d3ddd620459 --- requirements.txt | 4 ++-- test-requirements.txt | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index c1b23e695..c19e2a649 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,8 +6,8 @@ PyYAML>=3.10 # MIT appdirs>=1.3.0 # MIT License requestsexceptions>=1.2.0 # Apache-2.0 jsonpatch>=1.16 # BSD -six>=1.9.0 # MIT -os-service-types>=1.1.0 # Apache-2.0 +six>=1.10.0 # MIT +os-service-types>=1.1.0 # Apache-2.0 keystoneauth1>=3.2.0 # Apache-2.0 deprecation>=1.0 # Apache-2.0 diff --git a/test-requirements.txt b/test-requirements.txt index 4684d5266..c7e37a36e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,21 +7,21 @@ beautifulsoup4>=4.6.0 # MIT coverage!=4.4,>=4.0 # Apache-2.0 doc8>=0.6.0 # Apache-2.0 docutils>=0.11 # OSI-Approved Open Source, Public Domain -extras>=0.0.3 # MIT +extras>=1.0.0 # MIT fixtures>=3.0.0 # Apache-2.0/BSD jsonschema<3.0.0,>=2.6.0 # MIT mock>=2.0.0 # BSD -python-subunit>=0.0.18 # Apache-2.0/BSD +python-subunit>=1.0.0 # Apache-2.0/BSD openstackdocstheme>=1.17.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 reno>=2.5.0 # Apache-2.0 requests-mock>=1.1.0 # Apache-2.0 # Install shade for tests until the ansible modules import openstack -shade>=1.17.0 # Apache-2.0 +shade>=1.17.0 # Apache-2.0 sphinx>=1.6.2 # BSD stestr>=1.0.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD -testtools>=1.4.0 # MIT -python-glanceclient>=2.8.0 # Apache-2.0 +testtools>=2.2.0 # MIT +python-glanceclient>=2.8.0 # Apache-2.0 python-ironicclient>=1.14.0 # Apache-2.0 From 688fc5a6090d22919ef85c8c43c7a6afbe183c3a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 16 Nov 2017 08:26:26 -0600 Subject: [PATCH 1849/3836] Remove ansible functional tests for now These aren't actually testing openstacksdk in any way at the moment, and having them is making us have a test-requirements depend on shade which will make adding openstacksdk as a shade requirement a circular dependency. Remove them for now. To re-add them, we want to: - add openstacksdk as a shade requirement - update ansible to import openstack instead of import shade Once that's done, we can add the tests back, but without adding shade as an sdk test-requirement. That way we can have shade test that the ansible modules work if someone installs shade and doesn't explicitly install sdk, and we can have sdk test that the modules work if someone installs sdk and not shade. Change-Id: Icb26f9c066bad6c2c045ac949ac1864e26b5b837 --- .zuul.yaml | 28 ++-------------------------- test-requirements.txt | 2 -- 2 files changed, 2 insertions(+), 28 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index cad7ab651..0633b1c76 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -181,28 +181,6 @@ tox_environment: OPENSTACKSDK_HAS_SWIFT: 0 OPENSTACKSDK_HAS_MAGNUM: 1 - voting: false - -- job: - name: openstacksdk-ansible-functional-devstack - parent: openstacksdk-functional-devstack - description: | - Run openstacksdk ansible functional tests against a master devstack - using released version of ansible. - vars: - tox_envlist: ansible - -- job: - name: openstacksdk-ansible-devel-functional-devstack - parent: openstacksdk-ansible-functional-devstack - description: | - Run openstacksdk ansible functional tests against a master devstack - using git devel branch version of ansible. - # required-projects: - # - github.com/ansible/ansible - voting: false - vars: - tox_install_siblings: true - project-template: name: openstacksdk-functional-tips @@ -223,13 +201,11 @@ - openstacksdk-tox-tips check: jobs: - - openstacksdk-ansible-devel-functional-devstack - - openstacksdk-ansible-functional-devstack - openstacksdk-functional-devstack - - openstacksdk-functional-devstack-magnum + - openstacksdk-functional-devstack-magnum: + voting: false - openstacksdk-functional-devstack-python3 gate: jobs: - - openstacksdk-ansible-functional-devstack - openstacksdk-functional-devstack - openstacksdk-functional-devstack-python3 diff --git a/test-requirements.txt b/test-requirements.txt index c7e37a36e..d77c48500 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -16,8 +16,6 @@ openstackdocstheme>=1.17.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 reno>=2.5.0 # Apache-2.0 requests-mock>=1.1.0 # Apache-2.0 -# Install shade for tests until the ansible modules import openstack -shade>=1.17.0 # Apache-2.0 sphinx>=1.6.2 # BSD stestr>=1.0.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD From 5fd5e77cf4f8344063d02e2ae8798c218d64cf8d Mon Sep 17 00:00:00 2001 From: "Yuanbin.Chen" Date: Fri, 17 Nov 2017 10:37:17 +0800 Subject: [PATCH 1850/3836] Add cluster support force delete parameter when cluster/node delete Change-Id: Ifb5eb4167ae688c495bc5e59cef6ee1f5ccbcd02 Signed-off-by: Yuanbin.Chen --- openstack/clustering/v1/_proxy.py | 31 +++++++++++++------ openstack/clustering/v1/cluster.py | 8 +++++ openstack/clustering/v1/node.py | 8 +++++ .../tests/unit/cluster/v1/test_cluster.py | 16 ++++++++++ openstack/tests/unit/cluster/v1/test_node.py | 16 ++++++++++ openstack/tests/unit/cluster/v1/test_proxy.py | 10 ++++++ 6 files changed, 80 insertions(+), 9 deletions(-) diff --git a/openstack/clustering/v1/_proxy.py b/openstack/clustering/v1/_proxy.py index 95e444b85..cb9aca317 100644 --- a/openstack/clustering/v1/_proxy.py +++ b/openstack/clustering/v1/_proxy.py @@ -196,21 +196,27 @@ def create_cluster(self, **attrs): """ return self._create(_cluster.Cluster, **attrs) - def delete_cluster(self, cluster, ignore_missing=True): + def delete_cluster(self, cluster, ignore_missing=True, force_delete=False): """Delete a cluster. :param cluster: The value can be either the name or ID of a cluster or - a :class:`~openstack.clustering.v1.cluster.Cluster` instance. + a :class:`~openstack.cluster.v1.cluster.Cluster` instance. :param bool ignore_missing: When set to ``False``, an exception :class:`~openstack.exceptions.ResourceNotFound` will be raised when the cluster could not be found. When set to ``True``, no exception will be raised when attempting to delete a non-existent cluster. + :param bool force_delete: When set to ``True``, the cluster deletion + will be forced immediately. :returns: The instance of the Cluster which was deleted. - :rtype: :class:`~openstack.clustering.v1.cluster.Cluster`. + :rtype: :class:`~openstack.cluster.v1.cluster.Cluster`. """ - return self._delete(_cluster.Cluster, cluster, - ignore_missing=ignore_missing) + if force_delete: + server = self._get_resource(_cluster.Cluster, cluster) + return server.force_delete(self) + else: + return self._delete(_cluster.Cluster, cluster, + ignore_missing=ignore_missing) def find_cluster(self, name_or_id, ignore_missing=True): """Find a single cluster. @@ -614,20 +620,27 @@ def create_node(self, **attrs): """ return self._create(_node.Node, **attrs) - def delete_node(self, node, ignore_missing=True): + def delete_node(self, node, ignore_missing=True, force_delete=False): """Delete a node. :param node: The value can be either the name or ID of a node or a - :class:`~openstack.clustering.v1.node.Node` instance. + :class:`~openstack.cluster.v1.node.Node` instance. :param bool ignore_missing: When set to ``False``, an exception :class:`~openstack.exceptions.ResourceNotFound` will be raised when the node could not be found. When set to ``True``, no exception will be raised when attempting to delete a non-existent node. + :param bool force_delete: When set to ``True``, the node deletion + will be forced immediately. :returns: The instance of the Node which was deleted. - :rtype: :class:`~openstack.clustering.v1.node.Node`. + :rtype: :class:`~openstack.cluster.v1.node.Node`. """ - return self._delete(_node.Node, node, ignore_missing=ignore_missing) + if force_delete: + server = self._get_resource(_node.Node, node) + return server.force_delete(self) + else: + return self._delete(_node.Node, node, + ignore_missing=ignore_missing) def find_node(self, name_or_id, ignore_missing=True): """Find a single node. diff --git a/openstack/clustering/v1/cluster.py b/openstack/clustering/v1/cluster.py index 7563d0a07..8b6561ba5 100644 --- a/openstack/clustering/v1/cluster.py +++ b/openstack/clustering/v1/cluster.py @@ -181,3 +181,11 @@ def op(self, session, operation, **params): resp = session.post(url, json={operation: params}) return resp.json() + + def force_delete(self, session): + """Force delete a cluster.""" + body = {'force': True} + url = utils.urljoin(self.base_path, self.id) + resp = session.delete(url, json=body) + self._translate_response(resp) + return self diff --git a/openstack/clustering/v1/node.py b/openstack/clustering/v1/node.py index 1e3208ad8..c0108df04 100644 --- a/openstack/clustering/v1/node.py +++ b/openstack/clustering/v1/node.py @@ -156,6 +156,14 @@ def adopt(self, session, preview=False, **params): self._translate_response(resp) return self + def force_delete(self, session): + """Force delete a node.""" + body = {'force': True} + url = utils.urljoin(self.base_path, self.id) + resp = session.delete(url, json=body) + self._translate_response(resp) + return self + class NodeDetail(Node): base_path = '/nodes/%(node_id)s?show_details=True' diff --git a/openstack/tests/unit/cluster/v1/test_cluster.py b/openstack/tests/unit/cluster/v1/test_cluster.py index 31f34cab7..4c4cf55b5 100644 --- a/openstack/tests/unit/cluster/v1/test_cluster.py +++ b/openstack/tests/unit/cluster/v1/test_cluster.py @@ -307,3 +307,19 @@ def test_operation(self): body = {'dance': {'style': 'tango'}} sess.post.assert_called_once_with(url, json=body) + + def test_force_delete(self): + sot = cluster.Cluster(**FAKE) + + resp = mock.Mock() + resp.headers = {} + resp.json = mock.Mock(return_value={"foo": "bar"}) + resp.status_code = 200 + sess = mock.Mock() + sess.delete = mock.Mock(return_value=resp) + + res = sot.force_delete(sess) + self.assertEqual(sot, res) + url = 'clusters/%s' % sot.id + body = {'force': True} + sess.delete.assert_called_once_with(url, json=body) diff --git a/openstack/tests/unit/cluster/v1/test_node.py b/openstack/tests/unit/cluster/v1/test_node.py index 3aa3b26ae..9e6cbf7aa 100644 --- a/openstack/tests/unit/cluster/v1/test_node.py +++ b/openstack/tests/unit/cluster/v1/test_node.py @@ -139,6 +139,22 @@ def test_adopt(self): sess.post.assert_called_once_with("nodes/adopt", json={"param": "value"}) + def test_force_delete(self): + sot = node.Node(**FAKE) + + resp = mock.Mock() + resp.headers = {} + resp.json = mock.Mock(return_value={"foo": "bar"}) + resp.status_code = 200 + sess = mock.Mock() + sess.delete = mock.Mock(return_value=resp) + + res = sot.force_delete(sess) + self.assertEqual(sot, res) + url = 'nodes/%s' % sot.id + body = {'force': True} + sess.delete.assert_called_once_with(url, json=body) + class TestNodeDetail(testtools.TestCase): diff --git a/openstack/tests/unit/cluster/v1/test_proxy.py b/openstack/tests/unit/cluster/v1/test_proxy.py index 905a46eae..592509b1f 100644 --- a/openstack/tests/unit/cluster/v1/test_proxy.py +++ b/openstack/tests/unit/cluster/v1/test_proxy.py @@ -94,6 +94,11 @@ def test_cluster_delete(self): def test_cluster_delete_ignore(self): self.verify_delete(self.proxy.delete_cluster, cluster.Cluster, True) + def test_cluster_force_delete(self): + self._verify("openstack.clustering.v1.cluster.Cluster.force_delete", + self.proxy.delete_cluster, + method_args=["value", False, True]) + def test_cluster_find(self): self.verify_find(self.proxy.find_cluster, cluster.Cluster) @@ -349,6 +354,11 @@ def test_node_delete(self): def test_node_delete_ignore(self): self.verify_delete(self.proxy.delete_node, node.Node, True) + def test_node_force_delete(self): + self._verify("openstack.clustering.v1.node.Node.force_delete", + self.proxy.delete_node, + method_args=["value", False, True]) + def test_node_find(self): self.verify_find(self.proxy.find_node, node.Node) From cc53432ca49ef25fa8b8a38a4dbba59d92990559 Mon Sep 17 00:00:00 2001 From: rajat29 Date: Fri, 17 Nov 2017 15:36:52 +0530 Subject: [PATCH 1851/3836] Remove setting of version/release from releasenotes Release notes are version independent, so remove version/release values. We've found that projects now require the service package to be installed in order to build release notes, and this is entirely due to the current convention of pulling in the version information. Release notes should not need installation in order to build, so this unnecessary version setting needs to be removed. This is needed for new release notes publishing, see I56909152975f731a9d2c21b2825b972195e48ee8 and the discussion starting at http://lists.openstack.org/pipermail/openstack-dev/2017-November/124480.html Change-Id: Id0925ef2efce76024dbf1d519e92be3fdcff940b --- releasenotes/source/conf.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py index 1a6db74cd..0f0a0f713 100644 --- a/releasenotes/source/conf.py +++ b/releasenotes/source/conf.py @@ -64,16 +64,11 @@ project = u'OpenStack SDK Release Notes' copyright = u'2017, Various members of the OpenStack Foundation' -# 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. -# -import pbr.version -version_info = pbr.version.VersionInfo('openstacksdk') -# The full version, including alpha/beta/rc tags. -release = version_info.version_string_with_vcs() +# Release notes are version independent. # The short X.Y version. -version = version_info.canonical_version_string() +version = '' +# The full version, including alpha/beta/rc tags. +release = '' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From bc2e1bef7b1436dd7e4f882fc086948b8516bf6a Mon Sep 17 00:00:00 2001 From: "Yuanbin.Chen" Date: Mon, 13 Nov 2017 15:02:59 +0800 Subject: [PATCH 1852/3836] Add block_store support single volume display image message This patch fix When execute nova rebuild operation, the nova vm image message don't get from show volume. Change-Id: I5f252b34eb9ca43ea72b42c78e4b0b66ca67b43b Signed-off-by: Yuanbin.Chen --- openstack/block_storage/v2/volume.py | 2 ++ openstack/tests/unit/block_storage/v2/test_volume.py | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/openstack/block_storage/v2/volume.py b/openstack/block_storage/v2/volume.py index 82ef84071..ee8c60d62 100644 --- a/openstack/block_storage/v2/volume.py +++ b/openstack/block_storage/v2/volume.py @@ -63,6 +63,8 @@ class Volume(resource2.Resource): is_bootable = resource2.Body("bootable", type=format.BoolStr) #: One or more metadata key and value pairs to associate with the volume. metadata = resource2.Body("metadata") + #: One or more metadata key and value pairs about image + volume_image_metadata = resource2.Body("volume_image_metadata") #: One of the following values: creating, available, attaching, in-use #: deleting, error, error_deleting, backing-up, restoring-backup, diff --git a/openstack/tests/unit/block_storage/v2/test_volume.py b/openstack/tests/unit/block_storage/v2/test_volume.py index 70b0ce8e8..9c3207b8c 100644 --- a/openstack/tests/unit/block_storage/v2/test_volume.py +++ b/openstack/tests/unit/block_storage/v2/test_volume.py @@ -17,6 +17,14 @@ from openstack.block_storage.v2 import volume FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff" +IMAGE_METADATA = { + 'container_format': 'bare', + 'min_ram': '64', 'disk_format': u'qcow2', + 'image_name': 'TestVM', + 'image_id': '625d4f2c-cf67-4af3-afb6-c7220f766947', + 'checksum': '64d7c1cd2b6f60c92c14662941cb7913', + 'min_disk': '0', u'size': '13167616' +} VOLUME = { "status": "creating", @@ -31,6 +39,7 @@ "source_volid": None, "imageRef": "some_image", "metadata": {}, + "volume_image_metadata": IMAGE_METADATA, "id": FAKE_ID, "size": 10 } @@ -87,6 +96,8 @@ def test_create(self): self.assertEqual(VOLUME["snapshot_id"], sot.snapshot_id) self.assertEqual(VOLUME["source_volid"], sot.source_volume_id) self.assertEqual(VOLUME["metadata"], sot.metadata) + self.assertEqual(VOLUME["volume_image_metadata"], + sot.volume_image_metadata) self.assertEqual(VOLUME["size"], sot.size) self.assertEqual(VOLUME["imageRef"], sot.image_id) From 4419c553a458c6f22770327b25019329098cb740 Mon Sep 17 00:00:00 2001 From: Dongcan Ye Date: Mon, 20 Nov 2017 13:50:26 +0800 Subject: [PATCH 1853/3836] Add subnet_id property for FloatingIP In openstackclient we can pass a "subnet_id" param while creating floatingip, but missing in FloatingIP properties. Change-Id: I5c2b93d7e737755958ff074f3a84e592c94f0fe2 Closes-Bug: #1733258 --- openstack/network/v2/floating_ip.py | 7 +++++-- openstack/tests/unit/network/v2/test_floating_ip.py | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/openstack/network/v2/floating_ip.py b/openstack/network/v2/floating_ip.py index 9a7385096..79c3dd41a 100644 --- a/openstack/network/v2/floating_ip.py +++ b/openstack/network/v2/floating_ip.py @@ -30,8 +30,9 @@ class FloatingIP(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'description', 'fixed_ip_address', 'floating_ip_address', - 'floating_network_id', 'port_id', 'router_id', 'status', + 'description', 'fixed_ip_address', + 'floating_ip_address', 'floating_network_id', + 'port_id', 'router_id', 'status', 'subnet_id', project_id='tenant_id') # Properties @@ -65,6 +66,8 @@ class FloatingIP(resource.Resource): status = resource.Body('status') #: Timestamp at which the floating IP was last updated. updated_at = resource.Body('updated_at') + #: The Subnet ID associated with the floating IP. + subnet_id = resource.Body('subnet_id') @classmethod def find_available(cls, session): diff --git a/openstack/tests/unit/network/v2/test_floating_ip.py b/openstack/tests/unit/network/v2/test_floating_ip.py index a2448ac68..a1f013e03 100644 --- a/openstack/tests/unit/network/v2/test_floating_ip.py +++ b/openstack/tests/unit/network/v2/test_floating_ip.py @@ -29,6 +29,7 @@ 'status': 'ACTIVE', 'revision_number': 12, 'updated_at': '13', + 'subnet_id': '14' } @@ -62,6 +63,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['status'], sot.status) self.assertEqual(EXAMPLE['revision_number'], sot.revision_number) self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) + self.assertEqual(EXAMPLE['subnet_id'], sot.subnet_id) def test_find_available(self): mock_session = mock.Mock() From 1a8cd8419946e2d9374d6269220181e49df61ed0 Mon Sep 17 00:00:00 2001 From: Steven Relf Date: Thu, 21 Sep 2017 14:53:51 +0100 Subject: [PATCH 1854/3836] Adds support to retrieve cinder backend pools information This code additiona allows the collection of cinder backend pool stats directly from the api. This eliminates the need to poll for this data directly from ceph or other storage services. Change-Id: Iabf7a6a5143fc61ec044d6b672a18cfc951219d7 --- doc/source/users/proxies/block_storage.rst | 7 +++ openstack/block_storage/v2/_proxy.py | 8 ++++ openstack/block_storage/v2/stats.py | 34 ++++++++++++++ .../functional/block_store/v2/test_stats.py | 45 ++++++++++++++++++ .../tests/unit/block_storage/v2/test_proxy.py | 5 ++ .../tests/unit/block_store/v2/test_stats.py | 47 +++++++++++++++++++ 6 files changed, 146 insertions(+) create mode 100644 openstack/block_storage/v2/stats.py create mode 100644 openstack/tests/functional/block_store/v2/test_stats.py create mode 100644 openstack/tests/unit/block_store/v2/test_stats.py diff --git a/doc/source/users/proxies/block_storage.rst b/doc/source/users/proxies/block_storage.rst index 460624151..ba9f7e355 100644 --- a/doc/source/users/proxies/block_storage.rst +++ b/doc/source/users/proxies/block_storage.rst @@ -41,3 +41,10 @@ Snapshot Operations .. automethod:: openstack.block_storage.v2._proxy.Proxy.delete_snapshot .. automethod:: openstack.block_storage.v2._proxy.Proxy.get_snapshot .. automethod:: openstack.block_storage.v2._proxy.Proxy.snapshots + +Stats Operations +^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v2._proxy.Proxy + + .. automethod:: openstack.block_storage.v2._proxy.Proxy.backend_pools diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 202a2d8a0..90bb623b3 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -11,6 +11,7 @@ # under the License. from openstack.block_storage.v2 import snapshot as _snapshot +from openstack.block_storage.v2 import stats as _stats from openstack.block_storage.v2 import type as _type from openstack.block_storage.v2 import volume as _volume from openstack import proxy2 @@ -187,3 +188,10 @@ def delete_volume(self, volume, ignore_missing=True): :returns: ``None`` """ self._delete(_volume.Volume, volume, ignore_missing=ignore_missing) + + def backend_pools(self): + """Returns a generator of cinder Back-end storage pools + + :returns A generator of cinder Back-end storage pools objects + """ + return self._list(_stats.Pools, paginated=False) diff --git a/openstack/block_storage/v2/stats.py b/openstack/block_storage/v2/stats.py new file mode 100644 index 000000000..80a9c0b60 --- /dev/null +++ b/openstack/block_storage/v2/stats.py @@ -0,0 +1,34 @@ +# 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.block_storage import block_storage_service +from openstack import resource2 + + +class Pools(resource2.Resource): + resource_key = "pool" + resources_key = "pools" + base_path = "/scheduler-stats/get_pools?detail=True" + service = block_storage_service.BlockStorageService() + + # capabilities + allow_get = False + allow_create = False + allow_delete = False + allow_list = True + + # Properties + #: The Cinder name for the pool + name = resource2.Body("name") + #: returns a dict with information about the pool + capabilities = resource2.Body("capabilities", + type=dict) diff --git a/openstack/tests/functional/block_store/v2/test_stats.py b/openstack/tests/functional/block_store/v2/test_stats.py new file mode 100644 index 000000000..581f6e33e --- /dev/null +++ b/openstack/tests/functional/block_store/v2/test_stats.py @@ -0,0 +1,45 @@ +# 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.block_storage.v2 import stats as _stats +from openstack.tests.functional import base + + +class TestStats(base.BaseFunctionalTest): + + @classmethod + def setUpClass(cls): + super(TestStats, cls).setUpClass() + sot = cls.conn.block_storage.backend_pools() + for pool in sot: + assert isinstance(pool, _stats.Pools) + + def test_list(self): + capList = ['volume_backend_name', 'storage_protocol', + 'free_capacity_gb', 'driver_version', + 'goodness_function', 'QoS_support', + 'vendor_name', 'pool_name', 'thin_provisioning_support', + 'thick_provisioning_support', 'timestamp', + 'max_over_subscription_ratio', 'total_volumes', + 'total_capacity_gb', 'filter_function', + 'multiattach', 'provisioned_capacity_gb', + 'allocated_capacity_gb', 'reserved_percentage', + 'location_info'] + capList.sort() + pools = self.conn.block_storage.backend_pools() + for pool in pools: + caps = pool.capabilities + keys = caps.keys() + keys.sort() + assert isinstance(caps, dict) + self.assertListEqual(keys, capList) diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index 431b5f48a..ac616e89d 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -12,6 +12,7 @@ from openstack.block_storage.v2 import _proxy from openstack.block_storage.v2 import snapshot +from openstack.block_storage.v2 import stats from openstack.block_storage.v2 import type from openstack.block_storage.v2 import volume from openstack.tests.unit import test_proxy_base2 @@ -86,3 +87,7 @@ def test_volume_delete(self): def test_volume_delete_ignore(self): self.verify_delete(self.proxy.delete_volume, volume.Volume, True) + + def test_backend_pools(self): + self.verify_list(self.proxy.backend_pools, stats.Pools, + paginated=False) diff --git a/openstack/tests/unit/block_store/v2/test_stats.py b/openstack/tests/unit/block_store/v2/test_stats.py new file mode 100644 index 000000000..a8991a81b --- /dev/null +++ b/openstack/tests/unit/block_store/v2/test_stats.py @@ -0,0 +1,47 @@ +# 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 testtools + +from openstack.block_storage.v2 import stats + +POOLS = {"name": "pool1", + "capabilities": { + "updated": "2014-10-28T00=00=00-00=00", + "total_capacity": 1024, + "free_capacity": 100, + "volume_backend_name": "pool1", + "reserved_percentage": "0", + "driver_version": "1.0.0", + "storage_protocol": "iSCSI", + "QoS_support": "false" + } + } + + +class TestBackendPools(testtools.TestCase): + + def setUp(self): + super(TestBackendPools, self).setUp() + + def test_basic(self): + sot = stats.Pools(POOLS) + self.assertEqual("pool", sot.resource_key) + self.assertEqual("pools", sot.resources_key) + self.assertEqual("/scheduler-stats/get_pools?detail=True", + sot.base_path) + self.assertEqual("volume", sot.service.service_type) + self.assertFalse(sot.allow_create) + self.assertFalse(sot.allow_get) + self.assertFalse(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertFalse(sot.allow_update) From 4b8c45e916a13496186184b604a5dbc7a73496ba Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 29 Nov 2017 09:08:31 -0600 Subject: [PATCH 1855/3836] Sort image update results before comparing The patch created by jsonpatch is not in a consistent order. Sort the dicts before comparing them. Change-Id: Ieedd26d759b98cecca91f1efb9d021da46304891 --- openstack/tests/unit/image/v2/test_image.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 5c0399f5b..cf4a96355 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -11,6 +11,7 @@ # under the License. import json +import operator import mock import requests @@ -329,4 +330,8 @@ def test_image_update(self): call = self.sess.patch.call_args call_args, call_kwargs = call self.assertEqual(url, call_args[0]) - self.assertEqual(json.loads(value), json.loads(call_kwargs['data'])) + self.assertEqual( + sorted(json.loads(value), key=operator.itemgetter('value')), + sorted( + json.loads(call_kwargs['data']), + key=operator.itemgetter('value'))) From a446db5dce55d2c4239985bbdb39924365e88777 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 13 Nov 2017 09:37:54 -0600 Subject: [PATCH 1856/3836] Add ability to work in other auth contexts Add methods and context managers to facilitate working in different projects or different auth contexts, but sharing the Session and other config information from the existing cloud. Sharing the Session allows for version discovery cache to be shared, and keeps the user from needing to do a bunch of work to get a cloud that's like the current one but slightly different. Change-Id: I9190ee725cad00db4e0b6ed8321e3021a703a2e4 --- openstack/cloud/openstackcloud.py | 109 ++++++++++++++++++ .../tests/functional/cloud/test_project.py | 17 +++ .../tests/functional/cloud/test_users.py | 14 ++- ...ternate-auth-context-3939f1492a0e1355.yaml | 5 + 4 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/alternate-auth-context-3939f1492a0e1355.yaml diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index b9ed640d1..1c9f3543e 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -12,6 +12,7 @@ import base64 import collections +import copy import functools import hashlib import ipaddress @@ -33,6 +34,7 @@ from six.moves import urllib import keystoneauth1.exceptions +import keystoneauth1.session import openstack from openstack import _adapter @@ -306,6 +308,113 @@ def invalidate(self): self.cloud_config = cloud_config + def connect_as(self, **kwargs): + """Make a new OpenStackCloud object with new auth context. + + Take the existing settings from the current cloud and construct a new + OpenStackCloud object with some of the auth settings overridden. This + is useful for getting an object to perform tasks with as another user, + or in the context of a different project. + + .. code-block:: python + + cloud = shade.openstack_cloud(cloud='example') + # Work normally + servers = cloud.list_servers() + cloud2 = cloud.connect_as(username='different-user', password='') + # Work as different-user + servers = cloud2.list_servers() + + :param kwargs: keyword arguments can contain anything that would + normally go in an auth dict. They will override the same + settings from the parent cloud as appropriate. Entries + that do not want to be overridden can be ommitted. + """ + + config = openstack.config.OpenStackConfig( + app_name=self.cloud_config._app_name, + app_version=self.cloud_config._app_version, + load_yaml_config=False) + params = copy.deepcopy(self.cloud_config.config) + # Remove profile from current cloud so that overridding works + params.pop('profile', None) + + # Utility function to help with the stripping below. + def pop_keys(params, auth, name_key, id_key): + if name_key in auth or id_key in auth: + params['auth'].pop(name_key, None) + params['auth'].pop(id_key, None) + + # If there are user, project or domain settings in the incoming auth + # dict, strip out both id and name so that a user can say: + # cloud.connect_as(project_name='foo') + # and have that work with clouds that have a project_id set in their + # config. + for prefix in ('user', 'project'): + if prefix == 'user': + name_key = 'username' + else: + name_key = 'project_name' + id_key = '{prefix}_id'.format(prefix=prefix) + pop_keys(params, kwargs, name_key, id_key) + id_key = '{prefix}_domain_id'.format(prefix=prefix) + name_key = '{prefix}_domain_name'.format(prefix=prefix) + pop_keys(params, kwargs, name_key, id_key) + + for key, value in kwargs.items(): + params['auth'][key] = value + + # Closure to pass to OpenStackConfig to ensure the new cloud shares + # the Session with the current cloud. This will ensure that version + # discovery cache will be re-used. + def session_constructor(*args, **kwargs): + # We need to pass our current keystone session to the Session + # Constructor, otherwise the new auth plugin doesn't get used. + return keystoneauth1.session.Session(session=self.keystone_session) + + # Use cloud='defaults' so that we overlay settings properly + cloud_config = config.get_one_cloud( + cloud='defaults', + session_constructor=session_constructor, + **params) + # Override the cloud name so that logging/location work right + cloud_config.name = self.name + cloud_config.config['profile'] = self.name + # Use self.__class__ so that OperatorCloud will return an OperatorCloud + # instance. This should also help passthrough from sdk work better when + # we have it. + return self.__class__(cloud_config=cloud_config) + + def connect_as_project(self, project): + """Make a new OpenStackCloud object with a new project. + + Take the existing settings from the current cloud and construct a new + OpenStackCloud object with the project settings overridden. This + is useful for getting an object to perform tasks with as another user, + or in the context of a different project. + + .. code-block:: python + + cloud = shade.openstack_cloud(cloud='example') + # Work normally + servers = cloud.list_servers() + cloud2 = cloud.connect_as(dict(name='different-project')) + # Work in different-project + servers = cloud2.list_servers() + + :param project: Either a project name or a project dict as returned by + `list_projects`. + """ + auth = {} + if isinstance(project, dict): + auth['project_id'] = project.get('id') + auth['project_name'] = project.get('name') + if project.get('domain_id'): + auth['project_domain_id'] = project['domain_id'] + else: + auth['project_name'] = project + return self.connect_as(**auth) + def _make_cache(self, cache_class, expiration_time, arguments): return dogpile.cache.make_region( function_key_generator=self._make_cache_key diff --git a/openstack/tests/functional/cloud/test_project.py b/openstack/tests/functional/cloud/test_project.py index 1788a18a2..60cf31f17 100644 --- a/openstack/tests/functional/cloud/test_project.py +++ b/openstack/tests/functional/cloud/test_project.py @@ -20,6 +20,7 @@ Functional tests for `shade` project resource. """ +import pprint from openstack.cloud.exc import OpenStackCloudException from openstack.tests.functional.cloud import base @@ -61,6 +62,22 @@ def test_create_project(self): self.assertEqual(project_name, project['name']) self.assertEqual('test_create_project', project['description']) + user_id = self.operator_cloud.keystone_session.auth.get_access( + self.operator_cloud.keystone_session).user_id + + # Grant the current user access to the project + self.assertTrue(self.operator_cloud.grant_role( + 'Member', user=user_id, project=project['id'], wait=True)) + self.addCleanup( + self.operator_cloud.revoke_role, + 'Member', user=user_id, project=project['id'], wait=True) + + new_cloud = self.operator_cloud.connect_as_project(project) + self.add_info_on_exception( + 'new_cloud_config', pprint.pformat(new_cloud.cloud_config.config)) + location = new_cloud.current_location + self.assertEqual(project_name, location['project']['name']) + def test_update_project(self): project_name = self.new_project_name + '_update' diff --git a/openstack/tests/functional/cloud/test_users.py b/openstack/tests/functional/cloud/test_users.py index 73605cfce..df2195a1b 100644 --- a/openstack/tests/functional/cloud/test_users.py +++ b/openstack/tests/functional/cloud/test_users.py @@ -17,7 +17,6 @@ Functional tests for `shade` user methods. """ -from openstack import operator_cloud from openstack import OpenStackCloudException from openstack.tests.functional.cloud import base @@ -132,9 +131,16 @@ def test_update_user_password(self): self.addCleanup( self.operator_cloud.revoke_role, 'Member', user=user['id'], project='demo', wait=True) - self.assertIsNotNone(operator_cloud( - cloud=self._demo_name, - username=user_name, password='new_secret').service_catalog) + + new_cloud = self.operator_cloud.connect_as( + user_id=user['id'], + password='new_secret', + project_name='demo') + + self.assertIsNotNone(new_cloud) + location = new_cloud.current_location + self.assertEqual(location['project']['name'], 'demo') + self.assertIsNotNone(new_cloud.service_catalog) def test_users_and_groups(self): i_ver = self.operator_cloud.cloud_config.get_api_version('identity') diff --git a/releasenotes/notes/alternate-auth-context-3939f1492a0e1355.yaml b/releasenotes/notes/alternate-auth-context-3939f1492a0e1355.yaml new file mode 100644 index 000000000..fe24fbc44 --- /dev/null +++ b/releasenotes/notes/alternate-auth-context-3939f1492a0e1355.yaml @@ -0,0 +1,5 @@ +--- +features: + - Added methods and context managers for making new cloud connections + based on the current OpenStackCloud. This should enable working + more easily across projects or user accounts. From 0b4ee3317070c061a3d2aac8b11b7505cbb0bfa6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 13 Nov 2017 18:13:02 -0600 Subject: [PATCH 1857/3836] Add helper property to get the current user id The data is available, but it's not the MOST straightforward thing to find. We have helpers for project, add one for user. Change-Id: I325d7f867796e9a24e02576a1363ea6b0fe7687b --- openstack/cloud/openstackcloud.py | 6 ++++++ openstack/tests/functional/cloud/test_project.py | 3 +-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 1c9f3543e..dce6bda73 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -730,6 +730,12 @@ def auth_token(self): # We don't need to track validity here, just get_token() each time. return self.keystone_session.get_token() + @property + def current_user_id(self): + """Get the id of the currently logged-in user from the token.""" + return self.keystone_session.auth.get_access( + self.keystone_session).user_id + @property def current_project_id(self): """Get the current project ID. diff --git a/openstack/tests/functional/cloud/test_project.py b/openstack/tests/functional/cloud/test_project.py index 60cf31f17..4ac522aa6 100644 --- a/openstack/tests/functional/cloud/test_project.py +++ b/openstack/tests/functional/cloud/test_project.py @@ -62,8 +62,7 @@ def test_create_project(self): self.assertEqual(project_name, project['name']) self.assertEqual('test_create_project', project['description']) - user_id = self.operator_cloud.keystone_session.auth.get_access( - self.operator_cloud.keystone_session).user_id + user_id = self.operator_cloud.current_user_id # Grant the current user access to the project self.assertTrue(self.operator_cloud.grant_role( From dfe2577b15e202356fcc7d8be3604df30871788c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 14 Nov 2017 16:22:51 -0600 Subject: [PATCH 1858/3836] Remove reference to context-managers from release note We didn't wind up keeping these. Change-Id: Id111436ca9d0637a8d71250decede12f23122f0a --- releasenotes/notes/alternate-auth-context-3939f1492a0e1355.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/notes/alternate-auth-context-3939f1492a0e1355.yaml b/releasenotes/notes/alternate-auth-context-3939f1492a0e1355.yaml index fe24fbc44..e454f5b91 100644 --- a/releasenotes/notes/alternate-auth-context-3939f1492a0e1355.yaml +++ b/releasenotes/notes/alternate-auth-context-3939f1492a0e1355.yaml @@ -1,5 +1,5 @@ --- features: - - Added methods and context managers for making new cloud connections + - Added methods for making new cloud connections based on the current OpenStackCloud. This should enable working more easily across projects or user accounts. From eb9d3a5a4695a2be327b30074b78dcaff89d3584 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 15 Nov 2017 08:43:06 -0600 Subject: [PATCH 1859/3836] Document current_user_id in a release note Also fix the connect_as docstring. Change-Id: I53203b892bcf03d540d6e551ec9615439ad5c4c9 --- openstack/cloud/openstackcloud.py | 2 +- releasenotes/notes/add-current-user-id-49b6463e6bcc3b31.yaml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-current-user-id-49b6463e6bcc3b31.yaml diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index dce6bda73..006841997 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -398,7 +398,7 @@ def connect_as_project(self, project): cloud = shade.openstack_cloud(cloud='example') # Work normally servers = cloud.list_servers() - cloud2 = cloud.connect_as(dict(name='different-project')) + cloud2 = cloud.connect_as_project('different-project') # Work in different-project servers = cloud2.list_servers() diff --git a/releasenotes/notes/add-current-user-id-49b6463e6bcc3b31.yaml b/releasenotes/notes/add-current-user-id-49b6463e6bcc3b31.yaml new file mode 100644 index 000000000..fd9a1bece --- /dev/null +++ b/releasenotes/notes/add-current-user-id-49b6463e6bcc3b31.yaml @@ -0,0 +1,4 @@ +--- +features: + - Added a new property, 'current_user_id' which contains + the id of the currently authenticated user from the token. From 1a83a534b83899dd6a39fb1670569776d8d9208a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 29 Nov 2017 15:39:24 -0600 Subject: [PATCH 1860/3836] Remove use of tox-siblings role Its functionality has been merged into the tox role, so is no longer needed. Change-Id: Id952ddf5b140185a1bf6f5e9dd28798c6380b209 Depends-On: Id61ae52d48b28cfc2221cb556a1c1f7c6dfd60dd --- playbooks/devstack/pre.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/playbooks/devstack/pre.yaml b/playbooks/devstack/pre.yaml index c43248710..3ec41c9cb 100644 --- a/playbooks/devstack/pre.yaml +++ b/playbooks/devstack/pre.yaml @@ -6,5 +6,3 @@ bindep_dir: "{{ zuul_work_dir }}" - test-setup - ensure-tox - - role: tox-siblings - when: tox_install_siblings From d1b242ee850f286f1fae56021f2edf13cde1eb9c Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Sun, 26 Nov 2017 22:27:39 +0800 Subject: [PATCH 1861/3836] Add pools attribute to load balancer heath monitor The v2 api actually returns pools instead of pool_id in body, but it still uses pool_id in the request. Change-Id: I302537a33c4d48c702066439e566b423896fb70f --- openstack/load_balancer/v2/health_monitor.py | 3 +++ openstack/tests/unit/load_balancer/test_health_monitor.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/openstack/load_balancer/v2/health_monitor.py b/openstack/load_balancer/v2/health_monitor.py index da8820980..ab47d2a52 100644 --- a/openstack/load_balancer/v2/health_monitor.py +++ b/openstack/load_balancer/v2/health_monitor.py @@ -55,6 +55,9 @@ class HealthMonitor(resource.Resource): name = resource.Body('name') #: Operating status of the member. operating_status = resource.Body('operating_status') + #: List of associated pools. + #: *Type: list of dicts which contain the pool IDs* + pools = resource.Body('pools', type=list) #: The ID of the associated Pool pool_id = resource.Body('pool_id') #: The ID of the project diff --git a/openstack/tests/unit/load_balancer/test_health_monitor.py b/openstack/tests/unit/load_balancer/test_health_monitor.py index b5cf0a24b..515db7b32 100644 --- a/openstack/tests/unit/load_balancer/test_health_monitor.py +++ b/openstack/tests/unit/load_balancer/test_health_monitor.py @@ -27,6 +27,7 @@ 'max_retries_down': 3, 'name': 'test_health_monitor', 'operating_status': 'ONLINE', + 'pools': [{'id': uuid.uuid4()}], 'pool_id': uuid.uuid4(), 'project_id': uuid.uuid4(), 'provisioning_status': 'ACTIVE', @@ -63,6 +64,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['max_retries_down'], test_hm.max_retries_down) self.assertEqual(EXAMPLE['name'], test_hm.name) self.assertEqual(EXAMPLE['operating_status'], test_hm.operating_status) + self.assertEqual(EXAMPLE['pools'], test_hm.pools) self.assertEqual(EXAMPLE['pool_id'], test_hm.pool_id) self.assertEqual(EXAMPLE['project_id'], test_hm.project_id) self.assertEqual(EXAMPLE['provisioning_status'], From 895db171233cfc74728a2eb5db3b63d29139055a Mon Sep 17 00:00:00 2001 From: Adrian Turjak Date: Thu, 30 Nov 2017 18:15:45 +1300 Subject: [PATCH 1862/3836] Stop osSDK mangling Swift metadata keys Make the _calculate_headers function check the values as well as the keys in _system_metadata for metadata keys passed in, and otherwise first check if the key has the _custom_metadata_prefix before prepending it. Change-Id: Icb27c6fd43b143676b23c3aca7f23d9d8ab0f04e --- openstack/object_store/v1/_base.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openstack/object_store/v1/_base.py b/openstack/object_store/v1/_base.py index cd99503fc..a16e8fb7a 100644 --- a/openstack/object_store/v1/_base.py +++ b/openstack/object_store/v1/_base.py @@ -27,10 +27,15 @@ class BaseResource(resource.Resource): def _calculate_headers(self, metadata): headers = dict() for key in metadata: - if key in self._system_metadata: + if key in self._system_metadata.keys(): header = self._system_metadata[key] + elif key in self._system_metadata.values(): + header = key else: - header = self._custom_metadata_prefix + key + if key.startswith(self._custom_metadata_prefix): + header = key + else: + header = self._custom_metadata_prefix + key headers[header] = metadata[key] return headers From 392e2c4856b484a741ed83d53d967c3736f65ed6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 30 Nov 2017 15:19:14 -0600 Subject: [PATCH 1863/3836] Update the shade-merge document There are a few more discussions that have been had that have not been captured. Capture them. Change-Id: Ie1422bc9a48b600c61d8c906cdfaf9fe4d26c924 --- SHADE-MERGE-TODO.rst | 78 ++++++++++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 29 deletions(-) diff --git a/SHADE-MERGE-TODO.rst b/SHADE-MERGE-TODO.rst index 61cc4e1a8..6155dc628 100644 --- a/SHADE-MERGE-TODO.rst +++ b/SHADE-MERGE-TODO.rst @@ -1,8 +1,8 @@ -Tasks Needed for merging shade and openstacksdk -=============================================== +Tasks Needed for rationalizing shade and openstacksdk +====================================================== -A large portion of the important things have already been done in the stack -leading up to this change. For reference, those are: +A large portion of the important things have already been done and landed +already. For reference, those are: * shade and os-client-config library content have been merged into the tree. * Use official service-type names from Service Types Authority via @@ -16,7 +16,7 @@ leading up to this change. For reference, those are: .. code-block:: python connection = connection.Connection() - servers = connection.compute.list_servers() + servers = connection.compute.servers() server_response = connection.compute.get('/servers') * Removed the Profile object in favor of openstack.config. @@ -28,18 +28,58 @@ Next steps ========== * Finish migrating to Resource2 and Proxy2, rename them to Resource and Proxy. -* Rename self.session and session parameter in all usage in proxy and resource - to self.adapter. +* Maybe rename self.session and session parameter in all usage in proxy and + resource to self.adapter. They are Adapters not Sessions, but that may not + mean anything to people. * Migrate unit tests to requests-mock instead of mocking python calls to session. -* Consider removing ServiceFilter and the various Service objects if an +* Investigate removing ServiceFilter and the various Service objects if an acceptable plan can be found for using discovery. * Replace _prepare_request with requests.Session.prepare_request. +shade integration +----------------- + +* Merge OpenStackCloud and OperatorCloud into Connection. This should result + in being able to use the connection interact with the cloud using all three + interfaces. For instance: + + .. code-block:: python + + conn = connection.Connection() + servers = conn.list_servers() # High-level resource interface from shade + servers = conn.compute.servers() # SDK Service/Object Interface + response = conn.compute.get('/servers') # REST passthrough + +* Invent some terminology that is clear and makes sense to distinguish between + the object interface that came originally from python-openstacksdk and the + interface that came from shade. +* Shift the shade interface methods to use the Object Interface for their + operations. It's possible there may be cases where the REST layer needs to + be used instead, but we should try to sort those out. +* Investigate options and then make a plan as to whether shade methods should + return SDK objects or return dicts/munches as they do today. Should we make + Resource objects extend dict/munch so they can be used like the shade ones + today? Or should we just have the external shade shim library get objects + from the high-level SDK 'shade' interface and call to_dict() on them all? +* Add support for shade expressing normalization model/contract into Resource, + or for just leveraging what's in Resource for shade-layer normalization. +* Make a plan for normalization supporting shade users continuing + to get shade normalized resource Munch objects from shade API calls, sdk + proxy/resource users getting SDK objects, and both of them being able to opt + in to "strict" normalization at Connection constructor time. Perhaps making + Resource subclass Munch would allow mixed use? Needs investigation. +* Investigate auto-generating the bulk of shade's API based on introspection of + SDK objects, leaving only the code with extra special logic in the shade + layer. +* Rationalize openstack.util.enable_logging and shade.simple_logging. + Service Proxies --------------- -* Authenticate at Connection() creation time. Having done that, use the +These are all things to think about. + +* Authenticate at Connection() creation time? Having done that, use the catalog in the token to determine which service proxies to add to the Connection object. * Filter the above service list from the token by has_service() from @@ -98,23 +138,3 @@ Microversions support is added to calls, it needs to be on a per-request basis. This has implications to both Resource and Proxy, as cloud payloads for data mapping can be different on a per-microversion basis. - -shade integration ------------------ - -* Add support for shade expressing normalization model/contract into Resource. -* Make a plan for normalization supporting shade users continuing - to get shade normalized resource Munch objects from shade API calls, sdk - proxy/resource users getting SDK objects, and both of them being able to opt - in to "strict" normalization at Connection constructor time. Perhaps making - Resource subclass Munch would allow mixed use? Needs investigation. -* Investigate auto-generating the bulk of shade's API based on introspection of - SDK objects, leaving only the code with extra special logic in shade itself. -* Rationalize openstack.util.enable_logging and shade.simple_logging. - -caching -------- - -* Make a plan for caching that can work with shade's batched-access/client-side - rate-limiting, per-resource configurable caching and direct get models. It - may want to actually live in keystoneauth. From 7fb95b9e7598fdcd919082e2b98eb753ddbf0c69 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 28 Nov 2017 04:35:25 -0600 Subject: [PATCH 1864/3836] Fix py35 and pypy tox env The basepython line was causing the py35 tests to actually run under python2. Whoops. It was doing the same to pypy. Add pypy to bindep with the pypy tag. Cryptography and the version of pypi in xenial are incompatible, so update the pypy job to run on fedora-26 instead, which has a new enough pypy. Shift the line so that it only applies to functional and ansible tests, since that's where it's used. Depends-On: I7c22e23b73ddfc18ee28e87d34d1988417b49ccb Change-Id: Ia27a0aa0633dcf48c1bab06b3c803f996eae9efa --- .zuul.yaml | 5 ++++- bindep.txt | 4 ++++ openstack/tests/unit/cloud/test_create_server.py | 3 ++- openstack/tests/unit/test_resource2.py | 5 ++++- tox.ini | 3 ++- 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 0633b1c76..7451ba34f 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -196,16 +196,19 @@ - project: name: openstack/python-openstacksdk templates: - - openstack-pypy-jobs - openstacksdk-functional-tips - openstacksdk-tox-tips check: jobs: + - openstack-tox-pypy: + nodeset: fedora-26 - openstacksdk-functional-devstack - openstacksdk-functional-devstack-magnum: voting: false - openstacksdk-functional-devstack-python3 gate: jobs: + - openstack-tox-pypy: + nodeset: fedora-26 - openstacksdk-functional-devstack - openstacksdk-functional-devstack-python3 diff --git a/bindep.txt b/bindep.txt index e5d10a394..7f205ffc8 100644 --- a/bindep.txt +++ b/bindep.txt @@ -6,3 +6,7 @@ python-dev [platform:dpkg] python-devel [platform:rpm] libffi-dev [platform:dpkg] libffi-devel [platform:rpm] +openssl-devel [platform:rpm] +pypy [pypy] +pypy-dev [platform:dpkg pypy] +pypy-devel [platform:rpm pypy] diff --git a/openstack/tests/unit/cloud/test_create_server.py b/openstack/tests/unit/cloud/test_create_server.py index ba969011a..b37b14f3d 100644 --- a/openstack/tests/unit/cloud/test_create_server.py +++ b/openstack/tests/unit/cloud/test_create_server.py @@ -376,7 +376,8 @@ def test_create_server_user_data_base64(self): Test that a server passed user-data sends it base64 encoded. """ user_data = self.getUniqueString('user_data') - user_data_b64 = base64.b64encode(user_data).decode('utf-8') + user_data_b64 = base64.b64encode( + user_data.encode('utf-8')).decode('utf-8') fake_server = fakes.make_fake_server('1234', '', 'BUILD') fake_server['user_data'] = user_data diff --git a/openstack/tests/unit/test_resource2.py b/openstack/tests/unit/test_resource2.py index 8f72dbfdd..28e1c224a 100644 --- a/openstack/tests/unit/test_resource2.py +++ b/openstack/tests/unit/test_resource2.py @@ -1307,8 +1307,10 @@ class Base(resource2.Resource): @classmethod def existing(cls, **kwargs): + response = mock.Mock() + response.status_code = 404 raise exceptions.NotFoundException( - 'Not Found', response=mock.Mock()) + 'Not Found', response=response) @classmethod def list(cls, session): @@ -1485,6 +1487,7 @@ class TestWaitForDelete(base.TestCase): def test_success(self, mock_sleep): response = mock.Mock() response.headers = {} + response.status_code = 404 resource = mock.Mock() resource.get.side_effect = [ None, None, diff --git a/tox.ini b/tox.ini index a419cf316..ff30ac7de 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,6 @@ skipsdist = True [testenv] usedevelop = True -basepython = {env:OPENSTACKSDK_TOX_PYTHON:python2} passenv = UPPER_CONSTRAINTS_FILE install_command = {toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} setenv = @@ -25,6 +24,7 @@ commands = stestr --test-path ./openstack/tests/examples run {posargs} stestr slowest [testenv:functional] +basepython = {env:OPENSTACKSDK_TOX_PYTHON:python2} passenv = OS_* OPENSTACKSDK_* UPPER_CONSTRAINTS_FILE commands = stestr --test-path ./openstack/tests/functional run --serial {posargs} stestr slowest @@ -55,6 +55,7 @@ commands = [testenv:ansible] # Need to pass some env vars for the Ansible playbooks +basepython = {env:OPENSTACKSDK_TOX_PYTHON:python2} passenv = HOME USER commands = {toxinidir}/extras/run-ansible-tests.sh -e {envdir} {posargs} From 58bafb63e1daa1997686b91d019a3624520a3767 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Fri, 1 Dec 2017 08:14:32 +0100 Subject: [PATCH 1865/3836] Avoid tox_install.sh for constraints support We do not need tox_install.sh, pip can handle constraints itself and install the project correctly. Thus update tox.ini and remove the now obsolete tools/tox_install.sh file. This follows https://review.openstack.org/#/c/508061 to remove tools/tox_install.sh. Change-Id: I9ee471b253ec104d2b93829be3644ce7aef06876 --- tools/tox_install.sh | 30 ------------------------------ tox.ini | 18 ++++++++++-------- 2 files changed, 10 insertions(+), 38 deletions(-) delete mode 100755 tools/tox_install.sh diff --git a/tools/tox_install.sh b/tools/tox_install.sh deleted file mode 100755 index 43468e450..000000000 --- a/tools/tox_install.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash - -# Client constraint file contains this client version pin that is in conflict -# with installing the client from source. We should remove the version pin in -# the constraints file before applying it for from-source installation. - -CONSTRAINTS_FILE=$1 -shift 1 - -set -e - -# NOTE(tonyb): Place this in the tox enviroment's log dir so it will get -# published to logs.openstack.org for easy debugging. -localfile="$VIRTUAL_ENV/log/upper-constraints.txt" - -if [[ $CONSTRAINTS_FILE != http* ]]; then - CONSTRAINTS_FILE=file://$CONSTRAINTS_FILE -fi -# NOTE(tonyb): need to add curl to bindep.txt if the project supports bindep -curl $CONSTRAINTS_FILE --insecure --progress-bar --output $localfile - -pip install -c$localfile openstack-requirements - -# This is the main purpose of the script: Allow local installation of -# the current repo. It is listed in constraints file and thus any -# install will be constrained and we need to unconstrain it. -edit-constraints $localfile -- $CLIENT_NAME - -pip install -c$localfile -U $* -exit $? diff --git a/tox.ini b/tox.ini index ff30ac7de..9afc6f585 100644 --- a/tox.ini +++ b/tox.ini @@ -5,27 +5,27 @@ skipsdist = True [testenv] usedevelop = True -passenv = UPPER_CONSTRAINTS_FILE -install_command = {toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} +install_command = pip install -U {opts} {packages} setenv = VIRTUAL_ENV={envdir} LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=C - BRANCH_NAME=master - CLIENT_NAME=openstacksdk -deps = -r{toxinidir}/test-requirements.txt +deps = + -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} + -r{toxinidir}/test-requirements.txt + -r{toxinidir}/requirements.txt commands = stestr run {posargs} stestr slowest [testenv:examples] -passenv = OS_* OPENSTACKSDK_* UPPER_CONSTRAINTS_FILE +passenv = OS_* OPENSTACKSDK_* commands = stestr --test-path ./openstack/tests/examples run {posargs} stestr slowest [testenv:functional] basepython = {env:OPENSTACKSDK_TOX_PYTHON:python2} -passenv = OS_* OPENSTACKSDK_* UPPER_CONSTRAINTS_FILE +passenv = OS_* OPENSTACKSDK_* commands = stestr --test-path ./openstack/tests/functional run --serial {posargs} stestr slowest @@ -61,7 +61,9 @@ commands = {toxinidir}/extras/run-ansible-tests.sh -e {envdir} {posargs} [testenv:docs] skip_install = True -deps = -r{toxinidir}/test-requirements.txt +deps = + -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} + -r{toxinidir}/test-requirements.txt commands = sphinx-build -b html doc/source/ doc/build [testenv:releasenotes] From f8b9e82b2d67ce450a6a0882ccc88444f4be71ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89douard=20Thuleau?= Date: Wed, 29 Nov 2017 11:04:51 +0100 Subject: [PATCH 1866/3836] Set empty Tag list if Network Tag API extension not supported Since patch I872fa6e3c3925e521150d79bba864101d9a5f648, Neutron default services plugin and their API extensions could not be load depending on the core plugin implementation. That includes the Tag extensions. That patch sets Tag resource attribute to an empty list by default to prevent missing Tag property in the Network API responses. Change-Id: I2fab462c63a64a8c0af5c6c4ae211b04f5adbd7f Closes-Bug: #1734924 --- openstack/network/v2/network.py | 2 +- openstack/network/v2/port.py | 2 +- openstack/network/v2/router.py | 2 +- openstack/network/v2/subnet.py | 2 +- openstack/network/v2/subnet_pool.py | 2 +- openstack/tests/unit/network/v2/test_tag.py | 20 ++++++++++++++++---- 6 files changed, 21 insertions(+), 9 deletions(-) diff --git a/openstack/network/v2/network.py b/openstack/network/v2/network.py index cbdd4518e..f2aa0aa8e 100644 --- a/openstack/network/v2/network.py +++ b/openstack/network/v2/network.py @@ -115,7 +115,7 @@ class Network(resource.Resource, tag.TagMixin): is_vlan_transparent = resource.Body('vlan_transparent', type=bool) #: A list of assocaited tags #: *Type: list of tag strings* - tags = resource.Body('tags', type=list) + tags = resource.Body('tags', type=list, default=[]) class DHCPAgentHostingNetwork(Network): diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index 1356df49d..8240443ce 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -131,4 +131,4 @@ class Port(resource.Resource, tag.TagMixin): updated_at = resource.Body('updated_at') #: A list of assocaited tags #: *Type: list of tag strings* - tags = resource.Body('tags', type=list) + tags = resource.Body('tags', type=list, default=[]) diff --git a/openstack/network/v2/router.py b/openstack/network/v2/router.py index 2573dc62a..fd90f6a7a 100644 --- a/openstack/network/v2/router.py +++ b/openstack/network/v2/router.py @@ -78,7 +78,7 @@ class Router(resource.Resource, tag.TagMixin): updated_at = resource.Body('updated_at') #: A list of assocaited tags #: *Type: list of tag strings* - tags = resource.Body('tags', type=list) + tags = resource.Body('tags', type=list, default=[]) def add_interface(self, session, **body): """Add an internal interface to a logical router. diff --git a/openstack/network/v2/subnet.py b/openstack/network/v2/subnet.py index a9e95ad59..a79a26f9b 100644 --- a/openstack/network/v2/subnet.py +++ b/openstack/network/v2/subnet.py @@ -91,4 +91,4 @@ class Subnet(resource.Resource, tag.TagMixin): ) #: A list of assocaited tags #: *Type: list of tag strings* - tags = resource.Body('tags', type=list) + tags = resource.Body('tags', type=list, default=[]) diff --git a/openstack/network/v2/subnet_pool.py b/openstack/network/v2/subnet_pool.py index 86a43590a..b4115d4d7 100644 --- a/openstack/network/v2/subnet_pool.py +++ b/openstack/network/v2/subnet_pool.py @@ -81,4 +81,4 @@ class SubnetPool(resource.Resource, tag.TagMixin): updated_at = resource.Body('updated_at') #: A list of assocaited tags #: *Type: list of tag strings* - tags = resource.Body('tags', type=list) + tags = resource.Body('tags', type=list, default=[]) diff --git a/openstack/tests/unit/network/v2/test_tag.py b/openstack/tests/unit/network/v2/test_tag.py index 102247457..ff7270d2d 100644 --- a/openstack/tests/unit/network/v2/test_tag.py +++ b/openstack/tests/unit/network/v2/test_tag.py @@ -10,11 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. +import inspect import mock import testtools from openstack.network.v2 import network - +import openstack.network.v2 as network_resources +from openstack.network.v2.tag import TagMixin ID = 'IDENTIFIER' @@ -22,17 +24,17 @@ class TestTag(testtools.TestCase): @staticmethod - def _create_resource(tags=None): + def _create_network_resource(tags=None): tags = tags or [] return network.Network(id=ID, name='test-net', tags=tags) def test_tags_attribute(self): - net = self._create_resource() + net = self._create_network_resource() self.assertTrue(hasattr(net, 'tags')) self.assertIsInstance(net.tags, list) def test_set_tags(self): - net = self._create_resource() + net = self._create_network_resource() sess = mock.Mock() result = net.set_tags(sess, ['blue', 'green']) # Check tags attribute is updated @@ -42,3 +44,13 @@ def test_set_tags(self): url = 'networks/' + ID + '/tags' sess.put.assert_called_once_with(url, json={'tags': ['blue', 'green']}) + + def test_tagged_resource_always_created_with_empty_tag_list(self): + for _, module in inspect.getmembers(network_resources, + inspect.ismodule): + for _, resource in inspect.getmembers(module, inspect.isclass): + if issubclass(resource, TagMixin) and resource != TagMixin: + x_resource = resource.new( + id="%s_ID" % resource.resource_key.upper()) + self.assertIsNotNone(x_resource.tags) + self.assertEqual(x_resource.tags, list()) From f561bfde716ef45bdb4e0363cc23e9bef22f6946 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 1 Dec 2017 09:49:05 -0600 Subject: [PATCH 1867/3836] Remove openstack-tox-pypy jobs Now subunit is bombing out in crazy ways. This is too much effort for a thing nobody uses. Change-Id: I4bc73ef13b8a358d6a10bb0d025d1ebc1a1f2655 --- .zuul.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 7451ba34f..e69b81087 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -200,15 +200,11 @@ - openstacksdk-tox-tips check: jobs: - - openstack-tox-pypy: - nodeset: fedora-26 - openstacksdk-functional-devstack - openstacksdk-functional-devstack-magnum: voting: false - openstacksdk-functional-devstack-python3 gate: jobs: - - openstack-tox-pypy: - nodeset: fedora-26 - openstacksdk-functional-devstack - openstacksdk-functional-devstack-python3 From c4aae34843f420b3aa2cb75144dc608537d792f3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 16 Nov 2017 08:46:49 -0600 Subject: [PATCH 1868/3836] Cleanup objects that we create on behalf of images When the use performs create_image on a cloud that requires the image be uploaded to swift, we upload the image content to swift for them. Once the image is imported, the content in swift is no longer needed as glance copies the data into its own area. Clean the images up so that we're not just accumulating tons of content in swift. Change-Id: Iede6441c0cad1d99e92fdfacf96d81aad479a93e --- openstack/cloud/openstackcloud.py | 8 ++++++++ openstack/tests/unit/cloud/test_image.py | 19 +++++++++++++++++++ .../cleanup-objects-f99aeecf22ac13dd.yaml | 6 ++++++ 3 files changed, 33 insertions(+) create mode 100644 releasenotes/notes/cleanup-objects-f99aeecf22ac13dd.yaml diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 006841997..26e288e55 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -4883,6 +4883,9 @@ def _upload_image_task( self.log.debug( "Image Task %s imported %s in %s", glance_task.id, image_id, (time.time() - start)) + # Clean up after ourselves. The object we created is not + # needed after the import is done. + self.delete_object(container, name) return self.get_image(image_id) elif status['status'] == 'failure': if status['message'] == IMAGE_ERROR_396: @@ -4890,6 +4893,11 @@ def _upload_image_task( '/tasks', data=task_args) self.list_images.invalidate(self) else: + # Clean up after ourselves. The image did not import + # and this isn't a 'just retry' error - glance didn't + # like the content. So we don't want to keep it for + # next time. + self.delete_object(container, name) raise OpenStackCloudException( "Image creation failed: {message}".format( message=status['message']), diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 0ff6a53d1..5b73faa8b 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -363,6 +363,25 @@ def test_create_image_task(self): 'Content-Type': 'application/openstack-images-v2.1-json-patch'}) ), + dict(method='HEAD', + uri='{endpoint}/{container}/{object}'.format( + endpoint=endpoint, container=container_name, + object=image_name), + headers={ + 'X-Timestamp': '1429036140.50253', + 'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1', + 'Content-Length': '1290170880', + 'Last-Modified': 'Tue, 14 Apr 2015 18:29:01 GMT', + 'X-Object-Meta-X-Shade-Sha256': fakes.NO_SHA256, + 'X-Object-Meta-X-Shade-Md5': fakes.NO_MD5, + 'Date': 'Thu, 16 Nov 2017 15:24:30 GMT', + 'Accept-Ranges': 'bytes', + 'Content-Type': 'application/octet-stream', + 'Etag': fakes.NO_MD5}), + dict(method='DELETE', + uri='{endpoint}/{container}/{object}'.format( + endpoint=endpoint, container=container_name, + object=image_name)), dict(method='GET', uri='https://image.example.com/v2/images', json=self.fake_search_return) ]) diff --git a/releasenotes/notes/cleanup-objects-f99aeecf22ac13dd.yaml b/releasenotes/notes/cleanup-objects-f99aeecf22ac13dd.yaml new file mode 100644 index 000000000..e1e0752fd --- /dev/null +++ b/releasenotes/notes/cleanup-objects-f99aeecf22ac13dd.yaml @@ -0,0 +1,6 @@ +--- +features: + - If shade has to create objects in swift to upload an + image, it will now delete those objects upon successful + image creation as they are no longer needed. They will + also be deleted on fatal import errors. From dd431c3a22c4ce9fc25b6f910e2cc4152a4a99c2 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 16 Nov 2017 10:18:54 -0600 Subject: [PATCH 1869/3836] Add method to cleanup autocreated image objects This shouldn't really be needed, as the objects should get cleaned up automatically. BUT - if things leak, this method can be used to delete any objects shade has uploaded on behalf of the user for deleting images. While in there, clean up test_image to use a few more good practices. Change-Id: Ifb697944856e1922517074d84a7c00a4af75b1e6 --- openstack/cloud/openstackcloud.py | 34 ++++- openstack/tests/unit/base.py | 4 + openstack/tests/unit/cloud/test_image.py | 121 +++++++++++++++--- .../delete-autocreated-1839187b0aa35022.yaml | 5 + 4 files changed, 141 insertions(+), 23 deletions(-) create mode 100644 releasenotes/notes/delete-autocreated-1839187b0aa35022.yaml diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 26e288e55..4b671e7b2 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -52,6 +52,8 @@ # result in a re-upload OBJECT_MD5_KEY = 'x-object-meta-x-sdk-md5' OBJECT_SHA256_KEY = 'x-object-meta-x-sdk-sha256' +OBJECT_AUTOCREATE_KEY = 'x-object-meta-x-sdk-autocreated' +OBJECT_AUTOCREATE_CONTAINER = 'images' # TODO(shade) shade keys were owner_specified.shade.md5 - we need to add those # to freshness checks so that a shade->sdk transition doens't # result in a re-upload @@ -4534,7 +4536,7 @@ def _hashes_up_to_date(self, md5, sha256, md5_key, sha256_key): return up_to_date def create_image( - self, name, filename=None, container='images', + self, name, filename=None, container=OBJECT_AUTOCREATE_CONTAINER, md5=None, sha256=None, disk_format=None, container_format=None, disable_vendor_agent=True, @@ -4831,6 +4833,7 @@ def _upload_image_task( self.create_object( container, name, filename, md5=md5, sha256=sha256, + metadata={OBJECT_AUTOCREATE_KEY: 'true'}, **{'content-type': 'application/octet-stream'}) if not current_image: current_image = self.get_image(name) @@ -7568,11 +7571,13 @@ def list_objects(self, container, full_listing=True): return self._object_store_client.get( container, params=dict(format='json')) - def delete_object(self, container, name): + def delete_object(self, container, name, meta=None): """Delete an object from a container. :param string container: Name of the container holding the object. :param string name: Name of the object to delete. + :param dict meta: Metadata for the object in question. (optional, will + be fetched if not provided) :returns: True if delete succeeded, False if the object was not found. @@ -7587,7 +7592,8 @@ def delete_object(self, container, name): # Errors: # We should ultimately do something with that try: - meta = self.get_object_metadata(container, name) + if not meta: + meta = self.get_object_metadata(container, name) if not meta: return False params = {} @@ -7601,6 +7607,28 @@ def delete_object(self, container, name): except OpenStackCloudHTTPError: return False + def delete_autocreated_image_objects( + self, container=OBJECT_AUTOCREATE_CONTAINER): + """Delete all objects autocreated for image uploads. + + This method should generally not be needed, as shade should clean up + the objects it uses for object-based image creation. If something + goes wrong and it is found that there are leaked objects, this method + can be used to delete any objects that shade has created on the user's + behalf in service of image uploads. + """ + # This method only makes sense on clouds that use tasks + if not self.image_api_use_tasks: + return False + + deleted = False + for obj in self.list_objects(container): + meta = self.get_object_metadata(container, obj['name']) + if meta.get(OBJECT_AUTOCREATE_KEY) == 'true': + if self.delete_object(container, obj['name'], meta): + deleted = True + return deleted + def get_object_metadata(self, container, name): try: return self._object_store_client.head( diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 4f474e0f9..0edd69521 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -420,6 +420,10 @@ def use_broken_keystone(self): ]) self._make_test_cloud(identity_api_version='3') + def use_nothing(self): + self.calls = [] + self._uri_registry.clear() + def use_keystone_v3(self, catalog='catalog-v3.json'): self.adapter = self.useFixture(rm_fixture.Fixture()) self.calls = [] diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 5b73faa8b..5df8ac1ac 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -26,6 +26,7 @@ import openstack.cloud from openstack.cloud import exc from openstack.cloud import meta +from openstack.cloud import openstackcloud from openstack.tests import fakes from openstack.tests.unit import base @@ -44,6 +45,8 @@ def setUp(self): self.fake_image_dict = fakes.make_fake_image(image_id=self.image_id) self.fake_search_return = {'images': [self.fake_image_dict]} self.output = uuid.uuid4().bytes + self.image_name = self.getUniqueString('image') + self.container_name = self.getUniqueString('container') class TestImage(BaseTestImage): @@ -260,8 +263,6 @@ def test_create_image_put_v2(self): def test_create_image_task(self): self.cloud.image_api_use_tasks = True - image_name = 'name-99' - container_name = 'image_upload_v2_test_container' endpoint = self.cloud._object_store_client.get_endpoint() task_id = str(uuid.uuid4()) @@ -288,18 +289,18 @@ def test_create_image_task(self): slo={'min_segment_size': 500})), dict(method='HEAD', uri='{endpoint}/{container}'.format( - endpoint=endpoint, container=container_name), + endpoint=endpoint, container=self.container_name), status_code=404), dict(method='PUT', uri='{endpoint}/{container}'.format( - endpoint=endpoint, container=container_name), + endpoint=endpoint, container=self.container_name), status_code=201, headers={'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', 'Content-Length': '0', 'Content-Type': 'text/html; charset=UTF-8'}), dict(method='HEAD', uri='{endpoint}/{container}'.format( - endpoint=endpoint, container=container_name), + endpoint=endpoint, container=self.container_name), headers={'Content-Length': '0', 'X-Container-Object-Count': '0', 'Accept-Ranges': 'bytes', @@ -311,13 +312,13 @@ def test_create_image_task(self): 'Content-Type': 'text/plain; charset=utf-8'}), dict(method='HEAD', uri='{endpoint}/{container}/{object}'.format( - endpoint=endpoint, container=container_name, - object=image_name), + endpoint=endpoint, container=self.container_name, + object=self.image_name), status_code=404), dict(method='PUT', uri='{endpoint}/{container}/{object}'.format( - endpoint=endpoint, container=container_name, - object=image_name), + endpoint=endpoint, container=self.container_name, + object=self.image_name), status_code=201, validate=dict( headers={'x-object-meta-x-sdk-md5': fakes.NO_MD5, @@ -331,8 +332,9 @@ def test_create_image_task(self): json=dict( type='import', input={ 'import_from': '{container}/{object}'.format( - container=container_name, object=image_name), - 'image_properties': {'name': image_name}})) + container=self.container_name, + object=self.image_name), + 'image_properties': {'name': self.image_name}})) ), dict(method='GET', uri='https://image.example.com/v2/tasks/{id}'.format( @@ -351,22 +353,22 @@ def test_create_image_task(self): json=sorted([ {u'op': u'add', u'value': '{container}/{object}'.format( - container=container_name, - object=image_name), + container=self.container_name, + object=self.image_name), u'path': u'/owner_specified.openstack.object'}, {u'op': u'add', u'value': fakes.NO_MD5, u'path': u'/owner_specified.openstack.md5'}, {u'op': u'add', u'value': fakes.NO_SHA256, u'path': u'/owner_specified.openstack.sha256'}], - key=operator.itemgetter('value')), + key=operator.itemgetter('value')), headers={ 'Content-Type': 'application/openstack-images-v2.1-json-patch'}) ), dict(method='HEAD', uri='{endpoint}/{container}/{object}'.format( - endpoint=endpoint, container=container_name, - object=image_name), + endpoint=endpoint, container=self.container_name, + object=self.image_name), headers={ 'X-Timestamp': '1429036140.50253', 'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1', @@ -380,15 +382,94 @@ def test_create_image_task(self): 'Etag': fakes.NO_MD5}), dict(method='DELETE', uri='{endpoint}/{container}/{object}'.format( - endpoint=endpoint, container=container_name, - object=image_name)), + endpoint=endpoint, container=self.container_name, + object=self.image_name)), dict(method='GET', uri='https://image.example.com/v2/images', json=self.fake_search_return) ]) self.cloud.create_image( - image_name, self.imagefile.name, wait=True, timeout=1, - is_public=False, container=container_name) + self.image_name, self.imagefile.name, wait=True, timeout=1, + is_public=False, container=self.container_name) + + self.assert_calls() + + def test_delete_autocreated_no_tasks(self): + self.use_nothing() + self.cloud.image_api_use_tasks = False + deleted = self.cloud.delete_autocreated_image_objects( + container=self.container_name) + self.assertFalse(deleted) + self.assert_calls() + + def test_delete_autocreated_image_objects(self): + self.use_keystone_v3() + self.cloud.image_api_use_tasks = True + endpoint = self.cloud._object_store_client.get_endpoint() + other_image = self.getUniqueString('no-delete') + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + service_type='object-store', + resource=self.container_name, + qs_elements=['format=json']), + json=[{ + 'content_type': 'application/octet-stream', + 'bytes': 1437258240, + 'hash': '249219347276c331b87bf1ac2152d9af', + 'last_modified': '2015-02-16T17:50:05.289600', + 'name': other_image, + }, { + 'content_type': 'application/octet-stream', + 'bytes': 1290170880, + 'hash': fakes.NO_MD5, + 'last_modified': '2015-04-14T18:29:00.502530', + 'name': self.image_name, + }]), + dict(method='HEAD', + uri=self.get_mock_url( + service_type='object-store', + resource=self.container_name, + append=[other_image]), + headers={ + 'X-Timestamp': '1429036140.50253', + 'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1', + 'Content-Length': '1290170880', + 'Last-Modified': 'Tue, 14 Apr 2015 18:29:01 GMT', + 'X-Object-Meta-X-Shade-Sha256': 'does not matter', + 'X-Object-Meta-X-Shade-Md5': 'does not matter', + 'Date': 'Thu, 16 Nov 2017 15:24:30 GMT', + 'Accept-Ranges': 'bytes', + 'Content-Type': 'application/octet-stream', + 'Etag': '249219347276c331b87bf1ac2152d9af', + }), + dict(method='HEAD', + uri=self.get_mock_url( + service_type='object-store', + resource=self.container_name, + append=[self.image_name]), + headers={ + 'X-Timestamp': '1429036140.50253', + 'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1', + 'Content-Length': '1290170880', + 'Last-Modified': 'Tue, 14 Apr 2015 18:29:01 GMT', + 'X-Object-Meta-X-Shade-Sha256': fakes.NO_SHA256, + 'X-Object-Meta-X-Shade-Md5': fakes.NO_MD5, + 'Date': 'Thu, 16 Nov 2017 15:24:30 GMT', + 'Accept-Ranges': 'bytes', + 'Content-Type': 'application/octet-stream', + openstackcloud.OBJECT_AUTOCREATE_KEY: 'true', + 'Etag': fakes.NO_MD5}), + dict(method='DELETE', + uri='{endpoint}/{container}/{object}'.format( + endpoint=endpoint, container=self.container_name, + object=self.image_name)), + ]) + + deleted = self.cloud.delete_autocreated_image_objects( + container=self.container_name) + self.assertTrue(deleted) self.assert_calls() diff --git a/releasenotes/notes/delete-autocreated-1839187b0aa35022.yaml b/releasenotes/notes/delete-autocreated-1839187b0aa35022.yaml new file mode 100644 index 000000000..a0c2f8d76 --- /dev/null +++ b/releasenotes/notes/delete-autocreated-1839187b0aa35022.yaml @@ -0,0 +1,5 @@ +--- +features: + - Added new method, delete_autocreated_image_objects + that can be used to delete any leaked objects shade + may have created on behalf of the user. From 910fc10427596cadea422cb6f24cf17d61d118a1 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Sun, 10 Sep 2017 16:23:00 +0000 Subject: [PATCH 1870/3836] Complete move of baremetal machine tests Migrating tests to wrap up machine actions. Ports will be migrated separately as there will also need to be portgroups, which could be wrapped into the same file/class depending on how that ends up being implemented. Change-Id: I239bbc134f42a47e8df75531dbd50916a97a6068 --- openstack/cloud/_tasks.py | 109 ++-- openstack/cloud/operatorcloud.py | 25 +- openstack/tests/fakes.py | 14 +- .../tests/unit/cloud/test_baremetal_node.py | 542 ++++++++++++++++++ openstack/tests/unit/cloud/test_image.py | 4 +- openstack/tests/unit/cloud/test_operator.py | 170 ++++++ 6 files changed, 804 insertions(+), 60 deletions(-) create mode 100644 openstack/tests/unit/cloud/test_operator.py diff --git a/openstack/cloud/_tasks.py b/openstack/cloud/_tasks.py index 3ad6b8592..a41502f4f 100644 --- a/openstack/cloud/_tasks.py +++ b/openstack/cloud/_tasks.py @@ -17,81 +17,94 @@ from openstack import task_manager -class MachineCreate(task_manager.Task): - def main(self, client): - return client.ironic_client.node.create(**self.args) +class IronicTask(task_manager.Task): + def __init__(self, client, **kwargs): + super(IronicTask, self).__init__(**kwargs) + self.client = client -class MachineDelete(task_manager.Task): - def main(self, client): - return client.ironic_client.node.delete(**self.args) +class MachineCreate(IronicTask): + def main(self): + return self.client.ironic_client.node.create(*self.args, **self.kwargs) -class MachinePatch(task_manager.Task): - def main(self, client): - return client.ironic_client.node.update(**self.args) +class MachineDelete(IronicTask): + def main(self): + return self.client.ironic_client.node.delete(*self.args, **self.kwargs) -class MachinePortGet(task_manager.Task): - def main(self, client): - return client.ironic_client.port.get(**self.args) +class MachinePatch(IronicTask): + def main(self): + return self.client.ironic_client.node.update(*self.args, **self.kwargs) -class MachinePortGetByAddress(task_manager.Task): - def main(self, client): - return client.ironic_client.port.get_by_address(**self.args) +class MachinePortGet(IronicTask): + def main(self): + return self.client.ironic_client.port.get(*self.args, **self.kwargs) -class MachinePortCreate(task_manager.Task): - def main(self, client): - return client.ironic_client.port.create(**self.args) +class MachinePortGetByAddress(IronicTask): + def main(self): + return self.client.ironic_client.port.get_by_address( + *self.args, **self.kwargs) -class MachinePortDelete(task_manager.Task): - def main(self, client): - return client.ironic_client.port.delete(**self.args) +class MachinePortCreate(IronicTask): + def main(self): + return self.client.ironic_client.port.create(*self.args, **self.kwargs) -class MachinePortList(task_manager.Task): - def main(self, client): - return client.ironic_client.port.list() +class MachinePortDelete(IronicTask): + def main(self): + return self.client.ironic_client.port.delete(*self.args, **self.kwargs) -class MachineNodeGet(task_manager.Task): - def main(self, client): - return client.ironic_client.node.get(**self.args) +class MachinePortList(IronicTask): + def main(self): + return self.client.ironic_client.port.list() -class MachineNodeList(task_manager.Task): - def main(self, client): - return client.ironic_client.node.list(**self.args) +class MachineNodeGet(IronicTask): + def main(self): + return self.client.ironic_client.node.get(*self.args, **self.kwargs) -class MachineNodePortList(task_manager.Task): - def main(self, client): - return client.ironic_client.node.list_ports(**self.args) +class MachineNodeList(IronicTask): + def main(self): + return self.client.ironic_client.node.list(*self.args, **self.kwargs) -class MachineNodeUpdate(task_manager.Task): - def main(self, client): - return client.ironic_client.node.update(**self.args) +class MachineNodePortList(IronicTask): + def main(self): + return self.client.ironic_client.node.list_ports( + *self.args, **self.kwargs) -class MachineNodeValidate(task_manager.Task): - def main(self, client): - return client.ironic_client.node.validate(**self.args) +class MachineNodeUpdate(IronicTask): + def main(self): + return self.client.ironic_client.node.update(*self.args, **self.kwargs) -class MachineSetMaintenance(task_manager.Task): - def main(self, client): - return client.ironic_client.node.set_maintenance(**self.args) +class MachineNodeValidate(IronicTask): + def main(self): + return self.client.ironic_client.node.validate( + *self.args, **self.kwargs) -class MachineSetPower(task_manager.Task): - def main(self, client): - return client.ironic_client.node.set_power_state(**self.args) +class MachineSetMaintenance(IronicTask): + def main(self): + return self.client.ironic_client.node.set_maintenance( + *self.args, **self.kwargs) -class MachineSetProvision(task_manager.Task): - def main(self, client): - return client.ironic_client.node.set_provision_state(**self.args) + +class MachineSetPower(IronicTask): + def main(self): + return self.client.ironic_client.node.set_power_state( + *self.args, **self.kwargs) + + +class MachineSetProvision(IronicTask): + def main(self): + return self.client.ironic_client.node.set_provision_state( + *self.args, **self.kwargs) diff --git a/openstack/cloud/operatorcloud.py b/openstack/cloud/operatorcloud.py index 283ad802e..beba3a598 100644 --- a/openstack/cloud/operatorcloud.py +++ b/openstack/cloud/operatorcloud.py @@ -16,6 +16,7 @@ import munch from openstack.cloud.exc import * # noqa +from openstack.cloud import meta from openstack.cloud import openstackcloud from openstack.cloud import _tasks from openstack.cloud import _utils @@ -35,27 +36,28 @@ class OperatorCloud(openstackcloud.OpenStackCloud): def list_nics(self): with _utils.shade_exceptions("Error fetching machine port list"): - return self.manager.submit_task(_tasks.MachinePortList()) + return meta.obj_list_to_munch( + self.manager.submit_task(_tasks.MachinePortList(self))) def list_nics_for_machine(self, uuid): with _utils.shade_exceptions( "Error fetching port list for node {node_id}".format( node_id=uuid)): - return self.manager.submit_task( - _tasks.MachineNodePortList(node_id=uuid)) + return meta.obj_list_to_munch(self.manager.submit_task( + _tasks.MachineNodePortList(self, node_id=uuid))) def get_nic_by_mac(self, mac): """Get Machine by MAC address""" # TODO(shade) Finish porting ironic to REST/sdk # try: # return self.manager.submit_task( - # _tasks.MachineNodePortGet(port_id=mac)) + # _tasks.MachineNodePortGet(self, port_id=mac)) # except ironic_exceptions.ClientException: # return None def list_machines(self): return self._normalize_machines( - self.manager.submit_task(_tasks.MachineNodeList())) + self.manager.submit_task(_tasks.MachineNodeList(self))) def get_machine(self, name_or_id): """Get Machine by name or uuid @@ -211,7 +213,7 @@ def register_machine(self, nics, wait=False, timeout=3600, nic = self.manager.submit_task( _tasks.MachinePortCreate(address=row['mac'], node_uuid=machine['uuid'])) - created_nics.append(nic.uuid) + created_nics.append(nic['uuid']) except Exception as e: self.log.debug("ironic NIC registration failed", exc_info=True) @@ -277,7 +279,10 @@ def register_machine(self, nics, wait=False, timeout=3600, machine = self.get_machine(machine['uuid']) if (machine['reservation'] is None and machine['provision_state'] is not 'enroll'): - + # NOTE(TheJulia): In this case, the node has + # has moved on from the previous state and is + # likely not being verified, as no lock is + # present on the node. self.node_set_provision_state( machine['uuid'], 'provide') machine = self.get_machine(machine['uuid']) @@ -292,8 +297,10 @@ def register_machine(self, nics, wait=False, timeout=3600, raise OpenStackCloudException( "Machine encountered a failure: %s" % machine['last_error']) - - return machine + if not isinstance(machine, str): + return self._normalize_machine(machine) + else: + return machine def unregister_machine(self, nics, uuid, wait=False, timeout=600): """Unregister Baremetal from Ironic diff --git a/openstack/tests/fakes.py b/openstack/tests/fakes.py index cc35723f5..a0cfb74be 100644 --- a/openstack/tests/fakes.py +++ b/openstack/tests/fakes.py @@ -248,6 +248,16 @@ def make_fake_machine(machine_name, machine_id=None): id=machine_id, name=machine_name)) +def make_fake_port(address, node_id=None, port_id=None): + if not node_id: + node_id = uuid.uuid4().hex + if not port_id: + port_id = uuid.uuid4().hex + return meta.obj_to_munch(FakeMachinePort( + id=port_id, + address=address, + node_id=node_id)) + class FakeFloatingIP(object): def __init__(self, id, pool, ip, fixed_ip, instance_id): @@ -346,7 +356,7 @@ def __init__( class FakeMachine(object): def __init__(self, id, name=None, driver=None, driver_info=None, chassis_uuid=None, instance_info=None, instance_uuid=None, - properties=None): + properties=None, reservation=None, last_error=None): self.uuid = id self.name = name self.driver = driver @@ -355,6 +365,8 @@ def __init__(self, id, name=None, driver=None, driver_info=None, self.instance_info = instance_info self.instance_uuid = instance_uuid self.properties = properties + self.reservation = reservation + self.last_error = last_error class FakeMachinePort(object): diff --git a/openstack/tests/unit/cloud/test_baremetal_node.py b/openstack/tests/unit/cloud/test_baremetal_node.py index 837b51917..cd76c5a5c 100644 --- a/openstack/tests/unit/cloud/test_baremetal_node.py +++ b/openstack/tests/unit/cloud/test_baremetal_node.py @@ -34,6 +34,12 @@ def setUp(self): self.skipTest("Ironic operations not supported yet") self.fake_baremetal_node = fakes.make_fake_machine( self.name, self.uuid) + # TODO(TheJulia): Some tests below have fake ports, + # since they are required in some processes. Lets refactor + # them at some point to use self.fake_baremetal_port. + self.fake_baremetal_port = fakes.make_fake_port( + '00:01:02:03:04:05', + node_id=self.uuid) def test_list_machines(self): fake_baremetal_two = fakes.make_fake_machine('two', str(uuid.uuid4())) @@ -822,6 +828,542 @@ def test_deactivate_node(self): self.assertIsNone(return_value) self.assert_calls() + def test_register_machine(self): + mac_address = '00:01:02:03:04:05' + nics = [{'mac': mac_address}] + node_uuid = self.fake_baremetal_node['uuid'] + # TODO(TheJulia): There is a lot of duplication + # in testing creation. Surely this hsould be a helper + # or something. We should fix this, after we have + # ironicclient removed, as in the mean time visibility + # will be helpful. + node_to_post = { + 'chassis_uuid': None, + 'driver': None, + 'driver_info': None, + 'name': self.fake_baremetal_node['name'], + 'properties': None, + 'uuid': node_uuid} + self.fake_baremetal_node['provision_state'] = 'available' + if 'provision_state' in node_to_post: + node_to_post.pop('provision_state') + self.register_uris([ + dict( + method='POST', + uri=self.get_mock_url( + resource='nodes'), + json=self.fake_baremetal_node, + validate=dict(json=node_to_post)), + dict( + method='POST', + uri=self.get_mock_url( + resource='ports'), + validate=dict(json={'address': mac_address, + 'node_uuid': node_uuid}), + json=self.fake_baremetal_port), + ]) + return_value = self.op_cloud.register_machine(nics, **node_to_post) + + self.assertDictEqual(self.fake_baremetal_node, return_value) + self.assert_calls() + + # TODO(TheJulia): After we remove ironicclient, + # we need to de-duplicate these tests. Possibly + # a dedicated class, although we should do it then + # as we may find differences that need to be accounted + # for newer microversions. + def test_register_machine_enroll(self): + mac_address = '00:01:02:03:04:05' + nics = [{'mac': mac_address}] + node_uuid = self.fake_baremetal_node['uuid'] + node_to_post = { + 'chassis_uuid': None, + 'driver': None, + 'driver_info': None, + 'name': self.fake_baremetal_node['name'], + 'properties': None, + 'uuid': node_uuid} + self.fake_baremetal_node['provision_state'] = 'enroll' + manageable_node = self.fake_baremetal_node.copy() + manageable_node['provision_state'] = 'manageable' + available_node = self.fake_baremetal_node.copy() + available_node['provision_state'] = 'available' + self.register_uris([ + dict( + method='POST', + uri=self.get_mock_url( + resource='nodes'), + validate=dict(json=node_to_post), + json=self.fake_baremetal_node), + dict( + method='POST', + uri=self.get_mock_url( + resource='ports'), + validate=dict(json={'address': mac_address, + 'node_uuid': node_uuid}), + json=self.fake_baremetal_port), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'provision']), + validate=dict(json={'target': 'manage'})), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=manageable_node), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=manageable_node), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'provision']), + validate=dict(json={'target': 'provide'})), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=available_node), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=available_node), + ]) + # NOTE(When we migrate to a newer microversion, this test + # may require revision. It was written for microversion + # ?1.13?, which accidently got reverted to 1.6 at one + # point during code being refactored soon after the + # change landed. Presently, with the lock at 1.6, + # this code is never used in the current code path. + return_value = self.op_cloud.register_machine(nics, **node_to_post) + + self.assertDictEqual(available_node, return_value) + self.assert_calls() + + def test_register_machine_enroll_wait(self): + mac_address = self.fake_baremetal_port + nics = [{'mac': mac_address}] + node_uuid = self.fake_baremetal_node['uuid'] + node_to_post = { + 'chassis_uuid': None, + 'driver': None, + 'driver_info': None, + 'name': self.fake_baremetal_node['name'], + 'properties': None, + 'uuid': node_uuid} + self.fake_baremetal_node['provision_state'] = 'enroll' + manageable_node = self.fake_baremetal_node.copy() + manageable_node['provision_state'] = 'manageable' + available_node = self.fake_baremetal_node.copy() + available_node['provision_state'] = 'available' + self.register_uris([ + dict( + method='POST', + uri=self.get_mock_url( + resource='nodes'), + validate=dict(json=node_to_post), + json=self.fake_baremetal_node), + dict( + method='POST', + uri=self.get_mock_url( + resource='ports'), + validate=dict(json={'address': mac_address, + 'node_uuid': node_uuid}), + json=self.fake_baremetal_port), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'provision']), + validate=dict(json={'target': 'manage'})), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=manageable_node), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'provision']), + validate=dict(json={'target': 'provide'})), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=available_node), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=available_node), + ]) + return_value = self.op_cloud.register_machine(nics, wait=True, + **node_to_post) + + self.assertDictEqual(available_node, return_value) + self.assert_calls() + + def test_register_machine_enroll_failure(self): + mac_address = '00:01:02:03:04:05' + nics = [{'mac': mac_address}] + node_uuid = self.fake_baremetal_node['uuid'] + node_to_post = { + 'chassis_uuid': None, + 'driver': None, + 'driver_info': None, + 'name': self.fake_baremetal_node['name'], + 'properties': None, + 'uuid': node_uuid} + self.fake_baremetal_node['provision_state'] = 'enroll' + failed_node = self.fake_baremetal_node.copy() + failed_node['reservation'] = 'conductor0' + failed_node['provision_state'] = 'verifying' + failed_node['last_error'] = 'kaboom!' + self.register_uris([ + dict( + method='POST', + uri=self.get_mock_url( + resource='nodes'), + json=self.fake_baremetal_node, + validate=dict(json=node_to_post)), + dict( + method='POST', + uri=self.get_mock_url( + resource='ports'), + validate=dict(json={'address': mac_address, + 'node_uuid': node_uuid}), + json=self.fake_baremetal_port), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'provision']), + validate=dict(json={'target': 'manage'})), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=failed_node), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=failed_node), + ]) + + self.assertRaises( + exc.OpenStackCloudException, + self.op_cloud.register_machine, + nics, + **node_to_post) + self.assert_calls() + + def test_register_machine_enroll_timeout(self): + mac_address = '00:01:02:03:04:05' + nics = [{'mac': mac_address}] + node_uuid = self.fake_baremetal_node['uuid'] + node_to_post = { + 'chassis_uuid': None, + 'driver': None, + 'driver_info': None, + 'name': self.fake_baremetal_node['name'], + 'properties': None, + 'uuid': node_uuid} + self.fake_baremetal_node['provision_state'] = 'enroll' + busy_node = self.fake_baremetal_node.copy() + busy_node['reservation'] = 'conductor0' + busy_node['provision_state'] = 'verifying' + self.register_uris([ + dict( + method='POST', + uri=self.get_mock_url( + resource='nodes'), + json=self.fake_baremetal_node, + validate=dict(json=node_to_post)), + dict( + method='POST', + uri=self.get_mock_url( + resource='ports'), + validate=dict(json={'address': mac_address, + 'node_uuid': node_uuid}), + json=self.fake_baremetal_port), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'provision']), + validate=dict(json={'target': 'manage'})), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=busy_node), + ]) + # NOTE(TheJulia): This test shortcircuits the timeout loop + # such that it executes only once. The very last returned + # state to the API is essentially a busy state that we + # want to block on until it has cleared. + self.assertRaises( + exc.OpenStackCloudException, + self.op_cloud.register_machine, + nics, + timeout=0.001, + lock_timeout=0.001, + **node_to_post) + self.assert_calls() + + def test_register_machine_enroll_timeout_wait(self): + mac_address = '00:01:02:03:04:05' + nics = [{'mac': mac_address}] + node_uuid = self.fake_baremetal_node['uuid'] + node_to_post = { + 'chassis_uuid': None, + 'driver': None, + 'driver_info': None, + 'name': self.fake_baremetal_node['name'], + 'properties': None, + 'uuid': node_uuid} + self.fake_baremetal_node['provision_state'] = 'enroll' + self.register_uris([ + dict( + method='POST', + uri=self.get_mock_url( + resource='nodes'), + json=self.fake_baremetal_node, + validate=dict(json=node_to_post)), + dict( + method='POST', + uri=self.get_mock_url( + resource='ports'), + validate=dict(json={'address': mac_address, + 'node_uuid': node_uuid}), + json=self.fake_baremetal_port), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'provision']), + validate=dict(json={'target': 'manage'})), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + ]) + self.assertRaises( + exc.OpenStackCloudException, + self.op_cloud.register_machine, + nics, + wait=True, + timeout=0.001, + **node_to_post) + self.assert_calls() + + def test_register_machine_port_create_failed(self): + mac_address = '00:01:02:03:04:05' + nics = [{'mac': mac_address}] + node_uuid = self.fake_baremetal_node['uuid'] + node_to_post = { + 'chassis_uuid': None, + 'driver': None, + 'driver_info': None, + 'name': self.fake_baremetal_node['name'], + 'properties': None, + 'uuid': node_uuid} + self.fake_baremetal_node['provision_state'] = 'available' + self.register_uris([ + dict( + method='POST', + uri=self.get_mock_url( + resource='nodes'), + json=self.fake_baremetal_node, + validate=dict(json=node_to_post)), + dict( + method='POST', + uri=self.get_mock_url( + resource='ports'), + status_code=400, + json={'error': 'invalid'}, + validate=dict(json={'address': mac_address, + 'node_uuid': node_uuid})), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']])), + ]) + self.assertRaises(exc.OpenStackCloudException, + self.op_cloud.register_machine, + nics, **node_to_post) + + self.assert_calls() + + def test_unregister_machine(self): + mac_address = self.fake_baremetal_port['address'] + nics = [{'mac': mac_address}] + port_uuid = self.fake_baremetal_port['uuid'] + # NOTE(TheJulia): The two values below should be the same. + port_node_uuid = self.fake_baremetal_port['node_uuid'] + port_url_address = 'detail?address=%s' % mac_address + self.fake_baremetal_node['provision_state'] = 'available' + self.register_uris([ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + dict( + method='GET', + uri=self.get_mock_url( + resource='ports', + append=[port_url_address]), + json={'ports': [{'address': mac_address, + 'node_uuid': port_node_uuid, + 'uuid': port_uuid}]}), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='ports', + append=[self.fake_baremetal_port['uuid']])), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']])), + ]) + + self.op_cloud.unregister_machine(nics, + self.fake_baremetal_node['uuid']) + + self.assert_calls() + + def test_unregister_machine_timeout(self): + mac_address = self.fake_baremetal_port['address'] + nics = [{'mac': mac_address}] + port_uuid = self.fake_baremetal_port['uuid'] + port_node_uuid = self.fake_baremetal_port['node_uuid'] + port_url_address = 'detail?address=%s' % mac_address + self.fake_baremetal_node['provision_state'] = 'available' + self.register_uris([ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + dict( + method='GET', + uri=self.get_mock_url( + resource='ports', + append=[port_url_address]), + json={'ports': [{'address': mac_address, + 'node_uuid': port_node_uuid, + 'uuid': port_uuid}]}), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='ports', + append=[self.fake_baremetal_port['uuid']])), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']])), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + ]) + self.assertRaises( + exc.OpenStackCloudException, + self.op_cloud.unregister_machine, + nics, + self.fake_baremetal_node['uuid'], + wait=True, + timeout=0.001) + + self.assert_calls() + + def test_unregister_machine_unavailable(self): + # This is a list of invalid states that the method + # should fail on. + invalid_states = ['active', 'cleaning', 'clean wait', 'clean failed'] + mac_address = self.fake_baremetal_port['address'] + nics = [{'mac': mac_address}] + url_list = [] + for state in invalid_states: + self.fake_baremetal_node['provision_state'] = state + url_list.append( + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node)) + + self.register_uris(url_list) + + for state in invalid_states: + self.assertRaises( + exc.OpenStackCloudException, + self.op_cloud.unregister_machine, + nics, + self.fake_baremetal_node['uuid']) + + self.assert_calls() + def test_update_machine_patch_no_action(self): self.register_uris([dict( method='GET', diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 5df8ac1ac..5d4b8a4b7 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -374,8 +374,8 @@ def test_create_image_task(self): 'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1', 'Content-Length': '1290170880', 'Last-Modified': 'Tue, 14 Apr 2015 18:29:01 GMT', - 'X-Object-Meta-X-Shade-Sha256': fakes.NO_SHA256, - 'X-Object-Meta-X-Shade-Md5': fakes.NO_MD5, + 'X-Object-Meta-X-Sdk-Sha256': fakes.NO_SHA256, + 'X-Object-Meta-X-Sdk-Md5': fakes.NO_MD5, 'Date': 'Thu, 16 Nov 2017 15:24:30 GMT', 'Accept-Ranges': 'bytes', 'Content-Type': 'application/octet-stream', diff --git a/openstack/tests/unit/cloud/test_operator.py b/openstack/tests/unit/cloud/test_operator.py new file mode 100644 index 000000000..068e735d8 --- /dev/null +++ b/openstack/tests/unit/cloud/test_operator.py @@ -0,0 +1,170 @@ +# 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 munch +import testtools + +import openstack +from openstack.cloud import exc +from openstack.cloud import meta +from openstack.config import cloud_config +from openstack.tests import fakes +from openstack.tests.unit import base + + +class TestOperatorCloud(base.RequestsMockTestCase): + + def setUp(self): + super(TestOperatorCloud, self).setUp() + + def test_operator_cloud(self): + self.assertIsInstance(self.op_cloud, openstack.OperatorCloud) + + @mock.patch.object(openstack.OperatorCloud, 'ironic_client') + def test_list_nics(self, mock_client): + port1 = fakes.FakeMachinePort(1, "aa:bb:cc:dd", "node1") + port2 = fakes.FakeMachinePort(2, "dd:cc:bb:aa", "node2") + port_list = [port1, port2] + port_dict_list = meta.obj_list_to_munch(port_list) + + mock_client.port.list.return_value = port_list + nics = self.op_cloud.list_nics() + + self.assertTrue(mock_client.port.list.called) + self.assertEqual(port_dict_list, nics) + + @mock.patch.object(openstack.OperatorCloud, 'ironic_client') + def test_list_nics_failure(self, mock_client): + mock_client.port.list.side_effect = Exception() + self.assertRaises(exc.OpenStackCloudException, + self.op_cloud.list_nics) + + @mock.patch.object(openstack.OperatorCloud, 'ironic_client') + def test_list_nics_for_machine(self, mock_client): + mock_client.node.list_ports.return_value = [] + self.op_cloud.list_nics_for_machine("123") + mock_client.node.list_ports.assert_called_with(node_id="123") + + @mock.patch.object(openstack.OperatorCloud, 'ironic_client') + def test_list_nics_for_machine_failure(self, mock_client): + mock_client.node.list_ports.side_effect = Exception() + self.assertRaises(exc.OpenStackCloudException, + self.op_cloud.list_nics_for_machine, None) + + @mock.patch.object(openstack.OpenStackCloud, '_image_client') + def test_get_image_name(self, mock_client): + + fake_image = munch.Munch( + id='22', + name='22 name', + status='success') + mock_client.get.return_value = [fake_image] + self.assertEqual('22 name', self.op_cloud.get_image_name('22')) + self.assertEqual('22 name', self.op_cloud.get_image_name('22 name')) + + @mock.patch.object(openstack.OpenStackCloud, '_image_client') + def test_get_image_id(self, mock_client): + + fake_image = munch.Munch( + id='22', + name='22 name', + status='success') + mock_client.get.return_value = [fake_image] + self.assertEqual('22', self.op_cloud.get_image_id('22')) + self.assertEqual('22', self.op_cloud.get_image_id('22 name')) + + @mock.patch.object(cloud_config.CloudConfig, 'get_endpoint') + def test_get_session_endpoint_provided(self, fake_get_endpoint): + fake_get_endpoint.return_value = 'http://fake.url' + self.assertEqual( + 'http://fake.url', self.op_cloud.get_session_endpoint('image')) + + @mock.patch.object(cloud_config.CloudConfig, 'get_session') + def test_get_session_endpoint_session(self, get_session_mock): + session_mock = mock.Mock() + session_mock.get_endpoint.return_value = 'http://fake.url' + get_session_mock.return_value = session_mock + self.assertEqual( + 'http://fake.url', self.op_cloud.get_session_endpoint('image')) + + @mock.patch.object(cloud_config.CloudConfig, 'get_session') + def test_get_session_endpoint_exception(self, get_session_mock): + class FakeException(Exception): + pass + + def side_effect(*args, **kwargs): + raise FakeException("No service") + session_mock = mock.Mock() + session_mock.get_endpoint.side_effect = side_effect + get_session_mock.return_value = session_mock + self.op_cloud.name = 'testcloud' + self.op_cloud.region_name = 'testregion' + with testtools.ExpectedException( + exc.OpenStackCloudException, + "Error getting image endpoint on testcloud:testregion:" + " No service"): + self.op_cloud.get_session_endpoint("image") + + @mock.patch.object(cloud_config.CloudConfig, 'get_session') + def test_get_session_endpoint_unavailable(self, get_session_mock): + session_mock = mock.Mock() + session_mock.get_endpoint.return_value = None + get_session_mock.return_value = session_mock + image_endpoint = self.op_cloud.get_session_endpoint("image") + self.assertIsNone(image_endpoint) + + @mock.patch.object(cloud_config.CloudConfig, 'get_session') + def test_get_session_endpoint_identity(self, get_session_mock): + session_mock = mock.Mock() + get_session_mock.return_value = session_mock + self.op_cloud.get_session_endpoint('identity') + kwargs = dict( + interface='public', region_name='RegionOne', + service_name=None, service_type='identity') + + session_mock.get_endpoint.assert_called_with(**kwargs) + + @mock.patch.object(cloud_config.CloudConfig, 'get_session') + def test_has_service_no(self, get_session_mock): + session_mock = mock.Mock() + session_mock.get_endpoint.return_value = None + get_session_mock.return_value = session_mock + self.assertFalse(self.op_cloud.has_service("image")) + + @mock.patch.object(cloud_config.CloudConfig, 'get_session') + def test_has_service_yes(self, get_session_mock): + session_mock = mock.Mock() + session_mock.get_endpoint.return_value = 'http://fake.url' + get_session_mock.return_value = session_mock + self.assertTrue(self.op_cloud.has_service("image")) + + def test_list_hypervisors(self): + '''This test verifies that calling list_hypervisors results in a call + to nova client.''' + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-hypervisors', 'detail']), + json={'hypervisors': [ + fakes.make_fake_hypervisor('1', 'testserver1'), + fakes.make_fake_hypervisor('2', 'testserver2'), + ]}), + ]) + + r = self.op_cloud.list_hypervisors() + + self.assertEqual(2, len(r)) + self.assertEqual('testserver1', r[0]['hypervisor_hostname']) + self.assertEqual('testserver2', r[1]['hypervisor_hostname']) + + self.assert_calls() From be9da751a2e80f2b78caae50d6e6cce8c2ee51e4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 19 Sep 2017 14:40:53 -0500 Subject: [PATCH 1871/3836] Treat clouds.yaml with one cloud like envvars If there is only one cloud and that cloud is envvars, things work as expected. If there is only one cloud in clouds.yaml and no envvars cloud, we throw an error, even though it should be obvious which cloud was intended. Change-Id: Ia49d0fb2cc7dca36476d0e5ae3fe2b2aa1209e59 --- openstack/config/loader.py | 11 +++++++++++ openstack/tests/unit/config/test_config.py | 19 +++++++++++++++++++ .../notes/default-cloud-7ee0bcb9e5dd24b9.yaml | 7 +++++++ 3 files changed, 37 insertions(+) create mode 100644 releasenotes/notes/default-cloud-7ee0bcb9e5dd24b9.yaml diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 9049ebf74..58017ecb8 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -263,6 +263,17 @@ def __init__(self, config_files=None, vendor_files=None, if not self.default_cloud: self.default_cloud = self.envvar_key + if not self.default_cloud and self.cloud_config['clouds']: + if len(self.cloud_config['clouds'].keys()) == 1: + # If there is only one cloud just use it. This matches envvars + # behavior and allows for much less typing. + # TODO(mordred) allow someone to mark a cloud as "default" in + # clouds.yaml. + # The next/iter thing is for python3 compat where dict.keys + # returns an iterator but in python2 it's a list. + self.default_cloud = next(iter( + self.cloud_config['clouds'].keys())) + # Finally, fall through and make a cloud that starts with defaults # because we need somewhere to put arguments, and there are neither # config files or env vars diff --git a/openstack/tests/unit/config/test_config.py b/openstack/tests/unit/config/test_config.py index f87b18e25..60ef0cc8d 100644 --- a/openstack/tests/unit/config/test_config.py +++ b/openstack/tests/unit/config/test_config.py @@ -56,6 +56,25 @@ def test_get_one_cloud(self): self.assertIsInstance(cloud, cloud_config.CloudConfig) self.assertEqual(cloud.name, '') + def test_get_one_cloud_default_cloud_from_file(self): + single_conf = base._write_yaml({ + 'clouds': { + 'single': { + 'auth': { + 'auth_url': 'http://example.com/v2', + 'username': 'testuser', + 'password': 'testpass', + 'project_name': 'testproject', + }, + 'region_name': 'test-region', + } + } + }) + c = config.OpenStackConfig(config_files=[single_conf], + vendor_files=[self.vendor_yaml]) + cc = c.get_one_cloud() + self.assertEqual(cc.name, 'single') + def test_get_one_cloud_auth_defaults(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml]) cc = c.get_one_cloud(cloud='_test-cloud_', auth={'username': 'user'}) diff --git a/releasenotes/notes/default-cloud-7ee0bcb9e5dd24b9.yaml b/releasenotes/notes/default-cloud-7ee0bcb9e5dd24b9.yaml new file mode 100644 index 000000000..49aba3c9c --- /dev/null +++ b/releasenotes/notes/default-cloud-7ee0bcb9e5dd24b9.yaml @@ -0,0 +1,7 @@ +--- +issues: + - If there was only one cloud defined in clouds.yaml + os-client-config was requiring the cloud parameter + be passed. This is inconsistent with how the envvars + cloud works which WILL work without setting the cloud + parameter if it's the only cloud. From 3bb4c37c882283762c0e3c9839dca9e9fdf8d834 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 21 Sep 2017 09:06:24 -0500 Subject: [PATCH 1872/3836] Protect against p_opt not having prompt attribute In ansible/ansible#28746 it was reported that there are times when a p_opt is getting here that does not have a prompt attribute. Protecting against that is fairly easy to do. Change-Id: Ia02528f4a107893e480135bc214aa156b8684507 Closes-Bug: #1717906 --- openstack/config/loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 58017ecb8..288c97dde 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -961,7 +961,7 @@ def _validate_auth_correctly(self, config, loader): def option_prompt(self, config, p_opt): """Prompt user for option that requires a value""" if ( - p_opt.prompt is not None and + getattr(p_opt, 'prompt', None) is not None and p_opt.dest not in config['auth'] and self._pw_callback is not None ): From bae63fca37e465d1c86c6c45fff4ec35b9b3a864 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 18 Oct 2017 14:20:55 +0200 Subject: [PATCH 1873/3836] Update make_rest_client to work with version discovery Using make_rest_client on clouds that put unversioned endpoints in the catalog results in incorrectly set up adapters. Add the plumbing to get_session_client to pass version args to keystoneauth. Then use that from make_rest_client. Change-Id: I69ad746f672ef0b12680e9db3c7b0c691c9f87e4 --- openstack/config/__init__.py | 4 +-- openstack/config/cloud_config.py | 29 +++++++++++++++++-- ...nt-version-discovery-84125700f159491a.yaml | 6 ++++ 3 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/make-rest-client-version-discovery-84125700f159491a.yaml diff --git a/openstack/config/__init__.py b/openstack/config/__init__.py index 5fe8fcede..22ba51c1b 100644 --- a/openstack/config/__init__.py +++ b/openstack/config/__init__.py @@ -40,7 +40,7 @@ def get_config( def make_rest_client( service_key, options=None, - app_name=None, app_version=None, + app_name=None, app_version=None, version=None, **kwargs): """Simple wrapper function. It has almost no features. @@ -58,7 +58,7 @@ def make_rest_client( service_key=service_key, options=options, app_name=app_name, app_version=app_version, **kwargs) - return cloud.get_session_client(service_key) + return cloud.get_session_client(service_key, version=version) # Backwards compat - simple_client was a terrible name simple_client = make_rest_client # Backwards compat - session_client was a terrible name diff --git a/openstack/config/cloud_config.py b/openstack/config/cloud_config.py index 7f655a396..ea5f0f617 100644 --- a/openstack/config/cloud_config.py +++ b/openstack/config/cloud_config.py @@ -237,7 +237,27 @@ def get_service_catalog(self): """Helper method to grab the service catalog.""" return self._auth.get_access(self.get_session()).service_catalog - def get_session_client(self, service_key): + def _get_version_args(self, service_key, version): + """Translate OCC version args to those needed by ksa adapter. + + If no version is requested explicitly and we have a configured version, + set the version parameter and let ksa deal with expanding that to + min=ver.0, max=ver.latest. + + If version is set, pass it through. + + If version is not set and we don't have a configured version, default + to latest. + """ + if version == 'latest': + return None, None, 'latest' + if not version: + version = self.get_api_version(service_key) + if not version: + return None, None, 'latest' + return version, None, None + + def get_session_client(self, service_key, version=None): """Return a prepped requests adapter for a given service. This is useful for making direct requests calls against a @@ -251,13 +271,18 @@ def get_session_client(self, service_key): and it will work like you think. """ + (version, min_version, max_version) = self._get_version_args( + service_key, version) return adapter.Adapter( session=self.get_session(), service_type=self.get_service_type(service_key), service_name=self.get_service_name(service_key), interface=self.get_interface(service_key), - region_name=self.get_region_name(service_key)) + region_name=self.get_region_name(service_key), + version=version, + min_version=min_version, + max_version=max_version) def _get_highest_endpoint(self, service_types, kwargs): session = self.get_session() diff --git a/releasenotes/notes/make-rest-client-version-discovery-84125700f159491a.yaml b/releasenotes/notes/make-rest-client-version-discovery-84125700f159491a.yaml new file mode 100644 index 000000000..7326978f9 --- /dev/null +++ b/releasenotes/notes/make-rest-client-version-discovery-84125700f159491a.yaml @@ -0,0 +1,6 @@ +--- +features: + - Add version argument to make_rest_client and plumb + version discovery through get_session_client so that + versioned endpoints are properly found if unversioned + are in the catalog. From 6d8633f5dd2e32f8da800e00296398b40ee7bc90 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 20 Oct 2017 16:18:25 +0200 Subject: [PATCH 1874/3836] Added nat_source flag for networks. In some more complex clouds there can not only be more than one valid network on a server that NAT can attach to, there can also be more than one valid network from which to get a NAT address. Allow flagging a network so that it can be found. Change-Id: I3d8dd6d734a1013d2d4a43e11c3538c3a345820b --- doc/source/user/config/network-config.rst | 7 +++++++ openstack/config/cloud_config.py | 7 +++++++ openstack/config/loader.py | 1 + openstack/tests/unit/config/base.py | 1 + openstack/tests/unit/config/test_config.py | 3 +++ releasenotes/notes/nat-source-field-7c7db2a724616d59.yaml | 6 ++++++ 6 files changed, 25 insertions(+) create mode 100644 releasenotes/notes/nat-source-field-7c7db2a724616d59.yaml diff --git a/doc/source/user/config/network-config.rst b/doc/source/user/config/network-config.rst index 09571804e..ea8541478 100644 --- a/doc/source/user/config/network-config.rst +++ b/doc/source/user/config/network-config.rst @@ -58,3 +58,10 @@ by looking for a network that has subnets that have a gateway_ip. But it's possible to have more than one network that satisfies that condition, so the user might want to tell programs which one to pick. There can be only one `nat_destination` per cloud. + +`nat_source` is a boolean field that indicates which network floating +ips should be requested from. It defaults to false. Normally this can be +inferred by looking for a network that is attached to a router. But it's +possible to have more than one network that satisfies that condition, so the +user might want to tell programs which one to pick. There can be only one +`nat_source` per cloud. diff --git a/openstack/config/cloud_config.py b/openstack/config/cloud_config.py index ea5f0f617..e5b2be2a4 100644 --- a/openstack/config/cloud_config.py +++ b/openstack/config/cloud_config.py @@ -583,3 +583,10 @@ def get_nat_destination(self): if net['nat_destination']: return net['name'] return None + + def get_nat_source(self): + """Get network used for NAT source.""" + for net in self.config['networks']: + if net.get('nat_source'): + return net['name'] + return None diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 288c97dde..2c186fe82 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -550,6 +550,7 @@ def _fix_backwards_networks(self, cloud): network = dict( name=name, routes_externally=get_boolean(net.get('routes_externally')), + nat_source=get_boolean(net.get('nat_source')), nat_destination=get_boolean(net.get('nat_destination')), default_interface=get_boolean(net.get('default_interface')), ) diff --git a/openstack/tests/unit/config/base.py b/openstack/tests/unit/config/base.py index 60ca63d12..fab573d16 100644 --- a/openstack/tests/unit/config/base.py +++ b/openstack/tests/unit/config/base.py @@ -102,6 +102,7 @@ 'networks': [{ 'name': 'a-public', 'routes_externally': True, + 'nat_source': True, }, { 'name': 'another-public', 'routes_externally': True, diff --git a/openstack/tests/unit/config/test_config.py b/openstack/tests/unit/config/test_config.py index 60ef0cc8d..b67d04124 100644 --- a/openstack/tests/unit/config/test_config.py +++ b/openstack/tests/unit/config/test_config.py @@ -225,6 +225,7 @@ def test_get_one_cloud_networks(self): self.assertEqual( ['a-private', 'another-private', 'split-no-default'], cc.get_internal_networks()) + self.assertEqual('a-public', cc.get_nat_source()) self.assertEqual('another-private', cc.get_nat_destination()) self.assertEqual('another-public', cc.get_default_network()) self.assertEqual( @@ -240,6 +241,7 @@ def test_get_one_cloud_no_networks(self): cc = c.get_one_cloud('_test-cloud-domain-scoped_') self.assertEqual([], cc.get_external_networks()) self.assertEqual([], cc.get_internal_networks()) + self.assertIsNone(cc.get_nat_source()) self.assertIsNone(cc.get_nat_destination()) self.assertIsNone(cc.get_default_network()) @@ -1020,6 +1022,7 @@ def test_normalize_network(self): 'networks': [ {'name': 'private', 'routes_externally': False, 'nat_destination': False, 'default_interface': False, + 'nat_source': False, 'routes_ipv4_externally': False, 'routes_ipv6_externally': False}, ] diff --git a/releasenotes/notes/nat-source-field-7c7db2a724616d59.yaml b/releasenotes/notes/nat-source-field-7c7db2a724616d59.yaml new file mode 100644 index 000000000..3341c9f25 --- /dev/null +++ b/releasenotes/notes/nat-source-field-7c7db2a724616d59.yaml @@ -0,0 +1,6 @@ +--- +features: + - Added nat_source flag for networks. In some more complex clouds there + can not only be more than one valid network on a server that NAT can + attach to, there can also be more than one valid network from which to + get a NAT address. Allow flagging a network so that it can be found. From f31047c845f2b6739f5d8bece811e9866f135546 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 27 Nov 2017 09:38:16 -0600 Subject: [PATCH 1875/3836] Shift image tests from test_operator There are two image tests in test_operator that really belong in test_image. Move them there. Also, clean up a few things in the image tests (like using self.image_name as the image name for the image fakes) Change-Id: If6b4990bb979265dcbf0c1747632a0516ea5f172 --- openstack/tests/fakes.py | 8 +- openstack/tests/unit/base.py | 1 + openstack/tests/unit/cloud/test_image.py | 260 ++++++++++++++------ openstack/tests/unit/cloud/test_operator.py | 26 -- 4 files changed, 189 insertions(+), 106 deletions(-) diff --git a/openstack/tests/fakes.py b/openstack/tests/fakes.py index a0cfb74be..9188d7b54 100644 --- a/openstack/tests/fakes.py +++ b/openstack/tests/fakes.py @@ -211,7 +211,8 @@ def make_fake_stack_event( def make_fake_image( - image_id=None, md5=NO_MD5, sha256=NO_SHA256, status='active'): + image_id=None, md5=NO_MD5, sha256=NO_SHA256, status='active', + image_name=u'fake_image'): return { u'image_state': u'available', u'container_format': u'bare', @@ -232,12 +233,13 @@ def make_fake_image( u'metadata': {}}], u'min_disk': 40, u'virtual_size': None, - u'name': u'fake_image', + u'name': image_name, u'checksum': u'ee36e35a297980dee1b514de9803ec6d', u'created_at': u'2016-02-10T05:03:11Z', u'owner_specified.openstack.md5': NO_MD5, u'owner_specified.openstack.sha256': NO_SHA256, - u'owner_specified.openstack.object': 'images/fake_image', + u'owner_specified.openstack.object': 'images/{name}'.format( + name=image_name), u'protected': False} diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 0edd69521..676c4eac8 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -173,6 +173,7 @@ def setUp(self, cloud_config_fixture='clouds.yaml'): self.use_keystone_v3() self.__register_uris_called = False + # TODO(shade) Update this to handle service type aliases def get_mock_url(self, service_type, interface='public', resource=None, append=None, base_url_append=None, qs_elements=None): diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 5d4b8a4b7..4fa06cd83 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -39,13 +39,15 @@ class BaseTestImage(base.RequestsMockTestCase): def setUp(self): super(BaseTestImage, self).setUp() self.image_id = str(uuid.uuid4()) + self.image_name = self.getUniqueString('image') + self.object_name = u'images/{name}'.format(name=self.image_name) self.imagefile = tempfile.NamedTemporaryFile(delete=False) self.imagefile.write(b'\0') self.imagefile.close() - self.fake_image_dict = fakes.make_fake_image(image_id=self.image_id) + self.fake_image_dict = fakes.make_fake_image( + image_id=self.image_id, image_name=self.image_name) self.fake_search_return = {'images': [self.fake_image_dict]} self.output = uuid.uuid4().bytes - self.image_name = self.getUniqueString('image') self.container_name = self.getUniqueString('container') @@ -77,12 +79,12 @@ def test_config_v2(self): def test_download_image_no_output(self): self.assertRaises(exc.OpenStackCloudException, - self.cloud.download_image, 'fake_image') + self.cloud.download_image, self.image_name) def test_download_image_two_outputs(self): fake_fd = six.BytesIO() self.assertRaises(exc.OpenStackCloudException, - self.cloud.download_image, 'fake_image', + self.cloud.download_image, self.image_name, output_path='fake_path', output_file=fake_fd) def test_download_image_no_images_found(self): @@ -91,7 +93,7 @@ def test_download_image_no_images_found(self): uri='https://image.example.com/v2/images', json=dict(images=[]))]) self.assertRaises(exc.OpenStackCloudResourceNotFound, - self.cloud.download_image, 'fake_image', + self.cloud.download_image, self.image_name, output_path='fake_path') self.assert_calls() @@ -110,7 +112,7 @@ def _register_image_mocks(self): def test_download_image_with_fd(self): self._register_image_mocks() output_file = six.BytesIO() - self.cloud.download_image('fake_image', output_file=output_file) + self.cloud.download_image(self.image_name, output_file=output_file) output_file.seek(0) self.assertEqual(output_file.read(), self.output) self.assert_calls() @@ -118,14 +120,78 @@ def test_download_image_with_fd(self): def test_download_image_with_path(self): self._register_image_mocks() output_file = tempfile.NamedTemporaryFile() - self.cloud.download_image('fake_image', output_path=output_file.name) + self.cloud.download_image( + self.image_name, output_path=output_file.name) output_file.seek(0) self.assertEqual(output_file.read(), self.output) self.assert_calls() + def test_get_image_name(self, cloud=None): + cloud = cloud or self.cloud + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), + json=self.fake_search_return), + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), + json=self.fake_search_return), + ]) + + self.assertEqual( + self.image_name, cloud.get_image_name(self.image_id)) + self.assertEqual( + self.image_name, cloud.get_image_name(self.image_name)) + + self.assert_calls() + + def test_get_image_by_id(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images', self.image_id], + base_url_append='v2'), + json=self.fake_image_dict) + ]) + self.assertEqual( + self.cloud._normalize_image(self.fake_image_dict), + self.cloud.get_image_by_id(self.image_id)) + self.assert_calls() + + def test_get_image_id(self, cloud=None): + cloud = cloud or self.cloud + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), + json=self.fake_search_return), + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), + json=self.fake_search_return), + ]) + + self.assertEqual( + self.image_id, cloud.get_image_id(self.image_id)) + self.assertEqual( + self.image_id, cloud.get_image_id(self.image_name)) + + self.assert_calls() + + def test_get_image_name_operator(self): + # This should work the same as non-operator, just verifying it does. + self.test_get_image_name(cloud=self.op_cloud) + + def test_get_image_id_operator(self): + # This should work the same as the other test, just verifying it does. + self.test_get_image_id(cloud=self.op_cloud) + def test_empty_list_images(self): self.register_uris([ - dict(method='GET', uri='https://image.example.com/v2/images', + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), json={'images': []}) ]) self.assertEqual([], self.cloud.list_images()) @@ -133,7 +199,9 @@ def test_empty_list_images(self): def test_list_images(self): self.register_uris([ - dict(method='GET', uri='https://image.example.com/v2/images', + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), json=self.fake_search_return) ]) self.assertEqual( @@ -144,7 +212,9 @@ def test_list_images(self): def test_list_images_show_all(self): self.register_uris([ dict(method='GET', - uri='https://image.example.com/v2/images?member_status=all', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2', + qs_elements=['member_status=all']), json=self.fake_search_return) ]) self.assertEqual( @@ -157,7 +227,9 @@ def test_list_images_show_all_deleted(self): deleted_image['status'] = 'deleted' self.register_uris([ dict(method='GET', - uri='https://image.example.com/v2/images?member_status=all', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2', + qs_elements=['member_status=all']), json={'images': [self.fake_image_dict, deleted_image]}) ]) self.assertEqual( @@ -171,7 +243,8 @@ def test_list_images_no_filter_deleted(self): deleted_image['status'] = 'deleted' self.register_uris([ dict(method='GET', - uri='https://image.example.com/v2/images', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), json={'images': [self.fake_image_dict, deleted_image]}) ]) self.assertEqual( @@ -185,7 +258,8 @@ def test_list_images_filter_deleted(self): deleted_image['status'] = 'deleted' self.register_uris([ dict(method='GET', - uri='https://image.example.com/v2/images', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), json={'images': [self.fake_image_dict, deleted_image]}) ]) self.assertEqual( @@ -197,7 +271,9 @@ def test_list_images_string_properties(self): image_dict = self.fake_image_dict.copy() image_dict['properties'] = 'list,of,properties' self.register_uris([ - dict(method='GET', uri='https://image.example.com/v2/images', + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), json={'images': [image_dict]}), ]) images = self.cloud.list_images() @@ -212,13 +288,16 @@ def test_list_images_string_properties(self): def test_list_images_paginated(self): marker = str(uuid.uuid4()) self.register_uris([ - dict(method='GET', uri='https://image.example.com/v2/images', + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), json={'images': [self.fake_image_dict], 'next': '/v2/images?marker={marker}'.format( marker=marker)}), dict(method='GET', - uri=('https://image.example.com/v2/images?' - 'marker={marker}'.format(marker=marker)), + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2', + qs_elements=['marker={marker}'.format(marker=marker)]), json=self.fake_search_return) ]) self.assertEqual( @@ -231,31 +310,37 @@ def test_create_image_put_v2(self): self.cloud.image_api_use_tasks = False self.register_uris([ - dict(method='GET', uri='https://image.example.com/v2/images', + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), json={'images': []}), - dict(method='POST', uri='https://image.example.com/v2/images', + dict(method='POST', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), json=self.fake_image_dict, validate=dict( json={ u'container_format': u'bare', u'disk_format': u'qcow2', - u'name': u'fake_image', + u'name': self.image_name, u'owner_specified.openstack.md5': fakes.NO_MD5, - u'owner_specified.openstack.object': - u'images/fake_image', + u'owner_specified.openstack.object': self.object_name, u'owner_specified.openstack.sha256': fakes.NO_SHA256, u'visibility': u'private'}) ), dict(method='PUT', - uri='https://image.example.com/v2/images/{id}/file'.format( - id=self.image_id), + uri=self.get_mock_url( + 'image', append=['images', self.image_id, 'file'], + base_url_append='v2'), request_headers={'Content-Type': 'application/octet-stream'}), - dict(method='GET', uri='https://image.example.com/v2/images', + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), json=self.fake_search_return) ]) self.cloud.create_image( - 'fake_image', self.imagefile.name, wait=True, timeout=1, + self.image_name, self.imagefile.name, wait=True, timeout=1, is_public=False) self.assert_calls() @@ -281,9 +366,14 @@ def test_create_image_task(self): del(image_no_checksums['owner_specified.openstack.object']) self.register_uris([ - dict(method='GET', uri='https://image.example.com/v2/images', + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), json={'images': []}), - dict(method='GET', uri='https://object-store.example.com/info', + dict(method='GET', + # This is explicitly not using get_mock_url because that + # gets us a project-id oriented URL. + uri='https://object-store.example.com/info', json=dict( swift={'max_file_size': 1000}, slo={'min_segment_size': 500})), @@ -301,15 +391,16 @@ def test_create_image_task(self): dict(method='HEAD', uri='{endpoint}/{container}'.format( endpoint=endpoint, container=self.container_name), - headers={'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', # noqa - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}), + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}), dict(method='HEAD', uri='{endpoint}/{container}/{object}'.format( endpoint=endpoint, container=self.container_name, @@ -324,9 +415,13 @@ def test_create_image_task(self): headers={'x-object-meta-x-sdk-md5': fakes.NO_MD5, 'x-object-meta-x-sdk-sha256': fakes.NO_SHA256}) ), - dict(method='GET', uri='https://image.example.com/v2/images', + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), json={'images': []}), - dict(method='POST', uri='https://image.example.com/v2/tasks', + dict(method='POST', + uri=self.get_mock_url( + 'image', append=['tasks'], base_url_append='v2'), json=args, validate=dict( json=dict( @@ -337,18 +432,21 @@ def test_create_image_task(self): 'image_properties': {'name': self.image_name}})) ), dict(method='GET', - uri='https://image.example.com/v2/tasks/{id}'.format( - id=task_id), + uri=self.get_mock_url( + 'image', append=['tasks', task_id], base_url_append='v2'), status_code=503, text='Random error'), dict(method='GET', - uri='https://image.example.com/v2/tasks/{id}'.format( - id=task_id), + uri=self.get_mock_url( + 'image', append=['tasks', task_id], base_url_append='v2'), json=args), - dict(method='GET', uri='https://image.example.com/v2/images', + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), json={'images': [image_no_checksums]}), dict(method='PATCH', - uri='https://image.example.com/v2/images/{id}'.format( - id=self.image_id), + uri=self.get_mock_url( + 'image', append=['images', self.image_id], + base_url_append='v2'), validate=dict( json=sorted([ {u'op': u'add', @@ -384,7 +482,9 @@ def test_create_image_task(self): uri='{endpoint}/{container}/{object}'.format( endpoint=endpoint, container=self.container_name, object=self.image_name)), - dict(method='GET', uri='https://image.example.com/v2/images', + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), json=self.fake_search_return) ]) @@ -487,11 +587,11 @@ def _call_create_image(self, name, **kwargs): name, imagefile.name, wait=True, timeout=1, is_public=False, **kwargs) + # TODO(shade) Migrate this to requests-mock @mock.patch.object(openstack.OpenStackCloud, '_is_client_version') @mock.patch.object(openstack.OpenStackCloud, '_image_client') def test_create_image_put_v1( self, mock_image_client, mock_is_client_version): - # TODO(mordred) Fix this to use requests_mock mock_is_client_version.return_value = False mock_image_client.get.return_value = [] self.assertEqual([], self.cloud.list_images()) @@ -525,6 +625,7 @@ def test_create_image_put_v1( self.assertEqual( self._munch_images(ret), self.cloud.list_images()) + # TODO(shade) Migrate this to requests-mock @mock.patch.object(openstack.OpenStackCloud, '_is_client_version') @mock.patch.object(openstack.OpenStackCloud, '_image_client') def test_create_image_put_v1_bad_delete( @@ -563,6 +664,7 @@ def test_create_image_put_v1_bad_delete( }) mock_image_client.delete.assert_called_with('/images/42') + # TODO(shade) Migrate this to requests-mock @mock.patch.object(openstack.OpenStackCloud, '_is_client_version') @mock.patch.object(openstack.OpenStackCloud, '_image_client') def test_update_image_no_patch( @@ -594,6 +696,7 @@ def test_update_image_no_patch( mock_image_client.get.assert_called_with('/images', params={}) mock_image_client.patch.assert_not_called() + # TODO(shade) Migrate this to requests-mock @mock.patch.object(openstack.OpenStackCloud, '_is_client_version') @mock.patch.object(openstack.OpenStackCloud, '_image_client') def test_create_image_put_v2_bad_delete( @@ -633,6 +736,7 @@ def test_create_image_put_v2_bad_delete( data=mock.ANY) mock_image_client.delete.assert_called_with('/images/42') + # TODO(shade) Migrate this to requests-mock @mock.patch.object(openstack.OpenStackCloud, '_is_client_version') @mock.patch.object(openstack.OpenStackCloud, '_image_client') def test_create_image_put_bad_int( @@ -645,6 +749,7 @@ def test_create_image_put_bad_int( self._call_create_image, '42 name', min_disk='fish', min_ram=0) mock_image_client.post.assert_not_called() + # TODO(shade) Migrate this to requests-mock @mock.patch.object(openstack.OpenStackCloud, '_is_client_version') @mock.patch.object(openstack.OpenStackCloud, '_image_client') def test_create_image_put_user_int( @@ -680,6 +785,7 @@ def test_create_image_put_user_int( self.assertEqual( self._munch_images(ret), self.cloud.list_images()) + # TODO(shade) Migrate this to requests-mock @mock.patch.object(openstack.OpenStackCloud, '_is_client_version') @mock.patch.object(openstack.OpenStackCloud, '_image_client') def test_create_image_put_meta_int( @@ -709,6 +815,7 @@ def test_create_image_put_meta_int( self.assertEqual( self._munch_images(ret), self.cloud.list_images()) + # TODO(shade) Migrate this to requests-mock @mock.patch.object(openstack.OpenStackCloud, '_is_client_version') @mock.patch.object(openstack.OpenStackCloud, '_image_client') def test_create_image_put_protected( @@ -747,6 +854,7 @@ def test_create_image_put_protected( headers={'Content-Type': 'application/octet-stream'}) self.assertEqual(self._munch_images(ret), self.cloud.list_images()) + # TODO(shade) Migrate this to requests-mock @mock.patch.object(openstack.OpenStackCloud, '_is_client_version') @mock.patch.object(openstack.OpenStackCloud, '_image_client') def test_create_image_put_user_prop( @@ -777,18 +885,6 @@ def test_create_image_put_user_prop( self.assertEqual( self._munch_images(ret), self.cloud.list_images()) - def test_get_image_by_id(self): - self.register_uris([ - dict(method='GET', - uri='https://image.example.com/v2/images/{id}'.format( - id=self.image_id), - json=self.fake_image_dict) - ]) - self.assertEqual( - self.cloud._normalize_image(self.fake_image_dict), - self.cloud.get_image_by_id(self.image_id)) - self.assert_calls() - class TestImageSuburl(BaseTestImage): @@ -801,7 +897,9 @@ def setUp(self): def test_list_images(self): self.register_uris([ - dict(method='GET', uri='https://example.com/image/v2/images', + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), json=self.fake_search_return) ]) self.assertEqual( @@ -812,13 +910,16 @@ def test_list_images(self): def test_list_images_paginated(self): marker = str(uuid.uuid4()) self.register_uris([ - dict(method='GET', uri='https://example.com/image/v2/images', + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), json={'images': [self.fake_image_dict], 'next': '/v2/images?marker={marker}'.format( marker=marker)}), dict(method='GET', - uri=('https://example.com/image/v2/images?' - 'marker={marker}'.format(marker=marker)), + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2', + qs_elements=['marker={marker}'.format(marker=marker)]), json=self.fake_search_return) ]) self.assertEqual( @@ -881,14 +982,16 @@ def test_config_v2(self): class TestImageVolume(BaseTestImage): - def test_create_image_volume(self): + def setUp(self): + super(TestImageVolume, self).setUp() + self.volume_id = str(uuid.uuid4()) - volume_id = 'some-volume' + def test_create_image_volume(self): self.register_uris([ dict(method='POST', - uri='{endpoint}/volumes/{id}/action'.format( - endpoint=CINDER_URL, id=volume_id), + uri=self.get_mock_url( + 'volumev2', append=['volumes', self.volume_id, 'action']), json={'os-volume_upload_image': {'image_id': self.image_id}}, validate=dict(json={ u'os-volume_upload_image': { @@ -902,24 +1005,24 @@ def test_create_image_volume(self): # .use_glance() method, that is intended only for use in # .setUp self.get_glance_discovery_mock_dict(), - dict(method='GET', uri='https://image.example.com/v2/images', + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), json=self.fake_search_return) ]) self.cloud.create_image( 'fake_image', self.imagefile.name, wait=True, timeout=1, - volume={'id': volume_id}) + volume={'id': self.volume_id}) self.assert_calls() def test_create_image_volume_duplicate(self): - volume_id = 'some-volume' - self.register_uris([ dict(method='POST', - uri='{endpoint}/volumes/{id}/action'.format( - endpoint=CINDER_URL, id=volume_id), + uri=self.get_mock_url( + 'volumev2', append=['volumes', self.volume_id, 'action']), json={'os-volume_upload_image': {'image_id': self.image_id}}, validate=dict(json={ u'os-volume_upload_image': { @@ -933,13 +1036,15 @@ def test_create_image_volume_duplicate(self): # .use_glance() method, that is intended only for use in # .setUp self.get_glance_discovery_mock_dict(), - dict(method='GET', uri='https://image.example.com/v2/images', + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), json=self.fake_search_return) ]) self.cloud.create_image( 'fake_image', self.imagefile.name, wait=True, timeout=1, - volume={'id': volume_id}, allow_duplicates=True) + volume={'id': self.volume_id}, allow_duplicates=True) self.assert_calls() @@ -957,7 +1062,8 @@ def test_url_fix(self): # reason. self.register_uris([ dict(method='GET', - uri='https://image.example.com/v2/images', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), json={'images': []}) ]) self.assertEqual([], self.cloud.list_images()) diff --git a/openstack/tests/unit/cloud/test_operator.py b/openstack/tests/unit/cloud/test_operator.py index 068e735d8..3325a1cae 100644 --- a/openstack/tests/unit/cloud/test_operator.py +++ b/openstack/tests/unit/cloud/test_operator.py @@ -11,7 +11,6 @@ # under the License. import mock -import munch import testtools import openstack @@ -24,9 +23,6 @@ class TestOperatorCloud(base.RequestsMockTestCase): - def setUp(self): - super(TestOperatorCloud, self).setUp() - def test_operator_cloud(self): self.assertIsInstance(self.op_cloud, openstack.OperatorCloud) @@ -61,28 +57,6 @@ def test_list_nics_for_machine_failure(self, mock_client): self.assertRaises(exc.OpenStackCloudException, self.op_cloud.list_nics_for_machine, None) - @mock.patch.object(openstack.OpenStackCloud, '_image_client') - def test_get_image_name(self, mock_client): - - fake_image = munch.Munch( - id='22', - name='22 name', - status='success') - mock_client.get.return_value = [fake_image] - self.assertEqual('22 name', self.op_cloud.get_image_name('22')) - self.assertEqual('22 name', self.op_cloud.get_image_name('22 name')) - - @mock.patch.object(openstack.OpenStackCloud, '_image_client') - def test_get_image_id(self, mock_client): - - fake_image = munch.Munch( - id='22', - name='22 name', - status='success') - mock_client.get.return_value = [fake_image] - self.assertEqual('22', self.op_cloud.get_image_id('22')) - self.assertEqual('22', self.op_cloud.get_image_id('22 name')) - @mock.patch.object(cloud_config.CloudConfig, 'get_endpoint') def test_get_session_endpoint_provided(self, fake_get_endpoint): fake_get_endpoint.return_value = 'http://fake.url' From 3892119b83aa5fd89beaa2180f085736e06be04d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 1 Dec 2017 10:41:52 -0600 Subject: [PATCH 1876/3836] Add osc-tox-unit-tips jobs It's super important that changes to openstacksdk don't break python-openstackclient. Run openstacksdk's unit test tips jobs to make sure we don't. Change-Id: I8059ff058835e658c8192c46f6512de1f9ea3152 Depends-On: I599b18218c10cb08e508cca3b3bbc9c88b8f809c --- .zuul.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.zuul.yaml b/.zuul.yaml index e69b81087..5160dcff6 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -198,6 +198,7 @@ templates: - openstacksdk-functional-tips - openstacksdk-tox-tips + - osc-tox-unit-tips check: jobs: - openstacksdk-functional-devstack From 41c5bd9180aeef02dc84d943fe3e582657072c23 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Sat, 2 Dec 2017 19:27:41 +0100 Subject: [PATCH 1877/3836] Remove -U from pip install 'pip install -U' ugrades specified packages, this is not necessary since we use constraints, remove the parameter '-U' from the line. With tools/tox_install.sh - which a previous change of mine removed - the -U was not harmful, but with the current set up, it might cause upgrades, so remove it. Change-Id: I4270610053e3c541e57e2e539fbd56ffc1c266f0 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 9afc6f585..71440c949 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ skipsdist = True [testenv] usedevelop = True -install_command = pip install -U {opts} {packages} +install_command = pip install {opts} {packages} setenv = VIRTUAL_ENV={envdir} LANG=en_US.UTF-8 From ce01cfa29b848b87c6b5002494e7614b7b52238f Mon Sep 17 00:00:00 2001 From: Daniel Mellado Date: Mon, 4 Dec 2017 12:33:47 +0000 Subject: [PATCH 1878/3836] Add tag support to create_stack When creating a heat stack, this commit add tags support, as specified by [1] [1] https://developer.openstack.org/api-ref/orchestration/v1/#create-stack Change-Id: I61b3451c81bcc7e935c569b730da6446ebc1820d --- openstack/cloud/openstackcloud.py | 4 +++- openstack/tests/unit/cloud/test_stack.py | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 4b671e7b2..fc0828547 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -1286,7 +1286,7 @@ def get_template_contents( "Error in processing template files: %s" % str(e)) def create_stack( - self, name, + self, name, tags=None, template_file=None, template_url=None, template_object=None, files=None, rollback=True, @@ -1296,6 +1296,7 @@ def create_stack( """Create a stack. :param string name: Name of the stack. + :param tags: List of tag(s) of the stack. (optional) :param string template_file: Path to the template. :param string template_url: URL of template. :param string template_object: URL to retrieve template object. @@ -1325,6 +1326,7 @@ def create_stack( files=files) params = dict( stack_name=name, + tags=tags, disable_rollback=not rollback, parameters=parameters, template=template, diff --git a/openstack/tests/unit/cloud/test_stack.py b/openstack/tests/unit/cloud/test_stack.py index a9e928536..4e99dbddd 100644 --- a/openstack/tests/unit/cloud/test_stack.py +++ b/openstack/tests/unit/cloud/test_stack.py @@ -26,6 +26,7 @@ def setUp(self): super(TestStack, self).setUp() self.stack_id = self.getUniqueString('id') self.stack_name = self.getUniqueString('name') + self.stack_tag = self.getUniqueString('tag') self.stack = fakes.make_fake_stack(self.stack_id, self.stack_name) def test_list_stacks(self): @@ -308,6 +309,7 @@ def test_create_stack(self): 'files': {}, 'parameters': {}, 'stack_name': self.stack_name, + 'tags': self.stack_tag, 'template': fakes.FAKE_TEMPLATE_CONTENT, 'timeout_mins': 60} )), @@ -331,6 +333,7 @@ def test_create_stack(self): self.cloud.create_stack( self.stack_name, + tags=self.stack_tag, template_file=test_template.name ) @@ -354,6 +357,7 @@ def test_create_stack_wait(self): 'files': {}, 'parameters': {}, 'stack_name': self.stack_name, + 'tags': self.stack_tag, 'template': fakes.FAKE_TEMPLATE_CONTENT, 'timeout_mins': 60} )), @@ -387,6 +391,7 @@ def test_create_stack_wait(self): ]) self.cloud.create_stack( self.stack_name, + tags=self.stack_tag, template_file=test_template.name, wait=True) From e8a1c2ee9656df20510ca614184e55fb825a5e93 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 5 Dec 2017 03:34:21 +0000 Subject: [PATCH 1879/3836] Updated from global requirements Change-Id: I025d678bae840d9c4369361981993bdb841915f5 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c19e2a649..3f596cd74 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ requestsexceptions>=1.2.0 # Apache-2.0 jsonpatch>=1.16 # BSD six>=1.10.0 # MIT os-service-types>=1.1.0 # Apache-2.0 -keystoneauth1>=3.2.0 # Apache-2.0 +keystoneauth1>=3.3.0 # Apache-2.0 deprecation>=1.0 # Apache-2.0 munch>=2.1.0 # MIT From 41222cba2faaa84a71791bc2f45feec7adf8c015 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 5 Dec 2017 16:53:32 +0000 Subject: [PATCH 1880/3836] Updated from global requirements Change-Id: Ib89bd3d3c59fb5a7c9d82af3ede39bcfceeb80d1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3f596cd74..f131da00c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 PyYAML>=3.10 # MIT appdirs>=1.3.0 # MIT License requestsexceptions>=1.2.0 # Apache-2.0 -jsonpatch>=1.16 # BSD +jsonpatch!=1.20,>=1.16 # BSD six>=1.10.0 # MIT os-service-types>=1.1.0 # Apache-2.0 keystoneauth1>=3.3.0 # Apache-2.0 From 2f93b98938706e96b158dcb5065c21db9615d4a8 Mon Sep 17 00:00:00 2001 From: LIU Yulong Date: Wed, 6 Dec 2017 01:06:27 +0800 Subject: [PATCH 1881/3836] Add FloatingIP qos_policy_id attribute Now neutron can associate a qos policy to the floating IP, and dissociate it. So FloatingIP object should add qos_policy_id attribute. Based on the neutron change (merged): I4efe9e49d268dffeb3df4de4ea1780152218633b Partially-Implements blueprint: floating-ip-rate-limit Change-Id: Id0c2fa586746bb3bd9ee1b495a774964e5ff7024 --- openstack/network/v2/floating_ip.py | 2 ++ openstack/tests/unit/network/v2/test_floating_ip.py | 1 + 2 files changed, 3 insertions(+) diff --git a/openstack/network/v2/floating_ip.py b/openstack/network/v2/floating_ip.py index 79c3dd41a..2536e7f47 100644 --- a/openstack/network/v2/floating_ip.py +++ b/openstack/network/v2/floating_ip.py @@ -56,6 +56,8 @@ class FloatingIP(resource.Resource): floating_network_id = resource.Body('floating_network_id') #: The port ID. port_id = resource.Body('port_id') + #: The ID of the QoS policy attached to the floating IP. + qos_policy_id = resource.Body('qos_policy_id') #: The ID of the project this floating IP is associated with. project_id = resource.Body('tenant_id') #: Revision number of the floating IP. *Type: int* diff --git a/openstack/tests/unit/network/v2/test_floating_ip.py b/openstack/tests/unit/network/v2/test_floating_ip.py index a1f013e03..0005f13d7 100644 --- a/openstack/tests/unit/network/v2/test_floating_ip.py +++ b/openstack/tests/unit/network/v2/test_floating_ip.py @@ -23,6 +23,7 @@ 'floating_network_id': '3', 'id': IDENTIFIER, 'port_id': '5', + 'qos_policy_id': '51', 'tenant_id': '6', 'router_id': '7', 'description': '8', From e8de50f602bb564a233951986d35269ce4fbc587 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 6 Dec 2017 14:42:46 -0600 Subject: [PATCH 1882/3836] Make the get_service_type() overrides tolernat of no defaults The service type overrides in get_service_type() fail if the API version keys are not present in the config dict, which happens when CloudConfig is created without reading defaults. Cherry-picked from I8d035cfd1afc1cad01ceac7cd643568e94897e27 Change-Id: I8d035cfd1afc1cad01ceac7cd643568e94897e27 --- openstack/config/cloud_config.py | 8 +++++--- openstack/tests/unit/config/test_cloud_config.py | 7 +++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/openstack/config/cloud_config.py b/openstack/config/cloud_config.py index e5b2be2a4..ff84ced60 100644 --- a/openstack/config/cloud_config.py +++ b/openstack/config/cloud_config.py @@ -169,12 +169,14 @@ def get_service_type(self, service_type): # TODO(shade) This should get removed when we have os-service-types # alias support landed in keystoneauth. if service_type in ('volume', 'block-storage'): - if self.get_api_version('volume').startswith('2'): + vol_ver = self.get_api_version('volume') + if vol_ver and vol_ver.startswith('2'): service_type = 'volumev2' - elif self.get_api_version('volume').startswith('3'): + elif vol_ver and vol_ver.startswith('3'): service_type = 'volumev3' elif service_type == 'workflow': - if self.get_api_version(service_type).startswith('2'): + wk_ver = self.get_api_version(service_type) + if wk_ver and wk_ver.startswith('2'): service_type = 'workflowv2' return self.config.get(key, service_type) diff --git a/openstack/tests/unit/config/test_cloud_config.py b/openstack/tests/unit/config/test_cloud_config.py index 2b477ef80..696ca7993 100644 --- a/openstack/tests/unit/config/test_cloud_config.py +++ b/openstack/tests/unit/config/test_cloud_config.py @@ -167,6 +167,13 @@ def test_workflow_override_v2(self): cc.config['workflow_api_version'] = '2' self.assertEqual('workflowv2', cc.get_service_type('workflow')) + def test_no_override(self): + """Test no override happens when defaults are not configured""" + cc = cloud_config.CloudConfig("test1", "region-al", fake_services_dict) + self.assertEqual('volume', cc.get_service_type('volume')) + self.assertEqual('workflow', cc.get_service_type('workflow')) + self.assertEqual('not-exist', cc.get_service_type('not-exist')) + def test_get_session_no_auth(self): config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) From 192bb7c57e8423ac46a427d942be98f674282ca2 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Sun, 26 Nov 2017 16:12:30 -0500 Subject: [PATCH 1883/3836] Remove python-ironicclient After an epic battle with time, the python-ironicclient has been driven from the land of shade, and there will be joy! The patches doing this in shade have been squashed, as it was not worth fixing transitive issues in the middle of the stack that were only related to the merge. Switch baremetal nics/ports tests over Moved baremetal port tests to a separate file and updated them to utilize the new testing method. Additionally, normalized the output of the port lists as noise is introduced that is not needed. De-client-ify baremetal node_set_provision_state de-client-ify baremetal get_machine De-clientify baremetal create/delete De-client-ify baremetal machine port list De-client-ify machine patch operations Remove version arg from updated ironic calls Based upon discusison and feedback in change I783fd47db368035d283b4984e4daacf9dc4ac4bd, I am removing the ability for the caller to specify the version, as it is not presently needed. Add helper to wait until baremetal locks clear Add a halper to allow the methods to wait until locks have cleared before proceeding. De-client-ify many baremetal calls Update calls for numerous baremetal methods to utilize the _baremetal_client calls instead of python-ironicclient. Also corrected a minor baremetal port test and fixed the base noauth test endpoint override as python-ironicclient was previously injecting that for us. Fix and De-client-ify operator cloud get_nic_by_mac Apparently, this method either never worked or was silently broken at some point in the past, since we didn't have tests. Added two tests and got the method back into working shape. Additionally removed traces of ironic exceptions being parsed in operatorcloud.py and removed legacy tasks related to port lookups. Also remove patch machine task wrapper as that appears to have been migrated previously. Change-Id: I8d6ca516250e0e10fe8b6edf235330b93535021b --- extras/install-tips.sh | 32 -- openstack/cloud/operatorcloud.py | 393 ++++++++++++------ openstack/tests/fakes.py | 4 +- .../tests/unit/cloud/test_baremetal_node.py | 90 +++- .../tests/unit/cloud/test_baremetal_ports.py | 121 ++++++ openstack/tests/unit/cloud/test_operator.py | 32 -- .../tests/unit/cloud/test_operator_noauth.py | 62 ++- 7 files changed, 520 insertions(+), 214 deletions(-) delete mode 100644 extras/install-tips.sh create mode 100644 openstack/tests/unit/cloud/test_baremetal_ports.py diff --git a/extras/install-tips.sh b/extras/install-tips.sh deleted file mode 100644 index d96773ac4..000000000 --- a/extras/install-tips.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash -# Copyright (c) 2017 Red Hat, 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. - -for lib in \ - python-keystoneclient \ - python-ironicclient \ - os-client-config \ - keystoneauth -do - egg=$(echo $lib | tr '-' '_' | sed 's/python-//') - if [ -d /opt/stack/new/$lib ] ; then - tip_location="git+file:///opt/stack/new/$lib#egg=$egg" - echo "$(which pip) install -U -e $tip_location" - pip uninstall -y $lib - pip install -U -e $tip_location - else - echo "$lib not found in /opt/stack/new/$lib" - fi -done diff --git a/openstack/cloud/operatorcloud.py b/openstack/cloud/operatorcloud.py index beba3a598..ecb17c298 100644 --- a/openstack/cloud/operatorcloud.py +++ b/openstack/cloud/operatorcloud.py @@ -16,9 +16,7 @@ import munch from openstack.cloud.exc import * # noqa -from openstack.cloud import meta from openstack.cloud import openstackcloud -from openstack.cloud import _tasks from openstack.cloud import _utils @@ -35,29 +33,42 @@ class OperatorCloud(openstackcloud.OpenStackCloud): ironic_client = None def list_nics(self): - with _utils.shade_exceptions("Error fetching machine port list"): - return meta.obj_list_to_munch( - self.manager.submit_task(_tasks.MachinePortList(self))) + msg = "Error fetching machine port list" + return self._baremetal_client.get("/ports", + microversion="1.6", + error_message=msg) def list_nics_for_machine(self, uuid): - with _utils.shade_exceptions( - "Error fetching port list for node {node_id}".format( - node_id=uuid)): - return meta.obj_list_to_munch(self.manager.submit_task( - _tasks.MachineNodePortList(self, node_id=uuid))) + """Returns a list of ports present on the machine node. + + :param uuid: String representing machine UUID value in + order to identify the machine. + :returns: A dictionary containing containing a list of ports, + associated with the label "ports". + """ + msg = "Error fetching port list for node {node_id}".format( + node_id=uuid) + url = "/nodes/{node_id}/ports".format(node_id=uuid) + return self._baremetal_client.get(url, + microversion="1.6", + error_message=msg) def get_nic_by_mac(self, mac): - """Get Machine by MAC address""" - # TODO(shade) Finish porting ironic to REST/sdk - # try: - # return self.manager.submit_task( - # _tasks.MachineNodePortGet(self, port_id=mac)) - # except ironic_exceptions.ClientException: - # return None + try: + url = '/ports/detail?address=%s' % mac + data = self._baremetal_client.get(url) + if len(data['ports']) == 1: + return data['ports'][0] + except Exception: + pass + return None def list_machines(self): - return self._normalize_machines( - self.manager.submit_task(_tasks.MachineNodeList(self))) + msg = "Error fetching machine node list" + data = self._baremetal_client.get("/nodes", + microversion="1.6", + error_message=msg) + return self._get_and_munchify('nodes', data) def get_machine(self, name_or_id): """Get Machine by name or uuid @@ -70,13 +81,20 @@ def get_machine(self, name_or_id): :returns: ``munch.Munch`` representing the node found or None if no nodes are found. """ - # TODO(shade) Finish porting ironic to REST/sdk - # try: - # return self._normalize_machine( - # self.manager.submit_task( - # _tasks.MachineNodeGet(node_id=name_or_id))) - # except ironic_exceptions.ClientException: - # return None + # NOTE(TheJulia): This is the initial microversion shade support for + # ironic was created around. Ironic's default behavior for newer + # versions is to expose the field, but with a value of None for + # calls by a supported, yet older microversion. + # Consensus for moving forward with microversion handling in shade + # seems to be to take the same approach, although ironic's API + # does it for the user. + version = "1.6" + try: + url = '/nodes/{node_id}'.format(node_id=name_or_id) + return self._normalize_machine( + self._baremetal_client.get(url, microversion=version)) + except Exception: + return None def get_machine_by_mac(self, mac): """Get machine by port MAC address @@ -86,13 +104,14 @@ def get_machine_by_mac(self, mac): :returns: ``munch.Munch`` representing the node found or None if the node is not found. """ - # try: - # port = self.manager.submit_task( - # _tasks.MachinePortGetByAddress(address=mac)) - # return self.manager.submit_task( - # _tasks.MachineNodeGet(node_id=port.node_uuid)) - # except ironic_exceptions.ClientException: - # return None + try: + port_url = '/ports/detail?address={mac}'.format(mac=mac) + port = self._baremetal_client.get(port_url, microversion=1.6) + machine_url = '/nodes/{machine}'.format( + machine=port['ports'][0]['node_uuid']) + return self._baremetal_client.get(machine_url, microversion=1.6) + except Exception: + return None def inspect_machine(self, name_or_id, wait=False, timeout=3600): """Inspect a Barmetal machine @@ -204,15 +223,27 @@ def register_machine(self, nics, wait=False, timeout=3600, :returns: Returns a ``munch.Munch`` representing the new baremetal node. """ - with _utils.shade_exceptions("Error registering machine with Ironic"): - machine = self.manager.submit_task(_tasks.MachineCreate(**kwargs)) + + msg = ("Baremetal machine node failed to be created.") + port_msg = ("Baremetal machine port failed to be created.") + + url = '/nodes' + # TODO(TheJulia): At some point we need to figure out how to + # handle data across when the requestor is defining newer items + # with the older api. + machine = self._baremetal_client.post(url, + json=kwargs, + error_message=msg, + microversion="1.6") created_nics = [] try: for row in nics: - nic = self.manager.submit_task( - _tasks.MachinePortCreate(address=row['mac'], - node_uuid=machine['uuid'])) + payload = {'address': row['mac'], + 'node_uuid': machine['uuid']} + nic = self._baremetal_client.post('/ports', + json=payload, + error_message=port_msg) created_nics.append(nic['uuid']) except Exception as e: @@ -221,14 +252,23 @@ def register_machine(self, nics, wait=False, timeout=3600, try: for uuid in created_nics: try: - self.manager.submit_task( - _tasks.MachinePortDelete( - port_id=uuid)) + port_url = '/ports/{uuid}'.format(uuid=uuid) + # NOTE(TheJulia): Added in hope that it is logged. + port_msg = ('Failed to delete port {port} for node' + '{node}').format(port=uuid, + node=machine['uuid']) + self._baremetal_client.delete(port_url, + error_message=port_msg) except Exception: pass finally: - self.manager.submit_task( - _tasks.MachineDelete(node_id=machine['uuid'])) + version = "1.6" + msg = "Baremetal machine failed to be deleted." + url = '/nodes/{node_id}'.format( + node_id=machine['uuid']) + self._baremetal_client.delete(url, + error_message=msg, + microversion=version) raise OpenStackCloudException( "Error registering NICs with the baremetal service: %s" % str(e)) @@ -329,19 +369,44 @@ def unregister_machine(self, nics, uuid, wait=False, timeout=600): "Error unregistering node '%s' due to current provision " "state '%s'" % (uuid, machine['provision_state'])) + # NOTE(TheJulia) There is a high possibility of a lock being present + # if the machine was just moved through the state machine. This was + # previously concealed by exception retry logic that detected the + # failure, and resubitted the request in python-ironicclient. + try: + self.wait_for_baremetal_node_lock(machine, timeout=timeout) + except OpenStackCloudException as e: + raise OpenStackCloudException("Error unregistering node '%s': " + "Exception occured while waiting " + "to be able to proceed: %s" + % (machine['uuid'], e)) + for nic in nics: - with _utils.shade_exceptions( - "Error removing NIC {nic} from baremetal API for node " - "{uuid}".format(nic=nic, uuid=uuid)): - port = self.manager.submit_task( - _tasks.MachinePortGetByAddress(address=nic['mac'])) - self.manager.submit_task( - _tasks.MachinePortDelete(port_id=port.uuid)) + port_msg = ("Error removing NIC {nic} from baremetal API for " + "node {uuid}").format(nic=nic, uuid=uuid) + port_url = '/ports/detail?address={mac}'.format(mac=nic['mac']) + port = self._baremetal_client.get(port_url, microversion=1.6, + error_message=port_msg) + port_url = '/ports/{uuid}'.format(uuid=port['ports'][0]['uuid']) + self._baremetal_client.delete(port_url, error_message=port_msg) + with _utils.shade_exceptions( "Error unregistering machine {node_id} from the baremetal " "API".format(node_id=uuid)): - self.manager.submit_task( - _tasks.MachineDelete(node_id=uuid)) + + # NOTE(TheJulia): While this should not matter microversion wise, + # ironic assumes all calls without an explicit microversion to be + # version 1.0. Ironic expects to deprecate support for older + # microversions in future releases, as such, we explicitly set + # the version to what we have been using with the client library.. + version = "1.6" + msg = "Baremetal machine failed to be deleted." + url = '/nodes/{node_id}'.format( + node_id=uuid) + self._baremetal_client.delete(url, + error_message=msg, + microversion=version) + if wait: for count in _utils._iterate_timeout( timeout, @@ -353,9 +418,7 @@ def patch_machine(self, name_or_id, patch): """Patch Machine Information This method allows for an interface to manipulate node entries - within Ironic. Specifically, it is a pass-through for the - ironicclient.nodes.update interface which allows the Ironic Node - properties to be updated. + within Ironic. :param node_id: The server object to attach to. :param patch: @@ -386,15 +449,13 @@ def patch_machine(self, name_or_id, patch): :returns: ``munch.Munch`` representing the newly updated node. """ - with _utils.shade_exceptions( - "Error updating machine via patch operation on node " - "{node}".format(node=name_or_id) - ): - return self._normalize_machine( - self.manager.submit_task( - _tasks.MachinePatch(node_id=name_or_id, - patch=patch, - http_method='PATCH'))) + msg = ("Error updating machine via patch operation on node " + "{node}".format(node=name_or_id)) + url = '/nodes/{node_id}'.format(node_id=name_or_id) + return self._normalize_machine( + self._baremetal_client.patch(url, + json=patch, + error_message=msg)) def update_machine(self, name_or_id, chassis_uuid=None, driver=None, driver_info=None, name=None, instance_info=None, @@ -506,14 +567,20 @@ def update_machine(self, name_or_id, chassis_uuid=None, driver=None, ) def validate_node(self, uuid): - with _utils.shade_exceptions(): - ifaces = self.manager.submit_task( - _tasks.MachineNodeValidate(node_uuid=uuid)) - - if not ifaces.deploy or not ifaces.power: + # TODO(TheJulia): There are soooooo many other interfaces + # that we can support validating, while these are essential, + # we should support more. + # TODO(TheJulia): Add a doc string :( + msg = ("Failed to query the API for validation status of " + "node {node_id}").format(node_id=uuid) + url = '/nodes/{node_id}/validate'.format(node_id=uuid) + ifaces = self._baremetal_client.get(url, error_message=msg) + + if not ifaces['deploy'] or not ifaces['power']: raise OpenStackCloudException( "ironic node %s failed to validate. " - "(deploy: %s, power: %s)" % (ifaces.deploy, ifaces.power)) + "(deploy: %s, power: %s)" % (ifaces['deploy'], + ifaces['power'])) def node_set_provision_state(self, name_or_id, @@ -549,36 +616,44 @@ def node_set_provision_state(self, :returns: ``munch.Munch`` representing the current state of the machine upon exit of the method. """ - with _utils.shade_exceptions( - "Baremetal machine node failed change provision state to " - "{state}".format(state=state) - ): - machine = self.manager.submit_task( - _tasks.MachineSetProvision(node_uuid=name_or_id, - state=state, - configdrive=configdrive)) - - if wait: - for count in _utils._iterate_timeout( - timeout, - "Timeout waiting for node transition to " - "target state of '%s'" % state): - machine = self.get_machine(name_or_id) - if 'failed' in machine['provision_state']: - raise OpenStackCloudException( - "Machine encountered a failure.") - # NOTE(TheJulia): This performs matching if the requested - # end state matches the state the node has reached. - if state in machine['provision_state']: - break - # NOTE(TheJulia): This performs matching for cases where - # the reqeusted state action ends in available state. - if ("available" in machine['provision_state'] and - state in ["provide", "deleted"]): - break - else: + # NOTE(TheJulia): Default microversion for this call is 1.6. + # Setting locally until we have determined our master plan regarding + # microversion handling. + version = "1.6" + msg = ("Baremetal machine node failed change provision state to " + "{state}".format(state=state)) + + url = '/nodes/{node_id}/states/provision'.format( + node_id=name_or_id) + payload = {'target': state} + if configdrive: + payload['configdrive'] = configdrive + + machine = self._baremetal_client.put(url, + json=payload, + error_message=msg, + microversion=version) + if wait: + for count in _utils._iterate_timeout( + timeout, + "Timeout waiting for node transition to " + "target state of '%s'" % state): machine = self.get_machine(name_or_id) - return machine + if 'failed' in machine['provision_state']: + raise OpenStackCloudException( + "Machine encountered a failure.") + # NOTE(TheJulia): This performs matching if the requested + # end state matches the state the node has reached. + if state in machine['provision_state']: + break + # NOTE(TheJulia): This performs matching for cases where + # the reqeusted state action ends in available state. + if ("available" in machine['provision_state'] and + state in ["provide", "deleted"]): + break + else: + machine = self.get_machine(name_or_id) + return machine def set_machine_maintenance_state( self, @@ -603,25 +678,17 @@ def set_machine_maintenance_state( :returns: None """ - with _utils.shade_exceptions( - "Error setting machine maintenance state to {state} on node " - "{node}".format(state=state, node=name_or_id) - ): - if state: - result = self.manager.submit_task( - _tasks.MachineSetMaintenance(node_id=name_or_id, - state='true', - maint_reason=reason)) - else: - result = self.manager.submit_task( - _tasks.MachineSetMaintenance(node_id=name_or_id, - state='false')) - if result is not None: - raise OpenStackCloudException( - "Failed setting machine maintenance state to %s " - "on node %s. Received: %s" % ( - state, name_or_id, result)) - return None + msg = ("Error setting machine maintenance state to {state} on node " + "{node}").format(state=state, node=name_or_id) + url = '/nodes/{name_or_id}/maintenance'.format(name_or_id=name_or_id) + if state: + payload = {'reason': reason} + self._baremetal_client.put(url, + json=payload, + error_message=msg) + else: + self._baremetal_client.delete(url, error_message=msg) + return None def remove_machine_from_maintenance(self, name_or_id): """Remove Baremetal Machine from Maintenance State @@ -658,18 +725,19 @@ def _set_machine_power_state(self, name_or_id, state): :returns: None """ - with _utils.shade_exceptions( - "Error setting machine power state to {state} on node " - "{node}".format(state=state, node=name_or_id) - ): - power = self.manager.submit_task( - _tasks.MachineSetPower(node_id=name_or_id, - state=state)) - if power is not None: - raise OpenStackCloudException( - "Failed setting machine power state %s on node %s. " - "Received: %s" % (state, name_or_id, power)) - return None + msg = ("Error setting machine power state to {state} on node " + "{node}").format(state=state, node=name_or_id) + url = '/nodes/{name_or_id}/states/power'.format(name_or_id=name_or_id) + if 'reboot' in state: + desired_state = 'rebooting' + else: + desired_state = 'power {state}'.format(state=state) + payload = {'target': desired_state} + self._baremetal_client.put(url, + json=payload, + error_message=msg, + microversion="1.6") + return None def set_machine_power_on(self, name_or_id): """Activate baremetal machine power @@ -729,16 +797,69 @@ def deactivate_node(self, uuid, wait=False, uuid, 'deleted', wait=wait, timeout=timeout) def set_node_instance_info(self, uuid, patch): - with _utils.shade_exceptions(): - return self.manager.submit_task( - _tasks.MachineNodeUpdate(node_id=uuid, patch=patch)) + msg = ("Error updating machine via patch operation on node " + "{node}".format(node=uuid)) + url = '/nodes/{node_id}'.format(node_id=uuid) + return self._baremetal_client.patch(url, + json=patch, + error_message=msg) def purge_node_instance_info(self, uuid): patch = [] patch.append({'op': 'remove', 'path': '/instance_info'}) - with _utils.shade_exceptions(): - return self.manager.submit_task( - _tasks.MachineNodeUpdate(node_id=uuid, patch=patch)) + msg = ("Error updating machine via patch operation on node " + "{node}".format(node=uuid)) + url = '/nodes/{node_id}'.format(node_id=uuid) + return self._baremetal_client.patch(url, + json=patch, + error_message=msg) + + def wait_for_baremetal_node_lock(self, node, timeout=30): + """Wait for a baremetal node to have no lock. + + Baremetal nodes in ironic have a reservation lock that + is used to represent that a conductor has locked the node + while performing some sort of action, such as changing + configuration as a result of a machine state change. + + This lock can occur during power syncronization, and prevents + updates to objects attached to the node, such as ports. + + In the vast majority of cases, locks should clear in a few + seconds, and as such this method will only wait for 30 seconds. + The default wait is two seconds between checking if the lock + has cleared. + + This method is intended for use by methods that need to + gracefully block without genreating errors, however this + method does prevent another client or a timer from + triggering a lock immediately after we see the lock as + having cleared. + + :param node: The json representation of the node, + specificially looking for the node + 'uuid' and 'reservation' fields. + :param timeout: Integer in seconds to wait for the + lock to clear. Default: 30 + + :raises: OpenStackCloudException upon client failure. + :returns: None + """ + # TODO(TheJulia): This _can_ still fail with a race + # condition in that between us checking the status, + # a conductor where the conductor could still obtain + # a lock before we are able to obtain a lock. + # This means we should handle this with such conections + + if node['reservation'] is None: + return + else: + msg = 'Waiting for lock to be released for node {node}'.format( + node=node['uuid']) + for count in _utils._iterate_timeout(timeout, msg, 2): + current_node = self.get_machine(node['uuid']) + if current_node['reservation'] is None: + return @_utils.valid_kwargs('type', 'service_type', 'description') def create_service(self, name, enabled=True, **kwargs): diff --git a/openstack/tests/fakes.py b/openstack/tests/fakes.py index 9188d7b54..beb4dfeb0 100644 --- a/openstack/tests/fakes.py +++ b/openstack/tests/fakes.py @@ -358,7 +358,8 @@ def __init__( class FakeMachine(object): def __init__(self, id, name=None, driver=None, driver_info=None, chassis_uuid=None, instance_info=None, instance_uuid=None, - properties=None, reservation=None, last_error=None): + properties=None, reservation=None, last_error=None, + provision_state=None): self.uuid = id self.name = name self.driver = driver @@ -369,6 +370,7 @@ def __init__(self, id, name=None, driver=None, driver_info=None, self.properties = properties self.reservation = reservation self.last_error = last_error + self.provision_state = provision_state class FakeMachinePort(object): diff --git a/openstack/tests/unit/cloud/test_baremetal_node.py b/openstack/tests/unit/cloud/test_baremetal_node.py index cd76c5a5c..65bb15474 100644 --- a/openstack/tests/unit/cloud/test_baremetal_node.py +++ b/openstack/tests/unit/cloud/test_baremetal_node.py @@ -672,6 +672,7 @@ def test_node_set_provision_state_wait_timeout(self): def test_node_set_provision_state_wait_timeout_fails(self): # Intentionally time out. + self.fake_baremetal_node['provision_state'] = 'deploy wait' self.register_uris([ dict( method='PUT', @@ -780,6 +781,55 @@ def test_node_set_provision_state_wait_provide(self): self.assertEqual(available_node, return_value) self.assert_calls() + def test_wait_for_baremetal_node_lock_locked(self): + self.fake_baremetal_node['reservation'] = 'conductor0' + unlocked_node = self.fake_baremetal_node.copy() + unlocked_node['reservation'] = None + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + dict(method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=unlocked_node), + ]) + self.assertIsNone( + self.op_cloud.wait_for_baremetal_node_lock( + self.fake_baremetal_node, + timeout=1)) + + self.assert_calls() + + def test_wait_for_baremetal_node_lock_not_locked(self): + self.fake_baremetal_node['reservation'] = None + self.assertIsNone( + self.op_cloud.wait_for_baremetal_node_lock( + self.fake_baremetal_node, + timeout=1)) + + self.assertEqual(0, len(self.adapter.request_history)) + + def test_wait_for_baremetal_node_lock_timeout(self): + self.fake_baremetal_node['reservation'] = 'conductor0' + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + ]) + self.assertRaises( + exc.OpenStackCloudException, + self.op_cloud.wait_for_baremetal_node_lock, + self.fake_baremetal_node, + timeout=0.001) + + self.assert_calls() + def test_activate_node(self): self.fake_baremetal_node['provision_state'] = 'active' self.register_uris([ @@ -834,9 +884,7 @@ def test_register_machine(self): node_uuid = self.fake_baremetal_node['uuid'] # TODO(TheJulia): There is a lot of duplication # in testing creation. Surely this hsould be a helper - # or something. We should fix this, after we have - # ironicclient removed, as in the mean time visibility - # will be helpful. + # or something. We should fix this. node_to_post = { 'chassis_uuid': None, 'driver': None, @@ -867,11 +915,10 @@ def test_register_machine(self): self.assertDictEqual(self.fake_baremetal_node, return_value) self.assert_calls() - # TODO(TheJulia): After we remove ironicclient, - # we need to de-duplicate these tests. Possibly - # a dedicated class, although we should do it then - # as we may find differences that need to be accounted - # for newer microversions. + # TODO(TheJulia): We need to de-duplicate these tests. + # Possibly a dedicated class, although we should do it + # then as we may find differences that need to be + # accounted for newer microversions. def test_register_machine_enroll(self): mac_address = '00:01:02:03:04:05' nics = [{'mac': mac_address}] @@ -1336,6 +1383,33 @@ def test_unregister_machine_timeout(self): self.assert_calls() + def test_unregister_machine_locked_timeout(self): + mac_address = self.fake_baremetal_port['address'] + nics = [{'mac': mac_address}] + self.fake_baremetal_node['provision_state'] = 'available' + self.fake_baremetal_node['reservation'] = 'conductor99' + self.register_uris([ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + ]) + self.assertRaises( + exc.OpenStackCloudException, + self.op_cloud.unregister_machine, + nics, + self.fake_baremetal_node['uuid'], + timeout=0.001) + self.assert_calls() + def test_unregister_machine_unavailable(self): # This is a list of invalid states that the method # should fail on. diff --git a/openstack/tests/unit/cloud/test_baremetal_ports.py b/openstack/tests/unit/cloud/test_baremetal_ports.py new file mode 100644 index 000000000..a52b76ff4 --- /dev/null +++ b/openstack/tests/unit/cloud/test_baremetal_ports.py @@ -0,0 +1,121 @@ +# 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_baremetal_ports +---------------------------------- + +Tests for baremetal port related operations +""" + +from testscenarios import load_tests_apply_scenarios as load_tests # noqa + +from openstack.cloud import exc +from openstack.tests import fakes +from openstack.tests.unit import base + + +class TestBaremetalPort(base.IronicTestCase): + + def setUp(self): + super(TestBaremetalPort, self).setUp() + self.fake_baremetal_node = fakes.make_fake_machine( + self.name, self.uuid) + # TODO(TheJulia): Some tests below have fake ports, + # since they are required in some processes. Lets refactor + # them at some point to use self.fake_baremetal_port. + self.fake_baremetal_port = fakes.make_fake_port( + '00:01:02:03:04:05', + node_id=self.uuid) + self.fake_baremetal_port2 = fakes.make_fake_port( + '0a:0b:0c:0d:0e:0f', + node_id=self.uuid) + + def test_list_nics(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='ports'), + json={'ports': [self.fake_baremetal_port, + self.fake_baremetal_port2]}), + ]) + + return_value = self.op_cloud.list_nics() + self.assertEqual(2, len(return_value['ports'])) + self.assertEqual(self.fake_baremetal_port, return_value['ports'][0]) + self.assert_calls() + + def test_list_nics_failure(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='ports'), + status_code=400) + ]) + self.assertRaises(exc.OpenStackCloudException, + self.op_cloud.list_nics) + self.assert_calls() + + def test_list_nics_for_machine(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], 'ports']), + json={'ports': [self.fake_baremetal_port, + self.fake_baremetal_port2]}), + ]) + + return_value = self.op_cloud.list_nics_for_machine( + self.fake_baremetal_node['uuid']) + self.assertEqual(2, len(return_value['ports'])) + self.assertEqual(self.fake_baremetal_port, return_value['ports'][0]) + self.assert_calls() + + def test_list_nics_for_machine_failure(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], 'ports']), + status_code=400) + ]) + + self.assertRaises(exc.OpenStackCloudException, + self.op_cloud.list_nics_for_machine, + self.fake_baremetal_node['uuid']) + self.assert_calls() + + def test_get_nic_by_mac(self): + mac = self.fake_baremetal_port['address'] + query = 'detail?address=%s' % mac + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='ports', append=[query]), + json={'ports': [self.fake_baremetal_port]}), + ]) + + return_value = self.op_cloud.get_nic_by_mac(mac) + + self.assertEqual(self.fake_baremetal_port, return_value) + self.assert_calls() + + def test_get_nic_by_mac_failure(self): + mac = self.fake_baremetal_port['address'] + query = 'detail?address=%s' % mac + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url(resource='ports', append=[query]), + json={'ports': []}), + ]) + + self.assertIsNone(self.op_cloud.get_nic_by_mac(mac)) + + self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_operator.py b/openstack/tests/unit/cloud/test_operator.py index 3325a1cae..3d2bc3646 100644 --- a/openstack/tests/unit/cloud/test_operator.py +++ b/openstack/tests/unit/cloud/test_operator.py @@ -15,7 +15,6 @@ import openstack from openstack.cloud import exc -from openstack.cloud import meta from openstack.config import cloud_config from openstack.tests import fakes from openstack.tests.unit import base @@ -26,37 +25,6 @@ class TestOperatorCloud(base.RequestsMockTestCase): def test_operator_cloud(self): self.assertIsInstance(self.op_cloud, openstack.OperatorCloud) - @mock.patch.object(openstack.OperatorCloud, 'ironic_client') - def test_list_nics(self, mock_client): - port1 = fakes.FakeMachinePort(1, "aa:bb:cc:dd", "node1") - port2 = fakes.FakeMachinePort(2, "dd:cc:bb:aa", "node2") - port_list = [port1, port2] - port_dict_list = meta.obj_list_to_munch(port_list) - - mock_client.port.list.return_value = port_list - nics = self.op_cloud.list_nics() - - self.assertTrue(mock_client.port.list.called) - self.assertEqual(port_dict_list, nics) - - @mock.patch.object(openstack.OperatorCloud, 'ironic_client') - def test_list_nics_failure(self, mock_client): - mock_client.port.list.side_effect = Exception() - self.assertRaises(exc.OpenStackCloudException, - self.op_cloud.list_nics) - - @mock.patch.object(openstack.OperatorCloud, 'ironic_client') - def test_list_nics_for_machine(self, mock_client): - mock_client.node.list_ports.return_value = [] - self.op_cloud.list_nics_for_machine("123") - mock_client.node.list_ports.assert_called_with(node_id="123") - - @mock.patch.object(openstack.OperatorCloud, 'ironic_client') - def test_list_nics_for_machine_failure(self, mock_client): - mock_client.node.list_ports.side_effect = Exception() - self.assertRaises(exc.OpenStackCloudException, - self.op_cloud.list_nics_for_machine, None) - @mock.patch.object(cloud_config.CloudConfig, 'get_endpoint') def test_get_session_endpoint_provided(self, fake_get_endpoint): fake_get_endpoint.return_value = 'http://fake.url' diff --git a/openstack/tests/unit/cloud/test_operator_noauth.py b/openstack/tests/unit/cloud/test_operator_noauth.py index c4eefa3dd..5f4367b7d 100644 --- a/openstack/tests/unit/cloud/test_operator_noauth.py +++ b/openstack/tests/unit/cloud/test_operator_noauth.py @@ -12,11 +12,63 @@ # See the License for the specific language governing permissions and # limitations under the License. -# TODO(shade) Port this content back in from shade repo as tests don't have -# references to ironic_client. - +import openstack from openstack.tests.unit import base -class TestShadeOperatorNoAuth(base.RequestsMockTestCase): - pass +class TestOpenStackCloudOperatorNoAuth(base.RequestsMockTestCase): + def setUp(self): + """Setup Noauth OperatorCloud tests + + Setup the test to utilize no authentication and an endpoint + URL in the auth data. This is permits testing of the basic + mechanism that enables Ironic noauth mode to be utilized with + Shade. + + Uses base.RequestsMockTestCase instead of IronicTestCase because + we need to do completely different things with discovery. + """ + super(TestOpenStackCloudOperatorNoAuth, self).setUp() + # By clearing the URI registry, we remove all calls to a keystone + # catalog or getting a token + self._uri_registry.clear() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + service_type='baremetal', base_url_append='v1', + resource='nodes'), + json={'nodes': []}), + ]) + + def test_ironic_noauth_none_auth_type(self): + """Test noauth selection for Ironic in OperatorCloud + + The new way of doing this is with the keystoneauth none plugin. + """ + # NOTE(TheJulia): When we are using the python-ironicclient + # library, the library will automatically prepend the URI path + # with 'v1'. As such, since we are overriding the endpoint, + # we must explicitly do the same as we move away from the + # client library. + self.cloud_noauth = openstack.operator_cloud( + auth_type='none', + baremetal_endpoint_override="https://bare-metal.example.com/v1") + + self.cloud_noauth.list_machines() + + self.assert_calls() + + def test_ironic_noauth_admin_token_auth_type(self): + """Test noauth selection for Ironic in OperatorCloud + + The old way of doing this was to abuse admin_token. + """ + self.cloud_noauth = openstack.operator_cloud( + auth_type='admin_token', + auth=dict( + endpoint='https://bare-metal.example.com/v1', + token='ignored')) + + self.cloud_noauth.list_machines() + + self.assert_calls() From 284913f4022ab4c7cbdcaefd2e0ceedc0b46c12d Mon Sep 17 00:00:00 2001 From: Daniel Mellado Date: Tue, 12 Dec 2017 11:57:26 +0000 Subject: [PATCH 1884/3836] Add reno for tag support on heat stacks This commit adds a release note for tag support while creating heat stacks, which was addressed by https://review.openstack.org/#/c/525174/ Change-Id: I57ca196ea4fe284421025c840c3290e339de774f --- .../notes/add_heat_tag_support-135aa43ba1dce3bb.yaml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 releasenotes/notes/add_heat_tag_support-135aa43ba1dce3bb.yaml diff --git a/releasenotes/notes/add_heat_tag_support-135aa43ba1dce3bb.yaml b/releasenotes/notes/add_heat_tag_support-135aa43ba1dce3bb.yaml new file mode 100644 index 000000000..4e0a0ea87 --- /dev/null +++ b/releasenotes/notes/add_heat_tag_support-135aa43ba1dce3bb.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Add tags support when creating a stack, as specified by the openstack + orchestration api at [1] + + [1]https://developer.openstack.org/api-ref/orchestration/v1/#create-stack From 04bd6bfdeb8bb33e5841fac9122400a75ad9ad17 Mon Sep 17 00:00:00 2001 From: Akihiro Motoki Date: Sun, 24 Dec 2017 11:55:50 +0900 Subject: [PATCH 1885/3836] Allow to pass filters like domain to find_project/user In multi-domain environments, project names or user names can be same across domains. To retrieve a single project/user, we need to pass domain information to find_project/find_user. This commit defines _query_mapping to Project and User classes and adds filtering parameters to find_project/find_user in _proxy.py. The required field for filtering is 'domain_id' only, but it might be useful to allow to pass other filtering fields. Closes-Bug: #1739929 Needed-By: I8f59fa3f9b7c573485cd1572e5e9aae08f071e37 Change-Id: I60a8b3b83f6170b60d09c101b5c7035148283678 --- openstack/identity/v3/_proxy.py | 8 ++++---- openstack/identity/v3/project.py | 8 ++++++++ openstack/identity/v3/user.py | 7 +++++++ openstack/tests/unit/identity/v3/test_project.py | 12 ++++++++++++ openstack/tests/unit/identity/v3/test_user.py | 11 +++++++++++ 5 files changed, 42 insertions(+), 4 deletions(-) diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 29f90f12b..30aad2656 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -459,7 +459,7 @@ def delete_project(self, project, ignore_missing=True): """ self._delete(_project.Project, project, ignore_missing=ignore_missing) - def find_project(self, name_or_id, ignore_missing=True): + def find_project(self, name_or_id, ignore_missing=True, **attrs): """Find a single project :param name_or_id: The name or ID of a project. @@ -471,7 +471,7 @@ def find_project(self, name_or_id, ignore_missing=True): :returns: One :class:`~openstack.identity.v3.project.Project` or None """ return self._find(_project.Project, name_or_id, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, **attrs) def get_project(self, project): """Get a single project @@ -615,7 +615,7 @@ def delete_user(self, user, ignore_missing=True): """ self._delete(_user.User, user, ignore_missing=ignore_missing) - def find_user(self, name_or_id, ignore_missing=True): + def find_user(self, name_or_id, ignore_missing=True, **attrs): """Find a single user :param name_or_id: The name or ID of a user. @@ -627,7 +627,7 @@ def find_user(self, name_or_id, ignore_missing=True): :returns: One :class:`~openstack.identity.v3.user.User` or None """ return self._find(_user.User, name_or_id, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, **attrs) def get_user(self, user): """Get a single user diff --git a/openstack/identity/v3/project.py b/openstack/identity/v3/project.py index ab28e359f..dd99b52a7 100644 --- a/openstack/identity/v3/project.py +++ b/openstack/identity/v3/project.py @@ -29,6 +29,14 @@ class Project(resource.Resource): allow_list = True patch_update = True + _query_mapping = resource.QueryParameters( + 'domain_id', + 'is_domain', + 'name', + 'parent_id', + is_enabled='enabled', + ) + # Properties #: The description of the project. *Type: string* description = resource.Body('description') diff --git a/openstack/identity/v3/user.py b/openstack/identity/v3/user.py index 17f980894..f31154143 100644 --- a/openstack/identity/v3/user.py +++ b/openstack/identity/v3/user.py @@ -28,6 +28,13 @@ class User(resource.Resource): allow_list = True patch_update = True + _query_mapping = resource.QueryParameters( + 'domain_id', + 'name', + 'password_expires_at', + is_enabled='enabled', + ) + # Properties #: References the user's default project ID against which to authorize, #: if the API user does not explicitly specify one when creating a token. diff --git a/openstack/tests/unit/identity/v3/test_project.py b/openstack/tests/unit/identity/v3/test_project.py index f88a0ae5e..8e6998faa 100644 --- a/openstack/tests/unit/identity/v3/test_project.py +++ b/openstack/tests/unit/identity/v3/test_project.py @@ -41,6 +41,18 @@ def test_basic(self): self.assertTrue(sot.allow_list) self.assertTrue(sot.patch_update) + self.assertDictEqual( + { + 'domain_id': 'domain_id', + 'is_domain': 'is_domain', + 'name': 'name', + 'parent_id': 'parent_id', + 'is_enabled': 'enabled', + 'limit': 'limit', + 'marker': 'marker', + }, + sot._query_mapping._mapping) + def test_make_it(self): sot = project.Project(**EXAMPLE) self.assertEqual(EXAMPLE['description'], sot.description) diff --git a/openstack/tests/unit/identity/v3/test_user.py b/openstack/tests/unit/identity/v3/test_user.py index f0202b901..e0cbb369d 100644 --- a/openstack/tests/unit/identity/v3/test_user.py +++ b/openstack/tests/unit/identity/v3/test_user.py @@ -44,6 +44,17 @@ def test_basic(self): self.assertTrue(sot.allow_list) self.assertTrue(sot.patch_update) + self.assertDictEqual( + { + 'domain_id': 'domain_id', + 'name': 'name', + 'password_expires_at': 'password_expires_at', + 'is_enabled': 'enabled', + 'limit': 'limit', + 'marker': 'marker', + }, + sot._query_mapping._mapping) + def test_make_it(self): sot = user.User(**EXAMPLE) self.assertEqual(EXAMPLE['default_project_id'], From d7c0f11513da97777578cf52907623cd543c75a0 Mon Sep 17 00:00:00 2001 From: Akihiro Motoki Date: Sun, 24 Dec 2017 12:02:09 +0900 Subject: [PATCH 1886/3836] Add _query_mapping to identity resources The parent commit added _query_mapping to Project and User classes to address the bug on 'domain' filtering. Other identity resources also support filtering. This commit adds query parameters to the other resources. Change-Id: I87783c8244df47775e417151d825b75d39f30be3 Related-Bug: #1739929 --- openstack/identity/v3/credential.py | 4 ++++ openstack/identity/v3/domain.py | 5 +++++ openstack/identity/v3/endpoint.py | 4 ++++ openstack/identity/v3/group.py | 4 ++++ openstack/identity/v3/region.py | 4 ++++ openstack/identity/v3/service.py | 4 ++++ openstack/tests/unit/identity/v3/test_credential.py | 9 +++++++++ openstack/tests/unit/identity/v3/test_domain.py | 9 +++++++++ openstack/tests/unit/identity/v3/test_endpoint.py | 8 ++++++++ openstack/tests/unit/identity/v3/test_group.py | 9 +++++++++ openstack/tests/unit/identity/v3/test_region.py | 8 ++++++++ openstack/tests/unit/identity/v3/test_role.py | 9 +++++++++ openstack/tests/unit/identity/v3/test_service.py | 8 ++++++++ 13 files changed, 85 insertions(+) diff --git a/openstack/identity/v3/credential.py b/openstack/identity/v3/credential.py index e49209979..e12794f09 100644 --- a/openstack/identity/v3/credential.py +++ b/openstack/identity/v3/credential.py @@ -28,6 +28,10 @@ class Credential(resource.Resource): allow_list = True patch_update = True + _query_mapping = resource.QueryParameters( + 'type', 'user_id', + ) + # Properties #: Arbitrary blob of the credential data, to be parsed according to the #: ``type``. *Type: string* diff --git a/openstack/identity/v3/domain.py b/openstack/identity/v3/domain.py index f358e4913..b6d79fd15 100644 --- a/openstack/identity/v3/domain.py +++ b/openstack/identity/v3/domain.py @@ -29,6 +29,11 @@ class Domain(resource.Resource): allow_list = True patch_update = True + _query_mapping = resource.QueryParameters( + 'name', + is_enabled='enabled', + ) + # Properties #: The description of this domain. *Type: string* description = resource.Body('description') diff --git a/openstack/identity/v3/endpoint.py b/openstack/identity/v3/endpoint.py index a088f7de8..796f3375c 100644 --- a/openstack/identity/v3/endpoint.py +++ b/openstack/identity/v3/endpoint.py @@ -28,6 +28,10 @@ class Endpoint(resource.Resource): allow_list = True patch_update = True + _query_mapping = resource.QueryParameters( + 'interface', 'service_id', + ) + # Properties #: Describes the interface of the endpoint according to one of the #: following values: diff --git a/openstack/identity/v3/group.py b/openstack/identity/v3/group.py index b47c8cfd2..12ea7e403 100644 --- a/openstack/identity/v3/group.py +++ b/openstack/identity/v3/group.py @@ -28,6 +28,10 @@ class Group(resource.Resource): allow_list = True patch_update = True + _query_mapping = resource.QueryParameters( + 'domain_id', 'name', + ) + # Properties #: The description of this group. *Type: string* description = resource.Body('description') diff --git a/openstack/identity/v3/region.py b/openstack/identity/v3/region.py index 1fdd1c20c..a845614b0 100644 --- a/openstack/identity/v3/region.py +++ b/openstack/identity/v3/region.py @@ -28,6 +28,10 @@ class Region(resource.Resource): allow_list = True patch_update = True + _query_mapping = resource.QueryParameters( + 'parent_region_id', + ) + # Properties #: User-facing description of the region. *Type: string* description = resource.Body('description') diff --git a/openstack/identity/v3/service.py b/openstack/identity/v3/service.py index 4eb7c13ca..79baf40a4 100644 --- a/openstack/identity/v3/service.py +++ b/openstack/identity/v3/service.py @@ -28,6 +28,10 @@ class Service(resource.Resource): allow_list = True patch_update = True + _query_mapping = resource.QueryParameters( + 'type', + ) + # Properties #: User-facing description of the service. *Type: string* description = resource.Body('description') diff --git a/openstack/tests/unit/identity/v3/test_credential.py b/openstack/tests/unit/identity/v3/test_credential.py index 36066d9cd..654fe7f4e 100644 --- a/openstack/tests/unit/identity/v3/test_credential.py +++ b/openstack/tests/unit/identity/v3/test_credential.py @@ -39,6 +39,15 @@ def test_basic(self): self.assertTrue(sot.allow_list) self.assertTrue(sot.patch_update) + self.assertDictEqual( + { + 'type': 'type', + 'user_id': 'user_id', + 'limit': 'limit', + 'marker': 'marker', + }, + sot._query_mapping._mapping) + def test_make_it(self): sot = credential.Credential(**EXAMPLE) self.assertEqual(EXAMPLE['blob'], sot.blob) diff --git a/openstack/tests/unit/identity/v3/test_domain.py b/openstack/tests/unit/identity/v3/test_domain.py index 07faa7fe5..80c2fbe38 100644 --- a/openstack/tests/unit/identity/v3/test_domain.py +++ b/openstack/tests/unit/identity/v3/test_domain.py @@ -39,6 +39,15 @@ def test_basic(self): self.assertTrue(sot.allow_list) self.assertTrue(sot.patch_update) + self.assertDictEqual( + { + 'name': 'name', + 'is_enabled': 'enabled', + 'limit': 'limit', + 'marker': 'marker', + }, + sot._query_mapping._mapping) + def test_make_it(self): sot = domain.Domain(**EXAMPLE) self.assertEqual(EXAMPLE['description'], sot.description) diff --git a/openstack/tests/unit/identity/v3/test_endpoint.py b/openstack/tests/unit/identity/v3/test_endpoint.py index 44af71621..3e59f71b4 100644 --- a/openstack/tests/unit/identity/v3/test_endpoint.py +++ b/openstack/tests/unit/identity/v3/test_endpoint.py @@ -40,6 +40,14 @@ def test_basic(self): self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) self.assertTrue(sot.patch_update) + self.assertDictEqual( + { + 'interface': 'interface', + 'service_id': 'service_id', + 'limit': 'limit', + 'marker': 'marker', + }, + sot._query_mapping._mapping) def test_make_it(self): sot = endpoint.Endpoint(**EXAMPLE) diff --git a/openstack/tests/unit/identity/v3/test_group.py b/openstack/tests/unit/identity/v3/test_group.py index ff30c971e..480baf8de 100644 --- a/openstack/tests/unit/identity/v3/test_group.py +++ b/openstack/tests/unit/identity/v3/test_group.py @@ -38,6 +38,15 @@ def test_basic(self): self.assertTrue(sot.allow_list) self.assertTrue(sot.patch_update) + self.assertDictEqual( + { + 'domain_id': 'domain_id', + 'name': 'name', + 'limit': 'limit', + 'marker': 'marker', + }, + sot._query_mapping._mapping) + def test_make_it(self): sot = group.Group(**EXAMPLE) self.assertEqual(EXAMPLE['description'], sot.description) diff --git a/openstack/tests/unit/identity/v3/test_region.py b/openstack/tests/unit/identity/v3/test_region.py index 01711d0ea..9ee14e082 100644 --- a/openstack/tests/unit/identity/v3/test_region.py +++ b/openstack/tests/unit/identity/v3/test_region.py @@ -38,6 +38,14 @@ def test_basic(self): self.assertTrue(sot.allow_list) self.assertTrue(sot.patch_update) + self.assertDictEqual( + { + 'parent_region_id': 'parent_region_id', + 'limit': 'limit', + 'marker': 'marker', + }, + sot._query_mapping._mapping) + def test_make_it(self): sot = region.Region(**EXAMPLE) self.assertEqual(EXAMPLE['description'], sot.description) diff --git a/openstack/tests/unit/identity/v3/test_role.py b/openstack/tests/unit/identity/v3/test_role.py index 8f9e9809d..7342b7c66 100644 --- a/openstack/tests/unit/identity/v3/test_role.py +++ b/openstack/tests/unit/identity/v3/test_role.py @@ -36,6 +36,15 @@ def test_basic(self): self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) + self.assertDictEqual( + { + 'domain_id': 'domain_id', + 'name': 'name', + 'limit': 'limit', + 'marker': 'marker', + }, + sot._query_mapping._mapping) + def test_make_it(self): sot = role.Role(**EXAMPLE) self.assertEqual(EXAMPLE['id'], sot.id) diff --git a/openstack/tests/unit/identity/v3/test_service.py b/openstack/tests/unit/identity/v3/test_service.py index 848c847f4..578b5f656 100644 --- a/openstack/tests/unit/identity/v3/test_service.py +++ b/openstack/tests/unit/identity/v3/test_service.py @@ -40,6 +40,14 @@ def test_basic(self): self.assertTrue(sot.allow_list) self.assertTrue(sot.patch_update) + self.assertDictEqual( + { + 'type': 'type', + 'limit': 'limit', + 'marker': 'marker', + }, + sot._query_mapping._mapping) + def test_make_it(self): sot = service.Service(**EXAMPLE) self.assertEqual(EXAMPLE['description'], sot.description) From c4acd2352586ff647e4bf2511ec6a56463071079 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 31 Dec 2017 11:45:26 -0600 Subject: [PATCH 1887/3836] Remove self argument from connect helper function This is a factory function, not a class method. Change-Id: I2ff9654dec4acdbba5d311ef690935ff5d20cbbd --- openstack/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/__init__.py b/openstack/__init__.py index a34937014..cf2c692f1 100644 --- a/openstack/__init__.py +++ b/openstack/__init__.py @@ -133,6 +133,6 @@ def operator_cloud( return OperatorCloud(cloud_config=cloud_config, strict=strict) -def connect(self, *args, **kwargs): +def connect(*args, **kwargs): """Create a `openstack.connection.Connection`.""" return openstack.connection.Connection(*args, **kwargs) From 5fd4ec7421d4eb5998ba024df9bd1b46a9a47876 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 27 Nov 2017 19:08:20 -0600 Subject: [PATCH 1888/3836] Start using Connection in openstack.cloud Now that it's all one big happy tree, it's time to start using Connection instead of direct Adapters in the old shade calls. This is a baby step towards that. The _munch_response + _get_and_munchify is clearly a terrible pattern. We'll likely want to shift to using the actual SDK object layer, but let's taking it a bit at a time. This replaces all usage of _compute_client with _conn.compute. Amazingly only one test broke, and that only broke because the test was broken and removing an Exception rethrow caused it to be properly seen. Change-Id: Ice60b2343ba30520bc27bef09ee9d5d7ad19411f --- openstack/_adapter.py | 44 +-- openstack/cloud/openstackcloud.py | 266 ++++++++++-------- openstack/cloud/operatorcloud.py | 128 +++++---- openstack/exceptions.py | 6 +- .../functional/cloud/test_floating_ip.py | 4 +- openstack/tests/unit/cloud/test_flavors.py | 2 +- 6 files changed, 251 insertions(+), 199 deletions(-) diff --git a/openstack/_adapter.py b/openstack/_adapter.py index 4a375b10e..5c9918b3b 100644 --- a/openstack/_adapter.py +++ b/openstack/_adapter.py @@ -15,6 +15,12 @@ ''' Wrapper around keystoneauth Adapter to wrap calls in TaskManager ''' import functools +try: + import simplejson + JSONDecodeError = simplejson.scanner.JSONDecodeError +except ImportError: + JSONDecodeError = ValueError + from six.moves import urllib from keystoneauth1 import adapter @@ -81,6 +87,25 @@ def _extract_name(url): return [part for part in name_parts if part] +def _json_response(response, result_key=None, error_message=None): + """Temporary method to use to bridge from ShadeAdapter to SDK calls.""" + exceptions.raise_from_response(response, error_message=error_message) + + if not response.content: + # This doesn't have any content + return response + + # Some REST calls do not return json content. Don't decode it. + if 'application/json' not in response.headers.get('Content-Type'): + return response + + try: + result_json = response.json() + except JSONDecodeError: + return response + return result_json + + class OpenStackSDKAdapter(adapter.Adapter): """Wrapper around keystoneauth1.adapter.Adapter. @@ -127,21 +152,4 @@ def request(self, url, method, if run_async: return response else: - return self._munch_response(response, error_message=error_message) - - def _munch_response(self, response, result_key=None, error_message=None): - exceptions.raise_from_response(response, error_message=error_message) - - if not response.content: - # This doens't have any content - return response - - # Some REST calls do not return json content. Don't decode it. - if 'application/json' not in response.headers.get('Content-Type'): - return response - - try: - result_json = response.json() - except Exception: - return response - return result_json + return _json_response(response, error_message=error_message) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index fc0828547..84c79d53f 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -21,8 +21,6 @@ import keystoneauth1.session import operator import os -import openstack.config -import openstack.config.defaults import six import threading import time @@ -45,6 +43,9 @@ from openstack.cloud import _normalize from openstack.cloud import meta from openstack.cloud import _utils +import openstack.config +import openstack.config.defaults +import openstack.connection from openstack import task_manager # TODO(shade) shade keys were x-object-meta-x-sdk-md5 - we need to add those @@ -309,6 +310,14 @@ def invalidate(self): _utils.localhost_supports_ipv6() if not self.force_ipv4 else False) self.cloud_config = cloud_config + self._conn_object = None + + @property + def _conn(self): + if not self._conn_object: + self._conn_object = openstack.connection.Connection( + config=self.cloud_config, session=self._keystone_session) + return self._conn_object def connect_as(self, **kwargs): """Make a new OpenStackCloud object with new auth context. @@ -606,13 +615,6 @@ def _container_infra_client(self): 'container-infra') return self._raw_clients['container-infra'] - @property - def _compute_client(self): - # TODO(mordred) Deal with microversions - if 'compute' not in self._raw_clients: - self._raw_clients['compute'] = self._get_raw_client('compute') - return self._raw_clients['compute'] - @property def _database_client(self): if 'database' not in self._raw_clients: @@ -1515,9 +1517,10 @@ def has_service(self, service_key): @_utils.cache_on_arguments() def _nova_extensions(self): extensions = set() - data = self._compute_client.get( - '/extensions', + data = _adapter._json_response( + self._conn.compute.get('/extensions'), error_message="Error fetching extension list for nova") + for extension in self._get_and_munchify('extensions', data): extensions.add(extension['alias']) return extensions @@ -1719,8 +1722,8 @@ def list_keypairs(self): :returns: A list of ``munch.Munch`` containing keypair info. """ - data = self._compute_client.get( - '/os-keypairs', + data = _adapter._json_response( + self._conn.compute.get('/os-keypairs'), error_message="Error fetching keypair list") return self._normalize_keypairs([ k['keypair'] for k in self._get_and_munchify('keypairs', data)]) @@ -1944,7 +1947,8 @@ def list_availability_zone_names(self, unavailable=False): list could not be fetched. """ try: - data = self._compute_client.get('/os-availability-zone') + data = _adapter._json_response( + self._conn.compute.get('/os-availability-zone')) except OpenStackCloudHTTPError: self.log.debug( "Availability zone list could not be fetched", @@ -1970,8 +1974,9 @@ def list_flavors(self, get_extra=None): """ if get_extra is None: get_extra = self._extra_config['get_flavor_extra_specs'] - data = self._compute_client.get( - '/flavors/detail', params=dict(is_public='None'), + data = _adapter._json_response( + self._conn.compute.get( + '/flavors/detail', params=dict(is_public='None')), error_message="Error fetching flavor list") flavors = self._normalize_flavors( self._get_and_munchify('flavors', data)) @@ -1981,8 +1986,8 @@ def list_flavors(self, get_extra=None): endpoint = "/flavors/{id}/os-extra_specs".format( id=flavor.id) try: - data = self._compute_client.get( - endpoint, + data = _adapter._json_response( + self._conn.compute.get(endpoint), error_message="Error fetching flavor extra specs") flavor.extra_specs = self._get_and_munchify( 'extra_specs', data) @@ -2018,9 +2023,10 @@ def list_server_security_groups(self, server): if not self._has_secgroups(): return [] - data = self._compute_client.get( - '/servers/{server_id}/os-security-groups'.format( - server_id=server['id'])) + data = _adapter._json_response( + self._conn.compute.get( + '/servers/{server_id}/os-security-groups'.format( + server_id=server['id']))) return self._normalize_secgroups( self._get_and_munchify('security_groups', data)) @@ -2074,9 +2080,9 @@ def add_server_security_groups(self, server, security_groups): return False for sg in security_groups: - self._compute_client.post( + _adapter._json_response(self._conn.compute.post( '/servers/%s/action' % server['id'], - json={'addSecurityGroup': {'name': sg.name}}) + json={'addSecurityGroup': {'name': sg.name}})) return True @@ -2102,9 +2108,9 @@ def remove_server_security_groups(self, server, security_groups): for sg in security_groups: try: - self._compute_client.post( + _adapter._json_response(self._conn.compute.post( '/servers/%s/action' % server['id'], - json={'removeSecurityGroup': {'name': sg.name}}) + json={'removeSecurityGroup': {'name': sg.name}})) except OpenStackCloudURINotFound: # NOTE(jamielennox): Is this ok? If we remove something that @@ -2144,8 +2150,8 @@ def list_security_groups(self, filters=None): # Handle nova security groups else: - data = self._compute_client.get( - '/os-security-groups', params=filters) + data = _adapter._json_response(self._conn.compute.get( + '/os-security-groups', params=filters)) return self._normalize_secgroups( self._get_and_munchify('security_groups', data)) @@ -2197,8 +2203,10 @@ def _list_servers(self, detailed=False, all_projects=False, bare=False, params = filters or {} if all_projects: params['all_tenants'] = True - data = self._compute_client.get( - '/servers/detail', params=params, error_message=error_msg) + data = _adapter._json_response( + self._conn.compute.get( + '/servers/detail', params=params), + error_message=error_msg) servers = self._normalize_servers( self._get_and_munchify('servers', data)) return [ @@ -2212,8 +2220,8 @@ def list_server_groups(self): :returns: A list of server group dicts. """ - data = self._compute_client.get( - '/os-server-groups', + data = _adapter._json_response( + self._conn.compute.get('/os-server-groups'), error_message="Error fetching server group list") return self._get_and_munchify('server_groups', data) @@ -2239,7 +2247,8 @@ def get_compute_limits(self, name_or_id=None): error_msg = "{msg} for the project: {project} ".format( msg=error_msg, project=name_or_id) - data = self._compute_client.get('/limits', params=params) + data = _adapter._json_response( + self._conn.compute.get('/limits', params=params)) limits = self._get_and_munchify('limits', data) return self._normalize_compute_limits(limits, project_id=project_id) @@ -2273,7 +2282,8 @@ def list_images(self, filter_deleted=True, show_all=False): except keystoneauth1.exceptions.catalog.EndpointNotFound: # We didn't have glance, let's try nova # If this doesn't work - we just let the exception propagate - response = self._compute_client.get('/images/detail') + response = _adapter._json_response( + self._conn.compute.get('/images/detail')) while 'next' in response: image_list.extend(meta.obj_list_to_munch(response['images'])) endpoint = response['next'] @@ -2313,8 +2323,8 @@ def list_floating_ip_pools(self): raise OpenStackCloudUnavailableExtension( 'Floating IP pools extension is not available on target cloud') - data = self._compute_client.get( - 'os-floating-ip-pools', + data = _adapter._json_response( + self._conn.compute.get('os-floating-ip-pools'), error_message="Error fetching floating IP pool list") pools = self._get_and_munchify('floating_ip_pools', data) return [{'name': p['name']} for p in pools] @@ -2403,7 +2413,8 @@ def _neutron_list_floating_ips(self, filters=None): def _nova_list_floating_ips(self): try: - data = self._compute_client.get('/os-floating-ips') + data = _adapter._json_response( + self._conn.compute.get('/os-floating-ips')) except OpenStackCloudURINotFound: return [] return self._get_and_munchify('floating_ips', data) @@ -3007,8 +3018,8 @@ def get_flavor_by_id(self, id, get_extra=True): specs. :returns: A flavor ``munch.Munch``. """ - data = self._compute_client.get( - '/flavors/{id}'.format(id=id), + data = _adapter._json_response( + self._conn.compute.get('/flavors/{id}'.format(id=id)), error_message="Error getting flavor with ID {id}".format(id=id) ) flavor = self._normalize_flavor( @@ -3021,8 +3032,8 @@ def get_flavor_by_id(self, id, get_extra=True): endpoint = "/flavors/{id}/os-extra_specs".format( id=flavor.id) try: - data = self._compute_client.get( - endpoint, + data = _adapter._json_response( + self._conn.compute.get(endpoint), error_message="Error fetching flavor extra specs") flavor.extra_specs = self._get_and_munchify( 'extra_specs', data) @@ -3077,8 +3088,9 @@ def get_security_group_by_id(self, id): '/security-groups/{id}'.format(id=id), error_message=error_message) else: - data = self._compute_client.get( - '/os-security-groups/{id}'.format(id=id), + data = _adapter._json_response( + self._conn.compute.get( + '/os-security-groups/{id}'.format(id=id)), error_message=error_message) return self._normalize_secgroup( self._get_and_munchify('security_group', data)) @@ -3110,9 +3122,9 @@ def get_server_console(self, server, length=None): return "" def _get_server_console_output(self, server_id, length=None): - data = self._compute_client.post( + data = _adapter._json_response(self._conn.compute.post( '/servers/{server_id}/action'.format(server_id=server_id), - json={'os-getConsoleOutput': {'length': length}}) + json={'os-getConsoleOutput': {'length': length}})) return self._get_and_munchify('output', data) def get_server( @@ -3159,7 +3171,8 @@ def _expand_server(self, server, detailed, bare): return meta.add_server_interfaces(self, server) def get_server_by_id(self, id): - data = self._compute_client.get('/servers/{id}'.format(id=id)) + data = _adapter._json_response( + self._conn.compute.get('/servers/{id}'.format(id=id))) server = self._get_and_munchify('server', data) return meta.add_server_interfaces(self, self._normalize_server(server)) @@ -3317,8 +3330,8 @@ def get_floating_ip_by_id(self, id): return self._normalize_floating_ip( self._get_and_munchify('floatingip', data)) else: - data = self._compute_client.get( - '/os-floating-ips/{id}'.format(id=id), + data = _adapter._json_response( + self._conn.compute.get('/os-floating-ips/{id}'.format(id=id)), error_message=error_message) return self._normalize_floating_ip( self._get_and_munchify('floating_ip', data)) @@ -3368,9 +3381,10 @@ def create_keypair(self, name, public_key=None): } if public_key: keypair['public_key'] = public_key - data = self._compute_client.post( - '/os-keypairs', - json={'keypair': keypair}, + data = _adapter._json_response( + self._conn.compute.post( + '/os-keypairs', + json={'keypair': keypair}), error_message="Unable to create keypair {name}".format(name=name)) return self._normalize_keypair( self._get_and_munchify('keypair', data)) @@ -3385,8 +3399,8 @@ def delete_keypair(self, name): :raises: OpenStackCloudException on operation error. """ try: - self._compute_client.delete('/os-keypairs/{name}'.format( - name=name)) + _adapter._json_response(self._conn.compute.delete( + '/os-keypairs/{name}'.format(name=name))) except OpenStackCloudURINotFound: self.log.debug("Keypair %s not found for deleting", name) return False @@ -4419,14 +4433,15 @@ def create_image_snapshot( "Server {server} could not be found and therefore" " could not be snapshotted.".format(server=server)) server = server_obj - response = self._compute_client.post( - '/servers/{server_id}/action'.format(server_id=server['id']), - json={ - "createImage": { - "name": name, - "metadata": metadata, - } - }) + response = _adapter._json_response( + self._conn.compute.post( + '/servers/{server_id}/action'.format(server_id=server['id']), + json={ + "createImage": { + "name": name, + "metadata": metadata, + } + })) # You won't believe it - wait, who am I kidding - of course you will! # Nova returns the URL of the image created in the Location # header of the response. (what?) But, even better, the URL it responds @@ -5156,9 +5171,9 @@ def detach_volume(self, server, volume, wait=True, timeout=None): :raises: OpenStackCloudException on operation error. """ - self._compute_client.delete( + _adapter._json_response(self._conn.compute.delete( '/servers/{server_id}/os-volume_attachments/{volume_id}'.format( - server_id=server['id'], volume_id=volume['id']), + server_id=server['id'], volume_id=volume['id'])), error_message=( "Error detaching volume {volume} from server {server}".format( volume=volume['id'], server=server['id']))) @@ -5223,10 +5238,11 @@ def attach_volume(self, server, volume, device=None, payload = {'volumeId': volume['id']} if device: payload['device'] = device - data = self._compute_client.post( - '/servers/{server_id}/os-volume_attachments'.format( - server_id=server['id']), - json=dict(volumeAttachment=payload), + data = _adapter._json_response( + self._conn.compute.post( + '/servers/{server_id}/os-volume_attachments'.format( + server_id=server['id']), + json=dict(volumeAttachment=payload)), error_message="Error attaching volume {volume_id} to server " "{server_id}".format(volume_id=volume['id'], server_id=server['id'])) @@ -5870,12 +5886,13 @@ def _nova_create_floating_ip(self, pool=None): "unable to find a floating ip pool") pool = pools[0]['name'] - data = self._compute_client.post( - '/os-floating-ips', json=dict(pool=pool)) + data = _adapter._json_response(self._conn.compute.post( + '/os-floating-ips', json=dict(pool=pool))) pool_ip = self._get_and_munchify('floating_ip', data) # TODO(mordred) Remove this - it's just for compat - data = self._compute_client.get('/os-floating-ips/{id}'.format( - id=pool_ip['id'])) + data = _adapter._json_response( + self._conn.compute.get('/os-floating-ips/{id}'.format( + id=pool_ip['id']))) return self._get_and_munchify('floating_ip', data) def delete_floating_ip(self, floating_ip_id, retry=1): @@ -5943,8 +5960,9 @@ def _neutron_delete_floating_ip(self, floating_ip_id): def _nova_delete_floating_ip(self, floating_ip_id): try: - self._compute_client.delete( - '/os-floating-ips/{id}'.format(id=floating_ip_id), + _adapter._json_response( + self._conn.compute.delete( + '/os-floating-ips/{id}'.format(id=floating_ip_id)), error_message='Unable to delete floating IP {fip_id}'.format( fip_id=floating_ip_id)) except OpenStackCloudURINotFound: @@ -6192,9 +6210,10 @@ def _nova_attach_ip_to_server(self, server_id, floating_ip_id, } if fixed_address: body['fixed_address'] = fixed_address - return self._compute_client.post( - '/servers/{server_id}/action'.format(server_id=server_id), - json=dict(addFloatingIp=body), + return _adapter._json_response( + self._conn.compute.post( + '/servers/{server_id}/action'.format(server_id=server_id), + json=dict(addFloatingIp=body)), error_message=error_message) def detach_ip_from_server(self, server_id, floating_ip_id): @@ -6243,10 +6262,11 @@ def _nova_detach_ip_from_server(self, server_id, floating_ip_id): "unable to find floating IP {0}".format(floating_ip_id)) error_message = "Error detaching IP {ip} from instance {id}".format( ip=floating_ip_id, id=server_id) - return self._compute_client.post( - '/servers/{server_id}/action'.format(server_id=server_id), - json=dict(removeFloatingIp=dict( - address=f_ip['floating_ip_address'])), + return _adapter._json_response( + self._conn.compute.post( + '/servers/{server_id}/action'.format(server_id=server_id), + json=dict(removeFloatingIp=dict( + address=f_ip['floating_ip_address']))), error_message=error_message) return True @@ -6808,8 +6828,8 @@ def create_server( if 'block_device_mapping_v2' in kwargs: endpoint = '/os-volumes_boot' with _utils.shade_exceptions("Error in creating instance"): - data = self._compute_client.post( - endpoint, json={'server': kwargs}) + data = _adapter._json_response( + self._conn.compute.post(endpoint, json={'server': kwargs})) server = self._get_and_munchify('server', data) admin_pass = server.get('adminPass') or kwargs.get('admin_pass') if not wait: @@ -6928,10 +6948,11 @@ def rebuild_server(self, server_id, image_id, admin_pass=None, if admin_pass: kwargs['adminPass'] = admin_pass - data = self._compute_client.post( - '/servers/{server_id}/action'.format(server_id=server_id), - error_message="Error in rebuilding instance", - json={'rebuild': kwargs}) + data = _adapter._json_response( + self._conn.compute.post( + '/servers/{server_id}/action'.format(server_id=server_id), + json={'rebuild': kwargs}), + error_message="Error in rebuilding instance") server = self._get_and_munchify('server', data) if not wait: return self._expand_server( @@ -6977,9 +6998,10 @@ def set_server_metadata(self, name_or_id, metadata): raise OpenStackCloudException( 'Invalid Server {server}'.format(server=name_or_id)) - self._compute_client.post( - '/servers/{server_id}/metadata'.format(server_id=server['id']), - json={'metadata': metadata}, + _adapter._json_response( + self._conn.compute.post( + '/servers/{server_id}/metadata'.format(server_id=server['id']), + json={'metadata': metadata}), error_message='Error updating server metadata') def delete_server_metadata(self, name_or_id, metadata_keys): @@ -7000,10 +7022,11 @@ def delete_server_metadata(self, name_or_id, metadata_keys): for key in metadata_keys: error_message = 'Error deleting metadata {key} on {server}'.format( key=key, server=name_or_id) - self._compute_client.delete( - '/servers/{server_id}/metadata/{key}'.format( - server_id=server['id'], - key=key), + _adapter._json_response( + self._conn.compute.delete( + '/servers/{server_id}/metadata/{key}'.format( + server_id=server['id'], + key=key)), error_message=error_message) def delete_server( @@ -7073,8 +7096,9 @@ def _delete_server( self._delete_server_floating_ips(server, delete_ip_retry) try: - self._compute_client.delete( - '/servers/{id}'.format(id=server['id']), + _adapter._json_response( + self._conn.compute.delete( + '/servers/{id}'.format(id=server['id'])), error_message="Error in deleting server") except OpenStackCloudURINotFound: return False @@ -7137,10 +7161,11 @@ def update_server(self, name_or_id, detailed=False, bare=False, **kwargs): raise OpenStackCloudException( "failed to find server '{server}'".format(server=name_or_id)) - data = self._compute_client.put( - '/servers/{server_id}'.format(server_id=server['id']), - error_message="Error updating server {0}".format(name_or_id), - json={'server': kwargs}) + data = _adapter._json_response( + self._conn.compute.put( + '/servers/{server_id}'.format(server_id=server['id']), + json={'server': kwargs}), + error_message="Error updating server {0}".format(name_or_id)) server = self._normalize_server( self._get_and_munchify('server', data)) return self._expand_server(server, bare=bare, detailed=detailed) @@ -7155,12 +7180,13 @@ def create_server_group(self, name, policies): :raises: OpenStackCloudException on operation error. """ - data = self._compute_client.post( - '/os-server-groups', - json={ - 'server_group': { - 'name': name, - 'policies': policies}}, + data = _adapter._json_response( + self._conn.compute.post( + '/os-server-groups', + json={ + 'server_group': { + 'name': name, + 'policies': policies}}), error_message="Unable to create server group {name}".format( name=name)) return self._get_and_munchify('server_group', data) @@ -7180,8 +7206,9 @@ def delete_server_group(self, name_or_id): name_or_id) return False - self._compute_client.delete( - '/os-server-groups/{id}'.format(id=server_group['id']), + _adapter._json_response( + self._conn.compute.delete( + '/os-server-groups/{id}'.format(id=server_group['id'])), error_message="Error deleting server group {name}".format( name=name_or_id)) @@ -8124,8 +8151,8 @@ def create_security_group(self, name, description, project_id=None): json=security_group_json, error_message="Error creating security group {0}".format(name)) else: - data = self._compute_client.post( - '/os-security-groups', json=security_group_json) + data = _adapter._json_response(self._conn.compute.post( + '/os-security-groups', json=security_group_json)) return self._normalize_secgroup( self._get_and_munchify('security_group', data)) @@ -8163,8 +8190,8 @@ def delete_security_group(self, name_or_id): return True else: - self._compute_client.delete( - '/os-security-groups/{id}'.format(id=secgroup['id'])) + _adapter._json_response(self._conn.compute.delete( + '/os-security-groups/{id}'.format(id=secgroup['id']))) return True @_utils.valid_kwargs('name', 'description') @@ -8200,9 +8227,10 @@ def update_security_group(self, name_or_id, **kwargs): else: for key in ('name', 'description'): kwargs.setdefault(key, group[key]) - data = self._compute_client.put( - '/os-security-groups/{id}'.format(id=group['id']), - json={'security-group': kwargs}) + data = _adapter._json_response( + self._conn.compute.put( + '/os-security-groups/{id}'.format(id=group['id']), + json={'security-group': kwargs})) return self._normalize_secgroup( self._get_and_munchify('security_group', data)) @@ -8334,9 +8362,11 @@ def create_security_group_rule(self, if project_id is not None: security_group_rule_dict[ 'security_group_rule']['tenant_id'] = project_id - data = self._compute_client.post( - '/os-security-group-rules', json=security_group_rule_dict - ) + data = _adapter._json_response( + self._conn.compute.post( + '/os-security-group-rules', + json=security_group_rule_dict + )) return self._normalize_secgroup_rule( self._get_and_munchify('security_group_rule', data)) @@ -8368,8 +8398,8 @@ def delete_security_group_rule(self, rule_id): return True else: - self._compute_client.delete( - '/os-security-group-rules/{id}'.format(id=rule_id)) + _adapter._json_response(self._conn.compute.delete( + '/os-security-group-rules/{id}'.format(id=rule_id))) return True def list_zones(self): diff --git a/openstack/cloud/operatorcloud.py b/openstack/cloud/operatorcloud.py index ecb17c298..71146d967 100644 --- a/openstack/cloud/operatorcloud.py +++ b/openstack/cloud/operatorcloud.py @@ -15,6 +15,7 @@ import jsonpatch import munch +from openstack import _adapter from openstack.cloud.exc import * # noqa from openstack.cloud import openstackcloud from openstack.cloud import _utils @@ -1710,9 +1711,9 @@ def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", } if flavorid == 'auto': payload['id'] = None - data = self._compute_client.post( + data = _adapter._json_response(self._conn.compute.post( '/flavors', - json=dict(flavor=payload)) + json=dict(flavor=payload))) return self._normalize_flavor( self._get_and_munchify('flavor', data)) @@ -1732,10 +1733,11 @@ def delete_flavor(self, name_or_id): "Flavor %s not found for deleting", name_or_id) return False - with _utils.shade_exceptions("Unable to delete flavor {name}".format( - name=name_or_id)): - self._compute_client.delete( - '/flavors/{id}'.format(id=flavor['id'])) + _adapter._json_response( + self._conn.compute.delete( + '/flavors/{id}'.format(id=flavor['id'])), + error_message="Unable to delete flavor {name}".format( + name=name_or_id)) return True @@ -1748,14 +1750,11 @@ def set_flavor_specs(self, flavor_id, extra_specs): :raises: OpenStackCloudException on operation error. :raises: OpenStackCloudResourceNotFound if flavor ID is not found. """ - try: - self._compute_client.post( + _adapter._json_response( + self._conn.compute.post( "/flavors/{id}/os-extra_specs".format(id=flavor_id), - json=dict(extra_specs=extra_specs)) - except Exception as e: - raise OpenStackCloudException( - "Unable to set flavor specs: {0}".format(str(e)) - ) + json=dict(extra_specs=extra_specs)), + error_message="Unable to set flavor specs") def unset_flavor_specs(self, flavor_id, keys): """Delete extra specs from a flavor @@ -1767,14 +1766,11 @@ def unset_flavor_specs(self, flavor_id, keys): :raises: OpenStackCloudResourceNotFound if flavor ID is not found. """ for key in keys: - try: - self._compute_client.delete( + _adapter._json_response( + self._conn.compute.delete( "/flavors/{id}/os-extra_specs/{key}".format( - id=flavor_id, key=key)) - except Exception as e: - raise OpenStackCloudException( - "Unable to delete flavor spec {0}: {1}".format( - key, str(e))) + id=flavor_id, key=key)), + error_message="Unable to delete flavor spec {0}".format(key)) def _mod_flavor_access(self, action, flavor_id, project_id): """Common method for adding and removing flavor access @@ -1786,7 +1782,8 @@ def _mod_flavor_access(self, action, flavor_id, project_id): access = {'tenant': project_id} access_key = '{action}TenantAccess'.format(action=action) - self._compute_client.post(endpoint, json={access_key: access}) + _adapter._json_response( + self._conn.compute.post(endpoint, json={access_key: access})) def add_flavor_access(self, flavor_id, project_id): """Grant access to a private flavor for a project/tenant. @@ -1817,11 +1814,12 @@ def list_flavor_access(self, flavor_id): :raises: OpenStackCloudException on operation error. """ - with _utils.shade_exceptions("Error trying to list access from " - "flavor ID {flavor}".format( - flavor=flavor_id)): - data = self._compute_client.get( - '/flavors/{id}/os-flavor-access'.format(id=flavor_id)) + data = _adapter._json_response( + self._conn.compute.get( + '/flavors/{id}/os-flavor-access'.format(id=flavor_id)), + error_message=( + "Error trying to list access from flavorID {flavor}".format( + flavor=flavor_id))) return _utils.normalize_flavor_accesses( self._get_and_munchify('flavor_access', data)) @@ -2095,8 +2093,8 @@ def list_hypervisors(self): :returns: A list of hypervisor ``munch.Munch``. """ - data = self._compute_client.get( - '/os-hypervisors/detail', + data = _adapter._json_response( + self._conn.compute.get('/os-hypervisors/detail'), error_message="Error fetching hypervisor list") return self._get_and_munchify('hypervisors', data) @@ -2120,8 +2118,8 @@ def list_aggregates(self): :returns: A list of aggregate dicts. """ - data = self._compute_client.get( - '/os-aggregates', + data = _adapter._json_response( + self._conn.compute.get('/os-aggregates'), error_message="Error fetching aggregate list") return self._get_and_munchify('aggregates', data) @@ -2156,12 +2154,13 @@ def create_aggregate(self, name, availability_zone=None): :raises: OpenStackCloudException on operation error. """ - data = self._compute_client.post( - '/os-aggregates', - json={'aggregate': { - 'name': name, - 'availability_zone': availability_zone - }}, + data = _adapter._json_response( + self._conn.compute.post( + '/os-aggregates', + json={'aggregate': { + 'name': name, + 'availability_zone': availability_zone + }}), error_message="Unable to create host aggregate {name}".format( name=name)) return self._get_and_munchify('aggregate', data) @@ -2183,9 +2182,10 @@ def update_aggregate(self, name_or_id, **kwargs): raise OpenStackCloudException( "Host aggregate %s not found." % name_or_id) - data = self._compute_client.put( - '/os-aggregates/{id}'.format(id=aggregate['id']), - json={'aggregate': kwargs}, + data = _adapter._json_response( + self._conn.compute.put( + '/os-aggregates/{id}'.format(id=aggregate['id']), + json={'aggregate': kwargs}), error_message="Error updating aggregate {name}".format( name=name_or_id)) return self._get_and_munchify('aggregate', data) @@ -2204,8 +2204,9 @@ def delete_aggregate(self, name_or_id): self.log.debug("Aggregate %s not found for deleting", name_or_id) return False - return self._compute_client.delete( - '/os-aggregates/{id}'.format(id=aggregate['id']), + return _adapter._json_response( + self._conn.compute.delete( + '/os-aggregates/{id}'.format(id=aggregate['id'])), error_message="Error deleting aggregate {name}".format( name=name_or_id)) @@ -2230,9 +2231,10 @@ def set_aggregate_metadata(self, name_or_id, metadata): err_msg = "Unable to set metadata for host aggregate {name}".format( name=name_or_id) - data = self._compute_client.post( - '/os-aggregates/{id}/action'.format(id=aggregate['id']), - json={'set_metadata': {'metadata': metadata}}, + data = _adapter._json_response( + self._conn.compute.post( + '/os-aggregates/{id}/action'.format(id=aggregate['id']), + json={'set_metadata': {'metadata': metadata}}), error_message=err_msg) return self._get_and_munchify('aggregate', data) @@ -2252,9 +2254,10 @@ def add_host_to_aggregate(self, name_or_id, host_name): err_msg = "Unable to add host {host} to aggregate {name}".format( host=host_name, name=name_or_id) - return self._compute_client.post( - '/os-aggregates/{id}/action'.format(id=aggregate['id']), - json={'add_host': {'host': host_name}}, + return _adapter._json_response( + self._conn.compute.post( + '/os-aggregates/{id}/action'.format(id=aggregate['id']), + json={'add_host': {'host': host_name}}), error_message=err_msg) def remove_host_from_aggregate(self, name_or_id, host_name): @@ -2273,9 +2276,10 @@ def remove_host_from_aggregate(self, name_or_id, host_name): err_msg = "Unable to remove host {host} to aggregate {name}".format( host=host_name, name=name_or_id) - return self._compute_client.post( - '/os-aggregates/{id}/action'.format(id=aggregate['id']), - json={'remove_host': {'host': host_name}}, + return _adapter._json_response( + self._conn.compute.post( + '/os-aggregates/{id}/action'.format(id=aggregate['id']), + json={'remove_host': {'host': host_name}}), error_message=err_msg) def get_volume_type_access(self, name_or_id): @@ -2364,9 +2368,10 @@ def set_compute_quotas(self, name_or_id, **kwargs): # if key in quota.VOLUME_QUOTAS} kwargs['force'] = True - self._compute_client.put( - '/os-quota-sets/{project}'.format(project=proj.id), - json={'quota_set': kwargs}, + _adapter._json_response( + self._conn.compute.put( + '/os-quota-sets/{project}'.format(project=proj.id), + json={'quota_set': kwargs}), error_message="No valid quota or resource") def get_compute_quotas(self, name_or_id): @@ -2380,8 +2385,9 @@ def get_compute_quotas(self, name_or_id): proj = self.get_project(name_or_id) if not proj: raise OpenStackCloudException("project does not exist") - data = self._compute_client.get( - '/os-quota-sets/{project}'.format(project=proj.id)) + data = _adapter._json_response( + self._conn.compute.get( + '/os-quota-sets/{project}'.format(project=proj.id))) return self._get_and_munchify('quota_set', data) def delete_compute_quotas(self, name_or_id): @@ -2396,8 +2402,9 @@ def delete_compute_quotas(self, name_or_id): proj = self.get_project(name_or_id) if not proj: raise OpenStackCloudException("project does not exist") - return self._compute_client.delete( - '/os-quota-sets/{project}'.format(project=proj.id)) + return _adapter._json_response( + self._conn.compute.delete( + '/os-quota-sets/{project}'.format(project=proj.id))) def get_compute_usage(self, name_or_id, start=None, end=None): """ Get usage for a specific project @@ -2454,9 +2461,10 @@ def parse_datetime_for_nova(date): raise OpenStackCloudException("project does not exist: {}".format( name=proj.id)) - data = self._compute_client.get( - '/os-simple-tenant-usage/{project}'.format(project=proj.id), - params=dict(start=start.isoformat(), end=end.isoformat()), + data = _adapter._json_response( + self._conn.compute.get( + '/os-simple-tenant-usage/{project}'.format(project=proj.id), + params=dict(start=start.isoformat(), end=end.isoformat())), error_message="Unable to get usage for project: {name}".format( name=proj.id)) return self._normalize_compute_usage( diff --git a/openstack/exceptions.py b/openstack/exceptions.py index d56c92edc..c1acef598 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -86,7 +86,11 @@ def __init__(self, message='Error', response=None, self.source = "Client" def __unicode__(self): - if not self.url and not self.url: + # 'Error' is the default value for self.message. If self.message isn't + # 'Error', then someone has set a more informative error message + # and we should use it. If it is 'Error', then we should construct a + # better message from the information we do have. + if not self.url or self.message != 'Error': return super(HttpException, self).__str__() if self.url: remote_error = "{source} Error for url: {url}".format( diff --git a/openstack/tests/functional/cloud/test_floating_ip.py b/openstack/tests/functional/cloud/test_floating_ip.py index f07b34d28..aa4e478bc 100644 --- a/openstack/tests/functional/cloud/test_floating_ip.py +++ b/openstack/tests/functional/cloud/test_floating_ip.py @@ -23,6 +23,7 @@ from testtools import content +from openstack import _adapter from openstack.cloud import _utils from openstack.cloud import meta from openstack.cloud.exc import OpenStackCloudException @@ -161,7 +162,8 @@ def _setup_networks(self): self.user_cloud.list_networks()))) else: # Find network names for nova-net - data = self.user_cloud._compute_client.get('/os-tenant-networks') + data = _adapter._json_response( + self.user_cloud._conn.compute.get('/os-tenant-networks')) nets = meta.get_and_munchify('networks', data) self.addDetail( 'networks-nova', diff --git a/openstack/tests/unit/cloud/test_flavors.py b/openstack/tests/unit/cloud/test_flavors.py index eff2d7601..f72641927 100644 --- a/openstack/tests/unit/cloud/test_flavors.py +++ b/openstack/tests/unit/cloud/test_flavors.py @@ -75,7 +75,7 @@ def test_delete_flavor_exception(self): json={'flavors': fakes.FAKE_FLAVOR_LIST}), dict(method='DELETE', uri='{endpoint}/flavors/{id}'.format( - endpoint=fakes.FAKE_FLAVOR_LIST, id=fakes.FLAVOR_ID), + endpoint=fakes.COMPUTE_ENDPOINT, id=fakes.FLAVOR_ID), status_code=503)]) self.assertRaises(openstack.OpenStackCloudException, From da99fae94d8743ac10359324a864b9ec69f26d99 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 3 Jan 2018 11:23:13 -0600 Subject: [PATCH 1889/3836] Update for new docs PTI split docs requirements out into doc/requirements.txt and change the helper tox docs env to be what the gate is going to be running. As part of doing this, we can re-enable the enforcer code. However, it doesn't seem to be working, so leave it as info rather than warn for now. Set the jobs and the tox env to use python3. The enforcer module wants to use ifilterfalse/filterfalse which have different names in 2 vs. 3. There's no reason to NOT run sphinx in python3. Change-Id: I358db11b130b909084f7a9e8925477e931d87117 --- .zuul.yaml | 6 ++++++ doc/requirements.txt | 5 +++++ doc/source/enforcer.py | 15 +++++++++------ test-requirements.txt | 5 ----- tox.ini | 12 +++++++++--- 5 files changed, 29 insertions(+), 14 deletions(-) create mode 100644 doc/requirements.txt diff --git a/.zuul.yaml b/.zuul.yaml index 5160dcff6..9c16647e3 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -201,11 +201,17 @@ - osc-tox-unit-tips check: jobs: + - build-openstack-sphinx-docs: + vars: + sphinx_python: python3 - openstacksdk-functional-devstack - openstacksdk-functional-devstack-magnum: voting: false - openstacksdk-functional-devstack-python3 gate: jobs: + - build-openstack-sphinx-docs: + vars: + sphinx_python: python3 - openstacksdk-functional-devstack - openstacksdk-functional-devstack-python3 diff --git a/doc/requirements.txt b/doc/requirements.txt new file mode 100644 index 000000000..1ff13a32b --- /dev/null +++ b/doc/requirements.txt @@ -0,0 +1,5 @@ +sphinx>=1.6.2 # BSD +docutils>=0.11 # OSI-Approved Open Source, Public Domain +openstackdocstheme>=1.17.0 # Apache-2.0 +beautifulsoup4>=4.6.0 # MIT +reno>=2.5.0 # Apache-2.0 diff --git a/doc/source/enforcer.py b/doc/source/enforcer.py index ff705e9f3..74d0102d0 100644 --- a/doc/source/enforcer.py +++ b/doc/source/enforcer.py @@ -117,17 +117,20 @@ def is_ignored(name): # TEMPORARY: Ignore the wait_for names when determining what is missing. app.info("ENFORCER: Ignoring wait_for_* names...") - missing = set(itertools.ifilterfalse(is_ignored, missing)) + missing = set(itertools.filterfalse(is_ignored, missing)) missing_count = len(missing) app.info("ENFORCER: Found %d missing proxy methods " "in the output" % missing_count) - # TODO(shade) Remove the if DEBUG once the build-openstack-sphinx-docs - # has been updated to use sphinx-build. - if DEBUG: - for name in sorted(missing): - app.info("ENFORCER: %s was not included in the output" % name) + # TODO(shade) This is spewing a bunch of content for missing thing that + # are not actually missing. Leave it as info rather than warn so that the + # gate doesn't break ... but we should figure out why this is broken and + # fix it. + # We also need to deal with Proxy subclassing keystoneauth.adapter.Adapter + # now - some of the warnings come from Adapter elements. + for name in sorted(missing): + app.info("ENFORCER: %s was not included in the output" % name) if app.config.enforcer_warnings_as_errors and missing_count > 0: raise EnforcementError( diff --git a/test-requirements.txt b/test-requirements.txt index d77c48500..943a8376e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,20 +3,15 @@ # process, which may cause wedges in the gate later. hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 -beautifulsoup4>=4.6.0 # MIT coverage!=4.4,>=4.0 # Apache-2.0 doc8>=0.6.0 # Apache-2.0 -docutils>=0.11 # OSI-Approved Open Source, Public Domain extras>=1.0.0 # MIT fixtures>=3.0.0 # Apache-2.0/BSD jsonschema<3.0.0,>=2.6.0 # MIT mock>=2.0.0 # BSD python-subunit>=1.0.0 # Apache-2.0/BSD -openstackdocstheme>=1.17.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 -reno>=2.5.0 # Apache-2.0 requests-mock>=1.1.0 # Apache-2.0 -sphinx>=1.6.2 # BSD stestr>=1.0.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD diff --git a/tox.ini b/tox.ini index 71440c949..46b0ff93a 100644 --- a/tox.ini +++ b/tox.ini @@ -30,6 +30,11 @@ commands = stestr --test-path ./openstack/tests/functional run --serial {posargs stestr slowest [testenv:pep8] +deps = + -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} + -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt + -r{toxinidir}/doc/requirements.txt commands = doc8 doc/source flake8 @@ -60,11 +65,12 @@ passenv = HOME USER commands = {toxinidir}/extras/run-ansible-tests.sh -e {envdir} {posargs} [testenv:docs] -skip_install = True +basepython = python3 deps = -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} - -r{toxinidir}/test-requirements.txt -commands = sphinx-build -b html doc/source/ doc/build + -r{toxinidir}/requirements.txt + -r{toxinidir}/doc/requirements.txt +commands = sphinx-build -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 From 1c06fd3798296b3d2f6c2bcf050a073e89eef088 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 3 Jan 2018 11:27:39 -0600 Subject: [PATCH 1890/3836] Remove name from zuul project stanza These are optional for in-repo config. Remove it. (It'll make renaming to openstacksdk easier in the future, too) Change-Id: I224067aba1e48756cc6ab2d804bcba6a1ba016d0 --- .zuul.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index 9c16647e3..b28f069c9 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -194,7 +194,6 @@ - openstacksdk-functional-devstack-tips-python3 - project: - name: openstack/python-openstacksdk templates: - openstacksdk-functional-tips - openstacksdk-tox-tips From 0ff395d4f12282ba12a638db812d7e846b776f0f Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 9 Jan 2018 02:36:09 +0000 Subject: [PATCH 1891/3836] Updated from global requirements Change-Id: Ifd07dcf3f231b2d9bdc60eaede1a3e48a2620bef --- doc/requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/requirements.txt b/doc/requirements.txt index 1ff13a32b..ff656d7bc 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,3 +1,6 @@ +# 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. sphinx>=1.6.2 # BSD docutils>=0.11 # OSI-Approved Open Source, Public Domain openstackdocstheme>=1.17.0 # Apache-2.0 From 18fe7b4e2beaa328d7d36652d09d3d7b9c0f443d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 5 Jan 2018 18:10:00 -0600 Subject: [PATCH 1892/3836] Rename CloudConfig to CloudRegion The name CloudConfig has always sucked. While working on the next patch, it occurred to me that what a CloudConfig represents is the config for a given region of a cloud. We even reference "Cloud Region" as a unit of work when giving conference talks. Take the opportunity while we're doing this sdk/occ merge to rename it. Obviously we can provide naming compat shim in OCC itself. Leave in *some* naming compat shims to get us past the 0.10 release so that OSC continues to have the happies. Change-Id: Ia0bbc20eb28a3a36e69adba3a4b45323e4aa284e --- doc/source/contributor/layout.rst | 2 +- doc/source/user/config/using.rst | 20 +- doc/source/users/index.rst | 2 +- examples/connect.py | 2 +- openstack/__init__.py | 24 +- openstack/cloud/inventory.py | 6 +- openstack/cloud/openstackcloud.py | 8 +- openstack/config/__init__.py | 14 +- openstack/config/cloud_config.py | 582 +---------------- openstack/config/cloud_region.py | 599 ++++++++++++++++++ openstack/config/loader.py | 29 +- openstack/connection.py | 101 +-- openstack/tests/functional/base.py | 16 +- openstack/tests/functional/cloud/base.py | 4 +- .../tests/functional/image/v2/test_image.py | 2 +- openstack/tests/unit/base.py | 6 +- openstack/tests/unit/cloud/test_inventory.py | 22 +- openstack/tests/unit/cloud/test_operator.py | 16 +- openstack/tests/unit/config/base.py | 4 +- .../tests/unit/config/test_cloud_config.py | 128 ++-- openstack/tests/unit/config/test_config.py | 184 +++--- openstack/tests/unit/config/test_environ.py | 42 +- openstack/tests/unit/config/test_init.py | 12 +- openstack/tests/unit/test_connection.py | 40 +- .../removed-profile-437f3038025b0fb3.yaml | 2 +- 25 files changed, 972 insertions(+), 895 deletions(-) create mode 100644 openstack/config/cloud_region.py diff --git a/doc/source/contributor/layout.rst b/doc/source/contributor/layout.rst index 2c9d70395..b9dcc111a 100644 --- a/doc/source/contributor/layout.rst +++ b/doc/source/contributor/layout.rst @@ -80,7 +80,7 @@ Connection ---------- The :class:`openstack.connection.Connection` class builds atop a -:class:`os_client_config.config.CloudConfig` object, and provides a higher +:class:`os_client_config.config.CloudRegion` object, and provides a higher level interface constructed of ``Proxy`` objects from each of the services. The ``Connection`` class' primary purpose is to act as a high-level interface diff --git a/doc/source/user/config/using.rst b/doc/source/user/config/using.rst index b21356329..1e81b4b4f 100644 --- a/doc/source/user/config/using.rst +++ b/doc/source/user/config/using.rst @@ -20,9 +20,9 @@ Get a named cloud. import openstack.config - cloud_config = openstack.config.OpenStackConfig().get_one_cloud( + cloud_region = openstack.config.OpenStackConfig().get_one( 'internap', region_name='ams01') - print(cloud_config.name, cloud_config.region, cloud_config.config) + print(cloud_region.name, cloud_region.region, cloud_region.config) Or, get all of the clouds. @@ -30,9 +30,9 @@ Or, get all of the clouds. import openstack.config - cloud_config = openstack.config.OpenStackConfig().get_all_clouds() - for cloud in cloud_config: - print(cloud.name, cloud.region, cloud.config) + cloud_regions = openstack.config.OpenStackConfig().get_all() + for cloud_region in cloud_regions: + print(cloud_region.name, cloud_region.region, cloud_region.config) argparse -------- @@ -49,13 +49,13 @@ with - as well as a consumption argument. import openstack.config - cloud_config = openstack.config.OpenStackConfig() + config = openstack.config.OpenStackConfig() parser = argparse.ArgumentParser() - cloud_config.register_argparse_arguments(parser, sys.argv) + config.register_argparse_arguments(parser, sys.argv) options = parser.parse_args() - cloud = cloud_config.get_one_cloud(argparse=options) + cloud_region = config.get_one(argparse=options) Constructing a Connection object -------------------------------- @@ -89,8 +89,8 @@ If you want to do the same thing but also support command line parsing. conn = openstack.config.make_connection(options=argparse.ArgumentParser()) -Constructing cloud objects --------------------------- +Constructing OpenStackCloud objects +----------------------------------- If what you want to do is get an `opentack.cloud.openstackcloud.OpenStackCloud` object, a diff --git a/doc/source/users/index.rst b/doc/source/users/index.rst index 218d172ef..ded0ad57f 100644 --- a/doc/source/users/index.rst +++ b/doc/source/users/index.rst @@ -64,7 +64,7 @@ OpenStack services. connection Once you have a *Connection* instance, the following services may be exposed -to you. The combination of your ``CloudConfig`` and the catalog of the cloud +to you. The combination of your ``CloudRegion`` and the catalog of the cloud in question control which services are exposed, but listed below are the ones provided by the SDK. diff --git a/examples/connect.py b/examples/connect.py index cb819a7a4..d9c52c893 100644 --- a/examples/connect.py +++ b/examples/connect.py @@ -73,7 +73,7 @@ def create_connection_from_args(): config = occ.OpenStackConfig() config.register_argparse_arguments(parser, sys.argv[1:]) args = parser.parse_args() - return openstack.connect(config=config.get_one_cloud(argparse=args)) + return openstack.connect(config=config.get_one(argparse=args)) def create_connection(auth_url, region, project_name, username, password): diff --git a/openstack/__init__.py b/openstack/__init__.py index cf2c692f1..9d67f9083 100644 --- a/openstack/__init__.py +++ b/openstack/__init__.py @@ -81,20 +81,18 @@ def openstack_clouds( return [ OpenStackCloud( cloud=f.name, debug=debug, - cloud_config=f, - strict=strict, - **f.config) - for f in config.get_all_clouds() + cloud_config=cloud_region, + strict=strict) + for cloud_region in config.get_all() ] else: return [ OpenStackCloud( cloud=f.name, debug=debug, - cloud_config=f, - strict=strict, - **f.config) - for f in config.get_all_clouds() - if f.name == cloud + cloud_config=cloud_region, + strict=strict) + for cloud_region in config.get_all() + if cloud_region.name == cloud ] except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin as e: raise OpenStackCloudException( @@ -110,11 +108,11 @@ def openstack_cloud( if not config: config = _get_openstack_config(app_name, app_version) try: - cloud_config = config.get_one_cloud(**kwargs) + cloud_region = config.get_one(**kwargs) except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin as e: raise OpenStackCloudException( "Invalid cloud configuration: {exc}".format(exc=str(e))) - return OpenStackCloud(cloud_config=cloud_config, strict=strict) + return OpenStackCloud(cloud_config=cloud_region, strict=strict) # TODO(shade) This wants to be renamed before we make a release - there is @@ -126,11 +124,11 @@ def operator_cloud( if not config: config = _get_openstack_config(app_name, app_version) try: - cloud_config = config.get_one_cloud(**kwargs) + cloud_region = config.get_one(**kwargs) except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin as e: raise OpenStackCloudException( "Invalid cloud configuration: {exc}".format(exc=str(e))) - return OperatorCloud(cloud_config=cloud_config, strict=strict) + return OperatorCloud(cloud_config=cloud_region, strict=strict) def connect(*args, **kwargs): diff --git a/openstack/cloud/inventory.py b/openstack/cloud/inventory.py index c64f666c4..865339f54 100644 --- a/openstack/cloud/inventory.py +++ b/openstack/cloud/inventory.py @@ -37,14 +37,14 @@ def __init__( if cloud is None: self.clouds = [ - openstack.OpenStackCloud(cloud_config=cloud_config) - for cloud_config in config.get_all_clouds() + openstack.OpenStackCloud(cloud_config=cloud_region) + for cloud_region in config.get_all() ] else: try: self.clouds = [ openstack.OpenStackCloud( - cloud_config=config.get_one_cloud(cloud)) + cloud_config=config.get_one(cloud)) ] except openstack.config.exceptions.OpenStackConfigException as e: raise openstack.OpenStackCloudException(e) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 84c79d53f..c5da3b4d1 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -132,7 +132,7 @@ class OpenStackCloud(_normalize.Normalizer): string. Optional, defaults to None. :param app_version: Version of the application to be appended to the user-agent string. Optional, defaults to None. - :param CloudConfig cloud_config: Cloud config object from os-client-config + :param CloudRegion cloud_config: Cloud config object from os-client-config In the future, this will be the only way to pass in cloud configuration, but is being phased in currently. @@ -157,7 +157,7 @@ def __init__( config = openstack.config.OpenStackConfig( app_name=app_name, app_version=app_version) - cloud_config = config.get_one_cloud(**kwargs) + cloud_config = config.get_one(**kwargs) self.name = cloud_config.name self.auth = cloud_config.get_auth_args() @@ -375,6 +375,8 @@ def pop_keys(params, auth, name_key, id_key): for key, value in kwargs.items(): params['auth'][key] = value + # TODO(mordred) Replace this chunk with the next patch that allows + # passing a Session to CloudRegion. # Closure to pass to OpenStackConfig to ensure the new cloud shares # the Session with the current cloud. This will ensure that version # discovery cache will be re-used. @@ -384,7 +386,7 @@ def session_constructor(*args, **kwargs): return keystoneauth1.session.Session(session=self.keystone_session) # Use cloud='defaults' so that we overlay settings properly - cloud_config = config.get_one_cloud( + cloud_config = config.get_one( cloud='defaults', session_constructor=session_constructor, **params) diff --git a/openstack/config/__init__.py b/openstack/config/__init__.py index 22ba51c1b..e50e7f9db 100644 --- a/openstack/config/__init__.py +++ b/openstack/config/__init__.py @@ -35,7 +35,7 @@ def get_config( else: parsed_options = None - return _config.get_one_cloud(options=parsed_options, **kwargs) + return _config.get_one(options=parsed_options, **kwargs) def make_rest_client( @@ -54,11 +54,11 @@ def make_rest_client( get_session_client on it. This function is to make it easy to poke at OpenStack REST APIs with a properly configured keystone session. """ - cloud = get_config( + cloud_region = get_config( service_key=service_key, options=options, app_name=app_name, app_version=app_version, **kwargs) - return cloud.get_session_client(service_key, version=version) + return cloud_region.get_session_client(service_key, version=version) # Backwards compat - simple_client was a terrible name simple_client = make_rest_client # Backwards compat - session_client was a terrible name @@ -74,8 +74,8 @@ def make_connection(options=None, **kwargs): :rtype: :class:`~openstack.connection.Connection` """ from openstack import connection - cloud = get_config(options=options, **kwargs) - return connection.from_config(cloud_config=cloud, options=options) + cloud_region = get_config(options=options, **kwargs) + return connection.from_config(cloud_region=cloud_region, options=options) def make_cloud(options=None, **kwargs): @@ -86,5 +86,5 @@ def make_cloud(options=None, **kwargs): :rtype: :class:`~openstack.OpenStackCloud` """ import openstack.cloud - cloud = get_config(options=options, **kwargs) - return openstack.OpenStackCloud(cloud_config=cloud, **kwargs) + cloud_region = get_config(options=options, **kwargs) + return openstack.OpenStackCloud(cloud_config=cloud_region, **kwargs) diff --git a/openstack/config/cloud_config.py b/openstack/config/cloud_config.py index ff84ced60..168c43edf 100644 --- a/openstack/config/cloud_config.py +++ b/openstack/config/cloud_config.py @@ -1,4 +1,4 @@ -# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. +# Copyright (c) 2018 Red Hat, 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 @@ -12,583 +12,13 @@ # License for the specific language governing permissions and limitations # under the License. -import importlib -import math -import warnings +# TODO(mordred) This is only here to ease the OSC transition -from keystoneauth1 import adapter -import keystoneauth1.exceptions.catalog -from keystoneauth1 import session -import requestsexceptions +from openstack.config import cloud_region -import openstack -from openstack import _log -from openstack.config import constructors -from openstack.config import exceptions +class CloudConfig(cloud_region.CloudRegion): -def _get_client(service_key): - class_mapping = constructors.get_constructor_mapping() - if service_key not in class_mapping: - raise exceptions.OpenStackConfigException( - "Service {service_key} is unkown. Please pass in a client" - " constructor or submit a patch to os-client-config".format( - service_key=service_key)) - mod_name, ctr_name = class_mapping[service_key].rsplit('.', 1) - lib_name = mod_name.split('.')[0] - try: - mod = importlib.import_module(mod_name) - except ImportError: - raise exceptions.OpenStackConfigException( - "Client for '{service_key}' was requested, but" - " {mod_name} was unable to be imported. Either import" - " the module yourself and pass the constructor in as an argument," - " or perhaps you do not have python-{lib_name} installed.".format( - service_key=service_key, - mod_name=mod_name, - lib_name=lib_name)) - try: - ctr = getattr(mod, ctr_name) - except AttributeError: - raise exceptions.OpenStackConfigException( - "Client for '{service_key}' was requested, but although" - " {mod_name} imported fine, the constructor at {fullname}" - " as not found. Please check your installation, we have no" - " clue what is wrong with your computer.".format( - service_key=service_key, - mod_name=mod_name, - fullname=class_mapping[service_key])) - return ctr - - -def _make_key(key, service_type): - if not service_type: - return key - else: - service_type = service_type.lower().replace('-', '_') - return "_".join([service_type, key]) - - -class CloudConfig(object): - def __init__(self, name, region, config, - force_ipv4=False, auth_plugin=None, - openstack_config=None, session_constructor=None, - app_name=None, app_version=None): - self.name = name + def __init__(self, name, region, config, **kwargs): + super(CloudConfig, self).__init__(name, region, config, **kwargs) self.region = region - self.config = config - self.log = _log.setup_logging(__name__) - self._force_ipv4 = force_ipv4 - self._auth = auth_plugin - self._openstack_config = openstack_config - self._keystone_session = None - self._session_constructor = session_constructor or session.Session - self._app_name = app_name - self._app_version = app_version - - def __getattr__(self, key): - """Return arbitrary attributes.""" - - if key.startswith('os_'): - key = key[3:] - - if key in [attr.replace('-', '_') for attr in self.config]: - return self.config[key] - else: - return None - - def __iter__(self): - return self.config.__iter__() - - def __eq__(self, other): - return (self.name == other.name and self.region == other.region - and self.config == other.config) - - def __ne__(self, other): - return not self == other - - def set_session_constructor(self, session_constructor): - """Sets the Session constructor.""" - self._session_constructor = session_constructor - - def get_requests_verify_args(self): - """Return the verify and cert values for the requests library.""" - if self.config['verify'] and self.config['cacert']: - verify = self.config['cacert'] - else: - verify = self.config['verify'] - if self.config['cacert']: - warnings.warn( - "You are specifying a cacert for the cloud {0} but " - "also to ignore the host verification. The host SSL cert " - "will not be verified.".format(self.name)) - - cert = self.config.get('cert', None) - if cert: - if self.config['key']: - cert = (cert, self.config['key']) - return (verify, cert) - - def get_services(self): - """Return a list of service types we know something about.""" - services = [] - for key, val in self.config.items(): - if (key.endswith('api_version') - or key.endswith('service_type') - or key.endswith('service_name')): - services.append("_".join(key.split('_')[:-2])) - return list(set(services)) - - def get_auth_args(self): - return self.config['auth'] - - def get_interface(self, service_type=None): - key = _make_key('interface', service_type) - interface = self.config.get('interface') - return self.config.get(key, interface) - - def get_region_name(self, service_type=None): - if not service_type: - return self.region - key = _make_key('region_name', service_type) - return self.config.get(key, self.region) - - def get_api_version(self, service_type): - key = _make_key('api_version', service_type) - return self.config.get(key, None) - - def get_service_type(self, service_type): - key = _make_key('service_type', service_type) - # Cinder did an evil thing where they defined a second service - # type in the catalog. Of course, that's insane, so let's hide this - # atrocity from the as-yet-unsullied eyes of our users. - # Of course, if the user requests a volumev2, that structure should - # still work. - # What's even more amazing is that they did it AGAIN with cinder v3 - # And then I learned that mistral copied it. - # TODO(shade) This should get removed when we have os-service-types - # alias support landed in keystoneauth. - if service_type in ('volume', 'block-storage'): - vol_ver = self.get_api_version('volume') - if vol_ver and vol_ver.startswith('2'): - service_type = 'volumev2' - elif vol_ver and vol_ver.startswith('3'): - service_type = 'volumev3' - elif service_type == 'workflow': - wk_ver = self.get_api_version(service_type) - if wk_ver and wk_ver.startswith('2'): - service_type = 'workflowv2' - return self.config.get(key, service_type) - - def get_service_name(self, service_type): - key = _make_key('service_name', service_type) - return self.config.get(key, None) - - def get_endpoint(self, service_type): - key = _make_key('endpoint_override', service_type) - old_key = _make_key('endpoint', service_type) - return self.config.get(key, self.config.get(old_key, None)) - - @property - def prefer_ipv6(self): - return not self._force_ipv4 - - @property - def force_ipv4(self): - return self._force_ipv4 - - def get_auth(self): - """Return a keystoneauth plugin from the auth credentials.""" - return self._auth - - def get_session(self): - """Return a keystoneauth session based on the auth credentials.""" - if self._keystone_session is None: - if not self._auth: - raise exceptions.OpenStackConfigException( - "Problem with auth parameters") - (verify, cert) = self.get_requests_verify_args() - # Turn off urllib3 warnings about insecure certs if we have - # explicitly configured requests to tell it we do not want - # cert verification - if not verify: - self.log.debug( - "Turning off SSL warnings for {cloud}:{region}" - " since verify=False".format( - cloud=self.name, region=self.region)) - requestsexceptions.squelch_warnings(insecure_requests=not verify) - self._keystone_session = self._session_constructor( - auth=self._auth, - verify=verify, - cert=cert, - timeout=self.config['api_timeout']) - if hasattr(self._keystone_session, 'additional_user_agent'): - self._keystone_session.additional_user_agent.append( - ('openstacksdk', openstack.__version__)) - # Using old keystoneauth with new os-client-config fails if - # we pass in app_name and app_version. Those are not essential, - # nor a reason to bump our minimum, so just test for the session - # having the attribute post creation and set them then. - if hasattr(self._keystone_session, 'app_name'): - self._keystone_session.app_name = self._app_name - if hasattr(self._keystone_session, 'app_version'): - self._keystone_session.app_version = self._app_version - return self._keystone_session - - def get_service_catalog(self): - """Helper method to grab the service catalog.""" - return self._auth.get_access(self.get_session()).service_catalog - - def _get_version_args(self, service_key, version): - """Translate OCC version args to those needed by ksa adapter. - - If no version is requested explicitly and we have a configured version, - set the version parameter and let ksa deal with expanding that to - min=ver.0, max=ver.latest. - - If version is set, pass it through. - - If version is not set and we don't have a configured version, default - to latest. - """ - if version == 'latest': - return None, None, 'latest' - if not version: - version = self.get_api_version(service_key) - if not version: - return None, None, 'latest' - return version, None, None - - def get_session_client(self, service_key, version=None): - """Return a prepped requests adapter for a given service. - - This is useful for making direct requests calls against a - 'mounted' endpoint. That is, if you do: - - client = get_session_client('compute') - - then you can do: - - client.get('/flavors') - - and it will work like you think. - """ - (version, min_version, max_version) = self._get_version_args( - service_key, version) - - return adapter.Adapter( - session=self.get_session(), - service_type=self.get_service_type(service_key), - service_name=self.get_service_name(service_key), - interface=self.get_interface(service_key), - region_name=self.get_region_name(service_key), - version=version, - min_version=min_version, - max_version=max_version) - - def _get_highest_endpoint(self, service_types, kwargs): - session = self.get_session() - for service_type in service_types: - kwargs['service_type'] = service_type - try: - # Return the highest version we find that matches - # the request - return session.get_endpoint(**kwargs) - except keystoneauth1.exceptions.catalog.EndpointNotFound: - pass - - def get_session_endpoint( - self, service_key, min_version=None, max_version=None): - """Return the endpoint from config or the catalog. - - If a configuration lists an explicit endpoint for a service, - return that. Otherwise, fetch the service catalog from the - keystone session and return the appropriate endpoint. - - :param service_key: Generic key for service, such as 'compute' or - 'network' - - """ - - override_endpoint = self.get_endpoint(service_key) - if override_endpoint: - return override_endpoint - endpoint = None - kwargs = { - 'service_name': self.get_service_name(service_key), - 'region_name': self.region - } - kwargs['interface'] = self.get_interface(service_key) - if service_key == 'volume' and not self.get_api_version('volume'): - # If we don't have a configured cinder version, we can't know - # to request a different service_type - min_version = float(min_version or 1) - max_version = float(max_version or 3) - min_major = math.trunc(float(min_version)) - max_major = math.trunc(float(max_version)) - versions = range(int(max_major) + 1, int(min_major), -1) - service_types = [] - for version in versions: - if version == 1: - service_types.append('volume') - else: - service_types.append('volumev{v}'.format(v=version)) - else: - service_types = [self.get_service_type(service_key)] - endpoint = self._get_highest_endpoint(service_types, kwargs) - if not endpoint: - self.log.warning( - "Keystone catalog entry not found (" - "service_type=%s,service_name=%s" - "interface=%s,region_name=%s)", - service_key, - kwargs['service_name'], - kwargs['interface'], - kwargs['region_name']) - return endpoint - - def get_legacy_client( - self, service_key, client_class=None, interface_key=None, - pass_version_arg=True, version=None, min_version=None, - max_version=None, **kwargs): - """Return a legacy OpenStack client object for the given config. - - Most of the OpenStack python-*client libraries have the same - interface for their client constructors, but there are several - parameters one wants to pass given a :class:`CloudConfig` object. - - In the future, OpenStack API consumption should be done through - the OpenStack SDK, but that's not ready yet. This is for getting - Client objects from python-*client only. - - :param service_key: Generic key for service, such as 'compute' or - 'network' - :param client_class: Class of the client to be instantiated. This - should be the unversioned version if there - is one, such as novaclient.client.Client, or - the versioned one, such as - neutronclient.v2_0.client.Client if there isn't - :param interface_key: (optional) Some clients, such as glanceclient - only accept the parameter 'interface' instead - of 'endpoint_type' - this is a get-out-of-jail - parameter for those until they can be aligned. - os-client-config understands this to be the - case if service_key is image, so this is really - only for use with other unknown broken clients. - :param pass_version_arg: (optional) If a versioned Client constructor - was passed to client_class, set this to - False, which will tell get_client to not - pass a version parameter. os-client-config - already understand that this is the - case for network, so it can be omitted in - that case. - :param version: (optional) Version string to override the configured - version string. - :param min_version: (options) Minimum version acceptable. - :param max_version: (options) Maximum version acceptable. - :param kwargs: (optional) keyword args are passed through to the - Client constructor, so this is in case anything - additional needs to be passed in. - """ - if not client_class: - client_class = _get_client(service_key) - - interface = self.get_interface(service_key) - # trigger exception on lack of service - endpoint = self.get_session_endpoint( - service_key, min_version=min_version, max_version=max_version) - endpoint_override = self.get_endpoint(service_key) - - if service_key == 'object-store': - constructor_kwargs = dict( - session=self.get_session(), - os_options=dict( - service_type=self.get_service_type(service_key), - object_storage_url=endpoint_override, - region_name=self.region)) - else: - constructor_kwargs = dict( - session=self.get_session(), - service_name=self.get_service_name(service_key), - service_type=self.get_service_type(service_key), - endpoint_override=endpoint_override, - region_name=self.region) - - if service_key == 'image': - # os-client-config does not depend on glanceclient, but if - # the user passed in glanceclient.client.Client, which they - # would need to do if they were requesting 'image' - then - # they necessarily have glanceclient installed - from glanceclient.common import utils as glance_utils - endpoint, detected_version = glance_utils.strip_version(endpoint) - # If the user has passed in a version, that's explicit, use it - if not version: - version = detected_version - # If the user has passed in or configured an override, use it. - # Otherwise, ALWAYS pass in an endpoint_override becuase - # we've already done version stripping, so we don't want version - # reconstruction to happen twice - if not endpoint_override: - constructor_kwargs['endpoint_override'] = endpoint - constructor_kwargs.update(kwargs) - if pass_version_arg and service_key != 'object-store': - if not version: - version = self.get_api_version(service_key) - if not version and service_key == 'volume': - from cinderclient import client as cinder_client - version = cinder_client.get_volume_api_from_url(endpoint) - # Temporary workaround while we wait for python-openstackclient - # to be able to handle 2.0 which is what neutronclient expects - if service_key == 'network' and version == '2': - version = '2.0' - if service_key == 'identity': - # Workaround for bug#1513839 - if 'endpoint' not in constructor_kwargs: - endpoint = self.get_session_endpoint('identity') - constructor_kwargs['endpoint'] = endpoint - if service_key == 'network': - constructor_kwargs['api_version'] = version - elif service_key == 'baremetal': - if version != '1': - # Set Ironic Microversion - constructor_kwargs['os_ironic_api_version'] = version - # Version arg is the major version, not the full microstring - constructor_kwargs['version'] = version[0] - else: - constructor_kwargs['version'] = version - if min_version and min_version > float(version): - raise exceptions.OpenStackConfigVersionException( - "Minimum version {min_version} requested but {version}" - " found".format(min_version=min_version, version=version), - version=version) - if max_version and max_version < float(version): - raise exceptions.OpenStackConfigVersionException( - "Maximum version {max_version} requested but {version}" - " found".format(max_version=max_version, version=version), - version=version) - if service_key == 'database': - # TODO(mordred) Remove when https://review.openstack.org/314032 - # has landed and released. We're passing in a Session, but the - # trove Client object has username and password as required - # args - constructor_kwargs['username'] = None - constructor_kwargs['password'] = None - - if not interface_key: - if service_key in ('image', 'key-manager'): - interface_key = 'interface' - elif (service_key == 'identity' - and version and version.startswith('3')): - interface_key = 'interface' - else: - interface_key = 'endpoint_type' - if service_key == 'object-store': - constructor_kwargs['os_options'][interface_key] = interface - else: - constructor_kwargs[interface_key] = interface - - return client_class(**constructor_kwargs) - - def get_cache_expiration_time(self): - if self._openstack_config: - return self._openstack_config.get_cache_expiration_time() - - def get_cache_path(self): - if self._openstack_config: - return self._openstack_config.get_cache_path() - - def get_cache_class(self): - if self._openstack_config: - return self._openstack_config.get_cache_class() - - def get_cache_arguments(self): - if self._openstack_config: - return self._openstack_config.get_cache_arguments() - - def get_cache_expiration(self): - if self._openstack_config: - return self._openstack_config.get_cache_expiration() - - def get_cache_resource_expiration(self, resource, default=None): - """Get expiration time for a resource - - :param resource: Name of the resource type - :param default: Default value to return if not found (optional, - defaults to None) - - :returns: Expiration time for the resource type as float or default - """ - if self._openstack_config: - expiration = self._openstack_config.get_cache_expiration() - if resource not in expiration: - return default - return float(expiration[resource]) - - def requires_floating_ip(self): - """Return whether or not this cloud requires floating ips. - - - :returns: True of False if know, None if discovery is needed. - If requires_floating_ip is not configured but the cloud is - known to not provide floating ips, will return False. - """ - if self.config['floating_ip_source'] == "None": - return False - return self.config.get('requires_floating_ip') - - def get_external_networks(self): - """Get list of network names for external networks.""" - return [ - net['name'] for net in self.config['networks'] - if net['routes_externally']] - - def get_external_ipv4_networks(self): - """Get list of network names for external IPv4 networks.""" - return [ - net['name'] for net in self.config['networks'] - if net['routes_ipv4_externally']] - - def get_external_ipv6_networks(self): - """Get list of network names for external IPv6 networks.""" - return [ - net['name'] for net in self.config['networks'] - if net['routes_ipv6_externally']] - - def get_internal_networks(self): - """Get list of network names for internal networks.""" - return [ - net['name'] for net in self.config['networks'] - if not net['routes_externally']] - - def get_internal_ipv4_networks(self): - """Get list of network names for internal IPv4 networks.""" - return [ - net['name'] for net in self.config['networks'] - if not net['routes_ipv4_externally']] - - def get_internal_ipv6_networks(self): - """Get list of network names for internal IPv6 networks.""" - return [ - net['name'] for net in self.config['networks'] - if not net['routes_ipv6_externally']] - - def get_default_network(self): - """Get network used for default interactions.""" - for net in self.config['networks']: - if net['default_interface']: - return net['name'] - return None - - def get_nat_destination(self): - """Get network used for NAT destination.""" - for net in self.config['networks']: - if net['nat_destination']: - return net['name'] - return None - - def get_nat_source(self): - """Get network used for NAT source.""" - for net in self.config['networks']: - if net.get('nat_source'): - return net['name'] - return None diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py new file mode 100644 index 000000000..df838b46d --- /dev/null +++ b/openstack/config/cloud_region.py @@ -0,0 +1,599 @@ +# Copyright (c) 2014 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. + +import importlib +import math +import warnings + +from keystoneauth1 import adapter +import keystoneauth1.exceptions.catalog +from keystoneauth1 import session +import requestsexceptions + +import openstack +from openstack import _log +from openstack.config import constructors +from openstack.config import exceptions + + +def _get_client(service_key): + class_mapping = constructors.get_constructor_mapping() + if service_key not in class_mapping: + raise exceptions.OpenStackConfigException( + "Service {service_key} is unkown. Please pass in a client" + " constructor or submit a patch to os-client-config".format( + service_key=service_key)) + mod_name, ctr_name = class_mapping[service_key].rsplit('.', 1) + lib_name = mod_name.split('.')[0] + try: + mod = importlib.import_module(mod_name) + except ImportError: + raise exceptions.OpenStackConfigException( + "Client for '{service_key}' was requested, but" + " {mod_name} was unable to be imported. Either import" + " the module yourself and pass the constructor in as an argument," + " or perhaps you do not have python-{lib_name} installed.".format( + service_key=service_key, + mod_name=mod_name, + lib_name=lib_name)) + try: + ctr = getattr(mod, ctr_name) + except AttributeError: + raise exceptions.OpenStackConfigException( + "Client for '{service_key}' was requested, but although" + " {mod_name} imported fine, the constructor at {fullname}" + " as not found. Please check your installation, we have no" + " clue what is wrong with your computer.".format( + service_key=service_key, + mod_name=mod_name, + fullname=class_mapping[service_key])) + return ctr + + +def _make_key(key, service_type): + if not service_type: + return key + else: + service_type = service_type.lower().replace('-', '_') + return "_".join([service_type, key]) + + +class CloudRegion(object): + """The configuration for a Region of an OpenStack Cloud. + + A CloudRegion encapsulates the config information needed for connections + to all of the services in a Region of a Cloud. + """ + def __init__(self, name, region, config, + force_ipv4=False, auth_plugin=None, + openstack_config=None, session_constructor=None, + app_name=None, app_version=None): + self.name = name + self.region = region + self.config = config + self.log = _log.setup_logging(__name__) + self._force_ipv4 = force_ipv4 + self._auth = auth_plugin + self._openstack_config = openstack_config + self._keystone_session = None + self._session_constructor = session_constructor or session.Session + self._app_name = app_name + self._app_version = app_version + + def __getattr__(self, key): + """Return arbitrary attributes.""" + + if key.startswith('os_'): + key = key[3:] + + if key in [attr.replace('-', '_') for attr in self.config]: + return self.config[key] + else: + return None + + def __iter__(self): + return self.config.__iter__() + + def __eq__(self, other): + return (self.name == other.name and self.region == other.region + and self.config == other.config) + + def __ne__(self, other): + return not self == other + + def set_session_constructor(self, session_constructor): + """Sets the Session constructor.""" + self._session_constructor = session_constructor + + def get_requests_verify_args(self): + """Return the verify and cert values for the requests library.""" + if self.config['verify'] and self.config['cacert']: + verify = self.config['cacert'] + else: + verify = self.config['verify'] + if self.config['cacert']: + warnings.warn( + "You are specifying a cacert for the cloud {0} but " + "also to ignore the host verification. The host SSL cert " + "will not be verified.".format(self.name)) + + cert = self.config.get('cert', None) + if cert: + if self.config['key']: + cert = (cert, self.config['key']) + return (verify, cert) + + def get_services(self): + """Return a list of service types we know something about.""" + services = [] + for key, val in self.config.items(): + if (key.endswith('api_version') + or key.endswith('service_type') + or key.endswith('service_name')): + services.append("_".join(key.split('_')[:-2])) + return list(set(services)) + + def get_auth_args(self): + return self.config['auth'] + + def get_interface(self, service_type=None): + key = _make_key('interface', service_type) + interface = self.config.get('interface') + return self.config.get(key, interface) + + def get_region_name(self, service_type=None): + if not service_type: + return self.region + key = _make_key('region_name', service_type) + return self.config.get(key, self.region) + + def get_api_version(self, service_type): + key = _make_key('api_version', service_type) + return self.config.get(key, None) + + def get_service_type(self, service_type): + key = _make_key('service_type', service_type) + # Cinder did an evil thing where they defined a second service + # type in the catalog. Of course, that's insane, so let's hide this + # atrocity from the as-yet-unsullied eyes of our users. + # Of course, if the user requests a volumev2, that structure should + # still work. + # What's even more amazing is that they did it AGAIN with cinder v3 + # And then I learned that mistral copied it. + # TODO(shade) This should get removed when we have os-service-types + # alias support landed in keystoneauth. + if service_type in ('volume', 'block-storage'): + vol_ver = self.get_api_version('volume') + if vol_ver and vol_ver.startswith('2'): + service_type = 'volumev2' + elif vol_ver and vol_ver.startswith('3'): + service_type = 'volumev3' + elif service_type == 'workflow': + wk_ver = self.get_api_version(service_type) + if wk_ver and wk_ver.startswith('2'): + service_type = 'workflowv2' + return self.config.get(key, service_type) + + def get_service_name(self, service_type): + key = _make_key('service_name', service_type) + return self.config.get(key, None) + + def get_endpoint(self, service_type): + key = _make_key('endpoint_override', service_type) + old_key = _make_key('endpoint', service_type) + return self.config.get(key, self.config.get(old_key, None)) + + @property + def prefer_ipv6(self): + return not self._force_ipv4 + + @property + def force_ipv4(self): + return self._force_ipv4 + + def get_auth(self): + """Return a keystoneauth plugin from the auth credentials.""" + return self._auth + + def get_session(self): + """Return a keystoneauth session based on the auth credentials.""" + if self._keystone_session is None: + if not self._auth: + raise exceptions.OpenStackConfigException( + "Problem with auth parameters") + (verify, cert) = self.get_requests_verify_args() + # Turn off urllib3 warnings about insecure certs if we have + # explicitly configured requests to tell it we do not want + # cert verification + if not verify: + self.log.debug( + "Turning off SSL warnings for {cloud}:{region}" + " since verify=False".format( + cloud=self.name, region=self.region)) + requestsexceptions.squelch_warnings(insecure_requests=not verify) + self._keystone_session = self._session_constructor( + auth=self._auth, + verify=verify, + cert=cert, + timeout=self.config['api_timeout']) + if hasattr(self._keystone_session, 'additional_user_agent'): + self._keystone_session.additional_user_agent.append( + ('openstacksdk', openstack.__version__)) + # Using old keystoneauth with new os-client-config fails if + # we pass in app_name and app_version. Those are not essential, + # nor a reason to bump our minimum, so just test for the session + # having the attribute post creation and set them then. + if hasattr(self._keystone_session, 'app_name'): + self._keystone_session.app_name = self._app_name + if hasattr(self._keystone_session, 'app_version'): + self._keystone_session.app_version = self._app_version + return self._keystone_session + + def get_service_catalog(self): + """Helper method to grab the service catalog.""" + return self._auth.get_access(self.get_session()).service_catalog + + def _get_version_args(self, service_key, version): + """Translate OCC version args to those needed by ksa adapter. + + If no version is requested explicitly and we have a configured version, + set the version parameter and let ksa deal with expanding that to + min=ver.0, max=ver.latest. + + If version is set, pass it through. + + If version is not set and we don't have a configured version, default + to latest. + """ + if version == 'latest': + return None, None, 'latest' + if not version: + version = self.get_api_version(service_key) + if not version: + return None, None, 'latest' + return version, None, None + + def get_session_client(self, service_key, version=None): + """Return a prepped requests adapter for a given service. + + This is useful for making direct requests calls against a + 'mounted' endpoint. That is, if you do: + + client = get_session_client('compute') + + then you can do: + + client.get('/flavors') + + and it will work like you think. + """ + (version, min_version, max_version) = self._get_version_args( + service_key, version) + + return adapter.Adapter( + session=self.get_session(), + service_type=self.get_service_type(service_key), + service_name=self.get_service_name(service_key), + interface=self.get_interface(service_key), + region_name=self.get_region_name(service_key), + version=version, + min_version=min_version, + max_version=max_version) + + def _get_highest_endpoint(self, service_types, kwargs): + session = self.get_session() + for service_type in service_types: + kwargs['service_type'] = service_type + try: + # Return the highest version we find that matches + # the request + return session.get_endpoint(**kwargs) + except keystoneauth1.exceptions.catalog.EndpointNotFound: + pass + + def get_session_endpoint( + self, service_key, min_version=None, max_version=None): + """Return the endpoint from config or the catalog. + + If a configuration lists an explicit endpoint for a service, + return that. Otherwise, fetch the service catalog from the + keystone session and return the appropriate endpoint. + + :param service_key: Generic key for service, such as 'compute' or + 'network' + + """ + + override_endpoint = self.get_endpoint(service_key) + if override_endpoint: + return override_endpoint + endpoint = None + kwargs = { + 'service_name': self.get_service_name(service_key), + 'region_name': self.region + } + kwargs['interface'] = self.get_interface(service_key) + if service_key == 'volume' and not self.get_api_version('volume'): + # If we don't have a configured cinder version, we can't know + # to request a different service_type + min_version = float(min_version or 1) + max_version = float(max_version or 3) + min_major = math.trunc(float(min_version)) + max_major = math.trunc(float(max_version)) + versions = range(int(max_major) + 1, int(min_major), -1) + service_types = [] + for version in versions: + if version == 1: + service_types.append('volume') + else: + service_types.append('volumev{v}'.format(v=version)) + else: + service_types = [self.get_service_type(service_key)] + endpoint = self._get_highest_endpoint(service_types, kwargs) + if not endpoint: + self.log.warning( + "Keystone catalog entry not found (" + "service_type=%s,service_name=%s" + "interface=%s,region_name=%s)", + service_key, + kwargs['service_name'], + kwargs['interface'], + kwargs['region_name']) + return endpoint + + def get_legacy_client( + self, service_key, client_class=None, interface_key=None, + pass_version_arg=True, version=None, min_version=None, + max_version=None, **kwargs): + """Return a legacy OpenStack client object for the given config. + + Most of the OpenStack python-*client libraries have the same + interface for their client constructors, but there are several + parameters one wants to pass given a :class:`CloudRegion` object. + + In the future, OpenStack API consumption should be done through + the OpenStack SDK, but that's not ready yet. This is for getting + Client objects from python-*client only. + + :param service_key: Generic key for service, such as 'compute' or + 'network' + :param client_class: Class of the client to be instantiated. This + should be the unversioned version if there + is one, such as novaclient.client.Client, or + the versioned one, such as + neutronclient.v2_0.client.Client if there isn't + :param interface_key: (optional) Some clients, such as glanceclient + only accept the parameter 'interface' instead + of 'endpoint_type' - this is a get-out-of-jail + parameter for those until they can be aligned. + os-client-config understands this to be the + case if service_key is image, so this is really + only for use with other unknown broken clients. + :param pass_version_arg: (optional) If a versioned Client constructor + was passed to client_class, set this to + False, which will tell get_client to not + pass a version parameter. os-client-config + already understand that this is the + case for network, so it can be omitted in + that case. + :param version: (optional) Version string to override the configured + version string. + :param min_version: (options) Minimum version acceptable. + :param max_version: (options) Maximum version acceptable. + :param kwargs: (optional) keyword args are passed through to the + Client constructor, so this is in case anything + additional needs to be passed in. + """ + if not client_class: + client_class = _get_client(service_key) + + interface = self.get_interface(service_key) + # trigger exception on lack of service + endpoint = self.get_session_endpoint( + service_key, min_version=min_version, max_version=max_version) + endpoint_override = self.get_endpoint(service_key) + + if service_key == 'object-store': + constructor_kwargs = dict( + session=self.get_session(), + os_options=dict( + service_type=self.get_service_type(service_key), + object_storage_url=endpoint_override, + region_name=self.region)) + else: + constructor_kwargs = dict( + session=self.get_session(), + service_name=self.get_service_name(service_key), + service_type=self.get_service_type(service_key), + endpoint_override=endpoint_override, + region_name=self.region) + + if service_key == 'image': + # os-client-config does not depend on glanceclient, but if + # the user passed in glanceclient.client.Client, which they + # would need to do if they were requesting 'image' - then + # they necessarily have glanceclient installed + from glanceclient.common import utils as glance_utils + endpoint, detected_version = glance_utils.strip_version(endpoint) + # If the user has passed in a version, that's explicit, use it + if not version: + version = detected_version + # If the user has passed in or configured an override, use it. + # Otherwise, ALWAYS pass in an endpoint_override becuase + # we've already done version stripping, so we don't want version + # reconstruction to happen twice + if not endpoint_override: + constructor_kwargs['endpoint_override'] = endpoint + constructor_kwargs.update(kwargs) + if pass_version_arg and service_key != 'object-store': + if not version: + version = self.get_api_version(service_key) + if not version and service_key == 'volume': + from cinderclient import client as cinder_client + version = cinder_client.get_volume_api_from_url(endpoint) + # Temporary workaround while we wait for python-openstackclient + # to be able to handle 2.0 which is what neutronclient expects + if service_key == 'network' and version == '2': + version = '2.0' + if service_key == 'identity': + # Workaround for bug#1513839 + if 'endpoint' not in constructor_kwargs: + endpoint = self.get_session_endpoint('identity') + constructor_kwargs['endpoint'] = endpoint + if service_key == 'network': + constructor_kwargs['api_version'] = version + elif service_key == 'baremetal': + if version != '1': + # Set Ironic Microversion + constructor_kwargs['os_ironic_api_version'] = version + # Version arg is the major version, not the full microstring + constructor_kwargs['version'] = version[0] + else: + constructor_kwargs['version'] = version + if min_version and min_version > float(version): + raise exceptions.OpenStackConfigVersionException( + "Minimum version {min_version} requested but {version}" + " found".format(min_version=min_version, version=version), + version=version) + if max_version and max_version < float(version): + raise exceptions.OpenStackConfigVersionException( + "Maximum version {max_version} requested but {version}" + " found".format(max_version=max_version, version=version), + version=version) + if service_key == 'database': + # TODO(mordred) Remove when https://review.openstack.org/314032 + # has landed and released. We're passing in a Session, but the + # trove Client object has username and password as required + # args + constructor_kwargs['username'] = None + constructor_kwargs['password'] = None + + if not interface_key: + if service_key in ('image', 'key-manager'): + interface_key = 'interface' + elif (service_key == 'identity' + and version and version.startswith('3')): + interface_key = 'interface' + else: + interface_key = 'endpoint_type' + if service_key == 'object-store': + constructor_kwargs['os_options'][interface_key] = interface + else: + constructor_kwargs[interface_key] = interface + + return client_class(**constructor_kwargs) + + def get_cache_expiration_time(self): + if self._openstack_config: + return self._openstack_config.get_cache_expiration_time() + + def get_cache_path(self): + if self._openstack_config: + return self._openstack_config.get_cache_path() + + def get_cache_class(self): + if self._openstack_config: + return self._openstack_config.get_cache_class() + + def get_cache_arguments(self): + if self._openstack_config: + return self._openstack_config.get_cache_arguments() + + def get_cache_expiration(self): + if self._openstack_config: + return self._openstack_config.get_cache_expiration() + + def get_cache_resource_expiration(self, resource, default=None): + """Get expiration time for a resource + + :param resource: Name of the resource type + :param default: Default value to return if not found (optional, + defaults to None) + + :returns: Expiration time for the resource type as float or default + """ + if self._openstack_config: + expiration = self._openstack_config.get_cache_expiration() + if resource not in expiration: + return default + return float(expiration[resource]) + + def requires_floating_ip(self): + """Return whether or not this cloud requires floating ips. + + + :returns: True of False if know, None if discovery is needed. + If requires_floating_ip is not configured but the cloud is + known to not provide floating ips, will return False. + """ + if self.config['floating_ip_source'] == "None": + return False + return self.config.get('requires_floating_ip') + + def get_external_networks(self): + """Get list of network names for external networks.""" + return [ + net['name'] for net in self.config['networks'] + if net['routes_externally']] + + def get_external_ipv4_networks(self): + """Get list of network names for external IPv4 networks.""" + return [ + net['name'] for net in self.config['networks'] + if net['routes_ipv4_externally']] + + def get_external_ipv6_networks(self): + """Get list of network names for external IPv6 networks.""" + return [ + net['name'] for net in self.config['networks'] + if net['routes_ipv6_externally']] + + def get_internal_networks(self): + """Get list of network names for internal networks.""" + return [ + net['name'] for net in self.config['networks'] + if not net['routes_externally']] + + def get_internal_ipv4_networks(self): + """Get list of network names for internal IPv4 networks.""" + return [ + net['name'] for net in self.config['networks'] + if not net['routes_ipv4_externally']] + + def get_internal_ipv6_networks(self): + """Get list of network names for internal IPv6 networks.""" + return [ + net['name'] for net in self.config['networks'] + if not net['routes_ipv6_externally']] + + def get_default_network(self): + """Get network used for default interactions.""" + for net in self.config['networks']: + if net['default_interface']: + return net['name'] + return None + + def get_nat_destination(self): + """Get network used for NAT destination.""" + for net in self.config['networks']: + if net['nat_destination']: + return net['name'] + return None + + def get_nat_source(self): + """Get network used for NAT source.""" + for net in self.config['networks']: + if net.get('nat_source'): + return net['name'] + return None diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 2c186fe82..304bfc41a 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -29,7 +29,7 @@ import yaml from openstack import _log -from openstack.config import cloud_config +from openstack.config import cloud_region from openstack.config import defaults from openstack.config import exceptions from openstack.config import vendors @@ -707,7 +707,7 @@ def register_argparse_arguments(self, parser, argv, service_keys=None): # for from the user passing it explicitly. We'll stash it for later local_parser.add_argument('--timeout', metavar='') - # We need for get_one_cloud to be able to peek at whether a token + # We need for get_one to be able to peek at whether a token # was passed so that we can swap the default from password to # token if it was. And we need to also peek for --os-auth-token # for novaclient backwards compat @@ -729,8 +729,8 @@ def register_argparse_arguments(self, parser, argv, service_keys=None): # the rest of the arguments given are invalid for the plugin # chosen (for instance, --help may be requested, so that the # user can see what options he may want to give - cloud = self.get_one_cloud(argparse=options, validate=False) - default_auth_type = cloud.config['auth_type'] + cloud_region = self.get_one(argparse=options, validate=False) + default_auth_type = cloud_region.config['auth_type'] try: loading.register_auth_argparse_arguments( @@ -802,16 +802,18 @@ def _fix_backwards_api_timeout(self, cloud): new_cloud['api_timeout'] = new_cloud.pop('timeout') return new_cloud - def get_all_clouds(self): + def get_all(self): clouds = [] for cloud in self.get_cloud_names(): for region in self._get_regions(cloud): if region: - clouds.append(self.get_one_cloud( + clouds.append(self.get_one( cloud, region_name=region['name'])) return clouds + # TODO(mordred) Backwards compat for OSC transition + get_all_clouds = get_all def _fix_args(self, args=None, argparse=None): """Massage the passed-in options @@ -1022,9 +1024,9 @@ def magic_fixes(self, config): return config - def get_one_cloud(self, cloud=None, validate=True, - argparse=None, **kwargs): - """Retrieve a single cloud configuration and merge additional options + def get_one( + self, cloud=None, validate=True, argparse=None, **kwargs): + """Retrieve a single CloudRegion and merge additional options :param string cloud: The name of the configuration to load from clouds.yaml @@ -1038,6 +1040,7 @@ def get_one_cloud(self, cloud=None, validate=True, :param region_name: Name of the region of the cloud. :param kwargs: Additional configuration options + :returns: openstack.config.cloud_region.CloudRegion :raises: keystoneauth1.exceptions.MissingRequiredOptions on missing required auth parameters """ @@ -1101,7 +1104,7 @@ def get_one_cloud(self, cloud=None, validate=True, cloud_name = '' else: cloud_name = str(cloud) - return cloud_config.CloudConfig( + return cloud_region.CloudRegion( name=cloud_name, region=config['region_name'], config=config, @@ -1112,6 +1115,8 @@ def get_one_cloud(self, cloud=None, validate=True, app_name=self._app_name, app_version=self._app_version, ) + # TODO(mordred) Backwards compat for OSC transition + get_one_cloud = get_one def get_one_cloud_osc( self, @@ -1120,7 +1125,7 @@ def get_one_cloud_osc( argparse=None, **kwargs ): - """Retrieve a single cloud configuration and merge additional options + """Retrieve a single CloudRegion and merge additional options :param string cloud: The name of the configuration to load from clouds.yaml @@ -1196,7 +1201,7 @@ def get_one_cloud_osc( cloud_name = '' else: cloud_name = str(cloud) - return cloud_config.CloudConfig( + return cloud_region.CloudRegion( name=cloud_name, region=config['region_name'], config=self._normalize_keys(config), diff --git a/openstack/connection.py b/openstack/connection.py index 1e476682d..4a59b3541 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -31,13 +31,13 @@ conn = connection.Connection(cloud='example', region_name='earth1') -If you already have an :class:`~openstack.config.cloud_config.CloudConfig` +If you already have an :class:`~openstack.config.cloud_region.CloudRegion` you can pass it in instead.:: from openstack import connection import openstack.config - config = openstack.config.OpenStackConfig.get_one_cloud( + config = openstack.config.OpenStackConfig.get_one( cloud='example', region_name='earth') conn = connection.Connection(config=config) @@ -80,9 +80,10 @@ import keystoneauth1.exceptions import os_service_types +from six.moves import urllib import openstack.config -from openstack.config import cloud_config +from openstack.config import cloud_region from openstack import exceptions from openstack import proxy from openstack import proxy2 @@ -92,36 +93,36 @@ _logger = logging.getLogger(__name__) -def from_config(cloud_name=None, cloud_config=None, options=None): +def from_config(cloud=None, config=None, options=None, **kwargs): """Create a Connection using openstack.config - :param str cloud_name: Use the `cloud_name` configuration details when - creating the Connection instance. - :param cloud_config: An instance of - `openstack.config.loader.OpenStackConfig` - as returned from openstack.config. - If no `config` is provided, - `openstack.config.OpenStackConfig` will be called, - and the provided `cloud_name` will be used in - determining which cloud's configuration details - will be used in creation of the - `Connection` instance. - :param options: A namespace object; allows direct passing in of options to - be added to the cloud config. This does not have to be an - instance of argparse.Namespace, despite the naming of the - the `openstack.config.loader.OpenStackConfig.get_one_cloud` - argument to which it is passed. + :param str cloud: + Use the `cloud` configuration details when creating the Connection. + :param openstack.config.cloud_region.CloudRegion config: + An existing CloudRegion configuration. If no `config` is provided, + `openstack.config.OpenStackConfig` will be called, and the provided + `name` will be used in determining which cloud's configuration + details will be used in creation of the `Connection` instance. + :param argparse.Namespace options: + Allows direct passing in of options to be added to the cloud config. + This does not have to be an actual instance of argparse.Namespace, + despite the naming of the the + `openstack.config.loader.OpenStackConfig.get_one` argument to which + it is passed. :rtype: :class:`~openstack.connection.Connection` """ - if cloud_config is None: - occ = openstack.config.OpenStackConfig() - cloud_config = occ.get_one_cloud(cloud=cloud_name, argparse=options) - - if cloud_config.debug: + # TODO(mordred) Backwards compat while we transition + cloud = cloud or kwargs.get('cloud_name') + config = config or kwargs.get('cloud_config') + if config is None: + config = openstack.config.OpenStackConfig().get_one( + cloud=cloud, argparse=options) + + if config.debug: utils.enable_logging(True, stream=sys.stdout) - return Connection(config=cloud_config) + return Connection(config=config) class Connection(object): @@ -142,18 +143,18 @@ def __init__(self, cloud=None, config=None, session=None, name ``envvars`` may be used to consume a cloud configured via ``OS_`` environment variables. - A pre-existing :class:`~openstack.config.cloud_config.CloudConfig` + A pre-existing :class:`~openstack.config.cloud_region.CloudRegion` object can be passed in lieu of a cloud name, for cases where the user - already has a fully formed CloudConfig and just wants to use it. + already has a fully formed CloudRegion and just wants to use it. Similarly, if for some reason the user already has a :class:`~keystoneauth1.session.Session` and wants to use it, it may be passed in. :param str cloud: Name of the cloud from config to use. - :param config: CloudConfig object representing the config for the + :param config: CloudRegion object representing the config for the region of the cloud in question. - :type config: :class:`~openstack.config.cloud_config.CloudConfig` + :type config: :class:`~openstack.config.cloud_region.CloudRegion` :param session: A session object compatible with :class:`~keystoneauth1.session.Session`. :type session: :class:`~keystoneauth1.session.Session` @@ -168,22 +169,22 @@ def __init__(self, cloud=None, config=None, session=None, transition. :param kwargs: If a config is not provided, the rest of the parameters provided are assumed to be arguments to be passed to the - CloudConfig contructor. + CloudRegion contructor. """ self.config = config self.service_type_manager = os_service_types.ServiceTypes() if not self.config: - openstack_config = openstack.config.OpenStackConfig( - app_name=app_name, app_version=app_version, - load_yaml_config=profile is None) if profile: # TODO(shade) Remove this once we've shifted # python-openstackclient to not use the profile interface. self.config = self._get_config_from_profile( - openstack_config, profile, authenticator, **kwargs) + profile, authenticator, **kwargs) else: - self.config = openstack_config.get_one_cloud( + openstack_config = openstack.config.OpenStackConfig( + app_name=app_name, app_version=app_version, + load_yaml_config=profile is None) + self.config = openstack_config.get_one( cloud=cloud, validate=session is None, **kwargs) self.task_manager = task_manager.TaskManager( @@ -197,30 +198,32 @@ def __init__(self, cloud=None, config=None, session=None, self._open() - def _get_config_from_profile( - self, openstack_config, profile, authenticator, **kwargs): + def _get_config_from_profile(self, profile, authenticator, **kwargs): """Get openstack.config objects from legacy profile.""" # TODO(shade) Remove this once we've shifted python-openstackclient # to not use the profile interface. - config = openstack_config.get_one_cloud( - cloud='defaults', validate=False, **kwargs) - config._auth = authenticator + # We don't have a cloud name. Make one up from the auth_url hostname + # so that log messages work. + name = urllib.parse.urlparse(authenticator.auth_url).hostname + region_name = None for service in profile.get_services(): + if service.region: + region_name = service.region service_type = service.service_type if service.interface: - key = cloud_config._make_key('interface', service_type) - config.config[key] = service.interface - if service.region: - key = cloud_config._make_key('region_name', service_type) - config.config[key] = service.region + key = cloud_region._make_key('interface', service_type) + kwargs[key] = service.interface if service.version: version = service.version if version.startswith('v'): version = version[1:] - key = cloud_config._make_key('api_version', service_type) - config.config[key] = service.version - return config + key = cloud_region._make_key('api_version', service_type) + kwargs[key] = service.version + + config = cloud_region.CloudRegion( + name=name, region=region_name, config=kwargs) + config._auth = authenticator def _open(self): """Open the connection. """ diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index 77c76b35e..3614f34d2 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -22,24 +22,16 @@ #: file, typically in $HOME/.config/openstack/clouds.yaml. That configuration #: will determine where the functional tests will be run and what resource #: defaults will be used to run the functional tests. -TEST_CLOUD = os.getenv('OS_CLOUD', 'devstack-admin') - - -class Opts(object): - def __init__(self, cloud_name='devstack-admin', debug=False): - self.cloud = cloud_name - self.debug = debug +TEST_CLOUD_NAME = os.getenv('OS_CLOUD', 'devstack-admin') +TEST_CLOUD_REGION = openstack.config.get_config(cloud=TEST_CLOUD_NAME) def _get_resource_value(resource_key, default): try: - return cloud.config['functional'][resource_key] + return TEST_CLOUD_REGION.config['functional'][resource_key] except KeyError: return default -opts = Opts(cloud_name=TEST_CLOUD) -occ = openstack.config.OpenStackConfig() -cloud = occ.get_one_cloud(opts.cloud, argparse=opts) IMAGE_NAME = _get_resource_value('image_name', 'cirros-0.3.5-x86_64-disk') FLAVOR_NAME = _get_resource_value('flavor_name', 'm1.small') @@ -49,7 +41,7 @@ class BaseFunctionalTest(base.TestCase): def setUp(self): super(BaseFunctionalTest, self).setUp() - self.conn = connection.from_config(cloud_name=TEST_CLOUD) + self.conn = connection.Connection(config=TEST_CLOUD_REGION) def addEmptyCleanup(self, func, *args, **kwargs): def cleanup(): diff --git a/openstack/tests/functional/cloud/base.py b/openstack/tests/functional/cloud/base.py index 855d5557a..ccaabc6cd 100644 --- a/openstack/tests/functional/cloud/base.py +++ b/openstack/tests/functional/cloud/base.py @@ -36,14 +36,14 @@ def setUp(self): self.operator_cloud.cloud_config.get_api_version('identity') def _set_user_cloud(self, **kwargs): - user_config = self.config.get_one_cloud( + user_config = self.config.get_one( cloud=self._demo_name, **kwargs) self.user_cloud = openstack.OpenStackCloud( cloud_config=user_config, log_inner_exceptions=True) def _set_operator_cloud(self, **kwargs): - operator_config = self.config.get_one_cloud( + operator_config = self.config.get_one( cloud=self._op_name, **kwargs) self.operator_cloud = openstack.OperatorCloud( cloud_config=operator_config, diff --git a/openstack/tests/functional/image/v2/test_image.py b/openstack/tests/functional/image/v2/test_image.py index 54e4f6662..8b3005b5c 100644 --- a/openstack/tests/functional/image/v2/test_image.py +++ b/openstack/tests/functional/image/v2/test_image.py @@ -26,7 +26,7 @@ def setUp(self): super(TestImage, self).setUp() opts = self.ImageOpts() self.conn = connection.from_config( - cloud_name=base.TEST_CLOUD, options=opts) + cloud_name=base.TEST_CLOUD_NAME, options=opts) self.img = self.conn.image.upload_image( name=TEST_IMAGE_NAME, diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 676c4eac8..d2caed078 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -117,7 +117,7 @@ def _nosleep(seconds): config_files=[config.name], vendor_files=[vendor.name], secure_files=['non-existant']) - self.cloud_config = self.config.get_one_cloud( + self.cloud_config = self.config.get_one( cloud=test_cloud, validate=False) self.cloud = openstack.OpenStackCloud( cloud_config=self.cloud_config, @@ -141,7 +141,7 @@ def setUp(self, cloud_config_fixture='clouds.yaml'): super(TestCase, self).setUp(cloud_config_fixture=cloud_config_fixture) self.session_fixture = self.useFixture(fixtures.MonkeyPatch( - 'openstack.config.cloud_config.CloudConfig.get_session', + 'openstack.config.cloud_region.CloudRegion.get_session', mock.Mock())) @@ -461,7 +461,7 @@ def use_keystone_v2(self): def _make_test_cloud(self, cloud_name='_test_cloud_', **kwargs): test_cloud = os.environ.get('OPENSTACKSDK_OS_CLOUD', cloud_name) - self.cloud_config = self.config.get_one_cloud( + self.cloud_config = self.config.get_one( cloud=test_cloud, validate=True, **kwargs) self.conn = openstack.connection.Connection( config=self.cloud_config) diff --git a/openstack/tests/unit/cloud/test_inventory.py b/openstack/tests/unit/cloud/test_inventory.py index 621d97e7a..b06be882f 100644 --- a/openstack/tests/unit/cloud/test_inventory.py +++ b/openstack/tests/unit/cloud/test_inventory.py @@ -28,7 +28,7 @@ def setUp(self): @mock.patch("openstack.config.loader.OpenStackConfig") @mock.patch("openstack.OpenStackCloud") def test__init(self, mock_cloud, mock_config): - mock_config.return_value.get_all_clouds.return_value = [{}] + mock_config.return_value.get_all.return_value = [{}] inv = inventory.OpenStackInventory() @@ -37,12 +37,12 @@ def test__init(self, mock_cloud, mock_config): ) self.assertIsInstance(inv.clouds, list) self.assertEqual(1, len(inv.clouds)) - self.assertTrue(mock_config.return_value.get_all_clouds.called) + self.assertTrue(mock_config.return_value.get_all.called) @mock.patch("openstack.config.loader.OpenStackConfig") @mock.patch("openstack.OpenStackCloud") def test__init_one_cloud(self, mock_cloud, mock_config): - mock_config.return_value.get_one_cloud.return_value = [{}] + mock_config.return_value.get_one.return_value = [{}] inv = inventory.OpenStackInventory(cloud='supercloud') @@ -51,8 +51,8 @@ def test__init_one_cloud(self, mock_cloud, mock_config): ) self.assertIsInstance(inv.clouds, list) self.assertEqual(1, len(inv.clouds)) - self.assertFalse(mock_config.return_value.get_all_clouds.called) - mock_config.return_value.get_one_cloud.assert_called_once_with( + self.assertFalse(mock_config.return_value.get_all.called) + mock_config.return_value.get_one.assert_called_once_with( 'supercloud') @mock.patch("openstack.config.loader.OpenStackConfig") @@ -62,19 +62,19 @@ def test__raise_exception_on_no_cloud(self, mock_cloud, mock_config): Test that when os-client-config can't find a named cloud, a shade exception is emitted. """ - mock_config.return_value.get_one_cloud.side_effect = ( + mock_config.return_value.get_one.side_effect = ( occ_exc.OpenStackConfigException() ) self.assertRaises(exc.OpenStackCloudException, inventory.OpenStackInventory, cloud='supercloud') - mock_config.return_value.get_one_cloud.assert_called_once_with( + mock_config.return_value.get_one.assert_called_once_with( 'supercloud') @mock.patch("openstack.config.loader.OpenStackConfig") @mock.patch("openstack.OpenStackCloud") def test_list_hosts(self, mock_cloud, mock_config): - mock_config.return_value.get_all_clouds.return_value = [{}] + mock_config.return_value.get_all.return_value = [{}] inv = inventory.OpenStackInventory() @@ -93,7 +93,7 @@ def test_list_hosts(self, mock_cloud, mock_config): @mock.patch("openstack.config.loader.OpenStackConfig") @mock.patch("openstack.OpenStackCloud") def test_list_hosts_no_detail(self, mock_cloud, mock_config): - mock_config.return_value.get_all_clouds.return_value = [{}] + mock_config.return_value.get_all.return_value = [{}] inv = inventory.OpenStackInventory() @@ -112,7 +112,7 @@ def test_list_hosts_no_detail(self, mock_cloud, mock_config): @mock.patch("openstack.config.loader.OpenStackConfig") @mock.patch("openstack.OpenStackCloud") def test_search_hosts(self, mock_cloud, mock_config): - mock_config.return_value.get_all_clouds.return_value = [{}] + mock_config.return_value.get_all.return_value = [{}] inv = inventory.OpenStackInventory() @@ -128,7 +128,7 @@ def test_search_hosts(self, mock_cloud, mock_config): @mock.patch("openstack.config.loader.OpenStackConfig") @mock.patch("openstack.OpenStackCloud") def test_get_host(self, mock_cloud, mock_config): - mock_config.return_value.get_all_clouds.return_value = [{}] + mock_config.return_value.get_all.return_value = [{}] inv = inventory.OpenStackInventory() diff --git a/openstack/tests/unit/cloud/test_operator.py b/openstack/tests/unit/cloud/test_operator.py index 3d2bc3646..a9a5d0e45 100644 --- a/openstack/tests/unit/cloud/test_operator.py +++ b/openstack/tests/unit/cloud/test_operator.py @@ -15,7 +15,7 @@ import openstack from openstack.cloud import exc -from openstack.config import cloud_config +from openstack.config import cloud_region from openstack.tests import fakes from openstack.tests.unit import base @@ -25,13 +25,13 @@ class TestOperatorCloud(base.RequestsMockTestCase): def test_operator_cloud(self): self.assertIsInstance(self.op_cloud, openstack.OperatorCloud) - @mock.patch.object(cloud_config.CloudConfig, 'get_endpoint') + @mock.patch.object(cloud_region.CloudRegion, 'get_endpoint') def test_get_session_endpoint_provided(self, fake_get_endpoint): fake_get_endpoint.return_value = 'http://fake.url' self.assertEqual( 'http://fake.url', self.op_cloud.get_session_endpoint('image')) - @mock.patch.object(cloud_config.CloudConfig, 'get_session') + @mock.patch.object(cloud_region.CloudRegion, 'get_session') def test_get_session_endpoint_session(self, get_session_mock): session_mock = mock.Mock() session_mock.get_endpoint.return_value = 'http://fake.url' @@ -39,7 +39,7 @@ def test_get_session_endpoint_session(self, get_session_mock): self.assertEqual( 'http://fake.url', self.op_cloud.get_session_endpoint('image')) - @mock.patch.object(cloud_config.CloudConfig, 'get_session') + @mock.patch.object(cloud_region.CloudRegion, 'get_session') def test_get_session_endpoint_exception(self, get_session_mock): class FakeException(Exception): pass @@ -57,7 +57,7 @@ def side_effect(*args, **kwargs): " No service"): self.op_cloud.get_session_endpoint("image") - @mock.patch.object(cloud_config.CloudConfig, 'get_session') + @mock.patch.object(cloud_region.CloudRegion, 'get_session') def test_get_session_endpoint_unavailable(self, get_session_mock): session_mock = mock.Mock() session_mock.get_endpoint.return_value = None @@ -65,7 +65,7 @@ def test_get_session_endpoint_unavailable(self, get_session_mock): image_endpoint = self.op_cloud.get_session_endpoint("image") self.assertIsNone(image_endpoint) - @mock.patch.object(cloud_config.CloudConfig, 'get_session') + @mock.patch.object(cloud_region.CloudRegion, 'get_session') def test_get_session_endpoint_identity(self, get_session_mock): session_mock = mock.Mock() get_session_mock.return_value = session_mock @@ -76,14 +76,14 @@ def test_get_session_endpoint_identity(self, get_session_mock): session_mock.get_endpoint.assert_called_with(**kwargs) - @mock.patch.object(cloud_config.CloudConfig, 'get_session') + @mock.patch.object(cloud_region.CloudRegion, 'get_session') def test_has_service_no(self, get_session_mock): session_mock = mock.Mock() session_mock.get_endpoint.return_value = None get_session_mock.return_value = session_mock self.assertFalse(self.op_cloud.has_service("image")) - @mock.patch.object(cloud_config.CloudConfig, 'get_session') + @mock.patch.object(cloud_region.CloudRegion, 'get_session') def test_has_service_yes(self, get_session_mock): session_mock = mock.Mock() session_mock.get_endpoint.return_value = 'http://fake.url' diff --git a/openstack/tests/unit/config/base.py b/openstack/tests/unit/config/base.py index fab573d16..1a5fd4850 100644 --- a/openstack/tests/unit/config/base.py +++ b/openstack/tests/unit/config/base.py @@ -20,7 +20,7 @@ import os import tempfile -from openstack.config import cloud_config +from openstack.config import cloud_region import extras import fixtures @@ -227,7 +227,7 @@ def setUp(self): self.useFixture(fixtures.EnvironmentVariable(env)) def _assert_cloud_details(self, cc): - self.assertIsInstance(cc, cloud_config.CloudConfig) + self.assertIsInstance(cc, cloud_region.CloudRegion) self.assertTrue(extras.safe_hasattr(cc, 'auth')) self.assertIsInstance(cc.auth, dict) self.assertIsNone(cc.cloud) diff --git a/openstack/tests/unit/config/test_cloud_config.py b/openstack/tests/unit/config/test_cloud_config.py index 696ca7993..3f51093ac 100644 --- a/openstack/tests/unit/config/test_cloud_config.py +++ b/openstack/tests/unit/config/test_cloud_config.py @@ -16,7 +16,7 @@ from keystoneauth1 import session as ksa_session import mock -from openstack.config import cloud_config +from openstack.config import cloud_region from openstack.config import defaults from openstack.config import exceptions from openstack.tests.unit.config import base @@ -37,10 +37,10 @@ } -class TestCloudConfig(base.TestCase): +class TestCloudRegion(base.TestCase): def test_arbitrary_attributes(self): - cc = cloud_config.CloudConfig("test1", "region-al", fake_config_dict) + cc = cloud_region.CloudRegion("test1", "region-al", fake_config_dict) self.assertEqual("test1", cc.name) self.assertEqual("region-al", cc.region) @@ -61,25 +61,25 @@ def test_arbitrary_attributes(self): self.assertFalse(cc.force_ipv4) def test_iteration(self): - cc = cloud_config.CloudConfig("test1", "region-al", fake_config_dict) + cc = cloud_region.CloudRegion("test1", "region-al", fake_config_dict) self.assertTrue('a' in cc) self.assertFalse('x' in cc) def test_equality(self): - cc1 = cloud_config.CloudConfig("test1", "region-al", fake_config_dict) - cc2 = cloud_config.CloudConfig("test1", "region-al", fake_config_dict) + cc1 = cloud_region.CloudRegion("test1", "region-al", fake_config_dict) + cc2 = cloud_region.CloudRegion("test1", "region-al", fake_config_dict) self.assertEqual(cc1, cc2) def test_inequality(self): - cc1 = cloud_config.CloudConfig("test1", "region-al", fake_config_dict) + cc1 = cloud_region.CloudRegion("test1", "region-al", fake_config_dict) - cc2 = cloud_config.CloudConfig("test2", "region-al", fake_config_dict) + cc2 = cloud_region.CloudRegion("test2", "region-al", fake_config_dict) self.assertNotEqual(cc1, cc2) - cc2 = cloud_config.CloudConfig("test1", "region-xx", fake_config_dict) + cc2 = cloud_region.CloudRegion("test1", "region-xx", fake_config_dict) self.assertNotEqual(cc1, cc2) - cc2 = cloud_config.CloudConfig("test1", "region-al", {}) + cc2 = cloud_region.CloudRegion("test1", "region-al", {}) self.assertNotEqual(cc1, cc2) def test_verify(self): @@ -87,12 +87,12 @@ def test_verify(self): config_dict['cacert'] = None config_dict['verify'] = False - cc = cloud_config.CloudConfig("test1", "region-xx", config_dict) + cc = cloud_region.CloudRegion("test1", "region-xx", config_dict) (verify, cert) = cc.get_requests_verify_args() self.assertFalse(verify) config_dict['verify'] = True - cc = cloud_config.CloudConfig("test1", "region-xx", config_dict) + cc = cloud_region.CloudRegion("test1", "region-xx", config_dict) (verify, cert) = cc.get_requests_verify_args() self.assertTrue(verify) @@ -101,12 +101,12 @@ def test_verify_cacert(self): config_dict['cacert'] = "certfile" config_dict['verify'] = False - cc = cloud_config.CloudConfig("test1", "region-xx", config_dict) + cc = cloud_region.CloudRegion("test1", "region-xx", config_dict) (verify, cert) = cc.get_requests_verify_args() self.assertFalse(verify) config_dict['verify'] = True - cc = cloud_config.CloudConfig("test1", "region-xx", config_dict) + cc = cloud_region.CloudRegion("test1", "region-xx", config_dict) (verify, cert) = cc.get_requests_verify_args() self.assertEqual("certfile", verify) @@ -118,17 +118,17 @@ def test_cert_with_key(self): config_dict['cert'] = 'cert' config_dict['key'] = 'key' - cc = cloud_config.CloudConfig("test1", "region-xx", config_dict) + cc = cloud_region.CloudRegion("test1", "region-xx", config_dict) (verify, cert) = cc.get_requests_verify_args() self.assertEqual(("cert", "key"), cert) def test_ipv6(self): - cc = cloud_config.CloudConfig( + cc = cloud_region.CloudRegion( "test1", "region-al", fake_config_dict, force_ipv4=True) self.assertTrue(cc.force_ipv4) def test_getters(self): - cc = cloud_config.CloudConfig("test1", "region-al", fake_services_dict) + cc = cloud_region.CloudRegion("test1", "region-al", fake_services_dict) self.assertEqual(['compute', 'identity', 'image', 'volume'], sorted(cc.get_services())) @@ -153,23 +153,23 @@ def test_getters(self): self.assertEqual('locks', cc.get_service_name('identity')) def test_volume_override(self): - cc = cloud_config.CloudConfig("test1", "region-al", fake_services_dict) + cc = cloud_region.CloudRegion("test1", "region-al", fake_services_dict) cc.config['volume_api_version'] = '2' self.assertEqual('volumev2', cc.get_service_type('volume')) def test_volume_override_v3(self): - cc = cloud_config.CloudConfig("test1", "region-al", fake_services_dict) + cc = cloud_region.CloudRegion("test1", "region-al", fake_services_dict) cc.config['volume_api_version'] = '3' self.assertEqual('volumev3', cc.get_service_type('volume')) def test_workflow_override_v2(self): - cc = cloud_config.CloudConfig("test1", "region-al", fake_services_dict) + cc = cloud_region.CloudRegion("test1", "region-al", fake_services_dict) cc.config['workflow_api_version'] = '2' self.assertEqual('workflowv2', cc.get_service_type('workflow')) def test_no_override(self): """Test no override happens when defaults are not configured""" - cc = cloud_config.CloudConfig("test1", "region-al", fake_services_dict) + cc = cloud_region.CloudRegion("test1", "region-al", fake_services_dict) self.assertEqual('volume', cc.get_service_type('volume')) self.assertEqual('workflow', cc.get_service_type('workflow')) self.assertEqual('not-exist', cc.get_service_type('not-exist')) @@ -177,7 +177,7 @@ def test_no_override(self): def test_get_session_no_auth(self): config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) - cc = cloud_config.CloudConfig("test1", "region-al", config_dict) + cc = cloud_region.CloudRegion("test1", "region-al", config_dict) self.assertRaises( exceptions.OpenStackConfigException, cc.get_session) @@ -189,7 +189,7 @@ def test_get_session(self, mock_session): fake_session = mock.Mock() fake_session.additional_user_agent = [] mock_session.return_value = fake_session - cc = cloud_config.CloudConfig( + cc = cloud_region.CloudRegion( "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_session() mock_session.assert_called_with( @@ -208,7 +208,7 @@ def test_get_session_with_app_name(self, mock_session): fake_session.app_name = None fake_session.app_version = None mock_session.return_value = fake_session - cc = cloud_config.CloudConfig( + cc = cloud_region.CloudRegion( "test1", "region-al", config_dict, auth_plugin=mock.Mock(), app_name="test_app", app_version="test_version") cc.get_session() @@ -229,7 +229,7 @@ def test_get_session_with_timeout(self, mock_session): config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) config_dict['api_timeout'] = 9 - cc = cloud_config.CloudConfig( + cc = cloud_region.CloudRegion( "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_session() mock_session.assert_called_with( @@ -243,7 +243,7 @@ def test_get_session_with_timeout(self, mock_session): def test_override_session_endpoint_override(self, mock_session): config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) - cc = cloud_config.CloudConfig( + cc = cloud_region.CloudRegion( "test1", "region-al", config_dict, auth_plugin=mock.Mock()) self.assertEqual( cc.get_session_endpoint('compute'), @@ -253,19 +253,19 @@ def test_override_session_endpoint_override(self, mock_session): def test_override_session_endpoint(self, mock_session): config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) - cc = cloud_config.CloudConfig( + cc = cloud_region.CloudRegion( "test1", "region-al", config_dict, auth_plugin=mock.Mock()) self.assertEqual( cc.get_session_endpoint('telemetry'), fake_services_dict['telemetry_endpoint']) - @mock.patch.object(cloud_config.CloudConfig, 'get_session') + @mock.patch.object(cloud_region.CloudRegion, 'get_session') def test_session_endpoint(self, mock_get_session): mock_session = mock.Mock() mock_get_session.return_value = mock_session config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) - cc = cloud_config.CloudConfig( + cc = cloud_region.CloudRegion( "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_session_endpoint('orchestration') mock_session.get_endpoint.assert_called_with( @@ -274,17 +274,17 @@ def test_session_endpoint(self, mock_get_session): region_name='region-al', service_type='orchestration') - @mock.patch.object(cloud_config.CloudConfig, 'get_session') + @mock.patch.object(cloud_region.CloudRegion, 'get_session') def test_session_endpoint_not_found(self, mock_get_session): exc_to_raise = ksa_exceptions.catalog.EndpointNotFound mock_get_session.return_value.get_endpoint.side_effect = exc_to_raise - cc = cloud_config.CloudConfig( + cc = cloud_region.CloudRegion( "test1", "region-al", {}, auth_plugin=mock.Mock()) self.assertIsNone(cc.get_session_endpoint('notfound')) - @mock.patch.object(cloud_config.CloudConfig, 'get_api_version') - @mock.patch.object(cloud_config.CloudConfig, 'get_auth_args') - @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + @mock.patch.object(cloud_region.CloudRegion, 'get_api_version') + @mock.patch.object(cloud_region.CloudRegion, 'get_auth_args') + @mock.patch.object(cloud_region.CloudRegion, 'get_session_endpoint') def test_legacy_client_object_store_password( self, mock_get_session_endpoint, @@ -301,7 +301,7 @@ def test_legacy_client_object_store_password( ) config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) - cc = cloud_config.CloudConfig( + cc = cloud_region.CloudRegion( "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('object-store', mock_client) mock_client.assert_called_with( @@ -313,8 +313,8 @@ def test_legacy_client_object_store_password( 'endpoint_type': 'public', }) - @mock.patch.object(cloud_config.CloudConfig, 'get_auth_args') - @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + @mock.patch.object(cloud_region.CloudRegion, 'get_auth_args') + @mock.patch.object(cloud_region.CloudRegion, 'get_session_endpoint') def test_legacy_client_object_store_password_v2( self, mock_get_session_endpoint, mock_get_auth_args): mock_client = mock.Mock() @@ -327,7 +327,7 @@ def test_legacy_client_object_store_password_v2( ) config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) - cc = cloud_config.CloudConfig( + cc = cloud_region.CloudRegion( "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('object-store', mock_client) mock_client.assert_called_with( @@ -339,8 +339,8 @@ def test_legacy_client_object_store_password_v2( 'endpoint_type': 'public', }) - @mock.patch.object(cloud_config.CloudConfig, 'get_auth_args') - @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + @mock.patch.object(cloud_region.CloudRegion, 'get_auth_args') + @mock.patch.object(cloud_region.CloudRegion, 'get_session_endpoint') def test_legacy_client_object_store( self, mock_get_session_endpoint, mock_get_auth_args): mock_client = mock.Mock() @@ -348,7 +348,7 @@ def test_legacy_client_object_store( mock_get_auth_args.return_value = {} config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) - cc = cloud_config.CloudConfig( + cc = cloud_region.CloudRegion( "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('object-store', mock_client) mock_client.assert_called_with( @@ -360,8 +360,8 @@ def test_legacy_client_object_store( 'endpoint_type': 'public', }) - @mock.patch.object(cloud_config.CloudConfig, 'get_auth_args') - @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + @mock.patch.object(cloud_region.CloudRegion, 'get_auth_args') + @mock.patch.object(cloud_region.CloudRegion, 'get_session_endpoint') def test_legacy_client_object_store_timeout( self, mock_get_session_endpoint, mock_get_auth_args): mock_client = mock.Mock() @@ -370,7 +370,7 @@ def test_legacy_client_object_store_timeout( config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) config_dict['api_timeout'] = 9 - cc = cloud_config.CloudConfig( + cc = cloud_region.CloudRegion( "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('object-store', mock_client) mock_client.assert_called_with( @@ -382,7 +382,7 @@ def test_legacy_client_object_store_timeout( 'endpoint_type': 'public', }) - @mock.patch.object(cloud_config.CloudConfig, 'get_auth_args') + @mock.patch.object(cloud_region.CloudRegion, 'get_auth_args') def test_legacy_client_object_store_endpoint( self, mock_get_auth_args): mock_client = mock.Mock() @@ -390,7 +390,7 @@ def test_legacy_client_object_store_endpoint( config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) config_dict['object_store_endpoint'] = 'http://example.com/swift' - cc = cloud_config.CloudConfig( + cc = cloud_region.CloudRegion( "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('object-store', mock_client) mock_client.assert_called_with( @@ -402,13 +402,13 @@ def test_legacy_client_object_store_endpoint( 'endpoint_type': 'public', }) - @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + @mock.patch.object(cloud_region.CloudRegion, 'get_session_endpoint') def test_legacy_client_image(self, mock_get_session_endpoint): mock_client = mock.Mock() mock_get_session_endpoint.return_value = 'http://example.com/v2' config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) - cc = cloud_config.CloudConfig( + cc = cloud_region.CloudRegion( "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('image', mock_client) mock_client.assert_called_with( @@ -422,14 +422,14 @@ def test_legacy_client_image(self, mock_get_session_endpoint): service_type='mage' ) - @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + @mock.patch.object(cloud_region.CloudRegion, 'get_session_endpoint') def test_legacy_client_image_override(self, mock_get_session_endpoint): mock_client = mock.Mock() mock_get_session_endpoint.return_value = 'http://example.com/v2' config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) config_dict['image_endpoint_override'] = 'http://example.com/override' - cc = cloud_config.CloudConfig( + cc = cloud_region.CloudRegion( "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('image', mock_client) mock_client.assert_called_with( @@ -443,7 +443,7 @@ def test_legacy_client_image_override(self, mock_get_session_endpoint): service_type='mage' ) - @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + @mock.patch.object(cloud_region.CloudRegion, 'get_session_endpoint') def test_legacy_client_image_versioned(self, mock_get_session_endpoint): mock_client = mock.Mock() mock_get_session_endpoint.return_value = 'http://example.com/v2' @@ -451,7 +451,7 @@ def test_legacy_client_image_versioned(self, mock_get_session_endpoint): config_dict.update(fake_services_dict) # v2 endpoint was passed, 1 requested in config, endpoint wins config_dict['image_api_version'] = '1' - cc = cloud_config.CloudConfig( + cc = cloud_region.CloudRegion( "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('image', mock_client) mock_client.assert_called_with( @@ -465,7 +465,7 @@ def test_legacy_client_image_versioned(self, mock_get_session_endpoint): service_type='mage' ) - @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + @mock.patch.object(cloud_region.CloudRegion, 'get_session_endpoint') def test_legacy_client_image_unversioned(self, mock_get_session_endpoint): mock_client = mock.Mock() mock_get_session_endpoint.return_value = 'http://example.com/' @@ -473,7 +473,7 @@ def test_legacy_client_image_unversioned(self, mock_get_session_endpoint): config_dict.update(fake_services_dict) # Versionless endpoint, config wins config_dict['image_api_version'] = '1' - cc = cloud_config.CloudConfig( + cc = cloud_region.CloudRegion( "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('image', mock_client) mock_client.assert_called_with( @@ -487,7 +487,7 @@ def test_legacy_client_image_unversioned(self, mock_get_session_endpoint): service_type='mage' ) - @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + @mock.patch.object(cloud_region.CloudRegion, 'get_session_endpoint') def test_legacy_client_image_argument(self, mock_get_session_endpoint): mock_client = mock.Mock() mock_get_session_endpoint.return_value = 'http://example.com/v3' @@ -495,7 +495,7 @@ def test_legacy_client_image_argument(self, mock_get_session_endpoint): config_dict.update(fake_services_dict) # Versionless endpoint, config wins config_dict['image_api_version'] = '6' - cc = cloud_config.CloudConfig( + cc = cloud_region.CloudRegion( "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('image', mock_client, version='beef') mock_client.assert_called_with( @@ -509,13 +509,13 @@ def test_legacy_client_image_argument(self, mock_get_session_endpoint): service_type='mage' ) - @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + @mock.patch.object(cloud_region.CloudRegion, 'get_session_endpoint') def test_legacy_client_network(self, mock_get_session_endpoint): mock_client = mock.Mock() mock_get_session_endpoint.return_value = 'http://example.com/v2' config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) - cc = cloud_config.CloudConfig( + cc = cloud_region.CloudRegion( "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('network', mock_client) mock_client.assert_called_with( @@ -527,13 +527,13 @@ def test_legacy_client_network(self, mock_get_session_endpoint): session=mock.ANY, service_name=None) - @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + @mock.patch.object(cloud_region.CloudRegion, 'get_session_endpoint') def test_legacy_client_compute(self, mock_get_session_endpoint): mock_client = mock.Mock() mock_get_session_endpoint.return_value = 'http://example.com/v2' config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) - cc = cloud_config.CloudConfig( + cc = cloud_region.CloudRegion( "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('compute', mock_client) mock_client.assert_called_with( @@ -545,13 +545,13 @@ def test_legacy_client_compute(self, mock_get_session_endpoint): session=mock.ANY, service_name=None) - @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + @mock.patch.object(cloud_region.CloudRegion, 'get_session_endpoint') def test_legacy_client_identity(self, mock_get_session_endpoint): mock_client = mock.Mock() mock_get_session_endpoint.return_value = 'http://example.com/v2' config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) - cc = cloud_config.CloudConfig( + cc = cloud_region.CloudRegion( "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('identity', mock_client) mock_client.assert_called_with( @@ -564,14 +564,14 @@ def test_legacy_client_identity(self, mock_get_session_endpoint): session=mock.ANY, service_name='locks') - @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + @mock.patch.object(cloud_region.CloudRegion, 'get_session_endpoint') def test_legacy_client_identity_v3(self, mock_get_session_endpoint): mock_client = mock.Mock() mock_get_session_endpoint.return_value = 'http://example.com' config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) config_dict['identity_api_version'] = '3' - cc = cloud_config.CloudConfig( + cc = cloud_region.CloudRegion( "test1", "region-al", config_dict, auth_plugin=mock.Mock()) cc.get_legacy_client('identity', mock_client) mock_client.assert_called_with( diff --git a/openstack/tests/unit/config/test_config.py b/openstack/tests/unit/config/test_config.py index b67d04124..a8a7efb1c 100644 --- a/openstack/tests/unit/config/test_config.py +++ b/openstack/tests/unit/config/test_config.py @@ -22,7 +22,7 @@ import yaml from openstack import config -from openstack.config import cloud_config +from openstack.config import cloud_region from openstack.config import defaults from openstack.config import exceptions from openstack.config import loader @@ -36,7 +36,21 @@ def prompt_for_password(prompt=None): class TestConfig(base.TestCase): + def test_get_all(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + secure_files=[self.no_yaml]) + clouds = c.get_all() + # We add one by hand because the regions cloud is going to exist + # twice since it has two regions in it + user_clouds = [ + cloud for cloud in base.USER_CONF['clouds'].keys() + ] + ['_test_cloud_regions'] + configured_clouds = [cloud.name for cloud in clouds] + self.assertItemsEqual(user_clouds, configured_clouds) + def test_get_all_clouds(self): + # Ensure the alias is in place c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml], secure_files=[self.no_yaml]) @@ -49,14 +63,22 @@ def test_get_all_clouds(self): configured_clouds = [cloud.name for cloud in clouds] self.assertItemsEqual(user_clouds, configured_clouds) + def test_get_one(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + cloud = c.get_one(validate=False) + self.assertIsInstance(cloud, cloud_region.CloudRegion) + self.assertEqual(cloud.name, '') + def test_get_one_cloud(self): + # Ensure the alias is in place c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) cloud = c.get_one_cloud(validate=False) - self.assertIsInstance(cloud, cloud_config.CloudConfig) + self.assertIsInstance(cloud, cloud_region.CloudRegion) self.assertEqual(cloud.name, '') - def test_get_one_cloud_default_cloud_from_file(self): + def test_get_one_default_cloud_from_file(self): single_conf = base._write_yaml({ 'clouds': { 'single': { @@ -72,12 +94,12 @@ def test_get_one_cloud_default_cloud_from_file(self): }) c = config.OpenStackConfig(config_files=[single_conf], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud() + cc = c.get_one() self.assertEqual(cc.name, 'single') - def test_get_one_cloud_auth_defaults(self): + def test_get_one_auth_defaults(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml]) - cc = c.get_one_cloud(cloud='_test-cloud_', auth={'username': 'user'}) + cc = c.get_one(cloud='_test-cloud_', auth={'username': 'user'}) self.assertEqual('user', cc.auth['username']) self.assertEqual( defaults._defaults['auth_type'], @@ -88,11 +110,11 @@ def test_get_one_cloud_auth_defaults(self): cc.identity_api_version, ) - def test_get_one_cloud_auth_override_defaults(self): + def test_get_one_auth_override_defaults(self): default_options = {'compute_api_version': '4'} c = config.OpenStackConfig(config_files=[self.cloud_yaml], override_defaults=default_options) - cc = c.get_one_cloud(cloud='_test-cloud_', auth={'username': 'user'}) + cc = c.get_one(cloud='_test-cloud_', auth={'username': 'user'}) self.assertEqual('user', cc.auth['username']) self.assertEqual('4', cc.compute_api_version) self.assertEqual( @@ -100,7 +122,7 @@ def test_get_one_cloud_auth_override_defaults(self): cc.identity_api_version, ) - def test_get_one_cloud_with_config_files(self): + def test_get_one_with_config_files(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml], secure_files=[self.secure_yaml]) @@ -109,51 +131,51 @@ def test_get_one_cloud_with_config_files(self): self.assertIsInstance(c.cloud_config['cache'], dict) self.assertIn('max_age', c.cloud_config['cache']) self.assertIn('path', c.cloud_config['cache']) - cc = c.get_one_cloud('_test-cloud_') + cc = c.get_one('_test-cloud_') self._assert_cloud_details(cc) - cc = c.get_one_cloud('_test_cloud_no_vendor') + cc = c.get_one('_test_cloud_no_vendor') self._assert_cloud_details(cc) - def test_get_one_cloud_with_int_project_id(self): + def test_get_one_with_int_project_id(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud('_test-cloud-int-project_') + cc = c.get_one('_test-cloud-int-project_') self.assertEqual('12345', cc.auth['project_id']) - def test_get_one_cloud_with_domain_id(self): + def test_get_one_with_domain_id(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud('_test-cloud-domain-id_') + cc = c.get_one('_test-cloud-domain-id_') self.assertEqual('6789', cc.auth['user_domain_id']) self.assertEqual('123456789', cc.auth['project_domain_id']) self.assertNotIn('domain_id', cc.auth) self.assertNotIn('domain-id', cc.auth) self.assertNotIn('domain_id', cc) - def test_get_one_cloud_domain_scoped(self): + def test_get_one_domain_scoped(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud('_test-cloud-domain-scoped_') + cc = c.get_one('_test-cloud-domain-scoped_') self.assertEqual('12345', cc.auth['domain_id']) self.assertNotIn('user_domain_id', cc.auth) self.assertNotIn('project_domain_id', cc.auth) - def test_get_one_cloud_infer_user_domain(self): + def test_get_one_infer_user_domain(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud('_test-cloud-int-project_') + cc = c.get_one('_test-cloud-int-project_') self.assertEqual('awesome-domain', cc.auth['user_domain_id']) self.assertEqual('awesome-domain', cc.auth['project_domain_id']) self.assertNotIn('domain_id', cc.auth) self.assertNotIn('domain_id', cc) - def test_get_one_cloud_with_hyphenated_project_id(self): + def test_get_one_with_hyphenated_project_id(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud('_test_cloud_hyphenated') + cc = c.get_one('_test_cloud_hyphenated') self.assertEqual('12345', cc.auth['project_id']) - def test_get_one_cloud_with_hyphenated_kwargs(self): + def test_get_one_with_hyphenated_kwargs(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) args = { @@ -165,14 +187,14 @@ def test_get_one_cloud_with_hyphenated_kwargs(self): }, 'region_name': 'test-region', } - cc = c.get_one_cloud(**args) + cc = c.get_one(**args) self.assertEqual('http://example.com/v2', cc.auth['auth_url']) def test_no_environ(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) self.assertRaises( - exceptions.OpenStackConfigException, c.get_one_cloud, 'envvars') + exceptions.OpenStackConfigException, c.get_one, 'envvars') def test_fallthrough(self): c = config.OpenStackConfig(config_files=[self.no_yaml], @@ -181,44 +203,44 @@ def test_fallthrough(self): for k in os.environ.keys(): if k.startswith('OS_'): self.useFixture(fixtures.EnvironmentVariable(k)) - c.get_one_cloud(cloud='defaults', validate=False) + c.get_one(cloud='defaults', validate=False) def test_prefer_ipv6_true(self): c = config.OpenStackConfig(config_files=[self.no_yaml], vendor_files=[self.no_yaml], secure_files=[self.no_yaml]) - cc = c.get_one_cloud(cloud='defaults', validate=False) + cc = c.get_one(cloud='defaults', validate=False) self.assertTrue(cc.prefer_ipv6) def test_prefer_ipv6_false(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud(cloud='_test-cloud_') + cc = c.get_one(cloud='_test-cloud_') self.assertFalse(cc.prefer_ipv6) def test_force_ipv4_true(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud(cloud='_test-cloud_') + cc = c.get_one(cloud='_test-cloud_') self.assertTrue(cc.force_ipv4) def test_force_ipv4_false(self): c = config.OpenStackConfig(config_files=[self.no_yaml], vendor_files=[self.no_yaml], secure_files=[self.no_yaml]) - cc = c.get_one_cloud(cloud='defaults', validate=False) + cc = c.get_one(cloud='defaults', validate=False) self.assertFalse(cc.force_ipv4) - def test_get_one_cloud_auth_merge(self): + def test_get_one_auth_merge(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml]) - cc = c.get_one_cloud(cloud='_test-cloud_', auth={'username': 'user'}) + cc = c.get_one(cloud='_test-cloud_', auth={'username': 'user'}) self.assertEqual('user', cc.auth['username']) self.assertEqual('testpass', cc.auth['password']) - def test_get_one_cloud_networks(self): + def test_get_one_networks(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud('_test-cloud-networks_') + cc = c.get_one('_test-cloud-networks_') self.assertEqual( ['a-public', 'another-public', 'split-default'], cc.get_external_networks()) @@ -235,10 +257,10 @@ def test_get_one_cloud_networks(self): ['a-public', 'another-public', 'split-default'], cc.get_external_ipv6_networks()) - def test_get_one_cloud_no_networks(self): + def test_get_one_no_networks(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud('_test-cloud-domain-scoped_') + cc = c.get_one('_test-cloud-domain-scoped_') self.assertEqual([], cc.get_external_networks()) self.assertEqual([], cc.get_internal_networks()) self.assertIsNone(cc.get_nat_source()) @@ -249,7 +271,7 @@ def test_only_secure_yaml(self): c = config.OpenStackConfig(config_files=['nonexistent'], vendor_files=['nonexistent'], secure_files=[self.secure_yaml]) - cc = c.get_one_cloud(cloud='_test_cloud_no_vendor', validate=False) + cc = c.get_one(cloud='_test_cloud_no_vendor', validate=False) self.assertEqual('testpass', cc.auth['password']) def test_get_cloud_names(self): @@ -273,7 +295,7 @@ def test_get_cloud_names(self): for k in os.environ.keys(): if k.startswith('OS_'): self.useFixture(fixtures.EnvironmentVariable(k)) - c.get_one_cloud(cloud='defaults', validate=False) + c.get_one(cloud='defaults', validate=False) self.assertEqual(['defaults'], sorted(c.get_cloud_names())) def test_set_one_cloud_creates_file(self): @@ -394,24 +416,24 @@ def setUp(self): self.options = argparse.Namespace(**self.args) - def test_get_one_cloud_bad_region_argparse(self): + def test_get_one_bad_region_argparse(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) self.assertRaises( - exceptions.OpenStackConfigException, c.get_one_cloud, + exceptions.OpenStackConfigException, c.get_one, cloud='_test-cloud_', argparse=self.options) - def test_get_one_cloud_argparse(self): + def test_get_one_argparse(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud( + cc = c.get_one( cloud='_test_cloud_regions', argparse=self.options, validate=False) self.assertEqual(cc.region_name, 'region2') self.assertEqual(cc.snack_type, 'cookie') - def test_get_one_cloud_precedence(self): + def test_get_one_precedence(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) @@ -437,7 +459,7 @@ def test_get_one_cloud_precedence(self): ) options = argparse.Namespace(**args) - cc = c.get_one_cloud( + cc = c.get_one( argparse=options, **kwargs) self.assertEqual(cc.region_name, 'region2') self.assertEqual(cc.auth['password'], 'authpass') @@ -479,7 +501,7 @@ def test_get_one_cloud_precedence_osc(self): self.assertEqual(cc.auth['password'], 'argpass') self.assertEqual(cc.snack_type, 'cookie') - def test_get_one_cloud_precedence_no_argparse(self): + def test_get_one_precedence_no_argparse(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) @@ -495,30 +517,30 @@ def test_get_one_cloud_precedence_no_argparse(self): 'arbitrary': 'value', } - cc = c.get_one_cloud(**kwargs) + cc = c.get_one(**kwargs) self.assertEqual(cc.region_name, 'kwarg_region') self.assertEqual(cc.auth['password'], 'authpass') self.assertIsNone(cc.password) - def test_get_one_cloud_just_argparse(self): + def test_get_one_just_argparse(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud(argparse=self.options, validate=False) + cc = c.get_one(argparse=self.options, validate=False) self.assertIsNone(cc.cloud) self.assertEqual(cc.region_name, 'region2') self.assertEqual(cc.snack_type, 'cookie') - def test_get_one_cloud_just_kwargs(self): + def test_get_one_just_kwargs(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud(validate=False, **self.args) + cc = c.get_one(validate=False, **self.args) self.assertIsNone(cc.cloud) self.assertEqual(cc.region_name, 'region2') self.assertEqual(cc.snack_type, 'cookie') - def test_get_one_cloud_dash_kwargs(self): + def test_get_one_dash_kwargs(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) @@ -530,93 +552,93 @@ def test_get_one_cloud_dash_kwargs(self): 'region_name': 'other-test-region', 'snack_type': 'cookie', } - cc = c.get_one_cloud(**args) + cc = c.get_one(**args) self.assertIsNone(cc.cloud) self.assertEqual(cc.region_name, 'other-test-region') self.assertEqual(cc.snack_type, 'cookie') - def test_get_one_cloud_no_argparse(self): + def test_get_one_no_argparse(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud(cloud='_test-cloud_', argparse=None) + cc = c.get_one(cloud='_test-cloud_', argparse=None) self._assert_cloud_details(cc) self.assertEqual(cc.region_name, 'test-region') self.assertIsNone(cc.snack_type) - def test_get_one_cloud_no_argparse_regions(self): + def test_get_one_no_argparse_regions(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud(cloud='_test_cloud_regions', argparse=None) + cc = c.get_one(cloud='_test_cloud_regions', argparse=None) self._assert_cloud_details(cc) self.assertEqual(cc.region_name, 'region1') self.assertIsNone(cc.snack_type) - def test_get_one_cloud_bad_region(self): + def test_get_one_bad_region(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) self.assertRaises( exceptions.OpenStackConfigException, - c.get_one_cloud, + c.get_one, cloud='_test_cloud_regions', region_name='bad') - def test_get_one_cloud_bad_region_no_regions(self): + def test_get_one_bad_region_no_regions(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) self.assertRaises( exceptions.OpenStackConfigException, - c.get_one_cloud, + c.get_one, cloud='_test-cloud_', region_name='bad_region') - def test_get_one_cloud_no_argparse_region2(self): + def test_get_one_no_argparse_region2(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud( + cc = c.get_one( cloud='_test_cloud_regions', region_name='region2', argparse=None) self._assert_cloud_details(cc) self.assertEqual(cc.region_name, 'region2') self.assertIsNone(cc.snack_type) - def test_get_one_cloud_network(self): + def test_get_one_network(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud( + cc = c.get_one( cloud='_test_cloud_regions', region_name='region1', argparse=None) self._assert_cloud_details(cc) self.assertEqual(cc.region_name, 'region1') self.assertEqual('region1-network', cc.config['external_network']) - def test_get_one_cloud_per_region_network(self): + def test_get_one_per_region_network(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud( + cc = c.get_one( cloud='_test_cloud_regions', region_name='region2', argparse=None) self._assert_cloud_details(cc) self.assertEqual(cc.region_name, 'region2') self.assertEqual('my-network', cc.config['external_network']) - def test_get_one_cloud_no_yaml_no_cloud(self): + def test_get_one_no_yaml_no_cloud(self): c = config.OpenStackConfig(load_yaml_config=False) self.assertRaises( exceptions.OpenStackConfigException, - c.get_one_cloud, + c.get_one, cloud='_test_cloud_regions', region_name='region2', argparse=None) - def test_get_one_cloud_no_yaml(self): + def test_get_one_no_yaml(self): c = config.OpenStackConfig(load_yaml_config=False) - cc = c.get_one_cloud( + cc = c.get_one( region_name='region2', argparse=None, **base.USER_CONF['clouds']['_test_cloud_regions']) # Not using assert_cloud_details because of cache settings which # are not present without the file - self.assertIsInstance(cc, cloud_config.CloudConfig) + self.assertIsInstance(cc, cloud_region.CloudRegion) self.assertTrue(extras.safe_hasattr(cc, 'auth')) self.assertIsInstance(cc.auth, dict) self.assertIsNone(cc.cloud) @@ -675,7 +697,7 @@ def test_env_argparse_precedence(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud( + cc = c.get_one( cloud='envvars', argparse=self.options, validate=False) self.assertEqual(cc.auth['project_name'], 'project') @@ -688,7 +710,7 @@ def test_argparse_default_no_token(self): # novaclient will add this parser.add_argument('--os-auth-token') opts, _remain = parser.parse_known_args() - cc = c.get_one_cloud( + cc = c.get_one( cloud='_test_cloud_regions', argparse=opts) self.assertEqual(cc.config['auth_type'], 'password') self.assertNotIn('token', cc.config['auth']) @@ -704,7 +726,7 @@ def test_argparse_token(self): opts, _remain = parser.parse_known_args( ['--os-auth-token', 'very-bad-things', '--os-auth-type', 'token']) - cc = c.get_one_cloud(argparse=opts, validate=False) + cc = c.get_one(argparse=opts, validate=False) self.assertEqual(cc.config['auth_type'], 'token') self.assertEqual(cc.config['auth']['token'], 'very-bad-things') @@ -719,7 +741,7 @@ def test_argparse_underscores(self): '--os-auth-url', 'auth-url', '--os-project-name', 'project'] c.register_argparse_arguments(parser, argv=argv) opts, _remain = parser.parse_known_args(argv) - cc = c.get_one_cloud(argparse=opts) + cc = c.get_one(argparse=opts) self.assertEqual(cc.config['auth']['username'], 'user') self.assertEqual(cc.config['auth']['password'], 'pass') self.assertEqual(cc.config['auth']['auth_url'], 'auth-url') @@ -800,7 +822,7 @@ def test_register_argparse_service_type(self): self.assertEqual(opts.http_timeout, '20') with testtools.ExpectedException(AttributeError): opts.os_network_service_type - cloud = c.get_one_cloud(argparse=opts, validate=False) + cloud = c.get_one(argparse=opts, validate=False) self.assertEqual(cloud.config['service_type'], 'network') self.assertEqual(cloud.config['interface'], 'admin') self.assertEqual(cloud.config['api_timeout'], '20') @@ -821,7 +843,7 @@ def test_register_argparse_network_service_type(self): self.assertIsNone(opts.os_network_service_type) self.assertIsNone(opts.os_network_api_version) self.assertEqual(opts.network_api_version, '4') - cloud = c.get_one_cloud(argparse=opts, validate=False) + cloud = c.get_one(argparse=opts, validate=False) self.assertEqual(cloud.config['service_type'], 'network') self.assertEqual(cloud.config['interface'], 'admin') self.assertEqual(cloud.config['network_api_version'], '4') @@ -848,7 +870,7 @@ def test_register_argparse_network_service_types(self): self.assertEqual(opts.os_endpoint_type, 'admin') self.assertIsNone(opts.os_network_api_version) self.assertEqual(opts.network_api_version, '4') - cloud = c.get_one_cloud(argparse=opts, validate=False) + cloud = c.get_one(argparse=opts, validate=False) self.assertEqual(cloud.config['service_type'], 'compute') self.assertEqual(cloud.config['network_service_type'], 'badtype') self.assertEqual(cloud.config['interface'], 'admin') @@ -872,7 +894,7 @@ def setUp(self): self.options = argparse.Namespace(**self.args) - def test_get_one_cloud_prompt(self): + def test_get_one_prompt(self): c = config.OpenStackConfig( config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml], @@ -882,7 +904,7 @@ def test_get_one_cloud_prompt(self): # This needs a cloud definition without a password. # If this starts failing unexpectedly check that the cloud_yaml # and/or vendor_yaml do not have a password in the selected cloud. - cc = c.get_one_cloud( + cc = c.get_one( cloud='_test_cloud_no_vendor', argparse=self.options, ) @@ -904,7 +926,7 @@ def _reset_defaults(self): def test_set_no_default(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud(cloud='_test-cloud_', argparse=None) + cc = c.get_one(cloud='_test-cloud_', argparse=None) self._assert_cloud_details(cc) self.assertEqual('password', cc.auth_type) @@ -912,7 +934,7 @@ def test_set_default_before_init(self): loader.set_default('identity_api_version', '4') c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud(cloud='_test-cloud_', argparse=None) + cc = c.get_one(cloud='_test-cloud_', argparse=None) self.assertEqual('4', cc.identity_api_version) diff --git a/openstack/tests/unit/config/test_environ.py b/openstack/tests/unit/config/test_environ.py index 521d72ca2..fa0e34cee 100644 --- a/openstack/tests/unit/config/test_environ.py +++ b/openstack/tests/unit/config/test_environ.py @@ -14,7 +14,7 @@ from openstack import config -from openstack.config import cloud_config +from openstack.config import cloud_region from openstack.config import exceptions from openstack.tests.unit.config import base @@ -36,23 +36,23 @@ def setUp(self): self.useFixture( fixtures.EnvironmentVariable('NOVA_PROJECT_ID', 'testnova')) - def test_get_one_cloud(self): + def test_get_one(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - self.assertIsInstance(c.get_one_cloud(), cloud_config.CloudConfig) + self.assertIsInstance(c.get_one(), cloud_region.CloudRegion) def test_no_fallthrough(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) self.assertRaises( - exceptions.OpenStackConfigException, c.get_one_cloud, 'openstack') + exceptions.OpenStackConfigException, c.get_one, 'openstack') def test_envvar_name_override(self): self.useFixture( fixtures.EnvironmentVariable('OS_CLOUD_NAME', 'override')) c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud('override') + cc = c.get_one('override') self._assert_cloud_details(cc) def test_envvar_prefer_ipv6_override(self): @@ -61,22 +61,22 @@ def test_envvar_prefer_ipv6_override(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml], secure_files=[self.secure_yaml]) - cc = c.get_one_cloud('_test-cloud_') + cc = c.get_one('_test-cloud_') self.assertFalse(cc.prefer_ipv6) def test_environ_exists(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml], secure_files=[self.secure_yaml]) - cc = c.get_one_cloud('envvars') + cc = c.get_one('envvars') self._assert_cloud_details(cc) self.assertNotIn('auth_url', cc.config) self.assertIn('auth_url', cc.config['auth']) self.assertNotIn('project_id', cc.config['auth']) self.assertNotIn('auth_url', cc.config) - cc = c.get_one_cloud('_test-cloud_') + cc = c.get_one('_test-cloud_') self._assert_cloud_details(cc) - cc = c.get_one_cloud('_test_cloud_no_vendor') + cc = c.get_one('_test_cloud_no_vendor') self._assert_cloud_details(cc) def test_environ_prefix(self): @@ -84,18 +84,18 @@ def test_environ_prefix(self): vendor_files=[self.vendor_yaml], envvar_prefix='NOVA_', secure_files=[self.secure_yaml]) - cc = c.get_one_cloud('envvars') + cc = c.get_one('envvars') self._assert_cloud_details(cc) self.assertNotIn('auth_url', cc.config) self.assertIn('auth_url', cc.config['auth']) self.assertIn('project_id', cc.config['auth']) self.assertNotIn('auth_url', cc.config) - cc = c.get_one_cloud('_test-cloud_') + cc = c.get_one('_test-cloud_') self._assert_cloud_details(cc) - cc = c.get_one_cloud('_test_cloud_no_vendor') + cc = c.get_one('_test_cloud_no_vendor') self._assert_cloud_details(cc) - def test_get_one_cloud_with_config_files(self): + def test_get_one_with_config_files(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml], secure_files=[self.secure_yaml]) @@ -104,9 +104,9 @@ def test_get_one_cloud_with_config_files(self): self.assertIsInstance(c.cloud_config['cache'], dict) self.assertIn('max_age', c.cloud_config['cache']) self.assertIn('path', c.cloud_config['cache']) - cc = c.get_one_cloud('_test-cloud_') + cc = c.get_one('_test-cloud_') self._assert_cloud_details(cc) - cc = c.get_one_cloud('_test_cloud_no_vendor') + cc = c.get_one('_test_cloud_no_vendor') self._assert_cloud_details(cc) def test_config_file_override(self): @@ -115,7 +115,7 @@ def test_config_file_override(self): 'OS_CLIENT_CONFIG_FILE', self.cloud_yaml)) c = config.OpenStackConfig(config_files=[], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud('_test-cloud_') + cc = c.get_one('_test-cloud_') self._assert_cloud_details(cc) @@ -127,7 +127,7 @@ def test_no_envvars(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) self.assertRaises( - exceptions.OpenStackConfigException, c.get_one_cloud, 'envvars') + exceptions.OpenStackConfigException, c.get_one, 'envvars') def test_test_envvars(self): self.useFixture( @@ -137,7 +137,7 @@ def test_test_envvars(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) self.assertRaises( - exceptions.OpenStackConfigException, c.get_one_cloud, 'envvars') + exceptions.OpenStackConfigException, c.get_one, 'envvars') def test_incomplete_envvars(self): self.useFixture( @@ -150,7 +150,7 @@ def test_incomplete_envvars(self): # commenting it out in this patch to keep the patch size reasonable # self.assertRaises( # keystoneauth1.exceptions.auth_plugins.MissingRequiredOptions, - # c.get_one_cloud, 'envvars') + # c.get_one, 'envvars') def test_have_envvars(self): self.useFixture( @@ -165,7 +165,7 @@ def test_have_envvars(self): fixtures.EnvironmentVariable('OS_PROJECT_NAME', 'project')) c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) - cc = c.get_one_cloud('envvars') + cc = c.get_one('envvars') self.assertEqual(cc.config['auth']['username'], 'user') def test_old_envvars(self): @@ -181,5 +181,5 @@ def test_old_envvars(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml], envvar_prefix='NOVA_') - cc = c.get_one_cloud('envvars') + cc = c.get_one('envvars') self.assertEqual(cc.config['auth']['username'], 'nova') diff --git a/openstack/tests/unit/config/test_init.py b/openstack/tests/unit/config/test_init.py index 4f1af0c27..97171291a 100644 --- a/openstack/tests/unit/config/test_init.py +++ b/openstack/tests/unit/config/test_init.py @@ -18,18 +18,18 @@ class TestInit(base.TestCase): def test_get_config_without_arg_parser(self): - cloud_config = openstack.config.get_config( + cloud_region = openstack.config.get_config( options=None, validate=False) self.assertIsInstance( - cloud_config, - openstack.config.cloud_config.CloudConfig + cloud_region, + openstack.config.cloud_region.CloudRegion ) def test_get_config_with_arg_parser(self): - cloud_config = openstack.config.get_config( + cloud_region = openstack.config.get_config( options=argparse.ArgumentParser(), validate=False) self.assertIsInstance( - cloud_config, - openstack.config.cloud_config.CloudConfig + cloud_region, + openstack.config.cloud_region.CloudRegion ) diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index 5a87366c2..c03c80b50 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -107,10 +107,10 @@ def test_create_session(self): self.assertEqual('openstack.workflow.v2._proxy', conn.workflow.__class__.__module__) - def test_from_config_given_data(self): - data = openstack.config.OpenStackConfig().get_one_cloud("sample") + def test_from_config_given_config(self): + cloud_region = openstack.config.OpenStackConfig().get_one("sample") - sot = connection.from_config(cloud_config=data) + sot = connection.from_config(config=cloud_region) self.assertEqual(CONFIG_USERNAME, sot.config.config['auth']['username']) @@ -121,7 +121,7 @@ def test_from_config_given_data(self): self.assertEqual(CONFIG_PROJECT, sot.config.config['auth']['project_name']) - def test_from_config_given_name(self): + def test_from_config_given_cloud_name(self): sot = connection.from_config(cloud_name="sample") self.assertEqual(CONFIG_USERNAME, @@ -133,21 +133,47 @@ def test_from_config_given_name(self): self.assertEqual(CONFIG_PROJECT, sot.config.config['auth']['project_name']) + def test_from_config_given_cloud_config(self): + cloud_region = openstack.config.OpenStackConfig().get_one("sample") + + sot = connection.from_config(cloud_config=cloud_region) + + self.assertEqual(CONFIG_USERNAME, + sot.config.config['auth']['username']) + self.assertEqual(CONFIG_PASSWORD, + sot.config.config['auth']['password']) + self.assertEqual(CONFIG_AUTH_URL, + sot.config.config['auth']['auth_url']) + self.assertEqual(CONFIG_PROJECT, + sot.config.config['auth']['project_name']) + + def test_from_config_given_cloud(self): + sot = connection.from_config(cloud="sample") + + self.assertEqual(CONFIG_USERNAME, + sot.config.config['auth']['username']) + self.assertEqual(CONFIG_PASSWORD, + sot.config.config['auth']['password']) + self.assertEqual(CONFIG_AUTH_URL, + sot.config.config['auth']['auth_url']) + self.assertEqual(CONFIG_PROJECT, + sot.config.config['auth']['project_name']) + def test_from_config_given_options(self): version = "100" class Opts(object): compute_api_version = version - sot = connection.from_config(cloud_name="sample", options=Opts) + sot = connection.from_config(cloud="sample", options=Opts) self.assertEqual(version, sot.compute.version) def test_from_config_verify(self): - sot = connection.from_config(cloud_name="insecure") + sot = connection.from_config(cloud="insecure") self.assertFalse(sot.session.verify) - sot = connection.from_config(cloud_name="cacert") + sot = connection.from_config(cloud="cacert") self.assertEqual(CONFIG_CACERT, sot.session.verify) diff --git a/releasenotes/notes/removed-profile-437f3038025b0fb3.yaml b/releasenotes/notes/removed-profile-437f3038025b0fb3.yaml index ce8a383b3..84bc3bd26 100644 --- a/releasenotes/notes/removed-profile-437f3038025b0fb3.yaml +++ b/releasenotes/notes/removed-profile-437f3038025b0fb3.yaml @@ -1,7 +1,7 @@ --- upgrade: - The Profile object has been replaced with the use of - CloudConfig objects from openstack.config. + CloudRegion objects from openstack.config. - The openstacksdk specific Session object has been removed. - Proxy objects are now subclasses of keystoneauth1.adapter.Adapter. From 30debb730d3658dfa56c395e0d1838107aa5ff4a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 5 Jan 2018 19:52:36 -0600 Subject: [PATCH 1893/3836] Add function to make CloudRegion from session In cases where a user already has a Session that they're happy with, making a Connection to wrap it should be quite easy. The only settings that are needed are Adapter level settings, and all of those have sane defaults and don't actually need to be set. Add a factory function that can be used in those cases. Also, while we're in there, make CloudRegion use the word "region_name" like everyone else. This removes the get_region_name method that we added originally to occ in an early attempt to support openstacksdk. However, nothing in the data model makes sense if one service has one region_name and another service has another region_name. Connection is a connection to a region of a cloud. Change-Id: I52fde4df188fc2747776c4b65bc7ae68c6e82f06 --- openstack/cloud/openstackcloud.py | 7 +-- openstack/config/cloud_region.py | 59 ++++++++++++------- openstack/config/loader.py | 4 +- openstack/connection.py | 8 ++- .../tests/unit/config/test_cloud_config.py | 7 +-- .../tests/unit/config/test_from_session.py | 54 +++++++++++++++++ openstack/tests/unit/test_connection.py | 8 +-- 7 files changed, 109 insertions(+), 38 deletions(-) create mode 100644 openstack/tests/unit/config/test_from_session.py diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index c5da3b4d1..e82cabb00 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -164,7 +164,6 @@ def __init__( self.region_name = cloud_config.region_name self.default_interface = cloud_config.get_interface() self.private = cloud_config.config.get('private', False) - self.api_timeout = cloud_config.config['api_timeout'] self.image_api_use_tasks = cloud_config.config['image_api_use_tasks'] self.secgroup_source = cloud_config.config['secgroup_source'] self.force_ipv4 = cloud_config.force_ipv4 @@ -529,7 +528,7 @@ def _get_versioned_client( service_name=self.cloud_config.get_service_name(service_type), interface=self.cloud_config.get_interface(service_type), endpoint_override=self.cloud_config.get_endpoint(service_type), - region_name=self.cloud_config.region, + region_name=self.cloud_config.region_name, min_version=request_min_version, max_version=request_max_version) if adapter.get_endpoint(): @@ -542,7 +541,7 @@ def _get_versioned_client( service_name=self.cloud_config.get_service_name(service_type), interface=self.cloud_config.get_interface(service_type), endpoint_override=self.cloud_config.get_endpoint(service_type), - region_name=self.cloud_config.region, + region_name=self.cloud_config.region_name, min_version=min_version, max_version=max_version) @@ -582,7 +581,7 @@ def _get_raw_client( interface=self.cloud_config.get_interface(service_type), endpoint_override=self.cloud_config.get_endpoint( service_type) or endpoint_override, - region_name=self.cloud_config.region) + region_name=self.cloud_config.region_name) def _is_client_version(self, client, version): client_name = '_{client}_client'.format(client=client) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index df838b46d..8119fdf72 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -18,8 +18,9 @@ from keystoneauth1 import adapter import keystoneauth1.exceptions.catalog -from keystoneauth1 import session +from keystoneauth1 import session as ks_session import requestsexceptions +from six.moves import urllib import openstack from openstack import _log @@ -69,25 +70,47 @@ def _make_key(key, service_type): return "_".join([service_type, key]) +def from_session(session, name=None, config=None, **kwargs): + """Construct a CloudRegion from an existing `keystoneauth1.session.Session` + + When a Session already exists, we don't actually even need to go through + the OpenStackConfig.get_one_cloud dance. We have a Session with Auth info. + The only parameters that are really needed are adapter/catalog related. + + :param keystoneauth1.session.session session: + An existing Session to use. + :param str name: + A name to use for this cloud region in logging. If left empty, the + hostname of the auth_url found in the Session will be used. + :param dict config: + Config settings for this cloud region. + """ + # If someone is constructing one of these from a Session, then they are + # not using a named config. Use the hostname of their auth_url instead. + name = name or urllib.parse.urlparse(session.auth.auth_url).hostname + config = config or {} + return CloudRegion(name=name, session=session, config=config, **kwargs) + + class CloudRegion(object): """The configuration for a Region of an OpenStack Cloud. A CloudRegion encapsulates the config information needed for connections to all of the services in a Region of a Cloud. """ - def __init__(self, name, region, config, + def __init__(self, name, region_name=None, config=None, force_ipv4=False, auth_plugin=None, openstack_config=None, session_constructor=None, - app_name=None, app_version=None): + app_name=None, app_version=None, session=None): self.name = name - self.region = region + self.region_name = region_name self.config = config self.log = _log.setup_logging(__name__) self._force_ipv4 = force_ipv4 self._auth = auth_plugin self._openstack_config = openstack_config - self._keystone_session = None - self._session_constructor = session_constructor or session.Session + self._keystone_session = session + self._session_constructor = session_constructor or ks_session.Session self._app_name = app_name self._app_version = app_version @@ -106,8 +129,10 @@ def __iter__(self): return self.config.__iter__() def __eq__(self, other): - return (self.name == other.name and self.region == other.region - and self.config == other.config) + return ( + self.name == other.name + and self.region_name == other.region_name + and self.config == other.config) def __ne__(self, other): return not self == other @@ -145,19 +170,13 @@ def get_services(self): return list(set(services)) def get_auth_args(self): - return self.config['auth'] + return self.config.get('auth', {}) def get_interface(self, service_type=None): key = _make_key('interface', service_type) interface = self.config.get('interface') return self.config.get(key, interface) - def get_region_name(self, service_type=None): - if not service_type: - return self.region - key = _make_key('region_name', service_type) - return self.config.get(key, self.region) - def get_api_version(self, service_type): key = _make_key('api_version', service_type) return self.config.get(key, None) @@ -220,7 +239,7 @@ def get_session(self): self.log.debug( "Turning off SSL warnings for {cloud}:{region}" " since verify=False".format( - cloud=self.name, region=self.region)) + cloud=self.name, region=self.region_name)) requestsexceptions.squelch_warnings(insecure_requests=not verify) self._keystone_session = self._session_constructor( auth=self._auth, @@ -286,7 +305,7 @@ def get_session_client(self, service_key, version=None): service_type=self.get_service_type(service_key), service_name=self.get_service_name(service_key), interface=self.get_interface(service_key), - region_name=self.get_region_name(service_key), + region_name=self.region_name, version=version, min_version=min_version, max_version=max_version) @@ -321,7 +340,7 @@ def get_session_endpoint( endpoint = None kwargs = { 'service_name': self.get_service_name(service_key), - 'region_name': self.region + 'region_name': self.region_name } kwargs['interface'] = self.get_interface(service_key) if service_key == 'volume' and not self.get_api_version('volume'): @@ -410,14 +429,14 @@ def get_legacy_client( os_options=dict( service_type=self.get_service_type(service_key), object_storage_url=endpoint_override, - region_name=self.region)) + region_name=self.region_name)) else: constructor_kwargs = dict( session=self.get_session(), service_name=self.get_service_name(service_key), service_type=self.get_service_type(service_key), endpoint_override=endpoint_override, - region_name=self.region) + region_name=self.region_name) if service_key == 'image': # os-client-config does not depend on glanceclient, but if diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 304bfc41a..dcc22c14b 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -1106,7 +1106,7 @@ def get_one( cloud_name = str(cloud) return cloud_region.CloudRegion( name=cloud_name, - region=config['region_name'], + region_name=config['region_name'], config=config, force_ipv4=force_ipv4, auth_plugin=auth_plugin, @@ -1203,7 +1203,7 @@ def get_one_cloud_osc( cloud_name = str(cloud) return cloud_region.CloudRegion( name=cloud_name, - region=config['region_name'], + region_name=config['region_name'], config=self._normalize_keys(config), force_ipv4=force_ipv4, auth_plugin=auth_plugin, diff --git a/openstack/connection.py b/openstack/connection.py index 4a59b3541..f5797b7df 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -188,7 +188,9 @@ def __init__(self, cloud=None, config=None, session=None, cloud=cloud, validate=session is None, **kwargs) self.task_manager = task_manager.TaskManager( - name=':'.join([self.config.name, self.config.region])) + name=':'.join([ + self.config.name, + self.config.region_name or 'unknown'])) if session: # TODO(mordred) Expose constructor option for this in OCC @@ -222,7 +224,7 @@ def _get_config_from_profile(self, profile, authenticator, **kwargs): kwargs[key] = service.version config = cloud_region.CloudRegion( - name=name, region=region_name, config=kwargs) + name=name, region_name=region_name, config=kwargs) config._auth = authenticator def _open(self): @@ -256,7 +258,7 @@ def _load(self, service_type): service_type=self.config.get_service_type(service_type), service_name=self.config.get_service_name(service_type), interface=self.config.get_interface(service_type), - region_name=self.config.region, + region_name=self.config.region_name, version=self.config.get_api_version(service_type) ) all_types = self.service_type_manager.get_all_types(service_type) diff --git a/openstack/tests/unit/config/test_cloud_config.py b/openstack/tests/unit/config/test_cloud_config.py index 3f51093ac..6ce77db5b 100644 --- a/openstack/tests/unit/config/test_cloud_config.py +++ b/openstack/tests/unit/config/test_cloud_config.py @@ -26,7 +26,6 @@ fake_services_dict = { 'compute_api_version': '2', 'compute_endpoint_override': 'http://compute.example.com', - 'compute_region_name': 'region-bl', 'telemetry_endpoint': 'http://telemetry.example.com', 'interface': 'public', 'image_service_type': 'mage', @@ -42,7 +41,7 @@ class TestCloudRegion(base.TestCase): def test_arbitrary_attributes(self): cc = cloud_region.CloudRegion("test1", "region-al", fake_config_dict) self.assertEqual("test1", cc.name) - self.assertEqual("region-al", cc.region) + self.assertEqual("region-al", cc.region_name) # Look up straight value self.assertEqual(1, cc.a) @@ -137,9 +136,7 @@ def test_getters(self): self.assertEqual('public', cc.get_interface()) self.assertEqual('public', cc.get_interface('compute')) self.assertEqual('admin', cc.get_interface('identity')) - self.assertEqual('region-al', cc.get_region_name()) - self.assertEqual('region-al', cc.get_region_name('image')) - self.assertEqual('region-bl', cc.get_region_name('compute')) + self.assertEqual('region-al', cc.region_name) self.assertIsNone(cc.get_api_version('image')) self.assertEqual('2', cc.get_api_version('compute')) self.assertEqual('mage', cc.get_service_type('image')) diff --git a/openstack/tests/unit/config/test_from_session.py b/openstack/tests/unit/config/test_from_session.py new file mode 100644 index 000000000..abc13f14d --- /dev/null +++ b/openstack/tests/unit/config/test_from_session.py @@ -0,0 +1,54 @@ +# Copyright 2018 Red Hat, 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. + +from testscenarios import load_tests_apply_scenarios as load_tests # noqa + +import uuid + +from openstack.config import cloud_region +from openstack import connection +from openstack.tests import fakes +from openstack.tests.unit import base + + +class TestFromSession(base.RequestsMockTestCase): + + scenarios = [ + ('no_region', dict(test_region=None)), + ('with_region', dict(test_region='RegionOne')), + ] + + def test_from_session(self): + config = cloud_region.from_session( + self.cloud.keystone_session, region_name=self.test_region) + self.assertEqual(config.name, 'identity.example.com') + if not self.test_region: + self.assertIsNone(config.region_name) + else: + self.assertEqual(config.region_name, self.test_region) + + server_id = str(uuid.uuid4()) + server_name = self.getUniqueString('name') + fake_server = fakes.make_fake_server(server_id, server_name) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [fake_server]}), + ]) + + conn = connection.Connection(config=config) + s = next(conn.compute.servers()) + self.assertEqual(s.id, server_id) + self.assertEqual(s.name, server_name) + self.assert_calls() diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index c03c80b50..cfbb698f9 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -121,8 +121,8 @@ def test_from_config_given_config(self): self.assertEqual(CONFIG_PROJECT, sot.config.config['auth']['project_name']) - def test_from_config_given_cloud_name(self): - sot = connection.from_config(cloud_name="sample") + def test_from_config_given_cloud(self): + sot = connection.from_config(cloud="sample") self.assertEqual(CONFIG_USERNAME, sot.config.config['auth']['username']) @@ -147,8 +147,8 @@ def test_from_config_given_cloud_config(self): self.assertEqual(CONFIG_PROJECT, sot.config.config['auth']['project_name']) - def test_from_config_given_cloud(self): - sot = connection.from_config(cloud="sample") + def test_from_config_given_cloud_name(self): + sot = connection.from_config(cloud_name="sample") self.assertEqual(CONFIG_USERNAME, sot.config.config['auth']['username']) From 46135f9b857d0560820393e67b2142ff15c1dca0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 3 Jan 2018 14:24:24 -0600 Subject: [PATCH 1894/3836] Port wait_for_ methods to use iterate_timeout shade has a function called iterate_timeout which is used to, you guessed it, iterate until a timeout is met. Update wait_for_status and wait_for_deleted to use iterate_timeout. While doing that, shift iterate_timeout to be in openstack.utils. An attempt was made at shifting the parameter names of wait_for_ and iterate_timeout to align, but aligning in both directions reads wrong. wait_for_server with wait and interval reads well. iterate_timeout with timeout and wait reads well. I think consistency is just going to have to take a back seat to flow on this one. Change-Id: Ida691638b57ff6d41b0a9e066a180b79b20642e9 --- doc/source/user/logging.rst | 4 +- openstack/cloud/_utils.py | 37 ----------- openstack/cloud/exc.py | 5 +- openstack/cloud/openstackcloud.py | 39 ++++++------ openstack/cloud/operatorcloud.py | 17 +++--- openstack/resource2.py | 61 +++++++++++-------- .../tests/functional/cloud/test_compute.py | 4 +- .../functional/cloud/test_floating_ip.py | 6 +- .../tests/functional/cloud/test_volume.py | 4 +- openstack/tests/unit/cloud/test_shade.py | 10 +-- openstack/tests/unit/test_resource2.py | 56 ++++++++--------- openstack/utils.py | 39 ++++++++++++ 12 files changed, 148 insertions(+), 134 deletions(-) diff --git a/doc/source/user/logging.rst b/doc/source/user/logging.rst index 57bf99f61..e3239d3ff 100644 --- a/doc/source/user/logging.rst +++ b/doc/source/user/logging.rst @@ -57,10 +57,10 @@ openstack.cloud.exc exception to the `openstack.cloud.exc` logger. Wrapped exceptions are usually considered implementation details, but can be useful for debugging problems. -openstack.cloud.iterate_timeout +openstack.iterate_timeout When `shade` needs to poll a resource, it does so in a loop that waits between iterations and ultimately timesout. The - `openstack.cloud.iterate_timeout` logger emits messages for each iteration + `openstack.iterate_timeout` logger emits messages for each iteration indicating it is waiting and for how long. These can be useful to see for long running tasks so that one can know things are not stuck, but can also be noisy. diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index 58356c766..b5eb032b4 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -22,7 +22,6 @@ import six import sre_constants import sys -import time import uuid from decorator import decorator @@ -40,42 +39,6 @@ def _exc_clear(): sys.exc_clear() -def _iterate_timeout(timeout, message, wait=2): - """Iterate and raise an exception on timeout. - - This is a generator that will continually yield and sleep for - wait seconds, and if the timeout is reached, will raise an exception - with . - - """ - log = _log.setup_logging('openstack.cloud.iterate_timeout') - - try: - # None as a wait winds up flowing well in the per-resource cache - # flow. We could spread this logic around to all of the calling - # points, but just having this treat None as "I don't have a value" - # seems friendlier - if wait is None: - wait = 2 - elif wait == 0: - # wait should be < timeout, unless timeout is None - wait = 0.1 if timeout is None else min(0.1, timeout) - wait = float(wait) - except ValueError: - raise exc.OpenStackCloudException( - "Wait value must be an int or float value. {wait} given" - " instead".format(wait=wait)) - - start = time.time() - count = 0 - while (timeout is None) or (time.time() < start + timeout): - count += 1 - yield count - log.debug('Waiting %s seconds', wait) - time.sleep(wait) - raise exc.OpenStackCloudTimeout(message) - - def _make_unicode(input): """Turn an input into unicode unconditionally diff --git a/openstack/cloud/exc.py b/openstack/cloud/exc.py index c02de2d77..d82d73e6c 100644 --- a/openstack/cloud/exc.py +++ b/openstack/cloud/exc.py @@ -15,6 +15,7 @@ from openstack import exceptions OpenStackCloudException = exceptions.SDKException +OpenStackCloudTimeout = exceptions.ResourceTimeout class OpenStackCloudCreateException(OpenStackCloudException): @@ -27,10 +28,6 @@ def __init__(self, resource, resource_id, extra_data=None, **kwargs): self.resource_id = resource_id -class OpenStackCloudTimeout(OpenStackCloudException): - pass - - class OpenStackCloudUnavailableExtension(OpenStackCloudException): pass diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index e82cabb00..90fa03d56 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -47,6 +47,7 @@ import openstack.config.defaults import openstack.connection from openstack import task_manager +from openstack import utils # TODO(shade) shade keys were x-object-meta-x-sdk-md5 - we need to add those # to freshness checks so that a shade->sdk transition doens't @@ -4471,7 +4472,7 @@ def create_image_snapshot( def wait_for_image(self, image, timeout=3600): image_id = image['id'] - for count in _utils._iterate_timeout( + for count in utils.iterate_timeout( timeout, "Timeout waiting for image to snapshot"): self.list_images.invalidate(self) image = self.get_image(image_id) @@ -4510,7 +4511,7 @@ def delete_image( self.delete_object(container=container, name=objname) if wait: - for count in _utils._iterate_timeout( + for count in utils.iterate_timeout( timeout, "Timeout waiting for the image to be deleted."): self._get_cache(None).invalidate() @@ -4740,7 +4741,7 @@ def _upload_image_from_volume( if not wait: return self.get_image(response['image_id']) try: - for count in _utils._iterate_timeout( + for count in utils.iterate_timeout( timeout, "Timeout waiting for the image to finish."): image_obj = self.get_image(response['image_id']) @@ -4829,7 +4830,7 @@ def _upload_image_put( if not wait: return image try: - for count in _utils._iterate_timeout( + for count in utils.iterate_timeout( timeout, "Timeout waiting for the image to finish."): image_obj = self.get_image(image.id) @@ -4869,7 +4870,7 @@ def _upload_image_task( if wait: start = time.time() image_id = None - for count in _utils._iterate_timeout( + for count in utils.iterate_timeout( timeout, "Timeout waiting for the image to import."): try: @@ -5032,7 +5033,7 @@ def create_volume( if wait: vol_id = volume['id'] - for count in _utils._iterate_timeout( + for count in utils.iterate_timeout( timeout, "Timeout waiting for the volume to be available."): volume = self.get_volume(vol_id) @@ -5118,7 +5119,7 @@ def delete_volume(self, name_or_id=None, wait=True, timeout=None, self.list_volumes.invalidate(self) if wait: - for count in _utils._iterate_timeout( + for count in utils.iterate_timeout( timeout, "Timeout waiting for the volume to be deleted."): @@ -5180,7 +5181,7 @@ def detach_volume(self, server, volume, wait=True, timeout=None): volume=volume['id'], server=server['id']))) if wait: - for count in _utils._iterate_timeout( + for count in utils.iterate_timeout( timeout, "Timeout waiting for volume %s to detach." % volume['id']): try: @@ -5249,7 +5250,7 @@ def attach_volume(self, server, volume, device=None, server_id=server['id'])) if wait: - for count in _utils._iterate_timeout( + for count in utils.iterate_timeout( timeout, "Timeout waiting for volume %s to attach." % volume['id']): try: @@ -5324,7 +5325,7 @@ def create_volume_snapshot(self, volume_id, force=False, snapshot = self._get_and_munchify('snapshot', data) if wait: snapshot_id = snapshot['id'] - for count in _utils._iterate_timeout( + for count in utils.iterate_timeout( timeout, "Timeout waiting for the volume snapshot to be available." ): @@ -5420,7 +5421,7 @@ def create_volume_backup(self, volume_id, name=None, description=None, backup_id = backup['id'] msg = ("Timeout waiting for the volume backup {} to be " "available".format(backup_id)) - for _ in _utils._iterate_timeout(timeout, msg): + for _ in utils.iterate_timeout(timeout, msg): backup = self.get_volume_backup(backup_id) if backup['status'] == 'available': @@ -5511,7 +5512,7 @@ def delete_volume_backup(self, name_or_id=None, force=False, wait=False, error_message=msg) if wait: msg = "Timeout waiting for the volume backup to be deleted." - for count in _utils._iterate_timeout(timeout, msg): + for count in utils.iterate_timeout(timeout, msg): if not self.get_volume_backup(volume_backup['id']): break @@ -5541,7 +5542,7 @@ def delete_volume_snapshot(self, name_or_id=None, wait=False, error_message="Error in deleting volume snapshot") if wait: - for count in _utils._iterate_timeout( + for count in utils.iterate_timeout( timeout, "Timeout waiting for the volume snapshot to be deleted."): if not self.get_volume_snapshot(volumesnapshot['id']): @@ -5842,7 +5843,7 @@ def _neutron_create_floating_ip( # if we've provided a port as a parameter if wait: try: - for count in _utils._iterate_timeout( + for count in utils.iterate_timeout( timeout, "Timeout waiting for the floating IP" " to be ACTIVE", @@ -6050,7 +6051,7 @@ def _attach_ip_to_server( if wait: # Wait for the address to be assigned to the server server_id = server['id'] - for _ in _utils._iterate_timeout( + for _ in utils.iterate_timeout( timeout, "Timeout waiting for the floating IP to be attached.", wait=self._SERVER_AGE): @@ -6082,7 +6083,7 @@ def _nat_destination_port( timeout = self._PORT_AGE * 2 else: timeout = None - for count in _utils._iterate_timeout( + for count in utils.iterate_timeout( timeout, "Timeout waiting for port to show up in list", wait=self._PORT_AGE): @@ -6869,7 +6870,7 @@ def wait_for_server( start_time = time.time() # There is no point in iterating faster than the list_servers cache - for count in _utils._iterate_timeout( + for count in utils.iterate_timeout( timeout, timeout_message, # if _SERVER_AGE is 0 we still want to wait a bit @@ -6960,7 +6961,7 @@ def rebuild_server(self, server_id, image_id, admin_pass=None, self._normalize_server(server), bare=bare, detailed=detailed) admin_pass = server.get('adminPass') or admin_pass - for count in _utils._iterate_timeout( + for count in utils.iterate_timeout( timeout, "Timeout waiting for server {0} to " "rebuild.".format(server_id), @@ -7119,7 +7120,7 @@ def _delete_server( and self.get_volumes(server)): reset_volume_cache = True - for count in _utils._iterate_timeout( + for count in utils.iterate_timeout( timeout, "Timed out waiting for server to get deleted.", # if _SERVER_AGE is 0 we still want to wait a bit diff --git a/openstack/cloud/operatorcloud.py b/openstack/cloud/operatorcloud.py index 71146d967..ae5ffed39 100644 --- a/openstack/cloud/operatorcloud.py +++ b/openstack/cloud/operatorcloud.py @@ -19,6 +19,7 @@ from openstack.cloud.exc import * # noqa from openstack.cloud import openstackcloud from openstack.cloud import _utils +from openstack import utils class OperatorCloud(openstackcloud.OpenStackCloud): @@ -158,7 +159,7 @@ def inspect_machine(self, name_or_id, wait=False, timeout=3600): with _utils.shade_exceptions("Error inspecting machine"): machine = self.node_set_provision_state(machine['uuid'], 'inspect') if wait: - for count in _utils._iterate_timeout( + for count in utils.iterate_timeout( timeout, "Timeout waiting for node transition to " "target state of 'inspect'"): @@ -277,7 +278,7 @@ def register_machine(self, nics, wait=False, timeout=3600, with _utils.shade_exceptions( "Error transitioning node to available state"): if wait: - for count in _utils._iterate_timeout( + for count in utils.iterate_timeout( timeout, "Timeout waiting for node transition to " "available state"): @@ -313,7 +314,7 @@ def register_machine(self, nics, wait=False, timeout=3600, # Note(TheJulia): We need to wait for the lock to clear # before we attempt to set the machine into provide state # which allows for the transition to available. - for count in _utils._iterate_timeout( + for count in utils.iterate_timeout( lock_timeout, "Timeout waiting for reservation to clear " "before setting provide state"): @@ -409,7 +410,7 @@ def unregister_machine(self, nics, uuid, wait=False, timeout=600): microversion=version) if wait: - for count in _utils._iterate_timeout( + for count in utils.iterate_timeout( timeout, "Timeout waiting for machine to be deleted"): if not self.get_machine(uuid): @@ -635,7 +636,7 @@ def node_set_provision_state(self, error_message=msg, microversion=version) if wait: - for count in _utils._iterate_timeout( + for count in utils.iterate_timeout( timeout, "Timeout waiting for node transition to " "target state of '%s'" % state): @@ -857,7 +858,7 @@ def wait_for_baremetal_node_lock(self, node, timeout=30): else: msg = 'Waiting for lock to be released for node {node}'.format( node=node['uuid']) - for count in _utils._iterate_timeout(timeout, msg, 2): + for count in utils.iterate_timeout(timeout, msg, 2): current_node = self.get_machine(node['uuid']) if current_node['reservation'] is None: return @@ -2001,7 +2002,7 @@ def grant_role(self, name_or_id, user=None, group=None, self._identity_client.put(url, error_message=error_msg) if wait: - for count in _utils._iterate_timeout( + for count in utils.iterate_timeout( timeout, "Timeout waiting for role to be granted"): if self.list_role_assignments(filters=filters): @@ -2080,7 +2081,7 @@ def revoke_role(self, name_or_id, user=None, group=None, self._identity_client.delete(url, error_message=error_msg) if wait: - for count in _utils._iterate_timeout( + for count in utils.iterate_timeout( timeout, "Timeout waiting for role to be revoked"): if not self.list_role_assignments(filters=filters): diff --git a/openstack/resource2.py b/openstack/resource2.py index 0f411f25b..258e196cf 100644 --- a/openstack/resource2.py +++ b/openstack/resource2.py @@ -33,7 +33,6 @@ class that represent a remote resource. The attributes that import collections import itertools -import time from openstack import exceptions from openstack import format @@ -841,7 +840,7 @@ def wait_for_status(session, resource, status, failures, interval, wait): :type resource: :class:`~openstack.resource.Resource` :param status: Desired status of the resource. :param list failures: Statuses that would indicate the transition - failed such as 'ERROR'. + failed such as 'ERROR'. Defaults to ['ERROR']. :param interval: Number of seconds to wait between checks. :param wait: Maximum number of seconds to wait for transition. @@ -856,22 +855,31 @@ def wait_for_status(session, resource, status, failures, interval, wait): if resource.status == status: return resource - total_sleep = 0 if failures is None: - failures = [] - - while total_sleep < wait: - resource.get(session) - if resource.status == status: + failures = ['ERROR'] + + failures = [f.lower() for f in failures] + name = "{res}:{id}".format(res=resource.__class__.__name__, id=resource.id) + msg = "Timeout waiting for {name} to transition to {status}".format( + name=name, status=status) + + for count in utils.iterate_timeout( + timeout=wait, + message=msg, + wait=interval): + resource = resource.get(session) + new_status = resource.status + + if not resource: + raise exceptions.ResourceFailure( + "{name} went away while waiting for {status}".format( + name=name, status=status)) + if new_status.lower() == status.lower(): return resource - if resource.status in failures: - msg = ("Resource %s transitioned to failure state %s" % - (resource.id, resource.status)) - raise exceptions.ResourceFailure(msg) - time.sleep(interval) - total_sleep += interval - msg = "Timeout waiting for %s to transition to %s" % (resource.id, status) - raise exceptions.ResourceTimeout(msg) + if resource.status.lower() in failures: + raise exceptions.ResourceFailure( + "{name} transitioned to failure state {status}".format( + name=name, status=resource.status)) def wait_for_delete(session, resource, interval, wait): @@ -888,13 +896,18 @@ def wait_for_delete(session, resource, interval, wait): :raises: :class:`~openstack.exceptions.ResourceTimeout` transition to status failed to occur in wait seconds. """ - total_sleep = 0 - while total_sleep < wait: + orig_resource = resource + for count in utils.iterate_timeout( + timeout=wait, + message="Timeout waiting for {res}:{id} to delete".format( + res=resource.__class__.__name__, + id=resource.id), + wait=interval): try: - resource.get(session) + resource = resource.get(session) + if not resource: + return orig_resource + if resource.status.lower() == 'deleted': + return resource except exceptions.NotFoundException: - return resource - time.sleep(interval) - total_sleep += interval - msg = "Timeout waiting for %s delete" % (resource.id) - raise exceptions.ResourceTimeout(msg) + return orig_resource diff --git a/openstack/tests/functional/cloud/test_compute.py b/openstack/tests/functional/cloud/test_compute.py index 828f680f0..8e12cd3b2 100644 --- a/openstack/tests/functional/cloud/test_compute.py +++ b/openstack/tests/functional/cloud/test_compute.py @@ -25,7 +25,7 @@ from openstack.cloud import exc from openstack.tests.functional.cloud import base from openstack.tests.functional.cloud.util import pick_flavor -from openstack.cloud import _utils +from openstack import utils class TestCompute(base.BaseFunctionalTestCase): @@ -293,7 +293,7 @@ def _wait_for_detach(self, volume_id): # Volumes do not show up as unattached for a bit immediately after # deleting a server that had had a volume attached. Yay for eventual # consistency! - for count in _utils._iterate_timeout( + for count in utils.iterate_timeout( 60, 'Timeout waiting for volume {volume_id} to detach'.format( volume_id=volume_id)): diff --git a/openstack/tests/functional/cloud/test_floating_ip.py b/openstack/tests/functional/cloud/test_floating_ip.py index aa4e478bc..04a7b418f 100644 --- a/openstack/tests/functional/cloud/test_floating_ip.py +++ b/openstack/tests/functional/cloud/test_floating_ip.py @@ -24,11 +24,11 @@ from testtools import content from openstack import _adapter -from openstack.cloud import _utils from openstack.cloud import meta from openstack.cloud.exc import OpenStackCloudException from openstack.tests.functional.cloud import base from openstack.tests.functional.cloud.util import pick_flavor +from openstack import utils class TestFloatingIP(base.BaseFunctionalTestCase): @@ -195,7 +195,7 @@ def test_add_auto_ip(self): # ToDo: remove the following iteration when create_server waits for # the IP to be attached ip = None - for _ in _utils._iterate_timeout( + for _ in utils.iterate_timeout( self.timeout, "Timeout waiting for IP address to be attached"): ip = meta.get_server_external_ipv4(self.user_cloud, new_server) if ip is not None: @@ -215,7 +215,7 @@ def test_detach_ip_from_server(self): # ToDo: remove the following iteration when create_server waits for # the IP to be attached ip = None - for _ in _utils._iterate_timeout( + for _ in utils.iterate_timeout( self.timeout, "Timeout waiting for IP address to be attached"): ip = meta.get_server_external_ipv4(self.user_cloud, new_server) if ip is not None: diff --git a/openstack/tests/functional/cloud/test_volume.py b/openstack/tests/functional/cloud/test_volume.py index 21a3ebb5e..ee695bbe8 100644 --- a/openstack/tests/functional/cloud/test_volume.py +++ b/openstack/tests/functional/cloud/test_volume.py @@ -20,9 +20,9 @@ from fixtures import TimeoutException from testtools import content -from openstack.cloud import _utils from openstack.cloud import exc from openstack.tests.functional.cloud import base +from openstack import utils class TestVolume(base.BaseFunctionalTestCase): @@ -107,7 +107,7 @@ def cleanup(self, volume, snapshot_name=None, image_name=None): for v in volume: self.user_cloud.delete_volume(v, wait=False) try: - for count in _utils._iterate_timeout( + for count in utils.iterate_timeout( 180, "Timeout waiting for volume cleanup"): found = False for existing in self.user_cloud.list_volumes(): diff --git a/openstack/tests/unit/cloud/test_shade.py b/openstack/tests/unit/cloud/test_shade.py index 6283cf13e..6b3be1387 100644 --- a/openstack/tests/unit/cloud/test_shade.py +++ b/openstack/tests/unit/cloud/test_shade.py @@ -16,10 +16,10 @@ import testtools import openstack.cloud -from openstack.cloud import _utils from openstack.cloud import exc from openstack.tests import fakes from openstack.tests.unit import base +from openstack import utils RANGE_DATA = [ @@ -193,13 +193,13 @@ def test_iterate_timeout_bad_wait(self): with testtools.ExpectedException( exc.OpenStackCloudException, "Wait value must be an int or float value."): - for count in _utils._iterate_timeout( + for count in utils.iterate_timeout( 1, "test_iterate_timeout_bad_wait", wait="timeishard"): pass @mock.patch('time.sleep') def test_iterate_timeout_str_wait(self, mock_sleep): - iter = _utils._iterate_timeout( + iter = utils.iterate_timeout( 10, "test_iterate_timeout_str_wait", wait="1.6") next(iter) next(iter) @@ -207,7 +207,7 @@ def test_iterate_timeout_str_wait(self, mock_sleep): @mock.patch('time.sleep') def test_iterate_timeout_int_wait(self, mock_sleep): - iter = _utils._iterate_timeout( + iter = utils.iterate_timeout( 10, "test_iterate_timeout_int_wait", wait=1) next(iter) next(iter) @@ -219,7 +219,7 @@ def test_iterate_timeout_timeout(self, mock_sleep): with testtools.ExpectedException( exc.OpenStackCloudTimeout, message): - for count in _utils._iterate_timeout(0.1, message, wait=1): + for count in utils.iterate_timeout(0.1, message, wait=1): pass mock_sleep.assert_called_with(1.0) diff --git a/openstack/tests/unit/test_resource2.py b/openstack/tests/unit/test_resource2.py index 28e1c224a..fdfc449e9 100644 --- a/openstack/tests/unit/test_resource2.py +++ b/openstack/tests/unit/test_resource2.py @@ -1424,39 +1424,40 @@ def test_immediate_status(self): self.assertEqual(result, resource) - @mock.patch("time.sleep", return_value=None) - def test_status_match(self, mock_sleep): + def _resources_from_statuses(self, *statuses): + resources = [] + for status in statuses: + res = mock.Mock() + res.status = status + resources.append(res) + for index, res in enumerate(resources[:-1]): + res.get.return_value = resources[index + 1] + return resources + + def test_status_match(self): status = "loling" - resource = mock.Mock() # other gets past the first check, two anothers gets through # the sleep loop, and the third matches - statuses = ["other", "another", "another", status] - type(resource).status = mock.PropertyMock(side_effect=statuses) + resources = self._resources_from_statuses( + "first", "other", "another", "another", status) - result = resource2.wait_for_status("session", resource, status, - None, 1, 5) + result = resource2.wait_for_status( + mock.Mock(), resources[0], status, None, 1, 5) - self.assertEqual(result, resource) + self.assertEqual(result, resources[-1]) - @mock.patch("time.sleep", return_value=None) - def test_status_fails(self, mock_sleep): - status = "loling" + def test_status_fails(self): failure = "crying" - resource = mock.Mock() - # other gets past the first check, the first failure doesn't - # match the expected, the third matches the failure, - # the fourth is used in creating the exception message - statuses = ["other", failure, failure, failure] - type(resource).status = mock.PropertyMock(side_effect=statuses) + resources = self._resources_from_statuses("success", "other", failure) - self.assertRaises(exceptions.ResourceFailure, - resource2.wait_for_status, - "session", resource, status, [failure], 1, 5) + self.assertRaises( + exceptions.ResourceFailure, + resource2.wait_for_status, + mock.Mock(), resources[0], "loling", [failure], 1, 5) - @mock.patch("time.sleep", return_value=None) - def test_timeout(self, mock_sleep): + def test_timeout(self): status = "loling" resource = mock.Mock() @@ -1483,8 +1484,7 @@ def test_no_sleep(self): class TestWaitForDelete(base.TestCase): - @mock.patch("time.sleep", return_value=None) - def test_success(self, mock_sleep): + def test_success(self): response = mock.Mock() response.headers = {} response.status_code = 404 @@ -1497,11 +1497,11 @@ def test_success(self, mock_sleep): self.assertEqual(result, resource) - @mock.patch("time.sleep", return_value=None) - def test_timeout(self, mock_sleep): + def test_timeout(self): resource = mock.Mock() - resource.get.side_effect = [None, None, None] + resource.status = 'ACTIVE' + resource.get.return_value = resource self.assertRaises(exceptions.ResourceTimeout, resource2.wait_for_delete, - "session", resource, 1, 3) + "session", resource, 0.1, 0.3) diff --git a/openstack/utils.py b/openstack/utils.py index f14c87e3b..606705531 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -12,9 +12,12 @@ import functools import logging +import time import deprecation +from openstack import _log +from openstack import exceptions from openstack import version @@ -113,3 +116,39 @@ def urljoin(*args): link. We generally won't care about that in client. """ return '/'.join(str(a or '').strip('/') for a in args) + + +def iterate_timeout(timeout, message, wait=2): + """Iterate and raise an exception on timeout. + + This is a generator that will continually yield and sleep for + wait seconds, and if the timeout is reached, will raise an exception + with . + + """ + log = _log.setup_logging('openstack.iterate_timeout') + + try: + # None as a wait winds up flowing well in the per-resource cache + # flow. We could spread this logic around to all of the calling + # points, but just having this treat None as "I don't have a value" + # seems friendlier + if wait is None: + wait = 2 + elif wait == 0: + # wait should be < timeout, unless timeout is None + wait = 0.1 if timeout is None else min(0.1, timeout) + wait = float(wait) + except ValueError: + raise exceptions.SDKException( + "Wait value must be an int or float value. {wait} given" + " instead".format(wait=wait)) + + start = time.time() + count = 0 + while (timeout is None) or (time.time() < start + timeout): + count += 1 + yield count + log.debug('Waiting %s seconds', wait) + time.sleep(wait) + raise exceptions.ResourceTimeout(message) From e068d891ed1fbaf2b7ed01fc6a4eba29385f3ba7 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 31 Dec 2017 11:58:00 -0600 Subject: [PATCH 1895/3836] Prefer links dicts for pagination The current pagination logic makes call to look for additional items at times even if it's not needed, as it is only able to infer when to make an additional call or not if the user is requesting a limit value in their call. However, calls to list() without limit don't do the right thing. If the user does list() with no limit then list will always have to make an unneeded additional call because it has no limit to compare against. This additional call became evident in the next patch when attempting to use sdk primitives in the shade layer and for applications like nodepool would result in unacceptably higher traffic. Luckily, there are mechanisms available that can be checked to see if pagination is needed. Nova, Cinder and other Nova-like projects provide a top level links dict named {resources_key}_links. The api-wg recommendation is for a top level dict to be provided named 'links'. Glance provides a top-level key named 'next'. Swift returns the total number of objects in a container in a header. The first three can be looked for with no additional options but simply by inspecting the payload. The Swift case requires that we add an optional "pagination_key" option to Resource. Finally, we can keep the existing logic that applies when a limit was given and the number of resources returned exactly matches the limit as a heuristic for making an additional call for resources that do not otherwise return next links. Since most resources that support pagination should reutnr a next link, the fallback logic should only rarely be triggered. If anyone encounters a situation with an unacceptable additional call cost that stems from a service that does not return next links, we can use that as motivation to get the service in question fixed. Change-Id: I7a015f05fb43b63805a5aa9f7e266069d5e52438 --- openstack/resource2.py | 112 +++++-- openstack/tests/unit/meter/v2/test_sample.py | 1 + .../tests/unit/network/v2/test_floating_ip.py | 2 + openstack/tests/unit/test_resource2.py | 293 ++++++++++++++++-- 4 files changed, 354 insertions(+), 54 deletions(-) diff --git a/openstack/resource2.py b/openstack/resource2.py index 258e196cf..716b31604 100644 --- a/openstack/resource2.py +++ b/openstack/resource2.py @@ -216,6 +216,8 @@ class Resource(object): resource_key = None #: Plural form of key for resource. resources_key = None + #: Key used for pagination links + pagination_key = None #: The ID of this resource. id = Body("id") @@ -726,45 +728,111 @@ def list(cls, session, paginated=False, **params): if not cls.allow_list: raise exceptions.MethodNotSupported(cls, "list") - more_data = True query_params = cls._query_mapping._transpose(params) uri = cls.base_path % params - while more_data: - resp = session.get(uri, - headers={"Accept": "application/json"}, - params=query_params) - resp = resp.json() + limit = query_params.get('limit') + + # Track the total number of resources yielded so we can paginate + # swift objects + total_yielded = 0 + while uri: + # Copy query_params due to weird mock unittest interactions + response = session.get( + uri, + headers={"Accept": "application/json"}, + params=query_params.copy()) + exceptions.raise_from_response(response) + data = response.json() + + # Discard any existing pagination keys + query_params.pop('marker', None) + query_params.pop('limit', None) + if cls.resources_key: - resp = resp[cls.resources_key] + resources = data[cls.resources_key] + else: + resources = data - if not resp: - more_data = False + if not isinstance(resources, list): + resources = [resources] - # Keep track of how many items we've yielded. If we yielded - # less than our limit, we don't need to do an extra request - # to get back an empty data set, which acts as a sentinel. + # Keep track of how many items we've yielded. The server should + # handle this, but it's easy for us to as well. yielded = 0 - new_marker = None - for data in resp: + marker = None + for raw_resource in resources: # Do not allow keys called "self" through. Glance chose # to name a key "self", so we need to pop it out because # we can't send it through cls.existing and into the # Resource initializer. "self" is already the first # argument and is practically a reserved word. - data.pop("self", None) + raw_resource.pop("self", None) - value = cls.existing(**data) - new_marker = value.id - yielded += 1 + value = cls.existing(**raw_resource) + marker = value.id yield value + yielded += 1 + total_yielded += 1 - if not paginated: + # If a limit was given by the user and we have not returned + # as many records as the limit, then it stands to reason that + # there are no more records to return and we don't need to do + # anything else. + if limit and yielded < limit: return - if "limit" in query_params and yielded < query_params["limit"]: + + if resources and paginated: + uri, next_params = cls._get_next_link( + uri, response, data, marker, limit, total_yielded) + query_params.update(next_params) + else: return - query_params["limit"] = yielded - query_params["marker"] = new_marker + + @classmethod + def _get_next_link(cls, uri, response, data, marker, limit, total_yielded): + next_link = None + params = {} + if isinstance(data, dict): + pagination_key = cls.pagination_key + + if not pagination_key and 'links' in data: + # api-wg guidelines are for a links dict in the main body + pagination_key == 'links' + if not pagination_key and cls.resources_key: + # Nova has a {key}_links dict in the main body + pagination_key = '{key}_links'.format(key=cls.resources_key) + if pagination_key: + links = data.get(pagination_key, {}) + for item in links: + if item.get('rel') == 'next' and 'href' in item: + next_link = item['href'] + break + # Glance has a next field in the main body + next_link = next_link or data.get('next') + if not next_link and 'next' in response.links: + # RFC5988 specifies Link headers and requests parses them if they + # are there. We prefer link dicts in resource body, but if those + # aren't there and Link headers are, use them. + next_link = response.links['next']['uri'] + # Swift provides a count of resources in a header and a list body + if not next_link and cls.pagination_key: + total_count = response.headers.get(cls.pagination_key) + if total_count: + total_count = int(total_count) + if total_count > total_yielded: + params['marker'] = marker + if limit: + params['limit'] = limit + next_link = uri + # If we still have no link, and limit was given and is non-zero, + # and the number of records yielded equals the limit, then the user + # is playing pagination ball so we should go ahead and try once more. + if not next_link and limit: + next_link = uri + params['marker'] = marker + params['limit'] = limit + return next_link, params @classmethod def _get_one_match(cls, name_or_id, results): diff --git a/openstack/tests/unit/meter/v2/test_sample.py b/openstack/tests/unit/meter/v2/test_sample.py index ee2588512..8f7faf729 100644 --- a/openstack/tests/unit/meter/v2/test_sample.py +++ b/openstack/tests/unit/meter/v2/test_sample.py @@ -65,6 +65,7 @@ def test_list(self): sess = mock.Mock() resp = mock.Mock() resp.json = mock.Mock(return_value=[SAMPLE]) + resp.status_code = 200 sess.get = mock.Mock(return_value=resp) found = sample.Sample.list(sess, counter_name='name_of_meter') diff --git a/openstack/tests/unit/network/v2/test_floating_ip.py b/openstack/tests/unit/network/v2/test_floating_ip.py index 0005f13d7..0ae513989 100644 --- a/openstack/tests/unit/network/v2/test_floating_ip.py +++ b/openstack/tests/unit/network/v2/test_floating_ip.py @@ -73,6 +73,7 @@ def test_find_available(self): fake_response = mock.Mock() body = {floating_ip.FloatingIP.resources_key: [data]} fake_response.json = mock.Mock(return_value=body) + fake_response.status_code = 200 mock_session.get = mock.Mock(return_value=fake_response) result = floating_ip.FloatingIP.find_available(mock_session) @@ -88,6 +89,7 @@ def test_find_available_nada(self): fake_response = mock.Mock() body = {floating_ip.FloatingIP.resources_key: []} fake_response.json = mock.Mock(return_value=body) + fake_response.status_code = 200 mock_session.get = mock.Mock(return_value=fake_response) self.assertIsNone(floating_ip.FloatingIP.find_available(mock_session)) diff --git a/openstack/tests/unit/test_resource2.py b/openstack/tests/unit/test_resource2.py index fdfc449e9..a69b88dc0 100644 --- a/openstack/tests/unit/test_resource2.py +++ b/openstack/tests/unit/test_resource2.py @@ -938,6 +938,7 @@ def setUp(self): class Test(resource2.Resource): service = self.service_name base_path = self.base_path + resources_key = 'resources' allow_create = True allow_get = True allow_head = True @@ -1096,7 +1097,9 @@ def test_delete(self): # the generator. Wrap calls to self.sot.list in a `list` # and then test the results as a list of responses. def test_list_empty_response(self): - mock_response = FakeResponse([]) + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"resources": []} self.session.get.return_value = mock_response @@ -1112,8 +1115,9 @@ def test_list_empty_response(self): def test_list_one_page_response_paginated(self): id_value = 1 mock_response = mock.Mock() - mock_response.json.side_effect = [[{"id": id_value}], - []] + mock_response.status_code = 200 + mock_response.links = {} + mock_response.json.return_value = {"resources": [{"id": id_value}]} self.session.get.return_value = mock_response @@ -1123,17 +1127,15 @@ def test_list_one_page_response_paginated(self): self.assertEqual(1, len(results)) - # Look at the `params` argument to each of the get calls that - # were made. - self.session.get.call_args_list[0][1]["params"] = {} - self.session.get.call_args_list[1][1]["params"] = {"marker": id_value} + self.assertEqual(1, len(self.session.get.call_args_list)) self.assertEqual(id_value, results[0].id) self.assertIsInstance(results[0], self.test_class) def test_list_one_page_response_not_paginated(self): id_value = 1 mock_response = mock.Mock() - mock_response.json.return_value = [{"id": id_value}] + mock_response.status_code = 200 + mock_response.json.return_value = {"resources": [{"id": id_value}]} self.session.get.return_value = mock_response @@ -1156,6 +1158,7 @@ class Test(self.test_class): id_value = 1 mock_response = mock.Mock() + mock_response.status_code = 200 mock_response.json.return_value = {key: [{"id": id_value}]} self.session.get.return_value = mock_response @@ -1173,11 +1176,85 @@ class Test(self.test_class): self.assertEqual(id_value, results[0].id) self.assertIsInstance(results[0], self.test_class) + def test_list_response_paginated_without_links(self): + ids = [1, 2] + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.links = {} + mock_response.json.return_value = { + "resources": [{"id": ids[0]}], + "resources_links": [{ + "href": "https://example.com/next-url", + "rel": "next", + }] + } + mock_response2 = mock.Mock() + mock_response2.status_code = 200 + mock_response2.links = {} + mock_response2.json.return_value = { + "resources": [{"id": ids[1]}], + } + + self.session.get.side_effect = [mock_response, mock_response2] + + results = list(self.sot.list(self.session, paginated=True)) + + self.assertEqual(2, len(results)) + self.assertEqual(ids[0], results[0].id) + self.assertEqual(ids[1], results[1].id) + self.assertEqual( + mock.call('base_path', + headers={'Accept': 'application/json'}, params={}), + self.session.get.mock_calls[0]) + self.assertEqual( + mock.call('https://example.com/next-url', + headers={'Accept': 'application/json'}, params={}), + self.session.get.mock_calls[1]) + self.assertEqual(2, len(self.session.get.call_args_list)) + self.assertIsInstance(results[0], self.test_class) + + def test_list_response_paginated_with_links(self): + ids = [1, 2] + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.links = {} + mock_response.json.side_effect = [ + { + "resources": [{"id": ids[0]}], + "resources_links": [{ + "href": "https://example.com/next-url", + "rel": "next", + }] + }, { + "resources": [{"id": ids[1]}], + }] + + self.session.get.return_value = mock_response + + results = list(self.sot.list(self.session, paginated=True)) + + self.assertEqual(2, len(results)) + self.assertEqual(ids[0], results[0].id) + self.assertEqual(ids[1], results[1].id) + self.assertEqual( + mock.call('base_path', + headers={'Accept': 'application/json'}, params={}), + self.session.get.mock_calls[0]) + self.assertEqual( + mock.call('https://example.com/next-url', + headers={'Accept': 'application/json'}, params={}), + self.session.get.mock_calls[2]) + self.assertEqual(2, len(self.session.get.call_args_list)) + self.assertIsInstance(results[0], self.test_class) + def test_list_multi_page_response_not_paginated(self): ids = [1, 2] mock_response = mock.Mock() - mock_response.json.side_effect = [[{"id": ids[0]}], - [{"id": ids[1]}]] + mock_response.status_code = 200 + mock_response.json.side_effect = [ + {"resources": [{"id": ids[0]}]}, + {"resources": [{"id": ids[1]}]}, + ] self.session.get.return_value = mock_response @@ -1194,10 +1271,16 @@ def test_list_query_params(self): uri_param = "uri param!" mock_response = mock.Mock() - mock_response.json.side_effect = [[{"id": id}], - []] + mock_response.status_code = 200 + mock_response.links = {} + mock_response.json.return_value = {"resources": [{"id": id}]} - self.session.get.return_value = mock_response + mock_empty = mock.Mock() + mock_empty.status_code = 200 + mock_empty.links = {} + mock_empty.json.return_value = {"resources": []} + + self.session.get.side_effect = [mock_response, mock_empty] class Test(self.test_class): _query_mapping = resource2.QueryParameters(query_param=qp_name) @@ -1217,19 +1300,33 @@ class Test(self.test_class): Test.base_path % {"something": uri_param}) def test_list_multi_page_response_paginated(self): - # This tests our ability to stop making calls once - # we've received all of the data. However, this tests - # the case that we always receive full pages of data - # and then the signal that there is no more data - an empty list. - # In this case, we need to make one extra request beyond - # the end of data to ensure we've received it all. ids = [1, 2] resp1 = mock.Mock() - resp1.json.return_value = [{"id": ids[0]}] + resp1.status_code = 200 + resp1.links = {} + resp1.json.return_value = { + "resources": [{"id": ids[0]}], + "resources_links": [{ + "href": "https://example.com/next-url", + "rel": "next", + }], + } resp2 = mock.Mock() - resp2.json.return_value = [{"id": ids[1]}] + resp2.status_code = 200 + resp2.links = {} + resp2.json.return_value = { + "resources": [{"id": ids[1]}], + "resources_links": [{ + "href": "https://example.com/next-url", + "rel": "next", + }], + } resp3 = mock.Mock() - resp3.json.return_value = [] + resp3.status_code = 200 + resp3.links = {} + resp3.json.return_value = { + "resources": [] + } self.session.get.side_effect = [resp1, resp2, resp3] @@ -1245,26 +1342,113 @@ def test_list_multi_page_response_paginated(self): result1 = next(results) self.assertEqual(result1.id, ids[1]) self.session.get.assert_called_with( - self.base_path, + 'https://example.com/next-url', headers={"Accept": "application/json"}, - params={"limit": 1, "marker": 1}) + params={}) self.assertRaises(StopIteration, next, results) self.session.get.assert_called_with( - self.base_path, + 'https://example.com/next-url', headers={"Accept": "application/json"}, - params={"limit": 1, "marker": 2}) + params={}) def test_list_multi_page_early_termination(self): # This tests our ability to be somewhat smart when evaluating - # the contents of the responses. When we receive a full page - # of data, we can be smart about terminating our responses - # once we see that we've received a page with less data than - # expected, saving one request. + # the contents of the responses. When we request a limit and + # receive less than that limit and there are no next links, + # we can be pretty sure there are no more pages. + ids = [1, 2] + resp1 = mock.Mock() + resp1.status_code = 200 + resp1.json.return_value = { + "resources": [{"id": ids[0]}, {"id": ids[1]}], + } + + self.session.get.return_value = resp1 + + results = self.sot.list(self.session, limit=3, paginated=True) + + result0 = next(results) + self.assertEqual(result0.id, ids[0]) + result1 = next(results) + self.assertEqual(result1.id, ids[1]) + self.session.get.assert_called_with( + self.base_path, + headers={"Accept": "application/json"}, + params={"limit": 3}) + + # Ensure we're done after those two items + self.assertRaises(StopIteration, next, results) + + # Ensure we only made one calls to get this done + self.assertEqual(1, len(self.session.get.call_args_list)) + + def test_list_multi_page_inferred_additional(self): + # If we explicitly request a limit and we receive EXACTLY that + # amount of results and there is no next link, we make one additional + # call to check to see if there are more records and the service is + # just sad. + # NOTE(mordred) In a perfect world we would not do this. But it's 2018 + # and I don't think anyone has any illusions that we live in a perfect + # world anymore. ids = [1, 2, 3] resp1 = mock.Mock() + resp1.status_code = 200 + resp1.links = {} + resp1.json.return_value = { + "resources": [{"id": ids[0]}, {"id": ids[1]}], + } + resp2 = mock.Mock() + resp2.status_code = 200 + resp2.links = {} + resp2.json.return_value = {"resources": [{"id": ids[2]}]} + + self.session.get.side_effect = [resp1, resp2] + + results = self.sot.list(self.session, limit=2, paginated=True) + + # Get the first page's two items + result0 = next(results) + self.assertEqual(result0.id, ids[0]) + result1 = next(results) + self.assertEqual(result1.id, ids[1]) + self.session.get.assert_called_with( + self.base_path, + headers={"Accept": "application/json"}, + params={"limit": 2}) + + result2 = next(results) + self.assertEqual(result2.id, ids[2]) + self.session.get.assert_called_with( + self.base_path, + headers={"Accept": "application/json"}, + params={'limit': 2, 'marker': 2}) + + # Ensure we're done after those three items + self.assertRaises(StopIteration, next, results) + + # Ensure we only made two calls to get this done + self.assertEqual(2, len(self.session.get.call_args_list)) + + def test_list_multi_page_header_count(self): + class Test(self.test_class): + resources_key = None + pagination_key = 'X-Container-Object-Count' + self.sot = Test() + + # Swift returns a total number of objects in a header and we compare + # that against the total number returned to know if we need to fetch + # more objects. + ids = [1, 2, 3] + resp1 = mock.Mock() + resp1.status_code = 200 + resp1.links = {} + resp1.headers = {'X-Container-Object-Count': 3} resp1.json.return_value = [{"id": ids[0]}, {"id": ids[1]}] resp2 = mock.Mock() + resp2.status_code = 200 + resp2.links = {} + resp2.headers = {'X-Container-Object-Count': 3} resp2.json.return_value = [{"id": ids[2]}] self.session.get.side_effect = [resp1, resp2] @@ -1281,13 +1465,58 @@ def test_list_multi_page_early_termination(self): headers={"Accept": "application/json"}, params={}) - # Second page only has one item result2 = next(results) self.assertEqual(result2.id, ids[2]) self.session.get.assert_called_with( self.base_path, headers={"Accept": "application/json"}, - params={"limit": 2, "marker": 2}) + params={'marker': 2}) + + # Ensure we're done after those three items + self.assertRaises(StopIteration, next, results) + + # Ensure we only made two calls to get this done + self.assertEqual(2, len(self.session.get.call_args_list)) + + def test_list_multi_page_link_header(self): + # Swift returns a total number of objects in a header and we compare + # that against the total number returned to know if we need to fetch + # more objects. + ids = [1, 2, 3] + resp1 = mock.Mock() + resp1.status_code = 200 + resp1.links = { + 'next': {'uri': 'https://example.com/next-url', 'rel': 'next'}} + resp1.headers = {} + resp1.json.return_value = { + "resources": [{"id": ids[0]}, {"id": ids[1]}], + } + resp2 = mock.Mock() + resp2.status_code = 200 + resp2.links = {} + resp2.headers = {} + resp2.json.return_value = {"resources": [{"id": ids[2]}]} + + self.session.get.side_effect = [resp1, resp2] + + results = self.sot.list(self.session, paginated=True) + + # Get the first page's two items + result0 = next(results) + self.assertEqual(result0.id, ids[0]) + result1 = next(results) + self.assertEqual(result1.id, ids[1]) + self.session.get.assert_called_with( + self.base_path, + headers={"Accept": "application/json"}, + params={}) + + result2 = next(results) + self.assertEqual(result2.id, ids[2]) + self.session.get.assert_called_with( + 'https://example.com/next-url', + headers={"Accept": "application/json"}, + params={}) # Ensure we're done after those three items self.assertRaises(StopIteration, next, results) From cd9bd1ef39f6d45f4d9511269b5c37f6aa642ea5 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 9 Jan 2018 16:35:46 -0600 Subject: [PATCH 1896/3836] Remove legacy client factory functions os-client-config provided helper functions for constructing legacy python-*client objects, as well as Connection and OpenStackCloud objects. openstacksdk isn't in the business of constructing python-*client objects for people, so let's remove these. We also dont' need helper functions in the openstack.config space for constructing Connection since we already have openstack.connect for that (and we'll fold OpenStackCloud in soon enough) Change-Id: I09e4fe258ff58a8e8aec5919369ff856a5102b76 --- doc/source/user/config/using.rst | 83 ----- openstack/cloud/meta.py | 15 - openstack/cloud/openstackcloud.py | 20 -- openstack/config/__init__.py | 54 +--- openstack/config/cloud_region.py | 178 ----------- openstack/config/constructors.json | 16 - openstack/config/constructors.py | 36 --- openstack/tests/functional/base.py | 2 +- .../tests/unit/config/test_cloud_config.py | 302 ------------------ openstack/tests/unit/config/test_init.py | 8 +- test-requirements.txt | 2 - 11 files changed, 6 insertions(+), 710 deletions(-) delete mode 100644 openstack/config/constructors.json delete mode 100644 openstack/config/constructors.py diff --git a/doc/source/user/config/using.rst b/doc/source/user/config/using.rst index 1e81b4b4f..7792989cf 100644 --- a/doc/source/user/config/using.rst +++ b/doc/source/user/config/using.rst @@ -56,86 +56,3 @@ with - as well as a consumption argument. options = parser.parse_args() cloud_region = config.get_one(argparse=options) - -Constructing a Connection object --------------------------------- - -If what you want to do is get an `openstack.connection.Connection` and you -want it to do all the normal things related to clouds.yaml, `OS_` environment -variables, a helper function is provided. The following will get you a fully -configured `openstacksdk` instance. - -.. code-block:: python - - import openstack.config - - conn = openstack.config.make_connection() - -If you want to do the same thing but on a named cloud. - -.. code-block:: python - - import openstack.config - - conn = openstack.config.make_connection(cloud='mtvexx') - -If you want to do the same thing but also support command line parsing. - -.. code-block:: python - - import argparse - - import openstack.config - - conn = openstack.config.make_connection(options=argparse.ArgumentParser()) - -Constructing OpenStackCloud objects ------------------------------------ - -If what you want to do is get an -`opentack.cloud.openstackcloud.OpenStackCloud` object, a -helper function that honors clouds.yaml and `OS_` environment variables is -provided. The following will get you a fully configured `OpenStackCloud` -instance. - -.. code-block:: python - - import openstack.config - - cloud = openstack.config.make_cloud() - -If you want to do the same thing but on a named cloud. - -.. code-block:: python - - import openstack.config - - cloud = openstack.config.make_cloud(cloud='mtvexx') - -If you want to do the same thing but also support command line parsing. - -.. code-block:: python - - import argparse - - import openstack.config - - cloud = openstack.config.make_cloud(options=argparse.ArgumentParser()) - -Constructing REST API Clients ------------------------------ - -What if you want to make direct REST calls via a Session interface? You're -in luck. A similar interface is available as with `openstacksdk` and `shade`. -The main difference is that you need to specify which service you want to -talk to and `make_rest_client` will return you a keystoneauth Session object -that is mounted on the endpoint for the service you're looking for. - -.. code-block:: python - - import openstack.config - - session = openstack.config.make_rest_client('compute', cloud='vexxhost') - - response = session.get('/servers') - server_list = response.json()['servers'] diff --git a/openstack/cloud/meta.py b/openstack/cloud/meta.py index 957fea6a8..35bc242d2 100644 --- a/openstack/cloud/meta.py +++ b/openstack/cloud/meta.py @@ -542,21 +542,6 @@ def obj_list_to_munch(obj_list): obj_list_to_dict = obj_list_to_munch -def warlock_to_dict(obj): - # This function is unused in shade - but it is a public function, so - # removing it would be rude. We don't actually have to depend on warlock - # ourselves to keep this - so just leave it here. - # - # glanceclient v2 uses warlock to construct its objects. Warlock does - # deep black magic to attribute look up to support validation things that - # means we cannot use normal obj_to_munch - obj_dict = munch.Munch() - for (key, value) in obj.items(): - if isinstance(value, NON_CALLABLES) and not key.startswith('_'): - obj_dict[key] = value - return obj_dict - - def get_and_munchify(key, data): """Get the value associated to key and convert it. diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 90fa03d56..9a3103f66 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -459,26 +459,6 @@ def _get_cache(self, resource_name): else: return self._cache - def _get_client( - self, service_key, client_class=None, interface_key=None, - pass_version_arg=True, **kwargs): - try: - client = self.cloud_config.get_legacy_client( - service_key=service_key, client_class=client_class, - interface_key=interface_key, pass_version_arg=pass_version_arg, - **kwargs) - except Exception: - self.log.debug( - "Couldn't construct %(service)s object", - {'service': service_key}, exc_info=True) - raise - if client is None: - raise OpenStackCloudException( - "Failed to instantiate {service} client." - " This could mean that your credentials are wrong.".format( - service=service_key)) - return client - def _get_major_version_id(self, version): if isinstance(version, int): return version diff --git a/openstack/config/__init__.py b/openstack/config/__init__.py index e50e7f9db..4c4547c14 100644 --- a/openstack/config/__init__.py +++ b/openstack/config/__init__.py @@ -19,7 +19,7 @@ _config = None -def get_config( +def get_cloud_region( service_key=None, options=None, app_name=None, app_version=None, **kwargs): @@ -36,55 +36,3 @@ def get_config( parsed_options = None return _config.get_one(options=parsed_options, **kwargs) - - -def make_rest_client( - service_key, options=None, - app_name=None, app_version=None, version=None, - **kwargs): - """Simple wrapper function. It has almost no features. - - This will get you a raw requests Session Adapter that is mounted - on the given service from the keystone service catalog. If you leave - off cloud and region_name, it will assume that you've got env vars - set, but if you give them, it'll use clouds.yaml as you'd expect. - - This function is deliberately simple. It has no flexibility. If you - want flexibility, you can make a cloud config object and call - get_session_client on it. This function is to make it easy to poke - at OpenStack REST APIs with a properly configured keystone session. - """ - cloud_region = get_config( - service_key=service_key, options=options, - app_name=app_name, app_version=app_version, - **kwargs) - return cloud_region.get_session_client(service_key, version=version) -# Backwards compat - simple_client was a terrible name -simple_client = make_rest_client -# Backwards compat - session_client was a terrible name -session_client = make_rest_client - - -def make_connection(options=None, **kwargs): - """Simple wrapper for getting an OpenStack SDK Connection. - - For completeness, provide a mechanism that matches make_client and - make_rest_client. The heavy lifting here is done in openstacksdk. - - :rtype: :class:`~openstack.connection.Connection` - """ - from openstack import connection - cloud_region = get_config(options=options, **kwargs) - return connection.from_config(cloud_region=cloud_region, options=options) - - -def make_cloud(options=None, **kwargs): - """Simple wrapper for getting an OpenStackCloud object - - A mechanism that matches make_connection and make_rest_client. - - :rtype: :class:`~openstack.OpenStackCloud` - """ - import openstack.cloud - cloud_region = get_config(options=options, **kwargs) - return openstack.OpenStackCloud(cloud_config=cloud_region, **kwargs) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 8119fdf72..ffef7f60e 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -12,7 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. -import importlib import math import warnings @@ -24,44 +23,9 @@ import openstack from openstack import _log -from openstack.config import constructors from openstack.config import exceptions -def _get_client(service_key): - class_mapping = constructors.get_constructor_mapping() - if service_key not in class_mapping: - raise exceptions.OpenStackConfigException( - "Service {service_key} is unkown. Please pass in a client" - " constructor or submit a patch to os-client-config".format( - service_key=service_key)) - mod_name, ctr_name = class_mapping[service_key].rsplit('.', 1) - lib_name = mod_name.split('.')[0] - try: - mod = importlib.import_module(mod_name) - except ImportError: - raise exceptions.OpenStackConfigException( - "Client for '{service_key}' was requested, but" - " {mod_name} was unable to be imported. Either import" - " the module yourself and pass the constructor in as an argument," - " or perhaps you do not have python-{lib_name} installed.".format( - service_key=service_key, - mod_name=mod_name, - lib_name=lib_name)) - try: - ctr = getattr(mod, ctr_name) - except AttributeError: - raise exceptions.OpenStackConfigException( - "Client for '{service_key}' was requested, but although" - " {mod_name} imported fine, the constructor at {fullname}" - " as not found. Please check your installation, we have no" - " clue what is wrong with your computer.".format( - service_key=service_key, - mod_name=mod_name, - fullname=class_mapping[service_key])) - return ctr - - def _make_key(key, service_type): if not service_type: return key @@ -371,148 +335,6 @@ def get_session_endpoint( kwargs['region_name']) return endpoint - def get_legacy_client( - self, service_key, client_class=None, interface_key=None, - pass_version_arg=True, version=None, min_version=None, - max_version=None, **kwargs): - """Return a legacy OpenStack client object for the given config. - - Most of the OpenStack python-*client libraries have the same - interface for their client constructors, but there are several - parameters one wants to pass given a :class:`CloudRegion` object. - - In the future, OpenStack API consumption should be done through - the OpenStack SDK, but that's not ready yet. This is for getting - Client objects from python-*client only. - - :param service_key: Generic key for service, such as 'compute' or - 'network' - :param client_class: Class of the client to be instantiated. This - should be the unversioned version if there - is one, such as novaclient.client.Client, or - the versioned one, such as - neutronclient.v2_0.client.Client if there isn't - :param interface_key: (optional) Some clients, such as glanceclient - only accept the parameter 'interface' instead - of 'endpoint_type' - this is a get-out-of-jail - parameter for those until they can be aligned. - os-client-config understands this to be the - case if service_key is image, so this is really - only for use with other unknown broken clients. - :param pass_version_arg: (optional) If a versioned Client constructor - was passed to client_class, set this to - False, which will tell get_client to not - pass a version parameter. os-client-config - already understand that this is the - case for network, so it can be omitted in - that case. - :param version: (optional) Version string to override the configured - version string. - :param min_version: (options) Minimum version acceptable. - :param max_version: (options) Maximum version acceptable. - :param kwargs: (optional) keyword args are passed through to the - Client constructor, so this is in case anything - additional needs to be passed in. - """ - if not client_class: - client_class = _get_client(service_key) - - interface = self.get_interface(service_key) - # trigger exception on lack of service - endpoint = self.get_session_endpoint( - service_key, min_version=min_version, max_version=max_version) - endpoint_override = self.get_endpoint(service_key) - - if service_key == 'object-store': - constructor_kwargs = dict( - session=self.get_session(), - os_options=dict( - service_type=self.get_service_type(service_key), - object_storage_url=endpoint_override, - region_name=self.region_name)) - else: - constructor_kwargs = dict( - session=self.get_session(), - service_name=self.get_service_name(service_key), - service_type=self.get_service_type(service_key), - endpoint_override=endpoint_override, - region_name=self.region_name) - - if service_key == 'image': - # os-client-config does not depend on glanceclient, but if - # the user passed in glanceclient.client.Client, which they - # would need to do if they were requesting 'image' - then - # they necessarily have glanceclient installed - from glanceclient.common import utils as glance_utils - endpoint, detected_version = glance_utils.strip_version(endpoint) - # If the user has passed in a version, that's explicit, use it - if not version: - version = detected_version - # If the user has passed in or configured an override, use it. - # Otherwise, ALWAYS pass in an endpoint_override becuase - # we've already done version stripping, so we don't want version - # reconstruction to happen twice - if not endpoint_override: - constructor_kwargs['endpoint_override'] = endpoint - constructor_kwargs.update(kwargs) - if pass_version_arg and service_key != 'object-store': - if not version: - version = self.get_api_version(service_key) - if not version and service_key == 'volume': - from cinderclient import client as cinder_client - version = cinder_client.get_volume_api_from_url(endpoint) - # Temporary workaround while we wait for python-openstackclient - # to be able to handle 2.0 which is what neutronclient expects - if service_key == 'network' and version == '2': - version = '2.0' - if service_key == 'identity': - # Workaround for bug#1513839 - if 'endpoint' not in constructor_kwargs: - endpoint = self.get_session_endpoint('identity') - constructor_kwargs['endpoint'] = endpoint - if service_key == 'network': - constructor_kwargs['api_version'] = version - elif service_key == 'baremetal': - if version != '1': - # Set Ironic Microversion - constructor_kwargs['os_ironic_api_version'] = version - # Version arg is the major version, not the full microstring - constructor_kwargs['version'] = version[0] - else: - constructor_kwargs['version'] = version - if min_version and min_version > float(version): - raise exceptions.OpenStackConfigVersionException( - "Minimum version {min_version} requested but {version}" - " found".format(min_version=min_version, version=version), - version=version) - if max_version and max_version < float(version): - raise exceptions.OpenStackConfigVersionException( - "Maximum version {max_version} requested but {version}" - " found".format(max_version=max_version, version=version), - version=version) - if service_key == 'database': - # TODO(mordred) Remove when https://review.openstack.org/314032 - # has landed and released. We're passing in a Session, but the - # trove Client object has username and password as required - # args - constructor_kwargs['username'] = None - constructor_kwargs['password'] = None - - if not interface_key: - if service_key in ('image', 'key-manager'): - interface_key = 'interface' - elif (service_key == 'identity' - and version and version.startswith('3')): - interface_key = 'interface' - else: - interface_key = 'endpoint_type' - if service_key == 'object-store': - constructor_kwargs['os_options'][interface_key] = interface - else: - constructor_kwargs[interface_key] = interface - - return client_class(**constructor_kwargs) - def get_cache_expiration_time(self): if self._openstack_config: return self._openstack_config.get_cache_expiration_time() diff --git a/openstack/config/constructors.json b/openstack/config/constructors.json deleted file mode 100644 index 9acb7cfb9..000000000 --- a/openstack/config/constructors.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "application-catalog": "muranoclient.client.Client", - "baremetal": "ironicclient.client.Client", - "compute": "novaclient.client.Client", - "container-infra": "magnumclient.client.Client", - "database": "troveclient.client.Client", - "dns": "designateclient.client.Client", - "identity": "keystoneclient.client.Client", - "image": "glanceclient.Client", - "key-manager": "barbicanclient.client.Client", - "metering": "ceilometerclient.client.Client", - "network": "neutronclient.neutron.client.Client", - "object-store": "swiftclient.client.Connection", - "orchestration": "heatclient.client.Client", - "volume": "cinderclient.client.Client" -} diff --git a/openstack/config/constructors.py b/openstack/config/constructors.py deleted file mode 100644 index 579bb2d5e..000000000 --- a/openstack/config/constructors.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright (c) 2014 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. - -import json -import os -import threading - -_json_path = os.path.join( - os.path.dirname(os.path.realpath(__file__)), 'constructors.json') -_class_mapping = None -_class_mapping_lock = threading.Lock() - - -def get_constructor_mapping(): - global _class_mapping - if _class_mapping is not None: - return _class_mapping.copy() - with _class_mapping_lock: - if _class_mapping is not None: - return _class_mapping.copy() - tmp_class_mapping = {} - with open(_json_path, 'r') as json_file: - tmp_class_mapping.update(json.load(json_file)) - _class_mapping = tmp_class_mapping - return tmp_class_mapping.copy() diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index 3614f34d2..c2b5e6308 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -23,7 +23,7 @@ #: will determine where the functional tests will be run and what resource #: defaults will be used to run the functional tests. TEST_CLOUD_NAME = os.getenv('OS_CLOUD', 'devstack-admin') -TEST_CLOUD_REGION = openstack.config.get_config(cloud=TEST_CLOUD_NAME) +TEST_CLOUD_REGION = openstack.config.get_cloud_region(cloud=TEST_CLOUD_NAME) def _get_resource_value(resource_key, default): diff --git a/openstack/tests/unit/config/test_cloud_config.py b/openstack/tests/unit/config/test_cloud_config.py index 6ce77db5b..cf389acad 100644 --- a/openstack/tests/unit/config/test_cloud_config.py +++ b/openstack/tests/unit/config/test_cloud_config.py @@ -278,305 +278,3 @@ def test_session_endpoint_not_found(self, mock_get_session): cc = cloud_region.CloudRegion( "test1", "region-al", {}, auth_plugin=mock.Mock()) self.assertIsNone(cc.get_session_endpoint('notfound')) - - @mock.patch.object(cloud_region.CloudRegion, 'get_api_version') - @mock.patch.object(cloud_region.CloudRegion, 'get_auth_args') - @mock.patch.object(cloud_region.CloudRegion, 'get_session_endpoint') - def test_legacy_client_object_store_password( - self, - mock_get_session_endpoint, - mock_get_auth_args, - mock_get_api_version): - mock_client = mock.Mock() - mock_get_session_endpoint.return_value = 'http://swift.example.com' - mock_get_api_version.return_value = '3' - mock_get_auth_args.return_value = dict( - username='testuser', - password='testpassword', - project_name='testproject', - auth_url='http://example.com', - ) - config_dict = defaults.get_defaults() - config_dict.update(fake_services_dict) - cc = cloud_region.CloudRegion( - "test1", "region-al", config_dict, auth_plugin=mock.Mock()) - cc.get_legacy_client('object-store', mock_client) - mock_client.assert_called_with( - session=mock.ANY, - os_options={ - 'region_name': 'region-al', - 'service_type': 'object-store', - 'object_storage_url': None, - 'endpoint_type': 'public', - }) - - @mock.patch.object(cloud_region.CloudRegion, 'get_auth_args') - @mock.patch.object(cloud_region.CloudRegion, 'get_session_endpoint') - def test_legacy_client_object_store_password_v2( - self, mock_get_session_endpoint, mock_get_auth_args): - mock_client = mock.Mock() - mock_get_session_endpoint.return_value = 'http://swift.example.com' - mock_get_auth_args.return_value = dict( - username='testuser', - password='testpassword', - project_name='testproject', - auth_url='http://example.com', - ) - config_dict = defaults.get_defaults() - config_dict.update(fake_services_dict) - cc = cloud_region.CloudRegion( - "test1", "region-al", config_dict, auth_plugin=mock.Mock()) - cc.get_legacy_client('object-store', mock_client) - mock_client.assert_called_with( - session=mock.ANY, - os_options={ - 'region_name': 'region-al', - 'service_type': 'object-store', - 'object_storage_url': None, - 'endpoint_type': 'public', - }) - - @mock.patch.object(cloud_region.CloudRegion, 'get_auth_args') - @mock.patch.object(cloud_region.CloudRegion, 'get_session_endpoint') - def test_legacy_client_object_store( - self, mock_get_session_endpoint, mock_get_auth_args): - mock_client = mock.Mock() - mock_get_session_endpoint.return_value = 'http://example.com/v2' - mock_get_auth_args.return_value = {} - config_dict = defaults.get_defaults() - config_dict.update(fake_services_dict) - cc = cloud_region.CloudRegion( - "test1", "region-al", config_dict, auth_plugin=mock.Mock()) - cc.get_legacy_client('object-store', mock_client) - mock_client.assert_called_with( - session=mock.ANY, - os_options={ - 'region_name': 'region-al', - 'service_type': 'object-store', - 'object_storage_url': None, - 'endpoint_type': 'public', - }) - - @mock.patch.object(cloud_region.CloudRegion, 'get_auth_args') - @mock.patch.object(cloud_region.CloudRegion, 'get_session_endpoint') - def test_legacy_client_object_store_timeout( - self, mock_get_session_endpoint, mock_get_auth_args): - mock_client = mock.Mock() - mock_get_session_endpoint.return_value = 'http://example.com/v2' - mock_get_auth_args.return_value = {} - config_dict = defaults.get_defaults() - config_dict.update(fake_services_dict) - config_dict['api_timeout'] = 9 - cc = cloud_region.CloudRegion( - "test1", "region-al", config_dict, auth_plugin=mock.Mock()) - cc.get_legacy_client('object-store', mock_client) - mock_client.assert_called_with( - session=mock.ANY, - os_options={ - 'region_name': 'region-al', - 'service_type': 'object-store', - 'object_storage_url': None, - 'endpoint_type': 'public', - }) - - @mock.patch.object(cloud_region.CloudRegion, 'get_auth_args') - def test_legacy_client_object_store_endpoint( - self, mock_get_auth_args): - mock_client = mock.Mock() - mock_get_auth_args.return_value = {} - config_dict = defaults.get_defaults() - config_dict.update(fake_services_dict) - config_dict['object_store_endpoint'] = 'http://example.com/swift' - cc = cloud_region.CloudRegion( - "test1", "region-al", config_dict, auth_plugin=mock.Mock()) - cc.get_legacy_client('object-store', mock_client) - mock_client.assert_called_with( - session=mock.ANY, - os_options={ - 'region_name': 'region-al', - 'service_type': 'object-store', - 'object_storage_url': 'http://example.com/swift', - 'endpoint_type': 'public', - }) - - @mock.patch.object(cloud_region.CloudRegion, 'get_session_endpoint') - def test_legacy_client_image(self, mock_get_session_endpoint): - mock_client = mock.Mock() - mock_get_session_endpoint.return_value = 'http://example.com/v2' - config_dict = defaults.get_defaults() - config_dict.update(fake_services_dict) - cc = cloud_region.CloudRegion( - "test1", "region-al", config_dict, auth_plugin=mock.Mock()) - cc.get_legacy_client('image', mock_client) - mock_client.assert_called_with( - version=2.0, - service_name=None, - endpoint_override='http://example.com', - region_name='region-al', - interface='public', - session=mock.ANY, - # Not a typo - the config dict above overrides this - service_type='mage' - ) - - @mock.patch.object(cloud_region.CloudRegion, 'get_session_endpoint') - def test_legacy_client_image_override(self, mock_get_session_endpoint): - mock_client = mock.Mock() - mock_get_session_endpoint.return_value = 'http://example.com/v2' - config_dict = defaults.get_defaults() - config_dict.update(fake_services_dict) - config_dict['image_endpoint_override'] = 'http://example.com/override' - cc = cloud_region.CloudRegion( - "test1", "region-al", config_dict, auth_plugin=mock.Mock()) - cc.get_legacy_client('image', mock_client) - mock_client.assert_called_with( - version=2.0, - service_name=None, - endpoint_override='http://example.com/override', - region_name='region-al', - interface='public', - session=mock.ANY, - # Not a typo - the config dict above overrides this - service_type='mage' - ) - - @mock.patch.object(cloud_region.CloudRegion, 'get_session_endpoint') - def test_legacy_client_image_versioned(self, mock_get_session_endpoint): - mock_client = mock.Mock() - mock_get_session_endpoint.return_value = 'http://example.com/v2' - config_dict = defaults.get_defaults() - config_dict.update(fake_services_dict) - # v2 endpoint was passed, 1 requested in config, endpoint wins - config_dict['image_api_version'] = '1' - cc = cloud_region.CloudRegion( - "test1", "region-al", config_dict, auth_plugin=mock.Mock()) - cc.get_legacy_client('image', mock_client) - mock_client.assert_called_with( - version=2.0, - service_name=None, - endpoint_override='http://example.com', - region_name='region-al', - interface='public', - session=mock.ANY, - # Not a typo - the config dict above overrides this - service_type='mage' - ) - - @mock.patch.object(cloud_region.CloudRegion, 'get_session_endpoint') - def test_legacy_client_image_unversioned(self, mock_get_session_endpoint): - mock_client = mock.Mock() - mock_get_session_endpoint.return_value = 'http://example.com/' - config_dict = defaults.get_defaults() - config_dict.update(fake_services_dict) - # Versionless endpoint, config wins - config_dict['image_api_version'] = '1' - cc = cloud_region.CloudRegion( - "test1", "region-al", config_dict, auth_plugin=mock.Mock()) - cc.get_legacy_client('image', mock_client) - mock_client.assert_called_with( - version='1', - service_name=None, - endpoint_override='http://example.com', - region_name='region-al', - interface='public', - session=mock.ANY, - # Not a typo - the config dict above overrides this - service_type='mage' - ) - - @mock.patch.object(cloud_region.CloudRegion, 'get_session_endpoint') - def test_legacy_client_image_argument(self, mock_get_session_endpoint): - mock_client = mock.Mock() - mock_get_session_endpoint.return_value = 'http://example.com/v3' - config_dict = defaults.get_defaults() - config_dict.update(fake_services_dict) - # Versionless endpoint, config wins - config_dict['image_api_version'] = '6' - cc = cloud_region.CloudRegion( - "test1", "region-al", config_dict, auth_plugin=mock.Mock()) - cc.get_legacy_client('image', mock_client, version='beef') - mock_client.assert_called_with( - version='beef', - service_name=None, - endpoint_override='http://example.com', - region_name='region-al', - interface='public', - session=mock.ANY, - # Not a typo - the config dict above overrides this - service_type='mage' - ) - - @mock.patch.object(cloud_region.CloudRegion, 'get_session_endpoint') - def test_legacy_client_network(self, mock_get_session_endpoint): - mock_client = mock.Mock() - mock_get_session_endpoint.return_value = 'http://example.com/v2' - config_dict = defaults.get_defaults() - config_dict.update(fake_services_dict) - cc = cloud_region.CloudRegion( - "test1", "region-al", config_dict, auth_plugin=mock.Mock()) - cc.get_legacy_client('network', mock_client) - mock_client.assert_called_with( - api_version='2.0', - endpoint_type='public', - endpoint_override=None, - region_name='region-al', - service_type='network', - session=mock.ANY, - service_name=None) - - @mock.patch.object(cloud_region.CloudRegion, 'get_session_endpoint') - def test_legacy_client_compute(self, mock_get_session_endpoint): - mock_client = mock.Mock() - mock_get_session_endpoint.return_value = 'http://example.com/v2' - config_dict = defaults.get_defaults() - config_dict.update(fake_services_dict) - cc = cloud_region.CloudRegion( - "test1", "region-al", config_dict, auth_plugin=mock.Mock()) - cc.get_legacy_client('compute', mock_client) - mock_client.assert_called_with( - version='2', - endpoint_type='public', - endpoint_override='http://compute.example.com', - region_name='region-al', - service_type='compute', - session=mock.ANY, - service_name=None) - - @mock.patch.object(cloud_region.CloudRegion, 'get_session_endpoint') - def test_legacy_client_identity(self, mock_get_session_endpoint): - mock_client = mock.Mock() - mock_get_session_endpoint.return_value = 'http://example.com/v2' - config_dict = defaults.get_defaults() - config_dict.update(fake_services_dict) - cc = cloud_region.CloudRegion( - "test1", "region-al", config_dict, auth_plugin=mock.Mock()) - cc.get_legacy_client('identity', mock_client) - mock_client.assert_called_with( - version='2.0', - endpoint='http://example.com/v2', - endpoint_type='admin', - endpoint_override=None, - region_name='region-al', - service_type='identity', - session=mock.ANY, - service_name='locks') - - @mock.patch.object(cloud_region.CloudRegion, 'get_session_endpoint') - def test_legacy_client_identity_v3(self, mock_get_session_endpoint): - mock_client = mock.Mock() - mock_get_session_endpoint.return_value = 'http://example.com' - config_dict = defaults.get_defaults() - config_dict.update(fake_services_dict) - config_dict['identity_api_version'] = '3' - cc = cloud_region.CloudRegion( - "test1", "region-al", config_dict, auth_plugin=mock.Mock()) - cc.get_legacy_client('identity', mock_client) - mock_client.assert_called_with( - version='3', - endpoint='http://example.com', - interface='admin', - endpoint_override=None, - region_name='region-al', - service_type='identity', - session=mock.ANY, - service_name='locks') diff --git a/openstack/tests/unit/config/test_init.py b/openstack/tests/unit/config/test_init.py index 97171291a..62fb7ea45 100644 --- a/openstack/tests/unit/config/test_init.py +++ b/openstack/tests/unit/config/test_init.py @@ -17,16 +17,16 @@ class TestInit(base.TestCase): - def test_get_config_without_arg_parser(self): - cloud_region = openstack.config.get_config( + def test_get_cloud_region_without_arg_parser(self): + cloud_region = openstack.config.get_cloud_region( options=None, validate=False) self.assertIsInstance( cloud_region, openstack.config.cloud_region.CloudRegion ) - def test_get_config_with_arg_parser(self): - cloud_region = openstack.config.get_config( + def test_get_cloud_region_with_arg_parser(self): + cloud_region = openstack.config.get_cloud_region( options=argparse.ArgumentParser(), validate=False) self.assertIsInstance( diff --git a/test-requirements.txt b/test-requirements.txt index 943a8376e..4584371f0 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -16,5 +16,3 @@ stestr>=1.0.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=2.2.0 # MIT -python-glanceclient>=2.8.0 # Apache-2.0 -python-ironicclient>=1.14.0 # Apache-2.0 From 56cc254d63de3a7f2ffd6983b05a4f0d2d1bab66 Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Fri, 5 Jan 2018 14:39:03 -0800 Subject: [PATCH 1897/3836] Re-enable octavia functional tests This patch re-enables the octavia functional tests for openstacksdk. This patch fixes a bug in the l7rule l7policy URI parameter. This patch also fixes the test_inventory functional test that fails if there are multiple networks in the tenant. Change-Id: Iacdc92ec78a9ca0ad3a82172f426ed91a4b9023d --- .zuul.yaml | 25 +++++++++++++++++++ openstack/load_balancer/v2/_proxy.py | 12 ++++----- openstack/load_balancer/v2/l7_rule.py | 11 ++++---- .../tests/functional/cloud/test_devstack.py | 1 + .../tests/functional/cloud/test_inventory.py | 2 +- .../tests/unit/load_balancer/test_l7rule.py | 2 +- .../tests/unit/load_balancer/test_proxy.py | 12 ++++----- 7 files changed, 46 insertions(+), 19 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index b28f069c9..92983a485 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -121,10 +121,35 @@ parent: openstacksdk-functional-devstack-base description: | Run openstacksdk functional tests against a master devstack + required-projects: + - openstack/octavia vars: devstack_localrc: Q_SERVICE_PLUGIN_CLASSES: qos Q_ML2_PLUGIN_EXT_DRIVERS: qos,port_security + DISABLE_AMP_IMAGE_BUILD: True + devstack_local_conf: + post-config: + $OCTAVIA_CONF: + DEFAULT: + debug: True + controller_worker: + amphora_driver: amphora_noop_driver + compute_driver: compute_noop_driver + network_driver: network_noop_driver + certificates: + cert_manager: local_cert_manager + devstack_plugins: + octavia: https://git.openstack.org/openstack/octavia + devstack_services: + octavia: true + o-api: true + o-cw: true + o-hm: true + o-hk: true + neutron-qos: true + tox_environment: + OPENSTACKSDK_HAS_OCTAVIA: 1 - job: name: openstacksdk-functional-devstack-python3 diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index 575cbcc4d..8f02b1cd1 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -570,7 +570,7 @@ def create_l7_rule(self, l7_policy, **attrs): :rtype: :class:`~openstack.load_balancer.v2.l7_rule.L7Rule` """ l7policyobj = self._get_resource(_l7policy.L7Policy, l7_policy) - return self._create(_l7rule.L7Rule, l7_policy_id=l7policyobj.id, + return self._create(_l7rule.L7Rule, l7policy_id=l7policyobj.id, **attrs) def delete_l7_rule(self, l7rule, l7_policy, ignore_missing=True): @@ -592,7 +592,7 @@ def delete_l7_rule(self, l7rule, l7_policy, ignore_missing=True): """ l7policyobj = self._get_resource(_l7policy.L7Policy, l7_policy) self._delete(_l7rule.L7Rule, l7rule, ignore_missing=ignore_missing, - l7_policy_id=l7policyobj.id) + l7policy_id=l7policyobj.id) def find_l7_rule(self, name_or_id, l7_policy, ignore_missing=True): """Find a single l7rule @@ -613,7 +613,7 @@ def find_l7_rule(self, name_or_id, l7_policy, ignore_missing=True): l7policyobj = self._get_resource(_l7policy.L7Policy, l7_policy) return self._find(_l7rule.L7Rule, name_or_id, ignore_missing=ignore_missing, - l7_policy_id=l7policyobj.id) + l7policy_id=l7policyobj.id) def get_l7_rule(self, l7rule, l7_policy): """Get a single l7rule @@ -631,7 +631,7 @@ def get_l7_rule(self, l7rule, l7_policy): """ l7policyobj = self._get_resource(_l7policy.L7Policy, l7_policy) return self._get(_l7rule.L7Rule, l7rule, - l7_policy_id=l7policyobj.id) + l7policy_id=l7policyobj.id) def l7_rules(self, l7_policy, **query): """Return a generator of l7rules @@ -647,7 +647,7 @@ def l7_rules(self, l7_policy, **query): """ l7policyobj = self._get_resource(_l7policy.L7Policy, l7_policy) return self._list(_l7rule.L7Rule, paginated=True, - l7_policy_id=l7policyobj.id, **query) + l7policy_id=l7policyobj.id, **query) def update_l7_rule(self, l7rule, l7_policy, **attrs): """Update a l7rule @@ -666,4 +666,4 @@ def update_l7_rule(self, l7rule, l7_policy, **attrs): """ l7policyobj = self._get_resource(_l7policy.L7Policy, l7_policy) return self._update(_l7rule.L7Rule, l7rule, - l7_policy_id=l7policyobj.id, **attrs) + l7policy_id=l7policyobj.id, **attrs) diff --git a/openstack/load_balancer/v2/l7_rule.py b/openstack/load_balancer/v2/l7_rule.py index f9dca5432..bc6af906e 100644 --- a/openstack/load_balancer/v2/l7_rule.py +++ b/openstack/load_balancer/v2/l7_rule.py @@ -17,7 +17,7 @@ class L7Rule(resource.Resource): resource_key = 'rule' resources_key = 'rules' - base_path = '/v2.0/lbaas/l7policies/%(l7_policy_id)s/rules' + base_path = '/v2.0/lbaas/l7policies/%(l7policy_id)s/rules' service = lb_service.LoadBalancerService() # capabilities @@ -28,9 +28,10 @@ class L7Rule(resource.Resource): allow_delete = True _query_mapping = resource.QueryParameters( - 'compare_type', 'created_at', 'invert', 'key', 'l7_policy_id', - 'project_id', 'provisioning_status', 'type', 'updated_at', - 'rule_value', 'operating_status', is_admin_state_up='admin_state_up', + 'compare_type', 'created_at', 'invert', 'key', 'project_id', + 'provisioning_status', 'type', 'updated_at', 'rule_value', + 'operating_status', is_admin_state_up='admin_state_up', + l7_policy_id='l7policy_id', ) #: Properties @@ -46,7 +47,7 @@ class L7Rule(resource.Resource): #: The key to use for the comparison. key = resource.Body('key') #: The ID of the associated l7 policy - l7_policy_id = resource.URI('l7_policy_id') + l7_policy_id = resource.URI('l7policy_id') #: The operating status of this l7rule operating_status = resource.Body('operating_status') #: The ID of the project this l7policy is associated with. diff --git a/openstack/tests/functional/cloud/test_devstack.py b/openstack/tests/functional/cloud/test_devstack.py index e84d7745d..1a58b703d 100644 --- a/openstack/tests/functional/cloud/test_devstack.py +++ b/openstack/tests/functional/cloud/test_devstack.py @@ -34,6 +34,7 @@ class TestDevstack(base.BaseFunctionalTestCase): ('heat', dict(env='HEAT', service='orchestration')), ('magnum', dict(env='MAGNUM', service='container-infra')), ('neutron', dict(env='NEUTRON', service='network')), + ('octavia', dict(env='OCTAVIA', service='load-balancer')), ('swift', dict(env='SWIFT', service='object-store')), ] diff --git a/openstack/tests/functional/cloud/test_inventory.py b/openstack/tests/functional/cloud/test_inventory.py index 219594742..263d20250 100644 --- a/openstack/tests/functional/cloud/test_inventory.py +++ b/openstack/tests/functional/cloud/test_inventory.py @@ -40,7 +40,7 @@ def setUp(self): self.addCleanup(self._cleanup_server) server = self.operator_cloud.create_server( name=self.server_name, image=self.image, flavor=self.flavor, - wait=True, auto_ip=True) + wait=True, auto_ip=True, network='public') self.server_id = server['id'] def _cleanup_server(self): diff --git a/openstack/tests/unit/load_balancer/test_l7rule.py b/openstack/tests/unit/load_balancer/test_l7rule.py index fd2e3cf81..70046c9b2 100644 --- a/openstack/tests/unit/load_balancer/test_l7rule.py +++ b/openstack/tests/unit/load_balancer/test_l7rule.py @@ -38,7 +38,7 @@ def test_basic(self): test_l7rule = l7_rule.L7Rule() self.assertEqual('rule', test_l7rule.resource_key) self.assertEqual('rules', test_l7rule.resources_key) - self.assertEqual('/v2.0/lbaas/l7policies/%(l7_policy_id)s/rules', + self.assertEqual('/v2.0/lbaas/l7policies/%(l7policy_id)s/rules', test_l7rule.base_path) self.assertEqual('load-balancer', test_l7rule.service.service_type) self.assertTrue(test_l7rule.allow_create) diff --git a/openstack/tests/unit/load_balancer/test_proxy.py b/openstack/tests/unit/load_balancer/test_proxy.py index f3690d827..4db9ad239 100644 --- a/openstack/tests/unit/load_balancer/test_proxy.py +++ b/openstack/tests/unit/load_balancer/test_proxy.py @@ -203,33 +203,33 @@ def test_l7_rules(self): l7_rule.L7Rule, paginated=True, method_kwargs={'l7_policy': self.L7_POLICY_ID}, - expected_kwargs={'l7_policy_id': self.L7_POLICY_ID}) + expected_kwargs={'l7policy_id': self.L7_POLICY_ID}) def test_l7_rule_get(self): self.verify_get(self.proxy.get_l7_rule, l7_rule.L7Rule, method_kwargs={'l7_policy': self.L7_POLICY_ID}, - expected_kwargs={'l7_policy_id': self.L7_POLICY_ID}) + expected_kwargs={'l7policy_id': self.L7_POLICY_ID}) def test_l7_rule_create(self): self.verify_create(self.proxy.create_l7_rule, l7_rule.L7Rule, method_kwargs={'l7_policy': self.L7_POLICY_ID}, - expected_kwargs={'l7_policy_id': self.L7_POLICY_ID}) + expected_kwargs={'l7policy_id': self.L7_POLICY_ID}) def test_l7_rule_delete(self): self.verify_delete(self.proxy.delete_l7_rule, l7_rule.L7Rule, True, method_kwargs={'l7_policy': self.L7_POLICY_ID}, - expected_kwargs={'l7_policy_id': self.L7_POLICY_ID}) + expected_kwargs={'l7policy_id': self.L7_POLICY_ID}) def test_l7_rule_find(self): self._verify2('openstack.proxy2.BaseProxy._find', self.proxy.find_l7_rule, method_args=["RULE", self.L7_POLICY_ID], expected_args=[l7_rule.L7Rule, "RULE"], - expected_kwargs={"l7_policy_id": self.L7_POLICY_ID, + expected_kwargs={"l7policy_id": self.L7_POLICY_ID, "ignore_missing": True}) def test_l7_rule_update(self): @@ -237,4 +237,4 @@ def test_l7_rule_update(self): self.proxy.update_l7_rule, method_args=["RULE", self.L7_POLICY_ID], expected_args=[l7_rule.L7Rule, "RULE"], - expected_kwargs={"l7_policy_id": self.L7_POLICY_ID}) + expected_kwargs={"l7policy_id": self.L7_POLICY_ID}) From 1149329d9c10fd7e4116de6d782081e16543bae9 Mon Sep 17 00:00:00 2001 From: "zhang.lei" Date: Mon, 15 Jan 2018 02:24:32 +0000 Subject: [PATCH 1898/3836] Remove the deprecated "giturl" option From openstackdocstheme 1.18.0, valid Git URLs can be retrieved by openstackdocstheme[1], we do not need giturl option anymore. [1] https://review.openstack.org/532163 Change-Id: I1848f43d337d40248fce3f50554e4b5ba294396c --- doc/source/conf.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index a57f6fc87..8c3d3d431 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -60,12 +60,10 @@ copyright = u'2017, Various members of the OpenStack Foundation' # A few variables have to be set for the log-a-bug feature. -# giturl: The location of conf.py on Git. Must be set manually. # gitsha: The SHA checksum of the bug description. Extracted from git log. # bug_tag: Tag for categorizing the bug. Must be set manually. # bug_project: Launchpad project to file bugs against. # These variables are passed to the logabug code via html_context. -giturl = u'http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/doc/source' git_cmd = "/usr/bin/git log | head -n1 | cut -f2 -d' '" try: gitsha = os.popen(git_cmd).read().strip('\n') @@ -74,13 +72,11 @@ gitsha = "unknown" bug_tag = "docs" -# source tree pwd = os.getcwd() # html_context allows us to pass arbitrary values into the html template html_context = {"pwd": pwd, "gitsha": gitsha, "bug_tag": bug_tag, - "giturl": giturl, "bug_project": "python-openstacksdk"} # If true, '()' will be appended to :func: etc. cross-reference text. From 26732e837492db8806731869ee22976116f4ef88 Mon Sep 17 00:00:00 2001 From: Adrian Turjak Date: Mon, 15 Jan 2018 14:08:24 +1300 Subject: [PATCH 1899/3836] Change update/create method options Rather than use booleans, switch to explicit definition of which method is used. This is useful because 'update' can when including Swift be 3 different methods, and that gets messy with booleans. An explicit definition works nicer and gives us more flexibility in future as well as a nicer pattern for similar method remappings. Depends-On: Icfa811571ec7c26617e0f8e9db086001fc0bc81f Change-Id: I7e231a8c89ffd74fdd63aabafb408fadfdb69c67 --- openstack/baremetal/v1/chassis.py | 2 +- openstack/baremetal/v1/node.py | 2 +- openstack/baremetal/v1/port.py | 2 +- openstack/baremetal/v1/port_group.py | 2 +- openstack/clustering/v1/cluster.py | 2 +- openstack/clustering/v1/node.py | 2 +- openstack/clustering/v1/policy.py | 4 +-- openstack/clustering/v1/profile.py | 4 +-- openstack/clustering/v1/receiver.py | 2 +- openstack/identity/v3/credential.py | 2 +- openstack/identity/v3/domain.py | 2 +- openstack/identity/v3/endpoint.py | 2 +- openstack/identity/v3/group.py | 2 +- openstack/identity/v3/policy.py | 2 +- openstack/identity/v3/project.py | 2 +- openstack/identity/v3/region.py | 2 +- openstack/identity/v3/service.py | 2 +- openstack/identity/v3/user.py | 2 +- openstack/image/v2/image.py | 2 +- openstack/message/v2/claim.py | 2 +- openstack/resource2.py | 33 ++++++++++++------- .../tests/unit/baremetal/test_version.py | 4 +-- .../tests/unit/baremetal/v1/test_chassis.py | 2 +- .../tests/unit/baremetal/v1/test_node.py | 2 +- .../tests/unit/baremetal/v1/test_port.py | 2 +- .../unit/baremetal/v1/test_port_group.py | 2 +- .../tests/unit/cluster/v1/test_profile.py | 4 +-- .../tests/unit/identity/v3/test_credential.py | 2 +- .../tests/unit/identity/v3/test_domain.py | 2 +- .../tests/unit/identity/v3/test_endpoint.py | 2 +- .../tests/unit/identity/v3/test_group.py | 2 +- .../tests/unit/identity/v3/test_policy.py | 2 +- .../tests/unit/identity/v3/test_project.py | 2 +- .../tests/unit/identity/v3/test_region.py | 2 +- .../tests/unit/identity/v3/test_service.py | 2 +- openstack/tests/unit/identity/v3/test_user.py | 2 +- openstack/tests/unit/test_resource.py | 2 +- openstack/tests/unit/test_resource2.py | 25 ++++++++------ 38 files changed, 76 insertions(+), 62 deletions(-) diff --git a/openstack/baremetal/v1/chassis.py b/openstack/baremetal/v1/chassis.py index a8ba9746a..715acb250 100644 --- a/openstack/baremetal/v1/chassis.py +++ b/openstack/baremetal/v1/chassis.py @@ -26,7 +26,7 @@ class Chassis(resource.Resource): allow_update = True allow_delete = True allow_list = True - patch_update = True + update_method = 'PATCH' _query_mapping = resource.QueryParameters( 'fields' diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index c1b6266ed..008132c3d 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -26,7 +26,7 @@ class Node(resource.Resource): allow_update = True allow_delete = True allow_list = True - patch_update = True + update_method = 'PATCH' _query_mapping = resource.QueryParameters( 'associated', 'driver', 'fields', 'provision_state', 'resource_class', diff --git a/openstack/baremetal/v1/port.py b/openstack/baremetal/v1/port.py index a3f86594c..75f47f68d 100644 --- a/openstack/baremetal/v1/port.py +++ b/openstack/baremetal/v1/port.py @@ -26,7 +26,7 @@ class Port(resource.Resource): allow_update = True allow_delete = True allow_list = True - patch_update = True + update_method = 'PATCH' _query_mapping = resource.QueryParameters( 'fields' diff --git a/openstack/baremetal/v1/port_group.py b/openstack/baremetal/v1/port_group.py index b2db7467f..41fd78396 100644 --- a/openstack/baremetal/v1/port_group.py +++ b/openstack/baremetal/v1/port_group.py @@ -26,7 +26,7 @@ class PortGroup(resource.Resource): allow_update = True allow_delete = True allow_list = True - patch_update = True + update_method = 'PATCH' _query_mapping = resource.QueryParameters( 'node', 'address', 'fields', diff --git a/openstack/clustering/v1/cluster.py b/openstack/clustering/v1/cluster.py index 8b6561ba5..8b7f12757 100644 --- a/openstack/clustering/v1/cluster.py +++ b/openstack/clustering/v1/cluster.py @@ -27,7 +27,7 @@ class Cluster(resource.Resource): allow_update = True allow_delete = True allow_list = True - patch_update = True + update_method = 'PATCH' _query_mapping = resource.QueryParameters( 'name', 'status', 'sort', 'global_project') diff --git a/openstack/clustering/v1/node.py b/openstack/clustering/v1/node.py index c0108df04..4147e442d 100644 --- a/openstack/clustering/v1/node.py +++ b/openstack/clustering/v1/node.py @@ -28,7 +28,7 @@ class Node(resource.Resource): allow_delete = True allow_list = True - patch_update = True + update_method = 'PATCH' _query_mapping = resource.QueryParameters( 'show_details', 'name', 'sort', 'global_project', 'cluster_id', diff --git a/openstack/clustering/v1/policy.py b/openstack/clustering/v1/policy.py index a547c7f61..f8613f291 100644 --- a/openstack/clustering/v1/policy.py +++ b/openstack/clustering/v1/policy.py @@ -27,7 +27,7 @@ class Policy(resource.Resource): allow_delete = True allow_update = True - patch_update = True + update_method = 'PATCH' _query_mapping = resource.QueryParameters( 'name', 'type', 'sort', 'global_project') @@ -63,4 +63,4 @@ class PolicyValidate(Policy): allow_delete = False allow_update = False - patch_update = False + update_method = 'PUT' diff --git a/openstack/clustering/v1/profile.py b/openstack/clustering/v1/profile.py index 6420d8615..e01ebccb8 100644 --- a/openstack/clustering/v1/profile.py +++ b/openstack/clustering/v1/profile.py @@ -27,7 +27,7 @@ class Profile(resource.Resource): allow_delete = True allow_list = True - patch_update = True + update_method = 'PATCH' _query_mapping = resource.QueryParameters( 'sort', 'global_project', 'type', 'name') @@ -61,4 +61,4 @@ class ProfileValidate(Profile): allow_delete = False allow_list = False - patch_update = False + update_method = 'PUT' diff --git a/openstack/clustering/v1/receiver.py b/openstack/clustering/v1/receiver.py index 1437fce35..35265aeef 100644 --- a/openstack/clustering/v1/receiver.py +++ b/openstack/clustering/v1/receiver.py @@ -27,7 +27,7 @@ class Receiver(resource.Resource): allow_update = True allow_delete = True - patch_update = True + update_method = 'PATCH' _query_mapping = resource.QueryParameters( 'name', 'type', 'cluster_id', 'action', 'sort', 'global_project', diff --git a/openstack/identity/v3/credential.py b/openstack/identity/v3/credential.py index e12794f09..33bc40c49 100644 --- a/openstack/identity/v3/credential.py +++ b/openstack/identity/v3/credential.py @@ -26,7 +26,7 @@ class Credential(resource.Resource): allow_update = True allow_delete = True allow_list = True - patch_update = True + update_method = 'PATCH' _query_mapping = resource.QueryParameters( 'type', 'user_id', diff --git a/openstack/identity/v3/domain.py b/openstack/identity/v3/domain.py index b6d79fd15..45b06fcbc 100644 --- a/openstack/identity/v3/domain.py +++ b/openstack/identity/v3/domain.py @@ -27,7 +27,7 @@ class Domain(resource.Resource): allow_update = True allow_delete = True allow_list = True - patch_update = True + update_method = 'PATCH' _query_mapping = resource.QueryParameters( 'name', diff --git a/openstack/identity/v3/endpoint.py b/openstack/identity/v3/endpoint.py index 796f3375c..59899e25e 100644 --- a/openstack/identity/v3/endpoint.py +++ b/openstack/identity/v3/endpoint.py @@ -26,7 +26,7 @@ class Endpoint(resource.Resource): allow_update = True allow_delete = True allow_list = True - patch_update = True + update_method = 'PATCH' _query_mapping = resource.QueryParameters( 'interface', 'service_id', diff --git a/openstack/identity/v3/group.py b/openstack/identity/v3/group.py index 12ea7e403..2793f94da 100644 --- a/openstack/identity/v3/group.py +++ b/openstack/identity/v3/group.py @@ -26,7 +26,7 @@ class Group(resource.Resource): allow_update = True allow_delete = True allow_list = True - patch_update = True + update_method = 'PATCH' _query_mapping = resource.QueryParameters( 'domain_id', 'name', diff --git a/openstack/identity/v3/policy.py b/openstack/identity/v3/policy.py index bd814d5ba..3e5643c70 100644 --- a/openstack/identity/v3/policy.py +++ b/openstack/identity/v3/policy.py @@ -26,7 +26,7 @@ class Policy(resource.Resource): allow_update = True allow_delete = True allow_list = True - patch_update = True + update_method = 'PATCH' # Properties #: The policy rule set itself, as a serialized blob. *Type: string* diff --git a/openstack/identity/v3/project.py b/openstack/identity/v3/project.py index dd99b52a7..159192668 100644 --- a/openstack/identity/v3/project.py +++ b/openstack/identity/v3/project.py @@ -27,7 +27,7 @@ class Project(resource.Resource): allow_update = True allow_delete = True allow_list = True - patch_update = True + update_method = 'PATCH' _query_mapping = resource.QueryParameters( 'domain_id', diff --git a/openstack/identity/v3/region.py b/openstack/identity/v3/region.py index a845614b0..9169d9436 100644 --- a/openstack/identity/v3/region.py +++ b/openstack/identity/v3/region.py @@ -26,7 +26,7 @@ class Region(resource.Resource): allow_update = True allow_delete = True allow_list = True - patch_update = True + update_method = 'PATCH' _query_mapping = resource.QueryParameters( 'parent_region_id', diff --git a/openstack/identity/v3/service.py b/openstack/identity/v3/service.py index 79baf40a4..457d2995f 100644 --- a/openstack/identity/v3/service.py +++ b/openstack/identity/v3/service.py @@ -26,7 +26,7 @@ class Service(resource.Resource): allow_update = True allow_delete = True allow_list = True - patch_update = True + update_method = 'PATCH' _query_mapping = resource.QueryParameters( 'type', diff --git a/openstack/identity/v3/user.py b/openstack/identity/v3/user.py index f31154143..055a50ef8 100644 --- a/openstack/identity/v3/user.py +++ b/openstack/identity/v3/user.py @@ -26,7 +26,7 @@ class User(resource.Resource): allow_update = True allow_delete = True allow_list = True - patch_update = True + update_method = 'PATCH' _query_mapping = resource.QueryParameters( 'domain_id', diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index 79af09ea0..f1b8a8f57 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -34,7 +34,7 @@ class Image(resource2.Resource): allow_update = True allow_delete = True allow_list = True - patch_update = True + update_method = 'PATCH' _query_mapping = resource2.QueryParameters("name", "visibility", "member_status", "owner", diff --git a/openstack/message/v2/claim.py b/openstack/message/v2/claim.py index 6125418de..c4824c919 100644 --- a/openstack/message/v2/claim.py +++ b/openstack/message/v2/claim.py @@ -31,7 +31,7 @@ class Claim(resource2.Resource): allow_get = True allow_update = True allow_delete = True - patch_update = True + update_method = 'PATCH' # Properties #: The value in seconds indicating how long the claim has existed. diff --git a/openstack/resource2.py b/openstack/resource2.py index 716b31604..55cdf546c 100644 --- a/openstack/resource2.py +++ b/openstack/resource2.py @@ -247,10 +247,11 @@ class Resource(object): allow_list = False #: Allow head operation for this resource. allow_head = False - #: Use PATCH for update operations on this resource. - patch_update = False - #: Use PUT for create operations on this resource. - put_create = False + + #: Method for udating a resource (PUT, PATCH, POST) + update_method = "PUT" + #: Method for creating a resource (POST, PUT) + create_method = "POST" def __init__(self, _synchronized=False, **attrs): """The base resource @@ -578,16 +579,19 @@ def create(self, session, prepend_key=True): if not self.allow_create: raise exceptions.MethodNotSupported(self, "create") - if self.put_create: + if self.create_method == 'PUT': request = self._prepare_request(requires_id=True, prepend_key=prepend_key) response = session.put(request.url, json=request.body, headers=request.headers) - else: + elif self.create_method == 'POST': request = self._prepare_request(requires_id=False, prepend_key=prepend_key) response = session.post(request.url, json=request.body, headers=request.headers) + else: + raise exceptions.ResourceFailure( + msg="Invalid create method: %s" % self.create_method) self._translate_response(response) return self @@ -661,13 +665,18 @@ def update(self, session, prepend_key=True, has_body=True): request = self._prepare_request(prepend_key=prepend_key) - if self.patch_update: - response = session.patch(request.url, - json=request.body, - headers=request.headers) + if self.update_method == 'PATCH': + response = session.patch( + request.url, json=request.body, headers=request.headers) + elif self.update_method == 'POST': + response = session.post( + request.url, json=request.body, headers=request.headers) + elif self.update_method == 'PUT': + response = session.put( + request.url, json=request.body, headers=request.headers) else: - response = session.put(request.url, - json=request.body, headers=request.headers) + raise exceptions.ResourceFailure( + msg="Invalid update method: %s" % self.update_method) self._translate_response(response, has_body=has_body) return self diff --git a/openstack/tests/unit/baremetal/test_version.py b/openstack/tests/unit/baremetal/test_version.py index 7466730a9..75125e216 100644 --- a/openstack/tests/unit/baremetal/test_version.py +++ b/openstack/tests/unit/baremetal/test_version.py @@ -37,8 +37,8 @@ def test_basic(self): self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) self.assertFalse(sot.allow_head) - self.assertFalse(sot.patch_update) - self.assertFalse(sot.put_create) + self.assertEqual('PUT', sot.update_method) + self.assertEqual('POST', sot.create_method) def test_make_it(self): sot = version.Version(**EXAMPLE) diff --git a/openstack/tests/unit/baremetal/v1/test_chassis.py b/openstack/tests/unit/baremetal/v1/test_chassis.py index b9dfe82a5..3702ba596 100644 --- a/openstack/tests/unit/baremetal/v1/test_chassis.py +++ b/openstack/tests/unit/baremetal/v1/test_chassis.py @@ -56,7 +56,7 @@ def test_basic(self): self.assertTrue(sot.allow_update) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertTrue(sot.patch_update) + self.assertEqual('PATCH', sot.update_method) def test_instantiate(self): sot = chassis.Chassis(**FAKE) diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 05cff3e48..b96306111 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -104,7 +104,7 @@ def test_basic(self): self.assertTrue(sot.allow_update) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertTrue(sot.patch_update) + self.assertEqual('PATCH', sot.update_method) def test_instantiate(self): sot = node.Node(**FAKE) diff --git a/openstack/tests/unit/baremetal/v1/test_port.py b/openstack/tests/unit/baremetal/v1/test_port.py index 29d4ad4d5..1ae202f63 100644 --- a/openstack/tests/unit/baremetal/v1/test_port.py +++ b/openstack/tests/unit/baremetal/v1/test_port.py @@ -55,7 +55,7 @@ def test_basic(self): self.assertTrue(sot.allow_update) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertTrue(sot.patch_update) + self.assertEqual('PATCH', sot.update_method) def test_instantiate(self): sot = port.PortDetail(**FAKE) diff --git a/openstack/tests/unit/baremetal/v1/test_port_group.py b/openstack/tests/unit/baremetal/v1/test_port_group.py index b2c6ed147..d8d5abdfe 100644 --- a/openstack/tests/unit/baremetal/v1/test_port_group.py +++ b/openstack/tests/unit/baremetal/v1/test_port_group.py @@ -60,7 +60,7 @@ def test_basic(self): self.assertTrue(sot.allow_update) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertTrue(sot.patch_update) + self.assertEqual('PATCH', sot.update_method) def test_instantiate(self): sot = port_group.PortGroup(**FAKE) diff --git a/openstack/tests/unit/cluster/v1/test_profile.py b/openstack/tests/unit/cluster/v1/test_profile.py index 700382466..61de6a971 100644 --- a/openstack/tests/unit/cluster/v1/test_profile.py +++ b/openstack/tests/unit/cluster/v1/test_profile.py @@ -57,7 +57,7 @@ def test_basic(self): self.assertTrue(sot.allow_update) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertTrue(sot.patch_update) + self.assertEqual('PATCH', sot.update_method) def test_instantiate(self): sot = profile.Profile(**FAKE) @@ -89,4 +89,4 @@ def test_basic(self): self.assertFalse(sot.allow_update) self.assertFalse(sot.allow_delete) self.assertFalse(sot.allow_list) - self.assertFalse(sot.patch_update) + self.assertEqual('PUT', sot.update_method) diff --git a/openstack/tests/unit/identity/v3/test_credential.py b/openstack/tests/unit/identity/v3/test_credential.py index 654fe7f4e..eb4c1e216 100644 --- a/openstack/tests/unit/identity/v3/test_credential.py +++ b/openstack/tests/unit/identity/v3/test_credential.py @@ -37,7 +37,7 @@ def test_basic(self): self.assertTrue(sot.allow_update) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertTrue(sot.patch_update) + self.assertEqual('PATCH', sot.update_method) self.assertDictEqual( { diff --git a/openstack/tests/unit/identity/v3/test_domain.py b/openstack/tests/unit/identity/v3/test_domain.py index 80c2fbe38..6e6cb3d00 100644 --- a/openstack/tests/unit/identity/v3/test_domain.py +++ b/openstack/tests/unit/identity/v3/test_domain.py @@ -37,7 +37,7 @@ def test_basic(self): self.assertTrue(sot.allow_update) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertTrue(sot.patch_update) + self.assertEqual('PATCH', sot.update_method) self.assertDictEqual( { diff --git a/openstack/tests/unit/identity/v3/test_endpoint.py b/openstack/tests/unit/identity/v3/test_endpoint.py index 3e59f71b4..115e9b7ef 100644 --- a/openstack/tests/unit/identity/v3/test_endpoint.py +++ b/openstack/tests/unit/identity/v3/test_endpoint.py @@ -39,7 +39,7 @@ def test_basic(self): self.assertTrue(sot.allow_update) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertTrue(sot.patch_update) + self.assertEqual('PATCH', sot.update_method) self.assertDictEqual( { 'interface': 'interface', diff --git a/openstack/tests/unit/identity/v3/test_group.py b/openstack/tests/unit/identity/v3/test_group.py index 480baf8de..2051aeb3c 100644 --- a/openstack/tests/unit/identity/v3/test_group.py +++ b/openstack/tests/unit/identity/v3/test_group.py @@ -36,7 +36,7 @@ def test_basic(self): self.assertTrue(sot.allow_update) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertTrue(sot.patch_update) + self.assertEqual('PATCH', sot.update_method) self.assertDictEqual( { diff --git a/openstack/tests/unit/identity/v3/test_policy.py b/openstack/tests/unit/identity/v3/test_policy.py index cdd8b71dc..b514da96e 100644 --- a/openstack/tests/unit/identity/v3/test_policy.py +++ b/openstack/tests/unit/identity/v3/test_policy.py @@ -38,7 +38,7 @@ def test_basic(self): self.assertTrue(sot.allow_update) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertTrue(sot.patch_update) + self.assertEqual('PATCH', sot.update_method) def test_make_it(self): sot = policy.Policy(**EXAMPLE) diff --git a/openstack/tests/unit/identity/v3/test_project.py b/openstack/tests/unit/identity/v3/test_project.py index 8e6998faa..fc5ecd806 100644 --- a/openstack/tests/unit/identity/v3/test_project.py +++ b/openstack/tests/unit/identity/v3/test_project.py @@ -39,7 +39,7 @@ def test_basic(self): self.assertTrue(sot.allow_update) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertTrue(sot.patch_update) + self.assertEqual('PATCH', sot.update_method) self.assertDictEqual( { diff --git a/openstack/tests/unit/identity/v3/test_region.py b/openstack/tests/unit/identity/v3/test_region.py index 9ee14e082..cf7841683 100644 --- a/openstack/tests/unit/identity/v3/test_region.py +++ b/openstack/tests/unit/identity/v3/test_region.py @@ -36,7 +36,7 @@ def test_basic(self): self.assertTrue(sot.allow_update) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertTrue(sot.patch_update) + self.assertEqual('PATCH', sot.update_method) self.assertDictEqual( { diff --git a/openstack/tests/unit/identity/v3/test_service.py b/openstack/tests/unit/identity/v3/test_service.py index 578b5f656..be321a22a 100644 --- a/openstack/tests/unit/identity/v3/test_service.py +++ b/openstack/tests/unit/identity/v3/test_service.py @@ -38,7 +38,7 @@ def test_basic(self): self.assertTrue(sot.allow_update) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertTrue(sot.patch_update) + self.assertEqual('PATCH', sot.update_method) self.assertDictEqual( { diff --git a/openstack/tests/unit/identity/v3/test_user.py b/openstack/tests/unit/identity/v3/test_user.py index e0cbb369d..1f746d111 100644 --- a/openstack/tests/unit/identity/v3/test_user.py +++ b/openstack/tests/unit/identity/v3/test_user.py @@ -42,7 +42,7 @@ def test_basic(self): self.assertTrue(sot.allow_update) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertTrue(sot.patch_update) + self.assertEqual('PATCH', sot.update_method) self.assertDictEqual( { diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 2364920b2..daf5f4643 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -783,7 +783,7 @@ class FakeResourcePatch(FakeResource): def test_put_update(self): class FakeResourcePut(FakeResource): # This is False by default, but explicit for this test. - patch_update = False + update_method = 'PUT' resp = FakeResponse(fake_body, headers={'location': 'foo'}) self.session.put = mock.Mock(return_value=resp) diff --git a/openstack/tests/unit/test_resource2.py b/openstack/tests/unit/test_resource2.py index a69b88dc0..4a8b845b6 100644 --- a/openstack/tests/unit/test_resource2.py +++ b/openstack/tests/unit/test_resource2.py @@ -424,8 +424,8 @@ def test_initialize_basic(self): self.assertFalse(sot.allow_delete) self.assertFalse(sot.allow_list) self.assertFalse(sot.allow_head) - self.assertFalse(sot.patch_update) - self.assertFalse(sot.put_create) + self.assertEqual('PUT', sot.update_method) + self.assertEqual('POST', sot.create_method) def test_repr(self): a = {"a": 1} @@ -995,7 +995,7 @@ class Test(resource2.Resource): service = self.service_name base_path = self.base_path allow_create = True - put_create = True + create_method = 'PUT' self._test_create(Test, requires_id=True, prepend_key=True) @@ -1004,7 +1004,7 @@ class Test(resource2.Resource): service = self.service_name base_path = self.base_path allow_create = True - put_create = False + create_method = 'POST' self._test_create(Test, requires_id=False, prepend_key=True) @@ -1039,9 +1039,9 @@ def test_head(self): self.sot._translate_response.assert_called_once_with(self.response) self.assertEqual(result, self.sot) - def _test_update(self, patch_update=False, prepend_key=True, + def _test_update(self, update_method='PUT', prepend_key=True, has_body=True): - self.sot.patch_update = patch_update + self.sot.update_method = update_method # Need to make sot look dirty so we can attempt an update self.sot._body = mock.Mock() @@ -1053,11 +1053,15 @@ def _test_update(self, patch_update=False, prepend_key=True, self.sot._prepare_request.assert_called_once_with( prepend_key=prepend_key) - if patch_update: + if update_method == 'PATCH': self.session.patch.assert_called_once_with( self.request.url, json=self.request.body, headers=self.request.headers) - else: + elif update_method == 'POST': + self.session.post.assert_called_once_with( + self.request.url, + json=self.request.body, headers=self.request.headers) + elif update_method == 'PUT': self.session.put.assert_called_once_with( self.request.url, json=self.request.body, headers=self.request.headers) @@ -1066,10 +1070,11 @@ def _test_update(self, patch_update=False, prepend_key=True, self.response, has_body=has_body) def test_update_put(self): - self._test_update(patch_update=False, prepend_key=True, has_body=True) + self._test_update(update_method='PUT', prepend_key=True, has_body=True) def test_update_patch(self): - self._test_update(patch_update=True, prepend_key=False, has_body=False) + self._test_update( + update_method='PATCH', prepend_key=False, has_body=False) def test_update_not_dirty(self): self.sot._body = mock.Mock() From 40d425c5954bdfc5c798c49e047de4ad1c7ad6ec Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 10 Jan 2018 09:10:40 -0600 Subject: [PATCH 1900/3836] Merge user and users sections of the docs The openstack doc standard location is 'user', so go with that. Incorporate pages from the shade and occ docs into the appropriate places in the user index file. This still leaves user/logging and user/guide/logging which need to be rationalized. That will come in the next commit, as it also needs to actually rationalize the logging helper functions. Remove the Makefile, as it's not used by things. Change-Id: I26f36370ef9651f4bcaa7dee3b903309463d9592 --- doc/Makefile | 136 --------------- doc/source/index.rst | 23 ++- doc/source/{users => user}/connection.rst | 0 doc/source/user/examples | 1 + doc/source/user/examples/cleanup-servers.py | 13 -- doc/source/user/examples/debug-logging.py | 6 - doc/source/user/examples/find-an-image.py | 7 - .../user/examples/http-debug-logging.py | 6 - doc/source/user/examples/munch-dict-object.py | 7 - doc/source/user/examples/normalization.py | 7 - .../examples/service-conditional-overrides.py | 5 - .../user/examples/service-conditionals.py | 6 - doc/source/user/examples/strict-mode.py | 8 - .../user/examples/upload-large-object.py | 10 -- doc/source/user/examples/upload-object.py | 10 -- doc/source/user/examples/user-agent.py | 6 - .../{users => user}/guides/baremetal.rst | 0 .../{users => user}/guides/block_storage.rst | 0 .../{users => user}/guides/clustering.rst | 0 .../guides/clustering/action.rst | 0 .../guides/clustering/cluster.rst | 0 .../guides/clustering/event.rst | 0 .../guides/clustering/node.rst | 0 .../guides/clustering/policy.rst | 0 .../guides/clustering/policy_type.rst | 0 .../guides/clustering/profile.rst | 0 .../guides/clustering/profile_type.rst | 0 .../guides/clustering/receiver.rst | 0 doc/source/{users => user}/guides/compute.rst | 0 doc/source/{users => user}/guides/connect.rst | 0 .../guides/connect_from_config.rst | 0 .../{users => user}/guides/database.rst | 0 .../{users => user}/guides/identity.rst | 0 doc/source/{users => user}/guides/image.rst | 0 .../{users => user}/guides/key_manager.rst | 0 doc/source/{users => user}/guides/logging.rst | 0 doc/source/{users => user}/guides/message.rst | 0 doc/source/{users => user}/guides/meter.rst | 0 doc/source/{users => user}/guides/network.rst | 0 .../{users => user}/guides/object_store.rst | 0 .../{users => user}/guides/orchestration.rst | 0 doc/source/user/index.rst | 156 +++++++++++++++++- .../{users => user}/proxies/baremetal.rst | 2 +- .../{users => user}/proxies/block_storage.rst | 2 +- .../{users => user}/proxies/clustering.rst | 0 .../{users => user}/proxies/compute.rst | 2 +- .../{users => user}/proxies/database.rst | 2 +- .../{users => user}/proxies/identity_v2.rst | 2 +- .../{users => user}/proxies/identity_v3.rst | 2 +- .../{users => user}/proxies/image_v1.rst | 2 +- .../{users => user}/proxies/image_v2.rst | 2 +- .../{users => user}/proxies/key_manager.rst | 2 +- .../proxies/load_balancer_v2.rst | 0 .../{users => user}/proxies/message_v1.rst | 2 +- .../{users => user}/proxies/message_v2.rst | 2 +- doc/source/{users => user}/proxies/meter.rst | 2 +- doc/source/{users => user}/proxies/metric.rst | 0 .../{users => user}/proxies/network.rst | 2 +- .../{users => user}/proxies/object_store.rst | 2 +- .../{users => user}/proxies/orchestration.rst | 2 +- .../{users => user}/proxies/workflow.rst | 0 doc/source/{users => user}/resource.rst | 0 doc/source/{users => user}/resource2.rst | 0 .../resources/baremetal/index.rst | 0 .../resources/baremetal/v1/chassis.rst | 0 .../resources/baremetal/v1/driver.rst | 0 .../resources/baremetal/v1/node.rst | 0 .../resources/baremetal/v1/port.rst | 0 .../resources/baremetal/v1/port_group.rst | 0 .../resources/block_storage/index.rst | 0 .../resources/block_storage/v2/snapshot.rst | 0 .../resources/block_storage/v2/type.rst | 0 .../resources/block_storage/v2/volume.rst | 0 .../resources/clustering/index.rst | 0 .../resources/clustering/v1/action.rst | 0 .../resources/clustering/v1/build_info.rst | 0 .../resources/clustering/v1/cluster.rst | 0 .../clustering/v1/cluster_policy.rst | 0 .../resources/clustering/v1/event.rst | 0 .../resources/clustering/v1/node.rst | 0 .../resources/clustering/v1/policy.rst | 0 .../resources/clustering/v1/policy_type.rst | 0 .../resources/clustering/v1/profile.rst | 0 .../resources/clustering/v1/profile_type.rst | 0 .../resources/clustering/v1/receiver.rst | 0 .../resources/compute/index.rst | 0 .../resources/compute/v2/extension.rst | 0 .../resources/compute/v2/flavor.rst | 0 .../resources/compute/v2/image.rst | 0 .../resources/compute/v2/keypair.rst | 0 .../resources/compute/v2/limits.rst | 0 .../resources/compute/v2/server.rst | 0 .../resources/compute/v2/server_interface.rst | 0 .../resources/compute/v2/server_ip.rst | 0 .../resources/database/index.rst | 0 .../resources/database/v1/database.rst | 0 .../resources/database/v1/flavor.rst | 0 .../resources/database/v1/instance.rst | 0 .../resources/database/v1/user.rst | 0 .../resources/identity/index.rst | 0 .../resources/identity/v2/extension.rst | 0 .../resources/identity/v2/role.rst | 0 .../resources/identity/v2/tenant.rst | 0 .../resources/identity/v2/user.rst | 0 .../resources/identity/v3/credential.rst | 0 .../resources/identity/v3/domain.rst | 0 .../resources/identity/v3/endpoint.rst | 0 .../resources/identity/v3/group.rst | 0 .../resources/identity/v3/policy.rst | 0 .../resources/identity/v3/project.rst | 0 .../resources/identity/v3/service.rst | 0 .../resources/identity/v3/trust.rst | 0 .../resources/identity/v3/user.rst | 0 .../{users => user}/resources/image/index.rst | 0 .../resources/image/v1/image.rst | 0 .../resources/image/v2/image.rst | 0 .../resources/image/v2/member.rst | 0 .../resources/key_manager/index.rst | 0 .../resources/key_manager/v1/container.rst | 0 .../resources/key_manager/v1/order.rst | 0 .../resources/key_manager/v1/secret.rst | 0 .../resources/load_balancer/index.rst | 0 .../load_balancer/v2/health_monitor.rst | 0 .../resources/load_balancer/v2/l7_policy.rst | 0 .../resources/load_balancer/v2/l7_rule.rst | 0 .../resources/load_balancer/v2/listener.rst | 0 .../load_balancer/v2/load_balancer.rst | 0 .../resources/load_balancer/v2/member.rst | 0 .../resources/load_balancer/v2/pool.rst | 0 .../{users => user}/resources/meter/index.rst | 0 .../resources/meter/v2/capability.rst | 0 .../resources/meter/v2/meter.rst | 0 .../resources/meter/v2/resource.rst | 0 .../resources/meter/v2/sample.rst | 0 .../resources/meter/v2/statistics.rst | 0 .../resources/metric/index.rst | 0 .../resources/metric/v1/archive_policy.rst | 0 .../resources/metric/v1/capabilities.rst | 0 .../resources/metric/v1/metric.rst | 0 .../resources/metric/v1/resource.rst | 0 .../resources/network/index.rst | 0 .../resources/network/v2/address_scope.rst | 0 .../resources/network/v2/agent.rst | 0 .../network/v2/auto_allocated_topology.rst | 0 .../network/v2/availability_zone.rst | 0 .../resources/network/v2/extension.rst | 0 .../resources/network/v2/flavor.rst | 0 .../resources/network/v2/floating_ip.rst | 0 .../resources/network/v2/health_monitor.rst | 0 .../resources/network/v2/listener.rst | 0 .../resources/network/v2/load_balancer.rst | 0 .../resources/network/v2/metering_label.rst | 0 .../network/v2/metering_label_rule.rst | 0 .../resources/network/v2/network.rst | 0 .../network/v2/network_ip_availability.rst | 0 .../resources/network/v2/pool.rst | 0 .../resources/network/v2/pool_member.rst | 0 .../resources/network/v2/port.rst | 0 .../network/v2/qos_bandwidth_limit_rule.rst | 0 .../network/v2/qos_dscp_marking_rule.rst | 0 .../network/v2/qos_minimum_bandwidth_rule.rst | 0 .../resources/network/v2/qos_policy.rst | 0 .../resources/network/v2/qos_rule_type.rst | 0 .../resources/network/v2/quota.rst | 0 .../resources/network/v2/rbac_policy.rst | 0 .../resources/network/v2/router.rst | 0 .../resources/network/v2/security_group.rst | 0 .../network/v2/security_group_rule.rst | 0 .../resources/network/v2/segment.rst | 0 .../resources/network/v2/service_profile.rst | 0 .../resources/network/v2/service_provider.rst | 0 .../resources/network/v2/subnet.rst | 0 .../resources/network/v2/subnet_pool.rst | 0 .../resources/object_store/index.rst | 0 .../resources/object_store/v1/account.rst | 0 .../resources/object_store/v1/container.rst | 0 .../resources/object_store/v1/obj.rst | 0 .../resources/orchestration/index.rst | 0 .../resources/orchestration/v1/resource.rst | 0 .../resources/orchestration/v1/stack.rst | 0 .../resources/workflow/index.rst | 0 .../resources/workflow/v2/execution.rst | 0 .../resources/workflow/v2/workflow.rst | 0 doc/source/{users => user}/service_filter.rst | 0 doc/source/{users => user}/utils.rst | 0 doc/source/users/examples | 1 - doc/source/users/index.rst | 139 ---------------- examples/cloud/cleanup-servers.py | 26 +++ .../cloud}/create-server-dict.py | 19 ++- .../cloud}/create-server-name-or-id.py | 19 ++- examples/cloud/debug-logging.py | 18 ++ examples/cloud/find-an-image.py | 19 +++ examples/cloud/http-debug-logging.py | 18 ++ examples/cloud/munch-dict-object.py | 19 +++ examples/cloud/normalization.py | 19 +++ .../cloud}/server-information.py | 17 +- .../cloud/service-conditional-overrides.py | 17 ++ examples/cloud/service-conditionals.py | 18 ++ examples/cloud/strict-mode.py | 20 +++ examples/cloud/upload-large-object.py | 22 +++ examples/cloud/upload-object.py | 22 +++ examples/cloud/user-agent.py | 18 ++ examples/clustering/policy.py | 2 +- examples/clustering/policy_type.py | 2 +- examples/clustering/profile.py | 2 +- examples/clustering/profile_type.py | 2 +- examples/image/create.py | 2 +- examples/image/delete.py | 2 +- examples/image/download.py | 2 +- examples/image/list.py | 2 +- 210 files changed, 469 insertions(+), 415 deletions(-) delete mode 100644 doc/Makefile rename doc/source/{users => user}/connection.rst (100%) create mode 120000 doc/source/user/examples delete mode 100644 doc/source/user/examples/cleanup-servers.py delete mode 100644 doc/source/user/examples/debug-logging.py delete mode 100644 doc/source/user/examples/find-an-image.py delete mode 100644 doc/source/user/examples/http-debug-logging.py delete mode 100644 doc/source/user/examples/munch-dict-object.py delete mode 100644 doc/source/user/examples/normalization.py delete mode 100644 doc/source/user/examples/service-conditional-overrides.py delete mode 100644 doc/source/user/examples/service-conditionals.py delete mode 100644 doc/source/user/examples/strict-mode.py delete mode 100644 doc/source/user/examples/upload-large-object.py delete mode 100644 doc/source/user/examples/upload-object.py delete mode 100644 doc/source/user/examples/user-agent.py rename doc/source/{users => user}/guides/baremetal.rst (100%) rename doc/source/{users => user}/guides/block_storage.rst (100%) rename doc/source/{users => user}/guides/clustering.rst (100%) rename doc/source/{users => user}/guides/clustering/action.rst (100%) rename doc/source/{users => user}/guides/clustering/cluster.rst (100%) rename doc/source/{users => user}/guides/clustering/event.rst (100%) rename doc/source/{users => user}/guides/clustering/node.rst (100%) rename doc/source/{users => user}/guides/clustering/policy.rst (100%) rename doc/source/{users => user}/guides/clustering/policy_type.rst (100%) rename doc/source/{users => user}/guides/clustering/profile.rst (100%) rename doc/source/{users => user}/guides/clustering/profile_type.rst (100%) rename doc/source/{users => user}/guides/clustering/receiver.rst (100%) rename doc/source/{users => user}/guides/compute.rst (100%) rename doc/source/{users => user}/guides/connect.rst (100%) rename doc/source/{users => user}/guides/connect_from_config.rst (100%) rename doc/source/{users => user}/guides/database.rst (100%) rename doc/source/{users => user}/guides/identity.rst (100%) rename doc/source/{users => user}/guides/image.rst (100%) rename doc/source/{users => user}/guides/key_manager.rst (100%) rename doc/source/{users => user}/guides/logging.rst (100%) rename doc/source/{users => user}/guides/message.rst (100%) rename doc/source/{users => user}/guides/meter.rst (100%) rename doc/source/{users => user}/guides/network.rst (100%) rename doc/source/{users => user}/guides/object_store.rst (100%) rename doc/source/{users => user}/guides/orchestration.rst (100%) rename doc/source/{users => user}/proxies/baremetal.rst (97%) rename doc/source/{users => user}/proxies/block_storage.rst (95%) rename doc/source/{users => user}/proxies/clustering.rst (100%) rename doc/source/{users => user}/proxies/compute.rst (99%) rename doc/source/{users => user}/proxies/database.rst (96%) rename doc/source/{users => user}/proxies/identity_v2.rst (96%) rename doc/source/{users => user}/proxies/identity_v3.rst (98%) rename doc/source/{users => user}/proxies/image_v1.rst (91%) rename doc/source/{users => user}/proxies/image_v2.rst (96%) rename doc/source/{users => user}/proxies/key_manager.rst (98%) rename doc/source/{users => user}/proxies/load_balancer_v2.rst (100%) rename doc/source/{users => user}/proxies/message_v1.rst (92%) rename doc/source/{users => user}/proxies/message_v2.rst (96%) rename doc/source/{users => user}/proxies/meter.rst (97%) rename doc/source/{users => user}/proxies/metric.rst (100%) rename doc/source/{users => user}/proxies/network.rst (99%) rename doc/source/{users => user}/proxies/object_store.rst (96%) rename doc/source/{users => user}/proxies/orchestration.rst (96%) rename doc/source/{users => user}/proxies/workflow.rst (100%) rename doc/source/{users => user}/resource.rst (100%) rename doc/source/{users => user}/resource2.rst (100%) rename doc/source/{users => user}/resources/baremetal/index.rst (100%) rename doc/source/{users => user}/resources/baremetal/v1/chassis.rst (100%) rename doc/source/{users => user}/resources/baremetal/v1/driver.rst (100%) rename doc/source/{users => user}/resources/baremetal/v1/node.rst (100%) rename doc/source/{users => user}/resources/baremetal/v1/port.rst (100%) rename doc/source/{users => user}/resources/baremetal/v1/port_group.rst (100%) rename doc/source/{users => user}/resources/block_storage/index.rst (100%) rename doc/source/{users => user}/resources/block_storage/v2/snapshot.rst (100%) rename doc/source/{users => user}/resources/block_storage/v2/type.rst (100%) rename doc/source/{users => user}/resources/block_storage/v2/volume.rst (100%) rename doc/source/{users => user}/resources/clustering/index.rst (100%) rename doc/source/{users => user}/resources/clustering/v1/action.rst (100%) rename doc/source/{users => user}/resources/clustering/v1/build_info.rst (100%) rename doc/source/{users => user}/resources/clustering/v1/cluster.rst (100%) rename doc/source/{users => user}/resources/clustering/v1/cluster_policy.rst (100%) rename doc/source/{users => user}/resources/clustering/v1/event.rst (100%) rename doc/source/{users => user}/resources/clustering/v1/node.rst (100%) rename doc/source/{users => user}/resources/clustering/v1/policy.rst (100%) rename doc/source/{users => user}/resources/clustering/v1/policy_type.rst (100%) rename doc/source/{users => user}/resources/clustering/v1/profile.rst (100%) rename doc/source/{users => user}/resources/clustering/v1/profile_type.rst (100%) rename doc/source/{users => user}/resources/clustering/v1/receiver.rst (100%) rename doc/source/{users => user}/resources/compute/index.rst (100%) rename doc/source/{users => user}/resources/compute/v2/extension.rst (100%) rename doc/source/{users => user}/resources/compute/v2/flavor.rst (100%) rename doc/source/{users => user}/resources/compute/v2/image.rst (100%) rename doc/source/{users => user}/resources/compute/v2/keypair.rst (100%) rename doc/source/{users => user}/resources/compute/v2/limits.rst (100%) rename doc/source/{users => user}/resources/compute/v2/server.rst (100%) rename doc/source/{users => user}/resources/compute/v2/server_interface.rst (100%) rename doc/source/{users => user}/resources/compute/v2/server_ip.rst (100%) rename doc/source/{users => user}/resources/database/index.rst (100%) rename doc/source/{users => user}/resources/database/v1/database.rst (100%) rename doc/source/{users => user}/resources/database/v1/flavor.rst (100%) rename doc/source/{users => user}/resources/database/v1/instance.rst (100%) rename doc/source/{users => user}/resources/database/v1/user.rst (100%) rename doc/source/{users => user}/resources/identity/index.rst (100%) rename doc/source/{users => user}/resources/identity/v2/extension.rst (100%) rename doc/source/{users => user}/resources/identity/v2/role.rst (100%) rename doc/source/{users => user}/resources/identity/v2/tenant.rst (100%) rename doc/source/{users => user}/resources/identity/v2/user.rst (100%) rename doc/source/{users => user}/resources/identity/v3/credential.rst (100%) rename doc/source/{users => user}/resources/identity/v3/domain.rst (100%) rename doc/source/{users => user}/resources/identity/v3/endpoint.rst (100%) rename doc/source/{users => user}/resources/identity/v3/group.rst (100%) rename doc/source/{users => user}/resources/identity/v3/policy.rst (100%) rename doc/source/{users => user}/resources/identity/v3/project.rst (100%) rename doc/source/{users => user}/resources/identity/v3/service.rst (100%) rename doc/source/{users => user}/resources/identity/v3/trust.rst (100%) rename doc/source/{users => user}/resources/identity/v3/user.rst (100%) rename doc/source/{users => user}/resources/image/index.rst (100%) rename doc/source/{users => user}/resources/image/v1/image.rst (100%) rename doc/source/{users => user}/resources/image/v2/image.rst (100%) rename doc/source/{users => user}/resources/image/v2/member.rst (100%) rename doc/source/{users => user}/resources/key_manager/index.rst (100%) rename doc/source/{users => user}/resources/key_manager/v1/container.rst (100%) rename doc/source/{users => user}/resources/key_manager/v1/order.rst (100%) rename doc/source/{users => user}/resources/key_manager/v1/secret.rst (100%) rename doc/source/{users => user}/resources/load_balancer/index.rst (100%) rename doc/source/{users => user}/resources/load_balancer/v2/health_monitor.rst (100%) rename doc/source/{users => user}/resources/load_balancer/v2/l7_policy.rst (100%) rename doc/source/{users => user}/resources/load_balancer/v2/l7_rule.rst (100%) rename doc/source/{users => user}/resources/load_balancer/v2/listener.rst (100%) rename doc/source/{users => user}/resources/load_balancer/v2/load_balancer.rst (100%) rename doc/source/{users => user}/resources/load_balancer/v2/member.rst (100%) rename doc/source/{users => user}/resources/load_balancer/v2/pool.rst (100%) rename doc/source/{users => user}/resources/meter/index.rst (100%) rename doc/source/{users => user}/resources/meter/v2/capability.rst (100%) rename doc/source/{users => user}/resources/meter/v2/meter.rst (100%) rename doc/source/{users => user}/resources/meter/v2/resource.rst (100%) rename doc/source/{users => user}/resources/meter/v2/sample.rst (100%) rename doc/source/{users => user}/resources/meter/v2/statistics.rst (100%) rename doc/source/{users => user}/resources/metric/index.rst (100%) rename doc/source/{users => user}/resources/metric/v1/archive_policy.rst (100%) rename doc/source/{users => user}/resources/metric/v1/capabilities.rst (100%) rename doc/source/{users => user}/resources/metric/v1/metric.rst (100%) rename doc/source/{users => user}/resources/metric/v1/resource.rst (100%) rename doc/source/{users => user}/resources/network/index.rst (100%) rename doc/source/{users => user}/resources/network/v2/address_scope.rst (100%) rename doc/source/{users => user}/resources/network/v2/agent.rst (100%) rename doc/source/{users => user}/resources/network/v2/auto_allocated_topology.rst (100%) rename doc/source/{users => user}/resources/network/v2/availability_zone.rst (100%) rename doc/source/{users => user}/resources/network/v2/extension.rst (100%) rename doc/source/{users => user}/resources/network/v2/flavor.rst (100%) rename doc/source/{users => user}/resources/network/v2/floating_ip.rst (100%) rename doc/source/{users => user}/resources/network/v2/health_monitor.rst (100%) rename doc/source/{users => user}/resources/network/v2/listener.rst (100%) rename doc/source/{users => user}/resources/network/v2/load_balancer.rst (100%) rename doc/source/{users => user}/resources/network/v2/metering_label.rst (100%) rename doc/source/{users => user}/resources/network/v2/metering_label_rule.rst (100%) rename doc/source/{users => user}/resources/network/v2/network.rst (100%) rename doc/source/{users => user}/resources/network/v2/network_ip_availability.rst (100%) rename doc/source/{users => user}/resources/network/v2/pool.rst (100%) rename doc/source/{users => user}/resources/network/v2/pool_member.rst (100%) rename doc/source/{users => user}/resources/network/v2/port.rst (100%) rename doc/source/{users => user}/resources/network/v2/qos_bandwidth_limit_rule.rst (100%) rename doc/source/{users => user}/resources/network/v2/qos_dscp_marking_rule.rst (100%) rename doc/source/{users => user}/resources/network/v2/qos_minimum_bandwidth_rule.rst (100%) rename doc/source/{users => user}/resources/network/v2/qos_policy.rst (100%) rename doc/source/{users => user}/resources/network/v2/qos_rule_type.rst (100%) rename doc/source/{users => user}/resources/network/v2/quota.rst (100%) rename doc/source/{users => user}/resources/network/v2/rbac_policy.rst (100%) rename doc/source/{users => user}/resources/network/v2/router.rst (100%) rename doc/source/{users => user}/resources/network/v2/security_group.rst (100%) rename doc/source/{users => user}/resources/network/v2/security_group_rule.rst (100%) rename doc/source/{users => user}/resources/network/v2/segment.rst (100%) rename doc/source/{users => user}/resources/network/v2/service_profile.rst (100%) rename doc/source/{users => user}/resources/network/v2/service_provider.rst (100%) rename doc/source/{users => user}/resources/network/v2/subnet.rst (100%) rename doc/source/{users => user}/resources/network/v2/subnet_pool.rst (100%) rename doc/source/{users => user}/resources/object_store/index.rst (100%) rename doc/source/{users => user}/resources/object_store/v1/account.rst (100%) rename doc/source/{users => user}/resources/object_store/v1/container.rst (100%) rename doc/source/{users => user}/resources/object_store/v1/obj.rst (100%) rename doc/source/{users => user}/resources/orchestration/index.rst (100%) rename doc/source/{users => user}/resources/orchestration/v1/resource.rst (100%) rename doc/source/{users => user}/resources/orchestration/v1/stack.rst (100%) rename doc/source/{users => user}/resources/workflow/index.rst (100%) rename doc/source/{users => user}/resources/workflow/v2/execution.rst (100%) rename doc/source/{users => user}/resources/workflow/v2/workflow.rst (100%) rename doc/source/{users => user}/service_filter.rst (100%) rename doc/source/{users => user}/utils.rst (100%) delete mode 120000 doc/source/users/examples delete mode 100644 doc/source/users/index.rst create mode 100644 examples/cloud/cleanup-servers.py rename {doc/source/user/examples => examples/cloud}/create-server-dict.py (52%) rename {doc/source/user/examples => examples/cloud}/create-server-name-or-id.py (54%) create mode 100644 examples/cloud/debug-logging.py create mode 100644 examples/cloud/find-an-image.py create mode 100644 examples/cloud/http-debug-logging.py create mode 100644 examples/cloud/munch-dict-object.py create mode 100644 examples/cloud/normalization.py rename {doc/source/user/examples => examples/cloud}/server-information.py (51%) create mode 100644 examples/cloud/service-conditional-overrides.py create mode 100644 examples/cloud/service-conditionals.py create mode 100644 examples/cloud/strict-mode.py create mode 100644 examples/cloud/upload-large-object.py create mode 100644 examples/cloud/upload-object.py create mode 100644 examples/cloud/user-agent.py diff --git a/doc/Makefile b/doc/Makefile deleted file mode 100644 index 2cdd0f5cd..000000000 --- a/doc/Makefile +++ /dev/null @@ -1,136 +0,0 @@ -# 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 man 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 " man to make manual pages" - @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." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -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." diff --git a/doc/source/index.rst b/doc/source/index.rst index df613dd65..19b9bdead 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -1,10 +1,21 @@ Welcome to the OpenStack SDK! ============================= -This documentation is split into two sections: one for -:doc:`users ` looking to build applications which make use of -OpenStack, and another for those looking to -:doc:`contribute ` to this project. +This documentation is split into three sections: + +* an :doc:`installation ` guide +* a section for :doc:`users ` looking to build applications + which make use of OpenStack +* a section for those looking to :doc:`contribute ` + to this project + +Installation +------------ + +.. toctree:: + :maxdepth: 2 + + install/index For Users --------- @@ -12,12 +23,8 @@ For Users .. toctree:: :maxdepth: 2 - users/index - install/index user/index -.. TODO(shade) merge users/index and user/index into user/index - For Contributors ---------------- diff --git a/doc/source/users/connection.rst b/doc/source/user/connection.rst similarity index 100% rename from doc/source/users/connection.rst rename to doc/source/user/connection.rst diff --git a/doc/source/user/examples b/doc/source/user/examples new file mode 120000 index 000000000..9f9d1de88 --- /dev/null +++ b/doc/source/user/examples @@ -0,0 +1 @@ +../../../examples \ No newline at end of file diff --git a/doc/source/user/examples/cleanup-servers.py b/doc/source/user/examples/cleanup-servers.py deleted file mode 100644 index 628c8657f..000000000 --- a/doc/source/user/examples/cleanup-servers.py +++ /dev/null @@ -1,13 +0,0 @@ -import openstack.cloud - -# Initialize and turn on debug logging -openstack.cloud.simple_logging(debug=True) - -for cloud_name, region_name in [ - ('my-vexxhost', 'ca-ymq-1'), - ('my-citycloud', 'Buf1'), - ('my-internap', 'ams01')]: - # Initialize cloud - cloud = openstack.openstack_cloud(cloud=cloud_name, region_name=region_name) - for server in cloud.search_servers('my-server'): - cloud.delete_server(server, wait=True, delete_ips=True) diff --git a/doc/source/user/examples/debug-logging.py b/doc/source/user/examples/debug-logging.py deleted file mode 100644 index c0d91e125..000000000 --- a/doc/source/user/examples/debug-logging.py +++ /dev/null @@ -1,6 +0,0 @@ -import openstack.cloud -openstack.cloud.simple_logging(debug=True) - -cloud = openstack.openstack_cloud( - cloud='my-vexxhost', region_name='ca-ymq-1') -cloud.get_image('Ubuntu 16.04.1 LTS [2017-03-03]') diff --git a/doc/source/user/examples/find-an-image.py b/doc/source/user/examples/find-an-image.py deleted file mode 100644 index 74c666a60..000000000 --- a/doc/source/user/examples/find-an-image.py +++ /dev/null @@ -1,7 +0,0 @@ -import openstack.cloud -openstack.cloud.simple_logging() - -cloud = openstack.openstack_cloud(cloud='fuga', region_name='cystack') -cloud.pprint([ - image for image in cloud.list_images() - if 'ubuntu' in image.name.lower()]) diff --git a/doc/source/user/examples/http-debug-logging.py b/doc/source/user/examples/http-debug-logging.py deleted file mode 100644 index eff9d7572..000000000 --- a/doc/source/user/examples/http-debug-logging.py +++ /dev/null @@ -1,6 +0,0 @@ -import openstack.cloud -openstack.cloud.simple_logging(http_debug=True) - -cloud = openstack.openstack_cloud( - cloud='my-vexxhost', region_name='ca-ymq-1') -cloud.get_image('Ubuntu 16.04.1 LTS [2017-03-03]') diff --git a/doc/source/user/examples/munch-dict-object.py b/doc/source/user/examples/munch-dict-object.py deleted file mode 100644 index bfde7b41b..000000000 --- a/doc/source/user/examples/munch-dict-object.py +++ /dev/null @@ -1,7 +0,0 @@ -import openstack.cloud -openstack.cloud.simple_logging(debug=True) - -cloud = openstack.openstack_cloud(cloud='ovh', region_name='SBG1') -image = cloud.get_image('Ubuntu 16.10') -print(image.name) -print(image['name']) diff --git a/doc/source/user/examples/normalization.py b/doc/source/user/examples/normalization.py deleted file mode 100644 index 22b9b0f26..000000000 --- a/doc/source/user/examples/normalization.py +++ /dev/null @@ -1,7 +0,0 @@ -import openstack.cloud -openstack.cloud.simple_logging() - -cloud = openstack.openstack_cloud(cloud='fuga', region_name='cystack') -image = cloud.get_image( - 'Ubuntu 16.04 LTS - Xenial Xerus - 64-bit - Fuga Cloud Based Image') -cloud.pprint(image) diff --git a/doc/source/user/examples/service-conditional-overrides.py b/doc/source/user/examples/service-conditional-overrides.py deleted file mode 100644 index 845e7ab7c..000000000 --- a/doc/source/user/examples/service-conditional-overrides.py +++ /dev/null @@ -1,5 +0,0 @@ -import openstack.cloud -openstack.cloud.simple_logging(debug=True) - -cloud = openstack.openstack_cloud(cloud='rax', region_name='DFW') -print(cloud.has_service('network')) diff --git a/doc/source/user/examples/service-conditionals.py b/doc/source/user/examples/service-conditionals.py deleted file mode 100644 index 9bb198cbc..000000000 --- a/doc/source/user/examples/service-conditionals.py +++ /dev/null @@ -1,6 +0,0 @@ -import openstack.cloud -openstack.cloud.simple_logging(debug=True) - -cloud = openstack.openstack_cloud(cloud='kiss', region_name='region1') -print(cloud.has_service('network')) -print(cloud.has_service('container-orchestration')) diff --git a/doc/source/user/examples/strict-mode.py b/doc/source/user/examples/strict-mode.py deleted file mode 100644 index 251547160..000000000 --- a/doc/source/user/examples/strict-mode.py +++ /dev/null @@ -1,8 +0,0 @@ -import openstack.cloud -openstack.cloud.simple_logging() - -cloud = openstack.openstack_cloud( - cloud='fuga', region_name='cystack', strict=True) -image = cloud.get_image( - 'Ubuntu 16.04 LTS - Xenial Xerus - 64-bit - Fuga Cloud Based Image') -cloud.pprint(image) diff --git a/doc/source/user/examples/upload-large-object.py b/doc/source/user/examples/upload-large-object.py deleted file mode 100644 index 6b5c392a8..000000000 --- a/doc/source/user/examples/upload-large-object.py +++ /dev/null @@ -1,10 +0,0 @@ -import openstack.cloud -openstack.cloud.simple_logging(debug=True) - -cloud = openstack.openstack_cloud(cloud='ovh', region_name='SBG1') -cloud.create_object( - container='my-container', name='my-object', - filename='/home/mordred/briarcliff.sh3d', - segment_size=1000000) -cloud.delete_object('my-container', 'my-object') -cloud.delete_container('my-container') diff --git a/doc/source/user/examples/upload-object.py b/doc/source/user/examples/upload-object.py deleted file mode 100644 index 6b5c392a8..000000000 --- a/doc/source/user/examples/upload-object.py +++ /dev/null @@ -1,10 +0,0 @@ -import openstack.cloud -openstack.cloud.simple_logging(debug=True) - -cloud = openstack.openstack_cloud(cloud='ovh', region_name='SBG1') -cloud.create_object( - container='my-container', name='my-object', - filename='/home/mordred/briarcliff.sh3d', - segment_size=1000000) -cloud.delete_object('my-container', 'my-object') -cloud.delete_container('my-container') diff --git a/doc/source/user/examples/user-agent.py b/doc/source/user/examples/user-agent.py deleted file mode 100644 index 094c91e1c..000000000 --- a/doc/source/user/examples/user-agent.py +++ /dev/null @@ -1,6 +0,0 @@ -import openstack.cloud -openstack.cloud.simple_logging(http_debug=True) - -cloud = openstack.openstack_cloud( - cloud='datacentred', app_name='AmazingApp', app_version='1.0') -cloud.list_networks() diff --git a/doc/source/users/guides/baremetal.rst b/doc/source/user/guides/baremetal.rst similarity index 100% rename from doc/source/users/guides/baremetal.rst rename to doc/source/user/guides/baremetal.rst diff --git a/doc/source/users/guides/block_storage.rst b/doc/source/user/guides/block_storage.rst similarity index 100% rename from doc/source/users/guides/block_storage.rst rename to doc/source/user/guides/block_storage.rst diff --git a/doc/source/users/guides/clustering.rst b/doc/source/user/guides/clustering.rst similarity index 100% rename from doc/source/users/guides/clustering.rst rename to doc/source/user/guides/clustering.rst diff --git a/doc/source/users/guides/clustering/action.rst b/doc/source/user/guides/clustering/action.rst similarity index 100% rename from doc/source/users/guides/clustering/action.rst rename to doc/source/user/guides/clustering/action.rst diff --git a/doc/source/users/guides/clustering/cluster.rst b/doc/source/user/guides/clustering/cluster.rst similarity index 100% rename from doc/source/users/guides/clustering/cluster.rst rename to doc/source/user/guides/clustering/cluster.rst diff --git a/doc/source/users/guides/clustering/event.rst b/doc/source/user/guides/clustering/event.rst similarity index 100% rename from doc/source/users/guides/clustering/event.rst rename to doc/source/user/guides/clustering/event.rst diff --git a/doc/source/users/guides/clustering/node.rst b/doc/source/user/guides/clustering/node.rst similarity index 100% rename from doc/source/users/guides/clustering/node.rst rename to doc/source/user/guides/clustering/node.rst diff --git a/doc/source/users/guides/clustering/policy.rst b/doc/source/user/guides/clustering/policy.rst similarity index 100% rename from doc/source/users/guides/clustering/policy.rst rename to doc/source/user/guides/clustering/policy.rst diff --git a/doc/source/users/guides/clustering/policy_type.rst b/doc/source/user/guides/clustering/policy_type.rst similarity index 100% rename from doc/source/users/guides/clustering/policy_type.rst rename to doc/source/user/guides/clustering/policy_type.rst diff --git a/doc/source/users/guides/clustering/profile.rst b/doc/source/user/guides/clustering/profile.rst similarity index 100% rename from doc/source/users/guides/clustering/profile.rst rename to doc/source/user/guides/clustering/profile.rst diff --git a/doc/source/users/guides/clustering/profile_type.rst b/doc/source/user/guides/clustering/profile_type.rst similarity index 100% rename from doc/source/users/guides/clustering/profile_type.rst rename to doc/source/user/guides/clustering/profile_type.rst diff --git a/doc/source/users/guides/clustering/receiver.rst b/doc/source/user/guides/clustering/receiver.rst similarity index 100% rename from doc/source/users/guides/clustering/receiver.rst rename to doc/source/user/guides/clustering/receiver.rst diff --git a/doc/source/users/guides/compute.rst b/doc/source/user/guides/compute.rst similarity index 100% rename from doc/source/users/guides/compute.rst rename to doc/source/user/guides/compute.rst diff --git a/doc/source/users/guides/connect.rst b/doc/source/user/guides/connect.rst similarity index 100% rename from doc/source/users/guides/connect.rst rename to doc/source/user/guides/connect.rst diff --git a/doc/source/users/guides/connect_from_config.rst b/doc/source/user/guides/connect_from_config.rst similarity index 100% rename from doc/source/users/guides/connect_from_config.rst rename to doc/source/user/guides/connect_from_config.rst diff --git a/doc/source/users/guides/database.rst b/doc/source/user/guides/database.rst similarity index 100% rename from doc/source/users/guides/database.rst rename to doc/source/user/guides/database.rst diff --git a/doc/source/users/guides/identity.rst b/doc/source/user/guides/identity.rst similarity index 100% rename from doc/source/users/guides/identity.rst rename to doc/source/user/guides/identity.rst diff --git a/doc/source/users/guides/image.rst b/doc/source/user/guides/image.rst similarity index 100% rename from doc/source/users/guides/image.rst rename to doc/source/user/guides/image.rst diff --git a/doc/source/users/guides/key_manager.rst b/doc/source/user/guides/key_manager.rst similarity index 100% rename from doc/source/users/guides/key_manager.rst rename to doc/source/user/guides/key_manager.rst diff --git a/doc/source/users/guides/logging.rst b/doc/source/user/guides/logging.rst similarity index 100% rename from doc/source/users/guides/logging.rst rename to doc/source/user/guides/logging.rst diff --git a/doc/source/users/guides/message.rst b/doc/source/user/guides/message.rst similarity index 100% rename from doc/source/users/guides/message.rst rename to doc/source/user/guides/message.rst diff --git a/doc/source/users/guides/meter.rst b/doc/source/user/guides/meter.rst similarity index 100% rename from doc/source/users/guides/meter.rst rename to doc/source/user/guides/meter.rst diff --git a/doc/source/users/guides/network.rst b/doc/source/user/guides/network.rst similarity index 100% rename from doc/source/users/guides/network.rst rename to doc/source/user/guides/network.rst diff --git a/doc/source/users/guides/object_store.rst b/doc/source/user/guides/object_store.rst similarity index 100% rename from doc/source/users/guides/object_store.rst rename to doc/source/user/guides/object_store.rst diff --git a/doc/source/users/guides/orchestration.rst b/doc/source/user/guides/orchestration.rst similarity index 100% rename from doc/source/users/guides/orchestration.rst rename to doc/source/user/guides/orchestration.rst diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index 10513936f..d28c2abb6 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -1,15 +1,155 @@ -================== - Shade User Guide -================== +Getting started with the OpenStack SDK +====================================== + +For a listing of terms used throughout the SDK, including the names of +projects and services supported by it, see the :doc:`glossary <../glossary>`. + +Installation +------------ + +The OpenStack SDK is available on +`PyPI `_ under the name +**openstacksdk**. To install it, use ``pip``:: + + $ pip install openstacksdk + +.. _user_guides: + +User Guides +----------- + +These guides walk you through how to make use of the libraries we provide +to work with each OpenStack service. If you're looking for a cookbook +approach, this is where you'll want to begin. + +.. TODO(shade) Merge guides/logging and logging + +.. toctree:: + :maxdepth: 1 + + Configuration + Connect to an OpenStack Cloud + Connect to an OpenStack Cloud Using a Config File + Using Cloud Abstration Layer + Logging + Shade Logging + Microversions + Baremetal + Block Storage + Clustering + Compute + Database + Identity + Image + Key Manager + Message + Meter + Network + Object Store + Orchestration + +API Documentation +----------------- + +Service APIs are exposed through a two-layered approach. The classes +exposed through our *Connection* interface are the place to start if you're +an application developer consuming an OpenStack cloud. The *Resource* +interface is the layer upon which the *Connection* is built, with +*Connection* methods accepting and returning *Resource* objects. + +The Cloud Abstraction layer has a data model. .. toctree:: - :maxdepth: 2 + :maxdepth: 1 - config/index - usage - logging model - microversions + +Connection Interface +******************** + +A *Connection* instance maintains your cloud config, session and authentication +information providing you with a set of higher-level interfaces to work with +OpenStack services. + +.. toctree:: + :maxdepth: 1 + + connection + +Once you have a *Connection* instance, the following services may be exposed +to you. The combination of your ``CloudRegion`` and the catalog of the cloud +in question control which services are exposed, but listed below are the ones +provided by the SDK. + +.. toctree:: + :maxdepth: 1 + + Baremetal + Block Storage + Clustering + Compute + Database + Identity v2 + Identity v3 + Image v1 + Image v2 + Key Manager + Load Balancer + Message v1 + Message v2 + Network + Meter + Metric + Object Store + Orchestration + Workflow + +Resource Interface +****************** + +The *Resource* layer is a lower-level interface to communicate with OpenStack +services. While the classes exposed by the *Connection* build a convenience +layer on top of this, *Resources* can be used directly. However, the most +common usage of this layer is in receiving an object from a class in the +*Connection* layer, modifying it, and sending it back into the *Connection* +layer, such as to update a resource on the server. + +The following services have exposed *Resource* classes. + +.. toctree:: + :maxdepth: 1 + + Baremetal + Block Storage + Clustering + Compute + Database + Identity + Image + Key Management + Load Balancer + Meter + Metric + Network + Orchestration + Object Store + Workflow + +Low-Level Classes +***************** + +The following classes are not commonly used by application developers, +but are used to construct applications to talk to OpenStack APIs. Typically +these parts are managed through the `Connection Interface`_, but their use +can be customized. + +.. toctree:: + :maxdepth: 1 + + resource + resource2 + service_filter + utils Presentations ============= diff --git a/doc/source/users/proxies/baremetal.rst b/doc/source/user/proxies/baremetal.rst similarity index 97% rename from doc/source/users/proxies/baremetal.rst rename to doc/source/user/proxies/baremetal.rst index 5ca777b2a..b10f38b63 100644 --- a/doc/source/users/proxies/baremetal.rst +++ b/doc/source/user/proxies/baremetal.rst @@ -1,7 +1,7 @@ Baremetal API ============== -For details on how to use baremetal, see :doc:`/users/guides/baremetal` +For details on how to use baremetal, see :doc:`/user/guides/baremetal` .. automodule:: openstack.baremetal.v1._proxy diff --git a/doc/source/users/proxies/block_storage.rst b/doc/source/user/proxies/block_storage.rst similarity index 95% rename from doc/source/users/proxies/block_storage.rst rename to doc/source/user/proxies/block_storage.rst index ba9f7e355..8395709c3 100644 --- a/doc/source/users/proxies/block_storage.rst +++ b/doc/source/user/proxies/block_storage.rst @@ -1,7 +1,7 @@ Block Storage API ================= -For details on how to use block_storage, see :doc:`/users/guides/block_storage` +For details on how to use block_storage, see :doc:`/user/guides/block_storage` .. automodule:: openstack.block_storage.v2._proxy diff --git a/doc/source/users/proxies/clustering.rst b/doc/source/user/proxies/clustering.rst similarity index 100% rename from doc/source/users/proxies/clustering.rst rename to doc/source/user/proxies/clustering.rst diff --git a/doc/source/users/proxies/compute.rst b/doc/source/user/proxies/compute.rst similarity index 99% rename from doc/source/users/proxies/compute.rst rename to doc/source/user/proxies/compute.rst index 3d3b3cb13..d713b9c0a 100644 --- a/doc/source/users/proxies/compute.rst +++ b/doc/source/user/proxies/compute.rst @@ -1,7 +1,7 @@ Compute API =========== -For details on how to use compute, see :doc:`/users/guides/compute` +For details on how to use compute, see :doc:`/user/guides/compute` .. automodule:: openstack.compute.v2._proxy diff --git a/doc/source/users/proxies/database.rst b/doc/source/user/proxies/database.rst similarity index 96% rename from doc/source/users/proxies/database.rst rename to doc/source/user/proxies/database.rst index 493589b7b..6a3cdb372 100644 --- a/doc/source/users/proxies/database.rst +++ b/doc/source/user/proxies/database.rst @@ -1,7 +1,7 @@ Database API ============ -For details on how to use database, see :doc:`/users/guides/database` +For details on how to use database, see :doc:`/user/guides/database` .. automodule:: openstack.database.v1._proxy diff --git a/doc/source/users/proxies/identity_v2.rst b/doc/source/user/proxies/identity_v2.rst similarity index 96% rename from doc/source/users/proxies/identity_v2.rst rename to doc/source/user/proxies/identity_v2.rst index 0cd4b77e2..2bb5500a2 100644 --- a/doc/source/users/proxies/identity_v2.rst +++ b/doc/source/user/proxies/identity_v2.rst @@ -1,7 +1,7 @@ Identity API v2 =============== -For details on how to use identity, see :doc:`/users/guides/identity` +For details on how to use identity, see :doc:`/user/guides/identity` .. automodule:: openstack.identity.v2._proxy diff --git a/doc/source/users/proxies/identity_v3.rst b/doc/source/user/proxies/identity_v3.rst similarity index 98% rename from doc/source/users/proxies/identity_v3.rst rename to doc/source/user/proxies/identity_v3.rst index a5366a019..e2c5b125e 100644 --- a/doc/source/users/proxies/identity_v3.rst +++ b/doc/source/user/proxies/identity_v3.rst @@ -1,7 +1,7 @@ Identity API v3 =============== -For details on how to use identity, see :doc:`/users/guides/identity` +For details on how to use identity, see :doc:`/user/guides/identity` .. automodule:: openstack.identity.v3._proxy diff --git a/doc/source/users/proxies/image_v1.rst b/doc/source/user/proxies/image_v1.rst similarity index 91% rename from doc/source/users/proxies/image_v1.rst rename to doc/source/user/proxies/image_v1.rst index 185d60eb8..7be486538 100644 --- a/doc/source/users/proxies/image_v1.rst +++ b/doc/source/user/proxies/image_v1.rst @@ -1,7 +1,7 @@ Image API v1 ============ -For details on how to use image, see :doc:`/users/guides/image` +For details on how to use image, see :doc:`/user/guides/image` .. automodule:: openstack.image.v1._proxy diff --git a/doc/source/users/proxies/image_v2.rst b/doc/source/user/proxies/image_v2.rst similarity index 96% rename from doc/source/users/proxies/image_v2.rst rename to doc/source/user/proxies/image_v2.rst index f88d7450a..121e2cc7a 100644 --- a/doc/source/users/proxies/image_v2.rst +++ b/doc/source/user/proxies/image_v2.rst @@ -1,7 +1,7 @@ Image API v2 ============ -For details on how to use image, see :doc:`/users/guides/image` +For details on how to use image, see :doc:`/user/guides/image` .. automodule:: openstack.image.v2._proxy diff --git a/doc/source/users/proxies/key_manager.rst b/doc/source/user/proxies/key_manager.rst similarity index 98% rename from doc/source/users/proxies/key_manager.rst rename to doc/source/user/proxies/key_manager.rst index 7d24bb13e..291fb77fe 100644 --- a/doc/source/users/proxies/key_manager.rst +++ b/doc/source/user/proxies/key_manager.rst @@ -2,7 +2,7 @@ KeyManager API ============== For details on how to use key_management, see -:doc:`/users/guides/key_manager` +:doc:`/user/guides/key_manager` .. automodule:: openstack.key_manager.v1._proxy diff --git a/doc/source/users/proxies/load_balancer_v2.rst b/doc/source/user/proxies/load_balancer_v2.rst similarity index 100% rename from doc/source/users/proxies/load_balancer_v2.rst rename to doc/source/user/proxies/load_balancer_v2.rst diff --git a/doc/source/users/proxies/message_v1.rst b/doc/source/user/proxies/message_v1.rst similarity index 92% rename from doc/source/users/proxies/message_v1.rst rename to doc/source/user/proxies/message_v1.rst index 3803eaf24..42ed3ff7b 100644 --- a/doc/source/users/proxies/message_v1.rst +++ b/doc/source/user/proxies/message_v1.rst @@ -1,7 +1,7 @@ Message API v1 ============== -For details on how to use message, see :doc:`/users/guides/message` +For details on how to use message, see :doc:`/user/guides/message` .. automodule:: openstack.message.v1._proxy diff --git a/doc/source/users/proxies/message_v2.rst b/doc/source/user/proxies/message_v2.rst similarity index 96% rename from doc/source/users/proxies/message_v2.rst rename to doc/source/user/proxies/message_v2.rst index 5575663a9..dbf1f4778 100644 --- a/doc/source/users/proxies/message_v2.rst +++ b/doc/source/user/proxies/message_v2.rst @@ -1,7 +1,7 @@ Message API v2 ============== -For details on how to use message, see :doc:`/users/guides/message` +For details on how to use message, see :doc:`/user/guides/message` .. automodule:: openstack.message.v2._proxy diff --git a/doc/source/users/proxies/meter.rst b/doc/source/user/proxies/meter.rst similarity index 97% rename from doc/source/users/proxies/meter.rst rename to doc/source/user/proxies/meter.rst index a08e6f211..4a2f3a1bf 100644 --- a/doc/source/users/proxies/meter.rst +++ b/doc/source/user/proxies/meter.rst @@ -4,7 +4,7 @@ Meter API .. caution:: BETA: This API is a work in progress and is subject to change. -For details on how to use meter, see :doc:`/users/guides/meter` +For details on how to use meter, see :doc:`/user/guides/meter` .. automodule:: openstack.meter.v2._proxy diff --git a/doc/source/users/proxies/metric.rst b/doc/source/user/proxies/metric.rst similarity index 100% rename from doc/source/users/proxies/metric.rst rename to doc/source/user/proxies/metric.rst diff --git a/doc/source/users/proxies/network.rst b/doc/source/user/proxies/network.rst similarity index 99% rename from doc/source/users/proxies/network.rst rename to doc/source/user/proxies/network.rst index 20f895772..5bce51b28 100644 --- a/doc/source/users/proxies/network.rst +++ b/doc/source/user/proxies/network.rst @@ -1,7 +1,7 @@ Network API =========== -For details on how to use network, see :doc:`/users/guides/network` +For details on how to use network, see :doc:`/user/guides/network` .. automodule:: openstack.network.v2._proxy diff --git a/doc/source/users/proxies/object_store.rst b/doc/source/user/proxies/object_store.rst similarity index 96% rename from doc/source/users/proxies/object_store.rst rename to doc/source/user/proxies/object_store.rst index db685e4b9..ca6e28b34 100644 --- a/doc/source/users/proxies/object_store.rst +++ b/doc/source/user/proxies/object_store.rst @@ -1,7 +1,7 @@ Object Store API ================ -For details on how to use this API, see :doc:`/users/guides/object_store` +For details on how to use this API, see :doc:`/user/guides/object_store` .. automodule:: openstack.object_store.v1._proxy diff --git a/doc/source/users/proxies/orchestration.rst b/doc/source/user/proxies/orchestration.rst similarity index 96% rename from doc/source/users/proxies/orchestration.rst rename to doc/source/user/proxies/orchestration.rst index e663e64ed..9c2a9dbd9 100644 --- a/doc/source/users/proxies/orchestration.rst +++ b/doc/source/user/proxies/orchestration.rst @@ -1,7 +1,7 @@ Orchestration API ================= -For details on how to use orchestration, see :doc:`/users/guides/orchestration` +For details on how to use orchestration, see :doc:`/user/guides/orchestration` .. automodule:: openstack.orchestration.v1._proxy diff --git a/doc/source/users/proxies/workflow.rst b/doc/source/user/proxies/workflow.rst similarity index 100% rename from doc/source/users/proxies/workflow.rst rename to doc/source/user/proxies/workflow.rst diff --git a/doc/source/users/resource.rst b/doc/source/user/resource.rst similarity index 100% rename from doc/source/users/resource.rst rename to doc/source/user/resource.rst diff --git a/doc/source/users/resource2.rst b/doc/source/user/resource2.rst similarity index 100% rename from doc/source/users/resource2.rst rename to doc/source/user/resource2.rst diff --git a/doc/source/users/resources/baremetal/index.rst b/doc/source/user/resources/baremetal/index.rst similarity index 100% rename from doc/source/users/resources/baremetal/index.rst rename to doc/source/user/resources/baremetal/index.rst diff --git a/doc/source/users/resources/baremetal/v1/chassis.rst b/doc/source/user/resources/baremetal/v1/chassis.rst similarity index 100% rename from doc/source/users/resources/baremetal/v1/chassis.rst rename to doc/source/user/resources/baremetal/v1/chassis.rst diff --git a/doc/source/users/resources/baremetal/v1/driver.rst b/doc/source/user/resources/baremetal/v1/driver.rst similarity index 100% rename from doc/source/users/resources/baremetal/v1/driver.rst rename to doc/source/user/resources/baremetal/v1/driver.rst diff --git a/doc/source/users/resources/baremetal/v1/node.rst b/doc/source/user/resources/baremetal/v1/node.rst similarity index 100% rename from doc/source/users/resources/baremetal/v1/node.rst rename to doc/source/user/resources/baremetal/v1/node.rst diff --git a/doc/source/users/resources/baremetal/v1/port.rst b/doc/source/user/resources/baremetal/v1/port.rst similarity index 100% rename from doc/source/users/resources/baremetal/v1/port.rst rename to doc/source/user/resources/baremetal/v1/port.rst diff --git a/doc/source/users/resources/baremetal/v1/port_group.rst b/doc/source/user/resources/baremetal/v1/port_group.rst similarity index 100% rename from doc/source/users/resources/baremetal/v1/port_group.rst rename to doc/source/user/resources/baremetal/v1/port_group.rst diff --git a/doc/source/users/resources/block_storage/index.rst b/doc/source/user/resources/block_storage/index.rst similarity index 100% rename from doc/source/users/resources/block_storage/index.rst rename to doc/source/user/resources/block_storage/index.rst diff --git a/doc/source/users/resources/block_storage/v2/snapshot.rst b/doc/source/user/resources/block_storage/v2/snapshot.rst similarity index 100% rename from doc/source/users/resources/block_storage/v2/snapshot.rst rename to doc/source/user/resources/block_storage/v2/snapshot.rst diff --git a/doc/source/users/resources/block_storage/v2/type.rst b/doc/source/user/resources/block_storage/v2/type.rst similarity index 100% rename from doc/source/users/resources/block_storage/v2/type.rst rename to doc/source/user/resources/block_storage/v2/type.rst diff --git a/doc/source/users/resources/block_storage/v2/volume.rst b/doc/source/user/resources/block_storage/v2/volume.rst similarity index 100% rename from doc/source/users/resources/block_storage/v2/volume.rst rename to doc/source/user/resources/block_storage/v2/volume.rst diff --git a/doc/source/users/resources/clustering/index.rst b/doc/source/user/resources/clustering/index.rst similarity index 100% rename from doc/source/users/resources/clustering/index.rst rename to doc/source/user/resources/clustering/index.rst diff --git a/doc/source/users/resources/clustering/v1/action.rst b/doc/source/user/resources/clustering/v1/action.rst similarity index 100% rename from doc/source/users/resources/clustering/v1/action.rst rename to doc/source/user/resources/clustering/v1/action.rst diff --git a/doc/source/users/resources/clustering/v1/build_info.rst b/doc/source/user/resources/clustering/v1/build_info.rst similarity index 100% rename from doc/source/users/resources/clustering/v1/build_info.rst rename to doc/source/user/resources/clustering/v1/build_info.rst diff --git a/doc/source/users/resources/clustering/v1/cluster.rst b/doc/source/user/resources/clustering/v1/cluster.rst similarity index 100% rename from doc/source/users/resources/clustering/v1/cluster.rst rename to doc/source/user/resources/clustering/v1/cluster.rst diff --git a/doc/source/users/resources/clustering/v1/cluster_policy.rst b/doc/source/user/resources/clustering/v1/cluster_policy.rst similarity index 100% rename from doc/source/users/resources/clustering/v1/cluster_policy.rst rename to doc/source/user/resources/clustering/v1/cluster_policy.rst diff --git a/doc/source/users/resources/clustering/v1/event.rst b/doc/source/user/resources/clustering/v1/event.rst similarity index 100% rename from doc/source/users/resources/clustering/v1/event.rst rename to doc/source/user/resources/clustering/v1/event.rst diff --git a/doc/source/users/resources/clustering/v1/node.rst b/doc/source/user/resources/clustering/v1/node.rst similarity index 100% rename from doc/source/users/resources/clustering/v1/node.rst rename to doc/source/user/resources/clustering/v1/node.rst diff --git a/doc/source/users/resources/clustering/v1/policy.rst b/doc/source/user/resources/clustering/v1/policy.rst similarity index 100% rename from doc/source/users/resources/clustering/v1/policy.rst rename to doc/source/user/resources/clustering/v1/policy.rst diff --git a/doc/source/users/resources/clustering/v1/policy_type.rst b/doc/source/user/resources/clustering/v1/policy_type.rst similarity index 100% rename from doc/source/users/resources/clustering/v1/policy_type.rst rename to doc/source/user/resources/clustering/v1/policy_type.rst diff --git a/doc/source/users/resources/clustering/v1/profile.rst b/doc/source/user/resources/clustering/v1/profile.rst similarity index 100% rename from doc/source/users/resources/clustering/v1/profile.rst rename to doc/source/user/resources/clustering/v1/profile.rst diff --git a/doc/source/users/resources/clustering/v1/profile_type.rst b/doc/source/user/resources/clustering/v1/profile_type.rst similarity index 100% rename from doc/source/users/resources/clustering/v1/profile_type.rst rename to doc/source/user/resources/clustering/v1/profile_type.rst diff --git a/doc/source/users/resources/clustering/v1/receiver.rst b/doc/source/user/resources/clustering/v1/receiver.rst similarity index 100% rename from doc/source/users/resources/clustering/v1/receiver.rst rename to doc/source/user/resources/clustering/v1/receiver.rst diff --git a/doc/source/users/resources/compute/index.rst b/doc/source/user/resources/compute/index.rst similarity index 100% rename from doc/source/users/resources/compute/index.rst rename to doc/source/user/resources/compute/index.rst diff --git a/doc/source/users/resources/compute/v2/extension.rst b/doc/source/user/resources/compute/v2/extension.rst similarity index 100% rename from doc/source/users/resources/compute/v2/extension.rst rename to doc/source/user/resources/compute/v2/extension.rst diff --git a/doc/source/users/resources/compute/v2/flavor.rst b/doc/source/user/resources/compute/v2/flavor.rst similarity index 100% rename from doc/source/users/resources/compute/v2/flavor.rst rename to doc/source/user/resources/compute/v2/flavor.rst diff --git a/doc/source/users/resources/compute/v2/image.rst b/doc/source/user/resources/compute/v2/image.rst similarity index 100% rename from doc/source/users/resources/compute/v2/image.rst rename to doc/source/user/resources/compute/v2/image.rst diff --git a/doc/source/users/resources/compute/v2/keypair.rst b/doc/source/user/resources/compute/v2/keypair.rst similarity index 100% rename from doc/source/users/resources/compute/v2/keypair.rst rename to doc/source/user/resources/compute/v2/keypair.rst diff --git a/doc/source/users/resources/compute/v2/limits.rst b/doc/source/user/resources/compute/v2/limits.rst similarity index 100% rename from doc/source/users/resources/compute/v2/limits.rst rename to doc/source/user/resources/compute/v2/limits.rst diff --git a/doc/source/users/resources/compute/v2/server.rst b/doc/source/user/resources/compute/v2/server.rst similarity index 100% rename from doc/source/users/resources/compute/v2/server.rst rename to doc/source/user/resources/compute/v2/server.rst diff --git a/doc/source/users/resources/compute/v2/server_interface.rst b/doc/source/user/resources/compute/v2/server_interface.rst similarity index 100% rename from doc/source/users/resources/compute/v2/server_interface.rst rename to doc/source/user/resources/compute/v2/server_interface.rst diff --git a/doc/source/users/resources/compute/v2/server_ip.rst b/doc/source/user/resources/compute/v2/server_ip.rst similarity index 100% rename from doc/source/users/resources/compute/v2/server_ip.rst rename to doc/source/user/resources/compute/v2/server_ip.rst diff --git a/doc/source/users/resources/database/index.rst b/doc/source/user/resources/database/index.rst similarity index 100% rename from doc/source/users/resources/database/index.rst rename to doc/source/user/resources/database/index.rst diff --git a/doc/source/users/resources/database/v1/database.rst b/doc/source/user/resources/database/v1/database.rst similarity index 100% rename from doc/source/users/resources/database/v1/database.rst rename to doc/source/user/resources/database/v1/database.rst diff --git a/doc/source/users/resources/database/v1/flavor.rst b/doc/source/user/resources/database/v1/flavor.rst similarity index 100% rename from doc/source/users/resources/database/v1/flavor.rst rename to doc/source/user/resources/database/v1/flavor.rst diff --git a/doc/source/users/resources/database/v1/instance.rst b/doc/source/user/resources/database/v1/instance.rst similarity index 100% rename from doc/source/users/resources/database/v1/instance.rst rename to doc/source/user/resources/database/v1/instance.rst diff --git a/doc/source/users/resources/database/v1/user.rst b/doc/source/user/resources/database/v1/user.rst similarity index 100% rename from doc/source/users/resources/database/v1/user.rst rename to doc/source/user/resources/database/v1/user.rst diff --git a/doc/source/users/resources/identity/index.rst b/doc/source/user/resources/identity/index.rst similarity index 100% rename from doc/source/users/resources/identity/index.rst rename to doc/source/user/resources/identity/index.rst diff --git a/doc/source/users/resources/identity/v2/extension.rst b/doc/source/user/resources/identity/v2/extension.rst similarity index 100% rename from doc/source/users/resources/identity/v2/extension.rst rename to doc/source/user/resources/identity/v2/extension.rst diff --git a/doc/source/users/resources/identity/v2/role.rst b/doc/source/user/resources/identity/v2/role.rst similarity index 100% rename from doc/source/users/resources/identity/v2/role.rst rename to doc/source/user/resources/identity/v2/role.rst diff --git a/doc/source/users/resources/identity/v2/tenant.rst b/doc/source/user/resources/identity/v2/tenant.rst similarity index 100% rename from doc/source/users/resources/identity/v2/tenant.rst rename to doc/source/user/resources/identity/v2/tenant.rst diff --git a/doc/source/users/resources/identity/v2/user.rst b/doc/source/user/resources/identity/v2/user.rst similarity index 100% rename from doc/source/users/resources/identity/v2/user.rst rename to doc/source/user/resources/identity/v2/user.rst diff --git a/doc/source/users/resources/identity/v3/credential.rst b/doc/source/user/resources/identity/v3/credential.rst similarity index 100% rename from doc/source/users/resources/identity/v3/credential.rst rename to doc/source/user/resources/identity/v3/credential.rst diff --git a/doc/source/users/resources/identity/v3/domain.rst b/doc/source/user/resources/identity/v3/domain.rst similarity index 100% rename from doc/source/users/resources/identity/v3/domain.rst rename to doc/source/user/resources/identity/v3/domain.rst diff --git a/doc/source/users/resources/identity/v3/endpoint.rst b/doc/source/user/resources/identity/v3/endpoint.rst similarity index 100% rename from doc/source/users/resources/identity/v3/endpoint.rst rename to doc/source/user/resources/identity/v3/endpoint.rst diff --git a/doc/source/users/resources/identity/v3/group.rst b/doc/source/user/resources/identity/v3/group.rst similarity index 100% rename from doc/source/users/resources/identity/v3/group.rst rename to doc/source/user/resources/identity/v3/group.rst diff --git a/doc/source/users/resources/identity/v3/policy.rst b/doc/source/user/resources/identity/v3/policy.rst similarity index 100% rename from doc/source/users/resources/identity/v3/policy.rst rename to doc/source/user/resources/identity/v3/policy.rst diff --git a/doc/source/users/resources/identity/v3/project.rst b/doc/source/user/resources/identity/v3/project.rst similarity index 100% rename from doc/source/users/resources/identity/v3/project.rst rename to doc/source/user/resources/identity/v3/project.rst diff --git a/doc/source/users/resources/identity/v3/service.rst b/doc/source/user/resources/identity/v3/service.rst similarity index 100% rename from doc/source/users/resources/identity/v3/service.rst rename to doc/source/user/resources/identity/v3/service.rst diff --git a/doc/source/users/resources/identity/v3/trust.rst b/doc/source/user/resources/identity/v3/trust.rst similarity index 100% rename from doc/source/users/resources/identity/v3/trust.rst rename to doc/source/user/resources/identity/v3/trust.rst diff --git a/doc/source/users/resources/identity/v3/user.rst b/doc/source/user/resources/identity/v3/user.rst similarity index 100% rename from doc/source/users/resources/identity/v3/user.rst rename to doc/source/user/resources/identity/v3/user.rst diff --git a/doc/source/users/resources/image/index.rst b/doc/source/user/resources/image/index.rst similarity index 100% rename from doc/source/users/resources/image/index.rst rename to doc/source/user/resources/image/index.rst diff --git a/doc/source/users/resources/image/v1/image.rst b/doc/source/user/resources/image/v1/image.rst similarity index 100% rename from doc/source/users/resources/image/v1/image.rst rename to doc/source/user/resources/image/v1/image.rst diff --git a/doc/source/users/resources/image/v2/image.rst b/doc/source/user/resources/image/v2/image.rst similarity index 100% rename from doc/source/users/resources/image/v2/image.rst rename to doc/source/user/resources/image/v2/image.rst diff --git a/doc/source/users/resources/image/v2/member.rst b/doc/source/user/resources/image/v2/member.rst similarity index 100% rename from doc/source/users/resources/image/v2/member.rst rename to doc/source/user/resources/image/v2/member.rst diff --git a/doc/source/users/resources/key_manager/index.rst b/doc/source/user/resources/key_manager/index.rst similarity index 100% rename from doc/source/users/resources/key_manager/index.rst rename to doc/source/user/resources/key_manager/index.rst diff --git a/doc/source/users/resources/key_manager/v1/container.rst b/doc/source/user/resources/key_manager/v1/container.rst similarity index 100% rename from doc/source/users/resources/key_manager/v1/container.rst rename to doc/source/user/resources/key_manager/v1/container.rst diff --git a/doc/source/users/resources/key_manager/v1/order.rst b/doc/source/user/resources/key_manager/v1/order.rst similarity index 100% rename from doc/source/users/resources/key_manager/v1/order.rst rename to doc/source/user/resources/key_manager/v1/order.rst diff --git a/doc/source/users/resources/key_manager/v1/secret.rst b/doc/source/user/resources/key_manager/v1/secret.rst similarity index 100% rename from doc/source/users/resources/key_manager/v1/secret.rst rename to doc/source/user/resources/key_manager/v1/secret.rst diff --git a/doc/source/users/resources/load_balancer/index.rst b/doc/source/user/resources/load_balancer/index.rst similarity index 100% rename from doc/source/users/resources/load_balancer/index.rst rename to doc/source/user/resources/load_balancer/index.rst diff --git a/doc/source/users/resources/load_balancer/v2/health_monitor.rst b/doc/source/user/resources/load_balancer/v2/health_monitor.rst similarity index 100% rename from doc/source/users/resources/load_balancer/v2/health_monitor.rst rename to doc/source/user/resources/load_balancer/v2/health_monitor.rst diff --git a/doc/source/users/resources/load_balancer/v2/l7_policy.rst b/doc/source/user/resources/load_balancer/v2/l7_policy.rst similarity index 100% rename from doc/source/users/resources/load_balancer/v2/l7_policy.rst rename to doc/source/user/resources/load_balancer/v2/l7_policy.rst diff --git a/doc/source/users/resources/load_balancer/v2/l7_rule.rst b/doc/source/user/resources/load_balancer/v2/l7_rule.rst similarity index 100% rename from doc/source/users/resources/load_balancer/v2/l7_rule.rst rename to doc/source/user/resources/load_balancer/v2/l7_rule.rst diff --git a/doc/source/users/resources/load_balancer/v2/listener.rst b/doc/source/user/resources/load_balancer/v2/listener.rst similarity index 100% rename from doc/source/users/resources/load_balancer/v2/listener.rst rename to doc/source/user/resources/load_balancer/v2/listener.rst diff --git a/doc/source/users/resources/load_balancer/v2/load_balancer.rst b/doc/source/user/resources/load_balancer/v2/load_balancer.rst similarity index 100% rename from doc/source/users/resources/load_balancer/v2/load_balancer.rst rename to doc/source/user/resources/load_balancer/v2/load_balancer.rst diff --git a/doc/source/users/resources/load_balancer/v2/member.rst b/doc/source/user/resources/load_balancer/v2/member.rst similarity index 100% rename from doc/source/users/resources/load_balancer/v2/member.rst rename to doc/source/user/resources/load_balancer/v2/member.rst diff --git a/doc/source/users/resources/load_balancer/v2/pool.rst b/doc/source/user/resources/load_balancer/v2/pool.rst similarity index 100% rename from doc/source/users/resources/load_balancer/v2/pool.rst rename to doc/source/user/resources/load_balancer/v2/pool.rst diff --git a/doc/source/users/resources/meter/index.rst b/doc/source/user/resources/meter/index.rst similarity index 100% rename from doc/source/users/resources/meter/index.rst rename to doc/source/user/resources/meter/index.rst diff --git a/doc/source/users/resources/meter/v2/capability.rst b/doc/source/user/resources/meter/v2/capability.rst similarity index 100% rename from doc/source/users/resources/meter/v2/capability.rst rename to doc/source/user/resources/meter/v2/capability.rst diff --git a/doc/source/users/resources/meter/v2/meter.rst b/doc/source/user/resources/meter/v2/meter.rst similarity index 100% rename from doc/source/users/resources/meter/v2/meter.rst rename to doc/source/user/resources/meter/v2/meter.rst diff --git a/doc/source/users/resources/meter/v2/resource.rst b/doc/source/user/resources/meter/v2/resource.rst similarity index 100% rename from doc/source/users/resources/meter/v2/resource.rst rename to doc/source/user/resources/meter/v2/resource.rst diff --git a/doc/source/users/resources/meter/v2/sample.rst b/doc/source/user/resources/meter/v2/sample.rst similarity index 100% rename from doc/source/users/resources/meter/v2/sample.rst rename to doc/source/user/resources/meter/v2/sample.rst diff --git a/doc/source/users/resources/meter/v2/statistics.rst b/doc/source/user/resources/meter/v2/statistics.rst similarity index 100% rename from doc/source/users/resources/meter/v2/statistics.rst rename to doc/source/user/resources/meter/v2/statistics.rst diff --git a/doc/source/users/resources/metric/index.rst b/doc/source/user/resources/metric/index.rst similarity index 100% rename from doc/source/users/resources/metric/index.rst rename to doc/source/user/resources/metric/index.rst diff --git a/doc/source/users/resources/metric/v1/archive_policy.rst b/doc/source/user/resources/metric/v1/archive_policy.rst similarity index 100% rename from doc/source/users/resources/metric/v1/archive_policy.rst rename to doc/source/user/resources/metric/v1/archive_policy.rst diff --git a/doc/source/users/resources/metric/v1/capabilities.rst b/doc/source/user/resources/metric/v1/capabilities.rst similarity index 100% rename from doc/source/users/resources/metric/v1/capabilities.rst rename to doc/source/user/resources/metric/v1/capabilities.rst diff --git a/doc/source/users/resources/metric/v1/metric.rst b/doc/source/user/resources/metric/v1/metric.rst similarity index 100% rename from doc/source/users/resources/metric/v1/metric.rst rename to doc/source/user/resources/metric/v1/metric.rst diff --git a/doc/source/users/resources/metric/v1/resource.rst b/doc/source/user/resources/metric/v1/resource.rst similarity index 100% rename from doc/source/users/resources/metric/v1/resource.rst rename to doc/source/user/resources/metric/v1/resource.rst diff --git a/doc/source/users/resources/network/index.rst b/doc/source/user/resources/network/index.rst similarity index 100% rename from doc/source/users/resources/network/index.rst rename to doc/source/user/resources/network/index.rst diff --git a/doc/source/users/resources/network/v2/address_scope.rst b/doc/source/user/resources/network/v2/address_scope.rst similarity index 100% rename from doc/source/users/resources/network/v2/address_scope.rst rename to doc/source/user/resources/network/v2/address_scope.rst diff --git a/doc/source/users/resources/network/v2/agent.rst b/doc/source/user/resources/network/v2/agent.rst similarity index 100% rename from doc/source/users/resources/network/v2/agent.rst rename to doc/source/user/resources/network/v2/agent.rst diff --git a/doc/source/users/resources/network/v2/auto_allocated_topology.rst b/doc/source/user/resources/network/v2/auto_allocated_topology.rst similarity index 100% rename from doc/source/users/resources/network/v2/auto_allocated_topology.rst rename to doc/source/user/resources/network/v2/auto_allocated_topology.rst diff --git a/doc/source/users/resources/network/v2/availability_zone.rst b/doc/source/user/resources/network/v2/availability_zone.rst similarity index 100% rename from doc/source/users/resources/network/v2/availability_zone.rst rename to doc/source/user/resources/network/v2/availability_zone.rst diff --git a/doc/source/users/resources/network/v2/extension.rst b/doc/source/user/resources/network/v2/extension.rst similarity index 100% rename from doc/source/users/resources/network/v2/extension.rst rename to doc/source/user/resources/network/v2/extension.rst diff --git a/doc/source/users/resources/network/v2/flavor.rst b/doc/source/user/resources/network/v2/flavor.rst similarity index 100% rename from doc/source/users/resources/network/v2/flavor.rst rename to doc/source/user/resources/network/v2/flavor.rst diff --git a/doc/source/users/resources/network/v2/floating_ip.rst b/doc/source/user/resources/network/v2/floating_ip.rst similarity index 100% rename from doc/source/users/resources/network/v2/floating_ip.rst rename to doc/source/user/resources/network/v2/floating_ip.rst diff --git a/doc/source/users/resources/network/v2/health_monitor.rst b/doc/source/user/resources/network/v2/health_monitor.rst similarity index 100% rename from doc/source/users/resources/network/v2/health_monitor.rst rename to doc/source/user/resources/network/v2/health_monitor.rst diff --git a/doc/source/users/resources/network/v2/listener.rst b/doc/source/user/resources/network/v2/listener.rst similarity index 100% rename from doc/source/users/resources/network/v2/listener.rst rename to doc/source/user/resources/network/v2/listener.rst diff --git a/doc/source/users/resources/network/v2/load_balancer.rst b/doc/source/user/resources/network/v2/load_balancer.rst similarity index 100% rename from doc/source/users/resources/network/v2/load_balancer.rst rename to doc/source/user/resources/network/v2/load_balancer.rst diff --git a/doc/source/users/resources/network/v2/metering_label.rst b/doc/source/user/resources/network/v2/metering_label.rst similarity index 100% rename from doc/source/users/resources/network/v2/metering_label.rst rename to doc/source/user/resources/network/v2/metering_label.rst diff --git a/doc/source/users/resources/network/v2/metering_label_rule.rst b/doc/source/user/resources/network/v2/metering_label_rule.rst similarity index 100% rename from doc/source/users/resources/network/v2/metering_label_rule.rst rename to doc/source/user/resources/network/v2/metering_label_rule.rst diff --git a/doc/source/users/resources/network/v2/network.rst b/doc/source/user/resources/network/v2/network.rst similarity index 100% rename from doc/source/users/resources/network/v2/network.rst rename to doc/source/user/resources/network/v2/network.rst diff --git a/doc/source/users/resources/network/v2/network_ip_availability.rst b/doc/source/user/resources/network/v2/network_ip_availability.rst similarity index 100% rename from doc/source/users/resources/network/v2/network_ip_availability.rst rename to doc/source/user/resources/network/v2/network_ip_availability.rst diff --git a/doc/source/users/resources/network/v2/pool.rst b/doc/source/user/resources/network/v2/pool.rst similarity index 100% rename from doc/source/users/resources/network/v2/pool.rst rename to doc/source/user/resources/network/v2/pool.rst diff --git a/doc/source/users/resources/network/v2/pool_member.rst b/doc/source/user/resources/network/v2/pool_member.rst similarity index 100% rename from doc/source/users/resources/network/v2/pool_member.rst rename to doc/source/user/resources/network/v2/pool_member.rst diff --git a/doc/source/users/resources/network/v2/port.rst b/doc/source/user/resources/network/v2/port.rst similarity index 100% rename from doc/source/users/resources/network/v2/port.rst rename to doc/source/user/resources/network/v2/port.rst diff --git a/doc/source/users/resources/network/v2/qos_bandwidth_limit_rule.rst b/doc/source/user/resources/network/v2/qos_bandwidth_limit_rule.rst similarity index 100% rename from doc/source/users/resources/network/v2/qos_bandwidth_limit_rule.rst rename to doc/source/user/resources/network/v2/qos_bandwidth_limit_rule.rst diff --git a/doc/source/users/resources/network/v2/qos_dscp_marking_rule.rst b/doc/source/user/resources/network/v2/qos_dscp_marking_rule.rst similarity index 100% rename from doc/source/users/resources/network/v2/qos_dscp_marking_rule.rst rename to doc/source/user/resources/network/v2/qos_dscp_marking_rule.rst diff --git a/doc/source/users/resources/network/v2/qos_minimum_bandwidth_rule.rst b/doc/source/user/resources/network/v2/qos_minimum_bandwidth_rule.rst similarity index 100% rename from doc/source/users/resources/network/v2/qos_minimum_bandwidth_rule.rst rename to doc/source/user/resources/network/v2/qos_minimum_bandwidth_rule.rst diff --git a/doc/source/users/resources/network/v2/qos_policy.rst b/doc/source/user/resources/network/v2/qos_policy.rst similarity index 100% rename from doc/source/users/resources/network/v2/qos_policy.rst rename to doc/source/user/resources/network/v2/qos_policy.rst diff --git a/doc/source/users/resources/network/v2/qos_rule_type.rst b/doc/source/user/resources/network/v2/qos_rule_type.rst similarity index 100% rename from doc/source/users/resources/network/v2/qos_rule_type.rst rename to doc/source/user/resources/network/v2/qos_rule_type.rst diff --git a/doc/source/users/resources/network/v2/quota.rst b/doc/source/user/resources/network/v2/quota.rst similarity index 100% rename from doc/source/users/resources/network/v2/quota.rst rename to doc/source/user/resources/network/v2/quota.rst diff --git a/doc/source/users/resources/network/v2/rbac_policy.rst b/doc/source/user/resources/network/v2/rbac_policy.rst similarity index 100% rename from doc/source/users/resources/network/v2/rbac_policy.rst rename to doc/source/user/resources/network/v2/rbac_policy.rst diff --git a/doc/source/users/resources/network/v2/router.rst b/doc/source/user/resources/network/v2/router.rst similarity index 100% rename from doc/source/users/resources/network/v2/router.rst rename to doc/source/user/resources/network/v2/router.rst diff --git a/doc/source/users/resources/network/v2/security_group.rst b/doc/source/user/resources/network/v2/security_group.rst similarity index 100% rename from doc/source/users/resources/network/v2/security_group.rst rename to doc/source/user/resources/network/v2/security_group.rst diff --git a/doc/source/users/resources/network/v2/security_group_rule.rst b/doc/source/user/resources/network/v2/security_group_rule.rst similarity index 100% rename from doc/source/users/resources/network/v2/security_group_rule.rst rename to doc/source/user/resources/network/v2/security_group_rule.rst diff --git a/doc/source/users/resources/network/v2/segment.rst b/doc/source/user/resources/network/v2/segment.rst similarity index 100% rename from doc/source/users/resources/network/v2/segment.rst rename to doc/source/user/resources/network/v2/segment.rst diff --git a/doc/source/users/resources/network/v2/service_profile.rst b/doc/source/user/resources/network/v2/service_profile.rst similarity index 100% rename from doc/source/users/resources/network/v2/service_profile.rst rename to doc/source/user/resources/network/v2/service_profile.rst diff --git a/doc/source/users/resources/network/v2/service_provider.rst b/doc/source/user/resources/network/v2/service_provider.rst similarity index 100% rename from doc/source/users/resources/network/v2/service_provider.rst rename to doc/source/user/resources/network/v2/service_provider.rst diff --git a/doc/source/users/resources/network/v2/subnet.rst b/doc/source/user/resources/network/v2/subnet.rst similarity index 100% rename from doc/source/users/resources/network/v2/subnet.rst rename to doc/source/user/resources/network/v2/subnet.rst diff --git a/doc/source/users/resources/network/v2/subnet_pool.rst b/doc/source/user/resources/network/v2/subnet_pool.rst similarity index 100% rename from doc/source/users/resources/network/v2/subnet_pool.rst rename to doc/source/user/resources/network/v2/subnet_pool.rst diff --git a/doc/source/users/resources/object_store/index.rst b/doc/source/user/resources/object_store/index.rst similarity index 100% rename from doc/source/users/resources/object_store/index.rst rename to doc/source/user/resources/object_store/index.rst diff --git a/doc/source/users/resources/object_store/v1/account.rst b/doc/source/user/resources/object_store/v1/account.rst similarity index 100% rename from doc/source/users/resources/object_store/v1/account.rst rename to doc/source/user/resources/object_store/v1/account.rst diff --git a/doc/source/users/resources/object_store/v1/container.rst b/doc/source/user/resources/object_store/v1/container.rst similarity index 100% rename from doc/source/users/resources/object_store/v1/container.rst rename to doc/source/user/resources/object_store/v1/container.rst diff --git a/doc/source/users/resources/object_store/v1/obj.rst b/doc/source/user/resources/object_store/v1/obj.rst similarity index 100% rename from doc/source/users/resources/object_store/v1/obj.rst rename to doc/source/user/resources/object_store/v1/obj.rst diff --git a/doc/source/users/resources/orchestration/index.rst b/doc/source/user/resources/orchestration/index.rst similarity index 100% rename from doc/source/users/resources/orchestration/index.rst rename to doc/source/user/resources/orchestration/index.rst diff --git a/doc/source/users/resources/orchestration/v1/resource.rst b/doc/source/user/resources/orchestration/v1/resource.rst similarity index 100% rename from doc/source/users/resources/orchestration/v1/resource.rst rename to doc/source/user/resources/orchestration/v1/resource.rst diff --git a/doc/source/users/resources/orchestration/v1/stack.rst b/doc/source/user/resources/orchestration/v1/stack.rst similarity index 100% rename from doc/source/users/resources/orchestration/v1/stack.rst rename to doc/source/user/resources/orchestration/v1/stack.rst diff --git a/doc/source/users/resources/workflow/index.rst b/doc/source/user/resources/workflow/index.rst similarity index 100% rename from doc/source/users/resources/workflow/index.rst rename to doc/source/user/resources/workflow/index.rst diff --git a/doc/source/users/resources/workflow/v2/execution.rst b/doc/source/user/resources/workflow/v2/execution.rst similarity index 100% rename from doc/source/users/resources/workflow/v2/execution.rst rename to doc/source/user/resources/workflow/v2/execution.rst diff --git a/doc/source/users/resources/workflow/v2/workflow.rst b/doc/source/user/resources/workflow/v2/workflow.rst similarity index 100% rename from doc/source/users/resources/workflow/v2/workflow.rst rename to doc/source/user/resources/workflow/v2/workflow.rst diff --git a/doc/source/users/service_filter.rst b/doc/source/user/service_filter.rst similarity index 100% rename from doc/source/users/service_filter.rst rename to doc/source/user/service_filter.rst diff --git a/doc/source/users/utils.rst b/doc/source/user/utils.rst similarity index 100% rename from doc/source/users/utils.rst rename to doc/source/user/utils.rst diff --git a/doc/source/users/examples b/doc/source/users/examples deleted file mode 120000 index d4cb9b9c8..000000000 --- a/doc/source/users/examples +++ /dev/null @@ -1 +0,0 @@ -../../../examples/ \ No newline at end of file diff --git a/doc/source/users/index.rst b/doc/source/users/index.rst deleted file mode 100644 index ded0ad57f..000000000 --- a/doc/source/users/index.rst +++ /dev/null @@ -1,139 +0,0 @@ -Getting started with the OpenStack SDK -====================================== - -For a listing of terms used throughout the SDK, including the names of -projects and services supported by it, see the :doc:`glossary <../glossary>`. - -Installation ------------- - -The OpenStack SDK is available on -`PyPI `_ under the name -**openstacksdk**. To install it, use ``pip``:: - - $ pip install openstacksdk - -.. _user_guides: - -User Guides ------------ - -These guides walk you through how to make use of the libraries we provide -to work with each OpenStack service. If you're looking for a cookbook -approach, this is where you'll want to begin. - -.. toctree:: - :maxdepth: 1 - - Connect to an OpenStack Cloud - Connect to an OpenStack Cloud Using a Config File - Logging - Baremetal - Block Storage - Clustering - Compute - Database - Identity - Image - Key Manager - Message - Meter - Network - Object Store - Orchestration - -API Documentation ------------------ - -Service APIs are exposed through a two-layered approach. The classes -exposed through our *Connection* interface are the place to start if you're -an application developer consuming an OpenStack cloud. The *Resource* -interface is the layer upon which the *Connection* is built, with -*Connection* methods accepting and returning *Resource* objects. - -Connection Interface -******************** - -A *Connection* instance maintains your cloud config, session and authentication -information providing you with a set of higher-level interfaces to work with -OpenStack services. - -.. toctree:: - :maxdepth: 1 - - connection - -Once you have a *Connection* instance, the following services may be exposed -to you. The combination of your ``CloudRegion`` and the catalog of the cloud -in question control which services are exposed, but listed below are the ones -provided by the SDK. - -.. toctree:: - :maxdepth: 1 - - Baremetal - Block Storage - Clustering - Compute - Database - Identity v2 - Identity v3 - Image v1 - Image v2 - Key Manager - Load Balancer - Message v1 - Message v2 - Network - Meter - Metric - Object Store - Orchestration - Workflow - -Resource Interface -****************** - -The *Resource* layer is a lower-level interface to communicate with OpenStack -services. While the classes exposed by the *Connection* build a convenience -layer on top of this, *Resources* can be used directly. However, the most -common usage of this layer is in receiving an object from a class in the -*Connection* layer, modifying it, and sending it back into the *Connection* -layer, such as to update a resource on the server. - -The following services have exposed *Resource* classes. - -.. toctree:: - :maxdepth: 1 - - Baremetal - Block Storage - Clustering - Compute - Database - Identity - Image - Key Management - Load Balancer - Meter - Metric - Network - Orchestration - Object Store - Workflow - -Low-Level Classes -***************** - -The following classes are not commonly used by application developers, -but are used to construct applications to talk to OpenStack APIs. Typically -these parts are managed through the `Connection Interface`_, but their use -can be customized. - -.. toctree:: - :maxdepth: 1 - - resource - resource2 - service_filter - utils diff --git a/examples/cloud/cleanup-servers.py b/examples/cloud/cleanup-servers.py new file mode 100644 index 000000000..7f9257b58 --- /dev/null +++ b/examples/cloud/cleanup-servers.py @@ -0,0 +1,26 @@ +# 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 openstack + +# Initialize and turn on debug logging +openstack.simple_logging(debug=True) + +for cloud_name, region_name in [ + ('my-vexxhost', 'ca-ymq-1'), + ('my-citycloud', 'Buf1'), + ('my-internap', 'ams01')]: + # Initialize cloud + cloud = openstack.openstack_cloud( + cloud=cloud_name, region_name=region_name) + for server in cloud.search_servers('my-server'): + cloud.delete_server(server, wait=True, delete_ips=True) diff --git a/doc/source/user/examples/create-server-dict.py b/examples/cloud/create-server-dict.py similarity index 52% rename from doc/source/user/examples/create-server-dict.py rename to examples/cloud/create-server-dict.py index 3f9fc8223..4557ee188 100644 --- a/doc/source/user/examples/create-server-dict.py +++ b/examples/cloud/create-server-dict.py @@ -1,7 +1,19 @@ -import openstack.cloud +# 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 openstack # Initialize and turn on debug logging -openstack.cloud.simple_logging(debug=True) +openstack.simple_logging(debug=True) for cloud_name, region_name, image, flavor_id in [ ('my-vexxhost', 'ca-ymq-1', 'Ubuntu 16.04.1 LTS [2017-03-03]', @@ -11,7 +23,8 @@ ('my-internap', 'ams01', 'Ubuntu 16.04 LTS (Xenial Xerus)', 'A1.4')]: # Initialize cloud - cloud = openstack.openstack_cloud(cloud=cloud_name, region_name=region_name) + cloud = openstack.openstack_cloud( + cloud=cloud_name, region_name=region_name) # Boot a server, wait for it to boot, and then do whatever is needed # to get a public ip for it. diff --git a/doc/source/user/examples/create-server-name-or-id.py b/examples/cloud/create-server-name-or-id.py similarity index 54% rename from doc/source/user/examples/create-server-name-or-id.py rename to examples/cloud/create-server-name-or-id.py index 16c011aa1..a269f2fe6 100644 --- a/doc/source/user/examples/create-server-name-or-id.py +++ b/examples/cloud/create-server-name-or-id.py @@ -1,7 +1,19 @@ -import openstack.cloud +# 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 openstack # Initialize and turn on debug logging -openstack.cloud.simple_logging(debug=True) +openstack.simple_logging(debug=True) for cloud_name, region_name, image, flavor in [ ('my-vexxhost', 'ca-ymq-1', @@ -11,7 +23,8 @@ ('my-internap', 'ams01', 'Ubuntu 16.04 LTS (Xenial Xerus)', 'A1.4')]: # Initialize cloud - cloud = openstack.openstack_cloud(cloud=cloud_name, region_name=region_name) + cloud = openstack.openstack_cloud( + cloud=cloud_name, region_name=region_name) cloud.delete_server('my-server', wait=True, delete_ips=True) # Boot a server, wait for it to boot, and then do whatever is needed diff --git a/examples/cloud/debug-logging.py b/examples/cloud/debug-logging.py new file mode 100644 index 000000000..a9cc31c2b --- /dev/null +++ b/examples/cloud/debug-logging.py @@ -0,0 +1,18 @@ +# 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 openstack +openstack.simple_logging(debug=True) + +cloud = openstack.openstack_cloud( + cloud='my-vexxhost', region_name='ca-ymq-1') +cloud.get_image('Ubuntu 16.04.1 LTS [2017-03-03]') diff --git a/examples/cloud/find-an-image.py b/examples/cloud/find-an-image.py new file mode 100644 index 000000000..089b08640 --- /dev/null +++ b/examples/cloud/find-an-image.py @@ -0,0 +1,19 @@ +# 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 openstack +openstack.simple_logging() + +cloud = openstack.openstack_cloud(cloud='fuga', region_name='cystack') +cloud.pprint([ + image for image in cloud.list_images() + if 'ubuntu' in image.name.lower()]) diff --git a/examples/cloud/http-debug-logging.py b/examples/cloud/http-debug-logging.py new file mode 100644 index 000000000..e5b1a7e9d --- /dev/null +++ b/examples/cloud/http-debug-logging.py @@ -0,0 +1,18 @@ +# 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 openstack +openstack.simple_logging(http_debug=True) + +cloud = openstack.openstack_cloud( + cloud='my-vexxhost', region_name='ca-ymq-1') +cloud.get_image('Ubuntu 16.04.1 LTS [2017-03-03]') diff --git a/examples/cloud/munch-dict-object.py b/examples/cloud/munch-dict-object.py new file mode 100644 index 000000000..df3a0e76c --- /dev/null +++ b/examples/cloud/munch-dict-object.py @@ -0,0 +1,19 @@ +# 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 openstack +openstack.simple_logging(debug=True) + +cloud = openstack.openstack_cloud(cloud='ovh', region_name='SBG1') +image = cloud.get_image('Ubuntu 16.10') +print(image.name) +print(image['name']) diff --git a/examples/cloud/normalization.py b/examples/cloud/normalization.py new file mode 100644 index 000000000..80f1ff4c9 --- /dev/null +++ b/examples/cloud/normalization.py @@ -0,0 +1,19 @@ +# 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 openstack +openstack.simple_logging() + +cloud = openstack.openstack_cloud(cloud='fuga', region_name='cystack') +image = cloud.get_image( + 'Ubuntu 16.04 LTS - Xenial Xerus - 64-bit - Fuga Cloud Based Image') +cloud.pprint(image) diff --git a/doc/source/user/examples/server-information.py b/examples/cloud/server-information.py similarity index 51% rename from doc/source/user/examples/server-information.py rename to examples/cloud/server-information.py index 5d9599d06..8ae710631 100644 --- a/doc/source/user/examples/server-information.py +++ b/examples/cloud/server-information.py @@ -1,5 +1,17 @@ -import openstack.cloud -openstack.cloud.simple_logging(debug=True) +# 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 openstack +openstack.simple_logging(debug=True) cloud = openstack.openstack_cloud(cloud='my-citycloud', region_name='Buf1') try: @@ -20,4 +32,3 @@ finally: # Delete it - this is a demo cloud.delete_server(server, wait=True, delete_ips=True) - diff --git a/examples/cloud/service-conditional-overrides.py b/examples/cloud/service-conditional-overrides.py new file mode 100644 index 000000000..7991a1ec3 --- /dev/null +++ b/examples/cloud/service-conditional-overrides.py @@ -0,0 +1,17 @@ +# 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 openstack +openstack.simple_logging(debug=True) + +cloud = openstack.openstack_cloud(cloud='rax', region_name='DFW') +print(cloud.has_service('network')) diff --git a/examples/cloud/service-conditionals.py b/examples/cloud/service-conditionals.py new file mode 100644 index 000000000..5e81d1dad --- /dev/null +++ b/examples/cloud/service-conditionals.py @@ -0,0 +1,18 @@ +# 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 openstack +openstack.simple_logging(debug=True) + +cloud = openstack.openstack_cloud(cloud='kiss', region_name='region1') +print(cloud.has_service('network')) +print(cloud.has_service('container-orchestration')) diff --git a/examples/cloud/strict-mode.py b/examples/cloud/strict-mode.py new file mode 100644 index 000000000..74cfd05ff --- /dev/null +++ b/examples/cloud/strict-mode.py @@ -0,0 +1,20 @@ +# 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 openstack +openstack.simple_logging() + +cloud = openstack.openstack_cloud( + cloud='fuga', region_name='cystack', strict=True) +image = cloud.get_image( + 'Ubuntu 16.04 LTS - Xenial Xerus - 64-bit - Fuga Cloud Based Image') +cloud.pprint(image) diff --git a/examples/cloud/upload-large-object.py b/examples/cloud/upload-large-object.py new file mode 100644 index 000000000..f274c2e7e --- /dev/null +++ b/examples/cloud/upload-large-object.py @@ -0,0 +1,22 @@ +# 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 openstack +openstack.simple_logging(debug=True) + +cloud = openstack.openstack_cloud(cloud='ovh', region_name='SBG1') +cloud.create_object( + container='my-container', name='my-object', + filename='/home/mordred/briarcliff.sh3d', + segment_size=1000000) +cloud.delete_object('my-container', 'my-object') +cloud.delete_container('my-container') diff --git a/examples/cloud/upload-object.py b/examples/cloud/upload-object.py new file mode 100644 index 000000000..f274c2e7e --- /dev/null +++ b/examples/cloud/upload-object.py @@ -0,0 +1,22 @@ +# 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 openstack +openstack.simple_logging(debug=True) + +cloud = openstack.openstack_cloud(cloud='ovh', region_name='SBG1') +cloud.create_object( + container='my-container', name='my-object', + filename='/home/mordred/briarcliff.sh3d', + segment_size=1000000) +cloud.delete_object('my-container', 'my-object') +cloud.delete_container('my-container') diff --git a/examples/cloud/user-agent.py b/examples/cloud/user-agent.py new file mode 100644 index 000000000..309355b5e --- /dev/null +++ b/examples/cloud/user-agent.py @@ -0,0 +1,18 @@ +# 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 openstack +openstack.simple_logging(http_debug=True) + +cloud = openstack.openstack_cloud( + cloud='datacentred', app_name='AmazingApp', app_version='1.0') +cloud.list_networks() diff --git a/examples/clustering/policy.py b/examples/clustering/policy.py index 0f37820d2..5d7e8b25e 100644 --- a/examples/clustering/policy.py +++ b/examples/clustering/policy.py @@ -14,7 +14,7 @@ Managing policies in the Cluster service. For a full guide see -https://developer.openstack.org/sdks/python/openstacksdk/users/guides/cluster.html +https://developer.openstack.org/sdks/python/openstacksdk/user/guides/cluster.html """ diff --git a/examples/clustering/policy_type.py b/examples/clustering/policy_type.py index 447ecf265..27c06ac6a 100644 --- a/examples/clustering/policy_type.py +++ b/examples/clustering/policy_type.py @@ -14,7 +14,7 @@ Managing policy types in the Cluster service. For a full guide see -https://developer.openstack.org/sdks/python/openstacksdk/users/guides/cluster.html +https://developer.openstack.org/sdks/python/openstacksdk/user/guides/cluster.html """ diff --git a/examples/clustering/profile.py b/examples/clustering/profile.py index 0fad312c5..397374b8c 100644 --- a/examples/clustering/profile.py +++ b/examples/clustering/profile.py @@ -19,7 +19,7 @@ Managing profiles in the Cluster service. For a full guide see -https://developer.openstack.org/sdks/python/openstacksdk/users/guides/cluster.html +https://developer.openstack.org/sdks/python/openstacksdk/user/guides/cluster.html """ diff --git a/examples/clustering/profile_type.py b/examples/clustering/profile_type.py index 2856e4e0f..9f3f3cc26 100644 --- a/examples/clustering/profile_type.py +++ b/examples/clustering/profile_type.py @@ -14,7 +14,7 @@ Managing profile types in the Cluster service. For a full guide see -https://developer.openstack.org/sdks/python/openstacksdk/users/guides/clustering.html +https://developer.openstack.org/sdks/python/openstacksdk/user/guides/clustering.html """ diff --git a/examples/image/create.py b/examples/image/create.py index 45b4bc7d0..c3d64496b 100644 --- a/examples/image/create.py +++ b/examples/image/create.py @@ -16,7 +16,7 @@ Create resources with the Image service. For a full guide see -http://developer.openstack.org/sdks/python/openstacksdk/users/guides/image.html +http://developer.openstack.org/sdks/python/openstacksdk/user/guides/image.html """ diff --git a/examples/image/delete.py b/examples/image/delete.py index dc6560366..6344bf98a 100644 --- a/examples/image/delete.py +++ b/examples/image/delete.py @@ -16,7 +16,7 @@ Delete resources with the Image service. For a full guide see -http://developer.openstack.org/sdks/python/openstacksdk/users/guides/image.html +http://developer.openstack.org/sdks/python/openstacksdk/user/guides/image.html """ diff --git a/examples/image/download.py b/examples/image/download.py index 8b68dd6aa..f1611116f 100644 --- a/examples/image/download.py +++ b/examples/image/download.py @@ -16,7 +16,7 @@ Download an image with the Image service. For a full guide see -http://developer.openstack.org/sdks/python/openstacksdk/users/guides/image.html +http://developer.openstack.org/sdks/python/openstacksdk/user/guides/image.html """ diff --git a/examples/image/list.py b/examples/image/list.py index decb6e5d9..843e5f965 100644 --- a/examples/image/list.py +++ b/examples/image/list.py @@ -14,7 +14,7 @@ List resources from the Image service. For a full guide see -http://developer.openstack.org/sdks/python/openstacksdk/users/guides/image.html +http://developer.openstack.org/sdks/python/openstacksdk/user/guides/image.html """ From 7edf9f743dcc4e138ac077e241cd6a7f9726093f Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Tue, 16 Jan 2018 08:20:46 +0100 Subject: [PATCH 1901/3836] Use Zuul v3 fetch-subunit-output We have consolidated the fetch output roles into one fetch-subunit-output, replace useage of old roles with new one. Depends-On: I0cdfc66ee8b046affeb0b071fef38c21cb7a4948 Change-Id: I6d1726aa562f05b6d3bee7d0390fc1cf7cef4cd7 --- playbooks/devstack/post.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playbooks/devstack/post.yaml b/playbooks/devstack/post.yaml index db7ca7d67..7f0cb1982 100644 --- a/playbooks/devstack/post.yaml +++ b/playbooks/devstack/post.yaml @@ -1,4 +1,4 @@ - hosts: all roles: - fetch-tox-output - - fetch-stestr-output + - fetch-subunit-output From da2406bace893df7842b21d2bee7867f3a5c7855 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 10 Jan 2018 12:18:34 -0600 Subject: [PATCH 1902/3836] Rationalize logging helpers and docs shade and openstacksdk each have a logging doc and a logging setup helper function. They both basically do the same thing, and we have a TODO item about collapsing them. This moves openstack.utils.enable_logging to openstack.enable_logging (leaving behind a compat piece at openstack.utils.enable_logging) It adds the shade functionality to it, and also changes the behavior to match shade's WRT behavior when no parameters are passed (defaults to logging to stdout) Update the codebase to call openstack._log.setup_logging instead of logging.getLogger directly, as setup_logging attaches a NullHandler by default. Collapse the docs into a single document. There were only two places where openstacksdk was already logging to something other than 'openstack'. Collapse those down to 'openstack' until we come up with a reason to break them out more logically. Change-Id: I45fd5ffd18255450d38a1f56c80f5c157ea19ae3 --- README.rst | 102 ++++++++++---- SHADE-MERGE-TODO.rst | 1 - doc/source/user/guides/logging.rst | 133 +++++++++++------- doc/source/user/index.rst | 3 - doc/source/user/logging.rst | 99 ------------- doc/source/user/multi-cloud-demo.rst | 78 +++++----- examples/cloud/cleanup-servers.py | 2 +- examples/cloud/create-server-dict.py | 2 +- examples/cloud/create-server-name-or-id.py | 2 +- examples/cloud/debug-logging.py | 2 +- examples/cloud/find-an-image.py | 2 +- examples/cloud/http-debug-logging.py | 2 +- examples/cloud/munch-dict-object.py | 2 +- examples/cloud/normalization.py | 2 +- examples/cloud/server-information.py | 2 +- .../cloud/service-conditional-overrides.py | 2 +- examples/cloud/service-conditionals.py | 2 +- examples/cloud/strict-mode.py | 2 +- examples/cloud/upload-large-object.py | 2 +- examples/cloud/upload-object.py | 2 +- examples/cloud/user-agent.py | 2 +- openstack/__init__.py | 75 ++++------ openstack/_log.py | 98 ++++++++++++- openstack/cloud/_utils.py | 2 +- openstack/cloud/meta.py | 4 +- openstack/cloud/openstackcloud.py | 2 +- openstack/config/cloud_region.py | 2 +- openstack/config/loader.py | 2 +- openstack/connection.py | 9 +- openstack/image/v2/image.py | 4 +- openstack/profile.py | 4 +- openstack/task_manager.py | 4 +- openstack/tests/unit/image/v2/test_image.py | 2 +- openstack/tests/unit/test_utils.py | 100 +++++++------ openstack/utils.py | 64 +-------- 35 files changed, 407 insertions(+), 411 deletions(-) delete mode 100644 doc/source/user/logging.rst diff --git a/README.rst b/README.rst index 5989a8aae..7079da180 100644 --- a/README.rst +++ b/README.rst @@ -6,45 +6,95 @@ with OpenStack clouds. The project aims to provide a consistent and complete set of interactions with OpenStack's many services, along with complete documentation, examples, and tools. -It also contains a simple interface layer. Clouds can do many things, but +It also contains an abstraction interface layer. Clouds can do many things, but there are probably only about 10 of them that most people care about with any regularity. If you want to do complicated things, the per-service oriented -portions of the SDK are for you. However, if what you want is to be able to +portions of the SDK are for you. However, if what you want to be able to write an application that talks to clouds no matter what crazy choices the deployer has made in an attempt to be more hipster than their self-entitled -narcissist peers, then the ``openstack.cloud`` layer is for you. +narcissist peers, then the Cloud Abstraction layer is for you. A Brief History --------------- +.. TODO(shade) This history section should move to the docs. We can put a + link to the published URL here in the README, but it's too long. + openstacksdk started its life as three different libraries: shade, os-client-config and python-openstacksdk. -``shade`` started its life as some code inside of OpenStack Infra's nodepool -project, and as some code inside of Ansible. Ansible had a bunch of different -OpenStack related modules, and there was a ton of duplicated code. Eventually, -between refactoring that duplication into an internal library, and adding logic -and features that the OpenStack Infra team had developed to run client -applications at scale, it turned out that we'd written nine-tenths of what we'd -need to have a standalone library. +``shade`` started its life as some code inside of OpenStack Infra's `nodepool`_ +project, and as some code inside of the `Ansible OpenStack Modules`_. +Ansible had a bunch of different OpenStack related modules, and there was a +ton of duplicated code. Eventually, between refactoring that duplication into +an internal library, and adding the logic and features that the OpenStack Infra +team had developed to run client applications at scale, it turned out that we'd +written nine-tenths of what we'd need to have a standalone library. + +Because of its background from nodepool, shade contained abstractions to +work around deployment differences and is resource oriented rather than service +oriented. This allows a user to think about Security Groups without having to +know whether Security Groups are provided by Nova or Neutron on a given cloud. +On the other hand, as an interface that provides an abstraction, it deviates +from the published OpenStack REST API and adds its own opinions, which may not +get in the way of more advanced users with specific needs. ``os-client-config`` was a library for collecting client configuration for -using an OpenStack cloud in a consistent and comprehensive manner. -In parallel, the python-openstacksdk team was working on a library to expose -the OpenStack APIs to developers in a consistent and predictable manner. After -a while it became clear that there was value in both a high-level layer that -contains business logic, a lower-level SDK that exposes services and their -resources as Python objects, and also to be able to make direct REST calls -when needed with a properly configured Session or Adapter from python-requests. -This led to the merger of the three projects. - -The contents of the shade library have been moved into ``openstack.cloud`` -and os-client-config has been moved in to ``openstack.config``. The next -release of shade will be a thin compatibility layer that subclasses the objects -from ``openstack.cloud`` and provides different argument defaults where needed -for compat. Similarly the next release of os-client-config will be a compat +using an OpenStack cloud in a consistent and comprehensive manner, which +introduced the ``clouds.yaml`` file for expressing named cloud configurations. + +``python-openstacksdk`` was a library that exposed the OpenStack APIs to +developers in a consistent and predictable manner. + +After a while it became clear that there was value in both the high-level +layer that contains additional business logic and the lower-level SDK that +exposes services and their resources faithfully and consistently as Python +objects. + +Even with both of those layers, it is still beneficial at times to be able to +make direct REST calls and to do so with the same properly configured +`Session`_ from `python-requests`_. + +This led to the merge of the three projects. + +The original contents of the shade library have been moved into +``openstack.cloud`` and os-client-config has been moved in to +``openstack.config``. The next release of shade will be a thin compatibility +layer that subclasses the objects from ``openstack.cloud`` and provides +different argument defaults where needed for compat. +Similarly the next release of os-client-config will be a compat layer shim around ``openstack.config``. +.. note:: + + The ``openstack.cloud.OpenStackCloud`` object and the + ``openstack.connection.Connection`` object are going to be merged. It is + recommended to not write any new code which consumes objects from the + ``openstack.cloud`` namespace until that merge is complete. + +.. _nodepool: https://docs.openstack.org/infra/nodepool/ +.. _Ansible OpenStack Modules: http://docs.ansible.com/ansible/latest/list_of_cloud_modules.html#openstack +.. _Session: http://docs.python-requests.org/en/master/user/advanced/#session-objects +.. _python-requests: http://docs.python-requests.org/en/master/ + +openstack +========= + +List servers using objects configured with the ``clouds.yaml`` file: + +.. code-block:: python + + import openstack + + # Initialize and turn on debug logging + openstack.enable_logging(debug=True) + + # Initialize cloud + conn = openstack.connect(cloud='mordred') + + for server in conn.compute.servers(): + print(server.to_dict()) + openstack.config ================ @@ -88,10 +138,10 @@ Create a server using objects configured with the ``clouds.yaml`` file: .. code-block:: python - import openstack.cloud + import openstack # Initialize and turn on debug logging - openstack.cloud.simple_logging(debug=True) + openstack.enable_logging(debug=True) # Initialize cloud # Cloud configs are read with openstack.config diff --git a/SHADE-MERGE-TODO.rst b/SHADE-MERGE-TODO.rst index 6155dc628..32baa0925 100644 --- a/SHADE-MERGE-TODO.rst +++ b/SHADE-MERGE-TODO.rst @@ -72,7 +72,6 @@ shade integration * Investigate auto-generating the bulk of shade's API based on introspection of SDK objects, leaving only the code with extra special logic in the shade layer. -* Rationalize openstack.util.enable_logging and shade.simple_logging. Service Proxies --------------- diff --git a/doc/source/user/guides/logging.rst b/doc/source/user/guides/logging.rst index 00014e6b7..a1bc944cd 100644 --- a/doc/source/user/guides/logging.rst +++ b/doc/source/user/guides/logging.rst @@ -1,80 +1,113 @@ +======= Logging ======= -Logging can save you time and effort when developing your code or looking -for help. If your code is not behaving how you expect it to, enabling and -configuring logging can quickly give you valuable insight into the root -cause of the issue. If you need help from the OpenStack community, the -logs can help the people there assist you. +.. note:: TODO(shade) This document is written from a shade POV. It needs to + be combined with the existing logging guide, but also the logging + systems need to be rationalized. + +`openstacksdk` uses `Python Logging`_. As `openstacksdk` is a library, it does +not configure logging handlers automatically, expecting instead for that to be +the purview of the consuming application. + +Simple Usage +------------ + +For consumers who just want to get a basic logging setup without thinking +about it too deeply, there is a helper method. If used, it should be called +before any other openstacksdk functionality. + +.. autofunction:: openstack.enable_logging + +.. code-block:: python -.. note:: By default, no logging is done. + import openstack + openstack.enable_logging() -Enable SDK Logging ------------------- +The ``stream`` parameter controls the stream where log message are written to. +It defaults to `sys.stdout` which will result in log messages being written +to STDOUT. It can be set to another output stream, or to ``None`` to disable +logging to the console. + +The ``path`` parameter sets up logging to log to a file. By default, if +``path`` is given and ``stream`` is not, logging will only go to ``path``. -To enable logging you use :func:`~openstack.utils.enable_logging`. +You can combine the ``path`` and ``stream`` parameters to log to both places +simultaneously. -The ``debug`` parameter controls the logging level. Set ``debug=True`` to -log debug and higher messages. Set ``debug=False`` to log warning and higher -messages. +To log messages to a file called ``openstack.log`` and the console on +``stdout``: -To log debug and higher messages:: +.. code-block:: python import sys from openstack import utils - utils.enable_logging(debug=True, stream=sys.stdout) + utils.enable_logging(debug=True, path='openstack.log', stream=sys.stdout) -The ``path`` parameter controls the location of a log file. If set, this -parameter will send log messages to a file using a -:py:class:`~logging.FileHandler`. -To log messages to a file called ``openstack.log``:: +`openstack.enable_logging` also sets up a few other loggers and +squelches some warnings or log messages that are otherwise uninteresting or +unactionable by an openstacksdk user. - from openstack import utils +Advanced Usage +-------------- - utils.enable_logging(debug=True, path='openstack.log') +`openstacksdk` logs to a set of different named loggers. -The ``stream`` parameter controls the stream where log message are written to. -If set to ``sys.stdout`` or ``sys.stderr``, this parameter will send log -messages to that stream using a :py:class:`~logging.StreamHandler` +Most of the logging is set up to log to the root ``openstack`` logger. +There are additional sub-loggers that are used at times, primarily so that a +user can decide to turn on or off a specific type of logging. They are listed +below. -To log messages to the console on ``stdout``:: +openstack.config + Issues pertaining to configuration are logged to the ``openstack.config`` + logger. - import sys - from openstack import utils +openstack.task_manager + `openstacksdk` uses a Task Manager to perform remote calls. The + ``openstack.task_manager`` logger emits messages at the start and end + of each Task announcing what it is going to run and then what it ran and + how long it took. Logging ``openstack.task_manager`` is a good way to + get a trace of external actions `openstacksdk` is taking without full + `HTTP Tracing`_. - utils.enable_logging(debug=True, stream=sys.stdout) +openstack.iterate_timeout + When `openstacksdk` needs to poll a resource, it does so in a loop that waits + between iterations and ultimately times out. The + ``openstack.iterate_timeout`` logger emits messages for each iteration + indicating it is waiting and for how long. These can be useful to see for + long running tasks so that one can know things are not stuck, but can also + be noisy. -You can combine the ``path`` and ``stream`` parameters to log to both places -simultaneously. +openstack.fnmatch + `openstacksdk` will try to use `fnmatch`_ on given `name_or_id` arguments. + It's a best effort attempt, so pattern misses are logged to + ``openstack.fnmatch``. A user may not be intending to use an fnmatch + pattern - such as if they are trying to find an image named + ``Fedora 24 [official]``, so these messages are logged separately. -To log messages to a file called ``openstack.log`` and the console on -``stdout``:: +.. _fnmatch: https://pymotw.com/2/fnmatch/ - import sys - from openstack import utils +HTTP Tracing +------------ - utils.enable_logging(debug=True, path='openstack.log', stream=sys.stdout) +HTTP Interactions are handled by `keystoneauth`_. If you want to enable HTTP +tracing while using openstacksdk and are not using `openstack.enable_logging`, +set the log level of the ``keystoneauth`` logger to ``DEBUG``. +For more information see https://docs.openstack.org/keystoneauth/latest/using-sessions.html#logging -Enable requests Logging ------------------------ +.. _keystoneauth: https://docs.openstack.org/keystoneauth/latest/ -The SDK depends on a small number other libraries. Notably, it uses -`requests `_ for its transport layer. -To get even more information about the request/response cycle, you enable -logging of requests the same as you would any other library. +Python Logging +-------------- -To log messages to the console on ``stdout``:: +Python logging is a standard feature of Python and is documented fully in the +Python Documentation, which varies by version of Python. - import logging - import sys +For more information on Python Logging for Python v2, see +https://docs.python.org/2/library/logging.html. - logger = logging.getLogger('requests') - formatter = logging.Formatter( - '%(asctime)s %(levelname)s: %(name)s %(message)s') - console = logging.StreamHandler(sys.stdout) - console.setFormatter(formatter) - logger.setLevel(logging.DEBUG) - logger.addHandler(console) +For more information on Python Logging for Python v3, see +https://docs.python.org/3/library/logging.html. diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index d28c2abb6..778148176 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -22,8 +22,6 @@ These guides walk you through how to make use of the libraries we provide to work with each OpenStack service. If you're looking for a cookbook approach, this is where you'll want to begin. -.. TODO(shade) Merge guides/logging and logging - .. toctree:: :maxdepth: 1 @@ -32,7 +30,6 @@ approach, this is where you'll want to begin. Connect to an OpenStack Cloud Using a Config File Using Cloud Abstration Layer Logging - Shade Logging Microversions Baremetal Block Storage diff --git a/doc/source/user/logging.rst b/doc/source/user/logging.rst deleted file mode 100644 index e3239d3ff..000000000 --- a/doc/source/user/logging.rst +++ /dev/null @@ -1,99 +0,0 @@ -======= -Logging -======= - -.. note:: TODO(shade) This document is written from a shade POV. It needs to - be combined with the existing logging guide, but also the logging - systems need to be rationalized. - -`openstacksdk` uses `Python Logging`_. As `openstacksdk` is a library, it does -not configure logging handlers automatically, expecting instead for that to be -the purview of the consuming application. - -Simple Usage ------------- - -For consumers who just want to get a basic logging setup without thinking -about it too deeply, there is a helper method. If used, it should be called -before any other `shade` functionality. - -.. code-block:: python - - import openstack.cloud - openstack.cloud.simple_logging() - -`openstack.cloud.simple_logging` takes two optional boolean arguments: - -debug - Turns on debug logging. - -http_debug - Turns on debug logging as well as debug logging of the underlying HTTP calls. - -`openstack.cloud.simple_logging` also sets up a few other loggers and -squelches some warnings or log messages that are otherwise uninteresting or -unactionable by a `openstack.cloud` user. - -Advanced Usage --------------- - -`openstack.cloud` logs to a set of different named loggers. - -Most of the logging is set up to log to the root `openstack.cloud` logger. -There are additional sub-loggers that are used at times, primarily so that a -user can decide to turn on or off a specific type of logging. They are listed -below. - -openstack.task_manager - `openstack.cloud` uses a Task Manager to perform remote calls. The - `openstack.cloud.task_manager` logger emits messages at the start and end - of each Task announcing what it is going to run and then what it ran and - how long it took. Logging `openstack.cloud.task_manager` is a good way to - get a trace of external actions `openstack.cloud` is taking without full - `HTTP Tracing`_. - -openstack.cloud.exc - If `log_inner_exceptions` is set to True, `shade` will emit any wrapped - exception to the `openstack.cloud.exc` logger. Wrapped exceptions are usually - considered implementation details, but can be useful for debugging problems. - -openstack.iterate_timeout - When `shade` needs to poll a resource, it does so in a loop that waits - between iterations and ultimately timesout. The - `openstack.iterate_timeout` logger emits messages for each iteration - indicating it is waiting and for how long. These can be useful to see for - long running tasks so that one can know things are not stuck, but can also - be noisy. - -openstack.cloud.http - `shade` will sometimes log additional information about HTTP interactions - to the `openstack.cloud.http` logger. This can be verbose, as it sometimes - logs entire response bodies. - -openstack.cloud.fnmatch - `shade` will try to use `fnmatch`_ on given `name_or_id` arguments. It's a - best effort attempt, so pattern misses are logged to - `openstack.cloud.fnmatch`. A user may not be intending to use an fnmatch - pattern - such as if they are trying to find an image named - ``Fedora 24 [official]``, so these messages are logged separately. - -.. _fnmatch: https://pymotw.com/2/fnmatch/ - -HTTP Tracing ------------- - -HTTP Interactions are handled by `keystoneauth`. If you want to enable HTTP -tracing while using `shade` and are not using `openstack.cloud.simple_logging`, -set the log level of the `keystoneauth` logger to `DEBUG`. - -Python Logging --------------- - -Python logging is a standard feature of Python and is documented fully in the -Python Documentation, which varies by version of Python. - -For more information on Python Logging for Python v2, see -https://docs.python.org/2/library/logging.html. - -For more information on Python Logging for Python v3, see -https://docs.python.org/3/library/logging.html. diff --git a/doc/source/user/multi-cloud-demo.rst b/doc/source/user/multi-cloud-demo.rst index 6d6914231..aafd3a164 100644 --- a/doc/source/user/multi-cloud-demo.rst +++ b/doc/source/user/multi-cloud-demo.rst @@ -62,10 +62,10 @@ Complete Example .. code:: python - import openstack.cloud + import openstack # Initialize and turn on debug logging - openstack.cloud.simple_logging(debug=True) + openstack.enable_logging(debug=True) for cloud_name, region_name in [ ('my-vexxhost', 'ca-ymq-1'), @@ -314,10 +314,10 @@ Complete Example Again .. code:: python - import openstack.cloud + import openstack # Initialize and turn on debug logging - openstack.cloud.simple_logging(debug=True) + openstack.enable_logging(debug=True) for cloud_name, region_name in [ ('my-vexxhost', 'ca-ymq-1'), @@ -346,27 +346,25 @@ Import the library .. code:: python - import openstack.cloud + import openstack Logging ======= -* `shade` uses standard python logging -* Special `openstack.cloud.request_ids` logger for API request IDs -* `simple_logging` does easy defaults +* `openstacksdk` uses standard python logging +* ``openstack.enable_logging`` does easy defaults * Squelches some meaningless warnings * `debug` * Logs shade loggers at debug level - * Includes `openstack.cloud.request_ids` debug logging * `http_debug` Implies `debug`, turns on HTTP tracing .. code:: python # Initialize and turn on debug logging - openstack.cloud.simple_logging(debug=True) + openstack.enable_logging(debug=True) Example with Debug Logging ========================== @@ -375,8 +373,8 @@ Example with Debug Logging .. code:: python - import openstack.cloud - openstack.cloud.simple_logging(debug=True) + import openstack + openstack.enable_logging(debug=True) cloud = openstack.openstack_cloud( cloud='my-vexxhost', region_name='ca-ymq-1') @@ -389,8 +387,8 @@ Example with HTTP Debug Logging .. code:: python - import openstack.cloud - openstack.cloud.simple_logging(http_debug=True) + import openstack + openstack.enable_logging(http_debug=True) cloud = openstack.openstack_cloud( cloud='my-vexxhost', region_name='ca-ymq-1') @@ -486,10 +484,10 @@ Image and Flavor by Name or ID .. code:: python - import openstack.cloud + import openstack # Initialize and turn on debug logging - openstack.cloud.simple_logging(debug=True) + openstack.enable_logging(debug=True) for cloud_name, region_name, image, flavor in [ ('my-vexxhost', 'ca-ymq-1', @@ -533,10 +531,10 @@ Image and Flavor by Dict .. code:: python - import openstack.cloud + import openstack # Initialize and turn on debug logging - openstack.cloud.simple_logging(debug=True) + openstack.enable_logging(debug=True) for cloud_name, region_name, image, flavor_id in [ ('my-vexxhost', 'ca-ymq-1', 'Ubuntu 16.04.1 LTS [2017-03-03]', @@ -564,8 +562,8 @@ Munch Objects .. code:: python - import openstack.cloud - openstack.cloud.simple_logging(debug=True) + import openstack + openstack.enable_logging(debug=True) cloud = openstack.openstack_cloud(cloud='zetta', region_name='no-osl1') image = cloud.get_image('Ubuntu 14.04 (AMD64) [Local Storage]') @@ -596,10 +594,10 @@ Cleanup Script .. code:: python - import openstack.cloud + import openstack # Initialize and turn on debug logging - openstack.cloud.simple_logging(debug=True) + openstack.enable_logging(debug=True) for cloud_name, region_name in [ ('my-vexxhost', 'ca-ymq-1'), @@ -618,8 +616,8 @@ Normalization .. code:: python - import openstack.cloud - openstack.cloud.simple_logging() + import openstack + openstack.enable_logging() cloud = openstack.openstack_cloud(cloud='fuga', region_name='cystack') image = cloud.get_image( @@ -634,8 +632,8 @@ Strict Normalized Results .. code:: python - import openstack.cloud - openstack.cloud.simple_logging() + import openstack + openstack.enable_logging() cloud = openstack.openstack_cloud( cloud='fuga', region_name='cystack', strict=True) @@ -651,8 +649,8 @@ How Did I Find the Image Name for the Last Example? .. code:: python - import openstack.cloud - openstack.cloud.simple_logging() + import openstack + openstack.enable_logging() cloud = openstack.openstack_cloud(cloud='fuga', region_name='cystack') cloud.pprint([ @@ -672,8 +670,8 @@ Added / Modified Information .. code:: python - import openstack.cloud - openstack.cloud.simple_logging(debug=True) + import openstack + openstack.enable_logging(debug=True) cloud = openstack.openstack_cloud(cloud='my-citycloud', region_name='Buf1') try: @@ -714,8 +712,8 @@ User Agent Info .. code:: python - import openstack.cloud - openstack.cloud.simple_logging(http_debug=True) + import openstack + openstack.enable_logging(http_debug=True) cloud = openstack.openstack_cloud( cloud='datacentred', app_name='AmazingApp', app_version='1.0') @@ -732,8 +730,8 @@ Uploading Large Objects .. code:: python - import openstack.cloud - openstack.cloud.simple_logging(debug=True) + import openstack + openstack.enable_logging(debug=True) cloud = openstack.openstack_cloud(cloud='ovh', region_name='SBG1') cloud.create_object( @@ -753,8 +751,8 @@ Uploading Large Objects .. code:: python - import openstack.cloud - openstack.cloud.simple_logging(debug=True) + import openstack + openstack.enable_logging(debug=True) cloud = openstack.openstack_cloud(cloud='ovh', region_name='SBG1') cloud.create_object( @@ -769,8 +767,8 @@ Service Conditionals .. code:: python - import openstack.cloud - openstack.cloud.simple_logging(debug=True) + import openstack + openstack.enable_logging(debug=True) cloud = openstack.openstack_cloud(cloud='kiss', region_name='region1') print(cloud.has_service('network')) @@ -783,8 +781,8 @@ Service Conditional Overrides .. code:: python - import openstack.cloud - openstack.cloud.simple_logging(debug=True) + import openstack + openstack.enable_logging(debug=True) cloud = openstack.openstack_cloud(cloud='rax', region_name='DFW') print(cloud.has_service('network')) diff --git a/examples/cloud/cleanup-servers.py b/examples/cloud/cleanup-servers.py index 7f9257b58..7620f4c0d 100644 --- a/examples/cloud/cleanup-servers.py +++ b/examples/cloud/cleanup-servers.py @@ -13,7 +13,7 @@ import openstack # Initialize and turn on debug logging -openstack.simple_logging(debug=True) +openstack.enable_logging(debug=True) for cloud_name, region_name in [ ('my-vexxhost', 'ca-ymq-1'), diff --git a/examples/cloud/create-server-dict.py b/examples/cloud/create-server-dict.py index 4557ee188..7b7505010 100644 --- a/examples/cloud/create-server-dict.py +++ b/examples/cloud/create-server-dict.py @@ -13,7 +13,7 @@ import openstack # Initialize and turn on debug logging -openstack.simple_logging(debug=True) +openstack.enable_logging(debug=True) for cloud_name, region_name, image, flavor_id in [ ('my-vexxhost', 'ca-ymq-1', 'Ubuntu 16.04.1 LTS [2017-03-03]', diff --git a/examples/cloud/create-server-name-or-id.py b/examples/cloud/create-server-name-or-id.py index a269f2fe6..170904a9d 100644 --- a/examples/cloud/create-server-name-or-id.py +++ b/examples/cloud/create-server-name-or-id.py @@ -13,7 +13,7 @@ import openstack # Initialize and turn on debug logging -openstack.simple_logging(debug=True) +openstack.enable_logging(debug=True) for cloud_name, region_name, image, flavor in [ ('my-vexxhost', 'ca-ymq-1', diff --git a/examples/cloud/debug-logging.py b/examples/cloud/debug-logging.py index a9cc31c2b..0874e7b6c 100644 --- a/examples/cloud/debug-logging.py +++ b/examples/cloud/debug-logging.py @@ -11,7 +11,7 @@ # under the License. import openstack -openstack.simple_logging(debug=True) +openstack.enable_logging(debug=True) cloud = openstack.openstack_cloud( cloud='my-vexxhost', region_name='ca-ymq-1') diff --git a/examples/cloud/find-an-image.py b/examples/cloud/find-an-image.py index 089b08640..9be76817e 100644 --- a/examples/cloud/find-an-image.py +++ b/examples/cloud/find-an-image.py @@ -11,7 +11,7 @@ # under the License. import openstack -openstack.simple_logging() +openstack.enable_logging() cloud = openstack.openstack_cloud(cloud='fuga', region_name='cystack') cloud.pprint([ diff --git a/examples/cloud/http-debug-logging.py b/examples/cloud/http-debug-logging.py index e5b1a7e9d..62b942532 100644 --- a/examples/cloud/http-debug-logging.py +++ b/examples/cloud/http-debug-logging.py @@ -11,7 +11,7 @@ # under the License. import openstack -openstack.simple_logging(http_debug=True) +openstack.enable_logging(http_debug=True) cloud = openstack.openstack_cloud( cloud='my-vexxhost', region_name='ca-ymq-1') diff --git a/examples/cloud/munch-dict-object.py b/examples/cloud/munch-dict-object.py index df3a0e76c..5ba6e63ea 100644 --- a/examples/cloud/munch-dict-object.py +++ b/examples/cloud/munch-dict-object.py @@ -11,7 +11,7 @@ # under the License. import openstack -openstack.simple_logging(debug=True) +openstack.enable_logging(debug=True) cloud = openstack.openstack_cloud(cloud='ovh', region_name='SBG1') image = cloud.get_image('Ubuntu 16.10') diff --git a/examples/cloud/normalization.py b/examples/cloud/normalization.py index 80f1ff4c9..40a3748d3 100644 --- a/examples/cloud/normalization.py +++ b/examples/cloud/normalization.py @@ -11,7 +11,7 @@ # under the License. import openstack -openstack.simple_logging() +openstack.enable_logging() cloud = openstack.openstack_cloud(cloud='fuga', region_name='cystack') image = cloud.get_image( diff --git a/examples/cloud/server-information.py b/examples/cloud/server-information.py index 8ae710631..b851e96ad 100644 --- a/examples/cloud/server-information.py +++ b/examples/cloud/server-information.py @@ -11,7 +11,7 @@ # under the License. import openstack -openstack.simple_logging(debug=True) +openstack.enable_logging(debug=True) cloud = openstack.openstack_cloud(cloud='my-citycloud', region_name='Buf1') try: diff --git a/examples/cloud/service-conditional-overrides.py b/examples/cloud/service-conditional-overrides.py index 7991a1ec3..d3a2a88de 100644 --- a/examples/cloud/service-conditional-overrides.py +++ b/examples/cloud/service-conditional-overrides.py @@ -11,7 +11,7 @@ # under the License. import openstack -openstack.simple_logging(debug=True) +openstack.enable_logging(debug=True) cloud = openstack.openstack_cloud(cloud='rax', region_name='DFW') print(cloud.has_service('network')) diff --git a/examples/cloud/service-conditionals.py b/examples/cloud/service-conditionals.py index 5e81d1dad..46b3d2e91 100644 --- a/examples/cloud/service-conditionals.py +++ b/examples/cloud/service-conditionals.py @@ -11,7 +11,7 @@ # under the License. import openstack -openstack.simple_logging(debug=True) +openstack.enable_logging(debug=True) cloud = openstack.openstack_cloud(cloud='kiss', region_name='region1') print(cloud.has_service('network')) diff --git a/examples/cloud/strict-mode.py b/examples/cloud/strict-mode.py index 74cfd05ff..96eaa13b9 100644 --- a/examples/cloud/strict-mode.py +++ b/examples/cloud/strict-mode.py @@ -11,7 +11,7 @@ # under the License. import openstack -openstack.simple_logging() +openstack.enable_logging() cloud = openstack.openstack_cloud( cloud='fuga', region_name='cystack', strict=True) diff --git a/examples/cloud/upload-large-object.py b/examples/cloud/upload-large-object.py index f274c2e7e..d89ae557d 100644 --- a/examples/cloud/upload-large-object.py +++ b/examples/cloud/upload-large-object.py @@ -11,7 +11,7 @@ # under the License. import openstack -openstack.simple_logging(debug=True) +openstack.enable_logging(debug=True) cloud = openstack.openstack_cloud(cloud='ovh', region_name='SBG1') cloud.create_object( diff --git a/examples/cloud/upload-object.py b/examples/cloud/upload-object.py index f274c2e7e..d89ae557d 100644 --- a/examples/cloud/upload-object.py +++ b/examples/cloud/upload-object.py @@ -11,7 +11,7 @@ # under the License. import openstack -openstack.simple_logging(debug=True) +openstack.enable_logging(debug=True) cloud = openstack.openstack_cloud(cloud='ovh', region_name='SBG1') cloud.create_object( diff --git a/examples/cloud/user-agent.py b/examples/cloud/user-agent.py index 309355b5e..578c3c2e5 100644 --- a/examples/cloud/user-agent.py +++ b/examples/cloud/user-agent.py @@ -11,7 +11,7 @@ # under the License. import openstack -openstack.simple_logging(http_debug=True) +openstack.enable_logging(http_debug=True) cloud = openstack.openstack_cloud( cloud='datacentred', app_name='AmazingApp', app_version='1.0') diff --git a/openstack/__init__.py b/openstack/__init__.py index 9d67f9083..3fd98a2a5 100644 --- a/openstack/__init__.py +++ b/openstack/__init__.py @@ -12,15 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. -import logging +__all__ = [ + '__version__', + 'connect', + 'enable_logging', +] + import warnings import keystoneauth1.exceptions import pbr.version import requestsexceptions -from openstack import _log +from openstack._log import enable_logging # noqa from openstack.cloud.exc import * # noqa +# TODO(shade) These two want to be removed before we make a release from openstack.cloud.openstackcloud import OpenStackCloud from openstack.cloud.operatorcloud import OperatorCloud import openstack.connection @@ -34,43 +40,11 @@ def _get_openstack_config(app_name=None, app_version=None): import openstack.config - # Protect against older versions of os-client-config that don't expose this - try: - return openstack.config.OpenStackConfig( - app_name=app_name, app_version=app_version) - except Exception: - return openstack.config.OpenStackConfig() - - -def simple_logging(debug=False, http_debug=False): - if http_debug: - debug = True - if debug: - log_level = logging.DEBUG - else: - log_level = logging.INFO - if http_debug: - # Enable HTTP level tracing - log = _log.setup_logging('keystoneauth') - log.addHandler(logging.StreamHandler()) - log.setLevel(log_level) - # We only want extra shade HTTP tracing in http debug mode - log = _log.setup_logging('openstack.cloud.http') - log.setLevel(log_level) - else: - # We only want extra shade HTTP tracing in http debug mode - log = _log.setup_logging('openstack.cloud.http') - log.setLevel(logging.WARNING) - log = _log.setup_logging('openstack.cloud') - log.addHandler(logging.StreamHandler()) - log.setLevel(log_level) - # Suppress warning about keystoneauth loggers - log = _log.setup_logging('keystoneauth.identity.base') - log = _log.setup_logging('keystoneauth.identity.generic.base') - - -# TODO(shade) Document this and add some examples -# TODO(shade) This wants to be renamed before we make a release. + return openstack.config.OpenStackConfig( + app_name=app_name, app_version=app_version) + + +# TODO(shade) This wants to be remove before we make a release. def openstack_clouds( config=None, debug=False, cloud=None, strict=False, app_name=None, app_version=None): @@ -99,10 +73,7 @@ def openstack_clouds( "Invalid cloud configuration: {exc}".format(exc=str(e))) -# TODO(shade) This wants to be renamed before we make a release - there is -# ultimately no reason to have an openstack_cloud and a connect -# factory function - but we have a few steps to go first and this is used -# in the imported tests from shade. +# TODO(shade) This wants to be removed before we make a release. def openstack_cloud( config=None, strict=False, app_name=None, app_version=None, **kwargs): if not config: @@ -115,10 +86,7 @@ def openstack_cloud( return OpenStackCloud(cloud_config=cloud_region, strict=strict) -# TODO(shade) This wants to be renamed before we make a release - there is -# ultimately no reason to have an operator_cloud and a connect -# factory function - but we have a few steps to go first and this is used -# in the imported tests from shade. +# TODO(shade) This wants to be removed before we make a release. def operator_cloud( config=None, strict=False, app_name=None, app_version=None, **kwargs): if not config: @@ -134,3 +102,16 @@ def operator_cloud( def connect(*args, **kwargs): """Create a `openstack.connection.Connection`.""" return openstack.connection.Connection(*args, **kwargs) + + +def connect_all(config=None, app_name=None, app_version=None): + if not config: + config = _get_openstack_config(app_name, app_version) + try: + return [ + openstack.connection.Connection(config=cloud_region) + for cloud_region in config.get_all() + ] + except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin as e: + raise OpenStackCloudException( + "Invalid cloud configuration: {exc}".format(exc=str(e))) diff --git a/openstack/_log.py b/openstack/_log.py index ff2f2eac7..4637dfc8b 100644 --- a/openstack/_log.py +++ b/openstack/_log.py @@ -13,16 +13,102 @@ # limitations under the License. import logging +import sys -class NullHandler(logging.Handler): - def emit(self, record): - pass +def setup_logging(name, handlers=None, level=None): + """Set up logging for a named logger. + Gets and initializes a named logger, ensuring it at least has a + `logging.NullHandler` attached. -def setup_logging(name): + :param str name: + Name of the logger. + :param list handlers: + A list of `logging.Handler` objects to attach to the logger. + :param int level: + Log level to set the logger at. + + :returns: A `logging.Logger` object that can be used to emit log messages. + """ + handlers = handlers or [] log = logging.getLogger(name) - if len(log.handlers) == 0: - h = NullHandler() + if len(log.handlers) == 0 and not handlers: + h = logging.NullHandler() + log.addHandler(h) + for h in handlers: log.addHandler(h) + if level: + log.setLevel(level) return log + + +def enable_logging( + debug=False, http_debug=False, path=None, stream=None, + format_stream=False, + format_template='%(asctime)s %(levelname)s: %(name)s %(message)s'): + """Enable logging output. + + Helper function to enable logging. This function is available for + debugging purposes and for folks doing simple applications who want an + easy 'just make it work for me'. For more complex applications or for + those who want more flexibility, the standard library ``logging`` package + will receive these messages in any handlers you create. + + :param bool debug: + Set this to ``True`` to receive debug messages. + :param bool http_debug: + Set this to ``True`` to receive debug messages including + HTTP requests and responses. This implies ``debug=True``. + :param str path: + If a *path* is specified, logging output will written to that file + in addition to sys.stderr. + The path is passed to logging.FileHandler, which will append messages + the file (and create it if needed). + :param stream: + One of ``None `` or ``sys.stdout`` or ``sys.stderr``. + If it is ``None``, nothing is logged to a stream. + If it isn't ``None``, console output is logged to this stream. + :param bool format_stream: + If format_stream is False, the default, apply ``format_template`` to + ``path`` but not to ``stream`` outputs. If True, apply + ``format_template`` to ``stream`` outputs as well. + :param str format_template: + Template to pass to :class:`logging.Formatter`. + + :rtype: None + """ + if not stream and not path: + stream = sys.stdout + + if http_debug: + debug = True + if debug: + level = logging.DEBUG + else: + level = logging.INFO + + formatter = logging.Formatter(format_template) + + handlers = [] + + if stream is not None: + console = logging.StreamHandler(stream) + if format_stream: + console.setFormatter(formatter) + handlers.append(console) + + if path is not None: + file_handler = logging.FileHandler(path) + file_handler.setFormatter(formatter) + handlers.append(file_handler) + + if http_debug: + # Enable HTTP level tracing + setup_logging('keystoneauth', handlers=handlers, level=level) + + setup_logging('openstack', handlers=handlers, level=level) + # Suppress warning about keystoneauth loggers + setup_logging('keystoneauth.discovery') + setup_logging('keystoneauth.identity.base') + setup_logging('keystoneauth.identity.generic.base') diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index b5eb032b4..270dd95dd 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -94,7 +94,7 @@ def _filter_list(data, name_or_id, filters): # The logger is openstack.cloud.fmmatch to allow a user/operator to # configure logging not to communicate about fnmatch misses # (they shouldn't be too spammy, but one never knows) - log = _log.setup_logging('openstack.cloud.fnmatch') + log = _log.setup_logging('openstack.fnmatch') if name_or_id: # name_or_id might already be unicode name_or_id = _make_unicode(name_or_id) diff --git a/openstack/cloud/meta.py b/openstack/cloud/meta.py index 35bc242d2..3c57a1a04 100644 --- a/openstack/cloud/meta.py +++ b/openstack/cloud/meta.py @@ -232,7 +232,7 @@ def find_best_address(addresses, family, public=False, cloud_public=True): pass # Give up and return the first - none work as far as we can tell if do_check: - log = _log.setup_logging('shade') + log = _log.setup_logging('openstack') log.debug( 'The cloud returned multiple addresses, and none of them seem' ' to work. That might be what you wanted, but we have no clue' @@ -381,7 +381,7 @@ def _get_supplemental_addresses(cloud, server): # This SHOULD return one and only one FIP - but doing # it as a search/list lets the logic work regardless if fip['fixed_ip_address'] not in fixed_ip_mapping: - log = _log.setup_logging('shade') + log = _log.setup_logging('openstack') log.debug( "The cloud returned floating ip %(fip)s attached" " to server %(server)s but the fixed ip associated" diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 9a3103f66..4cb9380d7 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -152,7 +152,7 @@ def __init__( if log_inner_exceptions: OpenStackCloudException.log_inner_exceptions = True - self.log = _log.setup_logging('openstack.cloud') + self.log = _log.setup_logging('openstack') if not cloud_config: config = openstack.config.OpenStackConfig( diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index ffef7f60e..7c2541caf 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -69,7 +69,7 @@ def __init__(self, name, region_name=None, config=None, self.name = name self.region_name = region_name self.config = config - self.log = _log.setup_logging(__name__) + self.log = _log.setup_logging('openstack.config') self._force_ipv4 = force_ipv4 self._auth = auth_plugin self._openstack_config = openstack_config diff --git a/openstack/config/loader.py b/openstack/config/loader.py index dcc22c14b..f5d853bfa 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -182,7 +182,7 @@ def __init__(self, config_files=None, vendor_files=None, pw_func=None, session_constructor=None, app_name=None, app_version=None, load_yaml_config=True): - self.log = _log.setup_logging(__name__) + self.log = _log.setup_logging('openstack.config') self._session_constructor = session_constructor self._app_name = app_name self._app_version = app_version diff --git a/openstack/connection.py b/openstack/connection.py index f5797b7df..e59d8cc67 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -75,22 +75,20 @@ """ import importlib -import logging -import sys import keystoneauth1.exceptions import os_service_types from six.moves import urllib +from openstack import _log import openstack.config from openstack.config import cloud_region from openstack import exceptions from openstack import proxy from openstack import proxy2 from openstack import task_manager -from openstack import utils -_logger = logging.getLogger(__name__) +_logger = _log.setup_logging('openstack') def from_config(cloud=None, config=None, options=None, **kwargs): @@ -119,9 +117,6 @@ def from_config(cloud=None, config=None, options=None, **kwargs): config = openstack.config.OpenStackConfig().get_one( cloud=cloud, argparse=options) - if config.debug: - utils.enable_logging(True, stream=sys.stdout) - return Connection(config=config) diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index f1b8a8f57..4b397a443 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -11,16 +11,16 @@ # under the License. import hashlib -import logging import jsonpatch +from openstack import _log from openstack import exceptions from openstack.image import image_service from openstack import resource2 from openstack import utils -_logger = logging.getLogger(__name__) +_logger = _log.setup_logging('openstack') class Image(resource2.Resource): diff --git a/openstack/profile.py b/openstack/profile.py index d70c05afa..588fd60c7 100644 --- a/openstack/profile.py +++ b/openstack/profile.py @@ -52,8 +52,8 @@ """ import copy -import logging +from openstack import _log from openstack.baremetal import baremetal_service from openstack.block_storage import block_storage_service from openstack.clustering import clustering_service @@ -72,7 +72,7 @@ from openstack.orchestration import orchestration_service from openstack.workflow import workflow_service -_logger = logging.getLogger(__name__) +_logger = _log.setup_logging('openstack') class Profile(object): diff --git a/openstack/task_manager.py b/openstack/task_manager.py index b015ac85f..8a49ba4b2 100644 --- a/openstack/task_manager.py +++ b/openstack/task_manager.py @@ -22,10 +22,10 @@ import keystoneauth1.exceptions import six +import openstack._log from openstack import exceptions -from openstack import utils -_log = utils.setup_logging(__name__) +_log = openstack._log.setup_logging('openstack.task_manager') class Task(object): diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index cf4a96355..14e2cf1ed 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -272,7 +272,7 @@ def test_download_no_checksum_at_all2(self): self.sess.get.side_effect = [resp1, resp2] - with self.assertLogs(logger=image.__name__, level="WARNING") as log: + with self.assertLogs(logger='openstack', level="WARNING") as log: rv = sot.download(self.sess) self.assertEqual(len(log.records), 1, diff --git a/openstack/tests/unit/test_utils.py b/openstack/tests/unit/test_utils.py index f0ea12d60..1110330ff 100644 --- a/openstack/tests/unit/test_utils.py +++ b/openstack/tests/unit/test_utils.py @@ -10,67 +10,83 @@ # License for the specific language governing permissions and limitations # under the License. +import logging import mock import sys import testtools +import fixtures + from openstack import utils class Test_enable_logging(testtools.TestCase): - def _console_tests(self, fake_logging, level, debug, stream): - the_logger = mock.Mock() - fake_logging.getLogger.return_value = the_logger + def setUp(self): + super(Test_enable_logging, self).setUp() + self.openstack_logger = mock.Mock() + self.openstack_logger.handlers = [] + self.ksa_logger_1 = mock.Mock() + self.ksa_logger_1.handlers = [] + self.ksa_logger_2 = mock.Mock() + self.ksa_logger_2.handlers = [] + self.ksa_logger_3 = mock.Mock() + self.ksa_logger_3.handlers = [] + self.fake_get_logger = mock.Mock() + self.fake_get_logger.side_effect = [ + self.openstack_logger, + self.ksa_logger_1, + self.ksa_logger_2, + self.ksa_logger_3 + ] + self.useFixture( + fixtures.MonkeyPatch('logging.getLogger', self.fake_get_logger)) + + def _console_tests(self, level, debug, stream): utils.enable_logging(debug=debug, stream=stream) - self.assertEqual(the_logger.addHandler.call_count, 2) - the_logger.setLevel.assert_called_with(level) + self.assertEqual(self.openstack_logger.addHandler.call_count, 1) + self.openstack_logger.setLevel.assert_called_with(level) - def _file_tests(self, fake_logging, level, debug): - the_logger = mock.Mock() - fake_logging.getLogger.return_value = the_logger + def _file_tests(self, level, debug): + file_handler = mock.Mock() + self.useFixture( + fixtures.MonkeyPatch('logging.FileHandler', file_handler)) fake_path = "fake/path.log" utils.enable_logging(debug=debug, path=fake_path) - fake_logging.FileHandler.assert_called_with(fake_path) - self.assertEqual(the_logger.addHandler.call_count, 2) - the_logger.setLevel.assert_called_with(level) + file_handler.assert_called_with(fake_path) + self.assertEqual(self.openstack_logger.addHandler.call_count, 1) + self.openstack_logger.setLevel.assert_called_with(level) def test_none(self): - self.assertRaises( - ValueError, utils.enable_logging, - debug=True, path=None, stream=None) - - @mock.patch("openstack.utils.logging") - def test_debug_console_stderr(self, fake_logging): - self._console_tests(fake_logging, - fake_logging.DEBUG, True, sys.stderr) - - @mock.patch("openstack.utils.logging") - def test_warning_console_stderr(self, fake_logging): - self._console_tests(fake_logging, - fake_logging.WARNING, False, sys.stderr) - - @mock.patch("openstack.utils.logging") - def test_debug_console_stdout(self, fake_logging): - self._console_tests(fake_logging, - fake_logging.DEBUG, True, sys.stdout) - - @mock.patch("openstack.utils.logging") - def test_warning_console_stdout(self, fake_logging): - self._console_tests(fake_logging, - fake_logging.WARNING, False, sys.stdout) - - @mock.patch("openstack.utils.logging") - def test_debug_file(self, fake_logging): - self._file_tests(fake_logging, fake_logging.DEBUG, True) - - @mock.patch("openstack.utils.logging") - def test_warning_file(self, fake_logging): - self._file_tests(fake_logging, fake_logging.WARNING, False) + utils.enable_logging(debug=True) + self.fake_get_logger.assert_has_calls([]) + self.openstack_logger.setLevel.assert_called_with(logging.DEBUG) + self.assertEqual(self.openstack_logger.addHandler.call_count, 1) + self.assertIsInstance( + self.openstack_logger.addHandler.call_args_list[0][0][0], + logging.StreamHandler) + + def test_debug_console_stderr(self): + self._console_tests(logging.DEBUG, True, sys.stderr) + + def test_warning_console_stderr(self): + self._console_tests(logging.INFO, False, sys.stderr) + + def test_debug_console_stdout(self): + self._console_tests(logging.DEBUG, True, sys.stdout) + + def test_warning_console_stdout(self): + self._console_tests(logging.INFO, False, sys.stdout) + + def test_debug_file(self): + self._file_tests(logging.DEBUG, True) + + def test_warning_file(self): + self._file_tests(logging.INFO, False) class Test_urljoin(testtools.TestCase): diff --git a/openstack/utils.py b/openstack/utils.py index 606705531..462bb8cc4 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -11,12 +11,13 @@ # under the License. import functools -import logging import time import deprecation from openstack import _log +# SDK has had enable_logging in utils. Import the symbol here to not break them +from openstack._log import enable_logging # noqa from openstack import exceptions from openstack import version @@ -47,67 +48,6 @@ def deprecated(deprecated_in=None, removed_in=None, details=details) -class NullHandler(logging.Handler): - def emit(self, record): - pass - - -def setup_logging(name): - '''Get a logging.Logger and make sure there is at least a NullHandler.''' - log = logging.getLogger(name) - if len(log.handlers) == 0: - h = NullHandler() - log.addHandler(h) - return log - - -def enable_logging(debug=False, path=None, stream=None): - """Enable logging to a file at path and/or a console stream. - - This function is available for debugging purposes. If you wish to - log this package's message in your application, the standard library - ``logging`` package will receive these messages in any handlers you - create. - - :param bool debug: Set this to ``True`` to receive debug messages, - which includes HTTP requests and responses, - or ``False`` for warning messages. - :param str path: If a *path* is specified, logging output will - written to that file in addition to sys.stderr. - The path is passed to logging.FileHandler, - which will append messages the file (and create - it if needed). - :param stream: One of ``None `` or ``sys.stdout`` or ``sys.stderr``. - If it is ``None``, nothing is logged to a stream. - If it isn't ``None``, console output is logged - to this stream. - - :rtype: None - """ - if path is None and stream is None: - raise ValueError("path and/or stream must be set") - - logger = logging.getLogger('openstack') - ksalog = logging.getLogger('keystoneauth') - formatter = logging.Formatter( - '%(asctime)s %(levelname)s: %(name)s %(message)s') - - if stream is not None: - console = logging.StreamHandler(stream) - console.setFormatter(formatter) - logger.addHandler(console) - ksalog.addHandler(console) - - if path is not None: - file_handler = logging.FileHandler(path) - file_handler.setFormatter(formatter) - logger.addHandler(file_handler) - ksalog.addHandler(file_handler) - - logger.setLevel(logging.DEBUG if debug else logging.WARNING) - ksalog.setLevel(logging.DEBUG if debug else logging.WARNING) - - def urljoin(*args): """A custom version of urljoin that simply joins strings into a path. From dffe0f0463b662963e8d13f26e24aa6caa2f6ac3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 12 Jan 2018 09:46:49 -0600 Subject: [PATCH 1903/3836] Add ability to register non-official services openstacksdk is in the business of being an SDK for OpenStack. While it's tempting to support other services that people might want to run alongside their OpenStack cloud and register in their keystone catalog, doing so is just not feasible. At the same time, the 95% case is that the openstacksdk will be used for OpenStack services, so using entrpoints-based plugin loading as part of normal usage incurs a startup cost that can be rather high (it's based on the number of python packages installed on the system, not the number of plugins for openstacksdk) Add a method and a constructor parameter to Connection that allows people to programmatically enable support for additional non-OpenStack services. This introduce a new Service description class that maps service_type, Proxy class and optional service_type aliases. A subclass of Service could be provided by whoever is writing the Proxy class and associated Resoure objects, or the base class can be instantiated with type and proxy_class as arguments. While doing this, rework the loading of the official OpenStack services to use the Service descriptor objects. Add an OpenStackService subclass which does the importlib searching of the openstack module tree for proxy classes. This gets all of the module searching and loading into the openstack.services module and out of Connection. This should allow us to delete the metric service from the tree but provide people who want to use the metric service with openstacksdk a mechanism to do so. It also should provide a vehicle for people developing new not-yet-official services to develop their Resource and Proxy classes out of tree, and then move them in once they are official Change-Id: I6d1e0c45026a2e7b3c42983df9c0565b1c501bc3 --- openstack/connection.py | 134 +++++-------- openstack/service_description.py | 181 ++++++++++++++++++ .../notes/add-service-0bcc16eb026eade3.yaml | 5 + 3 files changed, 234 insertions(+), 86 deletions(-) create mode 100644 openstack/service_description.py create mode 100644 releasenotes/notes/add-service-0bcc16eb026eade3.yaml diff --git a/openstack/connection.py b/openstack/connection.py index e59d8cc67..300972894 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -74,18 +74,16 @@ network = conn.network.create_network({"name": "zuul"}) """ -import importlib - import keystoneauth1.exceptions import os_service_types +import six from six.moves import urllib from openstack import _log import openstack.config from openstack.config import cloud_region from openstack import exceptions -from openstack import proxy -from openstack import proxy2 +from openstack import service_description from openstack import task_manager _logger = _log.setup_logging('openstack') @@ -127,6 +125,7 @@ def __init__(self, cloud=None, config=None, session=None, # TODO(shade) Remove these once we've shifted # python-openstackclient to not use the profile interface. authenticator=None, profile=None, + extra_services=None, **kwargs): """Create a connection to a cloud. @@ -162,12 +161,19 @@ def __init__(self, cloud=None, config=None, session=None, :param profile: DEPRECATED. Only exists for short-term backwards compatibility for python-openstackclient while we transition. + :param extra_services: List of + :class:`~openstack.service_description.ServiceDescription` + objects describing services that openstacksdk otherwise does not + know about. :param kwargs: If a config is not provided, the rest of the parameters provided are assumed to be arguments to be passed to the CloudRegion contructor. """ self.config = config - self.service_type_manager = os_service_types.ServiceTypes() + self._extra_services = {} + if extra_services: + for service in extra_services: + self._extra_services[service.service_type] = service if not self.config: if profile: @@ -193,7 +199,16 @@ def __init__(self, cloud=None, config=None, session=None, self.session = self.config.get_session() - self._open() + service_type_manager = os_service_types.ServiceTypes() + for service in service_type_manager.services: + self.add_service( + service_description.OpenStackServiceDescription( + service, self.config)) + # TODO(mordred) openstacksdk has support for the metric service + # which is not in service-types-authority. What do we do about that? + self.add_service( + service_description.OpenStackServiceDescription( + dict(service_type='metric'), self.config)) def _get_config_from_profile(self, profile, authenticator, **kwargs): """Get openstack.config objects from legacy profile.""" @@ -222,31 +237,31 @@ def _get_config_from_profile(self, profile, authenticator, **kwargs): name=name, region_name=region_name, config=kwargs) config._auth = authenticator - def _open(self): - """Open the connection. """ - for service in self.service_type_manager.services: - self._load(service['service_type']) - # TODO(mordred) openstacksdk has support for the metric service - # which is not in service-types-authority. What do we do about that? - self._load('metric') - - def _load(self, service_type): - service = self._get_service(service_type) - - if service: - module_name = service.get_module() + "._proxy" - module = importlib.import_module(module_name) - proxy_class = getattr(module, "Proxy") - if not (issubclass(proxy_class, proxy.BaseProxy) or - issubclass(proxy_class, proxy2.BaseProxy)): - raise TypeError("%s.Proxy must inherit from BaseProxy" % - proxy_class.__module__) + def add_service(self, service): + """Add a service to the Connection. + + Attaches an instance of the :class:`~openstack.proxy2.BaseProxy` + class contained in + :class:`~openstack.service_description.ServiceDescription`. + The :class:`~openstack.proxy2.BaseProxy` will be attached to the + `Connection` by its ``service_type`` and by any ``aliases`` that + may be specified. + + :param openstack.service_description.ServiceDescription service: + Object describing the service to be attached. As a convenience, + if ``service`` is a string it will be treated as a ``service_type`` + and a basic + :class:`~openstack.service_description.ServiceDescription` + will be created. + """ + # If we don't have a proxy, just instantiate BaseProxy so that + # we get an adapter. + if isinstance(service, six.string_types): + service_type = service_description + service = service_description.ServiceDescription(service_type) else: - # If we don't have a proxy, just instantiate BaseProxy so that - # we get an adapter. - proxy_class = proxy2.BaseProxy - - proxy_object = proxy_class( + service_type = service.service_type + proxy_object = service.proxy_class( session=self.config.get_session(), task_manager=self.task_manager, allow_version_hack=True, @@ -256,63 +271,10 @@ def _load(self, service_type): region_name=self.config.region_name, version=self.config.get_api_version(service_type) ) - all_types = self.service_type_manager.get_all_types(service_type) + # Register the proxy class with every known alias - for attr_name in [name.replace('-', '_') for name in all_types]: - setattr(self, attr_name, proxy_object) - - def _get_all_types(self, service_type): - # We make connection attributes for all official real type names - # and aliases. Three services have names they were called by in - # openstacksdk that are not covered by Service Types Authority aliases. - # Include them here - but take heed, no additional values should ever - # be added to this list. - # that were only used in openstacksdk resource naming. - LOCAL_ALIASES = { - 'baremetal': 'bare_metal', - 'block_storage': 'block_store', - 'clustering': 'cluster', - } - all_types = self.service_type_manager.get_all_types(service_type) - if service_type in LOCAL_ALIASES: - all_types.append(LOCAL_ALIASES[service_type]) - return all_types - - def _get_service(self, official_service_type): - service_class = None - for service_type in self._get_all_types(official_service_type): - service_class = self._find_service_class(service_type) - if service_class: - break - if not service_class: - return None - # TODO(mordred) Replace this with proper discovery - version_string = self.config.get_api_version(official_service_type) - version = None - if version_string: - version = 'v{version}'.format(version=version_string[0]) - return service_class(version=version) - - def _find_service_class(self, service_type): - package_name = 'openstack.{service_type}'.format( - service_type=service_type).replace('-', '_') - module_name = service_type.replace('-', '_') + '_service' - class_name = ''.join( - [part.capitalize() for part in module_name.split('_')]) - try: - import_name = '.'.join([package_name, module_name]) - service_module = importlib.import_module(import_name) - except ImportError: - return None - service_class = getattr(service_module, class_name, None) - if not service_class: - _logger.warn( - 'Unable to find class {class_name} in module for service' - ' for service {service_type}'.format( - class_name=class_name, - service_type=service_type)) - return None - return service_class + for attr_name in service.all_types: + setattr(self, attr_name.replace('-', '_'), proxy_object) def authorize(self): """Authorize this Connection diff --git a/openstack/service_description.py b/openstack/service_description.py new file mode 100644 index 000000000..45a675f7d --- /dev/null +++ b/openstack/service_description.py @@ -0,0 +1,181 @@ +# Copyright 2018 Red Hat, 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. + +__all__ = [ + 'OpenStackServiceDescription', + 'ServiceDescription', +] + +import importlib +import warnings + +import os_service_types + +from openstack import _log +from openstack import proxy +from openstack import proxy2 + +_logger = _log.setup_logging('openstack') +_service_type_manager = os_service_types.ServiceTypes() + + +def _get_all_types(service_type, aliases=None): + # We make connection attributes for all official real type names + # and aliases. Three services have names they were called by in + # openstacksdk that are not covered by Service Types Authority aliases. + # Include them here - but take heed, no additional values should ever + # be added to this list. + # that were only used in openstacksdk resource naming. + LOCAL_ALIASES = { + 'baremetal': 'bare_metal', + 'block_storage': 'block_store', + 'clustering': 'cluster', + } + all_types = set(_service_type_manager.get_all_types(service_type)) + if aliases: + all_types.update(aliases) + if service_type in LOCAL_ALIASES: + all_types.add(LOCAL_ALIASES[service_type]) + return all_types + + +class ServiceDescription(object): + + #: Proxy class for this service + proxy_class = proxy2.BaseProxy + #: main service_type to use to find this service in the catalog + service_type = None + #: list of aliases this service might be registered as + aliases = [] + #: Internal temporary flag to control whether or not a warning is + #: emitted for use of old Proxy class. In-tree things should not + #: emit a warning - but out of tree things should only use Proxy2. + _warn_if_old = True + + def __init__(self, service_type, proxy_class=None, aliases=None): + """Class describing how to interact with a REST service. + + Each service in an OpenStack cloud needs to be found by looking + for it in the catalog. Once the endpoint is found, REST calls can + be made, but a Proxy class and some Resource objects are needed + to provide an object interface. + + Instances of ServiceDescription can be passed to + `openstack.connection.Connection.add_service`, or a list can be + passed to the `openstack.connection.Connection` constructor in + the ``extra_services`` argument. + + All three parameters can be provided at instantation time, or + a service-specific subclass can be used that sets the attributes + directly. + + :param string service_type: + service_type to look for in the keystone catalog + :param proxy2.BaseProxy proxy_class: + subclass of :class:`~openstack.proxy2.BaseProxy` implementing + an interface for this service. Defaults to + :class:`~openstack.proxy2.BaseProxy` which provides REST operations + but no additional features. + :param list aliases: + Optional list of aliases, if there is more than one name that might + be used to register the service in the catalog. + """ + self.service_type = service_type + self.proxy_class = proxy_class or self.proxy_class + self.all_types = _get_all_types(service_type, aliases) + + self._validate_proxy_class() + + def _validate_proxy_class(self): + if not issubclass( + self.proxy_class, (proxy.BaseProxy, proxy2.BaseProxy)): + raise TypeError( + "{module}.{proxy_class} must inherit from BaseProxy".format( + module=self.proxy_class.__module__, + proxy_class=self.proxy_class.__name__)) + if issubclass(self.proxy_class, proxy.BaseProxy) and self._warn_if_old: + warnings.warn( + "Use of proxy.BaseProxy is not supported." + " Please update to use proxy2.BaseProxy.", + DeprecationWarning) + + +class OpenStackServiceDescription(ServiceDescription): + + #: Override _warn_if_old so we don't spam people with warnings + _warn_if_old = False + + def __init__(self, service, config): + """Official OpenStack ServiceDescription. + + The OpenStackServiceDescription class is a helper class for + services listed in Service Types Authority and that are directly + supported by openstacksdk. + + It finds the proxy_class by looking in the openstacksdk tree for + appropriately named modules. + + :param dict service: + A service dict as found in `os_service_types.ServiceTypes.services` + :param openstack.config.cloud_region.CloudRegion config: + ConfigRegion for the connection. + """ + super(OpenStackServiceDescription, self).__init__( + service['service_type']) + self.config = config + service_filter = self._get_service_filter() + if service_filter: + module_name = service_filter.get_module() + "._proxy" + module = importlib.import_module(module_name) + self.proxy_class = getattr(module, "Proxy") + + def _get_service_filter(self): + service_filter_class = None + for service_type in self.all_types: + service_filter_class = self._find_service_filter_class() + if service_filter_class: + break + if not service_filter_class: + return None + # TODO(mordred) Replace this with proper discovery + version_string = self.config.get_api_version(self.service_type) + version = None + if version_string: + version = 'v{version}'.format(version=version_string[0]) + return service_filter_class(version=version) + + def _find_service_filter_class(self): + package_name = 'openstack.{service_type}'.format( + service_type=self.service_type).replace('-', '_') + module_name = self.service_type.replace('-', '_') + '_service' + class_name = ''.join( + [part.capitalize() for part in module_name.split('_')]) + try: + import_name = '.'.join([package_name, module_name]) + service_filter_module = importlib.import_module(import_name) + except ImportError as e: + # ImportWarning is ignored by default. This warning is here + # as an opt-in for people trying to figure out why something + # didn't work. + warnings.warn( + "Could not import {service_type} service filter: {e}".format( + service_type=self.service_type, e=str(e)), + ImportWarning) + return None + service_filter_class = getattr(service_filter_module, class_name, None) + if not service_filter_class: + _logger.warn( + 'Unable to find class %s in module for service %s', + class_name, self.service_type) + return None + return service_filter_class diff --git a/releasenotes/notes/add-service-0bcc16eb026eade3.yaml b/releasenotes/notes/add-service-0bcc16eb026eade3.yaml new file mode 100644 index 000000000..e515dc752 --- /dev/null +++ b/releasenotes/notes/add-service-0bcc16eb026eade3.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added a new method `openstack.connection.Connection.add_service` which + allows the registration of Proxy/Resource classes defined externally. From 1f05e3ac4c8b148b07cc7b695c0cdaab719d5db4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 11 Jan 2018 08:55:50 -0600 Subject: [PATCH 1904/3836] Remove meter service As per the mailing list thread on ceilometerclient, the REST API for ceilometer has been long deprecated and is now completely unsupported. Remove the meter service and its sub-service alarm. On the one hand, it's a REST API so there is no need for us to not support it. As a general rule removing support for services that we have already is a bad thing. On the other hand, we haven't cut the 1.0 yet, and as ceilometer doesn't exist anymore we can't really do devstack testing of it. The previous patch adds support for out-of-tree services. So anyone who cares about the 'meter' service can grab this, port it to Proxy2 and then do an add_service. Change-Id: Ic226c5ac048ba09e316c89cae1e8f2828e753e80 --- doc/source/enforcer.py | 2 - doc/source/user/guides/meter.rst | 11 -- doc/source/user/index.rst | 3 - doc/source/user/proxies/meter.rst | 85 --------- doc/source/user/resources/meter/index.rst | 11 -- .../user/resources/meter/v2/capability.rst | 12 -- doc/source/user/resources/meter/v2/meter.rst | 12 -- .../user/resources/meter/v2/resource.rst | 12 -- doc/source/user/resources/meter/v2/sample.rst | 12 -- .../user/resources/meter/v2/statistics.rst | 12 -- openstack/meter/__init__.py | 0 openstack/meter/alarm/__init__.py | 0 openstack/meter/alarm/alarm_service.py | 24 --- openstack/meter/alarm/v2/__init__.py | 0 openstack/meter/alarm/v2/_proxy.py | 129 -------------- openstack/meter/alarm/v2/alarm.py | 91 ---------- openstack/meter/alarm/v2/alarm_change.py | 52 ------ openstack/meter/meter_service.py | 24 --- openstack/meter/v2/__init__.py | 0 openstack/meter/v2/_proxy.py | 167 ------------------ openstack/meter/v2/capability.py | 37 ---- openstack/meter/v2/meter.py | 42 ----- openstack/meter/v2/resource.py | 44 ----- openstack/meter/v2/sample.py | 52 ------ openstack/meter/v2/statistics.py | 62 ------- openstack/profile.py | 4 - openstack/tests/functional/meter/__init__.py | 0 .../tests/functional/meter/alarm/__init__.py | 0 .../functional/meter/alarm/v2/__init__.py | 0 .../functional/meter/alarm/v2/test_alarm.py | 55 ------ .../meter/alarm/v2/test_alarm_change.py | 44 ----- .../tests/functional/meter/v2/__init__.py | 0 .../functional/meter/v2/test_capability.py | 27 --- .../tests/functional/meter/v2/test_meter.py | 31 ---- .../functional/meter/v2/test_resource.py | 24 --- .../tests/functional/meter/v2/test_sample.py | 26 --- .../functional/meter/v2/test_statistics.py | 26 --- openstack/tests/unit/meter/__init__.py | 0 openstack/tests/unit/meter/alarm/__init__.py | 0 .../unit/meter/alarm/test_alarm_service.py | 28 --- .../tests/unit/meter/alarm/v2/__init__.py | 0 .../tests/unit/meter/alarm/v2/test_alarm.py | 107 ----------- .../unit/meter/alarm/v2/test_alarm_change.py | 74 -------- .../tests/unit/meter/alarm/v2/test_proxy.py | 54 ------ .../tests/unit/meter/test_meter_service.py | 28 --- openstack/tests/unit/meter/v2/__init__.py | 0 .../tests/unit/meter/v2/test_capability.py | 70 -------- openstack/tests/unit/meter/v2/test_meter.py | 54 ------ openstack/tests/unit/meter/v2/test_proxy.py | 66 ------- .../tests/unit/meter/v2/test_resource.py | 59 ------- openstack/tests/unit/meter/v2/test_sample.py | 84 --------- .../tests/unit/meter/v2/test_statistics.py | 93 ---------- openstack/tests/unit/test_connection.py | 2 - .../notes/removed-meter-6f6651b6e452e000.yaml | 5 + 54 files changed, 5 insertions(+), 1852 deletions(-) delete mode 100644 doc/source/user/guides/meter.rst delete mode 100644 doc/source/user/proxies/meter.rst delete mode 100644 doc/source/user/resources/meter/index.rst delete mode 100644 doc/source/user/resources/meter/v2/capability.rst delete mode 100644 doc/source/user/resources/meter/v2/meter.rst delete mode 100644 doc/source/user/resources/meter/v2/resource.rst delete mode 100644 doc/source/user/resources/meter/v2/sample.rst delete mode 100644 doc/source/user/resources/meter/v2/statistics.rst delete mode 100644 openstack/meter/__init__.py delete mode 100644 openstack/meter/alarm/__init__.py delete mode 100644 openstack/meter/alarm/alarm_service.py delete mode 100644 openstack/meter/alarm/v2/__init__.py delete mode 100644 openstack/meter/alarm/v2/_proxy.py delete mode 100644 openstack/meter/alarm/v2/alarm.py delete mode 100644 openstack/meter/alarm/v2/alarm_change.py delete mode 100644 openstack/meter/meter_service.py delete mode 100644 openstack/meter/v2/__init__.py delete mode 100644 openstack/meter/v2/_proxy.py delete mode 100644 openstack/meter/v2/capability.py delete mode 100644 openstack/meter/v2/meter.py delete mode 100644 openstack/meter/v2/resource.py delete mode 100644 openstack/meter/v2/sample.py delete mode 100644 openstack/meter/v2/statistics.py delete mode 100644 openstack/tests/functional/meter/__init__.py delete mode 100644 openstack/tests/functional/meter/alarm/__init__.py delete mode 100644 openstack/tests/functional/meter/alarm/v2/__init__.py delete mode 100644 openstack/tests/functional/meter/alarm/v2/test_alarm.py delete mode 100644 openstack/tests/functional/meter/alarm/v2/test_alarm_change.py delete mode 100644 openstack/tests/functional/meter/v2/__init__.py delete mode 100644 openstack/tests/functional/meter/v2/test_capability.py delete mode 100644 openstack/tests/functional/meter/v2/test_meter.py delete mode 100644 openstack/tests/functional/meter/v2/test_resource.py delete mode 100644 openstack/tests/functional/meter/v2/test_sample.py delete mode 100644 openstack/tests/functional/meter/v2/test_statistics.py delete mode 100644 openstack/tests/unit/meter/__init__.py delete mode 100644 openstack/tests/unit/meter/alarm/__init__.py delete mode 100644 openstack/tests/unit/meter/alarm/test_alarm_service.py delete mode 100644 openstack/tests/unit/meter/alarm/v2/__init__.py delete mode 100644 openstack/tests/unit/meter/alarm/v2/test_alarm.py delete mode 100644 openstack/tests/unit/meter/alarm/v2/test_alarm_change.py delete mode 100644 openstack/tests/unit/meter/alarm/v2/test_proxy.py delete mode 100644 openstack/tests/unit/meter/test_meter_service.py delete mode 100644 openstack/tests/unit/meter/v2/__init__.py delete mode 100644 openstack/tests/unit/meter/v2/test_capability.py delete mode 100644 openstack/tests/unit/meter/v2/test_meter.py delete mode 100644 openstack/tests/unit/meter/v2/test_proxy.py delete mode 100644 openstack/tests/unit/meter/v2/test_resource.py delete mode 100644 openstack/tests/unit/meter/v2/test_sample.py delete mode 100644 openstack/tests/unit/meter/v2/test_statistics.py create mode 100644 releasenotes/notes/removed-meter-6f6651b6e452e000.yaml diff --git a/doc/source/enforcer.py b/doc/source/enforcer.py index 74d0102d0..d373ccd32 100644 --- a/doc/source/enforcer.py +++ b/doc/source/enforcer.py @@ -43,8 +43,6 @@ def get_proxy_methods(): "openstack.network.v2._proxy", "openstack.object_store.v1._proxy", "openstack.orchestration.v1._proxy", - "openstack.meter.v2._proxy", - "openstack.meter.alarm.v2._proxy", "openstack.workflow.v2._proxy"] modules = (importlib.import_module(name) for name in names) diff --git a/doc/source/user/guides/meter.rst b/doc/source/user/guides/meter.rst deleted file mode 100644 index a6850e282..000000000 --- a/doc/source/user/guides/meter.rst +++ /dev/null @@ -1,11 +0,0 @@ -Using OpenStack Meter -========================= - -.. caution:: - BETA: This API is a work in progress and is subject to change. - -Before working with the Meter service, you'll need to create a connection -to your OpenStack cloud by following the :doc:`connect` user guide. This will -provide you with the ``conn`` variable used in the examples below. - -.. TODO(thowe): Implement this guide diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index 778148176..719fd6162 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -40,7 +40,6 @@ approach, this is where you'll want to begin. Image Key Manager Message - Meter Network Object Store Orchestration @@ -95,7 +94,6 @@ provided by the SDK. Message v1 Message v2 Network - Meter Metric Object Store Orchestration @@ -125,7 +123,6 @@ The following services have exposed *Resource* classes. Image Key Management Load Balancer - Meter Metric Network Orchestration diff --git a/doc/source/user/proxies/meter.rst b/doc/source/user/proxies/meter.rst deleted file mode 100644 index 4a2f3a1bf..000000000 --- a/doc/source/user/proxies/meter.rst +++ /dev/null @@ -1,85 +0,0 @@ -Meter API -============= - -.. caution:: - BETA: This API is a work in progress and is subject to change. - -For details on how to use meter, see :doc:`/user/guides/meter` - -.. automodule:: openstack.meter.v2._proxy - -The Meter Class -------------------- - -The meter high-level interface is available through the ``meter`` -member of a :class:`~openstack.connection.Connection` object. The -``meter`` member will only be added if the service is detected. - -Sample Operations -^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.meter.v2._proxy.Proxy - - .. automethod:: openstack.meter.v2._proxy.Proxy.find_sample - .. automethod:: openstack.meter.v2._proxy.Proxy.samples - -Statistic Operations -^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.meter.v2._proxy.Proxy - - .. automethod:: openstack.meter.v2._proxy.Proxy.find_statistics - .. automethod:: openstack.meter.v2._proxy.Proxy.statistics - -Resource Operations -^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.meter.v2._proxy.Proxy - - .. automethod:: openstack.meter.v2._proxy.Proxy.get_resource - .. automethod:: openstack.meter.v2._proxy.Proxy.find_resource - .. automethod:: openstack.meter.v2._proxy.Proxy.resources - -Meter Operations -^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.meter.v2._proxy.Proxy - - .. automethod:: openstack.meter.v2._proxy.Proxy.find_meter - .. automethod:: openstack.meter.v2._proxy.Proxy.meters - -Capability Operations -^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.meter.v2._proxy.Proxy - - .. automethod:: openstack.meter.v2._proxy.Proxy.find_capability - .. automethod:: openstack.meter.v2._proxy.Proxy.capabilities - -The Alarm Class ---------------- - -The alarm high-level interface is available through the ``meter.alarm`` -member of a :class:`~openstack.connection.Connection` object. The -``meter.alarm`` member will only be added if the service is detected. - -Alarm Operations -^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.meter.alarm.v2._proxy.Proxy - - .. automethod:: openstack.meter.alarm.v2._proxy.Proxy.create_alarm - .. automethod:: openstack.meter.alarm.v2._proxy.Proxy.update_alarm - .. automethod:: openstack.meter.alarm.v2._proxy.Proxy.delete_alarm - .. automethod:: openstack.meter.alarm.v2._proxy.Proxy.get_alarm - .. automethod:: openstack.meter.alarm.v2._proxy.Proxy.find_alarm - .. automethod:: openstack.meter.alarm.v2._proxy.Proxy.alarms - - -Alarm Change Operations -^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.meter.alarm.v2._proxy.Proxy - - .. automethod:: openstack.meter.alarm.v2._proxy.Proxy.find_alarm_change - .. automethod:: openstack.meter.alarm.v2._proxy.Proxy.alarm_changes diff --git a/doc/source/user/resources/meter/index.rst b/doc/source/user/resources/meter/index.rst deleted file mode 100644 index 9efbaf68b..000000000 --- a/doc/source/user/resources/meter/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -Meter Resources -=================== - -.. toctree:: - :maxdepth: 1 - - v2/capability - v2/meter - v2/resource - v2/sample - v2/statistics diff --git a/doc/source/user/resources/meter/v2/capability.rst b/doc/source/user/resources/meter/v2/capability.rst deleted file mode 100644 index b710e907a..000000000 --- a/doc/source/user/resources/meter/v2/capability.rst +++ /dev/null @@ -1,12 +0,0 @@ -openstack.meter.v2.capability -================================= - -.. automodule:: openstack.meter.v2.capability - -The Capability Class --------------------- - -The ``Capability`` class inherits from :class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.meter.v2.capability.Capability - :members: diff --git a/doc/source/user/resources/meter/v2/meter.rst b/doc/source/user/resources/meter/v2/meter.rst deleted file mode 100644 index 4953ecdaa..000000000 --- a/doc/source/user/resources/meter/v2/meter.rst +++ /dev/null @@ -1,12 +0,0 @@ -openstack.meter.v2.meter -============================ - -.. automodule:: openstack.meter.v2.meter - -The Meter Class ----------------- - -The ``Meter`` class inherits from :class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.meter.v2.meter.Meter - :members: diff --git a/doc/source/user/resources/meter/v2/resource.rst b/doc/source/user/resources/meter/v2/resource.rst deleted file mode 100644 index c2d8c0fdb..000000000 --- a/doc/source/user/resources/meter/v2/resource.rst +++ /dev/null @@ -1,12 +0,0 @@ -openstack.meter.v2.resource -=============================== - -.. automodule:: openstack.meter.v2.resource - -The Resource Class ------------------- - -The ``Resource`` class inherits from :class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.meter.v2.resource.Resource - :members: diff --git a/doc/source/user/resources/meter/v2/sample.rst b/doc/source/user/resources/meter/v2/sample.rst deleted file mode 100644 index d5573dca6..000000000 --- a/doc/source/user/resources/meter/v2/sample.rst +++ /dev/null @@ -1,12 +0,0 @@ -openstack.meter.v2.sample -============================= - -.. automodule:: openstack.meter.v2.sample - -The Sample Class ----------------- - -The ``Sample`` class inherits from :class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.meter.v2.sample.Sample - :members: diff --git a/doc/source/user/resources/meter/v2/statistics.rst b/doc/source/user/resources/meter/v2/statistics.rst deleted file mode 100644 index 14661beb9..000000000 --- a/doc/source/user/resources/meter/v2/statistics.rst +++ /dev/null @@ -1,12 +0,0 @@ -openstack.meter.v2.statistics -================================= - -.. automodule:: openstack.meter.v2.statistics - -The Statistics Class --------------------- - -The ``Statistics`` class inherits from :class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.meter.v2.statistics.Statistics - :members: diff --git a/openstack/meter/__init__.py b/openstack/meter/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/openstack/meter/alarm/__init__.py b/openstack/meter/alarm/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/openstack/meter/alarm/alarm_service.py b/openstack/meter/alarm/alarm_service.py deleted file mode 100644 index a23cc3a64..000000000 --- a/openstack/meter/alarm/alarm_service.py +++ /dev/null @@ -1,24 +0,0 @@ -# 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 service_filter - - -class AlarmService(service_filter.ServiceFilter): - """The alarm service.""" - - valid_versions = [service_filter.ValidVersion('v2')] - - def __init__(self, version=None): - """Create an alarm service.""" - super(AlarmService, self).__init__(service_type='alarming', - version=version) diff --git a/openstack/meter/alarm/v2/__init__.py b/openstack/meter/alarm/v2/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/openstack/meter/alarm/v2/_proxy.py b/openstack/meter/alarm/v2/_proxy.py deleted file mode 100644 index 32b9e5795..000000000 --- a/openstack/meter/alarm/v2/_proxy.py +++ /dev/null @@ -1,129 +0,0 @@ -# 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 proxy -from openstack.meter.alarm.v2 import alarm as _alarm -from openstack.meter.alarm.v2 import alarm_change as _alarm_change - - -class Proxy(proxy.BaseProxy): - """.. caution:: This API is a work in progress and is subject to change.""" - - def create_alarm(self, **attrs): - """Create a new alarm from attributes - - :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.meter.v2.alarm.Alarm`, - comprised of the properties on the Alarm class. - - :returns: The results of alarm creation - :rtype: :class:`~openstack.meter.v2.alarm.Alarm` - """ - return self._create(_alarm.Alarm, **attrs) - - def delete_alarm(self, alarm, ignore_missing=True): - """Delete an alarm - - :param alarm: The value can be either the ID of an alarm or a - :class:`~openstack.meter.v2.alarm.Alarm` instance. - :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the alarm does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent alarm. - - :returns: ``None`` - """ - self._delete(_alarm.Alarm, alarm, ignore_missing=ignore_missing) - - def find_alarm(self, name_or_id, ignore_missing=True): - """Find a single alarm - - :param name_or_id: The name or ID of a alarm. - :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. - :returns: One :class:`~openstack.meter.v2.alarm.Alarm` or None - """ - return self._find(_alarm.Alarm, name_or_id, - ignore_missing=ignore_missing) - - def get_alarm(self, alarm): - """Get a single alarm - - :param alarm: The value can be the ID of an alarm or a - :class:`~openstack.meter.v2.alarm.Alarm` instance. - - :returns: One :class:`~openstack.meter.v2.alarm.Alarm` - :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. - """ - return self._get(_alarm.Alarm, alarm) - - def alarms(self, **query): - """Return a generator of alarms - - :param kwargs \*\*query: Optional query parameters to be sent to limit - the resources being returned. - - :returns: A generator of alarm objects - :rtype: :class:`~openstack.meter.v2.alarm.Alarm` - """ - # TODO(Qiming): Check the alarm service API docs/code to verify if - # the parameters need a change. - return self._list(_alarm.Alarm, paginated=False, **query) - - def update_alarm(self, alarm, **attrs): - """Update a alarm - - :param alarm: Either the id of a alarm or a - :class:`~openstack.meter.v2.alarm.Alarm` instance. - :attrs kwargs: The attributes to update on the alarm represented - by ``value``. - - :returns: The updated alarm - :rtype: :class:`~openstack.meter.v2.alarm.Alarm` - """ - return self._update(_alarm.Alarm, alarm, **attrs) - - def find_alarm_change(self, name_or_id, ignore_missing=True): - """Find a single alarm change - - :param name_or_id: The name or ID of a alarm change. - :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. - :returns: One :class:`~openstack.meter.v2.alarm_change.AlarmChange` - or None - """ - return self._find(_alarm_change.AlarmChange, name_or_id, - ignore_missing=ignore_missing) - - def alarm_changes(self, alarm, **query): - """Return a generator of alarm changes - - :param alarm: Alarm resource or id for alarm. - :param kwargs \*\*query: Optional query parameters to be sent to limit - the resources being returned. - - :returns: A generator of alarm change objects - :rtype: :class:`~openstack.meter.v2.alarm_change.AlarmChange` - """ - # TODO(Qiming): Check the alarm service API docs/code to verify if - # the parameters need a change. - alarm_id = _alarm.Alarm.from_id(alarm).id - return self._list(_alarm_change.AlarmChange, paginated=False, - path_args={'alarm_id': alarm_id}, **query) diff --git a/openstack/meter/alarm/v2/alarm.py b/openstack/meter/alarm/v2/alarm.py deleted file mode 100644 index 181a9b254..000000000 --- a/openstack/meter/alarm/v2/alarm.py +++ /dev/null @@ -1,91 +0,0 @@ -# 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 resource -from openstack.meter.alarm import alarm_service -from openstack import utils - - -class Alarm(resource.Resource): - """.. caution:: This API is a work in progress and is subject to change.""" - id_attribute = 'alarm_id' - base_path = '/alarms' - service = alarm_service.AlarmService() - - # Supported Operations - allow_create = True - allow_retrieve = True - allow_update = True - allow_delete = True - allow_list = True - - # Properties - #: The actions to do when alarm state changes to alarm - alarm_actions = resource.prop('alarm_actions') - #: The ID of the alarm - alarm_id = resource.prop('alarm_id') - # TODO(briancurtin): undocumented - combination_rule = resource.prop('combination_rule') - #: The description of the alarm - description = resource.prop('description') - #: ``True`` if this alarm is enabled. *Type: bool* - is_enabled = resource.prop('enabled', type=bool) - #: The actions to do when alarm state changes to insufficient data - insufficient_data_actions = resource.prop('insufficient_data_actions') - #: The actions should be re-triggered on each evaluation cycle. - #: *Type: bool* - is_repeat_actions = resource.prop('repeat_actions', type=bool) - #: The name for the alarm - name = resource.prop('name') - #: The actions to do when alarm state change to ok - ok_actions = resource.prop('ok_actions') - #: The ID of the project that owns the alarm - project_id = resource.prop('project_id') - #: The severity of the alarm - severity = resource.prop('severity') - #: The state off the alarm - state = resource.prop('state') - #: The timestamp of the last alarm state change. - #: *Type: ISO 8601 formatted string* - state_changed_at = resource.prop('state_timestamp') - # TODO(briancurtin): undocumented - threshold_rule = resource.prop('threshold_rule', type=dict) - #: Describe time constraints for the alarm - time_constraints = resource.prop('time_constraints') - #: Explicit type specifier to select which rule to follow - type = resource.prop('type') - #: The timestamp of the last alarm definition update. - #: *Type: ISO 8601 formatted string* - updated_at = resource.prop('timestamp') - #: The ID of the user who created the alarm - user_id = resource.prop('user_id') - - def change_state(self, session, next_state): - """Set the state of an alarm. - - :param next_state: The valid values can be one of: ``ok``, ``alarm``, - ``insufficient data``. - """ - url = utils.urljoin(self.base_path, self.id, 'state') - resp = session.put(url, json=next_state) - return resp.json() - - def check_state(self, session): - """Retrieve the current state of an alarm from the service. - - The properties of the alarm are not modified. - """ - url = utils.urljoin(self.base_path, self.id, 'state') - resp = session.get(url,) - resp = resp.json() - current_state = resp.replace('\"', '') - return current_state diff --git a/openstack/meter/alarm/v2/alarm_change.py b/openstack/meter/alarm/v2/alarm_change.py deleted file mode 100644 index a4dcdd624..000000000 --- a/openstack/meter/alarm/v2/alarm_change.py +++ /dev/null @@ -1,52 +0,0 @@ -# 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 resource -from openstack.meter.alarm import alarm_service - - -class AlarmChange(resource.Resource): - """.. caution:: This API is a work in progress and is subject to change.""" - id_attribute = 'event_id' - resource_key = 'alarm_change' - base_path = '/alarms/%(alarm_id)s/history' - service = alarm_service.AlarmService() - - # Supported Operations - allow_list = True - - # Properties - #: The ID of the alarm - alarm_id = resource.prop('alarm_id') - #: Data describing the change - detail = resource.prop('detail') - #: The ID of the change event - event_id = resource.prop('event_id') - #: The project ID on behalf of which the change is being made - on_behalf_of_id = resource.prop('on_behalf_of') - #: The project ID of the initiating identity - project_id = resource.prop('project_id') - #: The time/date of the alarm change. - #: *Type: ISO 8601 formatted string* - triggered_at = resource.prop('timestamp') - #: The type of change - type = resource.prop('type') - #: The user ID of the initiating identity - user_id = resource.prop('user_id') - - @classmethod - def list(cls, session, limit=None, marker=None, path_args=None, - paginated=False, **params): - url = cls._get_url(path_args) - resp = session.get(url, params=params) - for item in resp.json(): - yield cls.existing(**item) diff --git a/openstack/meter/meter_service.py b/openstack/meter/meter_service.py deleted file mode 100644 index d84bffb67..000000000 --- a/openstack/meter/meter_service.py +++ /dev/null @@ -1,24 +0,0 @@ -# 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 service_filter - - -class MeterService(service_filter.ServiceFilter): - """The meter service.""" - - valid_versions = [service_filter.ValidVersion('v2')] - - def __init__(self, version=None): - """Create a meter service.""" - super(MeterService, self).__init__(service_type='metering', - version=version) diff --git a/openstack/meter/v2/__init__.py b/openstack/meter/v2/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/openstack/meter/v2/_proxy.py b/openstack/meter/v2/_proxy.py deleted file mode 100644 index dcf4b108c..000000000 --- a/openstack/meter/v2/_proxy.py +++ /dev/null @@ -1,167 +0,0 @@ -# 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 proxy2 -from openstack.meter.v2 import capability -from openstack.meter.v2 import meter as _meter -from openstack.meter.v2 import resource as _resource -from openstack.meter.v2 import sample -from openstack.meter.v2 import statistics - - -class Proxy(proxy2.BaseProxy): - """.. caution:: This API is a work in progress and is subject to change.""" - - def find_capability(self, name_or_id, ignore_missing=True): - """Find a single capability - - :param name_or_id: The name or ID of a capability. - :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. - :returns: One :class:`~openstack.meter.v2.capability.Capability` - or None - """ - return self._find(capability.Capability, name_or_id, - ignore_missing=ignore_missing) - - def capabilities(self, **query): - """Return a generator of capabilities - - :param kwargs \*\*query: Optional query parameters to be sent to limit - the resources being returned. - - :returns: A generator of capability objects - :rtype: :class:`~openstack.meter.v2.capability.Capability` - """ - return self._list(capability.Capability, paginated=False, **query) - - def find_meter(self, name_or_id, ignore_missing=True): - """Find a single meter - - :param name_or_id: The name or ID of a meter. - :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. - :returns: One :class:`~openstack.meter.v2.meter.Meter` or None - """ - return self._find(_meter.Meter, name_or_id, - ignore_missing=ignore_missing) - - def meters(self, **query): - """Return a generator of meters - - :param kwargs \*\*query: Optional query parameters to be sent to limit - the resources being returned. - - :returns: A generator of meter objects - :rtype: :class:`~openstack.meter.v2.meter.Meter` - """ - return self._list(_meter.Meter, paginated=False, **query) - - def find_resource(self, name_or_id, ignore_missing=True): - """Find a single resource - - :param name_or_id: The name or ID of a resource. - :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. - :returns: One :class:`~openstack.meter.v2.resource.Resource` or - None - """ - return self._find(_resource.Resource, name_or_id, - ignore_missing=ignore_missing) - - def get_resource(self, resource): - """Get a single resource - - :param resource: The value can be the ID of a resource or a - :class:`~openstack.meter.v2.resource.Resource` - instance. - - :returns: One :class:`~openstack.meter.v2.resource.Resource` - :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. - """ - return self._get(_resource.Resource, resource) - - def resources(self, **query): - """Return a generator of resources - - :param kwargs \*\*query: Optional query parameters to be sent to limit - the resources being returned. - - :returns: A generator of resource objects - :rtype: :class:`~openstack.meter.v2.resource.Resource` - """ - return self._list(_resource.Resource, paginated=False, **query) - - def find_sample(self, name_or_id, ignore_missing=True): - """Find a single sample - - :param name_or_id: The name or ID of a sample. - :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. - :returns: One :class:`~openstack.meter.v2.sample.Sample` or None - """ - return self._find(sample.Sample, name_or_id, - ignore_missing=ignore_missing) - - def samples(self, meter, **query): - """Return a generator of samples - - :param value: Meter resource or name for a meter. - :param kwargs \*\*query: Optional query parameters to be sent to limit - the resources being returned. - - :returns: A generator of sample objects - :rtype: :class:`~openstack.meter.v2.sample.Sample` - """ - return self._list(sample.Sample, paginated=False, - counter_name=meter, **query) - - def find_statistics(self, name_or_id, ignore_missing=True): - """Find a single statistics - - :param name_or_id: The name or ID of a statistics. - :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. - :returns: One :class:`~openstack.meter.v2.statistics.Statistics` - or None - """ - return self._find(statistics.Statistics, name_or_id, - ignore_missing=ignore_missing) - - def statistics(self, meter, **query): - """Return a generator of statistics - - :param meter: Meter resource or name for a meter. - :param kwargs \*\*query: Optional query parameters to be sent to limit - the resources being returned. - - :returns: A generator of statistics objects - :rtype: :class:`~openstack.meter.v2.statistics.Statistics` - """ - return self._list(statistics.Statistics, paginated=False, - meter_name=meter, **query) diff --git a/openstack/meter/v2/capability.py b/openstack/meter/v2/capability.py deleted file mode 100644 index cdef31970..000000000 --- a/openstack/meter/v2/capability.py +++ /dev/null @@ -1,37 +0,0 @@ -# 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 resource2 as resource -from openstack.meter import meter_service - - -class Capability(resource.Resource): - """.. caution:: This API is a work in progress and is subject to change.""" - resource_key = 'capability' - resources_key = 'capabilities' - base_path = '/capabilities' - service = meter_service.MeterService() - - # Supported Operations - allow_list = True - - # Properties - is_enabled = resource.Body('enabled', type=bool) - - @classmethod - def list(cls, session, paginated=False, **params): - resp = session.get(cls.base_path, - params=params) - resp = resp.json() - for key, value in resp['api'].items(): - yield cls.existing(id=key, enabled=value) diff --git a/openstack/meter/v2/meter.py b/openstack/meter/v2/meter.py deleted file mode 100644 index b716252e9..000000000 --- a/openstack/meter/v2/meter.py +++ /dev/null @@ -1,42 +0,0 @@ -# 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 resource2 as resource -from openstack.meter import meter_service - - -class Meter(resource.Resource): - """.. caution:: This API is a work in progress and is subject to change.""" - resource_key = 'meter' - base_path = '/meters' - service = meter_service.MeterService() - - # Supported Operations - allow_list = True - - # Properties - #: The ID of the meter - meter_id = resource.Body('meter_id', alternate_id=True) - #: The unique name for the meter - name = resource.Body('name') - #: The ID of the project that owns the resource - project_id = resource.Body('project_id') - #: The ID of the resource for which the measurements are taken - resource_id = resource.Body('resource_id') - #: The name of the source where the meter comes from - source = resource.Body('source') - #: The meter type - type = resource.Body('type') - #: The unit of measure - unit = resource.Body('unit') - #: The ID of the user who last triggered an update to the resource - user_id = resource.Body('user_id') diff --git a/openstack/meter/v2/resource.py b/openstack/meter/v2/resource.py deleted file mode 100644 index 7e9f560ea..000000000 --- a/openstack/meter/v2/resource.py +++ /dev/null @@ -1,44 +0,0 @@ -# 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 resource2 as resource -from openstack.meter import meter_service - - -class Resource(resource.Resource): - """.. caution:: This API is a work in progress and is subject to change.""" - base_path = '/resources' - service = meter_service.MeterService() - - # Supported Operations - allow_get = True - allow_list = True - - # Properties - #: UTC date & time not later than the first sample known - #: for this resource. - first_sample_at = resource.Body('first_sample_timestamp') - #: UTC date & time not earlier than the last sample known - #: for this resource. - last_sample_at = resource.Body('last_sample_timestamp') - #: A list containing a self link and associated meter links - links = resource.Body('links') - #: Arbitrary metadata associated with the resource - metadata = resource.Body('metadata') - #: The ID of the owning project - project_id = resource.Body('project_id') - #: The ID for the resource - resource_id = resource.Body('resource_id', alternate_id=True) - #: The name of the source where the resource comes from - source = resource.Body('source') - #: The ID of the user who created the resource or updated it last - user_id = resource.Body('user_id') diff --git a/openstack/meter/v2/sample.py b/openstack/meter/v2/sample.py deleted file mode 100644 index d4838e44d..000000000 --- a/openstack/meter/v2/sample.py +++ /dev/null @@ -1,52 +0,0 @@ -# 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 resource2 as resource -from openstack.meter import meter_service - - -class Sample(resource.Resource): - """.. caution:: This API is a work in progress and is subject to change.""" - base_path = '/meters/%(counter_name)s' - service = meter_service.MeterService() - - # Supported Operations - allow_get = True - allow_list = True - - # Properties - #: When the sample has been generated. - generated_at = resource.Body('timestamp') - #: The message ID - message_id = resource.Body('message_id', alternate_id=True) - #: Arbitrary metadata associated with the sample - metadata = resource.Body('metadata') - #: The meter name this sample is for - counter_name = resource.Body('counter_name') - #: The meter name this sample is for - counter_type = resource.Body('counter_type') - #: The ID of the project this sample was taken for - project_id = resource.Body('project_id') - #: When the sample has been recorded. - recorded_at = resource.Body('recorded_at') - #: The ID of the resource this sample was taken for - resource_id = resource.Body('resource_id') - #: The name of the source that identifies where the sample comes from - source = resource.Body('source') - #: The meter type - type = resource.Body('type') - #: The unit of measure - unit = resource.Body('unit') - #: The ID of the user this sample was taken for - user_id = resource.Body('user_id') - #: The metered value - volume = resource.Body('volume') diff --git a/openstack/meter/v2/statistics.py b/openstack/meter/v2/statistics.py deleted file mode 100644 index 08a9e03b8..000000000 --- a/openstack/meter/v2/statistics.py +++ /dev/null @@ -1,62 +0,0 @@ -# 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 resource2 as resource -from openstack.meter import meter_service - - -class Statistics(resource.Resource): - """.. caution:: This API is a work in progress and is subject to change.""" - resource_key = 'statistics' - base_path = '/meters/%(meter_name)s/statistics' - service = meter_service.MeterService() - - # Supported Operations - allow_list = True - - # Properties - #: The selectable aggregate value(s) - aggregate = resource.Body('aggregate') - #: The average of all of the volume values seen in the data - avg = resource.Body('avg') - #: The number of samples seen - count = resource.Body('count') - #: The difference, in seconds, between the oldest and newest timestamp - duration = resource.Body('duration') - #: UTC date and time of the oldest timestamp, or the query end time. - duration_end_at = resource.Body('duration_end') - #: UTC date and time of the earliest timestamp, or the query start time. - duration_start_at = resource.Body('duration_start') - #: Dictionary of field names for group, if groupby statistics are requested - group_by = resource.Body('groupby') - #: The maximum volume seen in the data - max = resource.Body('max') - #: The minimum volume seen in the data - min = resource.Body('min') - #: The difference, in seconds, between the period start and end - period = resource.Body('period') - #: UTC date and time of the period end. - period_end_at = resource.Body('period_end') - #: UTC date and time of the period start. - period_start_at = resource.Body('period_start') - #: The total of all of the volume values seen in the data - sum = resource.Body('sum') - #: The unit type of the data set - #: TODO(Qiming): This is still incorrect - unit = resource.Body('unit', alternate_id=True) - - @classmethod - def list(cls, session, paginated=False, **params): - url = cls.base_path % {'meter_name': params.pop('meter_name')} - resp = session.get(url, params=params) - for stat in resp.json(): - yield cls.existing(**stat) diff --git a/openstack/profile.py b/openstack/profile.py index 588fd60c7..77aac0e34 100644 --- a/openstack/profile.py +++ b/openstack/profile.py @@ -65,8 +65,6 @@ from openstack.key_manager import key_manager_service from openstack.load_balancer import load_balancer_service as lb_service from openstack.message import message_service -from openstack.meter.alarm import alarm_service -from openstack.meter import meter_service from openstack.network import network_service from openstack.object_store import object_store_service from openstack.orchestration import orchestration_service @@ -92,7 +90,6 @@ def __init__(self, plugins=None): """ self._services = {} - self._add_service(alarm_service.AlarmService(version="v2")) self._add_service(baremetal_service.BaremetalService(version="v1")) self._add_service( block_storage_service.BlockStorageService(version="v2")) @@ -104,7 +101,6 @@ def __init__(self, plugins=None): self._add_service(key_manager_service.KeyManagerService(version="v1")) self._add_service(lb_service.LoadBalancerService(version="v2")) self._add_service(message_service.MessageService(version="v1")) - self._add_service(meter_service.MeterService(version="v2")) self._add_service(network_service.NetworkService(version="v2")) self._add_service( object_store_service.ObjectStoreService(version="v1")) diff --git a/openstack/tests/functional/meter/__init__.py b/openstack/tests/functional/meter/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/openstack/tests/functional/meter/alarm/__init__.py b/openstack/tests/functional/meter/alarm/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/openstack/tests/functional/meter/alarm/v2/__init__.py b/openstack/tests/functional/meter/alarm/v2/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/openstack/tests/functional/meter/alarm/v2/test_alarm.py b/openstack/tests/functional/meter/alarm/v2/test_alarm.py deleted file mode 100644 index 34cdf2703..000000000 --- a/openstack/tests/functional/meter/alarm/v2/test_alarm.py +++ /dev/null @@ -1,55 +0,0 @@ -# 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 unittest - -from openstack.meter.alarm.v2 import alarm -from openstack.tests.functional import base - - -@unittest.skip("bug/1524468") -class TestAlarm(base.BaseFunctionalTest): - - ID = None - - def setUp(self): - super(TestAlarm, self).setUp() - self.require_service('alarming') - self.require_service('metering') - - self.NAME = self.getUniqueString() - meter = next(self.conn.meter.meters()) - sot = self.conn.alarm.create_alarm( - name=self.NAME, - type='threshold', - threshold_rule={ - 'meter_name': meter.name, - 'threshold': 1.1, - }, - ) - assert isinstance(sot, alarm.Alarm) - self.assertEqual(self.NAME, sot.name) - self.ID = sot.id - - def tearDown(self): - sot = self.conn.alarm.delete_alarm(self.ID, ignore_missing=False) - self.assertIsNone(sot) - super(TestAlarm, self).tearDown() - - def test_get(self): - sot = self.conn.alarm.get_alarm(self.ID) - self.assertEqual(self.NAME, sot.name) - self.assertEqual(self.ID, sot.id) - - def test_list(self): - names = [o.name for o in self.conn.alarm.alarms()] - self.assertIn(self.NAME, names) diff --git a/openstack/tests/functional/meter/alarm/v2/test_alarm_change.py b/openstack/tests/functional/meter/alarm/v2/test_alarm_change.py deleted file mode 100644 index f8bd5dffe..000000000 --- a/openstack/tests/functional/meter/alarm/v2/test_alarm_change.py +++ /dev/null @@ -1,44 +0,0 @@ -# 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 unittest - -from openstack.tests.functional import base - - -@unittest.skip("bug/1524468") -class TestAlarmChange(base.BaseFunctionalTest): - - alarm = None - - def setUp(self): - super(TestAlarmChange, self).setUp() - self.require_service('alarming') - self.require_service('metering') - - self.NAME = self.getUniqueString() - meter = next(self.conn.meter.meters()) - self.alarm = self.conn.alarm.create_alarm( - name=self.NAME, - type='threshold', - threshold_rule={ - 'meter_name': meter.name, - 'threshold': 1.1, - }, - ) - self.addCleanup( - self.conn.alarm.delete_alarm, self.alarm, ignore_missing=False) - - def test_list(self): - change = next(self.conn.alarm.alarm_changes(self.alarm)) - self.assertEqual(self.alarm.id, change.alarm_id) - self.assertEqual('creation', change.type) diff --git a/openstack/tests/functional/meter/v2/__init__.py b/openstack/tests/functional/meter/v2/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/openstack/tests/functional/meter/v2/test_capability.py b/openstack/tests/functional/meter/v2/test_capability.py deleted file mode 100644 index 991fc3c11..000000000 --- a/openstack/tests/functional/meter/v2/test_capability.py +++ /dev/null @@ -1,27 +0,0 @@ -# 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.tests.functional import base - - -class TestCapability(base.BaseFunctionalTest): - - def setUp(self): - super(TestCapability, self).setUp() - self.require_service('metering') - - def test_list(self): - ids = [o.id for o in self.conn.meter.capabilities()] - self.assertIn('resources:query:simple', ids) - self.assertIn('meters:query:simple', ids) - self.assertIn('statistics:query:simple', ids) - self.assertIn('samples:query:simple', ids) diff --git a/openstack/tests/functional/meter/v2/test_meter.py b/openstack/tests/functional/meter/v2/test_meter.py deleted file mode 100644 index 913bf4b73..000000000 --- a/openstack/tests/functional/meter/v2/test_meter.py +++ /dev/null @@ -1,31 +0,0 @@ -# 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.tests.functional import base - - -class TestMeter(base.BaseFunctionalTest): - - def setUp(self): - super(TestMeter, self).setUp() - self.require_service('metering') - - def test_list(self): - # TODO(thowe): Remove this in favor of create_meter call. - # Since we do not have a create meter method at the moment - # make sure there is some data in there - name = self.getUniqueString() - tainer = self.conn.object_store.create_container(name=name) - self.conn.object_store.delete_container(tainer) - - names = set([o.name for o in self.conn.meter.meters()]) - self.assertIn('storage.objects.incoming.bytes', names) diff --git a/openstack/tests/functional/meter/v2/test_resource.py b/openstack/tests/functional/meter/v2/test_resource.py deleted file mode 100644 index 37c2bfbe7..000000000 --- a/openstack/tests/functional/meter/v2/test_resource.py +++ /dev/null @@ -1,24 +0,0 @@ -# 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.tests.functional import base - - -class TestResource(base.BaseFunctionalTest): - - def setUp(self): - super(TestResource, self).setUp() - self.require_service('metering') - - def test_list(self): - ids = [o.resource_id for o in self.conn.meter.resources()] - self.assertNotEqual(0, len(ids)) diff --git a/openstack/tests/functional/meter/v2/test_sample.py b/openstack/tests/functional/meter/v2/test_sample.py deleted file mode 100644 index 83adca7ab..000000000 --- a/openstack/tests/functional/meter/v2/test_sample.py +++ /dev/null @@ -1,26 +0,0 @@ -# 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.meter.v2 import sample -from openstack.tests.functional import base - - -class TestSample(base.BaseFunctionalTest): - - def setUp(self): - super(TestSample, self).setUp() - self.require_service('metering') - - def test_list(self): - for meter in self.conn.meter.meters(): - for sot in self.conn.meter.samples(meter): - assert isinstance(sot, sample.Sample) diff --git a/openstack/tests/functional/meter/v2/test_statistics.py b/openstack/tests/functional/meter/v2/test_statistics.py deleted file mode 100644 index 85cd47bdd..000000000 --- a/openstack/tests/functional/meter/v2/test_statistics.py +++ /dev/null @@ -1,26 +0,0 @@ -# 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.tests.functional import base - - -class TestStatistics(base.BaseFunctionalTest): - - def setUp(self): - super(TestStatistics, self).setUp() - self.require_service('metering') - - def test_list(self): - for met in self.conn.meter.meters(): - for stat in self.conn.meter.statistics(met): - self.assertTrue(stat.period_end_at is not None) - break diff --git a/openstack/tests/unit/meter/__init__.py b/openstack/tests/unit/meter/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/openstack/tests/unit/meter/alarm/__init__.py b/openstack/tests/unit/meter/alarm/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/openstack/tests/unit/meter/alarm/test_alarm_service.py b/openstack/tests/unit/meter/alarm/test_alarm_service.py deleted file mode 100644 index 3f6ffea85..000000000 --- a/openstack/tests/unit/meter/alarm/test_alarm_service.py +++ /dev/null @@ -1,28 +0,0 @@ -# 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 testtools - -from openstack.meter.alarm import alarm_service - - -class TestAlarmService(testtools.TestCase): - - def test_service(self): - sot = alarm_service.AlarmService() - self.assertEqual('alarming', sot.service_type) - self.assertEqual('public', sot.interface) - self.assertIsNone(sot.region) - self.assertIsNone(sot.service_name) - self.assertEqual(1, len(sot.valid_versions)) - self.assertEqual('v2', sot.valid_versions[0].module) - self.assertEqual('v2', sot.valid_versions[0].path) diff --git a/openstack/tests/unit/meter/alarm/v2/__init__.py b/openstack/tests/unit/meter/alarm/v2/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/openstack/tests/unit/meter/alarm/v2/test_alarm.py b/openstack/tests/unit/meter/alarm/v2/test_alarm.py deleted file mode 100644 index ea35e4a92..000000000 --- a/openstack/tests/unit/meter/alarm/v2/test_alarm.py +++ /dev/null @@ -1,107 +0,0 @@ -# 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 testtools - -from openstack.meter.alarm.v2 import alarm - -IDENTIFIER = 'IDENTIFIER' -EXAMPLE = { - 'alarm_actions': ['1'], - 'alarm_id': IDENTIFIER, - 'combination_rule': {'alarm_ids': ['2', 'b'], 'operator': 'or', }, - 'description': '3', - 'enabled': True, - 'insufficient_data_actions': ['4'], - 'name': '5', - 'ok_actions': ['6'], - 'project_id': '7', - 'repeat_actions': False, - 'severity': 'low', - 'state': 'insufficient data', - 'state_timestamp': '2015-03-09T12:15:57.233772', - 'timestamp': '2015-03-09T12:15:57.233772', - 'threshold_rule': { - 'meter_name': 'a', - 'evaluation_periods:': '1', - 'period': '60', - 'statistic': 'avg', - 'threshold': '92.6', - 'comparison_operator': 'gt', - 'exclude_outliers': True, - }, - 'time_constraints': [{'name': 'a', 'duration': 'b', 'start': 'c', }], - 'type': '10', - 'user_id': '11', -} - - -class TestAlarm(testtools.TestCase): - - def setUp(self): - super(TestAlarm, self).setUp() - self.resp = mock.Mock() - self.resp.body = '' - self.resp.json = mock.Mock(return_value=self.resp.body) - self.sess = mock.Mock() - self.sess.put = mock.Mock(return_value=self.resp) - - def test_basic(self): - sot = alarm.Alarm() - self.assertIsNone(sot.resource_key) - self.assertIsNone(sot.resources_key) - self.assertEqual('/alarms', sot.base_path) - self.assertEqual('alarming', sot.service.service_type) - self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_retrieve) - self.assertTrue(sot.allow_update) - self.assertTrue(sot.allow_delete) - self.assertTrue(sot.allow_list) - - def test_make_it(self): - sot = alarm.Alarm(EXAMPLE) - self.assertEqual(IDENTIFIER, sot.id) - self.assertEqual(EXAMPLE['alarm_actions'], sot.alarm_actions) - self.assertEqual(IDENTIFIER, sot.alarm_id) - self.assertEqual(EXAMPLE['combination_rule'], sot.combination_rule) - self.assertEqual(EXAMPLE['description'], sot.description) - self.assertTrue(sot.is_enabled) - self.assertEqual(EXAMPLE['insufficient_data_actions'], - sot.insufficient_data_actions) - self.assertEqual(EXAMPLE['name'], sot.name) - self.assertEqual(EXAMPLE['ok_actions'], sot.ok_actions) - self.assertEqual(EXAMPLE['project_id'], sot.project_id) - self.assertFalse(sot.is_repeat_actions) - self.assertEqual(EXAMPLE['severity'], sot.severity) - self.assertEqual(EXAMPLE['state'], sot.state) - self.assertEqual(EXAMPLE['state_timestamp'], sot.state_changed_at) - self.assertEqual(EXAMPLE['timestamp'], sot.updated_at) - self.assertEqual(EXAMPLE['threshold_rule'], sot.threshold_rule) - self.assertEqual(EXAMPLE['time_constraints'], sot.time_constraints) - self.assertEqual(EXAMPLE['type'], sot.type) - self.assertEqual(EXAMPLE['user_id'], sot.user_id) - - def test_check_status(self): - sot = alarm.Alarm(EXAMPLE) - sot.check_state(self.sess) - - url = 'alarms/IDENTIFIER/state' - self.sess.get.assert_called_with(url,) - - def test_change_status(self): - sot = alarm.Alarm(EXAMPLE) - self.assertEqual(self.resp.body, sot.change_state(self.sess, 'alarm')) - - url = 'alarms/IDENTIFIER/state' - self.sess.put.assert_called_with(url, - json='alarm') diff --git a/openstack/tests/unit/meter/alarm/v2/test_alarm_change.py b/openstack/tests/unit/meter/alarm/v2/test_alarm_change.py deleted file mode 100644 index 84326f4cb..000000000 --- a/openstack/tests/unit/meter/alarm/v2/test_alarm_change.py +++ /dev/null @@ -1,74 +0,0 @@ -# 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 testtools - -from openstack.meter.alarm.v2 import alarm_change - -IDENTIFIER = 'IDENTIFIER' -EXAMPLE = { - 'alarm_id': 0, - 'detail': '1', - 'event_id': IDENTIFIER, - 'on_behalf_of': '3', - 'project_id': '4', - 'timestamp': '2015-03-09T12:15:57.233772', - 'type': '6', - 'user_id': '7', -} - - -class TestAlarmChange(testtools.TestCase): - - def test_basic(self): - sot = alarm_change.AlarmChange() - self.assertEqual('alarm_change', sot.resource_key) - self.assertIsNone(sot.resources_key) - self.assertEqual('/alarms/%(alarm_id)s/history', sot.base_path) - self.assertEqual('alarming', sot.service.service_type) - self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_retrieve) - self.assertFalse(sot.allow_update) - self.assertFalse(sot.allow_delete) - self.assertTrue(sot.allow_list) - - def test_make_it(self): - sot = alarm_change.AlarmChange(EXAMPLE) - self.assertEqual(IDENTIFIER, sot.id) - self.assertEqual(EXAMPLE['alarm_id'], sot.alarm_id) - self.assertEqual(EXAMPLE['detail'], sot.detail) - self.assertEqual(IDENTIFIER, sot.event_id) - self.assertEqual(EXAMPLE['on_behalf_of'], sot.on_behalf_of_id) - self.assertEqual(EXAMPLE['project_id'], sot.project_id) - self.assertEqual(EXAMPLE['timestamp'], sot.triggered_at) - self.assertEqual(EXAMPLE['type'], sot.type) - self.assertEqual(EXAMPLE['user_id'], sot.user_id) - - def test_list(self): - sess = mock.Mock() - resp = mock.Mock() - resp.json = mock.Mock(return_value=[EXAMPLE, EXAMPLE]) - sess.get = mock.Mock(return_value=resp) - path_args = {'alarm_id': IDENTIFIER} - - found = alarm_change.AlarmChange.list(sess, path_args=path_args) - first = next(found) - self.assertEqual(IDENTIFIER, first.id) - self.assertEqual(EXAMPLE['alarm_id'], first.alarm_id) - self.assertEqual(EXAMPLE['detail'], first.detail) - self.assertEqual(IDENTIFIER, first.event_id) - self.assertEqual(EXAMPLE['on_behalf_of'], first.on_behalf_of_id) - self.assertEqual(EXAMPLE['project_id'], first.project_id) - self.assertEqual(EXAMPLE['timestamp'], first.triggered_at) - self.assertEqual(EXAMPLE['type'], first.type) - self.assertEqual(EXAMPLE['user_id'], first.user_id) diff --git a/openstack/tests/unit/meter/alarm/v2/test_proxy.py b/openstack/tests/unit/meter/alarm/v2/test_proxy.py deleted file mode 100644 index fc839e52e..000000000 --- a/openstack/tests/unit/meter/alarm/v2/test_proxy.py +++ /dev/null @@ -1,54 +0,0 @@ -# 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.meter.alarm.v2 import _proxy -from openstack.meter.alarm.v2 import alarm -from openstack.meter.alarm.v2 import alarm_change -from openstack.tests.unit import test_proxy_base - - -class TestAlarmProxy(test_proxy_base.TestProxyBase): - def setUp(self): - super(TestAlarmProxy, self).setUp() - self.proxy = _proxy.Proxy(self.session) - - def test_alarm_change_find(self): - self.verify_find(self.proxy.find_alarm_change, - alarm_change.AlarmChange) - - def test_alarm_changes(self): - larm = alarm.Alarm.existing(alarm_id='larm') - expected_kwargs = {'path_args': {'alarm_id': 'larm'}} - self.verify_list(self.proxy.alarm_changes, alarm_change.AlarmChange, - method_args=[larm], paginated=False, - expected_kwargs=expected_kwargs) - - def test_alarm_create_attrs(self): - self.verify_create(self.proxy.create_alarm, alarm.Alarm) - - def test_alarm_delete(self): - self.verify_delete(self.proxy.delete_alarm, alarm.Alarm, False) - - def test_alarm_delete_ignore(self): - self.verify_delete(self.proxy.delete_alarm, alarm.Alarm, True) - - def test_alarm_find(self): - self.verify_find(self.proxy.find_alarm, alarm.Alarm) - - def test_alarm_get(self): - self.verify_get(self.proxy.get_alarm, alarm.Alarm) - - def test_alarms(self): - self.verify_list(self.proxy.alarms, alarm.Alarm, paginated=False) - - def test_alarm_update(self): - self.verify_update(self.proxy.update_alarm, alarm.Alarm) diff --git a/openstack/tests/unit/meter/test_meter_service.py b/openstack/tests/unit/meter/test_meter_service.py deleted file mode 100644 index 330c6f1d0..000000000 --- a/openstack/tests/unit/meter/test_meter_service.py +++ /dev/null @@ -1,28 +0,0 @@ -# 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 testtools - -from openstack.meter import meter_service - - -class TestMeterService(testtools.TestCase): - - def test_service(self): - sot = meter_service.MeterService() - self.assertEqual('metering', sot.service_type) - self.assertEqual('public', sot.interface) - self.assertIsNone(sot.region) - self.assertIsNone(sot.service_name) - self.assertEqual(1, len(sot.valid_versions)) - self.assertEqual('v2', sot.valid_versions[0].module) - self.assertEqual('v2', sot.valid_versions[0].path) diff --git a/openstack/tests/unit/meter/v2/__init__.py b/openstack/tests/unit/meter/v2/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/openstack/tests/unit/meter/v2/test_capability.py b/openstack/tests/unit/meter/v2/test_capability.py deleted file mode 100644 index 0cde8b56c..000000000 --- a/openstack/tests/unit/meter/v2/test_capability.py +++ /dev/null @@ -1,70 +0,0 @@ -# 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 testtools - -from openstack.meter.v2 import capability - -EXAMPLE = { - "id": "123", - "enabled": False, -} -BODY = { - "api": { - "statistics:query:complex": False, - "alarms:history:query:simple": True, - "events:query:simple": True, - "alarms:query:simple": True, - "resources:query:simple": True, - } -} - - -class TestCapability(testtools.TestCase): - def test_basic(self): - sot = capability.Capability() - self.assertEqual('capability', sot.resource_key) - self.assertEqual('capabilities', sot.resources_key) - self.assertEqual('/capabilities', sot.base_path) - self.assertEqual('metering', sot.service.service_type) - self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) - self.assertFalse(sot.allow_delete) - self.assertTrue(sot.allow_list) - - def test_make_it(self): - sot = capability.Capability(**EXAMPLE) - self.assertEqual(EXAMPLE['id'], sot.id) - self.assertEqual(EXAMPLE['enabled'], sot.is_enabled) - - def test_list(self): - sess = mock.Mock() - resp = mock.Mock() - resp.json = mock.Mock(return_value=BODY) - sess.get = mock.Mock(return_value=resp) - - caps = capability.Capability.list(sess) - - caps = sorted(caps, key=lambda cap: cap.id) - self.assertEqual(5, len(caps)) - self.assertEqual('alarms:history:query:simple', caps[0].id) - self.assertTrue(caps[0].is_enabled) - self.assertEqual('alarms:query:simple', caps[1].id) - self.assertTrue(caps[1].is_enabled) - self.assertEqual('events:query:simple', caps[2].id) - self.assertTrue(caps[2].is_enabled) - self.assertEqual('resources:query:simple', caps[3].id) - self.assertTrue(caps[3].is_enabled) - self.assertEqual('statistics:query:complex', caps[4].id) - self.assertFalse(caps[4].is_enabled) diff --git a/openstack/tests/unit/meter/v2/test_meter.py b/openstack/tests/unit/meter/v2/test_meter.py deleted file mode 100644 index dced7f809..000000000 --- a/openstack/tests/unit/meter/v2/test_meter.py +++ /dev/null @@ -1,54 +0,0 @@ -# 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 testtools - -from openstack.meter.v2 import meter - -IDENTIFIER = 'IDENTIFIER' -EXAMPLE = { - 'meter_id': IDENTIFIER, - 'name': 'instance', - 'project_id': '123', - 'resource_id': '456', - 'source': 'abc', - 'type': 'def', - 'unit': 'ghi', - 'user_id': '789' -} - - -class TestMeter(testtools.TestCase): - - def test_basic(self): - sot = meter.Meter() - self.assertEqual('meter', sot.resource_key) - self.assertIsNone(sot.resources_key) - self.assertEqual('/meters', sot.base_path) - self.assertEqual('metering', sot.service.service_type) - self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) - self.assertFalse(sot.allow_delete) - self.assertTrue(sot.allow_list) - - def test_make_it(self): - sot = meter.Meter(**EXAMPLE) - self.assertEqual(EXAMPLE['meter_id'], sot.id) - self.assertEqual(EXAMPLE['meter_id'], sot.meter_id) - self.assertEqual(EXAMPLE['name'], sot.name) - self.assertEqual(EXAMPLE['project_id'], sot.project_id) - self.assertEqual(EXAMPLE['resource_id'], sot.resource_id) - self.assertEqual(EXAMPLE['source'], sot.source) - self.assertEqual(EXAMPLE['type'], sot.type) - self.assertEqual(EXAMPLE['unit'], sot.unit) - self.assertEqual(EXAMPLE['user_id'], sot.user_id) diff --git a/openstack/tests/unit/meter/v2/test_proxy.py b/openstack/tests/unit/meter/v2/test_proxy.py deleted file mode 100644 index 76883e973..000000000 --- a/openstack/tests/unit/meter/v2/test_proxy.py +++ /dev/null @@ -1,66 +0,0 @@ -# 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.meter.v2 import _proxy -from openstack.meter.v2 import capability -from openstack.meter.v2 import meter -from openstack.meter.v2 import resource -from openstack.meter.v2 import sample -from openstack.meter.v2 import statistics -from openstack.tests.unit import test_proxy_base2 - - -class TestMeterProxy(test_proxy_base2.TestProxyBase): - def setUp(self): - super(TestMeterProxy, self).setUp() - self.proxy = _proxy.Proxy(self.session) - - def test_capability_find(self): - self.verify_find(self.proxy.find_capability, capability.Capability) - - def test_capabilities(self): - self.verify_list(self.proxy.capabilities, capability.Capability, - paginated=False) - - def test_meter_find(self): - self.verify_find(self.proxy.find_meter, meter.Meter) - - def test_meters(self): - self.verify_list(self.proxy.meters, meter.Meter, paginated=False) - - def test_resource_find(self): - self.verify_find(self.proxy.find_resource, resource.Resource) - - def test_resource_get(self): - self.verify_get(self.proxy.get_resource, resource.Resource) - - def test_resources(self): - self.verify_list(self.proxy.resources, resource.Resource, - paginated=False) - - def test_sample_find(self): - self.verify_find(self.proxy.find_sample, sample.Sample) - - def test_samples(self): - expected_kwargs = {'counter_name': 'meterone'} - self.verify_list(self.proxy.samples, sample.Sample, - method_args=['meterone'], - paginated=False, expected_kwargs=expected_kwargs) - - def test_statistics_find(self): - self.verify_find(self.proxy.find_statistics, statistics.Statistics) - - def test_statistics(self): - expected_kwargs = {'meter_name': 'meterone'} - self.verify_list(self.proxy.statistics, statistics.Statistics, - method_args=['meterone'], - paginated=False, expected_kwargs=expected_kwargs) diff --git a/openstack/tests/unit/meter/v2/test_resource.py b/openstack/tests/unit/meter/v2/test_resource.py deleted file mode 100644 index 78bb937fa..000000000 --- a/openstack/tests/unit/meter/v2/test_resource.py +++ /dev/null @@ -1,59 +0,0 @@ -# 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 testtools - -from openstack.meter.v2 import resource - -IDENTIFIER = 'IDENTIFIER' -LINKS = [{'href': 'first_uri', 'rel': 'label 1', }, - {'href': 'other_uri', 'rel': 'label', }, ] -EXAMPLE = { - 'resource_id': IDENTIFIER, - 'first_sample_timestamp': '2015-03-09T12:15:57.233772', - 'last_sample_timestamp': '2015-03-09T12:15:57.233772', - 'links': LINKS, - 'metadata': {'name_one': '1', 'name_two': '2', }, - 'project_id': '123', - 'source': 'abc', - 'user_id': '789' -} - - -class TestResource(testtools.TestCase): - - def test_basic(self): - sot = resource.Resource() - self.assertIsNone(sot.resource_key) - self.assertIsNone(sot.resources_key) - self.assertEqual('/resources', sot.base_path) - self.assertEqual('metering', sot.service.service_type) - self.assertFalse(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertFalse(sot.allow_update) - self.assertFalse(sot.allow_delete) - self.assertTrue(sot.allow_list) - - def test_make_it(self): - sot = resource.Resource(**EXAMPLE) - self.assertEqual(EXAMPLE['resource_id'], sot.id) - self.assertEqual(EXAMPLE['resource_id'], sot.resource_id) - self.assertEqual(EXAMPLE['first_sample_timestamp'], - sot.first_sample_at) - self.assertEqual(EXAMPLE['last_sample_timestamp'], - sot.last_sample_at) - self.assertEqual(EXAMPLE['links'], sot.links) - self.assertEqual(EXAMPLE['metadata'], sot.metadata) - self.assertEqual(EXAMPLE['project_id'], sot.project_id) - self.assertEqual(EXAMPLE['resource_id'], sot.resource_id) - self.assertEqual(EXAMPLE['source'], sot.source) - self.assertEqual(EXAMPLE['user_id'], sot.user_id) diff --git a/openstack/tests/unit/meter/v2/test_sample.py b/openstack/tests/unit/meter/v2/test_sample.py deleted file mode 100644 index 8f7faf729..000000000 --- a/openstack/tests/unit/meter/v2/test_sample.py +++ /dev/null @@ -1,84 +0,0 @@ -# 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 testtools - -from openstack.meter.v2 import sample - -SAMPLE = { - 'sample_id': '0', - 'metadata': {'1': 'one'}, - 'counter_name': '2', - 'message_id': '4', - 'project_id': '3', - 'recorded_at': '2015-03-09T12:15:57.233772', - 'resource_id': '5', - 'source': '6', - 'timestamp': '2015-03-09T12:15:57.233772', - 'type': '8', - 'unit': '9', - 'user_id': '10', - 'volume': '11.1', -} - - -class TestSample(testtools.TestCase): - - def test_basic(self): - sot = sample.Sample() - self.assertIsNone(sot.resource_key) - self.assertIsNone(sot.resources_key) - self.assertEqual('/meters/%(counter_name)s', sot.base_path) - self.assertEqual('metering', sot.service.service_type) - self.assertFalse(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertFalse(sot.allow_update) - self.assertFalse(sot.allow_delete) - self.assertTrue(sot.allow_list) - - def test_make_new(self): - sot = sample.Sample(**SAMPLE) - self.assertEqual(SAMPLE['message_id'], sot.id) - self.assertEqual(SAMPLE['metadata'], sot.metadata) - self.assertEqual(SAMPLE['counter_name'], sot.counter_name) - self.assertEqual(SAMPLE['project_id'], sot.project_id) - self.assertEqual(SAMPLE['recorded_at'], sot.recorded_at) - self.assertEqual(SAMPLE['resource_id'], sot.resource_id) - self.assertEqual(SAMPLE['source'], sot.source) - self.assertEqual(SAMPLE['timestamp'], sot.generated_at) - self.assertEqual(SAMPLE['type'], sot.type) - self.assertEqual(SAMPLE['unit'], sot.unit) - self.assertEqual(SAMPLE['user_id'], sot.user_id) - self.assertEqual(SAMPLE['volume'], sot.volume) - - def test_list(self): - sess = mock.Mock() - resp = mock.Mock() - resp.json = mock.Mock(return_value=[SAMPLE]) - resp.status_code = 200 - sess.get = mock.Mock(return_value=resp) - - found = sample.Sample.list(sess, counter_name='name_of_meter') - first = next(found) - self.assertEqual(SAMPLE['message_id'], first.id) - self.assertEqual(SAMPLE['metadata'], first.metadata) - self.assertEqual(SAMPLE['counter_name'], first.counter_name) - self.assertEqual(SAMPLE['project_id'], first.project_id) - self.assertEqual(SAMPLE['recorded_at'], first.recorded_at) - self.assertEqual(SAMPLE['resource_id'], first.resource_id) - self.assertEqual(SAMPLE['source'], first.source) - self.assertEqual(SAMPLE['timestamp'], first.generated_at) - self.assertEqual(SAMPLE['type'], first.type) - self.assertEqual(SAMPLE['unit'], first.unit) - self.assertEqual(SAMPLE['user_id'], first.user_id) - self.assertEqual(SAMPLE['volume'], first.volume) diff --git a/openstack/tests/unit/meter/v2/test_statistics.py b/openstack/tests/unit/meter/v2/test_statistics.py deleted file mode 100644 index 8fd46f573..000000000 --- a/openstack/tests/unit/meter/v2/test_statistics.py +++ /dev/null @@ -1,93 +0,0 @@ -# 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 testtools - -from openstack.meter.v2 import statistics - -EXAMPLE = { - 'aggregate': '1', - 'avg': '2', - 'count': '3', - 'duration': '4', - 'duration_end': '2015-03-09T12:45:00.000000', - 'duration_start': '2015-03-09T12:15:00.000000', - 'groupby': '7', - 'max': '8', - 'min': '9', - 'period': '10', - 'period_end': '2015-03-09T12:45:00.000000', - 'period_start': '2015-03-09T12:15:00.000000', - 'sum': '13', - 'unit': '14', -} - - -class TestStatistics(testtools.TestCase): - - def test_basic(self): - sot = statistics.Statistics() - self.assertEqual('statistics', sot.resource_key) - self.assertIsNone(sot.resources_key) - self.assertEqual('/meters/%(meter_name)s/statistics', sot.base_path) - self.assertEqual('metering', sot.service.service_type) - self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) - self.assertFalse(sot.allow_delete) - self.assertTrue(sot.allow_list) - - def test_make_it(self): - sot = statistics.Statistics(**EXAMPLE) - self.assertEqual(EXAMPLE['unit'], sot.id) - self.assertEqual(EXAMPLE['aggregate'], sot.aggregate) - self.assertEqual(EXAMPLE['avg'], sot.avg) - self.assertEqual(EXAMPLE['count'], sot.count) - self.assertEqual(EXAMPLE['duration'], sot.duration) - self.assertEqual(EXAMPLE['duration_end'], sot.duration_end_at) - self.assertEqual(EXAMPLE['duration_start'], sot.duration_start_at) - self.assertEqual(EXAMPLE['groupby'], sot.group_by) - self.assertEqual(EXAMPLE['max'], sot.max) - self.assertEqual(EXAMPLE['min'], sot.min) - self.assertEqual(EXAMPLE['period'], sot.period) - self.assertEqual(EXAMPLE['period_end'], sot.period_end_at) - self.assertEqual(EXAMPLE['period_start'], sot.period_start_at) - self.assertEqual(EXAMPLE['sum'], sot.sum) - self.assertEqual(EXAMPLE['unit'], sot.unit) - - def test_list(self): - sess = mock.Mock() - resp = mock.Mock() - resp.json = mock.Mock(return_value=[EXAMPLE]) - sess.get = mock.Mock(return_value=resp) - reply = statistics.Statistics.list(sess, meter_name='example') - - url = '/meters/example/statistics' - stat = next(reply) - sess.get.assert_called_with(url, - params={}) - self.assertEqual(EXAMPLE['aggregate'], stat.aggregate) - self.assertEqual(EXAMPLE['avg'], stat.avg) - self.assertEqual(EXAMPLE['count'], stat.count) - self.assertEqual(EXAMPLE['duration'], stat.duration) - self.assertEqual(EXAMPLE['duration_end'], stat.duration_end_at) - self.assertEqual(EXAMPLE['duration_start'], stat.duration_start_at) - self.assertEqual(EXAMPLE['groupby'], stat.group_by) - self.assertEqual(EXAMPLE['max'], stat.max) - self.assertEqual(EXAMPLE['min'], stat.min) - self.assertEqual(EXAMPLE['period'], stat.period) - self.assertEqual(EXAMPLE['period_end'], stat.period_end_at) - self.assertEqual(EXAMPLE['period_start'], stat.period_start_at) - self.assertEqual(EXAMPLE['sum'], stat.sum) - self.assertEqual(EXAMPLE['unit'], stat.unit) - self.assertRaises(StopIteration, next, reply) diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index cfbb698f9..11887976b 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -102,8 +102,6 @@ def test_create_session(self): conn.load_balancer.__class__.__module__) self.assertEqual('openstack.orchestration.v1._proxy', conn.orchestration.__class__.__module__) - self.assertEqual('openstack.meter.v2._proxy', - conn.meter.__class__.__module__) self.assertEqual('openstack.workflow.v2._proxy', conn.workflow.__class__.__module__) diff --git a/releasenotes/notes/removed-meter-6f6651b6e452e000.yaml b/releasenotes/notes/removed-meter-6f6651b6e452e000.yaml new file mode 100644 index 000000000..c4c5a1e45 --- /dev/null +++ b/releasenotes/notes/removed-meter-6f6651b6e452e000.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + Meter and Alarm services have been removed. The Ceilometer REST API has + been deprecated for quite some time and is no longer supported. From cb4c425411aebafe590cbed3f7d626dfb4a254ef Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 10 Jan 2018 17:51:21 -0600 Subject: [PATCH 1905/3836] Remove metric service There is discussion on the mailing list about ceilometerclient removal that started this. The metric service is not an OpenStack service. It does not have an entry in service-types-authority, nor does it have an API documented at developer.openstack.org/api-ref. That's not saying that someone couldn't install it in their OpenStack cloud and that doing so is a bad idea. It's probably a great idea and those people are likely quite happy. Since this is openstacksdk we need to draw the line somewhere. The add_service patch adds the ability for consumers to opt-in to non-official services. Making an openstacksdk-metric package people could use with a MetricService object and the proxy/resource objects here would be a great idea. As a follow-up we should potentially add the ability to add a list of extra services to a clouds.yaml file. Change-Id: Ib3810e3b11dbacc0215d486397e05370a2d7521e --- .zuul.yaml | 9 --- doc/source/contributor/local.conf | 9 --- doc/source/enforcer.py | 1 - doc/source/user/index.rst | 2 - doc/source/user/proxies/metric.rst | 18 ----- doc/source/user/resources/metric/index.rst | 10 --- .../resources/metric/v1/archive_policy.rst | 13 ---- .../user/resources/metric/v1/capabilities.rst | 12 --- .../user/resources/metric/v1/metric.rst | 12 --- .../user/resources/metric/v1/resource.rst | 12 --- openstack/connection.py | 5 -- openstack/metric/__init__.py | 0 openstack/metric/metric_service.py | 24 ------ openstack/metric/v1/__init__.py | 0 openstack/metric/v1/_proxy.py | 28 ------- openstack/metric/v1/archive_policy.py | 35 --------- openstack/metric/v1/capabilities.py | 25 ------ openstack/metric/v1/metric.py | 39 ---------- openstack/metric/v1/resource.py | 46 ----------- openstack/tests/unit/metric/__init__.py | 0 .../tests/unit/metric/test_metric_service.py | 28 ------- openstack/tests/unit/metric/v1/__init__.py | 0 .../unit/metric/v1/test_archive_policy.py | 61 --------------- .../tests/unit/metric/v1/test_capabilities.py | 36 --------- openstack/tests/unit/metric/v1/test_metric.py | 78 ------------------- openstack/tests/unit/metric/v1/test_proxy.py | 25 ------ .../tests/unit/metric/v1/test_resource.py | 56 ------------- .../notes/remove-metric-fe5ddfd52b43c852.yaml | 5 ++ 28 files changed, 5 insertions(+), 584 deletions(-) delete mode 100644 doc/source/user/proxies/metric.rst delete mode 100644 doc/source/user/resources/metric/index.rst delete mode 100644 doc/source/user/resources/metric/v1/archive_policy.rst delete mode 100644 doc/source/user/resources/metric/v1/capabilities.rst delete mode 100644 doc/source/user/resources/metric/v1/metric.rst delete mode 100644 doc/source/user/resources/metric/v1/resource.rst delete mode 100644 openstack/metric/__init__.py delete mode 100644 openstack/metric/metric_service.py delete mode 100644 openstack/metric/v1/__init__.py delete mode 100644 openstack/metric/v1/_proxy.py delete mode 100644 openstack/metric/v1/archive_policy.py delete mode 100644 openstack/metric/v1/capabilities.py delete mode 100644 openstack/metric/v1/metric.py delete mode 100644 openstack/metric/v1/resource.py delete mode 100644 openstack/tests/unit/metric/__init__.py delete mode 100644 openstack/tests/unit/metric/test_metric_service.py delete mode 100644 openstack/tests/unit/metric/v1/__init__.py delete mode 100644 openstack/tests/unit/metric/v1/test_archive_policy.py delete mode 100644 openstack/tests/unit/metric/v1/test_capabilities.py delete mode 100644 openstack/tests/unit/metric/v1/test_metric.py delete mode 100644 openstack/tests/unit/metric/v1/test_proxy.py delete mode 100644 openstack/tests/unit/metric/v1/test_resource.py create mode 100644 releasenotes/notes/remove-metric-fe5ddfd52b43c852.yaml diff --git a/.zuul.yaml b/.zuul.yaml index 92983a485..a49691242 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -1,5 +1,3 @@ -# TODO(shade) Add job that enables ceilometer - - job: name: openstacksdk-tox-py27-tips parent: openstack-tox-py27 @@ -72,13 +70,6 @@ DEFAULT: osapi_max_limit: 6 devstack_services: - ceilometer-acentral: false - ceilometer-acompute: false - ceilometer-alarm-evaluator: false - ceilometer-alarm-notifier: false - ceilometer-anotification: false - ceilometer-api: false - ceilometer-collector: false horizon: false s-account: true s-container: true diff --git a/doc/source/contributor/local.conf b/doc/source/contributor/local.conf index 79106deac..2ce95caae 100644 --- a/doc/source/contributor/local.conf +++ b/doc/source/contributor/local.conf @@ -36,15 +36,6 @@ enable_service q-l3 enable_service q-meta enable_service q-metering -# Enable Ceilometer -enable_service ceilometer-acompute -enable_service ceilometer-acentral -enable_service ceilometer-anotification -enable_service ceilometer-collector -enable_service ceilometer-alarm-evaluator -enable_service ceilometer-alarm-notifier -enable_service ceilometer-api - # Enable Zaqar enable_plugin zaqar https://github.com/openstack/zaqar enable_service zaqar-server diff --git a/doc/source/enforcer.py b/doc/source/enforcer.py index d373ccd32..bfbf47c34 100644 --- a/doc/source/enforcer.py +++ b/doc/source/enforcer.py @@ -39,7 +39,6 @@ def get_proxy_methods(): "openstack.load_balancer.v2._proxy", "openstack.message.v1._proxy", "openstack.message.v2._proxy", - "openstack.metric.v1._proxy", "openstack.network.v2._proxy", "openstack.object_store.v1._proxy", "openstack.orchestration.v1._proxy", diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index 719fd6162..93f979e4e 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -94,7 +94,6 @@ provided by the SDK. Message v1 Message v2 Network - Metric Object Store Orchestration Workflow @@ -123,7 +122,6 @@ The following services have exposed *Resource* classes. Image Key Management Load Balancer - Metric Network Orchestration Object Store diff --git a/doc/source/user/proxies/metric.rst b/doc/source/user/proxies/metric.rst deleted file mode 100644 index 3abd98f9d..000000000 --- a/doc/source/user/proxies/metric.rst +++ /dev/null @@ -1,18 +0,0 @@ -Metric API -========== - -.. automodule:: openstack.metric.v1._proxy - -The Metric Class ----------------- - -The metric high-level interface is available through the ``metric`` -member of a :class:`~openstack.connection.Connection` object. The -``metric`` member will only be added if the service is detected. - -Capability Operations -^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.metric.v1._proxy.Proxy - - .. automethod:: openstack.metric.v1._proxy.Proxy.capabilities diff --git a/doc/source/user/resources/metric/index.rst b/doc/source/user/resources/metric/index.rst deleted file mode 100644 index 1bfa667db..000000000 --- a/doc/source/user/resources/metric/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -Metric Resources -================ - -.. toctree:: - :maxdepth: 1 - - v1/archive_policy - v1/capabilities - v1/metric - v1/resource diff --git a/doc/source/user/resources/metric/v1/archive_policy.rst b/doc/source/user/resources/metric/v1/archive_policy.rst deleted file mode 100644 index 7358e795e..000000000 --- a/doc/source/user/resources/metric/v1/archive_policy.rst +++ /dev/null @@ -1,13 +0,0 @@ -openstack.metric.v1.archive_policy -================================== - -.. automodule:: openstack.metric.v1.archive_policy - -The ArchivePolicy Class ------------------------ - -The ``ArchivePolicy`` class inherits from -:class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.metric.v1.archive_policy.ArchivePolicy - :members: diff --git a/doc/source/user/resources/metric/v1/capabilities.rst b/doc/source/user/resources/metric/v1/capabilities.rst deleted file mode 100644 index 571d460fe..000000000 --- a/doc/source/user/resources/metric/v1/capabilities.rst +++ /dev/null @@ -1,12 +0,0 @@ -openstack.metric.v1.capabilities -================================ - -.. automodule:: openstack.metric.v1.capabilities - -The Capabilities Class ----------------------- - -The ``Capabilities`` class inherits from :class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.metric.v1.capabilities.Capabilities - :members: diff --git a/doc/source/user/resources/metric/v1/metric.rst b/doc/source/user/resources/metric/v1/metric.rst deleted file mode 100644 index 4c4feb5bb..000000000 --- a/doc/source/user/resources/metric/v1/metric.rst +++ /dev/null @@ -1,12 +0,0 @@ -openstack.metric.v1.metric -========================== - -.. automodule:: openstack.metric.v1.metric - -The Metric Class ----------------- - -The ``Metric`` class inherits from :class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.metric.v1.metric.Metric - :members: diff --git a/doc/source/user/resources/metric/v1/resource.rst b/doc/source/user/resources/metric/v1/resource.rst deleted file mode 100644 index 748fb47cf..000000000 --- a/doc/source/user/resources/metric/v1/resource.rst +++ /dev/null @@ -1,12 +0,0 @@ -openstack.metric.v1.resource -============================ - -.. automodule:: openstack.metric.v1.resource - -The Generic Class ------------------ - -The ``Generic`` class inherits from :class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.metric.v1.resource.Generic - :members: diff --git a/openstack/connection.py b/openstack/connection.py index 300972894..37bb948a6 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -204,11 +204,6 @@ def __init__(self, cloud=None, config=None, session=None, self.add_service( service_description.OpenStackServiceDescription( service, self.config)) - # TODO(mordred) openstacksdk has support for the metric service - # which is not in service-types-authority. What do we do about that? - self.add_service( - service_description.OpenStackServiceDescription( - dict(service_type='metric'), self.config)) def _get_config_from_profile(self, profile, authenticator, **kwargs): """Get openstack.config objects from legacy profile.""" diff --git a/openstack/metric/__init__.py b/openstack/metric/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/openstack/metric/metric_service.py b/openstack/metric/metric_service.py deleted file mode 100644 index b18153fb5..000000000 --- a/openstack/metric/metric_service.py +++ /dev/null @@ -1,24 +0,0 @@ -# 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 service_filter - - -class MetricService(service_filter.ServiceFilter): - """The metric service.""" - - valid_versions = [service_filter.ValidVersion('v1')] - - def __init__(self, version=None): - """Create a metric service.""" - super(MetricService, self).__init__(service_type='metric', - version=version) diff --git a/openstack/metric/v1/__init__.py b/openstack/metric/v1/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/openstack/metric/v1/_proxy.py b/openstack/metric/v1/_proxy.py deleted file mode 100644 index 31958e943..000000000 --- a/openstack/metric/v1/_proxy.py +++ /dev/null @@ -1,28 +0,0 @@ -# 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.metric.v1 import capabilities -from openstack import proxy2 as proxy - - -class Proxy(proxy.BaseProxy): - - def capabilities(self, **query): - """Return a generator of capabilities - - :param kwargs \*\*query: Optional query parameters to be sent to limit - the resources being returned. - - :returns: A generator of capability objects - :rtype: :class:`~openstack.metric.v1.capabilities.Capabilities` - """ - return self._list(capabilities.Capabilities, paginated=False, **query) diff --git a/openstack/metric/v1/archive_policy.py b/openstack/metric/v1/archive_policy.py deleted file mode 100644 index 1c0d844ca..000000000 --- a/openstack/metric/v1/archive_policy.py +++ /dev/null @@ -1,35 +0,0 @@ -# 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.metric import metric_service -from openstack import resource2 as resource - - -class ArchivePolicy(resource.Resource): - base_path = '/archive_policy' - service = metric_service.MetricService() - - # Supported Operations - allow_create = True - allow_get = True - allow_delete = True - allow_list = True - - # Properties - #: The name of this policy - name = resource.Body('name', alternate_id=True) - #: The definition of this policy - definition = resource.Body('definition', type=list) - #: The window of time older than the period that archives can be requested - back_window = resource.Body('back_window') - #: A list of the aggregation methods supported - aggregation_methods = resource.Body("aggregation_methods", type=list) diff --git a/openstack/metric/v1/capabilities.py b/openstack/metric/v1/capabilities.py deleted file mode 100644 index 906574758..000000000 --- a/openstack/metric/v1/capabilities.py +++ /dev/null @@ -1,25 +0,0 @@ -# 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.metric import metric_service -from openstack import resource2 as resource - - -class Capabilities(resource.Resource): - base_path = '/capabilities' - service = metric_service.MetricService() - - # Supported Operations - allow_get = True - - #: The supported methods of aggregation. - aggregation_methods = resource.Body('aggregation_methods', type=list) diff --git a/openstack/metric/v1/metric.py b/openstack/metric/v1/metric.py deleted file mode 100644 index 3e71b2c0d..000000000 --- a/openstack/metric/v1/metric.py +++ /dev/null @@ -1,39 +0,0 @@ -# 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.metric import metric_service -from openstack import resource2 as resource - - -class Metric(resource.Resource): - base_path = '/metric' - service = metric_service.MetricService() - - # Supported Operations - allow_create = True - allow_get = True - allow_delete = True - allow_list = True - - # Properties - #: The name of the archive policy - archive_policy_name = resource.Body('archive_policy_name') - #: The archive policy - archive_policy = resource.Body('archive_policy') - #: The ID of the user who created this metric - created_by_user_id = resource.Body('created_by_user_id') - #: The ID of the project this metric was created under - created_by_project_id = resource.Body('created_by_project_id') - #: The identifier of this metric - resource_id = resource.Body('resource_id') - #: The name of this metric - name = resource.Body('name') diff --git a/openstack/metric/v1/resource.py b/openstack/metric/v1/resource.py deleted file mode 100644 index f66d5661b..000000000 --- a/openstack/metric/v1/resource.py +++ /dev/null @@ -1,46 +0,0 @@ -# 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.metric import metric_service -from openstack import resource2 as resource - - -class Generic(resource.Resource): - base_path = '/resource/generic' - service = metric_service.MetricService() - - # Supported Operations - allow_create = True - allow_get = True - allow_delete = True - allow_list = True - allow_update = True - - # Properties - #: The identifier of this resource - id = resource.Body('id') - #: The ID of the user who created this resource - created_by_user_id = resource.Body('created_by_user_id') - #: The ID of the project this resource was created under - created_by_project_id = resource.Body('created_by_project_id') - #: The ID of the user - user_id = resource.Body('user_id') - #: The ID of the project - project_id = resource.Body('project_id') - #: Timestamp when this resource was started - started_at = resource.Body('started_at') - #: Timestamp when this resource was ended - ended_at = resource.Body('ended_at') - #: A dictionary of metrics collected on this resource - metrics = resource.Body('metrics', type=dict) - #: The type of resource - type = resource.Body('type') diff --git a/openstack/tests/unit/metric/__init__.py b/openstack/tests/unit/metric/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/openstack/tests/unit/metric/test_metric_service.py b/openstack/tests/unit/metric/test_metric_service.py deleted file mode 100644 index acb846b3a..000000000 --- a/openstack/tests/unit/metric/test_metric_service.py +++ /dev/null @@ -1,28 +0,0 @@ -# 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 testtools - -from openstack.metric import metric_service - - -class TestMetricService(testtools.TestCase): - - def test_service(self): - sot = metric_service.MetricService() - self.assertEqual('metric', sot.service_type) - self.assertEqual('public', sot.interface) - self.assertIsNone(sot.region) - self.assertIsNone(sot.service_name) - self.assertEqual(1, len(sot.valid_versions)) - self.assertEqual('v1', sot.valid_versions[0].module) - self.assertEqual('v1', sot.valid_versions[0].path) diff --git a/openstack/tests/unit/metric/v1/__init__.py b/openstack/tests/unit/metric/v1/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/openstack/tests/unit/metric/v1/test_archive_policy.py b/openstack/tests/unit/metric/v1/test_archive_policy.py deleted file mode 100644 index e8c08c968..000000000 --- a/openstack/tests/unit/metric/v1/test_archive_policy.py +++ /dev/null @@ -1,61 +0,0 @@ -# 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 testtools - -from openstack.metric.v1 import archive_policy - -EXAMPLE = { - 'definition': - [ - {u'points': 12, u'timespan': u'1:00:00', - u'granularity': u'0:05:00'}, - {u'points': 24, u'timespan': u'1 day, 0:00:00', - u'granularity': u'1:00:00'}, - {u'points': 30, u'timespan': u'30 days, 0:00:00', - u'granularity': u'1 day, 0:00:00'}, - ], - u'back_window': 0, - u'name': u'low', - u'aggregation_methods': [u'sum', u'max'] -} - - -class TestArchivePolicy(testtools.TestCase): - - def setUp(self): - super(TestArchivePolicy, self).setUp() - self.resp = mock.Mock() - self.resp.body = '' - self.sess = mock.Mock() - self.sess.put = mock.Mock(return_value=self.resp) - - def test_basic(self): - m = archive_policy.ArchivePolicy() - self.assertIsNone(m.resource_key) - self.assertIsNone(m.resources_key) - self.assertEqual('/archive_policy', m.base_path) - self.assertEqual('metric', m.service.service_type) - self.assertTrue(m.allow_create) - self.assertTrue(m.allow_get) - self.assertFalse(m.allow_update) - self.assertTrue(m.allow_delete) - self.assertTrue(m.allow_list) - - def test_make_it(self): - m = archive_policy.ArchivePolicy(**EXAMPLE) - self.assertEqual(EXAMPLE['name'], m.name) - self.assertEqual(EXAMPLE['name'], m.id) - self.assertEqual(EXAMPLE['definition'], m.definition) - self.assertEqual(EXAMPLE['back_window'], m.back_window) - self.assertEqual(EXAMPLE['aggregation_methods'], m.aggregation_methods) diff --git a/openstack/tests/unit/metric/v1/test_capabilities.py b/openstack/tests/unit/metric/v1/test_capabilities.py deleted file mode 100644 index 72ee1cbe6..000000000 --- a/openstack/tests/unit/metric/v1/test_capabilities.py +++ /dev/null @@ -1,36 +0,0 @@ -# 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 testtools - -from openstack.metric.v1 import capabilities - -BODY = { - 'aggregation_methods': ['mean', 'max', 'avg'], -} - - -class TestCapabilites(testtools.TestCase): - def test_basic(self): - sot = capabilities.Capabilities() - self.assertEqual('/capabilities', sot.base_path) - self.assertEqual('metric', sot.service.service_type) - self.assertFalse(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertFalse(sot.allow_update) - self.assertFalse(sot.allow_delete) - self.assertFalse(sot.allow_list) - - def test_make_it(self): - sot = capabilities.Capabilities(**BODY) - self.assertEqual(BODY['aggregation_methods'], - sot.aggregation_methods) diff --git a/openstack/tests/unit/metric/v1/test_metric.py b/openstack/tests/unit/metric/v1/test_metric.py deleted file mode 100644 index 86512982a..000000000 --- a/openstack/tests/unit/metric/v1/test_metric.py +++ /dev/null @@ -1,78 +0,0 @@ -# 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 testtools - -from openstack.metric.v1 import metric - -EXAMPLE = { - 'id': '31bbd62e-b144-11e4-983c-bf9dbe7e25e6', - 'archive_policy_name': 'low', - 'created_by_user_id': '41bbd62e-b144-11e4-983c-bf9dbe7e25e6', - 'created_by_project_id': '51bbd62e-b144-11e4-983c-bf9dbe7e25e6', - 'resource_id': None, - 'name': None, -} - -EXAMPLE_AP = { - 'id': '31bbd62e-b144-11e4-983c-bf9dbe7e25e6', - 'archive_policy': { - 'name': "foobar", - }, - 'created_by_user_id': '41bbd62e-b144-11e4-983c-bf9dbe7e25e6', - 'created_by_project_id': '51bbd62e-b144-11e4-983c-bf9dbe7e25e6', - 'resource_id': "61bbd62e-b144-11e4-983c-bf9dbe7e25e6", - 'name': "foobaz", -} - - -class TestMetric(testtools.TestCase): - - def setUp(self): - super(TestMetric, self).setUp() - self.resp = mock.Mock() - self.resp.body = '' - self.sess = mock.Mock() - self.sess.put = mock.Mock(return_value=self.resp) - - def test_basic(self): - m = metric.Metric() - self.assertIsNone(m.resource_key) - self.assertIsNone(m.resources_key) - self.assertEqual('/metric', m.base_path) - self.assertEqual('metric', m.service.service_type) - self.assertTrue(m.allow_create) - self.assertTrue(m.allow_get) - self.assertFalse(m.allow_update) - self.assertTrue(m.allow_delete) - self.assertTrue(m.allow_list) - - def test_make_it(self): - m = metric.Metric(**EXAMPLE) - self.assertEqual(EXAMPLE['id'], m.id) - self.assertEqual(EXAMPLE['archive_policy_name'], m.archive_policy_name) - self.assertEqual(EXAMPLE['created_by_user_id'], m.created_by_user_id) - self.assertEqual(EXAMPLE['created_by_project_id'], - m.created_by_project_id) - self.assertEqual(EXAMPLE['resource_id'], m.resource_id) - self.assertEqual(EXAMPLE['name'], m.name) - - m = metric.Metric(**EXAMPLE_AP) - self.assertEqual(EXAMPLE_AP['id'], m.id) - self.assertEqual(EXAMPLE_AP['archive_policy'], m.archive_policy) - self.assertEqual(EXAMPLE_AP['created_by_user_id'], - m.created_by_user_id) - self.assertEqual(EXAMPLE_AP['created_by_project_id'], - m.created_by_project_id) - self.assertEqual(EXAMPLE_AP['resource_id'], m.resource_id) - self.assertEqual(EXAMPLE_AP['name'], m.name) diff --git a/openstack/tests/unit/metric/v1/test_proxy.py b/openstack/tests/unit/metric/v1/test_proxy.py deleted file mode 100644 index c3abc4ace..000000000 --- a/openstack/tests/unit/metric/v1/test_proxy.py +++ /dev/null @@ -1,25 +0,0 @@ -# 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.metric.v1 import _proxy -from openstack.metric.v1 import capabilities -from openstack.tests.unit import test_proxy_base2 as test_proxy_base - - -class TestMetricProxy(test_proxy_base.TestProxyBase): - def setUp(self): - super(TestMetricProxy, self).setUp() - self.proxy = _proxy.Proxy(self.session) - - def test_capabilities(self): - self.verify_list(self.proxy.capabilities, capabilities.Capabilities, - paginated=False) diff --git a/openstack/tests/unit/metric/v1/test_resource.py b/openstack/tests/unit/metric/v1/test_resource.py deleted file mode 100644 index 80dad8c52..000000000 --- a/openstack/tests/unit/metric/v1/test_resource.py +++ /dev/null @@ -1,56 +0,0 @@ -# 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 testtools - -from openstack.metric.v1 import resource - - -EXAMPLE_GENERIC = { - "created_by_user_id": "5521eab6-a3bc-4841-b253-d62871b65e76", - "started_at": "2015-03-09T12:14:57.233772", - "user_id": None, - "created_by_project_id": "41649c3e-5f7a-41d1-81fb-2efa76c09e6c", - "metrics": {}, - "ended_at": None, - "project_id": None, - "type": "generic", - "id": "a8d5e83b-0320-45ce-8282-7c8ad8fb8bf6", -} - - -class TestResource(testtools.TestCase): - def test_generic(self): - m = resource.Generic() - self.assertIsNone(m.resource_key) - self.assertIsNone(m.resources_key) - self.assertEqual('/resource/generic', m.base_path) - self.assertEqual('metric', m.service.service_type) - self.assertTrue(m.allow_create) - self.assertTrue(m.allow_get) - self.assertTrue(m.allow_update) - self.assertTrue(m.allow_delete) - self.assertTrue(m.allow_list) - - def test_make_generic(self): - r = resource.Generic(**EXAMPLE_GENERIC) - self.assertEqual(EXAMPLE_GENERIC['created_by_user_id'], - r.created_by_user_id) - self.assertEqual(EXAMPLE_GENERIC['created_by_project_id'], - r.created_by_project_id) - self.assertEqual(EXAMPLE_GENERIC['user_id'], r.user_id) - self.assertEqual(EXAMPLE_GENERIC['project_id'], r.project_id) - self.assertEqual(EXAMPLE_GENERIC['type'], r.type) - self.assertEqual(EXAMPLE_GENERIC['id'], r.id) - self.assertEqual(EXAMPLE_GENERIC['metrics'], r.metrics) - self.assertEqual(EXAMPLE_GENERIC['started_at'], r.started_at) - self.assertEqual(EXAMPLE_GENERIC['ended_at'], r.ended_at) diff --git a/releasenotes/notes/remove-metric-fe5ddfd52b43c852.yaml b/releasenotes/notes/remove-metric-fe5ddfd52b43c852.yaml new file mode 100644 index 000000000..971c4e296 --- /dev/null +++ b/releasenotes/notes/remove-metric-fe5ddfd52b43c852.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + Removed the metric service. It is not an OpenStack service and does not + have an entry in service-types-authority. From 536f347a6cefb6309063fb0f2fa4524f03dbf744 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 12 Jan 2018 12:52:37 -0600 Subject: [PATCH 1906/3836] Remove message v1 support This has been marked for deprecation for 9 months and the v1 message REST api has been deprecated since 2014. This also updates the message version resource to Resource2. Change-Id: I623297060fda2c7f44c08f5842d308efb62d4247 --- doc/source/enforcer.py | 1 - doc/source/user/index.rst | 1 - doc/source/user/proxies/message_v1.rst | 30 ----- openstack/message/message_service.py | 3 +- openstack/message/v1/__init__.py | 0 openstack/message/v1/_proxy.py | 93 --------------- openstack/message/v1/claim.py | 87 -------------- openstack/message/v1/message.py | 107 ------------------ openstack/message/v1/queue.py | 34 ------ openstack/message/version.py | 6 +- .../unit/message/test_message_service.py | 8 +- openstack/tests/unit/message/test_version.py | 4 +- openstack/tests/unit/message/v1/__init__.py | 0 openstack/tests/unit/message/v1/test_claim.py | 97 ---------------- .../tests/unit/message/v1/test_message.py | 85 -------------- openstack/tests/unit/message/v1/test_proxy.py | 55 --------- openstack/tests/unit/message/v1/test_queue.py | 55 --------- 17 files changed, 9 insertions(+), 657 deletions(-) delete mode 100644 doc/source/user/proxies/message_v1.rst delete mode 100644 openstack/message/v1/__init__.py delete mode 100644 openstack/message/v1/_proxy.py delete mode 100644 openstack/message/v1/claim.py delete mode 100644 openstack/message/v1/message.py delete mode 100644 openstack/message/v1/queue.py delete mode 100644 openstack/tests/unit/message/v1/__init__.py delete mode 100644 openstack/tests/unit/message/v1/test_claim.py delete mode 100644 openstack/tests/unit/message/v1/test_message.py delete mode 100644 openstack/tests/unit/message/v1/test_proxy.py delete mode 100644 openstack/tests/unit/message/v1/test_queue.py diff --git a/doc/source/enforcer.py b/doc/source/enforcer.py index bfbf47c34..b8dc182b7 100644 --- a/doc/source/enforcer.py +++ b/doc/source/enforcer.py @@ -37,7 +37,6 @@ def get_proxy_methods(): "openstack.image.v2._proxy", "openstack.key_manager.v1._proxy", "openstack.load_balancer.v2._proxy", - "openstack.message.v1._proxy", "openstack.message.v2._proxy", "openstack.network.v2._proxy", "openstack.object_store.v1._proxy", diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index 93f979e4e..fac178079 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -91,7 +91,6 @@ provided by the SDK. Image v2 Key Manager Load Balancer - Message v1 Message v2 Network Object Store diff --git a/doc/source/user/proxies/message_v1.rst b/doc/source/user/proxies/message_v1.rst deleted file mode 100644 index 42ed3ff7b..000000000 --- a/doc/source/user/proxies/message_v1.rst +++ /dev/null @@ -1,30 +0,0 @@ -Message API v1 -============== - -For details on how to use message, see :doc:`/user/guides/message` - -.. automodule:: openstack.message.v1._proxy - -The Message v1 Class --------------------- - -The message high-level interface is available through the ``message`` member -of a :class:`~openstack.connection.Connection` object. The ``message`` -member will only be added if the service is detected. - -Message Operations -^^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.message.v1._proxy.Proxy - - .. automethod:: openstack.message.v1._proxy.Proxy.claim_messages - .. automethod:: openstack.message.v1._proxy.Proxy.create_messages - .. automethod:: openstack.message.v1._proxy.Proxy.delete_message - -Queue Operations -^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.message.v1._proxy.Proxy - - .. automethod:: openstack.message.v1._proxy.Proxy.create_queue - .. automethod:: openstack.message.v1._proxy.Proxy.delete_queue diff --git a/openstack/message/message_service.py b/openstack/message/message_service.py index e74bf80bb..c9f57ad60 100644 --- a/openstack/message/message_service.py +++ b/openstack/message/message_service.py @@ -16,8 +16,7 @@ class MessageService(service_filter.ServiceFilter): """The message service.""" - valid_versions = [service_filter.ValidVersion('v1'), - service_filter.ValidVersion('v2')] + valid_versions = [service_filter.ValidVersion('v2')] def __init__(self, version=None): """Create a message service.""" diff --git a/openstack/message/v1/__init__.py b/openstack/message/v1/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/openstack/message/v1/_proxy.py b/openstack/message/v1/_proxy.py deleted file mode 100644 index 86a3af367..000000000 --- a/openstack/message/v1/_proxy.py +++ /dev/null @@ -1,93 +0,0 @@ -# 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.message.v1 import claim -from openstack.message.v1 import message -from openstack.message.v1 import queue -from openstack import proxy -from openstack import utils - - -class Proxy(proxy.BaseProxy): - - @utils.deprecated(deprecated_in="0.9.16", removed_in="0.9.17", - details="Message v1 is deprecated since 2014. Use v2.") - def create_queue(self, **attrs): - """Create a new queue from attributes - - :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.message.v1.queue.Queue`, - comprised of the properties on the Queue class. - - :returns: The results of queue creation - :rtype: :class:`~openstack.message.v1.queue.Queue` - """ - return self._create(queue.Queue, **attrs) - - @utils.deprecated(deprecated_in="0.9.16", removed_in="0.9.17", - details="Message v1 is deprecated since 2014. Use v2.") - def delete_queue(self, value, ignore_missing=True): - """Delete a queue - - :param value: The value can be either the name of a queue or a - :class:`~openstack.message.v1.queue.Queue` instance. - :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the queue does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent queue. - - :returns: ``None`` - """ - return self._delete(queue.Queue, value, ignore_missing=ignore_missing) - - @utils.deprecated(deprecated_in="0.9.16", removed_in="0.9.17", - details="Message v1 is deprecated since 2014. Use v2.") - def create_messages(self, values): - """Create new messages - - :param values: The list of - :class:`~openstack.message.v1.message.Message` objects - to create. - :type values: :py:class:`list` - - :returns: The list of - :class:`~openstack.message.v1.message.Message` objects - that were created. - """ - return message.Message.create_messages(self, values) - - @utils.deprecated(deprecated_in="0.9.16", removed_in="0.9.17", - details="Message v1 is deprecated since 2014. Use v2.") - def claim_messages(self, value): - """Claims a set of messages. - - :param value: The value must be a - :class:`~openstack.message.v1.claim.Claim` instance. - - :returns: The list of - :class:`~openstack.message.v1.message.Message` objects - that were claimed. - """ - return claim.Claim.claim_messages(self, value) - - @utils.deprecated(deprecated_in="0.9.16", removed_in="0.9.17", - details="Message v1 is deprecated since 2014. Use v2.") - def delete_message(self, value): - """Delete a message - - :param value: The value must be a - :class:`~openstack.message.v1.message.Message` instance. - - :returns: ``None`` - """ - message.Message.delete_by_id(self, value) diff --git a/openstack/message/v1/claim.py b/openstack/message/v1/claim.py deleted file mode 100644 index d993a443f..000000000 --- a/openstack/message/v1/claim.py +++ /dev/null @@ -1,87 +0,0 @@ -# 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 json - -from openstack import exceptions -from openstack.message import message_service -from openstack.message.v1 import message -from openstack import resource - - -class Claim(resource.Resource): - resources_key = 'claims' - base_path = "/queues/%(queue_name)s/claims" - service = message_service.MessageService() - - # capabilities - allow_create = True - allow_list = False - allow_retrieve = False - allow_delete = False - - #: A ID for each client instance. The ID must be submitted in its - #: canonical form (for example, 3381af92-2b9e-11e3-b191-71861300734c). - #: The client generates this ID once. The client ID persists between - #: restarts of the client so the client should reuse that same ID. - #: All message-related operations require the use of the client ID in - #: the headers to ensure that messages are not echoed back to the client - #: that posted them, unless the client explicitly requests this. - client_id = None - - #: The name of the queue this Claim belongs to. - queue_name = None - - #: Specifies the number of Messages to return. - limit = None - - #: Specifies how long the server waits before releasing the claim, - #: in seconds. - ttl = resource.prop("ttl") - - #: Specifies the message grace period, in seconds. - grace = resource.prop("grace") - - @classmethod - def claim_messages(cls, session, claim): - """Create a remote resource from this instance.""" - url = cls._get_url({'queue_name': claim.queue_name}) - headers = {'Client-ID': claim.client_id} - params = {'limit': claim.limit} if claim.limit else None - body = [] - - try: - resp = session.post(url, - headers=headers, - data=json.dumps(claim, cls=ClaimEncoder), - params=params) - body = resp.json() - except exceptions.InvalidResponse as e: - # The Message Service will respond with a 204 and no content in - # the body when there are no messages to claim. The transport - # layer doesn't like that and we have to correct for it here. - # Ultimately it's a bug in the v1.0 Message Service API. - # TODO(etoews): API is fixed in v1.1 so fix this for message.v1_1 - # https://wiki.openstack.org/wiki/Zaqar/specs/api/v1.1 - if e.response.status_code != 204: - raise e - - for message_attrs in body: - yield message.Message.new( - client_id=claim.client_id, - queue_name=claim.queue_name, - **message_attrs) - - -class ClaimEncoder(json.JSONEncoder): - def default(self, claim): - return {'ttl': claim.ttl, 'grace': claim.grace} diff --git a/openstack/message/v1/message.py b/openstack/message/v1/message.py deleted file mode 100644 index 6d6f703f0..000000000 --- a/openstack/message/v1/message.py +++ /dev/null @@ -1,107 +0,0 @@ -# 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 json - -from six.moves.urllib import parse - -from openstack.message import message_service -from openstack import resource - - -class Message(resource.Resource): - resources_key = 'messages' - base_path = "/queues/%(queue_name)s/messages" - service = message_service.MessageService() - - # capabilities - allow_create = True - allow_list = False - allow_retrieve = False - allow_delete = False - - #: A ID for each client instance. The ID must be submitted in its - #: canonical form (for example, 3381af92-2b9e-11e3-b191-71861300734c). - #: The client generates this ID once. The client ID persists between - #: restarts of the client so the client should reuse that same ID. - #: All message-related operations require the use of the client ID in - #: the headers to ensure that messages are not echoed back to the client - #: that posted them, unless the client explicitly requests this. - client_id = None - - #: The name of the queue this Message belongs to. - queue_name = None - - #: A relative href that references this Message. - href = resource.prop("href") - - #: An arbitrary JSON document that constitutes the body of the message - #: being sent. - body = resource.prop("body") - - #: Specifies how long the server waits, in seconds, before marking the - #: message as expired and removing it from the queue. - ttl = resource.prop("ttl") - - #: Specifies how long the message has been in the queue, in seconds. - age = resource.prop("age") - - @classmethod - def create_messages(cls, session, messages): - if len(messages) == 0: - raise ValueError('messages cannot be empty') - - for i, message in enumerate(messages, -1): - if message.queue_name != messages[i].queue_name: - raise ValueError('All queues in messages must be equal') - if message.client_id != messages[i].client_id: - raise ValueError('All clients in messages must be equal') - - url = cls._get_url({'queue_name': messages[0].queue_name}) - headers = {'Client-ID': messages[0].client_id} - - resp = session.post(url, headers=headers, - data=json.dumps(messages, cls=MessageEncoder)) - resp = resp.json() - - messages_created = [] - hrefs = resp['resources'] - - for i, href in enumerate(hrefs): - message = Message.existing(**messages[i]) - message.href = href - messages_created.append(message) - - return messages_created - - @classmethod - def _strip_version(cls, href): - path = parse.urlparse(href).path - - if path.startswith('/v'): - return href[href.find('/', 1):] - else: - return href - - @classmethod - def delete_by_id(cls, session, message, path_args=None): - url = cls._strip_version(message.href) - headers = { - 'Client-ID': message.client_id, - 'Accept': '', - } - session.delete(url, headers=headers) - - -class MessageEncoder(json.JSONEncoder): - def default(self, message): - return {'body': message.body, 'ttl': message.ttl} diff --git a/openstack/message/v1/queue.py b/openstack/message/v1/queue.py deleted file mode 100644 index 034ce3c2a..000000000 --- a/openstack/message/v1/queue.py +++ /dev/null @@ -1,34 +0,0 @@ -# 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.message import message_service -from openstack import resource - - -class Queue(resource.Resource): - id_attribute = 'name' - resources_key = 'queues' - base_path = '/queues' - service = message_service.MessageService() - - # capabilities - allow_create = True - allow_list = False - allow_retrieve = False - allow_delete = True - - @classmethod - def create_by_id(cls, session, attrs, resource_id=None, path_args=None): - url = cls._get_url(path_args, resource_id) - headers = {'Accept': ''} - session.put(url, headers=headers) - return {cls.id_attribute: resource_id} diff --git a/openstack/message/version.py b/openstack/message/version.py index 431fd239d..831effac7 100644 --- a/openstack/message/version.py +++ b/openstack/message/version.py @@ -11,7 +11,7 @@ # under the License. from openstack.message import message_service -from openstack import resource +from openstack import resource2 as resource class Version(resource.Resource): @@ -26,5 +26,5 @@ class Version(resource.Resource): allow_list = True # Properties - links = resource.prop('links') - status = resource.prop('status') + links = resource.Body('links') + status = resource.Body('status') diff --git a/openstack/tests/unit/message/test_message_service.py b/openstack/tests/unit/message/test_message_service.py index 66d074fc1..877a47a5d 100644 --- a/openstack/tests/unit/message/test_message_service.py +++ b/openstack/tests/unit/message/test_message_service.py @@ -23,8 +23,6 @@ def test_service(self): self.assertEqual('public', sot.interface) self.assertIsNone(sot.region) self.assertIsNone(sot.service_name) - self.assertEqual(2, len(sot.valid_versions)) - self.assertEqual('v1', sot.valid_versions[0].module) - self.assertEqual('v1', sot.valid_versions[0].path) - self.assertEqual('v2', sot.valid_versions[1].module) - self.assertEqual('v2', sot.valid_versions[1].path) + self.assertEqual(1, len(sot.valid_versions)) + self.assertEqual('v2', sot.valid_versions[0].module) + self.assertEqual('v2', sot.valid_versions[0].path) diff --git a/openstack/tests/unit/message/test_version.py b/openstack/tests/unit/message/test_version.py index bf9662e17..aea10840e 100644 --- a/openstack/tests/unit/message/test_version.py +++ b/openstack/tests/unit/message/test_version.py @@ -31,13 +31,13 @@ def test_basic(self): self.assertEqual('/', sot.base_path) self.assertEqual('messaging', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_retrieve) + self.assertFalse(sot.allow_get) self.assertFalse(sot.allow_update) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) def test_make_it(self): - sot = version.Version(EXAMPLE) + sot = version.Version(**EXAMPLE) self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['links'], sot.links) self.assertEqual(EXAMPLE['status'], sot.status) diff --git a/openstack/tests/unit/message/v1/__init__.py b/openstack/tests/unit/message/v1/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/openstack/tests/unit/message/v1/test_claim.py b/openstack/tests/unit/message/v1/test_claim.py deleted file mode 100644 index f6490a3d0..000000000 --- a/openstack/tests/unit/message/v1/test_claim.py +++ /dev/null @@ -1,97 +0,0 @@ -# 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 json -import mock -import testtools - -from openstack import exceptions -from openstack.message.v1 import claim - -CLIENT = '3381af92-2b9e-11e3-b191-71861300734c' -QUEUE = 'test_queue' -LIMIT = 2 -FAKE = { - 'ttl': 300, - 'grace': 60 -} - - -class TestClaim(testtools.TestCase): - - def test_basic(self): - sot = claim.Claim() - self.assertEqual('claims', sot.resources_key) - self.assertEqual('/queues/%(queue_name)s/claims', sot.base_path) - self.assertEqual('messaging', sot.service.service_type) - self.assertTrue(sot.allow_create) - self.assertFalse(sot.allow_retrieve) - self.assertFalse(sot.allow_update) - self.assertFalse(sot.allow_delete) - self.assertFalse(sot.allow_list) - - def test_make_it(self): - sot = claim.Claim.new(client_id=CLIENT, - queue_name=QUEUE, - limit=LIMIT, - **FAKE) - self.assertEqual(CLIENT, sot.client_id) - self.assertEqual(QUEUE, sot.queue_name) - self.assertEqual(LIMIT, sot.limit) - self.assertEqual(FAKE['ttl'], sot.ttl) - self.assertEqual(FAKE['grace'], sot.grace) - - def test_create(self): - sess = mock.Mock() - obj = mock.Mock() - fake_attrs = [{'foo': 'bar'}, {'zoo': 'lah'}] - obj.json = mock.Mock(return_value=fake_attrs) - sess.post = mock.Mock(return_value=obj) - sot = claim.Claim() - - c = claim.Claim.new(client_id=CLIENT, queue_name=QUEUE, **FAKE) - list(sot.claim_messages(sess, c)) - - url = '/queues/%s/claims' % QUEUE - sess.post.assert_called_with( - url, - headers={'Client-ID': CLIENT}, params=None, - data=json.dumps(FAKE, cls=claim.ClaimEncoder)) - - def test_claim_messages_no_invalid_response(self): - sess = mock.Mock() - resp = mock.Mock() - resp.status_code = 204 - sess.post = mock.Mock( - side_effect=exceptions.InvalidResponse(response=resp)) - sot = claim.Claim() - - messages = list(sot.claim_messages( - sess, claim.Claim.new(client_id=CLIENT, queue_name=QUEUE, **FAKE))) - - self.assertEqual(0, len(messages)) - - def test_claim_messages_invalid_response(self): - sess = mock.Mock() - resp = mock.Mock() - resp.status_code = 400 - sess.post = mock.Mock( - side_effect=exceptions.InvalidResponse(response=resp)) - sot = claim.Claim() - - try: - list(sot.claim_messages( - sess, claim.Claim.new(client_id=CLIENT, - queue_name=QUEUE, - **FAKE))) - except exceptions.InvalidResponse as e: - self.assertEqual(400, e.response.status_code) diff --git a/openstack/tests/unit/message/v1/test_message.py b/openstack/tests/unit/message/v1/test_message.py deleted file mode 100644 index e3cd59a7f..000000000 --- a/openstack/tests/unit/message/v1/test_message.py +++ /dev/null @@ -1,85 +0,0 @@ -# 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 json - -import mock -import testtools - -from openstack.message.v1 import message - -CLIENT = '3381af92-2b9e-11e3-b191-71861300734c' -QUEUE = 'test_queue' -FAKE = { - 'ttl': 300, - 'body': {'key': 'value'} -} -FAKE_HREF = { - 'href': '/v1/queues/test_queue/messages/1234', - 'ttl': 300, - 'body': {'key': 'value'} -} - - -class TestMessage(testtools.TestCase): - - def test_basic(self): - sot = message.Message() - self.assertEqual('messages', sot.resources_key) - self.assertEqual('/queues/%(queue_name)s/messages', sot.base_path) - self.assertEqual('messaging', sot.service.service_type) - self.assertTrue(sot.allow_create) - self.assertFalse(sot.allow_retrieve) - self.assertFalse(sot.allow_update) - self.assertFalse(sot.allow_delete) - self.assertFalse(sot.allow_list) - - def test_make_it(self): - sot = message.Message(FAKE) - self.assertEqual(FAKE['ttl'], sot.ttl) - self.assertEqual(FAKE['body'], sot.body) - - def test_create(self): - sess = mock.Mock() - obj = mock.Mock() - obj.json = mock.Mock(return_value={'resources': {'k': 'v'}}) - sess.post = mock.Mock(return_value=obj) - sot = message.Message() - - msg = message.Message.new(client_id=CLIENT, queue_name=QUEUE, **FAKE) - sot.create_messages(sess, [msg]) - - url = '/queues/%s/messages' % QUEUE - sess.post.assert_called_with( - url, - headers={'Client-ID': CLIENT}, - data=mock.ANY) - - args, kwargs = sess.post.call_args - self.assertIn("data", kwargs) - self.assertDictEqual(json.loads(kwargs["data"])[0], FAKE) - - def test_delete(self): - sess = mock.Mock() - sess.delete = mock.Mock() - sess.delete.return_value = mock.Mock() - sot = message.Message() - - sot.delete_by_id( - sess, message.Message.new(client_id=CLIENT, - queue_name=QUEUE, - **FAKE_HREF)) - - url = '/queues/%s/messages/1234' % QUEUE - sess.delete.assert_called_with( - url, - headers={'Client-ID': CLIENT, 'Accept': ''}) diff --git a/openstack/tests/unit/message/v1/test_proxy.py b/openstack/tests/unit/message/v1/test_proxy.py deleted file mode 100644 index 6f3c4a5ee..000000000 --- a/openstack/tests/unit/message/v1/test_proxy.py +++ /dev/null @@ -1,55 +0,0 @@ -# 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.message.v1 import _proxy -from openstack.message.v1 import claim -from openstack.message.v1 import message -from openstack.message.v1 import queue -from openstack.tests.unit import test_proxy_base - -CLIENT_ID = '3381af92-2b9e-11e3-b191-71861300734c' -QUEUE_NAME = 'test_queue' - - -class TestMessageProxy(test_proxy_base.TestProxyBase): - def setUp(self): - super(TestMessageProxy, self).setUp() - self.proxy = _proxy.Proxy(self.session) - - def test_queue_create_attrs(self): - self.verify_create(self.proxy.create_queue, queue.Queue) - - def test_queue_delete(self): - self.verify_delete(self.proxy.delete_queue, queue.Queue, False) - - def test_queue_delete_ignore(self): - self.verify_delete(self.proxy.delete_queue, queue.Queue, True) - - def test_messages_create(self): - self._verify2("openstack.message.v1.message.Message.create_messages", - self.proxy.create_messages, - expected_result="result", - method_args=[[]], - expected_args=[self.proxy, []]) - - def test_messages_claim(self): - self._verify2("openstack.message.v1.claim.Claim.claim_messages", - self.proxy.claim_messages, - expected_result="result", - method_args=[claim.Claim], - expected_args=[self.proxy, claim.Claim]) - - def test_message_delete(self): - self._verify2("openstack.message.v1.message.Message.delete_by_id", - self.proxy.delete_message, - method_args=[message.Message], - expected_args=[self.proxy, message.Message]) diff --git a/openstack/tests/unit/message/v1/test_queue.py b/openstack/tests/unit/message/v1/test_queue.py deleted file mode 100644 index 0e62b0fec..000000000 --- a/openstack/tests/unit/message/v1/test_queue.py +++ /dev/null @@ -1,55 +0,0 @@ -# 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 testtools - -from openstack.message.v1 import queue - - -FAKE_NAME = 'test_queue' -FAKE = { - 'name': FAKE_NAME, -} - - -class TestQueue(testtools.TestCase): - - def test_basic(self): - sot = queue.Queue() - self.assertEqual('queues', sot.resources_key) - self.assertEqual('/queues', sot.base_path) - self.assertEqual('messaging', sot.service.service_type) - self.assertTrue(sot.allow_create) - self.assertFalse(sot.allow_retrieve) - self.assertFalse(sot.allow_update) - self.assertTrue(sot.allow_delete) - self.assertFalse(sot.allow_list) - - def test_make_it(self): - sot = queue.Queue(FAKE) - self.assertEqual(FAKE['name'], sot.name) - - def test_create(self): - sess = mock.Mock() - sess.put = mock.Mock() - sess.put.return_value = mock.Mock() - sot = queue.Queue(FAKE) - - sot.create(sess) - - url = 'queues/%s' % FAKE_NAME - headers = {'Accept': ''} - sess.put.assert_called_with(url, - headers=headers) - self.assertEqual(FAKE_NAME, sot.id) - self.assertEqual(FAKE_NAME, sot.name) From dc8fc95d4b832fe376f4f03af6595d07b281ca49 Mon Sep 17 00:00:00 2001 From: wangqiangbj Date: Wed, 17 Jan 2018 12:34:40 +0800 Subject: [PATCH 1907/3836] fix misspelling of 'configuration' Change-Id: I1967d62aa8ac0af23fa576f67696bee19f1ac2b6 --- openstack/baremetal/v1/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 008132c3d..e99a61487 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -107,7 +107,7 @@ class Node(resource.Resource): target_provision_state = resource.Body("target_provision_state") #: The requested state during a state transition. target_power_state = resource.Body("target_power_state") - #: The requested RAID configration of the node which will be applied when + #: The requested RAID configuration of the node which will be applied when #: the node next transitions through the CLEANING state. target_raid_config = resource.Body("target_raid_config") #: Timestamp at which the node was last updated. From 4bbbffcc20236608be7baa4bc303292e6a0b7466 Mon Sep 17 00:00:00 2001 From: chenpengzi <1523688226@qq.com> Date: Wed, 17 Jan 2018 15:31:25 +0800 Subject: [PATCH 1908/3836] Update Release Notes links and add bugs links Change-Id: I20a1a5b7528a2d8bca74fef34984d246e8aa653a --- README.rst | 1 + doc/source/releasenotes.rst | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 5989a8aae..00f49d412 100644 --- a/README.rst +++ b/README.rst @@ -117,3 +117,4 @@ Links * `Documentation `_ * `PyPI `_ * `Mailing list `_ +* `Bugs `_ diff --git a/doc/source/releasenotes.rst b/doc/source/releasenotes.rst index a61e4d3e3..ecf99e888 100644 --- a/doc/source/releasenotes.rst +++ b/doc/source/releasenotes.rst @@ -3,4 +3,4 @@ Release Notes ============= Release notes for `python-openstacksdk` can be found at -http://docs.openstack.org/releasenotes/python-openstacksdk/ +https://releases.openstack.org/teams/openstacksdk.html From d42e200b874a4938f7898ae816ffd8b0405338a6 Mon Sep 17 00:00:00 2001 From: lvxianguo Date: Wed, 17 Jan 2018 17:11:13 +0800 Subject: [PATCH 1909/3836] modify spelling error of resource Change-Id: I921ead0076b87173a4627370cbcd5fd2ee344437 --- openstack/cloud/_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index b5eb032b4..4d32095c4 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -188,7 +188,7 @@ def _get_entity(cloud, resource, name_or_id, filters, **kwargs): return name_or_id # If a uuid is passed short-circuit it calling the - # get__by_id method + # get__by_id method if getattr(cloud, 'use_direct_get', False) and _is_uuid_like(name_or_id): get_resource = getattr(cloud, 'get_%s_by_id' % resource, None) if get_resource: From a292e473197fafbf322e6ca7b14b5f252d86a35d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 17 Jan 2018 10:37:57 -0600 Subject: [PATCH 1910/3836] Add deprecation warnings to profile Profile is being replaced by CloudRegion. While compatibility remains for the next release, it'll be gone by 1.0. Emit deprecation warnings so that people can work on migrating. Change-Id: Id58d563f7eaff48fc34b7bfa37851f3a1db8f25a --- openstack/profile.py | 57 +++++++------------ .../deprecated-profile-762afdef0e8fc9e8.yaml | 6 ++ 2 files changed, 25 insertions(+), 38 deletions(-) create mode 100644 releasenotes/notes/deprecated-profile-762afdef0e8fc9e8.yaml diff --git a/openstack/profile.py b/openstack/profile.py index d70c05afa..4e3f8b1e1 100644 --- a/openstack/profile.py +++ b/openstack/profile.py @@ -11,44 +11,8 @@ # under the License. """ -:class:`~openstack.profile.Profile` is the class that is used to -define the various preferences for different services. The preferences that -are currently supported are service name, region, version and interface. -The :class:`~openstack.profile.Profile` and the -:class:`~openstack.connection.Connection` classes are the most important -user facing classes. - -Examples --------- - -The :class:`~openstack.profile.Profile` class is constructed -with no arguments. - -Set Methods -~~~~~~~~~~~ - -A user's preferences are set based on the service type. Service type would -normally be something like 'compute', 'identity', 'object-store', etc.:: - - from openstack import profile - prof = profile.Profile() - prof.set_name('compute', 'matrix') - prof.set_region(prof.ALL, 'zion') - prof.set_version('identity', 'v3') - prof.set_interface('object-store', 'internal') - for service in prof.get_services(): - print(prof.get_filter(service.service_type) - -The resulting preference print out would look something like:: - - service_type=compute,region=zion,service_name=matrix - service_type=network,region=zion - service_type=database,region=zion - service_type=image,region=zion - service_type=metering,region=zion - service_type=orchestration,region=zion - service_type=object-store,interface=internal,region=zion - service_type=identity,region=zion,version=v3 +:class:`~openstack.profile.Profile` is deprecated. Code should use +:class:`~openstack.config.cloud_region.CloudRegion` instead. """ import copy @@ -70,6 +34,7 @@ from openstack.network import network_service from openstack.object_store import object_store_service from openstack.orchestration import orchestration_service +from openstack import utils from openstack.workflow import workflow_service _logger = logging.getLogger(__name__) @@ -80,6 +45,8 @@ class Profile(object): ALL = "*" """Wildcard service identifier representing all services.""" + @utils.deprecated(deprecated_in="0.10.0", removed_in="1.0", + details="Use openstack.config instead") def __init__(self, plugins=None): """User preference for each service. @@ -121,6 +88,8 @@ def _add_service(self, serv): serv.interface = None self._services[serv.service_type] = serv + @utils.deprecated(deprecated_in="0.10.0", removed_in="1.0", + details="Use openstack.config instead") def get_filter(self, service): """Get a service preference. @@ -147,6 +116,8 @@ def _setter(self, service, attr, value): for service in self._get_services(service): setattr(self._get_filter(service), attr, value) + @utils.deprecated(deprecated_in="0.10.0", removed_in="1.0", + details="Use openstack.config instead") def get_services(self): """Get a list of all the known services.""" services = [] @@ -154,6 +125,8 @@ def get_services(self): services.append(service) return services + @utils.deprecated(deprecated_in="0.10.0", removed_in="1.0", + details="Use openstack.config instead") def set_name(self, service, name): """Set the desired name for the specified service. @@ -162,6 +135,8 @@ def set_name(self, service, name): """ self._setter(service, "service_name", name) + @utils.deprecated(deprecated_in="0.10.0", removed_in="1.0", + details="Use openstack.config instead") def set_region(self, service, region): """Set the desired region for the specified service. @@ -170,6 +145,8 @@ def set_region(self, service, region): """ self._setter(service, "region", region) + @utils.deprecated(deprecated_in="0.10.0", removed_in="1.0", + details="Use openstack.config instead") def set_version(self, service, version): """Set the desired version for the specified service. @@ -178,6 +155,8 @@ def set_version(self, service, version): """ self._get_filter(service).version = version + @utils.deprecated(deprecated_in="0.10.0", removed_in="1.0", + details="Use openstack.config instead") def set_api_version(self, service, api_version): """Set the desired API micro-version for the specified service. @@ -186,6 +165,8 @@ def set_api_version(self, service, api_version): """ self._setter(service, "api_version", api_version) + @utils.deprecated(deprecated_in="0.10.0", removed_in="1.0", + details="Use openstack.config instead") def set_interface(self, service, interface): """Set the desired interface for the specified service. diff --git a/releasenotes/notes/deprecated-profile-762afdef0e8fc9e8.yaml b/releasenotes/notes/deprecated-profile-762afdef0e8fc9e8.yaml new file mode 100644 index 000000000..e09d17c34 --- /dev/null +++ b/releasenotes/notes/deprecated-profile-762afdef0e8fc9e8.yaml @@ -0,0 +1,6 @@ +--- +deprecations: + - | + ``openstack.profile.Profile`` has been deprecated and will be removed + in the ``1.0`` release. Users should use the functions in + ``openstack.config`` instead. From 34bae5a192d09136e5b788b80948fd6c0623d7a6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 12 Jan 2018 14:59:47 -0600 Subject: [PATCH 1911/3836] Migrate object_store to resource2/proxy2 This is the last thing we need to migrate before deleting the old resource/proxy class. There's a good pile of things related to uploading and downloading we should port over from the openstack.cloud code. One of them, streaming the object data, was added here. We can come back around and get all of the large-object upload code supported. Adds alias support into _BaseComponent much like prop used to have because Swift needs it. Port some of the unittests to requests-mock because it was just easier to do that. Doing so uncovered some issues with case-sensitivity and headers. As a result, there are some changes to resource2.Resource to get case sensitivity sorted out. TODO comments have been left indicating a few places for further cleanup, but those are internal and non-essential. The default value of has_body is changed to False for head calls. Because. Well. Let's be honest. It's HEAD. There is no body. By definition. Change-Id: I8c4f18f78a77149e23b98f78af82b1d25ab7c4cf --- openstack/_adapter.py | 17 +- openstack/object_store/v1/_base.py | 66 ++--- openstack/object_store/v1/_proxy.py | 113 +++++---- openstack/object_store/v1/account.py | 19 +- openstack/object_store/v1/container.py | 84 ++++--- openstack/object_store/v1/obj.py | 199 ++++++++++----- openstack/resource2.py | 68 ++++- .../functional/object_store/v1/test_obj.py | 6 +- openstack/tests/unit/base.py | 7 + .../unit/object_store/v1/test_account.py | 6 +- .../unit/object_store/v1/test_container.py | 236 +++++++++--------- .../tests/unit/object_store/v1/test_obj.py | 184 +++++++------- .../tests/unit/object_store/v1/test_proxy.py | 102 +++++--- openstack/tests/unit/test_proxy_base2.py | 12 +- openstack/tests/unit/test_resource2.py | 6 +- 15 files changed, 658 insertions(+), 467 deletions(-) diff --git a/openstack/_adapter.py b/openstack/_adapter.py index 5c9918b3b..c4a8df5ec 100644 --- a/openstack/_adapter.py +++ b/openstack/_adapter.py @@ -29,7 +29,7 @@ from openstack import task_manager as _task_manager -def _extract_name(url): +def _extract_name(url, service_type=None): '''Produce a key name to use in logging/metrics from the URL path. We want to be able to logic/metric sane general things, so we pull @@ -81,7 +81,10 @@ def _extract_name(url): # Getting the root of an endpoint is doing version discovery if not name_parts: - name_parts = ['discovery'] + if service_type == 'object-store': + name_parts = ['account'] + else: + name_parts = ['discovery'] # Strip out anything that's empty or None return [part for part in name_parts if part] @@ -124,8 +127,14 @@ def __init__(self, session=None, task_manager=None, *args, **kwargs): def request( self, url, method, run_async=False, error_message=None, raise_exc=False, connect_retries=1, *args, **kwargs): - name_parts = _extract_name(url) - name = '.'.join([self.service_type, method] + name_parts) + name_parts = _extract_name(url, self.service_type) + # TODO(mordred) This if is in service of unit tests that are making + # calls without a service_type. It should be fixable once we shift + # to requests-mock and stop mocking internals. + if self.service_type: + name = '.'.join([self.service_type, method] + name_parts) + else: + name = '.'.join([method] + name_parts) request_method = functools.partial( super(OpenStackSDKAdapter, self).request, url, method) diff --git a/openstack/object_store/v1/_base.py b/openstack/object_store/v1/_base.py index a16e8fb7a..d2f9c13d6 100644 --- a/openstack/object_store/v1/_base.py +++ b/openstack/object_store/v1/_base.py @@ -11,13 +11,17 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import exceptions from openstack.object_store import object_store_service -from openstack import resource +from openstack import resource2 as resource class BaseResource(resource.Resource): service = object_store_service.ObjectStoreService() + update_method = 'POST' + create_method = 'PUT' + #: Metadata stored for this resource. *Type: dict* metadata = dict() @@ -25,7 +29,7 @@ class BaseResource(resource.Resource): _system_metadata = dict() def _calculate_headers(self, metadata): - headers = dict() + headers = {} for key in metadata: if key in self._system_metadata.keys(): header = self._system_metadata[key] @@ -40,52 +44,34 @@ def _calculate_headers(self, metadata): return headers def set_metadata(self, session, metadata): - url = self._get_url(self, self.id) - session.post(url, - headers=self._calculate_headers(metadata)) + request = self._prepare_request() + response = session.post( + request.url, + headers=self._calculate_headers(metadata)) + self._translate_response(response, has_body=False) + response = session.head(request.url) + self._translate_response(response, has_body=False) + return self def delete_metadata(self, session, keys): - url = self._get_url(self, self.id) + request = self._prepare_request() headers = {key: '' for key in keys} - session.post(url, - headers=self._calculate_headers(headers)) + response = session.post( + request.url, + headers=self._calculate_headers(headers)) + exceptions.raise_from_response( + response, error_message="Error deleting metadata keys") + return self - def _set_metadata(self): + def _set_metadata(self, headers): self.metadata = dict() - headers = self.get_headers() for header in headers: if header.startswith(self._custom_metadata_prefix): key = header[len(self._custom_metadata_prefix):].lower() self.metadata[key] = headers[header] - def get(self, session, include_headers=False, args=None): - super(BaseResource, self).get(session, include_headers, args) - self._set_metadata() - return self - - def head(self, session): - super(BaseResource, self).head(session) - self._set_metadata() - return self - - @classmethod - def update_by_id(cls, session, resource_id, attrs, path_args=None): - """Update a Resource with the given attributes. - - :param session: The session to use for making this request. - :type session: :class:`~keystoneauth1.adapter.Adapter` - :param resource_id: This resource's identifier, if needed by - the request. The default is ``None``. - :param dict attrs: The attributes to be sent in the body - of the request. - :param dict path_args: This parameter is sent by the base - class but is ignored for this method. - - :return: A ``dict`` representing the response headers. - """ - url = cls._get_url(None, resource_id) - headers = attrs.get(resource.HEADERS, dict()) - headers['Accept'] = '' - return session.post(url, - headers=headers).headers + def _translate_response(self, response, has_body=None, error_message=None): + super(BaseResource, self)._translate_response( + response, has_body=has_body, error_message=error_message) + self._set_metadata(response.headers) diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index 6258eab53..8b8a7b4ec 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -13,11 +13,15 @@ from openstack.object_store.v1 import account as _account from openstack.object_store.v1 import container as _container from openstack.object_store.v1 import obj as _obj -from openstack import proxy +from openstack import proxy2 as proxy class Proxy(proxy.BaseProxy): + Account = _account.Account + Container = _container.Container + Object = _obj.Object + def get_account_metadata(self): """Get metadata for this account. @@ -54,11 +58,12 @@ def containers(self, **query): :rtype: A generator of :class:`~openstack.object_store.v1.container.Container` objects. """ - return _container.Container.list(self, **query) + return self._list(_container.Container, paginated=True, **query) - def create_container(self, **attrs): + def create_container(self, name, **attrs): """Create a new container from attributes + :param container: Name of the container to create. :param dict attrs: Keyword arguments which will be used to create a :class:`~openstack.object_store.v1.container.Container`, comprised of the properties on the Container class. @@ -66,7 +71,7 @@ def create_container(self, **attrs): :returns: The results of container creation :rtype: :class:`~openstack.object_store.v1.container.Container` """ - return self._create(_container.Container, **attrs) + return self._create(_container.Container, name=name, **attrs) def delete_container(self, container, ignore_missing=True): """Delete a container @@ -122,6 +127,7 @@ def set_container_metadata(self, container, **metadata): """ res = self._get_resource(_container.Container, container) res.set_metadata(self, metadata) + return res def delete_container_metadata(self, container, keys): """Delete metadata for a container. @@ -133,6 +139,7 @@ def delete_container_metadata(self, container, keys): """ res = self._get_resource(_container.Container, container) res.delete_metadata(self, keys) + return res def objects(self, container, **query): """Return a generator that yields the Container's objects. @@ -147,21 +154,21 @@ def objects(self, container, **query): :rtype: A generator of :class:`~openstack.object_store.v1.obj.Object` objects. """ - container = _container.Container.from_id(container) + container = self._get_container_name(container=container) - objs = _obj.Object.list(self, - path_args={"container": container.name}, - **query) - for obj in objs: - obj.container = container.name + for obj in self._list( + _obj.Object, container=container, + paginated=True, **query): + obj.container = container yield obj - def _get_container_name(self, obj, container): - if isinstance(obj, _obj.Object): + def _get_container_name(self, obj=None, container=None): + if obj is not None: + obj = self._get_resource(_obj.Object, obj) if obj.container is not None: return obj.container if container is not None: - container = _container.Container.from_id(container) + container = self._get_resource(_container.Container, container) return container.name raise ValueError("container must be specified") @@ -181,52 +188,69 @@ def get_object(self, obj, container=None): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - # TODO(briancurtin): call this download_object and make sure it's - # just returning the raw data, like download_image does - container_name = self._get_container_name(obj, container) + container_name = self._get_container_name( + obj=obj, container=container) + return self._get(_obj.Object, obj, container=container_name) + + def download_object(self, obj, container=None, **attrs): + """Download the data contained inside an object. + + :param obj: The value can be the name of an object or a + :class:`~openstack.object_store.v1.obj.Object` instance. + :param container: The value can be the name of a container or a + :class:`~openstack.object_store.v1.container.Container` + instance. - return self._get(_obj.Object, obj, - path_args={"container": container_name}) + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + container_name = self._get_container_name( + obj=obj, container=container) + obj = self._get_resource( + _obj.Object, obj, container=container_name, **attrs) + return obj.download(self) - def download_object(self, obj, container=None, path=None): - """Download the data contained inside an object to disk. + def stream_object(self, obj, container=None, chunk_size=1024, **attrs): + """Stream the data contained inside an object. :param obj: The value can be the name of an object or a :class:`~openstack.object_store.v1.obj.Object` instance. :param container: The value can be the name of a container or a :class:`~openstack.object_store.v1.container.Container` instance. - :param path str: Location to write the object contents. :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. + :returns: An iterator that iterates over chunk_size bytes """ - # TODO(briancurtin): download_object should really have the behavior - # of get_object, and this writing to a file should not exist. - # TODO(briancurtin): This method should probably offload the get - # operation into another thread or something of that nature. - with open(path, "w") as out: - out.write(self.get_object(obj, container)) - - def upload_object(self, **attrs): + container_name = self._get_container_name( + obj=obj, container=container) + container_name = self._get_container_name(container=container) + obj = self._get_resource( + _obj.Object, obj, container=container_name, **attrs) + return obj.stream(self, chunk_size=chunk_size) + + def create_object(self, container, name, **attrs): """Upload a new object from attributes + :param container: The value can be the name of a container or a + :class:`~openstack.object_store.v1.container.Container` + instance. + :param name: Name of the object to create. :param dict attrs: Keyword arguments which will be used to create a :class:`~openstack.object_store.v1.obj.Object`, comprised of the properties on the Object class. - **Required**: A `container` argument must be specified, - which is either the ID of a container or a - :class:`~openstack.object_store.v1.container.Container` - instance. :returns: The results of object creation :rtype: :class:`~openstack.object_store.v1.container.Container` """ - container = attrs.pop("container", None) - container_name = self._get_container_name(None, container) - - return self._create(_obj.Object, - path_args={"container": container_name}, **attrs) + # TODO(mordred) Add ability to stream data from a file + # TODO(mordred) Use create_object from OpenStackCloud + container_name = self._get_container_name(container=container) + return self._create( + _obj.Object, container=container_name, name=name, **attrs) + # Backwards compat + upload_object = create_object def copy_object(self): """Copy an object.""" @@ -252,7 +276,7 @@ def delete_object(self, obj, ignore_missing=True, container=None): container_name = self._get_container_name(obj, container) self._delete(_obj.Object, obj, ignore_missing=ignore_missing, - path_args={"container": container_name}) + container=container_name) def get_object_metadata(self, obj, container=None): """Get metadata for an object. @@ -269,8 +293,7 @@ def get_object_metadata(self, obj, container=None): """ container_name = self._get_container_name(obj, container) - return self._head(_obj.Object, obj, - path_args={"container": container_name}) + return self._head(_obj.Object, obj, container=container_name) def set_object_metadata(self, obj, container=None, **metadata): """Set metadata for an object. @@ -298,9 +321,9 @@ def set_object_metadata(self, obj, container=None, **metadata): - `is_content_type_detected` """ container_name = self._get_container_name(obj, container) - res = self._get_resource(_obj.Object, obj, - path_args={"container": container_name}) + res = self._get_resource(_obj.Object, obj, container=container_name) res.set_metadata(self, metadata) + return res def delete_object_metadata(self, obj, container=None, keys=None): """Delete metadata for an object. @@ -313,6 +336,6 @@ def delete_object_metadata(self, obj, container=None, keys=None): :param keys: The keys of metadata to be deleted. """ container_name = self._get_container_name(obj, container) - res = self._get_resource(_obj.Object, obj, - path_args={"container": container_name}) + res = self._get_resource(_obj.Object, obj, container=container_name) res.delete_metadata(self, keys) + return res diff --git a/openstack/object_store/v1/account.py b/openstack/object_store/v1/account.py index 8857882d2..410d3a339 100644 --- a/openstack/object_store/v1/account.py +++ b/openstack/object_store/v1/account.py @@ -12,7 +12,7 @@ # under the License. from openstack.object_store.v1 import _base -from openstack import resource +from openstack import resource2 as resource class Account(_base.BaseResource): @@ -20,23 +20,26 @@ class Account(_base.BaseResource): base_path = "/" - allow_retrieve = True + allow_get = True allow_update = True allow_head = True #: The total number of bytes that are stored in Object Storage for #: the account. - account_bytes_used = resource.header("x-account-bytes-used", type=int) + account_bytes_used = resource.Header("x-account-bytes-used", type=int) #: The number of containers. - account_container_count = resource.header("x-account-container-count", + account_container_count = resource.Header("x-account-container-count", type=int) #: The number of objects in the account. - account_object_count = resource.header("x-account-object-count", type=int) + account_object_count = resource.Header("x-account-object-count", type=int) #: The secret key value for temporary URLs. If not set, #: this header is not returned by this operation. - meta_temp_url_key = resource.header("x-account-meta-temp-url-key") + meta_temp_url_key = resource.Header("x-account-meta-temp-url-key") #: A second secret key value for temporary URLs. If not set, #: this header is not returned by this operation. - meta_temp_url_key_2 = resource.header("x-account-meta-temp-url-key-2") + meta_temp_url_key_2 = resource.Header("x-account-meta-temp-url-key-2") #: The timestamp of the transaction. - timestamp = resource.header("x-timestamp") + timestamp = resource.Header("x-timestamp") + + has_body = False + requires_id = False diff --git a/openstack/object_store/v1/container.py b/openstack/object_store/v1/container.py index dd440b929..f5594a8f6 100644 --- a/openstack/object_store/v1/container.py +++ b/openstack/object_store/v1/container.py @@ -12,7 +12,7 @@ # under the License. from openstack.object_store.v1 import _base -from openstack import resource +from openstack import resource2 as resource class Container(_base.BaseResource): @@ -28,10 +28,10 @@ class Container(_base.BaseResource): } base_path = "/" - id_attribute = "name" + pagination_key = 'X-Account-Container-Count' allow_create = True - allow_retrieve = True + allow_get = True allow_update = True allow_delete = True allow_list = True @@ -39,20 +39,22 @@ class Container(_base.BaseResource): # Container body data (when id=None) #: The name of the container. - name = resource.prop("name") + name = resource.Body("name", alternate_id=True, alias='id') #: The number of objects in the container. - count = resource.prop("count") + count = resource.Body("count", type=int, alias='object_count') #: The total number of bytes that are stored in Object Storage #: for the container. - bytes = resource.prop("bytes") + bytes = resource.Body("bytes", type=int, alias='bytes_used') # Container metadata (when id=name) #: The number of objects. - object_count = resource.header("x-container-object-count", type=int) + object_count = resource.Header( + "x-container-object-count", type=int, alias='count') #: The count of bytes used in total. - bytes_used = resource.header("x-container-bytes-used", type=int) + bytes_used = resource.Header( + "x-container-bytes-used", type=int, alias='bytes') #: The timestamp of the transaction. - timestamp = resource.header("x-timestamp") + timestamp = resource.Header("x-timestamp") # Request headers (when id=None) #: If set to True, Object Storage queries all replicas to return the @@ -60,66 +62,66 @@ class Container(_base.BaseResource): #: faster after it finds one valid replica. Because setting this #: header to True is more expensive for the back end, use it only #: when it is absolutely needed. *Type: bool* - is_newest = resource.header("x-newest", type=bool) + is_newest = resource.Header("x-newest", type=bool) # Request headers (when id=name) #: The ACL that grants read access. If not set, this header is not #: returned by this operation. - read_ACL = resource.header("x-container-read") + read_ACL = resource.Header("x-container-read") #: The ACL that grants write access. If not set, this header is not #: returned by this operation. - write_ACL = resource.header("x-container-write") + write_ACL = resource.Header("x-container-write") #: The destination for container synchronization. If not set, #: this header is not returned by this operation. - sync_to = resource.header("x-container-sync-to") + sync_to = resource.Header("x-container-sync-to") #: The secret key for container synchronization. If not set, #: this header is not returned by this operation. - sync_key = resource.header("x-container-sync-key") + sync_key = resource.Header("x-container-sync-key") #: Enables versioning on this container. The value is the name #: of another container. You must UTF-8-encode and then URL-encode #: the name before you include it in the header. To disable #: versioning, set the header to an empty string. - versions_location = resource.header("x-versions-location") + versions_location = resource.Header("x-versions-location") #: The MIME type of the list of names. - content_type = resource.header("content-type") + content_type = resource.Header("content-type") #: If set to true, Object Storage guesses the content type based #: on the file extension and ignores the value sent in the #: Content-Type header, if present. *Type: bool* - is_content_type_detected = resource.header("x-detect-content-type", + is_content_type_detected = resource.Header("x-detect-content-type", type=bool) + # TODO(mordred) Shouldn't if-none-match be handled more systemically? #: In combination with Expect: 100-Continue, specify an #: "If-None-Match: \*" header to query whether the server already #: has a copy of the object before any data is sent. - if_none_match = resource.header("if-none-match") + if_none_match = resource.Header("if-none-match") @classmethod - def create_by_id(cls, session, attrs, resource_id=None): - """Create a Resource from its attributes. + def new(cls, **kwargs): + # Container uses name as id. Proxy._get_resource calls + # Resource.new(id=name) but then we need to do container.name + # It's the same thing for Container - make it be the same. + name = kwargs.pop('id', None) + if name: + kwargs.setdefault('name', name) + return Container(_synchronized=True, **kwargs) + + def create(self, session, prepend_key=True): + """Create a remote resource based on this instance. :param session: The session to use for making this request. :type session: :class:`~keystoneauth1.adapter.Adapter` - :param dict attrs: The attributes to be sent in the body - of the request. - :param resource_id: This resource's identifier, if needed by - the request. The default is ``None``. + :param prepend_key: A boolean indicating whether the resource_key + should be prepended in a resource creation + request. Default to True. - :return: A ``dict`` representing the response headers. + :return: This :class:`Resource` instance. + :raises: :exc:`~openstack.exceptions.MethodNotSupported` if + :data:`Resource.allow_create` is not set to ``True``. """ - url = cls._get_url(None, resource_id) - headers = attrs.get(resource.HEADERS, dict()) - headers['Accept'] = '' - return session.put(url, - headers=headers).headers + request = self._prepare_request( + requires_id=True, prepend_key=prepend_key) + response = session.put( + request.url, json=request.body, headers=request.headers) - def create(self, session): - """Create a Resource from this instance. - - :param session: The session to use for making this request. - :type session: :class:`~keystoneauth1.adapter.Adapter` - - :return: This instance. - """ - resp = self.create_by_id(session, self._attrs, self.id) - self.set_headers(resp) - self._reset_dirty() + self._translate_response(response, has_body=False) return self diff --git a/openstack/object_store/v1/obj.py b/openstack/object_store/v1/obj.py index f18ff6275..f32d68de6 100644 --- a/openstack/object_store/v1/obj.py +++ b/openstack/object_store/v1/obj.py @@ -13,9 +13,10 @@ import copy +from openstack import exceptions from openstack.object_store import object_store_service from openstack.object_store.v1 import _base -from openstack import resource +from openstack import resource2 as resource class Object(_base.BaseResource): @@ -30,28 +31,36 @@ class Object(_base.BaseResource): } base_path = "/%(container)s" + pagination_key = 'X-Container-Object-Count' service = object_store_service.ObjectStoreService() - id_attribute = "name" allow_create = True - allow_retrieve = True + allow_get = True allow_update = True allow_delete = True allow_list = True allow_head = True # Data to be passed during a POST call to create an object on the server. + # TODO(mordred) Make a base class BaseDataResource that can be used here + # and with glance images that has standard overrides for dealing with + # binary data. data = None # URL parameters #: The unique name for the container. - container = resource.prop("container") + container = resource.URI("container") #: The unique name for the object. - name = resource.prop("name") + name = resource.Body("name", alternate_id=True) # Object details - hash = resource.prop("hash") - bytes = resource.prop("bytes") + # Make these private because they should only matter in the case where + # we have a Body with no headers (like if someone programmatically is + # creating an Object) + _hash = resource.Body("hash") + _bytes = resource.Body("bytes", type=int) + _last_modified = resource.Body("last_modified") + _content_type = resource.Body("content_type") # Headers for HEAD and GET requests #: If set to True, Object Storage queries all replicas to return @@ -59,46 +68,49 @@ class Object(_base.BaseResource): #: responds faster after it finds one valid replica. Because #: setting this header to True is more expensive for the back end, #: use it only when it is absolutely needed. *Type: bool* - is_newest = resource.header("x-newest", type=bool) + is_newest = resource.Header("x-newest", type=bool) #: TODO(briancurtin) there's a lot of content here... - range = resource.header("range", type=dict) + range = resource.Header("range", type=dict) #: See http://www.ietf.org/rfc/rfc2616.txt. - if_match = resource.header("if-match", type=dict) + # TODO(mordred) We need a string-or-list formatter. type=list with a string + # value results in a list containing the characters. + if_match = resource.Header("if-match", type=list) #: In combination with Expect: 100-Continue, specify an #: "If-None-Match: \*" header to query whether the server already #: has a copy of the object before any data is sent. - if_none_match = resource.header("if-none-match", type=dict) + if_none_match = resource.Header("if-none-match", type=list) #: See http://www.ietf.org/rfc/rfc2616.txt. - if_modified_since = resource.header("if-modified-since", type=dict) + if_modified_since = resource.Header("if-modified-since", type=str) #: See http://www.ietf.org/rfc/rfc2616.txt. - if_unmodified_since = resource.header("if-unmodified-since", type=dict) + if_unmodified_since = resource.Header("if-unmodified-since", type=str) # Query parameters #: Used with temporary URLs to sign the request. For more #: information about temporary URLs, see OpenStack Object Storage #: API v1 Reference. - signature = resource.header("signature") + signature = resource.Header("signature") #: Used with temporary URLs to specify the expiry time of the #: signature. For more information about temporary URLs, see #: OpenStack Object Storage API v1 Reference. - expires_at = resource.header("expires") + expires_at = resource.Header("expires") #: If you include the multipart-manifest=get query parameter and #: the object is a large object, the object contents are not #: returned. Instead, the manifest is returned in the #: X-Object-Manifest response header for dynamic large objects #: or in the response body for static large objects. - multipart_manifest = resource.header("multipart-manifest") + multipart_manifest = resource.Header("multipart-manifest") # Response headers from HEAD and GET #: HEAD operations do not return content. However, in this #: operation the value in the Content-Length header is not the #: size of the response body. Instead it contains the size of #: the object, in bytes. - content_length = resource.header("content-length") + content_length = resource.Header( + "content-length", type=int, alias='_bytes') #: The MIME type of the object. - content_type = resource.header("content-type") + content_type = resource.Header("content-type", alias="_content_type") #: The type of ranges that the object accepts. - accept_ranges = resource.header("accept-ranges") + accept_ranges = resource.Header("accept-ranges") #: For objects smaller than 5 GB, this value is the MD5 checksum #: of the object content. The value is not quoted. #: For manifest objects, this value is the MD5 checksum of the @@ -110,46 +122,46 @@ class Object(_base.BaseResource): #: the response body as it is received and compare this value #: with the one in the ETag header. If they differ, the content #: was corrupted, so retry the operation. - etag = resource.header("etag") + etag = resource.Header("etag", alias='_hash') #: Set to True if this object is a static large object manifest object. #: *Type: bool* - is_static_large_object = resource.header("x-static-large-object", + is_static_large_object = resource.Header("x-static-large-object", type=bool) #: If set, the value of the Content-Encoding metadata. #: If not set, this header is not returned by this operation. - content_encoding = resource.header("content-encoding") + content_encoding = resource.Header("content-encoding") #: If set, specifies the override behavior for the browser. #: For example, this header might specify that the browser use #: a download program to save this file rather than show the file, #: which is the default. #: If not set, this header is not returned by this operation. - content_disposition = resource.header("content-disposition") + content_disposition = resource.Header("content-disposition") #: Specifies the number of seconds after which the object is #: removed. Internally, the Object Storage system stores this #: value in the X-Delete-At metadata item. - delete_after = resource.header("x-delete-after", type=int) + delete_after = resource.Header("x-delete-after", type=int) #: If set, the time when the object will be deleted by the system #: in the format of a UNIX Epoch timestamp. #: If not set, this header is not returned by this operation. - delete_at = resource.header("x-delete-at") + delete_at = resource.Header("x-delete-at") #: If set, to this is a dynamic large object manifest object. #: The value is the container and object name prefix of the #: segment objects in the form container/prefix. - object_manifest = resource.header("x-object-manifest") + object_manifest = resource.Header("x-object-manifest") #: The timestamp of the transaction. - timestamp = resource.header("x-timestamp") + timestamp = resource.Header("x-timestamp") #: The date and time that the object was created or the last #: time that the metadata was changed. - last_modified_at = resource.header("last_modified", alias="last-modified") + last_modified_at = resource.Header("last-modified", alias='_last_modified') # Headers for PUT and POST requests #: Set to chunked to enable chunked transfer encoding. If used, #: do not set the Content-Length header to a non-zero value. - transfer_encoding = resource.header("transfer-encoding") + transfer_encoding = resource.Header("transfer-encoding") #: If set to true, Object Storage guesses the content type based #: on the file extension and ignores the value sent in the #: Content-Type header, if present. *Type: bool* - is_content_type_detected = resource.header("x-detect-content-type", + is_content_type_detected = resource.Header("x-detect-content-type", type=bool) #: If set, this is the name of an object used to create the new #: object by copying the X-Copy-From object. The value is in form @@ -158,7 +170,13 @@ class Object(_base.BaseResource): #: in the header. #: Using PUT with X-Copy-From has the same effect as using the #: COPY operation to copy an object. - copy_from = resource.header("x-copy-from") + copy_from = resource.Header("x-copy-from") + + has_body = False + + def __init__(self, data=None, **attrs): + super(_base.BaseResource, self).__init__(**attrs) + self.data = data # The Object Store treats the metadata for its resources inconsistently so # Object.set_metadata must override the BaseResource.set_metadata to @@ -169,66 +187,111 @@ def set_metadata(self, session, metadata): filtered_metadata = \ {key: value for key, value in metadata.items() if value} + # Update from remote if we only have locally created information + if not self.last_modified_at: + self.head(session) + # Get a copy of the original metadata so it doesn't get erased on POST # and update it with the new metadata values. - obj = self.head(session) - metadata2 = copy.deepcopy(obj.metadata) - metadata2.update(filtered_metadata) + metadata = copy.deepcopy(self.metadata) + metadata.update(filtered_metadata) # Include any original system metadata so it doesn't get erased on POST for key in self._system_metadata: - value = getattr(obj, key) - if value and key not in metadata2: - metadata2[key] = value + value = getattr(self, key) + if value and key not in metadata: + metadata[key] = value - super(Object, self).set_metadata(session, metadata2) + request = self._prepare_request() + headers = self._calculate_headers(metadata) + response = session.post(request.url, headers=headers) + self._translate_response(response, has_body=False) + self.metadata.update(metadata) + + return self # The Object Store treats the metadata for its resources inconsistently so # Object.delete_metadata must override the BaseResource.delete_metadata to # account for it. def delete_metadata(self, session, keys): - # Get a copy of the original metadata so it doesn't get erased on POST - # and update it with the new metadata values. - obj = self.head(session) - metadata = copy.deepcopy(obj.metadata) + if not keys: + return + # If we have an empty object, update it from the remote side so that + # we have a copy of the original metadata. Deleting metadata requires + # POSTing and overwriting all of the metadata. If we already have + # metadata locally, assume this is an existing object. + if not self.metadata: + self.head(session) + + metadata = copy.deepcopy(self.metadata) # Include any original system metadata so it doesn't get erased on POST for key in self._system_metadata: - value = getattr(obj, key) + value = getattr(self, key) if value: metadata[key] = value - # Remove the metadata + # Remove the requested metadata keys + # TODO(mordred) Why don't we just look at self._header_mapping() + # instead of having system_metadata? + deleted = False + attr_keys_to_delete = set() for key in keys: if key == 'delete_after': del(metadata['delete_at']) else: - del(metadata[key]) + if key in metadata: + del(metadata[key]) + # Delete the attribute from the local copy of the object. + # Metadata that doesn't have Component attributes is + # handled by self.metadata being reset when we run + # self.head + if hasattr(self, key): + attr_keys_to_delete.add(key) + deleted = True + + # Nothing to delete, skip the POST + if not deleted: + return self + + request = self._prepare_request() + response = session.post( + request.url, headers=self._calculate_headers(metadata)) + exceptions.raise_from_response( + response, error_message="Error deleting metadata keys") + + # Only delete from local object if the remote delete was successful + for key in attr_keys_to_delete: + delattr(self, key) - url = self._get_url(self, self.id) - session.post(url, - headers=self._calculate_headers(metadata)) + # Just update ourselves from remote again. + return self.head(session) - def get(self, session, include_headers=False, args=None, - error_message=None): - url = self._get_url(self, self.id) - headers = {'Accept': 'bytes'} - resp = session.get(url, headers=headers, error_message=error_message) - resp = resp.content - self._set_metadata() - return resp + def _download(self, session, error_message=None, stream=False): + request = self._prepare_request() + request.headers['Accept'] = 'bytes' + + response = session.get( + request.url, headers=request.headers, stream=stream) + exceptions.raise_from_response(response, error_message=error_message) + return response + + def download(self, session, error_message=None): + response = self._download(session, error_message=error_message) + return response.content + + def stream(self, session, error_message=None, chunk_size=1024): + response = self._download( + session, error_message=error_message, stream=True) + return response.iter_content(chunk_size, decode_unicode=False) def create(self, session): - url = self._get_url(self, self.id) - - headers = self.get_headers() - headers['Accept'] = '' - if self.data is not None: - resp = session.put(url, - data=self.data, - headers=headers).headers - else: - resp = session.post(url, data=None, - headers=headers).headers - self.set_headers(resp) + request = self._prepare_request() + request.headers['Accept'] = '' + + response = session.put( + request.url, + data=self.data, + headers=request.headers) + self._translate_response(response, has_body=False) return self diff --git a/openstack/resource2.py b/openstack/resource2.py index 55cdf546c..b40afa9c0 100644 --- a/openstack/resource2.py +++ b/openstack/resource2.py @@ -34,6 +34,8 @@ class that represent a remote resource. The attributes that import collections import itertools +from requests import structures + from openstack import exceptions from openstack import format from openstack import utils @@ -44,7 +46,8 @@ class _BaseComponent(object): # The name this component is being tracked as in the Resource key = None - def __init__(self, name, type=None, default=None, alternate_id=False): + def __init__(self, name, type=None, default=None, alias=None, + alternate_id=False, **kwargs): """A typed descriptor for a component that makes up a Resource :param name: The name this component exists as on the server @@ -53,6 +56,7 @@ def __init__(self, name, type=None, default=None, alternate_id=False): will work. If you specify type=dict and then set a component to a string, __set__ will fail, for example. :param default: Typically None, but any other default can be set. + :param alias: If set, alternative attribute on object to return. :param alternate_id: When `True`, this property is known internally as a value that can be sent with requests that require an ID but @@ -63,6 +67,7 @@ def __init__(self, name, type=None, default=None, alternate_id=False): self.name = name self.type = type self.default = default + self.alias = alias self.alternate_id = alternate_id def __get__(self, instance, owner): @@ -74,6 +79,8 @@ def __get__(self, instance, owner): try: value = attributes[self.name] except KeyError: + if self.alias: + return getattr(instance, self.alias) return self.default # self.type() should not be called on None objects. @@ -253,6 +260,11 @@ class Resource(object): #: Method for creating a resource (POST, PUT) create_method = "POST" + #: Do calls for this resource require an id + requires_id = True + #: Do responses for this resource have bodies + has_body = True + def __init__(self, _synchronized=False, **attrs): """The base resource @@ -331,12 +343,13 @@ def _collect_attrs(self, attrs): attributes that exist on this class. """ body = self._consume_attrs(self._body_mapping(), attrs) - header = self._consume_attrs(self._header_mapping(), attrs) + header = self._consume_attrs( + self._header_mapping(), attrs, insensitive=True) uri = self._consume_attrs(self._uri_mapping(), attrs) return body, header, uri - def _consume_attrs(self, mapping, attrs): + def _consume_attrs(self, mapping, attrs, insensitive=False): """Given a mapping and attributes, return relevant matches This method finds keys in attrs that exist in the mapping, then @@ -347,16 +360,29 @@ def _consume_attrs(self, mapping, attrs): same source dict several times. """ relevant_attrs = {} + if insensitive: + relevant_attrs = structures.CaseInsensitiveDict() consumed_keys = [] + nonce = object() + # TODO(mordred) Invert the loop - loop over mapping, look in attrs + # and we should be able to simplify the logic, since CID should + # handle the case matching for key in attrs: - if key in mapping: + value = mapping.get(key, nonce) + if value is not nonce: # Convert client-side key names into server-side. relevant_attrs[mapping[key]] = attrs[key] consumed_keys.append(key) - elif key in mapping.values(): + else: # Server-side names can be stored directly. - relevant_attrs[key] = attrs[key] - consumed_keys.append(key) + search_key = key + values = mapping.values() + if insensitive: + search_key = search_key.lower() + values = [v.lower() for v in values] + if search_key in values: + relevant_attrs[key] = attrs[key] + consumed_keys.append(key) for key in consumed_keys: attrs.pop(key) @@ -366,6 +392,10 @@ def _consume_attrs(self, mapping, attrs): @classmethod def _get_mapping(cls, component): """Return a dict of attributes of a given component on the class""" + # TODO(mordred) Invert this mapping, it should be server-side to local. + # The reason for that is that headers are case insensitive, whereas + # our local values are case sensitive. If we invert this dict, we can + # rely on CaseInsensitiveDict when doing comparisons. mapping = {} # Since we're looking at class definitions we need to include # subclasses, so check the whole MRO. @@ -386,7 +416,8 @@ def _body_mapping(cls): @classmethod def _header_mapping(cls): """Return all Header members of this class""" - return cls._get_mapping(Header) + # TODO(mordred) this isn't helpful until we invert the dict + return structures.CaseInsensitiveDict(cls._get_mapping(Header)) @classmethod def _uri_mapping(cls): @@ -501,7 +532,7 @@ def to_dict(self, body=True, headers=True, ignore_none=False): return mapping - def _prepare_request(self, requires_id=True, prepend_key=False): + def _prepare_request(self, requires_id=None, prepend_key=False): """Prepare a request to be sent to the server Create operations don't require an ID, but all others do, @@ -515,11 +546,20 @@ def _prepare_request(self, requires_id=True, prepend_key=False): as well a body and headers that are ready to send. Only dirty body and header contents will be returned. """ + if requires_id is None: + requires_id = self.requires_id + body = self._body.dirty if prepend_key and self.resource_key is not None: body = {self.resource_key: body} - headers = self._header.dirty + # TODO(mordred) Ensure headers have string values better than this + headers = {} + for k, v in self._header.dirty.items(): + if isinstance(v, list): + headers[k] = ", ".join(v) + else: + headers[k] = str(v) uri = self.base_path % self._uri.attributes if requires_id: @@ -539,7 +579,7 @@ def _filter_component(self, component, mapping): """ return {k: v for k, v in component.items() if k in mapping.values()} - def _translate_response(self, response, has_body=True, error_message=None): + def _translate_response(self, response, has_body=None, error_message=None): """Given a KSA response, inflate this instance with its data DELETE operations don't return a body, so only try to work @@ -548,6 +588,8 @@ def _translate_response(self, response, has_body=True, error_message=None): This method updates attributes that correspond to headers and body on this instance and clears the dirty set. """ + if has_body is None: + has_body = self.has_body exceptions.raise_from_response(response, error_message=error_message) if has_body: body = response.json() @@ -560,6 +602,8 @@ def _translate_response(self, response, has_body=True, error_message=None): headers = self._filter_component(response.headers, self._header_mapping()) + headers = self._consume_attrs( + self._header_mapping(), response.headers.copy(), insensitive=True) self._header.attributes.update(headers) self._header.clean() @@ -637,7 +681,7 @@ def head(self, session): response = session.head(request.url, headers={"Accept": ""}) - self._translate_response(response) + self._translate_response(response, has_body=False) return self def update(self, session, prepend_key=True, has_body=True): diff --git a/openstack/tests/functional/object_store/v1/test_obj.py b/openstack/tests/functional/object_store/v1/test_obj.py index 534b0dd22..fdbefcf6f 100644 --- a/openstack/tests/functional/object_store/v1/test_obj.py +++ b/openstack/tests/functional/object_store/v1/test_obj.py @@ -36,11 +36,11 @@ def test_list(self): in self.conn.object_store.objects(container=self.FOLDER)] self.assertIn(self.FILE, names) - def test_get_object(self): - result = self.conn.object_store.get_object( + def test_download_object(self): + result = self.conn.object_store.download_object( self.FILE, container=self.FOLDER) self.assertEqual(self.DATA, result) - result = self.conn.object_store.get_object(self.sot) + result = self.conn.object_store.download_object(self.sot) self.assertEqual(self.DATA, result) def test_system_metadata(self): diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index d2caed078..1905ba9b4 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -611,6 +611,13 @@ def __do_register_uris(self, uri_mock_list=None): mock_method, mock_uri, params['response_list'], **params['kw_params']) + def assert_no_calls(self): + # TODO(mordred) For now, creating the adapter for self.conn is + # triggering catalog lookups. Make sure no_calls is only 2. + # When we can make that on-demand through a descriptor object, + # drop this to 0. + self.assertEqual(2, len(self.adapter.request_history)) + def assert_calls(self, stop_after=None, do_count=True): for (x, (call, history)) in enumerate( zip(self.calls, self.adapter.request_history)): diff --git a/openstack/tests/unit/object_store/v1/test_account.py b/openstack/tests/unit/object_store/v1/test_account.py index e0df5390a..f498fa41b 100644 --- a/openstack/tests/unit/object_store/v1/test_account.py +++ b/openstack/tests/unit/object_store/v1/test_account.py @@ -32,20 +32,20 @@ class TestAccount(testtools.TestCase): def test_basic(self): - sot = account.Account.new(**ACCOUNT_EXAMPLE) + sot = account.Account(**ACCOUNT_EXAMPLE) self.assertIsNone(sot.resources_key) self.assertIsNone(sot.id) self.assertEqual('/', sot.base_path) self.assertEqual('object-store', sot.service.service_type) self.assertTrue(sot.allow_update) self.assertTrue(sot.allow_head) - self.assertTrue(sot.allow_retrieve) + self.assertTrue(sot.allow_get) self.assertFalse(sot.allow_delete) self.assertFalse(sot.allow_list) self.assertFalse(sot.allow_create) def test_make_it(self): - sot = account.Account.new(**{'headers': ACCOUNT_EXAMPLE}) + sot = account.Account(**ACCOUNT_EXAMPLE) self.assertIsNone(sot.id) self.assertEqual(int(ACCOUNT_EXAMPLE['x-account-bytes-used']), sot.account_bytes_used) diff --git a/openstack/tests/unit/object_store/v1/test_container.py b/openstack/tests/unit/object_store/v1/test_container.py index c18c49421..5c5681c1e 100644 --- a/openstack/tests/unit/object_store/v1/test_container.py +++ b/openstack/tests/unit/object_store/v1/test_container.py @@ -10,125 +10,123 @@ # License for the specific language governing permissions and limitations # under the License. -import mock -import testtools - from openstack.object_store.v1 import container +from openstack.tests.unit import base -CONTAINER_NAME = "mycontainer" - -CONT_EXAMPLE = { - "count": 999, - "bytes": 12345, - "name": CONTAINER_NAME -} - -HEAD_EXAMPLE = { - 'content-length': '346', - 'x-container-object-count': '2', - 'accept-ranges': 'bytes', - 'id': 'tx1878fdc50f9b4978a3fdc-0053c31462', - 'date': 'Sun, 13 Jul 2014 23:21:06 GMT', - 'x-container-read': 'read-settings', - 'x-container-write': 'write-settings', - 'x-container-sync-to': 'sync-to', - 'x-container-sync-key': 'sync-key', - 'x-container-bytes-used': '630666', - 'x-versions-location': 'versions-location', - 'content-type': 'application/json; charset=utf-8', - 'x-timestamp': '1453414055.48672' -} - -LIST_EXAMPLE = [ - { - "count": 999, - "bytes": 12345, - "name": "container1" - }, - { - "count": 888, - "bytes": 54321, - "name": "container2" - } -] - - -class TestContainer(testtools.TestCase): +class TestContainer(base.RequestsMockTestCase): def setUp(self): super(TestContainer, self).setUp() - self.resp = mock.Mock() - self.resp.body = {} - self.resp.json = mock.Mock(return_value=self.resp.body) - self.resp.headers = {"X-Trans-Id": "abcdef"} - self.sess = mock.Mock() - self.sess.put = mock.Mock(return_value=self.resp) - self.sess.post = mock.Mock(return_value=self.resp) + self.container = self.getUniqueString() + self.endpoint = self.conn.object_store.get_endpoint() + '/' + self.container_endpoint = '{endpoint}{container}'.format( + endpoint=self.endpoint, container=self.container) + + self.body = { + "count": 2, + "bytes": 630666, + "name": self.container, + } + + self.headers = { + 'x-container-object-count': '2', + 'x-container-read': 'read-settings', + 'x-container-write': 'write-settings', + 'x-container-sync-to': 'sync-to', + 'x-container-sync-key': 'sync-key', + 'x-container-bytes-used': '630666', + 'x-versions-location': 'versions-location', + 'content-type': 'application/json; charset=utf-8', + 'x-timestamp': '1453414055.48672' + } + self.body_plus_headers = dict(self.body, **self.headers) def test_basic(self): - sot = container.Container.new(**CONT_EXAMPLE) + sot = container.Container.new(**self.body) self.assertIsNone(sot.resources_key) - self.assertEqual('name', sot.id_attribute) + self.assertEqual('name', sot._alternate_id()) self.assertEqual('/', sot.base_path) self.assertEqual('object-store', sot.service.service_type) self.assertTrue(sot.allow_update) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_retrieve) + self.assertTrue(sot.allow_get) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) self.assertTrue(sot.allow_head) + self.assert_no_calls() def test_make_it(self): - sot = container.Container.new(**CONT_EXAMPLE) - self.assertEqual(CONT_EXAMPLE['name'], sot.id) - self.assertEqual(CONT_EXAMPLE['name'], sot.name) - self.assertEqual(CONT_EXAMPLE['count'], sot.count) - self.assertEqual(CONT_EXAMPLE['bytes'], sot.bytes) + sot = container.Container.new(**self.body) + self.assertEqual(self.body['name'], sot.id) + self.assertEqual(self.body['name'], sot.name) + self.assertEqual(self.body['count'], sot.count) + self.assertEqual(self.body['count'], sot.object_count) + self.assertEqual(self.body['bytes'], sot.bytes) + self.assertEqual(self.body['bytes'], sot.bytes_used) + self.assert_no_calls() def test_create_and_head(self): - sot = container.Container(CONT_EXAMPLE) - - # Update container with HEAD data - sot._attrs.update({'headers': HEAD_EXAMPLE}) + sot = container.Container(**self.body_plus_headers) # Attributes from create - self.assertEqual(CONT_EXAMPLE['name'], sot.id) - self.assertEqual(CONT_EXAMPLE['name'], sot.name) - self.assertEqual(CONT_EXAMPLE['count'], sot.count) - self.assertEqual(CONT_EXAMPLE['bytes'], sot.bytes) + self.assertEqual(self.body_plus_headers['name'], sot.id) + self.assertEqual(self.body_plus_headers['name'], sot.name) + self.assertEqual(self.body_plus_headers['count'], sot.count) + self.assertEqual(self.body_plus_headers['bytes'], sot.bytes) # Attributes from header - self.assertEqual(int(HEAD_EXAMPLE['x-container-object-count']), - sot.object_count) - self.assertEqual(int(HEAD_EXAMPLE['x-container-bytes-used']), - sot.bytes_used) - self.assertEqual(HEAD_EXAMPLE['x-container-read'], - sot.read_ACL) - self.assertEqual(HEAD_EXAMPLE['x-container-write'], - sot.write_ACL) - self.assertEqual(HEAD_EXAMPLE['x-container-sync-to'], - sot.sync_to) - self.assertEqual(HEAD_EXAMPLE['x-container-sync-key'], - sot.sync_key) - self.assertEqual(HEAD_EXAMPLE['x-versions-location'], - sot.versions_location) - self.assertEqual(HEAD_EXAMPLE['x-timestamp'], sot.timestamp) - - @mock.patch("openstack.resource.Resource.list") - def test_list(self, fake_list): - fake_val = [container.Container.existing(**ex) for ex in LIST_EXAMPLE] - fake_list.return_value = fake_val - - # Since the list method is mocked out, just pass None for the session. - response = container.Container.list(None) - - self.assertEqual(len(LIST_EXAMPLE), len(response)) - for item in range(len(response)): - self.assertEqual(container.Container, type(response[item])) - self.assertEqual(LIST_EXAMPLE[item]["name"], response[item].name) - self.assertEqual(LIST_EXAMPLE[item]["count"], response[item].count) - self.assertEqual(LIST_EXAMPLE[item]["bytes"], response[item].bytes) + self.assertEqual( + int(self.body_plus_headers['x-container-object-count']), + sot.object_count) + self.assertEqual( + int(self.body_plus_headers['x-container-bytes-used']), + sot.bytes_used) + self.assertEqual( + self.body_plus_headers['x-container-read'], + sot.read_ACL) + self.assertEqual( + self.body_plus_headers['x-container-write'], + sot.write_ACL) + self.assertEqual( + self.body_plus_headers['x-container-sync-to'], + sot.sync_to) + self.assertEqual( + self.body_plus_headers['x-container-sync-key'], + sot.sync_key) + self.assertEqual( + self.body_plus_headers['x-versions-location'], + sot.versions_location) + self.assertEqual(self.body_plus_headers['x-timestamp'], sot.timestamp) + + def test_list(self): + containers = [ + { + "count": 999, + "bytes": 12345, + "name": "container1" + }, + { + "count": 888, + "bytes": 54321, + "name": "container2" + } + ] + self.register_uris([ + dict(method='GET', uri=self.endpoint, + json=containers) + ]) + + response = container.Container.list(self.conn.object_store) + + self.assertEqual(len(containers), len(list(response))) + for index, item in enumerate(response): + self.assertEqual(container.Container, type(item)) + self.assertEqual(containers[index]["name"], item.name) + self.assertEqual(containers[index]["count"], item.count) + self.assertEqual(containers[index]["bytes"], item.bytes) + + self.assert_calls() def _test_create_update(self, sot, sot_call, sess_method): sot.read_ACL = "some ACL" @@ -137,35 +135,43 @@ def _test_create_update(self, sot, sot_call, sess_method): headers = { "x-container-read": "some ACL", "x-container-write": "another ACL", - "x-detect-content-type": True, - "Accept": "", + "x-detect-content-type": 'True', } - sot_call(self.sess) + self.register_uris([ + dict(method=sess_method, uri=self.container_endpoint, + json=self.body, + validate=dict(headers=headers)), + ]) + sot_call(self.conn.object_store) - url = "/%s" % CONTAINER_NAME - sess_method.assert_called_with(url, - headers=headers) + self.assert_calls() def test_create(self): - sot = container.Container.new(name=CONTAINER_NAME) - self._test_create_update(sot, sot.create, self.sess.put) + sot = container.Container.new(name=self.container) + self._test_create_update(sot, sot.create, 'PUT') def test_update(self): - sot = container.Container.new(name=CONTAINER_NAME) - self._test_create_update(sot, sot.update, self.sess.post) + sot = container.Container.new(name=self.container) + self._test_create_update(sot, sot.update, 'POST') def _test_no_headers(self, sot, sot_call, sess_method): - sot = container.Container.new(name=CONTAINER_NAME) - sot.create(self.sess) - url = "/%s" % CONTAINER_NAME - headers = {'Accept': ''} - self.sess.put.assert_called_with(url, - headers=headers) + headers = {} + data = {} + self.register_uris([ + dict(method=sess_method, uri=self.container_endpoint, + json=self.body, + validate=dict( + headers=headers, + json=data)) + ]) + sot_call(self.conn.object_store) def test_create_no_headers(self): - sot = container.Container.new(name=CONTAINER_NAME) - self._test_no_headers(sot, sot.create, self.sess.put) + sot = container.Container.new(name=self.container) + self._test_no_headers(sot, sot.create, 'PUT') + self.assert_calls() def test_update_no_headers(self): - sot = container.Container.new(name=CONTAINER_NAME) - self._test_no_headers(sot, sot.update, self.sess.post) + sot = container.Container.new(name=self.container) + self._test_no_headers(sot, sot.update, 'POST') + self.assert_no_calls() diff --git a/openstack/tests/unit/object_store/v1/test_obj.py b/openstack/tests/unit/object_store/v1/test_obj.py index f3dfb793f..85eedc59d 100644 --- a/openstack/tests/unit/object_store/v1/test_obj.py +++ b/openstack/tests/unit/object_store/v1/test_obj.py @@ -10,14 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -import mock -import testtools - from openstack.object_store.v1 import obj - - -CONTAINER_NAME = "mycontainer" -OBJECT_NAME = "myobject" +from openstack.tests.unit.cloud import test_object as base_test_object # Object can receive both last-modified in headers and last_modified in # the body. However, originally, only last-modified was handled as an @@ -30,109 +24,127 @@ # attribute which would follow the same pattern. # This example should represent the body values returned by a GET, so the keys # must be underscores. -OBJ_EXAMPLE = { - "hash": "243f87b91224d85722564a80fd3cb1f1", - "last_modified": "2014-07-13T18:41:03.319240", - "bytes": 252466, - "name": OBJECT_NAME, - "content_type": "application/octet-stream" -} - -DICT_EXAMPLE = { - 'container': CONTAINER_NAME, - 'name': OBJECT_NAME, - 'content_type': 'application/octet-stream', - 'headers': { - 'content-length': '252466', - 'accept-ranges': 'bytes', - 'last-modified': 'Sun, 13 Jul 2014 18:41:04 GMT', - 'etag': '243f87b91224d85722564a80fd3cb1f1', - 'x-timestamp': '1453414256.28112', - 'date': 'Thu, 28 Aug 2014 14:41:59 GMT', - 'id': 'tx5fb5ad4f4d0846c6b2bc7-0053ff3fb7', - 'x-delete-at': '1453416226.16744' - } -} - - -class TestObject(testtools.TestCase): + + +class TestObject(base_test_object.BaseTestObject): def setUp(self): super(TestObject, self).setUp() - self.resp = mock.Mock() - self.resp.content = "lol here's some content" - self.resp.headers = {"X-Trans-Id": "abcdef"} - self.sess = mock.Mock() - self.sess.get = mock.Mock(return_value=self.resp) - self.sess.put = mock.Mock(return_value=self.resp) - self.sess.post = mock.Mock(return_value=self.resp) + self.the_data = b'test body' + self.the_data_length = len(self.the_data) + # TODO(mordred) Make the_data be from getUniqueString and then + # have hash and etag be actual md5 sums of that string + self.body = { + "hash": "243f87b91224d85722564a80fd3cb1f1", + "last_modified": "2014-07-13T18:41:03.319240", + "bytes": self.the_data_length, + "name": self.object, + "content_type": "application/octet-stream" + } + self.headers = { + 'Content-Length': str(len(self.the_data)), + 'Content-Type': 'application/octet-stream', + 'Accept-Ranges': 'bytes', + 'Last-Modified': 'Thu, 15 Dec 2016 13:34:14 GMT', + 'Etag': '"b5c454b44fbd5344793e3fb7e3850768"', + 'X-Timestamp': '1481808853.65009', + 'X-Trans-Id': 'tx68c2a2278f0c469bb6de1-005857ed80dfw1', + 'Date': 'Mon, 19 Dec 2016 14:24:00 GMT', + 'X-Static-Large-Object': 'True', + 'X-Object-Meta-Mtime': '1481513709.168512', + 'X-Delete-At': '1453416226.16744', + } def test_basic(self): - sot = obj.Object.new(**OBJ_EXAMPLE) + sot = obj.Object.new(**self.body) + self.assert_no_calls() self.assertIsNone(sot.resources_key) - self.assertEqual("name", sot.id_attribute) + self.assertEqual('name', sot._alternate_id()) self.assertEqual('/%(container)s', sot.base_path) self.assertEqual('object-store', sot.service.service_type) self.assertTrue(sot.allow_update) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_retrieve) + self.assertTrue(sot.allow_get) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) self.assertTrue(sot.allow_head) def test_new(self): - sot = obj.Object.new(container=CONTAINER_NAME, name=OBJECT_NAME) - self.assertEqual(OBJECT_NAME, sot.name) - self.assertEqual(CONTAINER_NAME, sot.container) + sot = obj.Object.new(container=self.container, name=self.object) + self.assert_no_calls() + self.assertEqual(self.object, sot.name) + self.assertEqual(self.container, sot.container) + + def test_from_body(self): + sot = obj.Object.existing(container=self.container, **self.body) + self.assert_no_calls() - def test_head(self): - sot = obj.Object.existing(**DICT_EXAMPLE) + # Attributes from header + self.assertEqual(self.container, sot.container) + self.assertEqual( + int(self.body['bytes']), sot.content_length) + self.assertEqual(self.body['last_modified'], sot.last_modified_at) + self.assertEqual(self.body['hash'], sot.etag) + self.assertEqual(self.body['content_type'], sot.content_type) + + def test_from_headers(self): + sot = obj.Object.existing(container=self.container, **self.headers) + self.assert_no_calls() # Attributes from header - self.assertEqual(DICT_EXAMPLE['container'], sot.container) - headers = DICT_EXAMPLE['headers'] - self.assertEqual(headers['content-length'], sot.content_length) - self.assertEqual(headers['accept-ranges'], sot.accept_ranges) - self.assertEqual(headers['last-modified'], sot.last_modified_at) - self.assertEqual(headers['etag'], sot.etag) - self.assertEqual(headers['x-timestamp'], sot.timestamp) - self.assertEqual(headers['content-type'], sot.content_type) - self.assertEqual(headers['x-delete-at'], sot.delete_at) - - def test_get(self): - sot = obj.Object.new(container=CONTAINER_NAME, name=OBJECT_NAME) + self.assertEqual(self.container, sot.container) + self.assertEqual( + int(self.headers['Content-Length']), sot.content_length) + self.assertEqual(self.headers['Accept-Ranges'], sot.accept_ranges) + self.assertEqual(self.headers['Last-Modified'], sot.last_modified_at) + self.assertEqual(self.headers['Etag'], sot.etag) + self.assertEqual(self.headers['X-Timestamp'], sot.timestamp) + self.assertEqual(self.headers['Content-Type'], sot.content_type) + self.assertEqual(self.headers['X-Delete-At'], sot.delete_at) + + def test_download(self): + headers = { + 'X-Newest': 'True', + 'If-Match': self.headers['Etag'], + 'Accept': 'bytes' + } + self.register_uris([ + dict(method='GET', uri=self.object_endpoint, + headers=self.headers, + content=self.the_data, + validate=dict( + headers=headers + )) + ]) + sot = obj.Object.new(container=self.container, name=self.object) sot.is_newest = True - sot.if_match = {"who": "what"} - - rv = sot.get(self.sess) - - url = "%s/%s" % (CONTAINER_NAME, OBJECT_NAME) - # TODO(thowe): Should allow filtering bug #1488269 - # headers = { - # "x-newest": True, - # "if-match": {"who": "what"} - # } - headers = {'Accept': 'bytes'} - self.sess.get.assert_called_with(url, - headers=headers, - error_message=None) - self.assertEqual(self.resp.content, rv) - - def _test_create(self, method, data, accept): - sot = obj.Object.new(container=CONTAINER_NAME, name=OBJECT_NAME, + sot.if_match = [self.headers['Etag']] + + rv = sot.download(self.conn.object_store) + + self.assertEqual(self.the_data, rv) + + self.assert_calls() + + def _test_create(self, method, data): + sot = obj.Object.new(container=self.container, name=self.object, data=data) sot.is_newest = True - headers = {"x-newest": True, "Accept": ""} + sent_headers = {"x-newest": 'True', "Accept": ""} + self.register_uris([ + dict(method=method, uri=self.object_endpoint, + headers=self.headers, + validate=dict( + headers=sent_headers)) + ]) - rv = sot.create(self.sess) + rv = sot.create(self.conn.object_store) + self.assertEqual(rv.etag, self.headers['Etag']) - url = "%s/%s" % (CONTAINER_NAME, OBJECT_NAME) - method.assert_called_with(url, data=data, - headers=headers) - self.assertEqual(self.resp.headers, rv.get_headers()) + self.assert_calls() def test_create_data(self): - self._test_create(self.sess.put, "data", "bytes") + self._test_create('PUT', self.the_data) def test_create_no_data(self): - self._test_create(self.sess.post, None, None) + self._test_create('PUT', None) diff --git a/openstack/tests/unit/object_store/v1/test_proxy.py b/openstack/tests/unit/object_store/v1/test_proxy.py index 5fa85c76b..537f7b299 100644 --- a/openstack/tests/unit/object_store/v1/test_proxy.py +++ b/openstack/tests/unit/object_store/v1/test_proxy.py @@ -10,17 +10,19 @@ # License for the specific language governing permissions and limitations # under the License. -import mock import six from openstack.object_store.v1 import _proxy from openstack.object_store.v1 import account from openstack.object_store.v1 import container from openstack.object_store.v1 import obj -from openstack.tests.unit import test_proxy_base +from openstack.tests.unit.cloud import test_object as base_test_object +from openstack.tests.unit import test_proxy_base2 -class TestObjectStoreProxy(test_proxy_base.TestProxyBase): +class TestObjectStoreProxy(test_proxy_base2.TestProxyBase): + + kwargs_to_path_args = False def setUp(self): super(TestObjectStoreProxy, self).setUp() @@ -42,21 +44,26 @@ def test_container_delete_ignore(self): container.Container, True) def test_container_create_attrs(self): - self.verify_create(self.proxy.create_container, container.Container) + self.verify_create( + self.proxy.create_container, + container.Container, + method_args=['container_name'], + expected_kwargs={'name': 'container_name', "x": 1, "y": 2, "z": 3}) def test_object_metadata_get(self): self.verify_head(self.proxy.get_object_metadata, obj.Object, value="object", container="container") def _test_object_delete(self, ignore): - expected_kwargs = {"path_args": {"container": "name"}} - expected_kwargs["ignore_missing"] = ignore + expected_kwargs = { + "ignore_missing": ignore, + "container": "name", + } - self._verify2("openstack.proxy.BaseProxy._delete", + self._verify2("openstack.proxy2.BaseProxy._delete", self.proxy.delete_object, method_args=["resource"], - method_kwargs={"container": "name", - "ignore_missing": ignore}, + method_kwargs=expected_kwargs, expected_args=[obj.Object, "resource"], expected_kwargs=expected_kwargs) @@ -67,25 +74,24 @@ def test_object_delete_ignore(self): self._test_object_delete(True) def test_object_create_attrs(self): - path_args = {"path_args": {"container": "name"}} - method_kwargs = {"name": "test", "data": "data", "container": "name"} - - expected_kwargs = path_args.copy() - expected_kwargs.update(method_kwargs) - expected_kwargs.pop("container") + kwargs = {"name": "test", "data": "data", "container": "name"} - self._verify2("openstack.proxy.BaseProxy._create", + self._verify2("openstack.proxy2.BaseProxy._create", self.proxy.upload_object, - method_kwargs=method_kwargs, + method_kwargs=kwargs, expected_args=[obj.Object], - expected_kwargs=expected_kwargs) + expected_kwargs=kwargs) def test_object_create_no_container(self): - self.assertRaises(ValueError, self.proxy.upload_object) + self.assertRaises(TypeError, self.proxy.upload_object) def test_object_get(self): - self.verify_get(self.proxy.get_object, obj.Object, - value=["object"], container="container") + kwargs = dict(container="container") + self.verify_get( + self.proxy.get_object, obj.Object, + value=["object"], + method_kwargs=kwargs, + expected_kwargs=kwargs) class Test_containers(TestObjectStoreProxy): @@ -252,23 +258,45 @@ def setUp(self): # httpretty.last_request().path) -class Test_download_object(TestObjectStoreProxy): +class Test_download_object(base_test_object.BaseTestObject): - @mock.patch("openstack.object_store.v1._proxy.Proxy.get_object") - def test_download(self, mock_get): - the_data = "here's some data" - mock_get.return_value = the_data - ob = mock.Mock() - - fake_open = mock.mock_open() - file_path = "blarga/somefile" - with mock.patch("openstack.object_store.v1._proxy.open", - fake_open, create=True): - self.proxy.download_object(ob, container="tainer", path=file_path) - - fake_open.assert_called_once_with(file_path, "w") - fake_handle = fake_open() - fake_handle.write.assert_called_once_with(the_data) + def setUp(self): + super(Test_download_object, self).setUp() + self.the_data = b'test body' + self.register_uris([ + dict(method='GET', uri=self.object_endpoint, + headers={ + 'Content-Length': str(len(self.the_data)), + 'Content-Type': 'application/octet-stream', + 'Accept-Ranges': 'bytes', + 'Last-Modified': 'Thu, 15 Dec 2016 13:34:14 GMT', + 'Etag': '"b5c454b44fbd5344793e3fb7e3850768"', + 'X-Timestamp': '1481808853.65009', + 'X-Trans-Id': 'tx68c2a2278f0c469bb6de1-005857ed80dfw1', + 'Date': 'Mon, 19 Dec 2016 14:24:00 GMT', + 'X-Static-Large-Object': 'True', + 'X-Object-Meta-Mtime': '1481513709.168512', + }, + content=self.the_data)]) + + def test_download(self): + data = self.conn.object_store.download_object( + self.object, container=self.container) + + self.assertEqual(data, self.the_data) + self.assert_calls() + + def test_stream(self): + chunk_size = 2 + for index, chunk in enumerate(self.conn.object_store.stream_object( + self.object, container=self.container, + chunk_size=chunk_size)): + chunk_len = len(chunk) + start = index * chunk_size + end = start + chunk_len + self.assertLessEqual(chunk_len, chunk_size) + self.assertEqual(chunk, self.the_data[start:end]) + self.assert_calls() class Test_copy_object(TestObjectStoreProxy): diff --git a/openstack/tests/unit/test_proxy_base2.py b/openstack/tests/unit/test_proxy_base2.py index 27e1c150d..24d300fa2 100644 --- a/openstack/tests/unit/test_proxy_base2.py +++ b/openstack/tests/unit/test_proxy_base2.py @@ -16,6 +16,11 @@ class TestProxyBase(base.TestCase): + # object_store makes calls with container= rather than + # path_args=dict(container= because container needs to wind up + # in the uri components. + kwargs_to_path_args = True + def setUp(self): super(TestProxyBase, self).setUp() self.session = mock.Mock() @@ -131,7 +136,7 @@ def verify_get(self, test_method, resource_type, value=None, args=None, method_kwargs = kwargs.pop("method_kwargs", kwargs) if args: expected_kwargs["args"] = args - if kwargs: + if kwargs and self.kwargs_to_path_args: expected_kwargs["path_args"] = kwargs if not expected_args: expected_args = [resource_type] + the_value @@ -145,7 +150,10 @@ def verify_head(self, test_method, resource_type, mock_method="openstack.proxy2.BaseProxy._head", value=None, **kwargs): the_value = [value] if value is not None else [] - expected_kwargs = {"path_args": kwargs} if kwargs else {} + if self.kwargs_to_path_args: + expected_kwargs = {"path_args": kwargs} if kwargs else {} + else: + expected_kwargs = kwargs or {} self._verify2(mock_method, test_method, method_args=the_value, method_kwargs=kwargs, diff --git a/openstack/tests/unit/test_resource2.py b/openstack/tests/unit/test_resource2.py index 4a8b845b6..b9d990cbe 100644 --- a/openstack/tests/unit/test_resource2.py +++ b/openstack/tests/unit/test_resource2.py @@ -852,10 +852,9 @@ def test__translate_response_no_body(self): class Test(resource2.Resource): attr = resource2.Header("attr") - response = FakeResponse({}) + response = FakeResponse({}, headers={"attr": "value"}) sot = Test() - sot._filter_component = mock.Mock(return_value={"attr": "value"}) sot._translate_response(response, has_body=False) @@ -1036,7 +1035,8 @@ def test_head(self): self.request.url, headers={"Accept": ""}) - self.sot._translate_response.assert_called_once_with(self.response) + self.sot._translate_response.assert_called_once_with( + self.response, has_body=False) self.assertEqual(result, self.sot) def _test_update(self, update_method='PUT', prepend_key=True, From 8cd35fbe77d3db4b44173dab644e8069037c0aa7 Mon Sep 17 00:00:00 2001 From: brandonzhao Date: Thu, 18 Jan 2018 16:46:08 +0800 Subject: [PATCH 1912/3836] change spell error change spell error of 'resource' word Change-Id: Ie100e6354ca79b4a37e85b49e3967f1b0bbbd50e --- doc/source/contributor/create/resource.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/contributor/create/resource.rst b/doc/source/contributor/create/resource.rst index f4552b53d..42a7607d3 100644 --- a/doc/source/contributor/create/resource.rst +++ b/doc/source/contributor/create/resource.rst @@ -35,7 +35,7 @@ Resources Resources are named after the server-side resource, which is set in the ``base_path`` attribute of the resource class. This guide creates a -resouce class for the ``/fake`` server resource, so the resource module +resource class for the ``/fake`` server resource, so the resource module is called ``fake.py`` and the class is called ``Fake``. An Example From 111a27becf0c44e6608f960178a730839c886f7a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 18 Jan 2018 14:15:32 -0600 Subject: [PATCH 1913/3836] Make sure we use config defaults in profile compat code The compat code to allow creating a Connection from a Profile has a couple of bugs. First of all it doesn't return the config object it creates. (whoops) Second, it doesn't build its config kwargs from a base of the config defaults, so some code in CloudRegion breaks. The second bug would also exist in from_session, although we haven't seen it in the field yet. Change-Id: Ibd0056e6484cfa9a4c8be6a8bc40a0eb2df76977 --- openstack/config/cloud_region.py | 7 +++++-- openstack/connection.py | 16 ++++++++++++---- openstack/tests/unit/test_connection.py | 16 ++++++++++++++++ 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index ffef7f60e..c9ba21ce0 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -23,6 +23,7 @@ import openstack from openstack import _log +from openstack.config import defaults as config_defaults from openstack.config import exceptions @@ -52,8 +53,10 @@ def from_session(session, name=None, config=None, **kwargs): # If someone is constructing one of these from a Session, then they are # not using a named config. Use the hostname of their auth_url instead. name = name or urllib.parse.urlparse(session.auth.auth_url).hostname - config = config or {} - return CloudRegion(name=name, session=session, config=config, **kwargs) + config_dict = config_defaults.get_defaults() + config_dict.update(config or {}) + return CloudRegion( + name=name, session=session, config=config_dict, **kwargs) class CloudRegion(object): diff --git a/openstack/connection.py b/openstack/connection.py index f5797b7df..489a16157 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -84,6 +84,7 @@ import openstack.config from openstack.config import cloud_region +from openstack.config import defaults as config_defaults from openstack import exceptions from openstack import proxy from openstack import proxy2 @@ -187,10 +188,14 @@ def __init__(self, cloud=None, config=None, session=None, self.config = openstack_config.get_one( cloud=cloud, validate=session is None, **kwargs) - self.task_manager = task_manager.TaskManager( - name=':'.join([ + if self.config.name: + tm_name = ':'.join([ self.config.name, - self.config.region_name or 'unknown'])) + self.config.region_name or 'unknown']) + else: + tm_name = self.config.region_name or 'unknown' + + self.task_manager = task_manager.TaskManager(name=tm_name) if session: # TODO(mordred) Expose constructor option for this in OCC @@ -223,9 +228,12 @@ def _get_config_from_profile(self, profile, authenticator, **kwargs): key = cloud_region._make_key('api_version', service_type) kwargs[key] = service.version + config_kwargs = config_defaults.get_defaults() + config_kwargs.update(kwargs) config = cloud_region.CloudRegion( - name=name, region_name=region_name, config=kwargs) + name=name, region_name=region_name, config=config_kwargs) config._auth = authenticator + return config def _open(self): """Open the connection. """ diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index cfbb698f9..3004dc260 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -18,6 +18,7 @@ from openstack import connection import openstack.config +from openstack import profile from openstack.tests.unit import base @@ -176,6 +177,21 @@ def test_from_config_verify(self): sot = connection.from_config(cloud="cacert") self.assertEqual(CONFIG_CACERT, sot.session.verify) + def test_from_profile(self): + """Copied from openstackclient/network/client.py make_client.""" + API_NAME = "network" + instance = self.cloud_config + + prof = profile.Profile() + prof.set_region(API_NAME, instance.region_name) + prof.set_version(API_NAME, instance.get_api_version(API_NAME)) + prof.set_interface(API_NAME, instance.get_interface(API_NAME)) + connection.Connection( + authenticator=instance.get_session().auth, + verify=instance.get_session().verify, + cert=instance.get_session().cert, + profile=prof) + class TestAuthorize(base.RequestsMockTestCase): From 34f1e046b47af3131ec7dbe71ef28ac1a4ef9590 Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Thu, 18 Jan 2018 22:01:33 +0800 Subject: [PATCH 1914/3836] Use version definition from openstack.version Having openstack/__init__.py referencing pbr is causing some problems sometimes when invoking sdk apis. This patch propose using the version definition from version module under openstack/. This is a follow up of 2f05a3d0667efa07004c71e13217f1ed6d95b198. Change-Id: I3117becd0e91514447a426f2fec4d133dd11f404 --- openstack/__init__.py | 4 ---- openstack/cloud/openstackcloud.py | 4 ++-- openstack/config/cloud_region.py | 4 ++-- openstack/tests/unit/config/base.py | 2 -- openstack/tests/unit/config/test_cloud_config.py | 7 ++++--- 5 files changed, 8 insertions(+), 13 deletions(-) diff --git a/openstack/__init__.py b/openstack/__init__.py index 3fd98a2a5..0a0be0faa 100644 --- a/openstack/__init__.py +++ b/openstack/__init__.py @@ -13,7 +13,6 @@ # limitations under the License. __all__ = [ - '__version__', 'connect', 'enable_logging', ] @@ -21,7 +20,6 @@ import warnings import keystoneauth1.exceptions -import pbr.version import requestsexceptions from openstack._log import enable_logging # noqa @@ -31,8 +29,6 @@ from openstack.cloud.operatorcloud import OperatorCloud import openstack.connection -__version__ = pbr.version.VersionInfo('openstacksdk').version_string() - if requestsexceptions.SubjectAltNameWarning: warnings.filterwarnings( 'ignore', category=requestsexceptions.SubjectAltNameWarning) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 4cb9380d7..60a70effd 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -34,7 +34,7 @@ import keystoneauth1.exceptions import keystoneauth1.session -import openstack +from openstack import version as openstack_version from openstack import _adapter from openstack import _log from openstack.cloud.exc import * # noqa @@ -691,7 +691,7 @@ def keystone_session(self): self._keystone_session = self.cloud_config.get_session() if hasattr(self._keystone_session, 'additional_user_agent'): self._keystone_session.additional_user_agent.append( - ('openstacksdk', openstack.__version__)) + ('openstacksdk', openstack_version.__version__)) except Exception as e: raise OpenStackCloudException( "Error authenticating to keystone: %s " % str(e)) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 7c2541caf..1a3666099 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -21,7 +21,7 @@ import requestsexceptions from six.moves import urllib -import openstack +from openstack import version as openstack_version from openstack import _log from openstack.config import exceptions @@ -212,7 +212,7 @@ def get_session(self): timeout=self.config['api_timeout']) if hasattr(self._keystone_session, 'additional_user_agent'): self._keystone_session.additional_user_agent.append( - ('openstacksdk', openstack.__version__)) + ('openstacksdk', openstack_version.__version__)) # Using old keystoneauth with new os-client-config fails if # we pass in app_name and app_version. Those are not essential, # nor a reason to bump our minimum, so just test for the session diff --git a/openstack/tests/unit/config/base.py b/openstack/tests/unit/config/base.py index 1a5fd4850..cd569a4fd 100644 --- a/openstack/tests/unit/config/base.py +++ b/openstack/tests/unit/config/base.py @@ -213,8 +213,6 @@ def setUp(self): self.secure_yaml = _write_yaml(SECURE_CONF) self.vendor_yaml = _write_yaml(VENDOR_CONF) self.no_yaml = _write_yaml(NO_CONF) - self.useFixture(fixtures.MonkeyPatch( - 'openstack.__version__', '1.2.3')) # Isolate the test runs from the environment # Do this as two loops because you can't modify the dict in a loop diff --git a/openstack/tests/unit/config/test_cloud_config.py b/openstack/tests/unit/config/test_cloud_config.py index cf389acad..191833e0a 100644 --- a/openstack/tests/unit/config/test_cloud_config.py +++ b/openstack/tests/unit/config/test_cloud_config.py @@ -16,6 +16,7 @@ from keystoneauth1 import session as ksa_session import mock +from openstack import version as openstack_version from openstack.config import cloud_region from openstack.config import defaults from openstack.config import exceptions @@ -194,7 +195,7 @@ def test_get_session(self, mock_session): verify=True, cert=None, timeout=None) self.assertEqual( fake_session.additional_user_agent, - [('openstacksdk', '1.2.3')]) + [('openstacksdk', openstack_version.__version__)]) @mock.patch.object(ksa_session, 'Session') def test_get_session_with_app_name(self, mock_session): @@ -216,7 +217,7 @@ def test_get_session_with_app_name(self, mock_session): self.assertEqual(fake_session.app_version, "test_version") self.assertEqual( fake_session.additional_user_agent, - [('openstacksdk', '1.2.3')]) + [('openstacksdk', openstack_version.__version__)]) @mock.patch.object(ksa_session, 'Session') def test_get_session_with_timeout(self, mock_session): @@ -234,7 +235,7 @@ def test_get_session_with_timeout(self, mock_session): verify=True, cert=None, timeout=9) self.assertEqual( fake_session.additional_user_agent, - [('openstacksdk', '1.2.3')]) + [('openstacksdk', openstack_version.__version__)]) @mock.patch.object(ksa_session, 'Session') def test_override_session_endpoint_override(self, mock_session): From e636890d59c92345fe96f8dfdc6baa3401f8bef8 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 18 Jan 2018 16:53:26 -0600 Subject: [PATCH 1915/3836] Move openstack_cloud helper functions We don't want these where they look like supported interfaces just yet. Move them below openstack.cloud. Change-Id: I653e5aaba8084e8c226fd0e787dfff01050e7a6c --- README.rst | 4 +- doc/source/user/multi-cloud-demo.rst | 36 ++++---- doc/source/user/usage.rst | 4 +- examples/cloud/cleanup-servers.py | 2 +- examples/cloud/create-server-dict.py | 2 +- examples/cloud/create-server-name-or-id.py | 2 +- examples/cloud/debug-logging.py | 2 +- examples/cloud/find-an-image.py | 2 +- examples/cloud/http-debug-logging.py | 2 +- examples/cloud/munch-dict-object.py | 2 +- examples/cloud/normalization.py | 2 +- openstack/__init__.py | 92 +------------------ openstack/cloud/__init__.py | 79 ++++++++++++++++ openstack/cloud/cmd/inventory.py | 2 +- openstack/cloud/inventory.py | 10 +- openstack/cloud/openstackcloud.py | 4 +- openstack/connection.py | 7 ++ openstack/tests/functional/cloud/base.py | 4 +- .../tests/functional/cloud/test_domain.py | 2 +- .../tests/functional/cloud/test_groups.py | 2 +- .../tests/functional/cloud/test_identity.py | 2 +- .../tests/functional/cloud/test_users.py | 2 +- openstack/tests/unit/base.py | 10 +- openstack/tests/unit/cloud/test_caching.py | 11 ++- .../unit/cloud/test_cluster_templates.py | 2 +- .../tests/unit/cloud/test_create_server.py | 10 +- .../tests/unit/cloud/test_domain_params.py | 16 ++-- openstack/tests/unit/cloud/test_domains.py | 6 +- openstack/tests/unit/cloud/test_flavors.py | 4 +- .../unit/cloud/test_floating_ip_common.py | 2 +- .../tests/unit/cloud/test_floating_ip_pool.py | 2 +- .../tests/unit/cloud/test_identity_roles.py | 4 +- openstack/tests/unit/cloud/test_image.py | 36 ++++---- openstack/tests/unit/cloud/test_inventory.py | 14 +-- openstack/tests/unit/cloud/test_meta.py | 40 ++++---- openstack/tests/unit/cloud/test_network.py | 4 +- openstack/tests/unit/cloud/test_object.py | 8 +- openstack/tests/unit/cloud/test_operator.py | 4 +- .../tests/unit/cloud/test_operator_noauth.py | 6 +- openstack/tests/unit/cloud/test_project.py | 4 +- .../tests/unit/cloud/test_security_groups.py | 12 +-- openstack/tests/unit/cloud/test_services.py | 2 +- openstack/tests/unit/cloud/test_shade.py | 6 +- openstack/tests/unit/cloud/test_stack.py | 8 +- openstack/tests/unit/cloud/test_users.py | 2 +- openstack/tests/unit/cloud/test_volume.py | 12 +-- .../tests/unit/cloud/test_volume_access.py | 2 +- 47 files changed, 245 insertions(+), 248 deletions(-) diff --git a/README.rst b/README.rst index 7079da180..11f125950 100644 --- a/README.rst +++ b/README.rst @@ -138,14 +138,14 @@ Create a server using objects configured with the ``clouds.yaml`` file: .. code-block:: python - import openstack + import openstack.cloud # Initialize and turn on debug logging openstack.enable_logging(debug=True) # Initialize cloud # Cloud configs are read with openstack.config - cloud = openstack.openstack_cloud(cloud='mordred') + cloud = openstack.cloud.openstack_cloud(cloud='mordred') # Upload an image to the cloud image = cloud.create_image( diff --git a/doc/source/user/multi-cloud-demo.rst b/doc/source/user/multi-cloud-demo.rst index aafd3a164..757796096 100644 --- a/doc/source/user/multi-cloud-demo.rst +++ b/doc/source/user/multi-cloud-demo.rst @@ -62,7 +62,7 @@ Complete Example .. code:: python - import openstack + from openstack import cloud as openstack # Initialize and turn on debug logging openstack.enable_logging(debug=True) @@ -314,7 +314,7 @@ Complete Example Again .. code:: python - import openstack + from openstack import cloud as openstack # Initialize and turn on debug logging openstack.enable_logging(debug=True) @@ -346,7 +346,7 @@ Import the library .. code:: python - import openstack + from openstack import cloud as openstack Logging ======= @@ -373,7 +373,7 @@ Example with Debug Logging .. code:: python - import openstack + from openstack import cloud as openstack openstack.enable_logging(debug=True) cloud = openstack.openstack_cloud( @@ -387,7 +387,7 @@ Example with HTTP Debug Logging .. code:: python - import openstack + from openstack import cloud as openstack openstack.enable_logging(http_debug=True) cloud = openstack.openstack_cloud( @@ -484,7 +484,7 @@ Image and Flavor by Name or ID .. code:: python - import openstack + from openstack import cloud as openstack # Initialize and turn on debug logging openstack.enable_logging(debug=True) @@ -531,7 +531,7 @@ Image and Flavor by Dict .. code:: python - import openstack + from openstack import cloud as openstack # Initialize and turn on debug logging openstack.enable_logging(debug=True) @@ -562,7 +562,7 @@ Munch Objects .. code:: python - import openstack + from openstack import cloud as openstack openstack.enable_logging(debug=True) cloud = openstack.openstack_cloud(cloud='zetta', region_name='no-osl1') @@ -594,7 +594,7 @@ Cleanup Script .. code:: python - import openstack + from openstack import cloud as openstack # Initialize and turn on debug logging openstack.enable_logging(debug=True) @@ -616,7 +616,7 @@ Normalization .. code:: python - import openstack + from openstack import cloud as openstack openstack.enable_logging() cloud = openstack.openstack_cloud(cloud='fuga', region_name='cystack') @@ -632,7 +632,7 @@ Strict Normalized Results .. code:: python - import openstack + from openstack import cloud as openstack openstack.enable_logging() cloud = openstack.openstack_cloud( @@ -649,7 +649,7 @@ How Did I Find the Image Name for the Last Example? .. code:: python - import openstack + from openstack import cloud as openstack openstack.enable_logging() cloud = openstack.openstack_cloud(cloud='fuga', region_name='cystack') @@ -670,7 +670,7 @@ Added / Modified Information .. code:: python - import openstack + from openstack import cloud as openstack openstack.enable_logging(debug=True) cloud = openstack.openstack_cloud(cloud='my-citycloud', region_name='Buf1') @@ -712,7 +712,7 @@ User Agent Info .. code:: python - import openstack + from openstack import cloud as openstack openstack.enable_logging(http_debug=True) cloud = openstack.openstack_cloud( @@ -730,7 +730,7 @@ Uploading Large Objects .. code:: python - import openstack + from openstack import cloud as openstack openstack.enable_logging(debug=True) cloud = openstack.openstack_cloud(cloud='ovh', region_name='SBG1') @@ -751,7 +751,7 @@ Uploading Large Objects .. code:: python - import openstack + from openstack import cloud as openstack openstack.enable_logging(debug=True) cloud = openstack.openstack_cloud(cloud='ovh', region_name='SBG1') @@ -767,7 +767,7 @@ Service Conditionals .. code:: python - import openstack + from openstack import cloud as openstack openstack.enable_logging(debug=True) cloud = openstack.openstack_cloud(cloud='kiss', region_name='region1') @@ -781,7 +781,7 @@ Service Conditional Overrides .. code:: python - import openstack + from openstack import cloud as openstack openstack.enable_logging(debug=True) cloud = openstack.openstack_cloud(cloud='rax', region_name='DFW') diff --git a/doc/source/user/usage.rst b/doc/source/user/usage.rst index 7ef63d511..f707c7964 100644 --- a/doc/source/user/usage.rst +++ b/doc/source/user/usage.rst @@ -15,8 +15,8 @@ To use `openstack.cloud` in a project: objects can be accessed using either dictionary or object notation (e.g., ``server.id``, ``image.name`` and ``server['id']``, ``image['name']``) -.. autoclass:: openstack.OpenStackCloud +.. autoclass:: openstack.cloud.OpenStackCloud :members: -.. autoclass:: openstack.OperatorCloud +.. autoclass:: openstack.cloud.OperatorCloud :members: diff --git a/examples/cloud/cleanup-servers.py b/examples/cloud/cleanup-servers.py index 7620f4c0d..89b7c16e4 100644 --- a/examples/cloud/cleanup-servers.py +++ b/examples/cloud/cleanup-servers.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import openstack +from openstack import cloud as openstack # Initialize and turn on debug logging openstack.enable_logging(debug=True) diff --git a/examples/cloud/create-server-dict.py b/examples/cloud/create-server-dict.py index 7b7505010..d8d31cc34 100644 --- a/examples/cloud/create-server-dict.py +++ b/examples/cloud/create-server-dict.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import openstack +from openstack import cloud as openstack # Initialize and turn on debug logging openstack.enable_logging(debug=True) diff --git a/examples/cloud/create-server-name-or-id.py b/examples/cloud/create-server-name-or-id.py index 170904a9d..a1fc1b95c 100644 --- a/examples/cloud/create-server-name-or-id.py +++ b/examples/cloud/create-server-name-or-id.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import openstack +from openstack import cloud as openstack # Initialize and turn on debug logging openstack.enable_logging(debug=True) diff --git a/examples/cloud/debug-logging.py b/examples/cloud/debug-logging.py index 0874e7b6c..4c9cad328 100644 --- a/examples/cloud/debug-logging.py +++ b/examples/cloud/debug-logging.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import openstack +from openstack import cloud as openstack openstack.enable_logging(debug=True) cloud = openstack.openstack_cloud( diff --git a/examples/cloud/find-an-image.py b/examples/cloud/find-an-image.py index 9be76817e..f15787378 100644 --- a/examples/cloud/find-an-image.py +++ b/examples/cloud/find-an-image.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import openstack +from openstack import cloud as openstack openstack.enable_logging() cloud = openstack.openstack_cloud(cloud='fuga', region_name='cystack') diff --git a/examples/cloud/http-debug-logging.py b/examples/cloud/http-debug-logging.py index 62b942532..5e67f4a38 100644 --- a/examples/cloud/http-debug-logging.py +++ b/examples/cloud/http-debug-logging.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import openstack +from openstack import cloud as openstack openstack.enable_logging(http_debug=True) cloud = openstack.openstack_cloud( diff --git a/examples/cloud/munch-dict-object.py b/examples/cloud/munch-dict-object.py index 5ba6e63ea..65568c112 100644 --- a/examples/cloud/munch-dict-object.py +++ b/examples/cloud/munch-dict-object.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import openstack +from openstack import cloud as openstack openstack.enable_logging(debug=True) cloud = openstack.openstack_cloud(cloud='ovh', region_name='SBG1') diff --git a/examples/cloud/normalization.py b/examples/cloud/normalization.py index 40a3748d3..914baef70 100644 --- a/examples/cloud/normalization.py +++ b/examples/cloud/normalization.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import openstack +from openstack import cloud as openstack openstack.enable_logging() cloud = openstack.openstack_cloud(cloud='fuga', region_name='cystack') diff --git a/openstack/__init__.py b/openstack/__init__.py index 0a0be0faa..58ed37874 100644 --- a/openstack/__init__.py +++ b/openstack/__init__.py @@ -17,97 +17,7 @@ 'enable_logging', ] -import warnings - -import keystoneauth1.exceptions -import requestsexceptions - from openstack._log import enable_logging # noqa -from openstack.cloud.exc import * # noqa -# TODO(shade) These two want to be removed before we make a release -from openstack.cloud.openstackcloud import OpenStackCloud -from openstack.cloud.operatorcloud import OperatorCloud import openstack.connection -if requestsexceptions.SubjectAltNameWarning: - warnings.filterwarnings( - 'ignore', category=requestsexceptions.SubjectAltNameWarning) - - -def _get_openstack_config(app_name=None, app_version=None): - import openstack.config - return openstack.config.OpenStackConfig( - app_name=app_name, app_version=app_version) - - -# TODO(shade) This wants to be remove before we make a release. -def openstack_clouds( - config=None, debug=False, cloud=None, strict=False, - app_name=None, app_version=None): - if not config: - config = _get_openstack_config(app_name, app_version) - try: - if cloud is None: - return [ - OpenStackCloud( - cloud=f.name, debug=debug, - cloud_config=cloud_region, - strict=strict) - for cloud_region in config.get_all() - ] - else: - return [ - OpenStackCloud( - cloud=f.name, debug=debug, - cloud_config=cloud_region, - strict=strict) - for cloud_region in config.get_all() - if cloud_region.name == cloud - ] - except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin as e: - raise OpenStackCloudException( - "Invalid cloud configuration: {exc}".format(exc=str(e))) - - -# TODO(shade) This wants to be removed before we make a release. -def openstack_cloud( - config=None, strict=False, app_name=None, app_version=None, **kwargs): - if not config: - config = _get_openstack_config(app_name, app_version) - try: - cloud_region = config.get_one(**kwargs) - except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin as e: - raise OpenStackCloudException( - "Invalid cloud configuration: {exc}".format(exc=str(e))) - return OpenStackCloud(cloud_config=cloud_region, strict=strict) - - -# TODO(shade) This wants to be removed before we make a release. -def operator_cloud( - config=None, strict=False, app_name=None, app_version=None, **kwargs): - if not config: - config = _get_openstack_config(app_name, app_version) - try: - cloud_region = config.get_one(**kwargs) - except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin as e: - raise OpenStackCloudException( - "Invalid cloud configuration: {exc}".format(exc=str(e))) - return OperatorCloud(cloud_config=cloud_region, strict=strict) - - -def connect(*args, **kwargs): - """Create a `openstack.connection.Connection`.""" - return openstack.connection.Connection(*args, **kwargs) - - -def connect_all(config=None, app_name=None, app_version=None): - if not config: - config = _get_openstack_config(app_name, app_version) - try: - return [ - openstack.connection.Connection(config=cloud_region) - for cloud_region in config.get_all() - ] - except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin as e: - raise OpenStackCloudException( - "Invalid cloud configuration: {exc}".format(exc=str(e))) +connect = openstack.connection.Connection diff --git a/openstack/cloud/__init__.py b/openstack/cloud/__init__.py index e69de29bb..a28fe467e 100644 --- a/openstack/cloud/__init__.py +++ b/openstack/cloud/__init__.py @@ -0,0 +1,79 @@ +# Copyright (c) 2014 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. + +import keystoneauth1.exceptions + +from openstack._log import enable_logging # noqa +from openstack.cloud.exc import * # noqa +from openstack.cloud.openstackcloud import OpenStackCloud +from openstack.cloud.operatorcloud import OperatorCloud + + +def _get_openstack_config(app_name=None, app_version=None): + import openstack.config + return openstack.config.OpenStackConfig( + app_name=app_name, app_version=app_version) + + +# TODO(shade) This wants to be remove before we make a release. +def openstack_clouds( + config=None, debug=False, cloud=None, strict=False, + app_name=None, app_version=None): + if not config: + config = _get_openstack_config(app_name, app_version) + try: + if cloud is None: + return [ + OpenStackCloud( + cloud=f.name, debug=debug, + cloud_config=cloud_region, + strict=strict) + for cloud_region in config.get_all() + ] + else: + return [ + OpenStackCloud( + cloud=f.name, debug=debug, + cloud_config=cloud_region, + strict=strict) + for cloud_region in config.get_all() + if cloud_region.name == cloud + ] + except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin as e: + raise OpenStackCloudException( + "Invalid cloud configuration: {exc}".format(exc=str(e))) + + +def openstack_cloud( + config=None, strict=False, app_name=None, app_version=None, **kwargs): + if not config: + config = _get_openstack_config(app_name, app_version) + try: + cloud_region = config.get_one(**kwargs) + except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin as e: + raise OpenStackCloudException( + "Invalid cloud configuration: {exc}".format(exc=str(e))) + return OpenStackCloud(cloud_config=cloud_region, strict=strict) + + +def operator_cloud( + config=None, strict=False, app_name=None, app_version=None, **kwargs): + if not config: + config = _get_openstack_config(app_name, app_version) + try: + cloud_region = config.get_one(**kwargs) + except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin as e: + raise OpenStackCloudException( + "Invalid cloud configuration: {exc}".format(exc=str(e))) + return OperatorCloud(cloud_config=cloud_region, strict=strict) diff --git a/openstack/cloud/cmd/inventory.py b/openstack/cloud/cmd/inventory.py index c7bc09d97..c5bc8cf26 100755 --- a/openstack/cloud/cmd/inventory.py +++ b/openstack/cloud/cmd/inventory.py @@ -60,7 +60,7 @@ def main(): elif args.host: output = inventory.get_host(args.host) print(output_format_dict(output, args.yaml)) - except openstack.OpenStackCloudException as e: + except openstack.cloud.OpenStackCloudException as e: sys.stderr.write(e.message + '\n') sys.exit(1) sys.exit(0) diff --git a/openstack/cloud/inventory.py b/openstack/cloud/inventory.py index 865339f54..5c488c2f9 100644 --- a/openstack/cloud/inventory.py +++ b/openstack/cloud/inventory.py @@ -14,8 +14,8 @@ import functools -import openstack.cloud import openstack.config +import openstack.cloud from openstack.cloud import _utils @@ -37,17 +37,17 @@ def __init__( if cloud is None: self.clouds = [ - openstack.OpenStackCloud(cloud_config=cloud_region) + openstack.cloud.OpenStackCloud(cloud_config=cloud_region) for cloud_region in config.get_all() ] else: try: self.clouds = [ - openstack.OpenStackCloud( + openstack.cloud.OpenStackCloud( cloud_config=config.get_one(cloud)) ] except openstack.config.exceptions.OpenStackConfigException as e: - raise openstack.OpenStackCloudException(e) + raise openstack.cloud.OpenStackCloudException(e) if private: for cloud in self.clouds: @@ -66,7 +66,7 @@ def list_hosts(self, expand=True, fail_on_cloud_config=True): # Cycle on servers for server in cloud.list_servers(detailed=expand): hostvars.append(server) - except openstack.OpenStackCloudException: + except openstack.cloud.OpenStackCloudException: # Don't fail on one particular cloud as others may work if fail_on_cloud_config: raise diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 60a70effd..9a59156b5 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -329,7 +329,7 @@ def connect_as(self, **kwargs): .. code-block:: python - cloud = shade.openstack_cloud(cloud='example') + cloud = openstack.cloud.openstack_cloud(cloud='example') # Work normally servers = cloud.list_servers() cloud2 = cloud.connect_as(username='different-user', password='') @@ -408,7 +408,7 @@ def connect_as_project(self, project): .. code-block:: python - cloud = shade.openstack_cloud(cloud='example') + cloud = openstack.cloud.openstack_cloud(cloud='example') # Work normally servers = cloud.list_servers() cloud2 = cloud.connect_as_project('different-project') diff --git a/openstack/connection.py b/openstack/connection.py index 300972894..1cc1a6f0c 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -74,8 +74,11 @@ network = conn.network.create_network({"name": "zuul"}) """ +import warnings + import keystoneauth1.exceptions import os_service_types +import requestsexceptions import six from six.moves import urllib @@ -86,6 +89,10 @@ from openstack import service_description from openstack import task_manager +if requestsexceptions.SubjectAltNameWarning: + warnings.filterwarnings( + 'ignore', category=requestsexceptions.SubjectAltNameWarning) + _logger = _log.setup_logging('openstack') diff --git a/openstack/tests/functional/cloud/base.py b/openstack/tests/functional/cloud/base.py index ccaabc6cd..63f14f58c 100644 --- a/openstack/tests/functional/cloud/base.py +++ b/openstack/tests/functional/cloud/base.py @@ -38,14 +38,14 @@ def setUp(self): def _set_user_cloud(self, **kwargs): user_config = self.config.get_one( cloud=self._demo_name, **kwargs) - self.user_cloud = openstack.OpenStackCloud( + self.user_cloud = openstack.cloud.OpenStackCloud( cloud_config=user_config, log_inner_exceptions=True) def _set_operator_cloud(self, **kwargs): operator_config = self.config.get_one( cloud=self._op_name, **kwargs) - self.operator_cloud = openstack.OperatorCloud( + self.operator_cloud = openstack.cloud.OperatorCloud( cloud_config=operator_config, log_inner_exceptions=True) diff --git a/openstack/tests/functional/cloud/test_domain.py b/openstack/tests/functional/cloud/test_domain.py index 29f99b273..a2c22bf09 100644 --- a/openstack/tests/functional/cloud/test_domain.py +++ b/openstack/tests/functional/cloud/test_domain.py @@ -46,7 +46,7 @@ def _cleanup_domains(self): if exception_list: # Raise an error: we must make users aware that something went # wrong - raise openstack.OpenStackCloudException( + raise openstack.cloud.OpenStackCloudException( '\n'.join(exception_list)) def test_search_domains(self): diff --git a/openstack/tests/functional/cloud/test_groups.py b/openstack/tests/functional/cloud/test_groups.py index 8d8b050e9..b5e3a4f7d 100644 --- a/openstack/tests/functional/cloud/test_groups.py +++ b/openstack/tests/functional/cloud/test_groups.py @@ -46,7 +46,7 @@ def _cleanup_groups(self): if exception_list: # Raise an error: we must make users aware that something went # wrong - raise openstack.OpenStackCloudException( + raise openstack.cloud.OpenStackCloudException( '\n'.join(exception_list)) def test_create_group(self): diff --git a/openstack/tests/functional/cloud/test_identity.py b/openstack/tests/functional/cloud/test_identity.py index 2d9afe459..10f1be3c4 100644 --- a/openstack/tests/functional/cloud/test_identity.py +++ b/openstack/tests/functional/cloud/test_identity.py @@ -20,7 +20,7 @@ import random import string -from openstack import OpenStackCloudException +from openstack.cloud.exc import OpenStackCloudException from openstack.tests.functional.cloud import base diff --git a/openstack/tests/functional/cloud/test_users.py b/openstack/tests/functional/cloud/test_users.py index df2195a1b..3b8f5a27e 100644 --- a/openstack/tests/functional/cloud/test_users.py +++ b/openstack/tests/functional/cloud/test_users.py @@ -17,7 +17,7 @@ Functional tests for `shade` user methods. """ -from openstack import OpenStackCloudException +from openstack.cloud.exc import OpenStackCloudException from openstack.tests.functional.cloud import base diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index d2caed078..0463a1d4c 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -119,14 +119,14 @@ def _nosleep(seconds): secure_files=['non-existant']) self.cloud_config = self.config.get_one( cloud=test_cloud, validate=False) - self.cloud = openstack.OpenStackCloud( + self.cloud = openstack.cloud.OpenStackCloud( cloud_config=self.cloud_config, log_inner_exceptions=True) - self.strict_cloud = openstack.OpenStackCloud( + self.strict_cloud = openstack.cloud.OpenStackCloud( cloud_config=self.cloud_config, log_inner_exceptions=True, strict=True) - self.op_cloud = openstack.OperatorCloud( + self.op_cloud = openstack.cloud.OperatorCloud( cloud_config=self.cloud_config, log_inner_exceptions=True) @@ -465,10 +465,10 @@ def _make_test_cloud(self, cloud_name='_test_cloud_', **kwargs): cloud=test_cloud, validate=True, **kwargs) self.conn = openstack.connection.Connection( config=self.cloud_config) - self.cloud = openstack.OpenStackCloud( + self.cloud = openstack.cloud.OpenStackCloud( cloud_config=self.cloud_config, log_inner_exceptions=True) - self.op_cloud = openstack.OperatorCloud( + self.op_cloud = openstack.cloud.OperatorCloud( cloud_config=self.cloud_config, log_inner_exceptions=True) diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py index aa26f11eb..bcfc7cd63 100644 --- a/openstack/tests/unit/cloud/test_caching.py +++ b/openstack/tests/unit/cloud/test_caching.py @@ -17,6 +17,7 @@ import testtools import openstack +import openstack.cloud from openstack.cloud import exc from openstack.cloud import meta from openstack.tests import fakes @@ -104,7 +105,7 @@ def _munch_images(self, fake_image): return self.cloud._normalize_images([fake_image]) def test_openstack_cloud(self): - self.assertIsInstance(self.cloud, openstack.OpenStackCloud) + self.assertIsInstance(self.cloud, openstack.cloud.OpenStackCloud) def test_list_projects_v3(self): project_one = self._get_project_data() @@ -480,7 +481,7 @@ def test_list_images(self): self.assert_calls() - @mock.patch.object(openstack.OpenStackCloud, '_image_client') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') def test_list_images_ignores_unsteady_status(self, mock_image_client): steady_image = munch.Munch(id='68', name='Jagr', status='active') for status in ('queued', 'saving', 'pending_delete'): @@ -500,7 +501,7 @@ def test_list_images_ignores_unsteady_status(self, mock_image_client): self._image_dict(steady_image)], self.cloud.list_images()) - @mock.patch.object(openstack.OpenStackCloud, '_image_client') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') def test_list_images_caches_steady_status(self, mock_image_client): steady_image = munch.Munch(id='91', name='Federov', status='active') first_image = None @@ -523,7 +524,7 @@ def test_list_images_caches_steady_status(self, mock_image_client): self._munch_images(first_image), self.cloud.list_images()) - @mock.patch.object(openstack.OpenStackCloud, '_image_client') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') def test_cache_no_cloud_name(self, mock_image_client): self.cloud.name = None @@ -557,5 +558,5 @@ def setUp(self): def test_get_auth_bogus(self): with testtools.ExpectedException(exc.OpenStackCloudException): - openstack.openstack_cloud( + openstack.cloud.openstack_cloud( cloud='_bogus_test_', config=self.config) diff --git a/openstack/tests/unit/cloud/test_cluster_templates.py b/openstack/tests/unit/cloud/test_cluster_templates.py index 8beff5dee..56b45c028 100644 --- a/openstack/tests/unit/cloud/test_cluster_templates.py +++ b/openstack/tests/unit/cloud/test_cluster_templates.py @@ -152,7 +152,7 @@ def test_create_cluster_template_exception(self): # match the more specific HTTPError, even though it's a subclass # of OpenStackCloudException. with testtools.ExpectedException( - openstack.OpenStackCloudHTTPError): + openstack.cloud.OpenStackCloudHTTPError): self.cloud.create_cluster_template('fake-cluster-template') self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_create_server.py b/openstack/tests/unit/cloud/test_create_server.py index b37b14f3d..36050ed83 100644 --- a/openstack/tests/unit/cloud/test_create_server.py +++ b/openstack/tests/unit/cloud/test_create_server.py @@ -325,7 +325,7 @@ def test_create_server_with_admin_pass_no_wait(self): self.assert_calls() - @mock.patch.object(openstack.OpenStackCloud, "wait_for_server") + @mock.patch.object(openstack.cloud.OpenStackCloud, "wait_for_server") def test_create_server_with_admin_pass_wait(self, mock_wait): """ Test that a server with an admin_pass passed returns the password @@ -411,8 +411,8 @@ def test_create_server_user_data_base64(self): self.assert_calls() - @mock.patch.object(openstack.OpenStackCloud, "get_active_server") - @mock.patch.object(openstack.OpenStackCloud, "get_server") + @mock.patch.object(openstack.cloud.OpenStackCloud, "get_active_server") + @mock.patch.object(openstack.cloud.OpenStackCloud, "get_server") def test_wait_for_server(self, mock_get_server, mock_get_active_server): """ Test that waiting for a server returns the server instance when @@ -446,7 +446,7 @@ def test_wait_for_server(self, mock_get_server, mock_get_active_server): self.assertEqual('ACTIVE', server['status']) - @mock.patch.object(openstack.OpenStackCloud, 'wait_for_server') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'wait_for_server') def test_create_server_wait(self, mock_wait): """ Test that create_server with a wait actually does the wait. @@ -483,7 +483,7 @@ def test_create_server_wait(self, mock_wait): ) self.assert_calls() - @mock.patch.object(openstack.OpenStackCloud, 'add_ips_to_server') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'add_ips_to_server') @mock.patch('time.sleep') def test_create_server_no_addresses( self, mock_sleep, mock_add_ips_to_server): diff --git a/openstack/tests/unit/cloud/test_domain_params.py b/openstack/tests/unit/cloud/test_domain_params.py index e01dd88a0..1c5395c7e 100644 --- a/openstack/tests/unit/cloud/test_domain_params.py +++ b/openstack/tests/unit/cloud/test_domain_params.py @@ -21,8 +21,8 @@ class TestDomainParams(base.TestCase): - @mock.patch.object(openstack.OpenStackCloud, '_is_client_version') - @mock.patch.object(openstack.OpenStackCloud, 'get_project') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_project') def test_identity_params_v3(self, mock_get_project, mock_is_client_version): mock_get_project.return_value = munch.Munch(id=1234) @@ -34,8 +34,8 @@ def test_identity_params_v3(self, mock_get_project, self.assertIn('domain_id', ret) self.assertEqual(ret['domain_id'], '5678') - @mock.patch.object(openstack.OpenStackCloud, '_is_client_version') - @mock.patch.object(openstack.OpenStackCloud, 'get_project') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_project') def test_identity_params_v3_no_domain( self, mock_get_project, mock_is_client_version): mock_get_project.return_value = munch.Munch(id=1234) @@ -46,8 +46,8 @@ def test_identity_params_v3_no_domain( self.cloud._get_identity_params, domain_id=None, project='bar') - @mock.patch.object(openstack.OpenStackCloud, '_is_client_version') - @mock.patch.object(openstack.OpenStackCloud, 'get_project') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_project') def test_identity_params_v2(self, mock_get_project, mock_is_client_version): mock_get_project.return_value = munch.Munch(id=1234) @@ -58,8 +58,8 @@ def test_identity_params_v2(self, mock_get_project, self.assertEqual(ret['tenant_id'], 1234) self.assertNotIn('domain', ret) - @mock.patch.object(openstack.OpenStackCloud, '_is_client_version') - @mock.patch.object(openstack.OpenStackCloud, 'get_project') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_project') def test_identity_params_v2_no_domain(self, mock_get_project, mock_is_client_version): mock_get_project.return_value = munch.Munch(id=1234) diff --git a/openstack/tests/unit/cloud/test_domains.py b/openstack/tests/unit/cloud/test_domains.py index d531768b8..2bb69f8c6 100644 --- a/openstack/tests/unit/cloud/test_domains.py +++ b/openstack/tests/unit/cloud/test_domains.py @@ -94,7 +94,7 @@ def test_create_domain_exception(self): domain_data = self._get_domain_data(domain_name='domain_name', enabled=True) with testtools.ExpectedException( - openstack.OpenStackCloudBadRequest, + openstack.cloud.OpenStackCloudBadRequest, "Failed to create domain domain_name" ): self.register_uris([ @@ -149,7 +149,7 @@ def test_delete_domain_exception(self): validate=dict(json={'domain': {'enabled': False}})), dict(method='DELETE', uri=domain_resource_uri, status_code=404)]) with testtools.ExpectedException( - openstack.OpenStackCloudURINotFound, + openstack.cloud.OpenStackCloudURINotFound, "Failed to delete domain %s" % domain_data.domain_id ): self.op_cloud.delete_domain(domain_data.domain_id) @@ -203,7 +203,7 @@ def test_update_domain_exception(self): json=domain_data.json_response, validate=dict(json={'domain': {'enabled': False}}))]) with testtools.ExpectedException( - openstack.OpenStackCloudHTTPError, + openstack.cloud.OpenStackCloudHTTPError, "Error in updating domain %s" % domain_data.domain_id ): self.op_cloud.delete_domain(domain_data.domain_id) diff --git a/openstack/tests/unit/cloud/test_flavors.py b/openstack/tests/unit/cloud/test_flavors.py index f72641927..094068887 100644 --- a/openstack/tests/unit/cloud/test_flavors.py +++ b/openstack/tests/unit/cloud/test_flavors.py @@ -78,7 +78,7 @@ def test_delete_flavor_exception(self): endpoint=fakes.COMPUTE_ENDPOINT, id=fakes.FLAVOR_ID), status_code=503)]) - self.assertRaises(openstack.OpenStackCloudException, + self.assertRaises(openstack.cloud.OpenStackCloudException, self.op_cloud.delete_flavor, 'vanilla') def test_list_flavors(self): @@ -153,7 +153,7 @@ def test_get_flavor_by_ram_not_found(self): endpoint=fakes.COMPUTE_ENDPOINT), json={'flavors': []})]) self.assertRaises( - openstack.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, self.cloud.get_flavor_by_ram, ram=100) diff --git a/openstack/tests/unit/cloud/test_floating_ip_common.py b/openstack/tests/unit/cloud/test_floating_ip_common.py index f4c3a0fc3..e7c20f923 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_common.py +++ b/openstack/tests/unit/cloud/test_floating_ip_common.py @@ -22,7 +22,7 @@ from mock import patch from openstack.cloud import meta -from openstack import OpenStackCloud +from openstack.cloud import OpenStackCloud from openstack.tests import fakes from openstack.tests.unit import base diff --git a/openstack/tests/unit/cloud/test_floating_ip_pool.py b/openstack/tests/unit/cloud/test_floating_ip_pool.py index 4d3ee8369..37a27f812 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_pool.py +++ b/openstack/tests/unit/cloud/test_floating_ip_pool.py @@ -19,7 +19,7 @@ Test floating IP pool resource (managed by nova) """ -from openstack import OpenStackCloudException +from openstack.cloud.exc import OpenStackCloudException from openstack.tests.unit import base from openstack.tests import fakes diff --git a/openstack/tests/unit/cloud/test_identity_roles.py b/openstack/tests/unit/cloud/test_identity_roles.py index 8872da0c6..51a4a8c2a 100644 --- a/openstack/tests/unit/cloud/test_identity_roles.py +++ b/openstack/tests/unit/cloud/test_identity_roles.py @@ -292,7 +292,7 @@ def test_list_role_assignments_keystone_v2_with_role(self): def test_list_role_assignments_exception_v2(self): self.use_keystone_v2() with testtools.ExpectedException( - openstack.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, "Must provide project and user for keystone v2" ): self.op_cloud.list_role_assignments() @@ -301,7 +301,7 @@ def test_list_role_assignments_exception_v2(self): def test_list_role_assignments_exception_v2_no_project(self): self.use_keystone_v2() with testtools.ExpectedException( - openstack.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, "Must provide project and user for keystone v2" ): self.op_cloud.list_role_assignments(filters={'user': '12345'}) diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 4fa06cd83..1511d9b9a 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -588,8 +588,8 @@ def _call_create_image(self, name, **kwargs): is_public=False, **kwargs) # TODO(shade) Migrate this to requests-mock - @mock.patch.object(openstack.OpenStackCloud, '_is_client_version') - @mock.patch.object(openstack.OpenStackCloud, '_image_client') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') def test_create_image_put_v1( self, mock_image_client, mock_is_client_version): mock_is_client_version.return_value = False @@ -626,8 +626,8 @@ def test_create_image_put_v1( self._munch_images(ret), self.cloud.list_images()) # TODO(shade) Migrate this to requests-mock - @mock.patch.object(openstack.OpenStackCloud, '_is_client_version') - @mock.patch.object(openstack.OpenStackCloud, '_image_client') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') def test_create_image_put_v1_bad_delete( self, mock_image_client, mock_is_client_version): mock_is_client_version.return_value = False @@ -665,8 +665,8 @@ def test_create_image_put_v1_bad_delete( mock_image_client.delete.assert_called_with('/images/42') # TODO(shade) Migrate this to requests-mock - @mock.patch.object(openstack.OpenStackCloud, '_is_client_version') - @mock.patch.object(openstack.OpenStackCloud, '_image_client') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') def test_update_image_no_patch( self, mock_image_client, mock_is_client_version): mock_is_client_version.return_value = True @@ -697,8 +697,8 @@ def test_update_image_no_patch( mock_image_client.patch.assert_not_called() # TODO(shade) Migrate this to requests-mock - @mock.patch.object(openstack.OpenStackCloud, '_is_client_version') - @mock.patch.object(openstack.OpenStackCloud, '_image_client') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') def test_create_image_put_v2_bad_delete( self, mock_image_client, mock_is_client_version): mock_is_client_version.return_value = True @@ -737,8 +737,8 @@ def test_create_image_put_v2_bad_delete( mock_image_client.delete.assert_called_with('/images/42') # TODO(shade) Migrate this to requests-mock - @mock.patch.object(openstack.OpenStackCloud, '_is_client_version') - @mock.patch.object(openstack.OpenStackCloud, '_image_client') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') def test_create_image_put_bad_int( self, mock_image_client, mock_is_client_version): mock_is_client_version.return_value = True @@ -750,8 +750,8 @@ def test_create_image_put_bad_int( mock_image_client.post.assert_not_called() # TODO(shade) Migrate this to requests-mock - @mock.patch.object(openstack.OpenStackCloud, '_is_client_version') - @mock.patch.object(openstack.OpenStackCloud, '_image_client') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') def test_create_image_put_user_int( self, mock_image_client, mock_is_client_version): mock_is_client_version.return_value = True @@ -786,8 +786,8 @@ def test_create_image_put_user_int( self._munch_images(ret), self.cloud.list_images()) # TODO(shade) Migrate this to requests-mock - @mock.patch.object(openstack.OpenStackCloud, '_is_client_version') - @mock.patch.object(openstack.OpenStackCloud, '_image_client') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') def test_create_image_put_meta_int( self, mock_image_client, mock_is_client_version): mock_is_client_version.return_value = True @@ -816,8 +816,8 @@ def test_create_image_put_meta_int( self._munch_images(ret), self.cloud.list_images()) # TODO(shade) Migrate this to requests-mock - @mock.patch.object(openstack.OpenStackCloud, '_is_client_version') - @mock.patch.object(openstack.OpenStackCloud, '_image_client') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') def test_create_image_put_protected( self, mock_image_client, mock_is_client_version): mock_is_client_version.return_value = True @@ -855,8 +855,8 @@ def test_create_image_put_protected( self.assertEqual(self._munch_images(ret), self.cloud.list_images()) # TODO(shade) Migrate this to requests-mock - @mock.patch.object(openstack.OpenStackCloud, '_is_client_version') - @mock.patch.object(openstack.OpenStackCloud, '_image_client') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') + @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') def test_create_image_put_user_prop( self, mock_image_client, mock_is_client_version): mock_is_client_version.return_value = True diff --git a/openstack/tests/unit/cloud/test_inventory.py b/openstack/tests/unit/cloud/test_inventory.py index b06be882f..4f4b04104 100644 --- a/openstack/tests/unit/cloud/test_inventory.py +++ b/openstack/tests/unit/cloud/test_inventory.py @@ -26,7 +26,7 @@ def setUp(self): super(TestInventory, self).setUp() @mock.patch("openstack.config.loader.OpenStackConfig") - @mock.patch("openstack.OpenStackCloud") + @mock.patch("openstack.cloud.OpenStackCloud") def test__init(self, mock_cloud, mock_config): mock_config.return_value.get_all.return_value = [{}] @@ -40,7 +40,7 @@ def test__init(self, mock_cloud, mock_config): self.assertTrue(mock_config.return_value.get_all.called) @mock.patch("openstack.config.loader.OpenStackConfig") - @mock.patch("openstack.OpenStackCloud") + @mock.patch("openstack.cloud.OpenStackCloud") def test__init_one_cloud(self, mock_cloud, mock_config): mock_config.return_value.get_one.return_value = [{}] @@ -56,7 +56,7 @@ def test__init_one_cloud(self, mock_cloud, mock_config): 'supercloud') @mock.patch("openstack.config.loader.OpenStackConfig") - @mock.patch("openstack.OpenStackCloud") + @mock.patch("openstack.cloud.OpenStackCloud") def test__raise_exception_on_no_cloud(self, mock_cloud, mock_config): """ Test that when os-client-config can't find a named cloud, a @@ -72,7 +72,7 @@ def test__raise_exception_on_no_cloud(self, mock_cloud, mock_config): 'supercloud') @mock.patch("openstack.config.loader.OpenStackConfig") - @mock.patch("openstack.OpenStackCloud") + @mock.patch("openstack.cloud.OpenStackCloud") def test_list_hosts(self, mock_cloud, mock_config): mock_config.return_value.get_all.return_value = [{}] @@ -91,7 +91,7 @@ def test_list_hosts(self, mock_cloud, mock_config): self.assertEqual([server], ret) @mock.patch("openstack.config.loader.OpenStackConfig") - @mock.patch("openstack.OpenStackCloud") + @mock.patch("openstack.cloud.OpenStackCloud") def test_list_hosts_no_detail(self, mock_cloud, mock_config): mock_config.return_value.get_all.return_value = [{}] @@ -110,7 +110,7 @@ def test_list_hosts_no_detail(self, mock_cloud, mock_config): self.assertFalse(inv.clouds[0].get_openstack_vars.called) @mock.patch("openstack.config.loader.OpenStackConfig") - @mock.patch("openstack.OpenStackCloud") + @mock.patch("openstack.cloud.OpenStackCloud") def test_search_hosts(self, mock_cloud, mock_config): mock_config.return_value.get_all.return_value = [{}] @@ -126,7 +126,7 @@ def test_search_hosts(self, mock_cloud, mock_config): self.assertEqual([server], ret) @mock.patch("openstack.config.loader.OpenStackConfig") - @mock.patch("openstack.OpenStackCloud") + @mock.patch("openstack.cloud.OpenStackCloud") def test_get_host(self, mock_cloud, mock_config): mock_config.return_value.get_all.return_value = [{}] diff --git a/openstack/tests/unit/cloud/test_meta.py b/openstack/tests/unit/cloud/test_meta.py index de3da10d6..29634f9db 100644 --- a/openstack/tests/unit/cloud/test_meta.py +++ b/openstack/tests/unit/cloud/test_meta.py @@ -333,10 +333,10 @@ def test_get_server_multiple_private_ip(self): '10.0.0.101', meta.get_server_private_ip(srv, self.cloud)) self.assert_calls() - @mock.patch.object(openstack.OpenStackCloud, 'has_service') - @mock.patch.object(openstack.OpenStackCloud, 'get_volumes') - @mock.patch.object(openstack.OpenStackCloud, 'get_image_name') - @mock.patch.object(openstack.OpenStackCloud, 'get_flavor_name') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'has_service') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_volumes') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_image_name') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_flavor_name') def test_get_server_private_ip_devstack( self, mock_get_flavor_name, mock_get_image_name, @@ -398,9 +398,9 @@ def test_get_server_private_ip_devstack( self.assertEqual(PRIVATE_V4, srv['private_v4']) self.assert_calls() - @mock.patch.object(openstack.OpenStackCloud, 'get_volumes') - @mock.patch.object(openstack.OpenStackCloud, 'get_image_name') - @mock.patch.object(openstack.OpenStackCloud, 'get_flavor_name') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_volumes') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_image_name') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_flavor_name') def test_get_server_private_ip_no_fip( self, mock_get_flavor_name, mock_get_image_name, @@ -448,9 +448,9 @@ def test_get_server_private_ip_no_fip( self.assertEqual(PRIVATE_V4, srv['private_v4']) self.assert_calls() - @mock.patch.object(openstack.OpenStackCloud, 'get_volumes') - @mock.patch.object(openstack.OpenStackCloud, 'get_image_name') - @mock.patch.object(openstack.OpenStackCloud, 'get_flavor_name') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_volumes') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_image_name') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_flavor_name') def test_get_server_cloud_no_fips( self, mock_get_flavor_name, mock_get_image_name, @@ -496,10 +496,10 @@ def test_get_server_cloud_no_fips( self.assertEqual(PRIVATE_V4, srv['private_v4']) self.assert_calls() - @mock.patch.object(openstack.OpenStackCloud, 'has_service') - @mock.patch.object(openstack.OpenStackCloud, 'get_volumes') - @mock.patch.object(openstack.OpenStackCloud, 'get_image_name') - @mock.patch.object(openstack.OpenStackCloud, 'get_flavor_name') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'has_service') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_volumes') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_image_name') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_flavor_name') def test_get_server_cloud_missing_fips( self, mock_get_flavor_name, mock_get_image_name, @@ -565,9 +565,9 @@ def test_get_server_cloud_missing_fips( self.assertEqual(PUBLIC_V4, srv['public_v4']) self.assert_calls() - @mock.patch.object(openstack.OpenStackCloud, 'get_volumes') - @mock.patch.object(openstack.OpenStackCloud, 'get_image_name') - @mock.patch.object(openstack.OpenStackCloud, 'get_flavor_name') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_volumes') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_image_name') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_flavor_name') def test_get_server_cloud_rackspace_v6( self, mock_get_flavor_name, mock_get_image_name, mock_get_volumes): @@ -615,9 +615,9 @@ def test_get_server_cloud_rackspace_v6( "2001:4800:7819:103:be76:4eff:fe05:8525", srv['interface_ip']) self.assert_calls() - @mock.patch.object(openstack.OpenStackCloud, 'get_volumes') - @mock.patch.object(openstack.OpenStackCloud, 'get_image_name') - @mock.patch.object(openstack.OpenStackCloud, 'get_flavor_name') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_volumes') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_image_name') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_flavor_name') def test_get_server_cloud_osic_split( self, mock_get_flavor_name, mock_get_image_name, mock_get_volumes): diff --git a/openstack/tests/unit/cloud/test_network.py b/openstack/tests/unit/cloud/test_network.py index 52d0449dc..9a88501cb 100644 --- a/openstack/tests/unit/cloud/test_network.py +++ b/openstack/tests/unit/cloud/test_network.py @@ -183,7 +183,7 @@ def test_create_network_provider_ignored_value(self): def test_create_network_provider_wrong_type(self): provider_opts = "invalid" with testtools.ExpectedException( - openstack.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, "Parameter 'provider' must be a dict" ): self.cloud.create_network("netname", provider=provider_opts) @@ -231,7 +231,7 @@ def test_delete_network_exception(self): append=['v2.0', 'networks', "%s.json" % network_id]), status_code=503) ]) - self.assertRaises(openstack.OpenStackCloudException, + self.assertRaises(openstack.cloud.OpenStackCloudException, self.cloud.delete_network, network_name) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_object.py b/openstack/tests/unit/cloud/test_object.py index f63ddd6a4..bbb879ff4 100644 --- a/openstack/tests/unit/cloud/test_object.py +++ b/openstack/tests/unit/cloud/test_object.py @@ -144,7 +144,7 @@ def test_delete_container_error(self): dict(method='DELETE', uri=self.container_endpoint, status_code=409)]) self.assertRaises( - openstack.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, self.cloud.delete_container, self.container) self.assert_calls() @@ -171,7 +171,7 @@ def test_update_container_error(self): dict(method='POST', uri=self.container_endpoint, status_code=409)]) self.assertRaises( - openstack.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, self.cloud.update_container, self.container, dict(foo='bar')) self.assert_calls() @@ -205,7 +205,7 @@ def test_set_container_access_private(self): def test_set_container_access_invalid(self): self.assertRaises( - openstack.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, self.cloud.set_container_access, self.container, 'invalid') def test_get_container_access(self): @@ -360,7 +360,7 @@ def test_get_object_exception(self): status_code=416)]) self.assertRaises( - openstack.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, self.cloud.get_object, self.container, self.object) diff --git a/openstack/tests/unit/cloud/test_operator.py b/openstack/tests/unit/cloud/test_operator.py index a9a5d0e45..fb5702e6e 100644 --- a/openstack/tests/unit/cloud/test_operator.py +++ b/openstack/tests/unit/cloud/test_operator.py @@ -13,7 +13,7 @@ import mock import testtools -import openstack +import openstack.cloud from openstack.cloud import exc from openstack.config import cloud_region from openstack.tests import fakes @@ -23,7 +23,7 @@ class TestOperatorCloud(base.RequestsMockTestCase): def test_operator_cloud(self): - self.assertIsInstance(self.op_cloud, openstack.OperatorCloud) + self.assertIsInstance(self.op_cloud, openstack.cloud.OperatorCloud) @mock.patch.object(cloud_region.CloudRegion, 'get_endpoint') def test_get_session_endpoint_provided(self, fake_get_endpoint): diff --git a/openstack/tests/unit/cloud/test_operator_noauth.py b/openstack/tests/unit/cloud/test_operator_noauth.py index 5f4367b7d..14fdd9dad 100644 --- a/openstack/tests/unit/cloud/test_operator_noauth.py +++ b/openstack/tests/unit/cloud/test_operator_noauth.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -import openstack +import openstack.cloud from openstack.tests.unit import base @@ -50,7 +50,7 @@ def test_ironic_noauth_none_auth_type(self): # with 'v1'. As such, since we are overriding the endpoint, # we must explicitly do the same as we move away from the # client library. - self.cloud_noauth = openstack.operator_cloud( + self.cloud_noauth = openstack.cloud.operator_cloud( auth_type='none', baremetal_endpoint_override="https://bare-metal.example.com/v1") @@ -63,7 +63,7 @@ def test_ironic_noauth_admin_token_auth_type(self): The old way of doing this was to abuse admin_token. """ - self.cloud_noauth = openstack.operator_cloud( + self.cloud_noauth = openstack.cloud.operator_cloud( auth_type='admin_token', auth=dict( endpoint='https://bare-metal.example.com/v1', diff --git a/openstack/tests/unit/cloud/test_project.py b/openstack/tests/unit/cloud/test_project.py index 737967966..8001260c4 100644 --- a/openstack/tests/unit/cloud/test_project.py +++ b/openstack/tests/unit/cloud/test_project.py @@ -76,7 +76,7 @@ def test_create_project_v3(self,): def test_create_project_v3_no_domain(self): with testtools.ExpectedException( - openstack.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, "User or project creation requires an explicit" " domain_id argument." ): @@ -126,7 +126,7 @@ def test_update_project_not_found(self): # shade will raise an attribute error instead of the proper # project not found exception. with testtools.ExpectedException( - openstack.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, "Project %s not found." % project_data.project_id ): self.op_cloud.update_project(project_data.project_id) diff --git a/openstack/tests/unit/cloud/test_security_groups.py b/openstack/tests/unit/cloud/test_security_groups.py index e49a3424a..1960373b9 100644 --- a/openstack/tests/unit/cloud/test_security_groups.py +++ b/openstack/tests/unit/cloud/test_security_groups.py @@ -83,7 +83,7 @@ def test_list_security_groups_none(self): self.cloud.secgroup_source = None self.has_neutron = False - self.assertRaises(openstack.OpenStackCloudUnavailableFeature, + self.assertRaises(openstack.cloud.OpenStackCloudUnavailableFeature, self.cloud.list_security_groups) def test_delete_security_group_neutron(self): @@ -146,7 +146,7 @@ def test_delete_security_group_nova_not_found(self): def test_delete_security_group_none(self): self.cloud.secgroup_source = None - self.assertRaises(openstack.OpenStackCloudUnavailableFeature, + self.assertRaises(openstack.cloud.OpenStackCloudUnavailableFeature, self.cloud.delete_security_group, 'doesNotExist') @@ -246,7 +246,7 @@ def test_create_security_group_nova(self): def test_create_security_group_none(self): self.cloud.secgroup_source = None self.has_neutron = False - self.assertRaises(openstack.OpenStackCloudUnavailableFeature, + self.assertRaises(openstack.cloud.OpenStackCloudUnavailableFeature, self.cloud.create_security_group, '', '') @@ -465,7 +465,7 @@ def test_create_security_group_rule_nova_no_ports(self): def test_create_security_group_rule_none(self): self.has_neutron = False self.cloud.secgroup_source = None - self.assertRaises(openstack.OpenStackCloudUnavailableFeature, + self.assertRaises(openstack.cloud.OpenStackCloudUnavailableFeature, self.cloud.create_security_group_rule, '') @@ -498,7 +498,7 @@ def test_delete_security_group_rule_nova(self): def test_delete_security_group_rule_none(self): self.has_neutron = False self.cloud.secgroup_source = None - self.assertRaises(openstack.OpenStackCloudUnavailableFeature, + self.assertRaises(openstack.cloud.OpenStackCloudUnavailableFeature, self.cloud.delete_security_group_rule, '') @@ -538,7 +538,7 @@ def test_nova_egress_security_group_rule(self): endpoint=fakes.COMPUTE_ENDPOINT), json={'security_groups': [nova_grp_dict]}), ]) - self.assertRaises(openstack.OpenStackCloudException, + self.assertRaises(openstack.cloud.OpenStackCloudException, self.cloud.create_security_group_rule, secgroup_name_or_id='nova-sec-group', direction='egress') diff --git a/openstack/tests/unit/cloud/test_services.py b/openstack/tests/unit/cloud/test_services.py index cfbb261d0..488eacdc8 100644 --- a/openstack/tests/unit/cloud/test_services.py +++ b/openstack/tests/unit/cloud/test_services.py @@ -19,7 +19,7 @@ Tests Keystone services commands. """ -from openstack import OpenStackCloudException +from openstack.cloud.exc import OpenStackCloudException from openstack.cloud.exc import OpenStackCloudUnavailableFeature from openstack.tests.unit import base from testtools import matchers diff --git a/openstack/tests/unit/cloud/test_shade.py b/openstack/tests/unit/cloud/test_shade.py index 6b3be1387..d8ea062bf 100644 --- a/openstack/tests/unit/cloud/test_shade.py +++ b/openstack/tests/unit/cloud/test_shade.py @@ -52,9 +52,9 @@ def fake_has_service(*args, **kwargs): self.cloud.has_service = fake_has_service def test_openstack_cloud(self): - self.assertIsInstance(self.cloud, openstack.OpenStackCloud) + self.assertIsInstance(self.cloud, openstack.cloud.OpenStackCloud) - @mock.patch.object(openstack.OpenStackCloud, 'search_images') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'search_images') def test_get_images(self, mock_search): image1 = dict(id='123', name='mickey') mock_search.return_value = [image1] @@ -62,7 +62,7 @@ def test_get_images(self, mock_search): self.assertIsNotNone(r) self.assertDictEqual(image1, r) - @mock.patch.object(openstack.OpenStackCloud, 'search_images') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'search_images') def test_get_image_not_found(self, mock_search): mock_search.return_value = [] r = self.cloud.get_image('doesNotExist') diff --git a/openstack/tests/unit/cloud/test_stack.py b/openstack/tests/unit/cloud/test_stack.py index 4e99dbddd..448a09857 100644 --- a/openstack/tests/unit/cloud/test_stack.py +++ b/openstack/tests/unit/cloud/test_stack.py @@ -57,7 +57,7 @@ def test_list_stacks_exception(self): status_code=404) ]) with testtools.ExpectedException( - openstack.OpenStackCloudURINotFound): + openstack.cloud.OpenStackCloudURINotFound): self.cloud.list_stacks() self.assert_calls() @@ -110,7 +110,7 @@ def test_search_stacks_exception(self): status_code=404) ]) with testtools.ExpectedException( - openstack.OpenStackCloudURINotFound): + openstack.cloud.OpenStackCloudURINotFound): self.cloud.search_stacks() def test_delete_stack(self): @@ -171,7 +171,7 @@ def test_delete_stack_exception(self): reason="ouch"), ]) with testtools.ExpectedException( - openstack.OpenStackCloudBadRequest): + openstack.cloud.OpenStackCloudBadRequest): self.cloud.delete_stack(self.stack_id) self.assert_calls() @@ -288,7 +288,7 @@ def test_delete_stack_wait_failed(self): ]) with testtools.ExpectedException( - openstack.OpenStackCloudException): + openstack.cloud.OpenStackCloudException): self.cloud.delete_stack(self.stack_id, wait=True) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_users.py b/openstack/tests/unit/cloud/test_users.py index a4c8a466f..a5c00a6c8 100644 --- a/openstack/tests/unit/cloud/test_users.py +++ b/openstack/tests/unit/cloud/test_users.py @@ -124,7 +124,7 @@ def test_create_user_v3_no_domain(self): user_data = self._get_user_data(domain_id=uuid.uuid4().hex, email='test@example.com') with testtools.ExpectedException( - openstack.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, "User or project creation requires an explicit" " domain_id argument." ): diff --git a/openstack/tests/unit/cloud/test_volume.py b/openstack/tests/unit/cloud/test_volume.py index 84bf9b334..9c1e2380e 100644 --- a/openstack/tests/unit/cloud/test_volume.py +++ b/openstack/tests/unit/cloud/test_volume.py @@ -60,7 +60,7 @@ def test_attach_volume_exception(self): 'volumeId': vol['id']}}) )]) with testtools.ExpectedException( - openstack.OpenStackCloudURINotFound, + openstack.cloud.OpenStackCloudURINotFound, "Error attaching volume %s to server %s" % ( volume['id'], server['id']) ): @@ -126,7 +126,7 @@ def test_attach_volume_wait_error(self): json={'volumes': [errored_volume]})]) with testtools.ExpectedException( - openstack.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, "Error in attaching volume %s" % errored_volume['id'] ): self.cloud.attach_volume(server, volume) @@ -137,7 +137,7 @@ def test_attach_volume_not_available(self): volume = dict(id='volume001', status='error', attachments=[]) with testtools.ExpectedException( - openstack.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, "Volume %s is not available. Status is '%s'" % ( volume['id'], volume['status']) ): @@ -153,7 +153,7 @@ def test_attach_volume_already_attached(self): ]) with testtools.ExpectedException( - openstack.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, "Volume %s already attached to server %s on device %s" % ( volume['id'], server['id'], device_id) ): @@ -189,7 +189,7 @@ def test_detach_volume_exception(self): 'os-volume_attachments', volume['id']]), status_code=404)]) with testtools.ExpectedException( - openstack.OpenStackCloudURINotFound, + openstack.cloud.OpenStackCloudURINotFound, "Error detaching volume %s from server %s" % ( volume['id'], server['id']) ): @@ -238,7 +238,7 @@ def test_detach_volume_wait_error(self): 'volumev2', 'public', append=['volumes', 'detail']), json={'volumes': [errored_volume]})]) with testtools.ExpectedException( - openstack.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, "Error in detaching volume %s" % errored_volume['id'] ): self.cloud.detach_volume(server, volume) diff --git a/openstack/tests/unit/cloud/test_volume_access.py b/openstack/tests/unit/cloud/test_volume_access.py index 8124f3e59..79b1697ee 100644 --- a/openstack/tests/unit/cloud/test_volume_access.py +++ b/openstack/tests/unit/cloud/test_volume_access.py @@ -187,7 +187,7 @@ def test_add_volume_type_access_missing(self): qs_elements=['is_public=None']), json={'volume_types': [volume_type]})]) with testtools.ExpectedException( - openstack.OpenStackCloudException, + openstack.cloud.OpenStackCloudException, "VolumeType not found: MISSING"): self.op_cloud.add_volume_type_access( "MISSING", project_001['project_id']) From aa52477e8e66ae9a3335b42f325e07e85590ceef Mon Sep 17 00:00:00 2001 From: "Yuanbin.Chen" Date: Fri, 19 Jan 2018 15:12:12 +0800 Subject: [PATCH 1916/3836] Add clustering guides receiver file,examples receiver code Change-Id: I4a4a34c674195402aaa92929d2cfd2cb51775995 Signed-off-by: Yuanbin.Chen --- .../user/guides/clustering/receiver.rst | 84 ++++++++++++++++++- examples/clustering/receiver.py | 83 ++++++++++++++++++ 2 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 examples/clustering/receiver.py diff --git a/doc/source/user/guides/clustering/receiver.rst b/doc/source/user/guides/clustering/receiver.rst index a34f67b7e..2cd8e8bec 100644 --- a/doc/source/user/guides/clustering/receiver.rst +++ b/doc/source/user/guides/clustering/receiver.rst @@ -15,4 +15,86 @@ Managing Receivers ================== -.. TODO(Qiming): Implement this guide +Receivers are the event sinks associated to senlin clusters. When certain +events (or alarms) are seen by a monitoring software, the software can +notify the senlin clusters of those events (or alarms). When senlin receives +those notifications, it can automatically trigger some predefined operations +with preset parameter values. + + +List Receivers +~~~~~~~~~~~~~~ + +To examine the list of receivers: + +.. literalinclude:: ../../examples/clustering/receiver.py + :pyobject: list_receivers + +When listing receivers, you can specify the sorting option using the ``sort`` +parameter and you can do pagination using the ``limit`` and ``marker`` +parameters. + +Full example: `manage receiver`_ + + +Create Receiver +~~~~~~~~~~~~~~~ + +When creating a receiver, you will provide a dictionary with keys and values +according to the receiver type referenced. + +.. literalinclude:: ../../examples/clustering/receiver.py + :pyobject: create_receiver + +Optionally, you can specify a ``metadata`` keyword argument that contains some +key-value pairs to be associated with the receiver. + +Full example: `manage receiver`_ + + +Get Receiver +~~~~~~~~~~~~ + +To get a receiver based on its name or ID: + +.. literalinclude:: ../../examples/clustering/receiver.py + :pyobject: get_receiver + +Full example: `manage receiver`_ + + +Find Receiver +~~~~~~~~~~~~~ + +To find a receiver based on its name or ID: + +.. literalinclude:: ../../examples/clustering/receiver.py + :pyobject: find_receiver + +Full example: `manage receiver`_ + + +Update Receiver +~~~~~~~~~~~~~~~ + +After a receiver is created, most of its properties are immutable. Still, you +can update a receiver's ``name`` and/or ``params``. + +.. literalinclude:: ../../examples/clustering/receiver.py + :pyobject: update_receiver + +Full example: `manage receiver`_ + + +Delete Receiver +~~~~~~~~~~~~~~~ + +A receiver can be deleted after creation, provided that it is not referenced +by any active clusters. If you attempt to delete a receiver that is still in +use, you will get an error message. + +.. literalinclude:: ../../examples/clustering/receiver.py + :pyobject: delete_receiver + + +.. _manage receiver: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/clustering/receiver.py diff --git a/examples/clustering/receiver.py b/examples/clustering/receiver.py new file mode 100644 index 000000000..362988a05 --- /dev/null +++ b/examples/clustering/receiver.py @@ -0,0 +1,83 @@ +# 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. + +""" +Managing policies in the Cluster service. + +For a full guide see +https://developer.openstack.org/sdks/python/openstacksdk/user/guides/cluster.html +""" + +FAKE_NAME = 'test_receiver' +CLUSTER_ID = "ae63a10b-4a90-452c-aef1-113a0b255ee3" + + +def list_receivers(conn): + print("List Receivers:") + + for receiver in conn.cluster.receivers(): + print(receiver.to_dict()) + + for receiver in conn.cluster.receivers(sort='name:asc'): + print(receiver.to_dict()) + + +def create_receiver(conn): + print("Create Receiver:") + + # Build the receiver attributes and create the recever. + spec = { + "action": "CLUSTER_SCALE_OUT", + "cluster_id": CLUSTER_ID, + "name": FAKE_NAME, + "params": { + "count": "1" + }, + "type": "webhook" + } + + receiver = conn.cluster.create_receiver(**spec) + print(receiver.to_dict()) + + +def get_receiver(conn): + print("Get Receiver:") + + receiver = conn.cluster.get_receiver(FAKE_NAME) + print(receiver.to_dict()) + + +def find_receiver(conn): + print("Find Receiver:") + + receiver = conn.cluster.find_receiver(FAKE_NAME) + print(receiver.to_dict()) + + +def update_receiver(conn): + print("Update Receiver:") + + spec = { + "name": "test_receiver2", + "params": { + "count": "2" + } + } + receiver = conn.cluster.update_receiver(FAKE_NAME, **spec) + print(receiver.to_dict()) + + +def delete_receiver(conn): + print("Delete Receiver:") + + conn.cluster.delete_receiver(FAKE_NAME) + print("Receiver deleted.") From 7fd8217dc82c0741ef3244336fd21f764957b521 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 15 Jan 2018 14:45:24 -0600 Subject: [PATCH 1917/3836] Invert the attribute mapping requests returns headers in a CaseInsensitiveDict because Headers are case insensitive. Unfortunately, that only applies to keys, and our current attribute mapping that we use to consume values from payloads is {client_name: server_name}, which means we have to go through shenanigans to get the comparison right. Invert the mapping returned by _get_mapping and update the logic to deal with the inversion. While we're at it, remove _filter_component, as _consume_attrs already does the same thing and handles the case sensitivity properly. Change-Id: Ice38cc31635a364839472248f3814b0c8f65bfe1 --- openstack/compute/v2/keypair.py | 10 ++- openstack/compute/v2/limits.py | 8 +-- openstack/key_manager/v1/secret.py | 2 +- openstack/resource2.py | 72 +++++++------------ .../functional/compute/v2/test_keypair.py | 7 +- .../tests/unit/compute/v2/test_limits.py | 4 +- .../tests/unit/key_manager/v1/test_secret.py | 4 +- openstack/tests/unit/test_resource2.py | 23 ++---- 8 files changed, 52 insertions(+), 78 deletions(-) diff --git a/openstack/compute/v2/keypair.py b/openstack/compute/v2/keypair.py index b1fd2e4cb..324d29e7d 100644 --- a/openstack/compute/v2/keypair.py +++ b/openstack/compute/v2/keypair.py @@ -34,8 +34,8 @@ class Keypair(resource2.Resource): # because all operations use the 'name' as an identifier. # Additionally, the 'id' field only appears *after* creation, # so suddenly you have an 'id' field filled in after the fact, - # and it just gets in the way. We need to cover this up by having - # the name be both our id and name. + # and it just gets in the way. We need to cover this up by listing + # name as alternate_id and listing id as coming from name. #: The id identifying the keypair id = resource2.Body('name') #: A name identifying the keypair @@ -45,6 +45,12 @@ class Keypair(resource2.Resource): #: The SSH public key that is paired with the server. public_key = resource2.Body('public_key') + def __init__(self, **attrs): + # TODO(mordred) Maybe let's figure out how to make the base constructor + # do this if alternate_id is specified? + attrs.setdefault('name', attrs.pop('id', None)) + super(Keypair, self).__init__(**attrs) + @classmethod def list(cls, session, paginated=False): resp = session.get(cls.base_path, diff --git a/openstack/compute/v2/limits.py b/openstack/compute/v2/limits.py index 1e04f4d89..48360da26 100644 --- a/openstack/compute/v2/limits.py +++ b/openstack/compute/v2/limits.py @@ -92,16 +92,16 @@ def get(self, session, requires_id=False, error_message=None): body = response.json() body = body[self.resource_key] - absolute_body = self._filter_component( - body["absolute"], AbsoluteLimits._body_mapping()) + absolute_body = self._consume_attrs( + AbsoluteLimits._body_mapping(), body["absolute"]) self.absolute = AbsoluteLimits.existing(**absolute_body) rates_body = body["rate"] rates = [] for rate_body in rates_body: - rate_body = self._filter_component(rate_body, - RateLimit._body_mapping()) + rate_body = self._consume_attrs( + RateLimit._body_mapping(), rate_body) rates.append(RateLimit(**rate_body)) self.rate = rates diff --git a/openstack/key_manager/v1/secret.py b/openstack/key_manager/v1/secret.py index de11b28e7..91fe78e5b 100644 --- a/openstack/key_manager/v1/secret.py +++ b/openstack/key_manager/v1/secret.py @@ -97,7 +97,7 @@ def get(self, session, requires_id=True): response["payload"] = payload.text # We already have the JSON here so don't call into _translate_response - body = self._filter_component(response, self._body_mapping()) + body = self._consume_attrs(self._body_mapping(), response) self._body.attributes.update(body) self._body.clean() diff --git a/openstack/resource2.py b/openstack/resource2.py index b40afa9c0..e175d0db4 100644 --- a/openstack/resource2.py +++ b/openstack/resource2.py @@ -344,12 +344,13 @@ def _collect_attrs(self, attrs): """ body = self._consume_attrs(self._body_mapping(), attrs) header = self._consume_attrs( - self._header_mapping(), attrs, insensitive=True) + self._header_mapping(), attrs, + map_cls=structures.CaseInsensitiveDict) uri = self._consume_attrs(self._uri_mapping(), attrs) return body, header, uri - def _consume_attrs(self, mapping, attrs, insensitive=False): + def _consume_attrs(self, mapping, attrs, map_cls=dict): """Given a mapping and attributes, return relevant matches This method finds keys in attrs that exist in the mapping, then @@ -360,29 +361,20 @@ def _consume_attrs(self, mapping, attrs, insensitive=False): same source dict several times. """ relevant_attrs = {} - if insensitive: - relevant_attrs = structures.CaseInsensitiveDict() consumed_keys = [] - nonce = object() - # TODO(mordred) Invert the loop - loop over mapping, look in attrs - # and we should be able to simplify the logic, since CID should - # handle the case matching - for key in attrs: - value = mapping.get(key, nonce) - if value is not nonce: - # Convert client-side key names into server-side. - relevant_attrs[mapping[key]] = attrs[key] - consumed_keys.append(key) - else: - # Server-side names can be stored directly. - search_key = key - values = mapping.values() - if insensitive: - search_key = search_key.lower() - values = [v.lower() for v in values] - if search_key in values: - relevant_attrs[key] = attrs[key] - consumed_keys.append(key) + for key, value in attrs.items(): + # We want the key lookup in mapping to be case insensitive if the + # mapping is, thus the use of get. We want value to be exact. + # If we find a match, we then have to loop over the mapping for + # to find the key to return, as there isn't really a "get me the + # key that matches this other key". We lower() in the inner loop + # because we've already done case matching in the outer loop. + if key in mapping.values() or mapping.get(key): + for map_key, map_value in mapping.items(): + if key.lower() in (map_key.lower(), map_value.lower()): + relevant_attrs[map_key] = value + consumed_keys.append(key) + continue for key in consumed_keys: attrs.pop(key) @@ -390,13 +382,10 @@ def _consume_attrs(self, mapping, attrs, insensitive=False): return relevant_attrs @classmethod - def _get_mapping(cls, component): + def _get_mapping(cls, component, map_cls=dict): """Return a dict of attributes of a given component on the class""" - # TODO(mordred) Invert this mapping, it should be server-side to local. - # The reason for that is that headers are case insensitive, whereas - # our local values are case sensitive. If we invert this dict, we can - # rely on CaseInsensitiveDict when doing comparisons. - mapping = {} + mapping = map_cls() + ret = map_cls() # Since we're looking at class definitions we need to include # subclasses, so check the whole MRO. for klass in cls.__mro__: @@ -405,8 +394,11 @@ def _get_mapping(cls, component): # Make sure base classes don't end up overwriting # mappings we've found previously in subclasses. if key not in mapping: + # Make it this way first, to get MRO stuff correct. mapping[key] = value.name - return mapping + for k, v in mapping.items(): + ret[v] = k + return ret @classmethod def _body_mapping(cls): @@ -416,8 +408,8 @@ def _body_mapping(cls): @classmethod def _header_mapping(cls): """Return all Header members of this class""" - # TODO(mordred) this isn't helpful until we invert the dict - return structures.CaseInsensitiveDict(cls._get_mapping(Header)) + return cls._get_mapping( + Header, map_cls=structures.CaseInsensitiveDict) @classmethod def _uri_mapping(cls): @@ -571,14 +563,6 @@ def _prepare_request(self, requires_id=None, prepend_key=False): return _Request(uri, body, headers) - def _filter_component(self, component, mapping): - """Filter the keys in component based on a mapping - - This method converts a dict of server-side data to contain - only the appropriate keys for attributes on this instance. - """ - return {k: v for k, v in component.items() if k in mapping.values()} - def _translate_response(self, response, has_body=None, error_message=None): """Given a KSA response, inflate this instance with its data @@ -596,14 +580,12 @@ def _translate_response(self, response, has_body=None, error_message=None): if self.resource_key and self.resource_key in body: body = body[self.resource_key] - body = self._filter_component(body, self._body_mapping()) + body = self._consume_attrs(self._body_mapping(), body) self._body.attributes.update(body) self._body.clean() - headers = self._filter_component(response.headers, - self._header_mapping()) headers = self._consume_attrs( - self._header_mapping(), response.headers.copy(), insensitive=True) + self._header_mapping(), response.headers.copy()) self._header.attributes.update(headers) self._header.clean() diff --git a/openstack/tests/functional/compute/v2/test_keypair.py b/openstack/tests/functional/compute/v2/test_keypair.py index 1909ae42d..6fccafa27 100644 --- a/openstack/tests/functional/compute/v2/test_keypair.py +++ b/openstack/tests/functional/compute/v2/test_keypair.py @@ -22,13 +22,11 @@ def setUp(self): # Keypairs can't have .'s in the name. Because why? self.NAME = self.getUniqueString().split('.')[-1] - self.ID = None sot = self.conn.compute.create_keypair(name=self.NAME) assert isinstance(sot, keypair.Keypair) self.assertEqual(self.NAME, sot.name) self._keypair = sot - self.ID = sot.id def tearDown(self): sot = self.conn.compute.delete_keypair(self._keypair) @@ -37,12 +35,13 @@ def tearDown(self): def test_find(self): sot = self.conn.compute.find_keypair(self.NAME) - self.assertEqual(self.ID, sot.id) + self.assertEqual(self.NAME, sot.name) + self.assertEqual(self.NAME, sot.id) def test_get(self): sot = self.conn.compute.get_keypair(self.NAME) self.assertEqual(self.NAME, sot.name) - self.assertEqual(self.ID, sot.id) + self.assertEqual(self.NAME, sot.id) def test_list(self): names = [o.name for o in self.conn.compute.keypairs()] diff --git a/openstack/tests/unit/compute/v2/test_limits.py b/openstack/tests/unit/compute/v2/test_limits.py index e31349169..dee011bf2 100644 --- a/openstack/tests/unit/compute/v2/test_limits.py +++ b/openstack/tests/unit/compute/v2/test_limits.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import copy + import mock import testtools @@ -147,7 +149,7 @@ def test_get(self): sess = mock.Mock() resp = mock.Mock() sess.get.return_value = resp - resp.json.return_value = LIMITS_BODY + resp.json.return_value = copy.deepcopy(LIMITS_BODY) sot = limits.Limits().get(sess) diff --git a/openstack/tests/unit/key_manager/v1/test_secret.py b/openstack/tests/unit/key_manager/v1/test_secret.py index 31814bee8..cbdbfe7c8 100644 --- a/openstack/tests/unit/key_manager/v1/test_secret.py +++ b/openstack/tests/unit/key_manager/v1/test_secret.py @@ -98,10 +98,10 @@ def test_get_no_payload(self): def _test_payload(self, sot, metadata, content_type): content_type = "some/type" - sot = secret.Secret(id="id", payload_content_type=content_type) metadata_response = mock.Mock() - metadata_response.json = mock.Mock(return_value=metadata) + # Use copy because the dict gets consumed. + metadata_response.json = mock.Mock(return_value=metadata.copy()) payload_response = mock.Mock() payload = "secret info" diff --git a/openstack/tests/unit/test_resource2.py b/openstack/tests/unit/test_resource2.py index b9d990cbe..18bf10de4 100644 --- a/openstack/tests/unit/test_resource2.py +++ b/openstack/tests/unit/test_resource2.py @@ -508,8 +508,8 @@ def test__consume_attrs(self): clientside_key2 = "some_key2" value1 = "value1" value2 = "value2" - mapping = {clientside_key1: serverside_key1, - clientside_key2: serverside_key2} + mapping = {serverside_key1: clientside_key1, + serverside_key2: clientside_key2} other_key = "otherKey" other_value = "other" @@ -551,8 +551,8 @@ class Test(resource2.Resource): mapping = Test._body_mapping() - self.assertEqual(new_name, mapping["name"]) - self.assertEqual(new_id, mapping["id"]) + self.assertEqual("name", mapping["MyName"]) + self.assertEqual("id", mapping["MyID"]) def test__body_mapping(self): class Test(resource2.Resource): @@ -833,21 +833,6 @@ class Test(resource2.Resource): self.assertEqual({key: {"x": body_value}}, result.body) self.assertEqual({"y": header_value}, result.headers) - def test__filter_component(self): - client_name = "client_name" - server_name = "serverName" - value = "value" - # Include something in the mapping that we don't receive - # so the branch that looks at existence in the compoment is checked. - mapping = {client_name: server_name, "other": "blah"} - component = {server_name: value, "something": "else"} - - sot = resource2.Resource() - result = sot._filter_component(component, mapping) - - # The something:else mapping should not make it into here. - self.assertEqual({server_name: value}, result) - def test__translate_response_no_body(self): class Test(resource2.Resource): attr = resource2.Header("attr") From 27e767feed1316944257129f87b68184c62ed1ab Mon Sep 17 00:00:00 2001 From: "Yuanbin.Chen" Date: Sat, 20 Jan 2018 12:36:51 +0800 Subject: [PATCH 1918/3836] Replace clustering examples code cluster to clustering Change-Id: I013d6eb917c93ccfeb44b4d9c96108d3d6cde607 Signed-off-by: Yuanbin.Chen --- examples/clustering/policy.py | 14 +++++++------- examples/clustering/policy_type.py | 4 ++-- examples/clustering/profile.py | 14 +++++++------- examples/clustering/profile_type.py | 4 ++-- examples/clustering/receiver.py | 14 +++++++------- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/examples/clustering/policy.py b/examples/clustering/policy.py index 5d7e8b25e..dca4aca5d 100644 --- a/examples/clustering/policy.py +++ b/examples/clustering/policy.py @@ -21,10 +21,10 @@ def list_policies(conn): print("List Policies:") - for policy in conn.cluster.policies(): + for policy in conn.clustering.policies(): print(policy.to_dict()) - for policy in conn.cluster.policies(sort='name:asc'): + for policy in conn.clustering.policies(sort='name:asc'): print(policy.to_dict()) @@ -40,34 +40,34 @@ def create_policy(conn): } } - policy = conn.cluster.create_policy('dp01', spec) + policy = conn.clustering.create_policy('dp01', spec) print(policy.to_dict()) def get_policy(conn): print("Get Policy:") - policy = conn.cluster.get_policy('dp01') + policy = conn.clustering.get_policy('dp01') print(policy.to_dict()) def find_policy(conn): print("Find Policy:") - policy = conn.cluster.find_policy('dp01') + policy = conn.clustering.find_policy('dp01') print(policy.to_dict()) def update_policy(conn): print("Update Policy:") - policy = conn.cluster.update_policy('dp01', name='dp02') + policy = conn.clustering.update_policy('dp01', name='dp02') print(policy.to_dict()) def delete_policy(conn): print("Delete Policy:") - conn.cluster.delete_policy('dp01') + conn.clustering.delete_policy('dp01') print("Policy deleted.") diff --git a/examples/clustering/policy_type.py b/examples/clustering/policy_type.py index 27c06ac6a..2eb72f44a 100644 --- a/examples/clustering/policy_type.py +++ b/examples/clustering/policy_type.py @@ -21,13 +21,13 @@ def list_policy_types(conn): print("List Policy Types:") - for pt in conn.cluster.policy_types(): + for pt in conn.clustering.policy_types(): print(pt.to_dict()) def get_policy_type(conn): print("Get Policy Type:") - pt = conn.cluster.get_policy_type('senlin.policy.deletion-1.0') + pt = conn.clustering.get_policy_type('senlin.policy.deletion-1.0') print(pt.to_dict()) diff --git a/examples/clustering/profile.py b/examples/clustering/profile.py index 397374b8c..1c3ae6bcc 100644 --- a/examples/clustering/profile.py +++ b/examples/clustering/profile.py @@ -26,10 +26,10 @@ def list_profiles(conn): print("List Profiles:") - for profile in conn.cluster.profiles(): + for profile in conn.clustering.profiles(): print(profile.to_dict()) - for profile in conn.cluster.profiles(sort='name:asc'): + for profile in conn.clustering.profiles(sort='name:asc'): print(profile.to_dict()) @@ -49,34 +49,34 @@ def create_profile(conn): } } - profile = conn.cluster.create_profile('os_server', spec) + profile = conn.clustering.create_profile('os_server', spec) print(profile.to_dict()) def get_profile(conn): print("Get Profile:") - profile = conn.cluster.get_profile('os_server') + profile = conn.clustering.get_profile('os_server') print(profile.to_dict()) def find_profile(conn): print("Find Profile:") - profile = conn.cluster.find_profile('os_server') + profile = conn.clustering.find_profile('os_server') print(profile.to_dict()) def update_profile(conn): print("Update Profile:") - profile = conn.cluster.update_profile('os_server', name='old_server') + profile = conn.clustering.update_profile('os_server', name='old_server') print(profile.to_dict()) def delete_profile(conn): print("Delete Profile:") - conn.cluster.delete_profile('os_server') + conn.clustering.delete_profile('os_server') print("Profile deleted.") diff --git a/examples/clustering/profile_type.py b/examples/clustering/profile_type.py index 9f3f3cc26..fa5403b83 100644 --- a/examples/clustering/profile_type.py +++ b/examples/clustering/profile_type.py @@ -21,13 +21,13 @@ def list_profile_types(conn): print("List Profile Types:") - for pt in conn.cluster.profile_types(): + for pt in conn.clustering.profile_types(): print(pt.to_dict()) def get_profile_type(conn): print("Get Profile Type:") - pt = conn.cluster.get_profile_type('os.nova.server-1.0') + pt = conn.clustering.get_profile_type('os.nova.server-1.0') print(pt.to_dict()) diff --git a/examples/clustering/receiver.py b/examples/clustering/receiver.py index 362988a05..a0c374925 100644 --- a/examples/clustering/receiver.py +++ b/examples/clustering/receiver.py @@ -24,10 +24,10 @@ def list_receivers(conn): print("List Receivers:") - for receiver in conn.cluster.receivers(): + for receiver in conn.clustering.receivers(): print(receiver.to_dict()) - for receiver in conn.cluster.receivers(sort='name:asc'): + for receiver in conn.clustering.receivers(sort='name:asc'): print(receiver.to_dict()) @@ -45,21 +45,21 @@ def create_receiver(conn): "type": "webhook" } - receiver = conn.cluster.create_receiver(**spec) + receiver = conn.clustering.create_receiver(**spec) print(receiver.to_dict()) def get_receiver(conn): print("Get Receiver:") - receiver = conn.cluster.get_receiver(FAKE_NAME) + receiver = conn.clustering.get_receiver(FAKE_NAME) print(receiver.to_dict()) def find_receiver(conn): print("Find Receiver:") - receiver = conn.cluster.find_receiver(FAKE_NAME) + receiver = conn.clustering.find_receiver(FAKE_NAME) print(receiver.to_dict()) @@ -72,12 +72,12 @@ def update_receiver(conn): "count": "2" } } - receiver = conn.cluster.update_receiver(FAKE_NAME, **spec) + receiver = conn.clustering.update_receiver(FAKE_NAME, **spec) print(receiver.to_dict()) def delete_receiver(conn): print("Delete Receiver:") - conn.cluster.delete_receiver(FAKE_NAME) + conn.clustering.delete_receiver(FAKE_NAME) print("Receiver deleted.") From c0b2d084d290837949d2f327b2445fba9a7f9527 Mon Sep 17 00:00:00 2001 From: "Yuanbin.Chen" Date: Sat, 20 Jan 2018 12:25:35 +0800 Subject: [PATCH 1919/3836] Add clustering guides file, Examples code. Clustering guides add action/eventfile. Clustering examples add action/event example code. Change-Id: I47164b1539d53e51ac915817398ebf85fd8ddb24 Signed-off-by: Yuanbin.Chen --- doc/source/user/guides/clustering/action.rst | 31 +++++++++++++++- doc/source/user/guides/clustering/event.rst | 31 +++++++++++++++- examples/clustering/action.py | 37 ++++++++++++++++++++ examples/clustering/event.py | 37 ++++++++++++++++++++ 4 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 examples/clustering/action.py create mode 100644 examples/clustering/event.py diff --git a/doc/source/user/guides/clustering/action.rst b/doc/source/user/guides/clustering/action.rst index 1a07479eb..d6f6b0ea3 100644 --- a/doc/source/user/guides/clustering/action.rst +++ b/doc/source/user/guides/clustering/action.rst @@ -15,4 +15,33 @@ Working with Actions ==================== -.. TODO(Qiming): Implement this guide +An action is an abstraction of some logic that can be executed by a worker +thread. Most of the operations supported by Senlin are executed asynchronously, +which means they are queued into database and then picked up by certain worker +thread for execution. + + +List Actions +~~~~~~~~~~~~ + +To examine the list of actions: + +.. literalinclude:: ../../examples/clustering/action.py + :pyobject: list_actions + +When listing actions, you can specify the sorting option using the ``sort`` +parameter and you can do pagination using the ``limit`` and ``marker`` +parameters. + +Full example: `manage action`_ + + +Get Action +~~~~~~~~~~ + +To get a action based on its name or ID: + +.. literalinclude:: ../../examples/clustering/action.py + :pyobject: get_action + +.. _manage action: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/clustering/action.py diff --git a/doc/source/user/guides/clustering/event.rst b/doc/source/user/guides/clustering/event.rst index 185f454c5..5969ec323 100644 --- a/doc/source/user/guides/clustering/event.rst +++ b/doc/source/user/guides/clustering/event.rst @@ -15,4 +15,33 @@ Working with Events =================== -.. TODO(Qiming): Implement this guide +An event is a record generated during engine execution. Such an event +captures what has happened inside the senlin-engine. The senlin-engine service +generates event records when it is performing some actions or checking +policies. + + +List Events +~~~~~~~~~~~~ + +To examine the list of events: + +.. literalinclude:: ../../examples/clustering/event.py + :pyobject: list_events + +When listing events, you can specify the sorting option using the ``sort`` +parameter and you can do pagination using the ``limit`` and ``marker`` +parameters. + +Full example: `manage event`_ + + +Get Event +~~~~~~~~~ + +To get a event based on its name or ID: + +.. literalinclude:: ../../examples/clustering/event.py + :pyobject: get_event + +.. _manage event: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/clustering/event.py diff --git a/examples/clustering/action.py b/examples/clustering/action.py new file mode 100644 index 000000000..cdbe7c646 --- /dev/null +++ b/examples/clustering/action.py @@ -0,0 +1,37 @@ +# 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. + +""" +Managing policies in the Cluster service. + +For a full guide see +https://developer.openstack.org/sdks/python/openstacksdk/user/guides/cluster.html +""" + +ACTION_ID = "06ad259b-d6ab-4eb2-a0fa-fb144437eab1" + + +def list_actions(conn): + print("List Actions:") + + for actions in conn.clustering.actions(): + print(actions.to_dict()) + + for actions in conn.clustering.actions(sort='name:asc'): + print(actions.to_dict()) + + +def get_action(conn): + print("Get Action:") + + action = conn.clustering.get_action(ACTION_ID) + print(action.to_dict()) diff --git a/examples/clustering/event.py b/examples/clustering/event.py new file mode 100644 index 000000000..e4f477bc4 --- /dev/null +++ b/examples/clustering/event.py @@ -0,0 +1,37 @@ +# 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. + +""" +Managing policies in the Cluster service. + +For a full guide see +https://developer.openstack.org/sdks/python/openstacksdk/user/guides/cluster.html +""" + +EVENT_ID = "5d982071-76c5-4733-bf35-b9e38a563c99" + + +def list_events(conn): + print("List Events:") + + for events in conn.clustering.events(): + print(events.to_dict()) + + for events in conn.clustering.events(sort='name:asc'): + print(events.to_dict()) + + +def get_event(conn): + print("Get Event:") + + event = conn.clustering.get_event(EVENT_ID) + print(event.to_dict()) From 2eed3d2e5678e3c4289e9ea50daefed97c2c439b Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Fri, 19 Jan 2018 16:54:06 +0800 Subject: [PATCH 1920/3836] Handle resource deletion properly Fill status code and request id from upstream response directly into our response. Also use general error message for deletion, since there could be other errors than not found from upstream. Change-Id: I1687fecd4ebcd6d8fd4c53b29e496c778314eb17 --- openstack/exceptions.py | 10 +++- openstack/proxy2.py | 9 +++- openstack/tests/unit/test_exceptions.py | 68 +++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 4 deletions(-) diff --git a/openstack/exceptions.py b/openstack/exceptions.py index c1acef598..8d0c16cb9 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -76,7 +76,7 @@ def __init__(self, message='Error', response=None, self.request_id = response.headers.get('x-openstack-request-id') self.status_code = response.status_code else: - self.request_id = None + self.request_id = request_id self.status_code = http_status self.details = details self.url = self.request and self.request.url or None @@ -192,7 +192,13 @@ def raise_from_response(response, error_message=None): else: details = response.text - raise cls(message=error_message, response=response, details=details) + http_status = response.status_code + request_id = response.headers.get('x-openstack-request-id') + + raise cls( + message=error_message, response=response, details=details, + http_status=http_status, request_id=request_id + ) class ArgumentDeprecationWarning(Warning): diff --git a/openstack/proxy2.py b/openstack/proxy2.py index 9db09fe41..33d3ee6f2 100644 --- a/openstack/proxy2.py +++ b/openstack/proxy2.py @@ -136,8 +136,13 @@ def _delete(self, resource_type, value, ignore_missing=True, **attrs): try: rv = res.delete( self, - error_message="No {resource_type} found for {value}".format( - resource_type=resource_type.__name__, value=value)) + error_message=( + "Unable to delete {resource_type} for {value}".format( + resource_type=resource_type.__name__, + value=value, + ) + ) + ) except exceptions.NotFoundException: if ignore_missing: return None diff --git a/openstack/tests/unit/test_exceptions.py b/openstack/tests/unit/test_exceptions.py index 9deb5bab2..2cd55721a 100644 --- a/openstack/tests/unit/test_exceptions.py +++ b/openstack/tests/unit/test_exceptions.py @@ -10,7 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +import mock import testtools +import uuid from openstack import exceptions @@ -55,3 +57,69 @@ def test_http_status(self): self.assertEqual(self.message, exc.message) self.assertEqual(http_status, exc.status_code) + + +class TestRaiseFromResponse(testtools.TestCase): + + def setUp(self): + super(TestRaiseFromResponse, self).setUp() + self.message = "Where is my kitty?" + + def _do_raise(self, *args, **kwargs): + return exceptions.raise_from_response(*args, **kwargs) + + def test_raise_no_exception(self): + response = mock.Mock() + response.status_code = 200 + self.assertIsNone(self._do_raise(response)) + + def test_raise_not_found_exception(self): + response = mock.Mock() + response.status_code = 404 + response.headers = { + 'content-type': 'application/json', + 'x-openstack-request-id': uuid.uuid4().hex, + } + exc = self.assertRaises(exceptions.NotFoundException, + self._do_raise, response, + error_message=self.message) + self.assertEqual(self.message, exc.message) + self.assertEqual(response.status_code, exc.status_code) + self.assertEqual( + response.headers.get('x-openstack-request-id'), + exc.request_id + ) + + def test_raise_bad_request_exception(self): + response = mock.Mock() + response.status_code = 400 + response.headers = { + 'content-type': 'application/json', + 'x-openstack-request-id': uuid.uuid4().hex, + } + exc = self.assertRaises(exceptions.BadRequestException, + self._do_raise, response, + error_message=self.message) + self.assertEqual(self.message, exc.message) + self.assertEqual(response.status_code, exc.status_code) + self.assertEqual( + response.headers.get('x-openstack-request-id'), + exc.request_id + ) + + def test_raise_http_exception(self): + response = mock.Mock() + response.status_code = 403 + response.headers = { + 'content-type': 'application/json', + 'x-openstack-request-id': uuid.uuid4().hex, + } + exc = self.assertRaises(exceptions.HttpException, + self._do_raise, response, + error_message=self.message) + self.assertEqual(self.message, exc.message) + self.assertEqual(response.status_code, exc.status_code) + self.assertEqual( + response.headers.get('x-openstack-request-id'), + exc.request_id + ) From 4df6125166fc368811922636a5a9ad5e242f5677 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 20 Jan 2018 12:34:13 -0600 Subject: [PATCH 1921/3836] Fix releasenotes builds The shade/occ merge brought over stable branch releasenotes files that openstacksdk doesn't have. Remove them. Change-Id: I716ee9cb4146d39daee48f29480fc8fbd89bed4e --- releasenotes/source/index.rst | 8 ++++---- releasenotes/source/mainline.rst | 5 ----- releasenotes/source/mitaka.rst | 6 ------ releasenotes/source/newton.rst | 6 ------ releasenotes/source/unreleased.rst | 6 ------ 5 files changed, 4 insertions(+), 27 deletions(-) delete mode 100644 releasenotes/source/mainline.rst delete mode 100644 releasenotes/source/mitaka.rst delete mode 100644 releasenotes/source/newton.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 74d2b566c..dfea92ba7 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -1,10 +1,10 @@ -===================== - Shade Release Notes -===================== +============================ + openstacksdk Release Notes +============================ .. toctree:: :maxdepth: 1 - mainline unreleased pike + ocata diff --git a/releasenotes/source/mainline.rst b/releasenotes/source/mainline.rst deleted file mode 100644 index 065da7150..000000000 --- a/releasenotes/source/mainline.rst +++ /dev/null @@ -1,5 +0,0 @@ -========================= - Mainline Release Series -========================= - -.. release-notes:: diff --git a/releasenotes/source/mitaka.rst b/releasenotes/source/mitaka.rst deleted file mode 100644 index e54560965..000000000 --- a/releasenotes/source/mitaka.rst +++ /dev/null @@ -1,6 +0,0 @@ -=================================== - Mitaka Series Release Notes -=================================== - -.. release-notes:: - :branch: origin/stable/mitaka diff --git a/releasenotes/source/newton.rst b/releasenotes/source/newton.rst deleted file mode 100644 index 97036ed25..000000000 --- a/releasenotes/source/newton.rst +++ /dev/null @@ -1,6 +0,0 @@ -=================================== - Newton Series Release Notes -=================================== - -.. release-notes:: - :branch: origin/stable/newton diff --git a/releasenotes/source/unreleased.rst b/releasenotes/source/unreleased.rst index abed3d2e3..9826c05b8 100644 --- a/releasenotes/source/unreleased.rst +++ b/releasenotes/source/unreleased.rst @@ -2,10 +2,4 @@ Unreleased Versions ===================== -.. NOTE(dhellmann): The earliest-version field is set to avoid - duplicating *all* of the history on this page. When we start - creating stable branches the history should be truncated - automatically and we can remove the setting. - .. release-notes:: - :earliest-version: 1.17.0 From 32751d6d566b334906bdc1fabbf931ba89740f63 Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Mon, 30 Oct 2017 14:39:27 -0500 Subject: [PATCH 1922/3836] v2 image update fix Passing attributes to jsonpatch.make_patch requires that the "new" value also have the original attributes that are not being modified with the request. If only the new attributes are passed in, jsonpatch creates a bunch of "remove" requests that are rejected by the API. This patch copies the original attributes into a new dict and modifies the relevant ones before passing the new dict to jsonpatch Change-Id: I301e7e736c68a47b6caba8623c434a64cea9fc67 --- openstack/image/v2/image.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index 4b397a443..3ee02b305 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -293,7 +293,12 @@ def update(self, session, **attrs): 'Accept': '' } original = self.to_dict() - patch_string = jsonpatch.make_patch(original, attrs).to_string() + + # Update values from **attrs so they can be passed to jsonpatch + new = self.to_dict() + new.update(**attrs) + + patch_string = jsonpatch.make_patch(original, new).to_string() resp = session.patch(url, data=patch_string, headers=headers) From 7b59cc13f92fb331b874e2b4a9d6745b2744475e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 15 Jan 2018 15:25:51 -0600 Subject: [PATCH 1923/3836] Clean up a bit after the mapping inversion The previous patch got the mapping inverted, but involved places where CaseInsensitiveDict needed to be passed around. It's a quality of Headers, so rework things to allow the dict type to be dictated by the component class. Add some helper methods that do common actions. These, and consume_attrs can almost certainly be turned in to classmethods. Remove calling filter_component or consume_attrs from limits - it's a no-op. The Resource class handles this already. Update the find_keypairs test. requires_id is a bit of a mess ... Change-Id: Ic41f24fd70fb369370292c61a8867b31b8fd92ce --- openstack/compute/v2/keypair.py | 11 +++--- openstack/compute/v2/limits.py | 6 +--- openstack/key_manager/v1/secret.py | 4 +-- openstack/resource2.py | 55 ++++++++++++++++++++++-------- 4 files changed, 49 insertions(+), 27 deletions(-) diff --git a/openstack/compute/v2/keypair.py b/openstack/compute/v2/keypair.py index 324d29e7d..e11266218 100644 --- a/openstack/compute/v2/keypair.py +++ b/openstack/compute/v2/keypair.py @@ -45,11 +45,12 @@ class Keypair(resource2.Resource): #: The SSH public key that is paired with the server. public_key = resource2.Body('public_key') - def __init__(self, **attrs): - # TODO(mordred) Maybe let's figure out how to make the base constructor - # do this if alternate_id is specified? - attrs.setdefault('name', attrs.pop('id', None)) - super(Keypair, self).__init__(**attrs) + def _consume_attrs(self, mapping, attrs): + # TODO(mordred) This should not be required. However, without doing + # it **SOMETIMES** keypair picks up id and not name. This is a hammer. + if 'id' in attrs: + attrs.setdefault('name', attrs.pop('id')) + return super(Keypair, self)._consume_attrs(mapping, attrs) @classmethod def list(cls, session, paginated=False): diff --git a/openstack/compute/v2/limits.py b/openstack/compute/v2/limits.py index 48360da26..d3449efd3 100644 --- a/openstack/compute/v2/limits.py +++ b/openstack/compute/v2/limits.py @@ -92,16 +92,12 @@ def get(self, session, requires_id=False, error_message=None): body = response.json() body = body[self.resource_key] - absolute_body = self._consume_attrs( - AbsoluteLimits._body_mapping(), body["absolute"]) - self.absolute = AbsoluteLimits.existing(**absolute_body) + self.absolute = AbsoluteLimits.existing(**body["absolute"]) rates_body = body["rate"] rates = [] for rate_body in rates_body: - rate_body = self._consume_attrs( - RateLimit._body_mapping(), rate_body) rates.append(RateLimit(**rate_body)) self.rate = rates diff --git a/openstack/key_manager/v1/secret.py b/openstack/key_manager/v1/secret.py index 91fe78e5b..15b6ebea2 100644 --- a/openstack/key_manager/v1/secret.py +++ b/openstack/key_manager/v1/secret.py @@ -97,8 +97,6 @@ def get(self, session, requires_id=True): response["payload"] = payload.text # We already have the JSON here so don't call into _translate_response - body = self._consume_attrs(self._body_mapping(), response) - self._body.attributes.update(body) - self._body.clean() + self._update_from_body_attrs(response) return self diff --git a/openstack/resource2.py b/openstack/resource2.py index e175d0db4..e8e654eaa 100644 --- a/openstack/resource2.py +++ b/openstack/resource2.py @@ -45,6 +45,8 @@ class _BaseComponent(object): # The name this component is being tracked as in the Resource key = None + # The class to be used for mappings + _map_cls = dict def __init__(self, name, type=None, default=None, alias=None, alternate_id=False, **kwargs): @@ -124,6 +126,7 @@ class Header(_BaseComponent): """Header attributes""" key = "_header" + _map_cls = structures.CaseInsensitiveDict class URI(_BaseComponent): @@ -342,15 +345,41 @@ def _collect_attrs(self, attrs): that correspond to the relevant body, header, and uri attributes that exist on this class. """ - body = self._consume_attrs(self._body_mapping(), attrs) - header = self._consume_attrs( - self._header_mapping(), attrs, - map_cls=structures.CaseInsensitiveDict) - uri = self._consume_attrs(self._uri_mapping(), attrs) + body = self._consume_body_attrs(attrs) + header = self._consume_header_attrs(attrs) + uri = self._consume_uri_attrs(attrs) return body, header, uri - def _consume_attrs(self, mapping, attrs, map_cls=dict): + def _consume_body_attrs(self, attrs): + return self._consume_mapped_attrs(Body, attrs) + + def _consume_header_attrs(self, attrs): + return self._consume_mapped_attrs(Header, attrs) + + def _consume_uri_attrs(self, attrs): + return self._consume_mapped_attrs(URI, attrs) + + def _update_from_body_attrs(self, attrs): + body = self._consume_body_attrs(attrs) + self._body.attributes.update(body) + self._body.clean() + + def _update_from_header_attrs(self, attrs): + headers = self._consume_header_attrs(attrs) + self._header.attributes.update(headers) + self._header.clean() + + def _update_uri_from_attrs(self, attrs): + uri = self._consume_uri_attrs(attrs) + self._uri.attributes.update(uri) + self._uri.clean() + + def _consume_mapped_attrs(self, mapping_cls, attrs): + mapping = self._get_mapping(mapping_cls) + return self._consume_attrs(mapping, attrs) + + def _consume_attrs(self, mapping, attrs): """Given a mapping and attributes, return relevant matches This method finds keys in attrs that exist in the mapping, then @@ -382,10 +411,10 @@ def _consume_attrs(self, mapping, attrs, map_cls=dict): return relevant_attrs @classmethod - def _get_mapping(cls, component, map_cls=dict): + def _get_mapping(cls, component): """Return a dict of attributes of a given component on the class""" - mapping = map_cls() - ret = map_cls() + mapping = component._map_cls() + ret = component._map_cls() # Since we're looking at class definitions we need to include # subclasses, so check the whole MRO. for klass in cls.__mro__: @@ -408,8 +437,7 @@ def _body_mapping(cls): @classmethod def _header_mapping(cls): """Return all Header members of this class""" - return cls._get_mapping( - Header, map_cls=structures.CaseInsensitiveDict) + return cls._get_mapping(Header) @classmethod def _uri_mapping(cls): @@ -580,12 +608,11 @@ def _translate_response(self, response, has_body=None, error_message=None): if self.resource_key and self.resource_key in body: body = body[self.resource_key] - body = self._consume_attrs(self._body_mapping(), body) + body = self._consume_body_attrs(body) self._body.attributes.update(body) self._body.clean() - headers = self._consume_attrs( - self._header_mapping(), response.headers.copy()) + headers = self._consume_header_attrs(response.headers) self._header.attributes.update(headers) self._header.clean() From e1c16e6044d39fef2f3e6e21d9b813e20bd9549e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 16 Jan 2018 16:12:29 -0600 Subject: [PATCH 1924/3836] Fix typo in the external service loader code Change-Id: I57b4c6747688df229194622645265832f07bea20 --- openstack/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/connection.py b/openstack/connection.py index 37bb948a6..0b998a368 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -252,7 +252,7 @@ class contained in # If we don't have a proxy, just instantiate BaseProxy so that # we get an adapter. if isinstance(service, six.string_types): - service_type = service_description + service_type = service service = service_description.ServiceDescription(service_type) else: service_type = service.service_type From 3afec7512e58ab83bf00f3103b8250ac26ccac41 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 16 Jan 2018 16:16:05 -0600 Subject: [PATCH 1925/3836] Fixed a few nits in the README These were called out in an earlier review. Change-Id: Ib1da2606c93782641a3d7a07a1aad9ddf331d208 --- README.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 7079da180..0ffddfab5 100644 --- a/README.rst +++ b/README.rst @@ -9,7 +9,7 @@ complete documentation, examples, and tools. It also contains an abstraction interface layer. Clouds can do many things, but there are probably only about 10 of them that most people care about with any regularity. If you want to do complicated things, the per-service oriented -portions of the SDK are for you. However, if what you want to be able to +portions of the SDK are for you. However, if what you want is to be able to write an application that talks to clouds no matter what crazy choices the deployer has made in an attempt to be more hipster than their self-entitled narcissist peers, then the Cloud Abstraction layer is for you. @@ -59,10 +59,10 @@ This led to the merge of the three projects. The original contents of the shade library have been moved into ``openstack.cloud`` and os-client-config has been moved in to -``openstack.config``. The next release of shade will be a thin compatibility -layer that subclasses the objects from ``openstack.cloud`` and provides -different argument defaults where needed for compat. -Similarly the next release of os-client-config will be a compat +``openstack.config``. Future releases of shade will provide a thin +compatibility layer that subclasses the objects from ``openstack.cloud`` +and provides different argument defaults where needed for compatibility. +Similarly future releases of os-client-config will provide a compatibility layer shim around ``openstack.config``. .. note:: From a85f52d1094e9eac5ab9f5ee3fd007c3402f4920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Sat, 20 Jan 2018 22:43:01 +0100 Subject: [PATCH 1926/3836] Make meta.find_best_address() more generic This function now can choose address family with socket.getaddrinfo() method and there is no need to pass address family as argument to it. Change-Id: I72b1c1263876f2d047e5e540b72f2550ec73b65c Story: 2001475 Task: 6213 (cherry picked from commit c4067e3baf7b14d687c6ab6aafd43c291f138091) --- openstack/cloud/meta.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/openstack/cloud/meta.py b/openstack/cloud/meta.py index 3c57a1a04..da669ca5c 100644 --- a/openstack/cloud/meta.py +++ b/openstack/cloud/meta.py @@ -88,7 +88,7 @@ def get_server_ip(server, public=False, cloud_public=True, **kwargs): """ addrs = find_nova_addresses(server['addresses'], **kwargs) return find_best_address( - addrs, socket.AF_INET, public=public, cloud_public=cloud_public) + addrs, public=public, cloud_public=cloud_public) def get_server_private_ip(server, cloud=None): @@ -211,7 +211,7 @@ def get_server_external_ipv4(cloud, server): return None -def find_best_address(addresses, family, public=False, cloud_public=True): +def find_best_address(addresses, public=False, cloud_public=True): do_check = public == cloud_public if not addresses: return None @@ -224,10 +224,13 @@ def find_best_address(addresses, family, public=False, cloud_public=True): for address in addresses: # Return the first one that is reachable try: - connect_socket = socket.socket(family, socket.SOCK_STREAM, 0) - connect_socket.settimeout(1) - connect_socket.connect((address, 22, 0, 0)) - return address + for res in socket.getaddrinfo( + address, 22, socket.AF_UNSPEC, socket.SOCK_STREAM, 0): + family, socktype, proto, _, sa = res + connect_socket = socket.socket(family, socktype, proto) + connect_socket.settimeout(1) + connect_socket.connect(sa) + return address except Exception: pass # Give up and return the first - none work as far as we can tell @@ -252,7 +255,7 @@ def get_server_external_ipv6(server): if server['accessIPv6']: return server['accessIPv6'] addresses = find_nova_addresses(addresses=server['addresses'], version=6) - return find_best_address(addresses, socket.AF_INET6, public=True) + return find_best_address(addresses, public=True) def get_server_default_ip(cloud, server): From be063c4ef5068c114282be4dc3a2afaaf49e56f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Sat, 20 Jan 2018 21:57:36 +0100 Subject: [PATCH 1927/3836] Make floating IP to be prefered over fixed when looking for IP We can assume that floating IP is public IP address in clouds so floating IPs should be checked first if they can be used to connect to instance. Change-Id: I1024d78e68a788bf5a92cb63d008c02180908dfa Story: 2001475 Task: 6212 (cherry picked from commit 506b2f8adc431ae436201770e7dbfb135ed3e0e4) --- openstack/cloud/meta.py | 10 ++++++++-- openstack/tests/unit/cloud/test_meta.py | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/openstack/cloud/meta.py b/openstack/cloud/meta.py index 3c57a1a04..47fc75ef8 100644 --- a/openstack/cloud/meta.py +++ b/openstack/cloud/meta.py @@ -70,8 +70,14 @@ def find_nova_addresses(addresses, ext_tag=None, key_name=None, version=4, mac_addr=None): interfaces = find_nova_interfaces(addresses, ext_tag, key_name, version, mac_addr) - addrs = [i['addr'] for i in interfaces] - return addrs + floating_addrs = [] + fixed_addrs = [] + for i in interfaces: + if i.get('OS-EXT-IPS:type') == 'floating': + floating_addrs.append(i['addr']) + else: + fixed_addrs.append(i['addr']) + return floating_addrs + fixed_addrs def get_server_ip(server, public=False, cloud_public=True, **kwargs): diff --git a/openstack/tests/unit/cloud/test_meta.py b/openstack/tests/unit/cloud/test_meta.py index de3da10d6..02790cc80 100644 --- a/openstack/tests/unit/cloud/test_meta.py +++ b/openstack/tests/unit/cloud/test_meta.py @@ -264,6 +264,21 @@ def test_find_nova_addresses_all(self): self.assertEqual([], meta.find_nova_addresses( addrs, key_name='public', ext_tag='fixed', version=6)) + def test_find_nova_addresses_floating_first(self): + # Note 198.51.100.0/24 is TEST-NET-2 from rfc5737 + addrs = { + 'private': [{ + 'addr': '192.0.2.5', + 'version': 4, + 'OS-EXT-IPS:type': 'fixed'}], + 'public': [{ + 'addr': '198.51.100.1', + 'version': 4, + 'OS-EXT-IPS:type': 'floating'}]} + self.assertEqual( + ['198.51.100.1', '192.0.2.5'], + meta.find_nova_addresses(addrs)) + def test_get_server_ip(self): srv = meta.obj_to_munch(standard_fake_server) self.assertEqual( From 119a26dcfcb7bcb04e358a66a637652117422f11 Mon Sep 17 00:00:00 2001 From: Hunt Xu Date: Tue, 23 Jan 2018 21:34:28 +0800 Subject: [PATCH 1928/3836] Fix an error about listing projects in connection doc Change-Id: I0358e12f925e95d4913370149902cf0934719e6e --- openstack/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/connection.py b/openstack/connection.py index 16e857539..09ea4ab96 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -62,7 +62,7 @@ Services are accessed through an attribute named after the service's official service-type. A list of all the projects is retrieved in this manner:: - projects = conn.identity.list_projects() + projects = [project for project in conn.identity.projects()] Find or create ~~~~~~~~~~~~~~ From a984bd3a090a49870624d5c089342318e85fedb4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 16 Nov 2017 16:11:34 -0600 Subject: [PATCH 1929/3836] Add OSC functional tips jobs Breaking OSC is bad, mmmkay? Change-Id: Ie0d519a295cd405f80fd2a52eaba2d195973ac4f Depends-On: https://review.openstack.org/#/c/524991/ Depends-On: https://review.openstack.org/#/c/536839/ --- .zuul.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.zuul.yaml b/.zuul.yaml index 5160dcff6..9b57e2d8e 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -205,6 +205,8 @@ - openstacksdk-functional-devstack-magnum: voting: false - openstacksdk-functional-devstack-python3 + - osc-functional-devstack-tips: + voting: false gate: jobs: - openstacksdk-functional-devstack From 04bafdc669be02dd99db4a794f55182fddd23963 Mon Sep 17 00:00:00 2001 From: "Yuanbin.Chen" Date: Mon, 22 Jan 2018 09:40:02 +0800 Subject: [PATCH 1930/3836] Fix clustering detach policy describe error Change-Id: I1078dfb9d4e9e4a075c8508f538d978f98254ed0 Signed-off-by: Yuanbin.Chen --- openstack/clustering/v1/_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/clustering/v1/_proxy.py b/openstack/clustering/v1/_proxy.py index cb9aca317..562f8abde 100644 --- a/openstack/clustering/v1/_proxy.py +++ b/openstack/clustering/v1/_proxy.py @@ -488,7 +488,7 @@ def attach_policy_to_cluster(self, cluster, policy, **params): @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", details="Use detach_policy_from_cluster instead") def cluster_detach_policy(self, cluster, policy): - """Attach a policy to a cluster. + """Detach a policy from a cluster. :param cluster: Either the name or the ID of the cluster, or an instance of :class:`~openstack.clustering.v1.cluster.Cluster`. From 3c782b55822d0ebe52faaae80638715662bed8a0 Mon Sep 17 00:00:00 2001 From: "Yuanbin.Chen" Date: Mon, 22 Jan 2018 09:37:35 +0800 Subject: [PATCH 1931/3836] Add clustering guides cluster file, examples cluster code Change-Id: I7a86398b86a4317100b454f1bd45a8eafc6ae4d4 Signed-off-by: Yuanbin.Chen --- doc/source/user/guides/clustering/cluster.rst | 177 +++++++++++++++++- examples/clustering/cluster.py | 172 +++++++++++++++++ 2 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 examples/clustering/cluster.py diff --git a/doc/source/user/guides/clustering/cluster.rst b/doc/source/user/guides/clustering/cluster.rst index b4772ef33..7ee5134e0 100644 --- a/doc/source/user/guides/clustering/cluster.rst +++ b/doc/source/user/guides/clustering/cluster.rst @@ -15,4 +15,179 @@ Managing Clusters ================= -.. TODO(Qiming): Implement this guide +Clusters are first-class citizens in Senlin service design. A cluster is +defined as a collection of homogeneous objects. The "homogeneous" here means +that the objects managed (aka. Nodes) have to be instantiated from the same +"profile type". + + +List Clusters +~~~~~~~~~~~~~ + +To examine the list of receivers: + +.. literalinclude:: ../../examples/clustering/cluster.py + :pyobject: list_cluster + +When listing clusters, you can specify the sorting option using the ``sort`` +parameter and you can do pagination using the ``limit`` and ``marker`` +parameters. + +Full example: `manage cluster`_ + + +Create Cluster +~~~~~~~~~~~~~~ + +When creating a cluster, you will provide a dictionary with keys and values +according to the cluster type referenced. + +.. literalinclude:: ../../examples/clustering/cluster.py + :pyobject: create_cluster + +Optionally, you can specify a ``metadata`` keyword argument that contains some +key-value pairs to be associated with the cluster. + +Full example: `manage cluster`_ + + +Get Cluster +~~~~~~~~~~~ + +To get a cluster based on its name or ID: + +.. literalinclude:: ../../examples/clustering/cluster.py + :pyobject: get_cluster + +Full example: `manage cluster`_ + + +Find Cluster +~~~~~~~~~~~~ + +To find a cluster based on its name or ID: + +.. literalinclude:: ../../examples/clustering/cluster.py + :pyobject: find_cluster + +Full example: `manage cluster`_ + + +Update Cluster +~~~~~~~~~~~~~~ + +After a cluster is created, most of its properties are immutable. Still, you +can update a cluster's ``name`` and/or ``params``. + +.. literalinclude:: ../../examples/clustering/cluster.py + :pyobject: update_cluster + +Full example: `manage cluster`_ + + +Delete Cluster +~~~~~~~~~~~~~~ + +A cluster can be deleted after creation, When there are nodes in the cluster, +the Senlin engine will launch a process to delete all nodes from the cluster +and destroy them before deleting the cluster object itself. + +.. literalinclude:: ../../examples/clustering/cluster.py + :pyobject: delete_cluster + + +Cluster Add Nodes +~~~~~~~~~~~~~~~~~ + +Add some existing nodes into the specified cluster. + +.. literalinclude:: ../../examples/clustering/cluster.py + :pyobject: cluster_add_nodes + + +Cluster Del Nodes +~~~~~~~~~~~~~~~~~ + +Remove nodes from specified cluster. + +.. literalinclude:: ../../examples/clustering/cluster.py + :pyobject: cluster_del_nodes + + +Cluster Replace Nodes +~~~~~~~~~~~~~~~~~~~~~ + +Replace some existing nodes in the specified cluster. + +.. literalinclude:: ../../examples/clustering/cluster.py + :pyobject: cluster_replace_nodes + + +Cluster Scale Out +~~~~~~~~~~~~~~~~~ + +Inflate the size of a cluster. + +.. literalinclude:: ../../examples/clustering/cluster.py + :pyobject: cluster_scale_out + + +Cluster Scale In +~~~~~~~~~~~~~~~~ + +Shrink the size of a cluster. + +.. literalinclude:: ../../examples/clustering/cluster.py + :pyobject: cluster_scale_in + + +Cluster Resize +~~~~~~~~~~~~~~ + +Resize of cluster. + +.. literalinclude:: ../../examples/clustering/cluster.py + :pyobject: cluster_resize + + +Cluster Policy Attach +~~~~~~~~~~~~~~~~~~~~~ + +Once a policy is attached (bound) to a cluster, it will be +enforced when related actions are performed on that cluster, +unless the policy is (temporarily) disabled on the cluster + +.. literalinclude:: ../../examples/clustering/cluster.py + :pyobject: cluster_attach_policy + + +Cluster Policy Detach +~~~~~~~~~~~~~~~~~~~~~ + +Once a policy is attached to a cluster, it can be detached +from the cluster at user's request. + +.. literalinclude:: ../../examples/clustering/cluster.py + :pyobject: cluster_detach_policy + + +Cluster Check +~~~~~~~~~~~~~ + +Check cluster health status, Cluster members can be check. + +.. literalinclude:: ../../examples/clustering/cluster.py + :pyobject: check_cluster + + +Cluster Recover +~~~~~~~~~~~~~~~ + +To restore a specified cluster, members in the cluster will be checked. + +.. literalinclude:: ../../examples/clustering/cluster.py + :pyobject: recover_cluster + + +.. _manage cluster: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/clustering/cluster.py + diff --git a/examples/clustering/cluster.py b/examples/clustering/cluster.py new file mode 100644 index 000000000..a4d46ce08 --- /dev/null +++ b/examples/clustering/cluster.py @@ -0,0 +1,172 @@ +# 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. + +""" +Managing policies in the Cluster service. + +For a full guide see +https://developer.openstack.org/sdks/python/openstacksdk/user/guides/cluster.html +""" + +CLUSTER_NAME = "Test_Cluster" +CLUSTER_ID = "47d808e5-ce75-4a1e-bfd2-4ed4639e8640" +PROFILE_ID = "b0e3a680-e270-4eb8-9361-e5c9503fba0a" +NODE_ID = "dd803d4a-015d-4223-b15f-db29bad3146c" +POLICY_ID = "c0e3a680-e270-4eb8-9361-e5c9503fba00" + + +def list_cluster(conn): + print("List clusters:") + + for cluster in conn.clustering.clusters(): + print(cluster.to_dict()) + + for cluster in conn.clustering.clusters(sort='name:asc'): + print(cluster.to_dict()) + + +def create_cluster(conn): + print("Create cluster:") + + spec = { + "name": CLUSTER_NAME, + "profile_id": PROFILE_ID, + "min_size": 0, + "max_size": -1, + "desired_capacity": 1, + } + + cluster = conn.clustering.create_cluster(**spec) + print(cluster.to_dict()) + + +def get_cluster(conn): + print("Get cluster:") + + cluster = conn.clustering.get_cluster(CLUSTER_ID) + print(cluster.to_dict()) + + +def find_cluster(conn): + print("Find cluster:") + + cluster = conn.clustering.find_cluster(CLUSTER_ID) + print(cluster.to_dict()) + + +def update_cluster(conn): + print("Update cluster:") + + spec = { + "name": "Test_Cluster001", + "profile_id": "c0e3a680-e270-4eb8-9361-e5c9503fba0a", + "profile_only": True, + } + cluster = conn.clustering.update_cluster(CLUSTER_ID, **spec) + print(cluster.to_dict()) + + +def delete_cluster(conn): + print("Delete cluster:") + + conn.clustering.delete_cluster(CLUSTER_ID) + print("Cluster deleted.") + + # cluster support force delete + conn.clustering.delete_cluster(CLUSTER_ID, False, True) + print("Cluster deleted") + + +def cluster_add_nodes(conn): + print("Add nodes to cluster:") + + node_ids = [NODE_ID] + res = conn.clustering.cluster_add_nodes(CLUSTER_ID, node_ids) + print(res.to_dict()) + + +def cluster_del_nodes(conn): + print("Remove nodes from a cluster:") + + node_ids = [NODE_ID] + res = conn.clustering.cluster_del_nodes(CLUSTER_ID, node_ids) + print(res.to_dict()) + + +def cluster_replace_nodes(conn): + print("Replace the nodes in a cluster with specified nodes:") + + old_node = NODE_ID + new_node = "cd803d4a-015d-4223-b15f-db29bad3146c" + spec = { + old_node: new_node + } + res = conn.clustering.cluster_replace_nodes(CLUSTER_ID, **spec) + print(res.to_dict()) + + +def cluster_scale_out(conn): + print("Inflate the size of a cluster:") + + res = conn.clustering.cluster_scale_out(CLUSTER_ID, 1) + print(res.to_dict()) + + +def cluster_scale_in(conn): + print("Shrink the size of a cluster:") + + res = conn.clustering.cluster_scale_in(CLUSTER_ID, 1) + print(res.to_dict()) + + +def cluster_resize(conn): + print("Resize of cluster:") + + spec = { + 'min_size': 1, + 'max_size': 6, + 'adjustment_type': 'EXACT_CAPACITY', + 'number': 2 + } + res = conn.clustering.cluster_resize(CLUSTER_ID, **spec) + print(res.to_dict()) + + +def cluster_attach_policy(conn): + print("Attach policy to a cluster:") + + spec = {'enabled': True} + res = conn.clustering.cluster_attach_policy(CLUSTER_ID, POLICY_ID, + **spec) + print(res.to_dict()) + + +def cluster_detach_policy(conn): + print("Detach a policy from a cluster:") + + res = conn.clustering.cluster_detach_policy(CLUSTER_ID, POLICY_ID) + print(res.to_dict()) + + +def check_cluster(conn): + print("Check cluster:") + + res = conn.clustering.check_cluster(CLUSTER_ID) + print(res.to_dict()) + + +def recover_cluster(conn): + print("Recover cluster:") + + spec = {'check': True} + res = conn.clustering.recover_cluster(CLUSTER_ID, **spec) + print(res.to_dict()) From 7803466ed2b12502a4e16bbe4f1c2078a0198646 Mon Sep 17 00:00:00 2001 From: "Yuanbin.Chen" Date: Tue, 23 Jan 2018 17:35:47 +0800 Subject: [PATCH 1932/3836] Add clustering guides node file, examples node code Change-Id: I905c424f86ef1f838ad2b637cb623f9ce7025466 Signed-off-by: Yuanbin.Chen --- doc/source/user/guides/clustering/node.rst | 104 ++++++++++++++++++++- examples/clustering/node.py | 93 ++++++++++++++++++ 2 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 examples/clustering/node.py diff --git a/doc/source/user/guides/clustering/node.rst b/doc/source/user/guides/clustering/node.rst index d4e2f54f1..ec89ed48b 100644 --- a/doc/source/user/guides/clustering/node.rst +++ b/doc/source/user/guides/clustering/node.rst @@ -15,4 +15,106 @@ Managing Nodes ============== -.. TODO(Qiming): Implement this guide +Node is a logical object managed by the Senlin service. A node can be a member +of at most one cluster at any time. A node can be an orphan node which means +it doesn't belong to any clusters. + + +List Nodes +~~~~~~~~~~ + +To examine the list of Nodes: + +.. literalinclude:: ../../examples/clustering/node.py + :pyobject: list_nodes + +When listing nodes, you can specify the sorting option using the ``sort`` +parameter and you can do pagination using the ``limit`` and ``marker`` +parameters. + +Full example: `manage node`_ + + +Create Node +~~~~~~~~~~~ + +When creating a node, you will provide a dictionary with keys and values +according to the node type referenced. + +.. literalinclude:: ../../examples/clustering/node.py + :pyobject: create_node + +Optionally, you can specify a ``metadata`` keyword argument that contains some +key-value pairs to be associated with the node. + +Full example: `manage node`_ + + +Get Node +~~~~~~~~ + +To get a node based on its name or ID: + +.. literalinclude:: ../../examples/clustering/node.py + :pyobject: get_node + +Full example: `manage node`_ + + +Find Node +~~~~~~~~~ + +To find a node based on its name or ID: + +.. literalinclude:: ../../examples/clustering/node.py + :pyobject: find_node + +Full example: `manage node`_ + + +Update Node +~~~~~~~~~~~ + +After a node is created, most of its properties are immutable. Still, you +can update a node's ``name`` and/or ``params``. + +.. literalinclude:: ../../examples/clustering/node.py + :pyobject: update_node + +Full example: `manage node`_ + + +Delete Node +~~~~~~~~~~~ + +A node can be deleted after creation, provided that it is not referenced +by any active clusters. If you attempt to delete a node that is still in +use, you will get an error message. + +.. literalinclude:: ../../examples/clustering/node.py + :pyobject: delete_node + +Full example: `manage node`_ + + +Check Node +~~~~~~~~~~ + +If the underlying physical resource is not healthy, the node will be set +to ERROR status. + +.. literalinclude:: ../../examples/clustering/node.py + :pyobject: check_node + +Full example: `manage node`_ + + +Recover Node +~~~~~~~~~~~~ + +To restore a specified node. + +.. literalinclude:: ../../examples/clustering/node.py + :pyobject: recover_node + +.. _manage node: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/clustering/node.py diff --git a/examples/clustering/node.py b/examples/clustering/node.py new file mode 100644 index 000000000..40910b452 --- /dev/null +++ b/examples/clustering/node.py @@ -0,0 +1,93 @@ +# 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. + +""" +Managing policies in the Cluster service. + +For a full guide see +https://developer.openstack.org/sdks/python/openstacksdk/user/guides/cluster.html +""" + +NODE_NAME = 'Test_Node' +NODE_ID = 'dd803d4a-015d-4223-b15f-db29bad3146c' +PROFILE_ID = "b0e3a680-e270-4eb8-9361-e5c9503fba0a" + + +def list_nodes(conn): + print("List Nodes:") + + for node in conn.clustering.nodes(): + print(node.to_dict()) + for node in conn.clustering.nodes(sort='asc:name'): + print(node.to_dict()) + + +def create_node(conn): + print("Create Node:") + + spec = { + 'name': NODE_NAME, + 'profile_id': PROFILE_ID, + } + node = conn.clustering.create_node(**spec) + print(node.to_dict()) + + +def get_node(conn): + print("Get Node:") + + node = conn.clustering.get_node(NODE_ID) + print(node.to_dict()) + + +def find_node(conn): + print("Find Node:") + + node = conn.clustering.find_node(NODE_ID) + print(node.to_dict()) + + +def update_node(conn): + print("Update Node:") + + spec = { + 'name': 'Test_Node01', + 'profile_id': 'c0e3a680-e270-4eb8-9361-e5c9503fba0b', + } + + node = conn.clustering.update_node(NODE_ID, **spec) + print(node.to_dict()) + + +def delete_node(conn): + print("Delete Node:") + + conn.clustering.delete_node(NODE_ID) + print("Node deleted.") + # node support force delete + conn.clustering.delete_node(NODE_ID, False, True) + print("Node deleted") + + +def check_node(conn): + print("Check Node:") + + node = conn.clustering.check_node(NODE_ID) + print(node.to_dict()) + + +def recover_node(conn): + print("Recover Node:") + + spec = {'check': True} + node = conn.clustering.recover_node(NODE_ID, **spec) + print(node.to_dict()) From d26bc12f71f59594e886097d0999937757b36c62 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 24 Jan 2018 05:40:26 -0600 Subject: [PATCH 1933/3836] Update docs and pep8 tox environments Make the docs tox environment use doc/requirements. For pep8, just install the linters and skip installing the package. Doing this but leaving in the constraints file keeps us in line with upper-constraints on linters but lets our pep8 envs be tiny. Finally, remove the doc8 line-length setting. It's not a big enough difference to warrant changing it. Change-Id: I3df81474fb52e2587d22c7789b6b553139eb37f2 --- doc/source/user/config/configuration.rst | 14 +++++----- doc/source/user/guides/network.rst | 4 +-- test-requirements.txt | 3 --- tox.ini | 33 +++++++++++++----------- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/doc/source/user/config/configuration.rst b/doc/source/user/config/configuration.rst index 0282cf219..b766921c8 100644 --- a/doc/source/user/config/configuration.rst +++ b/doc/source/user/config/configuration.rst @@ -11,8 +11,8 @@ Environment Variables provide backwards compatibility to service-specific variables such as `NOVA_USERNAME`. -If you have OpenStack environment variables set, `os-client-config` will produce -a cloud config object named `envvars` containing your values from the +If you have OpenStack environment variables set, `os-client-config` will +produce a cloud config object named `envvars` containing your values from the environment. If you don't like the name `envvars`, that's ok, you can override it by setting `OS_CLOUD_NAME`. @@ -119,11 +119,11 @@ location rules previously mentioned for the config files. `regions` can be a list of regions. When you call `get_all_clouds`, you'll get a cloud config object for each cloud/region combo. -As seen with `dns_service_type`, any setting that makes sense to be per-service, -like `service_type` or `endpoint` or `api_version` can be set by prefixing -the setting with the default service type. That might strike you funny when -setting `service_type` and it does me too - but that's just the world we live -in. +As seen with `dns_service_type`, any setting that makes sense to be +per-service, like `service_type` or `endpoint` or `api_version` can be set +by prefixing the setting with the default service type. That might strike you +funny when setting `service_type` and it does me too - but that's just the +world we live in. Auth Settings ------------- diff --git a/doc/source/user/guides/network.rst b/doc/source/user/guides/network.rst index b403c4a6e..b8fe9338b 100644 --- a/doc/source/user/guides/network.rst +++ b/doc/source/user/guides/network.rst @@ -56,8 +56,8 @@ List Security Groups -------------------- A **security group** acts as a virtual firewall for servers. It is a container -for security group rules which specify the type of network traffic and direction -that is allowed to pass through a port. +for security group rules which specify the type of network traffic and +direction that is allowed to pass through a port. .. literalinclude:: ../examples/network/list.py :pyobject: list_security_groups diff --git a/test-requirements.txt b/test-requirements.txt index 4584371f0..236250da9 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,10 +1,7 @@ # 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.13.0,<0.14,>=0.12.0 # Apache-2.0 - coverage!=4.4,>=4.0 # Apache-2.0 -doc8>=0.6.0 # Apache-2.0 extras>=1.0.0 # MIT fixtures>=3.0.0 # Apache-2.0/BSD jsonschema<3.0.0,>=2.6.0 # MIT diff --git a/tox.ini b/tox.ini index 46b0ff93a..386918031 100644 --- a/tox.ini +++ b/tox.ini @@ -12,9 +12,9 @@ setenv = LANGUAGE=en_US:en LC_ALL=C deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} - -r{toxinidir}/test-requirements.txt - -r{toxinidir}/requirements.txt + -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} + -r{toxinidir}/test-requirements.txt + -r{toxinidir}/requirements.txt commands = stestr run {posargs} stestr slowest @@ -30,14 +30,18 @@ commands = stestr --test-path ./openstack/tests/functional run --serial {posargs stestr slowest [testenv:pep8] +usedevelop = False +skip_install = True deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} - -r{toxinidir}/requirements.txt - -r{toxinidir}/test-requirements.txt - -r{toxinidir}/doc/requirements.txt + -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} + doc8 + hacking + pygments + readme commands = - doc8 doc/source - flake8 + doc8 doc/source + python setup.py check -r -s + flake8 [testenv:venv] commands = {posargs} @@ -65,14 +69,15 @@ passenv = HOME USER commands = {toxinidir}/extras/run-ansible-tests.sh -e {envdir} {posargs} [testenv:docs] -basepython = python3 deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} - -r{toxinidir}/requirements.txt - -r{toxinidir}/doc/requirements.txt + -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} + -r{toxinidir}/requirements.txt + -r{toxinidir}/doc/requirements.txt commands = sphinx-build -W -d doc/build/doctrees -b html doc/source/ doc/build/html [testenv:releasenotes] +usedevelop = False +skip_install = True commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [flake8] @@ -91,5 +96,3 @@ exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build [doc8] extensions = .rst, .yaml -# Maximal line length should be 80. -max-line-length = 80 From 3326bb01aab237bb02d1234864241b90d0955b0b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 15 Jan 2018 16:41:47 -0600 Subject: [PATCH 1934/3836] Update type conversion to handle subtypes more better We have pretty much enough information in the Component declarations to handle arbitrarily nested data descriptions. That's super cool. The only thing we're missing is dealing with lists of declared types - and of transparently handling sub-resources. Resource objects want their construction parameters handed to them as **kwargs arguments. So we need to figure out if the source data is a dict and the target types is a Component, so we can pass the values to the type constructor with **. For lists, introduce a new Component parameter "list_type" which declares the type of list in question. We could probably swing around and make our own parameterizable List formatter and make this go away. For now, this is easy enough. The results can be seen with compute.v2.limits - the custom get method can (almost completely) go away, since the type declaration can just handle it for us. Change-Id: Ieb5c3b60853000101504b82f1cd01b1a25370bf1 --- openstack/compute/v2/limits.py | 26 +++----- openstack/object_store/v1/obj.py | 2 - openstack/resource2.py | 63 ++++++++++++++----- .../tests/unit/compute/v2/test_limits.py | 11 ++++ .../tests/unit/object_store/v1/test_obj.py | 4 +- openstack/tests/unit/test_resource2.py | 11 ++-- 6 files changed, 75 insertions(+), 42 deletions(-) diff --git a/openstack/compute/v2/limits.py b/openstack/compute/v2/limits.py index d3449efd3..e66b3e81b 100644 --- a/openstack/compute/v2/limits.py +++ b/openstack/compute/v2/limits.py @@ -58,6 +58,8 @@ class AbsoluteLimits(resource2.Resource): class RateLimit(resource2.Resource): + # TODO(mordred) Make a resource type for the contents of limit and add + # it to list_type here. #: A list of the specific limits that apply to the ``regex`` and ``uri``. limits = resource2.Body("limit", type=list) #: A regex representing which routes this rate limit applies to. @@ -74,7 +76,7 @@ class Limits(resource2.Resource): allow_get = True absolute = resource2.Body("absolute", type=AbsoluteLimits) - rate = resource2.Body("rate", type=list) + rate = resource2.Body("rate", type=list, list_type=RateLimit) def get(self, session, requires_id=False, error_message=None): """Get the Limits resource. @@ -85,21 +87,7 @@ def get(self, session, requires_id=False, error_message=None): :returns: A Limits instance :rtype: :class:`~openstack.compute.v2.limits.Limits` """ - request = self._prepare_request(requires_id=False, prepend_key=False) - - response = session.get(request.url, error_message=error_message) - - body = response.json() - body = body[self.resource_key] - - self.absolute = AbsoluteLimits.existing(**body["absolute"]) - - rates_body = body["rate"] - - rates = [] - for rate_body in rates_body: - rates.append(RateLimit(**rate_body)) - - self.rate = rates - - return self + # TODO(mordred) We shouldn't have to subclass just to declare + # requires_id = False. + return super(Limits, self).get( + session=session, requires_id=False, error_message=error_message) diff --git a/openstack/object_store/v1/obj.py b/openstack/object_store/v1/obj.py index f32d68de6..daacddcaf 100644 --- a/openstack/object_store/v1/obj.py +++ b/openstack/object_store/v1/obj.py @@ -72,8 +72,6 @@ class Object(_base.BaseResource): #: TODO(briancurtin) there's a lot of content here... range = resource.Header("range", type=dict) #: See http://www.ietf.org/rfc/rfc2616.txt. - # TODO(mordred) We need a string-or-list formatter. type=list with a string - # value results in a list containing the characters. if_match = resource.Header("if-match", type=list) #: In combination with Expect: 100-Continue, specify an #: "If-None-Match: \*" header to query whether the server already diff --git a/openstack/resource2.py b/openstack/resource2.py index e8e654eaa..8d45efa4f 100644 --- a/openstack/resource2.py +++ b/openstack/resource2.py @@ -41,6 +41,37 @@ class that represent a remote resource. The attributes that from openstack import utils +def _convert_type(value, data_type, list_type=None): + # This should allow handling list of dicts that have their own + # Component type directly. See openstack/compute/v2/limits.py + # and the RateLimit type for an example. + if not data_type: + return value + if issubclass(data_type, list): + if isinstance(value, (list, tuple, set)): + if not list_type: + return value + ret = [] + for raw in value: + ret.append(_convert_type(raw, list_type)) + return ret + elif list_type: + return [_convert_type(value, list_type)] + # "if-match" in Object is a good example of the need here + return [value] + elif isinstance(value, data_type): + return value + if not isinstance(value, data_type): + if issubclass(data_type, format.Formatter): + return data_type.deserialize(value) + # This should allow handling sub-dicts that have their own + # Component type directly. See openstack/compute/v2/limits.py + # and the AbsoluteLimits type for an example. + if isinstance(value, dict): + return data_type(**value) + return data_type(value) + + class _BaseComponent(object): # The name this component is being tracked as in the Resource @@ -49,7 +80,7 @@ class _BaseComponent(object): _map_cls = dict def __init__(self, name, type=None, default=None, alias=None, - alternate_id=False, **kwargs): + alternate_id=False, list_type=None, **kwargs): """A typed descriptor for a component that makes up a Resource :param name: The name this component exists as on the server @@ -65,12 +96,15 @@ def __init__(self, name, type=None, default=None, alias=None, when `id` is not a name the Resource has. This is a relatively uncommon case, and this setting should only be used once per Resource. + :param list_type: If type is `list`, list_type designates what the + type of the elements of the list should be. """ self.name = name self.type = type self.default = default self.alias = alias self.alternate_id = alternate_id + self.list_type = list_type def __get__(self, instance, owner): if instance is None: @@ -89,21 +123,11 @@ def __get__(self, instance, owner): if value is None: return None - if self.type and not isinstance(value, self.type): - if issubclass(self.type, format.Formatter): - value = self.type.deserialize(value) - else: - value = self.type(value) - - return value + return _convert_type(value, self.type, self.list_type) def __set__(self, instance, value): - if (self.type and not isinstance(value, self.type) and - value != self.default): - if issubclass(self.type, format.Formatter): - value = self.type.serialize(value) - else: - value = str(self.type(value)) # validate to fail fast + if value != self.default: + value = _convert_type(value, self.type, self.list_type) attributes = getattr(instance, self.key) attributes[self.name] = value @@ -548,7 +572,16 @@ def to_dict(self, body=True, headers=True, ignore_none=False): value = getattr(self, key, None) if ignore_none and value is None: continue - mapping[key] = value + if isinstance(value, Resource): + mapping[key] = value.to_dict() + elif (value and isinstance(value, list) + and isinstance(value[0], Resource)): + converted = [] + for raw in value: + converted.append(raw.to_dict()) + mapping[key] = converted + else: + mapping[key] = value return mapping diff --git a/openstack/tests/unit/compute/v2/test_limits.py b/openstack/tests/unit/compute/v2/test_limits.py index dee011bf2..2b0a97dfe 100644 --- a/openstack/tests/unit/compute/v2/test_limits.py +++ b/openstack/tests/unit/compute/v2/test_limits.py @@ -150,6 +150,8 @@ def test_get(self): resp = mock.Mock() sess.get.return_value = resp resp.json.return_value = copy.deepcopy(LIMITS_BODY) + resp.headers = {} + resp.status_code = 200 sot = limits.Limits().get(sess) @@ -195,3 +197,12 @@ def test_get(self): self.assertEqual(RATE_LIMIT["uri"], sot.rate[0].uri) self.assertEqual(RATE_LIMIT["regex"], sot.rate[0].regex) self.assertEqual(RATE_LIMIT["limit"], sot.rate[0].limits) + + dsot = sot.to_dict() + + self.assertIsInstance(dsot['rate'][0], dict) + self.assertIsInstance(dsot['absolute'], dict) + self.assertEqual(RATE_LIMIT["uri"], dsot['rate'][0]['uri']) + self.assertEqual( + ABSOLUTE_LIMITS["totalSecurityGroupsUsed"], + dsot['absolute']['security_groups_used']) diff --git a/openstack/tests/unit/object_store/v1/test_obj.py b/openstack/tests/unit/object_store/v1/test_obj.py index 85eedc59d..640523eba 100644 --- a/openstack/tests/unit/object_store/v1/test_obj.py +++ b/openstack/tests/unit/object_store/v1/test_obj.py @@ -118,7 +118,9 @@ def test_download(self): ]) sot = obj.Object.new(container=self.container, name=self.object) sot.is_newest = True - sot.if_match = [self.headers['Etag']] + # if_match is a list type, but we're passing a string. This tests + # the up-conversion works properly. + sot.if_match = self.headers['Etag'] rv = sot.download(self.conn.object_store) diff --git a/openstack/tests/unit/test_resource2.py b/openstack/tests/unit/test_resource2.py index 18bf10de4..8ea53d35b 100644 --- a/openstack/tests/unit/test_resource2.py +++ b/openstack/tests/unit/test_resource2.py @@ -135,7 +135,7 @@ def test_get_name_formatter(self): class Parent(object): _example = {name: value} - class FakeFormatter(object): + class FakeFormatter(format.Formatter): @classmethod def deserialize(cls, value): return expected_result @@ -145,10 +145,7 @@ def deserialize(cls, value): # Mock out issubclass rather than having an actual format.Formatter # This can't be mocked via decorator, isolate it to wrapping the call. - mock_issubclass = mock.Mock(return_value=True) - module = six.moves.builtins.__name__ - with mock.patch("%s.issubclass" % module, mock_issubclass): - result = sot.__get__(instance, None) + result = sot.__get__(instance, None) self.assertEqual(expected_result, result) def test_set_name_untyped(self): @@ -207,6 +204,10 @@ class FakeFormatter(format.Formatter): def serialize(cls, arg): FakeFormatter.calls.append(arg) + @classmethod + def deserialize(cls, arg): + FakeFormatter.calls.append(arg) + sot = TestComponent.ExampleComponent("name", type=FakeFormatter) # Test that we run the value through type conversion. From aebf01966085ccdf55700b8f1aa084dcf5ed53a8 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 12 Jan 2018 17:43:25 -0600 Subject: [PATCH 1935/3836] Remove resource and proxy Step one in the final removal of resource/proxy v1. Doing it as a two commit chain just as a sanity check. All of the services have been migrated to Resource2/Proxy2. Before we rename those back to Resource/Proxy, delete the v1 versions and make sure everything is still happy. The next patch will do the renames. Change-Id: I254a7474236ea4959db1bd00b397320b0ca27387 --- .../create/examples/resource/fake.py | 13 +- doc/source/contributor/create/resource.rst | 14 +- doc/source/user/index.rst | 1 - doc/source/user/resource.rst | 39 - openstack/proxy.py | 286 ---- openstack/resource.py | 1040 ------------ openstack/service_description.py | 9 +- openstack/tests/unit/test_proxy.py | 358 ---- openstack/tests/unit/test_resource.py | 1512 ----------------- 9 files changed, 12 insertions(+), 3260 deletions(-) delete mode 100644 doc/source/user/resource.rst delete mode 100644 openstack/proxy.py delete mode 100644 openstack/resource.py delete mode 100644 openstack/tests/unit/test_proxy.py delete mode 100644 openstack/tests/unit/test_resource.py diff --git a/doc/source/contributor/create/examples/resource/fake.py b/doc/source/contributor/create/examples/resource/fake.py index e87416f09..527e5ad11 100644 --- a/doc/source/contributor/create/examples/resource/fake.py +++ b/doc/source/contributor/create/examples/resource/fake.py @@ -1,7 +1,7 @@ # Apache 2 header omitted for brevity from openstack.fake import fake_service -from openstack import resource +from openstack import resource2 as resource class Fake(resource.Resource): @@ -9,21 +9,20 @@ class Fake(resource.Resource): resources_key = "resources" base_path = "/fake" service = fake_service.FakeService() - id_attribute = "name" allow_create = True - allow_retrieve = True + allow_get = True allow_update = True allow_delete = True allow_list = True allow_head = True #: The transaction date and time. - timestamp = resource.prop("x-timestamp") + timestamp = resource.Header("x-timestamp") #: The name of this resource. - name = resource.prop("name") + name = resource.Body("name", alternate_id=True) #: The value of the resource. Also available in headers. - value = resource.prop("value", alias="x-resource-value") + value = resource.Body("value", alias="x-resource-value") #: Is this resource cool? If so, set it to True. #: This is a multi-line comment about cool stuff. - cool = resource.prop("cool", type=bool) + cool = resource.Body("cool", type=bool) diff --git a/doc/source/contributor/create/resource.rst b/doc/source/contributor/create/resource.rst index 42a7607d3..d531a4717 100644 --- a/doc/source/contributor/create/resource.rst +++ b/doc/source/contributor/create/resource.rst @@ -1,3 +1,5 @@ +.. TODO(shade) Update this guide. + Creating a New Resource ======================= @@ -108,14 +110,6 @@ service is called in the service catalog. When a request is made for this resource, the Session now knows how to construct the appropriate URL using this ``FakeService`` instance. -``id_attribute`` -**************** - -*Line 12* specifies that this resource uses a different identifier than -the default of ``id``. While IDs are used internally, such as for creating -request URLs to interact with an individual resource, they are exposed for -consistency so users always have one place to find the resource's identity. - Supported Operations -------------------- @@ -136,7 +130,7 @@ value by setting it to ``True``: +----------------------------------------------+----------------+ | :class:`~openstack.resource.Resource.list` | allow_list | +----------------------------------------------+----------------+ -| :class:`~openstack.resource.Resource.get` | allow_retrieve | +| :class:`~openstack.resource.Resource.get` | allow_get | +----------------------------------------------+----------------+ | :class:`~openstack.resource.Resource.update` | allow_update | +----------------------------------------------+----------------+ @@ -148,6 +142,8 @@ used for ``Resource.update``. Properties ---------- +.. TODO(shade) Especially this section + The way resource classes communicate values between the user and the server are :class:`~openstack.resource.prop` objects. These act similarly to Python's built-in property objects, but they share only the name - they're not the same. diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index fac178079..fb5e2e082 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -137,7 +137,6 @@ can be customized. .. toctree:: :maxdepth: 1 - resource resource2 service_filter utils diff --git a/doc/source/user/resource.rst b/doc/source/user/resource.rst deleted file mode 100644 index f40188e18..000000000 --- a/doc/source/user/resource.rst +++ /dev/null @@ -1,39 +0,0 @@ -**NOTE: This module is being phased out in favor of** -:mod:`openstack.resource2`. **Once all services have been moved over to use -resource2, that module will take this `resource` name.** - -Resource -======== -.. automodule:: openstack.resource - -The prop class --------------- - -.. autoclass:: openstack.resource.prop - :members: - -The Resource class ------------------- - -.. autoclass:: openstack.resource.Resource - :members: - :member-order: bysource - -How path_args are used -********************** - -As :class:`Resource`\s often contain compound :data:`Resource.base_path`\s, -meaning the path is constructed from more than just that string, the -various request methods need a way to fill in the missing parts. -That's where ``path_args`` come in. - -For example:: - - class ServerIP(resource.Resource): - base_path = "/servers/%(server_id)s/ips" - -Making a GET request to obtain server IPs requires the ID of the server -to check. This is handled by passing ``{"server_id": "12345"}`` as the -``path_args`` argument when calling :meth:`Resource.get_by_id`. From there, -the method uses Python's string interpolation to fill in the ``server_id`` -piece of the URL, and then makes the request. diff --git a/openstack/proxy.py b/openstack/proxy.py deleted file mode 100644 index 739301b57..000000000 --- a/openstack/proxy.py +++ /dev/null @@ -1,286 +0,0 @@ -# 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 _adapter -from openstack import exceptions -from openstack import resource - - -# The _check_resource decorator is used on BaseProxy methods to ensure that -# the `actual` argument is in fact the type of the `expected` argument. -# It does so under two cases: -# 1. When strict=False, if and only if `actual` is a Resource instance, -# it is checked to see that it's an instance of the `expected` class. -# This allows `actual` to be other types, such as strings, when it makes -# sense to accept a raw id value. -# 2. When strict=True, `actual` must be an instance of the `expected` class. -def _check_resource(strict=False): - def wrap(method): - def check(self, expected, actual=None, *args, **kwargs): - if (strict and actual is not None and not - isinstance(actual, resource.Resource)): - raise ValueError("A %s must be passed" % expected.__name__) - elif (isinstance(actual, resource.Resource) and not - isinstance(actual, expected)): - raise ValueError("Expected %s but received %s" % ( - expected.__name__, actual.__class__.__name__)) - - return method(self, expected, actual, *args, **kwargs) - return check - return wrap - - -class BaseProxy(_adapter.OpenStackSDKAdapter): - - def _get_resource(self, resource_type, value, path_args=None): - """Get a resource object to work on - - :param resource_type: The type of resource to operate on. This should - be a subclass of - :class:`~openstack.resource.Resource` with a - ``from_id`` method. - :param value: The ID of a resource or an object of ``resource_type`` - class if using an existing instance, or None to create a - new instance. - :param path_args: A dict containing arguments for forming the request - URL, if needed. - """ - if value is None: - # Create a bare resource - res = resource_type() - elif not isinstance(value, resource_type): - # Create from an ID - args = {resource_type.id_attribute: - resource.Resource.get_id(value)} - res = resource_type.existing(**args) - else: - # An existing resource instance - res = value - - # Set any intermediate path arguments, but don't overwrite Nones. - if path_args is not None: - res.update_attrs(ignore_none=True, **path_args) - - return res - - def _find(self, resource_type, name_or_id, path_args=None, - ignore_missing=True): - """Find a resource - - :param name_or_id: The name or ID of a resource to find. - :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. - - :returns: An instance of ``resource_type`` or None - """ - return resource_type.find(self, name_or_id, - path_args=path_args, - ignore_missing=ignore_missing) - - @_check_resource(strict=False) - def _delete(self, resource_type, value, path_args=None, - ignore_missing=True): - """Delete a resource - - :param resource_type: The type of resource to delete. This should - be a :class:`~openstack.resource.Resource` - subclass with a ``from_id`` method. - :param value: The value to delete. Can be either the ID of a - resource or a :class:`~openstack.resource.Resource` - subclass. - :param path_args: A dict containing arguments for forming the request - URL, if needed. - :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent resource. - - :returns: The result of the ``delete`` - :raises: ``ValueError`` if ``value`` is a - :class:`~openstack.resource.Resource` that doesn't match - the ``resource_type``. - :class:`~openstack.exceptions.ResourceNotFound` when - ignore_missing if ``False`` and a nonexistent resource - is attempted to be deleted. - - """ - res = self._get_resource(resource_type, value, path_args) - - try: - rv = res.delete( - self, - error_message="No {resource_type} found for {value}".format( - resource_type=resource_type.__name__, value=value)) - except exceptions.NotFoundException: - if ignore_missing: - return None - else: - raise - - return rv - - @_check_resource(strict=False) - def _update(self, resource_type, value, path_args=None, **attrs): - """Update a resource - - :param resource_type: The type of resource to update. - :type resource_type: :class:`~openstack.resource.Resource` - :param value: The resource to update. This must either be a - :class:`~openstack.resource.Resource` or an id - that corresponds to a resource. - :param path_args: A dict containing arguments for forming the request - URL, if needed. - :param **attrs: Attributes to update on a Resource object. - These attributes will be used in conjunction with - ``resource_type``. - - :returns: The result of the ``update`` - :rtype: :class:`~openstack.resource.Resource` - """ - res = self._get_resource(resource_type, value, path_args) - res.update_attrs(attrs) - return res.update(self) - - def _create(self, resource_type, path_args=None, **attrs): - """Create a resource from attributes - - :param resource_type: The type of resource to create. - :type resource_type: :class:`~openstack.resource.Resource` - :param path_args: A dict containing arguments for forming the request - URL, if needed. - :param **attrs: Attributes from which to create a Resource object. - These attributes will be used in conjunction with - ``resource_type``. - - :returns: The result of the ``create`` - :rtype: :class:`~openstack.resource.Resource` - """ - res = resource_type.new(**attrs) - if path_args is not None: - res.update_attrs(path_args) - return res.create(self) - - @_check_resource(strict=False) - def _get(self, resource_type, value=None, path_args=None, args=None): - """Get a resource - - :param resource_type: The type of resource to get. - :type resource_type: :class:`~openstack.resource.Resource` - :param value: The value to get. Can be either the ID of a - resource or a :class:`~openstack.resource.Resource` - subclass. - :param path_args: A dict containing arguments for forming the request - URL, if needed. - :param args: A optional dict containing arguments that will be - translated into query strings when forming the request URL. - - :returns: The result of the ``get`` - :rtype: :class:`~openstack.resource.Resource` - """ - res = self._get_resource(resource_type, value, path_args) - - return res.get( - self, args=args, - error_message='No {resource} found for {value}'.format( - resource=resource_type.__name__, value=value)) - - def _list(self, resource_type, value=None, paginated=False, - path_args=None, **query): - """List a resource - - :param resource_type: The type of resource to delete. This should - be a :class:`~openstack.resource.Resource` - subclass with a ``from_id`` method. - :param value: The resource to list. It can be the ID of a resource, or - a :class:`~openstack.resource.Resource` object. When set - to None, a new bare resource is created. - :param bool paginated: When set to ``False``, expect all of the data - to be returned in one response. When set to - ``True``, the resource supports data being - returned across multiple pages. - :param path_args: A dictionary containing arguments for use when - forming the request URL for resource retrieval. - :param kwargs **query: Keyword arguments that are sent to the list - method, which are then attached as query - parameters on the request URL. - - :returns: A generator of Resource objects. - :raises: ``ValueError`` if ``value`` is a - :class:`~openstack.resource.Resource` that doesn't match - the ``resource_type``. - """ - res = self._get_resource(resource_type, value, path_args) - - query = res.convert_ids(query) - return res.list(self, path_args=path_args, - paginated=paginated, params=query) - - def _head(self, resource_type, value=None, path_args=None): - """Retrieve a resource's header - - :param resource_type: The type of resource to retrieve. - :type resource_type: :class:`~openstack.resource.Resource` - :param value: The value of a specific resource to retreive headers - for. Can be either the ID of a resource, - a :class:`~openstack.resource.Resource` subclass, - or ``None``. - :param path_args: A dict containing arguments for forming the request - URL, if needed. - - :returns: The result of the ``head`` call - :rtype: :class:`~openstack.resource.Resource` - """ - res = self._get_resource(resource_type, value, path_args) - - return res.head(self) - - def wait_for_status(self, value, status, failures=None, interval=2, - wait=120): - """Wait for a resource to be in a particular status. - - :param value: The resource to wait on to reach the status. The - resource must have a status attribute. - :type value: :class:`~openstack.resource.Resource` - :param status: Desired status of the resource. - :param list failures: Statuses that would indicate the transition - failed such as 'ERROR'. - :param interval: Number of seconds to wait between checks. - :param wait: Maximum number of seconds to wait for the change. - - :return: Method returns resource on success. - :raises: :class:`~openstack.exceptions.ResourceTimeout` transition - to status failed to occur in wait seconds. - :raises: :class:`~openstack.exceptions.ResourceFailure` resource - transitioned to one of the failure states. - :raises: :class:`~AttributeError` if the resource does not have a - status attribute - """ - failures = [] if failures is None else failures - return resource.wait_for_status(self, value, status, - failures, interval, wait) - - def wait_for_delete(self, value, interval=2, wait=120): - """Wait for the resource to be deleted. - - :param value: The resource to wait on to be deleted. - :type value: :class:`~openstack.resource.Resource` - :param interval: Number of seconds to wait between checks. - :param wait: Maximum number of seconds to wait for the delete. - - :return: Method returns resource on success. - :raises: :class:`~openstack.exceptions.ResourceTimeout` transition - to delete failed to occur in wait seconds. - """ - return resource.wait_for_delete(self, value, interval, wait) diff --git a/openstack/resource.py b/openstack/resource.py deleted file mode 100644 index c9c0671f7..000000000 --- a/openstack/resource.py +++ /dev/null @@ -1,1040 +0,0 @@ -# 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. - -""" -The :class:`~openstack.resource.Resource` class is a base -class that represent a remote resource. Attributes of the resource -are defined by the responses from the server rather than in code so -that we don't have to try and keep up with all possible attributes -and extensions. This may be changed in the future. - -The :class:`~openstack.resource.prop` class is a helper for -definiting properties in a resource. - -For update management, :class:`~openstack.resource.Resource` -maintains a dirty list so when updating an object only the attributes -that have actually been changed are sent to the server. - -There is also some support here for lazy loading that needs improvement. - -There are plenty of examples of use of this class in the SDK code. -""" - -import abc -import collections -import copy -import itertools -import time - -import six -from six.moves.urllib import parse as url_parse - -from openstack import exceptions -from openstack import format -from openstack import utils - - -class prop(object): - """A helper for defining properties in a resource. - - A prop defines some known attributes within a resource's values. - For example we know a User resource will have a name: - - >>> class User(Resource): - ... name = prop('name') - ... - >>> u = User() - >>> u.name = 'John Doe' - >>> print u['name'] - John Doe - - User objects can now be accessed via the User().name attribute. The 'name' - value we pass as an attribute is the name of the attribute in the message. - This means that you don't need to use the same name for your attribute as - will be set within the object. For example: - - >>> class User(Resource): - ... name = prop('userName') - ... - >>> u = User() - >>> u.name = 'John Doe' - >>> print u['userName'] - John Doe - - There is limited validation ability in props. - - You can validate the type of values that are set: - - >>> class User(Resource): - ... name = prop('userName') - ... age = prop('age', type=int) - ... - >>> u = User() - >>> u.age = 'thirty' - TypeError: Invalid type for attr age - - - By specifying an alias attribute name, that alias will be read when the - primary attribute name does not appear within the resource: - - >>> class User(Resource): - ... name = prop('address', alias='location') - ... - >>> u = User(location='Far Away') - >>> print u['address'] - Far Away - """ - - def __init__(self, name, alias=None, type=None, default=None): - self.name = name - self.type = type - self.alias = alias - self.default = default - - def __get__(self, instance, owner): - if instance is None: - return None - try: - value = instance[self.name] - # self.type() should not be called on None objects. - if value is None: - return None - except KeyError: - try: - value = instance[self.alias] - except (KeyError, AttributeError): - # If we either don't find the key or we don't have an alias - return self.default - - if self.type and not isinstance(value, self.type): - if issubclass(self.type, Resource): - if isinstance(value, six.string_types): - value = self.type({self.type.id_attribute: value}) - else: - value = self.type(value) - elif issubclass(self.type, format.Formatter): - value = self.type.deserialize(value) - else: - value = self.type(value) - - return value - - def __set__(self, instance, value): - if (self.type and not isinstance(value, self.type) and - value != self.default): - if issubclass(self.type, Resource): - if isinstance(value, six.string_types): - value = self.type({self.type.id_attribute: value}) - else: - value = self.type(value) - elif issubclass(self.type, format.Formatter): - value = self.type.serialize(value) - else: - value = str(self.type(value)) # validate to fail fast - - # If we already have a value set for the alias name, pop it out - # and store the real name instead. This happens when the alias - # has the same name as this prop is named. - if self.alias in instance._attrs: - instance._attrs.pop(self.alias) - - instance[self.name] = value - - def __delete__(self, instance): - try: - del instance[self.name] - except KeyError: - try: - del instance[self.alias] - except KeyError: - pass - - -#: Key in attributes for header properties -HEADERS = 'headers' - - -class header(prop): - """A helper for defining header properties in a resource. - - This property should be used for values passed in the header of a resource. - Header values are stored in a special 'headers' attribute of a resource. - Using this property will make it easier for users to access those values. - For example, and object store container: - - >>> class Container(Resource): - ... name = prop("name") - ... object_count = header("x-container-object-count") - ... - >>> c = Container({name='pix'}) - >>> c.head(session) - >>> print c["headers"]["x-container-object-count"] - 4 - >>> print c.object_count - 4 - - The first print shows accessing the header value without the property - and the second print shows accessing the header with the property helper. - """ - - def _get_headers(self, instance): - if instance is None: - return None - if HEADERS in instance: - return instance[HEADERS] - return None - - def __get__(self, instance, owner): - headers = self._get_headers(instance) - return super(header, self).__get__(headers, owner) - - def __set__(self, instance, value): - headers = self._get_headers(instance) - if headers is None: - headers = instance._attrs[HEADERS] = {} - headers[self.name] = value - instance.set_headers(headers) - - -@six.add_metaclass(abc.ABCMeta) -class Resource(collections.MutableMapping): - - #: Singular form of key for resource. - resource_key = None - #: Common name for resource. - resource_name = None - #: Plural form of key for resource. - resources_key = None - - #: Attribute key associated with the id for this resource. - id_attribute = 'id' - #: Attribute key associated with the name for this resource. - name_attribute = 'name' - #: Attribute key associated with 'location' from response headers - location = header('location') - - #: The base part of the url for this resource. - base_path = '' - - #: The service associated with this resource to find the service URL. - service = None - - #: Allow create operation for this resource. - allow_create = False - #: Allow retrieve/get operation for this resource. - allow_retrieve = False - #: Allow update operation for this resource. - allow_update = False - #: Allow delete operation for this resource. - allow_delete = False - #: Allow list operation for this resource. - allow_list = False - #: Allow head operation for this resource. - allow_head = False - - patch_update = False - - def __init__(self, attrs=None, loaded=False): - """Construct a Resource to interact with a service's REST API. - - The Resource class offers two class methods to construct - resource objects, which are preferrable to entering through - this initializer. See :meth:`Resource.new` and - :meth:`Resource.existing`. - - :param dict attrs: The attributes to set when constructing - this Resource. - :param bool loaded: ``True`` if this Resource exists on - the server, ``False`` if it does not. - """ - self._attrs = {} if attrs is None else attrs.copy() - self._dirty = set() if loaded else set(self._attrs.keys()) - self.update_attrs(self._attrs) - self._loaded = loaded - - def __repr__(self): - return "%s.%s(attrs=%s, loaded=%s)" % (self.__module__, - self.__class__.__name__, - self._attrs, self._loaded) - - @classmethod - def get_resource_name(cls): - if cls.resource_name: - return cls.resource_name - if cls.resource_key: - return cls.resource_key - return cls().__class__.__name__ - - ## - # CONSTRUCTORS - ## - - @classmethod - def new(cls, **kwargs): - """Create a new instance of this resource. - - Internally set flags such that it is marked as not present on the - server. - - :param dict kwargs: Each of the named arguments will be set as - attributes on the resulting Resource object. - """ - return cls(kwargs, loaded=False) - - @classmethod - def existing(cls, **kwargs): - """Create an instance of an existing remote resource. - - It is marked as an exact replication of a resource present on a server. - - :param dict kwargs: Each of the named arguments will be set as - attributes on the resulting Resource object. - """ - return cls(kwargs, loaded=True) - - @classmethod - def _from_attr(cls, attribute, value): - # This method is useful in the higher level, in cases where operations - # need to depend on having Resource objects, but the API is flexible - # in taking text values which represent those objects. - if isinstance(value, cls): - return value - elif isinstance(value, six.string_types): - return cls.new(**{attribute: value}) - else: - raise ValueError("value must be %s instance or %s" % ( - cls.__name__, attribute)) - - @classmethod - def from_id(cls, value): - """Create an instance from an ID or return an existing instance. - - New instances are created with :meth:`~openstack.resource.Resource.new` - - :param value: If ``value`` is an instance of this Resource type, - it is returned. - If ``value`` is an ID which an instance of this - Resource type can be created with, one is created - and returned. - - :rtype: :class:`~openstack.resource.Resource` or the - appropriate subclass. - :raises: :exc:`ValueError` if ``value`` is not an instance of - this Resource type or a valid ``id``. - """ - return cls._from_attr(cls.id_attribute, value) - - @classmethod - def from_name(cls, value): - """Create an instance from a name or return an existing instance. - - New instances are created with :meth:`~openstack.resource.Resource.new` - - :param value: If ``value`` is an instance of this Resource type, - it is returned. - If ``value`` is a name which an instance of this - Resource type can be created with, one is created - and returned. - - :rtype: :class:`~openstack.resource.Resource` or the - appropriate subclass. - :raises: :exc:`ValueError` if ``value`` is not an instance of - this Resource type or a valid ``name``. - """ - return cls._from_attr(cls.name_attribute, value) - - ## - # MUTABLE MAPPING IMPLEMENTATION - ## - - def __getitem__(self, name): - return self._attrs[name] - - def __setitem__(self, name, value): - try: - orig = self._attrs[name] - except KeyError: - changed = True - else: - changed = orig != value - - if changed: - self._attrs[name] = value - self._dirty.add(name) - - def __delitem__(self, name): - del self._attrs[name] - self._dirty.add(name) - - def __len__(self): - return len(self._attrs) - - def __iter__(self): - return iter(self._attrs) - - ## - # BASE PROPERTIES/OPERATIONS - ## - - @property - def id(self): - """The identifier associated with this resource. - - The true value of the ``id`` property comes from the - attribute set as :data:`id_attribute`. For example, - a container's name may be the appropirate identifier, - so ``id_attribute = "name"`` would be set on the - :class:`Resource`, and ``Resource.name`` would be - conveniently accessible through ``id``. - """ - return self._attrs.get(self.id_attribute, None) - - @id.deleter - def id(self): - del self._attrs[self.id_attribute] - - @property - def name(self): - """The name associated with this resource. - - The true value of the ``name`` property comes from the - attribute set as :data:`name_attribute`. - """ - return self._attrs.get(self.name_attribute, None) - - @name.setter - def name(self, value): - self._attrs[self.name_attribute] = value - - @name.deleter - def name(self): - del self._attrs[self.name_attribute] - - @property - def is_dirty(self): - """True if the resource needs to be updated to the remote.""" - return len(self._dirty) > 0 - - def _reset_dirty(self): - self._dirty = set() - - def _update_attrs_from_response(self, resp, include_headers=False, - error_message=None): - resp_headers = resp.pop(HEADERS, None) - self._attrs.update(resp) - self.update_attrs(self._attrs) - if include_headers and (resp_headers is not None): - self.set_headers(resp_headers) - - def update_attrs(self, *args, **kwargs): - """Update the attributes on this resource - - Note that this is implemented because Resource.update overrides - the update method we would get from the MutableMapping base class. - - :params args: A dictionary of attributes to be updated. - :params kwargs: Named arguments to be set on this instance. - When a key corresponds to a resource.prop, - it will be set via resource.prop.__set__. - - :rtype: None - """ - ignore_none = kwargs.pop("ignore_none", False) - - # ensure setters are called for type coercion - for key, value in itertools.chain(dict(*args).items(), kwargs.items()): - if key != self.id_attribute: # id property is read only - - # Don't allow None values to override a key unless we've - # explicitly specified they can. Proxy methods have default - # None arguments that we don't want to override any values - # that may have been passed in on Resource instances. - if not all([ignore_none, value is None]): - if key != "id": - setattr(self, key, value) - self[key] = value - - def get_headers(self): - if HEADERS in self._attrs: - return self._attrs[HEADERS] - return {} - - def set_headers(self, values): - self._attrs[HEADERS] = values - self._dirty.add(HEADERS) - - def to_dict(self): - attrs = copy.deepcopy(self._attrs) - headers = attrs.pop(HEADERS, {}) - attrs.update(headers) - return attrs - - ## - # CRUD OPERATIONS - ## - - @staticmethod - def get_id(value): - """If a value is a Resource, return the canonical ID.""" - if isinstance(value, Resource): - return value.id - else: - return value - - @staticmethod - def convert_ids(attrs): - """Return an attribute dictionary suitable for create/update - - As some attributes may be Resource types, their ``id`` attribute - needs to be put in the Resource instance's place in order - to be properly serialized and understood by the server. - """ - if attrs is None: - return - - converted = attrs.copy() - for key, value in converted.items(): - if isinstance(value, Resource): - converted[key] = value.id - - return converted - - @classmethod - def _get_create_body(cls, attrs): - if cls.resource_key: - return {cls.resource_key: attrs} - else: - return attrs - - @classmethod - def _get_url(cls, path_args=None, resource_id=None): - if path_args: - url = cls.base_path % path_args - else: - url = cls.base_path - if resource_id is not None: - url = utils.urljoin(url, resource_id) - return url - - @classmethod - def create_by_id(cls, session, attrs, resource_id=None, path_args=None): - """Create a remote resource from its attributes. - - :param session: The session to use for making this request. - :type session: :class:`~keystoneauth1.adapter.Adapter` - :param dict attrs: The attributes to be sent in the body - of the request. - :param resource_id: This resource's identifier, if needed by - the request. The default is ``None``. - :param dict path_args: A dictionary of arguments to construct - a compound URL. - See `How path_args are used`_ for details. - - :return: A ``dict`` representing the response body. - :raises: :exc:`~openstack.exceptions.MethodNotSupported` if - :data:`Resource.allow_create` is not set to ``True``. - """ - if not cls.allow_create: - raise exceptions.MethodNotSupported(cls, 'create') - - # Convert attributes from Resource types into their ids. - attrs = cls.convert_ids(attrs) - headers = attrs.pop(HEADERS, None) - - body = cls._get_create_body(attrs) - - url = cls._get_url(path_args, resource_id) - args = {'json': body} - if headers: - args[HEADERS] = headers - if resource_id: - resp = session.put(url, **args) - else: - resp = session.post(url, **args) - resp_headers = resp.headers - resp = resp.json() - - if cls.resource_key: - resp = resp[cls.resource_key] - if resp_headers: - resp[HEADERS] = copy.deepcopy(resp_headers) - - return resp - - def create(self, session): - """Create a remote resource from this instance. - - :param session: The session to use for making this request. - :type session: :class:`~keystoneauth1.adapter.Adapter` - - :return: This :class:`Resource` instance. - :raises: :exc:`~openstack.exceptions.MethodNotSupported` if - :data:`Resource.allow_create` is not set to ``True``. - """ - resp = self.create_by_id(session, self._attrs, self.id, path_args=self) - self._update_attrs_from_response(resp, include_headers=True) - self._reset_dirty() - return self - - @classmethod - def get_data_by_id(cls, session, resource_id, path_args=None, args=None, - include_headers=False): - """Get the attributes of a remote resource from an id. - - :param session: The session to use for making this request. - :type session: :class:`~keystoneauth1.adapter.Adapter` - :param resource_id: This resource's identifier, if needed by - the request. - :param dict path_args: A dictionary of arguments to construct - a compound URL. - See `How path_args are used`_ for details. - :param dict args: A dictionary of query parameters to be appended to - the compound URL. - :param bool include_headers: ``True`` if header data should be - included in the response body, - ``False`` if not. - - :return: A ``dict`` representing the response body. - :raises: :exc:`~openstack.exceptions.MethodNotSupported` if - :data:`Resource.allow_retrieve` is not set to ``True``. - """ - if not cls.allow_retrieve: - raise exceptions.MethodNotSupported(cls, 'retrieve') - - url = cls._get_url(path_args, resource_id) - if args: - url = '?'.join([url, url_parse.urlencode(args)]) - response = session.get(url,) - exceptions.raise_from_response(response) - body = response.json() - - if cls.resource_key: - body = body[cls.resource_key] - - if include_headers: - body[HEADERS] = response.headers - - return body - - @classmethod - def get_by_id(cls, session, resource_id, path_args=None, - include_headers=False): - """Get an object representing a remote resource from an id. - - :param session: The session to use for making this request. - :type session: :class:`~keystoneauth1.adapter.Adapter` - :param resource_id: This resource's identifier, if needed by - the request. - :param dict path_args: A dictionary of arguments to construct - a compound URL. - See `How path_args are used`_ for details. - :param bool include_headers: ``True`` if header data should be - included in the response body, - ``False`` if not. - - :return: A :class:`Resource` object representing the - response body. - :raises: :exc:`~openstack.exceptions.MethodNotSupported` if - :data:`Resource.allow_retrieve` is not set to ``True``. - """ - body = cls.get_data_by_id(session, resource_id, path_args=path_args, - include_headers=include_headers) - return cls.existing(**body) - - def get(self, session, include_headers=False, - args=None, error_message=None): - """Get the remote resource associated with this instance. - - :param session: The session to use for making this request. - :type session: :class:`~keystoneauth1.adapter.Adapter` - :param bool include_headers: ``True`` if header data should be - included in the response body, - ``False`` if not. - :param dict args: A dictionary of query parameters to be appended to - the compound URL. - :return: This :class:`Resource` instance. - :raises: :exc:`~openstack.exceptions.MethodNotSupported` if - :data:`Resource.allow_retrieve` is not set to ``True``. - """ - body = self.get_data_by_id(session, self.id, path_args=self, args=args, - include_headers=include_headers) - self._update_attrs_from_response(body, include_headers) - self._loaded = True - return self - - @classmethod - def head_data_by_id(cls, session, resource_id, path_args=None): - """Get a dictionary representing the headers of a remote resource. - - :param session: The session to use for making this request. - :type session: :class:`~keystoneauth1.adapter.Adapter` - :param resource_id: This resource's identifier, if needed by - the request. - :param dict path_args: A dictionary of arguments to construct - a compound URL. - See `How path_args are used`_ for details. - - :return: A ``dict`` containing the headers. - :raises: :exc:`~openstack.exceptions.MethodNotSupported` if - :data:`Resource.allow_head` is not set to ``True``. - """ - if not cls.allow_head: - raise exceptions.MethodNotSupported(cls, 'head') - - url = cls._get_url(path_args, resource_id) - - headers = {'Accept': ''} - resp = session.head(url, headers=headers) - - return {HEADERS: resp.headers} - - @classmethod - def head_by_id(cls, session, resource_id, path_args=None): - """Get an object representing the headers of a remote resource. - - :param session: The session to use for making this request. - :type session: :class:`~keystoneauth1.adapter.Adapter` - :param resource_id: This resource's identifier, if needed by - the request. - :param dict path_args: A dictionary of arguments to construct - a compound URL. - See `How path_args are used`_ for details. - - :return: A :class:`Resource` representing the headers. - :raises: :exc:`~openstack.exceptions.MethodNotSupported` if - :data:`Resource.allow_head` is not set to ``True``. - """ - data = cls.head_data_by_id(session, resource_id, path_args=path_args) - return cls.existing(**data) - - def head(self, session): - """Get the remote resource headers associated with this instance. - - :param session: The session to use for making this request. - :type session: :class:`~keystoneauth1.adapter.Adapter` - - :return: This :class:`Resource` instance. - :raises: :exc:`~openstack.exceptions.MethodNotSupported` if - :data:`Resource.allow_head` is not set to ``True``. - """ - data = self.head_data_by_id(session, self.id, path_args=self) - self._attrs.update(data) - self._loaded = True - return self - - @classmethod - def update_by_id(cls, session, resource_id, attrs, path_args=None): - """Update a remote resource with the given attributes. - - :param session: The session to use for making this request. - :type session: :class:`~keystoneauth1.adapter.Adapter` - :param resource_id: This resource's identifier, if needed by - the request. - :param dict attrs: The attributes to be sent in the body - of the request. - :param dict path_args: A dictionary of arguments to construct - a compound URL. - See `How path_args are used`_ for details. - - :return: A ``dict`` representing the response body. - :raises: :exc:`~openstack.exceptions.MethodNotSupported` if - :data:`Resource.allow_update` is not set to ``True``. - """ - if not cls.allow_update: - raise exceptions.MethodNotSupported(cls, 'update') - - # Convert attributes from Resource types into their ids. - attrs = cls.convert_ids(attrs) - if attrs and cls.id_attribute in attrs: - del attrs[cls.id_attribute] - headers = attrs.pop(HEADERS, None) - - body = cls._get_create_body(attrs) - - url = cls._get_url(path_args, resource_id) - args = {'json': body} - if headers: - args[HEADERS] = headers - if cls.patch_update: - resp = session.patch(url, **args) - else: - resp = session.put(url, **args) - resp_headers = resp.headers - resp = resp.json() - - if cls.resource_key and cls.resource_key in resp.keys(): - resp = resp[cls.resource_key] - if resp_headers: - resp[HEADERS] = resp_headers - - return resp - - def update(self, session): - """Update the remote resource associated with this instance. - - :param session: The session to use for making this request. - :type session: :class:`~keystoneauth1.adapter.Adapter` - - :return: This :class:`Resource` instance. - :raises: :exc:`~openstack.exceptions.MethodNotSupported` if - :data:`Resource.allow_update` is not set to ``True``. - """ - if not self.is_dirty: - return - - dirty_attrs = dict((k, self._attrs[k]) for k in self._dirty) - resp = self.update_by_id(session, self.id, dirty_attrs, path_args=self) - - try: - resp_id = resp.pop(self.id_attribute) - except KeyError: - pass - else: - assert resp_id == self.id - self._update_attrs_from_response(resp, include_headers=True) - self._reset_dirty() - return self - - @classmethod - def delete_by_id(cls, session, resource_id, path_args=None, - error_message=None): - """Delete a remote resource with the given id. - - :param session: The session to use for making this request. - :type session: :class:`~keystoneauth1.adapter.Adapter` - :param resource_id: This resource's identifier, if needed by - the request. - :param dict path_args: A dictionary of arguments to construct - a compound URL. - See `How path_args are used`_ for details. - - :return: ``None`` - :raises: :exc:`~openstack.exceptions.MethodNotSupported` if - :data:`Resource.allow_delete` is not set to ``True``. - """ - if not cls.allow_delete: - raise exceptions.MethodNotSupported(cls, 'delete') - - url = cls._get_url(path_args, resource_id) - headers = {'Accept': ''} - response = session.delete(url, headers=headers) - exceptions.raise_from_response(response, error_message=error_message) - - def delete(self, session, error_message=None): - """Delete the remote resource associated with this instance. - - :param session: The session to use for making this request. - :type session: :class:`~keystoneauth1.adapter.Adapter` - - :return: ``None`` - :raises: :exc:`~openstack.exceptions.MethodNotSupported` if - :data:`Resource.allow_update` is not set to ``True``. - """ - self.delete_by_id(session, self.id, path_args=self, - error_message=error_message) - - @classmethod - def list(cls, session, path_args=None, paginated=False, params=None): - """This method is a generator which yields resource objects. - - This resource object list generator handles pagination and takes query - params for response filtering. - - :param session: The session to use for making this request. - :type session: :class:`~keystoneauth1.adapter.Adapter` - :param dict path_args: A dictionary of arguments to construct - a compound URL. - See `How path_args are used`_ for details. - :param bool paginated: ``True`` if a GET to this resource returns - a paginated series of responses, or ``False`` - if a GET returns only one page of data. - **When paginated is False only one - page of data will be returned regardless - of the API's support of pagination.** - :param dict params: Query parameters to be passed into the underlying - :meth:`~keystoneauth1.adapter.Adapter.get` method. - Values that the server may support include `limit` - and `marker`. - - :return: A generator of :class:`Resource` objects. - :raises: :exc:`~openstack.exceptions.MethodNotSupported` if - :data:`Resource.allow_list` is not set to ``True``. - """ - if not cls.allow_list: - raise exceptions.MethodNotSupported(cls, 'list') - - more_data = True - params = {} if params is None else params - url = cls._get_url(path_args) - headers = {'Accept': 'application/json'} - while more_data: - resp = session.get(url, - headers=headers, params=params) - resp = resp.json() - if cls.resources_key: - resp = resp[cls.resources_key] - - if not resp: - more_data = False - - # Keep track of how many items we've yielded. If we yielded - # less than our limit, we don't need to do an extra request - # to get back an empty data set, which acts as a sentinel. - yielded = 0 - new_marker = None - for data in resp: - value = cls.existing(**data) - new_marker = value.id - yielded += 1 - yield value - - if not paginated: - return - if 'limit' in params and yielded < params['limit']: - return - params['limit'] = yielded - params['marker'] = new_marker - - @classmethod - def find(cls, session, name_or_id, path_args=None, ignore_missing=True): - """Find a resource by its name or id. - - :param session: The session to use for making this request. - :type session: :class:`~keystoneauth1.adapter.Adapter` - :param name_or_id: This resource's identifier, if needed by - the request. The default is ``None``. - :param dict path_args: A dictionary of arguments to construct - a compound URL. - See `How path_args are used`_ for details. - :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. - - :return: The :class:`Resource` object matching the given name or id - or None if nothing matches. - :raises: :class:`openstack.exceptions.DuplicateResource` if more - than one resource is found for this request. - :raises: :class:`openstack.exceptions.ResourceNotFound` if nothing - is found and ignore_missing is ``False``. - """ - # Only return one matching resource. - def get_one_match(results, the_id, the_name): - the_result = None - for item in results: - maybe_result = cls.existing(**item) - - id_value, name_value = None, None - if the_id is not None: - id_value = getattr(maybe_result, the_id, None) - if the_name is not None: - name_value = getattr(maybe_result, the_name, None) - - if (id_value == name_or_id) or (name_value == name_or_id): - # Only allow one resource to be found. If we already - # found a match, raise an exception to show it. - if the_result is None: - the_result = maybe_result - else: - msg = "More than one %s exists with the name '%s'." - msg = (msg % (cls.get_resource_name(), name_or_id)) - raise exceptions.DuplicateResource(msg) - - return the_result - - # Try to short-circuit by looking directly for a matching ID. - try: - if cls.allow_retrieve: - return cls.get_by_id(session, name_or_id, path_args=path_args) - except exceptions.NotFoundException: - pass - - data = cls.list(session, path_args=path_args) - - result = get_one_match(data, cls.id_attribute, cls.name_attribute) - if result is not None: - return result - - if ignore_missing: - return None - raise exceptions.ResourceNotFound( - "No %s found for %s" % (cls.__name__, name_or_id)) - - -def wait_for_status(session, resource, status, failures, interval, wait): - """Wait for the resource to be in a particular status. - - :param session: The session to use for making this request. - :type session: :class:`~keystoneauth1.adapter.Adapter` - :param resource: The resource to wait on to reach the status. The resource - must have a status attribute. - :type resource: :class:`~openstack.resource.Resource` - :param status: Desired status of the resource. - :param list failures: Statuses that would indicate the transition - failed such as 'ERROR'. - :param interval: Number of seconds to wait between checks. - :param wait: Maximum number of seconds to wait for transition. - - :return: Method returns self on success. - :raises: :class:`~openstack.exceptions.ResourceTimeout` transition - to status failed to occur in wait seconds. - :raises: :class:`~openstack.exceptions.ResourceFailure` resource - transitioned to one of the failure states. - :raises: :class:`~AttributeError` if the resource does not have a status - attribute - """ - if resource.status == status: - return resource - - total_sleep = 0 - if failures is None: - failures = [] - - while total_sleep < wait: - resource.get(session) - if resource.status == status: - return resource - if resource.status in failures: - msg = ("Resource %s transitioned to failure state %s" % - (resource.id, resource.status)) - raise exceptions.ResourceFailure(msg) - time.sleep(interval) - total_sleep += interval - msg = "Timeout waiting for %s to transition to %s" % (resource.id, status) - raise exceptions.ResourceTimeout(msg) - - -def wait_for_delete(session, resource, interval, wait): - """Wait for the resource to be deleted. - - :param session: The session to use for making this request. - :type session: :class:`~keystoneauth1.adapter.Adapter` - :param resource: The resource to wait on to be deleted. - :type resource: :class:`~openstack.resource.Resource` - :param interval: Number of seconds to wait between checks. - :param wait: Maximum number of seconds to wait for the delete. - - :return: Method returns self on success. - :raises: :class:`~openstack.exceptions.ResourceTimeout` transition - to status failed to occur in wait seconds. - """ - total_sleep = 0 - while total_sleep < wait: - try: - resource.get(session) - except exceptions.NotFoundException: - return resource - time.sleep(interval) - total_sleep += interval - msg = "Timeout waiting for %s delete" % (resource.id) - raise exceptions.ResourceTimeout(msg) diff --git a/openstack/service_description.py b/openstack/service_description.py index 45a675f7d..80e5eeaaf 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -22,7 +22,6 @@ import os_service_types from openstack import _log -from openstack import proxy from openstack import proxy2 _logger = _log.setup_logging('openstack') @@ -97,17 +96,11 @@ def __init__(self, service_type, proxy_class=None, aliases=None): self._validate_proxy_class() def _validate_proxy_class(self): - if not issubclass( - self.proxy_class, (proxy.BaseProxy, proxy2.BaseProxy)): + if not issubclass(self.proxy_class, proxy2.BaseProxy): raise TypeError( "{module}.{proxy_class} must inherit from BaseProxy".format( module=self.proxy_class.__module__, proxy_class=self.proxy_class.__name__)) - if issubclass(self.proxy_class, proxy.BaseProxy) and self._warn_if_old: - warnings.warn( - "Use of proxy.BaseProxy is not supported." - " Please update to use proxy2.BaseProxy.", - DeprecationWarning) class OpenStackServiceDescription(ServiceDescription): diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py deleted file mode 100644 index 6a9e9761c..000000000 --- a/openstack/tests/unit/test_proxy.py +++ /dev/null @@ -1,358 +0,0 @@ -# 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 testtools - -from openstack import exceptions -from openstack import proxy -from openstack import resource - - -class DeleteableResource(resource.Resource): - allow_delete = True - - -class UpdateableResource(resource.Resource): - allow_update = True - - -class CreateableResource(resource.Resource): - allow_create = True - - -class RetrieveableResource(resource.Resource): - allow_retrieve = True - - -class ListableResource(resource.Resource): - allow_list = True - - -class HeadableResource(resource.Resource): - allow_head = True - - -class Test_check_resource(testtools.TestCase): - - def setUp(self): - super(Test_check_resource, self).setUp() - - def method(self, expected_type, value): - return value - - self.sot = mock.Mock() - self.sot.method = method - - def _test_correct(self, value): - decorated = proxy._check_resource(strict=False)(self.sot.method) - rv = decorated(self.sot, resource.Resource, value) - - self.assertEqual(value, rv) - - def test_correct_resource(self): - res = resource.Resource() - self._test_correct(res) - - def test_notstrict_id(self): - self._test_correct("abc123-id") - - def test_strict_id(self): - decorated = proxy._check_resource(strict=True)(self.sot.method) - self.assertRaisesRegex(ValueError, "A Resource must be passed", - decorated, self.sot, resource.Resource, - "this-is-not-a-resource") - - def test_incorrect_resource(self): - class OneType(resource.Resource): - pass - - class AnotherType(resource.Resource): - pass - - value = AnotherType() - decorated = proxy._check_resource(strict=False)(self.sot.method) - self.assertRaisesRegex(ValueError, - "Expected OneType but received AnotherType", - decorated, self.sot, OneType, value) - - -class TestProxyDelete(testtools.TestCase): - - def setUp(self): - super(TestProxyDelete, self).setUp() - - self.session = mock.Mock() - - self.fake_id = 1 - self.res = mock.Mock(spec=DeleteableResource) - self.res.id = self.fake_id - self.res.delete = mock.Mock() - - self.sot = proxy.BaseProxy(self.session) - DeleteableResource.existing = mock.Mock(return_value=self.res) - - def test_delete(self): - self.sot._delete(DeleteableResource, self.res) - self.res.delete.assert_called_with(self.sot, error_message=mock.ANY) - - self.sot._delete(DeleteableResource, self.fake_id) - DeleteableResource.existing.assert_called_with(id=self.fake_id) - self.res.delete.assert_called_with(self.sot, error_message=mock.ANY) - - # Delete generally doesn't return anything, so we will normally - # swallow any return from within a service's proxy, but make sure - # we can still return for any cases where values are returned. - self.res.delete.return_value = self.fake_id - rv = self.sot._delete(DeleteableResource, self.fake_id) - self.assertEqual(rv, self.fake_id) - - def test_delete_ignore_missing(self): - self.res.delete.side_effect = exceptions.NotFoundException( - message="test", http_status=404) - - rv = self.sot._delete(DeleteableResource, self.fake_id) - self.assertIsNone(rv) - - def test_delete_NotFound(self): - self.res.delete.side_effect = exceptions.NotFoundException( - message="test", http_status=404) - - self.assertRaisesRegex( - exceptions.NotFoundException, "test", - self.sot._delete, DeleteableResource, self.res, - ignore_missing=False) - - def test_delete_HttpException(self): - self.res.delete.side_effect = exceptions.HttpException( - message="test", http_status=500) - - self.assertRaises(exceptions.HttpException, self.sot._delete, - DeleteableResource, self.res, ignore_missing=False) - - -class TestProxyUpdate(testtools.TestCase): - - def setUp(self): - super(TestProxyUpdate, self).setUp() - - self.session = mock.Mock() - - self.fake_id = 1 - self.fake_result = "fake_result" - - self.res = mock.Mock(spec=UpdateableResource) - self.res.update = mock.Mock(return_value=self.fake_result) - self.res.update_attrs = mock.Mock() - - self.sot = proxy.BaseProxy(self.session) - - self.attrs = {"x": 1, "y": 2, "z": 3} - - UpdateableResource.existing = mock.Mock(return_value=self.res) - - def _test_update(self, value): - rv = self.sot._update(UpdateableResource, value, **self.attrs) - - self.assertEqual(rv, self.fake_result) - self.res.update_attrs.assert_called_once_with(self.attrs) - self.res.update.assert_called_once_with(self.sot) - - def test_update_resource(self): - self._test_update(self.res) - - def test_update_id(self): - self._test_update(self.fake_id) - - -class TestProxyCreate(testtools.TestCase): - - def setUp(self): - super(TestProxyCreate, self).setUp() - - self.session = mock.Mock() - - self.fake_result = "fake_result" - self.res = mock.Mock(spec=CreateableResource) - self.res.create = mock.Mock(return_value=self.fake_result) - - self.sot = proxy.BaseProxy(self.session) - - def test_create_attributes(self): - CreateableResource.new = mock.Mock(return_value=self.res) - - attrs = {"x": 1, "y": 2, "z": 3} - rv = self.sot._create(CreateableResource, **attrs) - - self.assertEqual(rv, self.fake_result) - CreateableResource.new.assert_called_once_with(**attrs) - self.res.create.assert_called_once_with(self.sot) - - -class TestProxyGet(testtools.TestCase): - - def setUp(self): - super(TestProxyGet, self).setUp() - - self.session = mock.Mock() - - self.fake_id = 1 - self.fake_name = "fake_name" - self.fake_result = "fake_result" - self.res = mock.Mock(spec=RetrieveableResource) - self.res.id = self.fake_id - self.res.get = mock.Mock(return_value=self.fake_result) - - self.sot = proxy.BaseProxy(self.session) - RetrieveableResource.existing = mock.Mock(return_value=self.res) - - def test_get_resource(self): - rv = self.sot._get(RetrieveableResource, self.res) - - self.res.get.assert_called_with(self.sot, args=None, - error_message=mock.ANY) - self.assertEqual(rv, self.fake_result) - - def test_get_resource_with_args(self): - rv = self.sot._get(RetrieveableResource, self.res, args={'K': 'V'}) - - self.res.get.assert_called_with( - self.sot, args={'K': 'V'}, - error_message='No RetrieveableResource found for {res}'.format( - res=str(self.res))) - self.assertEqual(rv, self.fake_result) - - def test_get_id(self): - rv = self.sot._get(RetrieveableResource, self.fake_id) - - RetrieveableResource.existing.assert_called_with(id=self.fake_id) - self.res.get.assert_called_with(self.sot, args=None, - error_message=mock.ANY) - self.assertEqual(rv, self.fake_result) - - def test_get_not_found(self): - self.res.get.side_effect = exceptions.NotFoundException( - message="test", http_status=404) - - # TODO(shade) The mock here does not mock the right things, so we're - # not testing the actual exception mechanism. - self.assertRaisesRegex( - exceptions.NotFoundException, "test", - self.sot._get, RetrieveableResource, self.res) - - -class TestProxyList(testtools.TestCase): - - def setUp(self): - super(TestProxyList, self).setUp() - - self.session = mock.Mock() - - self.fake_a = 1 - self.fake_b = 2 - self.fake_c = 3 - self.fake_resource = resource.Resource.new(id=self.fake_a) - self.fake_response = [resource.Resource()] - self.fake_query = {"a": self.fake_resource, "b": self.fake_b} - self.fake_path_args = {"c": self.fake_c} - - self.sot = proxy.BaseProxy(self.session) - ListableResource.list = mock.Mock() - ListableResource.list.return_value = self.fake_response - - def _test_list(self, path_args, paginated, **query): - rv = self.sot._list(ListableResource, path_args=path_args, - paginated=paginated, **query) - - self.assertEqual(self.fake_response, rv) - ListableResource.list.assert_called_once_with( - self.sot, path_args=path_args, paginated=paginated, - params={'a': self.fake_a, 'b': self.fake_b}) - - def test_list_paginated(self): - self._test_list(self.fake_path_args, True, **self.fake_query) - - def test_list_non_paginated(self): - self._test_list(self.fake_path_args, False, **self.fake_query) - - -class TestProxyHead(testtools.TestCase): - - def setUp(self): - super(TestProxyHead, self).setUp() - - self.session = mock.Mock() - - self.fake_id = 1 - self.fake_name = "fake_name" - self.fake_result = "fake_result" - self.res = mock.Mock(spec=HeadableResource) - self.res.id = self.fake_id - self.res.head = mock.Mock(return_value=self.fake_result) - - self.sot = proxy.BaseProxy(self.session) - HeadableResource.existing = mock.Mock(return_value=self.res) - - def test_head_resource(self): - rv = self.sot._head(HeadableResource, self.res) - - self.res.head.assert_called_with(self.sot) - self.assertEqual(rv, self.fake_result) - - def test_head_id(self): - rv = self.sot._head(HeadableResource, self.fake_id) - - HeadableResource.existing.assert_called_with(id=self.fake_id) - self.res.head.assert_called_with(self.sot) - self.assertEqual(rv, self.fake_result) - - def test_head_no_value(self): - MockHeadResource = mock.Mock(spec=HeadableResource) - instance = mock.Mock() - MockHeadResource.return_value = instance - - self.sot._head(MockHeadResource) - - MockHeadResource.assert_called_with() - instance.head.assert_called_with(self.sot) - - @mock.patch("openstack.resource.wait_for_status") - def test_wait_for(self, mock_wait): - mock_resource = mock.Mock() - mock_wait.return_value = mock_resource - self.sot.wait_for_status(mock_resource, 'ACTIVE') - mock_wait.assert_called_once_with( - self.sot, mock_resource, 'ACTIVE', [], 2, 120) - - @mock.patch("openstack.resource.wait_for_status") - def test_wait_for_params(self, mock_wait): - mock_resource = mock.Mock() - mock_wait.return_value = mock_resource - self.sot.wait_for_status(mock_resource, 'ACTIVE', ['ERROR'], 1, 2) - mock_wait.assert_called_once_with( - self.sot, mock_resource, 'ACTIVE', ['ERROR'], 1, 2) - - @mock.patch("openstack.resource.wait_for_delete") - def test_wait_for_delete(self, mock_wait): - mock_resource = mock.Mock() - mock_wait.return_value = mock_resource - self.sot.wait_for_delete(mock_resource) - mock_wait.assert_called_once_with( - self.sot, mock_resource, 2, 120) - - @mock.patch("openstack.resource.wait_for_delete") - def test_wait_for_delete_params(self, mock_wait): - mock_resource = mock.Mock() - mock_wait.return_value = mock_resource - self.sot.wait_for_delete(mock_resource, 1, 2) - mock_wait.assert_called_once_with( - self.sot, mock_resource, 1, 2) diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py deleted file mode 100644 index daf5f4643..000000000 --- a/openstack/tests/unit/test_resource.py +++ /dev/null @@ -1,1512 +0,0 @@ -# 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 json -import os - -from keystoneauth1 import session -import mock -import requests -from testtools import matchers - -from openstack import exceptions -from openstack import format -from openstack import resource -from openstack.tests.unit import base -from openstack import utils - - -fake_parent = 'robert' -fake_name = 'rey' -fake_id = 99 -fake_attr1 = 'lana' -fake_attr2 = 'del' - -fake_resource = 'fake' -fake_resources = 'fakes' -fake_arguments = {'parent_name': fake_parent} -fake_base_path = '/fakes/%(parent_name)s/data' -fake_path = '/fakes/rey/data' - -fake_data = {'id': fake_id, - 'enabled': True, - 'name': fake_name, - 'parent': fake_parent, - 'attr1': fake_attr1, - 'attr2': fake_attr2, - 'status': None} -fake_body = {fake_resource: fake_data} - - -class FakeParent(resource.Resource): - id_attribute = "name" - name = resource.prop('name') - - -class FakeResource(resource.Resource): - - resource_key = fake_resource - resources_key = fake_resources - base_path = fake_base_path - - allow_create = allow_retrieve = allow_update = True - allow_delete = allow_list = allow_head = True - - enabled = resource.prop('enabled', type=format.BoolStr) - name = resource.prop('name') - parent = resource.prop('parent_name') - first = resource.prop('attr1') - second = resource.prop('attr2') - third = resource.prop('attr3', alias='attr_three') - status = resource.prop('status') - - -class FakeResourceNoKeys(FakeResource): - - resource_key = None - resources_key = None - - -class PropTests(base.TestCase): - - def test_with_alias_and_type(self): - class Test(resource.Resource): - attr = resource.prop("attr1", alias="attr2", type=bool) - - t = Test(attrs={"attr2": 500}) - - # Don't test with assertTrue because 500 evaluates to True. - # Need to test that bool(500) happened and attr2 *is* True. - self.assertIs(t.attr, True) - - def test_defaults(self): - new_default = "new_default" - - class Test(resource.Resource): - attr1 = resource.prop("attr1") - attr2 = resource.prop("attr2", default=new_default) - - t = Test() - - self.assertIsNone(t.attr1) - self.assertEqual(new_default, t.attr2) - - # When the default value is passed in, it is left untouched. - # Check that attr2 is literally the same object we set as default. - t.attr2 = new_default - self.assertIs(new_default, t.attr2) - - not_default = 'not default' - t2 = Test({'attr2': not_default}) - self.assertEqual(not_default, t2.attr2) - - # Assert that if the default is passed in, it overrides the previously - # set value (bug #1425996) - t2.attr2 = new_default - self.assertEqual(new_default, t2.attr2) - - def test_get_without_instance(self): - self.assertIsNone(FakeResource.name) - - def test_set_ValueError(self): - class Test(resource.Resource): - attr = resource.prop("attr", type=int) - - t = Test() - - def should_raise(): - t.attr = "this is not an int" - - self.assertThat(should_raise, matchers.raises(ValueError)) - - def test_set_TypeError(self): - class Type(object): - def __init__(self): - pass - - class Test(resource.Resource): - attr = resource.prop("attr", type=Type) - - t = Test() - - def should_raise(): - t.attr = "this type takes no args" - - self.assertThat(should_raise, matchers.raises(TypeError)) - - def test_resource_type(self): - class FakestResource(resource.Resource): - shortstop = resource.prop("shortstop", type=FakeResource) - third_base = resource.prop("third_base", type=FakeResource) - - sot = FakestResource() - id1 = "Ernie Banks" - id2 = "Ron Santo" - sot.shortstop = id1 - sot.third_base = id2 - - resource1 = FakeResource.new(id=id1) - self.assertEqual(resource1, sot.shortstop) - self.assertEqual(id1, sot.shortstop.id) - self.assertEqual(FakeResource, type(sot.shortstop)) - - resource2 = FakeResource.new(id=id2) - self.assertEqual(resource2, sot.third_base) - self.assertEqual(id2, sot.third_base.id) - self.assertEqual(FakeResource, type(sot.third_base)) - - sot2 = FakestResource() - sot2.shortstop = resource1 - sot2.third_base = resource2 - self.assertEqual(resource1, sot2.shortstop) - self.assertEqual(id1, sot2.shortstop.id) - self.assertEqual(FakeResource, type(sot2.shortstop)) - self.assertEqual(resource2, sot2.third_base) - self.assertEqual(id2, sot2.third_base.id) - self.assertEqual(FakeResource, type(sot2.third_base)) - - body = { - "shortstop": id1, - "third_base": id2 - } - sot3 = FakestResource(body) - self.assertEqual(FakeResource({"id": id1}), sot3.shortstop) - self.assertEqual(FakeResource({"id": id2}), sot3.third_base) - - def test_set_alias_same_name(self): - class Test(resource.Resource): - attr = resource.prop("something", alias="attr") - - val = "hey" - args = {"something": val} - sot = Test(args) - - self.assertEqual(val, sot._attrs["something"]) - self.assertEqual(val, sot.attr) - - def test_property_is_none(self): - class Test(resource.Resource): - attr = resource.prop("something", type=dict) - - args = {"something": None} - sot = Test(args) - - self.assertIsNone(sot._attrs["something"]) - self.assertIsNone(sot.attr) - - -class HeaderTests(base.TestCase): - class Test(resource.Resource): - base_path = "/ramones" - service = "punk" - allow_create = True - allow_update = True - hey = resource.header("vocals") - ho = resource.header("guitar") - letsgo = resource.header("bass") - - def test_get(self): - val = "joey" - args = {"vocals": val} - sot = HeaderTests.Test({'headers': args}) - self.assertEqual(val, sot.hey) - self.assertIsNone(sot.ho) - self.assertIsNone(sot.letsgo) - - def test_set_new(self): - args = {"vocals": "joey", "bass": "deedee"} - sot = HeaderTests.Test({'headers': args}) - sot._reset_dirty() - sot.ho = "johnny" - self.assertEqual("johnny", sot.ho) - self.assertTrue(sot.is_dirty) - - def test_set_old(self): - args = {"vocals": "joey", "bass": "deedee"} - sot = HeaderTests.Test({'headers': args}) - sot._reset_dirty() - sot.letsgo = "cj" - self.assertEqual("cj", sot.letsgo) - self.assertTrue(sot.is_dirty) - - def test_set_brand_new(self): - sot = HeaderTests.Test({'headers': {}}) - sot._reset_dirty() - sot.ho = "johnny" - self.assertEqual("johnny", sot.ho) - self.assertTrue(sot.is_dirty) - self.assertEqual({'headers': {"guitar": "johnny"}}, sot) - - def test_1428342(self): - sot = HeaderTests.Test({'headers': - requests.structures.CaseInsensitiveDict()}) - - self.assertIsNone(sot.hey) - - def test_create_update_headers(self): - sot = HeaderTests.Test() - sot._reset_dirty() - sot.ho = "johnny" - sot.letsgo = "deedee" - response = mock.Mock() - response_body = {'id': 1} - response.json = mock.Mock(return_value=response_body) - response.headers = None - sess = mock.Mock() - sess.post = mock.Mock(return_value=response) - sess.put = mock.Mock(return_value=response) - - sot.create(sess) - headers = {'guitar': 'johnny', 'bass': 'deedee'} - sess.post.assert_called_with(HeaderTests.Test.base_path, - headers=headers, - json={}) - - sot['id'] = 1 - sot.letsgo = "cj" - headers = {'guitar': 'johnny', 'bass': 'cj'} - sot.update(sess) - sess.put.assert_called_with('ramones/1', - headers=headers, - json={}) - - -class ResourceTests(base.TestCase): - - def setUp(self): - super(ResourceTests, self).setUp() - self.session = mock.Mock(spec=session.Session) - self.session.get_filter = mock.Mock(return_value={}) - - def assertCalledURL(self, method, url): - # call_args gives a tuple of *args and tuple of **kwargs. - # Check that the first arg in *args (the URL) has our url. - self.assertEqual(method.call_args[0][0], url) - - def test_empty_id(self): - resp = FakeResponse(fake_body) - self.session.get.return_value = resp - - obj = FakeResource.new(**fake_arguments) - self.assertEqual(obj, obj.get(self.session)) - - self.assertEqual(fake_id, obj.id) - self.assertEqual(fake_name, obj['name']) - self.assertEqual(fake_attr1, obj['attr1']) - self.assertEqual(fake_attr2, obj['attr2']) - - self.assertEqual(fake_name, obj.name) - self.assertEqual(fake_attr1, obj.first) - self.assertEqual(fake_attr2, obj.second) - - def test_not_allowed(self): - class Nope(resource.Resource): - allow_create = allow_retrieve = allow_update = False - allow_delete = allow_list = allow_head = False - - nope = Nope() - - def cant_create(): - nope.create_by_id(1, 2) - - def cant_retrieve(): - nope.get_data_by_id(1, 2) - - def cant_update(): - nope.update_by_id(1, 2, 3) - - def cant_delete(): - nope.delete_by_id(1, 2) - - def cant_list(): - for i in nope.list(1): - pass - - def cant_head(): - nope.head_data_by_id(1, 2) - - self.assertThat(cant_create, - matchers.raises(exceptions.MethodNotSupported)) - self.assertThat(cant_retrieve, - matchers.raises(exceptions.MethodNotSupported)) - self.assertThat(cant_update, - matchers.raises(exceptions.MethodNotSupported)) - self.assertThat(cant_delete, - matchers.raises(exceptions.MethodNotSupported)) - self.assertThat(cant_list, - matchers.raises(exceptions.MethodNotSupported)) - self.assertThat(cant_head, - matchers.raises(exceptions.MethodNotSupported)) - - def _test_create_by_id(self, key, response_value, response_body, - attrs, json_body, response_headers=None): - - class FakeResource2(FakeResource): - resource_key = key - service = "my_service" - - response = mock.Mock() - response.json = mock.Mock(return_value=response_body) - response.headers = response_headers - expected_resp = response_value.copy() - if response_headers: - expected_resp.update({'headers': response_headers}) - - sess = mock.Mock() - sess.put = mock.Mock(return_value=response) - sess.post = mock.Mock(return_value=response) - - resp = FakeResource2.create_by_id(sess, attrs) - self.assertEqual(expected_resp, resp) - sess.post.assert_called_with(FakeResource2.base_path, - json=json_body) - - r_id = "my_id" - resp = FakeResource2.create_by_id(sess, attrs, resource_id=r_id) - self.assertEqual(response_value, resp) - sess.put.assert_called_with( - utils.urljoin(FakeResource2.base_path, r_id), - json=json_body) - - path_args = {"parent_name": "my_name"} - resp = FakeResource2.create_by_id(sess, attrs, path_args=path_args) - self.assertEqual(response_value, resp) - sess.post.assert_called_with(FakeResource2.base_path % path_args, - json=json_body) - - resp = FakeResource2.create_by_id(sess, attrs, resource_id=r_id, - path_args=path_args) - self.assertEqual(response_value, resp) - sess.put.assert_called_with( - utils.urljoin(FakeResource2.base_path % path_args, r_id), - json=json_body) - - def test_create_without_resource_key(self): - key = None - response_value = {"a": 1, "b": 2, "c": 3} - response_body = response_value - attrs = response_value - json_body = attrs - self._test_create_by_id(key, response_value, response_body, - attrs, json_body) - - def test_create_with_response_headers(self): - key = None - response_value = {"a": 1, "b": 2, "c": 3} - response_body = response_value - response_headers = {'location': 'foo'} - attrs = response_value.copy() - json_body = attrs - self._test_create_by_id(key, response_value, response_body, - attrs, json_body, - response_headers=response_headers) - - def test_create_with_resource_key(self): - key = "my_key" - response_value = {"a": 1, "b": 2, "c": 3} - response_body = {key: response_value} - attrs = response_body - json_body = {key: attrs} - self._test_create_by_id(key, response_value, response_body, - attrs, json_body) - - def _test_get_data_by_id(self, key, response_value, response_body): - class FakeResource2(FakeResource): - resource_key = key - service = "my_service" - - response = FakeResponse(response_body) - - sess = mock.Mock() - sess.get = mock.Mock(return_value=response) - - r_id = "my_id" - resp = FakeResource2.get_data_by_id(sess, resource_id=r_id) - self.assertEqual(response_value, resp) - sess.get.assert_called_with( - utils.urljoin(FakeResource2.base_path, r_id), - ) - - path_args = {"parent_name": "my_name"} - resp = FakeResource2.get_data_by_id(sess, resource_id=r_id, - path_args=path_args) - self.assertEqual(response_value, resp) - sess.get.assert_called_with( - utils.urljoin(FakeResource2.base_path % path_args, r_id), - ) - - def test_get_data_without_resource_key(self): - key = None - response_value = {"a": 1, "b": 2, "c": 3} - response_body = response_value - self._test_get_data_by_id(key, response_value, response_body) - - def test_get_data_with_resource_key(self): - key = "my_key" - response_value = {"a": 1, "b": 2, "c": 3} - response_body = {key: response_value} - self._test_get_data_by_id(key, response_value, response_body) - - def _test_head_data_by_id(self, key, response_value): - class FakeResource2(FakeResource): - resource_key = key - service = "my_service" - - response = mock.Mock() - response.headers = response_value - - sess = mock.Mock() - sess.head = mock.Mock(return_value=response) - - r_id = "my_id" - resp = FakeResource2.head_data_by_id(sess, resource_id=r_id) - self.assertEqual({'headers': response_value}, resp) - headers = {'Accept': ''} - sess.head.assert_called_with( - utils.urljoin(FakeResource2.base_path, r_id), - headers=headers) - - path_args = {"parent_name": "my_name"} - resp = FakeResource2.head_data_by_id(sess, resource_id=r_id, - path_args=path_args) - self.assertEqual({'headers': response_value}, resp) - headers = {'Accept': ''} - sess.head.assert_called_with( - utils.urljoin(FakeResource2.base_path % path_args, r_id), - headers=headers) - - def test_head_data_without_resource_key(self): - key = None - response_value = {"key1": "value1", "key2": "value2"} - self._test_head_data_by_id(key, response_value) - - def test_head_data_with_resource_key(self): - key = "my_key" - response_value = {"key1": "value1", "key2": "value2"} - self._test_head_data_by_id(key, response_value) - - def _test_update_by_id(self, key, response_value, response_body, - attrs, json_body, response_headers=None): - - class FakeResource2(FakeResource): - patch_update = True - resource_key = key - service = "my_service" - - response = mock.Mock() - response.json = mock.Mock(return_value=response_body) - response.headers = response_headers - expected_resp = response_value.copy() - if response_headers: - expected_resp.update({'headers': response_headers}) - - sess = mock.Mock() - sess.patch = mock.Mock(return_value=response) - - r_id = "my_id" - resp = FakeResource2.update_by_id(sess, r_id, attrs) - self.assertEqual(expected_resp, resp) - sess.patch.assert_called_with( - utils.urljoin(FakeResource2.base_path, r_id), - json=json_body) - - path_args = {"parent_name": "my_name"} - resp = FakeResource2.update_by_id(sess, r_id, attrs, - path_args=path_args) - self.assertEqual(expected_resp, resp) - sess.patch.assert_called_with( - utils.urljoin(FakeResource2.base_path % path_args, r_id), - json=json_body) - - def test_update_without_resource_key(self): - key = None - response_value = {"a": 1, "b": 2, "c": 3} - response_body = response_value - attrs = response_value - json_body = attrs - self._test_update_by_id(key, response_value, response_body, - attrs, json_body) - - def test_update_with_resource_key(self): - key = "my_key" - response_value = {"a": 1, "b": 2, "c": 3} - response_body = {key: response_value} - attrs = response_value - json_body = {key: attrs} - self._test_update_by_id(key, response_value, response_body, - attrs, json_body) - - def test_update_with_response_headers(self): - key = "my_key" - response_value = {"a": 1, "b": 2, "c": 3} - response_body = {key: response_value} - response_headers = {'location': 'foo'} - attrs = response_value.copy() - json_body = {key: attrs} - self._test_update_by_id(key, response_value, response_body, - attrs, json_body, - response_headers=response_headers) - - def test_delete_by_id(self): - class FakeResource2(FakeResource): - service = "my_service" - - sess = mock.Mock() - sess.delete = mock.Mock(return_value=FakeResponse({})) - - r_id = "my_id" - resp = FakeResource2.delete_by_id(sess, r_id) - self.assertIsNone(resp) - headers = {'Accept': ''} - sess.delete.assert_called_with( - utils.urljoin(FakeResource2.base_path, r_id), - headers=headers) - - path_args = {"parent_name": "my_name"} - resp = FakeResource2.delete_by_id(sess, r_id, path_args=path_args) - self.assertIsNone(resp) - headers = {'Accept': ''} - sess.delete.assert_called_with( - utils.urljoin(FakeResource2.base_path % path_args, r_id), - headers=headers) - - def test_create(self): - resp = FakeResponse(fake_body, headers={'location': 'foo'}) - self.session.post = mock.Mock(return_value=resp) - - # Create resource with subset of attributes in order to - # verify create refreshes all attributes from response. - obj = FakeResource.new(parent_name=fake_parent, - name=fake_name, - enabled=True, - attr1=fake_attr1) - - self.assertEqual(obj, obj.create(self.session)) - self.assertFalse(obj.is_dirty) - - last_req = self.session.post.call_args[1]["json"][ - FakeResource.resource_key] - - self.assertEqual(4, len(last_req)) - self.assertTrue(last_req['enabled']) - self.assertEqual(fake_parent, last_req['parent_name']) - self.assertEqual(fake_name, last_req['name']) - self.assertEqual(fake_attr1, last_req['attr1']) - - self.assertTrue(obj['enabled']) - self.assertEqual(fake_name, obj['name']) - self.assertEqual(fake_parent, obj['parent_name']) - self.assertEqual(fake_attr1, obj['attr1']) - self.assertEqual(fake_attr2, obj['attr2']) - self.assertIsNone(obj['status']) - - self.assertTrue(obj.enabled) - self.assertEqual(fake_id, obj.id) - self.assertEqual(fake_name, obj.name) - self.assertEqual(fake_parent, obj.parent_name) - self.assertEqual(fake_parent, obj.parent) - self.assertEqual(fake_attr1, obj.first) - self.assertEqual(fake_attr1, obj.attr1) - self.assertEqual(fake_attr2, obj.second) - self.assertEqual(fake_attr2, obj.attr2) - self.assertIsNone(obj.status) - self.assertEqual('foo', obj.location) - - def test_get(self): - resp = FakeResponse(fake_body, headers={'location': 'foo'}) - self.session.get = mock.Mock(return_value=resp) - - # Create resource with subset of attributes in order to - # verify get refreshes all attributes from response. - obj = FakeResource.from_id(str(fake_id)) - obj['parent_name'] = fake_parent - - self.assertEqual(obj, obj.get(self.session)) - - # Check that the proper URL is being built. - self.assertCalledURL(self.session.get, - os.path.join(fake_base_path % fake_arguments, - str(fake_id))[1:]) - - self.assertTrue(obj['enabled']) - self.assertEqual(fake_name, obj['name']) - self.assertEqual(fake_parent, obj['parent_name']) - self.assertEqual(fake_attr1, obj['attr1']) - self.assertEqual(fake_attr2, obj['attr2']) - self.assertIsNone(obj['status']) - - self.assertTrue(obj.enabled) - self.assertEqual(fake_id, obj.id) - self.assertEqual(fake_name, obj.name) - self.assertEqual(fake_parent, obj.parent_name) - self.assertEqual(fake_parent, obj.parent) - self.assertEqual(fake_attr1, obj.first) - self.assertEqual(fake_attr1, obj.attr1) - self.assertEqual(fake_attr2, obj.second) - self.assertEqual(fake_attr2, obj.attr2) - self.assertIsNone(obj.status) - self.assertIsNone(obj.location) - - def test_get_by_id(self): - resp = FakeResponse(fake_body) - self.session.get = mock.Mock(return_value=resp) - - obj = FakeResource.get_by_id(self.session, fake_id, - path_args=fake_arguments) - - # Check that the proper URL is being built. - self.assertCalledURL(self.session.get, - os.path.join(fake_base_path % fake_arguments, - str(fake_id))[1:]) - - self.assertEqual(fake_id, obj.id) - self.assertEqual(fake_name, obj['name']) - self.assertEqual(fake_attr1, obj['attr1']) - self.assertEqual(fake_attr2, obj['attr2']) - - self.assertEqual(fake_name, obj.name) - self.assertEqual(fake_attr1, obj.first) - self.assertEqual(fake_attr2, obj.second) - - def test_get_by_id_with_headers(self): - header1 = "fake-value1" - header2 = "fake-value2" - headers = {"header1": header1, - "header2": header2} - - resp = FakeResponse(fake_body, headers=headers) - self.session.get = mock.Mock(return_value=resp) - - class FakeResource2(FakeResource): - header1 = resource.header("header1") - header2 = resource.header("header2") - - obj = FakeResource2.get_by_id(self.session, fake_id, - path_args=fake_arguments, - include_headers=True) - - self.assertCalledURL(self.session.get, - os.path.join(fake_base_path % fake_arguments, - str(fake_id))[1:]) - - self.assertEqual(fake_id, obj.id) - self.assertEqual(fake_name, obj['name']) - self.assertEqual(fake_attr1, obj['attr1']) - self.assertEqual(fake_attr2, obj['attr2']) - self.assertEqual(header1, obj['headers']['header1']) - self.assertEqual(header2, obj['headers']['header2']) - - self.assertEqual(fake_name, obj.name) - self.assertEqual(fake_attr1, obj.first) - self.assertEqual(fake_attr2, obj.second) - self.assertEqual(header1, obj.header1) - self.assertEqual(header2, obj.header2) - - def test_head_by_id(self): - class FakeResource2(FakeResource): - header1 = resource.header("header1") - header2 = resource.header("header2") - - resp = FakeResponse(None, headers={"header1": "one", "header2": "two"}) - self.session.head = mock.Mock(return_value=resp) - - obj = FakeResource2.head_by_id(self.session, fake_id, - path_args=fake_arguments) - - self.assertCalledURL(self.session.head, - os.path.join(fake_base_path % fake_arguments, - str(fake_id))[1:]) - - self.assertEqual('one', obj['headers']['header1']) - self.assertEqual('two', obj['headers']['header2']) - - self.assertEqual('one', obj.header1) - self.assertEqual('two', obj.header2) - - def test_patch_update(self): - class FakeResourcePatch(FakeResource): - patch_update = True - - resp = FakeResponse(fake_body, headers={'location': 'foo'}) - self.session.patch = mock.Mock(return_value=resp) - - # Create resource with subset of attributes in order to - # verify update refreshes all attributes from response. - obj = FakeResourcePatch.new(id=fake_id, parent_name=fake_parent, - name=fake_name, attr1=fake_attr1) - self.assertTrue(obj.is_dirty) - - self.assertEqual(obj, obj.update(self.session)) - self.assertFalse(obj.is_dirty) - - self.assertCalledURL(self.session.patch, - os.path.join(fake_base_path % fake_arguments, - str(fake_id))[1:]) - - last_req = self.session.patch.call_args[1]["json"][ - FakeResource.resource_key] - - self.assertEqual(3, len(last_req)) - self.assertEqual(fake_parent, last_req['parent_name']) - self.assertEqual(fake_name, last_req['name']) - self.assertEqual(fake_attr1, last_req['attr1']) - - self.assertTrue(obj['enabled']) - self.assertEqual(fake_name, obj['name']) - self.assertEqual(fake_parent, obj['parent_name']) - self.assertEqual(fake_attr1, obj['attr1']) - self.assertEqual(fake_attr2, obj['attr2']) - self.assertIsNone(obj['status']) - - self.assertTrue(obj.enabled) - self.assertEqual(fake_id, obj.id) - self.assertEqual(fake_name, obj.name) - self.assertEqual(fake_parent, obj.parent_name) - self.assertEqual(fake_parent, obj.parent) - self.assertEqual(fake_attr1, obj.first) - self.assertEqual(fake_attr1, obj.attr1) - self.assertEqual(fake_attr2, obj.second) - self.assertEqual(fake_attr2, obj.attr2) - self.assertIsNone(obj.status) - self.assertEqual('foo', obj.location) - - def test_put_update(self): - class FakeResourcePut(FakeResource): - # This is False by default, but explicit for this test. - update_method = 'PUT' - - resp = FakeResponse(fake_body, headers={'location': 'foo'}) - self.session.put = mock.Mock(return_value=resp) - - # Create resource with subset of attributes in order to - # verify update refreshes all attributes from response. - obj = FakeResourcePut.new(id=fake_id, parent_name=fake_parent, - name=fake_name, attr1=fake_attr1) - self.assertTrue(obj.is_dirty) - - self.assertEqual(obj, obj.update(self.session)) - self.assertFalse(obj.is_dirty) - - self.assertCalledURL(self.session.put, - os.path.join(fake_base_path % fake_arguments, - str(fake_id))[1:]) - - last_req = self.session.put.call_args[1]["json"][ - FakeResource.resource_key] - - self.assertEqual(3, len(last_req)) - self.assertEqual(fake_parent, last_req['parent_name']) - self.assertEqual(fake_name, last_req['name']) - self.assertEqual(fake_attr1, last_req['attr1']) - - self.assertTrue(obj['enabled']) - self.assertEqual(fake_name, obj['name']) - self.assertEqual(fake_parent, obj['parent_name']) - self.assertEqual(fake_attr1, obj['attr1']) - self.assertEqual(fake_attr2, obj['attr2']) - self.assertIsNone(obj['status']) - - self.assertTrue(obj.enabled) - self.assertEqual(fake_id, obj.id) - self.assertEqual(fake_name, obj.name) - self.assertEqual(fake_parent, obj.parent_name) - self.assertEqual(fake_parent, obj.parent) - self.assertEqual(fake_attr1, obj.first) - self.assertEqual(fake_attr1, obj.attr1) - self.assertEqual(fake_attr2, obj.second) - self.assertEqual(fake_attr2, obj.attr2) - self.assertIsNone(obj.status) - self.assertEqual('foo', obj.location) - - def test_update_early_exit(self): - obj = FakeResource() - obj._dirty = [] # Bail out early if there's nothing to update. - - self.assertIsNone(obj.update("session")) - - def test_update_no_id_attribute(self): - obj = FakeResource.existing(id=1, attr="value1", - parent_name=fake_parent) - obj.first = "value2" # Make it dirty - obj.update_by_id = mock.Mock(return_value=dict()) - # If no id_attribute is returned in the update response, make sure - # we handle the resulting KeyError. - self.assertEqual(obj, obj.update("session")) - - def test_delete(self): - obj = FakeResource({"id": fake_id, "parent_name": fake_parent}) - self.session.delete.return_value = FakeResponse({}) - obj.delete(self.session) - - self.assertCalledURL(self.session.delete, - os.path.join(fake_base_path % fake_arguments, - str(fake_id))[1:]) - - def _test_list(self, resource_class): - results = [fake_data.copy(), fake_data.copy(), fake_data.copy()] - for i in range(len(results)): - results[i]['id'] = fake_id + i - if resource_class.resources_key is not None: - body = {resource_class.resources_key: - self._get_expected_results()} - sentinel = {resource_class.resources_key: []} - else: - body = self._get_expected_results() - sentinel = [] - resp1 = mock.Mock() - resp1.json = mock.Mock(return_value=body) - resp2 = mock.Mock() - resp2.json = mock.Mock(return_value=sentinel) - self.session.get.side_effect = [resp1, resp2] - - objs = list(resource_class.list(self.session, path_args=fake_arguments, - paginated=True)) - - params = {'limit': 3, 'marker': results[-1]['id']} - self.assertEqual(params, self.session.get.call_args[1]['params']) - self.assertEqual(3, len(objs)) - for obj in objs: - self.assertIn(obj.id, range(fake_id, fake_id + 3)) - self.assertEqual(fake_name, obj['name']) - self.assertEqual(fake_name, obj.name) - self.assertIsInstance(obj, FakeResource) - - def _get_expected_results(self): - results = [fake_data.copy(), fake_data.copy(), fake_data.copy()] - for i in range(len(results)): - results[i]['id'] = fake_id + i - return results - - def test_list_keyed_resource(self): - self._test_list(FakeResource) - - def test_list_non_keyed_resource(self): - self._test_list(FakeResourceNoKeys) - - def _test_list_call_count(self, paginated): - # Test that we've only made one call to receive all data - results = [fake_data.copy(), fake_data.copy(), fake_data.copy()] - resp = FakeResponse({fake_resources: results}) - attrs = {"get.return_value": resp} - session = mock.Mock(**attrs) - - list(FakeResource.list(session, params={'limit': len(results) + 1}, - path_args=fake_arguments, - paginated=paginated)) - - # Ensure we only made one call to complete this. - self.assertEqual(1, session.get.call_count) - - def test_list_bail_out(self): - # When we get less data than limit, make sure we made one call - self._test_list_call_count(True) - - def test_list_nonpaginated(self): - # When we call with paginated=False, make sure we made one call - self._test_list_call_count(False) - - def test_determine_limit(self): - full_page = [fake_data.copy(), fake_data.copy(), fake_data.copy()] - last_page = [fake_data.copy()] - - session = mock.Mock() - session.get = mock.Mock() - full_response = mock.Mock() - response_body = {FakeResource.resources_key: full_page} - full_response.json = mock.Mock(return_value=response_body) - last_response = mock.Mock() - response_body = {FakeResource.resources_key: last_page} - last_response.json = mock.Mock(return_value=response_body) - pages = [full_response, full_response, last_response] - session.get.side_effect = pages - - # Don't specify a limit. Resource.list will determine the limit - # is 3 based on the first `full_page`. - results = list(FakeResource.list(session, path_args=fake_arguments, - paginated=True)) - - self.assertEqual(session.get.call_count, len(pages)) - self.assertEqual(len(full_page + full_page + last_page), len(results)) - - def test_empty_list(self): - page = [] - - session = mock.Mock() - session.get = mock.Mock() - full_response = mock.Mock() - response_body = {FakeResource.resources_key: page} - full_response.json = mock.Mock(return_value=response_body) - pages = [full_response] - session.get.side_effect = pages - - results = list(FakeResource.list(session, path_args=fake_arguments, - paginated=True)) - - self.assertEqual(session.get.call_count, len(pages)) - self.assertEqual(len(page), len(results)) - - def test_attrs_name(self): - obj = FakeResource() - - self.assertIsNone(obj.name) - del obj.name - - def test_to_dict(self): - kwargs = { - 'enabled': True, - 'name': 'FOO', - 'parent': 'dad', - 'attr1': 'BAR', - 'attr2': ['ZOO', 'BAZ'], - 'status': 'Active', - 'headers': { - 'key': 'value' - } - } - obj = FakeResource(kwargs) - res = obj.to_dict() - self.assertIsInstance(res, dict) - self.assertTrue(res['enabled']) - self.assertEqual('FOO', res['name']) - self.assertEqual('dad', res['parent']) - self.assertEqual('BAR', res['attr1']) - self.assertEqual(['ZOO', 'BAZ'], res['attr2']) - self.assertEqual('Active', res['status']) - self.assertNotIn('headers', res) - - def test_composite_attr_happy(self): - obj = FakeResource.existing(**{'attr3': '3'}) - - try: - self.assertEqual('3', obj.third) - except AttributeError: - self.fail("third was not found as expected") - - def test_composite_attr_fallback(self): - obj = FakeResource.existing(**{'attr_three': '3'}) - - try: - self.assertEqual('3', obj.third) - except AttributeError: - self.fail("third was not found in fallback as expected") - - def test_id_del(self): - - class Test(resource.Resource): - id_attribute = "my_id" - - attrs = {"my_id": 100} - t = Test(attrs=attrs) - - self.assertEqual(attrs["my_id"], t.id) - del t.id - self.assertTrue(Test.id_attribute not in t._attrs) - - def test_from_name_with_name(self): - name = "Ernie Banks" - - obj = FakeResource.from_name(name) - self.assertEqual(name, obj.name) - - def test_from_id_with_name(self): - name = "Sandy Koufax" - - obj = FakeResource.from_id(name) - self.assertEqual(name, obj.id) - - def test_from_id_with_object(self): - name = "Mickey Mantle" - obj = FakeResource.new(name=name) - - new_obj = FakeResource.from_id(obj) - self.assertIs(new_obj, obj) - self.assertEqual(obj.name, new_obj.name) - - def test_from_id_with_bad_value(self): - def should_raise(): - FakeResource.from_id(3.14) - - self.assertThat(should_raise, matchers.raises(ValueError)) - - def test_dirty_list(self): - class Test(resource.Resource): - attr = resource.prop("attr") - - # Check if dirty after setting by prop - sot1 = Test() - self.assertFalse(sot1.is_dirty) - sot1.attr = 1 - self.assertTrue(sot1.is_dirty) - - # Check if dirty after setting by mapping - sot2 = Test() - sot2["attr"] = 1 - self.assertTrue(sot1.is_dirty) - - # Check if dirty after creation - sot3 = Test({"attr": 1}) - self.assertTrue(sot3.is_dirty) - - def test_update_attrs(self): - class Test(resource.Resource): - moe = resource.prop("the-attr") - larry = resource.prop("the-attr2") - curly = resource.prop("the-attr3", type=int) - shemp = resource.prop("the-attr4") - - value1 = "one" - value2 = "two" - value3 = "3" - value4 = "fore" - value5 = "fiver" - - sot = Test({"the-attr": value1}) - - sot.update_attrs({"the-attr2": value2, "notprop": value4}) - self.assertTrue(sot.is_dirty) - self.assertEqual(value1, sot.moe) - self.assertEqual(value1, sot["the-attr"]) - self.assertEqual(value2, sot.larry) - self.assertEqual(value4, sot.notprop) - - sot._reset_dirty() - - sot.update_attrs(curly=value3) - self.assertTrue(sot.is_dirty) - self.assertEqual(int, type(sot.curly)) - self.assertEqual(int(value3), sot.curly) - - sot._reset_dirty() - - sot.update_attrs(**{"the-attr4": value5}) - self.assertTrue(sot.is_dirty) - self.assertEqual(value5, sot.shemp) - - def test_get_id(self): - class Test(resource.Resource): - pass - - ID = "an id" - res = Test({"id": ID}) - - self.assertEqual(ID, resource.Resource.get_id(ID)) - self.assertEqual(ID, resource.Resource.get_id(res)) - - def test_convert_ids(self): - class TestResourceFoo(resource.Resource): - pass - - class TestResourceBar(resource.Resource): - pass - - resfoo = TestResourceFoo({'id': 'FAKEFOO'}) - resbar = TestResourceBar({'id': 'FAKEBAR'}) - - self.assertIsNone(resource.Resource.convert_ids(None)) - attrs = { - 'key1': 'value1' - } - self.assertEqual(attrs, resource.Resource.convert_ids(attrs)) - - attrs = { - 'foo': resfoo, - 'bar': resbar, - 'other': 'whatever', - } - res = resource.Resource.convert_ids(attrs) - self.assertEqual('FAKEFOO', res['foo']) - self.assertEqual('FAKEBAR', res['bar']) - self.assertEqual('whatever', res['other']) - - def test_repr(self): - fr = FakeResource() - fr._loaded = False - fr.first = "hey" - fr.second = "hi" - fr.third = "nah" - the_repr = repr(fr) - the_repr = the_repr.replace('openstack.tests.unit.test_resource.', '') - result = eval(the_repr) - self.assertEqual(fr._loaded, result._loaded) - self.assertEqual(fr.first, result.first) - self.assertEqual(fr.second, result.second) - self.assertEqual(fr.third, result.third) - - def test_id_attribute(self): - faker = FakeResource(fake_data) - self.assertEqual(fake_id, faker.id) - faker.id_attribute = 'name' - self.assertEqual(fake_name, faker.id) - faker.id_attribute = 'attr1' - self.assertEqual(fake_attr1, faker.id) - faker.id_attribute = 'attr2' - self.assertEqual(fake_attr2, faker.id) - faker.id_attribute = 'id' - self.assertEqual(fake_id, faker.id) - - def test_name_attribute(self): - class Person_ES(resource.Resource): - name_attribute = "nombre" - nombre = resource.prop('nombre') - - name = "Brian" - args = {'nombre': name} - - person = Person_ES(args) - self.assertEqual(name, person.nombre) - self.assertEqual(name, person.name) - - new_name = "Julien" - person.name = new_name - self.assertEqual(new_name, person.nombre) - self.assertEqual(new_name, person.name) - - def test_boolstr_prop(self): - faker = FakeResource(fake_data) - self.assertTrue(faker.enabled) - self.assertTrue(faker['enabled']) - - faker._attrs['enabled'] = False - self.assertFalse(faker.enabled) - self.assertFalse(faker['enabled']) - - # should fail fast - def set_invalid(): - faker.enabled = 'INVALID' - self.assertRaises(ValueError, set_invalid) - - -class ResourceMapping(base.TestCase): - - def test__getitem(self): - value = 10 - - class Test(resource.Resource): - attr = resource.prop("attr") - - t = Test(attrs={"attr": value}) - - self.assertEqual(value, t["attr"]) - - def test__setitem__existing_item_changed(self): - - class Test(resource.Resource): - pass - - t = Test() - key = "attr" - value = 1 - t[key] = value - - self.assertEqual(value, t._attrs[key]) - self.assertTrue(key in t._dirty) - - def test__setitem__existing_item_unchanged(self): - - class Test(resource.Resource): - pass - - key = "attr" - value = 1 - t = Test(attrs={key: value}) - t._reset_dirty() # Clear dirty list so this checks as unchanged. - t[key] = value - - self.assertEqual(value, t._attrs[key]) - self.assertTrue(key not in t._dirty) - - def test__setitem__new_item(self): - - class Test(resource.Resource): - pass - - t = Test() - key = "attr" - value = 1 - t[key] = value - - self.assertEqual(value, t._attrs[key]) - self.assertTrue(key in t._dirty) - - def test__delitem__(self): - - class Test(resource.Resource): - pass - - key = "attr" - value = 1 - t = Test(attrs={key: value}) - - del t[key] - - self.assertTrue(key not in t._attrs) - self.assertTrue(key in t._dirty) - - def test__len__(self): - - class Test(resource.Resource): - pass - - attrs = {"a": 1, "b": 2, "c": 3} - t = Test(attrs=attrs) - - self.assertEqual(len(attrs.keys()), len(t)) - - def test__iter__(self): - - class Test(resource.Resource): - pass - - attrs = {"a": 1, "b": 2, "c": 3} - t = Test(attrs=attrs) - - for attr in t: - self.assertEqual(attrs[attr], t[attr]) - - def _test_resource_serialization(self, session_method, resource_method): - attr_type = resource.Resource - - class Test(resource.Resource): - allow_create = True - attr = resource.prop("attr", type=attr_type) - - the_id = 123 - sot = Test() - sot.attr = resource.Resource({"id": the_id}) - self.assertEqual(attr_type, type(sot.attr)) - - def fake_call(*args, **kwargs): - attrs = kwargs["json"] - try: - json.dumps(attrs) - except TypeError as e: - self.fail("Unable to serialize _attrs: %s" % e) - resp = FakeResponse(attrs) - return resp - - session = mock.Mock() - setattr(session, session_method, mock.Mock(side_effect=fake_call)) - - if resource_method == "create_by_id": - session.create_by_id(session, sot._attrs) - elif resource_method == "update_by_id": - session.update_by_id(session, None, sot._attrs) - - def test_create_serializes_resource_types(self): - self._test_resource_serialization("post", "create_by_id") - - def test_update_serializes_resource_types(self): - self._test_resource_serialization("patch", "update_by_id") - - -class FakeResponse(object): - def __init__(self, response, status_code=200, headers=None): - self.body = response - self.status_code = status_code - headers = headers if headers else {'content-type': 'application/json'} - self.headers = requests.structures.CaseInsensitiveDict(headers) - - def json(self): - return self.body - - -class TestFind(base.TestCase): - NAME = 'matrix' - ID = 'Fishburne' - PROP = 'attribute2' - - def setUp(self): - super(TestFind, self).setUp() - self.mock_session = mock.Mock() - self.mock_get = mock.Mock() - self.mock_session.get = self.mock_get - self.matrix = {'id': self.ID, 'name': self.NAME, 'prop': self.PROP} - - def test_name(self): - self.mock_get.side_effect = [ - exceptions.NotFoundException(), - FakeResponse({FakeResource.resources_key: [self.matrix]}) - ] - - result = FakeResource.find(self.mock_session, self.NAME, - path_args=fake_arguments) - - self.assertEqual(self.NAME, result.name) - self.assertEqual(self.PROP, result.prop) - - def test_id(self): - self.mock_get.side_effect = [ - FakeResponse({FakeResource.resource_key: self.matrix}) - ] - - result = FakeResource.find(self.mock_session, self.ID, - path_args=fake_arguments) - - self.assertEqual(self.ID, result.id) - self.assertEqual(self.PROP, result.prop) - - path = "fakes/" + fake_parent + "/data/" + self.ID - self.mock_get.assert_any_call(path,) - - def test_id_no_retrieve(self): - self.mock_get.side_effect = [ - FakeResponse({FakeResource.resources_key: [self.matrix]}) - ] - - class NoRetrieveResource(FakeResource): - allow_retrieve = False - - result = NoRetrieveResource.find(self.mock_session, self.ID, - path_args=fake_arguments) - - self.assertEqual(self.ID, result.id) - self.assertEqual(self.PROP, result.prop) - - def test_dups(self): - dupe = self.matrix.copy() - dupe['id'] = 'different' - self.mock_get.side_effect = [ - # Raise a 404 first so we get out of the ID search and into name. - exceptions.NotFoundException(), - FakeResponse({FakeResource.resources_key: [self.matrix, dupe]}) - ] - - self.assertRaises(exceptions.DuplicateResource, FakeResource.find, - self.mock_session, self.NAME) - - def test_id_attribute_find(self): - floater = {'ip_address': "127.0.0.1", 'prop': self.PROP} - self.mock_get.side_effect = [ - FakeResponse({FakeResource.resource_key: floater}) - ] - - FakeResource.id_attribute = 'ip_address' - FakeResource.id_attribute = 'ip_address' - result = FakeResource.find(self.mock_session, "127.0.0.1", - path_args=fake_arguments) - self.assertEqual("127.0.0.1", result.id) - self.assertEqual(self.PROP, result.prop) - - FakeResource.id_attribute = 'id' - - p = {'ip_address': "127.0.0.1"} - path = fake_path + "?limit=2" - self.mock_get.called_once_with(path, params=p,) - - def test_nada(self): - self.mock_get.side_effect = [ - exceptions.NotFoundException(), - FakeResponse({FakeResource.resources_key: []}) - ] - - self.assertIsNone(FakeResource.find(self.mock_session, self.NAME)) - - def test_no_name(self): - self.mock_get.side_effect = [ - exceptions.NotFoundException(), - FakeResponse({FakeResource.resources_key: [self.matrix]}) - ] - FakeResource.name_attribute = None - - self.assertIsNone(FakeResource.find(self.mock_session, self.NAME)) - - def test_nada_not_ignored(self): - self.mock_get.side_effect = [ - exceptions.NotFoundException(), - FakeResponse({FakeResource.resources_key: []}) - ] - - self.assertRaises(exceptions.ResourceNotFound, FakeResource.find, - self.mock_session, self.NAME, ignore_missing=False) - - -class TestWaitForStatus(base.TestCase): - - def __init__(self, *args, **kwargs): - super(TestWaitForStatus, self).__init__(*args, **kwargs) - self.build = FakeResponse(self.body_with_status(fake_body, 'BUILD')) - self.active = FakeResponse(self.body_with_status(fake_body, 'ACTIVE')) - self.error = FakeResponse(self.body_with_status(fake_body, 'ERROR')) - - def setUp(self): - super(TestWaitForStatus, self).setUp() - self.sess = mock.Mock() - - def body_with_status(self, body, status): - body_copy = copy.deepcopy(body) - body_copy[fake_resource]['status'] = status - return body_copy - - def test_wait_for_status_nothing(self): - self.sess.get = mock.Mock() - sot = FakeResource.new(**fake_data) - sot.status = 'ACTIVE' - - self.assertEqual(sot, resource.wait_for_status( - self.sess, sot, 'ACTIVE', [], 1, 2)) - self.assertEqual([], self.sess.get.call_args_list) - - def test_wait_for_status(self): - self.sess.get = mock.Mock() - self.sess.get.side_effect = [self.build, self.active] - sot = FakeResource.new(**fake_data) - - self.assertEqual(sot, resource.wait_for_status( - self.sess, sot, 'ACTIVE', [], 1, 2)) - - def test_wait_for_status_timeout(self): - self.sess.get = mock.Mock() - self.sess.get.side_effect = [self.build, self.build] - sot = FakeResource.new(**fake_data) - - self.assertRaises(exceptions.ResourceTimeout, resource.wait_for_status, - self.sess, sot, 'ACTIVE', ['ERROR'], 1, 2) - - def test_wait_for_status_failures(self): - self.sess.get = mock.Mock() - self.sess.get.side_effect = [self.build, self.error] - sot = FakeResource.new(**fake_data) - - self.assertRaises(exceptions.ResourceFailure, resource.wait_for_status, - self.sess, sot, 'ACTIVE', ['ERROR'], 1, 2) - - def test_wait_for_status_no_status(self): - class FakeResourceNoStatus(resource.Resource): - allow_retrieve = True - - sot = FakeResourceNoStatus.new(id=123) - - self.assertRaises(AttributeError, resource.wait_for_status, - self.sess, sot, 'ACTIVE', ['ERROR'], 1, 2) - - -class TestWaitForDelete(base.TestCase): - - def test_wait_for_delete(self): - sess = mock.Mock() - sot = FakeResource.new(**fake_data) - sot.get = mock.Mock() - sot.get.side_effect = [ - sot, - exceptions.NotFoundException( - 'not found', FakeResponse({}, status_code=404))] - - self.assertEqual(sot, resource.wait_for_delete(sess, sot, 1, 2)) - - def test_wait_for_delete_fail(self): - sess = mock.Mock() - sot = FakeResource.new(**fake_data) - sot.get = mock.Mock(return_value=sot) - - self.assertRaises(exceptions.ResourceTimeout, resource.wait_for_delete, - sess, sot, 1, 2) From 15e78f5d569a048b333f197ef1c766d2bc452cb3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 12 Jan 2018 17:45:52 -0600 Subject: [PATCH 1936/3836] Rename resource2 and proxy2 to resource and proxy This caps off a bunch of work to get to the new and improved Resouce2/Proxy2 interfaces. All of the services have been migrated and the old classes have been removed. Rename openstack.resource2 to openstack.resource and openstack.proxy2 to openstack.proxy. Change-Id: I0b7e1c679358e1f60b5316a255aad3a59fbbc8a9 --- SHADE-MERGE-TODO.rst | 2 +- .../create/examples/resource/fake.py | 2 +- doc/source/user/index.rst | 2 +- .../user/{resource2.rst => resource.rst} | 10 +- openstack/baremetal/v1/_proxy.py | 4 +- openstack/baremetal/v1/chassis.py | 2 +- openstack/baremetal/v1/driver.py | 2 +- openstack/baremetal/v1/node.py | 2 +- openstack/baremetal/v1/port.py | 2 +- openstack/baremetal/v1/port_group.py | 2 +- openstack/baremetal/version.py | 10 +- openstack/block_storage/v2/_proxy.py | 4 +- openstack/block_storage/v2/snapshot.py | 30 +- openstack/block_storage/v2/stats.py | 9 +- openstack/block_storage/v2/type.py | 10 +- openstack/block_storage/v2/volume.py | 58 ++-- openstack/clustering/v1/_proxy.py | 29 +- openstack/clustering/v1/action.py | 2 +- openstack/clustering/v1/build_info.py | 2 +- openstack/clustering/v1/cluster.py | 2 +- openstack/clustering/v1/cluster_attr.py | 2 +- openstack/clustering/v1/cluster_policy.py | 2 +- openstack/clustering/v1/event.py | 2 +- openstack/clustering/v1/node.py | 2 +- openstack/clustering/v1/policy.py | 2 +- openstack/clustering/v1/policy_type.py | 2 +- openstack/clustering/v1/profile.py | 2 +- openstack/clustering/v1/profile_type.py | 2 +- openstack/clustering/v1/receiver.py | 2 +- openstack/clustering/v1/service.py | 2 +- openstack/clustering/version.py | 2 +- openstack/compute/v2/_proxy.py | 36 +- openstack/compute/v2/availability_zone.py | 10 +- openstack/compute/v2/extension.py | 16 +- openstack/compute/v2/flavor.py | 31 +- openstack/compute/v2/hypervisor.py | 42 +-- openstack/compute/v2/image.py | 34 +- openstack/compute/v2/keypair.py | 14 +- openstack/compute/v2/limits.py | 56 ++-- openstack/compute/v2/server.py | 101 +++--- openstack/compute/v2/server_group.py | 14 +- openstack/compute/v2/server_interface.py | 16 +- openstack/compute/v2/server_ip.py | 12 +- openstack/compute/v2/service.py | 18 +- openstack/compute/v2/volume_attachment.py | 16 +- openstack/compute/version.py | 2 +- openstack/connection.py | 4 +- openstack/database/v1/_proxy.py | 4 +- openstack/database/v1/database.py | 2 +- openstack/database/v1/flavor.py | 2 +- openstack/database/v1/instance.py | 2 +- openstack/database/v1/user.py | 2 +- openstack/identity/v2/_proxy.py | 2 +- openstack/identity/v2/extension.py | 2 +- openstack/identity/v2/role.py | 2 +- openstack/identity/v2/tenant.py | 2 +- openstack/identity/v2/user.py | 2 +- openstack/identity/v3/_proxy.py | 2 +- openstack/identity/v3/credential.py | 2 +- openstack/identity/v3/domain.py | 2 +- openstack/identity/v3/endpoint.py | 2 +- openstack/identity/v3/group.py | 2 +- openstack/identity/v3/policy.py | 2 +- openstack/identity/v3/project.py | 2 +- openstack/identity/v3/region.py | 2 +- openstack/identity/v3/role.py | 2 +- openstack/identity/v3/role_assignment.py | 2 +- .../v3/role_domain_group_assignment.py | 2 +- .../v3/role_domain_user_assignment.py | 2 +- .../v3/role_project_group_assignment.py | 2 +- .../v3/role_project_user_assignment.py | 2 +- openstack/identity/v3/service.py | 2 +- openstack/identity/v3/trust.py | 2 +- openstack/identity/v3/user.py | 2 +- openstack/identity/version.py | 2 +- openstack/image/v1/_proxy.py | 2 +- openstack/image/v1/image.py | 2 +- openstack/image/v2/_proxy.py | 24 +- openstack/image/v2/image.py | 129 ++++---- openstack/image/v2/member.py | 16 +- openstack/key_manager/v1/_proxy.py | 4 +- openstack/key_manager/v1/container.py | 25 +- openstack/key_manager/v1/order.py | 30 +- openstack/key_manager/v1/secret.py | 49 +-- openstack/load_balancer/v2/_proxy.py | 4 +- openstack/load_balancer/v2/health_monitor.py | 2 +- openstack/load_balancer/v2/l7_policy.py | 2 +- openstack/load_balancer/v2/l7_rule.py | 2 +- openstack/load_balancer/v2/listener.py | 2 +- openstack/load_balancer/v2/load_balancer.py | 2 +- openstack/load_balancer/v2/member.py | 2 +- openstack/load_balancer/v2/pool.py | 2 +- openstack/load_balancer/version.py | 2 +- openstack/message/v2/_proxy.py | 8 +- openstack/message/v2/claim.py | 22 +- openstack/message/v2/message.py | 24 +- openstack/message/v2/queue.py | 20 +- openstack/message/v2/subscription.py | 26 +- openstack/message/version.py | 2 +- openstack/network/v2/_proxy.py | 8 +- openstack/network/v2/address_scope.py | 2 +- openstack/network/v2/agent.py | 2 +- .../network/v2/auto_allocated_topology.py | 2 +- openstack/network/v2/availability_zone.py | 2 +- openstack/network/v2/extension.py | 2 +- openstack/network/v2/flavor.py | 2 +- openstack/network/v2/floating_ip.py | 2 +- openstack/network/v2/health_monitor.py | 2 +- openstack/network/v2/listener.py | 2 +- openstack/network/v2/load_balancer.py | 2 +- openstack/network/v2/metering_label.py | 2 +- openstack/network/v2/metering_label_rule.py | 2 +- openstack/network/v2/network.py | 2 +- .../network/v2/network_ip_availability.py | 2 +- openstack/network/v2/pool.py | 2 +- openstack/network/v2/pool_member.py | 2 +- openstack/network/v2/port.py | 2 +- .../network/v2/qos_bandwidth_limit_rule.py | 2 +- openstack/network/v2/qos_dscp_marking_rule.py | 2 +- .../network/v2/qos_minimum_bandwidth_rule.py | 2 +- openstack/network/v2/qos_policy.py | 2 +- openstack/network/v2/qos_rule_type.py | 2 +- openstack/network/v2/quota.py | 2 +- openstack/network/v2/rbac_policy.py | 2 +- openstack/network/v2/router.py | 2 +- openstack/network/v2/security_group.py | 2 +- openstack/network/v2/security_group_rule.py | 2 +- openstack/network/v2/segment.py | 2 +- openstack/network/v2/service_profile.py | 2 +- openstack/network/v2/service_provider.py | 2 +- openstack/network/v2/subnet.py | 2 +- openstack/network/v2/subnet_pool.py | 2 +- openstack/network/v2/vpn_service.py | 2 +- openstack/network/version.py | 2 +- openstack/object_store/v1/_base.py | 2 +- openstack/object_store/v1/_proxy.py | 2 +- openstack/object_store/v1/account.py | 2 +- openstack/object_store/v1/container.py | 2 +- openstack/object_store/v1/obj.py | 2 +- openstack/orchestration/v1/_proxy.py | 4 +- openstack/orchestration/v1/resource.py | 2 +- openstack/orchestration/v1/software_config.py | 2 +- .../orchestration/v1/software_deployment.py | 2 +- openstack/orchestration/v1/stack.py | 2 +- .../orchestration/v1/stack_environment.py | 2 +- openstack/orchestration/v1/stack_files.py | 2 +- openstack/orchestration/v1/stack_template.py | 2 +- openstack/orchestration/v1/template.py | 2 +- openstack/orchestration/version.py | 2 +- openstack/{proxy2.py => proxy.py} | 98 +++--- openstack/{resource2.py => resource.py} | 22 +- openstack/service_description.py | 19 +- .../tests/unit/baremetal/v1/test_proxy.py | 4 +- .../tests/unit/block_storage/v2/test_proxy.py | 4 +- openstack/tests/unit/cluster/v1/test_proxy.py | 22 +- openstack/tests/unit/compute/v2/test_proxy.py | 12 +- .../tests/unit/database/v1/test_proxy.py | 8 +- .../tests/unit/identity/v2/test_proxy.py | 2 +- .../tests/unit/identity/v3/test_proxy.py | 4 +- openstack/tests/unit/image/v1/test_proxy.py | 2 +- openstack/tests/unit/image/v2/test_proxy.py | 18 +- .../tests/unit/key_manager/v1/test_proxy.py | 4 +- .../tests/unit/load_balancer/test_proxy.py | 12 +- openstack/tests/unit/message/v2/test_proxy.py | 20 +- openstack/tests/unit/network/v2/test_proxy.py | 46 +-- openstack/tests/unit/network/v2/test_quota.py | 2 +- .../tests/unit/object_store/v1/test_proxy.py | 4 +- .../tests/unit/orchestration/v1/test_proxy.py | 12 +- .../tests/unit/orchestration/v1/test_stack.py | 2 +- .../unit/orchestration/v1/test_stack_files.py | 2 +- .../unit/orchestration/v1/test_template.py | 2 +- openstack/tests/unit/test_connection.py | 2 +- .../unit/{test_proxy2.py => test_proxy.py} | 70 ++-- openstack/tests/unit/test_proxy_base.py | 15 +- openstack/tests/unit/test_proxy_base2.py | 18 +- .../{test_resource2.py => test_resource.py} | 309 +++++++++--------- openstack/tests/unit/workflow/test_proxy.py | 4 +- openstack/workflow/v2/_proxy.py | 4 +- openstack/workflow/v2/execution.py | 2 +- openstack/workflow/v2/workflow.py | 2 +- openstack/workflow/version.py | 2 +- 181 files changed, 974 insertions(+), 974 deletions(-) rename doc/source/user/{resource2.rst => resource.rst} (65%) rename openstack/{proxy2.py => proxy.py} (79%) rename openstack/{resource2.py => resource.py} (98%) rename openstack/tests/unit/{test_proxy2.py => test_proxy.py} (87%) rename openstack/tests/unit/{test_resource2.py => test_resource.py} (86%) diff --git a/SHADE-MERGE-TODO.rst b/SHADE-MERGE-TODO.rst index 32baa0925..2a12d00f6 100644 --- a/SHADE-MERGE-TODO.rst +++ b/SHADE-MERGE-TODO.rst @@ -23,11 +23,11 @@ already. For reference, those are: * Removed the Session object in favor of using keystoneauth. * Plumbed Proxy use of Adapter through the Adapter subclass from shade that uses the TaskManager to run REST calls. +* Finish migrating to Resource2 and Proxy2, rename them to Resource and Proxy. Next steps ========== -* Finish migrating to Resource2 and Proxy2, rename them to Resource and Proxy. * Maybe rename self.session and session parameter in all usage in proxy and resource to self.adapter. They are Adapters not Sessions, but that may not mean anything to people. diff --git a/doc/source/contributor/create/examples/resource/fake.py b/doc/source/contributor/create/examples/resource/fake.py index 527e5ad11..074a5fb78 100644 --- a/doc/source/contributor/create/examples/resource/fake.py +++ b/doc/source/contributor/create/examples/resource/fake.py @@ -1,7 +1,7 @@ # Apache 2 header omitted for brevity from openstack.fake import fake_service -from openstack import resource2 as resource +from openstack import resource class Fake(resource.Resource): diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index fb5e2e082..96b6c80a4 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -137,7 +137,7 @@ can be customized. .. toctree:: :maxdepth: 1 - resource2 + resource service_filter utils diff --git a/doc/source/user/resource2.rst b/doc/source/user/resource.rst similarity index 65% rename from doc/source/user/resource2.rst rename to doc/source/user/resource.rst index bc664213d..8453265f5 100644 --- a/doc/source/user/resource2.rst +++ b/doc/source/user/resource.rst @@ -4,23 +4,23 @@ this module will be drop the 2 suffix and be the only resource module.** Resource ======== -.. automodule:: openstack.resource2 +.. automodule:: openstack.resource Components ---------- -.. autoclass:: openstack.resource2.Body +.. autoclass:: openstack.resource.Body :members: -.. autoclass:: openstack.resource2.Header +.. autoclass:: openstack.resource.Header :members: -.. autoclass:: openstack.resource2.URI +.. autoclass:: openstack.resource.URI :members: The Resource class ------------------ -.. autoclass:: openstack.resource2.Resource +.. autoclass:: openstack.resource.Resource :members: :member-order: bysource diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 0e68cf357..a802f6973 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -15,11 +15,11 @@ from openstack.baremetal.v1 import node as _node from openstack.baremetal.v1 import port as _port from openstack.baremetal.v1 import port_group as _portgroup -from openstack import proxy2 +from openstack import proxy from openstack import utils -class Proxy(proxy2.BaseProxy): +class Proxy(proxy.BaseProxy): def chassis(self, details=False, **query): """Retrieve a generator of chassis. diff --git a/openstack/baremetal/v1/chassis.py b/openstack/baremetal/v1/chassis.py index 715acb250..116ea5696 100644 --- a/openstack/baremetal/v1/chassis.py +++ b/openstack/baremetal/v1/chassis.py @@ -11,7 +11,7 @@ # under the License. from openstack.baremetal import baremetal_service -from openstack import resource2 as resource +from openstack import resource class Chassis(resource.Resource): diff --git a/openstack/baremetal/v1/driver.py b/openstack/baremetal/v1/driver.py index 8421bdb54..c097edbd8 100644 --- a/openstack/baremetal/v1/driver.py +++ b/openstack/baremetal/v1/driver.py @@ -11,7 +11,7 @@ # under the License. from openstack.baremetal import baremetal_service -from openstack import resource2 as resource +from openstack import resource class Driver(resource.Resource): diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index e99a61487..3ba698cff 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -11,7 +11,7 @@ # under the License. from openstack.baremetal import baremetal_service -from openstack import resource2 as resource +from openstack import resource class Node(resource.Resource): diff --git a/openstack/baremetal/v1/port.py b/openstack/baremetal/v1/port.py index 75f47f68d..cca5d14aa 100644 --- a/openstack/baremetal/v1/port.py +++ b/openstack/baremetal/v1/port.py @@ -11,7 +11,7 @@ # under the License. from openstack.baremetal import baremetal_service -from openstack import resource2 as resource +from openstack import resource class Port(resource.Resource): diff --git a/openstack/baremetal/v1/port_group.py b/openstack/baremetal/v1/port_group.py index 41fd78396..d1d44e7d4 100644 --- a/openstack/baremetal/v1/port_group.py +++ b/openstack/baremetal/v1/port_group.py @@ -11,7 +11,7 @@ # under the License. from openstack.baremetal import baremetal_service -from openstack import resource2 as resource +from openstack import resource class PortGroup(resource.Resource): diff --git a/openstack/baremetal/version.py b/openstack/baremetal/version.py index 51d0e85bf..1613fe43a 100644 --- a/openstack/baremetal/version.py +++ b/openstack/baremetal/version.py @@ -11,10 +11,10 @@ # under the License. from openstack.baremetal import baremetal_service -from openstack import resource2 +from openstack import resource -class Version(resource2.Resource): +class Version(resource.Resource): resource_key = 'version' resources_key = 'versions' base_path = '/' @@ -26,6 +26,6 @@ class Version(resource2.Resource): allow_list = True # Attributes - links = resource2.Body('links') - status = resource2.Body('status') - updated = resource2.Body('updated') + links = resource.Body('links') + status = resource.Body('status') + updated = resource.Body('updated') diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 90bb623b3..4b7afd90c 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -14,10 +14,10 @@ from openstack.block_storage.v2 import stats as _stats from openstack.block_storage.v2 import type as _type from openstack.block_storage.v2 import volume as _volume -from openstack import proxy2 +from openstack import proxy -class Proxy(proxy2.BaseProxy): +class Proxy(proxy.BaseProxy): def get_snapshot(self, snapshot): """Get a single snapshot diff --git a/openstack/block_storage/v2/snapshot.py b/openstack/block_storage/v2/snapshot.py index 9d5b3dae6..d1080fd3f 100644 --- a/openstack/block_storage/v2/snapshot.py +++ b/openstack/block_storage/v2/snapshot.py @@ -12,17 +12,17 @@ from openstack.block_storage import block_storage_service from openstack import format -from openstack import resource2 +from openstack import resource -class Snapshot(resource2.Resource): +class Snapshot(resource.Resource): resource_key = "snapshot" resources_key = "snapshots" base_path = "/snapshots" service = block_storage_service.BlockStorageService() - _query_mapping = resource2.QueryParameters('all_tenants', 'name', 'status', - 'volume_id') + _query_mapping = resource.QueryParameters( + 'all_tenants', 'name', 'status', 'volume_id') # capabilities allow_get = True @@ -33,26 +33,26 @@ class Snapshot(resource2.Resource): # Properties #: A ID representing this snapshot. - id = resource2.Body("id") + id = resource.Body("id") #: Name of the snapshot. Default is None. - name = resource2.Body("name") + name = resource.Body("name") #: The current status of this snapshot. Potential values are creating, #: available, deleting, error, and error_deleting. - status = resource2.Body("status") + status = resource.Body("status") #: Description of snapshot. Default is None. - description = resource2.Body("description") + description = resource.Body("description") #: The timestamp of this snapshot creation. - created_at = resource2.Body("created_at") + created_at = resource.Body("created_at") #: Metadata associated with this snapshot. - metadata = resource2.Body("metadata", type=dict) + metadata = resource.Body("metadata", type=dict) #: The ID of the volume this snapshot was taken of. - volume_id = resource2.Body("volume_id") + volume_id = resource.Body("volume_id") #: The size of the volume, in GBs. - size = resource2.Body("size", type=int) + size = resource.Body("size", type=int) #: Indicate whether to create snapshot, even if the volume is attached. #: Default is ``False``. *Type: bool* - is_forced = resource2.Body("force", type=format.BoolStr) + is_forced = resource.Body("force", type=format.BoolStr) class SnapshotDetail(Snapshot): @@ -60,6 +60,6 @@ class SnapshotDetail(Snapshot): base_path = "/snapshots/detail" #: The percentage of completeness the snapshot is currently at. - progress = resource2.Body("os-extended-snapshot-attributes:progress") + progress = resource.Body("os-extended-snapshot-attributes:progress") #: The project ID this snapshot is associated with. - project_id = resource2.Body("os-extended-snapshot-attributes:project_id") + project_id = resource.Body("os-extended-snapshot-attributes:project_id") diff --git a/openstack/block_storage/v2/stats.py b/openstack/block_storage/v2/stats.py index 80a9c0b60..914ba9436 100644 --- a/openstack/block_storage/v2/stats.py +++ b/openstack/block_storage/v2/stats.py @@ -11,10 +11,10 @@ # under the License. from openstack.block_storage import block_storage_service -from openstack import resource2 +from openstack import resource -class Pools(resource2.Resource): +class Pools(resource.Resource): resource_key = "pool" resources_key = "pools" base_path = "/scheduler-stats/get_pools?detail=True" @@ -28,7 +28,6 @@ class Pools(resource2.Resource): # Properties #: The Cinder name for the pool - name = resource2.Body("name") + name = resource.Body("name") #: returns a dict with information about the pool - capabilities = resource2.Body("capabilities", - type=dict) + capabilities = resource.Body("capabilities", type=dict) diff --git a/openstack/block_storage/v2/type.py b/openstack/block_storage/v2/type.py index 7b477aee4..2ca9a51b0 100644 --- a/openstack/block_storage/v2/type.py +++ b/openstack/block_storage/v2/type.py @@ -11,10 +11,10 @@ # under the License. from openstack.block_storage import block_storage_service -from openstack import resource2 +from openstack import resource -class Type(resource2.Resource): +class Type(resource.Resource): resource_key = "volume_type" resources_key = "volume_types" base_path = "/types" @@ -28,8 +28,8 @@ class Type(resource2.Resource): # Properties #: A ID representing this type. - id = resource2.Body("id") + id = resource.Body("id") #: Name of the type. - name = resource2.Body("name") + name = resource.Body("name") #: A dict of extra specifications. "capabilities" is a usual key. - extra_specs = resource2.Body("extra_specs", type=dict) + extra_specs = resource.Body("extra_specs", type=dict) diff --git a/openstack/block_storage/v2/volume.py b/openstack/block_storage/v2/volume.py index ee8c60d62..25e77509f 100644 --- a/openstack/block_storage/v2/volume.py +++ b/openstack/block_storage/v2/volume.py @@ -12,17 +12,17 @@ from openstack.block_storage import block_storage_service from openstack import format -from openstack import resource2 +from openstack import resource -class Volume(resource2.Resource): +class Volume(resource.Resource): resource_key = "volume" resources_key = "volumes" base_path = "/volumes" service = block_storage_service.BlockStorageService() - _query_mapping = resource2.QueryParameters('all_tenants', 'name', - 'status', 'project_id') + _query_mapping = resource.QueryParameters( + 'all_tenants', 'name', 'status', 'project_id') # capabilities allow_get = True @@ -33,48 +33,48 @@ class Volume(resource2.Resource): # Properties #: A ID representing this volume. - id = resource2.Body("id") + id = resource.Body("id") #: The name of this volume. - name = resource2.Body("name") + name = resource.Body("name") #: A list of links associated with this volume. *Type: list* - links = resource2.Body("links", type=list) + links = resource.Body("links", type=list) #: The availability zone. - availability_zone = resource2.Body("availability_zone") + availability_zone = resource.Body("availability_zone") #: To create a volume from an existing volume, specify the ID of #: the existing volume. If specified, the volume is created with #: same size of the source volume. - source_volume_id = resource2.Body("source_volid") + source_volume_id = resource.Body("source_volid") #: The volume description. - description = resource2.Body("description") + description = resource.Body("description") #: To create a volume from an existing snapshot, specify the ID of #: the existing volume snapshot. If specified, the volume is created #: in same availability zone and with same size of the snapshot. - snapshot_id = resource2.Body("snapshot_id") + snapshot_id = resource.Body("snapshot_id") #: The size of the volume, in GBs. *Type: int* - size = resource2.Body("size", type=int) + size = resource.Body("size", type=int) #: The ID of the image from which you want to create the volume. #: Required to create a bootable volume. - image_id = resource2.Body("imageRef") + image_id = resource.Body("imageRef") #: The name of the associated volume type. - volume_type = resource2.Body("volume_type") + volume_type = resource.Body("volume_type") #: Enables or disables the bootable attribute. You can boot an #: instance from a bootable volume. *Type: bool* - is_bootable = resource2.Body("bootable", type=format.BoolStr) + is_bootable = resource.Body("bootable", type=format.BoolStr) #: One or more metadata key and value pairs to associate with the volume. - metadata = resource2.Body("metadata") + metadata = resource.Body("metadata") #: One or more metadata key and value pairs about image - volume_image_metadata = resource2.Body("volume_image_metadata") + volume_image_metadata = resource.Body("volume_image_metadata") #: One of the following values: creating, available, attaching, in-use #: deleting, error, error_deleting, backing-up, restoring-backup, #: error_restoring. For details on these statuses, see the #: Block Storage API documentation. - status = resource2.Body("status") + status = resource.Body("status") #: TODO(briancurtin): This is currently undocumented in the API. - attachments = resource2.Body("attachments") + attachments = resource.Body("attachments") #: The timestamp of this volume creation. - created_at = resource2.Body("created_at") + created_at = resource.Body("created_at") class VolumeDetail(Volume): @@ -82,24 +82,24 @@ class VolumeDetail(Volume): base_path = "/volumes/detail" #: The volume's current back-end. - host = resource2.Body("os-vol-host-attr:host") + host = resource.Body("os-vol-host-attr:host") #: The project ID associated with current back-end. - project_id = resource2.Body("os-vol-tenant-attr:tenant_id") + project_id = resource.Body("os-vol-tenant-attr:tenant_id") #: The status of this volume's migration (None means that a migration #: is not currently in progress). - migration_status = resource2.Body("os-vol-mig-status-attr:migstat") + migration_status = resource.Body("os-vol-mig-status-attr:migstat") #: The volume ID that this volume's name on the back-end is based on. - migration_id = resource2.Body("os-vol-mig-status-attr:name_id") + migration_id = resource.Body("os-vol-mig-status-attr:name_id") #: Status of replication on this volume. - replication_status = resource2.Body("replication_status") + replication_status = resource.Body("replication_status") #: Extended replication status on this volume. - extended_replication_status = resource2.Body( + extended_replication_status = resource.Body( "os-volume-replication:extended_status") #: ID of the consistency group. - consistency_group_id = resource2.Body("consistencygroup_id") + consistency_group_id = resource.Body("consistencygroup_id") #: Data set by the replication driver - replication_driver_data = resource2.Body( + replication_driver_data = resource.Body( "os-volume-replication:driver_data") #: ``True`` if this volume is encrypted, ``False`` if not. #: *Type: bool* - is_encrypted = resource2.Body("encrypted", type=format.BoolStr) + is_encrypted = resource.Body("encrypted", type=format.BoolStr) diff --git a/openstack/clustering/v1/_proxy.py b/openstack/clustering/v1/_proxy.py index 562f8abde..e17a23504 100644 --- a/openstack/clustering/v1/_proxy.py +++ b/openstack/clustering/v1/_proxy.py @@ -23,12 +23,12 @@ from openstack.clustering.v1 import profile_type as _profile_type from openstack.clustering.v1 import receiver as _receiver from openstack.clustering.v1 import service as _service -from openstack import proxy2 -from openstack import resource2 +from openstack import proxy +from openstack import resource from openstack import utils -class Proxy(proxy2.BaseProxy): +class Proxy(proxy.BaseProxy): def get_build_info(self): """Get build info for service engine and API @@ -913,7 +913,7 @@ def cluster_policies(self, cluster, **query): enabled on the cluster. :returns: A generator of cluster-policy binding instances. """ - cluster_id = resource2.Resource._get_id(cluster) + cluster_id = resource.Resource._get_id(cluster) return self._list(_cluster_policy.ClusterPolicy, paginated=False, cluster_id=cluster_id, **query) @@ -1102,13 +1102,13 @@ def events(self, **query): """ return self._list(_event.Event, paginated=True, **query) - def wait_for_status(self, resource, status, failures=None, interval=2, + def wait_for_status(self, res, status, failures=None, interval=2, wait=120): """Wait for a resource to be in a particular status. - :param resource: The resource to wait on to reach the specified status. - The resource must have a ``status`` attribute. - :type resource: A :class:`~openstack.resource2.Resource` object. + :param res: The resource to wait on to reach the specified status. + The resource must have a ``status`` attribute. + :type resource: A :class:`~openstack.resource.Resource` object. :param status: Desired status. :param failures: Statuses that would be interpreted as failures. :type failures: :py:class:`list` @@ -1125,14 +1125,14 @@ def wait_for_status(self, resource, status, failures=None, interval=2, ``status`` attribute. """ failures = [] if failures is None else failures - return resource2.wait_for_status(self, resource, status, - failures, interval, wait) + return resource.wait_for_status( + self, res, status, failures, interval, wait) - def wait_for_delete(self, resource, interval=2, wait=120): + def wait_for_delete(self, res, interval=2, wait=120): """Wait for a resource to be deleted. - :param resource: The resource to wait on to be deleted. - :type resource: A :class:`~openstack.resource2.Resource` object. + :param res: The resource to wait on to be deleted. + :type resource: A :class:`~openstack.resource.Resource` object. :param interval: Number of seconds to wait before to consecutive checks. Default to 2. :param wait: Maximum number of seconds to wait before the change. @@ -1141,8 +1141,7 @@ def wait_for_delete(self, resource, interval=2, wait=120): :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition to delete failed to occur in the specified seconds. """ - return resource2.wait_for_delete(self, resource, interval, - wait) + return resource.wait_for_delete(self, res, interval, wait) def services(self, **query): """Get a generator of services. diff --git a/openstack/clustering/v1/action.py b/openstack/clustering/v1/action.py index 46609f8a9..76a63c9b2 100644 --- a/openstack/clustering/v1/action.py +++ b/openstack/clustering/v1/action.py @@ -12,7 +12,7 @@ from openstack.clustering import clustering_service -from openstack import resource2 as resource +from openstack import resource class Action(resource.Resource): diff --git a/openstack/clustering/v1/build_info.py b/openstack/clustering/v1/build_info.py index e666050eb..901f43b5f 100644 --- a/openstack/clustering/v1/build_info.py +++ b/openstack/clustering/v1/build_info.py @@ -11,7 +11,7 @@ # under the License. from openstack.clustering import clustering_service -from openstack import resource2 as resource +from openstack import resource class BuildInfo(resource.Resource): diff --git a/openstack/clustering/v1/cluster.py b/openstack/clustering/v1/cluster.py index 8b7f12757..1b3668700 100644 --- a/openstack/clustering/v1/cluster.py +++ b/openstack/clustering/v1/cluster.py @@ -11,7 +11,7 @@ # under the License. from openstack.clustering import clustering_service -from openstack import resource2 as resource +from openstack import resource from openstack import utils diff --git a/openstack/clustering/v1/cluster_attr.py b/openstack/clustering/v1/cluster_attr.py index 353728503..68e1f53f8 100644 --- a/openstack/clustering/v1/cluster_attr.py +++ b/openstack/clustering/v1/cluster_attr.py @@ -11,7 +11,7 @@ # under the License. from openstack.clustering import clustering_service -from openstack import resource2 as resource +from openstack import resource class ClusterAttr(resource.Resource): diff --git a/openstack/clustering/v1/cluster_policy.py b/openstack/clustering/v1/cluster_policy.py index 6d48ece91..49c9c4c77 100644 --- a/openstack/clustering/v1/cluster_policy.py +++ b/openstack/clustering/v1/cluster_policy.py @@ -11,7 +11,7 @@ # under the License. from openstack.clustering import clustering_service -from openstack import resource2 as resource +from openstack import resource class ClusterPolicy(resource.Resource): diff --git a/openstack/clustering/v1/event.py b/openstack/clustering/v1/event.py index c248a5179..6ca6798c3 100644 --- a/openstack/clustering/v1/event.py +++ b/openstack/clustering/v1/event.py @@ -12,7 +12,7 @@ from openstack.clustering import clustering_service -from openstack import resource2 as resource +from openstack import resource class Event(resource.Resource): diff --git a/openstack/clustering/v1/node.py b/openstack/clustering/v1/node.py index 4147e442d..abfdbbdb4 100644 --- a/openstack/clustering/v1/node.py +++ b/openstack/clustering/v1/node.py @@ -11,7 +11,7 @@ # under the License. from openstack.clustering import clustering_service -from openstack import resource2 as resource +from openstack import resource from openstack import utils diff --git a/openstack/clustering/v1/policy.py b/openstack/clustering/v1/policy.py index f8613f291..5a1202e4f 100644 --- a/openstack/clustering/v1/policy.py +++ b/openstack/clustering/v1/policy.py @@ -11,7 +11,7 @@ # under the License. from openstack.clustering import clustering_service -from openstack import resource2 as resource +from openstack import resource class Policy(resource.Resource): diff --git a/openstack/clustering/v1/policy_type.py b/openstack/clustering/v1/policy_type.py index e63edf007..6acb10000 100644 --- a/openstack/clustering/v1/policy_type.py +++ b/openstack/clustering/v1/policy_type.py @@ -11,7 +11,7 @@ # under the License. from openstack.clustering import clustering_service -from openstack import resource2 as resource +from openstack import resource class PolicyType(resource.Resource): diff --git a/openstack/clustering/v1/profile.py b/openstack/clustering/v1/profile.py index e01ebccb8..b148a6167 100644 --- a/openstack/clustering/v1/profile.py +++ b/openstack/clustering/v1/profile.py @@ -11,7 +11,7 @@ # under the License. from openstack.clustering import clustering_service -from openstack import resource2 as resource +from openstack import resource class Profile(resource.Resource): diff --git a/openstack/clustering/v1/profile_type.py b/openstack/clustering/v1/profile_type.py index be04686ea..3d7e4df23 100644 --- a/openstack/clustering/v1/profile_type.py +++ b/openstack/clustering/v1/profile_type.py @@ -11,7 +11,7 @@ # under the License. from openstack.clustering import clustering_service -from openstack import resource2 as resource +from openstack import resource class ProfileType(resource.Resource): diff --git a/openstack/clustering/v1/receiver.py b/openstack/clustering/v1/receiver.py index 35265aeef..434244d75 100644 --- a/openstack/clustering/v1/receiver.py +++ b/openstack/clustering/v1/receiver.py @@ -11,7 +11,7 @@ # under the License. from openstack.clustering import clustering_service -from openstack import resource2 as resource +from openstack import resource class Receiver(resource.Resource): diff --git a/openstack/clustering/v1/service.py b/openstack/clustering/v1/service.py index dde009087..d9de49687 100644 --- a/openstack/clustering/v1/service.py +++ b/openstack/clustering/v1/service.py @@ -11,7 +11,7 @@ # under the License. from openstack.clustering import clustering_service -from openstack import resource2 as resource +from openstack import resource class Service(resource.Resource): diff --git a/openstack/clustering/version.py b/openstack/clustering/version.py index c08894012..8fc20902b 100644 --- a/openstack/clustering/version.py +++ b/openstack/clustering/version.py @@ -12,7 +12,7 @@ from openstack.clustering import clustering_service -from openstack import resource2 as resource +from openstack import resource class Version(resource.Resource): diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 9ba0ef6e4..889295939 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -23,11 +23,11 @@ from openstack.compute.v2 import server_ip from openstack.compute.v2 import service as _service from openstack.compute.v2 import volume_attachment as _volume_attachment -from openstack import proxy2 -from openstack import resource2 +from openstack import proxy +from openstack import resource -class Proxy(proxy2.BaseProxy): +class Proxy(proxy.BaseProxy): def find_extension(self, name_or_id, ignore_missing=True): """Find a single extension @@ -501,7 +501,7 @@ def resize_server(self, server, flavor): :returns: None """ server = self._get_resource(_server.Server, server) - flavor_id = resource2.Resource._get_id(flavor) + flavor_id = resource.Resource._get_id(flavor) server.resize(self, flavor_id) def confirm_server_resize(self, server): @@ -551,7 +551,7 @@ def add_security_group_to_server(self, server, security_group): :returns: None """ server = self._get_resource(_server.Server, server) - security_group_id = resource2.Resource._get_id(security_group) + security_group_id = resource.Resource._get_id(security_group) server.add_security_group(self, security_group_id) def remove_security_group_from_server(self, server, security_group): @@ -566,7 +566,7 @@ def remove_security_group_from_server(self, server, security_group): :returns: None """ server = self._get_resource(_server.Server, server) - security_group_id = resource2.Resource._get_id(security_group) + security_group_id = resource.Resource._get_id(security_group) server.remove_security_group(self, security_group_id) def add_fixed_ip_to_server(self, server, network_id): @@ -806,8 +806,8 @@ def get_server_console_output(self, server, length=None): def wait_for_server(self, server, status='ACTIVE', failures=['ERROR'], interval=2, wait=120): - return resource2.wait_for_status(self, server, status, - failures, interval, wait) + return resource.wait_for_status( + self, server, status, failures, interval, wait) def create_server_interface(self, server, **attrs): """Create a new server interface from attributes @@ -822,7 +822,7 @@ def create_server_interface(self, server, **attrs): :returns: The results of server interface creation :rtype: :class:`~openstack.compute.v2.server_interface.ServerInterface` """ - server_id = resource2.Resource._get_id(server) + server_id = resource.Resource._get_id(server) return self._create(_server_interface.ServerInterface, server_id=server_id, **attrs) @@ -848,7 +848,7 @@ def delete_server_interface(self, server_interface, server=None, """ server_id = self._get_uri_attribute(server_interface, server, "server_id") - server_interface = resource2.Resource._get_id(server_interface) + server_interface = resource.Resource._get_id(server_interface) self._delete(_server_interface.ServerInterface, port_id=server_interface, @@ -874,7 +874,7 @@ def get_server_interface(self, server_interface, server=None): """ server_id = self._get_uri_attribute(server_interface, server, "server_id") - server_interface = resource2.Resource._get_id(server_interface) + server_interface = resource.Resource._get_id(server_interface) return self._get(_server_interface.ServerInterface, server_id=server_id, port_id=server_interface) @@ -888,7 +888,7 @@ def server_interfaces(self, server): :returns: A generator of ServerInterface objects :rtype: :class:`~openstack.compute.v2.server_interface.ServerInterface` """ - server_id = resource2.Resource._get_id(server) + server_id = resource.Resource._get_id(server) return self._list(_server_interface.ServerInterface, paginated=False, server_id=server_id) @@ -903,7 +903,7 @@ def server_ips(self, server, network_label=None): :returns: A generator of ServerIP objects :rtype: :class:`~openstack.compute.v2.server_ip.ServerIP` """ - server_id = resource2.Resource._get_id(server) + server_id = resource.Resource._get_id(server) return self._list(server_ip.ServerIP, paginated=False, server_id=server_id, network_label=network_label) @@ -1150,7 +1150,7 @@ def create_volume_attachment(self, server, **attrs): :rtype: :class:`~openstack.compute.v2.volume_attachment.VolumeAttachment` """ - server_id = resource2.Resource._get_id(server) + server_id = resource.Resource._get_id(server) return self._create(_volume_attachment.VolumeAttachment, server_id=server_id, **attrs) @@ -1177,7 +1177,7 @@ def update_volume_attachment(self, volume_attachment, server, """ server_id = self._get_uri_attribute(volume_attachment, server, "server_id") - volume_attachment = resource2.Resource._get_id(volume_attachment) + volume_attachment = resource.Resource._get_id(volume_attachment) return self._update(_volume_attachment.VolumeAttachment, attachment_id=volume_attachment, @@ -1206,7 +1206,7 @@ def delete_volume_attachment(self, volume_attachment, server, """ server_id = self._get_uri_attribute(volume_attachment, server, "server_id") - volume_attachment = resource2.Resource._get_id(volume_attachment) + volume_attachment = resource.Resource._get_id(volume_attachment) self._delete(_volume_attachment.VolumeAttachment, attachment_id=volume_attachment, @@ -1239,7 +1239,7 @@ def get_volume_attachment(self, volume_attachment, server, """ server_id = self._get_uri_attribute(volume_attachment, server, "server_id") - volume_attachment = resource2.Resource._get_id(volume_attachment) + volume_attachment = resource.Resource._get_id(volume_attachment) return self._get(_volume_attachment.VolumeAttachment, server_id=server_id, @@ -1256,7 +1256,7 @@ def volume_attachments(self, server): :rtype: :class:`~openstack.compute.v2.volume_attachment.VolumeAttachment` """ - server_id = resource2.Resource._get_id(server) + server_id = resource.Resource._get_id(server) return self._list(_volume_attachment.VolumeAttachment, paginated=False, server_id=server_id) diff --git a/openstack/compute/v2/availability_zone.py b/openstack/compute/v2/availability_zone.py index 56f49295c..5727d4825 100644 --- a/openstack/compute/v2/availability_zone.py +++ b/openstack/compute/v2/availability_zone.py @@ -11,10 +11,10 @@ # under the License. from openstack.compute import compute_service -from openstack import resource2 +from openstack import resource -class AvailabilityZone(resource2.Resource): +class AvailabilityZone(resource.Resource): resources_key = 'availabilityZoneInfo' base_path = '/os-availability-zone' @@ -25,11 +25,11 @@ class AvailabilityZone(resource2.Resource): # Properties #: name of availability zone - name = resource2.Body('zoneName') + name = resource.Body('zoneName') #: state of availability zone - state = resource2.Body('zoneState') + state = resource.Body('zoneState') #: hosts of availability zone - hosts = resource2.Body('hosts') + hosts = resource.Body('hosts') class AvailabilityZoneDetail(AvailabilityZone): diff --git a/openstack/compute/v2/extension.py b/openstack/compute/v2/extension.py index 6d3681034..c5a938bf5 100644 --- a/openstack/compute/v2/extension.py +++ b/openstack/compute/v2/extension.py @@ -11,10 +11,10 @@ # under the License. from openstack.compute import compute_service -from openstack import resource2 +from openstack import resource -class Extension(resource2.Resource): +class Extension(resource.Resource): resource_key = 'extension' resources_key = 'extensions' base_path = '/extensions' @@ -27,15 +27,15 @@ class Extension(resource2.Resource): # Properties #: A short name by which this extension is also known. - alias = resource2.Body('alias', alternate_id=True) + alias = resource.Body('alias', alternate_id=True) #: Text describing this extension's purpose. - description = resource2.Body('description') + description = resource.Body('description') #: Links pertaining to this extension. This is a list of dictionaries, #: each including keys ``href`` and ``rel``. - links = resource2.Body('links') + links = resource.Body('links') #: The name of the extension. - name = resource2.Body('name') + name = resource.Body('name') #: A URL pointing to the namespace for this extension. - namespace = resource2.Body('namespace') + namespace = resource.Body('namespace') #: Timestamp when this extension was last updated. - updated_at = resource2.Body('updated') + updated_at = resource.Body('updated') diff --git a/openstack/compute/v2/flavor.py b/openstack/compute/v2/flavor.py index 34df465ce..b21555135 100644 --- a/openstack/compute/v2/flavor.py +++ b/openstack/compute/v2/flavor.py @@ -11,10 +11,10 @@ # under the License. from openstack.compute import compute_service -from openstack import resource2 +from openstack import resource -class Flavor(resource2.Resource): +class Flavor(resource.Resource): resource_key = 'flavor' resources_key = 'flavors' base_path = '/flavors' @@ -26,33 +26,34 @@ class Flavor(resource2.Resource): allow_delete = True allow_list = True - _query_mapping = resource2.QueryParameters("sort_key", "sort_dir", - min_disk="minDisk", - min_ram="minRam") + _query_mapping = resource.QueryParameters( + "sort_key", "sort_dir", + min_disk="minDisk", + min_ram="minRam") # Properties #: Links pertaining to this flavor. This is a list of dictionaries, #: each including keys ``href`` and ``rel``. - links = resource2.Body('links') + links = resource.Body('links') #: The name of this flavor. - name = resource2.Body('name') + name = resource.Body('name') #: Size of the disk this flavor offers. *Type: int* - disk = resource2.Body('disk', type=int) + disk = resource.Body('disk', type=int) #: ``True`` if this is a publicly visible flavor. ``False`` if this is #: a private image. *Type: bool* - is_public = resource2.Body('os-flavor-access:is_public', type=bool) + is_public = resource.Body('os-flavor-access:is_public', type=bool) #: The amount of RAM (in MB) this flavor offers. *Type: int* - ram = resource2.Body('ram', type=int) + ram = resource.Body('ram', type=int) #: The number of virtual CPUs this flavor offers. *Type: int* - vcpus = resource2.Body('vcpus', type=int) + vcpus = resource.Body('vcpus', type=int) #: Size of the swap partitions. - swap = resource2.Body('swap') + swap = resource.Body('swap') #: Size of the ephemeral data disk attached to this server. *Type: int* - ephemeral = resource2.Body('OS-FLV-EXT-DATA:ephemeral', type=int) + ephemeral = resource.Body('OS-FLV-EXT-DATA:ephemeral', type=int) #: ``True`` if this flavor is disabled, ``False`` if not. *Type: bool* - is_disabled = resource2.Body('OS-FLV-DISABLED:disabled', type=bool) + is_disabled = resource.Body('OS-FLV-DISABLED:disabled', type=bool) #: The bandwidth scaling factor this flavor receives on the network. - rxtx_factor = resource2.Body('rxtx_factor', type=float) + rxtx_factor = resource.Body('rxtx_factor', type=float) class FlavorDetail(Flavor): diff --git a/openstack/compute/v2/hypervisor.py b/openstack/compute/v2/hypervisor.py index 25293f7e9..1c532b9ad 100644 --- a/openstack/compute/v2/hypervisor.py +++ b/openstack/compute/v2/hypervisor.py @@ -12,10 +12,10 @@ from openstack.compute import compute_service -from openstack import resource2 +from openstack import resource -class Hypervisor(resource2.Resource): +class Hypervisor(resource.Resource): resource_key = 'hypervisor' resources_key = 'hypervisors' base_path = '/os-hypervisors' @@ -28,40 +28,40 @@ class Hypervisor(resource2.Resource): # Properties #: Status of hypervisor - status = resource2.Body('status') + status = resource.Body('status') #: State of hypervisor - state = resource2.Body('state') + state = resource.Body('state') #: Name of hypervisor - name = resource2.Body('hypervisor_hostname') + name = resource.Body('hypervisor_hostname') #: Service details - service_details = resource2.Body('service') + service_details = resource.Body('service') #: Count of the VCPUs in use - vcpus_used = resource2.Body('vcpus_used') + vcpus_used = resource.Body('vcpus_used') #: Count of all VCPUs - vcpus = resource2.Body('vcpus') + vcpus = resource.Body('vcpus') #: Count of the running virtual machines - running_vms = resource2.Body('running_vms') + running_vms = resource.Body('running_vms') #: The type of hypervisor - hypervisor_type = resource2.Body('hypervisor_type') + hypervisor_type = resource.Body('hypervisor_type') #: Version of the hypervisor - hypervisor_version = resource2.Body('hypervisor_version') + hypervisor_version = resource.Body('hypervisor_version') #: The amount, in gigabytes, of local storage used - local_disk_used = resource2.Body('local_gb_used') + local_disk_used = resource.Body('local_gb_used') #: The amount, in gigabytes, of the local storage device - local_disk_size = resource2.Body('local_gb') + local_disk_size = resource.Body('local_gb') #: The amount, in gigabytes, of free space on the local storage device - local_disk_free = resource2.Body('free_disk_gb') + local_disk_free = resource.Body('free_disk_gb') #: The amount, in megabytes, of memory - memory_used = resource2.Body('memory_mb_used') + memory_used = resource.Body('memory_mb_used') #: The amount, in megabytes, of total memory - memory_size = resource2.Body('memory_mb') + memory_size = resource.Body('memory_mb') #: The amount, in megabytes, of available memory - memory_free = resource2.Body('free_ram_mb') + memory_free = resource.Body('free_ram_mb') #: Measurement of the hypervisor's current workload - current_workload = resource2.Body('current_workload') + current_workload = resource.Body('current_workload') #: Information about the hypervisor's CPU - cpu_info = resource2.Body('cpu_info') + cpu_info = resource.Body('cpu_info') #: IP address of the host - host_ip = resource2.Body('host_ip') + host_ip = resource.Body('host_ip') #: Disk space available to the scheduler - disk_available = resource2.Body("disk_available_least") + disk_available = resource.Body("disk_available_least") diff --git a/openstack/compute/v2/image.py b/openstack/compute/v2/image.py index a0cd539b8..b201ac922 100644 --- a/openstack/compute/v2/image.py +++ b/openstack/compute/v2/image.py @@ -12,10 +12,10 @@ from openstack.compute import compute_service from openstack.compute.v2 import metadata -from openstack import resource2 +from openstack import resource -class Image(resource2.Resource, metadata.MetadataMixin): +class Image(resource.Resource, metadata.MetadataMixin): resource_key = 'image' resources_key = 'images' base_path = '/images' @@ -26,35 +26,35 @@ class Image(resource2.Resource, metadata.MetadataMixin): allow_delete = True allow_list = True - _query_mapping = resource2.QueryParameters("server", "name", - "status", "type", - min_disk="minDisk", - min_ram="minRam", - changes_since="changes-since") + _query_mapping = resource.QueryParameters( + "server", "name", "status", "type", + min_disk="minDisk", + min_ram="minRam", + changes_since="changes-since") # Properties #: Links pertaining to this image. This is a list of dictionaries, #: each including keys ``href`` and ``rel``, and optionally ``type``. - links = resource2.Body('links') + links = resource.Body('links') #: The name of this image. - name = resource2.Body('name') + name = resource.Body('name') #: Timestamp when the image was created. - created_at = resource2.Body('created') + created_at = resource.Body('created') #: Metadata pertaining to this image. *Type: dict* - metadata = resource2.Body('metadata', type=dict) + metadata = resource.Body('metadata', type=dict) #: The mimimum disk size. *Type: int* - min_disk = resource2.Body('minDisk', type=int) + min_disk = resource.Body('minDisk', type=int) #: The minimum RAM size. *Type: int* - min_ram = resource2.Body('minRam', type=int) + min_ram = resource.Body('minRam', type=int) #: If this image is still building, its progress is represented here. #: Once an image is created, progres will be 100. *Type: int* - progress = resource2.Body('progress', type=int) + progress = resource.Body('progress', type=int) #: The status of this image. - status = resource2.Body('status') + status = resource.Body('status') #: Timestamp when the image was updated. - updated_at = resource2.Body('updated') + updated_at = resource.Body('updated') #: Size of the image in bytes. *Type: int* - size = resource2.Body('OS-EXT-IMG-SIZE:size', type=int) + size = resource.Body('OS-EXT-IMG-SIZE:size', type=int) class ImageDetail(Image): diff --git a/openstack/compute/v2/keypair.py b/openstack/compute/v2/keypair.py index e11266218..268de02cc 100644 --- a/openstack/compute/v2/keypair.py +++ b/openstack/compute/v2/keypair.py @@ -11,10 +11,10 @@ # under the License. from openstack.compute import compute_service -from openstack import resource2 +from openstack import resource -class Keypair(resource2.Resource): +class Keypair(resource.Resource): resource_key = 'keypair' resources_key = 'keypairs' base_path = '/os-keypairs' @@ -29,7 +29,7 @@ class Keypair(resource2.Resource): # Properties #: The short fingerprint associated with the ``public_key`` for #: this keypair. - fingerprint = resource2.Body('fingerprint') + fingerprint = resource.Body('fingerprint') # NOTE: There is in fact an 'id' field. However, it's not useful # because all operations use the 'name' as an identifier. # Additionally, the 'id' field only appears *after* creation, @@ -37,13 +37,13 @@ class Keypair(resource2.Resource): # and it just gets in the way. We need to cover this up by listing # name as alternate_id and listing id as coming from name. #: The id identifying the keypair - id = resource2.Body('name') + id = resource.Body('name') #: A name identifying the keypair - name = resource2.Body('name', alternate_id=True) + name = resource.Body('name', alternate_id=True) #: The private key for the keypair - private_key = resource2.Body('private_key') + private_key = resource.Body('private_key') #: The SSH public key that is paired with the server. - public_key = resource2.Body('public_key') + public_key = resource.Body('public_key') def _consume_attrs(self, mapping, attrs): # TODO(mordred) This should not be required. However, without doing diff --git a/openstack/compute/v2/limits.py b/openstack/compute/v2/limits.py index e66b3e81b..645d6104f 100644 --- a/openstack/compute/v2/limits.py +++ b/openstack/compute/v2/limits.py @@ -11,72 +11,72 @@ # under the License. from openstack.compute import compute_service -from openstack import resource2 +from openstack import resource -class AbsoluteLimits(resource2.Resource): +class AbsoluteLimits(resource.Resource): #: The number of key-value pairs that can be set as image metadata. - image_meta = resource2.Body("maxImageMeta") + image_meta = resource.Body("maxImageMeta") #: The maximum number of personality contents that can be supplied. - personality = resource2.Body("maxPersonality") + personality = resource.Body("maxPersonality") #: The maximum size, in bytes, of a personality. - personality_size = resource2.Body("maxPersonalitySize") + personality_size = resource.Body("maxPersonalitySize") #: The maximum amount of security group rules allowed. - security_group_rules = resource2.Body("maxSecurityGroupRules") + security_group_rules = resource.Body("maxSecurityGroupRules") #: The maximum amount of security groups allowed. - security_groups = resource2.Body("maxSecurityGroups") + security_groups = resource.Body("maxSecurityGroups") #: The amount of security groups currently in use. - security_groups_used = resource2.Body("totalSecurityGroupsUsed") + security_groups_used = resource.Body("totalSecurityGroupsUsed") #: The number of key-value pairs that can be set as sever metadata. - server_meta = resource2.Body("maxServerMeta") + server_meta = resource.Body("maxServerMeta") #: The maximum amount of cores. - total_cores = resource2.Body("maxTotalCores") + total_cores = resource.Body("maxTotalCores") #: The amount of cores currently in use. - total_cores_used = resource2.Body("totalCoresUsed") + total_cores_used = resource.Body("totalCoresUsed") #: The maximum amount of floating IPs. - floating_ips = resource2.Body("maxTotalFloatingIps") + floating_ips = resource.Body("maxTotalFloatingIps") #: The amount of floating IPs currently in use. - floating_ips_used = resource2.Body("totalFloatingIpsUsed") + floating_ips_used = resource.Body("totalFloatingIpsUsed") #: The maximum amount of instances. - instances = resource2.Body("maxTotalInstances") + instances = resource.Body("maxTotalInstances") #: The amount of instances currently in use. - instances_used = resource2.Body("totalInstancesUsed") + instances_used = resource.Body("totalInstancesUsed") #: The maximum amount of keypairs. - keypairs = resource2.Body("maxTotalKeypairs") + keypairs = resource.Body("maxTotalKeypairs") #: The maximum RAM size in megabytes. - total_ram = resource2.Body("maxTotalRAMSize") + total_ram = resource.Body("maxTotalRAMSize") #: The RAM size in megabytes currently in use. - total_ram_used = resource2.Body("totalRAMUsed") + total_ram_used = resource.Body("totalRAMUsed") #: The maximum amount of server groups. - server_groups = resource2.Body("maxServerGroups") + server_groups = resource.Body("maxServerGroups") #: The amount of server groups currently in use. - server_groups_used = resource2.Body("totalServerGroupsUsed") + server_groups_used = resource.Body("totalServerGroupsUsed") #: The maximum number of members in a server group. - server_group_members = resource2.Body("maxServerGroupMembers") + server_group_members = resource.Body("maxServerGroupMembers") -class RateLimit(resource2.Resource): +class RateLimit(resource.Resource): # TODO(mordred) Make a resource type for the contents of limit and add # it to list_type here. #: A list of the specific limits that apply to the ``regex`` and ``uri``. - limits = resource2.Body("limit", type=list) + limits = resource.Body("limit", type=list) #: A regex representing which routes this rate limit applies to. - regex = resource2.Body("regex") + regex = resource.Body("regex") #: A URI representing which routes this rate limit applies to. - uri = resource2.Body("uri") + uri = resource.Body("uri") -class Limits(resource2.Resource): +class Limits(resource.Resource): base_path = "/limits" resource_key = "limits" service = compute_service.ComputeService() allow_get = True - absolute = resource2.Body("absolute", type=AbsoluteLimits) - rate = resource2.Body("rate", type=list, list_type=RateLimit) + absolute = resource.Body("absolute", type=AbsoluteLimits) + rate = resource.Body("rate", type=list, list_type=RateLimit) def get(self, session, requires_id=False, error_message=None): """Get the Limits resource. diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 01ef2ba65..6cde73b00 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -12,11 +12,11 @@ from openstack.compute import compute_service from openstack.compute.v2 import metadata -from openstack import resource2 +from openstack import resource from openstack import utils -class Server(resource2.Resource, metadata.MetadataMixin): +class Server(resource.Resource, metadata.MetadataMixin): resource_key = 'server' resources_key = 'servers' base_path = '/servers' @@ -29,115 +29,116 @@ class Server(resource2.Resource, metadata.MetadataMixin): allow_delete = True allow_list = True - _query_mapping = resource2.QueryParameters("image", "flavor", "name", - "status", "host", "all_tenants", - "sort_key", "sort_dir", - "reservation_id", "tags", - "project_id", - tags_any="tags-any", - not_tags="not-tags", - not_tags_any="not-tags-any", - is_deleted="deleted", - ipv4_address="ip", - ipv6_address="ip6", - changes_since="changes-since") + _query_mapping = resource.QueryParameters( + "image", "flavor", "name", + "status", "host", "all_tenants", + "sort_key", "sort_dir", + "reservation_id", "tags", + "project_id", + tags_any="tags-any", + not_tags="not-tags", + not_tags_any="not-tags-any", + is_deleted="deleted", + ipv4_address="ip", + ipv6_address="ip6", + changes_since="changes-since") #: A list of dictionaries holding links relevant to this server. - links = resource2.Body('links') + links = resource.Body('links') - access_ipv4 = resource2.Body('accessIPv4') - access_ipv6 = resource2.Body('accessIPv6') + access_ipv4 = resource.Body('accessIPv4') + access_ipv6 = resource.Body('accessIPv6') #: A dictionary of addresses this server can be accessed through. #: The dictionary contains keys such as ``private`` and ``public``, #: each containing a list of dictionaries for addresses of that type. #: The addresses are contained in a dictionary with keys ``addr`` #: and ``version``, which is either 4 or 6 depending on the protocol #: of the IP address. *Type: dict* - addresses = resource2.Body('addresses', type=dict) + addresses = resource.Body('addresses', type=dict) #: Timestamp of when the server was created. - created_at = resource2.Body('created') + created_at = resource.Body('created') #: The flavor reference, as a ID or full URL, for the flavor to use for #: this server. - flavor_id = resource2.Body('flavorRef') + flavor_id = resource.Body('flavorRef') #: The flavor property as returned from server. - flavor = resource2.Body('flavor', type=dict) + flavor = resource.Body('flavor', type=dict) #: An ID representing the host of this server. - host_id = resource2.Body('hostId') + host_id = resource.Body('hostId') #: The image reference, as a ID or full URL, for the image to use for #: this server. - image_id = resource2.Body('imageRef') + image_id = resource.Body('imageRef') #: The image property as returned from server. - image = resource2.Body('image', type=dict) + image = resource.Body('image', type=dict) #: Metadata stored for this server. *Type: dict* - metadata = resource2.Body('metadata', type=dict) + metadata = resource.Body('metadata', type=dict) #: While the server is building, this value represents the percentage #: of completion. Once it is completed, it will be 100. *Type: int* - progress = resource2.Body('progress', type=int) + progress = resource.Body('progress', type=int) #: The ID of the project this server is associated with. - project_id = resource2.Body('tenant_id') + project_id = resource.Body('tenant_id') #: The state this server is in. Valid values include ``ACTIVE``, #: ``BUILDING``, ``DELETED``, ``ERROR``, ``HARD_REBOOT``, ``PASSWORD``, #: ``PAUSED``, ``REBOOT``, ``REBUILD``, ``RESCUED``, ``RESIZED``, #: ``REVERT_RESIZE``, ``SHUTOFF``, ``SOFT_DELETED``, ``STOPPED``, #: ``SUSPENDED``, ``UNKNOWN``, or ``VERIFY_RESIZE``. - status = resource2.Body('status') + status = resource.Body('status') #: Timestamp of when this server was last updated. - updated_at = resource2.Body('updated') + updated_at = resource.Body('updated') #: The ID of the owners of this server. - user_id = resource2.Body('user_id') + user_id = resource.Body('user_id') #: The name of an associated keypair - key_name = resource2.Body('key_name') + key_name = resource.Body('key_name') #: The disk configuration. Either AUTO or MANUAL. - disk_config = resource2.Body('OS-DCF:diskConfig') + disk_config = resource.Body('OS-DCF:diskConfig') #: Indicates whether a configuration drive enables metadata injection. #: Not all cloud providers enable this feature. - has_config_drive = resource2.Body('config_drive') + has_config_drive = resource.Body('config_drive') #: The name of the availability zone this server is a part of. - availability_zone = resource2.Body('OS-EXT-AZ:availability_zone') + availability_zone = resource.Body('OS-EXT-AZ:availability_zone') #: The power state of this server. - power_state = resource2.Body('OS-EXT-STS:power_state') + power_state = resource.Body('OS-EXT-STS:power_state') #: The task state of this server. - task_state = resource2.Body('OS-EXT-STS:task_state') + task_state = resource.Body('OS-EXT-STS:task_state') #: The VM state of this server. - vm_state = resource2.Body('OS-EXT-STS:vm_state') + vm_state = resource.Body('OS-EXT-STS:vm_state') #: A list of an attached volumes. Each item in the list contains at least #: an "id" key to identify the specific volumes. - attached_volumes = resource2.Body( + attached_volumes = resource.Body( 'os-extended-volumes:volumes_attached') #: The timestamp when the server was launched. - launched_at = resource2.Body('OS-SRV-USG:launched_at') + launched_at = resource.Body('OS-SRV-USG:launched_at') #: The timestamp when the server was terminated (if it has been). - terminated_at = resource2.Body('OS-SRV-USG:terminated_at') + terminated_at = resource.Body('OS-SRV-USG:terminated_at') #: A list of applicable security groups. Each group contains keys for #: description, name, id, and rules. - security_groups = resource2.Body('security_groups') + security_groups = resource.Body('security_groups') #: When a server is first created, it provides the administrator password. - admin_password = resource2.Body('adminPass') + admin_password = resource.Body('adminPass') #: The file path and contents, text only, to inject into the server at #: launch. The maximum size of the file path data is 255 bytes. #: The maximum limit is The number of allowed bytes in the decoded, #: rather than encoded, data. - personality = resource2.Body('personality') + personality = resource.Body('personality') #: Configuration information or scripts to use upon launch. #: Must be Base64 encoded. - user_data = resource2.Body('OS-EXT-SRV-ATTR:user_data') + user_data = resource.Body('OS-EXT-SRV-ATTR:user_data') #: Enables fine grained control of the block device mapping for an #: instance. This is typically used for booting servers from volumes. - block_device_mapping = resource2.Body('block_device_mapping_v2') + block_device_mapping = resource.Body('block_device_mapping_v2') #: The dictionary of data to send to the scheduler. - scheduler_hints = resource2.Body('OS-SCH-HNT:scheduler_hints', type=dict) + scheduler_hints = resource.Body('OS-SCH-HNT:scheduler_hints', type=dict) #: A networks object. Required parameter when there are multiple #: networks defined for the tenant. When you do not specify the #: networks parameter, the server attaches to the only network #: created for the current tenant. - networks = resource2.Body('networks') + networks = resource.Body('networks') #: The hypervisor host name. Appears in the response for administrative #: users only. - hypervisor_hostname = resource2.Body('OS-EXT-SRV-ATTR:hypervisor_hostname') + hypervisor_hostname = resource.Body('OS-EXT-SRV-ATTR:hypervisor_hostname') #: The instance name. The Compute API generates the instance name from the #: instance name template. Appears in the response for administrative users #: only. - instance_name = resource2.Body('OS-EXT-SRV-ATTR:instance_name') + instance_name = resource.Body('OS-EXT-SRV-ATTR:instance_name') def _prepare_request(self, requires_id=True, prepend_key=True): request = super(Server, self)._prepare_request(requires_id=requires_id, @@ -206,7 +207,7 @@ def rebuild(self, session, name, admin_password, 'preserve_ephemeral': preserve_ephemeral } if image is not None: - action['imageRef'] = resource2.Resource._get_id(image) + action['imageRef'] = resource.Resource._get_id(image) if access_ipv4 is not None: action['accessIPv4'] = access_ipv4 if access_ipv6 is not None: diff --git a/openstack/compute/v2/server_group.py b/openstack/compute/v2/server_group.py index f57c8e58a..1807069c5 100644 --- a/openstack/compute/v2/server_group.py +++ b/openstack/compute/v2/server_group.py @@ -11,16 +11,16 @@ # under the License. from openstack.compute import compute_service -from openstack import resource2 +from openstack import resource -class ServerGroup(resource2.Resource): +class ServerGroup(resource.Resource): resource_key = 'server_group' resources_key = 'server_groups' base_path = '/os-server-groups' service = compute_service.ComputeService() - _query_mapping = resource2.QueryParameters("all_projects") + _query_mapping = resource.QueryParameters("all_projects") # capabilities allow_create = True @@ -30,10 +30,10 @@ class ServerGroup(resource2.Resource): # Properties #: A name identifying the server group - name = resource2.Body('name') + name = resource.Body('name') #: The list of policies supported by the server group - policies = resource2.Body('policies') + policies = resource.Body('policies') #: The list of members in the server group - member_ids = resource2.Body('members') + member_ids = resource.Body('members') #: The metadata associated with the server group - metadata = resource2.Body('metadata') + metadata = resource.Body('metadata') diff --git a/openstack/compute/v2/server_interface.py b/openstack/compute/v2/server_interface.py index 951f6e422..98659d9a1 100644 --- a/openstack/compute/v2/server_interface.py +++ b/openstack/compute/v2/server_interface.py @@ -11,10 +11,10 @@ # under the License. from openstack.compute import compute_service -from openstack import resource2 +from openstack import resource -class ServerInterface(resource2.Resource): +class ServerInterface(resource.Resource): resource_key = 'interfaceAttachment' resources_key = 'interfaceAttachments' base_path = '/servers/%(server_id)s/os-interface' @@ -28,14 +28,14 @@ class ServerInterface(resource2.Resource): allow_list = True #: Fixed IP addresses with subnet IDs. - fixed_ips = resource2.Body('fixed_ips') + fixed_ips = resource.Body('fixed_ips') #: The MAC address. - mac_addr = resource2.Body('mac_addr') + mac_addr = resource.Body('mac_addr') #: The network ID. - net_id = resource2.Body('net_id') + net_id = resource.Body('net_id') #: The ID of the port for which you want to create an interface. - port_id = resource2.Body('port_id', alternate_id=True) + port_id = resource.Body('port_id', alternate_id=True) #: The port state. - port_state = resource2.Body('port_state') + port_state = resource.Body('port_state') #: The ID for the server. - server_id = resource2.URI('server_id') + server_id = resource.URI('server_id') diff --git a/openstack/compute/v2/server_ip.py b/openstack/compute/v2/server_ip.py index 2f4ed9542..4c797a68a 100644 --- a/openstack/compute/v2/server_ip.py +++ b/openstack/compute/v2/server_ip.py @@ -11,11 +11,11 @@ # under the License. from openstack.compute import compute_service -from openstack import resource2 +from openstack import resource from openstack import utils -class ServerIP(resource2.Resource): +class ServerIP(resource.Resource): resources_key = 'addresses' base_path = '/servers/%(server_id)s/ips' service = compute_service.ComputeService() @@ -25,13 +25,13 @@ class ServerIP(resource2.Resource): # Properties #: The IP address. The format of the address depends on :attr:`version` - address = resource2.Body('addr') + address = resource.Body('addr') #: The network label, such as public or private. - network_label = resource2.URI('network_label') + network_label = resource.URI('network_label') #: The ID for the server. - server_id = resource2.URI('server_id') + server_id = resource.URI('server_id') # Version of the IP protocol. Currently either 4 or 6. - version = resource2.Body('version') + version = resource.Body('version') @classmethod def list(cls, session, paginated=False, server_id=None, diff --git a/openstack/compute/v2/service.py b/openstack/compute/v2/service.py index ece512703..4e9b0873e 100644 --- a/openstack/compute/v2/service.py +++ b/openstack/compute/v2/service.py @@ -11,11 +11,11 @@ # under the License. from openstack.compute import compute_service -from openstack import resource2 +from openstack import resource from openstack import utils -class Service(resource2.Resource): +class Service(resource.Resource): resource_key = 'service' resources_key = 'services' base_path = '/os-services' @@ -28,19 +28,19 @@ class Service(resource2.Resource): # Properties #: Status of service - status = resource2.Body('status') + status = resource.Body('status') #: State of service - state = resource2.Body('state') + state = resource.Body('state') #: Name of service - binary = resource2.Body('binary') + binary = resource.Body('binary') #: Id of service - id = resource2.Body('id') + id = resource.Body('id') #: Disabled reason of service - disables_reason = resource2.Body('disabled_reason') + disables_reason = resource.Body('disabled_reason') #: Host where service runs - host = resource2.Body('host') + host = resource.Body('host') #: The availability zone of service - zone = resource2.Body("zone") + zone = resource.Body("zone") def _action(self, session, action, body): url = utils.urljoin(Service.base_path, action) diff --git a/openstack/compute/v2/volume_attachment.py b/openstack/compute/v2/volume_attachment.py index 44ae47f61..aa3e20674 100644 --- a/openstack/compute/v2/volume_attachment.py +++ b/openstack/compute/v2/volume_attachment.py @@ -11,10 +11,10 @@ # under the License. from openstack.compute import compute_service -from openstack import resource2 +from openstack import resource -class VolumeAttachment(resource2.Resource): +class VolumeAttachment(resource.Resource): resource_key = 'volumeAttachment' resources_key = 'volumeAttachments' base_path = '/servers/%(server_id)s/os-volume_attachments' @@ -27,15 +27,15 @@ class VolumeAttachment(resource2.Resource): allow_delete = True allow_list = True - _query_mapping = resource2.QueryParameters("limit", "offset") + _query_mapping = resource.QueryParameters("limit", "offset") #: Name of the device such as, /dev/vdb. - device = resource2.Body('device') + device = resource.Body('device') #: The ID of the attachment. - id = resource2.Body('id') + id = resource.Body('id') #: The ID for the server. - server_id = resource2.URI('server_id') + server_id = resource.URI('server_id') #: The ID of the attached volume. - volume_id = resource2.Body('volumeId') + volume_id = resource.Body('volumeId') #: The ID of the attachment you want to delete or update. - attachment_id = resource2.Body('attachment_id', alternate_id=True) + attachment_id = resource.Body('attachment_id', alternate_id=True) diff --git a/openstack/compute/version.py b/openstack/compute/version.py index 186f35e18..d1230260a 100644 --- a/openstack/compute/version.py +++ b/openstack/compute/version.py @@ -11,7 +11,7 @@ # under the License. from openstack.compute import compute_service -from openstack import resource2 as resource +from openstack import resource class Version(resource.Resource): diff --git a/openstack/connection.py b/openstack/connection.py index dd968ae1a..aa5c56863 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -250,10 +250,10 @@ def _get_config_from_profile(self, profile, authenticator, **kwargs): def add_service(self, service): """Add a service to the Connection. - Attaches an instance of the :class:`~openstack.proxy2.BaseProxy` + Attaches an instance of the :class:`~openstack.proxy.BaseProxy` class contained in :class:`~openstack.service_description.ServiceDescription`. - The :class:`~openstack.proxy2.BaseProxy` will be attached to the + The :class:`~openstack.proxy.BaseProxy` will be attached to the `Connection` by its ``service_type`` and by any ``aliases`` that may be specified. diff --git a/openstack/database/v1/_proxy.py b/openstack/database/v1/_proxy.py index 89a1702cd..211e286f9 100644 --- a/openstack/database/v1/_proxy.py +++ b/openstack/database/v1/_proxy.py @@ -14,10 +14,10 @@ from openstack.database.v1 import flavor as _flavor from openstack.database.v1 import instance as _instance from openstack.database.v1 import user as _user -from openstack import proxy2 +from openstack import proxy -class Proxy(proxy2.BaseProxy): +class Proxy(proxy.BaseProxy): def create_database(self, instance, **attrs): """Create a new database from attributes diff --git a/openstack/database/v1/database.py b/openstack/database/v1/database.py index 3270ea98a..be46be803 100644 --- a/openstack/database/v1/database.py +++ b/openstack/database/v1/database.py @@ -11,7 +11,7 @@ # under the License. from openstack.database import database_service -from openstack import resource2 as resource +from openstack import resource class Database(resource.Resource): diff --git a/openstack/database/v1/flavor.py b/openstack/database/v1/flavor.py index 38a06098e..ac4bf7b9d 100644 --- a/openstack/database/v1/flavor.py +++ b/openstack/database/v1/flavor.py @@ -11,7 +11,7 @@ # under the License. from openstack.database import database_service -from openstack import resource2 as resource +from openstack import resource class Flavor(resource.Resource): diff --git a/openstack/database/v1/instance.py b/openstack/database/v1/instance.py index 30d03cc9a..76f67b035 100644 --- a/openstack/database/v1/instance.py +++ b/openstack/database/v1/instance.py @@ -11,7 +11,7 @@ # under the License. from openstack.database import database_service -from openstack import resource2 as resource +from openstack import resource from openstack import utils diff --git a/openstack/database/v1/user.py b/openstack/database/v1/user.py index a2f4116cb..2055544b9 100644 --- a/openstack/database/v1/user.py +++ b/openstack/database/v1/user.py @@ -11,7 +11,7 @@ # under the License. from openstack.database import database_service -from openstack import resource2 as resource +from openstack import resource from openstack import utils diff --git a/openstack/identity/v2/_proxy.py b/openstack/identity/v2/_proxy.py index e371da5e8..8b6b918e8 100644 --- a/openstack/identity/v2/_proxy.py +++ b/openstack/identity/v2/_proxy.py @@ -14,7 +14,7 @@ from openstack.identity.v2 import role as _role from openstack.identity.v2 import tenant as _tenant from openstack.identity.v2 import user as _user -from openstack import proxy2 as proxy +from openstack import proxy class Proxy(proxy.BaseProxy): diff --git a/openstack/identity/v2/extension.py b/openstack/identity/v2/extension.py index e8ba4fcfd..0012831e6 100644 --- a/openstack/identity/v2/extension.py +++ b/openstack/identity/v2/extension.py @@ -11,7 +11,7 @@ # under the License. from openstack.identity import identity_service -from openstack import resource2 as resource +from openstack import resource class Extension(resource.Resource): diff --git a/openstack/identity/v2/role.py b/openstack/identity/v2/role.py index b3b0a0363..be07ff7fb 100644 --- a/openstack/identity/v2/role.py +++ b/openstack/identity/v2/role.py @@ -12,7 +12,7 @@ from openstack import format from openstack.identity import identity_service -from openstack import resource2 as resource +from openstack import resource class Role(resource.Resource): diff --git a/openstack/identity/v2/tenant.py b/openstack/identity/v2/tenant.py index f6933d5a4..ccac50aee 100644 --- a/openstack/identity/v2/tenant.py +++ b/openstack/identity/v2/tenant.py @@ -11,7 +11,7 @@ # under the License. from openstack.identity import identity_service -from openstack import resource2 as resource +from openstack import resource class Tenant(resource.Resource): diff --git a/openstack/identity/v2/user.py b/openstack/identity/v2/user.py index 0e5a5846b..3f905712c 100644 --- a/openstack/identity/v2/user.py +++ b/openstack/identity/v2/user.py @@ -11,7 +11,7 @@ # under the License. from openstack.identity import identity_service -from openstack import resource2 as resource +from openstack import resource class User(resource.Resource): diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 30aad2656..7cb720ee9 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -31,7 +31,7 @@ from openstack.identity.v3 import service as _service from openstack.identity.v3 import trust as _trust from openstack.identity.v3 import user as _user -from openstack import proxy2 as proxy +from openstack import proxy class Proxy(proxy.BaseProxy): diff --git a/openstack/identity/v3/credential.py b/openstack/identity/v3/credential.py index 33bc40c49..781397964 100644 --- a/openstack/identity/v3/credential.py +++ b/openstack/identity/v3/credential.py @@ -11,7 +11,7 @@ # under the License. from openstack.identity import identity_service -from openstack import resource2 as resource +from openstack import resource class Credential(resource.Resource): diff --git a/openstack/identity/v3/domain.py b/openstack/identity/v3/domain.py index 45b06fcbc..7ce373499 100644 --- a/openstack/identity/v3/domain.py +++ b/openstack/identity/v3/domain.py @@ -11,7 +11,7 @@ # under the License. from openstack.identity import identity_service -from openstack import resource2 as resource +from openstack import resource from openstack import utils diff --git a/openstack/identity/v3/endpoint.py b/openstack/identity/v3/endpoint.py index 59899e25e..e68517a04 100644 --- a/openstack/identity/v3/endpoint.py +++ b/openstack/identity/v3/endpoint.py @@ -11,7 +11,7 @@ # under the License. from openstack.identity import identity_service -from openstack import resource2 as resource +from openstack import resource class Endpoint(resource.Resource): diff --git a/openstack/identity/v3/group.py b/openstack/identity/v3/group.py index 2793f94da..d0d132ec0 100644 --- a/openstack/identity/v3/group.py +++ b/openstack/identity/v3/group.py @@ -11,7 +11,7 @@ # under the License. from openstack.identity import identity_service -from openstack import resource2 as resource +from openstack import resource class Group(resource.Resource): diff --git a/openstack/identity/v3/policy.py b/openstack/identity/v3/policy.py index 3e5643c70..6d240d8a2 100644 --- a/openstack/identity/v3/policy.py +++ b/openstack/identity/v3/policy.py @@ -11,7 +11,7 @@ # under the License. from openstack.identity import identity_service -from openstack import resource2 as resource +from openstack import resource class Policy(resource.Resource): diff --git a/openstack/identity/v3/project.py b/openstack/identity/v3/project.py index 159192668..7b126b3a0 100644 --- a/openstack/identity/v3/project.py +++ b/openstack/identity/v3/project.py @@ -11,7 +11,7 @@ # under the License. from openstack.identity import identity_service -from openstack import resource2 as resource +from openstack import resource from openstack import utils diff --git a/openstack/identity/v3/region.py b/openstack/identity/v3/region.py index 9169d9436..d774aaa17 100644 --- a/openstack/identity/v3/region.py +++ b/openstack/identity/v3/region.py @@ -11,7 +11,7 @@ # under the License. from openstack.identity import identity_service -from openstack import resource2 as resource +from openstack import resource class Region(resource.Resource): diff --git a/openstack/identity/v3/role.py b/openstack/identity/v3/role.py index 58c8b0fbc..aad9bdc17 100644 --- a/openstack/identity/v3/role.py +++ b/openstack/identity/v3/role.py @@ -11,7 +11,7 @@ # under the License. from openstack.identity import identity_service -from openstack import resource2 as resource +from openstack import resource class Role(resource.Resource): diff --git a/openstack/identity/v3/role_assignment.py b/openstack/identity/v3/role_assignment.py index 459c275b7..412535f00 100644 --- a/openstack/identity/v3/role_assignment.py +++ b/openstack/identity/v3/role_assignment.py @@ -11,7 +11,7 @@ # under the License. from openstack.identity import identity_service -from openstack import resource2 as resource +from openstack import resource class RoleAssignment(resource.Resource): diff --git a/openstack/identity/v3/role_domain_group_assignment.py b/openstack/identity/v3/role_domain_group_assignment.py index d7fadc3cc..27b6963cb 100644 --- a/openstack/identity/v3/role_domain_group_assignment.py +++ b/openstack/identity/v3/role_domain_group_assignment.py @@ -11,7 +11,7 @@ # under the License. from openstack.identity import identity_service -from openstack import resource2 as resource +from openstack import resource class RoleDomainGroupAssignment(resource.Resource): diff --git a/openstack/identity/v3/role_domain_user_assignment.py b/openstack/identity/v3/role_domain_user_assignment.py index b4db0f522..085a77e0f 100644 --- a/openstack/identity/v3/role_domain_user_assignment.py +++ b/openstack/identity/v3/role_domain_user_assignment.py @@ -11,7 +11,7 @@ # under the License. from openstack.identity import identity_service -from openstack import resource2 as resource +from openstack import resource class RoleDomainUserAssignment(resource.Resource): diff --git a/openstack/identity/v3/role_project_group_assignment.py b/openstack/identity/v3/role_project_group_assignment.py index 225410b5d..cb48025c1 100644 --- a/openstack/identity/v3/role_project_group_assignment.py +++ b/openstack/identity/v3/role_project_group_assignment.py @@ -11,7 +11,7 @@ # under the License. from openstack.identity import identity_service -from openstack import resource2 as resource +from openstack import resource class RoleProjectGroupAssignment(resource.Resource): diff --git a/openstack/identity/v3/role_project_user_assignment.py b/openstack/identity/v3/role_project_user_assignment.py index b1989794a..a7e887099 100644 --- a/openstack/identity/v3/role_project_user_assignment.py +++ b/openstack/identity/v3/role_project_user_assignment.py @@ -11,7 +11,7 @@ # under the License. from openstack.identity import identity_service -from openstack import resource2 as resource +from openstack import resource class RoleProjectUserAssignment(resource.Resource): diff --git a/openstack/identity/v3/service.py b/openstack/identity/v3/service.py index 457d2995f..51bb7ced4 100644 --- a/openstack/identity/v3/service.py +++ b/openstack/identity/v3/service.py @@ -11,7 +11,7 @@ # under the License. from openstack.identity import identity_service -from openstack import resource2 as resource +from openstack import resource class Service(resource.Resource): diff --git a/openstack/identity/v3/trust.py b/openstack/identity/v3/trust.py index 5141e7514..1b334bb02 100644 --- a/openstack/identity/v3/trust.py +++ b/openstack/identity/v3/trust.py @@ -12,7 +12,7 @@ from openstack.identity import identity_service -from openstack import resource2 as resource +from openstack import resource class Trust(resource.Resource): diff --git a/openstack/identity/v3/user.py b/openstack/identity/v3/user.py index 055a50ef8..453541763 100644 --- a/openstack/identity/v3/user.py +++ b/openstack/identity/v3/user.py @@ -11,7 +11,7 @@ # under the License. from openstack.identity import identity_service -from openstack import resource2 as resource +from openstack import resource class User(resource.Resource): diff --git a/openstack/identity/version.py b/openstack/identity/version.py index 1f8442a68..30992f109 100644 --- a/openstack/identity/version.py +++ b/openstack/identity/version.py @@ -11,7 +11,7 @@ # under the License. from openstack.identity import identity_service -from openstack import resource2 as resource +from openstack import resource class Version(resource.Resource): diff --git a/openstack/image/v1/_proxy.py b/openstack/image/v1/_proxy.py index e8d2ff534..aa4ba6fbe 100644 --- a/openstack/image/v1/_proxy.py +++ b/openstack/image/v1/_proxy.py @@ -11,7 +11,7 @@ # under the License. from openstack.image.v1 import image as _image -from openstack import proxy2 as proxy +from openstack import proxy class Proxy(proxy.BaseProxy): diff --git a/openstack/image/v1/image.py b/openstack/image/v1/image.py index 882f61334..b5b10fa1d 100644 --- a/openstack/image/v1/image.py +++ b/openstack/image/v1/image.py @@ -11,7 +11,7 @@ # under the License. from openstack.image import image_service -from openstack import resource2 as resource +from openstack import resource class Image(resource.Resource): diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 55d7db916..b943bc9a7 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -13,11 +13,11 @@ from openstack import exceptions from openstack.image.v2 import image as _image from openstack.image.v2 import member as _member -from openstack import proxy2 -from openstack import resource2 +from openstack import proxy +from openstack import resource -class Proxy(proxy2.BaseProxy): +class Proxy(proxy.BaseProxy): def upload_image(self, container_format=None, disk_format=None, data=None, **attrs): @@ -221,7 +221,7 @@ def add_member(self, image, **attrs): :returns: The results of member creation :rtype: :class:`~openstack.image.v2.member.Member` """ - image_id = resource2.Resource._get_id(image) + image_id = resource.Resource._get_id(image) return self._create(_member.Member, image_id=image_id, **attrs) def remove_member(self, member, image, ignore_missing=True): @@ -237,8 +237,8 @@ def remove_member(self, member, image, ignore_missing=True): :returns: ``None`` """ - image_id = resource2.Resource._get_id(image) - member_id = resource2.Resource._get_id(member) + image_id = resource.Resource._get_id(image) + member_id = resource.Resource._get_id(member) self._delete(_member.Member, member_id=member_id, image_id=image_id, ignore_missing=ignore_missing) @@ -256,7 +256,7 @@ def find_member(self, name_or_id, image, ignore_missing=True): attempting to find a nonexistent resource. :returns: One :class:`~openstack.image.v2.member.Member` or None """ - image_id = resource2.Resource._get_id(image) + image_id = resource.Resource._get_id(image) return self._find(_member.Member, name_or_id, image_id=image_id, ignore_missing=ignore_missing) @@ -272,8 +272,8 @@ def get_member(self, member, image): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - member_id = resource2.Resource._get_id(member) - image_id = resource2.Resource._get_id(image) + member_id = resource.Resource._get_id(member) + image_id = resource.Resource._get_id(image) return self._get(_member.Member, member_id=member_id, image_id=image_id) @@ -287,7 +287,7 @@ def members(self, image): :returns: A generator of member objects :rtype: :class:`~openstack.image.v2.member.Member` """ - image_id = resource2.Resource._get_id(image) + image_id = resource.Resource._get_id(image) return self._list(_member.Member, paginated=False, image_id=image_id) @@ -305,7 +305,7 @@ def update_member(self, member, image, **attrs): :returns: The updated member :rtype: :class:`~openstack.image.v2.member.Member` """ - member_id = resource2.Resource._get_id(member) - image_id = resource2.Resource._get_id(image) + member_id = resource.Resource._get_id(member) + image_id = resource.Resource._get_id(image) return self._update(_member.Member, member_id=member_id, image_id=image_id, **attrs) diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index 4b397a443..91f83b3a3 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -17,13 +17,13 @@ from openstack import _log from openstack import exceptions from openstack.image import image_service -from openstack import resource2 +from openstack import resource from openstack import utils _logger = _log.setup_logging('openstack') -class Image(resource2.Resource): +class Image(resource.Resource): resources_key = 'images' base_path = '/images' service = image_service.ImageService() @@ -36,12 +36,13 @@ class Image(resource2.Resource): allow_list = True update_method = 'PATCH' - _query_mapping = resource2.QueryParameters("name", "visibility", - "member_status", "owner", - "status", "size_min", - "size_max", "sort_key", - "sort_dir", "sort", "tag", - "created_at", "updated_at") + _query_mapping = resource.QueryParameters( + "name", "visibility", + "member_status", "owner", + "status", "size_min", + "size_max", "sort_key", + "sort_dir", "sort", "tag", + "created_at", "updated_at") # NOTE: Do not add "self" support here. If you've used Python before, # you know that self, while not being a reserved word, has special @@ -57,159 +58,159 @@ class Image(resource2.Resource): # Properties #: Hash of the image data used. The Image service uses this value #: for verification. - checksum = resource2.Body('checksum') + checksum = resource.Body('checksum') #: The container format refers to whether the VM image is in a file #: format that also contains metadata about the actual VM. #: Container formats include OVF and Amazon AMI. In addition, #: a VM image might not have a container format - instead, #: the image is just a blob of unstructured data. - container_format = resource2.Body('container_format') + container_format = resource.Body('container_format') #: The date and time when the image was created. - created_at = resource2.Body('created_at') + created_at = resource.Body('created_at') #: Valid values are: aki, ari, ami, raw, iso, vhd, vdi, qcow2, or vmdk. #: The disk format of a VM image is the format of the underlying #: disk image. Virtual appliance vendors have different formats #: for laying out the information contained in a VM disk image. - disk_format = resource2.Body('disk_format') + disk_format = resource.Body('disk_format') #: Defines whether the image can be deleted. #: *Type: bool* - is_protected = resource2.Body('protected', type=bool) + is_protected = resource.Body('protected', type=bool) #: The minimum disk size in GB that is required to boot the image. - min_disk = resource2.Body('min_disk') + min_disk = resource.Body('min_disk') #: The minimum amount of RAM in MB that is required to boot the image. - min_ram = resource2.Body('min_ram') + min_ram = resource.Body('min_ram') #: The name of the image. - name = resource2.Body('name') + name = resource.Body('name') #: The ID of the owner, or project, of the image. - owner_id = resource2.Body('owner') + owner_id = resource.Body('owner') #: Properties, if any, that are associated with the image. - properties = resource2.Body('properties', type=dict) + properties = resource.Body('properties', type=dict) #: The size of the image data, in bytes. - size = resource2.Body('size', type=int) + size = resource.Body('size', type=int) #: When present, Glance will attempt to store the disk image data in the #: backing store indicated by the value of the header. When not present, #: Glance will store the disk image data in the backing store that is #: marked default. Valid values are: file, s3, rbd, swift, cinder, #: gridfs, sheepdog, or vsphere. - store = resource2.Body('store') + store = resource.Body('store') #: The image status. - status = resource2.Body('status') + status = resource.Body('status') #: Tags, if any, that are associated with the image. - tags = resource2.Body('tags') + tags = resource.Body('tags') #: The date and time when the image was updated. - updated_at = resource2.Body('updated_at') + updated_at = resource.Body('updated_at') #: The virtual size of the image. - virtual_size = resource2.Body('virtual_size') + virtual_size = resource.Body('virtual_size') #: The image visibility. - visibility = resource2.Body('visibility') + visibility = resource.Body('visibility') #: The URL for the virtual machine image file. - file = resource2.Body('file') + file = resource.Body('file') #: A list of URLs to access the image file in external store. #: This list appears if the show_multiple_locations option is set #: to true in the Image service's configuration file. - locations = resource2.Body('locations') + locations = resource.Body('locations') #: The URL to access the image file kept in external store. It appears #: when you set the show_image_direct_url option to true in the #: Image service's configuration file. - direct_url = resource2.Body('direct_url') + direct_url = resource.Body('direct_url') #: An image property. - path = resource2.Body('path') + path = resource.Body('path') #: Value of image property used in add or replace operations expressed #: in JSON notation. For example, you must enclose strings in quotation #: marks, and you do not enclose numeric values in quotation marks. - value = resource2.Body('value') + value = resource.Body('value') #: The URL to access the image file kept in external store. - url = resource2.Body('url') + url = resource.Body('url') #: The location metadata. - metadata = resource2.Body('metadata', type=dict) + metadata = resource.Body('metadata', type=dict) # Additional Image Properties # https://docs.openstack.org/glance/latest/user/common-image-properties.html # http://docs.openstack.org/cli-reference/glance-property-keys.html #: The CPU architecture that must be supported by the hypervisor. - architecture = resource2.Body("architecture") + architecture = resource.Body("architecture") #: The hypervisor type. Note that qemu is used for both QEMU and #: KVM hypervisor types. - hypervisor_type = resource2.Body("hypervisor-type") + hypervisor_type = resource.Body("hypervisor-type") #: Optional property allows created servers to have a different bandwidth #: cap than that defined in the network they are attached to. - instance_type_rxtx_factor = resource2.Body("instance_type_rxtx_factor", - type=float) + instance_type_rxtx_factor = resource.Body( + "instance_type_rxtx_factor", type=float) # For snapshot images, this is the UUID of the server used to #: create this image. - instance_uuid = resource2.Body('instance_uuid') + instance_uuid = resource.Body('instance_uuid') #: Specifies whether the image needs a config drive. #: `mandatory` or `optional` (default if property is not used). - needs_config_drive = resource2.Body('img_config_drive') + needs_config_drive = resource.Body('img_config_drive') #: The ID of an image stored in the Image service that should be used #: as the kernel when booting an AMI-style image. - kernel_id = resource2.Body('kernel_id') + kernel_id = resource.Body('kernel_id') #: The common name of the operating system distribution in lowercase - os_distro = resource2.Body('os_distro') + os_distro = resource.Body('os_distro') #: The operating system version as specified by the distributor. - os_version = resource2.Body('os_version') + os_version = resource.Body('os_version') #: Secure Boot is a security standard. When the instance starts, #: Secure Boot first examines software such as firmware and OS by #: their signature and only allows them to run if the signatures are valid. - needs_secure_boot = resource2.Body('os_secure_boot') + needs_secure_boot = resource.Body('os_secure_boot') #: The ID of image stored in the Image service that should be used as #: the ramdisk when booting an AMI-style image. - ramdisk_id = resource2.Body('ramdisk_id') + ramdisk_id = resource.Body('ramdisk_id') #: The virtual machine mode. This represents the host/guest ABI #: (application binary interface) used for the virtual machine. - vm_mode = resource2.Body('vm_mode') + vm_mode = resource.Body('vm_mode') #: The preferred number of sockets to expose to the guest. - hw_cpu_sockets = resource2.Body('hw_cpu_sockets', type=int) + hw_cpu_sockets = resource.Body('hw_cpu_sockets', type=int) #: The preferred number of cores to expose to the guest. - hw_cpu_cores = resource2.Body('hw_cpu_cores', type=int) + hw_cpu_cores = resource.Body('hw_cpu_cores', type=int) #: The preferred number of threads to expose to the guest. - hw_cpu_threads = resource2.Body('hw_cpu_threads', type=int) + hw_cpu_threads = resource.Body('hw_cpu_threads', type=int) #: Specifies the type of disk controller to attach disk devices to. #: One of scsi, virtio, uml, xen, ide, or usb. - hw_disk_bus = resource2.Body('hw_disk_bus') + hw_disk_bus = resource.Body('hw_disk_bus') #: Adds a random-number generator device to the image's instances. - hw_rng_model = resource2.Body('hw_rng_model') + hw_rng_model = resource.Body('hw_rng_model') #: For libvirt: Enables booting an ARM system using the specified #: machine type. #: For Hyper-V: Specifies whether the Hyper-V instance will be a #: generation 1 or generation 2 VM. - hw_machine_type = resource2.Body('hw_machine_type') + hw_machine_type = resource.Body('hw_machine_type') #: Enables the use of VirtIO SCSI (virtio-scsi) to provide block device #: access for compute instances; by default, instances use VirtIO Block #: (virtio-blk). - hw_scsi_model = resource2.Body('hw_scsi_model') + hw_scsi_model = resource.Body('hw_scsi_model') #: Specifies the count of serial ports that should be provided. - hw_serial_port_count = resource2.Body('hw_serial_port_count', type=int) + hw_serial_port_count = resource.Body('hw_serial_port_count', type=int) #: The video image driver used. - hw_video_model = resource2.Body('hw_video_model') + hw_video_model = resource.Body('hw_video_model') #: Maximum RAM for the video image. - hw_video_ram = resource2.Body('hw_video_ram', type=int) + hw_video_ram = resource.Body('hw_video_ram', type=int) #: Enables a virtual hardware watchdog device that carries out the #: specified action if the server hangs. - hw_watchdog_action = resource2.Body('hw_watchdog_action') + hw_watchdog_action = resource.Body('hw_watchdog_action') #: The kernel command line to be used by the libvirt driver, instead #: of the default. - os_command_line = resource2.Body('os_command_line') + os_command_line = resource.Body('os_command_line') #: Specifies the model of virtual network interface device to use. - hw_vif_model = resource2.Body('hw_vif_model') + hw_vif_model = resource.Body('hw_vif_model') #: If true, this enables the virtio-net multiqueue feature. #: In this case, the driver sets the number of queues equal to the #: number of guest vCPUs. This makes the network performance scale #: across a number of vCPUs. - is_hw_vif_multiqueue_enabled = resource2.Body('hw_vif_multiqueue_enabled', - type=bool) + is_hw_vif_multiqueue_enabled = resource.Body( + 'hw_vif_multiqueue_enabled', type=bool) #: If true, enables the BIOS bootmenu. - is_hw_boot_menu_enabled = resource2.Body('hw_boot_menu', type=bool) + is_hw_boot_menu_enabled = resource.Body('hw_boot_menu', type=bool) #: The virtual SCSI or IDE controller used by the hypervisor. - vmware_adaptertype = resource2.Body('vmware_adaptertype') + vmware_adaptertype = resource.Body('vmware_adaptertype') #: A VMware GuestID which describes the operating system installed #: in the image. - vmware_ostype = resource2.Body('vmware_ostype') + vmware_ostype = resource.Body('vmware_ostype') #: If true, the root partition on the disk is automatically resized #: before the instance boots. - has_auto_disk_config = resource2.Body('auto_disk_config', type=bool) + has_auto_disk_config = resource.Body('auto_disk_config', type=bool) #: The operating system installed on the image. - os_type = resource2.Body('os_type') + os_type = resource.Body('os_type') def _action(self, session, action): """Call an action on an image ID.""" diff --git a/openstack/image/v2/member.py b/openstack/image/v2/member.py index 5548efd7c..4157646e0 100644 --- a/openstack/image/v2/member.py +++ b/openstack/image/v2/member.py @@ -11,10 +11,10 @@ # under the License. from openstack.image import image_service -from openstack import resource2 +from openstack import resource -class Member(resource2.Resource): +class Member(resource.Resource): resources_key = 'members' base_path = '/images/%(image_id)s/members' service = image_service.ImageService() @@ -32,14 +32,14 @@ class Member(resource2.Resource): #: The ID of the image member. An image member is a tenant #: with whom the image is shared. - member_id = resource2.Body('member', alternate_id=True) + member_id = resource.Body('member', alternate_id=True) #: The date and time when the member was created. - created_at = resource2.Body('created_at') + created_at = resource.Body('created_at') #: Image ID stored through the image API. Typically a UUID. - image_id = resource2.URI('image_id') + image_id = resource.URI('image_id') #: The status of the image. - status = resource2.Body('status') + status = resource.Body('status') #: The URL for schema of the member. - schema = resource2.Body('schema') + schema = resource.Body('schema') #: The date and time when the member was updated. - updated_at = resource2.Body('updated_at') + updated_at = resource.Body('updated_at') diff --git a/openstack/key_manager/v1/_proxy.py b/openstack/key_manager/v1/_proxy.py index 0c65d2629..21215bad1 100644 --- a/openstack/key_manager/v1/_proxy.py +++ b/openstack/key_manager/v1/_proxy.py @@ -13,10 +13,10 @@ from openstack.key_manager.v1 import container as _container from openstack.key_manager.v1 import order as _order from openstack.key_manager.v1 import secret as _secret -from openstack import proxy2 +from openstack import proxy -class Proxy(proxy2.BaseProxy): +class Proxy(proxy.BaseProxy): def create_container(self, **attrs): """Create a new container from attributes diff --git a/openstack/key_manager/v1/container.py b/openstack/key_manager/v1/container.py index dbfcdfa56..5cc4ff1a0 100644 --- a/openstack/key_manager/v1/container.py +++ b/openstack/key_manager/v1/container.py @@ -12,10 +12,10 @@ from openstack.key_manager import key_manager_service from openstack.key_manager.v1 import _format -from openstack import resource2 +from openstack import resource -class Container(resource2.Resource): +class Container(resource.Resource): resources_key = 'containers' base_path = '/containers' service = key_manager_service.KeyManagerService() @@ -29,21 +29,22 @@ class Container(resource2.Resource): # Properties #: A URI for this container - container_ref = resource2.Body('container_ref') + container_ref = resource.Body('container_ref') #: The ID for this container - container_id = resource2.Body('container_ref', alternate_id=True, - type=_format.HREFToUUID) + container_id = resource.Body( + 'container_ref', alternate_id=True, + type=_format.HREFToUUID) #: The timestamp when this container was created. - created_at = resource2.Body('created') + created_at = resource.Body('created') #: The name of this container - name = resource2.Body('name') + name = resource.Body('name') #: A list of references to secrets in this container - secret_refs = resource2.Body('secret_refs', type=list) + secret_refs = resource.Body('secret_refs', type=list) #: The status of this container - status = resource2.Body('status') + status = resource.Body('status') #: The type of this container - type = resource2.Body('type') + type = resource.Body('type') #: The timestamp when this container was updated. - updated_at = resource2.Body('updated') + updated_at = resource.Body('updated') #: A party interested in this container. - consumers = resource2.Body('consumers', type=list) + consumers = resource.Body('consumers', type=list) diff --git a/openstack/key_manager/v1/order.py b/openstack/key_manager/v1/order.py index b7bed0654..a701275d5 100644 --- a/openstack/key_manager/v1/order.py +++ b/openstack/key_manager/v1/order.py @@ -12,10 +12,10 @@ from openstack.key_manager import key_manager_service from openstack.key_manager.v1 import _format -from openstack import resource2 +from openstack import resource -class Order(resource2.Resource): +class Order(resource.Resource): resources_key = 'orders' base_path = '/orders' service = key_manager_service.KeyManagerService() @@ -28,28 +28,28 @@ class Order(resource2.Resource): allow_list = True #: Timestamp in ISO8601 format of when the order was created - created_at = resource2.Body('created') + created_at = resource.Body('created') #: Keystone Id of the user who created the order - creator_id = resource2.Body('creator_id') + creator_id = resource.Body('creator_id') #: A dictionary containing key-value parameters which specify the #: details of an order request - meta = resource2.Body('meta', type=dict) + meta = resource.Body('meta', type=dict) #: A URI for this order - order_ref = resource2.Body('order_ref') + order_ref = resource.Body('order_ref') #: The ID of this order - order_id = resource2.Body('order_ref', alternate_id=True, - type=_format.HREFToUUID) + order_id = resource.Body( + 'order_ref', alternate_id=True, type=_format.HREFToUUID) #: Secret href associated with the order - secret_ref = resource2.Body('secret_ref') + secret_ref = resource.Body('secret_ref') #: Secret ID associated with the order - secret_id = resource2.Body('secret_ref', type=_format.HREFToUUID) + secret_id = resource.Body('secret_ref', type=_format.HREFToUUID) # The status of this order - status = resource2.Body('status') + status = resource.Body('status') #: Metadata associated with the order - sub_status = resource2.Body('sub_status') + sub_status = resource.Body('sub_status') #: Metadata associated with the order - sub_status_message = resource2.Body('sub_status_message') + sub_status_message = resource.Body('sub_status_message') # The type of order - type = resource2.Body('type') + type = resource.Body('type') #: Timestamp in ISO8601 format of the last time the order was updated. - updated_at = resource2.Body('updated') + updated_at = resource.Body('updated') diff --git a/openstack/key_manager/v1/secret.py b/openstack/key_manager/v1/secret.py index 15b6ebea2..fd04e49e0 100644 --- a/openstack/key_manager/v1/secret.py +++ b/openstack/key_manager/v1/secret.py @@ -12,11 +12,11 @@ from openstack.key_manager import key_manager_service from openstack.key_manager.v1 import _format -from openstack import resource2 +from openstack import resource from openstack import utils -class Secret(resource2.Resource): +class Secret(resource.Resource): resources_key = 'secrets' base_path = '/secrets' service = key_manager_service.KeyManagerService() @@ -28,55 +28,56 @@ class Secret(resource2.Resource): allow_delete = True allow_list = True - _query_mapping = resource2.QueryParameters("name", "mode", "bits", - "secret_type", "acl_only", - "created", "updated", - "expiration", "sort", - algorithm="alg") + _query_mapping = resource.QueryParameters( + "name", "mode", "bits", + "secret_type", "acl_only", + "created", "updated", + "expiration", "sort", + algorithm="alg") # Properties #: Metadata provided by a user or system for informational purposes - algorithm = resource2.Body('algorithm') + algorithm = resource.Body('algorithm') #: Metadata provided by a user or system for informational purposes. #: Value must be greater than zero. - bit_length = resource2.Body('bit_length') + bit_length = resource.Body('bit_length') #: A list of content types - content_types = resource2.Body('content_types', type=dict) + content_types = resource.Body('content_types', type=dict) #: Once this timestamp has past, the secret will no longer be available. - expires_at = resource2.Body('expiration') + expires_at = resource.Body('expiration') #: Timestamp of when the secret was created. - created_at = resource2.Body('created') + created_at = resource.Body('created') #: Timestamp of when the secret was last updated. - updated_at = resource2.Body('updated') + updated_at = resource.Body('updated') #: The type/mode of the algorithm associated with the secret information. - mode = resource2.Body('mode') + mode = resource.Body('mode') #: The name of the secret set by the user - name = resource2.Body('name') + name = resource.Body('name') #: A URI to the sercret - secret_ref = resource2.Body('secret_ref') + secret_ref = resource.Body('secret_ref') #: The ID of the secret # NOTE: This is not really how alternate IDs are supposed to work and # ultimately means this has to work differently than all other services # in all of OpenStack because of the departure from using actual IDs # that even this service can't even use itself. - secret_id = resource2.Body('secret_ref', alternate_id=True, - type=_format.HREFToUUID) + secret_id = resource.Body( + 'secret_ref', alternate_id=True, type=_format.HREFToUUID) #: Used to indicate the type of secret being stored. - secret_type = resource2.Body('secret_type') + secret_type = resource.Body('secret_type') #: The status of this secret - status = resource2.Body('status') + status = resource.Body('status') #: A timestamp when this secret was updated. - updated_at = resource2.Body('updated') + updated_at = resource.Body('updated') #: The secret's data to be stored. payload_content_type must also #: be supplied if payload is included. (optional) - payload = resource2.Body('payload') + payload = resource.Body('payload') #: The media type for the content of the payload. #: (required if payload is included) - payload_content_type = resource2.Body('payload_content_type') + payload_content_type = resource.Body('payload_content_type') #: The encoding used for the payload to be able to include it in #: the JSON request. Currently only base64 is supported. #: (required if payload is encoded) - payload_content_encoding = resource2.Body('payload_content_encoding') + payload_content_encoding = resource.Body('payload_content_encoding') def get(self, session, requires_id=True): request = self._prepare_request(requires_id=requires_id) diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index 8f02b1cd1..8542e2800 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -17,10 +17,10 @@ from openstack.load_balancer.v2 import load_balancer as _lb from openstack.load_balancer.v2 import member as _member from openstack.load_balancer.v2 import pool as _pool -from openstack import proxy2 +from openstack import proxy -class Proxy(proxy2.BaseProxy): +class Proxy(proxy.BaseProxy): def create_load_balancer(self, **attrs): """Create a new load balancer from attributes diff --git a/openstack/load_balancer/v2/health_monitor.py b/openstack/load_balancer/v2/health_monitor.py index ab47d2a52..d06de09a0 100644 --- a/openstack/load_balancer/v2/health_monitor.py +++ b/openstack/load_balancer/v2/health_monitor.py @@ -11,7 +11,7 @@ # under the License. from openstack.load_balancer import load_balancer_service as lb_service -from openstack import resource2 as resource +from openstack import resource class HealthMonitor(resource.Resource): diff --git a/openstack/load_balancer/v2/l7_policy.py b/openstack/load_balancer/v2/l7_policy.py index 2c700702a..6d41d595b 100644 --- a/openstack/load_balancer/v2/l7_policy.py +++ b/openstack/load_balancer/v2/l7_policy.py @@ -11,7 +11,7 @@ # under the License. from openstack.load_balancer import load_balancer_service as lb_service -from openstack import resource2 as resource +from openstack import resource class L7Policy(resource.Resource): diff --git a/openstack/load_balancer/v2/l7_rule.py b/openstack/load_balancer/v2/l7_rule.py index bc6af906e..398e31d24 100644 --- a/openstack/load_balancer/v2/l7_rule.py +++ b/openstack/load_balancer/v2/l7_rule.py @@ -11,7 +11,7 @@ # under the License. from openstack.load_balancer import load_balancer_service as lb_service -from openstack import resource2 as resource +from openstack import resource class L7Rule(resource.Resource): diff --git a/openstack/load_balancer/v2/listener.py b/openstack/load_balancer/v2/listener.py index 2612ff7ba..3ef086fc6 100644 --- a/openstack/load_balancer/v2/listener.py +++ b/openstack/load_balancer/v2/listener.py @@ -11,7 +11,7 @@ # under the License. from openstack.load_balancer import load_balancer_service as lb_service -from openstack import resource2 as resource +from openstack import resource class Listener(resource.Resource): diff --git a/openstack/load_balancer/v2/load_balancer.py b/openstack/load_balancer/v2/load_balancer.py index 1e2afa733..de640aa1f 100644 --- a/openstack/load_balancer/v2/load_balancer.py +++ b/openstack/load_balancer/v2/load_balancer.py @@ -11,7 +11,7 @@ # under the License. from openstack.load_balancer import load_balancer_service as lb_service -from openstack import resource2 as resource +from openstack import resource class LoadBalancer(resource.Resource): diff --git a/openstack/load_balancer/v2/member.py b/openstack/load_balancer/v2/member.py index f40b4dd83..da8200491 100644 --- a/openstack/load_balancer/v2/member.py +++ b/openstack/load_balancer/v2/member.py @@ -11,7 +11,7 @@ # under the License. from openstack.load_balancer import load_balancer_service as lb_service -from openstack import resource2 as resource +from openstack import resource class Member(resource.Resource): diff --git a/openstack/load_balancer/v2/pool.py b/openstack/load_balancer/v2/pool.py index 6d72681e6..305299c56 100644 --- a/openstack/load_balancer/v2/pool.py +++ b/openstack/load_balancer/v2/pool.py @@ -11,7 +11,7 @@ # under the License. from openstack.load_balancer import load_balancer_service as lb_service -from openstack import resource2 as resource +from openstack import resource class Pool(resource.Resource): diff --git a/openstack/load_balancer/version.py b/openstack/load_balancer/version.py index 4a829c2e3..c2266a2b0 100644 --- a/openstack/load_balancer/version.py +++ b/openstack/load_balancer/version.py @@ -11,7 +11,7 @@ # under the License. from openstack.load_balancer import load_balancer_service as lb_service -from openstack import resource2 as resource +from openstack import resource class Version(resource.Resource): diff --git a/openstack/message/v2/_proxy.py b/openstack/message/v2/_proxy.py index 948277278..f12c2174a 100644 --- a/openstack/message/v2/_proxy.py +++ b/openstack/message/v2/_proxy.py @@ -14,11 +14,11 @@ from openstack.message.v2 import message as _message from openstack.message.v2 import queue as _queue from openstack.message.v2 import subscription as _subscription -from openstack import proxy2 -from openstack import resource2 +from openstack import proxy +from openstack import resource -class Proxy(proxy2.BaseProxy): +class Proxy(proxy.BaseProxy): def create_queue(self, **attrs): """Create a new queue from attributes @@ -148,7 +148,7 @@ def delete_message(self, queue_name, value, claim=None, """ message = self._get_resource(_message.Message, value, queue_name=queue_name) - message.claim_id = resource2.Resource._get_id(claim) + message.claim_id = resource.Resource._get_id(claim) return self._delete(_message.Message, message, ignore_missing=ignore_missing) diff --git a/openstack/message/v2/claim.py b/openstack/message/v2/claim.py index c4824c919..9ee98f923 100644 --- a/openstack/message/v2/claim.py +++ b/openstack/message/v2/claim.py @@ -13,14 +13,14 @@ import uuid from openstack.message import message_service -from openstack import resource2 +from openstack import resource -class Claim(resource2.Resource): +class Claim(resource.Resource): # FIXME(anyone): The name string of `location` field of Zaqar API response # is lower case. That is inconsistent with the guide from API-WG. This is # a workaround for this issue. - location = resource2.Header("location") + location = resource.Header("location") resources_key = 'claims' base_path = '/queues/%(queue_name)s/claims' @@ -35,27 +35,27 @@ class Claim(resource2.Resource): # Properties #: The value in seconds indicating how long the claim has existed. - age = resource2.Body("age") + age = resource.Body("age") #: In case worker stops responding for a long time, the server will #: extend the lifetime of claimed messages to be at least as long as #: the lifetime of the claim itself, plus the specified grace period. #: Must between 60 and 43200 seconds(12 hours). - grace = resource2.Body("grace") + grace = resource.Body("grace") #: The number of messages to claim. Default 10, up to 20. - limit = resource2.Body("limit") + limit = resource.Body("limit") #: Messages have been successfully claimed. - messages = resource2.Body("messages") + messages = resource.Body("messages") #: Number of seconds the server wait before releasing the claim. Must #: between 60 and 43200 seconds(12 hours). - ttl = resource2.Body("ttl") + ttl = resource.Body("ttl") #: The name of queue to claim message from. - queue_name = resource2.URI("queue_name") + queue_name = resource.URI("queue_name") #: The ID to identify the client accessing Zaqar API. Must be specified #: in header for each API request. - client_id = resource2.Header("Client-ID") + client_id = resource.Header("Client-ID") #: The ID to identify the project. Must be provided when keystone #: authentication is not enabled in Zaqar service. - project_id = resource2.Header("X-PROJECT-ID") + project_id = resource.Header("X-PROJECT-ID") def _translate_response(self, response, has_body=True): super(Claim, self)._translate_response(response, has_body=has_body) diff --git a/openstack/message/v2/message.py b/openstack/message/v2/message.py index a889f14d2..2b8cccf65 100644 --- a/openstack/message/v2/message.py +++ b/openstack/message/v2/message.py @@ -13,14 +13,14 @@ import uuid from openstack.message import message_service -from openstack import resource2 +from openstack import resource -class Message(resource2.Resource): +class Message(resource.Resource): # FIXME(anyone): The name string of `location` field of Zaqar API response # is lower case. That is inconsistent with the guide from API-WG. This is # a workaround for this issue. - location = resource2.Header("location") + location = resource.Header("location") resources_key = 'messages' base_path = '/queues/%(queue_name)s/messages' @@ -32,28 +32,28 @@ class Message(resource2.Resource): allow_get = True allow_delete = True - _query_mapping = resource2.QueryParameters("echo", "include_claimed") + _query_mapping = resource.QueryParameters("echo", "include_claimed") # Properties #: The value in second to specify how long the message has been #: posted to the queue. - age = resource2.Body("age") + age = resource.Body("age") #: A dictionary specifies an arbitrary document that constitutes the #: body of the message being sent. - body = resource2.Body("body") + body = resource.Body("body") #: An uri string describe the location of the message resource. - href = resource2.Body("href") + href = resource.Body("href") #: The value in seconds to specify how long the server waits before #: marking the message as expired and removing it from the queue. - ttl = resource2.Body("ttl") + ttl = resource.Body("ttl") #: The name of target queue message is post to or got from. - queue_name = resource2.URI("queue_name") + queue_name = resource.URI("queue_name") #: The ID to identify the client accessing Zaqar API. Must be specified #: in header for each API request. - client_id = resource2.Header("Client-ID") + client_id = resource.Header("Client-ID") #: The ID to identify the project accessing Zaqar API. Must be specified #: in case keystone auth is not enabled in Zaqar service. - project_id = resource2.Header("X-PROJECT-ID") + project_id = resource.Header("X-PROJECT-ID") def post(self, session, messages): request = self._prepare_request(requires_id=False, prepend_key=True) @@ -72,7 +72,7 @@ def post(self, session, messages): def list(cls, session, paginated=True, **params): """This method is a generator which yields message objects. - This is almost the copy of list method of resource2.Resource class. + This is almost the copy of list method of resource.Resource class. The only difference is the request header now includes `Client-ID` and `X-PROJECT-ID` fields which are required by Zaqar v2 API. """ diff --git a/openstack/message/v2/queue.py b/openstack/message/v2/queue.py index 7be063789..d69b0034c 100644 --- a/openstack/message/v2/queue.py +++ b/openstack/message/v2/queue.py @@ -13,14 +13,14 @@ import uuid from openstack.message import message_service -from openstack import resource2 +from openstack import resource -class Queue(resource2.Resource): +class Queue(resource.Resource): # FIXME(anyone): The name string of `location` field of Zaqar API response # is lower case. That is inconsistent with the guide from API-WG. This is # a workaround for this issue. - location = resource2.Header("location") + location = resource.Header("location") resources_key = "queues" base_path = "/queues" @@ -35,22 +35,22 @@ class Queue(resource2.Resource): # Properties #: The default TTL of messages defined for a queue, which will effect for #: any messages posted to the queue. - default_message_ttl = resource2.Body("_default_message_ttl") + default_message_ttl = resource.Body("_default_message_ttl") #: Description of the queue. - description = resource2.Body("description") + description = resource.Body("description") #: The max post size of messages defined for a queue, which will effect #: for any messages posted to the queue. - max_messages_post_size = resource2.Body("_max_messages_post_size") + max_messages_post_size = resource.Body("_max_messages_post_size") #: Name of the queue. The name is the unique identity of a queue. It #: must not exceed 64 bytes in length, and it is limited to US-ASCII #: letters, digits, underscores, and hyphens. - name = resource2.Body("name", alternate_id=True) + name = resource.Body("name", alternate_id=True) #: The ID to identify the client accessing Zaqar API. Must be specified #: in header for each API request. - client_id = resource2.Header("Client-ID") + client_id = resource.Header("Client-ID") #: The ID to identify the project accessing Zaqar API. Must be specified #: in case keystone auth is not enabled in Zaqar service. - project_id = resource2.Header("X-PROJECT-ID") + project_id = resource.Header("X-PROJECT-ID") def create(self, session, prepend_key=True): request = self._prepare_request(requires_id=True, @@ -70,7 +70,7 @@ def create(self, session, prepend_key=True): def list(cls, session, paginated=False, **params): """This method is a generator which yields queue objects. - This is almost the copy of list method of resource2.Resource class. + This is almost the copy of list method of resource.Resource class. The only difference is the request header now includes `Client-ID` and `X-PROJECT-ID` fields which are required by Zaqar v2 API. """ diff --git a/openstack/message/v2/subscription.py b/openstack/message/v2/subscription.py index e95329fe2..f9de79b57 100644 --- a/openstack/message/v2/subscription.py +++ b/openstack/message/v2/subscription.py @@ -13,14 +13,14 @@ import uuid from openstack.message import message_service -from openstack import resource2 +from openstack import resource -class Subscription(resource2.Resource): +class Subscription(resource.Resource): # FIXME(anyone): The name string of `location` field of Zaqar API response # is lower case. That is inconsistent with the guide from API-WG. This is # a workaround for this issue. - location = resource2.Header("location") + location = resource.Header("location") resources_key = 'subscriptions' base_path = '/queues/%(queue_name)s/subscriptions' @@ -34,31 +34,31 @@ class Subscription(resource2.Resource): # Properties #: The value in seconds indicating how long the subscription has existed. - age = resource2.Body("age") + age = resource.Body("age") #: Alternate id of the subscription. This key is used in response of #: subscription create API to return id of subscription created. - subscription_id = resource2.Body("subscription_id", alternate_id=True) + subscription_id = resource.Body("subscription_id", alternate_id=True) #: The extra metadata for the subscription. The value must be a dict. #: If the subscriber is `mailto`. The options can contain `from` and #: `subject` to indicate the email's author and title. - options = resource2.Body("options", type=dict) + options = resource.Body("options", type=dict) #: The queue name which the subscription is registered on. - source = resource2.Body("source") + source = resource.Body("source") #: The destination of the message. Two kinds of subscribers are supported: #: http/https and email. The http/https subscriber should start with #: `http/https`. The email subscriber should start with `mailto`. - subscriber = resource2.Body("subscriber") + subscriber = resource.Body("subscriber") #: Number of seconds the subscription remains alive? The ttl value must #: be great than 60 seconds. The default value is 3600 seconds. - ttl = resource2.Body("ttl") + ttl = resource.Body("ttl") #: The queue name which the subscription is registered on. - queue_name = resource2.URI("queue_name") + queue_name = resource.URI("queue_name") #: The ID to identify the client accessing Zaqar API. Must be specified #: in header for each API request. - client_id = resource2.Header("Client-ID") + client_id = resource.Header("Client-ID") #: The ID to identify the project. Must be provided when keystone #: authentication is not enabled in Zaqar service. - project_id = resource2.Header("X-PROJECT-ID") + project_id = resource.Header("X-PROJECT-ID") def create(self, session, prepend_key=True): request = self._prepare_request(requires_id=False, @@ -78,7 +78,7 @@ def create(self, session, prepend_key=True): def list(cls, session, paginated=True, **params): """This method is a generator which yields subscription objects. - This is almost the copy of list method of resource2.Resource class. + This is almost the copy of list method of resource.Resource class. The only difference is the request header now includes `Client-ID` and `X-PROJECT-ID` fields which are required by Zaqar v2 API. """ diff --git a/openstack/message/version.py b/openstack/message/version.py index 831effac7..6cb2ee51d 100644 --- a/openstack/message/version.py +++ b/openstack/message/version.py @@ -11,7 +11,7 @@ # under the License. from openstack.message import message_service -from openstack import resource2 as resource +from openstack import resource class Version(resource.Resource): diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 09aeb3eac..a503a3723 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -48,11 +48,11 @@ from openstack.network.v2 import subnet as _subnet from openstack.network.v2 import subnet_pool as _subnet_pool from openstack.network.v2 import vpn_service as _vpn_service -from openstack import proxy2 +from openstack import proxy from openstack import utils -class Proxy(proxy2.BaseProxy): +class Proxy(proxy.BaseProxy): def create_address_scope(self, **attrs): """Create a new address scope from attributes @@ -3036,12 +3036,12 @@ def set_tags(self, resource, tags): """Replace tags of a specified resource with specified tags :param resource: - :class:`~openstack.resource2.Resource` instance. + :class:`~openstack.resource.Resource` instance. :param tags: New tags to be set. :type tags: "list" :returns: The updated resource - :rtype: :class:`~openstack.resource2.Resource` + :rtype: :class:`~openstack.resource.Resource` """ self._check_tag_support(resource) return resource.set_tags(self, tags) diff --git a/openstack/network/v2/address_scope.py b/openstack/network/v2/address_scope.py index 1364fcfdd..501800cb2 100644 --- a/openstack/network/v2/address_scope.py +++ b/openstack/network/v2/address_scope.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource2 as resource +from openstack import resource class AddressScope(resource.Resource): diff --git a/openstack/network/v2/agent.py b/openstack/network/v2/agent.py index 0259d2b4e..c657bfc8b 100644 --- a/openstack/network/v2/agent.py +++ b/openstack/network/v2/agent.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource2 as resource +from openstack import resource from openstack import utils diff --git a/openstack/network/v2/auto_allocated_topology.py b/openstack/network/v2/auto_allocated_topology.py index 84cf0bfa3..917de9780 100644 --- a/openstack/network/v2/auto_allocated_topology.py +++ b/openstack/network/v2/auto_allocated_topology.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource2 as resource +from openstack import resource class AutoAllocatedTopology(resource.Resource): diff --git a/openstack/network/v2/availability_zone.py b/openstack/network/v2/availability_zone.py index dd55f1797..7bc1a8d04 100644 --- a/openstack/network/v2/availability_zone.py +++ b/openstack/network/v2/availability_zone.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource2 as _resource +from openstack import resource as _resource class AvailabilityZone(_resource.Resource): diff --git a/openstack/network/v2/extension.py b/openstack/network/v2/extension.py index 76c79c8e9..50fc4371f 100644 --- a/openstack/network/v2/extension.py +++ b/openstack/network/v2/extension.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource2 as resource +from openstack import resource class Extension(resource.Resource): diff --git a/openstack/network/v2/flavor.py b/openstack/network/v2/flavor.py index 273d132f5..0947f5677 100644 --- a/openstack/network/v2/flavor.py +++ b/openstack/network/v2/flavor.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource2 as resource +from openstack import resource from openstack import utils diff --git a/openstack/network/v2/floating_ip.py b/openstack/network/v2/floating_ip.py index 2536e7f47..5ffbb1f4e 100644 --- a/openstack/network/v2/floating_ip.py +++ b/openstack/network/v2/floating_ip.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource2 as resource +from openstack import resource class FloatingIP(resource.Resource): diff --git a/openstack/network/v2/health_monitor.py b/openstack/network/v2/health_monitor.py index 74be84183..4430b3009 100644 --- a/openstack/network/v2/health_monitor.py +++ b/openstack/network/v2/health_monitor.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource2 as resource +from openstack import resource class HealthMonitor(resource.Resource): diff --git a/openstack/network/v2/listener.py b/openstack/network/v2/listener.py index 509367e91..b947cc171 100644 --- a/openstack/network/v2/listener.py +++ b/openstack/network/v2/listener.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource2 as resource +from openstack import resource class Listener(resource.Resource): diff --git a/openstack/network/v2/load_balancer.py b/openstack/network/v2/load_balancer.py index 60f6df747..29cedd299 100644 --- a/openstack/network/v2/load_balancer.py +++ b/openstack/network/v2/load_balancer.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource2 as resource +from openstack import resource class LoadBalancer(resource.Resource): diff --git a/openstack/network/v2/metering_label.py b/openstack/network/v2/metering_label.py index 31220e445..5d9cccf38 100644 --- a/openstack/network/v2/metering_label.py +++ b/openstack/network/v2/metering_label.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource2 as resource +from openstack import resource class MeteringLabel(resource.Resource): diff --git a/openstack/network/v2/metering_label_rule.py b/openstack/network/v2/metering_label_rule.py index 568d054bc..e6db81fd5 100644 --- a/openstack/network/v2/metering_label_rule.py +++ b/openstack/network/v2/metering_label_rule.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource2 as resource +from openstack import resource class MeteringLabelRule(resource.Resource): diff --git a/openstack/network/v2/network.py b/openstack/network/v2/network.py index f2aa0aa8e..0c1ca1d86 100644 --- a/openstack/network/v2/network.py +++ b/openstack/network/v2/network.py @@ -12,7 +12,7 @@ from openstack.network import network_service from openstack.network.v2 import tag -from openstack import resource2 as resource +from openstack import resource class Network(resource.Resource, tag.TagMixin): diff --git a/openstack/network/v2/network_ip_availability.py b/openstack/network/v2/network_ip_availability.py index 132fbc94f..e5ca95fda 100644 --- a/openstack/network/v2/network_ip_availability.py +++ b/openstack/network/v2/network_ip_availability.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource2 as resource +from openstack import resource class NetworkIPAvailability(resource.Resource): diff --git a/openstack/network/v2/pool.py b/openstack/network/v2/pool.py index 328d51afa..9cec41364 100644 --- a/openstack/network/v2/pool.py +++ b/openstack/network/v2/pool.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource2 as resource +from openstack import resource class Pool(resource.Resource): diff --git a/openstack/network/v2/pool_member.py b/openstack/network/v2/pool_member.py index 2d8dba058..2de93f951 100644 --- a/openstack/network/v2/pool_member.py +++ b/openstack/network/v2/pool_member.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource2 as resource +from openstack import resource class PoolMember(resource.Resource): diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index 8240443ce..2a5ad4b0c 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -12,7 +12,7 @@ from openstack.network import network_service from openstack.network.v2 import tag -from openstack import resource2 as resource +from openstack import resource class Port(resource.Resource, tag.TagMixin): diff --git a/openstack/network/v2/qos_bandwidth_limit_rule.py b/openstack/network/v2/qos_bandwidth_limit_rule.py index ba5c32d8e..f080938ec 100644 --- a/openstack/network/v2/qos_bandwidth_limit_rule.py +++ b/openstack/network/v2/qos_bandwidth_limit_rule.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource2 as resource +from openstack import resource class QoSBandwidthLimitRule(resource.Resource): diff --git a/openstack/network/v2/qos_dscp_marking_rule.py b/openstack/network/v2/qos_dscp_marking_rule.py index c01a6e548..bf247f1e1 100644 --- a/openstack/network/v2/qos_dscp_marking_rule.py +++ b/openstack/network/v2/qos_dscp_marking_rule.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource2 as resource +from openstack import resource class QoSDSCPMarkingRule(resource.Resource): diff --git a/openstack/network/v2/qos_minimum_bandwidth_rule.py b/openstack/network/v2/qos_minimum_bandwidth_rule.py index 09577dc97..ad773d5d8 100644 --- a/openstack/network/v2/qos_minimum_bandwidth_rule.py +++ b/openstack/network/v2/qos_minimum_bandwidth_rule.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource2 as resource +from openstack import resource class QoSMinimumBandwidthRule(resource.Resource): diff --git a/openstack/network/v2/qos_policy.py b/openstack/network/v2/qos_policy.py index f14de70aa..406285785 100644 --- a/openstack/network/v2/qos_policy.py +++ b/openstack/network/v2/qos_policy.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource2 as resource +from openstack import resource class QoSPolicy(resource.Resource): diff --git a/openstack/network/v2/qos_rule_type.py b/openstack/network/v2/qos_rule_type.py index ee2812c17..a241615ea 100644 --- a/openstack/network/v2/qos_rule_type.py +++ b/openstack/network/v2/qos_rule_type.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource2 as resource +from openstack import resource class QoSRuleType(resource.Resource): diff --git a/openstack/network/v2/quota.py b/openstack/network/v2/quota.py index 283242088..fadaff1b0 100644 --- a/openstack/network/v2/quota.py +++ b/openstack/network/v2/quota.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource2 as resource +from openstack import resource class Quota(resource.Resource): diff --git a/openstack/network/v2/rbac_policy.py b/openstack/network/v2/rbac_policy.py index 16c3022ae..e0262ab77 100644 --- a/openstack/network/v2/rbac_policy.py +++ b/openstack/network/v2/rbac_policy.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource2 as resource +from openstack import resource class RBACPolicy(resource.Resource): diff --git a/openstack/network/v2/router.py b/openstack/network/v2/router.py index fd90f6a7a..4c123777a 100644 --- a/openstack/network/v2/router.py +++ b/openstack/network/v2/router.py @@ -12,7 +12,7 @@ from openstack.network import network_service from openstack.network.v2 import tag -from openstack import resource2 as resource +from openstack import resource from openstack import utils diff --git a/openstack/network/v2/security_group.py b/openstack/network/v2/security_group.py index 2b7e0ff5f..d4c956263 100644 --- a/openstack/network/v2/security_group.py +++ b/openstack/network/v2/security_group.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource2 as resource +from openstack import resource class SecurityGroup(resource.Resource): diff --git a/openstack/network/v2/security_group_rule.py b/openstack/network/v2/security_group_rule.py index 6ea5e84a5..6bbf0d672 100644 --- a/openstack/network/v2/security_group_rule.py +++ b/openstack/network/v2/security_group_rule.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource2 as resource +from openstack import resource class SecurityGroupRule(resource.Resource): diff --git a/openstack/network/v2/segment.py b/openstack/network/v2/segment.py index 8d211ecbf..c81f21049 100644 --- a/openstack/network/v2/segment.py +++ b/openstack/network/v2/segment.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource2 as resource +from openstack import resource class Segment(resource.Resource): diff --git a/openstack/network/v2/service_profile.py b/openstack/network/v2/service_profile.py index 0af0a340f..2707e0ac9 100644 --- a/openstack/network/v2/service_profile.py +++ b/openstack/network/v2/service_profile.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource2 as resource +from openstack import resource class ServiceProfile(resource.Resource): diff --git a/openstack/network/v2/service_provider.py b/openstack/network/v2/service_provider.py index 0f6680303..d6d93d2fc 100644 --- a/openstack/network/v2/service_provider.py +++ b/openstack/network/v2/service_provider.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource2 as resource +from openstack import resource class ServiceProvider(resource.Resource): diff --git a/openstack/network/v2/subnet.py b/openstack/network/v2/subnet.py index a79a26f9b..0dbf88001 100644 --- a/openstack/network/v2/subnet.py +++ b/openstack/network/v2/subnet.py @@ -12,7 +12,7 @@ from openstack.network import network_service from openstack.network.v2 import tag -from openstack import resource2 as resource +from openstack import resource class Subnet(resource.Resource, tag.TagMixin): diff --git a/openstack/network/v2/subnet_pool.py b/openstack/network/v2/subnet_pool.py index b4115d4d7..49e51e60b 100644 --- a/openstack/network/v2/subnet_pool.py +++ b/openstack/network/v2/subnet_pool.py @@ -12,7 +12,7 @@ from openstack.network import network_service from openstack.network.v2 import tag -from openstack import resource2 as resource +from openstack import resource class SubnetPool(resource.Resource, tag.TagMixin): diff --git a/openstack/network/v2/vpn_service.py b/openstack/network/v2/vpn_service.py index 55556b425..c65ee121a 100644 --- a/openstack/network/v2/vpn_service.py +++ b/openstack/network/v2/vpn_service.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource2 as resource +from openstack import resource # NOTE: The VPN service is unmaintained, need to consider remove it diff --git a/openstack/network/version.py b/openstack/network/version.py index a3b520b72..9de4601ca 100644 --- a/openstack/network/version.py +++ b/openstack/network/version.py @@ -11,7 +11,7 @@ # under the License. from openstack.network import network_service -from openstack import resource2 as resource +from openstack import resource class Version(resource.Resource): diff --git a/openstack/object_store/v1/_base.py b/openstack/object_store/v1/_base.py index d2f9c13d6..ca297b0bb 100644 --- a/openstack/object_store/v1/_base.py +++ b/openstack/object_store/v1/_base.py @@ -13,7 +13,7 @@ from openstack import exceptions from openstack.object_store import object_store_service -from openstack import resource2 as resource +from openstack import resource class BaseResource(resource.Resource): diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index 8b8a7b4ec..e220cfd12 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -13,7 +13,7 @@ from openstack.object_store.v1 import account as _account from openstack.object_store.v1 import container as _container from openstack.object_store.v1 import obj as _obj -from openstack import proxy2 as proxy +from openstack import proxy class Proxy(proxy.BaseProxy): diff --git a/openstack/object_store/v1/account.py b/openstack/object_store/v1/account.py index 410d3a339..ede5716d7 100644 --- a/openstack/object_store/v1/account.py +++ b/openstack/object_store/v1/account.py @@ -12,7 +12,7 @@ # under the License. from openstack.object_store.v1 import _base -from openstack import resource2 as resource +from openstack import resource class Account(_base.BaseResource): diff --git a/openstack/object_store/v1/container.py b/openstack/object_store/v1/container.py index f5594a8f6..51452c882 100644 --- a/openstack/object_store/v1/container.py +++ b/openstack/object_store/v1/container.py @@ -12,7 +12,7 @@ # under the License. from openstack.object_store.v1 import _base -from openstack import resource2 as resource +from openstack import resource class Container(_base.BaseResource): diff --git a/openstack/object_store/v1/obj.py b/openstack/object_store/v1/obj.py index daacddcaf..2c305c9ef 100644 --- a/openstack/object_store/v1/obj.py +++ b/openstack/object_store/v1/obj.py @@ -16,7 +16,7 @@ from openstack import exceptions from openstack.object_store import object_store_service from openstack.object_store.v1 import _base -from openstack import resource2 as resource +from openstack import resource class Object(_base.BaseResource): diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index 0ce2f1ec5..d83e469a7 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -19,10 +19,10 @@ from openstack.orchestration.v1 import stack_files as _stack_files from openstack.orchestration.v1 import stack_template as _stack_template from openstack.orchestration.v1 import template as _template -from openstack import proxy2 +from openstack import proxy -class Proxy(proxy2.BaseProxy): +class Proxy(proxy.BaseProxy): def create_stack(self, preview=False, **attrs): """Create a new stack from attributes diff --git a/openstack/orchestration/v1/resource.py b/openstack/orchestration/v1/resource.py index f2a0a5633..710e5dcbf 100644 --- a/openstack/orchestration/v1/resource.py +++ b/openstack/orchestration/v1/resource.py @@ -11,7 +11,7 @@ # under the License. from openstack.orchestration import orchestration_service -from openstack import resource2 as resource +from openstack import resource class Resource(resource.Resource): diff --git a/openstack/orchestration/v1/software_config.py b/openstack/orchestration/v1/software_config.py index c0f128852..b8eb8e93d 100644 --- a/openstack/orchestration/v1/software_config.py +++ b/openstack/orchestration/v1/software_config.py @@ -11,7 +11,7 @@ # under the License. from openstack.orchestration import orchestration_service -from openstack import resource2 as resource +from openstack import resource class SoftwareConfig(resource.Resource): diff --git a/openstack/orchestration/v1/software_deployment.py b/openstack/orchestration/v1/software_deployment.py index 3fb22eafd..a76cc08ef 100644 --- a/openstack/orchestration/v1/software_deployment.py +++ b/openstack/orchestration/v1/software_deployment.py @@ -11,7 +11,7 @@ # under the License. from openstack.orchestration import orchestration_service -from openstack import resource2 as resource +from openstack import resource class SoftwareDeployment(resource.Resource): diff --git a/openstack/orchestration/v1/stack.py b/openstack/orchestration/v1/stack.py index 9172be71e..b42301eef 100644 --- a/openstack/orchestration/v1/stack.py +++ b/openstack/orchestration/v1/stack.py @@ -12,7 +12,7 @@ from openstack import exceptions from openstack.orchestration import orchestration_service -from openstack import resource2 as resource +from openstack import resource from openstack import utils diff --git a/openstack/orchestration/v1/stack_environment.py b/openstack/orchestration/v1/stack_environment.py index 7ffc5d5f8..dcf008f6e 100644 --- a/openstack/orchestration/v1/stack_environment.py +++ b/openstack/orchestration/v1/stack_environment.py @@ -11,7 +11,7 @@ # under the License. from openstack.orchestration import orchestration_service -from openstack import resource2 as resource +from openstack import resource class StackEnvironment(resource.Resource): diff --git a/openstack/orchestration/v1/stack_files.py b/openstack/orchestration/v1/stack_files.py index cb16dcd6e..e671688f2 100644 --- a/openstack/orchestration/v1/stack_files.py +++ b/openstack/orchestration/v1/stack_files.py @@ -11,7 +11,7 @@ # under the License. from openstack.orchestration import orchestration_service -from openstack import resource2 as resource +from openstack import resource class StackFiles(resource.Resource): diff --git a/openstack/orchestration/v1/stack_template.py b/openstack/orchestration/v1/stack_template.py index 7dace49a5..892ee4348 100644 --- a/openstack/orchestration/v1/stack_template.py +++ b/openstack/orchestration/v1/stack_template.py @@ -11,7 +11,7 @@ # under the License. from openstack.orchestration import orchestration_service -from openstack import resource2 as resource +from openstack import resource class StackTemplate(resource.Resource): diff --git a/openstack/orchestration/v1/template.py b/openstack/orchestration/v1/template.py index 7e2c5eab2..aa516d591 100644 --- a/openstack/orchestration/v1/template.py +++ b/openstack/orchestration/v1/template.py @@ -13,7 +13,7 @@ from six.moves.urllib import parse from openstack.orchestration import orchestration_service -from openstack import resource2 as resource +from openstack import resource class Template(resource.Resource): diff --git a/openstack/orchestration/version.py b/openstack/orchestration/version.py index ecfdbc8eb..2dd05c286 100644 --- a/openstack/orchestration/version.py +++ b/openstack/orchestration/version.py @@ -11,7 +11,7 @@ # under the License. from openstack.orchestration import orchestration_service -from openstack import resource2 as resource +from openstack import resource class Version(resource.Resource): diff --git a/openstack/proxy2.py b/openstack/proxy.py similarity index 79% rename from openstack/proxy2.py rename to openstack/proxy.py index 33d3ee6f2..6954c823a 100644 --- a/openstack/proxy2.py +++ b/openstack/proxy.py @@ -12,7 +12,7 @@ from openstack import _adapter from openstack import exceptions -from openstack import resource2 +from openstack import resource from openstack import utils @@ -28,9 +28,9 @@ def _check_resource(strict=False): def wrap(method): def check(self, expected, actual=None, *args, **kwargs): if (strict and actual is not None and not - isinstance(actual, resource2.Resource)): + isinstance(actual, resource.Resource)): raise ValueError("A %s must be passed" % expected.__name__) - elif (isinstance(actual, resource2.Resource) and not + elif (isinstance(actual, resource.Resource) and not isinstance(actual, expected)): raise ValueError("Expected %s but received %s" % ( expected.__name__, actual.__class__.__name__)) @@ -47,7 +47,7 @@ def _get_resource(self, resource_type, value, **attrs): :param resource_type: The type of resource to operate on. This should be a subclass of - :class:`~openstack.resource2.Resource` with a + :class:`~openstack.resource.Resource` with a ``from_id`` method. :param value: The ID of a resource or an object of ``resource_type`` class if using an existing instance, or None to create a @@ -80,7 +80,7 @@ def _get_uri_attribute(self, child, parent, name): if parent is None: value = getattr(child, name) else: - value = resource2.Resource._get_id(parent) + value = resource.Resource._get_id(parent) return value def _find(self, resource_type, name_or_id, ignore_missing=True, @@ -92,9 +92,9 @@ def _find(self, resource_type, name_or_id, ignore_missing=True, :class:`~openstack.exceptions.ResourceNotFound` will be raised when the resource does not exist. When set to ``True``, None will be returned when - attempting to find a nonexistent resource2. + attempting to find a nonexistent resource. :param dict attrs: Attributes to be passed onto the - :meth:`~openstack.resource2.Resource.find` + :meth:`~openstack.resource.Resource.find` method, such as query parameters. :returns: An instance of ``resource_type`` or None @@ -108,23 +108,23 @@ def _delete(self, resource_type, value, ignore_missing=True, **attrs): """Delete a resource :param resource_type: The type of resource to delete. This should - be a :class:`~openstack.resource2.Resource` + be a :class:`~openstack.resource.Resource` subclass with a ``from_id`` method. :param value: The value to delete. Can be either the ID of a - resource or a :class:`~openstack.resource2.Resource` + resource or a :class:`~openstack.resource.Resource` subclass. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the resource does not exist. When set to ``True``, no exception will be set when - attempting to delete a nonexistent resource2. + attempting to delete a nonexistent resource. :param dict attrs: Attributes to be passed onto the - :meth:`~openstack.resource2.Resource.delete` + :meth:`~openstack.resource.Resource.delete` method, such as the ID of a parent resource. :returns: The result of the ``delete`` :raises: ``ValueError`` if ``value`` is a - :class:`~openstack.resource2.Resource` that doesn't match + :class:`~openstack.resource.Resource` that doesn't match the ``resource_type``. :class:`~openstack.exceptions.ResourceNotFound` when ignore_missing if ``False`` and a nonexistent resource @@ -155,19 +155,19 @@ def _update(self, resource_type, value, **attrs): """Update a resource :param resource_type: The type of resource to update. - :type resource_type: :class:`~openstack.resource2.Resource` + :type resource_type: :class:`~openstack.resource.Resource` :param value: The resource to update. This must either be a - :class:`~openstack.resource2.Resource` or an id - that corresponds to a resource2. + :class:`~openstack.resource.Resource` or an id + that corresponds to a resource. :param dict attrs: Attributes to be passed onto the - :meth:`~openstack.resource2.Resource.update` + :meth:`~openstack.resource.Resource.update` method to be updated. These should correspond - to either :class:`~openstack.resource2.Body` - or :class:`~openstack.resource2.Header` + to either :class:`~openstack.resource.Body` + or :class:`~openstack.resource.Header` values on this resource. :returns: The result of the ``update`` - :rtype: :class:`~openstack.resource2.Resource` + :rtype: :class:`~openstack.resource.Resource` """ res = self._get_resource(resource_type, value, **attrs) return res.update(self) @@ -176,18 +176,18 @@ def _create(self, resource_type, **attrs): """Create a resource from attributes :param resource_type: The type of resource to create. - :type resource_type: :class:`~openstack.resource2.Resource` + :type resource_type: :class:`~openstack.resource.Resource` :param path_args: A dict containing arguments for forming the request URL, if needed. :param dict attrs: Attributes to be passed onto the - :meth:`~openstack.resource2.Resource.create` + :meth:`~openstack.resource.Resource.create` method to be created. These should correspond - to either :class:`~openstack.resource2.Body` - or :class:`~openstack.resource2.Header` + to either :class:`~openstack.resource.Body` + or :class:`~openstack.resource.Header` values on this resource. :returns: The result of the ``create`` - :rtype: :class:`~openstack.resource2.Resource` + :rtype: :class:`~openstack.resource.Resource` """ res = resource_type.new(**attrs) return res.create(self) @@ -197,19 +197,19 @@ def _get(self, resource_type, value=None, requires_id=True, **attrs): """Get a resource :param resource_type: The type of resource to get. - :type resource_type: :class:`~openstack.resource2.Resource` + :type resource_type: :class:`~openstack.resource.Resource` :param value: The value to get. Can be either the ID of a - resource or a :class:`~openstack.resource2.Resource` + resource or a :class:`~openstack.resource.Resource` subclass. :param dict attrs: Attributes to be passed onto the - :meth:`~openstack.resource2.Resource.get` + :meth:`~openstack.resource.Resource.get` method. These should correspond - to either :class:`~openstack.resource2.Body` - or :class:`~openstack.resource2.Header` + to either :class:`~openstack.resource.Body` + or :class:`~openstack.resource.Header` values on this resource. :returns: The result of the ``get`` - :rtype: :class:`~openstack.resource2.Resource` + :rtype: :class:`~openstack.resource.Resource` """ res = self._get_resource(resource_type, value, **attrs) @@ -222,23 +222,23 @@ def _list(self, resource_type, value=None, paginated=False, **attrs): """List a resource :param resource_type: The type of resource to delete. This should - be a :class:`~openstack.resource2.Resource` + be a :class:`~openstack.resource.Resource` subclass with a ``from_id`` method. :param value: The resource to list. It can be the ID of a resource, or - a :class:`~openstack.resource2.Resource` object. When set + a :class:`~openstack.resource.Resource` object. When set to None, a new bare resource is created. :param bool paginated: When set to ``False``, expect all of the data to be returned in one response. When set to ``True``, the resource supports data being returned across multiple pages. :param dict attrs: Attributes to be passed onto the - :meth:`~openstack.resource2.Resource.list` method. These should - correspond to either :class:`~openstack.resource2.URI` values - or appear in :data:`~openstack.resource2.Resource._query_mapping`. + :meth:`~openstack.resource.Resource.list` method. These should + correspond to either :class:`~openstack.resource.URI` values + or appear in :data:`~openstack.resource.Resource._query_mapping`. :returns: A generator of Resource objects. :raises: ``ValueError`` if ``value`` is a - :class:`~openstack.resource2.Resource` that doesn't match + :class:`~openstack.resource.Resource` that doesn't match the ``resource_type``. """ res = self._get_resource(resource_type, value, **attrs) @@ -248,18 +248,18 @@ def _head(self, resource_type, value=None, **attrs): """Retrieve a resource's header :param resource_type: The type of resource to retrieve. - :type resource_type: :class:`~openstack.resource2.Resource` + :type resource_type: :class:`~openstack.resource.Resource` :param value: The value of a specific resource to retreive headers for. Can be either the ID of a resource, - a :class:`~openstack.resource2.Resource` subclass, + a :class:`~openstack.resource.Resource` subclass, or ``None``. :param dict attrs: Attributes to be passed onto the - :meth:`~openstack.resource2.Resource.head` method. + :meth:`~openstack.resource.Resource.head` method. These should correspond to - :class:`~openstack.resource2.URI` values. + :class:`~openstack.resource.URI` values. :returns: The result of the ``head`` call - :rtype: :class:`~openstack.resource2.Resource` + :rtype: :class:`~openstack.resource.Resource` """ res = self._get_resource(resource_type, value, **attrs) return res.head(self) @@ -267,7 +267,7 @@ def _head(self, resource_type, value=None, **attrs): @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", details=("This is no longer a part of the proxy base, " "service-specific subclasses should expose " - "this as needed. See resource2.wait_for_status " + "this as needed. See resource.wait_for_status " "for this behavior")) def wait_for_status(self, value, status, failures=None, interval=2, wait=120): @@ -275,8 +275,8 @@ def wait_for_status(self, value, status, failures=None, interval=2, :param value: The resource to wait on to reach the status. The resource must have a status attribute. - :type value: :class:`~openstack.resource2.Resource` - :param status: Desired status of the resource2. + :type value: :class:`~openstack.resource.Resource` + :param status: Desired status of the resource. :param list failures: Statuses that would indicate the transition failed such as 'ERROR'. :param interval: Number of seconds to wait between checks. @@ -291,19 +291,19 @@ def wait_for_status(self, value, status, failures=None, interval=2, status attribute """ failures = [] if failures is None else failures - return resource2.wait_for_status(self, value, status, - failures, interval, wait) + return resource.wait_for_status( + self, value, status, failures, interval, wait) @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", details=("This is no longer a part of the proxy base, " "service-specific subclasses should expose " - "this as needed. See resource2.wait_for_delete " + "this as needed. See resource.wait_for_delete " "for this behavior")) def wait_for_delete(self, value, interval=2, wait=120): """Wait for the resource to be deleted. :param value: The resource to wait on to be deleted. - :type value: :class:`~openstack.resource2.Resource` + :type value: :class:`~openstack.resource.Resource` :param interval: Number of seconds to wait between checks. :param wait: Maximum number of seconds to wait for the delete. @@ -311,4 +311,4 @@ def wait_for_delete(self, value, interval=2, wait=120): :raises: :class:`~openstack.exceptions.ResourceTimeout` transition to delete failed to occur in wait seconds. """ - return resource2.wait_for_delete(self, value, interval, wait) + return resource.wait_for_delete(self, value, interval, wait) diff --git a/openstack/resource2.py b/openstack/resource.py similarity index 98% rename from openstack/resource2.py rename to openstack/resource.py index 8d45efa4f..2d992b171 100644 --- a/openstack/resource2.py +++ b/openstack/resource.py @@ -15,11 +15,11 @@ class that represent a remote resource. The attributes that comprise a request or response for this resource are specified as class members on the Resource subclass where their values -are of a component type, including :class:`~openstack.resource2.Body`, -:class:`~openstack.resource2.Header`, and :class:`~openstack.resource2.URI`. +are of a component type, including :class:`~openstack.resource.Body`, +:class:`~openstack.resource.Header`, and :class:`~openstack.resource.URI`. -For update management, :class:`~openstack.resource2.Resource` employs -a series of :class:`~openstack.resource2._ComponentManager` instances +For update management, :class:`~openstack.resource.Resource` employs +a series of :class:`~openstack.resource._ComponentManager` instances to look after the attributes of that particular component type. This is particularly useful for Body and Header types, so that only the values necessary are sent in requests to the server. @@ -296,8 +296,8 @@ def __init__(self, _synchronized=False, **attrs): """The base resource :param bool _synchronized: This is not intended to be used directly. - See :meth:`~openstack.resource2.Resource.new` and - :meth:`~openstack.resource2.Resource.existing`. + See :meth:`~openstack.resource.Resource.new` and + :meth:`~openstack.resource.Resource.existing`. """ # NOTE: _collect_attrs modifies **attrs in place, removing @@ -532,9 +532,9 @@ def existing(cls, **kwargs): def to_dict(self, body=True, headers=True, ignore_none=False): """Return a dictionary of this resource's contents - :param bool body: Include the :class:`~openstack.resource2.Body` + :param bool body: Include the :class:`~openstack.resource.Body` attributes in the returned dictionary. - :param bool headers: Include the :class:`~openstack.resource2.Header` + :param bool headers: Include the :class:`~openstack.resource.Header` attributes in the returned dictionary. :param bool ignore_none: When True, exclude key/value pairs where the value is None. This will exclude @@ -807,12 +807,12 @@ def list(cls, session, paginated=False, **params): page of data will be returned regardless of the API's support of pagination.** :param dict params: These keyword arguments are passed through the - :meth:`~openstack.resource2.QueryParamter._transpose` method + :meth:`~openstack.resource.QueryParamter._transpose` method to find if any of them match expected query parameters to be sent in the *params* argument to :meth:`~keystoneauth1.adapter.Adapter.get`. They are additionally checked against the - :data:`~openstack.resource2.Resource.base_path` format string + :data:`~openstack.resource.Resource.base_path` format string to see if any path fragments need to be filled in by the contents of this argument. @@ -964,7 +964,7 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): attempting to find a nonexistent resource. :param dict params: Any additional parameters to be passed into underlying methods, such as to - :meth:`~openstack.resource2.Resource.existing` + :meth:`~openstack.resource.Resource.existing` in order to pass on URI parameters. :return: The :class:`Resource` object matching the given name or id diff --git a/openstack/service_description.py b/openstack/service_description.py index 80e5eeaaf..c141318e9 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -22,7 +22,7 @@ import os_service_types from openstack import _log -from openstack import proxy2 +from openstack import proxy _logger = _log.setup_logging('openstack') _service_type_manager = os_service_types.ServiceTypes() @@ -51,15 +51,11 @@ def _get_all_types(service_type, aliases=None): class ServiceDescription(object): #: Proxy class for this service - proxy_class = proxy2.BaseProxy + proxy_class = proxy.BaseProxy #: main service_type to use to find this service in the catalog service_type = None #: list of aliases this service might be registered as aliases = [] - #: Internal temporary flag to control whether or not a warning is - #: emitted for use of old Proxy class. In-tree things should not - #: emit a warning - but out of tree things should only use Proxy2. - _warn_if_old = True def __init__(self, service_type, proxy_class=None, aliases=None): """Class describing how to interact with a REST service. @@ -80,10 +76,10 @@ def __init__(self, service_type, proxy_class=None, aliases=None): :param string service_type: service_type to look for in the keystone catalog - :param proxy2.BaseProxy proxy_class: - subclass of :class:`~openstack.proxy2.BaseProxy` implementing + :param proxy.BaseProxy proxy_class: + subclass of :class:`~openstack.proxy.BaseProxy` implementing an interface for this service. Defaults to - :class:`~openstack.proxy2.BaseProxy` which provides REST operations + :class:`~openstack.proxy.BaseProxy` which provides REST operations but no additional features. :param list aliases: Optional list of aliases, if there is more than one name that might @@ -96,7 +92,7 @@ def __init__(self, service_type, proxy_class=None, aliases=None): self._validate_proxy_class() def _validate_proxy_class(self): - if not issubclass(self.proxy_class, proxy2.BaseProxy): + if not issubclass(self.proxy_class, proxy.BaseProxy): raise TypeError( "{module}.{proxy_class} must inherit from BaseProxy".format( module=self.proxy_class.__module__, @@ -105,9 +101,6 @@ def _validate_proxy_class(self): class OpenStackServiceDescription(ServiceDescription): - #: Override _warn_if_old so we don't spam people with warnings - _warn_if_old = False - def __init__(self, service, config): """Official OpenStack ServiceDescription. diff --git a/openstack/tests/unit/baremetal/v1/test_proxy.py b/openstack/tests/unit/baremetal/v1/test_proxy.py index d1bb49c16..4d874e89c 100644 --- a/openstack/tests/unit/baremetal/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal/v1/test_proxy.py @@ -18,10 +18,10 @@ from openstack.baremetal.v1 import node from openstack.baremetal.v1 import port from openstack.baremetal.v1 import port_group -from openstack.tests.unit import test_proxy_base2 +from openstack.tests.unit import test_proxy_base -class TestBaremetalProxy(test_proxy_base2.TestProxyBase): +class TestBaremetalProxy(test_proxy_base.TestProxyBase): def setUp(self): super(TestBaremetalProxy, self).setUp() diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index ac616e89d..e7b7fe721 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -15,10 +15,10 @@ from openstack.block_storage.v2 import stats from openstack.block_storage.v2 import type from openstack.block_storage.v2 import volume -from openstack.tests.unit import test_proxy_base2 +from openstack.tests.unit import test_proxy_base -class TestVolumeProxy(test_proxy_base2.TestProxyBase): +class TestVolumeProxy(test_proxy_base.TestProxyBase): def setUp(self): super(TestVolumeProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) diff --git a/openstack/tests/unit/cluster/v1/test_proxy.py b/openstack/tests/unit/cluster/v1/test_proxy.py index 592509b1f..10b60b747 100644 --- a/openstack/tests/unit/cluster/v1/test_proxy.py +++ b/openstack/tests/unit/cluster/v1/test_proxy.py @@ -27,11 +27,11 @@ from openstack.clustering.v1 import profile_type from openstack.clustering.v1 import receiver from openstack.clustering.v1 import service -from openstack import proxy2 as proxy_base -from openstack.tests.unit import test_proxy_base2 +from openstack import proxy as proxy_base +from openstack.tests.unit import test_proxy_base -class TestClusterProxy(test_proxy_base2.TestProxyBase): +class TestClusterProxy(test_proxy_base.TestProxyBase): def setUp(self): super(TestClusterProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) @@ -366,7 +366,7 @@ def test_node_get(self): self.verify_get(self.proxy.get_node, node.Node) def test_node_get_with_details(self): - self._verify2('openstack.proxy2.BaseProxy._get', + self._verify2('openstack.proxy.BaseProxy._get', self.proxy.get_node, method_args=['NODE_ID'], method_kwargs={'details': True}, @@ -472,7 +472,7 @@ def test_get_cluster_policy(self): fake_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') # ClusterPolicy object as input - self._verify2('openstack.proxy2.BaseProxy._get', + self._verify2('openstack.proxy.BaseProxy._get', self.proxy.get_cluster_policy, method_args=[fake_policy, "FAKE_CLUSTER"], expected_args=[cluster_policy.ClusterPolicy, @@ -481,7 +481,7 @@ def test_get_cluster_policy(self): expected_result=fake_policy) # Policy ID as input - self._verify2('openstack.proxy2.BaseProxy._get', + self._verify2('openstack.proxy.BaseProxy._get', self.proxy.get_cluster_policy, method_args=["FAKE_POLICY", "FAKE_CLUSTER"], expected_args=[cluster_policy.ClusterPolicy, @@ -489,7 +489,7 @@ def test_get_cluster_policy(self): expected_kwargs={"cluster_id": "FAKE_CLUSTER"}) # Cluster object as input - self._verify2('openstack.proxy2.BaseProxy._get', + self._verify2('openstack.proxy.BaseProxy._get', self.proxy.get_cluster_policy, method_args=["FAKE_POLICY", fake_cluster], expected_args=[cluster_policy.ClusterPolicy, @@ -539,7 +539,7 @@ def test_events(self): method_kwargs={'limit': 2}, expected_kwargs={'limit': 2}) - @mock.patch("openstack.resource2.wait_for_status") + @mock.patch("openstack.resource.wait_for_status") def test_wait_for(self, mock_wait): mock_resource = mock.Mock() mock_wait.return_value = mock_resource @@ -549,7 +549,7 @@ def test_wait_for(self, mock_wait): mock_wait.assert_called_once_with(self.proxy, mock_resource, 'ACTIVE', [], 2, 120) - @mock.patch("openstack.resource2.wait_for_status") + @mock.patch("openstack.resource.wait_for_status") def test_wait_for_params(self, mock_wait): mock_resource = mock.Mock() mock_wait.return_value = mock_resource @@ -559,7 +559,7 @@ def test_wait_for_params(self, mock_wait): mock_wait.assert_called_once_with(self.proxy, mock_resource, 'ACTIVE', ['ERROR'], 1, 2) - @mock.patch("openstack.resource2.wait_for_delete") + @mock.patch("openstack.resource.wait_for_delete") def test_wait_for_delete(self, mock_wait): mock_resource = mock.Mock() mock_wait.return_value = mock_resource @@ -568,7 +568,7 @@ def test_wait_for_delete(self, mock_wait): mock_wait.assert_called_once_with(self.proxy, mock_resource, 2, 120) - @mock.patch("openstack.resource2.wait_for_delete") + @mock.patch("openstack.resource.wait_for_delete") def test_wait_for_delete_params(self, mock_wait): mock_resource = mock.Mock() mock_wait.return_value = mock_resource diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 39a971e6d..f0c192708 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -23,10 +23,10 @@ from openstack.compute.v2 import server_interface from openstack.compute.v2 import server_ip from openstack.compute.v2 import service -from openstack.tests.unit import test_proxy_base2 +from openstack.tests.unit import test_proxy_base -class TestComputeProxy(test_proxy_base2.TestProxyBase): +class TestComputeProxy(test_proxy_base.TestProxyBase): def setUp(self): super(TestComputeProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) @@ -126,7 +126,7 @@ def test_server_interface_delete(self): test_interface.server_id = server_id # Case1: ServerInterface instance is provided as value - self._verify2("openstack.proxy2.BaseProxy._delete", + self._verify2("openstack.proxy.BaseProxy._delete", self.proxy.delete_server_interface, method_args=[test_interface], method_kwargs={"server": server_id}, @@ -136,7 +136,7 @@ def test_server_interface_delete(self): "ignore_missing": True}) # Case2: ServerInterface ID is provided as value - self._verify2("openstack.proxy2.BaseProxy._delete", + self._verify2("openstack.proxy.BaseProxy._delete", self.proxy.delete_server_interface, method_args=[interface_id], method_kwargs={"server": server_id}, @@ -163,7 +163,7 @@ def test_server_interface_get(self): test_interface.server_id = server_id # Case1: ServerInterface instance is provided as value - self._verify2('openstack.proxy2.BaseProxy._get', + self._verify2('openstack.proxy.BaseProxy._get', self.proxy.get_server_interface, method_args=[test_interface], method_kwargs={"server": server_id}, @@ -172,7 +172,7 @@ def test_server_interface_get(self): "server_id": server_id}) # Case2: ServerInterface ID is provided as value - self._verify2('openstack.proxy2.BaseProxy._get', + self._verify2('openstack.proxy.BaseProxy._get', self.proxy.get_server_interface, method_args=[interface_id], method_kwargs={"server": server_id}, diff --git a/openstack/tests/unit/database/v1/test_proxy.py b/openstack/tests/unit/database/v1/test_proxy.py index ff54e5b20..516340f18 100644 --- a/openstack/tests/unit/database/v1/test_proxy.py +++ b/openstack/tests/unit/database/v1/test_proxy.py @@ -15,10 +15,10 @@ from openstack.database.v1 import flavor from openstack.database.v1 import instance from openstack.database.v1 import user -from openstack.tests.unit import test_proxy_base2 +from openstack.tests.unit import test_proxy_base -class TestDatabaseProxy(test_proxy_base2.TestProxyBase): +class TestDatabaseProxy(test_proxy_base.TestProxyBase): def setUp(self): super(TestDatabaseProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) @@ -41,7 +41,7 @@ def test_database_delete_ignore(self): expected_path_args={"instance_id": "test_id"}) def test_database_find(self): - self._verify2('openstack.proxy2.BaseProxy._find', + self._verify2('openstack.proxy.BaseProxy._find', self.proxy.find_database, method_args=["db", "instance"], expected_args=[database.Database, "db"], @@ -106,7 +106,7 @@ def test_user_delete_ignore(self): expected_path_args={"instance_id": "id"}) def test_user_find(self): - self._verify2('openstack.proxy2.BaseProxy._find', + self._verify2('openstack.proxy.BaseProxy._find', self.proxy.find_user, method_args=["user", "instance"], expected_args=[user.User, "user"], diff --git a/openstack/tests/unit/identity/v2/test_proxy.py b/openstack/tests/unit/identity/v2/test_proxy.py index cbca1fd7b..6e8ba830a 100644 --- a/openstack/tests/unit/identity/v2/test_proxy.py +++ b/openstack/tests/unit/identity/v2/test_proxy.py @@ -14,7 +14,7 @@ from openstack.identity.v2 import role from openstack.identity.v2 import tenant from openstack.identity.v2 import user -from openstack.tests.unit import test_proxy_base2 as test_proxy_base +from openstack.tests.unit import test_proxy_base as test_proxy_base class TestIdentityProxy(test_proxy_base.TestProxyBase): diff --git a/openstack/tests/unit/identity/v3/test_proxy.py b/openstack/tests/unit/identity/v3/test_proxy.py index aff747264..4fd0227b3 100644 --- a/openstack/tests/unit/identity/v3/test_proxy.py +++ b/openstack/tests/unit/identity/v3/test_proxy.py @@ -22,10 +22,10 @@ from openstack.identity.v3 import service from openstack.identity.v3 import trust from openstack.identity.v3 import user -from openstack.tests.unit import test_proxy_base2 +from openstack.tests.unit import test_proxy_base -class TestIdentityProxy(test_proxy_base2.TestProxyBase): +class TestIdentityProxy(test_proxy_base.TestProxyBase): def setUp(self): super(TestIdentityProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) diff --git a/openstack/tests/unit/image/v1/test_proxy.py b/openstack/tests/unit/image/v1/test_proxy.py index e0dbd679f..741770028 100644 --- a/openstack/tests/unit/image/v1/test_proxy.py +++ b/openstack/tests/unit/image/v1/test_proxy.py @@ -12,7 +12,7 @@ from openstack.image.v1 import _proxy from openstack.image.v1 import image -from openstack.tests.unit import test_proxy_base2 as test_proxy_base +from openstack.tests.unit import test_proxy_base as test_proxy_base class TestImageProxy(test_proxy_base.TestProxyBase): diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 0fafcd927..4b6e66ad2 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -17,12 +17,12 @@ from openstack.image.v2 import image from openstack.image.v2 import member from openstack.tests.unit.image.v2 import test_image as fake_image -from openstack.tests.unit import test_proxy_base2 +from openstack.tests.unit import test_proxy_base EXAMPLE = fake_image.EXAMPLE -class TestImageProxy(test_proxy_base2.TestProxyBase): +class TestImageProxy(test_proxy_base.TestProxyBase): def setUp(self): super(TestImageProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) @@ -56,8 +56,8 @@ def test_image_delete(self): def test_image_delete_ignore(self): self.verify_delete(self.proxy.delete_image, image.Image, True) - @mock.patch("openstack.resource2.Resource._translate_response") - @mock.patch("openstack.proxy2.BaseProxy._get") + @mock.patch("openstack.resource.Resource._translate_response") + @mock.patch("openstack.proxy.BaseProxy._get") @mock.patch("openstack.image.v2.image.Image.update") def test_image_update(self, mock_update_image, mock_get_image, mock_transpose): @@ -104,7 +104,7 @@ def test_member_create(self): expected_kwargs={"image_id": "test_id"}) def test_member_delete(self): - self._verify2("openstack.proxy2.BaseProxy._delete", + self._verify2("openstack.proxy.BaseProxy._delete", self.proxy.remove_member, method_args=["member_id"], method_kwargs={"image": "image_id", @@ -115,7 +115,7 @@ def test_member_delete(self): "ignore_missing": False}) def test_member_delete_ignore(self): - self._verify2("openstack.proxy2.BaseProxy._delete", + self._verify2("openstack.proxy.BaseProxy._delete", self.proxy.remove_member, method_args=["member_id"], method_kwargs={"image": "image_id"}, @@ -125,7 +125,7 @@ def test_member_delete_ignore(self): "ignore_missing": True}) def test_member_update(self): - self._verify2("openstack.proxy2.BaseProxy._update", + self._verify2("openstack.proxy.BaseProxy._update", self.proxy.update_member, method_args=['member_id', 'image_id'], expected_args=[member.Member], @@ -133,7 +133,7 @@ def test_member_update(self): 'image_id': 'image_id'}) def test_member_get(self): - self._verify2("openstack.proxy2.BaseProxy._get", + self._verify2("openstack.proxy.BaseProxy._get", self.proxy.get_member, method_args=['member_id'], method_kwargs={"image": "image_id"}, @@ -142,7 +142,7 @@ def test_member_get(self): 'image_id': 'image_id'}) def test_member_find(self): - self._verify2("openstack.proxy2.BaseProxy._find", + self._verify2("openstack.proxy.BaseProxy._find", self.proxy.find_member, method_args=['member_id'], method_kwargs={"image": "image_id"}, diff --git a/openstack/tests/unit/key_manager/v1/test_proxy.py b/openstack/tests/unit/key_manager/v1/test_proxy.py index e3652fb0e..ed3f9250a 100644 --- a/openstack/tests/unit/key_manager/v1/test_proxy.py +++ b/openstack/tests/unit/key_manager/v1/test_proxy.py @@ -14,10 +14,10 @@ from openstack.key_manager.v1 import container from openstack.key_manager.v1 import order from openstack.key_manager.v1 import secret -from openstack.tests.unit import test_proxy_base2 +from openstack.tests.unit import test_proxy_base -class TestKeyManagerProxy(test_proxy_base2.TestProxyBase): +class TestKeyManagerProxy(test_proxy_base.TestProxyBase): def setUp(self): super(TestKeyManagerProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) diff --git a/openstack/tests/unit/load_balancer/test_proxy.py b/openstack/tests/unit/load_balancer/test_proxy.py index 4db9ad239..efa32031e 100644 --- a/openstack/tests/unit/load_balancer/test_proxy.py +++ b/openstack/tests/unit/load_balancer/test_proxy.py @@ -20,10 +20,10 @@ from openstack.load_balancer.v2 import load_balancer as lb from openstack.load_balancer.v2 import member from openstack.load_balancer.v2 import pool -from openstack.tests.unit import test_proxy_base2 +from openstack.tests.unit import test_proxy_base -class TestLoadBalancerProxy(test_proxy_base2.TestProxyBase): +class TestLoadBalancerProxy(test_proxy_base.TestProxyBase): POOL_ID = uuid.uuid4() L7_POLICY_ID = uuid.uuid4() @@ -134,7 +134,7 @@ def test_member_delete(self): expected_kwargs={'pool_id': self.POOL_ID}) def test_member_find(self): - self._verify2('openstack.proxy2.BaseProxy._find', + self._verify2('openstack.proxy.BaseProxy._find', self.proxy.find_member, method_args=["MEMBER", self.POOL_ID], expected_args=[member.Member, "MEMBER"], @@ -142,7 +142,7 @@ def test_member_find(self): "ignore_missing": True}) def test_member_update(self): - self._verify2('openstack.proxy2.BaseProxy._update', + self._verify2('openstack.proxy.BaseProxy._update', self.proxy.update_member, method_args=["MEMBER", self.POOL_ID], expected_args=[member.Member, "MEMBER"], @@ -225,7 +225,7 @@ def test_l7_rule_delete(self): expected_kwargs={'l7policy_id': self.L7_POLICY_ID}) def test_l7_rule_find(self): - self._verify2('openstack.proxy2.BaseProxy._find', + self._verify2('openstack.proxy.BaseProxy._find', self.proxy.find_l7_rule, method_args=["RULE", self.L7_POLICY_ID], expected_args=[l7_rule.L7Rule, "RULE"], @@ -233,7 +233,7 @@ def test_l7_rule_find(self): "ignore_missing": True}) def test_l7_rule_update(self): - self._verify2('openstack.proxy2.BaseProxy._update', + self._verify2('openstack.proxy.BaseProxy._update', self.proxy.update_l7_rule, method_args=["RULE", self.L7_POLICY_ID], expected_args=[l7_rule.L7Rule, "RULE"], diff --git a/openstack/tests/unit/message/v2/test_proxy.py b/openstack/tests/unit/message/v2/test_proxy.py index 75a602c56..d41165597 100644 --- a/openstack/tests/unit/message/v2/test_proxy.py +++ b/openstack/tests/unit/message/v2/test_proxy.py @@ -17,13 +17,13 @@ from openstack.message.v2 import message from openstack.message.v2 import queue from openstack.message.v2 import subscription -from openstack import proxy2 as proxy_base -from openstack.tests.unit import test_proxy_base2 +from openstack import proxy as proxy_base +from openstack.tests.unit import test_proxy_base QUEUE_NAME = 'test_queue' -class TestMessageProxy(test_proxy_base2.TestProxyBase): +class TestMessageProxy(test_proxy_base.TestProxyBase): def setUp(self): super(TestMessageProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) @@ -57,7 +57,7 @@ def test_message_post(self, mock_get_resource): @mock.patch.object(proxy_base.BaseProxy, '_get_resource') def test_message_get(self, mock_get_resource): mock_get_resource.return_value = "resource_or_id" - self._verify2("openstack.proxy2.BaseProxy._get", + self._verify2("openstack.proxy.BaseProxy._get", self.proxy.get_message, method_args=["test_queue", "resource_or_id"], expected_args=[message.Message, "resource_or_id"]) @@ -75,7 +75,7 @@ def test_message_delete(self, mock_get_resource): fake_message = mock.Mock() fake_message.id = "message_id" mock_get_resource.return_value = fake_message - self._verify2("openstack.proxy2.BaseProxy._delete", + self._verify2("openstack.proxy.BaseProxy._delete", self.proxy.delete_message, method_args=["test_queue", "resource_or_id", None, False], @@ -92,7 +92,7 @@ def test_message_delete_claimed(self, mock_get_resource): fake_message = mock.Mock() fake_message.id = "message_id" mock_get_resource.return_value = fake_message - self._verify2("openstack.proxy2.BaseProxy._delete", + self._verify2("openstack.proxy.BaseProxy._delete", self.proxy.delete_message, method_args=["test_queue", "resource_or_id", "claim_id", False], @@ -109,7 +109,7 @@ def test_message_delete_ignore(self, mock_get_resource): fake_message = mock.Mock() fake_message.id = "message_id" mock_get_resource.return_value = fake_message - self._verify2("openstack.proxy2.BaseProxy._delete", + self._verify2("openstack.proxy.BaseProxy._delete", self.proxy.delete_message, method_args=["test_queue", "resource_or_id", None, True], @@ -129,7 +129,7 @@ def test_subscription_create(self): @mock.patch.object(proxy_base.BaseProxy, '_get_resource') def test_subscription_get(self, mock_get_resource): mock_get_resource.return_value = "resource_or_id" - self._verify2("openstack.proxy2.BaseProxy._get", + self._verify2("openstack.proxy.BaseProxy._get", self.proxy.get_subscription, method_args=["test_queue", "resource_or_id"], expected_args=[subscription.Subscription, @@ -169,7 +169,7 @@ def test_claim_create(self): method_args=["test_queue"]) def test_claim_get(self): - self._verify2("openstack.proxy2.BaseProxy._get", + self._verify2("openstack.proxy.BaseProxy._get", self.proxy.get_claim, method_args=["test_queue", "resource_or_id"], expected_args=[claim.Claim, @@ -177,7 +177,7 @@ def test_claim_get(self): expected_kwargs={"queue_name": "test_queue"}) def test_claim_update(self): - self._verify2("openstack.proxy2.BaseProxy._update", + self._verify2("openstack.proxy.BaseProxy._update", self.proxy.update_claim, method_args=["test_queue", "resource_or_id"], method_kwargs={"k1": "v1"}, diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index abfe01a46..f548a80a0 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -49,8 +49,8 @@ from openstack.network.v2 import subnet from openstack.network.v2 import subnet_pool from openstack.network.v2 import vpn_service -from openstack import proxy2 as proxy_base2 -from openstack.tests.unit import test_proxy_base2 +from openstack import proxy as proxy_base +from openstack.tests.unit import test_proxy_base QOS_POLICY_ID = 'qos-policy-id-' + uuid.uuid4().hex @@ -60,7 +60,7 @@ ROUTER_ID = 'router-id-' + uuid.uuid4().hex -class TestNetworkProxy(test_proxy_base2.TestProxyBase): +class TestNetworkProxy(test_proxy_base.TestProxyBase): def setUp(self): super(TestNetworkProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) @@ -316,7 +316,7 @@ def test_network_find(self): self.verify_find(self.proxy.find_network, network.Network) def test_network_find_with_filter(self): - self._verify2('openstack.proxy2.BaseProxy._find', + self._verify2('openstack.proxy.BaseProxy._find', self.proxy.find_network, method_args=["net1"], method_kwargs={"project_id": "1"}, @@ -406,7 +406,7 @@ def test_pool_member_delete_ignore(self): {"pool": "test_id"}, {"pool_id": "test_id"}) def test_pool_member_find(self): - self._verify2('openstack.proxy2.BaseProxy._find', + self._verify2('openstack.proxy.BaseProxy._find', self.proxy.find_pool_member, method_args=["MEMBER", "POOL"], expected_args=[pool_member.PoolMember, "MEMBER"], @@ -414,7 +414,7 @@ def test_pool_member_find(self): "ignore_missing": True}) def test_pool_member_get(self): - self._verify2('openstack.proxy2.BaseProxy._get', + self._verify2('openstack.proxy.BaseProxy._get', self.proxy.get_pool_member, method_args=["MEMBER", "POOL"], expected_args=[pool_member.PoolMember, "MEMBER"], @@ -426,7 +426,7 @@ def test_pool_members(self): expected_kwargs={"pool_id": "test_id"}) def test_pool_member_update(self): - self._verify2("openstack.proxy2.BaseProxy._update", + self._verify2("openstack.proxy.BaseProxy._update", self.proxy.update_pool_member, method_args=["MEMBER", "POOL"], expected_args=[pool_member.PoolMember, "MEMBER"], @@ -497,7 +497,7 @@ def test_qos_bandwidth_limit_rule_delete_ignore(self): def test_qos_bandwidth_limit_rule_find(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) - self._verify2('openstack.proxy2.BaseProxy._find', + self._verify2('openstack.proxy.BaseProxy._find', self.proxy.find_qos_bandwidth_limit_rule, method_args=['rule_id', policy], expected_args=[ @@ -523,7 +523,7 @@ def test_qos_bandwidth_limit_rules(self): def test_qos_bandwidth_limit_rule_update(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) - self._verify2('openstack.proxy2.BaseProxy._update', + self._verify2('openstack.proxy.BaseProxy._update', self.proxy.update_qos_bandwidth_limit_rule, method_args=['rule_id', policy], method_kwargs={'foo': 'bar'}, @@ -556,7 +556,7 @@ def test_qos_dscp_marking_rule_delete_ignore(self): def test_qos_dscp_marking_rule_find(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) - self._verify2('openstack.proxy2.BaseProxy._find', + self._verify2('openstack.proxy.BaseProxy._find', self.proxy.find_qos_dscp_marking_rule, method_args=['rule_id', policy], expected_args=[qos_dscp_marking_rule.QoSDSCPMarkingRule, @@ -581,7 +581,7 @@ def test_qos_dscp_marking_rules(self): def test_qos_dscp_marking_rule_update(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) - self._verify2('openstack.proxy2.BaseProxy._update', + self._verify2('openstack.proxy.BaseProxy._update', self.proxy.update_qos_dscp_marking_rule, method_args=['rule_id', policy], method_kwargs={'foo': 'bar'}, @@ -614,7 +614,7 @@ def test_qos_minimum_bandwidth_rule_delete_ignore(self): def test_qos_minimum_bandwidth_rule_find(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) - self._verify2('openstack.proxy2.BaseProxy._find', + self._verify2('openstack.proxy.BaseProxy._find', self.proxy.find_qos_minimum_bandwidth_rule, method_args=['rule_id', policy], expected_args=[ @@ -640,7 +640,7 @@ def test_qos_minimum_bandwidth_rules(self): def test_qos_minimum_bandwidth_rule_update(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) - self._verify2('openstack.proxy2.BaseProxy._update', + self._verify2('openstack.proxy.BaseProxy._update', self.proxy.update_qos_minimum_bandwidth_rule, method_args=['rule_id', policy], method_kwargs={'foo': 'bar'}, @@ -695,11 +695,11 @@ def test_quota_delete_ignore(self): def test_quota_get(self): self.verify_get(self.proxy.get_quota, quota.Quota) - @mock.patch.object(proxy_base2.BaseProxy, "_get_resource") + @mock.patch.object(proxy_base.BaseProxy, "_get_resource") def test_quota_get_details(self, mock_get): fake_quota = mock.Mock(project_id='PROJECT') mock_get.return_value = fake_quota - self._verify2("openstack.proxy2.BaseProxy._get", + self._verify2("openstack.proxy.BaseProxy._get", self.proxy.get_quota, method_args=['QUOTA_ID'], method_kwargs={'details': True}, @@ -708,11 +708,11 @@ def test_quota_get_details(self, mock_get): 'requires_id': False}) mock_get.assert_called_once_with(quota.Quota, 'QUOTA_ID') - @mock.patch.object(proxy_base2.BaseProxy, "_get_resource") + @mock.patch.object(proxy_base.BaseProxy, "_get_resource") def test_quota_default_get(self, mock_get): fake_quota = mock.Mock(project_id='PROJECT') mock_get.return_value = fake_quota - self._verify2("openstack.proxy2.BaseProxy._get", + self._verify2("openstack.proxy.BaseProxy._get", self.proxy.get_quota_default, method_args=['QUOTA_ID'], expected_args=[quota.QuotaDefault], @@ -773,7 +773,7 @@ def test_routers(self): def test_router_update(self): self.verify_update(self.proxy.update_router, router.Router) - @mock.patch.object(proxy_base2.BaseProxy, '_get_resource') + @mock.patch.object(proxy_base.BaseProxy, '_get_resource') @mock.patch.object(router.Router, 'add_interface') def test_add_interface_to_router_with_port(self, mock_add_interface, mock_get): @@ -787,7 +787,7 @@ def test_add_interface_to_router_with_port(self, mock_add_interface, expected_kwargs={"port_id": "PORT"}) mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") - @mock.patch.object(proxy_base2.BaseProxy, '_get_resource') + @mock.patch.object(proxy_base.BaseProxy, '_get_resource') @mock.patch.object(router.Router, 'add_interface') def test_add_interface_to_router_with_subnet(self, mock_add_interface, mock_get): @@ -801,7 +801,7 @@ def test_add_interface_to_router_with_subnet(self, mock_add_interface, expected_kwargs={"subnet_id": "SUBNET"}) mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") - @mock.patch.object(proxy_base2.BaseProxy, '_get_resource') + @mock.patch.object(proxy_base.BaseProxy, '_get_resource') @mock.patch.object(router.Router, 'remove_interface') def test_remove_interface_from_router_with_port(self, mock_remove, mock_get): @@ -815,7 +815,7 @@ def test_remove_interface_from_router_with_port(self, mock_remove, expected_kwargs={"port_id": "PORT"}) mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") - @mock.patch.object(proxy_base2.BaseProxy, '_get_resource') + @mock.patch.object(proxy_base.BaseProxy, '_get_resource') @mock.patch.object(router.Router, 'remove_interface') def test_remove_interface_from_router_with_subnet(self, mock_remove, mock_get): @@ -829,7 +829,7 @@ def test_remove_interface_from_router_with_subnet(self, mock_remove, expected_kwargs={"subnet_id": "SUBNET"}) mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") - @mock.patch.object(proxy_base2.BaseProxy, '_get_resource') + @mock.patch.object(proxy_base.BaseProxy, '_get_resource') @mock.patch.object(router.Router, 'add_gateway') def test_add_gateway_to_router(self, mock_add, mock_get): x_router = router.Router.new(id="ROUTER_ID") @@ -842,7 +842,7 @@ def test_add_gateway_to_router(self, mock_add, mock_get): expected_kwargs={"foo": "bar"}) mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") - @mock.patch.object(proxy_base2.BaseProxy, '_get_resource') + @mock.patch.object(proxy_base.BaseProxy, '_get_resource') @mock.patch.object(router.Router, 'remove_gateway') def test_remove_gateway_from_router(self, mock_remove, mock_get): x_router = router.Router.new(id="ROUTER_ID") diff --git a/openstack/tests/unit/network/v2/test_quota.py b/openstack/tests/unit/network/v2/test_quota.py index 20f55effa..1f35281e5 100644 --- a/openstack/tests/unit/network/v2/test_quota.py +++ b/openstack/tests/unit/network/v2/test_quota.py @@ -13,7 +13,7 @@ import testtools from openstack.network.v2 import quota -from openstack import resource2 as resource +from openstack import resource IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/object_store/v1/test_proxy.py b/openstack/tests/unit/object_store/v1/test_proxy.py index 537f7b299..1b5266750 100644 --- a/openstack/tests/unit/object_store/v1/test_proxy.py +++ b/openstack/tests/unit/object_store/v1/test_proxy.py @@ -60,7 +60,7 @@ def _test_object_delete(self, ignore): "container": "name", } - self._verify2("openstack.proxy2.BaseProxy._delete", + self._verify2("openstack.proxy.BaseProxy._delete", self.proxy.delete_object, method_args=["resource"], method_kwargs=expected_kwargs, @@ -76,7 +76,7 @@ def test_object_delete_ignore(self): def test_object_create_attrs(self): kwargs = {"name": "test", "data": "data", "container": "name"} - self._verify2("openstack.proxy2.BaseProxy._create", + self._verify2("openstack.proxy.BaseProxy._create", self.proxy.upload_object, method_kwargs=kwargs, expected_args=[obj.Object], diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index 3289f5656..0193781af 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -23,10 +23,10 @@ from openstack.orchestration.v1 import stack_files from openstack.orchestration.v1 import stack_template from openstack.orchestration.v1 import template -from openstack.tests.unit import test_proxy_base2 +from openstack.tests.unit import test_proxy_base -class TestOrchestrationProxy(test_proxy_base2.TestProxyBase): +class TestOrchestrationProxy(test_proxy_base.TestProxyBase): def setUp(self): super(TestOrchestrationProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) @@ -84,7 +84,7 @@ def test_get_stack_environment_with_stack_identity(self, mock_find): stk = stack.Stack(id=stack_id, name=stack_name) mock_find.return_value = stk - self._verify2('openstack.proxy2.BaseProxy._get', + self._verify2('openstack.proxy.BaseProxy._get', self.proxy.get_stack_environment, method_args=['IDENTITY'], expected_args=[stack_environment.StackEnvironment], @@ -99,7 +99,7 @@ def test_get_stack_environment_with_stack_object(self): stack_name = 'test_stack' stk = stack.Stack(id=stack_id, name=stack_name) - self._verify2('openstack.proxy2.BaseProxy._get', + self._verify2('openstack.proxy.BaseProxy._get', self.proxy.get_stack_environment, method_args=[stk], expected_args=[stack_environment.StackEnvironment], @@ -142,7 +142,7 @@ def test_get_stack_template_with_stack_identity(self, mock_find): stk = stack.Stack(id=stack_id, name=stack_name) mock_find.return_value = stk - self._verify2('openstack.proxy2.BaseProxy._get', + self._verify2('openstack.proxy.BaseProxy._get', self.proxy.get_stack_template, method_args=['IDENTITY'], expected_args=[stack_template.StackTemplate], @@ -157,7 +157,7 @@ def test_get_stack_template_with_stack_object(self): stack_name = 'test_stack' stk = stack.Stack(id=stack_id, name=stack_name) - self._verify2('openstack.proxy2.BaseProxy._get', + self._verify2('openstack.proxy.BaseProxy._get', self.proxy.get_stack_template, method_args=[stk], expected_args=[stack_template.StackTemplate], diff --git a/openstack/tests/unit/orchestration/v1/test_stack.py b/openstack/tests/unit/orchestration/v1/test_stack.py index fc91d720d..802b07660 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack.py +++ b/openstack/tests/unit/orchestration/v1/test_stack.py @@ -16,7 +16,7 @@ from openstack import exceptions from openstack.orchestration.v1 import stack -from openstack import resource2 as resource +from openstack import resource FAKE_ID = 'ce8ae86c-9810-4cb1-8888-7fb53bc523bf' diff --git a/openstack/tests/unit/orchestration/v1/test_stack_files.py b/openstack/tests/unit/orchestration/v1/test_stack_files.py index fe2d47c4e..7e7aa6c80 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack_files.py +++ b/openstack/tests/unit/orchestration/v1/test_stack_files.py @@ -14,7 +14,7 @@ import testtools from openstack.orchestration.v1 import stack_files as sf -from openstack import resource2 as resource +from openstack import resource FAKE = { 'stack_id': 'ID', diff --git a/openstack/tests/unit/orchestration/v1/test_template.py b/openstack/tests/unit/orchestration/v1/test_template.py index 24d7a369d..c127d9cab 100644 --- a/openstack/tests/unit/orchestration/v1/test_template.py +++ b/openstack/tests/unit/orchestration/v1/test_template.py @@ -14,7 +14,7 @@ import testtools from openstack.orchestration.v1 import template -from openstack import resource2 as resource +from openstack import resource FAKE = { diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index d43c0d895..8f434965b 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -83,7 +83,7 @@ def test_session_provided(self): def test_create_session(self): conn = connection.Connection(cloud='sample') - self.assertEqual('openstack.proxy2', + self.assertEqual('openstack.proxy', conn.alarm.__class__.__module__) self.assertEqual('openstack.clustering.v1._proxy', conn.clustering.__class__.__module__) diff --git a/openstack/tests/unit/test_proxy2.py b/openstack/tests/unit/test_proxy.py similarity index 87% rename from openstack/tests/unit/test_proxy2.py rename to openstack/tests/unit/test_proxy.py index 6c9832d63..dc72a6cd2 100644 --- a/openstack/tests/unit/test_proxy2.py +++ b/openstack/tests/unit/test_proxy.py @@ -14,31 +14,31 @@ import testtools from openstack import exceptions -from openstack import proxy2 -from openstack import resource2 +from openstack import proxy +from openstack import resource -class DeleteableResource(resource2.Resource): +class DeleteableResource(resource.Resource): allow_delete = True -class UpdateableResource(resource2.Resource): +class UpdateableResource(resource.Resource): allow_update = True -class CreateableResource(resource2.Resource): +class CreateableResource(resource.Resource): allow_create = True -class RetrieveableResource(resource2.Resource): +class RetrieveableResource(resource.Resource): allow_retrieve = True -class ListableResource(resource2.Resource): +class ListableResource(resource.Resource): allow_list = True -class HeadableResource(resource2.Resource): +class HeadableResource(resource.Resource): allow_head = True @@ -53,43 +53,43 @@ def method(self, expected_type, value): self.sot = mock.Mock() self.sot.method = method - self.fake_proxy = proxy2.BaseProxy("session") + self.fake_proxy = proxy.BaseProxy("session") def _test_correct(self, value): - decorated = proxy2._check_resource(strict=False)(self.sot.method) - rv = decorated(self.sot, resource2.Resource, value) + decorated = proxy._check_resource(strict=False)(self.sot.method) + rv = decorated(self.sot, resource.Resource, value) self.assertEqual(value, rv) def test__check_resource_correct_resource(self): - res = resource2.Resource() + res = resource.Resource() self._test_correct(res) def test__check_resource_notstrict_id(self): self._test_correct("abc123-id") def test__check_resource_strict_id(self): - decorated = proxy2._check_resource(strict=True)(self.sot.method) + decorated = proxy._check_resource(strict=True)(self.sot.method) self.assertRaisesRegex(ValueError, "A Resource must be passed", - decorated, self.sot, resource2.Resource, + decorated, self.sot, resource.Resource, "this-is-not-a-resource") def test__check_resource_incorrect_resource(self): - class OneType(resource2.Resource): + class OneType(resource.Resource): pass - class AnotherType(resource2.Resource): + class AnotherType(resource.Resource): pass value = AnotherType() - decorated = proxy2._check_resource(strict=False)(self.sot.method) + decorated = proxy._check_resource(strict=False)(self.sot.method) self.assertRaisesRegex(ValueError, "Expected OneType but received AnotherType", decorated, self.sot, OneType, value) def test__get_uri_attribute_no_parent(self): - class Child(resource2.Resource): - something = resource2.Body("something") + class Child(resource.Resource): + something = resource.Body("something") attr = "something" value = "nothing" @@ -100,7 +100,7 @@ class Child(resource2.Resource): self.assertEqual(value, result) def test__get_uri_attribute_with_parent(self): - class Parent(resource2.Resource): + class Parent(resource.Resource): pass value = "nothing" @@ -112,7 +112,7 @@ class Parent(resource2.Resource): def test__get_resource_new(self): value = "hello" - fake_type = mock.Mock(spec=resource2.Resource) + fake_type = mock.Mock(spec=resource.Resource) fake_type.new = mock.Mock(return_value=value) attrs = {"first": "Brian", "last": "Curtin"} @@ -146,12 +146,12 @@ def new(cls, **kwargs): self.assertEqual(value, result) def test__get_resource_from_resource(self): - res = mock.Mock(spec=resource2.Resource) + res = mock.Mock(spec=resource.Resource) res._update = mock.Mock() attrs = {"first": "Brian", "last": "Curtin"} - result = self.fake_proxy._get_resource(resource2.Resource, + result = self.fake_proxy._get_resource(resource.Resource, res, **attrs) res._update.assert_called_once_with(**attrs) @@ -170,7 +170,7 @@ def setUp(self): self.res.id = self.fake_id self.res.delete = mock.Mock() - self.sot = proxy2.BaseProxy(self.session) + self.sot = proxy.BaseProxy(self.session) DeleteableResource.new = mock.Mock(return_value=self.res) def test_delete(self): @@ -227,7 +227,7 @@ def setUp(self): self.res = mock.Mock(spec=UpdateableResource) self.res.update = mock.Mock(return_value=self.fake_result) - self.sot = proxy2.BaseProxy(self.session) + self.sot = proxy.BaseProxy(self.session) self.attrs = {"x": 1, "y": 2, "z": 3} @@ -258,7 +258,7 @@ def setUp(self): self.res = mock.Mock(spec=CreateableResource) self.res.create = mock.Mock(return_value=self.fake_result) - self.sot = proxy2.BaseProxy(self.session) + self.sot = proxy.BaseProxy(self.session) def test_create_attributes(self): CreateableResource.new = mock.Mock(return_value=self.res) @@ -285,7 +285,7 @@ def setUp(self): self.res.id = self.fake_id self.res.get = mock.Mock(return_value=self.fake_result) - self.sot = proxy2.BaseProxy(self.session) + self.sot = proxy.BaseProxy(self.session) RetrieveableResource.new = mock.Mock(return_value=self.res) def test_get_resource(self): @@ -329,9 +329,9 @@ def setUp(self): self.session = mock.Mock() self.args = {"a": "A", "b": "B", "c": "C"} - self.fake_response = [resource2.Resource()] + self.fake_response = [resource.Resource()] - self.sot = proxy2.BaseProxy(self.session) + self.sot = proxy.BaseProxy(self.session) ListableResource.list = mock.Mock() ListableResource.list.return_value = self.fake_response @@ -363,7 +363,7 @@ def setUp(self): self.res.id = self.fake_id self.res.head = mock.Mock(return_value=self.fake_result) - self.sot = proxy2.BaseProxy(self.session) + self.sot = proxy.BaseProxy(self.session) HeadableResource.new = mock.Mock(return_value=self.res) def test_head_resource(self): @@ -386,9 +386,9 @@ def setUp(self): super(TestProxyWaits, self).setUp() self.session = mock.Mock() - self.sot = proxy2.BaseProxy(self.session) + self.sot = proxy.BaseProxy(self.session) - @mock.patch("openstack.resource2.wait_for_status") + @mock.patch("openstack.resource.wait_for_status") def test_wait_for(self, mock_wait): mock_resource = mock.Mock() mock_wait.return_value = mock_resource @@ -396,7 +396,7 @@ def test_wait_for(self, mock_wait): mock_wait.assert_called_once_with( self.sot, mock_resource, 'ACTIVE', [], 2, 120) - @mock.patch("openstack.resource2.wait_for_status") + @mock.patch("openstack.resource.wait_for_status") def test_wait_for_params(self, mock_wait): mock_resource = mock.Mock() mock_wait.return_value = mock_resource @@ -404,14 +404,14 @@ def test_wait_for_params(self, mock_wait): mock_wait.assert_called_once_with( self.sot, mock_resource, 'ACTIVE', ['ERROR'], 1, 2) - @mock.patch("openstack.resource2.wait_for_delete") + @mock.patch("openstack.resource.wait_for_delete") def test_wait_for_delete(self, mock_wait): mock_resource = mock.Mock() mock_wait.return_value = mock_resource self.sot.wait_for_delete(mock_resource) mock_wait.assert_called_once_with(self.sot, mock_resource, 2, 120) - @mock.patch("openstack.resource2.wait_for_delete") + @mock.patch("openstack.resource.wait_for_delete") def test_wait_for_delete_params(self, mock_wait): mock_resource = mock.Mock() mock_wait.return_value = mock_resource diff --git a/openstack/tests/unit/test_proxy_base.py b/openstack/tests/unit/test_proxy_base.py index 38cb1dd52..296d7670e 100644 --- a/openstack/tests/unit/test_proxy_base.py +++ b/openstack/tests/unit/test_proxy_base.py @@ -98,21 +98,26 @@ def verify_create(self, test_method, resource_type, def verify_delete(self, test_method, resource_type, ignore, input_path_args=None, expected_path_args=None, + method_kwargs=None, expected_args=None, + expected_kwargs=None, mock_method="openstack.proxy.BaseProxy._delete"): method_args = ["resource_or_id"] - method_kwargs = {"ignore_missing": ignore} + method_kwargs = method_kwargs or {} + method_kwargs["ignore_missing"] = ignore if isinstance(input_path_args, dict): for key in input_path_args: method_kwargs[key] = input_path_args[key] elif isinstance(input_path_args, list): method_args = input_path_args - expected_kwargs = {"ignore_missing": ignore} + expected_kwargs = expected_kwargs or {} + expected_kwargs["ignore_missing"] = ignore if expected_path_args: - expected_kwargs["path_args"] = expected_path_args + expected_kwargs.update(expected_path_args) + expected_args = expected_args or [resource_type, "resource_or_id"] self._verify2(mock_method, test_method, method_args=method_args, method_kwargs=method_kwargs, - expected_args=[resource_type, "resource_or_id"], + expected_args=expected_args, expected_kwargs=expected_kwargs) def verify_get(self, test_method, resource_type, value=None, args=None, @@ -179,9 +184,7 @@ def verify_list(self, test_method, resource_type, paginated=False, **kwargs): expected_kwargs = kwargs.pop("expected_kwargs", {}) expected_kwargs.update({"paginated": paginated}) - expected_kwargs['limit'] = 2 method_kwargs = kwargs.pop("method_kwargs", {}) - method_kwargs['limit'] = 2 self._verify2(mock_method, test_method, method_kwargs=method_kwargs, expected_args=[resource_type], diff --git a/openstack/tests/unit/test_proxy_base2.py b/openstack/tests/unit/test_proxy_base2.py index 24d300fa2..06d88ca40 100644 --- a/openstack/tests/unit/test_proxy_base2.py +++ b/openstack/tests/unit/test_proxy_base2.py @@ -87,7 +87,7 @@ def _verify2(self, mock_method, test_method, mocked.assert_called_with(test_method.__self__) def verify_create(self, test_method, resource_type, - mock_method="openstack.proxy2.BaseProxy._create", + mock_method="openstack.proxy.BaseProxy._create", expected_result="result", **kwargs): the_kwargs = {"x": 1, "y": 2, "z": 3} method_kwargs = kwargs.pop("method_kwargs", the_kwargs) @@ -105,7 +105,7 @@ def verify_delete(self, test_method, resource_type, ignore, input_path_args=None, expected_path_args=None, method_kwargs=None, expected_args=None, expected_kwargs=None, - mock_method="openstack.proxy2.BaseProxy._delete"): + mock_method="openstack.proxy.BaseProxy._delete"): method_args = ["resource_or_id"] method_kwargs = method_kwargs or {} method_kwargs["ignore_missing"] = ignore @@ -126,7 +126,7 @@ def verify_delete(self, test_method, resource_type, ignore, expected_kwargs=expected_kwargs) def verify_get(self, test_method, resource_type, value=None, args=None, - mock_method="openstack.proxy2.BaseProxy._get", + mock_method="openstack.proxy.BaseProxy._get", ignore_value=False, **kwargs): the_value = value if value is None: @@ -147,7 +147,7 @@ def verify_get(self, test_method, resource_type, value=None, args=None, expected_kwargs=expected_kwargs) def verify_head(self, test_method, resource_type, - mock_method="openstack.proxy2.BaseProxy._head", + mock_method="openstack.proxy.BaseProxy._head", value=None, **kwargs): the_value = [value] if value is not None else [] if self.kwargs_to_path_args: @@ -161,7 +161,7 @@ def verify_head(self, test_method, resource_type, expected_kwargs=expected_kwargs) def verify_find(self, test_method, resource_type, value=None, - mock_method="openstack.proxy2.BaseProxy._find", + mock_method="openstack.proxy.BaseProxy._find", path_args=None, **kwargs): method_args = value or ["name_or_id"] expected_kwargs = {} @@ -188,7 +188,7 @@ def verify_find(self, test_method, resource_type, value=None, **kwargs) def verify_list(self, test_method, resource_type, paginated=False, - mock_method="openstack.proxy2.BaseProxy._list", + mock_method="openstack.proxy.BaseProxy._list", **kwargs): expected_kwargs = kwargs.pop("expected_kwargs", {}) expected_kwargs.update({"paginated": paginated}) @@ -202,7 +202,7 @@ def verify_list(self, test_method, resource_type, paginated=False, def verify_list_no_kwargs(self, test_method, resource_type, paginated=False, - mock_method="openstack.proxy2.BaseProxy._list"): + mock_method="openstack.proxy.BaseProxy._list"): self._verify2(mock_method, test_method, method_kwargs={}, expected_args=[resource_type], @@ -210,7 +210,7 @@ def verify_list_no_kwargs(self, test_method, resource_type, expected_result=["result"]) def verify_update(self, test_method, resource_type, value=None, - mock_method="openstack.proxy2.BaseProxy._update", + mock_method="openstack.proxy.BaseProxy._update", expected_result="result", path_args=None, **kwargs): method_args = value or ["resource_or_id"] method_kwargs = {"x": 1, "y": 2, "z": 3} @@ -230,5 +230,5 @@ def verify_update(self, test_method, resource_type, value=None, def verify_wait_for_status( self, test_method, - mock_method="openstack.resource2.wait_for_status", **kwargs): + mock_method="openstack.resource.wait_for_status", **kwargs): self._verify(mock_method, test_method, **kwargs) diff --git a/openstack/tests/unit/test_resource2.py b/openstack/tests/unit/test_resource.py similarity index 86% rename from openstack/tests/unit/test_resource2.py rename to openstack/tests/unit/test_resource.py index 8ea53d35b..ededc7e39 100644 --- a/openstack/tests/unit/test_resource2.py +++ b/openstack/tests/unit/test_resource.py @@ -19,7 +19,7 @@ from openstack import exceptions from openstack import format -from openstack import resource2 +from openstack import resource from openstack.tests.unit import base @@ -36,7 +36,7 @@ def json(self): class TestComponent(base.TestCase): - class ExampleComponent(resource2._BaseComponent): + class ExampleComponent(resource._BaseComponent): key = "_example" # Since we're testing ExampleComponent, which is as isolated as we @@ -47,13 +47,13 @@ class ExampleComponent(resource2._BaseComponent): # keys and values to test against. def test_implementations(self): - self.assertEqual("_body", resource2.Body.key) - self.assertEqual("_header", resource2.Header.key) - self.assertEqual("_uri", resource2.URI.key) + self.assertEqual("_body", resource.Body.key) + self.assertEqual("_header", resource.Header.key) + self.assertEqual("_uri", resource.URI.key) def test_creation(self): - sot = resource2._BaseComponent("name", type=int, default=1, - alternate_id=True) + sot = resource._BaseComponent( + "name", type=int, default=1, alternate_id=True) self.assertEqual("name", sot.name) self.assertEqual(int, sot.type) @@ -61,7 +61,7 @@ def test_creation(self): self.assertTrue(sot.alternate_id) def test_get_no_instance(self): - sot = resource2._BaseComponent("test") + sot = resource._BaseComponent("test") # Test that we short-circuit everything when given no instance. result = sot.__get__(None, None) @@ -248,7 +248,7 @@ class Parent(object): class TestComponentManager(base.TestCase): def test_create_basic(self): - sot = resource2._ComponentManager() + sot = resource._ComponentManager() self.assertEqual(dict(), sot.attributes) self.assertEqual(set(), sot._dirty) @@ -256,7 +256,7 @@ def test_create_unsynced(self): attrs = {"hey": 1, "hi": 2, "hello": 3} sync = False - sot = resource2._ComponentManager(attributes=attrs, synchronized=sync) + sot = resource._ComponentManager(attributes=attrs, synchronized=sync) self.assertEqual(attrs, sot.attributes) self.assertEqual(set(attrs.keys()), sot._dirty) @@ -264,7 +264,7 @@ def test_create_synced(self): attrs = {"hey": 1, "hi": 2, "hello": 3} sync = True - sot = resource2._ComponentManager(attributes=attrs, synchronized=sync) + sot = resource._ComponentManager(attributes=attrs, synchronized=sync) self.assertEqual(attrs, sot.attributes) self.assertEqual(set(), sot._dirty) @@ -273,14 +273,14 @@ def test_getitem(self): value = "value" attrs = {key: value} - sot = resource2._ComponentManager(attributes=attrs) + sot = resource._ComponentManager(attributes=attrs) self.assertEqual(value, sot.__getitem__(key)) def test_setitem_new(self): key = "key" value = "value" - sot = resource2._ComponentManager() + sot = resource._ComponentManager() sot.__setitem__(key, value) self.assertIn(key, sot.attributes) @@ -291,7 +291,7 @@ def test_setitem_unchanged(self): value = "value" attrs = {key: value} - sot = resource2._ComponentManager(attributes=attrs, synchronized=True) + sot = resource._ComponentManager(attributes=attrs, synchronized=True) # This shouldn't end up in the dirty list since we're just re-setting. sot.__setitem__(key, value) @@ -303,19 +303,19 @@ def test_delitem(self): value = "value" attrs = {key: value} - sot = resource2._ComponentManager(attributes=attrs, synchronized=True) + sot = resource._ComponentManager(attributes=attrs, synchronized=True) sot.__delitem__(key) self.assertIsNone(sot.dirty[key]) def test_iter(self): attrs = {"key": "value"} - sot = resource2._ComponentManager(attributes=attrs) + sot = resource._ComponentManager(attributes=attrs) self.assertItemsEqual(iter(attrs), sot.__iter__()) def test_len(self): attrs = {"key": "value"} - sot = resource2._ComponentManager(attributes=attrs) + sot = resource._ComponentManager(attributes=attrs) self.assertEqual(len(attrs), sot.__len__()) def test_dirty(self): @@ -323,7 +323,7 @@ def test_dirty(self): key2 = "key2" value = "value" attrs = {key: value} - sot = resource2._ComponentManager(attributes=attrs, synchronized=False) + sot = resource._ComponentManager(attributes=attrs, synchronized=False) self.assertEqual({key: value}, sot.dirty) sot.__setitem__(key2, value) @@ -333,7 +333,7 @@ def test_clean(self): key = "key" value = "value" attrs = {key: value} - sot = resource2._ComponentManager(attributes=attrs, synchronized=False) + sot = resource._ComponentManager(attributes=attrs, synchronized=False) self.assertEqual(attrs, sot.dirty) sot.clean() @@ -348,7 +348,7 @@ def test_create(self): body = 2 headers = 3 - sot = resource2._Request(uri, body, headers) + sot = resource._Request(uri, body, headers) self.assertEqual(uri, sot.url) self.assertEqual(body, sot.body) @@ -361,7 +361,7 @@ def test_create(self): location = "location" mapping = {"first_name": "first-name"} - sot = resource2.QueryParameters(location, **mapping) + sot = resource.QueryParameters(location, **mapping) self.assertEqual({"location": "location", "first_name": "first-name", @@ -373,7 +373,7 @@ def test_transpose_unmapped(self): location = "location" mapping = {"first_name": "first-name"} - sot = resource2.QueryParameters(location, **mapping) + sot = resource.QueryParameters(location, **mapping) result = sot._transpose({"location": "Brooklyn", "first_name": "Brian", "last_name": "Curtin"}) @@ -386,7 +386,7 @@ def test_transpose_not_in_query(self): location = "location" mapping = {"first_name": "first-name"} - sot = resource2.QueryParameters(location, **mapping) + sot = resource.QueryParameters(location, **mapping) result = sot._transpose({"location": "Brooklyn"}) # first_name not being in the query shouldn't affect results @@ -406,17 +406,17 @@ def test_initialize_basic(self): mock_collect = mock.Mock() mock_collect.return_value = body, header, uri - with mock.patch.object(resource2.Resource, + with mock.patch.object(resource.Resource, "_collect_attrs", mock_collect): - sot = resource2.Resource(_synchronized=False, **everything) + sot = resource.Resource(_synchronized=False, **everything) mock_collect.assert_called_once_with(everything) self.assertEqual("somewhere", sot.location) - self.assertIsInstance(sot._body, resource2._ComponentManager) + self.assertIsInstance(sot._body, resource._ComponentManager) self.assertEqual(body, sot._body.dirty) - self.assertIsInstance(sot._header, resource2._ComponentManager) + self.assertIsInstance(sot._header, resource._ComponentManager) self.assertEqual(header, sot._header.dirty) - self.assertIsInstance(sot._uri, resource2._ComponentManager) + self.assertIsInstance(sot._uri, resource._ComponentManager) self.assertEqual(uri, sot._uri.dirty) self.assertFalse(sot.allow_create) @@ -433,7 +433,7 @@ def test_repr(self): b = {"b": 2} c = {"c": 3} - class Test(resource2.Resource): + class Test(resource.Resource): def __init__(self): self._body = mock.Mock() self._body.attributes.items = mock.Mock( @@ -451,16 +451,16 @@ def __init__(self): # Don't test the arguments all together since the dictionary order # they're rendered in can't be depended on, nor does it matter. - self.assertIn("openstack.tests.unit.test_resource2.Test", the_repr) + self.assertIn("openstack.tests.unit.test_resource.Test", the_repr) self.assertIn("a=1", the_repr) self.assertIn("b=2", the_repr) self.assertIn("c=3", the_repr) def test_equality(self): - class Example(resource2.Resource): - x = resource2.Body("x") - y = resource2.Header("y") - z = resource2.URI("z") + class Example(resource.Resource): + x = resource.Body("x") + y = resource.Header("y") + z = resource.URI("z") e1 = Example(x=1, y=2, z=3) e2 = Example(x=1, y=2, z=3) @@ -470,7 +470,7 @@ class Example(resource2.Resource): self.assertNotEqual(e1, e3) def test__update(self): - sot = resource2.Resource() + sot = resource.Resource() body = "body" header = "header" @@ -490,7 +490,7 @@ def test__update(self): sot._uri.update.assert_called_once_with(uri) def test__collect_attrs(self): - sot = resource2.Resource() + sot = resource.Resource() expected_attrs = ["body", "header", "uri"] @@ -518,7 +518,7 @@ def test__consume_attrs(self): serverside_key2: value2, other_key: other_value} - sot = resource2.Resource() + sot = resource.Resource() result = sot._consume_attrs(mapping, attrs) @@ -536,9 +536,9 @@ def test__mapping_defaults(self): # Check that even on an empty class, we get the expected # built-in attributes. - self.assertIn("location", resource2.Resource._header_mapping()) - self.assertIn("name", resource2.Resource._body_mapping()) - self.assertIn("id", resource2.Resource._body_mapping()) + self.assertIn("location", resource.Resource._header_mapping()) + self.assertIn("name", resource.Resource._body_mapping()) + self.assertIn("id", resource.Resource._body_mapping()) def test__mapping_overrides(self): # Iterating through the MRO used to wipe out overrides of mappings @@ -546,9 +546,9 @@ def test__mapping_overrides(self): new_name = "MyName" new_id = "MyID" - class Test(resource2.Resource): - name = resource2.Body(new_name) - id = resource2.Body(new_id) + class Test(resource.Resource): + name = resource.Body(new_name) + id = resource.Body(new_id) mapping = Test._body_mapping() @@ -556,30 +556,30 @@ class Test(resource2.Resource): self.assertEqual("id", mapping["MyID"]) def test__body_mapping(self): - class Test(resource2.Resource): - x = resource2.Body("x") - y = resource2.Body("y") - z = resource2.Body("z") + class Test(resource.Resource): + x = resource.Body("x") + y = resource.Body("y") + z = resource.Body("z") self.assertIn("x", Test._body_mapping()) self.assertIn("y", Test._body_mapping()) self.assertIn("z", Test._body_mapping()) def test__header_mapping(self): - class Test(resource2.Resource): - x = resource2.Header("x") - y = resource2.Header("y") - z = resource2.Header("z") + class Test(resource.Resource): + x = resource.Header("x") + y = resource.Header("y") + z = resource.Header("z") self.assertIn("x", Test._header_mapping()) self.assertIn("y", Test._header_mapping()) self.assertIn("z", Test._header_mapping()) def test__uri_mapping(self): - class Test(resource2.Resource): - x = resource2.URI("x") - y = resource2.URI("y") - z = resource2.URI("z") + class Test(resource.Resource): + x = resource.URI("x") + y = resource.URI("y") + z = resource.URI("z") self.assertIn("x", Test._uri_mapping()) self.assertIn("y", Test._uri_mapping()) @@ -587,7 +587,7 @@ class Test(resource2.Resource): def test__getattribute__id_in_body(self): id = "lol" - sot = resource2.Resource(id=id) + sot = resource.Resource(id=id) result = getattr(sot, "id") self.assertEqual(result, id) @@ -595,8 +595,8 @@ def test__getattribute__id_in_body(self): def test__getattribute__id_with_alternate(self): id = "lol" - class Test(resource2.Resource): - blah = resource2.Body("blah", alternate_id=True) + class Test(resource.Resource): + blah = resource.Body("blah", alternate_id=True) sot = Test(blah=id) @@ -604,18 +604,18 @@ class Test(resource2.Resource): self.assertEqual(result, id) def test__getattribute__id_without_alternate(self): - class Test(resource2.Resource): + class Test(resource.Resource): id = None sot = Test() self.assertIsNone(sot.id) def test__alternate_id_None(self): - self.assertEqual("", resource2.Resource._alternate_id()) + self.assertEqual("", resource.Resource._alternate_id()) def test__alternate_id(self): - class Test(resource2.Resource): - alt = resource2.Body("the_alt", alternate_id=True) + class Test(resource.Resource): + alt = resource.Body("the_alt", alternate_id=True) self.assertTrue("the_alt", Test._alternate_id()) @@ -630,8 +630,8 @@ class Test(resource2.Resource): self.assertEqual(sot.id, value2) def test__get_id_instance(self): - class Test(resource2.Resource): - id = resource2.Body("id") + class Test(resource.Resource): + id = resource.Body("id") value = "id" sot = Test(id=value) @@ -639,8 +639,8 @@ class Test(resource2.Resource): self.assertEqual(value, sot._get_id(sot)) def test__get_id_instance_alternate(self): - class Test(resource2.Resource): - attr = resource2.Body("attr", alternate_id=True) + class Test(resource.Resource): + attr = resource.Body("attr", alternate_id=True) value = "id" sot = Test(attr=value) @@ -649,13 +649,13 @@ class Test(resource2.Resource): def test__get_id_value(self): value = "id" - self.assertEqual(value, resource2.Resource._get_id(value)) + self.assertEqual(value, resource.Resource._get_id(value)) def test_to_dict(self): - class Test(resource2.Resource): - foo = resource2.Header('foo') - bar = resource2.Body('bar') + class Test(resource.Resource): + foo = resource.Header('foo') + bar = resource.Body('bar') res = Test(id='FAKE_ID') @@ -670,9 +670,9 @@ class Test(resource2.Resource): def test_to_dict_no_body(self): - class Test(resource2.Resource): - foo = resource2.Header('foo') - bar = resource2.Body('bar') + class Test(resource.Resource): + foo = resource.Header('foo') + bar = resource.Body('bar') res = Test(id='FAKE_ID') @@ -684,9 +684,9 @@ class Test(resource2.Resource): def test_to_dict_no_header(self): - class Test(resource2.Resource): - foo = resource2.Header('foo') - bar = resource2.Body('bar') + class Test(resource.Resource): + foo = resource.Header('foo') + bar = resource.Body('bar') res = Test(id='FAKE_ID') @@ -699,9 +699,9 @@ class Test(resource2.Resource): def test_to_dict_ignore_none(self): - class Test(resource2.Resource): - foo = resource2.Header('foo') - bar = resource2.Body('bar') + class Test(resource.Resource): + foo = resource.Header('foo') + bar = resource.Body('bar') res = Test(id='FAKE_ID', bar='BAR') @@ -713,13 +713,13 @@ class Test(resource2.Resource): def test_to_dict_with_mro(self): - class Parent(resource2.Resource): - foo = resource2.Header('foo') - bar = resource2.Body('bar') + class Parent(resource.Resource): + foo = resource.Header('foo') + bar = resource.Body('bar') class Child(Parent): - foo_new = resource2.Header('foo_baz_server') - bar_new = resource2.Body('bar_baz_server') + foo_new = resource.Header('foo_baz_server') + bar_new = resource.Body('bar_baz_server') res = Child(id='FAKE_ID') @@ -736,9 +736,9 @@ class Child(Parent): def test_to_dict_value_error(self): - class Test(resource2.Resource): - foo = resource2.Header('foo') - bar = resource2.Body('bar') + class Test(resource.Resource): + foo = resource.Header('foo') + bar = resource.Body('bar') res = Test(id='FAKE_ID') @@ -749,15 +749,15 @@ class Test(resource2.Resource): def test_to_dict_with_mro_no_override(self): - class Parent(resource2.Resource): - header = resource2.Header('HEADER') - body = resource2.Body('BODY') + class Parent(resource.Resource): + header = resource.Header('HEADER') + body = resource.Body('BODY') class Child(Parent): # The following two properties are not supposed to be overridden # by the parent class property values. - header = resource2.Header('ANOTHER_HEADER') - body = resource2.Body('ANOTHER_BODY') + header = resource.Header('ANOTHER_HEADER') + body = resource.Body('ANOTHER_BODY') res = Child(id='FAKE_ID', body='BODY_VALUE', header='HEADER_VALUE') @@ -771,8 +771,8 @@ class Child(Parent): self.assertEqual(expected, res.to_dict()) def test_new(self): - class Test(resource2.Resource): - attr = resource2.Body("attr") + class Test(resource.Resource): + attr = resource.Body("attr") value = "value" sot = Test.new(attr=value) @@ -781,8 +781,8 @@ class Test(resource2.Resource): self.assertEqual(value, sot.attr) def test_existing(self): - class Test(resource2.Resource): - attr = resource2.Body("attr") + class Test(resource.Resource): + attr = resource.Body("attr") value = "value" sot = Test.existing(attr=value) @@ -791,10 +791,10 @@ class Test(resource2.Resource): self.assertEqual(value, sot.attr) def test__prepare_request_with_id(self): - class Test(resource2.Resource): + class Test(resource.Resource): base_path = "/something" - body_attr = resource2.Body("x") - header_attr = resource2.Header("y") + body_attr = resource.Body("x") + header_attr = resource.Header("y") the_id = "id" body_value = "body" @@ -809,7 +809,7 @@ class Test(resource2.Resource): self.assertEqual({"y": header_value}, result.headers) def test__prepare_request_missing_id(self): - sot = resource2.Resource(id=None) + sot = resource.Resource(id=None) self.assertRaises(exceptions.InvalidRequest, sot._prepare_request, requires_id=True) @@ -817,11 +817,11 @@ def test__prepare_request_missing_id(self): def test__prepare_request_with_key(self): key = "key" - class Test(resource2.Resource): + class Test(resource.Resource): base_path = "/something" resource_key = key - body_attr = resource2.Body("x") - header_attr = resource2.Header("y") + body_attr = resource.Body("x") + header_attr = resource.Header("y") body_value = "body" header_value = "header" @@ -835,8 +835,8 @@ class Test(resource2.Resource): self.assertEqual({"y": header_value}, result.headers) def test__translate_response_no_body(self): - class Test(resource2.Resource): - attr = resource2.Header("attr") + class Test(resource.Resource): + attr = resource.Header("attr") response = FakeResponse({}, headers={"attr": "value"}) @@ -848,8 +848,8 @@ class Test(resource2.Resource): self.assertEqual("value", sot.attr) def test__translate_response_with_body_no_resource_key(self): - class Test(resource2.Resource): - attr = resource2.Body("attr") + class Test(resource.Resource): + attr = resource.Body("attr") body = {"attr": "value"} response = FakeResponse(body) @@ -866,9 +866,9 @@ class Test(resource2.Resource): def test__translate_response_with_body_with_resource_key(self): key = "key" - class Test(resource2.Resource): + class Test(resource.Resource): resource_key = key - attr = resource2.Body("attr") + attr = resource.Body("attr") body = {"attr": "value"} response = FakeResponse({key: body}) @@ -883,7 +883,7 @@ class Test(resource2.Resource): self.assertEqual(dict(), sot._header.dirty) def test_cant_do_anything(self): - class Test(resource2.Resource): + class Test(resource.Resource): allow_create = False allow_get = False allow_update = False @@ -920,7 +920,7 @@ def setUp(self): self.service_name = "service" self.base_path = "base_path" - class Test(resource2.Resource): + class Test(resource.Resource): service = self.service_name base_path = self.base_path resources_key = 'resources' @@ -933,7 +933,7 @@ class Test(resource2.Resource): self.test_class = Test - self.request = mock.Mock(spec=resource2._Request) + self.request = mock.Mock(spec=resource._Request) self.request.url = "uri" self.request.body = "body" self.request.headers = "headers" @@ -976,7 +976,7 @@ def _test_create(self, cls, requires_id=False, prepend_key=False): self.assertEqual(result, sot) def test_put_create(self): - class Test(resource2.Resource): + class Test(resource.Resource): service = self.service_name base_path = self.base_path allow_create = True @@ -985,7 +985,7 @@ class Test(resource2.Resource): self._test_create(Test, requires_id=True, prepend_key=True) def test_post_create(self): - class Test(resource2.Resource): + class Test(resource.Resource): service = self.service_name base_path = self.base_path allow_create = True @@ -1274,9 +1274,9 @@ def test_list_query_params(self): self.session.get.side_effect = [mock_response, mock_empty] class Test(self.test_class): - _query_mapping = resource2.QueryParameters(query_param=qp_name) + _query_mapping = resource.QueryParameters(query_param=qp_name) base_path = "/%(something)s/blah" - something = resource2.URI("something") + something = resource.URI("something") results = list(Test.list(self.session, paginated=True, query_param=qp, something=uri_param)) @@ -1523,7 +1523,7 @@ def setUp(self): self.result = 1 - class Base(resource2.Resource): + class Base(resource.Resource): @classmethod def existing(cls, **kwargs): @@ -1554,7 +1554,7 @@ def _get_one_match(cls, *args): def test_find_short_circuit(self): value = 1 - class Test(resource2.Resource): + class Test(resource.Resource): @classmethod def existing(cls, **kwargs): @@ -1578,43 +1578,43 @@ def test_find_result(self): self.assertEqual(self.result, self.one_result.find("session", "name")) def test_match_empty_results(self): - self.assertIsNone(resource2.Resource._get_one_match("name", [])) + self.assertIsNone(resource.Resource._get_one_match("name", [])) def test_no_match_by_name(self): the_name = "Brian" - match = mock.Mock(spec=resource2.Resource) + match = mock.Mock(spec=resource.Resource) match.name = the_name - result = resource2.Resource._get_one_match("Richard", [match]) + result = resource.Resource._get_one_match("Richard", [match]) self.assertIsNone(result, match) def test_single_match_by_name(self): the_name = "Brian" - match = mock.Mock(spec=resource2.Resource) + match = mock.Mock(spec=resource.Resource) match.name = the_name - result = resource2.Resource._get_one_match(the_name, [match]) + result = resource.Resource._get_one_match(the_name, [match]) self.assertIs(result, match) def test_single_match_by_id(self): the_id = "Brian" - match = mock.Mock(spec=resource2.Resource) + match = mock.Mock(spec=resource.Resource) match.id = the_id - result = resource2.Resource._get_one_match(the_id, [match]) + result = resource.Resource._get_one_match(the_id, [match]) self.assertIs(result, match) def test_single_match_by_alternate_id(self): the_id = "Richard" - class Test(resource2.Resource): - other_id = resource2.Body("other_id", alternate_id=True) + class Test(resource.Resource): + other_id = resource.Body("other_id", alternate_id=True) match = Test(other_id=the_id) result = Test._get_one_match(the_id, [match]) @@ -1624,25 +1624,25 @@ class Test(resource2.Resource): def test_multiple_matches(self): the_id = "Brian" - match = mock.Mock(spec=resource2.Resource) + match = mock.Mock(spec=resource.Resource) match.id = the_id self.assertRaises( exceptions.DuplicateResource, - resource2.Resource._get_one_match, the_id, [match, match]) + resource.Resource._get_one_match, the_id, [match, match]) class TestWaitForStatus(base.TestCase): def test_immediate_status(self): status = "loling" - resource = mock.Mock() - resource.status = status + res = mock.Mock() + res.status = status - result = resource2.wait_for_status("session", resource, status, - "failures", "interval", "wait") + result = resource.wait_for_status( + "session", res, status, "failures", "interval", "wait") - self.assertEqual(result, resource) + self.assertTrue(result, res) def _resources_from_statuses(self, *statuses): resources = [] @@ -1662,7 +1662,7 @@ def test_status_match(self): resources = self._resources_from_statuses( "first", "other", "another", "another", status) - result = resource2.wait_for_status( + result = resource.wait_for_status( mock.Mock(), resources[0], status, None, 1, 5) self.assertEqual(result, resources[-1]) @@ -1674,32 +1674,32 @@ def test_status_fails(self): self.assertRaises( exceptions.ResourceFailure, - resource2.wait_for_status, + resource.wait_for_status, mock.Mock(), resources[0], "loling", [failure], 1, 5) def test_timeout(self): status = "loling" - resource = mock.Mock() + res = mock.Mock() # The first "other" gets past the first check, and then three # pairs of "other" statuses run through the sleep counter loop, # after which time should be up. This is because we have a # one second interval and three second waiting period. statuses = ["other"] * 7 - type(resource).status = mock.PropertyMock(side_effect=statuses) + type(res).status = mock.PropertyMock(side_effect=statuses) self.assertRaises(exceptions.ResourceTimeout, - resource2.wait_for_status, - "session", resource, status, None, 1, 3) + resource.wait_for_status, + "session", res, status, None, 1, 3) def test_no_sleep(self): - resource = mock.Mock() + res = mock.Mock() statuses = ["other"] - type(resource).status = mock.PropertyMock(side_effect=statuses) + type(res).status = mock.PropertyMock(side_effect=statuses) self.assertRaises(exceptions.ResourceTimeout, - resource2.wait_for_status, - "session", resource, "status", None, 0, -1) + resource.wait_for_status, + "session", res, "status", None, 0, -1) class TestWaitForDelete(base.TestCase): @@ -1708,20 +1708,21 @@ def test_success(self): response = mock.Mock() response.headers = {} response.status_code = 404 - resource = mock.Mock() - resource.get.side_effect = [ + res = mock.Mock() + res.get.side_effect = [ None, None, exceptions.NotFoundException('Not Found', response)] - result = resource2.wait_for_delete("session", resource, 1, 3) + result = resource.wait_for_delete("session", res, 1, 3) - self.assertEqual(result, resource) + self.assertEqual(result, res) def test_timeout(self): - resource = mock.Mock() - resource.status = 'ACTIVE' - resource.get.return_value = resource + res = mock.Mock() + res.status = 'ACTIVE' + res.get.return_value = res - self.assertRaises(exceptions.ResourceTimeout, - resource2.wait_for_delete, - "session", resource, 0.1, 0.3) + self.assertRaises( + exceptions.ResourceTimeout, + resource.wait_for_delete, + "session", res, 0.1, 0.3) diff --git a/openstack/tests/unit/workflow/test_proxy.py b/openstack/tests/unit/workflow/test_proxy.py index 3f01420c6..b17d1d67b 100644 --- a/openstack/tests/unit/workflow/test_proxy.py +++ b/openstack/tests/unit/workflow/test_proxy.py @@ -10,13 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import test_proxy_base2 +from openstack.tests.unit import test_proxy_base from openstack.workflow.v2 import _proxy from openstack.workflow.v2 import execution from openstack.workflow.v2 import workflow -class TestWorkflowProxy(test_proxy_base2.TestProxyBase): +class TestWorkflowProxy(test_proxy_base.TestProxyBase): def setUp(self): super(TestWorkflowProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) diff --git a/openstack/workflow/v2/_proxy.py b/openstack/workflow/v2/_proxy.py index 8a99531e4..ffe7259b7 100644 --- a/openstack/workflow/v2/_proxy.py +++ b/openstack/workflow/v2/_proxy.py @@ -10,12 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import proxy2 +from openstack import proxy from openstack.workflow.v2 import execution as _execution from openstack.workflow.v2 import workflow as _workflow -class Proxy(proxy2.BaseProxy): +class Proxy(proxy.BaseProxy): def create_workflow(self, **attrs): """Create a new workflow from attributes diff --git a/openstack/workflow/v2/execution.py b/openstack/workflow/v2/execution.py index 9877e2bc2..01287eacb 100644 --- a/openstack/workflow/v2/execution.py +++ b/openstack/workflow/v2/execution.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import resource2 as resource +from openstack import resource from openstack.workflow import workflow_service diff --git a/openstack/workflow/v2/workflow.py b/openstack/workflow/v2/workflow.py index 73f04b48e..c98237cc1 100644 --- a/openstack/workflow/v2/workflow.py +++ b/openstack/workflow/v2/workflow.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import resource2 as resource +from openstack import resource from openstack.workflow import workflow_service diff --git a/openstack/workflow/version.py b/openstack/workflow/version.py index 4834e952a..531938607 100644 --- a/openstack/workflow/version.py +++ b/openstack/workflow/version.py @@ -11,7 +11,7 @@ # under the License. -from openstack import resource2 as resource +from openstack import resource from openstack.workflow import workflow_service From db271b735ebd2a73ac84cccd8f5bac87e8ed3ed1 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 19 Jan 2018 11:19:17 -0600 Subject: [PATCH 1937/3836] Add resource2/proxy2 wrappers with deprecations Folks out there who may have resources subclassing resource2 will be broken by the removal and rename back to resource. Add a resource2 and a proxy2 that emits a deprecation warning from the constructor but otherwise just subclasses resource and proxy. While doing that, do the same thing for enable_logging which moved. Change-Id: I76a3c9c3172942069b1e740189af530f35f41a1c --- openstack/proxy2.py | 23 +++++++++++++++++++++++ openstack/resource2.py | 23 +++++++++++++++++++++++ openstack/utils.py | 13 +++++++++++-- 3 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 openstack/proxy2.py create mode 100644 openstack/resource2.py diff --git a/openstack/proxy2.py b/openstack/proxy2.py new file mode 100644 index 000000000..ded668269 --- /dev/null +++ b/openstack/proxy2.py @@ -0,0 +1,23 @@ +# Copyright 2018 Red Hat, 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. + +from openstack import proxy +from openstack import utils + + +class Proxy(proxy.Proxy): + + @utils.deprecated(deprecated_in="0.10.0", removed_in="1.0", + details="openstack.proxy2 is now openstack.proxy") + def __init__(self, *args, **kwargs): + super(Proxy, self).__init__(*args, **kwargs) diff --git a/openstack/resource2.py b/openstack/resource2.py new file mode 100644 index 000000000..312c832a3 --- /dev/null +++ b/openstack/resource2.py @@ -0,0 +1,23 @@ +# Copyright 2018 Red Hat, 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. + +from openstack import resource +from openstack import utils + + +class Resource(resource.Resource): + + @utils.deprecated(deprecated_in="0.10.0", removed_in="1.0", + details="openstack.resource2 is now openstack.resource") + def __init__(self, *args, **kwargs): + super(Resource, self).__init__(*args, **kwargs) diff --git a/openstack/utils.py b/openstack/utils.py index 462bb8cc4..5818390a3 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -16,8 +16,6 @@ import deprecation from openstack import _log -# SDK has had enable_logging in utils. Import the symbol here to not break them -from openstack._log import enable_logging # noqa from openstack import exceptions from openstack import version @@ -48,6 +46,17 @@ def deprecated(deprecated_in=None, removed_in=None, details=details) +@deprecated(deprecated_in="0.10.0", removed_in="1.0", + details="Use openstack.enable_logging instead") +def enable_logging(*args, **kwargs): + """Backwards compatibility wrapper function. + + openstacksdk has had enable_logging in utils. It's in _log now and + exposed directly at openstack.enable_logging. + """ + return _log.enable_logging(*args, **kwargs) + + def urljoin(*args): """A custom version of urljoin that simply joins strings into a path. From 91b6410941f303d7cd4479193925d84dcd7eecc2 Mon Sep 17 00:00:00 2001 From: John Dennis Date: Tue, 5 Dec 2017 15:19:56 -0500 Subject: [PATCH 1938/3836] Do not apply format expansions to passwords get_one_cloud() and get_one_cloud_osc() iterate over config values and try to expand any variables in those values by calling value.format(), however some config values (e.g. password) should never have format() applied to them, not only might that change the password but it will also cause the format() function to raise an exception if it can not parse the format string. Examples would be single brace (e.g. 'foo{') which raises an ValueError because it's looking for a matching end brace or a brace pair with a key value that cannot be found (e.g. 'foo{bar}') which raises a KeyError. It is not reasonsable to try to escape any braces because: 1) Escaping all braces breaks valid use of the format string syntax. 2) Trying to determine exactly which braces should be escaped and which should be preserved is a daunting task and likely would not be robust. 3) Some strings might look like valid format syntax but still should be escaped (e.g. "foo{bar}", if this appeared in a password we wouldn't escape it and there would be a key error on the 'bar' key. 4) In general passwords should never be modified, you never want to apply formatting to them. The right approach is to maintain a list of config values which are excluded from having formatting applied to them. At the moment that list just includes 'password' but perhaps down the road other exceptions might crop up. This patch follows this approach, the list of excluded values can easily be updated if others are discovered. Change-Id: I187bdec582d4c2cc6c7fda47a1538194137c616b Closes-Bug: 1635696 Signed-off-by: John Dennis --- openstack/config/loader.py | 6 ++- openstack/tests/unit/config/test_config.py | 60 ++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/openstack/config/loader.py b/openstack/config/loader.py index f5d853bfa..a0ad64fd3 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -69,6 +69,8 @@ BOOL_KEYS = ('insecure', 'cache') +FORMAT_EXCLUSIONS = frozenset(['password']) + # NOTE(dtroyer): This turns out to be not the best idea so let's move # overriding defaults to a kwarg to OpenStackConfig.__init__() @@ -1092,7 +1094,7 @@ def get_one( # If any of the defaults reference other values, we need to expand for (key, value) in config.items(): - if hasattr(value, 'format'): + if hasattr(value, 'format') and key not in FORMAT_EXCLUSIONS: config[key] = value.format(**config) force_ipv4 = config.pop('force_ipv4', self.force_ipv4) @@ -1189,7 +1191,7 @@ def get_one_cloud_osc( # If any of the defaults reference other values, we need to expand for (key, value) in config.items(): - if hasattr(value, 'format'): + if hasattr(value, 'format') and key not in FORMAT_EXCLUSIONS: config[key] = value.format(**config) force_ipv4 = config.pop('force_ipv4', self.force_ipv4) diff --git a/openstack/tests/unit/config/test_config.py b/openstack/tests/unit/config/test_config.py index a8a7efb1c..992b5d4f9 100644 --- a/openstack/tests/unit/config/test_config.py +++ b/openstack/tests/unit/config/test_config.py @@ -399,6 +399,66 @@ def test_get_region_no_cloud(self): self.assertEqual(region, {'name': 'no-cloud-region', 'values': {}}) +class TestExcludedFormattedConfigValue(base.TestCase): + # verify LaunchPad bug #1635696 + # + # get_one_cloud() and get_one_cloud_osc() iterate over config + # values and try to expand any variables in those values by + # calling value.format(), however some config values + # (e.g. password) should never have format() applied to them, not + # only might that change the password but it will also cause the + # format() function to raise an exception if it can not parse the + # format string. Examples would be single brace (e.g. 'foo{') + # which raises an ValueError because it's looking for a matching + # end brace or a brace pair with a key value that cannot be found + # (e.g. 'foo{bar}') which raises a KeyError. + + def setUp(self): + super(TestExcludedFormattedConfigValue, self).setUp() + + self.args = dict( + auth_url='http://example.com/v2', + username='user', + project_name='project', + region_name='region2', + snack_type='cookie', + os_auth_token='no-good-things', + ) + + self.options = argparse.Namespace(**self.args) + + def test_get_one_cloud_password_brace(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + + password = 'foo{' # Would raise ValueError, single brace + self.options.password = password + cc = c.get_one_cloud( + cloud='_test_cloud_regions', argparse=self.options, validate=False) + self.assertEqual(cc.password, password) + + password = 'foo{bar}' # Would raise KeyError, 'bar' not found + self.options.password = password + cc = c.get_one_cloud( + cloud='_test_cloud_regions', argparse=self.options, validate=False) + self.assertEqual(cc.password, password) + + def test_get_one_cloud_osc_password_brace(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + password = 'foo{' # Would raise ValueError, single brace + self.options.password = password + cc = c.get_one_cloud_osc( + cloud='_test_cloud_regions', argparse=self.options, validate=False) + self.assertEqual(cc.password, password) + + password = 'foo{bar}' # Would raise KeyError, 'bar' not found + self.options.password = password + cc = c.get_one_cloud_osc( + cloud='_test_cloud_regions', argparse=self.options, validate=False) + self.assertEqual(cc.password, password) + + class TestConfigArgparse(base.TestCase): def setUp(self): From 3a531d7f4a013edd926821874029663546d08335 Mon Sep 17 00:00:00 2001 From: purushothamgk Date: Sat, 29 Jul 2017 00:43:11 +0530 Subject: [PATCH 1939/3836] Adds get encrypted password support Change-Id: I98499f69e4211b448df29421f88e44e6b316843a --- doc/source/user/proxies/compute.rst | 1 + openstack/compute/v2/_proxy.py | 11 +++++++++++ openstack/compute/v2/server.py | 5 +++++ 3 files changed, 17 insertions(+) diff --git a/doc/source/user/proxies/compute.rst b/doc/source/user/proxies/compute.rst index d713b9c0a..0d29784f2 100644 --- a/doc/source/user/proxies/compute.rst +++ b/doc/source/user/proxies/compute.rst @@ -77,6 +77,7 @@ Modifying a Server .. automethod:: openstack.compute.v2._proxy.Proxy.rebuild_server .. automethod:: openstack.compute.v2._proxy.Proxy.reset_server_state .. automethod:: openstack.compute.v2._proxy.Proxy.change_server_password + .. automethod:: openstack.compute.v2._proxy.Proxy.get_server_password Image Operations ^^^^^^^^^^^^^^^^ diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 9ba0ef6e4..69c3ef86d 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -436,6 +436,17 @@ def change_server_password(self, server, new_password): server = self._get_resource(_server.Server, server) server.change_password(self, new_password) + def get_server_password(self, server): + """Get the administrator password + + :param server: Either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance. + + :returns: encrypted password. + """ + server = self._get_resource(_server.Server, server) + return server.get_password(self._session) + def reset_server_state(self, server, state): """Reset the state of server diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 01ef2ba65..f8bacc2b2 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -185,6 +185,11 @@ def change_password(self, session, new_password): body = {'changePassword': {'adminPass': new_password}} self._action(session, body) + def get_password(self, session): + """Get the encrypted administrator password.""" + url = utils.urljoin(Server.base_path, self.id, 'os-server-password') + return session.get(url, endpoint_filter=self.service) + def reboot(self, session, reboot_type): """Reboot server where reboot_type might be 'SOFT' or 'HARD'.""" body = {'reboot': {'type': reboot_type}} From f39ae3d33a3e0c5e4679ee94538ad05ab2fe0451 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 29 Nov 2017 17:17:24 -0600 Subject: [PATCH 1940/3836] Add some docs about not using Profile We list profile and authenticator as deprecated, but with no docs providing suggestions as to what to do. Add some docs. These can be cleaned up a little bit as we continue to reorganize the new code. For instance, you can actually do all of these just with the Connection object, but only once we've released. Pointing people at getting a cloud_config object for now allows writing code that will work with both old and new interfaces. Change-Id: I519c672d1a3905ef8695287d73fce0949576505d --- doc/source/user/connection.rst | 183 +++++++++++++++++++++++++++++++++ openstack/connection.py | 5 +- 2 files changed, 186 insertions(+), 2 deletions(-) diff --git a/doc/source/user/connection.rst b/doc/source/user/connection.rst index 70301a29b..6f41bee70 100644 --- a/doc/source/user/connection.rst +++ b/doc/source/user/connection.rst @@ -11,3 +11,186 @@ Connection Object .. autoclass:: openstack.connection.Connection :members: + + +Transition from Profile +======================= + +.. note:: This section describes migrating code from a previous interface of + python-openstacksdk and can be ignored by people writing new code. + +If you have code that currently uses the ``openstack.profile.Profile`` object +and/or an ``authenticator`` instance from an object based on +``openstack.auth.base.BaseAuthPlugin``, that code should be updated to use the +`openstack.config.cloud_region.CloudRegion` object instead. + +Writing Code that Works with Both +--------------------------------- + +These examples should all work with both the old and new interface, with one +caveat. With the old interface, the ``CloudConfig`` object comes from the +``os-client-config`` library, and in the new interface that has been moved +into the SDK. In order to write code that works with both the old and new +interfaces, use the following code to import the config namespace: + +.. code-block:: python + + try: + from openstack import config as occ + except ImportError: + from os_client_config import config as occ + +The examples will assume that the config module has been imported in that +manner. + +.. note:: Yes, there is an easier and less verbose way to do all of these. + These are verbose to handle both the old and new interfaces in the + same codebase. + +Replacing authenticator +----------------------- + +There is no direct replacement for ``openstack.auth.base.BaseAuthPlugin``. +``python-openstacksdk`` uses the `keystoneauth`_ library for authentication +and HTTP interactions. `keystoneauth`_ has `auth plugins`_ that can be used +to control how authentication is done. The ``auth_type`` config parameter +can be set to choose the correct authentication method to be used. + +Replacing Profile +----------------- + +The right way to replace the use of ``openstack.profile.Profile`` depends +a bit on what you're trying to accomplish. Common patterns are listed below, +but in general the approach is either to pass a cloud name to the +`openstack.connection.Connection` constructor, or to construct a +`openstack.config.cloud_region.CloudRegion` object and pass it to the +constructor. + +All of the examples on this page assume that you want to support old and +new interfaces simultaneously. There are easier and less verbose versions +of each that are available if you can just make a clean transition. + +Getting a Connection to a named cloud from clouds.yaml +------------------------------------------------------ + +If you want is to construct a `openstack.connection.Connection` based on +parameters configured in a ``clouds.yaml`` file, or from environment variables: + +.. code-block:: python + + import openstack.connection + + conn = connection.from_config(cloud_name='name-of-cloud-you-want') + +Getting a Connection from python arguments avoiding clouds.yaml +--------------------------------------------------------------- + +If, on the other hand, you want to construct a +`openstack.connection.Connection`, but are in a context where reading config +from a clouds.yaml file is undesirable, such as inside of a Service: + +* create a `openstack.config.loader.OpenStackConfig` object, telling + it to not load yaml files. Optionally pass an ``app_name`` and + ``app_version`` which will be added to user-agent strings. +* get a `openstack.config.cloud_region.CloudRegion` object from it +* get a `openstack.connection.Connection` + +.. code-block:: python + + try: + from openstack import config as occ + except ImportError: + from os_client_config import config as occ + from openstack import connection + + loader = occ.OpenStackConfig( + load_yaml_files=False, + app_name='spectacular-app', + app_version='1.0') + cloud_region = loader.get_one_cloud( + region_name='my-awesome-region', + auth_type='password', + auth=dict( + auth_url='https://auth.example.com', + username='amazing-user', + user_domain_name='example-domain', + project_name='astounding-project', + user_project_name='example-domain', + password='super-secret-password', + )) + conn = connection.from_config(cloud_config=cloud_region) + +.. note:: app_name and app_version are completely optional, and auth_type + defaults to 'password'. They are shown here for clarity as to + where they should go if they want to be set. + +Getting a Connection from python arguments and optionally clouds.yaml +--------------------------------------------------------------------- + +If you want to make a connection from python arguments and want to allow +one of them to optionally be ``cloud`` to allow selection of a named cloud, +it's essentially the same as the previous example, except without +``load_yaml_files=False``. + +.. code-block:: python + + try: + from openstack import config as occ + except ImportError: + from os_client_config import config as occ + from openstack import connection + + loader = occ.OpenStackConfig( + app_name='spectacular-app', + app_version='1.0') + cloud_region = loader.get_one_cloud( + region_name='my-awesome-region', + auth_type='password', + auth=dict( + auth_url='https://auth.example.com', + username='amazing-user', + user_domain_name='example-domain', + project_name='astounding-project', + user_project_name='example-domain', + password='super-secret-password', + )) + conn = connection.from_config(cloud_config=cloud_region) + +Parameters to get_one_cloud +--------------------------- + +The most important things to note are: + +* ``auth_type`` specifies which kind of authentication plugin to use. It + controls how authentication is done, as well as what parameters are required. +* ``auth`` is a dictionary containing the parameters needed by the auth plugin. + The most common information it needs are user, project, domain, auth_url + and password. +* The rest of the keyword arguments to + ``openstack.config.loader.OpenStackConfig.get_one_cloud`` are either + parameters needed by the `keystoneauth Session`_ object, which control how + HTTP connections are made, or parameters needed by the + `keystoneauth Adapter`_ object, which control how services are found in the + Keystone Catalog. + +For `keystoneauth Adapter`_ parameters, since there is one +`openstack.connection.Connection` object but many services, per-service +parameters are formed by using the official ``service_type`` of the service +in question. For instance, to override the endpoint for the ``compute`` +service, the parameter ``compute_endpoint_override`` would be used. + +``region_name`` in ``openstack.profile.Profile`` was a per-service parameter. +This is no longer a valid concept. An `openstack.connection.Connection` is a +connection to a region of a cloud. If you are in an extreme situation where +you have one service in one region and a different service in a different +region, you must use two different `openstack.connection.Connection` objects. + +.. note:: service_type, although a parameter for keystoneauth1.adapter.Adapter, + is not a valid parameter for get_one_cloud. service_type is the key + by which services are referred, so saying + 'compute_service_type="henry"' doesn't have any meaning. + +.. _keystoneauth: https://docs.openstack.org/keystoneauth/latest/ +.. _auth plugins: https://docs.openstack.org/keystoneauth/latest/authentication-plugins.html +.. _keystoneauth Adapter: https://docs.openstack.org/keystoneauth/latest/api/keystoneauth1.html#keystoneauth1.adapter.Adapter +.. _keystoneauth Session: https://docs.openstack.org/keystoneauth/latest/api/keystoneauth1.html#keystoneauth1.session.Session diff --git a/openstack/connection.py b/openstack/connection.py index dd968ae1a..3f8b64866 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -165,10 +165,11 @@ def __init__(self, cloud=None, config=None, session=None, User Agent. :param authenticator: DEPRECATED. Only exists for short-term backwards compatibility for python-openstackclient while we - transition. + transition. See `Transition from Profile`_ for + details. :param profile: DEPRECATED. Only exists for short-term backwards compatibility for python-openstackclient while we - transition. + transition. See `Transition from Profile`_ for details. :param extra_services: List of :class:`~openstack.service_description.ServiceDescription` objects describing services that openstacksdk otherwise does not From 861446ba206ca60dbd4741ea712cdd066900eb62 Mon Sep 17 00:00:00 2001 From: Trygve Vea Date: Tue, 21 Nov 2017 19:58:18 +0100 Subject: [PATCH 1941/3836] Implement availability_zone_hints for networks and routers. Adds an optional parameter to the create_network and create_router-methods, for use with availability zone-scheduling of network agents. Change-Id: Ifb93a10415dc676f5cc56b5315f2dff24fc395b8 --- openstack/cloud/openstackcloud.py | 33 ++++++++++++++- openstack/tests/unit/cloud/test_network.py | 41 ++++++++++++++++++ openstack/tests/unit/cloud/test_router.py | 42 +++++++++++++++++++ ...ility_zone_extension-675c2460ebb50a09.yaml | 8 ++++ 4 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/neutron_availability_zone_extension-675c2460ebb50a09.yaml diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 9a59156b5..d90da7ab2 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -24,6 +24,10 @@ import six import threading import time +# import types so that we can reference ListType in sphinx param declarations. +# We can't just use list, because sphinx gets confused by +# openstack.resource.Resource.list and openstack.resource2.Resource.list +import types # noqa import warnings import dogpile.cache @@ -3389,7 +3393,8 @@ def delete_keypair(self, name): return True def create_network(self, name, shared=False, admin_state_up=True, - external=False, provider=None, project_id=None): + external=False, provider=None, project_id=None, + availability_zone_hints=None): """Create a network. :param string name: Name of the network being created. @@ -3401,6 +3406,8 @@ def create_network(self, name, shared=False, admin_state_up=True, { 'network_type': 'vlan', 'segmentation_id': 'vlan1' } :param string project_id: Specify the project ID this network will be created on (admin-only). + :param types.ListType availability_zone_hints: A list of availability + zone hints. :returns: The network object. :raises: OpenStackCloudException on operation error. @@ -3416,6 +3423,16 @@ def create_network(self, name, shared=False, admin_state_up=True, if project_id is not None: network['tenant_id'] = project_id + if availability_zone_hints is not None: + if not isinstance(availability_zone_hints, list): + raise OpenStackCloudException( + "Parameter 'availability_zone_hints' must be a list") + if not self._has_neutron_extension('network_availability_zone'): + raise OpenStackCloudUnavailableExtension( + 'network_availability_zone extension is not available on ' + 'target cloud') + network['availability_zone_hints'] = availability_zone_hints + if provider: if not isinstance(provider, dict): raise OpenStackCloudException( @@ -4251,7 +4268,8 @@ def list_router_interfaces(self, router, interface_type=None): def create_router(self, name=None, admin_state_up=True, ext_gateway_net_id=None, enable_snat=None, - ext_fixed_ips=None, project_id=None): + ext_fixed_ips=None, project_id=None, + availability_zone_hints=None): """Create a logical router. :param string name: The router name. @@ -4269,6 +4287,8 @@ def create_router(self, name=None, admin_state_up=True, } ] :param string project_id: Project ID for the router. + :param types.ListType availability_zone_hints: + A list of availability zone hints. :returns: The router object. :raises: OpenStackCloudException on operation error. @@ -4285,6 +4305,15 @@ def create_router(self, name=None, admin_state_up=True, ) if ext_gw_info: router['external_gateway_info'] = ext_gw_info + if availability_zone_hints is not None: + if not isinstance(availability_zone_hints, list): + raise OpenStackCloudException( + "Parameter 'availability_zone_hints' must be a list") + if not self._has_neutron_extension('router_availability_zone'): + raise OpenStackCloudUnavailableExtension( + 'router_availability_zone extension is not available on ' + 'target cloud') + router['availability_zone_hints'] = availability_zone_hints data = self._network_client.post( "/routers.json", json={"router": router}, diff --git a/openstack/tests/unit/cloud/test_network.py b/openstack/tests/unit/cloud/test_network.py index 9a88501cb..825ab235a 100644 --- a/openstack/tests/unit/cloud/test_network.py +++ b/openstack/tests/unit/cloud/test_network.py @@ -13,6 +13,7 @@ import copy import testtools +import openstack import openstack.cloud from openstack.tests.unit import base @@ -47,6 +48,16 @@ class TestNetwork(base.RequestsMockTestCase): 'mtu': 0 } + network_availability_zone_extension = { + "alias": "network_availability_zone", + "updated": "2015-01-01T10:00:00-00:00", + "description": "Availability zone support for router.", + "links": [], + "name": "Network Availability Zone" + } + + enabled_neutron_extensions = [network_availability_zone_extension] + def test_list_networks(self): net1 = {'id': '1', 'name': 'net1'} net2 = {'id': '2', 'name': 'net2'} @@ -151,6 +162,27 @@ def test_create_network_provider(self): self.assertEqual(mock_new_network_rep, network) self.assert_calls() + def test_create_network_with_availability_zone_hints(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'network': self.mock_new_network_rep}, + validate=dict( + json={'network': { + 'admin_state_up': True, + 'name': 'netname', + 'availability_zone_hints': ['nova']}})) + ]) + network = self.cloud.create_network("netname", + availability_zone_hints=['nova']) + self.assertEqual(self.mock_new_network_rep, network) + self.assert_calls() + def test_create_network_provider_ignored_value(self): provider_opts = {'physical_network': 'mynet', 'network_type': 'vlan', @@ -180,6 +212,15 @@ def test_create_network_provider_ignored_value(self): self.assertEqual(mock_new_network_rep, network) self.assert_calls() + def test_create_network_wrong_availability_zone_hints_type(self): + azh_opts = "invalid" + with testtools.ExpectedException( + openstack.cloud.OpenStackCloudException, + "Parameter 'availability_zone_hints' must be a list" + ): + self.cloud.create_network("netname", + availability_zone_hints=azh_opts) + def test_create_network_provider_wrong_type(self): provider_opts = "invalid" with testtools.ExpectedException( diff --git a/openstack/tests/unit/cloud/test_router.py b/openstack/tests/unit/cloud/test_router.py index 2b3a74143..48747d925 100644 --- a/openstack/tests/unit/cloud/test_router.py +++ b/openstack/tests/unit/cloud/test_router.py @@ -14,6 +14,7 @@ # limitations under the License. import copy +import testtools from openstack.cloud import exc from openstack.tests.unit import base @@ -52,6 +53,16 @@ class TestRouter(base.RequestsMockTestCase): 'request_ids': ['req-f1b0b1b4-ae51-4ef9-b371-0cc3c3402cf7'] } + router_availability_zone_extension = { + "alias": "router_availability_zone", + "updated": "2015-01-01T10:00:00-00:00", + "description": "Availability zone support for router.", + "links": [], + "name": "Router Availability Zone" + } + + enabled_neutron_extensions = [router_availability_zone_extension] + def test_get_router(self): self.register_uris([ dict(method='GET', @@ -112,6 +123,27 @@ def test_create_router_specific_tenant(self): project_id=new_router_tenant_id) self.assert_calls() + def test_create_router_with_availability_zone_hints(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'routers.json']), + json={'router': self.mock_router_rep}, + validate=dict( + json={'router': { + 'name': self.router_name, + 'admin_state_up': True, + 'availability_zone_hints': ['nova']}})) + ]) + self.cloud.create_router( + name=self.router_name, admin_state_up=True, + availability_zone_hints=['nova']) + self.assert_calls() + def test_create_router_with_enable_snat_True(self): """Do not send enable_snat when same as neutron default.""" self.register_uris([ @@ -145,6 +177,16 @@ def test_create_router_with_enable_snat_False(self): name=self.router_name, admin_state_up=True, enable_snat=False) self.assert_calls() + def test_create_router_wrong_availability_zone_hints_type(self): + azh_opts = "invalid" + with testtools.ExpectedException( + exc.OpenStackCloudException, + "Parameter 'availability_zone_hints' must be a list" + ): + self.cloud.create_router( + name=self.router_name, admin_state_up=True, + availability_zone_hints=azh_opts) + def test_add_router_interface(self): self.register_uris([ dict(method='PUT', diff --git a/releasenotes/notes/neutron_availability_zone_extension-675c2460ebb50a09.yaml b/releasenotes/notes/neutron_availability_zone_extension-675c2460ebb50a09.yaml new file mode 100644 index 000000000..058f40bbc --- /dev/null +++ b/releasenotes/notes/neutron_availability_zone_extension-675c2460ebb50a09.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + ``availability_zone_hints`` now accepted for ``create_network()`` when + ``network_availability_zone`` extension is enabled on target cloud. + - | + ``availability_zone_hints`` now accepted for ``create_router()`` when + ``router_availability_zone`` extension is enabled on target cloud. From be8a3c651e2c448128ccc73a6ef9bcdeea69638e Mon Sep 17 00:00:00 2001 From: Adrian Turjak Date: Thu, 11 Jan 2018 17:02:58 +1300 Subject: [PATCH 1942/3836] Raise error when supplying invalid query params Rather than stripping invalid query params and then continuing the API call, lets actually throw an error since we explicitly define what valid query params are. This stops people from accidentally querying for an incorrect param, getting back a list of 'everything' and then acting on it assuming it was a valid query. Adds a util function for getting keys from format strings as the checking for invalid keys needs to take into account what key may be required for the base_url. Change-Id: If30badb4d71e521100a0e8974978eb6d5fa2699f --- openstack/exceptions.py | 5 +++++ openstack/network/v2/floating_ip.py | 2 +- openstack/resource.py | 11 +++++++++++ openstack/tests/unit/test_resource.py | 24 ++++++++++++++++++++++++ openstack/utils.py | 27 +++++++++++++++++++++++++++ 5 files changed, 68 insertions(+), 1 deletion(-) diff --git a/openstack/exceptions.py b/openstack/exceptions.py index 8d0c16cb9..b3f99ea0e 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -152,6 +152,11 @@ class ResourceFailure(SDKException): pass +class InvalidResourceQuery(SDKException): + """Invalid query params for resource.""" + pass + + def raise_from_response(response, error_message=None): """Raise an instance of an HTTPException based on keystoneauth response.""" if response.status_code < 400: diff --git a/openstack/network/v2/floating_ip.py b/openstack/network/v2/floating_ip.py index 5ffbb1f4e..9ae201b84 100644 --- a/openstack/network/v2/floating_ip.py +++ b/openstack/network/v2/floating_ip.py @@ -73,7 +73,7 @@ class FloatingIP(resource.Resource): @classmethod def find_available(cls, session): - info = cls.list(session, fields='id', port_id='') + info = cls.list(session, port_id='') try: return next(info) except StopIteration: diff --git a/openstack/resource.py b/openstack/resource.py index 2d992b171..96cb908ea 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -819,10 +819,21 @@ def list(cls, session, paginated=False, **params): :return: A generator of :class:`Resource` objects. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if :data:`Resource.allow_list` is not set to ``True``. + :raises: :exc:`~openstack.exceptions.InvalidResourceQuery` if query + contains invalid params. """ if not cls.allow_list: raise exceptions.MethodNotSupported(cls, "list") + expected_params = utils.get_string_format_keys(cls.base_path) + expected_params += cls._query_mapping._mapping.keys() + + invalid_keys = set(params.keys()) - set(expected_params) + if invalid_keys: + raise exceptions.InvalidResourceQuery( + message="Invalid query params: %s" % ",".join(invalid_keys), + extra_data=invalid_keys) + query_params = cls._query_mapping._transpose(params) uri = cls.base_path % params diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index ededc7e39..ee0467ade 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1290,6 +1290,30 @@ class Test(self.test_class): self.assertEqual(self.session.get.call_args_list[0][0][0], Test.base_path % {"something": uri_param}) + def test_invalid_list_params(self): + id = 1 + qp = "query param!" + qp_name = "query-param" + uri_param = "uri param!" + + mock_response = mock.Mock() + mock_response.json.side_effect = [[{"id": id}], + []] + + self.session.get.return_value = mock_response + + class Test(self.test_class): + _query_mapping = resource.QueryParameters(query_param=qp_name) + base_path = "/%(something)s/blah" + something = resource.URI("something") + + try: + list(Test.list(self.session, paginated=True, query_param=qp, + something=uri_param, something_wrong=True)) + self.assertFail('The above line should fail') + except exceptions.InvalidResourceQuery as err: + self.assertEqual(str(err), 'Invalid query params: something_wrong') + def test_list_multi_page_response_paginated(self): ids = [1, 2] resp1 = mock.Mock() diff --git a/openstack/utils.py b/openstack/utils.py index 5818390a3..dac11967b 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -11,6 +11,7 @@ # under the License. import functools +import string import time import deprecation @@ -101,3 +102,29 @@ def iterate_timeout(timeout, message, wait=2): log.debug('Waiting %s seconds', wait) time.sleep(wait) raise exceptions.ResourceTimeout(message) + + +def get_string_format_keys(fmt_string, old_style=True): + """Gets a list of required keys from a format string + + Required mostly for parsing base_path urls for required keys, which + use the old style string formatting. + """ + if old_style: + class AccessSaver(object): + def __init__(self): + self.keys = [] + + def __getitem__(self, key): + self.keys.append(key) + + a = AccessSaver() + fmt_string % a + + return a.keys + else: + keys = [] + for t in string.Formatter().parse(fmt_string): + if t[1] is not None: + keys.append(t[1]) + return keys From 586fca4b5d59a56131403dcdacfba62537a505e8 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 6 Dec 2017 11:06:47 -0600 Subject: [PATCH 1943/3836] Fix batching for floating ips and ports When cache settings are in place for ports and floating ips, we should be doing full list calls and filtering locally. This is done to prevent things lke nodepool from crushing clouds under the weight of our immense need for information. Change-Id: I304ff1c0e355bcfc00398316a296417c19e9b74f --- openstack/cloud/openstackcloud.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index d90da7ab2..8d7f9008e 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -1762,9 +1762,11 @@ def list_ports(self, filters=None): :returns: A list of port ``munch.Munch``. """ - # If pushdown filters are specified, bypass local caching. - if filters: + # If pushdown filters are specified and we do not have batched caching + # enabled, bypass local caching and push down the filters. + if filters and self._PORT_AGE == 0: return self._list_ports(filters) + # Translate None from search interface to empty {} for kwargs below filters = {} if (time.time() - self._ports_time) >= self._PORT_AGE: @@ -2369,8 +2371,9 @@ def list_floating_ips(self, filters=None): :returns: A list of floating IP ``munch.Munch``. """ - # If pushdown filters are specified, bypass local caching. - if filters: + # If pushdown filters are specified and we do not have batched caching + # enabled, bypass local caching and push down the filters. + if filters and self._FLOAT_AGE == 0: return self._list_floating_ips(filters) if (time.time() - self._floating_ips_time) >= self._FLOAT_AGE: From 07c8649931f63d611b638030c40102ab4403c38f Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Fri, 8 Dec 2017 13:06:13 -0800 Subject: [PATCH 1944/3836] Pass through all_projects for get_server This function currently does not allow getting of a server from all projects, and it can be quite useful to do so (for various admin/operator activities) so allow get_server to pass through 'all_projects' to the internally called 'search_servers' (and default it to false to retain the old behavior). Change-Id: I7b7534a044cfa0ccbaa11a635edcca388db27f0f --- openstack/cloud/openstackcloud.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 8d7f9008e..cf7e09205 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -3117,7 +3117,8 @@ def _get_server_console_output(self, server_id, length=None): return self._get_and_munchify('output', data) def get_server( - self, name_or_id=None, filters=None, detailed=False, bare=False): + self, name_or_id=None, filters=None, detailed=False, bare=False, + all_projects=False): """Get a server by name or ID. :param name_or_id: Name or ID of the server. @@ -3141,13 +3142,16 @@ def get_server( server record. Defaults to False, meaning the addresses dict will be populated as needed from neutron. Setting to True implies detailed = False. + :param all_projects: Whether to get server from all projects or just + the current auth scoped project. :returns: A server ``munch.Munch`` or None if no matching server is found. """ searchfunc = functools.partial(self.search_servers, - detailed=detailed, bare=True) + detailed=detailed, bare=True, + all_projects=all_projects) server = _utils._get_entity(self, searchfunc, name_or_id, filters) return self._expand_server(server, detailed, bare) From 63f41e88427688e8dd8fe07e1a122624a8e12104 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 19 Dec 2017 15:49:21 -0600 Subject: [PATCH 1945/3836] Throw OpenStackCloudCreateException on create errors The exception is OpenStackCloudCreateException, not OpenStackCloudCreationException. Change-Id: Ia1bad2e279ed472d93c818d07a1d6de8723ab151 --- openstack/cloud/openstackcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index cf7e09205..01bdfe6b3 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -6861,7 +6861,7 @@ def create_server( # going to do the wait loop below, this is a waste of a call server = self.get_server_by_id(server.id) if server.status == 'ERROR': - raise OpenStackCloudCreationException( + raise OpenStackCloudCreateException( resource='server', resource_id=server.id) if wait: From 5872184a6936c051974d867651f24369452da280 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 26 Jan 2018 08:36:44 -0600 Subject: [PATCH 1946/3836] Use devstack functional test base job We extracted some of our functional base job to the devstack repo. Consume it. Change-Id: I0b5172384ed1f4017d4f4ada7f4dadc5593f14be --- .zuul.yaml | 8 +------- playbooks/devstack/post.yaml | 4 ---- playbooks/devstack/pre.yaml | 8 -------- playbooks/devstack/run.yaml | 3 --- 4 files changed, 1 insertion(+), 22 deletions(-) delete mode 100644 playbooks/devstack/post.yaml delete mode 100644 playbooks/devstack/pre.yaml delete mode 100644 playbooks/devstack/run.yaml diff --git a/.zuul.yaml b/.zuul.yaml index 388e62f45..c54632b3b 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -41,12 +41,9 @@ - job: name: openstacksdk-functional-devstack-base - parent: devstack + parent: devstack-tox-functional-consumer description: | Base job for devstack-based functional tests - pre-run: playbooks/devstack/pre.yaml - run: playbooks/devstack/run.yaml - post-run: playbooks/devstack/post.yaml required-projects: # These jobs will DTRT when openstacksdk triggers them, but we want to # make sure stable branches of openstacksdk never get cloned by other @@ -62,15 +59,12 @@ - name: openstack/swift timeout: 9000 vars: - devstack_localrc: - SWIFT_HASH: '1234123412341234' devstack_local_conf: post-config: $CINDER_CONF: DEFAULT: osapi_max_limit: 6 devstack_services: - horizon: false s-account: true s-container: true s-object: true diff --git a/playbooks/devstack/post.yaml b/playbooks/devstack/post.yaml deleted file mode 100644 index 7f0cb1982..000000000 --- a/playbooks/devstack/post.yaml +++ /dev/null @@ -1,4 +0,0 @@ -- hosts: all - roles: - - fetch-tox-output - - fetch-subunit-output diff --git a/playbooks/devstack/pre.yaml b/playbooks/devstack/pre.yaml deleted file mode 100644 index 3ec41c9cb..000000000 --- a/playbooks/devstack/pre.yaml +++ /dev/null @@ -1,8 +0,0 @@ -- hosts: all - roles: - - run-devstack - - role: bindep - bindep_profile: test - bindep_dir: "{{ zuul_work_dir }}" - - test-setup - - ensure-tox diff --git a/playbooks/devstack/run.yaml b/playbooks/devstack/run.yaml deleted file mode 100644 index 22f82096c..000000000 --- a/playbooks/devstack/run.yaml +++ /dev/null @@ -1,3 +0,0 @@ -- hosts: all - roles: - - tox From bd3fad7d0ee51f0945173a6ef40455d3e9406ae8 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 26 Jan 2018 08:38:56 -0600 Subject: [PATCH 1947/3836] Remove inner_exceptions plumbing We've finally gotten out of the business of wrapping exceptions. Since we don't do that anymore, we don't need the log_inner_exceptions logic. Change-Id: Id1f709daa2e61c13efeeeffc2a08578c27265e56 --- openstack/cloud/openstackcloud.py | 14 +------------- openstack/tests/functional/cloud/base.py | 6 ++---- openstack/tests/unit/base.py | 13 ++++--------- 3 files changed, 7 insertions(+), 26 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 01bdfe6b3..017efb6f1 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -122,15 +122,6 @@ class OpenStackCloud(_normalize.Normalizer): OpenStack API tasks. Unless you're doing rate limiting client side, you almost certainly don't need this. (optional) - :param bool log_inner_exceptions: Send wrapped exceptions to the error log. - Defaults to false, because there are a - number of wrapped exceptions that are - noise for normal usage. It's possible - that for a user that has python logging - configured properly, it's desirable to - have all of the wrapped exceptions be - emitted to the error log. This flag - will enable that behavior. :param bool strict: Only return documented attributes for each resource as per the Data Model contract. (Default False) :param app_name: Name of the application to be appended to the user-agent @@ -146,16 +137,13 @@ class OpenStackCloud(_normalize.Normalizer): def __init__( self, cloud_config=None, - manager=None, log_inner_exceptions=False, + manager=None, strict=False, app_name=None, app_version=None, use_direct_get=False, **kwargs): - if log_inner_exceptions: - OpenStackCloudException.log_inner_exceptions = True - self.log = _log.setup_logging('openstack') if not cloud_config: diff --git a/openstack/tests/functional/cloud/base.py b/openstack/tests/functional/cloud/base.py index 63f14f58c..c8a9a5da1 100644 --- a/openstack/tests/functional/cloud/base.py +++ b/openstack/tests/functional/cloud/base.py @@ -39,15 +39,13 @@ def _set_user_cloud(self, **kwargs): user_config = self.config.get_one( cloud=self._demo_name, **kwargs) self.user_cloud = openstack.cloud.OpenStackCloud( - cloud_config=user_config, - log_inner_exceptions=True) + cloud_config=user_config) def _set_operator_cloud(self, **kwargs): operator_config = self.config.get_one( cloud=self._op_name, **kwargs) self.operator_cloud = openstack.cloud.OperatorCloud( - cloud_config=operator_config, - log_inner_exceptions=True) + cloud_config=operator_config) def pick_image(self): images = self.user_cloud.list_images() diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 3beaa91ed..4d969f363 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -120,15 +120,12 @@ def _nosleep(seconds): self.cloud_config = self.config.get_one( cloud=test_cloud, validate=False) self.cloud = openstack.cloud.OpenStackCloud( - cloud_config=self.cloud_config, - log_inner_exceptions=True) + cloud_config=self.cloud_config) self.strict_cloud = openstack.cloud.OpenStackCloud( cloud_config=self.cloud_config, - log_inner_exceptions=True, strict=True) self.op_cloud = openstack.cloud.OperatorCloud( - cloud_config=self.cloud_config, - log_inner_exceptions=True) + cloud_config=self.cloud_config) # TODO(shade) Remove this and rename RequestsMockTestCase to TestCase. @@ -466,11 +463,9 @@ def _make_test_cloud(self, cloud_name='_test_cloud_', **kwargs): self.conn = openstack.connection.Connection( config=self.cloud_config) self.cloud = openstack.cloud.OpenStackCloud( - cloud_config=self.cloud_config, - log_inner_exceptions=True) + cloud_config=self.cloud_config) self.op_cloud = openstack.cloud.OperatorCloud( - cloud_config=self.cloud_config, - log_inner_exceptions=True) + cloud_config=self.cloud_config) def get_glance_discovery_mock_dict( self, From ad73e46483b253d493057afdb241a1d07806f6a6 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 27 Jan 2018 18:35:23 +0000 Subject: [PATCH 1948/3836] Updated from global requirements Change-Id: Id1418b66d055d3efaa71825a8e1becc608e54b73 --- doc/requirements.txt | 4 ++-- test-requirements.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index ff656d7bc..f1688868f 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,8 +1,8 @@ # 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. -sphinx>=1.6.2 # BSD +sphinx!=1.6.6,>=1.6.2 # BSD docutils>=0.11 # OSI-Approved Open Source, Public Domain -openstackdocstheme>=1.17.0 # Apache-2.0 +openstackdocstheme>=1.18.1 # Apache-2.0 beautifulsoup4>=4.6.0 # MIT reno>=2.5.0 # Apache-2.0 diff --git a/test-requirements.txt b/test-requirements.txt index 236250da9..cede4be1d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,7 +7,7 @@ fixtures>=3.0.0 # Apache-2.0/BSD jsonschema<3.0.0,>=2.6.0 # MIT mock>=2.0.0 # BSD python-subunit>=1.0.0 # Apache-2.0/BSD -oslotest>=1.10.0 # Apache-2.0 +oslotest>=3.2.0 # Apache-2.0 requests-mock>=1.1.0 # Apache-2.0 stestr>=1.0.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD From a50a7843753b7c1470313afbf11e3b99a2089d2e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 26 Jan 2018 08:53:54 -0600 Subject: [PATCH 1949/3836] Merge OpenstackCloud and OperatorCloud files Change-Id: I4db00729eb9668baac25e722ee49263609c7dbb9 --- SHADE-MERGE-TODO.rst | 2 +- doc/source/user/usage.rst | 3 - openstack/cloud/__init__.py | 13 - openstack/cloud/openstackcloud.py | 2570 +++++++++++++++- openstack/cloud/operatorcloud.py | 2597 ----------------- openstack/tests/functional/cloud/base.py | 2 +- openstack/tests/unit/base.py | 4 - openstack/tests/unit/cloud/test_aggregate.py | 16 +- .../tests/unit/cloud/test_baremetal_node.py | 102 +- .../tests/unit/cloud/test_baremetal_ports.py | 12 +- openstack/tests/unit/cloud/test_domains.py | 24 +- openstack/tests/unit/cloud/test_endpoints.py | 26 +- openstack/tests/unit/cloud/test_flavors.py | 18 +- openstack/tests/unit/cloud/test_groups.py | 12 +- .../tests/unit/cloud/test_identity_roles.py | 30 +- openstack/tests/unit/cloud/test_image.py | 4 +- openstack/tests/unit/cloud/test_limits.py | 2 +- .../tests/unit/cloud/test_magnum_services.py | 2 +- openstack/tests/unit/cloud/test_operator.py | 24 +- .../tests/unit/cloud/test_operator_noauth.py | 10 +- openstack/tests/unit/cloud/test_project.py | 24 +- openstack/tests/unit/cloud/test_quotas.py | 22 +- .../tests/unit/cloud/test_role_assignment.py | 160 +- openstack/tests/unit/cloud/test_services.py | 32 +- openstack/tests/unit/cloud/test_usage.py | 2 +- openstack/tests/unit/cloud/test_users.py | 20 +- .../tests/unit/cloud/test_volume_access.py | 14 +- 27 files changed, 2844 insertions(+), 2903 deletions(-) delete mode 100644 openstack/cloud/operatorcloud.py diff --git a/SHADE-MERGE-TODO.rst b/SHADE-MERGE-TODO.rst index 2a12d00f6..bc5aa8349 100644 --- a/SHADE-MERGE-TODO.rst +++ b/SHADE-MERGE-TODO.rst @@ -40,7 +40,7 @@ Next steps shade integration ----------------- -* Merge OpenStackCloud and OperatorCloud into Connection. This should result +* Merge OpenStackCloud into Connection. This should result in being able to use the connection interact with the cloud using all three interfaces. For instance: diff --git a/doc/source/user/usage.rst b/doc/source/user/usage.rst index f707c7964..69bb36777 100644 --- a/doc/source/user/usage.rst +++ b/doc/source/user/usage.rst @@ -17,6 +17,3 @@ To use `openstack.cloud` in a project: .. autoclass:: openstack.cloud.OpenStackCloud :members: - -.. autoclass:: openstack.cloud.OperatorCloud - :members: diff --git a/openstack/cloud/__init__.py b/openstack/cloud/__init__.py index a28fe467e..22ea13756 100644 --- a/openstack/cloud/__init__.py +++ b/openstack/cloud/__init__.py @@ -17,7 +17,6 @@ from openstack._log import enable_logging # noqa from openstack.cloud.exc import * # noqa from openstack.cloud.openstackcloud import OpenStackCloud -from openstack.cloud.operatorcloud import OperatorCloud def _get_openstack_config(app_name=None, app_version=None): @@ -65,15 +64,3 @@ def openstack_cloud( raise OpenStackCloudException( "Invalid cloud configuration: {exc}".format(exc=str(e))) return OpenStackCloud(cloud_config=cloud_region, strict=strict) - - -def operator_cloud( - config=None, strict=False, app_name=None, app_version=None, **kwargs): - if not config: - config = _get_openstack_config(app_name, app_version) - try: - cloud_region = config.get_one(**kwargs) - except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin as e: - raise OpenStackCloudException( - "Invalid cloud configuration: {exc}".format(exc=str(e))) - return OperatorCloud(cloud_config=cloud_region, strict=strict) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 017efb6f1..71ac71ce9 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -13,12 +13,13 @@ import base64 import collections import copy +import datetime import functools import hashlib import ipaddress +import iso8601 import json import jsonpatch -import keystoneauth1.session import operator import os import six @@ -385,9 +386,8 @@ def session_constructor(*args, **kwargs): # Override the cloud name so that logging/location work right cloud_config.name = self.name cloud_config.config['profile'] = self.name - # Use self.__class__ so that OperatorCloud will return an OperatorCloud - # instance. This should also help passthrough from sdk work better when - # we have it. + # Use self.__class__ so that we return whatever this if, like if it's + # a subclass in the case of shade wrapping sdk. return self.__class__(cloud_config=cloud_config) def connect_as_project(self, project): @@ -8830,3 +8830,2565 @@ def update_cluster_template(self, name_or_id, operation, **kwargs): new_cluster_template = self.get_cluster_template(name_or_id) return new_cluster_template update_baymodel = update_cluster_template + + def list_nics(self): + msg = "Error fetching machine port list" + return self._baremetal_client.get("/ports", + microversion="1.6", + error_message=msg) + + def list_nics_for_machine(self, uuid): + """Returns a list of ports present on the machine node. + + :param uuid: String representing machine UUID value in + order to identify the machine. + :returns: A dictionary containing containing a list of ports, + associated with the label "ports". + """ + msg = "Error fetching port list for node {node_id}".format( + node_id=uuid) + url = "/nodes/{node_id}/ports".format(node_id=uuid) + return self._baremetal_client.get(url, + microversion="1.6", + error_message=msg) + + def get_nic_by_mac(self, mac): + try: + url = '/ports/detail?address=%s' % mac + data = self._baremetal_client.get(url) + if len(data['ports']) == 1: + return data['ports'][0] + except Exception: + pass + return None + + def list_machines(self): + msg = "Error fetching machine node list" + data = self._baremetal_client.get("/nodes", + microversion="1.6", + error_message=msg) + return self._get_and_munchify('nodes', data) + + def get_machine(self, name_or_id): + """Get Machine by name or uuid + + Search the baremetal host out by utilizing the supplied id value + which can consist of a name or UUID. + + :param name_or_id: A node name or UUID that will be looked up. + + :returns: ``munch.Munch`` representing the node found or None if no + nodes are found. + """ + # NOTE(TheJulia): This is the initial microversion shade support for + # ironic was created around. Ironic's default behavior for newer + # versions is to expose the field, but with a value of None for + # calls by a supported, yet older microversion. + # Consensus for moving forward with microversion handling in shade + # seems to be to take the same approach, although ironic's API + # does it for the user. + version = "1.6" + try: + url = '/nodes/{node_id}'.format(node_id=name_or_id) + return self._normalize_machine( + self._baremetal_client.get(url, microversion=version)) + except Exception: + return None + + def get_machine_by_mac(self, mac): + """Get machine by port MAC address + + :param mac: Port MAC address to query in order to return a node. + + :returns: ``munch.Munch`` representing the node found or None + if the node is not found. + """ + try: + port_url = '/ports/detail?address={mac}'.format(mac=mac) + port = self._baremetal_client.get(port_url, microversion=1.6) + machine_url = '/nodes/{machine}'.format( + machine=port['ports'][0]['node_uuid']) + return self._baremetal_client.get(machine_url, microversion=1.6) + except Exception: + return None + + def inspect_machine(self, name_or_id, wait=False, timeout=3600): + """Inspect a Barmetal machine + + Engages the Ironic node inspection behavior in order to collect + metadata about the baremetal machine. + + :param name_or_id: String representing machine name or UUID value in + order to identify the machine. + + :param wait: Boolean value controlling if the method is to wait for + the desired state to be reached or a failure to occur. + + :param timeout: Integer value, defautling to 3600 seconds, for the$ + wait state to reach completion. + + :returns: ``munch.Munch`` representing the current state of the machine + upon exit of the method. + """ + + return_to_available = False + + machine = self.get_machine(name_or_id) + if not machine: + raise OpenStackCloudException( + "Machine inspection failed to find: %s." % name_or_id) + + # NOTE(TheJulia): If in available state, we can do this, however + # We need to to move the host back to m + if "available" in machine['provision_state']: + return_to_available = True + # NOTE(TheJulia): Changing available machine to managedable state + # and due to state transitions we need to until that transition has + # completd. + self.node_set_provision_state(machine['uuid'], 'manage', + wait=True, timeout=timeout) + elif ("manage" not in machine['provision_state'] and + "inspect failed" not in machine['provision_state']): + raise OpenStackCloudException( + "Machine must be in 'manage' or 'available' state to " + "engage inspection: Machine: %s State: %s" + % (machine['uuid'], machine['provision_state'])) + with _utils.shade_exceptions("Error inspecting machine"): + machine = self.node_set_provision_state(machine['uuid'], 'inspect') + if wait: + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for node transition to " + "target state of 'inspect'"): + machine = self.get_machine(name_or_id) + + if "inspect failed" in machine['provision_state']: + raise OpenStackCloudException( + "Inspection of node %s failed, last error: %s" + % (machine['uuid'], machine['last_error'])) + + if "manageable" in machine['provision_state']: + break + + if return_to_available: + machine = self.node_set_provision_state( + machine['uuid'], 'provide', wait=wait, timeout=timeout) + + return(machine) + + def register_machine(self, nics, wait=False, timeout=3600, + lock_timeout=600, **kwargs): + """Register Baremetal with Ironic + + Allows for the registration of Baremetal nodes with Ironic + and population of pertinant node information or configuration + to be passed to the Ironic API for the node. + + This method also creates ports for a list of MAC addresses passed + in to be utilized for boot and potentially network configuration. + + If a failure is detected creating the network ports, any ports + created are deleted, and the node is removed from Ironic. + + :param nics: + An array of MAC addresses that represent the + network interfaces for the node to be created. + + Example:: + + [ + {'mac': 'aa:bb:cc:dd:ee:01'}, + {'mac': 'aa:bb:cc:dd:ee:02'} + ] + + :param wait: Boolean value, defaulting to false, to wait for the + node to reach the available state where the node can be + provisioned. It must be noted, when set to false, the + method will still wait for locks to clear before sending + the next required command. + + :param timeout: Integer value, defautling to 3600 seconds, for the + wait state to reach completion. + + :param lock_timeout: Integer value, defaulting to 600 seconds, for + locks to clear. + + :param kwargs: Key value pairs to be passed to the Ironic API, + including uuid, name, chassis_uuid, driver_info, + parameters. + + :raises: OpenStackCloudException on operation error. + + :returns: Returns a ``munch.Munch`` representing the new + baremetal node. + """ + + msg = ("Baremetal machine node failed to be created.") + port_msg = ("Baremetal machine port failed to be created.") + + url = '/nodes' + # TODO(TheJulia): At some point we need to figure out how to + # handle data across when the requestor is defining newer items + # with the older api. + machine = self._baremetal_client.post(url, + json=kwargs, + error_message=msg, + microversion="1.6") + + created_nics = [] + try: + for row in nics: + payload = {'address': row['mac'], + 'node_uuid': machine['uuid']} + nic = self._baremetal_client.post('/ports', + json=payload, + error_message=port_msg) + created_nics.append(nic['uuid']) + + except Exception as e: + self.log.debug("ironic NIC registration failed", exc_info=True) + # TODO(mordred) Handle failures here + try: + for uuid in created_nics: + try: + port_url = '/ports/{uuid}'.format(uuid=uuid) + # NOTE(TheJulia): Added in hope that it is logged. + port_msg = ('Failed to delete port {port} for node' + '{node}').format(port=uuid, + node=machine['uuid']) + self._baremetal_client.delete(port_url, + error_message=port_msg) + except Exception: + pass + finally: + version = "1.6" + msg = "Baremetal machine failed to be deleted." + url = '/nodes/{node_id}'.format( + node_id=machine['uuid']) + self._baremetal_client.delete(url, + error_message=msg, + microversion=version) + raise OpenStackCloudException( + "Error registering NICs with the baremetal service: %s" + % str(e)) + + with _utils.shade_exceptions( + "Error transitioning node to available state"): + if wait: + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for node transition to " + "available state"): + + machine = self.get_machine(machine['uuid']) + + # Note(TheJulia): Per the Ironic state code, a node + # that fails returns to enroll state, which means a failed + # node cannot be determined at this point in time. + if machine['provision_state'] in ['enroll']: + self.node_set_provision_state( + machine['uuid'], 'manage') + elif machine['provision_state'] in ['manageable']: + self.node_set_provision_state( + machine['uuid'], 'provide') + elif machine['last_error'] is not None: + raise OpenStackCloudException( + "Machine encountered a failure: %s" + % machine['last_error']) + + # Note(TheJulia): Earlier versions of Ironic default to + # None and later versions default to available up until + # the introduction of enroll state. + # Note(TheJulia): The node will transition through + # cleaning if it is enabled, and we will wait for + # completion. + elif machine['provision_state'] in ['available', None]: + break + + else: + if machine['provision_state'] in ['enroll']: + self.node_set_provision_state(machine['uuid'], 'manage') + # Note(TheJulia): We need to wait for the lock to clear + # before we attempt to set the machine into provide state + # which allows for the transition to available. + for count in utils.iterate_timeout( + lock_timeout, + "Timeout waiting for reservation to clear " + "before setting provide state"): + machine = self.get_machine(machine['uuid']) + if (machine['reservation'] is None and + machine['provision_state'] is not 'enroll'): + # NOTE(TheJulia): In this case, the node has + # has moved on from the previous state and is + # likely not being verified, as no lock is + # present on the node. + self.node_set_provision_state( + machine['uuid'], 'provide') + machine = self.get_machine(machine['uuid']) + break + + elif machine['provision_state'] in [ + 'cleaning', + 'available']: + break + + elif machine['last_error'] is not None: + raise OpenStackCloudException( + "Machine encountered a failure: %s" + % machine['last_error']) + if not isinstance(machine, str): + return self._normalize_machine(machine) + else: + return machine + + def unregister_machine(self, nics, uuid, wait=False, timeout=600): + """Unregister Baremetal from Ironic + + Removes entries for Network Interfaces and baremetal nodes + from an Ironic API + + :param nics: An array of strings that consist of MAC addresses + to be removed. + :param string uuid: The UUID of the node to be deleted. + + :param wait: Boolean value, defaults to false, if to block the method + upon the final step of unregistering the machine. + + :param timeout: Integer value, representing seconds with a default + value of 600, which controls the maximum amount of + time to block the method's completion on. + + :raises: OpenStackCloudException on operation failure. + """ + + machine = self.get_machine(uuid) + invalid_states = ['active', 'cleaning', 'clean wait', 'clean failed'] + if machine['provision_state'] in invalid_states: + raise OpenStackCloudException( + "Error unregistering node '%s' due to current provision " + "state '%s'" % (uuid, machine['provision_state'])) + + # NOTE(TheJulia) There is a high possibility of a lock being present + # if the machine was just moved through the state machine. This was + # previously concealed by exception retry logic that detected the + # failure, and resubitted the request in python-ironicclient. + try: + self.wait_for_baremetal_node_lock(machine, timeout=timeout) + except OpenStackCloudException as e: + raise OpenStackCloudException("Error unregistering node '%s': " + "Exception occured while waiting " + "to be able to proceed: %s" + % (machine['uuid'], e)) + + for nic in nics: + port_msg = ("Error removing NIC {nic} from baremetal API for " + "node {uuid}").format(nic=nic, uuid=uuid) + port_url = '/ports/detail?address={mac}'.format(mac=nic['mac']) + port = self._baremetal_client.get(port_url, microversion=1.6, + error_message=port_msg) + port_url = '/ports/{uuid}'.format(uuid=port['ports'][0]['uuid']) + self._baremetal_client.delete(port_url, error_message=port_msg) + + with _utils.shade_exceptions( + "Error unregistering machine {node_id} from the baremetal " + "API".format(node_id=uuid)): + + # NOTE(TheJulia): While this should not matter microversion wise, + # ironic assumes all calls without an explicit microversion to be + # version 1.0. Ironic expects to deprecate support for older + # microversions in future releases, as such, we explicitly set + # the version to what we have been using with the client library.. + version = "1.6" + msg = "Baremetal machine failed to be deleted." + url = '/nodes/{node_id}'.format( + node_id=uuid) + self._baremetal_client.delete(url, + error_message=msg, + microversion=version) + + if wait: + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for machine to be deleted"): + if not self.get_machine(uuid): + break + + def patch_machine(self, name_or_id, patch): + """Patch Machine Information + + This method allows for an interface to manipulate node entries + within Ironic. + + :param node_id: The server object to attach to. + :param patch: + The JSON Patch document is a list of dictonary objects + that comply with RFC 6902 which can be found at + https://tools.ietf.org/html/rfc6902. + + Example patch construction:: + + patch=[] + patch.append({ + 'op': 'remove', + 'path': '/instance_info' + }) + patch.append({ + 'op': 'replace', + 'path': '/name', + 'value': 'newname' + }) + patch.append({ + 'op': 'add', + 'path': '/driver_info/username', + 'value': 'administrator' + }) + + :raises: OpenStackCloudException on operation error. + + :returns: ``munch.Munch`` representing the newly updated node. + """ + + msg = ("Error updating machine via patch operation on node " + "{node}".format(node=name_or_id)) + url = '/nodes/{node_id}'.format(node_id=name_or_id) + return self._normalize_machine( + self._baremetal_client.patch(url, + json=patch, + error_message=msg)) + + def update_machine(self, name_or_id, chassis_uuid=None, driver=None, + driver_info=None, name=None, instance_info=None, + instance_uuid=None, properties=None): + """Update a machine with new configuration information + + A user-friendly method to perform updates of a machine, in whole or + part. + + :param string name_or_id: A machine name or UUID to be updated. + :param string chassis_uuid: Assign a chassis UUID to the machine. + NOTE: As of the Kilo release, this value + cannot be changed once set. If a user + attempts to change this value, then the + Ironic API, as of Kilo, will reject the + request. + :param string driver: The driver name for controlling the machine. + :param dict driver_info: The dictonary defining the configuration + that the driver will utilize to control + the machine. Permutations of this are + dependent upon the specific driver utilized. + :param string name: A human relatable name to represent the machine. + :param dict instance_info: A dictonary of configuration information + that conveys to the driver how the host + is to be configured when deployed. + be deployed to the machine. + :param string instance_uuid: A UUID value representing the instance + that the deployed machine represents. + :param dict properties: A dictonary defining the properties of a + machine. + + :raises: OpenStackCloudException on operation error. + + :returns: ``munch.Munch`` containing a machine sub-dictonary consisting + of the updated data returned from the API update operation, + and a list named changes which contains all of the API paths + that received updates. + """ + machine = self.get_machine(name_or_id) + if not machine: + raise OpenStackCloudException( + "Machine update failed to find Machine: %s. " % name_or_id) + + machine_config = {} + new_config = {} + + try: + if chassis_uuid: + machine_config['chassis_uuid'] = machine['chassis_uuid'] + new_config['chassis_uuid'] = chassis_uuid + + if driver: + machine_config['driver'] = machine['driver'] + new_config['driver'] = driver + + if driver_info: + machine_config['driver_info'] = machine['driver_info'] + new_config['driver_info'] = driver_info + + if name: + machine_config['name'] = machine['name'] + new_config['name'] = name + + if instance_info: + machine_config['instance_info'] = machine['instance_info'] + new_config['instance_info'] = instance_info + + if instance_uuid: + machine_config['instance_uuid'] = machine['instance_uuid'] + new_config['instance_uuid'] = instance_uuid + + if properties: + machine_config['properties'] = machine['properties'] + new_config['properties'] = properties + except KeyError as e: + self.log.debug( + "Unexpected machine response missing key %s [%s]", + e.args[0], name_or_id) + raise OpenStackCloudException( + "Machine update failed - machine [%s] missing key %s. " + "Potential API issue." + % (name_or_id, e.args[0])) + + try: + patch = jsonpatch.JsonPatch.from_diff(machine_config, new_config) + except Exception as e: + raise OpenStackCloudException( + "Machine update failed - Error generating JSON patch object " + "for submission to the API. Machine: %s Error: %s" + % (name_or_id, str(e))) + + with _utils.shade_exceptions( + "Machine update failed - patch operation failed on Machine " + "{node}".format(node=name_or_id) + ): + if not patch: + return dict( + node=machine, + changes=None + ) + else: + machine = self.patch_machine(machine['uuid'], list(patch)) + change_list = [] + for change in list(patch): + change_list.append(change['path']) + return dict( + node=machine, + changes=change_list + ) + + def validate_node(self, uuid): + # TODO(TheJulia): There are soooooo many other interfaces + # that we can support validating, while these are essential, + # we should support more. + # TODO(TheJulia): Add a doc string :( + msg = ("Failed to query the API for validation status of " + "node {node_id}").format(node_id=uuid) + url = '/nodes/{node_id}/validate'.format(node_id=uuid) + ifaces = self._baremetal_client.get(url, error_message=msg) + + if not ifaces['deploy'] or not ifaces['power']: + raise OpenStackCloudException( + "ironic node %s failed to validate. " + "(deploy: %s, power: %s)" % (ifaces['deploy'], + ifaces['power'])) + + def node_set_provision_state(self, + name_or_id, + state, + configdrive=None, + wait=False, + timeout=3600): + """Set Node Provision State + + Enables a user to provision a Machine and optionally define a + config drive to be utilized. + + :param string name_or_id: The Name or UUID value representing the + baremetal node. + :param string state: The desired provision state for the + baremetal node. + :param string configdrive: An optional URL or file or path + representing the configdrive. In the + case of a directory, the client API + will create a properly formatted + configuration drive file and post the + file contents to the API for + deployment. + :param boolean wait: A boolean value, defaulted to false, to control + if the method will wait for the desire end state + to be reached before returning. + :param integer timeout: Integer value, defaulting to 3600 seconds, + representing the amount of time to wait for + the desire end state to be reached. + + :raises: OpenStackCloudException on operation error. + + :returns: ``munch.Munch`` representing the current state of the machine + upon exit of the method. + """ + # NOTE(TheJulia): Default microversion for this call is 1.6. + # Setting locally until we have determined our master plan regarding + # microversion handling. + version = "1.6" + msg = ("Baremetal machine node failed change provision state to " + "{state}".format(state=state)) + + url = '/nodes/{node_id}/states/provision'.format( + node_id=name_or_id) + payload = {'target': state} + if configdrive: + payload['configdrive'] = configdrive + + machine = self._baremetal_client.put(url, + json=payload, + error_message=msg, + microversion=version) + if wait: + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for node transition to " + "target state of '%s'" % state): + machine = self.get_machine(name_or_id) + if 'failed' in machine['provision_state']: + raise OpenStackCloudException( + "Machine encountered a failure.") + # NOTE(TheJulia): This performs matching if the requested + # end state matches the state the node has reached. + if state in machine['provision_state']: + break + # NOTE(TheJulia): This performs matching for cases where + # the reqeusted state action ends in available state. + if ("available" in machine['provision_state'] and + state in ["provide", "deleted"]): + break + else: + machine = self.get_machine(name_or_id) + return machine + + def set_machine_maintenance_state( + self, + name_or_id, + state=True, + reason=None): + """Set Baremetal Machine Maintenance State + + Sets Baremetal maintenance state and maintenance reason. + + :param string name_or_id: The Name or UUID value representing the + baremetal node. + :param boolean state: The desired state of the node. True being in + maintenance where as False means the machine + is not in maintenance mode. This value + defaults to True if not explicitly set. + :param string reason: An optional freeform string that is supplied to + the baremetal API to allow for notation as to why + the node is in maintenance state. + + :raises: OpenStackCloudException on operation error. + + :returns: None + """ + msg = ("Error setting machine maintenance state to {state} on node " + "{node}").format(state=state, node=name_or_id) + url = '/nodes/{name_or_id}/maintenance'.format(name_or_id=name_or_id) + if state: + payload = {'reason': reason} + self._baremetal_client.put(url, + json=payload, + error_message=msg) + else: + self._baremetal_client.delete(url, error_message=msg) + return None + + def remove_machine_from_maintenance(self, name_or_id): + """Remove Baremetal Machine from Maintenance State + + Similarly to set_machine_maintenance_state, this method + removes a machine from maintenance state. It must be noted + that this method simpily calls set_machine_maintenace_state + for the name_or_id requested and sets the state to False. + + :param string name_or_id: The Name or UUID value representing the + baremetal node. + + :raises: OpenStackCloudException on operation error. + + :returns: None + """ + self.set_machine_maintenance_state(name_or_id, False) + + def _set_machine_power_state(self, name_or_id, state): + """Set machine power state to on or off + + This private method allows a user to turn power on or off to + a node via the Baremetal API. + + :params string name_or_id: A string representing the baremetal + node to have power turned to an "on" + state. + :params string state: A value of "on", "off", or "reboot" that is + passed to the baremetal API to be asserted to + the machine. In the case of the "reboot" state, + Ironic will return the host to the "on" state. + + :raises: OpenStackCloudException on operation error or. + + :returns: None + """ + msg = ("Error setting machine power state to {state} on node " + "{node}").format(state=state, node=name_or_id) + url = '/nodes/{name_or_id}/states/power'.format(name_or_id=name_or_id) + if 'reboot' in state: + desired_state = 'rebooting' + else: + desired_state = 'power {state}'.format(state=state) + payload = {'target': desired_state} + self._baremetal_client.put(url, + json=payload, + error_message=msg, + microversion="1.6") + return None + + def set_machine_power_on(self, name_or_id): + """Activate baremetal machine power + + This is a method that sets the node power state to "on". + + :params string name_or_id: A string representing the baremetal + node to have power turned to an "on" + state. + + :raises: OpenStackCloudException on operation error. + + :returns: None + """ + self._set_machine_power_state(name_or_id, 'on') + + def set_machine_power_off(self, name_or_id): + """De-activate baremetal machine power + + This is a method that sets the node power state to "off". + + :params string name_or_id: A string representing the baremetal + node to have power turned to an "off" + state. + + :raises: OpenStackCloudException on operation error. + + :returns: + """ + self._set_machine_power_state(name_or_id, 'off') + + def set_machine_power_reboot(self, name_or_id): + """De-activate baremetal machine power + + This is a method that sets the node power state to "reboot", which + in essence changes the machine power state to "off", and that back + to "on". + + :params string name_or_id: A string representing the baremetal + node to have power turned to an "off" + state. + + :raises: OpenStackCloudException on operation error. + + :returns: None + """ + self._set_machine_power_state(name_or_id, 'reboot') + + def activate_node(self, uuid, configdrive=None, + wait=False, timeout=1200): + self.node_set_provision_state( + uuid, 'active', configdrive, wait=wait, timeout=timeout) + + def deactivate_node(self, uuid, wait=False, + timeout=1200): + self.node_set_provision_state( + uuid, 'deleted', wait=wait, timeout=timeout) + + def set_node_instance_info(self, uuid, patch): + msg = ("Error updating machine via patch operation on node " + "{node}".format(node=uuid)) + url = '/nodes/{node_id}'.format(node_id=uuid) + return self._baremetal_client.patch(url, + json=patch, + error_message=msg) + + def purge_node_instance_info(self, uuid): + patch = [] + patch.append({'op': 'remove', 'path': '/instance_info'}) + msg = ("Error updating machine via patch operation on node " + "{node}".format(node=uuid)) + url = '/nodes/{node_id}'.format(node_id=uuid) + return self._baremetal_client.patch(url, + json=patch, + error_message=msg) + + def wait_for_baremetal_node_lock(self, node, timeout=30): + """Wait for a baremetal node to have no lock. + + Baremetal nodes in ironic have a reservation lock that + is used to represent that a conductor has locked the node + while performing some sort of action, such as changing + configuration as a result of a machine state change. + + This lock can occur during power syncronization, and prevents + updates to objects attached to the node, such as ports. + + In the vast majority of cases, locks should clear in a few + seconds, and as such this method will only wait for 30 seconds. + The default wait is two seconds between checking if the lock + has cleared. + + This method is intended for use by methods that need to + gracefully block without genreating errors, however this + method does prevent another client or a timer from + triggering a lock immediately after we see the lock as + having cleared. + + :param node: The json representation of the node, + specificially looking for the node + 'uuid' and 'reservation' fields. + :param timeout: Integer in seconds to wait for the + lock to clear. Default: 30 + + :raises: OpenStackCloudException upon client failure. + :returns: None + """ + # TODO(TheJulia): This _can_ still fail with a race + # condition in that between us checking the status, + # a conductor where the conductor could still obtain + # a lock before we are able to obtain a lock. + # This means we should handle this with such conections + + if node['reservation'] is None: + return + else: + msg = 'Waiting for lock to be released for node {node}'.format( + node=node['uuid']) + for count in utils.iterate_timeout(timeout, msg, 2): + current_node = self.get_machine(node['uuid']) + if current_node['reservation'] is None: + return + + @_utils.valid_kwargs('type', 'service_type', 'description') + def create_service(self, name, enabled=True, **kwargs): + """Create a service. + + :param name: Service name. + :param type: Service type. (type or service_type required.) + :param service_type: Service type. (type or service_type required.) + :param description: Service description (optional). + :param enabled: Whether the service is enabled (v3 only) + + :returns: a ``munch.Munch`` containing the services description, + i.e. the following attributes:: + - id: + - name: + - type: + - service_type: + - description: + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call. + + """ + type_ = kwargs.pop('type', None) + service_type = kwargs.pop('service_type', None) + + # TODO(mordred) When this changes to REST, force interface=admin + # in the adapter call + if self._is_client_version('identity', 2): + url, key = '/OS-KSADM/services', 'OS-KSADM:service' + kwargs['type'] = type_ or service_type + else: + url, key = '/services', 'service' + kwargs['type'] = type_ or service_type + kwargs['enabled'] = enabled + kwargs['name'] = name + + msg = 'Failed to create service {name}'.format(name=name) + data = self._identity_client.post( + url, json={key: kwargs}, error_message=msg) + service = self._get_and_munchify(key, data) + return _utils.normalize_keystone_services([service])[0] + + @_utils.valid_kwargs('name', 'enabled', 'type', 'service_type', + 'description') + def update_service(self, name_or_id, **kwargs): + # NOTE(SamYaple): Service updates are only available on v3 api + if self._is_client_version('identity', 2): + raise OpenStackCloudUnavailableFeature( + 'Unavailable Feature: Service update requires Identity v3' + ) + + # NOTE(SamYaple): Keystone v3 only accepts 'type' but shade accepts + # both 'type' and 'service_type' with a preference + # towards 'type' + type_ = kwargs.pop('type', None) + service_type = kwargs.pop('service_type', None) + if type_ or service_type: + kwargs['type'] = type_ or service_type + + if self._is_client_version('identity', 2): + url, key = '/OS-KSADM/services', 'OS-KSADM:service' + else: + url, key = '/services', 'service' + + service = self.get_service(name_or_id) + msg = 'Error in updating service {service}'.format(service=name_or_id) + data = self._identity_client.patch( + '{url}/{id}'.format(url=url, id=service['id']), json={key: kwargs}, + endpoint_filter={'interface': 'admin'}, error_message=msg) + service = self._get_and_munchify(key, data) + return _utils.normalize_keystone_services([service])[0] + + def list_services(self): + """List all Keystone services. + + :returns: a list of ``munch.Munch`` containing the services description + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call. + """ + if self._is_client_version('identity', 2): + url, key = '/OS-KSADM/services', 'OS-KSADM:services' + else: + url, key = '/services', 'services' + data = self._identity_client.get( + url, endpoint_filter={'interface': 'admin'}, + error_message="Failed to list services") + services = self._get_and_munchify(key, data) + return _utils.normalize_keystone_services(services) + + def search_services(self, name_or_id=None, filters=None): + """Search Keystone services. + + :param name_or_id: Name or id of the desired service. + :param filters: a dict containing additional filters to use. e.g. + {'type': 'network'}. + + :returns: a list of ``munch.Munch`` containing the services description + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call. + """ + services = self.list_services() + return _utils._filter_list(services, name_or_id, filters) + + def get_service(self, name_or_id, filters=None): + """Get exactly one Keystone service. + + :param name_or_id: Name or id of the desired service. + :param filters: a dict containing additional filters to use. e.g. + {'type': 'network'} + + :returns: a ``munch.Munch`` containing the services description, + i.e. the following attributes:: + - id: + - name: + - type: + - description: + + :raises: ``OpenStackCloudException`` if something goes wrong during the + openstack API call or if multiple matches are found. + """ + return _utils._get_entity(self, 'service', name_or_id, filters) + + def delete_service(self, name_or_id): + """Delete a Keystone service. + + :param name_or_id: Service name or id. + + :returns: True if delete succeeded, False otherwise. + + :raises: ``OpenStackCloudException`` if something goes wrong during + the openstack API call + """ + service = self.get_service(name_or_id=name_or_id) + if service is None: + self.log.debug("Service %s not found for deleting", name_or_id) + return False + + if self._is_client_version('identity', 2): + url = '/OS-KSADM/services' + else: + url = '/services' + + error_msg = 'Failed to delete service {id}'.format(id=service['id']) + self._identity_client.delete( + '{url}/{id}'.format(url=url, id=service['id']), + endpoint_filter={'interface': 'admin'}, error_message=error_msg) + + return True + + @_utils.valid_kwargs('public_url', 'internal_url', 'admin_url') + def create_endpoint(self, service_name_or_id, url=None, interface=None, + region=None, enabled=True, **kwargs): + """Create a Keystone endpoint. + + :param service_name_or_id: Service name or id for this endpoint. + :param url: URL of the endpoint + :param interface: Interface type of the endpoint + :param public_url: Endpoint public URL. + :param internal_url: Endpoint internal URL. + :param admin_url: Endpoint admin URL. + :param region: Endpoint region. + :param enabled: Whether the endpoint is enabled + + NOTE: Both v2 (public_url, internal_url, admin_url) and v3 + (url, interface) calling semantics are supported. But + you can only use one of them at a time. + + :returns: a list of ``munch.Munch`` containing the endpoint description + + :raises: OpenStackCloudException if the service cannot be found or if + something goes wrong during the openstack API call. + """ + public_url = kwargs.pop('public_url', None) + internal_url = kwargs.pop('internal_url', None) + admin_url = kwargs.pop('admin_url', None) + + if (url or interface) and (public_url or internal_url or admin_url): + raise OpenStackCloudException( + "create_endpoint takes either url and interface OR" + " public_url, internal_url, admin_url") + + service = self.get_service(name_or_id=service_name_or_id) + if service is None: + raise OpenStackCloudException("service {service} not found".format( + service=service_name_or_id)) + + if self._is_client_version('identity', 2): + if url: + # v2.0 in use, v3-like arguments, one endpoint created + if interface != 'public': + raise OpenStackCloudException( + "Error adding endpoint for service {service}." + " On a v2 cloud the url/interface API may only be" + " used for public url. Try using the public_url," + " internal_url, admin_url parameters instead of" + " url and interface".format( + service=service_name_or_id)) + endpoint_args = {'publicurl': url} + else: + # v2.0 in use, v2.0-like arguments, one endpoint created + endpoint_args = {} + if public_url: + endpoint_args.update({'publicurl': public_url}) + if internal_url: + endpoint_args.update({'internalurl': internal_url}) + if admin_url: + endpoint_args.update({'adminurl': admin_url}) + + # keystone v2.0 requires 'region' arg even if it is None + endpoint_args.update( + {'service_id': service['id'], 'region': region}) + + data = self._identity_client.post( + '/endpoints', json={'endpoint': endpoint_args}, + endpoint_filter={'interface': 'admin'}, + error_message=("Failed to create endpoint for service" + " {service}".format(service=service['name']))) + return [self._get_and_munchify('endpoint', data)] + else: + endpoints_args = [] + if url: + # v3 in use, v3-like arguments, one endpoint created + endpoints_args.append( + {'url': url, 'interface': interface, + 'service_id': service['id'], 'enabled': enabled, + 'region': region}) + else: + # v3 in use, v2.0-like arguments, one endpoint created for each + # interface url provided + endpoint_args = {'region': region, 'enabled': enabled, + 'service_id': service['id']} + if public_url: + endpoint_args.update({'url': public_url, + 'interface': 'public'}) + endpoints_args.append(endpoint_args.copy()) + if internal_url: + endpoint_args.update({'url': internal_url, + 'interface': 'internal'}) + endpoints_args.append(endpoint_args.copy()) + if admin_url: + endpoint_args.update({'url': admin_url, + 'interface': 'admin'}) + endpoints_args.append(endpoint_args.copy()) + + endpoints = [] + error_msg = ("Failed to create endpoint for service" + " {service}".format(service=service['name'])) + for args in endpoints_args: + data = self._identity_client.post( + '/endpoints', json={'endpoint': args}, + error_message=error_msg) + endpoints.append(self._get_and_munchify('endpoint', data)) + return endpoints + + @_utils.valid_kwargs('enabled', 'service_name_or_id', 'url', 'interface', + 'region') + def update_endpoint(self, endpoint_id, **kwargs): + # NOTE(SamYaple): Endpoint updates are only available on v3 api + if self._is_client_version('identity', 2): + raise OpenStackCloudUnavailableFeature( + 'Unavailable Feature: Endpoint update' + ) + + service_name_or_id = kwargs.pop('service_name_or_id', None) + if service_name_or_id is not None: + kwargs['service_id'] = service_name_or_id + + data = self._identity_client.patch( + '/endpoints/{}'.format(endpoint_id), json={'endpoint': kwargs}, + error_message="Failed to update endpoint {}".format(endpoint_id)) + return self._get_and_munchify('endpoint', data) + + def list_endpoints(self): + """List Keystone endpoints. + + :returns: a list of ``munch.Munch`` containing the endpoint description + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + # Force admin interface if v2.0 is in use + v2 = self._is_client_version('identity', 2) + kwargs = {'endpoint_filter': {'interface': 'admin'}} if v2 else {} + + data = self._identity_client.get( + '/endpoints', error_message="Failed to list endpoints", **kwargs) + endpoints = self._get_and_munchify('endpoints', data) + + return endpoints + + def search_endpoints(self, id=None, filters=None): + """List Keystone endpoints. + + :param id: endpoint id. + :param filters: a dict containing additional filters to use. e.g. + {'region': 'region-a.geo-1'} + + :returns: a list of ``munch.Munch`` containing the endpoint + description. Each dict contains the following attributes:: + - id: + - region: + - public_url: + - internal_url: (optional) + - admin_url: (optional) + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + # NOTE(SamYaple): With keystone v3 we can filter directly via the + # the keystone api, but since the return of all the endpoints even in + # large environments is small, we can continue to filter in shade just + # like the v2 api. + endpoints = self.list_endpoints() + return _utils._filter_list(endpoints, id, filters) + + def get_endpoint(self, id, filters=None): + """Get exactly one Keystone endpoint. + + :param id: endpoint id. + :param filters: a dict containing additional filters to use. e.g. + {'region': 'region-a.geo-1'} + + :returns: a ``munch.Munch`` containing the endpoint description. + i.e. a ``munch.Munch`` containing the following attributes:: + - id: + - region: + - public_url: + - internal_url: (optional) + - admin_url: (optional) + """ + return _utils._get_entity(self, 'endpoint', id, filters) + + def delete_endpoint(self, id): + """Delete a Keystone endpoint. + + :param id: Id of the endpoint to delete. + + :returns: True if delete succeeded, False otherwise. + + :raises: ``OpenStackCloudException`` if something goes wrong during + the openstack API call. + """ + endpoint = self.get_endpoint(id=id) + if endpoint is None: + self.log.debug("Endpoint %s not found for deleting", id) + return False + + # Force admin interface if v2.0 is in use + v2 = self._is_client_version('identity', 2) + kwargs = {'endpoint_filter': {'interface': 'admin'}} if v2 else {} + + error_msg = "Failed to delete endpoint {id}".format(id=id) + self._identity_client.delete('/endpoints/{id}'.format(id=id), + error_message=error_msg, **kwargs) + + return True + + def create_domain(self, name, description=None, enabled=True): + """Create a domain. + + :param name: The name of the domain. + :param description: A description of the domain. + :param enabled: Is the domain enabled or not (default True). + + :returns: a ``munch.Munch`` containing the domain representation. + + :raise OpenStackCloudException: if the domain cannot be created. + """ + domain_ref = {'name': name, 'enabled': enabled} + if description is not None: + domain_ref['description'] = description + msg = 'Failed to create domain {name}'.format(name=name) + data = self._identity_client.post( + '/domains', json={'domain': domain_ref}, error_message=msg) + domain = self._get_and_munchify('domain', data) + return _utils.normalize_domains([domain])[0] + + def update_domain( + self, domain_id=None, name=None, description=None, + enabled=None, name_or_id=None): + if domain_id is None: + if name_or_id is None: + raise OpenStackCloudException( + "You must pass either domain_id or name_or_id value" + ) + dom = self.get_domain(None, name_or_id) + if dom is None: + raise OpenStackCloudException( + "Domain {0} not found for updating".format(name_or_id) + ) + domain_id = dom['id'] + + domain_ref = {} + domain_ref.update({'name': name} if name else {}) + domain_ref.update({'description': description} if description else {}) + domain_ref.update({'enabled': enabled} if enabled is not None else {}) + + error_msg = "Error in updating domain {id}".format(id=domain_id) + data = self._identity_client.patch( + '/domains/{id}'.format(id=domain_id), + json={'domain': domain_ref}, error_message=error_msg) + domain = self._get_and_munchify('domain', data) + return _utils.normalize_domains([domain])[0] + + def delete_domain(self, domain_id=None, name_or_id=None): + """Delete a domain. + + :param domain_id: ID of the domain to delete. + :param name_or_id: Name or ID of the domain to delete. + + :returns: True if delete succeeded, False otherwise. + + :raises: ``OpenStackCloudException`` if something goes wrong during + the openstack API call. + """ + if domain_id is None: + if name_or_id is None: + raise OpenStackCloudException( + "You must pass either domain_id or name_or_id value" + ) + dom = self.get_domain(name_or_id=name_or_id) + if dom is None: + self.log.debug( + "Domain %s not found for deleting", name_or_id) + return False + domain_id = dom['id'] + + # A domain must be disabled before deleting + self.update_domain(domain_id, enabled=False) + error_msg = "Failed to delete domain {id}".format(id=domain_id) + self._identity_client.delete('/domains/{id}'.format(id=domain_id), + error_message=error_msg) + + return True + + def list_domains(self, **filters): + """List Keystone domains. + + :returns: a list of ``munch.Munch`` containing the domain description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + data = self._identity_client.get( + '/domains', params=filters, error_message="Failed to list domains") + domains = self._get_and_munchify('domains', data) + return _utils.normalize_domains(domains) + + def search_domains(self, filters=None, name_or_id=None): + """Search Keystone domains. + + :param name_or_id: domain name or id + :param dict filters: A dict containing additional filters to use. + Keys to search on are id, name, enabled and description. + + :returns: a list of ``munch.Munch`` containing the domain description. + Each ``munch.Munch`` contains the following attributes:: + - id: + - name: + - description: + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + if filters is None: + filters = {} + if name_or_id is not None: + domains = self.list_domains() + return _utils._filter_list(domains, name_or_id, filters) + else: + return self.list_domains(**filters) + + def get_domain(self, domain_id=None, name_or_id=None, filters=None): + """Get exactly one Keystone domain. + + :param domain_id: domain id. + :param name_or_id: domain name or id. + :param dict filters: A dict containing additional filters to use. + Keys to search on are id, name, enabled and description. + + :returns: a ``munch.Munch`` containing the domain description, or None + if not found. Each ``munch.Munch`` contains the following + attributes:: + - id: + - name: + - description: + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + if domain_id is None: + # NOTE(SamYaple): search_domains() has filters and name_or_id + # in the wrong positional order which prevents _get_entity from + # being able to return quickly if passing a domain object so we + # duplicate that logic here + if hasattr(name_or_id, 'id'): + return name_or_id + return _utils._get_entity(self, 'domain', filters, name_or_id) + else: + error_msg = 'Failed to get domain {id}'.format(id=domain_id) + data = self._identity_client.get( + '/domains/{id}'.format(id=domain_id), + error_message=error_msg) + domain = self._get_and_munchify('domain', data) + return _utils.normalize_domains([domain])[0] + + @_utils.valid_kwargs('domain_id') + @_utils.cache_on_arguments() + def list_groups(self, **kwargs): + """List Keystone Groups. + + :param domain_id: domain id. + + :returns: A list of ``munch.Munch`` containing the group description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + data = self._identity_client.get( + '/groups', params=kwargs, error_message="Failed to list groups") + return _utils.normalize_groups(self._get_and_munchify('groups', data)) + + @_utils.valid_kwargs('domain_id') + def search_groups(self, name_or_id=None, filters=None, **kwargs): + """Search Keystone groups. + + :param name: Group name or id. + :param filters: A dict containing additional filters to use. + :param domain_id: domain id. + + :returns: A list of ``munch.Munch`` containing the group description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + groups = self.list_groups(**kwargs) + return _utils._filter_list(groups, name_or_id, filters) + + @_utils.valid_kwargs('domain_id') + def get_group(self, name_or_id, filters=None, **kwargs): + """Get exactly one Keystone group. + + :param id: Group name or id. + :param filters: A dict containing additional filters to use. + :param domain_id: domain id. + + :returns: A ``munch.Munch`` containing the group description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + return _utils._get_entity(self, 'group', name_or_id, filters, **kwargs) + + def create_group(self, name, description, domain=None): + """Create a group. + + :param string name: Group name. + :param string description: Group description. + :param string domain: Domain name or ID for the group. + + :returns: A ``munch.Munch`` containing the group description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + group_ref = {'name': name} + if description: + group_ref['description'] = description + if domain: + dom = self.get_domain(domain) + if not dom: + raise OpenStackCloudException( + "Creating group {group} failed: Invalid domain " + "{domain}".format(group=name, domain=domain) + ) + group_ref['domain_id'] = dom['id'] + + error_msg = "Error creating group {group}".format(group=name) + data = self._identity_client.post( + '/groups', json={'group': group_ref}, error_message=error_msg) + group = self._get_and_munchify('group', data) + self.list_groups.invalidate(self) + return _utils.normalize_groups([group])[0] + + @_utils.valid_kwargs('domain_id') + def update_group(self, name_or_id, name=None, description=None, + **kwargs): + """Update an existing group + + :param string name: New group name. + :param string description: New group description. + :param domain_id: domain id. + + :returns: A ``munch.Munch`` containing the group description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + self.list_groups.invalidate(self) + group = self.get_group(name_or_id, **kwargs) + if group is None: + raise OpenStackCloudException( + "Group {0} not found for updating".format(name_or_id) + ) + + group_ref = {} + if name: + group_ref['name'] = name + if description: + group_ref['description'] = description + + error_msg = "Unable to update group {name}".format(name=name_or_id) + data = self._identity_client.patch( + '/groups/{id}'.format(id=group['id']), + json={'group': group_ref}, error_message=error_msg) + group = self._get_and_munchify('group', data) + self.list_groups.invalidate(self) + return _utils.normalize_groups([group])[0] + + @_utils.valid_kwargs('domain_id') + def delete_group(self, name_or_id, **kwargs): + """Delete a group + + :param name_or_id: ID or name of the group to delete. + :param domain_id: domain id. + + :returns: True if delete succeeded, False otherwise. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + group = self.get_group(name_or_id, **kwargs) + if group is None: + self.log.debug( + "Group %s not found for deleting", name_or_id) + return False + + error_msg = "Unable to delete group {name}".format(name=name_or_id) + self._identity_client.delete('/groups/{id}'.format(id=group['id']), + error_message=error_msg) + + self.list_groups.invalidate(self) + return True + + @_utils.valid_kwargs('domain_id') + def list_roles(self, **kwargs): + """List Keystone roles. + + :param domain_id: domain id for listing roles (v3) + + :returns: a list of ``munch.Munch`` containing the role description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + v2 = self._is_client_version('identity', 2) + url = '/OS-KSADM/roles' if v2 else '/roles' + data = self._identity_client.get( + url, params=kwargs, error_message="Failed to list roles") + return self._normalize_roles(self._get_and_munchify('roles', data)) + + @_utils.valid_kwargs('domain_id') + def search_roles(self, name_or_id=None, filters=None, **kwargs): + """Seach Keystone roles. + + :param string name: role name or id. + :param dict filters: a dict containing additional filters to use. + :param domain_id: domain id (v3) + + :returns: a list of ``munch.Munch`` containing the role description. + Each ``munch.Munch`` contains the following attributes:: + + - id: + - name: + - description: + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + roles = self.list_roles(**kwargs) + return _utils._filter_list(roles, name_or_id, filters) + + @_utils.valid_kwargs('domain_id') + def get_role(self, name_or_id, filters=None, **kwargs): + """Get exactly one Keystone role. + + :param id: role name or id. + :param filters: a dict containing additional filters to use. + :param domain_id: domain id (v3) + + :returns: a single ``munch.Munch`` containing the role description. + Each ``munch.Munch`` contains the following attributes:: + + - id: + - name: + - description: + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + return _utils._get_entity(self, 'role', name_or_id, filters, **kwargs) + + def _keystone_v2_role_assignments(self, user, project=None, + role=None, **kwargs): + data = self._identity_client.get( + "/tenants/{tenant}/users/{user}/roles".format( + tenant=project, user=user), + error_message="Failed to list role assignments") + + roles = self._get_and_munchify('roles', data) + + ret = [] + for tmprole in roles: + if role is not None and role != tmprole.id: + continue + ret.append({ + 'role': { + 'id': tmprole.id + }, + 'scope': { + 'project': { + 'id': project, + } + }, + 'user': { + 'id': user, + } + }) + return ret + + def _keystone_v3_role_assignments(self, **filters): + # NOTE(samueldmq): different parameters have different representation + # patterns as query parameters in the call to the list role assignments + # API. The code below handles each set of patterns separately and + # renames the parameters names accordingly, ignoring 'effective', + # 'include_names' and 'include_subtree' whose do not need any renaming. + for k in ('group', 'role', 'user'): + if k in filters: + filters[k + '.id'] = filters[k] + del filters[k] + for k in ('project', 'domain'): + if k in filters: + filters['scope.' + k + '.id'] = filters[k] + del filters[k] + if 'os_inherit_extension_inherited_to' in filters: + filters['scope.OS-INHERIT:inherited_to'] = ( + filters['os_inherit_extension_inherited_to']) + del filters['os_inherit_extension_inherited_to'] + + data = self._identity_client.get( + '/role_assignments', params=filters, + error_message="Failed to list role assignments") + return self._get_and_munchify('role_assignments', data) + + def list_role_assignments(self, filters=None): + """List Keystone role assignments + + :param dict filters: Dict of filter conditions. Acceptable keys are: + + * 'user' (string) - User ID to be used as query filter. + * 'group' (string) - Group ID to be used as query filter. + * 'project' (string) - Project ID to be used as query filter. + * 'domain' (string) - Domain ID to be used as query filter. + * 'role' (string) - Role ID to be used as query filter. + * 'os_inherit_extension_inherited_to' (string) - Return inherited + role assignments for either 'projects' or 'domains' + * 'effective' (boolean) - Return effective role assignments. + * 'include_subtree' (boolean) - Include subtree + + 'user' and 'group' are mutually exclusive, as are 'domain' and + 'project'. + + NOTE: For keystone v2, only user, project, and role are used. + Project and user are both required in filters. + + :returns: a list of ``munch.Munch`` containing the role assignment + description. Contains the following attributes:: + + - id: + - user|group: + - project|domain: + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + # NOTE(samueldmq): although 'include_names' is a valid query parameter + # in the keystone v3 list role assignments API, it would have NO effect + # on shade due to normalization. It is not documented as an acceptable + # filter in the docs above per design! + + if not filters: + filters = {} + + # NOTE(samueldmq): the docs above say filters are *IDs*, though if + # munch.Munch objects are passed, this still works for backwards + # compatibility as keystoneclient allows either IDs or objects to be + # passed in. + # TODO(samueldmq): fix the docs above to advertise munch.Munch objects + # can be provided as parameters too + for k, v in filters.items(): + if isinstance(v, munch.Munch): + filters[k] = v['id'] + + if self._is_client_version('identity', 2): + if filters.get('project') is None or filters.get('user') is None: + raise OpenStackCloudException( + "Must provide project and user for keystone v2" + ) + assignments = self._keystone_v2_role_assignments(**filters) + else: + assignments = self._keystone_v3_role_assignments(**filters) + + return _utils.normalize_role_assignments(assignments) + + def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", + ephemeral=0, swap=0, rxtx_factor=1.0, is_public=True): + """Create a new flavor. + + :param name: Descriptive name of the flavor + :param ram: Memory in MB for the flavor + :param vcpus: Number of VCPUs for the flavor + :param disk: Size of local disk in GB + :param flavorid: ID for the flavor (optional) + :param ephemeral: Ephemeral space size in GB + :param swap: Swap space in MB + :param rxtx_factor: RX/TX factor + :param is_public: Make flavor accessible to the public + + :returns: A ``munch.Munch`` describing the new flavor. + + :raises: OpenStackCloudException on operation error. + """ + with _utils.shade_exceptions("Failed to create flavor {name}".format( + name=name)): + payload = { + 'disk': disk, + 'OS-FLV-EXT-DATA:ephemeral': ephemeral, + 'id': flavorid, + 'os-flavor-access:is_public': is_public, + 'name': name, + 'ram': ram, + 'rxtx_factor': rxtx_factor, + 'swap': swap, + 'vcpus': vcpus, + } + if flavorid == 'auto': + payload['id'] = None + data = _adapter._json_response(self._conn.compute.post( + '/flavors', + json=dict(flavor=payload))) + + return self._normalize_flavor( + self._get_and_munchify('flavor', data)) + + def delete_flavor(self, name_or_id): + """Delete a flavor + + :param name_or_id: ID or name of the flavor to delete. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException on operation error. + """ + flavor = self.get_flavor(name_or_id, get_extra=False) + if flavor is None: + self.log.debug( + "Flavor %s not found for deleting", name_or_id) + return False + + _adapter._json_response( + self._conn.compute.delete( + '/flavors/{id}'.format(id=flavor['id'])), + error_message="Unable to delete flavor {name}".format( + name=name_or_id)) + + return True + + def set_flavor_specs(self, flavor_id, extra_specs): + """Add extra specs to a flavor + + :param string flavor_id: ID of the flavor to update. + :param dict extra_specs: Dictionary of key-value pairs. + + :raises: OpenStackCloudException on operation error. + :raises: OpenStackCloudResourceNotFound if flavor ID is not found. + """ + _adapter._json_response( + self._conn.compute.post( + "/flavors/{id}/os-extra_specs".format(id=flavor_id), + json=dict(extra_specs=extra_specs)), + error_message="Unable to set flavor specs") + + def unset_flavor_specs(self, flavor_id, keys): + """Delete extra specs from a flavor + + :param string flavor_id: ID of the flavor to update. + :param keys: List of spec keys to delete. + + :raises: OpenStackCloudException on operation error. + :raises: OpenStackCloudResourceNotFound if flavor ID is not found. + """ + for key in keys: + _adapter._json_response( + self._conn.compute.delete( + "/flavors/{id}/os-extra_specs/{key}".format( + id=flavor_id, key=key)), + error_message="Unable to delete flavor spec {0}".format(key)) + + def _mod_flavor_access(self, action, flavor_id, project_id): + """Common method for adding and removing flavor access + """ + with _utils.shade_exceptions("Error trying to {action} access from " + "flavor ID {flavor}".format( + action=action, flavor=flavor_id)): + endpoint = '/flavors/{id}/action'.format(id=flavor_id) + access = {'tenant': project_id} + access_key = '{action}TenantAccess'.format(action=action) + + _adapter._json_response( + self._conn.compute.post(endpoint, json={access_key: access})) + + def add_flavor_access(self, flavor_id, project_id): + """Grant access to a private flavor for a project/tenant. + + :param string flavor_id: ID of the private flavor. + :param string project_id: ID of the project/tenant. + + :raises: OpenStackCloudException on operation error. + """ + self._mod_flavor_access('add', flavor_id, project_id) + + def remove_flavor_access(self, flavor_id, project_id): + """Revoke access from a private flavor for a project/tenant. + + :param string flavor_id: ID of the private flavor. + :param string project_id: ID of the project/tenant. + + :raises: OpenStackCloudException on operation error. + """ + self._mod_flavor_access('remove', flavor_id, project_id) + + def list_flavor_access(self, flavor_id): + """List access from a private flavor for a project/tenant. + + :param string flavor_id: ID of the private flavor. + + :returns: a list of ``munch.Munch`` containing the access description + + :raises: OpenStackCloudException on operation error. + """ + data = _adapter._json_response( + self._conn.compute.get( + '/flavors/{id}/os-flavor-access'.format(id=flavor_id)), + error_message=( + "Error trying to list access from flavorID {flavor}".format( + flavor=flavor_id))) + return _utils.normalize_flavor_accesses( + self._get_and_munchify('flavor_access', data)) + + @_utils.valid_kwargs('domain_id') + def create_role(self, name, **kwargs): + """Create a Keystone role. + + :param string name: The name of the role. + :param domain_id: domain id (v3) + + :returns: a ``munch.Munch`` containing the role description + + :raise OpenStackCloudException: if the role cannot be created + """ + v2 = self._is_client_version('identity', 2) + url = '/OS-KSADM/roles' if v2 else '/roles' + kwargs['name'] = name + msg = 'Failed to create role {name}'.format(name=name) + data = self._identity_client.post( + url, json={'role': kwargs}, error_message=msg) + role = self._get_and_munchify('role', data) + return self._normalize_role(role) + + @_utils.valid_kwargs('domain_id') + def update_role(self, name_or_id, name, **kwargs): + """Update a Keystone role. + + :param name_or_id: Name or id of the role to update + :param string name: The new role name + :param domain_id: domain id + + :returns: a ``munch.Munch`` containing the role description + + :raise OpenStackCloudException: if the role cannot be created + """ + if self._is_client_version('identity', 2): + raise OpenStackCloudUnavailableFeature( + 'Unavailable Feature: Role update requires Identity v3' + ) + kwargs['name_or_id'] = name_or_id + role = self.get_role(**kwargs) + if role is None: + self.log.debug( + "Role %s not found for updating", name_or_id) + return False + msg = 'Failed to update role {name}'.format(name=name_or_id) + json_kwargs = {'role_id': role.id, 'role': {'name': name}} + data = self._identity_client.patch('/roles', error_message=msg, + json=json_kwargs) + role = self._get_and_munchify('role', data) + return self._normalize_role(role) + + @_utils.valid_kwargs('domain_id') + def delete_role(self, name_or_id, **kwargs): + """Delete a Keystone role. + + :param string id: Name or id of the role to delete. + :param domain_id: domain id (v3) + + :returns: True if delete succeeded, False otherwise. + + :raises: ``OpenStackCloudException`` if something goes wrong during + the openstack API call. + """ + role = self.get_role(name_or_id, **kwargs) + if role is None: + self.log.debug( + "Role %s not found for deleting", name_or_id) + return False + + v2 = self._is_client_version('identity', 2) + url = '{preffix}/{id}'.format( + preffix='/OS-KSADM/roles' if v2 else '/roles', id=role['id']) + error_msg = "Unable to delete role {name}".format(name=name_or_id) + self._identity_client.delete(url, error_message=error_msg) + + return True + + def _get_grant_revoke_params(self, role, user=None, group=None, + project=None, domain=None): + role = self.get_role(role) + if role is None: + return {} + data = {'role': role.id} + + # domain and group not available in keystone v2.0 + is_keystone_v2 = self._is_client_version('identity', 2) + + filters = {} + if not is_keystone_v2 and domain: + filters['domain_id'] = data['domain'] = \ + self.get_domain(domain)['id'] + + if user: + data['user'] = self.get_user(user, filters=filters) + + if project: + # drop domain in favor of project + data.pop('domain', None) + data['project'] = self.get_project(project, filters=filters) + + if not is_keystone_v2 and group: + data['group'] = self.get_group(group, filters=filters) + + return data + + def grant_role(self, name_or_id, user=None, group=None, + project=None, domain=None, wait=False, timeout=60): + """Grant a role to a user. + + :param string name_or_id: The name or id of the role. + :param string user: The name or id of the user. + :param string group: The name or id of the group. (v3) + :param string project: The name or id of the project. + :param string domain: The id of the domain. (v3) + :param bool wait: Wait for role to be granted + :param int timeout: Timeout to wait for role to be granted + + NOTE: domain is a required argument when the grant is on a project, + user or group specified by name. In that situation, they are all + considered to be in that domain. If different domains are in use + in the same role grant, it is required to specify those by ID. + + NOTE: for wait and timeout, sometimes granting roles is not + instantaneous. + + NOTE: project is required for keystone v2 + + :returns: True if the role is assigned, otherwise False + + :raise OpenStackCloudException: if the role cannot be granted + """ + data = self._get_grant_revoke_params(name_or_id, user, group, + project, domain) + filters = data.copy() + if not data: + raise OpenStackCloudException( + 'Role {0} not found.'.format(name_or_id)) + + if data.get('user') is not None and data.get('group') is not None: + raise OpenStackCloudException( + 'Specify either a group or a user, not both') + if data.get('user') is None and data.get('group') is None: + raise OpenStackCloudException( + 'Must specify either a user or a group') + if self._is_client_version('identity', 2) and \ + data.get('project') is None: + raise OpenStackCloudException( + 'Must specify project for keystone v2') + + if self.list_role_assignments(filters=filters): + self.log.debug('Assignment already exists') + return False + + error_msg = "Error granting access to role: {0}".format(data) + if self._is_client_version('identity', 2): + # For v2.0, only tenant/project assignment is supported + url = "/tenants/{t}/users/{u}/roles/OS-KSADM/{r}".format( + t=data['project']['id'], u=data['user']['id'], r=data['role']) + + self._identity_client.put(url, error_message=error_msg, + endpoint_filter={'interface': 'admin'}) + else: + if data.get('project') is None and data.get('domain') is None: + raise OpenStackCloudException( + 'Must specify either a domain or project') + + # For v3, figure out the assignment type and build the URL + if data.get('domain'): + url = "/domains/{}".format(data['domain']) + else: + url = "/projects/{}".format(data['project']['id']) + if data.get('group'): + url += "/groups/{}".format(data['group']['id']) + else: + url += "/users/{}".format(data['user']['id']) + url += "/roles/{}".format(data.get('role')) + + self._identity_client.put(url, error_message=error_msg) + + if wait: + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for role to be granted"): + if self.list_role_assignments(filters=filters): + break + return True + + def revoke_role(self, name_or_id, user=None, group=None, + project=None, domain=None, wait=False, timeout=60): + """Revoke a role from a user. + + :param string name_or_id: The name or id of the role. + :param string user: The name or id of the user. + :param string group: The name or id of the group. (v3) + :param string project: The name or id of the project. + :param string domain: The id of the domain. (v3) + :param bool wait: Wait for role to be revoked + :param int timeout: Timeout to wait for role to be revoked + + NOTE: for wait and timeout, sometimes revoking roles is not + instantaneous. + + NOTE: project is required for keystone v2 + + :returns: True if the role is revoke, otherwise False + + :raise OpenStackCloudException: if the role cannot be removed + """ + data = self._get_grant_revoke_params(name_or_id, user, group, + project, domain) + filters = data.copy() + + if not data: + raise OpenStackCloudException( + 'Role {0} not found.'.format(name_or_id)) + + if data.get('user') is not None and data.get('group') is not None: + raise OpenStackCloudException( + 'Specify either a group or a user, not both') + if data.get('user') is None and data.get('group') is None: + raise OpenStackCloudException( + 'Must specify either a user or a group') + if self._is_client_version('identity', 2) and \ + data.get('project') is None: + raise OpenStackCloudException( + 'Must specify project for keystone v2') + + if not self.list_role_assignments(filters=filters): + self.log.debug('Assignment does not exist') + return False + + error_msg = "Error revoking access to role: {0}".format(data) + if self._is_client_version('identity', 2): + # For v2.0, only tenant/project assignment is supported + url = "/tenants/{t}/users/{u}/roles/OS-KSADM/{r}".format( + t=data['project']['id'], u=data['user']['id'], r=data['role']) + + self._identity_client.delete( + url, error_message=error_msg, + endpoint_filter={'interface': 'admin'}) + else: + if data.get('project') is None and data.get('domain') is None: + raise OpenStackCloudException( + 'Must specify either a domain or project') + + # For v3, figure out the assignment type and build the URL + if data.get('domain'): + url = "/domains/{}".format(data['domain']) + else: + url = "/projects/{}".format(data['project']['id']) + if data.get('group'): + url += "/groups/{}".format(data['group']['id']) + else: + url += "/users/{}".format(data['user']['id']) + url += "/roles/{}".format(data.get('role')) + + self._identity_client.delete(url, error_message=error_msg) + + if wait: + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for role to be revoked"): + if not self.list_role_assignments(filters=filters): + break + return True + + def list_hypervisors(self): + """List all hypervisors + + :returns: A list of hypervisor ``munch.Munch``. + """ + + data = _adapter._json_response( + self._conn.compute.get('/os-hypervisors/detail'), + error_message="Error fetching hypervisor list") + return self._get_and_munchify('hypervisors', data) + + def search_aggregates(self, name_or_id=None, filters=None): + """Seach host aggregates. + + :param name: aggregate name or id. + :param filters: a dict containing additional filters to use. + + :returns: a list of dicts containing the aggregates + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + aggregates = self.list_aggregates() + return _utils._filter_list(aggregates, name_or_id, filters) + + def list_aggregates(self): + """List all available host aggregates. + + :returns: A list of aggregate dicts. + + """ + data = _adapter._json_response( + self._conn.compute.get('/os-aggregates'), + error_message="Error fetching aggregate list") + return self._get_and_munchify('aggregates', data) + + def get_aggregate(self, name_or_id, filters=None): + """Get an aggregate by name or ID. + + :param name_or_id: Name or ID of the aggregate. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'availability_zone': 'nova', + 'metadata': { + 'cpu_allocation_ratio': '1.0' + } + } + + :returns: An aggregate dict or None if no matching aggregate is + found. + + """ + return _utils._get_entity(self, 'aggregate', name_or_id, filters) + + def create_aggregate(self, name, availability_zone=None): + """Create a new host aggregate. + + :param name: Name of the host aggregate being created + :param availability_zone: Availability zone to assign hosts + + :returns: a dict representing the new host aggregate. + + :raises: OpenStackCloudException on operation error. + """ + data = _adapter._json_response( + self._conn.compute.post( + '/os-aggregates', + json={'aggregate': { + 'name': name, + 'availability_zone': availability_zone + }}), + error_message="Unable to create host aggregate {name}".format( + name=name)) + return self._get_and_munchify('aggregate', data) + + @_utils.valid_kwargs('name', 'availability_zone') + def update_aggregate(self, name_or_id, **kwargs): + """Update a host aggregate. + + :param name_or_id: Name or ID of the aggregate being updated. + :param name: New aggregate name + :param availability_zone: Availability zone to assign to hosts + + :returns: a dict representing the updated host aggregate. + + :raises: OpenStackCloudException on operation error. + """ + aggregate = self.get_aggregate(name_or_id) + if not aggregate: + raise OpenStackCloudException( + "Host aggregate %s not found." % name_or_id) + + data = _adapter._json_response( + self._conn.compute.put( + '/os-aggregates/{id}'.format(id=aggregate['id']), + json={'aggregate': kwargs}), + error_message="Error updating aggregate {name}".format( + name=name_or_id)) + return self._get_and_munchify('aggregate', data) + + def delete_aggregate(self, name_or_id): + """Delete a host aggregate. + + :param name_or_id: Name or ID of the host aggregate to delete. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException on operation error. + """ + aggregate = self.get_aggregate(name_or_id) + if not aggregate: + self.log.debug("Aggregate %s not found for deleting", name_or_id) + return False + + return _adapter._json_response( + self._conn.compute.delete( + '/os-aggregates/{id}'.format(id=aggregate['id'])), + error_message="Error deleting aggregate {name}".format( + name=name_or_id)) + + return True + + def set_aggregate_metadata(self, name_or_id, metadata): + """Set aggregate metadata, replacing the existing metadata. + + :param name_or_id: Name of the host aggregate to update + :param metadata: Dict containing metadata to replace (Use + {'key': None} to remove a key) + + :returns: a dict representing the new host aggregate. + + :raises: OpenStackCloudException on operation error. + """ + aggregate = self.get_aggregate(name_or_id) + if not aggregate: + raise OpenStackCloudException( + "Host aggregate %s not found." % name_or_id) + + err_msg = "Unable to set metadata for host aggregate {name}".format( + name=name_or_id) + + data = _adapter._json_response( + self._conn.compute.post( + '/os-aggregates/{id}/action'.format(id=aggregate['id']), + json={'set_metadata': {'metadata': metadata}}), + error_message=err_msg) + return self._get_and_munchify('aggregate', data) + + def add_host_to_aggregate(self, name_or_id, host_name): + """Add a host to an aggregate. + + :param name_or_id: Name or ID of the host aggregate. + :param host_name: Host to add. + + :raises: OpenStackCloudException on operation error. + """ + aggregate = self.get_aggregate(name_or_id) + if not aggregate: + raise OpenStackCloudException( + "Host aggregate %s not found." % name_or_id) + + err_msg = "Unable to add host {host} to aggregate {name}".format( + host=host_name, name=name_or_id) + + return _adapter._json_response( + self._conn.compute.post( + '/os-aggregates/{id}/action'.format(id=aggregate['id']), + json={'add_host': {'host': host_name}}), + error_message=err_msg) + + def remove_host_from_aggregate(self, name_or_id, host_name): + """Remove a host from an aggregate. + + :param name_or_id: Name or ID of the host aggregate. + :param host_name: Host to remove. + + :raises: OpenStackCloudException on operation error. + """ + aggregate = self.get_aggregate(name_or_id) + if not aggregate: + raise OpenStackCloudException( + "Host aggregate %s not found." % name_or_id) + + err_msg = "Unable to remove host {host} to aggregate {name}".format( + host=host_name, name=name_or_id) + + return _adapter._json_response( + self._conn.compute.post( + '/os-aggregates/{id}/action'.format(id=aggregate['id']), + json={'remove_host': {'host': host_name}}), + error_message=err_msg) + + def get_volume_type_access(self, name_or_id): + """Return a list of volume_type_access. + + :param name_or_id: Name or ID of the volume type. + + :raises: OpenStackCloudException on operation error. + """ + volume_type = self.get_volume_type(name_or_id) + if not volume_type: + raise OpenStackCloudException( + "VolumeType not found: %s" % name_or_id) + + data = self._volume_client.get( + '/types/{id}/os-volume-type-access'.format(id=volume_type.id), + error_message="Unable to get volume type access" + " {name}".format(name=name_or_id)) + return self._normalize_volume_type_accesses( + self._get_and_munchify('volume_type_access', data)) + + def add_volume_type_access(self, name_or_id, project_id): + """Grant access on a volume_type to a project. + + :param name_or_id: ID or name of a volume_type + :param project_id: A project id + + NOTE: the call works even if the project does not exist. + + :raises: OpenStackCloudException on operation error. + """ + volume_type = self.get_volume_type(name_or_id) + if not volume_type: + raise OpenStackCloudException( + "VolumeType not found: %s" % name_or_id) + with _utils.shade_exceptions(): + payload = {'project': project_id} + self._volume_client.post( + '/types/{id}/action'.format(id=volume_type.id), + json=dict(addProjectAccess=payload), + error_message="Unable to authorize {project} " + "to use volume type {name}".format( + name=name_or_id, project=project_id)) + + def remove_volume_type_access(self, name_or_id, project_id): + """Revoke access on a volume_type to a project. + + :param name_or_id: ID or name of a volume_type + :param project_id: A project id + + :raises: OpenStackCloudException on operation error. + """ + volume_type = self.get_volume_type(name_or_id) + if not volume_type: + raise OpenStackCloudException( + "VolumeType not found: %s" % name_or_id) + with _utils.shade_exceptions(): + payload = {'project': project_id} + self._volume_client.post( + '/types/{id}/action'.format(id=volume_type.id), + json=dict(removeProjectAccess=payload), + error_message="Unable to revoke {project} " + "to use volume type {name}".format( + name=name_or_id, project=project_id)) + + def set_compute_quotas(self, name_or_id, **kwargs): + """ Set a quota in a project + + :param name_or_id: project name or id + :param kwargs: key/value pairs of quota name and quota value + + :raises: OpenStackCloudException if the resource to set the + quota does not exist. + """ + + proj = self.get_project(name_or_id) + if not proj: + raise OpenStackCloudException("project does not exist") + + # compute_quotas = {key: val for key, val in kwargs.items() + # if key in quota.COMPUTE_QUOTAS} + # TODO(ghe): Manage volume and network quotas + # network_quotas = {key: val for key, val in kwargs.items() + # if key in quota.NETWORK_QUOTAS} + # volume_quotas = {key: val for key, val in kwargs.items() + # if key in quota.VOLUME_QUOTAS} + + kwargs['force'] = True + _adapter._json_response( + self._conn.compute.put( + '/os-quota-sets/{project}'.format(project=proj.id), + json={'quota_set': kwargs}), + error_message="No valid quota or resource") + + def get_compute_quotas(self, name_or_id): + """ Get quota for a project + + :param name_or_id: project name or id + :raises: OpenStackCloudException if it's not a valid project + + :returns: Munch object with the quotas + """ + proj = self.get_project(name_or_id) + if not proj: + raise OpenStackCloudException("project does not exist") + data = _adapter._json_response( + self._conn.compute.get( + '/os-quota-sets/{project}'.format(project=proj.id))) + return self._get_and_munchify('quota_set', data) + + def delete_compute_quotas(self, name_or_id): + """ Delete quota for a project + + :param name_or_id: project name or id + :raises: OpenStackCloudException if it's not a valid project or the + nova client call failed + + :returns: dict with the quotas + """ + proj = self.get_project(name_or_id) + if not proj: + raise OpenStackCloudException("project does not exist") + return _adapter._json_response( + self._conn.compute.delete( + '/os-quota-sets/{project}'.format(project=proj.id))) + + def get_compute_usage(self, name_or_id, start=None, end=None): + """ Get usage for a specific project + + :param name_or_id: project name or id + :param start: :class:`datetime.datetime` or string. Start date in UTC + Defaults to 2010-07-06T12:00:00Z (the date the OpenStack + project was started) + :param end: :class:`datetime.datetime` or string. End date in UTC. + Defaults to now + :raises: OpenStackCloudException if it's not a valid project + + :returns: Munch object with the usage + """ + def parse_date(date): + try: + return iso8601.parse_date(date) + except iso8601.iso8601.ParseError: + # Yes. This is an exception mask. However,iso8601 is an + # implementation detail - and the error message is actually + # less informative. + raise OpenStackCloudException( + "Date given, {date}, is invalid. Please pass in a date" + " string in ISO 8601 format -" + " YYYY-MM-DDTHH:MM:SS".format( + date=date)) + + def parse_datetime_for_nova(date): + # Must strip tzinfo from the date- it breaks Nova. Also, + # Nova is expecting this in UTC. If someone passes in an + # ISO8601 date string or a datetime with timzeone data attached, + # strip the timezone data but apply offset math first so that + # the user's well formed perfectly valid date will be used + # correctly. + offset = date.utcoffset() + if offset: + date = date - datetime.timedelta(hours=offset) + return date.replace(tzinfo=None) + + if not start: + start = parse_date('2010-07-06') + elif not isinstance(start, datetime.datetime): + start = parse_date(start) + if not end: + end = datetime.datetime.utcnow() + elif not isinstance(start, datetime.datetime): + end = parse_date(end) + + start = parse_datetime_for_nova(start) + end = parse_datetime_for_nova(end) + + proj = self.get_project(name_or_id) + if not proj: + raise OpenStackCloudException("project does not exist: {}".format( + name=proj.id)) + + data = _adapter._json_response( + self._conn.compute.get( + '/os-simple-tenant-usage/{project}'.format(project=proj.id), + params=dict(start=start.isoformat(), end=end.isoformat())), + error_message="Unable to get usage for project: {name}".format( + name=proj.id)) + return self._normalize_compute_usage( + self._get_and_munchify('tenant_usage', data)) + + def set_volume_quotas(self, name_or_id, **kwargs): + """ Set a volume quota in a project + + :param name_or_id: project name or id + :param kwargs: key/value pairs of quota name and quota value + + :raises: OpenStackCloudException if the resource to set the + quota does not exist. + """ + + proj = self.get_project(name_or_id) + if not proj: + raise OpenStackCloudException("project does not exist") + + kwargs['tenant_id'] = proj.id + self._volume_client.put( + '/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id), + json={'quota_set': kwargs}, + error_message="No valid quota or resource") + + def get_volume_quotas(self, name_or_id): + """ Get volume quotas for a project + + :param name_or_id: project name or id + :raises: OpenStackCloudException if it's not a valid project + + :returns: Munch object with the quotas + """ + proj = self.get_project(name_or_id) + if not proj: + raise OpenStackCloudException("project does not exist") + + data = self._volume_client.get( + '/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id), + error_message="cinder client call failed") + return self._get_and_munchify('quota_set', data) + + def delete_volume_quotas(self, name_or_id): + """ Delete volume quotas for a project + + :param name_or_id: project name or id + :raises: OpenStackCloudException if it's not a valid project or the + cinder client call failed + + :returns: dict with the quotas + """ + proj = self.get_project(name_or_id) + if not proj: + raise OpenStackCloudException("project does not exist") + + return self._volume_client.delete( + '/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id), + error_message="cinder client call failed") + + def set_network_quotas(self, name_or_id, **kwargs): + """ Set a network quota in a project + + :param name_or_id: project name or id + :param kwargs: key/value pairs of quota name and quota value + + :raises: OpenStackCloudException if the resource to set the + quota does not exist. + """ + + proj = self.get_project(name_or_id) + if not proj: + raise OpenStackCloudException("project does not exist") + + self._network_client.put( + '/quotas/{project_id}.json'.format(project_id=proj.id), + json={'quota': kwargs}, + error_message=("Error setting Neutron's quota for " + "project {0}".format(proj.id))) + + def get_network_quotas(self, name_or_id, details=False): + """ Get network quotas for a project + + :param name_or_id: project name or id + :param details: if set to True it will return details about usage + of quotas by given project + :raises: OpenStackCloudException if it's not a valid project + + :returns: Munch object with the quotas + """ + proj = self.get_project(name_or_id) + if not proj: + raise OpenStackCloudException("project does not exist") + url = '/quotas/{project_id}'.format(project_id=proj.id) + if details: + url = url + "/details" + url = url + ".json" + data = self._network_client.get( + url, + error_message=("Error fetching Neutron's quota for " + "project {0}".format(proj.id))) + return self._get_and_munchify('quota', data) + + def delete_network_quotas(self, name_or_id): + """ Delete network quotas for a project + + :param name_or_id: project name or id + :raises: OpenStackCloudException if it's not a valid project or the + network client call failed + + :returns: dict with the quotas + """ + proj = self.get_project(name_or_id) + if not proj: + raise OpenStackCloudException("project does not exist") + self._network_client.delete( + '/quotas/{project_id}.json'.format(project_id=proj.id), + error_message=("Error deleting Neutron's quota for " + "project {0}".format(proj.id))) + + def list_magnum_services(self): + """List all Magnum services. + :returns: a list of dicts containing the service details. + + :raises: OpenStackCloudException on operation error. + """ + with _utils.shade_exceptions("Error fetching Magnum services list"): + data = self._container_infra_client.get('/mservices') + return self._normalize_magnum_services( + self._get_and_munchify('mservices', data)) diff --git a/openstack/cloud/operatorcloud.py b/openstack/cloud/operatorcloud.py deleted file mode 100644 index ae5ffed39..000000000 --- a/openstack/cloud/operatorcloud.py +++ /dev/null @@ -1,2597 +0,0 @@ -# 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 datetime -import iso8601 -import jsonpatch -import munch - -from openstack import _adapter -from openstack.cloud.exc import * # noqa -from openstack.cloud import openstackcloud -from openstack.cloud import _utils -from openstack import utils - - -class OperatorCloud(openstackcloud.OpenStackCloud): - """Represent a privileged/operator connection to an OpenStack Cloud. - - `OperatorCloud` is the entry point for all admin operations, regardless - of which OpenStack service those operations are for. - - See the :class:`OpenStackCloud` class for a description of most options. - """ - - # TODO(shade) Finish porting ironic to REST/sdk - ironic_client = None - - def list_nics(self): - msg = "Error fetching machine port list" - return self._baremetal_client.get("/ports", - microversion="1.6", - error_message=msg) - - def list_nics_for_machine(self, uuid): - """Returns a list of ports present on the machine node. - - :param uuid: String representing machine UUID value in - order to identify the machine. - :returns: A dictionary containing containing a list of ports, - associated with the label "ports". - """ - msg = "Error fetching port list for node {node_id}".format( - node_id=uuid) - url = "/nodes/{node_id}/ports".format(node_id=uuid) - return self._baremetal_client.get(url, - microversion="1.6", - error_message=msg) - - def get_nic_by_mac(self, mac): - try: - url = '/ports/detail?address=%s' % mac - data = self._baremetal_client.get(url) - if len(data['ports']) == 1: - return data['ports'][0] - except Exception: - pass - return None - - def list_machines(self): - msg = "Error fetching machine node list" - data = self._baremetal_client.get("/nodes", - microversion="1.6", - error_message=msg) - return self._get_and_munchify('nodes', data) - - def get_machine(self, name_or_id): - """Get Machine by name or uuid - - Search the baremetal host out by utilizing the supplied id value - which can consist of a name or UUID. - - :param name_or_id: A node name or UUID that will be looked up. - - :returns: ``munch.Munch`` representing the node found or None if no - nodes are found. - """ - # NOTE(TheJulia): This is the initial microversion shade support for - # ironic was created around. Ironic's default behavior for newer - # versions is to expose the field, but with a value of None for - # calls by a supported, yet older microversion. - # Consensus for moving forward with microversion handling in shade - # seems to be to take the same approach, although ironic's API - # does it for the user. - version = "1.6" - try: - url = '/nodes/{node_id}'.format(node_id=name_or_id) - return self._normalize_machine( - self._baremetal_client.get(url, microversion=version)) - except Exception: - return None - - def get_machine_by_mac(self, mac): - """Get machine by port MAC address - - :param mac: Port MAC address to query in order to return a node. - - :returns: ``munch.Munch`` representing the node found or None - if the node is not found. - """ - try: - port_url = '/ports/detail?address={mac}'.format(mac=mac) - port = self._baremetal_client.get(port_url, microversion=1.6) - machine_url = '/nodes/{machine}'.format( - machine=port['ports'][0]['node_uuid']) - return self._baremetal_client.get(machine_url, microversion=1.6) - except Exception: - return None - - def inspect_machine(self, name_or_id, wait=False, timeout=3600): - """Inspect a Barmetal machine - - Engages the Ironic node inspection behavior in order to collect - metadata about the baremetal machine. - - :param name_or_id: String representing machine name or UUID value in - order to identify the machine. - - :param wait: Boolean value controlling if the method is to wait for - the desired state to be reached or a failure to occur. - - :param timeout: Integer value, defautling to 3600 seconds, for the$ - wait state to reach completion. - - :returns: ``munch.Munch`` representing the current state of the machine - upon exit of the method. - """ - - return_to_available = False - - machine = self.get_machine(name_or_id) - if not machine: - raise OpenStackCloudException( - "Machine inspection failed to find: %s." % name_or_id) - - # NOTE(TheJulia): If in available state, we can do this, however - # We need to to move the host back to m - if "available" in machine['provision_state']: - return_to_available = True - # NOTE(TheJulia): Changing available machine to managedable state - # and due to state transitions we need to until that transition has - # completd. - self.node_set_provision_state(machine['uuid'], 'manage', - wait=True, timeout=timeout) - elif ("manage" not in machine['provision_state'] and - "inspect failed" not in machine['provision_state']): - raise OpenStackCloudException( - "Machine must be in 'manage' or 'available' state to " - "engage inspection: Machine: %s State: %s" - % (machine['uuid'], machine['provision_state'])) - with _utils.shade_exceptions("Error inspecting machine"): - machine = self.node_set_provision_state(machine['uuid'], 'inspect') - if wait: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for node transition to " - "target state of 'inspect'"): - machine = self.get_machine(name_or_id) - - if "inspect failed" in machine['provision_state']: - raise OpenStackCloudException( - "Inspection of node %s failed, last error: %s" - % (machine['uuid'], machine['last_error'])) - - if "manageable" in machine['provision_state']: - break - - if return_to_available: - machine = self.node_set_provision_state( - machine['uuid'], 'provide', wait=wait, timeout=timeout) - - return(machine) - - def register_machine(self, nics, wait=False, timeout=3600, - lock_timeout=600, **kwargs): - """Register Baremetal with Ironic - - Allows for the registration of Baremetal nodes with Ironic - and population of pertinant node information or configuration - to be passed to the Ironic API for the node. - - This method also creates ports for a list of MAC addresses passed - in to be utilized for boot and potentially network configuration. - - If a failure is detected creating the network ports, any ports - created are deleted, and the node is removed from Ironic. - - :param nics: - An array of MAC addresses that represent the - network interfaces for the node to be created. - - Example:: - - [ - {'mac': 'aa:bb:cc:dd:ee:01'}, - {'mac': 'aa:bb:cc:dd:ee:02'} - ] - - :param wait: Boolean value, defaulting to false, to wait for the - node to reach the available state where the node can be - provisioned. It must be noted, when set to false, the - method will still wait for locks to clear before sending - the next required command. - - :param timeout: Integer value, defautling to 3600 seconds, for the - wait state to reach completion. - - :param lock_timeout: Integer value, defaulting to 600 seconds, for - locks to clear. - - :param kwargs: Key value pairs to be passed to the Ironic API, - including uuid, name, chassis_uuid, driver_info, - parameters. - - :raises: OpenStackCloudException on operation error. - - :returns: Returns a ``munch.Munch`` representing the new - baremetal node. - """ - - msg = ("Baremetal machine node failed to be created.") - port_msg = ("Baremetal machine port failed to be created.") - - url = '/nodes' - # TODO(TheJulia): At some point we need to figure out how to - # handle data across when the requestor is defining newer items - # with the older api. - machine = self._baremetal_client.post(url, - json=kwargs, - error_message=msg, - microversion="1.6") - - created_nics = [] - try: - for row in nics: - payload = {'address': row['mac'], - 'node_uuid': machine['uuid']} - nic = self._baremetal_client.post('/ports', - json=payload, - error_message=port_msg) - created_nics.append(nic['uuid']) - - except Exception as e: - self.log.debug("ironic NIC registration failed", exc_info=True) - # TODO(mordred) Handle failures here - try: - for uuid in created_nics: - try: - port_url = '/ports/{uuid}'.format(uuid=uuid) - # NOTE(TheJulia): Added in hope that it is logged. - port_msg = ('Failed to delete port {port} for node' - '{node}').format(port=uuid, - node=machine['uuid']) - self._baremetal_client.delete(port_url, - error_message=port_msg) - except Exception: - pass - finally: - version = "1.6" - msg = "Baremetal machine failed to be deleted." - url = '/nodes/{node_id}'.format( - node_id=machine['uuid']) - self._baremetal_client.delete(url, - error_message=msg, - microversion=version) - raise OpenStackCloudException( - "Error registering NICs with the baremetal service: %s" - % str(e)) - - with _utils.shade_exceptions( - "Error transitioning node to available state"): - if wait: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for node transition to " - "available state"): - - machine = self.get_machine(machine['uuid']) - - # Note(TheJulia): Per the Ironic state code, a node - # that fails returns to enroll state, which means a failed - # node cannot be determined at this point in time. - if machine['provision_state'] in ['enroll']: - self.node_set_provision_state( - machine['uuid'], 'manage') - elif machine['provision_state'] in ['manageable']: - self.node_set_provision_state( - machine['uuid'], 'provide') - elif machine['last_error'] is not None: - raise OpenStackCloudException( - "Machine encountered a failure: %s" - % machine['last_error']) - - # Note(TheJulia): Earlier versions of Ironic default to - # None and later versions default to available up until - # the introduction of enroll state. - # Note(TheJulia): The node will transition through - # cleaning if it is enabled, and we will wait for - # completion. - elif machine['provision_state'] in ['available', None]: - break - - else: - if machine['provision_state'] in ['enroll']: - self.node_set_provision_state(machine['uuid'], 'manage') - # Note(TheJulia): We need to wait for the lock to clear - # before we attempt to set the machine into provide state - # which allows for the transition to available. - for count in utils.iterate_timeout( - lock_timeout, - "Timeout waiting for reservation to clear " - "before setting provide state"): - machine = self.get_machine(machine['uuid']) - if (machine['reservation'] is None and - machine['provision_state'] is not 'enroll'): - # NOTE(TheJulia): In this case, the node has - # has moved on from the previous state and is - # likely not being verified, as no lock is - # present on the node. - self.node_set_provision_state( - machine['uuid'], 'provide') - machine = self.get_machine(machine['uuid']) - break - - elif machine['provision_state'] in [ - 'cleaning', - 'available']: - break - - elif machine['last_error'] is not None: - raise OpenStackCloudException( - "Machine encountered a failure: %s" - % machine['last_error']) - if not isinstance(machine, str): - return self._normalize_machine(machine) - else: - return machine - - def unregister_machine(self, nics, uuid, wait=False, timeout=600): - """Unregister Baremetal from Ironic - - Removes entries for Network Interfaces and baremetal nodes - from an Ironic API - - :param nics: An array of strings that consist of MAC addresses - to be removed. - :param string uuid: The UUID of the node to be deleted. - - :param wait: Boolean value, defaults to false, if to block the method - upon the final step of unregistering the machine. - - :param timeout: Integer value, representing seconds with a default - value of 600, which controls the maximum amount of - time to block the method's completion on. - - :raises: OpenStackCloudException on operation failure. - """ - - machine = self.get_machine(uuid) - invalid_states = ['active', 'cleaning', 'clean wait', 'clean failed'] - if machine['provision_state'] in invalid_states: - raise OpenStackCloudException( - "Error unregistering node '%s' due to current provision " - "state '%s'" % (uuid, machine['provision_state'])) - - # NOTE(TheJulia) There is a high possibility of a lock being present - # if the machine was just moved through the state machine. This was - # previously concealed by exception retry logic that detected the - # failure, and resubitted the request in python-ironicclient. - try: - self.wait_for_baremetal_node_lock(machine, timeout=timeout) - except OpenStackCloudException as e: - raise OpenStackCloudException("Error unregistering node '%s': " - "Exception occured while waiting " - "to be able to proceed: %s" - % (machine['uuid'], e)) - - for nic in nics: - port_msg = ("Error removing NIC {nic} from baremetal API for " - "node {uuid}").format(nic=nic, uuid=uuid) - port_url = '/ports/detail?address={mac}'.format(mac=nic['mac']) - port = self._baremetal_client.get(port_url, microversion=1.6, - error_message=port_msg) - port_url = '/ports/{uuid}'.format(uuid=port['ports'][0]['uuid']) - self._baremetal_client.delete(port_url, error_message=port_msg) - - with _utils.shade_exceptions( - "Error unregistering machine {node_id} from the baremetal " - "API".format(node_id=uuid)): - - # NOTE(TheJulia): While this should not matter microversion wise, - # ironic assumes all calls without an explicit microversion to be - # version 1.0. Ironic expects to deprecate support for older - # microversions in future releases, as such, we explicitly set - # the version to what we have been using with the client library.. - version = "1.6" - msg = "Baremetal machine failed to be deleted." - url = '/nodes/{node_id}'.format( - node_id=uuid) - self._baremetal_client.delete(url, - error_message=msg, - microversion=version) - - if wait: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for machine to be deleted"): - if not self.get_machine(uuid): - break - - def patch_machine(self, name_or_id, patch): - """Patch Machine Information - - This method allows for an interface to manipulate node entries - within Ironic. - - :param node_id: The server object to attach to. - :param patch: - The JSON Patch document is a list of dictonary objects - that comply with RFC 6902 which can be found at - https://tools.ietf.org/html/rfc6902. - - Example patch construction:: - - patch=[] - patch.append({ - 'op': 'remove', - 'path': '/instance_info' - }) - patch.append({ - 'op': 'replace', - 'path': '/name', - 'value': 'newname' - }) - patch.append({ - 'op': 'add', - 'path': '/driver_info/username', - 'value': 'administrator' - }) - - :raises: OpenStackCloudException on operation error. - - :returns: ``munch.Munch`` representing the newly updated node. - """ - - msg = ("Error updating machine via patch operation on node " - "{node}".format(node=name_or_id)) - url = '/nodes/{node_id}'.format(node_id=name_or_id) - return self._normalize_machine( - self._baremetal_client.patch(url, - json=patch, - error_message=msg)) - - def update_machine(self, name_or_id, chassis_uuid=None, driver=None, - driver_info=None, name=None, instance_info=None, - instance_uuid=None, properties=None): - """Update a machine with new configuration information - - A user-friendly method to perform updates of a machine, in whole or - part. - - :param string name_or_id: A machine name or UUID to be updated. - :param string chassis_uuid: Assign a chassis UUID to the machine. - NOTE: As of the Kilo release, this value - cannot be changed once set. If a user - attempts to change this value, then the - Ironic API, as of Kilo, will reject the - request. - :param string driver: The driver name for controlling the machine. - :param dict driver_info: The dictonary defining the configuration - that the driver will utilize to control - the machine. Permutations of this are - dependent upon the specific driver utilized. - :param string name: A human relatable name to represent the machine. - :param dict instance_info: A dictonary of configuration information - that conveys to the driver how the host - is to be configured when deployed. - be deployed to the machine. - :param string instance_uuid: A UUID value representing the instance - that the deployed machine represents. - :param dict properties: A dictonary defining the properties of a - machine. - - :raises: OpenStackCloudException on operation error. - - :returns: ``munch.Munch`` containing a machine sub-dictonary consisting - of the updated data returned from the API update operation, - and a list named changes which contains all of the API paths - that received updates. - """ - machine = self.get_machine(name_or_id) - if not machine: - raise OpenStackCloudException( - "Machine update failed to find Machine: %s. " % name_or_id) - - machine_config = {} - new_config = {} - - try: - if chassis_uuid: - machine_config['chassis_uuid'] = machine['chassis_uuid'] - new_config['chassis_uuid'] = chassis_uuid - - if driver: - machine_config['driver'] = machine['driver'] - new_config['driver'] = driver - - if driver_info: - machine_config['driver_info'] = machine['driver_info'] - new_config['driver_info'] = driver_info - - if name: - machine_config['name'] = machine['name'] - new_config['name'] = name - - if instance_info: - machine_config['instance_info'] = machine['instance_info'] - new_config['instance_info'] = instance_info - - if instance_uuid: - machine_config['instance_uuid'] = machine['instance_uuid'] - new_config['instance_uuid'] = instance_uuid - - if properties: - machine_config['properties'] = machine['properties'] - new_config['properties'] = properties - except KeyError as e: - self.log.debug( - "Unexpected machine response missing key %s [%s]", - e.args[0], name_or_id) - raise OpenStackCloudException( - "Machine update failed - machine [%s] missing key %s. " - "Potential API issue." - % (name_or_id, e.args[0])) - - try: - patch = jsonpatch.JsonPatch.from_diff(machine_config, new_config) - except Exception as e: - raise OpenStackCloudException( - "Machine update failed - Error generating JSON patch object " - "for submission to the API. Machine: %s Error: %s" - % (name_or_id, str(e))) - - with _utils.shade_exceptions( - "Machine update failed - patch operation failed on Machine " - "{node}".format(node=name_or_id) - ): - if not patch: - return dict( - node=machine, - changes=None - ) - else: - machine = self.patch_machine(machine['uuid'], list(patch)) - change_list = [] - for change in list(patch): - change_list.append(change['path']) - return dict( - node=machine, - changes=change_list - ) - - def validate_node(self, uuid): - # TODO(TheJulia): There are soooooo many other interfaces - # that we can support validating, while these are essential, - # we should support more. - # TODO(TheJulia): Add a doc string :( - msg = ("Failed to query the API for validation status of " - "node {node_id}").format(node_id=uuid) - url = '/nodes/{node_id}/validate'.format(node_id=uuid) - ifaces = self._baremetal_client.get(url, error_message=msg) - - if not ifaces['deploy'] or not ifaces['power']: - raise OpenStackCloudException( - "ironic node %s failed to validate. " - "(deploy: %s, power: %s)" % (ifaces['deploy'], - ifaces['power'])) - - def node_set_provision_state(self, - name_or_id, - state, - configdrive=None, - wait=False, - timeout=3600): - """Set Node Provision State - - Enables a user to provision a Machine and optionally define a - config drive to be utilized. - - :param string name_or_id: The Name or UUID value representing the - baremetal node. - :param string state: The desired provision state for the - baremetal node. - :param string configdrive: An optional URL or file or path - representing the configdrive. In the - case of a directory, the client API - will create a properly formatted - configuration drive file and post the - file contents to the API for - deployment. - :param boolean wait: A boolean value, defaulted to false, to control - if the method will wait for the desire end state - to be reached before returning. - :param integer timeout: Integer value, defaulting to 3600 seconds, - representing the amount of time to wait for - the desire end state to be reached. - - :raises: OpenStackCloudException on operation error. - - :returns: ``munch.Munch`` representing the current state of the machine - upon exit of the method. - """ - # NOTE(TheJulia): Default microversion for this call is 1.6. - # Setting locally until we have determined our master plan regarding - # microversion handling. - version = "1.6" - msg = ("Baremetal machine node failed change provision state to " - "{state}".format(state=state)) - - url = '/nodes/{node_id}/states/provision'.format( - node_id=name_or_id) - payload = {'target': state} - if configdrive: - payload['configdrive'] = configdrive - - machine = self._baremetal_client.put(url, - json=payload, - error_message=msg, - microversion=version) - if wait: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for node transition to " - "target state of '%s'" % state): - machine = self.get_machine(name_or_id) - if 'failed' in machine['provision_state']: - raise OpenStackCloudException( - "Machine encountered a failure.") - # NOTE(TheJulia): This performs matching if the requested - # end state matches the state the node has reached. - if state in machine['provision_state']: - break - # NOTE(TheJulia): This performs matching for cases where - # the reqeusted state action ends in available state. - if ("available" in machine['provision_state'] and - state in ["provide", "deleted"]): - break - else: - machine = self.get_machine(name_or_id) - return machine - - def set_machine_maintenance_state( - self, - name_or_id, - state=True, - reason=None): - """Set Baremetal Machine Maintenance State - - Sets Baremetal maintenance state and maintenance reason. - - :param string name_or_id: The Name or UUID value representing the - baremetal node. - :param boolean state: The desired state of the node. True being in - maintenance where as False means the machine - is not in maintenance mode. This value - defaults to True if not explicitly set. - :param string reason: An optional freeform string that is supplied to - the baremetal API to allow for notation as to why - the node is in maintenance state. - - :raises: OpenStackCloudException on operation error. - - :returns: None - """ - msg = ("Error setting machine maintenance state to {state} on node " - "{node}").format(state=state, node=name_or_id) - url = '/nodes/{name_or_id}/maintenance'.format(name_or_id=name_or_id) - if state: - payload = {'reason': reason} - self._baremetal_client.put(url, - json=payload, - error_message=msg) - else: - self._baremetal_client.delete(url, error_message=msg) - return None - - def remove_machine_from_maintenance(self, name_or_id): - """Remove Baremetal Machine from Maintenance State - - Similarly to set_machine_maintenance_state, this method - removes a machine from maintenance state. It must be noted - that this method simpily calls set_machine_maintenace_state - for the name_or_id requested and sets the state to False. - - :param string name_or_id: The Name or UUID value representing the - baremetal node. - - :raises: OpenStackCloudException on operation error. - - :returns: None - """ - self.set_machine_maintenance_state(name_or_id, False) - - def _set_machine_power_state(self, name_or_id, state): - """Set machine power state to on or off - - This private method allows a user to turn power on or off to - a node via the Baremetal API. - - :params string name_or_id: A string representing the baremetal - node to have power turned to an "on" - state. - :params string state: A value of "on", "off", or "reboot" that is - passed to the baremetal API to be asserted to - the machine. In the case of the "reboot" state, - Ironic will return the host to the "on" state. - - :raises: OpenStackCloudException on operation error or. - - :returns: None - """ - msg = ("Error setting machine power state to {state} on node " - "{node}").format(state=state, node=name_or_id) - url = '/nodes/{name_or_id}/states/power'.format(name_or_id=name_or_id) - if 'reboot' in state: - desired_state = 'rebooting' - else: - desired_state = 'power {state}'.format(state=state) - payload = {'target': desired_state} - self._baremetal_client.put(url, - json=payload, - error_message=msg, - microversion="1.6") - return None - - def set_machine_power_on(self, name_or_id): - """Activate baremetal machine power - - This is a method that sets the node power state to "on". - - :params string name_or_id: A string representing the baremetal - node to have power turned to an "on" - state. - - :raises: OpenStackCloudException on operation error. - - :returns: None - """ - self._set_machine_power_state(name_or_id, 'on') - - def set_machine_power_off(self, name_or_id): - """De-activate baremetal machine power - - This is a method that sets the node power state to "off". - - :params string name_or_id: A string representing the baremetal - node to have power turned to an "off" - state. - - :raises: OpenStackCloudException on operation error. - - :returns: - """ - self._set_machine_power_state(name_or_id, 'off') - - def set_machine_power_reboot(self, name_or_id): - """De-activate baremetal machine power - - This is a method that sets the node power state to "reboot", which - in essence changes the machine power state to "off", and that back - to "on". - - :params string name_or_id: A string representing the baremetal - node to have power turned to an "off" - state. - - :raises: OpenStackCloudException on operation error. - - :returns: None - """ - self._set_machine_power_state(name_or_id, 'reboot') - - def activate_node(self, uuid, configdrive=None, - wait=False, timeout=1200): - self.node_set_provision_state( - uuid, 'active', configdrive, wait=wait, timeout=timeout) - - def deactivate_node(self, uuid, wait=False, - timeout=1200): - self.node_set_provision_state( - uuid, 'deleted', wait=wait, timeout=timeout) - - def set_node_instance_info(self, uuid, patch): - msg = ("Error updating machine via patch operation on node " - "{node}".format(node=uuid)) - url = '/nodes/{node_id}'.format(node_id=uuid) - return self._baremetal_client.patch(url, - json=patch, - error_message=msg) - - def purge_node_instance_info(self, uuid): - patch = [] - patch.append({'op': 'remove', 'path': '/instance_info'}) - msg = ("Error updating machine via patch operation on node " - "{node}".format(node=uuid)) - url = '/nodes/{node_id}'.format(node_id=uuid) - return self._baremetal_client.patch(url, - json=patch, - error_message=msg) - - def wait_for_baremetal_node_lock(self, node, timeout=30): - """Wait for a baremetal node to have no lock. - - Baremetal nodes in ironic have a reservation lock that - is used to represent that a conductor has locked the node - while performing some sort of action, such as changing - configuration as a result of a machine state change. - - This lock can occur during power syncronization, and prevents - updates to objects attached to the node, such as ports. - - In the vast majority of cases, locks should clear in a few - seconds, and as such this method will only wait for 30 seconds. - The default wait is two seconds between checking if the lock - has cleared. - - This method is intended for use by methods that need to - gracefully block without genreating errors, however this - method does prevent another client or a timer from - triggering a lock immediately after we see the lock as - having cleared. - - :param node: The json representation of the node, - specificially looking for the node - 'uuid' and 'reservation' fields. - :param timeout: Integer in seconds to wait for the - lock to clear. Default: 30 - - :raises: OpenStackCloudException upon client failure. - :returns: None - """ - # TODO(TheJulia): This _can_ still fail with a race - # condition in that between us checking the status, - # a conductor where the conductor could still obtain - # a lock before we are able to obtain a lock. - # This means we should handle this with such conections - - if node['reservation'] is None: - return - else: - msg = 'Waiting for lock to be released for node {node}'.format( - node=node['uuid']) - for count in utils.iterate_timeout(timeout, msg, 2): - current_node = self.get_machine(node['uuid']) - if current_node['reservation'] is None: - return - - @_utils.valid_kwargs('type', 'service_type', 'description') - def create_service(self, name, enabled=True, **kwargs): - """Create a service. - - :param name: Service name. - :param type: Service type. (type or service_type required.) - :param service_type: Service type. (type or service_type required.) - :param description: Service description (optional). - :param enabled: Whether the service is enabled (v3 only) - - :returns: a ``munch.Munch`` containing the services description, - i.e. the following attributes:: - - id: - - name: - - type: - - service_type: - - description: - - :raises: ``OpenStackCloudException`` if something goes wrong during the - openstack API call. - - """ - type_ = kwargs.pop('type', None) - service_type = kwargs.pop('service_type', None) - - # TODO(mordred) When this changes to REST, force interface=admin - # in the adapter call - if self._is_client_version('identity', 2): - url, key = '/OS-KSADM/services', 'OS-KSADM:service' - kwargs['type'] = type_ or service_type - else: - url, key = '/services', 'service' - kwargs['type'] = type_ or service_type - kwargs['enabled'] = enabled - kwargs['name'] = name - - msg = 'Failed to create service {name}'.format(name=name) - data = self._identity_client.post( - url, json={key: kwargs}, error_message=msg) - service = self._get_and_munchify(key, data) - return _utils.normalize_keystone_services([service])[0] - - @_utils.valid_kwargs('name', 'enabled', 'type', 'service_type', - 'description') - def update_service(self, name_or_id, **kwargs): - # NOTE(SamYaple): Service updates are only available on v3 api - if self._is_client_version('identity', 2): - raise OpenStackCloudUnavailableFeature( - 'Unavailable Feature: Service update requires Identity v3' - ) - - # NOTE(SamYaple): Keystone v3 only accepts 'type' but shade accepts - # both 'type' and 'service_type' with a preference - # towards 'type' - type_ = kwargs.pop('type', None) - service_type = kwargs.pop('service_type', None) - if type_ or service_type: - kwargs['type'] = type_ or service_type - - if self._is_client_version('identity', 2): - url, key = '/OS-KSADM/services', 'OS-KSADM:service' - else: - url, key = '/services', 'service' - - service = self.get_service(name_or_id) - msg = 'Error in updating service {service}'.format(service=name_or_id) - data = self._identity_client.patch( - '{url}/{id}'.format(url=url, id=service['id']), json={key: kwargs}, - endpoint_filter={'interface': 'admin'}, error_message=msg) - service = self._get_and_munchify(key, data) - return _utils.normalize_keystone_services([service])[0] - - def list_services(self): - """List all Keystone services. - - :returns: a list of ``munch.Munch`` containing the services description - - :raises: ``OpenStackCloudException`` if something goes wrong during the - openstack API call. - """ - if self._is_client_version('identity', 2): - url, key = '/OS-KSADM/services', 'OS-KSADM:services' - else: - url, key = '/services', 'services' - data = self._identity_client.get( - url, endpoint_filter={'interface': 'admin'}, - error_message="Failed to list services") - services = self._get_and_munchify(key, data) - return _utils.normalize_keystone_services(services) - - def search_services(self, name_or_id=None, filters=None): - """Search Keystone services. - - :param name_or_id: Name or id of the desired service. - :param filters: a dict containing additional filters to use. e.g. - {'type': 'network'}. - - :returns: a list of ``munch.Munch`` containing the services description - - :raises: ``OpenStackCloudException`` if something goes wrong during the - openstack API call. - """ - services = self.list_services() - return _utils._filter_list(services, name_or_id, filters) - - def get_service(self, name_or_id, filters=None): - """Get exactly one Keystone service. - - :param name_or_id: Name or id of the desired service. - :param filters: a dict containing additional filters to use. e.g. - {'type': 'network'} - - :returns: a ``munch.Munch`` containing the services description, - i.e. the following attributes:: - - id: - - name: - - type: - - description: - - :raises: ``OpenStackCloudException`` if something goes wrong during the - openstack API call or if multiple matches are found. - """ - return _utils._get_entity(self, 'service', name_or_id, filters) - - def delete_service(self, name_or_id): - """Delete a Keystone service. - - :param name_or_id: Service name or id. - - :returns: True if delete succeeded, False otherwise. - - :raises: ``OpenStackCloudException`` if something goes wrong during - the openstack API call - """ - service = self.get_service(name_or_id=name_or_id) - if service is None: - self.log.debug("Service %s not found for deleting", name_or_id) - return False - - if self._is_client_version('identity', 2): - url = '/OS-KSADM/services' - else: - url = '/services' - - error_msg = 'Failed to delete service {id}'.format(id=service['id']) - self._identity_client.delete( - '{url}/{id}'.format(url=url, id=service['id']), - endpoint_filter={'interface': 'admin'}, error_message=error_msg) - - return True - - @_utils.valid_kwargs('public_url', 'internal_url', 'admin_url') - def create_endpoint(self, service_name_or_id, url=None, interface=None, - region=None, enabled=True, **kwargs): - """Create a Keystone endpoint. - - :param service_name_or_id: Service name or id for this endpoint. - :param url: URL of the endpoint - :param interface: Interface type of the endpoint - :param public_url: Endpoint public URL. - :param internal_url: Endpoint internal URL. - :param admin_url: Endpoint admin URL. - :param region: Endpoint region. - :param enabled: Whether the endpoint is enabled - - NOTE: Both v2 (public_url, internal_url, admin_url) and v3 - (url, interface) calling semantics are supported. But - you can only use one of them at a time. - - :returns: a list of ``munch.Munch`` containing the endpoint description - - :raises: OpenStackCloudException if the service cannot be found or if - something goes wrong during the openstack API call. - """ - public_url = kwargs.pop('public_url', None) - internal_url = kwargs.pop('internal_url', None) - admin_url = kwargs.pop('admin_url', None) - - if (url or interface) and (public_url or internal_url or admin_url): - raise OpenStackCloudException( - "create_endpoint takes either url and interface OR" - " public_url, internal_url, admin_url") - - service = self.get_service(name_or_id=service_name_or_id) - if service is None: - raise OpenStackCloudException("service {service} not found".format( - service=service_name_or_id)) - - if self._is_client_version('identity', 2): - if url: - # v2.0 in use, v3-like arguments, one endpoint created - if interface != 'public': - raise OpenStackCloudException( - "Error adding endpoint for service {service}." - " On a v2 cloud the url/interface API may only be" - " used for public url. Try using the public_url," - " internal_url, admin_url parameters instead of" - " url and interface".format( - service=service_name_or_id)) - endpoint_args = {'publicurl': url} - else: - # v2.0 in use, v2.0-like arguments, one endpoint created - endpoint_args = {} - if public_url: - endpoint_args.update({'publicurl': public_url}) - if internal_url: - endpoint_args.update({'internalurl': internal_url}) - if admin_url: - endpoint_args.update({'adminurl': admin_url}) - - # keystone v2.0 requires 'region' arg even if it is None - endpoint_args.update( - {'service_id': service['id'], 'region': region}) - - data = self._identity_client.post( - '/endpoints', json={'endpoint': endpoint_args}, - endpoint_filter={'interface': 'admin'}, - error_message=("Failed to create endpoint for service" - " {service}".format(service=service['name']))) - return [self._get_and_munchify('endpoint', data)] - else: - endpoints_args = [] - if url: - # v3 in use, v3-like arguments, one endpoint created - endpoints_args.append( - {'url': url, 'interface': interface, - 'service_id': service['id'], 'enabled': enabled, - 'region': region}) - else: - # v3 in use, v2.0-like arguments, one endpoint created for each - # interface url provided - endpoint_args = {'region': region, 'enabled': enabled, - 'service_id': service['id']} - if public_url: - endpoint_args.update({'url': public_url, - 'interface': 'public'}) - endpoints_args.append(endpoint_args.copy()) - if internal_url: - endpoint_args.update({'url': internal_url, - 'interface': 'internal'}) - endpoints_args.append(endpoint_args.copy()) - if admin_url: - endpoint_args.update({'url': admin_url, - 'interface': 'admin'}) - endpoints_args.append(endpoint_args.copy()) - - endpoints = [] - error_msg = ("Failed to create endpoint for service" - " {service}".format(service=service['name'])) - for args in endpoints_args: - data = self._identity_client.post( - '/endpoints', json={'endpoint': args}, - error_message=error_msg) - endpoints.append(self._get_and_munchify('endpoint', data)) - return endpoints - - @_utils.valid_kwargs('enabled', 'service_name_or_id', 'url', 'interface', - 'region') - def update_endpoint(self, endpoint_id, **kwargs): - # NOTE(SamYaple): Endpoint updates are only available on v3 api - if self._is_client_version('identity', 2): - raise OpenStackCloudUnavailableFeature( - 'Unavailable Feature: Endpoint update' - ) - - service_name_or_id = kwargs.pop('service_name_or_id', None) - if service_name_or_id is not None: - kwargs['service_id'] = service_name_or_id - - data = self._identity_client.patch( - '/endpoints/{}'.format(endpoint_id), json={'endpoint': kwargs}, - error_message="Failed to update endpoint {}".format(endpoint_id)) - return self._get_and_munchify('endpoint', data) - - def list_endpoints(self): - """List Keystone endpoints. - - :returns: a list of ``munch.Munch`` containing the endpoint description - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - # Force admin interface if v2.0 is in use - v2 = self._is_client_version('identity', 2) - kwargs = {'endpoint_filter': {'interface': 'admin'}} if v2 else {} - - data = self._identity_client.get( - '/endpoints', error_message="Failed to list endpoints", **kwargs) - endpoints = self._get_and_munchify('endpoints', data) - - return endpoints - - def search_endpoints(self, id=None, filters=None): - """List Keystone endpoints. - - :param id: endpoint id. - :param filters: a dict containing additional filters to use. e.g. - {'region': 'region-a.geo-1'} - - :returns: a list of ``munch.Munch`` containing the endpoint - description. Each dict contains the following attributes:: - - id: - - region: - - public_url: - - internal_url: (optional) - - admin_url: (optional) - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - # NOTE(SamYaple): With keystone v3 we can filter directly via the - # the keystone api, but since the return of all the endpoints even in - # large environments is small, we can continue to filter in shade just - # like the v2 api. - endpoints = self.list_endpoints() - return _utils._filter_list(endpoints, id, filters) - - def get_endpoint(self, id, filters=None): - """Get exactly one Keystone endpoint. - - :param id: endpoint id. - :param filters: a dict containing additional filters to use. e.g. - {'region': 'region-a.geo-1'} - - :returns: a ``munch.Munch`` containing the endpoint description. - i.e. a ``munch.Munch`` containing the following attributes:: - - id: - - region: - - public_url: - - internal_url: (optional) - - admin_url: (optional) - """ - return _utils._get_entity(self, 'endpoint', id, filters) - - def delete_endpoint(self, id): - """Delete a Keystone endpoint. - - :param id: Id of the endpoint to delete. - - :returns: True if delete succeeded, False otherwise. - - :raises: ``OpenStackCloudException`` if something goes wrong during - the openstack API call. - """ - endpoint = self.get_endpoint(id=id) - if endpoint is None: - self.log.debug("Endpoint %s not found for deleting", id) - return False - - # Force admin interface if v2.0 is in use - v2 = self._is_client_version('identity', 2) - kwargs = {'endpoint_filter': {'interface': 'admin'}} if v2 else {} - - error_msg = "Failed to delete endpoint {id}".format(id=id) - self._identity_client.delete('/endpoints/{id}'.format(id=id), - error_message=error_msg, **kwargs) - - return True - - def create_domain(self, name, description=None, enabled=True): - """Create a domain. - - :param name: The name of the domain. - :param description: A description of the domain. - :param enabled: Is the domain enabled or not (default True). - - :returns: a ``munch.Munch`` containing the domain representation. - - :raise OpenStackCloudException: if the domain cannot be created. - """ - domain_ref = {'name': name, 'enabled': enabled} - if description is not None: - domain_ref['description'] = description - msg = 'Failed to create domain {name}'.format(name=name) - data = self._identity_client.post( - '/domains', json={'domain': domain_ref}, error_message=msg) - domain = self._get_and_munchify('domain', data) - return _utils.normalize_domains([domain])[0] - - def update_domain( - self, domain_id=None, name=None, description=None, - enabled=None, name_or_id=None): - if domain_id is None: - if name_or_id is None: - raise OpenStackCloudException( - "You must pass either domain_id or name_or_id value" - ) - dom = self.get_domain(None, name_or_id) - if dom is None: - raise OpenStackCloudException( - "Domain {0} not found for updating".format(name_or_id) - ) - domain_id = dom['id'] - - domain_ref = {} - domain_ref.update({'name': name} if name else {}) - domain_ref.update({'description': description} if description else {}) - domain_ref.update({'enabled': enabled} if enabled is not None else {}) - - error_msg = "Error in updating domain {id}".format(id=domain_id) - data = self._identity_client.patch( - '/domains/{id}'.format(id=domain_id), - json={'domain': domain_ref}, error_message=error_msg) - domain = self._get_and_munchify('domain', data) - return _utils.normalize_domains([domain])[0] - - def delete_domain(self, domain_id=None, name_or_id=None): - """Delete a domain. - - :param domain_id: ID of the domain to delete. - :param name_or_id: Name or ID of the domain to delete. - - :returns: True if delete succeeded, False otherwise. - - :raises: ``OpenStackCloudException`` if something goes wrong during - the openstack API call. - """ - if domain_id is None: - if name_or_id is None: - raise OpenStackCloudException( - "You must pass either domain_id or name_or_id value" - ) - dom = self.get_domain(name_or_id=name_or_id) - if dom is None: - self.log.debug( - "Domain %s not found for deleting", name_or_id) - return False - domain_id = dom['id'] - - # A domain must be disabled before deleting - self.update_domain(domain_id, enabled=False) - error_msg = "Failed to delete domain {id}".format(id=domain_id) - self._identity_client.delete('/domains/{id}'.format(id=domain_id), - error_message=error_msg) - - return True - - def list_domains(self, **filters): - """List Keystone domains. - - :returns: a list of ``munch.Munch`` containing the domain description. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - data = self._identity_client.get( - '/domains', params=filters, error_message="Failed to list domains") - domains = self._get_and_munchify('domains', data) - return _utils.normalize_domains(domains) - - def search_domains(self, filters=None, name_or_id=None): - """Search Keystone domains. - - :param name_or_id: domain name or id - :param dict filters: A dict containing additional filters to use. - Keys to search on are id, name, enabled and description. - - :returns: a list of ``munch.Munch`` containing the domain description. - Each ``munch.Munch`` contains the following attributes:: - - id: - - name: - - description: - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - if filters is None: - filters = {} - if name_or_id is not None: - domains = self.list_domains() - return _utils._filter_list(domains, name_or_id, filters) - else: - return self.list_domains(**filters) - - def get_domain(self, domain_id=None, name_or_id=None, filters=None): - """Get exactly one Keystone domain. - - :param domain_id: domain id. - :param name_or_id: domain name or id. - :param dict filters: A dict containing additional filters to use. - Keys to search on are id, name, enabled and description. - - :returns: a ``munch.Munch`` containing the domain description, or None - if not found. Each ``munch.Munch`` contains the following - attributes:: - - id: - - name: - - description: - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - if domain_id is None: - # NOTE(SamYaple): search_domains() has filters and name_or_id - # in the wrong positional order which prevents _get_entity from - # being able to return quickly if passing a domain object so we - # duplicate that logic here - if hasattr(name_or_id, 'id'): - return name_or_id - return _utils._get_entity(self, 'domain', filters, name_or_id) - else: - error_msg = 'Failed to get domain {id}'.format(id=domain_id) - data = self._identity_client.get( - '/domains/{id}'.format(id=domain_id), - error_message=error_msg) - domain = self._get_and_munchify('domain', data) - return _utils.normalize_domains([domain])[0] - - @_utils.valid_kwargs('domain_id') - @_utils.cache_on_arguments() - def list_groups(self, **kwargs): - """List Keystone Groups. - - :param domain_id: domain id. - - :returns: A list of ``munch.Munch`` containing the group description. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - data = self._identity_client.get( - '/groups', params=kwargs, error_message="Failed to list groups") - return _utils.normalize_groups(self._get_and_munchify('groups', data)) - - @_utils.valid_kwargs('domain_id') - def search_groups(self, name_or_id=None, filters=None, **kwargs): - """Search Keystone groups. - - :param name: Group name or id. - :param filters: A dict containing additional filters to use. - :param domain_id: domain id. - - :returns: A list of ``munch.Munch`` containing the group description. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - groups = self.list_groups(**kwargs) - return _utils._filter_list(groups, name_or_id, filters) - - @_utils.valid_kwargs('domain_id') - def get_group(self, name_or_id, filters=None, **kwargs): - """Get exactly one Keystone group. - - :param id: Group name or id. - :param filters: A dict containing additional filters to use. - :param domain_id: domain id. - - :returns: A ``munch.Munch`` containing the group description. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - return _utils._get_entity(self, 'group', name_or_id, filters, **kwargs) - - def create_group(self, name, description, domain=None): - """Create a group. - - :param string name: Group name. - :param string description: Group description. - :param string domain: Domain name or ID for the group. - - :returns: A ``munch.Munch`` containing the group description. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - group_ref = {'name': name} - if description: - group_ref['description'] = description - if domain: - dom = self.get_domain(domain) - if not dom: - raise OpenStackCloudException( - "Creating group {group} failed: Invalid domain " - "{domain}".format(group=name, domain=domain) - ) - group_ref['domain_id'] = dom['id'] - - error_msg = "Error creating group {group}".format(group=name) - data = self._identity_client.post( - '/groups', json={'group': group_ref}, error_message=error_msg) - group = self._get_and_munchify('group', data) - self.list_groups.invalidate(self) - return _utils.normalize_groups([group])[0] - - @_utils.valid_kwargs('domain_id') - def update_group(self, name_or_id, name=None, description=None, - **kwargs): - """Update an existing group - - :param string name: New group name. - :param string description: New group description. - :param domain_id: domain id. - - :returns: A ``munch.Munch`` containing the group description. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - self.list_groups.invalidate(self) - group = self.get_group(name_or_id, **kwargs) - if group is None: - raise OpenStackCloudException( - "Group {0} not found for updating".format(name_or_id) - ) - - group_ref = {} - if name: - group_ref['name'] = name - if description: - group_ref['description'] = description - - error_msg = "Unable to update group {name}".format(name=name_or_id) - data = self._identity_client.patch( - '/groups/{id}'.format(id=group['id']), - json={'group': group_ref}, error_message=error_msg) - group = self._get_and_munchify('group', data) - self.list_groups.invalidate(self) - return _utils.normalize_groups([group])[0] - - @_utils.valid_kwargs('domain_id') - def delete_group(self, name_or_id, **kwargs): - """Delete a group - - :param name_or_id: ID or name of the group to delete. - :param domain_id: domain id. - - :returns: True if delete succeeded, False otherwise. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - group = self.get_group(name_or_id, **kwargs) - if group is None: - self.log.debug( - "Group %s not found for deleting", name_or_id) - return False - - error_msg = "Unable to delete group {name}".format(name=name_or_id) - self._identity_client.delete('/groups/{id}'.format(id=group['id']), - error_message=error_msg) - - self.list_groups.invalidate(self) - return True - - @_utils.valid_kwargs('domain_id') - def list_roles(self, **kwargs): - """List Keystone roles. - - :param domain_id: domain id for listing roles (v3) - - :returns: a list of ``munch.Munch`` containing the role description. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - v2 = self._is_client_version('identity', 2) - url = '/OS-KSADM/roles' if v2 else '/roles' - data = self._identity_client.get( - url, params=kwargs, error_message="Failed to list roles") - return self._normalize_roles(self._get_and_munchify('roles', data)) - - @_utils.valid_kwargs('domain_id') - def search_roles(self, name_or_id=None, filters=None, **kwargs): - """Seach Keystone roles. - - :param string name: role name or id. - :param dict filters: a dict containing additional filters to use. - :param domain_id: domain id (v3) - - :returns: a list of ``munch.Munch`` containing the role description. - Each ``munch.Munch`` contains the following attributes:: - - - id: - - name: - - description: - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - roles = self.list_roles(**kwargs) - return _utils._filter_list(roles, name_or_id, filters) - - @_utils.valid_kwargs('domain_id') - def get_role(self, name_or_id, filters=None, **kwargs): - """Get exactly one Keystone role. - - :param id: role name or id. - :param filters: a dict containing additional filters to use. - :param domain_id: domain id (v3) - - :returns: a single ``munch.Munch`` containing the role description. - Each ``munch.Munch`` contains the following attributes:: - - - id: - - name: - - description: - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - return _utils._get_entity(self, 'role', name_or_id, filters, **kwargs) - - def _keystone_v2_role_assignments(self, user, project=None, - role=None, **kwargs): - data = self._identity_client.get( - "/tenants/{tenant}/users/{user}/roles".format( - tenant=project, user=user), - error_message="Failed to list role assignments") - - roles = self._get_and_munchify('roles', data) - - ret = [] - for tmprole in roles: - if role is not None and role != tmprole.id: - continue - ret.append({ - 'role': { - 'id': tmprole.id - }, - 'scope': { - 'project': { - 'id': project, - } - }, - 'user': { - 'id': user, - } - }) - return ret - - def _keystone_v3_role_assignments(self, **filters): - # NOTE(samueldmq): different parameters have different representation - # patterns as query parameters in the call to the list role assignments - # API. The code below handles each set of patterns separately and - # renames the parameters names accordingly, ignoring 'effective', - # 'include_names' and 'include_subtree' whose do not need any renaming. - for k in ('group', 'role', 'user'): - if k in filters: - filters[k + '.id'] = filters[k] - del filters[k] - for k in ('project', 'domain'): - if k in filters: - filters['scope.' + k + '.id'] = filters[k] - del filters[k] - if 'os_inherit_extension_inherited_to' in filters: - filters['scope.OS-INHERIT:inherited_to'] = ( - filters['os_inherit_extension_inherited_to']) - del filters['os_inherit_extension_inherited_to'] - - data = self._identity_client.get( - '/role_assignments', params=filters, - error_message="Failed to list role assignments") - return self._get_and_munchify('role_assignments', data) - - def list_role_assignments(self, filters=None): - """List Keystone role assignments - - :param dict filters: Dict of filter conditions. Acceptable keys are: - - * 'user' (string) - User ID to be used as query filter. - * 'group' (string) - Group ID to be used as query filter. - * 'project' (string) - Project ID to be used as query filter. - * 'domain' (string) - Domain ID to be used as query filter. - * 'role' (string) - Role ID to be used as query filter. - * 'os_inherit_extension_inherited_to' (string) - Return inherited - role assignments for either 'projects' or 'domains' - * 'effective' (boolean) - Return effective role assignments. - * 'include_subtree' (boolean) - Include subtree - - 'user' and 'group' are mutually exclusive, as are 'domain' and - 'project'. - - NOTE: For keystone v2, only user, project, and role are used. - Project and user are both required in filters. - - :returns: a list of ``munch.Munch`` containing the role assignment - description. Contains the following attributes:: - - - id: - - user|group: - - project|domain: - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - # NOTE(samueldmq): although 'include_names' is a valid query parameter - # in the keystone v3 list role assignments API, it would have NO effect - # on shade due to normalization. It is not documented as an acceptable - # filter in the docs above per design! - - if not filters: - filters = {} - - # NOTE(samueldmq): the docs above say filters are *IDs*, though if - # munch.Munch objects are passed, this still works for backwards - # compatibility as keystoneclient allows either IDs or objects to be - # passed in. - # TODO(samueldmq): fix the docs above to advertise munch.Munch objects - # can be provided as parameters too - for k, v in filters.items(): - if isinstance(v, munch.Munch): - filters[k] = v['id'] - - if self._is_client_version('identity', 2): - if filters.get('project') is None or filters.get('user') is None: - raise OpenStackCloudException( - "Must provide project and user for keystone v2" - ) - assignments = self._keystone_v2_role_assignments(**filters) - else: - assignments = self._keystone_v3_role_assignments(**filters) - - return _utils.normalize_role_assignments(assignments) - - def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", - ephemeral=0, swap=0, rxtx_factor=1.0, is_public=True): - """Create a new flavor. - - :param name: Descriptive name of the flavor - :param ram: Memory in MB for the flavor - :param vcpus: Number of VCPUs for the flavor - :param disk: Size of local disk in GB - :param flavorid: ID for the flavor (optional) - :param ephemeral: Ephemeral space size in GB - :param swap: Swap space in MB - :param rxtx_factor: RX/TX factor - :param is_public: Make flavor accessible to the public - - :returns: A ``munch.Munch`` describing the new flavor. - - :raises: OpenStackCloudException on operation error. - """ - with _utils.shade_exceptions("Failed to create flavor {name}".format( - name=name)): - payload = { - 'disk': disk, - 'OS-FLV-EXT-DATA:ephemeral': ephemeral, - 'id': flavorid, - 'os-flavor-access:is_public': is_public, - 'name': name, - 'ram': ram, - 'rxtx_factor': rxtx_factor, - 'swap': swap, - 'vcpus': vcpus, - } - if flavorid == 'auto': - payload['id'] = None - data = _adapter._json_response(self._conn.compute.post( - '/flavors', - json=dict(flavor=payload))) - - return self._normalize_flavor( - self._get_and_munchify('flavor', data)) - - def delete_flavor(self, name_or_id): - """Delete a flavor - - :param name_or_id: ID or name of the flavor to delete. - - :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. - """ - flavor = self.get_flavor(name_or_id, get_extra=False) - if flavor is None: - self.log.debug( - "Flavor %s not found for deleting", name_or_id) - return False - - _adapter._json_response( - self._conn.compute.delete( - '/flavors/{id}'.format(id=flavor['id'])), - error_message="Unable to delete flavor {name}".format( - name=name_or_id)) - - return True - - def set_flavor_specs(self, flavor_id, extra_specs): - """Add extra specs to a flavor - - :param string flavor_id: ID of the flavor to update. - :param dict extra_specs: Dictionary of key-value pairs. - - :raises: OpenStackCloudException on operation error. - :raises: OpenStackCloudResourceNotFound if flavor ID is not found. - """ - _adapter._json_response( - self._conn.compute.post( - "/flavors/{id}/os-extra_specs".format(id=flavor_id), - json=dict(extra_specs=extra_specs)), - error_message="Unable to set flavor specs") - - def unset_flavor_specs(self, flavor_id, keys): - """Delete extra specs from a flavor - - :param string flavor_id: ID of the flavor to update. - :param keys: List of spec keys to delete. - - :raises: OpenStackCloudException on operation error. - :raises: OpenStackCloudResourceNotFound if flavor ID is not found. - """ - for key in keys: - _adapter._json_response( - self._conn.compute.delete( - "/flavors/{id}/os-extra_specs/{key}".format( - id=flavor_id, key=key)), - error_message="Unable to delete flavor spec {0}".format(key)) - - def _mod_flavor_access(self, action, flavor_id, project_id): - """Common method for adding and removing flavor access - """ - with _utils.shade_exceptions("Error trying to {action} access from " - "flavor ID {flavor}".format( - action=action, flavor=flavor_id)): - endpoint = '/flavors/{id}/action'.format(id=flavor_id) - access = {'tenant': project_id} - access_key = '{action}TenantAccess'.format(action=action) - - _adapter._json_response( - self._conn.compute.post(endpoint, json={access_key: access})) - - def add_flavor_access(self, flavor_id, project_id): - """Grant access to a private flavor for a project/tenant. - - :param string flavor_id: ID of the private flavor. - :param string project_id: ID of the project/tenant. - - :raises: OpenStackCloudException on operation error. - """ - self._mod_flavor_access('add', flavor_id, project_id) - - def remove_flavor_access(self, flavor_id, project_id): - """Revoke access from a private flavor for a project/tenant. - - :param string flavor_id: ID of the private flavor. - :param string project_id: ID of the project/tenant. - - :raises: OpenStackCloudException on operation error. - """ - self._mod_flavor_access('remove', flavor_id, project_id) - - def list_flavor_access(self, flavor_id): - """List access from a private flavor for a project/tenant. - - :param string flavor_id: ID of the private flavor. - - :returns: a list of ``munch.Munch`` containing the access description - - :raises: OpenStackCloudException on operation error. - """ - data = _adapter._json_response( - self._conn.compute.get( - '/flavors/{id}/os-flavor-access'.format(id=flavor_id)), - error_message=( - "Error trying to list access from flavorID {flavor}".format( - flavor=flavor_id))) - return _utils.normalize_flavor_accesses( - self._get_and_munchify('flavor_access', data)) - - @_utils.valid_kwargs('domain_id') - def create_role(self, name, **kwargs): - """Create a Keystone role. - - :param string name: The name of the role. - :param domain_id: domain id (v3) - - :returns: a ``munch.Munch`` containing the role description - - :raise OpenStackCloudException: if the role cannot be created - """ - v2 = self._is_client_version('identity', 2) - url = '/OS-KSADM/roles' if v2 else '/roles' - kwargs['name'] = name - msg = 'Failed to create role {name}'.format(name=name) - data = self._identity_client.post( - url, json={'role': kwargs}, error_message=msg) - role = self._get_and_munchify('role', data) - return self._normalize_role(role) - - @_utils.valid_kwargs('domain_id') - def update_role(self, name_or_id, name, **kwargs): - """Update a Keystone role. - - :param name_or_id: Name or id of the role to update - :param string name: The new role name - :param domain_id: domain id - - :returns: a ``munch.Munch`` containing the role description - - :raise OpenStackCloudException: if the role cannot be created - """ - if self._is_client_version('identity', 2): - raise OpenStackCloudUnavailableFeature( - 'Unavailable Feature: Role update requires Identity v3' - ) - kwargs['name_or_id'] = name_or_id - role = self.get_role(**kwargs) - if role is None: - self.log.debug( - "Role %s not found for updating", name_or_id) - return False - msg = 'Failed to update role {name}'.format(name=name_or_id) - json_kwargs = {'role_id': role.id, 'role': {'name': name}} - data = self._identity_client.patch('/roles', error_message=msg, - json=json_kwargs) - role = self._get_and_munchify('role', data) - return self._normalize_role(role) - - @_utils.valid_kwargs('domain_id') - def delete_role(self, name_or_id, **kwargs): - """Delete a Keystone role. - - :param string id: Name or id of the role to delete. - :param domain_id: domain id (v3) - - :returns: True if delete succeeded, False otherwise. - - :raises: ``OpenStackCloudException`` if something goes wrong during - the openstack API call. - """ - role = self.get_role(name_or_id, **kwargs) - if role is None: - self.log.debug( - "Role %s not found for deleting", name_or_id) - return False - - v2 = self._is_client_version('identity', 2) - url = '{preffix}/{id}'.format( - preffix='/OS-KSADM/roles' if v2 else '/roles', id=role['id']) - error_msg = "Unable to delete role {name}".format(name=name_or_id) - self._identity_client.delete(url, error_message=error_msg) - - return True - - def _get_grant_revoke_params(self, role, user=None, group=None, - project=None, domain=None): - role = self.get_role(role) - if role is None: - return {} - data = {'role': role.id} - - # domain and group not available in keystone v2.0 - is_keystone_v2 = self._is_client_version('identity', 2) - - filters = {} - if not is_keystone_v2 and domain: - filters['domain_id'] = data['domain'] = \ - self.get_domain(domain)['id'] - - if user: - data['user'] = self.get_user(user, filters=filters) - - if project: - # drop domain in favor of project - data.pop('domain', None) - data['project'] = self.get_project(project, filters=filters) - - if not is_keystone_v2 and group: - data['group'] = self.get_group(group, filters=filters) - - return data - - def grant_role(self, name_or_id, user=None, group=None, - project=None, domain=None, wait=False, timeout=60): - """Grant a role to a user. - - :param string name_or_id: The name or id of the role. - :param string user: The name or id of the user. - :param string group: The name or id of the group. (v3) - :param string project: The name or id of the project. - :param string domain: The id of the domain. (v3) - :param bool wait: Wait for role to be granted - :param int timeout: Timeout to wait for role to be granted - - NOTE: domain is a required argument when the grant is on a project, - user or group specified by name. In that situation, they are all - considered to be in that domain. If different domains are in use - in the same role grant, it is required to specify those by ID. - - NOTE: for wait and timeout, sometimes granting roles is not - instantaneous. - - NOTE: project is required for keystone v2 - - :returns: True if the role is assigned, otherwise False - - :raise OpenStackCloudException: if the role cannot be granted - """ - data = self._get_grant_revoke_params(name_or_id, user, group, - project, domain) - filters = data.copy() - if not data: - raise OpenStackCloudException( - 'Role {0} not found.'.format(name_or_id)) - - if data.get('user') is not None and data.get('group') is not None: - raise OpenStackCloudException( - 'Specify either a group or a user, not both') - if data.get('user') is None and data.get('group') is None: - raise OpenStackCloudException( - 'Must specify either a user or a group') - if self._is_client_version('identity', 2) and \ - data.get('project') is None: - raise OpenStackCloudException( - 'Must specify project for keystone v2') - - if self.list_role_assignments(filters=filters): - self.log.debug('Assignment already exists') - return False - - error_msg = "Error granting access to role: {0}".format(data) - if self._is_client_version('identity', 2): - # For v2.0, only tenant/project assignment is supported - url = "/tenants/{t}/users/{u}/roles/OS-KSADM/{r}".format( - t=data['project']['id'], u=data['user']['id'], r=data['role']) - - self._identity_client.put(url, error_message=error_msg, - endpoint_filter={'interface': 'admin'}) - else: - if data.get('project') is None and data.get('domain') is None: - raise OpenStackCloudException( - 'Must specify either a domain or project') - - # For v3, figure out the assignment type and build the URL - if data.get('domain'): - url = "/domains/{}".format(data['domain']) - else: - url = "/projects/{}".format(data['project']['id']) - if data.get('group'): - url += "/groups/{}".format(data['group']['id']) - else: - url += "/users/{}".format(data['user']['id']) - url += "/roles/{}".format(data.get('role')) - - self._identity_client.put(url, error_message=error_msg) - - if wait: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for role to be granted"): - if self.list_role_assignments(filters=filters): - break - return True - - def revoke_role(self, name_or_id, user=None, group=None, - project=None, domain=None, wait=False, timeout=60): - """Revoke a role from a user. - - :param string name_or_id: The name or id of the role. - :param string user: The name or id of the user. - :param string group: The name or id of the group. (v3) - :param string project: The name or id of the project. - :param string domain: The id of the domain. (v3) - :param bool wait: Wait for role to be revoked - :param int timeout: Timeout to wait for role to be revoked - - NOTE: for wait and timeout, sometimes revoking roles is not - instantaneous. - - NOTE: project is required for keystone v2 - - :returns: True if the role is revoke, otherwise False - - :raise OpenStackCloudException: if the role cannot be removed - """ - data = self._get_grant_revoke_params(name_or_id, user, group, - project, domain) - filters = data.copy() - - if not data: - raise OpenStackCloudException( - 'Role {0} not found.'.format(name_or_id)) - - if data.get('user') is not None and data.get('group') is not None: - raise OpenStackCloudException( - 'Specify either a group or a user, not both') - if data.get('user') is None and data.get('group') is None: - raise OpenStackCloudException( - 'Must specify either a user or a group') - if self._is_client_version('identity', 2) and \ - data.get('project') is None: - raise OpenStackCloudException( - 'Must specify project for keystone v2') - - if not self.list_role_assignments(filters=filters): - self.log.debug('Assignment does not exist') - return False - - error_msg = "Error revoking access to role: {0}".format(data) - if self._is_client_version('identity', 2): - # For v2.0, only tenant/project assignment is supported - url = "/tenants/{t}/users/{u}/roles/OS-KSADM/{r}".format( - t=data['project']['id'], u=data['user']['id'], r=data['role']) - - self._identity_client.delete( - url, error_message=error_msg, - endpoint_filter={'interface': 'admin'}) - else: - if data.get('project') is None and data.get('domain') is None: - raise OpenStackCloudException( - 'Must specify either a domain or project') - - # For v3, figure out the assignment type and build the URL - if data.get('domain'): - url = "/domains/{}".format(data['domain']) - else: - url = "/projects/{}".format(data['project']['id']) - if data.get('group'): - url += "/groups/{}".format(data['group']['id']) - else: - url += "/users/{}".format(data['user']['id']) - url += "/roles/{}".format(data.get('role')) - - self._identity_client.delete(url, error_message=error_msg) - - if wait: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for role to be revoked"): - if not self.list_role_assignments(filters=filters): - break - return True - - def list_hypervisors(self): - """List all hypervisors - - :returns: A list of hypervisor ``munch.Munch``. - """ - - data = _adapter._json_response( - self._conn.compute.get('/os-hypervisors/detail'), - error_message="Error fetching hypervisor list") - return self._get_and_munchify('hypervisors', data) - - def search_aggregates(self, name_or_id=None, filters=None): - """Seach host aggregates. - - :param name: aggregate name or id. - :param filters: a dict containing additional filters to use. - - :returns: a list of dicts containing the aggregates - - :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. - """ - aggregates = self.list_aggregates() - return _utils._filter_list(aggregates, name_or_id, filters) - - def list_aggregates(self): - """List all available host aggregates. - - :returns: A list of aggregate dicts. - - """ - data = _adapter._json_response( - self._conn.compute.get('/os-aggregates'), - error_message="Error fetching aggregate list") - return self._get_and_munchify('aggregates', data) - - def get_aggregate(self, name_or_id, filters=None): - """Get an aggregate by name or ID. - - :param name_or_id: Name or ID of the aggregate. - :param dict filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'availability_zone': 'nova', - 'metadata': { - 'cpu_allocation_ratio': '1.0' - } - } - - :returns: An aggregate dict or None if no matching aggregate is - found. - - """ - return _utils._get_entity(self, 'aggregate', name_or_id, filters) - - def create_aggregate(self, name, availability_zone=None): - """Create a new host aggregate. - - :param name: Name of the host aggregate being created - :param availability_zone: Availability zone to assign hosts - - :returns: a dict representing the new host aggregate. - - :raises: OpenStackCloudException on operation error. - """ - data = _adapter._json_response( - self._conn.compute.post( - '/os-aggregates', - json={'aggregate': { - 'name': name, - 'availability_zone': availability_zone - }}), - error_message="Unable to create host aggregate {name}".format( - name=name)) - return self._get_and_munchify('aggregate', data) - - @_utils.valid_kwargs('name', 'availability_zone') - def update_aggregate(self, name_or_id, **kwargs): - """Update a host aggregate. - - :param name_or_id: Name or ID of the aggregate being updated. - :param name: New aggregate name - :param availability_zone: Availability zone to assign to hosts - - :returns: a dict representing the updated host aggregate. - - :raises: OpenStackCloudException on operation error. - """ - aggregate = self.get_aggregate(name_or_id) - if not aggregate: - raise OpenStackCloudException( - "Host aggregate %s not found." % name_or_id) - - data = _adapter._json_response( - self._conn.compute.put( - '/os-aggregates/{id}'.format(id=aggregate['id']), - json={'aggregate': kwargs}), - error_message="Error updating aggregate {name}".format( - name=name_or_id)) - return self._get_and_munchify('aggregate', data) - - def delete_aggregate(self, name_or_id): - """Delete a host aggregate. - - :param name_or_id: Name or ID of the host aggregate to delete. - - :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. - """ - aggregate = self.get_aggregate(name_or_id) - if not aggregate: - self.log.debug("Aggregate %s not found for deleting", name_or_id) - return False - - return _adapter._json_response( - self._conn.compute.delete( - '/os-aggregates/{id}'.format(id=aggregate['id'])), - error_message="Error deleting aggregate {name}".format( - name=name_or_id)) - - return True - - def set_aggregate_metadata(self, name_or_id, metadata): - """Set aggregate metadata, replacing the existing metadata. - - :param name_or_id: Name of the host aggregate to update - :param metadata: Dict containing metadata to replace (Use - {'key': None} to remove a key) - - :returns: a dict representing the new host aggregate. - - :raises: OpenStackCloudException on operation error. - """ - aggregate = self.get_aggregate(name_or_id) - if not aggregate: - raise OpenStackCloudException( - "Host aggregate %s not found." % name_or_id) - - err_msg = "Unable to set metadata for host aggregate {name}".format( - name=name_or_id) - - data = _adapter._json_response( - self._conn.compute.post( - '/os-aggregates/{id}/action'.format(id=aggregate['id']), - json={'set_metadata': {'metadata': metadata}}), - error_message=err_msg) - return self._get_and_munchify('aggregate', data) - - def add_host_to_aggregate(self, name_or_id, host_name): - """Add a host to an aggregate. - - :param name_or_id: Name or ID of the host aggregate. - :param host_name: Host to add. - - :raises: OpenStackCloudException on operation error. - """ - aggregate = self.get_aggregate(name_or_id) - if not aggregate: - raise OpenStackCloudException( - "Host aggregate %s not found." % name_or_id) - - err_msg = "Unable to add host {host} to aggregate {name}".format( - host=host_name, name=name_or_id) - - return _adapter._json_response( - self._conn.compute.post( - '/os-aggregates/{id}/action'.format(id=aggregate['id']), - json={'add_host': {'host': host_name}}), - error_message=err_msg) - - def remove_host_from_aggregate(self, name_or_id, host_name): - """Remove a host from an aggregate. - - :param name_or_id: Name or ID of the host aggregate. - :param host_name: Host to remove. - - :raises: OpenStackCloudException on operation error. - """ - aggregate = self.get_aggregate(name_or_id) - if not aggregate: - raise OpenStackCloudException( - "Host aggregate %s not found." % name_or_id) - - err_msg = "Unable to remove host {host} to aggregate {name}".format( - host=host_name, name=name_or_id) - - return _adapter._json_response( - self._conn.compute.post( - '/os-aggregates/{id}/action'.format(id=aggregate['id']), - json={'remove_host': {'host': host_name}}), - error_message=err_msg) - - def get_volume_type_access(self, name_or_id): - """Return a list of volume_type_access. - - :param name_or_id: Name or ID of the volume type. - - :raises: OpenStackCloudException on operation error. - """ - volume_type = self.get_volume_type(name_or_id) - if not volume_type: - raise OpenStackCloudException( - "VolumeType not found: %s" % name_or_id) - - data = self._volume_client.get( - '/types/{id}/os-volume-type-access'.format(id=volume_type.id), - error_message="Unable to get volume type access" - " {name}".format(name=name_or_id)) - return self._normalize_volume_type_accesses( - self._get_and_munchify('volume_type_access', data)) - - def add_volume_type_access(self, name_or_id, project_id): - """Grant access on a volume_type to a project. - - :param name_or_id: ID or name of a volume_type - :param project_id: A project id - - NOTE: the call works even if the project does not exist. - - :raises: OpenStackCloudException on operation error. - """ - volume_type = self.get_volume_type(name_or_id) - if not volume_type: - raise OpenStackCloudException( - "VolumeType not found: %s" % name_or_id) - with _utils.shade_exceptions(): - payload = {'project': project_id} - self._volume_client.post( - '/types/{id}/action'.format(id=volume_type.id), - json=dict(addProjectAccess=payload), - error_message="Unable to authorize {project} " - "to use volume type {name}".format( - name=name_or_id, project=project_id)) - - def remove_volume_type_access(self, name_or_id, project_id): - """Revoke access on a volume_type to a project. - - :param name_or_id: ID or name of a volume_type - :param project_id: A project id - - :raises: OpenStackCloudException on operation error. - """ - volume_type = self.get_volume_type(name_or_id) - if not volume_type: - raise OpenStackCloudException( - "VolumeType not found: %s" % name_or_id) - with _utils.shade_exceptions(): - payload = {'project': project_id} - self._volume_client.post( - '/types/{id}/action'.format(id=volume_type.id), - json=dict(removeProjectAccess=payload), - error_message="Unable to revoke {project} " - "to use volume type {name}".format( - name=name_or_id, project=project_id)) - - def set_compute_quotas(self, name_or_id, **kwargs): - """ Set a quota in a project - - :param name_or_id: project name or id - :param kwargs: key/value pairs of quota name and quota value - - :raises: OpenStackCloudException if the resource to set the - quota does not exist. - """ - - proj = self.get_project(name_or_id) - if not proj: - raise OpenStackCloudException("project does not exist") - - # compute_quotas = {key: val for key, val in kwargs.items() - # if key in quota.COMPUTE_QUOTAS} - # TODO(ghe): Manage volume and network quotas - # network_quotas = {key: val for key, val in kwargs.items() - # if key in quota.NETWORK_QUOTAS} - # volume_quotas = {key: val for key, val in kwargs.items() - # if key in quota.VOLUME_QUOTAS} - - kwargs['force'] = True - _adapter._json_response( - self._conn.compute.put( - '/os-quota-sets/{project}'.format(project=proj.id), - json={'quota_set': kwargs}), - error_message="No valid quota or resource") - - def get_compute_quotas(self, name_or_id): - """ Get quota for a project - - :param name_or_id: project name or id - :raises: OpenStackCloudException if it's not a valid project - - :returns: Munch object with the quotas - """ - proj = self.get_project(name_or_id) - if not proj: - raise OpenStackCloudException("project does not exist") - data = _adapter._json_response( - self._conn.compute.get( - '/os-quota-sets/{project}'.format(project=proj.id))) - return self._get_and_munchify('quota_set', data) - - def delete_compute_quotas(self, name_or_id): - """ Delete quota for a project - - :param name_or_id: project name or id - :raises: OpenStackCloudException if it's not a valid project or the - nova client call failed - - :returns: dict with the quotas - """ - proj = self.get_project(name_or_id) - if not proj: - raise OpenStackCloudException("project does not exist") - return _adapter._json_response( - self._conn.compute.delete( - '/os-quota-sets/{project}'.format(project=proj.id))) - - def get_compute_usage(self, name_or_id, start=None, end=None): - """ Get usage for a specific project - - :param name_or_id: project name or id - :param start: :class:`datetime.datetime` or string. Start date in UTC - Defaults to 2010-07-06T12:00:00Z (the date the OpenStack - project was started) - :param end: :class:`datetime.datetime` or string. End date in UTC. - Defaults to now - :raises: OpenStackCloudException if it's not a valid project - - :returns: Munch object with the usage - """ - def parse_date(date): - try: - return iso8601.parse_date(date) - except iso8601.iso8601.ParseError: - # Yes. This is an exception mask. However,iso8601 is an - # implementation detail - and the error message is actually - # less informative. - raise OpenStackCloudException( - "Date given, {date}, is invalid. Please pass in a date" - " string in ISO 8601 format -" - " YYYY-MM-DDTHH:MM:SS".format( - date=date)) - - def parse_datetime_for_nova(date): - # Must strip tzinfo from the date- it breaks Nova. Also, - # Nova is expecting this in UTC. If someone passes in an - # ISO8601 date string or a datetime with timzeone data attached, - # strip the timezone data but apply offset math first so that - # the user's well formed perfectly valid date will be used - # correctly. - offset = date.utcoffset() - if offset: - date = date - datetime.timedelta(hours=offset) - return date.replace(tzinfo=None) - - if not start: - start = parse_date('2010-07-06') - elif not isinstance(start, datetime.datetime): - start = parse_date(start) - if not end: - end = datetime.datetime.utcnow() - elif not isinstance(start, datetime.datetime): - end = parse_date(end) - - start = parse_datetime_for_nova(start) - end = parse_datetime_for_nova(end) - - proj = self.get_project(name_or_id) - if not proj: - raise OpenStackCloudException("project does not exist: {}".format( - name=proj.id)) - - data = _adapter._json_response( - self._conn.compute.get( - '/os-simple-tenant-usage/{project}'.format(project=proj.id), - params=dict(start=start.isoformat(), end=end.isoformat())), - error_message="Unable to get usage for project: {name}".format( - name=proj.id)) - return self._normalize_compute_usage( - self._get_and_munchify('tenant_usage', data)) - - def set_volume_quotas(self, name_or_id, **kwargs): - """ Set a volume quota in a project - - :param name_or_id: project name or id - :param kwargs: key/value pairs of quota name and quota value - - :raises: OpenStackCloudException if the resource to set the - quota does not exist. - """ - - proj = self.get_project(name_or_id) - if not proj: - raise OpenStackCloudException("project does not exist") - - kwargs['tenant_id'] = proj.id - self._volume_client.put( - '/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id), - json={'quota_set': kwargs}, - error_message="No valid quota or resource") - - def get_volume_quotas(self, name_or_id): - """ Get volume quotas for a project - - :param name_or_id: project name or id - :raises: OpenStackCloudException if it's not a valid project - - :returns: Munch object with the quotas - """ - proj = self.get_project(name_or_id) - if not proj: - raise OpenStackCloudException("project does not exist") - - data = self._volume_client.get( - '/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id), - error_message="cinder client call failed") - return self._get_and_munchify('quota_set', data) - - def delete_volume_quotas(self, name_or_id): - """ Delete volume quotas for a project - - :param name_or_id: project name or id - :raises: OpenStackCloudException if it's not a valid project or the - cinder client call failed - - :returns: dict with the quotas - """ - proj = self.get_project(name_or_id) - if not proj: - raise OpenStackCloudException("project does not exist") - - return self._volume_client.delete( - '/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id), - error_message="cinder client call failed") - - def set_network_quotas(self, name_or_id, **kwargs): - """ Set a network quota in a project - - :param name_or_id: project name or id - :param kwargs: key/value pairs of quota name and quota value - - :raises: OpenStackCloudException if the resource to set the - quota does not exist. - """ - - proj = self.get_project(name_or_id) - if not proj: - raise OpenStackCloudException("project does not exist") - - self._network_client.put( - '/quotas/{project_id}.json'.format(project_id=proj.id), - json={'quota': kwargs}, - error_message=("Error setting Neutron's quota for " - "project {0}".format(proj.id))) - - def get_network_quotas(self, name_or_id, details=False): - """ Get network quotas for a project - - :param name_or_id: project name or id - :param details: if set to True it will return details about usage - of quotas by given project - :raises: OpenStackCloudException if it's not a valid project - - :returns: Munch object with the quotas - """ - proj = self.get_project(name_or_id) - if not proj: - raise OpenStackCloudException("project does not exist") - url = '/quotas/{project_id}'.format(project_id=proj.id) - if details: - url = url + "/details" - url = url + ".json" - data = self._network_client.get( - url, - error_message=("Error fetching Neutron's quota for " - "project {0}".format(proj.id))) - return self._get_and_munchify('quota', data) - - def delete_network_quotas(self, name_or_id): - """ Delete network quotas for a project - - :param name_or_id: project name or id - :raises: OpenStackCloudException if it's not a valid project or the - network client call failed - - :returns: dict with the quotas - """ - proj = self.get_project(name_or_id) - if not proj: - raise OpenStackCloudException("project does not exist") - self._network_client.delete( - '/quotas/{project_id}.json'.format(project_id=proj.id), - error_message=("Error deleting Neutron's quota for " - "project {0}".format(proj.id))) - - def list_magnum_services(self): - """List all Magnum services. - :returns: a list of dicts containing the service details. - - :raises: OpenStackCloudException on operation error. - """ - with _utils.shade_exceptions("Error fetching Magnum services list"): - data = self._container_infra_client.get('/mservices') - return self._normalize_magnum_services( - self._get_and_munchify('mservices', data)) diff --git a/openstack/tests/functional/cloud/base.py b/openstack/tests/functional/cloud/base.py index c8a9a5da1..dbbcb474b 100644 --- a/openstack/tests/functional/cloud/base.py +++ b/openstack/tests/functional/cloud/base.py @@ -44,7 +44,7 @@ def _set_user_cloud(self, **kwargs): def _set_operator_cloud(self, **kwargs): operator_config = self.config.get_one( cloud=self._op_name, **kwargs) - self.operator_cloud = openstack.cloud.OperatorCloud( + self.operator_cloud = openstack.cloud.OpenStackCloud( cloud_config=operator_config) def pick_image(self): diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 4d969f363..b784be1a0 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -124,8 +124,6 @@ def _nosleep(seconds): self.strict_cloud = openstack.cloud.OpenStackCloud( cloud_config=self.cloud_config, strict=True) - self.op_cloud = openstack.cloud.OperatorCloud( - cloud_config=self.cloud_config) # TODO(shade) Remove this and rename RequestsMockTestCase to TestCase. @@ -464,8 +462,6 @@ def _make_test_cloud(self, cloud_name='_test_cloud_', **kwargs): config=self.cloud_config) self.cloud = openstack.cloud.OpenStackCloud( cloud_config=self.cloud_config) - self.op_cloud = openstack.cloud.OperatorCloud( - cloud_config=self.cloud_config) def get_glance_discovery_mock_dict( self, diff --git a/openstack/tests/unit/cloud/test_aggregate.py b/openstack/tests/unit/cloud/test_aggregate.py index 6f0e9513d..5da65ef38 100644 --- a/openstack/tests/unit/cloud/test_aggregate.py +++ b/openstack/tests/unit/cloud/test_aggregate.py @@ -37,7 +37,7 @@ def test_create_aggregate(self): 'availability_zone': None, }})), ]) - self.op_cloud.create_aggregate(name=self.aggregate_name) + self.cloud.create_aggregate(name=self.aggregate_name) self.assert_calls() @@ -62,7 +62,7 @@ def test_create_aggregate_with_az(self): }})), ]) - self.op_cloud.create_aggregate( + self.cloud.create_aggregate( name=self.aggregate_name, availability_zone=availability_zone) self.assert_calls() @@ -78,7 +78,7 @@ def test_delete_aggregate(self): 'compute', 'public', append=['os-aggregates', '1'])), ]) - self.assertTrue(self.op_cloud.delete_aggregate('1')) + self.assertTrue(self.cloud.delete_aggregate('1')) self.assert_calls() @@ -99,7 +99,7 @@ def test_update_aggregate_set_az(self): }})), ]) - self.op_cloud.update_aggregate(1, availability_zone='az') + self.cloud.update_aggregate(1, availability_zone='az') self.assert_calls() @@ -120,7 +120,7 @@ def test_update_aggregate_unset_az(self): }})), ]) - self.op_cloud.update_aggregate(1, availability_zone=None) + self.cloud.update_aggregate(1, availability_zone=None) self.assert_calls() @@ -139,7 +139,7 @@ def test_set_aggregate_metadata(self): validate=dict( json={'set_metadata': {'metadata': metadata}})), ]) - self.op_cloud.set_aggregate_metadata('1', metadata) + self.cloud.set_aggregate_metadata('1', metadata) self.assert_calls() @@ -158,7 +158,7 @@ def test_add_host_to_aggregate(self): validate=dict( json={'add_host': {'host': hostname}})), ]) - self.op_cloud.add_host_to_aggregate('1', hostname) + self.cloud.add_host_to_aggregate('1', hostname) self.assert_calls() @@ -177,6 +177,6 @@ def test_remove_host_from_aggregate(self): validate=dict( json={'remove_host': {'host': hostname}})), ]) - self.op_cloud.remove_host_from_aggregate('1', hostname) + self.cloud.remove_host_from_aggregate('1', hostname) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_baremetal_node.py b/openstack/tests/unit/cloud/test_baremetal_node.py index 65bb15474..579c275b0 100644 --- a/openstack/tests/unit/cloud/test_baremetal_node.py +++ b/openstack/tests/unit/cloud/test_baremetal_node.py @@ -50,7 +50,7 @@ def test_list_machines(self): fake_baremetal_two]}), ]) - machines = self.op_cloud.list_machines() + machines = self.cloud.list_machines() self.assertEqual(2, len(machines)) self.assertEqual(self.fake_baremetal_node, machines[0]) self.assert_calls() @@ -64,7 +64,7 @@ def test_get_machine(self): json=self.fake_baremetal_node), ]) - machine = self.op_cloud.get_machine(self.fake_baremetal_node['uuid']) + machine = self.cloud.get_machine(self.fake_baremetal_node['uuid']) self.assertEqual(machine['uuid'], self.fake_baremetal_node['uuid']) self.assert_calls() @@ -87,7 +87,7 @@ def test_get_machine_by_mac(self): json=self.fake_baremetal_node), ]) - machine = self.op_cloud.get_machine_by_mac(mac_address) + machine = self.cloud.get_machine_by_mac(mac_address) self.assertEqual(machine['uuid'], self.fake_baremetal_node['uuid']) self.assert_calls() @@ -117,7 +117,7 @@ def test_validate_node(self): 'validate']), json=validate_return), ]) - self.op_cloud.validate_node(self.fake_baremetal_node['uuid']) + self.cloud.validate_node(self.fake_baremetal_node['uuid']) self.assert_calls() @@ -147,7 +147,7 @@ def test_validate_node(self): # ]) # self.assertRaises( # Exception, - # self.op_cloud.validate_node, + # self.cloud.validate_node, # self.fake_baremetal_node['uuid']) # # self.assert_calls() @@ -165,8 +165,8 @@ def test_patch_machine(self): json=self.fake_baremetal_node, validate=dict(json=test_patch)), ]) - self.op_cloud.patch_machine(self.fake_baremetal_node['uuid'], - test_patch) + self.cloud.patch_machine( + self.fake_baremetal_node['uuid'], test_patch) self.assert_calls() @@ -183,7 +183,7 @@ def test_set_node_instance_info(self): json=self.fake_baremetal_node, validate=dict(json=test_patch)), ]) - self.op_cloud.set_node_instance_info( + self.cloud.set_node_instance_info( self.fake_baremetal_node['uuid'], test_patch) self.assert_calls() @@ -201,7 +201,7 @@ def test_purge_node_instance_info(self): json=self.fake_baremetal_node, validate=dict(json=test_patch)), ]) - self.op_cloud.purge_node_instance_info( + self.cloud.purge_node_instance_info( self.fake_baremetal_node['uuid']) self.assert_calls() @@ -217,7 +217,7 @@ def test_inspect_machine_fail_active(self): ]) self.assertRaises( exc.OpenStackCloudException, - self.op_cloud.inspect_machine, + self.cloud.inspect_machine, self.fake_baremetal_node['uuid'], wait=True, timeout=1) @@ -251,7 +251,7 @@ def test_inspect_machine_failed(self): json=inspecting_node) ]) - self.op_cloud.inspect_machine(self.fake_baremetal_node['uuid']) + self.cloud.inspect_machine(self.fake_baremetal_node['uuid']) self.assert_calls() @@ -280,7 +280,7 @@ def test_inspect_machine_manageable(self): append=[self.fake_baremetal_node['uuid']]), json=inspecting_node), ]) - self.op_cloud.inspect_machine(self.fake_baremetal_node['uuid']) + self.cloud.inspect_machine(self.fake_baremetal_node['uuid']) self.assert_calls() @@ -337,7 +337,7 @@ def test_inspect_machine_available(self): append=[self.fake_baremetal_node['uuid']]), json=available_node), ]) - self.op_cloud.inspect_machine(self.fake_baremetal_node['uuid']) + self.cloud.inspect_machine(self.fake_baremetal_node['uuid']) self.assert_calls() @@ -408,8 +408,8 @@ def test_inspect_machine_available_wait(self): append=[self.fake_baremetal_node['uuid']]), json=available_node), ]) - self.op_cloud.inspect_machine(self.fake_baremetal_node['uuid'], - wait=True, timeout=1) + self.cloud.inspect_machine( + self.fake_baremetal_node['uuid'], wait=True, timeout=1) self.assert_calls() @@ -450,8 +450,8 @@ def test_inspect_machine_wait(self): append=[self.fake_baremetal_node['uuid']]), json=self.fake_baremetal_node), ]) - self.op_cloud.inspect_machine(self.fake_baremetal_node['uuid'], - wait=True, timeout=1) + self.cloud.inspect_machine( + self.fake_baremetal_node['uuid'], wait=True, timeout=1) self.assert_calls() @@ -490,7 +490,7 @@ def test_inspect_machine_inspect_failed(self): json=inspect_fail_node), ]) self.assertRaises(exc.OpenStackCloudException, - self.op_cloud.inspect_machine, + self.cloud.inspect_machine, self.fake_baremetal_node['uuid'], wait=True, timeout=1) @@ -506,7 +506,7 @@ def test_set_machine_maintenace_state(self): 'maintenance']), validate=dict(json={'reason': 'no reason'})), ]) - self.op_cloud.set_machine_maintenance_state( + self.cloud.set_machine_maintenance_state( self.fake_baremetal_node['uuid'], True, reason='no reason') self.assert_calls() @@ -520,7 +520,7 @@ def test_set_machine_maintenace_state_false(self): append=[self.fake_baremetal_node['uuid'], 'maintenance'])), ]) - self.op_cloud.set_machine_maintenance_state( + self.cloud.set_machine_maintenance_state( self.fake_baremetal_node['uuid'], False) self.assert_calls @@ -534,7 +534,7 @@ def test_remove_machine_from_maintenance(self): append=[self.fake_baremetal_node['uuid'], 'maintenance'])), ]) - self.op_cloud.remove_machine_from_maintenance( + self.cloud.remove_machine_from_maintenance( self.fake_baremetal_node['uuid']) self.assert_calls() @@ -549,7 +549,7 @@ def test_set_machine_power_on(self): 'states', 'power']), validate=dict(json={'target': 'power on'})), ]) - return_value = self.op_cloud.set_machine_power_on( + return_value = self.cloud.set_machine_power_on( self.fake_baremetal_node['uuid']) self.assertIsNone(return_value) @@ -565,7 +565,7 @@ def test_set_machine_power_off(self): 'states', 'power']), validate=dict(json={'target': 'power off'})), ]) - return_value = self.op_cloud.set_machine_power_off( + return_value = self.cloud.set_machine_power_off( self.fake_baremetal_node['uuid']) self.assertIsNone(return_value) @@ -581,7 +581,7 @@ def test_set_machine_power_reboot(self): 'states', 'power']), validate=dict(json={'target': 'rebooting'})), ]) - return_value = self.op_cloud.set_machine_power_reboot( + return_value = self.cloud.set_machine_power_reboot( self.fake_baremetal_node['uuid']) self.assertIsNone(return_value) @@ -600,7 +600,7 @@ def test_set_machine_power_reboot_failure(self): validate=dict(json={'target': 'rebooting'})), ]) self.assertRaises(exc.OpenStackCloudException, - self.op_cloud.set_machine_power_reboot, + self.cloud.set_machine_power_reboot, self.fake_baremetal_node['uuid']) self.assert_calls() @@ -625,7 +625,7 @@ def test_node_set_provision_state(self): append=[self.fake_baremetal_node['uuid']]), json=self.fake_baremetal_node), ]) - self.op_cloud.node_set_provision_state( + self.cloud.node_set_provision_state( self.fake_baremetal_node['uuid'], 'active', configdrive='http://host/file') @@ -662,7 +662,7 @@ def test_node_set_provision_state_wait_timeout(self): append=[self.fake_baremetal_node['uuid']]), json=active_node), ]) - return_value = self.op_cloud.node_set_provision_state( + return_value = self.cloud.node_set_provision_state( self.fake_baremetal_node['uuid'], 'active', wait=True) @@ -690,7 +690,7 @@ def test_node_set_provision_state_wait_timeout_fails(self): self.assertRaises( exc.OpenStackCloudException, - self.op_cloud.node_set_provision_state, + self.cloud.node_set_provision_state, self.fake_baremetal_node['uuid'], 'active', wait=True, @@ -715,7 +715,7 @@ def test_node_set_provision_state_wait_success(self): json=self.fake_baremetal_node), ]) - return_value = self.op_cloud.node_set_provision_state( + return_value = self.cloud.node_set_provision_state( self.fake_baremetal_node['uuid'], 'active', wait=True) @@ -742,7 +742,7 @@ def test_node_set_provision_state_wait_failure_cases(self): self.assertRaises( exc.OpenStackCloudException, - self.op_cloud.node_set_provision_state, + self.cloud.node_set_provision_state, self.fake_baremetal_node['uuid'], 'active', wait=True, @@ -773,7 +773,7 @@ def test_node_set_provision_state_wait_provide(self): append=[self.fake_baremetal_node['uuid']]), json=available_node), ]) - return_value = self.op_cloud.node_set_provision_state( + return_value = self.cloud.node_set_provision_state( self.fake_baremetal_node['uuid'], 'provide', wait=True) @@ -798,7 +798,7 @@ def test_wait_for_baremetal_node_lock_locked(self): json=unlocked_node), ]) self.assertIsNone( - self.op_cloud.wait_for_baremetal_node_lock( + self.cloud.wait_for_baremetal_node_lock( self.fake_baremetal_node, timeout=1)) @@ -807,7 +807,7 @@ def test_wait_for_baremetal_node_lock_locked(self): def test_wait_for_baremetal_node_lock_not_locked(self): self.fake_baremetal_node['reservation'] = None self.assertIsNone( - self.op_cloud.wait_for_baremetal_node_lock( + self.cloud.wait_for_baremetal_node_lock( self.fake_baremetal_node, timeout=1)) @@ -824,7 +824,7 @@ def test_wait_for_baremetal_node_lock_timeout(self): ]) self.assertRaises( exc.OpenStackCloudException, - self.op_cloud.wait_for_baremetal_node_lock, + self.cloud.wait_for_baremetal_node_lock, self.fake_baremetal_node, timeout=0.001) @@ -847,7 +847,7 @@ def test_activate_node(self): append=[self.fake_baremetal_node['uuid']]), json=self.fake_baremetal_node), ]) - return_value = self.op_cloud.activate_node( + return_value = self.cloud.activate_node( self.fake_baremetal_node['uuid'], configdrive='http://host/file', wait=True) @@ -871,7 +871,7 @@ def test_deactivate_node(self): append=[self.fake_baremetal_node['uuid']]), json=self.fake_baremetal_node), ]) - return_value = self.op_cloud.deactivate_node( + return_value = self.cloud.deactivate_node( self.fake_baremetal_node['uuid'], wait=True) @@ -910,7 +910,7 @@ def test_register_machine(self): 'node_uuid': node_uuid}), json=self.fake_baremetal_port), ]) - return_value = self.op_cloud.register_machine(nics, **node_to_post) + return_value = self.cloud.register_machine(nics, **node_to_post) self.assertDictEqual(self.fake_baremetal_node, return_value) self.assert_calls() @@ -994,7 +994,7 @@ def test_register_machine_enroll(self): # point during code being refactored soon after the # change landed. Presently, with the lock at 1.6, # this code is never used in the current code path. - return_value = self.op_cloud.register_machine(nics, **node_to_post) + return_value = self.cloud.register_machine(nics, **node_to_post) self.assertDictEqual(available_node, return_value) self.assert_calls() @@ -1074,8 +1074,8 @@ def test_register_machine_enroll_wait(self): append=[self.fake_baremetal_node['uuid']]), json=available_node), ]) - return_value = self.op_cloud.register_machine(nics, wait=True, - **node_to_post) + return_value = self.cloud.register_machine( + nics, wait=True, **node_to_post) self.assertDictEqual(available_node, return_value) self.assert_calls() @@ -1133,7 +1133,7 @@ def test_register_machine_enroll_failure(self): self.assertRaises( exc.OpenStackCloudException, - self.op_cloud.register_machine, + self.cloud.register_machine, nics, **node_to_post) self.assert_calls() @@ -1193,7 +1193,7 @@ def test_register_machine_enroll_timeout(self): # want to block on until it has cleared. self.assertRaises( exc.OpenStackCloudException, - self.op_cloud.register_machine, + self.cloud.register_machine, nics, timeout=0.001, lock_timeout=0.001, @@ -1248,7 +1248,7 @@ def test_register_machine_enroll_timeout_wait(self): ]) self.assertRaises( exc.OpenStackCloudException, - self.op_cloud.register_machine, + self.cloud.register_machine, nics, wait=True, timeout=0.001, @@ -1289,7 +1289,7 @@ def test_register_machine_port_create_failed(self): append=[self.fake_baremetal_node['uuid']])), ]) self.assertRaises(exc.OpenStackCloudException, - self.op_cloud.register_machine, + self.cloud.register_machine, nics, **node_to_post) self.assert_calls() @@ -1329,8 +1329,8 @@ def test_unregister_machine(self): append=[self.fake_baremetal_node['uuid']])), ]) - self.op_cloud.unregister_machine(nics, - self.fake_baremetal_node['uuid']) + self.cloud.unregister_machine( + nics, self.fake_baremetal_node['uuid']) self.assert_calls() @@ -1375,7 +1375,7 @@ def test_unregister_machine_timeout(self): ]) self.assertRaises( exc.OpenStackCloudException, - self.op_cloud.unregister_machine, + self.cloud.unregister_machine, nics, self.fake_baremetal_node['uuid'], wait=True, @@ -1404,7 +1404,7 @@ def test_unregister_machine_locked_timeout(self): ]) self.assertRaises( exc.OpenStackCloudException, - self.op_cloud.unregister_machine, + self.cloud.unregister_machine, nics, self.fake_baremetal_node['uuid'], timeout=0.001) @@ -1432,7 +1432,7 @@ def test_unregister_machine_unavailable(self): for state in invalid_states: self.assertRaises( exc.OpenStackCloudException, - self.op_cloud.unregister_machine, + self.cloud.unregister_machine, nics, self.fake_baremetal_node['uuid']) @@ -1447,7 +1447,7 @@ def test_update_machine_patch_no_action(self): json=self.fake_baremetal_node), ]) # NOTE(TheJulia): This is just testing mechanics. - update_dict = self.op_cloud.update_machine( + update_dict = self.cloud.update_machine( self.fake_baremetal_node['uuid']) self.assertIsNone(update_dict['changes']) self.assertDictEqual(self.fake_baremetal_node, update_dict['node']) @@ -1501,7 +1501,7 @@ def test_update_machine_patch(self): self.register_uris(uris) call_args = {self.field_name: value_to_send} - update_dict = self.op_cloud.update_machine( + update_dict = self.cloud.update_machine( self.fake_baremetal_node['uuid'], **call_args) if not self.changed: diff --git a/openstack/tests/unit/cloud/test_baremetal_ports.py b/openstack/tests/unit/cloud/test_baremetal_ports.py index a52b76ff4..7584b7b96 100644 --- a/openstack/tests/unit/cloud/test_baremetal_ports.py +++ b/openstack/tests/unit/cloud/test_baremetal_ports.py @@ -48,7 +48,7 @@ def test_list_nics(self): self.fake_baremetal_port2]}), ]) - return_value = self.op_cloud.list_nics() + return_value = self.cloud.list_nics() self.assertEqual(2, len(return_value['ports'])) self.assertEqual(self.fake_baremetal_port, return_value['ports'][0]) self.assert_calls() @@ -60,7 +60,7 @@ def test_list_nics_failure(self): status_code=400) ]) self.assertRaises(exc.OpenStackCloudException, - self.op_cloud.list_nics) + self.cloud.list_nics) self.assert_calls() def test_list_nics_for_machine(self): @@ -73,7 +73,7 @@ def test_list_nics_for_machine(self): self.fake_baremetal_port2]}), ]) - return_value = self.op_cloud.list_nics_for_machine( + return_value = self.cloud.list_nics_for_machine( self.fake_baremetal_node['uuid']) self.assertEqual(2, len(return_value['ports'])) self.assertEqual(self.fake_baremetal_port, return_value['ports'][0]) @@ -89,7 +89,7 @@ def test_list_nics_for_machine_failure(self): ]) self.assertRaises(exc.OpenStackCloudException, - self.op_cloud.list_nics_for_machine, + self.cloud.list_nics_for_machine, self.fake_baremetal_node['uuid']) self.assert_calls() @@ -102,7 +102,7 @@ def test_get_nic_by_mac(self): json={'ports': [self.fake_baremetal_port]}), ]) - return_value = self.op_cloud.get_nic_by_mac(mac) + return_value = self.cloud.get_nic_by_mac(mac) self.assertEqual(self.fake_baremetal_port, return_value) self.assert_calls() @@ -116,6 +116,6 @@ def test_get_nic_by_mac_failure(self): json={'ports': []}), ]) - self.assertIsNone(self.op_cloud.get_nic_by_mac(mac)) + self.assertIsNone(self.cloud.get_nic_by_mac(mac)) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_domains.py b/openstack/tests/unit/cloud/test_domains.py index 2bb69f8c6..dcbf69e3e 100644 --- a/openstack/tests/unit/cloud/test_domains.py +++ b/openstack/tests/unit/cloud/test_domains.py @@ -36,7 +36,7 @@ def test_list_domains(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url(), status_code=200, json={'domains': [domain_data.json_response['domain']]})]) - domains = self.op_cloud.list_domains() + domains = self.cloud.list_domains() self.assertThat(len(domains), matchers.Equals(1)) self.assertThat(domains[0].name, matchers.Equals(domain_data.domain_name)) @@ -51,7 +51,7 @@ def test_get_domain(self): uri=self.get_mock_url(append=[domain_data.domain_id]), status_code=200, json=domain_data.json_response)]) - domain = self.op_cloud.get_domain(domain_id=domain_data.domain_id) + domain = self.cloud.get_domain(domain_id=domain_data.domain_id) self.assertThat(domain.id, matchers.Equals(domain_data.domain_id)) self.assertThat(domain.name, matchers.Equals(domain_data.domain_name)) self.assert_calls() @@ -64,8 +64,8 @@ def test_get_domain_with_name_or_id(self): json=response), dict(method='GET', uri=self.get_mock_url(), status_code=200, json=response)]) - domain = self.op_cloud.get_domain(name_or_id=domain_data.domain_id) - domain_by_name = self.op_cloud.get_domain( + domain = self.cloud.get_domain(name_or_id=domain_data.domain_id) + domain_by_name = self.cloud.get_domain( name_or_id=domain_data.domain_name) self.assertThat(domain.id, matchers.Equals(domain_data.domain_id)) self.assertThat(domain.name, matchers.Equals(domain_data.domain_name)) @@ -82,7 +82,7 @@ def test_create_domain(self): dict(method='POST', uri=self.get_mock_url(), status_code=200, json=domain_data.json_response, validate=dict(json=domain_data.json_request))]) - domain = self.op_cloud.create_domain( + domain = self.cloud.create_domain( domain_data.domain_name, domain_data.description) self.assertThat(domain.id, matchers.Equals(domain_data.domain_id)) self.assertThat(domain.name, matchers.Equals(domain_data.domain_name)) @@ -101,7 +101,7 @@ def test_create_domain_exception(self): dict(method='POST', uri=self.get_mock_url(), status_code=400, json=domain_data.json_response, validate=dict(json=domain_data.json_request))]) - self.op_cloud.create_domain('domain_name') + self.cloud.create_domain('domain_name') self.assert_calls() def test_delete_domain(self): @@ -114,7 +114,7 @@ def test_delete_domain(self): json=new_resp, validate=dict(json={'domain': {'enabled': False}})), dict(method='DELETE', uri=domain_resource_uri, status_code=204)]) - self.op_cloud.delete_domain(domain_data.domain_id) + self.cloud.delete_domain(domain_data.domain_id) self.assert_calls() def test_delete_domain_name_or_id(self): @@ -130,7 +130,7 @@ def test_delete_domain_name_or_id(self): json=new_resp, validate=dict(json={'domain': {'enabled': False}})), dict(method='DELETE', uri=domain_resource_uri, status_code=204)]) - self.op_cloud.delete_domain(name_or_id=domain_data.domain_id) + self.cloud.delete_domain(name_or_id=domain_data.domain_id) self.assert_calls() def test_delete_domain_exception(self): @@ -152,7 +152,7 @@ def test_delete_domain_exception(self): openstack.cloud.OpenStackCloudURINotFound, "Failed to delete domain %s" % domain_data.domain_id ): - self.op_cloud.delete_domain(domain_data.domain_id) + self.cloud.delete_domain(domain_data.domain_id) self.assert_calls() def test_update_domain(self): @@ -163,7 +163,7 @@ def test_update_domain(self): dict(method='PATCH', uri=domain_resource_uri, status_code=200, json=domain_data.json_response, validate=dict(json=domain_data.json_request))]) - domain = self.op_cloud.update_domain( + domain = self.cloud.update_domain( domain_data.domain_id, name=domain_data.domain_name, description=domain_data.description) @@ -183,7 +183,7 @@ def test_update_domain_name_or_id(self): dict(method='PATCH', uri=domain_resource_uri, status_code=200, json=domain_data.json_response, validate=dict(json=domain_data.json_request))]) - domain = self.op_cloud.update_domain( + domain = self.cloud.update_domain( name_or_id=domain_data.domain_id, name=domain_data.domain_name, description=domain_data.description) @@ -206,5 +206,5 @@ def test_update_domain_exception(self): openstack.cloud.OpenStackCloudHTTPError, "Error in updating domain %s" % domain_data.domain_id ): - self.op_cloud.delete_domain(domain_data.domain_id) + self.cloud.delete_domain(domain_data.domain_id) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_endpoints.py b/openstack/tests/unit/cloud/test_endpoints.py index e0df712a9..58d9fe709 100644 --- a/openstack/tests/unit/cloud/test_endpoints.py +++ b/openstack/tests/unit/cloud/test_endpoints.py @@ -82,7 +82,7 @@ def test_create_endpoint_v2(self): validate=dict(json=other_endpoint_data.json_request)) ]) - endpoints = self.op_cloud.create_endpoint( + endpoints = self.cloud.create_endpoint( service_name_or_id=service_data.service_id, region=endpoint_data.region, public_url=endpoint_data.public_url, @@ -103,12 +103,12 @@ def test_create_endpoint_v2(self): # test v3 semantics on v2.0 endpoint self.assertRaises(OpenStackCloudException, - self.op_cloud.create_endpoint, + self.cloud.create_endpoint, service_name_or_id='service1', interface='mock_admin_url', url='admin') - endpoints_3on2 = self.op_cloud.create_endpoint( + endpoints_3on2 = self.cloud.create_endpoint( service_name_or_id=service_data.service_id, region=endpoint_data.region, interface='public', @@ -177,7 +177,7 @@ def test_create_endpoint_v3(self): validate=dict(json=admin_endpoint_data.json_request)), ]) - endpoints = self.op_cloud.create_endpoint( + endpoints = self.cloud.create_endpoint( service_name_or_id=service_data.service_id, region=public_endpoint_data_disabled.region, url=public_endpoint_data_disabled.url, @@ -202,7 +202,7 @@ def test_create_endpoint_v3(self): self.assertThat(endpoints[0].enabled, matchers.Equals(public_endpoint_data_disabled.enabled)) - endpoints_2on3 = self.op_cloud.create_endpoint( + endpoints_2on3 = self.cloud.create_endpoint( service_name_or_id=service_data.service_id, region=public_endpoint_data.region, public_url=public_endpoint_data.url, @@ -230,7 +230,7 @@ def test_create_endpoint_v3(self): def test_update_endpoint_v2(self): self.use_keystone_v2() self.assertRaises(OpenStackCloudUnavailableFeature, - self.op_cloud.update_endpoint, 'endpoint_id') + self.cloud.update_endpoint, 'endpoint_id') def test_update_endpoint_v3(self): service_data = self._get_service_data() @@ -247,7 +247,7 @@ def test_update_endpoint_v3(self): json=endpoint_data.json_response, validate=dict(json=reference_request)) ]) - endpoint = self.op_cloud.update_endpoint( + endpoint = self.cloud.update_endpoint( endpoint_data.endpoint_id, service_name_or_id=service_data.service_id, region=endpoint_data.region, @@ -278,7 +278,7 @@ def test_list_endpoints(self): for e in endpoints_data]}) ]) - endpoints = self.op_cloud.list_endpoints() + endpoints = self.cloud.list_endpoints() # test we are getting exactly len(self.mock_endpoints) elements self.assertThat(len(endpoints), matchers.Equals(len(endpoints_data))) @@ -324,7 +324,7 @@ def test_search_endpoints(self): ]) # Search by id - endpoints = self.op_cloud.search_endpoints( + endpoints = self.cloud.search_endpoints( id=endpoints_data[-1].endpoint_id) # # test we are getting exactly 1 element self.assertEqual(1, len(endpoints)) @@ -338,17 +338,17 @@ def test_search_endpoints(self): matchers.Equals(endpoints_data[-1].interface)) # Not found - endpoints = self.op_cloud.search_endpoints(id='!invalid!') + endpoints = self.cloud.search_endpoints(id='!invalid!') self.assertEqual(0, len(endpoints)) # Multiple matches - endpoints = self.op_cloud.search_endpoints( + endpoints = self.cloud.search_endpoints( filters={'region_id': 'region1'}) # # test we are getting exactly 2 elements self.assertEqual(2, len(endpoints)) # test we are getting the correct response for region/region_id compat - endpoints = self.op_cloud.search_endpoints( + endpoints = self.cloud.search_endpoints( filters={'region': 'region1'}) # # test we are getting exactly 2 elements, this is v3 self.assertEqual(2, len(endpoints)) @@ -369,5 +369,5 @@ def test_delete_endpoint(self): ]) # Delete by id - self.op_cloud.delete_endpoint(id=endpoint_data.endpoint_id) + self.cloud.delete_endpoint(id=endpoint_data.endpoint_id) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_flavors.py b/openstack/tests/unit/cloud/test_flavors.py index 094068887..e23e67b1f 100644 --- a/openstack/tests/unit/cloud/test_flavors.py +++ b/openstack/tests/unit/cloud/test_flavors.py @@ -38,7 +38,7 @@ def test_create_flavor(self): "disk": 1600, "id": None}}))]) - self.op_cloud.create_flavor( + self.cloud.create_flavor( 'vanilla', ram=65536, disk=1600, vcpus=24, ) self.assert_calls() @@ -52,7 +52,7 @@ def test_delete_flavor(self): dict(method='DELETE', uri='{endpoint}/flavors/{id}'.format( endpoint=fakes.COMPUTE_ENDPOINT, id=fakes.FLAVOR_ID))]) - self.assertTrue(self.op_cloud.delete_flavor('vanilla')) + self.assertTrue(self.cloud.delete_flavor('vanilla')) self.assert_calls() @@ -63,7 +63,7 @@ def test_delete_flavor_not_found(self): endpoint=fakes.COMPUTE_ENDPOINT), json={'flavors': fakes.FAKE_FLAVOR_LIST})]) - self.assertFalse(self.op_cloud.delete_flavor('invalid')) + self.assertFalse(self.cloud.delete_flavor('invalid')) self.assert_calls() @@ -79,7 +79,7 @@ def test_delete_flavor_exception(self): status_code=503)]) self.assertRaises(openstack.cloud.OpenStackCloudException, - self.op_cloud.delete_flavor, 'vanilla') + self.cloud.delete_flavor, 'vanilla') def test_list_flavors(self): uris_to_mock = [ @@ -185,7 +185,7 @@ def test_set_flavor_specs(self): endpoint=fakes.COMPUTE_ENDPOINT, id=1), json=dict(extra_specs=extra_specs))]) - self.op_cloud.set_flavor_specs(1, extra_specs) + self.cloud.set_flavor_specs(1, extra_specs) self.assert_calls() def test_unset_flavor_specs(self): @@ -196,7 +196,7 @@ def test_unset_flavor_specs(self): endpoint=fakes.COMPUTE_ENDPOINT, id=1, key=key)) for key in keys]) - self.op_cloud.unset_flavor_specs(1, keys) + self.cloud.unset_flavor_specs(1, keys) self.assert_calls() def test_add_flavor_access(self): @@ -210,7 +210,7 @@ def test_add_flavor_access(self): validate=dict( json={'addTenantAccess': {'tenant': 'tenant_id'}}))]) - self.op_cloud.add_flavor_access('flavor_id', 'tenant_id') + self.cloud.add_flavor_access('flavor_id', 'tenant_id') self.assert_calls() def test_remove_flavor_access(self): @@ -222,7 +222,7 @@ def test_remove_flavor_access(self): validate=dict( json={'removeTenantAccess': {'tenant': 'tenant_id'}}))]) - self.op_cloud.remove_flavor_access('flavor_id', 'tenant_id') + self.cloud.remove_flavor_access('flavor_id', 'tenant_id') self.assert_calls() def test_list_flavor_access(self): @@ -234,7 +234,7 @@ def test_list_flavor_access(self): 'flavor_access': [ {'flavor_id': 'vanilla', 'tenant_id': 'tenant_id'}]}) ]) - self.op_cloud.list_flavor_access('vanilla') + self.cloud.list_flavor_access('vanilla') self.assert_calls() def test_get_flavor_by_id(self): diff --git a/openstack/tests/unit/cloud/test_groups.py b/openstack/tests/unit/cloud/test_groups.py index afa6229aa..c498336b1 100644 --- a/openstack/tests/unit/cloud/test_groups.py +++ b/openstack/tests/unit/cloud/test_groups.py @@ -34,7 +34,7 @@ def test_list_groups(self): status_code=200, json={'groups': [group_data.json_response['group']]}) ]) - self.op_cloud.list_groups() + self.cloud.list_groups() def test_get_group(self): group_data = self._get_group_data() @@ -44,7 +44,7 @@ def test_get_group(self): status_code=200, json={'groups': [group_data.json_response['group']]}), ]) - self.op_cloud.get_group(group_data.group_id) + self.cloud.get_group(group_data.group_id) def test_delete_group(self): group_data = self._get_group_data() @@ -57,7 +57,7 @@ def test_delete_group(self): uri=self.get_mock_url(append=[group_data.group_id]), status_code=204), ]) - self.assertTrue(self.op_cloud.delete_group(group_data.group_id)) + self.assertTrue(self.cloud.delete_group(group_data.group_id)) def test_create_group(self): domain_data = self._get_domain_data() @@ -74,7 +74,7 @@ def test_create_group(self): json=group_data.json_response, validate=dict(json=group_data.json_request)) ]) - self.op_cloud.create_group( + self.cloud.create_group( name=group_data.group_name, description=group_data.description, domain=group_data.domain_id) @@ -93,5 +93,5 @@ def test_update_group(self): json=group_data.json_response, validate=dict(json=group_data.json_request)) ]) - self.op_cloud.update_group(group_data.group_id, group_data.group_name, - group_data.description) + self.cloud.update_group( + group_data.group_id, group_data.group_name, group_data.description) diff --git a/openstack/tests/unit/cloud/test_identity_roles.py b/openstack/tests/unit/cloud/test_identity_roles.py index 51a4a8c2a..211a3488a 100644 --- a/openstack/tests/unit/cloud/test_identity_roles.py +++ b/openstack/tests/unit/cloud/test_identity_roles.py @@ -51,7 +51,7 @@ def test_list_roles(self): status_code=200, json={'roles': [role_data.json_response['role']]}) ]) - self.op_cloud.list_roles() + self.cloud.list_roles() self.assert_calls() def test_get_role_by_name(self): @@ -62,7 +62,7 @@ def test_get_role_by_name(self): status_code=200, json={'roles': [role_data.json_response['role']]}) ]) - role = self.op_cloud.get_role(role_data.role_name) + role = self.cloud.get_role(role_data.role_name) self.assertIsNotNone(role) self.assertThat(role.id, matchers.Equals(role_data.role_id)) @@ -77,7 +77,7 @@ def test_get_role_by_id(self): status_code=200, json={'roles': [role_data.json_response['role']]}) ]) - role = self.op_cloud.get_role(role_data.role_id) + role = self.cloud.get_role(role_data.role_id) self.assertIsNotNone(role) self.assertThat(role.id, matchers.Equals(role_data.role_id)) @@ -94,7 +94,7 @@ def test_create_role(self): validate=dict(json=role_data.json_request)) ]) - role = self.op_cloud.create_role(role_data.role_name) + role = self.cloud.create_role(role_data.role_name) self.assertIsNotNone(role) self.assertThat(role.name, matchers.Equals(role_data.role_name)) @@ -117,8 +117,8 @@ def test_update_role(self): validate=dict(json=req)) ]) - role = self.op_cloud.update_role(role_data.role_id, - role_data.role_name) + role = self.cloud.update_role( + role_data.role_id, role_data.role_name) self.assertIsNotNone(role) self.assertThat(role.name, matchers.Equals(role_data.role_name)) @@ -136,7 +136,7 @@ def test_delete_role_by_id(self): uri=self.get_mock_url(append=[role_data.role_id]), status_code=204) ]) - role = self.op_cloud.delete_role(role_data.role_id) + role = self.cloud.delete_role(role_data.role_id) self.assertThat(role, matchers.Equals(True)) self.assert_calls() @@ -151,7 +151,7 @@ def test_delete_role_by_name(self): uri=self.get_mock_url(append=[role_data.role_id]), status_code=204) ]) - role = self.op_cloud.delete_role(role_data.role_name) + role = self.cloud.delete_role(role_data.role_name) self.assertThat(role, matchers.Equals(True)) self.assert_calls() @@ -179,7 +179,7 @@ def test_list_role_assignments(self): json={'role_assignments': response}, complete_qs=True) ]) - ret = self.op_cloud.list_role_assignments() + ret = self.cloud.list_role_assignments() self.assertThat(len(ret), matchers.Equals(2)) self.assertThat(ret[0].user, matchers.Equals(user_data.user_id)) self.assertThat(ret[0].id, matchers.Equals(role_data.role_id)) @@ -213,7 +213,7 @@ def test_list_role_assignments_filters(self): ]) params = dict(user=user_data.user_id, domain=domain_data.domain_id, effective=True) - ret = self.op_cloud.list_role_assignments(filters=params) + ret = self.cloud.list_role_assignments(filters=params) self.assertThat(len(ret), matchers.Equals(1)) self.assertThat(ret[0].user, matchers.Equals(user_data.user_id)) self.assertThat(ret[0].id, matchers.Equals(role_data.role_id)) @@ -229,7 +229,7 @@ def test_list_role_assignments_exception(self): openstack.cloud.exc.OpenStackCloudHTTPError, "Failed to list role assignments" ): - self.op_cloud.list_role_assignments() + self.cloud.list_role_assignments() self.assert_calls() def test_list_role_assignments_keystone_v2(self): @@ -249,7 +249,7 @@ def test_list_role_assignments_keystone_v2(self): status_code=200, json={'roles': [role_data.json_response['role']]}) ]) - ret = self.op_cloud.list_role_assignments( + ret = self.cloud.list_role_assignments( filters={ 'user': user_data.user_id, 'project': project_data.project_id}) @@ -277,7 +277,7 @@ def test_list_role_assignments_keystone_v2_with_role(self): status_code=200, json={'roles': [r.json_response['role'] for r in roles_data]}) ]) - ret = self.op_cloud.list_role_assignments( + ret = self.cloud.list_role_assignments( filters={ 'role': roles_data[0].role_id, 'user': user_data.user_id, @@ -295,7 +295,7 @@ def test_list_role_assignments_exception_v2(self): openstack.cloud.OpenStackCloudException, "Must provide project and user for keystone v2" ): - self.op_cloud.list_role_assignments() + self.cloud.list_role_assignments() self.assert_calls() def test_list_role_assignments_exception_v2_no_project(self): @@ -304,5 +304,5 @@ def test_list_role_assignments_exception_v2_no_project(self): openstack.cloud.OpenStackCloudException, "Must provide project and user for keystone v2" ): - self.op_cloud.list_role_assignments(filters={'user': '12345'}) + self.cloud.list_role_assignments(filters={'user': '12345'}) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 1511d9b9a..9bc08b346 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -181,11 +181,11 @@ def test_get_image_id(self, cloud=None): def test_get_image_name_operator(self): # This should work the same as non-operator, just verifying it does. - self.test_get_image_name(cloud=self.op_cloud) + self.test_get_image_name(cloud=self.cloud) def test_get_image_id_operator(self): # This should work the same as the other test, just verifying it does. - self.test_get_image_id(cloud=self.op_cloud) + self.test_get_image_id(cloud=self.cloud) def test_empty_list_images(self): self.register_uris([ diff --git a/openstack/tests/unit/cloud/test_limits.py b/openstack/tests/unit/cloud/test_limits.py index 8a33a93d6..537c8877a 100644 --- a/openstack/tests/unit/cloud/test_limits.py +++ b/openstack/tests/unit/cloud/test_limits.py @@ -90,6 +90,6 @@ def test_other_get_compute_limits(self): }), ]) - self.op_cloud.get_compute_limits(project.project_id) + self.cloud.get_compute_limits(project.project_id) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_magnum_services.py b/openstack/tests/unit/cloud/test_magnum_services.py index 914f1b5f4..c30f56ccd 100644 --- a/openstack/tests/unit/cloud/test_magnum_services.py +++ b/openstack/tests/unit/cloud/test_magnum_services.py @@ -33,7 +33,7 @@ def test_list_magnum_services(self): method='GET', uri='https://container-infra.example.com/v1/mservices', json=dict(mservices=[magnum_service_obj]))]) - mservices_list = self.op_cloud.list_magnum_services() + mservices_list = self.cloud.list_magnum_services() self.assertEqual( mservices_list[0], self.cloud._normalize_magnum_service(magnum_service_obj)) diff --git a/openstack/tests/unit/cloud/test_operator.py b/openstack/tests/unit/cloud/test_operator.py index fb5702e6e..90d72cd26 100644 --- a/openstack/tests/unit/cloud/test_operator.py +++ b/openstack/tests/unit/cloud/test_operator.py @@ -13,7 +13,6 @@ import mock import testtools -import openstack.cloud from openstack.cloud import exc from openstack.config import cloud_region from openstack.tests import fakes @@ -22,14 +21,11 @@ class TestOperatorCloud(base.RequestsMockTestCase): - def test_operator_cloud(self): - self.assertIsInstance(self.op_cloud, openstack.cloud.OperatorCloud) - @mock.patch.object(cloud_region.CloudRegion, 'get_endpoint') def test_get_session_endpoint_provided(self, fake_get_endpoint): fake_get_endpoint.return_value = 'http://fake.url' self.assertEqual( - 'http://fake.url', self.op_cloud.get_session_endpoint('image')) + 'http://fake.url', self.cloud.get_session_endpoint('image')) @mock.patch.object(cloud_region.CloudRegion, 'get_session') def test_get_session_endpoint_session(self, get_session_mock): @@ -37,7 +33,7 @@ def test_get_session_endpoint_session(self, get_session_mock): session_mock.get_endpoint.return_value = 'http://fake.url' get_session_mock.return_value = session_mock self.assertEqual( - 'http://fake.url', self.op_cloud.get_session_endpoint('image')) + 'http://fake.url', self.cloud.get_session_endpoint('image')) @mock.patch.object(cloud_region.CloudRegion, 'get_session') def test_get_session_endpoint_exception(self, get_session_mock): @@ -49,27 +45,27 @@ def side_effect(*args, **kwargs): session_mock = mock.Mock() session_mock.get_endpoint.side_effect = side_effect get_session_mock.return_value = session_mock - self.op_cloud.name = 'testcloud' - self.op_cloud.region_name = 'testregion' + self.cloud.name = 'testcloud' + self.cloud.region_name = 'testregion' with testtools.ExpectedException( exc.OpenStackCloudException, "Error getting image endpoint on testcloud:testregion:" " No service"): - self.op_cloud.get_session_endpoint("image") + self.cloud.get_session_endpoint("image") @mock.patch.object(cloud_region.CloudRegion, 'get_session') def test_get_session_endpoint_unavailable(self, get_session_mock): session_mock = mock.Mock() session_mock.get_endpoint.return_value = None get_session_mock.return_value = session_mock - image_endpoint = self.op_cloud.get_session_endpoint("image") + image_endpoint = self.cloud.get_session_endpoint("image") self.assertIsNone(image_endpoint) @mock.patch.object(cloud_region.CloudRegion, 'get_session') def test_get_session_endpoint_identity(self, get_session_mock): session_mock = mock.Mock() get_session_mock.return_value = session_mock - self.op_cloud.get_session_endpoint('identity') + self.cloud.get_session_endpoint('identity') kwargs = dict( interface='public', region_name='RegionOne', service_name=None, service_type='identity') @@ -81,14 +77,14 @@ def test_has_service_no(self, get_session_mock): session_mock = mock.Mock() session_mock.get_endpoint.return_value = None get_session_mock.return_value = session_mock - self.assertFalse(self.op_cloud.has_service("image")) + self.assertFalse(self.cloud.has_service("image")) @mock.patch.object(cloud_region.CloudRegion, 'get_session') def test_has_service_yes(self, get_session_mock): session_mock = mock.Mock() session_mock.get_endpoint.return_value = 'http://fake.url' get_session_mock.return_value = session_mock - self.assertTrue(self.op_cloud.has_service("image")) + self.assertTrue(self.cloud.has_service("image")) def test_list_hypervisors(self): '''This test verifies that calling list_hypervisors results in a call @@ -103,7 +99,7 @@ def test_list_hypervisors(self): ]}), ]) - r = self.op_cloud.list_hypervisors() + r = self.cloud.list_hypervisors() self.assertEqual(2, len(r)) self.assertEqual('testserver1', r[0]['hypervisor_hostname']) diff --git a/openstack/tests/unit/cloud/test_operator_noauth.py b/openstack/tests/unit/cloud/test_operator_noauth.py index 14fdd9dad..5b7dd3072 100644 --- a/openstack/tests/unit/cloud/test_operator_noauth.py +++ b/openstack/tests/unit/cloud/test_operator_noauth.py @@ -18,7 +18,7 @@ class TestOpenStackCloudOperatorNoAuth(base.RequestsMockTestCase): def setUp(self): - """Setup Noauth OperatorCloud tests + """Setup Noauth OpenStackCloud tests Setup the test to utilize no authentication and an endpoint URL in the auth data. This is permits testing of the basic @@ -41,7 +41,7 @@ def setUp(self): ]) def test_ironic_noauth_none_auth_type(self): - """Test noauth selection for Ironic in OperatorCloud + """Test noauth selection for Ironic in OpenStackCloud The new way of doing this is with the keystoneauth none plugin. """ @@ -50,7 +50,7 @@ def test_ironic_noauth_none_auth_type(self): # with 'v1'. As such, since we are overriding the endpoint, # we must explicitly do the same as we move away from the # client library. - self.cloud_noauth = openstack.cloud.operator_cloud( + self.cloud_noauth = openstack.cloud.openstack_cloud( auth_type='none', baremetal_endpoint_override="https://bare-metal.example.com/v1") @@ -59,11 +59,11 @@ def test_ironic_noauth_none_auth_type(self): self.assert_calls() def test_ironic_noauth_admin_token_auth_type(self): - """Test noauth selection for Ironic in OperatorCloud + """Test noauth selection for Ironic in OpenStackCloud The old way of doing this was to abuse admin_token. """ - self.cloud_noauth = openstack.cloud.operator_cloud( + self.cloud_noauth = openstack.cloud.openstack_cloud( auth_type='admin_token', auth=dict( endpoint='https://bare-metal.example.com/v1', diff --git a/openstack/tests/unit/cloud/test_project.py b/openstack/tests/unit/cloud/test_project.py index 8001260c4..a94b42322 100644 --- a/openstack/tests/unit/cloud/test_project.py +++ b/openstack/tests/unit/cloud/test_project.py @@ -41,7 +41,7 @@ def test_create_project_v2(self): status_code=200, json=project_data.json_response, validate=dict(json=project_data.json_request)) ]) - project = self.op_cloud.create_project( + project = self.cloud.create_project( name=project_data.project_name, description=project_data.description) self.assertThat(project.id, matchers.Equals(project_data.project_id)) @@ -61,7 +61,7 @@ def test_create_project_v3(self,): json=project_data.json_response, validate=dict(json=reference_req)) ]) - project = self.op_cloud.create_project( + project = self.cloud.create_project( name=project_data.project_name, description=project_data.description, domain_id=project_data.domain_id) @@ -80,7 +80,7 @@ def test_create_project_v3_no_domain(self): "User or project creation requires an explicit" " domain_id argument." ): - self.op_cloud.create_project(name='foo', description='bar') + self.cloud.create_project(name='foo', description='bar') def test_delete_project_v2(self): self.use_keystone_v2() @@ -95,7 +95,7 @@ def test_delete_project_v2(self): v3=False, append=[project_data.project_id]), status_code=204) ]) - self.op_cloud.delete_project(project_data.project_id) + self.cloud.delete_project(project_data.project_id) self.assert_calls() def test_delete_project_v3(self): @@ -109,7 +109,7 @@ def test_delete_project_v3(self): uri=self.get_mock_url(append=[project_data.project_id]), status_code=204) ]) - self.op_cloud.delete_project(project_data.project_id) + self.cloud.delete_project(project_data.project_id) self.assert_calls() def test_update_project_not_found(self): @@ -129,7 +129,7 @@ def test_update_project_not_found(self): openstack.cloud.OpenStackCloudException, "Project %s not found." % project_data.project_id ): - self.op_cloud.update_project(project_data.project_id) + self.cloud.update_project(project_data.project_id) self.assert_calls() def test_update_project_v2(self): @@ -152,7 +152,7 @@ def test_update_project_v2(self): json=project_data.json_response, validate=dict(json=project_data.json_request)) ]) - project = self.op_cloud.update_project( + project = self.cloud.update_project( project_data.project_id, description=project_data.description) self.assertThat(project.id, matchers.Equals(project_data.project_id)) @@ -182,7 +182,7 @@ def test_update_project_v3(self): status_code=200, json=project_data.json_response, validate=dict(json=reference_req)) ]) - project = self.op_cloud.update_project( + project = self.cloud.update_project( project_data.project_id, description=project_data.description, domain_id=project_data.domain_id) @@ -204,7 +204,7 @@ def test_list_projects_v3(self): status_code=200, json={'projects': [project_data.json_response['project']]}) ]) - projects = self.op_cloud.list_projects(project_data.domain_id) + projects = self.cloud.list_projects(project_data.domain_id) self.assertThat(len(projects), matchers.Equals(1)) self.assertThat( projects[0].id, matchers.Equals(project_data.project_id)) @@ -221,7 +221,7 @@ def test_list_projects_v3_kwarg(self): status_code=200, json={'projects': [project_data.json_response['project']]}) ]) - projects = self.op_cloud.list_projects( + projects = self.cloud.list_projects( domain_id=project_data.domain_id) self.assertThat(len(projects), matchers.Equals(1)) self.assertThat( @@ -237,7 +237,7 @@ def test_list_projects_search_compat(self): status_code=200, json={'projects': [project_data.json_response['project']]}) ]) - projects = self.op_cloud.search_projects(project_data.project_id) + projects = self.cloud.search_projects(project_data.project_id) self.assertThat(len(projects), matchers.Equals(1)) self.assertThat( projects[0].id, matchers.Equals(project_data.project_id)) @@ -254,7 +254,7 @@ def test_list_projects_search_compat_v3(self): status_code=200, json={'projects': [project_data.json_response['project']]}) ]) - projects = self.op_cloud.search_projects( + projects = self.cloud.search_projects( domain_id=project_data.domain_id) self.assertThat(len(projects), matchers.Equals(1)) self.assertThat( diff --git a/openstack/tests/unit/cloud/test_quotas.py b/openstack/tests/unit/cloud/test_quotas.py index 0494372fe..91870b523 100644 --- a/openstack/tests/unit/cloud/test_quotas.py +++ b/openstack/tests/unit/cloud/test_quotas.py @@ -54,7 +54,7 @@ def test_update_quotas(self): }})), ]) - self.op_cloud.set_compute_quotas(project.project_id, cores=1) + self.cloud.set_compute_quotas(project.project_id, cores=1) self.assert_calls() @@ -71,7 +71,7 @@ def test_update_quotas_bad_request(self): ]) self.assertRaises(exc.OpenStackCloudException, - self.op_cloud.set_compute_quotas, project.project_id) + self.cloud.set_compute_quotas, project.project_id) self.assert_calls() @@ -86,7 +86,7 @@ def test_get_quotas(self): json={'quota_set': fake_quota_set}), ]) - self.op_cloud.get_compute_quotas(project.project_id) + self.cloud.get_compute_quotas(project.project_id) self.assert_calls() @@ -101,7 +101,7 @@ def test_delete_quotas(self): append=['os-quota-sets', project.project_id])), ]) - self.op_cloud.delete_compute_quotas(project.project_id) + self.cloud.delete_compute_quotas(project.project_id) self.assert_calls() @@ -118,7 +118,7 @@ def test_cinder_update_quotas(self): json={'quota_set': { 'volumes': 1, 'tenant_id': project.project_id}}))]) - self.op_cloud.set_volume_quotas(project.project_id, volumes=1) + self.cloud.set_volume_quotas(project.project_id, volumes=1) self.assert_calls() def test_cinder_get_quotas(self): @@ -130,7 +130,7 @@ def test_cinder_get_quotas(self): 'volumev2', 'public', append=['os-quota-sets', project.project_id]), json=dict(quota_set={'snapshots': 10, 'volumes': 20}))]) - self.op_cloud.get_volume_quotas(project.project_id) + self.cloud.get_volume_quotas(project.project_id) self.assert_calls() def test_cinder_delete_quotas(self): @@ -141,7 +141,7 @@ def test_cinder_delete_quotas(self): uri=self.get_mock_url( 'volumev2', 'public', append=['os-quota-sets', project.project_id]))]) - self.op_cloud.delete_volume_quotas(project.project_id) + self.cloud.delete_volume_quotas(project.project_id) self.assert_calls() def test_neutron_update_quotas(self): @@ -157,7 +157,7 @@ def test_neutron_update_quotas(self): validate=dict( json={'quota': {'network': 1}})) ]) - self.op_cloud.set_network_quotas(project.project_id, network=1) + self.cloud.set_network_quotas(project.project_id, network=1) self.assert_calls() def test_neutron_get_quotas(self): @@ -182,7 +182,7 @@ def test_neutron_get_quotas(self): '%s.json' % project.project_id]), json={'quota': quota}) ]) - received_quota = self.op_cloud.get_network_quotas(project.project_id) + received_quota = self.cloud.get_network_quotas(project.project_id) self.assertDictEqual(quota, received_quota) self.assert_calls() @@ -235,7 +235,7 @@ def test_neutron_get_quotas_details(self): '%s/details.json' % project.project_id]), json={'quota': quota_details}) ]) - received_quota_details = self.op_cloud.get_network_quotas( + received_quota_details = self.cloud.get_network_quotas( project.project_id, details=True) self.assertDictEqual(quota_details, received_quota_details) self.assert_calls() @@ -251,5 +251,5 @@ def test_neutron_delete_quotas(self): '%s.json' % project.project_id]), json={}) ]) - self.op_cloud.delete_network_quotas(project.project_id) + self.cloud.delete_network_quotas(project.project_id) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_role_assignment.py b/openstack/tests/unit/cloud/test_role_assignment.py index dd7fa5bfb..c6a1b73ed 100644 --- a/openstack/tests/unit/cloud/test_role_assignment.py +++ b/openstack/tests/unit/cloud/test_role_assignment.py @@ -160,12 +160,12 @@ def test_grant_role_user_v2(self): status_code=201) ]) self.assertTrue( - self.op_cloud.grant_role( + self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, project=self.project_data.project_id)) self.assertTrue( - self.op_cloud.grant_role( + self.cloud.grant_role( self.role_data.role_name, user=self.user_data.user_id, project=self.project_data.project_id)) @@ -305,22 +305,22 @@ def test_grant_role_user_project_v2(self): status_code=201) ]) self.assertTrue( - self.op_cloud.grant_role( + self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, project=self.project_data_v2.project_id)) self.assertTrue( - self.op_cloud.grant_role( + self.cloud.grant_role( self.role_data.role_name, user=self.user_data.user_id, project=self.project_data_v2.project_id)) self.assertTrue( - self.op_cloud.grant_role( + self.cloud.grant_role( self.role_data.role_id, user=self.user_data.name, project=self.project_data_v2.project_id)) self.assertTrue( - self.op_cloud.grant_role( + self.cloud.grant_role( self.role_data.role_id, user=self.user_data.user_id, project=self.project_data_v2.project_id)) @@ -354,7 +354,7 @@ def test_grant_role_user_project_v2_exists(self): status_code=200, json={'roles': [self.role_data.json_response['role']]}), ]) - self.assertFalse(self.op_cloud.grant_role( + self.assertFalse(self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, project=self.project_data_v2.project_id)) @@ -424,12 +424,12 @@ def test_grant_role_user_project(self): status_code=204), ]) self.assertTrue( - self.op_cloud.grant_role( + self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, project=self.project_data.project_id)) self.assertTrue( - self.op_cloud.grant_role( + self.cloud.grant_role( self.role_data.role_name, user=self.user_data.user_id, project=self.project_data.project_id)) @@ -496,11 +496,11 @@ def test_grant_role_user_project_exists(self): entity_type='user', entity_id=self.user_data.user_id)}), ]) - self.assertFalse(self.op_cloud.grant_role( + self.assertFalse(self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, project=self.project_data.project_id)) - self.assertFalse(self.op_cloud.grant_role( + self.assertFalse(self.cloud.grant_role( self.role_data.role_id, user=self.user_data.user_id, project=self.project_data.project_id)) @@ -569,11 +569,11 @@ def test_grant_role_group_project(self): self.role_data.role_id]), status_code=204), ]) - self.assertTrue(self.op_cloud.grant_role( + self.assertTrue(self.cloud.grant_role( self.role_data.role_name, group=self.group_data.group_name, project=self.project_data.project_id)) - self.assertTrue(self.op_cloud.grant_role( + self.assertTrue(self.cloud.grant_role( self.role_data.role_name, group=self.group_data.group_id, project=self.project_data.project_id)) @@ -640,11 +640,11 @@ def test_grant_role_group_project_exists(self): entity_type='group', entity_id=self.group_data.group_id)}), ]) - self.assertFalse(self.op_cloud.grant_role( + self.assertFalse(self.cloud.grant_role( self.role_data.role_name, group=self.group_data.group_name, project=self.project_data.project_id)) - self.assertFalse(self.op_cloud.grant_role( + self.assertFalse(self.cloud.grant_role( self.role_data.role_name, group=self.group_data.group_id, project=self.project_data.project_id)) @@ -777,19 +777,19 @@ def test_grant_role_user_domain(self): self.role_data.role_id]), status_code=204), ]) - self.assertTrue(self.op_cloud.grant_role( + self.assertTrue(self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, domain=self.domain_data.domain_id)) - self.assertTrue(self.op_cloud.grant_role( + self.assertTrue(self.cloud.grant_role( self.role_data.role_name, user=self.user_data.user_id, domain=self.domain_data.domain_id)) - self.assertTrue(self.op_cloud.grant_role( + self.assertTrue(self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, domain=self.domain_data.domain_name)) - self.assertTrue(self.op_cloud.grant_role( + self.assertTrue(self.cloud.grant_role( self.role_data.role_name, user=self.user_data.user_id, domain=self.domain_data.domain_name)) @@ -914,19 +914,19 @@ def test_grant_role_user_domain_exists(self): entity_type='user', entity_id=self.user_data.user_id)}), ]) - self.assertFalse(self.op_cloud.grant_role( + self.assertFalse(self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, domain=self.domain_data.domain_id)) - self.assertFalse(self.op_cloud.grant_role( + self.assertFalse(self.cloud.grant_role( self.role_data.role_name, user=self.user_data.user_id, domain=self.domain_data.domain_id)) - self.assertFalse(self.op_cloud.grant_role( + self.assertFalse(self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, domain=self.domain_data.domain_name)) - self.assertFalse(self.op_cloud.grant_role( + self.assertFalse(self.cloud.grant_role( self.role_data.role_name, user=self.user_data.user_id, domain=self.domain_data.domain_name)) @@ -1059,19 +1059,19 @@ def test_grant_role_group_domain(self): self.role_data.role_id]), status_code=204), ]) - self.assertTrue(self.op_cloud.grant_role( + self.assertTrue(self.cloud.grant_role( self.role_data.role_name, group=self.group_data.group_name, domain=self.domain_data.domain_id)) - self.assertTrue(self.op_cloud.grant_role( + self.assertTrue(self.cloud.grant_role( self.role_data.role_name, group=self.group_data.group_id, domain=self.domain_data.domain_id)) - self.assertTrue(self.op_cloud.grant_role( + self.assertTrue(self.cloud.grant_role( self.role_data.role_name, group=self.group_data.group_name, domain=self.domain_data.domain_name)) - self.assertTrue(self.op_cloud.grant_role( + self.assertTrue(self.cloud.grant_role( self.role_data.role_name, group=self.group_data.group_id, domain=self.domain_data.domain_name)) @@ -1196,19 +1196,19 @@ def test_grant_role_group_domain_exists(self): entity_type='group', entity_id=self.group_data.group_id)}), ]) - self.assertFalse(self.op_cloud.grant_role( + self.assertFalse(self.cloud.grant_role( self.role_data.role_name, group=self.group_data.group_name, domain=self.domain_data.domain_id)) - self.assertFalse(self.op_cloud.grant_role( + self.assertFalse(self.cloud.grant_role( self.role_data.role_name, group=self.group_data.group_id, domain=self.domain_data.domain_id)) - self.assertFalse(self.op_cloud.grant_role( + self.assertFalse(self.cloud.grant_role( self.role_data.role_name, group=self.group_data.group_name, domain=self.domain_data.domain_name)) - self.assertFalse(self.op_cloud.grant_role( + self.assertFalse(self.cloud.grant_role( self.role_data.role_name, group=self.group_data.group_id, domain=self.domain_data.domain_name)) @@ -1292,11 +1292,11 @@ def test_revoke_role_user_v2(self): self.role_data.role_id]), status_code=204), ]) - self.assertTrue(self.op_cloud.revoke_role( + self.assertTrue(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name, project=self.project_data.project_id)) - self.assertTrue(self.op_cloud.revoke_role( + self.assertTrue(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.user_id, project=self.project_data.project_id)) @@ -1406,19 +1406,19 @@ def test_revoke_role_user_project_v2(self): status_code=200, json={'roles': []}) ]) - self.assertFalse(self.op_cloud.revoke_role( + self.assertFalse(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name, project=self.project_data.project_id)) - self.assertFalse(self.op_cloud.revoke_role( + self.assertFalse(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.user_id, project=self.project_data.project_id)) - self.assertFalse(self.op_cloud.revoke_role( + self.assertFalse(self.cloud.revoke_role( self.role_data.role_id, user=self.user_data.name, project=self.project_data.project_id)) - self.assertFalse(self.op_cloud.revoke_role( + self.assertFalse(self.cloud.revoke_role( self.role_data.role_id, user=self.user_data.user_id, project=self.project_data.project_id)) @@ -1462,7 +1462,7 @@ def test_revoke_role_user_project_v2_exists(self): self.role_data.role_id]), status_code=204), ]) - self.assertTrue(self.op_cloud.revoke_role( + self.assertTrue(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name, project=self.project_data.project_id)) @@ -1517,11 +1517,11 @@ def test_revoke_role_user_project(self): complete_qs=True, json={'role_assignments': []}), ]) - self.assertFalse(self.op_cloud.revoke_role( + self.assertFalse(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name, project=self.project_data.project_id)) - self.assertFalse(self.op_cloud.revoke_role( + self.assertFalse(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.user_id, project=self.project_data.project_id)) @@ -1602,11 +1602,11 @@ def test_revoke_role_user_project_exists(self): 'roles', self.role_data.role_id])), ]) - self.assertTrue(self.op_cloud.revoke_role( + self.assertTrue(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name, project=self.project_data.project_id)) - self.assertTrue(self.op_cloud.revoke_role( + self.assertTrue(self.cloud.revoke_role( self.role_data.role_id, user=self.user_data.user_id, project=self.project_data.project_id)) @@ -1661,11 +1661,11 @@ def test_revoke_role_group_project(self): complete_qs=True, json={'role_assignments': []}), ]) - self.assertFalse(self.op_cloud.revoke_role( + self.assertFalse(self.cloud.revoke_role( self.role_data.role_name, group=self.group_data.group_name, project=self.project_data.project_id)) - self.assertFalse(self.op_cloud.revoke_role( + self.assertFalse(self.cloud.revoke_role( self.role_data.role_name, group=self.group_data.group_id, project=self.project_data.project_id)) @@ -1746,11 +1746,11 @@ def test_revoke_role_group_project_exists(self): 'roles', self.role_data.role_id])), ]) - self.assertTrue(self.op_cloud.revoke_role( + self.assertTrue(self.cloud.revoke_role( self.role_data.role_name, group=self.group_data.group_name, project=self.project_data.project_id)) - self.assertTrue(self.op_cloud.revoke_role( + self.assertTrue(self.cloud.revoke_role( self.role_data.role_name, group=self.group_data.group_id, project=self.project_data.project_id)) @@ -1851,19 +1851,19 @@ def test_revoke_role_user_domain(self): complete_qs=True, json={'role_assignments': []}), ]) - self.assertFalse(self.op_cloud.revoke_role( + self.assertFalse(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name, domain=self.domain_data.domain_id)) - self.assertFalse(self.op_cloud.revoke_role( + self.assertFalse(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.user_id, domain=self.domain_data.domain_id)) - self.assertFalse(self.op_cloud.revoke_role( + self.assertFalse(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name, domain=self.domain_data.domain_name)) - self.assertFalse(self.op_cloud.revoke_role( + self.assertFalse(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.user_id, domain=self.domain_data.domain_name)) @@ -2016,19 +2016,19 @@ def test_revoke_role_user_domain_exists(self): 'roles', self.role_data.role_id])), ]) - self.assertTrue(self.op_cloud.revoke_role( + self.assertTrue(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name, domain=self.domain_data.domain_name)) - self.assertTrue(self.op_cloud.revoke_role( + self.assertTrue(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.user_id, domain=self.domain_data.domain_name)) - self.assertTrue(self.op_cloud.revoke_role( + self.assertTrue(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name, domain=self.domain_data.domain_id)) - self.assertTrue(self.op_cloud.revoke_role( + self.assertTrue(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.user_id, domain=self.domain_data.domain_id)) @@ -2129,19 +2129,19 @@ def test_revoke_role_group_domain(self): complete_qs=True, json={'role_assignments': []}), ]) - self.assertFalse(self.op_cloud.revoke_role( + self.assertFalse(self.cloud.revoke_role( self.role_data.role_name, group=self.group_data.group_name, domain=self.domain_data.domain_name)) - self.assertFalse(self.op_cloud.revoke_role( + self.assertFalse(self.cloud.revoke_role( self.role_data.role_name, group=self.group_data.group_id, domain=self.domain_data.domain_name)) - self.assertFalse(self.op_cloud.revoke_role( + self.assertFalse(self.cloud.revoke_role( self.role_data.role_name, group=self.group_data.group_name, domain=self.domain_data.domain_id)) - self.assertFalse(self.op_cloud.revoke_role( + self.assertFalse(self.cloud.revoke_role( self.role_data.role_name, group=self.group_data.group_id, domain=self.domain_data.domain_id)) @@ -2294,19 +2294,19 @@ def test_revoke_role_group_domain_exists(self): 'roles', self.role_data.role_id])), ]) - self.assertTrue(self.op_cloud.revoke_role( + self.assertTrue(self.cloud.revoke_role( self.role_data.role_name, group=self.group_data.group_name, domain=self.domain_data.domain_name)) - self.assertTrue(self.op_cloud.revoke_role( + self.assertTrue(self.cloud.revoke_role( self.role_data.role_name, group=self.group_data.group_id, domain=self.domain_data.domain_name)) - self.assertTrue(self.op_cloud.revoke_role( + self.assertTrue(self.cloud.revoke_role( self.role_data.role_name, group=self.group_data.group_name, domain=self.domain_data.domain_id)) - self.assertTrue(self.op_cloud.revoke_role( + self.assertTrue(self.cloud.revoke_role( self.role_data.role_name, group=self.group_data.group_id, domain=self.domain_data.domain_id)) @@ -2324,7 +2324,7 @@ def test_grant_no_role(self): exc.OpenStackCloudException, 'Role {0} not found'.format(self.role_data.role_name) ): - self.op_cloud.grant_role( + self.cloud.grant_role( self.role_data.role_name, group=self.group_data.group_name, domain=self.domain_data.domain_name) @@ -2342,7 +2342,7 @@ def test_revoke_no_role(self): exc.OpenStackCloudException, 'Role {0} not found'.format(self.role_data.role_name) ): - self.op_cloud.revoke_role( + self.cloud.revoke_role( self.role_data.role_name, group=self.group_data.group_name, domain=self.domain_data.domain_name) @@ -2359,7 +2359,7 @@ def test_grant_no_user_or_group_specified(self): exc.OpenStackCloudException, 'Must specify either a user or a group' ): - self.op_cloud.grant_role(self.role_data.role_name) + self.cloud.grant_role(self.role_data.role_name) self.assert_calls() def test_revoke_no_user_or_group_specified(self): @@ -2373,7 +2373,7 @@ def test_revoke_no_user_or_group_specified(self): exc.OpenStackCloudException, 'Must specify either a user or a group' ): - self.op_cloud.revoke_role(self.role_data.role_name) + self.cloud.revoke_role(self.role_data.role_name) self.assert_calls() def test_grant_no_user_or_group(self): @@ -2391,7 +2391,7 @@ def test_grant_no_user_or_group(self): exc.OpenStackCloudException, 'Must specify either a user or a group' ): - self.op_cloud.grant_role( + self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name) self.assert_calls() @@ -2411,7 +2411,7 @@ def test_revoke_no_user_or_group(self): exc.OpenStackCloudException, 'Must specify either a user or a group' ): - self.op_cloud.revoke_role( + self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name) self.assert_calls() @@ -2435,7 +2435,7 @@ def test_grant_both_user_and_group(self): exc.OpenStackCloudException, 'Specify either a group or a user, not both' ): - self.op_cloud.grant_role( + self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, group=self.group_data.group_name) @@ -2460,7 +2460,7 @@ def test_revoke_both_user_and_group(self): exc.OpenStackCloudException, 'Specify either a group or a user, not both' ): - self.op_cloud.revoke_role( + self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name, group=self.group_data.group_name) @@ -2508,7 +2508,7 @@ def test_grant_both_project_and_domain(self): status_code=204) ]) self.assertTrue( - self.op_cloud.grant_role( + self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, project=self.project_data.project_id, @@ -2562,7 +2562,7 @@ def test_revoke_both_project_and_domain(self): self.role_data.role_id]), status_code=204) ]) - self.assertTrue(self.op_cloud.revoke_role( + self.assertTrue(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name, project=self.project_data.project_id, @@ -2592,7 +2592,7 @@ def test_grant_no_project_or_domain(self): exc.OpenStackCloudException, 'Must specify either a domain or project' ): - self.op_cloud.grant_role( + self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name) self.assert_calls() @@ -2626,7 +2626,7 @@ def test_revoke_no_project_or_domain(self): exc.OpenStackCloudException, 'Must specify either a domain or project' ): - self.op_cloud.revoke_role( + self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name) self.assert_calls() @@ -2648,7 +2648,7 @@ def test_grant_bad_domain_exception(self): exc.OpenStackCloudURINotFound, 'Failed to get domain baddomain' ): - self.op_cloud.grant_role( + self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, domain='baddomain') @@ -2671,7 +2671,7 @@ def test_revoke_bad_domain_exception(self): exc.OpenStackCloudURINotFound, 'Failed to get domain baddomain' ): - self.op_cloud.revoke_role( + self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name, domain='baddomain') @@ -2723,7 +2723,7 @@ def test_grant_role_user_project_v2_wait(self): json={'roles': [self.role_data.json_response['role']]}), ]) self.assertTrue( - self.op_cloud.grant_role( + self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, project=self.project_data.project_id, @@ -2784,7 +2784,7 @@ def test_grant_role_user_project_v2_wait_exception(self): json={'roles': []}), ]) self.assertTrue( - self.op_cloud.grant_role( + self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, project=self.project_data.project_id, @@ -2839,7 +2839,7 @@ def test_revoke_role_user_project_v2_wait(self): json={'roles': []}), ]) self.assertTrue( - self.op_cloud.revoke_role( + self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name, project=self.project_data.project_id, @@ -2897,7 +2897,7 @@ def test_revoke_role_user_project_v2_wait_exception(self): exc.OpenStackCloudTimeout, 'Timeout waiting for role to be revoked' ): - self.assertTrue(self.op_cloud.revoke_role( + self.assertTrue(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name, project=self.project_data.project_id, wait=True, timeout=0.01)) self.assert_calls(do_count=False) diff --git a/openstack/tests/unit/cloud/test_services.py b/openstack/tests/unit/cloud/test_services.py index 488eacdc8..99a18f9d6 100644 --- a/openstack/tests/unit/cloud/test_services.py +++ b/openstack/tests/unit/cloud/test_services.py @@ -50,7 +50,7 @@ def test_create_service_v2(self): validate=dict(json={'OS-KSADM:service': reference_req})) ]) - service = self.op_cloud.create_service( + service = self.cloud.create_service( name=service_data.service_name, service_type=service_data.service_type, description=service_data.description) @@ -74,7 +74,7 @@ def test_create_service_v3(self): validate=dict(json={'service': service_data.json_request})) ]) - service = self.op_cloud.create_service( + service = self.cloud.create_service( name=service_data.service_name, service_type=service_data.service_type, description=service_data.description) @@ -91,7 +91,7 @@ def test_update_service_v2(self): self.use_keystone_v2() # NOTE(SamYaple): Update service only works with v3 api self.assertRaises(OpenStackCloudUnavailableFeature, - self.op_cloud.update_service, + self.cloud.update_service, 'service_id', name='new name') def test_update_service_v3(self): @@ -116,8 +116,8 @@ def test_update_service_v3(self): validate=dict(json={'service': request})) ]) - service = self.op_cloud.update_service(service_data.service_id, - enabled=False) + service = self.cloud.update_service( + service_data.service_id, enabled=False) self.assertThat(service.name, matchers.Equals(service_data.service_name)) self.assertThat(service.id, matchers.Equals(service_data.service_id)) @@ -135,7 +135,7 @@ def test_list_services(self): status_code=200, json={'services': [service_data.json_response_v3['service']]}) ]) - services = self.op_cloud.list_services() + services = self.cloud.list_services() self.assertThat(len(services), matchers.Equals(1)) self.assertThat(services[0].id, matchers.Equals(service_data.service_id)) @@ -173,22 +173,22 @@ def test_get_service(self): ]) # Search by id - service = self.op_cloud.get_service(name_or_id=service_data.service_id) + service = self.cloud.get_service(name_or_id=service_data.service_id) self.assertThat(service.id, matchers.Equals(service_data.service_id)) # Search by name - service = self.op_cloud.get_service( + service = self.cloud.get_service( name_or_id=service_data.service_name) # test we are getting exactly 1 element self.assertThat(service.id, matchers.Equals(service_data.service_id)) # Not found - service = self.op_cloud.get_service(name_or_id='INVALID SERVICE') + service = self.cloud.get_service(name_or_id='INVALID SERVICE') self.assertIs(None, service) # Multiple matches # test we are getting an Exception - self.assertRaises(OpenStackCloudException, self.op_cloud.get_service, + self.assertRaises(OpenStackCloudException, self.cloud.get_service, name_or_id=None, filters={'type': 'type2'}) self.assert_calls() @@ -223,7 +223,7 @@ def test_search_services(self): ]) # Search by id - services = self.op_cloud.search_services( + services = self.cloud.search_services( name_or_id=service_data.service_id) # test we are getting exactly 1 element self.assertThat(len(services), matchers.Equals(1)) @@ -231,7 +231,7 @@ def test_search_services(self): matchers.Equals(service_data.service_id)) # Search by name - services = self.op_cloud.search_services( + services = self.cloud.search_services( name_or_id=service_data.service_name) # test we are getting exactly 1 element self.assertThat(len(services), matchers.Equals(1)) @@ -239,11 +239,11 @@ def test_search_services(self): matchers.Equals(service_data.service_name)) # Not found - services = self.op_cloud.search_services(name_or_id='!INVALID!') + services = self.cloud.search_services(name_or_id='!INVALID!') self.assertThat(len(services), matchers.Equals(0)) # Multiple matches - services = self.op_cloud.search_services( + services = self.cloud.search_services( filters={'type': service_data.service_type}) # test we are getting exactly 2 elements self.assertThat(len(services), matchers.Equals(2)) @@ -275,9 +275,9 @@ def test_delete_service(self): ]) # Delete by name - self.op_cloud.delete_service(name_or_id=service_data.service_name) + self.cloud.delete_service(name_or_id=service_data.service_name) # Delete by id - self.op_cloud.delete_service(service_data.service_id) + self.cloud.delete_service(service_data.service_id) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_usage.py b/openstack/tests/unit/cloud/test_usage.py index a4f4d5f49..3e47602b5 100644 --- a/openstack/tests/unit/cloud/test_usage.py +++ b/openstack/tests/unit/cloud/test_usage.py @@ -60,6 +60,6 @@ def test_get_usage(self): }}) ]) - self.op_cloud.get_compute_usage(project.project_id, start, end) + self.cloud.get_compute_usage(project.project_id, start, end) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_users.py b/openstack/tests/unit/cloud/test_users.py index a5c00a6c8..0c155703e 100644 --- a/openstack/tests/unit/cloud/test_users.py +++ b/openstack/tests/unit/cloud/test_users.py @@ -54,7 +54,7 @@ def test_create_user_v2(self): validate=dict(json=user_data.json_request)), ]) - user = self.op_cloud.create_user( + user = self.cloud.create_user( name=user_data.name, email=user_data.email, password=user_data.password) @@ -75,7 +75,7 @@ def test_create_user_v3(self): validate=dict(json=user_data.json_request)), ]) - user = self.op_cloud.create_user( + user = self.cloud.create_user( name=user_data.name, email=user_data.email, password=user_data.password, description=user_data.description, @@ -114,7 +114,7 @@ def test_update_user_password_v2(self): json=user_data.json_response, validate=dict(json={'user': {}}))]) - user = self.op_cloud.update_user( + user = self.cloud.update_user( user_data.user_id, password=user_data.password) self.assertEqual(user_data.name, user.name) self.assertEqual(user_data.email, user.email) @@ -128,7 +128,7 @@ def test_create_user_v3_no_domain(self): "User or project creation requires an explicit" " domain_id argument." ): - self.op_cloud.create_user( + self.cloud.create_user( name=user_data.name, email=user_data.email, password=user_data.password) @@ -146,7 +146,7 @@ def test_delete_user(self): json=user_data.json_response), dict(method='DELETE', uri=user_resource_uri, status_code=204)]) - self.op_cloud.delete_user(user_data.name) + self.cloud.delete_user(user_data.name) self.assert_calls() def test_delete_user_not_found(self): @@ -154,7 +154,7 @@ def test_delete_user_not_found(self): dict(method='GET', uri=self._get_keystone_mock_url(resource='users'), status_code=200, json={'users': []})]) - self.assertFalse(self.op_cloud.delete_user(self.getUniqueString())) + self.assertFalse(self.cloud.delete_user(self.getUniqueString())) def test_add_user_to_group(self): user_data = self._get_user_data() @@ -174,7 +174,7 @@ def test_add_user_to_group(self): resource='groups', append=[group_data.group_id, 'users', user_data.user_id]), status_code=200)]) - self.op_cloud.add_user_to_group(user_data.user_id, group_data.group_id) + self.cloud.add_user_to_group(user_data.user_id, group_data.group_id) self.assert_calls() def test_is_user_in_group(self): @@ -196,7 +196,7 @@ def test_is_user_in_group(self): append=[group_data.group_id, 'users', user_data.user_id]), status_code=204)]) - self.assertTrue(self.op_cloud.is_user_in_group( + self.assertTrue(self.cloud.is_user_in_group( user_data.user_id, group_data.group_id)) self.assert_calls() @@ -218,6 +218,6 @@ def test_remove_user_from_group(self): append=[group_data.group_id, 'users', user_data.user_id]), status_code=204)]) - self.op_cloud.remove_user_from_group(user_data.user_id, - group_data.group_id) + self.cloud.remove_user_from_group( + user_data.user_id, group_data.group_id) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_volume_access.py b/openstack/tests/unit/cloud/test_volume_access.py index 79b1697ee..ffdaa2db4 100644 --- a/openstack/tests/unit/cloud/test_volume_access.py +++ b/openstack/tests/unit/cloud/test_volume_access.py @@ -70,7 +70,7 @@ def test_get_volume_type_access(self): 'os-volume-type-access']), json={'volume_type_access': volume_type_access})]) self.assertEqual( - len(self.op_cloud.get_volume_type_access(volume_type['name'])), 2) + len(self.cloud.get_volume_type_access(volume_type['name'])), 2) self.assert_calls() def test_remove_volume_type_access(self): @@ -122,12 +122,12 @@ def test_remove_volume_type_access(self): 'os-volume-type-access']), json={'volume_type_access': [project_001]})]) self.assertEqual( - len(self.op_cloud.get_volume_type_access( + len(self.cloud.get_volume_type_access( volume_type['name'])), 2) - self.op_cloud.remove_volume_type_access( + self.cloud.remove_volume_type_access( volume_type['name'], project_001['project_id']) self.assertEqual( - len(self.op_cloud.get_volume_type_access(volume_type['name'])), 1) + len(self.cloud.get_volume_type_access(volume_type['name'])), 1) self.assert_calls() def test_add_volume_type_access(self): @@ -167,10 +167,10 @@ def test_add_volume_type_access(self): append=['types', volume_type['id'], 'os-volume-type-access']), json={'volume_type_access': volume_type_access})]) - self.op_cloud.add_volume_type_access( + self.cloud.add_volume_type_access( volume_type['name'], project_002['project_id']) self.assertEqual( - len(self.op_cloud.get_volume_type_access(volume_type['name'])), 2) + len(self.cloud.get_volume_type_access(volume_type['name'])), 2) self.assert_calls() def test_add_volume_type_access_missing(self): @@ -189,6 +189,6 @@ def test_add_volume_type_access_missing(self): with testtools.ExpectedException( openstack.cloud.OpenStackCloudException, "VolumeType not found: MISSING"): - self.op_cloud.add_volume_type_access( + self.cloud.add_volume_type_access( "MISSING", project_001['project_id']) self.assert_calls() From baef9e52bc1e37ff38f4a4988e19a7f7f45ef68a Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Sun, 3 Dec 2017 22:47:39 -0500 Subject: [PATCH 1950/3836] Add retry logic mechanism Added retry logic mechanism and added them to the API calls where ironic presently attempts to pull an exclusive lock on the baremetal node. Change-Id: Ia6fbc9eec612793b3214d7883e0552913a088d5d --- openstack/cloud/_utils.py | 55 +++++++ openstack/cloud/openstackcloud.py | 29 ++-- .../tests/unit/cloud/test_baremetal_node.py | 137 ++++++++++++++++++ 3 files changed, 210 insertions(+), 11 deletions(-) diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index 7d3fbc76c..db83137fd 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -22,6 +22,7 @@ import six import sre_constants import sys +import time import uuid from decorator import decorator @@ -486,6 +487,60 @@ def safe_dict_max(key, data): return max_value +def _call_client_and_retry(client, url, retry_on=None, + call_retries=3, retry_wait=2, + **kwargs): + """Method to provide retry operations. + + Some APIs utilize HTTP errors on certian operations to indicate that + the resource is presently locked, and as such this mechanism provides + the ability to retry upon known error codes. + + :param object client: The client method, such as: + ``self.baremetal_client.post`` + :param string url: The URL to perform the operation upon. + :param integer retry_on: A list of error codes that can be retried on. + The method also supports a single integer to be + defined. + :param integer call_retries: The number of times to retry the call upon + the error code defined by the 'retry_on' + parameter. Default: 3 + :param integer retry_wait: The time in seconds to wait between retry + attempts. Default: 2 + + :returns: The object returned by the client call. + """ + + # NOTE(TheJulia): This method, as of this note, does not have direct + # unit tests, although is fairly well tested by the tests checking + # retry logic in test_baremetal_node.py. + log = _log.setup_logging('shade.http') + + if isinstance(retry_on, int): + retry_on = [retry_on] + + count = 0 + while (count < call_retries): + count += 1 + try: + ret_val = client(url, **kwargs) + except exc.OpenStackCloudHTTPError as e: + if (retry_on is not None and + e.response.status_code in retry_on): + log.debug('Received retryable error {err}, waiting ' + '{wait} seconds to retry', { + 'err': e.response.status_code, + 'wait': retry_wait + }) + time.sleep(retry_wait) + continue + else: + raise + # Break out of the loop, since the loop should only continue + # when we encounter a known connection error. + return ret_val + + def parse_range(value): """Parse a numerical range string. diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 71ac71ce9..5fd3f084a 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -9187,7 +9187,9 @@ def unregister_machine(self, nics, uuid, wait=False, timeout=600): port = self._baremetal_client.get(port_url, microversion=1.6, error_message=port_msg) port_url = '/ports/{uuid}'.format(uuid=port['ports'][0]['uuid']) - self._baremetal_client.delete(port_url, error_message=port_msg) + _utils._call_client_and_retry(self._baremetal_client.delete, + port_url, retry_on=[409, 503], + error_message=port_msg) with _utils.shade_exceptions( "Error unregistering machine {node_id} from the baremetal " @@ -9199,10 +9201,11 @@ def unregister_machine(self, nics, uuid, wait=False, timeout=600): # microversions in future releases, as such, we explicitly set # the version to what we have been using with the client library.. version = "1.6" - msg = "Baremetal machine failed to be deleted." + msg = "Baremetal machine failed to be deleted" url = '/nodes/{node_id}'.format( node_id=uuid) - self._baremetal_client.delete(url, + _utils._call_client_and_retry(self._baremetal_client.delete, + url, retry_on=[409, 503], error_message=msg, microversion=version) @@ -9428,10 +9431,12 @@ def node_set_provision_state(self, if configdrive: payload['configdrive'] = configdrive - machine = self._baremetal_client.put(url, - json=payload, - error_message=msg, - microversion=version) + machine = _utils._call_client_and_retry(self._baremetal_client.put, + url, + retry_on=[409, 503], + json=payload, + error_message=msg, + microversion=version) if wait: for count in utils.iterate_timeout( timeout, @@ -9532,10 +9537,12 @@ def _set_machine_power_state(self, name_or_id, state): else: desired_state = 'power {state}'.format(state=state) payload = {'target': desired_state} - self._baremetal_client.put(url, - json=payload, - error_message=msg, - microversion="1.6") + _utils._call_client_and_retry(self._baremetal_client.put, + url, + retry_on=[409, 503], + json=payload, + error_message=msg, + microversion="1.6") return None def set_machine_power_on(self, name_or_id): diff --git a/openstack/tests/unit/cloud/test_baremetal_node.py b/openstack/tests/unit/cloud/test_baremetal_node.py index 579c275b0..b54a417be 100644 --- a/openstack/tests/unit/cloud/test_baremetal_node.py +++ b/openstack/tests/unit/cloud/test_baremetal_node.py @@ -555,6 +555,40 @@ def test_set_machine_power_on(self): self.assert_calls() + def test_set_machine_power_on_with_retires(self): + # NOTE(TheJulia): This logic ends up testing power on/off and reboot + # as they all utilize the same helper method. + self.register_uris([ + dict( + method='PUT', + status_code=503, + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'power']), + validate=dict(json={'target': 'power on'})), + dict( + method='PUT', + status_code=409, + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'power']), + validate=dict(json={'target': 'power on'})), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'power']), + validate=dict(json={'target': 'power on'})), + ]) + return_value = self.cloud.set_machine_power_on( + self.fake_baremetal_node['uuid']) + self.assertIsNone(return_value) + + self.assert_calls() + def test_set_machine_power_off(self): self.register_uris([ dict( @@ -632,6 +666,51 @@ def test_node_set_provision_state(self): self.assert_calls() + def test_node_set_provision_state_with_retries(self): + deploy_node = self.fake_baremetal_node.copy() + deploy_node['provision_state'] = 'deploying' + active_node = self.fake_baremetal_node.copy() + active_node['provision_state'] = 'active' + self.register_uris([ + dict( + method='PUT', + status_code=409, + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'provision']), + validate=dict(json={'target': 'active', + 'configdrive': 'http://host/file'})), + dict( + method='PUT', + status_code=503, + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'provision']), + validate=dict(json={'target': 'active', + 'configdrive': 'http://host/file'})), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'provision']), + validate=dict(json={'target': 'active', + 'configdrive': 'http://host/file'})), + dict(method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + ]) + self.cloud.node_set_provision_state( + self.fake_baremetal_node['uuid'], + 'active', + configdrive='http://host/file') + + self.assert_calls() + def test_node_set_provision_state_wait_timeout(self): deploy_node = self.fake_baremetal_node.copy() deploy_node['provision_state'] = 'deploying' @@ -1410,6 +1489,64 @@ def test_unregister_machine_locked_timeout(self): timeout=0.001) self.assert_calls() + def test_unregister_machine_retries(self): + mac_address = self.fake_baremetal_port['address'] + nics = [{'mac': mac_address}] + port_uuid = self.fake_baremetal_port['uuid'] + # NOTE(TheJulia): The two values below should be the same. + port_node_uuid = self.fake_baremetal_port['node_uuid'] + port_url_address = 'detail?address=%s' % mac_address + self.fake_baremetal_node['provision_state'] = 'available' + self.register_uris([ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + dict( + method='GET', + uri=self.get_mock_url( + resource='ports', + append=[port_url_address]), + json={'ports': [{'address': mac_address, + 'node_uuid': port_node_uuid, + 'uuid': port_uuid}]}), + dict( + method='DELETE', + status_code=503, + uri=self.get_mock_url( + resource='ports', + append=[self.fake_baremetal_port['uuid']])), + dict( + method='DELETE', + status_code=409, + uri=self.get_mock_url( + resource='ports', + append=[self.fake_baremetal_port['uuid']])), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='ports', + append=[self.fake_baremetal_port['uuid']])), + dict( + method='DELETE', + status_code=409, + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']])), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']])), + ]) + + self.cloud.unregister_machine( + nics, self.fake_baremetal_node['uuid']) + + self.assert_calls() + def test_unregister_machine_unavailable(self): # This is a list of invalid states that the method # should fail on. From ff23cd0681ffaa757689d89847ce58763aabf0cf Mon Sep 17 00:00:00 2001 From: Mark Goddard Date: Wed, 6 Dec 2017 15:59:28 +0000 Subject: [PATCH 1951/3836] Baremetal NIC list should return a list Since the list_nics* methods were changed to use raw HTTP client rather than the ironic client, they return a dict rather than a list. Instead of getting this: [{'address': '00:11:22:33:44:55', ...}, ...] We get this: {'ports': [{'address': '00:11:22:33:44:55', ...}, ...]} This change removes this outer dict and returns to the old behaviour of returning a list. This affects list_nics and list_nics_for_machine. Change-Id: I3cb9ef5d97cf911cb4897b00ab4cef77b76efaa4 --- openstack/cloud/openstackcloud.py | 9 +++++---- openstack/tests/unit/cloud/test_baremetal_ports.py | 8 ++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 5fd3f084a..f25a48548 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -8833,24 +8833,25 @@ def update_cluster_template(self, name_or_id, operation, **kwargs): def list_nics(self): msg = "Error fetching machine port list" - return self._baremetal_client.get("/ports", + data = self._baremetal_client.get("/ports", microversion="1.6", error_message=msg) + return data['ports'] def list_nics_for_machine(self, uuid): """Returns a list of ports present on the machine node. :param uuid: String representing machine UUID value in order to identify the machine. - :returns: A dictionary containing containing a list of ports, - associated with the label "ports". + :returns: A list of ports. """ msg = "Error fetching port list for node {node_id}".format( node_id=uuid) url = "/nodes/{node_id}/ports".format(node_id=uuid) - return self._baremetal_client.get(url, + data = self._baremetal_client.get(url, microversion="1.6", error_message=msg) + return data['ports'] def get_nic_by_mac(self, mac): try: diff --git a/openstack/tests/unit/cloud/test_baremetal_ports.py b/openstack/tests/unit/cloud/test_baremetal_ports.py index 7584b7b96..942b472b2 100644 --- a/openstack/tests/unit/cloud/test_baremetal_ports.py +++ b/openstack/tests/unit/cloud/test_baremetal_ports.py @@ -49,8 +49,8 @@ def test_list_nics(self): ]) return_value = self.cloud.list_nics() - self.assertEqual(2, len(return_value['ports'])) - self.assertEqual(self.fake_baremetal_port, return_value['ports'][0]) + self.assertEqual(2, len(return_value)) + self.assertEqual(self.fake_baremetal_port, return_value[0]) self.assert_calls() def test_list_nics_failure(self): @@ -75,8 +75,8 @@ def test_list_nics_for_machine(self): return_value = self.cloud.list_nics_for_machine( self.fake_baremetal_node['uuid']) - self.assertEqual(2, len(return_value['ports'])) - self.assertEqual(self.fake_baremetal_port, return_value['ports'][0]) + self.assertEqual(2, len(return_value)) + self.assertEqual(self.fake_baremetal_port, return_value[0]) self.assert_calls() def test_list_nics_for_machine_failure(self): From 97827cdd3cf268f503674b345855012f6118f3f6 Mon Sep 17 00:00:00 2001 From: Antoni Segura Puimedon Date: Wed, 20 Dec 2017 16:39:31 +0100 Subject: [PATCH 1952/3836] Add supported method for checking the network exts It is useful for operators to be able to check if a cloud supports certain extensions to know whether they can perform some API actions. Change-Id: Ie99668173d4dc6e6b3992e496560b5b7598181a2 Signed-off-by: Antoni Segura Puimedon --- openstack/cloud/openstackcloud.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index f25a48548..c5dfe9d49 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -11373,6 +11373,13 @@ def get_network_quotas(self, name_or_id, details=False): "project {0}".format(proj.id))) return self._get_and_munchify('quota', data) + def get_network_extensions(self): + """Get Cloud provided network extensions + + :returns: set of Neutron extension aliases + """ + return self._neutron_extensions() + def delete_network_quotas(self, name_or_id): """ Delete network quotas for a project From aba542595b7b67f5ec54af4691dcb25ee9f2c445 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 26 Jan 2018 09:29:26 -0600 Subject: [PATCH 1953/3836] Clean up the release notes a bit 0.10.0 got out before we could mention the giant merge that had happened. There's also a few things in the prelude that were just holdovers from shade. Change-Id: I9966027e42c84f017db3c23c74b24c311273f87a --- .../notes/bug-2001080-de52ead3c5466792.yaml | 3 -- ...tch-up-release-notes-e385fad34e9f3d6e.yaml | 7 ---- ...removed-glanceclient-105c7fba9481b9be.yaml | 33 +++++++++++++++---- .../resource2-migration-835590b300bef621.yaml | 11 +++++++ 4 files changed, 37 insertions(+), 17 deletions(-) create mode 100644 releasenotes/notes/resource2-migration-835590b300bef621.yaml diff --git a/releasenotes/notes/bug-2001080-de52ead3c5466792.yaml b/releasenotes/notes/bug-2001080-de52ead3c5466792.yaml index 2b8b3c319..08f83f06a 100644 --- a/releasenotes/notes/bug-2001080-de52ead3c5466792.yaml +++ b/releasenotes/notes/bug-2001080-de52ead3c5466792.yaml @@ -1,7 +1,4 @@ --- -prelude: > - Fixed a bug where a project was always enabled upon update, unless - ``enabled=False`` is passed explicitly. fixes: - | [`bug 2001080 `_] diff --git a/releasenotes/notes/catch-up-release-notes-e385fad34e9f3d6e.yaml b/releasenotes/notes/catch-up-release-notes-e385fad34e9f3d6e.yaml index e7b98afe3..251fdb966 100644 --- a/releasenotes/notes/catch-up-release-notes-e385fad34e9f3d6e.yaml +++ b/releasenotes/notes/catch-up-release-notes-e385fad34e9f3d6e.yaml @@ -1,11 +1,4 @@ --- -prelude: > - Swiftclient instantiation now provides authentication - information so that long lived swiftclient objects can - reauthenticate if necessary. This should be a temporary - situation until swiftclient supports keystoneauth - sessions at which point os-client-config will instantiate - swiftclient with a keystoneauth session. features: - Swiftclient instantiation now provides authentication information so that long lived swiftclient objects can diff --git a/releasenotes/notes/removed-glanceclient-105c7fba9481b9be.yaml b/releasenotes/notes/removed-glanceclient-105c7fba9481b9be.yaml index 157e90e83..dc633522d 100644 --- a/releasenotes/notes/removed-glanceclient-105c7fba9481b9be.yaml +++ b/releasenotes/notes/removed-glanceclient-105c7fba9481b9be.yaml @@ -1,9 +1,28 @@ --- prelude: > - This release marks the beginning of the path towards removing all - of the 'python-\*client' libraries as dependencies. Subsequent releases - should expect to have fewer and fewer library depdencies. -upgrade: - - Removed glanceclient as a dependency. All glance operations - are now performed with direct REST calls using keystoneauth - Adapter. + The ``shade`` and ``os-client-config`` libraries have been merged + in to openstacksdk. As a result, their functionality is being + integrated into the sdk functionality, and in some cases is replacing + exisiting things. + + The ``openstack.profile.Profile`` and + ``openstack.auth.base.BaseAuthPlugin`` classes are no more. Profile has + been replace by ``openstack.config.cloud_region.CloudRegion`` from + `os-client-config + `_ + ``openstack.auth.base.BaseAuthPlugin`` has been replaced with the Auth + plugins from keystoneauth. + + Service proxy names on the ``openstack.connection.Connection`` are all + based on the official names from the OpenStack Service Types Authority. + + ``openstack.proxy.Proxy`` is now a subclass of + ``keystoneauth1.adapter.Adapter``. Removed local logic that duplicates + keystoneauth logic. This means every proxy also has direct REST primitives + available. + + .. code-block:: python + + connection = connection.Connection() + servers = connection.compute.servers() + server_response = connection.compute.get('/servers') diff --git a/releasenotes/notes/resource2-migration-835590b300bef621.yaml b/releasenotes/notes/resource2-migration-835590b300bef621.yaml new file mode 100644 index 000000000..ecf6adc4d --- /dev/null +++ b/releasenotes/notes/resource2-migration-835590b300bef621.yaml @@ -0,0 +1,11 @@ +--- +upgrade: + - | + The ``Resource2`` and ``Proxy2`` migration has been completed. The original + ``Resource`` and ``Proxy`` clases have been removed and replaced with + ``Resource2`` and ``Proxy2``. +deprecations: + - | + The ``shade`` functionality that has been merged in to openstacksdk is + found in ``openstack.cloud`` currently. None of these interfaces should + be relied upon as the merge has not yet completed. From af5507626f32698be28ce934c6b60cfec376b79b Mon Sep 17 00:00:00 2001 From: Christian Berendt Date: Mon, 11 Dec 2017 22:24:01 +0100 Subject: [PATCH 1954/3836] Add betacloud to the vendors Change-Id: I599156ee4e2ff52e4db5669fce6cacd22447c3b0 --- doc/source/user/config/vendor-support.rst | 16 ++++++++++++++++ openstack/config/vendors/betacloud.json | 14 ++++++++++++++ .../vendor-add-betacloud-03872c3485104853.yaml | 3 +++ 3 files changed, 33 insertions(+) create mode 100644 openstack/config/vendors/betacloud.json create mode 100644 releasenotes/notes/vendor-add-betacloud-03872c3485104853.yaml diff --git a/doc/source/user/config/vendor-support.rst b/doc/source/user/config/vendor-support.rst index 449fc5af8..836b55316 100644 --- a/doc/source/user/config/vendor-support.rst +++ b/doc/source/user/config/vendor-support.rst @@ -39,6 +39,22 @@ van1 Vancouver, BC * Public IPv4 is provided via NAT with Neutron Floating IP +betacloud +--------- + +https://api-1.betacloud.io:5000 + +============== ================== +Region Name Location +============== ================== +betacloud-1 Nuremberg, Germany +============== ================== + +* Identity API Version is 3 +* Images must be in `raw` format +* Public IPv4 is provided via NAT with Neutron Floating IP +* Volume API Version is 3 + catalyst -------- diff --git a/openstack/config/vendors/betacloud.json b/openstack/config/vendors/betacloud.json new file mode 100644 index 000000000..2387b09c2 --- /dev/null +++ b/openstack/config/vendors/betacloud.json @@ -0,0 +1,14 @@ +{ + "name": "betacloud", + "profile": { + "auth": { + "auth_url": "https://api-1.betacloud.io:5000" + }, + "regions": [ + "betacloud-1" + ], + "identity_api_version": "3", + "image_format": "raw", + "volume_api_version": "3" + } +} diff --git a/releasenotes/notes/vendor-add-betacloud-03872c3485104853.yaml b/releasenotes/notes/vendor-add-betacloud-03872c3485104853.yaml new file mode 100644 index 000000000..8fdded4b8 --- /dev/null +++ b/releasenotes/notes/vendor-add-betacloud-03872c3485104853.yaml @@ -0,0 +1,3 @@ +--- +other: + - Add betacloud region for Germany From 64d56d7d3f4279f3b0ebd8904bfe878b20e2b513 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 27 Jan 2018 12:28:44 -0600 Subject: [PATCH 1955/3836] Move profile helper method to openstack.profile We have a helper method for creating a Connection from a Profile to help with the transition. Move it to openstack/profile.py so that it is contained with the class it's associated with. Change-Id: I20ad8d96bd13a09afaee4401d71de7dba203682e --- openstack/connection.py | 47 ++++++++---------------------------- openstack/profile.py | 33 +++++++++++++++++++++++++ openstack/tests/unit/base.py | 2 +- 3 files changed, 44 insertions(+), 38 deletions(-) diff --git a/openstack/connection.py b/openstack/connection.py index e35460d6a..e032f18b2 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -74,18 +74,20 @@ network = conn.network.create_network({"name": "zuul"}) """ +__all__ = [ + 'from_config', + 'Connection', +] + import warnings import keystoneauth1.exceptions import os_service_types import requestsexceptions import six -from six.moves import urllib from openstack import _log -import openstack.config -from openstack.config import cloud_region -from openstack.config import defaults as config_defaults +from openstack import config as _config from openstack import exceptions from openstack import service_description from openstack import task_manager @@ -120,7 +122,7 @@ def from_config(cloud=None, config=None, options=None, **kwargs): cloud = cloud or kwargs.get('cloud_name') config = config or kwargs.get('cloud_config') if config is None: - config = openstack.config.OpenStackConfig().get_one( + config = _config.OpenStackConfig().get_one( cloud=cloud, argparse=options) return Connection(config=config) @@ -186,12 +188,13 @@ def __init__(self, cloud=None, config=None, session=None, if not self.config: if profile: + import openstack.profile # TODO(shade) Remove this once we've shifted # python-openstackclient to not use the profile interface. - self.config = self._get_config_from_profile( + self.config = openstack.profile._get_config_from_profile( profile, authenticator, **kwargs) else: - openstack_config = openstack.config.OpenStackConfig( + openstack_config = _config.OpenStackConfig( app_name=app_name, app_version=app_version, load_yaml_config=profile is None) self.config = openstack_config.get_one( @@ -218,36 +221,6 @@ def __init__(self, cloud=None, config=None, session=None, service_description.OpenStackServiceDescription( service, self.config)) - def _get_config_from_profile(self, profile, authenticator, **kwargs): - """Get openstack.config objects from legacy profile.""" - # TODO(shade) Remove this once we've shifted python-openstackclient - # to not use the profile interface. - - # We don't have a cloud name. Make one up from the auth_url hostname - # so that log messages work. - name = urllib.parse.urlparse(authenticator.auth_url).hostname - region_name = None - for service in profile.get_services(): - if service.region: - region_name = service.region - service_type = service.service_type - if service.interface: - key = cloud_region._make_key('interface', service_type) - kwargs[key] = service.interface - if service.version: - version = service.version - if version.startswith('v'): - version = version[1:] - key = cloud_region._make_key('api_version', service_type) - kwargs[key] = service.version - - config_kwargs = config_defaults.get_defaults() - config_kwargs.update(kwargs) - config = cloud_region.CloudRegion( - name=name, region_name=region_name, config=config_kwargs) - config._auth = authenticator - return config - def add_service(self, service): """Add a service to the Connection. diff --git a/openstack/profile.py b/openstack/profile.py index ecec83949..b502bf727 100644 --- a/openstack/profile.py +++ b/openstack/profile.py @@ -16,8 +16,11 @@ """ import copy +from six.moves import urllib from openstack import _log +from openstack.config import cloud_region +from openstack.config import defaults as config_defaults from openstack.baremetal import baremetal_service from openstack.block_storage import block_storage_service from openstack.clustering import clustering_service @@ -38,6 +41,36 @@ _logger = _log.setup_logging('openstack') +def _get_config_from_profile(profile, authenticator, **kwargs): + # TODO(shade) Remove this once we've shifted python-openstackclient + # to not use the profile interface. + + # We don't have a cloud name. Make one up from the auth_url hostname + # so that log messages work. + name = urllib.parse.urlparse(authenticator.auth_url).hostname + region_name = None + for service in profile.get_services(): + if service.region: + region_name = service.region + service_type = service.service_type + if service.interface: + key = cloud_region._make_key('interface', service_type) + kwargs[key] = service.interface + if service.version: + version = service.version + if version.startswith('v'): + version = version[1:] + key = cloud_region._make_key('api_version', service_type) + kwargs[key] = service.version + + config_kwargs = config_defaults.get_defaults() + config_kwargs.update(kwargs) + config = cloud_region.CloudRegion( + name=name, region_name=region_name, config=config_kwargs) + config._auth = authenticator + return config + + class Profile(object): ALL = "*" diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index b784be1a0..2c4eaf08a 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -26,7 +26,7 @@ from six.moves import urllib import tempfile -import openstack +import openstack.cloud import openstack.connection from openstack.tests import base From b31ca63e6db278286dea93b6534c0777491216d2 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Sun, 28 Jan 2018 23:33:57 +0000 Subject: [PATCH 1956/3836] Update reno for stable/queens Change-Id: Ib462b04d5a3498e72b166a093cbe842642c09868 --- releasenotes/source/index.rst | 1 + releasenotes/source/queens.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/queens.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index dfea92ba7..0dc3c6227 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,5 +6,6 @@ :maxdepth: 1 unreleased + queens pike ocata diff --git a/releasenotes/source/queens.rst b/releasenotes/source/queens.rst new file mode 100644 index 000000000..36ac6160c --- /dev/null +++ b/releasenotes/source/queens.rst @@ -0,0 +1,6 @@ +=================================== + Queens Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/queens From 163f502345b3605e9bbf80b938fa4c93aa5406b4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 29 Jan 2018 10:37:54 -0600 Subject: [PATCH 1957/3836] Provide compatibility for people passing raw sessions Code used to pass raw sessions to Resource methods. A common way to do that was to do thins like FloatingIP.get(conn.session, name_or_id). As conn.session is a session.Session that doesn't work anymore. To help ease upgrade path issues, attach a reference to the Connection into conn.session and so that we can pull the right adapter for a given resource back out. Add the neutron-grenade job to verify this works. Change-Id: Ief9a0215ea2399b91d1d03a8048e73e6d7bedd64 --- .zuul.yaml | 2 ++ openstack/connection.py | 6 +++- openstack/resource.py | 30 +++++++++++++++++++ .../tests/unit/compute/v2/test_limits.py | 3 +- openstack/tests/unit/image/v2/test_image.py | 3 +- .../tests/unit/network/v2/test_floating_ip.py | 5 ++-- openstack/tests/unit/test_resource.py | 4 +-- 7 files changed, 46 insertions(+), 7 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index c54632b3b..c13a97774 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -219,6 +219,7 @@ - openstacksdk-functional-devstack-python3 - osc-functional-devstack-tips: voting: false + - neutron-grenade gate: jobs: - build-openstack-sphinx-docs: @@ -226,3 +227,4 @@ sphinx_python: python3 - openstacksdk-functional-devstack - openstacksdk-functional-devstack-python3 + - neutron-grenade diff --git a/openstack/connection.py b/openstack/connection.py index e032f18b2..763621c09 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -214,6 +214,10 @@ def __init__(self, cloud=None, config=None, session=None, self.config._keystone_session = session self.session = self.config.get_session() + # Hide a reference to the connection on the session to help with + # backwards compatibility for folks trying to just pass conn.session + # to a Resource method's session argument. + self.session._sdk_connection = self service_type_manager = os_service_types.ServiceTypes() for service in service_type_manager.services: @@ -253,7 +257,7 @@ class contained in service_name=self.config.get_service_name(service_type), interface=self.config.get_interface(service_type), region_name=self.config.region_name, - version=self.config.get_api_version(service_type) + version=self.config.get_api_version(service_type), ) # Register the proxy class with every known alias diff --git a/openstack/resource.py b/openstack/resource.py index 96cb908ea..d2bc696ef 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -34,6 +34,7 @@ class that represent a remote resource. The attributes that import collections import itertools +from keystoneauth1 import adapter from requests import structures from openstack import exceptions @@ -649,6 +650,29 @@ def _translate_response(self, response, has_body=None, error_message=None): self._header.attributes.update(headers) self._header.clean() + @classmethod + def _get_session(cls, session): + """Attempt to get an Adapter from a raw session. + + Some older code used conn.session has the session argument to Resource + methods. That does not work anymore, as Resource methods expect an + Adapter not a session. We've hidden an _sdk_connection on the Session + stored on the connection. If we get something that isn't an Adapter, + pull the connection from the Session and look up the adapter by + service_type. + """ + # TODO(mordred) We'll need to do this for every method in every + # Resource class that is calling session.$something to be complete. + if isinstance(session, adapter.Adapter): + return session + if hasattr(session, '_sdk_connection'): + service_type = cls.service['service_type'] + return getattr(session._sdk_connection, service_type) + raise ValueError( + "The session argument to Resource methods requires either an" + " instance of an openstack.proxy.Proxy object or at the very least" + " a raw keystoneauth1.adapter.Adapter.") + def create(self, session, prepend_key=True): """Create a remote resource based on this instance. @@ -665,6 +689,7 @@ def create(self, session, prepend_key=True): if not self.allow_create: raise exceptions.MethodNotSupported(self, "create") + session = self._get_session(session) if self.create_method == 'PUT': request = self._prepare_request(requires_id=True, prepend_key=prepend_key) @@ -697,6 +722,7 @@ def get(self, session, requires_id=True, error_message=None): raise exceptions.MethodNotSupported(self, "get") request = self._prepare_request(requires_id=requires_id) + session = self._get_session(session) response = session.get(request.url) kwargs = {} if error_message: @@ -720,6 +746,7 @@ def head(self, session): request = self._prepare_request() + session = self._get_session(session) response = session.head(request.url, headers={"Accept": ""}) @@ -750,6 +777,7 @@ def update(self, session, prepend_key=True, has_body=True): raise exceptions.MethodNotSupported(self, "update") request = self._prepare_request(prepend_key=prepend_key) + session = self._get_session(session) if self.update_method == 'PATCH': response = session.patch( @@ -781,6 +809,7 @@ def delete(self, session, error_message=None): raise exceptions.MethodNotSupported(self, "delete") request = self._prepare_request() + session = self._get_session(session) response = session.delete(request.url, headers={"Accept": ""}) @@ -824,6 +853,7 @@ def list(cls, session, paginated=False, **params): """ if not cls.allow_list: raise exceptions.MethodNotSupported(cls, "list") + session = cls._get_session(session) expected_params = utils.get_string_format_keys(cls.base_path) expected_params += cls._query_mapping._mapping.keys() diff --git a/openstack/tests/unit/compute/v2/test_limits.py b/openstack/tests/unit/compute/v2/test_limits.py index 2b0a97dfe..a5cd5d66d 100644 --- a/openstack/tests/unit/compute/v2/test_limits.py +++ b/openstack/tests/unit/compute/v2/test_limits.py @@ -12,6 +12,7 @@ import copy +from keystoneauth1 import adapter import mock import testtools @@ -146,7 +147,7 @@ def test_basic(self): self.assertFalse(sot.allow_list) def test_get(self): - sess = mock.Mock() + sess = mock.Mock(spec=adapter.Adapter) resp = mock.Mock() sess.get.return_value = resp resp.json.return_value = copy.deepcopy(LIMITS_BODY) diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 14e2cf1ed..2ad47095c 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -13,6 +13,7 @@ import json import operator +from keystoneauth1 import adapter import mock import requests import testtools @@ -100,7 +101,7 @@ def setUp(self): self.resp = mock.Mock() self.resp.body = None self.resp.json = mock.Mock(return_value=self.resp.body) - self.sess = mock.Mock() + self.sess = mock.Mock(spec=adapter.Adapter) self.sess.post = mock.Mock(return_value=self.resp) def test_basic(self): diff --git a/openstack/tests/unit/network/v2/test_floating_ip.py b/openstack/tests/unit/network/v2/test_floating_ip.py index 0ae513989..81a84fed2 100644 --- a/openstack/tests/unit/network/v2/test_floating_ip.py +++ b/openstack/tests/unit/network/v2/test_floating_ip.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from keystoneauth1 import adapter import mock import testtools @@ -67,7 +68,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['subnet_id'], sot.subnet_id) def test_find_available(self): - mock_session = mock.Mock() + mock_session = mock.Mock(spec=adapter.Adapter) mock_session.get_filter = mock.Mock(return_value={}) data = {'id': 'one', 'floating_ip_address': '10.0.0.1'} fake_response = mock.Mock() @@ -85,7 +86,7 @@ def test_find_available(self): params={'port_id': ''}) def test_find_available_nada(self): - mock_session = mock.Mock() + mock_session = mock.Mock(spec=adapter.Adapter) fake_response = mock.Mock() body = {floating_ip.FloatingIP.resources_key: []} fake_response.json = mock.Mock(return_value=body) diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index ee0467ade..80d98a8a1 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -12,7 +12,7 @@ import itertools -from keystoneauth1 import session +from keystoneauth1 import adapter import mock import requests import six @@ -944,7 +944,7 @@ class Test(resource.Resource): self.sot._prepare_request = mock.Mock(return_value=self.request) self.sot._translate_response = mock.Mock() - self.session = mock.Mock(spec=session.Session) + self.session = mock.Mock(spec=adapter.Adapter) self.session.create = mock.Mock(return_value=self.response) self.session.get = mock.Mock(return_value=self.response) self.session.put = mock.Mock(return_value=self.response) From a5c9aa21fd3dff1afdc89991b526d5e83a23d9b7 Mon Sep 17 00:00:00 2001 From: "Yuanbin.Chen" Date: Tue, 30 Jan 2018 09:37:29 +0800 Subject: [PATCH 1958/3836] Rename unit test cluster to clustering In this patch, cluster is renamed clustering: https://review.openstack.org/#/c/519029/ Change-Id: Ied1ab2ad3d9c00cc0ebccbafe1fcd6e8094b7371 Signed-off-by: Yuanbin.Chen --- openstack/tests/unit/{cluster => clustering}/__init__.py | 0 .../tests/unit/{cluster => clustering}/test_cluster_service.py | 0 openstack/tests/unit/{cluster => clustering}/test_version.py | 0 openstack/tests/unit/{cluster => clustering}/v1/__init__.py | 0 openstack/tests/unit/{cluster => clustering}/v1/test_action.py | 0 .../tests/unit/{cluster => clustering}/v1/test_build_info.py | 0 openstack/tests/unit/{cluster => clustering}/v1/test_cluster.py | 0 .../tests/unit/{cluster => clustering}/v1/test_cluster_attr.py | 0 .../tests/unit/{cluster => clustering}/v1/test_cluster_policy.py | 0 openstack/tests/unit/{cluster => clustering}/v1/test_event.py | 0 openstack/tests/unit/{cluster => clustering}/v1/test_node.py | 0 openstack/tests/unit/{cluster => clustering}/v1/test_policy.py | 0 .../tests/unit/{cluster => clustering}/v1/test_policy_type.py | 0 openstack/tests/unit/{cluster => clustering}/v1/test_profile.py | 0 .../tests/unit/{cluster => clustering}/v1/test_profile_type.py | 0 openstack/tests/unit/{cluster => clustering}/v1/test_proxy.py | 0 openstack/tests/unit/{cluster => clustering}/v1/test_receiver.py | 0 openstack/tests/unit/{cluster => clustering}/v1/test_service.py | 0 18 files changed, 0 insertions(+), 0 deletions(-) rename openstack/tests/unit/{cluster => clustering}/__init__.py (100%) rename openstack/tests/unit/{cluster => clustering}/test_cluster_service.py (100%) rename openstack/tests/unit/{cluster => clustering}/test_version.py (100%) rename openstack/tests/unit/{cluster => clustering}/v1/__init__.py (100%) rename openstack/tests/unit/{cluster => clustering}/v1/test_action.py (100%) rename openstack/tests/unit/{cluster => clustering}/v1/test_build_info.py (100%) rename openstack/tests/unit/{cluster => clustering}/v1/test_cluster.py (100%) rename openstack/tests/unit/{cluster => clustering}/v1/test_cluster_attr.py (100%) rename openstack/tests/unit/{cluster => clustering}/v1/test_cluster_policy.py (100%) rename openstack/tests/unit/{cluster => clustering}/v1/test_event.py (100%) rename openstack/tests/unit/{cluster => clustering}/v1/test_node.py (100%) rename openstack/tests/unit/{cluster => clustering}/v1/test_policy.py (100%) rename openstack/tests/unit/{cluster => clustering}/v1/test_policy_type.py (100%) rename openstack/tests/unit/{cluster => clustering}/v1/test_profile.py (100%) rename openstack/tests/unit/{cluster => clustering}/v1/test_profile_type.py (100%) rename openstack/tests/unit/{cluster => clustering}/v1/test_proxy.py (100%) rename openstack/tests/unit/{cluster => clustering}/v1/test_receiver.py (100%) rename openstack/tests/unit/{cluster => clustering}/v1/test_service.py (100%) diff --git a/openstack/tests/unit/cluster/__init__.py b/openstack/tests/unit/clustering/__init__.py similarity index 100% rename from openstack/tests/unit/cluster/__init__.py rename to openstack/tests/unit/clustering/__init__.py diff --git a/openstack/tests/unit/cluster/test_cluster_service.py b/openstack/tests/unit/clustering/test_cluster_service.py similarity index 100% rename from openstack/tests/unit/cluster/test_cluster_service.py rename to openstack/tests/unit/clustering/test_cluster_service.py diff --git a/openstack/tests/unit/cluster/test_version.py b/openstack/tests/unit/clustering/test_version.py similarity index 100% rename from openstack/tests/unit/cluster/test_version.py rename to openstack/tests/unit/clustering/test_version.py diff --git a/openstack/tests/unit/cluster/v1/__init__.py b/openstack/tests/unit/clustering/v1/__init__.py similarity index 100% rename from openstack/tests/unit/cluster/v1/__init__.py rename to openstack/tests/unit/clustering/v1/__init__.py diff --git a/openstack/tests/unit/cluster/v1/test_action.py b/openstack/tests/unit/clustering/v1/test_action.py similarity index 100% rename from openstack/tests/unit/cluster/v1/test_action.py rename to openstack/tests/unit/clustering/v1/test_action.py diff --git a/openstack/tests/unit/cluster/v1/test_build_info.py b/openstack/tests/unit/clustering/v1/test_build_info.py similarity index 100% rename from openstack/tests/unit/cluster/v1/test_build_info.py rename to openstack/tests/unit/clustering/v1/test_build_info.py diff --git a/openstack/tests/unit/cluster/v1/test_cluster.py b/openstack/tests/unit/clustering/v1/test_cluster.py similarity index 100% rename from openstack/tests/unit/cluster/v1/test_cluster.py rename to openstack/tests/unit/clustering/v1/test_cluster.py diff --git a/openstack/tests/unit/cluster/v1/test_cluster_attr.py b/openstack/tests/unit/clustering/v1/test_cluster_attr.py similarity index 100% rename from openstack/tests/unit/cluster/v1/test_cluster_attr.py rename to openstack/tests/unit/clustering/v1/test_cluster_attr.py diff --git a/openstack/tests/unit/cluster/v1/test_cluster_policy.py b/openstack/tests/unit/clustering/v1/test_cluster_policy.py similarity index 100% rename from openstack/tests/unit/cluster/v1/test_cluster_policy.py rename to openstack/tests/unit/clustering/v1/test_cluster_policy.py diff --git a/openstack/tests/unit/cluster/v1/test_event.py b/openstack/tests/unit/clustering/v1/test_event.py similarity index 100% rename from openstack/tests/unit/cluster/v1/test_event.py rename to openstack/tests/unit/clustering/v1/test_event.py diff --git a/openstack/tests/unit/cluster/v1/test_node.py b/openstack/tests/unit/clustering/v1/test_node.py similarity index 100% rename from openstack/tests/unit/cluster/v1/test_node.py rename to openstack/tests/unit/clustering/v1/test_node.py diff --git a/openstack/tests/unit/cluster/v1/test_policy.py b/openstack/tests/unit/clustering/v1/test_policy.py similarity index 100% rename from openstack/tests/unit/cluster/v1/test_policy.py rename to openstack/tests/unit/clustering/v1/test_policy.py diff --git a/openstack/tests/unit/cluster/v1/test_policy_type.py b/openstack/tests/unit/clustering/v1/test_policy_type.py similarity index 100% rename from openstack/tests/unit/cluster/v1/test_policy_type.py rename to openstack/tests/unit/clustering/v1/test_policy_type.py diff --git a/openstack/tests/unit/cluster/v1/test_profile.py b/openstack/tests/unit/clustering/v1/test_profile.py similarity index 100% rename from openstack/tests/unit/cluster/v1/test_profile.py rename to openstack/tests/unit/clustering/v1/test_profile.py diff --git a/openstack/tests/unit/cluster/v1/test_profile_type.py b/openstack/tests/unit/clustering/v1/test_profile_type.py similarity index 100% rename from openstack/tests/unit/cluster/v1/test_profile_type.py rename to openstack/tests/unit/clustering/v1/test_profile_type.py diff --git a/openstack/tests/unit/cluster/v1/test_proxy.py b/openstack/tests/unit/clustering/v1/test_proxy.py similarity index 100% rename from openstack/tests/unit/cluster/v1/test_proxy.py rename to openstack/tests/unit/clustering/v1/test_proxy.py diff --git a/openstack/tests/unit/cluster/v1/test_receiver.py b/openstack/tests/unit/clustering/v1/test_receiver.py similarity index 100% rename from openstack/tests/unit/cluster/v1/test_receiver.py rename to openstack/tests/unit/clustering/v1/test_receiver.py diff --git a/openstack/tests/unit/cluster/v1/test_service.py b/openstack/tests/unit/clustering/v1/test_service.py similarity index 100% rename from openstack/tests/unit/cluster/v1/test_service.py rename to openstack/tests/unit/clustering/v1/test_service.py From 7c5266d495524c0310a7ec20f7d60e0bbaf5189c Mon Sep 17 00:00:00 2001 From: Hunt Xu Date: Tue, 30 Jan 2018 15:16:54 +0800 Subject: [PATCH 1959/3836] orchestration: fix typo in doc Change-Id: I83d56c0210e1f091773dd7c0dc3d16af25d6eed2 Closes-Bug: #1706400 --- openstack/orchestration/v1/_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index d83e469a7..161248c1a 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -27,7 +27,7 @@ class Proxy(proxy.BaseProxy): def create_stack(self, preview=False, **attrs): """Create a new stack from attributes - :param bool perview: When ``True``, returns + :param bool preview: When ``True``, returns an :class:`~openstack.orchestration.v1.stack.StackPreview` object, otherwise an :class:`~openstack.orchestration.v1.stack.Stack` object. From ff5b6bc0c9cca89d6c66ae311a9deecea6b2c28b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 29 Jan 2018 14:40:20 -0600 Subject: [PATCH 1960/3836] Add a descriptor object for each service proxy The end-goal here is to be able to do version discovery rather than relying on config and in-code version defaults for what gets attached to the connection as the service proxy for a service. However, since we're currently constructing Adapter objects at instantation time we'd have to authenticate AND do version discovery on every service when we create a Connection to be able to do that. That would be bad. Add a Descriptor class that creates the Proxy object on-demand. That is, when someone does "conn.compute", a Proxy will be created and returned. To support doing that without a ton of duplicate copy-pasta for each service, add a metaclass to Connection which reads os-service-types and does the import lookup / BaseProxy fallback as part of Connection class creation. One of the upsides to this is that we can add docstrings to the service descriptor objects - meaning that the docs for Connection actually list the proxy objects. While we're in here, fix a NOTE in a connection doc string and add a reference to BaseProxy in the docs so that it'll show up in the docs too. Change-Id: I3bef5de60b848146fc8563d853774769d0875c65 --- doc/source/user/index.rst | 7 +- openstack/_meta.py | 126 +++++++++++++++++++++++++++++++ openstack/connection.py | 29 ++----- openstack/proxy.py | 1 + openstack/service_description.py | 121 +++++++++++------------------ openstack/service_filter.py | 10 +++ 6 files changed, 194 insertions(+), 100 deletions(-) create mode 100644 openstack/_meta.py diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index 96b6c80a4..0a5d36eb5 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -73,7 +73,12 @@ OpenStack services. connection Once you have a *Connection* instance, the following services may be exposed -to you. The combination of your ``CloudRegion`` and the catalog of the cloud +to you via the :class:`~openstack.proxy.BaseProxy` interface. + +.. autoclass:: openstack.proxy.BaseProxy + :members: + +The combination of your ``CloudRegion`` and the catalog of the cloud in question control which services are exposed, but listed below are the ones provided by the SDK. diff --git a/openstack/_meta.py b/openstack/_meta.py new file mode 100644 index 000000000..da0bf6c83 --- /dev/null +++ b/openstack/_meta.py @@ -0,0 +1,126 @@ +# Copyright 2018 Red Hat, 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 importlib +import warnings + +import os_service_types + +from openstack import _log +from openstack import proxy +from openstack import service_description + +_logger = _log.setup_logging('openstack') +_service_type_manager = os_service_types.ServiceTypes() +_DOC_TEMPLATE = ( + ":class:`{class_name}` for {service_type} aka {project}") +_PROXY_TEMPLATE = """Proxy for {service_type} aka {project} + +This proxy object could be an instance of +{class_doc_strings} +depending on client configuration and which version of the service is +found on remotely on the cloud. +""" + + +class ConnectionMeta(type): + def __new__(meta, name, bases, dct): + for service in _service_type_manager.services: + service_type = service['service_type'] + if service_type == 'ec2-api': + # NOTE(mordred) It doesn't make any sense to use ec2-api + # from openstacksdk. The credentials API calls are all calls + # on identity endpoints. + continue + desc_class = service_description.ServiceDescription + service_filter_class = _find_service_filter_class(service_type) + descriptor_args = {'service_type': service_type} + if service_filter_class: + desc_class = service_description.OpenStackServiceDescription + descriptor_args['service_filter_class'] = service_filter_class + class_names = service_filter_class._get_proxy_class_names() + if len(class_names) == 1: + doc = _DOC_TEMPLATE.format( + class_name="{service_type} Proxy <{name}>".format( + service_type=service_type, name=class_names[0]), + **service) + else: + class_doc_strings = "\n".join([ + ":class:`{class_name}`".format(class_name=class_name) + for class_name in class_names]) + doc = _PROXY_TEMPLATE.format( + class_doc_strings=class_doc_strings, **service) + else: + descriptor_args['proxy_class'] = proxy.BaseProxy + doc = _DOC_TEMPLATE.format( + class_name='~openstack.proxy.BaseProxy', **service) + descriptor = desc_class(**descriptor_args) + descriptor.__doc__ = doc + dct[service_type.replace('-', '_')] = descriptor + + # Register the descriptor class with every known alias. Don't + # add doc strings though - although they are supported, we don't + # want to give anybody any bad ideas. Making a second descriptor + # does not introduce runtime cost as the descriptors all use + # the same _proxies dict on the instance. + for alias_name in _get_aliases(service_type): + if alias_name[-1].isdigit(): + continue + alias_descriptor = desc_class(**descriptor_args) + dct[alias_name.replace('-', '_')] = alias_descriptor + return super(ConnectionMeta, meta).__new__(meta, name, bases, dct) + + +def _get_aliases(service_type, aliases=None): + # We make connection attributes for all official real type names + # and aliases. Three services have names they were called by in + # openstacksdk that are not covered by Service Types Authority aliases. + # Include them here - but take heed, no additional values should ever + # be added to this list. + # that were only used in openstacksdk resource naming. + LOCAL_ALIASES = { + 'baremetal': 'bare_metal', + 'block_storage': 'block_store', + 'clustering': 'cluster', + } + all_types = set(_service_type_manager.get_aliases(service_type)) + if aliases: + all_types.update(aliases) + if service_type in LOCAL_ALIASES: + all_types.add(LOCAL_ALIASES[service_type]) + return all_types + + +def _find_service_filter_class(service_type): + package_name = 'openstack.{service_type}'.format( + service_type=service_type).replace('-', '_') + module_name = service_type.replace('-', '_') + '_service' + class_name = ''.join( + [part.capitalize() for part in module_name.split('_')]) + try: + import_name = '.'.join([package_name, module_name]) + service_filter_module = importlib.import_module(import_name) + except ImportError as e: + # ImportWarning is ignored by default. This warning is here + # as an opt-in for people trying to figure out why something + # didn't work. + warnings.warn( + "Could not import {service_type} service filter: {e}".format( + service_type=service_type, e=str(e)), + ImportWarning) + return None + # There are no cases in which we should have a module but not the class + # inside it. + service_filter_class = getattr(service_filter_module, class_name) + return service_filter_class diff --git a/openstack/connection.py b/openstack/connection.py index 763621c09..5f454480f 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -82,11 +82,11 @@ import warnings import keystoneauth1.exceptions -import os_service_types import requestsexceptions import six from openstack import _log +from openstack import _meta from openstack import config as _config from openstack import exceptions from openstack import service_description @@ -128,7 +128,7 @@ def from_config(cloud=None, config=None, options=None, **kwargs): return Connection(config=config) -class Connection(object): +class Connection(six.with_metaclass(_meta.ConnectionMeta)): def __init__(self, cloud=None, config=None, session=None, app_name=None, app_version=None, @@ -219,11 +219,7 @@ def __init__(self, cloud=None, config=None, session=None, # to a Resource method's session argument. self.session._sdk_connection = self - service_type_manager = os_service_types.ServiceTypes() - for service in service_type_manager.services: - self.add_service( - service_description.OpenStackServiceDescription( - service, self.config)) + self._proxies = {} def add_service(self, service): """Add a service to the Connection. @@ -245,29 +241,16 @@ class contained in # If we don't have a proxy, just instantiate BaseProxy so that # we get an adapter. if isinstance(service, six.string_types): - service_type = service - service = service_description.ServiceDescription(service_type) - else: - service_type = service.service_type - proxy_object = service.proxy_class( - session=self.config.get_session(), - task_manager=self.task_manager, - allow_version_hack=True, - service_type=self.config.get_service_type(service_type), - service_name=self.config.get_service_name(service_type), - interface=self.config.get_interface(service_type), - region_name=self.config.region_name, - version=self.config.get_api_version(service_type), - ) + service = service_description.ServiceDescription(service) # Register the proxy class with every known alias for attr_name in service.all_types: - setattr(self, attr_name.replace('-', '_'), proxy_object) + setattr(self, attr_name.replace('-', '_'), service) def authorize(self): """Authorize this Connection - **NOTE**: This method is optional. When an application makes a call + .. note:: This method is optional. When an application makes a call to any OpenStack service, this method allows you to request a token manually before attempting to do anything else. diff --git a/openstack/proxy.py b/openstack/proxy.py index 6954c823a..4ad09909a 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -41,6 +41,7 @@ def check(self, expected, actual=None, *args, **kwargs): class BaseProxy(_adapter.OpenStackSDKAdapter): + """Represents a service.""" def _get_resource(self, resource_type, value, **attrs): """Get a resource object to work on diff --git a/openstack/service_description.py b/openstack/service_description.py index c141318e9..ef3647bd1 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -17,7 +17,6 @@ ] import importlib -import warnings import os_service_types @@ -28,26 +27,6 @@ _service_type_manager = os_service_types.ServiceTypes() -def _get_all_types(service_type, aliases=None): - # We make connection attributes for all official real type names - # and aliases. Three services have names they were called by in - # openstacksdk that are not covered by Service Types Authority aliases. - # Include them here - but take heed, no additional values should ever - # be added to this list. - # that were only used in openstacksdk resource naming. - LOCAL_ALIASES = { - 'baremetal': 'bare_metal', - 'block_storage': 'block_store', - 'clustering': 'cluster', - } - all_types = set(_service_type_manager.get_all_types(service_type)) - if aliases: - all_types.update(aliases) - if service_type in LOCAL_ALIASES: - all_types.add(LOCAL_ALIASES[service_type]) - return all_types - - class ServiceDescription(object): #: Proxy class for this service @@ -85,11 +64,13 @@ def __init__(self, service_type, proxy_class=None, aliases=None): Optional list of aliases, if there is more than one name that might be used to register the service in the catalog. """ - self.service_type = service_type + self.service_type = service_type or self.service_type self.proxy_class = proxy_class or self.proxy_class - self.all_types = _get_all_types(service_type, aliases) - - self._validate_proxy_class() + if self.proxy_class: + self._validate_proxy_class() + self.aliases = aliases or self.aliases + self.all_types = [service_type] + self.aliases + self._proxy = None def _validate_proxy_class(self): if not issubclass(self.proxy_class, proxy.BaseProxy): @@ -98,10 +79,36 @@ def _validate_proxy_class(self): module=self.proxy_class.__module__, proxy_class=self.proxy_class.__name__)) + def get_proxy_class(self, config): + return self.proxy_class + + def __get__(self, instance, owner): + if instance is None: + return self + if self.service_type not in instance._proxies: + config = instance.config + proxy_class = self.get_proxy_class(config) + instance._proxies[self.service_type] = proxy_class( + session=instance.config.get_session(), + task_manager=instance.task_manager, + allow_version_hack=True, + service_type=config.get_service_type(self.service_type), + service_name=config.get_service_name(self.service_type), + interface=config.get_interface(self.service_type), + region_name=config.region_name, + version=config.get_api_version(self.service_type) + ) + return instance._proxies[self.service_type] + + def __set__(self, instance, value): + raise AttributeError('Service Descriptors cannot be set') + + def __delete__(self, instance): + raise AttributeError('Service Descriptors cannot be deleted') -class OpenStackServiceDescription(ServiceDescription): - def __init__(self, service, config): +class OpenStackServiceDescription(ServiceDescription): + def __init__(self, service_filter_class, *args, **kwargs): """Official OpenStack ServiceDescription. The OpenStackServiceDescription class is a helper class for @@ -111,57 +118,19 @@ def __init__(self, service, config): It finds the proxy_class by looking in the openstacksdk tree for appropriately named modules. - :param dict service: - A service dict as found in `os_service_types.ServiceTypes.services` - :param openstack.config.cloud_region.CloudRegion config: - ConfigRegion for the connection. + :param service_filter_class: + A subclass of :class:`~openstack.service_filter.ServiceFilter` """ - super(OpenStackServiceDescription, self).__init__( - service['service_type']) - self.config = config - service_filter = self._get_service_filter() - if service_filter: - module_name = service_filter.get_module() + "._proxy" - module = importlib.import_module(module_name) - self.proxy_class = getattr(module, "Proxy") - - def _get_service_filter(self): - service_filter_class = None - for service_type in self.all_types: - service_filter_class = self._find_service_filter_class() - if service_filter_class: - break - if not service_filter_class: - return None + super(OpenStackServiceDescription, self).__init__(*args, **kwargs) + self._service_filter_class = service_filter_class + + def get_proxy_class(self, config): # TODO(mordred) Replace this with proper discovery - version_string = self.config.get_api_version(self.service_type) + version_string = config.get_api_version(self.service_type) version = None if version_string: version = 'v{version}'.format(version=version_string[0]) - return service_filter_class(version=version) - - def _find_service_filter_class(self): - package_name = 'openstack.{service_type}'.format( - service_type=self.service_type).replace('-', '_') - module_name = self.service_type.replace('-', '_') + '_service' - class_name = ''.join( - [part.capitalize() for part in module_name.split('_')]) - try: - import_name = '.'.join([package_name, module_name]) - service_filter_module = importlib.import_module(import_name) - except ImportError as e: - # ImportWarning is ignored by default. This warning is here - # as an opt-in for people trying to figure out why something - # didn't work. - warnings.warn( - "Could not import {service_type} service filter: {e}".format( - service_type=self.service_type, e=str(e)), - ImportWarning) - return None - service_filter_class = getattr(service_filter_module, class_name, None) - if not service_filter_class: - _logger.warn( - 'Unable to find class %s in module for service %s', - class_name, self.service_type) - return None - return service_filter_class + service_filter = self._service_filter_class(version=version) + module_name = service_filter.get_module() + "._proxy" + module = importlib.import_module(module_name) + return getattr(module, "Proxy") diff --git a/openstack/service_filter.py b/openstack/service_filter.py index a0f60e3b6..b5c4fe82d 100644 --- a/openstack/service_filter.py +++ b/openstack/service_filter.py @@ -92,6 +92,16 @@ def __init__(self, service_type, interface=PUBLIC, region=None, self['api_version'] = api_version self['requires_project_id'] = requires_project_id + @classmethod + def _get_proxy_class_names(cls): + names = [] + module_name = ".".join(cls.__module__.split('.')[:-1]) + for version in cls.valid_versions: + names.append("{module}.{version}._proxy.Proxy".format( + module=module_name, + version=version.module)) + return names + @property def service_type(self): return self['service_type'] From aff2b6ab4039c8c7edfbb0286e1c2672eee400cb Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 30 Jan 2018 06:50:18 -0600 Subject: [PATCH 1961/3836] Ensure Connection can be made from keyword arguments Although driving configuration from clouds.yaml and environment variables is super handy for many use cases, there are also plenty where avoiding config files or environment variables is important. Add a flag, "load_envvars" which defaults to True but can be disabled. Update the Connection constructor to set load_yaml_config and load_envvars to False if a named cloud is not given. Update the docs to make it clear which form should be used for which use case. Also, move the profile backwards compat documentation to its own document so that it doesn't present needless complexity to users. Change-Id: I6e05da5e73aff4143550e1d18fb0f743d51f2b70 --- doc/source/user/config/configuration.rst | 26 +-- doc/source/user/connection.rst | 185 +------------------ doc/source/user/index.rst | 24 ++- doc/source/user/transition_from_profile.rst | 186 ++++++++++++++++++++ openstack/__init__.py | 39 +++- openstack/config/__init__.py | 18 +- openstack/config/cloud_region.py | 22 ++- openstack/config/loader.py | 33 ++-- openstack/connection.py | 169 ++++++++++++++---- openstack/tests/unit/test_connection.py | 3 + 10 files changed, 439 insertions(+), 266 deletions(-) create mode 100644 doc/source/user/transition_from_profile.rst diff --git a/doc/source/user/config/configuration.rst b/doc/source/user/config/configuration.rst index b766921c8..19460c8dc 100644 --- a/doc/source/user/config/configuration.rst +++ b/doc/source/user/config/configuration.rst @@ -1,17 +1,19 @@ -=========================================== - Configuring os-client-config Applications -=========================================== +.. _openstack-config: + +======================================== + Configuring OpenStack SDK Applications +======================================== .. _config-environment-variables: Environment Variables --------------------- -`os-client-config` honors all of the normal `OS_*` variables. It does not +`openstacksdk` honors all of the normal `OS_*` variables. It does not provide backwards compatibility to service-specific variables such as `NOVA_USERNAME`. -If you have OpenStack environment variables set, `os-client-config` will +If you have OpenStack environment variables set, `openstacksdk` will produce a cloud config object named `envvars` containing your values from the environment. If you don't like the name `envvars`, that's ok, you can override it by setting `OS_CLOUD_NAME`. @@ -29,7 +31,7 @@ for trove set Config Files ------------ -`os-client-config` will look for a file called `clouds.yaml` in the following +`openstacksdk` will look for a file called `clouds.yaml` in the following locations: * Current Directory @@ -58,7 +60,7 @@ Site Specific File Locations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In addition to `~/.config/openstack` and `/etc/openstack` - some platforms -have other locations they like to put things. `os-client-config` will also +have other locations they like to put things. `openstacksdk` will also look in an OS specific config dir * `USER_CONFIG_DIR` @@ -111,7 +113,7 @@ You may note a few things. First, since `auth_url` settings are silly and embarrassingly ugly, known cloud vendor profile information is included and may be referenced by name. One of the benefits of that is that `auth_url` isn't the only thing the vendor defaults contain. For instance, since -Rackspace lists `rax:database` as the service type for trove, `os-client-config` +Rackspace lists `rax:database` as the service type for trove, `openstacksdk` knows that so that you don't have to. In case the cloud vendor profile is not available, you can provide one called `clouds-public.yaml`, following the same location rules previously mentioned for the config files. @@ -129,7 +131,7 @@ Auth Settings ------------- Keystone has auth plugins - which means it's not possible to know ahead of time -which auth settings are needed. `os-client-config` sets the default plugin type +which auth settings are needed. `openstacksdk` sets the default plugin type to `password`, which is what things all were before plugins came about. In order to facilitate validation of values, all of the parameters that exist as a result of a chosen plugin need to go into the auth dict. For password @@ -167,7 +169,7 @@ file. SSL Settings ------------ -When the access to a cloud is done via a secure connection, `os-client-config` +When the access to a cloud is done via a secure connection, `openstacksdk` will always verify the SSL cert by default. This can be disabled by setting `verify` to `False`. In case the cert is signed by an unknown CA, a specific cacert can be provided via `cacert`. **WARNING:** `verify` will always have @@ -195,7 +197,7 @@ Cache Settings -------------- Accessing a cloud is often expensive, so it's quite common to want to do some -client-side caching of those operations. To facilitate that, `os-client-config` +client-side caching of those operations. To facilitate that, `openstacksdk` understands passing through cache settings to dogpile.cache, with the following behaviors: @@ -209,7 +211,7 @@ times on a per-resource basis by passing values, in seconds to an expiration mapping keyed on the singular name of the resource. A value of `-1` indicates that the resource should never expire. -`os-client-config` does not actually cache anything itself, but it collects +`openstacksdk` does not actually cache anything itself, but it collects and presents the cache information so that your various applications that are connecting to OpenStack can share a cache should you desire. diff --git a/doc/source/user/connection.rst b/doc/source/user/connection.rst index 6f41bee70..e5afce0d8 100644 --- a/doc/source/user/connection.rst +++ b/doc/source/user/connection.rst @@ -13,184 +13,13 @@ Connection Object :members: -Transition from Profile -======================= +Transitioning from Profile +-------------------------- -.. note:: This section describes migrating code from a previous interface of - python-openstacksdk and can be ignored by people writing new code. +Support exists for users coming from older releases of OpenStack SDK who have +been using the :class:`~openstack.profile.Profile` interface. -If you have code that currently uses the ``openstack.profile.Profile`` object -and/or an ``authenticator`` instance from an object based on -``openstack.auth.base.BaseAuthPlugin``, that code should be updated to use the -`openstack.config.cloud_region.CloudRegion` object instead. +.. toctree:: + :maxdepth: 1 -Writing Code that Works with Both ---------------------------------- - -These examples should all work with both the old and new interface, with one -caveat. With the old interface, the ``CloudConfig`` object comes from the -``os-client-config`` library, and in the new interface that has been moved -into the SDK. In order to write code that works with both the old and new -interfaces, use the following code to import the config namespace: - -.. code-block:: python - - try: - from openstack import config as occ - except ImportError: - from os_client_config import config as occ - -The examples will assume that the config module has been imported in that -manner. - -.. note:: Yes, there is an easier and less verbose way to do all of these. - These are verbose to handle both the old and new interfaces in the - same codebase. - -Replacing authenticator ------------------------ - -There is no direct replacement for ``openstack.auth.base.BaseAuthPlugin``. -``python-openstacksdk`` uses the `keystoneauth`_ library for authentication -and HTTP interactions. `keystoneauth`_ has `auth plugins`_ that can be used -to control how authentication is done. The ``auth_type`` config parameter -can be set to choose the correct authentication method to be used. - -Replacing Profile ------------------ - -The right way to replace the use of ``openstack.profile.Profile`` depends -a bit on what you're trying to accomplish. Common patterns are listed below, -but in general the approach is either to pass a cloud name to the -`openstack.connection.Connection` constructor, or to construct a -`openstack.config.cloud_region.CloudRegion` object and pass it to the -constructor. - -All of the examples on this page assume that you want to support old and -new interfaces simultaneously. There are easier and less verbose versions -of each that are available if you can just make a clean transition. - -Getting a Connection to a named cloud from clouds.yaml ------------------------------------------------------- - -If you want is to construct a `openstack.connection.Connection` based on -parameters configured in a ``clouds.yaml`` file, or from environment variables: - -.. code-block:: python - - import openstack.connection - - conn = connection.from_config(cloud_name='name-of-cloud-you-want') - -Getting a Connection from python arguments avoiding clouds.yaml ---------------------------------------------------------------- - -If, on the other hand, you want to construct a -`openstack.connection.Connection`, but are in a context where reading config -from a clouds.yaml file is undesirable, such as inside of a Service: - -* create a `openstack.config.loader.OpenStackConfig` object, telling - it to not load yaml files. Optionally pass an ``app_name`` and - ``app_version`` which will be added to user-agent strings. -* get a `openstack.config.cloud_region.CloudRegion` object from it -* get a `openstack.connection.Connection` - -.. code-block:: python - - try: - from openstack import config as occ - except ImportError: - from os_client_config import config as occ - from openstack import connection - - loader = occ.OpenStackConfig( - load_yaml_files=False, - app_name='spectacular-app', - app_version='1.0') - cloud_region = loader.get_one_cloud( - region_name='my-awesome-region', - auth_type='password', - auth=dict( - auth_url='https://auth.example.com', - username='amazing-user', - user_domain_name='example-domain', - project_name='astounding-project', - user_project_name='example-domain', - password='super-secret-password', - )) - conn = connection.from_config(cloud_config=cloud_region) - -.. note:: app_name and app_version are completely optional, and auth_type - defaults to 'password'. They are shown here for clarity as to - where they should go if they want to be set. - -Getting a Connection from python arguments and optionally clouds.yaml ---------------------------------------------------------------------- - -If you want to make a connection from python arguments and want to allow -one of them to optionally be ``cloud`` to allow selection of a named cloud, -it's essentially the same as the previous example, except without -``load_yaml_files=False``. - -.. code-block:: python - - try: - from openstack import config as occ - except ImportError: - from os_client_config import config as occ - from openstack import connection - - loader = occ.OpenStackConfig( - app_name='spectacular-app', - app_version='1.0') - cloud_region = loader.get_one_cloud( - region_name='my-awesome-region', - auth_type='password', - auth=dict( - auth_url='https://auth.example.com', - username='amazing-user', - user_domain_name='example-domain', - project_name='astounding-project', - user_project_name='example-domain', - password='super-secret-password', - )) - conn = connection.from_config(cloud_config=cloud_region) - -Parameters to get_one_cloud ---------------------------- - -The most important things to note are: - -* ``auth_type`` specifies which kind of authentication plugin to use. It - controls how authentication is done, as well as what parameters are required. -* ``auth`` is a dictionary containing the parameters needed by the auth plugin. - The most common information it needs are user, project, domain, auth_url - and password. -* The rest of the keyword arguments to - ``openstack.config.loader.OpenStackConfig.get_one_cloud`` are either - parameters needed by the `keystoneauth Session`_ object, which control how - HTTP connections are made, or parameters needed by the - `keystoneauth Adapter`_ object, which control how services are found in the - Keystone Catalog. - -For `keystoneauth Adapter`_ parameters, since there is one -`openstack.connection.Connection` object but many services, per-service -parameters are formed by using the official ``service_type`` of the service -in question. For instance, to override the endpoint for the ``compute`` -service, the parameter ``compute_endpoint_override`` would be used. - -``region_name`` in ``openstack.profile.Profile`` was a per-service parameter. -This is no longer a valid concept. An `openstack.connection.Connection` is a -connection to a region of a cloud. If you are in an extreme situation where -you have one service in one region and a different service in a different -region, you must use two different `openstack.connection.Connection` objects. - -.. note:: service_type, although a parameter for keystoneauth1.adapter.Adapter, - is not a valid parameter for get_one_cloud. service_type is the key - by which services are referred, so saying - 'compute_service_type="henry"' doesn't have any meaning. - -.. _keystoneauth: https://docs.openstack.org/keystoneauth/latest/ -.. _auth plugins: https://docs.openstack.org/keystoneauth/latest/authentication-plugins.html -.. _keystoneauth Adapter: https://docs.openstack.org/keystoneauth/latest/api/keystoneauth1.html#keystoneauth1.adapter.Adapter -.. _keystoneauth Session: https://docs.openstack.org/keystoneauth/latest/api/keystoneauth1.html#keystoneauth1.session.Session + transition_from_profile diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index 0a5d36eb5..90ccb90a4 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -61,7 +61,7 @@ The Cloud Abstraction layer has a data model. model Connection Interface -******************** +~~~~~~~~~~~~~~~~~~~~ A *Connection* instance maintains your cloud config, session and authentication information providing you with a set of higher-level interfaces to work with @@ -72,15 +72,23 @@ OpenStack services. connection -Once you have a *Connection* instance, the following services may be exposed -to you via the :class:`~openstack.proxy.BaseProxy` interface. +Once you have a *Connection* instance, services are accessed through instances +of :class:`~openstack.proxy.BaseProxy` or subclasses of it that exist as +attributes on the :class:`~openstack.connection.Connection`. .. autoclass:: openstack.proxy.BaseProxy :members: -The combination of your ``CloudRegion`` and the catalog of the cloud -in question control which services are exposed, but listed below are the ones -provided by the SDK. +.. _service-proxies: + +Service Proxies +~~~~~~~~~~~~~~~ + +The following service proxies exist on the +:class:`~openstack.connection.Connection`. The service proxies are all always +present on the :class:`~openstack.connection.Connection` object, but the +combination of your ``CloudRegion`` and the catalog of the cloud in question +control which services can be used. .. toctree:: :maxdepth: 1 @@ -103,7 +111,7 @@ provided by the SDK. Workflow Resource Interface -****************** +~~~~~~~~~~~~~~~~~~ The *Resource* layer is a lower-level interface to communicate with OpenStack services. While the classes exposed by the *Connection* build a convenience @@ -132,7 +140,7 @@ The following services have exposed *Resource* classes. Workflow Low-Level Classes -***************** +~~~~~~~~~~~~~~~~~ The following classes are not commonly used by application developers, but are used to construct applications to talk to OpenStack APIs. Typically diff --git a/doc/source/user/transition_from_profile.rst b/doc/source/user/transition_from_profile.rst new file mode 100644 index 000000000..0bc52254b --- /dev/null +++ b/doc/source/user/transition_from_profile.rst @@ -0,0 +1,186 @@ +Transition from Profile +======================= + +.. note:: This section describes migrating code from a previous interface of + python-openstacksdk and can be ignored by people writing new code. + +If you have code that currently uses the :class:`~openstack.profile.Profile` +object and/or an ``authenticator`` instance from an object based on +``openstack.auth.base.BaseAuthPlugin``, that code should be updated to use the +:class:`~openstack.config.cloud_region.CloudRegion` object instead. + +.. important:: + + :class:`~openstack.profile.Profile` is going away. Existing code using it + should be migrated as soon as possible. + +Writing Code that Works with Both +--------------------------------- + +These examples should all work with both the old and new interface, with one +caveat. With the old interface, the ``CloudConfig`` object comes from the +``os-client-config`` library, and in the new interface that has been moved +into the SDK. In order to write code that works with both the old and new +interfaces, use the following code to import the config namespace: + +.. code-block:: python + + try: + from openstack import config as occ + except ImportError: + from os_client_config import config as occ + +The examples will assume that the config module has been imported in that +manner. + +.. note:: Yes, there is an easier and less verbose way to do all of these. + These are verbose to handle both the old and new interfaces in the + same codebase. + +Replacing authenticator +----------------------- + +There is no direct replacement for ``openstack.auth.base.BaseAuthPlugin``. +``python-openstacksdk`` uses the `keystoneauth`_ library for authentication +and HTTP interactions. `keystoneauth`_ has `auth plugins`_ that can be used +to control how authentication is done. The ``auth_type`` config parameter +can be set to choose the correct authentication method to be used. + +Replacing Profile +----------------- + +The right way to replace the use of ``openstack.profile.Profile`` depends +a bit on what you're trying to accomplish. Common patterns are listed below, +but in general the approach is either to pass a cloud name to the +`openstack.connection.Connection` constructor, or to construct a +`openstack.config.cloud_region.CloudRegion` object and pass it to the +constructor. + +All of the examples on this page assume that you want to support old and +new interfaces simultaneously. There are easier and less verbose versions +of each that are available if you can just make a clean transition. + +Getting a Connection to a named cloud from clouds.yaml +------------------------------------------------------ + +If you want is to construct a `openstack.connection.Connection` based on +parameters configured in a ``clouds.yaml`` file, or from environment variables: + +.. code-block:: python + + import openstack.connection + + conn = connection.from_config(cloud_name='name-of-cloud-you-want') + +Getting a Connection from python arguments avoiding clouds.yaml +--------------------------------------------------------------- + +If, on the other hand, you want to construct a +`openstack.connection.Connection`, but are in a context where reading config +from a clouds.yaml file is undesirable, such as inside of a Service: + +* create a `openstack.config.loader.OpenStackConfig` object, telling + it to not load yaml files. Optionally pass an ``app_name`` and + ``app_version`` which will be added to user-agent strings. +* get a `openstack.config.cloud_region.CloudRegion` object from it +* get a `openstack.connection.Connection` + +.. code-block:: python + + try: + from openstack import config as occ + except ImportError: + from os_client_config import config as occ + from openstack import connection + + loader = occ.OpenStackConfig( + load_yaml_files=False, + app_name='spectacular-app', + app_version='1.0') + cloud_region = loader.get_one_cloud( + region_name='my-awesome-region', + auth_type='password', + auth=dict( + auth_url='https://auth.example.com', + username='amazing-user', + user_domain_name='example-domain', + project_name='astounding-project', + user_project_name='example-domain', + password='super-secret-password', + )) + conn = connection.from_config(cloud_config=cloud_region) + +.. note:: app_name and app_version are completely optional, and auth_type + defaults to 'password'. They are shown here for clarity as to + where they should go if they want to be set. + +Getting a Connection from python arguments and optionally clouds.yaml +--------------------------------------------------------------------- + +If you want to make a connection from python arguments and want to allow +one of them to optionally be ``cloud`` to allow selection of a named cloud, +it's essentially the same as the previous example, except without +``load_yaml_files=False``. + +.. code-block:: python + + try: + from openstack import config as occ + except ImportError: + from os_client_config import config as occ + from openstack import connection + + loader = occ.OpenStackConfig( + app_name='spectacular-app', + app_version='1.0') + cloud_region = loader.get_one_cloud( + region_name='my-awesome-region', + auth_type='password', + auth=dict( + auth_url='https://auth.example.com', + username='amazing-user', + user_domain_name='example-domain', + project_name='astounding-project', + user_project_name='example-domain', + password='super-secret-password', + )) + conn = connection.from_config(cloud_config=cloud_region) + +Parameters to get_one_cloud +--------------------------- + +The most important things to note are: + +* ``auth_type`` specifies which kind of authentication plugin to use. It + controls how authentication is done, as well as what parameters are required. +* ``auth`` is a dictionary containing the parameters needed by the auth plugin. + The most common information it needs are user, project, domain, auth_url + and password. +* The rest of the keyword arguments to + ``openstack.config.loader.OpenStackConfig.get_one_cloud`` are either + parameters needed by the `keystoneauth Session`_ object, which control how + HTTP connections are made, or parameters needed by the + `keystoneauth Adapter`_ object, which control how services are found in the + Keystone Catalog. + +For `keystoneauth Adapter`_ parameters, since there is one +`openstack.connection.Connection` object but many services, per-service +parameters are formed by using the official ``service_type`` of the service +in question. For instance, to override the endpoint for the ``compute`` +service, the parameter ``compute_endpoint_override`` would be used. + +``region_name`` in ``openstack.profile.Profile`` was a per-service parameter. +This is no longer a valid concept. An `openstack.connection.Connection` is a +connection to a region of a cloud. If you are in an extreme situation where +you have one service in one region and a different service in a different +region, you must use two different `openstack.connection.Connection` objects. + +.. note:: service_type, although a parameter for keystoneauth1.adapter.Adapter, + is not a valid parameter for get_one_cloud. service_type is the key + by which services are referred, so saying + 'compute_service_type="henry"' doesn't have any meaning. + +.. _keystoneauth: https://docs.openstack.org/keystoneauth/latest/ +.. _auth plugins: https://docs.openstack.org/keystoneauth/latest/authentication-plugins.html +.. _keystoneauth Adapter: https://docs.openstack.org/keystoneauth/latest/api/keystoneauth1.html#keystoneauth1.adapter.Adapter +.. _keystoneauth Session: https://docs.openstack.org/keystoneauth/latest/api/keystoneauth1.html#keystoneauth1.session.Session diff --git a/openstack/__init__.py b/openstack/__init__.py index 58ed37874..e7955388d 100644 --- a/openstack/__init__.py +++ b/openstack/__init__.py @@ -18,6 +18,43 @@ ] from openstack._log import enable_logging # noqa +import openstack.config import openstack.connection -connect = openstack.connection.Connection + +def connect( + cloud=None, + app_name=None, app_version=None, + options=None, + load_yaml_config=True, load_envvars=True, + **kwargs): + """Create a :class:`~openstack.connection.Connection` + + :param string cloud: + The name of the configuration to load from clouds.yaml. Defaults + to 'envvars' which will load configuration settings from environment + variables that start with ``OS_``. + :param argparse.Namespace options: + An argparse Namespace object. allows direct passing in of + argparse options to be added to the cloud config. Values + of None and '' will be removed. + :param bool load_yaml_config: + Whether or not to load config settings from clouds.yaml files. + Defaults to True. + :param bool load_envvars: + Whether or not to load config settings from environment variables. + Defaults to True. + :param kwargs: + Additional configuration options. + + :returns: openstack.connnection.Connection + :raises: keystoneauth1.exceptions.MissingRequiredOptions + on missing required auth parameters + """ + cloud_region = openstack.config.get_cloud_region( + cloud=cloud, + app_name=app_name, app_version=app_version, + load_yaml_config=load_yaml_config, + load_envvars=load_envvars, + options=options, **kwargs) + return openstack.connection.Connection(config=cloud_region) diff --git a/openstack/config/__init__.py b/openstack/config/__init__.py index 4c4547c14..4919b171b 100644 --- a/openstack/config/__init__.py +++ b/openstack/config/__init__.py @@ -16,23 +16,21 @@ from openstack.config.loader import OpenStackConfig # noqa -_config = None - def get_cloud_region( service_key=None, options=None, app_name=None, app_version=None, + load_yaml_config=True, + load_envvars=True, **kwargs): - load_yaml_config = kwargs.pop('load_yaml_config', True) - global _config - if not _config: - _config = OpenStackConfig( - load_yaml_config=load_yaml_config, - app_name=app_name, app_version=app_version) + config = OpenStackConfig( + load_yaml_config=load_yaml_config, + load_envvars=load_envvars, + app_name=app_name, app_version=app_version) if options: - _config.register_argparse_arguments(options, sys.argv, service_key) + config.register_argparse_arguments(options, sys.argv, service_key) parsed_options = options.parse_known_args(sys.argv) else: parsed_options = None - return _config.get_one(options=parsed_options, **kwargs) + return config.get_one(options=parsed_options, **kwargs) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 2c77389d8..43042d558 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -35,7 +35,9 @@ def _make_key(key, service_type): return "_".join([service_type, key]) -def from_session(session, name=None, config=None, **kwargs): +def from_session(session, name=None, region_name=None, + force_ipv4=False, + app_name=None, app_version=None, **kwargs): """Construct a CloudRegion from an existing `keystoneauth1.session.Session` When a Session already exists, we don't actually even need to go through @@ -43,20 +45,30 @@ def from_session(session, name=None, config=None, **kwargs): The only parameters that are really needed are adapter/catalog related. :param keystoneauth1.session.session session: - An existing Session to use. + An existing authenticated Session to use. :param str name: A name to use for this cloud region in logging. If left empty, the hostname of the auth_url found in the Session will be used. - :param dict config: + :param str region_name: + The region name to connect to. + :param bool force_ipv4: + Whether or not to disable IPv6 support. Defaults to False. + :param str app_name: + Name of the application to be added to User Agent. + :param str app_version: + Version of the application to be added to User Agent. + :param kwargs: Config settings for this cloud region. """ # If someone is constructing one of these from a Session, then they are # not using a named config. Use the hostname of their auth_url instead. name = name or urllib.parse.urlparse(session.auth.auth_url).hostname config_dict = config_defaults.get_defaults() - config_dict.update(config or {}) + config_dict.update(**kwargs) return CloudRegion( - name=name, session=session, config=config_dict, **kwargs) + name=name, session=session, config=config_dict, + region_name=region_name, force_ipv4=force_ipv4, + app_name=app_name, app_version=app_version) class CloudRegion(object): diff --git a/openstack/config/loader.py b/openstack/config/loader.py index a0ad64fd3..04dc06b37 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -183,11 +183,12 @@ def __init__(self, config_files=None, vendor_files=None, envvar_prefix=None, secure_files=None, pw_func=None, session_constructor=None, app_name=None, app_version=None, - load_yaml_config=True): + load_yaml_config=True, load_envvars=True): self.log = _log.setup_logging('openstack.config') self._session_constructor = session_constructor self._app_name = app_name self._app_version = app_version + self._load_envvars = load_envvars if load_yaml_config: self._config_files = config_files or CONFIG_FILES @@ -198,11 +199,11 @@ def __init__(self, config_files=None, vendor_files=None, self._secure_files = [] self._vendor_files = [] - config_file_override = os.environ.get('OS_CLIENT_CONFIG_FILE') + config_file_override = self._get_envvar('OS_CLIENT_CONFIG_FILE') if config_file_override: self._config_files.insert(0, config_file_override) - secure_file_override = os.environ.get('OS_CLIENT_SECURE_FILE') + secure_file_override = self._get_envvar('OS_CLIENT_SECURE_FILE') if secure_file_override: self._secure_files.insert(0, secure_file_override) @@ -231,12 +232,12 @@ def __init__(self, config_files=None, vendor_files=None, else: # Get the backwards compat value prefer_ipv6 = get_boolean( - os.environ.get( + self._get_envvar( 'OS_PREFER_IPV6', client_config.get( 'prefer_ipv6', client_config.get( 'prefer-ipv6', True)))) force_ipv4 = get_boolean( - os.environ.get( + self._get_envvar( 'OS_FORCE_IPV4', client_config.get( 'force_ipv4', client_config.get( 'broken-ipv6', False)))) @@ -248,7 +249,7 @@ def __init__(self, config_files=None, vendor_files=None, self.force_ipv4 = True # Next, process environment variables and add them to the mix - self.envvar_key = os.environ.get('OS_CLOUD_NAME', 'envvars') + self.envvar_key = self._get_envvar('OS_CLOUD_NAME', 'envvars') if self.envvar_key in self.cloud_config['clouds']: raise exceptions.OpenStackConfigException( '"{0}" defines a cloud named "{1}", but' @@ -257,13 +258,14 @@ def __init__(self, config_files=None, vendor_files=None, ' file-based clouds.'.format(self.config_filename, self.envvar_key)) - self.default_cloud = os.environ.get('OS_CLOUD') + self.default_cloud = self._get_envvar('OS_CLOUD') - envvars = _get_os_environ(envvar_prefix=envvar_prefix) - if envvars: - self.cloud_config['clouds'][self.envvar_key] = envvars - if not self.default_cloud: - self.default_cloud = self.envvar_key + if load_envvars: + envvars = _get_os_environ(envvar_prefix=envvar_prefix) + if envvars: + self.cloud_config['clouds'][self.envvar_key] = envvars + if not self.default_cloud: + self.default_cloud = self.envvar_key if not self.default_cloud and self.cloud_config['clouds']: if len(self.cloud_config['clouds'].keys()) == 1: @@ -320,6 +322,11 @@ def __init__(self, config_files=None, vendor_files=None, # password = self._pw_callback(prompt="Password: ") self._pw_callback = pw_func + def _get_envvar(self, key, default=None): + if not self._load_envvars: + return default + return os.environ.get(key, default) + def get_extra_config(self, key, defaults=None): """Fetch an arbitrary extra chunk of config, laying in defaults. @@ -700,7 +707,7 @@ def register_argparse_arguments(self, parser, argv, service_keys=None): p.add_argument( '--os-cloud', metavar='', - default=os.environ.get('OS_CLOUD', None), + default=self._get_envvar('OS_CLOUD', None), help='Named cloud to connect to') # we need to peek to see if timeout was actually passed, since diff --git a/openstack/connection.py b/openstack/connection.py index 5f454480f..cc8bd33b4 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -12,67 +12,148 @@ """ The :class:`~openstack.connection.Connection` class is the primary interface -to the Python SDK it maintains a context for a connection to a cloud provider. -The connection has an attribute to access each supported service. - -Examples --------- +to the Python SDK. It maintains a context for a connection to a region of +a cloud provider. The :class:`~openstack.connection.Connection` has an +attribute to access each OpenStack service. At a minimum, the :class:`~openstack.connection.Connection` class needs to be created with a config or the parameters to build one. -Create a connection -~~~~~~~~~~~~~~~~~~~ +While the overall system is very flexible, there are four main use cases +for different ways to create a :class:`~openstack.connection.Connection`. + +* Using config settings and keyword arguments as described in + :ref:`openstack-config` +* Using only keyword arguments passed to the constructor ignoring config files + and environment variables. +* Using an existing authenticated `keystoneauth1.session.Session`, such as + might exist inside of an OpenStack service operational context. +* Using an existing :class:`~openstack.config.cloud_region.CloudRegion`. + +Using config settings +--------------------- + +For users who want to create a :class:`~openstack.connection.Connection` making +use of named clouds in ``clouds.yaml`` files, ``OS_`` environment variables +and python keyword arguments, the :func:`openstack.connect` factory function +is the recommended way to go: + +.. code-block:: python + + import openstack + + conn = openstack.connect(cloud='example', region_name='earth1') + +If the application in question is a command line application that should also +accept command line arguments, an `argparse.Namespace` can be passed to +:func:`openstack.connect` that will have relevant arguments added to it and +then subsequently consumed by the construtor: + +.. code-block:: python + + import argparse + import openstack + + options = argparse.ArgumentParser(description='Awesome OpenStack App') + conn = openstack.connect(options=options) + +Using Only Keyword Arguments +---------------------------- + +If the application wants to avoid loading any settings from ``clouds.yaml`` or +environment variables, use the :class:`~openstack.connection.Connection` +constructor directly. As long as the ``cloud`` argument is omitted or ``None``, +the :class:`~openstack.connection.Connection` constructor will not load +settings from files or the environment. -The preferred way to create a connection is to manage named configuration -settings in your clouds.yaml file and refer to them by name.:: +.. note:: + + This is a different default behavior than the :func:`~openstack.connect` + factory function. In :func:`~openstack.connect` if ``cloud`` is omitted + or ``None``, a default cloud will be loaded, defaulting to the ``envvars`` + cloud if it exists. + +.. code-block:: python from openstack import connection - conn = connection.Connection(cloud='example', region_name='earth1') + conn = connection.Connection( + region_name='example-region', + auth=dict( + auth_url='https://auth.example.com', + username='amazing-user', + password='super-secret-password', + project_id='33aa1afc-03fe-43b8-8201-4e0d3b4b8ab5', + user_domain_id='054abd68-9ad9-418b-96d3-3437bb376703'), + compute_api_version='2', + identity_interface='internal') + +Per-service settings as needed by `keystoneauth1.adapter.Adapter` such as +``api_version``, ``service_name``, and ``interface`` can be set, as seen +above, by prefixing them with the official ``service-type`` name of the +service. ``region_name`` is a setting for the entire +:class:`~openstack.config.cloud_region.CloudRegion` and cannot be set per +service. + +From existing authenticated Session +----------------------------------- + +For applications that already have an authenticated Session, simply passing +it to the :class:`~openstack.connection.Connection` constructor is all that +is needed: + +.. code-block:: python + + from openstack import connection + + conn = connection.Connection( + session=session, + region_name='example-region', + compute_api_version='2', + identity_interface='internal') + +From existing CloudRegion +------------------------- If you already have an :class:`~openstack.config.cloud_region.CloudRegion` -you can pass it in instead.:: +you can pass it in instead: + +.. code-block:: python from openstack import connection import openstack.config - config = openstack.config.OpenStackConfig.get_one( + config = openstack.config.get_cloud_region( cloud='example', region_name='earth') conn = connection.Connection(config=config) -It's also possible to pass in parameters directly if needed. The following -example constructor uses the default identity password auth -plugin and provides a username and password.:: +Using the Connection +-------------------- - from openstack import connection - auth_args = { - 'auth_url': 'http://172.20.1.108:5000/v3', - 'project_name': 'admin', - 'user_domain_name': 'default', - 'project_domain_name': 'default', - 'username': 'admin', - 'password': 'admin', - } - conn = connection.Connection(**auth_args) +Services are accessed through an attribute named after the service's official +service-type. List ~~~~ -Services are accessed through an attribute named after the service's official -service-type. A list of all the projects is retrieved in this manner:: +An iterator containing a list of all the projects is retrieved in this manner: - projects = [project for project in conn.identity.projects()] +.. code-block:: python + + projects = conn.identity.projects() Find or create ~~~~~~~~~~~~~~ + If you wanted to make sure you had a network named 'zuul', you would first try to find it and if that fails, you would create it:: network = conn.network.find_network("zuul") if network is None: - network = conn.network.create_network({"name": "zuul"}) + network = conn.network.create_network(name="zuul") +Additional information about the services can be found in the +:ref:`service-proxies` documentation. """ __all__ = [ 'from_config', @@ -88,6 +169,7 @@ from openstack import _log from openstack import _meta from openstack import config as _config +from openstack.config import cloud_region from openstack import exceptions from openstack import service_description from openstack import task_manager @@ -119,11 +201,11 @@ def from_config(cloud=None, config=None, options=None, **kwargs): :rtype: :class:`~openstack.connection.Connection` """ # TODO(mordred) Backwards compat while we transition - cloud = cloud or kwargs.get('cloud_name') - config = config or kwargs.get('cloud_config') + cloud = kwargs.pop('cloud_name', cloud) + config = kwargs.pop('cloud_config', config) if config is None: config = _config.OpenStackConfig().get_one( - cloud=cloud, argparse=options) + cloud=cloud, argparse=options, **kwargs) return Connection(config=config) @@ -167,11 +249,12 @@ def __init__(self, cloud=None, config=None, session=None, User Agent. :param authenticator: DEPRECATED. Only exists for short-term backwards compatibility for python-openstackclient while we - transition. See `Transition from Profile`_ for - details. + transition. See :doc:`transition_from_profile` + for details. :param profile: DEPRECATED. Only exists for short-term backwards compatibility for python-openstackclient while we - transition. See `Transition from Profile`_ for details. + transition. See :doc:`transition_from_profile` + for details. :param extra_services: List of :class:`~openstack.service_description.ServiceDescription` objects describing services that openstacksdk otherwise does not @@ -193,12 +276,20 @@ def __init__(self, cloud=None, config=None, session=None, # python-openstackclient to not use the profile interface. self.config = openstack.profile._get_config_from_profile( profile, authenticator, **kwargs) + elif session: + self.config = cloud_region.from_session( + session=session, + app_name=app_name, app_version=app_version, + load_yaml_config=False, + load_envvars=False, + **kwargs) else: - openstack_config = _config.OpenStackConfig( + self.config = _config.get_cloud_region( + cloud=cloud, app_name=app_name, app_version=app_version, - load_yaml_config=profile is None) - self.config = openstack_config.get_one( - cloud=cloud, validate=session is None, **kwargs) + load_yaml_config=cloud is not None, + load_envvars=cloud is not None, + **kwargs) if self.config.name: tm_name = ':'.join([ diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index 8f434965b..b4f34f108 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -78,8 +78,11 @@ def test_other_parameters(self): def test_session_provided(self): mock_session = mock.Mock(spec=session.Session) + mock_session.auth = mock.Mock() + mock_session.auth.auth_url = 'https://auth.example.com' conn = connection.Connection(session=mock_session, cert='cert') self.assertEqual(mock_session, conn.session) + self.assertEqual('auth.example.com', conn.config.name) def test_create_session(self): conn = connection.Connection(cloud='sample') From 8483e1b1398d9230aef5f1a9d27720780f8b4340 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 30 Jan 2018 08:22:47 -0600 Subject: [PATCH 1962/3836] Add OpenStackCloud object to Connection Make a cloud attribute on Connection so that people with a Connection can also use shade features. This changes the default for shade's list_flavors to NOT fetching extra_specs, which is very much yay. Change-Id: I45a5f7f11a9c5ab3c77443a8f5df26089243334c --- openstack/cloud/openstackcloud.py | 22 +++------- openstack/config/cloud_region.py | 20 +++++---- openstack/connection.py | 5 +++ .../tests/functional/cloud/test_flavor.py | 6 ++- openstack/tests/unit/cloud/test_caching.py | 6 --- openstack/tests/unit/cloud/test_flavors.py | 44 ++++++++++++++++++- 6 files changed, 69 insertions(+), 34 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index c5dfe9d49..956514a04 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -50,7 +50,6 @@ from openstack.cloud import _utils import openstack.config import openstack.config.defaults -import openstack.connection from openstack import task_manager from openstack import utils @@ -143,6 +142,7 @@ def __init__( app_name=None, app_version=None, use_direct_get=False, + conn=None, **kwargs): self.log = _log.setup_logging('openstack') @@ -162,12 +162,6 @@ def __init__( self.secgroup_source = cloud_config.config['secgroup_source'] self.force_ipv4 = cloud_config.force_ipv4 self.strict_mode = strict - # TODO(shade) The openstack.cloud default for get_flavor_extra_specs - # should be changed and this should be removed completely - self._extra_config = cloud_config._openstack_config.get_extra_config( - 'shade', { - 'get_flavor_extra_specs': True, - }) if manager is not None: self.manager = manager @@ -303,11 +297,14 @@ def invalidate(self): _utils.localhost_supports_ipv6() if not self.force_ipv4 else False) self.cloud_config = cloud_config - self._conn_object = None + self._conn_object = conn @property def _conn(self): if not self._conn_object: + # Importing late to avoid import cycle. If the OpenStackCloud + # object comes via Connection, it'll have connection passed in. + import openstack.connection self._conn_object = openstack.connection.Connection( config=self.cloud_config, session=self._keystone_session) return self._conn_object @@ -1938,7 +1935,7 @@ def list_availability_zone_names(self, unavailable=False): return ret @_utils.cache_on_arguments() - def list_flavors(self, get_extra=None): + def list_flavors(self, get_extra=False): """List all available flavors. :param get_extra: Whether or not to fetch extra specs for each flavor. @@ -1948,8 +1945,6 @@ def list_flavors(self, get_extra=None): :returns: A list of flavor ``munch.Munch``. """ - if get_extra is None: - get_extra = self._extra_config['get_flavor_extra_specs'] data = _adapter._json_response( self._conn.compute.get( '/flavors/detail', params=dict(is_public='None')), @@ -2986,7 +2981,7 @@ def get_flavor(self, name_or_id, filters=None, get_extra=True): self.search_flavors, get_extra=get_extra) return _utils._get_entity(self, search_func, name_or_id, filters) - def get_flavor_by_id(self, id, get_extra=True): + def get_flavor_by_id(self, id, get_extra=False): """ Get a flavor by ID :param id: ID of the flavor. @@ -3002,9 +2997,6 @@ def get_flavor_by_id(self, id, get_extra=True): flavor = self._normalize_flavor( self._get_and_munchify('flavor', data)) - if get_extra is None: - get_extra = self._extra_config['get_flavor_extra_specs'] - if not flavor.extra_specs and get_extra: endpoint = "/flavors/{id}/os-extra_specs".format( id=flavor.id) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 43042d558..422bee3bf 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -353,6 +353,7 @@ def get_session_endpoint( def get_cache_expiration_time(self): if self._openstack_config: return self._openstack_config.get_cache_expiration_time() + return 0 def get_cache_path(self): if self._openstack_config: @@ -361,6 +362,7 @@ def get_cache_path(self): def get_cache_class(self): if self._openstack_config: return self._openstack_config.get_cache_class() + return 'dogpile.cache.null' def get_cache_arguments(self): if self._openstack_config: @@ -400,56 +402,56 @@ def requires_floating_ip(self): def get_external_networks(self): """Get list of network names for external networks.""" return [ - net['name'] for net in self.config['networks'] + net['name'] for net in self.config.get('networks', []) if net['routes_externally']] def get_external_ipv4_networks(self): """Get list of network names for external IPv4 networks.""" return [ - net['name'] for net in self.config['networks'] + net['name'] for net in self.config.get('networks', []) if net['routes_ipv4_externally']] def get_external_ipv6_networks(self): """Get list of network names for external IPv6 networks.""" return [ - net['name'] for net in self.config['networks'] + net['name'] for net in self.config.get('networks', []) if net['routes_ipv6_externally']] def get_internal_networks(self): """Get list of network names for internal networks.""" return [ - net['name'] for net in self.config['networks'] + net['name'] for net in self.config.get('networks', []) if not net['routes_externally']] def get_internal_ipv4_networks(self): """Get list of network names for internal IPv4 networks.""" return [ - net['name'] for net in self.config['networks'] + net['name'] for net in self.config.get('networks', []) if not net['routes_ipv4_externally']] def get_internal_ipv6_networks(self): """Get list of network names for internal IPv6 networks.""" return [ - net['name'] for net in self.config['networks'] + net['name'] for net in self.config.get('networks', []) if not net['routes_ipv6_externally']] def get_default_network(self): """Get network used for default interactions.""" - for net in self.config['networks']: + for net in self.config.get('networks', []): if net['default_interface']: return net['name'] return None def get_nat_destination(self): """Get network used for NAT destination.""" - for net in self.config['networks']: + for net in self.config.get('networks', []): if net['nat_destination']: return net['name'] return None def get_nat_source(self): """Get network used for NAT source.""" - for net in self.config['networks']: + for net in self.config.get('networks', []): if net.get('nat_source'): return net['name'] return None diff --git a/openstack/connection.py b/openstack/connection.py index cc8bd33b4..a7cad826d 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -168,6 +168,7 @@ from openstack import _log from openstack import _meta +from openstack import cloud as _cloud from openstack import config as _config from openstack.config import cloud_region from openstack import exceptions @@ -311,6 +312,10 @@ def __init__(self, cloud=None, config=None, session=None, self.session._sdk_connection = self self._proxies = {} + self.cloud = _cloud.OpenStackCloud( + cloud_config=self.config, + manager=self.task_manager, + conn=self) def add_service(self, service): """Add a service to the Connection. diff --git a/openstack/tests/functional/cloud/test_flavor.py b/openstack/tests/functional/cloud/test_flavor.py index 9bdcf8dd0..e4d4a7228 100644 --- a/openstack/tests/functional/cloud/test_flavor.py +++ b/openstack/tests/functional/cloud/test_flavor.py @@ -157,7 +157,8 @@ def test_set_unset_flavor_specs(self): # Now set them extra_specs = {'foo': 'aaa', 'bar': 'bbb'} self.operator_cloud.set_flavor_specs(new_flavor['id'], extra_specs) - mod_flavor = self.operator_cloud.get_flavor(new_flavor['id']) + mod_flavor = self.operator_cloud.get_flavor( + new_flavor['id'], get_extra=True) # Verify extra_specs were set self.assertIn('extra_specs', mod_flavor) @@ -165,7 +166,8 @@ def test_set_unset_flavor_specs(self): # Unset the 'foo' value self.operator_cloud.unset_flavor_specs(mod_flavor['id'], ['foo']) - mod_flavor = self.operator_cloud.get_flavor_by_id(new_flavor['id']) + mod_flavor = self.operator_cloud.get_flavor_by_id( + new_flavor['id'], get_extra=True) # Verify 'foo' is unset and 'bar' is still set self.assertEqual({'bar': 'bbb'}, mod_flavor['extra_specs']) diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py index bcfc7cd63..b1bcc38d8 100644 --- a/openstack/tests/unit/cloud/test_caching.py +++ b/openstack/tests/unit/cloud/test_caching.py @@ -437,12 +437,6 @@ def test_list_flavors(self): dict(method='GET', uri=mock_uri, json={'flavors': fakes.FAKE_FLAVOR_LIST}) ] - uris_to_mock.extend([ - dict(method='GET', - uri='{endpoint}/flavors/{id}/os-extra_specs'.format( - endpoint=fakes.COMPUTE_ENDPOINT, id=flavor['id']), - json={'extra_specs': {}}) - for flavor in fakes.FAKE_FLAVOR_LIST]) self.register_uris(uris_to_mock) diff --git a/openstack/tests/unit/cloud/test_flavors.py b/openstack/tests/unit/cloud/test_flavors.py index e23e67b1f..0fa5ae877 100644 --- a/openstack/tests/unit/cloud/test_flavors.py +++ b/openstack/tests/unit/cloud/test_flavors.py @@ -82,6 +82,30 @@ def test_delete_flavor_exception(self): self.cloud.delete_flavor, 'vanilla') def test_list_flavors(self): + uris_to_mock = [ + dict(method='GET', + uri='{endpoint}/flavors/detail?is_public=None'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'flavors': fakes.FAKE_FLAVOR_LIST}), + ] + self.register_uris(uris_to_mock) + + flavors = self.cloud.list_flavors() + + # test that new flavor is created correctly + found = False + for flavor in flavors: + if flavor['name'] == 'vanilla': + found = True + break + self.assertTrue(found) + needed_keys = {'name', 'ram', 'vcpus', 'id', 'is_public', 'disk'} + if found: + # check flavor content + self.assertTrue(needed_keys.issubset(flavor.keys())) + self.assert_calls() + + def test_list_flavors_with_extra(self): uris_to_mock = [ dict(method='GET', uri='{endpoint}/flavors/detail?is_public=None'.format( @@ -96,7 +120,7 @@ def test_list_flavors(self): for flavor in fakes.FAKE_FLAVOR_LIST]) self.register_uris(uris_to_mock) - flavors = self.cloud.list_flavors() + flavors = self.cloud.list_flavors(get_extra=True) # test that new flavor is created correctly found = False @@ -238,6 +262,22 @@ def test_list_flavor_access(self): self.assert_calls() def test_get_flavor_by_id(self): + flavor_uri = '{endpoint}/flavors/1'.format( + endpoint=fakes.COMPUTE_ENDPOINT) + flavor_json = {'flavor': fakes.make_fake_flavor('1', 'vanilla')} + + self.register_uris([ + dict(method='GET', uri=flavor_uri, json=flavor_json), + ]) + + flavor1 = self.cloud.get_flavor_by_id('1') + self.assertEqual('1', flavor1['id']) + self.assertEqual({}, flavor1.extra_specs) + flavor2 = self.cloud.get_flavor_by_id('1') + self.assertEqual('1', flavor2['id']) + self.assertEqual({}, flavor2.extra_specs) + + def test_get_flavor_with_extra_specs(self): flavor_uri = '{endpoint}/flavors/1'.format( endpoint=fakes.COMPUTE_ENDPOINT) flavor_extra_uri = '{endpoint}/flavors/1/os-extra_specs'.format( @@ -250,7 +290,7 @@ def test_get_flavor_by_id(self): dict(method='GET', uri=flavor_extra_uri, json=flavor_extra_json), ]) - flavor1 = self.cloud.get_flavor_by_id('1') + flavor1 = self.cloud.get_flavor_by_id('1', get_extra=True) self.assertEqual('1', flavor1['id']) self.assertEqual({'name': 'test'}, flavor1.extra_specs) flavor2 = self.cloud.get_flavor_by_id('1', get_extra=False) From a23f7423cf2fb103ad6933a69160443c82ef71d1 Mon Sep 17 00:00:00 2001 From: Hunt Xu Date: Wed, 31 Jan 2018 16:39:44 +0800 Subject: [PATCH 1963/3836] Implement list projects for user Change-Id: I25d542eb24fb06517904090af21b7606d8e8a7fa Closes-Bug: #1680063 --- openstack/identity/v3/_proxy.py | 16 ++++++++++++++++ openstack/identity/v3/project.py | 14 ++++++++++++++ openstack/tests/unit/identity/v3/test_project.py | 15 +++++++++++++++ openstack/tests/unit/identity/v3/test_proxy.py | 13 +++++++++++++ 4 files changed, 58 insertions(+) diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 7cb720ee9..e4796f86c 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -497,6 +497,22 @@ def projects(self, **query): # TODO(briancurtin): This is paginated but requires base list changes. return self._list(_project.Project, paginated=False, **query) + def user_projects(self, user, **query): + """Retrieve a generator of projects to which the user has authorization + to access. + + :param user: Either the user id or an instance of + :class:`~openstack.identity.v3.user.User` + :param kwargs \*\*query: Optional query parameters to be sent to limit + the resources being returned. + + :returns: A generator of project instances. + :rtype: :class:`~openstack.identity.v3.project.UserProject` + """ + user = self._get_resource(_user.User, user) + return self._list(_project.UserProject, paginated=True, + user_id=user.id, **query) + def update_project(self, project, **attrs): """Update a project diff --git a/openstack/identity/v3/project.py b/openstack/identity/v3/project.py index 7b126b3a0..12ddbd82c 100644 --- a/openstack/identity/v3/project.py +++ b/openstack/identity/v3/project.py @@ -113,3 +113,17 @@ def unassign_role_from_group(self, session, group, role): if resp.status_code == 204: return True return False + + +class UserProject(Project): + resource_key = 'project' + resources_key = 'projects' + base_path = '/users/%(user_id)s/projects' + service = identity_service.IdentityService() + + # capabilities + allow_create = False + allow_get = False + allow_update = False + allow_delete = False + allow_list = True diff --git a/openstack/tests/unit/identity/v3/test_project.py b/openstack/tests/unit/identity/v3/test_project.py index fc5ecd806..2c38efceb 100644 --- a/openstack/tests/unit/identity/v3/test_project.py +++ b/openstack/tests/unit/identity/v3/test_project.py @@ -62,3 +62,18 @@ def test_make_it(self): self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['parent_id'], sot.parent_id) + + +class TestUserProject(testtools.TestCase): + + def test_basic(self): + sot = project.UserProject() + self.assertEqual('project', sot.resource_key) + self.assertEqual('projects', sot.resources_key) + self.assertEqual('/users/%(user_id)s/projects', sot.base_path) + self.assertEqual('identity', sot.service.service_type) + self.assertFalse(sot.allow_create) + self.assertFalse(sot.allow_get) + self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_delete) + self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/identity/v3/test_proxy.py b/openstack/tests/unit/identity/v3/test_proxy.py index 4fd0227b3..028b322e1 100644 --- a/openstack/tests/unit/identity/v3/test_proxy.py +++ b/openstack/tests/unit/identity/v3/test_proxy.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import uuid + from openstack.identity.v3 import _proxy from openstack.identity.v3 import credential from openstack.identity.v3 import domain @@ -24,6 +26,8 @@ from openstack.identity.v3 import user from openstack.tests.unit import test_proxy_base +USER_ID = 'user-id-' + uuid.uuid4().hex + class TestIdentityProxy(test_proxy_base.TestProxyBase): def setUp(self): @@ -160,6 +164,15 @@ def test_project_get(self): def test_projects(self): self.verify_list(self.proxy.projects, project.Project, paginated=False) + def test_user_projects(self): + self.verify_list( + self.proxy.user_projects, + project.UserProject, + paginated=True, + method_kwargs={'user': USER_ID}, + expected_kwargs={'user_id': USER_ID} + ) + def test_project_update(self): self.verify_update(self.proxy.update_project, project.Project) From 63de3b22c0df6a08341731e84f2434626ccae41e Mon Sep 17 00:00:00 2001 From: Hunt Xu Date: Wed, 31 Jan 2018 14:20:47 +0800 Subject: [PATCH 1964/3836] resource: don't early terminate list APIs can set the max_limit to the number of items returned in each query. If that max_limit is smaller than the limit given by the user, the API will never return as many records as the limit, however that doesn't mean there are no more records. So the list should not be early terminated. Closes-Bug: #1746410 Change-Id: I893f4b3ba0856476f81189d741de1aed8aa65e25 --- openstack/resource.py | 11 ------ openstack/tests/unit/test_resource.py | 55 +++++++++++++++++++++------ 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/openstack/resource.py b/openstack/resource.py index d2bc696ef..630ffb738 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -893,9 +893,6 @@ def list(cls, session, paginated=False, **params): if not isinstance(resources, list): resources = [resources] - # Keep track of how many items we've yielded. The server should - # handle this, but it's easy for us to as well. - yielded = 0 marker = None for raw_resource in resources: # Do not allow keys called "self" through. Glance chose @@ -908,16 +905,8 @@ def list(cls, session, paginated=False, **params): value = cls.existing(**raw_resource) marker = value.id yield value - yielded += 1 total_yielded += 1 - # If a limit was given by the user and we have not returned - # as many records as the limit, then it stands to reason that - # there are no more records to return and we don't need to do - # anything else. - if limit and yielded < limit: - return - if resources and paginated: uri, next_params = cls._get_next_link( uri, response, data, marker, limit, total_yielded) diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 80d98a8a1..4322f3c4e 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1367,22 +1367,39 @@ def test_list_multi_page_response_paginated(self): headers={"Accept": "application/json"}, params={}) - def test_list_multi_page_early_termination(self): - # This tests our ability to be somewhat smart when evaluating - # the contents of the responses. When we request a limit and - # receive less than that limit and there are no next links, - # we can be pretty sure there are no more pages. - ids = [1, 2] + def test_list_multi_page_no_early_termination(self): + # This tests verifies that multipages are not early terminated. + # APIs can set max_limit to the number of items returned in each + # query. If that max_limit is smaller than the limit given by the + # user, the return value would contain less items than the limit, + # but that doesn't stand to reason that there are no more records, + # we should keep trying to get more results. + ids = [1, 2, 3, 4] resp1 = mock.Mock() resp1.status_code = 200 + resp1.links = {} resp1.json.return_value = { + # API's max_limit is set to 2. "resources": [{"id": ids[0]}, {"id": ids[1]}], } + resp2 = mock.Mock() + resp2.status_code = 200 + resp2.links = {} + resp2.json.return_value = { + # API's max_limit is set to 2. + "resources": [{"id": ids[2]}, {"id": ids[3]}], + } + resp3 = mock.Mock() + resp3.status_code = 200 + resp3.json.return_value = { + "resources": [], + } - self.session.get.return_value = resp1 + self.session.get.side_effect = [resp1, resp2, resp3] results = self.sot.list(self.session, limit=3, paginated=True) + # First page constains only two items, less than the limit given result0 = next(results) self.assertEqual(result0.id, ids[0]) result1 = next(results) @@ -1392,11 +1409,27 @@ def test_list_multi_page_early_termination(self): headers={"Accept": "application/json"}, params={"limit": 3}) - # Ensure we're done after those two items + # Second page contains another two items + result2 = next(results) + self.assertEqual(result2.id, ids[2]) + result3 = next(results) + self.assertEqual(result3.id, ids[3]) + self.session.get.assert_called_with( + self.base_path, + headers={"Accept": "application/json"}, + params={"limit": 3, "marker": 2}) + + # Ensure we're done after those four items self.assertRaises(StopIteration, next, results) - # Ensure we only made one calls to get this done - self.assertEqual(1, len(self.session.get.call_args_list)) + # Ensure we've given the last try to get more results + self.session.get.assert_called_with( + self.base_path, + headers={"Accept": "application/json"}, + params={"limit": 3, "marker": 4}) + + # Ensure we made three calls to get this done + self.assertEqual(3, len(self.session.get.call_args_list)) def test_list_multi_page_inferred_additional(self): # If we explicitly request a limit and we receive EXACTLY that @@ -1443,7 +1476,7 @@ def test_list_multi_page_inferred_additional(self): self.assertRaises(StopIteration, next, results) # Ensure we only made two calls to get this done - self.assertEqual(2, len(self.session.get.call_args_list)) + self.assertEqual(3, len(self.session.get.call_args_list)) def test_list_multi_page_header_count(self): class Test(self.test_class): From 0a6083b8766c86e198c0f6761d5da84d7f2bdfa5 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 30 Jan 2018 16:22:20 -0600 Subject: [PATCH 1965/3836] Merge Connection and OpenStackCloud There's no good reason to have the shade functions in a cloud attribute. Go ahead and dive all the way in and make OpenStackCloud a mixin class. There was one wrapped exception that got removed and thus the test to test that we were wrapping it. Change-Id: Iebd80fe5bc511ea879ea71aa88ce7d79c5e8fa58 --- SHADE-MERGE-TODO.rst | 22 +- doc/source/user/connection.rst | 1 + openstack/cloud/__init__.py | 4 +- openstack/cloud/_normalize.py | 4 +- openstack/cloud/inventory.py | 23 +- openstack/cloud/meta.py | 2 +- openstack/cloud/openstackcloud.py | 342 +++++++----------- openstack/connection.py | 31 +- openstack/tests/functional/cloud/base.py | 10 +- .../tests/functional/cloud/test_domain.py | 2 +- .../tests/functional/cloud/test_endpoints.py | 2 +- .../tests/functional/cloud/test_groups.py | 2 +- .../tests/functional/cloud/test_project.py | 2 +- .../tests/functional/cloud/test_services.py | 2 +- .../tests/functional/cloud/test_users.py | 4 +- openstack/tests/unit/base.py | 12 +- openstack/tests/unit/cloud/test_image.py | 12 +- openstack/tests/unit/cloud/test_inventory.py | 30 +- openstack/tests/unit/cloud/test_meta.py | 14 +- openstack/tests/unit/cloud/test_operator.py | 2 +- .../tests/unit/config/test_from_session.py | 2 +- ...hade-into-connection-81191fb3d0ddaf6e.yaml | 5 + 22 files changed, 229 insertions(+), 301 deletions(-) create mode 100644 releasenotes/notes/shade-into-connection-81191fb3d0ddaf6e.yaml diff --git a/SHADE-MERGE-TODO.rst b/SHADE-MERGE-TODO.rst index bc5aa8349..71be13eaa 100644 --- a/SHADE-MERGE-TODO.rst +++ b/SHADE-MERGE-TODO.rst @@ -24,6 +24,17 @@ already. For reference, those are: * Plumbed Proxy use of Adapter through the Adapter subclass from shade that uses the TaskManager to run REST calls. * Finish migrating to Resource2 and Proxy2, rename them to Resource and Proxy. +* Merge OpenStackCloud into Connection. This should result + in being able to use the connection interact with the cloud using all three + interfaces. For instance: + + .. code-block:: python + + conn = connection.Connection() + servers = conn.list_servers() # High-level resource interface from shade + servers = conn.compute.servers() # SDK Service/Object Interface + response = conn.compute.get('/servers') # REST passthrough + Next steps ========== @@ -40,17 +51,6 @@ Next steps shade integration ----------------- -* Merge OpenStackCloud into Connection. This should result - in being able to use the connection interact with the cloud using all three - interfaces. For instance: - - .. code-block:: python - - conn = connection.Connection() - servers = conn.list_servers() # High-level resource interface from shade - servers = conn.compute.servers() # SDK Service/Object Interface - response = conn.compute.get('/servers') # REST passthrough - * Invent some terminology that is clear and makes sense to distinguish between the object interface that came originally from python-openstacksdk and the interface that came from shade. diff --git a/doc/source/user/connection.rst b/doc/source/user/connection.rst index e5afce0d8..21833c314 100644 --- a/doc/source/user/connection.rst +++ b/doc/source/user/connection.rst @@ -11,6 +11,7 @@ Connection Object .. autoclass:: openstack.connection.Connection :members: + :inherited-members: Transitioning from Profile diff --git a/openstack/cloud/__init__.py b/openstack/cloud/__init__.py index 22ea13756..c3ad1f9cf 100644 --- a/openstack/cloud/__init__.py +++ b/openstack/cloud/__init__.py @@ -56,6 +56,8 @@ def openstack_clouds( def openstack_cloud( config=None, strict=False, app_name=None, app_version=None, **kwargs): + # Late import while we unwind things + from openstack import connection if not config: config = _get_openstack_config(app_name, app_version) try: @@ -63,4 +65,4 @@ def openstack_cloud( except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin as e: raise OpenStackCloudException( "Invalid cloud configuration: {exc}".format(exc=str(e))) - return OpenStackCloud(cloud_config=cloud_region, strict=strict) + return connection.Connection(config=cloud_region, strict=strict) diff --git a/openstack/cloud/_normalize.py b/openstack/cloud/_normalize.py index d56809069..ceb08bcb8 100644 --- a/openstack/cloud/_normalize.py +++ b/openstack/cloud/_normalize.py @@ -518,8 +518,8 @@ def _normalize_server(self, server): ret['config_drive'] = config_drive ret['project_id'] = project_id ret['tenant_id'] = project_id - ret['region'] = self.region_name - ret['cloud'] = self.name + ret['region'] = self.config.region_name + ret['cloud'] = self.config.name ret['az'] = az for key, val in ret['properties'].items(): ret.setdefault(key, val) diff --git a/openstack/cloud/inventory.py b/openstack/cloud/inventory.py index 5c488c2f9..9f0120c43 100644 --- a/openstack/cloud/inventory.py +++ b/openstack/cloud/inventory.py @@ -14,8 +14,9 @@ import functools -import openstack.config -import openstack.cloud +from openstack.config import loader +from openstack import connection +from openstack import exceptions from openstack.cloud import _utils @@ -30,24 +31,20 @@ def __init__( use_direct_get=False): if config_files is None: config_files = [] - config = openstack.config.loader.OpenStackConfig( - config_files=openstack.config.loader.CONFIG_FILES + config_files) + config = loader.OpenStackConfig( + config_files=loader.CONFIG_FILES + config_files) self.extra_config = config.get_extra_config( config_key, config_defaults) if cloud is None: self.clouds = [ - openstack.cloud.OpenStackCloud(cloud_config=cloud_region) + connection.Connection(config=cloud_region) for cloud_region in config.get_all() ] else: - try: - self.clouds = [ - openstack.cloud.OpenStackCloud( - cloud_config=config.get_one(cloud)) - ] - except openstack.config.exceptions.OpenStackConfigException as e: - raise openstack.cloud.OpenStackCloudException(e) + self.clouds = [ + connection.Connection(config=config.get_one(cloud)) + ] if private: for cloud in self.clouds: @@ -66,7 +63,7 @@ def list_hosts(self, expand=True, fail_on_cloud_config=True): # Cycle on servers for server in cloud.list_servers(detailed=expand): hostvars.append(server) - except openstack.cloud.OpenStackCloudException: + except exceptions.OpenStackCloudException: # Don't fail on one particular cloud as others may work if fail_on_cloud_config: raise diff --git a/openstack/cloud/meta.py b/openstack/cloud/meta.py index 173370150..30728a207 100644 --- a/openstack/cloud/meta.py +++ b/openstack/cloud/meta.py @@ -317,7 +317,7 @@ def _get_interface_ip(cloud, server): def get_groups_from_server(cloud, server, server_vars): groups = [] - region = cloud.region_name + region = cloud.config.region_name cloud_name = cloud.name # Create a group for the cloud diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 956514a04..badc38271 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -39,7 +39,6 @@ import keystoneauth1.exceptions import keystoneauth1.session -from openstack import version as openstack_version from openstack import _adapter from openstack import _log from openstack.cloud.exc import * # noqa @@ -118,65 +117,30 @@ class OpenStackCloud(_normalize.Normalizer): and that Floating IP will be actualized either via neutron or via nova depending on how this particular cloud has decided to arrange itself. - :param TaskManager manager: Optional task manager to use for running - OpenStack API tasks. Unless you're doing - rate limiting client side, you almost - certainly don't need this. (optional) :param bool strict: Only return documented attributes for each resource as per the Data Model contract. (Default False) - :param app_name: Name of the application to be appended to the user-agent - string. Optional, defaults to None. - :param app_version: Version of the application to be appended to the - user-agent string. Optional, defaults to None. - :param CloudRegion cloud_config: Cloud config object from os-client-config - In the future, this will be the only way - to pass in cloud configuration, but is - being phased in currently. """ - def __init__( - self, - cloud_config=None, - manager=None, - strict=False, - app_name=None, - app_version=None, - use_direct_get=False, - conn=None, - **kwargs): + def __init__(self): self.log = _log.setup_logging('openstack') - if not cloud_config: - config = openstack.config.OpenStackConfig( - app_name=app_name, app_version=app_version) - - cloud_config = config.get_one(**kwargs) - - self.name = cloud_config.name - self.auth = cloud_config.get_auth_args() - self.region_name = cloud_config.region_name - self.default_interface = cloud_config.get_interface() - self.private = cloud_config.config.get('private', False) - self.image_api_use_tasks = cloud_config.config['image_api_use_tasks'] - self.secgroup_source = cloud_config.config['secgroup_source'] - self.force_ipv4 = cloud_config.force_ipv4 - self.strict_mode = strict - - if manager is not None: - self.manager = manager - else: - self.manager = task_manager.TaskManager( - name=':'.join([self.name, self.region_name])) - - self._external_ipv4_names = cloud_config.get_external_ipv4_networks() - self._internal_ipv4_names = cloud_config.get_internal_ipv4_networks() - self._external_ipv6_names = cloud_config.get_external_ipv6_networks() - self._internal_ipv6_names = cloud_config.get_internal_ipv6_networks() - self._nat_destination = cloud_config.get_nat_destination() - self._default_network = cloud_config.get_default_network() - - self._floating_ip_source = cloud_config.config.get( + self.name = self.config.name + self.auth = self.config.get_auth_args() + self.default_interface = self.config.get_interface() + self.private = self.config.config.get('private', False) + self.image_api_use_tasks = self.config.config['image_api_use_tasks'] + self.secgroup_source = self.config.config['secgroup_source'] + self.force_ipv4 = self.config.force_ipv4 + + self._external_ipv4_names = self.config.get_external_ipv4_networks() + self._internal_ipv4_names = self.config.get_internal_ipv4_networks() + self._external_ipv6_names = self.config.get_external_ipv6_networks() + self._internal_ipv6_names = self.config.get_internal_ipv6_networks() + self._nat_destination = self.config.get_nat_destination() + self._default_network = self.config.get_default_network() + + self._floating_ip_source = self.config.config.get( 'floating_ip_source') if self._floating_ip_source: if self._floating_ip_source.lower() == 'none': @@ -184,16 +148,12 @@ def __init__( else: self._floating_ip_source = self._floating_ip_source.lower() - self._use_external_network = cloud_config.config.get( + self._use_external_network = self.config.config.get( 'use_external_network', True) - self._use_internal_network = cloud_config.config.get( + self._use_internal_network = self.config.config.get( 'use_internal_network', True) - # Work around older TaskManager objects that don't have submit_task - if not hasattr(self.manager, 'submit_task'): - self.manager.submit_task = self.manager.submitTask - - (self.verify, self.cert) = cloud_config.get_requests_verify_args() + (self.verify, self.cert) = self.config.get_requests_verify_args() # Turn off urllib3 warnings about insecure certs if we have # explicitly configured requests to tell it we do not want # cert verification @@ -206,7 +166,6 @@ def __init__( warnings.filterwarnings('ignore', category=category) self._disable_warnings = {} - self.use_direct_get = use_direct_get self._servers = None self._servers_time = 0 @@ -227,9 +186,9 @@ def __init__( self._networks_lock = threading.Lock() self._reset_network_caches() - cache_expiration_time = int(cloud_config.get_cache_expiration_time()) - cache_class = cloud_config.get_cache_class() - cache_arguments = cloud_config.get_cache_arguments() + cache_expiration_time = int(self.config.get_cache_expiration_time()) + cache_class = self.config.get_cache_class() + cache_arguments = self.config.get_cache_arguments() self._resource_caches = {} @@ -237,7 +196,7 @@ def __init__( self.cache_enabled = True self._cache = self._make_cache( cache_class, cache_expiration_time, cache_arguments) - expirations = cloud_config.get_cache_expiration() + expirations = self.config.get_cache_expiration() for expire_key in expirations.keys(): # Only build caches for things we have list operations for if getattr( @@ -279,36 +238,21 @@ def invalidate(self): # If server expiration time is set explicitly, use that. Otherwise # fall back to whatever it was before - self._SERVER_AGE = cloud_config.get_cache_resource_expiration( + self._SERVER_AGE = self.config.get_cache_resource_expiration( 'server', self._SERVER_AGE) - self._PORT_AGE = cloud_config.get_cache_resource_expiration( + self._PORT_AGE = self.config.get_cache_resource_expiration( 'port', self._PORT_AGE) - self._FLOAT_AGE = cloud_config.get_cache_resource_expiration( + self._FLOAT_AGE = self.config.get_cache_resource_expiration( 'floating_ip', self._FLOAT_AGE) self._container_cache = dict() self._file_hash_cache = dict() - self._keystone_session = None - self._raw_clients = {} self._local_ipv6 = ( _utils.localhost_supports_ipv6() if not self.force_ipv4 else False) - self.cloud_config = cloud_config - self._conn_object = conn - - @property - def _conn(self): - if not self._conn_object: - # Importing late to avoid import cycle. If the OpenStackCloud - # object comes via Connection, it'll have connection passed in. - import openstack.connection - self._conn_object = openstack.connection.Connection( - config=self.cloud_config, session=self._keystone_session) - return self._conn_object - def connect_as(self, **kwargs): """Make a new OpenStackCloud object with new auth context. @@ -332,11 +276,12 @@ def connect_as(self, **kwargs): that do not want to be overridden can be ommitted. """ + # TODO(mordred) Replace this with from_session config = openstack.config.OpenStackConfig( - app_name=self.cloud_config._app_name, - app_version=self.cloud_config._app_version, + app_name=self.config._app_name, + app_version=self.config._app_version, load_yaml_config=False) - params = copy.deepcopy(self.cloud_config.config) + params = copy.deepcopy(self.config.config) # Remove profile from current cloud so that overridding works params.pop('profile', None) @@ -373,7 +318,7 @@ def pop_keys(params, auth, name_key, id_key): def session_constructor(*args, **kwargs): # We need to pass our current keystone session to the Session # Constructor, otherwise the new auth plugin doesn't get used. - return keystoneauth1.session.Session(session=self.keystone_session) + return keystoneauth1.session.Session(session=self.session) # Use cloud='defaults' so that we overlay settings properly cloud_config = config.get_one( @@ -385,7 +330,7 @@ def session_constructor(*args, **kwargs): cloud_config.config['profile'] = self.name # Use self.__class__ so that we return whatever this if, like if it's # a subclass in the case of shade wrapping sdk. - return self.__class__(cloud_config=cloud_config) + return self.__class__(config=cloud_config) def connect_as_project(self, project): """Make a new OpenStackCloud object with a new project. @@ -457,7 +402,7 @@ def _get_major_version_id(self, version): def _get_versioned_client( self, service_type, min_version=None, max_version=None): - config_version = self.cloud_config.get_api_version(service_type) + config_version = self.config.get_api_version(service_type) config_major = self._get_major_version_id(config_version) max_major = self._get_major_version_id(max_version) min_major = self._get_major_version_id(min_version) @@ -492,33 +437,33 @@ def _get_versioned_client( request_max_version = '{version}.latest'.format( version=config_major) adapter = _adapter.ShadeAdapter( - session=self.keystone_session, - task_manager=self.manager, - service_type=self.cloud_config.get_service_type(service_type), - service_name=self.cloud_config.get_service_name(service_type), - interface=self.cloud_config.get_interface(service_type), - endpoint_override=self.cloud_config.get_endpoint(service_type), - region_name=self.cloud_config.region_name, + session=self.session, + task_manager=self.task_manager, + service_type=self.config.get_service_type(service_type), + service_name=self.config.get_service_name(service_type), + interface=self.config.get_interface(service_type), + endpoint_override=self.config.get_endpoint(service_type), + region_name=self.config.region_name, min_version=request_min_version, max_version=request_max_version) if adapter.get_endpoint(): return adapter adapter = _adapter.ShadeAdapter( - session=self.keystone_session, - task_manager=self.manager, - service_type=self.cloud_config.get_service_type(service_type), - service_name=self.cloud_config.get_service_name(service_type), - interface=self.cloud_config.get_interface(service_type), - endpoint_override=self.cloud_config.get_endpoint(service_type), - region_name=self.cloud_config.region_name, + session=self.session, + task_manager=self.task_manager, + service_type=self.config.get_service_type(service_type), + service_name=self.config.get_service_name(service_type), + interface=self.config.get_interface(service_type), + endpoint_override=self.config.get_endpoint(service_type), + region_name=self.config.region_name, min_version=min_version, max_version=max_version) # data.api_version can be None if no version was detected, such # as with neutron api_version = adapter.get_api_major_version( - endpoint_override=self.cloud_config.get_endpoint(service_type)) + endpoint_override=self.config.get_endpoint(service_type)) api_major = self._get_major_version_id(api_version) # If we detect a different version that was configured, warn the user. @@ -544,14 +489,14 @@ def _get_versioned_client( def _get_raw_client( self, service_type, api_version=None, endpoint_override=None): return _adapter.ShadeAdapter( - session=self.keystone_session, - task_manager=self.manager, - service_type=self.cloud_config.get_service_type(service_type), - service_name=self.cloud_config.get_service_name(service_type), - interface=self.cloud_config.get_interface(service_type), - endpoint_override=self.cloud_config.get_endpoint( + session=self.session, + task_manager=self.task_manager, + service_type=self.config.get_service_type(service_type), + service_name=self.config.get_service_name(service_type), + interface=self.config.get_interface(service_type), + endpoint_override=self.config.get_endpoint( service_type) or endpoint_override, - region_name=self.cloud_config.region_name) + region_name=self.config.region_name) def _is_client_version(self, client, version): client_name = '_{client}_client'.format(client=client) @@ -673,23 +618,9 @@ def pformat(self, resource): new_resource = _utils._dictify_resource(resource) return pprint.pformat(new_resource) - @property - def keystone_session(self): - if self._keystone_session is None: - try: - self._keystone_session = self.cloud_config.get_session() - if hasattr(self._keystone_session, 'additional_user_agent'): - self._keystone_session.additional_user_agent.append( - ('openstacksdk', openstack_version.__version__)) - except Exception as e: - raise OpenStackCloudException( - "Error authenticating to keystone: %s " % str(e)) - return self._keystone_session - @property def _keystone_catalog(self): - return self.keystone_session.auth.get_access( - self.keystone_session).service_catalog + return self.session.auth.get_access(self.session).service_catalog @property def service_catalog(self): @@ -703,13 +634,12 @@ def endpoint_for(self, service_type, interface='public'): def auth_token(self): # Keystone's session will reuse a token if it is still valid. # We don't need to track validity here, just get_token() each time. - return self.keystone_session.get_token() + return self.session.get_token() @property def current_user_id(self): """Get the id of the currently logged-in user from the token.""" - return self.keystone_session.auth.get_access( - self.keystone_session).user_id + return self.session.auth.get_access(self.session).user_id @property def current_project_id(self): @@ -723,7 +653,7 @@ def current_project_id(self): :raises keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin: if a plugin is not available. """ - return self.keystone_session.get_project_id() + return self.session.get_project_id() @property def current_project(self): @@ -748,7 +678,7 @@ def _get_project_info(self, project_id=None): # If they don't match, that means we're an admin who has pulled # an object from a different project, so adding info from the # current token would be wrong. - auth_args = self.cloud_config.config.get('auth', {}) + auth_args = self.config.config.get('auth', {}) project_info['id'] = self.current_project_id project_info['name'] = auth_args.get('project_name') project_info['domain_id'] = auth_args.get('project_domain_id') @@ -763,7 +693,7 @@ def current_location(self): def _get_current_location(self, project_id=None, zone=None): return munch.Munch( cloud=self.name, - region_name=self.region_name, + region_name=self.config.region_name, zone=zone, project=self._get_project_info(project_id), ) @@ -1419,7 +1349,7 @@ def get_name(self): return self.name def get_region(self): - return self.region_name + return self.config.region_name def get_flavor_name(self, flavor_id): flavor = self.get_flavor(flavor_id, get_extra=False) @@ -1449,7 +1379,7 @@ def get_flavor_by_ram(self, ram, include=None, get_extra=True): def get_session_endpoint(self, service_key): try: - return self.cloud_config.get_session_endpoint(service_key) + return self.config.get_session_endpoint(service_key) except keystoneauth1.exceptions.catalog.EndpointNotFound as e: self.log.debug( "Endpoint not found in %s cloud: %s", self.name, str(e)) @@ -1462,12 +1392,12 @@ def get_session_endpoint(self, service_key): " {error}".format( service=service_key, cloud=self.name, - region=self.region_name, + region=self.config.region_name, error=str(e))) return endpoint def has_service(self, service_key): - if not self.cloud_config.config.get('has_%s' % service_key, True): + if not self.config.config.get('has_%s' % service_key, True): # TODO(mordred) add a stamp here so that we only report this once if not (service_key in self._disable_warnings and self._disable_warnings[service_key]): @@ -1489,7 +1419,7 @@ def has_service(self, service_key): def _nova_extensions(self): extensions = set() data = _adapter._json_response( - self._conn.compute.get('/extensions'), + self.compute.get('/extensions'), error_message="Error fetching extension list for nova") for extension in self._get_and_munchify('extensions', data): @@ -1694,7 +1624,7 @@ def list_keypairs(self): """ data = _adapter._json_response( - self._conn.compute.get('/os-keypairs'), + self.compute.get('/os-keypairs'), error_message="Error fetching keypair list") return self._normalize_keypairs([ k['keypair'] for k in self._get_and_munchify('keypairs', data)]) @@ -1921,7 +1851,7 @@ def list_availability_zone_names(self, unavailable=False): """ try: data = _adapter._json_response( - self._conn.compute.get('/os-availability-zone')) + self.compute.get('/os-availability-zone')) except OpenStackCloudHTTPError: self.log.debug( "Availability zone list could not be fetched", @@ -1946,7 +1876,7 @@ def list_flavors(self, get_extra=False): """ data = _adapter._json_response( - self._conn.compute.get( + self.compute.get( '/flavors/detail', params=dict(is_public='None')), error_message="Error fetching flavor list") flavors = self._normalize_flavors( @@ -1958,7 +1888,7 @@ def list_flavors(self, get_extra=False): id=flavor.id) try: data = _adapter._json_response( - self._conn.compute.get(endpoint), + self.compute.get(endpoint), error_message="Error fetching flavor extra specs") flavor.extra_specs = self._get_and_munchify( 'extra_specs', data) @@ -1995,7 +1925,7 @@ def list_server_security_groups(self, server): return [] data = _adapter._json_response( - self._conn.compute.get( + self.compute.get( '/servers/{server_id}/os-security-groups'.format( server_id=server['id']))) return self._normalize_secgroups( @@ -2051,7 +1981,7 @@ def add_server_security_groups(self, server, security_groups): return False for sg in security_groups: - _adapter._json_response(self._conn.compute.post( + _adapter._json_response(self.compute.post( '/servers/%s/action' % server['id'], json={'addSecurityGroup': {'name': sg.name}})) @@ -2079,7 +2009,7 @@ def remove_server_security_groups(self, server, security_groups): for sg in security_groups: try: - _adapter._json_response(self._conn.compute.post( + _adapter._json_response(self.compute.post( '/servers/%s/action' % server['id'], json={'removeSecurityGroup': {'name': sg.name}})) @@ -2121,7 +2051,7 @@ def list_security_groups(self, filters=None): # Handle nova security groups else: - data = _adapter._json_response(self._conn.compute.get( + data = _adapter._json_response(self.compute.get( '/os-security-groups', params=filters)) return self._normalize_secgroups( self._get_and_munchify('security_groups', data)) @@ -2169,13 +2099,13 @@ def _list_servers(self, detailed=False, all_projects=False, bare=False, filters=None): error_msg = "Error fetching server list on {cloud}:{region}:".format( cloud=self.name, - region=self.region_name) + region=self.config.region_name) params = filters or {} if all_projects: params['all_tenants'] = True data = _adapter._json_response( - self._conn.compute.get( + self.compute.get( '/servers/detail', params=params), error_message=error_msg) servers = self._normalize_servers( @@ -2192,7 +2122,7 @@ def list_server_groups(self): """ data = _adapter._json_response( - self._conn.compute.get('/os-server-groups'), + self.compute.get('/os-server-groups'), error_message="Error fetching server group list") return self._get_and_munchify('server_groups', data) @@ -2219,7 +2149,7 @@ def get_compute_limits(self, name_or_id=None): msg=error_msg, project=name_or_id) data = _adapter._json_response( - self._conn.compute.get('/limits', params=params)) + self.compute.get('/limits', params=params)) limits = self._get_and_munchify('limits', data) return self._normalize_compute_limits(limits, project_id=project_id) @@ -2254,7 +2184,7 @@ def list_images(self, filter_deleted=True, show_all=False): # We didn't have glance, let's try nova # If this doesn't work - we just let the exception propagate response = _adapter._json_response( - self._conn.compute.get('/images/detail')) + self.compute.get('/images/detail')) while 'next' in response: image_list.extend(meta.obj_list_to_munch(response['images'])) endpoint = response['next'] @@ -2295,7 +2225,7 @@ def list_floating_ip_pools(self): 'Floating IP pools extension is not available on target cloud') data = _adapter._json_response( - self._conn.compute.get('os-floating-ip-pools'), + self.compute.get('os-floating-ip-pools'), error_message="Error fetching floating IP pool list") pools = self._get_and_munchify('floating_ip_pools', data) return [{'name': p['name']} for p in pools] @@ -2386,7 +2316,7 @@ def _neutron_list_floating_ips(self, filters=None): def _nova_list_floating_ips(self): try: data = _adapter._json_response( - self._conn.compute.get('/os-floating-ips')) + self.compute.get('/os-floating-ips')) except OpenStackCloudURINotFound: return [] return self._get_and_munchify('floating_ips', data) @@ -2991,7 +2921,7 @@ def get_flavor_by_id(self, id, get_extra=False): :returns: A flavor ``munch.Munch``. """ data = _adapter._json_response( - self._conn.compute.get('/flavors/{id}'.format(id=id)), + self.compute.get('/flavors/{id}'.format(id=id)), error_message="Error getting flavor with ID {id}".format(id=id) ) flavor = self._normalize_flavor( @@ -3002,7 +2932,7 @@ def get_flavor_by_id(self, id, get_extra=False): id=flavor.id) try: data = _adapter._json_response( - self._conn.compute.get(endpoint), + self.compute.get(endpoint), error_message="Error fetching flavor extra specs") flavor.extra_specs = self._get_and_munchify( 'extra_specs', data) @@ -3058,7 +2988,7 @@ def get_security_group_by_id(self, id): error_message=error_message) else: data = _adapter._json_response( - self._conn.compute.get( + self.compute.get( '/os-security-groups/{id}'.format(id=id)), error_message=error_message) return self._normalize_secgroup( @@ -3091,7 +3021,7 @@ def get_server_console(self, server, length=None): return "" def _get_server_console_output(self, server_id, length=None): - data = _adapter._json_response(self._conn.compute.post( + data = _adapter._json_response(self.compute.post( '/servers/{server_id}/action'.format(server_id=server_id), json={'os-getConsoleOutput': {'length': length}})) return self._get_and_munchify('output', data) @@ -3145,7 +3075,7 @@ def _expand_server(self, server, detailed, bare): def get_server_by_id(self, id): data = _adapter._json_response( - self._conn.compute.get('/servers/{id}'.format(id=id))) + self.compute.get('/servers/{id}'.format(id=id))) server = self._get_and_munchify('server', data) return meta.add_server_interfaces(self, self._normalize_server(server)) @@ -3304,7 +3234,7 @@ def get_floating_ip_by_id(self, id): self._get_and_munchify('floatingip', data)) else: data = _adapter._json_response( - self._conn.compute.get('/os-floating-ips/{id}'.format(id=id)), + self.compute.get('/os-floating-ips/{id}'.format(id=id)), error_message=error_message) return self._normalize_floating_ip( self._get_and_munchify('floating_ip', data)) @@ -3355,7 +3285,7 @@ def create_keypair(self, name, public_key=None): if public_key: keypair['public_key'] = public_key data = _adapter._json_response( - self._conn.compute.post( + self.compute.post( '/os-keypairs', json={'keypair': keypair}), error_message="Unable to create keypair {name}".format(name=name)) @@ -3372,7 +3302,7 @@ def delete_keypair(self, name): :raises: OpenStackCloudException on operation error. """ try: - _adapter._json_response(self._conn.compute.delete( + _adapter._json_response(self.compute.delete( '/os-keypairs/{name}'.format(name=name))) except OpenStackCloudURINotFound: self.log.debug("Keypair %s not found for deleting", name) @@ -4432,7 +4362,7 @@ def create_image_snapshot( " could not be snapshotted.".format(server=server)) server = server_obj response = _adapter._json_response( - self._conn.compute.post( + self.compute.post( '/servers/{server_id}/action'.format(server_id=server['id']), json={ "createImage": { @@ -4523,7 +4453,7 @@ def _get_name_and_filename(self, name): # Try appending the disk format name_with_ext = '.'.join(( - name, self.cloud_config.config['image_format'])) + name, self.config.config['image_format'])) if os.path.exists(name_with_ext): return (os.path.basename(name), name_with_ext) @@ -4615,7 +4545,7 @@ def create_image( meta = {} if not disk_format: - disk_format = self.cloud_config.config['image_format'] + disk_format = self.config.config['image_format'] if not container_format: # https://docs.openstack.org/image-guide/image-formats.html container_format = 'bare' @@ -4661,7 +4591,7 @@ def create_image( kwargs[IMAGE_OBJECT_KEY] = '/'.join([container, name]) if disable_vendor_agent: - kwargs.update(self.cloud_config.config['disable_vendor_agent']) + kwargs.update(self.config.config['disable_vendor_agent']) # We can never have nice things. Glance v1 took "is_public" as a # boolean. Glance v2 takes "visibility". If the user gives us @@ -5169,7 +5099,7 @@ def detach_volume(self, server, volume, wait=True, timeout=None): :raises: OpenStackCloudException on operation error. """ - _adapter._json_response(self._conn.compute.delete( + _adapter._json_response(self.compute.delete( '/servers/{server_id}/os-volume_attachments/{volume_id}'.format( server_id=server['id'], volume_id=volume['id'])), error_message=( @@ -5237,7 +5167,7 @@ def attach_volume(self, server, volume, device=None, if device: payload['device'] = device data = _adapter._json_response( - self._conn.compute.post( + self.compute.post( '/servers/{server_id}/os-volume_attachments'.format( server_id=server['id']), json=dict(volumeAttachment=payload)), @@ -5884,12 +5814,12 @@ def _nova_create_floating_ip(self, pool=None): "unable to find a floating ip pool") pool = pools[0]['name'] - data = _adapter._json_response(self._conn.compute.post( + data = _adapter._json_response(self.compute.post( '/os-floating-ips', json=dict(pool=pool))) pool_ip = self._get_and_munchify('floating_ip', data) # TODO(mordred) Remove this - it's just for compat data = _adapter._json_response( - self._conn.compute.get('/os-floating-ips/{id}'.format( + self.compute.get('/os-floating-ips/{id}'.format( id=pool_ip['id']))) return self._get_and_munchify('floating_ip', data) @@ -5959,7 +5889,7 @@ def _neutron_delete_floating_ip(self, floating_ip_id): def _nova_delete_floating_ip(self, floating_ip_id): try: _adapter._json_response( - self._conn.compute.delete( + self.compute.delete( '/os-floating-ips/{id}'.format(id=floating_ip_id)), error_message='Unable to delete floating IP {fip_id}'.format( fip_id=floating_ip_id)) @@ -6209,7 +6139,7 @@ def _nova_attach_ip_to_server(self, server_id, floating_ip_id, if fixed_address: body['fixed_address'] = fixed_address return _adapter._json_response( - self._conn.compute.post( + self.compute.post( '/servers/{server_id}/action'.format(server_id=server_id), json=dict(addFloatingIp=body)), error_message=error_message) @@ -6261,7 +6191,7 @@ def _nova_detach_ip_from_server(self, server_id, floating_ip_id): error_message = "Error detaching IP {ip} from instance {id}".format( ip=floating_ip_id, id=server_id) return _adapter._json_response( - self._conn.compute.post( + self.compute.post( '/servers/{server_id}/action'.format(server_id=server_id), json=dict(removeFloatingIp=dict( address=f_ip['floating_ip_address']))), @@ -6512,7 +6442,7 @@ def _get_boot_from_volume_kwargs( 'Volume {boot_volume} is not a valid volume' ' in {cloud}:{region}'.format( boot_volume=boot_volume, - cloud=self.name, region=self.region_name)) + cloud=self.name, region=self.config.region_name)) block_mapping = { 'boot_index': '0', 'delete_on_termination': terminate_volume, @@ -6533,7 +6463,7 @@ def _get_boot_from_volume_kwargs( 'Image {image} is not a valid image in' ' {cloud}:{region}'.format( image=image, - cloud=self.name, region=self.region_name)) + cloud=self.name, region=self.config.region_name)) block_mapping = { 'boot_index': '0', @@ -6563,7 +6493,7 @@ def _get_boot_from_volume_kwargs( 'Volume {volume} is not a valid volume' ' in {cloud}:{region}'.format( volume=volume, - cloud=self.name, region=self.region_name)) + cloud=self.name, region=self.config.region_name)) block_mapping = { 'boot_index': '-1', 'delete_on_termination': False, @@ -6757,7 +6687,7 @@ def create_server( 'Network {network} is not a valid network in' ' {cloud}:{region}'.format( network=network, - cloud=self.name, region=self.region_name)) + cloud=self.name, region=self.config.region_name)) nics.append({'net-id': network_obj['id']}) kwargs['nics'] = nics @@ -6827,7 +6757,7 @@ def create_server( endpoint = '/os-volumes_boot' with _utils.shade_exceptions("Error in creating instance"): data = _adapter._json_response( - self._conn.compute.post(endpoint, json={'server': kwargs})) + self.compute.post(endpoint, json={'server': kwargs})) server = self._get_and_munchify('server', data) admin_pass = server.get('adminPass') or kwargs.get('admin_pass') if not wait: @@ -6947,7 +6877,7 @@ def rebuild_server(self, server_id, image_id, admin_pass=None, kwargs['adminPass'] = admin_pass data = _adapter._json_response( - self._conn.compute.post( + self.compute.post( '/servers/{server_id}/action'.format(server_id=server_id), json={'rebuild': kwargs}), error_message="Error in rebuilding instance") @@ -6997,7 +6927,7 @@ def set_server_metadata(self, name_or_id, metadata): 'Invalid Server {server}'.format(server=name_or_id)) _adapter._json_response( - self._conn.compute.post( + self.compute.post( '/servers/{server_id}/metadata'.format(server_id=server['id']), json={'metadata': metadata}), error_message='Error updating server metadata') @@ -7021,7 +6951,7 @@ def delete_server_metadata(self, name_or_id, metadata_keys): error_message = 'Error deleting metadata {key} on {server}'.format( key=key, server=name_or_id) _adapter._json_response( - self._conn.compute.delete( + self.compute.delete( '/servers/{server_id}/metadata/{key}'.format( server_id=server['id'], key=key)), @@ -7095,7 +7025,7 @@ def _delete_server( try: _adapter._json_response( - self._conn.compute.delete( + self.compute.delete( '/servers/{id}'.format(id=server['id'])), error_message="Error in deleting server") except OpenStackCloudURINotFound: @@ -7160,7 +7090,7 @@ def update_server(self, name_or_id, detailed=False, bare=False, **kwargs): "failed to find server '{server}'".format(server=name_or_id)) data = _adapter._json_response( - self._conn.compute.put( + self.compute.put( '/servers/{server_id}'.format(server_id=server['id']), json={'server': kwargs}), error_message="Error updating server {0}".format(name_or_id)) @@ -7179,7 +7109,7 @@ def create_server_group(self, name, policies): :raises: OpenStackCloudException on operation error. """ data = _adapter._json_response( - self._conn.compute.post( + self.compute.post( '/os-server-groups', json={ 'server_group': { @@ -7205,7 +7135,7 @@ def delete_server_group(self, name_or_id): return False _adapter._json_response( - self._conn.compute.delete( + self.compute.delete( '/os-server-groups/{id}'.format(id=server_group['id'])), error_message="Error deleting server group {name}".format( name=name_or_id)) @@ -8149,7 +8079,7 @@ def create_security_group(self, name, description, project_id=None): json=security_group_json, error_message="Error creating security group {0}".format(name)) else: - data = _adapter._json_response(self._conn.compute.post( + data = _adapter._json_response(self.compute.post( '/os-security-groups', json=security_group_json)) return self._normalize_secgroup( self._get_and_munchify('security_group', data)) @@ -8188,7 +8118,7 @@ def delete_security_group(self, name_or_id): return True else: - _adapter._json_response(self._conn.compute.delete( + _adapter._json_response(self.compute.delete( '/os-security-groups/{id}'.format(id=secgroup['id']))) return True @@ -8226,7 +8156,7 @@ def update_security_group(self, name_or_id, **kwargs): for key in ('name', 'description'): kwargs.setdefault(key, group[key]) data = _adapter._json_response( - self._conn.compute.put( + self.compute.put( '/os-security-groups/{id}'.format(id=group['id']), json={'security-group': kwargs})) return self._normalize_secgroup( @@ -8361,7 +8291,7 @@ def create_security_group_rule(self, security_group_rule_dict[ 'security_group_rule']['tenant_id'] = project_id data = _adapter._json_response( - self._conn.compute.post( + self.compute.post( '/os-security-group-rules', json=security_group_rule_dict )) @@ -8396,7 +8326,7 @@ def delete_security_group_rule(self, rule_id): return True else: - _adapter._json_response(self._conn.compute.delete( + _adapter._json_response(self.compute.delete( '/os-security-group-rules/{id}'.format(id=rule_id))) return True @@ -10509,7 +10439,7 @@ def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", } if flavorid == 'auto': payload['id'] = None - data = _adapter._json_response(self._conn.compute.post( + data = _adapter._json_response(self.compute.post( '/flavors', json=dict(flavor=payload))) @@ -10532,7 +10462,7 @@ def delete_flavor(self, name_or_id): return False _adapter._json_response( - self._conn.compute.delete( + self.compute.delete( '/flavors/{id}'.format(id=flavor['id'])), error_message="Unable to delete flavor {name}".format( name=name_or_id)) @@ -10549,7 +10479,7 @@ def set_flavor_specs(self, flavor_id, extra_specs): :raises: OpenStackCloudResourceNotFound if flavor ID is not found. """ _adapter._json_response( - self._conn.compute.post( + self.compute.post( "/flavors/{id}/os-extra_specs".format(id=flavor_id), json=dict(extra_specs=extra_specs)), error_message="Unable to set flavor specs") @@ -10565,7 +10495,7 @@ def unset_flavor_specs(self, flavor_id, keys): """ for key in keys: _adapter._json_response( - self._conn.compute.delete( + self.compute.delete( "/flavors/{id}/os-extra_specs/{key}".format( id=flavor_id, key=key)), error_message="Unable to delete flavor spec {0}".format(key)) @@ -10581,7 +10511,7 @@ def _mod_flavor_access(self, action, flavor_id, project_id): access_key = '{action}TenantAccess'.format(action=action) _adapter._json_response( - self._conn.compute.post(endpoint, json={access_key: access})) + self.compute.post(endpoint, json={access_key: access})) def add_flavor_access(self, flavor_id, project_id): """Grant access to a private flavor for a project/tenant. @@ -10613,7 +10543,7 @@ def list_flavor_access(self, flavor_id): :raises: OpenStackCloudException on operation error. """ data = _adapter._json_response( - self._conn.compute.get( + self.compute.get( '/flavors/{id}/os-flavor-access'.format(id=flavor_id)), error_message=( "Error trying to list access from flavorID {flavor}".format( @@ -10892,7 +10822,7 @@ def list_hypervisors(self): """ data = _adapter._json_response( - self._conn.compute.get('/os-hypervisors/detail'), + self.compute.get('/os-hypervisors/detail'), error_message="Error fetching hypervisor list") return self._get_and_munchify('hypervisors', data) @@ -10917,7 +10847,7 @@ def list_aggregates(self): """ data = _adapter._json_response( - self._conn.compute.get('/os-aggregates'), + self.compute.get('/os-aggregates'), error_message="Error fetching aggregate list") return self._get_and_munchify('aggregates', data) @@ -10953,7 +10883,7 @@ def create_aggregate(self, name, availability_zone=None): :raises: OpenStackCloudException on operation error. """ data = _adapter._json_response( - self._conn.compute.post( + self.compute.post( '/os-aggregates', json={'aggregate': { 'name': name, @@ -10981,7 +10911,7 @@ def update_aggregate(self, name_or_id, **kwargs): "Host aggregate %s not found." % name_or_id) data = _adapter._json_response( - self._conn.compute.put( + self.compute.put( '/os-aggregates/{id}'.format(id=aggregate['id']), json={'aggregate': kwargs}), error_message="Error updating aggregate {name}".format( @@ -11003,7 +10933,7 @@ def delete_aggregate(self, name_or_id): return False return _adapter._json_response( - self._conn.compute.delete( + self.compute.delete( '/os-aggregates/{id}'.format(id=aggregate['id'])), error_message="Error deleting aggregate {name}".format( name=name_or_id)) @@ -11030,7 +10960,7 @@ def set_aggregate_metadata(self, name_or_id, metadata): name=name_or_id) data = _adapter._json_response( - self._conn.compute.post( + self.compute.post( '/os-aggregates/{id}/action'.format(id=aggregate['id']), json={'set_metadata': {'metadata': metadata}}), error_message=err_msg) @@ -11053,7 +10983,7 @@ def add_host_to_aggregate(self, name_or_id, host_name): host=host_name, name=name_or_id) return _adapter._json_response( - self._conn.compute.post( + self.compute.post( '/os-aggregates/{id}/action'.format(id=aggregate['id']), json={'add_host': {'host': host_name}}), error_message=err_msg) @@ -11075,7 +11005,7 @@ def remove_host_from_aggregate(self, name_or_id, host_name): host=host_name, name=name_or_id) return _adapter._json_response( - self._conn.compute.post( + self.compute.post( '/os-aggregates/{id}/action'.format(id=aggregate['id']), json={'remove_host': {'host': host_name}}), error_message=err_msg) @@ -11167,7 +11097,7 @@ def set_compute_quotas(self, name_or_id, **kwargs): kwargs['force'] = True _adapter._json_response( - self._conn.compute.put( + self.compute.put( '/os-quota-sets/{project}'.format(project=proj.id), json={'quota_set': kwargs}), error_message="No valid quota or resource") @@ -11184,7 +11114,7 @@ def get_compute_quotas(self, name_or_id): if not proj: raise OpenStackCloudException("project does not exist") data = _adapter._json_response( - self._conn.compute.get( + self.compute.get( '/os-quota-sets/{project}'.format(project=proj.id))) return self._get_and_munchify('quota_set', data) @@ -11201,7 +11131,7 @@ def delete_compute_quotas(self, name_or_id): if not proj: raise OpenStackCloudException("project does not exist") return _adapter._json_response( - self._conn.compute.delete( + self.compute.delete( '/os-quota-sets/{project}'.format(project=proj.id))) def get_compute_usage(self, name_or_id, start=None, end=None): @@ -11260,7 +11190,7 @@ def parse_datetime_for_nova(date): name=proj.id)) data = _adapter._json_response( - self._conn.compute.get( + self.compute.get( '/os-simple-tenant-usage/{project}'.format(project=proj.id), params=dict(start=start.isoformat(), end=end.isoformat())), error_message="Unable to get usage for project: {name}".format( diff --git a/openstack/connection.py b/openstack/connection.py index a7cad826d..57663b444 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -211,7 +211,8 @@ def from_config(cloud=None, config=None, options=None, **kwargs): return Connection(config=config) -class Connection(six.with_metaclass(_meta.ConnectionMeta)): +class Connection(six.with_metaclass(_meta.ConnectionMeta, + _cloud.OpenStackCloud)): def __init__(self, cloud=None, config=None, session=None, app_name=None, app_version=None, @@ -219,6 +220,8 @@ def __init__(self, cloud=None, config=None, session=None, # python-openstackclient to not use the profile interface. authenticator=None, profile=None, extra_services=None, + strict=False, + use_direct_get=False, **kwargs): """Create a connection to a cloud. @@ -305,17 +308,23 @@ def __init__(self, cloud=None, config=None, session=None, # TODO(mordred) Expose constructor option for this in OCC self.config._keystone_session = session - self.session = self.config.get_session() - # Hide a reference to the connection on the session to help with - # backwards compatibility for folks trying to just pass conn.session - # to a Resource method's session argument. - self.session._sdk_connection = self - + self._session = None self._proxies = {} - self.cloud = _cloud.OpenStackCloud( - cloud_config=self.config, - manager=self.task_manager, - conn=self) + self.use_direct_get = use_direct_get + self.strict_mode = strict + # Call the OpenStackCloud constructor while we work on integrating + # things better. + _cloud.OpenStackCloud.__init__(self) + + @property + def session(self): + if not self._session: + self._session = self.config.get_session() + # Hide a reference to the connection on the session to help with + # backwards compatibility for folks trying to just pass + # conn.session to a Resource method's session argument. + self.session._sdk_connection = self + return self._session def add_service(self, service): """Add a service to the Connection. diff --git a/openstack/tests/functional/cloud/base.py b/openstack/tests/functional/cloud/base.py index dbbcb474b..f84aa6684 100644 --- a/openstack/tests/functional/cloud/base.py +++ b/openstack/tests/functional/cloud/base.py @@ -16,7 +16,7 @@ import openstack.config as occ -import openstack.cloud +from openstack import connection from openstack.tests import base @@ -33,19 +33,17 @@ def setUp(self): self._set_operator_cloud() self.identity_version = \ - self.operator_cloud.cloud_config.get_api_version('identity') + self.operator_cloud.config.get_api_version('identity') def _set_user_cloud(self, **kwargs): user_config = self.config.get_one( cloud=self._demo_name, **kwargs) - self.user_cloud = openstack.cloud.OpenStackCloud( - cloud_config=user_config) + self.user_cloud = connection.Connection(config=user_config) def _set_operator_cloud(self, **kwargs): operator_config = self.config.get_one( cloud=self._op_name, **kwargs) - self.operator_cloud = openstack.cloud.OpenStackCloud( - cloud_config=operator_config) + self.operator_cloud = connection.Connection(config=operator_config) def pick_image(self): images = self.user_cloud.list_images() diff --git a/openstack/tests/functional/cloud/test_domain.py b/openstack/tests/functional/cloud/test_domain.py index a2c22bf09..bce1dc491 100644 --- a/openstack/tests/functional/cloud/test_domain.py +++ b/openstack/tests/functional/cloud/test_domain.py @@ -27,7 +27,7 @@ class TestDomain(base.BaseFunctionalTestCase): def setUp(self): super(TestDomain, self).setUp() - i_ver = self.operator_cloud.cloud_config.get_api_version('identity') + i_ver = self.operator_cloud.config.get_api_version('identity') if i_ver in ('2', '2.0'): self.skipTest('Identity service does not support domains') self.domain_prefix = self.getUniqueString('domain') diff --git a/openstack/tests/functional/cloud/test_endpoints.py b/openstack/tests/functional/cloud/test_endpoints.py index 2f5216905..8135a18da 100644 --- a/openstack/tests/functional/cloud/test_endpoints.py +++ b/openstack/tests/functional/cloud/test_endpoints.py @@ -103,7 +103,7 @@ def test_create_endpoint(self): self.assertIsNotNone(endpoints[0].get('id')) def test_update_endpoint(self): - ver = self.operator_cloud.cloud_config.get_api_version('identity') + ver = self.operator_cloud.config.get_api_version('identity') if ver.startswith('2'): # NOTE(SamYaple): Update endpoint only works with v3 api self.assertRaises(OpenStackCloudUnavailableFeature, diff --git a/openstack/tests/functional/cloud/test_groups.py b/openstack/tests/functional/cloud/test_groups.py index b5e3a4f7d..cad046752 100644 --- a/openstack/tests/functional/cloud/test_groups.py +++ b/openstack/tests/functional/cloud/test_groups.py @@ -27,7 +27,7 @@ class TestGroup(base.BaseFunctionalTestCase): def setUp(self): super(TestGroup, self).setUp() - i_ver = self.operator_cloud.cloud_config.get_api_version('identity') + i_ver = self.operator_cloud.config.get_api_version('identity') if i_ver in ('2', '2.0'): self.skipTest('Identity service does not support groups') self.group_prefix = self.getUniqueString('group') diff --git a/openstack/tests/functional/cloud/test_project.py b/openstack/tests/functional/cloud/test_project.py index 4ac522aa6..6f6277052 100644 --- a/openstack/tests/functional/cloud/test_project.py +++ b/openstack/tests/functional/cloud/test_project.py @@ -73,7 +73,7 @@ def test_create_project(self): new_cloud = self.operator_cloud.connect_as_project(project) self.add_info_on_exception( - 'new_cloud_config', pprint.pformat(new_cloud.cloud_config.config)) + 'new_cloud_config', pprint.pformat(new_cloud.config.config)) location = new_cloud.current_location self.assertEqual(project_name, location['project']['name']) diff --git a/openstack/tests/functional/cloud/test_services.py b/openstack/tests/functional/cloud/test_services.py index efaf1fd9b..8359e6598 100644 --- a/openstack/tests/functional/cloud/test_services.py +++ b/openstack/tests/functional/cloud/test_services.py @@ -65,7 +65,7 @@ def test_create_service(self): self.assertIsNotNone(service.get('id')) def test_update_service(self): - ver = self.operator_cloud.cloud_config.get_api_version('identity') + ver = self.operator_cloud.config.get_api_version('identity') if ver.startswith('2'): # NOTE(SamYaple): Update service only works with v3 api self.assertRaises(OpenStackCloudUnavailableFeature, diff --git a/openstack/tests/functional/cloud/test_users.py b/openstack/tests/functional/cloud/test_users.py index 3b8f5a27e..2ac99053a 100644 --- a/openstack/tests/functional/cloud/test_users.py +++ b/openstack/tests/functional/cloud/test_users.py @@ -42,7 +42,7 @@ def _cleanup_users(self): def _create_user(self, **kwargs): domain_id = None - i_ver = self.operator_cloud.cloud_config.get_api_version('identity') + i_ver = self.operator_cloud.config.get_api_version('identity') if i_ver not in ('2', '2.0'): domain = self.operator_cloud.get_domain('default') domain_id = domain['id'] @@ -143,7 +143,7 @@ def test_update_user_password(self): self.assertIsNotNone(new_cloud.service_catalog) def test_users_and_groups(self): - i_ver = self.operator_cloud.cloud_config.get_api_version('identity') + i_ver = self.operator_cloud.config.get_api_version('identity') if i_ver in ('2', '2.0'): self.skipTest('Identity service does not support groups') diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 2c4eaf08a..d32740a67 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -119,10 +119,11 @@ def _nosleep(seconds): secure_files=['non-existant']) self.cloud_config = self.config.get_one( cloud=test_cloud, validate=False) - self.cloud = openstack.cloud.OpenStackCloud( - cloud_config=self.cloud_config) - self.strict_cloud = openstack.cloud.OpenStackCloud( - cloud_config=self.cloud_config, + self.cloud = openstack.connection.Connection( + config=self.cloud_config, + strict=False) + self.strict_cloud = openstack.connection.Connection( + config=self.cloud_config, strict=True) @@ -460,8 +461,7 @@ def _make_test_cloud(self, cloud_name='_test_cloud_', **kwargs): cloud=test_cloud, validate=True, **kwargs) self.conn = openstack.connection.Connection( config=self.cloud_config) - self.cloud = openstack.cloud.OpenStackCloud( - cloud_config=self.cloud_config) + self.cloud = self.conn def get_glance_discovery_mock_dict( self, diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 9bc08b346..11a95deb1 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -58,7 +58,7 @@ def setUp(self): self.use_glance() def test_config_v1(self): - self.cloud.cloud_config.config['image_api_version'] = '1' + self.cloud.config.config['image_api_version'] = '1' # We override the scheme of the endpoint with the scheme of the service # because glance has a bug where it doesn't return https properly. self.assertEqual( @@ -68,7 +68,7 @@ def test_config_v1(self): '1', self.cloud_config.get_api_version('image')) def test_config_v2(self): - self.cloud.cloud_config.config['image_api_version'] = '2' + self.cloud.config.config['image_api_version'] = '2' # We override the scheme of the endpoint with the scheme of the service # because glance has a bug where it doesn't return https properly. self.assertEqual( @@ -937,7 +937,7 @@ def setUp(self): def test_config_v1(self): - self.cloud.cloud_config.config['image_api_version'] = '1' + self.cloud.config.config['image_api_version'] = '1' # We override the scheme of the endpoint with the scheme of the service # because glance has a bug where it doesn't return https properly. self.assertEqual( @@ -946,7 +946,7 @@ def test_config_v1(self): self.assertTrue(self.cloud._is_client_version('image', 1)) def test_config_v2(self): - self.cloud.cloud_config.config['image_api_version'] = '2' + self.cloud.config.config['image_api_version'] = '2' # We override the scheme of the endpoint with the scheme of the service # because glance has a bug where it doesn't return https properly. self.assertEqual( @@ -962,7 +962,7 @@ def setUp(self): self.use_glance(image_version_json='image-version-v2.json') def test_config_v1(self): - self.cloud.cloud_config.config['image_api_version'] = '1' + self.cloud.config.config['image_api_version'] = '1' # We override the scheme of the endpoint with the scheme of the service # because glance has a bug where it doesn't return https properly. self.assertEqual( @@ -971,7 +971,7 @@ def test_config_v1(self): self.assertTrue(self.cloud._is_client_version('image', 2)) def test_config_v2(self): - self.cloud.cloud_config.config['image_api_version'] = '2' + self.cloud.config.config['image_api_version'] = '2' # We override the scheme of the endpoint with the scheme of the service # because glance has a bug where it doesn't return https properly. self.assertEqual( diff --git a/openstack/tests/unit/cloud/test_inventory.py b/openstack/tests/unit/cloud/test_inventory.py index 4f4b04104..339fa6695 100644 --- a/openstack/tests/unit/cloud/test_inventory.py +++ b/openstack/tests/unit/cloud/test_inventory.py @@ -12,10 +12,8 @@ import mock -from openstack.cloud import exc from openstack.cloud import inventory import openstack.config -from openstack.config import exceptions as occ_exc from openstack.tests import fakes from openstack.tests.unit import base @@ -26,7 +24,7 @@ def setUp(self): super(TestInventory, self).setUp() @mock.patch("openstack.config.loader.OpenStackConfig") - @mock.patch("openstack.cloud.OpenStackCloud") + @mock.patch("openstack.connection.Connection") def test__init(self, mock_cloud, mock_config): mock_config.return_value.get_all.return_value = [{}] @@ -40,7 +38,7 @@ def test__init(self, mock_cloud, mock_config): self.assertTrue(mock_config.return_value.get_all.called) @mock.patch("openstack.config.loader.OpenStackConfig") - @mock.patch("openstack.cloud.OpenStackCloud") + @mock.patch("openstack.connection.Connection") def test__init_one_cloud(self, mock_cloud, mock_config): mock_config.return_value.get_one.return_value = [{}] @@ -56,23 +54,7 @@ def test__init_one_cloud(self, mock_cloud, mock_config): 'supercloud') @mock.patch("openstack.config.loader.OpenStackConfig") - @mock.patch("openstack.cloud.OpenStackCloud") - def test__raise_exception_on_no_cloud(self, mock_cloud, mock_config): - """ - Test that when os-client-config can't find a named cloud, a - shade exception is emitted. - """ - mock_config.return_value.get_one.side_effect = ( - occ_exc.OpenStackConfigException() - ) - self.assertRaises(exc.OpenStackCloudException, - inventory.OpenStackInventory, - cloud='supercloud') - mock_config.return_value.get_one.assert_called_once_with( - 'supercloud') - - @mock.patch("openstack.config.loader.OpenStackConfig") - @mock.patch("openstack.cloud.OpenStackCloud") + @mock.patch("openstack.connection.Connection") def test_list_hosts(self, mock_cloud, mock_config): mock_config.return_value.get_all.return_value = [{}] @@ -91,7 +73,7 @@ def test_list_hosts(self, mock_cloud, mock_config): self.assertEqual([server], ret) @mock.patch("openstack.config.loader.OpenStackConfig") - @mock.patch("openstack.cloud.OpenStackCloud") + @mock.patch("openstack.connection.Connection") def test_list_hosts_no_detail(self, mock_cloud, mock_config): mock_config.return_value.get_all.return_value = [{}] @@ -110,7 +92,7 @@ def test_list_hosts_no_detail(self, mock_cloud, mock_config): self.assertFalse(inv.clouds[0].get_openstack_vars.called) @mock.patch("openstack.config.loader.OpenStackConfig") - @mock.patch("openstack.cloud.OpenStackCloud") + @mock.patch("openstack.connection.Connection") def test_search_hosts(self, mock_cloud, mock_config): mock_config.return_value.get_all.return_value = [{}] @@ -126,7 +108,7 @@ def test_search_hosts(self, mock_cloud, mock_config): self.assertEqual([server], ret) @mock.patch("openstack.config.loader.OpenStackConfig") - @mock.patch("openstack.cloud.OpenStackCloud") + @mock.patch("openstack.connection.Connection") def test_get_host(self, mock_cloud, mock_config): mock_config.return_value.get_all.return_value = [{}] diff --git a/openstack/tests/unit/cloud/test_meta.py b/openstack/tests/unit/cloud/test_meta.py index cd2886713..5610d739e 100644 --- a/openstack/tests/unit/cloud/test_meta.py +++ b/openstack/tests/unit/cloud/test_meta.py @@ -1,4 +1,4 @@ -# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# Copyrigh # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -24,8 +24,12 @@ PUBLIC_V6 = '2001:0db8:face:0da0:face::0b00:1c' # rfc3849 -class FakeCloud(object): +class FakeConfig(object): region_name = 'test-region' + + +class FakeCloud(object): + config = FakeConfig() name = 'test-name' private = False force_ipv4 = False @@ -586,7 +590,7 @@ def test_get_server_cloud_missing_fips( def test_get_server_cloud_rackspace_v6( self, mock_get_flavor_name, mock_get_image_name, mock_get_volumes): - self.cloud.cloud_config.config['has_network'] = False + self.cloud.config.config['has_network'] = False self.cloud._floating_ip_source = None self.cloud.force_ipv4 = False self.cloud._local_ipv6 = True @@ -830,7 +834,7 @@ def test_get_server_external_ipv4_neutron_exception(self): def test_get_server_external_ipv4_nova_public(self): # Testing Clouds w/o Neutron and a network named public - self.cloud.cloud_config.config['has_network'] = False + self.cloud.config.config['has_network'] = False srv = fakes.make_fake_server( server_id='test-id', name='test-name', status='ACTIVE', @@ -841,7 +845,7 @@ def test_get_server_external_ipv4_nova_public(self): def test_get_server_external_ipv4_nova_none(self): # Testing Clouds w/o Neutron or a globally routable IP - self.cloud.cloud_config.config['has_network'] = False + self.cloud.config.config['has_network'] = False srv = fakes.make_fake_server( server_id='test-id', name='test-name', status='ACTIVE', diff --git a/openstack/tests/unit/cloud/test_operator.py b/openstack/tests/unit/cloud/test_operator.py index 90d72cd26..c1c3ed863 100644 --- a/openstack/tests/unit/cloud/test_operator.py +++ b/openstack/tests/unit/cloud/test_operator.py @@ -46,7 +46,7 @@ def side_effect(*args, **kwargs): session_mock.get_endpoint.side_effect = side_effect get_session_mock.return_value = session_mock self.cloud.name = 'testcloud' - self.cloud.region_name = 'testregion' + self.cloud.config.region_name = 'testregion' with testtools.ExpectedException( exc.OpenStackCloudException, "Error getting image endpoint on testcloud:testregion:" diff --git a/openstack/tests/unit/config/test_from_session.py b/openstack/tests/unit/config/test_from_session.py index abc13f14d..9b2b1fe29 100644 --- a/openstack/tests/unit/config/test_from_session.py +++ b/openstack/tests/unit/config/test_from_session.py @@ -30,7 +30,7 @@ class TestFromSession(base.RequestsMockTestCase): def test_from_session(self): config = cloud_region.from_session( - self.cloud.keystone_session, region_name=self.test_region) + self.cloud.session, region_name=self.test_region) self.assertEqual(config.name, 'identity.example.com') if not self.test_region: self.assertIsNone(config.region_name) diff --git a/releasenotes/notes/shade-into-connection-81191fb3d0ddaf6e.yaml b/releasenotes/notes/shade-into-connection-81191fb3d0ddaf6e.yaml new file mode 100644 index 000000000..7fdbf3b11 --- /dev/null +++ b/releasenotes/notes/shade-into-connection-81191fb3d0ddaf6e.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + All of the methods formerly part of the ``shade`` library have been added + to the `openstack.connection.Connection`` object. From 958a35a5ca7607f2f59b86b9583d1538eee78b8e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 30 Jan 2018 19:31:47 -0600 Subject: [PATCH 1966/3836] Remove openstack_cloud factory function This is mostly mechanical as these are no longer needed. Change-Id: I632675506bef591704068d17ec3dfad963a4c4b5 --- README.rst | 17 +++------ doc/source/user/multi-cloud-demo.rst | 38 +++++++++---------- examples/cloud/cleanup-servers.py | 2 +- examples/cloud/create-server-dict.py | 2 +- examples/cloud/create-server-name-or-id.py | 2 +- examples/cloud/debug-logging.py | 2 +- examples/cloud/find-an-image.py | 2 +- examples/cloud/http-debug-logging.py | 2 +- examples/cloud/munch-dict-object.py | 2 +- examples/cloud/normalization.py | 2 +- examples/cloud/server-information.py | 2 +- .../cloud/service-conditional-overrides.py | 2 +- examples/cloud/service-conditionals.py | 2 +- examples/cloud/strict-mode.py | 2 +- examples/cloud/upload-large-object.py | 2 +- examples/cloud/upload-object.py | 2 +- examples/cloud/user-agent.py | 2 +- openstack/cloud/__init__.py | 14 ------- openstack/cloud/inventory.py | 2 + openstack/cloud/openstackcloud.py | 10 ++--- openstack/tests/unit/cloud/test_caching.py | 6 +-- .../tests/unit/cloud/test_operator_noauth.py | 4 +- 22 files changed, 51 insertions(+), 70 deletions(-) diff --git a/README.rst b/README.rst index ab100dcdf..d453ef5a5 100644 --- a/README.rst +++ b/README.rst @@ -65,13 +65,6 @@ and provides different argument defaults where needed for compatibility. Similarly future releases of os-client-config will provide a compatibility layer shim around ``openstack.config``. -.. note:: - - The ``openstack.cloud.OpenStackCloud`` object and the - ``openstack.connection.Connection`` object are going to be merged. It is - recommended to not write any new code which consumes objects from the - ``openstack.cloud`` namespace until that merge is complete. - .. _nodepool: https://docs.openstack.org/infra/nodepool/ .. _Ansible OpenStack Modules: http://docs.ansible.com/ansible/latest/list_of_cloud_modules.html#openstack .. _Session: http://docs.python-requests.org/en/master/user/advanced/#session-objects @@ -143,20 +136,20 @@ Create a server using objects configured with the ``clouds.yaml`` file: # Initialize and turn on debug logging openstack.enable_logging(debug=True) - # Initialize cloud + # Initialize connection # Cloud configs are read with openstack.config - cloud = openstack.cloud.openstack_cloud(cloud='mordred') + conn = openstack.connect(cloud='mordred') # Upload an image to the cloud - image = cloud.create_image( + image = conn.create_image( 'ubuntu-trusty', filename='ubuntu-trusty.qcow2', wait=True) # Find a flavor with at least 512M of RAM - flavor = cloud.get_flavor_by_ram(512) + flavor = conn.get_flavor_by_ram(512) # Boot a server, wait for it to boot, and then do whatever is needed # to get a public ip for it. - cloud.create_server( + conn.create_server( 'my-server', image=image, flavor=flavor, wait=True, auto_ip=True) Links diff --git a/doc/source/user/multi-cloud-demo.rst b/doc/source/user/multi-cloud-demo.rst index 757796096..461af139b 100644 --- a/doc/source/user/multi-cloud-demo.rst +++ b/doc/source/user/multi-cloud-demo.rst @@ -72,7 +72,7 @@ Complete Example ('my-citycloud', 'Buf1'), ('my-internap', 'ams01')]: # Initialize cloud - cloud = openstack.openstack_cloud(cloud=cloud_name, region_name=region_name) + cloud = openstack.connect(cloud=cloud_name, region_name=region_name) # Upload an image to the cloud image = cloud.create_image( @@ -324,7 +324,7 @@ Complete Example Again ('my-citycloud', 'Buf1'), ('my-internap', 'ams01')]: # Initialize cloud - cloud = openstack.openstack_cloud(cloud=cloud_name, region_name=region_name) + cloud = openstack.connect(cloud=cloud_name, region_name=region_name) # Upload an image to the cloud image = cloud.create_image( @@ -376,7 +376,7 @@ Example with Debug Logging from openstack import cloud as openstack openstack.enable_logging(debug=True) - cloud = openstack.openstack_cloud( + cloud = openstack.connect( cloud='my-vexxhost', region_name='ca-ymq-1') cloud.get_image('Ubuntu 16.04.1 LTS [2017-03-03]') @@ -390,7 +390,7 @@ Example with HTTP Debug Logging from openstack import cloud as openstack openstack.enable_logging(http_debug=True) - cloud = openstack.openstack_cloud( + cloud = openstack.connect( cloud='my-vexxhost', region_name='ca-ymq-1') cloud.get_image('Ubuntu 16.04.1 LTS [2017-03-03]') @@ -398,7 +398,7 @@ Cloud Regions ============= * `cloud` constructor needs `cloud` and `region_name` -* `openstack.openstack_cloud` is a helper factory function +* `openstack.connect` is a helper factory function .. code:: python @@ -407,7 +407,7 @@ Cloud Regions ('my-citycloud', 'Buf1'), ('my-internap', 'ams01')]: # Initialize cloud - cloud = openstack.openstack_cloud(cloud=cloud_name, region_name=region_name) + cloud = openstack.connect(cloud=cloud_name, region_name=region_name) Upload an Image =============== @@ -497,7 +497,7 @@ Image and Flavor by Name or ID ('my-internap', 'ams01', 'Ubuntu 16.04 LTS (Xenial Xerus)', 'A1.4')]: # Initialize cloud - cloud = openstack.openstack_cloud(cloud=cloud_name, region_name=region_name) + cloud = openstack.connect(cloud=cloud_name, region_name=region_name) # Boot a server, wait for it to boot, and then do whatever is needed # to get a public ip for it. @@ -544,7 +544,7 @@ Image and Flavor by Dict ('my-internap', 'ams01', 'Ubuntu 16.04 LTS (Xenial Xerus)', 'A1.4')]: # Initialize cloud - cloud = openstack.openstack_cloud(cloud=cloud_name, region_name=region_name) + cloud = openstack.connect(cloud=cloud_name, region_name=region_name) # Boot a server, wait for it to boot, and then do whatever is needed # to get a public ip for it. @@ -565,7 +565,7 @@ Munch Objects from openstack import cloud as openstack openstack.enable_logging(debug=True) - cloud = openstack.openstack_cloud(cloud='zetta', region_name='no-osl1') + cloud = openstack.connect(cloud='zetta', region_name='no-osl1') image = cloud.get_image('Ubuntu 14.04 (AMD64) [Local Storage]') print(image.name) print(image['name']) @@ -604,7 +604,7 @@ Cleanup Script ('my-citycloud', 'Buf1'), ('my-internap', 'ams01')]: # Initialize cloud - cloud = openstack.openstack_cloud(cloud=cloud_name, region_name=region_name) + cloud = openstack.connect(cloud=cloud_name, region_name=region_name) for server in cloud.search_servers('my-server'): cloud.delete_server(server, wait=True, delete_ips=True) @@ -619,7 +619,7 @@ Normalization from openstack import cloud as openstack openstack.enable_logging() - cloud = openstack.openstack_cloud(cloud='fuga', region_name='cystack') + cloud = openstack.connect(cloud='fuga', region_name='cystack') image = cloud.get_image( 'Ubuntu 16.04 LTS - Xenial Xerus - 64-bit - Fuga Cloud Based Image') cloud.pprint(image) @@ -635,7 +635,7 @@ Strict Normalized Results from openstack import cloud as openstack openstack.enable_logging() - cloud = openstack.openstack_cloud( + cloud = openstack.connect( cloud='fuga', region_name='cystack', strict=True) image = cloud.get_image( 'Ubuntu 16.04 LTS - Xenial Xerus - 64-bit - Fuga Cloud Based Image') @@ -652,7 +652,7 @@ How Did I Find the Image Name for the Last Example? from openstack import cloud as openstack openstack.enable_logging() - cloud = openstack.openstack_cloud(cloud='fuga', region_name='cystack') + cloud = openstack.connect(cloud='fuga', region_name='cystack') cloud.pprint([ image for image in cloud.list_images() if 'ubuntu' in image.name.lower()]) @@ -673,7 +673,7 @@ Added / Modified Information from openstack import cloud as openstack openstack.enable_logging(debug=True) - cloud = openstack.openstack_cloud(cloud='my-citycloud', region_name='Buf1') + cloud = openstack.connect(cloud='my-citycloud', region_name='Buf1') try: server = cloud.create_server( 'my-server', image='Ubuntu 16.04 Xenial Xerus', @@ -715,7 +715,7 @@ User Agent Info from openstack import cloud as openstack openstack.enable_logging(http_debug=True) - cloud = openstack.openstack_cloud( + cloud = openstack.connect( cloud='datacentred', app_name='AmazingApp', app_version='1.0') cloud.list_networks() @@ -733,7 +733,7 @@ Uploading Large Objects from openstack import cloud as openstack openstack.enable_logging(debug=True) - cloud = openstack.openstack_cloud(cloud='ovh', region_name='SBG1') + cloud = openstack.connect(cloud='ovh', region_name='SBG1') cloud.create_object( container='my-container', name='my-object', filename='/home/mordred/briarcliff.sh3d') @@ -754,7 +754,7 @@ Uploading Large Objects from openstack import cloud as openstack openstack.enable_logging(debug=True) - cloud = openstack.openstack_cloud(cloud='ovh', region_name='SBG1') + cloud = openstack.connect(cloud='ovh', region_name='SBG1') cloud.create_object( container='my-container', name='my-object', filename='/home/mordred/briarcliff.sh3d', @@ -770,7 +770,7 @@ Service Conditionals from openstack import cloud as openstack openstack.enable_logging(debug=True) - cloud = openstack.openstack_cloud(cloud='kiss', region_name='region1') + cloud = openstack.connect(cloud='kiss', region_name='region1') print(cloud.has_service('network')) print(cloud.has_service('container-orchestration')) @@ -784,7 +784,7 @@ Service Conditional Overrides from openstack import cloud as openstack openstack.enable_logging(debug=True) - cloud = openstack.openstack_cloud(cloud='rax', region_name='DFW') + cloud = openstack.connect(cloud='rax', region_name='DFW') print(cloud.has_service('network')) .. code:: yaml diff --git a/examples/cloud/cleanup-servers.py b/examples/cloud/cleanup-servers.py index 89b7c16e4..2bf18cde5 100644 --- a/examples/cloud/cleanup-servers.py +++ b/examples/cloud/cleanup-servers.py @@ -20,7 +20,7 @@ ('my-citycloud', 'Buf1'), ('my-internap', 'ams01')]: # Initialize cloud - cloud = openstack.openstack_cloud( + cloud = openstack.connect( cloud=cloud_name, region_name=region_name) for server in cloud.search_servers('my-server'): cloud.delete_server(server, wait=True, delete_ips=True) diff --git a/examples/cloud/create-server-dict.py b/examples/cloud/create-server-dict.py index d8d31cc34..30d1f72dc 100644 --- a/examples/cloud/create-server-dict.py +++ b/examples/cloud/create-server-dict.py @@ -23,7 +23,7 @@ ('my-internap', 'ams01', 'Ubuntu 16.04 LTS (Xenial Xerus)', 'A1.4')]: # Initialize cloud - cloud = openstack.openstack_cloud( + cloud = openstack.connect( cloud=cloud_name, region_name=region_name) # Boot a server, wait for it to boot, and then do whatever is needed diff --git a/examples/cloud/create-server-name-or-id.py b/examples/cloud/create-server-name-or-id.py index a1fc1b95c..06b218848 100644 --- a/examples/cloud/create-server-name-or-id.py +++ b/examples/cloud/create-server-name-or-id.py @@ -23,7 +23,7 @@ ('my-internap', 'ams01', 'Ubuntu 16.04 LTS (Xenial Xerus)', 'A1.4')]: # Initialize cloud - cloud = openstack.openstack_cloud( + cloud = openstack.connect( cloud=cloud_name, region_name=region_name) cloud.delete_server('my-server', wait=True, delete_ips=True) diff --git a/examples/cloud/debug-logging.py b/examples/cloud/debug-logging.py index 4c9cad328..76fce6797 100644 --- a/examples/cloud/debug-logging.py +++ b/examples/cloud/debug-logging.py @@ -13,6 +13,6 @@ from openstack import cloud as openstack openstack.enable_logging(debug=True) -cloud = openstack.openstack_cloud( +cloud = openstack.connect( cloud='my-vexxhost', region_name='ca-ymq-1') cloud.get_image('Ubuntu 16.04.1 LTS [2017-03-03]') diff --git a/examples/cloud/find-an-image.py b/examples/cloud/find-an-image.py index f15787378..297a29a87 100644 --- a/examples/cloud/find-an-image.py +++ b/examples/cloud/find-an-image.py @@ -13,7 +13,7 @@ from openstack import cloud as openstack openstack.enable_logging() -cloud = openstack.openstack_cloud(cloud='fuga', region_name='cystack') +cloud = openstack.connect(cloud='fuga', region_name='cystack') cloud.pprint([ image for image in cloud.list_images() if 'ubuntu' in image.name.lower()]) diff --git a/examples/cloud/http-debug-logging.py b/examples/cloud/http-debug-logging.py index 5e67f4a38..b047c81f0 100644 --- a/examples/cloud/http-debug-logging.py +++ b/examples/cloud/http-debug-logging.py @@ -13,6 +13,6 @@ from openstack import cloud as openstack openstack.enable_logging(http_debug=True) -cloud = openstack.openstack_cloud( +cloud = openstack.connect( cloud='my-vexxhost', region_name='ca-ymq-1') cloud.get_image('Ubuntu 16.04.1 LTS [2017-03-03]') diff --git a/examples/cloud/munch-dict-object.py b/examples/cloud/munch-dict-object.py index 65568c112..d9e7ff1a0 100644 --- a/examples/cloud/munch-dict-object.py +++ b/examples/cloud/munch-dict-object.py @@ -13,7 +13,7 @@ from openstack import cloud as openstack openstack.enable_logging(debug=True) -cloud = openstack.openstack_cloud(cloud='ovh', region_name='SBG1') +cloud = openstack.connect(cloud='ovh', region_name='SBG1') image = cloud.get_image('Ubuntu 16.10') print(image.name) print(image['name']) diff --git a/examples/cloud/normalization.py b/examples/cloud/normalization.py index 914baef70..c7830ad8c 100644 --- a/examples/cloud/normalization.py +++ b/examples/cloud/normalization.py @@ -13,7 +13,7 @@ from openstack import cloud as openstack openstack.enable_logging() -cloud = openstack.openstack_cloud(cloud='fuga', region_name='cystack') +cloud = openstack.connect(cloud='fuga', region_name='cystack') image = cloud.get_image( 'Ubuntu 16.04 LTS - Xenial Xerus - 64-bit - Fuga Cloud Based Image') cloud.pprint(image) diff --git a/examples/cloud/server-information.py b/examples/cloud/server-information.py index b851e96ad..26896e22e 100644 --- a/examples/cloud/server-information.py +++ b/examples/cloud/server-information.py @@ -13,7 +13,7 @@ import openstack openstack.enable_logging(debug=True) -cloud = openstack.openstack_cloud(cloud='my-citycloud', region_name='Buf1') +cloud = openstack.connect(cloud='my-citycloud', region_name='Buf1') try: server = cloud.create_server( 'my-server', image='Ubuntu 16.04 Xenial Xerus', diff --git a/examples/cloud/service-conditional-overrides.py b/examples/cloud/service-conditional-overrides.py index d3a2a88de..31e7840e6 100644 --- a/examples/cloud/service-conditional-overrides.py +++ b/examples/cloud/service-conditional-overrides.py @@ -13,5 +13,5 @@ import openstack openstack.enable_logging(debug=True) -cloud = openstack.openstack_cloud(cloud='rax', region_name='DFW') +cloud = openstack.connect(cloud='rax', region_name='DFW') print(cloud.has_service('network')) diff --git a/examples/cloud/service-conditionals.py b/examples/cloud/service-conditionals.py index 46b3d2e91..d17d250b6 100644 --- a/examples/cloud/service-conditionals.py +++ b/examples/cloud/service-conditionals.py @@ -13,6 +13,6 @@ import openstack openstack.enable_logging(debug=True) -cloud = openstack.openstack_cloud(cloud='kiss', region_name='region1') +cloud = openstack.connect(cloud='kiss', region_name='region1') print(cloud.has_service('network')) print(cloud.has_service('container-orchestration')) diff --git a/examples/cloud/strict-mode.py b/examples/cloud/strict-mode.py index 96eaa13b9..14877fd78 100644 --- a/examples/cloud/strict-mode.py +++ b/examples/cloud/strict-mode.py @@ -13,7 +13,7 @@ import openstack openstack.enable_logging() -cloud = openstack.openstack_cloud( +cloud = openstack.connect( cloud='fuga', region_name='cystack', strict=True) image = cloud.get_image( 'Ubuntu 16.04 LTS - Xenial Xerus - 64-bit - Fuga Cloud Based Image') diff --git a/examples/cloud/upload-large-object.py b/examples/cloud/upload-large-object.py index d89ae557d..c88c21c11 100644 --- a/examples/cloud/upload-large-object.py +++ b/examples/cloud/upload-large-object.py @@ -13,7 +13,7 @@ import openstack openstack.enable_logging(debug=True) -cloud = openstack.openstack_cloud(cloud='ovh', region_name='SBG1') +cloud = openstack.connect(cloud='ovh', region_name='SBG1') cloud.create_object( container='my-container', name='my-object', filename='/home/mordred/briarcliff.sh3d', diff --git a/examples/cloud/upload-object.py b/examples/cloud/upload-object.py index d89ae557d..c88c21c11 100644 --- a/examples/cloud/upload-object.py +++ b/examples/cloud/upload-object.py @@ -13,7 +13,7 @@ import openstack openstack.enable_logging(debug=True) -cloud = openstack.openstack_cloud(cloud='ovh', region_name='SBG1') +cloud = openstack.connect(cloud='ovh', region_name='SBG1') cloud.create_object( container='my-container', name='my-object', filename='/home/mordred/briarcliff.sh3d', diff --git a/examples/cloud/user-agent.py b/examples/cloud/user-agent.py index 578c3c2e5..52ddd4750 100644 --- a/examples/cloud/user-agent.py +++ b/examples/cloud/user-agent.py @@ -13,6 +13,6 @@ import openstack openstack.enable_logging(http_debug=True) -cloud = openstack.openstack_cloud( +cloud = openstack.connect( cloud='datacentred', app_name='AmazingApp', app_version='1.0') cloud.list_networks() diff --git a/openstack/cloud/__init__.py b/openstack/cloud/__init__.py index c3ad1f9cf..4b9999279 100644 --- a/openstack/cloud/__init__.py +++ b/openstack/cloud/__init__.py @@ -52,17 +52,3 @@ def openstack_clouds( except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin as e: raise OpenStackCloudException( "Invalid cloud configuration: {exc}".format(exc=str(e))) - - -def openstack_cloud( - config=None, strict=False, app_name=None, app_version=None, **kwargs): - # Late import while we unwind things - from openstack import connection - if not config: - config = _get_openstack_config(app_name, app_version) - try: - cloud_region = config.get_one(**kwargs) - except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin as e: - raise OpenStackCloudException( - "Invalid cloud configuration: {exc}".format(exc=str(e))) - return connection.Connection(config=cloud_region, strict=strict) diff --git a/openstack/cloud/inventory.py b/openstack/cloud/inventory.py index 9f0120c43..f1f549ba7 100644 --- a/openstack/cloud/inventory.py +++ b/openstack/cloud/inventory.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +__all__ = ['OpenStackInventory'] + import functools from openstack.config import loader diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index badc38271..aba2a53b8 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -263,12 +263,12 @@ def connect_as(self, **kwargs): .. code-block:: python - cloud = openstack.cloud.openstack_cloud(cloud='example') + conn = openstack.connect(cloud='example') # Work normally - servers = cloud.list_servers() - cloud2 = cloud.connect_as(username='different-user', password='') + servers = conn.list_servers() + conn2 = conn.connect_as(username='different-user', password='') # Work as different-user - servers = cloud2.list_servers() + servers = conn2.list_servers() :param kwargs: keyword arguments can contain anything that would normally go in an auth dict. They will override the same @@ -342,7 +342,7 @@ def connect_as_project(self, project): .. code-block:: python - cloud = openstack.cloud.openstack_cloud(cloud='example') + cloud = openstack.connect(cloud='example') # Work normally servers = cloud.list_servers() cloud2 = cloud.connect_as_project('different-project') diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py index b1bcc38d8..975b5ba88 100644 --- a/openstack/tests/unit/cloud/test_caching.py +++ b/openstack/tests/unit/cloud/test_caching.py @@ -18,8 +18,8 @@ import openstack import openstack.cloud -from openstack.cloud import exc from openstack.cloud import meta +from openstack.config import exceptions as occ_exc from openstack.tests import fakes from openstack.tests.unit import base @@ -551,6 +551,6 @@ def setUp(self): cloud_config_fixture='clouds_cache.yaml') def test_get_auth_bogus(self): - with testtools.ExpectedException(exc.OpenStackCloudException): - openstack.cloud.openstack_cloud( + with testtools.ExpectedException(occ_exc.OpenStackConfigException): + openstack.connect( cloud='_bogus_test_', config=self.config) diff --git a/openstack/tests/unit/cloud/test_operator_noauth.py b/openstack/tests/unit/cloud/test_operator_noauth.py index 5b7dd3072..ab60dab90 100644 --- a/openstack/tests/unit/cloud/test_operator_noauth.py +++ b/openstack/tests/unit/cloud/test_operator_noauth.py @@ -50,7 +50,7 @@ def test_ironic_noauth_none_auth_type(self): # with 'v1'. As such, since we are overriding the endpoint, # we must explicitly do the same as we move away from the # client library. - self.cloud_noauth = openstack.cloud.openstack_cloud( + self.cloud_noauth = openstack.connect( auth_type='none', baremetal_endpoint_override="https://bare-metal.example.com/v1") @@ -63,7 +63,7 @@ def test_ironic_noauth_admin_token_auth_type(self): The old way of doing this was to abuse admin_token. """ - self.cloud_noauth = openstack.cloud.openstack_cloud( + self.cloud_noauth = openstack.connect( auth_type='admin_token', auth=dict( endpoint='https://bare-metal.example.com/v1', From a523ac7fc25e618e01b86fb0350008cde821e37c Mon Sep 17 00:00:00 2001 From: Tim Burke Date: Wed, 31 Jan 2018 13:01:21 -0800 Subject: [PATCH 1967/3836] Let enforcer.py work under both py2 and py3 I guess the gate uses py3, but some of the post jobs still use py2? Whatever, it's easy enough to support both. See http://logs.openstack.org/f9/f9b96861577e26f0540158e00706e2505213f4bf/post/publish-openstack-sphinx-docs/58260e1/ara/result/8a18f0c0-d4b0-4cef-9d31-4cf79f3b4999/ for an example failure. Change-Id: I37e507d37d4a41f5c55f2314bc074556f6262b50 --- doc/source/enforcer.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/source/enforcer.py b/doc/source/enforcer.py index b8dc182b7..5dc0c6d00 100644 --- a/doc/source/enforcer.py +++ b/doc/source/enforcer.py @@ -1,5 +1,4 @@ import importlib -import itertools import os from bs4 import BeautifulSoup @@ -113,7 +112,7 @@ def is_ignored(name): # TEMPORARY: Ignore the wait_for names when determining what is missing. app.info("ENFORCER: Ignoring wait_for_* names...") - missing = set(itertools.filterfalse(is_ignored, missing)) + missing = set(x for x in missing if not is_ignored(x)) missing_count = len(missing) app.info("ENFORCER: Found %d missing proxy methods " From 6f38b4ed927a5b425c107974fb51b37b6342acce Mon Sep 17 00:00:00 2001 From: Adrian Turjak Date: Thu, 1 Feb 2018 11:50:14 +1300 Subject: [PATCH 1968/3836] Add server-side names to query param checking Rework the query params checking to include server-side names, as well as move to dedicated function on QueryParameters. Change transpose logic to include server-side names as fallback, with precedence for client-side names. Add tests for these changes, and fix missing assertEqual in old query mapping test. Change-Id: I3705075a0ea2911a06619a6e4dd13233c725887e Closes-bug: #1746535 --- openstack/resource.py | 35 +++++++++--- openstack/tests/unit/test_resource.py | 79 ++++++++++++++++++++++++++- 2 files changed, 104 insertions(+), 10 deletions(-) diff --git a/openstack/resource.py b/openstack/resource.py index d2bc696ef..318901ac9 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -231,9 +231,32 @@ def __init__(self, *names, **mappings): self._mapping = {"limit": "limit", "marker": "marker"} self._mapping.update(dict({name: name for name in names}, **mappings)) + def _validate(self, query, base_path=None): + """Check that supplied query keys match known query mappings + + :param dict query: Collection of key-value pairs where each key is the + client-side parameter name or server side name. + :param base_path: Formatted python string of the base url path for + the resource. + """ + expected_params = list(self._mapping.keys()) + expected_params += self._mapping.values() + + if base_path: + expected_params += utils.get_string_format_keys(base_path) + + invalid_keys = set(query.keys()) - set(expected_params) + if invalid_keys: + raise exceptions.InvalidResourceQuery( + message="Invalid query params: %s" % ",".join(invalid_keys), + extra_data=invalid_keys) + def _transpose(self, query): """Transpose the keys in query based on the mapping + If a query is supplied with its server side name, we will still use + it, but take preference to the client-side name when both are supplied. + :param dict query: Collection of key-value pairs where each key is the client-side parameter name to be transposed to its server side name. @@ -242,6 +265,8 @@ def _transpose(self, query): for key, value in self._mapping.items(): if key in query: result[value] = query[key] + elif value in query: + result[value] = query[value] return result @@ -855,15 +880,7 @@ def list(cls, session, paginated=False, **params): raise exceptions.MethodNotSupported(cls, "list") session = cls._get_session(session) - expected_params = utils.get_string_format_keys(cls.base_path) - expected_params += cls._query_mapping._mapping.keys() - - invalid_keys = set(params.keys()) - set(expected_params) - if invalid_keys: - raise exceptions.InvalidResourceQuery( - message="Invalid query params: %s" % ",".join(invalid_keys), - extra_data=invalid_keys) - + cls._query_mapping._validate(params, base_path=cls.base_path) query_params = cls._query_mapping._transpose(params) uri = cls.base_path % params diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 80d98a8a1..4739ee01a 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1285,7 +1285,9 @@ class Test(self.test_class): # Look at the `params` argument to each of the get calls that # were made. - self.session.get.call_args_list[0][1]["params"] = {qp_name: qp} + self.assertEqual( + self.session.get.call_args_list[0][1]["params"], + {qp_name: qp}) self.assertEqual(self.session.get.call_args_list[0][0][0], Test.base_path % {"something": uri_param}) @@ -1314,6 +1316,81 @@ class Test(self.test_class): except exceptions.InvalidResourceQuery as err: self.assertEqual(str(err), 'Invalid query params: something_wrong') + def test_values_as_list_params(self): + id = 1 + qp = "query param!" + qp_name = "query-param" + uri_param = "uri param!" + + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.links = {} + mock_response.json.return_value = {"resources": [{"id": id}]} + + mock_empty = mock.Mock() + mock_empty.status_code = 200 + mock_empty.links = {} + mock_empty.json.return_value = {"resources": []} + + self.session.get.side_effect = [mock_response, mock_empty] + + class Test(self.test_class): + _query_mapping = resource.QueryParameters(query_param=qp_name) + base_path = "/%(something)s/blah" + something = resource.URI("something") + + results = list(Test.list(self.session, paginated=True, + something=uri_param, **{qp_name: qp})) + + self.assertEqual(1, len(results)) + + # Look at the `params` argument to each of the get calls that + # were made. + self.assertEqual( + self.session.get.call_args_list[0][1]["params"], + {qp_name: qp}) + + self.assertEqual(self.session.get.call_args_list[0][0][0], + Test.base_path % {"something": uri_param}) + + def test_values_as_list_params_precedence(self): + id = 1 + qp = "query param!" + qp2 = "query param!!!!!" + qp_name = "query-param" + uri_param = "uri param!" + + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.links = {} + mock_response.json.return_value = {"resources": [{"id": id}]} + + mock_empty = mock.Mock() + mock_empty.status_code = 200 + mock_empty.links = {} + mock_empty.json.return_value = {"resources": []} + + self.session.get.side_effect = [mock_response, mock_empty] + + class Test(self.test_class): + _query_mapping = resource.QueryParameters(query_param=qp_name) + base_path = "/%(something)s/blah" + something = resource.URI("something") + + results = list(Test.list(self.session, paginated=True, query_param=qp2, + something=uri_param, **{qp_name: qp})) + + self.assertEqual(1, len(results)) + + # Look at the `params` argument to each of the get calls that + # were made. + self.assertEqual( + self.session.get.call_args_list[0][1]["params"], + {qp_name: qp2}) + + self.assertEqual(self.session.get.call_args_list[0][0][0], + Test.base_path % {"something": uri_param}) + def test_list_multi_page_response_paginated(self): ids = [1, 2] resp1 = mock.Mock() From 37edda3d2df0246a066d12134e8a937eea093359 Mon Sep 17 00:00:00 2001 From: Hunt Xu Date: Wed, 31 Jan 2018 18:55:49 +0800 Subject: [PATCH 1969/3836] Fix TypeError for overrided get methods Get methods fixed in this commit would be called from openstack.proxy._get where an error_message parameter will be passed. TypeError will be raised if the keyword argument 'error_message' is missing. Change-Id: I2fdfb5c1cdf9e2e58669b44d6e8b5f2d118b3d63 Closes-Bug: #1743891 --- openstack/key_manager/v1/secret.py | 2 +- openstack/message/v2/claim.py | 2 +- openstack/message/v2/message.py | 2 +- openstack/message/v2/queue.py | 2 +- openstack/message/v2/subscription.py | 2 +- openstack/orchestration/v1/stack.py | 5 +++-- openstack/tests/unit/key_manager/v1/test_proxy.py | 3 +++ openstack/tests/unit/message/v2/test_proxy.py | 12 ++++++++++++ openstack/tests/unit/orchestration/v1/test_proxy.py | 3 +++ openstack/tests/unit/test_proxy_base.py | 7 +++++++ 10 files changed, 33 insertions(+), 7 deletions(-) diff --git a/openstack/key_manager/v1/secret.py b/openstack/key_manager/v1/secret.py index fd04e49e0..60d95f4d1 100644 --- a/openstack/key_manager/v1/secret.py +++ b/openstack/key_manager/v1/secret.py @@ -79,7 +79,7 @@ class Secret(resource.Resource): #: (required if payload is encoded) payload_content_encoding = resource.Body('payload_content_encoding') - def get(self, session, requires_id=True): + def get(self, session, requires_id=True, error_message=None): request = self._prepare_request(requires_id=requires_id) response = session.get(request.url).json() diff --git a/openstack/message/v2/claim.py b/openstack/message/v2/claim.py index 9ee98f923..c032eaae6 100644 --- a/openstack/message/v2/claim.py +++ b/openstack/message/v2/claim.py @@ -82,7 +82,7 @@ def create(self, session, prepend_key=False): return self - def get(self, session, requires_id=True): + def get(self, session, requires_id=True, error_message=None): request = self._prepare_request(requires_id=requires_id) headers = { "Client-ID": self.client_id or str(uuid.uuid4()), diff --git a/openstack/message/v2/message.py b/openstack/message/v2/message.py index 2b8cccf65..b496d7786 100644 --- a/openstack/message/v2/message.py +++ b/openstack/message/v2/message.py @@ -109,7 +109,7 @@ def list(cls, session, paginated=True, **params): query_params["limit"] = yielded query_params["marker"] = new_marker - def get(self, session, requires_id=True): + def get(self, session, requires_id=True, error_message=None): request = self._prepare_request(requires_id=requires_id) headers = { "Client-ID": self.client_id or str(uuid.uuid4()), diff --git a/openstack/message/v2/queue.py b/openstack/message/v2/queue.py index d69b0034c..4f7087cad 100644 --- a/openstack/message/v2/queue.py +++ b/openstack/message/v2/queue.py @@ -107,7 +107,7 @@ def list(cls, session, paginated=False, **params): query_params["limit"] = yielded query_params["marker"] = new_marker - def get(self, session, requires_id=True): + def get(self, session, requires_id=True, error_message=None): request = self._prepare_request(requires_id=requires_id) headers = { "Client-ID": self.client_id or str(uuid.uuid4()), diff --git a/openstack/message/v2/subscription.py b/openstack/message/v2/subscription.py index f9de79b57..6dcf9d540 100644 --- a/openstack/message/v2/subscription.py +++ b/openstack/message/v2/subscription.py @@ -115,7 +115,7 @@ def list(cls, session, paginated=True, **params): query_params["limit"] = yielded query_params["marker"] = new_marker - def get(self, session, requires_id=True): + def get(self, session, requires_id=True, error_message=None): request = self._prepare_request(requires_id=requires_id) headers = { "Client-ID": self.client_id or str(uuid.uuid4()), diff --git a/openstack/orchestration/v1/stack.py b/openstack/orchestration/v1/stack.py index b42301eef..8dad7b2c6 100644 --- a/openstack/orchestration/v1/stack.py +++ b/openstack/orchestration/v1/stack.py @@ -96,8 +96,9 @@ def _action(self, session, body): def check(self, session): return self._action(session, {'check': ''}) - def get(self, session, requires_id=True): - stk = super(Stack, self).get(session, requires_id=requires_id) + def get(self, session, requires_id=True, error_message=None): + stk = super(Stack, self).get(session, requires_id=requires_id, + error_message=error_message) if stk and stk.status in ['DELETE_COMPLETE', 'ADOPT_COMPLETE']: raise exceptions.NotFoundException( "No stack found for %s" % stk.id) diff --git a/openstack/tests/unit/key_manager/v1/test_proxy.py b/openstack/tests/unit/key_manager/v1/test_proxy.py index ed3f9250a..400e7150a 100644 --- a/openstack/tests/unit/key_manager/v1/test_proxy.py +++ b/openstack/tests/unit/key_manager/v1/test_proxy.py @@ -81,6 +81,9 @@ def test_secret_find(self): def test_secret_get(self): self.verify_get(self.proxy.get_secret, secret.Secret) + self.verify_get_overrided( + self.proxy, secret.Secret, + 'openstack.key_manager.v1.secret.Secret') def test_secrets(self): self.verify_list(self.proxy.secrets, secret.Secret, paginated=False) diff --git a/openstack/tests/unit/message/v2/test_proxy.py b/openstack/tests/unit/message/v2/test_proxy.py index d41165597..b52c24bea 100644 --- a/openstack/tests/unit/message/v2/test_proxy.py +++ b/openstack/tests/unit/message/v2/test_proxy.py @@ -33,6 +33,9 @@ def test_queue_create(self): def test_queue_get(self): self.verify_get(self.proxy.get_queue, queue.Queue) + self.verify_get_overrided( + self.proxy, queue.Queue, + 'openstack.message.v2.queue.Queue') def test_queues(self): self.verify_list(self.proxy.queues, queue.Queue, paginated=True) @@ -64,6 +67,9 @@ def test_message_get(self, mock_get_resource): mock_get_resource.assert_called_once_with(message.Message, "resource_or_id", queue_name="test_queue") + self.verify_get_overrided( + self.proxy, message.Message, + 'openstack.message.v2.message.Message') def test_messages(self): self.verify_list(self.proxy.messages, message.Message, @@ -137,6 +143,9 @@ def test_subscription_get(self, mock_get_resource): mock_get_resource.assert_called_once_with( subscription.Subscription, "resource_or_id", queue_name="test_queue") + self.verify_get_overrided( + self.proxy, subscription.Subscription, + 'openstack.message.v2.subscription.Subscription') def test_subscriptions(self): self.verify_list(self.proxy.subscriptions, subscription.Subscription, @@ -175,6 +184,9 @@ def test_claim_get(self): expected_args=[claim.Claim, "resource_or_id"], expected_kwargs={"queue_name": "test_queue"}) + self.verify_get_overrided( + self.proxy, claim.Claim, + 'openstack.message.v2.claim.Claim') def test_claim_update(self): self._verify2("openstack.proxy.BaseProxy._update", diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index 0193781af..54c5146ac 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -47,6 +47,9 @@ def test_stacks(self): def test_get_stack(self): self.verify_get(self.proxy.get_stack, stack.Stack) + self.verify_get_overrided( + self.proxy, stack.Stack, + 'openstack.orchestration.v1.stack.Stack') def test_update_stack(self): self.verify_update(self.proxy.update_stack, stack.Stack) diff --git a/openstack/tests/unit/test_proxy_base.py b/openstack/tests/unit/test_proxy_base.py index 296d7670e..f547e7869 100644 --- a/openstack/tests/unit/test_proxy_base.py +++ b/openstack/tests/unit/test_proxy_base.py @@ -141,6 +141,13 @@ def verify_get(self, test_method, resource_type, value=None, args=None, expected_args=expected_args, expected_kwargs=expected_kwargs) + def verify_get_overrided(self, proxy, resource_type, patch_target): + with mock.patch(patch_target, autospec=True) as res: + proxy._get_resource = mock.Mock(return_value=res) + proxy._get(resource_type) + res.get.assert_called_once_with(proxy, requires_id=True, + error_message=mock.ANY) + def verify_head(self, test_method, resource_type, mock_method="openstack.proxy.BaseProxy._head", value=None, **kwargs): From 767147c48962cea8c187fd3381c405541e830026 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 31 Jan 2018 14:28:37 -0600 Subject: [PATCH 1970/3836] Add get_client_config method to CloudRegion Client utilities sometimes need to be able to set some arbitrary settings in their clouds.yaml file. Add a get_client_config that takes an optional dictionary of defaults and looks for a config section named for the client, falling back to looking for a section named 'client'. (Thanks for the inspiration MySQL) Change-Id: I9016746584d043f5b62170da6a52d4152885fe5b --- openstack/config/cloud_region.py | 22 ++++++++++++++++ openstack/config/loader.py | 7 ++--- openstack/tests/unit/config/test_config.py | 30 ++++++++++++++++++++++ 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 2c77389d8..9375d585a 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -441,3 +441,25 @@ def get_nat_source(self): if net.get('nat_source'): return net['name'] return None + + def get_client_config(self, name=None, defaults=None): + """Get config settings for a named client. + + Settings will also be looked for in a section called 'client'. + If settings are found in both, they will be merged with the settings + from the named section winning over the settings from client section, + and both winning over provided defaults. + + :param string name: + Name of the config section to look for. + :param dict defaults: + Default settings to use. + + :returns: + A dict containing merged settings from the named section, the + client section and the defaults. + """ + if not self._openstack_config: + return defaults or {} + return self._openstack_config.get_extra_config( + name, self._openstack_config.get_extra_config('client', defaults)) diff --git a/openstack/config/loader.py b/openstack/config/loader.py index a0ad64fd3..5eb4df63a 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -327,10 +327,11 @@ def get_extra_config(self, key, defaults=None): :param dict defaults: (optional) default values to merge under the found config """ - if not defaults: - defaults = {} + defaults = self._normalize_keys(defaults or {}) + if not key: + return defaults return _merge_clouds( - self._normalize_keys(defaults), + defaults, self._normalize_keys(self.cloud_config.get(key, {}))) def _load_config_file(self): diff --git a/openstack/tests/unit/config/test_config.py b/openstack/tests/unit/config/test_config.py index 992b5d4f9..e14c1e113 100644 --- a/openstack/tests/unit/config/test_config.py +++ b/openstack/tests/unit/config/test_config.py @@ -743,6 +743,36 @@ def test_extra_config(self): }, ansible_options) + def test_get_client_config(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + + cc = c.get_one( + cloud='_test_cloud_regions') + + defaults = { + 'use_hostnames': False, + 'other-value': 'something', + 'force_ipv4': False, + } + ansible_options = cc.get_client_config('ansible', defaults) + + # This should show that the default for use_hostnames and force_ipv4 + # above is overridden by the value in the config file defined in + # base.py + # It should also show that other-value key is normalized and passed + # through even though there is no corresponding value in the config + # file, and that expand-hostvars key is normalized and the value + # from the config comes through even though there is no default. + self.assertDictEqual( + { + 'expand_hostvars': False, + 'use_hostnames': True, + 'other_value': 'something', + 'force_ipv4': True, + }, + ansible_options) + def test_register_argparse_cloud(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) From 188c9ea525173e5c6a334976d755ecd6794cda7c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 1 Feb 2018 06:48:44 -0600 Subject: [PATCH 1971/3836] Use keystone NoAuth plugin for auth_type none We've been hacking this with the admin_token plugin, but we got a NoAuth plugin added to keystoneauth. Use it instead. Change-Id: Id1a861d54e3ba4d815e93d29ab9ca9f49debfd03 --- openstack/config/loader.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 5eb4df63a..81ff37c5e 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -882,10 +882,7 @@ def _get_auth_loader(self, config): # still dealing with a keystoneauth Session object, so all the # _other_ things (SSL arg handling, timeout) all work consistently if config['auth_type'] in (None, "None", ''): - config['auth_type'] = 'admin_token' - # Set to notused rather than None because validate_auth will - # strip the value if it's actually python None - config['auth']['token'] = 'notused' + config['auth_type'] = 'none' elif config['auth_type'] == 'token_endpoint': # Humans have been trained to use a thing called token_endpoint # That it does not exist in keystoneauth is irrelvant- it not From f7bd3d8975eeacdc3cd041d64bc1574454c29b5c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 2 Feb 2018 17:23:39 -0600 Subject: [PATCH 1972/3836] Fix issue with missing url parameters OSC uses these. project_id and tenant_id for neutron are interchangable. If we list them in the QueryParameters list as project_id='tenant_id' then a user can specify either. We'd done this for all of the *other* Resources, but for some reason Network was left out. ip_version is a valid query parameter for network_ip_availability, and we even have it documented, but it's not in the list either. We should have caught this before the last release - but actually only just now got osc-functional-tips jobs turned back on. Sigh. Change-Id: I1037dac37b15edbd2b8a17d7ed4ba639cf8e960b --- openstack/network/v2/network.py | 3 ++- openstack/network/v2/network_ip_availability.py | 2 +- openstack/tests/unit/network/v2/test_network.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/openstack/network/v2/network.py b/openstack/network/v2/network.py index 0c1ca1d86..e2141613d 100644 --- a/openstack/network/v2/network.py +++ b/openstack/network/v2/network.py @@ -30,13 +30,14 @@ class Network(resource.Resource, tag.TagMixin): # NOTE: We don't support query on list or datetime fields yet _query_mapping = resource.QueryParameters( - 'description', 'name', 'project_id', 'status', + 'description', 'name', 'status', ipv4_address_scope_id='ipv4_address_scope', ipv6_address_scope_id='ipv6_address_scope', is_admin_state_up='admin_state_up', is_port_security_enabled='port_security_enabled', is_router_external='router:external', is_shared='shared', + project_id='tenant_id', provider_network_type='provider:network_type', provider_physical_network='provider:physical_network', provider_segmentation_id='provider:segmentation_id', diff --git a/openstack/network/v2/network_ip_availability.py b/openstack/network/v2/network_ip_availability.py index e5ca95fda..f9e136211 100644 --- a/openstack/network/v2/network_ip_availability.py +++ b/openstack/network/v2/network_ip_availability.py @@ -29,7 +29,7 @@ class NetworkIPAvailability(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'network_id', 'network_name', + 'ip_version', 'network_id', 'network_name', project_id='tenant_id' ) diff --git a/openstack/tests/unit/network/v2/test_network.py b/openstack/tests/unit/network/v2/test_network.py index f8fc88b6b..3b0c64630 100644 --- a/openstack/tests/unit/network/v2/test_network.py +++ b/openstack/tests/unit/network/v2/test_network.py @@ -100,7 +100,7 @@ def test_make_it(self): 'marker': 'marker', 'description': 'description', 'name': 'name', - 'project_id': 'project_id', + 'project_id': 'tenant_id', 'status': 'status', 'ipv4_address_scope_id': 'ipv4_address_scope', 'ipv6_address_scope_id': 'ipv6_address_scope', From 8b7c5d789c0a48cafd75bc34564802ab0b7d4f92 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 2 Feb 2018 17:30:03 -0600 Subject: [PATCH 1973/3836] Gate on osc-functional-tips OSC gates on this too. We should as well. Well, we should now that it is actually testing tips. Depends-On: https://review.openstack.org/540554/ Change-Id: Ib19e5a3a1dde673b0b6ce02ec96befd667a460a5 --- .zuul.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index c13a97774..97300e26b 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -217,8 +217,7 @@ - openstacksdk-functional-devstack-magnum: voting: false - openstacksdk-functional-devstack-python3 - - osc-functional-devstack-tips: - voting: false + - osc-functional-devstack-tips - neutron-grenade gate: jobs: @@ -227,4 +226,5 @@ sphinx_python: python3 - openstacksdk-functional-devstack - openstacksdk-functional-devstack-python3 + - osc-functional-devstack-tips - neutron-grenade From eacd6ba59c5c0c65461c098457636b325e0db89c Mon Sep 17 00:00:00 2001 From: pangliye Date: Tue, 6 Feb 2018 10:29:06 +0800 Subject: [PATCH 1974/3836] fix misspelling of 'volume' Change-Id: Id152268d1c176081ad910d3c8f1037fb805645ba --- openstack/cloud/_normalize.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/cloud/_normalize.py b/openstack/cloud/_normalize.py index ceb08bcb8..a7fa4f498 100644 --- a/openstack/cloud/_normalize.py +++ b/openstack/cloud/_normalize.py @@ -836,7 +836,7 @@ def _normalize_volume_backups(self, backups): return ret def _normalize_volume_backup(self, backup): - """ Normalize a valume backup object""" + """ Normalize a volume backup object""" backup = backup.copy() # Discard noise From 803c92c45c69fdb1bf1e7d4ef3413cb59dc1e74a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 7 Feb 2018 11:09:21 -0600 Subject: [PATCH 1975/3836] Add shade jobs to openstacksdk zuul config During rocky we'll be working on making shade a thin layer around sdk. In order to make that work well, we need to make sure that sdk changes don't break shade. shade also just added openstacksdk to the requirements list for depends, so the jobs should currently make sure requirements changes in sdk don't transitively break shade. Depends-On: https://review.openstack.org/541796 Change-Id: Ib4e7683c5ba312856a271a0473f4274569355f92 --- .zuul.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.zuul.yaml b/.zuul.yaml index 97300e26b..b870e903e 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -208,6 +208,8 @@ - openstacksdk-functional-tips - openstacksdk-tox-tips - osc-tox-unit-tips + - shade-functional-tips + - shade-tox-tips check: jobs: - build-openstack-sphinx-docs: From 8cf99a29f48dc9087c7bf6dfe0f38e24862221ce Mon Sep 17 00:00:00 2001 From: brandonzhao Date: Fri, 9 Feb 2018 14:45:53 +0800 Subject: [PATCH 1976/3836] modify typos of word password Change-Id: I92148c6222fd9dc92d5049149865540dadd0a32d --- openstack/identity/v3/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/identity/v3/user.py b/openstack/identity/v3/user.py index 453541763..bed77fb5f 100644 --- a/openstack/identity/v3/user.py +++ b/openstack/identity/v3/user.py @@ -68,7 +68,7 @@ class User(resource.Resource): #: The default form of credential used during authentication. #: *Type: string* password = resource.Body('password') - #: The date and time when the pasword expires. The time zone is UTC. + #: The date and time when the password expires. The time zone is UTC. #: A None value means the password never expires. #: This is a response object attribute, not valid for requests. #: *New in version 3.7* From 7cdb723c612cf6ed89c3d53c0b98430e2bd88f96 Mon Sep 17 00:00:00 2001 From: tengqm Date: Wed, 14 Feb 2018 08:49:50 -0500 Subject: [PATCH 1977/3836] Fix functional test about port Change-Id: Ifaef01ec5f348b7bd7466ec6c662a204d78af2d3 --- openstack/tests/functional/cloud/test_port.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openstack/tests/functional/cloud/test_port.py b/openstack/tests/functional/cloud/test_port.py index 20e2623a3..2e373059c 100644 --- a/openstack/tests/functional/cloud/test_port.py +++ b/openstack/tests/functional/cloud/test_port.py @@ -128,6 +128,15 @@ def test_update_port(self): updated_port = self.operator_cloud.get_port(name_or_id=port['id']) self.assertEqual(port.get('name'), new_port_name) + port.pop('revision_number', None) + port.pop(u'revision_number', None) + port.pop('updated_at', None) + port.pop(u'updated_at', None) + updated_port.pop('revision_number', None) + updated_port.pop(u'revision_number', None) + updated_port.pop('updated_at', None) + updated_port.pop(u'updated_at', None) + self.assertEqual(port, updated_port) def test_delete_port(self): From 2de4187366cb0fb328aaefce33d2d7c8db45adab Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 15 Feb 2018 08:53:47 -0600 Subject: [PATCH 1978/3836] Collect tox and testr output in functional tests We need to collect tox and testr html output so that it's easier to see what went wrong when things break. Change-Id: I728ca7f7ea7f8e1aca9e80eab28fdf44de91f88c --- .zuul.yaml | 1 + playbooks/devstack/post.yaml | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 playbooks/devstack/post.yaml diff --git a/.zuul.yaml b/.zuul.yaml index b870e903e..5375ee356 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -44,6 +44,7 @@ parent: devstack-tox-functional-consumer description: | Base job for devstack-based functional tests + post-run: playbooks/devstack/post.yaml required-projects: # These jobs will DTRT when openstacksdk triggers them, but we want to # make sure stable branches of openstacksdk never get cloned by other diff --git a/playbooks/devstack/post.yaml b/playbooks/devstack/post.yaml new file mode 100644 index 000000000..7f0cb1982 --- /dev/null +++ b/playbooks/devstack/post.yaml @@ -0,0 +1,4 @@ +- hosts: all + roles: + - fetch-tox-output + - fetch-subunit-output From 80716c144203a37edc7ddd324e0bc6be6cfda1e5 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 30 Jan 2018 19:40:24 -0600 Subject: [PATCH 1979/3836] Shift config exceptions to openstack.exceptions The main config exception can move. The other one isn't used. Change-Id: I6904340ce2e7d5f3224be87ae7e26f488be1802f --- openstack/config/cloud_region.py | 4 +-- openstack/config/exceptions.py | 25 ------------------- openstack/config/loader.py | 23 ++++++++--------- openstack/exceptions.py | 4 +++ openstack/tests/unit/cloud/test_caching.py | 4 +-- .../tests/unit/config/test_cloud_config.py | 4 +-- openstack/tests/unit/config/test_config.py | 22 ++++++++-------- openstack/tests/unit/config/test_environ.py | 8 +++--- 8 files changed, 36 insertions(+), 58 deletions(-) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 422bee3bf..0b19d23b8 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -24,7 +24,7 @@ from openstack import version as openstack_version from openstack import _log from openstack.config import defaults as config_defaults -from openstack.config import exceptions +from openstack import exceptions def _make_key(key, service_type): @@ -208,7 +208,7 @@ def get_session(self): """Return a keystoneauth session based on the auth credentials.""" if self._keystone_session is None: if not self._auth: - raise exceptions.OpenStackConfigException( + raise exceptions.ConfigException( "Problem with auth parameters") (verify, cert) = self.get_requests_verify_args() # Turn off urllib3 warnings about insecure certs if we have diff --git a/openstack/config/exceptions.py b/openstack/config/exceptions.py index 556dd49bc..e69de29bb 100644 --- a/openstack/config/exceptions.py +++ b/openstack/config/exceptions.py @@ -1,25 +0,0 @@ -# Copyright (c) 2014 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. - - -class OpenStackConfigException(Exception): - """Something went wrong with parsing your OpenStack Config.""" - - -class OpenStackConfigVersionException(OpenStackConfigException): - """A version was requested that is different than what was found.""" - - def __init__(self, version): - super(OpenStackConfigVersionException, self).__init__() - self.version = version diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 04dc06b37..475935ed2 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -31,8 +31,8 @@ from openstack import _log from openstack.config import cloud_region from openstack.config import defaults -from openstack.config import exceptions from openstack.config import vendors +from openstack import exceptions APPDIRS = appdirs.AppDirs('openstack', 'OpenStack', multipath='/etc') CONFIG_HOME = APPDIRS.user_config_dir @@ -168,7 +168,7 @@ def _fix_argv(argv): if len(old) > 1: overlap.extend(old) if overlap: - raise exceptions.OpenStackConfigException( + raise exceptions.ConfigException( "The following options were given: '{options}' which contain" " duplicates except that one has _ and one has -. There is" " no sane way for us to know what you're doing. Remove the" @@ -251,7 +251,7 @@ def __init__(self, config_files=None, vendor_files=None, # Next, process environment variables and add them to the mix self.envvar_key = self._get_envvar('OS_CLOUD_NAME', 'envvars') if self.envvar_key in self.cloud_config['clouds']: - raise exceptions.OpenStackConfigException( + raise exceptions.ConfigException( '"{0}" defines a cloud named "{1}", but' ' OS_CLOUD_NAME is also set to "{1}". Please rename' ' either your environment based cloud, or one of your' @@ -459,7 +459,7 @@ def _get_region(self, cloud=None, region_name=''): if region['name'] == region_name: return region - raise exceptions.OpenStackConfigException( + raise exceptions.ConfigException( 'Region {region_name} is not a valid region name for cloud' ' {cloud}. Valid choices are {region_list}. Please note that' ' region names are case sensitive.'.format( @@ -475,7 +475,7 @@ def _get_base_cloud_config(self, name): # Only validate cloud name if one was given if name and name not in self.cloud_config['clouds']: - raise exceptions.OpenStackConfigException( + raise exceptions.ConfigException( "Cloud {name} was not found.".format( name=name)) @@ -517,7 +517,7 @@ def _expand_vendor_profile(self, name, cloud, our_cloud): "{profile_name} is deprecated: {message}".format( profile_name=profile_name, message=message)) elif status == 'shutdown': - raise exceptions.OpenStackConfigException( + raise exceptions.ConfigException( "{profile_name} references a cloud that no longer" " exists: {message}".format( profile_name=profile_name, message=message)) @@ -537,7 +537,7 @@ def _validate_networks(self, networks, key): value = None for net in networks: if value and net[key]: - raise exceptions.OpenStackConfigException( + raise exceptions.ConfigException( "Duplicate network entries for {key}: {net1} and {net2}." " Only one network can be flagged with {key}".format( key=key, @@ -554,7 +554,7 @@ def _fix_backwards_networks(self, cloud): for net in cloud.get('networks', []): name = net.get('name') if not name: - raise exceptions.OpenStackConfigException( + raise exceptions.ConfigException( 'Entry in network list is missing required field "name".') network = dict( name=name, @@ -576,7 +576,7 @@ def _fix_backwards_networks(self, cloud): for key in ('external_network', 'internal_network'): external = key.startswith('external') if key in cloud and 'networks' in cloud: - raise exceptions.OpenStackConfigException( + raise exceptions.ConfigException( "Both {key} and networks were specified in the config." " Please remove {key} from the config and use the network" " list to configure network behavior.".format(key=key)) @@ -691,8 +691,7 @@ def register_argparse_arguments(self, parser, argv, service_keys=None): as the default value for service_type (optional) - :raises exceptions.OpenStackConfigException if an invalid auth-type - is requested + :raises exceptions.ConfigException if an invalid auth-type is requested """ if service_keys is None: @@ -750,7 +749,7 @@ def register_argparse_arguments(self, parser, argv, service_keys=None): # from it doesn't actually make sense to os-client-config users options, _args = parser.parse_known_args(argv) plugin_names = loading.get_available_plugin_names() - raise exceptions.OpenStackConfigException( + raise exceptions.ConfigException( "An invalid auth-type was specified: {auth_type}." " Valid choices are: {plugin_names}.".format( auth_type=options.os_auth_type, diff --git a/openstack/exceptions.py b/openstack/exceptions.py index b3f99ea0e..36718813e 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -209,3 +209,7 @@ def raise_from_response(response, error_message=None): class ArgumentDeprecationWarning(Warning): """A deprecated argument has been provided.""" pass + + +class ConfigException(SDKException): + """Something went wrong with parsing your OpenStack Config.""" diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py index 975b5ba88..791fdcbc7 100644 --- a/openstack/tests/unit/cloud/test_caching.py +++ b/openstack/tests/unit/cloud/test_caching.py @@ -19,7 +19,7 @@ import openstack import openstack.cloud from openstack.cloud import meta -from openstack.config import exceptions as occ_exc +from openstack import exceptions from openstack.tests import fakes from openstack.tests.unit import base @@ -551,6 +551,6 @@ def setUp(self): cloud_config_fixture='clouds_cache.yaml') def test_get_auth_bogus(self): - with testtools.ExpectedException(occ_exc.OpenStackConfigException): + with testtools.ExpectedException(exceptions.ConfigException): openstack.connect( cloud='_bogus_test_', config=self.config) diff --git a/openstack/tests/unit/config/test_cloud_config.py b/openstack/tests/unit/config/test_cloud_config.py index 191833e0a..722225f7d 100644 --- a/openstack/tests/unit/config/test_cloud_config.py +++ b/openstack/tests/unit/config/test_cloud_config.py @@ -19,7 +19,7 @@ from openstack import version as openstack_version from openstack.config import cloud_region from openstack.config import defaults -from openstack.config import exceptions +from openstack import exceptions from openstack.tests.unit.config import base @@ -177,7 +177,7 @@ def test_get_session_no_auth(self): config_dict.update(fake_services_dict) cc = cloud_region.CloudRegion("test1", "region-al", config_dict) self.assertRaises( - exceptions.OpenStackConfigException, + exceptions.ConfigException, cc.get_session) @mock.patch.object(ksa_session, 'Session') diff --git a/openstack/tests/unit/config/test_config.py b/openstack/tests/unit/config/test_config.py index 992b5d4f9..5a55e85cc 100644 --- a/openstack/tests/unit/config/test_config.py +++ b/openstack/tests/unit/config/test_config.py @@ -24,8 +24,8 @@ from openstack import config from openstack.config import cloud_region from openstack.config import defaults -from openstack.config import exceptions from openstack.config import loader +from openstack import exceptions from openstack.tests.unit.config import base @@ -194,7 +194,7 @@ def test_no_environ(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) self.assertRaises( - exceptions.OpenStackConfigException, c.get_one, 'envvars') + exceptions.ConfigException, c.get_one, 'envvars') def test_fallthrough(self): c = config.OpenStackConfig(config_files=[self.no_yaml], @@ -388,7 +388,7 @@ def test_get_region_invalid_region(self): vendor_files=[self.vendor_yaml], secure_files=[self.no_yaml]) self.assertRaises( - exceptions.OpenStackConfigException, c._get_region, + exceptions.ConfigException, c._get_region, cloud='_test_cloud_regions', region_name='invalid-region') def test_get_region_no_cloud(self): @@ -481,7 +481,7 @@ def test_get_one_bad_region_argparse(self): vendor_files=[self.vendor_yaml]) self.assertRaises( - exceptions.OpenStackConfigException, c.get_one, + exceptions.ConfigException, c.get_one, cloud='_test-cloud_', argparse=self.options) def test_get_one_argparse(self): @@ -640,7 +640,7 @@ def test_get_one_bad_region(self): vendor_files=[self.vendor_yaml]) self.assertRaises( - exceptions.OpenStackConfigException, + exceptions.ConfigException, c.get_one, cloud='_test_cloud_regions', region_name='bad') @@ -648,7 +648,7 @@ def test_get_one_bad_region_no_regions(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) self.assertRaises( - exceptions.OpenStackConfigException, + exceptions.ConfigException, c.get_one, cloud='_test-cloud_', region_name='bad_region') @@ -686,7 +686,7 @@ def test_get_one_no_yaml_no_cloud(self): c = config.OpenStackConfig(load_yaml_config=False) self.assertRaises( - exceptions.OpenStackConfigException, + exceptions.ConfigException, c.get_one, cloud='_test_cloud_regions', region_name='region2', argparse=None) @@ -828,7 +828,7 @@ def test_argparse_underscores_duplicate(self): '--os-username', 'user1', '--os-password', 'pass1', '--os-auth-url', 'auth-url', '--os-project-name', 'project'] self.assertRaises( - exceptions.OpenStackConfigException, + exceptions.ConfigException, c.register_argparse_arguments, parser=parser, argv=argv) @@ -837,7 +837,7 @@ def test_register_argparse_bad_plugin(self): vendor_files=[self.vendor_yaml]) parser = argparse.ArgumentParser() self.assertRaises( - exceptions.OpenStackConfigException, + exceptions.ConfigException, c.register_argparse_arguments, parser, ['--os-auth-type', 'foo']) @@ -1068,7 +1068,7 @@ def test_backwards_network_fail(self): ] } self.assertRaises( - exceptions.OpenStackConfigException, + exceptions.ConfigException, c._fix_backwards_networks, cloud) def test_backwards_network(self): @@ -1121,5 +1121,5 @@ def test_single_default_interface(self): ] } self.assertRaises( - exceptions.OpenStackConfigException, + exceptions.ConfigException, c._fix_backwards_networks, cloud) diff --git a/openstack/tests/unit/config/test_environ.py b/openstack/tests/unit/config/test_environ.py index fa0e34cee..95aa3f643 100644 --- a/openstack/tests/unit/config/test_environ.py +++ b/openstack/tests/unit/config/test_environ.py @@ -15,7 +15,7 @@ from openstack import config from openstack.config import cloud_region -from openstack.config import exceptions +from openstack import exceptions from openstack.tests.unit.config import base import fixtures @@ -45,7 +45,7 @@ def test_no_fallthrough(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) self.assertRaises( - exceptions.OpenStackConfigException, c.get_one, 'openstack') + exceptions.ConfigException, c.get_one, 'openstack') def test_envvar_name_override(self): self.useFixture( @@ -127,7 +127,7 @@ def test_no_envvars(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) self.assertRaises( - exceptions.OpenStackConfigException, c.get_one, 'envvars') + exceptions.ConfigException, c.get_one, 'envvars') def test_test_envvars(self): self.useFixture( @@ -137,7 +137,7 @@ def test_test_envvars(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) self.assertRaises( - exceptions.OpenStackConfigException, c.get_one, 'envvars') + exceptions.ConfigException, c.get_one, 'envvars') def test_incomplete_envvars(self): self.useFixture( From a99d6f19123ea540891f4ef00bf2c6b39d9963f6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 30 Jan 2018 11:30:38 -0600 Subject: [PATCH 1980/3836] Rename BaseProxy to Proxy The base resource class is "Resource" but the base proxy class is "BaseProxy" which makes reading the docs a bit strange. It was originally not really a big real because nothing used Proxy directly. But since we add Proxy instances to the Connection for every service in service-types-authority which are now documented as part of the Connection object, calling them BaseProxy is a bit weird. Keep a BaseProxy object around so we don't break out of tree Proxy objects, but add a deprecation to it so people can update. Shift BaseProxy from the main user docs to the contributor layout docs. Add showing inheritance, as well as docs for OpenStackSDKAdapter. Change-Id: I2778b2a04ad0aed4bb6b217151f41503997db1c6 --- doc/source/contributor/layout.rst | 33 +++++++++++---- doc/source/user/index.rst | 5 +-- openstack/_meta.py | 4 +- openstack/baremetal/v1/_proxy.py | 2 +- openstack/block_storage/v2/_proxy.py | 2 +- openstack/clustering/v1/_proxy.py | 2 +- openstack/compute/v2/_proxy.py | 2 +- openstack/connection.py | 6 +-- openstack/database/v1/_proxy.py | 2 +- openstack/identity/v2/_proxy.py | 2 +- openstack/identity/v3/_proxy.py | 2 +- openstack/image/v1/_proxy.py | 2 +- openstack/image/v2/_proxy.py | 2 +- openstack/key_manager/v1/_proxy.py | 2 +- openstack/load_balancer/v2/_proxy.py | 2 +- openstack/message/v2/_proxy.py | 2 +- openstack/network/v2/_proxy.py | 2 +- openstack/object_store/v1/_proxy.py | 2 +- openstack/orchestration/v1/_proxy.py | 2 +- openstack/proxy.py | 13 +++++- openstack/service_description.py | 12 +++--- .../tests/unit/clustering/v1/test_proxy.py | 42 +++++++++---------- openstack/tests/unit/compute/v2/test_proxy.py | 8 ++-- .../tests/unit/database/v1/test_proxy.py | 4 +- openstack/tests/unit/image/v2/test_proxy.py | 12 +++--- .../tests/unit/load_balancer/test_proxy.py | 8 ++-- openstack/tests/unit/message/v2/test_proxy.py | 30 ++++++------- openstack/tests/unit/network/v2/test_proxy.py | 40 +++++++++--------- .../tests/unit/object_store/v1/test_proxy.py | 4 +- .../tests/unit/orchestration/v1/test_proxy.py | 8 ++-- openstack/tests/unit/test_proxy.py | 16 +++---- openstack/tests/unit/test_proxy_base.py | 16 +++---- openstack/tests/unit/test_proxy_base2.py | 16 +++---- openstack/workflow/v2/_proxy.py | 2 +- .../rename-base-proxy-b9fcb22d373864a2.yaml | 5 +++ 35 files changed, 170 insertions(+), 144 deletions(-) create mode 100644 releasenotes/notes/rename-base-proxy-b9fcb22d373864a2.yaml diff --git a/doc/source/contributor/layout.rst b/doc/source/contributor/layout.rst index b9dcc111a..41d3ace5b 100644 --- a/doc/source/contributor/layout.rst +++ b/doc/source/contributor/layout.rst @@ -50,16 +50,30 @@ a dictionary keyed with the plural noun in the response. Proxy ----- -Each service implements a ``Proxy`` class, within the +Each service implements a ``Proxy`` class based on +:class:`~openstack.proxy.Proxy`, within the ``openstack//vX/_proxy.py`` module. For example, the v2 compute service's ``Proxy`` exists in ``openstack/compute/v2/_proxy.py``. -This ``Proxy`` class contains a :class:`~keystoneauth1.adapter.Adapter` and -provides a higher-level interface for users to work with via a -:class:`~openstack.connection.Connection` instance. Rather than requiring -users to maintain their own ``Adapter`` and work with lower-level -:class:`~openstack.resource.Resource` objects, the ``Proxy`` interface -offers a place to make things easier for the caller. +The :class:`~openstack.proxy.Proxy` class is based on +:class:`~openstack._adapter.OpenStackSDKAdapter` which is in turn based on +:class:`~keystoneauth1.adapter.Adapter`. + +.. autoclass:: openstack.proxy.Proxy + :members: + :show-inheritance: + +.. autoclass:: openstack._adapter.OpenStackSDKAdapter + :members: + :inherited-members: + :show-inheritance: + +Each service's ``Proxy`` provides a higher-level interface for users to work +with via a :class:`~openstack.connection.Connection` instance. + +Rather than requiring users to maintain their own ``Adapter`` and work with +lower-level :class:`~openstack.resource.Resource` objects, the ``Proxy`` +interface offers a place to make things easier for the caller. Each ``Proxy`` class implements methods which act on the underlying ``Resource`` classes which represent the service. For example:: @@ -80,8 +94,9 @@ Connection ---------- The :class:`openstack.connection.Connection` class builds atop a -:class:`os_client_config.config.CloudRegion` object, and provides a higher -level interface constructed of ``Proxy`` objects from each of the services. +:class:`openstack.config.cloud_region.CloudRegion` object, and provides a +higher level interface constructed of ``Proxy`` objects from each of the +services. The ``Connection`` class' primary purpose is to act as a high-level interface to this SDK, managing the lower level connecton bits and exposing the diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index 90ccb90a4..892994de5 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -73,12 +73,9 @@ OpenStack services. connection Once you have a *Connection* instance, services are accessed through instances -of :class:`~openstack.proxy.BaseProxy` or subclasses of it that exist as +of :class:`~openstack.proxy.Proxy` or subclasses of it that exist as attributes on the :class:`~openstack.connection.Connection`. -.. autoclass:: openstack.proxy.BaseProxy - :members: - .. _service-proxies: Service Proxies diff --git a/openstack/_meta.py b/openstack/_meta.py index da0bf6c83..eb4e3364c 100644 --- a/openstack/_meta.py +++ b/openstack/_meta.py @@ -62,9 +62,9 @@ def __new__(meta, name, bases, dct): doc = _PROXY_TEMPLATE.format( class_doc_strings=class_doc_strings, **service) else: - descriptor_args['proxy_class'] = proxy.BaseProxy + descriptor_args['proxy_class'] = proxy.Proxy doc = _DOC_TEMPLATE.format( - class_name='~openstack.proxy.BaseProxy', **service) + class_name='~openstack.proxy.Proxy', **service) descriptor = desc_class(**descriptor_args) descriptor.__doc__ = doc dct[service_type.replace('-', '_')] = descriptor diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index a802f6973..ba28099c0 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -19,7 +19,7 @@ from openstack import utils -class Proxy(proxy.BaseProxy): +class Proxy(proxy.Proxy): def chassis(self, details=False, **query): """Retrieve a generator of chassis. diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 4b7afd90c..825e6c935 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -17,7 +17,7 @@ from openstack import proxy -class Proxy(proxy.BaseProxy): +class Proxy(proxy.Proxy): def get_snapshot(self, snapshot): """Get a single snapshot diff --git a/openstack/clustering/v1/_proxy.py b/openstack/clustering/v1/_proxy.py index e17a23504..2179b8e0c 100644 --- a/openstack/clustering/v1/_proxy.py +++ b/openstack/clustering/v1/_proxy.py @@ -28,7 +28,7 @@ from openstack import utils -class Proxy(proxy.BaseProxy): +class Proxy(proxy.Proxy): def get_build_info(self): """Get build info for service engine and API diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 15d59acfb..7b32bdc96 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -27,7 +27,7 @@ from openstack import resource -class Proxy(proxy.BaseProxy): +class Proxy(proxy.Proxy): def find_extension(self, name_or_id, ignore_missing=True): """Find a single extension diff --git a/openstack/connection.py b/openstack/connection.py index 57663b444..d6d00b104 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -329,10 +329,10 @@ def session(self): def add_service(self, service): """Add a service to the Connection. - Attaches an instance of the :class:`~openstack.proxy.BaseProxy` + Attaches an instance of the :class:`~openstack.proxy.Proxy` class contained in :class:`~openstack.service_description.ServiceDescription`. - The :class:`~openstack.proxy.BaseProxy` will be attached to the + The :class:`~openstack.proxy.Proxy` will be attached to the `Connection` by its ``service_type`` and by any ``aliases`` that may be specified. @@ -343,7 +343,7 @@ class contained in :class:`~openstack.service_description.ServiceDescription` will be created. """ - # If we don't have a proxy, just instantiate BaseProxy so that + # If we don't have a proxy, just instantiate Proxy so that # we get an adapter. if isinstance(service, six.string_types): service = service_description.ServiceDescription(service) diff --git a/openstack/database/v1/_proxy.py b/openstack/database/v1/_proxy.py index 211e286f9..826e19297 100644 --- a/openstack/database/v1/_proxy.py +++ b/openstack/database/v1/_proxy.py @@ -17,7 +17,7 @@ from openstack import proxy -class Proxy(proxy.BaseProxy): +class Proxy(proxy.Proxy): def create_database(self, instance, **attrs): """Create a new database from attributes diff --git a/openstack/identity/v2/_proxy.py b/openstack/identity/v2/_proxy.py index 8b6b918e8..5c2175355 100644 --- a/openstack/identity/v2/_proxy.py +++ b/openstack/identity/v2/_proxy.py @@ -17,7 +17,7 @@ from openstack import proxy -class Proxy(proxy.BaseProxy): +class Proxy(proxy.Proxy): def extensions(self): """Retrieve a generator of extensions diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index e4796f86c..45e93df9c 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -34,7 +34,7 @@ from openstack import proxy -class Proxy(proxy.BaseProxy): +class Proxy(proxy.Proxy): def create_credential(self, **attrs): """Create a new credential from attributes diff --git a/openstack/image/v1/_proxy.py b/openstack/image/v1/_proxy.py index aa4ba6fbe..472107b16 100644 --- a/openstack/image/v1/_proxy.py +++ b/openstack/image/v1/_proxy.py @@ -14,7 +14,7 @@ from openstack import proxy -class Proxy(proxy.BaseProxy): +class Proxy(proxy.Proxy): def upload_image(self, **attrs): """Upload a new image from attributes diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index b943bc9a7..239c543e8 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -17,7 +17,7 @@ from openstack import resource -class Proxy(proxy.BaseProxy): +class Proxy(proxy.Proxy): def upload_image(self, container_format=None, disk_format=None, data=None, **attrs): diff --git a/openstack/key_manager/v1/_proxy.py b/openstack/key_manager/v1/_proxy.py index 21215bad1..c54bafeae 100644 --- a/openstack/key_manager/v1/_proxy.py +++ b/openstack/key_manager/v1/_proxy.py @@ -16,7 +16,7 @@ from openstack import proxy -class Proxy(proxy.BaseProxy): +class Proxy(proxy.Proxy): def create_container(self, **attrs): """Create a new container from attributes diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index 8542e2800..87c979a0e 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -20,7 +20,7 @@ from openstack import proxy -class Proxy(proxy.BaseProxy): +class Proxy(proxy.Proxy): def create_load_balancer(self, **attrs): """Create a new load balancer from attributes diff --git a/openstack/message/v2/_proxy.py b/openstack/message/v2/_proxy.py index f12c2174a..2d8dae961 100644 --- a/openstack/message/v2/_proxy.py +++ b/openstack/message/v2/_proxy.py @@ -18,7 +18,7 @@ from openstack import resource -class Proxy(proxy.BaseProxy): +class Proxy(proxy.Proxy): def create_queue(self, **attrs): """Create a new queue from attributes diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index a503a3723..0d5c67a7c 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -52,7 +52,7 @@ from openstack import utils -class Proxy(proxy.BaseProxy): +class Proxy(proxy.Proxy): def create_address_scope(self, **attrs): """Create a new address scope from attributes diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index e220cfd12..16e77a57c 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -16,7 +16,7 @@ from openstack import proxy -class Proxy(proxy.BaseProxy): +class Proxy(proxy.Proxy): Account = _account.Account Container = _container.Container diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index 161248c1a..f607de0e4 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -22,7 +22,7 @@ from openstack import proxy -class Proxy(proxy.BaseProxy): +class Proxy(proxy.Proxy): def create_stack(self, preview=False, **attrs): """Create a new stack from attributes diff --git a/openstack/proxy.py b/openstack/proxy.py index 4ad09909a..54ce196e0 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -16,7 +16,7 @@ from openstack import utils -# The _check_resource decorator is used on BaseProxy methods to ensure that +# The _check_resource decorator is used on Proxy methods to ensure that # the `actual` argument is in fact the type of the `expected` argument. # It does so under two cases: # 1. When strict=False, if and only if `actual` is a Resource instance, @@ -40,7 +40,7 @@ def check(self, expected, actual=None, *args, **kwargs): return wrap -class BaseProxy(_adapter.OpenStackSDKAdapter): +class Proxy(_adapter.OpenStackSDKAdapter): """Represents a service.""" def _get_resource(self, resource_type, value, **attrs): @@ -313,3 +313,12 @@ def wait_for_delete(self, value, interval=2, wait=120): to delete failed to occur in wait seconds. """ return resource.wait_for_delete(self, value, interval, wait) + + +class BaseProxy(Proxy): + # Backwards compat wrapper + + @utils.deprecated(deprecated_in="0.11.1", removed_in="1.0", + details="Use openstack.proxy.Proxy instead") + def __init__(self, *args, **kwargs): + super(BaseProxy, self).__init__(*args, **kwargs) diff --git a/openstack/service_description.py b/openstack/service_description.py index ef3647bd1..7526c5c4e 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -30,7 +30,7 @@ class ServiceDescription(object): #: Proxy class for this service - proxy_class = proxy.BaseProxy + proxy_class = proxy.Proxy #: main service_type to use to find this service in the catalog service_type = None #: list of aliases this service might be registered as @@ -55,10 +55,10 @@ def __init__(self, service_type, proxy_class=None, aliases=None): :param string service_type: service_type to look for in the keystone catalog - :param proxy.BaseProxy proxy_class: - subclass of :class:`~openstack.proxy.BaseProxy` implementing + :param proxy.Proxy proxy_class: + subclass of :class:`~openstack.proxy.Proxy` implementing an interface for this service. Defaults to - :class:`~openstack.proxy.BaseProxy` which provides REST operations + :class:`~openstack.proxy.Proxy` which provides REST operations but no additional features. :param list aliases: Optional list of aliases, if there is more than one name that might @@ -73,9 +73,9 @@ def __init__(self, service_type, proxy_class=None, aliases=None): self._proxy = None def _validate_proxy_class(self): - if not issubclass(self.proxy_class, proxy.BaseProxy): + if not issubclass(self.proxy_class, proxy.Proxy): raise TypeError( - "{module}.{proxy_class} must inherit from BaseProxy".format( + "{module}.{proxy_class} must inherit from Proxy".format( module=self.proxy_class.__module__, proxy_class=self.proxy_class.__name__)) diff --git a/openstack/tests/unit/clustering/v1/test_proxy.py b/openstack/tests/unit/clustering/v1/test_proxy.py index 10b60b747..74b472b6d 100644 --- a/openstack/tests/unit/clustering/v1/test_proxy.py +++ b/openstack/tests/unit/clustering/v1/test_proxy.py @@ -115,7 +115,7 @@ def test_cluster_update(self): self.verify_update(self.proxy.update_cluster, cluster.Cluster) @deprecation.fail_if_not_removed - @mock.patch.object(proxy_base.BaseProxy, '_find') + @mock.patch.object(proxy_base.Proxy, '_find') def test_cluster_add_nodes(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_find.return_value = mock_cluster @@ -135,7 +135,7 @@ def test_cluster_add_nodes_with_obj(self): expected_args=[["node1"]]) @deprecation.fail_if_not_removed - @mock.patch.object(proxy_base.BaseProxy, '_find') + @mock.patch.object(proxy_base.Proxy, '_find') def test_cluster_del_nodes(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_find.return_value = mock_cluster @@ -157,7 +157,7 @@ def test_cluster_del_nodes_with_obj(self): expected_kwargs={"key": "value"}) @deprecation.fail_if_not_removed - @mock.patch.object(proxy_base.BaseProxy, '_find') + @mock.patch.object(proxy_base.Proxy, '_find') def test_cluster_replace_nodes(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_find.return_value = mock_cluster @@ -177,7 +177,7 @@ def test_cluster_replace_nodes_with_obj(self): expected_args=[{"node1": "node2"}]) @deprecation.fail_if_not_removed - @mock.patch.object(proxy_base.BaseProxy, '_find') + @mock.patch.object(proxy_base.Proxy, '_find') def test_cluster_scale_out(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_find.return_value = mock_cluster @@ -197,7 +197,7 @@ def test_cluster_scale_out_with_obj(self): expected_args=[5]) @deprecation.fail_if_not_removed - @mock.patch.object(proxy_base.BaseProxy, '_find') + @mock.patch.object(proxy_base.Proxy, '_find') def test_cluster_scale_in(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_find.return_value = mock_cluster @@ -221,7 +221,7 @@ def test_services(self): service.Service, paginated=False) - @mock.patch.object(proxy_base.BaseProxy, '_find') + @mock.patch.object(proxy_base.Proxy, '_find') def test_cluster_resize(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_find.return_value = mock_cluster @@ -242,7 +242,7 @@ def test_cluster_resize_with_obj(self): expected_kwargs={'k1': 'v1', 'k2': 'v2'}) @deprecation.fail_if_not_removed - @mock.patch.object(proxy_base.BaseProxy, '_find') + @mock.patch.object(proxy_base.Proxy, '_find') def test_cluster_attach_policy(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_find.return_value = mock_cluster @@ -266,7 +266,7 @@ def test_cluster_attach_policy_with_obj(self): expected_kwargs={"k1": "v1", 'k2': "v2"}) @deprecation.fail_if_not_removed - @mock.patch.object(proxy_base.BaseProxy, '_find') + @mock.patch.object(proxy_base.Proxy, '_find') def test_cluster_detach_policy(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_find.return_value = mock_cluster @@ -286,7 +286,7 @@ def test_cluster_detach_policy_with_obj(self): expected_args=["FAKE_POLICY"]) @deprecation.fail_if_not_removed - @mock.patch.object(proxy_base.BaseProxy, '_find') + @mock.patch.object(proxy_base.Proxy, '_find') def test_cluster_update_policy(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_find.return_value = mock_cluster @@ -316,7 +316,7 @@ def test_collect_cluster_attrs(self): expected_kwargs={'cluster_id': 'FAKE_ID', 'path': 'path.to.attr'}) - @mock.patch.object(proxy_base.BaseProxy, '_get_resource') + @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_cluster_check(self, mock_get): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_get.return_value = mock_cluster @@ -325,7 +325,7 @@ def test_cluster_check(self, mock_get): method_args=["FAKE_CLUSTER"]) mock_get.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER") - @mock.patch.object(proxy_base.BaseProxy, '_get_resource') + @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_cluster_recover(self, mock_get): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_get.return_value = mock_cluster @@ -335,7 +335,7 @@ def test_cluster_recover(self, mock_get): mock_get.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER") @deprecation.fail_if_not_removed - @mock.patch.object(proxy_base.BaseProxy, '_get_resource') + @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_cluster_operation(self, mock_get): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_get.return_value = mock_cluster @@ -366,7 +366,7 @@ def test_node_get(self): self.verify_get(self.proxy.get_node, node.Node) def test_node_get_with_details(self): - self._verify2('openstack.proxy.BaseProxy._get', + self._verify2('openstack.proxy.Proxy._get', self.proxy.get_node, method_args=['NODE_ID'], method_kwargs={'details': True}, @@ -383,7 +383,7 @@ def test_nodes(self): def test_node_update(self): self.verify_update(self.proxy.update_node, node.Node) - @mock.patch.object(proxy_base.BaseProxy, '_get_resource') + @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_node_check(self, mock_get): mock_node = node.Node.new(id='FAKE_NODE') mock_get.return_value = mock_node @@ -392,7 +392,7 @@ def test_node_check(self, mock_get): method_args=["FAKE_NODE"]) mock_get.assert_called_once_with(node.Node, "FAKE_NODE") - @mock.patch.object(proxy_base.BaseProxy, '_get_resource') + @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_node_recover(self, mock_get): mock_node = node.Node.new(id='FAKE_NODE') mock_get.return_value = mock_node @@ -401,7 +401,7 @@ def test_node_recover(self, mock_get): method_args=["FAKE_NODE"]) mock_get.assert_called_once_with(node.Node, "FAKE_NODE") - @mock.patch.object(proxy_base.BaseProxy, '_get_resource') + @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_node_adopt(self, mock_get): mock_node = node.Node.new() mock_get.return_value = mock_node @@ -412,7 +412,7 @@ def test_node_adopt(self, mock_get): mock_get.assert_called_once_with(node.Node, None) - @mock.patch.object(proxy_base.BaseProxy, '_get_resource') + @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_node_adopt_preview(self, mock_get): mock_node = node.Node.new() mock_get.return_value = mock_node @@ -424,7 +424,7 @@ def test_node_adopt_preview(self, mock_get): mock_get.assert_called_once_with(node.Node, None) @deprecation.fail_if_not_removed - @mock.patch.object(proxy_base.BaseProxy, '_get_resource') + @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_node_operation(self, mock_get): mock_node = node.Node.new(id='FAKE_CLUSTER') mock_get.return_value = mock_node @@ -472,7 +472,7 @@ def test_get_cluster_policy(self): fake_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') # ClusterPolicy object as input - self._verify2('openstack.proxy.BaseProxy._get', + self._verify2('openstack.proxy.Proxy._get', self.proxy.get_cluster_policy, method_args=[fake_policy, "FAKE_CLUSTER"], expected_args=[cluster_policy.ClusterPolicy, @@ -481,7 +481,7 @@ def test_get_cluster_policy(self): expected_result=fake_policy) # Policy ID as input - self._verify2('openstack.proxy.BaseProxy._get', + self._verify2('openstack.proxy.Proxy._get', self.proxy.get_cluster_policy, method_args=["FAKE_POLICY", "FAKE_CLUSTER"], expected_args=[cluster_policy.ClusterPolicy, @@ -489,7 +489,7 @@ def test_get_cluster_policy(self): expected_kwargs={"cluster_id": "FAKE_CLUSTER"}) # Cluster object as input - self._verify2('openstack.proxy.BaseProxy._get', + self._verify2('openstack.proxy.Proxy._get', self.proxy.get_cluster_policy, method_args=["FAKE_POLICY", fake_cluster], expected_args=[cluster_policy.ClusterPolicy, diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index f0c192708..302340196 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -126,7 +126,7 @@ def test_server_interface_delete(self): test_interface.server_id = server_id # Case1: ServerInterface instance is provided as value - self._verify2("openstack.proxy.BaseProxy._delete", + self._verify2("openstack.proxy.Proxy._delete", self.proxy.delete_server_interface, method_args=[test_interface], method_kwargs={"server": server_id}, @@ -136,7 +136,7 @@ def test_server_interface_delete(self): "ignore_missing": True}) # Case2: ServerInterface ID is provided as value - self._verify2("openstack.proxy.BaseProxy._delete", + self._verify2("openstack.proxy.Proxy._delete", self.proxy.delete_server_interface, method_args=[interface_id], method_kwargs={"server": server_id}, @@ -163,7 +163,7 @@ def test_server_interface_get(self): test_interface.server_id = server_id # Case1: ServerInterface instance is provided as value - self._verify2('openstack.proxy.BaseProxy._get', + self._verify2('openstack.proxy.Proxy._get', self.proxy.get_server_interface, method_args=[test_interface], method_kwargs={"server": server_id}, @@ -172,7 +172,7 @@ def test_server_interface_get(self): "server_id": server_id}) # Case2: ServerInterface ID is provided as value - self._verify2('openstack.proxy.BaseProxy._get', + self._verify2('openstack.proxy.Proxy._get', self.proxy.get_server_interface, method_args=[interface_id], method_kwargs={"server": server_id}, diff --git a/openstack/tests/unit/database/v1/test_proxy.py b/openstack/tests/unit/database/v1/test_proxy.py index 516340f18..bbb51a9cb 100644 --- a/openstack/tests/unit/database/v1/test_proxy.py +++ b/openstack/tests/unit/database/v1/test_proxy.py @@ -41,7 +41,7 @@ def test_database_delete_ignore(self): expected_path_args={"instance_id": "test_id"}) def test_database_find(self): - self._verify2('openstack.proxy.BaseProxy._find', + self._verify2('openstack.proxy.Proxy._find', self.proxy.find_database, method_args=["db", "instance"], expected_args=[database.Database, "db"], @@ -106,7 +106,7 @@ def test_user_delete_ignore(self): expected_path_args={"instance_id": "id"}) def test_user_find(self): - self._verify2('openstack.proxy.BaseProxy._find', + self._verify2('openstack.proxy.Proxy._find', self.proxy.find_user, method_args=["user", "instance"], expected_args=[user.User, "user"], diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 4b6e66ad2..c8fe4afd5 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -57,7 +57,7 @@ def test_image_delete_ignore(self): self.verify_delete(self.proxy.delete_image, image.Image, True) @mock.patch("openstack.resource.Resource._translate_response") - @mock.patch("openstack.proxy.BaseProxy._get") + @mock.patch("openstack.proxy.Proxy._get") @mock.patch("openstack.image.v2.image.Image.update") def test_image_update(self, mock_update_image, mock_get_image, mock_transpose): @@ -104,7 +104,7 @@ def test_member_create(self): expected_kwargs={"image_id": "test_id"}) def test_member_delete(self): - self._verify2("openstack.proxy.BaseProxy._delete", + self._verify2("openstack.proxy.Proxy._delete", self.proxy.remove_member, method_args=["member_id"], method_kwargs={"image": "image_id", @@ -115,7 +115,7 @@ def test_member_delete(self): "ignore_missing": False}) def test_member_delete_ignore(self): - self._verify2("openstack.proxy.BaseProxy._delete", + self._verify2("openstack.proxy.Proxy._delete", self.proxy.remove_member, method_args=["member_id"], method_kwargs={"image": "image_id"}, @@ -125,7 +125,7 @@ def test_member_delete_ignore(self): "ignore_missing": True}) def test_member_update(self): - self._verify2("openstack.proxy.BaseProxy._update", + self._verify2("openstack.proxy.Proxy._update", self.proxy.update_member, method_args=['member_id', 'image_id'], expected_args=[member.Member], @@ -133,7 +133,7 @@ def test_member_update(self): 'image_id': 'image_id'}) def test_member_get(self): - self._verify2("openstack.proxy.BaseProxy._get", + self._verify2("openstack.proxy.Proxy._get", self.proxy.get_member, method_args=['member_id'], method_kwargs={"image": "image_id"}, @@ -142,7 +142,7 @@ def test_member_get(self): 'image_id': 'image_id'}) def test_member_find(self): - self._verify2("openstack.proxy.BaseProxy._find", + self._verify2("openstack.proxy.Proxy._find", self.proxy.find_member, method_args=['member_id'], method_kwargs={"image": "image_id"}, diff --git a/openstack/tests/unit/load_balancer/test_proxy.py b/openstack/tests/unit/load_balancer/test_proxy.py index efa32031e..f1c07e28e 100644 --- a/openstack/tests/unit/load_balancer/test_proxy.py +++ b/openstack/tests/unit/load_balancer/test_proxy.py @@ -134,7 +134,7 @@ def test_member_delete(self): expected_kwargs={'pool_id': self.POOL_ID}) def test_member_find(self): - self._verify2('openstack.proxy.BaseProxy._find', + self._verify2('openstack.proxy.Proxy._find', self.proxy.find_member, method_args=["MEMBER", self.POOL_ID], expected_args=[member.Member, "MEMBER"], @@ -142,7 +142,7 @@ def test_member_find(self): "ignore_missing": True}) def test_member_update(self): - self._verify2('openstack.proxy.BaseProxy._update', + self._verify2('openstack.proxy.Proxy._update', self.proxy.update_member, method_args=["MEMBER", self.POOL_ID], expected_args=[member.Member, "MEMBER"], @@ -225,7 +225,7 @@ def test_l7_rule_delete(self): expected_kwargs={'l7policy_id': self.L7_POLICY_ID}) def test_l7_rule_find(self): - self._verify2('openstack.proxy.BaseProxy._find', + self._verify2('openstack.proxy.Proxy._find', self.proxy.find_l7_rule, method_args=["RULE", self.L7_POLICY_ID], expected_args=[l7_rule.L7Rule, "RULE"], @@ -233,7 +233,7 @@ def test_l7_rule_find(self): "ignore_missing": True}) def test_l7_rule_update(self): - self._verify2('openstack.proxy.BaseProxy._update', + self._verify2('openstack.proxy.Proxy._update', self.proxy.update_l7_rule, method_args=["RULE", self.L7_POLICY_ID], expected_args=[l7_rule.L7Rule, "RULE"], diff --git a/openstack/tests/unit/message/v2/test_proxy.py b/openstack/tests/unit/message/v2/test_proxy.py index b52c24bea..fd9fb839c 100644 --- a/openstack/tests/unit/message/v2/test_proxy.py +++ b/openstack/tests/unit/message/v2/test_proxy.py @@ -46,7 +46,7 @@ def test_queue_delete(self): def test_queue_delete_ignore(self): self.verify_delete(self.proxy.delete_queue, queue.Queue, True) - @mock.patch.object(proxy_base.BaseProxy, '_get_resource') + @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_message_post(self, mock_get_resource): message_obj = message.Message(queue_name="test_queue") mock_get_resource.return_value = message_obj @@ -57,10 +57,10 @@ def test_message_post(self, mock_get_resource): mock_get_resource.assert_called_once_with(message.Message, None, queue_name="test_queue") - @mock.patch.object(proxy_base.BaseProxy, '_get_resource') + @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_message_get(self, mock_get_resource): mock_get_resource.return_value = "resource_or_id" - self._verify2("openstack.proxy.BaseProxy._get", + self._verify2("openstack.proxy.Proxy._get", self.proxy.get_message, method_args=["test_queue", "resource_or_id"], expected_args=[message.Message, "resource_or_id"]) @@ -76,12 +76,12 @@ def test_messages(self): paginated=True, method_args=["test_queue"], expected_kwargs={"queue_name": "test_queue"}) - @mock.patch.object(proxy_base.BaseProxy, '_get_resource') + @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_message_delete(self, mock_get_resource): fake_message = mock.Mock() fake_message.id = "message_id" mock_get_resource.return_value = fake_message - self._verify2("openstack.proxy.BaseProxy._delete", + self._verify2("openstack.proxy.Proxy._delete", self.proxy.delete_message, method_args=["test_queue", "resource_or_id", None, False], @@ -93,12 +93,12 @@ def test_message_delete(self, mock_get_resource): "resource_or_id", queue_name="test_queue") - @mock.patch.object(proxy_base.BaseProxy, '_get_resource') + @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_message_delete_claimed(self, mock_get_resource): fake_message = mock.Mock() fake_message.id = "message_id" mock_get_resource.return_value = fake_message - self._verify2("openstack.proxy.BaseProxy._delete", + self._verify2("openstack.proxy.Proxy._delete", self.proxy.delete_message, method_args=["test_queue", "resource_or_id", "claim_id", False], @@ -110,12 +110,12 @@ def test_message_delete_claimed(self, mock_get_resource): "resource_or_id", queue_name="test_queue") - @mock.patch.object(proxy_base.BaseProxy, '_get_resource') + @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_message_delete_ignore(self, mock_get_resource): fake_message = mock.Mock() fake_message.id = "message_id" mock_get_resource.return_value = fake_message - self._verify2("openstack.proxy.BaseProxy._delete", + self._verify2("openstack.proxy.Proxy._delete", self.proxy.delete_message, method_args=["test_queue", "resource_or_id", None, True], @@ -132,10 +132,10 @@ def test_subscription_create(self): self.proxy.create_subscription, method_args=["test_queue"]) - @mock.patch.object(proxy_base.BaseProxy, '_get_resource') + @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_subscription_get(self, mock_get_resource): mock_get_resource.return_value = "resource_or_id" - self._verify2("openstack.proxy.BaseProxy._get", + self._verify2("openstack.proxy.Proxy._get", self.proxy.get_subscription, method_args=["test_queue", "resource_or_id"], expected_args=[subscription.Subscription, @@ -152,7 +152,7 @@ def test_subscriptions(self): paginated=True, method_args=["test_queue"], expected_kwargs={"queue_name": "test_queue"}) - @mock.patch.object(proxy_base.BaseProxy, '_get_resource') + @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_subscription_delete(self, mock_get_resource): mock_get_resource.return_value = "resource_or_id" self.verify_delete(self.proxy.delete_subscription, @@ -162,7 +162,7 @@ def test_subscription_delete(self, mock_get_resource): subscription.Subscription, "resource_or_id", queue_name="test_queue") - @mock.patch.object(proxy_base.BaseProxy, '_get_resource') + @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_subscription_delete_ignore(self, mock_get_resource): mock_get_resource.return_value = "resource_or_id" self.verify_delete(self.proxy.delete_subscription, @@ -178,7 +178,7 @@ def test_claim_create(self): method_args=["test_queue"]) def test_claim_get(self): - self._verify2("openstack.proxy.BaseProxy._get", + self._verify2("openstack.proxy.Proxy._get", self.proxy.get_claim, method_args=["test_queue", "resource_or_id"], expected_args=[claim.Claim, @@ -189,7 +189,7 @@ def test_claim_get(self): 'openstack.message.v2.claim.Claim') def test_claim_update(self): - self._verify2("openstack.proxy.BaseProxy._update", + self._verify2("openstack.proxy.Proxy._update", self.proxy.update_claim, method_args=["test_queue", "resource_or_id"], method_kwargs={"k1": "v1"}, diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index f548a80a0..e71bf0c2d 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -316,7 +316,7 @@ def test_network_find(self): self.verify_find(self.proxy.find_network, network.Network) def test_network_find_with_filter(self): - self._verify2('openstack.proxy.BaseProxy._find', + self._verify2('openstack.proxy.Proxy._find', self.proxy.find_network, method_args=["net1"], method_kwargs={"project_id": "1"}, @@ -406,7 +406,7 @@ def test_pool_member_delete_ignore(self): {"pool": "test_id"}, {"pool_id": "test_id"}) def test_pool_member_find(self): - self._verify2('openstack.proxy.BaseProxy._find', + self._verify2('openstack.proxy.Proxy._find', self.proxy.find_pool_member, method_args=["MEMBER", "POOL"], expected_args=[pool_member.PoolMember, "MEMBER"], @@ -414,7 +414,7 @@ def test_pool_member_find(self): "ignore_missing": True}) def test_pool_member_get(self): - self._verify2('openstack.proxy.BaseProxy._get', + self._verify2('openstack.proxy.Proxy._get', self.proxy.get_pool_member, method_args=["MEMBER", "POOL"], expected_args=[pool_member.PoolMember, "MEMBER"], @@ -426,7 +426,7 @@ def test_pool_members(self): expected_kwargs={"pool_id": "test_id"}) def test_pool_member_update(self): - self._verify2("openstack.proxy.BaseProxy._update", + self._verify2("openstack.proxy.Proxy._update", self.proxy.update_pool_member, method_args=["MEMBER", "POOL"], expected_args=[pool_member.PoolMember, "MEMBER"], @@ -497,7 +497,7 @@ def test_qos_bandwidth_limit_rule_delete_ignore(self): def test_qos_bandwidth_limit_rule_find(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) - self._verify2('openstack.proxy.BaseProxy._find', + self._verify2('openstack.proxy.Proxy._find', self.proxy.find_qos_bandwidth_limit_rule, method_args=['rule_id', policy], expected_args=[ @@ -523,7 +523,7 @@ def test_qos_bandwidth_limit_rules(self): def test_qos_bandwidth_limit_rule_update(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) - self._verify2('openstack.proxy.BaseProxy._update', + self._verify2('openstack.proxy.Proxy._update', self.proxy.update_qos_bandwidth_limit_rule, method_args=['rule_id', policy], method_kwargs={'foo': 'bar'}, @@ -556,7 +556,7 @@ def test_qos_dscp_marking_rule_delete_ignore(self): def test_qos_dscp_marking_rule_find(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) - self._verify2('openstack.proxy.BaseProxy._find', + self._verify2('openstack.proxy.Proxy._find', self.proxy.find_qos_dscp_marking_rule, method_args=['rule_id', policy], expected_args=[qos_dscp_marking_rule.QoSDSCPMarkingRule, @@ -581,7 +581,7 @@ def test_qos_dscp_marking_rules(self): def test_qos_dscp_marking_rule_update(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) - self._verify2('openstack.proxy.BaseProxy._update', + self._verify2('openstack.proxy.Proxy._update', self.proxy.update_qos_dscp_marking_rule, method_args=['rule_id', policy], method_kwargs={'foo': 'bar'}, @@ -614,7 +614,7 @@ def test_qos_minimum_bandwidth_rule_delete_ignore(self): def test_qos_minimum_bandwidth_rule_find(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) - self._verify2('openstack.proxy.BaseProxy._find', + self._verify2('openstack.proxy.Proxy._find', self.proxy.find_qos_minimum_bandwidth_rule, method_args=['rule_id', policy], expected_args=[ @@ -640,7 +640,7 @@ def test_qos_minimum_bandwidth_rules(self): def test_qos_minimum_bandwidth_rule_update(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) - self._verify2('openstack.proxy.BaseProxy._update', + self._verify2('openstack.proxy.Proxy._update', self.proxy.update_qos_minimum_bandwidth_rule, method_args=['rule_id', policy], method_kwargs={'foo': 'bar'}, @@ -695,11 +695,11 @@ def test_quota_delete_ignore(self): def test_quota_get(self): self.verify_get(self.proxy.get_quota, quota.Quota) - @mock.patch.object(proxy_base.BaseProxy, "_get_resource") + @mock.patch.object(proxy_base.Proxy, "_get_resource") def test_quota_get_details(self, mock_get): fake_quota = mock.Mock(project_id='PROJECT') mock_get.return_value = fake_quota - self._verify2("openstack.proxy.BaseProxy._get", + self._verify2("openstack.proxy.Proxy._get", self.proxy.get_quota, method_args=['QUOTA_ID'], method_kwargs={'details': True}, @@ -708,11 +708,11 @@ def test_quota_get_details(self, mock_get): 'requires_id': False}) mock_get.assert_called_once_with(quota.Quota, 'QUOTA_ID') - @mock.patch.object(proxy_base.BaseProxy, "_get_resource") + @mock.patch.object(proxy_base.Proxy, "_get_resource") def test_quota_default_get(self, mock_get): fake_quota = mock.Mock(project_id='PROJECT') mock_get.return_value = fake_quota - self._verify2("openstack.proxy.BaseProxy._get", + self._verify2("openstack.proxy.Proxy._get", self.proxy.get_quota_default, method_args=['QUOTA_ID'], expected_args=[quota.QuotaDefault], @@ -773,7 +773,7 @@ def test_routers(self): def test_router_update(self): self.verify_update(self.proxy.update_router, router.Router) - @mock.patch.object(proxy_base.BaseProxy, '_get_resource') + @mock.patch.object(proxy_base.Proxy, '_get_resource') @mock.patch.object(router.Router, 'add_interface') def test_add_interface_to_router_with_port(self, mock_add_interface, mock_get): @@ -787,7 +787,7 @@ def test_add_interface_to_router_with_port(self, mock_add_interface, expected_kwargs={"port_id": "PORT"}) mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") - @mock.patch.object(proxy_base.BaseProxy, '_get_resource') + @mock.patch.object(proxy_base.Proxy, '_get_resource') @mock.patch.object(router.Router, 'add_interface') def test_add_interface_to_router_with_subnet(self, mock_add_interface, mock_get): @@ -801,7 +801,7 @@ def test_add_interface_to_router_with_subnet(self, mock_add_interface, expected_kwargs={"subnet_id": "SUBNET"}) mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") - @mock.patch.object(proxy_base.BaseProxy, '_get_resource') + @mock.patch.object(proxy_base.Proxy, '_get_resource') @mock.patch.object(router.Router, 'remove_interface') def test_remove_interface_from_router_with_port(self, mock_remove, mock_get): @@ -815,7 +815,7 @@ def test_remove_interface_from_router_with_port(self, mock_remove, expected_kwargs={"port_id": "PORT"}) mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") - @mock.patch.object(proxy_base.BaseProxy, '_get_resource') + @mock.patch.object(proxy_base.Proxy, '_get_resource') @mock.patch.object(router.Router, 'remove_interface') def test_remove_interface_from_router_with_subnet(self, mock_remove, mock_get): @@ -829,7 +829,7 @@ def test_remove_interface_from_router_with_subnet(self, mock_remove, expected_kwargs={"subnet_id": "SUBNET"}) mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") - @mock.patch.object(proxy_base.BaseProxy, '_get_resource') + @mock.patch.object(proxy_base.Proxy, '_get_resource') @mock.patch.object(router.Router, 'add_gateway') def test_add_gateway_to_router(self, mock_add, mock_get): x_router = router.Router.new(id="ROUTER_ID") @@ -842,7 +842,7 @@ def test_add_gateway_to_router(self, mock_add, mock_get): expected_kwargs={"foo": "bar"}) mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") - @mock.patch.object(proxy_base.BaseProxy, '_get_resource') + @mock.patch.object(proxy_base.Proxy, '_get_resource') @mock.patch.object(router.Router, 'remove_gateway') def test_remove_gateway_from_router(self, mock_remove, mock_get): x_router = router.Router.new(id="ROUTER_ID") diff --git a/openstack/tests/unit/object_store/v1/test_proxy.py b/openstack/tests/unit/object_store/v1/test_proxy.py index 1b5266750..f6634046a 100644 --- a/openstack/tests/unit/object_store/v1/test_proxy.py +++ b/openstack/tests/unit/object_store/v1/test_proxy.py @@ -60,7 +60,7 @@ def _test_object_delete(self, ignore): "container": "name", } - self._verify2("openstack.proxy.BaseProxy._delete", + self._verify2("openstack.proxy.Proxy._delete", self.proxy.delete_object, method_args=["resource"], method_kwargs=expected_kwargs, @@ -76,7 +76,7 @@ def test_object_delete_ignore(self): def test_object_create_attrs(self): kwargs = {"name": "test", "data": "data", "container": "name"} - self._verify2("openstack.proxy.BaseProxy._create", + self._verify2("openstack.proxy.Proxy._create", self.proxy.upload_object, method_kwargs=kwargs, expected_args=[obj.Object], diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index 54c5146ac..9fe788d6f 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -87,7 +87,7 @@ def test_get_stack_environment_with_stack_identity(self, mock_find): stk = stack.Stack(id=stack_id, name=stack_name) mock_find.return_value = stk - self._verify2('openstack.proxy.BaseProxy._get', + self._verify2('openstack.proxy.Proxy._get', self.proxy.get_stack_environment, method_args=['IDENTITY'], expected_args=[stack_environment.StackEnvironment], @@ -102,7 +102,7 @@ def test_get_stack_environment_with_stack_object(self): stack_name = 'test_stack' stk = stack.Stack(id=stack_id, name=stack_name) - self._verify2('openstack.proxy.BaseProxy._get', + self._verify2('openstack.proxy.Proxy._get', self.proxy.get_stack_environment, method_args=[stk], expected_args=[stack_environment.StackEnvironment], @@ -145,7 +145,7 @@ def test_get_stack_template_with_stack_identity(self, mock_find): stk = stack.Stack(id=stack_id, name=stack_name) mock_find.return_value = stk - self._verify2('openstack.proxy.BaseProxy._get', + self._verify2('openstack.proxy.Proxy._get', self.proxy.get_stack_template, method_args=['IDENTITY'], expected_args=[stack_template.StackTemplate], @@ -160,7 +160,7 @@ def test_get_stack_template_with_stack_object(self): stack_name = 'test_stack' stk = stack.Stack(id=stack_id, name=stack_name) - self._verify2('openstack.proxy.BaseProxy._get', + self._verify2('openstack.proxy.Proxy._get', self.proxy.get_stack_template, method_args=[stk], expected_args=[stack_template.StackTemplate], diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index dc72a6cd2..45880683c 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -53,7 +53,7 @@ def method(self, expected_type, value): self.sot = mock.Mock() self.sot.method = method - self.fake_proxy = proxy.BaseProxy("session") + self.fake_proxy = proxy.Proxy("session") def _test_correct(self, value): decorated = proxy._check_resource(strict=False)(self.sot.method) @@ -170,7 +170,7 @@ def setUp(self): self.res.id = self.fake_id self.res.delete = mock.Mock() - self.sot = proxy.BaseProxy(self.session) + self.sot = proxy.Proxy(self.session) DeleteableResource.new = mock.Mock(return_value=self.res) def test_delete(self): @@ -227,7 +227,7 @@ def setUp(self): self.res = mock.Mock(spec=UpdateableResource) self.res.update = mock.Mock(return_value=self.fake_result) - self.sot = proxy.BaseProxy(self.session) + self.sot = proxy.Proxy(self.session) self.attrs = {"x": 1, "y": 2, "z": 3} @@ -258,7 +258,7 @@ def setUp(self): self.res = mock.Mock(spec=CreateableResource) self.res.create = mock.Mock(return_value=self.fake_result) - self.sot = proxy.BaseProxy(self.session) + self.sot = proxy.Proxy(self.session) def test_create_attributes(self): CreateableResource.new = mock.Mock(return_value=self.res) @@ -285,7 +285,7 @@ def setUp(self): self.res.id = self.fake_id self.res.get = mock.Mock(return_value=self.fake_result) - self.sot = proxy.BaseProxy(self.session) + self.sot = proxy.Proxy(self.session) RetrieveableResource.new = mock.Mock(return_value=self.res) def test_get_resource(self): @@ -331,7 +331,7 @@ def setUp(self): self.args = {"a": "A", "b": "B", "c": "C"} self.fake_response = [resource.Resource()] - self.sot = proxy.BaseProxy(self.session) + self.sot = proxy.Proxy(self.session) ListableResource.list = mock.Mock() ListableResource.list.return_value = self.fake_response @@ -363,7 +363,7 @@ def setUp(self): self.res.id = self.fake_id self.res.head = mock.Mock(return_value=self.fake_result) - self.sot = proxy.BaseProxy(self.session) + self.sot = proxy.Proxy(self.session) HeadableResource.new = mock.Mock(return_value=self.res) def test_head_resource(self): @@ -386,7 +386,7 @@ def setUp(self): super(TestProxyWaits, self).setUp() self.session = mock.Mock() - self.sot = proxy.BaseProxy(self.session) + self.sot = proxy.Proxy(self.session) @mock.patch("openstack.resource.wait_for_status") def test_wait_for(self, mock_wait): diff --git a/openstack/tests/unit/test_proxy_base.py b/openstack/tests/unit/test_proxy_base.py index f547e7869..ad5261b4f 100644 --- a/openstack/tests/unit/test_proxy_base.py +++ b/openstack/tests/unit/test_proxy_base.py @@ -82,7 +82,7 @@ def _verify2(self, mock_method, test_method, mocked.assert_called_with(test_method.__self__) def verify_create(self, test_method, resource_type, - mock_method="openstack.proxy.BaseProxy._create", + mock_method="openstack.proxy.Proxy._create", expected_result="result", **kwargs): the_kwargs = {"x": 1, "y": 2, "z": 3} method_kwargs = kwargs.pop("method_kwargs", the_kwargs) @@ -100,7 +100,7 @@ def verify_delete(self, test_method, resource_type, ignore, input_path_args=None, expected_path_args=None, method_kwargs=None, expected_args=None, expected_kwargs=None, - mock_method="openstack.proxy.BaseProxy._delete"): + mock_method="openstack.proxy.Proxy._delete"): method_args = ["resource_or_id"] method_kwargs = method_kwargs or {} method_kwargs["ignore_missing"] = ignore @@ -121,7 +121,7 @@ def verify_delete(self, test_method, resource_type, ignore, expected_kwargs=expected_kwargs) def verify_get(self, test_method, resource_type, value=None, args=None, - mock_method="openstack.proxy.BaseProxy._get", + mock_method="openstack.proxy.Proxy._get", ignore_value=False, **kwargs): the_value = value if value is None: @@ -149,7 +149,7 @@ def verify_get_overrided(self, proxy, resource_type, patch_target): error_message=mock.ANY) def verify_head(self, test_method, resource_type, - mock_method="openstack.proxy.BaseProxy._head", + mock_method="openstack.proxy.Proxy._head", value=None, **kwargs): the_value = [value] if value is not None else [] expected_kwargs = {"path_args": kwargs} if kwargs else {} @@ -160,7 +160,7 @@ def verify_head(self, test_method, resource_type, expected_kwargs=expected_kwargs) def verify_find(self, test_method, resource_type, value=None, - mock_method="openstack.proxy.BaseProxy._find", + mock_method="openstack.proxy.Proxy._find", path_args=None, **kwargs): method_args = value or ["name_or_id"] expected_kwargs = {} @@ -187,7 +187,7 @@ def verify_find(self, test_method, resource_type, value=None, **kwargs) def verify_list(self, test_method, resource_type, paginated=False, - mock_method="openstack.proxy.BaseProxy._list", + mock_method="openstack.proxy.Proxy._list", **kwargs): expected_kwargs = kwargs.pop("expected_kwargs", {}) expected_kwargs.update({"paginated": paginated}) @@ -201,7 +201,7 @@ def verify_list(self, test_method, resource_type, paginated=False, def verify_list_no_kwargs(self, test_method, resource_type, paginated=False, - mock_method="openstack.proxy.BaseProxy._list"): + mock_method="openstack.proxy.Proxy._list"): self._verify2(mock_method, test_method, method_kwargs={}, expected_args=[resource_type], @@ -209,7 +209,7 @@ def verify_list_no_kwargs(self, test_method, resource_type, expected_result=["result"]) def verify_update(self, test_method, resource_type, value=None, - mock_method="openstack.proxy.BaseProxy._update", + mock_method="openstack.proxy.Proxy._update", expected_result="result", path_args=None, **kwargs): method_args = value or ["resource_or_id"] method_kwargs = {"x": 1, "y": 2, "z": 3} diff --git a/openstack/tests/unit/test_proxy_base2.py b/openstack/tests/unit/test_proxy_base2.py index 06d88ca40..b12b6ebcd 100644 --- a/openstack/tests/unit/test_proxy_base2.py +++ b/openstack/tests/unit/test_proxy_base2.py @@ -87,7 +87,7 @@ def _verify2(self, mock_method, test_method, mocked.assert_called_with(test_method.__self__) def verify_create(self, test_method, resource_type, - mock_method="openstack.proxy.BaseProxy._create", + mock_method="openstack.proxy.Proxy._create", expected_result="result", **kwargs): the_kwargs = {"x": 1, "y": 2, "z": 3} method_kwargs = kwargs.pop("method_kwargs", the_kwargs) @@ -105,7 +105,7 @@ def verify_delete(self, test_method, resource_type, ignore, input_path_args=None, expected_path_args=None, method_kwargs=None, expected_args=None, expected_kwargs=None, - mock_method="openstack.proxy.BaseProxy._delete"): + mock_method="openstack.proxy.Proxy._delete"): method_args = ["resource_or_id"] method_kwargs = method_kwargs or {} method_kwargs["ignore_missing"] = ignore @@ -126,7 +126,7 @@ def verify_delete(self, test_method, resource_type, ignore, expected_kwargs=expected_kwargs) def verify_get(self, test_method, resource_type, value=None, args=None, - mock_method="openstack.proxy.BaseProxy._get", + mock_method="openstack.proxy.Proxy._get", ignore_value=False, **kwargs): the_value = value if value is None: @@ -147,7 +147,7 @@ def verify_get(self, test_method, resource_type, value=None, args=None, expected_kwargs=expected_kwargs) def verify_head(self, test_method, resource_type, - mock_method="openstack.proxy.BaseProxy._head", + mock_method="openstack.proxy.Proxy._head", value=None, **kwargs): the_value = [value] if value is not None else [] if self.kwargs_to_path_args: @@ -161,7 +161,7 @@ def verify_head(self, test_method, resource_type, expected_kwargs=expected_kwargs) def verify_find(self, test_method, resource_type, value=None, - mock_method="openstack.proxy.BaseProxy._find", + mock_method="openstack.proxy.Proxy._find", path_args=None, **kwargs): method_args = value or ["name_or_id"] expected_kwargs = {} @@ -188,7 +188,7 @@ def verify_find(self, test_method, resource_type, value=None, **kwargs) def verify_list(self, test_method, resource_type, paginated=False, - mock_method="openstack.proxy.BaseProxy._list", + mock_method="openstack.proxy.Proxy._list", **kwargs): expected_kwargs = kwargs.pop("expected_kwargs", {}) expected_kwargs.update({"paginated": paginated}) @@ -202,7 +202,7 @@ def verify_list(self, test_method, resource_type, paginated=False, def verify_list_no_kwargs(self, test_method, resource_type, paginated=False, - mock_method="openstack.proxy.BaseProxy._list"): + mock_method="openstack.proxy.Proxy._list"): self._verify2(mock_method, test_method, method_kwargs={}, expected_args=[resource_type], @@ -210,7 +210,7 @@ def verify_list_no_kwargs(self, test_method, resource_type, expected_result=["result"]) def verify_update(self, test_method, resource_type, value=None, - mock_method="openstack.proxy.BaseProxy._update", + mock_method="openstack.proxy.Proxy._update", expected_result="result", path_args=None, **kwargs): method_args = value or ["resource_or_id"] method_kwargs = {"x": 1, "y": 2, "z": 3} diff --git a/openstack/workflow/v2/_proxy.py b/openstack/workflow/v2/_proxy.py index ffe7259b7..0e4a51333 100644 --- a/openstack/workflow/v2/_proxy.py +++ b/openstack/workflow/v2/_proxy.py @@ -15,7 +15,7 @@ from openstack.workflow.v2 import workflow as _workflow -class Proxy(proxy.BaseProxy): +class Proxy(proxy.Proxy): def create_workflow(self, **attrs): """Create a new workflow from attributes diff --git a/releasenotes/notes/rename-base-proxy-b9fcb22d373864a2.yaml b/releasenotes/notes/rename-base-proxy-b9fcb22d373864a2.yaml new file mode 100644 index 000000000..f34cdd970 --- /dev/null +++ b/releasenotes/notes/rename-base-proxy-b9fcb22d373864a2.yaml @@ -0,0 +1,5 @@ +--- +deprecations: + - | + `openstack.proxy.BaseProxy` has been renamed to `openstack.proxy.Proxy`. + A ``BaseProxy`` class remains for easing transition. From a61b5d25ded7adc3f4356ad41e42bb437932633a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 1 Feb 2018 12:35:56 -0600 Subject: [PATCH 1981/3836] Generate proxy methods from resource objects Each service has a set of Resources which define the fundamental qualities of the remote resources. Because of this, a large portion of the methods on Proxy classes are (or can be) boilerplate. Add a metaclass (two in a row!) that reads the definition of the Proxy class and looks for Resource classes attached to it. It then checks them to see which operations are allowed by looking at the Resource's allow_ flags. Based on that, it generates the standard methods and doc strings from a template and adds them to the class. If a method exists on the class when it is read, the generated method does not overwrite the existing method. Instead, it is attached as ``_generated_{method_name}``. This allows people to either write specific proxy methods and completely ignore the generated method, or to write specialized methods that then delegate action to the generated method. Since this is done as a metaclass at class object creation time, things like sphinx continue to work. One of the results of this is the addition of a reference to each resource class on the proxy object. I've wanted one of those before (I don't remember right now why I wanted it) This makes a change to just a few methods/resources in Server as an example of impact. If we like it we can go through and remove all of the boilerplate methods and leave only methods that are special. openstack.compute.v2._proxy.Proxy.servers is left in place largely because it has a special doc string. I think we could (and should) update the generation to look at the query parameters to find and document in the docstring for list methods what the supported parameters are. This stems from some thinking we had in shade about being able to generate most of the methods that fit the pattern. It's likely we'll want to do that for shade methods as well - but we should actually be able to piggyback shade methods on top of the proxy methods, or at least use a similar approach to reduce most of the boilerplate in the shade layer. Change-Id: I9bee095d90cad25acadbf311d4dd8af2e76ba00a --- openstack/_meta/__init__.py | 0 openstack/_meta/_proxy_templates.py | 150 ++++++++++++++++++ openstack/{_meta.py => _meta/connection.py} | 0 openstack/_meta/proxy.py | 124 +++++++++++++++ openstack/compute/v2/_proxy.py | 139 +--------------- openstack/compute/v2/flavor.py | 2 + openstack/compute/v2/server.py | 2 + openstack/connection.py | 2 +- openstack/proxy.py | 5 +- openstack/resource.py | 2 + openstack/tests/unit/compute/v2/test_proxy.py | 2 +- 11 files changed, 294 insertions(+), 134 deletions(-) create mode 100644 openstack/_meta/__init__.py create mode 100644 openstack/_meta/_proxy_templates.py rename openstack/{_meta.py => _meta/connection.py} (100%) create mode 100644 openstack/_meta/proxy.py diff --git a/openstack/_meta/__init__.py b/openstack/_meta/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/_meta/_proxy_templates.py b/openstack/_meta/_proxy_templates.py new file mode 100644 index 000000000..610f816a7 --- /dev/null +++ b/openstack/_meta/_proxy_templates.py @@ -0,0 +1,150 @@ +# Copyright 2018 Red Hat, 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. +"""Doc and Code templates to be used by the Proxy Metaclass. + +The doc templates and code templates are stored separately because having +either of them templated is weird in the first place, but having a doc +string inside of a function definition that's inside of a triple-quoted +string is just hard on the eyeballs. +""" + +_FIND_TEMPLATE = """Find a single {resource_name} + +:param name_or_id: The name or ID of an {resource_name}. +:param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. +:returns: One :class:`~{resource_class}` or None +""" + +_LIST_TEMPLATE = """Retrieve a generator of all {resource_name} + +:param bool details: When ``True``, returns + :class:`~{detail_class}` objects, + otherwise :class:`~{resource_class}`. + *Default: ``True``* +:param kwargs \*\*query: Optional query parameters to be sent to limit + the flavors being returned. + +:returns: A generator of {resource_name} instances. +:rtype: :class:`~{resource_class}` +""" + +_DELETE_TEMPLATE = """Delete a {resource_name} + +:param {name}: + The value can be either the ID of a {name} or a + :class:`~{resource_class}` instance. +:param bool ignore_missing: + When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` + will be raised when the {name} does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent {name}. + +:returns: ``None`` +""" + +_GET_TEMPLATE = """Get a single {resource_name} + +:param {name}: + The value can be the ID of a {name} or a + :class:`~{resource_class}` instance. + +:returns: One :class:`~{resource_class}` +:raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. +""" + +_CREATE_TEMPLATE = """Create a new {resource_name} from attributes + +:param dict attrs: + Keyword arguments which will be used to create a + :class:`~{resource_class}`. + +:returns: The results of {resource_name} creation +:rtype: :class:`~{resource_class}` +""" + +_UPDATE_TEMPLATE = """Update a {resource_name} + +:param {name}: + Either the ID of a {resource_name} or a :class:`~{resource_class}` + instance. +:attrs kwargs: + The attributes to update on the {resource_name} represented by + ``{name}``. + +:returns: The updated server +:rtype: :class:`~{resource_class}` +""" + +_DOC_TEMPLATES = { + 'create': _CREATE_TEMPLATE, + 'delete': _DELETE_TEMPLATE, + 'find': _FIND_TEMPLATE, + 'list': _LIST_TEMPLATE, + 'get': _GET_TEMPLATE, + 'update': _UPDATE_TEMPLATE, +} + +_FIND_SOURCE = """ +def find(self, name_or_id, ignore_missing=True): + return self._find( + self.{resource_name}, name_or_id, ignore_missing=ignore_missing) +""" + +_CREATE_SOURCE = """ +def create(self, **attrs): + return self._create(self.{resource_name}, **attrs) +""" + +_DELETE_SOURCE = """ +def delete(self, {name}, ignore_missing=True): + self._delete(self.{resource_name}, {name}, ignore_missing=ignore_missing) +""" + +_GET_SOURCE = """ +def get(self, {name}): + return self._get(self.{resource_name}, {name}) +""" + +_LIST_SOURCE = """ +def list(self, details=True, **query): + res_cls = self.{detail_name} if details else self.{resource_name} + return self._list(res_cls, paginated=True, **query) +""" + +_UPDATE_SOURCE = """ +def update(self, {name}, **attrs): + return self._update(self.{resource_name}, {name}, **attrs) +""" + +_SOURCE_TEMPLATES = { + 'create': _CREATE_SOURCE, + 'delete': _DELETE_SOURCE, + 'find': _FIND_SOURCE, + 'list': _LIST_SOURCE, + 'get': _GET_SOURCE, + 'update': _UPDATE_SOURCE, +} + + +def get_source_template(action, **kwargs): + return _SOURCE_TEMPLATES[action].format(**kwargs) + + +def get_doc_template(action, **kwargs): + return _DOC_TEMPLATES[action].format(**kwargs) diff --git a/openstack/_meta.py b/openstack/_meta/connection.py similarity index 100% rename from openstack/_meta.py rename to openstack/_meta/connection.py diff --git a/openstack/_meta/proxy.py b/openstack/_meta/proxy.py new file mode 100644 index 000000000..0186bbd84 --- /dev/null +++ b/openstack/_meta/proxy.py @@ -0,0 +1,124 @@ +# Copyright 2018 Red Hat, 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. + +# Inspired by code from +# https://github.com/micheles/decorator/blob/master/src/decorator.py +# which is MIT licensed. + +from openstack._meta import _proxy_templates +from openstack import resource + + +def compile_function(evaldict, action, module, **kwargs): + "Make a new functions" + + src = _proxy_templates.get_source_template(action, **kwargs) + + # Ensure each generated block of code has a unique filename for profilers + # (such as cProfile) that depend on the tuple of (, + # , ) being unique. + filename = ''.format(module=module) + code = compile(src, filename, 'exec') + exec(code, evaldict) + func = evaldict[action] + func.__source__ = src + return func + + +def add_function(dct, func, action, args, name_template='{action}_{name}'): + func_name = name_template.format(action=action, **args) + # If the class has the function already, don't override it + if func_name in dct: + func_name = '_generated_' + func_name + func.__name__ = func_name + func.__qualname__ = func_name + func.__doc__ = _proxy_templates.get_doc_template(action, **args) + func.__module__ = args['module'] + dct[func_name] = func + + +def expand_classname(res): + return '{module}.{name}'.format(module=res.__module__, name=res.__name__) + + +class ProxyMeta(type): + """Metaclass that generates standard methods based on Resources. + + Each service has a set of Resources which define the fundamental + qualities of the remote resources. A large portion of the methods + on Proxy classes are boilerplate. + + This metaclass reads the definition of the Proxy class and looks for + Resource classes attached to it. It then checks them to see which + operations are allowed by looking at the ``allow_`` flags. Based on that, + it generates the standard methods and adds them to the class. + + If a method exists on the class when it is read, the generated method + does not overwrite the existing method. Instead, it is attached as + ``_generated_{method_name}``. This allows people to either write + specific proxy methods and completely ignore the generated method, + or to write specialized methods that then delegate action to the generated + method. + + Since this is done as a metaclass at class object creation time, + things like sphinx continue to work. + """ + def __new__(meta, name, bases, dct): + # Build up a list of resource classes attached to the Proxy + resources = {} + details = {} + for k, v in dct.items(): + if isinstance(v, type) and issubclass(v, resource.Resource): + if v.detail_for: + details[v.detail_for.__name__] = v + else: + resources[v.__name__] = v + + for resource_name, res in resources.items(): + resource_class = expand_classname(res) + detail = details.get(resource_name, res) + detail_name = detail.__name__ + detail_class = expand_classname(detail) + + lower_name = resource_name.lower() + plural_name = getattr(res, 'plural_name', lower_name + 's') + args = dict( + resource_name=resource_name, + resource_class=resource_class, + name=lower_name, + module=res.__module__, + detail_name=detail_name, + detail_class=detail_class, + plural_name=plural_name, + ) + # Generate unbound methods from the template strings. + # We have to do a compile step rather than using somthing + # like existing function objects wrapped with closures + # because of the argument naming pattern for delete and update. + # You can't really change an argument name programmatically, + # at least not that I've been able to find. + # We pass in a copy of the dct dict so that the exec step can + # be done in the context of the class the methods will be attached + # to. This allows name resolution to work properly. + for action in ('create', 'get', 'update', 'delete'): + if getattr(res, 'allow_{action}'.format(action=action)): + func = compile_function(dct.copy(), action, **args) + add_function(dct, func, action, args) + if res.allow_list: + func = compile_function(dct.copy(), 'find', **args) + add_function(dct, func, 'find', args) + func = compile_function(dct.copy(), 'list', **args) + add_function(dct, func, 'list', args, plural_name) + + return super(ProxyMeta, meta).__new__(meta, name, bases, dct) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 7b32bdc96..a9cb8c2f9 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -29,96 +29,11 @@ class Proxy(proxy.Proxy): - def find_extension(self, name_or_id, ignore_missing=True): - """Find a single extension - - :param name_or_id: The name or ID of an extension. - :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. - :returns: One :class:`~openstack.compute.v2.extension.Extension` or - None - """ - return self._find(extension.Extension, name_or_id, - ignore_missing=ignore_missing) - - def extensions(self): - """Retrieve a generator of extensions - - :returns: A generator of extension instances. - :rtype: :class:`~openstack.compute.v2.extension.Extension` - """ - return self._list(extension.Extension, paginated=False) - - def find_flavor(self, name_or_id, ignore_missing=True): - """Find a single flavor - - :param name_or_id: The name or ID of a flavor. - :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. - :returns: One :class:`~openstack.compute.v2.flavor.Flavor` or None - """ - return self._find(_flavor.Flavor, name_or_id, - ignore_missing=ignore_missing) - - def create_flavor(self, **attrs): - """Create a new flavor from attributes - - :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.compute.v2.flavor.Flavor`, - comprised of the properties on the Flavor class. - - :returns: The results of flavor creation - :rtype: :class:`~openstack.compute.v2.flavor.Flavor` - """ - return self._create(_flavor.Flavor, **attrs) - - def delete_flavor(self, flavor, ignore_missing=True): - """Delete a flavor - - :param flavor: The value can be either the ID of a flavor or a - :class:`~openstack.compute.v2.flavor.Flavor` instance. - :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the flavor does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent flavor. - - :returns: ``None`` - """ - self._delete(_flavor.Flavor, flavor, ignore_missing=ignore_missing) - - def get_flavor(self, flavor): - """Get a single flavor - - :param flavor: The value can be the ID of a flavor or a - :class:`~openstack.compute.v2.flavor.Flavor` instance. - - :returns: One :class:`~openstack.compute.v2.flavor.Flavor` - :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. - """ - return self._get(_flavor.Flavor, flavor) - - def flavors(self, details=True, **query): - """Return a generator of flavors - - :param bool details: When ``True``, returns - :class:`~openstack.compute.v2.flavor.FlavorDetail` objects, - otherwise :class:`~openstack.compute.v2.flavor.Flavor`. - *Default: ``True``* - :param kwargs \*\*query: Optional query parameters to be sent to limit - the flavors being returned. - - :returns: A generator of flavor objects - """ - flv = _flavor.FlavorDetail if details else _flavor.Flavor - return self._list(flv, paginated=True, **query) + Extension = extension.Extension + Flavor = _flavor.Flavor + FlavorDetail = _flavor.FlavorDetail + Server = _server.Server + ServerDetail = _server.ServerDetail def delete_image(self, image, ignore_missing=True): """Delete an image @@ -312,18 +227,6 @@ def get_limits(self): """ return self._get(limits.Limits) - def create_server(self, **attrs): - """Create a new server from attributes - - :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.compute.v2.server.Server`, - comprised of the properties on the Server class. - - :returns: The results of server creation - :rtype: :class:`~openstack.compute.v2.server.Server` - """ - return self._create(_server.Server, **attrs) - def delete_server(self, server, ignore_missing=True, force=False): """Delete a server @@ -343,33 +246,8 @@ def delete_server(self, server, ignore_missing=True, force=False): server = self._get_resource(_server.Server, server) server.force_delete(self) else: - self._delete(_server.Server, server, ignore_missing=ignore_missing) - - def find_server(self, name_or_id, ignore_missing=True): - """Find a single server - - :param name_or_id: The name or ID of a server. - :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. - :returns: One :class:`~openstack.compute.v2.server.Server` or None - """ - return self._find(_server.Server, name_or_id, - ignore_missing=ignore_missing) - - def get_server(self, server): - """Get a single server - - :param server: The value can be the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. - - :returns: One :class:`~openstack.compute.v2.server.Server` - :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. - """ - return self._get(_server.Server, server) + self._generated_delete_server( + server, ignore_missing=ignore_missing) def servers(self, details=True, **query): """Retrieve a generator of servers @@ -408,8 +286,7 @@ def servers(self, details=True, **query): :returns: A generator of server instances. """ - srv = _server.ServerDetail if details else _server.Server - return self._list(srv, paginated=True, **query) + return self._generated_servers(details=details, **query) def update_server(self, server, **attrs): """Update a server diff --git a/openstack/compute/v2/flavor.py b/openstack/compute/v2/flavor.py index b21555135..20ed33a53 100644 --- a/openstack/compute/v2/flavor.py +++ b/openstack/compute/v2/flavor.py @@ -64,3 +64,5 @@ class FlavorDetail(Flavor): allow_update = False allow_delete = False allow_list = True + + detail_for = Flavor diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index bdacd39d9..fd7d93c27 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -383,3 +383,5 @@ class ServerDetail(Server): allow_update = False allow_delete = False allow_list = True + + detail_for = Server diff --git a/openstack/connection.py b/openstack/connection.py index d6d00b104..ac9e1b018 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -167,7 +167,7 @@ import six from openstack import _log -from openstack import _meta +from openstack._meta import connection as _meta from openstack import cloud as _cloud from openstack import config as _config from openstack.config import cloud_region diff --git a/openstack/proxy.py b/openstack/proxy.py index 54ce196e0..ac140816a 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -10,7 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. +import six + from openstack import _adapter +from openstack._meta import proxy as _meta from openstack import exceptions from openstack import resource from openstack import utils @@ -40,7 +43,7 @@ def check(self, expected, actual=None, *args, **kwargs): return wrap -class Proxy(_adapter.OpenStackSDKAdapter): +class Proxy(six.with_metaclass(_meta.ProxyMeta, _adapter.OpenStackSDKAdapter)): """Represents a service.""" def _get_resource(self, resource_type, value, **attrs): diff --git a/openstack/resource.py b/openstack/resource.py index ea58ee375..f16928653 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -317,6 +317,8 @@ class Resource(object): requires_id = True #: Do responses for this resource have bodies has_body = True + #: Is this a detailed version of another Resource + detail_for = None def __init__(self, _synchronized=False, **attrs): """The base resource diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 302340196..d4b1713cf 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -36,7 +36,7 @@ def test_extension_find(self): def test_extensions(self): self.verify_list_no_kwargs(self.proxy.extensions, extension.Extension, - paginated=False) + paginated=True) def test_flavor_create(self): self.verify_create(self.proxy.create_flavor, flavor.Flavor) From ebe5fde83068d351b072e669bc1c0d5755cc593b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 4 Feb 2018 12:00:00 -0600 Subject: [PATCH 1982/3836] Calculate name in CloudRegion We calculate a name from the auth_url in the from_session factory function, but it should really be a fundamental piece of logic for CloudRegion about what to do with name not being present since we've added in several paths for people to make a Connection that have nothing to do with named clouds in clouds.yaml. Change-Id: Ib4cdcde6be50295e214e7cd4ca8c65b7da431529 --- openstack/cloud/openstackcloud.py | 6 ++- openstack/config/cloud_region.py | 53 ++++++++++++++----- openstack/connection.py | 13 +---- openstack/profile.py | 6 +-- openstack/tests/unit/cloud/test_shade.py | 8 +++ .../tests/unit/config/test_cloud_config.py | 6 +-- 6 files changed, 57 insertions(+), 35 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index aba2a53b8..8a4a9dc6e 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -318,7 +318,9 @@ def pop_keys(params, auth, name_key, id_key): def session_constructor(*args, **kwargs): # We need to pass our current keystone session to the Session # Constructor, otherwise the new auth plugin doesn't get used. - return keystoneauth1.session.Session(session=self.session) + return keystoneauth1.session.Session( + session=self.session, + discovery_cache=self.config._discovery_cache) # Use cloud='defaults' so that we overlay settings properly cloud_config = config.get_one( @@ -326,7 +328,7 @@ def session_constructor(*args, **kwargs): session_constructor=session_constructor, **params) # Override the cloud name so that logging/location work right - cloud_config.name = self.name + cloud_config._name = self.name cloud_config.config['profile'] = self.name # Use self.__class__ so that we return whatever this if, like if it's # a subclass in the case of shade wrapping sdk. diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 0b19d23b8..84690aca0 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -60,9 +60,6 @@ def from_session(session, name=None, region_name=None, :param kwargs: Config settings for this cloud region. """ - # If someone is constructing one of these from a Session, then they are - # not using a named config. Use the hostname of their auth_url instead. - name = name or urllib.parse.urlparse(session.auth.auth_url).hostname config_dict = config_defaults.get_defaults() config_dict.update(**kwargs) return CloudRegion( @@ -77,11 +74,13 @@ class CloudRegion(object): A CloudRegion encapsulates the config information needed for connections to all of the services in a Region of a Cloud. """ - def __init__(self, name, region_name=None, config=None, + def __init__(self, name=None, region_name=None, config=None, force_ipv4=False, auth_plugin=None, openstack_config=None, session_constructor=None, - app_name=None, app_version=None, session=None): - self.name = name + app_name=None, app_version=None, session=None, + discovery_cache=None): + + self._name = name self.region_name = region_name self.config = config self.log = _log.setup_logging('openstack.config') @@ -92,6 +91,7 @@ def __init__(self, name, region_name=None, config=None, self._session_constructor = session_constructor or ks_session.Session self._app_name = app_name self._app_version = app_version + self._discovery_cache = discovery_cache or None def __getattr__(self, key): """Return arbitrary attributes.""" @@ -116,6 +116,32 @@ def __eq__(self, other): def __ne__(self, other): return not self == other + @property + def name(self): + if self._name is None: + try: + self._name = urllib.parse.urlparse( + self.get_session().auth.auth_url).hostname + except Exception: + self._name = self._app_name or '' + return self._name + + @property + def full_name(self): + """Return a string that can be used as an identifier. + + Always returns a valid string. It will have name and region_name + or just one of the two if only one is set, or else 'unknown'. + """ + if self.name and self.region_name: + return ":".join([self.name, self.region_name]) + elif self.name and not self.region_name: + return self.name + elif not self.name and self.region_name: + return self.region_name + else: + return 'unknown' + def set_session_constructor(self, session_constructor): """Sets the Session constructor.""" self._session_constructor = session_constructor @@ -128,9 +154,10 @@ def get_requests_verify_args(self): verify = self.config['verify'] if self.config['cacert']: warnings.warn( - "You are specifying a cacert for the cloud {0} but " - "also to ignore the host verification. The host SSL cert " - "will not be verified.".format(self.name)) + "You are specifying a cacert for the cloud {full_name}" + " but also to ignore the host verification. The host SSL" + " cert will not be verified.".format( + full_name=self.full_name)) cert = self.config.get('cert', None) if cert: @@ -216,15 +243,15 @@ def get_session(self): # cert verification if not verify: self.log.debug( - "Turning off SSL warnings for {cloud}:{region}" - " since verify=False".format( - cloud=self.name, region=self.region_name)) + "Turning off SSL warnings for {full_name}" + " since verify=False".format(full_name=self.full_name)) requestsexceptions.squelch_warnings(insecure_requests=not verify) self._keystone_session = self._session_constructor( auth=self._auth, verify=verify, cert=cert, - timeout=self.config['api_timeout']) + timeout=self.config['api_timeout'], + discovery_cache=self._discovery_cache) if hasattr(self._keystone_session, 'additional_user_agent'): self._keystone_session.additional_user_agent.append( ('openstacksdk', openstack_version.__version__)) diff --git a/openstack/connection.py b/openstack/connection.py index ac9e1b018..2deb1963d 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -295,18 +295,7 @@ def __init__(self, cloud=None, config=None, session=None, load_envvars=cloud is not None, **kwargs) - if self.config.name: - tm_name = ':'.join([ - self.config.name, - self.config.region_name or 'unknown']) - else: - tm_name = self.config.region_name or 'unknown' - - self.task_manager = task_manager.TaskManager(name=tm_name) - - if session: - # TODO(mordred) Expose constructor option for this in OCC - self.config._keystone_session = session + self.task_manager = task_manager.TaskManager(self.config.full_name) self._session = None self._proxies = {} diff --git a/openstack/profile.py b/openstack/profile.py index b502bf727..f19538aae 100644 --- a/openstack/profile.py +++ b/openstack/profile.py @@ -16,7 +16,6 @@ """ import copy -from six.moves import urllib from openstack import _log from openstack.config import cloud_region @@ -45,9 +44,6 @@ def _get_config_from_profile(profile, authenticator, **kwargs): # TODO(shade) Remove this once we've shifted python-openstackclient # to not use the profile interface. - # We don't have a cloud name. Make one up from the auth_url hostname - # so that log messages work. - name = urllib.parse.urlparse(authenticator.auth_url).hostname region_name = None for service in profile.get_services(): if service.region: @@ -66,7 +62,7 @@ def _get_config_from_profile(profile, authenticator, **kwargs): config_kwargs = config_defaults.get_defaults() config_kwargs.update(kwargs) config = cloud_region.CloudRegion( - name=name, region_name=region_name, config=config_kwargs) + region_name=region_name, config=config_kwargs) config._auth = authenticator return config diff --git a/openstack/tests/unit/cloud/test_shade.py b/openstack/tests/unit/cloud/test_shade.py index d8ea062bf..1c6932d3c 100644 --- a/openstack/tests/unit/cloud/test_shade.py +++ b/openstack/tests/unit/cloud/test_shade.py @@ -54,6 +54,14 @@ def fake_has_service(*args, **kwargs): def test_openstack_cloud(self): self.assertIsInstance(self.cloud, openstack.cloud.OpenStackCloud) + def test_connect_as(self): + # Do initial auth/catalog steps + # TODO(mordred) This only tests the constructor steps. Discovery + # cache sharing is broken. We need to get discovery_cache option + # plumbed through + # keystoneauth1.loading.base.BaseLoader.load_from_options + self.cloud.connect_as(project_name='test_project') + @mock.patch.object(openstack.cloud.OpenStackCloud, 'search_images') def test_get_images(self, mock_search): image1 = dict(id='123', name='mickey') diff --git a/openstack/tests/unit/config/test_cloud_config.py b/openstack/tests/unit/config/test_cloud_config.py index 722225f7d..ee2741940 100644 --- a/openstack/tests/unit/config/test_cloud_config.py +++ b/openstack/tests/unit/config/test_cloud_config.py @@ -192,7 +192,7 @@ def test_get_session(self, mock_session): cc.get_session() mock_session.assert_called_with( auth=mock.ANY, - verify=True, cert=None, timeout=None) + verify=True, cert=None, timeout=None, discovery_cache=None) self.assertEqual( fake_session.additional_user_agent, [('openstacksdk', openstack_version.__version__)]) @@ -212,7 +212,7 @@ def test_get_session_with_app_name(self, mock_session): cc.get_session() mock_session.assert_called_with( auth=mock.ANY, - verify=True, cert=None, timeout=None) + verify=True, cert=None, timeout=None, discovery_cache=None) self.assertEqual(fake_session.app_name, "test_app") self.assertEqual(fake_session.app_version, "test_version") self.assertEqual( @@ -232,7 +232,7 @@ def test_get_session_with_timeout(self, mock_session): cc.get_session() mock_session.assert_called_with( auth=mock.ANY, - verify=True, cert=None, timeout=9) + verify=True, cert=None, timeout=9, discovery_cache=None) self.assertEqual( fake_session.additional_user_agent, [('openstacksdk', openstack_version.__version__)]) From 821af87bb8759383a1b0eb9f5505a95bf76155a1 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 5 Feb 2018 16:56:15 -0600 Subject: [PATCH 1983/3836] Use get_session_client in Connection The openstack.config CloudRegion object knows how to make adapters, but we make them by hand in Connection. Add a constructor parameter and shift to using get_session_client from CloudRegion to make the proxy objects instead of duplicating the call. This should help us get version discovery plugged in properly. Change-Id: I486d1fc82ba9a1f1aafaf83e9a6ed8f01244ede6 --- openstack/config/cloud_region.py | 13 +++++++++---- openstack/service_description.py | 10 +++------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 84690aca0..8fce44ccc 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -285,11 +285,15 @@ def _get_version_args(self, service_key, version): return None, None, 'latest' if not version: version = self.get_api_version(service_key) - if not version: + # Octavia doens't have a version discovery document. Hard-code an + # exception to this logic for now. + if not version and service_key not in ('load-balancer',): return None, None, 'latest' return version, None, None - def get_session_client(self, service_key, version=None): + def get_session_client( + self, service_key, version=None, constructor=adapter.Adapter, + **kwargs): """Return a prepped requests adapter for a given service. This is useful for making direct requests calls against a @@ -306,7 +310,7 @@ def get_session_client(self, service_key, version=None): (version, min_version, max_version) = self._get_version_args( service_key, version) - return adapter.Adapter( + return constructor( session=self.get_session(), service_type=self.get_service_type(service_key), service_name=self.get_service_name(service_key), @@ -314,7 +318,8 @@ def get_session_client(self, service_key, version=None): region_name=self.region_name, version=version, min_version=min_version, - max_version=max_version) + max_version=max_version, + **kwargs) def _get_highest_endpoint(self, service_types, kwargs): session = self.get_session() diff --git a/openstack/service_description.py b/openstack/service_description.py index 7526c5c4e..245d7145f 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -88,15 +88,11 @@ def __get__(self, instance, owner): if self.service_type not in instance._proxies: config = instance.config proxy_class = self.get_proxy_class(config) - instance._proxies[self.service_type] = proxy_class( - session=instance.config.get_session(), + instance._proxies[self.service_type] = config.get_session_client( + self.service_type, + constructor=proxy_class, task_manager=instance.task_manager, allow_version_hack=True, - service_type=config.get_service_type(self.service_type), - service_name=config.get_service_name(self.service_type), - interface=config.get_interface(self.service_type), - region_name=config.region_name, - version=config.get_api_version(self.service_type) ) return instance._proxies[self.service_type] From 18a178456ca8499154163d13cb5b824f6726d182 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 2 Feb 2018 06:24:45 -0600 Subject: [PATCH 1984/3836] Update base test case to use base from oslotest We can get rid of our custom stdout/stderr handling by doing this. We should push our 'attach-on-exception' code up to oslotest so we can get rid of it locally. Remove a direct mock of time.sleep. This is already handled in the base test case. Also remove a time.sleep(40) - because of all of the reasons. Set the default for timeout to 3 seconds. Sometimes an error will cause a requests-mock test to spin in an infinite loop. All of the unit tests run in under a second and should stay that way. Change-Id: I7f9ea357a85754a6d3f79955d3ffb2a68a0ff621 --- openstack/cloud/openstackcloud.py | 15 +++--- openstack/config/loader.py | 3 +- openstack/tests/base.py | 49 +++++++++---------- .../functional/compute/v2/test_server.py | 4 -- .../functional/orchestration/v1/test_stack.py | 3 -- .../tests/unit/cloud/test_create_server.py | 3 +- openstack/tests/unit/test_resource.py | 2 +- tox.ini | 9 +++- 8 files changed, 43 insertions(+), 45 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 8a4a9dc6e..4354f6ed2 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -276,11 +276,14 @@ def connect_as(self, **kwargs): that do not want to be overridden can be ommitted. """ - # TODO(mordred) Replace this with from_session - config = openstack.config.OpenStackConfig( - app_name=self.config._app_name, - app_version=self.config._app_version, - load_yaml_config=False) + if self.config._openstack_config: + config = self.config._openstack_config + else: + # TODO(mordred) Replace this with from_session + config = openstack.config.OpenStackConfig( + app_name=self.config._app_name, + app_version=self.config._app_version, + load_yaml_config=False) params = copy.deepcopy(self.config.config) # Remove profile from current cloud so that overridding works params.pop('profile', None) @@ -322,9 +325,7 @@ def session_constructor(*args, **kwargs): session=self.session, discovery_cache=self.config._discovery_cache) - # Use cloud='defaults' so that we overlay settings properly cloud_config = config.get_one( - cloud='defaults', session_constructor=session_constructor, **params) # Override the cloud name so that logging/location work right diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 475935ed2..ac7f26994 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -103,7 +103,8 @@ def _get_os_environ(envvar_prefix=None): environkeys = [k for k in os.environ.keys() if (k.startswith('OS_') or k.startswith(envvar_prefix)) and not k.startswith('OS_TEST') # infra CI var - and not k.startswith('OS_STD') # infra CI var + and not k.startswith('OS_STD') # oslotest var + and not k.startswith('OS_LOG') # oslotest var ] for k in environkeys: newkey = k.split('_', 1)[-1].lower() diff --git a/openstack/tests/base.py b/openstack/tests/base.py index c09795f7d..3db9a0dac 100644 --- a/openstack/tests/base.py +++ b/openstack/tests/base.py @@ -14,19 +14,20 @@ # under the License. import os +import sys import fixtures import logging import munch +from oslotest import base import pprint from six import StringIO -import testtools import testtools.content _TRUE_VALUES = ('true', '1', 'yes') -class TestCase(testtools.TestCase): +class TestCase(base.BaseTestCase): """Test case base class for all tests.""" @@ -35,32 +36,25 @@ class TestCase(testtools.TestCase): def setUp(self): """Run before each test method to initialize test environment.""" + # No openstacksdk unit tests should EVER run longer than a second. + # Set this to 3 by default just to give us some fudge. + # Do this before super setUp so that we intercept the default value + # in oslotest. TODO(mordred) Make the default timeout configurable + # in oslotest. + self.useFixture( + fixtures.EnvironmentVariable( + 'OS_TEST_TIMEOUT', os.environ.get('OS_TEST_TIMEOUT', '3'))) super(TestCase, self).setUp() - test_timeout = int(os.environ.get('OS_TEST_TIMEOUT', 0)) - try: - test_timeout = int(test_timeout * self.TIMEOUT_SCALING_FACTOR) - except ValueError: - # If timeout value is invalid do not set a timeout. - test_timeout = 0 - if test_timeout > 0: - self.useFixture(fixtures.Timeout(test_timeout, gentle=True)) - - self.useFixture(fixtures.NestedTempfile()) - self.useFixture(fixtures.TempHomeDir()) - - if os.environ.get('OS_STDOUT_CAPTURE') in _TRUE_VALUES: - stdout = self.useFixture(fixtures.StringStream('stdout')).stream - self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout)) - if os.environ.get('OS_STDERR_CAPTURE') in _TRUE_VALUES: - stderr = self.useFixture(fixtures.StringStream('stderr')).stream - self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) - - self._log_stream = StringIO() - if os.environ.get('OS_ALWAYS_LOG') in _TRUE_VALUES: - self.addCleanup(self.printLogs) + + if os.environ.get('OS_LOG_CAPTURE') in _TRUE_VALUES: + self._log_stream = StringIO() + if os.environ.get('OS_ALWAYS_LOG') in _TRUE_VALUES: + self.addCleanup(self.printLogs) + else: + self.addOnException(self.attachLogs) else: - self.addOnException(self.attachLogs) + self._log_stream = sys.stdout handler = logging.StreamHandler(self._log_stream) formatter = logging.Formatter('%(asctime)s %(name)-32s %(message)s') @@ -76,6 +70,11 @@ def setUp(self): logger.addHandler(handler) logger.propagate = False + def _fake_logs(self): + # Override _fake_logs in oslotest until we can get our + # attach-on-exception logic added + pass + def assertEqual(self, first, second, *args, **kwargs): '''Munch aware wrapper''' if isinstance(first, munch.Munch): diff --git a/openstack/tests/functional/compute/v2/test_server.py b/openstack/tests/functional/compute/v2/test_server.py index 65883bbf7..1d618513b 100644 --- a/openstack/tests/functional/compute/v2/test_server.py +++ b/openstack/tests/functional/compute/v2/test_server.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import time - from openstack.compute.v2 import server from openstack.tests.functional import base from openstack.tests.functional.network.v2 import test_network @@ -50,8 +48,6 @@ def tearDown(self): self.assertIsNone(sot) # Need to wait for the stack to go away before network delete self.conn.compute.wait_for_delete(self.server) - # TODO(shade) sleeping in tests is bad mmkay? - time.sleep(40) test_network.delete_network(self.conn, self.network, self.subnet) super(TestServer, self).tearDown() diff --git a/openstack/tests/functional/orchestration/v1/test_stack.py b/openstack/tests/functional/orchestration/v1/test_stack.py index 4ccd14dee..aa6dc7cba 100644 --- a/openstack/tests/functional/orchestration/v1/test_stack.py +++ b/openstack/tests/functional/orchestration/v1/test_stack.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import time import unittest from openstack import exceptions @@ -68,8 +67,6 @@ def tearDown(self): self.stack, 'DELETE_COMPLETE') except exceptions.NotFoundException: pass - # TODO(shade) sleeping in tests is bad mmkay? - time.sleep(40) test_network.delete_network(self.conn, self.network, self.subnet) super(TestStack, self).tearDown() diff --git a/openstack/tests/unit/cloud/test_create_server.py b/openstack/tests/unit/cloud/test_create_server.py index 36050ed83..159d5912d 100644 --- a/openstack/tests/unit/cloud/test_create_server.py +++ b/openstack/tests/unit/cloud/test_create_server.py @@ -484,9 +484,8 @@ def test_create_server_wait(self, mock_wait): self.assert_calls() @mock.patch.object(openstack.cloud.OpenStackCloud, 'add_ips_to_server') - @mock.patch('time.sleep') def test_create_server_no_addresses( - self, mock_sleep, mock_add_ips_to_server): + self, mock_add_ips_to_server): """ Test that create_server with a wait throws an exception if the server doesn't have addresses. diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 1d0fab5bb..a5ad7052a 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1824,7 +1824,7 @@ def test_timeout(self): self.assertRaises(exceptions.ResourceTimeout, resource.wait_for_status, - "session", res, status, None, 1, 3) + "session", res, status, None, 0.01, 0.1) def test_no_sleep(self): res = mock.Mock() diff --git a/tox.ini b/tox.ini index 386918031..31396e3a3 100644 --- a/tox.ini +++ b/tox.ini @@ -6,11 +6,15 @@ skipsdist = True [testenv] usedevelop = True install_command = pip install {opts} {packages} +passenv = OS_* OPENSTACKSDK_* setenv = VIRTUAL_ENV={envdir} LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=C + OS_LOG_CAPTURE={env:OS_LOG_CAPTURE:true} + OS_STDOUT_CAPTURE={env:OS_STDOUT_CAPTURE:true} + OS_STDERR_CAPTURE={env:OS_STDERR_CAPTURE:true} deps = -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} -r{toxinidir}/test-requirements.txt @@ -19,13 +23,14 @@ commands = stestr run {posargs} stestr slowest [testenv:examples] -passenv = OS_* OPENSTACKSDK_* commands = stestr --test-path ./openstack/tests/examples run {posargs} stestr slowest [testenv:functional] basepython = {env:OPENSTACKSDK_TOX_PYTHON:python2} -passenv = OS_* OPENSTACKSDK_* +setenv = + {[testenv]setenv} + OS_TEST_TIMEOUT=60 commands = stestr --test-path ./openstack/tests/functional run --serial {posargs} stestr slowest From 1b43673c0464558f6929c1704dae94541d06f329 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 2 Feb 2018 06:45:29 -0600 Subject: [PATCH 1985/3836] Update all test base classes to use base.TestCase We have a centrally defined test case base class that handles a set of things like log capture, mocking time.sleep and setting up requests-mock. We also had a split base test case, with some things using TestCase and some using RequestsMockTestCase. This was a holdover from the transition to requests-mock. We are finally at the point where we don't need the split, so merge TestCase and RequestsMockTest case. Then, update all of the tests to use the new combined base class. Also, replace a use of unittest.skipTest with self.skipTest from the base class. Change-Id: I2cc3e201a5241262e5d102d3de8423c4fb2a8c4a --- HACKING.rst | 18 ++++++----- openstack/tests/examples/test_compute.py | 4 +-- openstack/tests/examples/test_identity.py | 4 +-- openstack/tests/examples/test_image.py | 4 +-- openstack/tests/examples/test_network.py | 4 +-- .../functional/orchestration/v1/test_stack.py | 6 ++-- .../unit/baremetal/test_baremetal_service.py | 4 +-- .../tests/unit/baremetal/test_version.py | 4 +-- .../tests/unit/baremetal/v1/test_chassis.py | 6 ++-- .../tests/unit/baremetal/v1/test_driver.py | 4 +-- .../tests/unit/baremetal/v1/test_node.py | 6 ++-- .../tests/unit/baremetal/v1/test_port.py | 6 ++-- .../unit/baremetal/v1/test_port_group.py | 6 ++-- openstack/tests/unit/base.py | 31 +++---------------- .../test_block_storage_service.py | 4 +-- .../unit/block_storage/v2/test_snapshot.py | 6 ++-- .../tests/unit/block_storage/v2/test_type.py | 4 +-- .../unit/block_storage/v2/test_volume.py | 6 ++-- .../tests/unit/block_store/v2/test_stats.py | 4 +-- openstack/tests/unit/cloud/test_aggregate.py | 2 +- .../unit/cloud/test_availability_zones.py | 2 +- openstack/tests/unit/cloud/test_caching.py | 2 +- .../unit/cloud/test_cluster_templates.py | 2 +- .../tests/unit/cloud/test_create_server.py | 2 +- .../unit/cloud/test_create_volume_snapshot.py | 2 +- .../tests/unit/cloud/test_delete_server.py | 2 +- .../unit/cloud/test_delete_volume_snapshot.py | 2 +- openstack/tests/unit/cloud/test_domains.py | 2 +- openstack/tests/unit/cloud/test_endpoints.py | 2 +- openstack/tests/unit/cloud/test_flavors.py | 2 +- .../unit/cloud/test_floating_ip_neutron.py | 2 +- .../tests/unit/cloud/test_floating_ip_nova.py | 2 +- .../tests/unit/cloud/test_floating_ip_pool.py | 2 +- openstack/tests/unit/cloud/test_groups.py | 2 +- .../tests/unit/cloud/test_identity_roles.py | 2 +- openstack/tests/unit/cloud/test_image.py | 8 ++--- .../tests/unit/cloud/test_image_snapshot.py | 2 +- openstack/tests/unit/cloud/test_keypair.py | 2 +- openstack/tests/unit/cloud/test_limits.py | 2 +- .../tests/unit/cloud/test_magnum_services.py | 2 +- openstack/tests/unit/cloud/test_meta.py | 2 +- openstack/tests/unit/cloud/test_network.py | 2 +- openstack/tests/unit/cloud/test_normalize.py | 10 +++++- openstack/tests/unit/cloud/test_object.py | 2 +- openstack/tests/unit/cloud/test_operator.py | 2 +- .../tests/unit/cloud/test_operator_noauth.py | 4 +-- openstack/tests/unit/cloud/test_port.py | 2 +- openstack/tests/unit/cloud/test_project.py | 2 +- .../cloud/test_qos_bandwidth_limit_rule.py | 2 +- .../unit/cloud/test_qos_dscp_marking_rule.py | 2 +- .../cloud/test_qos_minimum_bandwidth_rule.py | 2 +- openstack/tests/unit/cloud/test_qos_policy.py | 2 +- .../tests/unit/cloud/test_qos_rule_type.py | 2 +- openstack/tests/unit/cloud/test_quotas.py | 2 +- .../tests/unit/cloud/test_rebuild_server.py | 2 +- openstack/tests/unit/cloud/test_recordset.py | 2 +- .../tests/unit/cloud/test_role_assignment.py | 2 +- openstack/tests/unit/cloud/test_router.py | 2 +- .../tests/unit/cloud/test_security_groups.py | 2 +- .../tests/unit/cloud/test_server_console.py | 2 +- .../unit/cloud/test_server_delete_metadata.py | 2 +- .../tests/unit/cloud/test_server_group.py | 2 +- .../unit/cloud/test_server_set_metadata.py | 2 +- openstack/tests/unit/cloud/test_services.py | 2 +- openstack/tests/unit/cloud/test_shade.py | 2 +- .../tests/unit/cloud/test_shade_operator.py | 2 +- openstack/tests/unit/cloud/test_stack.py | 2 +- openstack/tests/unit/cloud/test_subnet.py | 2 +- .../tests/unit/cloud/test_update_server.py | 2 +- openstack/tests/unit/cloud/test_usage.py | 2 +- openstack/tests/unit/cloud/test_users.py | 2 +- openstack/tests/unit/cloud/test_volume.py | 2 +- .../tests/unit/cloud/test_volume_access.py | 2 +- .../tests/unit/cloud/test_volume_backups.py | 2 +- openstack/tests/unit/cloud/test_zone.py | 2 +- .../unit/clustering/test_cluster_service.py | 4 +-- .../tests/unit/clustering/test_version.py | 4 +-- .../tests/unit/clustering/v1/test_action.py | 4 +-- .../unit/clustering/v1/test_build_info.py | 4 +-- .../tests/unit/clustering/v1/test_cluster.py | 4 +-- .../unit/clustering/v1/test_cluster_attr.py | 4 +-- .../unit/clustering/v1/test_cluster_policy.py | 4 +-- .../tests/unit/clustering/v1/test_event.py | 4 +-- .../tests/unit/clustering/v1/test_node.py | 6 ++-- .../tests/unit/clustering/v1/test_policy.py | 6 ++-- .../unit/clustering/v1/test_policy_type.py | 4 +-- .../tests/unit/clustering/v1/test_profile.py | 6 ++-- .../unit/clustering/v1/test_profile_type.py | 4 +-- .../tests/unit/clustering/v1/test_receiver.py | 4 +-- .../tests/unit/clustering/v1/test_service.py | 4 +-- .../unit/compute/test_compute_service.py | 4 +-- openstack/tests/unit/compute/test_version.py | 4 +-- .../unit/compute/v2/test_availability_zone.py | 4 +-- .../tests/unit/compute/v2/test_extension.py | 4 +-- .../tests/unit/compute/v2/test_flavor.py | 4 +-- .../tests/unit/compute/v2/test_hypervisor.py | 4 +-- openstack/tests/unit/compute/v2/test_image.py | 4 +-- .../tests/unit/compute/v2/test_keypair.py | 4 +-- .../tests/unit/compute/v2/test_limits.py | 8 ++--- .../tests/unit/compute/v2/test_metadata.py | 4 +-- .../tests/unit/compute/v2/test_server.py | 4 +-- .../unit/compute/v2/test_server_group.py | 4 +-- .../unit/compute/v2/test_server_interface.py | 4 +-- .../tests/unit/compute/v2/test_server_ip.py | 4 +-- .../tests/unit/compute/v2/test_service.py | 4 +-- .../unit/compute/v2/test_volume_attachment.py | 4 +-- openstack/tests/unit/config/base.py | 9 ++---- .../tests/unit/config/test_from_session.py | 2 +- .../unit/database/test_database_service.py | 4 +-- .../tests/unit/database/v1/test_database.py | 4 +-- .../tests/unit/database/v1/test_flavor.py | 4 +-- .../tests/unit/database/v1/test_instance.py | 4 +-- openstack/tests/unit/database/v1/test_user.py | 4 +-- .../unit/identity/test_identity_service.py | 4 +-- openstack/tests/unit/identity/test_version.py | 4 +-- .../tests/unit/identity/v2/test_extension.py | 4 +-- openstack/tests/unit/identity/v2/test_role.py | 4 +-- .../tests/unit/identity/v2/test_tenant.py | 4 +-- openstack/tests/unit/identity/v2/test_user.py | 4 +-- .../tests/unit/identity/v3/test_credential.py | 4 +-- .../tests/unit/identity/v3/test_domain.py | 4 +-- .../tests/unit/identity/v3/test_endpoint.py | 4 +-- .../tests/unit/identity/v3/test_group.py | 4 +-- .../tests/unit/identity/v3/test_policy.py | 4 +-- .../tests/unit/identity/v3/test_project.py | 6 ++-- .../tests/unit/identity/v3/test_region.py | 4 +-- openstack/tests/unit/identity/v3/test_role.py | 4 +-- .../unit/identity/v3/test_role_assignment.py | 4 +-- .../v3/test_role_domain_group_assignment.py | 4 +-- .../v3/test_role_domain_user_assignment.py | 4 +-- .../v3/test_role_project_group_assignment.py | 4 +-- .../v3/test_role_project_user_assignment.py | 4 +-- .../tests/unit/identity/v3/test_service.py | 4 +-- .../tests/unit/identity/v3/test_trust.py | 4 +-- openstack/tests/unit/identity/v3/test_user.py | 4 +-- .../tests/unit/image/test_image_service.py | 4 +-- openstack/tests/unit/image/v1/test_image.py | 4 +-- openstack/tests/unit/image/v2/test_image.py | 4 +-- openstack/tests/unit/image/v2/test_member.py | 4 +-- .../test_key_management_service.py | 4 +-- .../unit/key_manager/v1/test_container.py | 4 +-- .../tests/unit/key_manager/v1/test_order.py | 4 +-- .../tests/unit/key_manager/v1/test_secret.py | 4 +-- .../unit/load_balancer/test_health_monitor.py | 4 +-- .../tests/unit/load_balancer/test_l7policy.py | 4 +-- .../tests/unit/load_balancer/test_l7rule.py | 4 +-- .../tests/unit/load_balancer/test_listener.py | 4 +-- .../unit/load_balancer/test_load_balancer.py | 4 +-- .../test_load_balancer_service.py | 4 +-- .../tests/unit/load_balancer/test_member.py | 4 +-- .../tests/unit/load_balancer/test_pool.py | 4 +-- .../tests/unit/load_balancer/test_version.py | 4 +-- .../unit/message/test_message_service.py | 4 +-- openstack/tests/unit/message/test_version.py | 4 +-- openstack/tests/unit/message/v2/test_claim.py | 4 +-- .../tests/unit/message/v2/test_message.py | 4 +-- openstack/tests/unit/message/v2/test_queue.py | 4 +-- .../unit/message/v2/test_subscription.py | 4 +-- .../unit/network/test_network_service.py | 4 +-- openstack/tests/unit/network/test_version.py | 4 +-- .../unit/network/v2/test_address_scope.py | 4 +-- openstack/tests/unit/network/v2/test_agent.py | 8 ++--- .../v2/test_auto_allocated_topology.py | 4 +-- .../unit/network/v2/test_availability_zone.py | 4 +-- .../tests/unit/network/v2/test_extension.py | 4 +-- .../tests/unit/network/v2/test_flavor.py | 4 +-- .../tests/unit/network/v2/test_floating_ip.py | 4 +-- .../unit/network/v2/test_health_monitor.py | 4 +-- .../tests/unit/network/v2/test_listener.py | 4 +-- .../unit/network/v2/test_load_balancer.py | 4 +-- .../unit/network/v2/test_metering_label.py | 4 +-- .../network/v2/test_metering_label_rule.py | 4 +-- .../tests/unit/network/v2/test_network.py | 6 ++-- .../v2/test_network_ip_availability.py | 4 +-- openstack/tests/unit/network/v2/test_pool.py | 4 +-- .../tests/unit/network/v2/test_pool_member.py | 4 +-- openstack/tests/unit/network/v2/test_port.py | 4 +-- .../v2/test_qos_bandwidth_limit_rule.py | 4 +-- .../network/v2/test_qos_dscp_marking_rule.py | 4 +-- .../v2/test_qos_minimum_bandwidth_rule.py | 4 +-- .../tests/unit/network/v2/test_qos_policy.py | 4 +-- .../unit/network/v2/test_qos_rule_type.py | 4 +-- openstack/tests/unit/network/v2/test_quota.py | 6 ++-- .../tests/unit/network/v2/test_rbac_policy.py | 4 +-- .../tests/unit/network/v2/test_router.py | 6 ++-- .../unit/network/v2/test_security_group.py | 4 +-- .../network/v2/test_security_group_rule.py | 4 +-- .../tests/unit/network/v2/test_segment.py | 4 +-- .../unit/network/v2/test_service_profile.py | 4 +-- .../unit/network/v2/test_service_provider.py | 4 +-- .../tests/unit/network/v2/test_subnet.py | 4 +-- .../tests/unit/network/v2/test_subnet_pool.py | 4 +-- openstack/tests/unit/network/v2/test_tag.py | 4 +-- .../tests/unit/network/v2/test_vpn_service.py | 4 +-- .../object_store/test_object_store_service.py | 4 +-- .../unit/object_store/v1/test_account.py | 4 +-- .../unit/object_store/v1/test_container.py | 2 +- .../test_orchestration_service.py | 4 +-- .../tests/unit/orchestration/test_version.py | 4 +-- .../unit/orchestration/v1/test_resource.py | 4 +-- .../orchestration/v1/test_software_config.py | 4 +-- .../v1/test_software_deployment.py | 4 +-- .../tests/unit/orchestration/v1/test_stack.py | 6 ++-- .../v1/test_stack_environment.py | 4 +-- .../unit/orchestration/v1/test_stack_files.py | 4 +-- .../orchestration/v1/test_stack_template.py | 4 +-- .../unit/orchestration/v1/test_template.py | 4 +-- openstack/tests/unit/test_connection.py | 4 +-- openstack/tests/unit/test_exceptions.py | 8 ++--- openstack/tests/unit/test_format.py | 4 +-- openstack/tests/unit/test_proxy.py | 18 +++++------ openstack/tests/unit/test_service_filter.py | 6 ++-- openstack/tests/unit/test_utils.py | 6 ++-- .../tests/unit/workflow/test_execution.py | 4 +-- openstack/tests/unit/workflow/test_version.py | 4 +-- .../tests/unit/workflow/test_workflow.py | 4 +-- .../unit/workflow/test_workflow_service.py | 4 +-- 217 files changed, 430 insertions(+), 444 deletions(-) diff --git a/HACKING.rst b/HACKING.rst index 6350ad49f..bccc1b455 100644 --- a/HACKING.rst +++ b/HACKING.rst @@ -7,8 +7,8 @@ http://docs.openstack.org/developer/hacking/ Indentation ----------- -PEP-8 allows for 'visual' indentation. Do not use it. Visual indentation looks -like this: +PEP-8 allows for 'visual' indentation. **Do not use it**. +Visual indentation looks like this: .. code-block:: python @@ -25,9 +25,10 @@ Instead of visual indentation, use this: arg1, arg1, arg3, arg4) That way, if some_method ever needs to be renamed, the only line that needs -to be touched is the line with some_method. Additionaly, if you need to -line break at the top of a block, please indent the continuation line -an additional 4 spaces, like this: +to be touched is the line with some_method. + +Additionaly, if you need to line break at the top of a block, please indent +the continuation line an additional 4 spaces, like this: .. code-block:: python @@ -44,6 +45,9 @@ Unit Tests Unit tests should be virtually instant. If a unit test takes more than 1 second to run, it is a bad unit test. Honestly, 1 second is too slow. -All unit test classes should subclass `openstack.tests.unit.base.BaseTestCase`. The -base TestCase class takes care of properly creating `OpenStackCloud` objects +All unit test classes should subclass `openstack.tests.unit.base.TestCase`. The +base TestCase class takes care of properly creating `Connection` objects in a way that protects against local environment. + +Test cases should use requests-mock to mock out HTTP interactions rather than +using mock to mock out object access. diff --git a/openstack/tests/examples/test_compute.py b/openstack/tests/examples/test_compute.py index 1e04b7085..127960898 100644 --- a/openstack/tests/examples/test_compute.py +++ b/openstack/tests/examples/test_compute.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from examples.compute import create from examples.compute import delete @@ -21,7 +21,7 @@ from examples.network import list as network_list -class TestCompute(testtools.TestCase): +class TestCompute(base.TestCase): """Test the compute examples The purpose of these tests is to ensure the examples run without erring diff --git a/openstack/tests/examples/test_identity.py b/openstack/tests/examples/test_identity.py index 29fc95fdd..0255f1103 100644 --- a/openstack/tests/examples/test_identity.py +++ b/openstack/tests/examples/test_identity.py @@ -10,13 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from examples import connect from examples.identity import list as identity_list -class TestIdentity(testtools.TestCase): +class TestIdentity(base.TestCase): """Test the identity examples The purpose of these tests is to ensure the examples run without erring diff --git a/openstack/tests/examples/test_image.py b/openstack/tests/examples/test_image.py index 0c1ec1054..b1e421da8 100644 --- a/openstack/tests/examples/test_image.py +++ b/openstack/tests/examples/test_image.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from examples import connect from examples.image import create as image_create @@ -18,7 +18,7 @@ from examples.image import list as image_list -class TestImage(testtools.TestCase): +class TestImage(base.TestCase): """Test the image examples The purpose of these tests is to ensure the examples run without erring diff --git a/openstack/tests/examples/test_network.py b/openstack/tests/examples/test_network.py index ee8646536..349d1f726 100644 --- a/openstack/tests/examples/test_network.py +++ b/openstack/tests/examples/test_network.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from examples import connect from examples.network import create as network_create @@ -19,7 +19,7 @@ from examples.network import list as network_list -class TestNetwork(testtools.TestCase): +class TestNetwork(base.TestCase): """Test the network examples The purpose of these tests is to ensure the examples run without erring diff --git a/openstack/tests/functional/orchestration/v1/test_stack.py b/openstack/tests/functional/orchestration/v1/test_stack.py index aa6dc7cba..d7673a1c6 100644 --- a/openstack/tests/functional/orchestration/v1/test_stack.py +++ b/openstack/tests/functional/orchestration/v1/test_stack.py @@ -10,15 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -import unittest - from openstack import exceptions from openstack.orchestration.v1 import stack from openstack.tests.functional import base from openstack.tests.functional.network.v2 import test_network -@unittest.skip("bug/1525005") class TestStack(base.BaseFunctionalTest): NAME = 'test_stack' @@ -29,6 +26,9 @@ class TestStack(base.BaseFunctionalTest): def setUp(self): super(TestStack, self).setUp() + self.skipTest( + 'Orchestration functional tests disabled:' + ' https://bugs.launchpad.net/python-openstacksdk/+bug/1525005') self.require_service('orchestration') if self.conn.compute.find_keypair(self.NAME) is None: diff --git a/openstack/tests/unit/baremetal/test_baremetal_service.py b/openstack/tests/unit/baremetal/test_baremetal_service.py index e808b6ef2..45ccd04e0 100644 --- a/openstack/tests/unit/baremetal/test_baremetal_service.py +++ b/openstack/tests/unit/baremetal/test_baremetal_service.py @@ -10,12 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.baremetal import baremetal_service -class TestBaremetalService(testtools.TestCase): +class TestBaremetalService(base.TestCase): def test_service(self): sot = baremetal_service.BaremetalService() diff --git a/openstack/tests/unit/baremetal/test_version.py b/openstack/tests/unit/baremetal/test_version.py index 75125e216..d6f63b6bd 100644 --- a/openstack/tests/unit/baremetal/test_version.py +++ b/openstack/tests/unit/baremetal/test_version.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.baremetal import version @@ -23,7 +23,7 @@ } -class TestVersion(testtools.TestCase): +class TestVersion(base.TestCase): def test_basic(self): sot = version.Version() diff --git a/openstack/tests/unit/baremetal/v1/test_chassis.py b/openstack/tests/unit/baremetal/v1/test_chassis.py index 3702ba596..216f0e92c 100644 --- a/openstack/tests/unit/baremetal/v1/test_chassis.py +++ b/openstack/tests/unit/baremetal/v1/test_chassis.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.baremetal.v1 import chassis @@ -43,7 +43,7 @@ } -class TestChassis(testtools.TestCase): +class TestChassis(base.TestCase): def test_basic(self): sot = chassis.Chassis() @@ -69,7 +69,7 @@ def test_instantiate(self): self.assertEqual(FAKE['updated_at'], sot.updated_at) -class TestChassisDetail(testtools.TestCase): +class TestChassisDetail(base.TestCase): def test_basic(self): sot = chassis.ChassisDetail() diff --git a/openstack/tests/unit/baremetal/v1/test_driver.py b/openstack/tests/unit/baremetal/v1/test_driver.py index c360e0eba..dafd870a6 100644 --- a/openstack/tests/unit/baremetal/v1/test_driver.py +++ b/openstack/tests/unit/baremetal/v1/test_driver.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.baremetal.v1 import driver @@ -43,7 +43,7 @@ } -class TestDriver(testtools.TestCase): +class TestDriver(base.TestCase): def test_basic(self): sot = driver.Driver() diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index b96306111..396a9d9ad 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.baremetal.v1 import node @@ -91,7 +91,7 @@ } -class TestNode(testtools.TestCase): +class TestNode(base.TestCase): def test_basic(self): sot = node.Node() @@ -145,7 +145,7 @@ def test_instantiate(self): self.assertEqual(FAKE['updated_at'], sot.updated_at) -class TestNodeDetail(testtools.TestCase): +class TestNodeDetail(base.TestCase): def test_basic(self): sot = node.NodeDetail() diff --git a/openstack/tests/unit/baremetal/v1/test_port.py b/openstack/tests/unit/baremetal/v1/test_port.py index 1ae202f63..466590146 100644 --- a/openstack/tests/unit/baremetal/v1/test_port.py +++ b/openstack/tests/unit/baremetal/v1/test_port.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.baremetal.v1 import port @@ -42,7 +42,7 @@ } -class TestPort(testtools.TestCase): +class TestPort(base.TestCase): def test_basic(self): sot = port.Port() @@ -73,7 +73,7 @@ def test_instantiate(self): self.assertEqual(FAKE['updated_at'], sot.updated_at) -class TestPortDetail(testtools.TestCase): +class TestPortDetail(base.TestCase): def test_basic(self): sot = port.PortDetail() diff --git a/openstack/tests/unit/baremetal/v1/test_port_group.py b/openstack/tests/unit/baremetal/v1/test_port_group.py index d8d5abdfe..0f0432ff2 100644 --- a/openstack/tests/unit/baremetal/v1/test_port_group.py +++ b/openstack/tests/unit/baremetal/v1/test_port_group.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.baremetal.v1 import port_group @@ -47,7 +47,7 @@ } -class TestPortGroup(testtools.TestCase): +class TestPortGroup(base.TestCase): def test_basic(self): sot = port_group.PortGroup() @@ -78,7 +78,7 @@ def test_instantiate(self): self.assertEqual(FAKE['updated_at'], sot.updated_at) -class TestPortGroupDetail(testtools.TestCase): +class TestPortGroupDetail(base.TestCase): def test_basic(self): sot = port_group.PortGroupDetail() diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index d32740a67..e9836f643 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -18,7 +18,6 @@ import uuid import fixtures -import mock import os import openstack.config as occ from requests import structures @@ -81,12 +80,12 @@ 'role_id, role_name, json_response, json_request') -class BaseTestCase(base.TestCase): +class TestCase(base.TestCase): def setUp(self, cloud_config_fixture='clouds.yaml'): """Run before each test method to initialize test environment.""" - super(BaseTestCase, self).setUp() + super(TestCase, self).setUp() # Sleeps are for real testing, but unit tests shouldn't need them realsleep = time.sleep @@ -99,7 +98,7 @@ def _nosleep(seconds): _nosleep)) self.fixtures_directory = 'openstack/tests/unit/fixtures' - # Isolate os-client-config from test environment + # Isolate openstack.config from test environment config = tempfile.NamedTemporaryFile(delete=False) cloud_path = '%s/clouds/%s' % (self.fixtures_directory, cloud_config_fixture) @@ -126,28 +125,6 @@ def _nosleep(seconds): config=self.cloud_config, strict=True) - -# TODO(shade) Remove this and rename RequestsMockTestCase to TestCase. -# There are still a few places, like test_normalize, that assume -# this mocking is in place rather than having the correct -# requests_mock entries set up that need to be converted. -class TestCase(BaseTestCase): - - def setUp(self, cloud_config_fixture='clouds.yaml'): - - super(TestCase, self).setUp(cloud_config_fixture=cloud_config_fixture) - self.session_fixture = self.useFixture(fixtures.MonkeyPatch( - 'openstack.config.cloud_region.CloudRegion.get_session', - mock.Mock())) - - -class RequestsMockTestCase(BaseTestCase): - - def setUp(self, cloud_config_fixture='clouds.yaml'): - - super(RequestsMockTestCase, self).setUp( - cloud_config_fixture=cloud_config_fixture) - # FIXME(notmorgan): Convert the uri_registry, discovery.json, and # use of keystone_v3/v2 to a proper fixtures.Fixture. For now this # is acceptable, but eventually this should become it's own fixture @@ -653,7 +630,7 @@ def assert_calls(self, stop_after=None, do_count=True): len(self.calls), len(self.adapter.request_history)) -class IronicTestCase(RequestsMockTestCase): +class IronicTestCase(TestCase): def setUp(self): super(IronicTestCase, self).setUp() diff --git a/openstack/tests/unit/block_storage/test_block_storage_service.py b/openstack/tests/unit/block_storage/test_block_storage_service.py index fd4ea444e..9624f3b92 100644 --- a/openstack/tests/unit/block_storage/test_block_storage_service.py +++ b/openstack/tests/unit/block_storage/test_block_storage_service.py @@ -10,12 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.block_storage import block_storage_service -class TestBlockStorageService(testtools.TestCase): +class TestBlockStorageService(base.TestCase): def test_service(self): sot = block_storage_service.BlockStorageService() diff --git a/openstack/tests/unit/block_storage/v2/test_snapshot.py b/openstack/tests/unit/block_storage/v2/test_snapshot.py index 6e542bd17..1beb99efc 100644 --- a/openstack/tests/unit/block_storage/v2/test_snapshot.py +++ b/openstack/tests/unit/block_storage/v2/test_snapshot.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.block_storage.v2 import snapshot @@ -38,7 +38,7 @@ DETAILED_SNAPSHOT.update(**DETAILS) -class TestSnapshot(testtools.TestCase): +class TestSnapshot(base.TestCase): def test_basic(self): sot = snapshot.Snapshot(SNAPSHOT) @@ -72,7 +72,7 @@ def test_create_basic(self): self.assertTrue(sot.is_forced) -class TestSnapshotDetail(testtools.TestCase): +class TestSnapshotDetail(base.TestCase): def test_basic(self): sot = snapshot.SnapshotDetail(DETAILED_SNAPSHOT) diff --git a/openstack/tests/unit/block_storage/v2/test_type.py b/openstack/tests/unit/block_storage/v2/test_type.py index b44c39dbe..ffb7a08bf 100644 --- a/openstack/tests/unit/block_storage/v2/test_type.py +++ b/openstack/tests/unit/block_storage/v2/test_type.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.block_storage.v2 import type @@ -24,7 +24,7 @@ } -class TestType(testtools.TestCase): +class TestType(base.TestCase): def test_basic(self): sot = type.Type(**TYPE) diff --git a/openstack/tests/unit/block_storage/v2/test_volume.py b/openstack/tests/unit/block_storage/v2/test_volume.py index 9c3207b8c..d08c83167 100644 --- a/openstack/tests/unit/block_storage/v2/test_volume.py +++ b/openstack/tests/unit/block_storage/v2/test_volume.py @@ -12,7 +12,7 @@ import copy -import testtools +from openstack.tests.unit import base from openstack.block_storage.v2 import volume @@ -61,7 +61,7 @@ VOLUME_DETAIL.update(DETAILS) -class TestVolume(testtools.TestCase): +class TestVolume(base.TestCase): def test_basic(self): sot = volume.Volume(VOLUME) @@ -102,7 +102,7 @@ def test_create(self): self.assertEqual(VOLUME["imageRef"], sot.image_id) -class TestVolumeDetail(testtools.TestCase): +class TestVolumeDetail(base.TestCase): def test_basic(self): sot = volume.VolumeDetail(VOLUME_DETAIL) diff --git a/openstack/tests/unit/block_store/v2/test_stats.py b/openstack/tests/unit/block_store/v2/test_stats.py index a8991a81b..0ccb7eef6 100644 --- a/openstack/tests/unit/block_store/v2/test_stats.py +++ b/openstack/tests/unit/block_store/v2/test_stats.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.block_storage.v2 import stats @@ -28,7 +28,7 @@ } -class TestBackendPools(testtools.TestCase): +class TestBackendPools(base.TestCase): def setUp(self): super(TestBackendPools, self).setUp() diff --git a/openstack/tests/unit/cloud/test_aggregate.py b/openstack/tests/unit/cloud/test_aggregate.py index 5da65ef38..974aba727 100644 --- a/openstack/tests/unit/cloud/test_aggregate.py +++ b/openstack/tests/unit/cloud/test_aggregate.py @@ -14,7 +14,7 @@ from openstack.tests import fakes -class TestAggregate(base.RequestsMockTestCase): +class TestAggregate(base.TestCase): def setUp(self): super(TestAggregate, self).setUp() diff --git a/openstack/tests/unit/cloud/test_availability_zones.py b/openstack/tests/unit/cloud/test_availability_zones.py index 965c41470..82c3f19e1 100644 --- a/openstack/tests/unit/cloud/test_availability_zones.py +++ b/openstack/tests/unit/cloud/test_availability_zones.py @@ -36,7 +36,7 @@ } -class TestAvailabilityZoneNames(base.RequestsMockTestCase): +class TestAvailabilityZoneNames(base.TestCase): def test_list_availability_zone_names(self): self.register_uris([ diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py index 791fdcbc7..012b16e79 100644 --- a/openstack/tests/unit/cloud/test_caching.py +++ b/openstack/tests/unit/cloud/test_caching.py @@ -92,7 +92,7 @@ def _(msg): ) -class TestMemoryCache(base.RequestsMockTestCase): +class TestMemoryCache(base.TestCase): def setUp(self): super(TestMemoryCache, self).setUp( diff --git a/openstack/tests/unit/cloud/test_cluster_templates.py b/openstack/tests/unit/cloud/test_cluster_templates.py index 56b45c028..32fe56f67 100644 --- a/openstack/tests/unit/cloud/test_cluster_templates.py +++ b/openstack/tests/unit/cloud/test_cluster_templates.py @@ -49,7 +49,7 @@ ) -class TestClusterTemplates(base.RequestsMockTestCase): +class TestClusterTemplates(base.TestCase): def test_list_cluster_templates_without_detail(self): diff --git a/openstack/tests/unit/cloud/test_create_server.py b/openstack/tests/unit/cloud/test_create_server.py index 159d5912d..2de7073ed 100644 --- a/openstack/tests/unit/cloud/test_create_server.py +++ b/openstack/tests/unit/cloud/test_create_server.py @@ -28,7 +28,7 @@ from openstack.tests.unit import base -class TestCreateServer(base.RequestsMockTestCase): +class TestCreateServer(base.TestCase): def test_create_server_with_get_exception(self): """ diff --git a/openstack/tests/unit/cloud/test_create_volume_snapshot.py b/openstack/tests/unit/cloud/test_create_volume_snapshot.py index 49386e3bd..1986fdb12 100644 --- a/openstack/tests/unit/cloud/test_create_volume_snapshot.py +++ b/openstack/tests/unit/cloud/test_create_volume_snapshot.py @@ -23,7 +23,7 @@ from openstack.tests.unit import base -class TestCreateVolumeSnapshot(base.RequestsMockTestCase): +class TestCreateVolumeSnapshot(base.TestCase): def test_create_volume_snapshot_wait(self): """ diff --git a/openstack/tests/unit/cloud/test_delete_server.py b/openstack/tests/unit/cloud/test_delete_server.py index 304818ae1..5ae11070e 100644 --- a/openstack/tests/unit/cloud/test_delete_server.py +++ b/openstack/tests/unit/cloud/test_delete_server.py @@ -23,7 +23,7 @@ from openstack.tests.unit import base -class TestDeleteServer(base.RequestsMockTestCase): +class TestDeleteServer(base.TestCase): def test_delete_server(self): """ diff --git a/openstack/tests/unit/cloud/test_delete_volume_snapshot.py b/openstack/tests/unit/cloud/test_delete_volume_snapshot.py index 92783f205..f3504d6f9 100644 --- a/openstack/tests/unit/cloud/test_delete_volume_snapshot.py +++ b/openstack/tests/unit/cloud/test_delete_volume_snapshot.py @@ -23,7 +23,7 @@ from openstack.tests.unit import base -class TestDeleteVolumeSnapshot(base.RequestsMockTestCase): +class TestDeleteVolumeSnapshot(base.TestCase): def test_delete_volume_snapshot(self): """ diff --git a/openstack/tests/unit/cloud/test_domains.py b/openstack/tests/unit/cloud/test_domains.py index dcbf69e3e..9ad408a7e 100644 --- a/openstack/tests/unit/cloud/test_domains.py +++ b/openstack/tests/unit/cloud/test_domains.py @@ -22,7 +22,7 @@ from openstack.tests.unit import base -class TestDomains(base.RequestsMockTestCase): +class TestDomains(base.TestCase): def get_mock_url(self, service_type='identity', interface='admin', resource='domains', diff --git a/openstack/tests/unit/cloud/test_endpoints.py b/openstack/tests/unit/cloud/test_endpoints.py index 58d9fe709..74d3c5cb7 100644 --- a/openstack/tests/unit/cloud/test_endpoints.py +++ b/openstack/tests/unit/cloud/test_endpoints.py @@ -27,7 +27,7 @@ from testtools import matchers -class TestCloudEndpoints(base.RequestsMockTestCase): +class TestCloudEndpoints(base.TestCase): def get_mock_url(self, service_type='identity', interface='admin', resource='endpoints', append=None, base_url_append='v3'): diff --git a/openstack/tests/unit/cloud/test_flavors.py b/openstack/tests/unit/cloud/test_flavors.py index 0fa5ae877..72a76bb33 100644 --- a/openstack/tests/unit/cloud/test_flavors.py +++ b/openstack/tests/unit/cloud/test_flavors.py @@ -16,7 +16,7 @@ from openstack.tests.unit import base -class TestFlavors(base.RequestsMockTestCase): +class TestFlavors(base.TestCase): def test_create_flavor(self): diff --git a/openstack/tests/unit/cloud/test_floating_ip_neutron.py b/openstack/tests/unit/cloud/test_floating_ip_neutron.py index 08fb752cd..892ce3ae1 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_neutron.py +++ b/openstack/tests/unit/cloud/test_floating_ip_neutron.py @@ -28,7 +28,7 @@ from openstack.tests.unit import base -class TestFloatingIP(base.RequestsMockTestCase): +class TestFloatingIP(base.TestCase): mock_floating_ip_list_rep = { 'floatingips': [ { diff --git a/openstack/tests/unit/cloud/test_floating_ip_nova.py b/openstack/tests/unit/cloud/test_floating_ip_nova.py index dc6261851..1ca9a68a3 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_nova.py +++ b/openstack/tests/unit/cloud/test_floating_ip_nova.py @@ -31,7 +31,7 @@ def fake_has_service(s): return fake_has_service -class TestFloatingIP(base.RequestsMockTestCase): +class TestFloatingIP(base.TestCase): mock_floating_ip_list_rep = [ { 'fixed_ip': None, diff --git a/openstack/tests/unit/cloud/test_floating_ip_pool.py b/openstack/tests/unit/cloud/test_floating_ip_pool.py index 37a27f812..42e23ed4d 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_pool.py +++ b/openstack/tests/unit/cloud/test_floating_ip_pool.py @@ -24,7 +24,7 @@ from openstack.tests import fakes -class TestFloatingIPPool(base.RequestsMockTestCase): +class TestFloatingIPPool(base.TestCase): pools = [{'name': u'public'}] def test_list_floating_ip_pools(self): diff --git a/openstack/tests/unit/cloud/test_groups.py b/openstack/tests/unit/cloud/test_groups.py index c498336b1..a5f6c336e 100644 --- a/openstack/tests/unit/cloud/test_groups.py +++ b/openstack/tests/unit/cloud/test_groups.py @@ -14,7 +14,7 @@ from openstack.tests.unit import base -class TestGroups(base.RequestsMockTestCase): +class TestGroups(base.TestCase): def setUp(self, cloud_config_fixture='clouds.yaml'): super(TestGroups, self).setUp( cloud_config_fixture=cloud_config_fixture) diff --git a/openstack/tests/unit/cloud/test_identity_roles.py b/openstack/tests/unit/cloud/test_identity_roles.py index 211a3488a..3409e7b63 100644 --- a/openstack/tests/unit/cloud/test_identity_roles.py +++ b/openstack/tests/unit/cloud/test_identity_roles.py @@ -34,7 +34,7 @@ ] -class TestIdentityRoles(base.RequestsMockTestCase): +class TestIdentityRoles(base.TestCase): def get_mock_url(self, service_type='identity', interface='admin', resource='roles', append=None, base_url_append='v3', diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 11a95deb1..27868b01d 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -34,7 +34,7 @@ CINDER_URL = 'https://volume.example.com/v2/1c36b64c840a42cd9e9b931a369337f0' -class BaseTestImage(base.RequestsMockTestCase): +class BaseTestImage(base.TestCase): def setUp(self): super(BaseTestImage, self).setUp() @@ -929,7 +929,7 @@ def test_list_images_paginated(self): self.assert_calls() -class TestImageV1Only(base.RequestsMockTestCase): +class TestImageV1Only(base.TestCase): def setUp(self): super(TestImageV1Only, self).setUp() @@ -955,7 +955,7 @@ def test_config_v2(self): self.assertFalse(self.cloud._is_client_version('image', 2)) -class TestImageV2Only(base.RequestsMockTestCase): +class TestImageV2Only(base.TestCase): def setUp(self): super(TestImageV2Only, self).setUp() @@ -1049,7 +1049,7 @@ def test_create_image_volume_duplicate(self): self.assert_calls() -class TestImageBrokenDiscovery(base.RequestsMockTestCase): +class TestImageBrokenDiscovery(base.TestCase): def setUp(self): super(TestImageBrokenDiscovery, self).setUp() diff --git a/openstack/tests/unit/cloud/test_image_snapshot.py b/openstack/tests/unit/cloud/test_image_snapshot.py index 3e5f83fdc..f6d4e261e 100644 --- a/openstack/tests/unit/cloud/test_image_snapshot.py +++ b/openstack/tests/unit/cloud/test_image_snapshot.py @@ -19,7 +19,7 @@ from openstack.tests.unit import base -class TestImageSnapshot(base.RequestsMockTestCase): +class TestImageSnapshot(base.TestCase): def setUp(self): super(TestImageSnapshot, self).setUp() diff --git a/openstack/tests/unit/cloud/test_keypair.py b/openstack/tests/unit/cloud/test_keypair.py index 2884e78c8..f7dd72e4d 100644 --- a/openstack/tests/unit/cloud/test_keypair.py +++ b/openstack/tests/unit/cloud/test_keypair.py @@ -17,7 +17,7 @@ from openstack.tests.unit import base -class TestKeypair(base.RequestsMockTestCase): +class TestKeypair(base.TestCase): def setUp(self): super(TestKeypair, self).setUp() diff --git a/openstack/tests/unit/cloud/test_limits.py b/openstack/tests/unit/cloud/test_limits.py index 537c8877a..f731a1ae7 100644 --- a/openstack/tests/unit/cloud/test_limits.py +++ b/openstack/tests/unit/cloud/test_limits.py @@ -13,7 +13,7 @@ from openstack.tests.unit import base -class TestLimits(base.RequestsMockTestCase): +class TestLimits(base.TestCase): def test_get_compute_limits(self): self.register_uris([ diff --git a/openstack/tests/unit/cloud/test_magnum_services.py b/openstack/tests/unit/cloud/test_magnum_services.py index c30f56ccd..a410201a8 100644 --- a/openstack/tests/unit/cloud/test_magnum_services.py +++ b/openstack/tests/unit/cloud/test_magnum_services.py @@ -26,7 +26,7 @@ ) -class TestMagnumServices(base.RequestsMockTestCase): +class TestMagnumServices(base.TestCase): def test_list_magnum_services(self): self.register_uris([dict( diff --git a/openstack/tests/unit/cloud/test_meta.py b/openstack/tests/unit/cloud/test_meta.py index 5610d739e..5bb38f0a3 100644 --- a/openstack/tests/unit/cloud/test_meta.py +++ b/openstack/tests/unit/cloud/test_meta.py @@ -228,7 +228,7 @@ def get_default_network(self): ] -class TestMeta(base.RequestsMockTestCase): +class TestMeta(base.TestCase): def test_find_nova_addresses_key_name(self): # Note 198.51.100.0/24 is TEST-NET-2 from rfc5737 addrs = {'public': [{'addr': '198.51.100.1', 'version': 4}], diff --git a/openstack/tests/unit/cloud/test_network.py b/openstack/tests/unit/cloud/test_network.py index 825ab235a..55bb950a6 100644 --- a/openstack/tests/unit/cloud/test_network.py +++ b/openstack/tests/unit/cloud/test_network.py @@ -18,7 +18,7 @@ from openstack.tests.unit import base -class TestNetwork(base.RequestsMockTestCase): +class TestNetwork(base.TestCase): mock_new_network_rep = { 'provider:physical_network': None, diff --git a/openstack/tests/unit/cloud/test_normalize.py b/openstack/tests/unit/cloud/test_normalize.py index 653927477..7265930b0 100644 --- a/openstack/tests/unit/cloud/test_normalize.py +++ b/openstack/tests/unit/cloud/test_normalize.py @@ -11,6 +11,7 @@ # under the License. import mock +import fixtures from openstack.tests.unit import base @@ -178,9 +179,16 @@ 'vcpus': 8} -# TODO(shade) Convert this to RequestsMockTestCase +# TODO(shade) Convert this to TestCase class TestUtils(base.TestCase): + def setUp(self): + + super(TestUtils, self).setUp() + self.session_fixture = self.useFixture(fixtures.MonkeyPatch( + 'openstack.config.cloud_region.CloudRegion.get_session', + mock.Mock())) + def test_normalize_flavors(self): raw_flavor = RAW_FLAVOR_DICT.copy() expected = { diff --git a/openstack/tests/unit/cloud/test_object.py b/openstack/tests/unit/cloud/test_object.py index bbb879ff4..7bb8506f9 100644 --- a/openstack/tests/unit/cloud/test_object.py +++ b/openstack/tests/unit/cloud/test_object.py @@ -22,7 +22,7 @@ from openstack.tests.unit import base -class BaseTestObject(base.RequestsMockTestCase): +class BaseTestObject(base.TestCase): def setUp(self): super(BaseTestObject, self).setUp() diff --git a/openstack/tests/unit/cloud/test_operator.py b/openstack/tests/unit/cloud/test_operator.py index c1c3ed863..1dd3a07d3 100644 --- a/openstack/tests/unit/cloud/test_operator.py +++ b/openstack/tests/unit/cloud/test_operator.py @@ -19,7 +19,7 @@ from openstack.tests.unit import base -class TestOperatorCloud(base.RequestsMockTestCase): +class TestOperatorCloud(base.TestCase): @mock.patch.object(cloud_region.CloudRegion, 'get_endpoint') def test_get_session_endpoint_provided(self, fake_get_endpoint): diff --git a/openstack/tests/unit/cloud/test_operator_noauth.py b/openstack/tests/unit/cloud/test_operator_noauth.py index ab60dab90..4462bbf71 100644 --- a/openstack/tests/unit/cloud/test_operator_noauth.py +++ b/openstack/tests/unit/cloud/test_operator_noauth.py @@ -16,7 +16,7 @@ from openstack.tests.unit import base -class TestOpenStackCloudOperatorNoAuth(base.RequestsMockTestCase): +class TestOpenStackCloudOperatorNoAuth(base.TestCase): def setUp(self): """Setup Noauth OpenStackCloud tests @@ -25,7 +25,7 @@ def setUp(self): mechanism that enables Ironic noauth mode to be utilized with Shade. - Uses base.RequestsMockTestCase instead of IronicTestCase because + Uses base.TestCase instead of IronicTestCase because we need to do completely different things with discovery. """ super(TestOpenStackCloudOperatorNoAuth, self).setUp() diff --git a/openstack/tests/unit/cloud/test_port.py b/openstack/tests/unit/cloud/test_port.py index 6cc953240..962b516c8 100644 --- a/openstack/tests/unit/cloud/test_port.py +++ b/openstack/tests/unit/cloud/test_port.py @@ -23,7 +23,7 @@ from openstack.tests.unit import base -class TestPort(base.RequestsMockTestCase): +class TestPort(base.TestCase): mock_neutron_port_create_rep = { 'port': { 'status': 'DOWN', diff --git a/openstack/tests/unit/cloud/test_project.py b/openstack/tests/unit/cloud/test_project.py index a94b42322..4bb337af0 100644 --- a/openstack/tests/unit/cloud/test_project.py +++ b/openstack/tests/unit/cloud/test_project.py @@ -18,7 +18,7 @@ from openstack.tests.unit import base -class TestProject(base.RequestsMockTestCase): +class TestProject(base.TestCase): def get_mock_url(self, service_type='identity', interface='admin', resource=None, append=None, base_url_append=None, diff --git a/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py b/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py index 1e5955a39..3ea365bb7 100644 --- a/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py @@ -19,7 +19,7 @@ from openstack.tests.unit import base -class TestQosBandwidthLimitRule(base.RequestsMockTestCase): +class TestQosBandwidthLimitRule(base.TestCase): policy_name = 'qos test policy' policy_id = '881d1bb7-a663-44c0-8f9f-ee2765b74486' diff --git a/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py b/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py index b8a2158a1..6ca1783f9 100644 --- a/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py +++ b/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py @@ -19,7 +19,7 @@ from openstack.tests.unit import base -class TestQosDscpMarkingRule(base.RequestsMockTestCase): +class TestQosDscpMarkingRule(base.TestCase): policy_name = 'qos test policy' policy_id = '881d1bb7-a663-44c0-8f9f-ee2765b74486' diff --git a/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py b/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py index 71de9e922..4e6f54847 100644 --- a/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py +++ b/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py @@ -19,7 +19,7 @@ from openstack.tests.unit import base -class TestQosMinimumBandwidthRule(base.RequestsMockTestCase): +class TestQosMinimumBandwidthRule(base.TestCase): policy_name = 'qos test policy' policy_id = '881d1bb7-a663-44c0-8f9f-ee2765b74486' diff --git a/openstack/tests/unit/cloud/test_qos_policy.py b/openstack/tests/unit/cloud/test_qos_policy.py index 38dbd27fa..89c454788 100644 --- a/openstack/tests/unit/cloud/test_qos_policy.py +++ b/openstack/tests/unit/cloud/test_qos_policy.py @@ -19,7 +19,7 @@ from openstack.tests.unit import base -class TestQosPolicy(base.RequestsMockTestCase): +class TestQosPolicy(base.TestCase): policy_name = 'qos test policy' policy_id = '881d1bb7-a663-44c0-8f9f-ee2765b74486' diff --git a/openstack/tests/unit/cloud/test_qos_rule_type.py b/openstack/tests/unit/cloud/test_qos_rule_type.py index d52c6a1ec..1c4ab3589 100644 --- a/openstack/tests/unit/cloud/test_qos_rule_type.py +++ b/openstack/tests/unit/cloud/test_qos_rule_type.py @@ -17,7 +17,7 @@ from openstack.tests.unit import base -class TestQosRuleType(base.RequestsMockTestCase): +class TestQosRuleType(base.TestCase): rule_type_name = "bandwidth_limit" diff --git a/openstack/tests/unit/cloud/test_quotas.py b/openstack/tests/unit/cloud/test_quotas.py index 91870b523..ade487987 100644 --- a/openstack/tests/unit/cloud/test_quotas.py +++ b/openstack/tests/unit/cloud/test_quotas.py @@ -31,7 +31,7 @@ } -class TestQuotas(base.RequestsMockTestCase): +class TestQuotas(base.TestCase): def setUp(self, cloud_config_fixture='clouds.yaml'): super(TestQuotas, self).setUp( cloud_config_fixture=cloud_config_fixture) diff --git a/openstack/tests/unit/cloud/test_rebuild_server.py b/openstack/tests/unit/cloud/test_rebuild_server.py index 764639467..a72723be9 100644 --- a/openstack/tests/unit/cloud/test_rebuild_server.py +++ b/openstack/tests/unit/cloud/test_rebuild_server.py @@ -26,7 +26,7 @@ from openstack.tests.unit import base -class TestRebuildServer(base.RequestsMockTestCase): +class TestRebuildServer(base.TestCase): def setUp(self): super(TestRebuildServer, self).setUp() diff --git a/openstack/tests/unit/cloud/test_recordset.py b/openstack/tests/unit/cloud/test_recordset.py index 26b27d47d..c8f911032 100644 --- a/openstack/tests/unit/cloud/test_recordset.py +++ b/openstack/tests/unit/cloud/test_recordset.py @@ -41,7 +41,7 @@ new_recordset['zone'] = recordset_zone -class TestRecordset(base.RequestsMockTestCase): +class TestRecordset(base.TestCase): def setUp(self): super(TestRecordset, self).setUp() diff --git a/openstack/tests/unit/cloud/test_role_assignment.py b/openstack/tests/unit/cloud/test_role_assignment.py index c6a1b73ed..c9ad2dbb2 100644 --- a/openstack/tests/unit/cloud/test_role_assignment.py +++ b/openstack/tests/unit/cloud/test_role_assignment.py @@ -17,7 +17,7 @@ from testtools import matchers -class TestRoleAssignment(base.RequestsMockTestCase): +class TestRoleAssignment(base.TestCase): def _build_role_assignment_response(self, role_id, scope_type, scope_id, entity_type, entity_id): diff --git a/openstack/tests/unit/cloud/test_router.py b/openstack/tests/unit/cloud/test_router.py index 48747d925..680b0e422 100644 --- a/openstack/tests/unit/cloud/test_router.py +++ b/openstack/tests/unit/cloud/test_router.py @@ -20,7 +20,7 @@ from openstack.tests.unit import base -class TestRouter(base.RequestsMockTestCase): +class TestRouter(base.TestCase): router_name = 'goofy' router_id = '57076620-dcfb-42ed-8ad6-79ccb4a79ed2' diff --git a/openstack/tests/unit/cloud/test_security_groups.py b/openstack/tests/unit/cloud/test_security_groups.py index 1960373b9..d65f5ffac 100644 --- a/openstack/tests/unit/cloud/test_security_groups.py +++ b/openstack/tests/unit/cloud/test_security_groups.py @@ -42,7 +42,7 @@ ) -class TestSecurityGroups(base.RequestsMockTestCase): +class TestSecurityGroups(base.TestCase): def setUp(self): super(TestSecurityGroups, self).setUp() diff --git a/openstack/tests/unit/cloud/test_server_console.py b/openstack/tests/unit/cloud/test_server_console.py index 7b7cd2b65..6ff8476d1 100644 --- a/openstack/tests/unit/cloud/test_server_console.py +++ b/openstack/tests/unit/cloud/test_server_console.py @@ -17,7 +17,7 @@ from openstack.tests import fakes -class TestServerConsole(base.RequestsMockTestCase): +class TestServerConsole(base.TestCase): def setUp(self): super(TestServerConsole, self).setUp() diff --git a/openstack/tests/unit/cloud/test_server_delete_metadata.py b/openstack/tests/unit/cloud/test_server_delete_metadata.py index d6c4cebd9..ab1d1ea52 100644 --- a/openstack/tests/unit/cloud/test_server_delete_metadata.py +++ b/openstack/tests/unit/cloud/test_server_delete_metadata.py @@ -24,7 +24,7 @@ from openstack.tests.unit import base -class TestServerDeleteMetadata(base.RequestsMockTestCase): +class TestServerDeleteMetadata(base.TestCase): def setUp(self): super(TestServerDeleteMetadata, self).setUp() diff --git a/openstack/tests/unit/cloud/test_server_group.py b/openstack/tests/unit/cloud/test_server_group.py index 3bef1c639..88b14e2b4 100644 --- a/openstack/tests/unit/cloud/test_server_group.py +++ b/openstack/tests/unit/cloud/test_server_group.py @@ -17,7 +17,7 @@ from openstack.tests import fakes -class TestServerGroup(base.RequestsMockTestCase): +class TestServerGroup(base.TestCase): def setUp(self): super(TestServerGroup, self).setUp() diff --git a/openstack/tests/unit/cloud/test_server_set_metadata.py b/openstack/tests/unit/cloud/test_server_set_metadata.py index f0e21d2a1..b777ea66d 100644 --- a/openstack/tests/unit/cloud/test_server_set_metadata.py +++ b/openstack/tests/unit/cloud/test_server_set_metadata.py @@ -24,7 +24,7 @@ from openstack.tests.unit import base -class TestServerSetMetadata(base.RequestsMockTestCase): +class TestServerSetMetadata(base.TestCase): def setUp(self): super(TestServerSetMetadata, self).setUp() diff --git a/openstack/tests/unit/cloud/test_services.py b/openstack/tests/unit/cloud/test_services.py index 99a18f9d6..c120aaa8a 100644 --- a/openstack/tests/unit/cloud/test_services.py +++ b/openstack/tests/unit/cloud/test_services.py @@ -25,7 +25,7 @@ from testtools import matchers -class CloudServices(base.RequestsMockTestCase): +class CloudServices(base.TestCase): def setUp(self, cloud_config_fixture='clouds.yaml'): super(CloudServices, self).setUp(cloud_config_fixture) diff --git a/openstack/tests/unit/cloud/test_shade.py b/openstack/tests/unit/cloud/test_shade.py index 1c6932d3c..b0e2309f7 100644 --- a/openstack/tests/unit/cloud/test_shade.py +++ b/openstack/tests/unit/cloud/test_shade.py @@ -32,7 +32,7 @@ ] -class TestShade(base.RequestsMockTestCase): +class TestShade(base.TestCase): def setUp(self): # This set of tests are not testing neutron, they're testing diff --git a/openstack/tests/unit/cloud/test_shade_operator.py b/openstack/tests/unit/cloud/test_shade_operator.py index a31bb57e7..1939d2daa 100644 --- a/openstack/tests/unit/cloud/test_shade_operator.py +++ b/openstack/tests/unit/cloud/test_shade_operator.py @@ -16,5 +16,5 @@ from openstack.tests.unit import base -class TestShadeOperator(base.RequestsMockTestCase): +class TestShadeOperator(base.TestCase): pass diff --git a/openstack/tests/unit/cloud/test_stack.py b/openstack/tests/unit/cloud/test_stack.py index 448a09857..049db96f5 100644 --- a/openstack/tests/unit/cloud/test_stack.py +++ b/openstack/tests/unit/cloud/test_stack.py @@ -20,7 +20,7 @@ from openstack.tests.unit import base -class TestStack(base.RequestsMockTestCase): +class TestStack(base.TestCase): def setUp(self): super(TestStack, self).setUp() diff --git a/openstack/tests/unit/cloud/test_subnet.py b/openstack/tests/unit/cloud/test_subnet.py index 83551513c..e8f05657b 100644 --- a/openstack/tests/unit/cloud/test_subnet.py +++ b/openstack/tests/unit/cloud/test_subnet.py @@ -20,7 +20,7 @@ from openstack.tests.unit import base -class TestSubnet(base.RequestsMockTestCase): +class TestSubnet(base.TestCase): network_name = 'network_name' subnet_name = 'subnet_name' diff --git a/openstack/tests/unit/cloud/test_update_server.py b/openstack/tests/unit/cloud/test_update_server.py index 674525490..252da3ce7 100644 --- a/openstack/tests/unit/cloud/test_update_server.py +++ b/openstack/tests/unit/cloud/test_update_server.py @@ -24,7 +24,7 @@ from openstack.tests.unit import base -class TestUpdateServer(base.RequestsMockTestCase): +class TestUpdateServer(base.TestCase): def setUp(self): super(TestUpdateServer, self).setUp() diff --git a/openstack/tests/unit/cloud/test_usage.py b/openstack/tests/unit/cloud/test_usage.py index 3e47602b5..0aedc4c0e 100644 --- a/openstack/tests/unit/cloud/test_usage.py +++ b/openstack/tests/unit/cloud/test_usage.py @@ -17,7 +17,7 @@ from openstack.tests.unit import base -class TestUsage(base.RequestsMockTestCase): +class TestUsage(base.TestCase): def test_get_usage(self): project = self.mock_for_keystone_projects(project_count=1, diff --git a/openstack/tests/unit/cloud/test_users.py b/openstack/tests/unit/cloud/test_users.py index 0c155703e..c3cd40cb5 100644 --- a/openstack/tests/unit/cloud/test_users.py +++ b/openstack/tests/unit/cloud/test_users.py @@ -18,7 +18,7 @@ from openstack.tests.unit import base -class TestUsers(base.RequestsMockTestCase): +class TestUsers(base.TestCase): def _get_keystone_mock_url(self, resource, append=None, v3=True): base_url_append = None diff --git a/openstack/tests/unit/cloud/test_volume.py b/openstack/tests/unit/cloud/test_volume.py index 9c1e2380e..72ce1a095 100644 --- a/openstack/tests/unit/cloud/test_volume.py +++ b/openstack/tests/unit/cloud/test_volume.py @@ -19,7 +19,7 @@ from openstack.tests.unit import base -class TestVolume(base.RequestsMockTestCase): +class TestVolume(base.TestCase): def test_attach_volume(self): server = dict(id='server001') diff --git a/openstack/tests/unit/cloud/test_volume_access.py b/openstack/tests/unit/cloud/test_volume_access.py index ffdaa2db4..02ecc4054 100644 --- a/openstack/tests/unit/cloud/test_volume_access.py +++ b/openstack/tests/unit/cloud/test_volume_access.py @@ -19,7 +19,7 @@ from openstack.tests.unit import base -class TestVolumeAccess(base.RequestsMockTestCase): +class TestVolumeAccess(base.TestCase): def test_list_volume_types(self): volume_type = dict( id='voltype01', description='volume type description', diff --git a/openstack/tests/unit/cloud/test_volume_backups.py b/openstack/tests/unit/cloud/test_volume_backups.py index 6b6392f15..49479d31e 100644 --- a/openstack/tests/unit/cloud/test_volume_backups.py +++ b/openstack/tests/unit/cloud/test_volume_backups.py @@ -13,7 +13,7 @@ from openstack.tests.unit import base -class TestVolumeBackups(base.RequestsMockTestCase): +class TestVolumeBackups(base.TestCase): def test_search_volume_backups(self): name = 'Volume1' vol1 = {'name': name, 'availability_zone': 'az1'} diff --git a/openstack/tests/unit/cloud/test_zone.py b/openstack/tests/unit/cloud/test_zone.py index 958d3faaa..193cc66d9 100644 --- a/openstack/tests/unit/cloud/test_zone.py +++ b/openstack/tests/unit/cloud/test_zone.py @@ -29,7 +29,7 @@ new_zone_dict['id'] = '1' -class TestZone(base.RequestsMockTestCase): +class TestZone(base.TestCase): def setUp(self): super(TestZone, self).setUp() diff --git a/openstack/tests/unit/clustering/test_cluster_service.py b/openstack/tests/unit/clustering/test_cluster_service.py index 4c828bd66..f52cafdaf 100644 --- a/openstack/tests/unit/clustering/test_cluster_service.py +++ b/openstack/tests/unit/clustering/test_cluster_service.py @@ -10,12 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.clustering import clustering_service -class TestClusteringService(testtools.TestCase): +class TestClusteringService(base.TestCase): def test_service(self): sot = clustering_service.ClusteringService() diff --git a/openstack/tests/unit/clustering/test_version.py b/openstack/tests/unit/clustering/test_version.py index 30646a5c7..60d7b0c3f 100644 --- a/openstack/tests/unit/clustering/test_version.py +++ b/openstack/tests/unit/clustering/test_version.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.clustering import version @@ -22,7 +22,7 @@ } -class TestVersion(testtools.TestCase): +class TestVersion(base.TestCase): def test_basic(self): sot = version.Version() diff --git a/openstack/tests/unit/clustering/v1/test_action.py b/openstack/tests/unit/clustering/v1/test_action.py index b12eaac98..a4d462b95 100644 --- a/openstack/tests/unit/clustering/v1/test_action.py +++ b/openstack/tests/unit/clustering/v1/test_action.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.clustering.v1 import action @@ -43,7 +43,7 @@ } -class TestAction(testtools.TestCase): +class TestAction(base.TestCase): def setUp(self): super(TestAction, self).setUp() diff --git a/openstack/tests/unit/clustering/v1/test_build_info.py b/openstack/tests/unit/clustering/v1/test_build_info.py index 080d0b15b..8d209d603 100644 --- a/openstack/tests/unit/clustering/v1/test_build_info.py +++ b/openstack/tests/unit/clustering/v1/test_build_info.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.clustering.v1 import build_info @@ -25,7 +25,7 @@ } -class TestBuildInfo(testtools.TestCase): +class TestBuildInfo(base.TestCase): def setUp(self): super(TestBuildInfo, self).setUp() diff --git a/openstack/tests/unit/clustering/v1/test_cluster.py b/openstack/tests/unit/clustering/v1/test_cluster.py index 4c4cf55b5..52ee8f8c4 100644 --- a/openstack/tests/unit/clustering/v1/test_cluster.py +++ b/openstack/tests/unit/clustering/v1/test_cluster.py @@ -11,7 +11,7 @@ # under the License. import mock -import testtools +from openstack.tests.unit import base from openstack.clustering.v1 import cluster @@ -65,7 +65,7 @@ } -class TestCluster(testtools.TestCase): +class TestCluster(base.TestCase): def setUp(self): super(TestCluster, self).setUp() diff --git a/openstack/tests/unit/clustering/v1/test_cluster_attr.py b/openstack/tests/unit/clustering/v1/test_cluster_attr.py index 3d7181682..e64cdcb9e 100644 --- a/openstack/tests/unit/clustering/v1/test_cluster_attr.py +++ b/openstack/tests/unit/clustering/v1/test_cluster_attr.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.clustering.v1 import cluster_attr as ca @@ -23,7 +23,7 @@ } -class TestClusterAttr(testtools.TestCase): +class TestClusterAttr(base.TestCase): def setUp(self): super(TestClusterAttr, self).setUp() diff --git a/openstack/tests/unit/clustering/v1/test_cluster_policy.py b/openstack/tests/unit/clustering/v1/test_cluster_policy.py index e84990e12..5ce71ae3e 100644 --- a/openstack/tests/unit/clustering/v1/test_cluster_policy.py +++ b/openstack/tests/unit/clustering/v1/test_cluster_policy.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.clustering.v1 import cluster_policy @@ -26,7 +26,7 @@ } -class TestClusterPolicy(testtools.TestCase): +class TestClusterPolicy(base.TestCase): def setUp(self): super(TestClusterPolicy, self).setUp() diff --git a/openstack/tests/unit/clustering/v1/test_event.py b/openstack/tests/unit/clustering/v1/test_event.py index 82a76593a..47c80c7d2 100644 --- a/openstack/tests/unit/clustering/v1/test_event.py +++ b/openstack/tests/unit/clustering/v1/test_event.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.clustering.v1 import event @@ -31,7 +31,7 @@ } -class TestEvent(testtools.TestCase): +class TestEvent(base.TestCase): def setUp(self): super(TestEvent, self).setUp() diff --git a/openstack/tests/unit/clustering/v1/test_node.py b/openstack/tests/unit/clustering/v1/test_node.py index 9e6cbf7aa..65868b728 100644 --- a/openstack/tests/unit/clustering/v1/test_node.py +++ b/openstack/tests/unit/clustering/v1/test_node.py @@ -11,7 +11,7 @@ # under the License. import mock -import testtools +from openstack.tests.unit import base from openstack.clustering.v1 import node @@ -37,7 +37,7 @@ } -class TestNode(testtools.TestCase): +class TestNode(base.TestCase): def test_basic(self): sot = node.Node() @@ -156,7 +156,7 @@ def test_force_delete(self): sess.delete.assert_called_once_with(url, json=body) -class TestNodeDetail(testtools.TestCase): +class TestNodeDetail(base.TestCase): def test_basic(self): sot = node.NodeDetail() diff --git a/openstack/tests/unit/clustering/v1/test_policy.py b/openstack/tests/unit/clustering/v1/test_policy.py index a238c08cf..137e6214b 100644 --- a/openstack/tests/unit/clustering/v1/test_policy.py +++ b/openstack/tests/unit/clustering/v1/test_policy.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.clustering.v1 import policy @@ -41,7 +41,7 @@ } -class TestPolicy(testtools.TestCase): +class TestPolicy(base.TestCase): def setUp(self): super(TestPolicy, self).setUp() @@ -71,7 +71,7 @@ def test_instantiate(self): self.assertEqual(FAKE['updated_at'], sot.updated_at) -class TestPolicyValidate(testtools.TestCase): +class TestPolicyValidate(base.TestCase): def setUp(self): super(TestPolicyValidate, self).setUp() diff --git a/openstack/tests/unit/clustering/v1/test_policy_type.py b/openstack/tests/unit/clustering/v1/test_policy_type.py index 7ce8f0507..defdd6944 100644 --- a/openstack/tests/unit/clustering/v1/test_policy_type.py +++ b/openstack/tests/unit/clustering/v1/test_policy_type.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.clustering.v1 import policy_type @@ -29,7 +29,7 @@ } -class TestPolicyType(testtools.TestCase): +class TestPolicyType(base.TestCase): def test_basic(self): sot = policy_type.PolicyType() diff --git a/openstack/tests/unit/clustering/v1/test_profile.py b/openstack/tests/unit/clustering/v1/test_profile.py index 61de6a971..047e5c037 100644 --- a/openstack/tests/unit/clustering/v1/test_profile.py +++ b/openstack/tests/unit/clustering/v1/test_profile.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.clustering.v1 import profile @@ -41,7 +41,7 @@ } -class TestProfile(testtools.TestCase): +class TestProfile(base.TestCase): def setUp(self): super(TestProfile, self).setUp() @@ -73,7 +73,7 @@ def test_instantiate(self): self.assertEqual(FAKE['updated_at'], sot.updated_at) -class TestProfileValidate(testtools.TestCase): +class TestProfileValidate(base.TestCase): def setUp(self): super(TestProfileValidate, self).setUp() diff --git a/openstack/tests/unit/clustering/v1/test_profile_type.py b/openstack/tests/unit/clustering/v1/test_profile_type.py index 88816e2c6..f3c7f91a8 100644 --- a/openstack/tests/unit/clustering/v1/test_profile_type.py +++ b/openstack/tests/unit/clustering/v1/test_profile_type.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.clustering.v1 import profile_type @@ -29,7 +29,7 @@ } -class TestProfileType(testtools.TestCase): +class TestProfileType(base.TestCase): def test_basic(self): sot = profile_type.ProfileType() diff --git a/openstack/tests/unit/clustering/v1/test_receiver.py b/openstack/tests/unit/clustering/v1/test_receiver.py index 39875b4ab..ad6fedf8a 100644 --- a/openstack/tests/unit/clustering/v1/test_receiver.py +++ b/openstack/tests/unit/clustering/v1/test_receiver.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.clustering.v1 import receiver @@ -40,7 +40,7 @@ } -class TestReceiver(testtools.TestCase): +class TestReceiver(base.TestCase): def setUp(self): super(TestReceiver, self).setUp() diff --git a/openstack/tests/unit/clustering/v1/test_service.py b/openstack/tests/unit/clustering/v1/test_service.py index cc94f42b4..a97fa678d 100644 --- a/openstack/tests/unit/clustering/v1/test_service.py +++ b/openstack/tests/unit/clustering/v1/test_service.py @@ -11,7 +11,7 @@ # under the License. import mock -import testtools +from openstack.tests.unit import base from openstack.clustering.v1 import service @@ -26,7 +26,7 @@ } -class TestService(testtools.TestCase): +class TestService(base.TestCase): def setUp(self): super(TestService, self).setUp() diff --git a/openstack/tests/unit/compute/test_compute_service.py b/openstack/tests/unit/compute/test_compute_service.py index 3c5b26c8f..3a3ed7bbe 100644 --- a/openstack/tests/unit/compute/test_compute_service.py +++ b/openstack/tests/unit/compute/test_compute_service.py @@ -10,12 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.compute import compute_service -class TestComputeService(testtools.TestCase): +class TestComputeService(base.TestCase): def test_service(self): sot = compute_service.ComputeService() diff --git a/openstack/tests/unit/compute/test_version.py b/openstack/tests/unit/compute/test_version.py index 0940b2cf4..45bf2d69a 100644 --- a/openstack/tests/unit/compute/test_version.py +++ b/openstack/tests/unit/compute/test_version.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.compute import version @@ -23,7 +23,7 @@ } -class TestVersion(testtools.TestCase): +class TestVersion(base.TestCase): def test_basic(self): sot = version.Version() diff --git a/openstack/tests/unit/compute/v2/test_availability_zone.py b/openstack/tests/unit/compute/v2/test_availability_zone.py index 4d4abe099..66489eb58 100644 --- a/openstack/tests/unit/compute/v2/test_availability_zone.py +++ b/openstack/tests/unit/compute/v2/test_availability_zone.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.compute.v2 import availability_zone as az @@ -23,7 +23,7 @@ } -class TestAvailabilityZone(testtools.TestCase): +class TestAvailabilityZone(base.TestCase): def test_basic(self): sot = az.AvailabilityZone() diff --git a/openstack/tests/unit/compute/v2/test_extension.py b/openstack/tests/unit/compute/v2/test_extension.py index 8d59084b2..86bfa96ec 100644 --- a/openstack/tests/unit/compute/v2/test_extension.py +++ b/openstack/tests/unit/compute/v2/test_extension.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.compute.v2 import extension @@ -25,7 +25,7 @@ } -class TestExtension(testtools.TestCase): +class TestExtension(base.TestCase): def test_basic(self): sot = extension.Extension() diff --git a/openstack/tests/unit/compute/v2/test_flavor.py b/openstack/tests/unit/compute/v2/test_flavor.py index dba4a4aaa..d4c154724 100644 --- a/openstack/tests/unit/compute/v2/test_flavor.py +++ b/openstack/tests/unit/compute/v2/test_flavor.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.compute.v2 import flavor @@ -30,7 +30,7 @@ } -class TestFlavor(testtools.TestCase): +class TestFlavor(base.TestCase): def test_basic(self): sot = flavor.Flavor() diff --git a/openstack/tests/unit/compute/v2/test_hypervisor.py b/openstack/tests/unit/compute/v2/test_hypervisor.py index 04829c081..e6bccba67 100644 --- a/openstack/tests/unit/compute/v2/test_hypervisor.py +++ b/openstack/tests/unit/compute/v2/test_hypervisor.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.compute.v2 import hypervisor @@ -42,7 +42,7 @@ } -class TestHypervisor(testtools.TestCase): +class TestHypervisor(base.TestCase): def test_basic(self): sot = hypervisor.Hypervisor() diff --git a/openstack/tests/unit/compute/v2/test_image.py b/openstack/tests/unit/compute/v2/test_image.py index 8e848a238..158beba41 100644 --- a/openstack/tests/unit/compute/v2/test_image.py +++ b/openstack/tests/unit/compute/v2/test_image.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.compute.v2 import image @@ -36,7 +36,7 @@ DETAIL_EXAMPLE.update(DETAILS) -class TestImage(testtools.TestCase): +class TestImage(base.TestCase): def test_basic(self): sot = image.Image() diff --git a/openstack/tests/unit/compute/v2/test_keypair.py b/openstack/tests/unit/compute/v2/test_keypair.py index 3fb3dbcbc..54efd3871 100644 --- a/openstack/tests/unit/compute/v2/test_keypair.py +++ b/openstack/tests/unit/compute/v2/test_keypair.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.compute.v2 import keypair @@ -22,7 +22,7 @@ } -class TestKeypair(testtools.TestCase): +class TestKeypair(base.TestCase): def test_basic(self): sot = keypair.Keypair() diff --git a/openstack/tests/unit/compute/v2/test_limits.py b/openstack/tests/unit/compute/v2/test_limits.py index a5cd5d66d..81ded5765 100644 --- a/openstack/tests/unit/compute/v2/test_limits.py +++ b/openstack/tests/unit/compute/v2/test_limits.py @@ -14,7 +14,7 @@ from keystoneauth1 import adapter import mock -import testtools +from openstack.tests.unit import base from openstack.compute.v2 import limits @@ -62,7 +62,7 @@ } -class TestAbsoluteLimits(testtools.TestCase): +class TestAbsoluteLimits(base.TestCase): def test_basic(self): sot = limits.AbsoluteLimits() @@ -112,7 +112,7 @@ def test_make_it(self): sot.total_cores_used) -class TestRateLimit(testtools.TestCase): +class TestRateLimit(base.TestCase): def test_basic(self): sot = limits.RateLimit() @@ -133,7 +133,7 @@ def test_make_it(self): self.assertEqual(RATE_LIMIT["limit"], sot.limits) -class TestLimits(testtools.TestCase): +class TestLimits(base.TestCase): def test_basic(self): sot = limits.Limits() diff --git a/openstack/tests/unit/compute/v2/test_metadata.py b/openstack/tests/unit/compute/v2/test_metadata.py index 8f2c4da64..6924b93e9 100644 --- a/openstack/tests/unit/compute/v2/test_metadata.py +++ b/openstack/tests/unit/compute/v2/test_metadata.py @@ -11,7 +11,7 @@ # under the License. import mock -import testtools +from openstack.tests.unit import base from openstack.compute.v2 import server @@ -24,7 +24,7 @@ # working. -class TestMetadata(testtools.TestCase): +class TestMetadata(base.TestCase): def setUp(self): super(TestMetadata, self).setUp() diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 601ec5f62..3de08fc04 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -11,7 +11,7 @@ # under the License. import mock -import testtools +from openstack.tests.unit import base from openstack.compute.v2 import server @@ -57,7 +57,7 @@ } -class TestServer(testtools.TestCase): +class TestServer(base.TestCase): def setUp(self): super(TestServer, self).setUp() diff --git a/openstack/tests/unit/compute/v2/test_server_group.py b/openstack/tests/unit/compute/v2/test_server_group.py index 3fabbfc12..224c5efca 100644 --- a/openstack/tests/unit/compute/v2/test_server_group.py +++ b/openstack/tests/unit/compute/v2/test_server_group.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.compute.v2 import server_group @@ -23,7 +23,7 @@ } -class TestServerGroup(testtools.TestCase): +class TestServerGroup(base.TestCase): def test_basic(self): sot = server_group.ServerGroup() diff --git a/openstack/tests/unit/compute/v2/test_server_interface.py b/openstack/tests/unit/compute/v2/test_server_interface.py index 64467541a..b0374f5b1 100644 --- a/openstack/tests/unit/compute/v2/test_server_interface.py +++ b/openstack/tests/unit/compute/v2/test_server_interface.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.compute.v2 import server_interface @@ -30,7 +30,7 @@ } -class TestServerInterface(testtools.TestCase): +class TestServerInterface(base.TestCase): def test_basic(self): sot = server_interface.ServerInterface() diff --git a/openstack/tests/unit/compute/v2/test_server_ip.py b/openstack/tests/unit/compute/v2/test_server_ip.py index 770b67a21..ab3fce4d3 100644 --- a/openstack/tests/unit/compute/v2/test_server_ip.py +++ b/openstack/tests/unit/compute/v2/test_server_ip.py @@ -11,7 +11,7 @@ # under the License. import mock -import testtools +from openstack.tests.unit import base from openstack.compute.v2 import server_ip @@ -23,7 +23,7 @@ } -class TestServerIP(testtools.TestCase): +class TestServerIP(base.TestCase): def test_basic(self): sot = server_ip.ServerIP() diff --git a/openstack/tests/unit/compute/v2/test_service.py b/openstack/tests/unit/compute/v2/test_service.py index 3a1e4ca40..4ef25c812 100644 --- a/openstack/tests/unit/compute/v2/test_service.py +++ b/openstack/tests/unit/compute/v2/test_service.py @@ -11,7 +11,7 @@ # under the License. import mock -import testtools +from openstack.tests.unit import base from openstack.compute.v2 import service @@ -26,7 +26,7 @@ } -class TestService(testtools.TestCase): +class TestService(base.TestCase): def setUp(self): super(TestService, self).setUp() diff --git a/openstack/tests/unit/compute/v2/test_volume_attachment.py b/openstack/tests/unit/compute/v2/test_volume_attachment.py index 87d4cdbd5..30d999dbd 100644 --- a/openstack/tests/unit/compute/v2/test_volume_attachment.py +++ b/openstack/tests/unit/compute/v2/test_volume_attachment.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.compute.v2 import volume_attachment @@ -21,7 +21,7 @@ } -class TestServerInterface(testtools.TestCase): +class TestServerInterface(base.TestCase): def test_basic(self): sot = volume_attachment.VolumeAttachment() diff --git a/openstack/tests/unit/config/base.py b/openstack/tests/unit/config/base.py index cd569a4fd..5e2a72a65 100644 --- a/openstack/tests/unit/config/base.py +++ b/openstack/tests/unit/config/base.py @@ -15,18 +15,16 @@ # License for the specific language governing permissions and limitations # under the License. -# TODO(shade) Shift to using new combined base unit test class import copy import os import tempfile -from openstack.config import cloud_region - import extras import fixtures -from oslotest import base import yaml +from openstack.config import cloud_region +from openstack.tests.unit import base VENDOR_CONF = { 'public-clouds': { @@ -199,13 +197,12 @@ def _write_yaml(obj): return obj_yaml.name -class TestCase(base.BaseTestCase): +class TestCase(base.TestCase): """Test case base class for all unit tests.""" def setUp(self): super(TestCase, self).setUp() - self.useFixture(fixtures.NestedTempfile()) conf = copy.deepcopy(USER_CONF) tdir = self.useFixture(fixtures.TempDir()) conf['cache']['path'] = tdir.path diff --git a/openstack/tests/unit/config/test_from_session.py b/openstack/tests/unit/config/test_from_session.py index 9b2b1fe29..69f2d898c 100644 --- a/openstack/tests/unit/config/test_from_session.py +++ b/openstack/tests/unit/config/test_from_session.py @@ -21,7 +21,7 @@ from openstack.tests.unit import base -class TestFromSession(base.RequestsMockTestCase): +class TestFromSession(base.TestCase): scenarios = [ ('no_region', dict(test_region=None)), diff --git a/openstack/tests/unit/database/test_database_service.py b/openstack/tests/unit/database/test_database_service.py index 6793acd43..85c57d887 100644 --- a/openstack/tests/unit/database/test_database_service.py +++ b/openstack/tests/unit/database/test_database_service.py @@ -10,12 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.database import database_service -class TestDatabaseService(testtools.TestCase): +class TestDatabaseService(base.TestCase): def test_service(self): sot = database_service.DatabaseService() diff --git a/openstack/tests/unit/database/v1/test_database.py b/openstack/tests/unit/database/v1/test_database.py index 6f8cda2ca..d136bcbb3 100644 --- a/openstack/tests/unit/database/v1/test_database.py +++ b/openstack/tests/unit/database/v1/test_database.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.database.v1 import database @@ -25,7 +25,7 @@ } -class TestDatabase(testtools.TestCase): +class TestDatabase(base.TestCase): def test_basic(self): sot = database.Database() diff --git a/openstack/tests/unit/database/v1/test_flavor.py b/openstack/tests/unit/database/v1/test_flavor.py index a851e45d2..bb6b1c83a 100644 --- a/openstack/tests/unit/database/v1/test_flavor.py +++ b/openstack/tests/unit/database/v1/test_flavor.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.database.v1 import flavor @@ -23,7 +23,7 @@ } -class TestFlavor(testtools.TestCase): +class TestFlavor(base.TestCase): def test_basic(self): sot = flavor.Flavor() diff --git a/openstack/tests/unit/database/v1/test_instance.py b/openstack/tests/unit/database/v1/test_instance.py index 116f63b7c..c217ff062 100644 --- a/openstack/tests/unit/database/v1/test_instance.py +++ b/openstack/tests/unit/database/v1/test_instance.py @@ -11,7 +11,7 @@ # under the License. import mock -import testtools +from openstack.tests.unit import base from openstack.database.v1 import instance @@ -31,7 +31,7 @@ } -class TestInstance(testtools.TestCase): +class TestInstance(base.TestCase): def test_basic(self): sot = instance.Instance() diff --git a/openstack/tests/unit/database/v1/test_user.py b/openstack/tests/unit/database/v1/test_user.py index 884a906e9..83a91a8ef 100644 --- a/openstack/tests/unit/database/v1/test_user.py +++ b/openstack/tests/unit/database/v1/test_user.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.database.v1 import user @@ -23,7 +23,7 @@ } -class TestUser(testtools.TestCase): +class TestUser(base.TestCase): def test_basic(self): sot = user.User() diff --git a/openstack/tests/unit/identity/test_identity_service.py b/openstack/tests/unit/identity/test_identity_service.py index dbfaea3d6..47125928b 100644 --- a/openstack/tests/unit/identity/test_identity_service.py +++ b/openstack/tests/unit/identity/test_identity_service.py @@ -10,12 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.identity import identity_service -class TestIdentityService(testtools.TestCase): +class TestIdentityService(base.TestCase): def test_regular_service(self): sot = identity_service.IdentityService() diff --git a/openstack/tests/unit/identity/test_version.py b/openstack/tests/unit/identity/test_version.py index a3693f0da..a53a8b2c1 100644 --- a/openstack/tests/unit/identity/test_version.py +++ b/openstack/tests/unit/identity/test_version.py @@ -11,7 +11,7 @@ # under the License. import mock -import testtools +from openstack.tests.unit import base from openstack.identity import version @@ -24,7 +24,7 @@ } -class TestVersion(testtools.TestCase): +class TestVersion(base.TestCase): def test_basic(self): sot = version.Version() diff --git a/openstack/tests/unit/identity/v2/test_extension.py b/openstack/tests/unit/identity/v2/test_extension.py index 13d0d29b1..d7e768f71 100644 --- a/openstack/tests/unit/identity/v2/test_extension.py +++ b/openstack/tests/unit/identity/v2/test_extension.py @@ -11,7 +11,7 @@ # under the License. import mock -import testtools +from openstack.tests.unit import base from openstack.identity.v2 import extension @@ -26,7 +26,7 @@ } -class TestExtension(testtools.TestCase): +class TestExtension(base.TestCase): def test_basic(self): sot = extension.Extension() diff --git a/openstack/tests/unit/identity/v2/test_role.py b/openstack/tests/unit/identity/v2/test_role.py index e1f67ba36..a1d2e060a 100644 --- a/openstack/tests/unit/identity/v2/test_role.py +++ b/openstack/tests/unit/identity/v2/test_role.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.identity.v2 import role @@ -23,7 +23,7 @@ } -class TestRole(testtools.TestCase): +class TestRole(base.TestCase): def test_basic(self): sot = role.Role() diff --git a/openstack/tests/unit/identity/v2/test_tenant.py b/openstack/tests/unit/identity/v2/test_tenant.py index b123ce40c..a0c63219e 100644 --- a/openstack/tests/unit/identity/v2/test_tenant.py +++ b/openstack/tests/unit/identity/v2/test_tenant.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.identity.v2 import tenant @@ -23,7 +23,7 @@ } -class TestTenant(testtools.TestCase): +class TestTenant(base.TestCase): def test_basic(self): sot = tenant.Tenant() diff --git a/openstack/tests/unit/identity/v2/test_user.py b/openstack/tests/unit/identity/v2/test_user.py index 161b8bade..3fae75902 100644 --- a/openstack/tests/unit/identity/v2/test_user.py +++ b/openstack/tests/unit/identity/v2/test_user.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.identity.v2 import user @@ -23,7 +23,7 @@ } -class TestUser(testtools.TestCase): +class TestUser(base.TestCase): def test_basic(self): sot = user.User() diff --git a/openstack/tests/unit/identity/v3/test_credential.py b/openstack/tests/unit/identity/v3/test_credential.py index eb4c1e216..8fc9676d3 100644 --- a/openstack/tests/unit/identity/v3/test_credential.py +++ b/openstack/tests/unit/identity/v3/test_credential.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.identity.v3 import credential @@ -24,7 +24,7 @@ } -class TestCredential(testtools.TestCase): +class TestCredential(base.TestCase): def test_basic(self): sot = credential.Credential() diff --git a/openstack/tests/unit/identity/v3/test_domain.py b/openstack/tests/unit/identity/v3/test_domain.py index 6e6cb3d00..2d75d9e3c 100644 --- a/openstack/tests/unit/identity/v3/test_domain.py +++ b/openstack/tests/unit/identity/v3/test_domain.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.identity.v3 import domain @@ -24,7 +24,7 @@ } -class TestDomain(testtools.TestCase): +class TestDomain(base.TestCase): def test_basic(self): sot = domain.Domain() diff --git a/openstack/tests/unit/identity/v3/test_endpoint.py b/openstack/tests/unit/identity/v3/test_endpoint.py index 115e9b7ef..53a27d4c6 100644 --- a/openstack/tests/unit/identity/v3/test_endpoint.py +++ b/openstack/tests/unit/identity/v3/test_endpoint.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.identity.v3 import endpoint @@ -26,7 +26,7 @@ } -class TestEndpoint(testtools.TestCase): +class TestEndpoint(base.TestCase): def test_basic(self): sot = endpoint.Endpoint() diff --git a/openstack/tests/unit/identity/v3/test_group.py b/openstack/tests/unit/identity/v3/test_group.py index 2051aeb3c..edb17f09c 100644 --- a/openstack/tests/unit/identity/v3/test_group.py +++ b/openstack/tests/unit/identity/v3/test_group.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.identity.v3 import group @@ -23,7 +23,7 @@ } -class TestGroup(testtools.TestCase): +class TestGroup(base.TestCase): def test_basic(self): sot = group.Group() diff --git a/openstack/tests/unit/identity/v3/test_policy.py b/openstack/tests/unit/identity/v3/test_policy.py index b514da96e..0533b9382 100644 --- a/openstack/tests/unit/identity/v3/test_policy.py +++ b/openstack/tests/unit/identity/v3/test_policy.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.identity.v3 import policy @@ -25,7 +25,7 @@ } -class TestPolicy(testtools.TestCase): +class TestPolicy(base.TestCase): def test_basic(self): sot = policy.Policy() diff --git a/openstack/tests/unit/identity/v3/test_project.py b/openstack/tests/unit/identity/v3/test_project.py index 2c38efceb..246adaf43 100644 --- a/openstack/tests/unit/identity/v3/test_project.py +++ b/openstack/tests/unit/identity/v3/test_project.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.identity.v3 import project @@ -26,7 +26,7 @@ } -class TestProject(testtools.TestCase): +class TestProject(base.TestCase): def test_basic(self): sot = project.Project() @@ -64,7 +64,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['parent_id'], sot.parent_id) -class TestUserProject(testtools.TestCase): +class TestUserProject(base.TestCase): def test_basic(self): sot = project.UserProject() diff --git a/openstack/tests/unit/identity/v3/test_region.py b/openstack/tests/unit/identity/v3/test_region.py index cf7841683..b3c54f121 100644 --- a/openstack/tests/unit/identity/v3/test_region.py +++ b/openstack/tests/unit/identity/v3/test_region.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.identity.v3 import region @@ -23,7 +23,7 @@ } -class TestRegion(testtools.TestCase): +class TestRegion(base.TestCase): def test_basic(self): sot = region.Region() diff --git a/openstack/tests/unit/identity/v3/test_role.py b/openstack/tests/unit/identity/v3/test_role.py index 7342b7c66..957cfbb99 100644 --- a/openstack/tests/unit/identity/v3/test_role.py +++ b/openstack/tests/unit/identity/v3/test_role.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.identity.v3 import role @@ -22,7 +22,7 @@ } -class TestRole(testtools.TestCase): +class TestRole(base.TestCase): def test_basic(self): sot = role.Role() diff --git a/openstack/tests/unit/identity/v3/test_role_assignment.py b/openstack/tests/unit/identity/v3/test_role_assignment.py index a0a1e5050..c54df84bd 100644 --- a/openstack/tests/unit/identity/v3/test_role_assignment.py +++ b/openstack/tests/unit/identity/v3/test_role_assignment.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.identity.v3 import role_assignment @@ -24,7 +24,7 @@ } -class TestRoleAssignment(testtools.TestCase): +class TestRoleAssignment(base.TestCase): def test_basic(self): sot = role_assignment.RoleAssignment() diff --git a/openstack/tests/unit/identity/v3/test_role_domain_group_assignment.py b/openstack/tests/unit/identity/v3/test_role_domain_group_assignment.py index 5e0777d20..92b6539a0 100644 --- a/openstack/tests/unit/identity/v3/test_role_domain_group_assignment.py +++ b/openstack/tests/unit/identity/v3/test_role_domain_group_assignment.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.identity.v3 import role_domain_group_assignment @@ -24,7 +24,7 @@ } -class TestRoleDomainGroupAssignment(testtools.TestCase): +class TestRoleDomainGroupAssignment(base.TestCase): def test_basic(self): sot = role_domain_group_assignment.RoleDomainGroupAssignment() diff --git a/openstack/tests/unit/identity/v3/test_role_domain_user_assignment.py b/openstack/tests/unit/identity/v3/test_role_domain_user_assignment.py index cc7de8bd7..bd19a8daf 100644 --- a/openstack/tests/unit/identity/v3/test_role_domain_user_assignment.py +++ b/openstack/tests/unit/identity/v3/test_role_domain_user_assignment.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.identity.v3 import role_domain_user_assignment @@ -24,7 +24,7 @@ } -class TestRoleDomainUserAssignment(testtools.TestCase): +class TestRoleDomainUserAssignment(base.TestCase): def test_basic(self): sot = role_domain_user_assignment.RoleDomainUserAssignment() diff --git a/openstack/tests/unit/identity/v3/test_role_project_group_assignment.py b/openstack/tests/unit/identity/v3/test_role_project_group_assignment.py index 81c96e7db..4070909a9 100644 --- a/openstack/tests/unit/identity/v3/test_role_project_group_assignment.py +++ b/openstack/tests/unit/identity/v3/test_role_project_group_assignment.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.identity.v3 import role_project_group_assignment @@ -24,7 +24,7 @@ } -class TestRoleProjectGroupAssignment(testtools.TestCase): +class TestRoleProjectGroupAssignment(base.TestCase): def test_basic(self): sot = role_project_group_assignment.RoleProjectGroupAssignment() diff --git a/openstack/tests/unit/identity/v3/test_role_project_user_assignment.py b/openstack/tests/unit/identity/v3/test_role_project_user_assignment.py index 5314c0bc5..c04ffdaa1 100644 --- a/openstack/tests/unit/identity/v3/test_role_project_user_assignment.py +++ b/openstack/tests/unit/identity/v3/test_role_project_user_assignment.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.identity.v3 import role_project_user_assignment @@ -24,7 +24,7 @@ } -class TestRoleProjectUserAssignment(testtools.TestCase): +class TestRoleProjectUserAssignment(base.TestCase): def test_basic(self): sot = role_project_user_assignment.RoleProjectUserAssignment() diff --git a/openstack/tests/unit/identity/v3/test_service.py b/openstack/tests/unit/identity/v3/test_service.py index be321a22a..0abceb6ea 100644 --- a/openstack/tests/unit/identity/v3/test_service.py +++ b/openstack/tests/unit/identity/v3/test_service.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.identity.v3 import service @@ -25,7 +25,7 @@ } -class TestService(testtools.TestCase): +class TestService(base.TestCase): def test_basic(self): sot = service.Service() diff --git a/openstack/tests/unit/identity/v3/test_trust.py b/openstack/tests/unit/identity/v3/test_trust.py index de5c07c6c..258a6ce8e 100644 --- a/openstack/tests/unit/identity/v3/test_trust.py +++ b/openstack/tests/unit/identity/v3/test_trust.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.identity.v3 import trust @@ -32,7 +32,7 @@ } -class TestTrust(testtools.TestCase): +class TestTrust(base.TestCase): def test_basic(self): sot = trust.Trust() diff --git a/openstack/tests/unit/identity/v3/test_user.py b/openstack/tests/unit/identity/v3/test_user.py index 1f746d111..d09835747 100644 --- a/openstack/tests/unit/identity/v3/test_user.py +++ b/openstack/tests/unit/identity/v3/test_user.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.identity.v3 import user @@ -29,7 +29,7 @@ } -class TestUser(testtools.TestCase): +class TestUser(base.TestCase): def test_basic(self): sot = user.User() diff --git a/openstack/tests/unit/image/test_image_service.py b/openstack/tests/unit/image/test_image_service.py index adc641255..236227430 100644 --- a/openstack/tests/unit/image/test_image_service.py +++ b/openstack/tests/unit/image/test_image_service.py @@ -10,12 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.image import image_service -class TestImageService(testtools.TestCase): +class TestImageService(base.TestCase): def test_service(self): sot = image_service.ImageService() diff --git a/openstack/tests/unit/image/v1/test_image.py b/openstack/tests/unit/image/v1/test_image.py index 4f0a4554d..d40e73535 100644 --- a/openstack/tests/unit/image/v1/test_image.py +++ b/openstack/tests/unit/image/v1/test_image.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.image.v1 import image @@ -36,7 +36,7 @@ } -class TestImage(testtools.TestCase): +class TestImage(base.TestCase): def test_basic(self): sot = image.Image() diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 2ad47095c..20098b6c4 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -16,7 +16,7 @@ from keystoneauth1 import adapter import mock import requests -import testtools +from openstack.tests.unit import base from openstack import exceptions from openstack.image.v2 import image @@ -94,7 +94,7 @@ def json(self): return self.body -class TestImage(testtools.TestCase): +class TestImage(base.TestCase): def setUp(self): super(TestImage, self).setUp() diff --git a/openstack/tests/unit/image/v2/test_member.py b/openstack/tests/unit/image/v2/test_member.py index 993639f52..8a3e35e50 100644 --- a/openstack/tests/unit/image/v2/test_member.py +++ b/openstack/tests/unit/image/v2/test_member.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.image.v2 import member @@ -24,7 +24,7 @@ } -class TestMember(testtools.TestCase): +class TestMember(base.TestCase): def test_basic(self): sot = member.Member() self.assertIsNone(sot.resource_key) diff --git a/openstack/tests/unit/key_manager/test_key_management_service.py b/openstack/tests/unit/key_manager/test_key_management_service.py index b458daf73..4024f2ab7 100644 --- a/openstack/tests/unit/key_manager/test_key_management_service.py +++ b/openstack/tests/unit/key_manager/test_key_management_service.py @@ -10,12 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.key_manager import key_manager_service -class TestKeyManagerService(testtools.TestCase): +class TestKeyManagerService(base.TestCase): def test_service(self): sot = key_manager_service.KeyManagerService() diff --git a/openstack/tests/unit/key_manager/v1/test_container.py b/openstack/tests/unit/key_manager/v1/test_container.py index 98fe51b6f..5bd665ff1 100644 --- a/openstack/tests/unit/key_manager/v1/test_container.py +++ b/openstack/tests/unit/key_manager/v1/test_container.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.key_manager.v1 import container @@ -28,7 +28,7 @@ } -class TestContainer(testtools.TestCase): +class TestContainer(base.TestCase): def test_basic(self): sot = container.Container() diff --git a/openstack/tests/unit/key_manager/v1/test_order.py b/openstack/tests/unit/key_manager/v1/test_order.py index 732500522..3c6590ff3 100644 --- a/openstack/tests/unit/key_manager/v1/test_order.py +++ b/openstack/tests/unit/key_manager/v1/test_order.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.key_manager.v1 import order @@ -31,7 +31,7 @@ } -class TestOrder(testtools.TestCase): +class TestOrder(base.TestCase): def test_basic(self): sot = order.Order() diff --git a/openstack/tests/unit/key_manager/v1/test_secret.py b/openstack/tests/unit/key_manager/v1/test_secret.py index cbdbfe7c8..2db2d4cb5 100644 --- a/openstack/tests/unit/key_manager/v1/test_secret.py +++ b/openstack/tests/unit/key_manager/v1/test_secret.py @@ -11,7 +11,7 @@ # under the License. import mock -import testtools +from openstack.tests.unit import base from openstack.key_manager.v1 import secret @@ -35,7 +35,7 @@ } -class TestSecret(testtools.TestCase): +class TestSecret(base.TestCase): def test_basic(self): sot = secret.Secret() diff --git a/openstack/tests/unit/load_balancer/test_health_monitor.py b/openstack/tests/unit/load_balancer/test_health_monitor.py index 515db7b32..771d30de9 100644 --- a/openstack/tests/unit/load_balancer/test_health_monitor.py +++ b/openstack/tests/unit/load_balancer/test_health_monitor.py @@ -11,7 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base import uuid from openstack.load_balancer.v2 import health_monitor @@ -38,7 +38,7 @@ } -class TestPoolHealthMonitor(testtools.TestCase): +class TestPoolHealthMonitor(base.TestCase): def test_basic(self): test_hm = health_monitor.HealthMonitor() diff --git a/openstack/tests/unit/load_balancer/test_l7policy.py b/openstack/tests/unit/load_balancer/test_l7policy.py index 74742b303..60492436b 100644 --- a/openstack/tests/unit/load_balancer/test_l7policy.py +++ b/openstack/tests/unit/load_balancer/test_l7policy.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base import uuid from openstack.load_balancer.v2 import l7_policy @@ -34,7 +34,7 @@ } -class TestL7Policy(testtools.TestCase): +class TestL7Policy(base.TestCase): def test_basic(self): test_l7_policy = l7_policy.L7Policy() diff --git a/openstack/tests/unit/load_balancer/test_l7rule.py b/openstack/tests/unit/load_balancer/test_l7rule.py index 70046c9b2..6afe925e2 100644 --- a/openstack/tests/unit/load_balancer/test_l7rule.py +++ b/openstack/tests/unit/load_balancer/test_l7rule.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base import uuid from openstack.load_balancer.v2 import l7_rule @@ -32,7 +32,7 @@ } -class TestL7Rule(testtools.TestCase): +class TestL7Rule(base.TestCase): def test_basic(self): test_l7rule = l7_rule.L7Rule() diff --git a/openstack/tests/unit/load_balancer/test_listener.py b/openstack/tests/unit/load_balancer/test_listener.py index 84fddf61c..940b3d878 100644 --- a/openstack/tests/unit/load_balancer/test_listener.py +++ b/openstack/tests/unit/load_balancer/test_listener.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base import uuid from openstack.load_balancer.v2 import listener @@ -39,7 +39,7 @@ } -class TestListener(testtools.TestCase): +class TestListener(base.TestCase): def test_basic(self): test_listener = listener.Listener() diff --git a/openstack/tests/unit/load_balancer/test_load_balancer.py b/openstack/tests/unit/load_balancer/test_load_balancer.py index 98196b312..0831d881d 100644 --- a/openstack/tests/unit/load_balancer/test_load_balancer.py +++ b/openstack/tests/unit/load_balancer/test_load_balancer.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base import uuid from openstack.load_balancer.v2 import load_balancer @@ -37,7 +37,7 @@ } -class TestLoadBalancer(testtools.TestCase): +class TestLoadBalancer(base.TestCase): def test_basic(self): test_load_balancer = load_balancer.LoadBalancer() diff --git a/openstack/tests/unit/load_balancer/test_load_balancer_service.py b/openstack/tests/unit/load_balancer/test_load_balancer_service.py index 6ad82dfc5..9d9a31f83 100644 --- a/openstack/tests/unit/load_balancer/test_load_balancer_service.py +++ b/openstack/tests/unit/load_balancer/test_load_balancer_service.py @@ -10,12 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.load_balancer import load_balancer_service as lb_service -class TestLoadBalancingService(testtools.TestCase): +class TestLoadBalancingService(base.TestCase): def test_service(self): sot = lb_service.LoadBalancerService() diff --git a/openstack/tests/unit/load_balancer/test_member.py b/openstack/tests/unit/load_balancer/test_member.py index 64446dfe9..7ad614ee5 100644 --- a/openstack/tests/unit/load_balancer/test_member.py +++ b/openstack/tests/unit/load_balancer/test_member.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base import uuid from openstack.load_balancer.v2 import member @@ -31,7 +31,7 @@ } -class TestPoolMember(testtools.TestCase): +class TestPoolMember(base.TestCase): def test_basic(self): test_member = member.Member() diff --git a/openstack/tests/unit/load_balancer/test_pool.py b/openstack/tests/unit/load_balancer/test_pool.py index 2000fb012..557dec3d5 100644 --- a/openstack/tests/unit/load_balancer/test_pool.py +++ b/openstack/tests/unit/load_balancer/test_pool.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base import uuid from openstack.load_balancer.v2 import pool @@ -38,7 +38,7 @@ } -class TestPool(testtools.TestCase): +class TestPool(base.TestCase): def test_basic(self): test_pool = pool.Pool() diff --git a/openstack/tests/unit/load_balancer/test_version.py b/openstack/tests/unit/load_balancer/test_version.py index 8461c4e51..101238544 100644 --- a/openstack/tests/unit/load_balancer/test_version.py +++ b/openstack/tests/unit/load_balancer/test_version.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.load_balancer import version @@ -22,7 +22,7 @@ } -class TestVersion(testtools.TestCase): +class TestVersion(base.TestCase): def test_basic(self): sot = version.Version() diff --git a/openstack/tests/unit/message/test_message_service.py b/openstack/tests/unit/message/test_message_service.py index 877a47a5d..d208c4706 100644 --- a/openstack/tests/unit/message/test_message_service.py +++ b/openstack/tests/unit/message/test_message_service.py @@ -10,12 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.message import message_service -class TestMessageService(testtools.TestCase): +class TestMessageService(base.TestCase): def test_service(self): sot = message_service.MessageService() diff --git a/openstack/tests/unit/message/test_version.py b/openstack/tests/unit/message/test_version.py index aea10840e..e9501e69a 100644 --- a/openstack/tests/unit/message/test_version.py +++ b/openstack/tests/unit/message/test_version.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.message import version @@ -22,7 +22,7 @@ } -class TestVersion(testtools.TestCase): +class TestVersion(base.TestCase): def test_basic(self): sot = version.Version() diff --git a/openstack/tests/unit/message/v2/test_claim.py b/openstack/tests/unit/message/v2/test_claim.py index 649f3ca51..9b299ecbc 100644 --- a/openstack/tests/unit/message/v2/test_claim.py +++ b/openstack/tests/unit/message/v2/test_claim.py @@ -12,7 +12,7 @@ import copy import mock -import testtools +from openstack.tests.unit import base import uuid from openstack.message.v2 import claim @@ -42,7 +42,7 @@ } -class TestClaim(testtools.TestCase): +class TestClaim(base.TestCase): def test_basic(self): sot = claim.Claim() self.assertEqual("claims", sot.resources_key) diff --git a/openstack/tests/unit/message/v2/test_message.py b/openstack/tests/unit/message/v2/test_message.py index 9a095b1ef..1a876a756 100644 --- a/openstack/tests/unit/message/v2/test_message.py +++ b/openstack/tests/unit/message/v2/test_message.py @@ -11,7 +11,7 @@ # under the License. import mock -import testtools +from openstack.tests.unit import base import uuid from openstack.message.v2 import message @@ -47,7 +47,7 @@ } -class TestMessage(testtools.TestCase): +class TestMessage(base.TestCase): def test_basic(self): sot = message.Message() self.assertEqual('messages', sot.resources_key) diff --git a/openstack/tests/unit/message/v2/test_queue.py b/openstack/tests/unit/message/v2/test_queue.py index 83acc66b4..187d400fb 100644 --- a/openstack/tests/unit/message/v2/test_queue.py +++ b/openstack/tests/unit/message/v2/test_queue.py @@ -11,7 +11,7 @@ # under the License. import mock -import testtools +from openstack.tests.unit import base import uuid from openstack.message.v2 import queue @@ -35,7 +35,7 @@ } -class TestQueue(testtools.TestCase): +class TestQueue(base.TestCase): def test_basic(self): sot = queue.Queue() self.assertEqual('queues', sot.resources_key) diff --git a/openstack/tests/unit/message/v2/test_subscription.py b/openstack/tests/unit/message/v2/test_subscription.py index 9c9c9e510..5567c9cad 100644 --- a/openstack/tests/unit/message/v2/test_subscription.py +++ b/openstack/tests/unit/message/v2/test_subscription.py @@ -12,7 +12,7 @@ import copy import mock -import testtools +from openstack.tests.unit import base import uuid from openstack.message.v2 import subscription @@ -48,7 +48,7 @@ } -class TestSubscription(testtools.TestCase): +class TestSubscription(base.TestCase): def test_basic(self): sot = subscription.Subscription() self.assertEqual("subscriptions", sot.resources_key) diff --git a/openstack/tests/unit/network/test_network_service.py b/openstack/tests/unit/network/test_network_service.py index 22980fe36..8ba9407c3 100644 --- a/openstack/tests/unit/network/test_network_service.py +++ b/openstack/tests/unit/network/test_network_service.py @@ -10,12 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.network import network_service -class TestNetworkService(testtools.TestCase): +class TestNetworkService(base.TestCase): def test_service(self): sot = network_service.NetworkService() diff --git a/openstack/tests/unit/network/test_version.py b/openstack/tests/unit/network/test_version.py index 6f3def463..6f9dec05c 100644 --- a/openstack/tests/unit/network/test_version.py +++ b/openstack/tests/unit/network/test_version.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.network import version @@ -22,7 +22,7 @@ } -class TestVersion(testtools.TestCase): +class TestVersion(base.TestCase): def test_basic(self): sot = version.Version() diff --git a/openstack/tests/unit/network/v2/test_address_scope.py b/openstack/tests/unit/network/v2/test_address_scope.py index ae9cbe85b..a031284f4 100644 --- a/openstack/tests/unit/network/v2/test_address_scope.py +++ b/openstack/tests/unit/network/v2/test_address_scope.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.network.v2 import address_scope @@ -24,7 +24,7 @@ } -class TestAddressScope(testtools.TestCase): +class TestAddressScope(base.TestCase): def test_basic(self): sot = address_scope.AddressScope() diff --git a/openstack/tests/unit/network/v2/test_agent.py b/openstack/tests/unit/network/v2/test_agent.py index 80bbbbd47..e279905ed 100644 --- a/openstack/tests/unit/network/v2/test_agent.py +++ b/openstack/tests/unit/network/v2/test_agent.py @@ -11,7 +11,7 @@ # under the License. import mock -import testtools +from openstack.tests.unit import base from openstack.network.v2 import agent @@ -34,7 +34,7 @@ } -class TestAgent(testtools.TestCase): +class TestAgent(base.TestCase): def test_basic(self): sot = agent.Agent() @@ -120,7 +120,7 @@ def test_remove_router_from_agent(self): json=body) -class TestNetworkHostingDHCPAgent(testtools.TestCase): +class TestNetworkHostingDHCPAgent(base.TestCase): def test_basic(self): net = agent.NetworkHostingDHCPAgent() @@ -136,7 +136,7 @@ def test_basic(self): self.assertTrue(net.allow_list) -class TestRouterL3Agent(testtools.TestCase): +class TestRouterL3Agent(base.TestCase): def test_basic(self): sot = agent.RouterL3Agent() diff --git a/openstack/tests/unit/network/v2/test_auto_allocated_topology.py b/openstack/tests/unit/network/v2/test_auto_allocated_topology.py index 941660a1f..c9853cfa8 100644 --- a/openstack/tests/unit/network/v2/test_auto_allocated_topology.py +++ b/openstack/tests/unit/network/v2/test_auto_allocated_topology.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.network.v2 import auto_allocated_topology @@ -20,7 +20,7 @@ } -class TestAutoAllocatedTopology(testtools.TestCase): +class TestAutoAllocatedTopology(base.TestCase): def test_basic(self): topo = auto_allocated_topology.AutoAllocatedTopology diff --git a/openstack/tests/unit/network/v2/test_availability_zone.py b/openstack/tests/unit/network/v2/test_availability_zone.py index 7ab1031e8..67576ca9c 100644 --- a/openstack/tests/unit/network/v2/test_availability_zone.py +++ b/openstack/tests/unit/network/v2/test_availability_zone.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.network.v2 import availability_zone @@ -23,7 +23,7 @@ } -class TestAvailabilityZone(testtools.TestCase): +class TestAvailabilityZone(base.TestCase): def test_basic(self): sot = availability_zone.AvailabilityZone() diff --git a/openstack/tests/unit/network/v2/test_extension.py b/openstack/tests/unit/network/v2/test_extension.py index fd2511b73..bd968746c 100644 --- a/openstack/tests/unit/network/v2/test_extension.py +++ b/openstack/tests/unit/network/v2/test_extension.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.network.v2 import extension @@ -24,7 +24,7 @@ } -class TestExtension(testtools.TestCase): +class TestExtension(base.TestCase): def test_basic(self): sot = extension.Extension() diff --git a/openstack/tests/unit/network/v2/test_flavor.py b/openstack/tests/unit/network/v2/test_flavor.py index 236511f20..024011b22 100644 --- a/openstack/tests/unit/network/v2/test_flavor.py +++ b/openstack/tests/unit/network/v2/test_flavor.py @@ -11,7 +11,7 @@ # under the License. import mock -import testtools +from openstack.tests.unit import base from openstack.network.v2 import flavor @@ -32,7 +32,7 @@ } -class TestFlavor(testtools.TestCase): +class TestFlavor(base.TestCase): def test_basic(self): flavors = flavor.Flavor() self.assertEqual('flavor', flavors.resource_key) diff --git a/openstack/tests/unit/network/v2/test_floating_ip.py b/openstack/tests/unit/network/v2/test_floating_ip.py index 81a84fed2..92dc36512 100644 --- a/openstack/tests/unit/network/v2/test_floating_ip.py +++ b/openstack/tests/unit/network/v2/test_floating_ip.py @@ -12,7 +12,7 @@ from keystoneauth1 import adapter import mock -import testtools +from openstack.tests.unit import base from openstack.network.v2 import floating_ip @@ -35,7 +35,7 @@ } -class TestFloatingIP(testtools.TestCase): +class TestFloatingIP(base.TestCase): def test_basic(self): sot = floating_ip.FloatingIP() diff --git a/openstack/tests/unit/network/v2/test_health_monitor.py b/openstack/tests/unit/network/v2/test_health_monitor.py index 1198a102e..7018ee185 100644 --- a/openstack/tests/unit/network/v2/test_health_monitor.py +++ b/openstack/tests/unit/network/v2/test_health_monitor.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.network.v2 import health_monitor @@ -32,7 +32,7 @@ } -class TestHealthMonitor(testtools.TestCase): +class TestHealthMonitor(base.TestCase): def test_basic(self): sot = health_monitor.HealthMonitor() diff --git a/openstack/tests/unit/network/v2/test_listener.py b/openstack/tests/unit/network/v2/test_listener.py index 4eb550e0e..55db10895 100644 --- a/openstack/tests/unit/network/v2/test_listener.py +++ b/openstack/tests/unit/network/v2/test_listener.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.network.v2 import listener @@ -32,7 +32,7 @@ } -class TestListener(testtools.TestCase): +class TestListener(base.TestCase): def test_basic(self): sot = listener.Listener() diff --git a/openstack/tests/unit/network/v2/test_load_balancer.py b/openstack/tests/unit/network/v2/test_load_balancer.py index 9a657c4b9..4283f4241 100644 --- a/openstack/tests/unit/network/v2/test_load_balancer.py +++ b/openstack/tests/unit/network/v2/test_load_balancer.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.network.v2 import load_balancer @@ -32,7 +32,7 @@ } -class TestLoadBalancer(testtools.TestCase): +class TestLoadBalancer(base.TestCase): def test_basic(self): sot = load_balancer.LoadBalancer() diff --git a/openstack/tests/unit/network/v2/test_metering_label.py b/openstack/tests/unit/network/v2/test_metering_label.py index 4d1e57a98..af9d293d5 100644 --- a/openstack/tests/unit/network/v2/test_metering_label.py +++ b/openstack/tests/unit/network/v2/test_metering_label.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.network.v2 import metering_label @@ -24,7 +24,7 @@ } -class TestMeteringLabel(testtools.TestCase): +class TestMeteringLabel(base.TestCase): def test_basic(self): sot = metering_label.MeteringLabel() diff --git a/openstack/tests/unit/network/v2/test_metering_label_rule.py b/openstack/tests/unit/network/v2/test_metering_label_rule.py index 03c134b42..b6b9a1317 100644 --- a/openstack/tests/unit/network/v2/test_metering_label_rule.py +++ b/openstack/tests/unit/network/v2/test_metering_label_rule.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.network.v2 import metering_label_rule @@ -25,7 +25,7 @@ } -class TestMeteringLabelRule(testtools.TestCase): +class TestMeteringLabelRule(base.TestCase): def test_basic(self): sot = metering_label_rule.MeteringLabelRule() diff --git a/openstack/tests/unit/network/v2/test_network.py b/openstack/tests/unit/network/v2/test_network.py index 3b0c64630..787cdbef1 100644 --- a/openstack/tests/unit/network/v2/test_network.py +++ b/openstack/tests/unit/network/v2/test_network.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.network.v2 import network @@ -45,7 +45,7 @@ } -class TestNetwork(testtools.TestCase): +class TestNetwork(base.TestCase): def test_basic(self): sot = network.Network() @@ -119,7 +119,7 @@ def test_make_it(self): sot._query_mapping._mapping) -class TestDHCPAgentHostingNetwork(testtools.TestCase): +class TestDHCPAgentHostingNetwork(base.TestCase): def test_basic(self): net = network.DHCPAgentHostingNetwork() diff --git a/openstack/tests/unit/network/v2/test_network_ip_availability.py b/openstack/tests/unit/network/v2/test_network_ip_availability.py index 3d0246b19..0dbecc1f2 100644 --- a/openstack/tests/unit/network/v2/test_network_ip_availability.py +++ b/openstack/tests/unit/network/v2/test_network_ip_availability.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.network.v2 import network_ip_availability @@ -38,7 +38,7 @@ } -class TestNetworkIPAvailability(testtools.TestCase): +class TestNetworkIPAvailability(base.TestCase): def test_basic(self): sot = network_ip_availability.NetworkIPAvailability() diff --git a/openstack/tests/unit/network/v2/test_pool.py b/openstack/tests/unit/network/v2/test_pool.py index fb580812d..3357c1a23 100644 --- a/openstack/tests/unit/network/v2/test_pool.py +++ b/openstack/tests/unit/network/v2/test_pool.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.network.v2 import pool @@ -39,7 +39,7 @@ } -class TestPool(testtools.TestCase): +class TestPool(base.TestCase): def test_basic(self): sot = pool.Pool() diff --git a/openstack/tests/unit/network/v2/test_pool_member.py b/openstack/tests/unit/network/v2/test_pool_member.py index f175cb35b..dd7e9aa01 100644 --- a/openstack/tests/unit/network/v2/test_pool_member.py +++ b/openstack/tests/unit/network/v2/test_pool_member.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.network.v2 import pool_member @@ -28,7 +28,7 @@ } -class TestPoolMember(testtools.TestCase): +class TestPoolMember(base.TestCase): def test_basic(self): sot = pool_member.PoolMember() diff --git a/openstack/tests/unit/network/v2/test_port.py b/openstack/tests/unit/network/v2/test_port.py index 2bfbcdac3..09ebf167f 100644 --- a/openstack/tests/unit/network/v2/test_port.py +++ b/openstack/tests/unit/network/v2/test_port.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.network.v2 import port @@ -57,7 +57,7 @@ } -class TestPort(testtools.TestCase): +class TestPort(base.TestCase): def test_basic(self): sot = port.Port() diff --git a/openstack/tests/unit/network/v2/test_qos_bandwidth_limit_rule.py b/openstack/tests/unit/network/v2/test_qos_bandwidth_limit_rule.py index f6fc9fd3c..cfb77f332 100644 --- a/openstack/tests/unit/network/v2/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/unit/network/v2/test_qos_bandwidth_limit_rule.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base import uuid from openstack.network.v2 import qos_bandwidth_limit_rule @@ -24,7 +24,7 @@ } -class TestQoSBandwidthLimitRule(testtools.TestCase): +class TestQoSBandwidthLimitRule(base.TestCase): def test_basic(self): sot = qos_bandwidth_limit_rule.QoSBandwidthLimitRule() diff --git a/openstack/tests/unit/network/v2/test_qos_dscp_marking_rule.py b/openstack/tests/unit/network/v2/test_qos_dscp_marking_rule.py index 46bffd1cd..11132fc70 100644 --- a/openstack/tests/unit/network/v2/test_qos_dscp_marking_rule.py +++ b/openstack/tests/unit/network/v2/test_qos_dscp_marking_rule.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base import uuid from openstack.network.v2 import qos_dscp_marking_rule @@ -22,7 +22,7 @@ } -class TestQoSDSCPMarkingRule(testtools.TestCase): +class TestQoSDSCPMarkingRule(base.TestCase): def test_basic(self): sot = qos_dscp_marking_rule.QoSDSCPMarkingRule() diff --git a/openstack/tests/unit/network/v2/test_qos_minimum_bandwidth_rule.py b/openstack/tests/unit/network/v2/test_qos_minimum_bandwidth_rule.py index f6b2f4422..090e8d198 100644 --- a/openstack/tests/unit/network/v2/test_qos_minimum_bandwidth_rule.py +++ b/openstack/tests/unit/network/v2/test_qos_minimum_bandwidth_rule.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base import uuid from openstack.network.v2 import qos_minimum_bandwidth_rule @@ -23,7 +23,7 @@ } -class TestQoSMinimumBandwidthRule(testtools.TestCase): +class TestQoSMinimumBandwidthRule(base.TestCase): def test_basic(self): sot = qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule() diff --git a/openstack/tests/unit/network/v2/test_qos_policy.py b/openstack/tests/unit/network/v2/test_qos_policy.py index 7c5e6c4de..525f8f33d 100644 --- a/openstack/tests/unit/network/v2/test_qos_policy.py +++ b/openstack/tests/unit/network/v2/test_qos_policy.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base import uuid from openstack.network.v2 import qos_policy @@ -26,7 +26,7 @@ } -class TestQoSPolicy(testtools.TestCase): +class TestQoSPolicy(base.TestCase): def test_basic(self): sot = qos_policy.QoSPolicy() diff --git a/openstack/tests/unit/network/v2/test_qos_rule_type.py b/openstack/tests/unit/network/v2/test_qos_rule_type.py index f0dc06fee..eb3c12d4b 100644 --- a/openstack/tests/unit/network/v2/test_qos_rule_type.py +++ b/openstack/tests/unit/network/v2/test_qos_rule_type.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.network.v2 import qos_rule_type @@ -35,7 +35,7 @@ } -class TestQoSRuleType(testtools.TestCase): +class TestQoSRuleType(base.TestCase): def test_basic(self): sot = qos_rule_type.QoSRuleType() diff --git a/openstack/tests/unit/network/v2/test_quota.py b/openstack/tests/unit/network/v2/test_quota.py index 1f35281e5..00a54f999 100644 --- a/openstack/tests/unit/network/v2/test_quota.py +++ b/openstack/tests/unit/network/v2/test_quota.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.network.v2 import quota from openstack import resource @@ -35,7 +35,7 @@ } -class TestQuota(testtools.TestCase): +class TestQuota(base.TestCase): def test_basic(self): sot = quota.Quota() @@ -82,7 +82,7 @@ def test_alternate_id(self): resource.Resource._get_id(quota_obj)) -class TestQuotaDefault(testtools.TestCase): +class TestQuotaDefault(base.TestCase): def test_basic(self): sot = quota.QuotaDefault() diff --git a/openstack/tests/unit/network/v2/test_rbac_policy.py b/openstack/tests/unit/network/v2/test_rbac_policy.py index c8189c9c2..f42237a7f 100644 --- a/openstack/tests/unit/network/v2/test_rbac_policy.py +++ b/openstack/tests/unit/network/v2/test_rbac_policy.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.network.v2 import rbac_policy @@ -24,7 +24,7 @@ } -class TestRBACPolicy(testtools.TestCase): +class TestRBACPolicy(base.TestCase): def test_basic(self): sot = rbac_policy.RBACPolicy() diff --git a/openstack/tests/unit/network/v2/test_router.py b/openstack/tests/unit/network/v2/test_router.py index 8cf9359ca..2bc12d26e 100644 --- a/openstack/tests/unit/network/v2/test_router.py +++ b/openstack/tests/unit/network/v2/test_router.py @@ -11,7 +11,7 @@ # under the License. import mock -import testtools +from openstack.tests.unit import base from openstack.network.v2 import router @@ -58,7 +58,7 @@ } -class TestRouter(testtools.TestCase): +class TestRouter(base.TestCase): def test_basic(self): sot = router.Router() @@ -205,7 +205,7 @@ def test_remove_router_gateway(self): json=body) -class TestL3AgentRouters(testtools.TestCase): +class TestL3AgentRouters(base.TestCase): def test_basic(self): sot = router.L3AgentRouter() diff --git a/openstack/tests/unit/network/v2/test_security_group.py b/openstack/tests/unit/network/v2/test_security_group.py index 2d18edb0c..73528f0c5 100644 --- a/openstack/tests/unit/network/v2/test_security_group.py +++ b/openstack/tests/unit/network/v2/test_security_group.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.network.v2 import security_group @@ -61,7 +61,7 @@ } -class TestSecurityGroup(testtools.TestCase): +class TestSecurityGroup(base.TestCase): def test_basic(self): sot = security_group.SecurityGroup() diff --git a/openstack/tests/unit/network/v2/test_security_group_rule.py b/openstack/tests/unit/network/v2/test_security_group_rule.py index 5ad00d91b..5292099c9 100644 --- a/openstack/tests/unit/network/v2/test_security_group_rule.py +++ b/openstack/tests/unit/network/v2/test_security_group_rule.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.network.v2 import security_group_rule @@ -33,7 +33,7 @@ } -class TestSecurityGroupRule(testtools.TestCase): +class TestSecurityGroupRule(base.TestCase): def test_basic(self): sot = security_group_rule.SecurityGroupRule() diff --git a/openstack/tests/unit/network/v2/test_segment.py b/openstack/tests/unit/network/v2/test_segment.py index 307f7568a..e651fd6d6 100644 --- a/openstack/tests/unit/network/v2/test_segment.py +++ b/openstack/tests/unit/network/v2/test_segment.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.network.v2 import segment @@ -26,7 +26,7 @@ } -class TestSegment(testtools.TestCase): +class TestSegment(base.TestCase): def test_basic(self): sot = segment.Segment() diff --git a/openstack/tests/unit/network/v2/test_service_profile.py b/openstack/tests/unit/network/v2/test_service_profile.py index 232cab5d9..e5961b4b9 100644 --- a/openstack/tests/unit/network/v2/test_service_profile.py +++ b/openstack/tests/unit/network/v2/test_service_profile.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.network.v2 import service_profile @@ -28,7 +28,7 @@ } -class TestServiceProfile(testtools.TestCase): +class TestServiceProfile(base.TestCase): def test_basic(self): service_profiles = service_profile.ServiceProfile() self.assertEqual('service_profile', service_profiles.resource_key) diff --git a/openstack/tests/unit/network/v2/test_service_provider.py b/openstack/tests/unit/network/v2/test_service_provider.py index bc946a03d..e3c3448cc 100644 --- a/openstack/tests/unit/network/v2/test_service_provider.py +++ b/openstack/tests/unit/network/v2/test_service_provider.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.network.v2 import service_provider @@ -22,7 +22,7 @@ } -class TestServiceProvider(testtools.TestCase): +class TestServiceProvider(base.TestCase): def test_basic(self): sot = service_provider.ServiceProvider() diff --git a/openstack/tests/unit/network/v2/test_subnet.py b/openstack/tests/unit/network/v2/test_subnet.py index 2e344a9a8..9d1224405 100644 --- a/openstack/tests/unit/network/v2/test_subnet.py +++ b/openstack/tests/unit/network/v2/test_subnet.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.network.v2 import subnet @@ -40,7 +40,7 @@ } -class TestSubnet(testtools.TestCase): +class TestSubnet(base.TestCase): def test_basic(self): sot = subnet.Subnet() diff --git a/openstack/tests/unit/network/v2/test_subnet_pool.py b/openstack/tests/unit/network/v2/test_subnet_pool.py index 908a03781..7751820c8 100644 --- a/openstack/tests/unit/network/v2/test_subnet_pool.py +++ b/openstack/tests/unit/network/v2/test_subnet_pool.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.network.v2 import subnet_pool @@ -35,7 +35,7 @@ } -class TestSubnetpool(testtools.TestCase): +class TestSubnetpool(base.TestCase): def test_basic(self): sot = subnet_pool.SubnetPool() diff --git a/openstack/tests/unit/network/v2/test_tag.py b/openstack/tests/unit/network/v2/test_tag.py index ff7270d2d..fdfb8d51d 100644 --- a/openstack/tests/unit/network/v2/test_tag.py +++ b/openstack/tests/unit/network/v2/test_tag.py @@ -12,7 +12,7 @@ import inspect import mock -import testtools +from openstack.tests.unit import base from openstack.network.v2 import network import openstack.network.v2 as network_resources @@ -21,7 +21,7 @@ ID = 'IDENTIFIER' -class TestTag(testtools.TestCase): +class TestTag(base.TestCase): @staticmethod def _create_network_resource(tags=None): diff --git a/openstack/tests/unit/network/v2/test_vpn_service.py b/openstack/tests/unit/network/v2/test_vpn_service.py index a5b54590c..662f461ef 100644 --- a/openstack/tests/unit/network/v2/test_vpn_service.py +++ b/openstack/tests/unit/network/v2/test_vpn_service.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.network.v2 import vpn_service @@ -29,7 +29,7 @@ } -class TestVPNService(testtools.TestCase): +class TestVPNService(base.TestCase): def test_basic(self): sot = vpn_service.VPNService() diff --git a/openstack/tests/unit/object_store/test_object_store_service.py b/openstack/tests/unit/object_store/test_object_store_service.py index a2707a134..65d20657b 100644 --- a/openstack/tests/unit/object_store/test_object_store_service.py +++ b/openstack/tests/unit/object_store/test_object_store_service.py @@ -10,12 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.object_store import object_store_service -class TestObjectStoreService(testtools.TestCase): +class TestObjectStoreService(base.TestCase): def test_service(self): sot = object_store_service.ObjectStoreService() diff --git a/openstack/tests/unit/object_store/v1/test_account.py b/openstack/tests/unit/object_store/v1/test_account.py index f498fa41b..50f511a0f 100644 --- a/openstack/tests/unit/object_store/v1/test_account.py +++ b/openstack/tests/unit/object_store/v1/test_account.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.object_store.v1 import account @@ -29,7 +29,7 @@ } -class TestAccount(testtools.TestCase): +class TestAccount(base.TestCase): def test_basic(self): sot = account.Account(**ACCOUNT_EXAMPLE) diff --git a/openstack/tests/unit/object_store/v1/test_container.py b/openstack/tests/unit/object_store/v1/test_container.py index 5c5681c1e..dd0216a2b 100644 --- a/openstack/tests/unit/object_store/v1/test_container.py +++ b/openstack/tests/unit/object_store/v1/test_container.py @@ -14,7 +14,7 @@ from openstack.tests.unit import base -class TestContainer(base.RequestsMockTestCase): +class TestContainer(base.TestCase): def setUp(self): super(TestContainer, self).setUp() diff --git a/openstack/tests/unit/orchestration/test_orchestration_service.py b/openstack/tests/unit/orchestration/test_orchestration_service.py index 9d0840add..aefb37110 100644 --- a/openstack/tests/unit/orchestration/test_orchestration_service.py +++ b/openstack/tests/unit/orchestration/test_orchestration_service.py @@ -10,12 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.orchestration import orchestration_service -class TestOrchestrationService(testtools.TestCase): +class TestOrchestrationService(base.TestCase): def test_service(self): sot = orchestration_service.OrchestrationService() diff --git a/openstack/tests/unit/orchestration/test_version.py b/openstack/tests/unit/orchestration/test_version.py index 5da8a37c7..464286dbe 100644 --- a/openstack/tests/unit/orchestration/test_version.py +++ b/openstack/tests/unit/orchestration/test_version.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.orchestration import version @@ -22,7 +22,7 @@ } -class TestVersion(testtools.TestCase): +class TestVersion(base.TestCase): def test_basic(self): sot = version.Version() diff --git a/openstack/tests/unit/orchestration/v1/test_resource.py b/openstack/tests/unit/orchestration/v1/test_resource.py index b4080ac3f..9e1968fe0 100644 --- a/openstack/tests/unit/orchestration/v1/test_resource.py +++ b/openstack/tests/unit/orchestration/v1/test_resource.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.orchestration.v1 import resource @@ -36,7 +36,7 @@ } -class TestResource(testtools.TestCase): +class TestResource(base.TestCase): def test_basic(self): sot = resource.Resource() diff --git a/openstack/tests/unit/orchestration/v1/test_software_config.py b/openstack/tests/unit/orchestration/v1/test_software_config.py index aa752a7ba..02dad3152 100644 --- a/openstack/tests/unit/orchestration/v1/test_software_config.py +++ b/openstack/tests/unit/orchestration/v1/test_software_config.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.orchestration.v1 import software_config @@ -29,7 +29,7 @@ } -class TestSoftwareConfig(testtools.TestCase): +class TestSoftwareConfig(base.TestCase): def test_basic(self): sot = software_config.SoftwareConfig() diff --git a/openstack/tests/unit/orchestration/v1/test_software_deployment.py b/openstack/tests/unit/orchestration/v1/test_software_deployment.py index cb03ac0fd..fb242ebeb 100644 --- a/openstack/tests/unit/orchestration/v1/test_software_deployment.py +++ b/openstack/tests/unit/orchestration/v1/test_software_deployment.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.orchestration.v1 import software_deployment @@ -29,7 +29,7 @@ } -class TestSoftwareDeployment(testtools.TestCase): +class TestSoftwareDeployment(base.TestCase): def test_basic(self): sot = software_deployment.SoftwareDeployment() diff --git a/openstack/tests/unit/orchestration/v1/test_stack.py b/openstack/tests/unit/orchestration/v1/test_stack.py index 802b07660..dc1a19b30 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack.py +++ b/openstack/tests/unit/orchestration/v1/test_stack.py @@ -12,7 +12,7 @@ import mock import six -import testtools +from openstack.tests.unit import base from openstack import exceptions from openstack.orchestration.v1 import stack @@ -51,7 +51,7 @@ } -class TestStack(testtools.TestCase): +class TestStack(base.TestCase): def test_basic(self): sot = stack.Stack() @@ -138,7 +138,7 @@ def test_get(self, mock_get): six.text_type(ex)) -class TestStackPreview(testtools.TestCase): +class TestStackPreview(base.TestCase): def test_basic(self): sot = stack.StackPreview() diff --git a/openstack/tests/unit/orchestration/v1/test_stack_environment.py b/openstack/tests/unit/orchestration/v1/test_stack_environment.py index 93b7286e1..692dc93cc 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack_environment.py +++ b/openstack/tests/unit/orchestration/v1/test_stack_environment.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.orchestration.v1 import stack_environment as se @@ -36,7 +36,7 @@ } -class TestStackTemplate(testtools.TestCase): +class TestStackTemplate(base.TestCase): def test_basic(self): sot = se.StackEnvironment() diff --git a/openstack/tests/unit/orchestration/v1/test_stack_files.py b/openstack/tests/unit/orchestration/v1/test_stack_files.py index 7e7aa6c80..4c4f06f9c 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack_files.py +++ b/openstack/tests/unit/orchestration/v1/test_stack_files.py @@ -11,7 +11,7 @@ # under the License. import mock -import testtools +from openstack.tests.unit import base from openstack.orchestration.v1 import stack_files as sf from openstack import resource @@ -22,7 +22,7 @@ } -class TestStackFiles(testtools.TestCase): +class TestStackFiles(base.TestCase): def test_basic(self): sot = sf.StackFiles() diff --git a/openstack/tests/unit/orchestration/v1/test_stack_template.py b/openstack/tests/unit/orchestration/v1/test_stack_template.py index 29cf5437d..7ea88142b 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack_template.py +++ b/openstack/tests/unit/orchestration/v1/test_stack_template.py @@ -11,7 +11,7 @@ # under the License. import copy -import testtools +from openstack.tests.unit import base from openstack.orchestration.v1 import stack_template @@ -36,7 +36,7 @@ } -class TestStackTemplate(testtools.TestCase): +class TestStackTemplate(base.TestCase): def test_basic(self): sot = stack_template.StackTemplate() diff --git a/openstack/tests/unit/orchestration/v1/test_template.py b/openstack/tests/unit/orchestration/v1/test_template.py index c127d9cab..7cc91f3da 100644 --- a/openstack/tests/unit/orchestration/v1/test_template.py +++ b/openstack/tests/unit/orchestration/v1/test_template.py @@ -11,7 +11,7 @@ # under the License. import mock -import testtools +from openstack.tests.unit import base from openstack.orchestration.v1 import template from openstack import resource @@ -31,7 +31,7 @@ } -class TestTemplate(testtools.TestCase): +class TestTemplate(base.TestCase): def test_basic(self): sot = template.Template() diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index b4f34f108..1c77aa403 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -57,7 +57,7 @@ cacert=CONFIG_CACERT) -class TestConnection(base.RequestsMockTestCase): +class TestConnection(base.TestCase): def setUp(self): super(TestConnection, self).setUp() @@ -194,7 +194,7 @@ def test_from_profile(self): profile=prof) -class TestAuthorize(base.RequestsMockTestCase): +class TestAuthorize(base.TestCase): def test_authorize_works(self): res = self.conn.authorize() diff --git a/openstack/tests/unit/test_exceptions.py b/openstack/tests/unit/test_exceptions.py index 2cd55721a..16841ecb6 100644 --- a/openstack/tests/unit/test_exceptions.py +++ b/openstack/tests/unit/test_exceptions.py @@ -11,13 +11,13 @@ # under the License. import mock -import testtools +from openstack.tests.unit import base import uuid from openstack import exceptions -class Test_Exception(testtools.TestCase): +class Test_Exception(base.TestCase): def test_method_not_supported(self): exc = exceptions.MethodNotSupported(self.__class__, 'list') expected = ('The list method is not supported for ' + @@ -25,7 +25,7 @@ def test_method_not_supported(self): self.assertEqual(expected, str(exc)) -class Test_HttpException(testtools.TestCase): +class Test_HttpException(base.TestCase): def setUp(self): super(Test_HttpException, self).setUp() @@ -59,7 +59,7 @@ def test_http_status(self): self.assertEqual(http_status, exc.status_code) -class TestRaiseFromResponse(testtools.TestCase): +class TestRaiseFromResponse(base.TestCase): def setUp(self): super(TestRaiseFromResponse, self).setUp() diff --git a/openstack/tests/unit/test_format.py b/openstack/tests/unit/test_format.py index f78454dd5..6c7bb04f5 100644 --- a/openstack/tests/unit/test_format.py +++ b/openstack/tests/unit/test_format.py @@ -10,12 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack import format -class TestBoolStrFormatter(testtools.TestCase): +class TestBoolStrFormatter(base.TestCase): def test_deserialize(self): self.assertTrue(format.BoolStr.deserialize(True)) diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index 45880683c..120ee77a8 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -11,7 +11,7 @@ # under the License. import mock -import testtools +from openstack.tests.unit import base from openstack import exceptions from openstack import proxy @@ -42,7 +42,7 @@ class HeadableResource(resource.Resource): allow_head = True -class TestProxyPrivate(testtools.TestCase): +class TestProxyPrivate(base.TestCase): def setUp(self): super(TestProxyPrivate, self).setUp() @@ -158,7 +158,7 @@ def test__get_resource_from_resource(self): self.assertEqual(result, res) -class TestProxyDelete(testtools.TestCase): +class TestProxyDelete(base.TestCase): def setUp(self): super(TestProxyDelete, self).setUp() @@ -214,7 +214,7 @@ def test_delete_HttpException(self): DeleteableResource, self.res, ignore_missing=False) -class TestProxyUpdate(testtools.TestCase): +class TestProxyUpdate(base.TestCase): def setUp(self): super(TestProxyUpdate, self).setUp() @@ -247,7 +247,7 @@ def test_update_id(self): self.res.update.assert_called_once_with(self.sot) -class TestProxyCreate(testtools.TestCase): +class TestProxyCreate(base.TestCase): def setUp(self): super(TestProxyCreate, self).setUp() @@ -271,7 +271,7 @@ def test_create_attributes(self): self.res.create.assert_called_once_with(self.sot) -class TestProxyGet(testtools.TestCase): +class TestProxyGet(base.TestCase): def setUp(self): super(TestProxyGet, self).setUp() @@ -321,7 +321,7 @@ def test_get_not_found(self): "test", self.sot._get, RetrieveableResource, self.res) -class TestProxyList(testtools.TestCase): +class TestProxyList(base.TestCase): def setUp(self): super(TestProxyList, self).setUp() @@ -349,7 +349,7 @@ def test_list_non_paginated(self): self._test_list(False) -class TestProxyHead(testtools.TestCase): +class TestProxyHead(base.TestCase): def setUp(self): super(TestProxyHead, self).setUp() @@ -380,7 +380,7 @@ def test_head_id(self): self.assertEqual(rv, self.fake_result) -class TestProxyWaits(testtools.TestCase): +class TestProxyWaits(base.TestCase): def setUp(self): super(TestProxyWaits, self).setUp() diff --git a/openstack/tests/unit/test_service_filter.py b/openstack/tests/unit/test_service_filter.py index 7d0129917..d63c623a5 100644 --- a/openstack/tests/unit/test_service_filter.py +++ b/openstack/tests/unit/test_service_filter.py @@ -10,20 +10,20 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.identity import identity_service from openstack import service_filter -class TestValidVersion(testtools.TestCase): +class TestValidVersion(base.TestCase): def test_constructor(self): sot = service_filter.ValidVersion('v1.0', 'v1') self.assertEqual('v1.0', sot.module) self.assertEqual('v1', sot.path) -class TestServiceFilter(testtools.TestCase): +class TestServiceFilter(base.TestCase): def test_init(self): sot = service_filter.ServiceFilter( 'ServiceType', region='REGION1', service_name='ServiceName', diff --git a/openstack/tests/unit/test_utils.py b/openstack/tests/unit/test_utils.py index 1110330ff..aa947ffa1 100644 --- a/openstack/tests/unit/test_utils.py +++ b/openstack/tests/unit/test_utils.py @@ -13,14 +13,14 @@ import logging import mock import sys -import testtools +from openstack.tests.unit import base import fixtures from openstack import utils -class Test_enable_logging(testtools.TestCase): +class Test_enable_logging(base.TestCase): def setUp(self): super(Test_enable_logging, self).setUp() @@ -89,7 +89,7 @@ def test_warning_file(self): self._file_tests(logging.INFO, False) -class Test_urljoin(testtools.TestCase): +class Test_urljoin(base.TestCase): def test_strings(self): root = "http://www.example.com" diff --git a/openstack/tests/unit/workflow/test_execution.py b/openstack/tests/unit/workflow/test_execution.py index bb8a4c001..a79a2b463 100644 --- a/openstack/tests/unit/workflow/test_execution.py +++ b/openstack/tests/unit/workflow/test_execution.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.workflow.v2 import execution @@ -27,7 +27,7 @@ } -class TestExecution(testtools.TestCase): +class TestExecution(base.TestCase): def setUp(self): super(TestExecution, self).setUp() diff --git a/openstack/tests/unit/workflow/test_version.py b/openstack/tests/unit/workflow/test_version.py index 1aca9a8fb..647418099 100644 --- a/openstack/tests/unit/workflow/test_version.py +++ b/openstack/tests/unit/workflow/test_version.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.workflow import version @@ -22,7 +22,7 @@ } -class TestVersion(testtools.TestCase): +class TestVersion(base.TestCase): def test_basic(self): sot = version.Version() diff --git a/openstack/tests/unit/workflow/test_workflow.py b/openstack/tests/unit/workflow/test_workflow.py index c5b2e22a8..b9d457a2a 100644 --- a/openstack/tests/unit/workflow/test_workflow.py +++ b/openstack/tests/unit/workflow/test_workflow.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.workflow.v2 import workflow @@ -22,7 +22,7 @@ } -class TestWorkflow(testtools.TestCase): +class TestWorkflow(base.TestCase): def setUp(self): super(TestWorkflow, self).setUp() diff --git a/openstack/tests/unit/workflow/test_workflow_service.py b/openstack/tests/unit/workflow/test_workflow_service.py index cc5dd9b0e..0427672e3 100644 --- a/openstack/tests/unit/workflow/test_workflow_service.py +++ b/openstack/tests/unit/workflow/test_workflow_service.py @@ -10,12 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -import testtools +from openstack.tests.unit import base from openstack.workflow import workflow_service -class TestWorkflowService(testtools.TestCase): +class TestWorkflowService(base.TestCase): def test_service(self): sot = workflow_service.WorkflowService() From 9c68fbe9d9ff7f47db797c73ff7e8295a4c17494 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 17 Feb 2018 10:17:32 +0000 Subject: [PATCH 1986/3836] Updated from global requirements Change-Id: I21937d230b9c439c6ce0f65621e2b4a57e8804c1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f131da00c..b7c4a0ad0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ requestsexceptions>=1.2.0 # Apache-2.0 jsonpatch!=1.20,>=1.16 # BSD six>=1.10.0 # MIT os-service-types>=1.1.0 # Apache-2.0 -keystoneauth1>=3.3.0 # Apache-2.0 +keystoneauth1>=3.4.0 # Apache-2.0 deprecation>=1.0 # Apache-2.0 munch>=2.1.0 # MIT From e8d4c458c3cb8736d77904532ad31f633224e7ee Mon Sep 17 00:00:00 2001 From: caishan Date: Mon, 12 Feb 2018 01:12:43 -0800 Subject: [PATCH 1987/3836] Update clustering module's _proxy comment message. fix wrong comment message in the proxy file of clustering module. Change-Id: I7c206db2edc079d0dbe6c7b718175a45c8b3d567 --- openstack/clustering/v1/_proxy.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/openstack/clustering/v1/_proxy.py b/openstack/clustering/v1/_proxy.py index e17a23504..955fed52f 100644 --- a/openstack/clustering/v1/_proxy.py +++ b/openstack/clustering/v1/_proxy.py @@ -46,10 +46,10 @@ def profile_types(self, **query): return self._list(_profile_type.ProfileType, paginated=False, **query) def get_profile_type(self, profile_type): - """Get the details about a profile_type. + """Get the details about a profile type. - :param name: The name of the profile_type to retrieve or an object of - :class:`~openstack.clustering.v1.profile_type.ProfileType`. + :param profile_type: The name of the profile_type to retrieve or an + object of :class:`~openstack.clustering.v1.profile_type.ProfileType`. :returns: A :class:`~openstack.clustering.v1.profile_type.ProfileType` object. @@ -67,7 +67,7 @@ def policy_types(self, **query): return self._list(_policy_type.PolicyType, paginated=False, **query) def get_policy_type(self, policy_type): - """Get the details about a policy_type. + """Get the details about a policy type. :param policy_type: The name of a poicy_type or an object of :class:`~openstack.clustering.v1.policy_type.PolicyType`. @@ -646,6 +646,11 @@ def find_node(self, name_or_id, ignore_missing=True): """Find a single node. :param str name_or_id: The name or ID of a node. + :param bool ignore_missing: When set to "False" + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the specified node does not exist. + when set to "True", None will be returned when + attempting to find a nonexistent policy :returns: One :class:`~openstack.clustering.v1.node.Node` object or None. """ @@ -773,7 +778,7 @@ def adopt_node(self, preview=False, **attrs): def node_operation(self, node, operation, **params): """Perform an operation on the specified node. - :param cluster: The value can be either the ID of a node or a + :param node: The value can be either the ID of a node or a :class:`~openstack.clustering.v1.node.Node` instance. :param operation: A string specifying the operation to be performed. :param dict params: A dictionary providing the parameters for the @@ -786,7 +791,7 @@ def node_operation(self, node, operation, **params): def perform_operation_on_node(self, node, operation, **params): """Perform an operation on the specified node. - :param cluster: The value can be either the ID of a node or a + :param node: The value can be either the ID of a node or a :class:`~openstack.clustering.v1.node.Node` instance. :param operation: A string specifying the operation to be performed. :param dict params: A dictionary providing the parameters for the From dbc3971f14a4d4babd5988868fb6df7900943632 Mon Sep 17 00:00:00 2001 From: "Yuanbin.Chen" Date: Thu, 22 Feb 2018 16:49:29 +0800 Subject: [PATCH 1988/3836] Fix clustering force delete return error This patch fix force delete appear "failed due to 'argument of type 'NoneType' is not iterable'" Change-Id: I23f99aa6cd41416558cba497225c4eff34e72e00 Signed-off-by: Yuanbin.Chen --- openstack/clustering/v1/cluster.py | 2 +- openstack/clustering/v1/node.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openstack/clustering/v1/cluster.py b/openstack/clustering/v1/cluster.py index 1b3668700..be01529fa 100644 --- a/openstack/clustering/v1/cluster.py +++ b/openstack/clustering/v1/cluster.py @@ -187,5 +187,5 @@ def force_delete(self, session): body = {'force': True} url = utils.urljoin(self.base_path, self.id) resp = session.delete(url, json=body) - self._translate_response(resp) + self._translate_response(resp, has_body=False) return self diff --git a/openstack/clustering/v1/node.py b/openstack/clustering/v1/node.py index abfdbbdb4..2c3775224 100644 --- a/openstack/clustering/v1/node.py +++ b/openstack/clustering/v1/node.py @@ -161,7 +161,7 @@ def force_delete(self, session): body = {'force': True} url = utils.urljoin(self.base_path, self.id) resp = session.delete(url, json=body) - self._translate_response(resp) + self._translate_response(resp, has_body=False) return self From 16e045db3245017cfc1a8dfb66941451ea9e1b0a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 2 Feb 2018 07:13:11 -0600 Subject: [PATCH 1989/3836] Run examples tests with functional tests The examples tests are currently not being run, which means they're probably broken. Move them into the functional test dir so that they get picked up when we run functional tests. Change-Id: Id4dc9d48544134f304c2d2a0a292c04184316d70 --- examples/connect.py | 18 +++++++----------- .../{ => functional}/examples/__init__.py | 0 .../{ => functional}/examples/test_compute.py | 4 ++-- .../{ => functional}/examples/test_identity.py | 4 ++-- .../{ => functional}/examples/test_image.py | 4 ++-- .../{ => functional}/examples/test_network.py | 4 ++-- 6 files changed, 15 insertions(+), 19 deletions(-) rename openstack/tests/{ => functional}/examples/__init__.py (100%) rename openstack/tests/{ => functional}/examples/test_compute.py (94%) rename openstack/tests/{ => functional}/examples/test_identity.py (93%) rename openstack/tests/{ => functional}/examples/test_image.py (92%) rename openstack/tests/{ => functional}/examples/test_network.py (94%) diff --git a/examples/connect.py b/examples/connect.py index d9c52c893..1864053a6 100644 --- a/examples/connect.py +++ b/examples/connect.py @@ -20,17 +20,19 @@ import os import openstack -from openstack import config as occ +from openstack.config import loader from openstack import utils import sys utils.enable_logging(True, stream=sys.stdout) -#: Defines the OpenStack Client Config (OCC) cloud key in your OCC config -#: file, typically in $HOME/.config/openstack/clouds.yaml. That configuration +#: Defines the OpenStack Config loud key in your config file, +#: typically in $HOME/.config/openstack/clouds.yaml. That configuration #: will determine where the examples will be run and what resource defaults #: will be used to run the examples. TEST_CLOUD = os.getenv('OS_TEST_CLOUD', 'devstack-admin') +config = loader.OpenStackConfig() +cloud = openstack.connect(cloud=TEST_CLOUD) class Opts(object): @@ -42,13 +44,7 @@ def __init__(self, cloud_name='devstack-admin', debug=False): def _get_resource_value(resource_key, default): - try: - return cloud.config['example'][resource_key] - except KeyError: - return default - -config = occ.OpenStackConfig() -cloud = openstack.connect(cloud=TEST_CLOUD) + return config.get_extra_config('example').get(resource_key, default) SERVER_NAME = 'openstacksdk-example' IMAGE_NAME = _get_resource_value('image_name', 'cirros-0.3.5-x86_64-disk') @@ -70,7 +66,7 @@ def create_connection_from_config(): def create_connection_from_args(): parser = argparse.ArgumentParser() - config = occ.OpenStackConfig() + config = loader.OpenStackConfig() config.register_argparse_arguments(parser, sys.argv[1:]) args = parser.parse_args() return openstack.connect(config=config.get_one(argparse=args)) diff --git a/openstack/tests/examples/__init__.py b/openstack/tests/functional/examples/__init__.py similarity index 100% rename from openstack/tests/examples/__init__.py rename to openstack/tests/functional/examples/__init__.py diff --git a/openstack/tests/examples/test_compute.py b/openstack/tests/functional/examples/test_compute.py similarity index 94% rename from openstack/tests/examples/test_compute.py rename to openstack/tests/functional/examples/test_compute.py index 127960898..188c77495 100644 --- a/openstack/tests/examples/test_compute.py +++ b/openstack/tests/functional/examples/test_compute.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base +from openstack.tests.functional import base from examples.compute import create from examples.compute import delete @@ -21,7 +21,7 @@ from examples.network import list as network_list -class TestCompute(base.TestCase): +class TestCompute(base.BaseFunctionalTest): """Test the compute examples The purpose of these tests is to ensure the examples run without erring diff --git a/openstack/tests/examples/test_identity.py b/openstack/tests/functional/examples/test_identity.py similarity index 93% rename from openstack/tests/examples/test_identity.py rename to openstack/tests/functional/examples/test_identity.py index 0255f1103..8db8bd7ea 100644 --- a/openstack/tests/examples/test_identity.py +++ b/openstack/tests/functional/examples/test_identity.py @@ -10,13 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base +from openstack.tests.functional import base from examples import connect from examples.identity import list as identity_list -class TestIdentity(base.TestCase): +class TestIdentity(base.BaseFunctionalTest): """Test the identity examples The purpose of these tests is to ensure the examples run without erring diff --git a/openstack/tests/examples/test_image.py b/openstack/tests/functional/examples/test_image.py similarity index 92% rename from openstack/tests/examples/test_image.py rename to openstack/tests/functional/examples/test_image.py index b1e421da8..117b9db3f 100644 --- a/openstack/tests/examples/test_image.py +++ b/openstack/tests/functional/examples/test_image.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base +from openstack.tests.functional import base from examples import connect from examples.image import create as image_create @@ -18,7 +18,7 @@ from examples.image import list as image_list -class TestImage(base.TestCase): +class TestImage(base.BaseFunctionalTest): """Test the image examples The purpose of these tests is to ensure the examples run without erring diff --git a/openstack/tests/examples/test_network.py b/openstack/tests/functional/examples/test_network.py similarity index 94% rename from openstack/tests/examples/test_network.py rename to openstack/tests/functional/examples/test_network.py index 349d1f726..ef99eedae 100644 --- a/openstack/tests/examples/test_network.py +++ b/openstack/tests/functional/examples/test_network.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base +from openstack.tests.functional import base from examples import connect from examples.network import create as network_create @@ -19,7 +19,7 @@ from examples.network import list as network_list -class TestNetwork(base.TestCase): +class TestNetwork(base.BaseFunctionalTest): """Test the network examples The purpose of these tests is to ensure the examples run without erring From 4f47eb50ea1134b7b5cca5cc333f0104b2722abd Mon Sep 17 00:00:00 2001 From: Reedip Date: Tue, 5 Sep 2017 02:53:51 +0000 Subject: [PATCH 1990/3836] Add support for dns-domain Change-Id: Ibd43b2af77a24783df1b0f8848aece712f5e0cb3 Partial-Bug: #1714878 --- openstack/network/v2/port.py | 2 ++ openstack/tests/unit/network/v2/test_port.py | 2 ++ .../add-dns-domain-support-for-port-3fa4568330dda07e.yaml | 5 +++++ 3 files changed, 9 insertions(+) create mode 100644 releasenotes/notes/add-dns-domain-support-for-port-3fa4568330dda07e.yaml diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index 1356df49d..40f8fa4ab 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -79,6 +79,8 @@ class Port(resource.Resource, tag.TagMixin): device_owner = resource.Body('device_owner') #: DNS assignment for the port. dns_assignment = resource.Body('dns_assignment') + #: DNS domain assigned to the port. + dns_domain = resource.Body('dns_domain') #: DNS name for the port. dns_name = resource.Body('dns_name') #: Extra DHCP options. diff --git a/openstack/tests/unit/network/v2/test_port.py b/openstack/tests/unit/network/v2/test_port.py index 2bfbcdac3..6270ff811 100644 --- a/openstack/tests/unit/network/v2/test_port.py +++ b/openstack/tests/unit/network/v2/test_port.py @@ -29,6 +29,7 @@ 'device_id': '9', 'device_owner': '10', 'dns_assignment': [{'11': 11}], + 'dns_domain': 'a11', 'dns_name': '12', 'extra_dhcp_opts': [{'13': 13}], 'fixed_ips': [{'14': '14'}], @@ -110,6 +111,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['device_id'], sot.device_id) self.assertEqual(EXAMPLE['device_owner'], sot.device_owner) self.assertEqual(EXAMPLE['dns_assignment'], sot.dns_assignment) + self.assertEqual(EXAMPLE['dns_domain'], sot.dns_domain) self.assertEqual(EXAMPLE['dns_name'], sot.dns_name) self.assertEqual(EXAMPLE['extra_dhcp_opts'], sot.extra_dhcp_opts) self.assertEqual(EXAMPLE['fixed_ips'], sot.fixed_ips) diff --git a/releasenotes/notes/add-dns-domain-support-for-port-3fa4568330dda07e.yaml b/releasenotes/notes/add-dns-domain-support-for-port-3fa4568330dda07e.yaml new file mode 100644 index 000000000..7c24608b4 --- /dev/null +++ b/releasenotes/notes/add-dns-domain-support-for-port-3fa4568330dda07e.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + ``dns_domain`` attribute support has been added to the network + port resource From bdb49fcf4b32d2b3bd3b12cf335b306d2033220c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 23 Feb 2018 09:57:08 -0600 Subject: [PATCH 1991/3836] Shift tag resource definition to TagMixin The TagMixin class defines things needed for resources that support tags. Currently the use of this involves a copy-pasta'd chunk (complete with mis-spelling) to each class that's using the mixin. Move the tags resource to the Mixin and remove it from the indivdual classes so that we don't have to copy/paste the chunk. Also, fix the mis-spelling. Change-Id: If4942cb0e58e0fb51125c53cf1cfae9d65c57cd0 --- openstack/network/v2/network.py | 3 --- openstack/network/v2/port.py | 3 --- openstack/network/v2/router.py | 3 --- openstack/network/v2/subnet.py | 3 --- openstack/network/v2/subnet_pool.py | 3 --- openstack/network/v2/tag.py | 5 +++++ 6 files changed, 5 insertions(+), 15 deletions(-) diff --git a/openstack/network/v2/network.py b/openstack/network/v2/network.py index e2141613d..f9067e57b 100644 --- a/openstack/network/v2/network.py +++ b/openstack/network/v2/network.py @@ -114,9 +114,6 @@ class Network(resource.Resource, tag.TagMixin): updated_at = resource.Body('updated_at') #: Indicates the VLAN transparency mode of the network is_vlan_transparent = resource.Body('vlan_transparent', type=bool) - #: A list of assocaited tags - #: *Type: list of tag strings* - tags = resource.Body('tags', type=list, default=[]) class DHCPAgentHostingNetwork(Network): diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index 2a5ad4b0c..5c762ef07 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -129,6 +129,3 @@ class Port(resource.Resource, tag.TagMixin): trunk_details = resource.Body('trunk_details', type=dict) #: Timestamp when the port was last updated. updated_at = resource.Body('updated_at') - #: A list of assocaited tags - #: *Type: list of tag strings* - tags = resource.Body('tags', type=list, default=[]) diff --git a/openstack/network/v2/router.py b/openstack/network/v2/router.py index 4c123777a..b5f82e791 100644 --- a/openstack/network/v2/router.py +++ b/openstack/network/v2/router.py @@ -76,9 +76,6 @@ class Router(resource.Resource, tag.TagMixin): status = resource.Body('status') #: Timestamp when the router was created. updated_at = resource.Body('updated_at') - #: A list of assocaited tags - #: *Type: list of tag strings* - tags = resource.Body('tags', type=list, default=[]) def add_interface(self, session, **body): """Add an internal interface to a logical router. diff --git a/openstack/network/v2/subnet.py b/openstack/network/v2/subnet.py index 0dbf88001..47c58afb2 100644 --- a/openstack/network/v2/subnet.py +++ b/openstack/network/v2/subnet.py @@ -89,6 +89,3 @@ class Subnet(resource.Resource, tag.TagMixin): 'use_default_subnetpool', type=bool ) - #: A list of assocaited tags - #: *Type: list of tag strings* - tags = resource.Body('tags', type=list, default=[]) diff --git a/openstack/network/v2/subnet_pool.py b/openstack/network/v2/subnet_pool.py index 49e51e60b..593cda01e 100644 --- a/openstack/network/v2/subnet_pool.py +++ b/openstack/network/v2/subnet_pool.py @@ -79,6 +79,3 @@ class SubnetPool(resource.Resource, tag.TagMixin): revision_number = resource.Body('revision_number', type=int) #: Timestamp when the subnet pool was last updated. updated_at = resource.Body('updated_at') - #: A list of assocaited tags - #: *Type: list of tag strings* - tags = resource.Body('tags', type=list, default=[]) diff --git a/openstack/network/v2/tag.py b/openstack/network/v2/tag.py index 83ed190f3..f59717b28 100644 --- a/openstack/network/v2/tag.py +++ b/openstack/network/v2/tag.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import resource from openstack import utils @@ -22,6 +23,10 @@ class TagMixin(object): 'not_any_tags': 'not-tags-any', } + #: A list of associated tags + #: *Type: list of tag strings* + tags = resource.Body('tags', type=list, default=[]) + def set_tags(self, session, tags): url = utils.urljoin(self.base_path, self.id, 'tags') session.put(url, From ecf07fb4efce928fe560f44920a8fd2f705664f1 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Sat, 24 Feb 2018 16:42:44 +0100 Subject: [PATCH 1992/3836] Fix coverage running The post coverage job fails, fix invocation - this is not shade but openstacksdk. Prior to this change, running tox -e cover resulted in: ... Coverage.py warning: Module shade was never imported. (module-not-imported) Coverage.py warning: No data was collected. (no-data-collected) Change-Id: I920fee18083d41d0938bbe19b4b7cf7759aece0c --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 31396e3a3..76012d59b 100644 --- a/tox.ini +++ b/tox.ini @@ -60,7 +60,7 @@ commands = [testenv:cover] setenv = {[testenv]setenv} - PYTHON=coverage run --source shade --parallel-mode + PYTHON=coverage run --source openstack --parallel-mode commands = stestr run {posargs} coverage combine From 9dd436f5136fab5cc21025cf652d1e0495e8d713 Mon Sep 17 00:00:00 2001 From: melissaml Date: Tue, 27 Feb 2018 13:37:35 +0800 Subject: [PATCH 1993/3836] Update the invalid url in pages Change-Id: I3d681495086fec4fb1eb92d2966be54934af94d5 --- doc/source/user/multi-cloud-demo.rst | 6 +++--- openstack/cloud/openstackcloud.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/source/user/multi-cloud-demo.rst b/doc/source/user/multi-cloud-demo.rst index 461af139b..9fd8c6c91 100644 --- a/doc/source/user/multi-cloud-demo.rst +++ b/doc/source/user/multi-cloud-demo.rst @@ -175,7 +175,7 @@ or system-wide: `/etc/openstack/clouds.yaml`. Information in your homedir, if it exists, takes precedence. Full docs on `clouds.yaml` are at -https://docs.openstack.org/developer/os-client-config/ +https://docs.openstack.org/os-client-config/latest/ What about Mac and Windows? =========================== @@ -221,7 +221,7 @@ Simple example of a clouds.yaml * Config for a named `cloud` "my-citycloud" * Reference a well-known "named" profile: `citycloud` * `os-client-config` has a built-in list of profiles at - https://docs.openstack.org/developer/os-client-config/vendor-support.html + https://docs.openstack.org/os-client-config/latest/user/vendor-support.html * Vendor profiles contain various advanced config * `cloud` name can match `profile` name (using different names for clarity) @@ -611,7 +611,7 @@ Cleanup Script Normalization ============= -* https://docs.openstack.org/developer/shade/model.html#image +* https://docs.openstack.org/shade/latest/user/model.html#image * doc/source/examples/normalization.py .. code:: python diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 4354f6ed2..1957be7ce 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -73,7 +73,7 @@ DEFAULT_SERVER_AGE = 5 DEFAULT_PORT_AGE = 5 DEFAULT_FLOAT_AGE = 5 -_OCC_DOC_URL = "https://docs.openstack.org/developer/os-client-config" +_OCC_DOC_URL = "https://docs.openstack.org/os-client-config/latest/" OBJECT_CONTAINER_ACLS = { From 5359232288e60c40e2d8cce23b84b3b615d01f6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Tue, 27 Feb 2018 22:00:35 +0100 Subject: [PATCH 1994/3836] Functional test for set_tags on Neutron resources This patch adds functional test to set/get/delete tags on Neutron's resources for which tags are already supported by OpenStack SDK. Resources for which test is added: * network * port * router * subnet * subnet pool Change-Id: I15917f49a3896f1da4aec42eade03b5c36e8b895 --- .../tests/functional/network/v2/test_network.py | 12 ++++++++++++ openstack/tests/functional/network/v2/test_port.py | 12 ++++++++++++ openstack/tests/functional/network/v2/test_router.py | 12 ++++++++++++ openstack/tests/functional/network/v2/test_subnet.py | 12 ++++++++++++ .../tests/functional/network/v2/test_subnet_pool.py | 12 ++++++++++++ 5 files changed, 60 insertions(+) diff --git a/openstack/tests/functional/network/v2/test_network.py b/openstack/tests/functional/network/v2/test_network.py index 835512151..18234f6d0 100644 --- a/openstack/tests/functional/network/v2/test_network.py +++ b/openstack/tests/functional/network/v2/test_network.py @@ -79,3 +79,15 @@ def test_get(self): def test_list(self): names = [o.name for o in self.conn.network.networks()] self.assertIn(self.NAME, names) + + def test_set_tags(self): + sot = self.conn.network.get_network(self.ID) + self.assertEqual([], sot.tags) + + self.conn.network.set_tags(sot, ['blue']) + sot = self.conn.network.get_network(self.ID) + self.assertEqual(['blue'], sot.tags) + + self.conn.network.set_tags(sot, []) + sot = self.conn.network.get_network(self.ID) + self.assertEqual([], sot.tags) diff --git a/openstack/tests/functional/network/v2/test_port.py b/openstack/tests/functional/network/v2/test_port.py index 7f5514669..346e919f7 100644 --- a/openstack/tests/functional/network/v2/test_port.py +++ b/openstack/tests/functional/network/v2/test_port.py @@ -80,3 +80,15 @@ def test_update(self): sot = self.conn.network.update_port(self.PORT_ID, name=self.UPDATE_NAME) self.assertEqual(self.UPDATE_NAME, sot.name) + + def test_set_tags(self): + sot = self.conn.network.get_port(self.PORT_ID) + self.assertEqual([], sot.tags) + + self.conn.network.set_tags(sot, ['blue']) + sot = self.conn.network.get_port(self.PORT_ID) + self.assertEqual(['blue'], sot.tags) + + self.conn.network.set_tags(sot, []) + sot = self.conn.network.get_port(self.PORT_ID) + self.assertEqual([], sot.tags) diff --git a/openstack/tests/functional/network/v2/test_router.py b/openstack/tests/functional/network/v2/test_router.py index a42bcf686..285bd6feb 100644 --- a/openstack/tests/functional/network/v2/test_router.py +++ b/openstack/tests/functional/network/v2/test_router.py @@ -52,3 +52,15 @@ def test_list(self): def test_update(self): sot = self.conn.network.update_router(self.ID, name=self.UPDATE_NAME) self.assertEqual(self.UPDATE_NAME, sot.name) + + def test_set_tags(self): + sot = self.conn.network.get_router(self.ID) + self.assertEqual([], sot.tags) + + self.conn.network.set_tags(sot, ['blue']) + sot = self.conn.network.get_router(self.ID) + self.assertEqual(['blue'], sot.tags) + + self.conn.network.set_tags(sot, []) + sot = self.conn.network.get_router(self.ID) + self.assertEqual([], sot.tags) diff --git a/openstack/tests/functional/network/v2/test_subnet.py b/openstack/tests/functional/network/v2/test_subnet.py index c6ad4f986..abf8ccb12 100644 --- a/openstack/tests/functional/network/v2/test_subnet.py +++ b/openstack/tests/functional/network/v2/test_subnet.py @@ -79,3 +79,15 @@ def test_update(self): sot = self.conn.network.update_subnet(self.SUB_ID, name=self.UPDATE_NAME) self.assertEqual(self.UPDATE_NAME, sot.name) + + def test_set_tags(self): + sot = self.conn.network.get_subnet(self.SUB_ID) + self.assertEqual([], sot.tags) + + self.conn.network.set_tags(sot, ['blue']) + sot = self.conn.network.get_subnet(self.SUB_ID) + self.assertEqual(['blue'], sot.tags) + + self.conn.network.set_tags(sot, []) + sot = self.conn.network.get_subnet(self.SUB_ID) + self.assertEqual([], sot.tags) diff --git a/openstack/tests/functional/network/v2/test_subnet_pool.py b/openstack/tests/functional/network/v2/test_subnet_pool.py index ebb4cd708..d238d0e9d 100644 --- a/openstack/tests/functional/network/v2/test_subnet_pool.py +++ b/openstack/tests/functional/network/v2/test_subnet_pool.py @@ -74,3 +74,15 @@ def test_update(self): self.SUBNET_POOL_ID, name=self.SUBNET_POOL_NAME_UPDATED) self.assertEqual(self.SUBNET_POOL_NAME_UPDATED, sot.name) + + def test_set_tags(self): + sot = self.conn.network.get_subnet_pool(self.SUBNET_POOL_ID) + self.assertEqual([], sot.tags) + + self.conn.network.set_tags(sot, ['blue']) + sot = self.conn.network.get_subnet_pool(self.SUBNET_POOL_ID) + self.assertEqual(['blue'], sot.tags) + + self.conn.network.set_tags(sot, []) + sot = self.conn.network.get_subnet_pool(self.SUBNET_POOL_ID) + self.assertEqual([], sot.tags) From d1dc2ee649def9892c7d270105b9e517d56e144d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 2 Mar 2018 14:17:31 -0500 Subject: [PATCH 1995/3836] Temporarily disable osc-functional-devstack-tips There is a deeper issue related to versions and sibling installation in the period between a release and the first release in the next cycle on the master branch. A fix is in progress for that, but it will take a few more days to get it in place. In the meantime, the workaround is actually to just cut a release of openstacksdk. The floating ip patch (next) is a pretty important bug, so disable the osc-functional-devstack-tips job for a second so that we can land the floating ip fix. Then we can cut a new release and re-enable the job. A revert of this patch follows. Change-Id: Idec38e3017db57ea56b3bce860a261abee7076d7 --- .zuul.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 5375ee356..68772fb1d 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -220,7 +220,8 @@ - openstacksdk-functional-devstack-magnum: voting: false - openstacksdk-functional-devstack-python3 - - osc-functional-devstack-tips + - osc-functional-devstack-tips: + voting: false - neutron-grenade gate: jobs: @@ -229,5 +230,4 @@ sphinx_python: python3 - openstacksdk-functional-devstack - openstacksdk-functional-devstack-python3 - - osc-functional-devstack-tips - neutron-grenade From 08e9f6ee6715d8dbf2fc370263784658869770cb Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 28 Feb 2018 20:55:38 +0000 Subject: [PATCH 1996/3836] Fix private_v4 selection related to floating ip matching The change to look for floating ips first when looking for the public address exposed a latent bug in the private ip finding code. When we get the mac address of the floating ip, we then look for the corresponding fixed ip to return for private_v4. However, we weren't specifying fixed before, so we were just getting the first one that matched. That *happened* to be the fixed ip by accident. Add in ext_tag='fixed' so that we look for the matching MAC from a fixed interface. We have to also add a second pass through the loop without the fixed tag, as old nova network dicts do not have the fixed/floating tag like that. Includes a test which shows the breakage. Story: 2001619 Change-Id: I60562a99f78c0c363f49106c285935448f804084 --- openstack/cloud/meta.py | 10 + openstack/tests/unit/cloud/test_shade.py | 190 ++++++++++++++++++ ...-ip-private-matching-84e369eee380a185.yaml | 6 + 3 files changed, 206 insertions(+) create mode 100644 releasenotes/notes/fix-floating-ip-private-matching-84e369eee380a185.yaml diff --git a/openstack/cloud/meta.py b/openstack/cloud/meta.py index 30728a207..6b5fd65cb 100644 --- a/openstack/cloud/meta.py +++ b/openstack/cloud/meta.py @@ -126,6 +126,16 @@ def get_server_private_ip(server, cloud=None): # and possibly pre-configured network name if cloud: int_nets = cloud.get_internal_ipv4_networks() + for int_net in int_nets: + int_ip = get_server_ip( + server, key_name=int_net['name'], + ext_tag='fixed', + cloud_public=not cloud.private, + mac_addr=fip_mac) + if int_ip is not None: + return int_ip + # Try a second time without the fixed tag. This is for old nova-network + # results that do not have the fixed/floating tag. for int_net in int_nets: int_ip = get_server_ip( server, key_name=int_net['name'], diff --git a/openstack/tests/unit/cloud/test_shade.py b/openstack/tests/unit/cloud/test_shade.py index b0e2309f7..d64a1baa1 100644 --- a/openstack/tests/unit/cloud/test_shade.py +++ b/openstack/tests/unit/cloud/test_shade.py @@ -159,6 +159,196 @@ def test_list_servers(self): self.assert_calls() + def test_list_server_private_ip(self): + self.has_neutron = True + fake_server = { + "OS-EXT-STS:task_state": None, + "addresses": { + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:b4:a3:07", + "version": 4, + "addr": "10.4.0.13", + "OS-EXT-IPS:type": "fixed" + }, { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:b4:a3:07", + "version": 4, + "addr": "89.40.216.229", + "OS-EXT-IPS:type": "floating" + }]}, + "links": [ + { + "href": "http://example.com/images/95e4c4", + "rel": "self" + }, { + "href": "http://example.com/images/95e4c4", + "rel": "bookmark" + } + ], + "image": { + "id": "95e4c449-8abf-486e-97d9-dc3f82417d2d", + "links": [ + { + "href": "http://example.com/images/95e4c4", + "rel": "bookmark" + } + ] + }, + "OS-EXT-STS:vm_state": "active", + "OS-SRV-USG:launched_at": "2018-03-01T02:44:50.000000", + "flavor": { + "id": "3bd99062-2fe8-4eac-93f0-9200cc0f97ae", + "links": [ + { + "href": "http://example.com/flavors/95e4c4", + "rel": "bookmark" + } + ] + }, + "id": "97fe35e9-756a-41a2-960a-1d057d2c9ee4", + "security_groups": [{"name": "default"}], + "user_id": "c17534835f8f42bf98fc367e0bf35e09", + "OS-DCF:diskConfig": "MANUAL", + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "OS-EXT-AZ:availability_zone": "nova", + "metadata": {}, + "status": "ACTIVE", + "updated": "2018-03-01T02:44:51Z", + "hostId": "", + "OS-SRV-USG:terminated_at": None, + "key_name": None, + "name": "mttest", + "created": "2018-03-01T02:44:46Z", + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", + "os-extended-volumes:volumes_attached": [], + "config_drive": "" + } + fake_networks = {"networks": [ + { + "status": "ACTIVE", + "router:external": True, + "availability_zone_hints": [], + "availability_zones": ["nova"], + "description": None, + "subnets": [ + "df3e17fa-a4b2-47ae-9015-bc93eb076ba2", + "6b0c3dc9-b0b8-4d87-976a-7f2ebf13e7ec", + "fc541f48-fc7f-48c0-a063-18de6ee7bdd7" + ], + "shared": False, + "tenant_id": "a564613210ee43708b8a7fc6274ebd63", + "tags": [], + "ipv6_address_scope": "9f03124f-89af-483a-b6fd-10f08079db4d", + "mtu": 1550, + "is_default": False, + "admin_state_up": True, + "revision_number": 0, + "ipv4_address_scope": None, + "port_security_enabled": True, + "project_id": "a564613210ee43708b8a7fc6274ebd63", + "id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", + "name": "ext-net" + }, { + "status": "ACTIVE", + "router:external": False, + "availability_zone_hints": [], + "availability_zones": ["nova"], + "description": "", + "subnets": ["f0ad1df5-53ee-473f-b86b-3604ea5591e9"], + "shared": False, + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", + "created_at": "2016-10-22T13:46:26Z", + "tags": [], + "ipv6_address_scope": None, + "updated_at": "2016-10-22T13:46:26Z", + "admin_state_up": True, + "mtu": 1500, + "revision_number": 0, + "ipv4_address_scope": None, + "port_security_enabled": True, + "project_id": "65222a4d09ea4c68934fa1028c77f394", + "id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", + "name": "private" + }]} + fake_subnets = { + "subnets": [ + { + "service_types": [], + "description": "", + "enable_dhcp": True, + "tags": [], + "network_id": "827c6bb6-492f-4168-9577-f3a131eb29e8", + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", + "created_at": "2017-06-12T13:23:57Z", + "dns_nameservers": [], + "updated_at": "2017-06-12T13:23:57Z", + "gateway_ip": "10.24.4.1", + "ipv6_ra_mode": None, + "allocation_pools": [ + { + "start": "10.24.4.2", + "end": "10.24.4.254" + }], + "host_routes": [], + "revision_number": 0, + "ip_version": 4, + "ipv6_address_mode": None, + "cidr": "10.24.4.0/24", + "project_id": "65222a4d09ea4c68934fa1028c77f394", + "id": "3f0642d9-4644-4dff-af25-bcf64f739698", + "subnetpool_id": None, + "name": "foo_subnet" + }, { + "service_types": [], + "description": "", + "enable_dhcp": True, + "tags": [], + "network_id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", + "created_at": "2016-10-22T13:46:26Z", + "dns_nameservers": ["89.36.90.101", "89.36.90.102"], + "updated_at": "2016-10-22T13:46:26Z", + "gateway_ip": "10.4.0.1", + "ipv6_ra_mode": None, + "allocation_pools": [ + { + "start": "10.4.0.2", + "end": "10.4.0.200" + }], + "host_routes": [], + "revision_number": 0, + "ip_version": 4, + "ipv6_address_mode": None, + "cidr": "10.4.0.0/24", + "project_id": "65222a4d09ea4c68934fa1028c77f394", + "id": "f0ad1df5-53ee-473f-b86b-3604ea5591e9", + "subnetpool_id": None, + "name": "private-subnet-ipv4" + }]} + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [fake_server]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json=fake_networks), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets.json']), + json=fake_subnets) + ]) + + r = self.cloud.get_server('97fe35e9-756a-41a2-960a-1d057d2c9ee4') + + self.assertEqual('10.4.0.13', r['private_v4']) + + self.assert_calls() + def test_list_servers_all_projects(self): '''This test verifies that when list_servers is called with `all_projects=True` that it passes `all_tenants=True` to nova.''' diff --git a/releasenotes/notes/fix-floating-ip-private-matching-84e369eee380a185.yaml b/releasenotes/notes/fix-floating-ip-private-matching-84e369eee380a185.yaml new file mode 100644 index 000000000..6ff00ef27 --- /dev/null +++ b/releasenotes/notes/fix-floating-ip-private-matching-84e369eee380a185.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixed an issue where an optimization in the logic to find floating ips + first when looking for public ip addresses broke finding the correct + private address. From 1d2753e0b8408f9a28ad5de3c8e83b30e721342b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 3 Mar 2018 08:40:45 -0600 Subject: [PATCH 1997/3836] Run os-client-config tests on sdk changes If os-client-config is going to be a thin shim, we need to make sure we don't break its unit tests. Depends-On: https://review.openstack.org/549473 Change-Id: I8ba1ab1cc7e430181463b4de5e0664970061afd0 --- .zuul.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.zuul.yaml b/.zuul.yaml index 68772fb1d..19fef2feb 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -208,6 +208,7 @@ templates: - openstacksdk-functional-tips - openstacksdk-tox-tips + - os-client-config-tox-tips - osc-tox-unit-tips - shade-functional-tips - shade-tox-tips From 2eec5ba20011fc1c4f07fd3e5f2a3e69cbf89347 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sun, 4 Mar 2018 10:27:18 +0000 Subject: [PATCH 1998/3836] Updated from global requirements Change-Id: I77c0aebc1502afeb5bf4507c5d76cd8a6c7f535c --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index b7c4a0ad0..7a4f9fede 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,19 +2,19 @@ # 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 -PyYAML>=3.10 # MIT +PyYAML>=3.12 # MIT appdirs>=1.3.0 # MIT License requestsexceptions>=1.2.0 # Apache-2.0 jsonpatch!=1.20,>=1.16 # BSD six>=1.10.0 # MIT -os-service-types>=1.1.0 # Apache-2.0 +os-service-types>=1.2.0 # Apache-2.0 keystoneauth1>=3.4.0 # Apache-2.0 deprecation>=1.0 # Apache-2.0 munch>=2.1.0 # MIT decorator>=3.4.0 # BSD jmespath>=0.9.0 # MIT -ipaddress>=1.0.16;python_version<'3.3' # PSF +ipaddress>=1.0.17;python_version<'3.3' # PSF futures>=3.0.0;python_version=='2.7' or python_version=='2.6' # BSD iso8601>=0.1.11 # MIT netifaces>=0.10.4 # MIT From 3ed046ed1aaf8b27a2a25ed8e4e199214f3315cf Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 2 Mar 2018 10:46:49 +0100 Subject: [PATCH 1999/3836] Prepare for os-client-config wrapper In order to remove the second copy of the code that is in os-client-config, we need to be able to set a few class/module names to allow openstacksdk methods to return the appropriate os-client-config wrapper subclasses when called via os-client-config. Also remove a function we had intended to remove back in 2015 that should not be used by anyone. Change-Id: Ie279c543fb2142c101f03dcdd45314bbc098ebc4 --- openstack/config/cloud_region.py | 21 +++++- openstack/config/defaults.py | 5 +- openstack/config/exceptions.py | 0 openstack/config/loader.py | 74 ++++++++++------------ openstack/tests/unit/config/test_config.py | 8 --- 5 files changed, 54 insertions(+), 54 deletions(-) delete mode 100644 openstack/config/exceptions.py diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index dcecdb00f..4977e356d 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -231,6 +231,23 @@ def get_auth(self): """Return a keystoneauth plugin from the auth credentials.""" return self._auth + def insert_user_agent(self): + """Set sdk information into the user agent of the Session. + + .. warning:: + This method is here to be used by os-client-config. It exists + as a hook point so that os-client-config can provice backwards + compatibility and still be in the User Agent for people using + os-client-config directly. + + Normal consumers of SDK should use app_name and app_version. However, + if someone else writes a subclass of + :class:`~openstack.config.cloud_region.CloudRegion` it may be + desirable. + """ + self._keystone_session.additional_user_agent.append( + ('openstacksdk', openstack_version.__version__)) + def get_session(self): """Return a keystoneauth session based on the auth credentials.""" if self._keystone_session is None: @@ -252,9 +269,7 @@ def get_session(self): cert=cert, timeout=self.config['api_timeout'], discovery_cache=self._discovery_cache) - if hasattr(self._keystone_session, 'additional_user_agent'): - self._keystone_session.additional_user_agent.append( - ('openstacksdk', openstack_version.__version__)) + self.insert_user_agent() # Using old keystoneauth with new os-client-config fails if # we pass in app_name and app_version. Those are not essential, # nor a reason to bump our minimum, so just test for the session diff --git a/openstack/config/defaults.py b/openstack/config/defaults.py index 1231cce92..1e7c7f4a0 100644 --- a/openstack/config/defaults.py +++ b/openstack/config/defaults.py @@ -22,7 +22,8 @@ _defaults_lock = threading.Lock() -def get_defaults(): +# json_path argument is there for os-client-config +def get_defaults(json_path=_json_path): global _defaults if _defaults is not None: return _defaults.copy() @@ -44,7 +45,7 @@ def get_defaults(): cert=None, key=None, ) - with open(_json_path, 'r') as json_file: + with open(json_path, 'r') as json_file: updates = json.load(json_file) if updates is not None: tmp_defaults.update(updates) diff --git a/openstack/config/exceptions.py b/openstack/config/exceptions.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 99ac1b27b..aa766dd3c 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -72,19 +72,6 @@ FORMAT_EXCLUSIONS = frozenset(['password']) -# NOTE(dtroyer): This turns out to be not the best idea so let's move -# overriding defaults to a kwarg to OpenStackConfig.__init__() -# Remove this sometime in June 2015 once OSC is comfortably -# changed-over and global-defaults is updated. -def set_default(key, value): - warnings.warn( - "Use of set_default() is deprecated. Defaults should be set with the " - "`override_defaults` parameter of OpenStackConfig." - ) - defaults.get_defaults() # make sure the dict is initialized - defaults._defaults[key] = value - - def get_boolean(value): if value is None: return False @@ -95,30 +82,6 @@ def get_boolean(value): return False -def _get_os_environ(envvar_prefix=None): - ret = defaults.get_defaults() - if not envvar_prefix: - # This makes the or below be OS_ or OS_ which is a no-op - envvar_prefix = 'OS_' - environkeys = [k for k in os.environ.keys() - if (k.startswith('OS_') or k.startswith(envvar_prefix)) - and not k.startswith('OS_TEST') # infra CI var - and not k.startswith('OS_STD') # oslotest var - and not k.startswith('OS_LOG') # oslotest var - ] - for k in environkeys: - newkey = k.split('_', 1)[-1].lower() - ret[newkey] = os.environ[k] - # If the only environ keys are selectors or behavior modification, don't - # return anything - selectors = set([ - 'OS_CLOUD', 'OS_REGION_NAME', - 'OS_CLIENT_CONFIG_FILE', 'OS_CLIENT_SECURE_FILE', 'OS_CLOUD_NAME']) - if set(environkeys) - selectors: - return ret - return None - - def _merge_clouds(old_dict, new_dict): """Like dict.update, except handling nested dicts.""" ret = old_dict.copy() @@ -179,6 +142,12 @@ def _fix_argv(argv): class OpenStackConfig(object): + # These two attribute are to allow os-client-config to plumb in its + # local versions for backwards compat. + # They should not be used by anyone else. + _cloud_region_class = cloud_region.CloudRegion + _defaults_module = defaults + def __init__(self, config_files=None, vendor_files=None, override_defaults=None, force_ipv4=None, envvar_prefix=None, secure_files=None, @@ -208,7 +177,7 @@ def __init__(self, config_files=None, vendor_files=None, if secure_file_override: self._secure_files.insert(0, secure_file_override) - self.defaults = defaults.get_defaults() + self.defaults = self._defaults_module.get_defaults() if override_defaults: self.defaults.update(override_defaults) @@ -262,7 +231,7 @@ def __init__(self, config_files=None, vendor_files=None, self.default_cloud = self._get_envvar('OS_CLOUD') if load_envvars: - envvars = _get_os_environ(envvar_prefix=envvar_prefix) + envvars = self._get_os_environ(envvar_prefix=envvar_prefix) if envvars: self.cloud_config['clouds'][self.envvar_key] = envvars if not self.default_cloud: @@ -323,6 +292,29 @@ def __init__(self, config_files=None, vendor_files=None, # password = self._pw_callback(prompt="Password: ") self._pw_callback = pw_func + def _get_os_environ(self, envvar_prefix=None): + ret = self._defaults_module.get_defaults() + if not envvar_prefix: + # This makes the or below be OS_ or OS_ which is a no-op + envvar_prefix = 'OS_' + environkeys = [k for k in os.environ.keys() + if (k.startswith('OS_') or k.startswith(envvar_prefix)) + and not k.startswith('OS_TEST') # infra CI var + and not k.startswith('OS_STD') # oslotest var + and not k.startswith('OS_LOG') # oslotest var + ] + for k in environkeys: + newkey = k.split('_', 1)[-1].lower() + ret[newkey] = os.environ[k] + # If the only environ keys are selectors or behavior modification, + # don't return anything + selectors = set([ + 'OS_CLOUD', 'OS_REGION_NAME', + 'OS_CLIENT_CONFIG_FILE', 'OS_CLIENT_SECURE_FILE', 'OS_CLOUD_NAME']) + if set(environkeys) - selectors: + return ret + return None + def _get_envvar(self, key, default=None): if not self._load_envvars: return default @@ -1111,7 +1103,7 @@ def get_one( cloud_name = '' else: cloud_name = str(cloud) - return cloud_region.CloudRegion( + return self._cloud_region_class( name=cloud_name, region_name=config['region_name'], config=config, @@ -1208,7 +1200,7 @@ def get_one_cloud_osc( cloud_name = '' else: cloud_name = str(cloud) - return cloud_region.CloudRegion( + return self._cloud_region_class( name=cloud_name, region_name=config['region_name'], config=self._normalize_keys(config), diff --git a/openstack/tests/unit/config/test_config.py b/openstack/tests/unit/config/test_config.py index 5ab01e2de..cdc569a94 100644 --- a/openstack/tests/unit/config/test_config.py +++ b/openstack/tests/unit/config/test_config.py @@ -24,7 +24,6 @@ from openstack import config from openstack.config import cloud_region from openstack.config import defaults -from openstack.config import loader from openstack import exceptions from openstack.tests.unit.config import base @@ -1020,13 +1019,6 @@ def test_set_no_default(self): self._assert_cloud_details(cc) self.assertEqual('password', cc.auth_type) - def test_set_default_before_init(self): - loader.set_default('identity_api_version', '4') - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) - cc = c.get_one(cloud='_test-cloud_', argparse=None) - self.assertEqual('4', cc.identity_api_version) - class TestBackwardsCompatibility(base.TestCase): From 7b6d71d206712ab7b5f6ab1ae5eaa85b1926b4fd Mon Sep 17 00:00:00 2001 From: Dongcan Ye Date: Fri, 23 Feb 2018 10:56:30 +0000 Subject: [PATCH 2000/3836] Network: Add tag support for QoS policy This patch adds set tags operation and query parameters for QoS policy. Meanwhile overwrite set_tags for QoS policy, The resource type for QoS policy tagging should be 'policies'. Change-Id: I7f7025c3c86fbbe3bfae992ed23352e37257ea64 Partial-Bug: #1750987 Closes-Bug: #1751984 --- openstack/network/v2/qos_policy.py | 11 ++++++++++- .../tests/functional/network/v2/test_qos_policy.py | 12 ++++++++++++ openstack/tests/unit/network/v2/test_qos_policy.py | 4 +++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/openstack/network/v2/qos_policy.py b/openstack/network/v2/qos_policy.py index 406285785..b425ab7b3 100644 --- a/openstack/network/v2/qos_policy.py +++ b/openstack/network/v2/qos_policy.py @@ -11,10 +11,12 @@ # under the License. from openstack.network import network_service +from openstack.network.v2 import tag from openstack import resource +from openstack import utils -class QoSPolicy(resource.Resource): +class QoSPolicy(resource.Resource, tag.TagMixin): resource_key = 'policy' resources_key = 'policies' base_path = '/qos/policies' @@ -31,6 +33,7 @@ class QoSPolicy(resource.Resource): 'name', 'description', 'is_default', project_id='tenant_id', is_shared='shared', + **tag.TagMixin._tag_query_parameters ) # Properties @@ -50,3 +53,9 @@ class QoSPolicy(resource.Resource): is_shared = resource.Body('shared', type=bool) #: List of QoS rules applied to this QoS policy. rules = resource.Body('rules') + + def set_tags(self, session, tags): + url = utils.urljoin('/policies', self.id, 'tags') + session.put(url, json={'tags': tags}) + self._body.attributes.update({'tags': tags}) + return self diff --git a/openstack/tests/functional/network/v2/test_qos_policy.py b/openstack/tests/functional/network/v2/test_qos_policy.py index abe85de2b..df081b8ed 100644 --- a/openstack/tests/functional/network/v2/test_qos_policy.py +++ b/openstack/tests/functional/network/v2/test_qos_policy.py @@ -63,3 +63,15 @@ def test_update(self): self.QOS_POLICY_ID, name=self.QOS_POLICY_NAME_UPDATED) self.assertEqual(self.QOS_POLICY_NAME_UPDATED, sot.name) + + def test_set_tags(self): + sot = self.conn.network.get_qos_policy(self.QOS_POLICY_ID) + self.assertEqual([], sot.tags) + + self.conn.network.set_tags(sot, ['blue']) + sot = self.conn.network.get_qos_policy(self.QOS_POLICY_ID) + self.assertEqual(['blue'], sot.tags) + + self.conn.network.set_tags(sot, []) + sot = self.conn.network.get_qos_policy(self.QOS_POLICY_ID) + self.assertEqual([], sot.tags) diff --git a/openstack/tests/unit/network/v2/test_qos_policy.py b/openstack/tests/unit/network/v2/test_qos_policy.py index 525f8f33d..11304db34 100644 --- a/openstack/tests/unit/network/v2/test_qos_policy.py +++ b/openstack/tests/unit/network/v2/test_qos_policy.py @@ -22,7 +22,8 @@ 'shared': True, 'tenant_id': '2', 'rules': [uuid.uuid4().hex], - 'is_default': False + 'is_default': False, + 'tags': ['3'] } @@ -49,3 +50,4 @@ def test_make_it(self): self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) self.assertEqual(EXAMPLE['rules'], sot.rules) self.assertEqual(EXAMPLE['is_default'], sot.is_default) + self.assertEqual(EXAMPLE['tags'], sot.tags) From 15a740594d21b418299fbab8ec6410fdef0f34d5 Mon Sep 17 00:00:00 2001 From: Dongcan Ye Date: Fri, 23 Feb 2018 10:47:00 +0000 Subject: [PATCH 2001/3836] Network: Add tag support for security group This patch adds set tags operation and query parameters for security group. Change-Id: I80fd6494fd4b86f5ace81297ec4617d94bcf5d06 Partial-Bug: #1750983 --- openstack/network/v2/security_group.py | 4 +++- .../functional/network/v2/test_security_group.py | 12 ++++++++++++ .../tests/unit/network/v2/test_security_group.py | 2 ++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/openstack/network/v2/security_group.py b/openstack/network/v2/security_group.py index d4c956263..6a87eabc5 100644 --- a/openstack/network/v2/security_group.py +++ b/openstack/network/v2/security_group.py @@ -11,10 +11,11 @@ # under the License. from openstack.network import network_service +from openstack.network.v2 import tag from openstack import resource -class SecurityGroup(resource.Resource): +class SecurityGroup(resource.Resource, tag.TagMixin): resource_key = 'security_group' resources_key = 'security_groups' base_path = '/security-groups' @@ -30,6 +31,7 @@ class SecurityGroup(resource.Resource): _query_mapping = resource.QueryParameters( 'description', 'name', project_id='tenant_id', + **tag.TagMixin._tag_query_parameters ) # Properties diff --git a/openstack/tests/functional/network/v2/test_security_group.py b/openstack/tests/functional/network/v2/test_security_group.py index 95120d03d..5bcdb3126 100644 --- a/openstack/tests/functional/network/v2/test_security_group.py +++ b/openstack/tests/functional/network/v2/test_security_group.py @@ -45,3 +45,15 @@ def test_get(self): def test_list(self): names = [o.name for o in self.conn.network.security_groups()] self.assertIn(self.NAME, names) + + def test_set_tags(self): + sot = self.conn.network.get_security_group(self.ID) + self.assertEqual([], sot.tags) + + self.conn.network.set_tags(sot, ['blue']) + sot = self.conn.network.get_security_group(self.ID) + self.assertEqual(['blue'], sot.tags) + + self.conn.network.set_tags(sot, []) + sot = self.conn.network.get_security_group(self.ID) + self.assertEqual([], sot.tags) diff --git a/openstack/tests/unit/network/v2/test_security_group.py b/openstack/tests/unit/network/v2/test_security_group.py index 73528f0c5..500f090d1 100644 --- a/openstack/tests/unit/network/v2/test_security_group.py +++ b/openstack/tests/unit/network/v2/test_security_group.py @@ -58,6 +58,7 @@ 'security_group_rules': RULES, 'tenant_id': '4', 'updated_at': '2016-10-14T12:16:57.233772', + 'tags': ['5'] } @@ -87,3 +88,4 @@ def test_make_it(self): self.assertEqual(dict, type(sot.security_group_rules[0])) self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) + self.assertEqual(EXAMPLE['tags'], sot.tags) From 1a0ec3e776fa03d27b77945c480baea4ff22b510 Mon Sep 17 00:00:00 2001 From: Dongcan Ye Date: Fri, 23 Feb 2018 10:31:07 +0000 Subject: [PATCH 2002/3836] Network: Add tag support for floating ip This patch adds set tags operation and query parameters for floating ip. Change-Id: I1f70d3f80f6be5a09d0982ed7525e13ab99bf7b0 Partial-Bug: #1750985 --- openstack/network/v2/floating_ip.py | 6 ++++-- .../tests/functional/network/v2/test_floating_ip.py | 12 ++++++++++++ openstack/tests/unit/network/v2/test_floating_ip.py | 4 +++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/openstack/network/v2/floating_ip.py b/openstack/network/v2/floating_ip.py index 9ae201b84..2a4a6e1f3 100644 --- a/openstack/network/v2/floating_ip.py +++ b/openstack/network/v2/floating_ip.py @@ -11,10 +11,11 @@ # under the License. from openstack.network import network_service +from openstack.network.v2 import tag from openstack import resource -class FloatingIP(resource.Resource): +class FloatingIP(resource.Resource, tag.TagMixin): name_attribute = "floating_ip_address" resource_name = "floating ip" resource_key = 'floatingip' @@ -33,7 +34,8 @@ class FloatingIP(resource.Resource): 'description', 'fixed_ip_address', 'floating_ip_address', 'floating_network_id', 'port_id', 'router_id', 'status', 'subnet_id', - project_id='tenant_id') + project_id='tenant_id', + **tag.TagMixin._tag_query_parameters) # Properties #: Timestamp at which the floating IP was created. diff --git a/openstack/tests/functional/network/v2/test_floating_ip.py b/openstack/tests/functional/network/v2/test_floating_ip.py index 8e0fb5afa..8ad4cbc43 100644 --- a/openstack/tests/functional/network/v2/test_floating_ip.py +++ b/openstack/tests/functional/network/v2/test_floating_ip.py @@ -148,3 +148,15 @@ def test_update(self): sot = self.conn.network.update_ip(self.FIP.id, port_id=self.PORT_ID) self.assertEqual(self.PORT_ID, sot.port_id) self.assertEqual(self.FIP.id, sot.id) + + def test_set_tags(self): + sot = self.conn.network.get_ip(self.FIP.id) + self.assertEqual([], sot.tags) + + self.conn.network.set_tags(sot, ['blue']) + sot = self.conn.network.get_ip(self.FIP.id) + self.assertEqual(['blue'], sot.tags) + + self.conn.network.set_tags(sot, []) + sot = self.conn.network.get_ip(self.FIP.id) + self.assertEqual([], sot.tags) diff --git a/openstack/tests/unit/network/v2/test_floating_ip.py b/openstack/tests/unit/network/v2/test_floating_ip.py index 92dc36512..d520004fd 100644 --- a/openstack/tests/unit/network/v2/test_floating_ip.py +++ b/openstack/tests/unit/network/v2/test_floating_ip.py @@ -31,7 +31,8 @@ 'status': 'ACTIVE', 'revision_number': 12, 'updated_at': '13', - 'subnet_id': '14' + 'subnet_id': '14', + 'tags': ['15', '16'] } @@ -66,6 +67,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['revision_number'], sot.revision_number) self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) self.assertEqual(EXAMPLE['subnet_id'], sot.subnet_id) + self.assertEqual(EXAMPLE['tags'], sot.tags) def test_find_available(self): mock_session = mock.Mock(spec=adapter.Adapter) From ea518083c6afac34b0aff55002f16153feaac6f3 Mon Sep 17 00:00:00 2001 From: Adrian Turjak Date: Thu, 8 Mar 2018 18:52:06 +1300 Subject: [PATCH 2003/3836] Redo role assignment list query filters Unlike all the other keystone filters, role assignment list uses '.' rather than '_' for some values. This now fixes that on our end to correctly remap for the API. Change-Id: Iaef99c856607a6cfec7c3747366671de4232d232 --- openstack/identity/v3/role_assignment.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openstack/identity/v3/role_assignment.py b/openstack/identity/v3/role_assignment.py index 412535f00..a5cc16326 100644 --- a/openstack/identity/v3/role_assignment.py +++ b/openstack/identity/v3/role_assignment.py @@ -24,8 +24,10 @@ class RoleAssignment(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'group_id', 'role_id', 'scope_domain_id', 'scope_project_id', - 'user_id', 'effective', 'include_names', 'include_subtree' + 'effective', 'include_names', 'include_subtree', + role_id='role.id', user_id='user.id', group_id='group.id', + scope_project_id='scope.project.id', scope_domain_id='scope.domain.id', + scope_system='scope.system', ) # Properties From 7a1c3c64f56401a16fa022d1861adb0bbbf1cde7 Mon Sep 17 00:00:00 2001 From: Thomas Herve Date: Wed, 28 Feb 2018 14:59:04 +0100 Subject: [PATCH 2004/3836] Allow not resolving outputs on get stacks Depending on the stack complexity, resolving outputs of a Heat stack can be fairly expensive. The API has a parameter to disable output resolution: this adds this parameter to the get_stack call, and use it in places where outputs are superfluous. This is a copy of shade commit b1bc65c599812a02f6861c1d80c31c3b9be894da. Change-Id: I932e42fe10af7de7d3a08182328ff2c8c70e8f9e --- openstack/cloud/_heat/event_utils.py | 2 +- openstack/cloud/openstackcloud.py | 13 ++-- openstack/tests/unit/cloud/test_stack.py | 77 ++++++++++++++---------- 3 files changed, 54 insertions(+), 38 deletions(-) diff --git a/openstack/cloud/_heat/event_utils.py b/openstack/cloud/_heat/event_utils.py index bceec38af..aa9574232 100644 --- a/openstack/cloud/_heat/event_utils.py +++ b/openstack/cloud/_heat/event_utils.py @@ -86,7 +86,7 @@ def is_stack_event(event): if no_event_polls >= 2: # after 2 polls with no events, fall back to a stack get - stack = cloud.get_stack(stack_name) + stack = cloud.get_stack(stack_name, resolve_outputs=False) stack_status = stack['stack_status'] msg = msg_template % dict( name=stack_name, status=stack_status) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 4354f6ed2..df10b403f 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -1318,7 +1318,7 @@ def delete_stack(self, name_or_id, wait=False): :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call """ - stack = self.get_stack(name_or_id) + stack = self.get_stack(name_or_id, resolve_outputs=False) if stack is None: self.log.debug("Stack %s not found for deleting", name_or_id) return False @@ -1340,7 +1340,7 @@ def delete_stack(self, name_or_id, wait=False): marker=marker) except OpenStackCloudHTTPError: pass - stack = self.get_stack(name_or_id) + stack = self.get_stack(name_or_id, resolve_outputs=False) if stack and stack['stack_status'] == 'DELETE_FAILED': raise OpenStackCloudException( "Failed to delete stack {id}: {reason}".format( @@ -3242,12 +3242,14 @@ def get_floating_ip_by_id(self, id): return self._normalize_floating_ip( self._get_and_munchify('floating_ip', data)) - def get_stack(self, name_or_id, filters=None): + def get_stack(self, name_or_id, filters=None, resolve_outputs=True): """Get exactly one stack. :param name_or_id: Name or ID of the desired stack. :param filters: a dict containing additional filters to use. e.g. {'stack_status': 'CREATE_COMPLETE'} + :param resolve_outputs: If True, then outputs for this + stack will be resolved :returns: a ``munch.Munch`` containing the stack description @@ -3259,8 +3261,11 @@ def _search_one_stack(name_or_id=None, filters=None): # stack names are mandatory and enforced unique in the project # so a StackGet can always be used for name or ID. try: + url = '/stacks/{name_or_id}'.format(name_or_id=name_or_id) + if not resolve_outputs: + url = '{url}?resolve_outputs=False'.format(url=url) data = self._orchestration_client.get( - '/stacks/{name_or_id}'.format(name_or_id=name_or_id), + url, error_message="Error fetching stack") stack = self._get_and_munchify('stack', data) # Treat DELETE_COMPLETE stacks as a NotFound diff --git a/openstack/tests/unit/cloud/test_stack.py b/openstack/tests/unit/cloud/test_stack.py index 049db96f5..08c856daa 100644 --- a/openstack/tests/unit/cloud/test_stack.py +++ b/openstack/tests/unit/cloud/test_stack.py @@ -114,20 +114,22 @@ def test_search_stacks_exception(self): self.cloud.search_stacks() def test_delete_stack(self): + resolve = 'resolve_outputs=False' self.register_uris([ dict(method='GET', - uri='{endpoint}/stacks/{name}'.format( + uri='{endpoint}/stacks/{name}?{resolve}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name), + name=self.stack_name, resolve=resolve), status_code=302, headers=dict( - location='{endpoint}/stacks/{name}/{id}'.format( + location='{endpoint}/stacks/{name}/{id}?{resolve}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name))), + id=self.stack_id, name=self.stack_name, + resolve=resolve))), dict(method='GET', - uri='{endpoint}/stacks/{name}/{id}'.format( + uri='{endpoint}/stacks/{name}/{id}?{resolve}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name), + id=self.stack_id, name=self.stack_name, resolve=resolve), json={"stack": self.stack}), dict(method='DELETE', uri='{endpoint}/stacks/{id}'.format( @@ -138,30 +140,33 @@ def test_delete_stack(self): self.assert_calls() def test_delete_stack_not_found(self): + resolve = 'resolve_outputs=False' self.register_uris([ dict(method='GET', - uri='{endpoint}/stacks/stack_name'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT), + uri='{endpoint}/stacks/stack_name?{resolve}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, resolve=resolve), status_code=404), ]) self.assertFalse(self.cloud.delete_stack('stack_name')) self.assert_calls() def test_delete_stack_exception(self): + resolve = 'resolve_outputs=False' self.register_uris([ dict(method='GET', - uri='{endpoint}/stacks/{id}'.format( + uri='{endpoint}/stacks/{id}?{resolve}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id), + id=self.stack_id, resolve=resolve), status_code=302, headers=dict( - location='{endpoint}/stacks/{name}/{id}'.format( + location='{endpoint}/stacks/{name}/{id}?{resolve}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name))), + id=self.stack_id, name=self.stack_name, + resolve=resolve))), dict(method='GET', - uri='{endpoint}/stacks/{name}/{id}'.format( + uri='{endpoint}/stacks/{name}/{id}?{resolve}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name), + id=self.stack_id, name=self.stack_name, resolve=resolve), json={"stack": self.stack}), dict(method='DELETE', uri='{endpoint}/stacks/{id}'.format( @@ -180,20 +185,23 @@ def test_delete_stack_wait(self): self.stack_id, self.stack_name, status='CREATE_COMPLETE') marker_qs = 'marker={e_id}&sort_dir=asc'.format( e_id=marker_event['id']) + resolve = 'resolve_outputs=False' self.register_uris([ dict(method='GET', - uri='{endpoint}/stacks/{id}'.format( + uri='{endpoint}/stacks/{id}?{resolve}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id), + id=self.stack_id, + resolve=resolve), status_code=302, headers=dict( - location='{endpoint}/stacks/{name}/{id}'.format( + location='{endpoint}/stacks/{name}/{id}?{resolve}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name))), + id=self.stack_id, name=self.stack_name, + resolve=resolve))), dict(method='GET', - uri='{endpoint}/stacks/{name}/{id}'.format( + uri='{endpoint}/stacks/{name}/{id}?{resolve}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name), + id=self.stack_id, name=self.stack_name, resolve=resolve), json={"stack": self.stack}), dict(method='GET', uri='{endpoint}/stacks/{id}/events?{qs}'.format( @@ -218,9 +226,9 @@ def test_delete_stack_wait(self): status='DELETE_COMPLETE'), ]}), dict(method='GET', - uri='{endpoint}/stacks/{id}'.format( + uri='{endpoint}/stacks/{id}?{resolve}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name), + id=self.stack_id, name=self.stack_name, resolve=resolve), status_code=404), ]) @@ -234,20 +242,22 @@ def test_delete_stack_wait_failed(self): self.stack_id, self.stack_name, status='CREATE_COMPLETE') marker_qs = 'marker={e_id}&sort_dir=asc'.format( e_id=marker_event['id']) + resolve = 'resolve_outputs=False' self.register_uris([ dict(method='GET', - uri='{endpoint}/stacks/{id}'.format( + uri='{endpoint}/stacks/{id}?{resolve}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id), + id=self.stack_id, resolve=resolve), status_code=302, headers=dict( - location='{endpoint}/stacks/{name}/{id}'.format( + location='{endpoint}/stacks/{name}/{id}?{resolve}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name))), + id=self.stack_id, name=self.stack_name, + resolve=resolve))), dict(method='GET', - uri='{endpoint}/stacks/{name}/{id}'.format( + uri='{endpoint}/stacks/{name}/{id}?{resolve}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name), + id=self.stack_id, name=self.stack_name, resolve=resolve), json={"stack": self.stack}), dict(method='GET', uri='{endpoint}/stacks/{id}/events?{qs}'.format( @@ -272,18 +282,19 @@ def test_delete_stack_wait_failed(self): status='DELETE_COMPLETE'), ]}), dict(method='GET', - uri='{endpoint}/stacks/{id}'.format( + uri='{endpoint}/stacks/{id}?resolve_outputs=False'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, id=self.stack_id, name=self.stack_name), status_code=302, headers=dict( - location='{endpoint}/stacks/{name}/{id}'.format( + location='{endpoint}/stacks/{name}/{id}?{resolve}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name))), + id=self.stack_id, name=self.stack_name, + resolve=resolve))), dict(method='GET', - uri='{endpoint}/stacks/{name}/{id}'.format( + uri='{endpoint}/stacks/{name}/{id}?{resolve}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name), + id=self.stack_id, name=self.stack_name, resolve=resolve), json={"stack": failed_stack}), ]) From 14e9d86dd0f44bff17c822fa0c0effb1a88b9d15 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 10 Mar 2018 13:50:10 +0000 Subject: [PATCH 2005/3836] Updated from global requirements Change-Id: I150d0f171e6dd2e5502bce1343449712f20f5f02 --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index f1688868f..524ff3472 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,7 +1,7 @@ # 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. -sphinx!=1.6.6,>=1.6.2 # BSD +sphinx!=1.6.6,<1.7.0,>=1.6.2 # BSD docutils>=0.11 # OSI-Approved Open Source, Public Domain openstackdocstheme>=1.18.1 # Apache-2.0 beautifulsoup4>=4.6.0 # MIT From 71188108c4838eed080229e95bc71465dd5455b7 Mon Sep 17 00:00:00 2001 From: wangqi Date: Mon, 12 Mar 2018 06:57:34 +0000 Subject: [PATCH 2006/3836] Replace old http links with the newest https ones in docs Change-Id: I3524a210a8b5d2146378de9f9d88a46b2d7232cb --- CONTRIBUTING.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 57b6bf563..9457276b9 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -21,16 +21,16 @@ Pull requests submitted through GitHub will be ignored. .. seealso:: - * http://wiki.openstack.org/HowToContribute - * http://wiki.openstack.org/CLA + * https://wiki.openstack.org/wiki/HowToContribute + * https://wiki.openstack.org/wiki/CLA -.. _DeveloperWorkflow: http://docs.openstack.org/infra/manual/developers.html#development-workflow +.. _DeveloperWorkflow: https://docs.openstack.org/infra/manual/developers.html#development-workflow Project Hosting Details ------------------------- Project Documentation - http://docs.openstack.org/sdks/python/openstacksdk/ + https://docs.openstack.org/python-openstacksdk/latest/ Bug tracker https://bugs.launchpad.net/python-openstacksdk From 5ca3bf7a4bee87e1ae1f3a3ced930a7dfab4c97e Mon Sep 17 00:00:00 2001 From: Jens Harbott Date: Sun, 11 Mar 2018 17:36:13 +0000 Subject: [PATCH 2007/3836] Fix devstack tests With the change in devstack from neutron-legacy to the new neutron lib setup, we need to tweak things a bit. Include neutron devstack plugin, that would have been needed for some time in order to make the neutron-qos service work. Depend on octavia change to make its devstack plugin compatible. Depends-On: https://review.openstack.org/544281 Change-Id: I023d76c56d9b72e390bf2c1d39ddc27584d5c52a --- .zuul.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index 19fef2feb..d8b12dfec 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -111,7 +111,6 @@ - openstack/octavia vars: devstack_localrc: - Q_SERVICE_PLUGIN_CLASSES: qos Q_ML2_PLUGIN_EXT_DRIVERS: qos,port_security DISABLE_AMP_IMAGE_BUILD: True devstack_local_conf: @@ -126,6 +125,7 @@ certificates: cert_manager: local_cert_manager devstack_plugins: + neutron: https://git.openstack.org/openstack/neutron octavia: https://git.openstack.org/openstack/octavia devstack_services: octavia: true From 8143b28e852ba4c6552f5555dfffe912fd607edd Mon Sep 17 00:00:00 2001 From: Peter BALOGH Date: Mon, 12 Mar 2018 18:36:08 +0100 Subject: [PATCH 2008/3836] Fix TypeError in case of FloatingIP add and remove Change-Id: I97e345e0270bcbd8f3467defd41dd2cadfc80f0f Closes-Bug: #1747275 --- openstack/network/v2/_proxy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 0d5c67a7c..a05f472f0 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -1536,11 +1536,11 @@ def update_port(self, port, **attrs): return self._update(_port.Port, port, **attrs) def add_ip_to_port(self, port, ip): - ip['port_id'] = port.id + ip.port_id = port.id return ip.update(self) def remove_ip_from_port(self, ip): - ip['port_id'] = None + ip.port_id = None return ip.update(self) def get_subnet_ports(self, subnet_id): From ca3c3def347faa4dd610c54753d75b6472ef0457 Mon Sep 17 00:00:00 2001 From: "Yuanbin.Chen" Date: Tue, 20 Mar 2018 10:42:23 +0800 Subject: [PATCH 2009/3836] Fix 'block_store' aliases define error This patch fix volume aliases error, the service type use 'block-storage', but aliases define use 'block_storage'. So, Can't use 'block_store' finish list volumes. Change-Id: I003f7064c13dc907833c2a1e66cc11a823d47be6 Closes-Bug: #1757032 --- openstack/_meta/connection.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openstack/_meta/connection.py b/openstack/_meta/connection.py index eb4e3364c..6511e9683 100644 --- a/openstack/_meta/connection.py +++ b/openstack/_meta/connection.py @@ -67,14 +67,15 @@ def __new__(meta, name, bases, dct): class_name='~openstack.proxy.Proxy', **service) descriptor = desc_class(**descriptor_args) descriptor.__doc__ = doc - dct[service_type.replace('-', '_')] = descriptor + st = service_type.replace('-', '_') + dct[st] = descriptor # Register the descriptor class with every known alias. Don't # add doc strings though - although they are supported, we don't # want to give anybody any bad ideas. Making a second descriptor # does not introduce runtime cost as the descriptors all use # the same _proxies dict on the instance. - for alias_name in _get_aliases(service_type): + for alias_name in _get_aliases(st): if alias_name[-1].isdigit(): continue alias_descriptor = desc_class(**descriptor_args) From 2f61a6757d55c329a3b7e6403fe0a9b5c928643f Mon Sep 17 00:00:00 2001 From: "Yuanbin.Chen" Date: Mon, 19 Mar 2018 21:11:06 +0800 Subject: [PATCH 2010/3836] Use defined version instead of service.version parameter When api_version contains a version that begins with 'v', We should use the defined version parameters. Change-Id: I6be932088b4e622efafb2ee83b87cd929b2525d2 Signed-off-by: Yuanbin.Chen --- openstack/profile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/profile.py b/openstack/profile.py index f19538aae..4c4396c98 100644 --- a/openstack/profile.py +++ b/openstack/profile.py @@ -57,7 +57,7 @@ def _get_config_from_profile(profile, authenticator, **kwargs): if version.startswith('v'): version = version[1:] key = cloud_region._make_key('api_version', service_type) - kwargs[key] = service.version + kwargs[key] = version config_kwargs = config_defaults.get_defaults() config_kwargs.update(kwargs) From c12108e720abe7b00c53248817ecaf27987cb561 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 20 Mar 2018 11:14:03 -0500 Subject: [PATCH 2011/3836] Provide OpenStackConfigException backwards compat osc-lib does a 'import os_client_config.exceptions as occ_exceptions' or 'import openstack.config.exceptions as occ_exceptions' and then try/caches occ_exceptions.OpenStackConfigException. Add openstack.config.exceptions.OpenStackConfigException back as an alias for openstack.exceptions.ConfigException to ease that transition. Closes-Bug: 1757391 Change-Id: Ifaab26e5c71ee3f7268443ca396e3f93b83e2b7f --- openstack/config/exceptions.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 openstack/config/exceptions.py diff --git a/openstack/config/exceptions.py b/openstack/config/exceptions.py new file mode 100644 index 000000000..932de73cf --- /dev/null +++ b/openstack/config/exceptions.py @@ -0,0 +1,17 @@ +# Copyright (c) 2014 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. + +from openstack import exceptions + +OpenStackConfigException = exceptions.ConfigException From 23ed0aa481847a5ae8a742cca8505ebe210897ca Mon Sep 17 00:00:00 2001 From: Adrian Turjak Date: Thu, 22 Mar 2018 16:25:58 +1300 Subject: [PATCH 2012/3836] Add 409 ConflictException Change-Id: I655f1bd462f0a167d384fe469db961332051822c --- openstack/exceptions.py | 9 ++++++++- openstack/tests/unit/cloud/test_domains.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/openstack/exceptions.py b/openstack/exceptions.py index 36718813e..de5b41426 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -118,6 +118,11 @@ class BadRequestException(HttpException): pass +class ConflictException(HttpException): + """HTTP 409 Conflict.""" + pass + + class MethodNotSupported(SDKException): """The resource does not support this operation type.""" def __init__(self, resource, method): @@ -162,7 +167,9 @@ def raise_from_response(response, error_message=None): if response.status_code < 400: return - if response.status_code == 404: + if response.status_code == 409: + cls = ConflictException + elif response.status_code == 404: cls = NotFoundException elif response.status_code == 400: cls = BadRequestException diff --git a/openstack/tests/unit/cloud/test_domains.py b/openstack/tests/unit/cloud/test_domains.py index 9ad408a7e..babdbd395 100644 --- a/openstack/tests/unit/cloud/test_domains.py +++ b/openstack/tests/unit/cloud/test_domains.py @@ -203,7 +203,7 @@ def test_update_domain_exception(self): json=domain_data.json_response, validate=dict(json={'domain': {'enabled': False}}))]) with testtools.ExpectedException( - openstack.cloud.OpenStackCloudHTTPError, + openstack.exceptions.ConflictException, "Error in updating domain %s" % domain_data.domain_id ): self.cloud.delete_domain(domain_data.domain_id) From 1b7e5c05d020e980dec84e4668072bd46f526fa6 Mon Sep 17 00:00:00 2001 From: Adrian Turjak Date: Thu, 22 Mar 2018 16:31:35 +1300 Subject: [PATCH 2013/3836] Fix response always being False From requests.models.Response: Returns True if :attr:`status_code` is less than 400. We're using it wrong, so have changed it to "is not None" Change-Id: I617e55d67d93f1e07f5192ba94dcc0997ba9e12f --- openstack/exceptions.py | 4 ++-- openstack/tests/unit/cloud/test_image.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openstack/exceptions.py b/openstack/exceptions.py index de5b41426..141129a42 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -60,7 +60,7 @@ def __init__(self, message='Error', response=None, # TODO(shade) Remove http_status parameter and the ability for response # to be None once we're not mocking Session everywhere. if not message: - if response: + if response is not None: message = "{name}: {code}".format( name=self.__class__.__name__, code=response.status_code) @@ -72,7 +72,7 @@ def __init__(self, message='Error', response=None, SDKException.__init__(self, message=message) _rex.HTTPError.__init__(self, message, response=response) - if response: + if response is not None: self.request_id = response.headers.get('x-openstack-request-id') self.status_code = response.status_code else: diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 27868b01d..8e08dad70 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -650,7 +650,7 @@ def test_create_image_put_v1_bad_delete( ] mock_image_client.post.return_value = ret mock_image_client.put.side_effect = exc.OpenStackCloudHTTPError( - "Some error", {}) + "Some error") self.assertRaises( exc.OpenStackCloudHTTPError, self._call_create_image, @@ -724,7 +724,7 @@ def test_create_image_put_v2_bad_delete( ] mock_image_client.post.return_value = ret mock_image_client.put.side_effect = exc.OpenStackCloudHTTPError( - "Some error", {}) + "Some error") self.assertRaises( exc.OpenStackCloudHTTPError, self._call_create_image, From 20442a9f63962cb96a995667623f24f03536c85f Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 23 Mar 2018 01:51:33 +0000 Subject: [PATCH 2014/3836] Updated from global requirements Change-Id: I5a73cf3edd601deba87e2bdc84ff88a8b9541197 --- doc/requirements.txt | 2 +- test-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 524ff3472..d3fbf2210 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,7 +1,7 @@ # 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. -sphinx!=1.6.6,<1.7.0,>=1.6.2 # BSD +sphinx!=1.6.6,!=1.6.7,>=1.6.2 # BSD docutils>=0.11 # OSI-Approved Open Source, Public Domain openstackdocstheme>=1.18.1 # Apache-2.0 beautifulsoup4>=4.6.0 # MIT diff --git a/test-requirements.txt b/test-requirements.txt index cede4be1d..54ba19d09 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,7 +8,7 @@ jsonschema<3.0.0,>=2.6.0 # MIT mock>=2.0.0 # BSD python-subunit>=1.0.0 # Apache-2.0/BSD oslotest>=3.2.0 # Apache-2.0 -requests-mock>=1.1.0 # Apache-2.0 +requests-mock>=1.2.0 # Apache-2.0 stestr>=1.0.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD From ad545ef35c3da083bb13b4d7dac1620fddface29 Mon Sep 17 00:00:00 2001 From: Kengo Takahara Date: Fri, 23 Mar 2018 18:22:09 +0900 Subject: [PATCH 2015/3836] Add instance_ha service This patch targets to support instance_ha service for masakari by openstacksdk. Change-Id: I9b2d140065390d94dda532c39777cf691775e21e --- openstack/instance_ha/__init__.py | 0 openstack/instance_ha/instance_ha_service.py | 26 +++ openstack/instance_ha/v1/__init__.py | 0 openstack/instance_ha/v1/_proxy.py | 216 ++++++++++++++++++ openstack/instance_ha/v1/host.py | 67 ++++++ openstack/instance_ha/v1/notification.py | 64 ++++++ openstack/instance_ha/v1/segment.py | 61 +++++ openstack/tests/unit/instance_ha/__init__.py | 0 .../instance_ha/test_instance_ha_service.py | 30 +++ .../tests/unit/instance_ha/v1/__init__.py | 0 .../tests/unit/instance_ha/v1/test_host.py | 76 ++++++ .../unit/instance_ha/v1/test_notification.py | 77 +++++++ .../tests/unit/instance_ha/v1/test_proxy.py | 86 +++++++ .../tests/unit/instance_ha/v1/test_segment.py | 63 +++++ openstack/tests/unit/test_proxy_base.py | 3 +- 15 files changed, 768 insertions(+), 1 deletion(-) create mode 100644 openstack/instance_ha/__init__.py create mode 100644 openstack/instance_ha/instance_ha_service.py create mode 100644 openstack/instance_ha/v1/__init__.py create mode 100644 openstack/instance_ha/v1/_proxy.py create mode 100644 openstack/instance_ha/v1/host.py create mode 100644 openstack/instance_ha/v1/notification.py create mode 100644 openstack/instance_ha/v1/segment.py create mode 100644 openstack/tests/unit/instance_ha/__init__.py create mode 100644 openstack/tests/unit/instance_ha/test_instance_ha_service.py create mode 100644 openstack/tests/unit/instance_ha/v1/__init__.py create mode 100644 openstack/tests/unit/instance_ha/v1/test_host.py create mode 100644 openstack/tests/unit/instance_ha/v1/test_notification.py create mode 100644 openstack/tests/unit/instance_ha/v1/test_proxy.py create mode 100644 openstack/tests/unit/instance_ha/v1/test_segment.py diff --git a/openstack/instance_ha/__init__.py b/openstack/instance_ha/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/instance_ha/instance_ha_service.py b/openstack/instance_ha/instance_ha_service.py new file mode 100644 index 000000000..228d9933a --- /dev/null +++ b/openstack/instance_ha/instance_ha_service.py @@ -0,0 +1,26 @@ +# Copyright(c) 2018 Nippon Telegraph and Telephone 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. + +from openstack import service_filter + + +class InstanceHaService(service_filter.ServiceFilter): + """The HA service.""" + + valid_versions = [service_filter.ValidVersion('v1')] + + def __init__(self, version=None): + """Create an ha service.""" + super(InstanceHaService, self).__init__(service_type='ha', + version=version) diff --git a/openstack/instance_ha/v1/__init__.py b/openstack/instance_ha/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/instance_ha/v1/_proxy.py b/openstack/instance_ha/v1/_proxy.py new file mode 100644 index 000000000..e283e33ec --- /dev/null +++ b/openstack/instance_ha/v1/_proxy.py @@ -0,0 +1,216 @@ +# Copyright(c) 2018 Nippon Telegraph and Telephone 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. + +from openstack import exceptions +from openstack.instance_ha.v1 import host as _host +from openstack.instance_ha.v1 import notification as _notification +from openstack.instance_ha.v1 import segment as _segment +from openstack import proxy +from openstack import resource + + +class Proxy(proxy.Proxy): + """Proxy class for ha resource handling. + + Create method for each action of each API. + """ + + def notifications(self, **query): + """Return a generator of notifications. + + :param kwargs \*\*query: Optional query parameters to be sent to + limit the notifications being returned. + :returns: A generator of notifications + """ + return self._list(_notification.Notification, paginated=False, **query) + + def get_notification(self, notification): + """Get a single notification. + + :param notification: The value can be the ID of a notification or a + :class: + `~masakariclient.sdk.ha.v1 + .notification.Notification` instance. + :returns: One :class:`~masakariclient.sdk.ha.v1 + .notification.Notification` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_notification.Notification, notification) + + def create_notification(self, **attrs): + """Create a new notification. + + :param dict attrs: Keyword arguments which will be used to create + a :class: + `masakariclient.sdk.ha.v1 + .notification.Notification`, + comprised of the propoerties on the Notification + class. + :returns: The result of notification creation + :rtype: :class: `masakariclient.sdk.ha.v1 + .notification.Notification` + """ + return self._create(_notification.Notification, **attrs) + + def segments(self, **query): + """Return a generator of segments. + + :param kwargs \*\*query: Optional query parameters to be sent to + limit the segments being returned. + :returns: A generator of segments + """ + return self._list(_segment.Segment, paginated=False, **query) + + def get_segment(self, segment): + """Get a single segment. + + :param segment: The value can be the ID of a segment or a + :class: + `~masakariclient.sdk.ha.v1.segment.Segment` instance. + :returns: One :class:`~masakariclient.sdk.ha.v1.segment.Segment` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_segment.Segment, segment) + + def create_segment(self, **attrs): + """Create a new segment. + + :param dict attrs: Keyword arguments which will be used to create + a :class: + `masakariclient.sdk.ha.v1.segment.Segment`, + comprised of the propoerties on the Segment class. + :returns: The result of segment creation + :rtype: :class: `masakariclient.sdk.ha.v1.segment.Segment` + """ + return self._create(_segment.Segment, **attrs) + + def update_segment(self, segment, **attrs): + """Update a segment. + + :param segment: The value can be the ID of a segment or a + :class: + `~masakariclient.sdk.ha.v1.segment.Segment` instance. + :param dict attrs: Keyword arguments which will be used to update + a :class: + `masakariclient.sdk.ha.v1.segment.Segment`, + comprised of the propoerties on the Segment class. + :returns: The updated segment. + :rtype: :class: `masakariclient.sdk.ha.v1.segment.Segment` + """ + return self._update(_segment.Segment, segment, **attrs) + + def delete_segment(self, segment, ignore_missing=True): + """Delete a segment. + + :param segment: + The value can be either the ID of a segment or a + :class:`~masakariclient.sdk.ha.v1.segment.Segment` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the segment does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent segment. + :returns: ``None`` + """ + return self._delete(_segment.Segment, segment, + ignore_missing=ignore_missing) + + def hosts(self, segment_id, **query): + """Return a generator of hosts. + + :param segment_id: The ID of a failover segment. + :param kwargs \*\*query: Optional query parameters to be sent to + limit the hosts being returned. + + :returns: A generator of hosts + """ + return self._list(_host.Host, segment_id=segment_id, paginated=False, + **query) + + def create_host(self, segment_id, **attrs): + """Create a new host. + + :param segment_id: The ID of a failover segment. + :param dict attrs: Keyword arguments which will be used to create + a :class: `masakariclient.sdk.ha.v1.host.Host`, + comprised of the propoerties on the Host class. + + :returns: The results of host creation + """ + return self._create(_host.Host, segment_id=segment_id, **attrs) + + def get_host(self, host, segment_id=None): + """Get a single host. + + :param segment_id: The ID of a failover segment. + :param host: The value can be the ID of a host or a :class: + `~masakariclient.sdk.ha.v1.host.Host` instance. + + :returns: One :class:`~masakariclient.sdk.ha.v1.host.Host` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + :raises: :class:`~openstack.exceptions.InvalidRequest` + when segment_id is None. + """ + if segment_id is None: + raise exceptions.InvalidRequest("'segment_id' must be specified.") + + host_id = resource.Resource._get_id(host) + return self._get(_host.Host, host_id, segment_id=segment_id) + + def update_host(self, host, segment_id, **attrs): + """Update the host. + + :param segment_id: The ID of a failover segment. + :param host: The value can be the ID of a host or a :class: + `~masakariclient.sdk.ha.v1.host.Host` instance. + :param dict attrs: The attributes to update on the host represented. + + :returns: The updated host + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + :raises: :class:`~openstack.exceptions.InvalidRequest` + when segment_id is None. + """ + host_id = resource.Resource._get_id(host) + return self._update(_host.Host, host_id, segment_id=segment_id, + **attrs) + + def delete_host(self, host, segment_id=None, ignore_missing=True): + """Delete the host. + + :param segment_id: The ID of a failover segment. + :param host: The value can be the ID of a host or a :class: + `~masakariclient.sdk.ha.v1.host.Host` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the host does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent host. + + :returns: ``None`` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + :raises: :class:`~openstack.exceptions.InvalidRequest` + when segment_id is None. + + """ + if segment_id is None: + raise exceptions.InvalidRequest("'segment_id' must be specified.") + + host_id = resource.Resource._get_id(host) + return self._delete(_host.Host, host_id, segment_id=segment_id, + ignore_missing=ignore_missing) diff --git a/openstack/instance_ha/v1/host.py b/openstack/instance_ha/v1/host.py new file mode 100644 index 000000000..63cc681f1 --- /dev/null +++ b/openstack/instance_ha/v1/host.py @@ -0,0 +1,67 @@ +# Copyright(c) 2018 Nippon Telegraph and Telephone 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. + +from openstack.instance_ha import instance_ha_service +from openstack import resource + + +class Host(resource.Resource): + resource_key = "host" + resources_key = "hosts" + base_path = "/segments/%(segment_id)s/hosts" + service = instance_ha_service.InstanceHaService() + + # capabilities + # 1] GET /v1/segments//hosts + # 2] GET /v1/segments//hosts/ + # 3] POST /v1/segments//hosts + # 4] PUT /v1/segments//hosts + # 5] DELETE /v1/segments//hosts + allow_list = True + allow_get = True + allow_create = True + allow_update = True + allow_delete = True + + # Properties + # Refer "https://github.com/openstack/masakari/blob/ + # master/masakari/api/openstack/ha/schemas/hosts.py" + # for properties of host API + + #: A ID of representing this host + id = resource.URI("id") + #: A Uuid of representing this host + uuid = resource.Body("uuid") + #: A failover segment ID of this host(in URI) + segment_id = resource.URI("segment_id") + #: A created time of this host + created_at = resource.Body("created_at") + #: A latest updated time of this host + updated_at = resource.Body("updated_at") + #: A name of this host + name = resource.Body("name") + #: A type of this host + type = resource.Body("type") + #: A control attributes of this host + control_attributes = resource.Body("control_attributes") + #: A maintenance status of this host + on_maintenance = resource.Body("on_maintenance") + #: A reservation status of this host + reserved = resource.Body("reserved") + #: A failover segment ID of this host(in Body) + failover_segment_id = resource.Body("failover_segment_id") + + _query_mapping = resource.QueryParameters( + "sort_key", "sort_dir", failover_segment_id="failover_segment_id", + type="type", on_maintenance="on_maintenance", reserved="reserved") diff --git a/openstack/instance_ha/v1/notification.py b/openstack/instance_ha/v1/notification.py new file mode 100644 index 000000000..ae58e699a --- /dev/null +++ b/openstack/instance_ha/v1/notification.py @@ -0,0 +1,64 @@ +# Copyright(c) 2018 Nippon Telegraph and Telephone 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. + +from openstack.instance_ha import instance_ha_service +from openstack import resource + + +class Notification(resource.Resource): + resource_key = "notification" + resources_key = "notifications" + base_path = "/notifications" + service = instance_ha_service.InstanceHaService() + + # capabilities + # 1] GET /v1/notifications + # 2] GET /v1/notifications/ + # 3] POST /v1/notifications + allow_list = True + allow_get = True + allow_create = True + allow_update = False + allow_delete = False + + # Properties + # Refer "https://github.com/openstack/masakari/tree/ + # master/masakari/api/openstack/ha/schemas/notificaions.py" + # for properties of notifications API + + #: A ID of representing this notification. + id = resource.Body("id") + #: A Uuid of representing this notification. + notification_uuid = resource.Body("notification_uuid") + #: A created time of representing this notification. + created_at = resource.Body("created_at") + #: A latest updated time of representing this notification. + updated_at = resource.Body("updated_at") + #: The type of failure. Valuse values include ''COMPUTE_HOST'', + #: ''VM'', ''PROCESS'' + type = resource.Body("type") + #: The hostname of this notification. + hostname = resource.Body("hostname") + #: The status for this notitication. + status = resource.Body("status") + #: The generated_time for this notitication. + generated_time = resource.Body("generated_time") + #: The payload of this notification. + payload = resource.Body("payload") + #: The source host uuid of this notification. + source_host_uuid = resource.Body("source_host_uuid") + + _query_mapping = resource.QueryParameters( + "sort_key", "sort_dir", source_host_uuid="source_host_uuid", + type="type", status="status", generated_since="generated-since") diff --git a/openstack/instance_ha/v1/segment.py b/openstack/instance_ha/v1/segment.py new file mode 100644 index 000000000..ee9bd43bf --- /dev/null +++ b/openstack/instance_ha/v1/segment.py @@ -0,0 +1,61 @@ +# Copyright(c) 2018 Nippon Telegraph and Telephone 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. + +from openstack.instance_ha import instance_ha_service +from openstack import resource + + +class Segment(resource.Resource): + resource_key = "segment" + resources_key = "segments" + base_path = "/segments" + service = instance_ha_service.InstanceHaService() + + # capabilities + # 1] GET /v1/segments + # 2] GET /v1/segments/ + # 3] POST /v1/segments + # 4] PUT /v1/segments/ + # 5] DELETE /v1/segments/ + allow_list = True + allow_get = True + allow_create = True + allow_update = True + allow_delete = True + + # Properties + # Refer "https://github.com/openstack/masakari/tree/ + # master/masakari/api/openstack/ha/schemas" + # for properties of each API + + #: A ID of representing this segment. + id = resource.Body("id") + #: A Uuid of representing this segment. + uuid = resource.Body("uuid") + #: A created time of representing this segment. + created_at = resource.Body("created_at") + #: A latest updated time of representing this segment. + updated_at = resource.Body("updated_at") + #: The name of this segment. + name = resource.Body("name") + #: The description of this segment. + description = resource.Body("description") + #: The recovery method of this segment. + recovery_method = resource.Body("recovery_method") + #: The service type of this segment. + service_type = resource.Body("service_type") + + _query_mapping = resource.QueryParameters( + "sort_key", "sort_dir", recovery_method="recovery_method", + service_type="service_type") diff --git a/openstack/tests/unit/instance_ha/__init__.py b/openstack/tests/unit/instance_ha/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/unit/instance_ha/test_instance_ha_service.py b/openstack/tests/unit/instance_ha/test_instance_ha_service.py new file mode 100644 index 000000000..fa46986dd --- /dev/null +++ b/openstack/tests/unit/instance_ha/test_instance_ha_service.py @@ -0,0 +1,30 @@ +# Copyright(c) 2018 Nippon Telegraph and Telephone 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. + +from openstack.tests.unit import base + +from openstack.instance_ha import instance_ha_service + + +class TestInstanceHaService(base.TestCase): + + def test_service(self): + sot = instance_ha_service.InstanceHaService() + self.assertEqual("ha", sot.service_type) + self.assertEqual("public", sot.interface) + self.assertIsNone(sot.region) + self.assertIsNone(sot.service_name) + self.assertEqual(1, len(sot.valid_versions)) + self.assertEqual("v1", sot.valid_versions[0].module) + self.assertEqual("v1", sot.valid_versions[0].path) diff --git a/openstack/tests/unit/instance_ha/v1/__init__.py b/openstack/tests/unit/instance_ha/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/unit/instance_ha/v1/test_host.py b/openstack/tests/unit/instance_ha/v1/test_host.py new file mode 100644 index 000000000..4aea38b56 --- /dev/null +++ b/openstack/tests/unit/instance_ha/v1/test_host.py @@ -0,0 +1,76 @@ +# Copyright(c) 2018 Nippon Telegraph and Telephone 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. + +from openstack.instance_ha.v1 import host +from openstack.tests.unit import base + +FAKE_ID = "1c2f1795-ce78-4d4c-afd0-ce141fdb3952" +FAKE_UUID = "11f7597f-87d2-4057-b754-ba611f989807" +FAKE_HOST_ID = "c27dec16-ed4d-4ebe-8e77-f1e28ec32417" +FAKE_CONTROL_ATTRIBUTES = { + "mcastaddr": "239.255.1.1", + "mcastport": "5405" +} +HOST = { + "id": FAKE_ID, + "uuid": FAKE_UUID, + "segment_id": FAKE_HOST_ID, + "created_at": "2018-03-22T00:00:00.000000", + "updated_at": "2018-03-23T00:00:00.000000", + "name": "my_host", + "type": "pacemaker", + "control_attributes": FAKE_CONTROL_ATTRIBUTES, + "on_maintenance": False, + "reserved": False, + "failover_segment_id": FAKE_HOST_ID +} + + +class TestHost(base.TestCase): + + def test_basic(self): + sot = host.Host(HOST) + self.assertEqual("host", sot.resource_key) + self.assertEqual("hosts", sot.resources_key) + self.assertEqual("/segments/%(segment_id)s/hosts", sot.base_path) + self.assertEqual("ha", sot.service.service_type) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_delete) + + self.assertDictEqual({"failover_segment_id": "failover_segment_id", + "limit": "limit", + "marker": "marker", + "on_maintenance": "on_maintenance", + "reserved": "reserved", + "sort_dir": "sort_dir", + "sort_key": "sort_key", + "type": "type"}, + sot._query_mapping._mapping) + + def test_create(self): + sot = host.Host(**HOST) + self.assertEqual(HOST["id"], sot.id) + self.assertEqual(HOST["uuid"], sot.uuid) + self.assertEqual(HOST["segment_id"], sot.segment_id) + self.assertEqual(HOST["created_at"], sot.created_at) + self.assertEqual(HOST["updated_at"], sot.updated_at) + self.assertEqual(HOST["name"], sot.name) + self.assertEqual(HOST["type"], sot.type) + self.assertEqual(HOST["control_attributes"], sot.control_attributes) + self.assertEqual(HOST["on_maintenance"], sot.on_maintenance) + self.assertEqual(HOST["reserved"], sot.reserved) + self.assertEqual(HOST["failover_segment_id"], sot.failover_segment_id) diff --git a/openstack/tests/unit/instance_ha/v1/test_notification.py b/openstack/tests/unit/instance_ha/v1/test_notification.py new file mode 100644 index 000000000..b18da458c --- /dev/null +++ b/openstack/tests/unit/instance_ha/v1/test_notification.py @@ -0,0 +1,77 @@ +# Copyright(c) 2018 Nippon Telegraph and Telephone 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. + +from openstack.instance_ha.v1 import notification +from openstack.tests.unit import base + +FAKE_ID = "569429e9-7f14-41be-a38e-920277e637db" +FAKE_UUID = "a0e70d3a-b3a2-4616-b65d-a7c03a2c85fc" +FAKE_HOST_UUID = "cad9ff01-c354-4414-ba3c-31b925be67f1" +PAYLOAD = { + "instance_uuid": "4032bc1d-d723-47f6-b5ac-b9b3e6dbb795", + "vir_domain_event": "STOPPED_FAILED", + "event": "LIFECYCLE" +} +NOTIFICATION = { + "id": FAKE_ID, + "notification_uuid": FAKE_UUID, + "created_at": "2018-03-22T00:00:00.000000", + "updated_at": "2018-03-23T00:00:00.000000", + "type": "pacemaker", + "hostname": "fake_host", + "status": "new", + "generated_time": "2018-03-21T00:00:00.000000", + "payload": PAYLOAD, + "source_host_uuid": FAKE_HOST_UUID +} + + +class TestNotification(base.TestCase): + + def test_basic(self): + sot = notification.Notification(NOTIFICATION) + self.assertEqual("notification", sot.resource_key) + self.assertEqual("notifications", sot.resources_key) + self.assertEqual("/notifications", sot.base_path) + self.assertEqual("ha", sot.service.service_type) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_create) + self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_delete) + + self.assertDictEqual({"generated_since": "generated-since", + "limit": "limit", + "marker": "marker", + "sort_dir": "sort_dir", + "sort_key": "sort_key", + "source_host_uuid": "source_host_uuid", + "status": "status", + "type": "type"}, + sot._query_mapping._mapping) + + def test_create(self): + sot = notification.Notification(**NOTIFICATION) + self.assertEqual(NOTIFICATION["id"], sot.id) + self.assertEqual( + NOTIFICATION["notification_uuid"], sot.notification_uuid) + self.assertEqual(NOTIFICATION["created_at"], sot.created_at) + self.assertEqual(NOTIFICATION["updated_at"], sot.updated_at) + self.assertEqual(NOTIFICATION["type"], sot.type) + self.assertEqual(NOTIFICATION["hostname"], sot.hostname) + self.assertEqual(NOTIFICATION["status"], sot.status) + self.assertEqual(NOTIFICATION["generated_time"], sot.generated_time) + self.assertEqual(NOTIFICATION["payload"], sot.payload) + self.assertEqual( + NOTIFICATION["source_host_uuid"], sot.source_host_uuid) diff --git a/openstack/tests/unit/instance_ha/v1/test_proxy.py b/openstack/tests/unit/instance_ha/v1/test_proxy.py new file mode 100644 index 000000000..5e68100f5 --- /dev/null +++ b/openstack/tests/unit/instance_ha/v1/test_proxy.py @@ -0,0 +1,86 @@ +# Copyright(c) 2018 Nippon Telegraph and Telephone 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. + +from openstack.instance_ha.v1 import _proxy +from openstack.instance_ha.v1 import host +from openstack.instance_ha.v1 import notification +from openstack.instance_ha.v1 import segment +from openstack.tests.unit import test_proxy_base + +SEGMENT_ID = "c50b96eb-2a66-40f8-bca8-c5fa90d595c0" +HOST_ID = "52d05e43-d08e-42b8-ae33-e47c8ea2ad47" + + +class TestInstanceHaProxy(test_proxy_base.TestProxyBase): + def setUp(self): + super(TestInstanceHaProxy, self).setUp() + self.proxy = _proxy.Proxy(self.session) + + def test_hosts(self): + self.verify_list(self.proxy.hosts, + host.Host, + method_args=[SEGMENT_ID], + expected_kwargs={"segment_id": SEGMENT_ID}) + + def test_host_get(self): + self.verify_get(self.proxy.get_host, + host.Host, + value=[HOST_ID], + method_kwargs={"segment_id": SEGMENT_ID}, + expected_kwargs={"segment_id": SEGMENT_ID}) + + def test_host_create(self): + self.verify_create(self.proxy.create_host, + host.Host, + method_args=[SEGMENT_ID], + method_kwargs={}, + expected_kwargs={"segment_id": SEGMENT_ID}) + + def test_host_update(self): + self.verify_update(self.proxy.update_host, + host.Host, + method_kwargs={"segment_id": SEGMENT_ID}) + + def test_host_delete(self): + self.verify_delete(self.proxy.delete_host, + host.Host, + True, + method_kwargs={"segment_id": SEGMENT_ID}, + expected_kwargs={"segment_id": SEGMENT_ID}) + + def test_notifications(self): + self.verify_list(self.proxy.notifications, notification.Notification) + + def test_notification_get(self): + self.verify_get(self.proxy.get_notification, + notification.Notification) + + def test_notification_create(self): + self.verify_create(self.proxy.create_notification, + notification.Notification) + + def test_segments(self): + self.verify_list(self.proxy.segments, segment.Segment) + + def test_segment_get(self): + self.verify_get(self.proxy.get_segment, segment.Segment) + + def test_segment_create(self): + self.verify_create(self.proxy.create_segment, segment.Segment) + + def test_segment_update(self): + self.verify_update(self.proxy.update_segment, segment.Segment) + + def test_segment_delete(self): + self.verify_delete(self.proxy.delete_segment, segment.Segment, True) diff --git a/openstack/tests/unit/instance_ha/v1/test_segment.py b/openstack/tests/unit/instance_ha/v1/test_segment.py new file mode 100644 index 000000000..87cf870f1 --- /dev/null +++ b/openstack/tests/unit/instance_ha/v1/test_segment.py @@ -0,0 +1,63 @@ +# Copyright(c) 2018 Nippon Telegraph and Telephone 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. + +from openstack.instance_ha.v1 import segment +from openstack.tests.unit import base + +FAKE_ID = "1c2f1795-ce78-4d4c-afd0-ce141fdb3952" +FAKE_UUID = "11f7597f-87d2-4057-b754-ba611f989807" +SEGMENT = { + "id": FAKE_ID, + "uuid": FAKE_UUID, + "created_at": "2018-03-22T00:00:00.000000", + "updated_at": "2018-03-23T00:00:00.000000", + "name": "my_segment", + "description": "something", + "recovery_method": "auto", + "service_type": "COMPUTE_HOST" +} + + +class TestSegment(base.TestCase): + + def test_basic(self): + sot = segment.Segment(SEGMENT) + self.assertEqual("segment", sot.resource_key) + self.assertEqual("segments", sot.resources_key) + self.assertEqual("/segments", sot.base_path) + self.assertEqual("ha", sot.service.service_type) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_delete) + + self.assertDictEqual({"limit": "limit", + "marker": "marker", + "recovery_method": "recovery_method", + "service_type": "service_type", + "sort_dir": "sort_dir", + "sort_key": "sort_key"}, + sot._query_mapping._mapping) + + def test_create(self): + sot = segment.Segment(**SEGMENT) + self.assertEqual(SEGMENT["id"], sot.id) + self.assertEqual(SEGMENT["uuid"], sot.uuid) + self.assertEqual(SEGMENT["created_at"], sot.created_at) + self.assertEqual(SEGMENT["updated_at"], sot.updated_at) + self.assertEqual(SEGMENT["name"], sot.name) + self.assertEqual(SEGMENT["description"], sot.description) + self.assertEqual(SEGMENT["recovery_method"], sot.recovery_method) + self.assertEqual(SEGMENT["service_type"], sot.service_type) diff --git a/openstack/tests/unit/test_proxy_base.py b/openstack/tests/unit/test_proxy_base.py index ad5261b4f..ed2d79948 100644 --- a/openstack/tests/unit/test_proxy_base.py +++ b/openstack/tests/unit/test_proxy_base.py @@ -212,7 +212,8 @@ def verify_update(self, test_method, resource_type, value=None, mock_method="openstack.proxy.Proxy._update", expected_result="result", path_args=None, **kwargs): method_args = value or ["resource_or_id"] - method_kwargs = {"x": 1, "y": 2, "z": 3} + method_kwargs = kwargs.pop("method_kwargs", {}) + method_kwargs.update({"x": 1, "y": 2, "z": 3}) expected_args = kwargs.pop("expected_args", ["resource_or_id"]) expected_kwargs = method_kwargs.copy() From 5d3d9de9ce371edd9e82b54d8f37af90e0121e4b Mon Sep 17 00:00:00 2001 From: "Yuanbin.Chen" Date: Fri, 23 Mar 2018 21:38:40 +0800 Subject: [PATCH 2016/3836] fix doc title format error This patch fixes the file title format inconsistent. Change-Id: I6ffff6f006cfb5c9b6663713acc55e2b2e30c28a Signed-off-by: Yuanbin.Chen --- CONTRIBUTING.rst | 2 +- SHADE-MERGE-TODO.rst | 2 +- doc/source/user/config/configuration.rst | 6 +++--- doc/source/user/config/index.rst | 6 +++--- doc/source/user/guides/baremetal.rst | 2 +- doc/source/user/guides/clustering.rst | 4 ++-- doc/source/user/guides/clustering/event.rst | 2 +- doc/source/user/guides/clustering/profile.rst | 2 +- doc/source/user/proxies/baremetal.rst | 4 ++-- doc/source/user/proxies/block_storage.rst | 2 +- doc/source/user/proxies/clustering.rst | 2 +- doc/source/user/proxies/load_balancer_v2.rst | 2 +- doc/source/user/resources/baremetal/index.rst | 2 +- doc/source/user/resources/baremetal/v1/chassis.rst | 2 +- doc/source/user/resources/baremetal/v1/driver.rst | 2 +- doc/source/user/resources/baremetal/v1/node.rst | 2 +- doc/source/user/resources/baremetal/v1/port.rst | 2 +- doc/source/user/resources/baremetal/v1/port_group.rst | 2 +- doc/source/user/resources/clustering/v1/cluster.rst | 2 +- doc/source/user/resources/compute/v2/flavor.rst | 2 +- doc/source/user/resources/compute/v2/server.rst | 2 +- doc/source/user/resources/database/index.rst | 2 +- doc/source/user/resources/key_manager/v1/container.rst | 2 +- doc/source/user/resources/load_balancer/v2/l7_rule.rst | 4 ++-- doc/source/user/resources/load_balancer/v2/pool.rst | 2 +- doc/source/user/resources/network/v2/agent.rst | 2 +- doc/source/user/service_filter.rst | 2 +- 27 files changed, 34 insertions(+), 34 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 9457276b9..d21643559 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -27,7 +27,7 @@ Pull requests submitted through GitHub will be ignored. .. _DeveloperWorkflow: https://docs.openstack.org/infra/manual/developers.html#development-workflow Project Hosting Details -------------------------- +----------------------- Project Documentation https://docs.openstack.org/python-openstacksdk/latest/ diff --git a/SHADE-MERGE-TODO.rst b/SHADE-MERGE-TODO.rst index 71be13eaa..baf684be0 100644 --- a/SHADE-MERGE-TODO.rst +++ b/SHADE-MERGE-TODO.rst @@ -1,5 +1,5 @@ Tasks Needed for rationalizing shade and openstacksdk -====================================================== +===================================================== A large portion of the important things have already been done and landed already. For reference, those are: diff --git a/doc/source/user/config/configuration.rst b/doc/source/user/config/configuration.rst index 19460c8dc..dc3b9000b 100644 --- a/doc/source/user/config/configuration.rst +++ b/doc/source/user/config/configuration.rst @@ -1,8 +1,8 @@ .. _openstack-config: -======================================== - Configuring OpenStack SDK Applications -======================================== +====================================== +Configuring OpenStack SDK Applications +====================================== .. _config-environment-variables: diff --git a/doc/source/user/config/index.rst b/doc/source/user/config/index.rst index d09b28351..11637239d 100644 --- a/doc/source/user/config/index.rst +++ b/doc/source/user/config/index.rst @@ -1,6 +1,6 @@ -======================== - Using os-client-config -======================== +====================== +Using os-client-config +====================== .. toctree:: :maxdepth: 2 diff --git a/doc/source/user/guides/baremetal.rst b/doc/source/user/guides/baremetal.rst index 81421ea99..9af1a86d2 100644 --- a/doc/source/user/guides/baremetal.rst +++ b/doc/source/user/guides/baremetal.rst @@ -1,5 +1,5 @@ Using OpenStack Baremetal -=========================== +========================= Before working with the Baremetal service, you'll need to create a connection to your OpenStack cloud by following the :doc:`connect` user diff --git a/doc/source/user/guides/clustering.rst b/doc/source/user/guides/clustering.rst index c0543fd5b..af56db52c 100644 --- a/doc/source/user/guides/clustering.rst +++ b/doc/source/user/guides/clustering.rst @@ -12,9 +12,9 @@ under the License. -================================ +========================== Using OpenStack Clustering -================================ +========================== Before working with the Clustering service, you'll need to create a connection to your OpenStack cloud by following the :doc:`connect` user guide. diff --git a/doc/source/user/guides/clustering/event.rst b/doc/source/user/guides/clustering/event.rst index 5969ec323..753208045 100644 --- a/doc/source/user/guides/clustering/event.rst +++ b/doc/source/user/guides/clustering/event.rst @@ -22,7 +22,7 @@ policies. List Events -~~~~~~~~~~~~ +~~~~~~~~~~~ To examine the list of events: diff --git a/doc/source/user/guides/clustering/profile.rst b/doc/source/user/guides/clustering/profile.rst index 1c8d7f96a..dc7e2ed72 100644 --- a/doc/source/user/guides/clustering/profile.rst +++ b/doc/source/user/guides/clustering/profile.rst @@ -66,7 +66,7 @@ Full example: `manage profile`_ Get Profile -~~~~~~~~~~~~ +~~~~~~~~~~~ To get a profile based on its name or ID: diff --git a/doc/source/user/proxies/baremetal.rst b/doc/source/user/proxies/baremetal.rst index b10f38b63..f3772bb8b 100644 --- a/doc/source/user/proxies/baremetal.rst +++ b/doc/source/user/proxies/baremetal.rst @@ -1,12 +1,12 @@ Baremetal API -============== +============= For details on how to use baremetal, see :doc:`/user/guides/baremetal` .. automodule:: openstack.baremetal.v1._proxy The Baremetal Class --------------------- +------------------- The baremetal high-level interface is available through the ``baremetal`` member of a :class:`~openstack.connection.Connection` object. diff --git a/doc/source/user/proxies/block_storage.rst b/doc/source/user/proxies/block_storage.rst index 8395709c3..cd4e79204 100644 --- a/doc/source/user/proxies/block_storage.rst +++ b/doc/source/user/proxies/block_storage.rst @@ -43,7 +43,7 @@ Snapshot Operations .. automethod:: openstack.block_storage.v2._proxy.Proxy.snapshots Stats Operations -^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^ .. autoclass:: openstack.block_storage.v2._proxy.Proxy diff --git a/doc/source/user/proxies/clustering.rst b/doc/source/user/proxies/clustering.rst index ff8b5c380..2c0d56e83 100644 --- a/doc/source/user/proxies/clustering.rst +++ b/doc/source/user/proxies/clustering.rst @@ -170,7 +170,7 @@ Helper Operations Service Operations -^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.clustering.v1._proxy.Proxy diff --git a/doc/source/user/proxies/load_balancer_v2.rst b/doc/source/user/proxies/load_balancer_v2.rst index 4ff99ffef..0ad940691 100644 --- a/doc/source/user/proxies/load_balancer_v2.rst +++ b/doc/source/user/proxies/load_balancer_v2.rst @@ -83,7 +83,7 @@ L7 Policy Operations .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_l7_policy L7 Rule Operations -^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.load_balancer.v2._proxy.Proxy diff --git a/doc/source/user/resources/baremetal/index.rst b/doc/source/user/resources/baremetal/index.rst index c8025248b..123fc5e4a 100644 --- a/doc/source/user/resources/baremetal/index.rst +++ b/doc/source/user/resources/baremetal/index.rst @@ -1,5 +1,5 @@ Baremetal Resources -===================== +=================== .. toctree:: :maxdepth: 1 diff --git a/doc/source/user/resources/baremetal/v1/chassis.rst b/doc/source/user/resources/baremetal/v1/chassis.rst index 8db33407a..87a606389 100644 --- a/doc/source/user/resources/baremetal/v1/chassis.rst +++ b/doc/source/user/resources/baremetal/v1/chassis.rst @@ -1,5 +1,5 @@ openstack.baremetal.v1.chassis -=============================== +============================== .. automodule:: openstack.baremetal.v1.chassis diff --git a/doc/source/user/resources/baremetal/v1/driver.rst b/doc/source/user/resources/baremetal/v1/driver.rst index 980a067bc..f987d0861 100644 --- a/doc/source/user/resources/baremetal/v1/driver.rst +++ b/doc/source/user/resources/baremetal/v1/driver.rst @@ -1,5 +1,5 @@ openstack.baremetal.v1.driver -============================== +============================= .. automodule:: openstack.baremetal.v1.driver diff --git a/doc/source/user/resources/baremetal/v1/node.rst b/doc/source/user/resources/baremetal/v1/node.rst index 323c8db6e..37d563f7d 100644 --- a/doc/source/user/resources/baremetal/v1/node.rst +++ b/doc/source/user/resources/baremetal/v1/node.rst @@ -1,5 +1,5 @@ openstack.baremetal.v1.Node -============================ +=========================== .. automodule:: openstack.baremetal.v1.node diff --git a/doc/source/user/resources/baremetal/v1/port.rst b/doc/source/user/resources/baremetal/v1/port.rst index 34f1ab9a3..2bf0e1865 100644 --- a/doc/source/user/resources/baremetal/v1/port.rst +++ b/doc/source/user/resources/baremetal/v1/port.rst @@ -1,5 +1,5 @@ openstack.baremetal.v1.port -============================ +=========================== .. automodule:: openstack.baremetal.v1.port diff --git a/doc/source/user/resources/baremetal/v1/port_group.rst b/doc/source/user/resources/baremetal/v1/port_group.rst index 8867dc16d..45c2bd91a 100644 --- a/doc/source/user/resources/baremetal/v1/port_group.rst +++ b/doc/source/user/resources/baremetal/v1/port_group.rst @@ -1,5 +1,5 @@ openstack.baremetal.v1.port_group -================================== +================================= .. automodule:: openstack.baremetal.v1.port_group diff --git a/doc/source/user/resources/clustering/v1/cluster.rst b/doc/source/user/resources/clustering/v1/cluster.rst index 43e8a6d51..779c7d6ce 100644 --- a/doc/source/user/resources/clustering/v1/cluster.rst +++ b/doc/source/user/resources/clustering/v1/cluster.rst @@ -1,5 +1,5 @@ openstack.clustering.v1.Cluster -===================================== +=============================== .. automodule:: openstack.clustering.v1.cluster diff --git a/doc/source/user/resources/compute/v2/flavor.rst b/doc/source/user/resources/compute/v2/flavor.rst index 9f62f96f2..45fee1b1e 100644 --- a/doc/source/user/resources/compute/v2/flavor.rst +++ b/doc/source/user/resources/compute/v2/flavor.rst @@ -1,5 +1,5 @@ openstack.compute.v2.flavor -============================ +=========================== .. automodule:: openstack.compute.v2.flavor diff --git a/doc/source/user/resources/compute/v2/server.rst b/doc/source/user/resources/compute/v2/server.rst index 6f0ddbb7d..5dc072c75 100644 --- a/doc/source/user/resources/compute/v2/server.rst +++ b/doc/source/user/resources/compute/v2/server.rst @@ -1,5 +1,5 @@ openstack.compute.v2.server -============================ +=========================== .. automodule:: openstack.compute.v2.server diff --git a/doc/source/user/resources/database/index.rst b/doc/source/user/resources/database/index.rst index 962aa081d..3218b40a7 100644 --- a/doc/source/user/resources/database/index.rst +++ b/doc/source/user/resources/database/index.rst @@ -1,5 +1,5 @@ Database Resources -====================== +================== .. toctree:: :maxdepth: 1 diff --git a/doc/source/user/resources/key_manager/v1/container.rst b/doc/source/user/resources/key_manager/v1/container.rst index ef09035dc..601e7b181 100644 --- a/doc/source/user/resources/key_manager/v1/container.rst +++ b/doc/source/user/resources/key_manager/v1/container.rst @@ -1,5 +1,5 @@ openstack.key_manager.v1.container -===================================== +================================== .. automodule:: openstack.key_manager.v1.container diff --git a/doc/source/user/resources/load_balancer/v2/l7_rule.rst b/doc/source/user/resources/load_balancer/v2/l7_rule.rst index 2b1f471af..c661cd676 100644 --- a/doc/source/user/resources/load_balancer/v2/l7_rule.rst +++ b/doc/source/user/resources/load_balancer/v2/l7_rule.rst @@ -1,10 +1,10 @@ openstack.load_balancer.v2.l7_rule -==================================== +================================== .. automodule:: openstack.load_balancer.v2.l7_rule The L7Rule Class ------------------- +---------------- The ``L7Rule`` class inherits from :class:`~openstack.resource.Resource`. diff --git a/doc/source/user/resources/load_balancer/v2/pool.rst b/doc/source/user/resources/load_balancer/v2/pool.rst index 19c04e3f1..e67f97c29 100644 --- a/doc/source/user/resources/load_balancer/v2/pool.rst +++ b/doc/source/user/resources/load_balancer/v2/pool.rst @@ -4,7 +4,7 @@ openstack.load_balancer.v2.pool .. automodule:: openstack.load_balancer.v2.pool The Pool Class ------------------- +-------------- The ``Pool`` class inherits from :class:`~openstack.resource.Resource`. diff --git a/doc/source/user/resources/network/v2/agent.rst b/doc/source/user/resources/network/v2/agent.rst index 1d99337dc..5593e6d94 100644 --- a/doc/source/user/resources/network/v2/agent.rst +++ b/doc/source/user/resources/network/v2/agent.rst @@ -4,7 +4,7 @@ openstack.network.v2.agent .. automodule:: openstack.network.v2.agent The Agent Class ------------------ +--------------- The ``Agent`` class inherits from :class:`~openstack.resource.Resource`. diff --git a/doc/source/user/service_filter.rst b/doc/source/user/service_filter.rst index 60910ce6e..a58177f25 100644 --- a/doc/source/user/service_filter.rst +++ b/doc/source/user/service_filter.rst @@ -1,5 +1,5 @@ ServiceFilter -============== +============= .. automodule:: openstack.service_filter From 1796d25a51e4e3072f65dd9969dbd7531bd34fa2 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 23 Mar 2018 07:57:25 -0500 Subject: [PATCH 2017/3836] Rename python-openstacksdk to openstacksdk in zuul.yaml This patch will need to be force-merged after the rename. Change-Id: I07df6626ca988c28ef36ef42dba3544994a5f809 --- .zuul.yaml | 10 +++++----- devstack/plugin.sh | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index d8b12dfec..5f3e1293e 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -10,8 +10,8 @@ required-projects: - openstack-infra/shade - openstack/keystoneauth + - openstack/openstacksdk - openstack/os-client-config - - openstack/python-openstacksdk - job: name: openstacksdk-tox-py35-tips @@ -25,8 +25,8 @@ required-projects: - openstack-infra/shade - openstack/keystoneauth + - openstack/openstacksdk - openstack/os-client-config - - openstack/python-openstacksdk - project-template: name: openstacksdk-tox-tips @@ -52,7 +52,7 @@ # things. - name: openstack-infra/shade override-branch: master - - name: openstack/python-openstacksdk + - name: openstack/openstacksdk override-branch: master - name: openstack/os-client-config override-branch: master @@ -84,7 +84,7 @@ OPENSTACKSDK_HAS_SWIFT: 1 tox_install_siblings: false tox_envlist: functional - zuul_work_dir: src/git.openstack.org/openstack/python-openstacksdk + zuul_work_dir: src/git.openstack.org/openstack/openstacksdk - job: name: openstacksdk-functional-devstack-legacy @@ -155,8 +155,8 @@ required-projects: - openstack-infra/shade - openstack/keystoneauth + - openstack/openstacksdk - openstack/os-client-config - - openstack/python-openstacksdk vars: tox_install_siblings: true diff --git a/devstack/plugin.sh b/devstack/plugin.sh index 4a710af2f..0aacd6070 100644 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -3,18 +3,18 @@ # To enable openstacksdk in devstack add an entry to local.conf that looks like # # [[local|localrc]] -# enable_plugin openstacksdk git://git.openstack.org/openstack/python-openstacksdk +# enable_plugin openstacksdk https://git.openstack.org/openstack/openstacksdk function preinstall_openstacksdk { : } function install_openstacksdk { - if use_library_from_git "python-openstacksdk"; then + if use_library_from_git "openstacksdk"; then # don't clone, it'll be done by the plugin install - setup_dev_lib "python-openstacksdk" + setup_dev_lib "openstacksdk" else - pip_install "python-openstacksdk" + pip_install "openstacksdk" fi } From f44ed7ab3541a38d277479ea510e3bbf0373d289 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 23 Mar 2018 08:10:10 -0500 Subject: [PATCH 2018/3836] Update python-openstacksdk references to openstacksdk Now that the repo is renamed, update all of the references. While we're at it, remote unused translation config. Change-Id: Ib9f80eb809317483f83f79952470c2b57b2bb7c6 --- .gitreview | 2 +- CONTRIBUTING.rst | 16 ++++++++-------- README.rst | 4 ++-- SHADE-MERGE-TODO.rst | 2 +- doc/source/conf.py | 4 ++-- doc/source/contributor/index.rst | 4 ++-- doc/source/contributor/setup.rst | 8 ++++---- doc/source/contributor/testing.rst | 2 +- doc/source/install/index.rst | 6 +++--- doc/source/releasenotes.rst | 2 +- doc/source/user/guides/clustering/action.rst | 2 +- doc/source/user/guides/clustering/cluster.rst | 2 +- doc/source/user/guides/clustering/event.rst | 2 +- doc/source/user/guides/clustering/node.rst | 2 +- doc/source/user/guides/clustering/policy.rst | 2 +- .../user/guides/clustering/policy_type.rst | 2 +- doc/source/user/guides/clustering/profile.rst | 2 +- .../user/guides/clustering/profile_type.rst | 2 +- doc/source/user/guides/clustering/receiver.rst | 2 +- doc/source/user/guides/compute.rst | 6 +++--- doc/source/user/guides/connect.rst | 2 +- doc/source/user/guides/identity.rst | 2 +- doc/source/user/guides/image.rst | 8 ++++---- doc/source/user/guides/network.rst | 8 ++++---- doc/source/user/transition_from_profile.rst | 4 ++-- openstack/tests/ansible/hooks/post_test_hook.sh | 2 +- releasenotes/source/conf.py | 2 +- setup.cfg | 14 -------------- 28 files changed, 51 insertions(+), 65 deletions(-) diff --git a/.gitreview b/.gitreview index d838a518f..6b3a0e0b8 100644 --- a/.gitreview +++ b/.gitreview @@ -1,4 +1,4 @@ [gerrit] host=review.openstack.org port=29418 -project=openstack/python-openstacksdk.git +project=openstack/openstacksdk.git diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index d21643559..4fc8209a6 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,10 +1,10 @@ .. _contributing: -=================================== -Contributing to python-openstacksdk -=================================== +============================ +Contributing to openstacksdk +============================ -If you're interested in contributing to the python-openstacksdk project, +If you're interested in contributing to the openstacksdk project, the following will help get you started. Contributor License Agreement @@ -13,7 +13,7 @@ Contributor License Agreement .. index:: single: license; agreement -In order to contribute to the python-openstacksdk project, you need to have +In order to contribute to the openstacksdk project, you need to have signed OpenStack's contributor's agreement. Please read `DeveloperWorkflow`_ before sending your first patch for review. @@ -30,7 +30,7 @@ Project Hosting Details ----------------------- Project Documentation - https://docs.openstack.org/python-openstacksdk/latest/ + https://docs.openstack.org/openstacksdk/latest/ Bug tracker https://bugs.launchpad.net/python-openstacksdk @@ -39,7 +39,7 @@ Mailing list (prefix subjects with ``[sdk]`` for faster responses) http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-dev Code Hosting - https://git.openstack.org/cgit/openstack/python-openstacksdk + https://git.openstack.org/cgit/openstack/openstacksdk Code Review - https://review.openstack.org/#/q/status:open+project:openstack/python-openstacksdk,n,z + https://review.openstack.org/#/q/status:open+project:openstack/openstacksdk,n,z diff --git a/README.rst b/README.rst index d453ef5a5..c68874626 100644 --- a/README.rst +++ b/README.rst @@ -156,8 +156,8 @@ Links ===== * `Issue Tracker `_ -* `Code Review `_ +* `Code Review `_ * `Documentation `_ -* `PyPI `_ +* `PyPI `_ * `Mailing list `_ * `Bugs `_ diff --git a/SHADE-MERGE-TODO.rst b/SHADE-MERGE-TODO.rst index baf684be0..15c266d4c 100644 --- a/SHADE-MERGE-TODO.rst +++ b/SHADE-MERGE-TODO.rst @@ -52,7 +52,7 @@ shade integration ----------------- * Invent some terminology that is clear and makes sense to distinguish between - the object interface that came originally from python-openstacksdk and the + the object interface that came originally from openstacksdk and the interface that came from shade. * Shift the shade interface methods to use the Object Interface for their operations. It's possible there may be cases where the REST layer needs to diff --git a/doc/source/conf.py b/doc/source/conf.py index 8c3d3d431..561e014e3 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -31,7 +31,7 @@ ] # openstackdocstheme options -repository_name = 'openstack/python-openstacksdk' +repository_name = 'openstack/openstacksdk' bug_project = '760' bug_tag = '' html_last_updated_fmt = '%Y-%m-%d %H:%M' @@ -56,7 +56,7 @@ master_doc = 'index' # General information about the project. -project = u'python-openstacksdk' +project = u'openstacksdk' copyright = u'2017, Various members of the OpenStack Foundation' # A few variables have to be set for the log-a-bug feature. diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst index d9e5ed723..444e5f5bf 100644 --- a/doc/source/contributor/index.rst +++ b/doc/source/contributor/index.rst @@ -36,9 +36,9 @@ as occasional talk about SDKs created for languages outside of Python. Email ***** -The `openstack-dev `_ +The `openstack-dev `_ mailing list fields questions of all types on OpenStack. Using the -``[python-openstacksdk]`` filter to begin your email subject will ensure +``[sdk]`` filter to begin your email subject will ensure that the message gets to SDK developers. Coding Standards diff --git a/doc/source/contributor/setup.rst b/doc/source/contributor/setup.rst index 628b685b2..47e227420 100644 --- a/doc/source/contributor/setup.rst +++ b/doc/source/contributor/setup.rst @@ -89,13 +89,13 @@ Getting the Source Code review systems that we use. The canonical Git repository is hosted on openstack.org at -http://git.openstack.org/cgit/openstack/python-openstacksdk/, with a -mirror on GitHub at https://github.com/openstack/python-openstacksdk. +http://git.openstack.org/cgit/openstack/openstacksdk/, with a +mirror on GitHub at https://github.com/openstack/openstacksdk. Because of how Git works, you can create a local clone from either of those, or your own personal fork.:: - (sdk3)$ git clone https://git.openstack.org/openstack/python-openstacksdk.git - (sdk3)$ cd python-openstacksdk + (sdk3)$ git clone https://git.openstack.org/openstack/openstacksdk.git + (sdk3)$ cd openstacksdk Installing Dependencies ----------------------- diff --git a/doc/source/contributor/testing.rst b/doc/source/contributor/testing.rst index 0998abe24..30e8d78a2 100644 --- a/doc/source/contributor/testing.rst +++ b/doc/source/contributor/testing.rst @@ -49,7 +49,7 @@ DevStack There are many ways to run and configure DevStack. The link above will show you how to run DevStack a number of ways. You'll need to choose a method you're familiar with and can run in your environment. Wherever DevStack is -running, we need to make sure that python-openstacksdk contributors are +running, we need to make sure that openstacksdk contributors are using the same configuration. This is the ``local.conf`` file we use to configure DevStack. diff --git a/doc/source/install/index.rst b/doc/source/install/index.rst index 5b06c9812..fcbfcb74a 100644 --- a/doc/source/install/index.rst +++ b/doc/source/install/index.rst @@ -4,9 +4,9 @@ Installation At the command line:: - $ pip install python-openstacksdk + $ pip install openstacksdk Or, if you have virtualenv wrapper installed:: - $ mkvirtualenv python-openstacksdk - $ pip install python-openstacksdk + $ mkvirtualenv openstacksdk + $ pip install openstacksdk diff --git a/doc/source/releasenotes.rst b/doc/source/releasenotes.rst index ecf99e888..17b9814e2 100644 --- a/doc/source/releasenotes.rst +++ b/doc/source/releasenotes.rst @@ -2,5 +2,5 @@ Release Notes ============= -Release notes for `python-openstacksdk` can be found at +Release notes for `openstacksdk` can be found at https://releases.openstack.org/teams/openstacksdk.html diff --git a/doc/source/user/guides/clustering/action.rst b/doc/source/user/guides/clustering/action.rst index d6f6b0ea3..2cf661bb5 100644 --- a/doc/source/user/guides/clustering/action.rst +++ b/doc/source/user/guides/clustering/action.rst @@ -44,4 +44,4 @@ To get a action based on its name or ID: .. literalinclude:: ../../examples/clustering/action.py :pyobject: get_action -.. _manage action: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/clustering/action.py +.. _manage action: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/clustering/action.py diff --git a/doc/source/user/guides/clustering/cluster.rst b/doc/source/user/guides/clustering/cluster.rst index 7ee5134e0..21792c523 100644 --- a/doc/source/user/guides/clustering/cluster.rst +++ b/doc/source/user/guides/clustering/cluster.rst @@ -189,5 +189,5 @@ To restore a specified cluster, members in the cluster will be checked. :pyobject: recover_cluster -.. _manage cluster: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/clustering/cluster.py +.. _manage cluster: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/clustering/cluster.py diff --git a/doc/source/user/guides/clustering/event.rst b/doc/source/user/guides/clustering/event.rst index 753208045..80896f76f 100644 --- a/doc/source/user/guides/clustering/event.rst +++ b/doc/source/user/guides/clustering/event.rst @@ -44,4 +44,4 @@ To get a event based on its name or ID: .. literalinclude:: ../../examples/clustering/event.py :pyobject: get_event -.. _manage event: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/clustering/event.py +.. _manage event: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/clustering/event.py diff --git a/doc/source/user/guides/clustering/node.rst b/doc/source/user/guides/clustering/node.rst index ec89ed48b..3d68396bf 100644 --- a/doc/source/user/guides/clustering/node.rst +++ b/doc/source/user/guides/clustering/node.rst @@ -117,4 +117,4 @@ To restore a specified node. .. literalinclude:: ../../examples/clustering/node.py :pyobject: recover_node -.. _manage node: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/clustering/node.py +.. _manage node: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/clustering/node.py diff --git a/doc/source/user/guides/clustering/policy.rst b/doc/source/user/guides/clustering/policy.rst index 07a8de9e9..5bdcbb61c 100644 --- a/doc/source/user/guides/clustering/policy.rst +++ b/doc/source/user/guides/clustering/policy.rst @@ -99,4 +99,4 @@ still in use, you will get an error message. :pyobject: delete_policy -.. _manage policy: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/clustering/policy.py +.. _manage policy: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/clustering/policy.py diff --git a/doc/source/user/guides/clustering/policy_type.rst b/doc/source/user/guides/clustering/policy_type.rst index ceea4aefe..d294028e1 100644 --- a/doc/source/user/guides/clustering/policy_type.rst +++ b/doc/source/user/guides/clustering/policy_type.rst @@ -42,4 +42,4 @@ it. Full example: `manage policy type`_ -.. _manage policy type: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/clustering/policy_type.py +.. _manage policy type: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/clustering/policy_type.py diff --git a/doc/source/user/guides/clustering/profile.rst b/doc/source/user/guides/clustering/profile.rst index dc7e2ed72..228fa4232 100644 --- a/doc/source/user/guides/clustering/profile.rst +++ b/doc/source/user/guides/clustering/profile.rst @@ -102,4 +102,4 @@ still in use, you will get an error message. :pyobject: delete_profile -.. _manage profile: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/clustering/profile.py +.. _manage profile: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/clustering/profile.py diff --git a/doc/source/user/guides/clustering/profile_type.rst b/doc/source/user/guides/clustering/profile_type.rst index b8a3fae35..2a15d19e9 100644 --- a/doc/source/user/guides/clustering/profile_type.rst +++ b/doc/source/user/guides/clustering/profile_type.rst @@ -41,4 +41,4 @@ To get the details about a profile type, you need to provide the name of it. Full example: `manage profile type`_ -.. _manage profile type: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/clustering/profile_type.py +.. _manage profile type: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/clustering/profile_type.py diff --git a/doc/source/user/guides/clustering/receiver.rst b/doc/source/user/guides/clustering/receiver.rst index 2cd8e8bec..587b37c18 100644 --- a/doc/source/user/guides/clustering/receiver.rst +++ b/doc/source/user/guides/clustering/receiver.rst @@ -97,4 +97,4 @@ use, you will get an error message. :pyobject: delete_receiver -.. _manage receiver: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/clustering/receiver.py +.. _manage receiver: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/clustering/receiver.py diff --git a/doc/source/user/guides/compute.rst b/doc/source/user/guides/compute.rst index 3954d584b..3915bcee4 100644 --- a/doc/source/user/guides/compute.rst +++ b/doc/source/user/guides/compute.rst @@ -83,7 +83,7 @@ for it to become active. Full example: `compute resource create`_ -.. _compute resource list: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/compute/list.py -.. _network resource list: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/network/list.py -.. _compute resource create: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/compute/create.py +.. _compute resource list: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/compute/list.py +.. _network resource list: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/network/list.py +.. _compute resource create: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/compute/create.py .. _public–key cryptography: https://en.wikipedia.org/wiki/Public-key_cryptography diff --git a/doc/source/user/guides/connect.rst b/doc/source/user/guides/connect.rst index 51be68bd6..c20698910 100644 --- a/doc/source/user/guides/connect.rst +++ b/doc/source/user/guides/connect.rst @@ -18,7 +18,7 @@ To create a :class:`~openstack.connection.Connection` instance, use the .. literalinclude:: ../examples/connect.py :pyobject: create_connection -Full example at `connect.py `_ +Full example at `connect.py `_ .. note:: To enable logging, see the :doc:`logging` user guide. diff --git a/doc/source/user/guides/identity.rst b/doc/source/user/guides/identity.rst index 1cd0c4422..5041f7a3a 100644 --- a/doc/source/user/guides/identity.rst +++ b/doc/source/user/guides/identity.rst @@ -108,4 +108,4 @@ sub-regions with a region to make a tree-like structured hierarchy. Full example: `identity resource list`_ -.. _identity resource list: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/identity/list.py +.. _identity resource list: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/identity/list.py diff --git a/doc/source/user/guides/image.rst b/doc/source/user/guides/image.rst index 10e28cc68..154f9d076 100644 --- a/doc/source/user/guides/image.rst +++ b/doc/source/user/guides/image.rst @@ -75,7 +75,7 @@ Delete an image. Full example: `image resource delete`_ -.. _image resource create: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/image/create.py -.. _image resource delete: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/image/delete.py -.. _image resource list: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/image/list.py -.. _image resource download: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/image/download.py +.. _image resource create: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/image/create.py +.. _image resource delete: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/image/delete.py +.. _image resource list: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/image/list.py +.. _image resource download: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/image/download.py diff --git a/doc/source/user/guides/network.rst b/doc/source/user/guides/network.rst index b8fe9338b..e824e9b55 100644 --- a/doc/source/user/guides/network.rst +++ b/doc/source/user/guides/network.rst @@ -136,7 +136,7 @@ Delete a project network and its subnets. Full example: `network resource delete`_ -.. _network resource create: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/network/create.py -.. _network resource delete: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/network/delete.py -.. _network resource list: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/network/list.py -.. _network security group create: http://git.openstack.org/cgit/openstack/python-openstacksdk/tree/examples/network/security_group_rules.py +.. _network resource create: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/network/create.py +.. _network resource delete: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/network/delete.py +.. _network resource list: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/network/list.py +.. _network security group create: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/network/security_group_rules.py diff --git a/doc/source/user/transition_from_profile.rst b/doc/source/user/transition_from_profile.rst index 0bc52254b..a6edc6a5e 100644 --- a/doc/source/user/transition_from_profile.rst +++ b/doc/source/user/transition_from_profile.rst @@ -2,7 +2,7 @@ Transition from Profile ======================= .. note:: This section describes migrating code from a previous interface of - python-openstacksdk and can be ignored by people writing new code. + openstacksdk and can be ignored by people writing new code. If you have code that currently uses the :class:`~openstack.profile.Profile` object and/or an ``authenticator`` instance from an object based on @@ -41,7 +41,7 @@ Replacing authenticator ----------------------- There is no direct replacement for ``openstack.auth.base.BaseAuthPlugin``. -``python-openstacksdk`` uses the `keystoneauth`_ library for authentication +``openstacksdk`` uses the `keystoneauth`_ library for authentication and HTTP interactions. `keystoneauth`_ has `auth plugins`_ that can be used to control how authentication is done. The ``auth_type`` config parameter can be set to choose the correct authentication method to be used. diff --git a/openstack/tests/ansible/hooks/post_test_hook.sh b/openstack/tests/ansible/hooks/post_test_hook.sh index 6b511719a..bbda4af3b 100755 --- a/openstack/tests/ansible/hooks/post_test_hook.sh +++ b/openstack/tests/ansible/hooks/post_test_hook.sh @@ -14,7 +14,7 @@ # TODO(shade) Rework for Zuul v3 -export OPENSTACKSDK_DIR="$BASE/new/python-openstacksdk" +export OPENSTACKSDK_DIR="$BASE/new/openstacksdk" cd $OPENSTACKSDK_DIR sudo chown -R jenkins:stack $OPENSTACKSDK_DIR diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py index 0f0a0f713..47ae299b6 100644 --- a/releasenotes/source/conf.py +++ b/releasenotes/source/conf.py @@ -43,7 +43,7 @@ ] # openstackdocstheme options -repository_name = 'openstack/python-openstacksdk' +repository_name = 'openstack/openstacksdk' bug_project = '760' bug_tag = '' html_last_updated_fmt = '%Y-%m-%d %H:%M' diff --git a/setup.cfg b/setup.cfg index 37e19d4bb..9d56d0e02 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,19 +36,5 @@ warning-is-error = 1 [upload_sphinx] upload-dir = doc/build/html -[compile_catalog] -directory = openstack/locale -domain = python-openstacksdk - -[update_catalog] -domain = python-openstacksdk -output_dir = openstack/locale -input_file = openstack/locale/python-openstacksdk.pot - -[extract_messages] -keywords = _ gettext ngettext l_ lazy_gettext -mapping_file = babel.cfg -output_file = openstack/locale/python-openstacksdk.pot - [wheel] universal = 1 From ff4a261ae468dee12545d907c496cd43dd9bf0c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Sat, 24 Mar 2018 23:36:20 +0100 Subject: [PATCH 2019/3836] Add support for trunk ports and subports This patch adds support for Neutron's trunks CRUD. It also adds subport for create/get/delete trunk's sub_ports. Change-Id: I835591ad517b84078a8ca87ef4d0bb93258c6e1c --- .zuul.yaml | 1 + openstack/network/v2/_proxy.py | 117 ++++++++++++++++++ openstack/network/v2/trunk.py | 72 +++++++++++ .../tests/functional/network/v2/test_trunk.py | 84 +++++++++++++ openstack/tests/unit/network/v2/test_trunk.py | 57 +++++++++ 5 files changed, 331 insertions(+) create mode 100644 openstack/network/v2/trunk.py create mode 100644 openstack/tests/functional/network/v2/test_trunk.py create mode 100644 openstack/tests/unit/network/v2/test_trunk.py diff --git a/.zuul.yaml b/.zuul.yaml index 5f3e1293e..a6e909948 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -134,6 +134,7 @@ o-hm: true o-hk: true neutron-qos: true + neutron-trunk: true tox_environment: OPENSTACKSDK_HAS_OCTAVIA: 1 diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index a05f472f0..f61335af7 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -47,6 +47,7 @@ from openstack.network.v2 import service_provider as _service_provider from openstack.network.v2 import subnet as _subnet from openstack.network.v2 import subnet_pool as _subnet_pool +from openstack.network.v2 import trunk as _trunk from openstack.network.v2 import vpn_service as _vpn_service from openstack import proxy from openstack import utils @@ -3046,6 +3047,122 @@ def set_tags(self, resource, tags): self._check_tag_support(resource) return resource.set_tags(self, tags) + def create_trunk(self, **attrs): + """Create a new trunk from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.network.v2.trunk.Trunk, + comprised of the properties on the Trunk class. + + :returns: The results of trunk creation + :rtype: :class:`~openstack.network.v2.trunk.Trunk` + """ + return self._create(_trunk.Trunk, **attrs) + + def delete_trunk(self, trunk, ignore_missing=True): + """Delete a trunk + + :param trunk: The value can be either the ID of trunk or a + :class:`openstack.network.v2.trunk.Trunk` instance + + :returns: ``None`` + """ + self._delete(_trunk.Trunk, trunk, ignore_missing=ignore_missing) + + def find_trunk(self, name_or_id, ignore_missing=True, **args): + """Find a single trunk + + :param name_or_id: The name or ID of a trunk. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. + :returns: One :class:`~openstack.network.v2.trunk.Trunk` + or None + """ + return self._find(_trunk.Trunk, name_or_id, + ignore_missing=ignore_missing, **args) + + def get_trunk(self, trunk): + """Get a single trunk + + :param trunk: The value can be the ID of a trunk or a + :class:`~openstack.network.v2.trunk.Trunk` instance. + + :returns: One + :class:`~openstack.network.v2.trunk.Trunk` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_trunk.Trunk, trunk) + + def trunks(self, **query): + """Return a generator of trunks + + :param dict query: Optional query parameters to be sent to limit + the resources being returned. + + :returns: A generator of trunk objects + :rtype: :class:`~openstack.network.v2.trunk.trunk` + """ + return self._list(_trunk.Trunk, paginated=False, **query) + + def update_trunk(self, trunk, **attrs): + """Update a trunk + + :param trunk: Either the id of a trunk or a + :class:`~openstack.network.v2.trunk.Trunk` instance. + :param dict attrs: The attributes to update on the trunk + represented by ``trunk``. + + :returns: The updated trunk + :rtype: :class:`~openstack.network.v2.trunk.Trunk` + """ + return self._update(_trunk.Trunk, trunk, **attrs) + + def add_trunk_subports(self, trunk, subports): + """Set sub_ports on trunk + + :param trunk: The value can be the ID of a trunk or a + :class:`~openstack.network.v2.trunk.Trunk` instance. + :param subports: New subports to be set. + :type subports: "list" + + :returns: The updated trunk + :rtype: :class:`~openstack.network.v2.trunk.Trunk` + """ + trunk = self._get_resource(_trunk.Trunk, trunk) + return trunk.add_subports(self, subports) + + def delete_trunk_subports(self, trunk, subports): + """Remove sub_ports from trunk + + :param trunk: The value can be the ID of a trunk or a + :class:`~openstack.network.v2.trunk.Trunk` instance. + :param subports: Subports to be removed. + :type subports: "list" + + :returns: The updated trunk + :rtype: :class:`~openstack.network.v2.trunk.Trunk` + """ + trunk = self._get_resource(_trunk.Trunk, trunk) + return trunk.delete_subports(self, subports) + + def get_trunk_subports(self, trunk): + """Get sub_ports configured on trunk + + :param trunk: The value can be the ID of a trunk or a + :class:`~openstack.network.v2.trunk.Trunk` instance. + + :returns: Trunk sub_ports + :rtype: "list" + """ + trunk = self._get_resource(_trunk.Trunk, trunk) + return trunk.get_subports(self) + def create_vpn_service(self, **attrs): """Create a new vpn service from attributes diff --git a/openstack/network/v2/trunk.py b/openstack/network/v2/trunk.py new file mode 100644 index 000000000..875a08790 --- /dev/null +++ b/openstack/network/v2/trunk.py @@ -0,0 +1,72 @@ +# 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.network import network_service +from openstack import resource +from openstack import utils + + +class Trunk(resource.Resource): + resource_key = 'trunk' + resources_key = 'trunks' + base_path = '/trunks' + service = network_service.NetworkService() + + # capabilities + allow_create = True + allow_get = True + allow_update = True + allow_delete = True + allow_list = True + + _query_mapping = resource.QueryParameters( + 'name', 'description', 'port_id', 'status', 'sub_ports', + project_id='tenant_id', + is_admin_state_up='admin_state_up', + ) + + # Properties + #: Trunk name. + name = resource.Body('name') + #: The ID of the project who owns the trunk. Only administrative + #: users can specify a project ID other than their own. + project_id = resource.Body('tenant_id') + #: The trunk description. + description = resource.Body('description') + #: The administrative state of the port, which is up ``True`` or + #: down ``False``. *Type: bool* + is_admin_state_up = resource.Body('admin_state_up', type=bool) + #: The ID of the trunk's parent port + port_id = resource.Body('port_id') + #: The status for the trunk. Possible values are ACTIVE, DOWN, BUILD, + #: DEGRADED, and ERROR. + status = resource.Body('status') + #: A list of ports associated with the trunk. + sub_ports = resource.Body('sub_ports', type=list) + + def add_subports(self, session, subports): + url = utils.urljoin('/trunks', self.id, 'add_subports') + session.put(url, json={'sub_ports': subports}) + self._body.attributes.update({'sub_ports': subports}) + return self + + def delete_subports(self, session, subports): + url = utils.urljoin('/trunks', self.id, 'remove_subports') + session.put(url, json={'sub_ports': subports}) + self._body.attributes.update({'sub_ports': subports}) + return self + + def get_subports(self, session): + url = utils.urljoin('/trunks', self.id, 'get_subports') + resp = session.get(url) + self._body.attributes.update(resp.json()) + return resp.json() diff --git a/openstack/tests/functional/network/v2/test_trunk.py b/openstack/tests/functional/network/v2/test_trunk.py new file mode 100644 index 000000000..62b625998 --- /dev/null +++ b/openstack/tests/functional/network/v2/test_trunk.py @@ -0,0 +1,84 @@ +# 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.network.v2 import network +from openstack.network.v2 import port +from openstack.network.v2 import trunk as _trunk +from openstack.tests.functional import base + + +class TestTrunk(base.BaseFunctionalTest): + + def setUp(self): + super(TestTrunk, self).setUp() + self.TRUNK_NAME = self.getUniqueString() + self.TRUNK_NAME_UPDATED = self.getUniqueString() + net = self.conn.network.create_network() + assert isinstance(net, network.Network) + self.NET_ID = net.id + prt = self.conn.network.create_port(network_id=self.NET_ID) + assert isinstance(prt, port.Port) + self.PORT_ID = prt.id + self.ports_to_clean = [self.PORT_ID] + trunk = self.conn.network.create_trunk( + name=self.TRUNK_NAME, + port_id=self.PORT_ID) + assert isinstance(trunk, _trunk.Trunk) + self.TRUNK_ID = trunk.id + + def tearDown(self): + self.conn.network.delete_trunk(self.TRUNK_ID, ignore_missing=False) + for port_id in self.ports_to_clean: + self.conn.network.delete_port(port_id, ignore_missing=False) + self.conn.network.delete_network(self.NET_ID, ignore_missing=False) + super(TestTrunk, self).tearDown() + + def test_find(self): + sot = self.conn.network.find_trunk(self.TRUNK_NAME) + self.assertEqual(self.TRUNK_ID, sot.id) + + def test_get(self): + sot = self.conn.network.get_trunk(self.TRUNK_ID) + self.assertEqual(self.TRUNK_ID, sot.id) + self.assertEqual(self.TRUNK_NAME, sot.name) + + def test_list(self): + ids = [o.id for o in self.conn.network.trunks()] + self.assertIn(self.TRUNK_ID, ids) + + def test_update(self): + sot = self.conn.network.update_trunk(self.TRUNK_ID, + name=self.TRUNK_NAME_UPDATED) + self.assertEqual(self.TRUNK_NAME_UPDATED, sot.name) + + def test_subports(self): + port_for_subport = self.conn.network.create_port( + network_id=self.NET_ID) + self.ports_to_clean.append(port_for_subport.id) + subports = [{ + 'port_id': port_for_subport.id, + 'segmentation_type': 'vlan', + 'segmentation_id': 111 + }] + + sot = self.conn.network.get_trunk_subports(self.TRUNK_ID) + self.assertEqual({'sub_ports': []}, sot) + + self.conn.network.add_trunk_subports(self.TRUNK_ID, subports) + sot = self.conn.network.get_trunk_subports(self.TRUNK_ID) + self.assertEqual({'sub_ports': subports}, sot) + + self.conn.network.delete_trunk_subports( + self.TRUNK_ID, [{'port_id': port_for_subport.id}]) + sot = self.conn.network.get_trunk_subports(self.TRUNK_ID) + self.assertEqual({'sub_ports': []}, sot) diff --git a/openstack/tests/unit/network/v2/test_trunk.py b/openstack/tests/unit/network/v2/test_trunk.py new file mode 100644 index 000000000..47abc14a9 --- /dev/null +++ b/openstack/tests/unit/network/v2/test_trunk.py @@ -0,0 +1,57 @@ +# 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.tests.unit import base + +from openstack.network.v2 import trunk + +EXAMPLE = { + 'id': 'IDENTIFIER', + 'description': 'Trunk description', + 'name': 'trunk-name', + 'tenant_id': '2', + 'admin_state_up': True, + 'port_id': 'fake_port_id', + 'status': 'ACTIVE', + 'sub_ports': [{ + 'port_id': 'subport_port_id', + 'segmentation_id': 1234, + 'segmentation_type': 'vlan' + }] + +} + + +class TestQoSPolicy(base.TestCase): + + def test_basic(self): + sot = trunk.Trunk() + self.assertEqual('trunk', sot.resource_key) + self.assertEqual('trunks', sot.resources_key) + self.assertEqual('/trunks', sot.base_path) + self.assertEqual('network', sot.service.service_type) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = trunk.Trunk(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertEqual(EXAMPLE['admin_state_up'], sot.is_admin_state_up) + self.assertEqual(EXAMPLE['port_id'], sot.port_id) + self.assertEqual(EXAMPLE['status'], sot.status) + self.assertEqual(EXAMPLE['sub_ports'], sot.sub_ports) From 1f70f692e6f2f99d04c82d077d9061cf0c76735a Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Thu, 22 Mar 2018 18:00:08 -0400 Subject: [PATCH 2020/3836] add lower-constraints job Create a tox environment for running the unit tests against the lower bounds of the dependencies. Create a lower-constraints.txt to be used to enforce the lower bounds in those tests. Add openstack-tox-lower-constraints job to the zuul configuration. See http://lists.openstack.org/pipermail/openstack-dev/2018-March/128352.html for more details. Change-Id: I7ec32aefc9caac30f4e93704fc33b9a98cb1c622 Depends-On: https://review.openstack.org/555034 Signed-off-by: Doug Hellmann --- .zuul.yaml | 4 +++- lower-constraints.txt | 37 +++++++++++++++++++++++++++++++++++++ tox.ini | 7 +++++++ 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 lower-constraints.txt diff --git a/.zuul.yaml b/.zuul.yaml index 5f3e1293e..276e507e0 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -117,7 +117,7 @@ post-config: $OCTAVIA_CONF: DEFAULT: - debug: True + debug: true controller_worker: amphora_driver: amphora_noop_driver compute_driver: compute_noop_driver @@ -224,6 +224,7 @@ - osc-functional-devstack-tips: voting: false - neutron-grenade + - openstack-tox-lower-constraints gate: jobs: - build-openstack-sphinx-docs: @@ -232,3 +233,4 @@ - openstacksdk-functional-devstack - openstacksdk-functional-devstack-python3 - neutron-grenade + - openstack-tox-lower-constraints diff --git a/lower-constraints.txt b/lower-constraints.txt new file mode 100644 index 000000000..895710c56 --- /dev/null +++ b/lower-constraints.txt @@ -0,0 +1,37 @@ +appdirs==1.3.0 +coverage==4.0 +decorator==3.4.0 +deprecation==1.0 +dogpile.cache==0.6.2 +extras==1.0.0 +fixtures==3.0.0 +future==0.16.0 +iso8601==0.1.11 +jmespath==0.9.0 +jsonpatch==1.16 +jsonpointer==1.13 +jsonschema==2.6.0 +keystoneauth1==3.4.0 +linecache2==1.0.0 +mock==2.0.0 +mox3==0.20.0 +munch==2.1.0 +netifaces==0.10.4 +os-client-config==1.28.0 +os-service-types==1.2.0 +oslotest==3.2.0 +pbr==2.0.0 +python-mimeparse==1.6.0 +python-subunit==1.0.0 +PyYAML==3.12 +requests==2.14.2 +requests-mock==1.2.0 +requestsexceptions==1.2.0 +six==1.10.0 +stestr==1.0.0 +stevedore==1.20.0 +testrepository==0.0.18 +testscenarios==0.4 +testtools==2.2.0 +traceback2==1.4.0 +unittest2==1.1.0 diff --git a/tox.ini b/tox.ini index 76012d59b..7378c3a78 100644 --- a/tox.ini +++ b/tox.ini @@ -101,3 +101,10 @@ exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build [doc8] extensions = .rst, .yaml + +[testenv:lower-constraints] +basepython = python3 +deps = + -c{toxinidir}/lower-constraints.txt + -r{toxinidir}/test-requirements.txt + -r{toxinidir}/requirements.txt From a4718c5fa8cf6c4e5a7399e7f31d44588ae4a1cf Mon Sep 17 00:00:00 2001 From: Mohammed Naser Date: Thu, 29 Mar 2018 11:07:52 -0500 Subject: [PATCH 2021/3836] Add image_format for VEXXHOST profile Change-Id: I1281c5339789d0e7dec1888a8ba8273005247c6b --- openstack/config/vendors/vexxhost.json | 1 + 1 file changed, 1 insertion(+) diff --git a/openstack/config/vendors/vexxhost.json b/openstack/config/vendors/vexxhost.json index 2227fff4f..10e131d4c 100644 --- a/openstack/config/vendors/vexxhost.json +++ b/openstack/config/vendors/vexxhost.json @@ -9,6 +9,7 @@ ], "dns_api_version": "1", "identity_api_version": "3", + "image_format": "raw", "floating_ip_source": "None", "requires_floating_ip": false } From dc564548e14c5efbf81f8940a248085979f1e6b4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 22 Feb 2018 16:44:05 -0600 Subject: [PATCH 2022/3836] Run normalize_keys on config for session codepath When we pass an authenticated session to Connection, we use the from_session codepath. That doesn't run through loader, and as a result the kwargs dict doesn't get passed through normalize_keys. That, in turn, means that int _api_version values are not turned in to strings, which in turn breaks a subscripting attempt in service_description. Run normalize_keys on the config dict and add some tests for this. Change-Id: I1c745c45512cb5d10cad36a3e2713a1dec9d494f --- openstack/config/_util.py | 31 +++++++++++++++++++ openstack/config/cloud_region.py | 4 +-- openstack/config/loader.py | 29 +++++------------ .../tests/unit/config/test_cloud_config.py | 6 ++-- openstack/tests/unit/test_connection.py | 28 +++++++++++++++++ 5 files changed, 71 insertions(+), 27 deletions(-) create mode 100644 openstack/config/_util.py diff --git a/openstack/config/_util.py b/openstack/config/_util.py new file mode 100644 index 000000000..8cfccc6fe --- /dev/null +++ b/openstack/config/_util.py @@ -0,0 +1,31 @@ +# Copyright (c) 2014 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. + + +def normalize_keys(config): + new_config = {} + for key, value in config.items(): + key = key.replace('-', '_') + if isinstance(value, dict): + new_config[key] = normalize_keys(value) + elif isinstance(value, bool): + new_config[key] = value + elif isinstance(value, int) and key not in ( + 'verbose_level', 'api_timeout'): + new_config[key] = str(value) + elif isinstance(value, float): + new_config[key] = str(value) + else: + new_config[key] = value + return new_config diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 4977e356d..62bacef36 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -23,6 +23,7 @@ from openstack import version as openstack_version from openstack import _log +from openstack.config import _util from openstack.config import defaults as config_defaults from openstack import exceptions @@ -79,10 +80,9 @@ def __init__(self, name=None, region_name=None, config=None, openstack_config=None, session_constructor=None, app_name=None, app_version=None, session=None, discovery_cache=None): - self._name = name self.region_name = region_name - self.config = config + self.config = _util.normalize_keys(config) self.log = _log.setup_logging('openstack.config') self._force_ipv4 = force_ipv4 self._auth = auth_plugin diff --git a/openstack/config/loader.py b/openstack/config/loader.py index aa766dd3c..1df0bc96e 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -29,6 +29,7 @@ import yaml from openstack import _log +from openstack.config import _util from openstack.config import cloud_region from openstack.config import defaults from openstack.config import vendors @@ -262,7 +263,7 @@ def __init__(self, config_files=None, vendor_files=None, self._cache_arguments = {} self._cache_expiration = {} if 'cache' in self.cloud_config: - cache_settings = self._normalize_keys(self.cloud_config['cache']) + cache_settings = _util.normalize_keys(self.cloud_config['cache']) # expiration_time used to be 'max_age' but the dogpile setting # is expiration_time. Support max_age for backwards compat. @@ -327,12 +328,12 @@ def get_extra_config(self, key, defaults=None): :param dict defaults: (optional) default values to merge under the found config """ - defaults = self._normalize_keys(defaults or {}) + defaults = _util.normalize_keys(defaults or {}) if not key: return defaults return _merge_clouds( defaults, - self._normalize_keys(self.cloud_config.get(key, {}))) + _util.normalize_keys(self.cloud_config.get(key, {}))) def _load_config_file(self): return self._load_yaml_json_file(self._config_files) @@ -353,22 +354,6 @@ def _load_yaml_json_file(self, filelist): return path, yaml.safe_load(f) return (None, {}) - def _normalize_keys(self, config): - new_config = {} - for key, value in config.items(): - key = key.replace('-', '_') - if isinstance(value, dict): - new_config[key] = self._normalize_keys(value) - elif isinstance(value, bool): - new_config[key] = value - elif isinstance(value, int) and key != 'verbose_level': - new_config[key] = str(value) - elif isinstance(value, float): - new_config[key] = str(value) - else: - new_config[key] = value - return new_config - def get_cache_expiration_time(self): return int(self._cache_expiration_time) @@ -412,7 +397,7 @@ def _get_regions(self, cloud): return regions def _get_known_regions(self, cloud): - config = self._normalize_keys(self.cloud_config['clouds'][cloud]) + config = _util.normalize_keys(self.cloud_config['clouds'][cloud]) if 'regions' in config: return self._expand_regions(config['regions']) elif 'region_name' in config: @@ -1075,7 +1060,7 @@ def get_one( config[key] = val config = self.magic_fixes(config) - config = self._normalize_keys(config) + config = _util.normalize_keys(config) # NOTE(dtroyer): OSC needs a hook into the auth args before the # plugin is loaded in order to maintain backward- @@ -1203,7 +1188,7 @@ def get_one_cloud_osc( return self._cloud_region_class( name=cloud_name, region_name=config['region_name'], - config=self._normalize_keys(config), + config=config, force_ipv4=force_ipv4, auth_plugin=auth_plugin, openstack_config=self, diff --git a/openstack/tests/unit/config/test_cloud_config.py b/openstack/tests/unit/config/test_cloud_config.py index ee2741940..f0c86922a 100644 --- a/openstack/tests/unit/config/test_cloud_config.py +++ b/openstack/tests/unit/config/test_cloud_config.py @@ -45,14 +45,14 @@ def test_arbitrary_attributes(self): self.assertEqual("region-al", cc.region_name) # Look up straight value - self.assertEqual(1, cc.a) + self.assertEqual("1", cc.a) # Look up prefixed attribute, fail - returns None self.assertIsNone(cc.os_b) # Look up straight value, then prefixed value - self.assertEqual(3, cc.c) - self.assertEqual(3, cc.os_c) + self.assertEqual("3", cc.c) + self.assertEqual("3", cc.os_c) # Lookup mystery attribute self.assertIsNone(cc.x) diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index 1c77aa403..df28d1402 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -109,6 +109,34 @@ def test_create_session(self): self.assertEqual('openstack.workflow.v2._proxy', conn.workflow.__class__.__module__) + def test_create_connection_version_param_default(self): + c1 = connection.Connection(cloud='sample') + conn = connection.Connection(session=c1.session) + self.assertEqual('openstack.identity.v2._proxy', + conn.identity.__class__.__module__) + + def test_create_connection_version_param_string(self): + c1 = connection.Connection(cloud='sample') + conn = connection.Connection( + session=c1.session, identity_api_version='3') + self.assertEqual('openstack.identity.v3._proxy', + conn.identity.__class__.__module__) + + def test_create_connection_version_param_int(self): + c1 = connection.Connection(cloud='sample') + conn = connection.Connection( + session=c1.session, identity_api_version=3) + self.assertEqual('openstack.identity.v3._proxy', + conn.identity.__class__.__module__) + + def test_create_connection_version_param_bogus(self): + c1 = connection.Connection(cloud='sample') + conn = connection.Connection( + session=c1.session, identity_api_version='red') + # TODO(mordred) This is obviously silly behavior + self.assertEqual('openstack.identity.v3._proxy', + conn.identity.__class__.__module__) + def test_from_config_given_config(self): cloud_region = openstack.config.OpenStackConfig().get_one("sample") From 35edf460bcd96ebc8f5d3285b90276d392acb546 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 23 Feb 2018 08:50:23 -0600 Subject: [PATCH 2023/3836] Remove the need for OpenStackConfig in CloudRegion There are a few things in CloudRegion that require calling back out to the creating OpenStackConfig object. However, now that creating a CloudRegion directly is a thing, those calls have become more awkward. Shift the cache settings and methods to CloudRegion along with the extra_config logic. Move two of the utility methods in loader into a _util file so that both loader and cloud_region can import it. This renames get_cache_expiration to get_cache_expirations, but I'm fairly confident that the only consumer of get_cache_expirations here is openstack.cloud. Leave passing in the OpenStackConfig for now, because osc uses it. However, if we can get osc shifted to using get_password_callback, we can get rid of it. Change-Id: Ia6ed0f00bfc2483bd09169811198cdf1a0ab2f15 --- openstack/cloud/openstackcloud.py | 2 +- openstack/config/_util.py | 14 ++++ openstack/config/cloud_region.py | 64 ++++++++++++------- openstack/config/loader.py | 63 +++++++----------- ...ud-region-standalone-848a2c4b5f3ebc29.yaml | 6 ++ 5 files changed, 85 insertions(+), 64 deletions(-) create mode 100644 releasenotes/notes/make-cloud-region-standalone-848a2c4b5f3ebc29.yaml diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index fef768e13..fe4c47eb0 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -196,7 +196,7 @@ def __init__(self): self.cache_enabled = True self._cache = self._make_cache( cache_class, cache_expiration_time, cache_arguments) - expirations = self.config.get_cache_expiration() + expirations = self.config.get_cache_expirations() for expire_key in expirations.keys(): # Only build caches for things we have list operations for if getattr( diff --git a/openstack/config/_util.py b/openstack/config/_util.py index 8cfccc6fe..9fd8b925b 100644 --- a/openstack/config/_util.py +++ b/openstack/config/_util.py @@ -29,3 +29,17 @@ def normalize_keys(config): else: new_config[key] = value return new_config + + +def merge_clouds(old_dict, new_dict): + """Like dict.update, except handling nested dicts.""" + ret = old_dict.copy() + for (k, v) in new_dict.items(): + if isinstance(v, dict): + if k in ret: + ret[k] = merge_clouds(ret[k], v) + else: + ret[k] = v.copy() + else: + ret[k] = v + return ret diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 62bacef36..f4e682c50 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. +import copy import math import warnings @@ -79,10 +80,14 @@ def __init__(self, name=None, region_name=None, config=None, force_ipv4=False, auth_plugin=None, openstack_config=None, session_constructor=None, app_name=None, app_version=None, session=None, - discovery_cache=None): + discovery_cache=None, extra_config=None, + cache_expiration_time=0, cache_expirations=None, + cache_path=None, cache_class='dogpile.cache.null', + cache_arguments=None, password_callback=None): self._name = name self.region_name = region_name self.config = _util.normalize_keys(config) + self._extra_config = extra_config or {} self.log = _log.setup_logging('openstack.config') self._force_ipv4 = force_ipv4 self._auth = auth_plugin @@ -92,6 +97,12 @@ def __init__(self, name=None, region_name=None, config=None, self._app_name = app_name self._app_version = app_version self._discovery_cache = discovery_cache or None + self._cache_expiration_time = cache_expiration_time + self._cache_expirations = cache_expirations or {} + self._cache_path = cache_path + self._cache_class = cache_class + self._cache_arguments = cache_arguments + self._password_callback = password_callback def __getattr__(self, key): """Return arbitrary attributes.""" @@ -398,26 +409,20 @@ def get_session_endpoint( return endpoint def get_cache_expiration_time(self): - if self._openstack_config: - return self._openstack_config.get_cache_expiration_time() - return 0 + # TODO(mordred) We should be validating/transforming this on input + return int(self._cache_expiration_time) def get_cache_path(self): - if self._openstack_config: - return self._openstack_config.get_cache_path() + return self._cache_path def get_cache_class(self): - if self._openstack_config: - return self._openstack_config.get_cache_class() - return 'dogpile.cache.null' + return self._cache_class def get_cache_arguments(self): - if self._openstack_config: - return self._openstack_config.get_cache_arguments() + return copy.deepcopy(self._cache_arguments) - def get_cache_expiration(self): - if self._openstack_config: - return self._openstack_config.get_cache_expiration() + def get_cache_expirations(self): + return copy.deepcopy(self._cache_expirations) def get_cache_resource_expiration(self, resource, default=None): """Get expiration time for a resource @@ -428,11 +433,9 @@ def get_cache_resource_expiration(self, resource, default=None): :returns: Expiration time for the resource type as float or default """ - if self._openstack_config: - expiration = self._openstack_config.get_cache_expiration() - if resource not in expiration: - return default - return float(expiration[resource]) + if resource not in self._cache_expirations: + return default + return float(self._cache_expirations[resource]) def requires_floating_ip(self): """Return whether or not this cloud requires floating ips. @@ -503,6 +506,20 @@ def get_nat_source(self): return net['name'] return None + def _get_extra_config(self, key, defaults=None): + """Fetch an arbitrary extra chunk of config, laying in defaults. + + :param string key: name of the config section to fetch + :param dict defaults: (optional) default values to merge under the + found config + """ + defaults = _util.normalize_keys(defaults or {}) + if not key: + return defaults + return _util.merge_clouds( + defaults, + _util.normalize_keys(self._extra_config.get(key, {}))) + def get_client_config(self, name=None, defaults=None): """Get config settings for a named client. @@ -520,7 +537,8 @@ def get_client_config(self, name=None, defaults=None): A dict containing merged settings from the named section, the client section and the defaults. """ - if not self._openstack_config: - return defaults or {} - return self._openstack_config.get_extra_config( - name, self._openstack_config.get_extra_config('client', defaults)) + return self._get_extra_config( + name, self._get_extra_config('client', defaults)) + + def get_password_callback(self): + return self._password_callback diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 1df0bc96e..67498ba12 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -83,20 +83,6 @@ def get_boolean(value): return False -def _merge_clouds(old_dict, new_dict): - """Like dict.update, except handling nested dicts.""" - ret = old_dict.copy() - for (k, v) in new_dict.items(): - if isinstance(v, dict): - if k in ret: - ret[k] = _merge_clouds(ret[k], v) - else: - ret[k] = v.copy() - else: - ret[k] = v - return ret - - def _auth_update(old_dict, new_dict_source): """Like dict.update, except handling the nested dict called auth.""" new_dict = copy.deepcopy(new_dict_source) @@ -186,7 +172,7 @@ def __init__(self, config_files=None, vendor_files=None, self.config_filename, self.cloud_config = self._load_config_file() _, secure_config = self._load_secure_file() if secure_config: - self.cloud_config = _merge_clouds( + self.cloud_config = _util.merge_clouds( self.cloud_config, secure_config) if not self.cloud_config: @@ -194,6 +180,10 @@ def __init__(self, config_files=None, vendor_files=None, if 'clouds' not in self.cloud_config: self.cloud_config['clouds'] = {} + # Save the other config + self.extra_config = copy.deepcopy(self.cloud_config) + self.extra_config.pop('clouds', None) + # Grab ipv6 preference settings from env client_config = self.cloud_config.get('client', {}) @@ -261,7 +251,7 @@ def __init__(self, config_files=None, vendor_files=None, self._cache_path = CACHE_PATH self._cache_class = 'dogpile.cache.null' self._cache_arguments = {} - self._cache_expiration = {} + self._cache_expirations = {} if 'cache' in self.cloud_config: cache_settings = _util.normalize_keys(self.cloud_config['cache']) @@ -283,8 +273,8 @@ def __init__(self, config_files=None, vendor_files=None, cache_settings.get('path', self._cache_path)) self._cache_arguments = cache_settings.get( 'arguments', self._cache_arguments) - self._cache_expiration = cache_settings.get( - 'expiration', self._cache_expiration) + self._cache_expirations = cache_settings.get( + 'expiration', self._cache_expirations) # Flag location to hold the peeked value of an argparse timeout value self._argv_timeout = False @@ -331,7 +321,7 @@ def get_extra_config(self, key, defaults=None): defaults = _util.normalize_keys(defaults or {}) if not key: return defaults - return _merge_clouds( + return _util.merge_clouds( defaults, _util.normalize_keys(self.cloud_config.get(key, {}))) @@ -354,27 +344,6 @@ def _load_yaml_json_file(self, filelist): return path, yaml.safe_load(f) return (None, {}) - def get_cache_expiration_time(self): - return int(self._cache_expiration_time) - - def get_cache_interval(self): - return self.get_cache_expiration_time() - - def get_cache_max_age(self): - return self.get_cache_expiration_time() - - def get_cache_path(self): - return self._cache_path - - def get_cache_class(self): - return self._cache_class - - def get_cache_arguments(self): - return copy.deepcopy(self._cache_arguments) - - def get_cache_expiration(self): - return copy.deepcopy(self._cache_expiration) - def _expand_region_name(self, region_name): return {'name': region_name, 'values': {}} @@ -1092,12 +1061,19 @@ def get_one( name=cloud_name, region_name=config['region_name'], config=config, + extra_config=self.extra_config, force_ipv4=force_ipv4, auth_plugin=auth_plugin, openstack_config=self, session_constructor=self._session_constructor, app_name=self._app_name, app_version=self._app_version, + cache_expiration_time=self._cache_expiration_time, + cache_expirations=self._cache_expirations, + cache_path=self._cache_path, + cache_class=self._cache_class, + cache_arguments=self._cache_arguments, + password_callback=self._pw_callback, ) # TODO(mordred) Backwards compat for OSC transition get_one_cloud = get_one @@ -1189,9 +1165,16 @@ def get_one_cloud_osc( name=cloud_name, region_name=config['region_name'], config=config, + extra_config=self.extra_config, force_ipv4=force_ipv4, auth_plugin=auth_plugin, openstack_config=self, + cache_expiration_time=self._cache_expiration_time, + cache_expirations=self._cache_expirations, + cache_path=self._cache_path, + cache_class=self._cache_class, + cache_arguments=self._cache_arguments, + password_callback=self._pw_callback, ) @staticmethod diff --git a/releasenotes/notes/make-cloud-region-standalone-848a2c4b5f3ebc29.yaml b/releasenotes/notes/make-cloud-region-standalone-848a2c4b5f3ebc29.yaml new file mode 100644 index 000000000..d5745f429 --- /dev/null +++ b/releasenotes/notes/make-cloud-region-standalone-848a2c4b5f3ebc29.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Updated the ``openstack.config.cloud_config.CloudRegion`` object to be + able to store and retreive cache settings and the password callback object + without needing an ``openstack.config.loader.OpenStackConfig`` object. From 5ba3ea380d870492e2eea77f268ee19232684620 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 30 Mar 2018 08:44:04 -0500 Subject: [PATCH 2024/3836] Refactor _get_version_arguments The _get_version_arguments method returns a tuple with a few values. Doing so makes reading the code harder. Make and use a utility class so that the code is easier to read. Change-Id: I5a84e7c3020584673f531f06dadf1252a63e28e6 --- openstack/config/_util.py | 12 ++++++++++++ openstack/config/cloud_region.py | 23 ++++++++++++++--------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/openstack/config/_util.py b/openstack/config/_util.py index 9fd8b925b..26f2428b9 100644 --- a/openstack/config/_util.py +++ b/openstack/config/_util.py @@ -43,3 +43,15 @@ def merge_clouds(old_dict, new_dict): else: ret[k] = v return ret + + +class VersionRequest(object): + def __init__( + self, + version=None, + min_api_version=None, + max_api_version=None, + ): + self.version = version + self.min_api_version = min_api_version + self.max_api_version = max_api_version diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index f4e682c50..b9eab3407 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -295,7 +295,7 @@ def get_service_catalog(self): """Helper method to grab the service catalog.""" return self._auth.get_access(self.get_session()).service_catalog - def _get_version_args(self, service_key, version): + def _get_version_request(self, service_key, version): """Translate OCC version args to those needed by ksa adapter. If no version is requested explicitly and we have a configured version, @@ -307,15 +307,21 @@ def _get_version_args(self, service_key, version): If version is not set and we don't have a configured version, default to latest. """ + version_request = _util.VersionRequest() if version == 'latest': - return None, None, 'latest' + version_request.max_api_version = 'latest' + return version_request + if not version: version = self.get_api_version(service_key) + # Octavia doens't have a version discovery document. Hard-code an # exception to this logic for now. if not version and service_key not in ('load-balancer',): - return None, None, 'latest' - return version, None, None + version_request.max_api_version = 'latest' + else: + version_request.version = version + return version_request def get_session_client( self, service_key, version=None, constructor=adapter.Adapter, @@ -333,8 +339,7 @@ def get_session_client( and it will work like you think. """ - (version, min_version, max_version) = self._get_version_args( - service_key, version) + version_request = self._get_version_request(service_key, version) return constructor( session=self.get_session(), @@ -342,9 +347,9 @@ def get_session_client( service_name=self.get_service_name(service_key), interface=self.get_interface(service_key), region_name=self.region_name, - version=version, - min_version=min_version, - max_version=max_version, + version=version_request.version, + min_version=version_request.min_api_version, + max_version=version_request.max_api_version, **kwargs) def _get_highest_endpoint(self, service_types, kwargs): From 242ad35020d9ec94e146826be040c764cdde42fc Mon Sep 17 00:00:00 2001 From: Pavlo Shchelokovskyy Date: Wed, 14 Mar 2018 10:32:00 +0200 Subject: [PATCH 2025/3836] Use 'none' auth plugin since keystoneauth 3.0.0 there is a proper 'none' auth plugin to use for standalone services deployed w/o Keystone support. Use this plugin instead of 'admin_token' for auth_type specified as variants of 'None'. Change-Id: I425f03574858dd582118d5544381af703134da72 --- openstack/config/loader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openstack/config/loader.py b/openstack/config/loader.py index aa766dd3c..952571c46 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -874,7 +874,7 @@ def auth_config_hook(self, config): return config def _get_auth_loader(self, config): - # Re-use the admin_token plugin for the "None" plugin + # Use the 'none' plugin for variants of None specified, # since it does not look up endpoints or tokens but rather # does a passthrough. This is useful for things like Ironic # that have a keystoneless operational mode, but means we're @@ -1240,6 +1240,7 @@ def set_one_cloud(config_file, cloud, set_config=None): with open(config_file, 'w') as fh: yaml.safe_dump(cur_config, fh, default_flow_style=False) + if __name__ == '__main__': config = OpenStackConfig().get_all_clouds() for cloud in config: From f24ca69ce2b77bc5dc2db54a215c5a099b549407 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 31 Mar 2018 07:47:57 -0500 Subject: [PATCH 2026/3836] Add release note for added masakari support We forgot to add a release note when we landed the masakari patch. Change-Id: I48448ee7ea577ddc515b2232c85859b892fa19e3 --- .../notes/add-masakara-support-3f7df4436ac869cf.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 releasenotes/notes/add-masakara-support-3f7df4436ac869cf.yaml diff --git a/releasenotes/notes/add-masakara-support-3f7df4436ac869cf.yaml b/releasenotes/notes/add-masakara-support-3f7df4436ac869cf.yaml new file mode 100644 index 000000000..baa5fd37e --- /dev/null +++ b/releasenotes/notes/add-masakara-support-3f7df4436ac869cf.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Ported in support for masakari/``instance_ha`` service from + `python-masakariclient`. From 082c6e6099f1096142bc9dcf970e4587228fa6e7 Mon Sep 17 00:00:00 2001 From: Romain Acciari Date: Sun, 11 Mar 2018 21:28:14 +0100 Subject: [PATCH 2027/3836] create_subnet: Add filter on tenant_id if specified Change-Id: I073b53c6bb34fa904e9595169b135fde7c393d0e Task: 12102 Story: 2001744 --- openstack/cloud/openstackcloud.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index fef768e13..e7acff53d 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -7733,7 +7733,12 @@ def create_subnet(self, network_name_or_id, cidr=None, ip_version=4, :raises: OpenStackCloudException on operation error. """ - network = self.get_network(network_name_or_id) + if tenant_id is not None: + filters = {'tenant_id': tenant_id} + else: + filters = None + + network = self.get_network(network_name_or_id, filters) if not network: raise OpenStackCloudException( "Network %s not found." % network_name_or_id) From 4bfba66ced4f4842f54207dc74f5a2fe31e2a828 Mon Sep 17 00:00:00 2001 From: "Yuanbin.Chen" Date: Wed, 4 Apr 2018 16:53:37 +0800 Subject: [PATCH 2028/3836] Fix resource not exist the resource.status error Change-Id: If04c1f03fa474b496d208d906007d2003017932b Signed-off-by: Yuanbin.Chen --- openstack/resource.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/resource.py b/openstack/resource.py index f16928653..afdaa5288 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1080,12 +1080,12 @@ def wait_for_status(session, resource, status, failures, interval, wait): message=msg, wait=interval): resource = resource.get(session) - new_status = resource.status if not resource: raise exceptions.ResourceFailure( "{name} went away while waiting for {status}".format( name=name, status=status)) + new_status = resource.status if new_status.lower() == status.lower(): return resource if resource.status.lower() in failures: From 76b941dbb3f2ab700af9573f1d31b974f5fa851d Mon Sep 17 00:00:00 2001 From: Sidharth Surana Date: Tue, 3 Apr 2018 02:04:31 -0700 Subject: [PATCH 2029/3836] Strip the version prefix from the next link for pagination keystoneauth1 appends the version prefix to the url causing the pagination url to be incorrect when using the url prefix from next link. Thus we are stripping the version prefix from the next link for pagination to work correctly Change-Id: Iad8e69d20783cfe59c3e86bd6980da7dee802869 Closes-Bug: 1748534 --- openstack/resource.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openstack/resource.py b/openstack/resource.py index f16928653..d64d99a03 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -954,6 +954,9 @@ def _get_next_link(cls, uri, response, data, marker, limit, total_yielded): break # Glance has a next field in the main body next_link = next_link or data.get('next') + if next_link and next_link.startswith('/v'): + next_link = next_link[next_link.find('/', 1) + 1:] + if not next_link and 'next' in response.links: # RFC5988 specifies Link headers and requests parses them if they # are there. We prefer link dicts in resource body, but if those From 9b188ba97d1809b81f6ece3b7395b14ebd00a363 Mon Sep 17 00:00:00 2001 From: "Yuanbin.Chen" Date: Sun, 8 Apr 2018 11:49:12 +0800 Subject: [PATCH 2030/3836] Fix wait for futures append 'result' error Change-Id: I3bdd91148abfec5b74a3aa8b6cac656dc7c645ab Signed-off-by: Yuanbin.Chen --- openstack/task_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/task_manager.py b/openstack/task_manager.py index 8a49ba4b2..baa6861ec 100644 --- a/openstack/task_manager.py +++ b/openstack/task_manager.py @@ -199,5 +199,5 @@ def wait_for_futures(futures, raise_on_error=True, log=_log): raise # If we get an exception, put the result into a list so we # can try again - retries.append(result) + retries.append(completed.result()) return results, retries From 480789522a448d84964b02425f5dea7ea9fc7891 Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Thu, 15 Mar 2018 21:41:00 +0800 Subject: [PATCH 2031/3836] Allow cascade deletion of load balancer According to the v2 api reference: https://developer.openstack.org/api-ref/load-balancer/v2/#remove-a-load-balancer The optional parameter cascade when defined as true will delete all child objects of the load balancer. Change-Id: I3290eeae6c11fc9c42bffc19ccac9c8375932f1e --- openstack/load_balancer/v2/_proxy.py | 7 ++- openstack/load_balancer/v2/load_balancer.py | 19 +++++++ .../unit/load_balancer/test_load_balancer.py | 49 +++++++++++++++++++ .../tests/unit/load_balancer/test_proxy.py | 36 ++++++++++++-- 4 files changed, 107 insertions(+), 4 deletions(-) diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index 87c979a0e..a81df06b0 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -55,7 +55,8 @@ def load_balancers(self, **query): """ return self._list(_lb.LoadBalancer, paginated=True, **query) - def delete_load_balancer(self, load_balancer, ignore_missing=True): + def delete_load_balancer(self, load_balancer, ignore_missing=True, + cascade=False): """Delete a load balancer :param load_balancer: The load_balancer can be either the name or a @@ -66,9 +67,13 @@ def delete_load_balancer(self, load_balancer, ignore_missing=True): the load balancer does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent load balancer. + :param bool cascade: If true will delete all child objects of + the load balancer. :returns: ``None`` """ + load_balancer = self._get_resource(_lb.LoadBalancer, load_balancer) + load_balancer.cascade = cascade return self._delete(_lb.LoadBalancer, load_balancer, ignore_missing=ignore_missing) diff --git a/openstack/load_balancer/v2/load_balancer.py b/openstack/load_balancer/v2/load_balancer.py index de640aa1f..c41d2d9dc 100644 --- a/openstack/load_balancer/v2/load_balancer.py +++ b/openstack/load_balancer/v2/load_balancer.py @@ -67,3 +67,22 @@ class LoadBalancer(resource.Resource): vip_port_id = resource.Body('vip_port_id') #: VIP subnet ID vip_subnet_id = resource.Body('vip_subnet_id') + + def delete(self, session, error_message=None): + request = self._prepare_request() + headers = { + "Accept": "" + } + + request.headers.update(headers) + params = {} + if (hasattr(self, 'cascade') and isinstance(self.cascade, bool) + and self.cascade): + params['cascade'] = True + response = session.delete(request.url, + headers=headers, + params=params) + + self._translate_response(response, has_body=False, + error_message=error_message) + return self diff --git a/openstack/tests/unit/load_balancer/test_load_balancer.py b/openstack/tests/unit/load_balancer/test_load_balancer.py index 0831d881d..53a2bf541 100644 --- a/openstack/tests/unit/load_balancer/test_load_balancer.py +++ b/openstack/tests/unit/load_balancer/test_load_balancer.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import mock from openstack.tests.unit import base import uuid @@ -79,3 +80,51 @@ def test_make_it(self): test_load_balancer.vip_port_id) self.assertEqual(EXAMPLE['vip_subnet_id'], test_load_balancer.vip_subnet_id) + + def test_delete_non_cascade(self): + sess = mock.Mock() + resp = mock.Mock() + sess.delete.return_value = resp + + sot = load_balancer.LoadBalancer(**EXAMPLE) + sot.cascade = False + sot._translate_response = mock.Mock() + sot.delete(sess) + + url = 'v2.0/lbaas/loadbalancers/%(lb)s' % { + 'lb': EXAMPLE['id'] + } + headers = {'Accept': ''} + params = {} + sess.delete.assert_called_with(url, + headers=headers, + params=params) + sot._translate_response.assert_called_once_with( + resp, + error_message=None, + has_body=False, + ) + + def test_delete_cascade(self): + sess = mock.Mock() + resp = mock.Mock() + sess.delete.return_value = resp + + sot = load_balancer.LoadBalancer(**EXAMPLE) + sot.cascade = True + sot._translate_response = mock.Mock() + sot.delete(sess) + + url = 'v2.0/lbaas/loadbalancers/%(lb)s' % { + 'lb': EXAMPLE['id'] + } + headers = {'Accept': ''} + params = {'cascade': True} + sess.delete.assert_called_with(url, + headers=headers, + params=params) + sot._translate_response.assert_called_once_with( + resp, + error_message=None, + has_body=False, + ) diff --git a/openstack/tests/unit/load_balancer/test_proxy.py b/openstack/tests/unit/load_balancer/test_proxy.py index f1c07e28e..d2c84e155 100644 --- a/openstack/tests/unit/load_balancer/test_proxy.py +++ b/openstack/tests/unit/load_balancer/test_proxy.py @@ -11,6 +11,7 @@ # under the License. import uuid +import mock from openstack.load_balancer.v2 import _proxy from openstack.load_balancer.v2 import health_monitor @@ -20,6 +21,7 @@ from openstack.load_balancer.v2 import load_balancer as lb from openstack.load_balancer.v2 import member from openstack.load_balancer.v2 import pool +from openstack import proxy as proxy_base from openstack.tests.unit import test_proxy_base @@ -45,9 +47,37 @@ def test_load_balancer_create(self): self.verify_create(self.proxy.create_load_balancer, lb.LoadBalancer) - def test_load_balancer_delete(self): - self.verify_delete(self.proxy.delete_load_balancer, - lb.LoadBalancer, True) + @mock.patch.object(proxy_base.Proxy, '_get_resource') + def test_load_balancer_delete_non_cascade(self, mock_get_resource): + fake_load_balancer = mock.Mock() + fake_load_balancer.id = "load_balancer_id" + mock_get_resource.return_value = fake_load_balancer + self._verify2("openstack.proxy.Proxy._delete", + self.proxy.delete_load_balancer, + method_args=["resource_or_id", True, + False], + expected_args=[lb.LoadBalancer, + fake_load_balancer], + expected_kwargs={"ignore_missing": True}) + self.assertFalse(fake_load_balancer.cascade) + mock_get_resource.assert_called_once_with(lb.LoadBalancer, + "resource_or_id") + + @mock.patch.object(proxy_base.Proxy, '_get_resource') + def test_load_balancer_delete_cascade(self, mock_get_resource): + fake_load_balancer = mock.Mock() + fake_load_balancer.id = "load_balancer_id" + mock_get_resource.return_value = fake_load_balancer + self._verify2("openstack.proxy.Proxy._delete", + self.proxy.delete_load_balancer, + method_args=["resource_or_id", True, + True], + expected_args=[lb.LoadBalancer, + fake_load_balancer], + expected_kwargs={"ignore_missing": True}) + self.assertTrue(fake_load_balancer.cascade) + mock_get_resource.assert_called_once_with(lb.LoadBalancer, + "resource_or_id") def test_load_balancer_find(self): self.verify_find(self.proxy.find_load_balancer, From 53cff8f39cf01ca32684e0140805f27ee98f9814 Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Sat, 14 Apr 2018 09:25:42 +0800 Subject: [PATCH 2032/3836] Allow members to be set as "backup" This is a follow up of octavia-api changes made in: I953abe71a0988da78efc6b3961f7518c81c2a06d Change-Id: Ibe7dbc9d1b2fcace7ada0bb4e0659e28de65dfa2 --- openstack/load_balancer/v2/member.py | 6 +++++- openstack/tests/unit/load_balancer/test_member.py | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/openstack/load_balancer/v2/member.py b/openstack/load_balancer/v2/member.py index da8200491..1e6d0fc23 100644 --- a/openstack/load_balancer/v2/member.py +++ b/openstack/load_balancer/v2/member.py @@ -30,7 +30,7 @@ class Member(resource.Resource): _query_mapping = resource.QueryParameters( 'address', 'name', 'protocol_port', 'subnet_id', 'weight', 'created_at', 'updated_at', 'provisioning_status', 'operating_status', - 'project_id', 'monitor_address', 'monitor_port', + 'project_id', 'monitor_address', 'monitor_port', 'backup', is_admin_state_up='admin_state_up', ) @@ -67,3 +67,7 @@ class Member(resource.Resource): #: with a weight of 10 receives five times as much traffic as a member #: with weight of 2. weight = resource.Body('weight', type=int) + #: A bool value that indicates whether the member is a backup or not. + #: Backup members only receive traffic when all non-backup members + #: are down. + backup = resource.Body('backup', type=bool) diff --git a/openstack/tests/unit/load_balancer/test_member.py b/openstack/tests/unit/load_balancer/test_member.py index 7ad614ee5..cc36c68c9 100644 --- a/openstack/tests/unit/load_balancer/test_member.py +++ b/openstack/tests/unit/load_balancer/test_member.py @@ -28,6 +28,7 @@ 'protocol_port': 5, 'subnet_id': uuid.uuid4(), 'weight': 7, + 'backup': False, } @@ -60,3 +61,4 @@ def test_make_it(self): self.assertEqual(EXAMPLE['protocol_port'], test_member.protocol_port) self.assertEqual(EXAMPLE['subnet_id'], test_member.subnet_id) self.assertEqual(EXAMPLE['weight'], test_member.weight) + self.assertFalse(test_member.backup) From 088fe7782897621c606644dd3b8c8f93822bd478 Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Sat, 14 Apr 2018 09:47:50 +0800 Subject: [PATCH 2033/3836] Add timeout options for listener This is a follow up of octavia-api change made in: Id4667201c1bfaa06f7af9060c936ba00c2f314f9 Change-Id: I39be392dd4b09e604b02def552981dfcb5c446a4 --- openstack/load_balancer/v2/listener.py | 11 +++++++++++ openstack/tests/unit/load_balancer/test_listener.py | 12 ++++++++++++ 2 files changed, 23 insertions(+) diff --git a/openstack/load_balancer/v2/listener.py b/openstack/load_balancer/v2/listener.py index 3ef086fc6..0738466d6 100644 --- a/openstack/load_balancer/v2/listener.py +++ b/openstack/load_balancer/v2/listener.py @@ -32,6 +32,8 @@ class Listener(resource.Resource): 'description', 'name', 'project_id', 'protocol', 'protocol_port', 'created_at', 'updated_at', 'provisioning_status', 'operating_status', 'sni_container_refs', 'insert_headers', 'load_balancer_id', + 'timeout_client_data', 'timeout_member_connect', + 'timeout_member_data', 'timeout_tcp_inspect', is_admin_state_up='admin_state_up', ) @@ -79,3 +81,12 @@ class Listener(resource.Resource): sni_container_refs = resource.Body('sni_container_refs') #: Timestamp when the listener was last updated. updated_at = resource.Body('updated_at') + #: Frontend client inactivity timeout in milliseconds. + timeout_client_data = resource.Body('timeout_client_data', type=int) + #: Backend member connection timeout in milliseconds. + timeout_member_connect = resource.Body('timeout_member_connect', type=int) + #: Backend member inactivity timeout in milliseconds. + timeout_member_data = resource.Body('timeout_member_data', type=int) + #: Time, in milliseconds, to wait for additional TCP packets for content + #: inspection. + timeout_tcp_inspect = resource.Body('timeout_tcp_inspect', type=int) diff --git a/openstack/tests/unit/load_balancer/test_listener.py b/openstack/tests/unit/load_balancer/test_listener.py index 940b3d878..3eeebf212 100644 --- a/openstack/tests/unit/load_balancer/test_listener.py +++ b/openstack/tests/unit/load_balancer/test_listener.py @@ -36,6 +36,10 @@ 'updated_at': '2017-07-17T12:16:57.233772', 'operating_status': 'ONLINE', 'provisioning_status': 'ACTIVE', + 'timeout_client_data': 50000, + 'timeout_member_connect': 5000, + 'timeout_member_data': 50000, + 'timeout_tcp_inspect': 0, } @@ -82,3 +86,11 @@ def test_make_it(self): test_listener.provisioning_status) self.assertEqual(EXAMPLE['operating_status'], test_listener.operating_status) + self.assertEqual(EXAMPLE['timeout_client_data'], + test_listener.timeout_client_data) + self.assertEqual(EXAMPLE['timeout_member_connect'], + test_listener.timeout_member_connect) + self.assertEqual(EXAMPLE['timeout_member_data'], + test_listener.timeout_member_data) + self.assertEqual(EXAMPLE['timeout_tcp_inspect'], + test_listener.timeout_tcp_inspect) From 60a3ef3eb905943988cd56ba288abeb744c214d6 Mon Sep 17 00:00:00 2001 From: Graham Hayes Date: Wed, 11 Apr 2018 16:43:57 +0100 Subject: [PATCH 2034/3836] Fix DNS Recordset CRUD If a zone name is supplied to a recordset function we should lookup the ID to use that in the URL, as the name in the URL will return a 404. The `name` parameter for a recordset needs to be the full FQDN including the zone part. Fix the tests accordingly. Looking up recordsets via name does not work tests for that behaviour have been removed. Also harmonised the naming of vars in the recordset section to make this clearer. Also add designate to the services deployed for functional tests These issues with the DNS related cloud functions went undiscovered for some time because even though there are tests for it, those get skipped when designate isn't deployed. Change-Id: I60d6400631f31cba89a6d07382c380006bd2ded2 Signed-off-by: Graham Hayes Co-Authored-By: Jens Harbott --- .zuul.yaml | 4 + openstack/cloud/openstackcloud.py | 25 +++-- .../tests/functional/cloud/test_recordset.py | 94 +++++++++++++++---- openstack/tests/unit/cloud/test_recordset.py | 42 ++++++++- 4 files changed, 140 insertions(+), 25 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index f0ca22c03..7fa8f59da 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -108,6 +108,7 @@ description: | Run openstacksdk functional tests against a master devstack required-projects: + - openstack/designate - openstack/octavia vars: devstack_localrc: @@ -125,9 +126,11 @@ certificates: cert_manager: local_cert_manager devstack_plugins: + designate: https://git.openstack.org/openstack/designate neutron: https://git.openstack.org/openstack/neutron octavia: https://git.openstack.org/openstack/octavia devstack_services: + designate: true octavia: true o-api: true o-cw: true @@ -136,6 +139,7 @@ neutron-qos: true neutron-trunk: true tox_environment: + OPENSTACKSDK_HAS_DESIGNATE: 1 OPENSTACKSDK_HAS_OCTAVIA: 1 - job: diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index fef768e13..390558793 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -8469,8 +8469,12 @@ def list_recordsets(self, zone): :returns: A list of recordsets. """ + zone_obj = self.get_zone(zone) + if zone_obj is None: + raise OpenStackCloudException( + "Zone %s not found." % zone) return self._dns_client.get( - "/zones/{zone_id}/recordsets".format(zone_id=zone), + "/zones/{zone_id}/recordsets".format(zone_id=zone_obj['id']), error_message="Error fetching recordsets list") def get_recordset(self, zone, name_or_id): @@ -8483,10 +8487,14 @@ def get_recordset(self, zone, name_or_id): found. """ + zone_obj = self.get_zone(zone) + if zone_obj is None: + raise OpenStackCloudException( + "Zone %s not found." % zone) try: return self._dns_client.get( "/zones/{zone_id}/recordsets/{recordset_id}".format( - zone_id=zone, recordset_id=name_or_id), + zone_id=zone_obj['id'], recordset_id=name_or_id), error_message="Error fetching recordset") except Exception: return None @@ -8511,7 +8519,8 @@ def create_recordset(self, zone, name, recordset_type, records, :raises: OpenStackCloudException on operation error. """ - if self.get_zone(zone) is None: + zone_obj = self.get_zone(zone) + if zone_obj is None: raise OpenStackCloudException( "Zone %s not found." % zone) @@ -8531,7 +8540,7 @@ def create_recordset(self, zone, name, recordset_type, records, body['ttl'] = ttl return self._dns_client.post( - "/zones/{zone_id}/recordsets".format(zone_id=zone), + "/zones/{zone_id}/recordsets".format(zone_id=zone_obj['id']), json=body, error_message="Error creating recordset {name}".format(name=name)) @@ -8577,19 +8586,19 @@ def delete_recordset(self, zone, name_or_id): :raises: OpenStackCloudException on operation error. """ - zone = self.get_zone(zone) - if zone is None: + zone_obj = self.get_zone(zone) + if zone_obj is None: self.log.debug("Zone %s not found for deleting", zone) return False - recordset = self.get_recordset(zone['id'], name_or_id) + recordset = self.get_recordset(zone_obj['id'], name_or_id) if recordset is None: self.log.debug("Recordset %s not found for deleting", name_or_id) return False self._dns_client.delete( "/zones/{zone_id}/recordsets/{recordset_id}".format( - zone_id=zone['id'], recordset_id=name_or_id), + zone_id=zone_obj['id'], recordset_id=name_or_id), error_message="Error deleting recordset {0}".format(name_or_id)) return True diff --git a/openstack/tests/functional/cloud/test_recordset.py b/openstack/tests/functional/cloud/test_recordset.py index 583bab05c..c31622335 100644 --- a/openstack/tests/functional/cloud/test_recordset.py +++ b/openstack/tests/functional/cloud/test_recordset.py @@ -16,6 +16,8 @@ Functional tests for `shade` recordset methods. """ +import random +import string from testtools import content @@ -29,11 +31,73 @@ def setUp(self): if not self.user_cloud.has_service('dns'): self.skipTest('dns service not supported by cloud') - def test_recordsets(self): + def test_recordsets_with_zone_id(self): '''Test DNS recordsets functionality''' - zone = 'example2.net.' + sub = ''.join(random.choice(string.ascii_lowercase) for _ in range(6)) + + zone = '%s.example2.net.' % sub email = 'test@example2.net' - name = 'www' + name = 'www.%s' % zone + type_ = 'a' + description = 'Test recordset' + ttl = 3600 + records = ['192.168.1.1'] + + self.addDetail('zone', content.text_content(zone)) + self.addDetail('recordset', content.text_content(name)) + + # Create a zone to hold the tested recordset + zone_obj = self.user_cloud.create_zone(name=zone, email=email) + + # Test we can create a recordset and we get it returned + created_recordset = self.user_cloud.create_recordset(zone_obj['id'], + name, + type_, + records, + description, ttl) + self.addCleanup(self.cleanup, zone, created_recordset['id']) + + self.assertEqual(created_recordset['zone_id'], zone_obj['id']) + self.assertEqual(created_recordset['name'], name) + self.assertEqual(created_recordset['type'], type_.upper()) + self.assertEqual(created_recordset['records'], records) + self.assertEqual(created_recordset['description'], description) + self.assertEqual(created_recordset['ttl'], ttl) + + # Test that we can list recordsets + recordsets = self.user_cloud.list_recordsets(zone_obj['id'],) + self.assertIsNotNone(recordsets) + + # Test we get the same recordset with the get_recordset method + get_recordset = self.user_cloud.get_recordset(zone_obj['id'], + created_recordset['id']) + self.assertEqual(get_recordset['id'], created_recordset['id']) + + # Test we can update a field on the recordset and only that field + # is updated + updated_recordset = self.user_cloud.update_recordset( + zone_obj['id'], + created_recordset['id'], + ttl=7200) + self.assertEqual(updated_recordset['id'], created_recordset['id']) + self.assertEqual(updated_recordset['name'], name) + self.assertEqual(updated_recordset['type'], type_.upper()) + self.assertEqual(updated_recordset['records'], records) + self.assertEqual(updated_recordset['description'], description) + self.assertEqual(updated_recordset['ttl'], 7200) + + # Test we can delete and get True returned + deleted_recordset = self.user_cloud.delete_recordset( + zone, created_recordset['id']) + self.assertTrue(deleted_recordset) + + def test_recordsets_with_zone_name(self): + '''Test DNS recordsets functionality''' + sub = ''.join(random.choice(string.ascii_lowercase) for _ in range(6)) + + zone = '%s.example2.net.' % sub + email = 'test@example2.net' + name = 'www.%s' % zone type_ = 'a' description = 'Test recordset' ttl = 3600 @@ -41,7 +105,6 @@ def test_recordsets(self): self.addDetail('zone', content.text_content(zone)) self.addDetail('recordset', content.text_content(name)) - self.addCleanup(self.cleanup, zone, name) # Create a zone to hold the tested recordset zone_obj = self.user_cloud.create_zone(name=zone, email=email) @@ -50,8 +113,10 @@ def test_recordsets(self): created_recordset = self.user_cloud.create_recordset(zone, name, type_, records, description, ttl) + self.addCleanup(self.cleanup, zone, created_recordset['id']) + self.assertEqual(created_recordset['zone_id'], zone_obj['id']) - self.assertEqual(created_recordset['name'], name + '.' + zone) + self.assertEqual(created_recordset['name'], name) self.assertEqual(created_recordset['type'], type_.upper()) self.assertEqual(created_recordset['records'], records) self.assertEqual(created_recordset['description'], description) @@ -66,17 +131,14 @@ def test_recordsets(self): created_recordset['id']) self.assertEqual(get_recordset['id'], created_recordset['id']) - # Test the get method also works by name - get_recordset = self.user_cloud.get_recordset(zone, name + '.' + zone) - self.assertEqual(get_recordset['id'], created_recordset['id']) - # Test we can update a field on the recordset and only that field # is updated - updated_recordset = self.user_cloud.update_recordset(zone_obj['id'], - name + '.' + zone, - ttl=7200) + updated_recordset = self.user_cloud.update_recordset( + zone_obj['id'], + created_recordset['id'], + ttl=7200) self.assertEqual(updated_recordset['id'], created_recordset['id']) - self.assertEqual(updated_recordset['name'], name + '.' + zone) + self.assertEqual(updated_recordset['name'], name) self.assertEqual(updated_recordset['type'], type_.upper()) self.assertEqual(updated_recordset['records'], records) self.assertEqual(updated_recordset['description'], description) @@ -84,10 +146,10 @@ def test_recordsets(self): # Test we can delete and get True returned deleted_recordset = self.user_cloud.delete_recordset( - zone, name + '.' + zone) + zone, created_recordset['id']) self.assertTrue(deleted_recordset) - def cleanup(self, zone_name, recordset_name): + def cleanup(self, zone_name, recordset_id): self.user_cloud.delete_recordset( - zone_name, recordset_name + '.' + zone_name) + zone_name, recordset_id) self.user_cloud.delete_zone(zone_name) diff --git a/openstack/tests/unit/cloud/test_recordset.py b/openstack/tests/unit/cloud/test_recordset.py index c8f911032..7b7aa1c8e 100644 --- a/openstack/tests/unit/cloud/test_recordset.py +++ b/openstack/tests/unit/cloud/test_recordset.py @@ -118,6 +118,14 @@ def test_update_recordset(self): "links": {}, "metadata": { 'total_count': 1}}), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['v2', 'zones']), + json={ + "zones": [zone], + "links": {}, + "metadata": { + 'total_count': 1}}), dict(method='GET', uri=self.get_mock_url( 'dns', 'public', @@ -146,6 +154,14 @@ def test_delete_recordset(self): "links": {}, "metadata": { 'total_count': 1}}), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['v2', 'zones']), + json={ + "zones": [zone], + "links": {}, + "metadata": { + 'total_count': 1}}), dict(method='GET', uri=self.get_mock_url( 'dns', 'public', @@ -164,6 +180,14 @@ def test_delete_recordset(self): def test_get_recordset_by_id(self): self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['v2', 'zones']), + json={ + "zones": [zone], + "links": {}, + "metadata": { + 'total_count': 1}}), dict(method='GET', uri=self.get_mock_url( 'dns', 'public', @@ -176,12 +200,20 @@ def test_get_recordset_by_id(self): def test_get_recordset_by_name(self): self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['v2', 'zones']), + json={ + "zones": [zone], + "links": {}, + "metadata": { + 'total_count': 1}}), dict(method='GET', uri=self.get_mock_url( 'dns', 'public', append=['v2', 'zones', '1', 'recordsets', new_recordset['name']]), - json=new_recordset), + json=new_recordset) ]) recordset = self.cloud.get_recordset('1', new_recordset['name']) self.assertEqual(new_recordset['name'], recordset['name']) @@ -190,6 +222,14 @@ def test_get_recordset_by_name(self): def test_get_recordset_not_found_returns_false(self): recordset_name = "www.nonexistingrecord.net." self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['v2', 'zones']), + json={ + "zones": [zone], + "links": {}, + "metadata": { + 'total_count': 1}}), dict(method='GET', uri=self.get_mock_url( 'dns', 'public', From 91ab0894af19bdb4e9c8f6d3fc0dfdcc61091795 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 14 Apr 2018 11:04:45 -0500 Subject: [PATCH 2035/3836] Temporarily disable neutron-grenade pip10 has broken the world. Sigh. Depends-On: https://review.openstack.org/561427 Change-Id: Ic9f6fbbf21851036595d0a1c92898d13e0534c9d --- .zuul.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index f0ca22c03..a0995587f 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -224,7 +224,8 @@ - openstacksdk-functional-devstack-python3 - osc-functional-devstack-tips: voting: false - - neutron-grenade + - neutron-grenade: + voting: false - openstack-tox-lower-constraints gate: jobs: @@ -233,5 +234,4 @@ sphinx_python: python3 - openstacksdk-functional-devstack - openstacksdk-functional-devstack-python3 - - neutron-grenade - openstack-tox-lower-constraints From 08169b6f9fca4acd8ee422373e08c8ecc4deb6da Mon Sep 17 00:00:00 2001 From: Jens Harbott Date: Wed, 4 Apr 2018 13:42:05 +0000 Subject: [PATCH 2036/3836] Add support for DNS attributes for floating IPs Add the DNS domain and name attributes for floating IPs. Change-Id: I79c0edc7efd7cf6c3f44dd7af3b38eb34a86bcba --- openstack/network/v2/floating_ip.py | 4 ++++ openstack/tests/unit/network/v2/test_floating_ip.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/openstack/network/v2/floating_ip.py b/openstack/network/v2/floating_ip.py index 2a4a6e1f3..5e06cf689 100644 --- a/openstack/network/v2/floating_ip.py +++ b/openstack/network/v2/floating_ip.py @@ -42,6 +42,10 @@ class FloatingIP(resource.Resource, tag.TagMixin): created_at = resource.Body('created_at') #: The floating IP description. description = resource.Body('description') + #: The DNS domain. + dns_domain = resource.Body('dns_domain') + #: The DNS name. + dns_name = resource.Body('dns_name') #: The fixed IP address associated with the floating IP. If you #: intend to associate the floating IP with a fixed IP at creation #: time, then you must indicate the identifier of the internal port. diff --git a/openstack/tests/unit/network/v2/test_floating_ip.py b/openstack/tests/unit/network/v2/test_floating_ip.py index d520004fd..34ca8d861 100644 --- a/openstack/tests/unit/network/v2/test_floating_ip.py +++ b/openstack/tests/unit/network/v2/test_floating_ip.py @@ -28,6 +28,8 @@ 'tenant_id': '6', 'router_id': '7', 'description': '8', + 'dns_domain': '9', + 'dns_name': '10', 'status': 'ACTIVE', 'revision_number': 12, 'updated_at': '13', @@ -63,6 +65,8 @@ def test_make_it(self): self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) self.assertEqual(EXAMPLE['router_id'], sot.router_id) self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['dns_domain'], sot.dns_domain) + self.assertEqual(EXAMPLE['dns_name'], sot.dns_name) self.assertEqual(EXAMPLE['status'], sot.status) self.assertEqual(EXAMPLE['revision_number'], sot.revision_number) self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) From 1fc0ea2d3bbd41a9f5360915d6eab5be68954797 Mon Sep 17 00:00:00 2001 From: Jens Harbott Date: Thu, 5 Apr 2018 12:40:24 +0000 Subject: [PATCH 2037/3836] Add functional tests for Neutron DNS extension Since with [0] we now support setting DNS domain and name attributes for floating IPs, add them to the functional test. Enable the neutron-dns service for the functional tests. [0] https://review.openstack.org/558820 Change-Id: Ie856e275a1bde03d70a8c3391593e987743d45fe --- .zuul.yaml | 1 + openstack/tests/functional/network/v2/test_floating_ip.py | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index 84102996e..08c83cbf6 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -136,6 +136,7 @@ o-cw: true o-hm: true o-hk: true + neutron-dns: true neutron-qos: true neutron-trunk: true tox_environment: diff --git a/openstack/tests/functional/network/v2/test_floating_ip.py b/openstack/tests/functional/network/v2/test_floating_ip.py index 8ad4cbc43..34799c566 100644 --- a/openstack/tests/functional/network/v2/test_floating_ip.py +++ b/openstack/tests/functional/network/v2/test_floating_ip.py @@ -31,6 +31,8 @@ class TestFloatingIP(base.BaseFunctionalTest): ROT_ID = None PORT_ID = None FIP = None + DNS_DOMAIN = "example.org." + DNS_NAME = "fip1" def setUp(self): super(TestFloatingIP, self).setUp() @@ -68,7 +70,9 @@ def setUp(self): assert isinstance(prt, port.Port) self.PORT_ID = prt.id # Create Floating IP. - fip = self.conn.network.create_ip(floating_network_id=self.EXT_NET_ID) + fip = self.conn.network.create_ip( + floating_network_id=self.EXT_NET_ID, + dns_domain=self.DNS_DOMAIN, dns_name=self.DNS_NAME) assert isinstance(fip, floating_ip.FloatingIP) self.FIP = fip @@ -139,6 +143,8 @@ def test_get(self): self.assertEqual(self.FIP.fixed_ip_address, sot.fixed_ip_address) self.assertEqual(self.FIP.port_id, sot.port_id) self.assertEqual(self.FIP.router_id, sot.router_id) + self.assertEqual(self.DNS_DOMAIN, sot.dns_domain) + self.assertEqual(self.DNS_NAME, sot.dns_name) def test_list(self): ids = [o.id for o in self.conn.network.ips()] From cc6dc84e721e782cf8d1c78c453f64bacd69e490 Mon Sep 17 00:00:00 2001 From: Kiseok Kim Date: Fri, 20 Apr 2018 03:05:24 +0000 Subject: [PATCH 2038/3836] Fix typo in README.rst Trivial fix. Change-Id: I8ce671350912879dcefd674da0d6dcd011f2c4db --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index c68874626..24c9fc556 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ openstacksdk ============ -openstacksdk is a client library for for building applications to work +openstacksdk is a client library for building applications to work with OpenStack clouds. The project aims to provide a consistent and complete set of interactions with OpenStack's many services, along with complete documentation, examples, and tools. From c2cde29c184e9acdaa56113b3f71845cebb8eb4a Mon Sep 17 00:00:00 2001 From: melissaml Date: Sat, 21 Apr 2018 04:30:49 +0800 Subject: [PATCH 2039/3836] Trivial: Update pypi url to new url Pypi url changed from [1] to [2] [1] https://pypi.python.org/pypi/ [2] https://pypi.org/project/ Change-Id: I398e0859f6c7c4975f00cf436505fccf5eb5ec4f --- README.rst | 2 +- doc/source/user/index.rst | 2 +- doc/source/user/multi-cloud-demo.rst | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index c68874626..71ca4202e 100644 --- a/README.rst +++ b/README.rst @@ -158,6 +158,6 @@ Links * `Issue Tracker `_ * `Code Review `_ * `Documentation `_ -* `PyPI `_ +* `PyPI `_ * `Mailing list `_ * `Bugs `_ diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index 892994de5..e88e7e298 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -8,7 +8,7 @@ Installation ------------ The OpenStack SDK is available on -`PyPI `_ under the name +`PyPI `_ under the name **openstacksdk**. To install it, use ``pip``:: $ pip install openstacksdk diff --git a/doc/source/user/multi-cloud-demo.rst b/doc/source/user/multi-cloud-demo.rst index 9fd8c6c91..de529d598 100644 --- a/doc/source/user/multi-cloud-demo.rst +++ b/doc/source/user/multi-cloud-demo.rst @@ -12,7 +12,7 @@ walk through it like a presentation, install `presentty` and run: The content is hopefully helpful even if it's not being narrated, so it's being included in the `shade` docs. -.. _presentty: https://pypi.python.org/pypi/presentty +.. _presentty: https://pypi.org/project/presentty Using Multiple OpenStack Clouds Easily with Shade ================================================= @@ -52,7 +52,7 @@ shade is Free Software This talk is Free Software, too =============================== -* Written for presentty (https://pypi.python.org/pypi/presentty) +* Written for presentty (https://pypi.org/project/presentty) * doc/source/multi-cloud-demo.rst * examples in doc/source/examples * Paths subject to change- this is the first presentation in tree! From 48aac40bf6af220a8ef51921cf7b8fe55fc6168b Mon Sep 17 00:00:00 2001 From: Ilya Margolin Date: Mon, 23 Apr 2018 21:53:40 +0200 Subject: [PATCH 2040/3836] Fix openstack-inventory which still uses openstack.cloud.simple_logging which was removed some time ago Change-Id: I60ac37605c9e0ff8ad4d9c4457882ee5de64a5b4 Story: 2001899 --- openstack/cloud/cmd/inventory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/cloud/cmd/inventory.py b/openstack/cloud/cmd/inventory.py index c5bc8cf26..0f1186e5e 100755 --- a/openstack/cloud/cmd/inventory.py +++ b/openstack/cloud/cmd/inventory.py @@ -51,7 +51,7 @@ def parse_args(): def main(): args = parse_args() try: - openstack.cloud.simple_logging(debug=args.debug) + openstack.enable_logging(debug=args.debug) inventory = openstack.cloud.inventory.OpenStackInventory( refresh=args.refresh, private=args.private, cloud=args.cloud) From 9c31aeca60f465ce692cb24354c557a1d3384819 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 25 Apr 2018 11:34:03 +0200 Subject: [PATCH 2041/3836] Fix bugtracker and documentation references These are all out of date. Co-Authored-By: Sergey Skripnick Change-Id: I7f59ec145946b316432357f3d0190b65157bcbfb --- CONTRIBUTING.rst | 2 +- README.rst | 7 +++---- doc/source/conf.py | 4 ++-- doc/source/user/guides/connect_from_config.rst | 2 +- openstack/image/v2/image.py | 2 +- openstack/tests/functional/orchestration/v1/test_stack.py | 2 +- openstack/tests/unit/config/test_config.py | 2 +- 7 files changed, 10 insertions(+), 11 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 4fc8209a6..9298e22db 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -33,7 +33,7 @@ Project Documentation https://docs.openstack.org/openstacksdk/latest/ Bug tracker - https://bugs.launchpad.net/python-openstacksdk + https://storyboard.openstack.org/#!/project/972 Mailing list (prefix subjects with ``[sdk]`` for faster responses) http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-dev diff --git a/README.rst b/README.rst index a420f431f..8effea873 100644 --- a/README.rst +++ b/README.rst @@ -122,7 +122,7 @@ in the following locations: * ``~/.config/openstack`` * ``/etc/openstack`` -More information at https://developer.openstack.org/sdks/python/openstacksdk/users/config +More information at https://docs.openstack.org/openstacksdk/latest/user/config/configuration.html openstack.cloud =============== @@ -155,9 +155,8 @@ Create a server using objects configured with the ``clouds.yaml`` file: Links ===== -* `Issue Tracker `_ +* `Issue Tracker `_ * `Code Review `_ -* `Documentation `_ +* `Documentation `_ * `PyPI `_ * `Mailing list `_ -* `Bugs `_ diff --git a/doc/source/conf.py b/doc/source/conf.py index 561e014e3..905223cfb 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -32,7 +32,7 @@ # openstackdocstheme options repository_name = 'openstack/openstacksdk' -bug_project = '760' +bug_project = '972' bug_tag = '' html_last_updated_fmt = '%Y-%m-%d %H:%M' html_theme = 'openstackdocs' @@ -77,7 +77,7 @@ html_context = {"pwd": pwd, "gitsha": gitsha, "bug_tag": bug_tag, - "bug_project": "python-openstacksdk"} + "bug_project": bug_project} # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True diff --git a/doc/source/user/guides/connect_from_config.rst b/doc/source/user/guides/connect_from_config.rst index 79a7927f7..dfa489ca6 100644 --- a/doc/source/user/guides/connect_from_config.rst +++ b/doc/source/user/guides/connect_from_config.rst @@ -63,7 +63,7 @@ of the cloud configuration to use, . .. Create Connection From Environment Variables -------------------------------------------- - TODO(etoews): Document when https://bugs.launchpad.net/os-client-config/+bug/1489617 + TODO(etoews): Document when https://storyboard.openstack.org/#!/story/1489617 is fixed. Next diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index db8e104de..177ed83ba 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -258,7 +258,7 @@ def download(self, session, stream=False): # See the following bug report for details on why the checksum # code may sometimes depend on a second GET call. - # https://bugs.launchpad.net/python-openstacksdk/+bug/1619675 + # https://storyboard.openstack.org/#!/story/1619675 checksum = resp.headers.get("Content-MD5") if checksum is None: diff --git a/openstack/tests/functional/orchestration/v1/test_stack.py b/openstack/tests/functional/orchestration/v1/test_stack.py index d7673a1c6..c0c12bcfd 100644 --- a/openstack/tests/functional/orchestration/v1/test_stack.py +++ b/openstack/tests/functional/orchestration/v1/test_stack.py @@ -28,7 +28,7 @@ def setUp(self): super(TestStack, self).setUp() self.skipTest( 'Orchestration functional tests disabled:' - ' https://bugs.launchpad.net/python-openstacksdk/+bug/1525005') + ' https://storyboard.openstack.org/#!/story/1525005') self.require_service('orchestration') if self.conn.compute.find_keypair(self.NAME) is None: diff --git a/openstack/tests/unit/config/test_config.py b/openstack/tests/unit/config/test_config.py index cdc569a94..b92071126 100644 --- a/openstack/tests/unit/config/test_config.py +++ b/openstack/tests/unit/config/test_config.py @@ -399,7 +399,7 @@ def test_get_region_no_cloud(self): class TestExcludedFormattedConfigValue(base.TestCase): - # verify LaunchPad bug #1635696 + # verify https://storyboard.openstack.org/#!/story/1635696 # # get_one_cloud() and get_one_cloud_osc() iterate over config # values and try to expand any variables in those values by From a829424b4accf35c35d738320f3b3f3c6d3d09fc Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 26 Apr 2018 07:54:11 -0500 Subject: [PATCH 2042/3836] Don't assume a full config dict Change-Id: I842d38c88be7fe9be0ce986c4e38e71dfe0986db --- openstack/config/cloud_region.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index b9eab3407..40f78bfcc 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -159,21 +159,21 @@ def set_session_constructor(self, session_constructor): def get_requests_verify_args(self): """Return the verify and cert values for the requests library.""" - if self.config['verify'] and self.config['cacert']: - verify = self.config['cacert'] + if self.config.get('verify') and self.config.get('cacert'): + verify = self.config.get('cacert') else: - verify = self.config['verify'] - if self.config['cacert']: + verify = self.config.get('verify') + if self.config.get('cacert'): warnings.warn( "You are specifying a cacert for the cloud {full_name}" " but also to ignore the host verification. The host SSL" " cert will not be verified.".format( full_name=self.full_name)) - cert = self.config.get('cert', None) + cert = self.config.get('cert') if cert: - if self.config['key']: - cert = (cert, self.config['key']) + if self.config.get('key'): + cert = (cert, self.config.get('key')) return (verify, cert) def get_services(self): @@ -278,7 +278,7 @@ def get_session(self): auth=self._auth, verify=verify, cert=cert, - timeout=self.config['api_timeout'], + timeout=self.config.get('api_timeout'), discovery_cache=self._discovery_cache) self.insert_user_agent() # Using old keystoneauth with new os-client-config fails if From 5c5cadfb375a51c7c0fefce59d5464679bde5454 Mon Sep 17 00:00:00 2001 From: Nick Jones Date: Thu, 12 Apr 2018 14:08:50 +0100 Subject: [PATCH 2043/3836] Remove DataCentred from list of vendors DataCentred's cloud service was shut down late 2017, and so this vendor should be removed from the list. Change-Id: Id10cbed6eaa02cafaf5d220032014b2a3f58ad82 Signed-off-by: Nick Jones --- doc/source/user/config/vendor-support.rst | 13 ------------- openstack/config/vendors/datacentred.json | 11 ----------- 2 files changed, 24 deletions(-) delete mode 100644 openstack/config/vendors/datacentred.json diff --git a/doc/source/user/config/vendor-support.rst b/doc/source/user/config/vendor-support.rst index 836b55316..8f2a4fc42 100644 --- a/doc/source/user/config/vendor-support.rst +++ b/doc/source/user/config/vendor-support.rst @@ -106,19 +106,6 @@ sjc1 San Jose, CA * Image upload is not supported -datacentred ------------ - -https://compute.datacentred.io:5000 - -============== ================ -Region Name Location -============== ================ -sal01 Manchester, UK -============== ================ - -* Image API Version is 1 - dreamcompute ------------ diff --git a/openstack/config/vendors/datacentred.json b/openstack/config/vendors/datacentred.json deleted file mode 100644 index e67d3da72..000000000 --- a/openstack/config/vendors/datacentred.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "datacentred", - "profile": { - "auth": { - "auth_url": "https://compute.datacentred.io:5000" - }, - "region-name": "sal01", - "identity_api_version": "3", - "image_api_version": "2" - } -} From 15d0f31775d3a64ee2a57dad948510210626d486 Mon Sep 17 00:00:00 2001 From: Logan V Date: Tue, 27 Mar 2018 22:43:20 -0500 Subject: [PATCH 2044/3836] Add Limestone Networks vendor info Adds the Limestone Networks provider info for the Dallas and Salt Lake regions. Change-Id: Id457ea68938aea955162dc42a112568b2e55b850 --- doc/source/user/config/vendor-support.rst | 16 ++++++++++++++++ .../vendors/vendors/limestonenetworks.json | 15 +++++++++++++++ ...r-add-limestonenetworks-99b2ffab9fc23b08.yaml | 4 ++++ 3 files changed, 35 insertions(+) create mode 100644 openstack/config/vendors/vendors/limestonenetworks.json create mode 100644 releasenotes/notes/vendor-add-limestonenetworks-99b2ffab9fc23b08.yaml diff --git a/doc/source/user/config/vendor-support.rst b/doc/source/user/config/vendor-support.rst index 8f2a4fc42..07168445b 100644 --- a/doc/source/user/config/vendor-support.rst +++ b/doc/source/user/config/vendor-support.rst @@ -213,6 +213,22 @@ sjc01 San Jose, CA * Floating IPs are not supported +limestonenetworks +----------------- + +https://auth.cloud.lstn.net:5000/v3 + +============== ================== +Region Name Location +============== ================== +us-dfw-1 Dallas, TX +us-slc Salt Lake City, UT +============== ================== + +* Identity API Version is 3 +* Images must be in `raw` format +* IPv6 is provided to every server connected to the `Public Internet` network + ovh --- diff --git a/openstack/config/vendors/vendors/limestonenetworks.json b/openstack/config/vendors/vendors/limestonenetworks.json new file mode 100644 index 000000000..659a95b9f --- /dev/null +++ b/openstack/config/vendors/vendors/limestonenetworks.json @@ -0,0 +1,15 @@ +{ + "name": "limestonenetworks", + "profile": { + "auth": { + "auth_url": "https://auth.cloud.lstn.net:5000/v3" + }, + "regions": [ + "us-dfw-1", + "us-slc" + ], + "identity_api_version": "3", + "image_format": "raw", + "volume_api_version": "3" + } + } diff --git a/releasenotes/notes/vendor-add-limestonenetworks-99b2ffab9fc23b08.yaml b/releasenotes/notes/vendor-add-limestonenetworks-99b2ffab9fc23b08.yaml new file mode 100644 index 000000000..d0e8332cf --- /dev/null +++ b/releasenotes/notes/vendor-add-limestonenetworks-99b2ffab9fc23b08.yaml @@ -0,0 +1,4 @@ +--- +other: + - | + Add Limestone Networks vendor info for us-dfw-1 and us-slc regions From fb0d34ba26b3d940b3381a265618785c14927ffd Mon Sep 17 00:00:00 2001 From: Saju Date: Wed, 21 Mar 2018 21:07:04 +0530 Subject: [PATCH 2045/3836] pypy is not checked at gate We also don't use python 3.4. Change-Id: I9aa2f3770610570166fc7004df1e05da9c5e0d4a --- bindep.txt | 3 --- doc/source/contributor/setup.rst | 2 +- doc/source/contributor/testing.rst | 9 ++++----- openstack/tests/unit/cloud/test_task_manager.py | 2 +- tox.ini | 2 +- 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/bindep.txt b/bindep.txt index 7f205ffc8..104a48089 100644 --- a/bindep.txt +++ b/bindep.txt @@ -7,6 +7,3 @@ python-devel [platform:rpm] libffi-dev [platform:dpkg] libffi-devel [platform:rpm] openssl-devel [platform:rpm] -pypy [pypy] -pypy-dev [platform:dpkg pypy] -pypy-devel [platform:rpm pypy] diff --git a/doc/source/contributor/setup.rst b/doc/source/contributor/setup.rst index 47e227420..7aee35416 100644 --- a/doc/source/contributor/setup.rst +++ b/doc/source/contributor/setup.rst @@ -42,7 +42,7 @@ the following:: To create an environment for a different version, such as Python 3, run the following:: - $ virtualenv -p python3.4 $HOME/envs/sdk3 + $ virtualenv -p python3.5 $HOME/envs/sdk3 When you want to enable your environment so that you can develop inside of it, you *activate* it. To activate an environment, run the /bin/activate diff --git a/doc/source/contributor/testing.rst b/doc/source/contributor/testing.rst index 30e8d78a2..3d2d02713 100644 --- a/doc/source/contributor/testing.rst +++ b/doc/source/contributor/testing.rst @@ -14,16 +14,15 @@ Run In order to run the entire unit test suite, simply run the ``tox`` command inside of your source checkout. This will attempt to run every test command -listed inside of ``tox.ini``, which includes Python 2.7, 3.4, PyPy, +listed inside of ``tox.ini``, which includes Python 2.7, 3.5, and a PEP 8 check. You should run the full test suite on all versions before submitting changes for review in order to avoid unexpected failures in the continuous integration system.:: (sdk3)$ tox ... - py34: commands succeeded + py35: commands succeeded py27: commands succeeded - pypy: commands succeeded pep8: commands succeeded congratulations :) @@ -31,8 +30,8 @@ During development, it may be more convenient to run a subset of the tests to keep test time to a minimum. You can choose to run the tests only on one version. A step further is to run only the tests you are working on.:: - (sdk3)$ tox -e py34 # Run run the tests on Python 3.4 - (sdk3)$ tox -e py34 TestContainer # Run only the TestContainer tests on 3.4 + (sdk3)$ tox -e py35 # Run run the tests on Python 3.5 + (sdk3)$ tox -e py35 TestContainer # Run only the TestContainer tests on 3.5 Functional Tests ---------------- diff --git a/openstack/tests/unit/cloud/test_task_manager.py b/openstack/tests/unit/cloud/test_task_manager.py index 64e521487..a4c7d2694 100644 --- a/openstack/tests/unit/cloud/test_task_manager.py +++ b/openstack/tests/unit/cloud/test_task_manager.py @@ -78,7 +78,7 @@ def test_wait_re_raise(self): This test is aimed to six.reraise(), called in Task::wait(). Specifically, we test if we get the same behaviour with all the - configured interpreters (e.g. py27, p34, pypy, ...) + configured interpreters (e.g. py27, p35, ...) """ self.assertRaises(TestException, self.manager.submit_task, TaskTest()) diff --git a/tox.ini b/tox.ini index 7378c3a78..71da5cacf 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 1.6 -envlist = py35,py27,pypy,pep8 +envlist = py35,py27,pep8 skipsdist = True [testenv] From b17faa7b73c3630ae2dc14421b6339a9b2ff6c41 Mon Sep 17 00:00:00 2001 From: Daniel Speichert Date: Fri, 27 Apr 2018 11:33:47 -0400 Subject: [PATCH 2046/3836] Flavor: added is_public query parameter and description property Change-Id: I882eb9a6fde2e687f687a55aa450ea4b34fa5ab3 --- openstack/compute/v2/flavor.py | 4 +++- openstack/tests/unit/compute/v2/test_flavor.py | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/openstack/compute/v2/flavor.py b/openstack/compute/v2/flavor.py index 20ed33a53..abdc08df6 100644 --- a/openstack/compute/v2/flavor.py +++ b/openstack/compute/v2/flavor.py @@ -27,7 +27,7 @@ class Flavor(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - "sort_key", "sort_dir", + "sort_key", "sort_dir", "is_public", min_disk="minDisk", min_ram="minRam") @@ -37,6 +37,8 @@ class Flavor(resource.Resource): links = resource.Body('links') #: The name of this flavor. name = resource.Body('name') + #: The description of the flavor. + description = resource.Body('description') #: Size of the disk this flavor offers. *Type: int* disk = resource.Body('disk', type=int) #: ``True`` if this is a publicly visible flavor. ``False`` if this is diff --git a/openstack/tests/unit/compute/v2/test_flavor.py b/openstack/tests/unit/compute/v2/test_flavor.py index d4c154724..745fddc13 100644 --- a/openstack/tests/unit/compute/v2/test_flavor.py +++ b/openstack/tests/unit/compute/v2/test_flavor.py @@ -19,6 +19,7 @@ 'id': IDENTIFIER, 'links': '2', 'name': '3', + 'description': 'Testing flavor', 'disk': 4, 'os-flavor-access:is_public': True, 'ram': 6, @@ -49,7 +50,8 @@ def test_basic(self): "min_disk": "minDisk", "min_ram": "minRam", "limit": "limit", - "marker": "marker"}, + "marker": "marker", + "is_public": "is_public"}, sot._query_mapping._mapping) def test_make_basic(self): @@ -57,6 +59,7 @@ def test_make_basic(self): self.assertEqual(BASIC_EXAMPLE['id'], sot.id) self.assertEqual(BASIC_EXAMPLE['links'], sot.links) self.assertEqual(BASIC_EXAMPLE['name'], sot.name) + self.assertEqual(BASIC_EXAMPLE['description'], sot.description) self.assertEqual(BASIC_EXAMPLE['disk'], sot.disk) self.assertEqual(BASIC_EXAMPLE['os-flavor-access:is_public'], sot.is_public) From 6ed083e8084d9e443e3ca8c778761411f8f4e7b5 Mon Sep 17 00:00:00 2001 From: Jens Harbott Date: Mon, 30 Apr 2018 12:57:58 +0000 Subject: [PATCH 2047/3836] Drop bogus attributes from network port resource In [0] some attributes were added to the port resource that do not exist in the API at the top level. They only appear as sub-attributes in objects returned for other attributes. Remove them again as they lead to useless output when doing e.g. "openstack port show". [0] I728ff349811e344396176d1c577cef653350bfdb Change-Id: I4bc81d13519ff4014b3a5b1ccff2771c533d2a45 --- openstack/network/v2/port.py | 11 ----------- openstack/tests/unit/network/v2/test_port.py | 8 -------- 2 files changed, 19 deletions(-) diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index 934ab2d93..335e885eb 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -85,8 +85,6 @@ class Port(resource.Resource, tag.TagMixin): dns_name = resource.Body('dns_name') #: Extra DHCP options. extra_dhcp_opts = resource.Body('extra_dhcp_opts', type=list) - #: IP addresses of an allowed address pair. - ip_address = resource.Body('ip_address') #: IP addresses for the port. Includes the IP address and subnet ID. fixed_ips = resource.Body('fixed_ips', type=list) #: The administrative state of the port, which is up ``True`` or @@ -105,10 +103,6 @@ class Port(resource.Resource, tag.TagMixin): #: The ID of the project who owns the network. Only administrative #: users can specify a project ID other than their own. project_id = resource.Body('tenant_id') - #: The extra DHCP option name. - option_name = resource.Body('opt_name') - #: The extra DHCP option value. - option_value = resource.Body('opt_value') #: The ID of the QoS policy attached to the port. qos_policy_id = resource.Body('qos_policy_id') #: Revision number of the port. *Type: int* @@ -118,11 +112,6 @@ class Port(resource.Resource, tag.TagMixin): security_group_ids = resource.Body('security_groups', type=list) #: The port status. Value is ``ACTIVE`` or ``DOWN``. status = resource.Body('status') - #: The ID of the subnet. If you specify only a subnet UUID, OpenStack - #: networking allocates an available IP from that subnet to the port. - #: If you specify both a subnet ID and an IP address, OpenStack networking - #: tries to allocate the address to the port. - subnet_id = resource.Body('subnet_id') #: Read-only. The trunk referring to this parent port and its subports. #: Present for trunk parent ports if ``trunk-details`` extension is loaded. #: *Type: dict with keys: trunk_id, sub_ports. diff --git a/openstack/tests/unit/network/v2/test_port.py b/openstack/tests/unit/network/v2/test_port.py index 2835e8cc4..23f3475e7 100644 --- a/openstack/tests/unit/network/v2/test_port.py +++ b/openstack/tests/unit/network/v2/test_port.py @@ -34,17 +34,13 @@ 'extra_dhcp_opts': [{'13': 13}], 'fixed_ips': [{'14': '14'}], 'id': IDENTIFIER, - 'ip_address': '15', 'mac_address': '16', 'name': '17', 'network_id': '18', - 'opt_name': '19', - 'opt_value': '20', 'port_security_enabled': True, 'qos_policy_id': '21', 'revision_number': 22, 'security_groups': ['23'], - 'subnet_id': '24', 'status': '25', 'tenant_id': '26', 'trunk_details': { @@ -116,18 +112,14 @@ def test_make_it(self): self.assertEqual(EXAMPLE['extra_dhcp_opts'], sot.extra_dhcp_opts) self.assertEqual(EXAMPLE['fixed_ips'], sot.fixed_ips) self.assertEqual(EXAMPLE['id'], sot.id) - self.assertEqual(EXAMPLE['ip_address'], sot.ip_address) self.assertEqual(EXAMPLE['mac_address'], sot.mac_address) self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['network_id'], sot.network_id) - self.assertEqual(EXAMPLE['opt_name'], sot.option_name) - self.assertEqual(EXAMPLE['opt_value'], sot.option_value) self.assertTrue(sot.is_port_security_enabled) self.assertEqual(EXAMPLE['qos_policy_id'], sot.qos_policy_id) self.assertEqual(EXAMPLE['revision_number'], sot.revision_number) self.assertEqual(EXAMPLE['security_groups'], sot.security_group_ids) self.assertEqual(EXAMPLE['status'], sot.status) - self.assertEqual(EXAMPLE['subnet_id'], sot.subnet_id) self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) self.assertEqual(EXAMPLE['trunk_details'], sot.trunk_details) self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) From b9be78d5508ad28a00076c2c7490f5545348f516 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 1 May 2018 07:21:52 -0500 Subject: [PATCH 2048/3836] Honor endpoint_override for get_session_client get_session_client is not passing endpoint_override settings to the Adapter constructor causing the setting to be ignored. Pass the setting in as a parameter. Change-Id: I1e57579291a3943fca090100b43905f71b2daab4 --- openstack/config/cloud_region.py | 1 + .../notes/fix-endpoint-override-ac41baeec9549ab3.yaml | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 releasenotes/notes/fix-endpoint-override-ac41baeec9549ab3.yaml diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index b9eab3407..341b000fe 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -350,6 +350,7 @@ def get_session_client( version=version_request.version, min_version=version_request.min_api_version, max_version=version_request.max_api_version, + endpoint_override=self.get_endpoint(service_key), **kwargs) def _get_highest_endpoint(self, service_types, kwargs): diff --git a/releasenotes/notes/fix-endpoint-override-ac41baeec9549ab3.yaml b/releasenotes/notes/fix-endpoint-override-ac41baeec9549ab3.yaml new file mode 100644 index 000000000..0496f24cb --- /dev/null +++ b/releasenotes/notes/fix-endpoint-override-ac41baeec9549ab3.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixed issue where ``endpoint_override`` settings were not getting passed + to the Adapter constructor in ``get_session_client``. From 9246894b3ac0fce05ca803c2a01944e417a5152a Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 4 May 2018 15:56:45 +0200 Subject: [PATCH 2049/3836] close files after open in unit/base keystone fixture files are not properly closed. When unittests are invoked individually there is a permanent warning. So do read files under `with open(` Change-Id: Ie4dc4f4ab5c1d9cea9fa09aba0818c22a5554d9d --- openstack/tests/unit/base.py | 45 +++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index e9836f643..85b69998d 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -402,17 +402,20 @@ def use_keystone_v3(self, catalog='catalog-v3.json'): self.adapter = self.useFixture(rm_fixture.Fixture()) self.calls = [] self._uri_registry.clear() - self.__do_register_uris([ - dict(method='GET', uri='https://identity.example.com/', - text=open(self.discovery_json, 'r').read()), - dict(method='POST', - uri='https://identity.example.com/v3/auth/tokens', - headers={ - 'X-Subject-Token': self.getUniqueString('KeystoneToken')}, - text=open(os.path.join( - self.fixtures_directory, catalog), 'r').read() - ), - ]) + with open(self.discovery_json, 'r') as discovery_file, \ + open(os.path.join( + self.fixtures_directory, catalog), 'r') as tokens_file: + self.__do_register_uris([ + dict(method='GET', uri='https://identity.example.com/', + text=discovery_file.read()), + dict(method='POST', + uri='https://identity.example.com/v3/auth/tokens', + headers={ + 'X-Subject-Token': + self.getUniqueString('KeystoneToken')}, + text=tokens_file.read() + ), + ]) self._make_test_cloud(identity_api_version='3') def use_keystone_v2(self): @@ -420,14 +423,18 @@ def use_keystone_v2(self): self.calls = [] self._uri_registry.clear() - self.__do_register_uris([ - dict(method='GET', uri='https://identity.example.com/', - text=open(self.discovery_json, 'r').read()), - dict(method='POST', uri='https://identity.example.com/v2.0/tokens', - text=open(os.path.join( - self.fixtures_directory, 'catalog-v2.json'), 'r').read() - ), - ]) + with open(self.discovery_json, 'r') as discovery_file, \ + open(os.path.join( + self.fixtures_directory, + 'catalog-v2.json'), 'r') as tokens_file: + self.__do_register_uris([ + dict(method='GET', uri='https://identity.example.com/', + text=discovery_file.read()), + dict(method='POST', + uri='https://identity.example.com/v2.0/tokens', + text=tokens_file.read() + ), + ]) self._make_test_cloud(cloud_name='_test_cloud_v2_', identity_api_version='2.0') From 6dc1e171d8789d87541a6d435783cd9bd4bbaee5 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Sat, 5 May 2018 13:58:22 +0200 Subject: [PATCH 2050/3836] add missing attribute in LBaaS v2 Pool API LBaaS V2 API reference is wrong (see http://lists.openstack.org/pipermail/openstack-dev/2018-April/129965.html). Some clouds are still using this API and not Octavia. Instead of polishing anyway deprecated API (and leave potential LBaaSv1 compatibility) just introduce missing attribute from LBaaS v2 API. Change-Id: I1686fcf52ea80cc31de31a6d5a97f64226430c58 --- openstack/network/v2/pool.py | 2 ++ openstack/tests/unit/network/v2/test_pool.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/openstack/network/v2/pool.py b/openstack/network/v2/pool.py index 9cec41364..cecba0072 100644 --- a/openstack/network/v2/pool.py +++ b/openstack/network/v2/pool.py @@ -39,6 +39,8 @@ class Pool(resource.Resource): #: Description for the pool. description = resource.Body('description') #: The ID of the associated health monitors. + health_monitor_id = resource.Body('healthmonitor_id') + #: The ID of the associated health monitors (LBaaS v1). health_monitor_ids = resource.Body('health_monitors', type=list) #: The statuses of the associated health monitors. health_monitor_status = resource.Body('health_monitor_status', type=list) diff --git a/openstack/tests/unit/network/v2/test_pool.py b/openstack/tests/unit/network/v2/test_pool.py index 3357c1a23..68e949a9b 100644 --- a/openstack/tests/unit/network/v2/test_pool.py +++ b/openstack/tests/unit/network/v2/test_pool.py @@ -18,6 +18,7 @@ EXAMPLE = { 'admin_state_up': True, 'description': '2', + 'healthmonitor_id': '3-1', 'health_monitors': ['3'], 'health_monitor_status': ['4'], 'id': IDENTIFIER, @@ -57,6 +58,7 @@ def test_make_it(self): sot = pool.Pool(**EXAMPLE) self.assertTrue(sot.is_admin_state_up) self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['healthmonitor_id'], sot.health_monitor_id) self.assertEqual(EXAMPLE['health_monitors'], sot.health_monitor_ids) self.assertEqual(EXAMPLE['health_monitor_status'], sot.health_monitor_status) From 9d56efeb391ebf26889b32d1b450cb59b7b01308 Mon Sep 17 00:00:00 2001 From: "Yuanbin.Chen" Date: Mon, 7 May 2018 14:29:25 +0800 Subject: [PATCH 2051/3836] Change clustering example test create parameter This patch fix profile and policy create parameter error, fix cluster test return to_dict error. Change-Id: I3ba61df97588a35d11149ed5c2edcc69023b2f53 Signed-off-by: Yuanbin.Chen --- examples/clustering/cluster.py | 20 ++++++++++---------- examples/clustering/node.py | 4 ++-- examples/clustering/policy.py | 18 ++++++++++-------- examples/clustering/profile.py | 3 ++- 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/examples/clustering/cluster.py b/examples/clustering/cluster.py index a4d46ce08..7710f5f7b 100644 --- a/examples/clustering/cluster.py +++ b/examples/clustering/cluster.py @@ -91,7 +91,7 @@ def cluster_add_nodes(conn): node_ids = [NODE_ID] res = conn.clustering.cluster_add_nodes(CLUSTER_ID, node_ids) - print(res.to_dict()) + print(res) def cluster_del_nodes(conn): @@ -99,7 +99,7 @@ def cluster_del_nodes(conn): node_ids = [NODE_ID] res = conn.clustering.cluster_del_nodes(CLUSTER_ID, node_ids) - print(res.to_dict()) + print(res) def cluster_replace_nodes(conn): @@ -111,21 +111,21 @@ def cluster_replace_nodes(conn): old_node: new_node } res = conn.clustering.cluster_replace_nodes(CLUSTER_ID, **spec) - print(res.to_dict()) + print(res) def cluster_scale_out(conn): print("Inflate the size of a cluster:") res = conn.clustering.cluster_scale_out(CLUSTER_ID, 1) - print(res.to_dict()) + print(res) def cluster_scale_in(conn): print("Shrink the size of a cluster:") res = conn.clustering.cluster_scale_in(CLUSTER_ID, 1) - print(res.to_dict()) + print(res) def cluster_resize(conn): @@ -138,7 +138,7 @@ def cluster_resize(conn): 'number': 2 } res = conn.clustering.cluster_resize(CLUSTER_ID, **spec) - print(res.to_dict()) + print(res) def cluster_attach_policy(conn): @@ -147,21 +147,21 @@ def cluster_attach_policy(conn): spec = {'enabled': True} res = conn.clustering.cluster_attach_policy(CLUSTER_ID, POLICY_ID, **spec) - print(res.to_dict()) + print(res) def cluster_detach_policy(conn): print("Detach a policy from a cluster:") res = conn.clustering.cluster_detach_policy(CLUSTER_ID, POLICY_ID) - print(res.to_dict()) + print(res) def check_cluster(conn): print("Check cluster:") res = conn.clustering.check_cluster(CLUSTER_ID) - print(res.to_dict()) + print(res) def recover_cluster(conn): @@ -169,4 +169,4 @@ def recover_cluster(conn): spec = {'check': True} res = conn.clustering.recover_cluster(CLUSTER_ID, **spec) - print(res.to_dict()) + print(res) diff --git a/examples/clustering/node.py b/examples/clustering/node.py index 40910b452..2ce8a0d99 100644 --- a/examples/clustering/node.py +++ b/examples/clustering/node.py @@ -82,7 +82,7 @@ def check_node(conn): print("Check Node:") node = conn.clustering.check_node(NODE_ID) - print(node.to_dict()) + print(node) def recover_node(conn): @@ -90,4 +90,4 @@ def recover_node(conn): spec = {'check': True} node = conn.clustering.recover_node(NODE_ID, **spec) - print(node.to_dict()) + print(node) diff --git a/examples/clustering/policy.py b/examples/clustering/policy.py index dca4aca5d..2f4c3361b 100644 --- a/examples/clustering/policy.py +++ b/examples/clustering/policy.py @@ -30,17 +30,19 @@ def list_policies(conn): def create_policy(conn): print("Create Policy:") - - spec = { - 'policy': 'senlin.policy.deletion', - 'version': 1.0, - 'properties': { - 'criteria': 'oldest_first', - 'destroy_after_deletion': True, + attrs = { + 'name': 'dp01', + 'spec': { + 'policy': 'senlin.policy.deletion', + 'version': 1.0, + 'properties': { + 'criteria': 'oldest_first', + 'destroy_after_deletion': True, + } } } - policy = conn.clustering.create_policy('dp01', spec) + policy = conn.clustering.create_policy(attrs) print(policy.to_dict()) diff --git a/examples/clustering/profile.py b/examples/clustering/profile.py index 1c3ae6bcc..ffaf3a6d2 100644 --- a/examples/clustering/profile.py +++ b/examples/clustering/profile.py @@ -39,6 +39,7 @@ def create_profile(conn): spec = { 'profile': 'os.nova.server', 'version': 1.0, + 'name': 'os_server', 'properties': { 'name': SERVER_NAME, 'flavor': FLAVOR_NAME, @@ -49,7 +50,7 @@ def create_profile(conn): } } - profile = conn.clustering.create_profile('os_server', spec) + profile = conn.clustering.create_profile(spec) print(profile.to_dict()) From ebdb9b3e905939d33951bf84159f2c73fc3389e7 Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Mon, 7 May 2018 12:01:36 -0500 Subject: [PATCH 2052/3836] Fixing bug where original and new dicts would always be the same * Calling Resource.to_dict() does not make a deep copy of the dict. Because of this, modifications to values in the 'new' dict would also modify the original dict, thus defeating the purpose of the jsonpatch comparison. Change-Id: I1b72775b729e040d1b44658d50dcd93768f07a6e --- openstack/image/v2/image.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index 177ed83ba..228f24b6f 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -12,6 +12,7 @@ import hashlib +import copy import jsonpatch from openstack import _log @@ -296,7 +297,7 @@ def update(self, session, **attrs): original = self.to_dict() # Update values from **attrs so they can be passed to jsonpatch - new = self.to_dict() + new = copy.deepcopy(self.to_dict()) new.update(**attrs) patch_string = jsonpatch.make_patch(original, new).to_string() From 984b9974fb5ffcd4120bfa029628690eda859848 Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Mon, 7 May 2018 16:11:33 -0500 Subject: [PATCH 2053/3836] Bugfix for block_storage not selecting the correct proxy When setting the block_storage API from config "volume_api_version" was not being honored and instead the default version was being used. This fix addresses that issue until a different service discovery algorithm arrives. Change-Id: I374d0d1a1ea2db8e4ecb9423ec7f94ba219c73b5 --- openstack/service_description.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openstack/service_description.py b/openstack/service_description.py index 245d7145f..102d7fae6 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -122,7 +122,10 @@ def __init__(self, service_filter_class, *args, **kwargs): def get_proxy_class(self, config): # TODO(mordred) Replace this with proper discovery - version_string = config.get_api_version(self.service_type) + if self.service_type == 'block-storage': + version_string = config.get_api_version('volume') + else: + version_string = config.get_api_version(self.service_type) version = None if version_string: version = 'v{version}'.format(version=version_string[0]) From d908f1dd7a0d0ff9500d43fa2204ed83bcdb0b3b Mon Sep 17 00:00:00 2001 From: Mohammed Naser Date: Wed, 9 May 2018 10:09:06 -0400 Subject: [PATCH 2054/3836] Avoid raising exception when comparing resource to None If we compare a resource to None, it will raise an exception due to the fact that 'comparand' has no attributes because it's value is None. This patch resolves it by assuming that if we're comparing a resource to None, it'll always be not equal. Change-Id: Iabe2129b80641ed933a94f34f2fd4337e9c45b8d --- openstack/resource.py | 2 ++ openstack/tests/unit/test_resource.py | 1 + 2 files changed, 3 insertions(+) diff --git a/openstack/resource.py b/openstack/resource.py index 9818bb42f..2801c33df 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -355,6 +355,8 @@ def __repr__(self): def __eq__(self, comparand): """Return True if another resource has the same contents""" + if not isinstance(comparand, Resource): + return False return all([self._body.attributes == comparand._body.attributes, self._header.attributes == comparand._header.attributes, self._uri.attributes == comparand._uri.attributes]) diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index a5ad7052a..090677c52 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -468,6 +468,7 @@ class Example(resource.Resource): self.assertEqual(e1, e2) self.assertNotEqual(e1, e3) + self.assertNotEqual(e1, None) def test__update(self): sot = resource.Resource() From d5fd3e8f1b783b31599d4f3b9b1aa6157f7af2e1 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 8 May 2018 12:26:44 -0500 Subject: [PATCH 2055/3836] Clean up floating ip tests There are timeout issues as well as sometimes something goes wrong in the shade floating ip tests. Update the tests to try to make them more stable. This also fixes a bug in find_available_ip that was causing ips with port_ids to be returned that was accidentally working in the functional tests as a race condition. While working on this, pycodestyle made a release and broke us. Add pep8 changes to allow landing this patch. Fix two of them, ignore two of them. Change-Id: I128e85048b91f0508798d6c0c2a7e3aacb1c92c1 --- openstack/__init__.py | 8 ++--- openstack/cloud/_heat/event_utils.py | 12 +++++-- openstack/cloud/inventory.py | 4 +-- openstack/connection.py | 10 +++--- openstack/network/v2/floating_ip.py | 11 ++++--- openstack/service_description.py | 10 +++--- .../functional/cloud/test_floating_ip.py | 31 +++++++++++++------ .../functional/network/v2/test_floating_ip.py | 1 + .../tests/unit/network/v2/test_floating_ip.py | 2 +- tox.ini | 2 +- 10 files changed, 56 insertions(+), 35 deletions(-) diff --git a/openstack/__init__.py b/openstack/__init__.py index e7955388d..0bf4d0f3a 100644 --- a/openstack/__init__.py +++ b/openstack/__init__.py @@ -12,15 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. +from openstack._log import enable_logging # noqa +import openstack.config +import openstack.connection + __all__ = [ 'connect', 'enable_logging', ] -from openstack._log import enable_logging # noqa -import openstack.config -import openstack.connection - def connect( cloud=None, diff --git a/openstack/cloud/_heat/event_utils.py b/openstack/cloud/_heat/event_utils.py index aa9574232..b24a929a3 100644 --- a/openstack/cloud/_heat/event_utils.py +++ b/openstack/cloud/_heat/event_utils.py @@ -44,11 +44,17 @@ def poll_for_events( cloud, stack_name, action=None, poll_period=5, marker=None): """Continuously poll events and logs for performed action on stack.""" - if action: + def stop_check_action(a): stop_status = ('%s_FAILED' % action, '%s_COMPLETE' % action) - stop_check = lambda a: a in stop_status + return a in stop_status + + def stop_check_no_action(a): + return a.endswith('_COMPLETE') or a.endswith('_FAILED') + + if action: + stop_check = stop_check_action else: - stop_check = lambda a: a.endswith('_COMPLETE') or a.endswith('_FAILED') + stop_check = stop_check_no_action no_event_polls = 0 msg_template = "\n Stack %(name)s %(status)s \n" diff --git a/openstack/cloud/inventory.py b/openstack/cloud/inventory.py index f1f549ba7..d62bef433 100644 --- a/openstack/cloud/inventory.py +++ b/openstack/cloud/inventory.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -__all__ = ['OpenStackInventory'] - import functools from openstack.config import loader @@ -21,6 +19,8 @@ from openstack import exceptions from openstack.cloud import _utils +__all__ = ['OpenStackInventory'] + class OpenStackInventory(object): diff --git a/openstack/connection.py b/openstack/connection.py index 2deb1963d..ffbebfafc 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -155,11 +155,6 @@ Additional information about the services can be found in the :ref:`service-proxies` documentation. """ -__all__ = [ - 'from_config', - 'Connection', -] - import warnings import keystoneauth1.exceptions @@ -175,6 +170,11 @@ from openstack import service_description from openstack import task_manager +__all__ = [ + 'from_config', + 'Connection', +] + if requestsexceptions.SubjectAltNameWarning: warnings.filterwarnings( 'ignore', category=requestsexceptions.SubjectAltNameWarning) diff --git a/openstack/network/v2/floating_ip.py b/openstack/network/v2/floating_ip.py index 5e06cf689..dd779b8b8 100644 --- a/openstack/network/v2/floating_ip.py +++ b/openstack/network/v2/floating_ip.py @@ -79,8 +79,9 @@ class FloatingIP(resource.Resource, tag.TagMixin): @classmethod def find_available(cls, session): - info = cls.list(session, port_id='') - try: - return next(info) - except StopIteration: - return None + # server-side filtering on empty values is not always supported. + # TODO(mordred) Make this check for support for the server-side filter + for ip in cls.list(session): + if not ip.port_id: + return ip + return None diff --git a/openstack/service_description.py b/openstack/service_description.py index 245d7145f..afe295756 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -11,11 +11,6 @@ # License for the specific language governing permissions and limitations # under the License. -__all__ = [ - 'OpenStackServiceDescription', - 'ServiceDescription', -] - import importlib import os_service_types @@ -23,6 +18,11 @@ from openstack import _log from openstack import proxy +__all__ = [ + 'OpenStackServiceDescription', + 'ServiceDescription', +] + _logger = _log.setup_logging('openstack') _service_type_manager = os_service_types.ServiceTypes() diff --git a/openstack/tests/functional/cloud/test_floating_ip.py b/openstack/tests/functional/cloud/test_floating_ip.py index 04a7b418f..f25b38a25 100644 --- a/openstack/tests/functional/cloud/test_floating_ip.py +++ b/openstack/tests/functional/cloud/test_floating_ip.py @@ -20,6 +20,8 @@ """ import pprint +import six +import sys from testtools import content @@ -50,6 +52,7 @@ def setUp(self): def _cleanup_network(self): exception_list = list() + tb_list = list() # Delete stale networks as well as networks created for this test if self.user_cloud.has_service('network'): @@ -58,7 +61,7 @@ def _cleanup_network(self): try: if r['name'].startswith(self.new_item_name): self.user_cloud.update_router( - r['id'], ext_gateway_net_id=None) + r, ext_gateway_net_id=None) for s in self.user_cloud.list_subnets(): if s['name'].startswith(self.new_item_name): try: @@ -66,31 +69,41 @@ def _cleanup_network(self): r, subnet_id=s['id']) except Exception: pass - self.user_cloud.delete_router(name_or_id=r['id']) + self.user_cloud.delete_router(r) except Exception as e: - exception_list.append(str(e)) + exception_list.append(e) + tb_list.append(sys.exc_info()[2]) continue # Delete subnets for s in self.user_cloud.list_subnets(): if s['name'].startswith(self.new_item_name): try: - self.user_cloud.delete_subnet(name_or_id=s['id']) + self.user_cloud.delete_subnet(s) except Exception as e: - exception_list.append(str(e)) + exception_list.append(e) + tb_list.append(sys.exc_info()[2]) continue # Delete networks for n in self.user_cloud.list_networks(): if n['name'].startswith(self.new_item_name): try: - self.user_cloud.delete_network(name_or_id=n['id']) + self.user_cloud.delete_network(n) except Exception as e: - exception_list.append(str(e)) + exception_list.append(e) + tb_list.append(sys.exc_info()[2]) continue if exception_list: # Raise an error: we must make users aware that something went # wrong - raise OpenStackCloudException('\n'.join(exception_list)) + if len(exception_list) > 1: + self.addDetail( + 'exceptions', + content.text_content( + '\n'.join([str(ex) for ex in exception_list]))) + exc = exception_list[0] + tb = tb_list[0] + six.reraise(type(exc), exc, tb) def _cleanup_servers(self): exception_list = list() @@ -119,7 +132,7 @@ def _cleanup_ips(self, server): if (ip.get('fixed_ip', None) == fixed_ip or ip.get('fixed_ip_address', None) == fixed_ip): try: - self.user_cloud.delete_floating_ip(ip['id']) + self.user_cloud.delete_floating_ip(ip) except Exception as e: exception_list.append(str(e)) continue diff --git a/openstack/tests/functional/network/v2/test_floating_ip.py b/openstack/tests/functional/network/v2/test_floating_ip.py index 34799c566..29013c224 100644 --- a/openstack/tests/functional/network/v2/test_floating_ip.py +++ b/openstack/tests/functional/network/v2/test_floating_ip.py @@ -36,6 +36,7 @@ class TestFloatingIP(base.BaseFunctionalTest): def setUp(self): super(TestFloatingIP, self).setUp() + self.TIMEOUT_SCALING_FACTOR = 1.5 self.ROT_NAME = self.getUniqueString() self.EXT_NET_NAME = self.getUniqueString() self.EXT_SUB_NAME = self.getUniqueString() diff --git a/openstack/tests/unit/network/v2/test_floating_ip.py b/openstack/tests/unit/network/v2/test_floating_ip.py index 34ca8d861..2c80684d5 100644 --- a/openstack/tests/unit/network/v2/test_floating_ip.py +++ b/openstack/tests/unit/network/v2/test_floating_ip.py @@ -89,7 +89,7 @@ def test_find_available(self): mock_session.get.assert_called_with( floating_ip.FloatingIP.base_path, headers={'Accept': 'application/json'}, - params={'port_id': ''}) + params={}) def test_find_available_nada(self): mock_session = mock.Mock(spec=adapter.Adapter) diff --git a/tox.ini b/tox.ini index 71da5cacf..b74e095d4 100644 --- a/tox.ini +++ b/tox.ini @@ -95,7 +95,7 @@ commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasen # H306 Is about alphabetical imports - there's a lot to fix. # H4 Are about docstrings and there's just a huge pile of pre-existing issues. # D* Came from sdk, unknown why they're skipped. -ignore = H103,H306,H4,D100,D101,D102,D103,D104,D105,D200,D202,D204,D205,D211,D301,D400,D401 +ignore = H103,H306,H4,D100,D101,D102,D103,D104,D105,D200,D202,D204,D205,D211,D301,D400,D401,F405,W503 show-source = True exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build From 1ac3a3b1f88adac686f44de94cefb714ca2c3b9f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 9 May 2018 08:58:42 -0500 Subject: [PATCH 2056/3836] Fix F405 errors F405 is a bit more aggressive about not being able to tell if a symbol is valid due to * imports. Just go ahead and fix it since * imports are, in fact, hard to deal with. As a result of fixing this, there are two actual bugs that were hidden that the check found, so the fix seems worthwhile. Change-Id: If76b0943e16e0c7264b75829838c4a715342ab9d --- openstack/cloud/__init__.py | 7 +- openstack/cloud/openstackcloud.py | 542 +++++++++++++++--------------- tox.ini | 2 +- 3 files changed, 277 insertions(+), 274 deletions(-) diff --git a/openstack/cloud/__init__.py b/openstack/cloud/__init__.py index 4b9999279..9aefbf6c5 100644 --- a/openstack/cloud/__init__.py +++ b/openstack/cloud/__init__.py @@ -16,6 +16,7 @@ from openstack._log import enable_logging # noqa from openstack.cloud.exc import * # noqa +from openstack.cloud import exc from openstack.cloud.openstackcloud import OpenStackCloud @@ -35,7 +36,7 @@ def openstack_clouds( if cloud is None: return [ OpenStackCloud( - cloud=f.name, debug=debug, + cloud=cloud_region.name, debug=debug, cloud_config=cloud_region, strict=strict) for cloud_region in config.get_all() @@ -43,12 +44,12 @@ def openstack_clouds( else: return [ OpenStackCloud( - cloud=f.name, debug=debug, + cloud=cloud_region.name, debug=debug, cloud_config=cloud_region, strict=strict) for cloud_region in config.get_all() if cloud_region.name == cloud ] except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin as e: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Invalid cloud configuration: {exc}".format(exc=str(e))) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index e37c6386c..fdda15152 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -41,7 +41,7 @@ from openstack import _adapter from openstack import _log -from openstack.cloud.exc import * # noqa +from openstack.cloud import exc from openstack.cloud._heat import event_utils from openstack.cloud._heat import template_utils from openstack.cloud import _normalize @@ -422,14 +422,14 @@ def _get_versioned_client( # openstack.cloud. if config_version: if min_major and config_major < min_major: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Version {config_version} requested for {service_type}" " but shade understands a minimum of {min_version}".format( config_version=config_version, service_type=service_type, min_version=min_version)) elif max_major and config_major > max_major: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Version {config_version} requested for {service_type}" " but openstack.cloud understands a maximum of" " {max_version}".format( @@ -734,7 +734,7 @@ def _get_domain_id_param_dict(self, domain_id): # mention api versions if self._is_client_version('identity', 3): if not domain_id: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "User or project creation requires an explicit" " domain_id argument.") else: @@ -847,7 +847,7 @@ def list_projects(self, domain_id=None, name_or_id=None, filters=None): self._get_and_munchify(key, data)) except Exception as e: self.log.debug("Failed to list projects", exc_info=True) - raise OpenStackCloudException(str(e)) + raise exc.OpenStackCloudException(str(e)) return _utils._filter_list(projects, name_or_id, filters) def search_projects(self, name_or_id=None, filters=None, domain_id=None): @@ -884,7 +884,7 @@ def update_project(self, name_or_id, enabled=None, domain_id=None, project=name_or_id)): proj = self.get_project(name_or_id, domain_id=domain_id) if not proj: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Project %s not found." % name_or_id) if enabled is not None: kwargs.update({'enabled': enabled}) @@ -1114,12 +1114,12 @@ def delete_user(self, name_or_id, **kwargs): def _get_user_and_group(self, user_name_or_id, group_name_or_id): user = self.get_user(user_name_or_id) if not user: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'User {user} not found'.format(user=user_name_or_id)) group = self.get_group(group_name_or_id) if not group: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'Group {user} not found'.format(user=group_name_or_id)) return (user, group) @@ -1158,7 +1158,7 @@ def is_user_in_group(self, name_or_id, group_name_or_id): self._identity_client.head( '/groups/{g}/users/{u}'.format(g=group['id'], u=user['id'])) return True - except OpenStackCloudURINotFound: + except exc.OpenStackCloudURINotFound: # NOTE(samueldmq): knowing this URI exists, let's interpret this as # user not found in group rather than URI not found. return False @@ -1188,7 +1188,7 @@ def get_template_contents( template_file=template_file, template_url=template_url, template_object=template_object, files=files) except Exception as e: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Error in processing template files: %s" % str(e)) def create_stack( @@ -1338,11 +1338,11 @@ def delete_stack(self, name_or_id, wait=False): stack_name=name_or_id, action='DELETE', marker=marker) - except OpenStackCloudHTTPError: + except exc.OpenStackCloudHTTPError: pass stack = self.get_stack(name_or_id, resolve_outputs=False) if stack and stack['stack_status'] == 'DELETE_FAILED': - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Failed to delete stack {id}: {reason}".format( id=name_or_id, reason=stack['stack_status_reason'])) @@ -1376,7 +1376,7 @@ def get_flavor_by_ram(self, ram, include=None, get_extra=True): if (flavor['ram'] >= ram and (not include or include in flavor['name'])): return flavor - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Could not find a flavor with {ram} and '{include}'".format( ram=ram, include=include)) @@ -1387,10 +1387,10 @@ def get_session_endpoint(self, service_key): self.log.debug( "Endpoint not found in %s cloud: %s", self.name, str(e)) endpoint = None - except OpenStackCloudException: + except exc.OpenStackCloudException: raise except Exception as e: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Error getting {service} endpoint on {cloud}:{region}:" " {error}".format( service=service_key, @@ -1411,7 +1411,7 @@ def has_service(self, service_key): return False try: endpoint = self.get_session_endpoint(service_key) - except OpenStackCloudException: + except exc.OpenStackCloudException: return False if endpoint: return True @@ -1719,7 +1719,7 @@ def list_qos_rule_types(self, filters=None): """ if not self._has_neutron_extension('qos'): - raise OpenStackCloudUnavailableExtension( + raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') # Translate None from search interface to empty {} for kwargs below @@ -1740,11 +1740,11 @@ def get_qos_rule_type_details(self, rule_type, filters=None): """ if not self._has_neutron_extension('qos'): - raise OpenStackCloudUnavailableExtension( + raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') if not self._has_neutron_extension('qos-rule-type-details'): - raise OpenStackCloudUnavailableExtension( + raise exc.OpenStackCloudUnavailableExtension( 'qos-rule-type-details extension is not available ' 'on target cloud') @@ -1762,7 +1762,7 @@ def list_qos_policies(self, filters=None): """ if not self._has_neutron_extension('qos'): - raise OpenStackCloudUnavailableExtension( + raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') # Translate None from search interface to empty {} for kwargs below if not filters: @@ -1789,7 +1789,7 @@ def _list(data): if endpoint: try: _list(self._volume_client.get(endpoint)) - except OpenStackCloudURINotFound: + except exc.OpenStackCloudURINotFound: # Catch and re-raise here because we are making recursive # calls and we just have context for the log here self.log.debug( @@ -1816,7 +1816,7 @@ def _list(data): try: _list(data) break - except OpenStackCloudURINotFound: + except exc.OpenStackCloudURINotFound: pass else: self.log.debug( @@ -1855,7 +1855,7 @@ def list_availability_zone_names(self, unavailable=False): try: data = _adapter._json_response( self.compute.get('/os-availability-zone')) - except OpenStackCloudHTTPError: + except exc.OpenStackCloudHTTPError: self.log.debug( "Availability zone list could not be fetched", exc_info=True) @@ -1895,7 +1895,7 @@ def list_flavors(self, get_extra=False): error_message="Error fetching flavor extra specs") flavor.extra_specs = self._get_and_munchify( 'extra_specs', data) - except OpenStackCloudHTTPError as e: + except exc.OpenStackCloudHTTPError as e: flavor.extra_specs = {} self.log.debug( 'Fetching extra specs for flavor failed:' @@ -1936,7 +1936,7 @@ def list_server_security_groups(self, server): def _get_server_security_groups(self, server, security_groups): if not self._has_secgroups(): - raise OpenStackCloudUnavailableFeature( + raise exc.OpenStackCloudUnavailableFeature( "Unavailable feature: security groups" ) @@ -2016,7 +2016,7 @@ def remove_server_security_groups(self, server, security_groups): '/servers/%s/action' % server['id'], json={'removeSecurityGroup': {'name': sg.name}})) - except OpenStackCloudURINotFound: + except exc.OpenStackCloudURINotFound: # NOTE(jamielennox): Is this ok? If we remove something that # isn't present should we just conclude job done or is that an # error? Nova returns ok if you try to add a group twice. @@ -2036,7 +2036,7 @@ def list_security_groups(self, filters=None): """ # Security groups not supported if not self._has_secgroups(): - raise OpenStackCloudUnavailableFeature( + raise exc.OpenStackCloudUnavailableFeature( "Unavailable feature: security groups" ) @@ -2145,7 +2145,7 @@ def get_compute_limits(self, name_or_id=None): proj = self.get_project(name_or_id) if not proj: - raise OpenStackCloudException("project does not exist") + raise exc.OpenStackCloudException("project does not exist") project_id = proj.id params['tenant_id'] = project_id error_msg = "{msg} for the project: {project} ".format( @@ -2224,7 +2224,7 @@ def list_floating_ip_pools(self): """ if not self._has_nova_extension('os-floating-ip-pools'): - raise OpenStackCloudUnavailableExtension( + raise exc.OpenStackCloudUnavailableExtension( 'Floating IP pools extension is not available on target cloud') data = _adapter._json_response( @@ -2238,7 +2238,7 @@ def _list_floating_ips(self, filters=None): try: return self._normalize_floating_ips( self._neutron_list_floating_ips(filters)) - except OpenStackCloudURINotFound as e: + except exc.OpenStackCloudURINotFound as e: # Nova-network don't support server-side floating ips # filtering, so it's safer to return and empty list than # to fallback to Nova which may return more results that @@ -2320,7 +2320,7 @@ def _nova_list_floating_ips(self): try: data = _adapter._json_response( self.compute.get('/os-floating-ips')) - except OpenStackCloudURINotFound: + except exc.OpenStackCloudURINotFound: return [] return self._get_and_munchify('floating_ips', data) @@ -2365,7 +2365,7 @@ def _set_interesting_networks(self): # though, that's fine, clearly the neutron introspection is # not going to work. all_networks = self.list_networks() - except OpenStackCloudException: + except exc.OpenStackCloudException: self._network_list_stamp = True return @@ -2418,7 +2418,7 @@ def _set_interesting_networks(self): if self._nat_destination in ( network['name'], network['id']): if nat_destination: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'Multiple networks were found matching' ' {nat_net} which is the network configured' ' to be the NAT destination. Please check your' @@ -2435,7 +2435,7 @@ def _set_interesting_networks(self): if all_subnets is None: try: all_subnets = self.list_subnets() - except OpenStackCloudException: + except exc.OpenStackCloudException: # Thanks Rackspace broken neutron all_subnets = [] @@ -2451,7 +2451,7 @@ def _set_interesting_networks(self): if self._default_network in ( network['name'], network['id']): if default_network: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'Multiple networks were found matching' ' {default_net} which is the network' ' configured to be the default interface' @@ -2465,41 +2465,41 @@ def _set_interesting_networks(self): # Validate config vs. reality for net_name in self._external_ipv4_names: if net_name not in [net['name'] for net in external_ipv4_networks]: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Networks: {network} was provided for external IPv4" " access and those networks could not be found".format( network=net_name)) for net_name in self._internal_ipv4_names: if net_name not in [net['name'] for net in internal_ipv4_networks]: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Networks: {network} was provided for internal IPv4" " access and those networks could not be found".format( network=net_name)) for net_name in self._external_ipv6_names: if net_name not in [net['name'] for net in external_ipv6_networks]: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Networks: {network} was provided for external IPv6" " access and those networks could not be found".format( network=net_name)) for net_name in self._internal_ipv6_names: if net_name not in [net['name'] for net in internal_ipv6_networks]: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Networks: {network} was provided for internal IPv6" " access and those networks could not be found".format( network=net_name)) if self._nat_destination and not nat_destination: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'Network {network} was configured to be the' ' destination for inbound NAT but it could not be' ' found'.format( network=self._nat_destination)) if self._default_network and not default_network: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'Network {network} was configured to be the' ' default network interface but it could not be' ' found'.format( @@ -2939,7 +2939,7 @@ def get_flavor_by_id(self, id, get_extra=False): error_message="Error fetching flavor extra specs") flavor.extra_specs = self._get_and_munchify( 'extra_specs', data) - except OpenStackCloudHTTPError as e: + except exc.OpenStackCloudHTTPError as e: flavor.extra_specs = {} self.log.debug( 'Fetching extra specs for flavor failed:' @@ -2980,7 +2980,7 @@ def get_security_group_by_id(self, id): :returns: A security group ``munch.Munch``. """ if not self._has_secgroups(): - raise OpenStackCloudUnavailableFeature( + raise exc.OpenStackCloudUnavailableFeature( "Unavailable feature: security groups" ) error_message = ("Error getting security group with" @@ -3015,12 +3015,12 @@ def get_server_console(self, server, length=None): server = self.get_server(server, bare=True) if not server: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Console log requested for invalid server") try: return self._get_server_console_output(server['id'], length) - except OpenStackCloudBadRequest: + except exc.OpenStackCloudBadRequest: return "" def _get_server_console_output(self, server_id, length=None): @@ -3166,17 +3166,17 @@ def download_image( the name or ID provided """ if output_path is None and output_file is None: - raise OpenStackCloudException('No output specified, an output path' - ' or file object is necessary to ' - 'write the image data to') + raise exc.OpenStackCloudException( + 'No output specified, an output path or file object' + ' is necessary to write the image data to') elif output_path is not None and output_file is not None: - raise OpenStackCloudException('Both an output path and file object' - ' were provided, however only one ' - 'can be used at once') + raise exc.OpenStackCloudException( + 'Both an output path and file object were provided,' + ' however only one can be used at once') image = self.search_images(name_or_id) if len(image) == 0: - raise OpenStackCloudResourceNotFound( + raise exc.OpenStackCloudResourceNotFound( "No images with name or ID %s were found" % name_or_id, None) if self._is_client_version('image', 2): endpoint = '/images/{id}/file'.format(id=image[0]['id']) @@ -3271,7 +3271,7 @@ def _search_one_stack(name_or_id=None, filters=None): # Treat DELETE_COMPLETE stacks as a NotFound if stack['stack_status'] == 'DELETE_COMPLETE': return [] - except OpenStackCloudURINotFound: + except exc.OpenStackCloudURINotFound: return [] stack = self._normalize_stack(stack) return _utils._filter_list([stack], name_or_id, filters) @@ -3312,7 +3312,7 @@ def delete_keypair(self, name): try: _adapter._json_response(self.compute.delete( '/os-keypairs/{name}'.format(name=name))) - except OpenStackCloudURINotFound: + except exc.OpenStackCloudURINotFound: self.log.debug("Keypair %s not found for deleting", name) return False return True @@ -3350,17 +3350,17 @@ def create_network(self, name, shared=False, admin_state_up=True, if availability_zone_hints is not None: if not isinstance(availability_zone_hints, list): - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Parameter 'availability_zone_hints' must be a list") if not self._has_neutron_extension('network_availability_zone'): - raise OpenStackCloudUnavailableExtension( + raise exc.OpenStackCloudUnavailableExtension( 'network_availability_zone extension is not available on ' 'target cloud') network['availability_zone_hints'] = availability_zone_hints if provider: if not isinstance(provider, dict): - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Parameter 'provider' must be a dict") # Only pass what we know for attr in ('physical_network', 'network_type', @@ -3420,7 +3420,7 @@ def create_qos_policy(self, **kwargs): :raises: OpenStackCloudException on operation error. """ if not self._has_neutron_extension('qos'): - raise OpenStackCloudUnavailableExtension( + raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') default = kwargs.pop("default", None) @@ -3455,7 +3455,7 @@ def update_qos_policy(self, name_or_id, **kwargs): :raises: OpenStackCloudException on operation error. """ if not self._has_neutron_extension('qos'): - raise OpenStackCloudUnavailableExtension( + raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') default = kwargs.pop("default", None) @@ -3472,7 +3472,7 @@ def update_qos_policy(self, name_or_id, **kwargs): curr_policy = self.get_qos_policy(name_or_id) if not curr_policy: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "QoS policy %s not found." % name_or_id) data = self._network_client.put( @@ -3491,7 +3491,7 @@ def delete_qos_policy(self, name_or_id): :raises: OpenStackCloudException on operation error. """ if not self._has_neutron_extension('qos'): - raise OpenStackCloudUnavailableExtension( + raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') policy = self.get_qos_policy(name_or_id) if not policy: @@ -3534,12 +3534,12 @@ def list_qos_bandwidth_limit_rules(self, policy_name_or_id, filters=None): found. """ if not self._has_neutron_extension('qos'): - raise OpenStackCloudUnavailableExtension( + raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') policy = self.get_qos_policy(policy_name_or_id) if not policy: - raise OpenStackCloudResourceNotFound( + raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) @@ -3567,12 +3567,12 @@ def get_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): """ if not self._has_neutron_extension('qos'): - raise OpenStackCloudUnavailableExtension( + raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') policy = self.get_qos_policy(policy_name_or_id) if not policy: - raise OpenStackCloudResourceNotFound( + raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) @@ -3601,12 +3601,12 @@ def create_qos_bandwidth_limit_rule(self, policy_name_or_id, max_kbps, :raises: OpenStackCloudException on operation error. """ if not self._has_neutron_extension('qos'): - raise OpenStackCloudUnavailableExtension( + raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') policy = self.get_qos_policy(policy_name_or_id) if not policy: - raise OpenStackCloudResourceNotFound( + raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) @@ -3642,12 +3642,12 @@ def update_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id, :raises: OpenStackCloudException on operation error. """ if not self._has_neutron_extension('qos'): - raise OpenStackCloudUnavailableExtension( + raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') policy = self.get_qos_policy(policy_name_or_id) if not policy: - raise OpenStackCloudResourceNotFound( + raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) @@ -3665,7 +3665,7 @@ def update_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id, curr_rule = self.get_qos_bandwidth_limit_rule( policy_name_or_id, rule_id) if not curr_rule: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "QoS bandwidth_limit_rule {rule_id} not found in policy " "{policy_id}".format(rule_id=rule_id, policy_id=policy['id'])) @@ -3686,12 +3686,12 @@ def delete_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): :raises: OpenStackCloudException on operation error. """ if not self._has_neutron_extension('qos'): - raise OpenStackCloudUnavailableExtension( + raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') policy = self.get_qos_policy(policy_name_or_id) if not policy: - raise OpenStackCloudResourceNotFound( + raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) @@ -3699,7 +3699,7 @@ def delete_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): self._network_client.delete( "/qos/policies/{policy}/bandwidth_limit_rules/{rule}.json". format(policy=policy['id'], rule=rule_id)) - except OpenStackCloudURINotFound: + except exc.OpenStackCloudURINotFound: self.log.debug( "QoS bandwidth limit rule {rule_id} not found in policy " "{policy_id}. Ignoring.".format(rule_id=rule_id, @@ -3739,12 +3739,12 @@ def list_qos_dscp_marking_rules(self, policy_name_or_id, filters=None): found. """ if not self._has_neutron_extension('qos'): - raise OpenStackCloudUnavailableExtension( + raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') policy = self.get_qos_policy(policy_name_or_id) if not policy: - raise OpenStackCloudResourceNotFound( + raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) @@ -3772,12 +3772,12 @@ def get_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): """ if not self._has_neutron_extension('qos'): - raise OpenStackCloudUnavailableExtension( + raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') policy = self.get_qos_policy(policy_name_or_id) if not policy: - raise OpenStackCloudResourceNotFound( + raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) @@ -3800,12 +3800,12 @@ def create_qos_dscp_marking_rule(self, policy_name_or_id, dscp_mark): :raises: OpenStackCloudException on operation error. """ if not self._has_neutron_extension('qos'): - raise OpenStackCloudUnavailableExtension( + raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') policy = self.get_qos_policy(policy_name_or_id) if not policy: - raise OpenStackCloudResourceNotFound( + raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) @@ -3832,12 +3832,12 @@ def update_qos_dscp_marking_rule(self, policy_name_or_id, rule_id, :raises: OpenStackCloudException on operation error. """ if not self._has_neutron_extension('qos'): - raise OpenStackCloudUnavailableExtension( + raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') policy = self.get_qos_policy(policy_name_or_id) if not policy: - raise OpenStackCloudResourceNotFound( + raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) @@ -3848,7 +3848,7 @@ def update_qos_dscp_marking_rule(self, policy_name_or_id, rule_id, curr_rule = self.get_qos_dscp_marking_rule( policy_name_or_id, rule_id) if not curr_rule: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "QoS dscp_marking_rule {rule_id} not found in policy " "{policy_id}".format(rule_id=rule_id, policy_id=policy['id'])) @@ -3869,12 +3869,12 @@ def delete_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): :raises: OpenStackCloudException on operation error. """ if not self._has_neutron_extension('qos'): - raise OpenStackCloudUnavailableExtension( + raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') policy = self.get_qos_policy(policy_name_or_id) if not policy: - raise OpenStackCloudResourceNotFound( + raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) @@ -3882,7 +3882,7 @@ def delete_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): self._network_client.delete( "/qos/policies/{policy}/dscp_marking_rules/{rule}.json". format(policy=policy['id'], rule=rule_id)) - except OpenStackCloudURINotFound: + except exc.OpenStackCloudURINotFound: self.log.debug( "QoS DSCP marking rule {rule_id} not found in policy " "{policy_id}. Ignoring.".format(rule_id=rule_id, @@ -3924,12 +3924,12 @@ def list_qos_minimum_bandwidth_rules(self, policy_name_or_id, found. """ if not self._has_neutron_extension('qos'): - raise OpenStackCloudUnavailableExtension( + raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') policy = self.get_qos_policy(policy_name_or_id) if not policy: - raise OpenStackCloudResourceNotFound( + raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) @@ -3957,12 +3957,12 @@ def get_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): """ if not self._has_neutron_extension('qos'): - raise OpenStackCloudUnavailableExtension( + raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') policy = self.get_qos_policy(policy_name_or_id) if not policy: - raise OpenStackCloudResourceNotFound( + raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) @@ -3989,12 +3989,12 @@ def create_qos_minimum_bandwidth_rule(self, policy_name_or_id, min_kbps, :raises: OpenStackCloudException on operation error. """ if not self._has_neutron_extension('qos'): - raise OpenStackCloudUnavailableExtension( + raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') policy = self.get_qos_policy(policy_name_or_id) if not policy: - raise OpenStackCloudResourceNotFound( + raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) @@ -4021,12 +4021,12 @@ def update_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id, :raises: OpenStackCloudException on operation error. """ if not self._has_neutron_extension('qos'): - raise OpenStackCloudUnavailableExtension( + raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') policy = self.get_qos_policy(policy_name_or_id) if not policy: - raise OpenStackCloudResourceNotFound( + raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) @@ -4037,7 +4037,7 @@ def update_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id, curr_rule = self.get_qos_minimum_bandwidth_rule( policy_name_or_id, rule_id) if not curr_rule: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "QoS minimum_bandwidth_rule {rule_id} not found in policy " "{policy_id}".format(rule_id=rule_id, policy_id=policy['id'])) @@ -4058,12 +4058,12 @@ def delete_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): :raises: OpenStackCloudException on operation error. """ if not self._has_neutron_extension('qos'): - raise OpenStackCloudUnavailableExtension( + raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') policy = self.get_qos_policy(policy_name_or_id) if not policy: - raise OpenStackCloudResourceNotFound( + raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) @@ -4071,7 +4071,7 @@ def delete_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): self._network_client.delete( "/qos/policies/{policy}/minimum_bandwidth_rules/{rule}.json". format(policy=policy['id'], rule=rule_id)) - except OpenStackCloudURINotFound: + except exc.OpenStackCloudURINotFound: self.log.debug( "QoS minimum bandwidth rule {rule_id} not found in policy " "{policy_id}. Ignoring.".format(rule_id=rule_id, @@ -4232,10 +4232,10 @@ def create_router(self, name=None, admin_state_up=True, router['external_gateway_info'] = ext_gw_info if availability_zone_hints is not None: if not isinstance(availability_zone_hints, list): - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Parameter 'availability_zone_hints' must be a list") if not self._has_neutron_extension('router_availability_zone'): - raise OpenStackCloudUnavailableExtension( + raise exc.OpenStackCloudUnavailableExtension( 'router_availability_zone extension is not available on ' 'target cloud') router['availability_zone_hints'] = availability_zone_hints @@ -4287,7 +4287,7 @@ def update_router(self, name_or_id, name=None, admin_state_up=None, curr_router = self.get_router(name_or_id) if not curr_router: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Router %s not found." % name_or_id) data = self._network_client.put( @@ -4365,7 +4365,7 @@ def create_image_snapshot( if not isinstance(server, dict): server_obj = self.get_server(server, bare=True) if not server_obj: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Server {server} could not be found and therefore" " could not be snapshotted.".format(server=server)) server = server_obj @@ -4415,7 +4415,7 @@ def wait_for_image(self, image, timeout=3600): if image['status'] == 'active': return image elif image['status'] == 'error': - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'Image {image} hit error state'.format(image=image_id)) def delete_image( @@ -4465,7 +4465,7 @@ def _get_name_and_filename(self, name): if os.path.exists(name_with_ext): return (os.path.basename(name), name_with_ext) - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'No filename parameter was given to create_image,' ' and {name} was not the path to an existing file.' ' Please provide either a path to an existing file' @@ -4564,7 +4564,7 @@ def create_image( else: volume_obj = self.get_volume(volume) if not volume_obj: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Volume {volume} given to create_image could" " not be foud".format(volume=volume)) volume_id = volume_obj['id'] @@ -4637,11 +4637,11 @@ def create_image( name, filename, meta=meta, wait=wait, timeout=timeout, **image_kwargs) - except OpenStackCloudException: + except exc.OpenStackCloudException: self.log.debug("Image creation failed", exc_info=True) raise except Exception as e: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Image creation failed: {message}".format(message=str(e))) def _make_v2_image_params(self, meta, properties): @@ -4681,7 +4681,7 @@ def _upload_image_from_volume( image_obj = self.get_image(response['image_id']) if image_obj and image_obj.status not in ('queued', 'saving'): return image_obj - except OpenStackCloudTimeout: + except exc.OpenStackCloudTimeout: self.log.debug( "Timeout waiting for image to become ready. Deleting.") self.delete_image(response['image_id'], wait=True) @@ -4707,7 +4707,7 @@ def _upload_image_put_v2(self, name, image_data, meta, **image_kwargs): try: self._image_client.delete( '/images/{id}'.format(id=image.id)) - except OpenStackCloudHTTPError: + except exc.OpenStackCloudHTTPError: # We're just trying to clean up - if it doesn't work - shrug self.log.debug( "Failed deleting image after we failed uploading it.", @@ -4738,11 +4738,11 @@ def _upload_image_put_v1( '/images/{id}'.format(id=image.id), headers=headers, data=image_data) - except OpenStackCloudHTTPError: + except exc.OpenStackCloudHTTPError: self.log.debug("Deleting failed upload of image %s", name) try: self._image_client.delete('/images/{id}'.format(id=image.id)) - except OpenStackCloudHTTPError: + except exc.OpenStackCloudHTTPError: # We're just trying to clean up - if it doesn't work - shrug self.log.debug( "Failed deleting image after we failed uploading it.", @@ -4770,7 +4770,7 @@ def _upload_image_put( image_obj = self.get_image(image.id) if image_obj and image_obj.status not in ('queued', 'saving'): return image_obj - except OpenStackCloudTimeout: + except exc.OpenStackCloudTimeout: self.log.debug( "Timeout waiting for image to become ready. Deleting.") self.delete_image(image.id, wait=True) @@ -4811,7 +4811,7 @@ def _upload_image_task( if image_id is None: status = self._image_client.get( '/tasks/{id}'.format(id=glance_task.id)) - except OpenStackCloudHTTPError as e: + except exc.OpenStackCloudHTTPError as e: if e.response.status_code == 503: # Clear the exception so that it doesn't linger # and get reported as an Inner Exception later @@ -4824,7 +4824,7 @@ def _upload_image_task( image_id = status['result']['image_id'] try: image = self.get_image(image_id) - except OpenStackCloudHTTPError as e: + except exc.OpenStackCloudHTTPError as e: if e.response.status_code == 503: # Clear the exception so that it doesn't linger # and get reported as an Inner Exception later @@ -4854,7 +4854,7 @@ def _upload_image_task( # like the content. So we don't want to keep it for # next time. self.delete_object(container, name) - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Image creation failed: {message}".format( message=status['message']), extra_data=status) @@ -4944,7 +4944,7 @@ def create_volume( if image: image_obj = self.get_image(image) if not image_obj: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Image {image} was requested as the basis for a new" " volume, but was not found on the cloud".format( image=image)) @@ -4963,7 +4963,7 @@ def create_volume( self.list_volumes.invalidate(self) if volume['status'] == 'error': - raise OpenStackCloudException("Error in creating volume") + raise exc.OpenStackCloudException("Error in creating volume") if wait: vol_id = volume['id'] @@ -4983,7 +4983,7 @@ def create_volume( return volume if volume['status'] == 'error': - raise OpenStackCloudException("Error in creating volume") + raise exc.OpenStackCloudException("Error creating volume") return self._normalize_volume(volume) @@ -5001,7 +5001,7 @@ def set_volume_bootable(self, name_or_id, bootable=True): volume = self.get_volume(name_or_id) if not volume: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Volume {name_or_id} does not exist".format( name_or_id=name_or_id)) @@ -5045,7 +5045,7 @@ def delete_volume(self, name_or_id=None, wait=True, timeout=None, else: self._volume_client.delete( 'volumes/{id}'.format(id=volume['id'])) - except OpenStackCloudURINotFound: + except exc.OpenStackCloudURINotFound: self.log.debug( "Volume {id} not found when deleting. Ignoring.".format( id=volume['id'])) @@ -5130,7 +5130,7 @@ def detach_volume(self, server, volume, wait=True, timeout=None): return if vol['status'] == 'error': - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Error in detaching volume %s" % volume['id'] ) @@ -5160,13 +5160,13 @@ def attach_volume(self, server, volume, device=None, """ dev = self.get_volume_attach_device(volume, server['id']) if dev: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Volume %s already attached to server %s on device %s" % (volume['id'], server['id'], dev) ) if volume['status'] != 'available': - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Volume %s is not available. Status is '%s'" % (volume['id'], volume['status']) ) @@ -5203,7 +5203,7 @@ def attach_volume(self, server, volume, device=None, # and also attached. If so, we should move this # above the get_volume_attach_device call if vol['status'] == 'error': - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Error in attaching volume %s" % volume['id'] ) return self._normalize_volume_attachment( @@ -5269,7 +5269,7 @@ def create_volume_snapshot(self, volume_id, force=False, break if snapshot['status'] == 'error': - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Error in creating volume snapshot") # TODO(mordred) need to normalize snapshots. We were normalizing them @@ -5362,7 +5362,7 @@ def create_volume_backup(self, volume_id, name=None, description=None, break if backup['status'] == 'error': - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Error in creating volume backup {id}".format( id=backup_id)) @@ -5548,7 +5548,7 @@ def available_floating_ip(self, network=None, server=None): self._neutron_available_floating_ips( network=network, server=server)) return f_ips[0] - except OpenStackCloudURINotFound as e: + except exc.OpenStackCloudURINotFound as e: self.log.debug( "Something went wrong talking to neutron API: " "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) @@ -5569,7 +5569,7 @@ def _get_floating_network_id(self): if floating_network: floating_network_id = floating_network else: - raise OpenStackCloudResourceNotFound( + raise exc.OpenStackCloudResourceNotFound( "unable to find an external network") return floating_network_id @@ -5608,7 +5608,7 @@ def _neutron_available_floating_ips( break if floating_network_id is None: - raise OpenStackCloudResourceNotFound( + raise exc.OpenStackCloudResourceNotFound( "unable to find external network {net}".format( net=network) ) @@ -5654,7 +5654,7 @@ def _nova_available_floating_ips(self, pool=None): if pool is None: pools = self.list_floating_ip_pools() if not pools: - raise OpenStackCloudResourceNotFound( + raise exc.OpenStackCloudResourceNotFound( "unable to find a floating ip pool") pool = pools[0]['name'] @@ -5712,14 +5712,14 @@ def create_floating_ip(self, network=None, server=None, nat_destination=nat_destination, port=port, wait=wait, timeout=timeout) - except OpenStackCloudURINotFound as e: + except exc.OpenStackCloudURINotFound as e: self.log.debug( "Something went wrong talking to neutron API: " "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) # Fall-through, trying with Nova if port: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "This cloud uses nova-network which does not support" " arbitrary floating-ip/port mappings. Please nudge" " your cloud provider to upgrade the networking stack" @@ -5747,7 +5747,7 @@ def _neutron_create_floating_ip( if network_name_or_id: network = self.get_network(network_name_or_id) if not network: - raise OpenStackCloudResourceNotFound( + raise exc.OpenStackCloudResourceNotFound( "unable to find network for floating ips with ID " "{0}".format(network_name_or_id)) network_id = network['id'] @@ -5785,7 +5785,7 @@ def _neutron_create_floating_ip( fip = self.get_floating_ip(fip_id) if fip and fip['status'] == 'ACTIVE': break - except OpenStackCloudTimeout: + except exc.OpenStackCloudTimeout: self.log.error( "Timed out on floating ip %(fip)s becoming active." " Deleting", {'fip': fip_id}) @@ -5800,13 +5800,13 @@ def _neutron_create_floating_ip( raise if fip['port_id'] != port: if server: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Attempted to create FIP on port {port} for server" " {server} but FIP has port {port_id}".format( port=port, port_id=fip['port_id'], server=server['id'])) else: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Attempted to create FIP on port {port}" " but something went wrong".format(port=port)) return fip @@ -5818,7 +5818,7 @@ def _nova_create_floating_ip(self, pool=None): if pool is None: pools = self.list_floating_ip_pools() if not pools: - raise OpenStackCloudResourceNotFound( + raise exc.OpenStackCloudResourceNotFound( "unable to find a floating ip pool") pool = pools[0]['name'] @@ -5863,7 +5863,7 @@ def delete_floating_ip(self, floating_ip_id, retry=1): if not f_ip or f_ip['status'] == 'DOWN': return True - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Attempted to delete Floating IP {ip} with ID {id} a total of" " {retry} times. Although the cloud did not indicate any errors" " the floating ip is still in existence. Aborting further" @@ -5875,7 +5875,7 @@ def _delete_floating_ip(self, floating_ip_id): if self._use_neutron_floating(): try: return self._neutron_delete_floating_ip(floating_ip_id) - except OpenStackCloudURINotFound as e: + except exc.OpenStackCloudURINotFound as e: self.log.debug( "Something went wrong talking to neutron API: " "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) @@ -5886,10 +5886,10 @@ def _neutron_delete_floating_ip(self, floating_ip_id): self._network_client.delete( "/floatingips/{fip_id}.json".format(fip_id=floating_ip_id), error_message="unable to delete floating IP") - except OpenStackCloudResourceNotFound: + except exc.OpenStackCloudResourceNotFound: return False except Exception as e: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Unable to delete floating IP ID {fip_id}: {msg}".format( fip_id=floating_ip_id, msg=str(e))) return True @@ -5901,7 +5901,7 @@ def _nova_delete_floating_ip(self, floating_ip_id): '/os-floating-ips/{id}'.format(id=floating_ip_id)), error_message='Unable to delete floating IP {fip_id}'.format( fip_id=floating_ip_id)) - except OpenStackCloudURINotFound: + except exc.OpenStackCloudURINotFound: return False return True @@ -5971,7 +5971,7 @@ def _attach_ip_to_server( server=server, floating_ip=floating_ip, fixed_address=fixed_address, nat_destination=nat_destination) - except OpenStackCloudURINotFound as e: + except exc.OpenStackCloudURINotFound as e: self.log.debug( "Something went wrong talking to neutron API: " "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) @@ -6025,7 +6025,7 @@ def _nat_destination_port( port_filter = {'device_id': server['id']} ports = self.search_ports(filters=port_filter) break - except OpenStackCloudTimeout: + except exc.OpenStackCloudTimeout: ports = None if not ports: return (None, None) @@ -6035,7 +6035,7 @@ def _nat_destination_port( if nat_destination: nat_network = self.get_network(nat_destination) if not nat_network: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'NAT Destination {nat_destination} was configured' ' but not found on the cloud. Please check your' ' config and your cloud and try again.'.format( @@ -6044,7 +6044,7 @@ def _nat_destination_port( nat_network = self.get_nat_destination() if not nat_network: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'Multiple ports were found for server {server}' ' but none of the networks are a valid NAT' ' destination, so it is impossible to add a' @@ -6061,7 +6061,7 @@ def _nat_destination_port( if maybe_port['network_id'] == nat_network['id']: maybe_ports.append(maybe_port) if not maybe_ports: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'No port on server {server} was found matching' ' your NAT destination network {dest}. Please ' ' check your config'.format( @@ -6086,7 +6086,7 @@ def _nat_destination_port( if ip.version == 4: fixed_address = address['ip_address'] return port, fixed_address - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "unable to find a free fixed IPv4 address for server " "{0}".format(server['id'])) # unfortunately a port can have more than one fixed IP: @@ -6116,7 +6116,7 @@ def _neutron_attach_ip_to_server( server, fixed_address=fixed_address, nat_destination=nat_destination) if not port: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "unable to find a port for server {0}".format( server['id'])) @@ -6137,7 +6137,7 @@ def _nova_attach_ip_to_server(self, server_id, floating_ip_id, f_ip = self.get_floating_ip( id=floating_ip_id) if f_ip is None: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "unable to find floating IP {0}".format(floating_ip_id)) error_message = "Error attaching IP {ip} to instance {id}".format( ip=floating_ip_id, id=server_id) @@ -6167,7 +6167,7 @@ def detach_ip_from_server(self, server_id, floating_ip_id): try: return self._neutron_detach_ip_from_server( server_id=server_id, floating_ip_id=floating_ip_id) - except OpenStackCloudURINotFound as e: + except exc.OpenStackCloudURINotFound as e: self.log.debug( "Something went wrong talking to neutron API: " "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) @@ -6194,7 +6194,7 @@ def _nova_detach_ip_from_server(self, server_id, floating_ip_id): f_ip = self.get_floating_ip(id=floating_ip_id) if f_ip is None: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "unable to find floating IP {0}".format(floating_ip_id)) error_message = "Error detaching IP {ip} from instance {id}".format( ip=floating_ip_id, id=server_id) @@ -6332,7 +6332,7 @@ def _add_auto_ip(self, server, wait=False, timeout=60, reuse=True): return self._attach_ip_to_server( server=server, floating_ip=f_ip, wait=wait, timeout=timeout, skip_attach=skip_attach) - except OpenStackCloudTimeout: + except exc.OpenStackCloudTimeout: if self._use_neutron_floating() and created: # We are here because we created an IP on the port # It failed. Delete so as not to leak an unmanaged @@ -6415,7 +6415,7 @@ def _needs_floating_ip(self, server, nat_destination): # No floating ip network - no FIPs try: self._get_floating_network_id() - except OpenStackCloudException: + except exc.OpenStackCloudException: return False (port_obj, fixed_ip_address) = self._nat_destination_port( @@ -6446,7 +6446,7 @@ def _get_boot_from_volume_kwargs( if boot_volume: volume = self.get_volume(boot_volume) if not volume: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'Volume {boot_volume} is not a valid volume' ' in {cloud}:{region}'.format( boot_volume=boot_volume, @@ -6467,7 +6467,7 @@ def _get_boot_from_volume_kwargs( else: image_obj = self.get_image(image) if not image_obj: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'Image {image} is not a valid image in' ' {cloud}:{region}'.format( image=image, @@ -6497,7 +6497,7 @@ def _get_boot_from_volume_kwargs( for volume in volumes: volume_obj = self.get_volume(volume) if not volume_obj: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'Volume {volume} is not a valid volume' ' in {cloud}:{region}'.format( volume=volume, @@ -6521,7 +6521,7 @@ def _encode_server_userdata(self, userdata): if not isinstance(userdata, six.binary_type): # If the userdata passed in is bytes, just send it unmodified if not isinstance(userdata, six.string_types): - raise TypeError("%s can't be encoded" % type(text)) + raise TypeError("%s can't be encoded" % type(userdata)) # If it's not bytes, make it bytes userdata = userdata.encode('utf-8', 'strict') @@ -6663,7 +6663,7 @@ def create_server( if group: group_obj = self.get_server_group(group) if not group_obj: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Server Group {group} was requested but was not found" " on the cloud".format(group=group)) hints['group'] = group_obj['id'] @@ -6677,7 +6677,7 @@ def create_server( # Be nice and help the user out kwargs['nics'] = [kwargs['nics']] else: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'nics parameter to create_server takes a list of dicts.' ' Got: {nics}'.format(nics=kwargs['nics'])) @@ -6691,7 +6691,7 @@ def create_server( else: network_obj = self.get_network(name_or_id=net_name) if not network_obj: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'Network {network} is not a valid network in' ' {cloud}:{region}'.format( network=network, @@ -6715,7 +6715,7 @@ def create_server( elif 'net-name' in nic: nic_net = self.get_network(nic['net-name']) if not nic_net: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Requested network {net} could not be found.".format( net=nic['net-name'])) net['uuid'] = nic_net['id'] @@ -6727,7 +6727,7 @@ def create_server( if 'port-id' in nic: net['port'] = nic.pop('port-id') if nic: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Additional unsupported keys given for server network" " creation: {keys}".format(keys=nic.keys())) networks.append(net) @@ -6779,7 +6779,7 @@ def create_server( # going to do the wait loop below, this is a waste of a call server = self.get_server_by_id(server.id) if server.status == 'ERROR': - raise OpenStackCloudCreateException( + raise exc.OpenStackCloudCreateException( resource='server', resource_id=server.id) if wait: @@ -6824,7 +6824,7 @@ def wait_for_server( # and pass it down into the IP stack. remaining_timeout = timeout - int(time.time() - start_time) if remaining_timeout <= 0: - raise OpenStackCloudTimeout(timeout_message) + raise exc.OpenStackCloudTimeout(timeout_message) server = self.get_active_server( server=server, reuse=reuse, @@ -6841,12 +6841,12 @@ def get_active_server( if server['status'] == 'ERROR': if 'fault' in server and 'message' in server['fault']: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Error in creating the server: {reason}".format( reason=server['fault']['message']), extra_data=dict(server=server)) - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Error in creating the server", extra_data=dict(server=server)) if server['status'] == 'ACTIVE': @@ -6864,12 +6864,12 @@ def get_active_server( self._delete_server( server=server, wait=wait, timeout=timeout) except Exception as e: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'Server reached ACTIVE state without being' ' allocated an IP address AND then could not' ' be deleted: {0}'.format(e), extra_data=dict(server=server)) - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'Server reached ACTIVE state without being' ' allocated an IP address.', extra_data=dict(server=server)) @@ -6908,7 +6908,7 @@ def rebuild_server(self, server_id, image_id, admin_pass=None, continue if server['status'] == 'ERROR': - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Error in rebuilding the server", extra_data=dict(server=server)) @@ -6931,7 +6931,7 @@ def set_server_metadata(self, name_or_id, metadata): """ server = self.get_server(name_or_id, bare=True) if not server: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'Invalid Server {server}'.format(server=name_or_id)) _adapter._json_response( @@ -6952,7 +6952,7 @@ def delete_server_metadata(self, name_or_id, metadata_keys): """ server = self.get_server(name_or_id, bare=True) if not server: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'Invalid Server {server}'.format(server=name_or_id)) for key in metadata_keys: @@ -7004,7 +7004,7 @@ def _delete_server_floating_ips(self, server, delete_ip_retry): try: ip = self.get_floating_ip(id=None, filters={ 'floating_ip_address': fip['addr']}) - except OpenStackCloudURINotFound: + except exc.OpenStackCloudURINotFound: # We're deleting. If it doesn't exist - awesome # NOTE(mordred) If the cloud is a nova FIP cloud but # floating_ip_source is set to neutron, this @@ -7015,7 +7015,7 @@ def _delete_server_floating_ips(self, server, delete_ip_retry): deleted = self.delete_floating_ip( ip['id'], retry=delete_ip_retry) if not deleted: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Tried to delete floating ip {floating_ip}" " associated with server {id} but there was" " an error deleting it. Not deleting server.".format( @@ -7036,7 +7036,7 @@ def _delete_server( self.compute.delete( '/servers/{id}'.format(id=server['id'])), error_message="Error in deleting server") - except OpenStackCloudURINotFound: + except exc.OpenStackCloudURINotFound: return False except Exception: raise @@ -7094,7 +7094,7 @@ def update_server(self, name_or_id, detailed=False, bare=False, **kwargs): """ server = self.get_server(name_or_id=name_or_id, bare=True) if server is None: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "failed to find server '{server}'".format(server=name_or_id)) data = _adapter._json_response( @@ -7166,7 +7166,7 @@ def get_container(self, name, skip_cache=False): try: container = self._object_store_client.head(name) self._container_cache[name] = container.headers - except OpenStackCloudHTTPError as e: + except exc.OpenStackCloudHTTPError as e: if e.response.status_code == 404: return None raise @@ -7185,11 +7185,11 @@ def delete_container(self, name): try: self._object_store_client.delete(name) return True - except OpenStackCloudHTTPError as e: + except exc.OpenStackCloudHTTPError as e: if e.response.status_code == 404: return False if e.response.status_code == 409: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'Attempt to delete container {container} failed. The' ' container is not empty. Please delete the objects' ' inside it before deleting the container'.format( @@ -7201,7 +7201,7 @@ def update_container(self, name, headers): def set_container_access(self, name, access): if access not in OBJECT_CONTAINER_ACLS: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Invalid container access specified: %s. Must be one of %s" % (access, list(OBJECT_CONTAINER_ACLS.keys()))) header = {'x-container-read': OBJECT_CONTAINER_ACLS[access]} @@ -7210,7 +7210,7 @@ def set_container_access(self, name, access): def get_container_access(self, name): container = self.get_container(name, skip_cache=True) if not container: - raise OpenStackCloudException("Container not found: %s" % name) + raise exc.OpenStackCloudException("Container not found: %s" % name) acl = container.get('x-container-read', '') for key, value in OBJECT_CONTAINER_ACLS.items(): # Convert to string for the comparison because swiftclient @@ -7218,7 +7218,7 @@ def get_container_access(self, name): # on bytes doesn't work like you'd think if str(acl) == str(value): return key - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Could not determine container access for ACL: %s." % acl) def _get_file_hashes(self, filename): @@ -7263,7 +7263,7 @@ def get_object_segment_size(self, segment_size): min_segment_size = 0 try: caps = self.get_object_capabilities() - except OpenStackCloudHTTPError as e: + except exc.OpenStackCloudHTTPError as e: if e.response.status_code in (404, 412): # Clear the exception so that it doesn't linger # and get reported as an Inner Exception later @@ -7569,7 +7569,7 @@ def delete_object(self, container, name, meta=None): container=container, object=name), params=params) return True - except OpenStackCloudHTTPError: + except exc.OpenStackCloudHTTPError: return False def delete_autocreated_image_objects( @@ -7599,7 +7599,7 @@ def get_object_metadata(self, container, name): return self._object_store_client.head( '{container}/{object}'.format( container=container, object=name)).headers - except OpenStackCloudException as e: + except exc.OpenStackCloudException as e: if e.response.status_code == 404: return None raise @@ -7651,7 +7651,7 @@ def get_object(self, container, obj, query_string=None, return (response_headers, None) else: return (response_headers, response.text) - except OpenStackCloudHTTPError as e: + except exc.OpenStackCloudHTTPError as e: if e.response.status_code == 404: return None raise @@ -7740,19 +7740,19 @@ def create_subnet(self, network_name_or_id, cidr=None, ip_version=4, network = self.get_network(network_name_or_id, filters) if not network: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Network %s not found." % network_name_or_id) if disable_gateway_ip and gateway_ip: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'arg:disable_gateway_ip is not allowed with arg:gateway_ip') if not cidr and not use_default_subnetpool: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'arg:cidr is required when a subnetpool is not used') if cidr and use_default_subnetpool: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'arg:cidr must be set to None when use_default_subnetpool == ' 'True') @@ -7761,7 +7761,8 @@ def create_subnet(self, network_name_or_id, cidr=None, ip_version=4, try: ip_version = int(ip_version) except ValueError: - raise OpenStackCloudException('ip_version must be an integer') + raise exc.OpenStackCloudException( + 'ip_version must be an integer') # The body of the neutron message for the subnet we wish to create. # This includes attributes that are required or have defaults. @@ -7896,12 +7897,12 @@ def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, return if disable_gateway_ip and gateway_ip: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'arg:disable_gateway_ip is not allowed with arg:gateway_ip') curr_subnet = self.get_subnet(name_or_id) if not curr_subnet: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Subnet %s not found." % name_or_id) data = self._network_client.put( @@ -8029,7 +8030,7 @@ def update_port(self, name_or_id, **kwargs): """ port = self.get_port(name_or_id=name_or_id) if port is None: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "failed to find port '{port}'".format(port=name_or_id)) data = self._network_client.put( @@ -8075,7 +8076,7 @@ def create_security_group(self, name, description, project_id=None): # Security groups not supported if not self._has_secgroups(): - raise OpenStackCloudUnavailableFeature( + raise exc.OpenStackCloudUnavailableFeature( "Unavailable feature: security groups" ) @@ -8110,7 +8111,7 @@ def delete_security_group(self, name_or_id): """ # Security groups not supported if not self._has_secgroups(): - raise OpenStackCloudUnavailableFeature( + raise exc.OpenStackCloudUnavailableFeature( "Unavailable feature: security groups" ) @@ -8149,14 +8150,14 @@ def update_security_group(self, name_or_id, **kwargs): """ # Security groups not supported if not self._has_secgroups(): - raise OpenStackCloudUnavailableFeature( + raise exc.OpenStackCloudUnavailableFeature( "Unavailable feature: security groups" ) group = self.get_security_group(name_or_id) if group is None: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Security group %s not found." % name_or_id) if self._use_neutron_secgroups(): @@ -8232,13 +8233,13 @@ def create_security_group_rule(self, """ # Security groups not supported if not self._has_secgroups(): - raise OpenStackCloudUnavailableFeature( + raise exc.OpenStackCloudUnavailableFeature( "Unavailable feature: security groups" ) secgroup = self.get_security_group(secgroup_name_or_id) if not secgroup: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Security group %s not found." % secgroup_name_or_id) if self._use_neutron_secgroups(): @@ -8266,13 +8267,14 @@ def create_security_group_rule(self, else: # NOTE: Neutron accepts None for protocol. Nova does not. if protocol is None: - raise OpenStackCloudException('Protocol must be specified') + raise exc.OpenStackCloudException('Protocol must be specified') if direction == 'egress': self.log.debug( 'Rule creation failed: Nova does not support egress rules' ) - raise OpenStackCloudException('No support for egress rules') + raise exc.OpenStackCloudException( + 'No support for egress rules') # NOTE: Neutron accepts None for ports, but Nova requires -1 # as the equivalent value for ICMP. @@ -8324,7 +8326,7 @@ def delete_security_group_rule(self, rule_id): """ # Security groups not supported if not self._has_secgroups(): - raise OpenStackCloudUnavailableFeature( + raise exc.OpenStackCloudUnavailableFeature( "Unavailable feature: security groups" ) @@ -8334,7 +8336,7 @@ def delete_security_group_rule(self, rule_id): '/security-group-rules/{sg_id}.json'.format(sg_id=rule_id), error_message="Error deleting security group rule " "{0}".format(rule_id)) - except OpenStackCloudResourceNotFound: + except exc.OpenStackCloudResourceNotFound: return False return True @@ -8396,7 +8398,7 @@ def create_zone(self, name, zone_type=None, email=None, description=None, if zone_type is not None: zone_type = zone_type.upper() if zone_type not in ('PRIMARY', 'SECONDARY'): - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Invalid type %s, valid choices are PRIMARY or SECONDARY" % zone_type) @@ -8437,7 +8439,7 @@ def update_zone(self, name_or_id, **kwargs): """ zone = self.get_zone(name_or_id) if not zone: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Zone %s not found." % name_or_id) data = self._dns_client.patch( @@ -8476,7 +8478,7 @@ def list_recordsets(self, zone): """ zone_obj = self.get_zone(zone) if zone_obj is None: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Zone %s not found." % zone) return self._dns_client.get( "/zones/{zone_id}/recordsets".format(zone_id=zone_obj['id']), @@ -8494,7 +8496,7 @@ def get_recordset(self, zone, name_or_id): """ zone_obj = self.get_zone(zone) if zone_obj is None: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Zone %s not found." % zone) try: return self._dns_client.get( @@ -8526,7 +8528,7 @@ def create_recordset(self, zone, name, recordset_type, records, """ zone_obj = self.get_zone(zone) if zone_obj is None: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Zone %s not found." % zone) # We capitalize the type in case the user sends in lowercase @@ -8565,12 +8567,12 @@ def update_recordset(self, zone, name_or_id, **kwargs): """ zone_obj = self.get_zone(zone) if zone_obj is None: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Zone %s not found." % zone) recordset_obj = self.get_recordset(zone, name_or_id) if recordset_obj is None: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Recordset %s not found." % name_or_id) new_recordset = self._dns_client.put( @@ -8753,7 +8755,7 @@ def update_cluster_template(self, name_or_id, operation, **kwargs): self.list_cluster_templates.invalidate(self) cluster_template = self.get_cluster_template(name_or_id) if not cluster_template: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Cluster template %s not found." % name_or_id) if operation not in ['add', 'replace', 'remove']: @@ -8880,7 +8882,7 @@ def inspect_machine(self, name_or_id, wait=False, timeout=3600): machine = self.get_machine(name_or_id) if not machine: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Machine inspection failed to find: %s." % name_or_id) # NOTE(TheJulia): If in available state, we can do this, however @@ -8894,7 +8896,7 @@ def inspect_machine(self, name_or_id, wait=False, timeout=3600): wait=True, timeout=timeout) elif ("manage" not in machine['provision_state'] and "inspect failed" not in machine['provision_state']): - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Machine must be in 'manage' or 'available' state to " "engage inspection: Machine: %s State: %s" % (machine['uuid'], machine['provision_state'])) @@ -8908,7 +8910,7 @@ def inspect_machine(self, name_or_id, wait=False, timeout=3600): machine = self.get_machine(name_or_id) if "inspect failed" in machine['provision_state']: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Inspection of node %s failed, last error: %s" % (machine['uuid'], machine['last_error'])) @@ -9013,7 +9015,7 @@ def register_machine(self, nics, wait=False, timeout=3600, self._baremetal_client.delete(url, error_message=msg, microversion=version) - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Error registering NICs with the baremetal service: %s" % str(e)) @@ -9037,7 +9039,7 @@ def register_machine(self, nics, wait=False, timeout=3600, self.node_set_provision_state( machine['uuid'], 'provide') elif machine['last_error'] is not None: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Machine encountered a failure: %s" % machine['last_error']) @@ -9078,7 +9080,7 @@ def register_machine(self, nics, wait=False, timeout=3600, break elif machine['last_error'] is not None: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Machine encountered a failure: %s" % machine['last_error']) if not isinstance(machine, str): @@ -9109,7 +9111,7 @@ def unregister_machine(self, nics, uuid, wait=False, timeout=600): machine = self.get_machine(uuid) invalid_states = ['active', 'cleaning', 'clean wait', 'clean failed'] if machine['provision_state'] in invalid_states: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Error unregistering node '%s' due to current provision " "state '%s'" % (uuid, machine['provision_state'])) @@ -9119,11 +9121,10 @@ def unregister_machine(self, nics, uuid, wait=False, timeout=600): # failure, and resubitted the request in python-ironicclient. try: self.wait_for_baremetal_node_lock(machine, timeout=timeout) - except OpenStackCloudException as e: - raise OpenStackCloudException("Error unregistering node '%s': " - "Exception occured while waiting " - "to be able to proceed: %s" - % (machine['uuid'], e)) + except exc.OpenStackCloudException as e: + raise exc.OpenStackCloudException( + "Error unregistering node '%s': Exception occured while" + " waiting to be able to proceed: %s" % (machine['uuid'], e)) for nic in nics: port_msg = ("Error removing NIC {nic} from baremetal API for " @@ -9243,7 +9244,7 @@ def update_machine(self, name_or_id, chassis_uuid=None, driver=None, """ machine = self.get_machine(name_or_id) if not machine: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Machine update failed to find Machine: %s. " % name_or_id) machine_config = {} @@ -9281,7 +9282,7 @@ def update_machine(self, name_or_id, chassis_uuid=None, driver=None, self.log.debug( "Unexpected machine response missing key %s [%s]", e.args[0], name_or_id) - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Machine update failed - machine [%s] missing key %s. " "Potential API issue." % (name_or_id, e.args[0])) @@ -9289,7 +9290,7 @@ def update_machine(self, name_or_id, chassis_uuid=None, driver=None, try: patch = jsonpatch.JsonPatch.from_diff(machine_config, new_config) except Exception as e: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Machine update failed - Error generating JSON patch object " "for submission to the API. Machine: %s Error: %s" % (name_or_id, str(e))) @@ -9324,7 +9325,7 @@ def validate_node(self, uuid): ifaces = self._baremetal_client.get(url, error_message=msg) if not ifaces['deploy'] or not ifaces['power']: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "ironic node %s failed to validate. " "(deploy: %s, power: %s)" % (ifaces['deploy'], ifaces['power'])) @@ -9389,7 +9390,7 @@ def node_set_provision_state(self, "target state of '%s'" % state): machine = self.get_machine(name_or_id) if 'failed' in machine['provision_state']: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Machine encountered a failure.") # NOTE(TheJulia): This performs matching if the requested # end state matches the state the node has reached. @@ -9659,7 +9660,7 @@ def create_service(self, name, enabled=True, **kwargs): def update_service(self, name_or_id, **kwargs): # NOTE(SamYaple): Service updates are only available on v3 api if self._is_client_version('identity', 2): - raise OpenStackCloudUnavailableFeature( + raise exc.OpenStackCloudUnavailableFeature( 'Unavailable Feature: Service update requires Identity v3' ) @@ -9791,20 +9792,21 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, admin_url = kwargs.pop('admin_url', None) if (url or interface) and (public_url or internal_url or admin_url): - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "create_endpoint takes either url and interface OR" " public_url, internal_url, admin_url") service = self.get_service(name_or_id=service_name_or_id) if service is None: - raise OpenStackCloudException("service {service} not found".format( - service=service_name_or_id)) + raise exc.OpenStackCloudException( + "service {service} not found".format( + service=service_name_or_id)) if self._is_client_version('identity', 2): if url: # v2.0 in use, v3-like arguments, one endpoint created if interface != 'public': - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Error adding endpoint for service {service}." " On a v2 cloud the url/interface API may only be" " used for public url. Try using the public_url," @@ -9873,7 +9875,7 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, def update_endpoint(self, endpoint_id, **kwargs): # NOTE(SamYaple): Endpoint updates are only available on v3 api if self._is_client_version('identity', 2): - raise OpenStackCloudUnavailableFeature( + raise exc.OpenStackCloudUnavailableFeature( 'Unavailable Feature: Endpoint update' ) @@ -9996,12 +9998,12 @@ def update_domain( enabled=None, name_or_id=None): if domain_id is None: if name_or_id is None: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "You must pass either domain_id or name_or_id value" ) dom = self.get_domain(None, name_or_id) if dom is None: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Domain {0} not found for updating".format(name_or_id) ) domain_id = dom['id'] @@ -10031,7 +10033,7 @@ def delete_domain(self, domain_id=None, name_or_id=None): """ if domain_id is None: if name_or_id is None: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "You must pass either domain_id or name_or_id value" ) dom = self.get_domain(name_or_id=name_or_id) @@ -10185,7 +10187,7 @@ def create_group(self, name, description, domain=None): if domain: dom = self.get_domain(domain) if not dom: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Creating group {group} failed: Invalid domain " "{domain}".format(group=name, domain=domain) ) @@ -10215,7 +10217,7 @@ def update_group(self, name_or_id, name=None, description=None, self.list_groups.invalidate(self) group = self.get_group(name_or_id, **kwargs) if group is None: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Group {0} not found for updating".format(name_or_id) ) @@ -10419,7 +10421,7 @@ def list_role_assignments(self, filters=None): if self._is_client_version('identity', 2): if filters.get('project') is None or filters.get('user') is None: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Must provide project and user for keystone v2" ) assignments = self._keystone_v2_role_assignments(**filters) @@ -10606,7 +10608,7 @@ def update_role(self, name_or_id, name, **kwargs): :raise OpenStackCloudException: if the role cannot be created """ if self._is_client_version('identity', 2): - raise OpenStackCloudUnavailableFeature( + raise exc.OpenStackCloudUnavailableFeature( 'Unavailable Feature: Role update requires Identity v3' ) kwargs['name_or_id'] = name_or_id @@ -10706,18 +10708,18 @@ def grant_role(self, name_or_id, user=None, group=None, project, domain) filters = data.copy() if not data: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'Role {0} not found.'.format(name_or_id)) if data.get('user') is not None and data.get('group') is not None: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'Specify either a group or a user, not both') if data.get('user') is None and data.get('group') is None: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'Must specify either a user or a group') if self._is_client_version('identity', 2) and \ data.get('project') is None: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'Must specify project for keystone v2') if self.list_role_assignments(filters=filters): @@ -10734,7 +10736,7 @@ def grant_role(self, name_or_id, user=None, group=None, endpoint_filter={'interface': 'admin'}) else: if data.get('project') is None and data.get('domain') is None: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'Must specify either a domain or project') # For v3, figure out the assignment type and build the URL @@ -10784,18 +10786,18 @@ def revoke_role(self, name_or_id, user=None, group=None, filters = data.copy() if not data: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'Role {0} not found.'.format(name_or_id)) if data.get('user') is not None and data.get('group') is not None: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'Specify either a group or a user, not both') if data.get('user') is None and data.get('group') is None: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'Must specify either a user or a group') if self._is_client_version('identity', 2) and \ data.get('project') is None: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'Must specify project for keystone v2') if not self.list_role_assignments(filters=filters): @@ -10813,7 +10815,7 @@ def revoke_role(self, name_or_id, user=None, group=None, endpoint_filter={'interface': 'admin'}) else: if data.get('project') is None and data.get('domain') is None: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( 'Must specify either a domain or project') # For v3, figure out the assignment type and build the URL @@ -10929,7 +10931,7 @@ def update_aggregate(self, name_or_id, **kwargs): """ aggregate = self.get_aggregate(name_or_id) if not aggregate: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Host aggregate %s not found." % name_or_id) data = _adapter._json_response( @@ -10975,7 +10977,7 @@ def set_aggregate_metadata(self, name_or_id, metadata): """ aggregate = self.get_aggregate(name_or_id) if not aggregate: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Host aggregate %s not found." % name_or_id) err_msg = "Unable to set metadata for host aggregate {name}".format( @@ -10998,7 +11000,7 @@ def add_host_to_aggregate(self, name_or_id, host_name): """ aggregate = self.get_aggregate(name_or_id) if not aggregate: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Host aggregate %s not found." % name_or_id) err_msg = "Unable to add host {host} to aggregate {name}".format( @@ -11020,7 +11022,7 @@ def remove_host_from_aggregate(self, name_or_id, host_name): """ aggregate = self.get_aggregate(name_or_id) if not aggregate: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Host aggregate %s not found." % name_or_id) err_msg = "Unable to remove host {host} to aggregate {name}".format( @@ -11041,7 +11043,7 @@ def get_volume_type_access(self, name_or_id): """ volume_type = self.get_volume_type(name_or_id) if not volume_type: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "VolumeType not found: %s" % name_or_id) data = self._volume_client.get( @@ -11063,7 +11065,7 @@ def add_volume_type_access(self, name_or_id, project_id): """ volume_type = self.get_volume_type(name_or_id) if not volume_type: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "VolumeType not found: %s" % name_or_id) with _utils.shade_exceptions(): payload = {'project': project_id} @@ -11084,7 +11086,7 @@ def remove_volume_type_access(self, name_or_id, project_id): """ volume_type = self.get_volume_type(name_or_id) if not volume_type: - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "VolumeType not found: %s" % name_or_id) with _utils.shade_exceptions(): payload = {'project': project_id} @@ -11107,7 +11109,7 @@ def set_compute_quotas(self, name_or_id, **kwargs): proj = self.get_project(name_or_id) if not proj: - raise OpenStackCloudException("project does not exist") + raise exc.OpenStackCloudException("project does not exist") # compute_quotas = {key: val for key, val in kwargs.items() # if key in quota.COMPUTE_QUOTAS} @@ -11134,7 +11136,7 @@ def get_compute_quotas(self, name_or_id): """ proj = self.get_project(name_or_id) if not proj: - raise OpenStackCloudException("project does not exist") + raise exc.OpenStackCloudException("project does not exist") data = _adapter._json_response( self.compute.get( '/os-quota-sets/{project}'.format(project=proj.id))) @@ -11151,7 +11153,7 @@ def delete_compute_quotas(self, name_or_id): """ proj = self.get_project(name_or_id) if not proj: - raise OpenStackCloudException("project does not exist") + raise exc.OpenStackCloudException("project does not exist") return _adapter._json_response( self.compute.delete( '/os-quota-sets/{project}'.format(project=proj.id))) @@ -11176,7 +11178,7 @@ def parse_date(date): # Yes. This is an exception mask. However,iso8601 is an # implementation detail - and the error message is actually # less informative. - raise OpenStackCloudException( + raise exc.OpenStackCloudException( "Date given, {date}, is invalid. Please pass in a date" " string in ISO 8601 format -" " YYYY-MM-DDTHH:MM:SS".format( @@ -11208,8 +11210,8 @@ def parse_datetime_for_nova(date): proj = self.get_project(name_or_id) if not proj: - raise OpenStackCloudException("project does not exist: {}".format( - name=proj.id)) + raise exc.OpenStackCloudException( + "project does not exist: {}".format(name=proj.id)) data = _adapter._json_response( self.compute.get( @@ -11232,7 +11234,7 @@ def set_volume_quotas(self, name_or_id, **kwargs): proj = self.get_project(name_or_id) if not proj: - raise OpenStackCloudException("project does not exist") + raise exc.OpenStackCloudException("project does not exist") kwargs['tenant_id'] = proj.id self._volume_client.put( @@ -11250,7 +11252,7 @@ def get_volume_quotas(self, name_or_id): """ proj = self.get_project(name_or_id) if not proj: - raise OpenStackCloudException("project does not exist") + raise exc.OpenStackCloudException("project does not exist") data = self._volume_client.get( '/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id), @@ -11268,7 +11270,7 @@ def delete_volume_quotas(self, name_or_id): """ proj = self.get_project(name_or_id) if not proj: - raise OpenStackCloudException("project does not exist") + raise exc.OpenStackCloudException("project does not exist") return self._volume_client.delete( '/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id), @@ -11286,7 +11288,7 @@ def set_network_quotas(self, name_or_id, **kwargs): proj = self.get_project(name_or_id) if not proj: - raise OpenStackCloudException("project does not exist") + raise exc.OpenStackCloudException("project does not exist") self._network_client.put( '/quotas/{project_id}.json'.format(project_id=proj.id), @@ -11306,7 +11308,7 @@ def get_network_quotas(self, name_or_id, details=False): """ proj = self.get_project(name_or_id) if not proj: - raise OpenStackCloudException("project does not exist") + raise exc.OpenStackCloudException("project does not exist") url = '/quotas/{project_id}'.format(project_id=proj.id) if details: url = url + "/details" @@ -11335,7 +11337,7 @@ def delete_network_quotas(self, name_or_id): """ proj = self.get_project(name_or_id) if not proj: - raise OpenStackCloudException("project does not exist") + raise exc.OpenStackCloudException("project does not exist") self._network_client.delete( '/quotas/{project_id}.json'.format(project_id=proj.id), error_message=("Error deleting Neutron's quota for " diff --git a/tox.ini b/tox.ini index b74e095d4..d8e42f659 100644 --- a/tox.ini +++ b/tox.ini @@ -95,7 +95,7 @@ commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasen # H306 Is about alphabetical imports - there's a lot to fix. # H4 Are about docstrings and there's just a huge pile of pre-existing issues. # D* Came from sdk, unknown why they're skipped. -ignore = H103,H306,H4,D100,D101,D102,D103,D104,D105,D200,D202,D204,D205,D211,D301,D400,D401,F405,W503 +ignore = H103,H306,H4,D100,D101,D102,D103,D104,D105,D200,D202,D204,D205,D211,D301,D400,D401,W503 show-source = True exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build From 9f641b038c72c2b589c4eb3bf4474f5054a53d8b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 9 May 2018 10:15:45 -0500 Subject: [PATCH 2057/3836] Add nodepool-functional-py35-src job As openstacksdk takes on more and more ownership of code that was previously in os-client-config and shade, it's important to make sure we're not going to break nodepool with changes. Add the nodepool-functional-py35-src job which now also includes openstacksdk in its required-projects list. Depends-On: https://review.openstack.org/566387 Change-Id: I464bd1ab548f23e5a143f50dcb32e52b6f353d7d --- .zuul.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.zuul.yaml b/.zuul.yaml index 08c83cbf6..783fa30c8 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -232,6 +232,7 @@ - neutron-grenade: voting: false - openstack-tox-lower-constraints + - nodepool-functional-py35-src gate: jobs: - build-openstack-sphinx-docs: @@ -240,3 +241,4 @@ - openstacksdk-functional-devstack - openstacksdk-functional-devstack-python3 - openstack-tox-lower-constraints + - nodepool-functional-py35-src From be87ac1d9bc0fb409806937a5cbf82e94e4f3ea4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 9 May 2018 10:18:10 -0500 Subject: [PATCH 2058/3836] Add python 3.6 jobs 3.6 is the future. Let's test it today. Change-Id: I1c240f10e42fadbdaed78b07ff26fde52c11f8cd --- .zuul.yaml | 1 + setup.cfg | 1 + tox.ini | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index 783fa30c8..e9e8bda93 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -212,6 +212,7 @@ - project: templates: + - openstack-python36-jobs - openstacksdk-functional-tips - openstacksdk-tox-tips - os-client-config-tox-tips diff --git a/setup.cfg b/setup.cfg index 9d56d0e02..fd51be71d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,6 +17,7 @@ classifier = Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 [files] packages = diff --git a/tox.ini b/tox.ini index b74e095d4..6eb25ed14 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 1.6 -envlist = py35,py27,pep8 +envlist = py35,py36,py27,pep8 skipsdist = True [testenv] From 9007ab7048c29b2f30a73fa40f6c344d591950a6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 9 May 2018 09:01:54 -0500 Subject: [PATCH 2059/3836] Add comment about W503 being skipped It's on purpose and we do not want to 'fix' occurances. If anything, a rule checking the opposite of W503 would be preferrable. Change-Id: I246b23e7e1ead1682167f3bf076ce563697217e6 --- tox.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tox.ini b/tox.ini index d8e42f659..27bed5ce3 100644 --- a/tox.ini +++ b/tox.ini @@ -95,6 +95,9 @@ commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasen # H306 Is about alphabetical imports - there's a lot to fix. # H4 Are about docstrings and there's just a huge pile of pre-existing issues. # D* Came from sdk, unknown why they're skipped. +# W503 Is supposed to be off by default but in the latest pycodestyle isn't. +# Also, both openstacksdk and Donald Knuth disagree with the rule. Line +# breaks should occur before the binary operator for readability. ignore = H103,H306,H4,D100,D101,D102,D103,D104,D105,D200,D202,D204,D205,D211,D301,D400,D401,W503 show-source = True exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build From ee1d1ad96b6e991cfc1f468d00e739f8e49bba72 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 9 May 2018 09:03:51 -0500 Subject: [PATCH 2060/3836] Remove D exclusions from flake8 config It was unknown why they exist, but removing them doesn't break anything. Change-Id: I9266ff844407ce4c9e52bda7ee570a7a3e707534 --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 27bed5ce3..36fcc8a4e 100644 --- a/tox.ini +++ b/tox.ini @@ -94,11 +94,10 @@ commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasen # this, please be sure to preseve all copyright lines. # H306 Is about alphabetical imports - there's a lot to fix. # H4 Are about docstrings and there's just a huge pile of pre-existing issues. -# D* Came from sdk, unknown why they're skipped. # W503 Is supposed to be off by default but in the latest pycodestyle isn't. # Also, both openstacksdk and Donald Knuth disagree with the rule. Line # breaks should occur before the binary operator for readability. -ignore = H103,H306,H4,D100,D101,D102,D103,D104,D105,D200,D202,D204,D205,D211,D301,D400,D401,W503 +ignore = H103,H306,H4,W503 show-source = True exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build From 055536ef51050e19f0d16503ae765f33cc48374b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 9 May 2018 09:21:29 -0500 Subject: [PATCH 2061/3836] Fix H103 Apache license header check It's good to have a license header check. Amusingly - this actually caught a real error in one of the headers. I'll leave it as an exercise for the reader as to which one was a real error. Change-Id: I46bee047d798fcb384e711462bc7463ca8745ee8 --- openstack/cloud/_tasks.py | 4 +--- openstack/cloud/openstackcloud.py | 2 +- openstack/task_manager.py | 4 +--- openstack/tests/functional/cloud/test_devstack.py | 4 +--- openstack/tests/functional/cloud/test_domain.py | 4 +--- openstack/tests/functional/cloud/test_endpoints.py | 4 +--- openstack/tests/functional/cloud/test_flavor.py | 4 +--- openstack/tests/functional/cloud/test_floating_ip_pool.py | 2 +- openstack/tests/functional/cloud/test_groups.py | 4 +--- openstack/tests/functional/cloud/test_port.py | 4 +--- openstack/tests/functional/cloud/test_project.py | 4 +--- openstack/tests/functional/cloud/test_range_search.py | 5 +---- openstack/tests/functional/cloud/test_services.py | 4 +--- openstack/tests/unit/cloud/test_floating_ip_pool.py | 2 +- openstack/tests/unit/cloud/test_keypair.py | 2 +- openstack/tests/unit/cloud/test_network.py | 2 +- openstack/tests/unit/cloud/test_port.py | 2 +- openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py | 2 +- openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py | 2 +- .../tests/unit/cloud/test_qos_minimum_bandwidth_rule.py | 2 +- openstack/tests/unit/cloud/test_qos_policy.py | 2 +- openstack/tests/unit/cloud/test_qos_rule_type.py | 2 +- openstack/tests/unit/cloud/test_router.py | 2 +- openstack/tests/unit/cloud/test_subnet.py | 2 +- tox.ini | 5 +---- 25 files changed, 25 insertions(+), 51 deletions(-) diff --git a/openstack/cloud/_tasks.py b/openstack/cloud/_tasks.py index a41502f4f..ee65566c6 100644 --- a/openstack/cloud/_tasks.py +++ b/openstack/cloud/_tasks.py @@ -8,9 +8,7 @@ # # 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. -# +# 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. diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index fdda15152..f9be0ecaa 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -1,4 +1,4 @@ -# Licensed under the Apache License, Version 3.0 (the "License"); +# 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 # diff --git a/openstack/task_manager.py b/openstack/task_manager.py index 8a49ba4b2..0b05e7107 100644 --- a/openstack/task_manager.py +++ b/openstack/task_manager.py @@ -8,9 +8,7 @@ # # 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. -# +# 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. diff --git a/openstack/tests/functional/cloud/test_devstack.py b/openstack/tests/functional/cloud/test_devstack.py index 1a58b703d..633a76e23 100644 --- a/openstack/tests/functional/cloud/test_devstack.py +++ b/openstack/tests/functional/cloud/test_devstack.py @@ -8,9 +8,7 @@ # # 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. -# +# 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. diff --git a/openstack/tests/functional/cloud/test_domain.py b/openstack/tests/functional/cloud/test_domain.py index bce1dc491..81bd67a1c 100644 --- a/openstack/tests/functional/cloud/test_domain.py +++ b/openstack/tests/functional/cloud/test_domain.py @@ -6,9 +6,7 @@ # # 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. -# +# 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. diff --git a/openstack/tests/functional/cloud/test_endpoints.py b/openstack/tests/functional/cloud/test_endpoints.py index 8135a18da..a1ce61c21 100644 --- a/openstack/tests/functional/cloud/test_endpoints.py +++ b/openstack/tests/functional/cloud/test_endpoints.py @@ -8,9 +8,7 @@ # # 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. -# +# 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. diff --git a/openstack/tests/functional/cloud/test_flavor.py b/openstack/tests/functional/cloud/test_flavor.py index e4d4a7228..755bb9f63 100644 --- a/openstack/tests/functional/cloud/test_flavor.py +++ b/openstack/tests/functional/cloud/test_flavor.py @@ -8,9 +8,7 @@ # # 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. -# +# 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. diff --git a/openstack/tests/functional/cloud/test_floating_ip_pool.py b/openstack/tests/functional/cloud/test_floating_ip_pool.py index e8bb821ac..8c8fc478b 100644 --- a/openstack/tests/functional/cloud/test_floating_ip_pool.py +++ b/openstack/tests/functional/cloud/test_floating_ip_pool.py @@ -1,6 +1,6 @@ # Copyright (c) 2015 Hewlett-Packard Development Company, L.P. # -# Licensed under the Apache License, Version 2.0 (the 'License'); +# 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 # diff --git a/openstack/tests/functional/cloud/test_groups.py b/openstack/tests/functional/cloud/test_groups.py index cad046752..aa073d73a 100644 --- a/openstack/tests/functional/cloud/test_groups.py +++ b/openstack/tests/functional/cloud/test_groups.py @@ -6,9 +6,7 @@ # # 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. -# +# 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. diff --git a/openstack/tests/functional/cloud/test_port.py b/openstack/tests/functional/cloud/test_port.py index 2e373059c..fd358b853 100644 --- a/openstack/tests/functional/cloud/test_port.py +++ b/openstack/tests/functional/cloud/test_port.py @@ -8,9 +8,7 @@ # # 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. -# +# 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. diff --git a/openstack/tests/functional/cloud/test_project.py b/openstack/tests/functional/cloud/test_project.py index 6f6277052..72f07c9bc 100644 --- a/openstack/tests/functional/cloud/test_project.py +++ b/openstack/tests/functional/cloud/test_project.py @@ -8,9 +8,7 @@ # # 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. -# +# 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. diff --git a/openstack/tests/functional/cloud/test_range_search.py b/openstack/tests/functional/cloud/test_range_search.py index 545604b31..5edb96afb 100644 --- a/openstack/tests/functional/cloud/test_range_search.py +++ b/openstack/tests/functional/cloud/test_range_search.py @@ -8,13 +8,10 @@ # # 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. -# +# 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.cloud import exc from openstack.tests.functional.cloud import base diff --git a/openstack/tests/functional/cloud/test_services.py b/openstack/tests/functional/cloud/test_services.py index 8359e6598..570ae9e48 100644 --- a/openstack/tests/functional/cloud/test_services.py +++ b/openstack/tests/functional/cloud/test_services.py @@ -8,9 +8,7 @@ # # 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. -# +# 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. diff --git a/openstack/tests/unit/cloud/test_floating_ip_pool.py b/openstack/tests/unit/cloud/test_floating_ip_pool.py index 42e23ed4d..6d5dbd3d0 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_pool.py +++ b/openstack/tests/unit/cloud/test_floating_ip_pool.py @@ -1,6 +1,6 @@ # Copyright (c) 2015 Hewlett-Packard Development Company, L.P. # -# Licensed under the Apache License, Version 2.0 (the 'License'); +# 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 # diff --git a/openstack/tests/unit/cloud/test_keypair.py b/openstack/tests/unit/cloud/test_keypair.py index f7dd72e4d..7e84622d0 100644 --- a/openstack/tests/unit/cloud/test_keypair.py +++ b/openstack/tests/unit/cloud/test_keypair.py @@ -1,5 +1,5 @@ # -# Licensed under the Apache License, Version 2.0 (the 'License'); +# 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 # diff --git a/openstack/tests/unit/cloud/test_network.py b/openstack/tests/unit/cloud/test_network.py index 55bb950a6..6d061d49c 100644 --- a/openstack/tests/unit/cloud/test_network.py +++ b/openstack/tests/unit/cloud/test_network.py @@ -1,4 +1,4 @@ -# Licensed under the Apache License, Version 2.0 (the 'License'); +# 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 # diff --git a/openstack/tests/unit/cloud/test_port.py b/openstack/tests/unit/cloud/test_port.py index 962b516c8..e7092674b 100644 --- a/openstack/tests/unit/cloud/test_port.py +++ b/openstack/tests/unit/cloud/test_port.py @@ -1,6 +1,6 @@ # Copyright (c) 2015 Hewlett-Packard Development Company, L.P. # -# Licensed under the Apache License, Version 2.0 (the 'License'); +# 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 # diff --git a/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py b/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py index 3ea365bb7..5e9982c07 100644 --- a/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py @@ -1,7 +1,7 @@ # Copyright 2017 OVH SAS # All Rights Reserved. # -# Licensed under the Apache License, Version 2.0 (the 'License'); +# 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 # diff --git a/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py b/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py index 6ca1783f9..5e8d554b8 100644 --- a/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py +++ b/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py @@ -1,7 +1,7 @@ # Copyright 2017 OVH SAS # All Rights Reserved. # -# Licensed under the Apache License, Version 2.0 (the 'License'); +# 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 # diff --git a/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py b/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py index 4e6f54847..e919b4f5b 100644 --- a/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py +++ b/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py @@ -1,7 +1,7 @@ # Copyright 2017 OVH SAS # All Rights Reserved. # -# Licensed under the Apache License, Version 2.0 (the 'License'); +# 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 # diff --git a/openstack/tests/unit/cloud/test_qos_policy.py b/openstack/tests/unit/cloud/test_qos_policy.py index 89c454788..a6d9c43ae 100644 --- a/openstack/tests/unit/cloud/test_qos_policy.py +++ b/openstack/tests/unit/cloud/test_qos_policy.py @@ -1,7 +1,7 @@ # Copyright 2017 OVH SAS # All Rights Reserved. # -# Licensed under the Apache License, Version 2.0 (the 'License'); +# 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 # diff --git a/openstack/tests/unit/cloud/test_qos_rule_type.py b/openstack/tests/unit/cloud/test_qos_rule_type.py index 1c4ab3589..e289bba4a 100644 --- a/openstack/tests/unit/cloud/test_qos_rule_type.py +++ b/openstack/tests/unit/cloud/test_qos_rule_type.py @@ -1,7 +1,7 @@ # Copyright 2017 OVH SAS # All Rights Reserved. # -# Licensed under the Apache License, Version 2.0 (the 'License'); +# 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 # diff --git a/openstack/tests/unit/cloud/test_router.py b/openstack/tests/unit/cloud/test_router.py index 680b0e422..047e05f04 100644 --- a/openstack/tests/unit/cloud/test_router.py +++ b/openstack/tests/unit/cloud/test_router.py @@ -1,7 +1,7 @@ # Copyright 2017 OVH SAS # All Rights Reserved. # -# Licensed under the Apache License, Version 2.0 (the 'License'); +# 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 # diff --git a/openstack/tests/unit/cloud/test_subnet.py b/openstack/tests/unit/cloud/test_subnet.py index e8f05657b..2cafa056c 100644 --- a/openstack/tests/unit/cloud/test_subnet.py +++ b/openstack/tests/unit/cloud/test_subnet.py @@ -1,7 +1,7 @@ # Copyright 2017 OVH SAS # All Rights Reserved. # -# Licensed under the Apache License, Version 2.0 (the 'License'); +# 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 # diff --git a/tox.ini b/tox.ini index 36fcc8a4e..318535bc1 100644 --- a/tox.ini +++ b/tox.ini @@ -89,15 +89,12 @@ commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasen # The following are ignored on purpose. It's not super worth it to fix them. # However, if you feel strongly about it, patches will be accepted to fix them # if they fix ALL of the occurances of one and only one of them. -# H103 Is about the Apache license. It's strangely strict about the use of -# single vs double quotes in the license text. If someone decides to fix -# this, please be sure to preseve all copyright lines. # H306 Is about alphabetical imports - there's a lot to fix. # H4 Are about docstrings and there's just a huge pile of pre-existing issues. # W503 Is supposed to be off by default but in the latest pycodestyle isn't. # Also, both openstacksdk and Donald Knuth disagree with the rule. Line # breaks should occur before the binary operator for readability. -ignore = H103,H306,H4,W503 +ignore = H306,H4,W503 show-source = True exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build From 8447e82b29a999c650c61c9bc97b8450da56f3dd Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 10 May 2018 11:58:13 -0500 Subject: [PATCH 2062/3836] Bump default timeout values We're getting a fair number of timeouts sporadically. The VMs seem to just be running a bit slower... give ourselves a bit more headroom. Change-Id: I74ffd304d581e7449b8f850b2412ef1dce6effb1 --- openstack/tests/base.py | 4 ++-- openstack/tests/functional/cloud/test_compute.py | 2 +- tox.ini | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openstack/tests/base.py b/openstack/tests/base.py index 3db9a0dac..02b8e8965 100644 --- a/openstack/tests/base.py +++ b/openstack/tests/base.py @@ -37,13 +37,13 @@ class TestCase(base.BaseTestCase): def setUp(self): """Run before each test method to initialize test environment.""" # No openstacksdk unit tests should EVER run longer than a second. - # Set this to 3 by default just to give us some fudge. + # Set this to 5 by default just to give us some fudge. # Do this before super setUp so that we intercept the default value # in oslotest. TODO(mordred) Make the default timeout configurable # in oslotest. self.useFixture( fixtures.EnvironmentVariable( - 'OS_TEST_TIMEOUT', os.environ.get('OS_TEST_TIMEOUT', '3'))) + 'OS_TEST_TIMEOUT', os.environ.get('OS_TEST_TIMEOUT', '5'))) super(TestCase, self).setUp() diff --git a/openstack/tests/functional/cloud/test_compute.py b/openstack/tests/functional/cloud/test_compute.py index 8e12cd3b2..0e0f17986 100644 --- a/openstack/tests/functional/cloud/test_compute.py +++ b/openstack/tests/functional/cloud/test_compute.py @@ -30,7 +30,7 @@ class TestCompute(base.BaseFunctionalTestCase): def setUp(self): - # OS_TEST_TIMEOUT is 60 sec by default + # OS_TEST_TIMEOUT is 90 sec by default # but on a bad day, test_attach_detach_volume can take more time. self.TIMEOUT_SCALING_FACTOR = 1.5 diff --git a/tox.ini b/tox.ini index 27bed5ce3..90d029476 100644 --- a/tox.ini +++ b/tox.ini @@ -30,7 +30,7 @@ commands = stestr --test-path ./openstack/tests/examples run {posargs} basepython = {env:OPENSTACKSDK_TOX_PYTHON:python2} setenv = {[testenv]setenv} - OS_TEST_TIMEOUT=60 + OS_TEST_TIMEOUT=90 commands = stestr --test-path ./openstack/tests/functional run --serial {posargs} stestr slowest From c9daa593ac44fc2e5089b13127046b23906b2218 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 10 May 2018 09:46:24 -0500 Subject: [PATCH 2063/3836] Add ipaddress and futures to lower-constraints These were missing but that didn't get noticed before. Changing the requirements in the next patch triggers the error. Change-Id: I65605e14fc73e4eb223b7c31a98d4758ca280c9d --- lower-constraints.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lower-constraints.txt b/lower-constraints.txt index 895710c56..93d0079f1 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -6,6 +6,8 @@ dogpile.cache==0.6.2 extras==1.0.0 fixtures==3.0.0 future==0.16.0 +futures==3.0.0 +ipaddress==1.0.17 iso8601==0.1.11 jmespath==0.9.0 jsonpatch==1.16 From a31c17483560e803572c375835ac0b63da79f645 Mon Sep 17 00:00:00 2001 From: Sergey Skripnick Date: Thu, 10 May 2018 20:03:19 +0300 Subject: [PATCH 2064/3836] Decode additional heat files All file contents should be json serializable Change-Id: I7da20f1f68ecd73b83bed237d09314eaffe6d4f6 Closes-Bug: https://storyboard.openstack.org/#!/story/2002002 --- openstack/cloud/_heat/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/cloud/_heat/utils.py b/openstack/cloud/_heat/utils.py index c916c8b63..d977754ca 100644 --- a/openstack/cloud/_heat/utils.py +++ b/openstack/cloud/_heat/utils.py @@ -46,7 +46,7 @@ def read_url_content(url): if content: try: - content.decode('utf-8') + content = content.decode('utf-8') except ValueError: content = base64.encodestring(content) return content From 9c774d2959be93fa87ca0ed68f64d3242999718a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 26 Apr 2018 07:39:59 -0500 Subject: [PATCH 2065/3836] Defer all endpoint discovery to keystoneauth keystoneauth has support for service type aliases and version discovery. Stop doing it locally and just pass data to keystoneauth. Depends-On: https://review.openstack.org/567602 Change-Id: If60d02a8216ca0719fa628431515a0c3b37bf607 --- lower-constraints.txt | 2 +- .../block_storage/block_storage_service.py | 3 +- openstack/cloud/openstackcloud.py | 7 +- openstack/config/cloud_region.py | 91 +++++++------------ .../test_block_storage_service.py | 2 +- .../unit/block_storage/v2/test_snapshot.py | 2 +- .../tests/unit/block_storage/v2/test_type.py | 2 +- .../unit/block_storage/v2/test_volume.py | 2 +- .../tests/unit/config/test_cloud_config.py | 19 +--- .../notes/ksa-discovery-86a4ef00d85ea87f.yaml | 5 + requirements.txt | 2 +- 11 files changed, 52 insertions(+), 85 deletions(-) create mode 100644 releasenotes/notes/ksa-discovery-86a4ef00d85ea87f.yaml diff --git a/lower-constraints.txt b/lower-constraints.txt index 93d0079f1..618724c54 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -13,7 +13,7 @@ jmespath==0.9.0 jsonpatch==1.16 jsonpointer==1.13 jsonschema==2.6.0 -keystoneauth1==3.4.0 +keystoneauth1==3.6.0 linecache2==1.0.0 mock==2.0.0 mox3==0.20.0 diff --git a/openstack/block_storage/block_storage_service.py b/openstack/block_storage/block_storage_service.py index 7192544ff..7a8b99913 100644 --- a/openstack/block_storage/block_storage_service.py +++ b/openstack/block_storage/block_storage_service.py @@ -21,4 +21,5 @@ class BlockStorageService(service_filter.ServiceFilter): def __init__(self, version=None): """Create a block storage service.""" super(BlockStorageService, self).__init__( - service_type='volume', version=version, requires_project_id=True) + service_type='block-storage', + version=version, requires_project_id=True) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index fdda15152..0a47f6c59 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -603,9 +603,10 @@ def _orchestration_client(self): @property def _volume_client(self): - if 'volume' not in self._raw_clients: - self._raw_clients['volume'] = self._get_raw_client('volume') - return self._raw_clients['volume'] + if 'block-storage' not in self._raw_clients: + client = self._get_raw_client('block-storage') + self._raw_clients['block-storage'] = client + return self._raw_clients['block-storage'] def pprint(self, resource): """Wrapper aroud pprint that groks munch objects""" diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 5144ada42..6679450c4 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -13,7 +13,6 @@ # under the License. import copy -import math import warnings from keystoneauth1 import adapter @@ -199,26 +198,13 @@ def get_api_version(self, service_type): return self.config.get(key, None) def get_service_type(self, service_type): + # People requesting 'volume' are doing so because os-client-config + # let them. What they want is block-storage, not explicitly the + # v1 of cinder. If someone actually wants v1, they'll have api_version + # set to 1, in which case block-storage will still work properly. + if service_type == 'volume': + service_type = 'block-storage' key = _make_key('service_type', service_type) - # Cinder did an evil thing where they defined a second service - # type in the catalog. Of course, that's insane, so let's hide this - # atrocity from the as-yet-unsullied eyes of our users. - # Of course, if the user requests a volumev2, that structure should - # still work. - # What's even more amazing is that they did it AGAIN with cinder v3 - # And then I learned that mistral copied it. - # TODO(shade) This should get removed when we have os-service-types - # alias support landed in keystoneauth. - if service_type in ('volume', 'block-storage'): - vol_ver = self.get_api_version('volume') - if vol_ver and vol_ver.startswith('2'): - service_type = 'volumev2' - elif vol_ver and vol_ver.startswith('3'): - service_type = 'volumev3' - elif service_type == 'workflow': - wk_ver = self.get_api_version(service_type) - if wk_ver and wk_ver.startswith('2'): - service_type = 'workflowv2' return self.config.get(key, service_type) def get_service_name(self, service_type): @@ -326,7 +312,7 @@ def _get_version_request(self, service_key, version): def get_session_client( self, service_key, version=None, constructor=adapter.Adapter, **kwargs): - """Return a prepped requests adapter for a given service. + """Return a prepped keystoneauth Adapter for a given service. This is useful for making direct requests calls against a 'mounted' endpoint. That is, if you do: @@ -353,17 +339,6 @@ def get_session_client( endpoint_override=self.get_endpoint(service_key), **kwargs) - def _get_highest_endpoint(self, service_types, kwargs): - session = self.get_session() - for service_type in service_types: - kwargs['service_type'] = service_type - try: - # Return the highest version we find that matches - # the request - return session.get_endpoint(**kwargs) - except keystoneauth1.exceptions.catalog.EndpointNotFound: - pass - def get_session_endpoint( self, service_key, min_version=None, max_version=None): """Return the endpoint from config or the catalog. @@ -380,38 +355,38 @@ def get_session_endpoint( override_endpoint = self.get_endpoint(service_key) if override_endpoint: return override_endpoint - endpoint = None - kwargs = { - 'service_name': self.get_service_name(service_key), - 'region_name': self.region_name - } - kwargs['interface'] = self.get_interface(service_key) - if service_key == 'volume' and not self.get_api_version('volume'): - # If we don't have a configured cinder version, we can't know - # to request a different service_type - min_version = float(min_version or 1) - max_version = float(max_version or 3) - min_major = math.trunc(float(min_version)) - max_major = math.trunc(float(max_version)) - versions = range(int(max_major) + 1, int(min_major), -1) - service_types = [] - for version in versions: - if version == 1: - service_types.append('volume') - else: - service_types.append('volumev{v}'.format(v=version)) - else: - service_types = [self.get_service_type(service_key)] - endpoint = self._get_highest_endpoint(service_types, kwargs) + + service_name = self.get_service_name(service_key) + interface = self.get_interface(service_key) + session = self.get_session() + # Do this as kwargs because of os-client-config unittest mocking + version_kwargs = {} + if min_version: + version_kwargs['min_version'] = min_version + if max_version: + version_kwargs['max_version'] = max_version + try: + # Return the highest version we find that matches + # the request + endpoint = session.get_endpoint( + service_type=service_key, + region_name=self.region_name, + interface=interface, + service_name=service_name, + **version_kwargs + ) + except keystoneauth1.exceptions.catalog.EndpointNotFound: + endpoint = None if not endpoint: self.log.warning( "Keystone catalog entry not found (" "service_type=%s,service_name=%s" "interface=%s,region_name=%s)", service_key, - kwargs['service_name'], - kwargs['interface'], - kwargs['region_name']) + service_name, + interface, + self.region_name, + ) return endpoint def get_cache_expiration_time(self): diff --git a/openstack/tests/unit/block_storage/test_block_storage_service.py b/openstack/tests/unit/block_storage/test_block_storage_service.py index 9624f3b92..ca11ec47f 100644 --- a/openstack/tests/unit/block_storage/test_block_storage_service.py +++ b/openstack/tests/unit/block_storage/test_block_storage_service.py @@ -19,7 +19,7 @@ class TestBlockStorageService(base.TestCase): def test_service(self): sot = block_storage_service.BlockStorageService() - self.assertEqual("volume", sot.service_type) + self.assertEqual("block-storage", sot.service_type) self.assertEqual("public", sot.interface) self.assertIsNone(sot.region) self.assertIsNone(sot.service_name) diff --git a/openstack/tests/unit/block_storage/v2/test_snapshot.py b/openstack/tests/unit/block_storage/v2/test_snapshot.py index 1beb99efc..46a048d1a 100644 --- a/openstack/tests/unit/block_storage/v2/test_snapshot.py +++ b/openstack/tests/unit/block_storage/v2/test_snapshot.py @@ -45,7 +45,7 @@ def test_basic(self): self.assertEqual("snapshot", sot.resource_key) self.assertEqual("snapshots", sot.resources_key) self.assertEqual("/snapshots", sot.base_path) - self.assertEqual("volume", sot.service.service_type) + self.assertEqual("block-storage", sot.service.service_type) self.assertTrue(sot.allow_get) self.assertTrue(sot.allow_update) self.assertTrue(sot.allow_create) diff --git a/openstack/tests/unit/block_storage/v2/test_type.py b/openstack/tests/unit/block_storage/v2/test_type.py index ffb7a08bf..f23372d19 100644 --- a/openstack/tests/unit/block_storage/v2/test_type.py +++ b/openstack/tests/unit/block_storage/v2/test_type.py @@ -31,7 +31,7 @@ def test_basic(self): self.assertEqual("volume_type", sot.resource_key) self.assertEqual("volume_types", sot.resources_key) self.assertEqual("/types", sot.base_path) - self.assertEqual("volume", sot.service.service_type) + self.assertEqual("block-storage", sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_get) self.assertTrue(sot.allow_delete) diff --git a/openstack/tests/unit/block_storage/v2/test_volume.py b/openstack/tests/unit/block_storage/v2/test_volume.py index d08c83167..a90c84cbd 100644 --- a/openstack/tests/unit/block_storage/v2/test_volume.py +++ b/openstack/tests/unit/block_storage/v2/test_volume.py @@ -68,7 +68,7 @@ def test_basic(self): self.assertEqual("volume", sot.resource_key) self.assertEqual("volumes", sot.resources_key) self.assertEqual("/volumes", sot.base_path) - self.assertEqual("volume", sot.service.service_type) + self.assertEqual("block-storage", sot.service.service_type) self.assertTrue(sot.allow_get) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_update) diff --git a/openstack/tests/unit/config/test_cloud_config.py b/openstack/tests/unit/config/test_cloud_config.py index f0c86922a..57ac56a0f 100644 --- a/openstack/tests/unit/config/test_cloud_config.py +++ b/openstack/tests/unit/config/test_cloud_config.py @@ -143,32 +143,17 @@ def test_getters(self): self.assertEqual('mage', cc.get_service_type('image')) self.assertEqual('compute', cc.get_service_type('compute')) self.assertEqual('1', cc.get_api_version('volume')) - self.assertEqual('volume', cc.get_service_type('volume')) + self.assertEqual('block-storage', cc.get_service_type('volume')) self.assertEqual('http://compute.example.com', cc.get_endpoint('compute')) self.assertIsNone(cc.get_endpoint('image')) self.assertIsNone(cc.get_service_name('compute')) self.assertEqual('locks', cc.get_service_name('identity')) - def test_volume_override(self): - cc = cloud_region.CloudRegion("test1", "region-al", fake_services_dict) - cc.config['volume_api_version'] = '2' - self.assertEqual('volumev2', cc.get_service_type('volume')) - - def test_volume_override_v3(self): - cc = cloud_region.CloudRegion("test1", "region-al", fake_services_dict) - cc.config['volume_api_version'] = '3' - self.assertEqual('volumev3', cc.get_service_type('volume')) - - def test_workflow_override_v2(self): - cc = cloud_region.CloudRegion("test1", "region-al", fake_services_dict) - cc.config['workflow_api_version'] = '2' - self.assertEqual('workflowv2', cc.get_service_type('workflow')) - def test_no_override(self): """Test no override happens when defaults are not configured""" cc = cloud_region.CloudRegion("test1", "region-al", fake_services_dict) - self.assertEqual('volume', cc.get_service_type('volume')) + self.assertEqual('block-storage', cc.get_service_type('volume')) self.assertEqual('workflow', cc.get_service_type('workflow')) self.assertEqual('not-exist', cc.get_service_type('not-exist')) diff --git a/releasenotes/notes/ksa-discovery-86a4ef00d85ea87f.yaml b/releasenotes/notes/ksa-discovery-86a4ef00d85ea87f.yaml new file mode 100644 index 000000000..fa27dce9f --- /dev/null +++ b/releasenotes/notes/ksa-discovery-86a4ef00d85ea87f.yaml @@ -0,0 +1,5 @@ +--- +other: + - | + All endpoint discovery logic is now handled by keystoneauth. There should + be no behavior differences. diff --git a/requirements.txt b/requirements.txt index 7a4f9fede..950d96899 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ requestsexceptions>=1.2.0 # Apache-2.0 jsonpatch!=1.20,>=1.16 # BSD six>=1.10.0 # MIT os-service-types>=1.2.0 # Apache-2.0 -keystoneauth1>=3.4.0 # Apache-2.0 +keystoneauth1>=3.6.0 # Apache-2.0 deprecation>=1.0 # Apache-2.0 munch>=2.1.0 # MIT From 9242148cd9d6d6b46cf3c2794a11fa3f48804383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Thu, 10 May 2018 16:36:55 +0200 Subject: [PATCH 2066/3836] Remove default values of router's is_ha and is_distributed arguments Those attributes of Neutron's router are by default blocked by policy.json for regular users. So in such case even if router is ha or is distributed, regular user would get False values for both parameters. This patch changes it by removing default value set to False for those attributes. Now in such case both those parameters will be set to None which should be less confusing for users. Change-Id: I6908000bfc542478775a5f873e4660a9206ce38a Closes-Bug: #1689510 --- openstack/network/v2/router.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openstack/network/v2/router.py b/openstack/network/v2/router.py index b5f82e791..d2f950cca 100644 --- a/openstack/network/v2/router.py +++ b/openstack/network/v2/router.py @@ -59,11 +59,11 @@ class Router(resource.Resource, tag.TagMixin): #: or down ``False``. *Type: bool* is_admin_state_up = resource.Body('admin_state_up', type=bool) #: The distributed state of the router, which is distributed ``True`` - #: or not ``False``. *Type: bool* *Default: False* - is_distributed = resource.Body('distributed', type=bool, default=False) + #: or not ``False``. *Type: bool* + is_distributed = resource.Body('distributed', type=bool) #: The highly-available state of the router, which is highly available - #: ``True`` or not ``False``. *Type: bool* *Default: False* - is_ha = resource.Body('ha', type=bool, default=False) + #: ``True`` or not ``False``. *Type: bool* + is_ha = resource.Body('ha', type=bool) #: The router name. name = resource.Body('name') #: The ID of the project this router is associated with. From d1f0a6df8222ceeb645dc6bdbd6cd5007d97729f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 2 Mar 2018 14:20:00 -0500 Subject: [PATCH 2067/3836] Reenable osc-functional-devstack-tips and neutron-grenade They both work now. This reverts commit 31f9c172e5693af4709407e696fda63a15164818. Change-Id: I419626ef6400cd9d579150c63a71a53dbf4f5dfd --- .zuul.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 08c83cbf6..42d971415 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -227,10 +227,8 @@ - openstacksdk-functional-devstack-magnum: voting: false - openstacksdk-functional-devstack-python3 - - osc-functional-devstack-tips: - voting: false - - neutron-grenade: - voting: false + - osc-functional-devstack-tips + - neutron-grenade - openstack-tox-lower-constraints gate: jobs: @@ -239,4 +237,6 @@ sphinx_python: python3 - openstacksdk-functional-devstack - openstacksdk-functional-devstack-python3 + - osc-functional-devstack-tips + - neutron-grenade - openstack-tox-lower-constraints From 9a145dc22be36fa86aa9e5afe03c6accb5ea7ad4 Mon Sep 17 00:00:00 2001 From: Paul Belanger Date: Wed, 25 Apr 2018 11:10:57 -0400 Subject: [PATCH 2068/3836] Add get_volume_limits() support Add the ability for a user (nodepool) to get the volume limits of a cloud. Change-Id: Id568d3ad408dd8976211c0576c45e8c9471d1849 Depends-On: https://review.openstack.org/564279 Signed-off-by: Paul Belanger --- openstack/cloud/openstackcloud.py | 26 +++++++++++++++++++ .../tests/functional/cloud/test_limits.py | 15 +++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index fdda15152..2649918fe 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -5070,6 +5070,32 @@ def get_volumes(self, server, cache=True): volumes.append(volume) return volumes + def get_volume_limits(self, name_or_id=None): + """ Get volume limits for a project + + :param name_or_id: (optional) project name or ID to get limits for + if different from the current project + :raises: OpenStackCloudException if it's not a valid project + + :returns: Munch object with the limits + """ + params = {} + project_id = None + error_msg = "Failed to get limits" + if name_or_id: + + proj = self.get_project(name_or_id) + if not proj: + raise exc.OpenStackCloudException("project does not exist") + project_id = proj.id + params['tenant_id'] = project_id + error_msg = "{msg} for the project: {project} ".format( + msg=error_msg, project=name_or_id) + + data = self._volume_client.get('/limits', params=params) + limits = self._get_and_munchify('limits', data) + return limits + def get_volume_id(self, name_or_id): volume = self.get_volume(name_or_id) if volume: diff --git a/openstack/tests/functional/cloud/test_limits.py b/openstack/tests/functional/cloud/test_limits.py index b9d2e3955..539f0dc83 100644 --- a/openstack/tests/functional/cloud/test_limits.py +++ b/openstack/tests/functional/cloud/test_limits.py @@ -21,7 +21,7 @@ class TestUsage(base.BaseFunctionalTestCase): - def test_get_our_limits(self): + def test_get_our_compute_limits(self): '''Test quotas functionality''' limits = self.user_cloud.get_compute_limits() self.assertIsNotNone(limits) @@ -30,7 +30,7 @@ def test_get_our_limits(self): # Test normalize limits self.assertFalse(hasattr(limits, 'maxImageMeta')) - def test_get_other_limits(self): + def test_get_other_compute_limits(self): '''Test quotas functionality''' limits = self.operator_cloud.get_compute_limits('demo') self.assertIsNotNone(limits) @@ -38,3 +38,14 @@ def test_get_other_limits(self): # Test normalize limits self.assertFalse(hasattr(limits, 'maxImageMeta')) + + def test_get_our_volume_limits(self): + '''Test quotas functionality''' + limits = self.user_cloud.get_volume_limits() + self.assertIsNotNone(limits) + self.assertFalse(hasattr(limits, 'maxTotalVolumes')) + + def test_get_other_volume_limits(self): + '''Test quotas functionality''' + limits = self.operator_cloud.get_volume_limits('demo') + self.assertFalse(hasattr(limits, 'maxTotalVolumes')) From 5834d5e7cf1d8332f4621b0de5fdf40c93456dae Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 15 May 2018 16:38:36 -0500 Subject: [PATCH 2069/3836] Avoid globally modifying yaml library The heat utils set up the yaml parser to always return unicode strings, but are currently doing it in a way that infects all uses of the library. Make a subclass of the loader and run add_constructor on that so that the new constructor is confined to the specific loader. Change-Id: I49e97b5e2ae9b7862915ff83263718cf6cad32b8 Story: 2002040 --- openstack/cloud/_heat/template_format.py | 15 +++++++-------- .../notes/fix-yaml-load-3e6bd852afe549b4.yaml | 5 +++++ 2 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/fix-yaml-load-3e6bd852afe549b4.yaml diff --git a/openstack/cloud/_heat/template_format.py b/openstack/cloud/_heat/template_format.py index 4bb6098dc..cf37ee528 100644 --- a/openstack/cloud/_heat/template_format.py +++ b/openstack/cloud/_heat/template_format.py @@ -18,23 +18,22 @@ else: yaml_loader = yaml.SafeLoader -if hasattr(yaml, 'CSafeDumper'): - yaml_dumper = yaml.CSafeDumper -else: - yaml_dumper = yaml.SafeDumper + +class HeatYamlLoader(yaml_loader): + pass def _construct_yaml_str(self, node): # Override the default string handling function # to always return unicode objects return self.construct_scalar(node) -yaml_loader.add_constructor(u'tag:yaml.org,2002:str', _construct_yaml_str) +HeatYamlLoader.add_constructor(u'tag:yaml.org,2002:str', _construct_yaml_str) # Unquoted dates like 2013-05-23 in yaml files get loaded as objects of type # datetime.data which causes problems in API layer when being processed by # openstack.common.jsonutils. Therefore, make unicode string out of timestamps # until jsonutils can handle dates. -yaml_loader.add_constructor(u'tag:yaml.org,2002:timestamp', - _construct_yaml_str) +HeatYamlLoader.add_constructor( + u'tag:yaml.org,2002:timestamp', _construct_yaml_str) def parse(tmpl_str): @@ -49,7 +48,7 @@ def parse(tmpl_str): tpl = json.loads(tmpl_str) else: try: - tpl = yaml.load(tmpl_str, Loader=yaml_loader) + tpl = yaml.load(tmpl_str, Loader=HeatYamlLoader) except yaml.YAMLError: # NOTE(prazumovsky): we need to return more informative error for # user, so use SafeLoader, which return error message with template diff --git a/releasenotes/notes/fix-yaml-load-3e6bd852afe549b4.yaml b/releasenotes/notes/fix-yaml-load-3e6bd852afe549b4.yaml new file mode 100644 index 000000000..ac34188cc --- /dev/null +++ b/releasenotes/notes/fix-yaml-load-3e6bd852afe549b4.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixed an issue where importing openstacksdk changed the behavior of + ``yaml.load`` globally. From ab8f6026491accb27bda3fb1f65415bf94b0e671 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Fri, 18 May 2018 14:34:13 +0200 Subject: [PATCH 2070/3836] Enable bare metal unit tests No idea why they were disabled, they seem to pass correctly. Change-Id: Ie936a9e7593f09225d992b9a8de1988963c33b14 --- openstack/tests/unit/cloud/test_baremetal_node.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openstack/tests/unit/cloud/test_baremetal_node.py b/openstack/tests/unit/cloud/test_baremetal_node.py index b54a417be..fe842202b 100644 --- a/openstack/tests/unit/cloud/test_baremetal_node.py +++ b/openstack/tests/unit/cloud/test_baremetal_node.py @@ -30,8 +30,6 @@ class TestBaremetalNode(base.IronicTestCase): def setUp(self): super(TestBaremetalNode, self).setUp() - # TODO(shade) Fix this when we get ironic update to REST - self.skipTest("Ironic operations not supported yet") self.fake_baremetal_node = fakes.make_fake_machine( self.name, self.uuid) # TODO(TheJulia): Some tests below have fake ports, @@ -1601,8 +1599,6 @@ class TestUpdateMachinePatch(base.IronicTestCase): def setUp(self): super(TestUpdateMachinePatch, self).setUp() - # TODO(shade) Fix this when we get ironic update to REST - self.skipTest("Ironic operations not supported yet") self.fake_baremetal_node = fakes.make_fake_machine( self.name, self.uuid) From f19d58a728ffa5f6b982f5373bfdb1a503006edf Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Fri, 18 May 2018 12:11:06 +0200 Subject: [PATCH 2071/3836] baremetal: refuse to inspect associated machines Automatically inspecting "available" machines is a controversial feature, since it enables "stealing" a machine that Nova already picked for deployment. To reduce this probability, refuse to inspect nodes with instance_uuid set. Also finish the incomplete comment. Change-Id: I6cde6a6f9303f2a21efcfce979ffc0c1fea4bdb3 --- openstack/cloud/openstackcloud.py | 12 +++++++++-- .../tests/unit/cloud/test_baremetal_node.py | 20 +++++++++++++++++++ ...o-inspect-associated-563e272785bb6016.yaml | 5 +++++ 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/no-inspect-associated-563e272785bb6016.yaml diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 0c4557ca2..f2ea4a90d 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -8912,9 +8912,17 @@ def inspect_machine(self, name_or_id, wait=False, timeout=3600): raise exc.OpenStackCloudException( "Machine inspection failed to find: %s." % name_or_id) - # NOTE(TheJulia): If in available state, we can do this, however - # We need to to move the host back to m + # NOTE(TheJulia): If in available state, we can do this. However, + # we need to to move the machine back to manageable first. if "available" in machine['provision_state']: + if machine['instance_uuid']: + raise exc.OpenStackCloudException( + "Refusing to inspect available machine %(node)s " + "which is associated with an instance " + "(instance_uuid %(inst)s)" % + {'node': machine['uuid'], + 'inst': machine['instance_uuid']}) + return_to_available = True # NOTE(TheJulia): Changing available machine to managedable state # and due to state transitions we need to until that transition has diff --git a/openstack/tests/unit/cloud/test_baremetal_node.py b/openstack/tests/unit/cloud/test_baremetal_node.py index fe842202b..a433f3385 100644 --- a/openstack/tests/unit/cloud/test_baremetal_node.py +++ b/openstack/tests/unit/cloud/test_baremetal_node.py @@ -222,6 +222,26 @@ def test_inspect_machine_fail_active(self): self.assert_calls() + def test_inspect_machine_fail_associated(self): + self.fake_baremetal_node['provision_state'] = 'available' + self.fake_baremetal_node['instance_uuid'] = '1234' + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + ]) + self.assertRaisesRegex( + exc.OpenStackCloudException, + 'associated with an instance', + self.cloud.inspect_machine, + self.fake_baremetal_node['uuid'], + wait=True, + timeout=1) + + self.assert_calls() + def test_inspect_machine_failed(self): inspecting_node = self.fake_baremetal_node.copy() self.fake_baremetal_node['provision_state'] = 'inspect failed' diff --git a/releasenotes/notes/no-inspect-associated-563e272785bb6016.yaml b/releasenotes/notes/no-inspect-associated-563e272785bb6016.yaml new file mode 100644 index 000000000..c2faab6a9 --- /dev/null +++ b/releasenotes/notes/no-inspect-associated-563e272785bb6016.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Machine inspection is now blocked for machines associated with an instance. + This is to avoid "stealing" a machine from under a provisioner (e.g. Nova). From d12bda5d0312685470cb96c231c6ada9b4bf9c3e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 18 May 2018 09:15:49 -0500 Subject: [PATCH 2072/3836] Allow explicitly setting enable_snat to either value The original code did not allow someone to set enable_snat to True. Turns out on some clouds the default is the opposite. Change-Id: I3cf0832c9fc81a8c78e9ec4ef0a16c7e63fa8413 --- openstack/cloud/openstackcloud.py | 8 +++----- openstack/tests/unit/cloud/test_router.py | 21 +++++++++++++++++++-- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 0c4557ca2..ff6863f75 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -4086,11 +4086,9 @@ def _build_external_gateway_info(self, ext_gateway_net_id, enable_snat, info = {} if ext_gateway_net_id: info['network_id'] = ext_gateway_net_id - # Only send enable_snat if it is different from the Neutron - # default of True. Sending it can cause a policy violation error - # on some clouds. - if enable_snat is not None and not enable_snat: - info['enable_snat'] = False + # Only send enable_snat if it is explicitly set. + if enable_snat is not None: + info['enable_snat'] = enable_snat if ext_fixed_ips: info['external_fixed_ips'] = ext_fixed_ips if info: diff --git a/openstack/tests/unit/cloud/test_router.py b/openstack/tests/unit/cloud/test_router.py index 047e05f04..9ba183087 100644 --- a/openstack/tests/unit/cloud/test_router.py +++ b/openstack/tests/unit/cloud/test_router.py @@ -144,8 +144,8 @@ def test_create_router_with_availability_zone_hints(self): availability_zone_hints=['nova']) self.assert_calls() - def test_create_router_with_enable_snat_True(self): - """Do not send enable_snat when same as neutron default.""" + def test_create_router_without_enable_snat(self): + """Do not send enable_snat when not given.""" self.register_uris([ dict(method='POST', uri=self.get_mock_url( @@ -156,6 +156,23 @@ def test_create_router_with_enable_snat_True(self): 'name': self.router_name, 'admin_state_up': True}})) ]) + self.cloud.create_router( + name=self.router_name, admin_state_up=True) + self.assert_calls() + + def test_create_router_with_enable_snat_True(self): + """Send enable_snat when it is True.""" + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'routers.json']), + json={'router': self.mock_router_rep}, + validate=dict( + json={'router': { + 'name': self.router_name, + 'admin_state_up': True, + 'external_gateway_info': {'enable_snat': True}}})) + ]) self.cloud.create_router( name=self.router_name, admin_state_up=True, enable_snat=True) self.assert_calls() From 05a340d325cb788837217a5d41057711f48fc1df Mon Sep 17 00:00:00 2001 From: "sonu.kumar" Date: Tue, 22 May 2018 11:07:54 +0530 Subject: [PATCH 2073/3836] Fix filter style consistency for keystone assignment API This patch adds the '_' version of filters back along with '.' version of filters for role assignment list API to make it consistent with others APIs of keystone. Change-Id: If15f314ddfbdd0af6ee68529cea4c7161e7a8e0c Closes-Bug: #1755056 --- openstack/identity/v3/role_assignment.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openstack/identity/v3/role_assignment.py b/openstack/identity/v3/role_assignment.py index a5cc16326..b047c9ddc 100644 --- a/openstack/identity/v3/role_assignment.py +++ b/openstack/identity/v3/role_assignment.py @@ -24,7 +24,8 @@ class RoleAssignment(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'effective', 'include_names', 'include_subtree', + 'group_id', 'role_id', 'scope_domain_id', 'scope_project_id', + 'user_id', 'effective', 'include_names', 'include_subtree', role_id='role.id', user_id='user.id', group_id='group.id', scope_project_id='scope.project.id', scope_domain_id='scope.domain.id', scope_system='scope.system', From 1f4b9fa52a176e70d6cc438aaece17f54e4f9009 Mon Sep 17 00:00:00 2001 From: Daniel Speichert Date: Tue, 13 Feb 2018 14:43:42 -0500 Subject: [PATCH 2074/3836] Added few image properties to Image class os_admin_user, hw_quemu_guest_agent, os_require_quiesce Change-Id: Id7a4437cd7d1efc1ee98b41d0936e6bed56a7997 --- openstack/image/v2/image.py | 6 ++++++ openstack/tests/unit/image/v2/test_image.py | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index 228f24b6f..376d96748 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -212,6 +212,12 @@ class Image(resource.Resource): has_auto_disk_config = resource.Body('auto_disk_config', type=bool) #: The operating system installed on the image. os_type = resource.Body('os_type') + #: The operating system admin username. + os_admin_user = resource.Body('os_admin_user') + #: If true, QEMU guest agent will be exposed to the instance. + hw_qemu_guest_agent = resource.Body('hw_qemu_guest_agent', type=bool) + #: If true, require quiesce on snapshot via QEMU guest agent. + os_require_quiesce = resource.Body('os_require_quiesce', type=bool) def _action(self, session, action): """Call an action on an image ID.""" diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 20098b6c4..a898b096a 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -79,6 +79,9 @@ 'vmware_ostype': '48', 'auto_disk_config': True, 'os_type': '49', + 'os_admin_user': 'ubuntu', + 'hw_qemu_guest_agent': True, + 'os_require_quiesce': True, } @@ -175,6 +178,10 @@ def test_make_it(self): self.assertEqual(EXAMPLE['vmware_ostype'], sot.vmware_ostype) self.assertEqual(EXAMPLE['auto_disk_config'], sot.has_auto_disk_config) self.assertEqual(EXAMPLE['os_type'], sot.os_type) + self.assertEqual(EXAMPLE['os_admin_user'], sot.os_admin_user) + self.assertEqual(EXAMPLE['hw_qemu_guest_agent'], + sot.hw_qemu_guest_agent) + self.assertEqual(EXAMPLE['os_require_quiesce'], sot.os_require_quiesce) def test_deactivate(self): sot = image.Image(**EXAMPLE) From 4e8a1f6ae96f3c4cf83fea8b3faa5dfce2104349 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Fri, 25 May 2018 10:28:22 +0200 Subject: [PATCH 2075/3836] Several improvements to resource.wait_for_status * Clarify that interval and wait can be None (already supported by the underlying interval_timeout function). * Fix incorrect :returns: docstring. * Allow the caller to override which attribute to use. (ironic nodes have provision_state and power_state, not status). * Add some logging when looping (the underlying interval_timeout call does it, but just "Waiting %d seconds" is not enough in the context of a big caller application). * Use lower() on the initial check for status to be consistent with how it is treated inside the loop (correctly handling None, which is a possible state according to the CI). Change-Id: Icd58fdab1438b738d3ce6cd3162bb7445e9a2bf9 --- openstack/resource.py | 35 ++++++++++---- openstack/tests/unit/test_resource.py | 68 +++++++++++++++++++++++++-- 2 files changed, 91 insertions(+), 12 deletions(-) diff --git a/openstack/resource.py b/openstack/resource.py index 2801c33df..d7d49e73c 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -37,6 +37,7 @@ class that represent a remote resource. The attributes that from keystoneauth1 import adapter from requests import structures +from openstack import _log from openstack import exceptions from openstack import format from openstack import utils @@ -1047,21 +1048,31 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): "No %s found for %s" % (cls.__name__, name_or_id)) -def wait_for_status(session, resource, status, failures, interval, wait): +def _normalize_status(status): + if status is not None: + status = status.lower() + return status + + +def wait_for_status(session, resource, status, failures, interval=None, + wait=None, attribute='status'): """Wait for the resource to be in a particular status. :param session: The session to use for making this request. :type session: :class:`~keystoneauth1.adapter.Adapter` :param resource: The resource to wait on to reach the status. The resource - must have a status attribute. + must have a status attribute specified via ``attribute``. :type resource: :class:`~openstack.resource.Resource` :param status: Desired status of the resource. :param list failures: Statuses that would indicate the transition failed such as 'ERROR'. Defaults to ['ERROR']. :param interval: Number of seconds to wait between checks. + Set to ``None`` to use the default interval. :param wait: Maximum number of seconds to wait for transition. + Set to ``None`` to wait forever. + :param attribute: Name of the resource attribute that contains the status. - :return: Method returns self on success. + :return: The updated resource. :raises: :class:`~openstack.exceptions.ResourceTimeout` transition to status failed to occur in wait seconds. :raises: :class:`~openstack.exceptions.ResourceFailure` resource @@ -1069,7 +1080,10 @@ def wait_for_status(session, resource, status, failures, interval, wait): :raises: :class:`~AttributeError` if the resource does not have a status attribute """ - if resource.status == status: + log = _log.setup_logging(__name__) + + current_status = getattr(resource, attribute) + if _normalize_status(current_status) == status.lower(): return resource if failures is None: @@ -1090,13 +1104,18 @@ def wait_for_status(session, resource, status, failures, interval, wait): raise exceptions.ResourceFailure( "{name} went away while waiting for {status}".format( name=name, status=status)) - new_status = resource.status - if new_status.lower() == status.lower(): + + new_status = getattr(resource, attribute) + normalized_status = _normalize_status(new_status) + if normalized_status == status.lower(): return resource - if resource.status.lower() in failures: + elif normalized_status in failures: raise exceptions.ResourceFailure( "{name} transitioned to failure state {status}".format( - name=name, status=resource.status)) + name=name, status=new_status)) + + log.debug('Still waiting for resource %s to reach state %s, ' + 'current state is %s', name, status, new_status) def wait_for_delete(session, resource, interval, wait): diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 090677c52..41ed10959 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1771,7 +1771,7 @@ class TestWaitForStatus(base.TestCase): def test_immediate_status(self): status = "loling" - res = mock.Mock() + res = mock.Mock(spec=['id', 'status']) res.status = status result = resource.wait_for_status( @@ -1779,11 +1779,34 @@ def test_immediate_status(self): self.assertTrue(result, res) - def _resources_from_statuses(self, *statuses): + def test_immediate_status_case(self): + status = "LOLing" + res = mock.Mock(spec=['id', 'status']) + res.status = status + + result = resource.wait_for_status( + "session", res, 'lOling', "failures", "interval", "wait") + + self.assertTrue(result, res) + + def test_immediate_status_different_attribute(self): + status = "loling" + res = mock.Mock(spec=['id', 'mood']) + res.mood = status + + result = resource.wait_for_status( + "session", res, status, "failures", "interval", "wait", + attribute='mood') + + self.assertTrue(result, res) + + def _resources_from_statuses(self, *statuses, **kwargs): + attribute = kwargs.pop('attribute', 'status') + assert not kwargs, 'Unexpected keyword arguments: %s' % kwargs resources = [] for status in statuses: - res = mock.Mock() - res.status = status + res = mock.Mock(spec=['id', 'get', attribute]) + setattr(res, attribute, status) resources.append(res) for index, res in enumerate(resources[:-1]): res.get.return_value = resources[index + 1] @@ -1802,6 +1825,31 @@ def test_status_match(self): self.assertEqual(result, resources[-1]) + def test_status_match_with_none(self): + status = "loling" + + # apparently, None is a correct state in some cases + resources = self._resources_from_statuses( + None, "other", None, "another", status) + + result = resource.wait_for_status( + mock.Mock(), resources[0], status, None, 1, 5) + + self.assertEqual(result, resources[-1]) + + def test_status_match_different_attribute(self): + status = "loling" + + resources = self._resources_from_statuses( + "first", "other", "another", "another", status, + attribute='mood') + + result = resource.wait_for_status( + mock.Mock(), resources[0], status, None, 1, 5, + attribute='mood') + + self.assertEqual(result, resources[-1]) + def test_status_fails(self): failure = "crying" @@ -1812,6 +1860,18 @@ def test_status_fails(self): resource.wait_for_status, mock.Mock(), resources[0], "loling", [failure], 1, 5) + def test_status_fails_different_attribute(self): + failure = "crying" + + resources = self._resources_from_statuses("success", "other", failure, + attribute='mood') + + self.assertRaises( + exceptions.ResourceFailure, + resource.wait_for_status, + mock.Mock(), resources[0], "loling", [failure.upper()], 1, 5, + attribute='mood') + def test_timeout(self): status = "loling" res = mock.Mock() From 6cefc8cf4ec6a77cd6066e8357e6d573014301b0 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 25 May 2018 19:17:32 +0200 Subject: [PATCH 2076/3836] allow passing ansible variables to ansible tests Provide possibility to pass ansible variables through environment (i.e. ANSIBLE_VAR_FLAVOR for setting `flavor` variable in ansible playbooks) to the ansible functional tests. This can be used i.e. for running tests in other than devstack cloud, where images, flavors, networks are not matching devstack. This is an initial change in a series of changes to enable executing ansible tests in other clouds as a validation. Change-Id: I40bc16241345ec4f39481f46dc0ad3a3075201fe --- extras/run-ansible-tests.sh | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/extras/run-ansible-tests.sh b/extras/run-ansible-tests.sh index bda6007c9..c95c2395e 100755 --- a/extras/run-ansible-tests.sh +++ b/extras/run-ansible-tests.sh @@ -81,6 +81,15 @@ then tag_opt="--tags ${TAGS}" fi +# Loop through all ANSIBLE_VAR_ environment variables to allow passing the further +for var in $(env | grep -e '^ANSIBLE_VAR_'); do + VAR_NAME=${var%%=*} # split variable name from value + ANSIBLE_VAR_NAME=${VAR_NAME#ANSIBLE_VAR_} # cut ANSIBLE_VAR_ prefix from variable name + ANSIBLE_VAR_NAME=${ANSIBLE_VAR_NAME,,} # lowercase ansible variable + ANSIBLE_VAR_VALUE=${!VAR_NAME} # Get the variable value + ANSIBLE_VARS+="${ANSIBLE_VAR_NAME}=${ANSIBLE_VAR_VALUE} " # concat variables +done + # Until we have a module that lets us determine the image we want from # within a playbook, we have to find the image here and pass it in. # We use the openstack client instead of nova client since it can use clouds.yaml. @@ -91,4 +100,4 @@ then exit 1 fi -ansible-playbook -vvv ./openstack/tests/ansible/run.yml -e "cloud=${CLOUD} image=${IMAGE}" ${tag_opt} +ansible-playbook -vvv ./openstack/tests/ansible/run.yml -e "cloud=${CLOUD} image=${IMAGE} ${ANSIBLE_VARS}" ${tag_opt} From d650791860dc9693ed83a9b5601abed679660fee Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 28 May 2018 11:01:47 +0200 Subject: [PATCH 2077/3836] rename vars to defaults to allow overriding in ansible tests After introducing support for setting ansible variables through environment in ansible tests we need to further allow use of those. Set ANSIBLE_VAR_FLAVOR=m1.large to redefine flavor variable in ansible test Change-Id: I1f6b5b77ebd53ec0551b61851bf4fb618de0eb8f --- openstack/tests/ansible/roles/group/{vars => defaults}/main.yml | 0 openstack/tests/ansible/roles/image/{vars => defaults}/main.yml | 0 openstack/tests/ansible/roles/keypair/{vars => defaults}/main.yml | 0 .../ansible/roles/keystone_domain/{vars => defaults}/main.yml | 0 .../tests/ansible/roles/keystone_role/{vars => defaults}/main.yml | 0 openstack/tests/ansible/roles/network/{vars => defaults}/main.yml | 0 openstack/tests/ansible/roles/port/{vars => defaults}/main.yml | 0 openstack/tests/ansible/roles/router/{vars => defaults}/main.yml | 0 .../ansible/roles/security_group/{vars => defaults}/main.yml | 0 openstack/tests/ansible/roles/server/{vars => defaults}/main.yaml | 0 openstack/tests/ansible/roles/subnet/{vars => defaults}/main.yml | 0 11 files changed, 0 insertions(+), 0 deletions(-) rename openstack/tests/ansible/roles/group/{vars => defaults}/main.yml (100%) rename openstack/tests/ansible/roles/image/{vars => defaults}/main.yml (100%) rename openstack/tests/ansible/roles/keypair/{vars => defaults}/main.yml (100%) rename openstack/tests/ansible/roles/keystone_domain/{vars => defaults}/main.yml (100%) rename openstack/tests/ansible/roles/keystone_role/{vars => defaults}/main.yml (100%) rename openstack/tests/ansible/roles/network/{vars => defaults}/main.yml (100%) rename openstack/tests/ansible/roles/port/{vars => defaults}/main.yml (100%) rename openstack/tests/ansible/roles/router/{vars => defaults}/main.yml (100%) rename openstack/tests/ansible/roles/security_group/{vars => defaults}/main.yml (100%) rename openstack/tests/ansible/roles/server/{vars => defaults}/main.yaml (100%) rename openstack/tests/ansible/roles/subnet/{vars => defaults}/main.yml (100%) diff --git a/openstack/tests/ansible/roles/group/vars/main.yml b/openstack/tests/ansible/roles/group/defaults/main.yml similarity index 100% rename from openstack/tests/ansible/roles/group/vars/main.yml rename to openstack/tests/ansible/roles/group/defaults/main.yml diff --git a/openstack/tests/ansible/roles/image/vars/main.yml b/openstack/tests/ansible/roles/image/defaults/main.yml similarity index 100% rename from openstack/tests/ansible/roles/image/vars/main.yml rename to openstack/tests/ansible/roles/image/defaults/main.yml diff --git a/openstack/tests/ansible/roles/keypair/vars/main.yml b/openstack/tests/ansible/roles/keypair/defaults/main.yml similarity index 100% rename from openstack/tests/ansible/roles/keypair/vars/main.yml rename to openstack/tests/ansible/roles/keypair/defaults/main.yml diff --git a/openstack/tests/ansible/roles/keystone_domain/vars/main.yml b/openstack/tests/ansible/roles/keystone_domain/defaults/main.yml similarity index 100% rename from openstack/tests/ansible/roles/keystone_domain/vars/main.yml rename to openstack/tests/ansible/roles/keystone_domain/defaults/main.yml diff --git a/openstack/tests/ansible/roles/keystone_role/vars/main.yml b/openstack/tests/ansible/roles/keystone_role/defaults/main.yml similarity index 100% rename from openstack/tests/ansible/roles/keystone_role/vars/main.yml rename to openstack/tests/ansible/roles/keystone_role/defaults/main.yml diff --git a/openstack/tests/ansible/roles/network/vars/main.yml b/openstack/tests/ansible/roles/network/defaults/main.yml similarity index 100% rename from openstack/tests/ansible/roles/network/vars/main.yml rename to openstack/tests/ansible/roles/network/defaults/main.yml diff --git a/openstack/tests/ansible/roles/port/vars/main.yml b/openstack/tests/ansible/roles/port/defaults/main.yml similarity index 100% rename from openstack/tests/ansible/roles/port/vars/main.yml rename to openstack/tests/ansible/roles/port/defaults/main.yml diff --git a/openstack/tests/ansible/roles/router/vars/main.yml b/openstack/tests/ansible/roles/router/defaults/main.yml similarity index 100% rename from openstack/tests/ansible/roles/router/vars/main.yml rename to openstack/tests/ansible/roles/router/defaults/main.yml diff --git a/openstack/tests/ansible/roles/security_group/vars/main.yml b/openstack/tests/ansible/roles/security_group/defaults/main.yml similarity index 100% rename from openstack/tests/ansible/roles/security_group/vars/main.yml rename to openstack/tests/ansible/roles/security_group/defaults/main.yml diff --git a/openstack/tests/ansible/roles/server/vars/main.yaml b/openstack/tests/ansible/roles/server/defaults/main.yaml similarity index 100% rename from openstack/tests/ansible/roles/server/vars/main.yaml rename to openstack/tests/ansible/roles/server/defaults/main.yaml diff --git a/openstack/tests/ansible/roles/subnet/vars/main.yml b/openstack/tests/ansible/roles/subnet/defaults/main.yml similarity index 100% rename from openstack/tests/ansible/roles/subnet/vars/main.yml rename to openstack/tests/ansible/roles/subnet/defaults/main.yml From a3f4570d3b8255f09fd676710e7a435e42b46060 Mon Sep 17 00:00:00 2001 From: Hongbin Lu Date: Mon, 15 Jan 2018 22:25:16 +0000 Subject: [PATCH 2078/3836] Add 'port_details' to Floating IP Change-Id: I4e4f2be5d787b961698dc48126ba5f5605900359 Partial-Bug: #1723026 --- openstack/network/v2/floating_ip.py | 5 +++++ .../functional/network/v2/test_floating_ip.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/openstack/network/v2/floating_ip.py b/openstack/network/v2/floating_ip.py index 5e06cf689..96422be54 100644 --- a/openstack/network/v2/floating_ip.py +++ b/openstack/network/v2/floating_ip.py @@ -60,6 +60,11 @@ class FloatingIP(resource.Resource, tag.TagMixin): name = floating_ip_address #: The ID of the network associated with the floating IP. floating_network_id = resource.Body('floating_network_id') + #: Read-only. The details of the port that this floating IP associates + #: with. Present if ``fip-port-details`` extension is loaded. + #: *Type: dict with keys: name, network_id, mac_address, admin_state_up, + #: status, device_id, device_owner* + port_details = resource.Body('port_details', type=dict) #: The port ID. port_id = resource.Body('port_id') #: The ID of the QoS policy attached to the floating IP. diff --git a/openstack/tests/functional/network/v2/test_floating_ip.py b/openstack/tests/functional/network/v2/test_floating_ip.py index 34799c566..89e614166 100644 --- a/openstack/tests/functional/network/v2/test_floating_ip.py +++ b/openstack/tests/functional/network/v2/test_floating_ip.py @@ -69,6 +69,7 @@ def setUp(self): prt = self.conn.network.create_port(network_id=self.INT_NET_ID) assert isinstance(prt, port.Port) self.PORT_ID = prt.id + self.PORT = prt # Create Floating IP. fip = self.conn.network.create_ip( floating_network_id=self.EXT_NET_ID, @@ -134,6 +135,7 @@ def test_find_available_ip(self): sot = self.conn.network.find_available_ip() self.assertIsNotNone(sot.id) self.assertIsNone(sot.port_id) + self.assertIsNone(sot.port_details) def test_get(self): sot = self.conn.network.get_ip(self.FIP.id) @@ -142,6 +144,7 @@ def test_get(self): self.assertEqual(self.FIP.floating_ip_address, sot.floating_ip_address) self.assertEqual(self.FIP.fixed_ip_address, sot.fixed_ip_address) self.assertEqual(self.FIP.port_id, sot.port_id) + self.assertEqual(self.FIP.port_details, sot.port_details) self.assertEqual(self.FIP.router_id, sot.router_id) self.assertEqual(self.DNS_DOMAIN, sot.dns_domain) self.assertEqual(self.DNS_NAME, sot.dns_name) @@ -153,6 +156,7 @@ def test_list(self): def test_update(self): sot = self.conn.network.update_ip(self.FIP.id, port_id=self.PORT_ID) self.assertEqual(self.PORT_ID, sot.port_id) + self._assert_port_details(self.PORT, sot.port_details) self.assertEqual(self.FIP.id, sot.id) def test_set_tags(self): @@ -166,3 +170,13 @@ def test_set_tags(self): self.conn.network.set_tags(sot, []) sot = self.conn.network.get_ip(self.FIP.id) self.assertEqual([], sot.tags) + + def _assert_port_details(self, port, port_details): + self.assertEqual(port.name, port_details['name']) + self.assertEqual(port.network_id, port_details['network_id']) + self.assertEqual(port.mac_address, port_details['mac_address']) + self.assertEqual(port.is_admin_state_up, + port_details['admin_state_up']) + self.assertEqual(port.status, port_details['status']) + self.assertEqual(port.device_id, port_details['device_id']) + self.assertEqual(port.device_owner, port_details['device_owner']) From 83d939dd4323e84cea698a22f03178f78aee9cd7 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 27 May 2018 08:42:36 -0500 Subject: [PATCH 2079/3836] Add ansible functional tests ansible now directly depends on and uses openstacksdk rather than shade. Make sure we don't land patches that break ansible. Remove the installation of ansible from run-ansible-tests as it's specified in the tox.ini file already, and we'll get it installed via tox-siblings in the gate. Depends-On: https://review.openstack.org/570956 Change-Id: I35103b8c75100725caf7a8fd264b81d402bf1973 --- .zuul.yaml | 32 ++++++++++++++++++++++++++++++++ extras/run-ansible-tests.sh | 3 --- tox.ini | 3 +++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 42109016b..fc698b904 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -199,6 +199,36 @@ OPENSTACKSDK_HAS_SWIFT: 0 OPENSTACKSDK_HAS_MAGNUM: 1 +- job: + name: openstacksdk-ansible-functional-devstack + parent: openstacksdk-functional-devstack + description: | + Run openstacksdk ansible functional tests against a master devstack + using released version of ansible. + vars: + tox_envlist: ansible + +- job: + name: openstacksdk-ansible-devel-functional-devstack + parent: openstacksdk-ansible-functional-devstack + description: | + Run openstacksdk ansible functional tests against a master devstack + using git devel branch version of ansible. + branches: ^(devel|master)$ + required-projects: + - name: github.com/ansible/ansible + override-checkout: devel + - name: openstack/openstacksdk + override-checkout: master + - name: openstack-dev/devstack + override-checkout: master + vars: + # test-matrix grabs branch from the zuul branch setting. If the job + # is triggered by ansible, that branch will be devel which doesn't + # make sense to devstack. Override so that we run the right thing. + test_matrix_branch: master + tox_install_siblings: true + - project-template: name: openstacksdk-functional-tips check: @@ -224,6 +254,8 @@ - build-openstack-sphinx-docs: vars: sphinx_python: python3 + - openstacksdk-ansible-devel-functional-devstack: + voting: false - openstacksdk-functional-devstack - openstacksdk-functional-devstack-magnum: voting: false diff --git a/extras/run-ansible-tests.sh b/extras/run-ansible-tests.sh index bda6007c9..f5a9c1679 100755 --- a/extras/run-ansible-tests.sh +++ b/extras/run-ansible-tests.sh @@ -69,9 +69,6 @@ then git clone --recursive https://github.com/ansible/ansible.git ${ENVDIR}/ansible fi source $ENVDIR/ansible/hacking/env-setup -else - echo "Installing Ansible from pip" - pip install ansible fi # Run the shade Ansible tests diff --git a/tox.ini b/tox.ini index 62979df74..b58c8ddd4 100644 --- a/tox.ini +++ b/tox.ini @@ -71,6 +71,9 @@ commands = # Need to pass some env vars for the Ansible playbooks basepython = {env:OPENSTACKSDK_TOX_PYTHON:python2} passenv = HOME USER +deps = + {[testenv]deps} + ansible commands = {toxinidir}/extras/run-ansible-tests.sh -e {envdir} {posargs} [testenv:docs] From d82c8f930606f51f671eb4c142174500f8efb085 Mon Sep 17 00:00:00 2001 From: yanpuqing Date: Tue, 29 May 2018 23:39:55 -0400 Subject: [PATCH 2080/3836] Modify the error message when unsetting gateway and setting FIP When assiging FIP without router between the external and removing an external gateway information from a using router, openstackclient just return http code. The patch shows the correct failure message when these errors displays. Change-Id: I3546bd6b63d10db2c0ae2d643ee1aa61cd83cf29 Closes-Bug: 1760405 Closes-Bug: 1757063 --- openstack/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/exceptions.py b/openstack/exceptions.py index 141129a42..619c0b491 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -90,7 +90,7 @@ def __unicode__(self): # 'Error', then someone has set a more informative error message # and we should use it. If it is 'Error', then we should construct a # better message from the information we do have. - if not self.url or self.message != 'Error': + if not self.url or self.message == 'Error': return super(HttpException, self).__str__() if self.url: remote_error = "{source} Error for url: {url}".format( From 765e7dd3e0adc1252253ff2fa0e2f9f3221aeb41 Mon Sep 17 00:00:00 2001 From: yanpuqing Date: Thu, 31 May 2018 23:38:37 -0400 Subject: [PATCH 2081/3836] Modify the unhelpful error message when delete network Openstackclient returns a dump of the network object which is unhelpful when Network delete error. The patch deletes unhelpful object message and exposes real error messages. story: 2001973 Change-Id: Id3f20214d034848d36f9b6c8c9c73ef66ad26cdd --- openstack/proxy.py | 10 +--------- openstack/tests/unit/test_proxy.py | 4 ++-- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/openstack/proxy.py b/openstack/proxy.py index ac140816a..33f3505e0 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -138,15 +138,7 @@ def _delete(self, resource_type, value, ignore_missing=True, **attrs): res = self._get_resource(resource_type, value, **attrs) try: - rv = res.delete( - self, - error_message=( - "Unable to delete {resource_type} for {value}".format( - resource_type=resource_type.__name__, - value=value, - ) - ) - ) + rv = res.delete(self) except exceptions.NotFoundException: if ignore_missing: return None diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index 120ee77a8..152c9c105 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -175,11 +175,11 @@ def setUp(self): def test_delete(self): self.sot._delete(DeleteableResource, self.res) - self.res.delete.assert_called_with(self.sot, error_message=mock.ANY) + self.res.delete.assert_called_with(self.sot) self.sot._delete(DeleteableResource, self.fake_id) DeleteableResource.new.assert_called_with(id=self.fake_id) - self.res.delete.assert_called_with(self.sot, error_message=mock.ANY) + self.res.delete.assert_called_with(self.sot) # Delete generally doesn't return anything, so we will normally # swallow any return from within a service's proxy, but make sure From fe98aef5181a5b1f9eb78c4b50265f8d5c69ea31 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 1 Jun 2018 15:21:19 +0200 Subject: [PATCH 2082/3836] Add ansible functional tests on stable-2.6 Ansible uses openstacksdk as of 2.6, so we should also run tests against the stable-2.6 branch. Change-Id: I260b9f0b7acbda3a6c9fccc1bef27906c1f298b4 --- .zuul.yaml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/.zuul.yaml b/.zuul.yaml index fc698b904..c4d8b7cc7 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -208,6 +208,15 @@ vars: tox_envlist: ansible +- job: + name: openstacksdk-ansible-functional-devstack + parent: openstacksdk-functional-devstack + description: | + Run openstacksdk ansible functional tests against a master devstack + using released version of ansible. + vars: + tox_envlist: ansible + - job: name: openstacksdk-ansible-devel-functional-devstack parent: openstacksdk-ansible-functional-devstack @@ -229,6 +238,27 @@ test_matrix_branch: master tox_install_siblings: true +- job: + name: openstacksdk-ansible-stable-2.6-functional-devstack + parent: openstacksdk-ansible-functional-devstack + description: | + Run openstacksdk ansible functional tests against a master devstack + using git stable-2.6 branch version of ansible. + branches: ^(stable-2.6|master)$ + required-projects: + - name: github.com/ansible/ansible + override-checkout: stable-2.6 + - name: openstack/openstacksdk + override-checkout: master + - name: openstack-dev/devstack + override-checkout: master + vars: + # test-matrix grabs branch from the zuul branch setting. If the job + # is triggered by ansible, that branch will be devel which doesn't + # make sense to devstack. Override so that we run the right thing. + test_matrix_branch: master + tox_install_siblings: true + - project-template: name: openstacksdk-functional-tips check: @@ -256,6 +286,8 @@ sphinx_python: python3 - openstacksdk-ansible-devel-functional-devstack: voting: false + - openstacksdk-ansible-stable-2.6-functional-devstack: + voting: false - openstacksdk-functional-devstack - openstacksdk-functional-devstack-magnum: voting: false From d3df639353a9c9e33c72632627c76b9b885dfe23 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Wed, 6 Jun 2018 15:27:00 -0400 Subject: [PATCH 2083/3836] fix tox python3 overrides We want to default to running all tox environments under python 3, so set the basepython value in each environment. We do not want to specify a minor version number, because we do not want to have to update the file every time we upgrade python. We do not want to set the override once in testenv, because that breaks the more specific versions used in default environments like py35 and py36. Change-Id: I3edccb24a3f714be8fcc32851342363e44f7ea2f Signed-off-by: Doug Hellmann --- tox.ini | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tox.ini b/tox.ini index b58c8ddd4..3f1666631 100644 --- a/tox.ini +++ b/tox.ini @@ -23,6 +23,7 @@ commands = stestr run {posargs} stestr slowest [testenv:examples] +basepython = python3 commands = stestr --test-path ./openstack/tests/examples run {posargs} stestr slowest @@ -35,6 +36,7 @@ commands = stestr --test-path ./openstack/tests/functional run --serial {posargs stestr slowest [testenv:pep8] +basepython = python3 usedevelop = False skip_install = True deps = @@ -49,15 +51,18 @@ commands = flake8 [testenv:venv] +basepython = python3 commands = {posargs} [testenv:debug] +basepython = python3 whitelist_externals = find commands = find . -type f -name "*.pyc" -delete oslo_debug_helper {posargs} [testenv:cover] +basepython = python3 setenv = {[testenv]setenv} PYTHON=coverage run --source openstack --parallel-mode @@ -77,6 +82,7 @@ deps = commands = {toxinidir}/extras/run-ansible-tests.sh -e {envdir} {posargs} [testenv:docs] +basepython = python3 deps = -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} -r{toxinidir}/requirements.txt @@ -84,6 +90,7 @@ deps = commands = sphinx-build -W -d doc/build/doctrees -b html doc/source/ doc/build/html [testenv:releasenotes] +basepython = python3 usedevelop = False skip_install = True commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html From dade89b51f89e8fe6ef071e3f6f7670a207a0081 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 8 May 2018 10:34:35 -0500 Subject: [PATCH 2084/3836] Honor service-type aliases in config The correct config names are based on the official service type, such as block_storage_api_version or OS_BLOCK_STORAGE_API_VERSION. But, there are folks who have been using volume_api_version or OS_VOLUME_API_VERSION. Support looking for config values based on service-type aliases. The official type will win. Change-Id: I13c77ad4fce14a491b6a82f4c7e332d284d14e0d --- openstack/config/cloud_region.py | 60 ++++++++++++++----- openstack/config/defaults.json | 1 + openstack/config/schema.json | 10 +++- openstack/config/vendor-schema.json | 5 ++ openstack/config/vendors/betacloud.json | 2 +- openstack/config/vendors/bluebox.json | 2 +- openstack/config/vendors/catalyst.json | 2 +- openstack/config/vendors/citycloud.json | 2 +- openstack/config/vendors/entercloudsuite.json | 2 +- openstack/config/vendors/fuga.json | 2 +- openstack/config/vendors/ibmcloud.json | 2 +- openstack/config/vendors/rackspace.json | 2 +- openstack/config/vendors/switchengines.json | 2 +- openstack/config/vendors/ultimum.json | 2 +- openstack/config/vendors/unitedstack.json | 2 +- .../tests/unit/config/test_cloud_config.py | 10 ++++ .../config-aliases-0f6297eafd05c07c.yaml | 7 +++ 17 files changed, 88 insertions(+), 27 deletions(-) create mode 100644 releasenotes/notes/config-aliases-0f6297eafd05c07c.yaml diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 6679450c4..99d6c4718 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -18,6 +18,7 @@ from keystoneauth1 import adapter import keystoneauth1.exceptions.catalog from keystoneauth1 import session as ks_session +import os_service_types import requestsexceptions from six.moves import urllib @@ -103,6 +104,8 @@ def __init__(self, name=None, region_name=None, config=None, self._cache_arguments = cache_arguments self._password_callback = password_callback + self._service_type_manager = os_service_types.ServiceTypes() + def __getattr__(self, key): """Return arbitrary attributes.""" @@ -188,33 +191,62 @@ def get_services(self): def get_auth_args(self): return self.config.get('auth', {}) + def _get_config( + self, key, service_type, + default=None, + fallback_to_unprefixed=False): + '''Get a config value for a service_type. + + Finds the config value for a key, looking first for it prefixed by + the given service_type, then by any known aliases of that service_type. + Finally, if fallback_to_unprefixed is True, a value will be looked + for without a prefix to support the config values where a global + default makes sense. + + For instance, ``_get_config('example', 'block-storage', True)`` would + first look for ``block_storage_example``, then ``volumev3_example``, + ``volumev2_example`` and ``volume_example``. If no value was found, it + would look for ``example``. + + If none of that works, it returns the value in ``default``. + ''' + for st in self._service_type_manager.get_all_types(service_type): + value = self.config.get(_make_key(key, st)) + if value is not None: + return value + if fallback_to_unprefixed: + return self.config.get(key) + return default + def get_interface(self, service_type=None): - key = _make_key('interface', service_type) - interface = self.config.get('interface') - return self.config.get(key, interface) + return self._get_config( + 'interface', service_type, fallback_to_unprefixed=True) def get_api_version(self, service_type): - key = _make_key('api_version', service_type) - return self.config.get(key, None) + return self._get_config('api_version', service_type) def get_service_type(self, service_type): # People requesting 'volume' are doing so because os-client-config # let them. What they want is block-storage, not explicitly the # v1 of cinder. If someone actually wants v1, they'll have api_version # set to 1, in which case block-storage will still work properly. - if service_type == 'volume': - service_type = 'block-storage' - key = _make_key('service_type', service_type) - return self.config.get(key, service_type) + # Use service-types-manager to grab the official type name. _get_config + # will still look for config by alias, but starting with the official + # type will get us things in the right order. + if self._service_type_manager.is_known(service_type): + service_type = self._service_type_manager.get_service_type( + service_type) + return self._get_config( + 'service_type', service_type, default=service_type) def get_service_name(self, service_type): - key = _make_key('service_name', service_type) - return self.config.get(key, None) + return self._get_config('service_name', service_type) def get_endpoint(self, service_type): - key = _make_key('endpoint_override', service_type) - old_key = _make_key('endpoint', service_type) - return self.config.get(key, self.config.get(old_key, None)) + value = self._get_config('endpoint_override', service_type) + if not value: + value = self._get_config('endpoint', service_type) + return value @property def prefer_ipv6(self): diff --git a/openstack/config/defaults.json b/openstack/config/defaults.json index 2a195c426..7c6296594 100644 --- a/openstack/config/defaults.json +++ b/openstack/config/defaults.json @@ -2,6 +2,7 @@ "application_catalog_api_version": "1", "auth_type": "password", "baremetal_api_version": "1", + "block_storage_api_version": "2", "container_api_version": "1", "container_infra_api_version": "1", "compute_api_version": "2", diff --git a/openstack/config/schema.json b/openstack/config/schema.json index 8110d58e9..1e5b6c66b 100644 --- a/openstack/config/schema.json +++ b/openstack/config/schema.json @@ -51,6 +51,12 @@ "default": "1", "type": "string" }, + "block_storage_api_version": { + "name": "Block Storage API Version", + "description": "Block Storage API Version", + "default": "2", + "type": "string" + }, "compute_api_version": { "name": "Compute API Version", "description": "Compute API Version", @@ -103,6 +109,7 @@ "required": [ "auth_type", "baremetal_api_version", + "block_storage_api_version", "compute_api_version", "database_api_version", "disable_vendor_agent", @@ -115,7 +122,6 @@ "interface", "network_api_version", "object_store_api_version", - "secgroup_source", - "volume_api_version" + "secgroup_source" ] } diff --git a/openstack/config/vendor-schema.json b/openstack/config/vendor-schema.json index 8193a19ba..5847ae545 100644 --- a/openstack/config/vendor-schema.json +++ b/openstack/config/vendor-schema.json @@ -168,6 +168,11 @@ "description": "Baremetal API Service Type", "type": "string" }, + "block_storage_api_version": { + "name": "Block Storage API Version", + "description": "Block Storage API Version", + "type": "string" + }, "compute_api_version": { "name": "Compute API Version", "description": "Compute API Version", diff --git a/openstack/config/vendors/betacloud.json b/openstack/config/vendors/betacloud.json index 2387b09c2..348d7a9e1 100644 --- a/openstack/config/vendors/betacloud.json +++ b/openstack/config/vendors/betacloud.json @@ -9,6 +9,6 @@ ], "identity_api_version": "3", "image_format": "raw", - "volume_api_version": "3" + "block_storage_api_version": "3" } } diff --git a/openstack/config/vendors/bluebox.json b/openstack/config/vendors/bluebox.json index 647c8429f..d50e5acd5 100644 --- a/openstack/config/vendors/bluebox.json +++ b/openstack/config/vendors/bluebox.json @@ -1,7 +1,7 @@ { "name": "bluebox", "profile": { - "volume_api_version": "1", + "block_storage_api_version": "1", "region_name": "RegionOne" } } diff --git a/openstack/config/vendors/catalyst.json b/openstack/config/vendors/catalyst.json index 3ad75075b..511d3b768 100644 --- a/openstack/config/vendors/catalyst.json +++ b/openstack/config/vendors/catalyst.json @@ -9,7 +9,7 @@ "nz_wlg_2" ], "image_api_version": "1", - "volume_api_version": "1", + "block_storage_api_version": "1", "image_format": "raw" } } diff --git a/openstack/config/vendors/citycloud.json b/openstack/config/vendors/citycloud.json index c9ac335c8..8a6318795 100644 --- a/openstack/config/vendors/citycloud.json +++ b/openstack/config/vendors/citycloud.json @@ -13,7 +13,7 @@ "Kna1" ], "requires_floating_ip": true, - "volume_api_version": "1", + "block_storage_api_version": "1", "identity_api_version": "3" } } diff --git a/openstack/config/vendors/entercloudsuite.json b/openstack/config/vendors/entercloudsuite.json index c58c478f0..711db59d8 100644 --- a/openstack/config/vendors/entercloudsuite.json +++ b/openstack/config/vendors/entercloudsuite.json @@ -6,7 +6,7 @@ }, "identity_api_version": "3", "image_api_version": "1", - "volume_api_version": "1", + "block_storage_api_version": "1", "regions": [ "it-mil1", "nl-ams1", diff --git a/openstack/config/vendors/fuga.json b/openstack/config/vendors/fuga.json index 388500b1b..c73198e7f 100644 --- a/openstack/config/vendors/fuga.json +++ b/openstack/config/vendors/fuga.json @@ -10,6 +10,6 @@ "cystack" ], "identity_api_version": "3", - "volume_api_version": "3" + "block_storage_api_version": "3" } } diff --git a/openstack/config/vendors/ibmcloud.json b/openstack/config/vendors/ibmcloud.json index 90962c60e..dc0a18220 100644 --- a/openstack/config/vendors/ibmcloud.json +++ b/openstack/config/vendors/ibmcloud.json @@ -4,7 +4,7 @@ "auth": { "auth_url": "https://identity.open.softlayer.com" }, - "volume_api_version": "2", + "block_storage_api_version": "2", "identity_api_version": "3", "regions": [ "london" diff --git a/openstack/config/vendors/rackspace.json b/openstack/config/vendors/rackspace.json index 6a4590f67..fd1cf89ca 100644 --- a/openstack/config/vendors/rackspace.json +++ b/openstack/config/vendors/rackspace.json @@ -19,7 +19,7 @@ "floating_ip_source": "None", "secgroup_source": "None", "requires_floating_ip": false, - "volume_api_version": "1", + "block_storage_api_version": "1", "disable_vendor_agent": { "vm_mode": "hvm", "xenapi_use_agent": false diff --git a/openstack/config/vendors/switchengines.json b/openstack/config/vendors/switchengines.json index 46f632515..0ec23c2a9 100644 --- a/openstack/config/vendors/switchengines.json +++ b/openstack/config/vendors/switchengines.json @@ -8,7 +8,7 @@ "LS", "ZH" ], - "volume_api_version": "1", + "block_storage_api_version": "1", "image_api_use_tasks": true, "image_format": "raw" } diff --git a/openstack/config/vendors/ultimum.json b/openstack/config/vendors/ultimum.json index 4bfd088cd..c24f4b530 100644 --- a/openstack/config/vendors/ultimum.json +++ b/openstack/config/vendors/ultimum.json @@ -5,7 +5,7 @@ "auth_url": "https://console.ultimum-cloud.com:5000/" }, "identity_api_version": "3", - "volume_api_version": "1", + "block_storage_api_version": "1", "region-name": "RegionOne" } } diff --git a/openstack/config/vendors/unitedstack.json b/openstack/config/vendors/unitedstack.json index ac8be117f..319707d7b 100644 --- a/openstack/config/vendors/unitedstack.json +++ b/openstack/config/vendors/unitedstack.json @@ -8,7 +8,7 @@ "bj1", "gd1" ], - "volume_api_version": "1", + "block_storage_api_version": "1", "identity_api_version": "3", "image_format": "raw", "floating_ip_source": "None" diff --git a/openstack/tests/unit/config/test_cloud_config.py b/openstack/tests/unit/config/test_cloud_config.py index 57ac56a0f..37bd2b6f0 100644 --- a/openstack/tests/unit/config/test_cloud_config.py +++ b/openstack/tests/unit/config/test_cloud_config.py @@ -150,6 +150,16 @@ def test_getters(self): self.assertIsNone(cc.get_service_name('compute')) self.assertEqual('locks', cc.get_service_name('identity')) + def test_aliases(self): + services_dict = fake_services_dict.copy() + services_dict['volume_api_version'] = 12 + services_dict['alarming_service_name'] = 'aodh' + cc = cloud_region.CloudRegion("test1", "region-al", services_dict) + self.assertEqual('12', cc.get_api_version('volume')) + self.assertEqual('12', cc.get_api_version('block-storage')) + self.assertEqual('aodh', cc.get_service_name('alarm')) + self.assertEqual('aodh', cc.get_service_name('alarming')) + def test_no_override(self): """Test no override happens when defaults are not configured""" cc = cloud_region.CloudRegion("test1", "region-al", fake_services_dict) diff --git a/releasenotes/notes/config-aliases-0f6297eafd05c07c.yaml b/releasenotes/notes/config-aliases-0f6297eafd05c07c.yaml new file mode 100644 index 000000000..d398c2ad9 --- /dev/null +++ b/releasenotes/notes/config-aliases-0f6297eafd05c07c.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Config values now support service-type aliases. The correct config names + are based on the official service type, such as + ``block_storage_api_version``, but with this change, legacy aliases such + as ``volume_api_version`` are also supported. From 264c802ad86225503697a1b860192e31f11411fa Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 30 Mar 2018 08:49:07 -0500 Subject: [PATCH 2085/3836] Pass default_microversion to adapter constructor Based on discussion with the API-SIG, we determined that if someone sets *_api_version or OS_*_API_VERSION to a value that looks like a microversion, we should pass it to default_microversion in addition to using it for major version discovery. Update the docs to reflect the microversion story (and to remove shade references) Also, throw an error on missing support for default microversion We've given the user the ability to set a default microversion. However, that microversion might not be available on the cloud in question. Throw an error in such cases. Depends-On: https://review.openstack.org/568640 Change-Id: Iad7194dc9e933660c0d0df9130c51d505dda50cb --- doc/source/user/microversions.rst | 85 +++++++++----- lower-constraints.txt | 2 +- openstack/config/_util.py | 2 + openstack/config/cloud_region.py | 68 ++++++++++- openstack/tests/unit/base.py | 19 +++ .../tests/unit/fixtures/compute-version.json | 30 +++++ openstack/tests/unit/test_microversions.py | 108 ++++++++++++++++++ ...default-microversion-b2401727cb591002.yaml | 7 ++ requirements.txt | 2 +- 9 files changed, 290 insertions(+), 33 deletions(-) create mode 100644 openstack/tests/unit/fixtures/compute-version.json create mode 100644 openstack/tests/unit/test_microversions.py create mode 100644 releasenotes/notes/default-microversion-b2401727cb591002.yaml diff --git a/doc/source/user/microversions.rst b/doc/source/user/microversions.rst index ce821dace..1b60f9df1 100644 --- a/doc/source/user/microversions.rst +++ b/doc/source/user/microversions.rst @@ -2,50 +2,74 @@ Microversions ============= -As shade rolls out support for consuming microversions, it will do so on a -call by call basis as needed. Just like with major versions, shade should have -logic to handle each microversion for a given REST call it makes, with the -following rules in mind: +As openstacksdk rolls out support for consuming microversions, it will do so +on a call by call basis as needed. Just like with major versions, openstacksdk +should have logic to handle each microversion for a given REST call it makes, +with the following rules in mind: -* If an activity shade performs can be done differently or more efficiently - with a new microversion, the support should be added to openstack.cloud. +* If an activity openstack performs can be done differently or more efficiently + with a new microversion, the support should be added to openstack.cloud and + to the appropriate Proxy class. -* shade should always attempt to use the latest microversion it is aware of - for a given call, unless a microversion removes important data. +* openstacksdk should always attempt to use the latest microversion it is aware + of for a given call, unless a microversion removes important data. -* Microversion selection should under no circumstances be exposed to the user, - except in the case of missing feature error messages. +* Microversion selection should under no circumstances be exposed to the user + in python API calls in the Resource layer or the openstack.cloud layer. + +* Microversion selection is exposed to the user in the REST layer via the + ``microversion`` argument to each REST call. + +* A user of the REST layer may set the default microversion by setting + ``{service_type}_default_microversion`` in clouds.yaml or + ``OS_{service_type|upper}_DEFAULT_MICROVERSION`` environment variable. + +.. note:: + + Setting the default microversion in any circumstance other than when using + the REST layer is highly discouraged. Both of the higher layers in + openstacksdk provide data normalization as well as logic about which REST + call to make. Setting the default microversion could change the behavior + of the service in question in such a way that openstacksdk does not + understand. If there is a feature of a service that needs a microversion + and it is not already transparently exposed in openstacksdk, please file + a bug. * If a feature is only exposed for a given microversion and cannot be simulated - for older clouds without that microversion, it is ok to add it to shade but + for older clouds without that microversion, it is ok to add it, but a clear error message should be given to the user that the given feature is - not available on their cloud. (A message such as "This cloud only supports + not available on their cloud. (A message such as "This cloud supports a maximum microversion of XXX for service YYY and this feature only exists on clouds with microversion ZZZ. Please contact your cloud provider for information about when this feature might be available") -* When adding a feature to shade that only exists behind a new microversion, +* When adding a feature that only exists behind a new microversion, every effort should be made to figure out how to provide the same functionality if at all possible, even if doing so is inefficient. If an inefficient workaround is employed, a warning should be provided to the user. (the user's workaround to skip the inefficient behavior would be to - stop using that shade API call) - -* If shade is aware of logic for more than one microversion, it should always - attempt to use the latest version available for the service for that call. + stop using that openstacksdk API call) An example of this is the nova + "get me a network" feature. The logic of "get me a network" can be done + client-side, albeit less efficiently. Adding support for the + "get me a network" feature via nova microversion should also add support for + doing the client-side workaround. -* Objects returned from shade should always go through normalization and thus - should always conform to shade's documented data model and should never look - different to the shade user regardless of the microversion used for the REST +* If openstacksdk is aware of logic for more than one microversion, it should + always attempt to use the latest version available for the service for that call. +* Objects returned from openstacksdk should always go through normalization and + thus should always conform to openstacksdk's documented data model. The + objects should never look different to the user regardless of the + microversion used for the REST call. + * If a microversion adds new fields to an object, those fields should be - added to shade's data model contract for that object and the data should - either be filled in by performing additional REST calls if the data is + added to openstacksdk's data model contract for that object and the data + should either be filled in by performing additional REST calls if the data is available that way, or the field should have a default value of None which the user can be expected to test for when attempting to use the new value. -* If a microversion removes fields from an object that are part of shade's +* If a microversion removes fields from an object that are part of the existing data model contract, care should be taken to not use the new microversion for that call unless forced to by lack of availablity of the old microversion on the cloud in question. In the case where an old @@ -54,21 +78,22 @@ following rules in mind: field and document for the user that on some clouds the value may not exist. * If a microversion removes a field and the outcome is particularly intractable - and impossible to work around without fundamentally breaking shade's users, + and impossible to work around without fundamentally breaking users, an issue should be raised with the service team in question. Hopefully a resolution can be found during the period while clouds still have the old microversion. -* As new calls or objects are added to shade, it is important to check in with +* As new calls or objects are added, it is important to check in with the service team in question on the expected stability of the object. If there are known changes expected in the future, even if they may be a few - years off, shade should take care to not add committments to its data model - for those fields/features. It is ok for shade to not have something. + years off, openstacksdk should take care to not add committments to its data + model for those fields/features. It is ok for openstacksdk to not have + something. ..note:: - shade does not currently have any sort of "experimental" opt-in API that - would allow a shade to expose things to a user that may not be supportable - under shade's normal compatibility contract. If a conflict arises in the + openstacksdk does not currently have any sort of "experimental" opt-in API + that would allow exposing things to a user that may not be supportable + under the normal compatibility contract. If a conflict arises in the future where there is a strong desire for a feature but also a lack of certainty about its stability over time, an experimental API may want to be explored ... but concrete use cases should arise before such a thing diff --git a/lower-constraints.txt b/lower-constraints.txt index 618724c54..1082ca74e 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -13,7 +13,7 @@ jmespath==0.9.0 jsonpatch==1.16 jsonpointer==1.13 jsonschema==2.6.0 -keystoneauth1==3.6.0 +keystoneauth1==3.7.0 linecache2==1.0.0 mock==2.0.0 mox3==0.20.0 diff --git a/openstack/config/_util.py b/openstack/config/_util.py index 26f2428b9..cdd6737e8 100644 --- a/openstack/config/_util.py +++ b/openstack/config/_util.py @@ -51,7 +51,9 @@ def __init__( version=None, min_api_version=None, max_api_version=None, + default_microversion=None, ): self.version = version self.min_api_version = min_api_version self.max_api_version = max_api_version + self.default_microversion = default_microversion diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 99d6c4718..57b694b3b 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -16,6 +16,7 @@ import warnings from keystoneauth1 import adapter +from keystoneauth1 import discover import keystoneauth1.exceptions.catalog from keystoneauth1 import session as ks_session import os_service_types @@ -225,6 +226,9 @@ def get_interface(self, service_type=None): def get_api_version(self, service_type): return self._get_config('api_version', service_type) + def get_default_microversion(self, service_type): + return self._get_config('default_microversion', service_type) + def get_service_type(self, service_type): # People requesting 'volume' are doing so because os-client-config # let them. What they want is block-storage, not explicitly the @@ -324,6 +328,9 @@ def _get_version_request(self, service_key, version): If version is not set and we don't have a configured version, default to latest. + + If version is set, contains a '.', and default_microversion is not + set, also pass it as a default microversion. """ version_request = _util.VersionRequest() if version == 'latest': @@ -339,6 +346,22 @@ def _get_version_request(self, service_key, version): version_request.max_api_version = 'latest' else: version_request.version = version + + default_microversion = self.get_default_microversion(service_key) + if not default_microversion and version and '.' in version: + # Some services historically had a .0 in their normal api version. + # Neutron springs to mind with version "2.0". If a user has "2.0" + # set in a variable or config file just because history, we don't + # need to send any microversion headers. + if version.split('.')[1] != "0": + default_microversion = version + # If we're inferring a microversion, don't pass the whole + # string in as api_version, since that tells keystoneauth + # we're looking for a major api version. + version_request.version = version[0] + + version_request.default_microversion = default_microversion + return version_request def get_session_client( @@ -359,7 +382,7 @@ def get_session_client( """ version_request = self._get_version_request(service_key, version) - return constructor( + client = constructor( session=self.get_session(), service_type=self.get_service_type(service_key), service_name=self.get_service_name(service_key), @@ -369,7 +392,50 @@ def get_session_client( min_version=version_request.min_api_version, max_version=version_request.max_api_version, endpoint_override=self.get_endpoint(service_key), + default_microversion=version_request.default_microversion, **kwargs) + if version_request.default_microversion: + default_microversion = version_request.default_microversion + info = client.get_endpoint_data() + if not discover.version_between( + info.min_microversion, + info.max_microversion, + default_microversion + ): + if self.get_default_microversion(service_key): + raise exceptions.ConfigException( + "A default microversion for service {service_type} of" + " {default_microversion} was requested, but the cloud" + " only supports a minimum of {min_microversion} and" + " a maximum of {max_microversion}.".format( + service_type=service_key, + default_microversion=default_microversion, + min_microversion=discover.version_to_string( + info.min_microversion), + max_microversion=discover.version_to_string( + info.max_microversion))) + else: + raise exceptions.ConfigException( + "A default microversion for service {service_type} of" + " {default_microversion} was requested, but the cloud" + " only supports a maximum of" + " only supports a minimum of {min_microversion} and" + " a maximum of {max_microversion}. The default" + " microversion was set because a microversion" + " formatted version string, '{api_version}', was" + " passed for the api_version of the service. If it" + " was not intended to set a default microversion" + " please remove anything other than an integer major" + " version from the version setting for" + " the service.".format( + service_type=service_key, + api_version=self.get_api_version(service_key), + default_microversion=default_microversion, + min_microversion=discover.version_to_string( + info.min_microversion), + max_microversion=discover.version_to_string( + info.max_microversion))) + return client def get_session_endpoint( self, service_key, min_version=None, max_version=None): diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 85b69998d..f81d97db6 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -457,6 +457,17 @@ def get_glance_discovery_mock_dict( status_code=300, text=open(discovery_fixture, 'r').read()) + def get_nova_discovery_mock_dict( + self, + compute_version_json='compute-version.json', + compute_discovery_url='https://compute.example.com/v2.1/'): + discovery_fixture = os.path.join( + self.fixtures_directory, compute_version_json) + return dict( + method='GET', + uri=compute_discovery_url, + text=open(discovery_fixture, 'r').read()) + def get_designate_discovery_mock_dict(self): discovery_fixture = os.path.join( self.fixtures_directory, "dns.json") @@ -469,6 +480,14 @@ def get_ironic_discovery_mock_dict(self): return dict(method='GET', uri="https://bare-metal.example.com/", text=open(discovery_fixture, 'r').read()) + def use_compute_discovery( + self, compute_version_json='compute-version.json', + compute_discovery_url='https://compute.example.com/v2.1/'): + self.__do_register_uris([ + self.get_nova_discovery_mock_dict( + compute_version_json, compute_discovery_url), + ]) + def use_glance( self, image_version_json='image-version.json', image_discovery_url='https://image.example.com/'): diff --git a/openstack/tests/unit/fixtures/compute-version.json b/openstack/tests/unit/fixtures/compute-version.json new file mode 100644 index 000000000..2e9ba6ae7 --- /dev/null +++ b/openstack/tests/unit/fixtures/compute-version.json @@ -0,0 +1,30 @@ +{ + "versions": [ + { + "status": "SUPPORTED", + "updated": "2011-01-21T11:33:21Z", + "links": [ + { + "href": "https://compute.example.com/v2/", + "rel": "self" + } + ], + "min_version": "", + "version": "", + "id": "v2.0" + }, + { + "status": "CURRENT", + "updated": "2013-07-23T11:33:21Z", + "links": [ + { + "href": "https://compute.example.com/v2.1/", + "rel": "self" + } + ], + "min_version": "2.10", + "version": "2.53", + "id": "v2.1" + } + ] +} diff --git a/openstack/tests/unit/test_microversions.py b/openstack/tests/unit/test_microversions.py new file mode 100644 index 000000000..97e3d99f8 --- /dev/null +++ b/openstack/tests/unit/test_microversions.py @@ -0,0 +1,108 @@ +# 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.tests import fakes +from openstack.tests.unit import base + + +class TestMicroversions(base.TestCase): + + def setUp(self): + super(TestMicroversions, self).setUp() + self.use_compute_discovery() + + def test_get_bad_inferred_max_microversion(self): + + self.cloud.config.config['compute_api_version'] = '2.61' + + self.assertRaises( + exceptions.ConfigException, + self.cloud.get_server, 'doesNotExist', + ) + + self.assert_calls() + + def test_get_bad_default_max_microversion(self): + + self.cloud.config.config['compute_default_microversion'] = '2.61' + + self.assertRaises( + exceptions.ConfigException, + self.cloud.get_server, 'doesNotExist', + ) + + self.assert_calls() + + def test_get_bad_inferred_min_microversion(self): + + self.cloud.config.config['compute_api_version'] = '2.7' + + self.assertRaises( + exceptions.ConfigException, + self.cloud.get_server, 'doesNotExist', + ) + + self.assert_calls() + + def test_get_bad_default_min_microversion(self): + + self.cloud.config.config['compute_default_microversion'] = '2.7' + + self.assertRaises( + exceptions.ConfigException, + self.cloud.get_server, 'doesNotExist', + ) + + self.assert_calls() + + def test_inferred_default_microversion(self): + + self.cloud.config.config['compute_api_version'] = '2.42' + + server1 = fakes.make_fake_server('123', 'mickey') + server2 = fakes.make_fake_server('345', 'mouse') + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + request_headers={'OpenStack-API-Version': 'compute 2.42'}, + json={'servers': [server1, server2]}), + ]) + + r = self.cloud.get_server('mickey', bare=True) + self.assertIsNotNone(r) + self.assertEqual(server1['name'], r['name']) + + self.assert_calls() + + def test_default_microversion(self): + + self.cloud.config.config['compute_default_microversion'] = '2.42' + + server1 = fakes.make_fake_server('123', 'mickey') + server2 = fakes.make_fake_server('345', 'mouse') + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + request_headers={'OpenStack-API-Version': 'compute 2.42'}, + json={'servers': [server1, server2]}), + ]) + + r = self.cloud.get_server('mickey', bare=True) + self.assertIsNotNone(r) + self.assertEqual(server1['name'], r['name']) + + self.assert_calls() diff --git a/releasenotes/notes/default-microversion-b2401727cb591002.yaml b/releasenotes/notes/default-microversion-b2401727cb591002.yaml new file mode 100644 index 000000000..3ef8ad957 --- /dev/null +++ b/releasenotes/notes/default-microversion-b2401727cb591002.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Versions set in config via ``*_api_version`` or ``OS_*_API_VERSION`` + that have a ``.`` in them will be also passed as the default microversion + to the Adapter constructor. An additional config option, + ``*_default_microversion`` has been added to support being more explicit. diff --git a/requirements.txt b/requirements.txt index 950d96899..4f679ba3c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ requestsexceptions>=1.2.0 # Apache-2.0 jsonpatch!=1.20,>=1.16 # BSD six>=1.10.0 # MIT os-service-types>=1.2.0 # Apache-2.0 -keystoneauth1>=3.6.0 # Apache-2.0 +keystoneauth1>=3.7.0 # Apache-2.0 deprecation>=1.0 # Apache-2.0 munch>=2.1.0 # MIT From 94174ff723b62742b5c61eb47451db7baf6e20d0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 31 Mar 2018 07:25:57 -0500 Subject: [PATCH 2086/3836] Rename service_key to service_type Back in the early days there wasn't clear terminology so os-client-config used the term 'service_key' internally to mean "official service-type". But we have service-types-authority now and it's understood that a service has a service-type. Use the word in the code so that things aren't confusing. Change-Id: Ia6484e3ee555c578e264c1e47c1817bbfe4d7925 --- openstack/config/cloud_region.py | 44 +++++++++++++++----------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 57b694b3b..d2950a4e9 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -317,7 +317,7 @@ def get_service_catalog(self): """Helper method to grab the service catalog.""" return self._auth.get_access(self.get_session()).service_catalog - def _get_version_request(self, service_key, version): + def _get_version_request(self, service_type, version): """Translate OCC version args to those needed by ksa adapter. If no version is requested explicitly and we have a configured version, @@ -338,16 +338,16 @@ def _get_version_request(self, service_key, version): return version_request if not version: - version = self.get_api_version(service_key) + version = self.get_api_version(service_type) # Octavia doens't have a version discovery document. Hard-code an # exception to this logic for now. - if not version and service_key not in ('load-balancer',): + if not version and service_type not in ('load-balancer',): version_request.max_api_version = 'latest' else: version_request.version = version - default_microversion = self.get_default_microversion(service_key) + default_microversion = self.get_default_microversion(service_type) if not default_microversion and version and '.' in version: # Some services historically had a .0 in their normal api version. # Neutron springs to mind with version "2.0". If a user has "2.0" @@ -365,7 +365,7 @@ def _get_version_request(self, service_key, version): return version_request def get_session_client( - self, service_key, version=None, constructor=adapter.Adapter, + self, service_type, version=None, constructor=adapter.Adapter, **kwargs): """Return a prepped keystoneauth Adapter for a given service. @@ -380,18 +380,18 @@ def get_session_client( and it will work like you think. """ - version_request = self._get_version_request(service_key, version) + version_request = self._get_version_request(service_type, version) client = constructor( session=self.get_session(), - service_type=self.get_service_type(service_key), - service_name=self.get_service_name(service_key), - interface=self.get_interface(service_key), + service_type=self.get_service_type(service_type), + service_name=self.get_service_name(service_type), + interface=self.get_interface(service_type), region_name=self.region_name, version=version_request.version, min_version=version_request.min_api_version, max_version=version_request.max_api_version, - endpoint_override=self.get_endpoint(service_key), + endpoint_override=self.get_endpoint(service_type), default_microversion=version_request.default_microversion, **kwargs) if version_request.default_microversion: @@ -402,13 +402,13 @@ def get_session_client( info.max_microversion, default_microversion ): - if self.get_default_microversion(service_key): + if self.get_default_microversion(service_type): raise exceptions.ConfigException( "A default microversion for service {service_type} of" " {default_microversion} was requested, but the cloud" " only supports a minimum of {min_microversion} and" " a maximum of {max_microversion}.".format( - service_type=service_key, + service_type=service_type, default_microversion=default_microversion, min_microversion=discover.version_to_string( info.min_microversion), @@ -428,8 +428,8 @@ def get_session_client( " please remove anything other than an integer major" " version from the version setting for" " the service.".format( - service_type=service_key, - api_version=self.get_api_version(service_key), + service_type=service_type, + api_version=self.get_api_version(service_type), default_microversion=default_microversion, min_microversion=discover.version_to_string( info.min_microversion), @@ -438,24 +438,22 @@ def get_session_client( return client def get_session_endpoint( - self, service_key, min_version=None, max_version=None): + self, service_type, min_version=None, max_version=None): """Return the endpoint from config or the catalog. If a configuration lists an explicit endpoint for a service, return that. Otherwise, fetch the service catalog from the keystone session and return the appropriate endpoint. - :param service_key: Generic key for service, such as 'compute' or - 'network' - + :param service_type: Official service type of service """ - override_endpoint = self.get_endpoint(service_key) + override_endpoint = self.get_endpoint(service_type) if override_endpoint: return override_endpoint - service_name = self.get_service_name(service_key) - interface = self.get_interface(service_key) + service_name = self.get_service_name(service_type) + interface = self.get_interface(service_type) session = self.get_session() # Do this as kwargs because of os-client-config unittest mocking version_kwargs = {} @@ -467,7 +465,7 @@ def get_session_endpoint( # Return the highest version we find that matches # the request endpoint = session.get_endpoint( - service_type=service_key, + service_type=service_type, region_name=self.region_name, interface=interface, service_name=service_name, @@ -480,7 +478,7 @@ def get_session_endpoint( "Keystone catalog entry not found (" "service_type=%s,service_name=%s" "interface=%s,region_name=%s)", - service_key, + service_type, service_name, interface, self.region_name, From b6b270f58ec40491f2c0e6465f9d255714e7225f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 11 Jun 2018 11:46:05 -0500 Subject: [PATCH 2087/3836] Throw an error on conflicting microversion config If we pull an implied default microversion from the api_version, but an explicit default_microversion has been given we have no idea what the user wants us to do. Throw an error. Change-Id: Id095df356a563bec90931b70f6d2c9cb4affc098 --- openstack/config/cloud_region.py | 39 ++++++++++++++++------ openstack/tests/unit/test_microversions.py | 10 ++++++ 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index d2950a4e9..b32c078e8 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -38,6 +38,18 @@ def _make_key(key, service_type): return "_".join([service_type, key]) +def _get_implied_microversion(version): + if not version: + return + if '.' in version: + # Some services historically had a .0 in their normal api version. + # Neutron springs to mind with version "2.0". If a user has "2.0" + # set in a variable or config file just because history, we don't + # need to send any microversion headers. + if version.split('.')[1] != "0": + return version + + def from_session(session, name=None, region_name=None, force_ipv4=False, app_name=None, app_version=None, **kwargs): @@ -348,17 +360,22 @@ def _get_version_request(self, service_type, version): version_request.version = version default_microversion = self.get_default_microversion(service_type) - if not default_microversion and version and '.' in version: - # Some services historically had a .0 in their normal api version. - # Neutron springs to mind with version "2.0". If a user has "2.0" - # set in a variable or config file just because history, we don't - # need to send any microversion headers. - if version.split('.')[1] != "0": - default_microversion = version - # If we're inferring a microversion, don't pass the whole - # string in as api_version, since that tells keystoneauth - # we're looking for a major api version. - version_request.version = version[0] + implied_microversion = _get_implied_microversion(version) + if (implied_microversion and default_microversion + and implied_microversion != default_microversion): + raise exceptions.ConfigException( + "default_microversion of {default_microversion} was given" + " for {service_type}, but api_version looks like a" + " microversion as well. Please set api_version to just the" + " desired major version, or omit default_microversion".format( + default_microversion=default_microversion, + service_type=service_type)) + if implied_microversion: + default_microversion = implied_microversion + # If we're inferring a microversion, don't pass the whole + # string in as api_version, since that tells keystoneauth + # we're looking for a major api version. + version_request.version = version[0] version_request.default_microversion = default_microversion diff --git a/openstack/tests/unit/test_microversions.py b/openstack/tests/unit/test_microversions.py index 97e3d99f8..67dc79d1a 100644 --- a/openstack/tests/unit/test_microversions.py +++ b/openstack/tests/unit/test_microversions.py @@ -106,3 +106,13 @@ def test_default_microversion(self): self.assertEqual(server1['name'], r['name']) self.assert_calls() + + def test_conflicting_implied_and_direct(self): + + self.cloud.config.config['compute_default_microversion'] = '2.7' + self.cloud.config.config['compute_api_version'] = '2.13' + + self.assertRaises(exceptions.ConfigException, self.cloud.get_server) + + # We should fail before we even authenticate + self.assertEqual(0, len(self.adapter.request_history)) From ca1efca33073e769105aa94a9436fd9df9fb2378 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 14 Jun 2018 07:57:12 -0500 Subject: [PATCH 2088/3836] Turn OSC tips jobs non-voting There are several flapping tests in OSC right now - one related to floating ips, and a set of volume tests. Turn the job to non-voting so we don't keep blocking the ability to land things. It's still important for SDK reviewers to look at failures here. Change-Id: I71d2bff15e518ce7d55cd4213e85efdbf4924209 --- .zuul.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index c4d8b7cc7..d1eb91d9b 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -292,7 +292,8 @@ - openstacksdk-functional-devstack-magnum: voting: false - openstacksdk-functional-devstack-python3 - - osc-functional-devstack-tips + - osc-functional-devstack-tips: + voting: false - neutron-grenade - openstack-tox-lower-constraints - nodepool-functional-py35-src @@ -303,7 +304,6 @@ sphinx_python: python3 - openstacksdk-functional-devstack - openstacksdk-functional-devstack-python3 - - osc-functional-devstack-tips - neutron-grenade - openstack-tox-lower-constraints - nodepool-functional-py35-src From 850b99bf8ccf7e3fe5baf4dd90232cc1bea0be59 Mon Sep 17 00:00:00 2001 From: Ian Wienand Date: Fri, 15 Jun 2018 10:52:44 +1000 Subject: [PATCH 2089/3836] Add some backoff to find_best_address I'm seeing The cloud returned multiple addresses, and none of them seem to work. That might be what you wanted, but we have no clue what's going on, so we just picked one at random Which isn't super helpful. The problem turns out to be that the address that is being probed returned "no route to host" (errno 113) temporarily while the interface came up; with just a second or two more this interface was actually active. It's hard to say exactly, as this is all a bit hand-wavy ... but I think it's worth guessing that a socket.error on a booting host *may* resolve itself, and is worth a few retries to be a bit more generous. Emperically, this has made finding the address more reliable when bringing up nodes from puppetmaster->packethost in infra. This also adds some more info to the debug message. Change-Id: I0429f7e77f42180f27bbdd063790474a9fd96cbb --- openstack/cloud/meta.py | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/openstack/cloud/meta.py b/openstack/cloud/meta.py index 6b5fd65cb..3396e3c84 100644 --- a/openstack/cloud/meta.py +++ b/openstack/cloud/meta.py @@ -19,6 +19,7 @@ import socket from openstack import _log +from openstack import utils from openstack.cloud import exc @@ -238,24 +239,36 @@ def find_best_address(addresses, public=False, cloud_public=True): # reachable. Otherwise we're just debug log spamming on every listing # of private ip addresses for address in addresses: - # Return the first one that is reachable try: - for res in socket.getaddrinfo( - address, 22, socket.AF_UNSPEC, socket.SOCK_STREAM, 0): - family, socktype, proto, _, sa = res - connect_socket = socket.socket(family, socktype, proto) - connect_socket.settimeout(1) - connect_socket.connect(sa) - return address + for count in utils.iterate_timeout( + 5, "Timeout waiting for %s" % address, wait=0.1): + # Return the first one that is reachable + try: + for res in socket.getaddrinfo( + address, 22, socket.AF_UNSPEC, + socket.SOCK_STREAM, 0): + family, socktype, proto, _, sa = res + connect_socket = socket.socket( + family, socktype, proto) + connect_socket.settimeout(1) + connect_socket.connect(sa) + return address + except socket.error: + # Sometimes a "no route to address" type error + # will fail fast, but can often come alive + # when retried. + continue except Exception: pass + # Give up and return the first - none work as far as we can tell if do_check: log = _log.setup_logging('openstack') log.debug( - 'The cloud returned multiple addresses, and none of them seem' - ' to work. That might be what you wanted, but we have no clue' - " what's going on, so we just picked one at random") + "The cloud returned multiple addresses %s:, and we could not " + "connect to port 22 on either. That might be what you wanted, " + "but we have no clue what's going on, so we picked the first one " + "%s" % (addresses, addresses[0])) return addresses[0] From 46bb367ad6a72025e23fc3f26f9f84b298da82d3 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Fri, 8 Jun 2018 18:50:10 +0200 Subject: [PATCH 2090/3836] Allow configuring status_code_retries and connect_retries via cloud config Change-Id: I45cf09f9aca8e4ed515a93e4903deb3df83ac65b --- lower-constraints.txt | 2 +- openstack/config/cloud_region.py | 33 ++++++++++++++++--- .../tests/unit/config/test_cloud_config.py | 7 ++++ .../wire-in-retries-10898f7bc81e2269.yaml | 7 ++++ requirements.txt | 2 +- 5 files changed, 44 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/wire-in-retries-10898f7bc81e2269.yaml diff --git a/lower-constraints.txt b/lower-constraints.txt index 1082ca74e..2043b090f 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -13,7 +13,7 @@ jmespath==0.9.0 jsonpatch==1.16 jsonpointer==1.13 jsonschema==2.6.0 -keystoneauth1==3.7.0 +keystoneauth1==3.8.0 linecache2==1.0.0 mock==2.0.0 mox3==0.20.0 diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index b32c078e8..f4c96af3c 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -207,7 +207,8 @@ def get_auth_args(self): def _get_config( self, key, service_type, default=None, - fallback_to_unprefixed=False): + fallback_to_unprefixed=False, + converter=None): '''Get a config value for a service_type. Finds the config value for a key, looking first for it prefixed by @@ -226,10 +227,17 @@ def _get_config( for st in self._service_type_manager.get_all_types(service_type): value = self.config.get(_make_key(key, st)) if value is not None: - return value - if fallback_to_unprefixed: - return self.config.get(key) - return default + break + else: + if fallback_to_unprefixed: + value = self.config.get(key) + + if value is None: + return default + else: + if converter is not None: + value = converter(value) + return value def get_interface(self, service_type=None): return self._get_config( @@ -264,6 +272,16 @@ def get_endpoint(self, service_type): value = self._get_config('endpoint', service_type) return value + def get_connect_retries(self, service_type): + return self._get_config('connect_retries', service_type, + fallback_to_unprefixed=True, + converter=int) + + def get_status_code_retries(self, service_type): + return self._get_config('status_code_retries', service_type, + fallback_to_unprefixed=True, + converter=int) + @property def prefer_ipv6(self): return not self._force_ipv4 @@ -399,6 +417,11 @@ def get_session_client( """ version_request = self._get_version_request(service_type, version) + kwargs.setdefault('connect_retries', + self.get_connect_retries(service_type)) + kwargs.setdefault('status_code_retries', + self.get_status_code_retries(service_type)) + client = constructor( session=self.get_session(), service_type=self.get_service_type(service_type), diff --git a/openstack/tests/unit/config/test_cloud_config.py b/openstack/tests/unit/config/test_cloud_config.py index 37bd2b6f0..dc0fc080a 100644 --- a/openstack/tests/unit/config/test_cloud_config.py +++ b/openstack/tests/unit/config/test_cloud_config.py @@ -34,6 +34,9 @@ 'identity_service_name': 'locks', 'volume_api_version': '1', 'auth': {'password': 'hunter2', 'username': 'AzureDiamond'}, + 'connect_retries': 1, + 'baremetal_status_code_retries': 5, + 'baremetal_connect_retries': 3, } @@ -149,6 +152,10 @@ def test_getters(self): self.assertIsNone(cc.get_endpoint('image')) self.assertIsNone(cc.get_service_name('compute')) self.assertEqual('locks', cc.get_service_name('identity')) + self.assertIsNone(cc.get_status_code_retries('compute')) + self.assertEqual(5, cc.get_status_code_retries('baremetal')) + self.assertEqual(1, cc.get_connect_retries('compute')) + self.assertEqual(3, cc.get_connect_retries('baremetal')) def test_aliases(self): services_dict = fake_services_dict.copy() diff --git a/releasenotes/notes/wire-in-retries-10898f7bc81e2269.yaml b/releasenotes/notes/wire-in-retries-10898f7bc81e2269.yaml new file mode 100644 index 000000000..a3de250f7 --- /dev/null +++ b/releasenotes/notes/wire-in-retries-10898f7bc81e2269.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Allows configuring Session's ``connect_retries`` and + ``status_code_retries`` via the cloud configuration (options + ``_connect_retries``, ``connect_retries``, + ``_status_code_retries`` and ``status_code_retries``). diff --git a/requirements.txt b/requirements.txt index 4f679ba3c..5ffab64c5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ requestsexceptions>=1.2.0 # Apache-2.0 jsonpatch!=1.20,>=1.16 # BSD six>=1.10.0 # MIT os-service-types>=1.2.0 # Apache-2.0 -keystoneauth1>=3.7.0 # Apache-2.0 +keystoneauth1>=3.8.0 # Apache-2.0 deprecation>=1.0 # Apache-2.0 munch>=2.1.0 # MIT From 98ee140bf6bca0d2101605d25ee372dda8aff6ee Mon Sep 17 00:00:00 2001 From: Corey Wright Date: Fri, 15 Jun 2018 17:29:34 -0500 Subject: [PATCH 2091/3836] Add testing of availability_zones() "details" argument Commit ccd8daa6 added the "details" argument to the availability_zones() method and the backing AvailabilityZoneDetails class, but didn't refactor the existing availability_zones() testing to account for the "details" argument and only added testing specific to the backing class. Change-Id: I56b9c0784848187e123381406b0a41d26c6d68bb Signed-off-by: Corey Wright --- openstack/tests/unit/compute/v2/test_proxy.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index d4b1713cf..5e55a3591 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -418,10 +418,17 @@ def test_get_server_output(self): method_args=["value", 1], expected_kwargs={"length": 1}) - def test_availability_zones(self): - self.verify_list_no_kwargs(self.proxy.availability_zones, - az.AvailabilityZone, - paginated=False) + def test_availability_zones_not_detailed(self): + self.verify_list(self.proxy.availability_zones, + az.AvailabilityZone, + paginated=False, + method_kwargs={"details": False}) + + def test_availability_zones_detailed(self): + self.verify_list(self.proxy.availability_zones, + az.AvailabilityZoneDetail, + paginated=False, + method_kwargs={"details": True}) def test_get_all_server_metadata(self): self._verify2("openstack.compute.v2.server.Server.get_metadata", From f7da0170aebf81a71dcec21b9d7bfba055faae75 Mon Sep 17 00:00:00 2001 From: Corey Wright Date: Fri, 15 Jun 2018 16:58:05 -0500 Subject: [PATCH 2092/3836] Add hypervisor details to hypervisors list if requested Allow optionally generating a list of hypervisors that includes hypervisor details to reduce both openstacksdk and Nova API calls when details are wanted for all hypervisors and to better mirror the Nova API which provides such a feature. The "details" argument defaults to "False" to maintain API backwards compatibility. Change-Id: Iefa32570a43a7e1fd6f05ab519882424272ae1c1 Signed-off-by: Corey Wright --- openstack/compute/v2/_proxy.py | 13 +++++++++++-- openstack/compute/v2/hypervisor.py | 8 ++++++++ openstack/tests/unit/compute/v2/test_hypervisor.py | 9 +++++++++ openstack/tests/unit/compute/v2/test_proxy.py | 13 +++++++++---- 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index a9cb8c2f9..fc0f2747e 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -937,14 +937,23 @@ def server_groups(self, **query): """ return self._list(_server_group.ServerGroup, paginated=False, **query) - def hypervisors(self): + def hypervisors(self, details=False): """Return a generator of hypervisor + :param bool details: When set to the default, ``False``, + :class:`~openstack.compute.v2.hypervisor.Hypervisor` + instances will be returned. ``True`` will cause + :class:`~openstack.compute.v2.hypervisor.HypervisorDetail` + instances to be returned. :returns: A generator of hypervisor :rtype: class: `~openstack.compute.v2.hypervisor.Hypervisor` """ + if details: + hypervisor = _hypervisor.HypervisorDetail + else: + hypervisor = _hypervisor.Hypervisor - return self._list(_hypervisor.Hypervisor, paginated=False) + return self._list(hypervisor, paginated=False) def find_hypervisor(self, name_or_id, ignore_missing=True): """Find a hypervisor from name or id to get the corresponding info diff --git a/openstack/compute/v2/hypervisor.py b/openstack/compute/v2/hypervisor.py index 1c532b9ad..e28cb095a 100644 --- a/openstack/compute/v2/hypervisor.py +++ b/openstack/compute/v2/hypervisor.py @@ -65,3 +65,11 @@ class Hypervisor(resource.Resource): host_ip = resource.Body('host_ip') #: Disk space available to the scheduler disk_available = resource.Body("disk_available_least") + + +class HypervisorDetail(Hypervisor): + base_path = '/os-hypervisors/detail' + + # capabilities + allow_get = False + allow_list = True diff --git a/openstack/tests/unit/compute/v2/test_hypervisor.py b/openstack/tests/unit/compute/v2/test_hypervisor.py index e6bccba67..9ef80b4cd 100644 --- a/openstack/tests/unit/compute/v2/test_hypervisor.py +++ b/openstack/tests/unit/compute/v2/test_hypervisor.py @@ -76,3 +76,12 @@ def test_make_it(self): self.assertEqual(EXAMPLE['disk_available_least'], sot.disk_available) self.assertEqual(EXAMPLE['local_gb'], sot.local_disk_size) self.assertEqual(EXAMPLE['free_ram_mb'], sot.memory_free) + + def test_detail(self): + sot = hypervisor.HypervisorDetail() + self.assertEqual('hypervisor', sot.resource_key) + self.assertEqual('hypervisors', sot.resources_key) + self.assertEqual('/os-hypervisors/detail', sot.base_path) + self.assertEqual('compute', sot.service.service_type) + self.assertFalse(sot.allow_get) + self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index d4b1713cf..75aa892d9 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -475,10 +475,15 @@ def test_server_groups(self): self.verify_list(self.proxy.server_groups, server_group.ServerGroup, paginated=False) - def test_hypervisors(self): - self.verify_list_no_kwargs(self.proxy.hypervisors, - hypervisor.Hypervisor, - paginated=False) + def test_hypervisors_not_detailed(self): + self.verify_list(self.proxy.hypervisors, hypervisor.Hypervisor, + paginated=False, + method_kwargs={"details": False}) + + def test_hypervisors_detailed(self): + self.verify_list(self.proxy.hypervisors, hypervisor.HypervisorDetail, + paginated=False, + method_kwargs={"details": True}) def test_find_hypervisor(self): self.verify_find(self.proxy.find_hypervisor, From 90396f4d35ff7e17324823a8ae4f465227bbe3ab Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 13 Jun 2018 11:16:11 -0500 Subject: [PATCH 2093/3836] Add tests to verify behavior on '' in self link placement is adding a self link of '' to satisfy version discovery. Put in a test to verify that we work properly with this. While we're in there, add a quick test to show microversion discovery too. Change-Id: I28e1b0ce6d372fc1e5aef335597b6db6e3b49660 --- openstack/tests/unit/base.py | 10 +++++ openstack/tests/unit/fixtures/catalog-v2.json | 14 +++++++ openstack/tests/unit/fixtures/catalog-v3.json | 13 ++++++ openstack/tests/unit/fixtures/placement.json | 11 +++++ openstack/tests/unit/test_placement_rest.py | 42 +++++++++++++++++++ 5 files changed, 90 insertions(+) create mode 100644 openstack/tests/unit/fixtures/placement.json create mode 100644 openstack/tests/unit/test_placement_rest.py diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index f81d97db6..c6f871452 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -468,6 +468,12 @@ def get_nova_discovery_mock_dict( uri=compute_discovery_url, text=open(discovery_fixture, 'r').read()) + def get_placement_discovery_mock_dict(self): + discovery_fixture = os.path.join( + self.fixtures_directory, "placement.json") + return dict(method='GET', uri="https://placement.example.com/", + text=open(discovery_fixture, 'r').read()) + def get_designate_discovery_mock_dict(self): discovery_fixture = os.path.join( self.fixtures_directory, "dns.json") @@ -500,6 +506,10 @@ def use_glance( self.get_glance_discovery_mock_dict( image_version_json, image_discovery_url)]) + def use_placement(self): + self.__do_register_uris([ + self.get_placement_discovery_mock_dict()]) + def use_designate(self): # NOTE(slaweq): This method is only meant to be used in "setUp" # where the ordering of the url being registered is tightly controlled diff --git a/openstack/tests/unit/fixtures/catalog-v2.json b/openstack/tests/unit/fixtures/catalog-v2.json index b63669bc9..be682cdd1 100644 --- a/openstack/tests/unit/fixtures/catalog-v2.json +++ b/openstack/tests/unit/fixtures/catalog-v2.json @@ -113,6 +113,20 @@ "type": "object-store", "name": "swift" }, + { + "endpoints_links": [], + "endpoints": [ + { + "adminURL": "https://placement.example.com", + "region": "RegionOne", + "publicURL": "https://placement.example.com", + "internalURL": "https://placement.example.com", + "id": "652f0612744042bfbb8a8bb2c777a16e" + } + ], + "type": "placement", + "name": "placement" + }, { "endpoints_links": [], "endpoints": [ diff --git a/openstack/tests/unit/fixtures/catalog-v3.json b/openstack/tests/unit/fixtures/catalog-v3.json index a08d3ab00..2c1805b11 100644 --- a/openstack/tests/unit/fixtures/catalog-v3.json +++ b/openstack/tests/unit/fixtures/catalog-v3.json @@ -136,6 +136,19 @@ "name": "heat", "type": "orchestration" }, + { + "endpoints": [ + { + "id": "10c76ffd2b744a67950ed1365190d353", + "interface": "public", + "region": "RegionOne", + "url": "https://placement.example.com" + } + ], + "endpoints_links": [], + "name": "placement", + "type": "placement" + }, { "endpoints": [ { diff --git a/openstack/tests/unit/fixtures/placement.json b/openstack/tests/unit/fixtures/placement.json new file mode 100644 index 000000000..6ba0f278e --- /dev/null +++ b/openstack/tests/unit/fixtures/placement.json @@ -0,0 +1,11 @@ +{ + "versions": [ + { + "id": "v1.0", + "links": [{"href": "", "rel": "self"}], + "max_version": "1.17", + "min_version": "1.0", + "status": "CURRENT" + } + ] +} diff --git a/openstack/tests/unit/test_placement_rest.py b/openstack/tests/unit/test_placement_rest.py new file mode 100644 index 000000000..e0c046166 --- /dev/null +++ b/openstack/tests/unit/test_placement_rest.py @@ -0,0 +1,42 @@ +# Copyright (c) 2018 Red Hat, 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. + +from openstack.tests.unit import base + + +class TestPlacementRest(base.TestCase): + + def setUp(self): + super(TestPlacementRest, self).setUp() + self.use_placement() + + def test_discovery(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'placement', 'public', append=['allocation_candidates']), + json={}) + ]) + rs = self.cloud.placement.get('/allocation_candidates') + self.assertEqual(200, rs.status_code) + self.assertEqual( + 'https://placement.example.com/allocation_candidates', + rs.url) + self.assert_calls() + + def test_microversion_discovery(self): + self.assertEqual( + (1, 17), + self.cloud.placement.get_endpoint_data().max_microversion) + self.assert_calls() From 130010db36fcca4d65392d9dc3b1fe4f23e8a1b2 Mon Sep 17 00:00:00 2001 From: Mohammed Naser Date: Mon, 18 Jun 2018 19:39:18 -0400 Subject: [PATCH 2094/3836] Change 'Member' role reference to 'member' Keystone has changed the default role during bootstrap from 'Member' to 'member' therefore the tests which operated under the assumption of having the 'Member' role assigned are no longer passing. This patch addresses this issue, however, long term, it's probably better to create a role and assign it during the test rather than assuming one already exists, but that's probably for another patch. Change-Id: I84f0a1004e45d72c72802b38eeeba2d2c259c483 --- openstack/tests/functional/cloud/test_project.py | 4 ++-- openstack/tests/functional/cloud/test_users.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openstack/tests/functional/cloud/test_project.py b/openstack/tests/functional/cloud/test_project.py index 72f07c9bc..48d58bcde 100644 --- a/openstack/tests/functional/cloud/test_project.py +++ b/openstack/tests/functional/cloud/test_project.py @@ -64,10 +64,10 @@ def test_create_project(self): # Grant the current user access to the project self.assertTrue(self.operator_cloud.grant_role( - 'Member', user=user_id, project=project['id'], wait=True)) + 'member', user=user_id, project=project['id'], wait=True)) self.addCleanup( self.operator_cloud.revoke_role, - 'Member', user=user_id, project=project['id'], wait=True) + 'member', user=user_id, project=project['id'], wait=True) new_cloud = self.operator_cloud.connect_as_project(project) self.add_info_on_exception( diff --git a/openstack/tests/functional/cloud/test_users.py b/openstack/tests/functional/cloud/test_users.py index 2ac99053a..6f215cb2c 100644 --- a/openstack/tests/functional/cloud/test_users.py +++ b/openstack/tests/functional/cloud/test_users.py @@ -127,10 +127,10 @@ def test_update_user_password(self): self.assertEqual(user_email, new_user['email']) self.assertTrue(new_user['enabled']) self.assertTrue(self.operator_cloud.grant_role( - 'Member', user=user['id'], project='demo', wait=True)) + 'member', user=user['id'], project='demo', wait=True)) self.addCleanup( self.operator_cloud.revoke_role, - 'Member', user=user['id'], project='demo', wait=True) + 'member', user=user['id'], project='demo', wait=True) new_cloud = self.operator_cloud.connect_as( user_id=user['id'], From bb980cd4d97cec0f09eb231c20f3bdc1267511b9 Mon Sep 17 00:00:00 2001 From: Mohammed Naser Date: Tue, 12 Jun 2018 10:33:09 -0400 Subject: [PATCH 2095/3836] Switch VEXXHOST to 'v3password' auth_type All of our infrastructure has been moved to Queens except for Keystone which should be happening in the next few days, therefore we no longer have v2.0 APIs. Change-Id: I5ccfc0e26aaf4eff4eaefbbec636efb50e137efa --- openstack/config/vendors/vexxhost.json | 1 + 1 file changed, 1 insertion(+) diff --git a/openstack/config/vendors/vexxhost.json b/openstack/config/vendors/vexxhost.json index 10e131d4c..488d961be 100644 --- a/openstack/config/vendors/vexxhost.json +++ b/openstack/config/vendors/vexxhost.json @@ -1,6 +1,7 @@ { "name": "vexxhost", "profile": { + "auth_type": "v3password", "auth": { "auth_url": "https://auth.vexxhost.net" }, From 1423fc7dde28c08b3517d8b0b283037f8e88c507 Mon Sep 17 00:00:00 2001 From: "Yuanbin.Chen" Date: Tue, 19 Jun 2018 14:17:03 +0800 Subject: [PATCH 2096/3836] Fix clustering profile type miss list operation This patch fix clustering profile type ops error, Senlin and senlinclient has support list 'op', but openstacksdk not exist profile type list 'op', Add openstacksdk support profile type list 'op'. Senlinclient merge support profile type list 'op' link: https://review.openstack.org/#/c/492047/ Closes-Bug: 1777454 Change-Id: I4e2a1ef6ceb34fbef2c4191582956e5556f4eb0e Signed-off-by: Yuanbin.Chen --- openstack/clustering/v1/_proxy.py | 14 ++++++++++++++ openstack/clustering/v1/profile_type.py | 6 ++++++ .../tests/unit/clustering/v1/test_profile_type.py | 12 ++++++++++++ openstack/tests/unit/clustering/v1/test_proxy.py | 12 ++++++++++++ 4 files changed, 44 insertions(+) diff --git a/openstack/clustering/v1/_proxy.py b/openstack/clustering/v1/_proxy.py index 36d697d53..f461aa0d8 100644 --- a/openstack/clustering/v1/_proxy.py +++ b/openstack/clustering/v1/_proxy.py @@ -1155,3 +1155,17 @@ def services(self, **query): :class:`~openstack.clustering.v1.service.Service` """ return self._list(_service.Service, paginated=False, **query) + + def list_profile_type_operations(self, profile_type): + """Get the operation about a profile type. + + :param profile_type: The name of the profile_type to retrieve or an + object of :class:`~openstack.clustering.v1.profile_type.ProfileType`. + + :returns: A :class:`~openstack.clustering.v1.profile_type.ProfileType` + object. + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + profile_type matching the name could be found. + """ + obj = self._get_resource(_profile_type.ProfileType, profile_type) + return obj.type_ops(self) diff --git a/openstack/clustering/v1/profile_type.py b/openstack/clustering/v1/profile_type.py index 3d7e4df23..0f105920a 100644 --- a/openstack/clustering/v1/profile_type.py +++ b/openstack/clustering/v1/profile_type.py @@ -12,6 +12,7 @@ from openstack.clustering import clustering_service from openstack import resource +from openstack import utils class ProfileType(resource.Resource): @@ -31,3 +32,8 @@ class ProfileType(resource.Resource): schema = resource.Body('schema') #: The support status of the profile type support_status = resource.Body('support_status') + + def type_ops(self, session): + url = utils.urljoin(self.base_path, self.id, 'ops') + resp = session.get(url) + return resp.json() diff --git a/openstack/tests/unit/clustering/v1/test_profile_type.py b/openstack/tests/unit/clustering/v1/test_profile_type.py index f3c7f91a8..6c1ffd5ca 100644 --- a/openstack/tests/unit/clustering/v1/test_profile_type.py +++ b/openstack/tests/unit/clustering/v1/test_profile_type.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import mock from openstack.tests.unit import base from openstack.clustering.v1 import profile_type @@ -46,3 +47,14 @@ def test_instantiate(self): self.assertEqual(FAKE['name'], sot.name) self.assertEqual(FAKE['schema'], sot.schema) self.assertEqual(FAKE['support_status'], sot.support_status) + + def test_ops(self): + sot = profile_type.ProfileType(**FAKE) + + resp = mock.Mock() + resp.json = mock.Mock(return_value='') + sess = mock.Mock() + sess.get = mock.Mock(return_value=resp) + self.assertEqual('', sot.type_ops(sess)) + url = 'profile-types/%s/ops' % sot.id + sess.get.assert_called_once_with(url) diff --git a/openstack/tests/unit/clustering/v1/test_proxy.py b/openstack/tests/unit/clustering/v1/test_proxy.py index 74b472b6d..6d8118100 100644 --- a/openstack/tests/unit/clustering/v1/test_proxy.py +++ b/openstack/tests/unit/clustering/v1/test_proxy.py @@ -576,3 +576,15 @@ def test_wait_for_delete_params(self, mock_wait): self.proxy.wait_for_delete(mock_resource, 1, 2) mock_wait.assert_called_once_with(self.proxy, mock_resource, 1, 2) + + @deprecation.fail_if_not_removed + @mock.patch.object(proxy_base.Proxy, '_get_resource') + def test_profile_type_ops(self, mock_get): + mock_profile = profile_type.ProfileType.new(id='FAKE_PROFILE') + mock_get.return_value = mock_profile + self._verify( + "openstack.clustering.v1.profile_type.ProfileType.type_ops", + self.proxy.list_profile_type_operations, + method_args=["FAKE_PROFILE"]) + mock_get.assert_called_once_with(profile_type.ProfileType, + "FAKE_PROFILE") From e2fce2f833ad88f176ba6b83cbda434ff826ba7d Mon Sep 17 00:00:00 2001 From: Mohammed Naser Date: Tue, 12 Jun 2018 10:34:37 -0400 Subject: [PATCH 2097/3836] Fix path for Limestone Networks vendor file It looks like it was mis-copied into a subfolder instead of being in the direct vendors folder. Change-Id: I331bc0df246668c0c3dca2b25a2ef7b2def21ffe --- openstack/config/vendors/{vendors => }/limestonenetworks.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename openstack/config/vendors/{vendors => }/limestonenetworks.json (100%) diff --git a/openstack/config/vendors/vendors/limestonenetworks.json b/openstack/config/vendors/limestonenetworks.json similarity index 100% rename from openstack/config/vendors/vendors/limestonenetworks.json rename to openstack/config/vendors/limestonenetworks.json From 5267c5bfed58714714c29030ef1a1673141f0d5b Mon Sep 17 00:00:00 2001 From: Yang Youseok Date: Fri, 8 Jun 2018 18:31:57 +0900 Subject: [PATCH 2098/3836] Support port binding extended attributes for querying port Currently Port class does not have port binding extended attributes for QueryParameters, so there is no way to filter using those properties. Add those properties to QueryParameters to pass query validation so that user can query using port binding attributes. Change-Id: I48e4a2efce88f6e2c68ae63337a2cf5bb7303013 --- openstack/network/v2/port.py | 2 ++ openstack/tests/unit/network/v2/test_port.py | 7 ++++++- .../add_support_port_binding_attrs-c70966724eb970f3.yaml | 5 +++++ 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add_support_port_binding_attrs-c70966724eb970f3.yaml diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index 335e885eb..64d034aeb 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -30,6 +30,8 @@ class Port(resource.Resource, tag.TagMixin): # NOTE: we skip query on list or datetime fields for now _query_mapping = resource.QueryParameters( + 'binding:host_id', 'binding:profile', 'binding:vif_details', + 'binding:vif_type', 'binding:vnic_type', 'description', 'device_id', 'device_owner', 'fixed_ips', 'ip_address', 'mac_address', 'name', 'network_id', 'status', 'subnet_id', is_admin_state_up='admin_state_up', diff --git a/openstack/tests/unit/network/v2/test_port.py b/openstack/tests/unit/network/v2/test_port.py index 23f3475e7..2fb241214 100644 --- a/openstack/tests/unit/network/v2/test_port.py +++ b/openstack/tests/unit/network/v2/test_port.py @@ -68,7 +68,12 @@ def test_basic(self): self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertDictEqual({"description": "description", + self.assertDictEqual({"binding:host_id": "binding:host_id", + "binding:profile": "binding:profile", + "binding:vif_details": "binding:vif_details", + "binding:vif_type": "binding:vif_type", + "binding:vnic_type": "binding:vnic_type", + "description": "description", "device_id": "device_id", "device_owner": "device_owner", "fixed_ips": "fixed_ips", diff --git a/releasenotes/notes/add_support_port_binding_attrs-c70966724eb970f3.yaml b/releasenotes/notes/add_support_port_binding_attrs-c70966724eb970f3.yaml new file mode 100644 index 000000000..bcee489ca --- /dev/null +++ b/releasenotes/notes/add_support_port_binding_attrs-c70966724eb970f3.yaml @@ -0,0 +1,5 @@ +--- +features: + - Add support for query of port binding extended attributes including + 'binding:host_id', 'binding:vnic_type', 'binding:vif_type', + 'binding:vif_details', and 'binding:profile'. From 4f92423f878d3b094f5d530c9ba3212a78e194d6 Mon Sep 17 00:00:00 2001 From: Bailey Miller Date: Fri, 9 Feb 2018 01:51:59 +0000 Subject: [PATCH 2099/3836] Adds Senlin support to openstacksdk Adds Senlin API to openstacksdk, along with unit and functional tests for each Senlin function. Allows openstacksdk to create, list, get, update and delete clusters, cluster policies, cluster profiles, and cluster receivers. Also allows for attaching and detaching policies to clusters, updating policies on clusters, and listing policies on clusters. Change-Id: I7e80e8ba74bdb415c2359f5c9672aa900f441fba --- .zuul.yaml | 21 + openstack/cloud/openstackcloud.py | 543 +++++++ openstack/tests/unit/base.py | 15 + openstack/tests/unit/fixtures/catalog-v2.json | 14 + .../unit/fixtures/catalog-v3-suburl.json | 15 +- openstack/tests/unit/fixtures/catalog-v3.json | 13 + ...added-senlin-support-1eb4e47c31258f66.yaml | 3 + shade/tests/functional/test_clustering.py | 1442 +++++++++++++++++ shade/tests/unit/fixtures/clustering.json | 27 + shade/tests/unit/test_clustering.py | 658 ++++++++ 10 files changed, 2750 insertions(+), 1 deletion(-) mode change 100644 => 100755 openstack/cloud/openstackcloud.py create mode 100644 shade/releasenotes/notes/added-senlin-support-1eb4e47c31258f66.yaml create mode 100644 shade/tests/functional/test_clustering.py create mode 100644 shade/tests/unit/fixtures/clustering.json create mode 100644 shade/tests/unit/test_clustering.py diff --git a/.zuul.yaml b/.zuul.yaml index d1eb91d9b..9a7853812 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -199,6 +199,25 @@ OPENSTACKSDK_HAS_SWIFT: 0 OPENSTACKSDK_HAS_MAGNUM: 1 +- job: + name: openstacksdk-functional-devstack-senlin + parent: openstacksdk-functional-devstack + description: | + Run shade functional tests against a master devstack with senlin + required-projects: + - openstack/senlin + vars: + devstack_plugins: + senlin: https://git.openstack.org/openstack/senlin + devstack_services: + s-account: false + s-container: false + s-object: false + s-proxy: false + tox_environment: + OPENSTACKSDK_HAS_SWIFT: 0 + OPENSTACKSDK_HAS_SENLIN: 1 + - job: name: openstacksdk-ansible-functional-devstack parent: openstacksdk-functional-devstack @@ -289,6 +308,7 @@ - openstacksdk-ansible-stable-2.6-functional-devstack: voting: false - openstacksdk-functional-devstack + - openstacksdk-functional-devstack-senlin - openstacksdk-functional-devstack-magnum: voting: false - openstacksdk-functional-devstack-python3 @@ -304,6 +324,7 @@ sphinx_python: python3 - openstacksdk-functional-devstack - openstacksdk-functional-devstack-python3 + - openstacksdk-functional-devstack-senlin - neutron-grenade - openstack-tox-lower-constraints - nodepool-functional-py35-src diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py old mode 100644 new mode 100755 index 104b5ed2f..7650abcd0 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -534,6 +534,14 @@ def _container_infra_client(self): 'container-infra') return self._raw_clients['container-infra'] + @property + def _clustering_client(self): + if 'clustering' not in self._raw_clients: + clustering_client = self._get_versioned_client( + 'clustering', min_version=1, max_version='1.latest') + self._raw_clients['clustering'] = clustering_client + return self._raw_clients['clustering'] + @property def _database_client(self): if 'database' not in self._raw_clients: @@ -11386,3 +11394,538 @@ def list_magnum_services(self): data = self._container_infra_client.get('/mservices') return self._normalize_magnum_services( self._get_and_munchify('mservices', data)) + + def create_cluster(self, name, profile, config=None, desired_capacity=0, + max_size=None, metadata=None, min_size=None, + timeout=None): + profile = self.get_cluster_profile(profile) + profile_id = profile['id'] + body = { + 'desired_capacity': desired_capacity, + 'name': name, + 'profile_id': profile_id + } + + if config is not None: + body['config'] = config + + if max_size is not None: + body['max_size'] = max_size + + if metadata is not None: + body['metadata'] = metadata + + if min_size is not None: + body['min_size'] = min_size + + if timeout is not None: + body['timeout'] = timeout + + data = self._clustering_client.post( + '/clusters', json={'cluster': body}, + error_message="Error creating cluster {name}".format(name=name)) + + return self._get_and_munchify(key=None, data=data) + + def set_cluster_metadata(self, name_or_id, metadata): + cluster = self.get_cluster(name_or_id) + if not cluster: + raise exc.OpenStackCloudException( + 'Invalid Cluster {cluster}'.format(cluster=name_or_id)) + + self._clustering_client.post( + '/clusters/{cluster_id}/metadata'.format(cluster_id=cluster['id']), + json={'metadata': metadata}, + error_message='Error updating cluster metadata') + + def get_cluster_by_id(self, cluster_id): + try: + data = self._clustering_client.get( + "/clusters/{cluster_id}".format(cluster_id=cluster_id), + error_message="Error fetching cluster {name}".format( + name=cluster_id)) + return self._get_and_munchify('cluster', data) + except Exception: + return None + + def get_cluster(self, name_or_id, filters=None): + return _utils._get_entity( + cloud=self, resource='cluster', + name_or_id=name_or_id, filters=filters) + + def update_cluster(self, name_or_id, new_name=None, + profile_name_or_id=None, config=None, metadata=None, + timeout=None, profile_only=False): + old_cluster = self.get_cluster(name_or_id) + if old_cluster is None: + raise exc.OpenStackCloudException( + 'Invalid Cluster {cluster}'.format(cluster=name_or_id)) + cluster = { + 'profile_only': profile_only + } + + if config is not None: + cluster['config'] = config + + if metadata is not None: + cluster['metadata'] = metadata + + if profile_name_or_id is not None: + profile = self.get_cluster_profile(profile_name_or_id) + if profile is None: + raise exc.OpenStackCloudException( + 'Invalid Cluster Profile {profile}'.format( + profile=profile_name_or_id)) + cluster['profile_id'] = profile.id + + if timeout is not None: + cluster['timeout'] = timeout + + if new_name is not None: + cluster['name'] = new_name + + data = self._clustering_client.patch( + "/clusters/{cluster_id}".format(cluster_id=old_cluster['id']), + json={'cluster': cluster}, + error_message="Error updating cluster " + "{name}".format(name=name_or_id)) + + return self._get_and_munchify(key=None, data=data) + + def delete_cluster(self, name_or_id): + cluster = self.get_cluster(name_or_id) + if cluster is None: + self.log.debug("Cluster %s not found for deleting", name_or_id) + return False + + for policy in self.list_policies_on_cluster(name_or_id): + detach_policy = self.get_cluster_policy_by_id( + policy['policy_id']) + self.detach_policy_from_cluster(cluster, detach_policy) + + for receiver in self.list_cluster_receivers(): + if cluster["id"] == receiver["cluster_id"]: + self.delete_cluster_receiver(receiver["id"], wait=True) + + self._clustering_client.delete( + "/clusters/{cluster_id}".format(cluster_id=name_or_id), + error_message="Error deleting cluster {name}".format( + name=name_or_id)) + + return True + + def search_clusters(self, name_or_id=None, filters=None): + clusters = self.list_clusters() + return _utils._filter_list(clusters, name_or_id, filters) + + def list_clusters(self): + try: + data = self._clustering_client.get( + '/clusters', + error_message="Error fetching clusters") + return self._get_and_munchify('clusters', data) + except exc.OpenStackCloudURINotFound as e: + self.log.debug(str(e), exc_info=True) + return [] + + def attach_policy_to_cluster(self, name_or_id, policy_name_or_id, + is_enabled): + cluster = self.get_cluster(name_or_id) + policy = self.get_cluster_policy(policy_name_or_id) + if cluster is None: + raise exc.OpenStackCloudException( + 'Cluster {cluster} not found for attaching'.format( + cluster=name_or_id)) + + if policy is None: + raise exc.OpenStackCloudException( + 'Policy {policy} not found for attaching'.format( + policy=policy_name_or_id)) + + body = { + 'policy_id': policy['id'], + 'enabled': is_enabled + } + + self._clustering_client.post( + "/clusters/{cluster_id}/actions".format(cluster_id=cluster['id']), + error_message="Error attaching policy {policy} to cluster " + "{cluster}".format( + policy=policy['id'], + cluster=cluster['id']), + json={'policy_attach': body}) + + return True + + def detach_policy_from_cluster( + self, name_or_id, policy_name_or_id, wait=False, timeout=3600): + cluster = self.get_cluster(name_or_id) + policy = self.get_cluster_policy(policy_name_or_id) + if cluster is None: + raise exc.OpenStackCloudException( + 'Cluster {cluster} not found for detaching'.format( + cluster=name_or_id)) + + if policy is None: + raise exc.OpenStackCloudException( + 'Policy {policy} not found for detaching'.format( + policy=policy_name_or_id)) + + body = {'policy_id': policy['id']} + self._clustering_client.post( + "/clusters/{cluster_id}/actions".format(cluster_id=cluster['id']), + error_message="Error detaching policy {policy} from cluster " + "{cluster}".format( + policy=policy['id'], + cluster=cluster['id']), + json={'policy_detach': body}) + + if not wait: + return True + + value = [] + + for count in _utils._iterate_timeout( + timeout, "Timeout waiting for cluster policy to detach"): + + # TODO(bjjohnson) This logic will wait until there are no policies. + # Since we're detaching a specific policy, checking to make sure + # that policy is not in the list of policies would be better. + policy_status = self.get_cluster_by_id(cluster['id'])['policies'] + + if policy_status == value: + break + return True + + def update_policy_on_cluster(self, name_or_id, policy_name_or_id, + is_enabled): + cluster = self.get_cluster(name_or_id) + policy = self.get_cluster_policy(policy_name_or_id) + if cluster is None: + raise exc.OpenStackCloudException( + 'Cluster {cluster} not found for updating'.format( + cluster=name_or_id)) + + if policy is None: + raise exc.OpenStackCloudException( + 'Policy {policy} not found for updating'.format( + policy=policy_name_or_id)) + + body = { + 'policy_id': policy['id'], + 'enabled': is_enabled + } + self._clustering_client.post( + "/clusters/{cluster_id}/actions".format(cluster_id=cluster['id']), + error_message="Error updating policy {policy} on cluster " + "{cluster}".format( + policy=policy['id'], + cluster=cluster['id']), + json={'policy_update': body}) + + return True + + def get_policy_on_cluster(self, name_or_id, policy_name_or_id): + try: + policy = self._clustering_client.get( + "/clusters/{cluster_id}/policies/{policy_id}".format( + cluster_id=name_or_id, policy_id=policy_name_or_id), + error_message="Error fetching policy " + "{name}".format(name=policy_name_or_id)) + return self._get_and_munchify('cluster_policy', policy) + except Exception: + return False + + def list_policies_on_cluster(self, name_or_id): + endpoint = "/clusters/{cluster_id}/policies".format( + cluster_id=name_or_id) + try: + data = self._clustering_client.get( + endpoint, + error_message="Error fetching cluster policies") + except exc.OpenStackCloudURINotFound as e: + self.log.debug(str(e), exc_info=True) + return [] + return self._get_and_munchify('cluster_policies', data) + + def create_cluster_profile(self, name, spec, metadata=None): + profile = { + 'name': name, + 'spec': spec + } + + if metadata is not None: + profile['metadata'] = metadata + + data = self._clustering_client.post( + '/profiles', json={'profile': profile}, + error_message="Error creating profile {name}".format(name=name)) + + return self._get_and_munchify('profile', data) + + def set_cluster_profile_metadata(self, name_or_id, metadata): + profile = self.get_profile(name_or_id) + if not profile: + raise exc.OpenStackCloudException( + 'Invalid Profile {profile}'.format(profile=name_or_id)) + + self._clustering_client.post( + '/profiles/{profile_id}/metadata'.format(profile_id=profile['id']), + json={'metadata': metadata}, + error_message='Error updating profile metadata') + + def search_cluster_profiles(self, name_or_id=None, filters=None): + cluster_profiles = self.list_cluster_profiles() + return _utils._filter_list(cluster_profiles, name_or_id, filters) + + def list_cluster_profiles(self): + try: + data = self._clustering_client.get( + '/profiles', + error_message="Error fetching profiles") + except exc.OpenStackCloudURINotFound as e: + self.log.debug(str(e), exc_info=True) + return [] + return self._get_and_munchify('profiles', data) + + def get_cluster_profile_by_id(self, profile_id): + try: + data = self._clustering_client.get( + "/profiles/{profile_id}".format(profile_id=profile_id), + error_message="Error fetching profile {name}".format( + name=profile_id)) + return self._get_and_munchify('profile', data) + except exc.OpenStackCloudURINotFound as e: + self.log.debug(str(e), exc_info=True) + return None + + def get_cluster_profile(self, name_or_id, filters=None): + return _utils._get_entity(self, 'cluster_profile', name_or_id, filters) + + def delete_cluster_profile(self, name_or_id): + profile = self.get_cluster_profile(name_or_id) + if profile is None: + self.log.debug("Profile %s not found for deleting", name_or_id) + return False + + for cluster in self.list_clusters(): + if (name_or_id, profile.id) in cluster.items(): + self.log.debug( + "Profile %s is being used by cluster %s, won't delete", + name_or_id, cluster.name) + return False + + self._clustering_client.delete( + "/profiles/{profile_id}".format(profile_id=profile['id']), + error_message="Error deleting profile " + "{name}".format(name=name_or_id)) + + return True + + def update_cluster_profile(self, name_or_id, metadata=None, new_name=None): + old_profile = self.get_profile(name_or_id) + if not old_profile: + raise exc.OpenStackCloudException( + 'Invalid Profile {profile}'.format(profile=name_or_id)) + + profile = {} + + if metadata is not None: + profile['metadata'] = metadata + + if new_name is not None: + profile['name'] = new_name + + data = self._clustering_client.patch( + "/profiles/{profile_id}".format(profile_id=old_profile.id), + json={'profile': profile}, + error_message="Error updating profile {name}".format( + name=name_or_id)) + + return self._get_and_munchify(key=None, data=data) + + def create_cluster_policy(self, name, spec): + policy = { + 'name': name, + 'spec': spec + } + + data = self._clustering_client.post( + '/policies', json={'policy': policy}, + error_message="Error creating policy {name}".format( + name=policy['name'])) + return self._get_and_munchify('policy', data) + + def search_cluster_policies(self, name_or_id=None, filters=None): + cluster_policies = self.list_cluster_policies() + return _utils._filter_list(cluster_policies, name_or_id, filters) + + def list_cluster_policies(self): + endpoint = "/policies" + try: + data = self._clustering_client.get( + endpoint, + error_message="Error fetching cluster policies") + except exc.OpenStackCloudURINotFound as e: + self.log.debug(str(e), exc_info=True) + return [] + return self._get_and_munchify('policies', data) + + def get_cluster_policy_by_id(self, policy_id): + try: + data = self._clustering_client.get( + "/policies/{policy_id}".format(policy_id=policy_id), + error_message="Error fetching policy {name}".format( + name=policy_id)) + return self._get_and_munchify('policy', data) + except exc.OpenStackCloudURINotFound as e: + self.log.debug(str(e), exc_info=True) + return None + + def get_cluster_policy(self, name_or_id, filters=None): + return _utils._get_entity( + self, 'cluster_policy', name_or_id, filters) + + def delete_cluster_policy(self, name_or_id): + policy = self.get_cluster_policy_by_id(name_or_id) + if policy is None: + self.log.debug("Policy %s not found for deleting", name_or_id) + return False + + for cluster in self.list_clusters(): + if (name_or_id, policy.id) in cluster.items(): + self.log.debug( + "Policy %s is being used by cluster %s, won't delete", + name_or_id, cluster.name) + return False + + self._clustering_client.delete( + "/policies/{policy_id}".format(policy_id=name_or_id), + error_message="Error deleting policy " + "{name}".format(name=name_or_id)) + + return True + + def update_cluster_policy(self, name_or_id, new_name): + old_policy = self.get_policy(name_or_id) + if not old_policy: + raise exc.OpenStackCloudException( + 'Invalid Policy {policy}'.format(policy=name_or_id)) + policy = {'name': new_name} + + data = self._clustering_client.patch( + "/policies/{policy_id}".format(policy_id=old_policy.id), + json={'policy': policy}, + error_message="Error updating policy " + "{name}".format(name=name_or_id)) + return self._get_and_munchify(key=None, data=data) + + def create_cluster_receiver(self, name, receiver_type, + cluster_name_or_id=None, action=None, + actor=None, params=None): + cluster = self.get_cluster(cluster_name_or_id) + if cluster is None: + raise exc.OpenStackCloudException( + 'Invalid cluster {cluster}'.format(cluster=cluster_name_or_id)) + + receiver = { + 'name': name, + 'type': receiver_type + } + + if cluster_name_or_id is not None: + receiver['cluster_id'] = cluster.id + + if action is not None: + receiver['action'] = action + + if actor is not None: + receiver['actor'] = actor + + if params is not None: + receiver['params'] = params + + data = self._clustering_client.post( + '/receivers', json={'receiver': receiver}, + error_message="Error creating receiver {name}".format(name=name)) + return self._get_and_munchify('receiver', data) + + def search_cluster_receivers(self, name_or_id=None, filters=None): + cluster_receivers = self.list_cluster_receivers() + return _utils._filter_list(cluster_receivers, name_or_id, filters) + + def list_cluster_receivers(self): + try: + data = self._clustering_client.get( + '/receivers', + error_message="Error fetching receivers") + except exc.OpenStackCloudURINotFound as e: + self.log.debug(str(e), exc_info=True) + return [] + return self._get_and_munchify('receivers', data) + + def get_cluster_receiver_by_id(self, receiver_id): + try: + data = self._clustering_client.get( + "/receivers/{receiver_id}".format(receiver_id=receiver_id), + error_message="Error fetching receiver {name}".format( + name=receiver_id)) + return self._get_and_munchify('receiver', data) + except exc.OpenStackCloudURINotFound as e: + self.log.debug(str(e), exc_info=True) + return None + + def get_cluster_receiver(self, name_or_id, filters=None): + return _utils._get_entity( + self, 'cluster_receiver', name_or_id, filters) + + def delete_cluster_receiver(self, name_or_id, wait=False, timeout=3600): + receiver = self.get_cluster_receiver(name_or_id) + if receiver is None: + self.log.debug("Receiver %s not found for deleting", name_or_id) + return False + + receiver_id = receiver['id'] + + self._clustering_client.delete( + "/receivers/{receiver_id}".format(receiver_id=receiver_id), + error_message="Error deleting receiver {name}".format( + name=name_or_id)) + + if not wait: + return True + + for count in _utils._iterate_timeout( + timeout, "Timeout waiting for cluster receiver to delete"): + + receiver = self.get_cluster_receiver_by_id(receiver_id) + + if not receiver: + break + + return True + + def update_cluster_receiver(self, name_or_id, new_name=None, action=None, + params=None): + old_receiver = self.get_cluster_receiver(name_or_id) + if old_receiver is None: + raise exc.OpenStackCloudException( + 'Invalid receiver {receiver}'.format(receiver=name_or_id)) + + receiver = {} + + if new_name is not None: + receiver['name'] = new_name + + if action is not None: + receiver['action'] = action + + if params is not None: + receiver['params'] = params + + data = self._clustering_client.patch( + "/receivers/{receiver_id}".format(receiver_id=old_receiver.id), + json={'receiver': receiver}, + error_message="Error updating receiver {name}".format( + name=name_or_id)) + return self._get_and_munchify(key=None, data=data) diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index f81d97db6..ee0072eb6 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -480,6 +480,12 @@ def get_ironic_discovery_mock_dict(self): return dict(method='GET', uri="https://bare-metal.example.com/", text=open(discovery_fixture, 'r').read()) + def get_senlin_discovery_mock_dict(self): + discovery_fixture = os.path.join( + self.fixtures_directory, "clustering.json") + return dict(method='GET', uri="https://clustering.example.com/", + text=open(discovery_fixture, 'r').read()) + def use_compute_discovery( self, compute_version_json='compute-version.json', compute_discovery_url='https://compute.example.com/v2.1/'): @@ -518,6 +524,15 @@ def use_ironic(self): self.__do_register_uris([ self.get_ironic_discovery_mock_dict()]) + def use_senlin(self): + # NOTE(elachance): This method is only meant to be used in "setUp" + # where the ordering of the url being registered is tightly controlled + # if the functionality of .use_senlin is meant to be used during an + # actual test case, use .get_senlin_discovery_mock and apply to the + # right location in the mock_uris when calling .register_uris + self.__do_register_uris([ + self.get_senlin_discovery_mock_dict()]) + def register_uris(self, uri_mock_list=None): """Mock a list of URIs and responses via requests mock. diff --git a/openstack/tests/unit/fixtures/catalog-v2.json b/openstack/tests/unit/fixtures/catalog-v2.json index b63669bc9..3d5060a24 100644 --- a/openstack/tests/unit/fixtures/catalog-v2.json +++ b/openstack/tests/unit/fixtures/catalog-v2.json @@ -126,6 +126,20 @@ ], "type": "dns", "name": "designate" + }, + { + "endpoints_links": [], + "endpoints": [ + { + "adminURL": "https://clustering.example.com", + "region": "RegionOne", + "publicURL": "https://clustering.example.com", + "internalURL": "https://clustering.example.com", + "id": "4deb4d0504a044a395d4480741ba624z" + } + ], + "type": "clustering", + "name": "senlin" } ], "user": { diff --git a/openstack/tests/unit/fixtures/catalog-v3-suburl.json b/openstack/tests/unit/fixtures/catalog-v3-suburl.json index 710815d06..ca2b68107 100644 --- a/openstack/tests/unit/fixtures/catalog-v3-suburl.json +++ b/openstack/tests/unit/fixtures/catalog-v3-suburl.json @@ -148,8 +148,21 @@ "endpoints_links": [], "name": "designate", "type": "dns" + }, + { + "endpoints": [ + { + "id": "4deb4d0504a044a395d4480741ba624z", + "interface": "public", + "region": "RegionOne", + "url": "https://example.com/clustering" + } + ], + "endpoint_links": [], + "name": "senlin", + "type": "clustering" } - ], + ], "expires_at": "9999-12-31T23:59:59Z", "issued_at": "2016-12-17T14:25:05.000000Z", "methods": [ diff --git a/openstack/tests/unit/fixtures/catalog-v3.json b/openstack/tests/unit/fixtures/catalog-v3.json index a08d3ab00..3a2fc9d6c 100644 --- a/openstack/tests/unit/fixtures/catalog-v3.json +++ b/openstack/tests/unit/fixtures/catalog-v3.json @@ -148,6 +148,19 @@ "endpoints_links": [], "name": "designate", "type": "dns" + }, + { + "endpoints": [ + { + "id": "4deb4d0504a044a395d4480741ba624z", + "interface": "public", + "region": "RegionOne", + "url": "https://clustering.example.com" + } + ], + "endpoints_links": [], + "name": "senlin", + "type": "clustering" } ], "expires_at": "9999-12-31T23:59:59Z", diff --git a/shade/releasenotes/notes/added-senlin-support-1eb4e47c31258f66.yaml b/shade/releasenotes/notes/added-senlin-support-1eb4e47c31258f66.yaml new file mode 100644 index 000000000..ccc38b29e --- /dev/null +++ b/shade/releasenotes/notes/added-senlin-support-1eb4e47c31258f66.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added support for senlin diff --git a/shade/tests/functional/test_clustering.py b/shade/tests/functional/test_clustering.py new file mode 100644 index 000000000..960245da5 --- /dev/null +++ b/shade/tests/functional/test_clustering.py @@ -0,0 +1,1442 @@ +# 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_clustering +---------------------------------- + +Functional tests for `shade` clustering methods. +""" + +from testtools import content + +from shade.tests.functional import base + +import time + + +def wait_for_status(client, client_args, field, value, check_interval=1, + timeout=60): + """Wait for an OpenStack resource to enter a specified state + + :param client: An uncalled client resource to be called with resource_args + :param client_args: Arguments to be passed to client + :param field: Dictionary field to check + :param value: Dictionary value to look for + :param check_interval: Interval between checks + :param timeout: Time in seconds to wait for status to update. + :returns: True if correct status was reached + :raises: TimeoutException + """ + resource_status = client(**client_args)[field] + start = time.time() + + while resource_status != value: + time.sleep(check_interval) + resource = client(**client_args) + resource_status = resource[field] + + timed_out = time.time() - start >= timeout + + if resource_status != value and timed_out: + return False + return True + + +def wait_for_create(client, client_args, check_interval=1, timeout=60): + """Wait for an OpenStack resource to be created + + :param client: An uncalled client resource to be called with resource_args + :param client_args: Arguments to be passed to client + :param name: Name of the resource (for logging) + :param check_interval: Interval between checks + :param timeout: Time in seconds to wait for status to update. + :returns: True if openstack.exceptions.NotFoundException is caught + :raises: TimeoutException + + """ + + resource = client(**client_args) + start = time.time() + + while not resource: + time.sleep(check_interval) + resource = client(**client_args) + + timed_out = time.time() - start >= timeout + + if (not resource) and timed_out: + return False + return True + + +def wait_for_delete(client, client_args, check_interval=1, timeout=60): + """Wait for an OpenStack resource to 404/delete + + :param client: An uncalled client resource to be called with resource_args + :param client_args: Arguments to be passed to client + :param name: Name of the resource (for logging) + :param check_interval: Interval between checks + :param timeout: Time in seconds to wait for status to update. + :returns: True if openstack.exceptions.NotFoundException is caught + :raises: TimeoutException + + """ + resource = client(**client_args) + start = time.time() + + while resource: + time.sleep(check_interval) + resource = client(**client_args) + + timed_out = time.time() - start >= timeout + + if resource and timed_out: + return False + return True + + +class TestClustering(base.BaseFunctionalTestCase): + + def setUp(self): + super(TestClustering, self).setUp() + if not self.user_cloud.has_service('clustering'): + self.skipTest('clustering service not supported by cloud') + + def test_create_profile(self): + profile_name = "test_profile" + spec = { + "properties": { + "flavor": "m1.tiny", + "image": "cirros-0.3.5-x86_64-disk", + "networks": [ + { + "network": "private" + } + ], + "security_groups": [ + "default" + ] + }, + "type": "os.nova.server", + "version": 1.0 + } + + self.addDetail('profile', content.text_content(profile_name)) + # Test that we can create a profile and we get it returned + + profile = self.user_cloud.create_cluster_profile(name=profile_name, + spec=spec) + + self.addCleanup(self.cleanup_profile, profile['id']) + + self.assertEqual(profile['name'], profile_name) + self.assertEqual(profile['spec'], spec) + + def test_create_cluster(self): + profile_name = "test_profile" + spec = { + "properties": { + "flavor": "m1.tiny", + "image": "cirros-0.3.5-x86_64-disk", + "networks": [ + { + "network": "private" + } + ], + "security_groups": [ + "default" + ] + }, + "type": "os.nova.server", + "version": 1.0 + } + + self.addDetail('profile', content.text_content(profile_name)) + # Test that we can create a profile and we get it returned + + profile = self.user_cloud.create_cluster_profile(name=profile_name, + spec=spec) + + self.addCleanup(self.cleanup_profile, profile['id']) + + cluster_name = 'example_cluster' + desired_capacity = 0 + + self.addDetail('cluster', content.text_content(cluster_name)) + + # Test that we can create a cluster and we get it returned + cluster = self.user_cloud.create_cluster( + name=cluster_name, profile=profile, + desired_capacity=desired_capacity) + + self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) + + self.assertEqual(cluster['cluster']['name'], cluster_name) + self.assertEqual(cluster['cluster']['profile_id'], profile['id']) + self.assertEqual(cluster['cluster']['desired_capacity'], + desired_capacity) + + def test_get_cluster_by_id(self): + profile_name = "test_profile" + spec = { + "properties": { + "flavor": "m1.tiny", + "image": "cirros-0.3.5-x86_64-disk", + "networks": [ + { + "network": "private" + } + ], + "security_groups": [ + "default" + ] + }, + "type": "os.nova.server", + "version": 1.0 + } + + self.addDetail('profile', content.text_content(profile_name)) + # Test that we can create a profile and we get it returned + + profile = self.user_cloud.create_cluster_profile(name=profile_name, + spec=spec) + + self.addCleanup(self.cleanup_profile, profile['id']) + cluster_name = 'example_cluster' + desired_capacity = 0 + + self.addDetail('cluster', content.text_content(cluster_name)) + + # Test that we can create a cluster and we get it returned + cluster = self.user_cloud.create_cluster( + name=cluster_name, profile=profile, + desired_capacity=desired_capacity) + + self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) + + # Test that we get the same cluster with the get_cluster method + cluster_get = self.user_cloud.get_cluster_by_id( + cluster['cluster']['id']) + self.assertEqual(cluster_get['id'], cluster['cluster']['id']) + + def test_update_cluster(self): + profile_name = "test_profile" + spec = { + "properties": { + "flavor": "m1.tiny", + "image": "cirros-0.3.5-x86_64-disk", + "networks": [ + { + "network": "private" + } + ], + "security_groups": [ + "default" + ] + }, + "type": "os.nova.server", + "version": 1.0 + } + + self.addDetail('profile', content.text_content(profile_name)) + # Test that we can create a profile and we get it returned + + profile = self.user_cloud.create_cluster_profile(name=profile_name, + spec=spec) + + self.addCleanup(self.cleanup_profile, profile['id']) + + cluster_name = 'example_cluster' + desired_capacity = 0 + + self.addDetail('cluster', content.text_content(cluster_name)) + + # Test that we can create a cluster and we get it returned + cluster = self.user_cloud.create_cluster( + name=cluster_name, profile=profile, + desired_capacity=desired_capacity) + + self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) + + # Test that we can update a field on the cluster and only that field + # is updated + + self.user_cloud.update_cluster(cluster['cluster']['id'], + new_name='new_cluster_name') + + wait = wait_for_status( + self.user_cloud.get_cluster_by_id, + {'name_or_id': cluster['cluster']['id']}, 'status', 'ACTIVE') + + self.assertTrue(wait) + cluster_update = self.user_cloud.get_cluster_by_id( + cluster['cluster']['id']) + self.assertEqual(cluster_update['id'], cluster['cluster']['id']) + self.assertEqual(cluster_update['name'], 'new_cluster_name') + self.assertEqual(cluster_update['profile_id'], + cluster['cluster']['profile_id']) + self.assertEqual(cluster_update['desired_capacity'], + cluster['cluster']['desired_capacity']) + + def test_create_cluster_policy(self): + policy_name = 'example_policy' + spec = { + "properties": { + "adjustment": { + "min_step": 1, + "number": 1, + "type": "CHANGE_IN_CAPACITY" + }, + "event": "CLUSTER_SCALE_IN" + }, + "type": "senlin.policy.scaling", + "version": "1.0" + } + + self.addDetail('policy', content.text_content(policy_name)) + + # Test that we can create a policy and we get it returned + + policy = self.user_cloud.create_cluster_policy(name=policy_name, + spec=spec) + + self.addCleanup(self.cleanup_policy, policy['id']) + + self.assertEqual(policy['name'], policy_name) + self.assertEqual(policy['spec'], spec) + + def test_attach_policy_to_cluster(self): + profile_name = "test_profile" + spec = { + "properties": { + "flavor": "m1.tiny", + "image": "cirros-0.3.5-x86_64-disk", + "networks": [ + { + "network": "private" + } + ], + "security_groups": [ + "default" + ] + }, + "type": "os.nova.server", + "version": 1.0 + } + + self.addDetail('profile', content.text_content(profile_name)) + # Test that we can create a profile and we get it returned + + profile = self.user_cloud.create_cluster_profile(name=profile_name, + spec=spec) + + self.addCleanup(self.cleanup_profile, profile['id']) + + cluster_name = 'example_cluster' + desired_capacity = 0 + + self.addDetail('cluster', content.text_content(cluster_name)) + + # Test that we can create a cluster and we get it returned + cluster = self.user_cloud.create_cluster( + name=cluster_name, profile=profile, + desired_capacity=desired_capacity) + + self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) + + policy_name = 'example_policy' + spec = { + "properties": { + "adjustment": { + "min_step": 1, + "number": 1, + "type": "CHANGE_IN_CAPACITY" + }, + "event": "CLUSTER_SCALE_IN" + }, + "type": "senlin.policy.scaling", + "version": "1.0" + } + + self.addDetail('policy', content.text_content(policy_name)) + + # Test that we can create a policy and we get it returned + + policy = self.user_cloud.create_cluster_policy(name=policy_name, + spec=spec) + + self.addCleanup(self.cleanup_policy, policy['id'], + cluster['cluster']['id']) + + # Test that we can attach policy to cluster and get True returned + + attach_cluster = self.user_cloud.get_cluster_by_id( + cluster['cluster']['id']) + attach_policy = self.user_cloud.get_cluster_policy_by_id( + policy['id']) + + policy_attach = self.user_cloud.attach_policy_to_cluster( + attach_cluster, attach_policy, is_enabled=True) + self.assertTrue(policy_attach) + + def test_detach_policy_from_cluster(self): + profile_name = "test_profile" + spec = { + "properties": { + "flavor": "m1.tiny", + "image": "cirros-0.3.5-x86_64-disk", + "networks": [ + { + "network": "private" + } + ], + "security_groups": [ + "default" + ] + }, + "type": "os.nova.server", + "version": 1.0 + } + + self.addDetail('profile', content.text_content(profile_name)) + # Test that we can create a profile and we get it returned + + profile = self.user_cloud.create_cluster_profile(name=profile_name, + spec=spec) + + self.addCleanup(self.cleanup_profile, profile['id']) + + cluster_name = 'example_cluster' + desired_capacity = 0 + + self.addDetail('cluster', content.text_content(cluster_name)) + + # Test that we can create a cluster and we get it returned + cluster = self.user_cloud.create_cluster( + name=cluster_name, profile=profile, + desired_capacity=desired_capacity) + + self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) + + policy_name = 'example_policy' + spec = { + "properties": { + "adjustment": { + "min_step": 1, + "number": 1, + "type": "CHANGE_IN_CAPACITY" + }, + "event": "CLUSTER_SCALE_IN" + }, + "type": "senlin.policy.scaling", + "version": "1.0" + } + + self.addDetail('policy', content.text_content(policy_name)) + + # Test that we can create a policy and we get it returned + + policy = self.user_cloud.create_cluster_policy(name=policy_name, + spec=spec) + + self.addCleanup(self.cleanup_policy, policy['id'], + cluster['cluster']['id']) + + attach_cluster = self.user_cloud.get_cluster_by_id( + cluster['cluster']['id']) + attach_policy = self.user_cloud.get_cluster_policy_by_id( + policy['id']) + + self.user_cloud.attach_policy_to_cluster( + attach_cluster, attach_policy, is_enabled=True) + + wait = wait_for_status( + self.user_cloud.get_cluster_by_id, + {'name_or_id': cluster['cluster']['id']}, 'policies', + ['{policy}'.format(policy=policy['id'])]) + + policy_detach = self.user_cloud.detach_policy_from_cluster( + attach_cluster, attach_policy) + + self.assertTrue(policy_detach) + self.assertTrue(wait) + + def test_get_policy_on_cluster_by_id(self): + profile_name = "test_profile" + spec = { + "properties": { + "flavor": "m1.tiny", + "image": "cirros-0.3.5-x86_64-disk", + "networks": [ + { + "network": "private" + } + ], + "security_groups": [ + "default" + ] + }, + "type": "os.nova.server", + "version": 1.0 + } + + self.addDetail('profile', content.text_content(profile_name)) + # Test that we can create a profile and we get it returned + + profile = self.user_cloud.create_cluster_profile(name=profile_name, + spec=spec) + + self.addCleanup(self.cleanup_profile, profile['id']) + + cluster_name = 'example_cluster' + desired_capacity = 0 + + self.addDetail('cluster', content.text_content(cluster_name)) + + # Test that we can create a cluster and we get it returned + cluster = self.user_cloud.create_cluster( + name=cluster_name, profile=profile, + desired_capacity=desired_capacity) + + self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) + + policy_name = 'example_policy' + spec = { + "properties": { + "adjustment": { + "min_step": 1, + "number": 1, + "type": "CHANGE_IN_CAPACITY" + }, + "event": "CLUSTER_SCALE_IN" + }, + "type": "senlin.policy.scaling", + "version": "1.0" + } + + self.addDetail('policy', content.text_content(policy_name)) + + # Test that we can create a policy and we get it returned + + policy = self.user_cloud.create_cluster_policy(name=policy_name, + spec=spec) + + self.addCleanup(self.cleanup_policy, policy['id'], + cluster['cluster']['id']) + + # Test that we can attach policy to cluster and get True returned + + attach_cluster = self.user_cloud.get_cluster_by_id( + cluster['cluster']['id']) + attach_policy = self.user_cloud.get_cluster_policy_by_id( + policy['id']) + + policy_attach = self.user_cloud.attach_policy_to_cluster( + attach_cluster, attach_policy, is_enabled=True) + self.assertTrue(policy_attach) + + wait = wait_for_status( + self.user_cloud.get_cluster_by_id, + {'name_or_id': cluster['cluster']['id']}, 'policies', + ["{policy}".format(policy=policy['id'])]) + + # Test that we get the same policy with the get_policy_on_cluster + # method + + cluster_policy_get = self.user_cloud.get_policy_on_cluster( + cluster['cluster']["id"], policy['id']) + + self.assertEqual(cluster_policy_get['cluster_id'], + cluster['cluster']["id"]) + self.assertEqual(cluster_policy_get['cluster_name'], + cluster['cluster']["name"]) + self.assertEqual(cluster_policy_get['policy_id'], policy['id']), + self.assertEqual(cluster_policy_get['policy_name'], policy['name']) + self.assertTrue(wait) + + def test_list_policies_on_cluster(self): + profile_name = "test_profile" + spec = { + "properties": { + "flavor": "m1.tiny", + "image": "cirros-0.3.5-x86_64-disk", + "networks": [ + { + "network": "private" + } + ], + "security_groups": [ + "default" + ] + }, + "type": "os.nova.server", + "version": 1.0 + } + + self.addDetail('profile', content.text_content(profile_name)) + # Test that we can create a profile and we get it returned + + profile = self.user_cloud.create_cluster_profile(name=profile_name, + spec=spec) + + self.addCleanup(self.cleanup_profile, profile['id']) + + cluster_name = 'example_cluster' + desired_capacity = 0 + + self.addDetail('cluster', content.text_content(cluster_name)) + + # Test that we can create a cluster and we get it returned + cluster = self.user_cloud.create_cluster( + name=cluster_name, profile=profile, + desired_capacity=desired_capacity) + + self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) + + policy_name = 'example_policy' + spec = { + "properties": { + "adjustment": { + "min_step": 1, + "number": 1, + "type": "CHANGE_IN_CAPACITY" + }, + "event": "CLUSTER_SCALE_IN" + }, + "type": "senlin.policy.scaling", + "version": "1.0" + } + + self.addDetail('policy', content.text_content(policy_name)) + + # Test that we can create a policy and we get it returned + + policy = self.user_cloud.create_cluster_policy(name=policy_name, + spec=spec) + + self.addCleanup(self.cleanup_policy, policy['id'], + cluster['cluster']['id']) + + attach_cluster = self.user_cloud.get_cluster_by_id( + cluster['cluster']['id']) + attach_policy = self.user_cloud.get_cluster_policy_by_id( + policy['id']) + + self.user_cloud.attach_policy_to_cluster( + attach_cluster, attach_policy, is_enabled=True) + + wait = wait_for_status( + self.user_cloud.get_cluster_by_id, + {'name_or_id': cluster['cluster']['id']}, 'policies', + ["{policy}".format(policy=policy['id'])]) + + cluster_policy = self.user_cloud.get_policy_on_cluster( + name_or_id=cluster['cluster']['id'], + policy_name_or_id=policy['id']) + + policy_list = {"cluster_policies": [cluster_policy]} + + # Test that we can list the policies on a cluster + cluster_policies = self.user_cloud.list_policies_on_cluster( + cluster['cluster']["id"]) + self.assertEqual( + cluster_policies, policy_list) + self.assertTrue(wait) + + def test_create_cluster_receiver(self): + profile_name = "test_profile" + spec = { + "properties": { + "flavor": "m1.tiny", + "image": "cirros-0.3.5-x86_64-disk", + "networks": [ + { + "network": "private" + } + ], + "security_groups": [ + "default" + ] + }, + "type": "os.nova.server", + "version": 1.0 + } + + self.addDetail('profile', content.text_content(profile_name)) + # Test that we can create a profile and we get it returned + + profile = self.user_cloud.create_cluster_profile(name=profile_name, + spec=spec) + + self.addCleanup(self.cleanup_profile, profile['id']) + + cluster_name = 'example_cluster' + desired_capacity = 0 + + self.addDetail('cluster', content.text_content(cluster_name)) + + # Test that we can create a cluster and we get it returned + cluster = self.user_cloud.create_cluster( + name=cluster_name, profile=profile, + desired_capacity=desired_capacity) + + self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) + + receiver_name = "example_receiver" + receiver_type = "webhook" + + self.addDetail('receiver', content.text_content(receiver_name)) + + # Test that we can create a receiver and we get it returned + + receiver = self.user_cloud.create_cluster_receiver( + name=receiver_name, receiver_type=receiver_type, + cluster_name_or_id=cluster['cluster']['id'], + action='CLUSTER_SCALE_OUT') + + self.addCleanup(self.cleanup_receiver, receiver['id']) + + self.assertEqual(receiver['name'], receiver_name) + self.assertEqual(receiver['type'], receiver_type) + self.assertEqual(receiver['cluster_id'], cluster['cluster']["id"]) + + def test_list_cluster_receivers(self): + profile_name = "test_profile" + spec = { + "properties": { + "flavor": "m1.tiny", + "image": "cirros-0.3.5-x86_64-disk", + "networks": [ + { + "network": "private" + } + ], + "security_groups": [ + "default" + ] + }, + "type": "os.nova.server", + "version": 1.0 + } + + self.addDetail('profile', content.text_content(profile_name)) + # Test that we can create a profile and we get it returned + + profile = self.user_cloud.create_cluster_profile(name=profile_name, + spec=spec) + + self.addCleanup(self.cleanup_profile, profile['id']) + + cluster_name = 'example_cluster' + desired_capacity = 0 + + self.addDetail('cluster', content.text_content(cluster_name)) + + # Test that we can create a cluster and we get it returned + cluster = self.user_cloud.create_cluster( + name=cluster_name, profile=profile, + desired_capacity=desired_capacity) + + self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) + + receiver_name = "example_receiver" + receiver_type = "webhook" + + self.addDetail('receiver', content.text_content(receiver_name)) + + # Test that we can create a receiver and we get it returned + + receiver = self.user_cloud.create_cluster_receiver( + name=receiver_name, receiver_type=receiver_type, + cluster_name_or_id=cluster['cluster']['id'], + action='CLUSTER_SCALE_OUT') + + self.addCleanup(self.cleanup_receiver, receiver['id']) + + get_receiver = self.user_cloud.get_cluster_receiver_by_id( + receiver['id']) + receiver_list = {"receivers": [get_receiver]} + + # Test that we can list receivers + + receivers = self.user_cloud.list_cluster_receivers() + self.assertEqual(receivers, receiver_list) + + def test_delete_cluster(self): + profile_name = "test_profile" + spec = { + "properties": { + "flavor": "m1.tiny", + "image": "cirros-0.3.5-x86_64-disk", + "networks": [ + { + "network": "private" + } + ], + "security_groups": [ + "default" + ] + }, + "type": "os.nova.server", + "version": 1.0 + } + + self.addDetail('profile', content.text_content(profile_name)) + # Test that we can create a profile and we get it returned + + profile = self.user_cloud.create_cluster_profile(name=profile_name, + spec=spec) + + self.addCleanup(self.cleanup_profile, profile['id']) + + cluster_name = 'example_cluster' + desired_capacity = 0 + + self.addDetail('cluster', content.text_content(cluster_name)) + + # Test that we can create a cluster and we get it returned + cluster = self.user_cloud.create_cluster( + name=cluster_name, profile=profile, + desired_capacity=desired_capacity) + + self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) + + policy_name = 'example_policy' + spec = { + "properties": { + "adjustment": { + "min_step": 1, + "number": 1, + "type": "CHANGE_IN_CAPACITY" + }, + "event": "CLUSTER_SCALE_IN" + }, + "type": "senlin.policy.scaling", + "version": "1.0" + } + + self.addDetail('policy', content.text_content(policy_name)) + + # Test that we can create a policy and we get it returned + + policy = self.user_cloud.create_cluster_policy(name=policy_name, + spec=spec) + + self.addCleanup(self.cleanup_policy, policy['id']) + + # Test that we can attach policy to cluster and get True returned + attach_cluster = self.user_cloud.get_cluster_by_id( + cluster['cluster']['id']) + attach_policy = self.user_cloud.get_cluster_policy_by_id( + policy['id']) + + self.user_cloud.attach_policy_to_cluster( + attach_cluster, attach_policy, is_enabled=True) + + receiver_name = "example_receiver" + receiver_type = "webhook" + + self.addDetail('receiver', content.text_content(receiver_name)) + + # Test that we can create a receiver and we get it returned + + self.user_cloud.create_cluster_receiver( + name=receiver_name, receiver_type=receiver_type, + cluster_name_or_id=cluster['cluster']['id'], + action='CLUSTER_SCALE_OUT') + + # Test that we can delete cluster and get True returned + cluster_delete = self.user_cloud.delete_cluster( + cluster['cluster']['id']) + self.assertTrue(cluster_delete) + + def test_list_clusters(self): + profile_name = "test_profile" + spec = { + "properties": { + "flavor": "m1.tiny", + "image": "cirros-0.3.5-x86_64-disk", + "networks": [ + { + "network": "private" + } + ], + "security_groups": [ + "default" + ] + }, + "type": "os.nova.server", + "version": 1.0 + } + + self.addDetail('profile', content.text_content(profile_name)) + # Test that we can create a profile and we get it returned + + profile = self.user_cloud.create_cluster_profile(name=profile_name, + spec=spec) + + self.addCleanup(self.cleanup_profile, profile['id']) + + cluster_name = 'example_cluster' + desired_capacity = 0 + + self.addDetail('cluster', content.text_content(cluster_name)) + + # Test that we can create a cluster and we get it returned + cluster = self.user_cloud.create_cluster( + name=cluster_name, profile=profile, + desired_capacity=desired_capacity) + + self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) + + wait = wait_for_status( + self.user_cloud.get_cluster_by_id, + {'name_or_id': cluster['cluster']['id']}, 'status', 'ACTIVE') + + get_cluster = self.user_cloud.get_cluster_by_id( + cluster['cluster']['id']) + + # Test that we can list clusters + clusters = self.user_cloud.list_clusters() + self.assertEqual(clusters, [get_cluster]) + self.assertTrue(wait) + + def test_update_policy_on_cluster(self): + profile_name = "test_profile" + spec = { + "properties": { + "flavor": "m1.tiny", + "image": "cirros-0.3.5-x86_64-disk", + "networks": [ + { + "network": "private" + } + ], + "security_groups": [ + "default" + ] + }, + "type": "os.nova.server", + "version": 1.0 + } + + self.addDetail('profile', content.text_content(profile_name)) + # Test that we can create a profile and we get it returned + + profile = self.user_cloud.create_cluster_profile(name=profile_name, + spec=spec) + + self.addCleanup(self.cleanup_profile, profile['id']) + + cluster_name = 'example_cluster' + desired_capacity = 0 + + self.addDetail('cluster', content.text_content(cluster_name)) + + # Test that we can create a cluster and we get it returned + cluster = self.user_cloud.create_cluster( + name=cluster_name, profile=profile, + desired_capacity=desired_capacity) + + self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) + + policy_name = 'example_policy' + spec = { + "properties": { + "adjustment": { + "min_step": 1, + "number": 1, + "type": "CHANGE_IN_CAPACITY" + }, + "event": "CLUSTER_SCALE_IN" + }, + "type": "senlin.policy.scaling", + "version": "1.0" + } + + self.addDetail('policy', content.text_content(policy_name)) + + # Test that we can create a policy and we get it returned + + policy = self.user_cloud.create_cluster_policy(name=policy_name, + spec=spec) + + self.addCleanup(self.cleanup_policy, policy['id'], + cluster['cluster']['id']) + + # Test that we can attach policy to cluster and get True returned + + attach_cluster = self.user_cloud.get_cluster_by_id( + cluster['cluster']['id']) + attach_policy = self.user_cloud.get_cluster_policy_by_id( + policy['id']) + + self.user_cloud.attach_policy_to_cluster( + attach_cluster, attach_policy, is_enabled=True) + + wait_attach = wait_for_status( + self.user_cloud.get_cluster_by_id, + {'name_or_id': cluster['cluster']['id']}, 'policies', + ["{policy}".format(policy=policy['id'])]) + + get_old_policy = self.user_cloud.get_policy_on_cluster( + cluster['cluster']["id"], policy['id']) + + # Test that we can update the policy on cluster + policy_update = self.user_cloud.update_policy_on_cluster( + attach_cluster, attach_policy, is_enabled=False) + + get_old_policy.update({'enabled': False}) + + wait_update = wait_for_status( + self.user_cloud.get_policy_on_cluster, + {'name_or_id': cluster['cluster']['id'], + 'policy_name_or_id': policy['id']}, 'enabled', + False) + + get_new_policy = self.user_cloud.get_policy_on_cluster( + cluster['cluster']["id"], policy['id']) + + get_old_policy['last_op'] = None + get_new_policy['last_op'] = None + + self.assertTrue(policy_update) + self.assertEqual(get_new_policy, get_old_policy) + self.assertTrue(wait_attach) + self.assertTrue(wait_update) + + def test_list_cluster_profiles(self): + profile_name = "test_profile" + spec = { + "properties": { + "flavor": "m1.tiny", + "image": "cirros-0.3.5-x86_64-disk", + "networks": [ + { + "network": "private" + } + ], + "security_groups": [ + "default" + ] + }, + "type": "os.nova.server", + "version": 1.0 + } + + self.addDetail('profile', content.text_content(profile_name)) + # Test that we can create a profile and we get it returned + + profile = self.user_cloud.create_cluster_profile(name=profile_name, + spec=spec) + + self.addCleanup(self.cleanup_profile, profile['id']) + + # Test that we can list profiles + + wait = wait_for_create(self.user_cloud.get_cluster_profile_by_id, + {'name_or_id': profile['id']}) + + get_profile = self.user_cloud.get_cluster_profile_by_id(profile['id']) + + profiles = self.user_cloud.list_cluster_profiles() + self.assertEqual(profiles, [get_profile]) + self.assertTrue(wait) + + def test_get_cluster_profile_by_id(self): + profile_name = "test_profile" + spec = { + "properties": { + "flavor": "m1.tiny", + "image": "cirros-0.3.5-x86_64-disk", + "networks": [ + { + "network": "private" + } + ], + "security_groups": [ + "default" + ] + }, + "type": "os.nova.server", + "version": 1.0 + } + + self.addDetail('profile', content.text_content(profile_name)) + # Test that we can create a profile and we get it returned + + profile = self.user_cloud.create_cluster_profile(name=profile_name, + spec=spec) + + self.addCleanup(self.cleanup_profile, profile['id']) + + profile_get = self.user_cloud.get_cluster_profile_by_id(profile['id']) + + # Test that we get the same profile with the get_profile method + # Format of the created_at variable differs between policy create + # and policy get so if we don't ignore this variable, comparison will + # always fail + profile['created_at'] = 'ignore' + profile_get['created_at'] = 'ignore' + + self.assertEqual(profile_get, profile) + + def test_update_cluster_profile(self): + profile_name = "test_profile" + spec = { + "properties": { + "flavor": "m1.tiny", + "image": "cirros-0.3.5-x86_64-disk", + "networks": [ + { + "network": "private" + } + ], + "security_groups": [ + "default" + ] + }, + "type": "os.nova.server", + "version": 1.0 + } + + self.addDetail('profile', content.text_content(profile_name)) + # Test that we can create a profile and we get it returned + + profile = self.user_cloud.create_cluster_profile(name=profile_name, + spec=spec) + + self.addCleanup(self.cleanup_profile, profile['id']) + + # Test that we can update a field on the profile and only that field + # is updated + + profile_update = self.user_cloud.update_cluster_profile( + profile['id'], new_name='new_profile_name') + self.assertEqual(profile_update['profile']['id'], profile['id']) + self.assertEqual(profile_update['profile']['spec'], profile['spec']) + self.assertEqual(profile_update['profile']['name'], 'new_profile_name') + + def test_delete_cluster_profile(self): + profile_name = "test_profile" + spec = { + "properties": { + "flavor": "m1.tiny", + "image": "cirros-0.3.5-x86_64-disk", + "networks": [ + { + "network": "private" + } + ], + "security_groups": [ + "default" + ] + }, + "type": "os.nova.server", + "version": 1.0 + } + + self.addDetail('profile', content.text_content(profile_name)) + # Test that we can create a profile and we get it returned + + profile = self.user_cloud.create_cluster_profile(name=profile_name, + spec=spec) + + self.addCleanup(self.cleanup_profile, profile['id']) + + # Test that we can delete a profile and get True returned + profile_delete = self.user_cloud.delete_cluster_profile(profile['id']) + self.assertTrue(profile_delete) + + def test_list_cluster_policies(self): + policy_name = 'example_policy' + spec = { + "properties": { + "adjustment": { + "min_step": 1, + "number": 1, + "type": "CHANGE_IN_CAPACITY" + }, + "event": "CLUSTER_SCALE_IN" + }, + "type": "senlin.policy.scaling", + "version": "1.0" + } + + self.addDetail('policy', content.text_content(policy_name)) + + # Test that we can create a policy and we get it returned + + policy = self.user_cloud.create_cluster_policy(name=policy_name, + spec=spec) + + self.addCleanup(self.cleanup_policy, policy['id']) + + policy_get = self.user_cloud.get_cluster_policy_by_id(policy['id']) + + # Test that we can list policies + + policies = self.user_cloud.list_cluster_policies() + + # Format of the created_at variable differs between policy create + # and policy get so if we don't ignore this variable, comparison will + # always fail + policies[0]['created_at'] = 'ignore' + policy_get['created_at'] = 'ignore' + + self.assertEqual(policies, [policy_get]) + + def test_get_cluster_policy_by_id(self): + policy_name = 'example_policy' + spec = { + "properties": { + "adjustment": { + "min_step": 1, + "number": 1, + "type": "CHANGE_IN_CAPACITY" + }, + "event": "CLUSTER_SCALE_IN" + }, + "type": "senlin.policy.scaling", + "version": "1.0" + } + + self.addDetail('policy', content.text_content(policy_name)) + + # Test that we can create a policy and we get it returned + + policy = self.user_cloud.create_cluster_policy(name=policy_name, + spec=spec) + + self.addCleanup(self.cleanup_policy, policy['id']) + + # Test that we get the same policy with the get_policy method + + policy_get = self.user_cloud.get_cluster_policy_by_id(policy['id']) + + # Format of the created_at variable differs between policy create + # and policy get so if we don't ignore this variable, comparison will + # always fail + policy['created_at'] = 'ignore' + policy_get['created_at'] = 'ignore' + + self.assertEqual(policy_get, policy) + + def test_update_cluster_policy(self): + policy_name = 'example_policy' + spec = { + "properties": { + "adjustment": { + "min_step": 1, + "number": 1, + "type": "CHANGE_IN_CAPACITY" + }, + "event": "CLUSTER_SCALE_IN" + }, + "type": "senlin.policy.scaling", + "version": "1.0" + } + + self.addDetail('policy', content.text_content(policy_name)) + + # Test that we can create a policy and we get it returned + + policy = self.user_cloud.create_cluster_policy(name=policy_name, + spec=spec) + + self.addCleanup(self.cleanup_policy, policy['id']) + + # Test that we can update a field on the policy and only that field + # is updated + + policy_update = self.user_cloud.update_cluster_policy( + policy['id'], new_name='new_policy_name') + self.assertEqual(policy_update['policy']['id'], policy['id']) + self.assertEqual(policy_update['policy']['spec'], policy['spec']) + self.assertEqual(policy_update['policy']['name'], 'new_policy_name') + + def test_delete_cluster_policy(self): + policy_name = 'example_policy' + spec = { + "properties": { + "adjustment": { + "min_step": 1, + "number": 1, + "type": "CHANGE_IN_CAPACITY" + }, + "event": "CLUSTER_SCALE_IN" + }, + "type": "senlin.policy.scaling", + "version": "1.0" + } + + self.addDetail('policy', content.text_content(policy_name)) + + # Test that we can create a policy and we get it returned + + policy = self.user_cloud.create_cluster_policy(name=policy_name, + spec=spec) + + self.addCleanup(self.cleanup_policy, policy['id']) + + # Test that we can delete a policy and get True returned + policy_delete = self.user_cloud.delete_cluster_policy( + policy['id']) + self.assertTrue(policy_delete) + + def test_get_cluster_receiver_by_id(self): + profile_name = "test_profile" + spec = { + "properties": { + "flavor": "m1.tiny", + "image": "cirros-0.3.5-x86_64-disk", + "networks": [ + { + "network": "private" + } + ], + "security_groups": [ + "default" + ] + }, + "type": "os.nova.server", + "version": 1.0 + } + + self.addDetail('profile', content.text_content(profile_name)) + # Test that we can create a profile and we get it returned + + profile = self.user_cloud.create_cluster_profile(name=profile_name, + spec=spec) + + self.addCleanup(self.cleanup_profile, profile['id']) + + cluster_name = 'example_cluster' + desired_capacity = 0 + + self.addDetail('cluster', content.text_content(cluster_name)) + + # Test that we can create a cluster and we get it returned + cluster = self.user_cloud.create_cluster( + name=cluster_name, profile=profile, + desired_capacity=desired_capacity) + + self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) + + receiver_name = "example_receiver" + receiver_type = "webhook" + + self.addDetail('receiver', content.text_content(receiver_name)) + + # Test that we can create a receiver and we get it returned + + receiver = self.user_cloud.create_cluster_receiver( + name=receiver_name, receiver_type=receiver_type, + cluster_name_or_id=cluster['cluster']['id'], + action='CLUSTER_SCALE_OUT') + + self.addCleanup(self.cleanup_receiver, receiver['id']) + + # Test that we get the same receiver with the get_receiver method + + receiver_get = self.user_cloud.get_cluster_receiver_by_id( + receiver['id']) + self.assertEqual(receiver_get['id'], receiver["id"]) + + def test_update_cluster_receiver(self): + profile_name = "test_profile" + spec = { + "properties": { + "flavor": "m1.tiny", + "image": "cirros-0.3.5-x86_64-disk", + "networks": [ + { + "network": "private" + } + ], + "security_groups": [ + "default" + ] + }, + "type": "os.nova.server", + "version": 1.0 + } + + self.addDetail('profile', content.text_content(profile_name)) + # Test that we can create a profile and we get it returned + + profile = self.user_cloud.create_cluster_profile(name=profile_name, + spec=spec) + + self.addCleanup(self.cleanup_profile, profile['id']) + + cluster_name = 'example_cluster' + desired_capacity = 0 + + self.addDetail('cluster', content.text_content(cluster_name)) + + # Test that we can create a cluster and we get it returned + cluster = self.user_cloud.create_cluster( + name=cluster_name, profile=profile, + desired_capacity=desired_capacity) + + self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) + + receiver_name = "example_receiver" + receiver_type = "webhook" + + self.addDetail('receiver', content.text_content(receiver_name)) + + # Test that we can create a receiver and we get it returned + + receiver = self.user_cloud.create_cluster_receiver( + name=receiver_name, receiver_type=receiver_type, + cluster_name_or_id=cluster['cluster']['id'], + action='CLUSTER_SCALE_OUT') + + self.addCleanup(self.cleanup_receiver, receiver['id']) + + # Test that we can update a field on the receiver and only that field + # is updated + + receiver_update = self.user_cloud.update_cluster_receiver( + receiver['id'], new_name='new_receiver_name') + self.assertEqual(receiver_update['receiver']['id'], receiver['id']) + self.assertEqual(receiver_update['receiver']['type'], receiver['type']) + self.assertEqual(receiver_update['receiver']['cluster_id'], + receiver['cluster_id']) + self.assertEqual(receiver_update['receiver']['name'], + 'new_receiver_name') + + def cleanup_profile(self, name): + time.sleep(5) + for cluster in self.user_cloud.list_clusters(): + if name == cluster["profile_id"]: + self.user_cloud.delete_cluster(cluster["id"]) + self.user_cloud.delete_cluster_profile(name) + + def cleanup_cluster(self, name): + self.user_cloud.delete_cluster(name) + + def cleanup_policy(self, name, cluster_name=None): + if cluster_name is not None: + cluster = self.user_cloud.get_cluster_by_id(cluster_name) + policy = self.user_cloud.get_cluster_policy_by_id(name) + policy_status = \ + self.user_cloud.get_cluster_by_id(cluster['id'])['policies'] + if policy_status != []: + self.user_cloud.detach_policy_from_cluster(cluster, policy) + self.user_cloud.delete_cluster_policy(name) + + def cleanup_receiver(self, name): + self.user_cloud.delete_cluster_receiver(name) diff --git a/shade/tests/unit/fixtures/clustering.json b/shade/tests/unit/fixtures/clustering.json new file mode 100644 index 000000000..228399c05 --- /dev/null +++ b/shade/tests/unit/fixtures/clustering.json @@ -0,0 +1,27 @@ +{ + "versions": [ + { + "id": "1.0", + "links": [ + { + "href": "/v1/", + "rel": "self" + }, + { + "href": "https://clustering.example.com/api-ref/clustering", + "rel": "help" + } + ], + "max_version": "1.7", + "media-types": [ + { + "base": "application/json", + "type": "application/vnd.openstack.clustering-v1+json" + } + ], + "min_version": "1.0", + "status": "CURRENT", + "updated": "2016-01-18T00:00:00Z" + } + ] +} diff --git a/shade/tests/unit/test_clustering.py b/shade/tests/unit/test_clustering.py new file mode 100644 index 000000000..42520a427 --- /dev/null +++ b/shade/tests/unit/test_clustering.py @@ -0,0 +1,658 @@ +# 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 testtools + +import shade +from shade.tests.unit import base + + +CLUSTERING_DICT = { + 'name': 'fake-name', + 'profile_id': '1', + 'desired_capacity': 1, + 'config': 'fake-config', + 'max_size': 1, + 'min_size': 1, + 'timeout': 100, + 'metadata': {} +} + +PROFILE_DICT = { + 'name': 'fake-profile-name', + 'spec': {}, + 'metadata': {} +} + +POLICY_DICT = { + 'name': 'fake-profile-name', + 'spec': {}, +} + +RECEIVER_DICT = { + 'action': 'FAKE_CLUSTER_ACTION', + 'cluster_id': 'fake-cluster-id', + 'name': 'fake-receiver-name', + 'params': {}, + 'type': 'webhook' +} + +NEW_CLUSTERING_DICT = copy.copy(CLUSTERING_DICT) +NEW_CLUSTERING_DICT['id'] = '1' +NEW_PROFILE_DICT = copy.copy(PROFILE_DICT) +NEW_PROFILE_DICT['id'] = '1' +NEW_POLICY_DICT = copy.copy(POLICY_DICT) +NEW_POLICY_DICT['id'] = '1' +NEW_RECEIVER_DICT = copy.copy(RECEIVER_DICT) +NEW_RECEIVER_DICT['id'] = '1' + + +class TestClustering(base.RequestsMockTestCase): + + def assertAreInstances(self, elements, elem_type): + for e in elements: + self.assertIsInstance(e, elem_type) + + def setUp(self): + super(TestClustering, self).setUp() + self.use_senlin() + + def test_create_cluster(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'profiles', '1']), + json={ + "profiles": [NEW_PROFILE_DICT]}), + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'profiles']), + json={ + "profiles": [NEW_PROFILE_DICT]}), + dict(method='POST', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'clusters']), + json=NEW_CLUSTERING_DICT) + ]) + profile = self.cloud.get_cluster_profile_by_id(NEW_PROFILE_DICT['id']) + c = self.cloud.create_cluster( + name=CLUSTERING_DICT['name'], + desired_capacity=CLUSTERING_DICT['desired_capacity'], + profile=profile, + config=CLUSTERING_DICT['config'], + max_size=CLUSTERING_DICT['max_size'], + min_size=CLUSTERING_DICT['min_size'], + metadata=CLUSTERING_DICT['metadata'], + timeout=CLUSTERING_DICT['timeout']) + + self.assertEqual(NEW_CLUSTERING_DICT, c) + self.assert_calls() + + def test_create_cluster_exception(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'profiles', '1']), + json={ + "profiles": [NEW_PROFILE_DICT]}), + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'profiles']), + json={ + "profiles": [NEW_PROFILE_DICT]}), + dict(method='POST', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'clusters']), + status_code=500) + ]) + profile = self.cloud.get_cluster_profile_by_id(NEW_PROFILE_DICT['id']) + with testtools.ExpectedException( + shade.exc.OpenStackCloudHTTPError, + "Error creating cluster fake-name.*"): + self.cloud.create_cluster(name='fake-name', profile=profile) + self.assert_calls() + + def test_get_cluster_by_id(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'clusters', '1']), + json={ + "cluster": NEW_CLUSTERING_DICT}) + ]) + cluster = self.cloud.get_cluster_by_id('1') + self.assertEqual(cluster['id'], '1') + self.assert_calls() + + def test_get_cluster_not_found_returns_false(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'clusters', + 'no-cluster']), + status_code=404) + ]) + c = self.cloud.get_cluster_by_id('no-cluster') + self.assertFalse(c) + self.assert_calls() + + def test_update_cluster(self): + new_max_size = 5 + updated_cluster = copy.copy(NEW_CLUSTERING_DICT) + updated_cluster['max_size'] = new_max_size + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'clusters', '1']), + json={ + "cluster": NEW_CLUSTERING_DICT}), + dict(method='PATCH', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'clusters', '1']), + json=updated_cluster, + ) + ]) + cluster = self.cloud.get_cluster_by_id('1') + c = self.cloud.update_cluster(cluster, new_max_size) + self.assertEqual(updated_cluster, c) + self.assert_calls() + + def test_delete_cluster(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'clusters', '1']), + json={ + "cluster": NEW_CLUSTERING_DICT}), + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'clusters', '1', + 'policies']), + json={"cluster_policies": []}), + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'receivers']), + json={"receivers": []}), + dict(method='DELETE', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'clusters', '1']), + json=NEW_CLUSTERING_DICT) + ]) + self.assertTrue(self.cloud.delete_cluster('1')) + self.assert_calls() + + def test_list_clusters(self): + clusters = {'clusters': [NEW_CLUSTERING_DICT]} + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'clusters']), + json=clusters) + ]) + c = self.cloud.list_clusters() + + self.assertIsInstance(c, list) + self.assertAreInstances(c, dict) + + self.assert_calls() + + def test_attach_policy_to_cluster(self): + policy = { + 'policy_id': '1', + 'enabled': 'true' + } + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'clusters', '1']), + json={ + "cluster": NEW_CLUSTERING_DICT}), + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'policies', '1']), + json={ + "policy": NEW_POLICY_DICT}), + dict(method='POST', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'clusters', '1', + 'actions']), + json={'policy_attach': policy}) + ]) + cluster = self.cloud.get_cluster_by_id('1') + policy = self.cloud.get_cluster_policy_by_id('1') + p = self.cloud.attach_policy_to_cluster(cluster, policy, 'true') + self.assertTrue(p) + self.assert_calls() + + def test_detach_policy_from_cluster(self): + updated_cluster = copy.copy(NEW_CLUSTERING_DICT) + updated_cluster['policies'] = ['1'] + detached_cluster = copy.copy(NEW_CLUSTERING_DICT) + detached_cluster['policies'] = [] + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'clusters', '1']), + json={ + "cluster": NEW_CLUSTERING_DICT}), + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'policies', '1']), + json={ + "policy": NEW_POLICY_DICT}), + dict(method='POST', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'clusters', '1', + 'actions']), + json={'policy_detach': {'policy_id': '1'}}), + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'clusters', '1']), + json={ + "cluster": updated_cluster}), + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'clusters', '1']), + json={ + "cluster": detached_cluster}), + ]) + cluster = self.cloud.get_cluster_by_id('1') + policy = self.cloud.get_cluster_policy_by_id('1') + p = self.cloud.detach_policy_from_cluster(cluster, policy, wait=True) + self.assertTrue(p) + self.assert_calls() + + def test_get_policy_on_cluster_by_id(self): + cluster_policy = { + "cluster_id": "1", + "cluster_name": "cluster1", + "enabled": True, + "id": "1", + "policy_id": "1", + "policy_name": "policy1", + "policy_type": "senlin.policy.deletion-1.0" + } + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'clusters', '1', + 'policies', '1']), + json={ + "cluster_policy": cluster_policy}) + ]) + policy = self.cloud.get_policy_on_cluster('1', '1') + self.assertEqual(policy['cluster_id'], '1') + self.assert_calls() + + def test_get_policy_on_cluster_not_found_returns_false(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'clusters', '1', + 'policies', 'no-policy']), + status_code=404) + ]) + p = self.cloud.get_policy_on_cluster('1', 'no-policy') + self.assertFalse(p) + self.assert_calls() + + def test_update_policy_on_cluster(self): + policy = { + 'policy_id': '1', + 'enabled': 'true' + } + updated_cluster = copy.copy(NEW_CLUSTERING_DICT) + updated_cluster['policies'] = policy + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'clusters', '1']), + json={ + "cluster": NEW_CLUSTERING_DICT}), + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'policies', + '1']), + json={ + "policy": NEW_POLICY_DICT}), + dict(method='POST', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'clusters', '1', + 'actions']), + json={'policies': []}) + ]) + cluster = self.cloud.get_cluster_by_id('1') + policy = self.cloud.get_cluster_policy_by_id('1') + p = self.cloud.update_policy_on_cluster(cluster, policy, True) + self.assertTrue(p) + self.assert_calls() + + def test_get_policy_on_cluster(self): + cluster_policy = { + 'cluster_id': '1', + 'cluster_name': 'cluster1', + 'enabled': 'true', + 'id': '1', + 'policy_id': '1', + 'policy_name': 'policy1', + 'policy_type': 'type' + } + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'clusters', '1', + 'policies', '1']), + json={ + "cluster_policy": cluster_policy}) + ]) + get_policy = self.cloud.get_policy_on_cluster('1', '1') + self.assertEqual(get_policy, cluster_policy) + self.assert_calls() + + def test_create_cluster_profile(self): + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'profiles']), + json={'profile': NEW_PROFILE_DICT}) + ]) + p = self.cloud.create_cluster_profile('fake-profile-name', {}) + + self.assertEqual(NEW_PROFILE_DICT, p) + self.assert_calls() + + def test_create_cluster_profile_exception(self): + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'profiles']), + status_code=500) + ]) + with testtools.ExpectedException( + shade.exc.OpenStackCloudHTTPError, + "Error creating profile fake-profile-name.*"): + self.cloud.create_cluster_profile('fake-profile-name', {}) + self.assert_calls() + + def test_list_cluster_profiles(self): + profiles = {'profiles': [NEW_PROFILE_DICT]} + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'profiles']), + json=profiles) + ]) + p = self.cloud.list_cluster_profiles() + + self.assertIsInstance(p, list) + self.assertAreInstances(p, dict) + + self.assert_calls() + + def test_get_cluster_profile_by_id(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'profiles', '1']), + json={ + "profile": NEW_PROFILE_DICT}) + ]) + p = self.cloud.get_cluster_profile_by_id('1') + self.assertEqual(p['id'], '1') + self.assert_calls() + + def test_get_cluster_profile_not_found_returns_false(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'profiles', + 'no-profile']), + status_code=404) + ]) + p = self.cloud.get_cluster_profile_by_id('no-profile') + self.assertFalse(p) + self.assert_calls() + + def test_update_cluster_profile(self): + new_name = "new-name" + updated_profile = copy.copy(NEW_PROFILE_DICT) + updated_profile['name'] = new_name + self.register_uris([ + dict(method='PATCH', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'profiles', '1']), + json=updated_profile, + ) + ]) + p = self.cloud.update_cluster_profile('1', new_name=new_name) + self.assertEqual(updated_profile, p) + self.assert_calls() + + def test_delete_cluster_profile(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'profiles', '1']), + json={ + "profile": NEW_PROFILE_DICT}), + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'clusters']), + json={}), + dict(method='DELETE', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'profiles', '1']), + json=NEW_PROFILE_DICT) + ]) + profile = self.cloud.get_cluster_profile_by_id('1') + self.assertTrue(self.cloud.delete_cluster_profile(profile)) + self.assert_calls() + + def test_create_cluster_policy(self): + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'policies']), + json={'policy': NEW_POLICY_DICT}) + ]) + p = self.cloud.create_cluster_policy('fake-policy-name', {}) + + self.assertEqual(NEW_POLICY_DICT, p) + self.assert_calls() + + def test_create_cluster_policy_exception(self): + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'policies']), + status_code=500) + ]) + with testtools.ExpectedException( + shade.exc.OpenStackCloudHTTPError, + "Error creating policy fake-policy-name.*"): + self.cloud.create_cluster_policy('fake-policy-name', {}) + self.assert_calls() + + def test_list_cluster_policies(self): + policies = {'policies': [NEW_POLICY_DICT]} + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'policies']), + json=policies) + ]) + p = self.cloud.list_cluster_policies() + + self.assertIsInstance(p, list) + self.assertAreInstances(p, dict) + + self.assert_calls() + + def test_get_cluster_policy_by_id(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'policies', '1']), + json={ + "policy": NEW_POLICY_DICT}) + ]) + p = self.cloud.get_cluster_policy_by_id('1') + self.assertEqual(p['id'], '1') + self.assert_calls() + + def test_get_cluster_policy_not_found_returns_false(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'policies', + 'no-policy']), + status_code=404) + ]) + p = self.cloud.get_cluster_policy_by_id('no-policy') + self.assertFalse(p) + self.assert_calls() + + def test_update_cluster_policy(self): + new_name = "new-name" + updated_policy = copy.copy(NEW_POLICY_DICT) + updated_policy['name'] = new_name + self.register_uris([ + dict(method='PATCH', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'policies', '1']), + json=updated_policy, + ) + ]) + p = self.cloud.update_cluster_policy('1', new_name=new_name) + self.assertEqual(updated_policy, p) + self.assert_calls() + + def test_delete_cluster_policy(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'policies', '1']), + json={ + "policy": NEW_POLICY_DICT}), + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'clusters']), + json={}), + dict(method='DELETE', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'policies', '1']), + json=NEW_POLICY_DICT) + ]) + self.assertTrue(self.cloud.delete_cluster_policy('1')) + self.assert_calls() + + def test_create_cluster_receiver(self): + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'receivers']), + json={'receiver': NEW_RECEIVER_DICT}) + ]) + r = self.cloud.create_cluster_receiver('fake-receiver-name', {}) + + self.assertEqual(NEW_RECEIVER_DICT, r) + self.assert_calls() + + def test_create_cluster_receiver_exception(self): + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'receivers']), + status_code=500) + ]) + with testtools.ExpectedException( + shade.exc.OpenStackCloudHTTPError, + "Error creating receiver fake-receiver-name.*"): + self.cloud.create_cluster_receiver('fake-receiver-name', {}) + self.assert_calls() + + def test_list_cluster_receivers(self): + receivers = {'receivers': [NEW_RECEIVER_DICT]} + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'receivers']), + json=receivers) + ]) + r = self.cloud.list_cluster_receivers() + + self.assertIsInstance(r, list) + self.assertAreInstances(r, dict) + + self.assert_calls() + + def test_get_cluster_receiver_by_id(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'receivers', '1']), + json={ + "receiver": NEW_RECEIVER_DICT}) + ]) + r = self.cloud.get_cluster_receiver_by_id('1') + self.assertEqual(r['id'], '1') + self.assert_calls() + + def test_get_cluster_receiver_not_found_returns_false(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'receivers', + 'no-receiver']), + json={'receivers': []}) + ]) + p = self.cloud.get_cluster_receiver_by_id('no-receiver') + self.assertFalse(p) + self.assert_calls() + + def test_update_cluster_receiver(self): + new_name = "new-name" + updated_receiver = copy.copy(NEW_RECEIVER_DICT) + updated_receiver['name'] = new_name + self.register_uris([ + dict(method='PATCH', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'receivers', '1']), + json=updated_receiver, + ) + ]) + r = self.cloud.update_cluster_receiver('1', new_name=new_name) + self.assertEqual(updated_receiver, r) + self.assert_calls() + + def test_delete_cluster_receiver(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'receivers']), + json={ + "receivers": [NEW_RECEIVER_DICT]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'receivers', '1']), + json=NEW_RECEIVER_DICT), + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'receivers', '1']), + json={}), + ]) + self.assertTrue(self.cloud.delete_cluster_receiver('1', wait=True)) + self.assert_calls() From 13849fea9d5bae8b41b83fa52a7cbd6631e35106 Mon Sep 17 00:00:00 2001 From: lvxianguo Date: Fri, 22 Jun 2018 17:09:52 +0800 Subject: [PATCH 2100/3836] fix misspelling of 'server' Change-Id: If4ed7e468ec4225af610b12976cf4c798446ce83 --- openstack/compute/v2/limits.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/compute/v2/limits.py b/openstack/compute/v2/limits.py index 645d6104f..090f312ac 100644 --- a/openstack/compute/v2/limits.py +++ b/openstack/compute/v2/limits.py @@ -28,7 +28,7 @@ class AbsoluteLimits(resource.Resource): security_groups = resource.Body("maxSecurityGroups") #: The amount of security groups currently in use. security_groups_used = resource.Body("totalSecurityGroupsUsed") - #: The number of key-value pairs that can be set as sever metadata. + #: The number of key-value pairs that can be set as server metadata. server_meta = resource.Body("maxServerMeta") #: The maximum amount of cores. total_cores = resource.Body("maxTotalCores") From 41a7c9f55a362a3f927b4df6f1297ea727d1d27a Mon Sep 17 00:00:00 2001 From: Mohammed Naser Date: Fri, 19 Jan 2018 19:32:00 -0800 Subject: [PATCH 2101/3836] Switch to providing created_at field for servers All of the current models return the creation date in created_at with the exception of servers which are currently returned with a created field instead. This patch addresses this and gives the ability to get access to both. Change-Id: Ifbd59834080eea62dead19d18a9a11a851a4ce2e --- doc/source/user/model.rst | 1 + openstack/cloud/_normalize.py | 5 +++++ openstack/tests/unit/cloud/test_normalize.py | 2 ++ .../notes/switch-nova-to-created_at-45b7b50af6a2d59e.yaml | 5 +++++ 4 files changed, 13 insertions(+) create mode 100644 releasenotes/notes/switch-nova-to-created_at-45b7b50af6a2d59e.yaml diff --git a/doc/source/user/model.rst b/doc/source/user/model.rst index ebfc3af97..ce857d315 100644 --- a/doc/source/user/model.rst +++ b/doc/source/user/model.rst @@ -208,6 +208,7 @@ A Server from Nova accessIPv6=str(), addresses=dict(), # string, list(Address) created=str(), + created_at=str(), key_name=str(), metadata=dict(), # string, string private_v4=str(), diff --git a/openstack/cloud/_normalize.py b/openstack/cloud/_normalize.py index a7fa4f498..ad4d54f3f 100644 --- a/openstack/cloud/_normalize.py +++ b/openstack/cloud/_normalize.py @@ -503,6 +503,11 @@ def _normalize_server(self, server): # Protect against security_groups being None ret['security_groups'] = server.pop('security_groups', None) or [] + # NOTE(mnaser): The Nova API returns the creation date in `created` + # however the Shade contract returns `created_at` for + # all resources. + ret['created_at'] = server.get('created') + for field in _SERVER_FIELDS: ret[field] = server.pop(field, None) if not ret['networks']: diff --git a/openstack/tests/unit/cloud/test_normalize.py b/openstack/tests/unit/cloud/test_normalize.py index 7265930b0..890e79f4f 100644 --- a/openstack/tests/unit/cloud/test_normalize.py +++ b/openstack/tests/unit/cloud/test_normalize.py @@ -575,6 +575,7 @@ def test_normalize_servers_strict(self): u'version': 4}]}, 'adminPass': None, 'created': u'2015-08-01T19:52:16Z', + 'created_at': u'2015-08-01T19:52:16Z', 'disk_config': u'MANUAL', 'flavor': {u'id': u'bbcb7eb5-5c8d-498f-9d7e-307c575d3566'}, 'has_config_drive': True, @@ -644,6 +645,7 @@ def test_normalize_servers_normal(self): 'cloud': '_test_cloud_', 'config_drive': u'True', 'created': u'2015-08-01T19:52:16Z', + 'created_at': u'2015-08-01T19:52:16Z', 'disk_config': u'MANUAL', 'flavor': {u'id': u'bbcb7eb5-5c8d-498f-9d7e-307c575d3566'}, 'has_config_drive': True, diff --git a/releasenotes/notes/switch-nova-to-created_at-45b7b50af6a2d59e.yaml b/releasenotes/notes/switch-nova-to-created_at-45b7b50af6a2d59e.yaml new file mode 100644 index 000000000..68cf0a5e3 --- /dev/null +++ b/releasenotes/notes/switch-nova-to-created_at-45b7b50af6a2d59e.yaml @@ -0,0 +1,5 @@ +--- +features: + - The `created` field which was returned by the Nova API is now returned as + `created_at` as well when not using strict mode for consistency with other + models. From bf7fbc4b1f3a7a38314f5bae56c61cd6b3ed05dc Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 24 Jun 2018 09:09:48 -0500 Subject: [PATCH 2102/3836] Move clustering additions from shade directories In the cherry-pick from shade, the file locations weren't updated. Also, several of the tests didn't work, which we didn't catch because they weren't run when we gated the original change. Fix the tests. Finally, the functional tests don't work, so disable them. The followup patch will re-enable them so we can work through fixing them. Change-Id: I4cf8639aaacedf64dcbd3bca1467a0d02d88350e --- openstack/cloud/openstackcloud.py | 12 ++--- openstack/config/defaults.json | 1 + .../functional/cloud}/test_clustering.py | 5 +-- .../tests/unit/cloud}/test_clustering.py | 45 ++++++++++++++----- .../tests/unit/fixtures/clustering.json | 0 ...added-senlin-support-1eb4e47c31258f66.yaml | 0 6 files changed, 44 insertions(+), 19 deletions(-) rename {shade/tests/functional => openstack/tests/functional/cloud}/test_clustering.py (99%) rename {shade/tests/unit => openstack/tests/unit/cloud}/test_clustering.py (94%) rename {shade => openstack}/tests/unit/fixtures/clustering.json (100%) rename {shade/releasenotes => releasenotes}/notes/added-senlin-support-1eb4e47c31258f66.yaml (100%) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 7650abcd0..03f820628 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -11585,7 +11585,7 @@ def detach_policy_from_cluster( value = [] - for count in _utils._iterate_timeout( + for count in utils.iterate_timeout( timeout, "Timeout waiting for cluster policy to detach"): # TODO(bjjohnson) This logic will wait until there are no policies. @@ -11664,7 +11664,7 @@ def create_cluster_profile(self, name, spec, metadata=None): return self._get_and_munchify('profile', data) def set_cluster_profile_metadata(self, name_or_id, metadata): - profile = self.get_profile(name_or_id) + profile = self.get_cluster_profile(name_or_id) if not profile: raise exc.OpenStackCloudException( 'Invalid Profile {profile}'.format(profile=name_or_id)) @@ -11723,7 +11723,7 @@ def delete_cluster_profile(self, name_or_id): return True def update_cluster_profile(self, name_or_id, metadata=None, new_name=None): - old_profile = self.get_profile(name_or_id) + old_profile = self.get_cluster_profile(name_or_id) if not old_profile: raise exc.OpenStackCloudException( 'Invalid Profile {profile}'.format(profile=name_or_id)) @@ -11784,7 +11784,7 @@ def get_cluster_policy_by_id(self, policy_id): def get_cluster_policy(self, name_or_id, filters=None): return _utils._get_entity( - self, 'cluster_policy', name_or_id, filters) + self, 'cluster_policie', name_or_id, filters) def delete_cluster_policy(self, name_or_id): policy = self.get_cluster_policy_by_id(name_or_id) @@ -11807,7 +11807,7 @@ def delete_cluster_policy(self, name_or_id): return True def update_cluster_policy(self, name_or_id, new_name): - old_policy = self.get_policy(name_or_id) + old_policy = self.get_cluster_policy(name_or_id) if not old_policy: raise exc.OpenStackCloudException( 'Invalid Policy {policy}'.format(policy=name_or_id)) @@ -11895,7 +11895,7 @@ def delete_cluster_receiver(self, name_or_id, wait=False, timeout=3600): if not wait: return True - for count in _utils._iterate_timeout( + for count in utils.iterate_timeout( timeout, "Timeout waiting for cluster receiver to delete"): receiver = self.get_cluster_receiver_by_id(receiver_id) diff --git a/openstack/config/defaults.json b/openstack/config/defaults.json index 7c6296594..c17cc041c 100644 --- a/openstack/config/defaults.json +++ b/openstack/config/defaults.json @@ -3,6 +3,7 @@ "auth_type": "password", "baremetal_api_version": "1", "block_storage_api_version": "2", + "clustering_api_version": "1", "container_api_version": "1", "container_infra_api_version": "1", "compute_api_version": "2", diff --git a/shade/tests/functional/test_clustering.py b/openstack/tests/functional/cloud/test_clustering.py similarity index 99% rename from shade/tests/functional/test_clustering.py rename to openstack/tests/functional/cloud/test_clustering.py index 960245da5..5e401b1d4 100644 --- a/shade/tests/functional/test_clustering.py +++ b/openstack/tests/functional/cloud/test_clustering.py @@ -19,7 +19,7 @@ from testtools import content -from shade.tests.functional import base +from openstack.tests.functional.cloud import base import time @@ -109,8 +109,7 @@ class TestClustering(base.BaseFunctionalTestCase): def setUp(self): super(TestClustering, self).setUp() - if not self.user_cloud.has_service('clustering'): - self.skipTest('clustering service not supported by cloud') + self.skipTest('clustering service not supported by cloud') def test_create_profile(self): profile_name = "test_profile" diff --git a/shade/tests/unit/test_clustering.py b/openstack/tests/unit/cloud/test_clustering.py similarity index 94% rename from shade/tests/unit/test_clustering.py rename to openstack/tests/unit/cloud/test_clustering.py index 42520a427..89b6c7326 100644 --- a/shade/tests/unit/test_clustering.py +++ b/openstack/tests/unit/cloud/test_clustering.py @@ -13,8 +13,8 @@ import copy import testtools -import shade -from shade.tests.unit import base +from openstack.cloud import exc +from openstack.tests.unit import base CLUSTERING_DICT = { @@ -57,7 +57,7 @@ NEW_RECEIVER_DICT['id'] = '1' -class TestClustering(base.RequestsMockTestCase): +class TestClustering(base.TestCase): def assertAreInstances(self, elements, elem_type): for e in elements: @@ -117,7 +117,7 @@ def test_create_cluster_exception(self): ]) profile = self.cloud.get_cluster_profile_by_id(NEW_PROFILE_DICT['id']) with testtools.ExpectedException( - shade.exc.OpenStackCloudHTTPError, + exc.OpenStackCloudHTTPError, "Error creating cluster fake-name.*"): self.cloud.create_cluster(name='fake-name', profile=profile) self.assert_calls() @@ -171,9 +171,9 @@ def test_delete_cluster(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'clusters', '1']), + 'clustering', 'public', append=['v1', 'clusters']), json={ - "cluster": NEW_CLUSTERING_DICT}), + "clusters": [NEW_CLUSTERING_DICT]}), dict(method='GET', uri=self.get_mock_url( 'clustering', 'public', append=['v1', 'clusters', '1', @@ -382,7 +382,7 @@ def test_create_cluster_profile_exception(self): status_code=500) ]) with testtools.ExpectedException( - shade.exc.OpenStackCloudHTTPError, + exc.OpenStackCloudHTTPError, "Error creating profile fake-profile-name.*"): self.cloud.create_cluster_profile('fake-profile-name', {}) self.assert_calls() @@ -431,6 +431,11 @@ def test_update_cluster_profile(self): updated_profile = copy.copy(NEW_PROFILE_DICT) updated_profile['name'] = new_name self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'profiles']), + json={ + "profiles": [NEW_PROFILE_DICT]}), dict(method='PATCH', uri=self.get_mock_url( 'clustering', 'public', append=['v1', 'profiles', '1']), @@ -481,7 +486,7 @@ def test_create_cluster_policy_exception(self): status_code=500) ]) with testtools.ExpectedException( - shade.exc.OpenStackCloudHTTPError, + exc.OpenStackCloudHTTPError, "Error creating policy fake-policy-name.*"): self.cloud.create_cluster_policy('fake-policy-name', {}) self.assert_calls() @@ -530,6 +535,11 @@ def test_update_cluster_policy(self): updated_policy = copy.copy(NEW_POLICY_DICT) updated_policy['name'] = new_name self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'policies']), + json={ + "policies": [NEW_POLICY_DICT]}), dict(method='PATCH', uri=self.get_mock_url( 'clustering', 'public', append=['v1', 'policies', '1']), @@ -560,7 +570,12 @@ def test_delete_cluster_policy(self): self.assert_calls() def test_create_cluster_receiver(self): + clusters = {'clusters': [NEW_CLUSTERING_DICT]} self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'clusters']), + json=clusters), dict(method='POST', uri=self.get_mock_url( 'clustering', 'public', append=['v1', 'receivers']), @@ -572,14 +587,19 @@ def test_create_cluster_receiver(self): self.assert_calls() def test_create_cluster_receiver_exception(self): + clusters = {'clusters': [NEW_CLUSTERING_DICT]} self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'clusters']), + json=clusters), dict(method='POST', uri=self.get_mock_url( 'clustering', 'public', append=['v1', 'receivers']), - status_code=500) + status_code=500), ]) with testtools.ExpectedException( - shade.exc.OpenStackCloudHTTPError, + exc.OpenStackCloudHTTPError, "Error creating receiver fake-receiver-name.*"): self.cloud.create_cluster_receiver('fake-receiver-name', {}) self.assert_calls() @@ -628,6 +648,11 @@ def test_update_cluster_receiver(self): updated_receiver = copy.copy(NEW_RECEIVER_DICT) updated_receiver['name'] = new_name self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'clustering', 'public', append=['v1', 'receivers']), + json={ + "receivers": [NEW_RECEIVER_DICT]}), dict(method='PATCH', uri=self.get_mock_url( 'clustering', 'public', append=['v1', 'receivers', '1']), diff --git a/shade/tests/unit/fixtures/clustering.json b/openstack/tests/unit/fixtures/clustering.json similarity index 100% rename from shade/tests/unit/fixtures/clustering.json rename to openstack/tests/unit/fixtures/clustering.json diff --git a/shade/releasenotes/notes/added-senlin-support-1eb4e47c31258f66.yaml b/releasenotes/notes/added-senlin-support-1eb4e47c31258f66.yaml similarity index 100% rename from shade/releasenotes/notes/added-senlin-support-1eb4e47c31258f66.yaml rename to releasenotes/notes/added-senlin-support-1eb4e47c31258f66.yaml From c3c3ea2ee28f224d9fdecbfe47c0b5b46bc70a83 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 24 Jun 2018 08:37:24 -0500 Subject: [PATCH 2103/3836] Convert domain params tests to requests_mock We still have places using client method mocking. Remove them. Change-Id: I34fbc19aca4b6514215ed497117c635fa810473d --- .../tests/unit/cloud/test_domain_params.py | 81 ++++++++++--------- 1 file changed, 43 insertions(+), 38 deletions(-) diff --git a/openstack/tests/unit/cloud/test_domain_params.py b/openstack/tests/unit/cloud/test_domain_params.py index 1c5395c7e..185a75ac3 100644 --- a/openstack/tests/unit/cloud/test_domain_params.py +++ b/openstack/tests/unit/cloud/test_domain_params.py @@ -10,64 +10,69 @@ # License for the specific language governing permissions and limitations # under the License. -import mock - -import munch - -import openstack.cloud from openstack.cloud import exc from openstack.tests.unit import base class TestDomainParams(base.TestCase): - @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') - @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_project') - def test_identity_params_v3(self, mock_get_project, - mock_is_client_version): - mock_get_project.return_value = munch.Munch(id=1234) - mock_is_client_version.return_value = True + def test_identity_params_v3(self): + project_data = self._get_project_data(v3=True) + self.register_uris([ + dict(method='GET', + uri='https://identity.example.com/v3/projects', + json=dict(projects=[project_data.json_response['project']])) + ]) - ret = self.cloud._get_identity_params(domain_id='5678', project='bar') + ret = self.cloud._get_identity_params( + domain_id='5678', project=project_data.project_name) self.assertIn('default_project_id', ret) - self.assertEqual(ret['default_project_id'], 1234) + self.assertEqual(ret['default_project_id'], project_data.project_id) self.assertIn('domain_id', ret) self.assertEqual(ret['domain_id'], '5678') - @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') - @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_project') - def test_identity_params_v3_no_domain( - self, mock_get_project, mock_is_client_version): - mock_get_project.return_value = munch.Munch(id=1234) - mock_is_client_version.return_value = True + self.assert_calls() + + def test_identity_params_v3_no_domain(self): + project_data = self._get_project_data(v3=True) self.assertRaises( exc.OpenStackCloudException, self.cloud._get_identity_params, - domain_id=None, project='bar') + domain_id=None, project=project_data.project_name) - @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') - @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_project') - def test_identity_params_v2(self, mock_get_project, - mock_is_client_version): - mock_get_project.return_value = munch.Munch(id=1234) - mock_is_client_version.return_value = False + self.assert_calls() - ret = self.cloud._get_identity_params(domain_id='foo', project='bar') + def test_identity_params_v2(self): + self.use_keystone_v2() + project_data = self._get_project_data(v3=False) + self.register_uris([ + dict(method='GET', + uri='https://identity.example.com/v2.0/tenants', + json=dict(tenants=[project_data.json_response['tenant']])) + ]) + + ret = self.cloud._get_identity_params( + domain_id='foo', project=project_data.project_name) self.assertIn('tenant_id', ret) - self.assertEqual(ret['tenant_id'], 1234) + self.assertEqual(ret['tenant_id'], project_data.project_id) self.assertNotIn('domain', ret) - @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') - @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_project') - def test_identity_params_v2_no_domain(self, mock_get_project, - mock_is_client_version): - mock_get_project.return_value = munch.Munch(id=1234) - mock_is_client_version.return_value = False + self.assert_calls() + + def test_identity_params_v2_no_domain(self): + self.use_keystone_v2() + project_data = self._get_project_data(v3=False) + self.register_uris([ + dict(method='GET', + uri='https://identity.example.com/v2.0/tenants', + json=dict(tenants=[project_data.json_response['tenant']])) + ]) - ret = self.cloud._get_identity_params(domain_id=None, project='bar') - api_calls = [mock.call('identity', 3), mock.call('identity', 3)] - mock_is_client_version.assert_has_calls(api_calls) + ret = self.cloud._get_identity_params( + domain_id=None, project=project_data.project_name) self.assertIn('tenant_id', ret) - self.assertEqual(ret['tenant_id'], 1234) + self.assertEqual(ret['tenant_id'], project_data.project_id) self.assertNotIn('domain', ret) + + self.assert_calls() From ac9d4c1eb1053117d8ff7a034a17cfd9fef7e2c0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 24 Jun 2018 10:13:32 -0500 Subject: [PATCH 2104/3836] Convert test_caching to requests-mock Remove the last bits in test_caching that were mocking clients directly. Change-Id: I4477993ca2d87bd9bfa4542f7884e41182ee2040 --- openstack/tests/unit/cloud/test_caching.py | 184 ++++++++++++++------- 1 file changed, 128 insertions(+), 56 deletions(-) diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py index 012b16e79..9e10fa442 100644 --- a/openstack/tests/unit/cloud/test_caching.py +++ b/openstack/tests/unit/cloud/test_caching.py @@ -12,9 +12,8 @@ import concurrent import time -import mock -import munch import testtools +from testscenarios import load_tests_apply_scenarios as load_tests # noqa import openstack import openstack.cloud @@ -475,74 +474,147 @@ def test_list_images(self): self.assert_calls() - @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') - def test_list_images_ignores_unsteady_status(self, mock_image_client): - steady_image = munch.Munch(id='68', name='Jagr', status='active') - for status in ('queued', 'saving', 'pending_delete'): - active_image = munch.Munch( - id=self.getUniqueString(), name=self.getUniqueString(), - status=status) - mock_image_client.get.return_value = [active_image] - - self.assertEqual( - self._munch_images(active_image), - self.cloud.list_images()) - mock_image_client.get.return_value = [ - active_image, steady_image] - # Should expect steady_image to appear if active wasn't cached - self.assertEqual( - [self._image_dict(active_image), - self._image_dict(steady_image)], - self.cloud.list_images()) - - @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') - def test_list_images_caches_steady_status(self, mock_image_client): - steady_image = munch.Munch(id='91', name='Federov', status='active') - first_image = None - for status in ('active', 'deleted', 'killed'): - active_image = munch.Munch( - id=self.getUniqueString(), name=self.getUniqueString(), - status=status) - mock_image_client.get.return_value = [active_image] - if not first_image: - first_image = active_image - self.assertEqual( - self._munch_images(first_image), - self.cloud.list_images()) - mock_image_client.get.return_value = [ - active_image, steady_image] - # because we skipped the create_image code path, no invalidation - # was done, so we _SHOULD_ expect steady state images to cache and - # therefore we should _not_ expect to see the new one here - self.assertEqual( - self._munch_images(first_image), - self.cloud.list_images()) - - @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') - def test_cache_no_cloud_name(self, mock_image_client): + def test_list_images_caches_deleted_status(self): + self.use_glance() + + deleted_image_id = self.getUniqueString() + deleted_image = fakes.make_fake_image( + image_id=deleted_image_id, status='deleted') + active_image_id = self.getUniqueString() + active_image = fakes.make_fake_image(image_id=active_image_id) + list_return = {'images': [active_image, deleted_image]} + self.register_uris([ + dict(method='GET', + uri='https://image.example.com/v2/images', + json=list_return), + ]) + + self.assertEqual( + [self.cloud._normalize_image(active_image)], + self.cloud.list_images()) + + self.assertEqual( + [self.cloud._normalize_image(active_image)], + self.cloud.list_images()) + + # We should only have one call + self.assert_calls() + + def test_cache_no_cloud_name(self): + self.use_glance() self.cloud.name = None - fi = munch.Munch( - id='1', name='None Test Image', - status='active', visibility='private') - mock_image_client.get.return_value = [fi] + fi = fakes.make_fake_image(image_id=self.getUniqueString()) + fi2 = fakes.make_fake_image(image_id=self.getUniqueString()) + + self.register_uris([ + dict(method='GET', + uri='https://image.example.com/v2/images', + json={'images': [fi]}), + dict(method='GET', + uri='https://image.example.com/v2/images', + json={'images': [fi, fi2]}), + ]) + self.assertEqual( self._munch_images(fi), self.cloud.list_images()) + # Now test that the list was cached - fi2 = munch.Munch( - id='2', name='None Test Image', - status='active', visibility='private') - mock_image_client.get.return_value = [fi, fi2] self.assertEqual( self._munch_images(fi), self.cloud.list_images()) + # Invalidation too self.cloud.list_images.invalidate(self.cloud) self.assertEqual( - [self._image_dict(fi), self._image_dict(fi2)], + [ + self.cloud._normalize_image(fi), + self.cloud._normalize_image(fi2) + ], + self.cloud.list_images()) + + +class TestCacheIgnoresQueuedStatus(base.TestCase): + + scenarios = [ + ('queued', dict(status='queued')), + ('saving', dict(status='saving')), + ('pending_delete', dict(status='pending_delete')), + ] + + def setUp(self): + super(TestCacheIgnoresQueuedStatus, self).setUp( + cloud_config_fixture='clouds_cache.yaml') + self.use_glance() + active_image_id = self.getUniqueString() + self.active_image = fakes.make_fake_image( + image_id=active_image_id, status=self.status) + self.active_list_return = {'images': [self.active_image]} + steady_image_id = self.getUniqueString() + self.steady_image = fakes.make_fake_image(image_id=steady_image_id) + self.steady_list_return = { + 'images': [self.active_image, self.steady_image]} + + def test_list_images_ignores_pending_status(self): + + self.register_uris([ + dict(method='GET', + uri='https://image.example.com/v2/images', + json=self.active_list_return), + dict(method='GET', + uri='https://image.example.com/v2/images', + json=self.steady_list_return), + ]) + + self.assertEqual( + [self.cloud._normalize_image(self.active_image)], self.cloud.list_images()) + # Should expect steady_image to appear if active wasn't cached + self.assertEqual( + [ + self.cloud._normalize_image(self.active_image), + self.cloud._normalize_image(self.steady_image) + ], + self.cloud.list_images()) + + +class TestCacheSteadyStatus(base.TestCase): + + scenarios = [ + ('active', dict(status='active')), + ('killed', dict(status='killed')), + ] + + def setUp(self): + super(TestCacheSteadyStatus, self).setUp( + cloud_config_fixture='clouds_cache.yaml') + self.use_glance() + active_image_id = self.getUniqueString() + self.active_image = fakes.make_fake_image( + image_id=active_image_id, status=self.status) + self.active_list_return = {'images': [self.active_image]} + + def test_list_images_caches_steady_status(self): + + self.register_uris([ + dict(method='GET', + uri='https://image.example.com/v2/images', + json=self.active_list_return), + ]) + + self.assertEqual( + [self.cloud._normalize_image(self.active_image)], + self.cloud.list_images()) + + self.assertEqual( + [self.cloud._normalize_image(self.active_image)], + self.cloud.list_images()) + + # We should only have one call + self.assert_calls() + class TestBogusAuth(base.TestCase): From 02a62a09d4ddba73e0a87ce1e8be81ae04a76437 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 24 Jun 2018 10:33:48 -0500 Subject: [PATCH 2105/3836] Convert image_client mocks in test_shade_operator These two methods were mocking the image client. Change-Id: I2a5c249f55662caaf3a669283721f283f6b1b565 --- openstack/tests/unit/cloud/test_operator.py | 52 ++++++++++++++++----- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/openstack/tests/unit/cloud/test_operator.py b/openstack/tests/unit/cloud/test_operator.py index 1dd3a07d3..1dee45e0d 100644 --- a/openstack/tests/unit/cloud/test_operator.py +++ b/openstack/tests/unit/cloud/test_operator.py @@ -21,19 +21,47 @@ class TestOperatorCloud(base.TestCase): - @mock.patch.object(cloud_region.CloudRegion, 'get_endpoint') - def test_get_session_endpoint_provided(self, fake_get_endpoint): - fake_get_endpoint.return_value = 'http://fake.url' - self.assertEqual( - 'http://fake.url', self.cloud.get_session_endpoint('image')) + def test_get_image_name(self): + self.use_glance() - @mock.patch.object(cloud_region.CloudRegion, 'get_session') - def test_get_session_endpoint_session(self, get_session_mock): - session_mock = mock.Mock() - session_mock.get_endpoint.return_value = 'http://fake.url' - get_session_mock.return_value = session_mock - self.assertEqual( - 'http://fake.url', self.cloud.get_session_endpoint('image')) + image_id = self.getUniqueString() + fake_image = fakes.make_fake_image(image_id=image_id) + list_return = {'images': [fake_image]} + + self.register_uris([ + dict(method='GET', + uri='https://image.example.com/v2/images', + json=list_return), + dict(method='GET', + uri='https://image.example.com/v2/images', + json=list_return), + ]) + + self.assertEqual('fake_image', self.cloud.get_image_name(image_id)) + self.assertEqual('fake_image', self.cloud.get_image_name('fake_image')) + + self.assert_calls() + + def test_get_image_id(self): + self.use_glance() + + image_id = self.getUniqueString() + fake_image = fakes.make_fake_image(image_id=image_id) + list_return = {'images': [fake_image]} + + self.register_uris([ + dict(method='GET', + uri='https://image.example.com/v2/images', + json=list_return), + dict(method='GET', + uri='https://image.example.com/v2/images', + json=list_return), + ]) + + self.assertEqual(image_id, self.cloud.get_image_id(image_id)) + self.assertEqual(image_id, self.cloud.get_image_id('fake_image')) + + self.assert_calls() @mock.patch.object(cloud_region.CloudRegion, 'get_session') def test_get_session_endpoint_exception(self, get_session_mock): From e8e733c142ab96cef80ed3f241bd00c89ca65674 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 24 Jun 2018 11:32:47 -0500 Subject: [PATCH 2106/3836] Finish migrating image tests to requests-mock We let some of these slip through originally. Fix them so we can do other things. Guess what? There were bugs in the v1 codepath. Change-Id: Ia51598548cde0ddb4f9e96b166274e9a104cf649 --- openstack/cloud/openstackcloud.py | 17 +- openstack/tests/unit/cloud/test_image.py | 485 +++++++++++------------ 2 files changed, 250 insertions(+), 252 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 03f820628..23b0b1ce1 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -4729,7 +4729,9 @@ def _upload_image_put_v1( image_kwargs['properties'].update(meta) image_kwargs['name'] = name - image = self._image_client.post('/images', json=image_kwargs) + image = self._get_and_munchify( + 'image', + self._image_client.post('/images', json=image_kwargs)) checksum = image_kwargs['properties'].get(IMAGE_MD5_KEY, '') try: @@ -4741,21 +4743,24 @@ def _upload_image_put_v1( if checksum: headers['x-image-meta-checksum'] = checksum - image = self._image_client.put( - '/images/{id}'.format(id=image.id), - headers=headers, data=image_data) + image = self._get_and_munchify( + 'image', + self._image_client.put( + '/images/{id}'.format(id=image.id), + headers=headers, data=image_data)) except exc.OpenStackCloudHTTPError: self.log.debug("Deleting failed upload of image %s", name) try: - self._image_client.delete('/images/{id}'.format(id=image.id)) + self._image_client.delete( + '/images/{id}'.format(id=image.id)) except exc.OpenStackCloudHTTPError: # We're just trying to clean up - if it doesn't work - shrug self.log.debug( "Failed deleting image after we failed uploading it.", exc_info=True) raise - return image + return self._normalize_image(image) def _upload_image_put( self, name, filename, meta, wait, timeout, **image_kwargs): diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 8e08dad70..53ed287dc 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -12,18 +12,12 @@ # License for the specific language governing permissions and limitations # under the License. -# TODO(mordred) There are mocks of the image_client in here that are not -# using requests_mock. Erradicate them. - import operator import tempfile import uuid -import mock -import munch import six -import openstack.cloud from openstack.cloud import exc from openstack.cloud import meta from openstack.cloud import openstackcloud @@ -587,303 +581,302 @@ def _call_create_image(self, name, **kwargs): name, imagefile.name, wait=True, timeout=1, is_public=False, **kwargs) - # TODO(shade) Migrate this to requests-mock - @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') - @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') - def test_create_image_put_v1( - self, mock_image_client, mock_is_client_version): - mock_is_client_version.return_value = False - mock_image_client.get.return_value = [] - self.assertEqual([], self.cloud.list_images()) + def test_create_image_put_v1(self): + self.cloud.config.config['image_api_version'] = '1' - args = {'name': '42 name', + args = {'name': self.image_name, 'container_format': 'bare', 'disk_format': 'qcow2', 'properties': { - 'owner_specified.openstack.md5': mock.ANY, - 'owner_specified.openstack.sha256': mock.ANY, - 'owner_specified.openstack.object': 'images/42 name', + 'owner_specified.openstack.md5': fakes.NO_MD5, + 'owner_specified.openstack.sha256': fakes.NO_SHA256, + 'owner_specified.openstack.object': 'images/{name}'.format( + name=self.image_name), 'is_public': False}} - ret = munch.Munch(args.copy()) - ret['id'] = '42' + + ret = args.copy() + ret['id'] = self.image_id ret['status'] = 'success' - mock_image_client.get.side_effect = [ - [], - [ret], - [ret], - ] - mock_image_client.post.return_value = ret - mock_image_client.put.return_value = ret - self._call_create_image('42 name') - mock_image_client.post.assert_called_with('/images', json=args) - mock_image_client.put.assert_called_with( - '/images/42', data=mock.ANY, - headers={ - 'x-image-meta-checksum': mock.ANY, - 'x-glance-registry-purge-props': 'false' - }) - mock_image_client.get.assert_called_with('/images/detail', params={}) - self.assertEqual( - self._munch_images(ret), self.cloud.list_images()) - - # TODO(shade) Migrate this to requests-mock - @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') - @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') - def test_create_image_put_v1_bad_delete( - self, mock_image_client, mock_is_client_version): - mock_is_client_version.return_value = False - mock_image_client.get.return_value = [] - self.assertEqual([], self.cloud.list_images()) - args = {'name': '42 name', + self.register_uris([ + dict(method='GET', + uri='https://image.example.com/v1/images/detail', + json={'images': []}), + dict(method='POST', + uri='https://image.example.com/v1/images', + json={'image': ret}, + validate=dict(json=args)), + dict(method='PUT', + uri='https://image.example.com/v1/images/{id}'.format( + id=self.image_id), + json={'image': ret}, + validate=dict(headers={ + 'x-image-meta-checksum': fakes.NO_MD5, + 'x-glance-registry-purge-props': 'false' + })), + dict(method='GET', + uri='https://image.example.com/v1/images/detail', + json={'images': [ret]}), + ]) + self._call_create_image(self.image_name) + self.assertEqual(self._munch_images(ret), self.cloud.list_images()) + + def test_create_image_put_v1_bad_delete(self): + self.cloud.config.config['image_api_version'] = '1' + + args = {'name': self.image_name, 'container_format': 'bare', 'disk_format': 'qcow2', 'properties': { - 'owner_specified.openstack.md5': mock.ANY, - 'owner_specified.openstack.sha256': mock.ANY, - 'owner_specified.openstack.object': 'images/42 name', + 'owner_specified.openstack.md5': fakes.NO_MD5, + 'owner_specified.openstack.sha256': fakes.NO_SHA256, + 'owner_specified.openstack.object': 'images/{name}'.format( + name=self.image_name), 'is_public': False}} - ret = munch.Munch(args.copy()) - ret['id'] = '42' + + ret = args.copy() + ret['id'] = self.image_id ret['status'] = 'success' - mock_image_client.get.side_effect = [ - [], - [ret], - ] - mock_image_client.post.return_value = ret - mock_image_client.put.side_effect = exc.OpenStackCloudHTTPError( - "Some error") + + self.register_uris([ + dict(method='GET', + uri='https://image.example.com/v1/images/detail', + json={'images': []}), + dict(method='POST', + uri='https://image.example.com/v1/images', + json={'image': ret}, + validate=dict(json=args)), + dict(method='PUT', + uri='https://image.example.com/v1/images/{id}'.format( + id=self.image_id), + status_code=400, + validate=dict(headers={ + 'x-image-meta-checksum': fakes.NO_MD5, + 'x-glance-registry-purge-props': 'false' + })), + dict(method='DELETE', + uri='https://image.example.com/v1/images/{id}'.format( + id=self.image_id), + json={'images': [ret]}), + ]) + self.assertRaises( exc.OpenStackCloudHTTPError, self._call_create_image, - '42 name') - mock_image_client.post.assert_called_with('/images', json=args) - mock_image_client.put.assert_called_with( - '/images/42', data=mock.ANY, - headers={ - 'x-image-meta-checksum': mock.ANY, - 'x-glance-registry-purge-props': 'false' - }) - mock_image_client.delete.assert_called_with('/images/42') - - # TODO(shade) Migrate this to requests-mock - @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') - @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') - def test_update_image_no_patch( - self, mock_image_client, mock_is_client_version): - mock_is_client_version.return_value = True - self.cloud.image_api_use_tasks = False + self.image_name) - mock_image_client.get.return_value = [] - self.assertEqual([], self.cloud.list_images()) + self.assert_calls() - args = {'name': '42 name', + def test_update_image_no_patch(self): + self.cloud.image_api_use_tasks = False + + args = {'name': self.image_name, 'container_format': 'bare', 'disk_format': 'qcow2', - 'owner_specified.openstack.md5': mock.ANY, - 'owner_specified.openstack.sha256': mock.ANY, - 'owner_specified.openstack.object': 'images/42 name', - 'visibility': 'private', - 'min_disk': 0, 'min_ram': 0} - ret = munch.Munch(args.copy()) - ret['id'] = '42' + 'owner_specified.openstack.md5': fakes.NO_MD5, + 'owner_specified.openstack.sha256': fakes.NO_SHA256, + 'owner_specified.openstack.object': 'images/{name}'.format( + name=self.image_name), + 'visibility': 'private'} + + ret = args.copy() + ret['id'] = self.image_id ret['status'] = 'success' - mock_image_client.get.side_effect = [ - [], - [ret], - [ret], - ] + self.cloud.update_image_properties( image=self._image_dict(ret), - **{'owner_specified.openstack.object': 'images/42 name'}) - mock_image_client.get.assert_called_with('/images', params={}) - mock_image_client.patch.assert_not_called() - - # TODO(shade) Migrate this to requests-mock - @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') - @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') - def test_create_image_put_v2_bad_delete( - self, mock_image_client, mock_is_client_version): - mock_is_client_version.return_value = True - self.cloud.image_api_use_tasks = False + **{'owner_specified.openstack.object': 'images/{name}'.format( + name=self.image_name)}) - mock_image_client.get.return_value = [] - self.assertEqual([], self.cloud.list_images()) + self.assert_calls() - args = {'name': '42 name', + def test_create_image_put_v2_bad_delete(self): + self.cloud.image_api_use_tasks = False + + args = {'name': self.image_name, 'container_format': 'bare', 'disk_format': 'qcow2', - 'owner_specified.openstack.md5': mock.ANY, - 'owner_specified.openstack.sha256': mock.ANY, - 'owner_specified.openstack.object': 'images/42 name', - 'visibility': 'private', - 'min_disk': 0, 'min_ram': 0} - ret = munch.Munch(args.copy()) - ret['id'] = '42' + 'owner_specified.openstack.md5': fakes.NO_MD5, + 'owner_specified.openstack.sha256': fakes.NO_SHA256, + 'owner_specified.openstack.object': 'images/{name}'.format( + name=self.image_name), + 'visibility': 'private'} + + ret = args.copy() + ret['id'] = self.image_id ret['status'] = 'success' - mock_image_client.get.side_effect = [ - [], - [ret], - [ret], - ] - mock_image_client.post.return_value = ret - mock_image_client.put.side_effect = exc.OpenStackCloudHTTPError( - "Some error") + + self.register_uris([ + dict(method='GET', + uri='https://image.example.com/v2/images', + json={'images': []}), + dict(method='POST', + uri='https://image.example.com/v2/images', + json=ret, + validate=dict(json=args)), + dict(method='PUT', + uri='https://image.example.com/v2/images/{id}/file'.format( + id=self.image_id), + status_code=400, + validate=dict( + headers={ + 'Content-Type': 'application/octet-stream', + }, + )), + dict(method='DELETE', + uri='https://image.example.com/v2/images/{id}'.format( + id=self.image_id)), + ]) + self.assertRaises( exc.OpenStackCloudHTTPError, self._call_create_image, - '42 name', min_disk='0', min_ram=0) - mock_image_client.post.assert_called_with('/images', json=args) - mock_image_client.put.assert_called_with( - '/images/42/file', - headers={'Content-Type': 'application/octet-stream'}, - data=mock.ANY) - mock_image_client.delete.assert_called_with('/images/42') - - # TODO(shade) Migrate this to requests-mock - @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') - @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') - def test_create_image_put_bad_int( - self, mock_image_client, mock_is_client_version): - mock_is_client_version.return_value = True + self.image_name) + + self.assert_calls() + + def test_create_image_put_bad_int(self): self.cloud.image_api_use_tasks = False + self.register_uris([ + dict(method='GET', + uri='https://image.example.com/v2/images', + json={'images': []}), + ]) + self.assertRaises( exc.OpenStackCloudException, - self._call_create_image, '42 name', min_disk='fish', min_ram=0) - mock_image_client.post.assert_not_called() - - # TODO(shade) Migrate this to requests-mock - @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') - @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') - def test_create_image_put_user_int( - self, mock_image_client, mock_is_client_version): - mock_is_client_version.return_value = True + self._call_create_image, self.image_name, + min_disk='fish', min_ram=0) + + self.assert_calls() + + def test_create_image_put_user_int(self): self.cloud.image_api_use_tasks = False - args = {'name': '42 name', + args = {'name': self.image_name, 'container_format': 'bare', 'disk_format': u'qcow2', - 'owner_specified.openstack.md5': mock.ANY, - 'owner_specified.openstack.sha256': mock.ANY, - 'owner_specified.openstack.object': 'images/42 name', + 'owner_specified.openstack.md5': fakes.NO_MD5, + 'owner_specified.openstack.sha256': fakes.NO_SHA256, + 'owner_specified.openstack.object': 'images/{name}'.format( + name=self.image_name), 'int_v': '12345', 'visibility': 'private', 'min_disk': 0, 'min_ram': 0} - ret = munch.Munch(args.copy()) - ret['id'] = '42' + + ret = args.copy() + ret['id'] = self.image_id ret['status'] = 'success' - mock_image_client.get.side_effect = [ - [], - [ret], - [ret] - ] - mock_image_client.post.return_value = ret - self._call_create_image( - '42 name', min_disk='0', min_ram=0, int_v=12345) - mock_image_client.post.assert_called_with('/images', json=args) - mock_image_client.put.assert_called_with( - '/images/42/file', - headers={'Content-Type': 'application/octet-stream'}, - data=mock.ANY) - mock_image_client.get.assert_called_with('/images', params={}) - self.assertEqual( - self._munch_images(ret), self.cloud.list_images()) - - # TODO(shade) Migrate this to requests-mock - @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') - @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') - def test_create_image_put_meta_int( - self, mock_image_client, mock_is_client_version): - mock_is_client_version.return_value = True - self.cloud.image_api_use_tasks = False - mock_image_client.get.return_value = [] - self.assertEqual([], self.cloud.list_images()) + self.register_uris([ + dict(method='GET', + uri='https://image.example.com/v2/images', + json={'images': []}), + dict(method='POST', + uri='https://image.example.com/v2/images', + json=ret, + validate=dict(json=args)), + dict(method='PUT', + uri='https://image.example.com/v2/images/{id}/file'.format( + id=self.image_id), + validate=dict( + headers={ + 'Content-Type': 'application/octet-stream', + }, + )), + dict(method='GET', + uri='https://image.example.com/v2/images', + json={'images': [ret]}), + ]) self._call_create_image( - '42 name', min_disk='0', min_ram=0, meta={'int_v': 12345}) - args = {'name': '42 name', + self.image_name, min_disk='0', min_ram=0, int_v=12345) + + self.assert_calls() + + def test_create_image_put_meta_int(self): + self.cloud.image_api_use_tasks = False + + args = {'name': self.image_name, 'container_format': 'bare', 'disk_format': u'qcow2', - 'owner_specified.openstack.md5': mock.ANY, - 'owner_specified.openstack.sha256': mock.ANY, - 'owner_specified.openstack.object': 'images/42 name', + 'owner_specified.openstack.md5': fakes.NO_MD5, + 'owner_specified.openstack.sha256': fakes.NO_SHA256, + 'owner_specified.openstack.object': 'images/{name}'.format( + name=self.image_name), 'int_v': 12345, 'visibility': 'private', 'min_disk': 0, 'min_ram': 0} - ret = munch.Munch(args.copy()) - ret['id'] = '42' + + ret = args.copy() + ret['id'] = self.image_id ret['status'] = 'success' - mock_image_client.get.return_value = [ret] - mock_image_client.post.return_value = ret - mock_image_client.get.assert_called_with('/images', params={}) - self.assertEqual( - self._munch_images(ret), self.cloud.list_images()) - - # TODO(shade) Migrate this to requests-mock - @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') - @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') - def test_create_image_put_protected( - self, mock_image_client, mock_is_client_version): - mock_is_client_version.return_value = True - self.cloud.image_api_use_tasks = False - mock_image_client.get.return_value = [] - self.assertEqual([], self.cloud.list_images()) + self.register_uris([ + dict(method='GET', + uri='https://image.example.com/v2/images', + json={'images': []}), + dict(method='POST', + uri='https://image.example.com/v2/images', + json=ret, + validate=dict(json=args)), + dict(method='PUT', + uri='https://image.example.com/v2/images/{id}/file'.format( + id=self.image_id), + validate=dict( + headers={ + 'Content-Type': 'application/octet-stream', + }, + )), + dict(method='GET', + uri='https://image.example.com/v2/images', + json={'images': [ret]}), + ]) - args = {'name': '42 name', - 'container_format': 'bare', 'disk_format': u'qcow2', - 'owner_specified.openstack.md5': mock.ANY, - 'owner_specified.openstack.sha256': mock.ANY, - 'owner_specified.openstack.object': 'images/42 name', - 'protected': False, - 'int_v': '12345', - 'visibility': 'private', - 'min_disk': 0, 'min_ram': 0} - ret = munch.Munch(args.copy()) - ret['id'] = '42' - ret['status'] = 'success' - mock_image_client.get.side_effect = [ - [], - [ret], - [ret], - ] - mock_image_client.put.return_value = ret - mock_image_client.post.return_value = ret self._call_create_image( - '42 name', min_disk='0', min_ram=0, properties={'int_v': 12345}, - protected=False) - mock_image_client.post.assert_called_with('/images', json=args) - mock_image_client.put.assert_called_with( - '/images/42/file', data=mock.ANY, - headers={'Content-Type': 'application/octet-stream'}) - self.assertEqual(self._munch_images(ret), self.cloud.list_images()) + self.image_name, min_disk='0', min_ram=0, meta={'int_v': 12345}) - # TODO(shade) Migrate this to requests-mock - @mock.patch.object(openstack.cloud.OpenStackCloud, '_is_client_version') - @mock.patch.object(openstack.cloud.OpenStackCloud, '_image_client') - def test_create_image_put_user_prop( - self, mock_image_client, mock_is_client_version): - mock_is_client_version.return_value = True - self.cloud.image_api_use_tasks = False + self.assert_calls() - mock_image_client.get.return_value = [] - self.assertEqual([], self.cloud.list_images()) + def test_create_image_put_protected(self): + self.cloud.image_api_use_tasks = False - args = {'name': '42 name', + args = {'name': self.image_name, 'container_format': 'bare', 'disk_format': u'qcow2', - 'owner_specified.openstack.md5': mock.ANY, - 'owner_specified.openstack.sha256': mock.ANY, - 'owner_specified.openstack.object': 'images/42 name', + 'owner_specified.openstack.md5': fakes.NO_MD5, + 'owner_specified.openstack.sha256': fakes.NO_SHA256, + 'owner_specified.openstack.object': 'images/{name}'.format( + name=self.image_name), 'int_v': '12345', - 'xenapi_use_agent': 'False', + 'protected': False, 'visibility': 'private', 'min_disk': 0, 'min_ram': 0} - ret = munch.Munch(args.copy()) - ret['id'] = '42' + + ret = args.copy() + ret['id'] = self.image_id ret['status'] = 'success' - mock_image_client.get.return_value = [ret] - mock_image_client.post.return_value = ret + + self.register_uris([ + dict(method='GET', + uri='https://image.example.com/v2/images', + json={'images': []}), + dict(method='POST', + uri='https://image.example.com/v2/images', + json=ret, + validate=dict(json=args)), + dict(method='PUT', + uri='https://image.example.com/v2/images/{id}/file'.format( + id=self.image_id), + validate=dict( + headers={ + 'Content-Type': 'application/octet-stream', + }, + )), + dict(method='GET', + uri='https://image.example.com/v2/images', + json={'images': [ret]}), + ]) + self._call_create_image( - '42 name', min_disk='0', min_ram=0, properties={'int_v': 12345}) - mock_image_client.get.assert_called_with('/images', params={}) - self.assertEqual( - self._munch_images(ret), self.cloud.list_images()) + self.image_name, min_disk='0', min_ram=0, + properties={'int_v': 12345}, protected=False) + + self.assert_calls() class TestImageSuburl(BaseTestImage): From 42836e431888d7cef07b25c82c75d0414567fd4b Mon Sep 17 00:00:00 2001 From: Feilong Wang Date: Fri, 22 Jun 2018 15:33:07 +1200 Subject: [PATCH 2107/3836] Improve Magnum cluster templates functions There are two improvements in this patch: 1. Add alias to rename xxx_cluster_template to xxx_coe_cluster_template. This a pre clean so that we can support xxx_coe_cluster to avoid conflits with Senlin's functions. 2. Support new Magnum API endpoints, /clustertemplates and /clusters. Those two endpoints added in Newton. Change-Id: I78c37f0df8f63a13c534f3dcaca2e27073f0d730 --- openstack/cloud/openstackcloud.py | 53 +++++-- .../unit/cloud/test_cluster_templates.py | 138 +++++++++++++----- 2 files changed, 141 insertions(+), 50 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 7650abcd0..18fa4d05b 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -8656,11 +8656,21 @@ def list_cluster_templates(self, detail=False): the OpenStack API call. """ with _utils.shade_exceptions("Error fetching cluster template list"): - data = self._container_infra_client.get( - '/baymodels/detail') - return self._normalize_cluster_templates( - self._get_and_munchify('baymodels', data)) + try: + data = self._container_infra_client.get('/clustertemplates') + # NOTE(flwang): Magnum adds /clustertemplates and /cluster + # to deprecate /baymodels and /bay since Newton release. So + # we're using a small tag to indicate if current + # cloud has those two new API endpoints. + self._container_infra_client._has_magnum_after_newton = True + return self._normalize_cluster_templates( + self._get_and_munchify('clustertemplates', data)) + except exc.OpenStackCloudURINotFound: + data = self._container_infra_client.get('/baymodels/detail') + return self._normalize_cluster_templates( + self._get_and_munchify('baymodels', data)) list_baymodels = list_cluster_templates + list_coe_cluster_templates = list_cluster_templates def search_cluster_templates( self, name_or_id=None, filters=None, detail=False): @@ -8680,6 +8690,7 @@ def search_cluster_templates( return _utils._filter_list( cluster_templates, name_or_id, filters) search_baymodels = search_cluster_templates + search_coe_cluster_templates = search_cluster_templates def get_cluster_template(self, name_or_id, filters=None, detail=False): """Get a cluster template by name or ID. @@ -8706,6 +8717,7 @@ def get_cluster_template(self, name_or_id, filters=None, detail=False): return _utils._get_entity(self, 'cluster_template', name_or_id, filters=filters, detail=detail) get_baymodel = get_cluster_template + get_coe_cluster_template = get_cluster_template def create_cluster_template( self, name, image_id=None, keypair_id=None, coe=None, **kwargs): @@ -8733,12 +8745,18 @@ def create_cluster_template( body['keypair_id'] = keypair_id body['coe'] = coe - cluster_template = self._container_infra_client.post( - '/baymodels', json=body) + try: + cluster_template = self._container_infra_client.post( + '/clustertemplates', json=body) + self._container_infra_client._has_magnum_after_newton = True + except exc.OpenStackCloudURINotFound: + cluster_template = self._container_infra_client.post( + '/baymodels', json=body) self.list_cluster_templates.invalidate(self) return cluster_template create_baymodel = create_cluster_template + create_coe_cluster_template = create_cluster_template def delete_cluster_template(self, name_or_id): """Delete a cluster template. @@ -8760,12 +8778,18 @@ def delete_cluster_template(self, name_or_id): return False with _utils.shade_exceptions("Error in deleting cluster template"): - self._container_infra_client.delete( - '/baymodels/{id}'.format(id=cluster_template['id'])) + if getattr(self._container_infra_client, + '_has_magnum_after_newton', False): + self._container_infra_client.delete( + '/clustertemplates/{id}'.format(id=cluster_template['id'])) + else: + self._container_infra_client.delete( + '/baymodels/{id}'.format(id=cluster_template['id'])) self.list_cluster_templates.invalidate(self) return True delete_baymodel = delete_cluster_template + delete_coe_cluster_template = delete_cluster_template @_utils.valid_kwargs('name', 'image_id', 'flavor_id', 'master_flavor_id', 'keypair_id', 'external_network_id', 'fixed_network', @@ -8802,13 +8826,20 @@ def update_cluster_template(self, name_or_id, operation, **kwargs): with _utils.shade_exceptions( "Error updating cluster template {0}".format(name_or_id)): - self._container_infra_client.patch( - '/baymodels/{id}'.format(id=cluster_template['id']), - json=patches) + if getattr(self._container_infra_client, + '_has_magnum_after_newton', False): + self._container_infra_client.patch( + '/clustertemplates/{id}'.format(id=cluster_template['id']), + json=patches) + else: + self._container_infra_client.patch( + '/baymodels/{id}'.format(id=cluster_template['id']), + json=patches) new_cluster_template = self.get_cluster_template(name_or_id) return new_cluster_template update_baymodel = update_cluster_template + update_coe_cluster_template = update_cluster_template def list_nics(self): msg = "Error fetching machine port list" diff --git a/openstack/tests/unit/cloud/test_cluster_templates.py b/openstack/tests/unit/cloud/test_cluster_templates.py index 32fe56f67..2e9f03be4 100644 --- a/openstack/tests/unit/cloud/test_cluster_templates.py +++ b/openstack/tests/unit/cloud/test_cluster_templates.py @@ -53,10 +53,15 @@ class TestClusterTemplates(base.TestCase): def test_list_cluster_templates_without_detail(self): - self.register_uris([dict( - method='GET', - uri='https://container-infra.example.com/v1/baymodels/detail', - json=dict(baymodels=[cluster_template_obj.toDict()]))]) + self.register_uris([ + dict( + method='GET', + uri='https://container-infra.example.com/v1/clustertemplates', + status_code=404), + dict( + method='GET', + uri='https://container-infra.example.com/v1/baymodels/detail', + json=dict(baymodels=[cluster_template_obj.toDict()]))]) cluster_templates_list = self.cloud.list_cluster_templates() self.assertEqual( cluster_templates_list[0], @@ -64,10 +69,15 @@ def test_list_cluster_templates_without_detail(self): self.assert_calls() def test_list_cluster_templates_with_detail(self): - self.register_uris([dict( - method='GET', - uri='https://container-infra.example.com/v1/baymodels/detail', - json=dict(baymodels=[cluster_template_obj.toDict()]))]) + self.register_uris([ + dict( + method='GET', + uri='https://container-infra.example.com/v1/clustertemplates', + status_code=404), + dict( + method='GET', + uri='https://container-infra.example.com/v1/baymodels/detail', + json=dict(baymodels=[cluster_template_obj.toDict()]))]) cluster_templates_list = self.cloud.list_cluster_templates(detail=True) self.assertEqual( cluster_templates_list[0], @@ -75,10 +85,15 @@ def test_list_cluster_templates_with_detail(self): self.assert_calls() def test_search_cluster_templates_by_name(self): - self.register_uris([dict( - method='GET', - uri='https://container-infra.example.com/v1/baymodels/detail', - json=dict(baymodels=[cluster_template_obj.toDict()]))]) + self.register_uris([ + dict( + method='GET', + uri='https://container-infra.example.com/v1/clustertemplates', + status_code=404), + dict( + method='GET', + uri='https://container-infra.example.com/v1/baymodels/detail', + json=dict(baymodels=[cluster_template_obj.toDict()]))]) cluster_templates = self.cloud.search_cluster_templates( name_or_id='fake-cluster-template') @@ -89,10 +104,15 @@ def test_search_cluster_templates_by_name(self): def test_search_cluster_templates_not_found(self): - self.register_uris([dict( - method='GET', - uri='https://container-infra.example.com/v1/baymodels/detail', - json=dict(baymodels=[cluster_template_obj.toDict()]))]) + self.register_uris([ + dict( + method='GET', + uri='https://container-infra.example.com/v1/clustertemplates', + status_code=404), + dict( + method='GET', + uri='https://container-infra.example.com/v1/baymodels/detail', + json=dict(baymodels=[cluster_template_obj.toDict()]))]) cluster_templates = self.cloud.search_cluster_templates( name_or_id='non-existent') @@ -101,10 +121,15 @@ def test_search_cluster_templates_not_found(self): self.assert_calls() def test_get_cluster_template(self): - self.register_uris([dict( - method='GET', - uri='https://container-infra.example.com/v1/baymodels/detail', - json=dict(baymodels=[cluster_template_obj.toDict()]))]) + self.register_uris([ + dict( + method='GET', + uri='https://container-infra.example.com/v1/clustertemplates', + status_code=404), + dict( + method='GET', + uri='https://container-infra.example.com/v1/baymodels/detail', + json=dict(baymodels=[cluster_template_obj.toDict()]))]) r = self.cloud.get_cluster_template('fake-cluster-template') self.assertIsNotNone(r) @@ -113,25 +138,34 @@ def test_get_cluster_template(self): self.assert_calls() def test_get_cluster_template_not_found(self): - self.register_uris([dict( - method='GET', - uri='https://container-infra.example.com/v1/baymodels/detail', - json=dict(baymodels=[]))]) + self.register_uris([ + dict( + method='GET', + uri='https://container-infra.example.com/v1/clustertemplates', + status_code=404), + dict( + method='GET', + uri='https://container-infra.example.com/v1/baymodels/detail', + json=dict(baymodels=[]))]) r = self.cloud.get_cluster_template('doesNotExist') self.assertIsNone(r) self.assert_calls() def test_create_cluster_template(self): - self.register_uris([dict( - method='POST', - uri='https://container-infra.example.com/v1/baymodels', - json=dict(baymodels=[cluster_template_obj.toDict()]), - validate=dict(json={ - 'coe': 'fake-coe', - 'image_id': 'fake-image', - 'keypair_id': 'fake-key', - 'name': 'fake-cluster-template'}), - )]) + self.register_uris([ + dict( + method='POST', + uri='https://container-infra.example.com/v1/clustertemplates', + status_code=404), + dict( + method='POST', + uri='https://container-infra.example.com/v1/baymodels', + json=dict(baymodels=[cluster_template_obj.toDict()]), + validate=dict(json={ + 'coe': 'fake-coe', + 'image_id': 'fake-image', + 'keypair_id': 'fake-key', + 'name': 'fake-cluster-template'}),)]) self.cloud.create_cluster_template( name=cluster_template_obj.name, image_id=cluster_template_obj.image_id, @@ -140,10 +174,15 @@ def test_create_cluster_template(self): self.assert_calls() def test_create_cluster_template_exception(self): - self.register_uris([dict( - method='POST', - uri='https://container-infra.example.com/v1/baymodels', - status_code=403)]) + self.register_uris([ + dict( + method='POST', + uri='https://container-infra.example.com/v1/clustertemplates', + status_code=404), + dict( + method='POST', + uri='https://container-infra.example.com/v1/baymodels', + status_code=403)]) # TODO(mordred) requests here doens't give us a great story # for matching the old error message text. Investigate plumbing # an error message in to the adapter call so that we can give a @@ -159,6 +198,10 @@ def test_create_cluster_template_exception(self): def test_delete_cluster_template(self): uri = 'https://container-infra.example.com/v1/baymodels/fake-uuid' self.register_uris([ + dict( + method='GET', + uri='https://container-infra.example.com/v1/clustertemplates', + status_code=404), dict( method='GET', uri='https://container-infra.example.com/v1/baymodels/detail', @@ -173,6 +216,10 @@ def test_delete_cluster_template(self): def test_update_cluster_template(self): uri = 'https://container-infra.example.com/v1/baymodels/fake-uuid' self.register_uris([ + dict( + method='GET', + uri='https://container-infra.example.com/v1/clustertemplates', + status_code=404), dict( method='GET', uri='https://container-infra.example.com/v1/baymodels/detail', @@ -190,7 +237,7 @@ def test_update_cluster_template(self): )), dict( method='GET', - uri='https://container-infra.example.com/v1/baymodels/detail', + uri='https://container-infra.example.com/v1/clustertemplates', # This json value is not meaningful to the test - it just has # to be valid. json=dict(baymodels=[cluster_template_obj.toDict()])), @@ -199,3 +246,16 @@ def test_update_cluster_template(self): self.cloud.update_cluster_template( 'fake-uuid', 'replace', name=new_name) self.assert_calls() + + def test_get_coe_cluster_template(self): + self.register_uris([ + dict( + method='GET', + uri='https://container-infra.example.com/v1/clustertemplates', + json=dict(clustertemplates=[cluster_template_obj.toDict()]))]) + + r = self.cloud.get_coe_cluster_template('fake-cluster-template') + self.assertIsNotNone(r) + self.assertDictEqual( + r, self.cloud._normalize_cluster_template(cluster_template_obj)) + self.assert_calls() From dcb6c53f943c7231e6c17beedac9fb9ad8d166f4 Mon Sep 17 00:00:00 2001 From: Feilong Wang Date: Fri, 22 Jun 2018 16:49:24 +1200 Subject: [PATCH 2108/3836] Add Magnum cluster support Add CRUD support for Magnum COE clusters. Please refer Magnum's API ref doc for the API design[1]. [1] https://developer.openstack.org/api-ref/container-infrastructure-management Change-Id: I7adff704083a4aeb4c509eb97671eb26f3b9a12c --- openstack/cloud/_normalize.py | 34 ++++ openstack/cloud/openstackcloud.py | 146 ++++++++++++++++ .../functional/cloud/test_coe_clusters.py | 28 +++ .../tests/unit/cloud/test_coe_clusters.py | 163 ++++++++++++++++++ ...gnum-cluster-support-843fe2709b8f4789.yaml | 4 + 5 files changed, 375 insertions(+) create mode 100644 openstack/tests/functional/cloud/test_coe_clusters.py create mode 100644 openstack/tests/unit/cloud/test_coe_clusters.py create mode 100644 releasenotes/notes/add-magnum-cluster-support-843fe2709b8f4789.yaml diff --git a/openstack/cloud/_normalize.py b/openstack/cloud/_normalize.py index ad4d54f3f..d8a1d59ce 100644 --- a/openstack/cloud/_normalize.py +++ b/openstack/cloud/_normalize.py @@ -919,6 +919,40 @@ def _normalize_server_usages(self, server_usages): ret.append(self._normalize_server_usage(server_usage)) return ret + def _normalize_coe_clusters(self, coe_clusters): + ret = [] + for coe_cluster in coe_clusters: + ret.append(self._normalize_coe_cluster(coe_cluster)) + return ret + + def _normalize_coe_cluster(self, coe_cluster): + """Normalize Magnum COE cluster.""" + coe_cluster = coe_cluster.copy() + + # Discard noise + coe_cluster.pop('links', None) + + c_id = coe_cluster.pop('uuid') + + ret = munch.Munch( + id=c_id, + location=self._get_current_location(), + ) + + for key in ( + 'status', + 'cluster_template_id', + 'stack_id', + 'keypair', + 'master_count', + 'create_timeout', + 'node_count', + 'name'): + ret[key] = coe_cluster.pop(key) + + ret['properties'] = coe_cluster + return ret + def _normalize_cluster_templates(self, cluster_templates): ret = [] for cluster_template in cluster_templates: diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index d8690fc75..fb141c5aa 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -8648,6 +8648,152 @@ def delete_recordset(self, zone, name_or_id): return True + @_utils.cache_on_arguments() + def list_coe_clusters(self): + """List COE(Ccontainer Orchestration Engine) cluster. + + :returns: a list of dicts containing the cluster. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + with _utils.shade_exceptions("Error fetching cluster list"): + data = self._container_infra_client.get('/clusters') + return self._normalize_coe_clusters( + self._get_and_munchify('clusters', data)) + + def search_coe_clusters( + self, name_or_id=None, filters=None): + """Search COE cluster. + + :param name_or_id: cluster name or ID. + :param filters: a dict containing additional filters to use. + :param detail: a boolean to control if we need summarized or + detailed output. + + :returns: a list of dict containing the cluster + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + coe_clusters = self.list_coe_clusters() + return _utils._filter_list( + coe_clusters, name_or_id, filters) + + def get_coe_cluster(self, name_or_id, filters=None): + """Get a COE cluster by name or ID. + + :param name_or_id: Name or ID of the cluster. + :param filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: A cluster dict or None if no matching cluster is found. + """ + return _utils._get_entity(self, 'coe_cluster', name_or_id, + filters=filters) + + def create_coe_cluster( + self, name, cluster_template_id, **kwargs): + """Create a COE cluster based on given cluster template. + + :param string name: Name of the cluster. + :param string image_id: ID of the cluster template to use. + + Other arguments will be passed in kwargs. + + :returns: a dict containing the cluster description + + :raises: ``OpenStackCloudException`` if something goes wrong during + the OpenStack API call + """ + error_message = ("Error creating cluster of name" + " {cluster_name}".format(cluster_name=name)) + with _utils.shade_exceptions(error_message): + body = kwargs.copy() + body['name'] = name + body['cluster_template_id'] = cluster_template_id + + cluster = self._container_infra_client.post( + '/clusters', json=body) + + self.list_coe_clusters.invalidate(self) + return cluster + + def delete_coe_cluster(self, name_or_id): + """Delete a COE cluster. + + :param name_or_id: Name or unique ID of the cluster. + :returns: True if the delete succeeded, False if the + cluster was not found. + + :raises: OpenStackCloudException on operation error. + """ + + cluster = self.get_coe_cluster(name_or_id) + + if not cluster: + self.log.debug( + "COE Cluster %(name_or_id)s does not exist", + {'name_or_id': name_or_id}, + exc_info=True) + return False + + with _utils.shade_exceptions("Error in deleting COE cluster"): + self._container_infra_client.delete( + '/clusters/{id}'.format(id=cluster['id'])) + self.list_coe_clusters.invalidate(self) + + return True + + @_utils.valid_kwargs('name') + def update_coe_cluster(self, name_or_id, operation, **kwargs): + """Update a COE cluster. + + :param name_or_id: Name or ID of the COE cluster being updated. + :param operation: Operation to perform - add, remove, replace. + + Other arguments will be passed with kwargs. + + :returns: a dict representing the updated cluster. + + :raises: OpenStackCloudException on operation error. + """ + self.list_coe_clusters.invalidate(self) + cluster = self.get_coe_cluster(name_or_id) + if not cluster: + raise exc.OpenStackCloudException( + "COE cluster %s not found." % name_or_id) + + if operation not in ['add', 'replace', 'remove']: + raise TypeError( + "%s operation not in 'add', 'replace', 'remove'" % operation) + + patches = _utils.generate_patches_from_kwargs(operation, **kwargs) + # No need to fire an API call if there is an empty patch + if not patches: + return cluster + + with _utils.shade_exceptions( + "Error updating COE cluster {0}".format(name_or_id)): + self._container_infra_client.patch( + '/clusters/{id}'.format(id=cluster['id']), + json=patches) + + new_cluster = self.get_coe_cluster(name_or_id) + return new_cluster + @_utils.cache_on_arguments() def list_cluster_templates(self, detail=False): """List cluster templates. diff --git a/openstack/tests/functional/cloud/test_coe_clusters.py b/openstack/tests/functional/cloud/test_coe_clusters.py new file mode 100644 index 000000000..e4a099896 --- /dev/null +++ b/openstack/tests/functional/cloud/test_coe_clusters.py @@ -0,0 +1,28 @@ +# 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_coe_clusters +---------------------------------- + +Functional tests for `shade` COE clusters methods. +""" + +from openstack.tests.functional.cloud import base + + +class TestCompute(base.BaseFunctionalTestCase): + # NOTE(flwang): Currently, running Magnum on a cloud which doesn't support + # nested virtualization will lead to timeout. So this test file is mostly + # like a note to document why we can't have function testing for Magnum + # clusters CRUD. + pass diff --git a/openstack/tests/unit/cloud/test_coe_clusters.py b/openstack/tests/unit/cloud/test_coe_clusters.py new file mode 100644 index 000000000..3d6055f15 --- /dev/null +++ b/openstack/tests/unit/cloud/test_coe_clusters.py @@ -0,0 +1,163 @@ +# 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 munch + +from openstack.tests.unit import base + +coe_cluster_obj = munch.Munch( + status="CREATE_IN_PROGRESS", + cluster_template_id="0562d357-8641-4759-8fed-8173f02c9633", + uuid="731387cf-a92b-4c36-981e-3271d63e5597", + links=[{}, {}], + stack_id="31c1ee6c-081e-4f39-9f0f-f1d87a7defa1", + keypair="my_keypair", + master_count=3, + create_timeout=60, + node_count=10, + name="k8s", + created_at="2016-08-29T06:51:31+00:00", + api_address="https://172.24.4.6:6443", + discovery_url="https://discovery.etcd.io/cbeb580da58915809d59ee69348a84f3", + updated_at="2016-08-29T06:53:24+00:00", + coe_version="v1.2.0", + master_addresses=["172.24.4.6"], + node_addresses=["172.24.4.13"], + status_reason="Stack CREATE completed successfully", +) + + +class TestCOEClusters(base.TestCase): + + def test_list_coe_clusters(self): + + self.register_uris([dict( + method='GET', + uri='https://container-infra.example.com/v1/clusters', + json=dict(clusters=[coe_cluster_obj.toDict()]))]) + cluster_list = self.cloud.list_coe_clusters() + self.assertEqual( + cluster_list[0], + self.cloud._normalize_coe_cluster(coe_cluster_obj)) + self.assert_calls() + + def test_create_coe_cluster(self): + self.register_uris([dict( + method='POST', + uri='https://container-infra.example.com/v1/clusters', + json=dict(baymodels=[coe_cluster_obj.toDict()]), + validate=dict(json={ + 'name': 'k8s', + 'cluster_template_id': '0562d357-8641-4759-8fed-8173f02c9633', + 'master_count': 3, + 'node_count': 10}), + )]) + self.cloud.create_coe_cluster( + name=coe_cluster_obj.name, + cluster_template_id=coe_cluster_obj.cluster_template_id, + master_count=coe_cluster_obj.master_count, + node_count=coe_cluster_obj.node_count) + self.assert_calls() + + def test_search_coe_cluster_by_name(self): + self.register_uris([dict( + method='GET', + uri='https://container-infra.example.com/v1/clusters', + json=dict(clusters=[coe_cluster_obj.toDict()]))]) + + coe_clusters = self.cloud.search_coe_clusters( + name_or_id='k8s') + + self.assertEqual(1, len(coe_clusters)) + self.assertEqual(coe_cluster_obj.uuid, coe_clusters[0]['id']) + self.assert_calls() + + def test_search_coe_cluster_not_found(self): + + self.register_uris([dict( + method='GET', + uri='https://container-infra.example.com/v1/clusters', + json=dict(clusters=[coe_cluster_obj.toDict()]))]) + + coe_clusters = self.cloud.search_coe_clusters( + name_or_id='non-existent') + + self.assertEqual(0, len(coe_clusters)) + self.assert_calls() + + def test_get_coe_cluster(self): + self.register_uris([dict( + method='GET', + uri='https://container-infra.example.com/v1/clusters', + json=dict(clusters=[coe_cluster_obj.toDict()]))]) + + r = self.cloud.get_coe_cluster(coe_cluster_obj.name) + self.assertIsNotNone(r) + self.assertDictEqual( + r, self.cloud._normalize_coe_cluster(coe_cluster_obj)) + self.assert_calls() + + def test_get_coe_cluster_not_found(self): + self.register_uris([dict( + method='GET', + uri='https://container-infra.example.com/v1/clusters', + json=dict(clusters=[]))]) + r = self.cloud.get_coe_cluster('doesNotExist') + self.assertIsNone(r) + self.assert_calls() + + def test_delete_coe_cluster(self): + uri = ('https://container-infra.example.com/v1/clusters/%s' % + coe_cluster_obj.uuid) + self.register_uris([ + dict( + method='GET', + uri='https://container-infra.example.com/v1/clusters', + json=dict(clusters=[coe_cluster_obj.toDict()])), + dict( + method='DELETE', + uri=uri), + ]) + self.cloud.delete_coe_cluster(coe_cluster_obj.uuid) + self.assert_calls() + + def test_update_coe_cluster(self): + uri = ('https://container-infra.example.com/v1/clusters/%s' % + coe_cluster_obj.uuid) + self.register_uris([ + dict( + method='GET', + uri='https://container-infra.example.com/v1/clusters', + json=dict(clusters=[coe_cluster_obj.toDict()])), + dict( + method='PATCH', + uri=uri, + status_code=200, + validate=dict( + json=[{ + u'op': u'replace', + u'path': u'/name', + u'value': u'new-coe-cluster' + }] + )), + dict( + method='GET', + uri='https://container-infra.example.com/v1/clusters', + # This json value is not meaningful to the test - it just has + # to be valid. + json=dict(clusters=[coe_cluster_obj.toDict()])), + ]) + new_name = 'new-coe-cluster' + self.cloud.update_coe_cluster( + coe_cluster_obj.uuid, 'replace', name=new_name) + self.assert_calls() diff --git a/releasenotes/notes/add-magnum-cluster-support-843fe2709b8f4789.yaml b/releasenotes/notes/add-magnum-cluster-support-843fe2709b8f4789.yaml new file mode 100644 index 000000000..28609a335 --- /dev/null +++ b/releasenotes/notes/add-magnum-cluster-support-843fe2709b8f4789.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Added magnum cluster CRUD support to cloud abstraction layer. From b6020dae7efa345ed346c6676984892cdcdfdd45 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 28 Jun 2018 16:11:25 -0500 Subject: [PATCH 2109/3836] Hardcode v2.0 onto end of neutron endpoints neutron makes life sad for version discovery because older clouds had hidden the discovery document. Work around it with terrible hacks. The shade layer already does this, incidentally. Change-Id: I8217796ec36140a851177583c75f95cee02c7a43 --- openstack/cloud/openstackcloud.py | 3 +- openstack/config/cloud_region.py | 33 ++++++++++++++++--- openstack/tests/unit/test_connection.py | 16 +++++++-- .../neutron-discovery-54399116d5f810ee.yaml | 5 +++ 4 files changed, 49 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/neutron-discovery-54399116d5f810ee.yaml diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index d8690fc75..bf3df3955 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -581,8 +581,7 @@ def _image_client(self): def _network_client(self): if 'network' not in self._raw_clients: client = self._get_raw_client('network') - # TODO(mordred) I don't care if this is what neutronclient does, - # fix this. + # TODO(mordred) Replace this with self.network # Don't bother with version discovery - there is only one version # of neutron. This is what neutronclient does, fwiw. endpoint = client.get_endpoint() diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index f4c96af3c..0bc7f68f7 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -421,6 +421,31 @@ def get_session_client( self.get_connect_retries(service_type)) kwargs.setdefault('status_code_retries', self.get_status_code_retries(service_type)) + endpoint_override = self.get_endpoint(service_type) + version = version_request.version + min_api_version = version_request.min_api_version + max_api_version = version_request.max_api_version + # Older neutron has inaccessible discovery document. Nobody noticed + # because neutronclient hard-codes an append of v2.0. YAY! + if service_type == 'network': + version = None + min_api_version = None + max_api_version = None + if endpoint_override is None: + network_adapter = constructor( + session=self.get_session(), + service_type=self.get_service_type(service_type), + service_name=self.get_service_name(service_type), + interface=self.get_interface(service_type), + region_name=self.region_name, + ) + network_endpoint = network_adapter.get_endpoint() + if not network_endpoint.rstrip().rsplit('/')[1] == 'v2.0': + if not network_endpoint.endswith('/'): + network_endpoint += '/' + network_endpoint = urllib.parse.urljoin( + network_endpoint, 'v2.0') + endpoint_override = network_endpoint client = constructor( session=self.get_session(), @@ -428,10 +453,10 @@ def get_session_client( service_name=self.get_service_name(service_type), interface=self.get_interface(service_type), region_name=self.region_name, - version=version_request.version, - min_version=version_request.min_api_version, - max_version=version_request.max_api_version, - endpoint_override=self.get_endpoint(service_type), + version=version, + min_version=min_api_version, + max_version=max_api_version, + endpoint_override=endpoint_override, default_microversion=version_request.default_microversion, **kwargs) if version_request.default_microversion: diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index df28d1402..b8bdda0f4 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -98,8 +98,6 @@ def test_create_session(self): conn.identity.__class__.__module__) self.assertEqual('openstack.image.v2._proxy', conn.image.__class__.__module__) - self.assertEqual('openstack.network.v2._proxy', - conn.network.__class__.__module__) self.assertEqual('openstack.object_store.v1._proxy', conn.object_store.__class__.__module__) self.assertEqual('openstack.load_balancer.v2._proxy', @@ -222,6 +220,20 @@ def test_from_profile(self): profile=prof) +class TestNetworkConnection(base.TestCase): + # We need to do the neutron adapter test differently because it needs + # to actually get a catalog. + + def test_network_proxy(self): + self.assertEqual( + 'openstack.network.v2._proxy', + self.conn.network.__class__.__module__) + self.assert_calls() + self.assertEqual( + "https://network.example.com/v2.0", + self.conn.network.get_endpoint()) + + class TestAuthorize(base.TestCase): def test_authorize_works(self): diff --git a/releasenotes/notes/neutron-discovery-54399116d5f810ee.yaml b/releasenotes/notes/neutron-discovery-54399116d5f810ee.yaml new file mode 100644 index 000000000..e102a1ef7 --- /dev/null +++ b/releasenotes/notes/neutron-discovery-54399116d5f810ee.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Added workaround for using neutron on older clouds where the version + discovery document requires auth. From 8eb9a9072435a5d94c9b94e1680604daab30a40a Mon Sep 17 00:00:00 2001 From: "wu.chunyang" Date: Thu, 28 Jun 2018 13:41:15 +0800 Subject: [PATCH 2110/3836] Add release note link in README Change-Id: I6058fa986729fc95d070940c6f00f33065477ced --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index 8effea873..2b26b1eee 100644 --- a/README.rst +++ b/README.rst @@ -160,3 +160,4 @@ Links * `Documentation `_ * `PyPI `_ * `Mailing list `_ +* `Release Notes `_ From b9768bdbcea108dc6557f58ae479dcc772ca2d1f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 29 Jun 2018 08:39:01 -0500 Subject: [PATCH 2111/3836] Add connection backreference to proxy instances Proxy instances need to be able to call methods on the main connection. Add a backreference. Change-Id: If54c4c6f1f43dc5503d59e87a79b08bad561bb54 --- openstack/service_description.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openstack/service_description.py b/openstack/service_description.py index 5594c6835..12a7dc66a 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -94,6 +94,7 @@ def __get__(self, instance, owner): task_manager=instance.task_manager, allow_version_hack=True, ) + instance._proxies[self.service_type]._connection = instance return instance._proxies[self.service_type] def __set__(self, instance, value): From 3fce613297865a6f9ab6657b4c84ce1e269f58d4 Mon Sep 17 00:00:00 2001 From: wacuuu Date: Mon, 2 Jul 2018 21:01:02 +0200 Subject: [PATCH 2112/3836] Implementing solution for 2002563 issue from story board Includes: patch in _get_grant_revoke_params to work with domain_id Fixed UT to work with change Change-Id: Ic892f028e1a3468abaafdcd079ae0142104b6f97 --- openstack/cloud/openstackcloud.py | 7 +- .../tests/unit/cloud/test_role_assignment.py | 108 +++++++++++++++--- 2 files changed, 96 insertions(+), 19 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index d118f13b4..30719c6d6 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -10888,7 +10888,12 @@ def _get_grant_revoke_params(self, role, user=None, group=None, self.get_domain(domain)['id'] if user: - data['user'] = self.get_user(user, filters=filters) + if domain: + data['user'] = self.get_user(user, + domain_id=filters['domain_id'], + filters=filters) + else: + data['user'] = self.get_user(user, filters=filters) if project: # drop domain in favor of project diff --git a/openstack/tests/unit/cloud/test_role_assignment.py b/openstack/tests/unit/cloud/test_role_assignment.py index c9ad2dbb2..10c3079df 100644 --- a/openstack/tests/unit/cloud/test_role_assignment.py +++ b/openstack/tests/unit/cloud/test_role_assignment.py @@ -662,7 +662,11 @@ def test_grant_role_user_domain(self): status_code=200, json=self.domain_data.json_response), dict(method='GET', - uri=self.get_mock_url(resource='users'), + uri=self.get_mock_url(resource='users', + qs_elements=['domain_id=%s' % + self.domain_data. + domain_id]), + complete_qs=True, status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -693,7 +697,11 @@ def test_grant_role_user_domain(self): status_code=200, json=self.domain_data.json_response), dict(method='GET', - uri=self.get_mock_url(resource='users'), + uri=self.get_mock_url(resource='users', + qs_elements=['domain_id=%s' % + self.domain_data. + domain_id]), + complete_qs=True, status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -724,7 +732,11 @@ def test_grant_role_user_domain(self): status_code=200, json=self.domain_data.json_response), dict(method='GET', - uri=self.get_mock_url(resource='users'), + uri=self.get_mock_url(resource='users', + qs_elements=['domain_id=%s' % + self.domain_data. + domain_id]), + complete_qs=True, status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -755,7 +767,11 @@ def test_grant_role_user_domain(self): status_code=200, json=self.domain_data.json_response), dict(method='GET', - uri=self.get_mock_url(resource='users'), + uri=self.get_mock_url(resource='users', + qs_elements=['domain_id=%s' % + self.domain_data. + domain_id]), + complete_qs=True, status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -807,7 +823,11 @@ def test_grant_role_user_domain_exists(self): status_code=200, json=self.domain_data.json_response), dict(method='GET', - uri=self.get_mock_url(resource='users'), + uri=self.get_mock_url(resource='users', + qs_elements=['domain_id=%s' % + self.domain_data. + domain_id]), + complete_qs=True, status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -836,7 +856,11 @@ def test_grant_role_user_domain_exists(self): status_code=200, json=self.domain_data.json_response), dict(method='GET', - uri=self.get_mock_url(resource='users'), + uri=self.get_mock_url(resource='users', + qs_elements=['domain_id=%s' % + self.domain_data. + domain_id]), + complete_qs=True, status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -865,7 +889,11 @@ def test_grant_role_user_domain_exists(self): status_code=200, json=self.domain_data.json_response), dict(method='GET', - uri=self.get_mock_url(resource='users'), + uri=self.get_mock_url(resource='users', + qs_elements=['domain_id=%s' % + self.domain_data. + domain_id]), + complete_qs=True, status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -894,7 +922,11 @@ def test_grant_role_user_domain_exists(self): status_code=200, json=self.domain_data.json_response), dict(method='GET', - uri=self.get_mock_url(resource='users'), + uri=self.get_mock_url(resource='users', + qs_elements=['domain_id=%s' % + self.domain_data. + domain_id]), + complete_qs=True, status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -1768,7 +1800,11 @@ def test_revoke_role_user_domain(self): status_code=200, json=self.domain_data.json_response), dict(method='GET', - uri=self.get_mock_url(resource='users'), + uri=self.get_mock_url(resource='users', + qs_elements=['domain_id=%s' % + self.domain_data. + domain_id]), + complete_qs=True, status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -1791,7 +1827,11 @@ def test_revoke_role_user_domain(self): status_code=200, json=self.domain_data.json_response), dict(method='GET', - uri=self.get_mock_url(resource='users'), + uri=self.get_mock_url(resource='users', + qs_elements=['domain_id=%s' % + self.domain_data. + domain_id]), + complete_qs=True, status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -1814,7 +1854,11 @@ def test_revoke_role_user_domain(self): status_code=200, json=self.domain_data.json_response), dict(method='GET', - uri=self.get_mock_url(resource='users'), + uri=self.get_mock_url(resource='users', + qs_elements=['domain_id=%s' % + self.domain_data. + domain_id]), + complete_qs=True, status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -1837,7 +1881,11 @@ def test_revoke_role_user_domain(self): status_code=200, json=self.domain_data.json_response), dict(method='GET', - uri=self.get_mock_url(resource='users'), + uri=self.get_mock_url(resource='users', + qs_elements=['domain_id=%s' % + self.domain_data. + domain_id]), + complete_qs=True, status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -1881,7 +1929,11 @@ def test_revoke_role_user_domain_exists(self): status_code=200, json=self.domain_data.json_response), dict(method='GET', - uri=self.get_mock_url(resource='users'), + uri=self.get_mock_url(resource='users', + qs_elements=['domain_id=%s' % + self.domain_data. + domain_id]), + complete_qs=True, status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -1917,7 +1969,11 @@ def test_revoke_role_user_domain_exists(self): status_code=200, json=self.domain_data.json_response), dict(method='GET', - uri=self.get_mock_url(resource='users'), + uri=self.get_mock_url(resource='users', + qs_elements=['domain_id=%s' % + self.domain_data. + domain_id]), + complete_qs=True, status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -1953,7 +2009,11 @@ def test_revoke_role_user_domain_exists(self): status_code=200, json=self.domain_data.json_response), dict(method='GET', - uri=self.get_mock_url(resource='users'), + uri=self.get_mock_url(resource='users', + qs_elements=['domain_id=%s' % + self.domain_data. + domain_id]), + complete_qs=True, status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -1989,7 +2049,11 @@ def test_revoke_role_user_domain_exists(self): status_code=200, json=self.domain_data.json_response), dict(method='GET', - uri=self.get_mock_url(resource='users'), + uri=self.get_mock_url(resource='users', + qs_elements=['domain_id=%s' % + self.domain_data. + domain_id]), + complete_qs=True, status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -2478,7 +2542,11 @@ def test_grant_both_project_and_domain(self): status_code=200, json=self.domain_data.json_response), dict(method='GET', - uri=self.get_mock_url(resource='users'), + uri=self.get_mock_url(resource='users', + qs_elements=['domain_id=%s' % + self.domain_data. + domain_id]), + complete_qs=True, status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -2527,7 +2595,11 @@ def test_revoke_both_project_and_domain(self): status_code=200, json=self.domain_data.json_response), dict(method='GET', - uri=self.get_mock_url(resource='users'), + uri=self.get_mock_url(resource='users', + qs_elements=['domain_id=%s' % + self.domain_data. + domain_id]), + complete_qs=True, status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', From 81e2bb4aaae1e8cd1317317c5a87c7fc8e12a5cf Mon Sep 17 00:00:00 2001 From: huangshan Date: Tue, 3 Jul 2018 15:28:25 +0800 Subject: [PATCH 2113/3836] Add vip_qos_policy_id options for loadbalancer This is a follow up of octavia-api changes made in: I43aba9d2ae816b1498d16da077936d6bdb62e30a Change-Id: Ied0f0e19d9d34c04737324536757b1404c360a0d --- openstack/load_balancer/v2/load_balancer.py | 4 +++- openstack/tests/unit/load_balancer/test_load_balancer.py | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/openstack/load_balancer/v2/load_balancer.py b/openstack/load_balancer/v2/load_balancer.py index c41d2d9dc..e1b9fe579 100644 --- a/openstack/load_balancer/v2/load_balancer.py +++ b/openstack/load_balancer/v2/load_balancer.py @@ -30,7 +30,7 @@ class LoadBalancer(resource.Resource): _query_mapping = resource.QueryParameters( 'description', 'flavor', 'name', 'project_id', 'provider', 'vip_address', 'vip_network_id', 'vip_port_id', 'vip_subnet_id', - 'provisioning_status', 'operating_status', + 'vip_qos_policy_id', 'provisioning_status', 'operating_status', is_admin_state_up='admin_state_up' ) @@ -67,6 +67,8 @@ class LoadBalancer(resource.Resource): vip_port_id = resource.Body('vip_port_id') #: VIP subnet ID vip_subnet_id = resource.Body('vip_subnet_id') + # VIP qos policy id + vip_qos_policy_id = resource.Body('vip_qos_policy_id') def delete(self, session, error_message=None): request = self._prepare_request() diff --git a/openstack/tests/unit/load_balancer/test_load_balancer.py b/openstack/tests/unit/load_balancer/test_load_balancer.py index 53a2bf541..a6e47a21c 100644 --- a/openstack/tests/unit/load_balancer/test_load_balancer.py +++ b/openstack/tests/unit/load_balancer/test_load_balancer.py @@ -35,6 +35,7 @@ 'vip_network_id': uuid.uuid4(), 'vip_port_id': uuid.uuid4(), 'vip_subnet_id': uuid.uuid4(), + 'vip_qos_policy_id': uuid.uuid4(), } @@ -80,6 +81,8 @@ def test_make_it(self): test_load_balancer.vip_port_id) self.assertEqual(EXAMPLE['vip_subnet_id'], test_load_balancer.vip_subnet_id) + self.assertEqual(EXAMPLE['vip_qos_policy_id'], + test_load_balancer.vip_qos_policy_id) def test_delete_non_cascade(self): sess = mock.Mock() From ed9cd865733e1c875ebbf1895c319fc74826baa7 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 28 Jun 2018 13:33:30 -0500 Subject: [PATCH 2114/3836] Only send force parameter to live migration if supported force isn't supported until microversion 2.30. Add a test for that microversion and only send it if the server supports it. Change-Id: Ic45e9cccac10d432162d27f9b42da4f4eb1ff167 Story: 2002752 Task: 22608 --- openstack/compute/v2/_proxy.py | 34 ++- openstack/compute/v2/server.py | 83 ++++++- openstack/tests/unit/compute/v2/test_proxy.py | 5 +- .../tests/unit/compute/v2/test_server.py | 206 ++++++++++++++---- openstack/utils.py | 26 +++ 5 files changed, 291 insertions(+), 63 deletions(-) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index fc0f2747e..97802584d 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -1167,15 +1167,31 @@ def migrate_server(self, server): server = self._get_resource(_server.Server, server) server.migrate(self) - def live_migrate_server(self, server, host=None, force=False): - """Migrate a server from one host to target host - - :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. - :param host: The host to which to migrate the server - :param force: Force a live-migration by not verifying the provided - destination host by the scheduler. + def live_migrate_server( + self, server, host=None, force=False, block_migration=None): + """Live migrate a server from one host to target host + + :param server: + Either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance. + :param str host: + The host to which to migrate the server. If the Nova service is + too old, the host parameter implies force=True which causes the + Nova scheduler to be bypassed. On such clouds, a ``ValueError`` + will be thrown if ``host`` is given without ``force``. + :param bool force: + Force a live-migration by not verifying the provided destination + host by the scheduler. This is unsafe and not recommended. + :param block_migration: + Perform a block live migration to the destination host by the + scheduler. Can be 'auto', True or False. Some clouds are too old + to support 'auto', in which case a ValueError will be thrown. If + omitted, the value will be 'auto' on clouds that support it, and + False on clouds that do not. :returns: None """ server = self._get_resource(_server.Server, server) - server.live_migrate(self, host, force) + server.live_migrate( + self, host, + force=force, + block_migration=block_migration) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index fd7d93c27..7408bf83b 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -171,7 +171,7 @@ def _prepare_request(self, requires_id=True, prepend_key=True): return request - def _action(self, session, body): + def _action(self, session, body, microversion=None): """Preform server actions given the message body.""" # NOTE: This is using Server.base_path instead of self.base_path # as both Server and ServerDetail instances can be acted on, but @@ -179,7 +179,7 @@ def _action(self, session, body): url = utils.urljoin(Server.base_path, self.id, 'action') headers = {'Accept': ''} return session.post( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=microversion) def change_password(self, session, new_password): """Change the administrator password to the given password.""" @@ -363,15 +363,80 @@ def get_console_output(self, session, length=None): resp = self._action(session, body) return resp.json() - def live_migrate(self, session, host, force): + def live_migrate(self, session, host, force, block_migration): + if utils.supports_microversion(session, '2.30'): + return self._live_migrate_30( + session, host, + force=force, + block_migration=block_migration) + elif utils.supports_microversion(session, '2.25'): + return self._live_migrate_25( + session, host, + force=force, + block_migration=block_migration) + else: + return self._live_migrate( + session, host, + force=force, + block_migration=block_migration) + + def _live_migrate_30(self, session, host, force, block_migration): + microversion = '2.30' + body = {'host': None} + if block_migration is None: + block_migration = 'auto' + body['block_migration'] = block_migration + if host: + body['host'] = host + if force: + body['force'] = force + self._action( + session, {'os-migrateLive': body}, microversion=microversion) + + def _live_migrate_25(self, session, host, force, block_migration): + microversion = '2.25' + body = {'host': None} + if block_migration is None: + block_migration = 'auto' + body['block_migration'] = block_migration + if host: + body['host'] = host + if not force: + raise ValueError( + "Live migration on this cloud implies 'force'" + " if the 'host' option has been given and it is not" + " possible to disable. It is recommended to not use 'host'" + " at all on this cloud as it is inherently unsafe, but if" + " it is unavoidable, please supply 'force=True' so that it" + " is clear you understand the risks.") + self._action( + session, {'os-migrateLive': body}, microversion=microversion) + + def _live_migrate(self, session, host, force, block_migration): + microversion = None + # disk_over_commit is not exposed because post 2.25 it has been + # removed and no SDK user is depending on it today. body = { - "os-migrateLive": { - "host": host, - "block_migration": "auto", - "force": force - } + 'host': None, + 'disk_over_commit': False, } - self._action(session, body) + if block_migration == 'auto': + raise ValueError( + "Live migration on this cloud does not support 'auto' as" + " a parameter to block_migration, but only True and False.") + body['block_migration'] = block_migration or False + if host: + body['host'] = host + if not force: + raise ValueError( + "Live migration on this cloud implies 'force'" + " if the 'host' option has been given and it is not" + " possible to disable. It is recommended to not use 'host'" + " at all on this cloud as it is inherently unsafe, but if" + " it is unavoidable, please supply 'force=True' so that it" + " is clear you understand the risks.") + self._action( + session, {'os-migrateLive': body}, microversion=microversion) class ServerDetail(Server): diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index dd9ff2292..8c41f956d 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -526,5 +526,6 @@ def test_force_service_down(self): def test_live_migrate_server(self): self._verify('openstack.compute.v2.server.Server.live_migrate', self.proxy.live_migrate_server, - method_args=["value", "host1", "force"], - expected_args=["host1", "force"]) + method_args=["value", "host1", False], + expected_args=["host1"], + expected_kwargs={'force': False, 'block_migration': None}) diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 3de08fc04..5bf52719b 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -11,6 +11,7 @@ # under the License. import mock +import six from openstack.tests.unit import base from openstack.compute.v2 import server @@ -193,7 +194,7 @@ def test_change_password(self): body = {"changePassword": {"adminPass": "a"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_reboot(self): sot = server.Server(**EXAMPLE) @@ -204,7 +205,7 @@ def test_reboot(self): body = {"reboot": {"type": "HARD"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_force_delete(self): sot = server.Server(**EXAMPLE) @@ -215,7 +216,7 @@ def test_force_delete(self): body = {'forceDelete': None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_rebuild(self): sot = server.Server(**EXAMPLE) @@ -246,7 +247,7 @@ def test_rebuild(self): } headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_rebuild_minimal(self): sot = server.Server(**EXAMPLE) @@ -270,7 +271,7 @@ def test_rebuild_minimal(self): } headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_resize(self): sot = server.Server(**EXAMPLE) @@ -281,7 +282,7 @@ def test_resize(self): body = {"resize": {"flavorRef": "2"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_confirm_resize(self): sot = server.Server(**EXAMPLE) @@ -292,7 +293,7 @@ def test_confirm_resize(self): body = {"confirmResize": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_revert_resize(self): sot = server.Server(**EXAMPLE) @@ -303,7 +304,7 @@ def test_revert_resize(self): body = {"revertResize": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_create_image(self): sot = server.Server(**EXAMPLE) @@ -316,7 +317,7 @@ def test_create_image(self): body = {"createImage": {'name': name, 'metadata': metadata}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_create_image_minimal(self): sot = server.Server(**EXAMPLE) @@ -328,7 +329,7 @@ def test_create_image_minimal(self): body = {"createImage": {'name': name}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_add_security_group(self): sot = server.Server(**EXAMPLE) @@ -339,7 +340,7 @@ def test_add_security_group(self): body = {"addSecurityGroup": {"name": "group"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_remove_security_group(self): sot = server.Server(**EXAMPLE) @@ -350,7 +351,7 @@ def test_remove_security_group(self): body = {"removeSecurityGroup": {"name": "group"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_reset_state(self): sot = server.Server(**EXAMPLE) @@ -361,7 +362,7 @@ def test_reset_state(self): body = {"os-resetState": {"state": 'active'}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_add_fixed_ip(self): sot = server.Server(**EXAMPLE) @@ -373,7 +374,7 @@ def test_add_fixed_ip(self): body = {"addFixedIp": {"networkId": "NETWORK-ID"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_remove_fixed_ip(self): sot = server.Server(**EXAMPLE) @@ -385,7 +386,7 @@ def test_remove_fixed_ip(self): body = {"removeFixedIp": {"address": "ADDRESS"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_add_floating_ip(self): sot = server.Server(**EXAMPLE) @@ -397,7 +398,7 @@ def test_add_floating_ip(self): body = {"addFloatingIp": {"address": "FLOATING-IP"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_add_floating_ip_with_fixed_addr(self): sot = server.Server(**EXAMPLE) @@ -410,7 +411,7 @@ def test_add_floating_ip_with_fixed_addr(self): "fixed_address": "FIXED-ADDR"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_remove_floating_ip(self): sot = server.Server(**EXAMPLE) @@ -422,7 +423,7 @@ def test_remove_floating_ip(self): body = {"removeFloatingIp": {"address": "I-AM-FLOATING"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_backup(self): sot = server.Server(**EXAMPLE) @@ -435,7 +436,7 @@ def test_backup(self): "rotation": 1}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_pause(self): sot = server.Server(**EXAMPLE) @@ -447,7 +448,7 @@ def test_pause(self): body = {"pause": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_unpause(self): sot = server.Server(**EXAMPLE) @@ -459,7 +460,7 @@ def test_unpause(self): body = {"unpause": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_suspend(self): sot = server.Server(**EXAMPLE) @@ -471,7 +472,7 @@ def test_suspend(self): body = {"suspend": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_resume(self): sot = server.Server(**EXAMPLE) @@ -483,7 +484,7 @@ def test_resume(self): body = {"resume": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_lock(self): sot = server.Server(**EXAMPLE) @@ -495,7 +496,7 @@ def test_lock(self): body = {"lock": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_unlock(self): sot = server.Server(**EXAMPLE) @@ -507,7 +508,7 @@ def test_unlock(self): body = {"unlock": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_rescue(self): sot = server.Server(**EXAMPLE) @@ -519,7 +520,7 @@ def test_rescue(self): body = {"rescue": {}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_rescue_with_options(self): sot = server.Server(**EXAMPLE) @@ -532,7 +533,7 @@ def test_rescue_with_options(self): 'rescue_image_ref': 'IMG-ID'}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_unrescue(self): sot = server.Server(**EXAMPLE) @@ -544,7 +545,7 @@ def test_unrescue(self): body = {"unrescue": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_evacuate(self): sot = server.Server(**EXAMPLE) @@ -556,7 +557,7 @@ def test_evacuate(self): body = {"evacuate": {}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_evacuate_with_options(self): sot = server.Server(**EXAMPLE) @@ -570,7 +571,7 @@ def test_evacuate_with_options(self): 'force': True}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_start(self): sot = server.Server(**EXAMPLE) @@ -582,7 +583,7 @@ def test_start(self): body = {"os-start": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_stop(self): sot = server.Server(**EXAMPLE) @@ -594,7 +595,7 @@ def test_stop(self): body = {"os-stop": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_shelve(self): sot = server.Server(**EXAMPLE) @@ -606,7 +607,7 @@ def test_shelve(self): body = {"shelve": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_unshelve(self): sot = server.Server(**EXAMPLE) @@ -618,7 +619,7 @@ def test_unshelve(self): body = {"unshelve": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_migrate(self): sot = server.Server(**EXAMPLE) @@ -631,7 +632,7 @@ def test_migrate(self): headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) def test_get_console_output(self): sot = server.Server(**EXAMPLE) @@ -643,7 +644,7 @@ def test_get_console_output(self): body = {'os-getConsoleOutput': {}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) res = sot.get_console_output(self.sess, length=1) @@ -653,23 +654,142 @@ def test_get_console_output(self): headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion=None) - def test_live_migrate(self): + def test_live_migrate_no_force(self): sot = server.Server(**EXAMPLE) - res = sot.live_migrate(self.sess, host='HOST2', force=False) + class FakeEndpointData(object): + min_microversion = None + max_microversion = None + self.sess.get_endpoint_data.return_value = FakeEndpointData() + + ex = self.assertRaises( + ValueError, + sot.live_migrate, + self.sess, host='HOST2', force=False, block_migration=False) + self.assertIn( + "Live migration on this cloud implies 'force'", + six.text_type(ex)) + + def test_live_migrate_no_microversion_force_true(self): + sot = server.Server(**EXAMPLE) + + class FakeEndpointData(object): + min_microversion = None + max_microversion = None + self.sess.get_endpoint_data.return_value = FakeEndpointData() + + res = sot.live_migrate( + self.sess, host='HOST2', force=True, block_migration=False) + + self.assertIsNone(res) + url = 'servers/IDENTIFIER/action' + body = { + 'os-migrateLive': { + 'host': 'HOST2', + 'disk_over_commit': False, + 'block_migration': False + } + } + + headers = {'Accept': ''} + self.sess.post.assert_called_with( + url, json=body, headers=headers, microversion=None) + + def test_live_migrate_25(self): + sot = server.Server(**EXAMPLE) + + class FakeEndpointData(object): + min_microversion = '2.1' + max_microversion = '2.25' + self.sess.get_endpoint_data.return_value = FakeEndpointData() + + res = sot.live_migrate( + self.sess, host='HOST2', force=True, block_migration=False) + + self.assertIsNone(res) + url = 'servers/IDENTIFIER/action' + body = { + "os-migrateLive": { + 'block_migration': False, + 'host': 'HOST2', + } + } + + headers = {'Accept': ''} + self.sess.post.assert_called_with( + url, json=body, headers=headers, microversion='2.25') + + def test_live_migrate_25_default_block(self): + sot = server.Server(**EXAMPLE) + + class FakeEndpointData(object): + min_microversion = '2.1' + max_microversion = '2.25' + self.sess.get_endpoint_data.return_value = FakeEndpointData() + + res = sot.live_migrate( + self.sess, host='HOST2', force=True, block_migration=None) self.assertIsNone(res) url = 'servers/IDENTIFIER/action' body = { "os-migrateLive": { - "host": 'HOST2', - "block_migration": "auto", - "force": False + 'block_migration': 'auto', + 'host': 'HOST2', + } + } + + headers = {'Accept': ''} + self.sess.post.assert_called_with( + url, json=body, headers=headers, microversion='2.25') + + def test_live_migrate_30(self): + sot = server.Server(**EXAMPLE) + + class FakeEndpointData(object): + min_microversion = '2.1' + max_microversion = '2.30' + self.sess.get_endpoint_data.return_value = FakeEndpointData() + + res = sot.live_migrate( + self.sess, host='HOST2', force=False, block_migration=False) + + self.assertIsNone(res) + url = 'servers/IDENTIFIER/action' + body = { + 'os-migrateLive': { + 'block_migration': False, + 'host': 'HOST2' + } + } + + headers = {'Accept': ''} + self.sess.post.assert_called_with( + url, json=body, headers=headers, microversion='2.30') + + def test_live_migrate_30_force(self): + sot = server.Server(**EXAMPLE) + + class FakeEndpointData(object): + min_microversion = '2.1' + max_microversion = '2.30' + self.sess.get_endpoint_data.return_value = FakeEndpointData() + + res = sot.live_migrate( + self.sess, host='HOST2', force=True, block_migration=None) + + self.assertIsNone(res) + url = 'servers/IDENTIFIER/action' + body = { + 'os-migrateLive': { + 'block_migration': 'auto', + 'host': 'HOST2', + 'force': True, } } headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers) + url, json=body, headers=headers, microversion='2.30') diff --git a/openstack/utils.py b/openstack/utils.py index dac11967b..8af0b07bf 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -15,6 +15,7 @@ import time import deprecation +from keystoneauth1 import discover from openstack import _log from openstack import exceptions @@ -128,3 +129,28 @@ def __getitem__(self, key): if t[1] is not None: keys.append(t[1]) return keys + + +def supports_microversion(adapter, microversion): + """Determine if the given adapter supports the given microversion. + + Checks the min and max microversion asserted by the service and checks + to make sure that ``min <= microversion <= max``. + + :param adapter: + :class:`~keystoneauth1.adapter.Adapter` instance. + :param str microversion: + String containing the desired microversion. + :returns: True if the service supports the microversion. + :rtype: bool + """ + + endpoint_data = adapter.get_endpoint_data() + if (endpoint_data.min_microversion + and endpoint_data.max_microversion + and discover.version_between( + endpoint_data.min_microversion, + endpoint_data.max_microversion, + microversion)): + return True + return False From 1fbfc59037bbf96e09eb7430515fe34046030d9a Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Tue, 3 Jul 2018 18:09:06 +0200 Subject: [PATCH 2115/3836] Handle Munch objects in proxies First, this patch adds Resource._from_munch. This method is intended as temporary means to bridge shade-style Munch objects and original openstacksdk resources. The base implementation just uses Resource.__init__ and can be overridden in more complex cases. Second, Proxy._get_resource was updated to accept Munch objects and build resources from them. This makes proxy methods transparently accept Munch objects as long as they contain enough information to construct the required resource. Change-Id: I098b0844d420b08fec523789102061dab2ad1e7c --- openstack/proxy.py | 7 +++++-- openstack/resource.py | 13 +++++++++++++ openstack/tests/unit/test_proxy.py | 16 ++++++++++++++++ openstack/tests/unit/test_resource.py | 23 +++++++++++++++++++++++ 4 files changed, 57 insertions(+), 2 deletions(-) diff --git a/openstack/proxy.py b/openstack/proxy.py index 33f3505e0..bc55b5218 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -54,14 +54,17 @@ def _get_resource(self, resource_type, value, **attrs): :class:`~openstack.resource.Resource` with a ``from_id`` method. :param value: The ID of a resource or an object of ``resource_type`` - class if using an existing instance, or None to create a - new instance. + class if using an existing instance, or ``munch.Munch``, + or None to create a new instance. :param path_args: A dict containing arguments for forming the request URL, if needed. """ if value is None: # Create a bare resource res = resource_type.new(**attrs) + elif isinstance(value, dict): + res = resource_type._from_munch(value) + res._update(**attrs) elif not isinstance(value, resource_type): # Create from an ID res = resource_type.new(id=value, **attrs) diff --git a/openstack/resource.py b/openstack/resource.py index d7d49e73c..34dfc2e20 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -560,6 +560,19 @@ def existing(cls, **kwargs): """ return cls(_synchronized=True, **kwargs) + @classmethod + def _from_munch(cls, obj, synchronized=True): + """Create an instance from a ``munch.Munch`` object. + + This is intended as a temporary measure to convert between shade-style + Munch objects and original openstacksdk resources. + + :param obj: a ``munch.Munch`` object to convert from. + :param bool synchronized: whether this object already exists on server + Must be set to ``False`` for newly created objects. + """ + return cls(_synchronized=synchronized, **obj) + def to_dict(self, body=True, headers=True, ignore_none=False): """Return a dictionary of this resource's contents diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index 152c9c105..54bc6be19 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -11,6 +11,7 @@ # under the License. import mock +import munch from openstack.tests.unit import base from openstack import exceptions @@ -157,6 +158,21 @@ def test__get_resource_from_resource(self): res._update.assert_called_once_with(**attrs) self.assertEqual(result, res) + def test__get_resource_from_munch(self): + cls = mock.Mock() + res = mock.Mock(spec=resource.Resource) + res._update = mock.Mock() + cls._from_munch.return_value = res + + m = munch.Munch(answer=42) + attrs = {"first": "Brian", "last": "Curtin"} + + result = self.fake_proxy._get_resource(cls, m, **attrs) + + cls._from_munch.assert_called_once_with(m) + res._update.assert_called_once_with(**attrs) + self.assertEqual(result, res) + class TestProxyDelete(base.TestCase): diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 41ed10959..1f7eff912 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -14,6 +14,7 @@ from keystoneauth1 import adapter import mock +import munch import requests import six @@ -791,6 +792,28 @@ class Test(resource.Resource): self.assertNotIn("attr", sot._body.dirty) self.assertEqual(value, sot.attr) + def test_from_munch_new(self): + class Test(resource.Resource): + attr = resource.Body("body_attr") + + value = "value" + orig = munch.Munch(body_attr=value) + sot = Test._from_munch(orig, synchronized=False) + + self.assertIn("body_attr", sot._body.dirty) + self.assertEqual(value, sot.attr) + + def test_from_munch_existing(self): + class Test(resource.Resource): + attr = resource.Body("body_attr") + + value = "value" + orig = munch.Munch(body_attr=value) + sot = Test._from_munch(orig) + + self.assertNotIn("body_attr", sot._body.dirty) + self.assertEqual(value, sot.attr) + def test__prepare_request_with_id(self): class Test(resource.Resource): base_path = "/something" From 0eedfb106cfb3e1068e329f416dc008123bbe350 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 3 Jul 2018 16:27:29 -0400 Subject: [PATCH 2116/3836] Fix for passing dict for get_* methods We are not accepting dicts for the name_or_id parameter, only objects. Change-Id: I31f0f127f71f10a2f11f89e10ac8911816786963 --- openstack/cloud/_utils.py | 7 +++++-- openstack/tests/unit/cloud/test__utils.py | 5 +++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index db83137fd..731db5763 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -171,7 +171,9 @@ def _get_entity(cloud, resource, name_or_id, filters, **kwargs): get_<>_by_id or search_s methods(Example: network) or a callable to invoke. :param string name_or_id: - The name or ID of the entity being filtered or a dict + The name or ID of the entity being filtered or an object or dict. + If this is an object/dict with an 'id' attr/key, we return it and + bypass resource lookup. :param filters: A dictionary of meta data to use for further filtering. OR @@ -185,7 +187,8 @@ def _get_entity(cloud, resource, name_or_id, filters, **kwargs): # an additional call, it's simple enough to test to see if we got an # object and just short-circuit return it. - if hasattr(name_or_id, 'id'): + if (hasattr(name_or_id, 'id') or + (isinstance(name_or_id, dict) and 'id' in name_or_id)): return name_or_id # If a uuid is passed short-circuit it calling the diff --git a/openstack/tests/unit/cloud/test__utils.py b/openstack/tests/unit/cloud/test__utils.py index ec7ac6150..bcb04dc12 100644 --- a/openstack/tests/unit/cloud/test__utils.py +++ b/openstack/tests/unit/cloud/test__utils.py @@ -326,6 +326,11 @@ def test_get_entity_pass_object(self): self.cloud.use_direct_get = True self.assertEqual(obj, _utils._get_entity(self.cloud, '', obj, {})) + def test_get_entity_pass_dict(self): + d = dict(id=uuid4().hex) + self.cloud.use_direct_get = True + self.assertEqual(d, _utils._get_entity(self.cloud, '', d, {})) + def test_get_entity_no_use_direct_get(self): # test we are defaulting to the search_ methods # if the use_direct_get flag is set to False(default). From cf47f3f7c01e3e878cdbc4c38ba7bd27c65df230 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 7 Jul 2018 09:34:07 -0400 Subject: [PATCH 2117/3836] Add support for processing insecure python-openstackclient uses this on the command line - and has to go through a bunch of shenanigans to get it processed correct. Just do it ourselves from the get-go so that we can just remove the stress from osc. Change-Id: I25ce459eb38ec246260e1792035f26d46729c920 --- openstack/config/cloud_region.py | 13 +++++++++---- openstack/tests/unit/config/test_cloud_config.py | 10 ++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 0bc7f68f7..d32cac4f5 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -174,11 +174,16 @@ def set_session_constructor(self, session_constructor): def get_requests_verify_args(self): """Return the verify and cert values for the requests library.""" - if self.config.get('verify') and self.config.get('cacert'): - verify = self.config.get('cacert') + insecure = self.config.get('insecure', False) + verify = self.config.get('verify', True) + cacert = self.config.get('cacert') + # Insecure is the most aggressive setting, so it wins + if insecure: + verify = False + if verify and cacert: + verify = cacert else: - verify = self.config.get('verify') - if self.config.get('cacert'): + if cacert: warnings.warn( "You are specifying a cacert for the cloud {full_name}" " but also to ignore the host verification. The host SSL" diff --git a/openstack/tests/unit/config/test_cloud_config.py b/openstack/tests/unit/config/test_cloud_config.py index dc0fc080a..fb2f54eb1 100644 --- a/openstack/tests/unit/config/test_cloud_config.py +++ b/openstack/tests/unit/config/test_cloud_config.py @@ -99,6 +99,11 @@ def test_verify(self): (verify, cert) = cc.get_requests_verify_args() self.assertTrue(verify) + config_dict['insecure'] = True + cc = cloud_region.CloudRegion("test1", "region-xx", config_dict) + (verify, cert) = cc.get_requests_verify_args() + self.assertFalse(verify) + def test_verify_cacert(self): config_dict = copy.deepcopy(fake_config_dict) config_dict['cacert'] = "certfile" @@ -113,6 +118,11 @@ def test_verify_cacert(self): (verify, cert) = cc.get_requests_verify_args() self.assertEqual("certfile", verify) + config_dict['insecure'] = True + cc = cloud_region.CloudRegion("test1", "region-xx", config_dict) + (verify, cert) = cc.get_requests_verify_args() + self.assertEqual(False, verify) + def test_cert_with_key(self): config_dict = copy.deepcopy(fake_config_dict) config_dict['cacert'] = None From 38fafc76809da866ed12e5e5cd3417fba77415ce Mon Sep 17 00:00:00 2001 From: Feilong Wang Date: Tue, 10 Jul 2018 10:59:19 +1200 Subject: [PATCH 2118/3836] Fix Magnum cluster update The only attribute can be updated is 'node_count' not 'name'[1]. This patch fixes it. [1] https://github.com/openstack/magnum/blob/master/magnum/api/validation.py#L32 Change-Id: Idb7578f63e688d9fdc85a8514d00ad03b99d8f83 --- openstack/cloud/openstackcloud.py | 2 +- openstack/tests/unit/cloud/test_coe_clusters.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 30719c6d6..cf767a813 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -8756,7 +8756,7 @@ def delete_coe_cluster(self, name_or_id): return True - @_utils.valid_kwargs('name') + @_utils.valid_kwargs('node_count') def update_coe_cluster(self, name_or_id, operation, **kwargs): """Update a COE cluster. diff --git a/openstack/tests/unit/cloud/test_coe_clusters.py b/openstack/tests/unit/cloud/test_coe_clusters.py index 3d6055f15..1ef38cb4b 100644 --- a/openstack/tests/unit/cloud/test_coe_clusters.py +++ b/openstack/tests/unit/cloud/test_coe_clusters.py @@ -146,8 +146,8 @@ def test_update_coe_cluster(self): validate=dict( json=[{ u'op': u'replace', - u'path': u'/name', - u'value': u'new-coe-cluster' + u'path': u'/node_count', + u'value': 3 }] )), dict( @@ -157,7 +157,6 @@ def test_update_coe_cluster(self): # to be valid. json=dict(clusters=[coe_cluster_obj.toDict()])), ]) - new_name = 'new-coe-cluster' self.cloud.update_coe_cluster( - coe_cluster_obj.uuid, 'replace', name=new_name) + coe_cluster_obj.uuid, 'replace', node_count=3) self.assert_calls() From abfc8580f41a4e49463788d07e2fdd1f5ddd9b48 Mon Sep 17 00:00:00 2001 From: Lingxian Kong Date: Tue, 10 Jul 2018 16:55:29 +1200 Subject: [PATCH 2119/3836] Support to wait for load balancer to be ACTIVE This patch is adding a utility method `wait_for_load_balancer` that can be used either in user's application or the ansible module. Story: 2002924 Task: 22907 Change-Id: I68daeb44d9206892078e8d199b076b2f93234241 --- openstack/load_balancer/v2/_proxy.py | 8 ++ .../tests/functional/load_balancer/base.py | 58 ------------ .../load_balancer/v2/test_load_balancer.py | 90 +++++++++---------- 3 files changed, 48 insertions(+), 108 deletions(-) delete mode 100644 openstack/tests/functional/load_balancer/base.py diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index a81df06b0..ab843e5f5 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -18,6 +18,7 @@ from openstack.load_balancer.v2 import member as _member from openstack.load_balancer.v2 import pool as _pool from openstack import proxy +from openstack import resource class Proxy(proxy.Proxy): @@ -106,6 +107,13 @@ def update_load_balancer(self, load_balancer, **attrs): """ return self._update(_lb.LoadBalancer, load_balancer, **attrs) + def wait_for_load_balancer(self, name_or_id, status='ACTIVE', + failures=['ERROR'], interval=2, wait=300): + lb = self._find(_lb.LoadBalancer, name_or_id, ignore_missing=False) + + return resource.wait_for_status(self, lb, status, failures, interval, + wait, attribute='provisioning_status') + def create_listener(self, **attrs): """Create a new listener from attributes diff --git a/openstack/tests/functional/load_balancer/base.py b/openstack/tests/functional/load_balancer/base.py deleted file mode 100644 index 975cd19cc..000000000 --- a/openstack/tests/functional/load_balancer/base.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright 2017 Rackspace, US 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 time - -from openstack import exceptions -from openstack.tests.functional import base - - -class BaseLBFunctionalTest(base.BaseFunctionalTest): - - def lb_wait_for_status(self, lb, status, failures, interval=1, wait=120): - """Wait for load balancer to be in a particular provisioning status. - - :param lb: The load balancer to wait on to reach the status. - :type lb: :class:`~openstack.load_blanacer.v2.load_balancer - :param status: Desired status of the resource. - :param list failures: Statuses that would indicate the transition - failed such as 'ERROR'. - :param interval: Number of seconds to wait between checks. - :param wait: Maximum number of seconds to wait for transition. - Note, most actions should easily finish in 120 seconds, - but for load balancer create slow hosts can take up to - ten minutes for nova to fully boot a VM. - :return: None - :raises: :class:`~openstack.exceptions.ResourceTimeout` transition - to status failed to occur in wait seconds. - :raises: :class:`~openstack.exceptions.ResourceFailure` resource - transitioned to one of the failure states. - """ - - total_sleep = 0 - if failures is None: - failures = [] - - while total_sleep < wait: - lb = self.conn.load_balancer.get_load_balancer(lb.id) - if lb.provisioning_status == status: - return None - if lb.provisioning_status in failures: - msg = ("Load Balancer %s transitioned to failure state %s" % - (lb.id, lb.provisioning_status)) - raise exceptions.ResourceFailure(msg) - time.sleep(interval) - total_sleep += interval - msg = "Timeout waiting for Load Balancer %s to transition to %s" % ( - lb.id, status) - raise exceptions.ResourceTimeout(msg) diff --git a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py index a78229e8d..ad93ea26e 100644 --- a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py +++ b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py @@ -17,10 +17,10 @@ from openstack.load_balancer.v2 import load_balancer from openstack.load_balancer.v2 import member from openstack.load_balancer.v2 import pool -from openstack.tests.functional.load_balancer import base as lb_base +from openstack.tests.functional import base -class TestLoadBalancer(lb_base.BaseLBFunctionalTest): +class TestLoadBalancer(base.BaseFunctionalTest): HM_ID = None L7POLICY_ID = None @@ -70,8 +70,8 @@ def setUp(self): self.assertEqual(self.LB_NAME, test_lb.name) # Wait for the LB to go ACTIVE. On non-virtualization enabled hosts # it can take nova up to ten minutes to boot a VM. - self.lb_wait_for_status(test_lb, status='ACTIVE', - failures=['ERROR'], interval=1, wait=600) + self.conn.load_balancer.wait_for_load_balancer(test_lb.id, interval=1, + wait=600) self.LB_ID = test_lb.id test_listener = self.conn.load_balancer.create_listener( @@ -80,8 +80,7 @@ def setUp(self): assert isinstance(test_listener, listener.Listener) self.assertEqual(self.LISTENER_NAME, test_listener.name) self.LISTENER_ID = test_listener.id - self.lb_wait_for_status(test_lb, status='ACTIVE', - failures=['ERROR']) + self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) test_pool = self.conn.load_balancer.create_pool( name=self.POOL_NAME, protocol=self.PROTOCOL, @@ -89,8 +88,7 @@ def setUp(self): assert isinstance(test_pool, pool.Pool) self.assertEqual(self.POOL_NAME, test_pool.name) self.POOL_ID = test_pool.id - self.lb_wait_for_status(test_lb, status='ACTIVE', - failures=['ERROR']) + self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) test_member = self.conn.load_balancer.create_member( pool=self.POOL_ID, name=self.MEMBER_NAME, @@ -99,8 +97,7 @@ def setUp(self): assert isinstance(test_member, member.Member) self.assertEqual(self.MEMBER_NAME, test_member.name) self.MEMBER_ID = test_member.id - self.lb_wait_for_status(test_lb, status='ACTIVE', - failures=['ERROR']) + self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) test_hm = self.conn.load_balancer.create_health_monitor( pool_id=self.POOL_ID, name=self.HM_NAME, delay=self.DELAY, @@ -109,8 +106,7 @@ def setUp(self): assert isinstance(test_hm, health_monitor.HealthMonitor) self.assertEqual(self.HM_NAME, test_hm.name) self.HM_ID = test_hm.id - self.lb_wait_for_status(test_lb, status='ACTIVE', - failures=['ERROR']) + self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) test_l7policy = self.conn.load_balancer.create_l7_policy( listener_id=self.LISTENER_ID, name=self.L7POLICY_NAME, @@ -118,8 +114,7 @@ def setUp(self): assert isinstance(test_l7policy, l7_policy.L7Policy) self.assertEqual(self.L7POLICY_NAME, test_l7policy.name) self.L7POLICY_ID = test_l7policy.id - self.lb_wait_for_status(test_lb, status='ACTIVE', - failures=['ERROR']) + self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) test_l7rule = self.conn.load_balancer.create_l7_rule( l7_policy=self.L7POLICY_ID, compare_type=self.COMPARE_TYPE, @@ -127,35 +122,34 @@ def setUp(self): assert isinstance(test_l7rule, l7_rule.L7Rule) self.assertEqual(self.COMPARE_TYPE, test_l7rule.compare_type) self.L7RULE_ID = test_l7rule.id - self.lb_wait_for_status(test_lb, status='ACTIVE', - failures=['ERROR']) + self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) def tearDown(self): - test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) - self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + self.conn.load_balancer.get_load_balancer(self.LB_ID) + self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) self.conn.load_balancer.delete_l7_rule( self.L7RULE_ID, l7_policy=self.L7POLICY_ID, ignore_missing=False) - self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) self.conn.load_balancer.delete_l7_policy( self.L7POLICY_ID, ignore_missing=False) - self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) self.conn.load_balancer.delete_health_monitor( self.HM_ID, ignore_missing=False) - self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) self.conn.load_balancer.delete_member( self.MEMBER_ID, self.POOL_ID, ignore_missing=False) - self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) self.conn.load_balancer.delete_pool(self.POOL_ID, ignore_missing=False) - self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) self.conn.load_balancer.delete_listener(self.LISTENER_ID, ignore_missing=False) - self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) self.conn.load_balancer.delete_load_balancer( self.LB_ID, ignore_missing=False) @@ -176,17 +170,15 @@ def test_lb_list(self): self.assertIn(self.LB_NAME, names) def test_lb_update(self): - update_lb = self.conn.load_balancer.update_load_balancer( + self.conn.load_balancer.update_load_balancer( self.LB_ID, name=self.UPDATE_NAME) - self.lb_wait_for_status(update_lb, status='ACTIVE', - failures=['ERROR']) + self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) self.assertEqual(self.UPDATE_NAME, test_lb.name) - update_lb = self.conn.load_balancer.update_load_balancer( + self.conn.load_balancer.update_load_balancer( self.LB_ID, name=self.LB_NAME) - self.lb_wait_for_status(update_lb, status='ACTIVE', - failures=['ERROR']) + self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) self.assertEqual(self.LB_NAME, test_lb.name) @@ -207,19 +199,17 @@ def test_listener_list(self): self.assertIn(self.LISTENER_NAME, names) def test_listener_update(self): - test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) + self.conn.load_balancer.get_load_balancer(self.LB_ID) self.conn.load_balancer.update_listener( self.LISTENER_ID, name=self.UPDATE_NAME) - self.lb_wait_for_status(test_lb, status='ACTIVE', - failures=['ERROR']) + self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) test_listener = self.conn.load_balancer.get_listener(self.LISTENER_ID) self.assertEqual(self.UPDATE_NAME, test_listener.name) self.conn.load_balancer.update_listener( self.LISTENER_ID, name=self.LISTENER_NAME) - self.lb_wait_for_status(test_lb, status='ACTIVE', - failures=['ERROR']) + self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) test_listener = self.conn.load_balancer.get_listener(self.LISTENER_ID) self.assertEqual(self.LISTENER_NAME, test_listener.name) @@ -238,17 +228,17 @@ def test_pool_list(self): self.assertIn(self.POOL_NAME, names) def test_pool_update(self): - test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) + self.conn.load_balancer.get_load_balancer(self.LB_ID) self.conn.load_balancer.update_pool(self.POOL_ID, name=self.UPDATE_NAME) - self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) test_pool = self.conn.load_balancer.get_pool(self.POOL_ID) self.assertEqual(self.UPDATE_NAME, test_pool.name) self.conn.load_balancer.update_pool(self.POOL_ID, name=self.POOL_NAME) - self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) test_pool = self.conn.load_balancer.get_pool(self.POOL_ID) self.assertEqual(self.POOL_NAME, test_pool.name) @@ -272,18 +262,18 @@ def test_member_list(self): self.assertIn(self.MEMBER_NAME, names) def test_member_update(self): - test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) + self.conn.load_balancer.get_load_balancer(self.LB_ID) self.conn.load_balancer.update_member(self.MEMBER_ID, self.POOL_ID, name=self.UPDATE_NAME) - self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) test_member = self.conn.load_balancer.get_member(self.MEMBER_ID, self.POOL_ID) self.assertEqual(self.UPDATE_NAME, test_member.name) self.conn.load_balancer.update_member(self.MEMBER_ID, self.POOL_ID, name=self.MEMBER_NAME) - self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) test_member = self.conn.load_balancer.get_member(self.MEMBER_ID, self.POOL_ID) self.assertEqual(self.MEMBER_NAME, test_member.name) @@ -306,17 +296,17 @@ def test_health_monitor_list(self): self.assertIn(self.HM_NAME, names) def test_health_monitor_update(self): - test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) + self.conn.load_balancer.get_load_balancer(self.LB_ID) self.conn.load_balancer.update_health_monitor(self.HM_ID, name=self.UPDATE_NAME) - self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) test_hm = self.conn.load_balancer.get_health_monitor(self.HM_ID) self.assertEqual(self.UPDATE_NAME, test_hm.name) self.conn.load_balancer.update_health_monitor(self.HM_ID, name=self.HM_NAME) - self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) test_hm = self.conn.load_balancer.get_health_monitor(self.HM_ID) self.assertEqual(self.HM_NAME, test_hm.name) @@ -337,18 +327,18 @@ def test_l7_policy_list(self): self.assertIn(self.L7POLICY_NAME, names) def test_l7_policy_update(self): - test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) + self.conn.load_balancer.get_load_balancer(self.LB_ID) self.conn.load_balancer.update_l7_policy( self.L7POLICY_ID, name=self.UPDATE_NAME) - self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) test_l7_policy = self.conn.load_balancer.get_l7_policy( self.L7POLICY_ID) self.assertEqual(self.UPDATE_NAME, test_l7_policy.name) self.conn.load_balancer.update_l7_policy(self.L7POLICY_ID, name=self.L7POLICY_NAME) - self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) test_l7_policy = self.conn.load_balancer.get_l7_policy( self.L7POLICY_ID) self.assertEqual(self.L7POLICY_NAME, test_l7_policy.name) @@ -373,12 +363,12 @@ def test_l7_rule_list(self): self.assertIn(self.L7RULE_ID, ids) def test_l7_rule_update(self): - test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) + self.conn.load_balancer.get_load_balancer(self.LB_ID) self.conn.load_balancer.update_l7_rule(self.L7RULE_ID, l7_policy=self.L7POLICY_ID, rule_value=self.UPDATE_NAME) - self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) test_l7_rule = self.conn.load_balancer.get_l7_rule( self.L7RULE_ID, l7_policy=self.L7POLICY_ID) self.assertEqual(self.UPDATE_NAME, test_l7_rule.rule_value) @@ -386,7 +376,7 @@ def test_l7_rule_update(self): self.conn.load_balancer.update_l7_rule(self.L7RULE_ID, l7_policy=self.L7POLICY_ID, rule_value=self.L7RULE_VALUE) - self.lb_wait_for_status(test_lb, status='ACTIVE', failures=['ERROR']) + self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) test_l7_rule = self.conn.load_balancer.get_l7_rule( self.L7RULE_ID, l7_policy=self.L7POLICY_ID,) self.assertEqual(self.L7RULE_VALUE, test_l7_rule.rule_value) From c1bc7c5879ac60985b3174b9f26decdd8d89a45b Mon Sep 17 00:00:00 2001 From: Dao Cong Tien Date: Wed, 11 Jul 2018 13:23:38 +0700 Subject: [PATCH 2120/3836] Invalid link of doc reference Change-Id: Ibd47a7a9324468bee38f7a5e001360a53b3fec0b --- doc/source/contributor/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst index 444e5f5bf..a4f7f32c1 100644 --- a/doc/source/contributor/index.rst +++ b/doc/source/contributor/index.rst @@ -3,7 +3,7 @@ Contributing to the OpenStack SDK This section of documentation pertains to those who wish to contribute to the development of this SDK. If you're looking for documentation on how to use -the SDK to build applications, please see the `user <../users>`_ section. +the SDK to build applications, please see the `user <../user>`_ section. About the Project ----------------- From 2f8d059075dd0a9cd5e07963e5888a1ce6f90903 Mon Sep 17 00:00:00 2001 From: Feilong Wang Date: Wed, 11 Jul 2018 19:00:50 +1200 Subject: [PATCH 2121/3836] Add Magnum /certificates support Change-Id: Ifdc3bd435b9e65160212a76899858b27ff95e7c8 --- openstack/cloud/openstackcloud.py | 40 ++++++++++++ .../cloud/test_coe_clusters_certificate.py | 61 +++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 openstack/tests/unit/cloud/test_coe_clusters_certificate.py diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 30719c6d6..157d82b56 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -8793,6 +8793,46 @@ def update_coe_cluster(self, name_or_id, operation, **kwargs): new_cluster = self.get_coe_cluster(name_or_id) return new_cluster + def get_coe_cluster_certificate(self, cluster_id): + """Get details about the CA certificate for a cluster by name or ID. + + :param cluster_id: ID of the cluster. + + :returns: Details about the CA certificate for the given cluster. + """ + msg = ("Error fetching CA cert for the cluster {cluster_id}".format( + cluster_id=cluster_id)) + url = "/certificates/{cluster_id}".format(cluster_id=cluster_id) + data = self._container_infra_client.get(url, + error_message=msg) + + return self._get_and_munchify(key=None, data=data) + + def sign_coe_cluster_certificate(self, cluster_id, csr): + """Sign client key and generate the CA certificate for a cluster + + :param cluster_id: UUID of the cluster. + :param csr: Certificate Signing Request (CSR) for authenticating + client key.The CSR will be used by Magnum to generate + a signed certificate that client will use to communicate + with the cluster. + + :returns: a dict representing the signed certs. + + :raises: OpenStackCloudException on operation error. + """ + error_message = ("Error signing certs for cluster" + " {cluster_id}".format(cluster_id=cluster_id)) + with _utils.shade_exceptions(error_message): + body = {} + body['cluster_uuid'] = cluster_id + body['csr'] = csr + + certs = self._container_infra_client.post( + '/certificates', json=body) + + return self._get_and_munchify(key=None, data=certs) + @_utils.cache_on_arguments() def list_cluster_templates(self, detail=False): """List cluster templates. diff --git a/openstack/tests/unit/cloud/test_coe_clusters_certificate.py b/openstack/tests/unit/cloud/test_coe_clusters_certificate.py new file mode 100644 index 000000000..88fe7fa37 --- /dev/null +++ b/openstack/tests/unit/cloud/test_coe_clusters_certificate.py @@ -0,0 +1,61 @@ +# 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 munch + +from openstack.tests.unit import base + +coe_cluster_ca_obj = munch.Munch( + cluster_uuid="43e305ce-3a5f-412a-8a14-087834c34c8c", + pem="-----BEGIN CERTIFICATE-----\nMIIDAO\n-----END CERTIFICATE-----\n", + bay_uuid="43e305ce-3a5f-412a-8a14-087834c34c8c", + links=[] +) + +coe_cluster_signed_cert_obj = munch.Munch( + cluster_uuid='43e305ce-3a5f-412a-8a14-087834c34c8c', + pem='-----BEGIN CERTIFICATE-----\nMIIDAO\n-----END CERTIFICATE-----', + bay_uuid='43e305ce-3a5f-412a-8a14-087834c34c8c', + links=[], + csr=('-----BEGIN CERTIFICATE REQUEST-----\nMIICfz==' + '\n-----END CERTIFICATE REQUEST-----\n') +) + + +class TestCOEClusters(base.TestCase): + + def test_get_coe_cluster_certificate(self): + self.register_uris([dict( + method='GET', + uri=('https://container-infra.example.com/v1/certificates/%s' % + coe_cluster_ca_obj.cluster_uuid), + json=coe_cluster_ca_obj) + ]) + ca_cert = self.cloud.get_coe_cluster_certificate( + coe_cluster_ca_obj.cluster_uuid) + self.assertEqual( + coe_cluster_ca_obj, + ca_cert) + self.assert_calls() + + def test_sign_coe_cluster_certificate(self): + self.register_uris([dict( + method='POST', + uri='https://container-infra.example.com/v1/certificates', + json={"cluster_uuid": coe_cluster_signed_cert_obj.cluster_uuid, + "csr": coe_cluster_signed_cert_obj.csr} + )]) + self.cloud.sign_coe_cluster_certificate( + coe_cluster_signed_cert_obj.cluster_uuid, + coe_cluster_signed_cert_obj.csr) + self.assert_calls() From 97aa69617f64d40ac35ebf26eec432f8a0b7d296 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 23 Jun 2018 08:31:47 -0500 Subject: [PATCH 2122/3836] Add task manager parameter to Connection TaskManager can be overridden but Connection doesn't currently expose this in the constructor. Change-Id: Iaa17fb40fd2e56eb60d3c3ed227ad82089ce8506 --- openstack/connection.py | 15 +++++++++++++-- .../task-manager-parameter-c6606653532248f2.yaml | 6 ++++++ 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/task-manager-parameter-c6606653532248f2.yaml diff --git a/openstack/connection.py b/openstack/connection.py index ffbebfafc..86cc33c54 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -168,7 +168,7 @@ from openstack.config import cloud_region from openstack import exceptions from openstack import service_description -from openstack import task_manager +from openstack import task_manager as _task_manager __all__ = [ 'from_config', @@ -222,6 +222,7 @@ def __init__(self, cloud=None, config=None, session=None, extra_services=None, strict=False, use_direct_get=False, + task_manager=None, **kwargs): """Create a connection to a cloud. @@ -263,6 +264,15 @@ def __init__(self, cloud=None, config=None, session=None, :class:`~openstack.service_description.ServiceDescription` objects describing services that openstacksdk otherwise does not know about. + :param bool use_direct_get: + For get methods, make specific REST calls for server-side + filtering instead of making list calls and filtering client-side. + Default false. + :param task_manager: + Task Manager to handle the execution of remote REST calls. + Defaults to None which causes a direct-action Task Manager to be + used. + :type manager: :class:`~openstack.task_manager.TaskManager` :param kwargs: If a config is not provided, the rest of the parameters provided are assumed to be arguments to be passed to the CloudRegion contructor. @@ -295,7 +305,8 @@ def __init__(self, cloud=None, config=None, session=None, load_envvars=cloud is not None, **kwargs) - self.task_manager = task_manager.TaskManager(self.config.full_name) + self.task_manager = task_manager or _task_manager.TaskManager( + self.config.full_name) self._session = None self._proxies = {} diff --git a/releasenotes/notes/task-manager-parameter-c6606653532248f2.yaml b/releasenotes/notes/task-manager-parameter-c6606653532248f2.yaml new file mode 100644 index 000000000..e1e2c4b5b --- /dev/null +++ b/releasenotes/notes/task-manager-parameter-c6606653532248f2.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + A new ``task_manager`` parameter to ``Connection`` has been added for + passing a TaskManager object. This was present in shade and is used by + nodepool, but was missing from the Connection constructor. From a0e5fa33a803d7efb868f6bf125dac458f4e2191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Knecht?= Date: Thu, 12 Jul 2018 09:18:03 +0200 Subject: [PATCH 2123/3836] openstackcloud: properly handle scheduler_hints Scheduler hints need to be specified at the top level of the JSON being submitted to the API, i.e. ``` { "server": {...}, "os:scheduler_hints": {...} } ``` instead of ``` { "server": { "os:scheduler_hints": {...} } } ``` Change-Id: I19b5736b87e4b323bb957bd247b45e87bf3e8227 Task: 22955 --- openstack/cloud/openstackcloud.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 6316cf6cf..eb21ab4e4 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -6674,6 +6674,9 @@ def create_server( if not image and not boot_volume: raise TypeError( "create_server() requires either 'image' or 'boot_volume'") + + server_json = {'server': kwargs} + # TODO(mordred) Add support for description starting in 2.19 security_groups = kwargs.get('security_groups', []) if security_groups and not isinstance(kwargs['security_groups'], list): @@ -6705,7 +6708,7 @@ def create_server( " on the cloud".format(group=group)) hints['group'] = group_obj['id'] if hints: - kwargs['os:scheduler_hints'] = hints + server_json['os:scheduler_hints'] = hints kwargs.setdefault('max_count', kwargs.get('max_count', 1)) kwargs.setdefault('min_count', kwargs.get('min_count', 1)) @@ -6802,7 +6805,7 @@ def create_server( endpoint = '/os-volumes_boot' with _utils.shade_exceptions("Error in creating instance"): data = _adapter._json_response( - self.compute.post(endpoint, json={'server': kwargs})) + self.compute.post(endpoint, json=server_json)) server = self._get_and_munchify('server', data) admin_pass = server.get('adminPass') or kwargs.get('admin_pass') if not wait: From 6b694a8225defe87695b9deb7928f304fda8817e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Knecht?= Date: Fri, 13 Jul 2018 09:24:30 +0200 Subject: [PATCH 2124/3836] meta: don't throw KeyError on misconfigured floating IPs When a floating IP is associated to the network port of a server with a fixed IP that doesn't belong to the server, `_get_supplemental_addresses()` throws a `KeyError`. While it's most likely a misconfiguration of the floating IP, we should handle that situation gracefully. Change-Id: I8093ce58dd22b901521803e878536ab4fa03141f Task: 22974 --- openstack/cloud/meta.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/openstack/cloud/meta.py b/openstack/cloud/meta.py index 6b5fd65cb..1fe49793c 100644 --- a/openstack/cloud/meta.py +++ b/openstack/cloud/meta.py @@ -395,11 +395,12 @@ def _get_supplemental_addresses(cloud, server): server['status'] == 'ACTIVE'): for port in cloud.search_ports( filters=dict(device_id=server['id'])): + # This SHOULD return one and only one FIP - but doing it as a + # search/list lets the logic work regardless for fip in cloud.search_floating_ips( filters=dict(port_id=port['id'])): - # This SHOULD return one and only one FIP - but doing - # it as a search/list lets the logic work regardless - if fip['fixed_ip_address'] not in fixed_ip_mapping: + fixed_net = fixed_ip_mapping.get(fip['fixed_ip_address']) + if fixed_net is None: log = _log.setup_logging('openstack') log.debug( "The cloud returned floating ip %(fip)s attached" @@ -408,9 +409,9 @@ def _get_supplemental_addresses(cloud, server): " does not exist in the nova listing. Something" " is exceptionally broken.", dict(fip=fip['id'], server=server['id'])) - fixed_net = fixed_ip_mapping[fip['fixed_ip_address']] - server['addresses'][fixed_net].append( - _make_address_dict(fip, port)) + else: + server['addresses'][fixed_net].append( + _make_address_dict(fip, port)) except exc.OpenStackCloudException: # If something goes wrong with a cloud call, that's cool - this is # an attempt to provide additional data and should not block forward From 722d8136b2f87601a722956465745c5e5d372e62 Mon Sep 17 00:00:00 2001 From: Rui Chen Date: Fri, 13 Jul 2018 14:57:07 +0800 Subject: [PATCH 2125/3836] Run ansible tests against specific public cloud We want to run ansible tests against some public cloud, but there are always some API policy limits for regular users in public cloud, like: can not create external network. So we define some variables that can be changed for different limit scenairoes. 1. Create port in external or internal network. 2. Specified public pool name when launching server. 3. Create simple router, then add interface and external gateway, change test step order, so that do not create external network when network_external=false. 4. Create subnet with enable_dhcp true or false. 5. Make ANSIBLE_VAR_* environment variables can be passed into virtual env. Change-Id: I69473756b23a6cb525e0f9bb40d09e6ed9880782 --- .../ansible/roles/port/defaults/main.yml | 1 + .../tests/ansible/roles/port/tasks/main.yml | 2 +- .../ansible/roles/router/defaults/main.yml | 1 + .../tests/ansible/roles/router/tasks/main.yml | 55 +++++++++++++------ .../ansible/roles/server/defaults/main.yaml | 1 + .../tests/ansible/roles/server/tasks/main.yml | 2 +- .../ansible/roles/subnet/defaults/main.yml | 1 + .../tests/ansible/roles/subnet/tasks/main.yml | 2 +- tox.ini | 2 +- 9 files changed, 45 insertions(+), 22 deletions(-) diff --git a/openstack/tests/ansible/roles/port/defaults/main.yml b/openstack/tests/ansible/roles/port/defaults/main.yml index a81f6a2ea..212afe346 100644 --- a/openstack/tests/ansible/roles/port/defaults/main.yml +++ b/openstack/tests/ansible/roles/port/defaults/main.yml @@ -1,4 +1,5 @@ network_name: ansible_port_network +network_external: true subnet_name: ansible_port_subnet port_name: ansible_port secgroup_name: ansible_port_secgroup diff --git a/openstack/tests/ansible/roles/port/tasks/main.yml b/openstack/tests/ansible/roles/port/tasks/main.yml index 05ce1e20f..5011d97ba 100644 --- a/openstack/tests/ansible/roles/port/tasks/main.yml +++ b/openstack/tests/ansible/roles/port/tasks/main.yml @@ -4,7 +4,7 @@ cloud: "{{ cloud }}" state: present name: "{{ network_name }}" - external: True + external: "{{ network_external }}" - name: Create subnet os_subnet: diff --git a/openstack/tests/ansible/roles/router/defaults/main.yml b/openstack/tests/ansible/roles/router/defaults/main.yml index df5cbeb55..f7d53933a 100644 --- a/openstack/tests/ansible/roles/router/defaults/main.yml +++ b/openstack/tests/ansible/roles/router/defaults/main.yml @@ -1,2 +1,3 @@ external_network_name: ansible_external_net +network_external: true router_name: ansible_router diff --git a/openstack/tests/ansible/roles/router/tasks/main.yml b/openstack/tests/ansible/roles/router/tasks/main.yml index 9987f4c9b..083d4f066 100644 --- a/openstack/tests/ansible/roles/router/tasks/main.yml +++ b/openstack/tests/ansible/roles/router/tasks/main.yml @@ -1,11 +1,5 @@ --- -- name: Create external network - os_network: - cloud: "{{ cloud }}" - state: present - name: "{{ external_network_name }}" - external: true - +# Regular user operation - name: Create internal network os_network: cloud: "{{ cloud }}" @@ -17,33 +11,54 @@ os_subnet: cloud: "{{ cloud }}" state: present - network_name: "{{ external_network_name }}" + network_name: "{{ network_name }}" name: shade_subnet1 - cidr: 10.6.6.0/24 + cidr: 10.7.7.0/24 -- name: Create subnet2 - os_subnet: +- name: Create router + os_router: cloud: "{{ cloud }}" state: present - network_name: "{{ network_name }}" - name: shade_subnet2 - cidr: 10.7.7.0/24 + name: "{{ router_name }}" -- name: Create router +- name: Update router (add interface) os_router: cloud: "{{ cloud }}" state: present name: "{{ router_name }}" - network: "{{ external_network_name }}" + interfaces: + - shade_subnet1 + +# Admin operation +- name: Create external network + os_network: + cloud: "{{ cloud }}" + state: present + name: "{{ external_network_name }}" + external: "{{ network_external }}" + when: + - network_external + +- name: Create subnet2 + os_subnet: + cloud: "{{ cloud }}" + state: present + network_name: "{{ external_network_name }}" + name: shade_subnet2 + cidr: 10.6.6.0/24 + when: + - network_external -- name: Update router +- name: Update router (add external gateway) os_router: cloud: "{{ cloud }}" state: present name: "{{ router_name }}" network: "{{ external_network_name }}" interfaces: - - shade_subnet2 + - shade_subnet1 + when: + - network_external - name: Delete router os_router: @@ -62,6 +77,8 @@ cloud: "{{ cloud }}" state: absent name: shade_subnet2 + when: + - network_external - name: Delete internal network os_network: @@ -74,3 +91,5 @@ cloud: "{{ cloud }}" state: absent name: "{{ external_network_name }}" + when: + - network_external diff --git a/openstack/tests/ansible/roles/server/defaults/main.yaml b/openstack/tests/ansible/roles/server/defaults/main.yaml index 3db7edf8a..7d7ec01dc 100644 --- a/openstack/tests/ansible/roles/server/defaults/main.yaml +++ b/openstack/tests/ansible/roles/server/defaults/main.yaml @@ -1,3 +1,4 @@ server_network: private server_name: ansible_server flavor: m1.tiny +floating_ip_pool_name: public diff --git a/openstack/tests/ansible/roles/server/tasks/main.yml b/openstack/tests/ansible/roles/server/tasks/main.yml index f25bc2ef6..f2ff6f639 100644 --- a/openstack/tests/ansible/roles/server/tasks/main.yml +++ b/openstack/tests/ansible/roles/server/tasks/main.yml @@ -54,7 +54,7 @@ flavor: "{{ flavor }}" network: "{{ server_network }}" floating_ip_pools: - - public + - "{{ floating_ip_pool_name }}" wait: true register: server diff --git a/openstack/tests/ansible/roles/subnet/defaults/main.yml b/openstack/tests/ansible/roles/subnet/defaults/main.yml index b9df9212a..5ccc85abc 100644 --- a/openstack/tests/ansible/roles/subnet/defaults/main.yml +++ b/openstack/tests/ansible/roles/subnet/defaults/main.yml @@ -1 +1,2 @@ subnet_name: shade_subnet +enable_subnet_dhcp: false diff --git a/openstack/tests/ansible/roles/subnet/tasks/main.yml b/openstack/tests/ansible/roles/subnet/tasks/main.yml index 8d70cd2b5..a7ca490ad 100644 --- a/openstack/tests/ansible/roles/subnet/tasks/main.yml +++ b/openstack/tests/ansible/roles/subnet/tasks/main.yml @@ -11,7 +11,7 @@ network_name: "{{ network_name }}" name: "{{ subnet_name }}" state: present - enable_dhcp: false + enable_dhcp: "{{ enable_subnet_dhcp }}" dns_nameservers: - 8.8.8.7 - 8.8.8.8 diff --git a/tox.ini b/tox.ini index 3f1666631..b70fd9563 100644 --- a/tox.ini +++ b/tox.ini @@ -75,7 +75,7 @@ commands = [testenv:ansible] # Need to pass some env vars for the Ansible playbooks basepython = {env:OPENSTACKSDK_TOX_PYTHON:python2} -passenv = HOME USER +passenv = HOME USER ANSIBLE_VAR_* deps = {[testenv]deps} ansible From 5e4420763ad502e6fcd8bd6f6e31cb0c898ff1ff Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Tue, 29 May 2018 12:46:47 +0200 Subject: [PATCH 2126/3836] Add set_provision_state and wait_for_provision_state for baremetal Node This change adds set_provision_state and wait_for_provision_state to openstack.baremetal.v1.Node, as well as set_node_provision_state to the bare metal Proxy. Also adds wait_for_nodes_provision_state, which is similar to Node.wait_for_provision_state but handles several nodes at the same time, which is important for bulk operations. The cloud's node_set_provision_state was updated to use the new calls. As a nice side effect, it now supports all provision states and actions up to the Queens release, as well as does proper microversioning. Some documentation was written for the bare metal proxy. Change-Id: I22a76c3623f4dd2cca0b2103cbd8b853d5cebb71 --- doc/source/user/guides/baremetal.rst | 59 ++++++- doc/source/user/proxies/baremetal.rst | 2 + examples/baremetal/list.py | 25 +++ examples/baremetal/provisioning.py | 35 ++++ openstack/baremetal/v1/_common.py | 48 ++++++ openstack/baremetal/v1/_proxy.py | 85 +++++++++ openstack/baremetal/v1/node.py | 161 ++++++++++++++++++ openstack/cloud/openstackcloud.py | 46 +---- openstack/config/defaults.json | 1 + openstack/resource.py | 21 ++- openstack/tests/base.py | 12 ++ .../tests/unit/baremetal/v1/test_node.py | 80 ++++++++- .../tests/unit/baremetal/v1/test_proxy.py | 41 +++++ .../tests/unit/cloud/test_baremetal_node.py | 6 +- openstack/utils.py | 29 ++++ ...-set-provision-state-3472cbd81c47458f.yaml | 11 ++ 16 files changed, 611 insertions(+), 51 deletions(-) create mode 100644 examples/baremetal/list.py create mode 100644 examples/baremetal/provisioning.py create mode 100644 openstack/baremetal/v1/_common.py create mode 100644 releasenotes/notes/node-set-provision-state-3472cbd81c47458f.yaml diff --git a/doc/source/user/guides/baremetal.rst b/doc/source/user/guides/baremetal.rst index 9af1a86d2..7189511a1 100644 --- a/doc/source/user/guides/baremetal.rst +++ b/doc/source/user/guides/baremetal.rst @@ -1,9 +1,64 @@ Using OpenStack Baremetal ========================= -Before working with the Baremetal service, you'll need to create a +Before working with the Bare Metal service, you'll need to create a connection to your OpenStack cloud by following the :doc:`connect` user guide. This will provide you with the ``conn`` variable used in the examples below. -.. TODO(Qiming): Implement this guide +.. contents:: Table of Contents + :local: + +The primary resource of the Bare Metal service is the **node**. + +CRUD operations +~~~~~~~~~~~~~~~ + +List Nodes +---------- + +A **node** is a bare metal machine. + +.. literalinclude:: ../examples/baremetal/list.py + :pyobject: list_nodes + +Full example: `baremetal resource list`_ + +Provisioning operations +~~~~~~~~~~~~~~~~~~~~~~~ + +Provisioning actions are the main way to manipulate the nodes. See `Bare Metal +service states documentation`_ for details. + +Manage and inspect Node +----------------------- + +*Managing* a node in the ``enroll`` provision state validates the management +(IPMI, Redfish, etc) credentials and moves the node to the ``manageable`` +state. *Managing* a node in the ``available`` state moves it to the +``manageable`` state. In this state additional actions, such as configuring +RAID or inspecting, are available. + +*Inspecting* a node detects its properties by either talking to its BMC or by +booting a special ramdisk. + +.. literalinclude:: ../examples/baremetal/provisioning.py + :pyobject: manage_and_inspect_node + +Full example: `baremetal provisioning`_ + +Provide Node +------------ + +*Providing* a node in the ``manageable`` provision state makes it available +for deployment. + +.. literalinclude:: ../examples/baremetal/provisioning.py + :pyobject: provide_node + +Full example: `baremetal provisioning`_ + + +.. _baremetal resource list: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/baremetal/list.py +.. _baremetal provisioning: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/baremetal/provisioning.py +.. _Bare Metal service states documentation: https://docs.openstack.org/ironic/latest/contributor/states.html diff --git a/doc/source/user/proxies/baremetal.rst b/doc/source/user/proxies/baremetal.rst index f3772bb8b..578724074 100644 --- a/doc/source/user/proxies/baremetal.rst +++ b/doc/source/user/proxies/baremetal.rst @@ -22,6 +22,8 @@ Node Operations .. automethod:: openstack.baremetal.v1._proxy.Proxy.get_node .. automethod:: openstack.baremetal.v1._proxy.Proxy.find_node .. automethod:: openstack.baremetal.v1._proxy.Proxy.nodes + .. automethod:: openstack.baremetal.v1._proxy.Proxy.set_node_provision_state + .. automethod:: openstack.baremetal.v1._proxy.Proxy.wait_for_nodes_provision_state Port Operations ^^^^^^^^^^^^^^^ diff --git a/examples/baremetal/list.py b/examples/baremetal/list.py new file mode 100644 index 000000000..a5595abd1 --- /dev/null +++ b/examples/baremetal/list.py @@ -0,0 +1,25 @@ +# 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. + +""" +List resources from the Bare Metal service. +""" + + +def list_nodes(conn): + print("List Nodes:") + + for node in conn.baremetal.nodes(): + print(node) + + +# TODO(dtantsur): other resources diff --git a/examples/baremetal/provisioning.py b/examples/baremetal/provisioning.py new file mode 100644 index 000000000..36ff49e13 --- /dev/null +++ b/examples/baremetal/provisioning.py @@ -0,0 +1,35 @@ +# 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. + +""" +Operations with the provision state in the Bare Metal service. +""" + +from __future__ import print_function + + +def manage_and_inspect_node(conn, uuid): + node = conn.baremetal.find_node(uuid) + print('Before:', node.provision_state) + conn.baremetal.set_node_provision_state(node, 'manage') + conn.baremetal.wait_for_nodes_provision_state([node], 'manageable') + conn.baremetal.set_node_provision_state(node, 'inspect') + res = conn.baremetal.wait_for_nodes_provision_state([node], 'manageable') + print('After:', res[0].provision_state) + + +def provide_node(conn, uuid): + node = conn.baremetal.find_node(uuid) + print('Before:', node.provision_state) + conn.baremetal.set_node_provision_state(node, 'provide') + res = conn.baremetal.wait_for_nodes_provision_state([node], 'available') + print('After:', res[0].provision_state) diff --git a/openstack/baremetal/v1/_common.py b/openstack/baremetal/v1/_common.py new file mode 100644 index 000000000..16b5506ae --- /dev/null +++ b/openstack/baremetal/v1/_common.py @@ -0,0 +1,48 @@ +# 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. + + +RETRIABLE_STATUS_CODES = [ + # HTTP Conflict - happens if a node is locked + 409, + # HTTP Service Unavailable happens if there's no free conductor + 503 +] +"""HTTP status codes that should be retried.""" + + +PROVISIONING_VERSIONS = { + 'abort': 13, + 'adopt': 17, + 'clean': 15, + 'inspect': 6, + 'manage': 4, + 'provide': 4, + 'rescue': 38, + 'unrescue': 38, +} +"""API microversions introducing provisioning verbs.""" + + +# Based on https://docs.openstack.org/ironic/latest/contributor/states.html +EXPECTED_STATES = { + 'active': 'active', + 'adopt': 'available', + 'clean': 'manageable', + 'deleted': 'available', + 'inspect': 'manageable', + 'manage': 'manageable', + 'provide': 'available', + 'rebuild': 'active', + 'rescue': 'rescue', +} +"""Mapping of provisioning actions to expected stable states.""" diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index ba28099c0..a838faa62 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import _log from openstack.baremetal.v1 import chassis as _chassis from openstack.baremetal.v1 import driver as _driver from openstack.baremetal.v1 import node as _node @@ -19,6 +20,9 @@ from openstack import utils +_logger = _log.setup_logging('openstack') + + class Proxy(proxy.Proxy): def chassis(self, details=False, **query): @@ -240,6 +244,87 @@ def update_node(self, node, **attrs): """ return self._update(_node.Node, node, **attrs) + def set_node_provision_state(self, node, target, config_drive=None, + clean_steps=None, rescue_password=None, + wait=False, timeout=None): + """Run an action modifying node's provision state. + + This call is asynchronous, it will return success as soon as the Bare + Metal service acknowledges the request. + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :param target: Provisioning action, e.g. ``active``, ``provide``. + See the Bare Metal service documentation for available actions. + :param config_drive: Config drive to pass to the node, only valid + for ``active` and ``rebuild`` targets. + :param clean_steps: Clean steps to execute, only valid for ``clean`` + target. + :param rescue_password: Password for the rescue operation, only valid + for ``rescue`` target. + :param wait: Whether to wait for the node to get into the expected + state. The expected state is determined from a combination of + the current provision state and ``target``. + :param timeout: If ``wait`` is set to ``True``, specifies how much (in + seconds) to wait for the expected state to be reached. The value of + ``None`` (the default) means no client-side timeout. + + :returns: The updated :class:`~openstack.baremetal.v1.node.Node` + :raises: ValueError if ``config_drive``, ``clean_steps`` or + ``rescue_password`` are provided with an invalid ``target``. + """ + res = self._get_resource(_node.Node, node) + return res.set_provision_state(self, target, config_drive=config_drive, + clean_steps=clean_steps, + rescue_password=rescue_password, + wait=wait, timeout=timeout) + + def wait_for_nodes_provision_state(self, nodes, expected_state, + timeout=None, + abort_on_failed_state=True): + """Wait for the nodes to reach the expected state. + + :param nodes: List of nodes - name, ID or + :class:`~openstack.baremetal.v1.node.Node` instance. + :param expected_state: The expected provisioning state to reach. + :param timeout: If ``wait`` is set to ``True``, specifies how much (in + seconds) to wait for the expected state to be reached. The value of + ``None`` (the default) means no client-side timeout. + :param abort_on_failed_state: If ``True`` (the default), abort waiting + if any node reaches a failure state which does not match the + expected one. Note that the failure state for ``enroll`` -> + ``manageable`` transition is ``enroll`` again. + + :return: The list of :class:`~openstack.baremetal.v1.node.Node` + instances that reached the requested state. + """ + log_nodes = ', '.join(n.id if isinstance(n, _node.Node) else n + for n in nodes) + + finished = [] + remaining = nodes + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for nodes %(nodes)s to reach " + "target state '%(state)s'" % {'nodes': log_nodes, + 'state': expected_state}): + nodes = [self.get_node(n) for n in remaining] + remaining = [] + for n in nodes: + if n._check_state_reached(self, expected_state, + abort_on_failed_state): + finished.append(n) + else: + remaining.append(n) + + if not remaining: + return finished + + _logger.debug('Still waiting for nodes %(nodes)s to reach state ' + '"%(target)s"', + {'nodes': ', '.join(n.id for n in remaining), + 'target': expected_state}) + def delete_node(self, node, ignore_missing=True): """Delete a node. diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 3ba698cff..ac5b2c9fa 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -10,8 +10,15 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import _log from openstack.baremetal import baremetal_service +from openstack.baremetal.v1 import _common +from openstack import exceptions from openstack import resource +from openstack import utils + + +_logger = _log.setup_logging('openstack') class Node(resource.Resource): @@ -113,6 +120,160 @@ class Node(resource.Resource): #: Timestamp at which the node was last updated. updated_at = resource.Body("updated_at") + def set_provision_state(self, session, target, config_drive=None, + clean_steps=None, rescue_password=None, + wait=False, timeout=True): + """Run an action modifying this node's provision state. + + This call is asynchronous, it will return success as soon as the Bare + Metal service acknowledges the request. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param target: Provisioning action, e.g. ``active``, ``provide``. + See the Bare Metal service documentation for available actions. + :param config_drive: Config drive to pass to the node, only valid + for ``active` and ``rebuild`` targets. + :param clean_steps: Clean steps to execute, only valid for ``clean`` + target. + :param rescue_password: Password for the rescue operation, only valid + for ``rescue`` target. + :param wait: Whether to wait for the target state to be reached. + :param timeout: Timeout (in seconds) to wait for the target state to be + reached. If ``None``, wait without timeout. + + :return: This :class:`Node` instance. + :raises: ValueError if ``config_drive``, ``clean_steps`` or + ``rescue_password`` are provided with an invalid ``target``. + """ + session = self._get_session(session) + + if target in _common.PROVISIONING_VERSIONS: + version = '1.%d' % _common.PROVISIONING_VERSIONS[target] + else: + if config_drive and target == 'rebuild': + version = '1.35' + else: + version = None + version = utils.pick_microversion(session, version) + + body = {'target': target} + if config_drive: + if target not in ('active', 'rebuild'): + raise ValueError('Config drive can only be provided with ' + '"active" and "rebuild" targets') + # Not a typo - ironic accepts "configdrive" (without underscore) + body['configdrive'] = config_drive + + if clean_steps is not None: + if target != 'clean': + raise ValueError('Clean steps can only be provided with ' + '"clean" target') + body['clean_steps'] = clean_steps + + if rescue_password is not None: + if target != 'rescue': + raise ValueError('Rescue password can only be provided with ' + '"rescue" target') + body['rescue_password'] = rescue_password + + if wait: + try: + expected_state = _common.EXPECTED_STATES[target] + except KeyError: + raise ValueError('For target %s the expected state is not ' + 'known, cannot wait for it' % target) + + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'states', 'provision') + response = session.put( + request.url, json=body, + headers=request.headers, microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + msg = ("Failed to set provision state for bare metal node {node} " + "to {target}".format(node=self.id, target=target)) + exceptions.raise_from_response(response, error_message=msg) + + if wait: + return self.wait_for_provision_state(session, + expected_state, + timeout=timeout) + else: + return self.get(session) + + def wait_for_provision_state(self, session, expected_state, timeout=None, + abort_on_failed_state=True): + """Wait for the node to reach the expected state. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param expected_state: The expected provisioning state to reach. + :param timeout: If ``wait`` is set to ``True``, specifies how much (in + seconds) to wait for the expected state to be reached. The value of + ``None`` (the default) means no client-side timeout. + :param abort_on_failed_state: If ``True`` (the default), abort waiting + if the node reaches a failure state which does not match the + expected one. Note that the failure state for ``enroll`` -> + ``manageable`` transition is ``enroll`` again. + + :return: This :class:`Node` instance. + """ + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for node %(node)s to reach " + "target state '%(state)s'" % {'node': self.id, + 'state': expected_state}): + self.get(session) + if self._check_state_reached(session, expected_state, + abort_on_failed_state): + return self + + _logger.debug('Still waiting for node %(node)s to reach state ' + '"%(target)s", the current state is "%(state)s"', + {'node': self.id, 'target': expected_state, + 'state': self.provision_state}) + + def _check_state_reached(self, session, expected_state, + abort_on_failed_state=True): + """Wait for the node to reach the expected state. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param expected_state: The expected provisioning state to reach. + :param abort_on_failed_state: If ``True`` (the default), abort waiting + if the node reaches a failure state which does not match the + expected one. Note that the failure state for ``enroll`` -> + ``manageable`` transition is ``enroll`` again. + + :return: ``True`` if the target state is reached + :raises: SDKException if ``abort_on_failed_state`` is ``True`` and + a failure state is reached. + """ + # NOTE(dtantsur): microversion 1.2 changed None to available + if (self.provision_state == expected_state or + (expected_state == 'available' and + self.provision_state is None)): + return True + elif not abort_on_failed_state: + return False + + if self.provision_state.endswith(' failed'): + raise exceptions.SDKException( + "Node %(node)s reached failure state \"%(state)s\"; " + "the last error is %(error)s" % + {'node': self.id, 'state': self.provision_state, + 'error': self.last_error}) + # Special case: a failure state for "manage" transition can be + # "enroll" + elif (expected_state == 'manageable' and + self.provision_state == 'enroll' and self.last_error): + raise exceptions.SDKException( + "Node %(node)s could not reach state manageable: " + "failed to verify management credentials; " + "the last error is %(error)s" % + {'node': self.id, 'error': self.last_error}) + class NodeDetail(Node): diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index eb21ab4e4..554906dfe 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -9607,7 +9607,7 @@ def node_set_provision_state(self, config drive to be utilized. :param string name_or_id: The Name or UUID value representing the - baremetal node. + baremetal node. :param string state: The desired provision state for the baremetal node. :param string configdrive: An optional URL or file or path @@ -9629,46 +9629,10 @@ def node_set_provision_state(self, :returns: ``munch.Munch`` representing the current state of the machine upon exit of the method. """ - # NOTE(TheJulia): Default microversion for this call is 1.6. - # Setting locally until we have determined our master plan regarding - # microversion handling. - version = "1.6" - msg = ("Baremetal machine node failed change provision state to " - "{state}".format(state=state)) - - url = '/nodes/{node_id}/states/provision'.format( - node_id=name_or_id) - payload = {'target': state} - if configdrive: - payload['configdrive'] = configdrive - - machine = _utils._call_client_and_retry(self._baremetal_client.put, - url, - retry_on=[409, 503], - json=payload, - error_message=msg, - microversion=version) - if wait: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for node transition to " - "target state of '%s'" % state): - machine = self.get_machine(name_or_id) - if 'failed' in machine['provision_state']: - raise exc.OpenStackCloudException( - "Machine encountered a failure.") - # NOTE(TheJulia): This performs matching if the requested - # end state matches the state the node has reached. - if state in machine['provision_state']: - break - # NOTE(TheJulia): This performs matching for cases where - # the reqeusted state action ends in available state. - if ("available" in machine['provision_state'] and - state in ["provide", "deleted"]): - break - else: - machine = self.get_machine(name_or_id) - return machine + node = self.baremetal.set_node_provision_state( + name_or_id, target=state, config_drive=configdrive, + wait=wait, timeout=timeout) + return node._to_munch() def set_machine_maintenance_state( self, diff --git a/openstack/config/defaults.json b/openstack/config/defaults.json index c17cc041c..17d69e264 100644 --- a/openstack/config/defaults.json +++ b/openstack/config/defaults.json @@ -2,6 +2,7 @@ "application_catalog_api_version": "1", "auth_type": "password", "baremetal_api_version": "1", + "baremetal_status_code_retries": 5, "block_storage_api_version": "2", "clustering_api_version": "1", "container_api_version": "1", diff --git a/openstack/resource.py b/openstack/resource.py index 34dfc2e20..9742d212d 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -35,6 +35,7 @@ class that represent a remote resource. The attributes that import itertools from keystoneauth1 import adapter +import munch from requests import structures from openstack import _log @@ -573,7 +574,8 @@ def _from_munch(cls, obj, synchronized=True): """ return cls(_synchronized=synchronized, **obj) - def to_dict(self, body=True, headers=True, ignore_none=False): + def to_dict(self, body=True, headers=True, ignore_none=False, + original_names=False): """Return a dictionary of this resource's contents :param bool body: Include the :class:`~openstack.resource.Body` @@ -583,6 +585,8 @@ def to_dict(self, body=True, headers=True, ignore_none=False): :param bool ignore_none: When True, exclude key/value pairs where the value is None. This will exclude attributes that the server hasn't returned. + :param bool original_names: When True, use attribute names as they + were received from the server. :return: A dictionary of key/value pairs where keys are named as they exist as attributes of this class. @@ -608,12 +612,16 @@ def to_dict(self, body=True, headers=True, ignore_none=False): # Since we're looking at class definitions we need to include # subclasses, so check the whole MRO. for klass in self.__class__.__mro__: - for key, value in klass.__dict__.items(): - if isinstance(value, components): + for attr, component in klass.__dict__.items(): + if isinstance(component, components): + if original_names: + key = component.name + else: + key = attr # Make sure base classes don't end up overwriting # mappings we've found previously in subclasses. if key not in mapping: - value = getattr(self, key, None) + value = getattr(self, attr, None) if ignore_none and value is None: continue if isinstance(value, Resource): @@ -629,6 +637,11 @@ def to_dict(self, body=True, headers=True, ignore_none=False): return mapping + def _to_munch(self): + """Convert this resource into a Munch compatible with shade.""" + return munch.Munch(self.to_dict(body=True, headers=False, + original_names=True)) + def _prepare_request(self, requires_id=None, prepend_key=False): """Prepare a request to be sent to the server diff --git a/openstack/tests/base.py b/openstack/tests/base.py index 02b8e8965..128ec9c8e 100644 --- a/openstack/tests/base.py +++ b/openstack/tests/base.py @@ -107,3 +107,15 @@ def add_content(unused): self.addDetail(name, testtools.content.text_content( pprint.pformat(text))) self.addOnException(add_content) + + def assertSubdict(self, part, whole): + missing_keys = set(part) - set(whole) + if missing_keys: + self.fail("Keys %s are in %s but not in %s" % + (missing_keys, part, whole)) + wrong_values = [(key, part[key], whole[key]) + for key in part if part[key] != whole[key]] + if wrong_values: + self.fail("Mismatched values: %s" % + ", ".join("for %s got %s and %s" % tpl + for tpl in wrong_values)) diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 396a9d9ad..5866ea256 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -10,9 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base +from keystoneauth1 import adapter +import mock from openstack.baremetal.v1 import node +from openstack import exceptions +from openstack.tests.unit import base # NOTE: Sample data from api-ref doc FAKE = { @@ -196,3 +199,78 @@ def test_instantiate(self): self.assertEqual(FAKE['target_power_state'], sot.target_power_state) self.assertEqual(FAKE['target_raid_config'], sot.target_raid_config) self.assertEqual(FAKE['updated_at'], sot.updated_at) + + +@mock.patch('time.sleep', lambda _t: None) +@mock.patch.object(node.Node, 'get', autospec=True) +class TestNodeWaitForProvisionState(base.TestCase): + def setUp(self): + super(TestNodeWaitForProvisionState, self).setUp() + self.node = node.Node(**FAKE) + self.session = mock.Mock() + + def test_success(self, mock_get): + def _get_side_effect(_self, session): + self.node.provision_state = 'manageable' + self.assertIs(session, self.session) + + mock_get.side_effect = _get_side_effect + + node = self.node.wait_for_provision_state(self.session, 'manageable') + self.assertIs(node, self.node) + + def test_failure(self, mock_get): + def _get_side_effect(_self, session): + self.node.provision_state = 'deploy failed' + self.assertIs(session, self.session) + + mock_get.side_effect = _get_side_effect + + self.assertRaisesRegex(exceptions.SDKException, + 'failure state "deploy failed"', + self.node.wait_for_provision_state, + self.session, 'manageable') + + def test_enroll_as_failure(self, mock_get): + def _get_side_effect(_self, session): + self.node.provision_state = 'enroll' + self.node.last_error = 'power failure' + self.assertIs(session, self.session) + + mock_get.side_effect = _get_side_effect + + self.assertRaisesRegex(exceptions.SDKException, + 'failed to verify management credentials', + self.node.wait_for_provision_state, + self.session, 'manageable') + + def test_timeout(self, mock_get): + self.assertRaises(exceptions.ResourceTimeout, + self.node.wait_for_provision_state, + self.session, 'manageable', timeout=0.001) + + def test_not_abort_on_failed_state(self, mock_get): + def _get_side_effect(_self, session): + self.node.provision_state = 'deploy failed' + self.assertIs(session, self.session) + + mock_get.side_effect = _get_side_effect + + self.assertRaises(exceptions.ResourceTimeout, + self.node.wait_for_provision_state, + self.session, 'manageable', timeout=0.001, + abort_on_failed_state=False) + + +@mock.patch.object(node.Node, 'get', lambda self, session: self) +@mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) +class TestNodeSetProvisionState(base.TestCase): + + def setUp(self): + super(TestNodeSetProvisionState, self).setUp() + self.node = node.Node(**FAKE) + self.session = mock.Mock(spec=adapter.Adapter, + default_microversion=None) + + def test_no_arguments(self): + self.node.set_provision_state(self.session, 'manage') diff --git a/openstack/tests/unit/baremetal/v1/test_proxy.py b/openstack/tests/unit/baremetal/v1/test_proxy.py index 4d874e89c..5193078c5 100644 --- a/openstack/tests/unit/baremetal/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal/v1/test_proxy.py @@ -11,6 +11,7 @@ # under the License. import deprecation +import mock from openstack.baremetal.v1 import _proxy from openstack.baremetal.v1 import chassis @@ -18,6 +19,8 @@ from openstack.baremetal.v1 import node from openstack.baremetal.v1 import port from openstack.baremetal.v1 import port_group +from openstack import exceptions +from openstack.tests.unit import base from openstack.tests.unit import test_proxy_base @@ -162,3 +165,41 @@ def test_delete_portgroup(self): def test_delete_portgroup_ignore(self): self.verify_delete(self.proxy.delete_portgroup, port_group.PortGroup, True) + + +@mock.patch('time.sleep', lambda _sec: None) +@mock.patch.object(_proxy.Proxy, 'get_node', autospec=True) +class TestWaitForNodesProvisionState(base.TestCase): + + def setUp(self): + super(TestWaitForNodesProvisionState, self).setUp() + self.session = mock.Mock() + self.proxy = _proxy.Proxy(self.session) + + def test_success(self, mock_get): + # two attempts, one node succeeds after the 1st + nodes = [mock.Mock(spec=node.Node, id=str(i)) + for i in range(3)] + for i, n in enumerate(nodes): + # 1st attempt on 1st node, 2nd attempt on 2nd node + n._check_state_reached.return_value = not (i % 2) + mock_get.side_effect = nodes + + result = self.proxy.wait_for_nodes_provision_state( + ['abcd', node.Node(id='1234')], 'fake state') + self.assertEqual([nodes[0], nodes[2]], result) + + for n in nodes: + n._check_state_reached.assert_called_once_with( + self.proxy, 'fake state', True) + + def test_timeout(self, mock_get): + mock_get.return_value._check_state_reached.return_value = False + mock_get.return_value.id = '1234' + + self.assertRaises(exceptions.ResourceTimeout, + self.proxy.wait_for_nodes_provision_state, + ['abcd', node.Node(id='1234')], 'fake state', + timeout=0.001) + mock_get.return_value._check_state_reached.assert_called_with( + self.proxy, 'fake state', True) diff --git a/openstack/tests/unit/cloud/test_baremetal_node.py b/openstack/tests/unit/cloud/test_baremetal_node.py index a433f3385..83ac06b77 100644 --- a/openstack/tests/unit/cloud/test_baremetal_node.py +++ b/openstack/tests/unit/cloud/test_baremetal_node.py @@ -764,7 +764,7 @@ def test_node_set_provision_state_wait_timeout(self): 'active', wait=True) - self.assertEqual(active_node, return_value) + self.assertSubdict(active_node, return_value) self.assert_calls() def test_node_set_provision_state_wait_timeout_fails(self): @@ -817,7 +817,7 @@ def test_node_set_provision_state_wait_success(self): 'active', wait=True) - self.assertEqual(self.fake_baremetal_node, return_value) + self.assertSubdict(self.fake_baremetal_node, return_value) self.assert_calls() def test_node_set_provision_state_wait_failure_cases(self): @@ -875,7 +875,7 @@ def test_node_set_provision_state_wait_provide(self): 'provide', wait=True) - self.assertEqual(available_node, return_value) + self.assertSubdict(available_node, return_value) self.assert_calls() def test_wait_for_baremetal_node_lock_locked(self): diff --git a/openstack/utils.py b/openstack/utils.py index 8af0b07bf..4f67f3603 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -154,3 +154,32 @@ def supports_microversion(adapter, microversion): microversion)): return True return False + + +def pick_microversion(session, required): + """Get a new microversion if it is higher than session's default. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param required: Version that is required for an action. + :type required: String or tuple or None. + :return: ``required`` as a string if the ``session``'s default is too low, + the ``session``'s default otherwise. Returns ``None`` of both + are ``None``. + :raises: TypeError if ``required`` is invalid. + """ + if required is not None: + required = discover.normalize_version_number(required) + + if session.default_microversion is not None: + default = discover.normalize_version_number( + session.default_microversion) + + if required is None: + required = default + else: + required = (default if discover.version_match(required, default) + else required) + + if required is not None: + return discover.version_to_string(required) diff --git a/releasenotes/notes/node-set-provision-state-3472cbd81c47458f.yaml b/releasenotes/notes/node-set-provision-state-3472cbd81c47458f.yaml new file mode 100644 index 000000000..f75f6dfec --- /dev/null +++ b/releasenotes/notes/node-set-provision-state-3472cbd81c47458f.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + Adds ``set_provision_state`` and ``wait_for_provision_state`` to + ``openstack.baremetal.v1.Node``. + - | + Adds ``node_set_provision_state`` and ``wait_for_nodes_provision_state`` + to the baremetal Proxy. + - | + The ``node_set_provision_state`` call now supports provision states + up to the Queens release. From f2aeaead02379b376ad3af3bd430914296a0526a Mon Sep 17 00:00:00 2001 From: Josephine Seifert Date: Wed, 4 Jul 2018 09:56:11 +0200 Subject: [PATCH 2127/3836] Implement signature generation functionality Openstacksdk is extended with a signature generation functionality. This adds cryptography to the requirements and lower constraints. Change-Id: Idc15b9a12d408bd4b2e096da8402c374be56f9fa Story: 2002128 Co-Authored-By: Markus Hentsch --- lower-constraints.txt | 1 + openstack/image/image_signer.py | 69 ++++++++++++++++++++++++ openstack/image/iterable_chunked_file.py | 39 ++++++++++++++ requirements.txt | 1 + 4 files changed, 110 insertions(+) create mode 100644 openstack/image/image_signer.py create mode 100644 openstack/image/iterable_chunked_file.py diff --git a/lower-constraints.txt b/lower-constraints.txt index 2043b090f..d3253c46f 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -1,5 +1,6 @@ appdirs==1.3.0 coverage==4.0 +cryptography==2.1 decorator==3.4.0 deprecation==1.0 dogpile.cache==0.6.2 diff --git a/openstack/image/image_signer.py b/openstack/image/image_signer.py new file mode 100644 index 000000000..19c6ec965 --- /dev/null +++ b/openstack/image/image_signer.py @@ -0,0 +1,69 @@ +# 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 cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import padding +from cryptography.hazmat.primitives.asymmetric import utils +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization + +from openstack.image.iterable_chunked_file import IterableChunkedFile + +HASH_METHODS = { + 'SHA-224': hashes.SHA224(), + 'SHA-256': hashes.SHA256(), + 'SHA-384': hashes.SHA384(), + 'SHA-512': hashes.SHA512(), +} + + +class ImageSigner(object): + """Image file signature generator. + + Generates signatures for files using a specified private key file. + """ + + def __init__(self, hash_method='SHA-256', padding_method='RSA-PSS'): + padding_types = { + 'RSA-PSS': padding.PSS( + mgf=padding.MGF1(HASH_METHODS[hash_method]), + salt_length=padding.PSS.MAX_LENGTH + ) + } + # informational attributes + self.hash_method = hash_method + self.padding_method = padding_method + # runtime objects + self.private_key = None + self.hash = HASH_METHODS[hash_method] + self.hasher = hashes.Hash(self.hash, default_backend()) + self.padding = padding_types[padding_method] + + def load_private_key(self, file_path, password=None): + with open(file_path, 'rb') as key_file: + self.private_key = serialization.load_pem_private_key( + key_file.read(), password=password, backend=default_backend() + ) + + def generate_signature(self, file_obj): + file_obj.seek(0) + chunked_file = IterableChunkedFile(file_obj) + for chunk in chunked_file: + self.hasher.update(chunk) + file_obj.seek(0) + digest = self.hasher.finalize() + signature = self.private_key.sign( + digest, self.padding, utils.Prehashed(self.hash) + ) + return signature diff --git a/openstack/image/iterable_chunked_file.py b/openstack/image/iterable_chunked_file.py new file mode 100644 index 000000000..d887ace5b --- /dev/null +++ b/openstack/image/iterable_chunked_file.py @@ -0,0 +1,39 @@ +# 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. +# + + +class IterableChunkedFile(object): + """File object chunk iterator using yield. + + Represents a local file as an iterable object by splitting the file + into chunks. Avoids the file from being completely loaded into memory. + """ + + def __init__(self, file_object, chunk_size=1024 * 1024 * 128, close=False): + self.close_after_read = close + self.file_object = file_object + self.chunk_size = chunk_size + + def __iter__(self): + try: + while True: + data = self.file_object.read(self.chunk_size) + if not data: + break + yield data + finally: + if self.close_after_read: + self.file_object.close() + + def __len__(self): + return len(self.file_object) diff --git a/requirements.txt b/requirements.txt index 5ffab64c5..8eb788aaf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,3 +20,4 @@ iso8601>=0.1.11 # MIT netifaces>=0.10.4 # MIT dogpile.cache>=0.6.2 # BSD +cryptography>=2.1 # BSD/Apache-2.0 From 2ad74a54aa9a2d0d4b13943aa5ecc5164cc38c50 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 18 Jul 2018 15:51:45 -0500 Subject: [PATCH 2128/3836] Add /v3 to the auth_url for vexxhost v3password skips discovery, so v3password with unversioned endpoint produces incorrect results. We could go to password, but then people won't get clear errors if they leave out domain info. Change-Id: Ia070ebafb608aec6a85a38feb746113f72c58eec --- openstack/config/vendors/vexxhost.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/config/vendors/vexxhost.json b/openstack/config/vendors/vexxhost.json index 488d961be..0598d10dc 100644 --- a/openstack/config/vendors/vexxhost.json +++ b/openstack/config/vendors/vexxhost.json @@ -3,7 +3,7 @@ "profile": { "auth_type": "v3password", "auth": { - "auth_url": "https://auth.vexxhost.net" + "auth_url": "https://auth.vexxhost.net/v3" }, "regions": [ "ca-ymq-1" From ad9f8a03951d5cc8479932e288db4161511f3a8e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 18 Jul 2018 17:45:34 -0500 Subject: [PATCH 2129/3836] Add missing swift docstrings These methods don't show up in the docs without doc strings. Change-Id: I5640eeacd32b1444301c3f900844386300428572 --- openstack/cloud/openstackcloud.py | 68 ++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index eb21ab4e4..53abde1fc 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -7202,6 +7202,14 @@ def list_containers(self, full_listing=True): return self._object_store_client.get('/', params=dict(format='json')) def get_container(self, name, skip_cache=False): + """Get metadata about a container. + + :param str name: + Name of the container to get metadata for. + :param bool skip_cache: + Ignore the cache of container metadata for this container.o + Defaults to ``False``. + """ if skip_cache or name not in self._container_cache: try: container = self._object_store_client.head(name) @@ -7213,6 +7221,13 @@ def get_container(self, name, skip_cache=False): return self._container_cache[name] def create_container(self, name, public=False): + """Create an object-store container. + + :param str name: + Name of the container to create. + :param bool public: + Whether to set this container to be public. Defaults to ``False``. + """ container = self.get_container(name) if container: return container @@ -7222,6 +7237,10 @@ def create_container(self, name, public=False): return self.get_container(name, skip_cache=True) def delete_container(self, name): + """Delete an object-store container. + + :param str name: Name of the container to delete. + """ try: self._object_store_client.delete(name) return True @@ -7237,9 +7256,32 @@ def delete_container(self, name): raise def update_container(self, name, headers): + """Update the metadata in a container. + + :param str name: + Name of the container to create. + :param dict headers: + Key/Value headers to set on the container. + """ + """Update the metadata in a container. + + :param str name: + Name of the container to update. + :param dict headers: + Key/Value headers to set on the container. + """ self._object_store_client.post(name, headers=headers) def set_container_access(self, name, access): + """Set the access control list on a container. + + :param str name: + Name of the container. + :param str access: + ACL string to set on the container. Can also be ``public`` + or ``private`` which will be translated into appropriate ACL + strings. + """ if access not in OBJECT_CONTAINER_ACLS: raise exc.OpenStackCloudException( "Invalid container access specified: %s. Must be one of %s" @@ -7248,6 +7290,10 @@ def set_container_access(self, name, access): self.update_container(name, header) def get_container_access(self, name): + """Get the control list from a container. + + :param str name: Name of the container. + """ container = self.get_container(name, skip_cache=True) if not container: raise exc.OpenStackCloudException("Container not found: %s" % name) @@ -7286,6 +7332,11 @@ def _get_file_hashes(self, filename): @_utils.cache_on_arguments() def get_object_capabilities(self): + """Get infomation about the object-storage service + + The object-storage service publishes a set of capabilities that + include metadata about maximum values and thresholds. + """ # The endpoint in the catalog has version and project-id in it # To get capabilities, we have to disassemble and reassemble the URL # This logic is taken from swiftclient @@ -7327,7 +7378,18 @@ def get_object_segment_size(self, segment_size): def is_object_stale( self, container, name, filename, file_md5=None, file_sha256=None): - + """Check to see if an object matches the hashes of a file. + + :param container: Name of the container. + :param name: Name of the object. + :param filename: Path to the file. + :param file_md5: + Pre-calculated md5 of the file contents. Defaults to None which + means calculate locally. + :param file_sha256: + Pre-calculated sha256 of the file contents. Defaults to None which + means calculate locally. + """ metadata = self.get_object_metadata(container, name) if not metadata: self.log.debug( @@ -7360,7 +7422,9 @@ def create_object( md5=None, sha256=None, segment_size=None, use_slo=True, metadata=None, **headers): - """Create a file object + """Create a file object. + + Automatically uses large-object segments if needed. :param container: The name of the container to store the file in. This container will be created if it does not exist already. From 58ed1278d89c79e3faaa97c9b2bdebe977901502 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 19 Jul 2018 08:50:22 -0500 Subject: [PATCH 2130/3836] Add missing release note about vexxhost auth_url The auth_url change deserves a release note, and the why of it is actually interesting information for people. Change-Id: I0ec28a4f25593ce1ebcb7254780b41685944bd3b --- .../notes/auth-url-vexxhost-8d63cd17bde21320.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 releasenotes/notes/auth-url-vexxhost-8d63cd17bde21320.yaml diff --git a/releasenotes/notes/auth-url-vexxhost-8d63cd17bde21320.yaml b/releasenotes/notes/auth-url-vexxhost-8d63cd17bde21320.yaml new file mode 100644 index 000000000..f32e1ecca --- /dev/null +++ b/releasenotes/notes/auth-url-vexxhost-8d63cd17bde21320.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + The ``v3password`` ``auth_type`` implies that the ``auth_url`` given + is a versioned endpoint and so discovery is skipped for auth. Previously + the ``auth_type`` for Vexxhost had been set to ``v3password`` due to v2 + being no longer available to give better errors to users. The ``auth_url`` + was unfortunately left unversioned, so authentication ceased working. The + ``auth_url`` has been changed to the versioned endpoint. From 645d148ddb82bc9a6d3f34149b51ec7b2cb605d9 Mon Sep 17 00:00:00 2001 From: Hongbin Lu Date: Thu, 19 Jul 2018 16:42:55 +0000 Subject: [PATCH 2131/3836] Use valid filters to list floating IPs in neutron OpenStackSDK used 'attached' as a filter for listing floating IPs, but this is not a supported parameter in neutron. In before, the test passed because neutron server ignored this parameter and returned all the floating IPs. However, neutron is planing to employ a strict validation on the list endpoint [1]. As a result, neutron will reject the request with 400 response if the request contains unknown filter. This patch fixes the usage of neutron API. It passes filters to neutron API only if they are known supported filters. The list of supported filters for listing floating IPs can be found in the neutron API reference [2]. [1] https://review.openstack.org/#/c/574907/ [2] https://developer.openstack.org/api-ref/network/v2/#id130 Depends-On: I124adfc5e9cdd5bc20aacf23f9dcc10c55b0870b Change-Id: I47eac210bd0364166647345ff14ea0c49ef4e9e3 --- openstack/cloud/openstackcloud.py | 10 +++++++++- openstack/tests/unit/cloud/test_floating_ip_neutron.py | 3 +-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 7b7194342..1c3c269d3 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -1607,7 +1607,15 @@ def search_floating_ips(self, id=None, filters=None): # `filters` could be a jmespath expression which Neutron server doesn't # understand, obviously. if self._use_neutron_floating() and isinstance(filters, dict): - kwargs = {'filters': filters} + filter_keys = ['router_id', 'status', 'tenant_id', 'project_id', + 'revision_number', 'description', + 'floating_network_id', 'fixed_ip_address', + 'floating_ip_address', 'port_id', 'sort_dir', + 'sort_key', 'tags', 'tags-any', 'not-tags', + 'not-tags-any', 'fields'] + neutron_filters = {k: v for k, v in filters.items() + if k in filter_keys} + kwargs = {'filters': neutron_filters} else: kwargs = {} floating_ips = self.list_floating_ips(**kwargs) diff --git a/openstack/tests/unit/cloud/test_floating_ip_neutron.py b/openstack/tests/unit/cloud/test_floating_ip_neutron.py index 892ce3ae1..8c8b7a66b 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_neutron.py +++ b/openstack/tests/unit/cloud/test_floating_ip_neutron.py @@ -190,8 +190,7 @@ def test_list_floating_ips_with_filters(self): def test_search_floating_ips(self): self.register_uris([ dict(method='GET', - uri=('https://network.example.com/v2.0/floatingips.json' - '?attached=False'), + uri=('https://network.example.com/v2.0/floatingips.json'), json=self.mock_floating_ip_list_rep)]) floating_ips = self.cloud.search_floating_ips( From 5f46fe88424d0fc37705fb2a59d67a93a2993279 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 18 Jul 2018 17:55:50 -0500 Subject: [PATCH 2132/3836] Add flag for disabling object checksum generation Checksums are calculated and added to the object's metadata as a way to prevent double-uploading identical copies of large data. For some use cases, such as uploading log files, there is no risk of that and there is no point to paying the calculation cost. Change-Id: I7b3c94b72e99f9abd3c961bd811da6fd563144bb --- openstack/cloud/openstackcloud.py | 12 +++- openstack/tests/unit/cloud/test_object.py | 55 +++++++++++++++++++ ...-checksum-generation-ea1c1e47d2290054.yaml | 4 ++ 3 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/object-checksum-generation-ea1c1e47d2290054.yaml diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 53abde1fc..5d8773803 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -7421,6 +7421,7 @@ def create_object( self, container, name, filename=None, md5=None, sha256=None, segment_size=None, use_slo=True, metadata=None, + generate_checksums=True, **headers): """Create a file object. @@ -7445,6 +7446,9 @@ def create_object( Object, use a static rather than dynamic object. Static Objects will delete segment objects when the manifest object is deleted. (optional, defaults to True) + :param generate_checksums: Whether to generate checksums on the client + side that get added to headers for later prevention of double + uploads of identical data. (optional, defaults to True) :param metadata: This dict will get changed into headers that set metadata of the object @@ -7463,10 +7467,12 @@ def create_object( segment_size = self.get_object_segment_size(segment_size) file_size = os.path.getsize(filename) - if not (md5 or sha256): + if generate_checksums and (md5 is None or sha256 is None): (md5, sha256) = self._get_file_hashes(filename) - headers[OBJECT_MD5_KEY] = md5 or '' - headers[OBJECT_SHA256_KEY] = sha256 or '' + if md5: + headers[OBJECT_MD5_KEY] = md5 or '' + if sha256: + headers[OBJECT_SHA256_KEY] = sha256 or '' for (k, v) in metadata.items(): headers['x-object-meta-' + k] = v diff --git a/openstack/tests/unit/cloud/test_object.py b/openstack/tests/unit/cloud/test_object.py index 7bb8506f9..b13d81446 100644 --- a/openstack/tests/unit/cloud/test_object.py +++ b/openstack/tests/unit/cloud/test_object.py @@ -895,3 +895,58 @@ def test_object_segment_retries(self): 'etag': 'etag3', }, ], self.adapter.request_history[-1].json()) + + def test_create_object_skip_checksum(self): + + self.register_uris([ + dict(method='GET', + uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': 1000}, + slo={'min_segment_size': 500})), + dict(method='HEAD', + uri='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container), + status_code=404), + dict(method='PUT', + uri='{endpoint}/{container}'.format( + endpoint=self.endpoint, container=self.container), + status_code=201, + headers={ + 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=UTF-8', + }), + dict(method='HEAD', + uri='{endpoint}/{container}'.format( + endpoint=self.endpoint, container=self.container), + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}), + dict(method='HEAD', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, container=self.container, + object=self.object), + status_code=200), + dict(method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=201, + validate=dict(headers={})), + ]) + + self.cloud.create_object( + container=self.container, name=self.object, + filename=self.object_file.name, + generate_checksums=False) + + self.assert_calls() diff --git a/releasenotes/notes/object-checksum-generation-ea1c1e47d2290054.yaml b/releasenotes/notes/object-checksum-generation-ea1c1e47d2290054.yaml new file mode 100644 index 000000000..e27a87396 --- /dev/null +++ b/releasenotes/notes/object-checksum-generation-ea1c1e47d2290054.yaml @@ -0,0 +1,4 @@ +--- +features: + - Add flag for disabling object checksum generation + From 6ef2b1fffb648e3ca9aaaf2241f3848ba0a7a2a2 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 19 Jul 2018 12:53:03 -0500 Subject: [PATCH 2133/3836] Add ability to pass data to create_object create_object currently only accepts data in files. That's not awesome for some usecases, such as creating 0-byte 'directory' nodes. Add a data parameter so that data can be passed in. Change-Id: I35e08d7b1a4fd6ded822edeba9e62a1390a6c4e8 --- openstack/cloud/openstackcloud.py | 43 ++++++++++++--- openstack/tests/unit/base.py | 2 +- openstack/tests/unit/cloud/test_object.py | 52 +++++++++++++++++++ .../create-object-data-870cb543543aa983.yaml | 5 ++ 4 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/create-object-data-870cb543543aa983.yaml diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 5d8773803..cf8c9fdcb 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -7421,7 +7421,7 @@ def create_object( self, container, name, filename=None, md5=None, sha256=None, segment_size=None, use_slo=True, metadata=None, - generate_checksums=True, + generate_checksums=None, data=None, **headers): """Create a file object. @@ -7431,7 +7431,9 @@ def create_object( This container will be created if it does not exist already. :param name: Name for the object within the container. :param filename: The path to the local file whose contents will be - uploaded. + uploaded. Mutually exclusive with data. + :param data: The content to upload to the object. Mutually exclusive + with filename. :param md5: A hexadecimal md5 of the file. (Optional), if it is known and can be passed here, it will save repeating the expensive md5 process. It is assumed to be accurate. @@ -7454,10 +7456,25 @@ def create_object( :raises: ``OpenStackCloudException`` on operation error. """ + if data is not None and filename: + raise ValueError( + "Both filename and data given. Please choose one.") + if data is not None and not name: + raise ValueError( + "name is a required parameter when data is given") + if data is not None and generate_checksums: + raise ValueError( + "checksums cannot be generated with data parameter") + if generate_checksums is None: + if data is not None: + generate_checksums = False + else: + generate_checksums = True + if not metadata: metadata = {} - if not filename: + if not filename and data is None: filename = name # segment_size gets used as a step value in a range call, so needs @@ -7465,7 +7482,10 @@ def create_object( if segment_size: segment_size = int(segment_size) segment_size = self.get_object_segment_size(segment_size) - file_size = os.path.getsize(filename) + if filename: + file_size = os.path.getsize(filename) + else: + file_size = len(data) if generate_checksums and (md5 is None or sha256 is None): (md5, sha256) = self._get_file_hashes(filename) @@ -7479,10 +7499,17 @@ def create_object( # On some clouds this is not necessary. On others it is. I'm confused. self.create_container(container) + endpoint = '{container}/{name}'.format(container=container, name=name) + + if data is not None: + self.log.debug( + "swift uploading data to %(endpoint)s", + {'endpoint': endpoint}) + + return self._upload_object_data(endpoint, data, headers) + if self.is_object_stale(container, name, filename, md5, sha256): - endpoint = '{container}/{name}'.format( - container=container, name=name) self.log.debug( "swift uploading %(filename)s to %(endpoint)s", {'filename': filename, 'endpoint': endpoint}) @@ -7494,6 +7521,10 @@ def create_object( endpoint, filename, headers, file_size, segment_size, use_slo) + def _upload_object_data(self, endpoint, data, headers): + return self._object_store_client.put( + endpoint, headers=headers, data=data) + def _upload_object(self, endpoint, filename, headers): return self._object_store_client.put( endpoint, headers=headers, data=open(filename, 'r')) diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index f4e3e3ee4..2b115a1ce 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -594,7 +594,7 @@ def __do_register_uris(self, uri_mock_list=None): key = '{method}|{uri}|{params}'.format( method=method, uri=uri, params=kw_params) validate = to_mock.pop('validate', {}) - valid_keys = set(['json', 'headers', 'params']) + valid_keys = set(['json', 'headers', 'params', 'data']) invalid_keys = set(validate.keys()) - valid_keys if invalid_keys: raise TypeError( diff --git a/openstack/tests/unit/cloud/test_object.py b/openstack/tests/unit/cloud/test_object.py index b13d81446..6e08d52b6 100644 --- a/openstack/tests/unit/cloud/test_object.py +++ b/openstack/tests/unit/cloud/test_object.py @@ -950,3 +950,55 @@ def test_create_object_skip_checksum(self): generate_checksums=False) self.assert_calls() + + def test_create_object_data(self): + + self.register_uris([ + dict(method='GET', + uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': 1000}, + slo={'min_segment_size': 500})), + dict(method='HEAD', + uri='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container), + status_code=404), + dict(method='PUT', + uri='{endpoint}/{container}'.format( + endpoint=self.endpoint, container=self.container), + status_code=201, + headers={ + 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=UTF-8', + }), + dict(method='HEAD', + uri='{endpoint}/{container}'.format( + endpoint=self.endpoint, container=self.container), + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}), + dict(method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=201, + validate=dict( + headers={}, + data=self.content, + )), + ]) + + self.cloud.create_object( + container=self.container, name=self.object, + data=self.content) + + self.assert_calls() diff --git a/releasenotes/notes/create-object-data-870cb543543aa983.yaml b/releasenotes/notes/create-object-data-870cb543543aa983.yaml new file mode 100644 index 000000000..4e0255050 --- /dev/null +++ b/releasenotes/notes/create-object-data-870cb543543aa983.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add a data parameter to ``openstack.connection.Connection.create_object`` + so that data can be passed in directly instead of through a file. From d7d6072f15f5995e36d6f7406113989de2365e95 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 19 Jul 2018 13:02:51 -0500 Subject: [PATCH 2134/3836] Add create_directory_marker_object method Add a helper method for creating zero-byte directory objects for web traversal. Change-Id: If21964f2c1cf7f30cb058921f0a63fb1824c2af2 --- openstack/cloud/openstackcloud.py | 29 +++++++++++ openstack/tests/unit/cloud/test_object.py | 51 +++++++++++++++++++ ...ate-object-directory-98e2cae175cc5082.yaml | 7 +++ 3 files changed, 87 insertions(+) create mode 100644 releasenotes/notes/create-object-directory-98e2cae175cc5082.yaml diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index cf8c9fdcb..f075e862a 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -7417,6 +7417,35 @@ def is_object_stale( {'container': container, 'name': name}) return False + def create_directory_marker_object(self, container, name, **headers): + """Create a zero-byte directory marker object + + .. note:: + + This method is not needed in most cases. Modern swift does not + require directory marker objects. However, some swift installs may + need these. + + When using swift Static Web and Web Listings to serve static content + one may need to create a zero-byte object to represent each + "directory". Doing so allows Web Listings to generate an index of the + objects inside of it, and allows Static Web to render index.html + "files" that are "inside" the directory. + + :param container: The name of the container. + :param name: Name for the directory marker object within the container. + :param headers: These will be passed through to the object creation + API as HTTP Headers. + """ + headers['content-type'] = 'application/directory' + + return self.create_object( + container, + name, + data='', + generate_checksums=False, + **headers) + def create_object( self, container, name, filename=None, md5=None, sha256=None, segment_size=None, diff --git a/openstack/tests/unit/cloud/test_object.py b/openstack/tests/unit/cloud/test_object.py index 6e08d52b6..448a30f77 100644 --- a/openstack/tests/unit/cloud/test_object.py +++ b/openstack/tests/unit/cloud/test_object.py @@ -470,6 +470,57 @@ def test_create_object(self): self.assert_calls() + def test_create_directory_marker_object(self): + + self.register_uris([ + dict(method='GET', + uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': 1000}, + slo={'min_segment_size': 500})), + dict(method='HEAD', + uri='{endpoint}/{container}'.format( + endpoint=self.endpoint, + container=self.container), + status_code=404), + dict(method='PUT', + uri='{endpoint}/{container}'.format( + endpoint=self.endpoint, container=self.container), + status_code=201, + headers={ + 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=UTF-8', + }), + dict(method='HEAD', + uri='{endpoint}/{container}'.format( + endpoint=self.endpoint, container=self.container), + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}), + dict(method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=201, + validate=dict( + headers={ + 'content-type': 'application/directory', + })) + ]) + + self.cloud.create_directory_marker_object( + container=self.container, name=self.object) + + self.assert_calls() + def test_create_dynamic_large_object(self): max_file_size = 2 diff --git a/releasenotes/notes/create-object-directory-98e2cae175cc5082.yaml b/releasenotes/notes/create-object-directory-98e2cae175cc5082.yaml new file mode 100644 index 000000000..08cce1665 --- /dev/null +++ b/releasenotes/notes/create-object-directory-98e2cae175cc5082.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Added a ``create_directory_marker_object``' method to allow for easy + creation of zero-byte 'directory' marker objects. These are not needed + in most cases, but on some clouds they are used by Static + Web and Web Listings in swift to facilitate directory traversal. From 031acd99916d6835399a777cbbad476f63d68646 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Fri, 20 Jul 2018 13:43:11 +0200 Subject: [PATCH 2135/3836] baremetal: correct the default timeout in Node.set_provision_state It should be None, which allows the client to wait until the server side timeout. True is probably cast to 1, which is incorrect. Change-Id: I6ae42b44b5460fed5291584576bb9eb7bcdfecb4 --- openstack/baremetal/v1/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index ac5b2c9fa..a57574411 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -122,7 +122,7 @@ class Node(resource.Resource): def set_provision_state(self, session, target, config_drive=None, clean_steps=None, rescue_password=None, - wait=False, timeout=True): + wait=False, timeout=None): """Run an action modifying this node's provision state. This call is asynchronous, it will return success as soon as the Bare From 66ebf04bac3d2738937e4a45137556556c627bab Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Fri, 20 Jul 2018 19:25:29 -0700 Subject: [PATCH 2136/3836] Docs: Include CloudRegion class This class was referenced (several times) from the configuration documentation, but not included in the docs. Change-Id: Ib8a075713694fb0e52c730624fbe01d8964c6903 --- doc/source/user/config/reference.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/source/user/config/reference.rst b/doc/source/user/config/reference.rst index 1fc509baa..b4909ad32 100644 --- a/doc/source/user/config/reference.rst +++ b/doc/source/user/config/reference.rst @@ -6,5 +6,9 @@ API Reference :synopsis: OpenStack client configuration .. autoclass:: openstack.config.OpenStackConfig - :members: - :inherited-members: + :members: + :inherited-members: + +.. autoclass:: openstack.config.cloud_region.CloudRegion + :members: + :inherited-members: From d9f648dd547d741182ee81d16acc6270411c1e6c Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Fri, 20 Jul 2018 19:26:21 -0700 Subject: [PATCH 2137/3836] Docs: Remove duplicate content in connection page This paragraph at the bottom of the connection page duplicates the content just above it. Change-Id: Icb718a469940d17dff7d0401ceb3f03e88d5af30 --- doc/source/user/guides/connect.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/doc/source/user/guides/connect.rst b/doc/source/user/guides/connect.rst index c20698910..bcf15fe02 100644 --- a/doc/source/user/guides/connect.rst +++ b/doc/source/user/guides/connect.rst @@ -27,10 +27,6 @@ Next Now that you can create a connection, continue with the :ref:`user_guides` to work with an OpenStack service. -As an alternative to creating a :class:`~openstack.connection.Connection` -using :ref:config-clouds-yaml, you can connect using -`config-environment-variables`. - .. TODO(shade) Update the text here and consolidate with the old os-client-config docs so that we have a single and consistent explanation of the envvars cloud, etc. From 71067ce6788e70c6e6183873de35b21e3e43e276 Mon Sep 17 00:00:00 2001 From: Artom Lifshitz Date: Tue, 24 Jul 2018 20:58:14 -0400 Subject: [PATCH 2138/3836] Send disk_over_commit if nova api < 2.25 Previously, disk_over_commit was not exposed and was defaulted to False. At least one future SDK user (openstackclient) will need disk_over_commit support from the SDK. This patch adds disk_over_commit to the live_migrate parameters, and sends it to the server if running with api < 2.25. Change-Id: I3f1c6eab41432b9db79ad147cfac3ff63eb70efa Story: #2002963 Task: #23177 --- openstack/compute/v2/server.py | 13 +++++++------ openstack/tests/unit/compute/v2/test_server.py | 7 ++++--- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 7408bf83b..30e39fe36 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -363,7 +363,8 @@ def get_console_output(self, session, length=None): resp = self._action(session, body) return resp.json() - def live_migrate(self, session, host, force, block_migration): + def live_migrate(self, session, host, force, block_migration, + disk_over_commit=False): if utils.supports_microversion(session, '2.30'): return self._live_migrate_30( session, host, @@ -378,7 +379,8 @@ def live_migrate(self, session, host, force, block_migration): return self._live_migrate( session, host, force=force, - block_migration=block_migration) + block_migration=block_migration, + disk_over_commit=disk_over_commit) def _live_migrate_30(self, session, host, force, block_migration): microversion = '2.30' @@ -412,19 +414,18 @@ def _live_migrate_25(self, session, host, force, block_migration): self._action( session, {'os-migrateLive': body}, microversion=microversion) - def _live_migrate(self, session, host, force, block_migration): + def _live_migrate(self, session, host, force, block_migration, + disk_over_commit): microversion = None - # disk_over_commit is not exposed because post 2.25 it has been - # removed and no SDK user is depending on it today. body = { 'host': None, - 'disk_over_commit': False, } if block_migration == 'auto': raise ValueError( "Live migration on this cloud does not support 'auto' as" " a parameter to block_migration, but only True and False.") body['block_migration'] = block_migration or False + body['disk_over_commit'] = disk_over_commit or False if host: body['host'] = host if not force: diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 5bf52719b..bb9cc6b28 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -681,15 +681,16 @@ class FakeEndpointData(object): self.sess.get_endpoint_data.return_value = FakeEndpointData() res = sot.live_migrate( - self.sess, host='HOST2', force=True, block_migration=False) + self.sess, host='HOST2', force=True, block_migration=True, + disk_over_commit=True) self.assertIsNone(res) url = 'servers/IDENTIFIER/action' body = { 'os-migrateLive': { 'host': 'HOST2', - 'disk_over_commit': False, - 'block_migration': False + 'disk_over_commit': True, + 'block_migration': True } } From 73ab97c5572369cea7b9646d7ba09f4cd9e759b3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 25 Jul 2018 10:01:05 -0500 Subject: [PATCH 2139/3836] Support passing profile to get_one If someone is constructing a CloudRegion programmatically, we actually don't let them pass a profile in the kwargs to get_one. Extract it and pass it to expand_vendor_profile. Change-Id: I70af6c8a0faf935b321d778d978ce77cda17ccae --- openstack/config/loader.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 41e8acb31..d38cc4bf6 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -418,7 +418,7 @@ def _get_region(self, cloud=None, region_name=''): def get_cloud_names(self): return self.cloud_config['clouds'].keys() - def _get_base_cloud_config(self, name): + def _get_base_cloud_config(self, name, profile=None): cloud = dict() # Only validate cloud name if one was given @@ -428,6 +428,8 @@ def _get_base_cloud_config(self, name): name=name)) our_cloud = self.cloud_config['clouds'].get(name, dict()) + if profile: + our_cloud['profile'] = profile # Get the defaults cloud.update(self.defaults) @@ -998,6 +1000,7 @@ def get_one( on missing required auth parameters """ + profile = kwargs.pop('profile', None) args = self._fix_args(kwargs, argparse=argparse) if cloud is None: @@ -1006,7 +1009,7 @@ def get_one( else: cloud = self.default_cloud - config = self._get_base_cloud_config(cloud) + config = self._get_base_cloud_config(cloud, profile) # Get region specific settings if 'region_name' not in args: From 9281c5b755757f5c4342d0edf4207dcfb7d3ffe0 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Thu, 26 Jul 2018 12:35:04 +0000 Subject: [PATCH 2140/3836] Update reno for stable/rocky Change-Id: Id86c6eecea0a39a994dbed9672610fd3f5f34fd3 --- releasenotes/source/index.rst | 1 + releasenotes/source/rocky.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/rocky.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 0dc3c6227..dba663344 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ :maxdepth: 1 unreleased + rocky queens pike ocata diff --git a/releasenotes/source/rocky.rst b/releasenotes/source/rocky.rst new file mode 100644 index 000000000..40dd517b7 --- /dev/null +++ b/releasenotes/source/rocky.rst @@ -0,0 +1,6 @@ +=================================== + Rocky Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/rocky From a3c1690231f8ca98e53ce26e6916de26a71eab48 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Wed, 18 Jul 2018 10:10:11 +0200 Subject: [PATCH 2141/3836] Support for microversions in base Resource This change allows a Resource to negotiate the highest microversion supported by both the client (openstacksdk) and the server. It is done by overriding Class._max_microversion to a non-None value. The microversion used to load or update a Resource is stored on it as the "microversion" attribute. Change-Id: I78703bece7c1a3898e4a5b60cd7c2912cf4b5595 --- openstack/resource.py | 83 ++++++- .../tests/unit/compute/v2/test_limits.py | 1 + openstack/tests/unit/image/v2/test_image.py | 5 +- .../tests/unit/network/v2/test_floating_ip.py | 5 +- openstack/tests/unit/test_resource.py | 222 +++++++++++++++--- openstack/tests/unit/test_utils.py | 30 +++ openstack/utils.py | 32 +++ 7 files changed, 335 insertions(+), 43 deletions(-) diff --git a/openstack/resource.py b/openstack/resource.py index 9742d212d..cc8371049 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -322,6 +322,11 @@ class Resource(object): #: Is this a detailed version of another Resource detail_for = None + #: Maximum microversion to use for getting/creating/updating the Resource + _max_microversion = None + #: API microversion (string or None) this Resource was loaded with + microversion = None + def __init__(self, _synchronized=False, **attrs): """The base resource @@ -330,6 +335,7 @@ def __init__(self, _synchronized=False, **attrs): :meth:`~openstack.resource.Resource.existing`. """ + self.microversion = attrs.pop('microversion', None) # NOTE: _collect_attrs modifies **attrs in place, removing # items as they match up with any of the body, header, # or uri mappings. @@ -388,6 +394,7 @@ def _update(self, **attrs): layer when updating instances that may have already been created. """ + self.microversion = attrs.pop('microversion', None) body, header, uri = self._collect_attrs(attrs) self._body.update(body) @@ -729,6 +736,44 @@ def _get_session(cls, session): " instance of an openstack.proxy.Proxy object or at the very least" " a raw keystoneauth1.adapter.Adapter.") + @classmethod + def _get_microversion_for_list(cls, session): + """Get microversion to use when listing resources. + + The base version uses the following logic: + 1. If the session has a default microversion for the current service, + just use it. + 2. If ``self._max_microversion`` is not ``None``, use minimum between + it and the maximum microversion supported by the server. + 3. Otherwise use ``None``. + + Subclasses can override this method if more complex logic is needed. + + :param session: :class`keystoneauth1.adapter.Adapter` + :return: microversion as string or ``None`` + """ + if session.default_microversion: + return session.default_microversion + + return utils.maximum_supported_microversion(session, + cls._max_microversion) + + def _get_microversion_for(self, session, action): + """Get microversion to use for the given action. + + The base version uses :meth:`_get_microversion_for_list`. + Subclasses can override this method if more complex logic is needed. + + :param session: :class`keystoneauth1.adapter.Adapter` + :param action: One of "get", "update", "create", "delete". Unused in + the base implementation. + :return: microversion as string or ``None`` + """ + if action not in ('get', 'update', 'create', 'delete'): + raise ValueError('Invalid action: %s' % action) + + return self._get_microversion_for_list(session) + def create(self, session, prepend_key=True): """Create a remote resource based on this instance. @@ -746,20 +791,24 @@ def create(self, session, prepend_key=True): raise exceptions.MethodNotSupported(self, "create") session = self._get_session(session) + microversion = self._get_microversion_for(session, 'create') if self.create_method == 'PUT': request = self._prepare_request(requires_id=True, prepend_key=prepend_key) response = session.put(request.url, - json=request.body, headers=request.headers) + json=request.body, headers=request.headers, + microversion=microversion) elif self.create_method == 'POST': request = self._prepare_request(requires_id=False, prepend_key=prepend_key) response = session.post(request.url, - json=request.body, headers=request.headers) + json=request.body, headers=request.headers, + microversion=microversion) else: raise exceptions.ResourceFailure( msg="Invalid create method: %s" % self.create_method) + self.microversion = microversion self._translate_response(response) return self @@ -779,11 +828,13 @@ def get(self, session, requires_id=True, error_message=None): request = self._prepare_request(requires_id=requires_id) session = self._get_session(session) - response = session.get(request.url) + microversion = self._get_microversion_for(session, 'get') + response = session.get(request.url, microversion=microversion) kwargs = {} if error_message: kwargs['error_message'] = error_message + self.microversion = microversion self._translate_response(response, **kwargs) return self @@ -803,9 +854,12 @@ def head(self, session): request = self._prepare_request() session = self._get_session(session) + microversion = self._get_microversion_for(session, 'get') response = session.head(request.url, - headers={"Accept": ""}) + headers={"Accept": ""}, + microversion=microversion) + self.microversion = microversion self._translate_response(response, has_body=False) return self @@ -834,20 +888,25 @@ def update(self, session, prepend_key=True, has_body=True): request = self._prepare_request(prepend_key=prepend_key) session = self._get_session(session) + microversion = self._get_microversion_for(session, 'update') if self.update_method == 'PATCH': response = session.patch( - request.url, json=request.body, headers=request.headers) + request.url, json=request.body, headers=request.headers, + microversion=microversion) elif self.update_method == 'POST': response = session.post( - request.url, json=request.body, headers=request.headers) + request.url, json=request.body, headers=request.headers, + microversion=microversion) elif self.update_method == 'PUT': response = session.put( - request.url, json=request.body, headers=request.headers) + request.url, json=request.body, headers=request.headers, + microversion=microversion) else: raise exceptions.ResourceFailure( msg="Invalid update method: %s" % self.update_method) + self.microversion = microversion self._translate_response(response, has_body=has_body) return self @@ -866,9 +925,11 @@ def delete(self, session, error_message=None): request = self._prepare_request() session = self._get_session(session) + microversion = self._get_microversion_for(session, 'delete') response = session.delete(request.url, - headers={"Accept": ""}) + headers={"Accept": ""}, + microversion=microversion) kwargs = {} if error_message: kwargs['error_message'] = error_message @@ -910,6 +971,7 @@ def list(cls, session, paginated=False, **params): if not cls.allow_list: raise exceptions.MethodNotSupported(cls, "list") session = cls._get_session(session) + microversion = cls._get_microversion_for_list(session) cls._query_mapping._validate(params, base_path=cls.base_path) query_params = cls._query_mapping._transpose(params) @@ -925,7 +987,8 @@ def list(cls, session, paginated=False, **params): response = session.get( uri, headers={"Accept": "application/json"}, - params=query_params.copy()) + params=query_params.copy(), + microversion=microversion) exceptions.raise_from_response(response) data = response.json() @@ -950,7 +1013,7 @@ def list(cls, session, paginated=False, **params): # argument and is practically a reserved word. raw_resource.pop("self", None) - value = cls.existing(**raw_resource) + value = cls.existing(microversion=microversion, **raw_resource) marker = value.id yield value total_yielded += 1 diff --git a/openstack/tests/unit/compute/v2/test_limits.py b/openstack/tests/unit/compute/v2/test_limits.py index 81ded5765..90144875a 100644 --- a/openstack/tests/unit/compute/v2/test_limits.py +++ b/openstack/tests/unit/compute/v2/test_limits.py @@ -148,6 +148,7 @@ def test_basic(self): def test_get(self): sess = mock.Mock(spec=adapter.Adapter) + sess.default_microversion = None resp = mock.Mock() sess.get.return_value = resp resp.json.return_value = copy.deepcopy(LIMITS_BODY) diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index a898b096a..d1367f3d0 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -106,6 +106,7 @@ def setUp(self): self.resp.json = mock.Mock(return_value=self.resp.body) self.sess = mock.Mock(spec=adapter.Adapter) self.sess.post = mock.Mock(return_value=self.resp) + self.sess.default_microversion = None def test_basic(self): sot = image.Image() @@ -266,7 +267,7 @@ def test_download_no_checksum_header(self): self.sess.get.assert_has_calls( [mock.call('images/IDENTIFIER/file', stream=False), - mock.call('images/IDENTIFIER',)]) + mock.call('images/IDENTIFIER', microversion=None)]) self.assertEqual(rv, resp1.content) @@ -292,7 +293,7 @@ def test_download_no_checksum_at_all2(self): self.sess.get.assert_has_calls( [mock.call('images/IDENTIFIER/file', stream=False), - mock.call('images/IDENTIFIER',)]) + mock.call('images/IDENTIFIER', microversion=None)]) self.assertEqual(rv, resp1.content) diff --git a/openstack/tests/unit/network/v2/test_floating_ip.py b/openstack/tests/unit/network/v2/test_floating_ip.py index 2c80684d5..b708dd5bc 100644 --- a/openstack/tests/unit/network/v2/test_floating_ip.py +++ b/openstack/tests/unit/network/v2/test_floating_ip.py @@ -76,6 +76,7 @@ def test_make_it(self): def test_find_available(self): mock_session = mock.Mock(spec=adapter.Adapter) mock_session.get_filter = mock.Mock(return_value={}) + mock_session.default_microversion = None data = {'id': 'one', 'floating_ip_address': '10.0.0.1'} fake_response = mock.Mock() body = {floating_ip.FloatingIP.resources_key: [data]} @@ -89,10 +90,12 @@ def test_find_available(self): mock_session.get.assert_called_with( floating_ip.FloatingIP.base_path, headers={'Accept': 'application/json'}, - params={}) + params={}, + microversion=None) def test_find_available_nada(self): mock_session = mock.Mock(spec=adapter.Adapter) + mock_session.default_microversion = None fake_response = mock.Mock() body = {floating_ip.FloatingIP.resources_key: []} fake_response.json = mock.Mock(return_value=body) diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 1f7eff912..be5e18c26 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -976,8 +976,14 @@ class Test(resource.Resource): self.session.post = mock.Mock(return_value=self.response) self.session.delete = mock.Mock(return_value=self.response) self.session.head = mock.Mock(return_value=self.response) + self.session.default_microversion = None - def _test_create(self, cls, requires_id=False, prepend_key=False): + self.endpoint_data = mock.Mock(max_microversion='1.99', + min_microversion=None) + self.session.get_endpoint_data.return_value = self.endpoint_data + + def _test_create(self, cls, requires_id=False, prepend_key=False, + microversion=None): id = "id" if requires_id else None sot = cls(id=id) sot._prepare_request = mock.Mock(return_value=self.request) @@ -990,12 +996,15 @@ def _test_create(self, cls, requires_id=False, prepend_key=False): if requires_id: self.session.put.assert_called_once_with( self.request.url, - json=self.request.body, headers=self.request.headers) + json=self.request.body, headers=self.request.headers, + microversion=microversion) else: self.session.post.assert_called_once_with( self.request.url, - json=self.request.body, headers=self.request.headers) + json=self.request.body, headers=self.request.headers, + microversion=microversion) + self.assertEqual(sot.microversion, microversion) sot._translate_response.assert_called_once_with(self.response) self.assertEqual(result, sot) @@ -1008,6 +1017,17 @@ class Test(resource.Resource): self._test_create(Test, requires_id=True, prepend_key=True) + def test_put_create_with_microversion(self): + class Test(resource.Resource): + service = self.service_name + base_path = self.base_path + allow_create = True + create_method = 'PUT' + _max_microversion = '1.42' + + self._test_create(Test, requires_id=True, prepend_key=True, + microversion='1.42') + def test_post_create(self): class Test(resource.Resource): service = self.service_name @@ -1022,17 +1042,39 @@ def test_get(self): self.sot._prepare_request.assert_called_once_with(requires_id=True) self.session.get.assert_called_once_with( - self.request.url,) + self.request.url, microversion=None) + self.assertIsNone(self.sot.microversion) self.sot._translate_response.assert_called_once_with(self.response) self.assertEqual(result, self.sot) + def test_get_with_microversion(self): + class Test(resource.Resource): + service = self.service_name + base_path = self.base_path + allow_get = True + _max_microversion = '1.42' + + sot = Test(id='id') + sot._prepare_request = mock.Mock(return_value=self.request) + sot._translate_response = mock.Mock() + + result = sot.get(self.session) + + sot._prepare_request.assert_called_once_with(requires_id=True) + self.session.get.assert_called_once_with( + self.request.url, microversion='1.42') + + self.assertEqual(sot.microversion, '1.42') + sot._translate_response.assert_called_once_with(self.response) + self.assertEqual(result, sot) + def test_get_not_requires_id(self): result = self.sot.get(self.session, False) self.sot._prepare_request.assert_called_once_with(requires_id=False) self.session.get.assert_called_once_with( - self.request.url,) + self.request.url, microversion=None) self.sot._translate_response.assert_called_once_with(self.response) self.assertEqual(result, self.sot) @@ -1043,14 +1085,40 @@ def test_head(self): self.sot._prepare_request.assert_called_once_with() self.session.head.assert_called_once_with( self.request.url, - headers={"Accept": ""}) + headers={"Accept": ""}, + microversion=None) + self.assertIsNone(self.sot.microversion) self.sot._translate_response.assert_called_once_with( self.response, has_body=False) self.assertEqual(result, self.sot) + def test_head_with_microversion(self): + class Test(resource.Resource): + service = self.service_name + base_path = self.base_path + allow_head = True + _max_microversion = '1.42' + + sot = Test(id='id') + sot._prepare_request = mock.Mock(return_value=self.request) + sot._translate_response = mock.Mock() + + result = sot.head(self.session) + + sot._prepare_request.assert_called_once_with() + self.session.head.assert_called_once_with( + self.request.url, + headers={"Accept": ""}, + microversion='1.42') + + self.assertEqual(sot.microversion, '1.42') + sot._translate_response.assert_called_once_with( + self.response, has_body=False) + self.assertEqual(result, sot) + def _test_update(self, update_method='PUT', prepend_key=True, - has_body=True): + has_body=True, microversion=None): self.sot.update_method = update_method # Need to make sot look dirty so we can attempt an update @@ -1066,16 +1134,20 @@ def _test_update(self, update_method='PUT', prepend_key=True, if update_method == 'PATCH': self.session.patch.assert_called_once_with( self.request.url, - json=self.request.body, headers=self.request.headers) + json=self.request.body, headers=self.request.headers, + microversion=microversion) elif update_method == 'POST': self.session.post.assert_called_once_with( self.request.url, - json=self.request.body, headers=self.request.headers) + json=self.request.body, headers=self.request.headers, + microversion=microversion) elif update_method == 'PUT': self.session.put.assert_called_once_with( self.request.url, - json=self.request.body, headers=self.request.headers) + json=self.request.body, headers=self.request.headers, + microversion=microversion) + self.assertEqual(self.sot.microversion, microversion) self.sot._translate_response.assert_called_once_with( self.response, has_body=has_body) @@ -1102,12 +1174,36 @@ def test_delete(self): self.sot._prepare_request.assert_called_once_with() self.session.delete.assert_called_once_with( self.request.url, - headers={"Accept": ""}) + headers={"Accept": ""}, + microversion=None) self.sot._translate_response.assert_called_once_with( self.response, has_body=False) self.assertEqual(result, self.sot) + def test_delete_with_microversion(self): + class Test(resource.Resource): + service = self.service_name + base_path = self.base_path + allow_delete = True + _max_microversion = '1.42' + + sot = Test(id='id') + sot._prepare_request = mock.Mock(return_value=self.request) + sot._translate_response = mock.Mock() + + result = sot.delete(self.session) + + sot._prepare_request.assert_called_once_with() + self.session.delete.assert_called_once_with( + self.request.url, + headers={"Accept": ""}, + microversion='1.42') + + sot._translate_response.assert_called_once_with( + self.response, has_body=False) + self.assertEqual(result, sot) + # NOTE: As list returns a generator, testing it requires consuming # the generator. Wrap calls to self.sot.list in a `list` # and then test the results as a list of responses. @@ -1123,7 +1219,8 @@ def test_list_empty_response(self): self.session.get.assert_called_once_with( self.base_path, headers={"Accept": "application/json"}, - params={}) + params={}, + microversion=None) self.assertEqual([], result) @@ -1159,7 +1256,8 @@ def test_list_one_page_response_not_paginated(self): self.session.get.assert_called_once_with( self.base_path, headers={"Accept": "application/json"}, - params={}) + params={}, + microversion=None) self.assertEqual(1, len(results)) self.assertEqual(id_value, results[0].id) @@ -1185,7 +1283,8 @@ class Test(self.test_class): self.session.get.assert_called_once_with( self.base_path, headers={"Accept": "application/json"}, - params={}) + params={}, + microversion=None) self.assertEqual(1, len(results)) self.assertEqual(id_value, results[0].id) @@ -1219,11 +1318,13 @@ def test_list_response_paginated_without_links(self): self.assertEqual(ids[1], results[1].id) self.assertEqual( mock.call('base_path', - headers={'Accept': 'application/json'}, params={}), + headers={'Accept': 'application/json'}, params={}, + microversion=None), self.session.get.mock_calls[0]) self.assertEqual( mock.call('https://example.com/next-url', - headers={'Accept': 'application/json'}, params={}), + headers={'Accept': 'application/json'}, params={}, + microversion=None), self.session.get.mock_calls[1]) self.assertEqual(2, len(self.session.get.call_args_list)) self.assertIsInstance(results[0], self.test_class) @@ -1253,15 +1354,64 @@ def test_list_response_paginated_with_links(self): self.assertEqual(ids[1], results[1].id) self.assertEqual( mock.call('base_path', - headers={'Accept': 'application/json'}, params={}), + headers={'Accept': 'application/json'}, params={}, + microversion=None), self.session.get.mock_calls[0]) self.assertEqual( mock.call('https://example.com/next-url', - headers={'Accept': 'application/json'}, params={}), + headers={'Accept': 'application/json'}, params={}, + microversion=None), self.session.get.mock_calls[2]) self.assertEqual(2, len(self.session.get.call_args_list)) self.assertIsInstance(results[0], self.test_class) + def test_list_response_paginated_with_microversions(self): + class Test(resource.Resource): + service = self.service_name + base_path = self.base_path + resources_key = 'resources' + allow_list = True + _max_microversion = '1.42' + + ids = [1, 2] + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.links = {} + mock_response.json.return_value = { + "resources": [{"id": ids[0]}], + "resources_links": [{ + "href": "https://example.com/next-url", + "rel": "next", + }] + } + mock_response2 = mock.Mock() + mock_response2.status_code = 200 + mock_response2.links = {} + mock_response2.json.return_value = { + "resources": [{"id": ids[1]}], + } + + self.session.get.side_effect = [mock_response, mock_response2] + + results = list(Test.list(self.session, paginated=True)) + + self.assertEqual(2, len(results)) + self.assertEqual(ids[0], results[0].id) + self.assertEqual(ids[1], results[1].id) + self.assertEqual( + mock.call('base_path', + headers={'Accept': 'application/json'}, params={}, + microversion='1.42'), + self.session.get.mock_calls[0]) + self.assertEqual( + mock.call('https://example.com/next-url', + headers={'Accept': 'application/json'}, params={}, + microversion='1.42'), + self.session.get.mock_calls[1]) + self.assertEqual(2, len(self.session.get.call_args_list)) + self.assertIsInstance(results[0], Test) + self.assertEqual('1.42', results[0].microversion) + def test_list_multi_page_response_not_paginated(self): ids = [1, 2] mock_response = mock.Mock() @@ -1453,20 +1603,23 @@ def test_list_multi_page_response_paginated(self): self.session.get.assert_called_with( self.base_path, headers={"Accept": "application/json"}, - params={}) + params={}, + microversion=None) result1 = next(results) self.assertEqual(result1.id, ids[1]) self.session.get.assert_called_with( 'https://example.com/next-url', headers={"Accept": "application/json"}, - params={}) + params={}, + microversion=None) self.assertRaises(StopIteration, next, results) self.session.get.assert_called_with( 'https://example.com/next-url', headers={"Accept": "application/json"}, - params={}) + params={}, + microversion=None) def test_list_multi_page_no_early_termination(self): # This tests verifies that multipages are not early terminated. @@ -1508,7 +1661,8 @@ def test_list_multi_page_no_early_termination(self): self.session.get.assert_called_with( self.base_path, headers={"Accept": "application/json"}, - params={"limit": 3}) + params={"limit": 3}, + microversion=None) # Second page contains another two items result2 = next(results) @@ -1518,7 +1672,8 @@ def test_list_multi_page_no_early_termination(self): self.session.get.assert_called_with( self.base_path, headers={"Accept": "application/json"}, - params={"limit": 3, "marker": 2}) + params={"limit": 3, "marker": 2}, + microversion=None) # Ensure we're done after those four items self.assertRaises(StopIteration, next, results) @@ -1527,7 +1682,8 @@ def test_list_multi_page_no_early_termination(self): self.session.get.assert_called_with( self.base_path, headers={"Accept": "application/json"}, - params={"limit": 3, "marker": 4}) + params={"limit": 3, "marker": 4}, + microversion=None) # Ensure we made three calls to get this done self.assertEqual(3, len(self.session.get.call_args_list)) @@ -1564,14 +1720,16 @@ def test_list_multi_page_inferred_additional(self): self.session.get.assert_called_with( self.base_path, headers={"Accept": "application/json"}, - params={"limit": 2}) + params={"limit": 2}, + microversion=None) result2 = next(results) self.assertEqual(result2.id, ids[2]) self.session.get.assert_called_with( self.base_path, headers={"Accept": "application/json"}, - params={'limit': 2, 'marker': 2}) + params={'limit': 2, 'marker': 2}, + microversion=None) # Ensure we're done after those three items self.assertRaises(StopIteration, next, results) @@ -1612,14 +1770,16 @@ class Test(self.test_class): self.session.get.assert_called_with( self.base_path, headers={"Accept": "application/json"}, - params={}) + params={}, + microversion=None) result2 = next(results) self.assertEqual(result2.id, ids[2]) self.session.get.assert_called_with( self.base_path, headers={"Accept": "application/json"}, - params={'marker': 2}) + params={'marker': 2}, + microversion=None) # Ensure we're done after those three items self.assertRaises(StopIteration, next, results) @@ -1658,14 +1818,16 @@ def test_list_multi_page_link_header(self): self.session.get.assert_called_with( self.base_path, headers={"Accept": "application/json"}, - params={}) + params={}, + microversion=None) result2 = next(results) self.assertEqual(result2.id, ids[2]) self.session.get.assert_called_with( 'https://example.com/next-url', headers={"Accept": "application/json"}, - params={}) + params={}, + microversion=None) # Ensure we're done after those three items self.assertRaises(StopIteration, next, results) diff --git a/openstack/tests/unit/test_utils.py b/openstack/tests/unit/test_utils.py index aa947ffa1..8b7e5dc81 100644 --- a/openstack/tests/unit/test_utils.py +++ b/openstack/tests/unit/test_utils.py @@ -104,3 +104,33 @@ def test_with_none(self): result = utils.urljoin(root, *leaves) self.assertEqual(result, "http://www.example.com/foo/") + + +class TestMaximumSupportedMicroversion(base.TestCase): + def setUp(self): + super(TestMaximumSupportedMicroversion, self).setUp() + self.adapter = mock.Mock(spec=['get_endpoint_data']) + self.endpoint_data = mock.Mock(spec=['min_microversion', + 'max_microversion'], + min_microversion=None, + max_microversion='1.99') + self.adapter.get_endpoint_data.return_value = self.endpoint_data + + def test_with_none(self): + self.assertIsNone(utils.maximum_supported_microversion(self.adapter, + None)) + + def test_with_value(self): + self.assertEqual('1.42', + utils.maximum_supported_microversion(self.adapter, + '1.42')) + + def test_value_more_than_max(self): + self.assertEqual('1.99', + utils.maximum_supported_microversion(self.adapter, + '1.100')) + + def test_value_less_than_min(self): + self.endpoint_data.min_microversion = '1.42' + self.assertIsNone(utils.maximum_supported_microversion(self.adapter, + '1.2')) diff --git a/openstack/utils.py b/openstack/utils.py index 4f67f3603..fafa47e92 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -183,3 +183,35 @@ def pick_microversion(session, required): if required is not None: return discover.version_to_string(required) + + +def maximum_supported_microversion(adapter, client_maximum): + """Determinte the maximum microversion supported by both client and server. + + :param adapter: :class:`~keystoneauth1.adapter.Adapter` instance. + :param client_maximum: Maximum microversion supported by the client. + If ``None``, ``None`` is returned. + + :returns: the maximum supported microversion as string or ``None``. + """ + if client_maximum is None: + return None + + endpoint_data = adapter.get_endpoint_data() + if not endpoint_data.max_microversion: + return None + + client_max = discover.normalize_version_number(client_maximum) + server_max = discover.normalize_version_number( + endpoint_data.max_microversion) + + if endpoint_data.min_microversion: + server_min = discover.normalize_version_number( + endpoint_data.min_microversion) + if client_max < server_min: + # NOTE(dtantsur): we may want to raise in this case, but this keeps + # the current behavior intact. + return None + + result = min(client_max, server_max) + return discover.version_to_string(result) From 100fd90401bd05893f9b4d6552dd3793971f37b8 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Wed, 18 Jul 2018 10:10:11 +0200 Subject: [PATCH 2142/3836] Support for microversions in baremetal resources The baremetal resources are updated with a _max_microversion reflecting the currently supported set of fields. Doing that would make create() call create nodes in the "enroll" state, which would be a breaking change. Thus, the create() call is updated to handle provision_state being set to enroll, manageable or available, with available being the default. Change-Id: If46e339070514bcd34bc3ba336f0cfb5556a5cea --- openstack/baremetal/v1/_common.py | 6 ++ openstack/baremetal/v1/node.py | 67 +++++++++++++ openstack/baremetal/v1/port.py | 5 +- openstack/baremetal/v1/port_group.py | 3 + openstack/exceptions.py | 4 + openstack/resource.py | 50 +++++++++- .../tests/unit/baremetal/v1/test_node.py | 97 +++++++++++++++++++ openstack/tests/unit/test_resource.py | 42 ++++++++ 8 files changed, 270 insertions(+), 4 deletions(-) diff --git a/openstack/baremetal/v1/_common.py b/openstack/baremetal/v1/_common.py index 16b5506ae..ffa1ee668 100644 --- a/openstack/baremetal/v1/_common.py +++ b/openstack/baremetal/v1/_common.py @@ -46,3 +46,9 @@ 'rescue': 'rescue', } """Mapping of provisioning actions to expected stable states.""" + +STATE_VERSIONS = { + 'enroll': '1.11', + 'manageable': '1.4', +} +"""API versions when certain states were introduced.""" diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index a57574411..428d8ab39 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -41,6 +41,9 @@ class Node(resource.Resource): is_maintenance='maintenance', ) + # Full port groups support introduced in 1.24 + _max_microversion = '1.24' + # Properties #: The UUID of the chassis associated wit this node. Can be empty or None. chassis_id = resource.Body("chassis_uuid") @@ -120,6 +123,70 @@ class Node(resource.Resource): #: Timestamp at which the node was last updated. updated_at = resource.Body("updated_at") + def create(self, session, *args, **kwargs): + """Create a remote resource based on this instance. + + The overridden version is capable of handling the populated + ``provision_state`` field of one of three values: ``enroll``, + ``manageable`` or ``available``. The default is currently + ``available``, since it's the only state supported by all API versions. + + Note that Bare Metal API 1.4 is required for ``manageable`` and + 1.11 is required for ``enroll``. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + + :return: This :class:`Resource` instance. + :raises: ValueError if the Node's ``provision_state`` is not one of + ``None``, ``enroll``, ``manageable`` or ``available``. + :raises: :exc:`~openstack.exceptions.NotSupported` if + the ``provision_state`` cannot be reached with any API version + supported by the server. + """ + expected_provision_state = self.provision_state + if expected_provision_state is None: + expected_provision_state = 'available' + + if expected_provision_state not in ('enroll', + 'manageable', + 'available'): + raise ValueError( + "Node's provision_state must be one of 'enroll', " + "'manageable' or 'available' for creation, got %s" % + expected_provision_state) + + session = self._get_session(session) + # Verify that the requested provision state is reachable with the API + # version we are going to use. + try: + expected_version = _common.STATE_VERSIONS[expected_provision_state] + except KeyError: + pass + else: + self._assert_microversion_for( + session, 'create', expected_version, + error_message="Cannot create a node with initial provision " + "state %s" % expected_provision_state) + + # Ironic cannot set provision_state itself, so marking it as unchanged + self._body.clean(only={'provision_state'}) + super(Node, self).create(session, *args, **kwargs) + + if (self.provision_state == 'enroll' and + expected_provision_state != 'enroll'): + self.set_provision_state(session, 'manage', wait=True) + + if (self.provision_state == 'manageable' and + expected_provision_state == 'available'): + self.set_provision_state(session, 'provide', wait=True) + + if (self.provision_state == 'available' and + expected_provision_state == 'manageable'): + self.set_provision_state(session, 'manage', wait=True) + + return self + def set_provision_state(self, session, target, config_drive=None, clean_steps=None, rescue_password=None, wait=False, timeout=None): diff --git a/openstack/baremetal/v1/port.py b/openstack/baremetal/v1/port.py index cca5d14aa..99879de35 100644 --- a/openstack/baremetal/v1/port.py +++ b/openstack/baremetal/v1/port.py @@ -32,6 +32,9 @@ class Port(resource.Resource): 'fields' ) + # Port group ID introduced in 1.24 + _max_microversion = '1.24' + #: The physical hardware address of the network port, typically the #: hardware MAC address. address = resource.Body('address') @@ -56,7 +59,7 @@ class Port(resource.Resource): #: The UUID of node this port belongs to node_id = resource.Body('node_uuid') #: The UUID of PortGroup this port belongs to. Added in API microversion - #: 1.23. + #: 1.24. port_group_id = resource.Body('portgroup_uuid') #: Timestamp at which the port was last updated. updated_at = resource.Body('updated_at') diff --git a/openstack/baremetal/v1/port_group.py b/openstack/baremetal/v1/port_group.py index d1d44e7d4..d8fda6429 100644 --- a/openstack/baremetal/v1/port_group.py +++ b/openstack/baremetal/v1/port_group.py @@ -32,6 +32,9 @@ class PortGroup(resource.Resource): 'node', 'address', 'fields', ) + # Port groups introduced in 1.23 + _max_microversion = '1.23' + #: The physical hardware address of the portgroup, typically the hardware #: MAC address. Added in API microversion 1.23. address = resource.Body('address') diff --git a/openstack/exceptions.py b/openstack/exceptions.py index 619c0b491..8d5ca7dcf 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -220,3 +220,7 @@ class ArgumentDeprecationWarning(Warning): class ConfigException(SDKException): """Something went wrong with parsing your OpenStack Config.""" + + +class NotSupported(SDKException): + """Request cannot be performed by any supported API version.""" diff --git a/openstack/resource.py b/openstack/resource.py index cc8371049..efa04e4a6 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -35,6 +35,7 @@ class that represent a remote resource. The attributes that import itertools from keystoneauth1 import adapter +from keystoneauth1 import discover import munch from requests import structures @@ -200,9 +201,16 @@ def dirty(self): return dict((key, self.attributes.get(key, None)) for key in self._dirty) - def clean(self): - """Signal that the resource no longer has modified attributes""" - self._dirty = set() + def clean(self, only=None): + """Signal that the resource no longer has modified attributes. + + :param only: an optional set of attributes to no longer consider + changed + """ + if only: + self._dirty = self._dirty - set(only) + else: + self._dirty = set() class _Request(object): @@ -774,6 +782,42 @@ def _get_microversion_for(self, session, action): return self._get_microversion_for_list(session) + def _assert_microversion_for(self, session, action, expected, + error_message=None): + """Enforce that the microversion for action satisfies the requirement. + + :param session: :class`keystoneauth1.adapter.Adapter` + :param action: One of "get", "update", "create", "delete". + :param expected: Expected microversion. + :param error_message: Optional error message with details. Will be + prepended to the message generated here. + :return: resulting microversion as string. + :raises: :exc:`~openstack.exceptions.NotSupported` if the version + used for the action is lower than the expected one. + """ + def _raise(message): + if error_message: + error_message.rstrip('.') + message = '%s. %s' % (error_message, message) + + raise exceptions.NotSupported(message) + + actual = self._get_microversion_for(session, action) + if actual is None: + message = ("API version %s is required, but the default " + "version will be used.") % expected + _raise(message) + + actual_n = discover.normalize_version_number(actual) + expected_n = discover.normalize_version_number(expected) + if actual_n < expected_n: + message = ("API version %(expected)s is required, but %(actual)s " + "will be used.") % {'expected': expected, + 'actual': actual} + _raise(message) + + return actual + def create(self, session, prepend_key=True): """Create a remote resource based on this instance. diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 5866ea256..3ae7da564 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -274,3 +274,100 @@ def setUp(self): def test_no_arguments(self): self.node.set_provision_state(self.session, 'manage') + + +@mock.patch.object(node.Node, '_translate_response', mock.Mock()) +@mock.patch.object(node.Node, '_get_session', lambda self, x: x) +@mock.patch.object(node.Node, 'set_provision_state', autospec=True) +class TestNodeCreate(base.TestCase): + + def setUp(self): + super(TestNodeCreate, self).setUp() + self.new_state = None + self.session = mock.Mock(spec=adapter.Adapter) + self.session.default_microversion = '1.1' + self.node = node.Node(driver=FAKE['driver']) + + def _change_state(*args, **kwargs): + self.node.provision_state = self.new_state + + self.session.post.side_effect = _change_state + + def test_available_old_version(self, mock_prov): + result = self.node.create(self.session) + self.assertIs(result, self.node) + self.session.post.assert_called_once_with( + mock.ANY, json={'driver': FAKE['driver']}, + headers=mock.ANY, microversion=self.session.default_microversion) + self.assertFalse(mock_prov.called) + + def test_available_new_version(self, mock_prov): + def _change_state(*args, **kwargs): + self.node.provision_state = 'manageable' + + self.session.default_microversion = '1.11' + self.node.provision_state = 'available' + self.new_state = 'enroll' + mock_prov.side_effect = _change_state + + result = self.node.create(self.session) + self.assertIs(result, self.node) + self.session.post.assert_called_once_with( + mock.ANY, json={'driver': FAKE['driver']}, + headers=mock.ANY, microversion=self.session.default_microversion) + mock_prov.assert_has_calls([ + mock.call(self.node, self.session, 'manage', wait=True), + mock.call(self.node, self.session, 'provide', wait=True) + ]) + + def test_no_enroll_in_old_version(self, mock_prov): + self.node.provision_state = 'enroll' + self.assertRaises(exceptions.NotSupported, + self.node.create, self.session) + self.assertFalse(self.session.post.called) + self.assertFalse(mock_prov.called) + + def test_enroll_new_version(self, mock_prov): + self.session.default_microversion = '1.11' + self.node.provision_state = 'enroll' + self.new_state = 'enroll' + + result = self.node.create(self.session) + self.assertIs(result, self.node) + self.session.post.assert_called_once_with( + mock.ANY, json={'driver': FAKE['driver']}, + headers=mock.ANY, microversion=self.session.default_microversion) + self.assertFalse(mock_prov.called) + + def test_no_manageable_in_old_version(self, mock_prov): + self.node.provision_state = 'manageable' + self.assertRaises(exceptions.NotSupported, + self.node.create, self.session) + self.assertFalse(self.session.post.called) + self.assertFalse(mock_prov.called) + + def test_manageable_old_version(self, mock_prov): + self.session.default_microversion = '1.4' + self.node.provision_state = 'manageable' + self.new_state = 'available' + + result = self.node.create(self.session) + self.assertIs(result, self.node) + self.session.post.assert_called_once_with( + mock.ANY, json={'driver': FAKE['driver']}, + headers=mock.ANY, microversion=self.session.default_microversion) + mock_prov.assert_called_once_with(self.node, self.session, 'manage', + wait=True) + + def test_manageable_new_version(self, mock_prov): + self.session.default_microversion = '1.11' + self.node.provision_state = 'manageable' + self.new_state = 'enroll' + + result = self.node.create(self.session) + self.assertIs(result, self.node) + self.session.post.assert_called_once_with( + mock.ANY, json={'driver': FAKE['driver']}, + headers=mock.ANY, microversion=self.session.default_microversion) + mock_prov.assert_called_once_with(self.node, self.session, 'manage', + wait=True) diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index be5e18c26..df5f70eb5 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -2106,3 +2106,45 @@ def test_timeout(self): exceptions.ResourceTimeout, resource.wait_for_delete, "session", res, 0.1, 0.3) + + +@mock.patch.object(resource.Resource, '_get_microversion_for', autospec=True) +class TestAssertMicroversionFor(base.TestCase): + session = mock.Mock() + res = resource.Resource() + + def test_compatible(self, mock_get_ver): + mock_get_ver.return_value = '1.42' + + self.assertEqual( + '1.42', + self.res._assert_microversion_for(self.session, 'get', '1.6')) + mock_get_ver.assert_called_once_with(self.res, self.session, 'get') + + def test_incompatible(self, mock_get_ver): + mock_get_ver.return_value = '1.1' + + self.assertRaisesRegex(exceptions.NotSupported, + '1.6 is required, but 1.1 will be used', + self.res._assert_microversion_for, + self.session, 'get', '1.6') + mock_get_ver.assert_called_once_with(self.res, self.session, 'get') + + def test_custom_message(self, mock_get_ver): + mock_get_ver.return_value = '1.1' + + self.assertRaisesRegex(exceptions.NotSupported, + 'boom.*1.6 is required, but 1.1 will be used', + self.res._assert_microversion_for, + self.session, 'get', '1.6', + error_message='boom') + mock_get_ver.assert_called_once_with(self.res, self.session, 'get') + + def test_none(self, mock_get_ver): + mock_get_ver.return_value = None + + self.assertRaisesRegex(exceptions.NotSupported, + '1.6 is required, but the default version', + self.res._assert_microversion_for, + self.session, 'get', '1.6') + mock_get_ver.assert_called_once_with(self.res, self.session, 'get') From 94b2bf06de6d063b56f33a10e477271ff923dbf9 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 24 Jul 2018 15:00:14 -0500 Subject: [PATCH 2143/3836] Update create_object to handled chunked data We try to calculate len for data, which breaks with chunked uploads, but we only use that with filename. Move the code so that we don't try to calculate length of things where we don't need to. We still need to figure out streaming uploads and large objects. This changes several unit tests due to the re-ordering of when get_object_segment_size gets called. It's not necessary to call it for the data path, so it was moved to later, which puts it after the container create. For data paths this results in one less call. For file paths, the calls are the same but the /info call happens later. Change-Id: I841d7049ff2f05e00a31fecbc27dda6a0007be16 --- openstack/cloud/openstackcloud.py | 17 ++--- openstack/tests/unit/cloud/test_image.py | 14 ++-- openstack/tests/unit/cloud/test_object.py | 65 ++++++++----------- .../object-chunked-data-ee619b7d4759b8d2.yaml | 6 ++ 4 files changed, 47 insertions(+), 55 deletions(-) create mode 100644 releasenotes/notes/object-chunked-data-ee619b7d4759b8d2.yaml diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 516b69c6d..7a8e2c234 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -7514,16 +7514,6 @@ def create_object( if not filename and data is None: filename = name - # segment_size gets used as a step value in a range call, so needs - # to be an int - if segment_size: - segment_size = int(segment_size) - segment_size = self.get_object_segment_size(segment_size) - if filename: - file_size = os.path.getsize(filename) - else: - file_size = len(data) - if generate_checksums and (md5 is None or sha256 is None): (md5, sha256) = self._get_file_hashes(filename) if md5: @@ -7545,6 +7535,13 @@ def create_object( return self._upload_object_data(endpoint, data, headers) + # segment_size gets used as a step value in a range call, so needs + # to be an int + if segment_size: + segment_size = int(segment_size) + segment_size = self.get_object_segment_size(segment_size) + file_size = os.path.getsize(filename) + if self.is_object_stale(container, name, filename, md5, sha256): self.log.debug( diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 53ed287dc..73f6075e9 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -364,13 +364,6 @@ def test_create_image_task(self): uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2'), json={'images': []}), - dict(method='GET', - # This is explicitly not using get_mock_url because that - # gets us a project-id oriented URL. - uri='https://object-store.example.com/info', - json=dict( - swift={'max_file_size': 1000}, - slo={'min_segment_size': 500})), dict(method='HEAD', uri='{endpoint}/{container}'.format( endpoint=endpoint, container=self.container_name), @@ -395,6 +388,13 @@ def test_create_image_task(self): 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', 'X-Container-Bytes-Used': '0', 'Content-Type': 'text/plain; charset=utf-8'}), + dict(method='GET', + # This is explicitly not using get_mock_url because that + # gets us a project-id oriented URL. + uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': 1000}, + slo={'min_segment_size': 500})), dict(method='HEAD', uri='{endpoint}/{container}/{object}'.format( endpoint=endpoint, container=self.container_name, diff --git a/openstack/tests/unit/cloud/test_object.py b/openstack/tests/unit/cloud/test_object.py index 448a30f77..47f5f35e3 100644 --- a/openstack/tests/unit/cloud/test_object.py +++ b/openstack/tests/unit/cloud/test_object.py @@ -415,11 +415,6 @@ def setUp(self): def test_create_object(self): self.register_uris([ - dict(method='GET', - uri='https://object-store.example.com/info', - json=dict( - swift={'max_file_size': 1000}, - slo={'min_segment_size': 500})), dict(method='HEAD', uri='{endpoint}/{container}'.format( endpoint=self.endpoint, @@ -447,6 +442,11 @@ def test_create_object(self): 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', 'X-Container-Bytes-Used': '0', 'Content-Type': 'text/plain; charset=utf-8'}), + dict(method='GET', + uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': 1000}, + slo={'min_segment_size': 500})), dict(method='HEAD', uri='{endpoint}/{container}/{object}'.format( endpoint=self.endpoint, container=self.container, @@ -473,11 +473,6 @@ def test_create_object(self): def test_create_directory_marker_object(self): self.register_uris([ - dict(method='GET', - uri='https://object-store.example.com/info', - json=dict( - swift={'max_file_size': 1000}, - slo={'min_segment_size': 500})), dict(method='HEAD', uri='{endpoint}/{container}'.format( endpoint=self.endpoint, @@ -527,12 +522,6 @@ def test_create_dynamic_large_object(self): min_file_size = 1 uris_to_mock = [ - dict(method='GET', - uri='https://object-store.example.com/info', - json=dict( - swift={'max_file_size': max_file_size}, - slo={'min_segment_size': min_file_size})), - dict(method='HEAD', uri='{endpoint}/{container}'.format( endpoint=self.endpoint, @@ -562,6 +551,11 @@ def test_create_dynamic_large_object(self): 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', 'X-Container-Bytes-Used': '0', 'Content-Type': 'text/plain; charset=utf-8'}), + dict(method='GET', + uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': max_file_size}, + slo={'min_segment_size': min_file_size})), dict(method='HEAD', uri='{endpoint}/{container}/{object}'.format( endpoint=self.endpoint, container=self.container, @@ -613,10 +607,6 @@ def test_create_static_large_object(self): min_file_size = 1 uris_to_mock = [ - dict(method='GET', uri='https://object-store.example.com/info', - json=dict( - swift={'max_file_size': max_file_size}, - slo={'min_segment_size': min_file_size})), dict(method='HEAD', uri='{endpoint}/{container}'.format( endpoint=self.endpoint, @@ -646,6 +636,10 @@ def test_create_static_large_object(self): 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', 'X-Container-Bytes-Used': '0', 'Content-Type': 'text/plain; charset=utf-8'}), + dict(method='GET', uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': max_file_size}, + slo={'min_segment_size': min_file_size})), dict(method='HEAD', uri='{endpoint}/{container}/{object}'.format( endpoint=self.endpoint, @@ -732,10 +726,6 @@ def test_object_segment_retry_failure(self): min_file_size = 1 self.register_uris([ - dict(method='GET', uri='https://object-store.example.com/info', - json=dict( - swift={'max_file_size': max_file_size}, - slo={'min_segment_size': min_file_size})), dict(method='HEAD', uri='{endpoint}/{container}'.format( endpoint=self.endpoint, @@ -765,6 +755,10 @@ def test_object_segment_retry_failure(self): 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', 'X-Container-Bytes-Used': '0', 'Content-Type': 'text/plain; charset=utf-8'}), + dict(method='GET', uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': max_file_size}, + slo={'min_segment_size': min_file_size})), dict(method='HEAD', uri='{endpoint}/{container}/{object}'.format( endpoint=self.endpoint, @@ -816,10 +810,6 @@ def test_object_segment_retries(self): min_file_size = 1 self.register_uris([ - dict(method='GET', uri='https://object-store.example.com/info', - json=dict( - swift={'max_file_size': max_file_size}, - slo={'min_segment_size': min_file_size})), dict(method='HEAD', uri='{endpoint}/{container}'.format( endpoint=self.endpoint, @@ -849,6 +839,10 @@ def test_object_segment_retries(self): 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', 'X-Container-Bytes-Used': '0', 'Content-Type': 'text/plain; charset=utf-8'}), + dict(method='GET', uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': max_file_size}, + slo={'min_segment_size': min_file_size})), dict(method='HEAD', uri='{endpoint}/{container}/{object}'.format( endpoint=self.endpoint, @@ -950,11 +944,6 @@ def test_object_segment_retries(self): def test_create_object_skip_checksum(self): self.register_uris([ - dict(method='GET', - uri='https://object-store.example.com/info', - json=dict( - swift={'max_file_size': 1000}, - slo={'min_segment_size': 500})), dict(method='HEAD', uri='{endpoint}/{container}'.format( endpoint=self.endpoint, @@ -982,6 +971,11 @@ def test_create_object_skip_checksum(self): 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', 'X-Container-Bytes-Used': '0', 'Content-Type': 'text/plain; charset=utf-8'}), + dict(method='GET', + uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': 1000}, + slo={'min_segment_size': 500})), dict(method='HEAD', uri='{endpoint}/{container}/{object}'.format( endpoint=self.endpoint, container=self.container, @@ -1005,11 +999,6 @@ def test_create_object_skip_checksum(self): def test_create_object_data(self): self.register_uris([ - dict(method='GET', - uri='https://object-store.example.com/info', - json=dict( - swift={'max_file_size': 1000}, - slo={'min_segment_size': 500})), dict(method='HEAD', uri='{endpoint}/{container}'.format( endpoint=self.endpoint, diff --git a/releasenotes/notes/object-chunked-data-ee619b7d4759b8d2.yaml b/releasenotes/notes/object-chunked-data-ee619b7d4759b8d2.yaml new file mode 100644 index 000000000..99c317a33 --- /dev/null +++ b/releasenotes/notes/object-chunked-data-ee619b7d4759b8d2.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixed an issue where passing an iterator to the ``data`` parameter of + ``create_object`` for chunked uploads failed due to attempting to + calculate the length of the data. From de981a324a8cd967c946812895ccdc0a7e4d6145 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 26 Jul 2018 10:48:22 -0400 Subject: [PATCH 2144/3836] Add method for returning a raw response for an object Some use cases are complex and need access to the raw response object. Add a method, get_object_raw, which just returns the requests response object. python-requests needs at least 2.18 for Response to be a contextmanager. Change-Id: I03f31f33cbef93c30acf56de5e06acf0d721bc03 --- lower-constraints.txt | 2 +- openstack/cloud/openstackcloud.py | 95 ++++++++++++------- .../get-object-raw-e58284e59c81c8ef.yaml | 5 + 3 files changed, 67 insertions(+), 35 deletions(-) create mode 100644 releasenotes/notes/get-object-raw-e58284e59c81c8ef.yaml diff --git a/lower-constraints.txt b/lower-constraints.txt index d3253c46f..2842cdfd0 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -27,7 +27,7 @@ pbr==2.0.0 python-mimeparse==1.6.0 python-subunit==1.0.0 PyYAML==3.12 -requests==2.14.2 +requests==2.18.0 requests-mock==1.2.0 requestsexceptions==1.2.0 six==1.10.0 diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 7a8e2c234..e655dae74 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -7779,53 +7779,80 @@ def get_object_metadata(self, container, name): return None raise + def get_object_raw(self, container, obj, query_string=None, stream=False): + """Get a raw response object for an object. + + :param string container: name of the container. + :param string obj: name of the object. + :param string query_string: + query args for uri. (delimiter, prefix, etc.) + :param bool stream: + Whether to stream the response or not. + + :returns: A `requests.Response` + :raises: OpenStackCloudException on operation error. + """ + endpoint = self._get_object_endpoint(container, obj, query_string) + try: + return self._object_store_client.get(endpoint, stream=stream) + except exc.OpenStackCloudHTTPError as e: + if e.response.status_code == 404: + return None + raise + + def _get_object_endpoint(self, container, obj, query_string): + endpoint = '{container}/{object}'.format( + container=container, object=obj) + if query_string: + endpoint = '{endpoint}?{query_string}'.format( + endpoint=endpoint, query_string=query_string) + return endpoint + def get_object(self, container, obj, query_string=None, resp_chunk_size=1024, outfile=None): """Get the headers and body of an object :param string container: name of the container. :param string obj: name of the object. - :param string query_string: query args for uri. - (delimiter, prefix, etc.) - :param int resp_chunk_size: chunk size of data to read. Only used - if the results are being written to a - file. (optional, defaults to 1k) - :param outfile: Write the object to a file instead of - returning the contents. If this option is - given, body in the return tuple will be None. outfile - can either be a file path given as a string, or a - File like object. + :param string query_string: + query args for uri. (delimiter, prefix, etc.) + :param int resp_chunk_size: + chunk size of data to read. Only used if the results are + being written to a file or stream is True. + (optional, defaults to 1k) + :param outfile: + Write the object to a file instead of returning the contents. + If this option is given, body in the return tuple will be None. + outfile can either be a file path given as a string, or a + File like object. :returns: Tuple (headers, body) of the object, or None if the object - is not found (404) + is not found (404). :raises: OpenStackCloudException on operation error. """ # TODO(mordred) implement resp_chunk_size + endpoint = self._get_object_endpoint(container, obj, query_string) try: - endpoint = '{container}/{object}'.format( - container=container, object=obj) - if query_string: - endpoint = '{endpoint}?{query_string}'.format( - endpoint=endpoint, query_string=query_string) - response = self._object_store_client.get( - endpoint, stream=True) - response_headers = { - k.lower(): v for k, v in response.headers.items()} - if outfile: - if isinstance(outfile, six.string_types): - outfile_handle = open(outfile, 'wb') - else: - outfile_handle = outfile - for chunk in response.iter_content( - resp_chunk_size, decode_unicode=False): - outfile_handle.write(chunk) - if isinstance(outfile, six.string_types): - outfile_handle.close() + get_stream = (outfile is not None) + with self._object_store_client.get( + endpoint, stream=get_stream) as response: + response_headers = { + k.lower(): v for k, v in response.headers.items()} + if outfile: + if isinstance(outfile, six.string_types): + outfile_handle = open(outfile, 'wb') + else: + outfile_handle = outfile + for chunk in response.iter_content( + resp_chunk_size, decode_unicode=False): + outfile_handle.write(chunk) + if isinstance(outfile, six.string_types): + outfile_handle.close() + else: + outfile_handle.flush() + return (response_headers, None) else: - outfile_handle.flush() - return (response_headers, None) - else: - return (response_headers, response.text) + return (response_headers, response.text) except exc.OpenStackCloudHTTPError as e: if e.response.status_code == 404: return None diff --git a/releasenotes/notes/get-object-raw-e58284e59c81c8ef.yaml b/releasenotes/notes/get-object-raw-e58284e59c81c8ef.yaml new file mode 100644 index 000000000..d854d8ea4 --- /dev/null +++ b/releasenotes/notes/get-object-raw-e58284e59c81c8ef.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added ``get_object_raw`` method for downloading an object from swift + and returning a raw requests Response object. From 96eac6a018ebb7b9b7b909a927b1190c33367e58 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 26 Jul 2018 10:28:02 -0400 Subject: [PATCH 2145/3836] Add support for streaming object responses When getting objects, sometimes you want a stream rather than reading the whole value or passing to a file-like object. Add a stream_object method that returns an iterator over the content to be returned. Change-Id: I6f7ae89799e733657adc9989b3accfdf6e3f2f82 --- openstack/cloud/openstackcloud.py | 34 ++++++++++++---- openstack/tests/unit/cloud/test_object.py | 39 +++++++++++++++++++ .../notes/stream-object-6ecd43511dca726b.yaml | 4 ++ 3 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/stream-object-6ecd43511dca726b.yaml diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index e655dae74..51276244e 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -7793,12 +7793,7 @@ def get_object_raw(self, container, obj, query_string=None, stream=False): :raises: OpenStackCloudException on operation error. """ endpoint = self._get_object_endpoint(container, obj, query_string) - try: - return self._object_store_client.get(endpoint, stream=stream) - except exc.OpenStackCloudHTTPError as e: - if e.response.status_code == 404: - return None - raise + return self._object_store_client.get(endpoint, stream=stream) def _get_object_endpoint(self, container, obj, query_string): endpoint = '{container}/{object}'.format( @@ -7808,8 +7803,33 @@ def _get_object_endpoint(self, container, obj, query_string): endpoint=endpoint, query_string=query_string) return endpoint + def stream_object( + self, container, obj, query_string=None, resp_chunk_size=1024): + """Download the content via a streaming iterator. + + :param string container: name of the container. + :param string obj: name of the object. + :param string query_string: + query args for uri. (delimiter, prefix, etc.) + :param int resp_chunk_size: + chunk size of data to read. Only used if the results are + + :returns: + An iterator over the content or None if the object is not found. + :raises: OpenStackCloudException on operation error. + """ + try: + with self.get_object_raw( + container, obj, query_string=query_string) as response: + for ret in response.iter_content(chunk_size=resp_chunk_size): + yield ret + except exc.OpenStackCloudHTTPError as e: + if e.response.status_code == 404: + return + raise + def get_object(self, container, obj, query_string=None, - resp_chunk_size=1024, outfile=None): + resp_chunk_size=1024, outfile=None, stream=False): """Get the headers and body of an object :param string container: name of the container. diff --git a/openstack/tests/unit/cloud/test_object.py b/openstack/tests/unit/cloud/test_object.py index 47f5f35e3..d87f32724 100644 --- a/openstack/tests/unit/cloud/test_object.py +++ b/openstack/tests/unit/cloud/test_object.py @@ -347,6 +347,45 @@ def test_get_object(self): self.assertEqual((response_headers, text), resp) + def test_stream_object(self): + text = b'test body' + self.register_uris([ + dict(method='GET', uri=self.object_endpoint, + headers={ + 'Content-Length': '20304400896', + 'Content-Type': 'application/octet-stream', + 'Accept-Ranges': 'bytes', + 'Last-Modified': 'Thu, 15 Dec 2016 13:34:14 GMT', + 'Etag': '"b5c454b44fbd5344793e3fb7e3850768"', + 'X-Timestamp': '1481808853.65009', + 'X-Trans-Id': 'tx68c2a2278f0c469bb6de1-005857ed80dfw1', + 'Date': 'Mon, 19 Dec 2016 14:24:00 GMT', + 'X-Static-Large-Object': 'True', + 'X-Object-Meta-Mtime': '1481513709.168512', + }, + text='test body')]) + + response_text = b'' + for data in self.cloud.stream_object(self.container, self.object): + response_text += data + + self.assert_calls() + + self.assertEqual(text, response_text) + + def test_stream_object_not_found(self): + self.register_uris([ + dict(method='GET', uri=self.object_endpoint, status_code=404), + ]) + + response_text = b'' + for data in self.cloud.stream_object(self.container, self.object): + response_text += data + + self.assert_calls() + + self.assertEqual(b'', response_text) + def test_get_object_not_found(self): self.register_uris([dict(method='GET', uri=self.object_endpoint, status_code=404)]) diff --git a/releasenotes/notes/stream-object-6ecd43511dca726b.yaml b/releasenotes/notes/stream-object-6ecd43511dca726b.yaml new file mode 100644 index 000000000..9e102c8fc --- /dev/null +++ b/releasenotes/notes/stream-object-6ecd43511dca726b.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Added ``stream_object`` method for getting object content in an iterator. From 8ccece34fffad58d764a5bbffa9b1b8adcbb8b68 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 27 Jul 2018 12:32:22 -0400 Subject: [PATCH 2146/3836] Remove the auto-creation of containers in create_object The auto-creation of containers in create_object came from shade and seemed like a good idea at the time. However, containers are where things like storage policies and other metadata/config go, so it's actually important that a user create those themselves. Removing the autocreation means create_object no longer does a HEAD call to check to see if the container exists first. Change-Id: I259ae3b9c729de44f219a1ad2dbe0082e2967409 --- openstack/cloud/openstackcloud.py | 4 +- openstack/tests/unit/cloud/test_object.py | 240 +----------------- ...emove-auto-container-527f1807605b42c0.yaml | 6 + 3 files changed, 15 insertions(+), 235 deletions(-) create mode 100644 releasenotes/notes/remove-auto-container-527f1807605b42c0.yaml diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 51276244e..a0fe600ca 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -4802,6 +4802,7 @@ def _upload_image_task( parameters = image_kwargs.pop('parameters', {}) image_kwargs.update(parameters) + self.create_container(container) self.create_object( container, name, filename, md5=md5, sha256=sha256, @@ -7523,9 +7524,6 @@ def create_object( for (k, v) in metadata.items(): headers['x-object-meta-' + k] = v - # On some clouds this is not necessary. On others it is. I'm confused. - self.create_container(container) - endpoint = '{container}/{name}'.format(container=container, name=name) if data is not None: diff --git a/openstack/tests/unit/cloud/test_object.py b/openstack/tests/unit/cloud/test_object.py index d87f32724..762dd7412 100644 --- a/openstack/tests/unit/cloud/test_object.py +++ b/openstack/tests/unit/cloud/test_object.py @@ -454,33 +454,6 @@ def setUp(self): def test_create_object(self): self.register_uris([ - dict(method='HEAD', - uri='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container), - status_code=404), - dict(method='PUT', - uri='{endpoint}/{container}'.format( - endpoint=self.endpoint, container=self.container), - status_code=201, - headers={ - 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', - 'Content-Length': '0', - 'Content-Type': 'text/html; charset=UTF-8', - }), - dict(method='HEAD', - uri='{endpoint}/{container}'.format( - endpoint=self.endpoint, container=self.container), - headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}), dict(method='GET', uri='https://object-store.example.com/info', json=dict( @@ -512,33 +485,6 @@ def test_create_object(self): def test_create_directory_marker_object(self): self.register_uris([ - dict(method='HEAD', - uri='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container), - status_code=404), - dict(method='PUT', - uri='{endpoint}/{container}'.format( - endpoint=self.endpoint, container=self.container), - status_code=201, - headers={ - 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', - 'Content-Length': '0', - 'Content-Type': 'text/html; charset=UTF-8', - }), - dict(method='HEAD', - uri='{endpoint}/{container}'.format( - endpoint=self.endpoint, container=self.container), - headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}), dict(method='PUT', uri='{endpoint}/{container}/{object}'.format( endpoint=self.endpoint, @@ -561,35 +507,6 @@ def test_create_dynamic_large_object(self): min_file_size = 1 uris_to_mock = [ - dict(method='HEAD', - uri='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container), - status_code=404), - dict(method='PUT', - uri='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container, ), - status_code=201, - headers={ - 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', - 'Content-Length': '0', - 'Content-Type': 'text/html; charset=UTF-8', - }), - dict(method='HEAD', - uri='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container), - headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}), dict(method='GET', uri='https://object-store.example.com/info', json=dict( @@ -632,8 +549,8 @@ def test_create_dynamic_large_object(self): container=self.container, name=self.object, filename=self.object_file.name, use_slo=False) - # After call 6, order become indeterminate because of thread pool - self.assert_calls(stop_after=6) + # After call 3, order become indeterminate because of thread pool + self.assert_calls(stop_after=3) for key, value in self.calls[-1]['headers'].items(): self.assertEqual( @@ -646,35 +563,6 @@ def test_create_static_large_object(self): min_file_size = 1 uris_to_mock = [ - dict(method='HEAD', - uri='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container), - status_code=404), - dict(method='PUT', - uri='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container, ), - status_code=201, - headers={ - 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', - 'Content-Length': '0', - 'Content-Type': 'text/html; charset=UTF-8', - }), - dict(method='HEAD', - uri='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container), - headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}), dict(method='GET', uri='https://object-store.example.com/info', json=dict( swift={'max_file_size': max_file_size}, @@ -719,8 +607,8 @@ def test_create_static_large_object(self): container=self.container, name=self.object, filename=self.object_file.name, use_slo=True) - # After call 6, order become indeterminate because of thread pool - self.assert_calls(stop_after=6) + # After call 3, order become indeterminate because of thread pool + self.assert_calls(stop_after=3) for key, value in self.calls[-1]['headers'].items(): self.assertEqual( @@ -765,35 +653,6 @@ def test_object_segment_retry_failure(self): min_file_size = 1 self.register_uris([ - dict(method='HEAD', - uri='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container), - status_code=404), - dict(method='PUT', - uri='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container, ), - status_code=201, - headers={ - 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', - 'Content-Length': '0', - 'Content-Type': 'text/html; charset=UTF-8', - }), - dict(method='HEAD', - uri='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container), - headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}), dict(method='GET', uri='https://object-store.example.com/info', json=dict( swift={'max_file_size': max_file_size}, @@ -840,8 +699,8 @@ def test_object_segment_retry_failure(self): container=self.container, name=self.object, filename=self.object_file.name, use_slo=True) - # After call 6, order become indeterminate because of thread pool - self.assert_calls(stop_after=6) + # After call 3, order become indeterminate because of thread pool + self.assert_calls(stop_after=3) def test_object_segment_retries(self): @@ -849,35 +708,6 @@ def test_object_segment_retries(self): min_file_size = 1 self.register_uris([ - dict(method='HEAD', - uri='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container), - status_code=404), - dict(method='PUT', - uri='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container, ), - status_code=201, - headers={ - 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', - 'Content-Length': '0', - 'Content-Type': 'text/html; charset=UTF-8', - }), - dict(method='HEAD', - uri='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container), - headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}), dict(method='GET', uri='https://object-store.example.com/info', json=dict( swift={'max_file_size': max_file_size}, @@ -940,8 +770,8 @@ def test_object_segment_retries(self): container=self.container, name=self.object, filename=self.object_file.name, use_slo=True) - # After call 6, order become indeterminate because of thread pool - self.assert_calls(stop_after=6) + # After call 3, order become indeterminate because of thread pool + self.assert_calls(stop_after=3) for key, value in self.calls[-1]['headers'].items(): self.assertEqual( @@ -983,33 +813,6 @@ def test_object_segment_retries(self): def test_create_object_skip_checksum(self): self.register_uris([ - dict(method='HEAD', - uri='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container), - status_code=404), - dict(method='PUT', - uri='{endpoint}/{container}'.format( - endpoint=self.endpoint, container=self.container), - status_code=201, - headers={ - 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', - 'Content-Length': '0', - 'Content-Type': 'text/html; charset=UTF-8', - }), - dict(method='HEAD', - uri='{endpoint}/{container}'.format( - endpoint=self.endpoint, container=self.container), - headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}), dict(method='GET', uri='https://object-store.example.com/info', json=dict( @@ -1038,33 +841,6 @@ def test_create_object_skip_checksum(self): def test_create_object_data(self): self.register_uris([ - dict(method='HEAD', - uri='{endpoint}/{container}'.format( - endpoint=self.endpoint, - container=self.container), - status_code=404), - dict(method='PUT', - uri='{endpoint}/{container}'.format( - endpoint=self.endpoint, container=self.container), - status_code=201, - headers={ - 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', - 'Content-Length': '0', - 'Content-Type': 'text/html; charset=UTF-8', - }), - dict(method='HEAD', - uri='{endpoint}/{container}'.format( - endpoint=self.endpoint, container=self.container), - headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}), dict(method='PUT', uri='{endpoint}/{container}/{object}'.format( endpoint=self.endpoint, diff --git a/releasenotes/notes/remove-auto-container-527f1807605b42c0.yaml b/releasenotes/notes/remove-auto-container-527f1807605b42c0.yaml new file mode 100644 index 000000000..ef36ff6a3 --- /dev/null +++ b/releasenotes/notes/remove-auto-container-527f1807605b42c0.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + ``openstack.connection.Connection.create_object`` no longer creates + a container if one doesn't exist. It is the user's responsibility to + create a container before using it. From 1cd992c53f736e79c5c9e9687a6c87b8cc7431f4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 27 Jul 2018 12:53:13 -0400 Subject: [PATCH 2147/3836] Update config doc url to point to openstacksdk There is an error message which points people to the docs for config. Those are in openstacksdk now. Point people to the right place. Change-Id: I0bb7e4eb8682cf75641e4ccb677631da7ea03885 --- openstack/cloud/openstackcloud.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index a0fe600ca..92290f389 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -73,7 +73,9 @@ DEFAULT_SERVER_AGE = 5 DEFAULT_PORT_AGE = 5 DEFAULT_FLOAT_AGE = 5 -_OCC_DOC_URL = "https://docs.openstack.org/os-client-config/latest/" +_CONFIG_DOC_URL = ( + "https://docs.openstack.org/openstacksdk/latest/" + "user/config/configuration.html") OBJECT_CONTAINER_ACLS = { @@ -2275,7 +2277,7 @@ def _list_floating_ips(self, filters=None): " you will need a clouds.yaml file. For more" " information, please see %(doc_url)s", { 'cloud': self.name, - 'doc_url': _OCC_DOC_URL, + 'doc_url': _CONFIG_DOC_URL, } ) # We can't fallback to nova because we push-down filters. From f213de3a9c728af729e00c3e082d81a13ad8bfb9 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 27 Jul 2018 13:12:54 -0400 Subject: [PATCH 2148/3836] Handle image and object key metadata for shade transition We need to be able to override the object and image metadata keys in the shade subclass so that shade users get shade keys when they use these via SDK. We also need to check for the shade values when reading the old commands. Change-Id: I6f4cb7cd94ea8e9dcff8958452891dca0296e099 --- openstack/cloud/openstackcloud.py | 83 +++++++++++++++--------- openstack/tests/unit/cloud/test_image.py | 3 +- 2 files changed, 54 insertions(+), 32 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 92290f389..4b435fc46 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -52,19 +52,6 @@ from openstack import task_manager from openstack import utils -# TODO(shade) shade keys were x-object-meta-x-sdk-md5 - we need to add those -# to freshness checks so that a shade->sdk transition doens't -# result in a re-upload -OBJECT_MD5_KEY = 'x-object-meta-x-sdk-md5' -OBJECT_SHA256_KEY = 'x-object-meta-x-sdk-sha256' -OBJECT_AUTOCREATE_KEY = 'x-object-meta-x-sdk-autocreated' -OBJECT_AUTOCREATE_CONTAINER = 'images' -# TODO(shade) shade keys were owner_specified.shade.md5 - we need to add those -# to freshness checks so that a shade->sdk transition doens't -# result in a re-upload -IMAGE_MD5_KEY = 'owner_specified.openstack.md5' -IMAGE_SHA256_KEY = 'owner_specified.openstack.sha256' -IMAGE_OBJECT_KEY = 'owner_specified.openstack.object' # Rackspace returns this for intermittent import errors IMAGE_ERROR_396 = "Image cannot be imported. Error code: '396'" DEFAULT_OBJECT_SEGMENT_SIZE = 1073741824 # 1GB @@ -122,6 +109,25 @@ class OpenStackCloud(_normalize.Normalizer): :param bool strict: Only return documented attributes for each resource as per the Data Model contract. (Default False) """ + _OBJECT_MD5_KEY = 'x-object-meta-x-sdk-md5' + _OBJECT_SHA256_KEY = 'x-object-meta-x-sdk-sha256' + _OBJECT_AUTOCREATE_KEY = 'x-object-meta-x-sdk-autocreated' + _OBJECT_AUTOCREATE_CONTAINER = 'images' + _IMAGE_MD5_KEY = 'owner_specified.openstack.md5' + _IMAGE_SHA256_KEY = 'owner_specified.openstack.sha256' + _IMAGE_OBJECT_KEY = 'owner_specified.openstack.object' + # NOTE(shade) shade keys were x-object-meta-x-shade-md5 - we need to check + # those in freshness checks so that a shade->sdk transition + # doesn't result in a re-upload + _SHADE_OBJECT_MD5_KEY = 'x-object-meta-x-shade-md5' + _SHADE_OBJECT_SHA256_KEY = 'x-object-meta-x-shade-sha256' + _SHADE_OBJECT_AUTOCREATE_KEY = 'x-object-meta-x-shade-autocreated' + # NOTE(shade) shade keys were owner_specified.shade.md5 - we need to add + # those to freshness checks so that a shade->sdk transition + # doesn't result in a re-upload + _SHADE_IMAGE_MD5_KEY = 'owner_specified.shade.md5' + _SHADE_IMAGE_SHA256_KEY = 'owner_specified.shade.sha256' + _SHADE_IMAGE_OBJECT_KEY = 'owner_specified.shade.object' def __init__(self): @@ -4456,8 +4462,12 @@ def delete_image( self.list_images.invalidate(self) # Task API means an image was uploaded to swift - if self.image_api_use_tasks and IMAGE_OBJECT_KEY in image: - (container, objname) = image[IMAGE_OBJECT_KEY].split('/', 1) + if self.image_api_use_tasks and ( + self._IMAGE_OBJECT_KEY in image + or self._SHADE_IMAGE_OBJECT_KEY in image): + (container, objname) = image.get( + self._IMAGE_OBJECT_KEY, image.get( + self._SHADE_IMAGE_OBJECT_KEY)).split('/', 1) self.delete_object(container=container, name=objname) if wait: @@ -4505,7 +4515,8 @@ def _hashes_up_to_date(self, md5, sha256, md5_key, sha256_key): return up_to_date def create_image( - self, name, filename=None, container=OBJECT_AUTOCREATE_CONTAINER, + self, name, filename=None, + container=None, md5=None, sha256=None, disk_format=None, container_format=None, disable_vendor_agent=True, @@ -4565,6 +4576,8 @@ def create_image( :raises: OpenStackCloudException if there are problems uploading """ + if container is None: + container = self._OBJECT_AUTOCREATE_CONTAINER if not meta: meta = {} @@ -4600,8 +4613,12 @@ def create_image( else: current_image = self.get_image(name) if current_image: - md5_key = current_image.get(IMAGE_MD5_KEY, '') - sha256_key = current_image.get(IMAGE_SHA256_KEY, '') + md5_key = current_image.get( + self._IMAGE_MD5_KEY, + current_image.get(self._SHADE_IMAGE_MD5_KEY, '')) + sha256_key = current_image.get( + self._IMAGE_SHA256_KEY, + current_image.get(self._SHADE_IMAGE_SHA256_KEY, '')) up_to_date = self._hashes_up_to_date( md5=md5, sha256=sha256, md5_key=md5_key, sha256_key=sha256_key) @@ -4610,9 +4627,9 @@ def create_image( "image %(name)s exists and is up to date", {'name': name}) return current_image - kwargs[IMAGE_MD5_KEY] = md5 or '' - kwargs[IMAGE_SHA256_KEY] = sha256 or '' - kwargs[IMAGE_OBJECT_KEY] = '/'.join([container, name]) + kwargs[self._IMAGE_MD5_KEY] = md5 or '' + kwargs[self._IMAGE_SHA256_KEY] = sha256 or '' + kwargs[self._IMAGE_OBJECT_KEY] = '/'.join([container, name]) if disable_vendor_agent: kwargs.update(self.config.config['disable_vendor_agent']) @@ -4741,7 +4758,7 @@ def _upload_image_put_v1( image = self._get_and_munchify( 'image', self._image_client.post('/images', json=image_kwargs)) - checksum = image_kwargs['properties'].get(IMAGE_MD5_KEY, '') + checksum = image_kwargs['properties'].get(self._IMAGE_MD5_KEY, '') try: # Let us all take a brief moment to be grateful that this @@ -4808,7 +4825,7 @@ def _upload_image_task( self.create_object( container, name, filename, md5=md5, sha256=sha256, - metadata={OBJECT_AUTOCREATE_KEY: 'true'}, + metadata={self._OBJECT_AUTOCREATE_KEY: 'true'}, **{'content-type': 'application/octet-stream'}) if not current_image: current_image = self.get_image(name) @@ -7410,8 +7427,11 @@ def is_object_stale( if not (file_md5 or file_sha256): (file_md5, file_sha256) = self._get_file_hashes(filename) - md5_key = metadata.get(OBJECT_MD5_KEY, '') - sha256_key = metadata.get(OBJECT_SHA256_KEY, '') + md5_key = metadata.get( + self._OBJECT_MD5_KEY, metadata.get(self._SHADE_OBJECT_MD5_KEY, '')) + sha256_key = metadata.get( + self._OBJECT_SHA256_KEY, metadata.get( + self._SHADE_OBJECT_SHA256_KEY, '')) up_to_date = self._hashes_up_to_date( md5=file_md5, sha256=file_sha256, md5_key=md5_key, sha256_key=sha256_key) @@ -7520,9 +7540,9 @@ def create_object( if generate_checksums and (md5 is None or sha256 is None): (md5, sha256) = self._get_file_hashes(filename) if md5: - headers[OBJECT_MD5_KEY] = md5 or '' + headers[self._OBJECT_MD5_KEY] = md5 or '' if sha256: - headers[OBJECT_SHA256_KEY] = sha256 or '' + headers[self._OBJECT_SHA256_KEY] = sha256 or '' for (k, v) in metadata.items(): headers['x-object-meta-' + k] = v @@ -7747,8 +7767,7 @@ def delete_object(self, container, name, meta=None): except exc.OpenStackCloudHTTPError: return False - def delete_autocreated_image_objects( - self, container=OBJECT_AUTOCREATE_CONTAINER): + def delete_autocreated_image_objects(self, container=None): """Delete all objects autocreated for image uploads. This method should generally not be needed, as shade should clean up @@ -7757,6 +7776,8 @@ def delete_autocreated_image_objects( can be used to delete any objects that shade has created on the user's behalf in service of image uploads. """ + if container is None: + container = self._OBJECT_AUTOCREATE_CONTAINER # This method only makes sense on clouds that use tasks if not self.image_api_use_tasks: return False @@ -7764,7 +7785,9 @@ def delete_autocreated_image_objects( deleted = False for obj in self.list_objects(container): meta = self.get_object_metadata(container, obj['name']) - if meta.get(OBJECT_AUTOCREATE_KEY) == 'true': + if meta.get( + self._OBJECT_AUTOCREATE_KEY, meta.get( + self._SHADE_OBJECT_AUTOCREATE_KEY)) == 'true': if self.delete_object(container, obj['name'], meta): deleted = True return deleted diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 73f6075e9..f678f31cd 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -20,7 +20,6 @@ from openstack.cloud import exc from openstack.cloud import meta -from openstack.cloud import openstackcloud from openstack.tests import fakes from openstack.tests.unit import base @@ -553,7 +552,7 @@ def test_delete_autocreated_image_objects(self): 'Date': 'Thu, 16 Nov 2017 15:24:30 GMT', 'Accept-Ranges': 'bytes', 'Content-Type': 'application/octet-stream', - openstackcloud.OBJECT_AUTOCREATE_KEY: 'true', + self.cloud._OBJECT_AUTOCREATE_KEY: 'true', 'Etag': fakes.NO_MD5}), dict(method='DELETE', uri='{endpoint}/{container}/{object}'.format( From 4c49acd831e17a2eeeb2c77e1cff7648025a3490 Mon Sep 17 00:00:00 2001 From: Ryan Brady Date: Fri, 6 Jul 2018 09:42:46 -0400 Subject: [PATCH 2149/3836] Add support for static routes The networking API v2 specification, which is implemented by OpenStack Neutron, features an optional routes parameter - when updating a router PUT requests). Static routes are crucial for routers to handle traffic from subnets not directly connected to a router. This patch adds the routes parameter to the OpenStackCloud.update_router method as a list of dictionaries with destination and nexthop parameters. Example: [ { "destination": "179.24.1.0/24", "nexthop": "172.24.3.99" } ] Change-Id: I14205b6bb071d0b46967f29b6287f74d8796add8 --- openstack/cloud/openstackcloud.py | 18 ++++++++++++- .../tests/functional/cloud/test_router.py | 25 +++++++++++++++++++ ...etting-static-routes-b3ce6cac2c5e9e51.yaml | 9 +++++++ 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-support-for-setting-static-routes-b3ce6cac2c5e9e51.yaml diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 7a8e2c234..feb19354c 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -4261,7 +4261,7 @@ def create_router(self, name=None, admin_state_up=True, def update_router(self, name_or_id, name=None, admin_state_up=None, ext_gateway_net_id=None, enable_snat=None, - ext_fixed_ips=None): + ext_fixed_ips=None, routes=None): """Update an existing logical router. :param string name_or_id: The name or UUID of the router to update. @@ -4280,7 +4280,16 @@ def update_router(self, name_or_id, name=None, admin_state_up=None, "ip_address": "192.168.10.2" } ] + :param list routes: + A list of dictionaries with destination and nexthop parameters. + Example:: + [ + { + "destination": "179.24.1.0/24", + "nexthop": "172.24.3.99" + } + ] :returns: The router object. :raises: OpenStackCloudException on operation error. """ @@ -4295,6 +4304,13 @@ def update_router(self, name_or_id, name=None, admin_state_up=None, if ext_gw_info: router['external_gateway_info'] = ext_gw_info + if routes: + if self._has_neutron_extension('extraroute'): + router['routes'] = routes + else: + self.log.warn( + 'extra routes extension is not available on target cloud') + if not router: self.log.debug("No router data to update") return diff --git a/openstack/tests/functional/cloud/test_router.py b/openstack/tests/functional/cloud/test_router.py index c94277677..73940c11c 100644 --- a/openstack/tests/functional/cloud/test_router.py +++ b/openstack/tests/functional/cloud/test_router.py @@ -276,6 +276,31 @@ def test_update_router_name(self): self.assertEqual(router['external_gateway_info'], updated['external_gateway_info']) + def test_update_router_routes(self): + router = self._create_and_verify_advanced_router( + external_cidr=u'10.7.7.0/24') + + routes = [{ + "destination": "10.7.7.0/24", + "nexthop": "10.7.7.99" + }] + + updated = self.operator_cloud.update_router( + router['id'], routes=routes) + self.assertIsNotNone(updated) + + for field in EXPECTED_TOPLEVEL_FIELDS: + self.assertIn(field, updated) + + # Name is the only change we expect + self.assertEqual(routes, updated['routes']) + + # Validate nothing else changed + self.assertEqual(router['status'], updated['status']) + self.assertEqual(router['admin_state_up'], updated['admin_state_up']) + self.assertEqual(router['external_gateway_info'], + updated['external_gateway_info']) + def test_update_router_admin_state(self): router = self._create_and_verify_advanced_router( external_cidr=u'10.8.8.0/24') diff --git a/releasenotes/notes/add-support-for-setting-static-routes-b3ce6cac2c5e9e51.yaml b/releasenotes/notes/add-support-for-setting-static-routes-b3ce6cac2c5e9e51.yaml new file mode 100644 index 000000000..0b7c577dd --- /dev/null +++ b/releasenotes/notes/add-support-for-setting-static-routes-b3ce6cac2c5e9e51.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + The networking API v2 specification, which is implemented by OpenStack + Neutron, features an optional routes parameter - when updating a router + (PUT requests). Static routes are crucial for routers to handle traffic + from subnets not directly connected to a router. The routes parameter has + now been added to the OpenStackCloud.update_router method as a list of + dictionaries with destination and nexthop parameters. From 7fd4c3309162f34b7bf8aa2785372c944ec15965 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Fri, 27 Jul 2018 16:20:15 +0200 Subject: [PATCH 2150/3836] Add a simple baremetal functional job This creates a new job with a limites set of services, including ironic. Since many regular tests won't work with the baremetal backend or will take too much, only baremetal tests are run in it. Change-Id: Ia8af5ebc9aec46d405180f6894dd4228eb816b59 --- .zuul.yaml | 91 ++++++++++++++++--- .../tests/functional/baremetal/__init__.py | 0 .../baremetal/test_baremetal_node.py | 87 ++++++++++++++++++ tox.ini | 2 +- 4 files changed, 164 insertions(+), 16 deletions(-) create mode 100644 openstack/tests/functional/baremetal/__init__.py create mode 100644 openstack/tests/functional/baremetal/test_baremetal_node.py diff --git a/.zuul.yaml b/.zuul.yaml index 9a7853812..4324f33b6 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -40,10 +40,10 @@ - openstacksdk-tox-py35-tips - job: - name: openstacksdk-functional-devstack-base + name: openstacksdk-functional-devstack-minimum parent: devstack-tox-functional-consumer description: | - Base job for devstack-based functional tests + Minimum job for devstack-based functional tests post-run: playbooks/devstack/post.yaml required-projects: # These jobs will DTRT when openstacksdk triggers them, but we want to @@ -56,9 +56,32 @@ override-branch: master - name: openstack/os-client-config override-branch: master + timeout: 9000 + vars: + devstack_localrc: + Q_ML2_PLUGIN_EXT_DRIVERS: qos,port_security + tox_environment: + # Do we really need to set this? It's cargo culted + PYTHONUNBUFFERED: 'true' + # Is there a way we can query the localconf variable to get these + # rather than setting them explicitly? + OPENSTACKSDK_HAS_DESIGNATE: 0 + OPENSTACKSDK_HAS_HEAT: 0 + OPENSTACKSDK_HAS_MAGNUM: 0 + OPENSTACKSDK_HAS_NEUTRON: 1 + OPENSTACKSDK_HAS_SWIFT: 1 + tox_install_siblings: false + tox_envlist: functional + zuul_work_dir: src/git.openstack.org/openstack/openstacksdk + +- job: + name: openstacksdk-functional-devstack-base + parent: openstacksdk-functional-devstack-minimum + description: | + Base job for devstack-based functional tests + required-projects: - name: openstack/heat - name: openstack/swift - timeout: 9000 vars: devstack_local_conf: post-config: @@ -73,18 +96,7 @@ devstack_plugins: heat: https://git.openstack.org/openstack/heat tox_environment: - # Do we really need to set this? It's cargo culted - PYTHONUNBUFFERED: 'true' - # Is there a way we can query the localconf variable to get these - # rather than setting them explicitly? - OPENSTACKSDK_HAS_DESIGNATE: 0 OPENSTACKSDK_HAS_HEAT: 1 - OPENSTACKSDK_HAS_MAGNUM: 0 - OPENSTACKSDK_HAS_NEUTRON: 1 - OPENSTACKSDK_HAS_SWIFT: 1 - tox_install_siblings: false - tox_envlist: functional - zuul_work_dir: src/git.openstack.org/openstack/openstacksdk - job: name: openstacksdk-functional-devstack-legacy @@ -112,7 +124,6 @@ - openstack/octavia vars: devstack_localrc: - Q_ML2_PLUGIN_EXT_DRIVERS: qos,port_security DISABLE_AMP_IMAGE_BUILD: True devstack_local_conf: post-config: @@ -218,6 +229,54 @@ OPENSTACKSDK_HAS_SWIFT: 0 OPENSTACKSDK_HAS_SENLIN: 1 +- job: + name: openstacksdk-functional-devstack-ironic + parent: openstacksdk-functional-devstack-minimum + description: | + Run openstacksdk functional tests against a master devstack with ironic + required-projects: + - openstack/ironic + vars: + devstack_localrc: + OVERRIDE_PUBLIC_BRIDGE_MTU: 1400 + IRONIC_BAREMETAL_BASIC_OPS: True + IRONIC_BUILD_DEPLOY_RAMDISK: False + IRONIC_CALLBACK_TIMEOUT: 600 + IRONIC_DEPLOY_DRIVER: ipmi + IRONIC_RAMDISK_TYPE: tinyipa + IRONIC_VM_COUNT: 6 + IRONIC_VM_LOG_DIR: '{{ devstack_base_dir }}/ironic-bm-logs' + IRONIC_VM_SPECS_RAM: 384 + devstack_plugins: + ironic: git://git.openstack.org/openstack/ironic + devstack_services: + c-api: False + c-bak: False + c-sch: False + c-vol: False + cinder: False + s-account: False + s-container: False + s-object: False + s-proxy: False + n-api: False + n-api-meta: False + n-cauth: False + n-cond: False + n-cpu: False + n-novnc: False + n-obj: False + n-sch: False + nova: False + placement-api: False + tox_environment: + OPENSTACKSDK_HAS_IRONIC: 1 + # NOTE(dtantsur): this job cannot run many regular tests (e.g. compute + # tests will take too long), so limiting it to baremetal tests only. + OPENSTACKSDK_TESTS_SUBDIR: baremetal + zuul_copy_output: + '{{ devstack_base_dir }}/ironic-bm-logs': 'logs' + - job: name: openstacksdk-ansible-functional-devstack parent: openstacksdk-functional-devstack @@ -311,6 +370,8 @@ - openstacksdk-functional-devstack-senlin - openstacksdk-functional-devstack-magnum: voting: false + - openstacksdk-functional-devstack-ironic: + voting: false - openstacksdk-functional-devstack-python3 - osc-functional-devstack-tips: voting: false diff --git a/openstack/tests/functional/baremetal/__init__.py b/openstack/tests/functional/baremetal/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py new file mode 100644 index 000000000..103009086 --- /dev/null +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -0,0 +1,87 @@ +# 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.tests.functional import base + + +class TestBareMetalNode(base.BaseFunctionalTest): + + node_id = None + + def setUp(self): + super(TestBareMetalNode, self).setUp() + self.require_service('baremetal') + + def tearDown(self): + if self.node_id: + self.conn.baremetal.delete_node(self.node_id, ignore_missing=True) + super(TestBareMetalNode, self).tearDown() + + def test_node_create_get_delete(self): + node = self.conn.baremetal.create_node(driver='fake-hardware', + name='node-name') + self.node_id = node.id + self.assertIsNotNone(self.node_id) + + self.assertEqual(node.name, 'node-name') + self.assertEqual(node.driver, 'fake-hardware') + self.assertEqual(node.provision_state, 'available') + self.assertFalse(node.is_maintenance) + + # NOTE(dtantsur): get_node and find_node only differ in handing missing + # nodes, otherwise they are identical. + for call, ident in [(self.conn.baremetal.get_node, self.node_id), + (self.conn.baremetal.get_node, 'node-name'), + (self.conn.baremetal.find_node, self.node_id), + (self.conn.baremetal.find_node, 'node-name')]: + found = call(ident) + self.assertEqual(node.id, found.id) + self.assertEqual(node.name, found.name) + + nodes = self.conn.baremetal.nodes() + self.assertIn(node.id, [n.id for n in nodes]) + + self.conn.baremetal.delete_node(node, ignore_missing=False) + self.assertRaises(exceptions.NotFoundException, + self.conn.baremetal.get_node, self.node_id) + + def test_node_create_in_enroll_provide(self): + node = self.conn.baremetal.create_node(driver='fake-hardware', + provision_state='enroll') + self.node_id = node.id + + self.assertEqual(node.driver, 'fake-hardware') + self.assertEqual(node.provision_state, 'enroll') + self.assertIsNone(node.power_state) + self.assertFalse(node.is_maintenance) + + self.conn.baremetal.set_node_provision_state(node, 'manage', + wait=True) + self.assertEqual(node.provision_state, 'manageable') + + self.conn.baremetal.set_node_provision_state(node, 'provide', + wait=True) + self.assertEqual(node.provision_state, 'available') + + def test_node_negative_non_existing(self): + uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" + self.assertRaises(exceptions.NotFoundException, + self.conn.baremetal.get_node, uuid) + self.assertRaises(exceptions.NotFoundException, + self.conn.baremetal.find_node, uuid, + ignore_missing=False) + self.assertRaises(exceptions.NotFoundException, + self.conn.baremetal.delete_node, uuid, + ignore_missing=False) + self.assertIsNone(self.conn.baremetal.find_node(uuid)) + self.assertIsNone(self.conn.baremetal.delete_node(uuid)) diff --git a/tox.ini b/tox.ini index b70fd9563..af3f6ba9f 100644 --- a/tox.ini +++ b/tox.ini @@ -32,7 +32,7 @@ basepython = {env:OPENSTACKSDK_TOX_PYTHON:python2} setenv = {[testenv]setenv} OS_TEST_TIMEOUT=90 -commands = stestr --test-path ./openstack/tests/functional run --serial {posargs} +commands = stestr --test-path ./openstack/tests/functional/{env:OPENSTACKSDK_TESTS_SUBDIR:} run --serial {posargs} stestr slowest [testenv:pep8] From 8eb788af07ed88166db7b8a58ce1fecacd7a29b1 Mon Sep 17 00:00:00 2001 From: Rarm Nagalingam Date: Thu, 8 Mar 2018 10:39:27 +1100 Subject: [PATCH 2151/3836] Adds toggle port security on network create Added a new property, 'port_security_enabled' which is a boolean to enable or disable port_secuirty during network creation. The default behavior will enable port security, security group and anti spoofing will act as before. When the attribute is set to False, security group and anti spoofing are disabled on the ports created on this network. Change-Id: If984a82ca5f6fb69ee644f4fa84333df09d7f8bc --- openstack/cloud/openstackcloud.py | 10 ++++++++- .../tests/functional/cloud/test_network.py | 13 +++++++++++ openstack/tests/unit/cloud/test_network.py | 22 +++++++++++++++++++ ...toggle-port-security-f5bc606e82141feb.yaml | 9 ++++++++ 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/toggle-port-security-f5bc606e82141feb.yaml diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 7a8e2c234..4e8aa106d 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -3335,7 +3335,8 @@ def delete_keypair(self, name): def create_network(self, name, shared=False, admin_state_up=True, external=False, provider=None, project_id=None, - availability_zone_hints=None): + availability_zone_hints=None, + port_security_enabled=None): """Create a network. :param string name: Name of the network being created. @@ -3349,6 +3350,7 @@ def create_network(self, name, shared=False, admin_state_up=True, will be created on (admin-only). :param types.ListType availability_zone_hints: A list of availability zone hints. + :param bool port_security_enabled: Enable / Disable port security :returns: The network object. :raises: OpenStackCloudException on operation error. @@ -3391,6 +3393,12 @@ def create_network(self, name, shared=False, admin_state_up=True, if external: network['router:external'] = True + if port_security_enabled is not None: + if not isinstance(port_security_enabled, bool): + raise exc.OpenStackCloudException( + "Parameter 'port_security_enabled' must be a bool") + network['port_security_enabled'] = port_security_enabled + data = self._network_client.post("/networks.json", json={'network': network}) diff --git a/openstack/tests/functional/cloud/test_network.py b/openstack/tests/functional/cloud/test_network.py index 48395bd8d..a4911b9e4 100644 --- a/openstack/tests/functional/cloud/test_network.py +++ b/openstack/tests/functional/cloud/test_network.py @@ -49,6 +49,7 @@ def test_create_network_basic(self): self.assertFalse(net1['shared']) self.assertFalse(net1['router:external']) self.assertTrue(net1['admin_state_up']) + self.assertTrue(net1['port_security_enabled']) def test_get_network_by_id(self): net1 = self.operator_cloud.create_network(name=self.network_name) @@ -97,6 +98,18 @@ def test_create_network_provider_flat(self): self.assertEqual('public', net1['provider:physical_network']) self.assertIsNone(net1['provider:segmentation_id']) + def test_create_network_port_security_disabled(self): + net1 = self.operator_cloud.create_network( + name=self.network_name, + port_security_enabled=False, + ) + self.assertIn('id', net1) + self.assertEqual(self.network_name, net1['name']) + self.assertTrue(net1['admin_state_up']) + self.assertFalse(net1['shared']) + self.assertFalse(net1['router:external']) + self.assertFalse(net1['port_security_enabled']) + def test_list_networks_filtered(self): net1 = self.operator_cloud.create_network(name=self.network_name) self.assertIsNotNone(net1) diff --git a/openstack/tests/unit/cloud/test_network.py b/openstack/tests/unit/cloud/test_network.py index 6d061d49c..5075b87a7 100644 --- a/openstack/tests/unit/cloud/test_network.py +++ b/openstack/tests/unit/cloud/test_network.py @@ -229,6 +229,28 @@ def test_create_network_provider_wrong_type(self): ): self.cloud.create_network("netname", provider=provider_opts) + def test_create_network_port_security_disabled(self): + port_security_state = False + mock_new_network_rep = copy.copy(self.mock_new_network_rep) + mock_new_network_rep['port_security_enabled'] = port_security_state + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'network': mock_new_network_rep}, + validate=dict( + json={'network': { + 'admin_state_up': True, + 'name': 'netname', + 'port_security_enabled': port_security_state}})) + ]) + network = self.cloud.create_network( + "netname", + port_security_enabled=port_security_state + ) + self.assertEqual(mock_new_network_rep, network) + self.assert_calls() + def test_delete_network(self): network_id = "test-net-id" network_name = "network" diff --git a/releasenotes/notes/toggle-port-security-f5bc606e82141feb.yaml b/releasenotes/notes/toggle-port-security-f5bc606e82141feb.yaml new file mode 100644 index 000000000..821a20fb6 --- /dev/null +++ b/releasenotes/notes/toggle-port-security-f5bc606e82141feb.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Added a new property, 'port_security_enabled' which is a boolean + to enable or disable port_secuirty during network creation. The + default behavior will enable port security, security group and + anti spoofing will act as before. When the attribute is set to + False, security group and anti spoofing are disabled on the ports + created on this network. From a1fc820a2f3669f64d222506c17be99c4d6be7b7 Mon Sep 17 00:00:00 2001 From: Toure Dunnon Date: Thu, 28 Jun 2018 11:51:03 -0400 Subject: [PATCH 2152/3836] python-shade expose MTU setting. The networking API v2 specification, which is implemented by openstack neutron, features an optional MTU parameter - when creating a network, this allows operators to specify the value for the maximum transmission unit value. Change-Id: I288f02551555fff3e8b350fc6d7c6ae8f60c405c --- openstack/cloud/openstackcloud.py | 20 +++++++++-- openstack/tests/unit/cloud/test_network.py | 35 +++++++++++++++++++ .../notes/mtu-settings-8ce8b54d096580a2.yaml | 6 ++++ 3 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/mtu-settings-8ce8b54d096580a2.yaml diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 4e8aa106d..1ad7375b1 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -3336,7 +3336,8 @@ def delete_keypair(self, name): def create_network(self, name, shared=False, admin_state_up=True, external=False, provider=None, project_id=None, availability_zone_hints=None, - port_security_enabled=None): + port_security_enabled=None, + mtu_size=None): """Create a network. :param string name: Name of the network being created. @@ -3351,6 +3352,8 @@ def create_network(self, name, shared=False, admin_state_up=True, :param types.ListType availability_zone_hints: A list of availability zone hints. :param bool port_security_enabled: Enable / Disable port security + :param int mtu_size: maximum transmission unit value to address + fragmentation. Minimum value is 68 for IPv4, and 1280 for IPv6. :returns: The network object. :raises: OpenStackCloudException on operation error. @@ -3399,6 +3402,16 @@ def create_network(self, name, shared=False, admin_state_up=True, "Parameter 'port_security_enabled' must be a bool") network['port_security_enabled'] = port_security_enabled + if mtu_size: + if not isinstance(mtu_size, int): + raise exc.OpenStackCloudException( + "Parameter 'mtu_size' must be an integer.") + if not mtu_size >= 68: + raise exc.OpenStackCloudException( + "Parameter 'mtu_size' must be greater than 67.") + + network['mtu'] = mtu_size + data = self._network_client.post("/networks.json", json={'network': network}) @@ -11451,7 +11464,7 @@ def add_volume_type_access(self, name_or_id, project_id): json=dict(addProjectAccess=payload), error_message="Unable to authorize {project} " "to use volume type {name}".format( - name=name_or_id, project=project_id)) + name=name_or_id, project=project_id)) def remove_volume_type_access(self, name_or_id, project_id): """Revoke access on a volume_type to a project. @@ -11472,7 +11485,7 @@ def remove_volume_type_access(self, name_or_id, project_id): json=dict(removeProjectAccess=payload), error_message="Unable to revoke {project} " "to use volume type {name}".format( - name=name_or_id, project=project_id)) + name=name_or_id, project=project_id)) def set_compute_quotas(self, name_or_id, **kwargs): """ Set a quota in a project @@ -11548,6 +11561,7 @@ def get_compute_usage(self, name_or_id, start=None, end=None): :returns: Munch object with the usage """ + def parse_date(date): try: return iso8601.parse_date(date) diff --git a/openstack/tests/unit/cloud/test_network.py b/openstack/tests/unit/cloud/test_network.py index 5075b87a7..ca0b6073c 100644 --- a/openstack/tests/unit/cloud/test_network.py +++ b/openstack/tests/unit/cloud/test_network.py @@ -251,6 +251,41 @@ def test_create_network_port_security_disabled(self): self.assertEqual(mock_new_network_rep, network) self.assert_calls() + def test_create_network_with_mtu(self): + mtu_size = 1500 + mock_new_network_rep = copy.copy(self.mock_new_network_rep) + mock_new_network_rep['mtu'] = mtu_size + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'network': mock_new_network_rep}, + validate=dict( + json={'network': { + 'admin_state_up': True, + 'name': 'netname', + 'mtu': mtu_size}})) + ]) + network = self.cloud.create_network("netname", + mtu_size=mtu_size + ) + self.assertEqual(mock_new_network_rep, network) + self.assert_calls() + + def test_create_network_with_wrong_mtu_size(self): + with testtools.ExpectedException( + openstack.cloud.OpenStackCloudException, + "Parameter 'mtu_size' must be greater than 67." + ): + self.cloud.create_network("netname", mtu_size=42) + + def test_create_network_with_wrong_mtu_type(self): + with testtools.ExpectedException( + openstack.cloud.OpenStackCloudException, + "Parameter 'mtu_size' must be an integer." + ): + self.cloud.create_network("netname", mtu_size="fourty_two") + def test_delete_network(self): network_id = "test-net-id" network_name = "network" diff --git a/releasenotes/notes/mtu-settings-8ce8b54d096580a2.yaml b/releasenotes/notes/mtu-settings-8ce8b54d096580a2.yaml new file mode 100644 index 000000000..4de74d662 --- /dev/null +++ b/releasenotes/notes/mtu-settings-8ce8b54d096580a2.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + create_network now exposes the mtu api option in accordance to network + v2 api. This allows the operator to adjust the given MTU value which + is needed in various complex network deployments. From 6464422a2b911499556d4b82b44532f6d569857b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 31 Jul 2018 08:34:29 -0500 Subject: [PATCH 2153/3836] Pass microversion info through from Profile Even though Profile is going away, senlinclient is still using it. There is a patch up to fix that, but in the meantime, let's fix it for their issue. api_version in Profile is the variable for setting a default microversion, but the translation method didn't set it. Story: 2003146 Change-Id: I6efd25959127bbe28e76b1967caa5b3716346c7e --- openstack/profile.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openstack/profile.py b/openstack/profile.py index 4c4396c98..26d3b5661 100644 --- a/openstack/profile.py +++ b/openstack/profile.py @@ -58,6 +58,10 @@ def _get_config_from_profile(profile, authenticator, **kwargs): version = version[1:] key = cloud_region._make_key('api_version', service_type) kwargs[key] = version + if service.api_version: + version = service.api_version + key = cloud_region._make_key('default_microversion', service_type) + kwargs[key] = version config_kwargs = config_defaults.get_defaults() config_kwargs.update(kwargs) From 32801989baa34d91646ae3cb0694e48f560a7406 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Tue, 31 Jul 2018 17:16:40 -0400 Subject: [PATCH 2154/3836] fix 2 typos in documentation Change-Id: I242a674a085c2e24ef480258f7868a26b92443ea Signed-off-by: Doug Hellmann --- openstack/cloud/openstackcloud.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 605d4150c..902189b60 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -616,14 +616,14 @@ def _volume_client(self): return self._raw_clients['block-storage'] def pprint(self, resource): - """Wrapper aroud pprint that groks munch objects""" + """Wrapper around pprint that groks munch objects""" # import late since this is a utility function import pprint new_resource = _utils._dictify_resource(resource) pprint.pprint(new_resource) def pformat(self, resource): - """Wrapper aroud pformat that groks munch objects""" + """Wrapper around pformat that groks munch objects""" # import late since this is a utility function import pprint new_resource = _utils._dictify_resource(resource) From 5b8ef645023ba3f60d843350224cc2836c58d416 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 3 Aug 2018 10:16:21 -0500 Subject: [PATCH 2155/3836] Update storyboard links to use name storyboard now supports project name in the urls. Switch to using that form, since it just looks nicer. Change-Id: Ibb4185446392af68ddfcc9de4f4f76b44db676c5 --- CONTRIBUTING.rst | 2 +- README.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 9298e22db..873294e88 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -33,7 +33,7 @@ Project Documentation https://docs.openstack.org/openstacksdk/latest/ Bug tracker - https://storyboard.openstack.org/#!/project/972 + https://storyboard.openstack.org/#!/project/openstack/openstacksdk Mailing list (prefix subjects with ``[sdk]`` for faster responses) http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-dev diff --git a/README.rst b/README.rst index 2b26b1eee..ea5ada641 100644 --- a/README.rst +++ b/README.rst @@ -155,7 +155,7 @@ Create a server using objects configured with the ``clouds.yaml`` file: Links ===== -* `Issue Tracker `_ +* `Issue Tracker `_ * `Code Review `_ * `Documentation `_ * `PyPI `_ From 75eaaab0b8afcdc60e0aeedcd8ac6fd5798456be Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Fri, 3 Aug 2018 13:50:55 -0700 Subject: [PATCH 2156/3836] Don't wait for task in submit_task The exception shifting performed by the Task class is meant to protect the TaskManager run method from encountering any exceptions done in the course of running a task. This is especially important for subclasses of TaskManager which implement alternate run strategies. The exception is shifted so that it is raised by the Task.wait method. In a multi-thread environment, it is the caller of the submit_task method which should receive the exception, and therefore it is submit_task which should call Task.wait. Currently it is the _run_task method which calls Task.wait, which means the TaskManager itself receives the exception. Change-Id: I3a6e61601164811fdd255ae54470011768c99a7d --- openstack/task_manager.py | 28 ++++++- .../tests/unit/cloud/test_task_manager.py | 82 +++++++++++++++++++ 2 files changed, 108 insertions(+), 2 deletions(-) diff --git a/openstack/task_manager.py b/openstack/task_manager.py index 0b05e7107..0f47da0bc 100644 --- a/openstack/task_manager.py +++ b/openstack/task_manager.py @@ -121,8 +121,21 @@ def submit_task(self, task): :param task: The task to execute. :param bool raw: If True, return the raw result as received from the underlying client call. + + This method calls task.wait() so that it only returns when the + task is complete. """ - return self.run_task(task=task) + if task.run_async: + # Async tasks run the wait lower in the stack because the wait + # is just returning the concurrent Future object. That future + # object handles the exception shifting across threads. + return self.run_task(task=task) + else: + # It's important that we call task.wait() here, rather than in + # the run_task call stack below here, since subclasses may + # cause run_task to be called from a different thread. + self.run_task(task=task) + return task.wait() def submit_function(self, method, name=None, *args, **kwargs): """ Allows submitting an arbitrary method for work. @@ -164,9 +177,14 @@ def post_run_task(self, elapsed_time, task): def _run_task_async(self, task): self._log.debug( "Manager %s submitting task %s", self.name, task.name) - return self.executor.submit(self._run_task, task) + return self.executor.submit(self._run_task_wait, task) def _run_task(self, task): + # Never call task.wait() in the run_task call stack because we + # might be running in another thread. The exception-shifting + # code is designed so that caller of submit_task (which may be + # in a different thread than this run_task) gets the + # exception. self.pre_run_task(task) start = time.time() task.run() @@ -174,6 +192,12 @@ def _run_task(self, task): dt = end - start self.post_run_task(dt, task) + def _run_task_wait(self, task): + # For async tasks, the action being performed is getting a + # future back from concurrent.futures.ThreadPoolExecutor. + # We do need to run the wait because the Future object is + # handling the exception shifting for us. + self._run_task(task) return task.wait() diff --git a/openstack/tests/unit/cloud/test_task_manager.py b/openstack/tests/unit/cloud/test_task_manager.py index a4c7d2694..6403d947b 100644 --- a/openstack/tests/unit/cloud/test_task_manager.py +++ b/openstack/tests/unit/cloud/test_task_manager.py @@ -14,7 +14,10 @@ import concurrent.futures +import fixtures import mock +import queue +import threading from openstack import task_manager from openstack.tests.unit import base @@ -106,3 +109,82 @@ def test_dont_munchify_set(self): def test_async(self, mock_submit): self.manager.submit_task(TaskTestAsync()) self.assertTrue(mock_submit.called) + + +class ThreadingTaskManager(task_manager.TaskManager): + """A subclass of TaskManager which exercises the thread-shifting + exception handling behavior.""" + + def __init__(self, *args, **kw): + super(ThreadingTaskManager, self).__init__( + *args, **kw) + self.queue = queue.Queue() + self._running = True + self._thread = threading.Thread(name=self.name, target=self.run) + self._thread.daemon = True + self.failed = False + + def start(self): + self._thread.start() + + def stop(self): + self._running = False + self.queue.put(None) + + def join(self): + self._thread.join() + + def run(self): + # No exception should ever cause this method to hit its + # exception handler. + try: + while True: + task = self.queue.get() + if not task: + if not self._running: + break + continue + self.run_task(task) + self.queue.task_done() + except Exception: + self.failed = True + raise + + def submit_task(self, task, raw=False): + # An important part of the exception-shifting feature is that + # this method should raise the exception. + self.queue.put(task) + return task.wait() + + +class ThreadingTaskManagerFixture(fixtures.Fixture): + def _setUp(self): + self.manager = ThreadingTaskManager(name='threading test') + self.manager.start() + self.addCleanup(self._cleanup) + + def _cleanup(self): + self.manager.stop() + self.manager.join() + + +class TestThreadingTaskManager(base.TestCase): + + def setUp(self): + super(TestThreadingTaskManager, self).setUp() + f = self.useFixture(ThreadingTaskManagerFixture()) + self.manager = f.manager + + def test_wait_re_raise(self): + """Test that Exceptions thrown in a Task is reraised correctly + + This test is aimed to six.reraise(), called in Task::wait(). + Specifically, we test if we get the same behaviour with all the + configured interpreters (e.g. py27, p35, ...) + """ + self.assertRaises(TestException, self.manager.submit_task, TaskTest()) + # Stop the manager and join the run thread to ensure the + # exception handler has run. + self.manager.stop() + self.manager.join() + self.assertFalse(self.manager.failed) From 23ace6b1b958ae0b3de691593d8a178ba7e2a8b1 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 6 Aug 2018 11:05:00 -0500 Subject: [PATCH 2157/3836] Stop calling get_all_types when service-type is None If service_type=None makes it all the way here, there is really no need to call get_all_types. Story: 2003314 Change-Id: I04a4857a4ba131cc9aa05831ddc0c2d3d67903fd --- openstack/config/cloud_region.py | 3 +++ openstack/tests/unit/config/test_cloud_config.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index d32cac4f5..6ef1369d8 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -229,6 +229,9 @@ def _get_config( If none of that works, it returns the value in ``default``. ''' + if service_type is None: + return self.config.get(key) + for st in self._service_type_manager.get_all_types(service_type): value = self.config.get(_make_key(key, st)) if value is not None: diff --git a/openstack/tests/unit/config/test_cloud_config.py b/openstack/tests/unit/config/test_cloud_config.py index fb2f54eb1..df21ef2e5 100644 --- a/openstack/tests/unit/config/test_cloud_config.py +++ b/openstack/tests/unit/config/test_cloud_config.py @@ -85,6 +85,20 @@ def test_inequality(self): cc2 = cloud_region.CloudRegion("test1", "region-al", {}) self.assertNotEqual(cc1, cc2) + def test_get_config(self): + cc = cloud_region.CloudRegion("test1", "region-al", fake_services_dict) + self.assertIsNone(cc._get_config('nothing', None)) + # This is what is happening behind the scenes in get_default_interface. + self.assertEqual( + fake_services_dict['interface'], + cc._get_config('interface', None)) + # The same call as above, but from one step up the stack + self.assertEqual( + fake_services_dict['interface'], + cc.get_interface()) + # Which finally is what is called to populate the below + self.assertEqual('public', self.cloud.default_interface) + def test_verify(self): config_dict = copy.deepcopy(fake_config_dict) config_dict['cacert'] = None From 17c7a295512571c1c5aaa5dedf87e23d6f51f854 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 6 Aug 2018 13:16:53 -0500 Subject: [PATCH 2158/3836] Fix to_dict recursion issues with circular aliases Resources that have two different attributes that are aliases of each other cause to_dict to go into infinite recursion. Because of how the attributes and descriptors work, set the first one to None if it's not found, so that when the second one is tried it's either returned or when it goes to try its own alias (the first value) it'll hit the None and exit the recursion. Change-Id: I5aff07311967c0150d74d1f834516e8e92d8a73b --- openstack/resource.py | 22 ++++++++++++++++++- .../unit/object_store/v1/test_container.py | 11 ++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/openstack/resource.py b/openstack/resource.py index efa04e4a6..0cea03e57 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -44,6 +44,8 @@ class that represent a remote resource. The attributes that from openstack import format from openstack import utils +_SEEN_FORMAT = '{name}_seen' + def _convert_type(value, data_type, list_type=None): # This should allow handling list of dicts that have their own @@ -120,7 +122,25 @@ def __get__(self, instance, owner): value = attributes[self.name] except KeyError: if self.alias: - return getattr(instance, self.alias) + # Resource attributes can be aliased to each other. If neither + # of them exist, then simply doing a + # getattr(instance, self.alias) here sends things into + # infinite recursion (this _get method is what gets called + # when getattr(instance) is called. + # To combat that, we set a flag on the instance saying that + # we have seen the current name, and we check before trying + # to resolve the alias if there is already a flag set for that + # alias name. We then remove the seen flag for ourselves after + # we exit the alias getattr to clean up after ourselves for + # the next time. + alias_flag = _SEEN_FORMAT.format(name=self.alias) + if not getattr(instance, alias_flag, False): + seen_flag = _SEEN_FORMAT.format(name=self.name) + # Prevent infinite recursion + setattr(instance, seen_flag, True) + value = getattr(instance, self.alias) + delattr(instance, seen_flag) + return value return self.default # self.type() should not be called on None objects. diff --git a/openstack/tests/unit/object_store/v1/test_container.py b/openstack/tests/unit/object_store/v1/test_container.py index dd0216a2b..84fa993ce 100644 --- a/openstack/tests/unit/object_store/v1/test_container.py +++ b/openstack/tests/unit/object_store/v1/test_container.py @@ -154,6 +154,17 @@ def test_update(self): sot = container.Container.new(name=self.container) self._test_create_update(sot, sot.update, 'POST') + def test_to_dict_recursion(self): + # This test is verifying that circular aliases in a Resource + # do not cause infinite recursion. count is aliased to object_count + # and object_count is aliased to count. + sot = container.Container.new(name=self.container) + sot_dict = sot.to_dict() + self.assertIsNone(sot_dict['count']) + self.assertIsNone(sot_dict['object_count']) + self.assertEqual(sot_dict['id'], self.container) + self.assertEqual(sot_dict['name'], self.container) + def _test_no_headers(self, sot, sot_call, sess_method): headers = {} data = {} From 8c2eac3ccf7f3b22b42d6edefc329a9db37d4c22 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 7 Aug 2018 18:53:45 -0500 Subject: [PATCH 2159/3836] Stop using the -consumer devstack jobs The jobs came from shade, which didn't have the ability to break devstack builds. However, openstacksdk is involved in devstack, so running devstack in pre rather than in run is actually counter productive. Switch to using the non-consumer version of the base job. Change-Id: Ifddc61c4dbb1a7297a6fe6dc5f3f45c51db6548d --- .zuul.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index 4324f33b6..cefd3ab40 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -41,7 +41,7 @@ - job: name: openstacksdk-functional-devstack-minimum - parent: devstack-tox-functional-consumer + parent: devstack-tox-functional description: | Minimum job for devstack-based functional tests post-run: playbooks/devstack/post.yaml From 7590beafa053cfe6fdac26b0f9f1b50c193fa0c0 Mon Sep 17 00:00:00 2001 From: Rui Chen Date: Thu, 9 Aug 2018 10:53:59 +0800 Subject: [PATCH 2160/3836] Add more options to enable ansible testing feature 1. Specified volume size for boot from volume server scenario. 2. Create port in default security group or no security group. Change-Id: If2ccf726f518a5d6ba72bece09504c0f005a08f1 Related-Patch: https://review.openstack.org/#/c/582459/ --- openstack/tests/ansible/roles/port/defaults/main.yml | 1 + openstack/tests/ansible/roles/port/tasks/main.yml | 8 ++++---- openstack/tests/ansible/roles/server/defaults/main.yaml | 1 + openstack/tests/ansible/roles/server/tasks/main.yml | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/openstack/tests/ansible/roles/port/defaults/main.yml b/openstack/tests/ansible/roles/port/defaults/main.yml index 212afe346..de022001b 100644 --- a/openstack/tests/ansible/roles/port/defaults/main.yml +++ b/openstack/tests/ansible/roles/port/defaults/main.yml @@ -3,3 +3,4 @@ network_external: true subnet_name: ansible_port_subnet port_name: ansible_port secgroup_name: ansible_port_secgroup +no_security_groups: True diff --git a/openstack/tests/ansible/roles/port/tasks/main.yml b/openstack/tests/ansible/roles/port/tasks/main.yml index 5011d97ba..1a39140e5 100644 --- a/openstack/tests/ansible/roles/port/tasks/main.yml +++ b/openstack/tests/ansible/roles/port/tasks/main.yml @@ -14,20 +14,20 @@ network_name: "{{ network_name }}" cidr: 10.5.5.0/24 -- name: Create port (no security group) +- name: Create port (no security group or default security group) os_port: cloud: "{{ cloud }}" state: present name: "{{ port_name }}" network: "{{ network_name }}" - no_security_groups: True + no_security_groups: "{{ no_security_groups }}" fixed_ips: - ip_address: 10.5.5.69 register: port - debug: var=port -- name: Delete port (no security group) +- name: Delete port (no security group or default security group) os_port: cloud: "{{ cloud }}" state: absent @@ -66,7 +66,7 @@ state: present name: "{{ port_name }}" network: "{{ network_name }}" - no_security_groups: True + no_security_groups: "{{ no_security_groups }}" allowed_address_pairs: - ip_address: 10.6.7.0/24 extra_dhcp_opts: diff --git a/openstack/tests/ansible/roles/server/defaults/main.yaml b/openstack/tests/ansible/roles/server/defaults/main.yaml index 7d7ec01dc..e3bd5f33b 100644 --- a/openstack/tests/ansible/roles/server/defaults/main.yaml +++ b/openstack/tests/ansible/roles/server/defaults/main.yaml @@ -2,3 +2,4 @@ server_network: private server_name: ansible_server flavor: m1.tiny floating_ip_pool_name: public +boot_volume_size: 5 diff --git a/openstack/tests/ansible/roles/server/tasks/main.yml b/openstack/tests/ansible/roles/server/tasks/main.yml index f2ff6f639..ac0311554 100644 --- a/openstack/tests/ansible/roles/server/tasks/main.yml +++ b/openstack/tests/ansible/roles/server/tasks/main.yml @@ -77,7 +77,7 @@ network: "{{ server_network }}" auto_floating_ip: false boot_from_volume: true - volume_size: 5 + volume_size: "{{ boot_volume_size }}" terminate_volume: true wait: true register: server From 2d472aea49cf1ebac7e431c7754d9fccf77728b4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 5 Aug 2018 10:30:25 -0500 Subject: [PATCH 2161/3836] Rename Resource get and update to not clash with dict The next change is going to make Resource a subclass of dict so that we can return Resource objects in the shade layer. Unfortunately, the Resource base class has two methods, get and update, that conflict with standard dict method names. To keep things reviewable, just change the method names. Most consumers should not be using either of these methods directly. They are mostly there for lower-level things to use. However, they COULD be using them, so it's important to note that this is a breaking change. Change-Id: I2fedeea6e405dcbd333482c1964173ade98ca04d --- .../create/examples/resource/fake.py | 4 +- doc/source/contributor/create/resource.rst | 10 +-- doc/source/contributor/layout.rst | 2 +- openstack/_meta/_proxy_templates.py | 22 ++--- openstack/_meta/proxy.py | 9 +- openstack/baremetal/v1/chassis.py | 10 +-- openstack/baremetal/v1/driver.py | 4 +- openstack/baremetal/v1/node.py | 14 ++-- openstack/baremetal/v1/port.py | 10 +-- openstack/baremetal/v1/port_group.py | 10 +-- openstack/block_storage/v2/snapshot.py | 4 +- openstack/block_storage/v2/stats.py | 2 +- openstack/block_storage/v2/type.py | 2 +- openstack/block_storage/v2/volume.py | 4 +- openstack/clustering/v1/action.py | 2 +- openstack/clustering/v1/build_info.py | 2 +- openstack/clustering/v1/cluster.py | 6 +- openstack/clustering/v1/cluster_policy.py | 2 +- openstack/clustering/v1/event.py | 2 +- openstack/clustering/v1/node.py | 10 +-- openstack/clustering/v1/policy.py | 12 +-- openstack/clustering/v1/policy_type.py | 2 +- openstack/clustering/v1/profile.py | 12 +-- openstack/clustering/v1/profile_type.py | 2 +- openstack/clustering/v1/receiver.py | 6 +- openstack/compute/v2/extension.py | 2 +- openstack/compute/v2/flavor.py | 6 +- openstack/compute/v2/hypervisor.py | 4 +- openstack/compute/v2/image.py | 4 +- openstack/compute/v2/keypair.py | 2 +- openstack/compute/v2/limits.py | 6 +- openstack/compute/v2/server.py | 8 +- openstack/compute/v2/server_group.py | 2 +- openstack/compute/v2/server_interface.py | 4 +- openstack/compute/v2/service.py | 2 +- openstack/compute/v2/volume_attachment.py | 4 +- openstack/database/v1/flavor.py | 2 +- openstack/database/v1/instance.py | 4 +- openstack/identity/v2/extension.py | 2 +- openstack/identity/v2/role.py | 4 +- openstack/identity/v2/tenant.py | 4 +- openstack/identity/v2/user.py | 4 +- openstack/identity/v3/credential.py | 6 +- openstack/identity/v3/domain.py | 6 +- openstack/identity/v3/endpoint.py | 6 +- openstack/identity/v3/group.py | 6 +- openstack/identity/v3/policy.py | 6 +- openstack/identity/v3/project.py | 10 +-- openstack/identity/v3/region.py | 6 +- openstack/identity/v3/role.py | 4 +- openstack/identity/v3/service.py | 6 +- openstack/identity/v3/trust.py | 2 +- openstack/identity/v3/user.py | 6 +- openstack/image/v1/image.py | 4 +- openstack/image/v2/_proxy.py | 4 +- openstack/image/v2/image.py | 10 +-- openstack/image/v2/member.py | 4 +- openstack/instance_ha/v1/host.py | 4 +- openstack/instance_ha/v1/notification.py | 4 +- openstack/instance_ha/v1/segment.py | 4 +- openstack/key_manager/v1/container.py | 4 +- openstack/key_manager/v1/order.py | 4 +- openstack/key_manager/v1/secret.py | 6 +- openstack/load_balancer/v2/health_monitor.py | 4 +- openstack/load_balancer/v2/l7_policy.py | 4 +- openstack/load_balancer/v2/l7_rule.py | 4 +- openstack/load_balancer/v2/listener.py | 4 +- openstack/load_balancer/v2/load_balancer.py | 4 +- openstack/load_balancer/v2/member.py | 4 +- openstack/load_balancer/v2/pool.py | 4 +- openstack/message/v2/claim.py | 10 +-- openstack/message/v2/message.py | 4 +- openstack/message/v2/queue.py | 4 +- openstack/message/v2/subscription.py | 4 +- openstack/network/v2/_proxy.py | 4 +- openstack/network/v2/address_scope.py | 4 +- openstack/network/v2/agent.py | 10 +-- .../network/v2/auto_allocated_topology.py | 4 +- openstack/network/v2/availability_zone.py | 4 +- openstack/network/v2/extension.py | 2 +- openstack/network/v2/flavor.py | 4 +- openstack/network/v2/floating_ip.py | 4 +- openstack/network/v2/health_monitor.py | 4 +- openstack/network/v2/listener.py | 4 +- openstack/network/v2/load_balancer.py | 4 +- openstack/network/v2/metering_label.py | 4 +- openstack/network/v2/metering_label_rule.py | 4 +- openstack/network/v2/network.py | 8 +- .../network/v2/network_ip_availability.py | 4 +- openstack/network/v2/pool.py | 4 +- openstack/network/v2/pool_member.py | 4 +- openstack/network/v2/port.py | 4 +- .../network/v2/qos_bandwidth_limit_rule.py | 4 +- openstack/network/v2/qos_dscp_marking_rule.py | 4 +- .../network/v2/qos_minimum_bandwidth_rule.py | 4 +- openstack/network/v2/qos_policy.py | 4 +- openstack/network/v2/qos_rule_type.py | 4 +- openstack/network/v2/quota.py | 8 +- openstack/network/v2/rbac_policy.py | 4 +- openstack/network/v2/router.py | 6 +- openstack/network/v2/security_group.py | 4 +- openstack/network/v2/security_group_rule.py | 4 +- openstack/network/v2/segment.py | 4 +- openstack/network/v2/service_profile.py | 4 +- openstack/network/v2/service_provider.py | 4 +- openstack/network/v2/subnet.py | 4 +- openstack/network/v2/subnet_pool.py | 4 +- openstack/network/v2/trunk.py | 4 +- openstack/network/v2/vpn_service.py | 4 +- openstack/object_store/v1/_base.py | 2 +- openstack/object_store/v1/account.py | 4 +- openstack/object_store/v1/container.py | 4 +- openstack/object_store/v1/obj.py | 4 +- openstack/orchestration/v1/_proxy.py | 2 +- openstack/orchestration/v1/resource.py | 2 +- openstack/orchestration/v1/software_config.py | 4 +- .../orchestration/v1/software_deployment.py | 8 +- openstack/orchestration/v1/stack.py | 20 +++-- .../orchestration/v1/stack_environment.py | 4 +- openstack/orchestration/v1/stack_files.py | 6 +- openstack/orchestration/v1/stack_template.py | 4 +- openstack/orchestration/v1/template.py | 4 +- openstack/proxy.py | 8 +- openstack/resource.py | 62 +++++++------- .../tests/unit/baremetal/test_version.py | 6 +- .../tests/unit/baremetal/v1/test_chassis.py | 10 +-- .../tests/unit/baremetal/v1/test_driver.py | 4 +- .../tests/unit/baremetal/v1/test_node.py | 32 +++---- .../tests/unit/baremetal/v1/test_port.py | 10 +-- .../unit/baremetal/v1/test_port_group.py | 10 +-- .../unit/block_storage/v2/test_snapshot.py | 4 +- .../tests/unit/block_storage/v2/test_type.py | 4 +- .../unit/block_storage/v2/test_volume.py | 4 +- .../tests/unit/block_store/v2/test_stats.py | 4 +- .../tests/unit/clustering/test_version.py | 4 +- .../tests/unit/clustering/v1/test_action.py | 2 +- .../unit/clustering/v1/test_build_info.py | 2 +- .../tests/unit/clustering/v1/test_cluster.py | 4 +- .../unit/clustering/v1/test_cluster_policy.py | 2 +- .../tests/unit/clustering/v1/test_event.py | 2 +- .../tests/unit/clustering/v1/test_node.py | 8 +- .../tests/unit/clustering/v1/test_policy.py | 8 +- .../unit/clustering/v1/test_policy_type.py | 2 +- .../tests/unit/clustering/v1/test_profile.py | 12 +-- .../unit/clustering/v1/test_profile_type.py | 2 +- .../tests/unit/clustering/v1/test_receiver.py | 4 +- openstack/tests/unit/compute/test_version.py | 4 +- .../tests/unit/compute/v2/test_extension.py | 4 +- .../tests/unit/compute/v2/test_flavor.py | 8 +- .../tests/unit/compute/v2/test_hypervisor.py | 4 +- openstack/tests/unit/compute/v2/test_image.py | 8 +- .../tests/unit/compute/v2/test_keypair.py | 4 +- .../tests/unit/compute/v2/test_limits.py | 14 ++-- .../tests/unit/compute/v2/test_server.py | 8 +- .../unit/compute/v2/test_server_group.py | 4 +- .../unit/compute/v2/test_server_interface.py | 4 +- .../tests/unit/compute/v2/test_server_ip.py | 4 +- .../tests/unit/compute/v2/test_service.py | 4 +- .../unit/compute/v2/test_volume_attachment.py | 4 +- .../tests/unit/database/v1/test_database.py | 4 +- .../tests/unit/database/v1/test_flavor.py | 4 +- .../tests/unit/database/v1/test_instance.py | 4 +- openstack/tests/unit/database/v1/test_user.py | 4 +- openstack/tests/unit/identity/test_version.py | 4 +- .../tests/unit/identity/v2/test_extension.py | 4 +- openstack/tests/unit/identity/v2/test_role.py | 4 +- .../tests/unit/identity/v2/test_tenant.py | 4 +- openstack/tests/unit/identity/v2/test_user.py | 4 +- .../tests/unit/identity/v3/test_credential.py | 6 +- .../tests/unit/identity/v3/test_domain.py | 6 +- .../tests/unit/identity/v3/test_endpoint.py | 6 +- .../tests/unit/identity/v3/test_group.py | 6 +- .../tests/unit/identity/v3/test_policy.py | 6 +- .../tests/unit/identity/v3/test_project.py | 10 +-- .../tests/unit/identity/v3/test_region.py | 6 +- openstack/tests/unit/identity/v3/test_role.py | 4 +- .../tests/unit/identity/v3/test_service.py | 6 +- .../tests/unit/identity/v3/test_trust.py | 2 +- openstack/tests/unit/identity/v3/test_user.py | 6 +- openstack/tests/unit/image/v1/test_image.py | 4 +- openstack/tests/unit/image/v2/test_image.py | 6 +- openstack/tests/unit/image/v2/test_member.py | 4 +- openstack/tests/unit/image/v2/test_proxy.py | 6 +- .../tests/unit/instance_ha/v1/test_host.py | 4 +- .../unit/instance_ha/v1/test_notification.py | 4 +- .../tests/unit/instance_ha/v1/test_segment.py | 4 +- .../unit/key_manager/v1/test_container.py | 4 +- .../tests/unit/key_manager/v1/test_order.py | 4 +- .../tests/unit/key_manager/v1/test_secret.py | 8 +- .../unit/load_balancer/test_health_monitor.py | 4 +- .../tests/unit/load_balancer/test_l7policy.py | 4 +- .../tests/unit/load_balancer/test_l7rule.py | 4 +- .../tests/unit/load_balancer/test_listener.py | 4 +- .../unit/load_balancer/test_load_balancer.py | 4 +- .../tests/unit/load_balancer/test_member.py | 4 +- .../tests/unit/load_balancer/test_pool.py | 4 +- .../tests/unit/load_balancer/test_version.py | 4 +- openstack/tests/unit/message/test_version.py | 4 +- openstack/tests/unit/message/v2/test_claim.py | 12 +-- .../tests/unit/message/v2/test_message.py | 8 +- openstack/tests/unit/message/v2/test_queue.py | 6 +- .../unit/message/v2/test_subscription.py | 6 +- openstack/tests/unit/network/test_version.py | 4 +- .../unit/network/v2/test_address_scope.py | 4 +- openstack/tests/unit/network/v2/test_agent.py | 10 +-- .../v2/test_auto_allocated_topology.py | 4 +- .../unit/network/v2/test_availability_zone.py | 4 +- .../tests/unit/network/v2/test_extension.py | 4 +- .../tests/unit/network/v2/test_flavor.py | 4 +- .../tests/unit/network/v2/test_floating_ip.py | 4 +- .../unit/network/v2/test_health_monitor.py | 4 +- .../tests/unit/network/v2/test_listener.py | 4 +- .../unit/network/v2/test_load_balancer.py | 4 +- .../unit/network/v2/test_metering_label.py | 4 +- .../network/v2/test_metering_label_rule.py | 4 +- .../tests/unit/network/v2/test_network.py | 8 +- .../v2/test_network_ip_availability.py | 4 +- openstack/tests/unit/network/v2/test_pool.py | 4 +- .../tests/unit/network/v2/test_pool_member.py | 4 +- openstack/tests/unit/network/v2/test_port.py | 4 +- .../v2/test_qos_bandwidth_limit_rule.py | 4 +- .../network/v2/test_qos_dscp_marking_rule.py | 4 +- .../v2/test_qos_minimum_bandwidth_rule.py | 4 +- .../tests/unit/network/v2/test_qos_policy.py | 4 +- .../unit/network/v2/test_qos_rule_type.py | 4 +- openstack/tests/unit/network/v2/test_quota.py | 8 +- .../tests/unit/network/v2/test_rbac_policy.py | 4 +- .../tests/unit/network/v2/test_router.py | 6 +- .../unit/network/v2/test_security_group.py | 4 +- .../network/v2/test_security_group_rule.py | 4 +- .../tests/unit/network/v2/test_segment.py | 4 +- .../unit/network/v2/test_service_profile.py | 4 +- .../unit/network/v2/test_service_provider.py | 4 +- .../tests/unit/network/v2/test_subnet.py | 4 +- .../tests/unit/network/v2/test_subnet_pool.py | 4 +- openstack/tests/unit/network/v2/test_trunk.py | 4 +- .../tests/unit/network/v2/test_vpn_service.py | 4 +- .../unit/object_store/v1/test_account.py | 4 +- .../unit/object_store/v1/test_container.py | 12 +-- .../tests/unit/object_store/v1/test_obj.py | 4 +- .../tests/unit/orchestration/test_version.py | 4 +- .../tests/unit/orchestration/v1/test_proxy.py | 16 ++-- .../unit/orchestration/v1/test_resource.py | 2 +- .../orchestration/v1/test_software_config.py | 4 +- .../v1/test_software_deployment.py | 4 +- .../tests/unit/orchestration/v1/test_stack.py | 30 +++---- .../v1/test_stack_environment.py | 4 +- .../unit/orchestration/v1/test_stack_files.py | 6 +- .../orchestration/v1/test_stack_template.py | 4 +- .../unit/orchestration/v1/test_template.py | 4 +- openstack/tests/unit/test_proxy.py | 27 +++--- openstack/tests/unit/test_proxy_base.py | 5 +- openstack/tests/unit/test_resource.py | 84 +++++++++---------- .../tests/unit/workflow/test_execution.py | 2 +- openstack/tests/unit/workflow/test_version.py | 4 +- .../tests/unit/workflow/test_workflow.py | 2 +- openstack/workflow/v2/execution.py | 2 +- openstack/workflow/v2/workflow.py | 2 +- ...ame-resource-methods-5f2a716b08156765.yaml | 12 +++ 259 files changed, 767 insertions(+), 740 deletions(-) create mode 100644 releasenotes/notes/rename-resource-methods-5f2a716b08156765.yaml diff --git a/doc/source/contributor/create/examples/resource/fake.py b/doc/source/contributor/create/examples/resource/fake.py index 074a5fb78..d73a4f07b 100644 --- a/doc/source/contributor/create/examples/resource/fake.py +++ b/doc/source/contributor/create/examples/resource/fake.py @@ -11,8 +11,8 @@ class Fake(resource.Resource): service = fake_service.FakeService() allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True allow_head = True diff --git a/doc/source/contributor/create/resource.rst b/doc/source/contributor/create/resource.rst index d531a4717..ffe5ab3b5 100644 --- a/doc/source/contributor/create/resource.rst +++ b/doc/source/contributor/create/resource.rst @@ -130,14 +130,14 @@ value by setting it to ``True``: +----------------------------------------------+----------------+ | :class:`~openstack.resource.Resource.list` | allow_list | +----------------------------------------------+----------------+ -| :class:`~openstack.resource.Resource.get` | allow_get | +| :class:`~openstack.resource.Resource.fetch` | allow_fetch | +----------------------------------------------+----------------+ -| :class:`~openstack.resource.Resource.update` | allow_update | +| :class:`~openstack.resource.Resource.commit` | allow_commit | +----------------------------------------------+----------------+ -An additional attribute to set is ``put_update`` if your service uses ``PUT`` -requests in order to update a resource. By default, ``PATCH`` requests are -used for ``Resource.update``. +An additional attribute to set is ``commit_method``. It defaults to ``PUT``, +but some services use ``POST`` or ``PATCH`` to commit changes back to the +remote resource. Properties ---------- diff --git a/doc/source/contributor/layout.rst b/doc/source/contributor/layout.rst index 41d3ace5b..3605f7ec6 100644 --- a/doc/source/contributor/layout.rst +++ b/doc/source/contributor/layout.rst @@ -30,7 +30,7 @@ the server-side expects, as this ``prop`` becomes a mapping between the two.:: There are six additional attributes which the ``Resource`` class checks before making requests to the REST API. ``allow_create``, ``allow_retreive``, -``allow_update``, ``allow_delete``, ``allow_head``, and ``allow_list`` are set +``allow_commit``, ``allow_delete``, ``allow_head``, and ``allow_list`` are set to ``True`` or ``False``, and are checked before making the corresponding method call. diff --git a/openstack/_meta/_proxy_templates.py b/openstack/_meta/_proxy_templates.py index 610f816a7..9661a5bd3 100644 --- a/openstack/_meta/_proxy_templates.py +++ b/openstack/_meta/_proxy_templates.py @@ -57,7 +57,7 @@ :returns: ``None`` """ -_GET_TEMPLATE = """Get a single {resource_name} +_FETCH_TEMPLATE = """Fetch a single {resource_name} :param {name}: The value can be the ID of a {name} or a @@ -78,13 +78,13 @@ :rtype: :class:`~{resource_class}` """ -_UPDATE_TEMPLATE = """Update a {resource_name} +_COMMIT_TEMPLATE = """Commit the state of a {resource_name} :param {name}: Either the ID of a {resource_name} or a :class:`~{resource_class}` instance. :attrs kwargs: - The attributes to update on the {resource_name} represented by + The attributes to commit on the {resource_name} represented by ``{name}``. :returns: The updated server @@ -96,8 +96,8 @@ 'delete': _DELETE_TEMPLATE, 'find': _FIND_TEMPLATE, 'list': _LIST_TEMPLATE, - 'get': _GET_TEMPLATE, - 'update': _UPDATE_TEMPLATE, + 'fetch': _FETCH_TEMPLATE, + 'commit': _COMMIT_TEMPLATE, } _FIND_SOURCE = """ @@ -116,8 +116,8 @@ def delete(self, {name}, ignore_missing=True): self._delete(self.{resource_name}, {name}, ignore_missing=ignore_missing) """ -_GET_SOURCE = """ -def get(self, {name}): +_FETCH_SOURCE = """ +def fetch(self, {name}): return self._get(self.{resource_name}, {name}) """ @@ -127,8 +127,8 @@ def list(self, details=True, **query): return self._list(res_cls, paginated=True, **query) """ -_UPDATE_SOURCE = """ -def update(self, {name}, **attrs): +_COMMIT_SOURCE = """ +def commit(self, {name}, **attrs): return self._update(self.{resource_name}, {name}, **attrs) """ @@ -137,8 +137,8 @@ def update(self, {name}, **attrs): 'delete': _DELETE_SOURCE, 'find': _FIND_SOURCE, 'list': _LIST_SOURCE, - 'get': _GET_SOURCE, - 'update': _UPDATE_SOURCE, + 'fetch': _FETCH_SOURCE, + 'commit': _COMMIT_SOURCE, } diff --git a/openstack/_meta/proxy.py b/openstack/_meta/proxy.py index 0186bbd84..f96acb627 100644 --- a/openstack/_meta/proxy.py +++ b/openstack/_meta/proxy.py @@ -111,10 +111,15 @@ def __new__(meta, name, bases, dct): # We pass in a copy of the dct dict so that the exec step can # be done in the context of the class the methods will be attached # to. This allows name resolution to work properly. - for action in ('create', 'get', 'update', 'delete'): + for action in ('create', 'fetch', 'commit', 'delete'): if getattr(res, 'allow_{action}'.format(action=action)): func = compile_function(dct.copy(), action, **args) - add_function(dct, func, action, args) + kwargs = {} + if action == 'fetch': + kwargs['name_template'] = 'get_{name}' + elif action == 'commit': + kwargs['name_template'] = 'update_{name}' + add_function(dct, func, action, args, **kwargs) if res.allow_list: func = compile_function(dct.copy(), 'find', **args) add_function(dct, func, 'find', args) diff --git a/openstack/baremetal/v1/chassis.py b/openstack/baremetal/v1/chassis.py index 116ea5696..32185d7cc 100644 --- a/openstack/baremetal/v1/chassis.py +++ b/openstack/baremetal/v1/chassis.py @@ -22,11 +22,11 @@ class Chassis(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True - update_method = 'PATCH' + commit_method = 'PATCH' _query_mapping = resource.QueryParameters( 'fields' @@ -54,8 +54,8 @@ class ChassisDetail(Chassis): # capabilities allow_create = False - allow_get = False - allow_update = False + allow_fetch = False + allow_commit = False allow_delete = False allow_list = True diff --git a/openstack/baremetal/v1/driver.py b/openstack/baremetal/v1/driver.py index c097edbd8..5434e8259 100644 --- a/openstack/baremetal/v1/driver.py +++ b/openstack/baremetal/v1/driver.py @@ -22,8 +22,8 @@ class Driver(resource.Resource): # capabilities allow_create = False - allow_get = True - allow_update = False + allow_fetch = True + allow_commit = False allow_delete = False allow_list = True diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 428d8ab39..c2f82cb81 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -29,11 +29,11 @@ class Node(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True - update_method = 'PATCH' + commit_method = 'PATCH' _query_mapping = resource.QueryParameters( 'associated', 'driver', 'fields', 'provision_state', 'resource_class', @@ -267,7 +267,7 @@ def set_provision_state(self, session, target, config_drive=None, expected_state, timeout=timeout) else: - return self.get(session) + return self.fetch(session) def wait_for_provision_state(self, session, expected_state, timeout=None, abort_on_failed_state=True): @@ -291,7 +291,7 @@ def wait_for_provision_state(self, session, expected_state, timeout=None, "Timeout waiting for node %(node)s to reach " "target state '%(state)s'" % {'node': self.id, 'state': expected_state}): - self.get(session) + self.fetch(session) if self._check_state_reached(session, expected_state, abort_on_failed_state): return self @@ -348,8 +348,8 @@ class NodeDetail(Node): # capabilities allow_create = False - allow_get = False - allow_update = False + allow_fetch = False + allow_commit = False allow_delete = False allow_list = True diff --git a/openstack/baremetal/v1/port.py b/openstack/baremetal/v1/port.py index 99879de35..a8083d393 100644 --- a/openstack/baremetal/v1/port.py +++ b/openstack/baremetal/v1/port.py @@ -22,11 +22,11 @@ class Port(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True - update_method = 'PATCH' + commit_method = 'PATCH' _query_mapping = resource.QueryParameters( 'fields' @@ -71,8 +71,8 @@ class PortDetail(Port): # capabilities allow_create = False - allow_get = False - allow_update = False + allow_fetch = False + allow_commit = False allow_delete = False allow_list = True diff --git a/openstack/baremetal/v1/port_group.py b/openstack/baremetal/v1/port_group.py index d8fda6429..3c07d70f3 100644 --- a/openstack/baremetal/v1/port_group.py +++ b/openstack/baremetal/v1/port_group.py @@ -22,11 +22,11 @@ class PortGroup(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True - update_method = 'PATCH' + commit_method = 'PATCH' _query_mapping = resource.QueryParameters( 'node', 'address', 'fields', @@ -68,8 +68,8 @@ class PortGroupDetail(PortGroup): base_path = '/portgroups/detail' allow_create = False - allow_get = False - allow_update = False + allow_fetch = False + allow_commit = False allow_delete = False allow_list = True diff --git a/openstack/block_storage/v2/snapshot.py b/openstack/block_storage/v2/snapshot.py index d1080fd3f..9b5b91f66 100644 --- a/openstack/block_storage/v2/snapshot.py +++ b/openstack/block_storage/v2/snapshot.py @@ -25,10 +25,10 @@ class Snapshot(resource.Resource): 'all_tenants', 'name', 'status', 'volume_id') # capabilities - allow_get = True + allow_fetch = True allow_create = True allow_delete = True - allow_update = True + allow_commit = True allow_list = True # Properties diff --git a/openstack/block_storage/v2/stats.py b/openstack/block_storage/v2/stats.py index 914ba9436..5a3f6ae3f 100644 --- a/openstack/block_storage/v2/stats.py +++ b/openstack/block_storage/v2/stats.py @@ -21,7 +21,7 @@ class Pools(resource.Resource): service = block_storage_service.BlockStorageService() # capabilities - allow_get = False + allow_fetch = False allow_create = False allow_delete = False allow_list = True diff --git a/openstack/block_storage/v2/type.py b/openstack/block_storage/v2/type.py index 2ca9a51b0..d57d31853 100644 --- a/openstack/block_storage/v2/type.py +++ b/openstack/block_storage/v2/type.py @@ -21,7 +21,7 @@ class Type(resource.Resource): service = block_storage_service.BlockStorageService() # capabilities - allow_get = True + allow_fetch = True allow_create = True allow_delete = True allow_list = True diff --git a/openstack/block_storage/v2/volume.py b/openstack/block_storage/v2/volume.py index 25e77509f..04a05d963 100644 --- a/openstack/block_storage/v2/volume.py +++ b/openstack/block_storage/v2/volume.py @@ -25,10 +25,10 @@ class Volume(resource.Resource): 'all_tenants', 'name', 'status', 'project_id') # capabilities - allow_get = True + allow_fetch = True allow_create = True allow_delete = True - allow_update = True + allow_commit = True allow_list = True # Properties diff --git a/openstack/clustering/v1/action.py b/openstack/clustering/v1/action.py index 76a63c9b2..79c5d4edc 100644 --- a/openstack/clustering/v1/action.py +++ b/openstack/clustering/v1/action.py @@ -23,7 +23,7 @@ class Action(resource.Resource): # Capabilities allow_list = True - allow_get = True + allow_fetch = True _query_mapping = resource.QueryParameters( 'name', 'action', 'status', 'sort', 'global_project', diff --git a/openstack/clustering/v1/build_info.py b/openstack/clustering/v1/build_info.py index 901f43b5f..705f4065c 100644 --- a/openstack/clustering/v1/build_info.py +++ b/openstack/clustering/v1/build_info.py @@ -20,7 +20,7 @@ class BuildInfo(resource.Resource): service = clustering_service.ClusteringService() # Capabilities - allow_get = True + allow_fetch = True # Properties #: String representation of the API build version diff --git a/openstack/clustering/v1/cluster.py b/openstack/clustering/v1/cluster.py index be01529fa..e8ab4c969 100644 --- a/openstack/clustering/v1/cluster.py +++ b/openstack/clustering/v1/cluster.py @@ -23,11 +23,11 @@ class Cluster(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True - update_method = 'PATCH' + commit_method = 'PATCH' _query_mapping = resource.QueryParameters( 'name', 'status', 'sort', 'global_project') diff --git a/openstack/clustering/v1/cluster_policy.py b/openstack/clustering/v1/cluster_policy.py index 49c9c4c77..6e430cc5f 100644 --- a/openstack/clustering/v1/cluster_policy.py +++ b/openstack/clustering/v1/cluster_policy.py @@ -22,7 +22,7 @@ class ClusterPolicy(resource.Resource): # Capabilities allow_list = True - allow_get = True + allow_fetch = True _query_mapping = resource.QueryParameters( 'sort', 'policy_name', 'policy_type', is_enabled='enabled') diff --git a/openstack/clustering/v1/event.py b/openstack/clustering/v1/event.py index 6ca6798c3..d187fa69c 100644 --- a/openstack/clustering/v1/event.py +++ b/openstack/clustering/v1/event.py @@ -23,7 +23,7 @@ class Event(resource.Resource): # Capabilities allow_list = True - allow_get = True + allow_fetch = True _query_mapping = resource.QueryParameters( 'cluster_id', 'action', 'level', 'sort', 'global_project', diff --git a/openstack/clustering/v1/node.py b/openstack/clustering/v1/node.py index 2c3775224..655e3a959 100644 --- a/openstack/clustering/v1/node.py +++ b/openstack/clustering/v1/node.py @@ -23,12 +23,12 @@ class Node(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True - update_method = 'PATCH' + commit_method = 'PATCH' _query_mapping = resource.QueryParameters( 'show_details', 'name', 'sort', 'global_project', 'cluster_id', @@ -169,8 +169,8 @@ class NodeDetail(Node): base_path = '/nodes/%(node_id)s?show_details=True' allow_create = False - allow_get = True - allow_update = False + allow_fetch = True + allow_commit = False allow_delete = False allow_list = False diff --git a/openstack/clustering/v1/policy.py b/openstack/clustering/v1/policy.py index 5a1202e4f..be98984f7 100644 --- a/openstack/clustering/v1/policy.py +++ b/openstack/clustering/v1/policy.py @@ -22,12 +22,12 @@ class Policy(resource.Resource): # Capabilities allow_list = True - allow_get = True + allow_fetch = True allow_create = True allow_delete = True - allow_update = True + allow_commit = True - update_method = 'PATCH' + commit_method = 'PATCH' _query_mapping = resource.QueryParameters( 'name', 'type', 'sort', 'global_project') @@ -58,9 +58,9 @@ class PolicyValidate(Policy): # Capabilities allow_list = False - allow_get = False + allow_fetch = False allow_create = True allow_delete = False - allow_update = False + allow_commit = False - update_method = 'PUT' + commit_method = 'PUT' diff --git a/openstack/clustering/v1/policy_type.py b/openstack/clustering/v1/policy_type.py index 6acb10000..bd2bdd490 100644 --- a/openstack/clustering/v1/policy_type.py +++ b/openstack/clustering/v1/policy_type.py @@ -22,7 +22,7 @@ class PolicyType(resource.Resource): # Capabilities allow_list = True - allow_get = True + allow_fetch = True # Properties #: Name of policy type. diff --git a/openstack/clustering/v1/profile.py b/openstack/clustering/v1/profile.py index b148a6167..77f117dde 100644 --- a/openstack/clustering/v1/profile.py +++ b/openstack/clustering/v1/profile.py @@ -22,12 +22,12 @@ class Profile(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True - update_method = 'PATCH' + commit_method = 'PATCH' _query_mapping = resource.QueryParameters( 'sort', 'global_project', 'type', 'name') @@ -56,9 +56,9 @@ class Profile(resource.Resource): class ProfileValidate(Profile): base_path = '/profiles/validate' allow_create = True - allow_get = False - allow_update = False + allow_fetch = False + allow_commit = False allow_delete = False allow_list = False - update_method = 'PUT' + commit_method = 'PUT' diff --git a/openstack/clustering/v1/profile_type.py b/openstack/clustering/v1/profile_type.py index 0f105920a..f2b7ad88f 100644 --- a/openstack/clustering/v1/profile_type.py +++ b/openstack/clustering/v1/profile_type.py @@ -23,7 +23,7 @@ class ProfileType(resource.Resource): # Capabilities allow_list = True - allow_get = True + allow_fetch = True # Properties #: Name of the profile type. diff --git a/openstack/clustering/v1/receiver.py b/openstack/clustering/v1/receiver.py index 434244d75..d42d7a48d 100644 --- a/openstack/clustering/v1/receiver.py +++ b/openstack/clustering/v1/receiver.py @@ -22,12 +22,12 @@ class Receiver(resource.Resource): # Capabilities allow_list = True - allow_get = True + allow_fetch = True allow_create = True - allow_update = True + allow_commit = True allow_delete = True - update_method = 'PATCH' + commit_method = 'PATCH' _query_mapping = resource.QueryParameters( 'name', 'type', 'cluster_id', 'action', 'sort', 'global_project', diff --git a/openstack/compute/v2/extension.py b/openstack/compute/v2/extension.py index c5a938bf5..a12f22efb 100644 --- a/openstack/compute/v2/extension.py +++ b/openstack/compute/v2/extension.py @@ -22,7 +22,7 @@ class Extension(resource.Resource): id_attribute = "alias" # capabilities - allow_get = True + allow_fetch = True allow_list = True # Properties diff --git a/openstack/compute/v2/flavor.py b/openstack/compute/v2/flavor.py index abdc08df6..14920403a 100644 --- a/openstack/compute/v2/flavor.py +++ b/openstack/compute/v2/flavor.py @@ -22,7 +22,7 @@ class Flavor(resource.Resource): # capabilities allow_create = True - allow_get = True + allow_fetch = True allow_delete = True allow_list = True @@ -62,8 +62,8 @@ class FlavorDetail(Flavor): base_path = '/flavors/detail' allow_create = False - allow_get = False - allow_update = False + allow_fetch = False + allow_commit = False allow_delete = False allow_list = True diff --git a/openstack/compute/v2/hypervisor.py b/openstack/compute/v2/hypervisor.py index e28cb095a..2f49f5a3c 100644 --- a/openstack/compute/v2/hypervisor.py +++ b/openstack/compute/v2/hypervisor.py @@ -23,7 +23,7 @@ class Hypervisor(resource.Resource): service = compute_service.ComputeService() # capabilities - allow_get = True + allow_fetch = True allow_list = True # Properties @@ -71,5 +71,5 @@ class HypervisorDetail(Hypervisor): base_path = '/os-hypervisors/detail' # capabilities - allow_get = False + allow_fetch = False allow_list = True diff --git a/openstack/compute/v2/image.py b/openstack/compute/v2/image.py index b201ac922..44c037a68 100644 --- a/openstack/compute/v2/image.py +++ b/openstack/compute/v2/image.py @@ -22,7 +22,7 @@ class Image(resource.Resource, metadata.MetadataMixin): service = compute_service.ComputeService() # capabilities - allow_get = True + allow_fetch = True allow_delete = True allow_list = True @@ -60,6 +60,6 @@ class Image(resource.Resource, metadata.MetadataMixin): class ImageDetail(Image): base_path = '/images/detail' - allow_get = False + allow_fetch = False allow_delete = False allow_list = True diff --git a/openstack/compute/v2/keypair.py b/openstack/compute/v2/keypair.py index 268de02cc..a1fb16e16 100644 --- a/openstack/compute/v2/keypair.py +++ b/openstack/compute/v2/keypair.py @@ -22,7 +22,7 @@ class Keypair(resource.Resource): # capabilities allow_create = True - allow_get = True + allow_fetch = True allow_delete = True allow_list = True diff --git a/openstack/compute/v2/limits.py b/openstack/compute/v2/limits.py index 090f312ac..1dc92bb15 100644 --- a/openstack/compute/v2/limits.py +++ b/openstack/compute/v2/limits.py @@ -73,12 +73,12 @@ class Limits(resource.Resource): resource_key = "limits" service = compute_service.ComputeService() - allow_get = True + allow_fetch = True absolute = resource.Body("absolute", type=AbsoluteLimits) rate = resource.Body("rate", type=list, list_type=RateLimit) - def get(self, session, requires_id=False, error_message=None): + def fetch(self, session, requires_id=False, error_message=None): """Get the Limits resource. :param session: The session to use for making this request. @@ -89,5 +89,5 @@ def get(self, session, requires_id=False, error_message=None): """ # TODO(mordred) We shouldn't have to subclass just to declare # requires_id = False. - return super(Limits, self).get( + return super(Limits, self).fetch( session=session, requires_id=False, error_message=error_message) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 30e39fe36..f1b14771f 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -24,8 +24,8 @@ class Server(resource.Resource, metadata.MetadataMixin): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True @@ -445,8 +445,8 @@ class ServerDetail(Server): # capabilities allow_create = False - allow_get = False - allow_update = False + allow_fetch = False + allow_commit = False allow_delete = False allow_list = True diff --git a/openstack/compute/v2/server_group.py b/openstack/compute/v2/server_group.py index 1807069c5..18a211358 100644 --- a/openstack/compute/v2/server_group.py +++ b/openstack/compute/v2/server_group.py @@ -24,7 +24,7 @@ class ServerGroup(resource.Resource): # capabilities allow_create = True - allow_get = True + allow_fetch = True allow_delete = True allow_list = True diff --git a/openstack/compute/v2/server_interface.py b/openstack/compute/v2/server_interface.py index 98659d9a1..0a2750fc2 100644 --- a/openstack/compute/v2/server_interface.py +++ b/openstack/compute/v2/server_interface.py @@ -22,8 +22,8 @@ class ServerInterface(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = False + allow_fetch = True + allow_commit = False allow_delete = True allow_list = True diff --git a/openstack/compute/v2/service.py b/openstack/compute/v2/service.py index 4e9b0873e..78d0735c6 100644 --- a/openstack/compute/v2/service.py +++ b/openstack/compute/v2/service.py @@ -24,7 +24,7 @@ class Service(resource.Resource): # capabilities allow_list = True - allow_update = True + allow_commit = True # Properties #: Status of service diff --git a/openstack/compute/v2/volume_attachment.py b/openstack/compute/v2/volume_attachment.py index aa3e20674..982ae42b0 100644 --- a/openstack/compute/v2/volume_attachment.py +++ b/openstack/compute/v2/volume_attachment.py @@ -22,8 +22,8 @@ class VolumeAttachment(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = False + allow_fetch = True + allow_commit = False allow_delete = True allow_list = True diff --git a/openstack/database/v1/flavor.py b/openstack/database/v1/flavor.py index ac4bf7b9d..10f78e3cc 100644 --- a/openstack/database/v1/flavor.py +++ b/openstack/database/v1/flavor.py @@ -22,7 +22,7 @@ class Flavor(resource.Resource): # capabilities allow_list = True - allow_get = True + allow_fetch = True # Properties #: Links associated with the flavor diff --git a/openstack/database/v1/instance.py b/openstack/database/v1/instance.py index 76f67b035..b0b8424c6 100644 --- a/openstack/database/v1/instance.py +++ b/openstack/database/v1/instance.py @@ -23,8 +23,8 @@ class Instance(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/identity/v2/extension.py b/openstack/identity/v2/extension.py index 0012831e6..7f05fb747 100644 --- a/openstack/identity/v2/extension.py +++ b/openstack/identity/v2/extension.py @@ -22,7 +22,7 @@ class Extension(resource.Resource): # capabilities allow_list = True - allow_get = True + allow_fetch = True # Properties #: A unique identifier, which will be used for accessing the extension diff --git a/openstack/identity/v2/role.py b/openstack/identity/v2/role.py index be07ff7fb..625c1413b 100644 --- a/openstack/identity/v2/role.py +++ b/openstack/identity/v2/role.py @@ -23,8 +23,8 @@ class Role(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/identity/v2/tenant.py b/openstack/identity/v2/tenant.py index ccac50aee..66a54aad5 100644 --- a/openstack/identity/v2/tenant.py +++ b/openstack/identity/v2/tenant.py @@ -22,8 +22,8 @@ class Tenant(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/identity/v2/user.py b/openstack/identity/v2/user.py index 3f905712c..a8f219e92 100644 --- a/openstack/identity/v2/user.py +++ b/openstack/identity/v2/user.py @@ -22,8 +22,8 @@ class User(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/identity/v3/credential.py b/openstack/identity/v3/credential.py index 781397964..2b2bf0c0b 100644 --- a/openstack/identity/v3/credential.py +++ b/openstack/identity/v3/credential.py @@ -22,11 +22,11 @@ class Credential(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True - update_method = 'PATCH' + commit_method = 'PATCH' _query_mapping = resource.QueryParameters( 'type', 'user_id', diff --git a/openstack/identity/v3/domain.py b/openstack/identity/v3/domain.py index 7ce373499..6c32a5c4c 100644 --- a/openstack/identity/v3/domain.py +++ b/openstack/identity/v3/domain.py @@ -23,11 +23,11 @@ class Domain(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True - update_method = 'PATCH' + commit_method = 'PATCH' _query_mapping = resource.QueryParameters( 'name', diff --git a/openstack/identity/v3/endpoint.py b/openstack/identity/v3/endpoint.py index e68517a04..841f5c59a 100644 --- a/openstack/identity/v3/endpoint.py +++ b/openstack/identity/v3/endpoint.py @@ -22,11 +22,11 @@ class Endpoint(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True - update_method = 'PATCH' + commit_method = 'PATCH' _query_mapping = resource.QueryParameters( 'interface', 'service_id', diff --git a/openstack/identity/v3/group.py b/openstack/identity/v3/group.py index d0d132ec0..acf763006 100644 --- a/openstack/identity/v3/group.py +++ b/openstack/identity/v3/group.py @@ -22,11 +22,11 @@ class Group(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True - update_method = 'PATCH' + commit_method = 'PATCH' _query_mapping = resource.QueryParameters( 'domain_id', 'name', diff --git a/openstack/identity/v3/policy.py b/openstack/identity/v3/policy.py index 6d240d8a2..a4a6f7cfd 100644 --- a/openstack/identity/v3/policy.py +++ b/openstack/identity/v3/policy.py @@ -22,11 +22,11 @@ class Policy(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True - update_method = 'PATCH' + commit_method = 'PATCH' # Properties #: The policy rule set itself, as a serialized blob. *Type: string* diff --git a/openstack/identity/v3/project.py b/openstack/identity/v3/project.py index 12ddbd82c..56ee7dab2 100644 --- a/openstack/identity/v3/project.py +++ b/openstack/identity/v3/project.py @@ -23,11 +23,11 @@ class Project(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True - update_method = 'PATCH' + commit_method = 'PATCH' _query_mapping = resource.QueryParameters( 'domain_id', @@ -123,7 +123,7 @@ class UserProject(Project): # capabilities allow_create = False - allow_get = False - allow_update = False + allow_fetch = False + allow_commit = False allow_delete = False allow_list = True diff --git a/openstack/identity/v3/region.py b/openstack/identity/v3/region.py index d774aaa17..fc0e44e73 100644 --- a/openstack/identity/v3/region.py +++ b/openstack/identity/v3/region.py @@ -22,11 +22,11 @@ class Region(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True - update_method = 'PATCH' + commit_method = 'PATCH' _query_mapping = resource.QueryParameters( 'parent_region_id', diff --git a/openstack/identity/v3/role.py b/openstack/identity/v3/role.py index aad9bdc17..e4a191fde 100644 --- a/openstack/identity/v3/role.py +++ b/openstack/identity/v3/role.py @@ -22,8 +22,8 @@ class Role(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/identity/v3/service.py b/openstack/identity/v3/service.py index 51bb7ced4..6445fb759 100644 --- a/openstack/identity/v3/service.py +++ b/openstack/identity/v3/service.py @@ -22,11 +22,11 @@ class Service(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True - update_method = 'PATCH' + commit_method = 'PATCH' _query_mapping = resource.QueryParameters( 'type', diff --git a/openstack/identity/v3/trust.py b/openstack/identity/v3/trust.py index 1b334bb02..07985f1b4 100644 --- a/openstack/identity/v3/trust.py +++ b/openstack/identity/v3/trust.py @@ -23,7 +23,7 @@ class Trust(resource.Resource): # capabilities allow_create = True - allow_get = True + allow_fetch = True allow_delete = True allow_list = True diff --git a/openstack/identity/v3/user.py b/openstack/identity/v3/user.py index bed77fb5f..bf1b34615 100644 --- a/openstack/identity/v3/user.py +++ b/openstack/identity/v3/user.py @@ -22,11 +22,11 @@ class User(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True - update_method = 'PATCH' + commit_method = 'PATCH' _query_mapping = resource.QueryParameters( 'domain_id', diff --git a/openstack/image/v1/image.py b/openstack/image/v1/image.py index b5b10fa1d..3f93a51df 100644 --- a/openstack/image/v1/image.py +++ b/openstack/image/v1/image.py @@ -22,8 +22,8 @@ class Image(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 239c543e8..3a689d991 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -157,8 +157,8 @@ def update_image(self, image, **attrs): :returns: The updated image :rtype: :class:`~openstack.image.v2.image.Image` """ - img = self._get_resource(_image.Image, image) - return img.update(self, **attrs) + res = self._get_resource(_image.Image, image) + return res.commit(self, **attrs) def deactivate_image(self, image): """Deactivate an image diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index 376d96748..42f54e23c 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -31,11 +31,11 @@ class Image(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True - update_method = 'PATCH' + commit_method = 'PATCH' _query_mapping = resource.QueryParameters( "name", "visibility", @@ -272,7 +272,7 @@ def download(self, session, stream=False): # If we don't receive the Content-MD5 header with the download, # make an additional call to get the image details and look at # the checksum attribute. - details = self.get(session) + details = self.fetch(session) checksum = details.checksum # if we are returning the repsonse object, ensure that it @@ -294,7 +294,7 @@ def download(self, session, stream=False): return resp.content - def update(self, session, **attrs): + def commit(self, session, **attrs): url = utils.urljoin(self.base_path, self.id) headers = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', diff --git a/openstack/image/v2/member.py b/openstack/image/v2/member.py index 4157646e0..6d565b447 100644 --- a/openstack/image/v2/member.py +++ b/openstack/image/v2/member.py @@ -21,8 +21,8 @@ class Member(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/instance_ha/v1/host.py b/openstack/instance_ha/v1/host.py index 63cc681f1..a6d9a0142 100644 --- a/openstack/instance_ha/v1/host.py +++ b/openstack/instance_ha/v1/host.py @@ -29,9 +29,9 @@ class Host(resource.Resource): # 4] PUT /v1/segments//hosts # 5] DELETE /v1/segments//hosts allow_list = True - allow_get = True + allow_fetch = True allow_create = True - allow_update = True + allow_commit = True allow_delete = True # Properties diff --git a/openstack/instance_ha/v1/notification.py b/openstack/instance_ha/v1/notification.py index ae58e699a..45f1a2e27 100644 --- a/openstack/instance_ha/v1/notification.py +++ b/openstack/instance_ha/v1/notification.py @@ -27,9 +27,9 @@ class Notification(resource.Resource): # 2] GET /v1/notifications/ # 3] POST /v1/notifications allow_list = True - allow_get = True + allow_fetch = True allow_create = True - allow_update = False + allow_commit = False allow_delete = False # Properties diff --git a/openstack/instance_ha/v1/segment.py b/openstack/instance_ha/v1/segment.py index ee9bd43bf..eec2c6c13 100644 --- a/openstack/instance_ha/v1/segment.py +++ b/openstack/instance_ha/v1/segment.py @@ -29,9 +29,9 @@ class Segment(resource.Resource): # 4] PUT /v1/segments/ # 5] DELETE /v1/segments/ allow_list = True - allow_get = True + allow_fetch = True allow_create = True - allow_update = True + allow_commit = True allow_delete = True # Properties diff --git a/openstack/key_manager/v1/container.py b/openstack/key_manager/v1/container.py index 5cc4ff1a0..e5b2629c6 100644 --- a/openstack/key_manager/v1/container.py +++ b/openstack/key_manager/v1/container.py @@ -22,8 +22,8 @@ class Container(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/key_manager/v1/order.py b/openstack/key_manager/v1/order.py index a701275d5..fec0592f2 100644 --- a/openstack/key_manager/v1/order.py +++ b/openstack/key_manager/v1/order.py @@ -22,8 +22,8 @@ class Order(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/key_manager/v1/secret.py b/openstack/key_manager/v1/secret.py index 60d95f4d1..2ba4ab552 100644 --- a/openstack/key_manager/v1/secret.py +++ b/openstack/key_manager/v1/secret.py @@ -23,8 +23,8 @@ class Secret(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True @@ -79,7 +79,7 @@ class Secret(resource.Resource): #: (required if payload is encoded) payload_content_encoding = resource.Body('payload_content_encoding') - def get(self, session, requires_id=True, error_message=None): + def fetch(self, session, requires_id=True, error_message=None): request = self._prepare_request(requires_id=requires_id) response = session.get(request.url).json() diff --git a/openstack/load_balancer/v2/health_monitor.py b/openstack/load_balancer/v2/health_monitor.py index d06de09a0..aca7fcb5d 100644 --- a/openstack/load_balancer/v2/health_monitor.py +++ b/openstack/load_balancer/v2/health_monitor.py @@ -23,9 +23,9 @@ class HealthMonitor(resource.Resource): # capabilities allow_create = True allow_list = True - allow_get = True + allow_fetch = True allow_delete = True - allow_update = True + allow_commit = True _query_mapping = resource.QueryParameters( 'name', 'created_at', 'updated_at', 'delay', 'expected_codes', diff --git a/openstack/load_balancer/v2/l7_policy.py b/openstack/load_balancer/v2/l7_policy.py index 6d41d595b..37e434a69 100644 --- a/openstack/load_balancer/v2/l7_policy.py +++ b/openstack/load_balancer/v2/l7_policy.py @@ -23,8 +23,8 @@ class L7Policy(resource.Resource): # capabilities allow_create = True allow_list = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True _query_mapping = resource.QueryParameters( diff --git a/openstack/load_balancer/v2/l7_rule.py b/openstack/load_balancer/v2/l7_rule.py index 398e31d24..cc2aa4364 100644 --- a/openstack/load_balancer/v2/l7_rule.py +++ b/openstack/load_balancer/v2/l7_rule.py @@ -23,8 +23,8 @@ class L7Rule(resource.Resource): # capabilities allow_create = True allow_list = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True _query_mapping = resource.QueryParameters( diff --git a/openstack/load_balancer/v2/listener.py b/openstack/load_balancer/v2/listener.py index 0738466d6..6597c8998 100644 --- a/openstack/load_balancer/v2/listener.py +++ b/openstack/load_balancer/v2/listener.py @@ -22,8 +22,8 @@ class Listener(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/load_balancer/v2/load_balancer.py b/openstack/load_balancer/v2/load_balancer.py index e1b9fe579..a3ce64999 100644 --- a/openstack/load_balancer/v2/load_balancer.py +++ b/openstack/load_balancer/v2/load_balancer.py @@ -22,8 +22,8 @@ class LoadBalancer(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/load_balancer/v2/member.py b/openstack/load_balancer/v2/member.py index 1e6d0fc23..ac65eb268 100644 --- a/openstack/load_balancer/v2/member.py +++ b/openstack/load_balancer/v2/member.py @@ -22,8 +22,8 @@ class Member(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/load_balancer/v2/pool.py b/openstack/load_balancer/v2/pool.py index 305299c56..1868d208c 100644 --- a/openstack/load_balancer/v2/pool.py +++ b/openstack/load_balancer/v2/pool.py @@ -23,9 +23,9 @@ class Pool(resource.Resource): # capabilities allow_create = True allow_list = True - allow_get = True + allow_fetch = True allow_delete = True - allow_update = True + allow_commit = True _query_mapping = resource.QueryParameters( 'health_monitor_id', 'lb_algorithm', 'listener_id', 'loadbalancer_id', diff --git a/openstack/message/v2/claim.py b/openstack/message/v2/claim.py index c032eaae6..51c1c0a14 100644 --- a/openstack/message/v2/claim.py +++ b/openstack/message/v2/claim.py @@ -28,10 +28,10 @@ class Claim(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True - update_method = 'PATCH' + commit_method = 'PATCH' # Properties #: The value in seconds indicating how long the claim has existed. @@ -82,7 +82,7 @@ def create(self, session, prepend_key=False): return self - def get(self, session, requires_id=True, error_message=None): + def fetch(self, session, requires_id=True, error_message=None): request = self._prepare_request(requires_id=requires_id) headers = { "Client-ID": self.client_id or str(uuid.uuid4()), @@ -96,7 +96,7 @@ def get(self, session, requires_id=True, error_message=None): return self - def update(self, session, prepend_key=False, has_body=False): + def commit(self, session, prepend_key=False, has_body=False): request = self._prepare_request(prepend_key=prepend_key) headers = { "Client-ID": self.client_id or str(uuid.uuid4()), diff --git a/openstack/message/v2/message.py b/openstack/message/v2/message.py index b496d7786..d85f5d2b5 100644 --- a/openstack/message/v2/message.py +++ b/openstack/message/v2/message.py @@ -29,7 +29,7 @@ class Message(resource.Resource): # capabilities allow_create = True allow_list = True - allow_get = True + allow_fetch = True allow_delete = True _query_mapping = resource.QueryParameters("echo", "include_claimed") @@ -109,7 +109,7 @@ def list(cls, session, paginated=True, **params): query_params["limit"] = yielded query_params["marker"] = new_marker - def get(self, session, requires_id=True, error_message=None): + def fetch(self, session, requires_id=True, error_message=None): request = self._prepare_request(requires_id=requires_id) headers = { "Client-ID": self.client_id or str(uuid.uuid4()), diff --git a/openstack/message/v2/queue.py b/openstack/message/v2/queue.py index 4f7087cad..2da9e87c9 100644 --- a/openstack/message/v2/queue.py +++ b/openstack/message/v2/queue.py @@ -29,7 +29,7 @@ class Queue(resource.Resource): # capabilities allow_create = True allow_list = True - allow_get = True + allow_fetch = True allow_delete = True # Properties @@ -107,7 +107,7 @@ def list(cls, session, paginated=False, **params): query_params["limit"] = yielded query_params["marker"] = new_marker - def get(self, session, requires_id=True, error_message=None): + def fetch(self, session, requires_id=True, error_message=None): request = self._prepare_request(requires_id=requires_id) headers = { "Client-ID": self.client_id or str(uuid.uuid4()), diff --git a/openstack/message/v2/subscription.py b/openstack/message/v2/subscription.py index 6dcf9d540..2bd779711 100644 --- a/openstack/message/v2/subscription.py +++ b/openstack/message/v2/subscription.py @@ -29,7 +29,7 @@ class Subscription(resource.Resource): # capabilities allow_create = True allow_list = True - allow_get = True + allow_fetch = True allow_delete = True # Properties @@ -115,7 +115,7 @@ def list(cls, session, paginated=True, **params): query_params["limit"] = yielded query_params["marker"] = new_marker - def get(self, session, requires_id=True, error_message=None): + def fetch(self, session, requires_id=True, error_message=None): request = self._prepare_request(requires_id=requires_id) headers = { "Client-ID": self.client_id or str(uuid.uuid4()), diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index f61335af7..24a0c9f1a 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -1538,11 +1538,11 @@ def update_port(self, port, **attrs): def add_ip_to_port(self, port, ip): ip.port_id = port.id - return ip.update(self) + return ip.commit(self) def remove_ip_from_port(self, ip): ip.port_id = None - return ip.update(self) + return ip.commit(self) def get_subnet_ports(self, subnet_id): result = [] diff --git a/openstack/network/v2/address_scope.py b/openstack/network/v2/address_scope.py index 501800cb2..326cf2035 100644 --- a/openstack/network/v2/address_scope.py +++ b/openstack/network/v2/address_scope.py @@ -23,8 +23,8 @@ class AddressScope(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/network/v2/agent.py b/openstack/network/v2/agent.py index c657bfc8b..6df7af057 100644 --- a/openstack/network/v2/agent.py +++ b/openstack/network/v2/agent.py @@ -24,8 +24,8 @@ class Agent(resource.Resource): # capabilities allow_create = False - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True @@ -100,8 +100,8 @@ class NetworkHostingDHCPAgent(Agent): # capabilities allow_create = False - allow_get = True - allow_update = False + allow_fetch = True + allow_commit = False allow_delete = False allow_list = True @@ -118,7 +118,7 @@ class RouterL3Agent(Agent): # capabilities allow_create = False allow_retrieve = True - allow_update = False + allow_commit = False allow_delete = False allow_list = True diff --git a/openstack/network/v2/auto_allocated_topology.py b/openstack/network/v2/auto_allocated_topology.py index 917de9780..98b2d9fbe 100644 --- a/openstack/network/v2/auto_allocated_topology.py +++ b/openstack/network/v2/auto_allocated_topology.py @@ -22,8 +22,8 @@ class AutoAllocatedTopology(resource.Resource): # Capabilities allow_create = False - allow_get = True - allow_update = False + allow_fetch = True + allow_commit = False allow_delete = True allow_list = False diff --git a/openstack/network/v2/availability_zone.py b/openstack/network/v2/availability_zone.py index 7bc1a8d04..295ac42ac 100644 --- a/openstack/network/v2/availability_zone.py +++ b/openstack/network/v2/availability_zone.py @@ -22,8 +22,8 @@ class AvailabilityZone(_resource.Resource): # capabilities allow_create = False - allow_get = False - allow_update = False + allow_fetch = False + allow_commit = False allow_delete = False allow_list = True diff --git a/openstack/network/v2/extension.py b/openstack/network/v2/extension.py index 50fc4371f..d13c806b2 100644 --- a/openstack/network/v2/extension.py +++ b/openstack/network/v2/extension.py @@ -21,7 +21,7 @@ class Extension(resource.Resource): service = network_service.NetworkService() # capabilities - allow_get = True + allow_fetch = True allow_list = True # NOTE: No query parameters supported diff --git a/openstack/network/v2/flavor.py b/openstack/network/v2/flavor.py index 0947f5677..cb7f0c1eb 100644 --- a/openstack/network/v2/flavor.py +++ b/openstack/network/v2/flavor.py @@ -23,8 +23,8 @@ class Flavor(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/network/v2/floating_ip.py b/openstack/network/v2/floating_ip.py index e562856b0..dcde09905 100644 --- a/openstack/network/v2/floating_ip.py +++ b/openstack/network/v2/floating_ip.py @@ -25,8 +25,8 @@ class FloatingIP(resource.Resource, tag.TagMixin): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/network/v2/health_monitor.py b/openstack/network/v2/health_monitor.py index 4430b3009..6ed64bf05 100644 --- a/openstack/network/v2/health_monitor.py +++ b/openstack/network/v2/health_monitor.py @@ -22,8 +22,8 @@ class HealthMonitor(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/network/v2/listener.py b/openstack/network/v2/listener.py index b947cc171..e90398577 100644 --- a/openstack/network/v2/listener.py +++ b/openstack/network/v2/listener.py @@ -22,8 +22,8 @@ class Listener(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/network/v2/load_balancer.py b/openstack/network/v2/load_balancer.py index 29cedd299..6f66b752d 100644 --- a/openstack/network/v2/load_balancer.py +++ b/openstack/network/v2/load_balancer.py @@ -22,8 +22,8 @@ class LoadBalancer(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/network/v2/metering_label.py b/openstack/network/v2/metering_label.py index 5d9cccf38..90a62781b 100644 --- a/openstack/network/v2/metering_label.py +++ b/openstack/network/v2/metering_label.py @@ -22,8 +22,8 @@ class MeteringLabel(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/network/v2/metering_label_rule.py b/openstack/network/v2/metering_label_rule.py index e6db81fd5..eb766c8de 100644 --- a/openstack/network/v2/metering_label_rule.py +++ b/openstack/network/v2/metering_label_rule.py @@ -22,8 +22,8 @@ class MeteringLabelRule(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/network/v2/network.py b/openstack/network/v2/network.py index f9067e57b..5b706375a 100644 --- a/openstack/network/v2/network.py +++ b/openstack/network/v2/network.py @@ -23,8 +23,8 @@ class Network(resource.Resource, tag.TagMixin): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True @@ -125,8 +125,8 @@ class DHCPAgentHostingNetwork(Network): # capabilities allow_create = False - allow_get = True - allow_update = False + allow_fetch = True + allow_commit = False allow_delete = False allow_list = True diff --git a/openstack/network/v2/network_ip_availability.py b/openstack/network/v2/network_ip_availability.py index f9e136211..ce2c372b7 100644 --- a/openstack/network/v2/network_ip_availability.py +++ b/openstack/network/v2/network_ip_availability.py @@ -23,8 +23,8 @@ class NetworkIPAvailability(resource.Resource): # capabilities allow_create = False - allow_get = True - allow_update = False + allow_fetch = True + allow_commit = False allow_delete = False allow_list = True diff --git a/openstack/network/v2/pool.py b/openstack/network/v2/pool.py index cecba0072..671610087 100644 --- a/openstack/network/v2/pool.py +++ b/openstack/network/v2/pool.py @@ -22,8 +22,8 @@ class Pool(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/network/v2/pool_member.py b/openstack/network/v2/pool_member.py index 2de93f951..c75e0c3bb 100644 --- a/openstack/network/v2/pool_member.py +++ b/openstack/network/v2/pool_member.py @@ -22,8 +22,8 @@ class PoolMember(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index 64d034aeb..625464713 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -23,8 +23,8 @@ class Port(resource.Resource, tag.TagMixin): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/network/v2/qos_bandwidth_limit_rule.py b/openstack/network/v2/qos_bandwidth_limit_rule.py index f080938ec..1f289ebb6 100644 --- a/openstack/network/v2/qos_bandwidth_limit_rule.py +++ b/openstack/network/v2/qos_bandwidth_limit_rule.py @@ -22,8 +22,8 @@ class QoSBandwidthLimitRule(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/network/v2/qos_dscp_marking_rule.py b/openstack/network/v2/qos_dscp_marking_rule.py index bf247f1e1..9333d9813 100644 --- a/openstack/network/v2/qos_dscp_marking_rule.py +++ b/openstack/network/v2/qos_dscp_marking_rule.py @@ -22,8 +22,8 @@ class QoSDSCPMarkingRule(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/network/v2/qos_minimum_bandwidth_rule.py b/openstack/network/v2/qos_minimum_bandwidth_rule.py index ad773d5d8..476079570 100644 --- a/openstack/network/v2/qos_minimum_bandwidth_rule.py +++ b/openstack/network/v2/qos_minimum_bandwidth_rule.py @@ -22,8 +22,8 @@ class QoSMinimumBandwidthRule(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/network/v2/qos_policy.py b/openstack/network/v2/qos_policy.py index b425ab7b3..941132899 100644 --- a/openstack/network/v2/qos_policy.py +++ b/openstack/network/v2/qos_policy.py @@ -24,8 +24,8 @@ class QoSPolicy(resource.Resource, tag.TagMixin): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/network/v2/qos_rule_type.py b/openstack/network/v2/qos_rule_type.py index a241615ea..a917af2a1 100644 --- a/openstack/network/v2/qos_rule_type.py +++ b/openstack/network/v2/qos_rule_type.py @@ -22,8 +22,8 @@ class QoSRuleType(resource.Resource): # capabilities allow_create = False - allow_get = True - allow_update = False + allow_fetch = True + allow_commit = False allow_delete = False allow_list = True diff --git a/openstack/network/v2/quota.py b/openstack/network/v2/quota.py index fadaff1b0..fbed68f5f 100644 --- a/openstack/network/v2/quota.py +++ b/openstack/network/v2/quota.py @@ -21,8 +21,8 @@ class Quota(resource.Resource): service = network_service.NetworkService() # capabilities - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True @@ -75,7 +75,7 @@ class QuotaDefault(Quota): # capabilities allow_retrieve = True - allow_update = False + allow_commit = False allow_delete = False allow_list = False @@ -89,7 +89,7 @@ class QuotaDetails(Quota): # capabilities allow_retrieve = True - allow_update = False + allow_commit = False allow_delete = False allow_list = False diff --git a/openstack/network/v2/rbac_policy.py b/openstack/network/v2/rbac_policy.py index e0262ab77..4043f0e44 100644 --- a/openstack/network/v2/rbac_policy.py +++ b/openstack/network/v2/rbac_policy.py @@ -22,8 +22,8 @@ class RBACPolicy(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/network/v2/router.py b/openstack/network/v2/router.py index d2f950cca..71cb19495 100644 --- a/openstack/network/v2/router.py +++ b/openstack/network/v2/router.py @@ -24,8 +24,8 @@ class Router(resource.Resource, tag.TagMixin): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True @@ -142,7 +142,7 @@ class L3AgentRouter(Router): # capabilities allow_create = False allow_retrieve = True - allow_update = False + allow_commit = False allow_delete = False allow_list = True diff --git a/openstack/network/v2/security_group.py b/openstack/network/v2/security_group.py index 6a87eabc5..2ec91d5a1 100644 --- a/openstack/network/v2/security_group.py +++ b/openstack/network/v2/security_group.py @@ -23,8 +23,8 @@ class SecurityGroup(resource.Resource, tag.TagMixin): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/network/v2/security_group_rule.py b/openstack/network/v2/security_group_rule.py index 6bbf0d672..286a7adc7 100644 --- a/openstack/network/v2/security_group_rule.py +++ b/openstack/network/v2/security_group_rule.py @@ -22,8 +22,8 @@ class SecurityGroupRule(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = False + allow_fetch = True + allow_commit = False allow_delete = True allow_list = True diff --git a/openstack/network/v2/segment.py b/openstack/network/v2/segment.py index c81f21049..808d08cdb 100644 --- a/openstack/network/v2/segment.py +++ b/openstack/network/v2/segment.py @@ -22,8 +22,8 @@ class Segment(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/network/v2/service_profile.py b/openstack/network/v2/service_profile.py index 2707e0ac9..4b999ef8c 100644 --- a/openstack/network/v2/service_profile.py +++ b/openstack/network/v2/service_profile.py @@ -22,8 +22,8 @@ class ServiceProfile(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/network/v2/service_provider.py b/openstack/network/v2/service_provider.py index d6d93d2fc..9f0ec773a 100644 --- a/openstack/network/v2/service_provider.py +++ b/openstack/network/v2/service_provider.py @@ -21,8 +21,8 @@ class ServiceProvider(resource.Resource): # Capabilities allow_create = False - allow_get = False - allow_update = False + allow_fetch = False + allow_commit = False allow_delete = False allow_list = True diff --git a/openstack/network/v2/subnet.py b/openstack/network/v2/subnet.py index 47c58afb2..e611d0b6a 100644 --- a/openstack/network/v2/subnet.py +++ b/openstack/network/v2/subnet.py @@ -23,8 +23,8 @@ class Subnet(resource.Resource, tag.TagMixin): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/network/v2/subnet_pool.py b/openstack/network/v2/subnet_pool.py index 593cda01e..a8648fdf3 100644 --- a/openstack/network/v2/subnet_pool.py +++ b/openstack/network/v2/subnet_pool.py @@ -23,8 +23,8 @@ class SubnetPool(resource.Resource, tag.TagMixin): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/network/v2/trunk.py b/openstack/network/v2/trunk.py index 875a08790..14bb47e10 100644 --- a/openstack/network/v2/trunk.py +++ b/openstack/network/v2/trunk.py @@ -23,8 +23,8 @@ class Trunk(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/network/v2/vpn_service.py b/openstack/network/v2/vpn_service.py index c65ee121a..f40d20c8a 100644 --- a/openstack/network/v2/vpn_service.py +++ b/openstack/network/v2/vpn_service.py @@ -24,8 +24,8 @@ class VPNService(resource.Resource): # capabilities allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True diff --git a/openstack/object_store/v1/_base.py b/openstack/object_store/v1/_base.py index ca297b0bb..4c576da64 100644 --- a/openstack/object_store/v1/_base.py +++ b/openstack/object_store/v1/_base.py @@ -19,7 +19,7 @@ class BaseResource(resource.Resource): service = object_store_service.ObjectStoreService() - update_method = 'POST' + commit_method = 'POST' create_method = 'PUT' #: Metadata stored for this resource. *Type: dict* diff --git a/openstack/object_store/v1/account.py b/openstack/object_store/v1/account.py index ede5716d7..01dbd40cd 100644 --- a/openstack/object_store/v1/account.py +++ b/openstack/object_store/v1/account.py @@ -20,8 +20,8 @@ class Account(_base.BaseResource): base_path = "/" - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_head = True #: The total number of bytes that are stored in Object Storage for diff --git a/openstack/object_store/v1/container.py b/openstack/object_store/v1/container.py index 51452c882..8f90c730a 100644 --- a/openstack/object_store/v1/container.py +++ b/openstack/object_store/v1/container.py @@ -31,8 +31,8 @@ class Container(_base.BaseResource): pagination_key = 'X-Account-Container-Count' allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True allow_head = True diff --git a/openstack/object_store/v1/obj.py b/openstack/object_store/v1/obj.py index 2c305c9ef..595d0e4cd 100644 --- a/openstack/object_store/v1/obj.py +++ b/openstack/object_store/v1/obj.py @@ -35,8 +35,8 @@ class Object(_base.BaseResource): service = object_store_service.ObjectStoreService() allow_create = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True allow_list = True allow_head = True diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index f607de0e4..ccf9790fc 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -184,7 +184,7 @@ def get_stack_files(self, stack): stk = self._find(_stack.Stack, stack, ignore_missing=False) obj = _stack_files.StackFiles(stack_name=stk.name, stack_id=stk.id) - return obj.get(self) + return obj.fetch(self) def resources(self, stack, **query): """Return a generator of resources diff --git a/openstack/orchestration/v1/resource.py b/openstack/orchestration/v1/resource.py index 710e5dcbf..97184bb9c 100644 --- a/openstack/orchestration/v1/resource.py +++ b/openstack/orchestration/v1/resource.py @@ -26,7 +26,7 @@ class Resource(resource.Resource): allow_list = True allow_retrieve = False allow_delete = False - allow_update = False + allow_commit = False # Properties #: A list of dictionaries containing links relevant to the resource. diff --git a/openstack/orchestration/v1/software_config.py b/openstack/orchestration/v1/software_config.py index b8eb8e93d..a4f4b4fae 100644 --- a/openstack/orchestration/v1/software_config.py +++ b/openstack/orchestration/v1/software_config.py @@ -23,9 +23,9 @@ class SoftwareConfig(resource.Resource): # capabilities allow_create = True allow_list = True - allow_get = True + allow_fetch = True allow_delete = True - allow_update = False + allow_commit = False # Properties #: Configuration script or manifest that defines which configuration is diff --git a/openstack/orchestration/v1/software_deployment.py b/openstack/orchestration/v1/software_deployment.py index a76cc08ef..00e4acd0c 100644 --- a/openstack/orchestration/v1/software_deployment.py +++ b/openstack/orchestration/v1/software_deployment.py @@ -23,9 +23,9 @@ class SoftwareDeployment(resource.Resource): # capabilities allow_create = True allow_list = True - allow_get = True + allow_fetch = True allow_delete = True - allow_update = True + allow_commit = True # Properties #: The stack action that triggers this deployment resource. @@ -57,8 +57,8 @@ def create(self, session): return super(SoftwareDeployment, self).create( session, prepend_key=False) - def update(self, session): + def commit(self, session): # This overrides the default behavior of resource creation because # heat doesn't accept resource_key in its request. - return super(SoftwareDeployment, self).update( + return super(SoftwareDeployment, self).commit( session, prepend_key=False) diff --git a/openstack/orchestration/v1/stack.py b/openstack/orchestration/v1/stack.py index 8dad7b2c6..ea4c1c889 100644 --- a/openstack/orchestration/v1/stack.py +++ b/openstack/orchestration/v1/stack.py @@ -26,8 +26,8 @@ class Stack(resource.Resource): # capabilities allow_create = True allow_list = True - allow_get = True - allow_update = True + allow_fetch = True + allow_commit = True allow_delete = True # Properties @@ -81,10 +81,10 @@ def create(self, session): # heat doesn't accept resource_key in its request. return super(Stack, self).create(session, prepend_key=False) - def update(self, session): + def commit(self, session): # This overrides the default behavior of resource creation because # heat doesn't accept resource_key in its request. - return super(Stack, self).update(session, prepend_key=False, + return super(Stack, self).commit(session, prepend_key=False, has_body=False) def _action(self, session, body): @@ -96,9 +96,11 @@ def _action(self, session, body): def check(self, session): return self._action(session, {'check': ''}) - def get(self, session, requires_id=True, error_message=None): - stk = super(Stack, self).get(session, requires_id=requires_id, - error_message=error_message) + def fetch(self, session, requires_id=True, error_message=None): + stk = super(Stack, self).fetch( + session, + requires_id=requires_id, + error_message=error_message) if stk and stk.status in ['DELETE_COMPLETE', 'ADOPT_COMPLETE']: raise exceptions.NotFoundException( "No stack found for %s" % stk.id) @@ -110,6 +112,6 @@ class StackPreview(Stack): allow_create = True allow_list = False - allow_get = False - allow_update = False + allow_fetch = False + allow_commit = False allow_delete = False diff --git a/openstack/orchestration/v1/stack_environment.py b/openstack/orchestration/v1/stack_environment.py index dcf008f6e..2902818a9 100644 --- a/openstack/orchestration/v1/stack_environment.py +++ b/openstack/orchestration/v1/stack_environment.py @@ -22,9 +22,9 @@ class StackEnvironment(resource.Resource): # capabilities allow_create = False allow_list = False - allow_get = True + allow_fetch = True allow_delete = False - allow_update = False + allow_commit = False # Properties #: Name of the stack where the template is referenced. diff --git a/openstack/orchestration/v1/stack_files.py b/openstack/orchestration/v1/stack_files.py index e671688f2..364f50822 100644 --- a/openstack/orchestration/v1/stack_files.py +++ b/openstack/orchestration/v1/stack_files.py @@ -22,9 +22,9 @@ class StackFiles(resource.Resource): # capabilities allow_create = False allow_list = False - allow_get = True + allow_fetch = True allow_delete = False - allow_update = False + allow_commit = False # Properties #: Name of the stack where the template is referenced. @@ -32,7 +32,7 @@ class StackFiles(resource.Resource): #: ID of the stack where the template is referenced. stack_id = resource.URI('stack_id') - def get(self, session): + def fetch(self, session): # The stack files response contains a map of filenames and file # contents. request = self._prepare_request(requires_id=False) diff --git a/openstack/orchestration/v1/stack_template.py b/openstack/orchestration/v1/stack_template.py index cb8a6b896..0f0ab3545 100644 --- a/openstack/orchestration/v1/stack_template.py +++ b/openstack/orchestration/v1/stack_template.py @@ -22,9 +22,9 @@ class StackTemplate(resource.Resource): # capabilities allow_create = False allow_list = False - allow_get = True + allow_fetch = True allow_delete = False - allow_update = False + allow_commit = False # Properties #: Name of the stack where the template is referenced. diff --git a/openstack/orchestration/v1/template.py b/openstack/orchestration/v1/template.py index aa516d591..ec04b6cec 100644 --- a/openstack/orchestration/v1/template.py +++ b/openstack/orchestration/v1/template.py @@ -22,9 +22,9 @@ class Template(resource.Resource): # capabilities allow_create = False allow_list = False - allow_get = False + allow_fetch = False allow_delete = False - allow_update = False + allow_commit = False # Properties #: The description specified in the template diff --git a/openstack/proxy.py b/openstack/proxy.py index bc55b5218..7ccf2a997 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -169,7 +169,7 @@ def _update(self, resource_type, value, **attrs): :rtype: :class:`~openstack.resource.Resource` """ res = self._get_resource(resource_type, value, **attrs) - return res.update(self) + return res.commit(self) def _create(self, resource_type, **attrs): """Create a resource from attributes @@ -193,7 +193,7 @@ def _create(self, resource_type, **attrs): @_check_resource(strict=False) def _get(self, resource_type, value=None, requires_id=True, **attrs): - """Get a resource + """Fetch a resource :param resource_type: The type of resource to get. :type resource_type: :class:`~openstack.resource.Resource` @@ -207,12 +207,12 @@ def _get(self, resource_type, value=None, requires_id=True, **attrs): or :class:`~openstack.resource.Header` values on this resource. - :returns: The result of the ``get`` + :returns: The result of the ``fetch`` :rtype: :class:`~openstack.resource.Resource` """ res = self._get_resource(resource_type, value, **attrs) - return res.get( + return res.fetch( self, requires_id=requires_id, error_message="No {resource_type} found for {value}".format( resource_type=resource_type.__name__, value=value)) diff --git a/openstack/resource.py b/openstack/resource.py index 0cea03e57..d344a6db1 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -328,9 +328,9 @@ class Resource(object): #: Allow create operation for this resource. allow_create = False #: Allow get operation for this resource. - allow_get = False + allow_fetch = False #: Allow update operation for this resource. - allow_update = False + allow_commit = False #: Allow delete operation for this resource. allow_delete = False #: Allow list operation for this resource. @@ -338,8 +338,12 @@ class Resource(object): #: Allow head operation for this resource. allow_head = False - #: Method for udating a resource (PUT, PATCH, POST) - update_method = "PUT" + # TODO(mordred) Unused - here for transition with OSC. Remove once + # OSC no longer checks for allow_get + allow_get = True + + #: Method for committing a resource (PUT, PATCH, POST) + commit_method = "PUT" #: Method for creating a resource (POST, PUT) create_method = "POST" @@ -793,11 +797,11 @@ def _get_microversion_for(self, session, action): Subclasses can override this method if more complex logic is needed. :param session: :class`keystoneauth1.adapter.Adapter` - :param action: One of "get", "update", "create", "delete". Unused in + :param action: One of "fetch", "commit", "create", "delete". Unused in the base implementation. :return: microversion as string or ``None`` """ - if action not in ('get', 'update', 'create', 'delete'): + if action not in ('fetch', 'commit', 'create', 'delete'): raise ValueError('Invalid action: %s' % action) return self._get_microversion_for_list(session) @@ -807,7 +811,7 @@ def _assert_microversion_for(self, session, action, expected, """Enforce that the microversion for action satisfies the requirement. :param session: :class`keystoneauth1.adapter.Adapter` - :param action: One of "get", "update", "create", "delete". + :param action: One of "fetch", "commit", "create", "delete". :param expected: Expected microversion. :param error_message: Optional error message with details. Will be prepended to the message generated here. @@ -876,7 +880,7 @@ def create(self, session, prepend_key=True): self._translate_response(response) return self - def get(self, session, requires_id=True, error_message=None): + def fetch(self, session, requires_id=True, error_message=None): """Get a remote resource based on this instance. :param session: The session to use for making this request. @@ -885,14 +889,14 @@ def get(self, session, requires_id=True, error_message=None): should be part of the requested URI. :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if - :data:`Resource.allow_get` is not set to ``True``. + :data:`Resource.allow_fetch` is not set to ``True``. """ - if not self.allow_get: - raise exceptions.MethodNotSupported(self, "get") + if not self.allow_fetch: + raise exceptions.MethodNotSupported(self, "fetch") request = self._prepare_request(requires_id=requires_id) session = self._get_session(session) - microversion = self._get_microversion_for(session, 'get') + microversion = self._get_microversion_for(session, 'fetch') response = session.get(request.url, microversion=microversion) kwargs = {} if error_message: @@ -918,7 +922,7 @@ def head(self, session): request = self._prepare_request() session = self._get_session(session) - microversion = self._get_microversion_for(session, 'get') + microversion = self._get_microversion_for(session, 'fetch') response = session.head(request.url, headers={"Accept": ""}, microversion=microversion) @@ -927,8 +931,8 @@ def head(self, session): self._translate_response(response, has_body=False) return self - def update(self, session, prepend_key=True, has_body=True): - """Update the remote resource based on this instance. + def commit(self, session, prepend_key=True, has_body=True): + """Commit the state of the instance to the remote resource. :param session: The session to use for making this request. :type session: :class:`~keystoneauth1.adapter.Adapter` @@ -938,37 +942,37 @@ def update(self, session, prepend_key=True, has_body=True): :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if - :data:`Resource.allow_update` is not set to ``True``. + :data:`Resource.allow_commit` is not set to ``True``. """ - # The id cannot be dirty for an update + # The id cannot be dirty for an commit self._body._dirty.discard("id") - # Only try to update if we actually have anything to update. + # Only try to update if we actually have anything to commit. if not any([self._body.dirty, self._header.dirty]): return self - if not self.allow_update: - raise exceptions.MethodNotSupported(self, "update") + if not self.allow_commit: + raise exceptions.MethodNotSupported(self, "commit") request = self._prepare_request(prepend_key=prepend_key) session = self._get_session(session) - microversion = self._get_microversion_for(session, 'update') + microversion = self._get_microversion_for(session, 'commit') - if self.update_method == 'PATCH': + if self.commit_method == 'PATCH': response = session.patch( request.url, json=request.body, headers=request.headers, microversion=microversion) - elif self.update_method == 'POST': + elif self.commit_method == 'POST': response = session.post( request.url, json=request.body, headers=request.headers, microversion=microversion) - elif self.update_method == 'PUT': + elif self.commit_method == 'PUT': response = session.put( request.url, json=request.body, headers=request.headers, microversion=microversion) else: raise exceptions.ResourceFailure( - msg="Invalid update method: %s" % self.update_method) + msg="Invalid commit method: %s" % self.commit_method) self.microversion = microversion self._translate_response(response, has_body=has_body) @@ -982,7 +986,7 @@ def delete(self, session, error_message=None): :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if - :data:`Resource.allow_update` is not set to ``True``. + :data:`Resource.allow_commit` is not set to ``True``. """ if not self.allow_delete: raise exceptions.MethodNotSupported(self, "delete") @@ -1185,7 +1189,7 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): # Try to short-circuit by looking directly for a matching ID. try: match = cls.existing(id=name_or_id, **params) - return match.get(session) + return match.fetch(session) except exceptions.NotFoundException: pass @@ -1251,7 +1255,7 @@ def wait_for_status(session, resource, status, failures, interval=None, timeout=wait, message=msg, wait=interval): - resource = resource.get(session) + resource = resource.fetch(session) if not resource: raise exceptions.ResourceFailure( @@ -1293,7 +1297,7 @@ def wait_for_delete(session, resource, interval, wait): id=resource.id), wait=interval): try: - resource = resource.get(session) + resource = resource.fetch(session) if not resource: return orig_resource if resource.status.lower() == 'deleted': diff --git a/openstack/tests/unit/baremetal/test_version.py b/openstack/tests/unit/baremetal/test_version.py index d6f63b6bd..ddc807944 100644 --- a/openstack/tests/unit/baremetal/test_version.py +++ b/openstack/tests/unit/baremetal/test_version.py @@ -32,12 +32,12 @@ def test_basic(self): self.assertEqual('/', sot.base_path) self.assertEqual('baremetal', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) self.assertFalse(sot.allow_head) - self.assertEqual('PUT', sot.update_method) + self.assertEqual('PUT', sot.commit_method) self.assertEqual('POST', sot.create_method) def test_make_it(self): diff --git a/openstack/tests/unit/baremetal/v1/test_chassis.py b/openstack/tests/unit/baremetal/v1/test_chassis.py index 216f0e92c..76e4ef44b 100644 --- a/openstack/tests/unit/baremetal/v1/test_chassis.py +++ b/openstack/tests/unit/baremetal/v1/test_chassis.py @@ -52,11 +52,11 @@ def test_basic(self): self.assertEqual('/chassis', sot.base_path) self.assertEqual('baremetal', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertEqual('PATCH', sot.update_method) + self.assertEqual('PATCH', sot.commit_method) def test_instantiate(self): sot = chassis.Chassis(**FAKE) @@ -78,8 +78,8 @@ def test_basic(self): self.assertEqual('/chassis/detail', sot.base_path) self.assertEqual('baremetal', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/baremetal/v1/test_driver.py b/openstack/tests/unit/baremetal/v1/test_driver.py index dafd870a6..075ac0c14 100644 --- a/openstack/tests/unit/baremetal/v1/test_driver.py +++ b/openstack/tests/unit/baremetal/v1/test_driver.py @@ -52,8 +52,8 @@ def test_basic(self): self.assertEqual('/drivers', sot.base_path) self.assertEqual('baremetal', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 3ae7da564..3629b1adc 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -103,11 +103,11 @@ def test_basic(self): self.assertEqual('/nodes', sot.base_path) self.assertEqual('baremetal', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertEqual('PATCH', sot.update_method) + self.assertEqual('PATCH', sot.commit_method) def test_instantiate(self): sot = node.Node(**FAKE) @@ -157,8 +157,8 @@ def test_basic(self): self.assertEqual('/nodes/detail', sot.base_path) self.assertEqual('baremetal', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) @@ -202,59 +202,59 @@ def test_instantiate(self): @mock.patch('time.sleep', lambda _t: None) -@mock.patch.object(node.Node, 'get', autospec=True) +@mock.patch.object(node.Node, 'fetch', autospec=True) class TestNodeWaitForProvisionState(base.TestCase): def setUp(self): super(TestNodeWaitForProvisionState, self).setUp() self.node = node.Node(**FAKE) self.session = mock.Mock() - def test_success(self, mock_get): + def test_success(self, mock_fetch): def _get_side_effect(_self, session): self.node.provision_state = 'manageable' self.assertIs(session, self.session) - mock_get.side_effect = _get_side_effect + mock_fetch.side_effect = _get_side_effect node = self.node.wait_for_provision_state(self.session, 'manageable') self.assertIs(node, self.node) - def test_failure(self, mock_get): + def test_failure(self, mock_fetch): def _get_side_effect(_self, session): self.node.provision_state = 'deploy failed' self.assertIs(session, self.session) - mock_get.side_effect = _get_side_effect + mock_fetch.side_effect = _get_side_effect self.assertRaisesRegex(exceptions.SDKException, 'failure state "deploy failed"', self.node.wait_for_provision_state, self.session, 'manageable') - def test_enroll_as_failure(self, mock_get): + def test_enroll_as_failure(self, mock_fetch): def _get_side_effect(_self, session): self.node.provision_state = 'enroll' self.node.last_error = 'power failure' self.assertIs(session, self.session) - mock_get.side_effect = _get_side_effect + mock_fetch.side_effect = _get_side_effect self.assertRaisesRegex(exceptions.SDKException, 'failed to verify management credentials', self.node.wait_for_provision_state, self.session, 'manageable') - def test_timeout(self, mock_get): + def test_timeout(self, mock_fetch): self.assertRaises(exceptions.ResourceTimeout, self.node.wait_for_provision_state, self.session, 'manageable', timeout=0.001) - def test_not_abort_on_failed_state(self, mock_get): + def test_not_abort_on_failed_state(self, mock_fetch): def _get_side_effect(_self, session): self.node.provision_state = 'deploy failed' self.assertIs(session, self.session) - mock_get.side_effect = _get_side_effect + mock_fetch.side_effect = _get_side_effect self.assertRaises(exceptions.ResourceTimeout, self.node.wait_for_provision_state, @@ -262,7 +262,7 @@ def _get_side_effect(_self, session): abort_on_failed_state=False) -@mock.patch.object(node.Node, 'get', lambda self, session: self) +@mock.patch.object(node.Node, 'fetch', lambda self, session: self) @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) class TestNodeSetProvisionState(base.TestCase): diff --git a/openstack/tests/unit/baremetal/v1/test_port.py b/openstack/tests/unit/baremetal/v1/test_port.py index 466590146..61b9cce75 100644 --- a/openstack/tests/unit/baremetal/v1/test_port.py +++ b/openstack/tests/unit/baremetal/v1/test_port.py @@ -51,11 +51,11 @@ def test_basic(self): self.assertEqual('/ports', sot.base_path) self.assertEqual('baremetal', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertEqual('PATCH', sot.update_method) + self.assertEqual('PATCH', sot.commit_method) def test_instantiate(self): sot = port.PortDetail(**FAKE) @@ -82,8 +82,8 @@ def test_basic(self): self.assertEqual('/ports/detail', sot.base_path) self.assertEqual('baremetal', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/baremetal/v1/test_port_group.py b/openstack/tests/unit/baremetal/v1/test_port_group.py index 0f0432ff2..3cfe051fb 100644 --- a/openstack/tests/unit/baremetal/v1/test_port_group.py +++ b/openstack/tests/unit/baremetal/v1/test_port_group.py @@ -56,11 +56,11 @@ def test_basic(self): self.assertEqual('/portgroups', sot.base_path) self.assertEqual('baremetal', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertEqual('PATCH', sot.update_method) + self.assertEqual('PATCH', sot.commit_method) def test_instantiate(self): sot = port_group.PortGroup(**FAKE) @@ -87,8 +87,8 @@ def test_basic(self): self.assertEqual('/portgroups/detail', sot.base_path) self.assertEqual('baremetal', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/block_storage/v2/test_snapshot.py b/openstack/tests/unit/block_storage/v2/test_snapshot.py index 46a048d1a..a062ef9b9 100644 --- a/openstack/tests/unit/block_storage/v2/test_snapshot.py +++ b/openstack/tests/unit/block_storage/v2/test_snapshot.py @@ -46,8 +46,8 @@ def test_basic(self): self.assertEqual("snapshots", sot.resources_key) self.assertEqual("/snapshots", sot.base_path) self.assertEqual("block-storage", sot.service.service_type) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/block_storage/v2/test_type.py b/openstack/tests/unit/block_storage/v2/test_type.py index f23372d19..393d9cdd2 100644 --- a/openstack/tests/unit/block_storage/v2/test_type.py +++ b/openstack/tests/unit/block_storage/v2/test_type.py @@ -33,10 +33,10 @@ def test_basic(self): self.assertEqual("/types", sot.base_path) self.assertEqual("block-storage", sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_commit) def test_new(self): sot = type.Type.new(id=FAKE_ID) diff --git a/openstack/tests/unit/block_storage/v2/test_volume.py b/openstack/tests/unit/block_storage/v2/test_volume.py index a90c84cbd..e3edcd072 100644 --- a/openstack/tests/unit/block_storage/v2/test_volume.py +++ b/openstack/tests/unit/block_storage/v2/test_volume.py @@ -69,9 +69,9 @@ def test_basic(self): self.assertEqual("volumes", sot.resources_key) self.assertEqual("/volumes", sot.base_path) self.assertEqual("block-storage", sot.service.service_type) - self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/block_store/v2/test_stats.py b/openstack/tests/unit/block_store/v2/test_stats.py index 0ccb7eef6..ed87eba5e 100644 --- a/openstack/tests/unit/block_store/v2/test_stats.py +++ b/openstack/tests/unit/block_store/v2/test_stats.py @@ -41,7 +41,7 @@ def test_basic(self): sot.base_path) self.assertEqual("volume", sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) + self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/clustering/test_version.py b/openstack/tests/unit/clustering/test_version.py index 60d7b0c3f..f6fff193b 100644 --- a/openstack/tests/unit/clustering/test_version.py +++ b/openstack/tests/unit/clustering/test_version.py @@ -31,8 +31,8 @@ def test_basic(self): self.assertEqual('/', sot.base_path) self.assertEqual('clustering', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/clustering/v1/test_action.py b/openstack/tests/unit/clustering/v1/test_action.py index a4d462b95..e506fc203 100644 --- a/openstack/tests/unit/clustering/v1/test_action.py +++ b/openstack/tests/unit/clustering/v1/test_action.py @@ -54,7 +54,7 @@ def test_basic(self): self.assertEqual('actions', sot.resources_key) self.assertEqual('/actions', sot.base_path) self.assertEqual('clustering', sot.service.service_type) - self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_list) def test_instantiate(self): diff --git a/openstack/tests/unit/clustering/v1/test_build_info.py b/openstack/tests/unit/clustering/v1/test_build_info.py index 8d209d603..4532c2634 100644 --- a/openstack/tests/unit/clustering/v1/test_build_info.py +++ b/openstack/tests/unit/clustering/v1/test_build_info.py @@ -35,7 +35,7 @@ def test_basic(self): self.assertEqual('/build-info', sot.base_path) self.assertEqual('build_info', sot.resource_key) self.assertEqual('clustering', sot.service.service_type) - self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_fetch) def test_instantiate(self): sot = build_info.BuildInfo(**FAKE) diff --git a/openstack/tests/unit/clustering/v1/test_cluster.py b/openstack/tests/unit/clustering/v1/test_cluster.py index 52ee8f8c4..e79e397e3 100644 --- a/openstack/tests/unit/clustering/v1/test_cluster.py +++ b/openstack/tests/unit/clustering/v1/test_cluster.py @@ -77,8 +77,8 @@ def test_basic(self): self.assertEqual('/clusters', sot.base_path) self.assertEqual('clustering', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/clustering/v1/test_cluster_policy.py b/openstack/tests/unit/clustering/v1/test_cluster_policy.py index 5ce71ae3e..e6f3de548 100644 --- a/openstack/tests/unit/clustering/v1/test_cluster_policy.py +++ b/openstack/tests/unit/clustering/v1/test_cluster_policy.py @@ -38,7 +38,7 @@ def test_basic(self): self.assertEqual('/clusters/%(cluster_id)s/policies', sot.base_path) self.assertEqual('clustering', sot.service.service_type) - self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_list) self.assertDictEqual({"policy_name": "policy_name", diff --git a/openstack/tests/unit/clustering/v1/test_event.py b/openstack/tests/unit/clustering/v1/test_event.py index 47c80c7d2..8464c0ea3 100644 --- a/openstack/tests/unit/clustering/v1/test_event.py +++ b/openstack/tests/unit/clustering/v1/test_event.py @@ -42,7 +42,7 @@ def test_basic(self): self.assertEqual('events', sot.resources_key) self.assertEqual('/events', sot.base_path) self.assertEqual('clustering', sot.service.service_type) - self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_list) def test_instantiate(self): diff --git a/openstack/tests/unit/clustering/v1/test_node.py b/openstack/tests/unit/clustering/v1/test_node.py index 65868b728..6a454cb05 100644 --- a/openstack/tests/unit/clustering/v1/test_node.py +++ b/openstack/tests/unit/clustering/v1/test_node.py @@ -46,8 +46,8 @@ def test_basic(self): self.assertEqual('/nodes', sot.base_path) self.assertEqual('clustering', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) @@ -162,7 +162,7 @@ def test_basic(self): sot = node.NodeDetail() self.assertEqual('/nodes/%(node_id)s?show_details=True', sot.base_path) self.assertFalse(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertFalse(sot.allow_list) diff --git a/openstack/tests/unit/clustering/v1/test_policy.py b/openstack/tests/unit/clustering/v1/test_policy.py index 137e6214b..f565b6032 100644 --- a/openstack/tests/unit/clustering/v1/test_policy.py +++ b/openstack/tests/unit/clustering/v1/test_policy.py @@ -53,8 +53,8 @@ def test_basic(self): self.assertEqual('/policies', sot.base_path) self.assertEqual('clustering', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) @@ -83,7 +83,7 @@ def test_basic(self): self.assertEqual('/policies/validate', sot.base_path) self.assertEqual('clustering', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertFalse(sot.allow_list) diff --git a/openstack/tests/unit/clustering/v1/test_policy_type.py b/openstack/tests/unit/clustering/v1/test_policy_type.py index defdd6944..6ebf45db2 100644 --- a/openstack/tests/unit/clustering/v1/test_policy_type.py +++ b/openstack/tests/unit/clustering/v1/test_policy_type.py @@ -37,7 +37,7 @@ def test_basic(self): self.assertEqual('policy_types', sot.resources_key) self.assertEqual('/policy-types', sot.base_path) self.assertEqual('clustering', sot.service.service_type) - self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_list) def test_instantiate(self): diff --git a/openstack/tests/unit/clustering/v1/test_profile.py b/openstack/tests/unit/clustering/v1/test_profile.py index 047e5c037..4165f4cec 100644 --- a/openstack/tests/unit/clustering/v1/test_profile.py +++ b/openstack/tests/unit/clustering/v1/test_profile.py @@ -53,11 +53,11 @@ def test_basic(self): self.assertEqual('/profiles', sot.base_path) self.assertEqual('clustering', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertEqual('PATCH', sot.update_method) + self.assertEqual('PATCH', sot.commit_method) def test_instantiate(self): sot = profile.Profile(**FAKE) @@ -85,8 +85,8 @@ def test_basic(self): self.assertEqual('/profiles/validate', sot.base_path) self.assertEqual('clustering', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertFalse(sot.allow_list) - self.assertEqual('PUT', sot.update_method) + self.assertEqual('PUT', sot.commit_method) diff --git a/openstack/tests/unit/clustering/v1/test_profile_type.py b/openstack/tests/unit/clustering/v1/test_profile_type.py index 6c1ffd5ca..ee5e7fa43 100644 --- a/openstack/tests/unit/clustering/v1/test_profile_type.py +++ b/openstack/tests/unit/clustering/v1/test_profile_type.py @@ -38,7 +38,7 @@ def test_basic(self): self.assertEqual('profile_types', sot.resources_key) self.assertEqual('/profile-types', sot.base_path) self.assertEqual('clustering', sot.service.service_type) - self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_list) def test_instantiate(self): diff --git a/openstack/tests/unit/clustering/v1/test_receiver.py b/openstack/tests/unit/clustering/v1/test_receiver.py index ad6fedf8a..27b340c4d 100644 --- a/openstack/tests/unit/clustering/v1/test_receiver.py +++ b/openstack/tests/unit/clustering/v1/test_receiver.py @@ -52,8 +52,8 @@ def test_basic(self): self.assertEqual('/receivers', sot.base_path) self.assertEqual('clustering', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/compute/test_version.py b/openstack/tests/unit/compute/test_version.py index 45bf2d69a..2f460f260 100644 --- a/openstack/tests/unit/compute/test_version.py +++ b/openstack/tests/unit/compute/test_version.py @@ -32,8 +32,8 @@ def test_basic(self): self.assertEqual('/', sot.base_path) self.assertEqual('compute', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/compute/v2/test_extension.py b/openstack/tests/unit/compute/v2/test_extension.py index 86bfa96ec..1cc35f680 100644 --- a/openstack/tests/unit/compute/v2/test_extension.py +++ b/openstack/tests/unit/compute/v2/test_extension.py @@ -34,8 +34,8 @@ def test_basic(self): self.assertEqual('/extensions', sot.base_path) self.assertEqual('compute', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/compute/v2/test_flavor.py b/openstack/tests/unit/compute/v2/test_flavor.py index 745fddc13..9c5080c73 100644 --- a/openstack/tests/unit/compute/v2/test_flavor.py +++ b/openstack/tests/unit/compute/v2/test_flavor.py @@ -40,10 +40,10 @@ def test_basic(self): self.assertEqual('/flavors', sot.base_path) self.assertEqual('compute', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_commit) self.assertDictEqual({"sort_key": "sort_key", "sort_dir": "sort_dir", @@ -79,7 +79,7 @@ def test_detail(self): self.assertEqual('/flavors/detail', sot.base_path) self.assertEqual('compute', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/compute/v2/test_hypervisor.py b/openstack/tests/unit/compute/v2/test_hypervisor.py index 9ef80b4cd..ae369351e 100644 --- a/openstack/tests/unit/compute/v2/test_hypervisor.py +++ b/openstack/tests/unit/compute/v2/test_hypervisor.py @@ -50,7 +50,7 @@ def test_basic(self): self.assertEqual('hypervisors', sot.resources_key) self.assertEqual('/os-hypervisors', sot.base_path) self.assertEqual('compute', sot.service.service_type) - self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_list) def test_make_it(self): @@ -83,5 +83,5 @@ def test_detail(self): self.assertEqual('hypervisors', sot.resources_key) self.assertEqual('/os-hypervisors/detail', sot.base_path) self.assertEqual('compute', sot.service.service_type) - self.assertFalse(sot.allow_get) + self.assertFalse(sot.allow_fetch) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/compute/v2/test_image.py b/openstack/tests/unit/compute/v2/test_image.py index 158beba41..24f7f2a91 100644 --- a/openstack/tests/unit/compute/v2/test_image.py +++ b/openstack/tests/unit/compute/v2/test_image.py @@ -45,8 +45,8 @@ def test_basic(self): self.assertEqual('/images', sot.base_path) self.assertEqual('compute', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) @@ -74,8 +74,8 @@ def test_detail(self): self.assertEqual('/images/detail', sot.base_path) self.assertEqual('compute', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/compute/v2/test_keypair.py b/openstack/tests/unit/compute/v2/test_keypair.py index 54efd3871..c0e7d974a 100644 --- a/openstack/tests/unit/compute/v2/test_keypair.py +++ b/openstack/tests/unit/compute/v2/test_keypair.py @@ -31,8 +31,8 @@ def test_basic(self): self.assertEqual('/os-keypairs', sot.base_path) self.assertEqual('compute', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/compute/v2/test_limits.py b/openstack/tests/unit/compute/v2/test_limits.py index 90144875a..398d324e8 100644 --- a/openstack/tests/unit/compute/v2/test_limits.py +++ b/openstack/tests/unit/compute/v2/test_limits.py @@ -71,8 +71,8 @@ def test_basic(self): self.assertEqual("", sot.base_path) self.assertIsNone(sot.service) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertFalse(sot.allow_list) @@ -121,8 +121,8 @@ def test_basic(self): self.assertEqual("", sot.base_path) self.assertIsNone(sot.service) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertFalse(sot.allow_list) @@ -140,9 +140,9 @@ def test_basic(self): self.assertEqual("limits", sot.resource_key) self.assertEqual("/limits", sot.base_path) self.assertEqual("compute", sot.service.service_type) - self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_fetch) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertFalse(sot.allow_list) @@ -155,7 +155,7 @@ def test_get(self): resp.headers = {} resp.status_code = 200 - sot = limits.Limits().get(sess) + sot = limits.Limits().fetch(sess) self.assertEqual(ABSOLUTE_LIMITS["maxImageMeta"], sot.absolute.image_meta) diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index bb9cc6b28..45bf4ebc2 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -75,8 +75,8 @@ def test_basic(self): self.assertEqual('/servers', sot.base_path) self.assertEqual('compute', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) @@ -157,8 +157,8 @@ def test_detail(self): self.assertEqual('/servers/detail', sot.base_path) self.assertEqual('compute', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/compute/v2/test_server_group.py b/openstack/tests/unit/compute/v2/test_server_group.py index 224c5efca..d9c954dd7 100644 --- a/openstack/tests/unit/compute/v2/test_server_group.py +++ b/openstack/tests/unit/compute/v2/test_server_group.py @@ -32,8 +32,8 @@ def test_basic(self): self.assertEqual('/os-server-groups', sot.base_path) self.assertEqual('compute', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/compute/v2/test_server_interface.py b/openstack/tests/unit/compute/v2/test_server_interface.py index b0374f5b1..973923204 100644 --- a/openstack/tests/unit/compute/v2/test_server_interface.py +++ b/openstack/tests/unit/compute/v2/test_server_interface.py @@ -39,8 +39,8 @@ def test_basic(self): self.assertEqual('/servers/%(server_id)s/os-interface', sot.base_path) self.assertEqual('compute', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/compute/v2/test_server_ip.py b/openstack/tests/unit/compute/v2/test_server_ip.py index ab3fce4d3..66fed38df 100644 --- a/openstack/tests/unit/compute/v2/test_server_ip.py +++ b/openstack/tests/unit/compute/v2/test_server_ip.py @@ -31,8 +31,8 @@ def test_basic(self): self.assertEqual('/servers/%(server_id)s/ips', sot.base_path) self.assertEqual('compute', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/compute/v2/test_service.py b/openstack/tests/unit/compute/v2/test_service.py index 4ef25c812..094792a7f 100644 --- a/openstack/tests/unit/compute/v2/test_service.py +++ b/openstack/tests/unit/compute/v2/test_service.py @@ -42,9 +42,9 @@ def test_basic(self): self.assertEqual('services', sot.resources_key) self.assertEqual('/os-services', sot.base_path) self.assertEqual('compute', sot.service.service_type) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_list) - self.assertFalse(sot.allow_get) + self.assertFalse(sot.allow_fetch) def test_make_it(self): sot = service.Service(**EXAMPLE) diff --git a/openstack/tests/unit/compute/v2/test_volume_attachment.py b/openstack/tests/unit/compute/v2/test_volume_attachment.py index 30d999dbd..623a931e7 100644 --- a/openstack/tests/unit/compute/v2/test_volume_attachment.py +++ b/openstack/tests/unit/compute/v2/test_volume_attachment.py @@ -31,8 +31,8 @@ def test_basic(self): sot.base_path) self.assertEqual('compute', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) self.assertDictEqual({"limit": "limit", diff --git a/openstack/tests/unit/database/v1/test_database.py b/openstack/tests/unit/database/v1/test_database.py index d136bcbb3..477672a36 100644 --- a/openstack/tests/unit/database/v1/test_database.py +++ b/openstack/tests/unit/database/v1/test_database.py @@ -36,8 +36,8 @@ def test_basic(self): self.assertEqual('database', sot.service.service_type) self.assertTrue(sot.allow_list) self.assertTrue(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertTrue(sot.allow_delete) def test_make_it(self): diff --git a/openstack/tests/unit/database/v1/test_flavor.py b/openstack/tests/unit/database/v1/test_flavor.py index bb6b1c83a..82f850e6f 100644 --- a/openstack/tests/unit/database/v1/test_flavor.py +++ b/openstack/tests/unit/database/v1/test_flavor.py @@ -33,8 +33,8 @@ def test_basic(self): self.assertEqual('database', sot.service.service_type) self.assertTrue(sot.allow_list) self.assertFalse(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) def test_make_it(self): diff --git a/openstack/tests/unit/database/v1/test_instance.py b/openstack/tests/unit/database/v1/test_instance.py index c217ff062..8fd650ebb 100644 --- a/openstack/tests/unit/database/v1/test_instance.py +++ b/openstack/tests/unit/database/v1/test_instance.py @@ -40,8 +40,8 @@ def test_basic(self): self.assertEqual('/instances', sot.base_path) self.assertEqual('database', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/database/v1/test_user.py b/openstack/tests/unit/database/v1/test_user.py index 83a91a8ef..5752024d7 100644 --- a/openstack/tests/unit/database/v1/test_user.py +++ b/openstack/tests/unit/database/v1/test_user.py @@ -32,8 +32,8 @@ def test_basic(self): self.assertEqual('/instances/%(instance_id)s/users', sot.base_path) self.assertEqual('database', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/identity/test_version.py b/openstack/tests/unit/identity/test_version.py index a53a8b2c1..c18eaadef 100644 --- a/openstack/tests/unit/identity/test_version.py +++ b/openstack/tests/unit/identity/test_version.py @@ -33,8 +33,8 @@ def test_basic(self): self.assertEqual('/', sot.base_path) self.assertEqual('identity', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/identity/v2/test_extension.py b/openstack/tests/unit/identity/v2/test_extension.py index d7e768f71..557d3fb54 100644 --- a/openstack/tests/unit/identity/v2/test_extension.py +++ b/openstack/tests/unit/identity/v2/test_extension.py @@ -35,8 +35,8 @@ def test_basic(self): self.assertEqual('/extensions', sot.base_path) self.assertEqual('identity', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/identity/v2/test_role.py b/openstack/tests/unit/identity/v2/test_role.py index a1d2e060a..02912a9f2 100644 --- a/openstack/tests/unit/identity/v2/test_role.py +++ b/openstack/tests/unit/identity/v2/test_role.py @@ -32,8 +32,8 @@ def test_basic(self): self.assertEqual('/OS-KSADM/roles', sot.base_path) self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/identity/v2/test_tenant.py b/openstack/tests/unit/identity/v2/test_tenant.py index a0c63219e..3c7fd7bf4 100644 --- a/openstack/tests/unit/identity/v2/test_tenant.py +++ b/openstack/tests/unit/identity/v2/test_tenant.py @@ -32,8 +32,8 @@ def test_basic(self): self.assertEqual('/tenants', sot.base_path) self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/identity/v2/test_user.py b/openstack/tests/unit/identity/v2/test_user.py index 3fae75902..0c495c2e5 100644 --- a/openstack/tests/unit/identity/v2/test_user.py +++ b/openstack/tests/unit/identity/v2/test_user.py @@ -32,8 +32,8 @@ def test_basic(self): self.assertEqual('/users', sot.base_path) self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/identity/v3/test_credential.py b/openstack/tests/unit/identity/v3/test_credential.py index 8fc9676d3..7f6d2dc1d 100644 --- a/openstack/tests/unit/identity/v3/test_credential.py +++ b/openstack/tests/unit/identity/v3/test_credential.py @@ -33,11 +33,11 @@ def test_basic(self): self.assertEqual('/credentials', sot.base_path) self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertEqual('PATCH', sot.update_method) + self.assertEqual('PATCH', sot.commit_method) self.assertDictEqual( { diff --git a/openstack/tests/unit/identity/v3/test_domain.py b/openstack/tests/unit/identity/v3/test_domain.py index 2d75d9e3c..33ec294dd 100644 --- a/openstack/tests/unit/identity/v3/test_domain.py +++ b/openstack/tests/unit/identity/v3/test_domain.py @@ -33,11 +33,11 @@ def test_basic(self): self.assertEqual('/domains', sot.base_path) self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertEqual('PATCH', sot.update_method) + self.assertEqual('PATCH', sot.commit_method) self.assertDictEqual( { diff --git a/openstack/tests/unit/identity/v3/test_endpoint.py b/openstack/tests/unit/identity/v3/test_endpoint.py index 53a27d4c6..090c0b346 100644 --- a/openstack/tests/unit/identity/v3/test_endpoint.py +++ b/openstack/tests/unit/identity/v3/test_endpoint.py @@ -35,11 +35,11 @@ def test_basic(self): self.assertEqual('/endpoints', sot.base_path) self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertEqual('PATCH', sot.update_method) + self.assertEqual('PATCH', sot.commit_method) self.assertDictEqual( { 'interface': 'interface', diff --git a/openstack/tests/unit/identity/v3/test_group.py b/openstack/tests/unit/identity/v3/test_group.py index edb17f09c..515ee9c3b 100644 --- a/openstack/tests/unit/identity/v3/test_group.py +++ b/openstack/tests/unit/identity/v3/test_group.py @@ -32,11 +32,11 @@ def test_basic(self): self.assertEqual('/groups', sot.base_path) self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertEqual('PATCH', sot.update_method) + self.assertEqual('PATCH', sot.commit_method) self.assertDictEqual( { diff --git a/openstack/tests/unit/identity/v3/test_policy.py b/openstack/tests/unit/identity/v3/test_policy.py index 0533b9382..885d7b002 100644 --- a/openstack/tests/unit/identity/v3/test_policy.py +++ b/openstack/tests/unit/identity/v3/test_policy.py @@ -34,11 +34,11 @@ def test_basic(self): self.assertEqual('/policies', sot.base_path) self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertEqual('PATCH', sot.update_method) + self.assertEqual('PATCH', sot.commit_method) def test_make_it(self): sot = policy.Policy(**EXAMPLE) diff --git a/openstack/tests/unit/identity/v3/test_project.py b/openstack/tests/unit/identity/v3/test_project.py index 246adaf43..62d3437a3 100644 --- a/openstack/tests/unit/identity/v3/test_project.py +++ b/openstack/tests/unit/identity/v3/test_project.py @@ -35,11 +35,11 @@ def test_basic(self): self.assertEqual('/projects', sot.base_path) self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertEqual('PATCH', sot.update_method) + self.assertEqual('PATCH', sot.commit_method) self.assertDictEqual( { @@ -73,7 +73,7 @@ def test_basic(self): self.assertEqual('/users/%(user_id)s/projects', sot.base_path) self.assertEqual('identity', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/identity/v3/test_region.py b/openstack/tests/unit/identity/v3/test_region.py index b3c54f121..6657ca5cb 100644 --- a/openstack/tests/unit/identity/v3/test_region.py +++ b/openstack/tests/unit/identity/v3/test_region.py @@ -32,11 +32,11 @@ def test_basic(self): self.assertEqual('/regions', sot.base_path) self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertEqual('PATCH', sot.update_method) + self.assertEqual('PATCH', sot.commit_method) self.assertDictEqual( { diff --git a/openstack/tests/unit/identity/v3/test_role.py b/openstack/tests/unit/identity/v3/test_role.py index 957cfbb99..59d2a592a 100644 --- a/openstack/tests/unit/identity/v3/test_role.py +++ b/openstack/tests/unit/identity/v3/test_role.py @@ -31,8 +31,8 @@ def test_basic(self): self.assertEqual('/roles', sot.base_path) self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/identity/v3/test_service.py b/openstack/tests/unit/identity/v3/test_service.py index 0abceb6ea..d6716f3f6 100644 --- a/openstack/tests/unit/identity/v3/test_service.py +++ b/openstack/tests/unit/identity/v3/test_service.py @@ -34,11 +34,11 @@ def test_basic(self): self.assertEqual('/services', sot.base_path) self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertEqual('PATCH', sot.update_method) + self.assertEqual('PATCH', sot.commit_method) self.assertDictEqual( { diff --git a/openstack/tests/unit/identity/v3/test_trust.py b/openstack/tests/unit/identity/v3/test_trust.py index 258a6ce8e..7028e0af0 100644 --- a/openstack/tests/unit/identity/v3/test_trust.py +++ b/openstack/tests/unit/identity/v3/test_trust.py @@ -41,7 +41,7 @@ def test_basic(self): self.assertEqual('/OS-TRUST/trusts', sot.base_path) self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/identity/v3/test_user.py b/openstack/tests/unit/identity/v3/test_user.py index d09835747..f75eb6e7d 100644 --- a/openstack/tests/unit/identity/v3/test_user.py +++ b/openstack/tests/unit/identity/v3/test_user.py @@ -38,11 +38,11 @@ def test_basic(self): self.assertEqual('/users', sot.base_path) self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertEqual('PATCH', sot.update_method) + self.assertEqual('PATCH', sot.commit_method) self.assertDictEqual( { diff --git a/openstack/tests/unit/image/v1/test_image.py b/openstack/tests/unit/image/v1/test_image.py index d40e73535..256f9892c 100644 --- a/openstack/tests/unit/image/v1/test_image.py +++ b/openstack/tests/unit/image/v1/test_image.py @@ -45,8 +45,8 @@ def test_basic(self): self.assertEqual('/images', sot.base_path) self.assertEqual('image', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index d1367f3d0..b5a8b870c 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -115,8 +115,8 @@ def test_basic(self): self.assertEqual('/images', sot.base_path) self.assertEqual('image', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) @@ -333,7 +333,7 @@ def test_image_update(self): fake_img['name'] = 'fake_name' fake_img['new_property'] = 'fake_value' - sot.update(self.sess, **fake_img) + sot.commit(self.sess, **fake_img) url = 'images/' + IDENTIFIER self.sess.patch.assert_called_once() call = self.sess.patch.call_args diff --git a/openstack/tests/unit/image/v2/test_member.py b/openstack/tests/unit/image/v2/test_member.py index 8a3e35e50..e0e2a6fe0 100644 --- a/openstack/tests/unit/image/v2/test_member.py +++ b/openstack/tests/unit/image/v2/test_member.py @@ -33,8 +33,8 @@ def test_basic(self): self.assertEqual('image', sot.service.service_type) self.assertEqual('member', sot._alternate_id()) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index c8fe4afd5..14cff8271 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -58,14 +58,14 @@ def test_image_delete_ignore(self): @mock.patch("openstack.resource.Resource._translate_response") @mock.patch("openstack.proxy.Proxy._get") - @mock.patch("openstack.image.v2.image.Image.update") - def test_image_update(self, mock_update_image, mock_get_image, + @mock.patch("openstack.image.v2.image.Image.commit") + def test_image_update(self, mock_commit_image, mock_get_image, mock_transpose): original_image = image.Image(**EXAMPLE) mock_get_image.return_value = original_image EXAMPLE['name'] = 'fake_name' updated_image = image.Image(**EXAMPLE) - mock_update_image.return_value = updated_image.to_dict() + mock_commit_image.return_value = updated_image.to_dict() result = self.proxy.update_image(original_image, **updated_image.to_dict()) self.assertEqual('fake_name', result.get('name')) diff --git a/openstack/tests/unit/instance_ha/v1/test_host.py b/openstack/tests/unit/instance_ha/v1/test_host.py index 4aea38b56..2d20bdfdc 100644 --- a/openstack/tests/unit/instance_ha/v1/test_host.py +++ b/openstack/tests/unit/instance_ha/v1/test_host.py @@ -46,9 +46,9 @@ def test_basic(self): self.assertEqual("/segments/%(segment_id)s/hosts", sot.base_path) self.assertEqual("ha", sot.service.service_type) self.assertTrue(sot.allow_list) - self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertDictEqual({"failover_segment_id": "failover_segment_id", diff --git a/openstack/tests/unit/instance_ha/v1/test_notification.py b/openstack/tests/unit/instance_ha/v1/test_notification.py index b18da458c..e5566404f 100644 --- a/openstack/tests/unit/instance_ha/v1/test_notification.py +++ b/openstack/tests/unit/instance_ha/v1/test_notification.py @@ -46,9 +46,9 @@ def test_basic(self): self.assertEqual("/notifications", sot.base_path) self.assertEqual("ha", sot.service.service_type) self.assertTrue(sot.allow_list) - self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_create) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertDictEqual({"generated_since": "generated-since", diff --git a/openstack/tests/unit/instance_ha/v1/test_segment.py b/openstack/tests/unit/instance_ha/v1/test_segment.py index 87cf870f1..7659eb8e1 100644 --- a/openstack/tests/unit/instance_ha/v1/test_segment.py +++ b/openstack/tests/unit/instance_ha/v1/test_segment.py @@ -38,9 +38,9 @@ def test_basic(self): self.assertEqual("/segments", sot.base_path) self.assertEqual("ha", sot.service.service_type) self.assertTrue(sot.allow_list) - self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertDictEqual({"limit": "limit", diff --git a/openstack/tests/unit/key_manager/v1/test_container.py b/openstack/tests/unit/key_manager/v1/test_container.py index 5bd665ff1..ffc395a47 100644 --- a/openstack/tests/unit/key_manager/v1/test_container.py +++ b/openstack/tests/unit/key_manager/v1/test_container.py @@ -37,8 +37,8 @@ def test_basic(self): self.assertEqual('/containers', sot.base_path) self.assertEqual('key-manager', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/key_manager/v1/test_order.py b/openstack/tests/unit/key_manager/v1/test_order.py index 3c6590ff3..fd246e418 100644 --- a/openstack/tests/unit/key_manager/v1/test_order.py +++ b/openstack/tests/unit/key_manager/v1/test_order.py @@ -40,8 +40,8 @@ def test_basic(self): self.assertEqual('/orders', sot.base_path) self.assertEqual('key-manager', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/key_manager/v1/test_secret.py b/openstack/tests/unit/key_manager/v1/test_secret.py index 2db2d4cb5..ae4154c4d 100644 --- a/openstack/tests/unit/key_manager/v1/test_secret.py +++ b/openstack/tests/unit/key_manager/v1/test_secret.py @@ -44,8 +44,8 @@ def test_basic(self): self.assertEqual('/secrets', sot.base_path) self.assertEqual('key-manager', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) @@ -92,7 +92,7 @@ def test_get_no_payload(self): rv.json = mock.Mock(return_value=return_body) sess.get = mock.Mock(return_value=rv) - sot.get(sess) + sot.fetch(sess) sess.get.assert_called_once_with("secrets/id") @@ -110,7 +110,7 @@ def _test_payload(self, sot, metadata, content_type): sess = mock.Mock() sess.get = mock.Mock(side_effect=[metadata_response, payload_response]) - rv = sot.get(sess) + rv = sot.fetch(sess) sess.get.assert_has_calls( [mock.call("secrets/id",), diff --git a/openstack/tests/unit/load_balancer/test_health_monitor.py b/openstack/tests/unit/load_balancer/test_health_monitor.py index 771d30de9..0840f44a5 100644 --- a/openstack/tests/unit/load_balancer/test_health_monitor.py +++ b/openstack/tests/unit/load_balancer/test_health_monitor.py @@ -47,8 +47,8 @@ def test_basic(self): self.assertEqual('/v2.0/lbaas/healthmonitors', test_hm.base_path) self.assertEqual('load-balancer', test_hm.service.service_type) self.assertTrue(test_hm.allow_create) - self.assertTrue(test_hm.allow_get) - self.assertTrue(test_hm.allow_update) + self.assertTrue(test_hm.allow_fetch) + self.assertTrue(test_hm.allow_commit) self.assertTrue(test_hm.allow_delete) self.assertTrue(test_hm.allow_list) diff --git a/openstack/tests/unit/load_balancer/test_l7policy.py b/openstack/tests/unit/load_balancer/test_l7policy.py index 60492436b..91a62ff0a 100644 --- a/openstack/tests/unit/load_balancer/test_l7policy.py +++ b/openstack/tests/unit/load_balancer/test_l7policy.py @@ -43,8 +43,8 @@ def test_basic(self): self.assertEqual('/v2.0/lbaas/l7policies', test_l7_policy.base_path) self.assertEqual('load-balancer', test_l7_policy.service.service_type) self.assertTrue(test_l7_policy.allow_create) - self.assertTrue(test_l7_policy.allow_get) - self.assertTrue(test_l7_policy.allow_update) + self.assertTrue(test_l7_policy.allow_fetch) + self.assertTrue(test_l7_policy.allow_commit) self.assertTrue(test_l7_policy.allow_delete) self.assertTrue(test_l7_policy.allow_list) diff --git a/openstack/tests/unit/load_balancer/test_l7rule.py b/openstack/tests/unit/load_balancer/test_l7rule.py index 6afe925e2..b46f0b3c1 100644 --- a/openstack/tests/unit/load_balancer/test_l7rule.py +++ b/openstack/tests/unit/load_balancer/test_l7rule.py @@ -42,8 +42,8 @@ def test_basic(self): test_l7rule.base_path) self.assertEqual('load-balancer', test_l7rule.service.service_type) self.assertTrue(test_l7rule.allow_create) - self.assertTrue(test_l7rule.allow_get) - self.assertTrue(test_l7rule.allow_update) + self.assertTrue(test_l7rule.allow_fetch) + self.assertTrue(test_l7rule.allow_commit) self.assertTrue(test_l7rule.allow_delete) self.assertTrue(test_l7rule.allow_list) diff --git a/openstack/tests/unit/load_balancer/test_listener.py b/openstack/tests/unit/load_balancer/test_listener.py index 3eeebf212..b104d8e84 100644 --- a/openstack/tests/unit/load_balancer/test_listener.py +++ b/openstack/tests/unit/load_balancer/test_listener.py @@ -52,8 +52,8 @@ def test_basic(self): self.assertEqual('/v2.0/lbaas/listeners', test_listener.base_path) self.assertEqual('load-balancer', test_listener.service.service_type) self.assertTrue(test_listener.allow_create) - self.assertTrue(test_listener.allow_get) - self.assertTrue(test_listener.allow_update) + self.assertTrue(test_listener.allow_fetch) + self.assertTrue(test_listener.allow_commit) self.assertTrue(test_listener.allow_delete) self.assertTrue(test_listener.allow_list) diff --git a/openstack/tests/unit/load_balancer/test_load_balancer.py b/openstack/tests/unit/load_balancer/test_load_balancer.py index a6e47a21c..7a96179e4 100644 --- a/openstack/tests/unit/load_balancer/test_load_balancer.py +++ b/openstack/tests/unit/load_balancer/test_load_balancer.py @@ -50,10 +50,10 @@ def test_basic(self): self.assertEqual('load-balancer', test_load_balancer.service.service_type) self.assertTrue(test_load_balancer.allow_create) - self.assertTrue(test_load_balancer.allow_get) + self.assertTrue(test_load_balancer.allow_fetch) self.assertTrue(test_load_balancer.allow_delete) self.assertTrue(test_load_balancer.allow_list) - self.assertTrue(test_load_balancer.allow_update) + self.assertTrue(test_load_balancer.allow_commit) def test_make_it(self): test_load_balancer = load_balancer.LoadBalancer(**EXAMPLE) diff --git a/openstack/tests/unit/load_balancer/test_member.py b/openstack/tests/unit/load_balancer/test_member.py index cc36c68c9..0d40da371 100644 --- a/openstack/tests/unit/load_balancer/test_member.py +++ b/openstack/tests/unit/load_balancer/test_member.py @@ -42,8 +42,8 @@ def test_basic(self): test_member.base_path) self.assertEqual('load-balancer', test_member.service.service_type) self.assertTrue(test_member.allow_create) - self.assertTrue(test_member.allow_get) - self.assertTrue(test_member.allow_update) + self.assertTrue(test_member.allow_fetch) + self.assertTrue(test_member.allow_commit) self.assertTrue(test_member.allow_delete) self.assertTrue(test_member.allow_list) diff --git a/openstack/tests/unit/load_balancer/test_pool.py b/openstack/tests/unit/load_balancer/test_pool.py index 557dec3d5..fe8a81338 100644 --- a/openstack/tests/unit/load_balancer/test_pool.py +++ b/openstack/tests/unit/load_balancer/test_pool.py @@ -48,10 +48,10 @@ def test_basic(self): self.assertEqual('load-balancer', test_pool.service.service_type) self.assertTrue(test_pool.allow_create) - self.assertTrue(test_pool.allow_get) + self.assertTrue(test_pool.allow_fetch) self.assertTrue(test_pool.allow_delete) self.assertTrue(test_pool.allow_list) - self.assertTrue(test_pool.allow_update) + self.assertTrue(test_pool.allow_commit) def test_make_it(self): test_pool = pool.Pool(**EXAMPLE) diff --git a/openstack/tests/unit/load_balancer/test_version.py b/openstack/tests/unit/load_balancer/test_version.py index 101238544..e09a57faa 100644 --- a/openstack/tests/unit/load_balancer/test_version.py +++ b/openstack/tests/unit/load_balancer/test_version.py @@ -31,8 +31,8 @@ def test_basic(self): self.assertEqual('/', sot.base_path) self.assertEqual('load-balancer', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/message/test_version.py b/openstack/tests/unit/message/test_version.py index e9501e69a..f9a3669fb 100644 --- a/openstack/tests/unit/message/test_version.py +++ b/openstack/tests/unit/message/test_version.py @@ -31,8 +31,8 @@ def test_basic(self): self.assertEqual('/', sot.base_path) self.assertEqual('messaging', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/message/v2/test_claim.py b/openstack/tests/unit/message/v2/test_claim.py index 9b299ecbc..0ef520990 100644 --- a/openstack/tests/unit/message/v2/test_claim.py +++ b/openstack/tests/unit/message/v2/test_claim.py @@ -49,9 +49,9 @@ def test_basic(self): self.assertEqual("/queues/%(queue_name)s/claims", sot.base_path) self.assertEqual("messaging", sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_delete) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_commit) def test_make_it(self): sot = claim.Claim.new(**FAKE2) @@ -137,7 +137,7 @@ def test_get(self, mock_uuid): sot = claim.Claim(**FAKE1) sot._translate_response = mock.Mock() - res = sot.get(sess) + res = sot.fetch(sess) url = "queues/%(queue)s/claims/%(claim)s" % { "queue": FAKE1["queue_name"], "claim": FAKE1["id"]} @@ -156,7 +156,7 @@ def test_get_client_id_project_id_exist(self): sot = claim.Claim(**FAKE2) sot._translate_response = mock.Mock() - res = sot.get(sess) + res = sot.fetch(sess) url = "queues/%(queue)s/claims/%(claim)s" % { "queue": FAKE2["queue_name"], "claim": FAKE2["id"]} @@ -177,7 +177,7 @@ def test_update(self, mock_uuid): FAKE = copy.deepcopy(FAKE1) sot = claim.Claim(**FAKE1) - res = sot.update(sess) + res = sot.commit(sess) url = "queues/%(queue)s/claims/%(claim)s" % { "queue": FAKE.pop("queue_name"), "claim": FAKE["id"]} @@ -195,7 +195,7 @@ def test_update_client_id_project_id_exist(self): FAKE = copy.deepcopy(FAKE2) sot = claim.Claim(**FAKE2) - res = sot.update(sess) + res = sot.commit(sess) url = "queues/%(queue)s/claims/%(claim)s" % { "queue": FAKE.pop("queue_name"), "claim": FAKE["id"]} diff --git a/openstack/tests/unit/message/v2/test_message.py b/openstack/tests/unit/message/v2/test_message.py index 1a876a756..cbee7ea7d 100644 --- a/openstack/tests/unit/message/v2/test_message.py +++ b/openstack/tests/unit/message/v2/test_message.py @@ -54,7 +54,7 @@ def test_basic(self): self.assertEqual('/queues/%(queue_name)s/messages', sot.base_path) self.assertEqual('messaging', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) @@ -147,7 +147,7 @@ def test_get(self, mock_uuid): sot = message.Message(**FAKE1) sot._translate_response = mock.Mock() - res = sot.get(sess) + res = sot.fetch(sess) url = 'queues/%(queue)s/messages/%(message)s' % { 'queue': FAKE1['queue_name'], 'message': FAKE1['id']} @@ -166,13 +166,13 @@ def test_get_client_id_project_id_exist(self): sot = message.Message(**FAKE1) sot._translate_response = mock.Mock() - res = sot.get(sess) + res = sot.fetch(sess) url = 'queues/%(queue)s/messages/%(message)s' % { 'queue': FAKE2['queue_name'], 'message': FAKE2['id']} sot = message.Message(**FAKE2) sot._translate_response = mock.Mock() - res = sot.get(sess) + res = sot.fetch(sess) headers = {'Client-ID': 'OLD_CLIENT_ID', 'X-PROJECT-ID': 'OLD_PROJECT_ID'} sess.get.assert_called_with(url, diff --git a/openstack/tests/unit/message/v2/test_queue.py b/openstack/tests/unit/message/v2/test_queue.py index 187d400fb..c8277aac3 100644 --- a/openstack/tests/unit/message/v2/test_queue.py +++ b/openstack/tests/unit/message/v2/test_queue.py @@ -42,7 +42,7 @@ def test_basic(self): self.assertEqual('/queues', sot.base_path) self.assertEqual('messaging', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) @@ -106,7 +106,7 @@ def test_get(self, mock_uuid): sot = queue.Queue(**FAKE1) sot._translate_response = mock.Mock() - res = sot.get(sess) + res = sot.fetch(sess) url = 'queues/%s' % FAKE1['name'] headers = {'Client-ID': 'NEW_CLIENT_ID', @@ -124,7 +124,7 @@ def test_get_client_id_project_id_exist(self): sot = queue.Queue(**FAKE2) sot._translate_response = mock.Mock() - res = sot.get(sess) + res = sot.fetch(sess) url = 'queues/%s' % FAKE2['name'] headers = {'Client-ID': 'OLD_CLIENT_ID', diff --git a/openstack/tests/unit/message/v2/test_subscription.py b/openstack/tests/unit/message/v2/test_subscription.py index 5567c9cad..d465a7f05 100644 --- a/openstack/tests/unit/message/v2/test_subscription.py +++ b/openstack/tests/unit/message/v2/test_subscription.py @@ -55,7 +55,7 @@ def test_basic(self): self.assertEqual("/queues/%(queue_name)s/subscriptions", sot.base_path) self.assertEqual("messaging", sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) @@ -122,7 +122,7 @@ def test_get(self, mock_uuid): sot = subscription.Subscription(**FAKE1) sot._translate_response = mock.Mock() - res = sot.get(sess) + res = sot.fetch(sess) url = "queues/%(queue)s/subscriptions/%(subscription)s" % { "queue": FAKE1["queue_name"], "subscription": FAKE1["id"]} @@ -141,7 +141,7 @@ def test_get_client_id_project_id_exist(self): sot = subscription.Subscription(**FAKE2) sot._translate_response = mock.Mock() - res = sot.get(sess) + res = sot.fetch(sess) url = "queues/%(queue)s/subscriptions/%(subscription)s" % { "queue": FAKE2["queue_name"], "subscription": FAKE2["id"]} diff --git a/openstack/tests/unit/network/test_version.py b/openstack/tests/unit/network/test_version.py index 6f9dec05c..290d93dbf 100644 --- a/openstack/tests/unit/network/test_version.py +++ b/openstack/tests/unit/network/test_version.py @@ -31,8 +31,8 @@ def test_basic(self): self.assertEqual('/', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_address_scope.py b/openstack/tests/unit/network/v2/test_address_scope.py index a031284f4..4dd4cb0d3 100644 --- a/openstack/tests/unit/network/v2/test_address_scope.py +++ b/openstack/tests/unit/network/v2/test_address_scope.py @@ -33,8 +33,8 @@ def test_basic(self): self.assertEqual('/address-scopes', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_agent.py b/openstack/tests/unit/network/v2/test_agent.py index e279905ed..43fe862d2 100644 --- a/openstack/tests/unit/network/v2/test_agent.py +++ b/openstack/tests/unit/network/v2/test_agent.py @@ -43,8 +43,8 @@ def test_basic(self): self.assertEqual('/agents', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) @@ -130,8 +130,8 @@ def test_basic(self): self.assertEqual('dhcp-agent', net.resource_name) self.assertEqual('network', net.service.service_type) self.assertFalse(net.allow_create) - self.assertTrue(net.allow_get) - self.assertFalse(net.allow_update) + self.assertTrue(net.allow_fetch) + self.assertFalse(net.allow_commit) self.assertFalse(net.allow_delete) self.assertTrue(net.allow_list) @@ -147,6 +147,6 @@ def test_basic(self): self.assertEqual('network', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertTrue(sot.allow_retrieve) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_auto_allocated_topology.py b/openstack/tests/unit/network/v2/test_auto_allocated_topology.py index c9853cfa8..500837949 100644 --- a/openstack/tests/unit/network/v2/test_auto_allocated_topology.py +++ b/openstack/tests/unit/network/v2/test_auto_allocated_topology.py @@ -27,8 +27,8 @@ def test_basic(self): self.assertEqual('auto_allocated_topology', topo.resource_key) self.assertEqual('/auto-allocated-topology', topo.base_path) self.assertFalse(topo.allow_create) - self.assertTrue(topo.allow_get) - self.assertFalse(topo.allow_update) + self.assertTrue(topo.allow_fetch) + self.assertFalse(topo.allow_commit) self.assertTrue(topo.allow_delete) self.assertFalse(topo.allow_list) diff --git a/openstack/tests/unit/network/v2/test_availability_zone.py b/openstack/tests/unit/network/v2/test_availability_zone.py index 67576ca9c..21b3e04c5 100644 --- a/openstack/tests/unit/network/v2/test_availability_zone.py +++ b/openstack/tests/unit/network/v2/test_availability_zone.py @@ -32,8 +32,8 @@ def test_basic(self): self.assertEqual('/availability_zones', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_extension.py b/openstack/tests/unit/network/v2/test_extension.py index bd968746c..88fe38c39 100644 --- a/openstack/tests/unit/network/v2/test_extension.py +++ b/openstack/tests/unit/network/v2/test_extension.py @@ -33,8 +33,8 @@ def test_basic(self): self.assertEqual('/extensions', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_flavor.py b/openstack/tests/unit/network/v2/test_flavor.py index 024011b22..414edde7d 100644 --- a/openstack/tests/unit/network/v2/test_flavor.py +++ b/openstack/tests/unit/network/v2/test_flavor.py @@ -40,8 +40,8 @@ def test_basic(self): self.assertEqual('/flavors', flavors.base_path) self.assertEqual('network', flavors.service.service_type) self.assertTrue(flavors.allow_create) - self.assertTrue(flavors.allow_get) - self.assertTrue(flavors.allow_update) + self.assertTrue(flavors.allow_fetch) + self.assertTrue(flavors.allow_commit) self.assertTrue(flavors.allow_delete) self.assertTrue(flavors.allow_list) diff --git a/openstack/tests/unit/network/v2/test_floating_ip.py b/openstack/tests/unit/network/v2/test_floating_ip.py index b708dd5bc..3912733ff 100644 --- a/openstack/tests/unit/network/v2/test_floating_ip.py +++ b/openstack/tests/unit/network/v2/test_floating_ip.py @@ -47,8 +47,8 @@ def test_basic(self): self.assertEqual('/floatingips', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_health_monitor.py b/openstack/tests/unit/network/v2/test_health_monitor.py index 7018ee185..82454aff6 100644 --- a/openstack/tests/unit/network/v2/test_health_monitor.py +++ b/openstack/tests/unit/network/v2/test_health_monitor.py @@ -41,8 +41,8 @@ def test_basic(self): self.assertEqual('/lbaas/healthmonitors', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_listener.py b/openstack/tests/unit/network/v2/test_listener.py index 55db10895..2181046c2 100644 --- a/openstack/tests/unit/network/v2/test_listener.py +++ b/openstack/tests/unit/network/v2/test_listener.py @@ -41,8 +41,8 @@ def test_basic(self): self.assertEqual('/lbaas/listeners', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_load_balancer.py b/openstack/tests/unit/network/v2/test_load_balancer.py index 4283f4241..755bc3437 100644 --- a/openstack/tests/unit/network/v2/test_load_balancer.py +++ b/openstack/tests/unit/network/v2/test_load_balancer.py @@ -41,8 +41,8 @@ def test_basic(self): self.assertEqual('/lbaas/loadbalancers', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_metering_label.py b/openstack/tests/unit/network/v2/test_metering_label.py index af9d293d5..6e80b3dbb 100644 --- a/openstack/tests/unit/network/v2/test_metering_label.py +++ b/openstack/tests/unit/network/v2/test_metering_label.py @@ -33,8 +33,8 @@ def test_basic(self): self.assertEqual('/metering/metering-labels', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_metering_label_rule.py b/openstack/tests/unit/network/v2/test_metering_label_rule.py index b6b9a1317..d672ecb02 100644 --- a/openstack/tests/unit/network/v2/test_metering_label_rule.py +++ b/openstack/tests/unit/network/v2/test_metering_label_rule.py @@ -34,8 +34,8 @@ def test_basic(self): self.assertEqual('/metering/metering-label-rules', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_network.py b/openstack/tests/unit/network/v2/test_network.py index 787cdbef1..6974ebacc 100644 --- a/openstack/tests/unit/network/v2/test_network.py +++ b/openstack/tests/unit/network/v2/test_network.py @@ -54,8 +54,8 @@ def test_basic(self): self.assertEqual('/networks', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) @@ -129,7 +129,7 @@ def test_basic(self): self.assertEqual('dhcp-network', net.resource_name) self.assertEqual('network', net.service.service_type) self.assertFalse(net.allow_create) - self.assertTrue(net.allow_get) - self.assertFalse(net.allow_update) + self.assertTrue(net.allow_fetch) + self.assertFalse(net.allow_commit) self.assertFalse(net.allow_delete) self.assertTrue(net.allow_list) diff --git a/openstack/tests/unit/network/v2/test_network_ip_availability.py b/openstack/tests/unit/network/v2/test_network_ip_availability.py index 0dbecc1f2..90caacebb 100644 --- a/openstack/tests/unit/network/v2/test_network_ip_availability.py +++ b/openstack/tests/unit/network/v2/test_network_ip_availability.py @@ -48,8 +48,8 @@ def test_basic(self): self.assertEqual('network_name', sot.name_attribute) self.assertEqual('network', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_pool.py b/openstack/tests/unit/network/v2/test_pool.py index 68e949a9b..ac00277a4 100644 --- a/openstack/tests/unit/network/v2/test_pool.py +++ b/openstack/tests/unit/network/v2/test_pool.py @@ -49,8 +49,8 @@ def test_basic(self): self.assertEqual('/lbaas/pools', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_pool_member.py b/openstack/tests/unit/network/v2/test_pool_member.py index dd7e9aa01..742609295 100644 --- a/openstack/tests/unit/network/v2/test_pool_member.py +++ b/openstack/tests/unit/network/v2/test_pool_member.py @@ -37,8 +37,8 @@ def test_basic(self): self.assertEqual('/lbaas/pools/%(pool_id)s/members', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_port.py b/openstack/tests/unit/network/v2/test_port.py index 2fb241214..9984b3519 100644 --- a/openstack/tests/unit/network/v2/test_port.py +++ b/openstack/tests/unit/network/v2/test_port.py @@ -63,8 +63,8 @@ def test_basic(self): self.assertEqual('/ports', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_qos_bandwidth_limit_rule.py b/openstack/tests/unit/network/v2/test_qos_bandwidth_limit_rule.py index cfb77f332..57c53fbdd 100644 --- a/openstack/tests/unit/network/v2/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/unit/network/v2/test_qos_bandwidth_limit_rule.py @@ -35,8 +35,8 @@ def test_basic(self): sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_qos_dscp_marking_rule.py b/openstack/tests/unit/network/v2/test_qos_dscp_marking_rule.py index 11132fc70..6320c3ee2 100644 --- a/openstack/tests/unit/network/v2/test_qos_dscp_marking_rule.py +++ b/openstack/tests/unit/network/v2/test_qos_dscp_marking_rule.py @@ -32,8 +32,8 @@ def test_basic(self): sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_qos_minimum_bandwidth_rule.py b/openstack/tests/unit/network/v2/test_qos_minimum_bandwidth_rule.py index 090e8d198..2e7c48f42 100644 --- a/openstack/tests/unit/network/v2/test_qos_minimum_bandwidth_rule.py +++ b/openstack/tests/unit/network/v2/test_qos_minimum_bandwidth_rule.py @@ -34,8 +34,8 @@ def test_basic(self): sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_qos_policy.py b/openstack/tests/unit/network/v2/test_qos_policy.py index 11304db34..b2c96e2a0 100644 --- a/openstack/tests/unit/network/v2/test_qos_policy.py +++ b/openstack/tests/unit/network/v2/test_qos_policy.py @@ -36,8 +36,8 @@ def test_basic(self): self.assertEqual('/qos/policies', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_qos_rule_type.py b/openstack/tests/unit/network/v2/test_qos_rule_type.py index eb3c12d4b..58df91540 100644 --- a/openstack/tests/unit/network/v2/test_qos_rule_type.py +++ b/openstack/tests/unit/network/v2/test_qos_rule_type.py @@ -44,8 +44,8 @@ def test_basic(self): self.assertEqual('/qos/rule-types', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_quota.py b/openstack/tests/unit/network/v2/test_quota.py index 00a54f999..81c557dd0 100644 --- a/openstack/tests/unit/network/v2/test_quota.py +++ b/openstack/tests/unit/network/v2/test_quota.py @@ -44,8 +44,8 @@ def test_basic(self): self.assertEqual('/quotas', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) @@ -91,8 +91,8 @@ def test_basic(self): self.assertEqual('/quotas/%(project)s/default', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertFalse(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_rbac_policy.py b/openstack/tests/unit/network/v2/test_rbac_policy.py index f42237a7f..af8aa5d2c 100644 --- a/openstack/tests/unit/network/v2/test_rbac_policy.py +++ b/openstack/tests/unit/network/v2/test_rbac_policy.py @@ -33,8 +33,8 @@ def test_basic(self): self.assertEqual('/rbac-policies', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_router.py b/openstack/tests/unit/network/v2/test_router.py index 2bc12d26e..5ba0cc272 100644 --- a/openstack/tests/unit/network/v2/test_router.py +++ b/openstack/tests/unit/network/v2/test_router.py @@ -67,8 +67,8 @@ def test_basic(self): self.assertEqual('/routers', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) @@ -216,6 +216,6 @@ def test_basic(self): self.assertEqual('network', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertTrue(sot.allow_retrieve) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_security_group.py b/openstack/tests/unit/network/v2/test_security_group.py index 500f090d1..d08a89019 100644 --- a/openstack/tests/unit/network/v2/test_security_group.py +++ b/openstack/tests/unit/network/v2/test_security_group.py @@ -71,8 +71,8 @@ def test_basic(self): self.assertEqual('/security-groups', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_security_group_rule.py b/openstack/tests/unit/network/v2/test_security_group_rule.py index 5292099c9..fe347b686 100644 --- a/openstack/tests/unit/network/v2/test_security_group_rule.py +++ b/openstack/tests/unit/network/v2/test_security_group_rule.py @@ -42,8 +42,8 @@ def test_basic(self): self.assertEqual('/security-group-rules', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_segment.py b/openstack/tests/unit/network/v2/test_segment.py index e651fd6d6..25cb4c68d 100644 --- a/openstack/tests/unit/network/v2/test_segment.py +++ b/openstack/tests/unit/network/v2/test_segment.py @@ -36,8 +36,8 @@ def test_basic(self): self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_service_profile.py b/openstack/tests/unit/network/v2/test_service_profile.py index e5961b4b9..2e6e26f24 100644 --- a/openstack/tests/unit/network/v2/test_service_profile.py +++ b/openstack/tests/unit/network/v2/test_service_profile.py @@ -35,8 +35,8 @@ def test_basic(self): self.assertEqual('service_profiles', service_profiles.resources_key) self.assertEqual('/service_profiles', service_profiles.base_path) self.assertTrue(service_profiles.allow_create) - self.assertTrue(service_profiles.allow_get) - self.assertTrue(service_profiles.allow_update) + self.assertTrue(service_profiles.allow_fetch) + self.assertTrue(service_profiles.allow_commit) self.assertTrue(service_profiles.allow_delete) self.assertTrue(service_profiles.allow_list) diff --git a/openstack/tests/unit/network/v2/test_service_provider.py b/openstack/tests/unit/network/v2/test_service_provider.py index e3c3448cc..25635a449 100644 --- a/openstack/tests/unit/network/v2/test_service_provider.py +++ b/openstack/tests/unit/network/v2/test_service_provider.py @@ -31,8 +31,8 @@ def test_basic(self): self.assertEqual('/service-providers', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_subnet.py b/openstack/tests/unit/network/v2/test_subnet.py index 9d1224405..a076a8fbc 100644 --- a/openstack/tests/unit/network/v2/test_subnet.py +++ b/openstack/tests/unit/network/v2/test_subnet.py @@ -49,8 +49,8 @@ def test_basic(self): self.assertEqual('/subnets', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_subnet_pool.py b/openstack/tests/unit/network/v2/test_subnet_pool.py index 7751820c8..975316bf6 100644 --- a/openstack/tests/unit/network/v2/test_subnet_pool.py +++ b/openstack/tests/unit/network/v2/test_subnet_pool.py @@ -44,8 +44,8 @@ def test_basic(self): self.assertEqual('/subnetpools', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_trunk.py b/openstack/tests/unit/network/v2/test_trunk.py index 47abc14a9..ab0674857 100644 --- a/openstack/tests/unit/network/v2/test_trunk.py +++ b/openstack/tests/unit/network/v2/test_trunk.py @@ -40,8 +40,8 @@ def test_basic(self): self.assertEqual('/trunks', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/network/v2/test_vpn_service.py b/openstack/tests/unit/network/v2/test_vpn_service.py index 662f461ef..981e1186b 100644 --- a/openstack/tests/unit/network/v2/test_vpn_service.py +++ b/openstack/tests/unit/network/v2/test_vpn_service.py @@ -38,8 +38,8 @@ def test_basic(self): self.assertEqual('/vpn/vpnservices', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/object_store/v1/test_account.py b/openstack/tests/unit/object_store/v1/test_account.py index 50f511a0f..4851fb125 100644 --- a/openstack/tests/unit/object_store/v1/test_account.py +++ b/openstack/tests/unit/object_store/v1/test_account.py @@ -37,9 +37,9 @@ def test_basic(self): self.assertIsNone(sot.id) self.assertEqual('/', sot.base_path) self.assertEqual('object-store', sot.service.service_type) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_head) - self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_fetch) self.assertFalse(sot.allow_delete) self.assertFalse(sot.allow_list) self.assertFalse(sot.allow_create) diff --git a/openstack/tests/unit/object_store/v1/test_container.py b/openstack/tests/unit/object_store/v1/test_container.py index 84fa993ce..88075beb6 100644 --- a/openstack/tests/unit/object_store/v1/test_container.py +++ b/openstack/tests/unit/object_store/v1/test_container.py @@ -48,9 +48,9 @@ def test_basic(self): self.assertEqual('name', sot._alternate_id()) self.assertEqual('/', sot.base_path) self.assertEqual('object-store', sot.service.service_type) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) self.assertTrue(sot.allow_head) @@ -150,9 +150,9 @@ def test_create(self): sot = container.Container.new(name=self.container) self._test_create_update(sot, sot.create, 'PUT') - def test_update(self): + def test_commit(self): sot = container.Container.new(name=self.container) - self._test_create_update(sot, sot.update, 'POST') + self._test_create_update(sot, sot.commit, 'POST') def test_to_dict_recursion(self): # This test is verifying that circular aliases in a Resource @@ -182,7 +182,7 @@ def test_create_no_headers(self): self._test_no_headers(sot, sot.create, 'PUT') self.assert_calls() - def test_update_no_headers(self): + def test_commit_no_headers(self): sot = container.Container.new(name=self.container) - self._test_no_headers(sot, sot.update, 'POST') + self._test_no_headers(sot, sot.commit, 'POST') self.assert_no_calls() diff --git a/openstack/tests/unit/object_store/v1/test_obj.py b/openstack/tests/unit/object_store/v1/test_obj.py index 640523eba..6ffb93b98 100644 --- a/openstack/tests/unit/object_store/v1/test_obj.py +++ b/openstack/tests/unit/object_store/v1/test_obj.py @@ -62,9 +62,9 @@ def test_basic(self): self.assertEqual('name', sot._alternate_id()) self.assertEqual('/%(container)s', sot.base_path) self.assertEqual('object-store', sot.service.service_type) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) self.assertTrue(sot.allow_head) diff --git a/openstack/tests/unit/orchestration/test_version.py b/openstack/tests/unit/orchestration/test_version.py index 464286dbe..7432d005b 100644 --- a/openstack/tests/unit/orchestration/test_version.py +++ b/openstack/tests/unit/orchestration/test_version.py @@ -31,8 +31,8 @@ def test_basic(self): self.assertEqual('/', sot.base_path) self.assertEqual('orchestration', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index 9fe788d6f..0fc39487b 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -110,33 +110,33 @@ def test_get_stack_environment_with_stack_object(self): 'stack_name': stack_name, 'stack_id': stack_id}) - @mock.patch.object(stack_files.StackFiles, 'get') + @mock.patch.object(stack_files.StackFiles, 'fetch') @mock.patch.object(stack.Stack, 'find') - def test_get_stack_files_with_stack_identity(self, mock_find, mock_get): + def test_get_stack_files_with_stack_identity(self, mock_find, mock_fetch): stack_id = '1234' stack_name = 'test_stack' stk = stack.Stack(id=stack_id, name=stack_name) mock_find.return_value = stk - mock_get.return_value = {'file': 'content'} + mock_fetch.return_value = {'file': 'content'} res = self.proxy.get_stack_files('IDENTITY') self.assertEqual({'file': 'content'}, res) mock_find.assert_called_once_with(mock.ANY, 'IDENTITY', ignore_missing=False) - mock_get.assert_called_once_with(self.proxy) + mock_fetch.assert_called_once_with(self.proxy) - @mock.patch.object(stack_files.StackFiles, 'get') - def test_get_stack_files_with_stack_object(self, mock_get): + @mock.patch.object(stack_files.StackFiles, 'fetch') + def test_get_stack_files_with_stack_object(self, mock_fetch): stack_id = '1234' stack_name = 'test_stack' stk = stack.Stack(id=stack_id, name=stack_name) - mock_get.return_value = {'file': 'content'} + mock_fetch.return_value = {'file': 'content'} res = self.proxy.get_stack_files(stk) self.assertEqual({'file': 'content'}, res) - mock_get.assert_called_once_with(self.proxy) + mock_fetch.assert_called_once_with(self.proxy) @mock.patch.object(stack.Stack, 'find') def test_get_stack_template_with_stack_identity(self, mock_find): diff --git a/openstack/tests/unit/orchestration/v1/test_resource.py b/openstack/tests/unit/orchestration/v1/test_resource.py index 9e1968fe0..1e6a01f7c 100644 --- a/openstack/tests/unit/orchestration/v1/test_resource.py +++ b/openstack/tests/unit/orchestration/v1/test_resource.py @@ -47,7 +47,7 @@ def test_basic(self): self.assertEqual('orchestration', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_retrieve) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/orchestration/v1/test_software_config.py b/openstack/tests/unit/orchestration/v1/test_software_config.py index 02dad3152..99c4f2de2 100644 --- a/openstack/tests/unit/orchestration/v1/test_software_config.py +++ b/openstack/tests/unit/orchestration/v1/test_software_config.py @@ -38,8 +38,8 @@ def test_basic(self): self.assertEqual('/software_configs', sot.base_path) self.assertEqual('orchestration', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/orchestration/v1/test_software_deployment.py b/openstack/tests/unit/orchestration/v1/test_software_deployment.py index fb242ebeb..1ffb33974 100644 --- a/openstack/tests/unit/orchestration/v1/test_software_deployment.py +++ b/openstack/tests/unit/orchestration/v1/test_software_deployment.py @@ -38,8 +38,8 @@ def test_basic(self): self.assertEqual('/software_deployments', sot.base_path) self.assertEqual('orchestration', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/orchestration/v1/test_stack.py b/openstack/tests/unit/orchestration/v1/test_stack.py index dc1a19b30..e7e7d506f 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack.py +++ b/openstack/tests/unit/orchestration/v1/test_stack.py @@ -60,8 +60,8 @@ def test_basic(self): self.assertEqual('/stacks', sot.base_path) self.assertEqual('orchestration', sot.service.service_type) self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) @@ -97,16 +97,16 @@ def test_create(self, mock_create): mock_create.assert_called_once_with(sess, prepend_key=False) self.assertEqual(mock_create.return_value, res) - @mock.patch.object(resource.Resource, 'update') - def test_update(self, mock_update): + @mock.patch.object(resource.Resource, 'commit') + def test_commit(self, mock_commit): sess = mock.Mock() sot = stack.Stack(FAKE) - res = sot.update(sess) + res = sot.commit(sess) - mock_update.assert_called_once_with(sess, prepend_key=False, + mock_commit.assert_called_once_with(sess, prepend_key=False, has_body=False) - self.assertEqual(mock_update.return_value, res) + self.assertEqual(mock_commit.return_value, res) def test_check(self): sess = mock.Mock() @@ -118,22 +118,22 @@ def test_check(self): sot._action.assert_called_with(sess, body) - @mock.patch.object(resource.Resource, 'get') - def test_get(self, mock_get): + @mock.patch.object(resource.Resource, 'fetch') + def test_fetch(self, mock_fetch): sess = mock.Mock() sot = stack.Stack(**FAKE) deleted_stack = mock.Mock(id=FAKE_ID, status='DELETE_COMPLETE') normal_stack = mock.Mock(status='CREATE_COMPLETE') - mock_get.side_effect = [ + mock_fetch.side_effect = [ normal_stack, exceptions.NotFoundException(message='oops'), deleted_stack, ] - self.assertEqual(normal_stack, sot.get(sess)) - ex = self.assertRaises(exceptions.NotFoundException, sot.get, sess) + self.assertEqual(normal_stack, sot.fetch(sess)) + ex = self.assertRaises(exceptions.NotFoundException, sot.fetch, sess) self.assertEqual('oops', six.text_type(ex)) - ex = self.assertRaises(exceptions.NotFoundException, sot.get, sess) + ex = self.assertRaises(exceptions.NotFoundException, sot.fetch, sess) self.assertEqual('No stack found for %s' % FAKE_ID, six.text_type(ex)) @@ -146,6 +146,6 @@ def test_basic(self): self.assertEqual('/stacks/preview', sot.base_path) self.assertTrue(sot.allow_create) self.assertFalse(sot.allow_list) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) diff --git a/openstack/tests/unit/orchestration/v1/test_stack_environment.py b/openstack/tests/unit/orchestration/v1/test_stack_environment.py index 692dc93cc..2864a4a45 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack_environment.py +++ b/openstack/tests/unit/orchestration/v1/test_stack_environment.py @@ -42,8 +42,8 @@ def test_basic(self): sot = se.StackEnvironment() self.assertEqual('orchestration', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertFalse(sot.allow_list) diff --git a/openstack/tests/unit/orchestration/v1/test_stack_files.py b/openstack/tests/unit/orchestration/v1/test_stack_files.py index 4c4f06f9c..f43a7d936 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack_files.py +++ b/openstack/tests/unit/orchestration/v1/test_stack_files.py @@ -28,8 +28,8 @@ def test_basic(self): sot = sf.StackFiles() self.assertEqual('orchestration', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertFalse(sot.allow_list) @@ -55,7 +55,7 @@ def test_get(self, mock_prepare_request): 'stack_id': FAKE['stack_id']}) mock_prepare_request.return_value = req - files = sot.get(sess) + files = sot.fetch(sess) sess.get.assert_called_once_with(req.url) self.assertEqual({'file': 'file-content'}, files) diff --git a/openstack/tests/unit/orchestration/v1/test_stack_template.py b/openstack/tests/unit/orchestration/v1/test_stack_template.py index 7ea88142b..51c03cbf4 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack_template.py +++ b/openstack/tests/unit/orchestration/v1/test_stack_template.py @@ -42,8 +42,8 @@ def test_basic(self): sot = stack_template.StackTemplate() self.assertEqual('orchestration', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertTrue(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertFalse(sot.allow_list) diff --git a/openstack/tests/unit/orchestration/v1/test_template.py b/openstack/tests/unit/orchestration/v1/test_template.py index 7cc91f3da..966560239 100644 --- a/openstack/tests/unit/orchestration/v1/test_template.py +++ b/openstack/tests/unit/orchestration/v1/test_template.py @@ -37,8 +37,8 @@ def test_basic(self): sot = template.Template() self.assertEqual('orchestration', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertFalse(sot.allow_list) diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index 54bc6be19..e1520ed97 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -24,7 +24,7 @@ class DeleteableResource(resource.Resource): class UpdateableResource(resource.Resource): - allow_update = True + allow_commit = True class CreateableResource(resource.Resource): @@ -241,7 +241,7 @@ def setUp(self): self.fake_result = "fake_result" self.res = mock.Mock(spec=UpdateableResource) - self.res.update = mock.Mock(return_value=self.fake_result) + self.res.commit = mock.Mock(return_value=self.fake_result) self.sot = proxy.Proxy(self.session) @@ -254,13 +254,13 @@ def test_update_resource(self): self.assertEqual(rv, self.fake_result) self.res._update.assert_called_once_with(**self.attrs) - self.res.update.assert_called_once_with(self.sot) + self.res.commit.assert_called_once_with(self.sot) def test_update_id(self): rv = self.sot._update(UpdateableResource, self.fake_id, **self.attrs) self.assertEqual(rv, self.fake_result) - self.res.update.assert_called_once_with(self.sot) + self.res.commit.assert_called_once_with(self.sot) class TestProxyCreate(base.TestCase): @@ -299,7 +299,7 @@ def setUp(self): self.fake_result = "fake_result" self.res = mock.Mock(spec=RetrieveableResource) self.res.id = self.fake_id - self.res.get = mock.Mock(return_value=self.fake_result) + self.res.fetch = mock.Mock(return_value=self.fake_result) self.sot = proxy.Proxy(self.session) RetrieveableResource.new = mock.Mock(return_value=self.res) @@ -307,8 +307,9 @@ def setUp(self): def test_get_resource(self): rv = self.sot._get(RetrieveableResource, self.res) - self.res.get.assert_called_with(self.sot, requires_id=True, - error_message=mock.ANY) + self.res.fetch.assert_called_with( + self.sot, requires_id=True, + error_message=mock.ANY) self.assertEqual(rv, self.fake_result) def test_get_resource_with_args(self): @@ -316,20 +317,22 @@ def test_get_resource_with_args(self): rv = self.sot._get(RetrieveableResource, self.res, **args) self.res._update.assert_called_once_with(**args) - self.res.get.assert_called_with(self.sot, requires_id=True, - error_message=mock.ANY) + self.res.fetch.assert_called_with( + self.sot, requires_id=True, + error_message=mock.ANY) self.assertEqual(rv, self.fake_result) def test_get_id(self): rv = self.sot._get(RetrieveableResource, self.fake_id) RetrieveableResource.new.assert_called_with(id=self.fake_id) - self.res.get.assert_called_with(self.sot, requires_id=True, - error_message=mock.ANY) + self.res.fetch.assert_called_with( + self.sot, requires_id=True, + error_message=mock.ANY) self.assertEqual(rv, self.fake_result) def test_get_not_found(self): - self.res.get.side_effect = exceptions.NotFoundException( + self.res.fetch.side_effect = exceptions.NotFoundException( message="test", http_status=404) self.assertRaisesRegex( diff --git a/openstack/tests/unit/test_proxy_base.py b/openstack/tests/unit/test_proxy_base.py index ed2d79948..c2c0dab97 100644 --- a/openstack/tests/unit/test_proxy_base.py +++ b/openstack/tests/unit/test_proxy_base.py @@ -145,8 +145,9 @@ def verify_get_overrided(self, proxy, resource_type, patch_target): with mock.patch(patch_target, autospec=True) as res: proxy._get_resource = mock.Mock(return_value=res) proxy._get(resource_type) - res.get.assert_called_once_with(proxy, requires_id=True, - error_message=mock.ANY) + res.fetch.assert_called_once_with( + proxy, requires_id=True, + error_message=mock.ANY) def verify_head(self, test_method, resource_type, mock_method="openstack.proxy.Proxy._head", diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index df5f70eb5..de7e6d950 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -421,12 +421,12 @@ def test_initialize_basic(self): self.assertEqual(uri, sot._uri.dirty) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertFalse(sot.allow_list) self.assertFalse(sot.allow_head) - self.assertEqual('PUT', sot.update_method) + self.assertEqual('PUT', sot.commit_method) self.assertEqual('POST', sot.create_method) def test_repr(self): @@ -909,8 +909,8 @@ class Test(resource.Resource): def test_cant_do_anything(self): class Test(resource.Resource): allow_create = False - allow_get = False - allow_update = False + allow_fetch = False + allow_commit = False allow_delete = False allow_head = False allow_list = False @@ -920,7 +920,7 @@ class Test(resource.Resource): # The first argument to all of these operations is the session, # but we raise before we get to it so just pass anything in. self.assertRaises(exceptions.MethodNotSupported, sot.create, "") - self.assertRaises(exceptions.MethodNotSupported, sot.get, "") + self.assertRaises(exceptions.MethodNotSupported, sot.fetch, "") self.assertRaises(exceptions.MethodNotSupported, sot.delete, "") self.assertRaises(exceptions.MethodNotSupported, sot.head, "") @@ -933,7 +933,7 @@ class Test(resource.Resource): # if the call can be made, so fake a dirty list. sot._body = mock.Mock() sot._body.dirty = mock.Mock(return_value={"x": "y"}) - self.assertRaises(exceptions.MethodNotSupported, sot.update, "") + self.assertRaises(exceptions.MethodNotSupported, sot.commit, "") class TestResourceActions(base.TestCase): @@ -949,9 +949,9 @@ class Test(resource.Resource): base_path = self.base_path resources_key = 'resources' allow_create = True - allow_get = True + allow_fetch = True allow_head = True - allow_update = True + allow_commit = True allow_delete = True allow_list = True @@ -1037,8 +1037,8 @@ class Test(resource.Resource): self._test_create(Test, requires_id=False, prepend_key=True) - def test_get(self): - result = self.sot.get(self.session) + def test_fetch(self): + result = self.sot.fetch(self.session) self.sot._prepare_request.assert_called_once_with(requires_id=True) self.session.get.assert_called_once_with( @@ -1048,18 +1048,18 @@ def test_get(self): self.sot._translate_response.assert_called_once_with(self.response) self.assertEqual(result, self.sot) - def test_get_with_microversion(self): + def test_fetch_with_microversion(self): class Test(resource.Resource): service = self.service_name base_path = self.base_path - allow_get = True + allow_fetch = True _max_microversion = '1.42' sot = Test(id='id') sot._prepare_request = mock.Mock(return_value=self.request) sot._translate_response = mock.Mock() - result = sot.get(self.session) + result = sot.fetch(self.session) sot._prepare_request.assert_called_once_with(requires_id=True) self.session.get.assert_called_once_with( @@ -1069,8 +1069,8 @@ class Test(resource.Resource): sot._translate_response.assert_called_once_with(self.response) self.assertEqual(result, sot) - def test_get_not_requires_id(self): - result = self.sot.get(self.session, False) + def test_fetch_not_requires_id(self): + result = self.sot.fetch(self.session, False) self.sot._prepare_request.assert_called_once_with(requires_id=False) self.session.get.assert_called_once_with( @@ -1117,31 +1117,31 @@ class Test(resource.Resource): self.response, has_body=False) self.assertEqual(result, sot) - def _test_update(self, update_method='PUT', prepend_key=True, + def _test_commit(self, commit_method='PUT', prepend_key=True, has_body=True, microversion=None): - self.sot.update_method = update_method + self.sot.commit_method = commit_method # Need to make sot look dirty so we can attempt an update self.sot._body = mock.Mock() self.sot._body.dirty = mock.Mock(return_value={"x": "y"}) - self.sot.update(self.session, prepend_key=prepend_key, + self.sot.commit(self.session, prepend_key=prepend_key, has_body=has_body) self.sot._prepare_request.assert_called_once_with( prepend_key=prepend_key) - if update_method == 'PATCH': + if commit_method == 'PATCH': self.session.patch.assert_called_once_with( self.request.url, json=self.request.body, headers=self.request.headers, microversion=microversion) - elif update_method == 'POST': + elif commit_method == 'POST': self.session.post.assert_called_once_with( self.request.url, json=self.request.body, headers=self.request.headers, microversion=microversion) - elif update_method == 'PUT': + elif commit_method == 'PUT': self.session.put.assert_called_once_with( self.request.url, json=self.request.body, headers=self.request.headers, @@ -1151,20 +1151,20 @@ def _test_update(self, update_method='PUT', prepend_key=True, self.sot._translate_response.assert_called_once_with( self.response, has_body=has_body) - def test_update_put(self): - self._test_update(update_method='PUT', prepend_key=True, has_body=True) + def test_commit_put(self): + self._test_commit(commit_method='PUT', prepend_key=True, has_body=True) - def test_update_patch(self): - self._test_update( - update_method='PATCH', prepend_key=False, has_body=False) + def test_commit_patch(self): + self._test_commit( + commit_method='PATCH', prepend_key=False, has_body=False) - def test_update_not_dirty(self): + def test_commit_not_dirty(self): self.sot._body = mock.Mock() self.sot._body.dirty = dict() self.sot._header = mock.Mock() self.sot._header.dirty = dict() - self.sot.update(self.session) + self.sot.commit(self.session) self.session.put.assert_not_called() @@ -1879,7 +1879,7 @@ class Test(resource.Resource): @classmethod def existing(cls, **kwargs): mock_match = mock.Mock() - mock_match.get.return_value = value + mock_match.fetch.return_value = value return mock_match result = Test.find("session", "name") @@ -1990,11 +1990,11 @@ def _resources_from_statuses(self, *statuses, **kwargs): assert not kwargs, 'Unexpected keyword arguments: %s' % kwargs resources = [] for status in statuses: - res = mock.Mock(spec=['id', 'get', attribute]) + res = mock.Mock(spec=['id', 'fetch', attribute]) setattr(res, attribute, status) resources.append(res) for index, res in enumerate(resources[:-1]): - res.get.return_value = resources[index + 1] + res.fetch.return_value = resources[index + 1] return resources def test_status_match(self): @@ -2089,7 +2089,7 @@ def test_success(self): response.headers = {} response.status_code = 404 res = mock.Mock() - res.get.side_effect = [ + res.fetch.side_effect = [ None, None, exceptions.NotFoundException('Not Found', response)] @@ -2100,7 +2100,7 @@ def test_success(self): def test_timeout(self): res = mock.Mock() res.status = 'ACTIVE' - res.get.return_value = res + res.fetch.return_value = res self.assertRaises( exceptions.ResourceTimeout, @@ -2118,8 +2118,8 @@ def test_compatible(self, mock_get_ver): self.assertEqual( '1.42', - self.res._assert_microversion_for(self.session, 'get', '1.6')) - mock_get_ver.assert_called_once_with(self.res, self.session, 'get') + self.res._assert_microversion_for(self.session, 'fetch', '1.6')) + mock_get_ver.assert_called_once_with(self.res, self.session, 'fetch') def test_incompatible(self, mock_get_ver): mock_get_ver.return_value = '1.1' @@ -2127,8 +2127,8 @@ def test_incompatible(self, mock_get_ver): self.assertRaisesRegex(exceptions.NotSupported, '1.6 is required, but 1.1 will be used', self.res._assert_microversion_for, - self.session, 'get', '1.6') - mock_get_ver.assert_called_once_with(self.res, self.session, 'get') + self.session, 'fetch', '1.6') + mock_get_ver.assert_called_once_with(self.res, self.session, 'fetch') def test_custom_message(self, mock_get_ver): mock_get_ver.return_value = '1.1' @@ -2136,9 +2136,9 @@ def test_custom_message(self, mock_get_ver): self.assertRaisesRegex(exceptions.NotSupported, 'boom.*1.6 is required, but 1.1 will be used', self.res._assert_microversion_for, - self.session, 'get', '1.6', + self.session, 'fetch', '1.6', error_message='boom') - mock_get_ver.assert_called_once_with(self.res, self.session, 'get') + mock_get_ver.assert_called_once_with(self.res, self.session, 'fetch') def test_none(self, mock_get_ver): mock_get_ver.return_value = None @@ -2146,5 +2146,5 @@ def test_none(self, mock_get_ver): self.assertRaisesRegex(exceptions.NotSupported, '1.6 is required, but the default version', self.res._assert_microversion_for, - self.session, 'get', '1.6') - mock_get_ver.assert_called_once_with(self.res, self.session, 'get') + self.session, 'fetch', '1.6') + mock_get_ver.assert_called_once_with(self.res, self.session, 'fetch') diff --git a/openstack/tests/unit/workflow/test_execution.py b/openstack/tests/unit/workflow/test_execution.py index a79a2b463..d11b5cfa8 100644 --- a/openstack/tests/unit/workflow/test_execution.py +++ b/openstack/tests/unit/workflow/test_execution.py @@ -38,7 +38,7 @@ def test_basic(self): self.assertEqual('executions', sot.resources_key) self.assertEqual('/executions', sot.base_path) self.assertEqual('workflowv2', sot.service.service_type) - self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_list) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_delete) diff --git a/openstack/tests/unit/workflow/test_version.py b/openstack/tests/unit/workflow/test_version.py index 647418099..e349d438d 100644 --- a/openstack/tests/unit/workflow/test_version.py +++ b/openstack/tests/unit/workflow/test_version.py @@ -31,8 +31,8 @@ def test_basic(self): self.assertEqual('/', sot.base_path) self.assertEqual('workflowv2', sot.service.service_type) self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_get) - self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/workflow/test_workflow.py b/openstack/tests/unit/workflow/test_workflow.py index b9d457a2a..f1af93f89 100644 --- a/openstack/tests/unit/workflow/test_workflow.py +++ b/openstack/tests/unit/workflow/test_workflow.py @@ -33,7 +33,7 @@ def test_basic(self): self.assertEqual('workflows', sot.resources_key) self.assertEqual('/workflows', sot.base_path) self.assertEqual('workflowv2', sot.service.service_type) - self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_list) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_delete) diff --git a/openstack/workflow/v2/execution.py b/openstack/workflow/v2/execution.py index 01287eacb..697410de1 100644 --- a/openstack/workflow/v2/execution.py +++ b/openstack/workflow/v2/execution.py @@ -23,7 +23,7 @@ class Execution(resource.Resource): # capabilities allow_create = True allow_list = True - allow_get = True + allow_fetch = True allow_delete = True _query_mapping = resource.QueryParameters( diff --git a/openstack/workflow/v2/workflow.py b/openstack/workflow/v2/workflow.py index c98237cc1..3214a6b64 100644 --- a/openstack/workflow/v2/workflow.py +++ b/openstack/workflow/v2/workflow.py @@ -23,7 +23,7 @@ class Workflow(resource.Resource): # capabilities allow_create = True allow_list = True - allow_get = True + allow_fetch = True allow_delete = True _query_mapping = resource.QueryParameters( diff --git a/releasenotes/notes/rename-resource-methods-5f2a716b08156765.yaml b/releasenotes/notes/rename-resource-methods-5f2a716b08156765.yaml new file mode 100644 index 000000000..071371c73 --- /dev/null +++ b/releasenotes/notes/rename-resource-methods-5f2a716b08156765.yaml @@ -0,0 +1,12 @@ +--- +upgrade: + - | + ``openstack.resource.Resource.get`` has been renamed to + ``openstack.resource.Resource.fetch`` to prevent conflicting with a + ``dict`` method of the same name. While most consumer code is unlikely + to call this method directly, this is a breaking change. + - | + ``openstack.resource.Resource.update`` has been renamed to + ``openstack.resource.Resource.commit`` to prevent conflicting with a + ``dict`` method of the same name. While most consumer code is unlikely + to call this method directly, this is a breaking change. From e3e9a9d39c6e7c43adc3813db1f7640cdf6b7de4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 6 Aug 2018 08:47:12 -0500 Subject: [PATCH 2162/3836] Add computed attribute type and location to base resource The shade objects all have a 'location' attribute which includes information about the cloud, domain and project an object is from. Add this to the base Resource object so that Resource objects start to match shade objects. Also, add a new type of attribute, 'computed' to set location to since location is not necessarily information from the remote payload. Also add an attribute parameter "coerce_to_default" which can be set to tell the SDK to force the use of the default value if the attribute would have been None or missing. Change-Id: I315bc863bc87a17fb6f6a3e265a46b9e0acd41ed --- openstack/resource.py | 171 +++++++++++++----- openstack/tests/unit/test_resource.py | 54 +++--- .../shade-location-b0d2e5cae743b738.yaml | 8 + 3 files changed, 165 insertions(+), 68 deletions(-) create mode 100644 releasenotes/notes/shade-location-b0d2e5cae743b738.yaml diff --git a/openstack/resource.py b/openstack/resource.py index d344a6db1..d49b1512a 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -86,31 +86,41 @@ class _BaseComponent(object): _map_cls = dict def __init__(self, name, type=None, default=None, alias=None, - alternate_id=False, list_type=None, **kwargs): + alternate_id=False, list_type=None, coerce_to_default=False, + **kwargs): """A typed descriptor for a component that makes up a Resource :param name: The name this component exists as on the server - :param type: The type this component is expected to be by the server. - By default this is None, meaning any value you specify - will work. If you specify type=dict and then set a - component to a string, __set__ will fail, for example. + :param type: + The type this component is expected to be by the server. + By default this is None, meaning any value you specify + will work. If you specify type=dict and then set a + component to a string, __set__ will fail, for example. :param default: Typically None, but any other default can be set. :param alias: If set, alternative attribute on object to return. - :param alternate_id: When `True`, this property is known - internally as a value that can be sent - with requests that require an ID but - when `id` is not a name the Resource has. - This is a relatively uncommon case, and this - setting should only be used once per Resource. - :param list_type: If type is `list`, list_type designates what the - type of the elements of the list should be. + :param alternate_id: + When `True`, this property is known internally as a value that + can be sent with requests that require an ID but when `id` is + not a name the Resource has. This is a relatively uncommon case, + and this setting should only be used once per Resource. + :param list_type: + If type is `list`, list_type designates what the type of the + elements of the list should be. + :param coerce_to_default: + If the Component is None or not present, force the given default + to be used. If a default is not given but a type is given, + construct an empty version of the type in question. """ self.name = name self.type = type - self.default = default + if type is not None and coerce_to_default and not default: + self.default = type() + else: + self.default = default self.alias = alias self.alternate_id = alternate_id self.list_type = list_type + self.coerce_to_default = coerce_to_default def __get__(self, instance, owner): if instance is None: @@ -150,6 +160,8 @@ def __get__(self, instance, owner): return _convert_type(value, self.type, self.list_type) def __set__(self, instance, value): + if self.coerce_to_default and value is None: + value = self.default if value != self.default: value = _convert_type(value, self.type, self.list_type) @@ -183,6 +195,12 @@ class URI(_BaseComponent): key = "_uri" +class Computed(_BaseComponent): + """Computed attributes""" + + key = "_computed" + + class _ComponentManager(collections.MutableMapping): """Storage of a component type""" @@ -313,8 +331,8 @@ class Resource(object): id = Body("id") #: The name of this resource. name = Body("name") - #: The location of this resource. - location = Header("Location") + #: The OpenStack location of this resource. + location = Computed('location') #: Mapping of accepted query parameter names. _query_mapping = QueryParameters() @@ -359,35 +377,57 @@ class Resource(object): #: API microversion (string or None) this Resource was loaded with microversion = None - def __init__(self, _synchronized=False, **attrs): + _connection = None + _body = None + _header = None + _uri = None + _computed = None + + def __init__(self, _synchronized=False, connection=None, **attrs): """The base resource - :param bool _synchronized: This is not intended to be used directly. - See :meth:`~openstack.resource.Resource.new` and - :meth:`~openstack.resource.Resource.existing`. + :param bool _synchronized: + This is not intended to be used directly. See + :meth:`~openstack.resource.Resource.new` and + :meth:`~openstack.resource.Resource.existing`. + :param openstack.connection.Connection connection: + Reference to the Connection being used. Defaults to None to allow + Resource objects to be used without an active Connection, such as + in unit tests. Use of ``self._connection`` in Resource code should + protect itself with a check for None. """ - + self._connection = connection self.microversion = attrs.pop('microversion', None) # NOTE: _collect_attrs modifies **attrs in place, removing # items as they match up with any of the body, header, # or uri mappings. - body, header, uri = self._collect_attrs(attrs) + body, header, uri, computed = self._collect_attrs(attrs) # TODO(briancurtin): at this point if attrs has anything left # they're not being set anywhere. Log this? Raise exception? # How strict should we be here? Should strict be an option? - self._body = _ComponentManager(attributes=body, - synchronized=_synchronized) - self._header = _ComponentManager(attributes=header, - synchronized=_synchronized) - self._uri = _ComponentManager(attributes=uri, - synchronized=_synchronized) + self._body = _ComponentManager( + attributes=body, + synchronized=_synchronized) + self._header = _ComponentManager( + attributes=header, + synchronized=_synchronized) + self._uri = _ComponentManager( + attributes=uri, + synchronized=_synchronized) + self._computed = _ComponentManager( + attributes=computed, + synchronized=_synchronized) def __repr__(self): - pairs = ["%s=%s" % (k, v) for k, v in dict(itertools.chain( - self._body.attributes.items(), - self._header.attributes.items(), - self._uri.attributes.items())).items()] + pairs = [ + "%s=%s" % (k, v if v is not None else 'None') + for k, v in dict(itertools.chain( + self._body.attributes.items(), + self._header.attributes.items(), + self._uri.attributes.items(), + self._computed.attributes.items())).items() + ] args = ", ".join(pairs) return "%s.%s(%s)" % ( @@ -397,9 +437,12 @@ def __eq__(self, comparand): """Return True if another resource has the same contents""" if not isinstance(comparand, Resource): return False - return all([self._body.attributes == comparand._body.attributes, - self._header.attributes == comparand._header.attributes, - self._uri.attributes == comparand._uri.attributes]) + return all([ + self._body.attributes == comparand._body.attributes, + self._header.attributes == comparand._header.attributes, + self._uri.attributes == comparand._uri.attributes, + self._computed.attributes == comparand._computed.attributes, + ]) def __getattribute__(self, name): """Return an attribute on this instance @@ -427,11 +470,12 @@ def _update(self, **attrs): been created. """ self.microversion = attrs.pop('microversion', None) - body, header, uri = self._collect_attrs(attrs) + body, header, uri, computed = self._collect_attrs(attrs) self._body.update(body) self._header.update(header) self._uri.update(uri) + self._computed.update(computed) def _collect_attrs(self, attrs): """Given attributes, return a dict per type of attribute @@ -444,7 +488,25 @@ def _collect_attrs(self, attrs): header = self._consume_header_attrs(attrs) uri = self._consume_uri_attrs(attrs) - return body, header, uri + if any([body, header, uri]): + attrs = self._compute_attributes(body, header, uri) + + body.update(self._consume_attrs(self._body_mapping(), attrs)) + + header.update(self._consume_attrs(self._header_mapping(), attrs)) + uri.update(self._consume_attrs(self._uri_mapping(), attrs)) + computed = self._consume_attrs(self._computed_mapping(), attrs) + # TODO(mordred) We should make a Location Resource and add it here + # instead of just the dict. + if self._connection: + computed['location'] = munch.unmunchify( + self._connection._openstackcloud.current_location) + + return body, header, uri, computed + + def _compute_attributes(self, body, header, uri): + """Compute additional attributes from the remote resource.""" + return {} def _consume_body_attrs(self, attrs): return self._consume_mapped_attrs(Body, attrs) @@ -539,6 +601,11 @@ def _uri_mapping(cls): """Return all URI members of this class""" return cls._get_mapping(URI) + @classmethod + def _computed_mapping(cls): + """Return all URI members of this class""" + return cls._get_mapping(Computed) + @classmethod def _alternate_id(cls): """Return the name of any value known as an alternate_id @@ -585,7 +652,7 @@ def new(cls, **kwargs): return cls(_synchronized=False, **kwargs) @classmethod - def existing(cls, **kwargs): + def existing(cls, connection=None, **kwargs): """Create an instance of an existing remote resource. When creating the instance set the ``_synchronized`` parameter @@ -598,10 +665,14 @@ def existing(cls, **kwargs): :param dict kwargs: Each of the named arguments will be set as attributes on the resulting Resource object. """ - return cls(_synchronized=True, **kwargs) + res = cls(_synchronized=True, **kwargs) + # TODO(shade) Done as a second call rather than a constructor param + # because otherwise the mocking in the tests goes nuts. + res._connection = connection + return res @classmethod - def _from_munch(cls, obj, synchronized=True): + def _from_munch(cls, obj, synchronized=True, connection=None): """Create an instance from a ``munch.Munch`` object. This is intended as a temporary measure to convert between shade-style @@ -611,16 +682,18 @@ def _from_munch(cls, obj, synchronized=True): :param bool synchronized: whether this object already exists on server Must be set to ``False`` for newly created objects. """ - return cls(_synchronized=synchronized, **obj) + return cls(_synchronized=synchronized, connection=connection, **obj) - def to_dict(self, body=True, headers=True, ignore_none=False, - original_names=False): + def to_dict(self, body=True, headers=True, computed=True, + ignore_none=False, original_names=False): """Return a dictionary of this resource's contents :param bool body: Include the :class:`~openstack.resource.Body` attributes in the returned dictionary. :param bool headers: Include the :class:`~openstack.resource.Header` attributes in the returned dictionary. + :param bool computed: Include the :class:`~openstack.resource.Computed` + attributes in the returned dictionary. :param bool ignore_none: When True, exclude key/value pairs where the value is None. This will exclude attributes that the server hasn't returned. @@ -637,9 +710,11 @@ def to_dict(self, body=True, headers=True, ignore_none=False, components.append(Body) if headers: components.append(Header) + if computed: + components.append(Computed) if not components: raise ValueError( - "At least one of `body` or `headers` must be True") + "At least one of `body`, `headers` or `computed` must be True") # isinstance stricly requires this to be a tuple components = tuple(components) @@ -1082,6 +1157,10 @@ def list(cls, session, paginated=False, **params): raw_resource.pop("self", None) value = cls.existing(microversion=microversion, **raw_resource) + # TODO(shade) Done as a second call rather than a constructor + # param because otherwise the mocking in the tests goes nuts. + if hasattr(session, '_sdk_connection'): + value._connection = session._sdk_connection marker = value.id yield value total_yielded += 1 @@ -1189,6 +1268,10 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): # Try to short-circuit by looking directly for a matching ID. try: match = cls.existing(id=name_or_id, **params) + # TODO(shade) Done as a second call rather than a constructor + # param because otherwise the mocking in the tests goes nuts. + if hasattr(session, '_sdk_connection'): + match._connection = session._sdk_connection return match.fetch(session) except exceptions.NotFoundException: pass diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index de7e6d950..ce133f2e9 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -401,17 +401,22 @@ def test_initialize_basic(self): body = {"body": 1} header = {"header": 2, "Location": "somewhere"} uri = {"uri": 3} - everything = dict(itertools.chain(body.items(), header.items(), - uri.items())) + computed = {"computed": 4} + everything = dict(itertools.chain( + body.items(), + header.items(), + uri.items(), + computed.items(), + )) mock_collect = mock.Mock() - mock_collect.return_value = body, header, uri + mock_collect.return_value = body, header, uri, computed with mock.patch.object(resource.Resource, "_collect_attrs", mock_collect): sot = resource.Resource(_synchronized=False, **everything) mock_collect.assert_called_once_with(everything) - self.assertEqual("somewhere", sot.location) + self.assertIsNone(sot.location) self.assertIsInstance(sot._body, resource._ComponentManager) self.assertEqual(body, sot._body.dirty) @@ -433,6 +438,7 @@ def test_repr(self): a = {"a": 1} b = {"b": 2} c = {"c": 3} + d = {"d": 4} class Test(resource.Resource): def __init__(self): @@ -448,6 +454,10 @@ def __init__(self): self._uri.attributes.items = mock.Mock( return_value=c.items()) + self._computed = mock.Mock() + self._computed.attributes.items = mock.Mock( + return_value=d.items()) + the_repr = repr(Test()) # Don't test the arguments all together since the dictionary order @@ -456,6 +466,7 @@ def __init__(self): self.assertIn("a=1", the_repr) self.assertIn("b=2", the_repr) self.assertIn("c=3", the_repr) + self.assertIn("d=4", the_repr) def test_equality(self): class Example(resource.Resource): @@ -477,11 +488,14 @@ def test__update(self): body = "body" header = "header" uri = "uri" + computed = "computed" - sot._collect_attrs = mock.Mock(return_value=(body, header, uri)) + sot._collect_attrs = mock.Mock( + return_value=(body, header, uri, computed)) sot._body.update = mock.Mock() sot._header.update = mock.Mock() sot._uri.update = mock.Mock() + sot._computed.update = mock.Mock() args = {"arg": 1} sot._update(**args) @@ -490,19 +504,7 @@ def test__update(self): sot._body.update.assert_called_once_with(body) sot._header.update.assert_called_once_with(header) sot._uri.update.assert_called_once_with(uri) - - def test__collect_attrs(self): - sot = resource.Resource() - - expected_attrs = ["body", "header", "uri"] - - sot._consume_attrs = mock.Mock() - sot._consume_attrs.side_effect = expected_attrs - - # It'll get passed an empty dict at the least. - actual_attrs = sot._collect_attrs(dict()) - - self.assertItemsEqual(expected_attrs, actual_attrs) + sot._computed.update.assert_called_with(computed) def test__consume_attrs(self): serverside_key1 = "someKey1" @@ -538,7 +540,7 @@ def test__mapping_defaults(self): # Check that even on an empty class, we get the expected # built-in attributes. - self.assertIn("location", resource.Resource._header_mapping()) + self.assertIn("location", resource.Resource._computed_mapping()) self.assertIn("name", resource.Resource._body_mapping()) self.assertIn("id", resource.Resource._body_mapping()) @@ -695,7 +697,8 @@ class Test(resource.Resource): expected = { 'id': 'FAKE_ID', 'name': None, - 'bar': None + 'bar': None, + 'location': None, } self.assertEqual(expected, res.to_dict(headers=False)) @@ -744,10 +747,13 @@ class Test(resource.Resource): res = Test(id='FAKE_ID') - err = self.assertRaises(ValueError, - res.to_dict, body=False, headers=False) - self.assertEqual('At least one of `body` or `headers` must be True', - six.text_type(err)) + err = self.assertRaises( + ValueError, + res.to_dict, + body=False, headers=False, computed=False) + self.assertEqual( + 'At least one of `body`, `headers` or `computed` must be True', + six.text_type(err)) def test_to_dict_with_mro_no_override(self): diff --git a/releasenotes/notes/shade-location-b0d2e5cae743b738.yaml b/releasenotes/notes/shade-location-b0d2e5cae743b738.yaml new file mode 100644 index 000000000..616a475da --- /dev/null +++ b/releasenotes/notes/shade-location-b0d2e5cae743b738.yaml @@ -0,0 +1,8 @@ +--- +upgrade: + - | + The base ``Resource`` field ``location`` is no longer drawn from the + ``Location`` HTTP header, but is instead a dict containing information + about cloud, domain and project. The location dict is a feature of shade + objects and is being added to all objects as part of the alignment of + shade and sdk. From d4db52f2fa9e8b995d90d5dd2fbdf168a634133e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 6 Aug 2018 09:29:09 -0500 Subject: [PATCH 2163/3836] Remove special handling of stacks SDK should not be removing keys from the object based on remote version. The behavior of the objects should be the behavior of the objects. Also, all objects should have id and name. stack_id and stack_name are fine for the REST API - not for the SDK. Change-Id: I42a6d9d3cbd86a55b92b450d70480c1ead7a7f6d --- openstack/orchestration/v1/stack_environment.py | 8 ++++++-- openstack/orchestration/v1/stack_files.py | 8 ++++++-- openstack/orchestration/v1/stack_template.py | 17 +++++------------ .../orchestration/v1/test_stack_template.py | 17 +++-------------- 4 files changed, 20 insertions(+), 30 deletions(-) diff --git a/openstack/orchestration/v1/stack_environment.py b/openstack/orchestration/v1/stack_environment.py index 2902818a9..2b972bdf4 100644 --- a/openstack/orchestration/v1/stack_environment.py +++ b/openstack/orchestration/v1/stack_environment.py @@ -28,9 +28,13 @@ class StackEnvironment(resource.Resource): # Properties #: Name of the stack where the template is referenced. - stack_name = resource.URI('stack_name') + name = resource.URI('stack_name') + # Backwards compat + stack_name = name #: ID of the stack where the template is referenced. - stack_id = resource.URI('stack_id') + id = resource.URI('stack_id') + # Backwards compat + stack_id = id #: A list of parameter names whose values are encrypted encrypted_param_names = resource.Body('encrypted_param_names') #: A list of event sinks diff --git a/openstack/orchestration/v1/stack_files.py b/openstack/orchestration/v1/stack_files.py index 364f50822..6963d5b84 100644 --- a/openstack/orchestration/v1/stack_files.py +++ b/openstack/orchestration/v1/stack_files.py @@ -28,9 +28,13 @@ class StackFiles(resource.Resource): # Properties #: Name of the stack where the template is referenced. - stack_name = resource.URI('stack_name') + name = resource.URI('stack_name') + # Backwards compat + stack_name = name #: ID of the stack where the template is referenced. - stack_id = resource.URI('stack_id') + id = resource.URI('stack_id') + # Backwards compat + stack_id = id def fetch(self, session): # The stack files response contains a map of filenames and file diff --git a/openstack/orchestration/v1/stack_template.py b/openstack/orchestration/v1/stack_template.py index 0f0ab3545..af5d8dba7 100644 --- a/openstack/orchestration/v1/stack_template.py +++ b/openstack/orchestration/v1/stack_template.py @@ -28,9 +28,12 @@ class StackTemplate(resource.Resource): # Properties #: Name of the stack where the template is referenced. - stack_name = resource.URI('stack_name') + name = resource.URI('stack_name') + # Backwards compat + stack_name = name #: ID of the stack where the template is referenced. - stack_id = resource.URI('stack_id') + id = resource.URI('stack_id', alternate_id=True) + stack_id = id #: The description specified in the template description = resource.Body('Description') #: The version of the orchestration HOT template. @@ -46,13 +49,3 @@ class StackTemplate(resource.Resource): parameter_groups = resource.Body('parameter_groups', type=list) # Restrict conditions which supported since '2016-10-14'. conditions = resource.Body('conditions', type=dict) - - def to_dict(self): - mapping = super(StackTemplate, self).to_dict() - mapping.pop('location') - mapping.pop('id') - mapping.pop('name') - if self.heat_template_version < '2016-10-14': - mapping.pop('conditions') - - return mapping diff --git a/openstack/tests/unit/orchestration/v1/test_stack_template.py b/openstack/tests/unit/orchestration/v1/test_stack_template.py index 51c03cbf4..be7a4bc22 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack_template.py +++ b/openstack/tests/unit/orchestration/v1/test_stack_template.py @@ -63,6 +63,9 @@ def test_to_dict(self): "description": "server parameters", "parameters": ["key_name", "image_id"], "label": "server_parameters"}] + fake_sot['location'] = None + fake_sot['id'] = None + fake_sot['name'] = None for temp_version in ['2016-10-14', '2017-02-24', '2017-02-24', '2017-09-01', '2018-03-02', 'newton', @@ -70,17 +73,3 @@ def test_to_dict(self): fake_sot['heat_template_version'] = temp_version sot = stack_template.StackTemplate(**fake_sot) self.assertEqual(fake_sot, sot.to_dict()) - - def test_to_dict_without_conditions(self): - fake_sot = copy.deepcopy(FAKE) - fake_sot['parameter_groups'] = [{ - "description": "server parameters", - "parameters": ["key_name", "image_id"], - "label": "server_parameters"}] - fake_sot.pop('conditions') - - for temp_version in ['2013-05-23', '2014-10-16', '2015-04-30', - '2015-10-15', '2016-04-08']: - fake_sot['heat_template_version'] = temp_version - sot = stack_template.StackTemplate(**fake_sot) - self.assertEqual(fake_sot, sot.to_dict()) From 2f973948473b52a6f1eb9e2eae5962d1df7a992f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 6 Aug 2018 10:05:54 -0500 Subject: [PATCH 2164/3836] Make resource a dict subclass usable by shade layer Users of shade/openstack.cloud expect munch.Munch objects to be returned by all the methods. These objects are basically dicts that allow object-notation access. In an attempt to minimize the size of the mapping layer between shade and sdk code, make the Resource objects behave like dicts as well as like objects. We want the attributes exposed as dictionary attributes to be the data model defined with the Component descriptors, so we wind up needing to override the munch methods anyway - at which point munch itself ceases being valuable. he overall idea here is allowing the shade-layer API to return SDK Resource objects directly without transforming them with to_dict first. If we can make that work, it means that a user could use shade and sdk methods interchangably, being able to do something like: # Create server using shade layer server = conn.create_server('my-server', auto_ip) # reboot server using sdk conn.compute.servers.reboot(server, 'HARD') There is a REALLY ugly hack in here (with a comment) that's needed to make json.dumps(server) work. It's not a hack we should actually use, but my brain is too tired right now to figure out how to make it actually work. There is a latent bug in image. A TODO has been left. Co-Authored-By: Rosario Di Somma Change-Id: Ib48f0bfa74231bf3171a798a179f6606177c95b0 --- openstack/image/v2/image.py | 5 +- openstack/proxy.py | 3 +- openstack/resource.py | 83 +++++++++++++++++-- .../tests/functional/image/v2/test_image.py | 5 ++ .../unit/object_store/v1/test_container.py | 25 ++++++ openstack/tests/unit/test_resource.py | 2 +- 6 files changed, 115 insertions(+), 8 deletions(-) diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index 42f54e23c..68c3e97c8 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -84,8 +84,11 @@ class Image(resource.Resource): name = resource.Body('name') #: The ID of the owner, or project, of the image. owner_id = resource.Body('owner') + # TODO(mordred) This is not how this works in v2. I mean, it's how it + # should work, but it's not. We need to fix properties. They work right + # in shade, so we can draw some logic from there. #: Properties, if any, that are associated with the image. - properties = resource.Body('properties', type=dict) + properties = resource.Body('properties') #: The size of the image data, in bytes. size = resource.Body('size', type=int) #: When present, Glance will attempt to store the disk image data in the diff --git a/openstack/proxy.py b/openstack/proxy.py index 7ccf2a997..ca84c8738 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -62,7 +62,8 @@ class if using an existing instance, or ``munch.Munch``, if value is None: # Create a bare resource res = resource_type.new(**attrs) - elif isinstance(value, dict): + elif (isinstance(value, dict) + and not isinstance(value, resource.Resource)): res = resource_type._from_munch(value) res._update(**attrs) elif not isinstance(value, resource_type): diff --git a/openstack/resource.py b/openstack/resource.py index d49b1512a..6e012fc7e 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -38,6 +38,7 @@ class that represent a remote resource. The attributes that from keystoneauth1 import discover import munch from requests import structures +import six from openstack import _log from openstack import exceptions @@ -124,7 +125,7 @@ def __init__(self, name, type=None, default=None, alias=None, def __get__(self, instance, owner): if instance is None: - return None + return self attributes = getattr(instance, self.key) @@ -318,7 +319,16 @@ def _transpose(self, query): return result -class Resource(object): +class Resource(dict): + # TODO(mordred) While this behaves mostly like a munch for the purposes + # we need, sub-resources, such as Server.security_groups, which is a list + # of dicts, will contain lists of real dicts, not lists of munch-like dict + # objects. We should probably figure out a Resource class, perhaps + # SubResource or something, that we can use to define the data-model of + # complex object attributes where those attributes are not already covered + # by a different resource such as Server.image which should ultimately + # be an Image. We subclass dict so that things like json.dumps and pprint + # will work properly. #: Singular form of key for resource. resource_key = None @@ -419,6 +429,13 @@ def __init__(self, _synchronized=False, connection=None, **attrs): attributes=computed, synchronized=_synchronized) + # TODO(mordred) This is terrible, but is a hack at the moment to ensure + # json.dumps works. The json library does basically if not obj: and + # obj.items() ... but I think the if not obj: is short-circuiting down + # in the C code and thus since we don't store the data in self[] it's + # always False even if we override __len__ or __bool__. + dict.update(self, self.to_dict()) + def __repr__(self): pairs = [ "%s=%s" % (k, v if v is not None else 'None') @@ -462,6 +479,49 @@ def __getattribute__(self, name): else: return object.__getattribute__(self, name) + def __getitem__(self, name): + """Provide dictionary access for elements of the data model.""" + # Check the class, since BaseComponent is a descriptor and thus + # behaves like its wrapped content. If we get it on the class, + # it returns the BaseComponent itself, not the results of __get__. + real_item = getattr(self.__class__, name, None) + if isinstance(real_item, _BaseComponent): + return getattr(self, name) + raise KeyError(name) + + def __delitem__(self, name): + delattr(self, name) + + def __setitem__(self, name, value): + real_item = getattr(self.__class__, name, None) + if isinstance(real_item, _BaseComponent): + self.__setattr__(name, value) + else: + raise KeyError( + "{name} is not found. {module}.{cls} objects do not support" + " setting arbitrary keys through the" + " dict interface.".format( + module=self.__module__, + cls=self.__class__.__name__, + name=name)) + + def keys(self): + # NOTE(mordred) In python2, dict.keys returns a list. In python3 it + # returns a dict_keys view. For 2, we can return a list from the + # itertools chain. In 3, return the chain so it's at least an iterator. + # It won't strictly speaking be an actual dict_keys, so it's possible + # we may want to get more clever, but for now let's see how far this + # will take us. + underlying_keys = itertools.chain( + self._body.attributes.keys(), + self._header.attributes.keys(), + self._uri.attributes.keys(), + self._computed.attributes.keys()) + if six.PY2: + return list(underlying_keys) + else: + return underlying_keys + def _update(self, **attrs): """Given attributes, update them on this instance @@ -477,6 +537,13 @@ def _update(self, **attrs): self._uri.update(uri) self._computed.update(computed) + # TODO(mordred) This is terrible, but is a hack at the moment to ensure + # json.dumps works. The json library does basically if not obj: and + # obj.items() ... but I think the if not obj: is short-circuiting down + # in the C code and thus since we don't store the data in self[] it's + # always False even if we override __len__ or __bool__. + dict.update(self, self.to_dict()) + def _collect_attrs(self, attrs): """Given attributes, return a dict per type of attribute @@ -740,16 +807,22 @@ def to_dict(self, body=True, headers=True, computed=True, continue if isinstance(value, Resource): mapping[key] = value.to_dict() - elif (value and isinstance(value, list) - and isinstance(value[0], Resource)): + elif value and isinstance(value, list): converted = [] for raw in value: - converted.append(raw.to_dict()) + if isinstance(raw, Resource): + converted.append(raw.to_dict()) + else: + converted.append(raw) mapping[key] = converted else: mapping[key] = value return mapping + # Compatibility with the munch.Munch.toDict method + toDict = to_dict + # Make the munch copy method use to_dict + copy = to_dict def _to_munch(self): """Convert this resource into a Munch compatible with shade.""" diff --git a/openstack/tests/functional/image/v2/test_image.py b/openstack/tests/functional/image/v2/test_image.py index 8b3005b5c..a2501f93d 100644 --- a/openstack/tests/functional/image/v2/test_image.py +++ b/openstack/tests/functional/image/v2/test_image.py @@ -32,6 +32,11 @@ def setUp(self): name=TEST_IMAGE_NAME, disk_format='raw', container_format='bare', + # TODO(mordred): This is not doing what people think it is doing. + # This is EPICLY broken. However, rather than fixing it as it is, + # we need to just replace the image upload code with the stuff + # from shade. Figuring out mapping the crap-tastic arbitrary + # extra key-value pairs into Resource is going to be fun. properties='{"description": "This is not an image"}', data=open('CONTRIBUTING.rst', 'r') ) diff --git a/openstack/tests/unit/object_store/v1/test_container.py b/openstack/tests/unit/object_store/v1/test_container.py index 88075beb6..f18f29832 100644 --- a/openstack/tests/unit/object_store/v1/test_container.py +++ b/openstack/tests/unit/object_store/v1/test_container.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import json + from openstack.object_store.v1 import container from openstack.tests.unit import base @@ -165,6 +167,29 @@ def test_to_dict_recursion(self): self.assertEqual(sot_dict['id'], self.container) self.assertEqual(sot_dict['name'], self.container) + def test_to_json(self): + sot = container.Container.new(name=self.container) + self.assertEqual( + { + 'bytes': None, + 'bytes_used': None, + 'content_type': None, + 'count': None, + 'id': self.container, + 'if_none_match': None, + 'is_content_type_detected': None, + 'is_newest': None, + 'location': None, + 'name': self.container, + 'object_count': None, + 'read_ACL': None, + 'sync_key': None, + 'sync_to': None, + 'timestamp': None, + 'versions_location': None, + 'write_ACL': None, + }, json.loads(json.dumps(sot))) + def _test_no_headers(self, sot, sot_call, sess_method): headers = {} data = {} diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index ce133f2e9..85685ba15 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -66,7 +66,7 @@ def test_get_no_instance(self): # Test that we short-circuit everything when given no instance. result = sot.__get__(None, None) - self.assertIsNone(result) + self.assertIs(sot, result) # NOTE: Some tests will use a default=1 setting when testing result # values that should be None because the default-for-default is also None. From 5d440e00f63c9118b94d6a3920d589709644de6f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 10 Aug 2018 09:52:44 -0500 Subject: [PATCH 2165/3836] Fix the heat template resource more cleaner Resource attributes can have aliases. Use that for name/stack_name. Additionally, we can make stack_id as "alternate_id" which means that it can be referenced as "id". Use that. Change-Id: Ic2e73d39d396ae2cea77a87c314a7a9fee3ae3d6 --- openstack/orchestration/v1/stack_template.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openstack/orchestration/v1/stack_template.py b/openstack/orchestration/v1/stack_template.py index af5d8dba7..0bea61ab9 100644 --- a/openstack/orchestration/v1/stack_template.py +++ b/openstack/orchestration/v1/stack_template.py @@ -29,11 +29,11 @@ class StackTemplate(resource.Resource): # Properties #: Name of the stack where the template is referenced. name = resource.URI('stack_name') - # Backwards compat - stack_name = name + # Backwards compat. _stack_name will never match, but the alias will + # point it to the value pulled for name. + stack_name = resource.URI('_stack_name', alias='name') #: ID of the stack where the template is referenced. - id = resource.URI('stack_id', alternate_id=True) - stack_id = id + stack_id = resource.URI('stack_id', alternate_id=True) #: The description specified in the template description = resource.Body('Description') #: The version of the orchestration HOT template. From ac8df03fd1f26b1c17ea45eec808fd7e8633dfef Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Fri, 10 Aug 2018 17:19:24 +0200 Subject: [PATCH 2166/3836] Add simple create/show/delete functional tests for all baremetal resources Required adding support for min_microversion to the base test class. Change-Id: I0aeb2b5fcb4575938bfbb8d7ca9b29d1f77a2ee5 --- openstack/tests/functional/baremetal/base.py | 57 +++++++++++++++++++ .../baremetal/test_baremetal_chassis.py | 41 +++++++++++++ .../baremetal/test_baremetal_node.py | 25 ++------ .../baremetal/test_baremetal_port.py | 50 ++++++++++++++++ .../baremetal/test_baremetal_port_group.py | 48 ++++++++++++++++ openstack/tests/functional/base.py | 18 +++++- 6 files changed, 216 insertions(+), 23 deletions(-) create mode 100644 openstack/tests/functional/baremetal/base.py create mode 100644 openstack/tests/functional/baremetal/test_baremetal_chassis.py create mode 100644 openstack/tests/functional/baremetal/test_baremetal_port.py create mode 100644 openstack/tests/functional/baremetal/test_baremetal_port_group.py diff --git a/openstack/tests/functional/baremetal/base.py b/openstack/tests/functional/baremetal/base.py new file mode 100644 index 000000000..4faf913a0 --- /dev/null +++ b/openstack/tests/functional/baremetal/base.py @@ -0,0 +1,57 @@ +# 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.tests.functional import base + + +class BaseBaremetalTest(base.BaseFunctionalTest): + + min_microversion = None + node_id = None + + def setUp(self): + super(BaseBaremetalTest, self).setUp() + self.require_service('baremetal', + min_microversion=self.min_microversion) + + def create_chassis(self, **kwargs): + chassis = self.conn.baremetal.create_chassis(**kwargs) + self.addCleanup( + lambda: self.conn.baremetal.delete_chassis(chassis.id, + ignore_missing=True)) + return chassis + + def create_node(self, driver='fake-hardware', **kwargs): + node = self.conn.baremetal.create_node(driver=driver, **kwargs) + self.node_id = node.id + self.addCleanup( + lambda: self.conn.baremetal.delete_node(self.node_id, + ignore_missing=True)) + self.assertIsNotNone(self.node_id) + return node + + def create_port(self, node_id=None, **kwargs): + node_id = node_id or self.node_id + port = self.conn.baremetal.create_port(node_uuid=node_id, **kwargs) + self.addCleanup( + lambda: self.conn.baremetal.delete_port(port.id, + ignore_missing=True)) + return port + + def create_port_group(self, node_id=None, **kwargs): + node_id = node_id or self.node_id + port_group = self.conn.baremetal.create_port_group(node_uuid=node_id, + **kwargs) + self.addCleanup( + lambda: self.conn.baremetal.delete_port_group(port_group.id, + ignore_missing=True)) + return port_group diff --git a/openstack/tests/functional/baremetal/test_baremetal_chassis.py b/openstack/tests/functional/baremetal/test_baremetal_chassis.py new file mode 100644 index 000000000..c5bcfa8ab --- /dev/null +++ b/openstack/tests/functional/baremetal/test_baremetal_chassis.py @@ -0,0 +1,41 @@ +# 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.tests.functional.baremetal import base + + +class TestBareMetalChassis(base.BaseBaremetalTest): + + def test_chassis_create_get_delete(self): + chassis = self.create_chassis() + + loaded = self.conn.baremetal.get_chassis(chassis.id) + self.assertEqual(loaded.id, chassis.id) + + self.conn.baremetal.delete_chassis(chassis, ignore_missing=False) + self.assertRaises(exceptions.NotFoundException, + self.conn.baremetal.get_chassis, chassis.id) + + def test_chassis_negative_non_existing(self): + uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" + self.assertRaises(exceptions.NotFoundException, + self.conn.baremetal.get_chassis, uuid) + self.assertRaises(exceptions.NotFoundException, + self.conn.baremetal.find_chassis, uuid, + ignore_missing=False) + self.assertRaises(exceptions.NotFoundException, + self.conn.baremetal.delete_chassis, uuid, + ignore_missing=False) + self.assertIsNone(self.conn.baremetal.find_chassis(uuid)) + self.assertIsNone(self.conn.baremetal.delete_chassis(uuid)) diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index 103009086..0d44264ad 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -11,28 +11,12 @@ # under the License. from openstack import exceptions -from openstack.tests.functional import base +from openstack.tests.functional.baremetal import base -class TestBareMetalNode(base.BaseFunctionalTest): - - node_id = None - - def setUp(self): - super(TestBareMetalNode, self).setUp() - self.require_service('baremetal') - - def tearDown(self): - if self.node_id: - self.conn.baremetal.delete_node(self.node_id, ignore_missing=True) - super(TestBareMetalNode, self).tearDown() - +class TestBareMetalNode(base.BaseBaremetalTest): def test_node_create_get_delete(self): - node = self.conn.baremetal.create_node(driver='fake-hardware', - name='node-name') - self.node_id = node.id - self.assertIsNotNone(self.node_id) - + node = self.create_node(name='node-name') self.assertEqual(node.name, 'node-name') self.assertEqual(node.driver, 'fake-hardware') self.assertEqual(node.provision_state, 'available') @@ -56,8 +40,7 @@ def test_node_create_get_delete(self): self.conn.baremetal.get_node, self.node_id) def test_node_create_in_enroll_provide(self): - node = self.conn.baremetal.create_node(driver='fake-hardware', - provision_state='enroll') + node = self.create_node(provision_state='enroll') self.node_id = node.id self.assertEqual(node.driver, 'fake-hardware') diff --git a/openstack/tests/functional/baremetal/test_baremetal_port.py b/openstack/tests/functional/baremetal/test_baremetal_port.py new file mode 100644 index 000000000..602120979 --- /dev/null +++ b/openstack/tests/functional/baremetal/test_baremetal_port.py @@ -0,0 +1,50 @@ +# 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.tests.functional.baremetal import base + + +class TestBareMetalPort(base.BaseBaremetalTest): + + def setUp(self): + super(TestBareMetalPort, self).setUp() + self.node = self.create_node() + + def test_port_create_get_delete(self): + port = self.create_port(address='11:22:33:44:55:66') + self.assertEqual(self.node_id, port.node_id) + # Can be None if the microversion is too small, so we make sure it is + # not False. + self.assertNotEqual(port.is_pxe_enabled, False) + self.assertIsNone(port.port_group_id) + + loaded = self.conn.baremetal.get_port(port.id) + self.assertEqual(loaded.id, port.id) + + self.conn.baremetal.delete_port(port, ignore_missing=False) + self.assertRaises(exceptions.NotFoundException, + self.conn.baremetal.get_port, port.id) + + def test_port_negative_non_existing(self): + uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" + self.assertRaises(exceptions.NotFoundException, + self.conn.baremetal.get_port, uuid) + self.assertRaises(exceptions.NotFoundException, + self.conn.baremetal.find_port, uuid, + ignore_missing=False) + self.assertRaises(exceptions.NotFoundException, + self.conn.baremetal.delete_port, uuid, + ignore_missing=False) + self.assertIsNone(self.conn.baremetal.find_port(uuid)) + self.assertIsNone(self.conn.baremetal.delete_port(uuid)) diff --git a/openstack/tests/functional/baremetal/test_baremetal_port_group.py b/openstack/tests/functional/baremetal/test_baremetal_port_group.py new file mode 100644 index 000000000..6ed8041df --- /dev/null +++ b/openstack/tests/functional/baremetal/test_baremetal_port_group.py @@ -0,0 +1,48 @@ +# 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.tests.functional.baremetal import base + + +class TestBareMetalPortGroup(base.BaseBaremetalTest): + + min_microversion = '1.23' + + def setUp(self): + super(TestBareMetalPortGroup, self).setUp() + self.node = self.create_node() + + def test_port_group_create_get_delete(self): + port_group = self.create_port_group() + + loaded = self.conn.baremetal.get_port_group(port_group.id) + self.assertEqual(loaded.id, port_group.id) + + self.conn.baremetal.delete_port_group(port_group, + ignore_missing=False) + self.assertRaises(exceptions.NotFoundException, + self.conn.baremetal.get_port_group, port_group.id) + + def test_port_group_negative_non_existing(self): + uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" + self.assertRaises(exceptions.NotFoundException, + self.conn.baremetal.get_port_group, uuid) + self.assertRaises(exceptions.NotFoundException, + self.conn.baremetal.find_port_group, uuid, + ignore_missing=False) + self.assertRaises(exceptions.NotFoundException, + self.conn.baremetal.delete_port_group, uuid, + ignore_missing=False) + self.assertIsNone(self.conn.baremetal.find_port_group(uuid)) + self.assertIsNone(self.conn.baremetal.delete_port_group(uuid)) diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index c2b5e6308..5fbda5cbd 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -13,6 +13,7 @@ import os import openstack.config +from keystoneauth1 import discover from keystoneauth1 import exceptions as _exceptions from openstack import connection from openstack.tests import base @@ -51,7 +52,7 @@ def cleanup(): # TODO(shade) Replace this with call to conn.has_service when we've merged # the shade methods into Connection. - def require_service(self, service_type, **kwargs): + def require_service(self, service_type, min_microversion=None, **kwargs): """Method to check whether a service exists Usage: @@ -64,7 +65,20 @@ def setUp(self): :returns: True if the service exists, otherwise False. """ try: - self.conn.session.get_endpoint(service_type=service_type, **kwargs) + data = self.conn.session.get_endpoint_data( + service_type=service_type, **kwargs) except _exceptions.EndpointNotFound: self.skipTest('Service {service_type} not found in cloud'.format( service_type=service_type)) + + if not min_microversion: + return + + if not (data.min_microversion and data.max_microversion and + discover.version_between(data.min_microversion, + data.max_microversion, + min_microversion)): + self.skipTest('Service {service_type} does not provide ' + 'microversion {ver}'.format( + service_type=service_type, + ver=min_microversion)) From 14b460946cd352839d590d84f2b7b04f2ad2559d Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Fri, 10 Aug 2018 15:10:47 +0200 Subject: [PATCH 2167/3836] Correct update operations for baremetal The bare metal service only accepts JSON patch format for updating resources. However, we try to sent the whole bodies. This change introduces support for JSON patch into the base Resource. The Image resource, also using JSON patch, is not updated here, because it may end up a breaking change. Change-Id: I4c7866a1246f9e1d8025f5bde1e1930b5d1e2122 --- openstack/baremetal/v1/chassis.py | 1 + openstack/baremetal/v1/node.py | 1 + openstack/baremetal/v1/port.py | 1 + openstack/baremetal/v1/port_group.py | 1 + openstack/resource.py | 32 ++++++++++++++++--- .../baremetal/test_baremetal_chassis.py | 10 ++++++ .../baremetal/test_baremetal_node.py | 29 +++++++++++++++++ .../baremetal/test_baremetal_port.py | 16 ++++++++++ .../baremetal/test_baremetal_port_group.py | 10 ++++++ openstack/tests/unit/test_resource.py | 17 ++++++++++ .../baremetal-update-80effb38aae8e02d.yaml | 5 +++ 11 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/baremetal-update-80effb38aae8e02d.yaml diff --git a/openstack/baremetal/v1/chassis.py b/openstack/baremetal/v1/chassis.py index 32185d7cc..ffe22ebc6 100644 --- a/openstack/baremetal/v1/chassis.py +++ b/openstack/baremetal/v1/chassis.py @@ -27,6 +27,7 @@ class Chassis(resource.Resource): allow_delete = True allow_list = True commit_method = 'PATCH' + commit_jsonpatch = True _query_mapping = resource.QueryParameters( 'fields' diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index c2f82cb81..a0ab01b05 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -34,6 +34,7 @@ class Node(resource.Resource): allow_delete = True allow_list = True commit_method = 'PATCH' + commit_jsonpatch = True _query_mapping = resource.QueryParameters( 'associated', 'driver', 'fields', 'provision_state', 'resource_class', diff --git a/openstack/baremetal/v1/port.py b/openstack/baremetal/v1/port.py index a8083d393..3f12affaf 100644 --- a/openstack/baremetal/v1/port.py +++ b/openstack/baremetal/v1/port.py @@ -27,6 +27,7 @@ class Port(resource.Resource): allow_delete = True allow_list = True commit_method = 'PATCH' + commit_jsonpatch = True _query_mapping = resource.QueryParameters( 'fields' diff --git a/openstack/baremetal/v1/port_group.py b/openstack/baremetal/v1/port_group.py index 3c07d70f3..9ab4c5682 100644 --- a/openstack/baremetal/v1/port_group.py +++ b/openstack/baremetal/v1/port_group.py @@ -27,6 +27,7 @@ class PortGroup(resource.Resource): allow_delete = True allow_list = True commit_method = 'PATCH' + commit_jsonpatch = True _query_mapping = resource.QueryParameters( 'node', 'address', 'fields', diff --git a/openstack/resource.py b/openstack/resource.py index d49b1512a..7d32a45c0 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -34,6 +34,7 @@ class that represent a remote resource. The attributes that import collections import itertools +import jsonpatch from keystoneauth1 import adapter from keystoneauth1 import discover import munch @@ -364,6 +365,8 @@ class Resource(object): commit_method = "PUT" #: Method for creating a resource (POST, PUT) create_method = "POST" + #: Whether commit uses JSON patch format. + commit_jsonpatch = False #: Do calls for this resource require an id requires_id = True @@ -382,6 +385,7 @@ class Resource(object): _header = None _uri = None _computed = None + _original_body = None def __init__(self, _synchronized=False, connection=None, **attrs): """The base resource @@ -418,6 +422,9 @@ def __init__(self, _synchronized=False, connection=None, **attrs): self._computed = _ComponentManager( attributes=computed, synchronized=_synchronized) + if self.commit_jsonpatch: + # We need the original body to compare against + self._original_body = self._body.attributes.copy() def __repr__(self): pairs = [ @@ -756,7 +763,8 @@ def _to_munch(self): return munch.Munch(self.to_dict(body=True, headers=False, original_names=True)) - def _prepare_request(self, requires_id=None, prepend_key=False): + def _prepare_request(self, requires_id=None, prepend_key=False, + patch=False): """Prepare a request to be sent to the server Create operations don't require an ID, but all others do, @@ -765,6 +773,7 @@ def _prepare_request(self, requires_id=None, prepend_key=False): their bodies to be contained within an dict -- if the instance contains a resource_key and prepend_key=True, the body will be wrapped in a dict with that key. + If patch=True, a JSON patch is prepared instead of the full body. Return a _Request object that contains the constructed URI as well a body and headers that are ready to send. @@ -773,9 +782,13 @@ def _prepare_request(self, requires_id=None, prepend_key=False): if requires_id is None: requires_id = self.requires_id - body = self._body.dirty - if prepend_key and self.resource_key is not None: - body = {self.resource_key: body} + if patch: + new = self._body.attributes + body = jsonpatch.make_patch(self._original_body, new).patch + else: + body = self._body.dirty + if prepend_key and self.resource_key is not None: + body = {self.resource_key: body} # TODO(mordred) Ensure headers have string values better than this headers = {} @@ -815,6 +828,9 @@ def _translate_response(self, response, has_body=None, error_message=None): body = self._consume_body_attrs(body) self._body.attributes.update(body) self._body.clean() + if self.commit_jsonpatch: + # We need the original body to compare against + self._original_body = body.copy() headers = self._consume_header_attrs(response.headers) self._header.attributes.update(headers) @@ -1029,7 +1045,13 @@ def commit(self, session, prepend_key=True, has_body=True): if not self.allow_commit: raise exceptions.MethodNotSupported(self, "commit") - request = self._prepare_request(prepend_key=prepend_key) + # Avoid providing patch unconditionally to avoid breaking subclasses + # without it. + kwargs = {} + if self.commit_jsonpatch: + kwargs['patch'] = True + + request = self._prepare_request(prepend_key=prepend_key, **kwargs) session = self._get_session(session) microversion = self._get_microversion_for(session, 'commit') diff --git a/openstack/tests/functional/baremetal/test_baremetal_chassis.py b/openstack/tests/functional/baremetal/test_baremetal_chassis.py index c5bcfa8ab..c830b2101 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_chassis.py +++ b/openstack/tests/functional/baremetal/test_baremetal_chassis.py @@ -27,6 +27,16 @@ def test_chassis_create_get_delete(self): self.assertRaises(exceptions.NotFoundException, self.conn.baremetal.get_chassis, chassis.id) + def test_chassis_update(self): + chassis = self.create_chassis() + chassis.extra = {'answer': 42} + + chassis = self.conn.baremetal.update_chassis(chassis) + self.assertEqual({'answer': 42}, chassis.extra) + + chassis = self.conn.baremetal.get_chassis(chassis.id) + self.assertEqual({'answer': 42}, chassis.extra) + def test_chassis_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" self.assertRaises(exceptions.NotFoundException, diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index 0d44264ad..97fc589c1 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import uuid + from openstack import exceptions from openstack.tests.functional.baremetal import base @@ -39,6 +41,30 @@ def test_node_create_get_delete(self): self.assertRaises(exceptions.NotFoundException, self.conn.baremetal.get_node, self.node_id) + def test_node_update(self): + node = self.create_node(name='node-name', extra={'foo': 'bar'}) + node.name = 'new-name' + node.extra = {'answer': 42} + instance_uuid = str(uuid.uuid4()) + + node = self.conn.baremetal.update_node(node, + instance_uuid=instance_uuid) + self.assertEqual('new-name', node.name) + self.assertEqual({'answer': 42}, node.extra) + self.assertEqual(instance_uuid, node.instance_id) + + node = self.conn.baremetal.get_node('new-name') + self.assertEqual('new-name', node.name) + self.assertEqual({'answer': 42}, node.extra) + self.assertEqual(instance_uuid, node.instance_id) + + node = self.conn.baremetal.update_node(node, + instance_uuid=None) + self.assertIsNone(node.instance_id) + + node = self.conn.baremetal.get_node('new-name') + self.assertIsNone(node.instance_id) + def test_node_create_in_enroll_provide(self): node = self.create_node(provision_state='enroll') self.node_id = node.id @@ -66,5 +92,8 @@ def test_node_negative_non_existing(self): self.assertRaises(exceptions.NotFoundException, self.conn.baremetal.delete_node, uuid, ignore_missing=False) + self.assertRaises(exceptions.NotFoundException, + self.conn.baremetal.update_node, uuid, + name='new-name') self.assertIsNone(self.conn.baremetal.find_node(uuid)) self.assertIsNone(self.conn.baremetal.delete_node(uuid)) diff --git a/openstack/tests/functional/baremetal/test_baremetal_port.py b/openstack/tests/functional/baremetal/test_baremetal_port.py index 602120979..6ea6d0188 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_port.py +++ b/openstack/tests/functional/baremetal/test_baremetal_port.py @@ -36,6 +36,19 @@ def test_port_create_get_delete(self): self.assertRaises(exceptions.NotFoundException, self.conn.baremetal.get_port, port.id) + def test_port_update(self): + port = self.create_port(address='11:22:33:44:55:66') + port.address = '66:55:44:33:22:11' + port.extra = {'answer': 42} + + port = self.conn.baremetal.update_port(port) + self.assertEqual('66:55:44:33:22:11', port.address) + self.assertEqual({'answer': 42}, port.extra) + + port = self.conn.baremetal.get_port(port.id) + self.assertEqual('66:55:44:33:22:11', port.address) + self.assertEqual({'answer': 42}, port.extra) + def test_port_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" self.assertRaises(exceptions.NotFoundException, @@ -46,5 +59,8 @@ def test_port_negative_non_existing(self): self.assertRaises(exceptions.NotFoundException, self.conn.baremetal.delete_port, uuid, ignore_missing=False) + self.assertRaises(exceptions.NotFoundException, + self.conn.baremetal.update_port, uuid, + pxe_enabled=True) self.assertIsNone(self.conn.baremetal.find_port(uuid)) self.assertIsNone(self.conn.baremetal.delete_port(uuid)) diff --git a/openstack/tests/functional/baremetal/test_baremetal_port_group.py b/openstack/tests/functional/baremetal/test_baremetal_port_group.py index 6ed8041df..3822e9eb3 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_port_group.py +++ b/openstack/tests/functional/baremetal/test_baremetal_port_group.py @@ -34,6 +34,16 @@ def test_port_group_create_get_delete(self): self.assertRaises(exceptions.NotFoundException, self.conn.baremetal.get_port_group, port_group.id) + def test_port_group_update(self): + port_group = self.create_port_group() + port_group.extra = {'answer': 42} + + port_group = self.conn.baremetal.update_port_group(port_group) + self.assertEqual({'answer': 42}, port_group.extra) + + port_group = self.conn.baremetal.get_port_group(port_group.id) + self.assertEqual({'answer': 42}, port_group.extra) + def test_port_group_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" self.assertRaises(exceptions.NotFoundException, diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index ce133f2e9..f8d7a19c5 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -864,6 +864,23 @@ class Test(resource.Resource): self.assertEqual({key: {"x": body_value}}, result.body) self.assertEqual({"y": header_value}, result.headers) + def test__prepare_request_with_patch(self): + class Test(resource.Resource): + commit_jsonpatch = True + base_path = "/something" + x = resource.Body("x") + y = resource.Body("y") + + the_id = "id" + sot = Test(id=the_id, x=1, y=2) + sot.x = 3 + + result = sot._prepare_request(requires_id=True, patch=True) + + self.assertEqual("something/id", result.url) + self.assertEqual([{'op': 'replace', 'path': '/x', 'value': 3}], + result.body) + def test__translate_response_no_body(self): class Test(resource.Resource): attr = resource.Header("attr") diff --git a/releasenotes/notes/baremetal-update-80effb38aae8e02d.yaml b/releasenotes/notes/baremetal-update-80effb38aae8e02d.yaml new file mode 100644 index 000000000..45ddbb254 --- /dev/null +++ b/releasenotes/notes/baremetal-update-80effb38aae8e02d.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Correct updating bare metal resources. Previously an incorrect body used + to be sent. From 04f7af7a5a032cfeedb0e0925c90606ba6d7326f Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Fri, 10 Aug 2018 15:10:47 +0200 Subject: [PATCH 2168/3836] Use the base Resource's JSON patch support in Image Note that as a side effect, the image service resources no longer allow setting unsupported attributes. Their support was a bit awkward since they were essentially write-only. On the other hand, this change can be breaking for consumers that rely on it. Change-Id: I04dc37e0570b84c80d7c19fd090ba7043e70bd74 --- openstack/image/v2/_proxy.py | 3 +- openstack/image/v2/image.py | 39 +++++++------------ openstack/tests/unit/image/v2/test_image.py | 28 ++++++------- .../notes/image-update-76bd3bf24c1c1380.yaml | 5 +++ 4 files changed, 30 insertions(+), 45 deletions(-) create mode 100644 releasenotes/notes/image-update-76bd3bf24c1c1380.yaml diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 3a689d991..3b2d65f3c 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -157,8 +157,7 @@ def update_image(self, image, **attrs): :returns: The updated image :rtype: :class:`~openstack.image.v2.image.Image` """ - res = self._get_resource(_image.Image, image) - return res.commit(self, **attrs) + return self._update(_image.Image, image, **attrs) def deactivate_image(self, image): """Deactivate an image diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index 42f54e23c..bcf4681f9 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -12,9 +12,6 @@ import hashlib -import copy -import jsonpatch - from openstack import _log from openstack import exceptions from openstack.image import image_service @@ -36,6 +33,7 @@ class Image(resource.Resource): allow_delete = True allow_list = True commit_method = 'PATCH' + commit_jsonpatch = True _query_mapping = resource.QueryParameters( "name", "visibility", @@ -114,12 +112,6 @@ class Image(resource.Resource): #: when you set the show_image_direct_url option to true in the #: Image service's configuration file. direct_url = resource.Body('direct_url') - #: An image property. - path = resource.Body('path') - #: Value of image property used in add or replace operations expressed - #: in JSON notation. For example, you must enclose strings in quotation - #: marks, and you do not enclose numeric values in quotation marks. - value = resource.Body('value') #: The URL to access the image file kept in external store. url = resource.Body('url') #: The location metadata. @@ -294,21 +286,16 @@ def download(self, session, stream=False): return resp.content - def commit(self, session, **attrs): - url = utils.urljoin(self.base_path, self.id) - headers = { - 'Content-Type': 'application/openstack-images-v2.1-json-patch', - 'Accept': '' - } - original = self.to_dict() - - # Update values from **attrs so they can be passed to jsonpatch - new = copy.deepcopy(self.to_dict()) - new.update(**attrs) + def _prepare_request(self, requires_id=None, prepend_key=False, + patch=False): + request = super(Image, self)._prepare_request(requires_id=requires_id, + prepend_key=prepend_key, + patch=patch) + if patch: + headers = { + 'Content-Type': 'application/openstack-images-v2.1-json-patch', + 'Accept': '' + } + request.headers.update(headers) - patch_string = jsonpatch.make_patch(original, new).to_string() - resp = session.patch(url, - data=patch_string, - headers=headers) - self._translate_response(resp, has_body=True) - return self + return request diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index b5a8b870c..e2d1ba1b1 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import json import operator from keystoneauth1 import adapter @@ -45,8 +44,6 @@ 'file': '15', 'locations': ['15', '16'], 'direct_url': '17', - 'path': '18', - 'value': '19', 'url': '20', 'metadata': {'21': '22'}, 'architecture': '23', @@ -142,8 +139,6 @@ def test_make_it(self): self.assertEqual(EXAMPLE['file'], sot.file) self.assertEqual(EXAMPLE['locations'], sot.locations) self.assertEqual(EXAMPLE['direct_url'], sot.direct_url) - self.assertEqual(EXAMPLE['path'], sot.path) - self.assertEqual(EXAMPLE['value'], sot.value) self.assertEqual(EXAMPLE['url'], sot.url) self.assertEqual(EXAMPLE['metadata'], sot.metadata) self.assertEqual(EXAMPLE['architecture'], sot.architecture) @@ -312,7 +307,9 @@ def test_download_stream(self): self.assertEqual(rv, resp) def test_image_update(self): - sot = image.Image(**EXAMPLE) + values = EXAMPLE.copy() + del values['instance_uuid'] + sot = image.Image(**values) # Let the translate pass through, that portion is tested elsewhere sot._translate_response = mock.Mock() @@ -326,21 +323,18 @@ def test_image_update(self): resp.status_code = 200 self.sess.patch.return_value = resp - value = ('[{"value": "fake_name", "op": "replace", "path": "/name"}, ' - '{"value": "fake_value", "op": "add", ' - '"path": "/new_property"}]') - fake_img = sot.to_dict() - fake_img['name'] = 'fake_name' - fake_img['new_property'] = 'fake_value' + value = [{"value": "fake_name", "op": "replace", "path": "/name"}, + {"value": "fake_value", "op": "add", + "path": "/instance_uuid"}] - sot.commit(self.sess, **fake_img) + sot.name = 'fake_name' + sot.instance_uuid = 'fake_value' + sot.commit(self.sess) url = 'images/' + IDENTIFIER self.sess.patch.assert_called_once() call = self.sess.patch.call_args call_args, call_kwargs = call self.assertEqual(url, call_args[0]) self.assertEqual( - sorted(json.loads(value), key=operator.itemgetter('value')), - sorted( - json.loads(call_kwargs['data']), - key=operator.itemgetter('value'))) + sorted(value, key=operator.itemgetter('value')), + sorted(call_kwargs['json'], key=operator.itemgetter('value'))) diff --git a/releasenotes/notes/image-update-76bd3bf24c1c1380.yaml b/releasenotes/notes/image-update-76bd3bf24c1c1380.yaml new file mode 100644 index 000000000..47599e085 --- /dev/null +++ b/releasenotes/notes/image-update-76bd3bf24c1c1380.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + When using the Image API, it is no longer possible to set arbitrary + properties, not known to the SDK, via ``image.update_image`` API. From d87624069f82fa766eb6f1575cdd33899822e6db Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Mon, 13 Aug 2018 11:50:57 +0200 Subject: [PATCH 2169/3836] baremetal: add support for VIF attach/detach API Change-Id: Ifb311531e9750502a60afac0961e4919c4f8f1e5 --- doc/source/user/proxies/baremetal.rst | 8 ++ openstack/baremetal/v1/_common.py | 3 + openstack/baremetal/v1/_proxy.py | 55 +++++++++ openstack/baremetal/v1/node.py | 105 +++++++++++++++++- openstack/cloud/openstackcloud.py | 34 ++++++ .../baremetal/test_baremetal_node.py | 31 ++++++ .../tests/unit/baremetal/v1/test_node.py | 61 ++++++++++ openstack/tests/unit/base.py | 9 +- .../tests/unit/cloud/test_baremetal_node.py | 82 ++++++++++++++ .../notes/baremetal-vif-122457118c722a9b.yaml | 4 + 10 files changed, 386 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/baremetal-vif-122457118c722a9b.yaml diff --git a/doc/source/user/proxies/baremetal.rst b/doc/source/user/proxies/baremetal.rst index 578724074..b3bacb035 100644 --- a/doc/source/user/proxies/baremetal.rst +++ b/doc/source/user/proxies/baremetal.rst @@ -65,6 +65,14 @@ Chassis Operations .. automethod:: openstack.baremetal.v1._proxy.Proxy.find_chassis .. automethod:: openstack.baremetal.v1._proxy.Proxy.chassis +VIF Operations +^^^^^^^^^^^^^^ +.. autoclass:: openstack.baremetal.v1._proxy.Proxy + + .. automethod:: openstack.baremetal.v1._proxy.Proxy.attach_vif_to_node + .. automethod:: openstack.baremetal.v1._proxy.Proxy.detach_vif_from_node + .. automethod:: openstack.baremetal.v1._proxy.Proxy.list_node_vifs + Deprecated Methods ^^^^^^^^^^^^^^^^^^ diff --git a/openstack/baremetal/v1/_common.py b/openstack/baremetal/v1/_common.py index ffa1ee668..4be2bd47e 100644 --- a/openstack/baremetal/v1/_common.py +++ b/openstack/baremetal/v1/_common.py @@ -52,3 +52,6 @@ 'manageable': '1.4', } """API versions when certain states were introduced.""" + +VIF_VERSION = '1.28' +"""API version in which the VIF operations were introduced.""" diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index a838faa62..abf02dc4d 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -702,3 +702,58 @@ def delete_port_group(self, port_group, ignore_missing=True): """ return self._delete(_portgroup.PortGroup, port_group, ignore_missing=ignore_missing) + + def attach_vif_to_node(self, node, vif_id): + """Attach a VIF to the node. + + The exact form of the VIF ID depends on the network interface used by + the node. In the most common case it is a Network service port + (NOT a Bare Metal port) ID. A VIF can only be attached to one node + at a time. + + :param node: The value can be either the name or ID of a node or + a :class:`~openstack.baremetal.v1.node.Node` instance. + :param string vif_id: Backend-specific VIF ID. + :return: ``None`` + :raises: :exc:`~openstack.exceptions.NotSupported` if the server + does not support the VIF API. + """ + res = self._get_resource(_node.Node, node) + res.attach_vif(self, vif_id) + + def detach_vif_from_node(self, node, vif_id, ignore_missing=True): + """Detach a VIF from the node. + + The exact form of the VIF ID depends on the network interface used by + the node. In the most common case it is a Network service port + (NOT a Bare Metal port) ID. + + :param node: The value can be either the name or ID of a node or + a :class:`~openstack.baremetal.v1.node.Node` instance. + :param string vif_id: Backend-specific VIF ID. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the VIF does not exist. Otherwise, ``False`` + is returned. + :return: ``True`` if the VIF was detached, otherwise ``False``. + :raises: :exc:`~openstack.exceptions.NotSupported` if the server + does not support the VIF API. + """ + res = self._get_resource(_node.Node, node) + return res.detach_vif(self, vif_id, ignore_missing=ignore_missing) + + def list_node_vifs(self, node): + """List IDs of VIFs attached to the node. + + The exact form of the VIF ID depends on the network interface used by + the node. In the most common case it is a Network service port + (NOT a Bare Metal port) ID. + + :param node: The value can be either the name or ID of a node or + a :class:`~openstack.baremetal.v1.node.Node` instance. + :return: List of VIF IDs as strings. + :raises: :exc:`~openstack.exceptions.NotSupported` if the server + does not support the VIF API. + """ + res = self._get_resource(_node.Node, node) + return res.list_vifs(self) diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index c2f82cb81..1736b7290 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -41,8 +41,8 @@ class Node(resource.Resource): is_maintenance='maintenance', ) - # Full port groups support introduced in 1.24 - _max_microversion = '1.24' + # VIF attach/detach support introduced in 1.28. + _max_microversion = '1.28' # Properties #: The UUID of the chassis associated wit this node. Can be empty or None. @@ -341,6 +341,107 @@ def _check_state_reached(self, session, expected_state, "the last error is %(error)s" % {'node': self.id, 'error': self.last_error}) + def attach_vif(self, session, vif_id): + """Attach a VIF to the node. + + The exact form of the VIF ID depends on the network interface used by + the node. In the most common case it is a Network service port + (NOT a Bare Metal port) ID. A VIF can only be attached to one node + at a time. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param string vif_id: Backend-specific VIF ID. + :return: ``None`` + :raises: :exc:`~openstack.exceptions.NotSupported` if the server + does not support the VIF API. + """ + session = self._get_session(session) + version = self._assert_microversion_for( + session, 'commit', _common.VIF_VERSION, + error_message=("Cannot use VIF attachment API")) + + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'vifs') + body = {'id': vif_id} + response = session.post( + request.url, json=body, + headers=request.headers, microversion=version, + # NOTE(dtantsur): do not retry CONFLICT, it's a valid status code + # in this API when the VIF is already attached to another node. + retriable_status_codes=[503]) + + msg = ("Failed to attach VIF {vif} to bare metal node {node}" + .format(node=self.id, vif=vif_id)) + exceptions.raise_from_response(response, error_message=msg) + + def detach_vif(self, session, vif_id, ignore_missing=True): + """Detach a VIF from the node. + + The exact form of the VIF ID depends on the network interface used by + the node. In the most common case it is a Network service port + (NOT a Bare Metal port) ID. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param string vif_id: Backend-specific VIF ID. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the VIF does not exist. Otherwise, ``False`` + is returned. + :return: ``True`` if the VIF was detached, otherwise ``False``. + :raises: :exc:`~openstack.exceptions.NotSupported` if the server + does not support the VIF API. + """ + session = self._get_session(session) + version = self._assert_microversion_for( + session, 'commit', _common.VIF_VERSION, + error_message=("Cannot use VIF attachment API")) + + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'vifs', vif_id) + response = session.delete( + request.url, headers=request.headers, microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + if ignore_missing and response.status_code == 400: + _logger.debug('VIF %(vif)s was already removed from node %(node)s', + {'vif': vif_id, 'node': self.id}) + return False + + msg = ("Failed to detach VIF {vif} from bare metal node {node}" + .format(node=self.id, vif=vif_id)) + exceptions.raise_from_response(response, error_message=msg) + return True + + def list_vifs(self, session): + """List IDs of VIFs attached to the node. + + The exact form of the VIF ID depends on the network interface used by + the node. In the most common case it is a Network service port + (NOT a Bare Metal port) ID. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :return: List of VIF IDs as strings. + :raises: :exc:`~openstack.exceptions.NotSupported` if the server + does not support the VIF API. + """ + session = self._get_session(session) + version = self._assert_microversion_for( + session, 'fetch', _common.VIF_VERSION, + error_message=("Cannot use VIF attachment API")) + + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'vifs') + response = session.get( + request.url, headers=request.headers, microversion=version) + + msg = ("Failed to list VIFs attached to bare metal node {node}" + .format(node=self.id)) + exceptions.raise_from_response(response, error_message=msg) + return [vif['id'] for vif in response.json()['vifs']] + class NodeDetail(Node): diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 8ba114599..8de706eba 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -9821,6 +9821,40 @@ def update_machine(self, name_or_id, chassis_uuid=None, driver=None, changes=change_list ) + def attach_port_to_machine(self, name_or_id, port_name_or_id): + """Attach a virtual port to the bare metal machine. + + :param string name_or_id: A machine name or UUID. + :param string port_name_or_id: A port name or UUID. + Note that this is a Network service port, not a bare metal NIC. + :return: Nothing. + """ + machine = self.get_machine(name_or_id) + port = self.get_port(port_name_or_id) + self.baremetal.attach_vif_to_node(machine, port['id']) + + def detach_port_from_machine(self, name_or_id, port_name_or_id): + """Detach a virtual port from the bare metal machine. + + :param string name_or_id: A machine name or UUID. + :param string port_name_or_id: A port name or UUID. + Note that this is a Network service port, not a bare metal NIC. + :return: Nothing. + """ + machine = self.get_machine(name_or_id) + port = self.get_port(port_name_or_id) + self.baremetal.detach_vif_from_node(machine, port['id']) + + def list_ports_attached_to_machine(self, name_or_id): + """List virtual ports attached to the bare metal machine. + + :param string name_or_id: A machine name or UUID. + :returns: List of ``munch.Munch`` representing the ports. + """ + machine = self.get_machine(name_or_id) + vif_ids = self.baremetal.list_node_vifs(machine) + return [self.get_port(vif) for vif in vif_ids] + def validate_node(self, uuid): # TODO(TheJulia): There are soooooo many other interfaces # that we can support validating, while these are essential, diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index 0d44264ad..a9e005b97 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -68,3 +68,34 @@ def test_node_negative_non_existing(self): ignore_missing=False) self.assertIsNone(self.conn.baremetal.find_node(uuid)) self.assertIsNone(self.conn.baremetal.delete_node(uuid)) + + +class TestBareMetalVif(base.BaseBaremetalTest): + + min_microversion = '1.28' + + def setUp(self): + super(TestBareMetalVif, self).setUp() + self.node = self.create_node(network_interface='noop') + self.vif_id = "200712fc-fdfb-47da-89a6-2d19f76c7618" + + def test_node_vif_attach_detach(self): + self.conn.baremetal.attach_vif_to_node(self.node, self.vif_id) + # NOTE(dtantsur): The noop networking driver is completely noop - the + # VIF list does not return anything of value. + self.conn.baremetal.list_node_vifs(self.node) + res = self.conn.baremetal.detach_vif_from_node(self.node, self.vif_id, + ignore_missing=False) + self.assertTrue(res) + + def test_node_vif_negative(self): + uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" + self.assertRaises(exceptions.NotFoundException, + self.conn.baremetal.attach_vif_to_node, + uuid, self.vif_id) + self.assertRaises(exceptions.NotFoundException, + self.conn.baremetal.list_node_vifs, + uuid) + self.assertRaises(exceptions.NotFoundException, + self.conn.baremetal.detach_vif_from_node, + uuid, self.vif_id, ignore_missing=False) diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 3629b1adc..e965590f9 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -13,6 +13,7 @@ from keystoneauth1 import adapter import mock +from openstack.baremetal.v1 import _common from openstack.baremetal.v1 import node from openstack import exceptions from openstack.tests.unit import base @@ -371,3 +372,63 @@ def test_manageable_new_version(self, mock_prov): headers=mock.ANY, microversion=self.session.default_microversion) mock_prov.assert_called_once_with(self.node, self.session, 'manage', wait=True) + + +@mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) +@mock.patch.object(node.Node, '_get_session', lambda self, x: x) +class TestNodeVif(base.TestCase): + + def setUp(self): + super(TestNodeVif, self).setUp() + self.session = mock.Mock(spec=adapter.Adapter) + self.session.default_microversion = '1.28' + self.node = node.Node(id='c29db401-b6a7-4530-af8e-20a720dee946', + driver=FAKE['driver']) + self.vif_id = '714bdf6d-2386-4b5e-bd0d-bc036f04b1ef' + + def test_attach_vif(self): + self.assertIsNone(self.node.attach_vif(self.session, self.vif_id)) + self.session.post.assert_called_once_with( + 'nodes/%s/vifs' % self.node.id, json={'id': self.vif_id}, + headers=mock.ANY, microversion='1.28', + retriable_status_codes=[503]) + + def test_detach_vif_existing(self): + self.assertTrue(self.node.detach_vif(self.session, self.vif_id)) + self.session.delete.assert_called_once_with( + 'nodes/%s/vifs/%s' % (self.node.id, self.vif_id), + headers=mock.ANY, microversion='1.28', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + def test_detach_vif_missing(self): + self.session.delete.return_value.status_code = 400 + self.assertFalse(self.node.detach_vif(self.session, self.vif_id)) + self.session.delete.assert_called_once_with( + 'nodes/%s/vifs/%s' % (self.node.id, self.vif_id), + headers=mock.ANY, microversion='1.28', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + def test_list_vifs(self): + self.session.get.return_value.json.return_value = { + 'vifs': [ + {'id': '1234'}, + {'id': '5678'}, + ] + } + res = self.node.list_vifs(self.session) + self.assertEqual(['1234', '5678'], res) + self.session.get.assert_called_once_with( + 'nodes/%s/vifs' % self.node.id, + headers=mock.ANY, microversion='1.28') + + def test_incompatible_microversion(self): + self.session.default_microversion = '1.1' + self.assertRaises(exceptions.NotSupported, + self.node.attach_vif, + self.session, self.vif_id) + self.assertRaises(exceptions.NotSupported, + self.node.detach_vif, + self.session, self.vif_id) + self.assertRaises(exceptions.NotSupported, + self.node.list_vifs, + self.session) diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 2b115a1ce..8062486bf 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -689,7 +689,8 @@ def setUp(self): self.uuid = str(uuid.uuid4()) self.name = self.getUniqueString('name') - def get_mock_url(self, resource=None, append=None, qs_elements=None): - return super(IronicTestCase, self).get_mock_url( - service_type='baremetal', interface='public', resource=resource, - append=append, base_url_append='v1', qs_elements=qs_elements) + def get_mock_url(self, **kwargs): + kwargs.setdefault('service_type', 'baremetal') + kwargs.setdefault('interface', 'public') + kwargs.setdefault('base_url_append', 'v1') + return super(IronicTestCase, self).get_mock_url(**kwargs) diff --git a/openstack/tests/unit/cloud/test_baremetal_node.py b/openstack/tests/unit/cloud/test_baremetal_node.py index 83ac06b77..da1ae81c1 100644 --- a/openstack/tests/unit/cloud/test_baremetal_node.py +++ b/openstack/tests/unit/cloud/test_baremetal_node.py @@ -1609,6 +1609,88 @@ def test_update_machine_patch_no_action(self): self.assert_calls() + def test_attach_port_to_machine(self): + vif_id = '953ccbee-e854-450f-95fe-fe5e40d611ec' + self.register_uris([ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + dict( + method='GET', + uri=self.get_mock_url( + service_type='network', + resource='ports.json', + base_url_append='v2.0'), + json={'ports': [{'id': vif_id}]}), + dict( + method='POST', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], 'vifs'])), + ]) + self.cloud.attach_port_to_machine(self.fake_baremetal_node['uuid'], + vif_id) + self.assert_calls() + + def test_detach_port_from_machine(self): + vif_id = '953ccbee-e854-450f-95fe-fe5e40d611ec' + self.register_uris([ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + dict( + method='GET', + uri=self.get_mock_url( + service_type='network', + resource='ports.json', + base_url_append='v2.0'), + json={'ports': [{'id': vif_id}]}), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], 'vifs', + vif_id])), + ]) + self.cloud.detach_port_from_machine(self.fake_baremetal_node['uuid'], + vif_id) + self.assert_calls() + + def test_list_ports_attached_to_machine(self): + vif_id = '953ccbee-e854-450f-95fe-fe5e40d611ec' + fake_port = {'id': vif_id, 'name': 'test'} + self.register_uris([ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], 'vifs']), + json={'vifs': [{'id': vif_id}]}), + dict( + method='GET', + uri=self.get_mock_url( + service_type='network', + resource='ports.json', + base_url_append='v2.0'), + json={'ports': [fake_port]}), + ]) + res = self.cloud.list_ports_attached_to_machine( + self.fake_baremetal_node['uuid']) + self.assert_calls() + self.assertEqual([fake_port], res) + class TestUpdateMachinePatch(base.IronicTestCase): # NOTE(TheJulia): As appears, and mordred describes, diff --git a/releasenotes/notes/baremetal-vif-122457118c722a9b.yaml b/releasenotes/notes/baremetal-vif-122457118c722a9b.yaml new file mode 100644 index 000000000..061d703a1 --- /dev/null +++ b/releasenotes/notes/baremetal-vif-122457118c722a9b.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Implements VIF attach/detach API for bare metal nodes. From 934725d6cb3eafad767f8ffbe49a911dc14fb636 Mon Sep 17 00:00:00 2001 From: melissaml Date: Fri, 17 Aug 2018 14:35:16 +0800 Subject: [PATCH 2170/3836] Remove the duplicated word Change-Id: I1808e0277fe90fabec5888f38443366f39e72a00 --- openstack/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/connection.py b/openstack/connection.py index 86cc33c54..4a9f4e135 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -195,7 +195,7 @@ def from_config(cloud=None, config=None, options=None, **kwargs): :param argparse.Namespace options: Allows direct passing in of options to be added to the cloud config. This does not have to be an actual instance of argparse.Namespace, - despite the naming of the the + despite the naming of the `openstack.config.loader.OpenStackConfig.get_one` argument to which it is passed. From 4a34f8c6521d4fa55a182e154877a3b358107783 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Tue, 21 Aug 2018 14:50:51 +0200 Subject: [PATCH 2171/3836] Alias NotFoundException to ResourceNotFound All resource proxies claim to raise ResourceNotFound, while they actually raise its ancestor - NotFoundException. This may cause confusion e.g. if trying to catch ResourceNotFound. To solve this issue without breaking backward compatibility, alias NotFoundException to ResourceNotFound. ResourceNotFound becomes the "primary" exception in this pair because it's actively advertised in docstrings of all proxy methods. Change-Id: I34b119e344e65635851a94addaad5114d074b255 --- openstack/exceptions.py | 10 ++++------ openstack/orchestration/v1/stack.py | 2 +- openstack/proxy.py | 2 +- openstack/resource.py | 6 ++++++ .../baremetal/test_baremetal_chassis.py | 8 ++++---- .../functional/baremetal/test_baremetal_node.py | 16 ++++++++-------- .../functional/baremetal/test_baremetal_port.py | 10 +++++----- .../baremetal/test_baremetal_port_group.py | 8 ++++---- .../functional/orchestration/v1/test_stack.py | 2 +- .../tests/unit/orchestration/v1/test_stack.py | 6 +++--- openstack/tests/unit/test_proxy.py | 10 +++++----- openstack/tests/unit/test_resource.py | 4 ++-- 12 files changed, 44 insertions(+), 40 deletions(-) diff --git a/openstack/exceptions.py b/openstack/exceptions.py index 8d5ca7dcf..c6687e080 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -108,11 +108,6 @@ def __str__(self): return self.__unicode__() -class NotFoundException(HttpException): - """HTTP 404 Not Found.""" - pass - - class BadRequestException(HttpException): """HTTP 400 Bad Request.""" pass @@ -142,11 +137,14 @@ class DuplicateResource(SDKException): pass -class ResourceNotFound(NotFoundException): +class ResourceNotFound(HttpException): """No resource exists with that name or id.""" pass +NotFoundException = ResourceNotFound + + class ResourceTimeout(SDKException): """Timeout waiting for resource.""" pass diff --git a/openstack/orchestration/v1/stack.py b/openstack/orchestration/v1/stack.py index ea4c1c889..4828b1332 100644 --- a/openstack/orchestration/v1/stack.py +++ b/openstack/orchestration/v1/stack.py @@ -102,7 +102,7 @@ def fetch(self, session, requires_id=True, error_message=None): requires_id=requires_id, error_message=error_message) if stk and stk.status in ['DELETE_COMPLETE', 'ADOPT_COMPLETE']: - raise exceptions.NotFoundException( + raise exceptions.ResourceNotFound( "No stack found for %s" % stk.id) return stk diff --git a/openstack/proxy.py b/openstack/proxy.py index ca84c8738..cc6a751ec 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -143,7 +143,7 @@ def _delete(self, resource_type, value, ignore_missing=True, **attrs): try: rv = res.delete(self) - except exceptions.NotFoundException: + except exceptions.ResourceNotFound: if ignore_missing: return None raise diff --git a/openstack/resource.py b/openstack/resource.py index 3a02bbc18..07a822c5b 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1054,6 +1054,8 @@ def fetch(self, session, requires_id=True, error_message=None): :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if :data:`Resource.allow_fetch` is not set to ``True``. + :raises: :exc:`~openstack.exceptions.ResourceNotFound` if + the resource was not found. """ if not self.allow_fetch: raise exceptions.MethodNotSupported(self, "fetch") @@ -1079,6 +1081,8 @@ def head(self, session): :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if :data:`Resource.allow_head` is not set to ``True``. + :raises: :exc:`~openstack.exceptions.ResourceNotFound` if + the resource was not found. """ if not self.allow_head: raise exceptions.MethodNotSupported(self, "head") @@ -1157,6 +1161,8 @@ def delete(self, session, error_message=None): :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if :data:`Resource.allow_commit` is not set to ``True``. + :raises: :exc:`~openstack.exceptions.ResourceNotFound` if + the resource was not found. """ if not self.allow_delete: raise exceptions.MethodNotSupported(self, "delete") diff --git a/openstack/tests/functional/baremetal/test_baremetal_chassis.py b/openstack/tests/functional/baremetal/test_baremetal_chassis.py index c830b2101..2947e0bb6 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_chassis.py +++ b/openstack/tests/functional/baremetal/test_baremetal_chassis.py @@ -24,7 +24,7 @@ def test_chassis_create_get_delete(self): self.assertEqual(loaded.id, chassis.id) self.conn.baremetal.delete_chassis(chassis, ignore_missing=False) - self.assertRaises(exceptions.NotFoundException, + self.assertRaises(exceptions.ResourceNotFound, self.conn.baremetal.get_chassis, chassis.id) def test_chassis_update(self): @@ -39,12 +39,12 @@ def test_chassis_update(self): def test_chassis_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" - self.assertRaises(exceptions.NotFoundException, + self.assertRaises(exceptions.ResourceNotFound, self.conn.baremetal.get_chassis, uuid) - self.assertRaises(exceptions.NotFoundException, + self.assertRaises(exceptions.ResourceNotFound, self.conn.baremetal.find_chassis, uuid, ignore_missing=False) - self.assertRaises(exceptions.NotFoundException, + self.assertRaises(exceptions.ResourceNotFound, self.conn.baremetal.delete_chassis, uuid, ignore_missing=False) self.assertIsNone(self.conn.baremetal.find_chassis(uuid)) diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index cb12b2923..04c5d8acc 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -38,7 +38,7 @@ def test_node_create_get_delete(self): self.assertIn(node.id, [n.id for n in nodes]) self.conn.baremetal.delete_node(node, ignore_missing=False) - self.assertRaises(exceptions.NotFoundException, + self.assertRaises(exceptions.ResourceNotFound, self.conn.baremetal.get_node, self.node_id) def test_node_update(self): @@ -84,15 +84,15 @@ def test_node_create_in_enroll_provide(self): def test_node_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" - self.assertRaises(exceptions.NotFoundException, + self.assertRaises(exceptions.ResourceNotFound, self.conn.baremetal.get_node, uuid) - self.assertRaises(exceptions.NotFoundException, + self.assertRaises(exceptions.ResourceNotFound, self.conn.baremetal.find_node, uuid, ignore_missing=False) - self.assertRaises(exceptions.NotFoundException, + self.assertRaises(exceptions.ResourceNotFound, self.conn.baremetal.delete_node, uuid, ignore_missing=False) - self.assertRaises(exceptions.NotFoundException, + self.assertRaises(exceptions.ResourceNotFound, self.conn.baremetal.update_node, uuid, name='new-name') self.assertIsNone(self.conn.baremetal.find_node(uuid)) @@ -119,12 +119,12 @@ def test_node_vif_attach_detach(self): def test_node_vif_negative(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" - self.assertRaises(exceptions.NotFoundException, + self.assertRaises(exceptions.ResourceNotFound, self.conn.baremetal.attach_vif_to_node, uuid, self.vif_id) - self.assertRaises(exceptions.NotFoundException, + self.assertRaises(exceptions.ResourceNotFound, self.conn.baremetal.list_node_vifs, uuid) - self.assertRaises(exceptions.NotFoundException, + self.assertRaises(exceptions.ResourceNotFound, self.conn.baremetal.detach_vif_from_node, uuid, self.vif_id, ignore_missing=False) diff --git a/openstack/tests/functional/baremetal/test_baremetal_port.py b/openstack/tests/functional/baremetal/test_baremetal_port.py index 6ea6d0188..29bd6b887 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_port.py +++ b/openstack/tests/functional/baremetal/test_baremetal_port.py @@ -33,7 +33,7 @@ def test_port_create_get_delete(self): self.assertEqual(loaded.id, port.id) self.conn.baremetal.delete_port(port, ignore_missing=False) - self.assertRaises(exceptions.NotFoundException, + self.assertRaises(exceptions.ResourceNotFound, self.conn.baremetal.get_port, port.id) def test_port_update(self): @@ -51,15 +51,15 @@ def test_port_update(self): def test_port_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" - self.assertRaises(exceptions.NotFoundException, + self.assertRaises(exceptions.ResourceNotFound, self.conn.baremetal.get_port, uuid) - self.assertRaises(exceptions.NotFoundException, + self.assertRaises(exceptions.ResourceNotFound, self.conn.baremetal.find_port, uuid, ignore_missing=False) - self.assertRaises(exceptions.NotFoundException, + self.assertRaises(exceptions.ResourceNotFound, self.conn.baremetal.delete_port, uuid, ignore_missing=False) - self.assertRaises(exceptions.NotFoundException, + self.assertRaises(exceptions.ResourceNotFound, self.conn.baremetal.update_port, uuid, pxe_enabled=True) self.assertIsNone(self.conn.baremetal.find_port(uuid)) diff --git a/openstack/tests/functional/baremetal/test_baremetal_port_group.py b/openstack/tests/functional/baremetal/test_baremetal_port_group.py index 3822e9eb3..819e4d67e 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_port_group.py +++ b/openstack/tests/functional/baremetal/test_baremetal_port_group.py @@ -31,7 +31,7 @@ def test_port_group_create_get_delete(self): self.conn.baremetal.delete_port_group(port_group, ignore_missing=False) - self.assertRaises(exceptions.NotFoundException, + self.assertRaises(exceptions.ResourceNotFound, self.conn.baremetal.get_port_group, port_group.id) def test_port_group_update(self): @@ -46,12 +46,12 @@ def test_port_group_update(self): def test_port_group_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" - self.assertRaises(exceptions.NotFoundException, + self.assertRaises(exceptions.ResourceNotFound, self.conn.baremetal.get_port_group, uuid) - self.assertRaises(exceptions.NotFoundException, + self.assertRaises(exceptions.ResourceNotFound, self.conn.baremetal.find_port_group, uuid, ignore_missing=False) - self.assertRaises(exceptions.NotFoundException, + self.assertRaises(exceptions.ResourceNotFound, self.conn.baremetal.delete_port_group, uuid, ignore_missing=False) self.assertIsNone(self.conn.baremetal.find_port_group(uuid)) diff --git a/openstack/tests/functional/orchestration/v1/test_stack.py b/openstack/tests/functional/orchestration/v1/test_stack.py index c0c12bcfd..76ae4cf7d 100644 --- a/openstack/tests/functional/orchestration/v1/test_stack.py +++ b/openstack/tests/functional/orchestration/v1/test_stack.py @@ -65,7 +65,7 @@ def tearDown(self): try: self.conn.orchestration.wait_for_status( self.stack, 'DELETE_COMPLETE') - except exceptions.NotFoundException: + except exceptions.ResourceNotFound: pass test_network.delete_network(self.conn, self.network, self.subnet) super(TestStack, self).tearDown() diff --git a/openstack/tests/unit/orchestration/v1/test_stack.py b/openstack/tests/unit/orchestration/v1/test_stack.py index e7e7d506f..46ef6e740 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack.py +++ b/openstack/tests/unit/orchestration/v1/test_stack.py @@ -126,14 +126,14 @@ def test_fetch(self, mock_fetch): normal_stack = mock.Mock(status='CREATE_COMPLETE') mock_fetch.side_effect = [ normal_stack, - exceptions.NotFoundException(message='oops'), + exceptions.ResourceNotFound(message='oops'), deleted_stack, ] self.assertEqual(normal_stack, sot.fetch(sess)) - ex = self.assertRaises(exceptions.NotFoundException, sot.fetch, sess) + ex = self.assertRaises(exceptions.ResourceNotFound, sot.fetch, sess) self.assertEqual('oops', six.text_type(ex)) - ex = self.assertRaises(exceptions.NotFoundException, sot.fetch, sess) + ex = self.assertRaises(exceptions.ResourceNotFound, sot.fetch, sess) self.assertEqual('No stack found for %s' % FAKE_ID, six.text_type(ex)) diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index e1520ed97..c30872a80 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -205,18 +205,18 @@ def test_delete(self): self.assertEqual(rv, self.fake_id) def test_delete_ignore_missing(self): - self.res.delete.side_effect = exceptions.NotFoundException( + self.res.delete.side_effect = exceptions.ResourceNotFound( message="test", http_status=404) rv = self.sot._delete(DeleteableResource, self.fake_id) self.assertIsNone(rv) def test_delete_NotFound(self): - self.res.delete.side_effect = exceptions.NotFoundException( + self.res.delete.side_effect = exceptions.ResourceNotFound( message="test", http_status=404) self.assertRaisesRegex( - exceptions.NotFoundException, + exceptions.ResourceNotFound, # TODO(shade) The mocks here are hiding the thing we want to test. "test", self.sot._delete, DeleteableResource, self.res, @@ -332,11 +332,11 @@ def test_get_id(self): self.assertEqual(rv, self.fake_result) def test_get_not_found(self): - self.res.fetch.side_effect = exceptions.NotFoundException( + self.res.fetch.side_effect = exceptions.ResourceNotFound( message="test", http_status=404) self.assertRaisesRegex( - exceptions.NotFoundException, + exceptions.ResourceNotFound, "test", self.sot._get, RetrieveableResource, self.res) diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 97a61f371..70f3a4b2d 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1872,7 +1872,7 @@ class Base(resource.Resource): def existing(cls, **kwargs): response = mock.Mock() response.status_code = 404 - raise exceptions.NotFoundException( + raise exceptions.ResourceNotFound( 'Not Found', response=response) @classmethod @@ -2114,7 +2114,7 @@ def test_success(self): res = mock.Mock() res.fetch.side_effect = [ None, None, - exceptions.NotFoundException('Not Found', response)] + exceptions.ResourceNotFound('Not Found', response)] result = resource.wait_for_delete("session", res, 1, 3) From d92678a7acc83dd91764a1a37dd96e48ea89f8a3 Mon Sep 17 00:00:00 2001 From: Vieri <15050873171@163.com> Date: Tue, 21 Aug 2018 16:23:21 +0000 Subject: [PATCH 2172/3836] import zuul job settings from project-config This is a mechanically generated patch to complete step 1 of moving the zuul job settings out of project-config and into each project repository. Because there will be a separate patch on each branch, the branch specifiers for branch-specific jobs have been removed. Because this patch is generated by a script, there may be some cosmetic changes to the layout of the YAML file(s) as the contents are normalized. See the python3-first goal document for details: https://governance.openstack.org/tc/goals/stein/python3-first.html Change-Id: I0e4ac9cd38dd31e98ad29d52633829874ca29646 Story: #2002586 Task: #24321 --- .zuul.yaml | 54 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index cefd3ab40..2112a0f48 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -124,7 +124,7 @@ - openstack/octavia vars: devstack_localrc: - DISABLE_AMP_IMAGE_BUILD: True + DISABLE_AMP_IMAGE_BUILD: true devstack_local_conf: post-config: $OCTAVIA_CONF: @@ -239,8 +239,8 @@ vars: devstack_localrc: OVERRIDE_PUBLIC_BRIDGE_MTU: 1400 - IRONIC_BAREMETAL_BASIC_OPS: True - IRONIC_BUILD_DEPLOY_RAMDISK: False + IRONIC_BAREMETAL_BASIC_OPS: true + IRONIC_BUILD_DEPLOY_RAMDISK: false IRONIC_CALLBACK_TIMEOUT: 600 IRONIC_DEPLOY_DRIVER: ipmi IRONIC_RAMDISK_TYPE: tinyipa @@ -250,32 +250,32 @@ devstack_plugins: ironic: git://git.openstack.org/openstack/ironic devstack_services: - c-api: False - c-bak: False - c-sch: False - c-vol: False - cinder: False - s-account: False - s-container: False - s-object: False - s-proxy: False - n-api: False - n-api-meta: False - n-cauth: False - n-cond: False - n-cpu: False - n-novnc: False - n-obj: False - n-sch: False - nova: False - placement-api: False + c-api: false + c-bak: false + c-sch: false + c-vol: false + cinder: false + s-account: false + s-container: false + s-object: false + s-proxy: false + n-api: false + n-api-meta: false + n-cauth: false + n-cond: false + n-cpu: false + n-novnc: false + n-obj: false + n-sch: false + nova: false + placement-api: false tox_environment: OPENSTACKSDK_HAS_IRONIC: 1 # NOTE(dtantsur): this job cannot run many regular tests (e.g. compute # tests will take too long), so limiting it to baremetal tests only. OPENSTACKSDK_TESTS_SUBDIR: baremetal zuul_copy_output: - '{{ devstack_base_dir }}/ironic-bm-logs': 'logs' + '{{ devstack_base_dir }}/ironic-bm-logs': logs - job: name: openstacksdk-ansible-functional-devstack @@ -357,6 +357,11 @@ - osc-tox-unit-tips - shade-functional-tips - shade-tox-tips + - openstack-python-jobs + - openstack-python35-jobs + - check-requirements + - publish-openstack-sphinx-docs + - release-notes-jobs check: jobs: - build-openstack-sphinx-docs: @@ -389,3 +394,6 @@ - neutron-grenade - openstack-tox-lower-constraints - nodepool-functional-py35-src + post: + jobs: + - openstack-tox-cover From edbcb6cd9baa796c04e69a6b7e4a66c61ab0492d Mon Sep 17 00:00:00 2001 From: Vieri <15050873171@163.com> Date: Tue, 21 Aug 2018 16:23:49 +0000 Subject: [PATCH 2173/3836] switch documentation job to new PTI This is a mechanically generated patch to switch the documentation jobs to use the new PTI versions of the jobs as part of the python3-first goal. See the python3-first goal document for details: https://governance.openstack.org/tc/goals/stein/python3-first.html Change-Id: I5f0e7fe263db1a5de89ef5e910df56197cf56f10 Story: #2002586 Task: #24321 --- .zuul.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 2112a0f48..25a8bf3ec 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -360,11 +360,11 @@ - openstack-python-jobs - openstack-python35-jobs - check-requirements - - publish-openstack-sphinx-docs - - release-notes-jobs + - publish-openstack-docs-pti + - release-notes-jobs-python3 check: jobs: - - build-openstack-sphinx-docs: + - openstack-tox-docs: vars: sphinx_python: python3 - openstacksdk-ansible-devel-functional-devstack: @@ -385,7 +385,7 @@ - nodepool-functional-py35-src gate: jobs: - - build-openstack-sphinx-docs: + - openstack-tox-docs: vars: sphinx_python: python3 - openstacksdk-functional-devstack From 45629a9e76a6887ab14e0549e09b18ae23038295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Douglas=20Mendiz=C3=A1bal?= Date: Thu, 29 Sep 2016 11:36:30 -0500 Subject: [PATCH 2174/3836] Test _alternate_id logic This unit test shows an underlying issue that was causing some key_manager service methods to fail. The issue has since been fixed, as the test passes now. But let's add the test. Change-Id: I3e9e737064a167694a176cc3afb20bb5bcc59f98 Related-Bug: #1628957 --- openstack/tests/unit/test_resource.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 97a61f371..35d6e0769 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -633,6 +633,24 @@ class Test(resource.Resource): self.assertEqual(sot.alt, value2) self.assertEqual(sot.id, value2) + def test__alternate_id_from_other_property(self): + class Test(resource.Resource): + foo = resource.Body("foo") + bar = resource.Body("bar", alternate_id=True) + + # NOTE(redrobot): My expectation looking at the Test class defined + # in this test is that because the alternate_id parameter is + # is being set to True on the "bar" property of the Test class, + # then the _alternate_id() method should return the name of that "bar" + # property. + self.assertEqual("bar", Test._alternate_id()) + sot = Test(bar='bunnies') + self.assertEqual(sot.id, 'bunnies') + self.assertEqual(sot.bar, 'bunnies') + sot = Test(id='chickens', bar='bunnies') + self.assertEqual(sot.id, 'chickens') + self.assertEqual(sot.bar, 'bunnies') + def test__get_id_instance(self): class Test(resource.Resource): id = resource.Body("id") From 17578cdb9ca5722bf4a3b56a64ff8bf4655d6d85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20Gagne=CC=81?= Date: Thu, 23 Aug 2018 15:25:36 -0400 Subject: [PATCH 2175/3836] Add the ability to extend a volume size Change-Id: Ia3a316ec159ced9be0b6770c598aff4ea9ab42ac Story: 2003539 Task: 24827 --- openstack/block_storage/v2/_proxy.py | 12 ++++++++++++ openstack/block_storage/v2/volume.py | 15 +++++++++++++++ .../tests/unit/block_storage/v2/test_proxy.py | 6 ++++++ .../unit/block_storage/v2/test_volume.py | 19 +++++++++++++++++++ ...olume-extend-support-86e5c8cff5d6874e.yaml | 3 +++ 5 files changed, 55 insertions(+) create mode 100644 releasenotes/notes/add-volume-extend-support-86e5c8cff5d6874e.yaml diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 825e6c935..8b8f15fd2 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -189,6 +189,18 @@ def delete_volume(self, volume, ignore_missing=True): """ self._delete(_volume.Volume, volume, ignore_missing=ignore_missing) + def extend_volume(self, volume, size): + """Extend a volume + + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.volume.v2.volume.Volume` instance. + :param size: New volume size + + :returns: None + """ + volume = self._get_resource(_volume.Volume, volume) + volume.extend(self, size) + def backend_pools(self): """Returns a generator of cinder Back-end storage pools diff --git a/openstack/block_storage/v2/volume.py b/openstack/block_storage/v2/volume.py index 04a05d963..905883f1d 100644 --- a/openstack/block_storage/v2/volume.py +++ b/openstack/block_storage/v2/volume.py @@ -13,6 +13,7 @@ from openstack.block_storage import block_storage_service from openstack import format from openstack import resource +from openstack import utils class Volume(resource.Resource): @@ -76,6 +77,20 @@ class Volume(resource.Resource): #: The timestamp of this volume creation. created_at = resource.Body("created_at") + def _action(self, session, body): + """Preform volume actions given the message body.""" + # NOTE: This is using Volume.base_path instead of self.base_path + # as both Volume and VolumeDetail instances can be acted on, but + # the URL used is sans any additional /detail/ part. + url = utils.urljoin(Volume.base_path, self.id, 'action') + headers = {'Accept': ''} + return session.post(url, json=body, headers=headers) + + def extend(self, session, size): + """Extend a volume size.""" + body = {'os-extend': {'new_size': size}} + self._action(session, body) + class VolumeDetail(Volume): diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index e7b7fe721..53a46773e 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -88,6 +88,12 @@ def test_volume_delete(self): def test_volume_delete_ignore(self): self.verify_delete(self.proxy.delete_volume, volume.Volume, True) + def test_volume_extend(self): + self._verify("openstack.block_storage.v2.volume.Volume.extend", + self.proxy.extend_volume, + method_args=["value", "new-size"], + expected_args=["new-size"]) + def test_backend_pools(self): self.verify_list(self.proxy.backend_pools, stats.Pools, paginated=False) diff --git a/openstack/tests/unit/block_storage/v2/test_volume.py b/openstack/tests/unit/block_storage/v2/test_volume.py index e3edcd072..6334d2afd 100644 --- a/openstack/tests/unit/block_storage/v2/test_volume.py +++ b/openstack/tests/unit/block_storage/v2/test_volume.py @@ -11,6 +11,7 @@ # under the License. import copy +import mock from openstack.tests.unit import base @@ -63,6 +64,14 @@ class TestVolume(base.TestCase): + def setUp(self): + super(TestVolume, self).setUp() + self.resp = mock.Mock() + self.resp.body = None + self.resp.json = mock.Mock(return_value=self.resp.body) + self.sess = mock.Mock() + self.sess.post = mock.Mock(return_value=self.resp) + def test_basic(self): sot = volume.Volume(VOLUME) self.assertEqual("volume", sot.resource_key) @@ -101,6 +110,16 @@ def test_create(self): self.assertEqual(VOLUME["size"], sot.size) self.assertEqual(VOLUME["imageRef"], sot.image_id) + def test_extend(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.extend(self.sess, '20')) + + url = 'volumes/%s/action' % FAKE_ID + body = {"os-extend": {"new_size": "20"}} + headers = {'Accept': ''} + self.sess.post.assert_called_with(url, json=body, headers=headers) + class TestVolumeDetail(base.TestCase): diff --git a/releasenotes/notes/add-volume-extend-support-86e5c8cff5d6874e.yaml b/releasenotes/notes/add-volume-extend-support-86e5c8cff5d6874e.yaml new file mode 100644 index 000000000..b0816fb8a --- /dev/null +++ b/releasenotes/notes/add-volume-extend-support-86e5c8cff5d6874e.yaml @@ -0,0 +1,3 @@ +--- +features: + - Add the ability to extend a volume size with extend_volume method. From d44598703358888bee085f889d0f7db9bdfc85fc Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Sat, 25 Aug 2018 13:36:48 +0200 Subject: [PATCH 2176/3836] Handle missing endpoint_data in maximum_supported_microversions Apparently, for standalone case get_endpoint_data can 1. Raise an exception in case of e.g. versioned baremetal endpoint 2. Return None This change inserts safeguards for both cases. Change-Id: Iea9f5f576519f0bec7ba0f2edc6106f5780c5465 --- openstack/utils.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/openstack/utils.py b/openstack/utils.py index fafa47e92..196ebec20 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -15,6 +15,7 @@ import time import deprecation +import keystoneauth1 from keystoneauth1 import discover from openstack import _log @@ -197,7 +198,19 @@ def maximum_supported_microversion(adapter, client_maximum): if client_maximum is None: return None - endpoint_data = adapter.get_endpoint_data() + # NOTE(dtantsur): if we cannot determine supported microversions, fall back + # to the default one. + try: + endpoint_data = adapter.get_endpoint_data() + except keystoneauth1.exceptions.discovery.DiscoveryFailure: + endpoint_data = None + + if endpoint_data is None: + log = _log.setup_logging('openstack') + log.warning('Cannot determine endpoint data for service %s', + adapter.service_type or adapter.service_name) + return None + if not endpoint_data.max_microversion: return None From 46763e1842d47011d8ddad1eab2342fe90568036 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Sat, 25 Aug 2018 13:51:51 -0300 Subject: [PATCH 2177/3836] Invalidate cache upon container deletion Delete entry in the self._container_cache in OpenStackCloud when a container is deleted in the server. Story: #2003561 Task: #24852 Change-Id: Id1f20188396b8bbb26f56293d0dda962500cd860 --- openstack/cloud/openstackcloud.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 0144d71e0..be0411599 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -7308,6 +7308,7 @@ def delete_container(self, name): """ try: self._object_store_client.delete(name) + self._container_cache.pop(name, None) return True except exc.OpenStackCloudHTTPError as e: if e.response.status_code == 404: From 8c2646ef8a1c254e5d7476c2b0e0985f13bd9393 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Sat, 25 Aug 2018 12:53:19 -0300 Subject: [PATCH 2178/3836] Listing containers to return Munch objects list_containers was returning a list of dict objects, being incosistent with other listing methods in OpenStackCloud. It is now consistent, returning a list of Munch objects. Story: #2003560 Task: #24851 Change-Id: If8e6a52acede287369efc0ce676c909173be8f70 --- openstack/cloud/openstackcloud.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index be0411599..f6a1534ab 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -7264,7 +7264,8 @@ def list_containers(self, full_listing=True): :raises: OpenStackCloudException on operation error. """ - return self._object_store_client.get('/', params=dict(format='json')) + data = self._object_store_client.get('/', params=dict(format='json')) + return self._get_and_munchify(None, data) def get_container(self, name, skip_cache=False): """Get metadata about a container. From 88efc2915a1e89fc1712d241f611ac8c7ef823a9 Mon Sep 17 00:00:00 2001 From: Igor Gnatenko Date: Sun, 26 Aug 2018 18:10:43 +0200 Subject: [PATCH 2179/3836] compute: fix typo in update_security_groups() RESP BODY: {"badRequest": {"message": "Missing parameter security_group", "code": 400}} According to Compute API[0], key should be "security_group" (with underscore). Just fixing this small typo that was there from day 0. [0] https://developer.openstack.org/api-ref/compute/#update-security-group Change-Id: I0f8f4007dac6cbea94068f1654ea2f3d550c5583 Signed-off-by: Igor Gnatenko --- openstack/cloud/openstackcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 0144d71e0..f541aa3d3 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -8454,7 +8454,7 @@ def update_security_group(self, name_or_id, **kwargs): data = _adapter._json_response( self.compute.put( '/os-security-groups/{id}'.format(id=group['id']), - json={'security-group': kwargs})) + json={'security_group': kwargs})) return self._normalize_secgroup( self._get_and_munchify('security_group', data)) From 5abdc605906901620b56602d3c49289c386fca42 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 27 Jul 2018 09:56:40 -0400 Subject: [PATCH 2180/3836] Run bifrost integration test jobs Ansible uses openstacksdk now, so add the bifrost jobs. Depends-On: https://review.openstack.org/#/c/596425/ Change-Id: I2f54448850990ff526d9b0cd9ed8250adc5a6fa2 --- .zuul.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.zuul.yaml b/.zuul.yaml index 2112a0f48..acc8da770 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -383,6 +383,7 @@ - neutron-grenade - openstack-tox-lower-constraints - nodepool-functional-py35-src + - bifrost-integration-tinyipa-ubuntu-xenial gate: jobs: - build-openstack-sphinx-docs: @@ -394,6 +395,7 @@ - neutron-grenade - openstack-tox-lower-constraints - nodepool-functional-py35-src + - bifrost-integration-tinyipa-ubuntu-xenial post: jobs: - openstack-tox-cover From 7488588031fec59059971109e2964018d64bdffe Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Tue, 21 Aug 2018 15:01:53 +0200 Subject: [PATCH 2181/3836] baremetal: support newer microversions in {get,update,patch}_machine This change switches get_machine and update_machine to use the baremetal proxy. patch_machine still uses the session directly, but detects a microversion by calling into Node._get_microversion_for. API 1.1 (early Kilo) and older return None instead of "available". This change forces the value to be available in this case. While potentially breaking for the non-shade part, it improves consistency in the SDK. Change-Id: Iff682b9f307d11c8bb4f133e37e6d1cb0811edfd --- openstack/baremetal/v1/node.py | 7 + openstack/cloud/openstackcloud.py | 132 ++++-------------- openstack/tests/fakes.py | 2 +- .../tests/unit/baremetal/v1/test_node.py | 5 + .../tests/unit/cloud/test_baremetal_node.py | 45 ++++-- .../tests/unit/cloud/test_operator_noauth.py | 6 + ...update-microversions-4b910e63cebd65e2.yaml | 11 ++ 7 files changed, 91 insertions(+), 117 deletions(-) create mode 100644 releasenotes/notes/machine-get-update-microversions-4b910e63cebd65e2.yaml diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index fafdfd5d2..d60758f8c 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -124,6 +124,13 @@ class Node(resource.Resource): #: Timestamp at which the node was last updated. updated_at = resource.Body("updated_at") + def _consume_body_attrs(self, attrs): + if 'provision_state' in attrs and attrs['provision_state'] is None: + # API version 1.1 uses None instead of "available". Make it + # consistent. + attrs['provision_state'] = 'available' + return super(Node, self)._consume_body_attrs(attrs) + def create(self, session, *args, **kwargs): """Create a remote resource based on this instance. diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 0144d71e0..77ffe775c 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -9309,11 +9309,12 @@ def get_nic_by_mac(self, mac): return None def list_machines(self): - msg = "Error fetching machine node list" - data = self._baremetal_client.get("/nodes", - microversion="1.6", - error_message=msg) - return self._get_and_munchify('nodes', data) + """List Machines. + + :returns: list of ``munch.Munch`` representing machines. + """ + return [self._normalize_machine(node._to_munch()) + for node in self.baremetal.nodes()] def get_machine(self, name_or_id): """Get Machine by name or uuid @@ -9326,19 +9327,10 @@ def get_machine(self, name_or_id): :returns: ``munch.Munch`` representing the node found or None if no nodes are found. """ - # NOTE(TheJulia): This is the initial microversion shade support for - # ironic was created around. Ironic's default behavior for newer - # versions is to expose the field, but with a value of None for - # calls by a supported, yet older microversion. - # Consensus for moving forward with microversion handling in shade - # seems to be to take the same approach, although ironic's API - # does it for the user. - version = "1.6" try: - url = '/nodes/{node_id}'.format(node_id=name_or_id) return self._normalize_machine( - self._baremetal_client.get(url, microversion=version)) - except Exception: + self.baremetal.get_node(name_or_id)._to_munch()) + except exc.OpenStackCloudResourceNotFound: return None def get_machine_by_mac(self, mac): @@ -9675,7 +9667,7 @@ def patch_machine(self, name_or_id, patch): This method allows for an interface to manipulate node entries within Ironic. - :param node_id: The server object to attach to. + :param string name_or_id: A machine name or UUID to be updated. :param patch: The JSON Patch document is a list of dictonary objects that comply with RFC 6902 which can be found at @@ -9703,44 +9695,26 @@ def patch_machine(self, name_or_id, patch): :returns: ``munch.Munch`` representing the newly updated node. """ - + node = self.baremetal.get_node(name_or_id) + microversion = node._get_microversion_for(self._baremetal_client, + 'commit') msg = ("Error updating machine via patch operation on node " "{node}".format(node=name_or_id)) - url = '/nodes/{node_id}'.format(node_id=name_or_id) + url = '/nodes/{node_id}'.format(node_id=node.id) return self._normalize_machine( self._baremetal_client.patch(url, json=patch, + microversion=microversion, error_message=msg)) - def update_machine(self, name_or_id, chassis_uuid=None, driver=None, - driver_info=None, name=None, instance_info=None, - instance_uuid=None, properties=None): + def update_machine(self, name_or_id, **attrs): """Update a machine with new configuration information A user-friendly method to perform updates of a machine, in whole or part. :param string name_or_id: A machine name or UUID to be updated. - :param string chassis_uuid: Assign a chassis UUID to the machine. - NOTE: As of the Kilo release, this value - cannot be changed once set. If a user - attempts to change this value, then the - Ironic API, as of Kilo, will reject the - request. - :param string driver: The driver name for controlling the machine. - :param dict driver_info: The dictonary defining the configuration - that the driver will utilize to control - the machine. Permutations of this are - dependent upon the specific driver utilized. - :param string name: A human relatable name to represent the machine. - :param dict instance_info: A dictonary of configuration information - that conveys to the driver how the host - is to be configured when deployed. - be deployed to the machine. - :param string instance_uuid: A UUID value representing the instance - that the deployed machine represents. - :param dict properties: A dictonary defining the properties of a - machine. + :param attrs: Attributes to updated on the machine. :raises: OpenStackCloudException on operation error. @@ -9754,72 +9728,28 @@ def update_machine(self, name_or_id, chassis_uuid=None, driver=None, raise exc.OpenStackCloudException( "Machine update failed to find Machine: %s. " % name_or_id) - machine_config = {} - new_config = {} + new_config = dict(machine, **attrs) try: - if chassis_uuid: - machine_config['chassis_uuid'] = machine['chassis_uuid'] - new_config['chassis_uuid'] = chassis_uuid - - if driver: - machine_config['driver'] = machine['driver'] - new_config['driver'] = driver - - if driver_info: - machine_config['driver_info'] = machine['driver_info'] - new_config['driver_info'] = driver_info - - if name: - machine_config['name'] = machine['name'] - new_config['name'] = name - - if instance_info: - machine_config['instance_info'] = machine['instance_info'] - new_config['instance_info'] = instance_info - - if instance_uuid: - machine_config['instance_uuid'] = machine['instance_uuid'] - new_config['instance_uuid'] = instance_uuid - - if properties: - machine_config['properties'] = machine['properties'] - new_config['properties'] = properties - except KeyError as e: - self.log.debug( - "Unexpected machine response missing key %s [%s]", - e.args[0], name_or_id) - raise exc.OpenStackCloudException( - "Machine update failed - machine [%s] missing key %s. " - "Potential API issue." - % (name_or_id, e.args[0])) - - try: - patch = jsonpatch.JsonPatch.from_diff(machine_config, new_config) + patch = jsonpatch.JsonPatch.from_diff(machine, new_config) except Exception as e: raise exc.OpenStackCloudException( "Machine update failed - Error generating JSON patch object " "for submission to the API. Machine: %s Error: %s" - % (name_or_id, str(e))) + % (name_or_id, e)) - with _utils.shade_exceptions( - "Machine update failed - patch operation failed on Machine " - "{node}".format(node=name_or_id) - ): - if not patch: - return dict( - node=machine, - changes=None - ) - else: - machine = self.patch_machine(machine['uuid'], list(patch)) - change_list = [] - for change in list(patch): - change_list.append(change['path']) - return dict( - node=machine, - changes=change_list - ) + if not patch: + return dict( + node=machine, + changes=None + ) + + change_list = [change['path'] for change in patch] + node = self.baremetal.update_node(machine, **attrs) + return dict( + node=self._normalize_machine(node._to_munch()), + changes=change_list + ) def attach_port_to_machine(self, name_or_id, port_name_or_id): """Attach a virtual port to the bare metal machine. diff --git a/openstack/tests/fakes.py b/openstack/tests/fakes.py index beb4dfeb0..d224811da 100644 --- a/openstack/tests/fakes.py +++ b/openstack/tests/fakes.py @@ -359,7 +359,7 @@ class FakeMachine(object): def __init__(self, id, name=None, driver=None, driver_info=None, chassis_uuid=None, instance_info=None, instance_uuid=None, properties=None, reservation=None, last_error=None, - provision_state=None): + provision_state='available'): self.uuid = id self.name = name self.driver = driver diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index e965590f9..063414779 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -148,6 +148,11 @@ def test_instantiate(self): self.assertEqual(FAKE['target_raid_config'], sot.target_raid_config) self.assertEqual(FAKE['updated_at'], sot.updated_at) + def test_normalize_provision_state(self): + attrs = dict(FAKE, provision_state=None) + sot = node.Node(**attrs) + self.assertEqual('available', sot.provision_state) + class TestNodeDetail(base.TestCase): diff --git a/openstack/tests/unit/cloud/test_baremetal_node.py b/openstack/tests/unit/cloud/test_baremetal_node.py index da1ae81c1..982fe64ce 100644 --- a/openstack/tests/unit/cloud/test_baremetal_node.py +++ b/openstack/tests/unit/cloud/test_baremetal_node.py @@ -50,7 +50,8 @@ def test_list_machines(self): machines = self.cloud.list_machines() self.assertEqual(2, len(machines)) - self.assertEqual(self.fake_baremetal_node, machines[0]) + self.assertSubdict(self.fake_baremetal_node, machines[0]) + self.assertSubdict(fake_baremetal_two, machines[1]) self.assert_calls() def test_get_machine(self): @@ -156,6 +157,11 @@ def test_patch_machine(self): 'path': '/instance_info'}] self.fake_baremetal_node['instance_info'] = {} self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), dict(method='PATCH', uri=self.get_mock_url( resource='nodes', @@ -1093,7 +1099,7 @@ def test_register_machine_enroll(self): # this code is never used in the current code path. return_value = self.cloud.register_machine(nics, **node_to_post) - self.assertDictEqual(available_node, return_value) + self.assertSubdict(available_node, return_value) self.assert_calls() def test_register_machine_enroll_wait(self): @@ -1174,7 +1180,7 @@ def test_register_machine_enroll_wait(self): return_value = self.cloud.register_machine( nics, wait=True, **node_to_post) - self.assertDictEqual(available_node, return_value) + self.assertSubdict(available_node, return_value) self.assert_calls() def test_register_machine_enroll_failure(self): @@ -1605,7 +1611,7 @@ def test_update_machine_patch_no_action(self): update_dict = self.cloud.update_machine( self.fake_baremetal_node['uuid']) self.assertIsNone(update_dict['changes']) - self.assertDictEqual(self.fake_baremetal_node, update_dict['node']) + self.assertSubdict(self.fake_baremetal_node, update_dict['node']) self.assert_calls() @@ -1711,7 +1717,7 @@ def test_update_machine_patch(self): self.fake_baremetal_node[self.field_name] = None value_to_send = self.fake_baremetal_node[self.field_name] if self.changed: - value_to_send = 'meow' + value_to_send = self.new_value uris = [dict( method='GET', uri=self.get_mock_url( @@ -1723,7 +1729,7 @@ def test_update_machine_patch(self): test_patch = [{ 'op': 'replace', 'path': '/' + self.field_name, - 'value': 'meow'}] + 'value': value_to_send}] uris.append( dict( method='PATCH', @@ -1739,28 +1745,37 @@ def test_update_machine_patch(self): update_dict = self.cloud.update_machine( self.fake_baremetal_node['uuid'], **call_args) - if not self.changed: + if self.changed: + self.assertEqual(['/' + self.field_name], update_dict['changes']) + else: self.assertIsNone(update_dict['changes']) - self.assertDictEqual(self.fake_baremetal_node, update_dict['node']) + self.assertSubdict(self.fake_baremetal_node, update_dict['node']) self.assert_calls() scenarios = [ ('chassis_uuid', dict(field_name='chassis_uuid', changed=False)), ('chassis_uuid_changed', - dict(field_name='chassis_uuid', changed=True)), + dict(field_name='chassis_uuid', changed=True, + new_value='meow')), ('driver', dict(field_name='driver', changed=False)), - ('driver_changed', dict(field_name='driver', changed=True)), + ('driver_changed', dict(field_name='driver', changed=True, + new_value='meow')), ('driver_info', dict(field_name='driver_info', changed=False)), - ('driver_info_changed', dict(field_name='driver_info', changed=True)), + ('driver_info_changed', dict(field_name='driver_info', changed=True, + new_value={'cat': 'meow'})), ('instance_info', dict(field_name='instance_info', changed=False)), ('instance_info_changed', - dict(field_name='instance_info', changed=True)), + dict(field_name='instance_info', changed=True, + new_value={'cat': 'meow'})), ('instance_uuid', dict(field_name='instance_uuid', changed=False)), ('instance_uuid_changed', - dict(field_name='instance_uuid', changed=True)), + dict(field_name='instance_uuid', changed=True, + new_value='meow')), ('name', dict(field_name='name', changed=False)), - ('name_changed', dict(field_name='name', changed=True)), + ('name_changed', dict(field_name='name', changed=True, + new_value='meow')), ('properties', dict(field_name='properties', changed=False)), - ('properties_changed', dict(field_name='properties', changed=True)) + ('properties_changed', dict(field_name='properties', changed=True, + new_value={'cat': 'meow'})) ] diff --git a/openstack/tests/unit/cloud/test_operator_noauth.py b/openstack/tests/unit/cloud/test_operator_noauth.py index 4462bbf71..f62c44488 100644 --- a/openstack/tests/unit/cloud/test_operator_noauth.py +++ b/openstack/tests/unit/cloud/test_operator_noauth.py @@ -33,6 +33,12 @@ def setUp(self): # catalog or getting a token self._uri_registry.clear() self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + service_type='baremetal', base_url_append='v1'), + json={'id': 'v1', + 'links': [{"href": "https://bare-metal.example.com/v1/", + "rel": "self"}]}), dict(method='GET', uri=self.get_mock_url( service_type='baremetal', base_url_append='v1', diff --git a/releasenotes/notes/machine-get-update-microversions-4b910e63cebd65e2.yaml b/releasenotes/notes/machine-get-update-microversions-4b910e63cebd65e2.yaml new file mode 100644 index 000000000..7c0f47749 --- /dev/null +++ b/releasenotes/notes/machine-get-update-microversions-4b910e63cebd65e2.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + The ``get_machine``, ``update_machine`` and ``patch_machine`` calls now + support all Bare Metal API microversions supported by the SDK. Previously + they used 1.6 unconditionally. +upgrade: + - | + The baremetal API now returns ``available`` as provision state for nodes + available for deployment. Previously, ``None`` could be returned for API + version 1.1 (early Kilo) and older. From 4dd309f56da87b0119a3f0e648bf49d2ecd1a8f4 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Mon, 3 Sep 2018 14:25:08 +0200 Subject: [PATCH 2182/3836] Support bare metal service error messages The bare metal service returns errors in a non-standard format. To make it worse, it also JSON-encodes errors twice due to weird interaction between WSME and Pecan. This patch implements both the double-encoded and the future correct format. Change-Id: Icfdd223e3a2b6f7b390be8d6581007be8b14666f --- openstack/exceptions.py | 29 +++++-- openstack/tests/unit/test_exceptions.py | 81 +++++++++++++++++++ .../baremetal-errors-5cc871e8df4c9d95.yaml | 4 + 3 files changed, 108 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/baremetal-errors-5cc871e8df4c9d95.yaml diff --git a/openstack/exceptions.py b/openstack/exceptions.py index c6687e080..dc36dad6b 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -16,6 +16,7 @@ Exception definitions. """ +import json import re from requests import exceptions as _rex @@ -160,6 +161,24 @@ class InvalidResourceQuery(SDKException): pass +def _extract_message(obj): + if isinstance(obj, dict): + # Most of services: compute, network + if obj.get('message'): + return obj['message'] + # Ironic starting with Stein + elif obj.get('faultstring'): + return obj['faultstring'] + elif isinstance(obj, six.string_types): + # Ironic before Stein has double JSON encoding, nobody remembers why. + try: + obj = json.loads(obj) + except Exception: + pass + else: + return _extract_message(obj) + + def raise_from_response(response, error_message=None): """Raise an instance of an HTTPException based on keystoneauth response.""" if response.status_code < 400: @@ -183,8 +202,7 @@ def raise_from_response(response, error_message=None): try: content = response.json() - messages = [obj.get('message') for obj in content.values() - if isinstance(obj, dict)] + messages = [_extract_message(obj) for obj in content.values()] # Join all of the messages together nicely and filter out any # objects that don't have a "message" attr. details = '\n'.join(msg for msg in messages if msg) @@ -197,10 +215,9 @@ def raise_from_response(response, error_message=None): details = list(set([msg for msg in details if msg])) # Return joined string separated by colons. details = ': '.join(details) - if not details and response.reason: - details = response.reason - else: - details = response.text + + if not details: + details = response.reason if response.reason else response.text http_status = response.status_code request_id = response.headers.get('x-openstack-request-id') diff --git a/openstack/tests/unit/test_exceptions.py b/openstack/tests/unit/test_exceptions.py index 16841ecb6..63b15b91e 100644 --- a/openstack/tests/unit/test_exceptions.py +++ b/openstack/tests/unit/test_exceptions.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import json + import mock from openstack.tests.unit import base import uuid @@ -123,3 +125,82 @@ def test_raise_http_exception(self): response.headers.get('x-openstack-request-id'), exc.request_id ) + + def test_raise_compute_format(self): + response = mock.Mock() + response.status_code = 404 + response.headers = { + 'content-type': 'application/json', + } + response.json.return_value = { + 'itemNotFound': { + 'message': self.message, + 'code': 404, + } + } + exc = self.assertRaises(exceptions.NotFoundException, + self._do_raise, response, + error_message=self.message) + self.assertEqual(response.status_code, exc.status_code) + self.assertEqual(self.message, exc.details) + self.assertIn(self.message, str(exc)) + + def test_raise_network_format(self): + response = mock.Mock() + response.status_code = 404 + response.headers = { + 'content-type': 'application/json', + } + response.json.return_value = { + 'NeutronError': { + 'message': self.message, + 'type': 'FooNotFound', + 'detail': '', + } + } + exc = self.assertRaises(exceptions.NotFoundException, + self._do_raise, response, + error_message=self.message) + self.assertEqual(response.status_code, exc.status_code) + self.assertEqual(self.message, exc.details) + self.assertIn(self.message, str(exc)) + + def test_raise_baremetal_old_format(self): + response = mock.Mock() + response.status_code = 404 + response.headers = { + 'content-type': 'application/json', + } + response.json.return_value = { + 'error_message': json.dumps({ + 'faultstring': self.message, + 'faultcode': 'Client', + 'debuginfo': None, + }) + } + exc = self.assertRaises(exceptions.NotFoundException, + self._do_raise, response, + error_message=self.message) + self.assertEqual(response.status_code, exc.status_code) + self.assertEqual(self.message, exc.details) + self.assertIn(self.message, str(exc)) + + def test_raise_baremetal_corrected_format(self): + response = mock.Mock() + response.status_code = 404 + response.headers = { + 'content-type': 'application/json', + } + response.json.return_value = { + 'error_message': { + 'faultstring': self.message, + 'faultcode': 'Client', + 'debuginfo': None, + } + } + exc = self.assertRaises(exceptions.NotFoundException, + self._do_raise, response, + error_message=self.message) + self.assertEqual(response.status_code, exc.status_code) + self.assertEqual(self.message, exc.details) + self.assertIn(self.message, str(exc)) diff --git a/releasenotes/notes/baremetal-errors-5cc871e8df4c9d95.yaml b/releasenotes/notes/baremetal-errors-5cc871e8df4c9d95.yaml new file mode 100644 index 000000000..cc1fd8a40 --- /dev/null +++ b/releasenotes/notes/baremetal-errors-5cc871e8df4c9d95.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - | + Adds support for error messages from the bare metal service. From a7489106b4f6f5e0b9019631775a44438a16df60 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Tue, 4 Sep 2018 10:48:19 +0200 Subject: [PATCH 2183/3836] baremetal: implement validate_node Change-Id: I2cc71931e0352f70dacb479512cdb4fb7cb011dc --- doc/source/user/proxies/baremetal.rst | 1 + .../user/resources/baremetal/v1/node.rst | 8 +++ openstack/baremetal/v1/_proxy.py | 17 ++++++ openstack/baremetal/v1/node.py | 56 ++++++++++++++++++ openstack/cloud/openstackcloud.py | 17 +----- openstack/exceptions.py | 4 ++ .../baremetal/test_baremetal_node.py | 8 +++ .../tests/unit/baremetal/v1/test_node.py | 57 ++++++++++++++++++ .../tests/unit/cloud/test_baremetal_node.py | 58 +++++++++---------- .../baremetal-validate-ccce2a37d2a20d96.yaml | 4 ++ 10 files changed, 186 insertions(+), 44 deletions(-) create mode 100644 releasenotes/notes/baremetal-validate-ccce2a37d2a20d96.yaml diff --git a/doc/source/user/proxies/baremetal.rst b/doc/source/user/proxies/baremetal.rst index b3bacb035..e6faffe4e 100644 --- a/doc/source/user/proxies/baremetal.rst +++ b/doc/source/user/proxies/baremetal.rst @@ -24,6 +24,7 @@ Node Operations .. automethod:: openstack.baremetal.v1._proxy.Proxy.nodes .. automethod:: openstack.baremetal.v1._proxy.Proxy.set_node_provision_state .. automethod:: openstack.baremetal.v1._proxy.Proxy.wait_for_nodes_provision_state + .. automethod:: openstack.baremetal.v1._proxy.Proxy.validate_node Port Operations ^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/baremetal/v1/node.rst b/doc/source/user/resources/baremetal/v1/node.rst index 37d563f7d..bf5f8a694 100644 --- a/doc/source/user/resources/baremetal/v1/node.rst +++ b/doc/source/user/resources/baremetal/v1/node.rst @@ -10,3 +10,11 @@ The ``Node`` class inherits from :class:`~openstack.resource.Resource`. .. autoclass:: openstack.baremetal.v1.node.Node :members: + +The ValidationResult Class +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``ValidationResult`` class represents the result of a validation. + +.. autoclass:: openstack.baremetal.v1.node.ValidationResult + :members: diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index abf02dc4d..df71e216d 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -325,6 +325,23 @@ def wait_for_nodes_provision_state(self, nodes, expected_state, {'nodes': ', '.join(n.id for n in remaining), 'target': expected_state}) + def validate_node(self, node, required=('boot', 'deploy', 'power')): + """Validate required information on a node. + + :param node: The value can be either the name or ID of a node or + a :class:`~openstack.baremetal.v1.node.Node` instance. + :param required: List of interfaces that are required to pass + validation. The default value is the list of minimum required + interfaces for provisioning. + + :return: dict mapping interface names to + :class:`~openstack.baremetal.v1.node.ValidationResult` objects. + :raises: :exc:`~openstack.exceptions.ValidationException` if validation + fails for a required interface. + """ + res = self._get_resource(_node.Node, node) + return res.validate(self, required=required) + def delete_node(self, node, ignore_missing=True): """Delete a node. diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index fafdfd5d2..0c312d94a 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -21,6 +21,20 @@ _logger = _log.setup_logging('openstack') +class ValidationResult(object): + """Result of a single interface validation. + + :ivar result: Result of a validation, ``True`` for success, ``False`` for + failure, ``None`` for unsupported interface. + :ivar reason: If ``result`` is ``False`` or ``None``, explanation of + the result. + """ + + def __init__(self, result, reason): + self.result = result + self.reason = reason + + class Node(resource.Resource): resources_key = 'nodes' @@ -443,6 +457,48 @@ def list_vifs(self, session): exceptions.raise_from_response(response, error_message=msg) return [vif['id'] for vif in response.json()['vifs']] + def validate(self, session, required=('boot', 'deploy', 'power')): + """Validate required information on a node. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param required: List of interfaces that are required to pass + validation. The default value is the list of minimum required + interfaces for provisioning. + + :return: dict mapping interface names to :class:`ValidationResult` + objects. + :raises: :exc:`~openstack.exceptions.ValidationException` if validation + fails for a required interface. + """ + session = self._get_session(session) + version = self._get_microversion_for(session, 'fetch') + + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'validate') + response = session.get(request.url, headers=request.headers, + microversion=version) + + msg = ("Failed to validate node {node}".format(node=self.id)) + exceptions.raise_from_response(response, error_message=msg) + result = response.json() + + if required: + failed = [ + '%s (%s)' % (key, value.get('reason', 'no reason')) + for key, value in result.items() + if key in required and not value.get('result') + ] + + if failed: + raise exceptions.ValidationException( + 'Validation failed for required interfaces of node {node}:' + ' {failures}'.format(node=self.id, + failures=', '.join(failed))) + + return {key: ValidationResult(value.get('result'), value.get('reason')) + for key, value in result.items()} + class NodeDetail(Node): diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 0144d71e0..e75664df8 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -9856,20 +9856,9 @@ def list_ports_attached_to_machine(self, name_or_id): return [self.get_port(vif) for vif in vif_ids] def validate_node(self, uuid): - # TODO(TheJulia): There are soooooo many other interfaces - # that we can support validating, while these are essential, - # we should support more. - # TODO(TheJulia): Add a doc string :( - msg = ("Failed to query the API for validation status of " - "node {node_id}").format(node_id=uuid) - url = '/nodes/{node_id}/validate'.format(node_id=uuid) - ifaces = self._baremetal_client.get(url, error_message=msg) - - if not ifaces['deploy'] or not ifaces['power']: - raise exc.OpenStackCloudException( - "ironic node %s failed to validate. " - "(deploy: %s, power: %s)" % (ifaces['deploy'], - ifaces['power'])) + # TODO(dtantsur): deprecate this short method in favor of a fully + # written validate_machine call. + self.baremetal.validate_node(uuid) def node_set_provision_state(self, name_or_id, diff --git a/openstack/exceptions.py b/openstack/exceptions.py index c6687e080..9e5a76152 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -222,3 +222,7 @@ class ConfigException(SDKException): class NotSupported(SDKException): """Request cannot be performed by any supported API version.""" + + +class ValidationException(SDKException): + """Validation failed for resource.""" diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index 04c5d8acc..45c4f2c94 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -82,6 +82,14 @@ def test_node_create_in_enroll_provide(self): wait=True) self.assertEqual(node.provision_state, 'available') + def test_node_validate(self): + node = self.create_node() + # Fake hardware passes validation for all interfaces + result = self.conn.baremetal.validate_node(node) + for iface in ('boot', 'deploy', 'management', 'power'): + self.assertTrue(result[iface].result) + self.assertFalse(result[iface].reason) + def test_node_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" self.assertRaises(exceptions.ResourceNotFound, diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index e965590f9..7756f2ec8 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -432,3 +432,60 @@ def test_incompatible_microversion(self): self.assertRaises(exceptions.NotSupported, self.node.list_vifs, self.session) + + +@mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) +@mock.patch.object(node.Node, '_get_session', lambda self, x: x) +class TestNodeValidate(base.TestCase): + + def setUp(self): + super(TestNodeValidate, self).setUp() + self.session = mock.Mock(spec=adapter.Adapter) + self.session.default_microversion = '1.28' + self.node = node.Node(**FAKE) + + def test_validate_ok(self): + self.session.get.return_value.json.return_value = { + 'boot': {'result': True}, + 'console': {'result': False, 'reason': 'Not configured'}, + 'deploy': {'result': True}, + 'inspect': {'result': None, 'reason': 'Not supported'}, + 'power': {'result': True} + } + result = self.node.validate(self.session) + for iface in ('boot', 'deploy', 'power'): + self.assertTrue(result[iface].result) + self.assertFalse(result[iface].reason) + for iface in ('console', 'inspect'): + self.assertIsNot(True, result[iface].result) + self.assertTrue(result[iface].reason) + + def test_validate_failed(self): + self.session.get.return_value.json.return_value = { + 'boot': {'result': False}, + 'console': {'result': False, 'reason': 'Not configured'}, + 'deploy': {'result': False, 'reason': 'No deploy for you'}, + 'inspect': {'result': None, 'reason': 'Not supported'}, + 'power': {'result': True} + } + self.assertRaisesRegex(exceptions.ValidationException, + 'No deploy for you', + self.node.validate, self.session) + + def test_validate_no_failure(self): + self.session.get.return_value.json.return_value = { + 'boot': {'result': False}, + 'console': {'result': False, 'reason': 'Not configured'}, + 'deploy': {'result': False, 'reason': 'No deploy for you'}, + 'inspect': {'result': None, 'reason': 'Not supported'}, + 'power': {'result': True} + } + result = self.node.validate(self.session, required=None) + self.assertTrue(result['power'].result) + self.assertFalse(result['power'].reason) + for iface in ('deploy', 'console', 'inspect'): + self.assertIsNot(True, result[iface].result) + self.assertTrue(result[iface].reason) + # Reason can be empty + self.assertFalse(result['boot'].result) + self.assertIsNone(result['boot'].reason) diff --git a/openstack/tests/unit/cloud/test_baremetal_node.py b/openstack/tests/unit/cloud/test_baremetal_node.py index da1ae81c1..769d0fc16 100644 --- a/openstack/tests/unit/cloud/test_baremetal_node.py +++ b/openstack/tests/unit/cloud/test_baremetal_node.py @@ -22,6 +22,7 @@ from testscenarios import load_tests_apply_scenarios as load_tests # noqa from openstack.cloud import exc +from openstack import exceptions from openstack.tests import fakes from openstack.tests.unit import base @@ -119,36 +120,33 @@ def test_validate_node(self): self.assert_calls() - # FIXME(TheJulia): So, this doesn't presently fail, but should fail. - # Placing the test here, so we can sort out the issue in the actual - # method later. - # def test_validate_node_raises_exception(self): - # validate_return = { - # 'deploy': { - # 'result': False, - # 'reason': 'error!', - # }, - # 'power': { - # 'result': False, - # 'reason': 'meow!', - # }, - # 'foo': { - # 'result': True - # }} - # self.register_uris([ - # dict(method='GET', - # uri=self.get_mock_url( - # resource='nodes', - # append=[self.fake_baremetal_node['uuid'], - # 'validate']), - # json=validate_return), - # ]) - # self.assertRaises( - # Exception, - # self.cloud.validate_node, - # self.fake_baremetal_node['uuid']) - # - # self.assert_calls() + def test_validate_node_raises_exception(self): + validate_return = { + 'deploy': { + 'result': False, + 'reason': 'error!', + }, + 'power': { + 'result': False, + 'reason': 'meow!', + }, + 'foo': { + 'result': True + }} + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'validate']), + json=validate_return), + ]) + self.assertRaises( + exceptions.ValidationException, + self.cloud.validate_node, + self.fake_baremetal_node['uuid']) + + self.assert_calls() def test_patch_machine(self): test_patch = [{ diff --git a/releasenotes/notes/baremetal-validate-ccce2a37d2a20d96.yaml b/releasenotes/notes/baremetal-validate-ccce2a37d2a20d96.yaml new file mode 100644 index 000000000..7783c2fb9 --- /dev/null +++ b/releasenotes/notes/baremetal-validate-ccce2a37d2a20d96.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Adds support for bare metal node validation to the bare metal proxy. From cb2f9de046548d92c6b678227d6e790eb8d197ee Mon Sep 17 00:00:00 2001 From: bmike78 Date: Thu, 6 Sep 2018 08:08:24 -0500 Subject: [PATCH 2184/3836] Fix list_recordsets to deal with top-level key During use of the os_recordset Ansible module, it was found that from the change from shade to openstacksdk and newer versions of Ansible and Designate that the module lost idempotence and the ability to create DNS records using the filter in function "search_recordsets" in openstack/cloud/openstackcloud.py search_recordsets passes expected output to _utils._filter_list, but this can not be done since new versions of Designate nest the output in an array called "recordsets". If that is stripped out, then the filter works and output is displayed. Change-Id: If5e85d0b5b1a86ba6d05508a5615699aba1de78a Story: #2003607 --- openstack/cloud/openstackcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index f6a1534ab..d2a7727f1 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -8766,7 +8766,7 @@ def list_recordsets(self, zone): "Zone %s not found." % zone) return self._dns_client.get( "/zones/{zone_id}/recordsets".format(zone_id=zone_obj['id']), - error_message="Error fetching recordsets list") + error_message="Error fetching recordsets list")['recordsets'] def get_recordset(self, zone, name_or_id): """Get a recordset by name or ID. From 782919e50766a14b787ce474038fc83b5932b38d Mon Sep 17 00:00:00 2001 From: liuzhuangzhuang Date: Fri, 7 Sep 2018 16:10:11 +0800 Subject: [PATCH 2185/3836] Change the method of role update change the method of role update from PUT to PATCH Change-Id: I207519a625434ce205218c2b6be242a8b3d49ba8 Signed-off-by: liuzhuangzhuang --- openstack/identity/v3/role.py | 1 + openstack/tests/unit/identity/v3/test_role.py | 1 + 2 files changed, 2 insertions(+) diff --git a/openstack/identity/v3/role.py b/openstack/identity/v3/role.py index e4a191fde..cd8e272e7 100644 --- a/openstack/identity/v3/role.py +++ b/openstack/identity/v3/role.py @@ -26,6 +26,7 @@ class Role(resource.Resource): allow_commit = True allow_delete = True allow_list = True + commit_method = 'PATCH' _query_mapping = resource.QueryParameters( 'name', 'domain_id') diff --git a/openstack/tests/unit/identity/v3/test_role.py b/openstack/tests/unit/identity/v3/test_role.py index 59d2a592a..a32e616cd 100644 --- a/openstack/tests/unit/identity/v3/test_role.py +++ b/openstack/tests/unit/identity/v3/test_role.py @@ -35,6 +35,7 @@ def test_basic(self): self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) + self.assertEqual('PATCH', sot.commit_method) self.assertDictEqual( { From a967956e47510c63880cb4a3860e860e6a131ba7 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Fri, 7 Sep 2018 06:33:25 -0300 Subject: [PATCH 2186/3836] Allow search on containers Implements search_containers, which can take a JMESPath expression or a dict of container attributes to filter containers in. Story: #2003694 Task: #26338 Change-Id: If348e20f14292660044b215a54a7adc66c37671e --- openstack/cloud/openstackcloud.py | 17 +++++++++++++++++ .../container-search-b0f4253ce2deeda5.yaml | 6 ++++++ 2 files changed, 23 insertions(+) create mode 100644 releasenotes/notes/container-search-b0f4253ce2deeda5.yaml diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index f6a1534ab..ac22ac27f 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -7267,6 +7267,23 @@ def list_containers(self, full_listing=True): data = self._object_store_client.get('/', params=dict(format='json')) return self._get_and_munchify(None, data) + def search_containers(self, name=None, filters=None): + """Search containers. + + :param string name: container name. + :param filters: a dict containing additional filters to use. + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: a list of ``munch.Munch`` containing the containers. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + containers = self.list_containers() + return _utils._filter_list(containers, name, filters) + def get_container(self, name, skip_cache=False): """Get metadata about a container. diff --git a/releasenotes/notes/container-search-b0f4253ce2deeda5.yaml b/releasenotes/notes/container-search-b0f4253ce2deeda5.yaml new file mode 100644 index 000000000..3587a7b5c --- /dev/null +++ b/releasenotes/notes/container-search-b0f4253ce2deeda5.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Containers are now searchable both with a JMESPath expression or a dict of + container attributes via the + ``openstack.connection.Connection.search_containers`` function. From c340e203e4ff780c9b8fc66d9e0a83d294e9dc17 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Mon, 27 Aug 2018 11:45:09 -0300 Subject: [PATCH 2187/3836] Listing objects to return Munch objects list_objects was returning a list of dict objects, being incosistent with other listing methods in OpenStackCloud. It is now consistent, returning a list of Munch objects. Story: #2003568 Task: #24861 Change-Id: I39ca8d6221aa34db1f4ba983d169b7701d0328d8 --- openstack/cloud/openstackcloud.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index f6a1534ab..00612881a 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -7767,8 +7767,9 @@ def list_objects(self, container, full_listing=True): :raises: OpenStackCloudException on operation error. """ - return self._object_store_client.get( + data = self._object_store_client.get( container, params=dict(format='json')) + return self._get_and_munchify(None, data) def delete_object(self, container, name, meta=None): """Delete an object from a container. From f96b13210cbce12131e0e2f8aa3433db38628106 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Fri, 7 Sep 2018 06:43:36 -0300 Subject: [PATCH 2188/3836] Allow search on objects Implements search_objects, which can take a JMESPath expression or a dict of object attributes to filter objects in. Story: #2003695 Task: #26339 Change-Id: I7cda29f778acdcdeb8da737d001e957674e9284b --- openstack/cloud/openstackcloud.py | 17 +++++++++++++++++ .../notes/object-search-a5f5ec4b2df3e045.yaml | 6 ++++++ 2 files changed, 23 insertions(+) create mode 100644 releasenotes/notes/object-search-a5f5ec4b2df3e045.yaml diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 00612881a..023c42c6f 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -7771,6 +7771,23 @@ def list_objects(self, container, full_listing=True): container, params=dict(format='json')) return self._get_and_munchify(None, data) + def search_objects(self, container, name=None, filters=None): + """Search objects. + + :param string name: object name. + :param filters: a dict containing additional filters to use. + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: a list of ``munch.Munch`` containing the objects. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + objects = self.list_objects(container) + return _utils._filter_list(objects, name, filters) + def delete_object(self, container, name, meta=None): """Delete an object from a container. diff --git a/releasenotes/notes/object-search-a5f5ec4b2df3e045.yaml b/releasenotes/notes/object-search-a5f5ec4b2df3e045.yaml new file mode 100644 index 000000000..1ac05e9bf --- /dev/null +++ b/releasenotes/notes/object-search-a5f5ec4b2df3e045.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Objects are now searchable both with a JMESPath expression or a dict of + object attributes via the + ``openstack.connection.Connection.search_object`` function. From e40ccf742f00c318d77037d7d6dcdcb27b34506d Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Fri, 31 Aug 2018 18:11:42 -0300 Subject: [PATCH 2189/3836] Allow JMESPath on searching networking resources As most of search methods in SDK, allowing a JMESPath expression for network, subnet, port and router resources is powerful to end users. Story: #2003611 Task: #24945 Change-Id: I85a51e16c4f714c15b1920c0085d98a095832904 --- openstack/cloud/openstackcloud.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index f6a1534ab..3d4ea198a 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -1477,7 +1477,8 @@ def search_networks(self, name_or_id=None, filters=None): :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ - networks = self.list_networks(filters) + networks = self.list_networks( + filters if isinstance(filters, dict) else None) return _utils._filter_list(networks, name_or_id, filters) def search_routers(self, name_or_id=None, filters=None): @@ -1492,7 +1493,8 @@ def search_routers(self, name_or_id=None, filters=None): :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ - routers = self.list_routers(filters) + routers = self.list_routers( + filters if isinstance(filters, dict) else None) return _utils._filter_list(routers, name_or_id, filters) def search_subnets(self, name_or_id=None, filters=None): @@ -1507,7 +1509,8 @@ def search_subnets(self, name_or_id=None, filters=None): :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ - subnets = self.list_subnets(filters) + subnets = self.list_subnets( + filters if isinstance(filters, dict) else None) return _utils._filter_list(subnets, name_or_id, filters) def search_ports(self, name_or_id=None, filters=None): @@ -1525,7 +1528,7 @@ def search_ports(self, name_or_id=None, filters=None): # If port caching is enabled, do not push the filter down to # neutron; get all the ports (potentially from the cache) and # filter locally. - if self._PORT_AGE: + if self._PORT_AGE or isinstance(filters, str): pushdown_filters = None else: pushdown_filters = filters From e0c7d2446c664e7743850fcb8cd756c13b18cbad Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Wed, 5 Sep 2018 11:04:09 -0300 Subject: [PATCH 2190/3836] Implement volume update Volumes could only be created, retrieved and deleted. Updating volume attributes (e.g name) is useful to end users. Story: #2003670 Task: #26188 Change-Id: I9f5334cd8740db9a445b2d2a04d9f019789a68ec --- openstack/cloud/openstackcloud.py | 17 +++++++++++++++++ openstack/tests/functional/cloud/test_volume.py | 12 ++++++++++++ 2 files changed, 29 insertions(+) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index f6a1534ab..836de4315 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -5063,6 +5063,23 @@ def create_volume( return self._normalize_volume(volume) + def update_volume(self, name_or_id, **kwargs): + kwargs = self._get_volume_kwargs(kwargs) + + volume = self.get_volume(name_or_id) + if not volume: + raise exc.OpenStackCloudException( + "Volume %s not found." % name_or_id) + + data = self._volume_client.put( + '/volumes/{volume_id}'.format(volume_id=volume.id), + json=dict({'volume': kwargs}), + error_message='Error updating volume') + + self.list_volumes.invalidate(self) + + return self._normalize_volume(self._get_and_munchify('volume', data)) + def set_volume_bootable(self, name_or_id, bootable=True): """Set a volume's bootable flag. diff --git a/openstack/tests/functional/cloud/test_volume.py b/openstack/tests/functional/cloud/test_volume.py index ee695bbe8..e0c1b63ef 100644 --- a/openstack/tests/functional/cloud/test_volume.py +++ b/openstack/tests/functional/cloud/test_volume.py @@ -149,3 +149,15 @@ def test_list_volumes_pagination(self): self.assertEqual( sorted([i['id'] for i in volumes]), sorted(result)) + + def test_update_volume(self): + name, desc = self.getUniqueString('name'), self.getUniqueString('desc') + self.addCleanup(self.cleanup, name) + volume = self.user_cloud.create_volume(1, name=name, description=desc) + self.assertEqual(volume.name, name) + self.assertEqual(volume.description, desc) + new_name = self.getUniqueString('name') + volume = self.user_cloud.update_volume(volume.id, name=new_name) + self.assertNotEqual(volume.name, name) + self.assertEqual(volume.name, new_name) + self.assertEqual(volume.description, desc) From eb9284984e3c3de05cc7a9253208566fd3b5e2e2 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Fri, 10 Aug 2018 15:07:37 -0300 Subject: [PATCH 2191/3836] Consolidate cloud/base.py into functional/base.py This was left as a TODO in the merge of Shade into SDK. Shade's BaseFunctionalTestCase is now BaseFunctionalTest; KeystoneBaseFunctionalTestCase is now KeystoneBaseFunctionalTest. Change-Id: I3b3055eb8e55b9696ac628a88bdee460d6cc0918 --- openstack/tests/functional/base.py | 64 +++++++++++++ openstack/tests/functional/cloud/base.py | 89 ------------------- .../tests/functional/cloud/test_aggregate.py | 4 +- .../cloud/test_cluster_templates.py | 4 +- .../tests/functional/cloud/test_clustering.py | 4 +- .../functional/cloud/test_coe_clusters.py | 4 +- .../tests/functional/cloud/test_compute.py | 4 +- .../tests/functional/cloud/test_devstack.py | 6 +- .../tests/functional/cloud/test_domain.py | 4 +- .../tests/functional/cloud/test_endpoints.py | 4 +- .../tests/functional/cloud/test_flavor.py | 4 +- .../functional/cloud/test_floating_ip.py | 4 +- .../functional/cloud/test_floating_ip_pool.py | 4 +- .../tests/functional/cloud/test_groups.py | 4 +- .../tests/functional/cloud/test_identity.py | 4 +- .../tests/functional/cloud/test_image.py | 4 +- .../tests/functional/cloud/test_inventory.py | 4 +- .../tests/functional/cloud/test_keypairs.py | 4 +- .../tests/functional/cloud/test_limits.py | 4 +- .../functional/cloud/test_magnum_services.py | 4 +- .../tests/functional/cloud/test_network.py | 4 +- .../tests/functional/cloud/test_object.py | 4 +- openstack/tests/functional/cloud/test_port.py | 4 +- .../tests/functional/cloud/test_project.py | 4 +- .../cloud/test_qos_bandwidth_limit_rule.py | 4 +- .../cloud/test_qos_dscp_marking_rule.py | 4 +- .../cloud/test_qos_minimum_bandwidth_rule.py | 4 +- .../tests/functional/cloud/test_qos_policy.py | 4 +- .../tests/functional/cloud/test_quotas.py | 8 +- .../functional/cloud/test_range_search.py | 4 +- .../tests/functional/cloud/test_recordset.py | 4 +- .../tests/functional/cloud/test_router.py | 4 +- .../functional/cloud/test_security_groups.py | 4 +- .../functional/cloud/test_server_group.py | 4 +- .../tests/functional/cloud/test_services.py | 4 +- .../tests/functional/cloud/test_stack.py | 4 +- .../tests/functional/cloud/test_users.py | 4 +- .../tests/functional/cloud/test_volume.py | 4 +- .../functional/cloud/test_volume_backup.py | 4 +- .../functional/cloud/test_volume_type.py | 4 +- openstack/tests/functional/cloud/test_zone.py | 4 +- 41 files changed, 145 insertions(+), 170 deletions(-) delete mode 100644 openstack/tests/functional/cloud/base.py diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index 5fbda5cbd..7211c2c8d 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -44,6 +44,54 @@ def setUp(self): super(BaseFunctionalTest, self).setUp() self.conn = connection.Connection(config=TEST_CLOUD_REGION) + self._demo_name = os.environ.get('OPENSTACKSDK_DEMO_CLOUD', 'devstack') + self._op_name = os.environ.get( + 'OPENSTACKSDK_OPERATOR_CLOUD', 'devstack-admin') + + self.config = openstack.config.OpenStackConfig() + self._set_user_cloud() + self._set_operator_cloud() + + self.identity_version = \ + self.operator_cloud.config.get_api_version('identity') + + def _set_user_cloud(self, **kwargs): + user_config = self.config.get_one( + cloud=self._demo_name, **kwargs) + self.user_cloud = connection.Connection(config=user_config) + + def _set_operator_cloud(self, **kwargs): + operator_config = self.config.get_one( + cloud=self._op_name, **kwargs) + self.operator_cloud = connection.Connection(config=operator_config) + + def pick_image(self): + images = self.user_cloud.list_images() + self.add_info_on_exception('images', images) + + image_name = os.environ.get('OPENSTACKSDK_IMAGE') + if image_name: + for image in images: + if image.name == image_name: + return image + self.assertFalse( + "Cloud does not have {image}".format(image=image_name)) + + for image in images: + if image.name.startswith('cirros') and image.name.endswith('-uec'): + return image + for image in images: + if (image.name.startswith('cirros') + and image.disk_format == 'qcow2'): + return image + for image in images: + if image.name.lower().startswith('ubuntu'): + return image + for image in images: + if image.name.lower().startswith('centos'): + return image + self.assertFalse('no sensible image available') + def addEmptyCleanup(self, func, *args, **kwargs): def cleanup(): result = func(*args, **kwargs) @@ -82,3 +130,19 @@ def setUp(self): 'microversion {ver}'.format( service_type=service_type, ver=min_microversion)) + + +class KeystoneBaseFunctionalTest(BaseFunctionalTest): + + def setUp(self): + super(KeystoneBaseFunctionalTest, self).setUp() + + use_keystone_v2 = os.environ.get('OPENSTACKSDK_USE_KEYSTONE_V2', False) + if use_keystone_v2: + # keystone v2 has special behavior for the admin + # interface and some of the operations, so make a new cloud + # object with interface set to admin. + # We only do it for keystone tests on v2 because otherwise + # the admin interface is not a thing that wants to actually + # be used + self._set_operator_cloud(interface='admin') diff --git a/openstack/tests/functional/cloud/base.py b/openstack/tests/functional/cloud/base.py deleted file mode 100644 index f84aa6684..000000000 --- a/openstack/tests/functional/cloud/base.py +++ /dev/null @@ -1,89 +0,0 @@ -# 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. - -# TODO(shade) Merge this with openstack.tests.functional.base - -import os - -import openstack.config as occ - -from openstack import connection -from openstack.tests import base - - -class BaseFunctionalTestCase(base.TestCase): - def setUp(self): - super(BaseFunctionalTestCase, self).setUp() - - self._demo_name = os.environ.get('OPENSTACKSDK_DEMO_CLOUD', 'devstack') - self._op_name = os.environ.get( - 'OPENSTACKSDK_OPERATOR_CLOUD', 'devstack-admin') - - self.config = occ.OpenStackConfig() - self._set_user_cloud() - self._set_operator_cloud() - - self.identity_version = \ - self.operator_cloud.config.get_api_version('identity') - - def _set_user_cloud(self, **kwargs): - user_config = self.config.get_one( - cloud=self._demo_name, **kwargs) - self.user_cloud = connection.Connection(config=user_config) - - def _set_operator_cloud(self, **kwargs): - operator_config = self.config.get_one( - cloud=self._op_name, **kwargs) - self.operator_cloud = connection.Connection(config=operator_config) - - def pick_image(self): - images = self.user_cloud.list_images() - self.add_info_on_exception('images', images) - - image_name = os.environ.get('OPENSTACKSDK_IMAGE') - if image_name: - for image in images: - if image.name == image_name: - return image - self.assertFalse( - "Cloud does not have {image}".format(image=image_name)) - - for image in images: - if image.name.startswith('cirros') and image.name.endswith('-uec'): - return image - for image in images: - if (image.name.startswith('cirros') - and image.disk_format == 'qcow2'): - return image - for image in images: - if image.name.lower().startswith('ubuntu'): - return image - for image in images: - if image.name.lower().startswith('centos'): - return image - self.assertFalse('no sensible image available') - - -class KeystoneBaseFunctionalTestCase(BaseFunctionalTestCase): - - def setUp(self): - super(KeystoneBaseFunctionalTestCase, self).setUp() - - use_keystone_v2 = os.environ.get('OPENSTACKSDK_USE_KEYSTONE_V2', False) - if use_keystone_v2: - # keystone v2 has special behavior for the admin - # interface and some of the operations, so make a new cloud - # object with interface set to admin. - # We only do it for keystone tests on v2 because otherwise - # the admin interface is not a thing that wants to actually - # be used - self._set_operator_cloud(interface='admin') diff --git a/openstack/tests/functional/cloud/test_aggregate.py b/openstack/tests/functional/cloud/test_aggregate.py index 4830c2103..0f06e3a8c 100644 --- a/openstack/tests/functional/cloud/test_aggregate.py +++ b/openstack/tests/functional/cloud/test_aggregate.py @@ -17,10 +17,10 @@ Functional tests for `shade` aggregate resource. """ -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base -class TestAggregate(base.BaseFunctionalTestCase): +class TestAggregate(base.BaseFunctionalTest): def test_aggregates(self): aggregate_name = self.getUniqueString() diff --git a/openstack/tests/functional/cloud/test_cluster_templates.py b/openstack/tests/functional/cloud/test_cluster_templates.py index f104855cd..bb8975473 100644 --- a/openstack/tests/functional/cloud/test_cluster_templates.py +++ b/openstack/tests/functional/cloud/test_cluster_templates.py @@ -20,12 +20,12 @@ import fixtures from testtools import content -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base import subprocess -class TestClusterTemplate(base.BaseFunctionalTestCase): +class TestClusterTemplate(base.BaseFunctionalTest): def setUp(self): super(TestClusterTemplate, self).setUp() diff --git a/openstack/tests/functional/cloud/test_clustering.py b/openstack/tests/functional/cloud/test_clustering.py index 5e401b1d4..4825e6142 100644 --- a/openstack/tests/functional/cloud/test_clustering.py +++ b/openstack/tests/functional/cloud/test_clustering.py @@ -19,7 +19,7 @@ from testtools import content -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base import time @@ -105,7 +105,7 @@ def wait_for_delete(client, client_args, check_interval=1, timeout=60): return True -class TestClustering(base.BaseFunctionalTestCase): +class TestClustering(base.BaseFunctionalTest): def setUp(self): super(TestClustering, self).setUp() diff --git a/openstack/tests/functional/cloud/test_coe_clusters.py b/openstack/tests/functional/cloud/test_coe_clusters.py index e4a099896..317d5f03a 100644 --- a/openstack/tests/functional/cloud/test_coe_clusters.py +++ b/openstack/tests/functional/cloud/test_coe_clusters.py @@ -17,10 +17,10 @@ Functional tests for `shade` COE clusters methods. """ -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base -class TestCompute(base.BaseFunctionalTestCase): +class TestCompute(base.BaseFunctionalTest): # NOTE(flwang): Currently, running Magnum on a cloud which doesn't support # nested virtualization will lead to timeout. So this test file is mostly # like a note to document why we can't have function testing for Magnum diff --git a/openstack/tests/functional/cloud/test_compute.py b/openstack/tests/functional/cloud/test_compute.py index 0e0f17986..eb4cd78e3 100644 --- a/openstack/tests/functional/cloud/test_compute.py +++ b/openstack/tests/functional/cloud/test_compute.py @@ -23,12 +23,12 @@ import six from openstack.cloud import exc -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base from openstack.tests.functional.cloud.util import pick_flavor from openstack import utils -class TestCompute(base.BaseFunctionalTestCase): +class TestCompute(base.BaseFunctionalTest): def setUp(self): # OS_TEST_TIMEOUT is 90 sec by default # but on a bad day, test_attach_detach_volume can take more time. diff --git a/openstack/tests/functional/cloud/test_devstack.py b/openstack/tests/functional/cloud/test_devstack.py index 633a76e23..f6957fb09 100644 --- a/openstack/tests/functional/cloud/test_devstack.py +++ b/openstack/tests/functional/cloud/test_devstack.py @@ -22,10 +22,10 @@ from testscenarios import load_tests_apply_scenarios as load_tests # noqa -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base -class TestDevstack(base.BaseFunctionalTestCase): +class TestDevstack(base.BaseFunctionalTest): scenarios = [ ('designate', dict(env='DESIGNATE', service='dns')), @@ -42,7 +42,7 @@ def test_has_service(self): self.assertTrue(self.user_cloud.has_service(self.service)) -class TestKeystoneVersion(base.BaseFunctionalTestCase): +class TestKeystoneVersion(base.BaseFunctionalTest): def test_keystone_version(self): use_keystone_v2 = os.environ.get('OPENSTACKSDK_USE_KEYSTONE_V2', False) diff --git a/openstack/tests/functional/cloud/test_domain.py b/openstack/tests/functional/cloud/test_domain.py index 81bd67a1c..18a070f91 100644 --- a/openstack/tests/functional/cloud/test_domain.py +++ b/openstack/tests/functional/cloud/test_domain.py @@ -18,10 +18,10 @@ """ import openstack.cloud -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base -class TestDomain(base.BaseFunctionalTestCase): +class TestDomain(base.BaseFunctionalTest): def setUp(self): super(TestDomain, self).setUp() diff --git a/openstack/tests/functional/cloud/test_endpoints.py b/openstack/tests/functional/cloud/test_endpoints.py index a1ce61c21..c55581996 100644 --- a/openstack/tests/functional/cloud/test_endpoints.py +++ b/openstack/tests/functional/cloud/test_endpoints.py @@ -24,10 +24,10 @@ from openstack.cloud.exc import OpenStackCloudException from openstack.cloud.exc import OpenStackCloudUnavailableFeature -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base -class TestEndpoints(base.KeystoneBaseFunctionalTestCase): +class TestEndpoints(base.KeystoneBaseFunctionalTest): endpoint_attributes = ['id', 'region', 'publicurl', 'internalurl', 'service_id', 'adminurl'] diff --git a/openstack/tests/functional/cloud/test_flavor.py b/openstack/tests/functional/cloud/test_flavor.py index 755bb9f63..f207d82f9 100644 --- a/openstack/tests/functional/cloud/test_flavor.py +++ b/openstack/tests/functional/cloud/test_flavor.py @@ -20,10 +20,10 @@ """ from openstack.cloud.exc import OpenStackCloudException -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base -class TestFlavor(base.BaseFunctionalTestCase): +class TestFlavor(base.BaseFunctionalTest): def setUp(self): super(TestFlavor, self).setUp() diff --git a/openstack/tests/functional/cloud/test_floating_ip.py b/openstack/tests/functional/cloud/test_floating_ip.py index f25b38a25..2be3bcea7 100644 --- a/openstack/tests/functional/cloud/test_floating_ip.py +++ b/openstack/tests/functional/cloud/test_floating_ip.py @@ -28,12 +28,12 @@ from openstack import _adapter from openstack.cloud import meta from openstack.cloud.exc import OpenStackCloudException -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base from openstack.tests.functional.cloud.util import pick_flavor from openstack import utils -class TestFloatingIP(base.BaseFunctionalTestCase): +class TestFloatingIP(base.BaseFunctionalTest): timeout = 60 def setUp(self): diff --git a/openstack/tests/functional/cloud/test_floating_ip_pool.py b/openstack/tests/functional/cloud/test_floating_ip_pool.py index 8c8fc478b..2eba99af1 100644 --- a/openstack/tests/functional/cloud/test_floating_ip_pool.py +++ b/openstack/tests/functional/cloud/test_floating_ip_pool.py @@ -19,7 +19,7 @@ Functional tests for floating IP pool resource (managed by nova) """ -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base # When using nova-network, floating IP pools are created with nova-manage @@ -31,7 +31,7 @@ # nova floating-ip-pool-list returns 404. -class TestFloatingIPPool(base.BaseFunctionalTestCase): +class TestFloatingIPPool(base.BaseFunctionalTest): def setUp(self): super(TestFloatingIPPool, self).setUp() diff --git a/openstack/tests/functional/cloud/test_groups.py b/openstack/tests/functional/cloud/test_groups.py index aa073d73a..e9ae28a84 100644 --- a/openstack/tests/functional/cloud/test_groups.py +++ b/openstack/tests/functional/cloud/test_groups.py @@ -18,10 +18,10 @@ """ import openstack.cloud -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base -class TestGroup(base.BaseFunctionalTestCase): +class TestGroup(base.BaseFunctionalTest): def setUp(self): super(TestGroup, self).setUp() diff --git a/openstack/tests/functional/cloud/test_identity.py b/openstack/tests/functional/cloud/test_identity.py index 10f1be3c4..f9ecdf663 100644 --- a/openstack/tests/functional/cloud/test_identity.py +++ b/openstack/tests/functional/cloud/test_identity.py @@ -21,10 +21,10 @@ import string from openstack.cloud.exc import OpenStackCloudException -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base -class TestIdentity(base.KeystoneBaseFunctionalTestCase): +class TestIdentity(base.KeystoneBaseFunctionalTest): def setUp(self): super(TestIdentity, self).setUp() self.role_prefix = 'test_role' + ''.join( diff --git a/openstack/tests/functional/cloud/test_image.py b/openstack/tests/functional/cloud/test_image.py index 6b21388f6..08f416c9c 100644 --- a/openstack/tests/functional/cloud/test_image.py +++ b/openstack/tests/functional/cloud/test_image.py @@ -21,10 +21,10 @@ import os import tempfile -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base -class TestImage(base.BaseFunctionalTestCase): +class TestImage(base.BaseFunctionalTest): def setUp(self): super(TestImage, self).setUp() self.image = self.pick_image() diff --git a/openstack/tests/functional/cloud/test_inventory.py b/openstack/tests/functional/cloud/test_inventory.py index 263d20250..ff3ff3a50 100644 --- a/openstack/tests/functional/cloud/test_inventory.py +++ b/openstack/tests/functional/cloud/test_inventory.py @@ -21,11 +21,11 @@ from openstack.cloud import inventory -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base from openstack.tests.functional.cloud.util import pick_flavor -class TestInventory(base.BaseFunctionalTestCase): +class TestInventory(base.BaseFunctionalTest): def setUp(self): super(TestInventory, self).setUp() # This needs to use an admin account, otherwise a public IP diff --git a/openstack/tests/functional/cloud/test_keypairs.py b/openstack/tests/functional/cloud/test_keypairs.py index d5bbd7fdd..80eee3844 100644 --- a/openstack/tests/functional/cloud/test_keypairs.py +++ b/openstack/tests/functional/cloud/test_keypairs.py @@ -17,10 +17,10 @@ Functional tests for `shade` keypairs methods """ from openstack.tests import fakes -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base -class TestKeypairs(base.BaseFunctionalTestCase): +class TestKeypairs(base.BaseFunctionalTest): def test_create_and_delete(self): '''Test creating and deleting keypairs functionality''' diff --git a/openstack/tests/functional/cloud/test_limits.py b/openstack/tests/functional/cloud/test_limits.py index 539f0dc83..5917edf39 100644 --- a/openstack/tests/functional/cloud/test_limits.py +++ b/openstack/tests/functional/cloud/test_limits.py @@ -16,10 +16,10 @@ Functional tests for `shade` limits method """ -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base -class TestUsage(base.BaseFunctionalTestCase): +class TestUsage(base.BaseFunctionalTest): def test_get_our_compute_limits(self): '''Test quotas functionality''' diff --git a/openstack/tests/functional/cloud/test_magnum_services.py b/openstack/tests/functional/cloud/test_magnum_services.py index 407213e93..f3ffd9e82 100644 --- a/openstack/tests/functional/cloud/test_magnum_services.py +++ b/openstack/tests/functional/cloud/test_magnum_services.py @@ -17,10 +17,10 @@ Functional tests for `shade` services method. """ -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base -class TestMagnumServices(base.BaseFunctionalTestCase): +class TestMagnumServices(base.BaseFunctionalTest): def setUp(self): super(TestMagnumServices, self).setUp() diff --git a/openstack/tests/functional/cloud/test_network.py b/openstack/tests/functional/cloud/test_network.py index a4911b9e4..0001424e7 100644 --- a/openstack/tests/functional/cloud/test_network.py +++ b/openstack/tests/functional/cloud/test_network.py @@ -18,10 +18,10 @@ """ from openstack.cloud.exc import OpenStackCloudException -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base -class TestNetwork(base.BaseFunctionalTestCase): +class TestNetwork(base.BaseFunctionalTest): def setUp(self): super(TestNetwork, self).setUp() if not self.operator_cloud.has_service('network'): diff --git a/openstack/tests/functional/cloud/test_object.py b/openstack/tests/functional/cloud/test_object.py index 2881a6dcb..1f17013c0 100644 --- a/openstack/tests/functional/cloud/test_object.py +++ b/openstack/tests/functional/cloud/test_object.py @@ -24,10 +24,10 @@ from testtools import content from openstack.cloud import exc -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base -class TestObject(base.BaseFunctionalTestCase): +class TestObject(base.BaseFunctionalTest): def setUp(self): super(TestObject, self).setUp() diff --git a/openstack/tests/functional/cloud/test_port.py b/openstack/tests/functional/cloud/test_port.py index fd358b853..d8033c1d0 100644 --- a/openstack/tests/functional/cloud/test_port.py +++ b/openstack/tests/functional/cloud/test_port.py @@ -23,10 +23,10 @@ import random from openstack.cloud.exc import OpenStackCloudException -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base -class TestPort(base.BaseFunctionalTestCase): +class TestPort(base.BaseFunctionalTest): def setUp(self): super(TestPort, self).setUp() diff --git a/openstack/tests/functional/cloud/test_project.py b/openstack/tests/functional/cloud/test_project.py index 48d58bcde..b51e1d4d7 100644 --- a/openstack/tests/functional/cloud/test_project.py +++ b/openstack/tests/functional/cloud/test_project.py @@ -21,10 +21,10 @@ import pprint from openstack.cloud.exc import OpenStackCloudException -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base -class TestProject(base.KeystoneBaseFunctionalTestCase): +class TestProject(base.KeystoneBaseFunctionalTest): def setUp(self): super(TestProject, self).setUp() diff --git a/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py b/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py index 2175960c4..8b731ec5e 100644 --- a/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py @@ -19,10 +19,10 @@ """ from openstack.cloud.exc import OpenStackCloudException -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base -class TestQosBandwidthLimitRule(base.BaseFunctionalTestCase): +class TestQosBandwidthLimitRule(base.BaseFunctionalTest): def setUp(self): super(TestQosBandwidthLimitRule, self).setUp() if not self.operator_cloud.has_service('network'): diff --git a/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py b/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py index 3fcef5154..a08f128a6 100644 --- a/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py +++ b/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py @@ -19,10 +19,10 @@ """ from openstack.cloud.exc import OpenStackCloudException -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base -class TestQosDscpMarkingRule(base.BaseFunctionalTestCase): +class TestQosDscpMarkingRule(base.BaseFunctionalTest): def setUp(self): super(TestQosDscpMarkingRule, self).setUp() if not self.operator_cloud.has_service('network'): diff --git a/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py b/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py index e5e4fda2f..36e3d7588 100644 --- a/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py +++ b/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py @@ -19,10 +19,10 @@ """ from openstack.cloud.exc import OpenStackCloudException -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base -class TestQosMinimumBandwidthRule(base.BaseFunctionalTestCase): +class TestQosMinimumBandwidthRule(base.BaseFunctionalTest): def setUp(self): super(TestQosMinimumBandwidthRule, self).setUp() if not self.operator_cloud.has_service('network'): diff --git a/openstack/tests/functional/cloud/test_qos_policy.py b/openstack/tests/functional/cloud/test_qos_policy.py index 8e619f077..5a6763eab 100644 --- a/openstack/tests/functional/cloud/test_qos_policy.py +++ b/openstack/tests/functional/cloud/test_qos_policy.py @@ -19,10 +19,10 @@ """ from openstack.cloud.exc import OpenStackCloudException -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base -class TestQosPolicy(base.BaseFunctionalTestCase): +class TestQosPolicy(base.BaseFunctionalTest): def setUp(self): super(TestQosPolicy, self).setUp() if not self.operator_cloud.has_service('network'): diff --git a/openstack/tests/functional/cloud/test_quotas.py b/openstack/tests/functional/cloud/test_quotas.py index cb145480d..bbf066480 100644 --- a/openstack/tests/functional/cloud/test_quotas.py +++ b/openstack/tests/functional/cloud/test_quotas.py @@ -17,10 +17,10 @@ Functional tests for `shade` quotas methods. """ -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base -class TestComputeQuotas(base.BaseFunctionalTestCase): +class TestComputeQuotas(base.BaseFunctionalTest): def test_quotas(self): '''Test quotas functionality''' @@ -35,7 +35,7 @@ def test_quotas(self): cores, self.operator_cloud.get_compute_quotas('demo')['cores']) -class TestVolumeQuotas(base.BaseFunctionalTestCase): +class TestVolumeQuotas(base.BaseFunctionalTest): def setUp(self): super(TestVolumeQuotas, self).setUp() @@ -56,7 +56,7 @@ def test_quotas(self): self.operator_cloud.get_volume_quotas('demo')['volumes']) -class TestNetworkQuotas(base.BaseFunctionalTestCase): +class TestNetworkQuotas(base.BaseFunctionalTest): def setUp(self): super(TestNetworkQuotas, self).setUp() diff --git a/openstack/tests/functional/cloud/test_range_search.py b/openstack/tests/functional/cloud/test_range_search.py index 5edb96afb..ab4870d7f 100644 --- a/openstack/tests/functional/cloud/test_range_search.py +++ b/openstack/tests/functional/cloud/test_range_search.py @@ -13,10 +13,10 @@ # limitations under the License. from openstack.cloud import exc -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base -class TestRangeSearch(base.BaseFunctionalTestCase): +class TestRangeSearch(base.BaseFunctionalTest): def _filter_m1_flavors(self, results): """The m1 flavors are the original devstack flavors""" diff --git a/openstack/tests/functional/cloud/test_recordset.py b/openstack/tests/functional/cloud/test_recordset.py index c31622335..3b8550b46 100644 --- a/openstack/tests/functional/cloud/test_recordset.py +++ b/openstack/tests/functional/cloud/test_recordset.py @@ -21,10 +21,10 @@ from testtools import content -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base -class TestRecordset(base.BaseFunctionalTestCase): +class TestRecordset(base.BaseFunctionalTest): def setUp(self): super(TestRecordset, self).setUp() diff --git a/openstack/tests/functional/cloud/test_router.py b/openstack/tests/functional/cloud/test_router.py index 73940c11c..7f4e2e146 100644 --- a/openstack/tests/functional/cloud/test_router.py +++ b/openstack/tests/functional/cloud/test_router.py @@ -20,7 +20,7 @@ import ipaddress from openstack.cloud.exc import OpenStackCloudException -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base EXPECTED_TOPLEVEL_FIELDS = ( @@ -31,7 +31,7 @@ EXPECTED_GW_INFO_FIELDS = ('network_id', 'enable_snat', 'external_fixed_ips') -class TestRouter(base.BaseFunctionalTestCase): +class TestRouter(base.BaseFunctionalTest): def setUp(self): super(TestRouter, self).setUp() if not self.operator_cloud.has_service('network'): diff --git a/openstack/tests/functional/cloud/test_security_groups.py b/openstack/tests/functional/cloud/test_security_groups.py index 8cd379dfc..1a249da77 100644 --- a/openstack/tests/functional/cloud/test_security_groups.py +++ b/openstack/tests/functional/cloud/test_security_groups.py @@ -17,10 +17,10 @@ Functional tests for `shade` security_groups resource. """ -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base -class TestSecurityGroups(base.BaseFunctionalTestCase): +class TestSecurityGroups(base.BaseFunctionalTest): def test_create_list_security_groups(self): sg1 = self.user_cloud.create_security_group( name="sg1", description="sg1") diff --git a/openstack/tests/functional/cloud/test_server_group.py b/openstack/tests/functional/cloud/test_server_group.py index 3b359f081..a1f645039 100644 --- a/openstack/tests/functional/cloud/test_server_group.py +++ b/openstack/tests/functional/cloud/test_server_group.py @@ -17,10 +17,10 @@ Functional tests for `shade` server_group resource. """ -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base -class TestServerGroup(base.BaseFunctionalTestCase): +class TestServerGroup(base.BaseFunctionalTest): def test_server_group(self): server_group_name = self.getUniqueString() diff --git a/openstack/tests/functional/cloud/test_services.py b/openstack/tests/functional/cloud/test_services.py index 570ae9e48..68c30859b 100644 --- a/openstack/tests/functional/cloud/test_services.py +++ b/openstack/tests/functional/cloud/test_services.py @@ -24,10 +24,10 @@ from openstack.cloud.exc import OpenStackCloudException from openstack.cloud.exc import OpenStackCloudUnavailableFeature -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base -class TestServices(base.KeystoneBaseFunctionalTestCase): +class TestServices(base.KeystoneBaseFunctionalTest): service_attributes = ['id', 'name', 'type', 'description'] diff --git a/openstack/tests/functional/cloud/test_stack.py b/openstack/tests/functional/cloud/test_stack.py index 3e1f90694..0df37c462 100644 --- a/openstack/tests/functional/cloud/test_stack.py +++ b/openstack/tests/functional/cloud/test_stack.py @@ -21,7 +21,7 @@ from openstack.cloud import exc from openstack.tests import fakes -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base simple_template = '''heat_template_version: 2014-10-16 parameters: @@ -72,7 +72,7 @@ validate_template = '''heat_template_version: asdf-no-such-version ''' -class TestStack(base.BaseFunctionalTestCase): +class TestStack(base.BaseFunctionalTest): def setUp(self): super(TestStack, self).setUp() diff --git a/openstack/tests/functional/cloud/test_users.py b/openstack/tests/functional/cloud/test_users.py index 6f215cb2c..1dec4b9f1 100644 --- a/openstack/tests/functional/cloud/test_users.py +++ b/openstack/tests/functional/cloud/test_users.py @@ -18,10 +18,10 @@ """ from openstack.cloud.exc import OpenStackCloudException -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base -class TestUsers(base.KeystoneBaseFunctionalTestCase): +class TestUsers(base.KeystoneBaseFunctionalTest): def setUp(self): super(TestUsers, self).setUp() self.user_prefix = self.getUniqueString('user') diff --git a/openstack/tests/functional/cloud/test_volume.py b/openstack/tests/functional/cloud/test_volume.py index ee695bbe8..b5a8b219b 100644 --- a/openstack/tests/functional/cloud/test_volume.py +++ b/openstack/tests/functional/cloud/test_volume.py @@ -21,11 +21,11 @@ from testtools import content from openstack.cloud import exc -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base from openstack import utils -class TestVolume(base.BaseFunctionalTestCase): +class TestVolume(base.BaseFunctionalTest): # Creating and deleting volumes is slow TIMEOUT_SCALING_FACTOR = 1.5 diff --git a/openstack/tests/functional/cloud/test_volume_backup.py b/openstack/tests/functional/cloud/test_volume_backup.py index 486fef9cc..61eabfeae 100644 --- a/openstack/tests/functional/cloud/test_volume_backup.py +++ b/openstack/tests/functional/cloud/test_volume_backup.py @@ -9,10 +9,10 @@ # 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.tests.functional.cloud import base +from openstack.tests.functional import base -class TestVolume(base.BaseFunctionalTestCase): +class TestVolume(base.BaseFunctionalTest): # Creating a volume backup is incredibly slow. TIMEOUT_SCALING_FACTOR = 1.5 diff --git a/openstack/tests/functional/cloud/test_volume_type.py b/openstack/tests/functional/cloud/test_volume_type.py index a48f6f8c5..1eda9f8aa 100644 --- a/openstack/tests/functional/cloud/test_volume_type.py +++ b/openstack/tests/functional/cloud/test_volume_type.py @@ -20,10 +20,10 @@ """ import testtools from openstack.cloud import exc -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base -class TestVolumeType(base.BaseFunctionalTestCase): +class TestVolumeType(base.BaseFunctionalTest): def _assert_project(self, volume_name_or_id, project_id, allowed=True): acls = self.operator_cloud.get_volume_type_access(volume_name_or_id) diff --git a/openstack/tests/functional/cloud/test_zone.py b/openstack/tests/functional/cloud/test_zone.py index 6d67647f6..31ea5de15 100644 --- a/openstack/tests/functional/cloud/test_zone.py +++ b/openstack/tests/functional/cloud/test_zone.py @@ -19,10 +19,10 @@ from testtools import content -from openstack.tests.functional.cloud import base +from openstack.tests.functional import base -class TestZone(base.BaseFunctionalTestCase): +class TestZone(base.BaseFunctionalTest): def setUp(self): super(TestZone, self).setUp() From a4f1f2f59cec08023bb3e870d0aa71c44d4ed5a3 Mon Sep 17 00:00:00 2001 From: zhangdebo Date: Fri, 7 Sep 2018 22:52:52 -0400 Subject: [PATCH 2192/3836] Fix typo Change-Id: I8e2d38b77f4b78330164dbeed51d6ce00a48eefe --- openstack/cloud/_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index 731db5763..1f139f48a 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -495,7 +495,7 @@ def _call_client_and_retry(client, url, retry_on=None, **kwargs): """Method to provide retry operations. - Some APIs utilize HTTP errors on certian operations to indicate that + Some APIs utilize HTTP errors on certain operations to indicate that the resource is presently locked, and as such this mechanism provides the ability to retry upon known error codes. From e0801484c687acdc84009fc5845b88c51529475f Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Wed, 12 Sep 2018 10:27:14 -0300 Subject: [PATCH 2193/3836] Normalize image when using PUT on Glance v2 When Glance v2 was used, the created image was not being normalized, which doesn't help end users on having a consistent representation of the resource regardless the cloud configuration choices. Story: #2003735 Task: #26401 Change-Id: I197762c0e31ccff9cf6fd6a2f9f2ae6174ad36d1 --- openstack/cloud/openstackcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index f6a1534ab..c91c873b5 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -4784,7 +4784,7 @@ def _upload_image_put_v2(self, name, image_data, meta, **image_kwargs): exc_info=True) raise - return image + return self._normalize_image(image) def _upload_image_put_v1( self, name, image_data, meta, **image_kwargs): From d7470b5e952a85718734c837a958660e94f699ff Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Wed, 12 Sep 2018 16:59:52 -0300 Subject: [PATCH 2194/3836] Normalize security groups when using Neutron When Neutron was used, the listed security groups were not being normalized, which doesn't help end users on having a consistent representation of them regardless the cloud configuration choices. Story: #2003741 Task: #26423 Change-Id: I6b132c9aac119d94b1085741f557fa405836a703 --- openstack/cloud/openstackcloud.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index f6a1534ab..c64052c45 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -2074,7 +2074,8 @@ def list_security_groups(self, filters=None): data = self._network_client.get( '/security-groups.json', params=filters, error_message="Error fetching security group list") - return self._get_and_munchify('security_groups', data) + return self._normalize_secgroups( + self._get_and_munchify('security_groups', data)) # Handle nova security groups else: From c3e5eeb09b616e83b2bba01d133f9ba31e1f8da0 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Wed, 5 Sep 2018 11:01:15 -0300 Subject: [PATCH 2195/3836] Implement network update Unlike other networking resources, a network could not be updated yet. Story: #2003669 Task: #26187 Change-Id: Ie259944de003c2afbd768ae118a768fffce474da --- openstack/cloud/openstackcloud.py | 63 +++++++++++++++++++ .../tests/functional/cloud/test_network.py | 9 +++ 2 files changed, 72 insertions(+) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index f6a1534ab..546322724 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -3427,6 +3427,69 @@ def create_network(self, name, shared=False, admin_state_up=True, self._reset_network_caches() return self._get_and_munchify('network', data) + @_utils.valid_kwargs("name", "shared", "admin_state_up", "external", + "provider", "mtu_size", "port_security_enabled") + def update_network(self, name_or_id, **kwargs): + """Update a network. + + :param string name_or_id: Name or ID of the network being updated. + :param string name: New name of the network. + :param bool shared: Set the network as shared. + :param bool admin_state_up: Set the network administrative state to up. + :param bool external: Whether this network is externally accessible. + :param dict provider: A dict of network provider options. Example:: + + { 'network_type': 'vlan', 'segmentation_id': 'vlan1' } + :param int mtu_size: New maximum transmission unit value to address + fragmentation. Minimum value is 68 for IPv4, and 1280 for IPv6. + :param bool port_security_enabled: Enable or disable port security. + + :returns: The updated network object. + :raises: OpenStackCloudException on operation error. + """ + if 'provider' in kwargs: + if not isinstance(kwargs['provider'], dict): + raise exc.OpenStackCloudException( + "Parameter 'provider' must be a dict") + # Only pass what we know + provider = {} + for key in kwargs['provider']: + if key in ('physical_network', 'network_type', + 'segmentation_id'): + provider['provider:' + key] = kwargs['provider'][key] + kwargs['provider'] = provider + + if 'external' in kwargs: + kwargs['router:external'] = kwargs.pop('external') + + if 'port_security_enabled' in kwargs: + if not isinstance(kwargs['port_security_enabled'], bool): + raise exc.OpenStackCloudException( + "Parameter 'port_security_enabled' must be a bool") + + if 'mtu_size' in kwargs: + if not isinstance(kwargs['mtu_size'], int): + raise exc.OpenStackCloudException( + "Parameter 'mtu_size' must be an integer.") + if kwargs['mtu_size'] < 68: + raise exc.OpenStackCloudException( + "Parameter 'mtu_size' must be greater than 67.") + kwargs['mtu'] = kwargs.pop('mtu_size') + + network = self.get_network(name_or_id) + if not network: + raise exc.OpenStackCloudException( + "Network %s not found." % name_or_id) + + data = self._network_client.put( + "/networks/{net_id}.json".format(net_id=network.id), + json={"network": kwargs}, + error_message="Error updating network {0}".format(name_or_id)) + + self._reset_network_caches() + + return self._get_and_munchify('network', data) + def delete_network(self, name_or_id): """Delete a network. diff --git a/openstack/tests/functional/cloud/test_network.py b/openstack/tests/functional/cloud/test_network.py index a4911b9e4..ead3466dc 100644 --- a/openstack/tests/functional/cloud/test_network.py +++ b/openstack/tests/functional/cloud/test_network.py @@ -120,3 +120,12 @@ def test_list_networks_filtered(self): filters=dict(name=self.network_name)) self.assertEqual(1, len(match)) self.assertEqual(net1['name'], match[0]['name']) + + def test_update_network(self): + net = self.operator_cloud.create_network(name=self.network_name) + self.assertEqual(net.name, self.network_name) + new_name = self.getUniqueString('network') + net = self.operator_cloud.update_network(net.id, name=new_name) + self.addCleanup(self.operator_cloud.delete_network, new_name) + self.assertNotEqual(net.name, self.network_name) + self.assertEqual(net.name, new_name) From 4cdf15cc23a5abe2cd134eeb8d51453b0e26b549 Mon Sep 17 00:00:00 2001 From: Clark Boylan Date: Wed, 12 Sep 2018 09:21:38 -0700 Subject: [PATCH 2196/3836] Explicitly set logging levels for external libs Previously logging levels were only set explicitly if we wanted debug level. The problem with this is if you are including the sdk in a system that sets a global debug logging level then you cannot set a less verbose logging level in the sdk via enable_logging. To fix this explicitly set the logging levels in enable_logging which allows you to override any global application logging levels. We do this for keystoneauth, urllib3, and stevedore. Change-Id: I6ac281023b9fd8ccd89d7540169e591b4ea1a470 --- openstack/_log.py | 11 +++++++---- openstack/tests/unit/test_utils.py | 9 +++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/openstack/_log.py b/openstack/_log.py index 4637dfc8b..80c864b38 100644 --- a/openstack/_log.py +++ b/openstack/_log.py @@ -103,11 +103,14 @@ def enable_logging( file_handler.setFormatter(formatter) handlers.append(file_handler) - if http_debug: - # Enable HTTP level tracing - setup_logging('keystoneauth', handlers=handlers, level=level) - + # Always set the appropriate level so that if loggers higher in the tree + # are more verbose we only get what we want out of the SDK. This is + # particularly useful when combined with tools like ansible which set + # debug logging level at the logging root. setup_logging('openstack', handlers=handlers, level=level) + setup_logging('keystoneauth', handlers=handlers, level=level) + setup_logging('urllib3', handlers=handlers, level=level) + setup_logging('stevedore', handlers=handlers, level=level) # Suppress warning about keystoneauth loggers setup_logging('keystoneauth.discovery') setup_logging('keystoneauth.identity.base') diff --git a/openstack/tests/unit/test_utils.py b/openstack/tests/unit/test_utils.py index 8b7e5dc81..c4fbf8b97 100644 --- a/openstack/tests/unit/test_utils.py +++ b/openstack/tests/unit/test_utils.py @@ -26,15 +26,24 @@ def setUp(self): super(Test_enable_logging, self).setUp() self.openstack_logger = mock.Mock() self.openstack_logger.handlers = [] + self.ksa_logger_root = mock.Mock() + self.ksa_logger_root.handlers = [] self.ksa_logger_1 = mock.Mock() self.ksa_logger_1.handlers = [] self.ksa_logger_2 = mock.Mock() self.ksa_logger_2.handlers = [] self.ksa_logger_3 = mock.Mock() self.ksa_logger_3.handlers = [] + self.urllib3_logger = mock.Mock() + self.urllib3_logger.handlers = [] + self.stevedore_logger = mock.Mock() + self.stevedore_logger.handlers = [] self.fake_get_logger = mock.Mock() self.fake_get_logger.side_effect = [ self.openstack_logger, + self.ksa_logger_root, + self.urllib3_logger, + self.stevedore_logger, self.ksa_logger_1, self.ksa_logger_2, self.ksa_logger_3 From 595f4cabae7f9469a00dea8f3317a4a7484ee2e9 Mon Sep 17 00:00:00 2001 From: Tobias Henkel Date: Fri, 14 Sep 2018 15:40:17 +0200 Subject: [PATCH 2197/3836] Add support for configured NAT source variable In some clouds there are more than one potential NAT source and shade's auto_ip can't figure it out propery. os-client-config added a config option similar to nat_destination called nat_source to help people set a config in such environments. Add support for it. This is a port of I4b50c2323a487b5ce90f9d38a48be249cfb739c5 by Monty Taylor from shade to openstacksdk. Change-Id: Ie2d01168a24172f37ebf32f754d8e1a52149cb6e Co-Authored-By: Monty Taylor --- openstack/cloud/openstackcloud.py | 44 +++- .../unit/cloud/test_floating_ip_neutron.py | 194 ++++++++++++++++++ .../nat-source-support-92aaf6b336d0b848.yaml | 4 + 3 files changed, 237 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/nat-source-support-92aaf6b336d0b848.yaml diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index f6a1534ab..ae713856b 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -146,6 +146,7 @@ def __init__(self): self._external_ipv6_names = self.config.get_external_ipv6_networks() self._internal_ipv6_names = self.config.get_internal_ipv6_networks() self._nat_destination = self.config.get_nat_destination() + self._nat_source = self.config.get_nat_source() self._default_network = self.config.get_default_network() self._floating_ip_source = self.config.config.get( @@ -2365,6 +2366,7 @@ def _reset_network_caches(self): self._external_ipv6_networks = [] self._internal_ipv6_networks = [] self._nat_destination_network = None + self._nat_source_network = None self._default_network_network = None self._network_list_stamp = False @@ -2375,6 +2377,7 @@ def _set_interesting_networks(self): external_ipv6_networks = [] internal_ipv6_networks = [] nat_destination = None + nat_source = None default_network = None all_subnets = None @@ -2406,10 +2409,6 @@ def _set_interesting_networks(self): network['id'] not in self._internal_ipv4_names): external_ipv4_networks.append(network) - # External Floating IPv4 networks - if ('router:external' in network and network['router:external']): - external_ipv4_floating_networks.append(network) - # Internal networks if (network['name'] in self._internal_ipv4_names or network['id'] in self._internal_ipv4_names): @@ -2438,6 +2437,25 @@ def _set_interesting_networks(self): network['id'] not in self._external_ipv6_names): internal_ipv6_networks.append(network) + # External Floating IPv4 networks + if self._nat_source in ( + network['name'], network['id']): + if nat_source: + raise exc.OpenStackCloudException( + 'Multiple networks were found matching' + ' {nat_net} which is the network configured' + ' to be the NAT source. Please check your' + ' cloud resources. It is probably a good idea' + ' to configure this network by ID rather than' + ' by name.'.format( + nat_net=self._nat_source)) + external_ipv4_floating_networks.append(network) + nat_source = network + elif self._nat_source is None: + if network.get('router:external'): + external_ipv4_floating_networks.append(network) + nat_source = nat_source or network + # NAT Destination if self._nat_destination in ( network['name'], network['id']): @@ -2522,6 +2540,13 @@ def _set_interesting_networks(self): ' found'.format( network=self._nat_destination)) + if self._nat_source and not nat_source: + raise exc.OpenStackCloudException( + 'Network {network} was configured to be the' + ' source for inbound NAT but it could not be' + ' found'.format( + network=self._nat_source)) + if self._default_network and not default_network: raise exc.OpenStackCloudException( 'Network {network} was configured to be the' @@ -2535,6 +2560,7 @@ def _set_interesting_networks(self): self._external_ipv6_networks = external_ipv6_networks self._internal_ipv6_networks = internal_ipv6_networks self._nat_destination_network = nat_destination + self._nat_source_network = nat_source self._default_network_network = default_network def _find_interesting_networks(self): @@ -2561,6 +2587,14 @@ def get_nat_destination(self): self._find_interesting_networks() return self._nat_destination_network + def get_nat_source(self): + """Return the network that is configured to be the NAT destination. + + :returns: A network dict if one is found + """ + self._find_interesting_networks() + return self._nat_source_network + def get_default_network(self): """Return the network that is configured to be the default interface. @@ -6413,7 +6447,7 @@ def _add_auto_ip(self, server, wait=False, timeout=60, reuse=True): skip_attach = False created = False if reuse: - f_ip = self.available_floating_ip() + f_ip = self.available_floating_ip(server=server) else: start_time = time.time() f_ip = self.create_floating_ip( diff --git a/openstack/tests/unit/cloud/test_floating_ip_neutron.py b/openstack/tests/unit/cloud/test_floating_ip_neutron.py index 8c8b7a66b..bf4871f2c 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_neutron.py +++ b/openstack/tests/unit/cloud/test_floating_ip_neutron.py @@ -998,3 +998,197 @@ def test_create_floating_ip_no_port(self): self.cloud._neutron_create_floating_ip, server=dict(id='some-server')) self.assert_calls() + + def test_find_nat_source_inferred(self): + # payloads contrived but based on ones from citycloud + self.register_uris([ + dict(method='GET', + uri='https://network.example.com/v2.0/networks.json', + json={"networks": [{ + "status": "ACTIVE", + "subnets": [ + "df3e17fa-a4b2-47ae-9015-bc93eb076ba2", + "6b0c3dc9-b0b8-4d87-976a-7f2ebf13e7ec", + "fc541f48-fc7f-48c0-a063-18de6ee7bdd7"], + "availability_zone_hints": [], + "availability_zones": ["nova"], + "name": "ext-net", + "admin_state_up": True, + "tenant_id": "a564613210ee43708b8a7fc6274ebd63", + "tags": [], + "ipv6_address_scope": "9f03124f-89af-483a-b6fd-10f08079db4d", # noqa + "mtu": 0, + "is_default": False, + "router:external": True, + "ipv4_address_scope": None, + "shared": False, + "id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", + "description": None + }, { + "status": "ACTIVE", + "subnets": [ + "df3e17fa-a4b2-47ae-9015-bc93eb076ba2", + "6b0c3dc9-b0b8-4d87-976a-7f2ebf13e7ec", + "fc541f48-fc7f-48c0-a063-18de6ee7bdd7"], + "availability_zone_hints": [], + "availability_zones": ["nova"], + "name": "my-network", + "admin_state_up": True, + "tenant_id": "a564613210ee43708b8a7fc6274ebd63", + "tags": [], + "ipv6_address_scope": "9f03124f-89af-483a-b6fd-10f08079db4d", # noqa + "mtu": 0, + "is_default": False, + "router:external": True, + "ipv4_address_scope": None, + "shared": False, + "id": "0232c17f-2096-49bc-b205-d3dcd9a30ebg", + "description": None + }, { + "status": "ACTIVE", + "subnets": ["f0ad1df5-53ee-473f-b86b-3604ea5591e9"], + "availability_zone_hints": [], + "availability_zones": ["nova"], + "name": "private", + "admin_state_up": True, + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", + "created_at": "2016-10-22T13:46:26", + "tags": [], + "updated_at": "2016-10-22T13:46:26", + "ipv6_address_scope": None, + "router:external": False, + "ipv4_address_scope": None, + "shared": False, + "mtu": 1450, + "id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", + "description": "" + }]}), + dict(method='GET', + uri='https://network.example.com/v2.0/subnets.json', + json={"subnets": [{ + "description": "", + "enable_dhcp": True, + "network_id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", + "created_at": "2016-10-22T13:46:26", + "dns_nameservers": [ + "89.36.90.101", + "89.36.90.102"], + "updated_at": "2016-10-22T13:46:26", + "gateway_ip": "10.4.0.1", + "ipv6_ra_mode": None, + "allocation_pools": [{ + "start": "10.4.0.2", + "end": "10.4.0.200"}], + "host_routes": [], + "ip_version": 4, + "ipv6_address_mode": None, + "cidr": "10.4.0.0/24", + "id": "f0ad1df5-53ee-473f-b86b-3604ea5591e9", + "subnetpool_id": None, + "name": "private-subnet-ipv4", + }]}) + ]) + + self.assertEqual( + 'ext-net', self.cloud.get_nat_source()['name']) + + self.assert_calls() + + def test_find_nat_source_config(self): + self.cloud._nat_source = 'my-network' + + # payloads contrived but based on ones from citycloud + self.register_uris([ + dict(method='GET', + uri='https://network.example.com/v2.0/networks.json', + json={"networks": [{ + "status": "ACTIVE", + "subnets": [ + "df3e17fa-a4b2-47ae-9015-bc93eb076ba2", + "6b0c3dc9-b0b8-4d87-976a-7f2ebf13e7ec", + "fc541f48-fc7f-48c0-a063-18de6ee7bdd7"], + "availability_zone_hints": [], + "availability_zones": ["nova"], + "name": "ext-net", + "admin_state_up": True, + "tenant_id": "a564613210ee43708b8a7fc6274ebd63", + "tags": [], + "ipv6_address_scope": "9f03124f-89af-483a-b6fd-10f08079db4d", # noqa + "mtu": 0, + "is_default": False, + "router:external": True, + "ipv4_address_scope": None, + "shared": False, + "id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", + "description": None + }, { + "status": "ACTIVE", + "subnets": [ + "df3e17fa-a4b2-47ae-9015-bc93eb076ba2", + "6b0c3dc9-b0b8-4d87-976a-7f2ebf13e7ec", + "fc541f48-fc7f-48c0-a063-18de6ee7bdd7"], + "availability_zone_hints": [], + "availability_zones": ["nova"], + "name": "my-network", + "admin_state_up": True, + "tenant_id": "a564613210ee43708b8a7fc6274ebd63", + "tags": [], + "ipv6_address_scope": "9f03124f-89af-483a-b6fd-10f08079db4d", # noqa + "mtu": 0, + "is_default": False, + "router:external": True, + "ipv4_address_scope": None, + "shared": False, + "id": "0232c17f-2096-49bc-b205-d3dcd9a30ebg", + "description": None + }, { + "status": "ACTIVE", + "subnets": ["f0ad1df5-53ee-473f-b86b-3604ea5591e9"], + "availability_zone_hints": [], + "availability_zones": ["nova"], + "name": "private", + "admin_state_up": True, + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", + "created_at": "2016-10-22T13:46:26", + "tags": [], + "updated_at": "2016-10-22T13:46:26", + "ipv6_address_scope": None, + "router:external": False, + "ipv4_address_scope": None, + "shared": False, + "mtu": 1450, + "id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", + "description": "" + }]}), + dict(method='GET', + uri='https://network.example.com/v2.0/subnets.json', + json={"subnets": [{ + "description": "", + "enable_dhcp": True, + "network_id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", + "created_at": "2016-10-22T13:46:26", + "dns_nameservers": [ + "89.36.90.101", + "89.36.90.102"], + "updated_at": "2016-10-22T13:46:26", + "gateway_ip": "10.4.0.1", + "ipv6_ra_mode": None, + "allocation_pools": [{ + "start": "10.4.0.2", + "end": "10.4.0.200"}], + "host_routes": [], + "ip_version": 4, + "ipv6_address_mode": None, + "cidr": "10.4.0.0/24", + "id": "f0ad1df5-53ee-473f-b86b-3604ea5591e9", + "subnetpool_id": None, + "name": "private-subnet-ipv4", + }]}) + ]) + + self.assertEqual( + 'my-network', self.cloud.get_nat_source()['name']) + + self.assert_calls() diff --git a/releasenotes/notes/nat-source-support-92aaf6b336d0b848.yaml b/releasenotes/notes/nat-source-support-92aaf6b336d0b848.yaml new file mode 100644 index 000000000..efd8713a4 --- /dev/null +++ b/releasenotes/notes/nat-source-support-92aaf6b336d0b848.yaml @@ -0,0 +1,4 @@ +--- +features: + - Add support for networks being configured as the + primary nat_source in clouds.yaml. From 19124c9ec404e3c0c8e688c5b8bb55539816d749 Mon Sep 17 00:00:00 2001 From: Mohammed Naser Date: Sun, 16 Sep 2018 14:38:03 -0400 Subject: [PATCH 2198/3836] Add sjc1 to vexxhost profile Change-Id: I131053b50b033f6b2a4096910f0820efdc337ddf --- openstack/config/vendors/vexxhost.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openstack/config/vendors/vexxhost.json b/openstack/config/vendors/vexxhost.json index 0598d10dc..6ec6fd5c3 100644 --- a/openstack/config/vendors/vexxhost.json +++ b/openstack/config/vendors/vexxhost.json @@ -6,7 +6,8 @@ "auth_url": "https://auth.vexxhost.net/v3" }, "regions": [ - "ca-ymq-1" + "ca-ymq-1", + "sjc1" ], "dns_api_version": "1", "identity_api_version": "3", From 2ea35b22ac025b812d266b4a341e49b0d671d258 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Mon, 3 Sep 2018 16:52:30 +0200 Subject: [PATCH 2199/3836] Update baremetal objects with fields added up to Rocky Change-Id: If336ae3fa8dc629bd86730d833a4f917b68df827 --- openstack/baremetal/v1/_proxy.py | 11 ++- openstack/baremetal/v1/driver.py | 88 ++++++++++++++++++- openstack/baremetal/v1/node.py | 61 +++++++++++-- openstack/baremetal/v1/port.py | 9 +- openstack/baremetal/v1/port_group.py | 8 +- .../baremetal/test_baremetal_driver.py | 55 ++++++++++++ .../baremetal/test_baremetal_port.py | 17 ++++ .../baremetal/test_baremetal_port_group.py | 17 ++++ 8 files changed, 249 insertions(+), 17 deletions(-) create mode 100644 openstack/tests/functional/baremetal/test_baremetal_driver.py diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index abf02dc4d..a37c32763 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -129,12 +129,19 @@ def delete_chassis(self, chassis, ignore_missing=True): return self._delete(_chassis.Chassis, chassis, ignore_missing=ignore_missing) - def drivers(self): + def drivers(self, details=False): """Retrieve a generator of drivers. + :param bool details: A boolean indicating whether the detailed + information for every driver should be returned. :returns: A generator of driver instances. """ - return self._list(_driver.Driver, paginated=False) + kwargs = {} + # NOTE(dtantsur): details are available starting with API microversion + # 1.30. Thus we do not send any value if not needed. + if details: + kwargs['details'] = True + return self._list(_driver.Driver, paginated=False, **kwargs) def get_driver(self, driver): """Get a specific driver. diff --git a/openstack/baremetal/v1/driver.py b/openstack/baremetal/v1/driver.py index 5434e8259..63803127a 100644 --- a/openstack/baremetal/v1/driver.py +++ b/openstack/baremetal/v1/driver.py @@ -27,13 +27,95 @@ class Driver(resource.Resource): allow_delete = False allow_list = True - # NOTE: Query mapping? + _query_mapping = resource.QueryParameters(details='detail') + + # The BIOS interface fields introduced in 1.40 (Rocky). + _max_microversion = '1.40' - #: The name of the driver - name = resource.Body('name', alternate_id=True) #: A list of active hosts that support this driver. hosts = resource.Body('hosts', type=list) #: A list of relative links, including the self and bookmark links. links = resource.Body('links', type=list) + #: The name of the driver + name = resource.Body('name', alternate_id=True) #: A list of links to driver properties. properties = resource.Body('properties', type=list) + + # Hardware interface properties grouped together for convenience, + # available with detail=True. + + #: Default BIOS interface implementation. + #: Introduced in API microversion 1.40. + default_bios_interface = resource.Body("default_bios_interface") + #: Default boot interface implementation. + #: Introduced in API microversion 1.30. + default_boot_interface = resource.Body("default_boot_interface") + #: Default console interface implementation. + #: Introduced in API microversion 1.30. + default_console_interface = resource.Body("default_console_interface") + #: Default deploy interface implementation. + #: Introduced in API microversion 1.30. + default_deploy_interface = resource.Body("default_deploy_interface") + #: Default inspect interface implementation. + #: Introduced in API microversion 1.30. + default_inspect_interface = resource.Body("default_inspect_interface") + #: Default management interface implementation. + #: Introduced in API microversion 1.30. + default_management_interface = resource.Body( + "default_management_interface") + #: Default network interface implementation. + #: Introduced in API microversion 1.30. + default_network_interface = resource.Body("default_network_interface") + #: Default port interface implementation. + #: Introduced in API microversion 1.30. + default_power_interface = resource.Body("default_power_interface") + #: Default RAID interface implementation. + #: Introduced in API microversion 1.30. + default_raid_interface = resource.Body("default_raid_interface") + #: Default rescue interface implementation. + #: Introduced in API microversion 1.38. + default_rescue_interface = resource.Body("default_rescue_interface") + #: Default storage interface implementation. + #: Introduced in API microversion 1.33. + default_storage_interface = resource.Body("default_storage_interface") + #: Default vendor interface implementation. + #: Introduced in API microversion 1.30. + default_vendor_interface = resource.Body("default_vendor_interface") + + #: Enabled BIOS interface implementations. + #: Introduced in API microversion 1.40. + enabled_bios_interfaces = resource.Body("enabled_bios_interfaces") + #: Enabled boot interface implementations. + #: Introduced in API microversion 1.30. + enabled_boot_interfaces = resource.Body("enabled_boot_interfaces") + #: Enabled console interface implementations. + #: Introduced in API microversion 1.30. + enabled_console_interfaces = resource.Body("enabled_console_interfaces") + #: Enabled deploy interface implementations. + #: Introduced in API microversion 1.30. + enabled_deploy_interfaces = resource.Body("enabled_deploy_interfaces") + #: Enabled inspect interface implementations. + #: Introduced in API microversion 1.30. + enabled_inspect_interfaces = resource.Body("enabled_inspect_interfaces") + #: Enabled management interface implementations. + #: Introduced in API microversion 1.30. + enabled_management_interfaces = resource.Body( + "enabled_management_interfaces") + #: Enabled network interface implementations. + #: Introduced in API microversion 1.30. + enabled_network_interfaces = resource.Body("enabled_network_interfaces") + #: Enabled port interface implementations. + #: Introduced in API microversion 1.30. + enabled_power_interfaces = resource.Body("enabled_power_interfaces") + #: Enabled RAID interface implementations. + #: Introduced in API microversion 1.30. + enabled_raid_interfaces = resource.Body("enabled_raid_interfaces") + #: Enabled rescue interface implementations. + #: Introduced in API microversion 1.38. + enabled_rescue_interfaces = resource.Body("enabled_rescue_interfaces") + #: Enabled storage interface implementations. + #: Introduced in API microversion 1.33. + enabled_storage_interfaces = resource.Body("enabled_storage_interfaces") + #: Enabled vendor interface implementations. + #: Introduced in API microversion 1.30. + enabled_vendor_interfaces = resource.Body("enabled_vendor_interfaces") diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index d60758f8c..85667060e 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -37,21 +37,26 @@ class Node(resource.Resource): commit_jsonpatch = True _query_mapping = resource.QueryParameters( - 'associated', 'driver', 'fields', 'provision_state', 'resource_class', + 'associated', 'conductor_group', 'driver', 'fault', 'fields', + 'provision_state', 'resource_class', instance_id='instance_uuid', is_maintenance='maintenance', ) - # VIF attach/detach support introduced in 1.28. - _max_microversion = '1.28' + # The conductor_group field introduced in 1.46 (Rocky). + _max_microversion = '1.46' # Properties #: The UUID of the chassis associated wit this node. Can be empty or None. chassis_id = resource.Body("chassis_uuid") #: The current clean step. clean_step = resource.Body("clean_step") + #: Conductor group this node is managed by. Added in API microversion 1.46. + conductor_group = resource.Body("conductor_group") #: Timestamp at which the node was last updated. created_at = resource.Body("created_at") + #: The current deploy step. Added in API microversion 1.44. + deploy_step = resource.Body("deploy_step") #: The name of the driver. driver = resource.Body("driver") #: All the metadata required by the driver to manage this node. List of @@ -62,6 +67,9 @@ class Node(resource.Resource): driver_internal_info = resource.Body("driver_internal_info", type=dict) #: A set of one or more arbitrary metadata key and value pairs. extra = resource.Body("extra") + #: Fault type that caused the node to enter maintenance mode. + #: Introduced in API microversion 1.42. + fault = resource.Body("fault") #: The UUID of the node resource. id = resource.Body("uuid", alternate_id=True) #: Information used to customize the deployed image, e.g. size of root @@ -86,9 +94,6 @@ class Node(resource.Resource): #: Human readable identifier for the node. May be undefined. Certain words #: are reserved. Added in API microversion 1.5 name = resource.Body("name") - #: Network interface provider to use when plumbing the network connections - #: for this node. Introduced in API microversion 1.20. - network_interface = resource.Body("network_interface") #: Links to the collection of ports on this node. ports = resource.Body("ports", type=list) #: Links to the collection of portgroups on this node. Available since @@ -121,9 +126,50 @@ class Node(resource.Resource): #: The requested RAID configuration of the node which will be applied when #: the node next transitions through the CLEANING state. target_raid_config = resource.Body("target_raid_config") + #: Traits of the node. Introduced in API microversion 1.37. + traits = resource.Body("traits", type=list) #: Timestamp at which the node was last updated. updated_at = resource.Body("updated_at") + # Hardware interfaces grouped together for convenience. + + #: BIOS interface to use when setting BIOS properties of the node. + #: Introduced in API microversion 1.40. + bios_interface = resource.Body("bios_interface") + #: Boot interface to use when configuring boot of the node. + #: Introduced in API microversion 1.31. + boot_interface = resource.Body("boot_interface") + #: Console interface to use when working with serial console. + #: Introduced in API microversion 1.31. + console_interface = resource.Body("console_interface") + #: Deploy interface to use when deploying the node. + #: Introduced in API microversion 1.31. + deploy_interface = resource.Body("deploy_interface") + #: Inspect interface to use when inspecting the node. + #: Introduced in API microversion 1.31. + inspect_interface = resource.Body("inspect_interface") + #: Management interface to use for management actions on the node. + #: Introduced in API microversion 1.31. + management_interface = resource.Body("management_interface") + #: Network interface provider to use when plumbing the network connections + #: for this node. Introduced in API microversion 1.20. + network_interface = resource.Body("network_interface") + #: Power interface to use for power actions on the node. + #: Introduced in API microversion 1.31. + power_interface = resource.Body("power_interface") + #: RAID interface to use for configuring RAID on the node. + #: Introduced in API microversion 1.31. + raid_interface = resource.Body("raid_interface") + #: Rescue interface to use for rescuing of the node. + #: Introduced in API microversion 1.38. + rescue_interface = resource.Body("rescue_interface") + #: Storage interface to use when attaching remote storage. + #: Introduced in API microversion 1.33. + storage_interface = resource.Body("storage_interface") + #: Vendor interface to use for vendor-specific actions on the node. + #: Introduced in API microversion 1.31. + vendor_interface = resource.Body("vendor_interface") + def _consume_body_attrs(self, attrs): if 'provision_state' in attrs and attrs['provision_state'] is None: # API version 1.1 uses None instead of "available". Make it @@ -463,7 +509,8 @@ class NodeDetail(Node): allow_list = True _query_mapping = resource.QueryParameters( - 'associated', 'driver', 'fields', 'provision_state', 'resource_class', + 'associated', 'conductor_group', 'driver', 'fault', + 'provision_state', 'resource_class', instance_id='instance_uuid', is_maintenance='maintenance', ) diff --git a/openstack/baremetal/v1/port.py b/openstack/baremetal/v1/port.py index 3f12affaf..871b97a2a 100644 --- a/openstack/baremetal/v1/port.py +++ b/openstack/baremetal/v1/port.py @@ -30,11 +30,11 @@ class Port(resource.Resource): commit_jsonpatch = True _query_mapping = resource.QueryParameters( - 'fields' + 'address', 'fields', 'node', 'portgroup', ) - # Port group ID introduced in 1.24 - _max_microversion = '1.24' + # The physical_network field introduced in 1.34 + _max_microversion = '1.34' #: The physical hardware address of the network port, typically the #: hardware MAC address. @@ -59,6 +59,9 @@ class Port(resource.Resource): local_link_connection = resource.Body('local_link_connection') #: The UUID of node this port belongs to node_id = resource.Body('node_uuid') + #: The name of physical network this port is attached to. + #: Added in API microversion 1.34. + physical_network = resource.Body('physical_network') #: The UUID of PortGroup this port belongs to. Added in API microversion #: 1.24. port_group_id = resource.Body('portgroup_uuid') diff --git a/openstack/baremetal/v1/port_group.py b/openstack/baremetal/v1/port_group.py index 9ab4c5682..dbee1d93f 100644 --- a/openstack/baremetal/v1/port_group.py +++ b/openstack/baremetal/v1/port_group.py @@ -33,8 +33,8 @@ class PortGroup(resource.Resource): 'node', 'address', 'fields', ) - # Port groups introduced in 1.23 - _max_microversion = '1.23' + # The mode and properties field introduced in 1.26. + _max_microversion = '1.26' #: The physical hardware address of the portgroup, typically the hardware #: MAC address. Added in API microversion 1.23. @@ -55,11 +55,15 @@ class PortGroup(resource.Resource): type=bool) #: A list of relative links, including the self and bookmark links. links = resource.Body('links', type=list) + #: Port bonding mode. Added in API microversion 1.26. + mode = resource.Body('mode') #: UUID of the node this portgroup belongs to. node_id = resource.Body('node_uuid') #: A list of links to the collection of ports belonging to this portgroup. #: Added in API microversion 1.24. ports = resource.Body('ports') + #: Port group properties. Added in API microversion 1.26. + properties = resource.Body('properties', type=dict) #: Timestamp at which the portgroup was last updated. updated_at = resource.Body('updated_at') diff --git a/openstack/tests/functional/baremetal/test_baremetal_driver.py b/openstack/tests/functional/baremetal/test_baremetal_driver.py new file mode 100644 index 000000000..793330bb6 --- /dev/null +++ b/openstack/tests/functional/baremetal/test_baremetal_driver.py @@ -0,0 +1,55 @@ +# 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.tests.functional.baremetal import base + + +class TestBareMetalDriver(base.BaseBaremetalTest): + + def test_fake_hardware_get(self): + driver = self.conn.baremetal.get_driver('fake-hardware') + self.assertEqual('fake-hardware', driver.name) + self.assertNotEqual([], driver.hosts) + + def test_fake_hardware_list(self): + drivers = self.conn.baremetal.drivers() + self.assertIn('fake-hardware', [d.name for d in drivers]) + + def test_driver_negative_non_existing(self): + self.assertRaises(exceptions.ResourceNotFound, + self.conn.baremetal.get_driver, 'not-a-driver') + + +class TestBareMetalDriverDetails(base.BaseBaremetalTest): + + min_microversion = '1.30' + + def test_fake_hardware_get(self): + driver = self.conn.baremetal.get_driver('fake-hardware') + self.assertEqual('fake-hardware', driver.name) + for iface in ('boot', 'deploy', 'management', 'power'): + self.assertIn('fake', + getattr(driver, 'enabled_%s_interfaces' % iface)) + self.assertEqual('fake', + getattr(driver, 'default_%s_interface' % iface)) + self.assertNotEqual([], driver.hosts) + + def test_fake_hardware_list_details(self): + drivers = self.conn.baremetal.drivers(details=True) + driver = [d for d in drivers if d.name == 'fake-hardware'][0] + for iface in ('boot', 'deploy', 'management', 'power'): + self.assertIn('fake', + getattr(driver, 'enabled_%s_interfaces' % iface)) + self.assertEqual('fake', + getattr(driver, 'default_%s_interface' % iface)) diff --git a/openstack/tests/functional/baremetal/test_baremetal_port.py b/openstack/tests/functional/baremetal/test_baremetal_port.py index 29bd6b887..f7abde8b6 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_port.py +++ b/openstack/tests/functional/baremetal/test_baremetal_port.py @@ -36,6 +36,23 @@ def test_port_create_get_delete(self): self.assertRaises(exceptions.ResourceNotFound, self.conn.baremetal.get_port, port.id) + def test_port_list(self): + node2 = self.create_node(name='test-node') + + port1 = self.create_port(address='11:22:33:44:55:66', + node_id=node2.id) + port2 = self.create_port(address='11:22:33:44:55:77', + node_id=self.node.id) + + ports = self.conn.baremetal.ports(address='11:22:33:44:55:77') + self.assertEqual([p.id for p in ports], [port2.id]) + + ports = self.conn.baremetal.ports(node=node2.id) + self.assertEqual([p.id for p in ports], [port1.id]) + + ports = self.conn.baremetal.ports(node='test-node') + self.assertEqual([p.id for p in ports], [port1.id]) + def test_port_update(self): port = self.create_port(address='11:22:33:44:55:66') port.address = '66:55:44:33:22:11' diff --git a/openstack/tests/functional/baremetal/test_baremetal_port_group.py b/openstack/tests/functional/baremetal/test_baremetal_port_group.py index 819e4d67e..71f2f0b14 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_port_group.py +++ b/openstack/tests/functional/baremetal/test_baremetal_port_group.py @@ -34,6 +34,23 @@ def test_port_group_create_get_delete(self): self.assertRaises(exceptions.ResourceNotFound, self.conn.baremetal.get_port_group, port_group.id) + def test_port_list(self): + node2 = self.create_node(name='test-node') + + pg1 = self.create_port_group(address='11:22:33:44:55:66', + node_id=node2.id) + pg2 = self.create_port_group(address='11:22:33:44:55:77', + node_id=self.node.id) + + pgs = self.conn.baremetal.port_groups(address='11:22:33:44:55:77') + self.assertEqual([p.id for p in pgs], [pg2.id]) + + pgs = self.conn.baremetal.port_groups(node=node2.id) + self.assertEqual([p.id for p in pgs], [pg1.id]) + + pgs = self.conn.baremetal.port_groups(node='test-node') + self.assertEqual([p.id for p in pgs], [pg1.id]) + def test_port_group_update(self): port_group = self.create_port_group() port_group.extra = {'answer': 42} From f0138e4adb0e3fec889fd3960b30fcb36cc6b913 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Wed, 19 Sep 2018 08:18:45 +0200 Subject: [PATCH 2200/3836] Update .zuul.yaml Some cleanups: * Remove python3 variable from tox-docs, tox.ini is setup correctly; no need for this setting - and the docs-pti template uses tox.ini * Use lower-constraints template instead of individual jobs * Use cover template to run cover job in check instead of post queue. * Sort template list alphabetically. Change-Id: I6b77786007070794601c0b7d6262a4c2db8fbe0c --- .zuul.yaml | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 9e543f757..9b0739afd 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -350,23 +350,22 @@ - project: templates: + - check-requirements + - openstack-cover-jobs + - openstack-lower-constraints-jobs + - openstack-python-jobs + - openstack-python35-jobs - openstack-python36-jobs - openstacksdk-functional-tips - openstacksdk-tox-tips - os-client-config-tox-tips - osc-tox-unit-tips - - shade-functional-tips - - shade-tox-tips - - openstack-python-jobs - - openstack-python35-jobs - - check-requirements - publish-openstack-docs-pti - release-notes-jobs-python3 + - shade-functional-tips + - shade-tox-tips check: jobs: - - openstack-tox-docs: - vars: - sphinx_python: python3 - openstacksdk-ansible-devel-functional-devstack: voting: false - openstacksdk-ansible-stable-2.6-functional-devstack: @@ -381,21 +380,13 @@ - osc-functional-devstack-tips: voting: false - neutron-grenade - - openstack-tox-lower-constraints - nodepool-functional-py35-src - bifrost-integration-tinyipa-ubuntu-xenial gate: jobs: - - openstack-tox-docs: - vars: - sphinx_python: python3 - openstacksdk-functional-devstack - openstacksdk-functional-devstack-python3 - openstacksdk-functional-devstack-senlin - neutron-grenade - - openstack-tox-lower-constraints - nodepool-functional-py35-src - bifrost-integration-tinyipa-ubuntu-xenial - post: - jobs: - - openstack-tox-cover From c2311d619f4a195091694c26956dadbacc344acc Mon Sep 17 00:00:00 2001 From: Ian Wienand Date: Tue, 11 Sep 2018 12:19:57 +1000 Subject: [PATCH 2201/3836] Handle empty values in regions I had a cloud config file that looked like clouds: mycloud: regions: - name: region auth: ... and the client became rather unhelpful: $ OS_CLIENT_CONFIG_FILE=./clouds.yaml openstack --os-cloud=mycloud ... 'values' $ # huh? Using '--debug' did give the traceback, but I still had to dig around in the code to figure it out. Anyway, it turns out that if a "regions:" entry is a dictionary, it is assumed it has a "values:" key. It seems like there's nothing really wrong with just defaulting to a blank "values" in this case and allowing it to be a valid config. With this the above config was handled correctly. Add some validation on the regions entries to ensure it has a 'name' key, and only the keys 'name' and 'values' while we're here. Tests are added to cover both cases Change-Id: I503b40a5d93d384d7d86268194245dc3e8d75744 --- openstack/config/loader.py | 7 ++++ openstack/tests/unit/config/base.py | 5 ++- openstack/tests/unit/config/test_config.py | 44 +++++++++++++++++++--- 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/openstack/config/loader.py b/openstack/config/loader.py index d38cc4bf6..fb65e589f 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -351,6 +351,13 @@ def _expand_regions(self, regions): ret = [] for region in regions: if isinstance(region, dict): + # i.e. must have name key, and only name,values keys + if 'name' not in region or \ + not {'name', 'values'} >= set(region): + raise exceptions.ConfigException( + 'Invalid region entry at: %s' % region) + if 'values' not in region: + region['values'] = {} ret.append(copy.deepcopy(region)) else: ret.append(self._expand_region_name(region)) diff --git a/openstack/tests/unit/config/base.py b/openstack/tests/unit/config/base.py index 5e2a72a65..93dda054e 100644 --- a/openstack/tests/unit/config/base.py +++ b/openstack/tests/unit/config/base.py @@ -142,7 +142,10 @@ 'values': { 'external_network': 'my-network', } - } + }, + { + 'name': 'region-no-value', + }, ], }, '_test_cloud_hyphenated': { diff --git a/openstack/tests/unit/config/test_config.py b/openstack/tests/unit/config/test_config.py index b92071126..fda047040 100644 --- a/openstack/tests/unit/config/test_config.py +++ b/openstack/tests/unit/config/test_config.py @@ -40,11 +40,11 @@ def test_get_all(self): vendor_files=[self.vendor_yaml], secure_files=[self.no_yaml]) clouds = c.get_all() - # We add one by hand because the regions cloud is going to exist - # twice since it has two regions in it + # We add two by hand because the regions cloud is going to exist + # thrice since it has three regions in it user_clouds = [ cloud for cloud in base.USER_CONF['clouds'].keys() - ] + ['_test_cloud_regions'] + ] + ['_test_cloud_regions', '_test_cloud_regions'] configured_clouds = [cloud.name for cloud in clouds] self.assertItemsEqual(user_clouds, configured_clouds) @@ -54,11 +54,11 @@ def test_get_all_clouds(self): vendor_files=[self.vendor_yaml], secure_files=[self.no_yaml]) clouds = c.get_all_clouds() - # We add one by hand because the regions cloud is going to exist - # twice since it has two regions in it + # We add two by hand because the regions cloud is going to exist + # thrice since it has three regions in it user_clouds = [ cloud for cloud in base.USER_CONF['clouds'].keys() - ] + ['_test_cloud_regions'] + ] + ['_test_cloud_regions', '_test_cloud_regions'] configured_clouds = [cloud.name for cloud in clouds] self.assertItemsEqual(user_clouds, configured_clouds) @@ -382,6 +382,13 @@ def test_get_region_many_regions(self): self.assertEqual(region, {'name': 'region2', 'values': {'external_network': 'my-network'}}) + def test_get_region_by_name_no_value(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml]) + region = c._get_region(cloud='_test_cloud_regions', + region_name='region-no-value') + self.assertEqual(region, {'name': 'region-no-value', 'values': {}}) + def test_get_region_invalid_region(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml], @@ -397,6 +404,31 @@ def test_get_region_no_cloud(self): region = c._get_region(region_name='no-cloud-region') self.assertEqual(region, {'name': 'no-cloud-region', 'values': {}}) + def test_get_region_invalid_keys(self): + invalid_conf = base._write_yaml({ + 'clouds': { + '_test_cloud': { + 'profile': '_test_cloud_in_our_cloud', + 'auth': { + 'auth_url': 'http://example.com/v2', + 'username': 'testuser', + 'password': 'testpass', + }, + 'regions': [ + { + 'name': 'region1', + 'foo': 'bar' + }, + ] + } + } + }) + c = config.OpenStackConfig(config_files=[invalid_conf], + vendor_files=[self.vendor_yaml]) + self.assertRaises( + exceptions.ConfigException, c._get_region, + cloud='_test_cloud', region_name='region1') + class TestExcludedFormattedConfigValue(base.TestCase): # verify https://storyboard.openstack.org/#!/story/1635696 From fb74c7d721993305f172bf8b8d1e2eceb7596cc0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 22 Sep 2018 05:50:49 -0500 Subject: [PATCH 2202/3836] Turn down stevedore and urllib logging Having stevedore logging set to debug makes actual debug output almost useless, and it's never interesting logging for openstacksdk. enable_logging should do the most expected and useful thing. If someone wants to use openstacksdk in debug mode and see stevedore debug messages, the full power of python logging is available to them. Change-Id: I4cd7e6eb06081768afbc077e5bda397e6ae13e1b --- openstack/_log.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/openstack/_log.py b/openstack/_log.py index 80c864b38..ea759ce3d 100644 --- a/openstack/_log.py +++ b/openstack/_log.py @@ -103,14 +103,20 @@ def enable_logging( file_handler.setFormatter(formatter) handlers.append(file_handler) - # Always set the appropriate level so that if loggers higher in the tree + setup_logging('openstack', handlers=handlers, level=level) + setup_logging('keystoneauth', handlers=handlers, level=level) + + # Turn off logging on these so that if loggers higher in the tree # are more verbose we only get what we want out of the SDK. This is # particularly useful when combined with tools like ansible which set # debug logging level at the logging root. - setup_logging('openstack', handlers=handlers, level=level) - setup_logging('keystoneauth', handlers=handlers, level=level) - setup_logging('urllib3', handlers=handlers, level=level) - setup_logging('stevedore', handlers=handlers, level=level) + # If more complex logging is desired including stevedore debug logging, + # enable_logging should not be used and instead python logging should + # be configured directly. + setup_logging( + 'urllib3', handlers=[logging.NullHandler()], level=logging.INFO) + setup_logging( + 'stevedore', handlers=[logging.NullHandler()], level=logging.INFO) # Suppress warning about keystoneauth loggers setup_logging('keystoneauth.discovery') setup_logging('keystoneauth.identity.base') From e4ef46095651b527be226c62bb82cbbe2a40735f Mon Sep 17 00:00:00 2001 From: melissaml Date: Mon, 24 Sep 2018 18:51:16 +0800 Subject: [PATCH 2203/3836] Update the URL in doc Change-Id: I2bb2683a53b22e8aefffb3a7aa6c9cc1303ded7e --- HACKING.rst | 2 +- doc/source/contributor/coding.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/HACKING.rst b/HACKING.rst index bccc1b455..bda11bcbd 100644 --- a/HACKING.rst +++ b/HACKING.rst @@ -2,7 +2,7 @@ openstacksdk Style Commandments =============================== Read the OpenStack Style Commandments -http://docs.openstack.org/developer/hacking/ +https://docs.openstack.org/hacking/latest/ Indentation ----------- diff --git a/doc/source/contributor/coding.rst b/doc/source/contributor/coding.rst index 7ee1f557b..14ab10ff8 100644 --- a/doc/source/contributor/coding.rst +++ b/doc/source/contributor/coding.rst @@ -18,7 +18,7 @@ Below are the patterns that we expect openstacksdk developers to follow. Release Notes ============= -openstacksdk uses `reno `_ for +openstacksdk uses `reno `_ for managing its release notes. A new release note should be added to your contribution anytime you add new API calls, fix significant bugs, add new functionality or parameters to existing API calls, or make any From 1e46a950eba5986baaf785287e3d8e8a14cbb3c8 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 19 Sep 2018 07:42:55 -0500 Subject: [PATCH 2204/3836] Run all tasks through the threadpool We have support for handling async tasks in the task manager and tracking their rate limiting at task start time. Extend this to everything, even the sync calls. Change-Id: I33325fb5be21264df0a68ceef2202ab7875f63ec --- openstack/_adapter.py | 8 ++- openstack/task_manager.py | 60 +++++++------------ .../tests/unit/cloud/test_task_manager.py | 11 +--- 3 files changed, 30 insertions(+), 49 deletions(-) diff --git a/openstack/_adapter.py b/openstack/_adapter.py index c4a8df5ec..5340888b9 100644 --- a/openstack/_adapter.py +++ b/openstack/_adapter.py @@ -139,10 +139,14 @@ def request( request_method = functools.partial( super(OpenStackSDKAdapter, self).request, url, method) - return self.task_manager.submit_function( - request_method, run_async=run_async, name=name, + ret = self.task_manager.submit_function( + request_method, run_async=True, name=name, connect_retries=connect_retries, raise_exc=raise_exc, **kwargs) + if run_async: + return ret + else: + return ret.result() def _version_matches(self, version): api_version = self.get_api_major_version() diff --git a/openstack/task_manager.py b/openstack/task_manager.py index 7bc52c1cd..9293a3d19 100644 --- a/openstack/task_manager.py +++ b/openstack/task_manager.py @@ -13,6 +13,7 @@ # limitations under the License. import concurrent.futures +import functools import sys import threading import time @@ -125,27 +126,29 @@ def submit_task(self, task): This method calls task.wait() so that it only returns when the task is complete. """ - if task.run_async: - # Async tasks run the wait lower in the stack because the wait - # is just returning the concurrent Future object. That future - # object handles the exception shifting across threads. - return self.run_task(task=task) - else: - # It's important that we call task.wait() here, rather than in - # the run_task call stack below here, since subclasses may - # cause run_task to be called from a different thread. - self.run_task(task=task) - return task.wait() + self.run_task(task=task) + return task.wait() - def submit_function(self, method, name=None, *args, **kwargs): + def submit_function( + self, method, name=None, run_async=False, *args, **kwargs): """ Allows submitting an arbitrary method for work. :param method: Callable to run in the TaskManager. :param str name: Name to use for the generated Task object. + :param bool run_async: Whether to run this task async or not. :param args: positional arguments to pass to the method when it runs. :param kwargs: keyword arguments to pass to the method when it runs. """ - task = Task(main=method, name=name, *args, **kwargs) + if run_async: + payload = functools.partial( + self.executor.submit, method, *args, **kwargs) + task = Task( + main=payload, name=name, + run_async=run_async) + else: + task = Task( + main=method, name=name, + *args, **kwargs) return self.submit_task(task) def submit_function_async(self, method, name=None, *args, **kwargs): @@ -156,30 +159,14 @@ def submit_function_async(self, method, name=None, *args, **kwargs): :param args: positional arguments to pass to the method when it runs. :param kwargs: keyword arguments to pass to the method when it runs. """ - task = Task(method=method, name=name, run_async=True, **kwargs) - return self.submit_task(task) + return self.submit_function( + method, name=name, run_async=True, *args, **kwargs) def pre_run_task(self, task): self._log.debug( "Manager %s running task %s", self.name, task.name) def run_task(self, task): - if task.run_async: - return self._run_task_async(task) - else: - return self._run_task(task) - - def post_run_task(self, elapsed_time, task): - self._log.debug( - "Manager %s ran task %s in %ss", - self.name, task.name, elapsed_time) - - def _run_task_async(self, task): - self._log.debug( - "Manager %s submitting task %s", self.name, task.name) - return self.executor.submit(self._run_task_wait, task) - - def _run_task(self, task): # Never call task.wait() in the run_task call stack because we # might be running in another thread. The exception-shifting # code is designed so that caller of submit_task (which may be @@ -192,13 +179,10 @@ def _run_task(self, task): dt = end - start self.post_run_task(dt, task) - def _run_task_wait(self, task): - # For async tasks, the action being performed is getting a - # future back from concurrent.futures.ThreadPoolExecutor. - # We do need to run the wait because the Future object is - # handling the exception shifting for us. - self._run_task(task) - return task.wait() + def post_run_task(self, elapsed_time, task): + self._log.debug( + "Manager %s ran task %s in %ss", + self.name, task.name, elapsed_time) def wait_for_futures(futures, raise_on_error=True, log=_log): diff --git a/openstack/tests/unit/cloud/test_task_manager.py b/openstack/tests/unit/cloud/test_task_manager.py index 6403d947b..269099400 100644 --- a/openstack/tests/unit/cloud/test_task_manager.py +++ b/openstack/tests/unit/cloud/test_task_manager.py @@ -62,14 +62,6 @@ def main(self): return set([1, 2]) -class TaskTestAsync(task_manager.Task): - def __init__(self): - super(TaskTestAsync, self).__init__(run_async=True) - - def main(self): - pass - - class TestTaskManager(base.TestCase): def setUp(self): @@ -107,7 +99,8 @@ def test_dont_munchify_set(self): @mock.patch.object(concurrent.futures.ThreadPoolExecutor, 'submit') def test_async(self, mock_submit): - self.manager.submit_task(TaskTestAsync()) + + self.manager.submit_function(set, run_async=True) self.assertTrue(mock_submit.called) From d70bdeb8656eb9a268b711c816ae64af3fe0539a Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Tue, 25 Sep 2018 10:57:55 -0300 Subject: [PATCH 2205/3836] Fix location region field in docs The docs said a location object had a 'region' field, however the field name is 'region_name'. https://github.com/openstack/openstacksdk/blob/4efbf96192805d336f3b013f37bb933b21dab2cc/openstack/cloud/openstackcloud.py#L716 Story: #2003871 Task: #26709 Change-Id: I0271a5a738e998032d67c2c5c6152c8583f41bd7 --- doc/source/user/model.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/user/model.rst b/doc/source/user/model.rst index ce857d315..62fa748ef 100644 --- a/doc/source/user/model.rst +++ b/doc/source/user/model.rst @@ -56,7 +56,7 @@ If all of the project information is None, then Location = dict( cloud=str(), - region=str(), + region_name=str(), zone=str() or None, project=dict( id=str() or None, From b5c96c5e44e43ca47e396cb4a9c8986fd684edf4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 23 Sep 2018 08:33:07 -0500 Subject: [PATCH 2206/3836] Restore timeout_scaling_factor In a previous refactor, we lost the part where we actually multiple TIMEOUT_SCALING_FACTOR to the timeout. Whoops. Also, turn off keepalive in the underlying session. For devstack functional tests, we're seeing a lot of "Resetting dropped connection" so I'm thinking we should just disable that. Also, increase the individual test timeout, and bump the scaling factor on trunk tests, which always seem to be timing out. Change-Id: Ie5b3eb319fed10cabfb2e818f3fcfd52b6fd7b14 --- openstack/tests/base.py | 12 +++++++++--- openstack/tests/functional/base.py | 7 +++++++ openstack/tests/functional/network/v2/test_trunk.py | 2 ++ tox.ini | 2 +- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/openstack/tests/base.py b/openstack/tests/base.py index 128ec9c8e..b0d87a6b3 100644 --- a/openstack/tests/base.py +++ b/openstack/tests/base.py @@ -41,9 +41,15 @@ def setUp(self): # Do this before super setUp so that we intercept the default value # in oslotest. TODO(mordred) Make the default timeout configurable # in oslotest. - self.useFixture( - fixtures.EnvironmentVariable( - 'OS_TEST_TIMEOUT', os.environ.get('OS_TEST_TIMEOUT', '5'))) + test_timeout = int(os.environ.get('OS_TEST_TIMEOUT', '5')) + try: + test_timeout = int(test_timeout * self.TIMEOUT_SCALING_FACTOR) + self.useFixture( + fixtures.EnvironmentVariable( + 'OS_TEST_TIMEOUT', str(test_timeout))) + except ValueError: + # Let oslotest do its thing + pass super(TestCase, self).setUp() diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index 7211c2c8d..b6f38c3f5 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -34,6 +34,10 @@ def _get_resource_value(resource_key, default): return default +def _disable_keep_alive(conn): + sess = conn.config.get_session() + sess.keep_alive = False + IMAGE_NAME = _get_resource_value('image_name', 'cirros-0.3.5-x86_64-disk') FLAVOR_NAME = _get_resource_value('flavor_name', 'm1.small') @@ -43,6 +47,7 @@ class BaseFunctionalTest(base.TestCase): def setUp(self): super(BaseFunctionalTest, self).setUp() self.conn = connection.Connection(config=TEST_CLOUD_REGION) + _disable_keep_alive(self.conn) self._demo_name = os.environ.get('OPENSTACKSDK_DEMO_CLOUD', 'devstack') self._op_name = os.environ.get( @@ -59,11 +64,13 @@ def _set_user_cloud(self, **kwargs): user_config = self.config.get_one( cloud=self._demo_name, **kwargs) self.user_cloud = connection.Connection(config=user_config) + _disable_keep_alive(self.user_cloud) def _set_operator_cloud(self, **kwargs): operator_config = self.config.get_one( cloud=self._op_name, **kwargs) self.operator_cloud = connection.Connection(config=operator_config) + _disable_keep_alive(self.operator_cloud) def pick_image(self): images = self.user_cloud.list_images() diff --git a/openstack/tests/functional/network/v2/test_trunk.py b/openstack/tests/functional/network/v2/test_trunk.py index 62b625998..0c20a6b20 100644 --- a/openstack/tests/functional/network/v2/test_trunk.py +++ b/openstack/tests/functional/network/v2/test_trunk.py @@ -19,6 +19,8 @@ class TestTrunk(base.BaseFunctionalTest): + TIMEOUT_SCALING_FACTOR = 2.0 + def setUp(self): super(TestTrunk, self).setUp() self.TRUNK_NAME = self.getUniqueString() diff --git a/tox.ini b/tox.ini index af3f6ba9f..b82013e7e 100644 --- a/tox.ini +++ b/tox.ini @@ -31,7 +31,7 @@ commands = stestr --test-path ./openstack/tests/examples run {posargs} basepython = {env:OPENSTACKSDK_TOX_PYTHON:python2} setenv = {[testenv]setenv} - OS_TEST_TIMEOUT=90 + OS_TEST_TIMEOUT=120 commands = stestr --test-path ./openstack/tests/functional/{env:OPENSTACKSDK_TESTS_SUBDIR:} run --serial {posargs} stestr slowest From 943f60679809144fb841894d1495240c3a751033 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Tue, 25 Sep 2018 18:57:09 -0300 Subject: [PATCH 2207/3836] Update vendor support info for catalyst The service URLs (via discovery [1]) for catalyst are as follow: identity: https://example.com:5000/v3/ compute: https://example.com:8774/v2/16ccee5e96ed466d86b23a5b7d95d377 image: https://example.com:9292/v2/ network: https://example.com:9696/v2.0 block-storage: https://example.com:8776/v3/16ccee5e96ed466d86b23a5b7d95d377 object-store: https://example.com:443/v1/AUTH_16ccee5e96ed466d86b23a5b7d95d377 [1] cloud.session.auth.get_endpoint_data(cloud.session, service).url Change-Id: If2a8c7a936c7bd8cdb4554a91ba25bb7a9b85459 --- doc/source/user/config/vendor-support.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/source/user/config/vendor-support.rst b/doc/source/user/config/vendor-support.rst index 07168445b..099e3aef5 100644 --- a/doc/source/user/config/vendor-support.rst +++ b/doc/source/user/config/vendor-support.rst @@ -67,9 +67,10 @@ nz-por-1 Porirua, NZ nz_wlg_2 Wellington, NZ ============== ================ -* Image API Version is 1 +* Identity API Version is 3 +* Compute API Version is 2 * Images must be in `raw` format -* Volume API Version is 1 +* Volume API Version is 3 citycloud --------- From 97bd7e4f7f227bc359a34473c143680185fd68fe Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Tue, 25 Sep 2018 19:08:30 -0300 Subject: [PATCH 2208/3836] Update vendor support info for ecs The service URLs (via discovery [1]) for ECS are as follow: identity: https://example.com/v2.0/ compute: https://example.com/v2/cb7088c9306742858e8a38b97744fba8 image: https://example.com/v2/ network: https://example.com/v2.0 block-storage: https://example.com/v2/cb7088c9306742858e8a38b97744fba8 object-store: https://example.com/v1/KEY_cb7088c9306742858e8a38b97744fba8 [1] cloud.session.auth.get_endpoint_data(cloud.session, service).url Change-Id: I6a176977f620ac5b55b2cc311f15f47f897ae42c --- doc/source/user/config/vendor-support.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/source/user/config/vendor-support.rst b/doc/source/user/config/vendor-support.rst index 099e3aef5..82550d348 100644 --- a/doc/source/user/config/vendor-support.rst +++ b/doc/source/user/config/vendor-support.rst @@ -180,8 +180,7 @@ it-mil1 Milan, IT de-fra1 Frankfurt, DE ============== ================ -* Image API Version is 1 -* Volume API Version is 1 +* Compute API Version is 2 fuga ---- From 12a4679145ac98b41a06d99d5f32bee9845aeb6b Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Tue, 25 Sep 2018 19:14:06 -0300 Subject: [PATCH 2209/3836] Update vendor support info for switchengines The service URLs (via discovery [1]) for switchengines are as follow: identity: https://example.com:5000/v3/ compute: https://example.com:8774/v2/7d04eab08a194304a8942217a8292be3 image: https://example.com:9292/v2/ network: https://example.com:9696/v2.0 block-storage: https://example.com:8776/v3/7d04eab08a194304a8942217a8292be3 object-store: https://example.com/swift/v1 Image creation was checked and upload via PUT is possible. [1] cloud.session.auth.get_endpoint_data(cloud.session, service).url Change-Id: I8a1c856e723c2fb6a6d1f907d51d2952e8b05b21 --- doc/source/user/config/vendor-support.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/source/user/config/vendor-support.rst b/doc/source/user/config/vendor-support.rst index 82550d348..3b5ddb9b0 100644 --- a/doc/source/user/config/vendor-support.rst +++ b/doc/source/user/config/vendor-support.rst @@ -294,9 +294,10 @@ LS Lausanne, CH ZH Zurich, CH ============== ================ +* Identity API Version is 3 +* Compute API Version is 2 * Images must be in `raw` format -* Images must be uploaded using the Glance Task Interface -* Volume API Version is 1 +* Volume API Version is 3 ultimum ------- From 23c6f3a3821fe0801258c4a1af0cc9a43395c2c7 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Tue, 25 Sep 2018 19:18:41 -0300 Subject: [PATCH 2210/3836] Update vendor support info for vexxhost The service URLs (via discovery [1]) for vexxhost are as follow: identity: https://example.com/v3/ compute: https://example.com/v2.1 image: https://example.com/v2/ network: https://example.com/v2.0/ block-storage: https://example.com/v3/70dadafa31184691b8f1a97c95bcfa1b object-store: https://example.com/v1/70dadafa31184691b8f1a97c95bcfa1b The new region sjc1 in Santa Clara, CA was added. [1] cloud.session.auth.get_endpoint_data(cloud.session, service).url Change-Id: I8ac3e5b33cc5de55526e4fa4e05065870316edc8 --- doc/source/user/config/vendor-support.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/source/user/config/vendor-support.rst b/doc/source/user/config/vendor-support.rst index 3b5ddb9b0..ecd070d11 100644 --- a/doc/source/user/config/vendor-support.rst +++ b/doc/source/user/config/vendor-support.rst @@ -337,10 +337,12 @@ http://auth.vexxhost.net Region Name Location ============== ================ ca-ymq-1 Montreal, QC +sjc1 Santa Clara, CA ============== ================ * DNS API Version is 1 * Identity API Version is 3 +* Volume API Version is 3 zetta ----- From 70d665b02bd93b7a99a88fac77e5b3f739cbe69e Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Tue, 25 Sep 2018 19:28:59 -0300 Subject: [PATCH 2211/3836] Add compute API info and fix provider names Sets the default Compute API to v2.1 and fix the provider names, as they appear on the internet (respecting uppercase letters). Change-Id: Ic5eb8b028fbbcdb08b0b7dee7695857321e89089 --- doc/source/user/config/vendor-support.rst | 49 ++++++++++++----------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/doc/source/user/config/vendor-support.rst b/doc/source/user/config/vendor-support.rst index ecd070d11..5469fe0e4 100644 --- a/doc/source/user/config/vendor-support.rst +++ b/doc/source/user/config/vendor-support.rst @@ -17,6 +17,7 @@ These are the default behaviors unless a cloud is configured differently. * Identity API Version is 2 * Image API Version is 2 * Volume API Version is 2 +* Compute API Version is 2.1 * Images must be in `qcow2` format * Images are uploaded using PUT interface * Public IPv4 is directly routable via DHCP from Neutron @@ -26,7 +27,7 @@ These are the default behaviors unless a cloud is configured differently. * Security groups are provided by Neutron * Vendor specific agents are not used -auro +AURO ---- https://api.auro.io:5000/v2.0 @@ -39,7 +40,7 @@ van1 Vancouver, BC * Public IPv4 is provided via NAT with Neutron Floating IP -betacloud +Betacloud --------- https://api-1.betacloud.io:5000 @@ -55,7 +56,7 @@ betacloud-1 Nuremberg, Germany * Public IPv4 is provided via NAT with Neutron Floating IP * Volume API Version is 3 -catalyst +Catalyst -------- https://api.cloud.catalyst.net.nz:5000/v2.0 @@ -72,8 +73,8 @@ nz_wlg_2 Wellington, NZ * Images must be in `raw` format * Volume API Version is 3 -citycloud ---------- +City Cloud +---------- https://identity1.citycloud.com:5000/v3/ @@ -92,7 +93,7 @@ Sto2 Stockholm, SE * Public IPv4 is provided via NAT with Neutron Floating IP * Volume API Version is 1 -conoha +ConoHa ------ https://identity.%(region_name)s.conoha.io @@ -107,7 +108,7 @@ sjc1 San Jose, CA * Image upload is not supported -dreamcompute +DreamCompute ------------ https://iad2.dream.io:5000 @@ -122,7 +123,7 @@ RegionOne Ashburn, VA * Images must be in `raw` format * IPv6 is provided to every server -dreamhost +DreamHost --------- Deprecated, please use dreamcompute @@ -139,8 +140,8 @@ RegionOne Ashburn, VA * Public IPv4 is provided via NAT with Neutron Floating IP * IPv6 is provided to every server -otc ---- +Open Telekom Cloud +------------------ https://iam.%(region_name)s.otc.t-systems.com/v3 @@ -154,7 +155,7 @@ eu-de Germany * Images must be in `vhd` format * Public IPv4 is provided via NAT with Neutron Floating IP -elastx +ELASTX ------ https://ops.elastx.net:5000/v2.0 @@ -167,8 +168,8 @@ regionOne Stockholm, SE * Public IPv4 is provided via NAT with Neutron Floating IP -entercloudsuite ---------------- +Enter Cloud Suite +----------------- https://api.entercloudsuite.com/v2.0 @@ -182,7 +183,7 @@ de-fra1 Frankfurt, DE * Compute API Version is 2 -fuga +Fuga ---- https://identity.api.fuga.io:5000 @@ -196,7 +197,7 @@ cystack Netherlands * Identity API Version is 3 * Volume API Version is 3 -internap +Internap -------- https://identity.api.cloud.iweb.com/v2.0 @@ -213,8 +214,8 @@ sjc01 San Jose, CA * Floating IPs are not supported -limestonenetworks ------------------ +Limestone Networks +------------------ https://auth.cloud.lstn.net:5000/v3 @@ -229,7 +230,7 @@ us-slc Salt Lake City, UT * Images must be in `raw` format * IPv6 is provided to every server connected to the `Public Internet` network -ovh +OVH --- https://auth.cloud.ovh.net/v2.0 @@ -245,7 +246,7 @@ GRA1 Gravelines, FR * Images may be in `raw` format. The `qcow2` default is also supported * Floating IPs are not supported -rackspace +Rackspace --------- https://identity.api.rackspacecloud.com/v2.0/ @@ -282,7 +283,7 @@ SYD Sydney, NSW api_key: myapikey auth_type: rackspace_apikey -switchengines +SWITCHengines ------------- https://keystone.cloud.switch.ch:5000/v2.0 @@ -299,7 +300,7 @@ ZH Zurich, CH * Images must be in `raw` format * Volume API Version is 3 -ultimum +Ultimum ------- https://console.ultimum-cloud.com:5000/v2.0 @@ -312,7 +313,7 @@ RegionOne Prague, CZ * Volume API Version is 1 -unitedstack +UnitedStack ----------- https://identity.api.ustack.com/v3 @@ -328,7 +329,7 @@ gd1 Guangdong, CN * Images must be in `raw` format * Volume API Version is 1 -vexxhost +VEXXHOST -------- http://auth.vexxhost.net @@ -344,7 +345,7 @@ sjc1 Santa Clara, CA * Identity API Version is 3 * Volume API Version is 3 -zetta +Zetta ----- https://identity.api.zetta.io/v3 From 3f49aa3ce607960318fb1fc41e07774b7acad389 Mon Sep 17 00:00:00 2001 From: Samuel de Medeiros Queiroz Date: Wed, 26 Sep 2018 18:03:51 -0300 Subject: [PATCH 2212/3836] Format URL when updating image props in Glance v1 With Glance v1, there is a formatting error when calling the service. Story: #2003884 Task: #26746 Change-Id: Ia676c399f3d026b6599fc795988d4ac2c45dafdf --- openstack/cloud/openstackcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 25bafb536..67d4547d0 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -5023,7 +5023,7 @@ def _update_image_properties_v1(self, image, meta, properties): if not img_props: return False self._image_client.put( - '/images/{id}'.format(image.id), headers=img_props) + '/images/{id}'.format(id=image.id), headers=img_props) self.list_images.invalidate(self) return True From 7721af136823af33b14a8d90cda9930abbcfb95a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 26 Sep 2018 17:27:33 -0500 Subject: [PATCH 2213/3836] Clarify error message is from nova We pass on a nova error message to the user when we have it - but sometimes hat error message looks like: Build of instance 7bbfc561-5a4a-4fc7-8f5c-02e60bc61511 aborted: Request to https://network.example.com/v2.0/ports.json timed out which can look like it was openstacksdk having issues talking to the ports api, when it was really nova. Add a few words to clarify. Change-Id: Ibd95c8d57f3e070760d0f98b2642e3cbf552c5b8 --- openstack/cloud/openstackcloud.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 25bafb536..91d803c75 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -6998,7 +6998,8 @@ def get_active_server( if server['status'] == 'ERROR': if 'fault' in server and 'message' in server['fault']: raise exc.OpenStackCloudException( - "Error in creating the server: {reason}".format( + "Error in creating the server." + " Compute service reports fault: {reason}".format( reason=server['fault']['message']), extra_data=dict(server=server)) From 6b274292f2175c5d39274d233e0993368eee6be8 Mon Sep 17 00:00:00 2001 From: Duc Truong Date: Mon, 1 Oct 2018 22:27:47 +0000 Subject: [PATCH 2214/3836] Add functional tests for clustering Change-Id: I1b68d64b6c0089a26665840131dcf8658faec73f --- .../tests/functional/clustering/__init__.py | 0 .../functional/clustering/test_cluster.py | 91 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 openstack/tests/functional/clustering/__init__.py create mode 100644 openstack/tests/functional/clustering/test_cluster.py diff --git a/openstack/tests/functional/clustering/__init__.py b/openstack/tests/functional/clustering/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/functional/clustering/test_cluster.py b/openstack/tests/functional/clustering/test_cluster.py new file mode 100644 index 000000000..a58e17840 --- /dev/null +++ b/openstack/tests/functional/clustering/test_cluster.py @@ -0,0 +1,91 @@ +# 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 time + +from openstack.clustering.v1 import cluster +from openstack.tests.functional import base +from openstack.tests.functional.network.v2 import test_network + + +class TestCluster(base.BaseFunctionalTest): + + def setUp(self): + super(TestCluster, self).setUp() + self.require_service('clustering') + + self.cidr = '10.99.99.0/16' + + self.network, self.subnet = test_network.create_network( + self.conn, + self.getUniqueString(), + self.cidr) + self.assertIsNotNone(self.network) + + profile_attrs = { + 'name': self.getUniqueString(), + 'spec': { + 'type': 'os.nova.server', + 'version': 1.0, + 'properties': { + 'name': self.getUniqueString(), + 'flavor': base.FLAVOR_NAME, + 'image': base.IMAGE_NAME, + 'networks': [{'network': self.network.id}] + }}} + + self.profile = self.conn.clustering.create_profile(**profile_attrs) + self.assertIsNotNone(self.profile) + + self.cluster_name = self.getUniqueString() + cluster_spec = { + "name": self.cluster_name, + "profile_id": self.profile.name, + "min_size": 0, + "max_size": -1, + "desired_capacity": 0, + } + + self.cluster = self.conn.clustering.create_cluster(**cluster_spec) + self.conn.clustering.wait_for_status(self.cluster, 'ACTIVE') + assert isinstance(self.cluster, cluster.Cluster) + + def tearDown(self): + self.conn.clustering.delete_cluster(self.cluster.id) + self.conn.clustering.wait_for_delete(self.cluster) + + test_network.delete_network(self.conn, self.network, self.subnet) + + self.conn.clustering.delete_profile(self.profile) + + super(TestCluster, self).tearDown() + + def test_find(self): + sot = self.conn.clustering.find_cluster(self.cluster.id) + self.assertEqual(self.cluster.id, sot.id) + + def test_get(self): + sot = self.conn.clustering.get_cluster(self.cluster) + self.assertEqual(self.cluster.id, sot.id) + + def test_list(self): + names = [o.name for o in self.conn.clustering.clusters()] + self.assertIn(self.cluster_name, names) + + def test_update(self): + new_cluster_name = self.getUniqueString() + sot = self.conn.clustering.update_cluster( + self.cluster, name=new_cluster_name, profile_only=False) + + time.sleep(2) + sot = self.conn.clustering.get_cluster(self.cluster) + self.assertEqual(new_cluster_name, sot.name) From 6befbbe76cc9afedcefb032de9d67b26ce04cd0d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 22 Sep 2018 06:18:26 -0500 Subject: [PATCH 2215/3836] Clean up python3 test and split networking into a job We want to run with python3 always with python2 being the exception. Update the functional tests to be python3 by default with a python2 job to check compat. Remove python3.5 jobs, since that's covered by python3.6 and the differences between 3.5 and 3.6 are covered by also having 2.7 in the gate. Similarly, make the tips jobs python 3.6 only because they are jobs that aim to ensure interactions between openstacksdk and keystoneauth and os-client-config and shade are correct at the unit test level. The individual unit tests should catch actual python 2.7 issues. Remove cover jobs because we historically haven't cared about the output and they take up test nodes. In general it's not that big of a deal, but we tend to be patch-heavy around here, so it feels extravagant to run a job we don't actually care about the output of. Split advanced networking services into a job We're hitting timeouts like crazy- maybe we're just running low on resources. Split octavia and designate into their own job. Turn swift off in that job. Skip sdk layer dns functional tests when designate is not present. We should refactor these to deal with both cases. Change-Id: Ica5a47cc200c8abff1d20af9883c5192fcbb95e1 --- .zuul.yaml | 100 +++++++++--------- .../functional/network/v2/test_floating_ip.py | 2 + tox.ini | 10 +- 3 files changed, 51 insertions(+), 61 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 9b0739afd..779e97f80 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -1,23 +1,8 @@ - job: - name: openstacksdk-tox-py27-tips - parent: openstack-tox-py27 + name: openstacksdk-tox-py36-tips + parent: openstack-tox-py36 description: | - Run tox python 27 unittests against master of important libs - vars: - tox_install_siblings: true - # openstacksdk in required-projects so that os-client-config - # and keystoneauth can add the job as well - required-projects: - - openstack-infra/shade - - openstack/keystoneauth - - openstack/openstacksdk - - openstack/os-client-config - -- job: - name: openstacksdk-tox-py35-tips - parent: openstack-tox-py35 - description: | - Run tox python 35 unittests against master of important libs + Run tox python 36 unittests against master of important libs vars: tox_install_siblings: true # openstacksdk in required-projects so that osc and keystoneauth @@ -32,12 +17,10 @@ name: openstacksdk-tox-tips check: jobs: - - openstacksdk-tox-py27-tips - - openstacksdk-tox-py35-tips + - openstacksdk-tox-py36-tips gate: jobs: - - openstacksdk-tox-py27-tips - - openstacksdk-tox-py35-tips + - openstacksdk-tox-py36-tips - job: name: openstacksdk-functional-devstack-minimum @@ -79,24 +62,14 @@ parent: openstacksdk-functional-devstack-minimum description: | Base job for devstack-based functional tests - required-projects: - - name: openstack/heat - - name: openstack/swift vars: + devstack_plugins: + neutron: https://git.openstack.org/openstack/neutron devstack_local_conf: post-config: $CINDER_CONF: DEFAULT: osapi_max_limit: 6 - devstack_services: - s-account: true - s-container: true - s-object: true - s-proxy: true - devstack_plugins: - heat: https://git.openstack.org/openstack/heat - tox_environment: - OPENSTACKSDK_HAS_HEAT: 1 - job: name: openstacksdk-functional-devstack-legacy @@ -120,11 +93,29 @@ description: | Run openstacksdk functional tests against a master devstack required-projects: - - openstack/designate - - openstack/octavia + - openstack/heat vars: devstack_localrc: DISABLE_AMP_IMAGE_BUILD: true + Q_SERVICE_PLUGIN_CLASSES: qos,trunk + devstack_plugins: + heat: https://git.openstack.org/openstack/heat + tox_environment: + OPENSTACKSDK_HAS_HEAT: 1 + devstack_services: + neutron-qos: true + neutron-trunk: true + +- job: + name: openstacksdk-functional-devstack-networking + parent: openstacksdk-functional-devstack + description: | + Run openstacksdk functional tests against a devstack with advanced + networking services enabled. + required-projects: + - openstack/designate + - openstack/octavia + vars: devstack_local_conf: post-config: $OCTAVIA_CONF: @@ -138,7 +129,6 @@ cert_manager: local_cert_manager devstack_plugins: designate: https://git.openstack.org/openstack/designate - neutron: https://git.openstack.org/openstack/neutron octavia: https://git.openstack.org/openstack/octavia devstack_services: designate: true @@ -148,20 +138,26 @@ o-hm: true o-hk: true neutron-dns: true - neutron-qos: true - neutron-trunk: true + s-account: false + s-container: false + s-object: false + s-proxy: false + h-eng: false + h-api: false + h-api-cfn: false tox_environment: OPENSTACKSDK_HAS_DESIGNATE: 1 - OPENSTACKSDK_HAS_OCTAVIA: 1 + OPENSTACKSDK_HAS_SWIFT: 0 + OPENSTACKSDK_HAS_HEAT: 0 - job: - name: openstacksdk-functional-devstack-python3 + name: openstacksdk-functional-devstack-python2 parent: openstacksdk-functional-devstack description: | - Run openstacksdk functional tests using python3 against a master devstack + Run openstacksdk functional tests using python2 against a master devstack vars: tox_environment: - OPENSTACKSDK_TOX_PYTHON: python3 + OPENSTACKSDK_TOX_PYTHON: python2 - job: name: openstacksdk-functional-devstack-tips @@ -178,14 +174,14 @@ tox_install_siblings: true - job: - name: openstacksdk-functional-devstack-tips-python3 + name: openstacksdk-functional-devstack-tips-python2 parent: openstacksdk-functional-devstack-tips description: | Run openstacksdk functional tests with tips of library dependencies using - python3 against a master devstack. + python2 against a master devstack. vars: tox_environment: - OPENSTACKSDK_TOX_PYTHON: python3 + OPENSTACKSDK_TOX_PYTHON: python2 - job: name: openstacksdk-functional-devstack-magnum @@ -342,19 +338,17 @@ check: jobs: - openstacksdk-functional-devstack-tips - - openstacksdk-functional-devstack-tips-python3 + - openstacksdk-functional-devstack-tips-python2 gate: jobs: - openstacksdk-functional-devstack-tips - - openstacksdk-functional-devstack-tips-python3 + - openstacksdk-functional-devstack-tips-python2 - project: templates: - check-requirements - - openstack-cover-jobs - openstack-lower-constraints-jobs - openstack-python-jobs - - openstack-python35-jobs - openstack-python36-jobs - openstacksdk-functional-tips - openstacksdk-tox-tips @@ -371,12 +365,13 @@ - openstacksdk-ansible-stable-2.6-functional-devstack: voting: false - openstacksdk-functional-devstack + - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-senlin - openstacksdk-functional-devstack-magnum: voting: false - openstacksdk-functional-devstack-ironic: voting: false - - openstacksdk-functional-devstack-python3 + - openstacksdk-functional-devstack-python2 - osc-functional-devstack-tips: voting: false - neutron-grenade @@ -385,7 +380,8 @@ gate: jobs: - openstacksdk-functional-devstack - - openstacksdk-functional-devstack-python3 + - openstacksdk-functional-devstack-python2 + - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-senlin - neutron-grenade - nodepool-functional-py35-src diff --git a/openstack/tests/functional/network/v2/test_floating_ip.py b/openstack/tests/functional/network/v2/test_floating_ip.py index 1a7d19273..69e6e9ed1 100644 --- a/openstack/tests/functional/network/v2/test_floating_ip.py +++ b/openstack/tests/functional/network/v2/test_floating_ip.py @@ -36,6 +36,8 @@ class TestFloatingIP(base.BaseFunctionalTest): def setUp(self): super(TestFloatingIP, self).setUp() + if not self.conn.has_service('dns'): + self.skipTest('dns service not supported by cloud') self.TIMEOUT_SCALING_FACTOR = 1.5 self.ROT_NAME = self.getUniqueString() self.EXT_NET_NAME = self.getUniqueString() diff --git a/tox.ini b/tox.ini index af3f6ba9f..2ed269a94 100644 --- a/tox.ini +++ b/tox.ini @@ -7,6 +7,7 @@ skipsdist = True usedevelop = True install_command = pip install {opts} {packages} passenv = OS_* OPENSTACKSDK_* +basepython = {env:OPENSTACKSDK_TOX_PYTHON:python3} setenv = VIRTUAL_ENV={envdir} LANG=en_US.UTF-8 @@ -23,12 +24,10 @@ commands = stestr run {posargs} stestr slowest [testenv:examples] -basepython = python3 commands = stestr --test-path ./openstack/tests/examples run {posargs} stestr slowest [testenv:functional] -basepython = {env:OPENSTACKSDK_TOX_PYTHON:python2} setenv = {[testenv]setenv} OS_TEST_TIMEOUT=90 @@ -36,7 +35,6 @@ commands = stestr --test-path ./openstack/tests/functional/{env:OPENSTACKSDK_TES stestr slowest [testenv:pep8] -basepython = python3 usedevelop = False skip_install = True deps = @@ -51,18 +49,15 @@ commands = flake8 [testenv:venv] -basepython = python3 commands = {posargs} [testenv:debug] -basepython = python3 whitelist_externals = find commands = find . -type f -name "*.pyc" -delete oslo_debug_helper {posargs} [testenv:cover] -basepython = python3 setenv = {[testenv]setenv} PYTHON=coverage run --source openstack --parallel-mode @@ -82,7 +77,6 @@ deps = commands = {toxinidir}/extras/run-ansible-tests.sh -e {envdir} {posargs} [testenv:docs] -basepython = python3 deps = -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} -r{toxinidir}/requirements.txt @@ -90,7 +84,6 @@ deps = commands = sphinx-build -W -d doc/build/doctrees -b html doc/source/ doc/build/html [testenv:releasenotes] -basepython = python3 usedevelop = False skip_install = True commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html @@ -112,7 +105,6 @@ exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build extensions = .rst, .yaml [testenv:lower-constraints] -basepython = python3 deps = -c{toxinidir}/lower-constraints.txt -r{toxinidir}/test-requirements.txt From 5a998529cce3341fa3ee1aba584e4ba39eb8c9dd Mon Sep 17 00:00:00 2001 From: wangweijia Date: Fri, 10 Aug 2018 11:24:26 +0800 Subject: [PATCH 2216/3836] Support firewall service for SDK FWaaS v2 API, add SDK API for firewall group, firewall policy and firewall rule. Implements blueprint: firewall-v2-support Change-Id: I2abdedb0e810cbd2278012c8a7cf56d14c7b705d Co-Authored-By: LIU Yulong Co-Authored-By: Matthias Lisin --- .zuul.yaml | 11 + openstack/network/v2/_proxy.py | 343 ++++++++++++++++++ openstack/network/v2/firewall_group.py | 60 +++ openstack/network/v2/firewall_policy.py | 97 +++++ openstack/network/v2/firewall_rule.py | 68 ++++ .../network/v2/test_firewall_group.py | 52 +++ .../network/v2/test_firewall_policy.py | 52 +++ .../network/v2/test_firewall_rule.py | 69 ++++ ...test_firewall_rule_insert_remove_policy.py | 91 +++++ .../unit/network/v2/test_firewall_group.py | 60 +++ .../unit/network/v2/test_firewall_policy.py | 52 +++ .../unit/network/v2/test_firewall_rule.py | 65 ++++ openstack/tests/unit/network/v2/test_proxy.py | 90 +++++ .../firewall-resources-c7589d288dd57e35.yaml | 5 + 14 files changed, 1115 insertions(+) create mode 100644 openstack/network/v2/firewall_group.py create mode 100644 openstack/network/v2/firewall_policy.py create mode 100644 openstack/network/v2/firewall_rule.py create mode 100644 openstack/tests/functional/network/v2/test_firewall_group.py create mode 100644 openstack/tests/functional/network/v2/test_firewall_policy.py create mode 100644 openstack/tests/functional/network/v2/test_firewall_rule.py create mode 100644 openstack/tests/functional/network/v2/test_firewall_rule_insert_remove_policy.py create mode 100644 openstack/tests/unit/network/v2/test_firewall_group.py create mode 100644 openstack/tests/unit/network/v2/test_firewall_policy.py create mode 100644 openstack/tests/unit/network/v2/test_firewall_rule.py create mode 100644 releasenotes/notes/firewall-resources-c7589d288dd57e35.yaml diff --git a/.zuul.yaml b/.zuul.yaml index 779e97f80..67a8847ad 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -114,6 +114,7 @@ networking services enabled. required-projects: - openstack/designate + - openstack/neutron-fwaas - openstack/octavia vars: devstack_local_conf: @@ -127,9 +128,18 @@ network_driver: network_noop_driver certificates: cert_manager: local_cert_manager + $NEUTRON_CONF: + fwaas: + agent_version: v2 + driver: iptables_v2 + enabled: true + firewall_l2_driver: ovs + devstack_localrc: + Q_SERVICE_PLUGIN_CLASSES: qos,trunk,firewall_v2 devstack_plugins: designate: https://git.openstack.org/openstack/designate octavia: https://git.openstack.org/openstack/octavia + neutron-fwaas: https://git.openstack.org/openstack/neutron-fwaas devstack_services: designate: true octavia: true @@ -138,6 +148,7 @@ o-hm: true o-hk: true neutron-dns: true + neutron-fwaas-v2: true s-account: false s-container: false s-object: false diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 24a0c9f1a..538968fd9 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -17,6 +17,9 @@ _auto_allocated_topology from openstack.network.v2 import availability_zone from openstack.network.v2 import extension +from openstack.network.v2 import firewall_group as _firewall_group +from openstack.network.v2 import firewall_policy as _firewall_policy +from openstack.network.v2 import firewall_rule as _firewall_rule from openstack.network.v2 import flavor as _flavor from openstack.network.v2 import floating_ip as _floating_ip from openstack.network.v2 import health_monitor as _health_monitor @@ -2434,6 +2437,346 @@ def remove_router_from_agent(self, agent, router): router = self._get_resource(_router.Router, router) return agent.remove_router_from_agent(self, router.id) + def create_firewall_group(self, **attrs): + """Create a new firewall group from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.network.v2.firewall_group.FirewallGroup`, + comprised of the properties on the FirewallGroup class. + + :returns: The results of firewall group creation + :rtype: :class:`~openstack.network.v2.firewall_group.FirewallGroup` + """ + return self._create(_firewall_group.FirewallGroup, **attrs) + + def delete_firewall_group(self, firewall_group, ignore_missing=True): + """Delete a firewall group + + :param firewall_group: + The value can be either the ID of a firewall group or a + :class:`~openstack.network.v2.firewall_group.FirewallGroup` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the firewall group does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent firewall group. + + :returns: ``None`` + """ + self._delete(_firewall_group.FirewallGroup, firewall_group, + ignore_missing=ignore_missing) + + def find_firewall_group(self, name_or_id, ignore_missing=True, **args): + """Find a single firewall group + + :param name_or_id: The name or ID of a firewall group. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. + :returns: One :class:`~openstack.network.v2.firewall_group. + FirewallGroup` or None + """ + return self._find(_firewall_group.FirewallGroup, + name_or_id, ignore_missing=ignore_missing, **args) + + def get_firewall_group(self, firewall_group): + """Get a single firewall group + + :param firewall_group: The value can be the ID of a firewall group or a + :class:`~openstack.network.v2.firewall_group.FirewallGroup` + instance. + + :returns: One + :class:`~openstack.network.v2.firewall_group.FirewallGroup` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_firewall_group.FirewallGroup, firewall_group) + + def firewall_groups(self, **query): + """Return a generator of firewall_groups + + :param dict query: Optional query parameters to be sent to limit + the resources being returned. Valid parameters are: + + * ``description``: Firewall group description + * ``egress_policy_id``: The ID of egress firewall policy + * ``ingress_policy_id``: The ID of ingress firewall policy + * ``name``: The name of a firewall group + * ``shared``: Indicates whether this firewall group is shared + across all projects. + * ``status``: The status of the firewall group. Valid values are + ACTIVE, INACTIVE, ERROR, PENDING_UPDATE, or + PENDING_DELETE. + * ``ports``: A list of the IDs of the ports associated with the + firewall group. + * ``project_id``: The ID of the project this firewall group is + associated with. + + :returns: A generator of firewall group objects + """ + return self._list(_firewall_group.FirewallGroup, + paginated=False, **query) + + def update_firewall_group(self, firewall_group, **attrs): + """Update a firewall group + + :param firewall_group: Either the id of a firewall group or a + :class:`~openstack.network.v2.firewall_group.FirewallGroup` + instance. + :param dict attrs: The attributes to update on the firewall group + represented by ``firewall_group``. + + :returns: The updated firewall group + :rtype: :class:`~openstack.network.v2.firewall_group.FirewallGroup` + """ + return self._update(_firewall_group.FirewallGroup, firewall_group, + **attrs) + + def create_firewall_policy(self, **attrs): + """Create a new firewall policy from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.network.v2.firewall_policy.FirewallPolicy`, + comprised of the properties on the FirewallPolicy class. + + :returns: The results of firewall policy creation + :rtype: :class:`~openstack.network.v2.firewall_policy.FirewallPolicy` + """ + return self._create(_firewall_policy.FirewallPolicy, **attrs) + + def delete_firewall_policy(self, firewall_policy, ignore_missing=True): + """Delete a firewall policy + + :param firewall_policy: + The value can be either the ID of a firewall policy or a + :class:`~openstack.network.v2.firewall_policy.FirewallPolicy` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the firewall policy does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent firewall policy. + + :returns: ``None`` + """ + self._delete(_firewall_policy.FirewallPolicy, firewall_policy, + ignore_missing=ignore_missing) + + def find_firewall_policy(self, name_or_id, ignore_missing=True, **args): + """Find a single firewall policy + + :param name_or_id: The name or ID of a firewall policy. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. + :returns: One :class:`~openstack.network.v2.firewall_policy. + FirewallPolicy` or None + """ + return self._find(_firewall_policy.FirewallPolicy, + name_or_id, ignore_missing=ignore_missing, **args) + + def get_firewall_policy(self, firewall_policy): + """Get a single firewall policy + + :param firewall_policy: The value can be the ID of a firewall policy + or a + :class:`~openstack.network.v2.firewall_policy.FirewallPolicy` + instance. + + :returns: One + :class:`~openstack.network.v2.firewall_policy.FirewallPolicy` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_firewall_policy.FirewallPolicy, firewall_policy) + + def firewall_policies(self, **query): + """Return a generator of firewall_policies + + :param dict query: Optional query parameters to be sent to limit + the resources being returned. Valid parameters are: + + * ``description``: Firewall policy description + * ``firewall_rule``: A list of the IDs of the firewall rules + associated with the firewall policy. + * ``name``: The name of a firewall policy + * ``shared``: Indicates whether this firewall policy is shared + across all projects. + * ``project_id``: The ID of the project that owns the resource. + + :returns: A generator of firewall policy objects + """ + return self._list(_firewall_policy.FirewallPolicy, + paginated=False, **query) + + def update_firewall_policy(self, firewall_policy, **attrs): + """Update a firewall policy + + :param firewall_policy: Either the id of a firewall policy or a + :class:`~openstack.network.v2.firewall_policy.FirewallPolicy` + instance. + :param dict attrs: The attributes to update on the firewall policy + represented by ``firewall_policy``. + + :returns: The updated firewall policy + :rtype: :class:`~openstack.network.v2.firewall_policy.FirewallPolicy` + """ + return self._update(_firewall_policy.FirewallPolicy, firewall_policy, + **attrs) + + def insert_rule_into_policy(self, firewall_policy_id, firewall_rule_id, + insert_after=None, insert_before=None): + """Insert a firewall_rule into a firewall_policy in order + + :param firewall_policy_id: The ID of the firewall policy. + :param firewall_rule_id: The ID of the firewall rule. + :param insert_after: The ID of the firewall rule to insert the new + rule after. It will be worked only when + insert_before is none. + :param insert_before: The ID of the firewall rule to insert the new + rule before. + + :returns: The updated firewall policy + :rtype: :class:`~openstack.network.v2.firewall_policy.FirewallPolicy` + """ + body = {'firewall_rule_id': firewall_rule_id, + 'insert_after': insert_after, + 'insert_before': insert_before} + policy = self._get_resource(_firewall_policy.FirewallPolicy, + firewall_policy_id) + return policy.insert_rule(self, **body) + + def remove_rule_from_policy(self, firewall_policy_id, firewall_rule_id): + """Remove a firewall_rule from a firewall_policy. + + :param firewall_policy_id: The ID of the firewall policy. + :param firewall_rule_id: The ID of the firewall rule. + + :returns: The updated firewall policy + :rtype: :class:`~openstack.network.v2.firewall_policy.FirewallPolicy` + """ + body = {'firewall_rule_id': firewall_rule_id} + policy = self._get_resource(_firewall_policy.FirewallPolicy, + firewall_policy_id) + return policy.remove_rule(self, **body) + + def create_firewall_rule(self, **attrs): + """Create a new firewall rule from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.network.v2.firewall_rule.FirewallRule`, + comprised of the properties on the FirewallRule class. + + :returns: The results of firewall rule creation + :rtype: :class:`~openstack.network.v2.firewall_rule.FirewallRule` + """ + return self._create(_firewall_rule.FirewallRule, **attrs) + + def delete_firewall_rule(self, firewall_rule, ignore_missing=True): + """Delete a firewall rule + + :param firewall_rule: + The value can be either the ID of a firewall rule or a + :class:`~openstack.network.v2.firewall_rule.FirewallRule` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the firewall rule does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent firewall rule. + + :returns: ``None`` + """ + self._delete(_firewall_rule.FirewallRule, firewall_rule, + ignore_missing=ignore_missing) + + def find_firewall_rule(self, name_or_id, ignore_missing=True, **args): + """Find a single firewall rule + + :param name_or_id: The name or ID of a firewall rule. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. + :returns: One :class:`~openstack.network.v2.firewall_rule. + FirewallRule` or None + """ + return self._find(_firewall_rule.FirewallRule, + name_or_id, ignore_missing=ignore_missing, **args) + + def get_firewall_rule(self, firewall_rule): + """Get a single firewall rule + + :param firewall_rule: The value can be the ID of a firewall rule or a + :class:`~openstack.network.v2.firewall_rule.FirewallRule` + instance. + + :returns: One + :class:`~openstack.network.v2.firewall_rule.FirewallRule` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_firewall_rule.FirewallRule, firewall_rule) + + def firewall_rules(self, **query): + """Return a generator of firewall_rules + + :param dict query: Optional query parameters to be sent to limit + the resources being returned. Valid parameters are: + + * ``action``: The action that the API performs on traffic that + matches the firewall rule. + * ``description``: Firewall rule description + * ``name``: The name of a firewall group + * ``destination_ip_address``: The destination IPv4 or IPv6 address + or CIDR for the firewall rule. + * ``destination_port``: The destination port or port range for + the firewall rule. + * ``enabled``: Facilitates selectively turning off rules. + * ``shared``: Indicates whether this firewall group is shared + across all projects. + * ``ip_version``: The IP protocol version for the firewall rule. + * ``protocol``: The IP protocol for the firewall rule. + * ``source_ip_address``: The source IPv4 or IPv6 address or CIDR + for the firewall rule. + * ``source_port``: The source port or port range for the firewall + rule. + * ``project_id``: The ID of the project this firewall group is + associated with. + + :returns: A generator of firewall rule objects + """ + return self._list(_firewall_rule.FirewallRule, + paginated=False, **query) + + def update_firewall_rule(self, firewall_rule, **attrs): + """Update a firewall rule + + :param firewall_rule: Either the id of a firewall rule or a + :class:`~openstack.network.v2.firewall_rule.FirewallRule` + instance. + :param dict attrs: The attributes to update on the firewall rule + represented by ``firewall_rule``. + + :returns: The updated firewall rule + :rtype: :class:`~openstack.network.v2.firewall_rule.FirewallRule` + """ + return self._update(_firewall_rule.FirewallRule, firewall_rule, + **attrs) + def create_security_group(self, **attrs): """Create a new security group from attributes diff --git a/openstack/network/v2/firewall_group.py b/openstack/network/v2/firewall_group.py new file mode 100644 index 000000000..aff20e747 --- /dev/null +++ b/openstack/network/v2/firewall_group.py @@ -0,0 +1,60 @@ +# Copyright (c) 2018 China Telecom Corporation +# 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 openstack.network import network_service +from openstack import resource + + +class FirewallGroup(resource.Resource): + resource_key = 'firewall_group' + resources_key = 'firewall_groups' + base_path = '/fwaas/firewall_groups' + service = network_service.NetworkService() + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + _query_mapping = resource.QueryParameters( + 'description', 'egress_firewall_policy_id', + 'ingress_firewall_policy_id', 'name', 'shared', 'status', 'ports', + 'project_id') + + # Properties + #: The administrative state of the firewall group, which is up (true) or + #: down (false). Default is true. + admin_state_up = resource.Body('admin_state_up') + #: The firewall group rule description. + description = resource.Body('description') + #: The ID of the egress firewall policy for the firewall group. + egress_firewall_policy_id = resource.Body('egress_firewall_policy_id') + #: The ID of the ingress firewall policy for the firewall group. + ingress_firewall_policy_id = resource.Body('ingress_firewall_policy_id') + #: The ID of the firewall group. + id = resource.Body('id') + #: The name of a firewall group + name = resource.Body('name') + #: A list of the IDs of the ports associated with the firewall group. + ports = resource.Body('ports') + #: The ID of the project that owns the resource. + project_id = resource.Body('project_id') + #: Indicates whether this firewall group is shared across all projects. + shared = resource.Body('shared') + #: The status of the firewall group. Valid values are ACTIVE, INACTIVE, + #: ERROR, PENDING_UPDATE, or PENDING_DELETE. + status = resource.Body('status') diff --git a/openstack/network/v2/firewall_policy.py b/openstack/network/v2/firewall_policy.py new file mode 100644 index 000000000..e94753027 --- /dev/null +++ b/openstack/network/v2/firewall_policy.py @@ -0,0 +1,97 @@ +# Copyright (c) 2018 China Telecom Corporation +# 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 openstack.exceptions import HttpException + +from openstack.network import network_service +from openstack import resource +from openstack import utils + + +class FirewallPolicy(resource.Resource): + resource_key = 'firewall_policy' + resources_key = 'firewall_policies' + base_path = '/fwaas/firewall_policies' + service = network_service.NetworkService() + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + _query_mapping = resource.QueryParameters( + 'description', 'firewall_rules', 'name', 'project_id', 'shared') + + # Properties + #: Each time that the firewall policy or its associated rules are changed, + #: the API sets this attribute to false. To audit the policy, + #: explicitly set this attribute to true. + audited = resource.Body('audited') + #: The firewall group rule description. + description = resource.Body('description') + #: The ID of the firewall policy. + id = resource.Body('id') + #: A list of the IDs of the firewall rules associated with the + #: firewall policy. + firewall_rules = resource.Body('firewall_rules') + #: The name of a firewall policy + name = resource.Body('name') + #: The ID of the project that owns the resource. + project_id = resource.Body('project_id') + #: Set to true to make this firewall policy visible to other projects. + shared = resource.Body('shared') + + def insert_rule(self, session, **body): + """Insert a firewall_rule into a firewall_policy in order. + + :param session: The session to communicate through. + :type session: :class:`~openstack.session.Session` + :param dict body: The body requested to be updated on the router + + :returns: The updated firewall policy + :rtype: :class:`~openstack.network.v2.firewall_policy.FirewallPolicy` + + :raises: :class:`~openstack.exceptions.HttpException` on error. + """ + url = utils.urljoin(self.base_path, self.id, 'insert_rule') + return self._put_request(session, url, body) + + def remove_rule(self, session, **body): + """Remove a firewall_rule from a firewall_policy. + + :param session: The session to communicate through. + :type session: :class:`~openstack.session.Session` + :param dict body: The body requested to be updated on the router + + :returns: The updated firewall policy + :rtype: :class:`~openstack.network.v2.firewall_policy.FirewallPolicy` + + :raises: :class:`~openstack.exceptions.HttpException` on error. + """ + url = utils.urljoin(self.base_path, self.id, 'remove_rule') + return self._put_request(session, url, body) + + def _put_request(self, session, url, json_data): + resp = session.put(url, json=json_data) + data = resp.json() + if not resp.ok: + message = None + if 'NeutronError' in data: + message = data['NeutronError']['message'] + raise HttpException(message=message, response=resp) + + self._body.attributes.update(data) + return self diff --git a/openstack/network/v2/firewall_rule.py b/openstack/network/v2/firewall_rule.py new file mode 100644 index 000000000..6f4e537bb --- /dev/null +++ b/openstack/network/v2/firewall_rule.py @@ -0,0 +1,68 @@ +# Copyright (c) 2018 China Telecom Corporation +# 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 openstack.network import network_service +from openstack import resource + + +class FirewallRule(resource.Resource): + resource_key = 'firewall_rule' + resources_key = 'firewall_rules' + base_path = '/fwaas/firewall_rules' + service = network_service.NetworkService() + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + _query_mapping = resource.QueryParameters( + 'action', 'description', 'destination_ip_address', 'name', + 'destination_port', 'enabled', 'ip_version', 'project_id', 'protocol', + 'shared', 'source_ip_address', 'source_port', 'firewall_policy_id') + + # Properties + #: The action that the API performs on traffic that matches the firewall + #: rule. Valid values are allow or deny. Default is deny. + action = resource.Body('action') + #: The description of the firewall rule + description = resource.Body('description') + #: The destination IPv4 or IPv6 address or CIDR for the firewall rule. + destination_ip_address = resource.Body('destination_ip_address') + #: The destination port or port range for the firewall rule. + destination_port = resource.Body('destination_port') + #: Facilitates selectively turning off rules without having to disassociate + #: the rule from the firewall policy + enabled = resource.Body('enabled') + #: The IP protocol version for the firewall rule. Valid values are 4 or 6. + ip_version = resource.Body('ip_version') + #: The name of the firewall rule. + name = resource.Body('name') + #: The ID of the project that owns the resource. + project_id = resource.Body('project_id') + #: The IP protocol for the firewall rule. + protocol = resource.Body('protocol') + #: Indicates whether this firewall rule is shared across all projects. + shared = resource.Body('shared') + #: The source IPv4 or IPv6 address or CIDR for the firewall rule. + source_ip_address = resource.Body('source_ip_address') + #: The source port or port range for the firewall rule. + source_port = resource.Body('source_port') + #: The ID of the firewall policy. + firewall_policy_id = resource.Body('firewall_policy_id') + #: The ID of the firewall rule. + id = resource.Body('id') diff --git a/openstack/tests/functional/network/v2/test_firewall_group.py b/openstack/tests/functional/network/v2/test_firewall_group.py new file mode 100644 index 000000000..7e19f82ab --- /dev/null +++ b/openstack/tests/functional/network/v2/test_firewall_group.py @@ -0,0 +1,52 @@ +# Copyright (c) 2018 China Telecom Corporation +# 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 openstack.network.v2 import firewall_group +from openstack.tests.functional import base + + +class TestFirewallGroup(base.BaseFunctionalTest): + + ID = None + + def setUp(self): + super(TestFirewallGroup, self).setUp() + if not self.conn._has_neutron_extension('fwaas_v2'): + self.skipTest('fwaas_v2 service not supported by cloud') + self.NAME = self.getUniqueString() + sot = self.conn.network.create_firewall_group(name=self.NAME) + assert isinstance(sot, firewall_group.FirewallGroup) + self.assertEqual(self.NAME, sot.name) + self.ID = sot.id + + def tearDown(self): + sot = self.conn.network.delete_firewall_group(self.ID, + ignore_missing=False) + self.assertIs(None, sot) + super(TestFirewallGroup, self).tearDown() + + def test_find(self): + sot = self.conn.network.find_firewall_group(self.NAME) + self.assertEqual(self.ID, sot.id) + + def test_get(self): + sot = self.conn.network.get_firewall_group(self.ID) + self.assertEqual(self.NAME, sot.name) + self.assertEqual(self.ID, sot.id) + + def test_list(self): + names = [o.name for o in self.conn.network.firewall_groups()] + self.assertIn(self.NAME, names) diff --git a/openstack/tests/functional/network/v2/test_firewall_policy.py b/openstack/tests/functional/network/v2/test_firewall_policy.py new file mode 100644 index 000000000..e0409cd8e --- /dev/null +++ b/openstack/tests/functional/network/v2/test_firewall_policy.py @@ -0,0 +1,52 @@ +# Copyright (c) 2018 China Telecom Corporation +# 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 openstack.network.v2 import firewall_policy +from openstack.tests.functional import base + + +class TestFirewallPolicy(base.BaseFunctionalTest): + + ID = None + + def setUp(self): + super(TestFirewallPolicy, self).setUp() + if not self.conn._has_neutron_extension('fwaas_v2'): + self.skipTest('fwaas_v2 service not supported by cloud') + self.NAME = self.getUniqueString() + sot = self.conn.network.create_firewall_policy(name=self.NAME) + assert isinstance(sot, firewall_policy.FirewallPolicy) + self.assertEqual(self.NAME, sot.name) + self.ID = sot.id + + def tearDown(self): + sot = self.conn.network.delete_firewall_policy(self.ID, + ignore_missing=False) + self.assertIs(None, sot) + super(TestFirewallPolicy, self).tearDown() + + def test_find(self): + sot = self.conn.network.find_firewall_policy(self.NAME) + self.assertEqual(self.ID, sot.id) + + def test_get(self): + sot = self.conn.network.get_firewall_policy(self.ID) + self.assertEqual(self.NAME, sot.name) + self.assertEqual(self.ID, sot.id) + + def test_list(self): + names = [o.name for o in self.conn.network.firewall_policies()] + self.assertIn(self.NAME, names) diff --git a/openstack/tests/functional/network/v2/test_firewall_rule.py b/openstack/tests/functional/network/v2/test_firewall_rule.py new file mode 100644 index 000000000..b8c1cbca2 --- /dev/null +++ b/openstack/tests/functional/network/v2/test_firewall_rule.py @@ -0,0 +1,69 @@ +# Copyright (c) 2018 China Telecom Corporation +# 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 openstack.network.v2 import firewall_rule +from openstack.tests.functional import base + + +class TestFirewallRule(base.BaseFunctionalTest): + + ACTION = 'allow' + DEST_IP = '10.0.0.0/24' + DEST_PORT = '80' + IP_VERSION = 4 + PROTOCOL = 'tcp' + SOUR_IP = '10.0.1.0/24' + SOUR_PORT = '8000' + ID = None + + def setUp(self): + super(TestFirewallRule, self).setUp() + if not self.conn._has_neutron_extension('fwaas_v2'): + self.skipTest('fwaas_v2 service not supported by cloud') + self.NAME = self.getUniqueString() + sot = self.conn.network.create_firewall_rule( + name=self.NAME, action=self.ACTION, source_port=self.SOUR_PORT, + destination_port=self.DEST_PORT, source_ip_address=self.SOUR_IP, + destination_ip_address=self.DEST_IP, ip_version=self.IP_VERSION, + protocol=self.PROTOCOL) + assert isinstance(sot, firewall_rule.FirewallRule) + self.assertEqual(self.NAME, sot.name) + self.ID = sot.id + + def tearDown(self): + sot = self.conn.network.delete_firewall_rule(self.ID, + ignore_missing=False) + self.assertIs(None, sot) + super(TestFirewallRule, self).tearDown() + + def test_find(self): + sot = self.conn.network.find_firewall_rule(self.NAME) + self.assertEqual(self.ID, sot.id) + + def test_get(self): + sot = self.conn.network.get_firewall_rule(self.ID) + self.assertEqual(self.ID, sot.id) + self.assertEqual(self.NAME, sot.name) + self.assertEqual(self.ACTION, sot.action) + self.assertEqual(self.DEST_IP, sot.destination_ip_address) + self.assertEqual(self.DEST_PORT, sot.destination_port) + self.assertEqual(self.IP_VERSION, sot.ip_version) + self.assertEqual(self.SOUR_IP, sot.source_ip_address) + self.assertEqual(self.SOUR_PORT, sot.source_port) + + def test_list(self): + ids = [o.id for o in self.conn.network.firewall_rules()] + self.assertIn(self.ID, ids) diff --git a/openstack/tests/functional/network/v2/test_firewall_rule_insert_remove_policy.py b/openstack/tests/functional/network/v2/test_firewall_rule_insert_remove_policy.py new file mode 100644 index 000000000..1dc7f9788 --- /dev/null +++ b/openstack/tests/functional/network/v2/test_firewall_rule_insert_remove_policy.py @@ -0,0 +1,91 @@ +# Copyright (c) 2018 China Telecom Corporation +# 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 uuid + +from openstack.network.v2 import firewall_policy +from openstack.network.v2 import firewall_rule +from openstack.tests.functional import base + + +class TestFirewallPolicyRuleAssociations(base.BaseFunctionalTest): + + POLICY_NAME = uuid.uuid4().hex + RULE1_NAME = uuid.uuid4().hex + RULE2_NAME = uuid.uuid4().hex + POLICY_ID = None + RULE1_ID = None + RULE2_ID = None + + def setUp(self): + super(TestFirewallPolicyRuleAssociations, self).setUp() + if not self.conn._has_neutron_extension('fwaas_v2'): + self.skipTest('fwaas_v2 service not supported by cloud') + rul1 = self.conn.network.create_firewall_rule(name=self.RULE1_NAME) + assert isinstance(rul1, firewall_rule.FirewallRule) + self.assertEqual(self.RULE1_NAME, rul1.name) + rul2 = self.conn.network.create_firewall_rule(name=self.RULE2_NAME) + assert isinstance(rul2, firewall_rule.FirewallRule) + self.assertEqual(self.RULE2_NAME, rul2.name) + pol = self.conn.network.create_firewall_policy(name=self.POLICY_NAME) + assert isinstance(pol, firewall_policy.FirewallPolicy) + self.assertEqual(self.POLICY_NAME, pol.name) + self.RULE1_ID = rul1.id + self.RULE2_ID = rul2.id + self.POLICY_ID = pol.id + + def tearDown(self): + sot = self.conn.network.delete_firewall_policy(self.POLICY_ID, + ignore_missing=False) + self.assertIs(None, sot) + sot = self.conn.network.delete_firewall_rule(self.RULE1_ID, + ignore_missing=False) + self.assertIs(None, sot) + sot = self.conn.network.delete_firewall_rule(self.RULE2_ID, + ignore_missing=False) + self.assertIs(None, sot) + super(TestFirewallPolicyRuleAssociations, self).tearDown() + + def test_insert_rule_into_policy(self): + policy = self.conn.network.insert_rule_into_policy( + self.POLICY_ID, + firewall_rule_id=self.RULE1_ID) + self.assertIn(self.RULE1_ID, policy['firewall_rules']) + policy = self.conn.network.insert_rule_into_policy( + self.POLICY_ID, + firewall_rule_id=self.RULE2_ID, + insert_before=self.RULE1_ID) + self.assertEqual(self.RULE1_ID, policy['firewall_rules'][1]) + self.assertEqual(self.RULE2_ID, policy['firewall_rules'][0]) + + def test_remove_rule_from_policy(self): + # insert rules into policy before we remove it again + policy = self.conn.network.insert_rule_into_policy( + self.POLICY_ID, firewall_rule_id=self.RULE1_ID) + self.assertIn(self.RULE1_ID, policy['firewall_rules']) + + policy = self.conn.network.insert_rule_into_policy( + self.POLICY_ID, firewall_rule_id=self.RULE2_ID) + self.assertIn(self.RULE2_ID, policy['firewall_rules']) + + policy = self.conn.network.remove_rule_from_policy( + self.POLICY_ID, + firewall_rule_id=self.RULE1_ID) + self.assertNotIn(self.RULE1_ID, policy['firewall_rules']) + + policy = self.conn.network.remove_rule_from_policy( + self.POLICY_ID, + firewall_rule_id=self.RULE2_ID) + self.assertNotIn(self.RULE2_ID, policy['firewall_rules']) diff --git a/openstack/tests/unit/network/v2/test_firewall_group.py b/openstack/tests/unit/network/v2/test_firewall_group.py new file mode 100644 index 000000000..a9c7925b4 --- /dev/null +++ b/openstack/tests/unit/network/v2/test_firewall_group.py @@ -0,0 +1,60 @@ +# Copyright (c) 2018 China Telecom Corporation +# 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 testtools + +from openstack.network.v2 import firewall_group + +IDENTIFIER = 'IDENTIFIER' + +EXAMPLE = { + 'description': '1', + 'name': '2', + 'egress_firewall_policy_id': '3', + 'ingress_firewall_policy_id': '4', + 'shared': True, + 'status': 'ACTIVE', + 'ports': ['5', '6'], + 'project_id': '7', +} + + +class TestFirewallGroup(testtools.TestCase): + + def test_basic(self): + sot = firewall_group.FirewallGroup() + self.assertEqual('firewall_group', sot.resource_key) + self.assertEqual('firewall_groups', sot.resources_key) + self.assertEqual('/fwaas/firewall_groups', sot.base_path) + self.assertEqual('network', sot.service.service_type) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = firewall_group.FirewallGroup(**EXAMPLE) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['egress_firewall_policy_id'], + sot.egress_firewall_policy_id) + self.assertEqual(EXAMPLE['ingress_firewall_policy_id'], + sot.ingress_firewall_policy_id) + self.assertEqual(EXAMPLE['shared'], sot.shared) + self.assertEqual(EXAMPLE['status'], sot.status) + self.assertEqual(list, type(sot.ports)) + self.assertEqual(EXAMPLE['ports'], sot.ports) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) diff --git a/openstack/tests/unit/network/v2/test_firewall_policy.py b/openstack/tests/unit/network/v2/test_firewall_policy.py new file mode 100644 index 000000000..f29ec1cce --- /dev/null +++ b/openstack/tests/unit/network/v2/test_firewall_policy.py @@ -0,0 +1,52 @@ +# Copyright (c) 2018 China Telecom Corporation +# 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 testtools + +from openstack.network.v2 import firewall_policy + + +EXAMPLE = { + 'description': '1', + 'name': '2', + 'firewall_rules': ['a30b0ec2-a468-4b1c-8dbf-928ded2a57a8', + '8d562e98-24f3-46e1-bbf3-d9347c0a67ee'], + 'shared': True, + 'project_id': '4', +} + + +class TestFirewallPolicy(testtools.TestCase): + + def test_basic(self): + sot = firewall_policy.FirewallPolicy() + self.assertEqual('firewall_policy', sot.resource_key) + self.assertEqual('firewall_policies', sot.resources_key) + self.assertEqual('/fwaas/firewall_policies', sot.base_path) + self.assertEqual('network', sot.service.service_type) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = firewall_policy.FirewallPolicy(**EXAMPLE) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['firewall_rules'], sot.firewall_rules) + self.assertEqual(EXAMPLE['shared'], sot.shared) + self.assertEqual(list, type(sot.firewall_rules)) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) diff --git a/openstack/tests/unit/network/v2/test_firewall_rule.py b/openstack/tests/unit/network/v2/test_firewall_rule.py new file mode 100644 index 000000000..8865a0ff3 --- /dev/null +++ b/openstack/tests/unit/network/v2/test_firewall_rule.py @@ -0,0 +1,65 @@ +# Copyright (c) 2018 China Telecom Corporation +# 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 testtools + +from openstack.network.v2 import firewall_rule + +EXAMPLE = { + 'action': 'allow', + 'description': '1', + 'destination_ip_address': '10.0.0.2/24', + 'destination_port': '2', + 'name': '3', + 'enabled': True, + 'ip_version': 4, + 'protocol': 'tcp', + 'shared': True, + 'source_ip_address': '10.0.1.2/24', + 'source_port': '5', + 'project_id': '6', +} + + +class TestFirewallRule(testtools.TestCase): + + def test_basic(self): + sot = firewall_rule.FirewallRule() + self.assertEqual('firewall_rule', sot.resource_key) + self.assertEqual('firewall_rules', sot.resources_key) + self.assertEqual('/fwaas/firewall_rules', sot.base_path) + self.assertEqual('network', sot.service.service_type) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = firewall_rule.FirewallRule(**EXAMPLE) + self.assertEqual(EXAMPLE['action'], sot.action) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['destination_ip_address'], + sot.destination_ip_address) + self.assertEqual(EXAMPLE['destination_port'], sot.destination_port) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['enabled'], sot.enabled) + self.assertEqual(EXAMPLE['ip_version'], sot.ip_version) + self.assertEqual(EXAMPLE['protocol'], sot.protocol) + self.assertEqual(EXAMPLE['shared'], sot.shared) + self.assertEqual(EXAMPLE['source_ip_address'], + sot.source_ip_address) + self.assertEqual(EXAMPLE['source_port'], sot.source_port) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index e71bf0c2d..600596f12 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -21,6 +21,9 @@ from openstack.network.v2 import auto_allocated_topology from openstack.network.v2 import availability_zone from openstack.network.v2 import extension +from openstack.network.v2 import firewall_group +from openstack.network.v2 import firewall_policy +from openstack.network.v2 import firewall_rule from openstack.network.v2 import flavor from openstack.network.v2 import floating_ip from openstack.network.v2 import health_monitor @@ -873,6 +876,93 @@ def test_agent_hosted_routers_list(self): expected_kwargs={'agent_id': AGENT_ID}, ) + def test_firewall_group_create_attrs(self): + self.verify_create(self.proxy.create_firewall_group, + firewall_group.FirewallGroup) + + def test_firewall_group_delete(self): + self.verify_delete(self.proxy.delete_firewall_group, + firewall_group.FirewallGroup, False) + + def test_firewall_group_delete_ignore(self): + self.verify_delete(self.proxy.delete_firewall_group, + firewall_group.FirewallGroup, True) + + def test_firewall_group_find(self): + self.verify_find(self.proxy.find_firewall_group, + firewall_group.FirewallGroup) + + def test_firewall_group_get(self): + self.verify_get(self.proxy.get_firewall_group, + firewall_group.FirewallGroup) + + def test_firewall_groups(self): + self.verify_list(self.proxy.firewall_groups, + firewall_group.FirewallGroup, + paginated=False) + + def test_firewall_group_update(self): + self.verify_update(self.proxy.update_firewall_group, + firewall_group.FirewallGroup) + + def test_firewall_policy_create_attrs(self): + self.verify_create(self.proxy.create_firewall_policy, + firewall_policy.FirewallPolicy) + + def test_firewall_policy_delete(self): + self.verify_delete(self.proxy.delete_firewall_policy, + firewall_policy.FirewallPolicy, False) + + def test_firewall_policy_delete_ignore(self): + self.verify_delete(self.proxy.delete_firewall_policy, + firewall_policy.FirewallPolicy, True) + + def test_firewall_policy_find(self): + self.verify_find(self.proxy.find_firewall_policy, + firewall_policy.FirewallPolicy) + + def test_firewall_policy_get(self): + self.verify_get(self.proxy.get_firewall_policy, + firewall_policy.FirewallPolicy) + + def test_firewall_policies(self): + self.verify_list(self.proxy.firewall_policies, + firewall_policy.FirewallPolicy, + paginated=False) + + def test_firewall_policy_update(self): + self.verify_update(self.proxy.update_firewall_policy, + firewall_policy.FirewallPolicy) + + def test_firewall_rule_create_attrs(self): + self.verify_create(self.proxy.create_firewall_rule, + firewall_rule.FirewallRule) + + def test_firewall_rule_delete(self): + self.verify_delete(self.proxy.delete_firewall_rule, + firewall_rule.FirewallRule, False) + + def test_firewall_rule_delete_ignore(self): + self.verify_delete(self.proxy.delete_firewall_rule, + firewall_rule.FirewallRule, True) + + def test_firewall_rule_find(self): + self.verify_find(self.proxy.find_firewall_rule, + firewall_rule.FirewallRule) + + def test_firewall_rule_get(self): + self.verify_get(self.proxy.get_firewall_rule, + firewall_rule.FirewallRule) + + def test_firewall_rules(self): + self.verify_list(self.proxy.firewall_rules, + firewall_rule.FirewallRule, + paginated=False) + + def test_firewall_rule_update(self): + self.verify_update(self.proxy.update_firewall_rule, + firewall_rule.FirewallRule) + def test_security_group_create_attrs(self): self.verify_create(self.proxy.create_security_group, security_group.SecurityGroup) diff --git a/releasenotes/notes/firewall-resources-c7589d288dd57e35.yaml b/releasenotes/notes/firewall-resources-c7589d288dd57e35.yaml new file mode 100644 index 000000000..92e4a09bd --- /dev/null +++ b/releasenotes/notes/firewall-resources-c7589d288dd57e35.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Implement fwaas v2 resources for managing firewall groups, rules + and policies. From a030d82e509f14e3bd2177b01f36115773712917 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 21 Sep 2018 09:17:41 -0500 Subject: [PATCH 2217/3836] Remove profile profile has been deprecated for a couple of releases now and its use has been removed from python-openstackclient and python-senlinclient. Remove it in anticipation of a 1.0 release. Change-Id: I2005b05eeed4f84ee8fc8745c13f8d68a0da0ace --- openstack/connection.py | 19 +- openstack/profile.py | 205 ------------------ openstack/tests/unit/test_connection.py | 16 -- .../removed-profile-b033d870937868a1.yaml | 5 + 4 files changed, 6 insertions(+), 239 deletions(-) delete mode 100644 openstack/profile.py create mode 100644 releasenotes/notes/removed-profile-b033d870937868a1.yaml diff --git a/openstack/connection.py b/openstack/connection.py index 4a9f4e135..3459d3a86 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -216,9 +216,6 @@ class Connection(six.with_metaclass(_meta.ConnectionMeta, def __init__(self, cloud=None, config=None, session=None, app_name=None, app_version=None, - # TODO(shade) Remove these once we've shifted - # python-openstackclient to not use the profile interface. - authenticator=None, profile=None, extra_services=None, strict=False, use_direct_get=False, @@ -252,14 +249,6 @@ def __init__(self, cloud=None, config=None, session=None, :param str app_name: Name of the application to be added to User Agent. :param str app_version: Version of the application to be added to User Agent. - :param authenticator: DEPRECATED. Only exists for short-term backwards - compatibility for python-openstackclient while we - transition. See :doc:`transition_from_profile` - for details. - :param profile: DEPRECATED. Only exists for short-term backwards - compatibility for python-openstackclient while we - transition. See :doc:`transition_from_profile` - for details. :param extra_services: List of :class:`~openstack.service_description.ServiceDescription` objects describing services that openstacksdk otherwise does not @@ -284,13 +273,7 @@ def __init__(self, cloud=None, config=None, session=None, self._extra_services[service.service_type] = service if not self.config: - if profile: - import openstack.profile - # TODO(shade) Remove this once we've shifted - # python-openstackclient to not use the profile interface. - self.config = openstack.profile._get_config_from_profile( - profile, authenticator, **kwargs) - elif session: + if session: self.config = cloud_region.from_session( session=session, app_name=app_name, app_version=app_version, diff --git a/openstack/profile.py b/openstack/profile.py deleted file mode 100644 index 26d3b5661..000000000 --- a/openstack/profile.py +++ /dev/null @@ -1,205 +0,0 @@ -# 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. - -""" -:class:`~openstack.profile.Profile` is deprecated. Code should use -:class:`~openstack.config.cloud_region.CloudRegion` instead. -""" - -import copy - -from openstack import _log -from openstack.config import cloud_region -from openstack.config import defaults as config_defaults -from openstack.baremetal import baremetal_service -from openstack.block_storage import block_storage_service -from openstack.clustering import clustering_service -from openstack.compute import compute_service -from openstack.database import database_service -from openstack import exceptions -from openstack.identity import identity_service -from openstack.image import image_service -from openstack.key_manager import key_manager_service -from openstack.load_balancer import load_balancer_service as lb_service -from openstack.message import message_service -from openstack.network import network_service -from openstack.object_store import object_store_service -from openstack.orchestration import orchestration_service -from openstack import utils -from openstack.workflow import workflow_service - -_logger = _log.setup_logging('openstack') - - -def _get_config_from_profile(profile, authenticator, **kwargs): - # TODO(shade) Remove this once we've shifted python-openstackclient - # to not use the profile interface. - - region_name = None - for service in profile.get_services(): - if service.region: - region_name = service.region - service_type = service.service_type - if service.interface: - key = cloud_region._make_key('interface', service_type) - kwargs[key] = service.interface - if service.version: - version = service.version - if version.startswith('v'): - version = version[1:] - key = cloud_region._make_key('api_version', service_type) - kwargs[key] = version - if service.api_version: - version = service.api_version - key = cloud_region._make_key('default_microversion', service_type) - kwargs[key] = version - - config_kwargs = config_defaults.get_defaults() - config_kwargs.update(kwargs) - config = cloud_region.CloudRegion( - region_name=region_name, config=config_kwargs) - config._auth = authenticator - return config - - -class Profile(object): - - ALL = "*" - """Wildcard service identifier representing all services.""" - - @utils.deprecated(deprecated_in="0.10.0", removed_in="1.0", - details="Use openstack.config instead") - def __init__(self, plugins=None): - """User preference for each service. - - :param plugins: List of entry point namespaces to load. - - Create a new :class:`~openstack.profile.Profile` - object with no preferences defined, but knowledge of the services. - Services are identified by their service type, e.g.: 'identity', - 'compute', etc. - """ - self._services = {} - - self._add_service(baremetal_service.BaremetalService(version="v1")) - self._add_service( - block_storage_service.BlockStorageService(version="v2")) - self._add_service(clustering_service.ClusteringService(version="v1")) - self._add_service(compute_service.ComputeService(version="v2")) - self._add_service(database_service.DatabaseService(version="v1")) - self._add_service(identity_service.IdentityService(version="v3")) - self._add_service(image_service.ImageService(version="v2")) - self._add_service(key_manager_service.KeyManagerService(version="v1")) - self._add_service(lb_service.LoadBalancerService(version="v2")) - self._add_service(message_service.MessageService(version="v1")) - self._add_service(network_service.NetworkService(version="v2")) - self._add_service( - object_store_service.ObjectStoreService(version="v1")) - self._add_service( - orchestration_service.OrchestrationService(version="v1")) - self._add_service(workflow_service.WorkflowService(version="v2")) - - self.service_keys = sorted(self._services.keys()) - - def __repr__(self): - return repr(self._services) - - def _add_service(self, serv): - serv.interface = None - self._services[serv.service_type] = serv - - @utils.deprecated(deprecated_in="0.10.0", removed_in="1.0", - details="Use openstack.config instead") - def get_filter(self, service): - """Get a service preference. - - :param str service: Desired service type. - """ - return copy.copy(self._get_filter(service)) - - def _get_filter(self, service): - """Get a service preference. - - :param str service: Desired service type. - """ - serv = self._services.get(service, None) - if serv is not None: - return serv - msg = ("Service %s not in list of valid services: %s" % - (service, self.service_keys)) - raise exceptions.SDKException(msg) - - def _get_services(self, service): - return self.service_keys if service == self.ALL else [service] - - def _setter(self, service, attr, value): - for service in self._get_services(service): - setattr(self._get_filter(service), attr, value) - - @utils.deprecated(deprecated_in="0.10.0", removed_in="1.0", - details="Use openstack.config instead") - def get_services(self): - """Get a list of all the known services.""" - services = [] - for name, service in self._services.items(): - services.append(service) - return services - - @utils.deprecated(deprecated_in="0.10.0", removed_in="1.0", - details="Use openstack.config instead") - def set_name(self, service, name): - """Set the desired name for the specified service. - - :param str service: Service type. - :param str name: Desired service name. - """ - self._setter(service, "service_name", name) - - @utils.deprecated(deprecated_in="0.10.0", removed_in="1.0", - details="Use openstack.config instead") - def set_region(self, service, region): - """Set the desired region for the specified service. - - :param str service: Service type. - :param str region: Desired service region. - """ - self._setter(service, "region", region) - - @utils.deprecated(deprecated_in="0.10.0", removed_in="1.0", - details="Use openstack.config instead") - def set_version(self, service, version): - """Set the desired version for the specified service. - - :param str service: Service type. - :param str version: Desired service version. - """ - self._get_filter(service).version = version - - @utils.deprecated(deprecated_in="0.10.0", removed_in="1.0", - details="Use openstack.config instead") - def set_api_version(self, service, api_version): - """Set the desired API micro-version for the specified service. - - :param str service: Service type. - :param str api_version: Desired service API micro-version. - """ - self._setter(service, "api_version", api_version) - - @utils.deprecated(deprecated_in="0.10.0", removed_in="1.0", - details="Use openstack.config instead") - def set_interface(self, service, interface): - """Set the desired interface for the specified service. - - :param str service: Service type. - :param str interface: Desired service interface. - """ - self._setter(service, "interface", interface) diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index b8bdda0f4..ff8fc9d87 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -18,7 +18,6 @@ from openstack import connection import openstack.config -from openstack import profile from openstack.tests.unit import base @@ -204,21 +203,6 @@ def test_from_config_verify(self): sot = connection.from_config(cloud="cacert") self.assertEqual(CONFIG_CACERT, sot.session.verify) - def test_from_profile(self): - """Copied from openstackclient/network/client.py make_client.""" - API_NAME = "network" - instance = self.cloud_config - - prof = profile.Profile() - prof.set_region(API_NAME, instance.region_name) - prof.set_version(API_NAME, instance.get_api_version(API_NAME)) - prof.set_interface(API_NAME, instance.get_interface(API_NAME)) - connection.Connection( - authenticator=instance.get_session().auth, - verify=instance.get_session().verify, - cert=instance.get_session().cert, - profile=prof) - class TestNetworkConnection(base.TestCase): # We need to do the neutron adapter test differently because it needs diff --git a/releasenotes/notes/removed-profile-b033d870937868a1.yaml b/releasenotes/notes/removed-profile-b033d870937868a1.yaml new file mode 100644 index 000000000..c5cac152b --- /dev/null +++ b/releasenotes/notes/removed-profile-b033d870937868a1.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + ``openstack.profile.Profile`` has been removed. ``openstack.config`` + should be used directly instead. From a70c29134ef0c426d4a28d41940ee661601dceec Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 21 Sep 2018 11:26:57 -0500 Subject: [PATCH 2218/3836] Set endpoint_override from endpoint with noauth When noauth is used via the 'none' auth_type, the endpoint given actually wants to get passed to keystoneauth as an endpoint_override for that service, otherwise discovery from keystoneauth no worky. Change-Id: I005a6d9b195a6cceae855d1392a29f12d6782a55 --- openstack/config/cloud_region.py | 5 +++++ .../tests/unit/cloud/test_operator_noauth.py | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 6ef1369d8..24b9ab789 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -278,6 +278,11 @@ def get_endpoint(self, service_type): value = self._get_config('endpoint_override', service_type) if not value: value = self._get_config('endpoint', service_type) + if not value and self.config.get('auth_type') == 'none': + # If endpoint is given and we're using the none auth type, + # then the endpoint value is the endpoint_override for every + # service. + value = self.config.get('auth', {}).get('endpoint') return value def get_connect_retries(self, service_type): diff --git a/openstack/tests/unit/cloud/test_operator_noauth.py b/openstack/tests/unit/cloud/test_operator_noauth.py index f62c44488..7630c805c 100644 --- a/openstack/tests/unit/cloud/test_operator_noauth.py +++ b/openstack/tests/unit/cloud/test_operator_noauth.py @@ -64,6 +64,26 @@ def test_ironic_noauth_none_auth_type(self): self.assert_calls() + def test_ironic_noauth_auth_endpoint(self): + """Test noauth selection for Ironic in OpenStackCloud + + Sometimes people also write clouds.yaml files that look like this: + + :: + clouds: + bifrost: + auth_type: "none" + endpoint: https://bare-metal.example.com + """ + self.cloud_noauth = openstack.connect( + auth_type='none', + endpoint='https://bare-metal.example.com/v1', + ) + + self.cloud_noauth.list_machines() + + self.assert_calls() + def test_ironic_noauth_admin_token_auth_type(self): """Test noauth selection for Ironic in OpenStackCloud From 071e567b320dc71f79658dfe74b82be0bbac9c1e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 25 Aug 2018 10:57:46 +0900 Subject: [PATCH 2219/3836] Use discovery instead of config to create proxies Since the dawn of time we've labored under the crippling burden of needing to explicitly request a version via configuration in order to get a usable handle to the cloud. This is despite the hilarity of the existence of a system for discovering available versions since basically the beginning of the entire OpenStack project. Today we shall be liberated from the tyranny of terrible past life decisions on the part of our forefathers and shall usher forth the shining freedom of actually using the discovery system. Change-Id: I11c16d37d3ab3d77bed3a0bcbd98f1fa33b9555f --- SHADE-MERGE-TODO.rst | 4 +- .../create/examples/resource/fake.py | 2 - .../create/examples/resource/fake_service.py | 13 +- doc/source/user/index.rst | 2 +- doc/source/user/service_description.rst | 10 + doc/source/user/service_filter.rst | 10 - lower-constraints.txt | 2 +- openstack/_meta/connection.py | 53 +++-- openstack/baremetal/baremetal_service.py | 14 +- openstack/baremetal/v1/chassis.py | 2 - openstack/baremetal/v1/driver.py | 2 - openstack/baremetal/v1/node.py | 2 - openstack/baremetal/v1/port.py | 2 - openstack/baremetal/v1/port_group.py | 2 - openstack/baremetal/version.py | 4 - .../block_storage/block_storage_service.py | 15 +- openstack/block_storage/v2/snapshot.py | 2 - openstack/block_storage/v2/stats.py | 2 - openstack/block_storage/v2/type.py | 2 - openstack/block_storage/v2/volume.py | 2 - openstack/clustering/clustering_service.py | 17 +- openstack/clustering/v1/action.py | 2 - openstack/clustering/v1/build_info.py | 2 - openstack/clustering/v1/cluster.py | 2 - openstack/clustering/v1/cluster_attr.py | 2 - openstack/clustering/v1/cluster_policy.py | 2 - openstack/clustering/v1/event.py | 2 - openstack/clustering/v1/node.py | 2 - openstack/clustering/v1/policy.py | 2 - openstack/clustering/v1/policy_type.py | 2 - openstack/clustering/v1/profile.py | 2 - openstack/clustering/v1/profile_type.py | 2 - openstack/clustering/v1/receiver.py | 2 - openstack/clustering/v1/service.py | 2 - openstack/clustering/version.py | 4 - openstack/compute/compute_service.py | 14 +- openstack/compute/v2/availability_zone.py | 3 - openstack/compute/v2/extension.py | 2 - openstack/compute/v2/flavor.py | 2 - openstack/compute/v2/hypervisor.py | 3 - openstack/compute/v2/image.py | 2 - openstack/compute/v2/keypair.py | 2 - openstack/compute/v2/limits.py | 2 - openstack/compute/v2/server.py | 4 +- openstack/compute/v2/server_group.py | 2 - openstack/compute/v2/server_interface.py | 2 - openstack/compute/v2/server_ip.py | 2 - openstack/compute/v2/service.py | 3 - openstack/compute/v2/volume_attachment.py | 2 - openstack/compute/version.py | 4 - openstack/config/cloud_region.py | 28 ++- openstack/database/database_service.py | 14 +- openstack/database/v1/database.py | 2 - openstack/database/v1/flavor.py | 2 - openstack/database/v1/instance.py | 2 - openstack/database/v1/user.py | 2 - openstack/identity/identity_service.py | 26 +-- openstack/identity/v2/extension.py | 2 - openstack/identity/v2/role.py | 2 - openstack/identity/v2/tenant.py | 2 - openstack/identity/v2/user.py | 2 - openstack/identity/v3/credential.py | 2 - openstack/identity/v3/domain.py | 2 - openstack/identity/v3/endpoint.py | 2 - openstack/identity/v3/group.py | 2 - openstack/identity/v3/policy.py | 2 - openstack/identity/v3/project.py | 3 - openstack/identity/v3/region.py | 2 - openstack/identity/v3/role.py | 2 - openstack/identity/v3/role_assignment.py | 2 - .../v3/role_domain_group_assignment.py | 2 - .../v3/role_domain_user_assignment.py | 2 - .../v3/role_project_group_assignment.py | 2 - .../v3/role_project_user_assignment.py | 2 - openstack/identity/v3/service.py | 2 - openstack/identity/v3/trust.py | 2 - openstack/identity/v3/user.py | 2 - openstack/identity/version.py | 4 - openstack/image/image_service.py | 19 +- openstack/image/v1/image.py | 2 - openstack/image/v2/image.py | 2 - openstack/image/v2/member.py | 2 - openstack/instance_ha/instance_ha_service.py | 14 +- openstack/instance_ha/v1/host.py | 2 - openstack/instance_ha/v1/notification.py | 2 - openstack/instance_ha/v1/segment.py | 2 - openstack/key_manager/key_manager_service.py | 14 +- openstack/key_manager/v1/container.py | 2 - openstack/key_manager/v1/order.py | 2 - openstack/key_manager/v1/secret.py | 2 - .../load_balancer/load_balancer_service.py | 16 +- openstack/load_balancer/v2/health_monitor.py | 4 +- openstack/load_balancer/v2/l7_policy.py | 4 +- openstack/load_balancer/v2/l7_rule.py | 4 +- openstack/load_balancer/v2/listener.py | 4 +- openstack/load_balancer/v2/load_balancer.py | 4 +- openstack/load_balancer/v2/member.py | 4 +- openstack/load_balancer/v2/pool.py | 4 +- openstack/load_balancer/version.py | 4 - openstack/message/message_service.py | 16 +- openstack/message/v2/claim.py | 2 - openstack/message/v2/message.py | 2 - openstack/message/v2/queue.py | 2 - openstack/message/v2/subscription.py | 2 - openstack/message/version.py | 4 - openstack/network/network_service.py | 14 +- openstack/network/v2/address_scope.py | 2 - openstack/network/v2/agent.py | 4 - .../network/v2/auto_allocated_topology.py | 2 - openstack/network/v2/availability_zone.py | 2 - openstack/network/v2/extension.py | 2 - openstack/network/v2/firewall_group.py | 2 - openstack/network/v2/firewall_policy.py | 2 - openstack/network/v2/firewall_rule.py | 2 - openstack/network/v2/flavor.py | 2 - openstack/network/v2/floating_ip.py | 2 - openstack/network/v2/health_monitor.py | 2 - openstack/network/v2/listener.py | 2 - openstack/network/v2/load_balancer.py | 2 - openstack/network/v2/metering_label.py | 2 - openstack/network/v2/metering_label_rule.py | 2 - openstack/network/v2/network.py | 3 - .../network/v2/network_ip_availability.py | 2 - openstack/network/v2/pool.py | 2 - openstack/network/v2/pool_member.py | 2 - openstack/network/v2/port.py | 2 - .../network/v2/qos_bandwidth_limit_rule.py | 2 - openstack/network/v2/qos_dscp_marking_rule.py | 2 - .../network/v2/qos_minimum_bandwidth_rule.py | 2 - openstack/network/v2/qos_policy.py | 2 - openstack/network/v2/qos_rule_type.py | 2 - openstack/network/v2/quota.py | 2 - openstack/network/v2/rbac_policy.py | 2 - openstack/network/v2/router.py | 3 - openstack/network/v2/security_group.py | 2 - openstack/network/v2/security_group_rule.py | 2 - openstack/network/v2/segment.py | 2 - openstack/network/v2/service_profile.py | 2 - openstack/network/v2/service_provider.py | 2 - openstack/network/v2/subnet.py | 2 - openstack/network/v2/subnet_pool.py | 2 - openstack/network/v2/trunk.py | 2 - openstack/network/v2/vpn_service.py | 2 - openstack/network/version.py | 4 - .../object_store/object_store_service.py | 14 +- openstack/object_store/v1/_base.py | 2 - openstack/object_store/v1/obj.py | 2 - .../orchestration/orchestration_service.py | 17 +- openstack/orchestration/v1/resource.py | 2 - openstack/orchestration/v1/software_config.py | 2 - .../orchestration/v1/software_deployment.py | 2 - openstack/orchestration/v1/stack.py | 2 - .../orchestration/v1/stack_environment.py | 2 - openstack/orchestration/v1/stack_files.py | 2 - openstack/orchestration/v1/stack_template.py | 2 - openstack/orchestration/v1/template.py | 2 - openstack/orchestration/version.py | 4 - openstack/service_description.py | 157 ++++++++------ openstack/service_filter.py | 199 ------------------ .../unit/baremetal/test_baremetal_service.py | 28 --- .../tests/unit/baremetal/test_version.py | 1 - .../tests/unit/baremetal/v1/test_chassis.py | 2 - .../tests/unit/baremetal/v1/test_driver.py | 1 - .../tests/unit/baremetal/v1/test_node.py | 2 - .../tests/unit/baremetal/v1/test_port.py | 2 - .../unit/baremetal/v1/test_port_group.py | 2 - .../test_block_storage_service.py | 28 --- .../unit/block_storage/v2/test_snapshot.py | 1 - .../tests/unit/block_storage/v2/test_type.py | 1 - .../unit/block_storage/v2/test_volume.py | 1 - .../tests/unit/block_store/v2/test_stats.py | 1 - .../tests/unit/cloud/test_operator_noauth.py | 117 +++++++++- .../unit/clustering/test_cluster_service.py | 28 --- .../tests/unit/clustering/test_version.py | 1 - .../tests/unit/clustering/v1/test_action.py | 1 - .../unit/clustering/v1/test_build_info.py | 1 - .../tests/unit/clustering/v1/test_cluster.py | 1 - .../unit/clustering/v1/test_cluster_attr.py | 1 - .../unit/clustering/v1/test_cluster_policy.py | 1 - .../tests/unit/clustering/v1/test_event.py | 1 - .../tests/unit/clustering/v1/test_node.py | 1 - .../tests/unit/clustering/v1/test_policy.py | 2 - .../unit/clustering/v1/test_policy_type.py | 1 - .../tests/unit/clustering/v1/test_profile.py | 2 - .../unit/clustering/v1/test_profile_type.py | 1 - .../tests/unit/clustering/v1/test_receiver.py | 1 - .../tests/unit/clustering/v1/test_service.py | 1 - .../unit/compute/test_compute_service.py | 28 --- openstack/tests/unit/compute/test_version.py | 1 - .../unit/compute/v2/test_availability_zone.py | 2 - .../tests/unit/compute/v2/test_extension.py | 1 - .../tests/unit/compute/v2/test_flavor.py | 2 - .../tests/unit/compute/v2/test_hypervisor.py | 2 - openstack/tests/unit/compute/v2/test_image.py | 2 - .../tests/unit/compute/v2/test_keypair.py | 1 - .../tests/unit/compute/v2/test_limits.py | 2 - .../tests/unit/compute/v2/test_server.py | 2 - .../unit/compute/v2/test_server_group.py | 1 - .../unit/compute/v2/test_server_interface.py | 1 - .../tests/unit/compute/v2/test_server_ip.py | 1 - .../tests/unit/compute/v2/test_service.py | 1 - .../unit/compute/v2/test_volume_attachment.py | 1 - .../unit/database/test_database_service.py | 28 --- .../tests/unit/database/v1/test_database.py | 1 - .../tests/unit/database/v1/test_flavor.py | 1 - .../tests/unit/database/v1/test_instance.py | 1 - openstack/tests/unit/database/v1/test_user.py | 1 - .../unit/identity/test_identity_service.py | 37 ---- openstack/tests/unit/identity/test_version.py | 1 - .../tests/unit/identity/v2/test_extension.py | 1 - openstack/tests/unit/identity/v2/test_role.py | 1 - .../tests/unit/identity/v2/test_tenant.py | 1 - openstack/tests/unit/identity/v2/test_user.py | 1 - .../tests/unit/identity/v3/test_credential.py | 1 - .../tests/unit/identity/v3/test_domain.py | 1 - .../tests/unit/identity/v3/test_endpoint.py | 1 - .../tests/unit/identity/v3/test_group.py | 1 - .../tests/unit/identity/v3/test_policy.py | 1 - .../tests/unit/identity/v3/test_project.py | 2 - .../tests/unit/identity/v3/test_region.py | 1 - openstack/tests/unit/identity/v3/test_role.py | 1 - .../unit/identity/v3/test_role_assignment.py | 1 - .../v3/test_role_domain_group_assignment.py | 1 - .../v3/test_role_domain_user_assignment.py | 1 - .../v3/test_role_project_group_assignment.py | 1 - .../v3/test_role_project_user_assignment.py | 1 - .../tests/unit/identity/v3/test_service.py | 1 - .../tests/unit/identity/v3/test_trust.py | 1 - openstack/tests/unit/identity/v3/test_user.py | 1 - .../tests/unit/image/test_image_service.py | 30 --- openstack/tests/unit/image/v1/test_image.py | 1 - openstack/tests/unit/image/v2/test_image.py | 1 - openstack/tests/unit/image/v2/test_member.py | 1 - .../instance_ha/test_instance_ha_service.py | 30 --- .../tests/unit/instance_ha/v1/test_host.py | 1 - .../unit/instance_ha/v1/test_notification.py | 1 - .../tests/unit/instance_ha/v1/test_segment.py | 1 - .../test_key_management_service.py | 28 --- .../unit/key_manager/v1/test_container.py | 1 - .../tests/unit/key_manager/v1/test_order.py | 1 - .../tests/unit/key_manager/v1/test_secret.py | 1 - .../unit/load_balancer/test_health_monitor.py | 3 +- .../tests/unit/load_balancer/test_l7policy.py | 3 +- .../tests/unit/load_balancer/test_l7rule.py | 3 +- .../tests/unit/load_balancer/test_listener.py | 3 +- .../unit/load_balancer/test_load_balancer.py | 8 +- .../test_load_balancer_service.py | 28 --- .../tests/unit/load_balancer/test_member.py | 3 +- .../tests/unit/load_balancer/test_pool.py | 4 +- .../tests/unit/load_balancer/test_version.py | 1 - .../unit/message/test_message_service.py | 28 --- openstack/tests/unit/message/test_version.py | 1 - openstack/tests/unit/message/v2/test_claim.py | 1 - .../tests/unit/message/v2/test_message.py | 1 - openstack/tests/unit/message/v2/test_queue.py | 1 - .../unit/message/v2/test_subscription.py | 1 - .../unit/network/test_network_service.py | 28 --- openstack/tests/unit/network/test_version.py | 1 - .../unit/network/v2/test_address_scope.py | 1 - openstack/tests/unit/network/v2/test_agent.py | 3 - .../unit/network/v2/test_availability_zone.py | 1 - .../tests/unit/network/v2/test_extension.py | 1 - .../unit/network/v2/test_firewall_group.py | 1 - .../unit/network/v2/test_firewall_policy.py | 1 - .../unit/network/v2/test_firewall_rule.py | 1 - .../tests/unit/network/v2/test_flavor.py | 1 - .../tests/unit/network/v2/test_floating_ip.py | 1 - .../unit/network/v2/test_health_monitor.py | 1 - .../tests/unit/network/v2/test_listener.py | 1 - .../unit/network/v2/test_load_balancer.py | 1 - .../unit/network/v2/test_metering_label.py | 1 - .../network/v2/test_metering_label_rule.py | 1 - .../tests/unit/network/v2/test_network.py | 2 - .../v2/test_network_ip_availability.py | 1 - openstack/tests/unit/network/v2/test_pool.py | 1 - .../tests/unit/network/v2/test_pool_member.py | 1 - openstack/tests/unit/network/v2/test_port.py | 1 - .../v2/test_qos_bandwidth_limit_rule.py | 1 - .../network/v2/test_qos_dscp_marking_rule.py | 1 - .../v2/test_qos_minimum_bandwidth_rule.py | 1 - .../tests/unit/network/v2/test_qos_policy.py | 1 - .../unit/network/v2/test_qos_rule_type.py | 1 - openstack/tests/unit/network/v2/test_quota.py | 2 - .../tests/unit/network/v2/test_rbac_policy.py | 1 - .../tests/unit/network/v2/test_router.py | 2 - .../unit/network/v2/test_security_group.py | 1 - .../network/v2/test_security_group_rule.py | 1 - .../tests/unit/network/v2/test_segment.py | 1 - .../unit/network/v2/test_service_provider.py | 1 - .../tests/unit/network/v2/test_subnet.py | 1 - .../tests/unit/network/v2/test_subnet_pool.py | 1 - openstack/tests/unit/network/v2/test_trunk.py | 1 - .../tests/unit/network/v2/test_vpn_service.py | 2 +- .../object_store/test_object_store_service.py | 28 --- .../unit/object_store/v1/test_account.py | 1 - .../unit/object_store/v1/test_container.py | 1 - .../tests/unit/object_store/v1/test_obj.py | 1 - .../test_orchestration_service.py | 29 --- .../tests/unit/orchestration/test_version.py | 1 - .../unit/orchestration/v1/test_resource.py | 1 - .../orchestration/v1/test_software_config.py | 1 - .../v1/test_software_deployment.py | 1 - .../tests/unit/orchestration/v1/test_stack.py | 1 - .../v1/test_stack_environment.py | 1 - .../unit/orchestration/v1/test_stack_files.py | 2 - .../orchestration/v1/test_stack_template.py | 1 - .../unit/orchestration/v1/test_template.py | 1 - openstack/tests/unit/test_connection.py | 56 +++-- openstack/tests/unit/test_service_filter.py | 41 ---- .../tests/unit/workflow/test_execution.py | 1 - openstack/tests/unit/workflow/test_version.py | 1 - .../tests/unit/workflow/test_workflow.py | 1 - .../unit/workflow/test_workflow_service.py | 28 --- openstack/workflow/v2/execution.py | 2 - openstack/workflow/v2/workflow.py | 2 - openstack/workflow/version.py | 4 - openstack/workflow/workflow_service.py | 16 +- requirements.txt | 2 +- 318 files changed, 421 insertions(+), 1441 deletions(-) create mode 100644 doc/source/user/service_description.rst delete mode 100644 doc/source/user/service_filter.rst delete mode 100644 openstack/service_filter.py delete mode 100644 openstack/tests/unit/baremetal/test_baremetal_service.py delete mode 100644 openstack/tests/unit/block_storage/test_block_storage_service.py delete mode 100644 openstack/tests/unit/clustering/test_cluster_service.py delete mode 100644 openstack/tests/unit/compute/test_compute_service.py delete mode 100644 openstack/tests/unit/database/test_database_service.py delete mode 100644 openstack/tests/unit/identity/test_identity_service.py delete mode 100644 openstack/tests/unit/image/test_image_service.py delete mode 100644 openstack/tests/unit/instance_ha/test_instance_ha_service.py delete mode 100644 openstack/tests/unit/key_manager/test_key_management_service.py delete mode 100644 openstack/tests/unit/load_balancer/test_load_balancer_service.py delete mode 100644 openstack/tests/unit/message/test_message_service.py delete mode 100644 openstack/tests/unit/network/test_network_service.py delete mode 100644 openstack/tests/unit/object_store/test_object_store_service.py delete mode 100644 openstack/tests/unit/orchestration/test_orchestration_service.py delete mode 100644 openstack/tests/unit/test_service_filter.py delete mode 100644 openstack/tests/unit/workflow/test_workflow_service.py diff --git a/SHADE-MERGE-TODO.rst b/SHADE-MERGE-TODO.rst index 15c266d4c..e34d87810 100644 --- a/SHADE-MERGE-TODO.rst +++ b/SHADE-MERGE-TODO.rst @@ -34,7 +34,7 @@ already. For reference, those are: servers = conn.list_servers() # High-level resource interface from shade servers = conn.compute.servers() # SDK Service/Object Interface response = conn.compute.get('/servers') # REST passthrough - +* Removed ServiceFilter and the various Service objects in favor of discovery. Next steps ========== @@ -44,8 +44,6 @@ Next steps mean anything to people. * Migrate unit tests to requests-mock instead of mocking python calls to session. -* Investigate removing ServiceFilter and the various Service objects if an - acceptable plan can be found for using discovery. * Replace _prepare_request with requests.Session.prepare_request. shade integration diff --git a/doc/source/contributor/create/examples/resource/fake.py b/doc/source/contributor/create/examples/resource/fake.py index d73a4f07b..b02175c12 100644 --- a/doc/source/contributor/create/examples/resource/fake.py +++ b/doc/source/contributor/create/examples/resource/fake.py @@ -1,6 +1,5 @@ # Apache 2 header omitted for brevity -from openstack.fake import fake_service from openstack import resource @@ -8,7 +7,6 @@ class Fake(resource.Resource): resource_key = "resource" resources_key = "resources" base_path = "/fake" - service = fake_service.FakeService() allow_create = True allow_fetch = True diff --git a/doc/source/contributor/create/examples/resource/fake_service.py b/doc/source/contributor/create/examples/resource/fake_service.py index 9524b967d..34000b986 100644 --- a/doc/source/contributor/create/examples/resource/fake_service.py +++ b/doc/source/contributor/create/examples/resource/fake_service.py @@ -1,13 +1,12 @@ # Apache 2 header omitted for brevity -from openstack import service_filter +from openstack import service_description +from openstack.fake.v2 import _proxy as _proxy_v2 -class FakeService(service_filter.ServiceFilter): +class FakeService(service_description.ServiceDescription): """The fake service.""" - valid_versions = [service_filter.ValidVersion('v2')] - - def __init__(self, version=None): - """Create a fake service.""" - super(FakeService, self).__init__(service_type='fake', version=version) + supported_versions = { + '2': _proxy_v2.Proxy, + } diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index e88e7e298..d0eeb6b65 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -148,7 +148,7 @@ can be customized. :maxdepth: 1 resource - service_filter + service_description utils Presentations diff --git a/doc/source/user/service_description.rst b/doc/source/user/service_description.rst new file mode 100644 index 000000000..3eeeb4a7a --- /dev/null +++ b/doc/source/user/service_description.rst @@ -0,0 +1,10 @@ +ServiceDescription +================== +.. automodule:: openstack.service_description + + +ServiceDescription object +------------------------- + +.. autoclass:: openstack.service_description.ServiceDescription + :members: diff --git a/doc/source/user/service_filter.rst b/doc/source/user/service_filter.rst deleted file mode 100644 index a58177f25..000000000 --- a/doc/source/user/service_filter.rst +++ /dev/null @@ -1,10 +0,0 @@ -ServiceFilter -============= -.. automodule:: openstack.service_filter - - -ServiceFilter object --------------------- - -.. autoclass:: openstack.service_filter.ServiceFilter - :members: diff --git a/lower-constraints.txt b/lower-constraints.txt index 2842cdfd0..59dee61ee 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -14,7 +14,7 @@ jmespath==0.9.0 jsonpatch==1.16 jsonpointer==1.13 jsonschema==2.6.0 -keystoneauth1==3.8.0 +keystoneauth1==3.11.0 linecache2==1.0.0 mock==2.0.0 mox3==0.20.0 diff --git a/openstack/_meta/connection.py b/openstack/_meta/connection.py index 6511e9683..61db44b0c 100644 --- a/openstack/_meta/connection.py +++ b/openstack/_meta/connection.py @@ -18,7 +18,6 @@ import os_service_types from openstack import _log -from openstack import proxy from openstack import service_description _logger = _log.setup_logging('openstack') @@ -43,28 +42,28 @@ def __new__(meta, name, bases, dct): # from openstacksdk. The credentials API calls are all calls # on identity endpoints. continue - desc_class = service_description.ServiceDescription - service_filter_class = _find_service_filter_class(service_type) + desc_class = _find_service_description_class(service_type) descriptor_args = {'service_type': service_type} - if service_filter_class: - desc_class = service_description.OpenStackServiceDescription - descriptor_args['service_filter_class'] = service_filter_class - class_names = service_filter_class._get_proxy_class_names() - if len(class_names) == 1: - doc = _DOC_TEMPLATE.format( - class_name="{service_type} Proxy <{name}>".format( - service_type=service_type, name=class_names[0]), - **service) - else: - class_doc_strings = "\n".join([ - ":class:`{class_name}`".format(class_name=class_name) - for class_name in class_names]) - doc = _PROXY_TEMPLATE.format( - class_doc_strings=class_doc_strings, **service) - else: - descriptor_args['proxy_class'] = proxy.Proxy + + if not desc_class.supported_versions: doc = _DOC_TEMPLATE.format( - class_name='~openstack.proxy.Proxy', **service) + class_name="{service_type} Proxy".format( + service_type=service_type), + **service) + elif len(desc_class.supported_versions) == 1: + supported_version = list( + desc_class.supported_versions.keys())[0] + doc = _DOC_TEMPLATE.format( + class_name="{service_type} Proxy <{name}>".format( + service_type=service_type, name=supported_version), + **service) + else: + class_doc_strings = "\n".join([ + ":class:`{class_name}`".format( + class_name=proxy_class.__name__) + for proxy_class in desc_class.supported_versions.values()]) + doc = _PROXY_TEMPLATE.format( + class_doc_strings=class_doc_strings, **service) descriptor = desc_class(**descriptor_args) descriptor.__doc__ = doc st = service_type.replace('-', '_') @@ -103,7 +102,7 @@ def _get_aliases(service_type, aliases=None): return all_types -def _find_service_filter_class(service_type): +def _find_service_description_class(service_type): package_name = 'openstack.{service_type}'.format( service_type=service_type).replace('-', '_') module_name = service_type.replace('-', '_') + '_service' @@ -111,17 +110,17 @@ def _find_service_filter_class(service_type): [part.capitalize() for part in module_name.split('_')]) try: import_name = '.'.join([package_name, module_name]) - service_filter_module = importlib.import_module(import_name) + service_description_module = importlib.import_module(import_name) except ImportError as e: # ImportWarning is ignored by default. This warning is here # as an opt-in for people trying to figure out why something # didn't work. warnings.warn( - "Could not import {service_type} service filter: {e}".format( + "Could not import {service_type} service description: {e}".format( service_type=service_type, e=str(e)), ImportWarning) - return None + return service_description.ServiceDescription # There are no cases in which we should have a module but not the class # inside it. - service_filter_class = getattr(service_filter_module, class_name) - return service_filter_class + service_description_class = getattr(service_description_module, class_name) + return service_description_class diff --git a/openstack/baremetal/baremetal_service.py b/openstack/baremetal/baremetal_service.py index 9853be081..0b0cb818a 100644 --- a/openstack/baremetal/baremetal_service.py +++ b/openstack/baremetal/baremetal_service.py @@ -10,15 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import service_filter +from openstack import service_description +from openstack.baremetal.v1 import _proxy -class BaremetalService(service_filter.ServiceFilter): +class BaremetalService(service_description.ServiceDescription): """The bare metal service.""" - valid_versions = [service_filter.ValidVersion('v1')] - - def __init__(self, version=None): - """Create a bare metal service.""" - super(BaremetalService, self).__init__(service_type='baremetal', - version=version) + supported_versions = { + '1': _proxy.Proxy, + } diff --git a/openstack/baremetal/v1/chassis.py b/openstack/baremetal/v1/chassis.py index ffe22ebc6..eb9842f5b 100644 --- a/openstack/baremetal/v1/chassis.py +++ b/openstack/baremetal/v1/chassis.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.baremetal import baremetal_service from openstack import resource @@ -18,7 +17,6 @@ class Chassis(resource.Resource): resources_key = 'chassis' base_path = '/chassis' - service = baremetal_service.BaremetalService() # capabilities allow_create = True diff --git a/openstack/baremetal/v1/driver.py b/openstack/baremetal/v1/driver.py index 63803127a..00a31954d 100644 --- a/openstack/baremetal/v1/driver.py +++ b/openstack/baremetal/v1/driver.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.baremetal import baremetal_service from openstack import resource @@ -18,7 +17,6 @@ class Driver(resource.Resource): resources_key = 'drivers' base_path = '/drivers' - service = baremetal_service.BaremetalService() # capabilities allow_create = False diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 69779fae5..913301cc6 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -11,7 +11,6 @@ # under the License. from openstack import _log -from openstack.baremetal import baremetal_service from openstack.baremetal.v1 import _common from openstack import exceptions from openstack import resource @@ -39,7 +38,6 @@ class Node(resource.Resource): resources_key = 'nodes' base_path = '/nodes' - service = baremetal_service.BaremetalService() # capabilities allow_create = True diff --git a/openstack/baremetal/v1/port.py b/openstack/baremetal/v1/port.py index 871b97a2a..c05f1f7a5 100644 --- a/openstack/baremetal/v1/port.py +++ b/openstack/baremetal/v1/port.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.baremetal import baremetal_service from openstack import resource @@ -18,7 +17,6 @@ class Port(resource.Resource): resources_key = 'ports' base_path = '/ports' - service = baremetal_service.BaremetalService() # capabilities allow_create = True diff --git a/openstack/baremetal/v1/port_group.py b/openstack/baremetal/v1/port_group.py index dbee1d93f..6e587fe2d 100644 --- a/openstack/baremetal/v1/port_group.py +++ b/openstack/baremetal/v1/port_group.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.baremetal import baremetal_service from openstack import resource @@ -18,7 +17,6 @@ class PortGroup(resource.Resource): resources_key = 'portgroups' base_path = '/portgroups' - service = baremetal_service.BaremetalService() # capabilities allow_create = True diff --git a/openstack/baremetal/version.py b/openstack/baremetal/version.py index 1613fe43a..9311893e5 100644 --- a/openstack/baremetal/version.py +++ b/openstack/baremetal/version.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.baremetal import baremetal_service from openstack import resource @@ -18,9 +17,6 @@ class Version(resource.Resource): resource_key = 'version' resources_key = 'versions' base_path = '/' - service = baremetal_service.BaremetalService( - version=baremetal_service.BaremetalService.UNVERSIONED - ) # Capabilities allow_list = True diff --git a/openstack/block_storage/block_storage_service.py b/openstack/block_storage/block_storage_service.py index 7a8b99913..a4906298a 100644 --- a/openstack/block_storage/block_storage_service.py +++ b/openstack/block_storage/block_storage_service.py @@ -10,16 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import service_filter +from openstack.block_storage.v2 import _proxy +from openstack import service_description -class BlockStorageService(service_filter.ServiceFilter): +class BlockStorageService(service_description.ServiceDescription): """The block storage service.""" - valid_versions = [service_filter.ValidVersion('v2')] - - def __init__(self, version=None): - """Create a block storage service.""" - super(BlockStorageService, self).__init__( - service_type='block-storage', - version=version, requires_project_id=True) + supported_versions = { + '2': _proxy.Proxy, + } diff --git a/openstack/block_storage/v2/snapshot.py b/openstack/block_storage/v2/snapshot.py index 9b5b91f66..d9137934c 100644 --- a/openstack/block_storage/v2/snapshot.py +++ b/openstack/block_storage/v2/snapshot.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.block_storage import block_storage_service from openstack import format from openstack import resource @@ -19,7 +18,6 @@ class Snapshot(resource.Resource): resource_key = "snapshot" resources_key = "snapshots" base_path = "/snapshots" - service = block_storage_service.BlockStorageService() _query_mapping = resource.QueryParameters( 'all_tenants', 'name', 'status', 'volume_id') diff --git a/openstack/block_storage/v2/stats.py b/openstack/block_storage/v2/stats.py index 5a3f6ae3f..dcd2e2945 100644 --- a/openstack/block_storage/v2/stats.py +++ b/openstack/block_storage/v2/stats.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.block_storage import block_storage_service from openstack import resource @@ -18,7 +17,6 @@ class Pools(resource.Resource): resource_key = "pool" resources_key = "pools" base_path = "/scheduler-stats/get_pools?detail=True" - service = block_storage_service.BlockStorageService() # capabilities allow_fetch = False diff --git a/openstack/block_storage/v2/type.py b/openstack/block_storage/v2/type.py index d57d31853..db0726051 100644 --- a/openstack/block_storage/v2/type.py +++ b/openstack/block_storage/v2/type.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.block_storage import block_storage_service from openstack import resource @@ -18,7 +17,6 @@ class Type(resource.Resource): resource_key = "volume_type" resources_key = "volume_types" base_path = "/types" - service = block_storage_service.BlockStorageService() # capabilities allow_fetch = True diff --git a/openstack/block_storage/v2/volume.py b/openstack/block_storage/v2/volume.py index 04a05d963..4402e624f 100644 --- a/openstack/block_storage/v2/volume.py +++ b/openstack/block_storage/v2/volume.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.block_storage import block_storage_service from openstack import format from openstack import resource @@ -19,7 +18,6 @@ class Volume(resource.Resource): resource_key = "volume" resources_key = "volumes" base_path = "/volumes" - service = block_storage_service.BlockStorageService() _query_mapping = resource.QueryParameters( 'all_tenants', 'name', 'status', 'project_id') diff --git a/openstack/clustering/clustering_service.py b/openstack/clustering/clustering_service.py index 0d5e57e4a..5920fe50a 100644 --- a/openstack/clustering/clustering_service.py +++ b/openstack/clustering/clustering_service.py @@ -10,18 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import service_filter +from openstack.clustering.v1 import _proxy +from openstack import service_description -class ClusteringService(service_filter.ServiceFilter): +class ClusteringService(service_description.ServiceDescription): """The clustering service.""" - valid_versions = [service_filter.ValidVersion('v1')] - UNVERSIONED = None - - def __init__(self, version=None): - """Create a clustering service.""" - super(ClusteringService, self).__init__( - service_type='clustering', - version=version - ) + supported_versions = { + '1': _proxy.Proxy, + } diff --git a/openstack/clustering/v1/action.py b/openstack/clustering/v1/action.py index 79c5d4edc..2bfdf9de0 100644 --- a/openstack/clustering/v1/action.py +++ b/openstack/clustering/v1/action.py @@ -11,7 +11,6 @@ # under the License. -from openstack.clustering import clustering_service from openstack import resource @@ -19,7 +18,6 @@ class Action(resource.Resource): resource_key = 'action' resources_key = 'actions' base_path = '/actions' - service = clustering_service.ClusteringService() # Capabilities allow_list = True diff --git a/openstack/clustering/v1/build_info.py b/openstack/clustering/v1/build_info.py index 705f4065c..a031fac13 100644 --- a/openstack/clustering/v1/build_info.py +++ b/openstack/clustering/v1/build_info.py @@ -10,14 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.clustering import clustering_service from openstack import resource class BuildInfo(resource.Resource): base_path = '/build-info' resource_key = 'build_info' - service = clustering_service.ClusteringService() # Capabilities allow_fetch = True diff --git a/openstack/clustering/v1/cluster.py b/openstack/clustering/v1/cluster.py index e8ab4c969..26e22cd2f 100644 --- a/openstack/clustering/v1/cluster.py +++ b/openstack/clustering/v1/cluster.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.clustering import clustering_service from openstack import resource from openstack import utils @@ -19,7 +18,6 @@ class Cluster(resource.Resource): resource_key = 'cluster' resources_key = 'clusters' base_path = '/clusters' - service = clustering_service.ClusteringService() # capabilities allow_create = True diff --git a/openstack/clustering/v1/cluster_attr.py b/openstack/clustering/v1/cluster_attr.py index 68e1f53f8..1ca1aaf1e 100644 --- a/openstack/clustering/v1/cluster_attr.py +++ b/openstack/clustering/v1/cluster_attr.py @@ -10,14 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.clustering import clustering_service from openstack import resource class ClusterAttr(resource.Resource): resources_key = 'cluster_attributes' base_path = '/clusters/%(cluster_id)s/attrs/%(path)s' - service = clustering_service.ClusteringService() # capabilities allow_list = True diff --git a/openstack/clustering/v1/cluster_policy.py b/openstack/clustering/v1/cluster_policy.py index 6e430cc5f..f9af351c7 100644 --- a/openstack/clustering/v1/cluster_policy.py +++ b/openstack/clustering/v1/cluster_policy.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.clustering import clustering_service from openstack import resource @@ -18,7 +17,6 @@ class ClusterPolicy(resource.Resource): resource_key = 'cluster_policy' resources_key = 'cluster_policies' base_path = '/clusters/%(cluster_id)s/policies' - service = clustering_service.ClusteringService() # Capabilities allow_list = True diff --git a/openstack/clustering/v1/event.py b/openstack/clustering/v1/event.py index d187fa69c..cd556257c 100644 --- a/openstack/clustering/v1/event.py +++ b/openstack/clustering/v1/event.py @@ -11,7 +11,6 @@ # under the License. -from openstack.clustering import clustering_service from openstack import resource @@ -19,7 +18,6 @@ class Event(resource.Resource): resource_key = 'event' resources_key = 'events' base_path = '/events' - service = clustering_service.ClusteringService() # Capabilities allow_list = True diff --git a/openstack/clustering/v1/node.py b/openstack/clustering/v1/node.py index 655e3a959..cf9fe05e2 100644 --- a/openstack/clustering/v1/node.py +++ b/openstack/clustering/v1/node.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.clustering import clustering_service from openstack import resource from openstack import utils @@ -19,7 +18,6 @@ class Node(resource.Resource): resource_key = 'node' resources_key = 'nodes' base_path = '/nodes' - service = clustering_service.ClusteringService() # capabilities allow_create = True diff --git a/openstack/clustering/v1/policy.py b/openstack/clustering/v1/policy.py index be98984f7..f6e8111f5 100644 --- a/openstack/clustering/v1/policy.py +++ b/openstack/clustering/v1/policy.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.clustering import clustering_service from openstack import resource @@ -18,7 +17,6 @@ class Policy(resource.Resource): resource_key = 'policy' resources_key = 'policies' base_path = '/policies' - service = clustering_service.ClusteringService() # Capabilities allow_list = True diff --git a/openstack/clustering/v1/policy_type.py b/openstack/clustering/v1/policy_type.py index bd2bdd490..96bf477f8 100644 --- a/openstack/clustering/v1/policy_type.py +++ b/openstack/clustering/v1/policy_type.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.clustering import clustering_service from openstack import resource @@ -18,7 +17,6 @@ class PolicyType(resource.Resource): resource_key = 'policy_type' resources_key = 'policy_types' base_path = '/policy-types' - service = clustering_service.ClusteringService() # Capabilities allow_list = True diff --git a/openstack/clustering/v1/profile.py b/openstack/clustering/v1/profile.py index 77f117dde..595de3946 100644 --- a/openstack/clustering/v1/profile.py +++ b/openstack/clustering/v1/profile.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.clustering import clustering_service from openstack import resource @@ -18,7 +17,6 @@ class Profile(resource.Resource): resource_key = 'profile' resources_key = 'profiles' base_path = '/profiles' - service = clustering_service.ClusteringService() # capabilities allow_create = True diff --git a/openstack/clustering/v1/profile_type.py b/openstack/clustering/v1/profile_type.py index f2b7ad88f..de6452502 100644 --- a/openstack/clustering/v1/profile_type.py +++ b/openstack/clustering/v1/profile_type.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.clustering import clustering_service from openstack import resource from openstack import utils @@ -19,7 +18,6 @@ class ProfileType(resource.Resource): resource_key = 'profile_type' resources_key = 'profile_types' base_path = '/profile-types' - service = clustering_service.ClusteringService() # Capabilities allow_list = True diff --git a/openstack/clustering/v1/receiver.py b/openstack/clustering/v1/receiver.py index d42d7a48d..95faa06c9 100644 --- a/openstack/clustering/v1/receiver.py +++ b/openstack/clustering/v1/receiver.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.clustering import clustering_service from openstack import resource @@ -18,7 +17,6 @@ class Receiver(resource.Resource): resource_key = 'receiver' resources_key = 'receivers' base_path = '/receivers' - service = clustering_service.ClusteringService() # Capabilities allow_list = True diff --git a/openstack/clustering/v1/service.py b/openstack/clustering/v1/service.py index d9de49687..52f380298 100644 --- a/openstack/clustering/v1/service.py +++ b/openstack/clustering/v1/service.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.clustering import clustering_service from openstack import resource @@ -18,7 +17,6 @@ class Service(resource.Resource): resource_key = 'service' resources_key = 'services' base_path = '/services' - service = clustering_service.ClusteringService() # Capabilities allow_list = True diff --git a/openstack/clustering/version.py b/openstack/clustering/version.py index 8fc20902b..692230a19 100644 --- a/openstack/clustering/version.py +++ b/openstack/clustering/version.py @@ -11,7 +11,6 @@ # under the License. -from openstack.clustering import clustering_service from openstack import resource @@ -19,9 +18,6 @@ class Version(resource.Resource): resource_key = 'version' resources_key = 'versions' base_path = '/' - service = clustering_service.ClusteringService( - version=clustering_service.ClusteringService.UNVERSIONED - ) # capabilities allow_list = True diff --git a/openstack/compute/compute_service.py b/openstack/compute/compute_service.py index 64c8bd9e0..2204f04d4 100644 --- a/openstack/compute/compute_service.py +++ b/openstack/compute/compute_service.py @@ -10,15 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import service_filter +from openstack import service_description +from openstack.compute.v2 import _proxy -class ComputeService(service_filter.ServiceFilter): +class ComputeService(service_description.ServiceDescription): """The compute service.""" - valid_versions = [service_filter.ValidVersion('v2')] - - def __init__(self, version=None): - """Create a compute service.""" - super(ComputeService, self).__init__(service_type='compute', - version=version) + supported_versions = { + '2': _proxy.Proxy + } diff --git a/openstack/compute/v2/availability_zone.py b/openstack/compute/v2/availability_zone.py index 5727d4825..0238dd435 100644 --- a/openstack/compute/v2/availability_zone.py +++ b/openstack/compute/v2/availability_zone.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.compute import compute_service from openstack import resource @@ -18,8 +17,6 @@ class AvailabilityZone(resource.Resource): resources_key = 'availabilityZoneInfo' base_path = '/os-availability-zone' - service = compute_service.ComputeService() - # capabilities allow_list = True diff --git a/openstack/compute/v2/extension.py b/openstack/compute/v2/extension.py index a12f22efb..4b19b0934 100644 --- a/openstack/compute/v2/extension.py +++ b/openstack/compute/v2/extension.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.compute import compute_service from openstack import resource @@ -18,7 +17,6 @@ class Extension(resource.Resource): resource_key = 'extension' resources_key = 'extensions' base_path = '/extensions' - service = compute_service.ComputeService() id_attribute = "alias" # capabilities diff --git a/openstack/compute/v2/flavor.py b/openstack/compute/v2/flavor.py index 14920403a..7128e9d1c 100644 --- a/openstack/compute/v2/flavor.py +++ b/openstack/compute/v2/flavor.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.compute import compute_service from openstack import resource @@ -18,7 +17,6 @@ class Flavor(resource.Resource): resource_key = 'flavor' resources_key = 'flavors' base_path = '/flavors' - service = compute_service.ComputeService() # capabilities allow_create = True diff --git a/openstack/compute/v2/hypervisor.py b/openstack/compute/v2/hypervisor.py index 2f49f5a3c..50d5ae64c 100644 --- a/openstack/compute/v2/hypervisor.py +++ b/openstack/compute/v2/hypervisor.py @@ -11,7 +11,6 @@ # under the License. -from openstack.compute import compute_service from openstack import resource @@ -20,8 +19,6 @@ class Hypervisor(resource.Resource): resources_key = 'hypervisors' base_path = '/os-hypervisors' - service = compute_service.ComputeService() - # capabilities allow_fetch = True allow_list = True diff --git a/openstack/compute/v2/image.py b/openstack/compute/v2/image.py index 44c037a68..ed6a1682b 100644 --- a/openstack/compute/v2/image.py +++ b/openstack/compute/v2/image.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.compute import compute_service from openstack.compute.v2 import metadata from openstack import resource @@ -19,7 +18,6 @@ class Image(resource.Resource, metadata.MetadataMixin): resource_key = 'image' resources_key = 'images' base_path = '/images' - service = compute_service.ComputeService() # capabilities allow_fetch = True diff --git a/openstack/compute/v2/keypair.py b/openstack/compute/v2/keypair.py index a1fb16e16..51109632c 100644 --- a/openstack/compute/v2/keypair.py +++ b/openstack/compute/v2/keypair.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.compute import compute_service from openstack import resource @@ -18,7 +17,6 @@ class Keypair(resource.Resource): resource_key = 'keypair' resources_key = 'keypairs' base_path = '/os-keypairs' - service = compute_service.ComputeService() # capabilities allow_create = True diff --git a/openstack/compute/v2/limits.py b/openstack/compute/v2/limits.py index 1dc92bb15..b08b29219 100644 --- a/openstack/compute/v2/limits.py +++ b/openstack/compute/v2/limits.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.compute import compute_service from openstack import resource @@ -71,7 +70,6 @@ class RateLimit(resource.Resource): class Limits(resource.Resource): base_path = "/limits" resource_key = "limits" - service = compute_service.ComputeService() allow_fetch = True diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index f1b14771f..17b155853 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.compute import compute_service from openstack.compute.v2 import metadata from openstack import resource from openstack import utils @@ -20,7 +19,6 @@ class Server(resource.Resource, metadata.MetadataMixin): resource_key = 'server' resources_key = 'servers' base_path = '/servers' - service = compute_service.ComputeService() # capabilities allow_create = True @@ -189,7 +187,7 @@ def change_password(self, session, new_password): def get_password(self, session): """Get the encrypted administrator password.""" url = utils.urljoin(Server.base_path, self.id, 'os-server-password') - return session.get(url, endpoint_filter=self.service) + return session.get(url) def reboot(self, session, reboot_type): """Reboot server where reboot_type might be 'SOFT' or 'HARD'.""" diff --git a/openstack/compute/v2/server_group.py b/openstack/compute/v2/server_group.py index 18a211358..4e3e4a4ba 100644 --- a/openstack/compute/v2/server_group.py +++ b/openstack/compute/v2/server_group.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.compute import compute_service from openstack import resource @@ -18,7 +17,6 @@ class ServerGroup(resource.Resource): resource_key = 'server_group' resources_key = 'server_groups' base_path = '/os-server-groups' - service = compute_service.ComputeService() _query_mapping = resource.QueryParameters("all_projects") diff --git a/openstack/compute/v2/server_interface.py b/openstack/compute/v2/server_interface.py index 0a2750fc2..f12213c54 100644 --- a/openstack/compute/v2/server_interface.py +++ b/openstack/compute/v2/server_interface.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.compute import compute_service from openstack import resource @@ -18,7 +17,6 @@ class ServerInterface(resource.Resource): resource_key = 'interfaceAttachment' resources_key = 'interfaceAttachments' base_path = '/servers/%(server_id)s/os-interface' - service = compute_service.ComputeService() # capabilities allow_create = True diff --git a/openstack/compute/v2/server_ip.py b/openstack/compute/v2/server_ip.py index 4c797a68a..bc868fb6c 100644 --- a/openstack/compute/v2/server_ip.py +++ b/openstack/compute/v2/server_ip.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.compute import compute_service from openstack import resource from openstack import utils @@ -18,7 +17,6 @@ class ServerIP(resource.Resource): resources_key = 'addresses' base_path = '/servers/%(server_id)s/ips' - service = compute_service.ComputeService() # capabilities allow_list = True diff --git a/openstack/compute/v2/service.py b/openstack/compute/v2/service.py index 78d0735c6..f13cac1c4 100644 --- a/openstack/compute/v2/service.py +++ b/openstack/compute/v2/service.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.compute import compute_service from openstack import resource from openstack import utils @@ -20,8 +19,6 @@ class Service(resource.Resource): resources_key = 'services' base_path = '/os-services' - service = compute_service.ComputeService() - # capabilities allow_list = True allow_commit = True diff --git a/openstack/compute/v2/volume_attachment.py b/openstack/compute/v2/volume_attachment.py index 982ae42b0..750a62a58 100644 --- a/openstack/compute/v2/volume_attachment.py +++ b/openstack/compute/v2/volume_attachment.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.compute import compute_service from openstack import resource @@ -18,7 +17,6 @@ class VolumeAttachment(resource.Resource): resource_key = 'volumeAttachment' resources_key = 'volumeAttachments' base_path = '/servers/%(server_id)s/os-volume_attachments' - service = compute_service.ComputeService() # capabilities allow_create = True diff --git a/openstack/compute/version.py b/openstack/compute/version.py index d1230260a..f78d3addd 100644 --- a/openstack/compute/version.py +++ b/openstack/compute/version.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.compute import compute_service from openstack import resource @@ -18,9 +17,6 @@ class Version(resource.Resource): resource_key = 'version' resources_key = 'versions' base_path = '/' - service = compute_service.ComputeService( - version=compute_service.ComputeService.UNVERSIONED - ) # capabilities allow_list = True diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 24b9ab789..32902bcf7 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -252,7 +252,13 @@ def get_interface(self, service_type=None): 'interface', service_type, fallback_to_unprefixed=True) def get_api_version(self, service_type): - return self._get_config('api_version', service_type) + version = self._get_config('api_version', service_type) + if version: + try: + float(version) + except ValueError: + return None + return version def get_default_microversion(self, service_type): return self._get_config('default_microversion', service_type) @@ -412,6 +418,20 @@ def _get_version_request(self, service_type, version): return version_request + def get_all_version_data(self, service_type): + # Seriously. Don't think about the existential crisis + # that is the next line. You'll wind up in cthulhu's lair. + service_type = self.get_service_type(service_type) + versions = self.get_session().get_all_version_data( + service_type=service_type, + interface=self.get_interface(service_type), + region_name=self.region_name, + ) + region_versions = versions.get(self.region_name, {}) + interface_versions = region_versions.get( + self.get_interface(service_type), {}) + return interface_versions.get(service_type, []) + def get_session_client( self, service_type, version=None, constructor=adapter.Adapter, **kwargs): @@ -436,8 +456,10 @@ def get_session_client( self.get_status_code_retries(service_type)) endpoint_override = self.get_endpoint(service_type) version = version_request.version - min_api_version = version_request.min_api_version - max_api_version = version_request.max_api_version + min_api_version = ( + kwargs.pop('min_version', None) or version_request.min_api_version) + max_api_version = ( + kwargs.pop('max_version', None) or version_request.max_api_version) # Older neutron has inaccessible discovery document. Nobody noticed # because neutronclient hard-codes an append of v2.0. YAY! if service_type == 'network': diff --git a/openstack/database/database_service.py b/openstack/database/database_service.py index ced980534..e37f45715 100644 --- a/openstack/database/database_service.py +++ b/openstack/database/database_service.py @@ -10,15 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import service_filter +from openstack.database.v1 import _proxy +from openstack import service_description -class DatabaseService(service_filter.ServiceFilter): +class DatabaseService(service_description.ServiceDescription): """The database service.""" - valid_versions = [service_filter.ValidVersion('v1')] - - def __init__(self, version=None): - """Create a database service.""" - super(DatabaseService, self).__init__(service_type='database', - version=version) + supported_versions = { + '1': _proxy.Proxy, + } diff --git a/openstack/database/v1/database.py b/openstack/database/v1/database.py index be46be803..7bf8eef8a 100644 --- a/openstack/database/v1/database.py +++ b/openstack/database/v1/database.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.database import database_service from openstack import resource @@ -18,7 +17,6 @@ class Database(resource.Resource): resource_key = 'database' resources_key = 'databases' base_path = '/instances/%(instance_id)s/databases' - service = database_service.DatabaseService() # capabilities allow_create = True diff --git a/openstack/database/v1/flavor.py b/openstack/database/v1/flavor.py index 10f78e3cc..64a9dd362 100644 --- a/openstack/database/v1/flavor.py +++ b/openstack/database/v1/flavor.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.database import database_service from openstack import resource @@ -18,7 +17,6 @@ class Flavor(resource.Resource): resource_key = 'flavor' resources_key = 'flavors' base_path = '/flavors' - service = database_service.DatabaseService() # capabilities allow_list = True diff --git a/openstack/database/v1/instance.py b/openstack/database/v1/instance.py index b0b8424c6..02c033bae 100644 --- a/openstack/database/v1/instance.py +++ b/openstack/database/v1/instance.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.database import database_service from openstack import resource from openstack import utils @@ -19,7 +18,6 @@ class Instance(resource.Resource): resource_key = 'instance' resources_key = 'instances' base_path = '/instances' - service = database_service.DatabaseService() # capabilities allow_create = True diff --git a/openstack/database/v1/user.py b/openstack/database/v1/user.py index 2055544b9..c3a0d3d6e 100644 --- a/openstack/database/v1/user.py +++ b/openstack/database/v1/user.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.database import database_service from openstack import resource from openstack import utils @@ -19,7 +18,6 @@ class User(resource.Resource): resource_key = 'user' resources_key = 'users' base_path = '/instances/%(instance_id)s/users' - service = database_service.DatabaseService() # capabilities allow_create = True diff --git a/openstack/identity/identity_service.py b/openstack/identity/identity_service.py index 4bcfafa5b..df8051170 100644 --- a/openstack/identity/identity_service.py +++ b/openstack/identity/identity_service.py @@ -10,25 +10,15 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import service_filter +from openstack.identity.v2 import _proxy as _proxy_v2 +from openstack.identity.v3 import _proxy as _proxy_v3 +from openstack import service_description -class IdentityService(service_filter.ServiceFilter): +class IdentityService(service_description.ServiceDescription): """The identity service.""" - valid_versions = [ - service_filter.ValidVersion('v3'), - service_filter.ValidVersion('v2'), - ] - - def __init__(self, **kwargs): - """Create an identity service.""" - kwargs['service_type'] = 'identity' - super(IdentityService, self).__init__(**kwargs) - - -class AdminService(IdentityService): - - def __init__(self, **kwargs): - kwargs['interface'] = service_filter.ServiceFilter.ADMIN - super(AdminService, self).__init__(**kwargs) + supported_versions = { + '2': _proxy_v2.Proxy, + '3': _proxy_v3.Proxy, + } diff --git a/openstack/identity/v2/extension.py b/openstack/identity/v2/extension.py index 7f05fb747..153d6df91 100644 --- a/openstack/identity/v2/extension.py +++ b/openstack/identity/v2/extension.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.identity import identity_service from openstack import resource @@ -18,7 +17,6 @@ class Extension(resource.Resource): resource_key = 'extension' resources_key = 'extensions' base_path = '/extensions' - service = identity_service.IdentityService() # capabilities allow_list = True diff --git a/openstack/identity/v2/role.py b/openstack/identity/v2/role.py index 625c1413b..df27e5596 100644 --- a/openstack/identity/v2/role.py +++ b/openstack/identity/v2/role.py @@ -11,7 +11,6 @@ # under the License. from openstack import format -from openstack.identity import identity_service from openstack import resource @@ -19,7 +18,6 @@ class Role(resource.Resource): resource_key = 'role' resources_key = 'roles' base_path = '/OS-KSADM/roles' - service = identity_service.IdentityService() # capabilities allow_create = True diff --git a/openstack/identity/v2/tenant.py b/openstack/identity/v2/tenant.py index 66a54aad5..1ea272ffa 100644 --- a/openstack/identity/v2/tenant.py +++ b/openstack/identity/v2/tenant.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.identity import identity_service from openstack import resource @@ -18,7 +17,6 @@ class Tenant(resource.Resource): resource_key = 'tenant' resources_key = 'tenants' base_path = '/tenants' - service = identity_service.AdminService() # capabilities allow_create = True diff --git a/openstack/identity/v2/user.py b/openstack/identity/v2/user.py index a8f219e92..7b4d893a5 100644 --- a/openstack/identity/v2/user.py +++ b/openstack/identity/v2/user.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.identity import identity_service from openstack import resource @@ -18,7 +17,6 @@ class User(resource.Resource): resource_key = 'user' resources_key = 'users' base_path = '/users' - service = identity_service.AdminService() # capabilities allow_create = True diff --git a/openstack/identity/v3/credential.py b/openstack/identity/v3/credential.py index 2b2bf0c0b..c388dbf96 100644 --- a/openstack/identity/v3/credential.py +++ b/openstack/identity/v3/credential.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.identity import identity_service from openstack import resource @@ -18,7 +17,6 @@ class Credential(resource.Resource): resource_key = 'credential' resources_key = 'credentials' base_path = '/credentials' - service = identity_service.IdentityService() # capabilities allow_create = True diff --git a/openstack/identity/v3/domain.py b/openstack/identity/v3/domain.py index 6c32a5c4c..8a39d0a8b 100644 --- a/openstack/identity/v3/domain.py +++ b/openstack/identity/v3/domain.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.identity import identity_service from openstack import resource from openstack import utils @@ -19,7 +18,6 @@ class Domain(resource.Resource): resource_key = 'domain' resources_key = 'domains' base_path = '/domains' - service = identity_service.IdentityService() # capabilities allow_create = True diff --git a/openstack/identity/v3/endpoint.py b/openstack/identity/v3/endpoint.py index 841f5c59a..e18b17a4b 100644 --- a/openstack/identity/v3/endpoint.py +++ b/openstack/identity/v3/endpoint.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.identity import identity_service from openstack import resource @@ -18,7 +17,6 @@ class Endpoint(resource.Resource): resource_key = 'endpoint' resources_key = 'endpoints' base_path = '/endpoints' - service = identity_service.IdentityService() # capabilities allow_create = True diff --git a/openstack/identity/v3/group.py b/openstack/identity/v3/group.py index acf763006..001f5124c 100644 --- a/openstack/identity/v3/group.py +++ b/openstack/identity/v3/group.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.identity import identity_service from openstack import resource @@ -18,7 +17,6 @@ class Group(resource.Resource): resource_key = 'group' resources_key = 'groups' base_path = '/groups' - service = identity_service.IdentityService() # capabilities allow_create = True diff --git a/openstack/identity/v3/policy.py b/openstack/identity/v3/policy.py index a4a6f7cfd..35d3bbcb5 100644 --- a/openstack/identity/v3/policy.py +++ b/openstack/identity/v3/policy.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.identity import identity_service from openstack import resource @@ -18,7 +17,6 @@ class Policy(resource.Resource): resource_key = 'policy' resources_key = 'policies' base_path = '/policies' - service = identity_service.IdentityService() # capabilities allow_create = True diff --git a/openstack/identity/v3/project.py b/openstack/identity/v3/project.py index 56ee7dab2..7f11cae17 100644 --- a/openstack/identity/v3/project.py +++ b/openstack/identity/v3/project.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.identity import identity_service from openstack import resource from openstack import utils @@ -19,7 +18,6 @@ class Project(resource.Resource): resource_key = 'project' resources_key = 'projects' base_path = '/projects' - service = identity_service.IdentityService() # capabilities allow_create = True @@ -119,7 +117,6 @@ class UserProject(Project): resource_key = 'project' resources_key = 'projects' base_path = '/users/%(user_id)s/projects' - service = identity_service.IdentityService() # capabilities allow_create = False diff --git a/openstack/identity/v3/region.py b/openstack/identity/v3/region.py index fc0e44e73..33db63c8f 100644 --- a/openstack/identity/v3/region.py +++ b/openstack/identity/v3/region.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.identity import identity_service from openstack import resource @@ -18,7 +17,6 @@ class Region(resource.Resource): resource_key = 'region' resources_key = 'regions' base_path = '/regions' - service = identity_service.IdentityService() # capabilities allow_create = True diff --git a/openstack/identity/v3/role.py b/openstack/identity/v3/role.py index cd8e272e7..cb3518bbe 100644 --- a/openstack/identity/v3/role.py +++ b/openstack/identity/v3/role.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.identity import identity_service from openstack import resource @@ -18,7 +17,6 @@ class Role(resource.Resource): resource_key = 'role' resources_key = 'roles' base_path = '/roles' - service = identity_service.IdentityService() # capabilities allow_create = True diff --git a/openstack/identity/v3/role_assignment.py b/openstack/identity/v3/role_assignment.py index b047c9ddc..a12cce27a 100644 --- a/openstack/identity/v3/role_assignment.py +++ b/openstack/identity/v3/role_assignment.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.identity import identity_service from openstack import resource @@ -18,7 +17,6 @@ class RoleAssignment(resource.Resource): resource_key = 'role_assignment' resources_key = 'role_assignments' base_path = '/role_assignments' - service = identity_service.IdentityService() # capabilities allow_list = True diff --git a/openstack/identity/v3/role_domain_group_assignment.py b/openstack/identity/v3/role_domain_group_assignment.py index 27b6963cb..e65ef8c9e 100644 --- a/openstack/identity/v3/role_domain_group_assignment.py +++ b/openstack/identity/v3/role_domain_group_assignment.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.identity import identity_service from openstack import resource @@ -18,7 +17,6 @@ class RoleDomainGroupAssignment(resource.Resource): resource_key = 'role' resources_key = 'roles' base_path = '/domains/%(domain_id)s/groups/%(group_id)s/roles' - service = identity_service.IdentityService() # capabilities allow_list = True diff --git a/openstack/identity/v3/role_domain_user_assignment.py b/openstack/identity/v3/role_domain_user_assignment.py index 085a77e0f..242932cab 100644 --- a/openstack/identity/v3/role_domain_user_assignment.py +++ b/openstack/identity/v3/role_domain_user_assignment.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.identity import identity_service from openstack import resource @@ -18,7 +17,6 @@ class RoleDomainUserAssignment(resource.Resource): resource_key = 'role' resources_key = 'roles' base_path = '/domains/%(domain_id)s/users/%(user_id)s/roles' - service = identity_service.IdentityService() # capabilities allow_list = True diff --git a/openstack/identity/v3/role_project_group_assignment.py b/openstack/identity/v3/role_project_group_assignment.py index cb48025c1..95f2f1863 100644 --- a/openstack/identity/v3/role_project_group_assignment.py +++ b/openstack/identity/v3/role_project_group_assignment.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.identity import identity_service from openstack import resource @@ -18,7 +17,6 @@ class RoleProjectGroupAssignment(resource.Resource): resource_key = 'role' resources_key = 'roles' base_path = '/projects/%(project_id)s/groups/%(group_id)s/roles' - service = identity_service.IdentityService() # capabilities allow_list = True diff --git a/openstack/identity/v3/role_project_user_assignment.py b/openstack/identity/v3/role_project_user_assignment.py index a7e887099..c5c99bdf2 100644 --- a/openstack/identity/v3/role_project_user_assignment.py +++ b/openstack/identity/v3/role_project_user_assignment.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.identity import identity_service from openstack import resource @@ -18,7 +17,6 @@ class RoleProjectUserAssignment(resource.Resource): resource_key = 'role' resources_key = 'roles' base_path = '/projects/%(project_id)s/users/%(user_id)s/roles' - service = identity_service.IdentityService() # capabilities allow_list = True diff --git a/openstack/identity/v3/service.py b/openstack/identity/v3/service.py index 6445fb759..982254ad4 100644 --- a/openstack/identity/v3/service.py +++ b/openstack/identity/v3/service.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.identity import identity_service from openstack import resource @@ -18,7 +17,6 @@ class Service(resource.Resource): resource_key = 'service' resources_key = 'services' base_path = '/services' - service = identity_service.IdentityService() # capabilities allow_create = True diff --git a/openstack/identity/v3/trust.py b/openstack/identity/v3/trust.py index 07985f1b4..66aaa4422 100644 --- a/openstack/identity/v3/trust.py +++ b/openstack/identity/v3/trust.py @@ -11,7 +11,6 @@ # under the License. -from openstack.identity import identity_service from openstack import resource @@ -19,7 +18,6 @@ class Trust(resource.Resource): resource_key = 'trust' resources_key = 'trusts' base_path = '/OS-TRUST/trusts' - service = identity_service.IdentityService() # capabilities allow_create = True diff --git a/openstack/identity/v3/user.py b/openstack/identity/v3/user.py index bf1b34615..609c9f652 100644 --- a/openstack/identity/v3/user.py +++ b/openstack/identity/v3/user.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.identity import identity_service from openstack import resource @@ -18,7 +17,6 @@ class User(resource.Resource): resource_key = 'user' resources_key = 'users' base_path = '/users' - service = identity_service.IdentityService() # capabilities allow_create = True diff --git a/openstack/identity/version.py b/openstack/identity/version.py index 30992f109..3ad27ee86 100644 --- a/openstack/identity/version.py +++ b/openstack/identity/version.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.identity import identity_service from openstack import resource @@ -18,9 +17,6 @@ class Version(resource.Resource): resource_key = 'version' resources_key = 'versions' base_path = '/' - service = identity_service.IdentityService( - version=identity_service.IdentityService.UNVERSIONED - ) # capabilities allow_list = True diff --git a/openstack/image/image_service.py b/openstack/image/image_service.py index 55c43fd5f..b091a0100 100644 --- a/openstack/image/image_service.py +++ b/openstack/image/image_service.py @@ -10,18 +10,15 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import service_filter +from openstack.image.v1 import _proxy as _proxy_v1 +from openstack.image.v2 import _proxy as _proxy_v2 +from openstack import service_description -class ImageService(service_filter.ServiceFilter): +class ImageService(service_description.ServiceDescription): """The image service.""" - valid_versions = [ - service_filter.ValidVersion('v2'), - service_filter.ValidVersion('v1') - ] - - def __init__(self, version=None): - """Create an image service.""" - super(ImageService, self).__init__(service_type='image', - version=version) + supported_versions = { + '1': _proxy_v1.Proxy, + '2': _proxy_v2.Proxy, + } diff --git a/openstack/image/v1/image.py b/openstack/image/v1/image.py index 3f93a51df..bd060146c 100644 --- a/openstack/image/v1/image.py +++ b/openstack/image/v1/image.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.image import image_service from openstack import resource @@ -18,7 +17,6 @@ class Image(resource.Resource): resource_key = 'image' resources_key = 'images' base_path = '/images' - service = image_service.ImageService() # capabilities allow_create = True diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index 7be9e6816..271f82d98 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -14,7 +14,6 @@ from openstack import _log from openstack import exceptions -from openstack.image import image_service from openstack import resource from openstack import utils @@ -24,7 +23,6 @@ class Image(resource.Resource): resources_key = 'images' base_path = '/images' - service = image_service.ImageService() # capabilities allow_create = True diff --git a/openstack/image/v2/member.py b/openstack/image/v2/member.py index 6d565b447..b9568545d 100644 --- a/openstack/image/v2/member.py +++ b/openstack/image/v2/member.py @@ -10,14 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.image import image_service from openstack import resource class Member(resource.Resource): resources_key = 'members' base_path = '/images/%(image_id)s/members' - service = image_service.ImageService() # capabilities allow_create = True diff --git a/openstack/instance_ha/instance_ha_service.py b/openstack/instance_ha/instance_ha_service.py index 228d9933a..89a1d9a22 100644 --- a/openstack/instance_ha/instance_ha_service.py +++ b/openstack/instance_ha/instance_ha_service.py @@ -12,15 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -from openstack import service_filter +from openstack.instance_ha.v1 import _proxy +from openstack import service_description -class InstanceHaService(service_filter.ServiceFilter): +class InstanceHaService(service_description.ServiceDescription): """The HA service.""" - valid_versions = [service_filter.ValidVersion('v1')] - - def __init__(self, version=None): - """Create an ha service.""" - super(InstanceHaService, self).__init__(service_type='ha', - version=version) + supported_versions = { + '1': _proxy.Proxy, + } diff --git a/openstack/instance_ha/v1/host.py b/openstack/instance_ha/v1/host.py index a6d9a0142..a505a648d 100644 --- a/openstack/instance_ha/v1/host.py +++ b/openstack/instance_ha/v1/host.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from openstack.instance_ha import instance_ha_service from openstack import resource @@ -20,7 +19,6 @@ class Host(resource.Resource): resource_key = "host" resources_key = "hosts" base_path = "/segments/%(segment_id)s/hosts" - service = instance_ha_service.InstanceHaService() # capabilities # 1] GET /v1/segments//hosts diff --git a/openstack/instance_ha/v1/notification.py b/openstack/instance_ha/v1/notification.py index 45f1a2e27..4cf538861 100644 --- a/openstack/instance_ha/v1/notification.py +++ b/openstack/instance_ha/v1/notification.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from openstack.instance_ha import instance_ha_service from openstack import resource @@ -20,7 +19,6 @@ class Notification(resource.Resource): resource_key = "notification" resources_key = "notifications" base_path = "/notifications" - service = instance_ha_service.InstanceHaService() # capabilities # 1] GET /v1/notifications diff --git a/openstack/instance_ha/v1/segment.py b/openstack/instance_ha/v1/segment.py index eec2c6c13..a2668f71c 100644 --- a/openstack/instance_ha/v1/segment.py +++ b/openstack/instance_ha/v1/segment.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from openstack.instance_ha import instance_ha_service from openstack import resource @@ -20,7 +19,6 @@ class Segment(resource.Resource): resource_key = "segment" resources_key = "segments" base_path = "/segments" - service = instance_ha_service.InstanceHaService() # capabilities # 1] GET /v1/segments diff --git a/openstack/key_manager/key_manager_service.py b/openstack/key_manager/key_manager_service.py index 36d3b413b..64d76d210 100644 --- a/openstack/key_manager/key_manager_service.py +++ b/openstack/key_manager/key_manager_service.py @@ -10,15 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import service_filter +from openstack.key_manager.v1 import _proxy +from openstack import service_description -class KeyManagerService(service_filter.ServiceFilter): +class KeyManagerService(service_description.ServiceDescription): """The key manager service.""" - valid_versions = [service_filter.ValidVersion('v1')] - - def __init__(self, version=None): - """Create a key manager service.""" - super(KeyManagerService, self).__init__(service_type='key-manager', - version=version) + supported_versions = { + '1': _proxy.Proxy, + } diff --git a/openstack/key_manager/v1/container.py b/openstack/key_manager/v1/container.py index e5b2629c6..4b14020ab 100644 --- a/openstack/key_manager/v1/container.py +++ b/openstack/key_manager/v1/container.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.key_manager import key_manager_service from openstack.key_manager.v1 import _format from openstack import resource @@ -18,7 +17,6 @@ class Container(resource.Resource): resources_key = 'containers' base_path = '/containers' - service = key_manager_service.KeyManagerService() # capabilities allow_create = True diff --git a/openstack/key_manager/v1/order.py b/openstack/key_manager/v1/order.py index fec0592f2..1f7198c4e 100644 --- a/openstack/key_manager/v1/order.py +++ b/openstack/key_manager/v1/order.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.key_manager import key_manager_service from openstack.key_manager.v1 import _format from openstack import resource @@ -18,7 +17,6 @@ class Order(resource.Resource): resources_key = 'orders' base_path = '/orders' - service = key_manager_service.KeyManagerService() # capabilities allow_create = True diff --git a/openstack/key_manager/v1/secret.py b/openstack/key_manager/v1/secret.py index 2ba4ab552..4e2be6c46 100644 --- a/openstack/key_manager/v1/secret.py +++ b/openstack/key_manager/v1/secret.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.key_manager import key_manager_service from openstack.key_manager.v1 import _format from openstack import resource from openstack import utils @@ -19,7 +18,6 @@ class Secret(resource.Resource): resources_key = 'secrets' base_path = '/secrets' - service = key_manager_service.KeyManagerService() # capabilities allow_create = True diff --git a/openstack/load_balancer/load_balancer_service.py b/openstack/load_balancer/load_balancer_service.py index 3c645e6f6..31542a317 100644 --- a/openstack/load_balancer/load_balancer_service.py +++ b/openstack/load_balancer/load_balancer_service.py @@ -10,17 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import service_filter +from openstack.load_balancer.v2 import _proxy +from openstack import service_description -class LoadBalancerService(service_filter.ServiceFilter): +class LoadBalancerService(service_description.ServiceDescription): """The load balancer service.""" - valid_versions = [service_filter.ValidVersion('v2', 'v2.0')] - - def __init__(self, version=None): - """Create a load balancer service.""" - super(LoadBalancerService, self).__init__( - service_type='load-balancer', - version=version - ) + supported_versions = { + '2': _proxy.Proxy, + } diff --git a/openstack/load_balancer/v2/health_monitor.py b/openstack/load_balancer/v2/health_monitor.py index aca7fcb5d..4cca0262d 100644 --- a/openstack/load_balancer/v2/health_monitor.py +++ b/openstack/load_balancer/v2/health_monitor.py @@ -10,15 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.load_balancer import load_balancer_service as lb_service from openstack import resource class HealthMonitor(resource.Resource): resource_key = 'healthmonitor' resources_key = 'healthmonitors' - base_path = '/v2.0/lbaas/healthmonitors' - service = lb_service.LoadBalancerService() + base_path = '/lbaas/healthmonitors' # capabilities allow_create = True diff --git a/openstack/load_balancer/v2/l7_policy.py b/openstack/load_balancer/v2/l7_policy.py index 37e434a69..099b176da 100644 --- a/openstack/load_balancer/v2/l7_policy.py +++ b/openstack/load_balancer/v2/l7_policy.py @@ -10,15 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.load_balancer import load_balancer_service as lb_service from openstack import resource class L7Policy(resource.Resource): resource_key = 'l7policy' resources_key = 'l7policies' - base_path = '/v2.0/lbaas/l7policies' - service = lb_service.LoadBalancerService() + base_path = '/lbaas/l7policies' # capabilities allow_create = True diff --git a/openstack/load_balancer/v2/l7_rule.py b/openstack/load_balancer/v2/l7_rule.py index cc2aa4364..be5c118f5 100644 --- a/openstack/load_balancer/v2/l7_rule.py +++ b/openstack/load_balancer/v2/l7_rule.py @@ -10,15 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.load_balancer import load_balancer_service as lb_service from openstack import resource class L7Rule(resource.Resource): resource_key = 'rule' resources_key = 'rules' - base_path = '/v2.0/lbaas/l7policies/%(l7policy_id)s/rules' - service = lb_service.LoadBalancerService() + base_path = '/lbaas/l7policies/%(l7policy_id)s/rules' # capabilities allow_create = True diff --git a/openstack/load_balancer/v2/listener.py b/openstack/load_balancer/v2/listener.py index 6597c8998..6ea5d74bb 100644 --- a/openstack/load_balancer/v2/listener.py +++ b/openstack/load_balancer/v2/listener.py @@ -10,15 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.load_balancer import load_balancer_service as lb_service from openstack import resource class Listener(resource.Resource): resource_key = 'listener' resources_key = 'listeners' - base_path = '/v2.0/lbaas/listeners' - service = lb_service.LoadBalancerService() + base_path = '/lbaas/listeners' # capabilities allow_create = True diff --git a/openstack/load_balancer/v2/load_balancer.py b/openstack/load_balancer/v2/load_balancer.py index a3ce64999..9b36f7e5f 100644 --- a/openstack/load_balancer/v2/load_balancer.py +++ b/openstack/load_balancer/v2/load_balancer.py @@ -10,15 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.load_balancer import load_balancer_service as lb_service from openstack import resource class LoadBalancer(resource.Resource): resource_key = 'loadbalancer' resources_key = 'loadbalancers' - base_path = '/v2.0/lbaas/loadbalancers' - service = lb_service.LoadBalancerService() + base_path = '/lbaas/loadbalancers' # capabilities allow_create = True diff --git a/openstack/load_balancer/v2/member.py b/openstack/load_balancer/v2/member.py index ac65eb268..f67ad3b74 100644 --- a/openstack/load_balancer/v2/member.py +++ b/openstack/load_balancer/v2/member.py @@ -10,15 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.load_balancer import load_balancer_service as lb_service from openstack import resource class Member(resource.Resource): resource_key = 'member' resources_key = 'members' - base_path = '/v2.0/lbaas/pools/%(pool_id)s/members' - service = lb_service.LoadBalancerService() + base_path = '/lbaas/pools/%(pool_id)s/members' # capabilities allow_create = True diff --git a/openstack/load_balancer/v2/pool.py b/openstack/load_balancer/v2/pool.py index 1868d208c..9bbc13bf0 100644 --- a/openstack/load_balancer/v2/pool.py +++ b/openstack/load_balancer/v2/pool.py @@ -10,15 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.load_balancer import load_balancer_service as lb_service from openstack import resource class Pool(resource.Resource): resource_key = 'pool' resources_key = 'pools' - base_path = '/v2.0/lbaas/pools' - service = lb_service.LoadBalancerService() + base_path = '/lbaas/pools' # capabilities allow_create = True diff --git a/openstack/load_balancer/version.py b/openstack/load_balancer/version.py index c2266a2b0..bb0891768 100644 --- a/openstack/load_balancer/version.py +++ b/openstack/load_balancer/version.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.load_balancer import load_balancer_service as lb_service from openstack import resource @@ -18,9 +17,6 @@ class Version(resource.Resource): resource_key = 'version' resources_key = 'versions' base_path = '/' - service = lb_service.LoadBalancerService( - version=lb_service.LoadBalancerService.UNVERSIONED - ) # capabilities allow_list = True diff --git a/openstack/message/message_service.py b/openstack/message/message_service.py index c9f57ad60..204d391c1 100644 --- a/openstack/message/message_service.py +++ b/openstack/message/message_service.py @@ -10,17 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import service_filter +from openstack.message.v2 import _proxy +from openstack import service_description -class MessageService(service_filter.ServiceFilter): +class MessageService(service_description.ServiceDescription): """The message service.""" - valid_versions = [service_filter.ValidVersion('v2')] - - def __init__(self, version=None): - """Create a message service.""" - super(MessageService, self).__init__( - service_type='messaging', - version=version - ) + supported_versions = { + '2': _proxy.Proxy, + } diff --git a/openstack/message/v2/claim.py b/openstack/message/v2/claim.py index 51c1c0a14..f02e51748 100644 --- a/openstack/message/v2/claim.py +++ b/openstack/message/v2/claim.py @@ -12,7 +12,6 @@ import uuid -from openstack.message import message_service from openstack import resource @@ -24,7 +23,6 @@ class Claim(resource.Resource): resources_key = 'claims' base_path = '/queues/%(queue_name)s/claims' - service = message_service.MessageService() # capabilities allow_create = True diff --git a/openstack/message/v2/message.py b/openstack/message/v2/message.py index d85f5d2b5..5f7acb090 100644 --- a/openstack/message/v2/message.py +++ b/openstack/message/v2/message.py @@ -12,7 +12,6 @@ import uuid -from openstack.message import message_service from openstack import resource @@ -24,7 +23,6 @@ class Message(resource.Resource): resources_key = 'messages' base_path = '/queues/%(queue_name)s/messages' - service = message_service.MessageService() # capabilities allow_create = True diff --git a/openstack/message/v2/queue.py b/openstack/message/v2/queue.py index 2da9e87c9..7901f1959 100644 --- a/openstack/message/v2/queue.py +++ b/openstack/message/v2/queue.py @@ -12,7 +12,6 @@ import uuid -from openstack.message import message_service from openstack import resource @@ -24,7 +23,6 @@ class Queue(resource.Resource): resources_key = "queues" base_path = "/queues" - service = message_service.MessageService() # capabilities allow_create = True diff --git a/openstack/message/v2/subscription.py b/openstack/message/v2/subscription.py index 2bd779711..7372951b8 100644 --- a/openstack/message/v2/subscription.py +++ b/openstack/message/v2/subscription.py @@ -12,7 +12,6 @@ import uuid -from openstack.message import message_service from openstack import resource @@ -24,7 +23,6 @@ class Subscription(resource.Resource): resources_key = 'subscriptions' base_path = '/queues/%(queue_name)s/subscriptions' - service = message_service.MessageService() # capabilities allow_create = True diff --git a/openstack/message/version.py b/openstack/message/version.py index 6cb2ee51d..805ce6a34 100644 --- a/openstack/message/version.py +++ b/openstack/message/version.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.message import message_service from openstack import resource @@ -18,9 +17,6 @@ class Version(resource.Resource): resource_key = 'version' resources_key = 'versions' base_path = '/' - service = message_service.MessageService( - version=message_service.MessageService.UNVERSIONED - ) # capabilities allow_list = True diff --git a/openstack/network/network_service.py b/openstack/network/network_service.py index 28c20e4b8..17070efed 100644 --- a/openstack/network/network_service.py +++ b/openstack/network/network_service.py @@ -10,15 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import service_filter +from openstack.network.v2 import _proxy +from openstack import service_description -class NetworkService(service_filter.ServiceFilter): +class NetworkService(service_description.ServiceDescription): """The network service.""" - valid_versions = [service_filter.ValidVersion('v2', 'v2.0')] - - def __init__(self, version=None): - """Create a network service.""" - super(NetworkService, self).__init__(service_type='network', - version=version) + supported_versions = { + '2': _proxy.Proxy, + } diff --git a/openstack/network/v2/address_scope.py b/openstack/network/v2/address_scope.py index 326cf2035..ff256e035 100644 --- a/openstack/network/v2/address_scope.py +++ b/openstack/network/v2/address_scope.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack import resource @@ -19,7 +18,6 @@ class AddressScope(resource.Resource): resource_key = 'address_scope' resources_key = 'address_scopes' base_path = '/address-scopes' - service = network_service.NetworkService() # capabilities allow_create = True diff --git a/openstack/network/v2/agent.py b/openstack/network/v2/agent.py index 6df7af057..b6a6097fe 100644 --- a/openstack/network/v2/agent.py +++ b/openstack/network/v2/agent.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack import resource from openstack import utils @@ -20,7 +19,6 @@ class Agent(resource.Resource): resource_key = 'agent' resources_key = 'agents' base_path = '/agents' - service = network_service.NetworkService() # capabilities allow_create = False @@ -96,7 +94,6 @@ class NetworkHostingDHCPAgent(Agent): resources_key = 'agents' resource_name = 'dhcp-agent' base_path = '/networks/%(network_id)s/dhcp-agents' - service = network_service.NetworkService() # capabilities allow_create = False @@ -113,7 +110,6 @@ class RouterL3Agent(Agent): resources_key = 'agents' base_path = '/routers/%(router_id)s/l3-agents' resource_name = 'l3-agent' - service = network_service.NetworkService() # capabilities allow_create = False diff --git a/openstack/network/v2/auto_allocated_topology.py b/openstack/network/v2/auto_allocated_topology.py index 98b2d9fbe..66ea343e9 100644 --- a/openstack/network/v2/auto_allocated_topology.py +++ b/openstack/network/v2/auto_allocated_topology.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack import resource @@ -18,7 +17,6 @@ class AutoAllocatedTopology(resource.Resource): resource_name = 'auto_allocated_topology' resource_key = 'auto_allocated_topology' base_path = '/auto-allocated-topology' - service = network_service.NetworkService() # Capabilities allow_create = False diff --git a/openstack/network/v2/availability_zone.py b/openstack/network/v2/availability_zone.py index 295ac42ac..9ff48135e 100644 --- a/openstack/network/v2/availability_zone.py +++ b/openstack/network/v2/availability_zone.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack import resource as _resource @@ -18,7 +17,6 @@ class AvailabilityZone(_resource.Resource): resource_key = 'availability_zone' resources_key = 'availability_zones' base_path = '/availability_zones' - service = network_service.NetworkService() # capabilities allow_create = False diff --git a/openstack/network/v2/extension.py b/openstack/network/v2/extension.py index d13c806b2..00c91a7f8 100644 --- a/openstack/network/v2/extension.py +++ b/openstack/network/v2/extension.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack import resource @@ -18,7 +17,6 @@ class Extension(resource.Resource): resource_key = 'extension' resources_key = 'extensions' base_path = '/extensions' - service = network_service.NetworkService() # capabilities allow_fetch = True diff --git a/openstack/network/v2/firewall_group.py b/openstack/network/v2/firewall_group.py index aff20e747..2ed0ad2d5 100644 --- a/openstack/network/v2/firewall_group.py +++ b/openstack/network/v2/firewall_group.py @@ -13,7 +13,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack import resource @@ -21,7 +20,6 @@ class FirewallGroup(resource.Resource): resource_key = 'firewall_group' resources_key = 'firewall_groups' base_path = '/fwaas/firewall_groups' - service = network_service.NetworkService() # capabilities allow_create = True diff --git a/openstack/network/v2/firewall_policy.py b/openstack/network/v2/firewall_policy.py index e94753027..aa4df63dd 100644 --- a/openstack/network/v2/firewall_policy.py +++ b/openstack/network/v2/firewall_policy.py @@ -14,7 +14,6 @@ # under the License. from openstack.exceptions import HttpException -from openstack.network import network_service from openstack import resource from openstack import utils @@ -23,7 +22,6 @@ class FirewallPolicy(resource.Resource): resource_key = 'firewall_policy' resources_key = 'firewall_policies' base_path = '/fwaas/firewall_policies' - service = network_service.NetworkService() # capabilities allow_create = True diff --git a/openstack/network/v2/firewall_rule.py b/openstack/network/v2/firewall_rule.py index 6f4e537bb..786d86e8b 100644 --- a/openstack/network/v2/firewall_rule.py +++ b/openstack/network/v2/firewall_rule.py @@ -13,7 +13,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack import resource @@ -21,7 +20,6 @@ class FirewallRule(resource.Resource): resource_key = 'firewall_rule' resources_key = 'firewall_rules' base_path = '/fwaas/firewall_rules' - service = network_service.NetworkService() # capabilities allow_create = True diff --git a/openstack/network/v2/flavor.py b/openstack/network/v2/flavor.py index cb7f0c1eb..987a3483b 100644 --- a/openstack/network/v2/flavor.py +++ b/openstack/network/v2/flavor.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack import resource from openstack import utils @@ -19,7 +18,6 @@ class Flavor(resource.Resource): resource_key = 'flavor' resources_key = 'flavors' base_path = '/flavors' - service = network_service.NetworkService() # capabilities allow_create = True diff --git a/openstack/network/v2/floating_ip.py b/openstack/network/v2/floating_ip.py index dcde09905..1511fb8d2 100644 --- a/openstack/network/v2/floating_ip.py +++ b/openstack/network/v2/floating_ip.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack.network.v2 import tag from openstack import resource @@ -21,7 +20,6 @@ class FloatingIP(resource.Resource, tag.TagMixin): resource_key = 'floatingip' resources_key = 'floatingips' base_path = '/floatingips' - service = network_service.NetworkService() # capabilities allow_create = True diff --git a/openstack/network/v2/health_monitor.py b/openstack/network/v2/health_monitor.py index 6ed64bf05..c8cfa59f0 100644 --- a/openstack/network/v2/health_monitor.py +++ b/openstack/network/v2/health_monitor.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack import resource @@ -18,7 +17,6 @@ class HealthMonitor(resource.Resource): resource_key = 'healthmonitor' resources_key = 'healthmonitors' base_path = '/lbaas/healthmonitors' - service = network_service.NetworkService() # capabilities allow_create = True diff --git a/openstack/network/v2/listener.py b/openstack/network/v2/listener.py index e90398577..64595b5f4 100644 --- a/openstack/network/v2/listener.py +++ b/openstack/network/v2/listener.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack import resource @@ -18,7 +17,6 @@ class Listener(resource.Resource): resource_key = 'listener' resources_key = 'listeners' base_path = '/lbaas/listeners' - service = network_service.NetworkService() # capabilities allow_create = True diff --git a/openstack/network/v2/load_balancer.py b/openstack/network/v2/load_balancer.py index 6f66b752d..ab88913c9 100644 --- a/openstack/network/v2/load_balancer.py +++ b/openstack/network/v2/load_balancer.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack import resource @@ -18,7 +17,6 @@ class LoadBalancer(resource.Resource): resource_key = 'loadbalancer' resources_key = 'loadbalancers' base_path = '/lbaas/loadbalancers' - service = network_service.NetworkService() # capabilities allow_create = True diff --git a/openstack/network/v2/metering_label.py b/openstack/network/v2/metering_label.py index 90a62781b..31f2b51c8 100644 --- a/openstack/network/v2/metering_label.py +++ b/openstack/network/v2/metering_label.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack import resource @@ -18,7 +17,6 @@ class MeteringLabel(resource.Resource): resource_key = 'metering_label' resources_key = 'metering_labels' base_path = '/metering/metering-labels' - service = network_service.NetworkService() # capabilities allow_create = True diff --git a/openstack/network/v2/metering_label_rule.py b/openstack/network/v2/metering_label_rule.py index eb766c8de..ed643ed17 100644 --- a/openstack/network/v2/metering_label_rule.py +++ b/openstack/network/v2/metering_label_rule.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack import resource @@ -18,7 +17,6 @@ class MeteringLabelRule(resource.Resource): resource_key = 'metering_label_rule' resources_key = 'metering_label_rules' base_path = '/metering/metering-label-rules' - service = network_service.NetworkService() # capabilities allow_create = True diff --git a/openstack/network/v2/network.py b/openstack/network/v2/network.py index 5b706375a..3ba20e2ed 100644 --- a/openstack/network/v2/network.py +++ b/openstack/network/v2/network.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack.network.v2 import tag from openstack import resource @@ -19,7 +18,6 @@ class Network(resource.Resource, tag.TagMixin): resource_key = 'network' resources_key = 'networks' base_path = '/networks' - service = network_service.NetworkService() # capabilities allow_create = True @@ -121,7 +119,6 @@ class DHCPAgentHostingNetwork(Network): resources_key = 'networks' base_path = '/agents/%(agent_id)s/dhcp-networks' resource_name = 'dhcp-network' - service = network_service.NetworkService() # capabilities allow_create = False diff --git a/openstack/network/v2/network_ip_availability.py b/openstack/network/v2/network_ip_availability.py index ce2c372b7..c2946b6a6 100644 --- a/openstack/network/v2/network_ip_availability.py +++ b/openstack/network/v2/network_ip_availability.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack import resource @@ -19,7 +18,6 @@ class NetworkIPAvailability(resource.Resource): resources_key = 'network_ip_availabilities' base_path = '/network-ip-availabilities' name_attribute = 'network_name' - service = network_service.NetworkService() # capabilities allow_create = False diff --git a/openstack/network/v2/pool.py b/openstack/network/v2/pool.py index 671610087..4e463c92f 100644 --- a/openstack/network/v2/pool.py +++ b/openstack/network/v2/pool.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack import resource @@ -18,7 +17,6 @@ class Pool(resource.Resource): resource_key = 'pool' resources_key = 'pools' base_path = '/lbaas/pools' - service = network_service.NetworkService() # capabilities allow_create = True diff --git a/openstack/network/v2/pool_member.py b/openstack/network/v2/pool_member.py index c75e0c3bb..909bbe21f 100644 --- a/openstack/network/v2/pool_member.py +++ b/openstack/network/v2/pool_member.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack import resource @@ -18,7 +17,6 @@ class PoolMember(resource.Resource): resource_key = 'member' resources_key = 'members' base_path = '/lbaas/pools/%(pool_id)s/members' - service = network_service.NetworkService() # capabilities allow_create = True diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index 625464713..e3290aef2 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack.network.v2 import tag from openstack import resource @@ -19,7 +18,6 @@ class Port(resource.Resource, tag.TagMixin): resource_key = 'port' resources_key = 'ports' base_path = '/ports' - service = network_service.NetworkService() # capabilities allow_create = True diff --git a/openstack/network/v2/qos_bandwidth_limit_rule.py b/openstack/network/v2/qos_bandwidth_limit_rule.py index 1f289ebb6..d10939d36 100644 --- a/openstack/network/v2/qos_bandwidth_limit_rule.py +++ b/openstack/network/v2/qos_bandwidth_limit_rule.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack import resource @@ -18,7 +17,6 @@ class QoSBandwidthLimitRule(resource.Resource): resource_key = 'bandwidth_limit_rule' resources_key = 'bandwidth_limit_rules' base_path = '/qos/policies/%(qos_policy_id)s/bandwidth_limit_rules' - service = network_service.NetworkService() # capabilities allow_create = True diff --git a/openstack/network/v2/qos_dscp_marking_rule.py b/openstack/network/v2/qos_dscp_marking_rule.py index 9333d9813..fbdf86de1 100644 --- a/openstack/network/v2/qos_dscp_marking_rule.py +++ b/openstack/network/v2/qos_dscp_marking_rule.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack import resource @@ -18,7 +17,6 @@ class QoSDSCPMarkingRule(resource.Resource): resource_key = 'dscp_marking_rule' resources_key = 'dscp_marking_rules' base_path = '/qos/policies/%(qos_policy_id)s/dscp_marking_rules' - service = network_service.NetworkService() # capabilities allow_create = True diff --git a/openstack/network/v2/qos_minimum_bandwidth_rule.py b/openstack/network/v2/qos_minimum_bandwidth_rule.py index 476079570..06d2ce75c 100644 --- a/openstack/network/v2/qos_minimum_bandwidth_rule.py +++ b/openstack/network/v2/qos_minimum_bandwidth_rule.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack import resource @@ -18,7 +17,6 @@ class QoSMinimumBandwidthRule(resource.Resource): resource_key = 'minimum_bandwidth_rule' resources_key = 'minimum_bandwidth_rules' base_path = '/qos/policies/%(qos_policy_id)s/minimum_bandwidth_rules' - service = network_service.NetworkService() # capabilities allow_create = True diff --git a/openstack/network/v2/qos_policy.py b/openstack/network/v2/qos_policy.py index 941132899..7efa40efc 100644 --- a/openstack/network/v2/qos_policy.py +++ b/openstack/network/v2/qos_policy.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack.network.v2 import tag from openstack import resource from openstack import utils @@ -20,7 +19,6 @@ class QoSPolicy(resource.Resource, tag.TagMixin): resource_key = 'policy' resources_key = 'policies' base_path = '/qos/policies' - service = network_service.NetworkService() # capabilities allow_create = True diff --git a/openstack/network/v2/qos_rule_type.py b/openstack/network/v2/qos_rule_type.py index a917af2a1..cf23a448f 100644 --- a/openstack/network/v2/qos_rule_type.py +++ b/openstack/network/v2/qos_rule_type.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack import resource @@ -18,7 +17,6 @@ class QoSRuleType(resource.Resource): resource_key = 'rule_type' resources_key = 'rule_types' base_path = '/qos/rule-types' - service = network_service.NetworkService() # capabilities allow_create = False diff --git a/openstack/network/v2/quota.py b/openstack/network/v2/quota.py index fbed68f5f..eab5aa79b 100644 --- a/openstack/network/v2/quota.py +++ b/openstack/network/v2/quota.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack import resource @@ -18,7 +17,6 @@ class Quota(resource.Resource): resource_key = 'quota' resources_key = 'quotas' base_path = '/quotas' - service = network_service.NetworkService() # capabilities allow_fetch = True diff --git a/openstack/network/v2/rbac_policy.py b/openstack/network/v2/rbac_policy.py index 4043f0e44..3fdb158fa 100644 --- a/openstack/network/v2/rbac_policy.py +++ b/openstack/network/v2/rbac_policy.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack import resource @@ -18,7 +17,6 @@ class RBACPolicy(resource.Resource): resource_key = 'rbac_policy' resources_key = 'rbac_policies' base_path = '/rbac-policies' - service = network_service.NetworkService() # capabilities allow_create = True diff --git a/openstack/network/v2/router.py b/openstack/network/v2/router.py index 71cb19495..73db5b697 100644 --- a/openstack/network/v2/router.py +++ b/openstack/network/v2/router.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack.network.v2 import tag from openstack import resource from openstack import utils @@ -20,7 +19,6 @@ class Router(resource.Resource, tag.TagMixin): resource_key = 'router' resources_key = 'routers' base_path = '/routers' - service = network_service.NetworkService() # capabilities allow_create = True @@ -137,7 +135,6 @@ class L3AgentRouter(Router): resources_key = 'routers' base_path = '/agents/%(agent_id)s/l3-routers' resource_name = 'l3-router' - service = network_service.NetworkService() # capabilities allow_create = False diff --git a/openstack/network/v2/security_group.py b/openstack/network/v2/security_group.py index 2ec91d5a1..e80b96c92 100644 --- a/openstack/network/v2/security_group.py +++ b/openstack/network/v2/security_group.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack.network.v2 import tag from openstack import resource @@ -19,7 +18,6 @@ class SecurityGroup(resource.Resource, tag.TagMixin): resource_key = 'security_group' resources_key = 'security_groups' base_path = '/security-groups' - service = network_service.NetworkService() # capabilities allow_create = True diff --git a/openstack/network/v2/security_group_rule.py b/openstack/network/v2/security_group_rule.py index 286a7adc7..54de7fdd2 100644 --- a/openstack/network/v2/security_group_rule.py +++ b/openstack/network/v2/security_group_rule.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack import resource @@ -18,7 +17,6 @@ class SecurityGroupRule(resource.Resource): resource_key = 'security_group_rule' resources_key = 'security_group_rules' base_path = '/security-group-rules' - service = network_service.NetworkService() # capabilities allow_create = True diff --git a/openstack/network/v2/segment.py b/openstack/network/v2/segment.py index 808d08cdb..ce6aa2710 100644 --- a/openstack/network/v2/segment.py +++ b/openstack/network/v2/segment.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack import resource @@ -18,7 +17,6 @@ class Segment(resource.Resource): resource_key = 'segment' resources_key = 'segments' base_path = '/segments' - service = network_service.NetworkService() # capabilities allow_create = True diff --git a/openstack/network/v2/service_profile.py b/openstack/network/v2/service_profile.py index 4b999ef8c..78178a95e 100644 --- a/openstack/network/v2/service_profile.py +++ b/openstack/network/v2/service_profile.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack import resource @@ -18,7 +17,6 @@ class ServiceProfile(resource.Resource): resource_key = 'service_profile' resources_key = 'service_profiles' base_path = '/service_profiles' - service = network_service.NetworkService() # capabilities allow_create = True diff --git a/openstack/network/v2/service_provider.py b/openstack/network/v2/service_provider.py index 9f0ec773a..1b19b6002 100644 --- a/openstack/network/v2/service_provider.py +++ b/openstack/network/v2/service_provider.py @@ -10,14 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack import resource class ServiceProvider(resource.Resource): resources_key = 'service_providers' base_path = '/service-providers' - service = network_service.NetworkService() # Capabilities allow_create = False diff --git a/openstack/network/v2/subnet.py b/openstack/network/v2/subnet.py index e611d0b6a..4c9078acb 100644 --- a/openstack/network/v2/subnet.py +++ b/openstack/network/v2/subnet.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack.network.v2 import tag from openstack import resource @@ -19,7 +18,6 @@ class Subnet(resource.Resource, tag.TagMixin): resource_key = 'subnet' resources_key = 'subnets' base_path = '/subnets' - service = network_service.NetworkService() # capabilities allow_create = True diff --git a/openstack/network/v2/subnet_pool.py b/openstack/network/v2/subnet_pool.py index a8648fdf3..e7d186dd6 100644 --- a/openstack/network/v2/subnet_pool.py +++ b/openstack/network/v2/subnet_pool.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack.network.v2 import tag from openstack import resource @@ -19,7 +18,6 @@ class SubnetPool(resource.Resource, tag.TagMixin): resource_key = 'subnetpool' resources_key = 'subnetpools' base_path = '/subnetpools' - service = network_service.NetworkService() # capabilities allow_create = True diff --git a/openstack/network/v2/trunk.py b/openstack/network/v2/trunk.py index 14bb47e10..0168f9933 100644 --- a/openstack/network/v2/trunk.py +++ b/openstack/network/v2/trunk.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack import resource from openstack import utils @@ -19,7 +18,6 @@ class Trunk(resource.Resource): resource_key = 'trunk' resources_key = 'trunks' base_path = '/trunks' - service = network_service.NetworkService() # capabilities allow_create = True diff --git a/openstack/network/v2/vpn_service.py b/openstack/network/v2/vpn_service.py index f40d20c8a..d9c1483ec 100644 --- a/openstack/network/v2/vpn_service.py +++ b/openstack/network/v2/vpn_service.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack import resource @@ -20,7 +19,6 @@ class VPNService(resource.Resource): resource_key = 'vpnservice' resources_key = 'vpnservices' base_path = '/vpn/vpnservices' - service = network_service.NetworkService() # capabilities allow_create = True diff --git a/openstack/network/version.py b/openstack/network/version.py index 9de4601ca..805ce6a34 100644 --- a/openstack/network/version.py +++ b/openstack/network/version.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network import network_service from openstack import resource @@ -18,9 +17,6 @@ class Version(resource.Resource): resource_key = 'version' resources_key = 'versions' base_path = '/' - service = network_service.NetworkService( - version=network_service.NetworkService.UNVERSIONED - ) # capabilities allow_list = True diff --git a/openstack/object_store/object_store_service.py b/openstack/object_store/object_store_service.py index 1f58b97cf..2a0aa3d1a 100644 --- a/openstack/object_store/object_store_service.py +++ b/openstack/object_store/object_store_service.py @@ -10,15 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import service_filter +from openstack.object_store.v1 import _proxy +from openstack import service_description -class ObjectStoreService(service_filter.ServiceFilter): +class ObjectStoreService(service_description.ServiceDescription): """The object store service.""" - valid_versions = [service_filter.ValidVersion('v1')] - - def __init__(self, version=None): - """Create an object store service.""" - super(ObjectStoreService, self).__init__(service_type='object-store', - version=version) + supported_versions = { + '1': _proxy.Proxy, + } diff --git a/openstack/object_store/v1/_base.py b/openstack/object_store/v1/_base.py index 4c576da64..21372406f 100644 --- a/openstack/object_store/v1/_base.py +++ b/openstack/object_store/v1/_base.py @@ -12,12 +12,10 @@ # under the License. from openstack import exceptions -from openstack.object_store import object_store_service from openstack import resource class BaseResource(resource.Resource): - service = object_store_service.ObjectStoreService() commit_method = 'POST' create_method = 'PUT' diff --git a/openstack/object_store/v1/obj.py b/openstack/object_store/v1/obj.py index 595d0e4cd..e77588f0d 100644 --- a/openstack/object_store/v1/obj.py +++ b/openstack/object_store/v1/obj.py @@ -14,7 +14,6 @@ import copy from openstack import exceptions -from openstack.object_store import object_store_service from openstack.object_store.v1 import _base from openstack import resource @@ -32,7 +31,6 @@ class Object(_base.BaseResource): base_path = "/%(container)s" pagination_key = 'X-Container-Object-Count' - service = object_store_service.ObjectStoreService() allow_create = True allow_fetch = True diff --git a/openstack/orchestration/orchestration_service.py b/openstack/orchestration/orchestration_service.py index 0333ff74f..827f9831c 100644 --- a/openstack/orchestration/orchestration_service.py +++ b/openstack/orchestration/orchestration_service.py @@ -10,18 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import service_filter +from openstack.orchestration.v1 import _proxy +from openstack import service_description -class OrchestrationService(service_filter.ServiceFilter): +class OrchestrationService(service_description.ServiceDescription): """The orchestration service.""" - valid_versions = [service_filter.ValidVersion('v1')] - - def __init__(self, version=None): - """Create an orchestration service.""" - super(OrchestrationService, self).__init__( - service_type='orchestration', - version=version, - requires_project_id=True, - ) + supported_versions = { + '1': _proxy.Proxy, + } diff --git a/openstack/orchestration/v1/resource.py b/openstack/orchestration/v1/resource.py index 97184bb9c..ddc466fda 100644 --- a/openstack/orchestration/v1/resource.py +++ b/openstack/orchestration/v1/resource.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.orchestration import orchestration_service from openstack import resource @@ -19,7 +18,6 @@ class Resource(resource.Resource): resource_key = 'resource' resources_key = 'resources' base_path = '/stacks/%(stack_name)s/%(stack_id)s/resources' - service = orchestration_service.OrchestrationService() # capabilities allow_create = False diff --git a/openstack/orchestration/v1/software_config.py b/openstack/orchestration/v1/software_config.py index a4f4b4fae..af59e8989 100644 --- a/openstack/orchestration/v1/software_config.py +++ b/openstack/orchestration/v1/software_config.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.orchestration import orchestration_service from openstack import resource @@ -18,7 +17,6 @@ class SoftwareConfig(resource.Resource): resource_key = 'software_config' resources_key = 'software_configs' base_path = '/software_configs' - service = orchestration_service.OrchestrationService() # capabilities allow_create = True diff --git a/openstack/orchestration/v1/software_deployment.py b/openstack/orchestration/v1/software_deployment.py index 00e4acd0c..5ac670973 100644 --- a/openstack/orchestration/v1/software_deployment.py +++ b/openstack/orchestration/v1/software_deployment.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.orchestration import orchestration_service from openstack import resource @@ -18,7 +17,6 @@ class SoftwareDeployment(resource.Resource): resource_key = 'software_deployment' resources_key = 'software_deployments' base_path = '/software_deployments' - service = orchestration_service.OrchestrationService() # capabilities allow_create = True diff --git a/openstack/orchestration/v1/stack.py b/openstack/orchestration/v1/stack.py index 4828b1332..97bb6e3f6 100644 --- a/openstack/orchestration/v1/stack.py +++ b/openstack/orchestration/v1/stack.py @@ -11,7 +11,6 @@ # under the License. from openstack import exceptions -from openstack.orchestration import orchestration_service from openstack import resource from openstack import utils @@ -21,7 +20,6 @@ class Stack(resource.Resource): resource_key = 'stack' resources_key = 'stacks' base_path = '/stacks' - service = orchestration_service.OrchestrationService() # capabilities allow_create = True diff --git a/openstack/orchestration/v1/stack_environment.py b/openstack/orchestration/v1/stack_environment.py index 2b972bdf4..24b05e750 100644 --- a/openstack/orchestration/v1/stack_environment.py +++ b/openstack/orchestration/v1/stack_environment.py @@ -10,13 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.orchestration import orchestration_service from openstack import resource class StackEnvironment(resource.Resource): - service = orchestration_service.OrchestrationService() base_path = "/stacks/%(stack_name)s/%(stack_id)s/environment" # capabilities diff --git a/openstack/orchestration/v1/stack_files.py b/openstack/orchestration/v1/stack_files.py index 6963d5b84..faa4c942b 100644 --- a/openstack/orchestration/v1/stack_files.py +++ b/openstack/orchestration/v1/stack_files.py @@ -10,13 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.orchestration import orchestration_service from openstack import resource class StackFiles(resource.Resource): - service = orchestration_service.OrchestrationService() base_path = "/stacks/%(stack_name)s/%(stack_id)s/files" # capabilities diff --git a/openstack/orchestration/v1/stack_template.py b/openstack/orchestration/v1/stack_template.py index 0bea61ab9..7ba46e767 100644 --- a/openstack/orchestration/v1/stack_template.py +++ b/openstack/orchestration/v1/stack_template.py @@ -10,13 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.orchestration import orchestration_service from openstack import resource class StackTemplate(resource.Resource): - service = orchestration_service.OrchestrationService() base_path = "/stacks/%(stack_name)s/%(stack_id)s/template" # capabilities diff --git a/openstack/orchestration/v1/template.py b/openstack/orchestration/v1/template.py index ec04b6cec..ab0bc5655 100644 --- a/openstack/orchestration/v1/template.py +++ b/openstack/orchestration/v1/template.py @@ -12,12 +12,10 @@ from six.moves.urllib import parse -from openstack.orchestration import orchestration_service from openstack import resource class Template(resource.Resource): - service = orchestration_service.OrchestrationService() # capabilities allow_create = False diff --git a/openstack/orchestration/version.py b/openstack/orchestration/version.py index 2dd05c286..805ce6a34 100644 --- a/openstack/orchestration/version.py +++ b/openstack/orchestration/version.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.orchestration import orchestration_service from openstack import resource @@ -18,9 +17,6 @@ class Version(resource.Resource): resource_key = 'version' resources_key = 'versions' base_path = '/' - service = orchestration_service.OrchestrationService( - version=orchestration_service.OrchestrationService.UNVERSIONED - ) # capabilities allow_list = True diff --git a/openstack/service_description.py b/openstack/service_description.py index 12a7dc66a..e49d9db83 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -11,15 +11,12 @@ # License for the specific language governing permissions and limitations # under the License. -import importlib - import os_service_types from openstack import _log from openstack import proxy __all__ = [ - 'OpenStackServiceDescription', 'ServiceDescription', ] @@ -29,14 +26,14 @@ class ServiceDescription(object): - #: Proxy class for this service - proxy_class = proxy.Proxy + #: Dictionary of supported versions and proxy classes for that version + supported_versions = None #: main service_type to use to find this service in the catalog service_type = None #: list of aliases this service might be registered as aliases = [] - def __init__(self, service_type, proxy_class=None, aliases=None): + def __init__(self, service_type, supported_versions=None, aliases=None): """Class describing how to interact with a REST service. Each service in an OpenStack cloud needs to be found by looking @@ -65,72 +62,112 @@ def __init__(self, service_type, proxy_class=None, aliases=None): be used to register the service in the catalog. """ self.service_type = service_type or self.service_type - self.proxy_class = proxy_class or self.proxy_class - if self.proxy_class: - self._validate_proxy_class() + self.supported_versions = ( + supported_versions + or self.supported_versions + or {}) + self.aliases = aliases or self.aliases self.all_types = [service_type] + self.aliases - self._proxy = None - - def _validate_proxy_class(self): - if not issubclass(self.proxy_class, proxy.Proxy): - raise TypeError( - "{module}.{proxy_class} must inherit from Proxy".format( - module=self.proxy_class.__module__, - proxy_class=self.proxy_class.__name__)) - - def get_proxy_class(self, config): - return self.proxy_class def __get__(self, instance, owner): if instance is None: return self if self.service_type not in instance._proxies: - config = instance.config - proxy_class = self.get_proxy_class(config) - instance._proxies[self.service_type] = config.get_session_client( - self.service_type, - constructor=proxy_class, - task_manager=instance.task_manager, - allow_version_hack=True, - ) + instance._proxies[self.service_type] = self._make_proxy( + instance, owner) instance._proxies[self.service_type]._connection = instance return instance._proxies[self.service_type] + def _make_proxy(self, instance, owner): + config = instance.config + + # First, check to see if we've got config that matches what we + # understand in the SDK. + version_string = config.get_api_version(self.service_type) + endpoint_override = config.get_endpoint(self.service_type) + + # If the user doesn't give a version in config, but we only support + # one version, then just use that version. + if not version_string and len(self.supported_versions) == 1: + version_string = list(self.supported_versions.keys())[0] + + proxy_obj = None + if endpoint_override and version_string and self.supported_versions: + # Both endpoint override and version_string are set, we don't + # need to do discovery - just trust the user. + proxy_class = self.supported_versions.get(version_string[0]) + if proxy_class: + proxy_obj = config.get_session_client( + self.service_type, + constructor=proxy_class, + task_manager=instance.task_manager, + ) + elif endpoint_override and self.supported_versions: + temp_adapter = config.get_session_client( + self.service_type + ) + api_version = temp_adapter.get_endpoint_data().api_version + proxy_class = self.supported_versions.get(str(api_version[0])) + if proxy_class: + proxy_obj = config.get_session_client( + self.service_type, + constructor=proxy_class, + task_manager=instance.task_manager, + ) + + if proxy_obj: + data = proxy_obj.get_endpoint_data() + if data.catalog_url != data.service_url: + ep_key = '{service_type}_endpoint_override'.format( + service_type=self.service_type) + config.config[ep_key] = data.service_url + proxy_obj = config.get_session_client( + self.service_type, + constructor=proxy_class, + task_manager=instance.task_manager, + ) + return proxy_obj + + # Make an adapter to let discovery take over + version_kwargs = {} + if version_string: + version_kwargs['version'] = version_string + elif self.supported_versions: + supported_versions = sorted([ + int(f) for f in self.supported_versions]) + version_kwargs['min_version'] = str(supported_versions[0]) + version_kwargs['max_version'] = '{version}.latest'.format( + version=str(supported_versions[-1])) + + temp_adapter = config.get_session_client( + self.service_type, + constructor=proxy.Proxy, + allow_version_hack=True, + **version_kwargs + ) + found_version = temp_adapter.get_api_major_version() + if found_version is None: + # Maybe openstacksdk is being used for the passthrough + # REST API proxy layer for an unknown service in the + # service catalog that also doesn't have any useful + # version discovery? + instance._proxies[self.service_type] = temp_adapter + instance._proxies[self.service_type]._connection = instance + return instance._proxies[self.service_type] + proxy_class = self.supported_versions.get(str(found_version[0])) + if not proxy_class: + proxy_class = proxy.Proxy + return config.get_session_client( + self.service_type, + constructor=proxy_class, + task_manager=instance.task_manager, + allow_version_hack=True, + **version_kwargs + ) + def __set__(self, instance, value): raise AttributeError('Service Descriptors cannot be set') def __delete__(self, instance): raise AttributeError('Service Descriptors cannot be deleted') - - -class OpenStackServiceDescription(ServiceDescription): - def __init__(self, service_filter_class, *args, **kwargs): - """Official OpenStack ServiceDescription. - - The OpenStackServiceDescription class is a helper class for - services listed in Service Types Authority and that are directly - supported by openstacksdk. - - It finds the proxy_class by looking in the openstacksdk tree for - appropriately named modules. - - :param service_filter_class: - A subclass of :class:`~openstack.service_filter.ServiceFilter` - """ - super(OpenStackServiceDescription, self).__init__(*args, **kwargs) - self._service_filter_class = service_filter_class - - def get_proxy_class(self, config): - # TODO(mordred) Replace this with proper discovery - if self.service_type == 'block-storage': - version_string = config.get_api_version('volume') - else: - version_string = config.get_api_version(self.service_type) - version = None - if version_string: - version = 'v{version}'.format(version=version_string[0]) - service_filter = self._service_filter_class(version=version) - module_name = service_filter.get_module() + "._proxy" - module = importlib.import_module(module_name) - return getattr(module, "Proxy") diff --git a/openstack/service_filter.py b/openstack/service_filter.py deleted file mode 100644 index b5c4fe82d..000000000 --- a/openstack/service_filter.py +++ /dev/null @@ -1,199 +0,0 @@ -# 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. - -""" -The :class:`~openstack.service_filter.ServiceFilter` is the base class -for service identifiers and user service preferences. Each -:class:`~openstack.resource.Resource` has a service identifier to -associate the resource with a service. An example of a service identifier -would be ``openstack.compute.compute_service.ComputeService``. -The service preference and the service identifier are joined to create a -filter to match a service. - -Examples --------- - -The :class:`~openstack.service_filter.ServiceFilter` class can be built -with a service type, interface, region, name, and version. - -Create a service filter -~~~~~~~~~~~~~~~~~~~~~~~ - -Create a compute service and service preference. Join the services -and match:: - - from openstack import service_filter - from openstack.compute import compute_service - default = compute_service.ComputeService() - preference = service_filter.ServiceFilter('compute', version='v2') - result = preference.join(default) - matches = (result.match_service_type('compute') and - result.match_service_name('Hal9000') and - result.match_region('DiscoveryOne') and - result.match_interface('public')) - print(str(result)) - print("matches=" + str(matches)) - -The resulting output from the code:: - - service_type=compute,interface=public,version=v2 - matches=True -""" - - -class ValidVersion(object): - - def __init__(self, module, path=None): - """" Valid service version. - - :param string module: Module associated with version. - :param string path: URL path version. - """ - self.module = module - self.path = path or module - - -class ServiceFilter(dict): - UNVERSIONED = '' - PUBLIC = 'public' - INTERNAL = 'internal' - ADMIN = 'admin' - valid_versions = [] - - def __init__(self, service_type, interface=PUBLIC, region=None, - service_name=None, version=None, api_version=None, - requires_project_id=False): - """Create a service identifier. - - :param string service_type: The desired type of service. - :param string interface: The exposure of the endpoint. Should be - `public` (default), `internal` or `admin`. - :param string region: The desired region (optional). - :param string service_name: Name of the service - :param string version: Version of service to use. - :param string api_version: Microversion of service supported. - :param bool requires_project_id: True if this service's endpoint - expects project id to be included. - """ - self['service_type'] = service_type.lower() - self['interface'] = interface - self['region_name'] = region - self['service_name'] = service_name - self['version'] = version - self['api_version'] = api_version - self['requires_project_id'] = requires_project_id - - @classmethod - def _get_proxy_class_names(cls): - names = [] - module_name = ".".join(cls.__module__.split('.')[:-1]) - for version in cls.valid_versions: - names.append("{module}.{version}._proxy.Proxy".format( - module=module_name, - version=version.module)) - return names - - @property - def service_type(self): - return self['service_type'] - - @property - def interface(self): - return self['interface'] - - @interface.setter - def interface(self, value): - self['interface'] = value - - @property - def region(self): - return self['region_name'] - - @region.setter - def region(self, value): - self['region_name'] = value - - @property - def service_name(self): - return self['service_name'] - - @service_name.setter - def service_name(self, value): - self['service_name'] = value - - @property - def version(self): - return self['version'] - - @version.setter - def version(self, value): - self['version'] = value - - @property - def api_version(self): - return self['api_version'] - - @api_version.setter - def api_version(self, value): - self['api_version'] = value - - @property - def requires_project_id(self): - return self['requires_project_id'] - - @requires_project_id.setter - def requires_project_id(self, value): - self['requires_project_id'] = value - - @property - def path(self): - return self['path'] - - @path.setter - def path(self, value): - self['path'] = value - - def get_path(self, version=None): - if not self.version: - self.version = version - return self.get('path', self._get_valid_version().path) - - def get_filter(self): - filter = dict(self) - del filter['version'] - return filter - - def _get_valid_version(self): - if self.valid_versions: - if self.version: - for valid in self.valid_versions: - # NOTE(thowe): should support fuzzy match e.g: v2.1==v2 - if self.version.startswith(valid.module): - return valid - return self.valid_versions[0] - return ValidVersion('') - - def get_module(self): - """Get the full module name associated with the service.""" - module = self.__class__.__module__.split('.') - module = ".".join(module[:-1]) - module = module + "." + self._get_valid_version().module - return module - - def get_service_module(self): - """Get the module version of the service name. - - This would often be the same as the service type except in cases like - object store where the service type is `object-store` and the module - is `object_store`. - """ - return self.__class__.__module__.split('.')[-2] diff --git a/openstack/tests/unit/baremetal/test_baremetal_service.py b/openstack/tests/unit/baremetal/test_baremetal_service.py deleted file mode 100644 index 45ccd04e0..000000000 --- a/openstack/tests/unit/baremetal/test_baremetal_service.py +++ /dev/null @@ -1,28 +0,0 @@ -# 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.tests.unit import base - -from openstack.baremetal import baremetal_service - - -class TestBaremetalService(base.TestCase): - - def test_service(self): - sot = baremetal_service.BaremetalService() - self.assertEqual('baremetal', sot.service_type) - self.assertEqual('public', sot.interface) - self.assertIsNone(sot.region) - self.assertIsNone(sot.service_name) - self.assertEqual(1, len(sot.valid_versions)) - self.assertEqual('v1', sot.valid_versions[0].module) - self.assertEqual('v1', sot.valid_versions[0].path) diff --git a/openstack/tests/unit/baremetal/test_version.py b/openstack/tests/unit/baremetal/test_version.py index ddc807944..06ce752c3 100644 --- a/openstack/tests/unit/baremetal/test_version.py +++ b/openstack/tests/unit/baremetal/test_version.py @@ -30,7 +30,6 @@ def test_basic(self): self.assertEqual('version', sot.resource_key) self.assertEqual('versions', sot.resources_key) self.assertEqual('/', sot.base_path) - self.assertEqual('baremetal', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/baremetal/v1/test_chassis.py b/openstack/tests/unit/baremetal/v1/test_chassis.py index 76e4ef44b..4c52f01de 100644 --- a/openstack/tests/unit/baremetal/v1/test_chassis.py +++ b/openstack/tests/unit/baremetal/v1/test_chassis.py @@ -50,7 +50,6 @@ def test_basic(self): self.assertIsNone(sot.resource_key) self.assertEqual('chassis', sot.resources_key) self.assertEqual('/chassis', sot.base_path) - self.assertEqual('baremetal', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) @@ -76,7 +75,6 @@ def test_basic(self): self.assertIsNone(sot.resource_key) self.assertEqual('chassis', sot.resources_key) self.assertEqual('/chassis/detail', sot.base_path) - self.assertEqual('baremetal', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/baremetal/v1/test_driver.py b/openstack/tests/unit/baremetal/v1/test_driver.py index 075ac0c14..8fdcabcd9 100644 --- a/openstack/tests/unit/baremetal/v1/test_driver.py +++ b/openstack/tests/unit/baremetal/v1/test_driver.py @@ -50,7 +50,6 @@ def test_basic(self): self.assertIsNone(sot.resource_key) self.assertEqual('drivers', sot.resources_key) self.assertEqual('/drivers', sot.base_path) - self.assertEqual('baremetal', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index b3a5e928b..85d77bb7a 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -102,7 +102,6 @@ def test_basic(self): self.assertIsNone(sot.resource_key) self.assertEqual('nodes', sot.resources_key) self.assertEqual('/nodes', sot.base_path) - self.assertEqual('baremetal', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) @@ -161,7 +160,6 @@ def test_basic(self): self.assertIsNone(sot.resource_key) self.assertEqual('nodes', sot.resources_key) self.assertEqual('/nodes/detail', sot.base_path) - self.assertEqual('baremetal', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/baremetal/v1/test_port.py b/openstack/tests/unit/baremetal/v1/test_port.py index 61b9cce75..0ab7eae1a 100644 --- a/openstack/tests/unit/baremetal/v1/test_port.py +++ b/openstack/tests/unit/baremetal/v1/test_port.py @@ -49,7 +49,6 @@ def test_basic(self): self.assertIsNone(sot.resource_key) self.assertEqual('ports', sot.resources_key) self.assertEqual('/ports', sot.base_path) - self.assertEqual('baremetal', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) @@ -80,7 +79,6 @@ def test_basic(self): self.assertIsNone(sot.resource_key) self.assertEqual('ports', sot.resources_key) self.assertEqual('/ports/detail', sot.base_path) - self.assertEqual('baremetal', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/baremetal/v1/test_port_group.py b/openstack/tests/unit/baremetal/v1/test_port_group.py index 3cfe051fb..81fb69a60 100644 --- a/openstack/tests/unit/baremetal/v1/test_port_group.py +++ b/openstack/tests/unit/baremetal/v1/test_port_group.py @@ -54,7 +54,6 @@ def test_basic(self): self.assertIsNone(sot.resource_key) self.assertEqual('portgroups', sot.resources_key) self.assertEqual('/portgroups', sot.base_path) - self.assertEqual('baremetal', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) @@ -85,7 +84,6 @@ def test_basic(self): self.assertIsNone(sot.resource_key) self.assertEqual('portgroups', sot.resources_key) self.assertEqual('/portgroups/detail', sot.base_path) - self.assertEqual('baremetal', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/block_storage/test_block_storage_service.py b/openstack/tests/unit/block_storage/test_block_storage_service.py deleted file mode 100644 index ca11ec47f..000000000 --- a/openstack/tests/unit/block_storage/test_block_storage_service.py +++ /dev/null @@ -1,28 +0,0 @@ -# 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.tests.unit import base - -from openstack.block_storage import block_storage_service - - -class TestBlockStorageService(base.TestCase): - - def test_service(self): - sot = block_storage_service.BlockStorageService() - self.assertEqual("block-storage", sot.service_type) - self.assertEqual("public", sot.interface) - self.assertIsNone(sot.region) - self.assertIsNone(sot.service_name) - self.assertEqual(1, len(sot.valid_versions)) - self.assertEqual("v2", sot.valid_versions[0].module) - self.assertEqual("v2", sot.valid_versions[0].path) diff --git a/openstack/tests/unit/block_storage/v2/test_snapshot.py b/openstack/tests/unit/block_storage/v2/test_snapshot.py index a062ef9b9..16b59355c 100644 --- a/openstack/tests/unit/block_storage/v2/test_snapshot.py +++ b/openstack/tests/unit/block_storage/v2/test_snapshot.py @@ -45,7 +45,6 @@ def test_basic(self): self.assertEqual("snapshot", sot.resource_key) self.assertEqual("snapshots", sot.resources_key) self.assertEqual("/snapshots", sot.base_path) - self.assertEqual("block-storage", sot.service.service_type) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_create) diff --git a/openstack/tests/unit/block_storage/v2/test_type.py b/openstack/tests/unit/block_storage/v2/test_type.py index 393d9cdd2..d5cf4e7a3 100644 --- a/openstack/tests/unit/block_storage/v2/test_type.py +++ b/openstack/tests/unit/block_storage/v2/test_type.py @@ -31,7 +31,6 @@ def test_basic(self): self.assertEqual("volume_type", sot.resource_key) self.assertEqual("volume_types", sot.resources_key) self.assertEqual("/types", sot.base_path) - self.assertEqual("block-storage", sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_delete) diff --git a/openstack/tests/unit/block_storage/v2/test_volume.py b/openstack/tests/unit/block_storage/v2/test_volume.py index e3edcd072..3dc40afd0 100644 --- a/openstack/tests/unit/block_storage/v2/test_volume.py +++ b/openstack/tests/unit/block_storage/v2/test_volume.py @@ -68,7 +68,6 @@ def test_basic(self): self.assertEqual("volume", sot.resource_key) self.assertEqual("volumes", sot.resources_key) self.assertEqual("/volumes", sot.base_path) - self.assertEqual("block-storage", sot.service.service_type) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/block_store/v2/test_stats.py b/openstack/tests/unit/block_store/v2/test_stats.py index ed87eba5e..729ec6370 100644 --- a/openstack/tests/unit/block_store/v2/test_stats.py +++ b/openstack/tests/unit/block_store/v2/test_stats.py @@ -39,7 +39,6 @@ def test_basic(self): self.assertEqual("pools", sot.resources_key) self.assertEqual("/scheduler-stats/get_pools?detail=True", sot.base_path) - self.assertEqual("volume", sot.service.service_type) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_delete) diff --git a/openstack/tests/unit/cloud/test_operator_noauth.py b/openstack/tests/unit/cloud/test_operator_noauth.py index 7630c805c..d9faee222 100644 --- a/openstack/tests/unit/cloud/test_operator_noauth.py +++ b/openstack/tests/unit/cloud/test_operator_noauth.py @@ -37,7 +37,7 @@ def setUp(self): uri=self.get_mock_url( service_type='baremetal', base_url_append='v1'), json={'id': 'v1', - 'links': [{"href": "https://bare-metal.example.com/v1/", + 'links': [{"href": "https://bare-metal.example.com/v1", "rel": "self"}]}), dict(method='GET', uri=self.get_mock_url( @@ -98,3 +98,118 @@ def test_ironic_noauth_admin_token_auth_type(self): self.cloud_noauth.list_machines() self.assert_calls() + + +class TestOpenStackCloudOperatorNoAuthUnversioned(base.TestCase): + def setUp(self): + """Setup Noauth OpenStackCloud tests for unversioned endpoints + + Setup the test to utilize no authentication and an endpoint + URL in the auth data. This is permits testing of the basic + mechanism that enables Ironic noauth mode to be utilized with + Shade. + + Uses base.TestCase instead of IronicTestCase because + we need to do completely different things with discovery. + """ + super(TestOpenStackCloudOperatorNoAuthUnversioned, self).setUp() + # By clearing the URI registry, we remove all calls to a keystone + # catalog or getting a token + self._uri_registry.clear() + self.register_uris([ + dict(method='GET', + uri='https://bare-metal.example.com/', + json={ + "default_version": { + "status": "CURRENT", + "min_version": "1.1", + "version": "1.46", + "id": "v1", + "links": [{ + "href": "https://bare-metal.example.com/v1", + "rel": "self" + }]}, + "versions": [{ + "status": "CURRENT", + "min_version": "1.1", + "version": "1.46", + "id": "v1", + "links": [{ + "href": "https://bare-metal.example.com/v1", + "rel": "self" + }]}], + "name": "OpenStack Ironic API", + "description": "Ironic is an OpenStack project." + }), + dict(method='GET', + uri=self.get_mock_url( + service_type='baremetal', base_url_append='v1'), + json={ + "media_types": [{ + "base": "application/json", + "type": "application/vnd.openstack.ironic.v1+json" + }], + "links": [{ + "href": "https://bare-metal.example.com/v1", + "rel": "self" + }], + "ports": [{ + "href": "https://bare-metal.example.com/v1/ports/", + "rel": "self" + }, { + "href": "https://bare-metal.example.com/ports/", + "rel": "bookmark" + }], + "nodes": [{ + "href": "https://bare-metal.example.com/v1/nodes/", + "rel": "self" + }, { + "href": "https://bare-metal.example.com/nodes/", + "rel": "bookmark" + }], + "id": "v1" + }), + dict(method='GET', + uri=self.get_mock_url( + service_type='baremetal', base_url_append='v1', + resource='nodes'), + json={'nodes': []}), + ]) + + def test_ironic_noauth_none_auth_type(self): + """Test noauth selection for Ironic in OpenStackCloud + + The new way of doing this is with the keystoneauth none plugin. + """ + # NOTE(TheJulia): When we are using the python-ironicclient + # library, the library will automatically prepend the URI path + # with 'v1'. As such, since we are overriding the endpoint, + # we must explicitly do the same as we move away from the + # client library. + self.cloud_noauth = openstack.connect( + auth_type='none', + baremetal_endpoint_override="https://bare-metal.example.com") + + self.cloud_noauth.list_machines() + + self.assert_calls() + + def test_ironic_noauth_auth_endpoint(self): + """Test noauth selection for Ironic in OpenStackCloud + + Sometimes people also write clouds.yaml files that look like this: + + :: + clouds: + bifrost: + auth_type: "none" + endpoint: https://bare-metal.example.com + """ + self.cloud_noauth = openstack.connect( + auth_type='none', + endpoint='https://bare-metal.example.com/', + ) + + self.cloud_noauth.list_machines() + + self.assert_calls() diff --git a/openstack/tests/unit/clustering/test_cluster_service.py b/openstack/tests/unit/clustering/test_cluster_service.py deleted file mode 100644 index f52cafdaf..000000000 --- a/openstack/tests/unit/clustering/test_cluster_service.py +++ /dev/null @@ -1,28 +0,0 @@ -# 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.tests.unit import base - -from openstack.clustering import clustering_service - - -class TestClusteringService(base.TestCase): - - def test_service(self): - sot = clustering_service.ClusteringService() - self.assertEqual('clustering', sot.service_type) - self.assertEqual('public', sot.interface) - self.assertIsNone(sot.region) - self.assertIsNone(sot.service_name) - self.assertEqual(1, len(sot.valid_versions)) - self.assertEqual('v1', sot.valid_versions[0].module) - self.assertEqual('v1', sot.valid_versions[0].path) diff --git a/openstack/tests/unit/clustering/test_version.py b/openstack/tests/unit/clustering/test_version.py index f6fff193b..461a8f4e9 100644 --- a/openstack/tests/unit/clustering/test_version.py +++ b/openstack/tests/unit/clustering/test_version.py @@ -29,7 +29,6 @@ def test_basic(self): self.assertEqual('version', sot.resource_key) self.assertEqual('versions', sot.resources_key) self.assertEqual('/', sot.base_path) - self.assertEqual('clustering', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/clustering/v1/test_action.py b/openstack/tests/unit/clustering/v1/test_action.py index e506fc203..268202d25 100644 --- a/openstack/tests/unit/clustering/v1/test_action.py +++ b/openstack/tests/unit/clustering/v1/test_action.py @@ -53,7 +53,6 @@ def test_basic(self): self.assertEqual('action', sot.resource_key) self.assertEqual('actions', sot.resources_key) self.assertEqual('/actions', sot.base_path) - self.assertEqual('clustering', sot.service.service_type) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/clustering/v1/test_build_info.py b/openstack/tests/unit/clustering/v1/test_build_info.py index 4532c2634..0056691d6 100644 --- a/openstack/tests/unit/clustering/v1/test_build_info.py +++ b/openstack/tests/unit/clustering/v1/test_build_info.py @@ -34,7 +34,6 @@ def test_basic(self): sot = build_info.BuildInfo() self.assertEqual('/build-info', sot.base_path) self.assertEqual('build_info', sot.resource_key) - self.assertEqual('clustering', sot.service.service_type) self.assertTrue(sot.allow_fetch) def test_instantiate(self): diff --git a/openstack/tests/unit/clustering/v1/test_cluster.py b/openstack/tests/unit/clustering/v1/test_cluster.py index e79e397e3..2d0a22ada 100644 --- a/openstack/tests/unit/clustering/v1/test_cluster.py +++ b/openstack/tests/unit/clustering/v1/test_cluster.py @@ -75,7 +75,6 @@ def test_basic(self): self.assertEqual('cluster', sot.resource_key) self.assertEqual('clusters', sot.resources_key) self.assertEqual('/clusters', sot.base_path) - self.assertEqual('clustering', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/clustering/v1/test_cluster_attr.py b/openstack/tests/unit/clustering/v1/test_cluster_attr.py index e64cdcb9e..3ef9641ce 100644 --- a/openstack/tests/unit/clustering/v1/test_cluster_attr.py +++ b/openstack/tests/unit/clustering/v1/test_cluster_attr.py @@ -33,7 +33,6 @@ def test_basic(self): self.assertEqual('cluster_attributes', sot.resources_key) self.assertEqual('/clusters/%(cluster_id)s/attrs/%(path)s', sot.base_path) - self.assertEqual('clustering', sot.service.service_type) self.assertTrue(sot.allow_list) def test_instantiate(self): diff --git a/openstack/tests/unit/clustering/v1/test_cluster_policy.py b/openstack/tests/unit/clustering/v1/test_cluster_policy.py index e6f3de548..463fa642e 100644 --- a/openstack/tests/unit/clustering/v1/test_cluster_policy.py +++ b/openstack/tests/unit/clustering/v1/test_cluster_policy.py @@ -37,7 +37,6 @@ def test_basic(self): self.assertEqual('cluster_policies', sot.resources_key) self.assertEqual('/clusters/%(cluster_id)s/policies', sot.base_path) - self.assertEqual('clustering', sot.service.service_type) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/clustering/v1/test_event.py b/openstack/tests/unit/clustering/v1/test_event.py index 8464c0ea3..e52a44218 100644 --- a/openstack/tests/unit/clustering/v1/test_event.py +++ b/openstack/tests/unit/clustering/v1/test_event.py @@ -41,7 +41,6 @@ def test_basic(self): self.assertEqual('event', sot.resource_key) self.assertEqual('events', sot.resources_key) self.assertEqual('/events', sot.base_path) - self.assertEqual('clustering', sot.service.service_type) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/clustering/v1/test_node.py b/openstack/tests/unit/clustering/v1/test_node.py index 6a454cb05..c2838a093 100644 --- a/openstack/tests/unit/clustering/v1/test_node.py +++ b/openstack/tests/unit/clustering/v1/test_node.py @@ -44,7 +44,6 @@ def test_basic(self): self.assertEqual('node', sot.resource_key) self.assertEqual('nodes', sot.resources_key) self.assertEqual('/nodes', sot.base_path) - self.assertEqual('clustering', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/clustering/v1/test_policy.py b/openstack/tests/unit/clustering/v1/test_policy.py index f565b6032..6af6bd6b6 100644 --- a/openstack/tests/unit/clustering/v1/test_policy.py +++ b/openstack/tests/unit/clustering/v1/test_policy.py @@ -51,7 +51,6 @@ def test_basic(self): self.assertEqual('policy', sot.resource_key) self.assertEqual('policies', sot.resources_key) self.assertEqual('/policies', sot.base_path) - self.assertEqual('clustering', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) @@ -81,7 +80,6 @@ def test_basic(self): self.assertEqual('policy', sot.resource_key) self.assertEqual('policies', sot.resources_key) self.assertEqual('/policies/validate', sot.base_path) - self.assertEqual('clustering', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/clustering/v1/test_policy_type.py b/openstack/tests/unit/clustering/v1/test_policy_type.py index 6ebf45db2..63f88fdec 100644 --- a/openstack/tests/unit/clustering/v1/test_policy_type.py +++ b/openstack/tests/unit/clustering/v1/test_policy_type.py @@ -36,7 +36,6 @@ def test_basic(self): self.assertEqual('policy_type', sot.resource_key) self.assertEqual('policy_types', sot.resources_key) self.assertEqual('/policy-types', sot.base_path) - self.assertEqual('clustering', sot.service.service_type) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/clustering/v1/test_profile.py b/openstack/tests/unit/clustering/v1/test_profile.py index 4165f4cec..abcaf098d 100644 --- a/openstack/tests/unit/clustering/v1/test_profile.py +++ b/openstack/tests/unit/clustering/v1/test_profile.py @@ -51,7 +51,6 @@ def test_basic(self): self.assertEqual('profile', sot.resource_key) self.assertEqual('profiles', sot.resources_key) self.assertEqual('/profiles', sot.base_path) - self.assertEqual('clustering', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) @@ -83,7 +82,6 @@ def test_basic(self): self.assertEqual('profile', sot.resource_key) self.assertEqual('profiles', sot.resources_key) self.assertEqual('/profiles/validate', sot.base_path) - self.assertEqual('clustering', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/clustering/v1/test_profile_type.py b/openstack/tests/unit/clustering/v1/test_profile_type.py index ee5e7fa43..f709c3d8c 100644 --- a/openstack/tests/unit/clustering/v1/test_profile_type.py +++ b/openstack/tests/unit/clustering/v1/test_profile_type.py @@ -37,7 +37,6 @@ def test_basic(self): self.assertEqual('profile_type', sot.resource_key) self.assertEqual('profile_types', sot.resources_key) self.assertEqual('/profile-types', sot.base_path) - self.assertEqual('clustering', sot.service.service_type) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/clustering/v1/test_receiver.py b/openstack/tests/unit/clustering/v1/test_receiver.py index 27b340c4d..197cfb01e 100644 --- a/openstack/tests/unit/clustering/v1/test_receiver.py +++ b/openstack/tests/unit/clustering/v1/test_receiver.py @@ -50,7 +50,6 @@ def test_basic(self): self.assertEqual('receiver', sot.resource_key) self.assertEqual('receivers', sot.resources_key) self.assertEqual('/receivers', sot.base_path) - self.assertEqual('clustering', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/clustering/v1/test_service.py b/openstack/tests/unit/clustering/v1/test_service.py index a97fa678d..f361a0b6f 100644 --- a/openstack/tests/unit/clustering/v1/test_service.py +++ b/openstack/tests/unit/clustering/v1/test_service.py @@ -41,7 +41,6 @@ def test_basic(self): self.assertEqual('service', sot.resource_key) self.assertEqual('services', sot.resources_key) self.assertEqual('/services', sot.base_path) - self.assertEqual('clustering', sot.service.service_type) self.assertTrue(sot.allow_list) def test_make_it(self): diff --git a/openstack/tests/unit/compute/test_compute_service.py b/openstack/tests/unit/compute/test_compute_service.py deleted file mode 100644 index 3a3ed7bbe..000000000 --- a/openstack/tests/unit/compute/test_compute_service.py +++ /dev/null @@ -1,28 +0,0 @@ -# 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.tests.unit import base - -from openstack.compute import compute_service - - -class TestComputeService(base.TestCase): - - def test_service(self): - sot = compute_service.ComputeService() - self.assertEqual('compute', sot.service_type) - self.assertEqual('public', sot.interface) - self.assertIsNone(sot.region) - self.assertIsNone(sot.service_name) - self.assertEqual(1, len(sot.valid_versions)) - self.assertEqual('v2', sot.valid_versions[0].module) - self.assertEqual('v2', sot.valid_versions[0].path) diff --git a/openstack/tests/unit/compute/test_version.py b/openstack/tests/unit/compute/test_version.py index 2f460f260..76fdaf2dd 100644 --- a/openstack/tests/unit/compute/test_version.py +++ b/openstack/tests/unit/compute/test_version.py @@ -30,7 +30,6 @@ def test_basic(self): self.assertEqual('version', sot.resource_key) self.assertEqual('versions', sot.resources_key) self.assertEqual('/', sot.base_path) - self.assertEqual('compute', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/compute/v2/test_availability_zone.py b/openstack/tests/unit/compute/v2/test_availability_zone.py index 66489eb58..f8d409366 100644 --- a/openstack/tests/unit/compute/v2/test_availability_zone.py +++ b/openstack/tests/unit/compute/v2/test_availability_zone.py @@ -30,14 +30,12 @@ def test_basic(self): self.assertEqual('availabilityZoneInfo', sot.resources_key) self.assertEqual('/os-availability-zone', sot.base_path) self.assertTrue(sot.allow_list) - self.assertEqual('compute', sot.service.service_type) def test_basic_detail(self): sot = az.AvailabilityZoneDetail() self.assertEqual('availabilityZoneInfo', sot.resources_key) self.assertEqual('/os-availability-zone/detail', sot.base_path) self.assertTrue(sot.allow_list) - self.assertEqual('compute', sot.service.service_type) def test_make_basic(self): sot = az.AvailabilityZone(**BASIC_EXAMPLE) diff --git a/openstack/tests/unit/compute/v2/test_extension.py b/openstack/tests/unit/compute/v2/test_extension.py index 1cc35f680..4ee1ad331 100644 --- a/openstack/tests/unit/compute/v2/test_extension.py +++ b/openstack/tests/unit/compute/v2/test_extension.py @@ -32,7 +32,6 @@ def test_basic(self): self.assertEqual('extension', sot.resource_key) self.assertEqual('extensions', sot.resources_key) self.assertEqual('/extensions', sot.base_path) - self.assertEqual('compute', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/compute/v2/test_flavor.py b/openstack/tests/unit/compute/v2/test_flavor.py index 9c5080c73..c781711d2 100644 --- a/openstack/tests/unit/compute/v2/test_flavor.py +++ b/openstack/tests/unit/compute/v2/test_flavor.py @@ -38,7 +38,6 @@ def test_basic(self): self.assertEqual('flavor', sot.resource_key) self.assertEqual('flavors', sot.resources_key) self.assertEqual('/flavors', sot.base_path) - self.assertEqual('compute', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_delete) @@ -77,7 +76,6 @@ def test_detail(self): self.assertEqual('flavor', sot.resource_key) self.assertEqual('flavors', sot.resources_key) self.assertEqual('/flavors/detail', sot.base_path) - self.assertEqual('compute', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/compute/v2/test_hypervisor.py b/openstack/tests/unit/compute/v2/test_hypervisor.py index ae369351e..feb96b18e 100644 --- a/openstack/tests/unit/compute/v2/test_hypervisor.py +++ b/openstack/tests/unit/compute/v2/test_hypervisor.py @@ -49,7 +49,6 @@ def test_basic(self): self.assertEqual('hypervisor', sot.resource_key) self.assertEqual('hypervisors', sot.resources_key) self.assertEqual('/os-hypervisors', sot.base_path) - self.assertEqual('compute', sot.service.service_type) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_list) @@ -82,6 +81,5 @@ def test_detail(self): self.assertEqual('hypervisor', sot.resource_key) self.assertEqual('hypervisors', sot.resources_key) self.assertEqual('/os-hypervisors/detail', sot.base_path) - self.assertEqual('compute', sot.service.service_type) self.assertFalse(sot.allow_fetch) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/compute/v2/test_image.py b/openstack/tests/unit/compute/v2/test_image.py index 24f7f2a91..59c101b50 100644 --- a/openstack/tests/unit/compute/v2/test_image.py +++ b/openstack/tests/unit/compute/v2/test_image.py @@ -43,7 +43,6 @@ def test_basic(self): self.assertEqual('image', sot.resource_key) self.assertEqual('images', sot.resources_key) self.assertEqual('/images', sot.base_path) - self.assertEqual('compute', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertFalse(sot.allow_commit) @@ -72,7 +71,6 @@ def test_detail(self): self.assertEqual('image', sot.resource_key) self.assertEqual('images', sot.resources_key) self.assertEqual('/images/detail', sot.base_path) - self.assertEqual('compute', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/compute/v2/test_keypair.py b/openstack/tests/unit/compute/v2/test_keypair.py index c0e7d974a..c058b5507 100644 --- a/openstack/tests/unit/compute/v2/test_keypair.py +++ b/openstack/tests/unit/compute/v2/test_keypair.py @@ -29,7 +29,6 @@ def test_basic(self): self.assertEqual('keypair', sot.resource_key) self.assertEqual('keypairs', sot.resources_key) self.assertEqual('/os-keypairs', sot.base_path) - self.assertEqual('compute', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/compute/v2/test_limits.py b/openstack/tests/unit/compute/v2/test_limits.py index 398d324e8..d77b8ebdf 100644 --- a/openstack/tests/unit/compute/v2/test_limits.py +++ b/openstack/tests/unit/compute/v2/test_limits.py @@ -69,7 +69,6 @@ def test_basic(self): self.assertIsNone(sot.resource_key) self.assertIsNone(sot.resources_key) self.assertEqual("", sot.base_path) - self.assertIsNone(sot.service) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_commit) @@ -139,7 +138,6 @@ def test_basic(self): sot = limits.Limits() self.assertEqual("limits", sot.resource_key) self.assertEqual("/limits", sot.base_path) - self.assertEqual("compute", sot.service.service_type) self.assertTrue(sot.allow_fetch) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 45bf4ebc2..7de99b494 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -73,7 +73,6 @@ def test_basic(self): self.assertEqual('server', sot.resource_key) self.assertEqual('servers', sot.resources_key) self.assertEqual('/servers', sot.base_path) - self.assertEqual('compute', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) @@ -155,7 +154,6 @@ def test_detail(self): self.assertEqual('server', sot.resource_key) self.assertEqual('servers', sot.resources_key) self.assertEqual('/servers/detail', sot.base_path) - self.assertEqual('compute', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/compute/v2/test_server_group.py b/openstack/tests/unit/compute/v2/test_server_group.py index d9c954dd7..173761364 100644 --- a/openstack/tests/unit/compute/v2/test_server_group.py +++ b/openstack/tests/unit/compute/v2/test_server_group.py @@ -30,7 +30,6 @@ def test_basic(self): self.assertEqual('server_group', sot.resource_key) self.assertEqual('server_groups', sot.resources_key) self.assertEqual('/os-server-groups', sot.base_path) - self.assertEqual('compute', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/compute/v2/test_server_interface.py b/openstack/tests/unit/compute/v2/test_server_interface.py index 973923204..9b4fa7f2b 100644 --- a/openstack/tests/unit/compute/v2/test_server_interface.py +++ b/openstack/tests/unit/compute/v2/test_server_interface.py @@ -37,7 +37,6 @@ def test_basic(self): self.assertEqual('interfaceAttachment', sot.resource_key) self.assertEqual('interfaceAttachments', sot.resources_key) self.assertEqual('/servers/%(server_id)s/os-interface', sot.base_path) - self.assertEqual('compute', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/compute/v2/test_server_ip.py b/openstack/tests/unit/compute/v2/test_server_ip.py index 66fed38df..6cbc0e94a 100644 --- a/openstack/tests/unit/compute/v2/test_server_ip.py +++ b/openstack/tests/unit/compute/v2/test_server_ip.py @@ -29,7 +29,6 @@ def test_basic(self): sot = server_ip.ServerIP() self.assertEqual('addresses', sot.resources_key) self.assertEqual('/servers/%(server_id)s/ips', sot.base_path) - self.assertEqual('compute', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/compute/v2/test_service.py b/openstack/tests/unit/compute/v2/test_service.py index 094792a7f..159423505 100644 --- a/openstack/tests/unit/compute/v2/test_service.py +++ b/openstack/tests/unit/compute/v2/test_service.py @@ -41,7 +41,6 @@ def test_basic(self): self.assertEqual('service', sot.resource_key) self.assertEqual('services', sot.resources_key) self.assertEqual('/os-services', sot.base_path) - self.assertEqual('compute', sot.service.service_type) self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_list) self.assertFalse(sot.allow_fetch) diff --git a/openstack/tests/unit/compute/v2/test_volume_attachment.py b/openstack/tests/unit/compute/v2/test_volume_attachment.py index 623a931e7..66068dce9 100644 --- a/openstack/tests/unit/compute/v2/test_volume_attachment.py +++ b/openstack/tests/unit/compute/v2/test_volume_attachment.py @@ -29,7 +29,6 @@ def test_basic(self): self.assertEqual('volumeAttachments', sot.resources_key) self.assertEqual('/servers/%(server_id)s/os-volume_attachments', sot.base_path) - self.assertEqual('compute', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/database/test_database_service.py b/openstack/tests/unit/database/test_database_service.py deleted file mode 100644 index 85c57d887..000000000 --- a/openstack/tests/unit/database/test_database_service.py +++ /dev/null @@ -1,28 +0,0 @@ -# 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.tests.unit import base - -from openstack.database import database_service - - -class TestDatabaseService(base.TestCase): - - def test_service(self): - sot = database_service.DatabaseService() - self.assertEqual('database', sot.service_type) - self.assertEqual('public', sot.interface) - self.assertIsNone(sot.region) - self.assertIsNone(sot.service_name) - self.assertEqual(1, len(sot.valid_versions)) - self.assertEqual('v1', sot.valid_versions[0].module) - self.assertEqual('v1', sot.valid_versions[0].path) diff --git a/openstack/tests/unit/database/v1/test_database.py b/openstack/tests/unit/database/v1/test_database.py index 477672a36..08df9e118 100644 --- a/openstack/tests/unit/database/v1/test_database.py +++ b/openstack/tests/unit/database/v1/test_database.py @@ -33,7 +33,6 @@ def test_basic(self): self.assertEqual('databases', sot.resources_key) path = '/instances/%(instance_id)s/databases' self.assertEqual(path, sot.base_path) - self.assertEqual('database', sot.service.service_type) self.assertTrue(sot.allow_list) self.assertTrue(sot.allow_create) self.assertFalse(sot.allow_fetch) diff --git a/openstack/tests/unit/database/v1/test_flavor.py b/openstack/tests/unit/database/v1/test_flavor.py index 82f850e6f..4fdd21f60 100644 --- a/openstack/tests/unit/database/v1/test_flavor.py +++ b/openstack/tests/unit/database/v1/test_flavor.py @@ -30,7 +30,6 @@ def test_basic(self): self.assertEqual('flavor', sot.resource_key) self.assertEqual('flavors', sot.resources_key) self.assertEqual('/flavors', sot.base_path) - self.assertEqual('database', sot.service.service_type) self.assertTrue(sot.allow_list) self.assertFalse(sot.allow_create) self.assertTrue(sot.allow_fetch) diff --git a/openstack/tests/unit/database/v1/test_instance.py b/openstack/tests/unit/database/v1/test_instance.py index 8fd650ebb..98a8c8a54 100644 --- a/openstack/tests/unit/database/v1/test_instance.py +++ b/openstack/tests/unit/database/v1/test_instance.py @@ -38,7 +38,6 @@ def test_basic(self): self.assertEqual('instance', sot.resource_key) self.assertEqual('instances', sot.resources_key) self.assertEqual('/instances', sot.base_path) - self.assertEqual('database', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/database/v1/test_user.py b/openstack/tests/unit/database/v1/test_user.py index 5752024d7..ab4b63fe4 100644 --- a/openstack/tests/unit/database/v1/test_user.py +++ b/openstack/tests/unit/database/v1/test_user.py @@ -30,7 +30,6 @@ def test_basic(self): self.assertEqual('user', sot.resource_key) self.assertEqual('users', sot.resources_key) self.assertEqual('/instances/%(instance_id)s/users', sot.base_path) - self.assertEqual('database', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/identity/test_identity_service.py b/openstack/tests/unit/identity/test_identity_service.py deleted file mode 100644 index 47125928b..000000000 --- a/openstack/tests/unit/identity/test_identity_service.py +++ /dev/null @@ -1,37 +0,0 @@ -# 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.tests.unit import base - -from openstack.identity import identity_service - - -class TestIdentityService(base.TestCase): - - def test_regular_service(self): - sot = identity_service.IdentityService() - self.assertEqual('identity', sot.service_type) - self.assertEqual('public', sot.interface) - self.assertIsNone(sot.region) - self.assertIsNone(sot.service_name) - self.assertEqual(2, len(sot.valid_versions)) - self.assertEqual('v3', sot.valid_versions[0].module) - self.assertEqual('v3', sot.valid_versions[0].path) - self.assertEqual('v2', sot.valid_versions[1].module) - self.assertEqual('v2', sot.valid_versions[1].path) - - def test_admin_service(self): - sot = identity_service.AdminService() - self.assertEqual('identity', sot.service_type) - self.assertEqual('admin', sot.interface) - self.assertIsNone(sot.region) - self.assertIsNone(sot.service_name) diff --git a/openstack/tests/unit/identity/test_version.py b/openstack/tests/unit/identity/test_version.py index c18eaadef..c4351ebd9 100644 --- a/openstack/tests/unit/identity/test_version.py +++ b/openstack/tests/unit/identity/test_version.py @@ -31,7 +31,6 @@ def test_basic(self): self.assertEqual('version', sot.resource_key) self.assertEqual('versions', sot.resources_key) self.assertEqual('/', sot.base_path) - self.assertEqual('identity', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/identity/v2/test_extension.py b/openstack/tests/unit/identity/v2/test_extension.py index 557d3fb54..d7ff73145 100644 --- a/openstack/tests/unit/identity/v2/test_extension.py +++ b/openstack/tests/unit/identity/v2/test_extension.py @@ -33,7 +33,6 @@ def test_basic(self): self.assertEqual('extension', sot.resource_key) self.assertEqual('extensions', sot.resources_key) self.assertEqual('/extensions', sot.base_path) - self.assertEqual('identity', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/identity/v2/test_role.py b/openstack/tests/unit/identity/v2/test_role.py index 02912a9f2..bb7b9f79a 100644 --- a/openstack/tests/unit/identity/v2/test_role.py +++ b/openstack/tests/unit/identity/v2/test_role.py @@ -30,7 +30,6 @@ def test_basic(self): self.assertEqual('role', sot.resource_key) self.assertEqual('roles', sot.resources_key) self.assertEqual('/OS-KSADM/roles', sot.base_path) - self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/identity/v2/test_tenant.py b/openstack/tests/unit/identity/v2/test_tenant.py index 3c7fd7bf4..31847538f 100644 --- a/openstack/tests/unit/identity/v2/test_tenant.py +++ b/openstack/tests/unit/identity/v2/test_tenant.py @@ -30,7 +30,6 @@ def test_basic(self): self.assertEqual('tenant', sot.resource_key) self.assertEqual('tenants', sot.resources_key) self.assertEqual('/tenants', sot.base_path) - self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/identity/v2/test_user.py b/openstack/tests/unit/identity/v2/test_user.py index 0c495c2e5..300b20b0d 100644 --- a/openstack/tests/unit/identity/v2/test_user.py +++ b/openstack/tests/unit/identity/v2/test_user.py @@ -30,7 +30,6 @@ def test_basic(self): self.assertEqual('user', sot.resource_key) self.assertEqual('users', sot.resources_key) self.assertEqual('/users', sot.base_path) - self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/identity/v3/test_credential.py b/openstack/tests/unit/identity/v3/test_credential.py index 7f6d2dc1d..0e073f97c 100644 --- a/openstack/tests/unit/identity/v3/test_credential.py +++ b/openstack/tests/unit/identity/v3/test_credential.py @@ -31,7 +31,6 @@ def test_basic(self): self.assertEqual('credential', sot.resource_key) self.assertEqual('credentials', sot.resources_key) self.assertEqual('/credentials', sot.base_path) - self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/identity/v3/test_domain.py b/openstack/tests/unit/identity/v3/test_domain.py index 33ec294dd..782e3f277 100644 --- a/openstack/tests/unit/identity/v3/test_domain.py +++ b/openstack/tests/unit/identity/v3/test_domain.py @@ -31,7 +31,6 @@ def test_basic(self): self.assertEqual('domain', sot.resource_key) self.assertEqual('domains', sot.resources_key) self.assertEqual('/domains', sot.base_path) - self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/identity/v3/test_endpoint.py b/openstack/tests/unit/identity/v3/test_endpoint.py index 090c0b346..da5a4e818 100644 --- a/openstack/tests/unit/identity/v3/test_endpoint.py +++ b/openstack/tests/unit/identity/v3/test_endpoint.py @@ -33,7 +33,6 @@ def test_basic(self): self.assertEqual('endpoint', sot.resource_key) self.assertEqual('endpoints', sot.resources_key) self.assertEqual('/endpoints', sot.base_path) - self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/identity/v3/test_group.py b/openstack/tests/unit/identity/v3/test_group.py index 515ee9c3b..bc308e6ce 100644 --- a/openstack/tests/unit/identity/v3/test_group.py +++ b/openstack/tests/unit/identity/v3/test_group.py @@ -30,7 +30,6 @@ def test_basic(self): self.assertEqual('group', sot.resource_key) self.assertEqual('groups', sot.resources_key) self.assertEqual('/groups', sot.base_path) - self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/identity/v3/test_policy.py b/openstack/tests/unit/identity/v3/test_policy.py index 885d7b002..4bd2256f8 100644 --- a/openstack/tests/unit/identity/v3/test_policy.py +++ b/openstack/tests/unit/identity/v3/test_policy.py @@ -32,7 +32,6 @@ def test_basic(self): self.assertEqual('policy', sot.resource_key) self.assertEqual('policies', sot.resources_key) self.assertEqual('/policies', sot.base_path) - self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/identity/v3/test_project.py b/openstack/tests/unit/identity/v3/test_project.py index 62d3437a3..6f2c1d145 100644 --- a/openstack/tests/unit/identity/v3/test_project.py +++ b/openstack/tests/unit/identity/v3/test_project.py @@ -33,7 +33,6 @@ def test_basic(self): self.assertEqual('project', sot.resource_key) self.assertEqual('projects', sot.resources_key) self.assertEqual('/projects', sot.base_path) - self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) @@ -71,7 +70,6 @@ def test_basic(self): self.assertEqual('project', sot.resource_key) self.assertEqual('projects', sot.resources_key) self.assertEqual('/users/%(user_id)s/projects', sot.base_path) - self.assertEqual('identity', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/identity/v3/test_region.py b/openstack/tests/unit/identity/v3/test_region.py index 6657ca5cb..1ac7a745b 100644 --- a/openstack/tests/unit/identity/v3/test_region.py +++ b/openstack/tests/unit/identity/v3/test_region.py @@ -30,7 +30,6 @@ def test_basic(self): self.assertEqual('region', sot.resource_key) self.assertEqual('regions', sot.resources_key) self.assertEqual('/regions', sot.base_path) - self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/identity/v3/test_role.py b/openstack/tests/unit/identity/v3/test_role.py index a32e616cd..4a67d6dc4 100644 --- a/openstack/tests/unit/identity/v3/test_role.py +++ b/openstack/tests/unit/identity/v3/test_role.py @@ -29,7 +29,6 @@ def test_basic(self): self.assertEqual('role', sot.resource_key) self.assertEqual('roles', sot.resources_key) self.assertEqual('/roles', sot.base_path) - self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/identity/v3/test_role_assignment.py b/openstack/tests/unit/identity/v3/test_role_assignment.py index c54df84bd..fac7a6dcf 100644 --- a/openstack/tests/unit/identity/v3/test_role_assignment.py +++ b/openstack/tests/unit/identity/v3/test_role_assignment.py @@ -32,7 +32,6 @@ def test_basic(self): self.assertEqual('role_assignments', sot.resources_key) self.assertEqual('/role_assignments', sot.base_path) - self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_list) def test_make_it(self): diff --git a/openstack/tests/unit/identity/v3/test_role_domain_group_assignment.py b/openstack/tests/unit/identity/v3/test_role_domain_group_assignment.py index 92b6539a0..764da5035 100644 --- a/openstack/tests/unit/identity/v3/test_role_domain_group_assignment.py +++ b/openstack/tests/unit/identity/v3/test_role_domain_group_assignment.py @@ -32,7 +32,6 @@ def test_basic(self): self.assertEqual('roles', sot.resources_key) self.assertEqual('/domains/%(domain_id)s/groups/%(group_id)s/roles', sot.base_path) - self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_list) def test_make_it(self): diff --git a/openstack/tests/unit/identity/v3/test_role_domain_user_assignment.py b/openstack/tests/unit/identity/v3/test_role_domain_user_assignment.py index bd19a8daf..185c89ce8 100644 --- a/openstack/tests/unit/identity/v3/test_role_domain_user_assignment.py +++ b/openstack/tests/unit/identity/v3/test_role_domain_user_assignment.py @@ -32,7 +32,6 @@ def test_basic(self): self.assertEqual('roles', sot.resources_key) self.assertEqual('/domains/%(domain_id)s/users/%(user_id)s/roles', sot.base_path) - self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_list) def test_make_it(self): diff --git a/openstack/tests/unit/identity/v3/test_role_project_group_assignment.py b/openstack/tests/unit/identity/v3/test_role_project_group_assignment.py index 4070909a9..28351652e 100644 --- a/openstack/tests/unit/identity/v3/test_role_project_group_assignment.py +++ b/openstack/tests/unit/identity/v3/test_role_project_group_assignment.py @@ -32,7 +32,6 @@ def test_basic(self): self.assertEqual('roles', sot.resources_key) self.assertEqual('/projects/%(project_id)s/groups/%(group_id)s/roles', sot.base_path) - self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_list) def test_make_it(self): diff --git a/openstack/tests/unit/identity/v3/test_role_project_user_assignment.py b/openstack/tests/unit/identity/v3/test_role_project_user_assignment.py index c04ffdaa1..61e1b8963 100644 --- a/openstack/tests/unit/identity/v3/test_role_project_user_assignment.py +++ b/openstack/tests/unit/identity/v3/test_role_project_user_assignment.py @@ -32,7 +32,6 @@ def test_basic(self): self.assertEqual('roles', sot.resources_key) self.assertEqual('/projects/%(project_id)s/users/%(user_id)s/roles', sot.base_path) - self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_list) def test_make_it(self): diff --git a/openstack/tests/unit/identity/v3/test_service.py b/openstack/tests/unit/identity/v3/test_service.py index d6716f3f6..a6da1fddd 100644 --- a/openstack/tests/unit/identity/v3/test_service.py +++ b/openstack/tests/unit/identity/v3/test_service.py @@ -32,7 +32,6 @@ def test_basic(self): self.assertEqual('service', sot.resource_key) self.assertEqual('services', sot.resources_key) self.assertEqual('/services', sot.base_path) - self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/identity/v3/test_trust.py b/openstack/tests/unit/identity/v3/test_trust.py index 7028e0af0..63c38a70c 100644 --- a/openstack/tests/unit/identity/v3/test_trust.py +++ b/openstack/tests/unit/identity/v3/test_trust.py @@ -39,7 +39,6 @@ def test_basic(self): self.assertEqual('trust', sot.resource_key) self.assertEqual('trusts', sot.resources_key) self.assertEqual('/OS-TRUST/trusts', sot.base_path) - self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_delete) diff --git a/openstack/tests/unit/identity/v3/test_user.py b/openstack/tests/unit/identity/v3/test_user.py index f75eb6e7d..9d8040467 100644 --- a/openstack/tests/unit/identity/v3/test_user.py +++ b/openstack/tests/unit/identity/v3/test_user.py @@ -36,7 +36,6 @@ def test_basic(self): self.assertEqual('user', sot.resource_key) self.assertEqual('users', sot.resources_key) self.assertEqual('/users', sot.base_path) - self.assertEqual('identity', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/image/test_image_service.py b/openstack/tests/unit/image/test_image_service.py deleted file mode 100644 index 236227430..000000000 --- a/openstack/tests/unit/image/test_image_service.py +++ /dev/null @@ -1,30 +0,0 @@ -# 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.tests.unit import base - -from openstack.image import image_service - - -class TestImageService(base.TestCase): - - def test_service(self): - sot = image_service.ImageService() - self.assertEqual('image', sot.service_type) - self.assertEqual('public', sot.interface) - self.assertIsNone(sot.region) - self.assertIsNone(sot.service_name) - self.assertEqual(2, len(sot.valid_versions)) - self.assertEqual('v2', sot.valid_versions[0].module) - self.assertEqual('v2', sot.valid_versions[0].path) - self.assertEqual('v1', sot.valid_versions[1].module) - self.assertEqual('v1', sot.valid_versions[1].path) diff --git a/openstack/tests/unit/image/v1/test_image.py b/openstack/tests/unit/image/v1/test_image.py index 256f9892c..133c4232e 100644 --- a/openstack/tests/unit/image/v1/test_image.py +++ b/openstack/tests/unit/image/v1/test_image.py @@ -43,7 +43,6 @@ def test_basic(self): self.assertEqual('image', sot.resource_key) self.assertEqual('images', sot.resources_key) self.assertEqual('/images', sot.base_path) - self.assertEqual('image', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index e2d1ba1b1..94934122a 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -110,7 +110,6 @@ def test_basic(self): self.assertIsNone(sot.resource_key) self.assertEqual('images', sot.resources_key) self.assertEqual('/images', sot.base_path) - self.assertEqual('image', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/image/v2/test_member.py b/openstack/tests/unit/image/v2/test_member.py index e0e2a6fe0..7fc603862 100644 --- a/openstack/tests/unit/image/v2/test_member.py +++ b/openstack/tests/unit/image/v2/test_member.py @@ -30,7 +30,6 @@ def test_basic(self): self.assertIsNone(sot.resource_key) self.assertEqual('members', sot.resources_key) self.assertEqual('/images/%(image_id)s/members', sot.base_path) - self.assertEqual('image', sot.service.service_type) self.assertEqual('member', sot._alternate_id()) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) diff --git a/openstack/tests/unit/instance_ha/test_instance_ha_service.py b/openstack/tests/unit/instance_ha/test_instance_ha_service.py deleted file mode 100644 index fa46986dd..000000000 --- a/openstack/tests/unit/instance_ha/test_instance_ha_service.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright(c) 2018 Nippon Telegraph and Telephone 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. - -from openstack.tests.unit import base - -from openstack.instance_ha import instance_ha_service - - -class TestInstanceHaService(base.TestCase): - - def test_service(self): - sot = instance_ha_service.InstanceHaService() - self.assertEqual("ha", sot.service_type) - self.assertEqual("public", sot.interface) - self.assertIsNone(sot.region) - self.assertIsNone(sot.service_name) - self.assertEqual(1, len(sot.valid_versions)) - self.assertEqual("v1", sot.valid_versions[0].module) - self.assertEqual("v1", sot.valid_versions[0].path) diff --git a/openstack/tests/unit/instance_ha/v1/test_host.py b/openstack/tests/unit/instance_ha/v1/test_host.py index 2d20bdfdc..4b388a663 100644 --- a/openstack/tests/unit/instance_ha/v1/test_host.py +++ b/openstack/tests/unit/instance_ha/v1/test_host.py @@ -44,7 +44,6 @@ def test_basic(self): self.assertEqual("host", sot.resource_key) self.assertEqual("hosts", sot.resources_key) self.assertEqual("/segments/%(segment_id)s/hosts", sot.base_path) - self.assertEqual("ha", sot.service.service_type) self.assertTrue(sot.allow_list) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_create) diff --git a/openstack/tests/unit/instance_ha/v1/test_notification.py b/openstack/tests/unit/instance_ha/v1/test_notification.py index e5566404f..c5e94a03f 100644 --- a/openstack/tests/unit/instance_ha/v1/test_notification.py +++ b/openstack/tests/unit/instance_ha/v1/test_notification.py @@ -44,7 +44,6 @@ def test_basic(self): self.assertEqual("notification", sot.resource_key) self.assertEqual("notifications", sot.resources_key) self.assertEqual("/notifications", sot.base_path) - self.assertEqual("ha", sot.service.service_type) self.assertTrue(sot.allow_list) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_create) diff --git a/openstack/tests/unit/instance_ha/v1/test_segment.py b/openstack/tests/unit/instance_ha/v1/test_segment.py index 7659eb8e1..e38fefae4 100644 --- a/openstack/tests/unit/instance_ha/v1/test_segment.py +++ b/openstack/tests/unit/instance_ha/v1/test_segment.py @@ -36,7 +36,6 @@ def test_basic(self): self.assertEqual("segment", sot.resource_key) self.assertEqual("segments", sot.resources_key) self.assertEqual("/segments", sot.base_path) - self.assertEqual("ha", sot.service.service_type) self.assertTrue(sot.allow_list) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_create) diff --git a/openstack/tests/unit/key_manager/test_key_management_service.py b/openstack/tests/unit/key_manager/test_key_management_service.py deleted file mode 100644 index 4024f2ab7..000000000 --- a/openstack/tests/unit/key_manager/test_key_management_service.py +++ /dev/null @@ -1,28 +0,0 @@ -# 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.tests.unit import base - -from openstack.key_manager import key_manager_service - - -class TestKeyManagerService(base.TestCase): - - def test_service(self): - sot = key_manager_service.KeyManagerService() - self.assertEqual('key-manager', sot.service_type) - self.assertEqual('public', sot.interface) - self.assertIsNone(sot.region) - self.assertIsNone(sot.service_name) - self.assertEqual(1, len(sot.valid_versions)) - self.assertEqual('v1', sot.valid_versions[0].module) - self.assertEqual('v1', sot.valid_versions[0].path) diff --git a/openstack/tests/unit/key_manager/v1/test_container.py b/openstack/tests/unit/key_manager/v1/test_container.py index ffc395a47..2095e6c41 100644 --- a/openstack/tests/unit/key_manager/v1/test_container.py +++ b/openstack/tests/unit/key_manager/v1/test_container.py @@ -35,7 +35,6 @@ def test_basic(self): self.assertIsNone(sot.resource_key) self.assertEqual('containers', sot.resources_key) self.assertEqual('/containers', sot.base_path) - self.assertEqual('key-manager', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/key_manager/v1/test_order.py b/openstack/tests/unit/key_manager/v1/test_order.py index fd246e418..de357678a 100644 --- a/openstack/tests/unit/key_manager/v1/test_order.py +++ b/openstack/tests/unit/key_manager/v1/test_order.py @@ -38,7 +38,6 @@ def test_basic(self): self.assertIsNone(sot.resource_key) self.assertEqual('orders', sot.resources_key) self.assertEqual('/orders', sot.base_path) - self.assertEqual('key-manager', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/key_manager/v1/test_secret.py b/openstack/tests/unit/key_manager/v1/test_secret.py index ae4154c4d..96a43e949 100644 --- a/openstack/tests/unit/key_manager/v1/test_secret.py +++ b/openstack/tests/unit/key_manager/v1/test_secret.py @@ -42,7 +42,6 @@ def test_basic(self): self.assertIsNone(sot.resource_key) self.assertEqual('secrets', sot.resources_key) self.assertEqual('/secrets', sot.base_path) - self.assertEqual('key-manager', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/load_balancer/test_health_monitor.py b/openstack/tests/unit/load_balancer/test_health_monitor.py index 0840f44a5..710a8e79b 100644 --- a/openstack/tests/unit/load_balancer/test_health_monitor.py +++ b/openstack/tests/unit/load_balancer/test_health_monitor.py @@ -44,8 +44,7 @@ def test_basic(self): test_hm = health_monitor.HealthMonitor() self.assertEqual('healthmonitor', test_hm.resource_key) self.assertEqual('healthmonitors', test_hm.resources_key) - self.assertEqual('/v2.0/lbaas/healthmonitors', test_hm.base_path) - self.assertEqual('load-balancer', test_hm.service.service_type) + self.assertEqual('/lbaas/healthmonitors', test_hm.base_path) self.assertTrue(test_hm.allow_create) self.assertTrue(test_hm.allow_fetch) self.assertTrue(test_hm.allow_commit) diff --git a/openstack/tests/unit/load_balancer/test_l7policy.py b/openstack/tests/unit/load_balancer/test_l7policy.py index 91a62ff0a..f8001ad69 100644 --- a/openstack/tests/unit/load_balancer/test_l7policy.py +++ b/openstack/tests/unit/load_balancer/test_l7policy.py @@ -40,8 +40,7 @@ def test_basic(self): test_l7_policy = l7_policy.L7Policy() self.assertEqual('l7policy', test_l7_policy.resource_key) self.assertEqual('l7policies', test_l7_policy.resources_key) - self.assertEqual('/v2.0/lbaas/l7policies', test_l7_policy.base_path) - self.assertEqual('load-balancer', test_l7_policy.service.service_type) + self.assertEqual('/lbaas/l7policies', test_l7_policy.base_path) self.assertTrue(test_l7_policy.allow_create) self.assertTrue(test_l7_policy.allow_fetch) self.assertTrue(test_l7_policy.allow_commit) diff --git a/openstack/tests/unit/load_balancer/test_l7rule.py b/openstack/tests/unit/load_balancer/test_l7rule.py index b46f0b3c1..c67615a5c 100644 --- a/openstack/tests/unit/load_balancer/test_l7rule.py +++ b/openstack/tests/unit/load_balancer/test_l7rule.py @@ -38,9 +38,8 @@ def test_basic(self): test_l7rule = l7_rule.L7Rule() self.assertEqual('rule', test_l7rule.resource_key) self.assertEqual('rules', test_l7rule.resources_key) - self.assertEqual('/v2.0/lbaas/l7policies/%(l7policy_id)s/rules', + self.assertEqual('/lbaas/l7policies/%(l7policy_id)s/rules', test_l7rule.base_path) - self.assertEqual('load-balancer', test_l7rule.service.service_type) self.assertTrue(test_l7rule.allow_create) self.assertTrue(test_l7rule.allow_fetch) self.assertTrue(test_l7rule.allow_commit) diff --git a/openstack/tests/unit/load_balancer/test_listener.py b/openstack/tests/unit/load_balancer/test_listener.py index b104d8e84..93aad261b 100644 --- a/openstack/tests/unit/load_balancer/test_listener.py +++ b/openstack/tests/unit/load_balancer/test_listener.py @@ -49,8 +49,7 @@ def test_basic(self): test_listener = listener.Listener() self.assertEqual('listener', test_listener.resource_key) self.assertEqual('listeners', test_listener.resources_key) - self.assertEqual('/v2.0/lbaas/listeners', test_listener.base_path) - self.assertEqual('load-balancer', test_listener.service.service_type) + self.assertEqual('/lbaas/listeners', test_listener.base_path) self.assertTrue(test_listener.allow_create) self.assertTrue(test_listener.allow_fetch) self.assertTrue(test_listener.allow_commit) diff --git a/openstack/tests/unit/load_balancer/test_load_balancer.py b/openstack/tests/unit/load_balancer/test_load_balancer.py index 7a96179e4..4c2ae6d59 100644 --- a/openstack/tests/unit/load_balancer/test_load_balancer.py +++ b/openstack/tests/unit/load_balancer/test_load_balancer.py @@ -45,10 +45,8 @@ def test_basic(self): test_load_balancer = load_balancer.LoadBalancer() self.assertEqual('loadbalancer', test_load_balancer.resource_key) self.assertEqual('loadbalancers', test_load_balancer.resources_key) - self.assertEqual('/v2.0/lbaas/loadbalancers', + self.assertEqual('/lbaas/loadbalancers', test_load_balancer.base_path) - self.assertEqual('load-balancer', - test_load_balancer.service.service_type) self.assertTrue(test_load_balancer.allow_create) self.assertTrue(test_load_balancer.allow_fetch) self.assertTrue(test_load_balancer.allow_delete) @@ -94,7 +92,7 @@ def test_delete_non_cascade(self): sot._translate_response = mock.Mock() sot.delete(sess) - url = 'v2.0/lbaas/loadbalancers/%(lb)s' % { + url = 'lbaas/loadbalancers/%(lb)s' % { 'lb': EXAMPLE['id'] } headers = {'Accept': ''} @@ -118,7 +116,7 @@ def test_delete_cascade(self): sot._translate_response = mock.Mock() sot.delete(sess) - url = 'v2.0/lbaas/loadbalancers/%(lb)s' % { + url = 'lbaas/loadbalancers/%(lb)s' % { 'lb': EXAMPLE['id'] } headers = {'Accept': ''} diff --git a/openstack/tests/unit/load_balancer/test_load_balancer_service.py b/openstack/tests/unit/load_balancer/test_load_balancer_service.py deleted file mode 100644 index 9d9a31f83..000000000 --- a/openstack/tests/unit/load_balancer/test_load_balancer_service.py +++ /dev/null @@ -1,28 +0,0 @@ -# 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.tests.unit import base - -from openstack.load_balancer import load_balancer_service as lb_service - - -class TestLoadBalancingService(base.TestCase): - - def test_service(self): - sot = lb_service.LoadBalancerService() - self.assertEqual('load-balancer', sot.service_type) - self.assertEqual('public', sot.interface) - self.assertIsNone(sot.region) - self.assertIsNone(sot.service_name) - self.assertEqual(1, len(sot.valid_versions)) - self.assertEqual('v2', sot.valid_versions[0].module) - self.assertEqual('v2.0', sot.valid_versions[0].path) diff --git a/openstack/tests/unit/load_balancer/test_member.py b/openstack/tests/unit/load_balancer/test_member.py index 0d40da371..4efcadc40 100644 --- a/openstack/tests/unit/load_balancer/test_member.py +++ b/openstack/tests/unit/load_balancer/test_member.py @@ -38,9 +38,8 @@ def test_basic(self): test_member = member.Member() self.assertEqual('member', test_member.resource_key) self.assertEqual('members', test_member.resources_key) - self.assertEqual('/v2.0/lbaas/pools/%(pool_id)s/members', + self.assertEqual('/lbaas/pools/%(pool_id)s/members', test_member.base_path) - self.assertEqual('load-balancer', test_member.service.service_type) self.assertTrue(test_member.allow_create) self.assertTrue(test_member.allow_fetch) self.assertTrue(test_member.allow_commit) diff --git a/openstack/tests/unit/load_balancer/test_pool.py b/openstack/tests/unit/load_balancer/test_pool.py index fe8a81338..5465798fe 100644 --- a/openstack/tests/unit/load_balancer/test_pool.py +++ b/openstack/tests/unit/load_balancer/test_pool.py @@ -44,9 +44,7 @@ def test_basic(self): test_pool = pool.Pool() self.assertEqual('pool', test_pool.resource_key) self.assertEqual('pools', test_pool.resources_key) - self.assertEqual('/v2.0/lbaas/pools', test_pool.base_path) - self.assertEqual('load-balancer', - test_pool.service.service_type) + self.assertEqual('/lbaas/pools', test_pool.base_path) self.assertTrue(test_pool.allow_create) self.assertTrue(test_pool.allow_fetch) self.assertTrue(test_pool.allow_delete) diff --git a/openstack/tests/unit/load_balancer/test_version.py b/openstack/tests/unit/load_balancer/test_version.py index e09a57faa..0e141dfce 100644 --- a/openstack/tests/unit/load_balancer/test_version.py +++ b/openstack/tests/unit/load_balancer/test_version.py @@ -29,7 +29,6 @@ def test_basic(self): self.assertEqual('version', sot.resource_key) self.assertEqual('versions', sot.resources_key) self.assertEqual('/', sot.base_path) - self.assertEqual('load-balancer', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/message/test_message_service.py b/openstack/tests/unit/message/test_message_service.py deleted file mode 100644 index d208c4706..000000000 --- a/openstack/tests/unit/message/test_message_service.py +++ /dev/null @@ -1,28 +0,0 @@ -# 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.tests.unit import base - -from openstack.message import message_service - - -class TestMessageService(base.TestCase): - - def test_service(self): - sot = message_service.MessageService() - self.assertEqual('messaging', sot.service_type) - self.assertEqual('public', sot.interface) - self.assertIsNone(sot.region) - self.assertIsNone(sot.service_name) - self.assertEqual(1, len(sot.valid_versions)) - self.assertEqual('v2', sot.valid_versions[0].module) - self.assertEqual('v2', sot.valid_versions[0].path) diff --git a/openstack/tests/unit/message/test_version.py b/openstack/tests/unit/message/test_version.py index f9a3669fb..6e3e9bc72 100644 --- a/openstack/tests/unit/message/test_version.py +++ b/openstack/tests/unit/message/test_version.py @@ -29,7 +29,6 @@ def test_basic(self): self.assertEqual('version', sot.resource_key) self.assertEqual('versions', sot.resources_key) self.assertEqual('/', sot.base_path) - self.assertEqual('messaging', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/message/v2/test_claim.py b/openstack/tests/unit/message/v2/test_claim.py index 0ef520990..d2ebc1350 100644 --- a/openstack/tests/unit/message/v2/test_claim.py +++ b/openstack/tests/unit/message/v2/test_claim.py @@ -47,7 +47,6 @@ def test_basic(self): sot = claim.Claim() self.assertEqual("claims", sot.resources_key) self.assertEqual("/queues/%(queue_name)s/claims", sot.base_path) - self.assertEqual("messaging", sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_delete) diff --git a/openstack/tests/unit/message/v2/test_message.py b/openstack/tests/unit/message/v2/test_message.py index cbee7ea7d..d00af7c62 100644 --- a/openstack/tests/unit/message/v2/test_message.py +++ b/openstack/tests/unit/message/v2/test_message.py @@ -52,7 +52,6 @@ def test_basic(self): sot = message.Message() self.assertEqual('messages', sot.resources_key) self.assertEqual('/queues/%(queue_name)s/messages', sot.base_path) - self.assertEqual('messaging', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_delete) diff --git a/openstack/tests/unit/message/v2/test_queue.py b/openstack/tests/unit/message/v2/test_queue.py index c8277aac3..1183076f1 100644 --- a/openstack/tests/unit/message/v2/test_queue.py +++ b/openstack/tests/unit/message/v2/test_queue.py @@ -40,7 +40,6 @@ def test_basic(self): sot = queue.Queue() self.assertEqual('queues', sot.resources_key) self.assertEqual('/queues', sot.base_path) - self.assertEqual('messaging', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_delete) diff --git a/openstack/tests/unit/message/v2/test_subscription.py b/openstack/tests/unit/message/v2/test_subscription.py index d465a7f05..c334a3526 100644 --- a/openstack/tests/unit/message/v2/test_subscription.py +++ b/openstack/tests/unit/message/v2/test_subscription.py @@ -53,7 +53,6 @@ def test_basic(self): sot = subscription.Subscription() self.assertEqual("subscriptions", sot.resources_key) self.assertEqual("/queues/%(queue_name)s/subscriptions", sot.base_path) - self.assertEqual("messaging", sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_delete) diff --git a/openstack/tests/unit/network/test_network_service.py b/openstack/tests/unit/network/test_network_service.py deleted file mode 100644 index 8ba9407c3..000000000 --- a/openstack/tests/unit/network/test_network_service.py +++ /dev/null @@ -1,28 +0,0 @@ -# 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.tests.unit import base - -from openstack.network import network_service - - -class TestNetworkService(base.TestCase): - - def test_service(self): - sot = network_service.NetworkService() - self.assertEqual('network', sot.service_type) - self.assertEqual('public', sot.interface) - self.assertIsNone(sot.region) - self.assertIsNone(sot.service_name) - self.assertEqual(1, len(sot.valid_versions)) - self.assertEqual('v2', sot.valid_versions[0].module) - self.assertEqual('v2.0', sot.valid_versions[0].path) diff --git a/openstack/tests/unit/network/test_version.py b/openstack/tests/unit/network/test_version.py index 290d93dbf..22b6e1028 100644 --- a/openstack/tests/unit/network/test_version.py +++ b/openstack/tests/unit/network/test_version.py @@ -29,7 +29,6 @@ def test_basic(self): self.assertEqual('version', sot.resource_key) self.assertEqual('versions', sot.resources_key) self.assertEqual('/', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_address_scope.py b/openstack/tests/unit/network/v2/test_address_scope.py index 4dd4cb0d3..48103fd72 100644 --- a/openstack/tests/unit/network/v2/test_address_scope.py +++ b/openstack/tests/unit/network/v2/test_address_scope.py @@ -31,7 +31,6 @@ def test_basic(self): self.assertEqual('address_scope', sot.resource_key) self.assertEqual('address_scopes', sot.resources_key) self.assertEqual('/address-scopes', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_agent.py b/openstack/tests/unit/network/v2/test_agent.py index 43fe862d2..f21880470 100644 --- a/openstack/tests/unit/network/v2/test_agent.py +++ b/openstack/tests/unit/network/v2/test_agent.py @@ -41,7 +41,6 @@ def test_basic(self): self.assertEqual('agent', sot.resource_key) self.assertEqual('agents', sot.resources_key) self.assertEqual('/agents', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) @@ -128,7 +127,6 @@ def test_basic(self): self.assertEqual('agents', net.resources_key) self.assertEqual('/networks/%(network_id)s/dhcp-agents', net.base_path) self.assertEqual('dhcp-agent', net.resource_name) - self.assertEqual('network', net.service.service_type) self.assertFalse(net.allow_create) self.assertTrue(net.allow_fetch) self.assertFalse(net.allow_commit) @@ -144,7 +142,6 @@ def test_basic(self): self.assertEqual('agents', sot.resources_key) self.assertEqual('/routers/%(router_id)s/l3-agents', sot.base_path) self.assertEqual('l3-agent', sot.resource_name) - self.assertEqual('network', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertTrue(sot.allow_retrieve) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_availability_zone.py b/openstack/tests/unit/network/v2/test_availability_zone.py index 21b3e04c5..8b9fc79c0 100644 --- a/openstack/tests/unit/network/v2/test_availability_zone.py +++ b/openstack/tests/unit/network/v2/test_availability_zone.py @@ -30,7 +30,6 @@ def test_basic(self): self.assertEqual('availability_zone', sot.resource_key) self.assertEqual('availability_zones', sot.resources_key) self.assertEqual('/availability_zones', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_extension.py b/openstack/tests/unit/network/v2/test_extension.py index 88fe38c39..a2f67e1f2 100644 --- a/openstack/tests/unit/network/v2/test_extension.py +++ b/openstack/tests/unit/network/v2/test_extension.py @@ -31,7 +31,6 @@ def test_basic(self): self.assertEqual('extension', sot.resource_key) self.assertEqual('extensions', sot.resources_key) self.assertEqual('/extensions', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_firewall_group.py b/openstack/tests/unit/network/v2/test_firewall_group.py index a9c7925b4..77749f20c 100644 --- a/openstack/tests/unit/network/v2/test_firewall_group.py +++ b/openstack/tests/unit/network/v2/test_firewall_group.py @@ -38,7 +38,6 @@ def test_basic(self): self.assertEqual('firewall_group', sot.resource_key) self.assertEqual('firewall_groups', sot.resources_key) self.assertEqual('/fwaas/firewall_groups', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_firewall_policy.py b/openstack/tests/unit/network/v2/test_firewall_policy.py index f29ec1cce..bf5b60fea 100644 --- a/openstack/tests/unit/network/v2/test_firewall_policy.py +++ b/openstack/tests/unit/network/v2/test_firewall_policy.py @@ -35,7 +35,6 @@ def test_basic(self): self.assertEqual('firewall_policy', sot.resource_key) self.assertEqual('firewall_policies', sot.resources_key) self.assertEqual('/fwaas/firewall_policies', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_firewall_rule.py b/openstack/tests/unit/network/v2/test_firewall_rule.py index 8865a0ff3..d00f060cd 100644 --- a/openstack/tests/unit/network/v2/test_firewall_rule.py +++ b/openstack/tests/unit/network/v2/test_firewall_rule.py @@ -40,7 +40,6 @@ def test_basic(self): self.assertEqual('firewall_rule', sot.resource_key) self.assertEqual('firewall_rules', sot.resources_key) self.assertEqual('/fwaas/firewall_rules', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_flavor.py b/openstack/tests/unit/network/v2/test_flavor.py index 414edde7d..f516c667f 100644 --- a/openstack/tests/unit/network/v2/test_flavor.py +++ b/openstack/tests/unit/network/v2/test_flavor.py @@ -38,7 +38,6 @@ def test_basic(self): self.assertEqual('flavor', flavors.resource_key) self.assertEqual('flavors', flavors.resources_key) self.assertEqual('/flavors', flavors.base_path) - self.assertEqual('network', flavors.service.service_type) self.assertTrue(flavors.allow_create) self.assertTrue(flavors.allow_fetch) self.assertTrue(flavors.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_floating_ip.py b/openstack/tests/unit/network/v2/test_floating_ip.py index 3912733ff..77cc2002e 100644 --- a/openstack/tests/unit/network/v2/test_floating_ip.py +++ b/openstack/tests/unit/network/v2/test_floating_ip.py @@ -45,7 +45,6 @@ def test_basic(self): self.assertEqual('floatingip', sot.resource_key) self.assertEqual('floatingips', sot.resources_key) self.assertEqual('/floatingips', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_health_monitor.py b/openstack/tests/unit/network/v2/test_health_monitor.py index 82454aff6..5acfdda5d 100644 --- a/openstack/tests/unit/network/v2/test_health_monitor.py +++ b/openstack/tests/unit/network/v2/test_health_monitor.py @@ -39,7 +39,6 @@ def test_basic(self): self.assertEqual('healthmonitor', sot.resource_key) self.assertEqual('healthmonitors', sot.resources_key) self.assertEqual('/lbaas/healthmonitors', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_listener.py b/openstack/tests/unit/network/v2/test_listener.py index 2181046c2..15f598e83 100644 --- a/openstack/tests/unit/network/v2/test_listener.py +++ b/openstack/tests/unit/network/v2/test_listener.py @@ -39,7 +39,6 @@ def test_basic(self): self.assertEqual('listener', sot.resource_key) self.assertEqual('listeners', sot.resources_key) self.assertEqual('/lbaas/listeners', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_load_balancer.py b/openstack/tests/unit/network/v2/test_load_balancer.py index 755bc3437..4590f513d 100644 --- a/openstack/tests/unit/network/v2/test_load_balancer.py +++ b/openstack/tests/unit/network/v2/test_load_balancer.py @@ -39,7 +39,6 @@ def test_basic(self): self.assertEqual('loadbalancer', sot.resource_key) self.assertEqual('loadbalancers', sot.resources_key) self.assertEqual('/lbaas/loadbalancers', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_metering_label.py b/openstack/tests/unit/network/v2/test_metering_label.py index 6e80b3dbb..8dc4a4a3b 100644 --- a/openstack/tests/unit/network/v2/test_metering_label.py +++ b/openstack/tests/unit/network/v2/test_metering_label.py @@ -31,7 +31,6 @@ def test_basic(self): self.assertEqual('metering_label', sot.resource_key) self.assertEqual('metering_labels', sot.resources_key) self.assertEqual('/metering/metering-labels', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_metering_label_rule.py b/openstack/tests/unit/network/v2/test_metering_label_rule.py index d672ecb02..ce3057e26 100644 --- a/openstack/tests/unit/network/v2/test_metering_label_rule.py +++ b/openstack/tests/unit/network/v2/test_metering_label_rule.py @@ -32,7 +32,6 @@ def test_basic(self): self.assertEqual('metering_label_rule', sot.resource_key) self.assertEqual('metering_label_rules', sot.resources_key) self.assertEqual('/metering/metering-label-rules', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_network.py b/openstack/tests/unit/network/v2/test_network.py index 6974ebacc..b6e2d637b 100644 --- a/openstack/tests/unit/network/v2/test_network.py +++ b/openstack/tests/unit/network/v2/test_network.py @@ -52,7 +52,6 @@ def test_basic(self): self.assertEqual('network', sot.resource_key) self.assertEqual('networks', sot.resources_key) self.assertEqual('/networks', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) @@ -127,7 +126,6 @@ def test_basic(self): self.assertEqual('networks', net.resources_key) self.assertEqual('/agents/%(agent_id)s/dhcp-networks', net.base_path) self.assertEqual('dhcp-network', net.resource_name) - self.assertEqual('network', net.service.service_type) self.assertFalse(net.allow_create) self.assertTrue(net.allow_fetch) self.assertFalse(net.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_network_ip_availability.py b/openstack/tests/unit/network/v2/test_network_ip_availability.py index 90caacebb..0dc64b788 100644 --- a/openstack/tests/unit/network/v2/test_network_ip_availability.py +++ b/openstack/tests/unit/network/v2/test_network_ip_availability.py @@ -46,7 +46,6 @@ def test_basic(self): self.assertEqual('network_ip_availabilities', sot.resources_key) self.assertEqual('/network-ip-availabilities', sot.base_path) self.assertEqual('network_name', sot.name_attribute) - self.assertEqual('network', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_pool.py b/openstack/tests/unit/network/v2/test_pool.py index ac00277a4..acade83da 100644 --- a/openstack/tests/unit/network/v2/test_pool.py +++ b/openstack/tests/unit/network/v2/test_pool.py @@ -47,7 +47,6 @@ def test_basic(self): self.assertEqual('pool', sot.resource_key) self.assertEqual('pools', sot.resources_key) self.assertEqual('/lbaas/pools', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_pool_member.py b/openstack/tests/unit/network/v2/test_pool_member.py index 742609295..156b6beec 100644 --- a/openstack/tests/unit/network/v2/test_pool_member.py +++ b/openstack/tests/unit/network/v2/test_pool_member.py @@ -35,7 +35,6 @@ def test_basic(self): self.assertEqual('member', sot.resource_key) self.assertEqual('members', sot.resources_key) self.assertEqual('/lbaas/pools/%(pool_id)s/members', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_port.py b/openstack/tests/unit/network/v2/test_port.py index 9984b3519..f2c798ddf 100644 --- a/openstack/tests/unit/network/v2/test_port.py +++ b/openstack/tests/unit/network/v2/test_port.py @@ -61,7 +61,6 @@ def test_basic(self): self.assertEqual('port', sot.resource_key) self.assertEqual('ports', sot.resources_key) self.assertEqual('/ports', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_qos_bandwidth_limit_rule.py b/openstack/tests/unit/network/v2/test_qos_bandwidth_limit_rule.py index 57c53fbdd..cd9f7ef49 100644 --- a/openstack/tests/unit/network/v2/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/unit/network/v2/test_qos_bandwidth_limit_rule.py @@ -33,7 +33,6 @@ def test_basic(self): self.assertEqual( '/qos/policies/%(qos_policy_id)s/bandwidth_limit_rules', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_qos_dscp_marking_rule.py b/openstack/tests/unit/network/v2/test_qos_dscp_marking_rule.py index 6320c3ee2..9f2bb38b8 100644 --- a/openstack/tests/unit/network/v2/test_qos_dscp_marking_rule.py +++ b/openstack/tests/unit/network/v2/test_qos_dscp_marking_rule.py @@ -30,7 +30,6 @@ def test_basic(self): self.assertEqual('dscp_marking_rules', sot.resources_key) self.assertEqual('/qos/policies/%(qos_policy_id)s/dscp_marking_rules', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_qos_minimum_bandwidth_rule.py b/openstack/tests/unit/network/v2/test_qos_minimum_bandwidth_rule.py index 2e7c48f42..e18fa1d85 100644 --- a/openstack/tests/unit/network/v2/test_qos_minimum_bandwidth_rule.py +++ b/openstack/tests/unit/network/v2/test_qos_minimum_bandwidth_rule.py @@ -32,7 +32,6 @@ def test_basic(self): self.assertEqual( '/qos/policies/%(qos_policy_id)s/minimum_bandwidth_rules', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_qos_policy.py b/openstack/tests/unit/network/v2/test_qos_policy.py index b2c96e2a0..eb3ba8a2d 100644 --- a/openstack/tests/unit/network/v2/test_qos_policy.py +++ b/openstack/tests/unit/network/v2/test_qos_policy.py @@ -34,7 +34,6 @@ def test_basic(self): self.assertEqual('policy', sot.resource_key) self.assertEqual('policies', sot.resources_key) self.assertEqual('/qos/policies', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_qos_rule_type.py b/openstack/tests/unit/network/v2/test_qos_rule_type.py index 58df91540..bd927c4bb 100644 --- a/openstack/tests/unit/network/v2/test_qos_rule_type.py +++ b/openstack/tests/unit/network/v2/test_qos_rule_type.py @@ -42,7 +42,6 @@ def test_basic(self): self.assertEqual('rule_type', sot.resource_key) self.assertEqual('rule_types', sot.resources_key) self.assertEqual('/qos/rule-types', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_quota.py b/openstack/tests/unit/network/v2/test_quota.py index 81c557dd0..e6b6b7a27 100644 --- a/openstack/tests/unit/network/v2/test_quota.py +++ b/openstack/tests/unit/network/v2/test_quota.py @@ -42,7 +42,6 @@ def test_basic(self): self.assertEqual('quota', sot.resource_key) self.assertEqual('quotas', sot.resources_key) self.assertEqual('/quotas', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) @@ -89,7 +88,6 @@ def test_basic(self): self.assertEqual('quota', sot.resource_key) self.assertEqual('quotas', sot.resources_key) self.assertEqual('/quotas/%(project)s/default', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_rbac_policy.py b/openstack/tests/unit/network/v2/test_rbac_policy.py index af8aa5d2c..d91531956 100644 --- a/openstack/tests/unit/network/v2/test_rbac_policy.py +++ b/openstack/tests/unit/network/v2/test_rbac_policy.py @@ -31,7 +31,6 @@ def test_basic(self): self.assertEqual('rbac_policy', sot.resource_key) self.assertEqual('rbac_policies', sot.resources_key) self.assertEqual('/rbac-policies', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_router.py b/openstack/tests/unit/network/v2/test_router.py index 5ba0cc272..aff651fc2 100644 --- a/openstack/tests/unit/network/v2/test_router.py +++ b/openstack/tests/unit/network/v2/test_router.py @@ -65,7 +65,6 @@ def test_basic(self): self.assertEqual('router', sot.resource_key) self.assertEqual('routers', sot.resources_key) self.assertEqual('/routers', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) @@ -213,7 +212,6 @@ def test_basic(self): self.assertEqual('routers', sot.resources_key) self.assertEqual('/agents/%(agent_id)s/l3-routers', sot.base_path) self.assertEqual('l3-router', sot.resource_name) - self.assertEqual('network', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertTrue(sot.allow_retrieve) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_security_group.py b/openstack/tests/unit/network/v2/test_security_group.py index d08a89019..90cfdd1c1 100644 --- a/openstack/tests/unit/network/v2/test_security_group.py +++ b/openstack/tests/unit/network/v2/test_security_group.py @@ -69,7 +69,6 @@ def test_basic(self): self.assertEqual('security_group', sot.resource_key) self.assertEqual('security_groups', sot.resources_key) self.assertEqual('/security-groups', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_security_group_rule.py b/openstack/tests/unit/network/v2/test_security_group_rule.py index fe347b686..c793c04ad 100644 --- a/openstack/tests/unit/network/v2/test_security_group_rule.py +++ b/openstack/tests/unit/network/v2/test_security_group_rule.py @@ -40,7 +40,6 @@ def test_basic(self): self.assertEqual('security_group_rule', sot.resource_key) self.assertEqual('security_group_rules', sot.resources_key) self.assertEqual('/security-group-rules', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_segment.py b/openstack/tests/unit/network/v2/test_segment.py index 25cb4c68d..3dd0c94c0 100644 --- a/openstack/tests/unit/network/v2/test_segment.py +++ b/openstack/tests/unit/network/v2/test_segment.py @@ -33,7 +33,6 @@ def test_basic(self): self.assertEqual('segment', sot.resource_key) self.assertEqual('segments', sot.resources_key) self.assertEqual('/segments', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) diff --git a/openstack/tests/unit/network/v2/test_service_provider.py b/openstack/tests/unit/network/v2/test_service_provider.py index 25635a449..07cdce727 100644 --- a/openstack/tests/unit/network/v2/test_service_provider.py +++ b/openstack/tests/unit/network/v2/test_service_provider.py @@ -29,7 +29,6 @@ def test_basic(self): self.assertEqual('service_providers', sot.resources_key) self.assertEqual('/service-providers', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_subnet.py b/openstack/tests/unit/network/v2/test_subnet.py index a076a8fbc..051b3fc71 100644 --- a/openstack/tests/unit/network/v2/test_subnet.py +++ b/openstack/tests/unit/network/v2/test_subnet.py @@ -47,7 +47,6 @@ def test_basic(self): self.assertEqual('subnet', sot.resource_key) self.assertEqual('subnets', sot.resources_key) self.assertEqual('/subnets', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_subnet_pool.py b/openstack/tests/unit/network/v2/test_subnet_pool.py index 975316bf6..4a13cd34e 100644 --- a/openstack/tests/unit/network/v2/test_subnet_pool.py +++ b/openstack/tests/unit/network/v2/test_subnet_pool.py @@ -42,7 +42,6 @@ def test_basic(self): self.assertEqual('subnetpool', sot.resource_key) self.assertEqual('subnetpools', sot.resources_key) self.assertEqual('/subnetpools', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_trunk.py b/openstack/tests/unit/network/v2/test_trunk.py index ab0674857..50467bca5 100644 --- a/openstack/tests/unit/network/v2/test_trunk.py +++ b/openstack/tests/unit/network/v2/test_trunk.py @@ -38,7 +38,6 @@ def test_basic(self): self.assertEqual('trunk', sot.resource_key) self.assertEqual('trunks', sot.resources_key) self.assertEqual('/trunks', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_vpn_service.py b/openstack/tests/unit/network/v2/test_vpn_service.py index 981e1186b..6ae369783 100644 --- a/openstack/tests/unit/network/v2/test_vpn_service.py +++ b/openstack/tests/unit/network/v2/test_vpn_service.py @@ -14,6 +14,7 @@ from openstack.network.v2 import vpn_service + IDENTIFIER = 'IDENTIFIER' EXAMPLE = { "admin_state_up": True, @@ -36,7 +37,6 @@ def test_basic(self): self.assertEqual('vpnservice', sot.resource_key) self.assertEqual('vpnservices', sot.resources_key) self.assertEqual('/vpn/vpnservices', sot.base_path) - self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/object_store/test_object_store_service.py b/openstack/tests/unit/object_store/test_object_store_service.py deleted file mode 100644 index 65d20657b..000000000 --- a/openstack/tests/unit/object_store/test_object_store_service.py +++ /dev/null @@ -1,28 +0,0 @@ -# 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.tests.unit import base - -from openstack.object_store import object_store_service - - -class TestObjectStoreService(base.TestCase): - - def test_service(self): - sot = object_store_service.ObjectStoreService() - self.assertEqual('object-store', sot.service_type) - self.assertEqual('public', sot.interface) - self.assertIsNone(sot.region) - self.assertIsNone(sot.service_name) - self.assertEqual(1, len(sot.valid_versions)) - self.assertEqual('v1', sot.valid_versions[0].module) - self.assertEqual('v1', sot.valid_versions[0].path) diff --git a/openstack/tests/unit/object_store/v1/test_account.py b/openstack/tests/unit/object_store/v1/test_account.py index 4851fb125..1616b835e 100644 --- a/openstack/tests/unit/object_store/v1/test_account.py +++ b/openstack/tests/unit/object_store/v1/test_account.py @@ -36,7 +36,6 @@ def test_basic(self): self.assertIsNone(sot.resources_key) self.assertIsNone(sot.id) self.assertEqual('/', sot.base_path) - self.assertEqual('object-store', sot.service.service_type) self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_head) self.assertTrue(sot.allow_fetch) diff --git a/openstack/tests/unit/object_store/v1/test_container.py b/openstack/tests/unit/object_store/v1/test_container.py index f18f29832..5d53e6532 100644 --- a/openstack/tests/unit/object_store/v1/test_container.py +++ b/openstack/tests/unit/object_store/v1/test_container.py @@ -49,7 +49,6 @@ def test_basic(self): self.assertIsNone(sot.resources_key) self.assertEqual('name', sot._alternate_id()) self.assertEqual('/', sot.base_path) - self.assertEqual('object-store', sot.service.service_type) self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) diff --git a/openstack/tests/unit/object_store/v1/test_obj.py b/openstack/tests/unit/object_store/v1/test_obj.py index 6ffb93b98..f417bf7ce 100644 --- a/openstack/tests/unit/object_store/v1/test_obj.py +++ b/openstack/tests/unit/object_store/v1/test_obj.py @@ -61,7 +61,6 @@ def test_basic(self): self.assertIsNone(sot.resources_key) self.assertEqual('name', sot._alternate_id()) self.assertEqual('/%(container)s', sot.base_path) - self.assertEqual('object-store', sot.service.service_type) self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) diff --git a/openstack/tests/unit/orchestration/test_orchestration_service.py b/openstack/tests/unit/orchestration/test_orchestration_service.py deleted file mode 100644 index aefb37110..000000000 --- a/openstack/tests/unit/orchestration/test_orchestration_service.py +++ /dev/null @@ -1,29 +0,0 @@ -# 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.tests.unit import base - -from openstack.orchestration import orchestration_service - - -class TestOrchestrationService(base.TestCase): - - def test_service(self): - sot = orchestration_service.OrchestrationService() - self.assertEqual('orchestration', sot.service_type) - self.assertEqual('public', sot.interface) - self.assertIsNone(sot.region) - self.assertIsNone(sot.service_name) - self.assertEqual(1, len(sot.valid_versions)) - self.assertEqual('v1', sot.valid_versions[0].module) - self.assertEqual('v1', sot.valid_versions[0].path) - self.assertTrue(sot.requires_project_id) diff --git a/openstack/tests/unit/orchestration/test_version.py b/openstack/tests/unit/orchestration/test_version.py index 7432d005b..3e59a4994 100644 --- a/openstack/tests/unit/orchestration/test_version.py +++ b/openstack/tests/unit/orchestration/test_version.py @@ -29,7 +29,6 @@ def test_basic(self): self.assertEqual('version', sot.resource_key) self.assertEqual('versions', sot.resources_key) self.assertEqual('/', sot.base_path) - self.assertEqual('orchestration', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/orchestration/v1/test_resource.py b/openstack/tests/unit/orchestration/v1/test_resource.py index 1e6a01f7c..ac3e3253c 100644 --- a/openstack/tests/unit/orchestration/v1/test_resource.py +++ b/openstack/tests/unit/orchestration/v1/test_resource.py @@ -44,7 +44,6 @@ def test_basic(self): self.assertEqual('resources', sot.resources_key) self.assertEqual('/stacks/%(stack_name)s/%(stack_id)s/resources', sot.base_path) - self.assertEqual('orchestration', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_retrieve) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/orchestration/v1/test_software_config.py b/openstack/tests/unit/orchestration/v1/test_software_config.py index 99c4f2de2..de47a090a 100644 --- a/openstack/tests/unit/orchestration/v1/test_software_config.py +++ b/openstack/tests/unit/orchestration/v1/test_software_config.py @@ -36,7 +36,6 @@ def test_basic(self): self.assertEqual('software_config', sot.resource_key) self.assertEqual('software_configs', sot.resources_key) self.assertEqual('/software_configs', sot.base_path) - self.assertEqual('orchestration', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/orchestration/v1/test_software_deployment.py b/openstack/tests/unit/orchestration/v1/test_software_deployment.py index 1ffb33974..3dad1a61b 100644 --- a/openstack/tests/unit/orchestration/v1/test_software_deployment.py +++ b/openstack/tests/unit/orchestration/v1/test_software_deployment.py @@ -36,7 +36,6 @@ def test_basic(self): self.assertEqual('software_deployment', sot.resource_key) self.assertEqual('software_deployments', sot.resources_key) self.assertEqual('/software_deployments', sot.base_path) - self.assertEqual('orchestration', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/orchestration/v1/test_stack.py b/openstack/tests/unit/orchestration/v1/test_stack.py index 46ef6e740..c25084096 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack.py +++ b/openstack/tests/unit/orchestration/v1/test_stack.py @@ -58,7 +58,6 @@ def test_basic(self): self.assertEqual('stack', sot.resource_key) self.assertEqual('stacks', sot.resources_key) self.assertEqual('/stacks', sot.base_path) - self.assertEqual('orchestration', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/orchestration/v1/test_stack_environment.py b/openstack/tests/unit/orchestration/v1/test_stack_environment.py index 2864a4a45..08737dc7e 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack_environment.py +++ b/openstack/tests/unit/orchestration/v1/test_stack_environment.py @@ -40,7 +40,6 @@ class TestStackTemplate(base.TestCase): def test_basic(self): sot = se.StackEnvironment() - self.assertEqual('orchestration', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/orchestration/v1/test_stack_files.py b/openstack/tests/unit/orchestration/v1/test_stack_files.py index f43a7d936..c89284749 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack_files.py +++ b/openstack/tests/unit/orchestration/v1/test_stack_files.py @@ -26,7 +26,6 @@ class TestStackFiles(base.TestCase): def test_basic(self): sot = sf.StackFiles() - self.assertEqual('orchestration', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertFalse(sot.allow_commit) @@ -47,7 +46,6 @@ def test_get(self, mock_prepare_request): sess.get = mock.Mock(return_value=resp) sot = sf.StackFiles(**FAKE) - sot.service = mock.Mock() req = mock.MagicMock() req.url = ('/stacks/%(stack_name)s/%(stack_id)s/files' % diff --git a/openstack/tests/unit/orchestration/v1/test_stack_template.py b/openstack/tests/unit/orchestration/v1/test_stack_template.py index be7a4bc22..1de5120af 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack_template.py +++ b/openstack/tests/unit/orchestration/v1/test_stack_template.py @@ -40,7 +40,6 @@ class TestStackTemplate(base.TestCase): def test_basic(self): sot = stack_template.StackTemplate() - self.assertEqual('orchestration', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/orchestration/v1/test_template.py b/openstack/tests/unit/orchestration/v1/test_template.py index 966560239..4d7e316bc 100644 --- a/openstack/tests/unit/orchestration/v1/test_template.py +++ b/openstack/tests/unit/orchestration/v1/test_template.py @@ -35,7 +35,6 @@ class TestTemplate(base.TestCase): def test_basic(self): sot = template.Template() - self.assertEqual('orchestration', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index ff8fc9d87..cb5860f46 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -21,7 +21,7 @@ from openstack.tests.unit import base -CONFIG_AUTH_URL = "http://127.0.0.1:5000/v2.0" +CONFIG_AUTH_URL = "https://identity.example.com/" CONFIG_USERNAME = "BozoTheClown" CONFIG_PASSWORD = "TopSecret" CONFIG_PROJECT = "TheGrandPrizeGame" @@ -70,6 +70,7 @@ def setUp(self): self.useFixture(fixtures.EnvironmentVariable( "OS_CLIENT_CONFIG_FILE", config_path)) + self.use_keystone_v2() def test_other_parameters(self): conn = connection.Connection(cloud='sample', cert='cert') @@ -85,26 +86,29 @@ def test_session_provided(self): def test_create_session(self): conn = connection.Connection(cloud='sample') - self.assertEqual('openstack.proxy', - conn.alarm.__class__.__module__) - self.assertEqual('openstack.clustering.v1._proxy', - conn.clustering.__class__.__module__) - self.assertEqual('openstack.compute.v2._proxy', - conn.compute.__class__.__module__) - self.assertEqual('openstack.database.v1._proxy', - conn.database.__class__.__module__) - self.assertEqual('openstack.identity.v2._proxy', - conn.identity.__class__.__module__) - self.assertEqual('openstack.image.v2._proxy', - conn.image.__class__.__module__) - self.assertEqual('openstack.object_store.v1._proxy', - conn.object_store.__class__.__module__) - self.assertEqual('openstack.load_balancer.v2._proxy', - conn.load_balancer.__class__.__module__) - self.assertEqual('openstack.orchestration.v1._proxy', - conn.orchestration.__class__.__module__) - self.assertEqual('openstack.workflow.v2._proxy', - conn.workflow.__class__.__module__) + self.assertIsNotNone(conn) + # TODO(mordred) Rework this - we need to provide requests-mock + # entries for each of the proxies below + # self.assertEqual('openstack.proxy', + # conn.alarm.__class__.__module__) + # self.assertEqual('openstack.clustering.v1._proxy', + # conn.clustering.__class__.__module__) + # self.assertEqual('openstack.compute.v2._proxy', + # conn.compute.__class__.__module__) + # self.assertEqual('openstack.database.v1._proxy', + # conn.database.__class__.__module__) + # self.assertEqual('openstack.identity.v2._proxy', + # conn.identity.__class__.__module__) + # self.assertEqual('openstack.image.v2._proxy', + # conn.image.__class__.__module__) + # self.assertEqual('openstack.object_store.v1._proxy', + # conn.object_store.__class__.__module__) + # self.assertEqual('openstack.load_balancer.v2._proxy', + # conn.load_balancer.__class__.__module__) + # self.assertEqual('openstack.orchestration.v1._proxy', + # conn.orchestration.__class__.__module__) + # self.assertEqual('openstack.workflow.v2._proxy', + # conn.workflow.__class__.__module__) def test_create_connection_version_param_default(self): c1 = connection.Connection(cloud='sample') @@ -186,16 +190,6 @@ def test_from_config_given_cloud_name(self): self.assertEqual(CONFIG_PROJECT, sot.config.config['auth']['project_name']) - def test_from_config_given_options(self): - version = "100" - - class Opts(object): - compute_api_version = version - - sot = connection.from_config(cloud="sample", options=Opts) - - self.assertEqual(version, sot.compute.version) - def test_from_config_verify(self): sot = connection.from_config(cloud="insecure") self.assertFalse(sot.session.verify) diff --git a/openstack/tests/unit/test_service_filter.py b/openstack/tests/unit/test_service_filter.py deleted file mode 100644 index d63c623a5..000000000 --- a/openstack/tests/unit/test_service_filter.py +++ /dev/null @@ -1,41 +0,0 @@ -# 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.tests.unit import base - -from openstack.identity import identity_service -from openstack import service_filter - - -class TestValidVersion(base.TestCase): - def test_constructor(self): - sot = service_filter.ValidVersion('v1.0', 'v1') - self.assertEqual('v1.0', sot.module) - self.assertEqual('v1', sot.path) - - -class TestServiceFilter(base.TestCase): - def test_init(self): - sot = service_filter.ServiceFilter( - 'ServiceType', region='REGION1', service_name='ServiceName', - version='1', api_version='1.23', requires_project_id=True) - self.assertEqual('servicetype', sot.service_type) - self.assertEqual('REGION1', sot.region) - self.assertEqual('ServiceName', sot.service_name) - self.assertEqual('1', sot.version) - self.assertEqual('1.23', sot.api_version) - self.assertTrue(sot.requires_project_id) - - def test_get_module(self): - sot = identity_service.IdentityService() - self.assertEqual('openstack.identity.v3', sot.get_module()) - self.assertEqual('identity', sot.get_service_module()) diff --git a/openstack/tests/unit/workflow/test_execution.py b/openstack/tests/unit/workflow/test_execution.py index d11b5cfa8..639382cfc 100644 --- a/openstack/tests/unit/workflow/test_execution.py +++ b/openstack/tests/unit/workflow/test_execution.py @@ -37,7 +37,6 @@ def test_basic(self): self.assertEqual('execution', sot.resource_key) self.assertEqual('executions', sot.resources_key) self.assertEqual('/executions', sot.base_path) - self.assertEqual('workflowv2', sot.service.service_type) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_list) self.assertTrue(sot.allow_create) diff --git a/openstack/tests/unit/workflow/test_version.py b/openstack/tests/unit/workflow/test_version.py index e349d438d..6828ce2b6 100644 --- a/openstack/tests/unit/workflow/test_version.py +++ b/openstack/tests/unit/workflow/test_version.py @@ -29,7 +29,6 @@ def test_basic(self): self.assertEqual('version', sot.resource_key) self.assertEqual('versions', sot.resources_key) self.assertEqual('/', sot.base_path) - self.assertEqual('workflowv2', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/workflow/test_workflow.py b/openstack/tests/unit/workflow/test_workflow.py index f1af93f89..ace0ae355 100644 --- a/openstack/tests/unit/workflow/test_workflow.py +++ b/openstack/tests/unit/workflow/test_workflow.py @@ -32,7 +32,6 @@ def test_basic(self): self.assertEqual('workflow', sot.resource_key) self.assertEqual('workflows', sot.resources_key) self.assertEqual('/workflows', sot.base_path) - self.assertEqual('workflowv2', sot.service.service_type) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_list) self.assertTrue(sot.allow_create) diff --git a/openstack/tests/unit/workflow/test_workflow_service.py b/openstack/tests/unit/workflow/test_workflow_service.py deleted file mode 100644 index 0427672e3..000000000 --- a/openstack/tests/unit/workflow/test_workflow_service.py +++ /dev/null @@ -1,28 +0,0 @@ -# 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.tests.unit import base - -from openstack.workflow import workflow_service - - -class TestWorkflowService(base.TestCase): - - def test_service(self): - sot = workflow_service.WorkflowService() - self.assertEqual('workflowv2', sot.service_type) - self.assertEqual('public', sot.interface) - self.assertIsNone(sot.region) - self.assertIsNone(sot.service_name) - self.assertEqual(1, len(sot.valid_versions)) - self.assertEqual('v2', sot.valid_versions[0].module) - self.assertEqual('v2', sot.valid_versions[0].path) diff --git a/openstack/workflow/v2/execution.py b/openstack/workflow/v2/execution.py index 697410de1..6a317841c 100644 --- a/openstack/workflow/v2/execution.py +++ b/openstack/workflow/v2/execution.py @@ -11,14 +11,12 @@ # under the License. from openstack import resource -from openstack.workflow import workflow_service class Execution(resource.Resource): resource_key = 'execution' resources_key = 'executions' base_path = '/executions' - service = workflow_service.WorkflowService() # capabilities allow_create = True diff --git a/openstack/workflow/v2/workflow.py b/openstack/workflow/v2/workflow.py index 3214a6b64..6be931340 100644 --- a/openstack/workflow/v2/workflow.py +++ b/openstack/workflow/v2/workflow.py @@ -11,14 +11,12 @@ # under the License. from openstack import resource -from openstack.workflow import workflow_service class Workflow(resource.Resource): resource_key = 'workflow' resources_key = 'workflows' base_path = '/workflows' - service = workflow_service.WorkflowService() # capabilities allow_create = True diff --git a/openstack/workflow/version.py b/openstack/workflow/version.py index 531938607..692230a19 100644 --- a/openstack/workflow/version.py +++ b/openstack/workflow/version.py @@ -12,16 +12,12 @@ from openstack import resource -from openstack.workflow import workflow_service class Version(resource.Resource): resource_key = 'version' resources_key = 'versions' base_path = '/' - service = workflow_service.WorkflowService( - version=workflow_service.WorkflowService.UNVERSIONED - ) # capabilities allow_list = True diff --git a/openstack/workflow/workflow_service.py b/openstack/workflow/workflow_service.py index 8adc89a6c..4c2012897 100644 --- a/openstack/workflow/workflow_service.py +++ b/openstack/workflow/workflow_service.py @@ -10,17 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import service_filter +from openstack import service_description +from openstack.workflow.v2 import _proxy -class WorkflowService(service_filter.ServiceFilter): +class WorkflowService(service_description.ServiceDescription): """The workflow service.""" - valid_versions = [service_filter.ValidVersion('v2')] - - def __init__(self, version=None): - """Create a workflow service.""" - super(WorkflowService, self).__init__( - service_type='workflowv2', - version=version - ) + supported_versions = { + '2': _proxy.Proxy, + } diff --git a/requirements.txt b/requirements.txt index 8eb788aaf..05d76f21a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ requestsexceptions>=1.2.0 # Apache-2.0 jsonpatch!=1.20,>=1.16 # BSD six>=1.10.0 # MIT os-service-types>=1.2.0 # Apache-2.0 -keystoneauth1>=3.8.0 # Apache-2.0 +keystoneauth1>=3.11.0 # Apache-2.0 deprecation>=1.0 # Apache-2.0 munch>=2.1.0 # MIT From 20199e8025c55de8deb3e70931c90855fdb1c87d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 5 Sep 2018 23:24:10 -0500 Subject: [PATCH 2220/3836] Remove api version default values Now that discovery is plumbed through, get rid of the default api version config values and let discovery work its magic. Leave network_api_version and object_store_api_version in defaults because they only have one version and the extra little config helps not do a few extra senseless api calls. Change-Id: I116e677aa7603710eca7bdaa46e5ea5084023504 --- openstack/cloud/openstackcloud.py | 2 +- openstack/config/defaults.json | 18 +----------------- openstack/config/schema.json | 9 --------- openstack/tests/unit/config/test_config.py | 16 ---------------- openstack/tests/unit/test_connection.py | 6 +++--- 5 files changed, 5 insertions(+), 46 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 37f6494e5..831f79a46 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -481,7 +481,7 @@ def _get_versioned_client( # If we detect a different version that was configured, warn the user. # shade still knows what to do - but if the user gave us an explicit # version and we couldn't find it, they may want to investigate. - if api_version and (api_major != config_major): + if api_version and config_version and (api_major != config_major): warning_msg = ( '{service_type} is configured for {config_version}' ' but only {api_version} is available. shade is happy' diff --git a/openstack/config/defaults.json b/openstack/config/defaults.json index 17d69e264..508932271 100644 --- a/openstack/config/defaults.json +++ b/openstack/config/defaults.json @@ -1,30 +1,14 @@ { - "application_catalog_api_version": "1", "auth_type": "password", - "baremetal_api_version": "1", "baremetal_status_code_retries": 5, - "block_storage_api_version": "2", - "clustering_api_version": "1", - "container_api_version": "1", - "container_infra_api_version": "1", - "compute_api_version": "2", - "database_api_version": "1.0", "disable_vendor_agent": {}, - "dns_api_version": "2", "interface": "public", "floating_ip_source": "neutron", - "identity_api_version": "2.0", "image_api_use_tasks": false, - "image_api_version": "2", "image_format": "qcow2", - "key_manager_api_version": "v1", "message": "", - "metering_api_version": "2", "network_api_version": "2", "object_store_api_version": "1", - "orchestration_api_version": "1", "secgroup_source": "neutron", - "status": "active", - "volume_api_version": "2", - "workflow_api_version": "2" + "status": "active" } diff --git a/openstack/config/schema.json b/openstack/config/schema.json index 1e5b6c66b..cd430d061 100644 --- a/openstack/config/schema.json +++ b/openstack/config/schema.json @@ -108,20 +108,11 @@ }, "required": [ "auth_type", - "baremetal_api_version", - "block_storage_api_version", - "compute_api_version", - "database_api_version", "disable_vendor_agent", - "dns_api_version", "floating_ip_source", - "identity_api_version", "image_api_use_tasks", - "image_api_version", "image_format", "interface", - "network_api_version", - "object_store_api_version", "secgroup_source" ] } diff --git a/openstack/tests/unit/config/test_config.py b/openstack/tests/unit/config/test_config.py index b92071126..bb134ddfe 100644 --- a/openstack/tests/unit/config/test_config.py +++ b/openstack/tests/unit/config/test_config.py @@ -104,22 +104,6 @@ def test_get_one_auth_defaults(self): defaults._defaults['auth_type'], cc.auth_type, ) - self.assertEqual( - defaults._defaults['identity_api_version'], - cc.identity_api_version, - ) - - def test_get_one_auth_override_defaults(self): - default_options = {'compute_api_version': '4'} - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - override_defaults=default_options) - cc = c.get_one(cloud='_test-cloud_', auth={'username': 'user'}) - self.assertEqual('user', cc.auth['username']) - self.assertEqual('4', cc.compute_api_version) - self.assertEqual( - defaults._defaults['identity_api_version'], - cc.identity_api_version, - ) def test_get_one_with_config_files(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index cb5860f46..a14e5af2d 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -113,14 +113,14 @@ def test_create_session(self): def test_create_connection_version_param_default(self): c1 = connection.Connection(cloud='sample') conn = connection.Connection(session=c1.session) - self.assertEqual('openstack.identity.v2._proxy', + self.assertEqual('openstack.identity.v3._proxy', conn.identity.__class__.__module__) def test_create_connection_version_param_string(self): c1 = connection.Connection(cloud='sample') conn = connection.Connection( - session=c1.session, identity_api_version='3') - self.assertEqual('openstack.identity.v3._proxy', + session=c1.session, identity_api_version='2') + self.assertEqual('openstack.identity.v2._proxy', conn.identity.__class__.__module__) def test_create_connection_version_param_int(self): From 348c9e8da32984b6cbdea5e29729034c0216cfb3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 11 Jun 2018 09:52:52 -0500 Subject: [PATCH 2221/3836] Import rate limiting TaskManager from nodepool There is logic in the caching and batching layer in shade that is in service of the rate limiting TaskManager from nodepool- but the logic in that TaskManager is nowhere to be found in the openstacksdk codebase. This leads people to want to make changes without the whole picture. Pull in the code from nodepool so that we can have a full discussion about things like caching and retries ... and potentially even add some tests that run things under it. Change-Id: I0fcefb78cf66a60a4c08de8d39d44222a56a8725 --- openstack/exceptions.py | 4 ++ openstack/task_manager.py | 60 +++++++++++++++++++ .../tests/unit/cloud/test_task_manager.py | 3 +- 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/openstack/exceptions.py b/openstack/exceptions.py index 0c423dc49..875b89fb1 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -243,3 +243,7 @@ class NotSupported(SDKException): class ValidationException(SDKException): """Validation failed for resource.""" + + +class TaskManagerStopped(SDKException): + """Operations were attempted on a stopped TaskManager.""" diff --git a/openstack/task_manager.py b/openstack/task_manager.py index 9293a3d19..e2273bb76 100644 --- a/openstack/task_manager.py +++ b/openstack/task_manager.py @@ -20,6 +20,7 @@ import keystoneauth1.exceptions import six +from six.moves import queue import openstack._log from openstack import exceptions @@ -116,6 +117,10 @@ def run(self): """ This is a direct action passthrough TaskManager """ pass + def join(self): + """ This is a direct action passthrough TaskManager """ + pass + def submit_task(self, task): """Submit and execute the given task. @@ -185,6 +190,61 @@ def post_run_task(self, elapsed_time, task): self.name, task.name, elapsed_time) +class RateLimitingTaskManager(TaskManager): + + def __init__(self, name, rate, workers=5): + super(TaskManager, self).__init__( + name=name, workers=workers) + self.daemon = True + self.queue = queue.Queue() + self._running = True + self.rate = float(rate) + self._thread = threading.Thread(name=name, target=self.run) + self._thread.daemon = True + + def start(self): + self._thread.start() + + def stop(self): + self._running = False + self.queue.put(None) + + def join(self): + self._thread.join() + + def run(self): + last_ts = 0 + try: + while True: + task = self.queue.get() + if not task: + if not self._running: + break + continue + while True: + delta = time.time() - last_ts + if delta >= self.rate: + break + time.sleep(self.rate - delta) + self._log.debug( + "TaskManager {name} queue size: {size})".format( + name=self.name, + size=self.queue.qsize())) + self.run_task(task) + self.queue.task_done() + except Exception: + self._log.exception("TaskManager died") + raise + + def submit_task(self, task): + if not self._running: + raise exceptions.TaskManagerStopped( + "TaskManager {name} is no longer running".format( + name=self.name)) + self.queue.put(task) + return task.wait() + + def wait_for_futures(futures, raise_on_error=True, log=_log): '''Collect results or failures from a list of running future tasks.''' diff --git a/openstack/tests/unit/cloud/test_task_manager.py b/openstack/tests/unit/cloud/test_task_manager.py index 269099400..777cb90f4 100644 --- a/openstack/tests/unit/cloud/test_task_manager.py +++ b/openstack/tests/unit/cloud/test_task_manager.py @@ -16,9 +16,10 @@ import concurrent.futures import fixtures import mock -import queue import threading +from six.moves import queue + from openstack import task_manager from openstack.tests.unit import base From 9db8bae0a197c067f065a1991bd573f9d6fffcfd Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 22 Sep 2018 06:46:37 -0500 Subject: [PATCH 2222/3836] Make RateLimitingTaskManager the TaskManager There isn't really a reason to not run the multi-threaded rate-limiting task manager all the time. Modify it slightly so that rate=None means "don't rate limit", which should keep the existing behavior. Change-Id: I3ede4fd0b12a65effade238c4ea967aca51869ba --- openstack/_adapter.py | 1 + openstack/connection.py | 1 + openstack/task_manager.py | 109 ++++++++---------- .../tests/unit/cloud/test_task_manager.py | 1 + 4 files changed, 48 insertions(+), 64 deletions(-) diff --git a/openstack/_adapter.py b/openstack/_adapter.py index 5340888b9..dfe2e3691 100644 --- a/openstack/_adapter.py +++ b/openstack/_adapter.py @@ -121,6 +121,7 @@ def __init__(self, session=None, task_manager=None, *args, **kwargs): session=session, *args, **kwargs) if not task_manager: task_manager = _task_manager.TaskManager(name=self.service_type) + task_manager.start() self.task_manager = task_manager diff --git a/openstack/connection.py b/openstack/connection.py index 3459d3a86..3e19f6757 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -290,6 +290,7 @@ def __init__(self, cloud=None, config=None, session=None, self.task_manager = task_manager or _task_manager.TaskManager( self.config.full_name) + self.task_manager.start() self._session = None self._proxies = {} diff --git a/openstack/task_manager.py b/openstack/task_manager.py index e2273bb76..6a58bb06a 100644 --- a/openstack/task_manager.py +++ b/openstack/task_manager.py @@ -95,11 +95,19 @@ def run(self): class TaskManager(object): - def __init__(self, name, log=_log, workers=5, **kwargs): + def __init__(self, name, rate=None, log=_log, workers=5, **kwargs): self.name = name self._executor = None self._log = log self._workers = workers + self.daemon = True + self.queue = queue.Queue() + self._running = True + if rate is not None: + rate = float(rate) + self.rate = rate + self._thread = threading.Thread(name=name, target=self.run) + self._thread.daemon = True @property def executor(self): @@ -108,18 +116,42 @@ def executor(self): max_workers=self._workers) return self._executor + def start(self): + self._thread.start() + def stop(self): - """ This is a direct action passthrough TaskManager """ + self._running = False + self.queue.put(None) if self._executor: self._executor.shutdown() - def run(self): - """ This is a direct action passthrough TaskManager """ - pass - def join(self): - """ This is a direct action passthrough TaskManager """ - pass + self._thread.join() + + def run(self): + last_ts = 0 + try: + while True: + task = self.queue.get() + if not task: + if not self._running: + break + continue + if self.rate: + while True: + delta = time.time() - last_ts + if delta >= self.rate: + break + time.sleep(self.rate - delta) + self._log.debug( + "TaskManager {name} queue size: {size})".format( + name=self.name, + size=self.queue.qsize())) + self.run_task(task) + self.queue.task_done() + except Exception: + self._log.exception("TaskManager died") + raise def submit_task(self, task): """Submit and execute the given task. @@ -131,7 +163,11 @@ def submit_task(self, task): This method calls task.wait() so that it only returns when the task is complete. """ - self.run_task(task=task) + if not self._running: + raise exceptions.TaskManagerStopped( + "TaskManager {name} is no longer running".format( + name=self.name)) + self.queue.put(task) return task.wait() def submit_function( @@ -190,61 +226,6 @@ def post_run_task(self, elapsed_time, task): self.name, task.name, elapsed_time) -class RateLimitingTaskManager(TaskManager): - - def __init__(self, name, rate, workers=5): - super(TaskManager, self).__init__( - name=name, workers=workers) - self.daemon = True - self.queue = queue.Queue() - self._running = True - self.rate = float(rate) - self._thread = threading.Thread(name=name, target=self.run) - self._thread.daemon = True - - def start(self): - self._thread.start() - - def stop(self): - self._running = False - self.queue.put(None) - - def join(self): - self._thread.join() - - def run(self): - last_ts = 0 - try: - while True: - task = self.queue.get() - if not task: - if not self._running: - break - continue - while True: - delta = time.time() - last_ts - if delta >= self.rate: - break - time.sleep(self.rate - delta) - self._log.debug( - "TaskManager {name} queue size: {size})".format( - name=self.name, - size=self.queue.qsize())) - self.run_task(task) - self.queue.task_done() - except Exception: - self._log.exception("TaskManager died") - raise - - def submit_task(self, task): - if not self._running: - raise exceptions.TaskManagerStopped( - "TaskManager {name} is no longer running".format( - name=self.name)) - self.queue.put(task) - return task.wait() - - def wait_for_futures(futures, raise_on_error=True, log=_log): '''Collect results or failures from a list of running future tasks.''' diff --git a/openstack/tests/unit/cloud/test_task_manager.py b/openstack/tests/unit/cloud/test_task_manager.py index 777cb90f4..6c4b19c0c 100644 --- a/openstack/tests/unit/cloud/test_task_manager.py +++ b/openstack/tests/unit/cloud/test_task_manager.py @@ -68,6 +68,7 @@ class TestTaskManager(base.TestCase): def setUp(self): super(TestTaskManager, self).setUp() self.manager = task_manager.TaskManager(name='test') + self.manager.start() def test_wait_re_raise(self): """Test that Exceptions thrown in a Task is reraised correctly From 3ce157c384a09074549391fd86dc4c7d8cf043ba Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 28 Sep 2018 10:17:56 -0500 Subject: [PATCH 2223/3836] Add some warnings and clarifications for discovery There are some choices openstacksdk is making as part of discovery. Some of them may not be 100% obvious, so throw some warnings. Make them named warnings so that they can be silenced by a user who is using sdk to get adapters for unsupported versions of things on purpose. Change-Id: I686d90891554de2ecdec3d744699d75b0d12647a --- openstack/config/cloud_region.py | 5 ++++ openstack/exceptions.py | 4 +++ openstack/service_description.py | 51 +++++++++++++++++++++++++++----- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 32902bcf7..1bb421d84 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -257,6 +257,11 @@ def get_api_version(self, service_type): try: float(version) except ValueError: + if 'latest' in version: + warnings.warn( + "You have a configured API_VERSION with 'latest' in" + " it. In the context of openstacksdk this doesn't make" + " any sense.") return None return version diff --git a/openstack/exceptions.py b/openstack/exceptions.py index 0c423dc49..55cb9808a 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -228,6 +228,10 @@ def raise_from_response(response, error_message=None): ) +class UnsupportedServiceVersion(Warning): + """The user has configured a major version that SDK doesn't know.""" + + class ArgumentDeprecationWarning(Warning): """A deprecated argument has been provided.""" pass diff --git a/openstack/service_description.py b/openstack/service_description.py index e49d9db83..9ae36488b 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -11,9 +11,12 @@ # License for the specific language governing permissions and limitations # under the License. +import warnings + import os_service_types from openstack import _log +from openstack import exceptions from openstack import proxy __all__ = [ @@ -74,12 +77,16 @@ def __get__(self, instance, owner): if instance is None: return self if self.service_type not in instance._proxies: - instance._proxies[self.service_type] = self._make_proxy( - instance, owner) + instance._proxies[self.service_type] = self._make_proxy(instance) instance._proxies[self.service_type]._connection = instance return instance._proxies[self.service_type] - def _make_proxy(self, instance, owner): + def _make_proxy(self, instance): + """Create a Proxy for the service in question. + + :param instance: + The `openstack.connection.Connection` we're working with. + """ config = instance.config # First, check to see if we've got config that matches what we @@ -90,7 +97,7 @@ def _make_proxy(self, instance, owner): # If the user doesn't give a version in config, but we only support # one version, then just use that version. if not version_string and len(self.supported_versions) == 1: - version_string = list(self.supported_versions.keys())[0] + version_string = list(self.supported_versions)[0] proxy_obj = None if endpoint_override and version_string and self.supported_versions: @@ -103,6 +110,15 @@ def _make_proxy(self, instance, owner): constructor=proxy_class, task_manager=instance.task_manager, ) + else: + warnings.warn( + "The configured version, {version} for service" + " {service_type} is not known or supported by" + " openstacksdk. The resulting Proxy object will only" + " have direct passthrough REST capabilities.".format( + version=version_string, + service_type=self.service_type), + category=exceptions.UnsupportedVersionWarning) elif endpoint_override and self.supported_versions: temp_adapter = config.get_session_client( self.service_type @@ -115,9 +131,25 @@ def _make_proxy(self, instance, owner): constructor=proxy_class, task_manager=instance.task_manager, ) + else: + warnings.warn( + "Service {service_type) has an endpoint override set" + " but the version discovered at that endpoint, {version}" + " is not supported by openstacksdk. The resulting Proxy" + " object will only have direct passthrough REST" + " capabilities.".format( + version=api_version, + service_type=self.service_type), + category=exceptions.UnsupportedVersionWarning) if proxy_obj: data = proxy_obj.get_endpoint_data() + # If we've gotten here with a proxy object it means we have + # an endpoint_override in place. If the catalog_url and + # service_url don't match, which can happen if there is a + # None plugin and auth.endpoint like with standalone ironic, + # we need to be explicit that this service has an endpoint_override + # so that subsequent discovery calls don't get made incorrectly. if data.catalog_url != data.service_url: ep_key = '{service_type}_endpoint_override'.format( service_type=self.service_type) @@ -152,9 +184,14 @@ def _make_proxy(self, instance, owner): # REST API proxy layer for an unknown service in the # service catalog that also doesn't have any useful # version discovery? - instance._proxies[self.service_type] = temp_adapter - instance._proxies[self.service_type]._connection = instance - return instance._proxies[self.service_type] + warnings.warn( + "Service {service_type) has no discoverable version." + " The resulting Proxy object will only have direct" + " passthrough REST capabilities.".format( + version=api_version, + service_type=self.service_type), + category=exceptions.UnsupportedVersionWarning) + return temp_adapter proxy_class = self.supported_versions.get(str(found_version[0])) if not proxy_class: proxy_class = proxy.Proxy From 0962342cf29f4aa431fe6889524d33844f0c2d68 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Mon, 8 Oct 2018 15:41:19 +0200 Subject: [PATCH 2224/3836] Correct updating baremetal nodes by name or ID Currently any resource created via Resource.new is marked as clean from the point of view of JSON patch. This is not correct and prevents node updates from working when the first argument is an ID. Change-Id: I850cb6defb71c62121eafbf853473013e2780e2b --- openstack/resource.py | 10 +++++++- .../baremetal/test_baremetal_node.py | 25 +++++++++++++++++-- openstack/tests/unit/image/v2/test_image.py | 2 +- openstack/tests/unit/test_resource.py | 18 ++++++++++++- 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/openstack/resource.py b/openstack/resource.py index 07a822c5b..f7ac4ca6c 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -434,7 +434,15 @@ def __init__(self, _synchronized=False, connection=None, **attrs): synchronized=_synchronized) if self.commit_jsonpatch: # We need the original body to compare against - self._original_body = self._body.attributes.copy() + if _synchronized: + self._original_body = self._body.attributes.copy() + elif self.id: + # Never record ID as dirty. + self._original_body = { + self._alternate_id() or 'id': self.id + } + else: + self._original_body = {} # TODO(mordred) This is terrible, but is a hack at the moment to ensure # json.dumps works. The json library does basically if not obj: and diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index 45c4f2c94..60910951c 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -48,7 +48,7 @@ def test_node_update(self): instance_uuid = str(uuid.uuid4()) node = self.conn.baremetal.update_node(node, - instance_uuid=instance_uuid) + instance_id=instance_uuid) self.assertEqual('new-name', node.name) self.assertEqual({'answer': 42}, node.extra) self.assertEqual(instance_uuid, node.instance_id) @@ -59,12 +59,33 @@ def test_node_update(self): self.assertEqual(instance_uuid, node.instance_id) node = self.conn.baremetal.update_node(node, - instance_uuid=None) + instance_id=None) self.assertIsNone(node.instance_id) node = self.conn.baremetal.get_node('new-name') self.assertIsNone(node.instance_id) + def test_node_update_by_name(self): + self.create_node(name='node-name', extra={'foo': 'bar'}) + instance_uuid = str(uuid.uuid4()) + + node = self.conn.baremetal.update_node('node-name', + instance_id=instance_uuid, + extra={'answer': 42}) + self.assertEqual({'answer': 42}, node.extra) + self.assertEqual(instance_uuid, node.instance_id) + + node = self.conn.baremetal.get_node('node-name') + self.assertEqual({'answer': 42}, node.extra) + self.assertEqual(instance_uuid, node.instance_id) + + node = self.conn.baremetal.update_node('node-name', + instance_id=None) + self.assertIsNone(node.instance_id) + + node = self.conn.baremetal.get_node('node-name') + self.assertIsNone(node.instance_id) + def test_node_create_in_enroll_provide(self): node = self.create_node(provision_state='enroll') self.node_id = node.id diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index e2d1ba1b1..c9a756a98 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -309,7 +309,7 @@ def test_download_stream(self): def test_image_update(self): values = EXAMPLE.copy() del values['instance_uuid'] - sot = image.Image(**values) + sot = image.Image.existing(**values) # Let the translate pass through, that portion is tested elsewhere sot._translate_response = mock.Mock() diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 70f3a4b2d..09679210f 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -872,7 +872,7 @@ class Test(resource.Resource): y = resource.Body("y") the_id = "id" - sot = Test(id=the_id, x=1, y=2) + sot = Test.existing(id=the_id, x=1, y=2) sot.x = 3 result = sot._prepare_request(requires_id=True, patch=True) @@ -881,6 +881,22 @@ class Test(resource.Resource): self.assertEqual([{'op': 'replace', 'path': '/x', 'value': 3}], result.body) + def test__prepare_request_with_patch_not_synchronized(self): + class Test(resource.Resource): + commit_jsonpatch = True + base_path = "/something" + x = resource.Body("x") + y = resource.Body("y") + + the_id = "id" + sot = Test.new(id=the_id, x=1) + + result = sot._prepare_request(requires_id=True, patch=True) + + self.assertEqual("something/id", result.url) + self.assertEqual([{'op': 'add', 'path': '/x', 'value': 1}], + result.body) + def test__translate_response_no_body(self): class Test(resource.Resource): attr = resource.Header("attr") From af3a8f4c12d5fcb00043507d11ec09487cd5d236 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Tue, 18 Sep 2018 17:38:39 +0200 Subject: [PATCH 2225/3836] Wire in retries for all baremetal actions The baremetal API can output HTTP 409 during its normal operation and 503 under load. This change enables retries for them. Since node update can naturally result in HTTP 409, provide a way to opt out of retrying on it. Additionally, allow retrying on HTTP 409 for all services via commit(). Change-Id: I765c4066e26706abe5f576c98353a48a17da6f9c --- openstack/baremetal/v1/_proxy.py | 12 +++++- openstack/proxy.py | 14 +++++++ openstack/resource.py | 22 +++++++++-- .../tests/unit/baremetal/v1/test_proxy.py | 16 +++++++- openstack/tests/unit/image/v2/test_image.py | 1 + openstack/tests/unit/test_resource.py | 38 ++++++++++++++++--- .../baremetal-retries-ff8aa8f73fb97415.yaml | 6 +++ 7 files changed, 96 insertions(+), 13 deletions(-) create mode 100644 releasenotes/notes/baremetal-retries-ff8aa8f73fb97415.yaml diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index ab62b9758..875836849 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -11,6 +11,7 @@ # under the License. from openstack import _log +from openstack.baremetal.v1 import _common from openstack.baremetal.v1 import chassis as _chassis from openstack.baremetal.v1 import driver as _driver from openstack.baremetal.v1 import node as _node @@ -25,6 +26,8 @@ class Proxy(proxy.Proxy): + retriable_status_codes = _common.RETRIABLE_STATUS_CODES + def chassis(self, details=False, **query): """Retrieve a generator of chassis. @@ -238,18 +241,23 @@ def get_node(self, node): """ return self._get(_node.Node, node) - def update_node(self, node, **attrs): + def update_node(self, node, retry_on_conflict=True, **attrs): """Update a node. :param chassis: Either the name or the ID of a node or an instance of :class:`~openstack.baremetal.v1.node.Node`. + :param bool retry_on_conflict: Whether to retry HTTP CONFLICT error. + Most of the time it can be retried, since it is caused by the node + being locked. However, when setting ``instance_id``, this is + a normal code and should not be retried. :param dict attrs: The attributes to update on the node represented by the ``node`` parameter. :returns: The updated node. :rtype: :class:`~openstack.baremetal.v1.node.Node` """ - return self._update(_node.Node, node, **attrs) + res = self._get_resource(_node.Node, node, **attrs) + return res.commit(self, retry_on_conflict=retry_on_conflict) def set_node_provision_state(self, node, target, config_drive=None, clean_steps=None, rescue_password=None, diff --git a/openstack/proxy.py b/openstack/proxy.py index cc6a751ec..26e8a0bd9 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -46,6 +46,20 @@ def check(self, expected, actual=None, *args, **kwargs): class Proxy(six.with_metaclass(_meta.ProxyMeta, _adapter.OpenStackSDKAdapter)): """Represents a service.""" + retriable_status_codes = None + """HTTP status codes that should be retried by default. + + The number of retries is defined by the configuration in parameters called + ``_status_code_retries``. + """ + + def __init__(self, *args, **kwargs): + # NOTE(dtantsur): keystoneauth defaults retriable_status_codes to None, + # override it with a class-level value. + kwargs.setdefault('retriable_status_codes', + self.retriable_status_codes) + super(Proxy, self).__init__(*args, **kwargs) + def _get_resource(self, resource_type, value, **attrs): """Get a resource object to work on diff --git a/openstack/resource.py b/openstack/resource.py index f7ac4ca6c..84737c095 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1107,7 +1107,8 @@ def head(self, session): self._translate_response(response, has_body=False) return self - def commit(self, session, prepend_key=True, has_body=True): + def commit(self, session, prepend_key=True, has_body=True, + retry_on_conflict=None): """Commit the state of the instance to the remote resource. :param session: The session to use for making this request. @@ -1115,6 +1116,9 @@ def commit(self, session, prepend_key=True, has_body=True): :param prepend_key: A boolean indicating whether the resource_key should be prepended in a resource update request. Default to True. + :param bool retry_on_conflict: Whether to enable retries on HTTP + CONFLICT (409). Value of ``None`` leaves + the `Adapter` defaults. :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if @@ -1138,20 +1142,30 @@ def commit(self, session, prepend_key=True, has_body=True): request = self._prepare_request(prepend_key=prepend_key, **kwargs) session = self._get_session(session) + + kwargs = {} + retriable_status_codes = set(session.retriable_status_codes or ()) + if retry_on_conflict: + kwargs['retriable_status_codes'] = retriable_status_codes | {409} + elif retry_on_conflict is not None and retriable_status_codes: + # The baremetal proxy defaults to retrying on conflict, allow + # overriding it via an explicit retry_on_conflict=False. + kwargs['retriable_status_codes'] = retriable_status_codes - {409} + microversion = self._get_microversion_for(session, 'commit') if self.commit_method == 'PATCH': response = session.patch( request.url, json=request.body, headers=request.headers, - microversion=microversion) + microversion=microversion, **kwargs) elif self.commit_method == 'POST': response = session.post( request.url, json=request.body, headers=request.headers, - microversion=microversion) + microversion=microversion, **kwargs) elif self.commit_method == 'PUT': response = session.put( request.url, json=request.body, headers=request.headers, - microversion=microversion) + microversion=microversion, **kwargs) else: raise exceptions.ResourceFailure( msg="Invalid commit method: %s" % self.commit_method) diff --git a/openstack/tests/unit/baremetal/v1/test_proxy.py b/openstack/tests/unit/baremetal/v1/test_proxy.py index 5193078c5..930ed6bdd 100644 --- a/openstack/tests/unit/baremetal/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal/v1/test_proxy.py @@ -87,8 +87,20 @@ def test_find_node(self): def test_get_node(self): self.verify_get(self.proxy.get_node, node.Node) - def test_update_node(self): - self.verify_update(self.proxy.update_node, node.Node) + @mock.patch.object(node.Node, 'commit', autospec=True) + def test_update_node(self, mock_commit): + self.proxy.update_node('uuid', instance_id='new value') + mock_commit.assert_called_once_with(mock.ANY, self.proxy, + retry_on_conflict=True) + self.assertEqual('new value', mock_commit.call_args[0][0].instance_id) + + @mock.patch.object(node.Node, 'commit', autospec=True) + def test_update_node_no_retries(self, mock_commit): + self.proxy.update_node('uuid', instance_id='new value', + retry_on_conflict=False) + mock_commit.assert_called_once_with(mock.ANY, self.proxy, + retry_on_conflict=False) + self.assertEqual('new value', mock_commit.call_args[0][0].instance_id) def test_delete_node(self): self.verify_delete(self.proxy.delete_node, node.Node, False) diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index c9a756a98..393aacccc 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -104,6 +104,7 @@ def setUp(self): self.sess = mock.Mock(spec=adapter.Adapter) self.sess.post = mock.Mock(return_value=self.resp) self.sess.default_microversion = None + self.sess.retriable_status_codes = None def test_basic(self): sot = image.Image() diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 09679210f..2719caf4a 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1016,6 +1016,7 @@ class Test(resource.Resource): self.session.delete = mock.Mock(return_value=self.response) self.session.head = mock.Mock(return_value=self.response) self.session.default_microversion = None + self.session.retriable_status_codes = None self.endpoint_data = mock.Mock(max_microversion='1.99', min_microversion=None) @@ -1157,7 +1158,8 @@ class Test(resource.Resource): self.assertEqual(result, sot) def _test_commit(self, commit_method='PUT', prepend_key=True, - has_body=True, microversion=None): + has_body=True, microversion=None, + commit_args=None, expected_args=None): self.sot.commit_method = commit_method # Need to make sot look dirty so we can attempt an update @@ -1165,7 +1167,7 @@ def _test_commit(self, commit_method='PUT', prepend_key=True, self.sot._body.dirty = mock.Mock(return_value={"x": "y"}) self.sot.commit(self.session, prepend_key=prepend_key, - has_body=has_body) + has_body=has_body, **(commit_args or {})) self.sot._prepare_request.assert_called_once_with( prepend_key=prepend_key) @@ -1174,17 +1176,17 @@ def _test_commit(self, commit_method='PUT', prepend_key=True, self.session.patch.assert_called_once_with( self.request.url, json=self.request.body, headers=self.request.headers, - microversion=microversion) + microversion=microversion, **(expected_args or {})) elif commit_method == 'POST': self.session.post.assert_called_once_with( self.request.url, json=self.request.body, headers=self.request.headers, - microversion=microversion) + microversion=microversion, **(expected_args or {})) elif commit_method == 'PUT': self.session.put.assert_called_once_with( self.request.url, json=self.request.body, headers=self.request.headers, - microversion=microversion) + microversion=microversion, **(expected_args or {})) self.assertEqual(self.sot.microversion, microversion) self.sot._translate_response.assert_called_once_with( @@ -1197,6 +1199,32 @@ def test_commit_patch(self): self._test_commit( commit_method='PATCH', prepend_key=False, has_body=False) + def test_commit_patch_retry_on_conflict(self): + self._test_commit( + commit_method='PATCH', + commit_args={'retry_on_conflict': True}, + expected_args={'retriable_status_codes': {409}}) + + def test_commit_put_retry_on_conflict(self): + self._test_commit( + commit_method='PUT', + commit_args={'retry_on_conflict': True}, + expected_args={'retriable_status_codes': {409}}) + + def test_commit_patch_no_retry_on_conflict(self): + self.session.retriable_status_codes = {409, 503} + self._test_commit( + commit_method='PATCH', + commit_args={'retry_on_conflict': False}, + expected_args={'retriable_status_codes': {503}}) + + def test_commit_put_no_retry_on_conflict(self): + self.session.retriable_status_codes = {409, 503} + self._test_commit( + commit_method='PATCH', + commit_args={'retry_on_conflict': False}, + expected_args={'retriable_status_codes': {503}}) + def test_commit_not_dirty(self): self.sot._body = mock.Mock() self.sot._body.dirty = dict() diff --git a/releasenotes/notes/baremetal-retries-ff8aa8f73fb97415.yaml b/releasenotes/notes/baremetal-retries-ff8aa8f73fb97415.yaml new file mode 100644 index 000000000..c654c5b18 --- /dev/null +++ b/releasenotes/notes/baremetal-retries-ff8aa8f73fb97415.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + The bare metal operations now retry HTTP 409 and 503 by default. The number + of retries can be changes via the ``baremetal_status_code_retries`` + configuration option (defaulting to 5). From 7b8a2e0cd2773e15dfa03d0e015f6dc8fa76b3ee Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Wed, 10 Oct 2018 15:07:15 +0200 Subject: [PATCH 2226/3836] cloud: rename with deprecation validate_node -> validate_machine Most of other cloud methods call nodes "machines". The new call also has an ability to only validate the power interface. Change-Id: I35f0521a57342dc888c00790935f20de230d6de5 --- openstack/cloud/openstackcloud.py | 20 +++++- .../tests/unit/cloud/test_baremetal_node.py | 69 ++++++++++++++++--- .../validate-machine-dcf528b8f587e3f0.yaml | 5 ++ 3 files changed, 83 insertions(+), 11 deletions(-) create mode 100644 releasenotes/notes/validate-machine-dcf528b8f587e3f0.yaml diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 37f6494e5..dbe704df9 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -9941,9 +9941,25 @@ def list_ports_attached_to_machine(self, name_or_id): vif_ids = self.baremetal.list_node_vifs(machine) return [self.get_port(vif) for vif in vif_ids] + def validate_machine(self, name_or_id, for_deploy=True): + """Validate parameters of the machine. + + :param string name_or_id: The Name or UUID value representing the + baremetal node. + :param bool for_deploy: If ``True``, validate readiness for deployment, + otherwise validate only the power management + properties. + :raises: :exc:`~openstack.exceptions.ValidationException` + """ + if for_deploy: + ifaces = ('boot', 'deploy', 'management', 'power') + else: + ifaces = ('power',) + self.baremetal.validate_node(name_or_id, required=ifaces) + def validate_node(self, uuid): - # TODO(dtantsur): deprecate this short method in favor of a fully - # written validate_machine call. + warnings.warn('validate_node is deprecated, please use ' + 'validate_machine instead', DeprecationWarning) self.baremetal.validate_node(uuid) def node_set_provision_state(self, diff --git a/openstack/tests/unit/cloud/test_baremetal_node.py b/openstack/tests/unit/cloud/test_baremetal_node.py index a89ed5c45..46b427b38 100644 --- a/openstack/tests/unit/cloud/test_baremetal_node.py +++ b/openstack/tests/unit/cloud/test_baremetal_node.py @@ -92,13 +92,64 @@ def test_get_machine_by_mac(self): self.fake_baremetal_node['uuid']) self.assert_calls() - def test_validate_node(self): + def test_validate_machine(self): # NOTE(TheJulia): Note: These are only the interfaces - # that are validated, and both must be true for an + # that are validated, and all must be true for an # exception to not be raised. - # This should be fixed at some point, as some interfaces - # are important in some cases and should be validated, - # such as storage. + validate_return = { + 'boot': { + 'result': True, + }, + 'deploy': { + 'result': True, + }, + 'management': { + 'result': True, + }, + 'power': { + 'result': True, + }, + 'foo': { + 'result': False, + }} + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'validate']), + json=validate_return), + ]) + self.cloud.validate_machine(self.fake_baremetal_node['uuid']) + + self.assert_calls() + + def test_validate_machine_not_for_deploy(self): + validate_return = { + 'deploy': { + 'result': False, + 'reason': 'Not ready', + }, + 'power': { + 'result': True, + }, + 'foo': { + 'result': False, + }} + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'validate']), + json=validate_return), + ]) + self.cloud.validate_machine(self.fake_baremetal_node['uuid'], + for_deploy=False) + + self.assert_calls() + + def test_deprecated_validate_node(self): validate_return = { 'deploy': { 'result': True, @@ -121,15 +172,15 @@ def test_validate_node(self): self.assert_calls() - def test_validate_node_raises_exception(self): + def test_validate_machine_raises_exception(self): validate_return = { 'deploy': { 'result': False, 'reason': 'error!', }, 'power': { - 'result': False, - 'reason': 'meow!', + 'result': True, + 'reason': None, }, 'foo': { 'result': True @@ -144,7 +195,7 @@ def test_validate_node_raises_exception(self): ]) self.assertRaises( exceptions.ValidationException, - self.cloud.validate_node, + self.cloud.validate_machine, self.fake_baremetal_node['uuid']) self.assert_calls() diff --git a/releasenotes/notes/validate-machine-dcf528b8f587e3f0.yaml b/releasenotes/notes/validate-machine-dcf528b8f587e3f0.yaml new file mode 100644 index 000000000..0f73ae8d3 --- /dev/null +++ b/releasenotes/notes/validate-machine-dcf528b8f587e3f0.yaml @@ -0,0 +1,5 @@ +--- +deprecations: + - | + The ``OpenStackCloud.validate_node`` call was deprecated in favor of + ``OpenStackCloud.validate_machine``. From dff07f49fd410aca64344b7db95564ed22e3f11d Mon Sep 17 00:00:00 2001 From: Matthias Lisin Date: Mon, 6 Aug 2018 17:16:27 +0200 Subject: [PATCH 2227/3836] openstackcloud.py: Implement FWaaS wrapper methods. Change-Id: I06c74ff6cdf9115ce75ea231b3dcacda7d286b64 --- openstack/cloud/openstackcloud.py | 487 +++++++++ openstack/tests/unit/cloud/test_fwaas.py | 1150 ++++++++++++++++++++++ 2 files changed, 1637 insertions(+) create mode 100644 openstack/tests/unit/cloud/test_fwaas.py diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 831f79a46..eb2f0e5e8 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -41,6 +41,7 @@ from openstack import _adapter from openstack import _log +from openstack import exceptions from openstack.cloud import exc from openstack.cloud._heat import event_utils from openstack.cloud._heat import template_utils @@ -12474,3 +12475,489 @@ def update_cluster_receiver(self, name_or_id, new_name=None, action=None, error_message="Error updating receiver {name}".format( name=name_or_id)) return self._get_and_munchify(key=None, data=data) + + @_utils.valid_kwargs( + 'action', 'description', 'destination_firewall_group_id', + 'destination_ip_address', 'destination_port', 'enabled', 'ip_version', + 'name', 'project_id', 'protocol', 'shared', 'source_firewall_group_id', + 'source_ip_address', 'source_port') + def create_firewall_rule(self, **kwargs): + """ + Creates firewall rule. + + :param action: Action performed on traffic. + Valid values: allow, deny + Defaults to deny. + :param description: Human-readable description. + :param destination_firewall_group_id: ID of destination firewall group. + :param destination_ip_address: IPv4-, IPv6 address or CIDR. + :param destination_port: Port or port range (e.g. 80:90) + :param bool enabled: Status of firewall rule. You can disable rules + without disassociating them from firewall + policies. Defaults to True. + :param int ip_version: IP Version. + Valid values: 4, 6 + Defaults to 4. + :param name: Human-readable name. + :param project_id: Project id. + :param protocol: IP protocol. + Valid values: icmp, tcp, udp, null + :param bool shared: Visibility to other projects. + Defaults to False. + :param source_firewall_group_id: ID of source firewall group. + :param source_ip_address: IPv4-, IPv6 adress or CIDR. + :param source_port: Port or port range (e.g. 80:90) + :raises: BadRequestException if parameters are malformed + :return: created firewall rule + :rtype: FirewallRule + """ + return self.network.create_firewall_rule(**kwargs) + + def delete_firewall_rule(self, name_or_id, filters=None): + """ + Deletes firewall rule. + Prints debug message in case to-be-deleted resource was not found. + + :param name_or_id: firewall rule name or id + :param dict filters: optional filters + :raises: DuplicateResource on multiple matches + :return: True if resource is successfully deleted, False otherwise. + :rtype: bool + """ + if not filters: + filters = {} + try: + firewall_rule = self.network.find_firewall_rule( + name_or_id, ignore_missing=False, **filters) + self.network.delete_firewall_rule(firewall_rule, + ignore_missing=False) + except exceptions.ResourceNotFound: + self.log.debug('Firewall rule %s not found for deleting', + name_or_id) + return False + return True + + def get_firewall_rule(self, name_or_id, filters=None): + """ + Retrieves a single firewall rule. + + :param name_or_id: firewall rule name or id + :param dict filters: optional filters + :raises: DuplicateResource on multiple matches + :return: firewall rule dict or None if not found + :rtype: FirewallRule + """ + if not filters: + filters = {} + return self.network.find_firewall_rule(name_or_id, **filters) + + def list_firewall_rules(self, filters=None): + """ + Lists firewall rules. + + :param dict filters: optional filters + :return: list of firewall rules + :rtype: list[FirewallRule] + """ + if not filters: + filters = {} + return list(self.network.firewall_rules(**filters)) + + @_utils.valid_kwargs( + 'action', 'description', 'destination_firewall_group_id', + 'destination_ip_address', 'destination_port', 'enabled', 'ip_version', + 'name', 'project_id', 'protocol', 'shared', 'source_firewall_group_id', + 'source_ip_address', 'source_port') + def update_firewall_rule(self, name_or_id, filters=None, **kwargs): + """ + Updates firewall rule. + + :param name_or_id: firewall rule name or id + :param dict filters: optional filters + :param kwargs: firewall rule update parameters. + See create_firewall_rule docstring for valid parameters. + :raises: BadRequestException if parameters are malformed + :raises: NotFoundException if resource is not found + :return: updated firewall rule + :rtype: FirewallRule + """ + if not filters: + filters = {} + firewall_rule = self.network.find_firewall_rule( + name_or_id, ignore_missing=False, **filters) + + return self.network.update_firewall_rule(firewall_rule, **kwargs) + + def _get_firewall_rule_ids(self, name_or_id_list, filters=None): + """ + Takes a list of firewall rule name or ids, looks them up and returns + a list of firewall rule ids. + + Used by `create_firewall_policy` and `update_firewall_policy`. + + :param list[str] name_or_id_list: firewall rule name or id list + :param dict filters: optional filters + :raises: DuplicateResource on multiple matches + :raises: NotFoundException if resource is not found + :return: list of firewall rule ids + :rtype: list[str] + """ + if not filters: + filters = {} + ids_list = [] + for name_or_id in name_or_id_list: + ids_list.append(self.network.find_firewall_rule( + name_or_id, ignore_missing=False, **filters)['id']) + return ids_list + + @_utils.valid_kwargs('audited', 'description', 'firewall_rules', 'name', + 'project_id', 'shared') + def create_firewall_policy(self, **kwargs): + """ + Create firewall policy. + + :param bool audited: Status of audition of firewall policy. + Set to False each time the firewall policy or the + associated firewall rules are changed. + Has to be explicitly set to True. + :param description: Human-readable description. + :param list[str] firewall_rules: List of associated firewall rules. + :param name: Human-readable name. + :param project_id: Project id. + :param bool shared: Visibility to other projects. + Defaults to False. + :raises: BadRequestException if parameters are malformed + :raises: ResourceNotFound if a resource from firewall_list not found + :return: created firewall policy + :rtype: FirewallPolicy + """ + if 'firewall_rules' in kwargs: + kwargs['firewall_rules'] = self._get_firewall_rule_ids( + kwargs['firewall_rules']) + + return self.network.create_firewall_policy(**kwargs) + + def delete_firewall_policy(self, name_or_id, filters=None): + """ + Deletes firewall policy. + Prints debug message in case to-be-deleted resource was not found. + + :param name_or_id: firewall policy name or id + :param dict filters: optional filters + :raises: DuplicateResource on multiple matches + :return: True if resource is successfully deleted, False otherwise. + :rtype: bool + """ + if not filters: + filters = {} + try: + firewall_policy = self.network.find_firewall_policy( + name_or_id, ignore_missing=False, **filters) + self.network.delete_firewall_policy(firewall_policy, + ignore_missing=False) + except exceptions.ResourceNotFound: + self.log.debug('Firewall policy %s not found for deleting', + name_or_id) + return False + return True + + def get_firewall_policy(self, name_or_id, filters=None): + """ + Retrieves a single firewall policy. + + :param name_or_id: firewall policy name or id + :param dict filters: optional filters + :raises: DuplicateResource on multiple matches + :return: firewall policy or None if not found + :rtype: FirewallPolicy + """ + if not filters: + filters = {} + return self.network.find_firewall_policy(name_or_id, **filters) + + def list_firewall_policies(self, filters=None): + """ + Lists firewall policies. + + :param dict filters: optional filters + :return: list of firewall policies + :rtype: list[FirewallPolicy] + """ + if not filters: + filters = {} + return list(self.network.firewall_policies(**filters)) + + @_utils.valid_kwargs('audited', 'description', 'firewall_rules', 'name', + 'project_id', 'shared') + def update_firewall_policy(self, name_or_id, filters=None, **kwargs): + """ + Updates firewall policy. + + :param name_or_id: firewall policy name or id + :param dict filters: optional filters + :param kwargs: firewall policy update parameters + See create_firewall_policy docstring for valid parameters. + :raises: BadRequestException if parameters are malformed + :raises: DuplicateResource on multiple matches + :raises: ResourceNotFound if resource is not found + :return: updated firewall policy + :rtype: FirewallPolicy + """ + if not filters: + filters = {} + firewall_policy = self.network.find_firewall_policy( + name_or_id, ignore_missing=False, **filters) + + if 'firewall_rules' in kwargs: + kwargs['firewall_rules'] = self._get_firewall_rule_ids( + kwargs['firewall_rules']) + + return self.network.update_firewall_policy(firewall_policy, **kwargs) + + def insert_rule_into_policy(self, name_or_id, rule_name_or_id, + insert_after=None, insert_before=None, + filters=None): + """ + Adds firewall rule to the firewall_rules list of a firewall policy. + Short-circuits and returns the firewall policy early if the firewall + rule id is already present in the firewall_rules list. + This method doesn't do re-ordering. If you want to move a firewall rule + or or down the list, you have to remove and re-add it. + + :param name_or_id: firewall policy name or id + :param rule_name_or_id: firewall rule name or id + :param insert_after: rule name or id that should precede added rule + :param insert_before: rule name or id that should succeed added rule + :param dict filters: optional filters + :raises: DuplicateResource on multiple matches + :raises: ResourceNotFound if firewall policy or any of the firewall + rules (inserted, after, before) is not found. + :return: updated firewall policy + :rtype: FirewallPolicy + """ + if not filters: + filters = {} + firewall_policy = self.network.find_firewall_policy( + name_or_id, ignore_missing=False, **filters) + + firewall_rule = self.network.find_firewall_rule( + rule_name_or_id, ignore_missing=False) + # short-circuit if rule already in firewall_rules list + # the API can't do any re-ordering of existing rules + if firewall_rule['id'] in firewall_policy['firewall_rules']: + self.log.debug( + 'Firewall rule %s already associated with firewall policy %s', + rule_name_or_id, name_or_id) + return firewall_policy + + pos_params = {} + if insert_after is not None: + pos_params['insert_after'] = self.network.find_firewall_rule( + insert_after, ignore_missing=False)['id'] + + if insert_before is not None: + pos_params['insert_before'] = self.network.find_firewall_rule( + insert_before, ignore_missing=False)['id'] + + return self.network.insert_rule_into_policy(firewall_policy['id'], + firewall_rule['id'], + **pos_params) + + def remove_rule_from_policy(self, name_or_id, rule_name_or_id, + filters=None): + """ + Remove firewall rule from firewall policy's firewall_rules list. + Short-circuits and returns firewall policy early if firewall rule + is already absent from the firewall_rules list. + + :param name_or_id: firewall policy name or id + :param rule_name_or_id: firewall rule name or id + :param dict filters: optional filters + :raises: DuplicateResource on multiple matches + :raises: ResourceNotFound if firewall policy is not found + :return: updated firewall policy + :rtype: FirewallPolicy + """ + if not filters: + filters = {} + firewall_policy = self.network.find_firewall_policy( + name_or_id, ignore_missing=False, **filters) + + firewall_rule = self.network.find_firewall_rule(rule_name_or_id) + if not firewall_rule: + # short-circuit: if firewall rule is not found, + # return current firewall policy + self.log.debug('Firewall rule %s not found for removing', + rule_name_or_id) + return firewall_policy + + if firewall_rule['id'] not in firewall_policy['firewall_rules']: + # short-circuit: if firewall rule id is not associated, + # log it to debug and return current firewall policy + self.log.debug( + 'Firewall rule %s not associated with firewall policy %s', + rule_name_or_id, name_or_id) + return firewall_policy + + return self.network.remove_rule_from_policy(firewall_policy['id'], + firewall_rule['id']) + + @_utils.valid_kwargs( + 'admin_state_up', 'description', 'egress_firewall_policy', + 'ingress_firewall_policy', 'name', 'ports', 'project_id', 'shared') + def create_firewall_group(self, **kwargs): + """ + Creates firewall group. The keys egress_firewall_policy and + ingress_firewall_policy are looked up and mapped as + egress_firewall_policy_id and ingress_firewall_policy_id respectively. + Port name or ids list is transformed to port ids list before the POST + request. + + :param bool admin_state_up: State of firewall group. + Will block all traffic if set to False. + Defaults to True. + :param description: Human-readable description. + :param egress_firewall_policy: Name or id of egress firewall policy. + :param ingress_firewall_policy: Name or id of ingress firewall policy. + :param name: Human-readable name. + :param list[str] ports: List of associated ports (name or id) + :param project_id: Project id. + :param shared: Visibility to other projects. + Defaults to False. + :raises: BadRequestException if parameters are malformed + :raises: DuplicateResource on multiple matches + :raises: ResourceNotFound if (ingress-, egress-) firewall policy or + a port is not found. + :return: created firewall group + :rtype: FirewallGroup + """ + self._lookup_ingress_egress_firewall_policy_ids(kwargs) + if 'ports' in kwargs: + kwargs['ports'] = self._get_port_ids(kwargs['ports']) + return self.network.create_firewall_group(**kwargs) + + def delete_firewall_group(self, name_or_id, filters=None): + """ + Deletes firewall group. + Prints debug message in case to-be-deleted resource was not found. + + :param name_or_id: firewall group name or id + :param dict filters: optional filters + :raises: DuplicateResource on multiple matches + :return: True if resource is successfully deleted, False otherwise. + :rtype: bool + """ + if not filters: + filters = {} + try: + firewall_group = self.network.find_firewall_group( + name_or_id, ignore_missing=False, **filters) + self.network.delete_firewall_group(firewall_group, + ignore_missing=False) + except exceptions.ResourceNotFound: + self.log.debug('Firewall group %s not found for deleting', + name_or_id) + return False + return True + + def get_firewall_group(self, name_or_id, filters=None): + """ + Retrieves firewall group. + + :param name_or_id: firewall group name or id + :param dict filters: optional filters + :raises: DuplicateResource on multiple matches + :return: firewall group or None if not found + :rtype: FirewallGroup + """ + if not filters: + filters = {} + return self.network.find_firewall_group(name_or_id, **filters) + + def list_firewall_groups(self, filters=None): + """ + Lists firewall groups. + + :param dict filters: optional filters + :return: list of firewall groups + :rtype: list[FirewallGroup] + """ + if not filters: + filters = {} + return list(self.network.firewall_groups(**filters)) + + @_utils.valid_kwargs( + 'admin_state_up', 'description', 'egress_firewall_policy', + 'ingress_firewall_policy', 'name', 'ports', 'project_id', 'shared') + def update_firewall_group(self, name_or_id, filters=None, **kwargs): + """ + Updates firewall group. + To unset egress- or ingress firewall policy, set egress_firewall_policy + or ingress_firewall_policy to None. You can also set + egress_firewall_policy_id and ingress_firewall_policy_id directly, + which will skip the policy lookups. + + :param name_or_id: firewall group name or id + :param dict filters: optional filters + :param kwargs: firewall group update parameters + See create_firewall_group docstring for valid parameters. + :raises: BadRequestException if parameters are malformed + :raises: DuplicateResource on multiple matches + :raises: ResourceNotFound if firewall group, a firewall policy + (egress, ingress) or port is not found + :return: updated firewall group + :rtype: FirewallGroup + """ + if not filters: + filters = {} + firewall_group = self.network.find_firewall_group( + name_or_id, ignore_missing=False, **filters) + self._lookup_ingress_egress_firewall_policy_ids(kwargs) + + if 'ports' in kwargs: + kwargs['ports'] = self._get_port_ids(kwargs['ports']) + return self.network.update_firewall_group(firewall_group, **kwargs) + + def _lookup_ingress_egress_firewall_policy_ids(self, firewall_group): + """ + Transforms firewall_group dict IN-PLACE. Takes the value of the keys + egress_firewall_policy and ingress_firewall_policy, looks up the + policy ids and maps them to egress_firewall_policy_id and + ingress_firewall_policy_id. Old keys which were used for the lookup + are deleted. + + :param dict firewall_group: firewall group dict + :raises: DuplicateResource on multiple matches + :raises: ResourceNotFound if a firewall policy is not found + """ + for key in ('egress_firewall_policy', 'ingress_firewall_policy'): + if key not in firewall_group: + continue + if firewall_group[key] is None: + val = None + else: + val = self.network.find_firewall_policy( + firewall_group[key], ignore_missing=False)['id'] + firewall_group[key + '_id'] = val + del firewall_group[key] + + def _get_port_ids(self, name_or_id_list, filters=None): + """ + Takes a list of port names or ids, retrieves ports and returns a list + with port ids only. + + :param list[str] name_or_id_list: list of port names or ids + :param dict filters: optional filters + :raises: SDKException on multiple matches + :raises: ResourceNotFound if a port is not found + :return: list of port ids + :rtype: list[str] + """ + ids_list = [] + for name_or_id in name_or_id_list: + port = self.get_port(name_or_id, filters) + if not port: + raise exceptions.ResourceNotFound( + 'Port {id} not found'.format(id=name_or_id)) + ids_list.append(port['id']) + return ids_list diff --git a/openstack/tests/unit/cloud/test_fwaas.py b/openstack/tests/unit/cloud/test_fwaas.py new file mode 100644 index 000000000..abfc3a498 --- /dev/null +++ b/openstack/tests/unit/cloud/test_fwaas.py @@ -0,0 +1,1150 @@ +# 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 copy import deepcopy +from mock import Mock + +from openstack import exceptions +from openstack.network.v2.firewall_group import FirewallGroup +from openstack.network.v2.firewall_rule import FirewallRule +from openstack.network.v2.firewall_policy import FirewallPolicy +from openstack.tests.unit import base + + +class FirewallTestCase(base.TestCase): + def _make_mock_url(self, *args, **params): + params_list = ['='.join([k, v]) for k, v in params.items()] + return self.get_mock_url('network', 'public', + append=['v2.0', 'fwaas'] + list(args), + qs_elements=params_list or None) + + +class TestFirewallRule(FirewallTestCase): + firewall_rule_name = 'deny_ssh' + firewall_rule_id = 'd525a9b2-ab28-493d-b988-b824c8c033b1' + _mock_firewall_rule_attrs = { + 'action': 'deny', + 'description': 'Deny SSH access', + 'destination_ip_address': None, + 'destination_port': 22, + 'enabled': True, + 'id': firewall_rule_id, + 'ip_version': 4, + 'name': firewall_rule_name, + 'project_id': 'ef44f1efcb9548d9a441cdc252a979a6', + 'protocol': 'tcp', + 'shared': False, + 'source_ip_address': None, + 'source_port': None + } + mock_firewall_rule = None + + def setUp(self, cloud_config_fixture='clouds.yaml'): + self.mock_firewall_rule = FirewallRule( + **self._mock_firewall_rule_attrs).to_dict() + super(TestFirewallRule, self).setUp() + + def test_create_firewall_rule(self): + # attributes that are passed to the tested function + passed_attrs = self._mock_firewall_rule_attrs.copy() + del passed_attrs['id'] + + self.register_uris([ + # no validate due to added location key + dict(method='POST', + uri=self._make_mock_url('firewall_rules'), + json={'firewall_rule': self.mock_firewall_rule.copy()}) + ]) + r = self.cloud.create_firewall_rule(**passed_attrs) + self.assertDictEqual(self.mock_firewall_rule, r.to_dict()) + self.assert_calls() + + def test_create_firewall_rule_bad_protocol(self): + bad_rule = self._mock_firewall_rule_attrs.copy() + del bad_rule['id'] # id not allowed + bad_rule['ip_version'] = 5 + self.register_uris([ + # no validate due to added location key + dict(method='POST', + uri=self._make_mock_url('firewall_rules'), + status_code=400, + json={}) + ]) + self.assertRaises(exceptions.BadRequestException, + self.cloud.create_firewall_rule, **bad_rule) + self.assert_calls() + + def test_delete_firewall_rule(self): + self.register_uris([ + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_rules', + self.firewall_rule_name), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_rules'), + json={'firewall_rules': [self.mock_firewall_rule]}), + dict(method='DELETE', + uri=self._make_mock_url('firewall_rules', + self.firewall_rule_id), + json={}, status_code=204) + ]) + self.assertTrue( + self.cloud.delete_firewall_rule(self.firewall_rule_name)) + self.assert_calls() + + def test_delete_firewall_rule_filters(self): + filters = {'project_id': self.mock_firewall_rule['project_id']} + self.register_uris([ + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_rules', + self.firewall_rule_name), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_rules', **filters), + json={'firewall_rules': [self.mock_firewall_rule]}, ), + dict(method='DELETE', + uri=self._make_mock_url('firewall_rules', + self.firewall_rule_id), + json={}, status_code=204), + ]) + self.assertTrue( + self.cloud.delete_firewall_rule(self.firewall_rule_name, filters)) + self.assert_calls() + + def test_delete_firewall_rule_not_found(self): + _delete = self.cloud.network.delete_firewall_rule + _log = self.cloud.log.debug + self.register_uris([ + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_rules', + self.firewall_rule_name), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_rules'), + json={'firewall_rules': []}) + ]) + self.cloud.network.delete_firewall_rule = Mock() + self.cloud.log.debug = Mock() + + self.assertFalse( + self.cloud.delete_firewall_rule(self.firewall_rule_name)) + + self.cloud.network.delete_firewall_rule.assert_not_called() + self.cloud.log.debug.assert_called_once() + + # restore methods + self.cloud.network.delete_firewall_rule = _delete + self.cloud.log.debug = _log + + def test_delete_firewall_multiple_matches(self): + self.register_uris([ + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_rules', + self.firewall_rule_name), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_rules'), + json={'firewall_rules': [self.mock_firewall_rule, + self.mock_firewall_rule]}) + ]) + self.assertRaises(exceptions.DuplicateResource, + self.cloud.delete_firewall_rule, + self.firewall_rule_name) + self.assert_calls() + + def test_get_firewall_rule(self): + self.register_uris([ + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_rules', + self.firewall_rule_name), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_rules'), + json={'firewall_rules': [self.mock_firewall_rule]}) + ]) + r = self.cloud.get_firewall_rule(self.firewall_rule_name) + self.assertDictEqual(self.mock_firewall_rule, r) + self.assert_calls() + + def test_get_firewall_rule_not_found(self): + name = 'not_found' + self.register_uris([ + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_rules', name), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_rules'), + json={'firewall_rules': []}) + ]) + self.assertIsNone(self.cloud.get_firewall_rule(name)) + self.assert_calls() + + def test_list_firewall_rules(self): + self.register_uris([ + dict(method='GET', + uri=self._make_mock_url('firewall_rules'), + json={'firewall_rules': [self.mock_firewall_rule]}) + ]) + self.assertDictEqual(self.mock_firewall_rule, + self.cloud.list_firewall_rules()[0]) + self.assert_calls() + + def test_update_firewall_rule(self): + params = {'description': 'UpdatedDescription'} + updated = self.mock_firewall_rule.copy() + updated.update(params) + self.register_uris([ + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_rules', + self.firewall_rule_name), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_rules'), + json={'firewall_rules': [self.mock_firewall_rule]}), + dict(method='PUT', + uri=self._make_mock_url('firewall_rules', + self.firewall_rule_id), + json={'firewall_rule': updated}, + validate=dict(json={'firewall_rule': params})) + ]) + self.assertDictEqual( + updated, + self.cloud.update_firewall_rule(self.firewall_rule_name, **params)) + self.assert_calls() + + def test_update_firewall_rule_filters(self): + params = {'description': 'Updated!'} + filters = {'project_id': self.mock_firewall_rule['project_id']} + updated = self.mock_firewall_rule.copy() + updated.update(params) + _find = self.cloud.network.find_firewall_rule + self.cloud.network.find_firewall_rule = Mock( + return_value=self.mock_firewall_rule) + self.register_uris([ + dict(method='PUT', + uri=self._make_mock_url('firewall_rules', + self.firewall_rule_id), + json={'firewall_rule': updated}) + ]) + self.assertDictEqual(updated, + self.cloud.update_firewall_rule( + self.firewall_rule_name, filters, **params)) + self.assert_calls() + + self.cloud.network.find_firewall_rule.assert_called_once_with( + self.firewall_rule_name, ignore_missing=False, **filters) + # restore + self.cloud.network.find_firewall_rule = _find + + +class TestFirewallPolicy(FirewallTestCase): + firewall_policy_id = '78d05d20-d406-41ec-819d-06b65c2684e4' + firewall_policy_name = 'block_popular_services' + _mock_firewall_policy_attrs = { + 'audited': True, + 'description': 'block ports of well-known services', + 'firewall_rules': ['deny_ssh'], + 'id': firewall_policy_id, + 'name': firewall_policy_name, + 'project_id': 'b64238cb-a25d-41af-9ee1-42deb4587d20', + 'shared': False + } + mock_firewall_policy = None + + def setUp(self, cloud_config_fixture='clouds.yaml'): + self.mock_firewall_policy = FirewallPolicy( + **self._mock_firewall_policy_attrs).to_dict() + super(TestFirewallPolicy, self).setUp() + + def test_create_firewall_policy(self): + # attributes that are passed to the tested method + passed_attrs = deepcopy(self._mock_firewall_policy_attrs) + del passed_attrs['id'] + + # policy that is returned by the POST request + created_attrs = deepcopy(self._mock_firewall_policy_attrs) + created_attrs['firewall_rules'][0] = TestFirewallRule.firewall_rule_id + created_policy = FirewallPolicy(**created_attrs) + + # attributes used to validate the request inside register_uris() + validate_attrs = deepcopy(created_attrs) + del validate_attrs['id'] + + self.register_uris([ + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_rules', + TestFirewallRule.firewall_rule_name), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_rules'), + json={'firewall_rules': [ + TestFirewallRule._mock_firewall_rule_attrs]}), + dict(method='POST', + uri=self._make_mock_url('firewall_policies'), + json={'firewall_policy': created_attrs}, + validate=dict( + json={'firewall_policy': validate_attrs})) + ]) + res = self.cloud.create_firewall_policy(**passed_attrs) + self.assertDictEqual(created_policy, res.to_dict()) + self.assert_calls() + + def test_create_firewall_policy_rule_not_found(self): + posted_policy = deepcopy(self._mock_firewall_policy_attrs) + del posted_policy['id'] + _create = self.cloud.network.create_firewall_policy + self.cloud.network.create_firewall_policy = Mock() + self.register_uris([ + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_rules', + posted_policy['firewall_rules'][0]), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_rules'), + json={'firewall_rules': []}) + ]) + self.assertRaises(exceptions.ResourceNotFound, + self.cloud.create_firewall_policy, **posted_policy) + self.cloud.network.create_firewall_policy.assert_not_called() + self.assert_calls() + # restore + self.cloud.network.create_firewall_policy = _create + + def test_delete_firewall_policy(self): + _log = self.cloud.log.debug + self.cloud.log.debug = Mock() + self.register_uris([ + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_policies', + self.firewall_policy_name), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_policies'), + json={'firewall_policies': [self.mock_firewall_policy]}), + dict(method='DELETE', + uri=self._make_mock_url('firewall_policies', + self.firewall_policy_id), + json={}, status_code=204) + ]) + self.assertTrue( + self.cloud.delete_firewall_policy(self.firewall_policy_name)) + self.assert_calls() + + self.cloud.log.debug.assert_not_called() + # restore + self.cloud.log.debug = _log + + def test_delete_firewall_policy_filters(self): + filters = {'project_id': self.mock_firewall_policy['project_id']} + _find = self.cloud.network.find_firewall_policy + _log = self.cloud.log.debug + self.cloud.log.debug = Mock() + self.cloud.network.find_firewall_policy = Mock( + return_value=self.mock_firewall_policy) + self.register_uris([ + dict(method='DELETE', + uri=self._make_mock_url('firewall_policies', + self.firewall_policy_id), + json={}, status_code=204) + ]) + self.assertTrue( + self.cloud.delete_firewall_policy(self.firewall_policy_name, + filters)) + self.assert_calls() + self.cloud.network.find_firewall_policy.assert_called_once_with( + self.firewall_policy_name, ignore_missing=False, **filters) + self.cloud.log.debug.assert_not_called() + # restore + self.cloud.network.find_firewall_policy = _find + self.cloud.log.debug = _log + + def test_delete_firewall_policy_not_found(self): + self.register_uris([ + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_policies', + self.firewall_policy_name), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_policies'), + json={'firewall_policies': []}) + ]) + _log = self.cloud.log.debug + self.cloud.log.debug = Mock() + self.assertFalse( + self.cloud.delete_firewall_policy(self.firewall_policy_name)) + self.assert_calls() + self.cloud.log.debug.assert_called_once() + # restore + self.cloud.log.debug = _log + + def test_get_firewall_policy(self): + self.register_uris([ + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_policies', + self.firewall_policy_name), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_policies'), + json={'firewall_policies': [self.mock_firewall_policy]}) + ]) + self.assertDictEqual(self.mock_firewall_policy, + self.cloud.get_firewall_policy( + self.firewall_policy_name)) + self.assert_calls() + + def test_get_firewall_policy_not_found(self): + name = 'not_found' + self.register_uris([ + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_policies', name), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_policies'), + json={'firewall_policies': []}) + ]) + self.assertIsNone(self.cloud.get_firewall_policy(name)) + self.assert_calls() + + def test_list_firewall_policies(self): + self.register_uris([ + dict(method='GET', + uri=self._make_mock_url('firewall_policies'), + json={'firewall_policies': [ + self.mock_firewall_policy.copy(), + self.mock_firewall_policy.copy()]}) + ]) + policy = FirewallPolicy(**self.mock_firewall_policy) + self.assertListEqual(self.cloud.list_firewall_policies(), + [policy, policy]) + self.assert_calls() + + def test_list_firewall_policies_filters(self): + filters = {'project_id': self.mock_firewall_policy['project_id']} + self.register_uris([ + dict(method='GET', + uri=self._make_mock_url('firewall_policies', **filters), + json={'firewall_policies': [ + self.mock_firewall_policy]}) + ]) + self.assertListEqual(self.cloud.list_firewall_policies(filters), + [FirewallPolicy(**self.mock_firewall_policy)]) + self.assert_calls() + + def test_update_firewall_policy(self): + lookup_rule = FirewallRule( + **TestFirewallRule._mock_firewall_rule_attrs).to_dict() + params = {'firewall_rules': [lookup_rule['id']], + 'description': 'updated!'} + retrieved_policy = deepcopy(self.mock_firewall_policy) + del retrieved_policy['firewall_rules'][0] + updated_policy = deepcopy(self.mock_firewall_policy) + updated_policy.update(params) + self.register_uris([ + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_policies', + self.firewall_policy_name), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_policies'), + json={'firewall_policies': [retrieved_policy]}), + dict(method='GET', + uri=self._make_mock_url('firewall_rules', lookup_rule['id']), + json={'firewall_rule': lookup_rule}), + dict(method='PUT', + uri=self._make_mock_url('firewall_policies', + self.firewall_policy_id), + json={'firewall_policy': updated_policy}, + validate=dict(json={'firewall_policy': params})) + ]) + self.assertDictEqual(updated_policy, + self.cloud.update_firewall_policy( + self.firewall_policy_name, **params)) + self.assert_calls() + + def test_update_firewall_policy_no_rules(self): + params = {'description': 'updated!'} + updated_policy = deepcopy(self.mock_firewall_policy) + updated_policy.update(params) + self.register_uris([ + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_policies', + self.firewall_policy_name), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_policies'), + json={'firewall_policies': [ + deepcopy(self.mock_firewall_policy)]}), + dict(method='PUT', + uri=self._make_mock_url('firewall_policies', + self.firewall_policy_id), + json={'firewall_policy': updated_policy}, + validate=dict(json={'firewall_policy': params})), + ]) + self.assertDictEqual(updated_policy, + self.cloud.update_firewall_policy( + self.firewall_policy_name, **params)) + self.assert_calls() + + def test_update_firewall_policy_filters(self): + filters = {'project_id': self.mock_firewall_policy['project_id']} + params = {'description': 'updated!'} + updated_policy = deepcopy(self.mock_firewall_policy) + updated_policy.update(params) + + _find = self.cloud.network.find_firewall_policy + self.cloud.network.find_firewall_policy = Mock( + return_value=deepcopy(self.mock_firewall_policy)) + + self.register_uris([ + dict(method='PUT', + uri=self._make_mock_url('firewall_policies', + self.firewall_policy_id), + json={'firewall_policy': updated_policy}, + validate=dict(json={'firewall_policy': params})), + ]) + self.assertDictEqual(updated_policy, + self.cloud.update_firewall_policy( + self.firewall_policy_name, filters, **params)) + self.assert_calls() + self.cloud.network.find_firewall_policy.assert_called_once_with( + self.firewall_policy_name, ignore_missing=False, **filters) + # restore + self.cloud.network.find_firewall_policy = _find + + def test_insert_rule_into_policy(self): + rule0 = FirewallRule(**TestFirewallRule._mock_firewall_rule_attrs) + + _rule1_attrs = deepcopy( + TestFirewallRule._mock_firewall_rule_attrs) + _rule1_attrs.update(id='8068fc06-0e72-43f2-a76f-a51a33b46e08', + name='after_rule') + rule1 = FirewallRule(**_rule1_attrs) + + _rule2_attrs = deepcopy(TestFirewallRule._mock_firewall_rule_attrs) + _rule2_attrs.update(id='c716382d-183b-475d-b500-dcc762f45ce3', + name='before_rule') + rule2 = FirewallRule(**_rule2_attrs) + retrieved_policy = deepcopy(self.mock_firewall_policy) + retrieved_policy['firewall_rules'] = [rule1['id'], rule2['id']] + updated_policy = deepcopy(self.mock_firewall_policy) + updated_policy['firewall_rules'] = [rule0['id'], rule1['id'], + rule2['id']] + self.register_uris([ + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_policies', + self.firewall_policy_name), + status_code=404), + dict(method='GET', # get policy + uri=self._make_mock_url('firewall_policies'), + json={'firewall_policies': [retrieved_policy]}), + + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_rules', rule0['name']), + status_code=404), + dict(method='GET', # get rule to add + uri=self._make_mock_url('firewall_rules'), + json={'firewall_rules': [rule0]}), + + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_rules', rule1['name']), + status_code=404), + dict(method='GET', # get after rule + uri=self._make_mock_url('firewall_rules'), + json={'firewall_rules': [rule1]}), + + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_rules', rule2['name']), + status_code=404), + dict(method='GET', # get before rule + uri=self._make_mock_url('firewall_rules'), + json={'firewall_rules': [rule2]}), + + dict(method='PUT', # add rule + uri=self._make_mock_url('firewall_policies', + self.firewall_policy_id, + 'insert_rule'), + json=updated_policy, + validate=dict(json={'firewall_rule_id': rule0['id'], + 'insert_after': rule1['id'], + 'insert_before': rule2['id']})), + ]) + r = self.cloud.insert_rule_into_policy( + name_or_id=self.firewall_policy_name, + rule_name_or_id=rule0['name'], + insert_after=rule1['name'], + insert_before=rule2['name']) + self.assertDictEqual(updated_policy, r.to_dict()) + self.assert_calls() + + def test_insert_rule_into_policy_compact(self): + """ + Tests without insert_after and insert_before + """ + rule = FirewallRule(**TestFirewallRule._mock_firewall_rule_attrs) + retrieved_policy = deepcopy(self.mock_firewall_policy) + retrieved_policy['firewall_rules'] = [] + updated_policy = deepcopy(retrieved_policy) + updated_policy['firewall_rules'].append(rule['id']) + self.register_uris([ + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_policies', + self.firewall_policy_name), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_policies'), + json={'firewall_policies': [retrieved_policy]}), + + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_rules', rule['name']), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_rules'), + json={'firewall_rules': [rule]}), + + dict(method='PUT', + uri=self._make_mock_url('firewall_policies', + retrieved_policy['id'], + 'insert_rule'), + json=updated_policy, + validate=dict(json={'firewall_rule_id': rule['id'], + 'insert_after': None, + 'insert_before': None})) + ]) + r = self.cloud.insert_rule_into_policy(self.firewall_policy_name, + rule['name']) + self.assertDictEqual(updated_policy, r.to_dict()) + self.assert_calls() + + def test_insert_rule_into_policy_not_found(self): + policy_name = 'bogus_policy' + _find_rule = self.cloud.network.find_firewall_rule + self.cloud.network.find_firewall_rule = Mock() + self.register_uris([ + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_policies', policy_name), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_policies'), + json={'firewall_policies': []}) + ]) + self.assertRaises(exceptions.ResourceNotFound, + self.cloud.insert_rule_into_policy, + policy_name, 'bogus_rule') + self.assert_calls() + self.cloud.network.find_firewall_rule.assert_not_called() + # restore + self.cloud.network.find_firewall_rule = _find_rule + + def test_insert_rule_into_policy_rule_not_found(self): + rule_name = 'unknown_rule' + self.register_uris([ + dict(method='GET', + uri=self._make_mock_url('firewall_policies', + self.firewall_policy_id), + json={'firewall_policy': self.mock_firewall_policy}), + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_rules', rule_name), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_rules'), + json={'firewall_rules': []}) + ]) + self.assertRaises(exceptions.ResourceNotFound, + self.cloud.insert_rule_into_policy, + self.firewall_policy_id, rule_name) + self.assert_calls() + + def test_insert_rule_into_policy_already_associated(self): + _log = self.cloud.log.debug + self.cloud.log.debug = Mock() + rule = FirewallRule( + **TestFirewallRule._mock_firewall_rule_attrs).to_dict() + policy = deepcopy(self.mock_firewall_policy) + policy['firewall_rules'] = [rule['id']] + self.register_uris([ + dict(method='GET', + uri=self._make_mock_url('firewall_policies', + self.firewall_policy_id), + json={'firewall_policy': policy}), + dict(method='GET', + uri=self._make_mock_url('firewall_rules', rule['id']), + json={'firewall_rule': rule}) + ]) + r = self.cloud.insert_rule_into_policy(policy['id'], rule['id']) + self.assertDictEqual(policy, r.to_dict()) + self.assert_calls() + self.cloud.log.debug.assert_called() + # restore + self.cloud.log.debug = _log + + def test_remove_rule_from_policy(self): + policy_name = self.firewall_policy_name + rule = FirewallRule(**TestFirewallRule._mock_firewall_rule_attrs) + + retrieved_policy = deepcopy(self.mock_firewall_policy) + retrieved_policy['firewall_rules'][0] = rule['id'] + + updated_policy = deepcopy(self.mock_firewall_policy) + del updated_policy['firewall_rules'][0] + self.register_uris([ + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_policies', policy_name), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_policies'), + json={'firewall_policies': [retrieved_policy]}), + + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_rules', rule['name']), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_rules'), + json={'firewall_rules': [rule]}), + + dict(method='PUT', + uri=self._make_mock_url('firewall_policies', + self.firewall_policy_id, + 'remove_rule'), + json=updated_policy, + validate=dict(json={'firewall_rule_id': rule['id']})) + ]) + r = self.cloud.remove_rule_from_policy(policy_name, rule['name']) + self.assertDictEqual(updated_policy, r.to_dict()) + self.assert_calls() + + def test_remove_rule_from_policy_not_found(self): + _find_rule = self.cloud.network.find_firewall_rule + self.cloud.network.find_firewall_rule = Mock() + self.register_uris([ + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_policies', + self.firewall_policy_name), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_policies'), + json={'firewall_policies': []}) + ]) + self.assertRaises(exceptions.ResourceNotFound, + self.cloud.remove_rule_from_policy, + self.firewall_policy_name, + TestFirewallRule.firewall_rule_name) + self.assert_calls() + self.cloud.network.find_firewall_rule.assert_not_called() + # restore + self.cloud.network.find_firewall_rule = _find_rule + + def test_remove_rule_from_policy_rule_not_found(self): + retrieved_policy = deepcopy(self.mock_firewall_policy) + rule = FirewallRule(**TestFirewallRule._mock_firewall_rule_attrs) + retrieved_policy['firewall_rules'][0] = rule['id'] + self.register_uris([ + dict(method='GET', + uri=self._make_mock_url('firewall_policies', + self.firewall_policy_id), + json={'firewall_policy': retrieved_policy}), + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_rules', + rule['name']), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_rules'), + json={'firewall_rules': []}) + ]) + r = self.cloud.remove_rule_from_policy(self.firewall_policy_id, + rule['name']) + self.assertDictEqual(retrieved_policy, r.to_dict()) + self.assert_calls() + + def test_remove_rule_from_policy_not_associated(self): + rule = FirewallRule( + **TestFirewallRule._mock_firewall_rule_attrs).to_dict() + policy = deepcopy(self.mock_firewall_policy) + del policy['firewall_rules'][0] + + _log = self.cloud.log.debug + _remove = self.cloud.network.remove_rule_from_policy + self.cloud.log.debug = Mock() + self.cloud.network.remove_rule_from_policy = Mock() + self.register_uris([ + dict(method='GET', + uri=self._make_mock_url('firewall_policies', policy['id']), + json={'firewall_policy': policy}), + dict(method='GET', + uri=self._make_mock_url('firewall_rules', rule['id']), + json={'firewall_rule': rule}) + ]) + r = self.cloud.remove_rule_from_policy(policy['id'], rule['id']) + self.assertDictEqual(policy, r.to_dict()) + self.assert_calls() + self.cloud.log.debug.assert_called_once() + self.cloud.network.remove_rule_from_policy.assert_not_called() + # restore + self.cloud.log.debug = _log + self.cloud.network.remove_rule_from_policy = _remove + + +class TestFirewallGroup(FirewallTestCase): + firewall_group_id = '700eed7a-b979-4b80-a06d-14f000d0f645' + firewall_group_name = 'max_security_group' + mock_port = { + 'name': 'mock_port', + 'id': '7d90977c-45ec-467e-a16d-dcaed772a161' + } + _mock_egress_policy_attrs = { + 'id': '34335e5b-44af-4ffd-9dcf-518133f897c7', + 'name': 'safe_outgoing_data' + } + _mock_ingress_policy_attrs = { + 'id': 'cd28fb50-85d0-4f36-89af-50fac08ac174', + 'name': 'bad_incoming_data' + } + _mock_firewall_group_attrs = { + 'admin_state_up': True, + 'description': 'Providing max security!', + 'egress_firewall_policy': _mock_egress_policy_attrs['name'], + 'ingress_firewall_policy': _mock_ingress_policy_attrs['name'], + 'id': firewall_group_id, + 'name': firewall_group_name, + 'ports': [mock_port['name']], + 'project_id': 'da347b09-0b4f-4994-a3ef-05d13eaecb2c', + 'shared': False + } + _mock_returned_firewall_group_attrs = { + 'admin_state_up': True, + 'description': 'Providing max security!', + 'egress_firewall_policy_id': _mock_egress_policy_attrs['id'], + 'ingress_firewall_policy_id': _mock_ingress_policy_attrs['id'], + 'id': firewall_group_id, + 'name': firewall_group_name, + 'ports': [mock_port['id']], + 'project_id': 'da347b09-0b4f-4994-a3ef-05d13eaecb2c', + 'shared': False + } + mock_egress_policy = None + mock_ingress_policy = None + mock_firewall_rule = None + mock_returned_firewall_rule = None + + def setUp(self, cloud_config_fixture='clouds.yaml'): + self.mock_egress_policy = FirewallPolicy( + **self._mock_egress_policy_attrs).to_dict() + self.mock_ingress_policy = FirewallPolicy( + **self._mock_ingress_policy_attrs).to_dict() + self.mock_firewall_group = FirewallGroup( + **self._mock_firewall_group_attrs).to_dict() + self.mock_returned_firewall_group = FirewallGroup( + **self._mock_returned_firewall_group_attrs).to_dict() + super(TestFirewallGroup, self).setUp() + + def test_create_firewall_group(self): + create_group_attrs = self._mock_firewall_group_attrs.copy() + del create_group_attrs['id'] + posted_group_attrs = self._mock_returned_firewall_group_attrs.copy() + del posted_group_attrs['id'] + self.register_uris([ + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_policies', + self.mock_egress_policy['name']), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_policies'), + json={'firewall_policies': [self.mock_egress_policy]}), + + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_policies', + self.mock_ingress_policy['name']), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_policies'), + json={'firewall_policies': [self.mock_ingress_policy]}), + + dict(method='GET', + uri=self.get_mock_url('network', 'public', + append=['v2.0', 'ports.json']), + json={'ports': [self.mock_port]}), + dict(method='POST', + uri=self._make_mock_url('firewall_groups'), + json={'firewall_group': deepcopy( + self.mock_returned_firewall_group)}, + validate=dict(json={'firewall_group': posted_group_attrs})) + ]) + r = self.cloud.create_firewall_group(**create_group_attrs) + self.assertDictEqual(self.mock_returned_firewall_group, r.to_dict()) + self.assert_calls() + + def test_create_firewall_group_compact(self): + """ + Tests firewall group creation without policies or ports + """ + firewall_group = deepcopy(self._mock_firewall_group_attrs) + del firewall_group['ports'] + del firewall_group['egress_firewall_policy'] + del firewall_group['ingress_firewall_policy'] + created_firewall = deepcopy(firewall_group) + created_firewall.update(egress_firewall_policy_id=None, + ingress_firewall_policy_id=None, + ports=[]) + del firewall_group['id'] + self.register_uris([ + dict(method='POST', + uri=self._make_mock_url('firewall_groups'), + json={'firewall_group': created_firewall}, + validate=dict(json={'firewall_group': firewall_group})) + ]) + r = self.cloud.create_firewall_group(**firewall_group) + self.assertDictEqual( + FirewallGroup(**created_firewall).to_dict(), r.to_dict()) + self.assert_calls() + + def test_delete_firewall_group(self): + self.register_uris([ + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_groups', + self.firewall_group_name), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_groups'), + json={'firewall_groups': [ + deepcopy(self.mock_returned_firewall_group)]}), + dict(method='DELETE', + uri=self._make_mock_url('firewall_groups', + self.firewall_group_id), + status_code=204) + ]) + self.assertTrue( + self.cloud.delete_firewall_group(self.firewall_group_name)) + self.assert_calls() + + def test_delete_firewall_group_filters(self): + filters = {'project_id': self.mock_firewall_group['project_id']} + _find = self.cloud.network.find_firewall_group + self.cloud.network.find_firewall_group = Mock( + return_value=deepcopy(self.mock_firewall_group)) + self.register_uris([ + dict(method='DELETE', + uri=self._make_mock_url('firewall_groups', + self.firewall_group_id), + status_code=204) + ]) + self.assertTrue( + self.cloud.delete_firewall_group(self.firewall_group_name, + filters)) + self.assert_calls() + + self.cloud.network.find_firewall_group.assert_called_once_with( + self.firewall_group_name, ignore_missing=False, **filters) + # restore + self.cloud.network.find_firewall_group = _find + + def test_delete_firewall_group_not_found(self): + self.register_uris([ + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_groups', + self.firewall_group_name), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_groups'), + json={'firewall_groups': []}) + ]) + _log = self.cloud.log.debug + self.cloud.log.debug = Mock() + self.assertFalse( + self.cloud.delete_firewall_group(self.firewall_group_name)) + self.assert_calls() + self.cloud.log.debug.assert_called_once() + # restore + self.cloud.log.debug = _log + + def test_get_firewall_group(self): + returned_group = deepcopy(self.mock_returned_firewall_group) + self.register_uris([ + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_groups', + self.firewall_group_name), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_groups'), + json={'firewall_groups': [returned_group]}) + ]) + self.assertDictEqual( + returned_group, + self.cloud.get_firewall_group(self.firewall_group_name)) + self.assert_calls() + + def test_get_firewall_group_not_found(self): + name = 'not_found' + self.register_uris([ + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_groups', name), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_groups'), + json={'firewall_groups': []}) + ]) + self.assertIsNone(self.cloud.get_firewall_group(name)) + self.assert_calls() + + def test_get_firewall_group_by_id(self): + returned_group = deepcopy(self.mock_returned_firewall_group) + self.register_uris([ + dict(method='GET', + uri=self._make_mock_url('firewall_groups', + self.firewall_group_id), + json={'firewall_group': returned_group})]) + r = self.cloud.get_firewall_group(self.firewall_group_id) + self.assertDictEqual(returned_group, r.to_dict()) + self.assert_calls() + + def test_list_firewall_groups(self): + returned_attrs = deepcopy(self.mock_returned_firewall_group) + self.register_uris([ + dict(method='GET', + uri=self._make_mock_url('firewall_groups'), + json={'firewall_groups': [returned_attrs, returned_attrs]}) + ]) + group = FirewallGroup(**returned_attrs) + self.assertListEqual([group, group], self.cloud.list_firewall_groups()) + self.assert_calls() + + def test_update_firewall_group(self): + params = { + 'description': 'updated!', + 'egress_firewall_policy': self.mock_egress_policy['name'], + 'ingress_firewall_policy': self.mock_ingress_policy['name'], + 'ports': [self.mock_port['name']] + } + updated_group = deepcopy(self.mock_returned_firewall_group) + updated_group['description'] = params['description'] + + returned_group = deepcopy(self.mock_returned_firewall_group) + # unset attributes that will be updated! + returned_group.update( + ingress_firewall_policy_id=None, + egress_firewall_policy_id=None, + ports=[]) + self.register_uris([ + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_groups', + self.firewall_group_name), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_groups'), + json={'firewall_groups': [returned_group]}), + + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_policies', + self.mock_egress_policy['name']), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_policies'), + json={'firewall_policies': [ + deepcopy(self.mock_egress_policy)]}), + + dict(method='GET', # short-circuit + uri=self._make_mock_url('firewall_policies', + self.mock_ingress_policy['name']), + status_code=404), + dict(method='GET', + uri=self._make_mock_url('firewall_policies'), + json={'firewall_policies': [ + deepcopy(self.mock_ingress_policy)]}), + + dict(method='GET', + uri=self.get_mock_url('network', 'public', + append=['v2.0', 'ports.json']), + json={'ports': [self.mock_port]}), + dict(method='PUT', + uri=self._make_mock_url('firewall_groups', + self.firewall_group_id), + json={'firewall_group': updated_group}, + validate=dict(json={'firewall_group': { + 'description': params['description'], + 'egress_firewall_policy_id': + self.mock_egress_policy['id'], + 'ingress_firewall_policy_id': + self.mock_ingress_policy['id'], + 'ports': [self.mock_port['id']] + }})) + ]) + self.assertDictEqual(updated_group, + self.cloud.update_firewall_group( + self.firewall_group_name, **params)) + self.assert_calls() + + def test_update_firewall_group_compact(self): + params = {'description': 'updated again!'} + updated_group = deepcopy(self.mock_returned_firewall_group) + updated_group.update(params) + + self.register_uris([ + dict(method='GET', + uri=self._make_mock_url('firewall_groups', + self.firewall_group_id), + json={'firewall_group': deepcopy( + self.mock_returned_firewall_group)}), + dict(method='PUT', + uri=self._make_mock_url('firewall_groups', + self.firewall_group_id), + json={'firewall_group': updated_group}, + validate=dict(json={'firewall_group': params})) + ]) + self.assertDictEqual( + updated_group, + self.cloud.update_firewall_group(self.firewall_group_id, **params)) + self.assert_calls() + + def test_update_firewall_group_filters(self): + filters = {'project_id': self.mock_firewall_group['project_id']} + _find = self.cloud.network.find_firewall_group + self.cloud.network.find_firewall_group = Mock( + return_value=deepcopy(self.mock_firewall_group)) + params = {'description': 'updated again!'} + updated_group = deepcopy(self.mock_returned_firewall_group) + self.register_uris([ + dict(method='PUT', + uri=self._make_mock_url('firewall_groups', + self.firewall_group_id), + json={'firewall_group': updated_group}, + validate=dict(json={'firewall_group': params})) + ]) + r = self.cloud.update_firewall_group(self.firewall_group_name, filters, + **params) + self.assertDictEqual(updated_group, r.to_dict()) + self.assert_calls() + + self.cloud.network.find_firewall_group.assert_called_once_with( + self.firewall_group_name, ignore_missing=False, **filters) + # restore + self.cloud.network.find_firewall_group = _find + + def test_update_firewall_group_unset_policies(self): + transformed_params = {'ingress_firewall_policy_id': None, + 'egress_firewall_policy_id': None} + updated_group = deepcopy(self.mock_returned_firewall_group) + updated_group.update(**transformed_params) + returned_group = deepcopy(self.mock_returned_firewall_group) + self.register_uris([ + dict(method='GET', + uri=self._make_mock_url('firewall_groups', + self.firewall_group_id), + json={'firewall_group': returned_group}), + dict(method='PUT', + uri=self._make_mock_url('firewall_groups', + self.firewall_group_id), + json={'firewall_group': updated_group}, + validate=dict(json={'firewall_group': transformed_params})) + ]) + self.assertDictEqual(updated_group, + self.cloud.update_firewall_group( + self.firewall_group_id, + ingress_firewall_policy=None, + egress_firewall_policy=None)) + self.assert_calls() From 5a4845cbfbd63025b84c86a512446c0d80770442 Mon Sep 17 00:00:00 2001 From: wangxiyuan Date: Mon, 15 Oct 2018 11:04:41 +0800 Subject: [PATCH 2228/3836] Remove duplicate code is_impersonation in trust is duplicate defined. Change-Id: Ie2bd41b54672d723d2721c6cca46fc5ad630aa2c --- openstack/identity/v3/trust.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openstack/identity/v3/trust.py b/openstack/identity/v3/trust.py index 66aaa4422..dfc5f8279 100644 --- a/openstack/identity/v3/trust.py +++ b/openstack/identity/v3/trust.py @@ -32,9 +32,6 @@ class Trust(resource.Resource): #: A boolean indicating whether the trust can be issued by the trustee as #: a regulart trust. Default is ``False``. allow_redelegation = resource.Body('allow_redelegation', type=bool) - #: If ``impersonation`` is set to ``False``, then the token's ``user`` - #: attribute will represent that of the trustee. *Type: bool* - is_impersonation = resource.Body('impersonation', type=bool) #: Specifies the expiration time of the trust. A trust may be revoked #: ahead of expiration. If the value represents a time in the past, #: the trust is deactivated. From d75056a8be8cbf5a51b4a2e59704ace93e7e2d1e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 23 Sep 2018 10:01:49 -0500 Subject: [PATCH 2229/3836] Use network proxy in openstack.cloud Stop creating our own client in the shade layer and use the sdk adapter. Change-Id: If5f8b55a13a61b954f16712504aa8c34a66aeab1 --- openstack/cloud/openstackcloud.py | 314 ++++++++++++++++-------------- openstack/service_description.py | 1 + 2 files changed, 172 insertions(+), 143 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 831f79a46..f9e7c20e0 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -33,6 +33,7 @@ import dogpile.cache import munch +import requests.models import requestsexceptions from six.moves import urllib @@ -41,6 +42,7 @@ from openstack import _adapter from openstack import _log +from openstack import exceptions from openstack.cloud import exc from openstack.cloud._heat import event_utils from openstack.cloud._heat import template_utils @@ -586,23 +588,6 @@ def _image_client(self): 'image', min_version=1, max_version='2.latest') return self._raw_clients['image'] - @property - def _network_client(self): - if 'network' not in self._raw_clients: - client = self._get_raw_client('network') - # TODO(mordred) Replace this with self.network - # Don't bother with version discovery - there is only one version - # of neutron. This is what neutronclient does, fwiw. - endpoint = client.get_endpoint() - if not endpoint.rstrip().rsplit('/')[1] == 'v2.0': - if not endpoint.endswith('/'): - endpoint += '/' - endpoint = urllib.parse.urljoin( - endpoint, 'v2.0') - client.endpoint_override = endpoint - self._raw_clients['network'] = client - return self._raw_clients['network'] - @property def _object_store_client(self): if 'object-store' not in self._raw_clients: @@ -825,6 +810,8 @@ def _get_and_munchify(self, key, data): overriding the meta module making the call to meta.get_and_munchify to fail. """ + if isinstance(data, requests.models.Response): + data = _adapter._json_response(data) return meta.get_and_munchify(key, data) @_utils.cache_on_arguments() @@ -1456,8 +1443,9 @@ def search_keypairs(self, name_or_id=None, filters=None): @_utils.cache_on_arguments() def _neutron_extensions(self): extensions = set() - data = self._network_client.get( - '/extensions.json', + resp = self.network.get('/extensions.json') + data = _adapter._json_response( + resp, error_message="Error fetching extension list for neutron") for extension in self._get_and_munchify('extensions', data): extensions.add(extension['alias']) @@ -1670,7 +1658,7 @@ def list_networks(self, filters=None): # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} - data = self._network_client.get("/networks.json", params=filters) + data = self.network.get("/networks.json", params=filters) return self._get_and_munchify('networks', data) def list_routers(self, filters=None): @@ -1683,8 +1671,9 @@ def list_routers(self, filters=None): # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} - data = self._network_client.get( - "/routers.json", params=filters, + resp = self.network.get("/routers.json", params=filters) + data = _adapter._json_response( + resp, error_message="Error fetching router list") return self._get_and_munchify('routers', data) @@ -1698,7 +1687,7 @@ def list_subnets(self, filters=None): # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} - data = self._network_client.get("/subnets.json", params=filters) + data = self.network.get("/subnets.json", params=filters) return self._get_and_munchify('subnets', data) def list_ports(self, filters=None): @@ -1734,8 +1723,9 @@ def list_ports(self, filters=None): return self._ports def _list_ports(self, filters): - data = self._network_client.get( - "/ports.json", params=filters, + resp = self.network.get("/ports.json", params=filters) + data = _adapter._json_response( + resp, error_message="Error fetching port list") return self._get_and_munchify('ports', data) @@ -1753,8 +1743,9 @@ def list_qos_rule_types(self, filters=None): # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} - data = self._network_client.get( - "/qos/rule-types.json", params=filters, + resp = self.network.get("/qos/rule-types.json", params=filters) + data = _adapter._json_response( + resp, error_message="Error fetching QoS rule types list") return self._get_and_munchify('rule_types', data) @@ -1776,8 +1767,10 @@ def get_qos_rule_type_details(self, rule_type, filters=None): 'qos-rule-type-details extension is not available ' 'on target cloud') - data = self._network_client.get( - "/qos/rule-types/{rule_type}.json".format(rule_type=rule_type), + resp = self.network.get( + "/qos/rule-types/{rule_type}.json".format(rule_type=rule_type)) + data = _adapter._json_response( + resp, error_message="Error fetching QoS details of {rule_type} " "rule type".format(rule_type=rule_type)) return self._get_and_munchify('rule_type', data) @@ -1795,8 +1788,9 @@ def list_qos_policies(self, filters=None): # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} - data = self._network_client.get( - "/qos/policies.json", params=filters, + resp = self.network.get("/qos/policies.json", params=filters) + data = _adapter._json_response( + resp, error_message="Error fetching QoS policies list") return self._get_and_munchify('policies', data) @@ -2075,8 +2069,9 @@ def list_security_groups(self, filters=None): # Handle neutron security groups if self._use_neutron_secgroups(): # Neutron returns dicts, so no need to convert objects here. - data = self._network_client.get( - '/security-groups.json', params=filters, + resp = self.network.get('/security-groups.json', params=filters) + data = _adapter._json_response( + resp, error_message="Error fetching security group list") return self._normalize_secgroups( self._get_and_munchify('security_groups', data)) @@ -2342,7 +2337,7 @@ def list_floating_ips(self, filters=None): def _neutron_list_floating_ips(self, filters=None): if not filters: filters = {} - data = self._network_client.get('/floatingips.json', params=filters) + data = self.network.get('/floatingips.json', params=filters) return self._get_and_munchify('floatingips', data) def _nova_list_floating_ips(self): @@ -2748,8 +2743,9 @@ def get_network_by_id(self, id): :param id: ID of the network. :returns: A network ``munch.Munch``. """ - data = self._network_client.get( - '/networks/{id}'.format(id=id), + resp = self.network.get('/networks/{id}'.format(id=id)) + data = _adapter._json_response( + resp, error_message="Error getting network with ID {id}".format(id=id) ) network = self._get_and_munchify('network', data) @@ -2808,8 +2804,9 @@ def get_subnet_by_id(self, id): :param id: ID of the subnet. :returns: A subnet ``munch.Munch``. """ - data = self._network_client.get( - '/subnets/{id}'.format(id=id), + resp = self.network.get('/subnets/{id}'.format(id=id)) + data = _adapter._json_response( + resp, error_message="Error getting subnet with ID {id}".format(id=id) ) subnet = self._get_and_munchify('subnet', data) @@ -2846,8 +2843,9 @@ def get_port_by_id(self, id): :param id: ID of the port. :returns: A port ``munch.Munch``. """ - data = self._network_client.get( - '/ports/{id}'.format(id=id), + resp = self.network.get('/ports/{id}'.format(id=id)) + data = _adapter._json_response( + resp, error_message="Error getting port with ID {id}".format(id=id) ) port = self._get_and_munchify('port', data) @@ -3048,9 +3046,8 @@ def get_security_group_by_id(self, id): error_message = ("Error getting security group with" " ID {id}".format(id=id)) if self._use_neutron_secgroups(): - data = self._network_client.get( - '/security-groups/{id}'.format(id=id), - error_message=error_message) + resp = self.network.get('/security-groups/{id}'.format(id=id)) + data = _adapter._json_response(resp, error_message=error_message) else: data = _adapter._json_response( self.compute.get( @@ -3292,8 +3289,8 @@ def get_floating_ip_by_id(self, id): error_message = "Error getting floating ip with ID {id}".format(id=id) if self._use_neutron_floating(): - data = self._network_client.get( - '/floatingips/{id}'.format(id=id), + data = _adapter._json_response( + self.network.get('/floatingips/{id}'.format(id=id)), error_message=error_message) return self._normalize_floating_ip( self._get_and_munchify('floatingip', data)) @@ -3458,8 +3455,7 @@ def create_network(self, name, shared=False, admin_state_up=True, network['mtu'] = mtu_size - data = self._network_client.post("/networks.json", - json={'network': network}) + data = self.network.post("/networks.json", json={'network': network}) # Reset cache so the new network is picked up self._reset_network_caches() @@ -3519,9 +3515,9 @@ def update_network(self, name_or_id, **kwargs): raise exc.OpenStackCloudException( "Network %s not found." % name_or_id) - data = self._network_client.put( + data = _adapter._json_response(self.network.put( "/networks/{net_id}.json".format(net_id=network.id), - json={"network": kwargs}, + json={"network": kwargs}), error_message="Error updating network {0}".format(name_or_id)) self._reset_network_caches() @@ -3542,8 +3538,8 @@ def delete_network(self, name_or_id): self.log.debug("Network %s not found for deleting", name_or_id) return False - self._network_client.delete( - "/networks/{network_id}.json".format(network_id=network['id'])) + exceptions.raise_from_response(self.network.delete( + "/networks/{network_id}.json".format(network_id=network['id']))) # Reset cache so the deleted network is removed self._reset_network_caches() @@ -3577,8 +3573,7 @@ def create_qos_policy(self, **kwargs): self.log.debug("'qos-default' extension is not available on " "target cloud") - data = self._network_client.post("/qos/policies.json", - json={'policy': kwargs}) + data = self.network.post("/qos/policies.json", json={'policy': kwargs}) return self._get_and_munchify('policy', data) @_utils.valid_kwargs("name", "description", "shared", "default", @@ -3621,7 +3616,7 @@ def update_qos_policy(self, name_or_id, **kwargs): raise exc.OpenStackCloudException( "QoS policy %s not found." % name_or_id) - data = self._network_client.put( + data = self.network.put( "/qos/policies/{policy_id}.json".format( policy_id=curr_policy['id']), json={'policy': kwargs}) @@ -3644,8 +3639,8 @@ def delete_qos_policy(self, name_or_id): self.log.debug("QoS policy %s not found for deleting", name_or_id) return False - self._network_client.delete( - "/qos/policies/{policy_id}.json".format(policy_id=policy['id'])) + exceptions.raise_from_response(self.network.delete( + "/qos/policies/{policy_id}.json".format(policy_id=policy['id']))) return True @@ -3693,10 +3688,12 @@ def list_qos_bandwidth_limit_rules(self, policy_name_or_id, filters=None): if not filters: filters = {} - data = self._network_client.get( + resp = self.network.get( "/qos/policies/{policy_id}/bandwidth_limit_rules.json".format( policy_id=policy['id']), - params=filters, + params=filters) + data = _adapter._json_response( + resp, error_message="Error fetching QoS bandwith limit rules from " "{policy}".format(policy=policy['id'])) return self._get_and_munchify('bandwidth_limit_rules', data) @@ -3722,9 +3719,11 @@ def get_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) - data = self._network_client.get( + resp = self.network.get( "/qos/policies/{policy_id}/bandwidth_limit_rules/{rule_id}.json". - format(policy_id=policy['id'], rule_id=rule_id), + format(policy_id=policy['id'], rule_id=rule_id)) + data = _adapter._json_response( + resp, error_message="Error fetching QoS bandwith limit rule {rule_id} " "from {policy}".format(rule_id=rule_id, policy=policy['id'])) @@ -3764,7 +3763,7 @@ def create_qos_bandwidth_limit_rule(self, policy_name_or_id, max_kbps, "target cloud") kwargs['max_kbps'] = max_kbps - data = self._network_client.post( + data = self.network.post( "/qos/policies/{policy_id}/bandwidth_limit_rules".format( policy_id=policy['id']), json={'bandwidth_limit_rule': kwargs}) @@ -3816,7 +3815,7 @@ def update_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id, "{policy_id}".format(rule_id=rule_id, policy_id=policy['id'])) - data = self._network_client.put( + data = self.network.put( "/qos/policies/{policy_id}/bandwidth_limit_rules/{rule_id}.json". format(policy_id=policy['id'], rule_id=rule_id), json={'bandwidth_limit_rule': kwargs}) @@ -3842,9 +3841,9 @@ def delete_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): name_or_id=policy_name_or_id)) try: - self._network_client.delete( + exceptions.raise_from_response(self.network.delete( "/qos/policies/{policy}/bandwidth_limit_rules/{rule}.json". - format(policy=policy['id'], rule=rule_id)) + format(policy=policy['id'], rule=rule_id))) except exc.OpenStackCloudURINotFound: self.log.debug( "QoS bandwidth limit rule {rule_id} not found in policy " @@ -3898,13 +3897,15 @@ def list_qos_dscp_marking_rules(self, policy_name_or_id, filters=None): if not filters: filters = {} - data = self._network_client.get( + resp = self.network.get( "/qos/policies/{policy_id}/dscp_marking_rules.json".format( policy_id=policy['id']), - params=filters, + params=filters) + data = _adapter._json_response( + resp, error_message="Error fetching QoS DSCP marking rules from " "{policy}".format(policy=policy['id'])) - return meta.get_and_munchify('dscp_marking_rules', data) + return self._get_and_munchify('dscp_marking_rules', data) def get_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): """Get a QoS DSCP marking rule by name or ID. @@ -3927,13 +3928,15 @@ def get_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) - data = self._network_client.get( + resp = self.network.get( "/qos/policies/{policy_id}/dscp_marking_rules/{rule_id}.json". - format(policy_id=policy['id'], rule_id=rule_id), + format(policy_id=policy['id'], rule_id=rule_id)) + data = _adapter._json_response( + resp, error_message="Error fetching QoS DSCP marking rule {rule_id} " "from {policy}".format(rule_id=rule_id, policy=policy['id'])) - return meta.get_and_munchify('dscp_marking_rule', data) + return self._get_and_munchify('dscp_marking_rule', data) def create_qos_dscp_marking_rule(self, policy_name_or_id, dscp_mark): """Create a QoS DSCP marking rule. @@ -3958,11 +3961,11 @@ def create_qos_dscp_marking_rule(self, policy_name_or_id, dscp_mark): body = { 'dscp_mark': dscp_mark } - data = self._network_client.post( + data = self.network.post( "/qos/policies/{policy_id}/dscp_marking_rules".format( policy_id=policy['id']), json={'dscp_marking_rule': body}) - return meta.get_and_munchify('dscp_marking_rule', data) + return self._get_and_munchify('dscp_marking_rule', data) @_utils.valid_kwargs("dscp_mark") def update_qos_dscp_marking_rule(self, policy_name_or_id, rule_id, @@ -3999,11 +4002,11 @@ def update_qos_dscp_marking_rule(self, policy_name_or_id, rule_id, "{policy_id}".format(rule_id=rule_id, policy_id=policy['id'])) - data = self._network_client.put( + data = self.network.put( "/qos/policies/{policy_id}/dscp_marking_rules/{rule_id}.json". format(policy_id=policy['id'], rule_id=rule_id), json={'dscp_marking_rule': kwargs}) - return meta.get_and_munchify('dscp_marking_rule', data) + return self._get_and_munchify('dscp_marking_rule', data) def delete_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): """Delete a QoS DSCP marking rule. @@ -4025,9 +4028,9 @@ def delete_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): name_or_id=policy_name_or_id)) try: - self._network_client.delete( + exceptions.raise_from_response(self.network.delete( "/qos/policies/{policy}/dscp_marking_rules/{rule}.json". - format(policy=policy['id'], rule=rule_id)) + format(policy=policy['id'], rule=rule_id))) except exc.OpenStackCloudURINotFound: self.log.debug( "QoS DSCP marking rule {rule_id} not found in policy " @@ -4083,10 +4086,12 @@ def list_qos_minimum_bandwidth_rules(self, policy_name_or_id, if not filters: filters = {} - data = self._network_client.get( + resp = self.network.get( "/qos/policies/{policy_id}/minimum_bandwidth_rules.json".format( policy_id=policy['id']), - params=filters, + params=filters) + data = _adapter._json_response( + resp, error_message="Error fetching QoS minimum bandwith rules from " "{policy}".format(policy=policy['id'])) return self._get_and_munchify('minimum_bandwidth_rules', data) @@ -4112,9 +4117,11 @@ def get_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) - data = self._network_client.get( + resp = self.network.get( "/qos/policies/{policy_id}/minimum_bandwidth_rules/{rule_id}.json". - format(policy_id=policy['id'], rule_id=rule_id), + format(policy_id=policy['id'], rule_id=rule_id)) + data = _adapter._json_response( + resp, error_message="Error fetching QoS minimum_bandwith rule {rule_id} " "from {policy}".format(rule_id=rule_id, policy=policy['id'])) @@ -4145,7 +4152,7 @@ def create_qos_minimum_bandwidth_rule(self, policy_name_or_id, min_kbps, name_or_id=policy_name_or_id)) kwargs['min_kbps'] = min_kbps - data = self._network_client.post( + data = self.network.post( "/qos/policies/{policy_id}/minimum_bandwidth_rules".format( policy_id=policy['id']), json={'minimum_bandwidth_rule': kwargs}) @@ -4188,7 +4195,7 @@ def update_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id, "{policy_id}".format(rule_id=rule_id, policy_id=policy['id'])) - data = self._network_client.put( + data = self.network.put( "/qos/policies/{policy_id}/minimum_bandwidth_rules/{rule_id}.json". format(policy_id=policy['id'], rule_id=rule_id), json={'minimum_bandwidth_rule': kwargs}) @@ -4214,9 +4221,9 @@ def delete_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): name_or_id=policy_name_or_id)) try: - self._network_client.delete( + exceptions.raise_from_response(self.network.delete( "/qos/policies/{policy}/minimum_bandwidth_rules/{rule}.json". - format(policy=policy['id'], rule=rule_id)) + format(policy=policy['id'], rule=rule_id))) except exc.OpenStackCloudURINotFound: self.log.debug( "QoS minimum bandwidth rule {rule_id} not found in policy " @@ -4262,10 +4269,11 @@ def add_router_interface(self, router, subnet_id=None, port_id=None): if port_id: json_body['port_id'] = port_id - return self._network_client.put( - "/routers/{router_id}/add_router_interface.json".format( - router_id=router['id']), - json=json_body, + return _adapter._json_response( + self.network.put( + "/routers/{router_id}/add_router_interface.json".format( + router_id=router['id']), + json=json_body), error_message="Error attaching interface to router {0}".format( router['id'])) @@ -4296,10 +4304,11 @@ def remove_router_interface(self, router, subnet_id=None, port_id=None): raise ValueError( "At least one of subnet_id or port_id must be supplied.") - self._network_client.put( - "/routers/{router_id}/remove_router_interface.json".format( - router_id=router['id']), - json=json_body, + exceptions.raise_from_response( + self.network.put( + "/routers/{router_id}/remove_router_interface.json".format( + router_id=router['id']), + json=json_body), error_message="Error detaching interface from router {0}".format( router['id'])) @@ -4384,8 +4393,8 @@ def create_router(self, name=None, admin_state_up=True, 'target cloud') router['availability_zone_hints'] = availability_zone_hints - data = self._network_client.post( - "/routers.json", json={"router": router}, + data = _adapter._json_response( + self.network.post("/routers.json", json={"router": router}), error_message="Error creating router {0}".format(name)) return self._get_and_munchify('router', data) @@ -4450,9 +4459,11 @@ def update_router(self, name_or_id, name=None, admin_state_up=None, raise exc.OpenStackCloudException( "Router %s not found." % name_or_id) - data = self._network_client.put( + resp = self.network.put( "/routers/{router_id}.json".format(router_id=curr_router['id']), - json={"router": router}, + json={"router": router}) + data = _adapter._json_response( + resp, error_message="Error updating router {0}".format(name_or_id)) return self._get_and_munchify('router', data) @@ -4474,9 +4485,9 @@ def delete_router(self, name_or_id): self.log.debug("Router %s not found for deleting", name_or_id) return False - self._network_client.delete( + exceptions.raise_from_response(self.network.delete( "/routers/{router_id}.json".format(router_id=router['id']), - error_message="Error deleting router {0}".format(name_or_id)) + error_message="Error deleting router {0}".format(name_or_id))) return True @@ -5952,7 +5963,7 @@ def create_floating_ip(self, network=None, server=None, def _submit_create_fip(self, kwargs): # Split into a method to aid in test mocking - data = self._network_client.post( + data = self.network.post( "/floatingips.json", json={"floatingip": kwargs}) return self._normalize_floating_ip( self._get_and_munchify('floatingip', data)) @@ -6103,9 +6114,9 @@ def _delete_floating_ip(self, floating_ip_id): def _neutron_delete_floating_ip(self, floating_ip_id): try: - self._network_client.delete( + _adapter._json_response(self.network.delete( "/floatingips/{fip_id}.json".format(fip_id=floating_ip_id), - error_message="unable to delete floating IP") + error_message="unable to delete floating IP")) except exc.OpenStackCloudResourceNotFound: return False except Exception as e: @@ -6344,9 +6355,10 @@ def _neutron_attach_ip_to_server( if fixed_address is not None: floating_ip_args['fixed_ip_address'] = fixed_address - return self._network_client.put( - "/floatingips/{fip_id}.json".format(fip_id=floating_ip['id']), - json={'floatingip': floating_ip_args}, + return _adapter._json_response( + self.network.put( + "/floatingips/{fip_id}.json".format(fip_id=floating_ip['id']), + json={'floatingip': floating_ip_args}), error_message=("Error attaching IP {ip} to " "server {server_id}".format( ip=floating_ip['id'], @@ -6401,9 +6413,10 @@ def _neutron_detach_ip_from_server(self, server_id, floating_ip_id): f_ip = self.get_floating_ip(id=floating_ip_id) if f_ip is None or not f_ip['attached']: return False - self._network_client.put( - "/floatingips/{fip_id}.json".format(fip_id=floating_ip_id), - json={"floatingip": {"port_id": None}}, + exceptions.raise_from_response( + self.network.put( + "/floatingips/{fip_id}.json".format(fip_id=floating_ip_id), + json={"floatingip": {"port_id": None}}), error_message=("Error detaching IP {ip} from " "server {server_id}".format( ip=floating_ip_id, server_id=server_id))) @@ -8234,10 +8247,9 @@ def create_subnet(self, network_name_or_id, cidr=None, ip_version=4, if use_default_subnetpool: subnet['use_default_subnetpool'] = True - data = self._network_client.post("/subnets.json", - json={"subnet": subnet}) + response = self.network.post("/subnets.json", json={"subnet": subnet}) - return self._get_and_munchify('subnet', data) + return self._get_and_munchify('subnet', response) def delete_subnet(self, name_or_id): """Delete a subnet. @@ -8257,8 +8269,8 @@ def delete_subnet(self, name_or_id): self.log.debug("Subnet %s not found for deleting", name_or_id) return False - self._network_client.delete( - "/subnets/{subnet_id}.json".format(subnet_id=subnet['id'])) + exceptions.raise_from_response(self.network.delete( + "/subnets/{subnet_id}.json".format(subnet_id=subnet['id']))) return True def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, @@ -8343,10 +8355,10 @@ def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, raise exc.OpenStackCloudException( "Subnet %s not found." % name_or_id) - data = self._network_client.put( + response = self.network.put( "/subnets/{subnet_id}.json".format(subnet_id=curr_subnet['id']), json={"subnet": subnet}) - return self._get_and_munchify('subnet', data) + return self._get_and_munchify('subnet', response) @_utils.valid_kwargs('name', 'admin_state_up', 'mac_address', 'fixed_ips', 'subnet_id', 'ip_address', 'security_groups', @@ -8407,8 +8419,8 @@ def create_port(self, network_id, **kwargs): """ kwargs['network_id'] = network_id - data = self._network_client.post( - "/ports.json", json={'port': kwargs}, + data = _adapter._json_response( + self.network.post("/ports.json", json={'port': kwargs}), error_message="Error creating port for network {0}".format( network_id)) return self._get_and_munchify('port', data) @@ -8471,9 +8483,10 @@ def update_port(self, name_or_id, **kwargs): raise exc.OpenStackCloudException( "failed to find port '{port}'".format(port=name_or_id)) - data = self._network_client.put( - "/ports/{port_id}.json".format(port_id=port['id']), - json={"port": kwargs}, + data = _adapter._json_response( + self.network.put( + "/ports/{port_id}.json".format(port_id=port['id']), + json={"port": kwargs}), error_message="Error updating port {0}".format(name_or_id)) return self._get_and_munchify('port', data) @@ -8491,8 +8504,9 @@ def delete_port(self, name_or_id): self.log.debug("Port %s not found for deleting", name_or_id) return False - self._network_client.delete( - "/ports/{port_id}.json".format(port_id=port['id']), + exceptions.raise_from_response( + self.network.delete( + "/ports/{port_id}.json".format(port_id=port['id'])), error_message="Error deleting port {0}".format(name_or_id)) return True @@ -8526,9 +8540,10 @@ def create_security_group(self, name, description, project_id=None): if project_id is not None: security_group_json['security_group']['tenant_id'] = project_id if self._use_neutron_secgroups(): - data = self._network_client.post( - '/security-groups.json', - json=security_group_json, + data = _adapter._json_response( + self.network.post( + '/security-groups.json', + json=security_group_json), error_message="Error creating security group {0}".format(name)) else: data = _adapter._json_response(self.compute.post( @@ -8562,8 +8577,10 @@ def delete_security_group(self, name_or_id): return False if self._use_neutron_secgroups(): - self._network_client.delete( - '/security-groups/{sg_id}.json'.format(sg_id=secgroup['id']), + exceptions.raise_from_response( + self.network.delete( + '/security-groups/{sg_id}.json'.format( + sg_id=secgroup['id'])), error_message="Error deleting security group {0}".format( name_or_id) ) @@ -8599,9 +8616,10 @@ def update_security_group(self, name_or_id, **kwargs): "Security group %s not found." % name_or_id) if self._use_neutron_secgroups(): - data = self._network_client.put( - '/security-groups/{sg_id}.json'.format(sg_id=group['id']), - json={'security_group': kwargs}, + data = _adapter._json_response( + self.network.put( + '/security-groups/{sg_id}.json'.format(sg_id=group['id']), + json={'security_group': kwargs}), error_message="Error updating security group {0}".format( name_or_id)) else: @@ -8698,9 +8716,10 @@ def create_security_group_rule(self, if project_id is not None: rule_def['tenant_id'] = project_id - data = self._network_client.post( - '/security-group-rules.json', - json={'security_group_rule': rule_def}, + data = _adapter._json_response( + self.network.post( + '/security-group-rules.json', + json={'security_group_rule': rule_def}), error_message="Error creating security group rule") else: # NOTE: Neutron accepts None for protocol. Nova does not. @@ -8770,8 +8789,10 @@ def delete_security_group_rule(self, rule_id): if self._use_neutron_secgroups(): try: - self._network_client.delete( - '/security-group-rules/{sg_id}.json'.format(sg_id=rule_id), + exceptions.raise_from_response( + self.network.delete( + '/security-group-rules/{sg_id}.json'.format( + sg_id=rule_id)), error_message="Error deleting security group rule " "{0}".format(rule_id)) except exc.OpenStackCloudResourceNotFound: @@ -8779,8 +8800,13 @@ def delete_security_group_rule(self, rule_id): return True else: - _adapter._json_response(self.compute.delete( - '/os-security-group-rules/{id}'.format(id=rule_id))) + try: + exceptions.raise_from_response( + self.compute.delete( + '/os-security-group-rules/{id}'.format(id=rule_id))) + except exc.OpenStackCloudResourceNotFound: + return False + return True def list_zones(self): @@ -11876,9 +11902,10 @@ def set_network_quotas(self, name_or_id, **kwargs): if not proj: raise exc.OpenStackCloudException("project does not exist") - self._network_client.put( - '/quotas/{project_id}.json'.format(project_id=proj.id), - json={'quota': kwargs}, + exceptions.raise_from_response( + self.network.put( + '/quotas/{project_id}.json'.format(project_id=proj.id), + json={'quota': kwargs}), error_message=("Error setting Neutron's quota for " "project {0}".format(proj.id))) @@ -11899,8 +11926,8 @@ def get_network_quotas(self, name_or_id, details=False): if details: url = url + "/details" url = url + ".json" - data = self._network_client.get( - url, + data = _adapter._json_response( + self.network.get(url), error_message=("Error fetching Neutron's quota for " "project {0}".format(proj.id))) return self._get_and_munchify('quota', data) @@ -11924,8 +11951,9 @@ def delete_network_quotas(self, name_or_id): proj = self.get_project(name_or_id) if not proj: raise exc.OpenStackCloudException("project does not exist") - self._network_client.delete( - '/quotas/{project_id}.json'.format(project_id=proj.id), + exceptions.raise_from_response( + self.network.delete( + '/quotas/{project_id}.json'.format(project_id=proj.id)), error_message=("Error deleting Neutron's quota for " "project {0}".format(proj.id))) diff --git a/openstack/service_description.py b/openstack/service_description.py index e49d9db83..a692d261f 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -117,6 +117,7 @@ def _make_proxy(self, instance, owner): ) if proxy_obj: + data = proxy_obj.get_endpoint_data() if data.catalog_url != data.service_url: ep_key = '{service_type}_endpoint_override'.format( From 25f74ebba886ed26d46c6553f113114218b34a8d Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Fri, 12 Oct 2018 14:48:14 +0200 Subject: [PATCH 2230/3836] Switch bare metal NIC actions in OpenStackCloud to baremetal Proxy calls This change allows bare metal NIC actions to use the microversion negotiation process instead of the hardcoded version 1.6. Change-Id: I0d31251fc110b9db744509ce6f254745a13acebf --- openstack/cloud/openstackcloud.py | 41 +++++++------------ .../tests/unit/cloud/test_baremetal_ports.py | 20 ++++----- .../baremetal-ports-cc0f56ae0d192aba.yaml | 5 +++ 3 files changed, 29 insertions(+), 37 deletions(-) create mode 100644 releasenotes/notes/baremetal-ports-cc0f56ae0d192aba.yaml diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 831f79a46..372255225 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -9433,11 +9433,8 @@ def update_cluster_template(self, name_or_id, operation, **kwargs): update_coe_cluster_template = update_cluster_template def list_nics(self): - msg = "Error fetching machine port list" - data = self._baremetal_client.get("/ports", - microversion="1.6", - error_message=msg) - return data['ports'] + """Return a list of all bare metal ports.""" + return [nic._to_munch() for nic in self.baremetal.ports(details=True)] def list_nics_for_machine(self, uuid): """Returns a list of ports present on the machine node. @@ -9446,23 +9443,18 @@ def list_nics_for_machine(self, uuid): order to identify the machine. :returns: A list of ports. """ - msg = "Error fetching port list for node {node_id}".format( - node_id=uuid) - url = "/nodes/{node_id}/ports".format(node_id=uuid) - data = self._baremetal_client.get(url, - microversion="1.6", - error_message=msg) - return data['ports'] + # TODO(dtantsur): support node names here. + return [nic._to_munch() + for nic in self.baremetal.ports(details=True, node_id=uuid)] def get_nic_by_mac(self, mac): + """Get bare metal NIC by its hardware address (usually MAC).""" + results = [nic._to_munch() + for nic in self.baremetal.ports(address=mac, details=True)] try: - url = '/ports/detail?address=%s' % mac - data = self._baremetal_client.get(url) - if len(data['ports']) == 1: - return data['ports'][0] - except Exception: - pass - return None + return results[0] + except IndexError: + return None def list_machines(self): """List Machines. @@ -9497,14 +9489,11 @@ def get_machine_by_mac(self, mac): :returns: ``munch.Munch`` representing the node found or None if the node is not found. """ - try: - port_url = '/ports/detail?address={mac}'.format(mac=mac) - port = self._baremetal_client.get(port_url, microversion=1.6) - machine_url = '/nodes/{machine}'.format( - machine=port['ports'][0]['node_uuid']) - return self._baremetal_client.get(machine_url, microversion=1.6) - except Exception: + nic = self.get_nic_by_mac(mac) + if nic is None: return None + else: + return self.get_machine(nic['node_uuid']) def inspect_machine(self, name_or_id, wait=False, timeout=3600): """Inspect a Barmetal machine diff --git a/openstack/tests/unit/cloud/test_baremetal_ports.py b/openstack/tests/unit/cloud/test_baremetal_ports.py index 942b472b2..aa39b67ed 100644 --- a/openstack/tests/unit/cloud/test_baremetal_ports.py +++ b/openstack/tests/unit/cloud/test_baremetal_ports.py @@ -43,20 +43,20 @@ def setUp(self): def test_list_nics(self): self.register_uris([ dict(method='GET', - uri=self.get_mock_url(resource='ports'), + uri=self.get_mock_url(resource='ports', append=['detail']), json={'ports': [self.fake_baremetal_port, self.fake_baremetal_port2]}), ]) return_value = self.cloud.list_nics() self.assertEqual(2, len(return_value)) - self.assertEqual(self.fake_baremetal_port, return_value[0]) + self.assertSubdict(self.fake_baremetal_port, return_value[0]) self.assert_calls() def test_list_nics_failure(self): self.register_uris([ dict(method='GET', - uri=self.get_mock_url(resource='ports'), + uri=self.get_mock_url(resource='ports', append=['detail']), status_code=400) ]) self.assertRaises(exc.OpenStackCloudException, @@ -64,11 +64,10 @@ def test_list_nics_failure(self): self.assert_calls() def test_list_nics_for_machine(self): + query = 'detail?node_uuid=%s' % self.fake_baremetal_node['uuid'] self.register_uris([ dict(method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], 'ports']), + uri=self.get_mock_url(resource='ports', append=[query]), json={'ports': [self.fake_baremetal_port, self.fake_baremetal_port2]}), ]) @@ -76,15 +75,14 @@ def test_list_nics_for_machine(self): return_value = self.cloud.list_nics_for_machine( self.fake_baremetal_node['uuid']) self.assertEqual(2, len(return_value)) - self.assertEqual(self.fake_baremetal_port, return_value[0]) + self.assertSubdict(self.fake_baremetal_port, return_value[0]) self.assert_calls() def test_list_nics_for_machine_failure(self): + query = 'detail?node_uuid=%s' % self.fake_baremetal_node['uuid'] self.register_uris([ dict(method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], 'ports']), + uri=self.get_mock_url(resource='ports', append=[query]), status_code=400) ]) @@ -104,7 +102,7 @@ def test_get_nic_by_mac(self): return_value = self.cloud.get_nic_by_mac(mac) - self.assertEqual(self.fake_baremetal_port, return_value) + self.assertSubdict(self.fake_baremetal_port, return_value) self.assert_calls() def test_get_nic_by_mac_failure(self): diff --git a/releasenotes/notes/baremetal-ports-cc0f56ae0d192aba.yaml b/releasenotes/notes/baremetal-ports-cc0f56ae0d192aba.yaml new file mode 100644 index 000000000..90adce120 --- /dev/null +++ b/releasenotes/notes/baremetal-ports-cc0f56ae0d192aba.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + The ``OpenStackCloud`` bare metal NIC calls now support all microversions + supported by the SDK. Previously version 1.6 was hardcoded. From 603ba0d7d9361807be98f6f60ad8f8298b3887be Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 26 Sep 2018 13:42:41 -0500 Subject: [PATCH 2231/3836] Remove all the deprecated stuff We're really close to a 1.0 release. Let's go ahead and remove the stuff that was marked deprecated and see what breaks. Depends-On: https://review.openstack.org/610695 Change-Id: I0b1b7a0060a3cc0ac1772e5ef2d2a9673159cd77 --- doc/source/enforcer.py | 21 -- doc/source/user/config/vendor-support.rst | 17 -- doc/source/user/guides/clustering/cluster.rst | 36 +-- doc/source/user/guides/logging.rst | 5 +- doc/source/user/proxies/baremetal.rst | 12 - doc/source/user/proxies/clustering.rst | 12 - doc/source/user/proxies/network.rst | 3 - doc/source/user/utils.rst | 1 - examples/clustering/cluster.py | 34 +-- examples/connect.py | 3 +- lower-constraints.txt | 1 - openstack/baremetal/v1/_proxy.py | 126 ----------- openstack/block_storage/v2/_proxy.py | 42 ++++ openstack/clustering/v1/_proxy.py | 151 ------------- openstack/compute/v2/_proxy.py | 38 +++- openstack/config/vendors/dreamhost.json | 13 -- openstack/network/v2/_proxy.py | 29 --- openstack/proxy.py | 59 ----- openstack/proxy2.py | 23 -- openstack/resource2.py | 23 -- .../tests/unit/baremetal/v1/test_proxy.py | 42 ---- .../tests/unit/clustering/v1/test_proxy.py | 213 +----------------- openstack/tests/unit/network/v2/test_proxy.py | 44 ---- openstack/tests/unit/test_proxy.py | 39 ---- openstack/tests/unit/test_utils.py | 7 +- openstack/utils.py | 40 ---- ...ed-deprecated-things-8700fe3592c3bf18.yaml | 5 + requirements.txt | 1 - 28 files changed, 131 insertions(+), 909 deletions(-) delete mode 100644 openstack/config/vendors/dreamhost.json delete mode 100644 openstack/proxy2.py delete mode 100644 openstack/resource2.py create mode 100644 releasenotes/notes/removed-deprecated-things-8700fe3592c3bf18.yaml diff --git a/doc/source/enforcer.py b/doc/source/enforcer.py index 5dc0c6d00..3cd38986c 100644 --- a/doc/source/enforcer.py +++ b/doc/source/enforcer.py @@ -10,13 +10,6 @@ WRITTEN_METHODS = set() -# NOTE: This is temporary! These methods currently exist on the base -# Proxy class as public methods, but they're deprecated in favor of -# subclasses actually exposing them if necessary. However, as they're -# public and purposely undocumented, they cause spurious warnings. -# Ignore these methods until they're actually removed from the API, -# and then we can take this special case out. -IGNORED_METHODS = ("wait_for_delete", "wait_for_status") class EnforcementError(errors.SphinxError): """A mismatch between what exists and what's documented""" @@ -52,10 +45,6 @@ def get_proxy_methods(): # We only document public names names = [name for name in dir(instance) if not name.startswith("_")] - # Remove the wait_for_* names temporarily. - for name in IGNORED_METHODS: - names.remove(name) - good_names = [module.__name__ + ".Proxy." + name for name in names] methods.update(good_names) @@ -104,16 +93,6 @@ def build_finished(app, exception): app.info("ENFORCER: %d proxy methods written" % len(WRITTEN_METHODS)) missing = all_methods - WRITTEN_METHODS - def is_ignored(name): - for ignored_name in IGNORED_METHODS: - if ignored_name in name: - return True - return False - - # TEMPORARY: Ignore the wait_for names when determining what is missing. - app.info("ENFORCER: Ignoring wait_for_* names...") - missing = set(x for x in missing if not is_ignored(x)) - missing_count = len(missing) app.info("ENFORCER: Found %d missing proxy methods " "in the output" % missing_count) diff --git a/doc/source/user/config/vendor-support.rst b/doc/source/user/config/vendor-support.rst index 5469fe0e4..4143da248 100644 --- a/doc/source/user/config/vendor-support.rst +++ b/doc/source/user/config/vendor-support.rst @@ -123,23 +123,6 @@ RegionOne Ashburn, VA * Images must be in `raw` format * IPv6 is provided to every server -DreamHost ---------- - -Deprecated, please use dreamcompute - -https://keystone.dream.io/v2.0 - -============== ================ -Region Name Location -============== ================ -RegionOne Ashburn, VA -============== ================ - -* Images must be in `raw` format -* Public IPv4 is provided via NAT with Neutron Floating IP -* IPv6 is provided to every server - Open Telekom Cloud ------------------ diff --git a/doc/source/user/guides/clustering/cluster.rst b/doc/source/user/guides/clustering/cluster.rst index 21792c523..280de5bab 100644 --- a/doc/source/user/guides/clustering/cluster.rst +++ b/doc/source/user/guides/clustering/cluster.rst @@ -96,31 +96,31 @@ and destroy them before deleting the cluster object itself. :pyobject: delete_cluster -Cluster Add Nodes -~~~~~~~~~~~~~~~~~ +Add Nodes to Cluster +~~~~~~~~~~~~~~~~~~~~ Add some existing nodes into the specified cluster. .. literalinclude:: ../../examples/clustering/cluster.py - :pyobject: cluster_add_nodes + :pyobject: add_nodes_to_cluster -Cluster Del Nodes -~~~~~~~~~~~~~~~~~ +Remove Nodes from Cluster +~~~~~~~~~~~~~~~~~~~~~~~~~ Remove nodes from specified cluster. .. literalinclude:: ../../examples/clustering/cluster.py - :pyobject: cluster_del_nodes + :pyobject: remove_nodes_from_cluster -Cluster Replace Nodes -~~~~~~~~~~~~~~~~~~~~~ +Replace Nodes in Cluster +~~~~~~~~~~~~~~~~~~~~~~~~ Replace some existing nodes in the specified cluster. .. literalinclude:: ../../examples/clustering/cluster.py - :pyobject: cluster_replace_nodes + :pyobject: replace_nodes_in_cluster Cluster Scale Out @@ -129,7 +129,7 @@ Cluster Scale Out Inflate the size of a cluster. .. literalinclude:: ../../examples/clustering/cluster.py - :pyobject: cluster_scale_out + :pyobject: scale_out_cluster Cluster Scale In @@ -138,7 +138,7 @@ Cluster Scale In Shrink the size of a cluster. .. literalinclude:: ../../examples/clustering/cluster.py - :pyobject: cluster_scale_in + :pyobject: scale_out_cluster Cluster Resize @@ -147,28 +147,28 @@ Cluster Resize Resize of cluster. .. literalinclude:: ../../examples/clustering/cluster.py - :pyobject: cluster_resize + :pyobject: resize_cluster -Cluster Policy Attach -~~~~~~~~~~~~~~~~~~~~~ +Attach Policy to Cluster +~~~~~~~~~~~~~~~~~~~~~~~~ Once a policy is attached (bound) to a cluster, it will be enforced when related actions are performed on that cluster, unless the policy is (temporarily) disabled on the cluster .. literalinclude:: ../../examples/clustering/cluster.py - :pyobject: cluster_attach_policy + :pyobject: attach_policy_to_cluster -Cluster Policy Detach -~~~~~~~~~~~~~~~~~~~~~ +Detach Policy from Cluster +~~~~~~~~~~~~~~~~~~~~~~~~~~ Once a policy is attached to a cluster, it can be detached from the cluster at user's request. .. literalinclude:: ../../examples/clustering/cluster.py - :pyobject: cluster_detach_policy + :pyobject: detach_policy_from_cluster Cluster Check diff --git a/doc/source/user/guides/logging.rst b/doc/source/user/guides/logging.rst index a1bc944cd..6eb4da4a5 100644 --- a/doc/source/user/guides/logging.rst +++ b/doc/source/user/guides/logging.rst @@ -41,9 +41,10 @@ To log messages to a file called ``openstack.log`` and the console on .. code-block:: python import sys - from openstack import utils + import openstack - utils.enable_logging(debug=True, path='openstack.log', stream=sys.stdout) + openstack.enable_logging( + debug=True, path='openstack.log', stream=sys.stdout) `openstack.enable_logging` also sets up a few other loggers and diff --git a/doc/source/user/proxies/baremetal.rst b/doc/source/user/proxies/baremetal.rst index e6faffe4e..048b55c5b 100644 --- a/doc/source/user/proxies/baremetal.rst +++ b/doc/source/user/proxies/baremetal.rst @@ -73,15 +73,3 @@ VIF Operations .. automethod:: openstack.baremetal.v1._proxy.Proxy.attach_vif_to_node .. automethod:: openstack.baremetal.v1._proxy.Proxy.detach_vif_from_node .. automethod:: openstack.baremetal.v1._proxy.Proxy.list_node_vifs - -Deprecated Methods -^^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.baremetal.v1._proxy.Proxy - - .. automethod:: openstack.baremetal.v1._proxy.Proxy.create_portgroup - .. automethod:: openstack.baremetal.v1._proxy.Proxy.update_portgroup - .. automethod:: openstack.baremetal.v1._proxy.Proxy.delete_portgroup - .. automethod:: openstack.baremetal.v1._proxy.Proxy.get_portgroup - .. automethod:: openstack.baremetal.v1._proxy.Proxy.find_portgroup - .. automethod:: openstack.baremetal.v1._proxy.Proxy.portgroups diff --git a/doc/source/user/proxies/clustering.rst b/doc/source/user/proxies/clustering.rst index 2c0d56e83..26c409f56 100644 --- a/doc/source/user/proxies/clustering.rst +++ b/doc/source/user/proxies/clustering.rst @@ -97,17 +97,6 @@ Cluster Operations .. automethod:: openstack.clustering.v1._proxy.Proxy.get_cluster_policy .. automethod:: openstack.clustering.v1._proxy.Proxy.cluster_policies - .. automethod:: openstack.clustering.v1._proxy.Proxy.cluster_add_nodes - .. automethod:: openstack.clustering.v1._proxy.Proxy.cluster_attach_policy - .. automethod:: openstack.clustering.v1._proxy.Proxy.cluster_del_nodes - .. automethod:: openstack.clustering.v1._proxy.Proxy.cluster_detach_policy - .. automethod:: openstack.clustering.v1._proxy.Proxy.cluster_operation - .. automethod:: openstack.clustering.v1._proxy.Proxy.cluster_replace_nodes - .. automethod:: openstack.clustering.v1._proxy.Proxy.cluster_resize - .. automethod:: openstack.clustering.v1._proxy.Proxy.cluster_scale_in - .. automethod:: openstack.clustering.v1._proxy.Proxy.cluster_scale_out - .. automethod:: openstack.clustering.v1._proxy.Proxy.cluster_update_policy - Node Operations ^^^^^^^^^^^^^^^ @@ -126,7 +115,6 @@ Node Operations .. automethod:: openstack.clustering.v1._proxy.Proxy.perform_operation_on_node .. automethod:: openstack.clustering.v1._proxy.Proxy.adopt_node - .. automethod:: openstack.clustering.v1._proxy.Proxy.node_operation Receiver Operations diff --git a/doc/source/user/proxies/network.rst b/doc/source/user/proxies/network.rst index 5bce51b28..e69f6cda1 100644 --- a/doc/source/user/proxies/network.rst +++ b/doc/source/user/proxies/network.rst @@ -120,9 +120,6 @@ Security Group Operations .. automethod:: openstack.network.v2._proxy.Proxy.security_group_rules .. automethod:: openstack.network.v2._proxy.Proxy.security_groups - .. automethod:: openstack.network.v2._proxy.Proxy.security_group_allow_ping - .. automethod:: openstack.network.v2._proxy.Proxy.security_group_open_port - .. automethod:: openstack.network.v2._proxy.Proxy.create_security_group_rule .. automethod:: openstack.network.v2._proxy.Proxy.delete_security_group_rule diff --git a/doc/source/user/utils.rst b/doc/source/user/utils.rst index f69638e30..5c1f39de9 100644 --- a/doc/source/user/utils.rst +++ b/doc/source/user/utils.rst @@ -1,4 +1,3 @@ Utilities ========= .. automodule:: openstack.utils - :members: enable_logging diff --git a/examples/clustering/cluster.py b/examples/clustering/cluster.py index 7710f5f7b..0c49beb2a 100644 --- a/examples/clustering/cluster.py +++ b/examples/clustering/cluster.py @@ -86,23 +86,23 @@ def delete_cluster(conn): print("Cluster deleted") -def cluster_add_nodes(conn): +def add_nodes_to_cluster(conn): print("Add nodes to cluster:") node_ids = [NODE_ID] - res = conn.clustering.cluster_add_nodes(CLUSTER_ID, node_ids) + res = conn.clustering.add_nodes_to_cluster(CLUSTER_ID, node_ids) print(res) -def cluster_del_nodes(conn): +def remove_nodes_from_cluster(conn): print("Remove nodes from a cluster:") node_ids = [NODE_ID] - res = conn.clustering.cluster_del_nodes(CLUSTER_ID, node_ids) + res = conn.clustering.remove_nodes_from_cluster(CLUSTER_ID, node_ids) print(res) -def cluster_replace_nodes(conn): +def replace_nodes_in_cluster(conn): print("Replace the nodes in a cluster with specified nodes:") old_node = NODE_ID @@ -110,25 +110,25 @@ def cluster_replace_nodes(conn): spec = { old_node: new_node } - res = conn.clustering.cluster_replace_nodes(CLUSTER_ID, **spec) + res = conn.clustering.replace_nodes_in_cluster(CLUSTER_ID, **spec) print(res) -def cluster_scale_out(conn): +def scale_out_cluster(conn): print("Inflate the size of a cluster:") - res = conn.clustering.cluster_scale_out(CLUSTER_ID, 1) + res = conn.clustering.scale_out_cluster(CLUSTER_ID, 1) print(res) -def cluster_scale_in(conn): +def scale_in_cluster(conn): print("Shrink the size of a cluster:") - res = conn.clustering.cluster_scale_in(CLUSTER_ID, 1) + res = conn.clustering.scale_in_cluster(CLUSTER_ID, 1) print(res) -def cluster_resize(conn): +def resize_cluster(conn): print("Resize of cluster:") spec = { @@ -137,23 +137,23 @@ def cluster_resize(conn): 'adjustment_type': 'EXACT_CAPACITY', 'number': 2 } - res = conn.clustering.cluster_resize(CLUSTER_ID, **spec) + res = conn.clustering.resize_cluster(CLUSTER_ID, **spec) print(res) -def cluster_attach_policy(conn): +def attach_policy_to_cluster(conn): print("Attach policy to a cluster:") spec = {'enabled': True} - res = conn.clustering.cluster_attach_policy(CLUSTER_ID, POLICY_ID, - **spec) + res = conn.clustering.attach_policy_to_cluster( + CLUSTER_ID, POLICY_ID, **spec) print(res) -def cluster_detach_policy(conn): +def detach_policy_from_cluster(conn): print("Detach a policy from a cluster:") - res = conn.clustering.cluster_detach_policy(CLUSTER_ID, POLICY_ID) + res = conn.clustering.detach_policy_from_cluster(CLUSTER_ID, POLICY_ID) print(res) diff --git a/examples/connect.py b/examples/connect.py index 1864053a6..d5cce1c09 100644 --- a/examples/connect.py +++ b/examples/connect.py @@ -21,10 +21,9 @@ import openstack from openstack.config import loader -from openstack import utils import sys -utils.enable_logging(True, stream=sys.stdout) +openstack.enable_logging(True, stream=sys.stdout) #: Defines the OpenStack Config loud key in your config file, #: typically in $HOME/.config/openstack/clouds.yaml. That configuration diff --git a/lower-constraints.txt b/lower-constraints.txt index 59dee61ee..6eb10512e 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -2,7 +2,6 @@ appdirs==1.3.0 coverage==4.0 cryptography==2.1 decorator==3.4.0 -deprecation==1.0 dogpile.cache==0.6.2 extras==1.0.0 fixtures==3.0.0 diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index ab62b9758..eb3c1d9f6 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -484,47 +484,6 @@ def delete_port(self, port, ignore_missing=True): """ return self._delete(_port.Port, port, ignore_missing=ignore_missing) - @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", - details="Use port_groups instead") - def portgroups(self, details=False, **query): - """Retrieve a generator of port groups. - - :param details: A boolean indicating whether the detailed information - for every portgroup should be returned. - :param dict query: Optional query parameters to be sent to restrict - the portgroups returned. Available parameters include: - - * ``address``: Only return portgroups with the specified physical - hardware address, typically a MAC address. - * ``fields``: A list containing one or more fields to be returned - in the response. This may lead to some performance gain - because other fields of the resource are not refreshed. - * ``limit``: Requests at most the specified number of portgroups - returned from the query. - * ``marker``: Specifies the ID of the last-seen portgroup. Use the - ``limit`` parameter to make an initial limited request and - use the ID of the last-seen portgroup from the response as - the ``marker`` value in a subsequent limited request. - * ``node``:only return the ones associated with this specific node - (name or UUID), or an empty set if not found. - * ``sort_dir``: Sorts the response by the requested sort direction. - A valid value is ``asc`` (ascending) or ``desc`` - (descending). Default is ``asc``. You can specify multiple - pairs of sort key and sort direction query parameters. If - you omit the sort direction in a pair, the API uses the - natural sorting direction of the server attribute that is - provided as the ``sort_key``. - * ``sort_key``: Sorts the response by the this attribute value. - Default is ``id``. You can specify multiple pairs of sort - key and sort direction query parameters. If you omit the - sort direction in a pair, the API uses the natural sorting - direction of the server attribute that is provided as the - ``sort_key``. - - :returns: A generator of portgroup instances. - """ - return self.port_groups(details=details, **query) - def port_groups(self, details=False, **query): """Retrieve a generator of port groups. @@ -565,20 +524,6 @@ def port_groups(self, details=False, **query): cls = _portgroup.PortGroupDetail if details else _portgroup.PortGroup return self._list(cls, paginated=True, **query) - @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", - details="Use create_port_group instead") - def create_portgroup(self, **attrs): - """Create a new port group from attributes. - - :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.baremetal.v1.port_group.PortGroup`, it - comprises of the properties on the ``PortGroup`` class. - - :returns: The results of portgroup creation. - :rtype: :class:`~openstack.baremetal.v1.port_group.PortGroup`. - """ - return self.create_port_group(**attrs) - def create_port_group(self, **attrs): """Create a new portgroup from attributes. @@ -591,21 +536,6 @@ def create_port_group(self, **attrs): """ return self._create(_portgroup.PortGroup, **attrs) - @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", - details="Use find_port_group instead") - def find_portgroup(self, name_or_id, ignore_missing=True): - """Find a single port group. - - :param str name_or_id: The name or ID of a portgroup. - :param bool ignore_missing: When set to ``False``, an exception of - :class:`~openstack.exceptions.ResourceNotFound` will be raised - when the port group does not exist. When set to `True``, None will - be returned when attempting to find a nonexistent port group. - :returns: One :class:`~openstack.baremetal.v1.port_group.PortGroup` - object or None. - """ - return self.find_port_group(name_or_id, ignore_missing=ignore_missing) - def find_port_group(self, name_or_id, ignore_missing=True): """Find a single port group. @@ -620,26 +550,6 @@ def find_port_group(self, name_or_id, ignore_missing=True): return self._find(_portgroup.PortGroup, name_or_id, ignore_missing=ignore_missing) - @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", - details="Use get_port_group instead") - def get_portgroup(self, portgroup, **query): - """Get a specific port group. - - :param portgroup: The value can be the name or ID of a chassis or a - :class:`~openstack.baremetal.v1.port_group.PortGroup` instance. - :param dict query: Optional query parameters to be sent to restrict - the portgroup properties returned. Available parameters include: - - * ``fields``: A list containing one or more fields to be returned - in the response. This may lead to some performance gain - because other fields of the resource are not refreshed. - - :returns: One :class:`~openstack.baremetal.v1.port_group.PortGroup` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no - port group matching the name or ID could be found. - """ - return self.get_port_group(portgroup, **query) - def get_port_group(self, port_group, **query): """Get a specific port group. @@ -658,22 +568,6 @@ def get_port_group(self, port_group, **query): """ return self._get(_portgroup.PortGroup, port_group, **query) - @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", - details="Use update_port_group instead") - def update_portgroup(self, portgroup, **attrs): - """Update a port group. - - :param chassis: Either the name or the ID of a port group or - an instance of - :class:`~openstack.baremetal.v1.port_group.PortGroup`. - :param dict attrs: The attributes to update on the port group - represented by the ``portgroup`` parameter. - - :returns: The updated port group. - :rtype: :class:`~openstack.baremetal.v1.port_group.PortGroup` - """ - return self.update_port_group(portgroup, **attrs) - def update_port_group(self, port_group, **attrs): """Update a port group. @@ -688,26 +582,6 @@ def update_port_group(self, port_group, **attrs): """ return self._update(_portgroup.PortGroup, port_group, **attrs) - @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", - details="Use delete_port_group instead") - def delete_portgroup(self, portgroup, ignore_missing=True): - """Delete a port group. - - :param portgroup: The value can be either the name or ID of a port - group or a - :class:`~openstack.baremetal.v1.port_group.PortGroup` - instance. - :param bool ignore_missing: When set to ``False``, an exception - :class:`~openstack.exceptions.ResourceNotFound` will be raised - when the port group could not be found. When set to ``True``, no - exception will be raised when attempting to delete a non-existent - port group. - - :returns: The instance of the port group which was deleted. - :rtype: :class:`~openstack.baremetal.v1.port_group.PortGroup`. - """ - return self.delete_port_group(portgroup, ignore_missing=ignore_missing) - def delete_port_group(self, port_group, ignore_missing=True): """Delete a port group. diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 825e6c935..88091c45d 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -15,6 +15,7 @@ from openstack.block_storage.v2 import type as _type from openstack.block_storage.v2 import volume as _volume from openstack import proxy +from openstack import resource class Proxy(proxy.Proxy): @@ -195,3 +196,44 @@ def backend_pools(self): :returns A generator of cinder Back-end storage pools objects """ return self._list(_stats.Pools, paginated=False) + + def wait_for_status(self, res, status='ACTIVE', failures=None, + interval=2, wait=120): + """Wait for a resource to be in a particular status. + + :param res: The resource to wait on to reach the specified status. + The resource must have a ``status`` attribute. + :type resource: A :class:`~openstack.resource.Resource` object. + :param status: Desired status. + :param failures: Statuses that would be interpreted as failures. + :type failures: :py:class:`list` + :param interval: Number of seconds to wait before to consecutive + checks. Default to 2. + :param wait: Maximum number of seconds to wait before the change. + Default to 120. + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to the desired status failed to occur in specified seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + has transited to one of the failure statuses. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute. + """ + failures = ['Error'] if failures is None else failures + return resource.wait_for_status( + self, res, status, failures, interval, wait) + + def wait_for_delete(self, res, interval=2, wait=120): + """Wait for a resource to be deleted. + + :param res: The resource to wait on to be deleted. + :type resource: A :class:`~openstack.resource.Resource` object. + :param interval: Number of seconds to wait before to consecutive + checks. Default to 2. + :param wait: Maximum number of seconds to wait before the change. + Default to 120. + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete(self, res, interval, wait) diff --git a/openstack/clustering/v1/_proxy.py b/openstack/clustering/v1/_proxy.py index f461aa0d8..e78d68bdf 100644 --- a/openstack/clustering/v1/_proxy.py +++ b/openstack/clustering/v1/_proxy.py @@ -25,7 +25,6 @@ from openstack.clustering.v1 import service as _service from openstack import proxy from openstack import resource -from openstack import utils class Proxy(proxy.Proxy): @@ -283,18 +282,6 @@ def update_cluster(self, cluster, **attrs): """ return self._update(_cluster.Cluster, cluster, **attrs) - @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", - details="Use add_nodes_to_cluster instead") - def cluster_add_nodes(self, cluster, nodes): - """Add nodes to a cluster. - - :param cluster: Either the name or the ID of the cluster, or an - instance of :class:`~openstack.clustering.v1.cluster.Cluster`. - :param nodes: List of nodes to be added to the cluster. - :returns: A dict containing the action initiated by this operation. - """ - return self.add_nodes_to_cluster(cluster, nodes) - def add_nodes_to_cluster(self, cluster, nodes): """Add nodes to a cluster. @@ -309,23 +296,6 @@ def add_nodes_to_cluster(self, cluster, nodes): obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) return obj.add_nodes(self, nodes) - @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", - details="Use remove_nodes_from_cluster instead") - def cluster_del_nodes(self, cluster, nodes, **params): - """Remove nodes from a cluster. - - :param cluster: Either the name or the ID of the cluster, or an - instance of :class:`~openstack.clustering.v1.cluster.Cluster`. - :param nodes: List of nodes to be removed from the cluster. - :param kwargs \*\*params: Optional query parameters to be sent to - restrict the nodes to be returned. Available parameters include: - - * destroy_after_deletion: A boolean value indicating whether the - deleted nodes to be destroyed right away. - :returns: A dict containing the action initiated by this operation. - """ - return self.remove_nodes_from_cluster(cluster, nodes, **params) - def remove_nodes_from_cluster(self, cluster, nodes, **params): """Remove nodes from a cluster. @@ -345,18 +315,6 @@ def remove_nodes_from_cluster(self, cluster, nodes, **params): obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) return obj.del_nodes(self, nodes, **params) - @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", - details="Use replace_nodes_in_cluster instead") - def cluster_replace_nodes(self, cluster, nodes): - """Replace the nodes in a cluster with specified nodes. - - :param cluster: Either the name or the ID of the cluster, or an - instance of :class:`~openstack.clustering.v1.cluster.Cluster`. - :param nodes: List of nodes to be deleted/added to the cluster. - :returns: A dict containing the action initiated by this operation. - """ - return self.replace_nodes_in_cluster(cluster, nodes) - def replace_nodes_in_cluster(self, cluster, nodes): """Replace the nodes in a cluster with specified nodes. @@ -371,19 +329,6 @@ def replace_nodes_in_cluster(self, cluster, nodes): obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) return obj.replace_nodes(self, nodes) - @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", - details="Use scale_out_cluster instead") - def cluster_scale_out(self, cluster, count=None): - """Inflate the size of a cluster. - - :param cluster: Either the name or the ID of the cluster, or an - instance of :class:`~openstack.clustering.v1.cluster.Cluster`. - :param count: Optional parameter specifying the number of nodes to - be added. - :returns: A dict containing the action initiated by this operation. - """ - return self.scale_out_cluster(cluster, count) - def scale_out_cluster(self, cluster, count=None): """Inflate the size of a cluster. @@ -399,19 +344,6 @@ def scale_out_cluster(self, cluster, count=None): obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) return obj.scale_out(self, count) - @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", - details="Use scale_in_cluster instead") - def cluster_scale_in(self, cluster, count=None): - """Shrink the size of a cluster. - - :param cluster: Either the name or the ID of the cluster, or an - instance of :class:`~openstack.clustering.v1.cluster.Cluster`. - :param count: Optional parameter specifying the number of nodes to - be removed. - :returns: A dict containing the action initiated by this operation. - """ - return self.scale_in_cluster(cluster, count) - def scale_in_cluster(self, cluster, count=None): """Shrink the size of a cluster. @@ -427,19 +359,6 @@ def scale_in_cluster(self, cluster, count=None): obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) return obj.scale_in(self, count) - @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", - details="Use resize_cluster instead") - def cluster_resize(self, cluster, **params): - """Resize of cluster. - - :param cluster: Either the name or the ID of the cluster, or an - instance of :class:`~openstack.clustering.v1.cluster.Cluster`. - :param dict \*\*params: A dictionary providing the parameters for the - resize action. - :returns: A dict containing the action initiated by this operation. - """ - return self.resize_cluster(cluster, **params) - def resize_cluster(self, cluster, **params): """Resize of cluster. @@ -455,20 +374,6 @@ def resize_cluster(self, cluster, **params): obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) return obj.resize(self, **params) - @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", - details="Use attach_policy_to_cluster instead") - def cluster_attach_policy(self, cluster, policy, **params): - """Attach a policy to a cluster. - - :param cluster: Either the name or the ID of the cluster, or an - instance of :class:`~openstack.clustering.v1.cluster.Cluster`. - :param policy: Either the name or the ID of a policy. - :param dict \*\*params: A dictionary containing the properties for the - policy to be attached. - :returns: A dict containing the action initiated by this operation. - """ - return self.attach_policy_to_cluster(cluster, policy, **params) - def attach_policy_to_cluster(self, cluster, policy, **params): """Attach a policy to a cluster. @@ -485,18 +390,6 @@ def attach_policy_to_cluster(self, cluster, policy, **params): obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) return obj.policy_attach(self, policy, **params) - @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", - details="Use detach_policy_from_cluster instead") - def cluster_detach_policy(self, cluster, policy): - """Detach a policy from a cluster. - - :param cluster: Either the name or the ID of the cluster, or an - instance of :class:`~openstack.clustering.v1.cluster.Cluster`. - :param policy: Either the name or the ID of a policy. - :returns: A dict containing the action initiated by this operation. - """ - return self.detach_policy_from_cluster(cluster, policy) - def detach_policy_from_cluster(self, cluster, policy): """Detach a policy from a cluster. @@ -511,20 +404,6 @@ def detach_policy_from_cluster(self, cluster, policy): obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) return obj.policy_detach(self, policy) - @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", - details="Use update_cluster_policy instead") - def cluster_update_policy(self, cluster, policy, **params): - """Change properties of a policy which is bound to the cluster. - - :param cluster: Either the name or the ID of the cluster, or an - instance of :class:`~openstack.clustering.v1.cluster.Cluster`. - :param policy: Either the name or the ID of a policy. - :param dict \*\*params: A dictionary containing the new properties for - the policy. - :returns: A dict containing the action initiated by this operation. - """ - return self.update_cluster_policy(cluster, policy, **params) - def update_cluster_policy(self, cluster, policy, **params): """Change properties of a policy which is bound to the cluster. @@ -579,21 +458,6 @@ def recover_cluster(self, cluster, **params): obj = self._get_resource(_cluster.Cluster, cluster) return obj.recover(self, **params) - @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", - details="Use perform_operation_on_cluster instead") - def cluster_operation(self, cluster, operation, **params): - """Perform an operation on the specified cluster. - - :param cluster: The value can be either the ID of a cluster or a - :class:`~openstack.clustering.v1.cluster.Cluster` instance. - :param operation: A string specifying the operation to be performed. - :param dict params: A dictionary providing the parameters for the - operation. - - :returns: A dictionary containing the action ID. - """ - return self.perform_operation_on_cluster(cluster, operation, **params) - def perform_operation_on_cluster(self, cluster, operation, **params): """Perform an operation on the specified cluster. @@ -773,21 +637,6 @@ def adopt_node(self, preview=False, **attrs): node = self._get_resource(_node.Node, None) return node.adopt(self, preview=preview, **attrs) - @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", - details="Use perform_operation_on_node instead") - def node_operation(self, node, operation, **params): - """Perform an operation on the specified node. - - :param node: The value can be either the ID of a node or a - :class:`~openstack.clustering.v1.node.Node` instance. - :param operation: A string specifying the operation to be performed. - :param dict params: A dictionary providing the parameters for the - operation. - - :returns: A dictionary containing the action ID. - """ - return self.perform_operation_on_node(node, operation, **params) - def perform_operation_on_node(self, node, operation, **params): """Perform an operation on the specified node. diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 97802584d..2aa53b307 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -692,8 +692,29 @@ def get_server_console_output(self, server, length=None): server = self._get_resource(_server.Server, server) return server.get_console_output(self, length=length) - def wait_for_server(self, server, status='ACTIVE', failures=['ERROR'], + def wait_for_server(self, server, status='ACTIVE', failures=None, interval=2, wait=120): + """Wait for a server to be in a particular status. + + :param res: The resource to wait on to reach the specified status. + The resource must have a ``status`` attribute. + :type resource: A :class:`~openstack.resource.Resource` object. + :param status: Desired status. + :param failures: Statuses that would be interpreted as failures. + :type failures: :py:class:`list` + :param interval: Number of seconds to wait before to consecutive + checks. Default to 2. + :param wait: Maximum number of seconds to wait before the change. + Default to 120. + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to the desired status failed to occur in specified seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + has transited to one of the failure statuses. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute. + """ + failures = ['ERROR'] if failures is None else failures return resource.wait_for_status( self, server, status, failures, interval, wait) @@ -1195,3 +1216,18 @@ def live_migrate_server( self, host, force=force, block_migration=block_migration) + + def wait_for_delete(self, res, interval=2, wait=120): + """Wait for a resource to be deleted. + + :param res: The resource to wait on to be deleted. + :type resource: A :class:`~openstack.resource.Resource` object. + :param interval: Number of seconds to wait before to consecutive + checks. Default to 2. + :param wait: Maximum number of seconds to wait before the change. + Default to 120. + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete(self, res, interval, wait) diff --git a/openstack/config/vendors/dreamhost.json b/openstack/config/vendors/dreamhost.json deleted file mode 100644 index ea2ebac1e..000000000 --- a/openstack/config/vendors/dreamhost.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "dreamhost", - "profile": { - "status": "deprecated", - "message": "The dreamhost profile is deprecated. Please use the dreamcompute profile instead", - "auth": { - "auth_url": "https://keystone.dream.io" - }, - "identity_api_version": "3", - "region_name": "RegionOne", - "image_format": "raw" - } -} diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 538968fd9..77cc26dde 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -53,7 +53,6 @@ from openstack.network.v2 import trunk as _trunk from openstack.network.v2 import vpn_service as _vpn_service from openstack import proxy -from openstack import utils class Proxy(proxy.Proxy): @@ -2870,34 +2869,6 @@ def update_security_group(self, security_group, **attrs): return self._update(_security_group.SecurityGroup, security_group, **attrs) - @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", - details="See the Network user guide for an example") - def security_group_open_port(self, sgid, port, protocol='tcp'): - rule = { - 'direction': 'ingress', - 'remote_ip_prefix': '0.0.0.0/0', - 'protocol': protocol, - 'port_range_max': port, - 'port_range_min': port, - 'security_group_id': sgid, - 'ethertype': 'IPv4' - } - return self.create_security_group_rule(**rule) - - @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", - details="See the Network user guide for an example") - def security_group_allow_ping(self, sgid): - rule = { - 'direction': 'ingress', - 'remote_ip_prefix': '0.0.0.0/0', - 'protocol': 'icmp', - 'port_range_max': None, - 'port_range_min': None, - 'security_group_id': sgid, - 'ethertype': 'IPv4' - } - return self.create_security_group_rule(**rule) - def create_security_group_rule(self, **attrs): """Create a new security group rule from attributes diff --git a/openstack/proxy.py b/openstack/proxy.py index cc6a751ec..ea3f15f12 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -16,7 +16,6 @@ from openstack._meta import proxy as _meta from openstack import exceptions from openstack import resource -from openstack import utils # The _check_resource decorator is used on Proxy methods to ensure that @@ -263,61 +262,3 @@ def _head(self, resource_type, value=None, **attrs): """ res = self._get_resource(resource_type, value, **attrs) return res.head(self) - - @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", - details=("This is no longer a part of the proxy base, " - "service-specific subclasses should expose " - "this as needed. See resource.wait_for_status " - "for this behavior")) - def wait_for_status(self, value, status, failures=None, interval=2, - wait=120): - """Wait for a resource to be in a particular status. - - :param value: The resource to wait on to reach the status. The - resource must have a status attribute. - :type value: :class:`~openstack.resource.Resource` - :param status: Desired status of the resource. - :param list failures: Statuses that would indicate the transition - failed such as 'ERROR'. - :param interval: Number of seconds to wait between checks. - :param wait: Maximum number of seconds to wait for the change. - - :return: Method returns resource on success. - :raises: :class:`~openstack.exceptions.ResourceTimeout` transition - to status failed to occur in wait seconds. - :raises: :class:`~openstack.exceptions.ResourceFailure` resource - transitioned to one of the failure states. - :raises: :class:`~AttributeError` if the resource does not have a - status attribute - """ - failures = [] if failures is None else failures - return resource.wait_for_status( - self, value, status, failures, interval, wait) - - @utils.deprecated(deprecated_in="0.9.14", removed_in="1.0", - details=("This is no longer a part of the proxy base, " - "service-specific subclasses should expose " - "this as needed. See resource.wait_for_delete " - "for this behavior")) - def wait_for_delete(self, value, interval=2, wait=120): - """Wait for the resource to be deleted. - - :param value: The resource to wait on to be deleted. - :type value: :class:`~openstack.resource.Resource` - :param interval: Number of seconds to wait between checks. - :param wait: Maximum number of seconds to wait for the delete. - - :return: Method returns resource on success. - :raises: :class:`~openstack.exceptions.ResourceTimeout` transition - to delete failed to occur in wait seconds. - """ - return resource.wait_for_delete(self, value, interval, wait) - - -class BaseProxy(Proxy): - # Backwards compat wrapper - - @utils.deprecated(deprecated_in="0.11.1", removed_in="1.0", - details="Use openstack.proxy.Proxy instead") - def __init__(self, *args, **kwargs): - super(BaseProxy, self).__init__(*args, **kwargs) diff --git a/openstack/proxy2.py b/openstack/proxy2.py deleted file mode 100644 index ded668269..000000000 --- a/openstack/proxy2.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2018 Red Hat, 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. - -from openstack import proxy -from openstack import utils - - -class Proxy(proxy.Proxy): - - @utils.deprecated(deprecated_in="0.10.0", removed_in="1.0", - details="openstack.proxy2 is now openstack.proxy") - def __init__(self, *args, **kwargs): - super(Proxy, self).__init__(*args, **kwargs) diff --git a/openstack/resource2.py b/openstack/resource2.py deleted file mode 100644 index 312c832a3..000000000 --- a/openstack/resource2.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2018 Red Hat, 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. - -from openstack import resource -from openstack import utils - - -class Resource(resource.Resource): - - @utils.deprecated(deprecated_in="0.10.0", removed_in="1.0", - details="openstack.resource2 is now openstack.resource") - def __init__(self, *args, **kwargs): - super(Resource, self).__init__(*args, **kwargs) diff --git a/openstack/tests/unit/baremetal/v1/test_proxy.py b/openstack/tests/unit/baremetal/v1/test_proxy.py index 5193078c5..ce23ada20 100644 --- a/openstack/tests/unit/baremetal/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal/v1/test_proxy.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import deprecation import mock from openstack.baremetal.v1 import _proxy @@ -18,7 +17,6 @@ from openstack.baremetal.v1 import driver from openstack.baremetal.v1 import node from openstack.baremetal.v1 import port -from openstack.baremetal.v1 import port_group from openstack import exceptions from openstack.tests.unit import base from openstack.tests.unit import test_proxy_base @@ -126,46 +124,6 @@ def test_delete_port(self): def test_delete_port_ignore(self): self.verify_delete(self.proxy.delete_port, port.Port, True) - @deprecation.fail_if_not_removed - def test_portgroups_detailed(self): - self.verify_list(self.proxy.portgroups, port_group.PortGroupDetail, - paginated=True, - method_kwargs={"details": True, "query": 1}, - expected_kwargs={"query": 1}) - - @deprecation.fail_if_not_removed - def test_portgroups_not_detailed(self): - self.verify_list(self.proxy.portgroups, port_group.PortGroup, - paginated=True, - method_kwargs={"details": False, "query": 1}, - expected_kwargs={"query": 1}) - - @deprecation.fail_if_not_removed - def test_create_portgroup(self): - self.verify_create(self.proxy.create_portgroup, port_group.PortGroup) - - @deprecation.fail_if_not_removed - def test_find_portgroup(self): - self.verify_find(self.proxy.find_portgroup, port_group.PortGroup) - - @deprecation.fail_if_not_removed - def test_get_portgroup(self): - self.verify_get(self.proxy.get_portgroup, port_group.PortGroup) - - @deprecation.fail_if_not_removed - def test_update_portgroup(self): - self.verify_update(self.proxy.update_portgroup, port_group.PortGroup) - - @deprecation.fail_if_not_removed - def test_delete_portgroup(self): - self.verify_delete(self.proxy.delete_portgroup, port_group.PortGroup, - False) - - @deprecation.fail_if_not_removed - def test_delete_portgroup_ignore(self): - self.verify_delete(self.proxy.delete_portgroup, port_group.PortGroup, - True) - @mock.patch('time.sleep', lambda _sec: None) @mock.patch.object(_proxy.Proxy, 'get_node', autospec=True) diff --git a/openstack/tests/unit/clustering/v1/test_proxy.py b/openstack/tests/unit/clustering/v1/test_proxy.py index 6d8118100..8095b1156 100644 --- a/openstack/tests/unit/clustering/v1/test_proxy.py +++ b/openstack/tests/unit/clustering/v1/test_proxy.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import deprecation import mock from openstack.clustering.v1 import _proxy @@ -114,201 +113,31 @@ def test_clusters(self): def test_cluster_update(self): self.verify_update(self.proxy.update_cluster, cluster.Cluster) - @deprecation.fail_if_not_removed - @mock.patch.object(proxy_base.Proxy, '_find') - def test_cluster_add_nodes(self, mock_find): - mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') - mock_find.return_value = mock_cluster - self._verify("openstack.clustering.v1.cluster.Cluster.add_nodes", - self.proxy.cluster_add_nodes, - method_args=["FAKE_CLUSTER", ["node1"]], - expected_args=[["node1"]]) - mock_find.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER", - ignore_missing=False) - - @deprecation.fail_if_not_removed - def test_cluster_add_nodes_with_obj(self): - mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') - self._verify("openstack.clustering.v1.cluster.Cluster.add_nodes", - self.proxy.cluster_add_nodes, - method_args=[mock_cluster, ["node1"]], - expected_args=[["node1"]]) - - @deprecation.fail_if_not_removed - @mock.patch.object(proxy_base.Proxy, '_find') - def test_cluster_del_nodes(self, mock_find): - mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') - mock_find.return_value = mock_cluster - self._verify("openstack.clustering.v1.cluster.Cluster.del_nodes", - self.proxy.cluster_del_nodes, - method_args=["FAKE_CLUSTER", ["node1"]], - expected_args=[["node1"]]) - mock_find.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER", - ignore_missing=False) - - @deprecation.fail_if_not_removed - def test_cluster_del_nodes_with_obj(self): - mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') - self._verify("openstack.clustering.v1.cluster.Cluster.del_nodes", - self.proxy.cluster_del_nodes, - method_args=[mock_cluster, ["node1"]], - method_kwargs={"key": "value"}, - expected_args=[["node1"]], - expected_kwargs={"key": "value"}) - - @deprecation.fail_if_not_removed - @mock.patch.object(proxy_base.Proxy, '_find') - def test_cluster_replace_nodes(self, mock_find): - mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') - mock_find.return_value = mock_cluster - self._verify("openstack.clustering.v1.cluster.Cluster.replace_nodes", - self.proxy.cluster_replace_nodes, - method_args=["FAKE_CLUSTER", {"node1": "node2"}], - expected_args=[{"node1": "node2"}]) - mock_find.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER", - ignore_missing=False) - - @deprecation.fail_if_not_removed - def test_cluster_replace_nodes_with_obj(self): - mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') - self._verify("openstack.clustering.v1.cluster.Cluster.replace_nodes", - self.proxy.cluster_replace_nodes, - method_args=[mock_cluster, {"node1": "node2"}], - expected_args=[{"node1": "node2"}]) - - @deprecation.fail_if_not_removed - @mock.patch.object(proxy_base.Proxy, '_find') - def test_cluster_scale_out(self, mock_find): - mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') - mock_find.return_value = mock_cluster - self._verify("openstack.clustering.v1.cluster.Cluster.scale_out", - self.proxy.cluster_scale_out, - method_args=["FAKE_CLUSTER", 3], - expected_args=[3]) - mock_find.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER", - ignore_missing=False) - - @deprecation.fail_if_not_removed - def test_cluster_scale_out_with_obj(self): - mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') - self._verify("openstack.clustering.v1.cluster.Cluster.scale_out", - self.proxy.cluster_scale_out, - method_args=[mock_cluster, 5], - expected_args=[5]) - - @deprecation.fail_if_not_removed - @mock.patch.object(proxy_base.Proxy, '_find') - def test_cluster_scale_in(self, mock_find): - mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') - mock_find.return_value = mock_cluster - self._verify("openstack.clustering.v1.cluster.Cluster.scale_in", - self.proxy.cluster_scale_in, - method_args=["FAKE_CLUSTER", 3], - expected_args=[3]) - mock_find.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER", - ignore_missing=False) - - @deprecation.fail_if_not_removed - def test_cluster_scale_in_with_obj(self): - mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') - self._verify("openstack.clustering.v1.cluster.Cluster.scale_in", - self.proxy.cluster_scale_in, - method_args=[mock_cluster, 5], - expected_args=[5]) - def test_services(self): self.verify_list(self.proxy.services, service.Service, paginated=False) @mock.patch.object(proxy_base.Proxy, '_find') - def test_cluster_resize(self, mock_find): + def test_resize_cluster(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_find.return_value = mock_cluster self._verify("openstack.clustering.v1.cluster.Cluster.resize", - self.proxy.cluster_resize, + self.proxy.resize_cluster, method_args=["FAKE_CLUSTER"], method_kwargs={'k1': 'v1', 'k2': 'v2'}, expected_kwargs={'k1': 'v1', 'k2': 'v2'}) mock_find.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER", ignore_missing=False) - def test_cluster_resize_with_obj(self): + def test_resize_cluster_with_obj(self): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') self._verify("openstack.clustering.v1.cluster.Cluster.resize", - self.proxy.cluster_resize, + self.proxy.resize_cluster, method_args=[mock_cluster], method_kwargs={'k1': 'v1', 'k2': 'v2'}, expected_kwargs={'k1': 'v1', 'k2': 'v2'}) - @deprecation.fail_if_not_removed - @mock.patch.object(proxy_base.Proxy, '_find') - def test_cluster_attach_policy(self, mock_find): - mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') - mock_find.return_value = mock_cluster - self._verify("openstack.clustering.v1.cluster.Cluster.policy_attach", - self.proxy.cluster_attach_policy, - method_args=["FAKE_CLUSTER", "FAKE_POLICY"], - method_kwargs={"k1": "v1", "k2": "v2"}, - expected_args=["FAKE_POLICY"], - expected_kwargs={"k1": "v1", 'k2': "v2"}) - mock_find.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER", - ignore_missing=False) - - @deprecation.fail_if_not_removed - def test_cluster_attach_policy_with_obj(self): - mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') - self._verify("openstack.clustering.v1.cluster.Cluster.policy_attach", - self.proxy.cluster_attach_policy, - method_args=[mock_cluster, "FAKE_POLICY"], - method_kwargs={"k1": "v1", "k2": "v2"}, - expected_args=["FAKE_POLICY"], - expected_kwargs={"k1": "v1", 'k2': "v2"}) - - @deprecation.fail_if_not_removed - @mock.patch.object(proxy_base.Proxy, '_find') - def test_cluster_detach_policy(self, mock_find): - mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') - mock_find.return_value = mock_cluster - self._verify("openstack.clustering.v1.cluster.Cluster.policy_detach", - self.proxy.cluster_detach_policy, - method_args=["FAKE_CLUSTER", "FAKE_POLICY"], - expected_args=["FAKE_POLICY"]) - mock_find.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER", - ignore_missing=False) - - @deprecation.fail_if_not_removed - def test_cluster_detach_policy_with_obj(self): - mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') - self._verify("openstack.clustering.v1.cluster.Cluster.policy_detach", - self.proxy.cluster_detach_policy, - method_args=[mock_cluster, "FAKE_POLICY"], - expected_args=["FAKE_POLICY"]) - - @deprecation.fail_if_not_removed - @mock.patch.object(proxy_base.Proxy, '_find') - def test_cluster_update_policy(self, mock_find): - mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') - mock_find.return_value = mock_cluster - self._verify("openstack.clustering.v1.cluster.Cluster.policy_update", - self.proxy.cluster_update_policy, - method_args=["FAKE_CLUSTER", "FAKE_POLICY"], - method_kwargs={"k1": "v1", "k2": "v2"}, - expected_args=["FAKE_POLICY"], - expected_kwargs={"k1": "v1", 'k2': "v2"}) - mock_find.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER", - ignore_missing=False) - - @deprecation.fail_if_not_removed - def test_cluster_update_policy_with_obj(self): - mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') - self._verify("openstack.clustering.v1.cluster.Cluster.policy_update", - self.proxy.cluster_update_policy, - method_args=[mock_cluster, "FAKE_POLICY"], - method_kwargs={"k1": "v1", "k2": "v2"}, - expected_args=["FAKE_POLICY"], - expected_kwargs={"k1": "v1", 'k2': "v2"}) - def test_collect_cluster_attrs(self): self.verify_list(self.proxy.collect_cluster_attrs, cluster_attr.ClusterAttr, paginated=False, @@ -334,17 +163,6 @@ def test_cluster_recover(self, mock_get): method_args=["FAKE_CLUSTER"]) mock_get.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER") - @deprecation.fail_if_not_removed - @mock.patch.object(proxy_base.Proxy, '_get_resource') - def test_cluster_operation(self, mock_get): - mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') - mock_get.return_value = mock_cluster - self._verify("openstack.clustering.v1.cluster.Cluster.op", - self.proxy.cluster_operation, - method_args=["FAKE_CLUSTER", "dance"], - expected_args=["dance"]) - mock_get.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER") - def test_node_create(self): self.verify_create(self.proxy.create_node, node.Node) @@ -423,17 +241,6 @@ def test_node_adopt_preview(self, mock_get): mock_get.assert_called_once_with(node.Node, None) - @deprecation.fail_if_not_removed - @mock.patch.object(proxy_base.Proxy, '_get_resource') - def test_node_operation(self, mock_get): - mock_node = node.Node.new(id='FAKE_CLUSTER') - mock_get.return_value = mock_node - self._verify("openstack.clustering.v1.node.Node.op", - self.proxy.node_operation, - method_args=["FAKE_NODE", "dance"], - expected_args=["dance"]) - mock_get.assert_called_once_with(node.Node, "FAKE_NODE") - def test_policy_create(self): self.verify_create(self.proxy.create_policy, policy.Policy) @@ -576,15 +383,3 @@ def test_wait_for_delete_params(self, mock_wait): self.proxy.wait_for_delete(mock_resource, 1, 2) mock_wait.assert_called_once_with(self.proxy, mock_resource, 1, 2) - - @deprecation.fail_if_not_removed - @mock.patch.object(proxy_base.Proxy, '_get_resource') - def test_profile_type_ops(self, mock_get): - mock_profile = profile_type.ProfileType.new(id='FAKE_PROFILE') - mock_get.return_value = mock_profile - self._verify( - "openstack.clustering.v1.profile_type.ProfileType.type_ops", - self.proxy.list_profile_type_operations, - method_args=["FAKE_PROFILE"]) - mock_get.assert_called_once_with(profile_type.ProfileType, - "FAKE_PROFILE") diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 600596f12..5f5bef458 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import deprecation import mock import uuid @@ -992,49 +991,6 @@ def test_security_group_update(self): self.verify_update(self.proxy.update_security_group, security_group.SecurityGroup) - @deprecation.fail_if_not_removed - def test_security_group_open_port(self): - mock_class = 'openstack.network.v2._proxy.Proxy' - mock_method = mock_class + '.create_security_group_rule' - expected_result = 'result' - sgid = 1 - port = 2 - with mock.patch(mock_method) as mocked: - mocked.return_value = expected_result - actual = self.proxy.security_group_open_port(sgid, port) - self.assertEqual(expected_result, actual) - expected_args = { - 'direction': 'ingress', - 'protocol': 'tcp', - 'remote_ip_prefix': '0.0.0.0/0', - 'port_range_max': port, - 'security_group_id': sgid, - 'port_range_min': port, - 'ethertype': 'IPv4', - } - mocked.assert_called_with(**expected_args) - - @deprecation.fail_if_not_removed - def test_security_group_allow_ping(self): - mock_class = 'openstack.network.v2._proxy.Proxy' - mock_method = mock_class + '.create_security_group_rule' - expected_result = 'result' - sgid = 1 - with mock.patch(mock_method) as mocked: - mocked.return_value = expected_result - actual = self.proxy.security_group_allow_ping(sgid) - self.assertEqual(expected_result, actual) - expected_args = { - 'direction': 'ingress', - 'protocol': 'icmp', - 'remote_ip_prefix': '0.0.0.0/0', - 'port_range_max': None, - 'security_group_id': sgid, - 'port_range_min': None, - 'ethertype': 'IPv4', - } - mocked.assert_called_with(**expected_args) - def test_security_group_rule_create_attrs(self): self.verify_create(self.proxy.create_security_group_rule, security_group_rule.SecurityGroupRule) diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index c30872a80..c03759d07 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -397,42 +397,3 @@ def test_head_id(self): HeadableResource.new.assert_called_with(id=self.fake_id) self.res.head.assert_called_with(self.sot) self.assertEqual(rv, self.fake_result) - - -class TestProxyWaits(base.TestCase): - - def setUp(self): - super(TestProxyWaits, self).setUp() - - self.session = mock.Mock() - self.sot = proxy.Proxy(self.session) - - @mock.patch("openstack.resource.wait_for_status") - def test_wait_for(self, mock_wait): - mock_resource = mock.Mock() - mock_wait.return_value = mock_resource - self.sot.wait_for_status(mock_resource, 'ACTIVE') - mock_wait.assert_called_once_with( - self.sot, mock_resource, 'ACTIVE', [], 2, 120) - - @mock.patch("openstack.resource.wait_for_status") - def test_wait_for_params(self, mock_wait): - mock_resource = mock.Mock() - mock_wait.return_value = mock_resource - self.sot.wait_for_status(mock_resource, 'ACTIVE', ['ERROR'], 1, 2) - mock_wait.assert_called_once_with( - self.sot, mock_resource, 'ACTIVE', ['ERROR'], 1, 2) - - @mock.patch("openstack.resource.wait_for_delete") - def test_wait_for_delete(self, mock_wait): - mock_resource = mock.Mock() - mock_wait.return_value = mock_resource - self.sot.wait_for_delete(mock_resource) - mock_wait.assert_called_once_with(self.sot, mock_resource, 2, 120) - - @mock.patch("openstack.resource.wait_for_delete") - def test_wait_for_delete_params(self, mock_wait): - mock_resource = mock.Mock() - mock_wait.return_value = mock_resource - self.sot.wait_for_delete(mock_resource, 1, 2) - mock_wait.assert_called_once_with(self.sot, mock_resource, 1, 2) diff --git a/openstack/tests/unit/test_utils.py b/openstack/tests/unit/test_utils.py index c4fbf8b97..42821906c 100644 --- a/openstack/tests/unit/test_utils.py +++ b/openstack/tests/unit/test_utils.py @@ -17,6 +17,7 @@ import fixtures +import openstack from openstack import utils @@ -53,7 +54,7 @@ def setUp(self): def _console_tests(self, level, debug, stream): - utils.enable_logging(debug=debug, stream=stream) + openstack.enable_logging(debug=debug, stream=stream) self.assertEqual(self.openstack_logger.addHandler.call_count, 1) self.openstack_logger.setLevel.assert_called_with(level) @@ -64,14 +65,14 @@ def _file_tests(self, level, debug): fixtures.MonkeyPatch('logging.FileHandler', file_handler)) fake_path = "fake/path.log" - utils.enable_logging(debug=debug, path=fake_path) + openstack.enable_logging(debug=debug, path=fake_path) file_handler.assert_called_with(fake_path) self.assertEqual(self.openstack_logger.addHandler.call_count, 1) self.openstack_logger.setLevel.assert_called_with(level) def test_none(self): - utils.enable_logging(debug=True) + openstack.enable_logging(debug=True) self.fake_get_logger.assert_has_calls([]) self.openstack_logger.setLevel.assert_called_with(logging.DEBUG) self.assertEqual(self.openstack_logger.addHandler.call_count, 1) diff --git a/openstack/utils.py b/openstack/utils.py index 196ebec20..a3c8c908d 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -10,54 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. -import functools import string import time -import deprecation import keystoneauth1 from keystoneauth1 import discover from openstack import _log from openstack import exceptions -from openstack import version - - -def deprecated(deprecated_in=None, removed_in=None, - details=""): - """Mark a method as deprecated - - :param deprecated_in: The version string where this method is deprecated. - Generally this is the next version to be released. - :param removed_in: The version where this method will be removed - from the code base. Generally this is the next - major version. This argument is helpful for the - tests when using ``deprecation.fail_if_not_removed``. - :param str details: Helpful details to callers and the documentation. - This will usually be a recommendation for alternate - code to use. - """ - # As all deprecations within this library have the same current_version, - # return a partial function with the library version always set. - partial = functools.partial(deprecation.deprecated, - current_version=version.__version__) - - # TODO(shade) shade's tags break these - so hard override them for now. - # We'll want a patch fixing this before we cut any releases. - removed_in = '2.0.0' - return partial(deprecated_in=deprecated_in, removed_in=removed_in, - details=details) - - -@deprecated(deprecated_in="0.10.0", removed_in="1.0", - details="Use openstack.enable_logging instead") -def enable_logging(*args, **kwargs): - """Backwards compatibility wrapper function. - - openstacksdk has had enable_logging in utils. It's in _log now and - exposed directly at openstack.enable_logging. - """ - return _log.enable_logging(*args, **kwargs) def urljoin(*args): diff --git a/releasenotes/notes/removed-deprecated-things-8700fe3592c3bf18.yaml b/releasenotes/notes/removed-deprecated-things-8700fe3592c3bf18.yaml new file mode 100644 index 000000000..120c7d1cb --- /dev/null +++ b/releasenotes/notes/removed-deprecated-things-8700fe3592c3bf18.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + In anticipation of the upcoming 1.0 release, all the things that have been + marked as deprecated have been removed. diff --git a/requirements.txt b/requirements.txt index 05d76f21a..78f758519 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,6 @@ jsonpatch!=1.20,>=1.16 # BSD six>=1.10.0 # MIT os-service-types>=1.2.0 # Apache-2.0 keystoneauth1>=3.11.0 # Apache-2.0 -deprecation>=1.0 # Apache-2.0 munch>=2.1.0 # MIT decorator>=3.4.0 # BSD From 877326aa7365da883486f0010bc9a8c43546d820 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 5 Oct 2018 11:08:59 -0500 Subject: [PATCH 2232/3836] Start shifting cloud object-store methods to proxy This doesn't do all of them because the task manager patches will merge conflict if we do. But it gets the ball rolling. Also, adds a skip_discovery attribute that can be set to avoid running discovery for services, like object-store, for which that's never the right choice. Change-Id: I01d19119288cdd580b354b648f35b116818c349f --- openstack/cloud/openstackcloud.py | 31 +++++++++++++++-------------- openstack/object_store/v1/_proxy.py | 2 ++ openstack/service_description.py | 7 +++++++ 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index f9e7c20e0..5102f27b9 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -7396,8 +7396,8 @@ def list_containers(self, full_listing=True): :raises: OpenStackCloudException on operation error. """ - data = self._object_store_client.get('/', params=dict(format='json')) - return self._get_and_munchify(None, data) + response = self.object_store.get('/', params=dict(format='json')) + return self._get_and_munchify(None, _adapter._json_response(response)) def search_containers(self, name=None, filters=None): """Search containers. @@ -7427,8 +7427,9 @@ def get_container(self, name, skip_cache=False): """ if skip_cache or name not in self._container_cache: try: - container = self._object_store_client.head(name) - self._container_cache[name] = container.headers + response = self.object_store.head(name) + exceptions.raise_from_response(response) + self._container_cache[name] = response.headers except exc.OpenStackCloudHTTPError as e: if e.response.status_code == 404: return None @@ -7446,7 +7447,7 @@ def create_container(self, name, public=False): container = self.get_container(name) if container: return container - self._object_store_client.put(name) + exceptions.raise_from_response(self.object_store.put(name)) if public: self.set_container_access(name, 'public') return self.get_container(name, skip_cache=True) @@ -7457,7 +7458,7 @@ def delete_container(self, name): :param str name: Name of the container to delete. """ try: - self._object_store_client.delete(name) + exceptions.raise_from_response(self.object_store.delete(name)) self._container_cache.pop(name, None) return True except exc.OpenStackCloudHTTPError as e: @@ -7486,7 +7487,8 @@ def update_container(self, name, headers): :param dict headers: Key/Value headers to set on the container. """ - self._object_store_client.post(name, headers=headers) + exceptions.raise_from_response( + self.object_store.post(name, headers=headers)) def set_container_access(self, name, access): """Set the access control list on a container. @@ -7556,12 +7558,11 @@ def get_object_capabilities(self): # The endpoint in the catalog has version and project-id in it # To get capabilities, we have to disassemble and reassemble the URL # This logic is taken from swiftclient - endpoint = urllib.parse.urlparse( - self._object_store_client.get_endpoint()) + endpoint = urllib.parse.urlparse(self.object_store.get_endpoint()) url = "{scheme}://{netloc}/info".format( scheme=endpoint.scheme, netloc=endpoint.netloc) - return self._object_store_client.get(url) + return _adapter._json_response(self.object_store.get(url)) def get_object_segment_size(self, segment_size): """Get a segment size that will work given capabilities""" @@ -7764,12 +7765,12 @@ def create_object( file_size, segment_size, use_slo) def _upload_object_data(self, endpoint, data, headers): - return self._object_store_client.put( - endpoint, headers=headers, data=data) + return _adapter._json_response(self.object_store.put( + endpoint, headers=headers, data=data)) def _upload_object(self, endpoint, filename, headers): - return self._object_store_client.put( - endpoint, headers=headers, data=open(filename, 'r')) + return _adapter._json_response(self.object_store.put( + endpoint, headers=headers, data=open(filename, 'r'))) def _get_file_segments(self, endpoint, filename, file_size, segment_size): # Use an ordered dict here so that testing can replicate things @@ -7789,7 +7790,7 @@ def _object_name_from_url(self, url): Remove the Swift endpoint from the front of the URL, and remove the leaving / that will leave behind.''' - endpoint = self._object_store_client.get_endpoint() + endpoint = self.object_store.get_endpoint() object_name = url.replace(endpoint, '') if object_name.startswith('/'): object_name = object_name[1:] diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index 16e77a57c..fef003eb5 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -18,6 +18,8 @@ class Proxy(proxy.Proxy): + skip_discovery = True + Account = _account.Account Container = _container.Container Object = _obj.Object diff --git a/openstack/service_description.py b/openstack/service_description.py index a692d261f..4ba5f5eb2 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -118,6 +118,13 @@ def _make_proxy(self, instance, owner): if proxy_obj: + if getattr(proxy_obj, 'skip_discovery', False): + # Some services, like swift, don't have discovery. While + # keystoneauth will behave correctly and handle such + # scenarios, it's not super efficient as it involves trying + # and falling back a few times. + return proxy_obj + data = proxy_obj.get_endpoint_data() if data.catalog_url != data.service_url: ep_key = '{service_type}_endpoint_override'.format( From 1370553e7320649345d173a9b3e66bde71efaf62 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 5 Oct 2018 12:28:25 -0500 Subject: [PATCH 2233/3836] Make it clear that OpenStackCloud is a mixin It's not clear that this is a mixin. Make it clear. There was also a documentation page that referenced it like it was a separate thing. Remove that, since it's covered in the Connection docs. Change-Id: I13bbf5155f7ee2aabe78ec93eb22175ec2403cfb --- doc/source/user/index.rst | 1 - doc/source/user/usage.rst | 19 ------- openstack/cloud/__init__.py | 39 -------------- openstack/cloud/openstackcloud.py | 2 +- openstack/connection.py | 10 ++-- openstack/tests/unit/cloud/test_caching.py | 2 +- .../tests/unit/cloud/test_create_server.py | 14 ++--- .../unit/cloud/test_floating_ip_common.py | 34 ++++++------ openstack/tests/unit/cloud/test_meta.py | 54 +++++++++---------- openstack/tests/unit/cloud/test_shade.py | 9 ++-- 10 files changed, 64 insertions(+), 120 deletions(-) delete mode 100644 doc/source/user/usage.rst diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index d0eeb6b65..8e66c897b 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -28,7 +28,6 @@ approach, this is where you'll want to begin. Configuration Connect to an OpenStack Cloud Connect to an OpenStack Cloud Using a Config File - Using Cloud Abstration Layer Logging Microversions Baremetal diff --git a/doc/source/user/usage.rst b/doc/source/user/usage.rst deleted file mode 100644 index 69bb36777..000000000 --- a/doc/source/user/usage.rst +++ /dev/null @@ -1,19 +0,0 @@ -===== -Usage -===== - -To use `openstack.cloud` in a project: - -.. code-block:: python - - import openstack.cloud - -.. note:: - API methods that return a description of an OpenStack resource (e.g., - server instance, image, volume, etc.) do so using a `munch.Munch` object - from the `Munch library `_. `Munch` - objects can be accessed using either dictionary or object notation - (e.g., ``server.id``, ``image.name`` and ``server['id']``, ``image['name']``) - -.. autoclass:: openstack.cloud.OpenStackCloud - :members: diff --git a/openstack/cloud/__init__.py b/openstack/cloud/__init__.py index 9aefbf6c5..b8472ea90 100644 --- a/openstack/cloud/__init__.py +++ b/openstack/cloud/__init__.py @@ -12,44 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -import keystoneauth1.exceptions -from openstack._log import enable_logging # noqa from openstack.cloud.exc import * # noqa -from openstack.cloud import exc -from openstack.cloud.openstackcloud import OpenStackCloud - - -def _get_openstack_config(app_name=None, app_version=None): - import openstack.config - return openstack.config.OpenStackConfig( - app_name=app_name, app_version=app_version) - - -# TODO(shade) This wants to be remove before we make a release. -def openstack_clouds( - config=None, debug=False, cloud=None, strict=False, - app_name=None, app_version=None): - if not config: - config = _get_openstack_config(app_name, app_version) - try: - if cloud is None: - return [ - OpenStackCloud( - cloud=cloud_region.name, debug=debug, - cloud_config=cloud_region, - strict=strict) - for cloud_region in config.get_all() - ] - else: - return [ - OpenStackCloud( - cloud=cloud_region.name, debug=debug, - cloud_config=cloud_region, - strict=strict) - for cloud_region in config.get_all() - if cloud_region.name == cloud - ] - except keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin as e: - raise exc.OpenStackCloudException( - "Invalid cloud configuration: {exc}".format(exc=str(e))) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 5102f27b9..0da232d99 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -98,7 +98,7 @@ def _no_pending_stacks(stacks): return True -class OpenStackCloud(_normalize.Normalizer): +class _OpenStackCloudMixin(_normalize.Normalizer): """Represent a connection to an OpenStack Cloud. OpenStackCloud is the entry point for all cloud operations, regardless diff --git a/openstack/connection.py b/openstack/connection.py index 3459d3a86..5cd828dc1 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -163,7 +163,7 @@ from openstack import _log from openstack._meta import connection as _meta -from openstack import cloud as _cloud +from openstack.cloud import openstackcloud as _cloud from openstack import config as _config from openstack.config import cloud_region from openstack import exceptions @@ -212,7 +212,7 @@ def from_config(cloud=None, config=None, options=None, **kwargs): class Connection(six.with_metaclass(_meta.ConnectionMeta, - _cloud.OpenStackCloud)): + _cloud._OpenStackCloudMixin)): def __init__(self, cloud=None, config=None, session=None, app_name=None, app_version=None, @@ -295,9 +295,9 @@ def __init__(self, cloud=None, config=None, session=None, self._proxies = {} self.use_direct_get = use_direct_get self.strict_mode = strict - # Call the OpenStackCloud constructor while we work on integrating - # things better. - _cloud.OpenStackCloud.__init__(self) + # Call the _OpenStackCloudMixin constructor while we work on + # integrating things better. + _cloud._OpenStackCloudMixin.__init__(self) @property def session(self): diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py index 9e10fa442..54ed32542 100644 --- a/openstack/tests/unit/cloud/test_caching.py +++ b/openstack/tests/unit/cloud/test_caching.py @@ -104,7 +104,7 @@ def _munch_images(self, fake_image): return self.cloud._normalize_images([fake_image]) def test_openstack_cloud(self): - self.assertIsInstance(self.cloud, openstack.cloud.OpenStackCloud) + self.assertIsInstance(self.cloud, openstack.connection.Connection) def test_list_projects_v3(self): project_one = self._get_project_data() diff --git a/openstack/tests/unit/cloud/test_create_server.py b/openstack/tests/unit/cloud/test_create_server.py index 2de7073ed..616d45eb3 100644 --- a/openstack/tests/unit/cloud/test_create_server.py +++ b/openstack/tests/unit/cloud/test_create_server.py @@ -21,7 +21,7 @@ import mock -import openstack.cloud +from openstack.cloud import openstackcloud from openstack.cloud import exc from openstack.cloud import meta from openstack.tests import fakes @@ -325,7 +325,7 @@ def test_create_server_with_admin_pass_no_wait(self): self.assert_calls() - @mock.patch.object(openstack.cloud.OpenStackCloud, "wait_for_server") + @mock.patch.object(openstackcloud._OpenStackCloudMixin, "wait_for_server") def test_create_server_with_admin_pass_wait(self, mock_wait): """ Test that a server with an admin_pass passed returns the password @@ -411,8 +411,9 @@ def test_create_server_user_data_base64(self): self.assert_calls() - @mock.patch.object(openstack.cloud.OpenStackCloud, "get_active_server") - @mock.patch.object(openstack.cloud.OpenStackCloud, "get_server") + @mock.patch.object( + openstackcloud._OpenStackCloudMixin, "get_active_server") + @mock.patch.object(openstackcloud._OpenStackCloudMixin, "get_server") def test_wait_for_server(self, mock_get_server, mock_get_active_server): """ Test that waiting for a server returns the server instance when @@ -446,7 +447,7 @@ def test_wait_for_server(self, mock_get_server, mock_get_active_server): self.assertEqual('ACTIVE', server['status']) - @mock.patch.object(openstack.cloud.OpenStackCloud, 'wait_for_server') + @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'wait_for_server') def test_create_server_wait(self, mock_wait): """ Test that create_server with a wait actually does the wait. @@ -483,7 +484,8 @@ def test_create_server_wait(self, mock_wait): ) self.assert_calls() - @mock.patch.object(openstack.cloud.OpenStackCloud, 'add_ips_to_server') + @mock.patch.object( + openstackcloud._OpenStackCloudMixin, 'add_ips_to_server') def test_create_server_no_addresses( self, mock_add_ips_to_server): """ diff --git a/openstack/tests/unit/cloud/test_floating_ip_common.py b/openstack/tests/unit/cloud/test_floating_ip_common.py index e7c20f923..c17e81636 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_common.py +++ b/openstack/tests/unit/cloud/test_floating_ip_common.py @@ -22,16 +22,16 @@ from mock import patch from openstack.cloud import meta -from openstack.cloud import OpenStackCloud +from openstack.cloud import openstackcloud from openstack.tests import fakes from openstack.tests.unit import base class TestFloatingIP(base.TestCase): - @patch.object(OpenStackCloud, 'get_floating_ip') - @patch.object(OpenStackCloud, '_attach_ip_to_server') - @patch.object(OpenStackCloud, 'available_floating_ip') + @patch.object(openstackcloud._OpenStackCloudMixin, 'get_floating_ip') + @patch.object(openstackcloud._OpenStackCloudMixin, '_attach_ip_to_server') + @patch.object(openstackcloud._OpenStackCloudMixin, 'available_floating_ip') def test_add_auto_ip( self, mock_available_floating_ip, mock_attach_ip_to_server, mock_get_floating_ip): @@ -57,7 +57,7 @@ def test_add_auto_ip( timeout=60, wait=False, server=server_dict, floating_ip=floating_ip_dict, skip_attach=False) - @patch.object(OpenStackCloud, '_add_ip_from_pool') + @patch.object(openstackcloud._OpenStackCloudMixin, '_add_ip_from_pool') def test_add_ips_to_server_pool(self, mock_add_ip_from_pool): server_dict = fakes.make_fake_server( server_id='romeo', name='test-server', status="ACTIVE", @@ -70,9 +70,9 @@ def test_add_ips_to_server_pool(self, mock_add_ip_from_pool): server_dict, pool, reuse=True, wait=False, timeout=60, fixed_address=None, nat_destination=None) - @patch.object(OpenStackCloud, 'has_service') - @patch.object(OpenStackCloud, 'get_floating_ip') - @patch.object(OpenStackCloud, '_add_auto_ip') + @patch.object(openstackcloud._OpenStackCloudMixin, 'has_service') + @patch.object(openstackcloud._OpenStackCloudMixin, 'get_floating_ip') + @patch.object(openstackcloud._OpenStackCloudMixin, '_add_auto_ip') def test_add_ips_to_server_ipv6_only( self, mock_add_auto_ip, mock_get_floating_ip, @@ -109,9 +109,9 @@ def test_add_ips_to_server_ipv6_only( self.assertEqual( new_server['public_v6'], '2001:4800:7819:103:be76:4eff:fe05:8525') - @patch.object(OpenStackCloud, 'has_service') - @patch.object(OpenStackCloud, 'get_floating_ip') - @patch.object(OpenStackCloud, '_add_auto_ip') + @patch.object(openstackcloud._OpenStackCloudMixin, 'has_service') + @patch.object(openstackcloud._OpenStackCloudMixin, 'get_floating_ip') + @patch.object(openstackcloud._OpenStackCloudMixin, '_add_auto_ip') def test_add_ips_to_server_rackspace( self, mock_add_auto_ip, mock_get_floating_ip, @@ -145,9 +145,9 @@ def test_add_ips_to_server_rackspace( new_server['interface_ip'], '2001:4800:7819:103:be76:4eff:fe05:8525') - @patch.object(OpenStackCloud, 'has_service') - @patch.object(OpenStackCloud, 'get_floating_ip') - @patch.object(OpenStackCloud, '_add_auto_ip') + @patch.object(openstackcloud._OpenStackCloudMixin, 'has_service') + @patch.object(openstackcloud._OpenStackCloudMixin, 'get_floating_ip') + @patch.object(openstackcloud._OpenStackCloudMixin, '_add_auto_ip') def test_add_ips_to_server_rackspace_local_ipv4( self, mock_add_auto_ip, mock_get_floating_ip, @@ -179,7 +179,7 @@ def test_add_ips_to_server_rackspace_local_ipv4( mock_add_auto_ip.assert_not_called() self.assertEqual(new_server['interface_ip'], '104.130.246.91') - @patch.object(OpenStackCloud, 'add_ip_list') + @patch.object(openstackcloud._OpenStackCloudMixin, 'add_ip_list') def test_add_ips_to_server_ip_list(self, mock_add_ip_list): server_dict = fakes.make_fake_server( server_id='server-id', name='test-server', status="ACTIVE", @@ -191,8 +191,8 @@ def test_add_ips_to_server_ip_list(self, mock_add_ip_list): mock_add_ip_list.assert_called_with( server_dict, ips, wait=False, timeout=60, fixed_address=None) - @patch.object(OpenStackCloud, '_needs_floating_ip') - @patch.object(OpenStackCloud, '_add_auto_ip') + @patch.object(openstackcloud._OpenStackCloudMixin, '_needs_floating_ip') + @patch.object(openstackcloud._OpenStackCloudMixin, '_add_auto_ip') def test_add_ips_to_server_auto_ip( self, mock_add_auto_ip, mock_needs_floating_ip): server_dict = fakes.make_fake_server( diff --git a/openstack/tests/unit/cloud/test_meta.py b/openstack/tests/unit/cloud/test_meta.py index 5bb38f0a3..f7e82894f 100644 --- a/openstack/tests/unit/cloud/test_meta.py +++ b/openstack/tests/unit/cloud/test_meta.py @@ -14,7 +14,7 @@ import mock -import openstack.cloud +from openstack.cloud import openstackcloud from openstack.cloud import meta from openstack.tests import fakes from openstack.tests.unit import base @@ -352,10 +352,10 @@ def test_get_server_multiple_private_ip(self): '10.0.0.101', meta.get_server_private_ip(srv, self.cloud)) self.assert_calls() - @mock.patch.object(openstack.cloud.OpenStackCloud, 'has_service') - @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_volumes') - @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_image_name') - @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_flavor_name') + @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'has_service') + @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_volumes') + @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_image_name') + @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_flavor_name') def test_get_server_private_ip_devstack( self, mock_get_flavor_name, mock_get_image_name, @@ -417,9 +417,9 @@ def test_get_server_private_ip_devstack( self.assertEqual(PRIVATE_V4, srv['private_v4']) self.assert_calls() - @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_volumes') - @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_image_name') - @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_flavor_name') + @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_volumes') + @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_image_name') + @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_flavor_name') def test_get_server_private_ip_no_fip( self, mock_get_flavor_name, mock_get_image_name, @@ -467,9 +467,9 @@ def test_get_server_private_ip_no_fip( self.assertEqual(PRIVATE_V4, srv['private_v4']) self.assert_calls() - @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_volumes') - @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_image_name') - @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_flavor_name') + @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_volumes') + @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_image_name') + @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_flavor_name') def test_get_server_cloud_no_fips( self, mock_get_flavor_name, mock_get_image_name, @@ -515,10 +515,10 @@ def test_get_server_cloud_no_fips( self.assertEqual(PRIVATE_V4, srv['private_v4']) self.assert_calls() - @mock.patch.object(openstack.cloud.OpenStackCloud, 'has_service') - @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_volumes') - @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_image_name') - @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_flavor_name') + @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'has_service') + @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_volumes') + @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_image_name') + @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_flavor_name') def test_get_server_cloud_missing_fips( self, mock_get_flavor_name, mock_get_image_name, @@ -584,9 +584,9 @@ def test_get_server_cloud_missing_fips( self.assertEqual(PUBLIC_V4, srv['public_v4']) self.assert_calls() - @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_volumes') - @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_image_name') - @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_flavor_name') + @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_volumes') + @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_image_name') + @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_flavor_name') def test_get_server_cloud_rackspace_v6( self, mock_get_flavor_name, mock_get_image_name, mock_get_volumes): @@ -634,9 +634,9 @@ def test_get_server_cloud_rackspace_v6( "2001:4800:7819:103:be76:4eff:fe05:8525", srv['interface_ip']) self.assert_calls() - @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_volumes') - @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_image_name') - @mock.patch.object(openstack.cloud.OpenStackCloud, 'get_flavor_name') + @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_volumes') + @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_image_name') + @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_flavor_name') def test_get_server_cloud_osic_split( self, mock_get_flavor_name, mock_get_image_name, mock_get_volumes): @@ -918,8 +918,8 @@ def test_get_security_groups(self, self.assertEqual('testgroup', hostvars['security_groups'][0]['name']) - @mock.patch.object(openstack.cloud.meta, 'get_server_external_ipv6') - @mock.patch.object(openstack.cloud.meta, 'get_server_external_ipv4') + @mock.patch.object(meta, 'get_server_external_ipv6') + @mock.patch.object(meta, 'get_server_external_ipv4') def test_basic_hostvars( self, mock_get_server_external_ipv4, mock_get_server_external_ipv6): @@ -952,8 +952,8 @@ def test_basic_hostvars( # test volume exception self.assertEqual([], hostvars['volumes']) - @mock.patch.object(openstack.cloud.meta, 'get_server_external_ipv6') - @mock.patch.object(openstack.cloud.meta, 'get_server_external_ipv4') + @mock.patch.object(meta, 'get_server_external_ipv6') + @mock.patch.object(meta, 'get_server_external_ipv4') def test_ipv4_hostvars( self, mock_get_server_external_ipv4, mock_get_server_external_ipv6): @@ -966,7 +966,7 @@ def test_ipv4_hostvars( fake_cloud, meta.obj_to_munch(standard_fake_server)) self.assertEqual(PUBLIC_V4, hostvars['interface_ip']) - @mock.patch.object(openstack.cloud.meta, 'get_server_external_ipv4') + @mock.patch.object(meta, 'get_server_external_ipv4') def test_private_interface_ip(self, mock_get_server_external_ipv4): mock_get_server_external_ipv4.return_value = PUBLIC_V4 @@ -976,7 +976,7 @@ def test_private_interface_ip(self, mock_get_server_external_ipv4): cloud, meta.obj_to_munch(standard_fake_server)) self.assertEqual(PRIVATE_V4, hostvars['interface_ip']) - @mock.patch.object(openstack.cloud.meta, 'get_server_external_ipv4') + @mock.patch.object(meta, 'get_server_external_ipv4') def test_image_string(self, mock_get_server_external_ipv4): mock_get_server_external_ipv4.return_value = PUBLIC_V4 diff --git a/openstack/tests/unit/cloud/test_shade.py b/openstack/tests/unit/cloud/test_shade.py index d64a1baa1..5d95adbcb 100644 --- a/openstack/tests/unit/cloud/test_shade.py +++ b/openstack/tests/unit/cloud/test_shade.py @@ -15,8 +15,9 @@ import testtools -import openstack.cloud +from openstack.cloud import openstackcloud from openstack.cloud import exc +from openstack import connection from openstack.tests import fakes from openstack.tests.unit import base from openstack import utils @@ -52,7 +53,7 @@ def fake_has_service(*args, **kwargs): self.cloud.has_service = fake_has_service def test_openstack_cloud(self): - self.assertIsInstance(self.cloud, openstack.cloud.OpenStackCloud) + self.assertIsInstance(self.cloud, connection.Connection) def test_connect_as(self): # Do initial auth/catalog steps @@ -62,7 +63,7 @@ def test_connect_as(self): # keystoneauth1.loading.base.BaseLoader.load_from_options self.cloud.connect_as(project_name='test_project') - @mock.patch.object(openstack.cloud.OpenStackCloud, 'search_images') + @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'search_images') def test_get_images(self, mock_search): image1 = dict(id='123', name='mickey') mock_search.return_value = [image1] @@ -70,7 +71,7 @@ def test_get_images(self, mock_search): self.assertIsNotNone(r) self.assertDictEqual(image1, r) - @mock.patch.object(openstack.cloud.OpenStackCloud, 'search_images') + @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'search_images') def test_get_image_not_found(self, mock_search): mock_search.return_value = [] r = self.cloud.get_image('doesNotExist') From c9e337422df7fb9bbd49b1aaafdb7b4400fd716d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 11 Oct 2018 10:31:38 -0500 Subject: [PATCH 2234/3836] Revert the Proxy metaclass Sometimes one can be too clever. The win of generating methods here doesn't really outweigh the complexity or the increased difficulty understanding the codebase. Additionally, we run in to an EXCELLENTLY obtuse error: TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases when trying to make an abstract base class for the image proxy base class. Just get rid of the metaclass and restore the (very few) generated methods. Change-Id: Ib53d7b29526a734f1dcf4088bf156e9a29746f5b --- openstack/_meta/_proxy_templates.py | 150 ---------------------------- openstack/_meta/proxy.py | 129 ------------------------ openstack/compute/v2/_proxy.py | 139 ++++++++++++++++++++++++-- openstack/compute/v2/flavor.py | 2 - openstack/compute/v2/server.py | 2 - openstack/proxy.py | 5 +- openstack/resource.py | 2 - 7 files changed, 132 insertions(+), 297 deletions(-) delete mode 100644 openstack/_meta/_proxy_templates.py delete mode 100644 openstack/_meta/proxy.py diff --git a/openstack/_meta/_proxy_templates.py b/openstack/_meta/_proxy_templates.py deleted file mode 100644 index 9661a5bd3..000000000 --- a/openstack/_meta/_proxy_templates.py +++ /dev/null @@ -1,150 +0,0 @@ -# Copyright 2018 Red Hat, 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. -"""Doc and Code templates to be used by the Proxy Metaclass. - -The doc templates and code templates are stored separately because having -either of them templated is weird in the first place, but having a doc -string inside of a function definition that's inside of a triple-quoted -string is just hard on the eyeballs. -""" - -_FIND_TEMPLATE = """Find a single {resource_name} - -:param name_or_id: The name or ID of an {resource_name}. -:param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. -:returns: One :class:`~{resource_class}` or None -""" - -_LIST_TEMPLATE = """Retrieve a generator of all {resource_name} - -:param bool details: When ``True``, returns - :class:`~{detail_class}` objects, - otherwise :class:`~{resource_class}`. - *Default: ``True``* -:param kwargs \*\*query: Optional query parameters to be sent to limit - the flavors being returned. - -:returns: A generator of {resource_name} instances. -:rtype: :class:`~{resource_class}` -""" - -_DELETE_TEMPLATE = """Delete a {resource_name} - -:param {name}: - The value can be either the ID of a {name} or a - :class:`~{resource_class}` instance. -:param bool ignore_missing: - When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` - will be raised when the {name} does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent {name}. - -:returns: ``None`` -""" - -_FETCH_TEMPLATE = """Fetch a single {resource_name} - -:param {name}: - The value can be the ID of a {name} or a - :class:`~{resource_class}` instance. - -:returns: One :class:`~{resource_class}` -:raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. -""" - -_CREATE_TEMPLATE = """Create a new {resource_name} from attributes - -:param dict attrs: - Keyword arguments which will be used to create a - :class:`~{resource_class}`. - -:returns: The results of {resource_name} creation -:rtype: :class:`~{resource_class}` -""" - -_COMMIT_TEMPLATE = """Commit the state of a {resource_name} - -:param {name}: - Either the ID of a {resource_name} or a :class:`~{resource_class}` - instance. -:attrs kwargs: - The attributes to commit on the {resource_name} represented by - ``{name}``. - -:returns: The updated server -:rtype: :class:`~{resource_class}` -""" - -_DOC_TEMPLATES = { - 'create': _CREATE_TEMPLATE, - 'delete': _DELETE_TEMPLATE, - 'find': _FIND_TEMPLATE, - 'list': _LIST_TEMPLATE, - 'fetch': _FETCH_TEMPLATE, - 'commit': _COMMIT_TEMPLATE, -} - -_FIND_SOURCE = """ -def find(self, name_or_id, ignore_missing=True): - return self._find( - self.{resource_name}, name_or_id, ignore_missing=ignore_missing) -""" - -_CREATE_SOURCE = """ -def create(self, **attrs): - return self._create(self.{resource_name}, **attrs) -""" - -_DELETE_SOURCE = """ -def delete(self, {name}, ignore_missing=True): - self._delete(self.{resource_name}, {name}, ignore_missing=ignore_missing) -""" - -_FETCH_SOURCE = """ -def fetch(self, {name}): - return self._get(self.{resource_name}, {name}) -""" - -_LIST_SOURCE = """ -def list(self, details=True, **query): - res_cls = self.{detail_name} if details else self.{resource_name} - return self._list(res_cls, paginated=True, **query) -""" - -_COMMIT_SOURCE = """ -def commit(self, {name}, **attrs): - return self._update(self.{resource_name}, {name}, **attrs) -""" - -_SOURCE_TEMPLATES = { - 'create': _CREATE_SOURCE, - 'delete': _DELETE_SOURCE, - 'find': _FIND_SOURCE, - 'list': _LIST_SOURCE, - 'fetch': _FETCH_SOURCE, - 'commit': _COMMIT_SOURCE, -} - - -def get_source_template(action, **kwargs): - return _SOURCE_TEMPLATES[action].format(**kwargs) - - -def get_doc_template(action, **kwargs): - return _DOC_TEMPLATES[action].format(**kwargs) diff --git a/openstack/_meta/proxy.py b/openstack/_meta/proxy.py deleted file mode 100644 index f96acb627..000000000 --- a/openstack/_meta/proxy.py +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright 2018 Red Hat, 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. - -# Inspired by code from -# https://github.com/micheles/decorator/blob/master/src/decorator.py -# which is MIT licensed. - -from openstack._meta import _proxy_templates -from openstack import resource - - -def compile_function(evaldict, action, module, **kwargs): - "Make a new functions" - - src = _proxy_templates.get_source_template(action, **kwargs) - - # Ensure each generated block of code has a unique filename for profilers - # (such as cProfile) that depend on the tuple of (, - # , ) being unique. - filename = ''.format(module=module) - code = compile(src, filename, 'exec') - exec(code, evaldict) - func = evaldict[action] - func.__source__ = src - return func - - -def add_function(dct, func, action, args, name_template='{action}_{name}'): - func_name = name_template.format(action=action, **args) - # If the class has the function already, don't override it - if func_name in dct: - func_name = '_generated_' + func_name - func.__name__ = func_name - func.__qualname__ = func_name - func.__doc__ = _proxy_templates.get_doc_template(action, **args) - func.__module__ = args['module'] - dct[func_name] = func - - -def expand_classname(res): - return '{module}.{name}'.format(module=res.__module__, name=res.__name__) - - -class ProxyMeta(type): - """Metaclass that generates standard methods based on Resources. - - Each service has a set of Resources which define the fundamental - qualities of the remote resources. A large portion of the methods - on Proxy classes are boilerplate. - - This metaclass reads the definition of the Proxy class and looks for - Resource classes attached to it. It then checks them to see which - operations are allowed by looking at the ``allow_`` flags. Based on that, - it generates the standard methods and adds them to the class. - - If a method exists on the class when it is read, the generated method - does not overwrite the existing method. Instead, it is attached as - ``_generated_{method_name}``. This allows people to either write - specific proxy methods and completely ignore the generated method, - or to write specialized methods that then delegate action to the generated - method. - - Since this is done as a metaclass at class object creation time, - things like sphinx continue to work. - """ - def __new__(meta, name, bases, dct): - # Build up a list of resource classes attached to the Proxy - resources = {} - details = {} - for k, v in dct.items(): - if isinstance(v, type) and issubclass(v, resource.Resource): - if v.detail_for: - details[v.detail_for.__name__] = v - else: - resources[v.__name__] = v - - for resource_name, res in resources.items(): - resource_class = expand_classname(res) - detail = details.get(resource_name, res) - detail_name = detail.__name__ - detail_class = expand_classname(detail) - - lower_name = resource_name.lower() - plural_name = getattr(res, 'plural_name', lower_name + 's') - args = dict( - resource_name=resource_name, - resource_class=resource_class, - name=lower_name, - module=res.__module__, - detail_name=detail_name, - detail_class=detail_class, - plural_name=plural_name, - ) - # Generate unbound methods from the template strings. - # We have to do a compile step rather than using somthing - # like existing function objects wrapped with closures - # because of the argument naming pattern for delete and update. - # You can't really change an argument name programmatically, - # at least not that I've been able to find. - # We pass in a copy of the dct dict so that the exec step can - # be done in the context of the class the methods will be attached - # to. This allows name resolution to work properly. - for action in ('create', 'fetch', 'commit', 'delete'): - if getattr(res, 'allow_{action}'.format(action=action)): - func = compile_function(dct.copy(), action, **args) - kwargs = {} - if action == 'fetch': - kwargs['name_template'] = 'get_{name}' - elif action == 'commit': - kwargs['name_template'] = 'update_{name}' - add_function(dct, func, action, args, **kwargs) - if res.allow_list: - func = compile_function(dct.copy(), 'find', **args) - add_function(dct, func, 'find', args) - func = compile_function(dct.copy(), 'list', **args) - add_function(dct, func, 'list', args, plural_name) - - return super(ProxyMeta, meta).__new__(meta, name, bases, dct) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 2aa53b307..20c7ebffb 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -29,11 +29,96 @@ class Proxy(proxy.Proxy): - Extension = extension.Extension - Flavor = _flavor.Flavor - FlavorDetail = _flavor.FlavorDetail - Server = _server.Server - ServerDetail = _server.ServerDetail + def find_extension(self, name_or_id, ignore_missing=True): + """Find a single extension + + :param name_or_id: The name or ID of an extension. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :returns: One :class:`~openstack.compute.v2.extension.Extension` or + None + """ + return self._find(extension.Extension, name_or_id, + ignore_missing=ignore_missing) + + def extensions(self): + """Retrieve a generator of extensions + + :returns: A generator of extension instances. + :rtype: :class:`~openstack.compute.v2.extension.Extension` + """ + return self._list(extension.Extension, paginated=True) + + def find_flavor(self, name_or_id, ignore_missing=True): + """Find a single flavor + + :param name_or_id: The name or ID of a flavor. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :returns: One :class:`~openstack.compute.v2.flavor.Flavor` or None + """ + return self._find(_flavor.Flavor, name_or_id, + ignore_missing=ignore_missing) + + def create_flavor(self, **attrs): + """Create a new flavor from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.compute.v2.flavor.Flavor`, + comprised of the properties on the Flavor class. + + :returns: The results of flavor creation + :rtype: :class:`~openstack.compute.v2.flavor.Flavor` + """ + return self._create(_flavor.Flavor, **attrs) + + def delete_flavor(self, flavor, ignore_missing=True): + """Delete a flavor + + :param flavor: The value can be either the ID of a flavor or a + :class:`~openstack.compute.v2.flavor.Flavor` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the flavor does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent flavor. + + :returns: ``None`` + """ + self._delete(_flavor.Flavor, flavor, ignore_missing=ignore_missing) + + def get_flavor(self, flavor): + """Get a single flavor + + :param flavor: The value can be the ID of a flavor or a + :class:`~openstack.compute.v2.flavor.Flavor` instance. + + :returns: One :class:`~openstack.compute.v2.flavor.Flavor` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_flavor.Flavor, flavor) + + def flavors(self, details=True, **query): + """Return a generator of flavors + + :param bool details: When ``True``, returns + :class:`~openstack.compute.v2.flavor.FlavorDetail` objects, + otherwise :class:`~openstack.compute.v2.flavor.Flavor`. + *Default: ``True``* + :param kwargs \*\*query: Optional query parameters to be sent to limit + the flavors being returned. + + :returns: A generator of flavor objects + """ + flv = _flavor.FlavorDetail if details else _flavor.Flavor + return self._list(flv, paginated=True, **query) def delete_image(self, image, ignore_missing=True): """Delete an image @@ -227,6 +312,18 @@ def get_limits(self): """ return self._get(limits.Limits) + def create_server(self, **attrs): + """Create a new server from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.compute.v2.server.Server`, + comprised of the properties on the Server class. + + :returns: The results of server creation + :rtype: :class:`~openstack.compute.v2.server.Server` + """ + return self._create(_server.Server, **attrs) + def delete_server(self, server, ignore_missing=True, force=False): """Delete a server @@ -246,8 +343,33 @@ def delete_server(self, server, ignore_missing=True, force=False): server = self._get_resource(_server.Server, server) server.force_delete(self) else: - self._generated_delete_server( - server, ignore_missing=ignore_missing) + self._delete(_server.Server, server, ignore_missing=ignore_missing) + + def find_server(self, name_or_id, ignore_missing=True): + """Find a single server + + :param name_or_id: The name or ID of a server. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :returns: One :class:`~openstack.compute.v2.server.Server` or None + """ + return self._find(_server.Server, name_or_id, + ignore_missing=ignore_missing) + + def get_server(self, server): + """Get a single server + + :param server: The value can be the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance. + + :returns: One :class:`~openstack.compute.v2.server.Server` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_server.Server, server) def servers(self, details=True, **query): """Retrieve a generator of servers @@ -286,7 +408,8 @@ def servers(self, details=True, **query): :returns: A generator of server instances. """ - return self._generated_servers(details=details, **query) + srv = _server.ServerDetail if details else _server.Server + return self._list(srv, paginated=True, **query) def update_server(self, server, **attrs): """Update a server diff --git a/openstack/compute/v2/flavor.py b/openstack/compute/v2/flavor.py index 7128e9d1c..b68c726a5 100644 --- a/openstack/compute/v2/flavor.py +++ b/openstack/compute/v2/flavor.py @@ -64,5 +64,3 @@ class FlavorDetail(Flavor): allow_commit = False allow_delete = False allow_list = True - - detail_for = Flavor diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 17b155853..2207b2f8c 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -447,5 +447,3 @@ class ServerDetail(Server): allow_commit = False allow_delete = False allow_list = True - - detail_for = Server diff --git a/openstack/proxy.py b/openstack/proxy.py index ea3f15f12..3dcf0549a 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -10,10 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import six - from openstack import _adapter -from openstack._meta import proxy as _meta from openstack import exceptions from openstack import resource @@ -42,7 +39,7 @@ def check(self, expected, actual=None, *args, **kwargs): return wrap -class Proxy(six.with_metaclass(_meta.ProxyMeta, _adapter.OpenStackSDKAdapter)): +class Proxy(_adapter.OpenStackSDKAdapter): """Represents a service.""" def _get_resource(self, resource_type, value, **attrs): diff --git a/openstack/resource.py b/openstack/resource.py index 07a822c5b..d2b3ee7f2 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -382,8 +382,6 @@ class Resource(dict): requires_id = True #: Do responses for this resource have bodies has_body = True - #: Is this a detailed version of another Resource - detail_for = None #: Maximum microversion to use for getting/creating/updating the Resource _max_microversion = None From c07c39b82268ee84a1a671f1f2746d01026f3dcc Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 11 Oct 2018 10:25:29 +0200 Subject: [PATCH 2235/3836] Rearrange shade image code In anticipation of moving the image creation logic down into the sdk image proxy class which has natural v1 and v2 classes, rearrange the branching logic to start with v1/v2 then go to put/task. This will let us do that reorg in place so that when we move things into proxy classes it'll be more of a direct copy/paste. There is one test update. In doing the code shuffle, there was a duplicate unneeded call to get_image found in the task codepath. That call was removed. However, to compensate, putting the wait in create_image itself needlessly calls get_image after it has been done in the task workflow. The next patch should be able to remove the second get_image call. Change-Id: Id76d1750e5af46b39d08428e0c6d9d8b30a43e5e --- openstack/cloud/openstackcloud.py | 126 ++++++++++++----------- openstack/tests/unit/cloud/test_image.py | 12 ++- 2 files changed, 75 insertions(+), 63 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 0da232d99..706fea588 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -4783,42 +4783,68 @@ def create_image( if disable_vendor_agent: kwargs.update(self.config.config['disable_vendor_agent']) + # If a user used the v1 calling format, they will have + # passed a dict called properties along + properties = kwargs.pop('properties', {}) + kwargs.update(properties) + image_kwargs = dict(properties=kwargs) + if disk_format: + image_kwargs['disk_format'] = disk_format + if container_format: + image_kwargs['container_format'] = container_format + + if self._is_client_version('image', 2): + image = self._upload_image_v2( + name, filename, + wait=wait, timeout=timeout, + meta=meta, **image_kwargs) + else: + image = self._upload_image_v1( + name, filename, + wait=wait, timeout=timeout, + meta=meta, **image_kwargs) + self._get_cache(None).invalidate() + if not wait: + return image + try: + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for the image to finish."): + image_obj = self.get_image(image.id) + if image_obj and image_obj.status not in ('queued', 'saving'): + return image_obj + except exc.OpenStackCloudTimeout: + self.log.debug( + "Timeout waiting for image to become ready. Deleting.") + self.delete_image(image.id, wait=True) + raise + + def _upload_image_v2( + self, name, filename=None, + wait=False, timeout=3600, + meta=None, **kwargs): # We can never have nice things. Glance v1 took "is_public" as a # boolean. Glance v2 takes "visibility". If the user gives us # is_public, we know what they mean. If they give us visibility, they # know that they mean. - if self._is_client_version('image', 2): - if 'is_public' in kwargs: - is_public = kwargs.pop('is_public') - if is_public: - kwargs['visibility'] = 'public' - else: - kwargs['visibility'] = 'private' + if 'is_public' in kwargs['properties']: + is_public = kwargs['properties'].pop('is_public') + if is_public: + kwargs['visibility'] = 'public' + else: + kwargs['visibility'] = 'private' try: # This makes me want to die inside if self.image_api_use_tasks: return self._upload_image_task( - name, filename, container, - current_image=current_image, + name, filename, wait=wait, timeout=timeout, - md5=md5, sha256=sha256, meta=meta, **kwargs) else: - # If a user used the v1 calling format, they will have - # passed a dict called properties along - properties = kwargs.pop('properties', {}) - kwargs.update(properties) - image_kwargs = dict(properties=kwargs) - if disk_format: - image_kwargs['disk_format'] = disk_format - if container_format: - image_kwargs['container_format'] = container_format - - return self._upload_image_put( + return self._upload_image_put_v2( name, filename, meta=meta, - wait=wait, timeout=timeout, - **image_kwargs) + **kwargs) except exc.OpenStackCloudException: self.log.debug("Image creation failed", exc_info=True) raise @@ -4869,7 +4895,9 @@ def _upload_image_from_volume( self.delete_image(response['image_id'], wait=True) raise - def _upload_image_put_v2(self, name, image_data, meta, **image_kwargs): + def _upload_image_put_v2(self, name, filename, meta, **image_kwargs): + image_data = open(filename, 'rb') + properties = image_kwargs.pop('properties', {}) image_kwargs.update(self._make_v2_image_params(meta, properties)) @@ -4898,9 +4926,13 @@ def _upload_image_put_v2(self, name, image_data, meta, **image_kwargs): return self._normalize_image(image) - def _upload_image_put_v1( - self, name, image_data, meta, **image_kwargs): - + def _upload_image_v1( + self, name, filename, + wait=False, timeout=3600, + meta=None, **image_kwargs): + # NOTE(mordred) wait and timeout parameters are unused, but + # are present for ease at calling site. + image_data = open(filename, 'rb') image_kwargs['properties'].update(meta) image_kwargs['name'] = name @@ -4937,38 +4969,16 @@ def _upload_image_put_v1( raise return self._normalize_image(image) - def _upload_image_put( - self, name, filename, meta, wait, timeout, **image_kwargs): - image_data = open(filename, 'rb') - # Because reasons and crying bunnies - if self._is_client_version('image', 2): - image = self._upload_image_put_v2( - name, image_data, meta, **image_kwargs) - else: - image = self._upload_image_put_v1( - name, image_data, meta, **image_kwargs) - self._get_cache(None).invalidate() - if not wait: - return image - try: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for the image to finish."): - image_obj = self.get_image(image.id) - if image_obj and image_obj.status not in ('queued', 'saving'): - return image_obj - except exc.OpenStackCloudTimeout: - self.log.debug( - "Timeout waiting for image to become ready. Deleting.") - self.delete_image(image.id, wait=True) - raise - def _upload_image_task( - self, name, filename, container, current_image, - wait, timeout, meta, md5=None, sha256=None, **image_kwargs): + self, name, filename, + wait, timeout, meta, **image_kwargs): - parameters = image_kwargs.pop('parameters', {}) - image_kwargs.update(parameters) + properties = image_kwargs['properties'] + md5 = properties[self._IMAGE_MD5_KEY] + sha256 = properties[self._IMAGE_SHA256_KEY] + container = properties[self._IMAGE_OBJECT_KEY].split('/', 1)[0] + properties = image_kwargs.pop('properties', {}) + image_kwargs.update(properties) self.create_container(container) self.create_object( @@ -4976,8 +4986,6 @@ def _upload_image_task( md5=md5, sha256=sha256, metadata={self._OBJECT_AUTOCREATE_KEY: 'true'}, **{'content-type': 'application/octet-stream'}) - if not current_image: - current_image = self.get_image(name) # TODO(mordred): Can we do something similar to what nodepool does # using glance properties to not delete then upload but instead make a # new "good" image and then mark the old one as "bad" diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index f678f31cd..e6927a516 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -408,10 +408,6 @@ def test_create_image_task(self): headers={'x-object-meta-x-sdk-md5': fakes.NO_MD5, 'x-object-meta-x-sdk-sha256': fakes.NO_SHA256}) ), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - json={'images': []}), dict(method='POST', uri=self.get_mock_url( 'image', append=['tasks'], base_url_append='v2'), @@ -475,6 +471,14 @@ def test_create_image_task(self): uri='{endpoint}/{container}/{object}'.format( endpoint=endpoint, container=self.container_name, object=self.image_name)), + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), + json=self.fake_search_return), + # TODO(mordred) The task workflow results in an extra call + # in the upper level wait. We should be able to make this + # go away once we refactor a wait_for_image out in the next + # patch. dict(method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2'), From beedbccd04d23c15b3905ac78764b8a6a30cdd28 Mon Sep 17 00:00:00 2001 From: Maxime Guyot Date: Mon, 15 Oct 2018 20:37:13 +0200 Subject: [PATCH 2236/3836] Update Auro cloud profile Auro is now available over Keystone v3 and correct region name Change-Id: I8d84839557faad5967da5af8d28dcc2bf81217e2 --- openstack/config/vendors/auro.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openstack/config/vendors/auro.json b/openstack/config/vendors/auro.json index 410a8e19c..21d832b2b 100644 --- a/openstack/config/vendors/auro.json +++ b/openstack/config/vendors/auro.json @@ -2,10 +2,12 @@ "name": "auro", "profile": { "auth": { - "auth_url": "https://api.van1.auro.io:5000/v2.0" + "auth_url": "https://api.van2.auro.io:5000/v3", + "user_domain_name": "Default", + "project_domain_name": "Default" }, - "identity_api_version": "2", - "region_name": "van1", + "identity_api_version": "3", + "region_name": "RegionOne", "requires_floating_ip": true } } From 0646a7376b6ec60c5d51b44ccffbf5fe4f479bc0 Mon Sep 17 00:00:00 2001 From: Maxime Guyot Date: Mon, 15 Oct 2018 21:03:16 +0200 Subject: [PATCH 2237/3836] Update ElastX cloud profile Region has been renamed to se-sto Change-Id: Ibdffc7cf9338c00f890cacd153c7a13499e545de --- openstack/config/vendors/elastx.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/config/vendors/elastx.json b/openstack/config/vendors/elastx.json index 1e7248213..676c00290 100644 --- a/openstack/config/vendors/elastx.json +++ b/openstack/config/vendors/elastx.json @@ -5,6 +5,6 @@ "auth_url": "https://ops.elastx.net:5000" }, "identity_api_version": "3", - "region_name": "regionOne" + "region_name": "se-sto" } } From 38f6050d3f0afe68ccfea68656a301c9e063aa55 Mon Sep 17 00:00:00 2001 From: Tobias Rydberg Date: Tue, 16 Oct 2018 09:48:05 +0000 Subject: [PATCH 2238/3836] Adding two new regions and dynamic auth_url based on region name. New setup for authentication - one per region. Change-Id: Ida97e6ecebbcf9930363d5ddc1aa22d9c97b6f39 --- openstack/config/vendors/citycloud.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openstack/config/vendors/citycloud.json b/openstack/config/vendors/citycloud.json index 8a6318795..eee22a852 100644 --- a/openstack/config/vendors/citycloud.json +++ b/openstack/config/vendors/citycloud.json @@ -2,7 +2,7 @@ "name": "citycloud", "profile": { "auth": { - "auth_url": "https://identity1.citycloud.com:5000/v3/" + "auth_url": "https://%(region_name)s.citycloud.com:5000/v3/" }, "regions": [ "Buf1", @@ -10,7 +10,9 @@ "Fra1", "Lon1", "Sto2", - "Kna1" + "Kna1", + "dx1", + "tky1" ], "requires_floating_ip": true, "block_storage_api_version": "1", From 41e0c4673dc42f10e201ba4c99509e430790b7e7 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Tue, 16 Oct 2018 14:15:15 +0200 Subject: [PATCH 2239/3836] Convert inspect_machine to use the baremetal proxy Change-Id: I5729af24a34dc082c8f61b8370642d5dbca9dbee --- openstack/cloud/openstackcloud.py | 57 +++++++------------ .../tests/unit/cloud/test_baremetal_node.py | 16 +++++- 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 831f79a46..8a8b1a8f0 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -9527,56 +9527,43 @@ def inspect_machine(self, name_or_id, wait=False, timeout=3600): return_to_available = False - machine = self.get_machine(name_or_id) - if not machine: - raise exc.OpenStackCloudException( - "Machine inspection failed to find: %s." % name_or_id) + node = self.baremetal.get_node(name_or_id) # NOTE(TheJulia): If in available state, we can do this. However, # we need to to move the machine back to manageable first. - if "available" in machine['provision_state']: - if machine['instance_uuid']: + if node.provision_state == 'available': + if node.instance_id: raise exc.OpenStackCloudException( "Refusing to inspect available machine %(node)s " "which is associated with an instance " "(instance_uuid %(inst)s)" % - {'node': machine['uuid'], - 'inst': machine['instance_uuid']}) + {'node': node.id, 'inst': node.instance_id}) return_to_available = True # NOTE(TheJulia): Changing available machine to managedable state # and due to state transitions we need to until that transition has - # completd. - self.node_set_provision_state(machine['uuid'], 'manage', - wait=True, timeout=timeout) - elif ("manage" not in machine['provision_state'] and - "inspect failed" not in machine['provision_state']): - raise exc.OpenStackCloudException( - "Machine must be in 'manage' or 'available' state to " - "engage inspection: Machine: %s State: %s" - % (machine['uuid'], machine['provision_state'])) - with _utils.shade_exceptions("Error inspecting machine"): - machine = self.node_set_provision_state(machine['uuid'], 'inspect') - if wait: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for node transition to " - "target state of 'inspect'"): - machine = self.get_machine(name_or_id) + # completed. + node = self.baremetal.set_node_provision_state(node, 'manage', + wait=True, + timeout=timeout) - if "inspect failed" in machine['provision_state']: - raise exc.OpenStackCloudException( - "Inspection of node %s failed, last error: %s" - % (machine['uuid'], machine['last_error'])) + if node.provision_state not in ('manageable', 'inspect failed'): + raise exc.OpenStackCloudException( + "Machine %(node)s must be in 'manageable', 'inspect failed' " + "or 'available' provision state to start inspection, the " + "current state is %(state)s" % + {'node': node.id, 'state': node.provision_state}) - if "manageable" in machine['provision_state']: - break + node = self.baremetal.set_node_provision_state(node, 'inspect', + wait=True, + timeout=timeout) - if return_to_available: - machine = self.node_set_provision_state( - machine['uuid'], 'provide', wait=wait, timeout=timeout) + if return_to_available: + node = self.baremetal.set_node_provision_state(node, 'provide', + wait=True, + timeout=timeout) - return(machine) + return node._to_munch() def register_machine(self, nics, wait=False, timeout=3600, lock_timeout=600, **kwargs): diff --git a/openstack/tests/unit/cloud/test_baremetal_node.py b/openstack/tests/unit/cloud/test_baremetal_node.py index a89ed5c45..30ec4b959 100644 --- a/openstack/tests/unit/cloud/test_baremetal_node.py +++ b/openstack/tests/unit/cloud/test_baremetal_node.py @@ -251,6 +251,8 @@ def test_inspect_machine_failed(self): self.fake_baremetal_node['provision_state'] = 'inspect failed' self.fake_baremetal_node['last_error'] = 'kaboom!' inspecting_node['provision_state'] = 'inspecting' + finished_node = self.fake_baremetal_node.copy() + finished_node['provision_state'] = 'manageable' self.register_uris([ dict( method='GET', @@ -270,7 +272,13 @@ def test_inspect_machine_failed(self): uri=self.get_mock_url( resource='nodes', append=[self.fake_baremetal_node['uuid']]), - json=inspecting_node) + json=inspecting_node), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=finished_node), ]) self.cloud.inspect_machine(self.fake_baremetal_node['uuid']) @@ -301,6 +309,12 @@ def test_inspect_machine_manageable(self): resource='nodes', append=[self.fake_baremetal_node['uuid']]), json=inspecting_node), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), ]) self.cloud.inspect_machine(self.fake_baremetal_node['uuid']) From 17230af4fff10e9cccb7acb02460bf2c4f35d8c9 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Tue, 16 Oct 2018 14:52:19 +0200 Subject: [PATCH 2240/3836] Move wait_for_baremetal_node_lock to the baremetal proxy It is a quite low-level method that regular users should not use. This patch deprecates it in favor of new baremetal.wait_for_node_reservation. Change-Id: I071bdea02898cf911c2c018d014895c8a856524c --- doc/source/user/proxies/baremetal.rst | 1 + openstack/baremetal/v1/_proxy.py | 26 +++++++++++ openstack/baremetal/v1/node.py | 38 ++++++++++++++++ openstack/cloud/openstackcloud.py | 44 +++---------------- .../tests/unit/baremetal/v1/test_node.py | 39 ++++++++++++++++ .../tests/unit/cloud/test_baremetal_node.py | 3 +- ...aremetal-reservation-40327923092e9647.yaml | 10 +++++ 7 files changed, 121 insertions(+), 40 deletions(-) create mode 100644 releasenotes/notes/baremetal-reservation-40327923092e9647.yaml diff --git a/doc/source/user/proxies/baremetal.rst b/doc/source/user/proxies/baremetal.rst index e6faffe4e..63d5d0c5f 100644 --- a/doc/source/user/proxies/baremetal.rst +++ b/doc/source/user/proxies/baremetal.rst @@ -24,6 +24,7 @@ Node Operations .. automethod:: openstack.baremetal.v1._proxy.Proxy.nodes .. automethod:: openstack.baremetal.v1._proxy.Proxy.set_node_provision_state .. automethod:: openstack.baremetal.v1._proxy.Proxy.wait_for_nodes_provision_state + .. automethod:: openstack.baremetal.v1._proxy.Proxy.wait_for_node_reservation .. automethod:: openstack.baremetal.v1._proxy.Proxy.validate_node Port Operations diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index ab62b9758..1696e266c 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -332,6 +332,32 @@ def wait_for_nodes_provision_state(self, nodes, expected_state, {'nodes': ', '.join(n.id for n in remaining), 'target': expected_state}) + def wait_for_node_reservation(self, node, timeout=None): + """Wait for a lock on the node to be released. + + Bare metal nodes in ironic have a reservation lock that + is used to represent that a conductor has locked the node + while performing some sort of action, such as changing + configuration as a result of a machine state change. + + This lock can occur during power syncronization, and prevents + updates to objects attached to the node, such as ports. + + Note that nothing prevents a conductor from acquiring the lock again + after this call returns, so it should be treated as best effort. + + Returns immediately if there is no reservation on the node. + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :param timeout: How much (in seconds) to wait for the lock to be + released. The value of ``None`` (the default) means no timeout. + + :returns: The updated :class:`~openstack.baremetal.v1.node.Node` + """ + res = self._get_resource(_node.Node, node) + return res.wait_for_reservation(self, timeout=timeout) + def validate_node(self, node, required=('boot', 'deploy', 'power')): """Validate required information on a node. diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 913301cc6..d8a5bc18a 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -367,6 +367,44 @@ def wait_for_provision_state(self, session, expected_state, timeout=None, {'node': self.id, 'target': expected_state, 'state': self.provision_state}) + def wait_for_reservation(self, session, timeout=None): + """Wait for a lock on the node to be released. + + Bare metal nodes in ironic have a reservation lock that + is used to represent that a conductor has locked the node + while performing some sort of action, such as changing + configuration as a result of a machine state change. + + This lock can occur during power syncronization, and prevents + updates to objects attached to the node, such as ports. + + Note that nothing prevents a conductor from acquiring the lock again + after this call returns, so it should be treated as best effort. + + Returns immediately if there is no reservation on the node. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param timeout: How much (in seconds) to wait for the lock to be + released. The value of ``None`` (the default) means no timeout. + + :return: This :class:`Node` instance. + """ + if self.reservation is None: + return self + + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for the lock to be released on node %s" % + self.id): + self.fetch(session) + if self.reservation is None: + return self + + _logger.debug('Still waiting for the lock to be released on node ' + '%(node)s, currently locked by conductor %(host)s', + {'node': self.id, 'host': self.reservation}) + def _check_state_reached(self, session, expected_state, abort_on_failed_state=True): """Wait for the node to reach the expected state. diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 831f79a46..ee39f69a2 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -10149,49 +10149,15 @@ def purge_node_instance_info(self, uuid): def wait_for_baremetal_node_lock(self, node, timeout=30): """Wait for a baremetal node to have no lock. - Baremetal nodes in ironic have a reservation lock that - is used to represent that a conductor has locked the node - while performing some sort of action, such as changing - configuration as a result of a machine state change. - - This lock can occur during power syncronization, and prevents - updates to objects attached to the node, such as ports. - - In the vast majority of cases, locks should clear in a few - seconds, and as such this method will only wait for 30 seconds. - The default wait is two seconds between checking if the lock - has cleared. - - This method is intended for use by methods that need to - gracefully block without genreating errors, however this - method does prevent another client or a timer from - triggering a lock immediately after we see the lock as - having cleared. - - :param node: The json representation of the node, - specificially looking for the node - 'uuid' and 'reservation' fields. - :param timeout: Integer in seconds to wait for the - lock to clear. Default: 30 + DEPRECATED, use ``wait_for_node_reservation`` on the `baremetal` proxy. :raises: OpenStackCloudException upon client failure. :returns: None """ - # TODO(TheJulia): This _can_ still fail with a race - # condition in that between us checking the status, - # a conductor where the conductor could still obtain - # a lock before we are able to obtain a lock. - # This means we should handle this with such conections - - if node['reservation'] is None: - return - else: - msg = 'Waiting for lock to be released for node {node}'.format( - node=node['uuid']) - for count in utils.iterate_timeout(timeout, msg, 2): - current_node = self.get_machine(node['uuid']) - if current_node['reservation'] is None: - return + warnings.warn("The wait_for_baremetal_node_lock call is deprecated " + "in favor of wait_for_node_reservation on the baremetal " + "proxy", DeprecationWarning) + self.baremetal.wait_for_node_reservation(node, timeout) @_utils.valid_kwargs('type', 'service_type', 'description') def create_service(self, name, enabled=True, **kwargs): diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 85d77bb7a..03cfd5ad1 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -492,3 +492,42 @@ def test_validate_no_failure(self): # Reason can be empty self.assertFalse(result['boot'].result) self.assertIsNone(result['boot'].reason) + + +@mock.patch('time.sleep', lambda _t: None) +@mock.patch.object(node.Node, 'fetch', autospec=True) +class TestNodeWaitForReservation(base.TestCase): + + def setUp(self): + super(TestNodeWaitForReservation, self).setUp() + self.session = mock.Mock(spec=adapter.Adapter) + self.session.default_microversion = '1.6' + self.node = node.Node(**FAKE) + + def test_no_reservation(self, mock_fetch): + self.node.reservation = None + node = self.node.wait_for_reservation(None) + self.assertIs(node, self.node) + self.assertFalse(mock_fetch.called) + + def test_reservation(self, mock_fetch): + self.node.reservation = 'example.com' + + def _side_effect(node, session): + if self.node.reservation == 'example.com': + self.node.reservation = 'example2.com' + else: + self.node.reservation = None + + mock_fetch.side_effect = _side_effect + node = self.node.wait_for_reservation(self.session) + self.assertIs(node, self.node) + self.assertEqual(2, mock_fetch.call_count) + + def test_timeout(self, mock_fetch): + self.node.reservation = 'example.com' + + self.assertRaises(exceptions.ResourceTimeout, + self.node.wait_for_reservation, + self.session, timeout=0.001) + mock_fetch.assert_called_with(self.node, self.session) diff --git a/openstack/tests/unit/cloud/test_baremetal_node.py b/openstack/tests/unit/cloud/test_baremetal_node.py index a89ed5c45..d958e7075 100644 --- a/openstack/tests/unit/cloud/test_baremetal_node.py +++ b/openstack/tests/unit/cloud/test_baremetal_node.py @@ -912,7 +912,8 @@ def test_wait_for_baremetal_node_lock_not_locked(self): self.fake_baremetal_node, timeout=1)) - self.assertEqual(0, len(self.adapter.request_history)) + # NOTE(dtantsur): service discovery apparently requires 3 calls + self.assertEqual(3, len(self.adapter.request_history)) def test_wait_for_baremetal_node_lock_timeout(self): self.fake_baremetal_node['reservation'] = 'conductor0' diff --git a/releasenotes/notes/baremetal-reservation-40327923092e9647.yaml b/releasenotes/notes/baremetal-reservation-40327923092e9647.yaml new file mode 100644 index 000000000..9b5f7704e --- /dev/null +++ b/releasenotes/notes/baremetal-reservation-40327923092e9647.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Added ``wait_for_node_reservation`` to the baremetal proxy. +deprecations: + - | + The `OpenStackCloud` ``wait_for_baremetal_node_lock`` call is deprecated. + Generally, users should not have to call it. The new + ``wait_for_node_reservation`` from the baremetal proxy can be used when + needed. From d36e835deb80c8818b301c3f183090a3102a0f86 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 12 Oct 2018 16:08:25 -0500 Subject: [PATCH 2241/3836] Add stackviz processing to functional tests We have devstack-based functional tests that use subunit. Let's get ourselves some stackviz! Change-Id: I80e4ac780d6d14ba0a73ceca0e410235707817bb --- .zuul.yaml | 5 +++++ playbooks/devstack/post.yaml | 1 + 2 files changed, 6 insertions(+) diff --git a/.zuul.yaml b/.zuul.yaml index 67a8847ad..d08f38b08 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -28,6 +28,9 @@ description: | Minimum job for devstack-based functional tests post-run: playbooks/devstack/post.yaml + roles: + # NOTE: We pull in roles from the tempest repo for stackviz processing. + - zuul: git.openstack.org/openstack/tempest required-projects: # These jobs will DTRT when openstacksdk triggers them, but we want to # make sure stable branches of openstacksdk never get cloned by other @@ -55,6 +58,8 @@ OPENSTACKSDK_HAS_SWIFT: 1 tox_install_siblings: false tox_envlist: functional + zuul_copy_output: + '{{ ansible_user_dir }}/stackviz': logs zuul_work_dir: src/git.openstack.org/openstack/openstacksdk - job: diff --git a/playbooks/devstack/post.yaml b/playbooks/devstack/post.yaml index 7f0cb1982..0b18f2357 100644 --- a/playbooks/devstack/post.yaml +++ b/playbooks/devstack/post.yaml @@ -2,3 +2,4 @@ roles: - fetch-tox-output - fetch-subunit-output + - process-stackviz From a7dce7b2c62eb935f5ad8e83477db0a0534a6ac5 Mon Sep 17 00:00:00 2001 From: Sean McGinnis Date: Tue, 16 Oct 2018 14:24:17 -0500 Subject: [PATCH 2242/3836] Update sphinx extension logging Sphinx 1.6 deprecated using the application object to perform logging and it will be removed in the upcoming 2.0 release. This updates our extensions to use the recommended sphinx.util.logging instead. Change-Id: Ic4102e6989b9e30386edc45cbb7d858d23d448e1 Signed-off-by: Sean McGinnis --- doc/source/enforcer.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/doc/source/enforcer.py b/doc/source/enforcer.py index 3cd38986c..6eea11e40 100644 --- a/doc/source/enforcer.py +++ b/doc/source/enforcer.py @@ -1,8 +1,23 @@ +# 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 importlib import os from bs4 import BeautifulSoup from sphinx import errors +from sphinx.util import logging + +LOG = logging.getLogger(__name__) # NOTE: We do this because I can't find any way to pass "-v" # into sphinx-build through pbr... @@ -73,7 +88,7 @@ def page_context(app, pagename, templatename, context, doctree): written += 1 if DEBUG: - app.info("ENFORCER: Wrote %d proxy methods for %s" % ( + LOG.info("ENFORCER: Wrote %d proxy methods for %s" % ( written, pagename)) @@ -89,12 +104,12 @@ def build_finished(app, exception): """ all_methods = get_proxy_methods() - app.info("ENFORCER: %d proxy methods exist" % len(all_methods)) - app.info("ENFORCER: %d proxy methods written" % len(WRITTEN_METHODS)) + LOG.info("ENFORCER: %d proxy methods exist" % len(all_methods)) + LOG.info("ENFORCER: %d proxy methods written" % len(WRITTEN_METHODS)) missing = all_methods - WRITTEN_METHODS missing_count = len(missing) - app.info("ENFORCER: Found %d missing proxy methods " + LOG.info("ENFORCER: Found %d missing proxy methods " "in the output" % missing_count) # TODO(shade) This is spewing a bunch of content for missing thing that @@ -104,7 +119,7 @@ def build_finished(app, exception): # We also need to deal with Proxy subclassing keystoneauth.adapter.Adapter # now - some of the warnings come from Adapter elements. for name in sorted(missing): - app.info("ENFORCER: %s was not included in the output" % name) + LOG.info("ENFORCER: %s was not included in the output" % name) if app.config.enforcer_warnings_as_errors and missing_count > 0: raise EnforcementError( From b36487a1c378b362ecf8b82a4833797492f21799 Mon Sep 17 00:00:00 2001 From: wangxiyuan Date: Thu, 11 Oct 2018 09:58:13 +0800 Subject: [PATCH 2243/3836] Add registered limit CRUD support Add unified limit CRUD support. This patch is the registered limit part. Change-Id: I6ccb00181782e8a8550fc7a51f2ceba7778a46f7 --- openstack/identity/v3/_proxy.py | 75 +++++++++++++++++++ openstack/identity/v3/registered_limit.py | 57 ++++++++++++++ openstack/resource.py | 18 +++-- .../unit/identity/v3/test_registered_limit.py | 57 ++++++++++++++ 4 files changed, 200 insertions(+), 7 deletions(-) create mode 100644 openstack/identity/v3/registered_limit.py create mode 100644 openstack/tests/unit/identity/v3/test_registered_limit.py diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 45e93df9c..6dbba819d 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -18,6 +18,7 @@ from openstack.identity.v3 import policy as _policy from openstack.identity.v3 import project as _project from openstack.identity.v3 import region as _region +from openstack.identity.v3 import registered_limit as _registered_limit from openstack.identity.v3 import role as _role from openstack.identity.v3 import role_assignment as _role_assignment from openstack.identity.v3 import role_domain_group_assignment \ @@ -972,3 +973,77 @@ def role_assignments(self, **query): """ return self._list(_role_assignment.RoleAssignment, paginated=False, **query) + + def registered_limits(self, **query): + """Retrieve a generator of registered_limits + + :param kwargs \*\*query: Optional query parameters to be sent to limit + the registered_limits being returned. + + :returns: A generator of registered_limits instances. + :rtype: :class: + `~openstack.identity.v3.registered_limit.RegisteredLimit` + """ + return self._list(_registered_limit.RegisteredLimit, paginated=False, + **query) + + def get_registered_limit(self, registered_limit): + """Get a single registered_limit + + :param registered_limit: The value can be the ID of a registered_limit + or a :class: + `~openstack.identity.v3.registered_limit.RegisteredLimit` instance. + + :returns: One :class: + `~openstack.identity.v3.registered_limit.RegisteredLimit` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_registered_limit.RegisteredLimit, registered_limit) + + def create_registered_limit(self, **attrs): + """Create a new registered_limit from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.identity.v3.registered_limit.RegisteredLimit`, + comprised of the properties on the RegisteredLimit class. + + :returns: The results of registered_limit creation. + :rtype: :class: + `~openstack.identity.v3.registered_limit.RegisteredLimit` + """ + return self._create(_registered_limit.RegisteredLimit, **attrs) + + def update_registered_limit(self, registered_limit, **attrs): + """Update a registered_limit + + :param registered_limit: Either the ID of a registered_limit. or a + :class:`~openstack.identity.v3.registered_limit.RegisteredLimit` + instance. + :param dict kwargs: The attributes to update on the registered_limit + represented by ``value``. + + :returns: The updated registered_limit. + :rtype: :class: + `~openstack.identity.v3.registered_limit.RegisteredLimit` + """ + return self._update(_registered_limit.RegisteredLimit, + registered_limit, **attrs) + + def delete_registered_limit(self, registered_limit, ignore_missing=True): + """Delete a registered_limit + + :param registered_limit: The value can be either the ID of a + registered_limit or a + :class:`~openstack.identity.v3.registered_limit.RegisteredLimit` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the registered_limit does not exist. When set to ``True``, no + exception will be thrown when attempting to delete a nonexistent + registered_limit. + + :returns: ``None`` + """ + self._delete(_registered_limit.RegisteredLimit, registered_limit, + ignore_missing=ignore_missing) diff --git a/openstack/identity/v3/registered_limit.py b/openstack/identity/v3/registered_limit.py new file mode 100644 index 000000000..227f212c2 --- /dev/null +++ b/openstack/identity/v3/registered_limit.py @@ -0,0 +1,57 @@ +# 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 resource + + +class RegisteredLimit(resource.Resource): + resource_key = 'registered_limit' + resources_key = 'registered_limits' + base_path = '/registered_limits' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + commit_method = 'PATCH' + commit_jsonpatch = True + + _query_mapping = resource.QueryParameters( + 'service_id', 'region_id', 'resource_name') + + # Properties + #: User-facing description of the registered_limit. *Type: string* + description = resource.Body('description') + #: The links for the registered_limit resource. + links = resource.Body('links') + #: ID of service. *Type: string* + service_id = resource.Body('service_id') + #: ID of region, if any. *Type: string* + region_id = resource.Body('region_id') + #: The resource name. *Type: string* + resource_name = resource.Body('resource_name') + #: The default limit value. *Type: int* + default_limit = resource.Body('default_limit') + + def _prepare_request_body(self, patch, prepend_key): + body = self._body.dirty + if prepend_key and self.resource_key is not None: + if patch: + body = {self.resource_key: body} + else: + # Keystone support bunch create for unified limit. So the + # request body for creating registered_limit is a list instead + # of dict. + body = {self.resources_key: [body]} + return body diff --git a/openstack/resource.py b/openstack/resource.py index f34c47bf7..dd645679a 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -842,6 +842,16 @@ def _to_munch(self): return munch.Munch(self.to_dict(body=True, headers=False, original_names=True)) + def _prepare_request_body(self, patch, prepend_key): + if patch: + new = self._body.attributes + body = jsonpatch.make_patch(self._original_body, new).patch + else: + body = self._body.dirty + if prepend_key and self.resource_key is not None: + body = {self.resource_key: body} + return body + def _prepare_request(self, requires_id=None, prepend_key=False, patch=False): """Prepare a request to be sent to the server @@ -861,13 +871,7 @@ def _prepare_request(self, requires_id=None, prepend_key=False, if requires_id is None: requires_id = self.requires_id - if patch: - new = self._body.attributes - body = jsonpatch.make_patch(self._original_body, new).patch - else: - body = self._body.dirty - if prepend_key and self.resource_key is not None: - body = {self.resource_key: body} + body = self._prepare_request_body(patch, prepend_key) # TODO(mordred) Ensure headers have string values better than this headers = {} diff --git a/openstack/tests/unit/identity/v3/test_registered_limit.py b/openstack/tests/unit/identity/v3/test_registered_limit.py new file mode 100644 index 000000000..dafb5a81a --- /dev/null +++ b/openstack/tests/unit/identity/v3/test_registered_limit.py @@ -0,0 +1,57 @@ +# 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.tests.unit import base + +from openstack.identity.v3 import registered_limit + +EXAMPLE = { + "service_id": "8ac43bb0926245cead88676a96c750d3", + "region_id": 'RegionOne', + "resource_name": 'cores', + "default_limit": 10, + "description": "compute cores", + "links": {"self": "http://example.com/v3/registered_limit_1"} +} + + +class TestRegistered_limit(base.TestCase): + + def test_basic(self): + sot = registered_limit.RegisteredLimit() + self.assertEqual('registered_limit', sot.resource_key) + self.assertEqual('registered_limits', sot.resources_key) + self.assertEqual('/registered_limits', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertEqual('PATCH', sot.commit_method) + + self.assertDictEqual( + { + 'service_id': 'service_id', + 'region_id': 'region_id', + 'resource_name': 'resource_name', + 'marker': 'marker', + 'limit': 'limit' + }, + sot._query_mapping._mapping) + + def test_make_it(self): + sot = registered_limit.RegisteredLimit(**EXAMPLE) + self.assertEqual(EXAMPLE['service_id'], sot.service_id) + self.assertEqual(EXAMPLE['region_id'], sot.region_id) + self.assertEqual(EXAMPLE['resource_name'], sot.resource_name) + self.assertEqual(EXAMPLE['default_limit'], sot.default_limit) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['links'], sot.links) From 17786215ed2f6116d540eef93f2f109b246364b4 Mon Sep 17 00:00:00 2001 From: wangxiyuan Date: Thu, 11 Oct 2018 15:24:42 +0800 Subject: [PATCH 2244/3836] Add limit CRUD support This patch is the limit part for unified limit CRUD support. Change-Id: I1b486b5a3dd641e932b892b8bdda03997719d893 --- openstack/identity/v3/_proxy.py | 67 +++++++++++++++++++ openstack/identity/v3/limit.py | 58 ++++++++++++++++ .../tests/unit/identity/v3/test_limit.py | 60 +++++++++++++++++ .../add-unified-limit-5ac334a08e137a70.yaml | 5 ++ 4 files changed, 190 insertions(+) create mode 100644 openstack/identity/v3/limit.py create mode 100644 openstack/tests/unit/identity/v3/test_limit.py create mode 100644 releasenotes/notes/add-unified-limit-5ac334a08e137a70.yaml diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 6dbba819d..ae0e3012d 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -15,6 +15,7 @@ from openstack.identity.v3 import domain as _domain from openstack.identity.v3 import endpoint as _endpoint from openstack.identity.v3 import group as _group +from openstack.identity.v3 import limit as _limit from openstack.identity.v3 import policy as _policy from openstack.identity.v3 import project as _project from openstack.identity.v3 import region as _region @@ -1047,3 +1048,69 @@ def delete_registered_limit(self, registered_limit, ignore_missing=True): """ self._delete(_registered_limit.RegisteredLimit, registered_limit, ignore_missing=ignore_missing) + + def limits(self, **query): + """Retrieve a generator of limits + + :param kwargs \*\*query: Optional query parameters to be sent to limit + the limits being returned. + + :returns: A generator of limits instances. + :rtype: :class:`~openstack.identity.v3.limit.Limit` + """ + return self._list(_limit.Limit, paginated=False, + **query) + + def get_limit(self, limit): + """Get a single limit + + :param limit: The value can be the ID of a limit + or a :class:`~openstack.identity.v3.limit.Limit` instance. + + :returns: One :class: + `~openstack.identity.v3.limit.Limit` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource can be found. + """ + return self._get(_limit.Limit, limit) + + def create_limit(self, **attrs): + """Create a new limit from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.identity.v3.limit.Limit`, comprised of the + properties on the Limit class. + + :returns: The results of limit creation. + :rtype: :class:`~openstack.identity.v3.limit.Limit` + """ + return self._create(_limit.Limit, **attrs) + + def update_limit(self, limit, **attrs): + """Update a limit + + :param limit: Either the ID of a limit. or a + :class:`~openstack.identity.v3.limit.Limit` instance. + :param dict kwargs: The attributes to update on the limit represented + by ``value``. + + :returns: The updated limit. + :rtype: :class:`~openstack.identity.v3.limit.Limit` + """ + return self._update(_limit.Limit, + limit, **attrs) + + def delete_limit(self, limit, ignore_missing=True): + """Delete a limit + + :param limit: The value can be either the ID of a limit or a + :class:`~openstack.identity.v3.limit.Limit` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the limit does not exist. When set to ``True``, no exception will + be thrown when attempting to delete a nonexistent limit. + + :returns: ``None`` + """ + self._delete(limit.Limit, limit, + ignore_missing=ignore_missing) diff --git a/openstack/identity/v3/limit.py b/openstack/identity/v3/limit.py new file mode 100644 index 000000000..280e6ca80 --- /dev/null +++ b/openstack/identity/v3/limit.py @@ -0,0 +1,58 @@ +# 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 resource + + +class Limit(resource.Resource): + resource_key = 'limit' + resources_key = 'limits' + base_path = '/limits' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + commit_method = 'PATCH' + commit_jsonpatch = True + + _query_mapping = resource.QueryParameters( + 'service_id', 'region_id', 'resource_name', 'project_id') + + # Properties + #: User-facing description of the registered_limit. *Type: string* + description = resource.Body('description') + #: The links for the registered_limit resource. + links = resource.Body('links') + #: ID of service. *Type: string* + service_id = resource.Body('service_id') + #: ID of region, if any. *Type: string* + region_id = resource.Body('region_id') + #: The resource name. *Type: string* + resource_name = resource.Body('resource_name') + #: The resource limit value. *Type: int* + resource_limit = resource.Body('resource_limit') + #: ID of project. *Type: string* + project_id = resource.Body('project_id') + + def _prepare_request_body(self, patch, prepend_key): + body = self._body.dirty + if prepend_key and self.resource_key is not None: + if patch: + body = {self.resource_key: body} + else: + # Keystone support bunch create for unified limit. So the + # request body for creating limit is a list instead of dict. + body = {self.resources_key: [body]} + return body diff --git a/openstack/tests/unit/identity/v3/test_limit.py b/openstack/tests/unit/identity/v3/test_limit.py new file mode 100644 index 000000000..3d1bec8de --- /dev/null +++ b/openstack/tests/unit/identity/v3/test_limit.py @@ -0,0 +1,60 @@ +# 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.tests.unit import base + +from openstack.identity.v3 import limit + +EXAMPLE = { + "service_id": "8ac43bb0926245cead88676a96c750d3", + "region_id": 'RegionOne', + "resource_name": 'cores', + "resource_limit": 10, + "project_id": 'a8455cdd4249498f99b63d5af2fb4bc8', + "description": "compute cores for project 123", + "links": {"self": "http://example.com/v3/limit_1"} +} + + +class TestLimit(base.TestCase): + + def test_basic(self): + sot = limit.Limit() + self.assertEqual('limit', sot.resource_key) + self.assertEqual('limits', sot.resources_key) + self.assertEqual('/limits', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertEqual('PATCH', sot.commit_method) + + self.assertDictEqual( + { + 'service_id': 'service_id', + 'region_id': 'region_id', + 'resource_name': 'resource_name', + 'project_id': 'project_id', + 'marker': 'marker', + 'limit': 'limit' + }, + sot._query_mapping._mapping) + + def test_make_it(self): + sot = limit.Limit(**EXAMPLE) + self.assertEqual(EXAMPLE['service_id'], sot.service_id) + self.assertEqual(EXAMPLE['region_id'], sot.region_id) + self.assertEqual(EXAMPLE['resource_name'], sot.resource_name) + self.assertEqual(EXAMPLE['resource_limit'], sot.resource_limit) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['links'], sot.links) diff --git a/releasenotes/notes/add-unified-limit-5ac334a08e137a70.yaml b/releasenotes/notes/add-unified-limit-5ac334a08e137a70.yaml new file mode 100644 index 000000000..8bb65ec01 --- /dev/null +++ b/releasenotes/notes/add-unified-limit-5ac334a08e137a70.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added the unified limits basic CRUD methods. It includes two kinds of + resources: `registered_limit` and `limit`. From d91723dbabe690c97784b43e3a30571d9d9dac1d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 18 Oct 2018 10:25:53 -0500 Subject: [PATCH 2245/3836] Support v4-fixed-ip and v6-fixed-ip in create_server novaclient supports v4-fixed-ip and v6-fixed-ip as aliases for fixed_ip. As shade used to use novaclient and passed this content through the switch to REST resulted in a behavior regression for shade users. openstackclient also exposes these for the --nics parameter, so it's important that we also support it as we look forward to switching OSC to sdk. Change-Id: I3da91b6f18528b515a45d868e8c3faf824ade5a0 --- openstack/cloud/openstackcloud.py | 14 +- .../tests/unit/cloud/test_create_server.py | 146 ++++++++++++++++++ .../notes/v4-fixed-ip-325740fdae85ffa9.yaml | 7 + 3 files changed, 163 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/v4-fixed-ip-325740fdae85ffa9.yaml diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 5e0c68b54..518f3788c 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -6963,13 +6963,19 @@ def create_server( "Requested network {net} could not be found.".format( net=nic['net-name'])) net['uuid'] = nic_net['id'] + for ip_key in ('v4-fixed-ip', 'v6-fixed-ip', 'fixed_ip'): + fixed_ip = nic.pop(ip_key, None) + if fixed_ip and net.get('fixed_ip'): + raise exc.OpenStackCloudException( + "Only one of v4-fixed-ip, v6-fixed-ip or fixed_ip" + " may be given") + if fixed_ip: + net['fixed_ip'] = fixed_ip # TODO(mordred) Add support for tag if server supports microversion # 2.32-2.36 or >= 2.42 - for key in ('port', 'fixed_ip'): + for key in ('port', 'port-id'): if key in nic: - net[key] = nic.pop(key) - if 'port-id' in nic: - net['port'] = nic.pop('port-id') + net['port'] = nic.pop(key) if nic: raise exc.OpenStackCloudException( "Additional unsupported keys given for server network" diff --git a/openstack/tests/unit/cloud/test_create_server.py b/openstack/tests/unit/cloud/test_create_server.py index 616d45eb3..af43c2d72 100644 --- a/openstack/tests/unit/cloud/test_create_server.py +++ b/openstack/tests/unit/cloud/test_create_server.py @@ -632,6 +632,152 @@ def test_create_server_network_with_empty_nics(self): network='network-name', nics=[]) self.assert_calls() + def test_create_server_network_fixed_ip(self): + """ + Verify that if 'fixed_ip' is supplied in nics, we pass it to networks + appropriately. + """ + network = { + 'id': 'network-id', + 'name': 'network-name' + } + fixed_ip = '10.0.0.1' + build_server = fakes.make_fake_server('1234', '', 'BUILD') + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers']), + json={'server': build_server}, + validate=dict( + json={'server': { + u'flavorRef': u'flavor-id', + u'imageRef': u'image-id', + u'max_count': 1, + u'min_count': 1, + u'networks': [{'fixed_ip': fixed_ip}], + u'name': u'server-name'}})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234']), + json={'server': build_server}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': [network]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets.json']), + json={'subnets': []}), + ]) + self.cloud.create_server( + 'server-name', dict(id='image-id'), dict(id='flavor-id'), + nics=[{'fixed_ip': fixed_ip}]) + self.assert_calls() + + def test_create_server_network_v4_fixed_ip(self): + """ + Verify that if 'v4-fixed-ip' is supplied in nics, we pass it to + networks appropriately. + """ + network = { + 'id': 'network-id', + 'name': 'network-name' + } + fixed_ip = '10.0.0.1' + build_server = fakes.make_fake_server('1234', '', 'BUILD') + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers']), + json={'server': build_server}, + validate=dict( + json={'server': { + u'flavorRef': u'flavor-id', + u'imageRef': u'image-id', + u'max_count': 1, + u'min_count': 1, + u'networks': [{'fixed_ip': fixed_ip}], + u'name': u'server-name'}})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234']), + json={'server': build_server}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': [network]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets.json']), + json={'subnets': []}), + ]) + self.cloud.create_server( + 'server-name', dict(id='image-id'), dict(id='flavor-id'), + nics=[{'fixed_ip': fixed_ip}]) + self.assert_calls() + + def test_create_server_network_v6_fixed_ip(self): + """ + Verify that if 'v6-fixed-ip' is supplied in nics, we pass it to + networks appropriately. + """ + network = { + 'id': 'network-id', + 'name': 'network-name' + } + # Note - it doesn't actually have to be a v6 address - it's just + # an alias. + fixed_ip = 'fe80::28da:5fff:fe57:13ed' + build_server = fakes.make_fake_server('1234', '', 'BUILD') + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers']), + json={'server': build_server}, + validate=dict( + json={'server': { + u'flavorRef': u'flavor-id', + u'imageRef': u'image-id', + u'max_count': 1, + u'min_count': 1, + u'networks': [{'fixed_ip': fixed_ip}], + u'name': u'server-name'}})), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234']), + json={'server': build_server}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': [network]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets.json']), + json={'subnets': []}), + ]) + self.cloud.create_server( + 'server-name', dict(id='image-id'), dict(id='flavor-id'), + nics=[{'fixed_ip': fixed_ip}]) + self.assert_calls() + + def test_create_server_network_fixed_ip_conflicts(self): + """ + Verify that if 'fixed_ip' and 'v4-fixed-ip' are both supplied in nics, + we throw an exception. + """ + # Note - it doesn't actually have to be a v6 address - it's just + # an alias. + self.use_nothing() + fixed_ip = '10.0.0.1' + self.assertRaises( + exc.OpenStackCloudException, self.cloud.create_server, + 'server-name', dict(id='image-id'), dict(id='flavor-id'), + nics=[{ + 'fixed_ip': fixed_ip, + 'v4-fixed-ip': fixed_ip + }]) + self.assert_calls() + def test_create_server_get_flavor_image(self): self.use_glance() image_id = str(uuid.uuid4()) diff --git a/releasenotes/notes/v4-fixed-ip-325740fdae85ffa9.yaml b/releasenotes/notes/v4-fixed-ip-325740fdae85ffa9.yaml new file mode 100644 index 000000000..99fe5b8d0 --- /dev/null +++ b/releasenotes/notes/v4-fixed-ip-325740fdae85ffa9.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Re-added support for `v4-fixed-ip` and `v6-fixed-ip` in the `nics` + parameter to `create_server`. These are aliaes for `fixed_ip` provided + by novaclient which shade used to use. The switch to REST didn't include + support for these aliases, resulting in a behavior regression. From 3ba6e5fe5e7dd9355b27172f004203612a37dee2 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 2 Feb 2018 07:24:19 -0600 Subject: [PATCH 2246/3836] Add all_projects as a preferred alias for all_tenants Now that we support both server-side and client-side names for parameters, we can add an all_projects alias. We already use project_id in resources instead of tenant_id. Put in logic to only pass all_projects along to the underlying list if it's True. That way we can easily use the interface with True/False and only send query parameters when we need to. Pehaps we could enhance QueryParameters to understand default values of parameters and that they don't need to be sent? Change-Id: I66117ab4c82d30ae3700e17f7598b3d3fbbac60d --- openstack/block_storage/v2/_proxy.py | 4 ++-- openstack/block_storage/v2/snapshot.py | 2 +- openstack/block_storage/v2/volume.py | 2 +- openstack/compute/v2/_proxy.py | 6 +++++- openstack/compute/v2/server.py | 5 +++-- openstack/tests/unit/block_storage/v2/test_snapshot.py | 2 +- openstack/tests/unit/block_storage/v2/test_volume.py | 2 +- openstack/tests/unit/compute/v2/test_server.py | 2 +- 8 files changed, 15 insertions(+), 10 deletions(-) diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 88091c45d..cabff42b1 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -45,7 +45,7 @@ def snapshots(self, details=True, **query): the snapshots being returned. Available parameters include: * name: Name of the snapshot as a string. - * all_tenants: Whether return the snapshots of all tenants. + * all_projects: Whether return the snapshots in all projects. * volume_id: volume id of a snapshot. * status: Value of the status of the snapshot so that you can filter on "available" for example. @@ -154,7 +154,7 @@ def volumes(self, details=True, **query): the volumes being returned. Available parameters include: * name: Name of the volume as a string. - * all_tenants: Whether return the volumes of all tenants + * all_projects: Whether return the volumes in all projects * status: Value of the status of the volume so that you can filter on "available" for example. diff --git a/openstack/block_storage/v2/snapshot.py b/openstack/block_storage/v2/snapshot.py index d9137934c..4962014cf 100644 --- a/openstack/block_storage/v2/snapshot.py +++ b/openstack/block_storage/v2/snapshot.py @@ -20,7 +20,7 @@ class Snapshot(resource.Resource): base_path = "/snapshots" _query_mapping = resource.QueryParameters( - 'all_tenants', 'name', 'status', 'volume_id') + 'name', 'status', 'volume_id', all_projects='all_tenants') # capabilities allow_fetch = True diff --git a/openstack/block_storage/v2/volume.py b/openstack/block_storage/v2/volume.py index 4402e624f..215e45ed1 100644 --- a/openstack/block_storage/v2/volume.py +++ b/openstack/block_storage/v2/volume.py @@ -20,7 +20,7 @@ class Volume(resource.Resource): base_path = "/volumes" _query_mapping = resource.QueryParameters( - 'all_tenants', 'name', 'status', 'project_id') + 'name', 'status', 'project_id', all_projects='all_tenants') # capabilities allow_fetch = True diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 20c7ebffb..b0a14ede8 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -371,7 +371,7 @@ def get_server(self, server): """ return self._get(_server.Server, server) - def servers(self, details=True, **query): + def servers(self, details=True, all_projects=False, **query): """Retrieve a generator of servers :param bool details: When set to ``False`` @@ -395,6 +395,8 @@ def servers(self, details=True, **query): * status: Value of the status of the server so that you can filter on "ACTIVE" for example. * host: Name of the host as a string. + * all_projects: Flag to request servers be returned from all + projects, not just the currently scoped one. * limit: Requests a specified page size of returned items from the query. Returns a number of items up to the specified limit value. Use the limit parameter to make an initial @@ -408,6 +410,8 @@ def servers(self, details=True, **query): :returns: A generator of server instances. """ + if all_projects: + query['all_projects'] = True srv = _server.ServerDetail if details else _server.Server return self._list(srv, paginated=True, **query) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 2207b2f8c..c60a7962c 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -29,7 +29,7 @@ class Server(resource.Resource, metadata.MetadataMixin): _query_mapping = resource.QueryParameters( "image", "flavor", "name", - "status", "host", "all_tenants", + "status", "host", "sort_key", "sort_dir", "reservation_id", "tags", "project_id", @@ -39,7 +39,8 @@ class Server(resource.Resource, metadata.MetadataMixin): is_deleted="deleted", ipv4_address="ip", ipv6_address="ip6", - changes_since="changes-since") + changes_since="changes-since", + all_projects="all_tenants") #: A list of dictionaries holding links relevant to this server. links = resource.Body('links') diff --git a/openstack/tests/unit/block_storage/v2/test_snapshot.py b/openstack/tests/unit/block_storage/v2/test_snapshot.py index 16b59355c..e502f8b70 100644 --- a/openstack/tests/unit/block_storage/v2/test_snapshot.py +++ b/openstack/tests/unit/block_storage/v2/test_snapshot.py @@ -53,7 +53,7 @@ def test_basic(self): self.assertDictEqual({"name": "name", "status": "status", - "all_tenants": "all_tenants", + "all_projects": "all_tenants", "volume_id": "volume_id", "limit": "limit", "marker": "marker"}, diff --git a/openstack/tests/unit/block_storage/v2/test_volume.py b/openstack/tests/unit/block_storage/v2/test_volume.py index 3dc40afd0..739ffc5a2 100644 --- a/openstack/tests/unit/block_storage/v2/test_volume.py +++ b/openstack/tests/unit/block_storage/v2/test_volume.py @@ -76,7 +76,7 @@ def test_basic(self): self.assertDictEqual({"name": "name", "status": "status", - "all_tenants": "all_tenants", + "all_projects": "all_tenants", "project_id": "project_id", "limit": "limit", "marker": "marker"}, diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 7de99b494..a39e937c0 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -84,7 +84,7 @@ def test_basic(self): "name": "name", "status": "status", "host": "host", - "all_tenants": "all_tenants", + "all_projects": "all_tenants", "changes_since": "changes-since", "limit": "limit", "marker": "marker", From 5d7a44c3cc104d9e54b90974ec2634fdde1a78b9 Mon Sep 17 00:00:00 2001 From: Daniel Speichert Date: Sat, 20 Oct 2018 22:17:34 -0400 Subject: [PATCH 2247/3836] Fix upload of Swift object smaller than segment limit (create_object) Opening file in text mode results in silently failing upload Change-Id: I0aed6f7080076d44f0c75bf58628eb39406ed6ef --- openstack/cloud/openstackcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 518f3788c..5251966b8 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -7784,7 +7784,7 @@ def _upload_object_data(self, endpoint, data, headers): def _upload_object(self, endpoint, filename, headers): return _adapter._json_response(self.object_store.put( - endpoint, headers=headers, data=open(filename, 'r'))) + endpoint, headers=headers, data=open(filename, 'rb'))) def _get_file_segments(self, endpoint, filename, file_size, segment_size): # Use an ordered dict here so that testing can replicate things From 448cda91e3ea2a9234b150ad6bfd5228472212ed Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 21 Oct 2018 10:27:38 -0500 Subject: [PATCH 2248/3836] Don't start task managers passed in to Connection If someone (such as our friend nodepool) creates and passes in a TaskManager, it should be assumed that the calling context controls the lifecycle of that TaskManager. In that case, don't run start() on it. Depends-On: https://review.openstack.org/612168 Change-Id: I0ac5dc428250158471cb64d5b1601cabbb4deb86 --- openstack/connection.py | 11 ++++++++--- .../notes/no-start-task-manager-56773f3ea5eb3a59.yaml | 6 ++++++ 2 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/no-start-task-manager-56773f3ea5eb3a59.yaml diff --git a/openstack/connection.py b/openstack/connection.py index b67cc5905..945283598 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -288,9 +288,14 @@ def __init__(self, cloud=None, config=None, session=None, load_envvars=cloud is not None, **kwargs) - self.task_manager = task_manager or _task_manager.TaskManager( - self.config.full_name) - self.task_manager.start() + if task_manager: + # If a TaskManager was passed in, don't start it, assume it's + # under the control of the calling context. + self.task_manager = task_manager + else: + self.task_manager = _task_manager.TaskManager( + self.config.full_name) + self.task_manager.start() self._session = None self._proxies = {} diff --git a/releasenotes/notes/no-start-task-manager-56773f3ea5eb3a59.yaml b/releasenotes/notes/no-start-task-manager-56773f3ea5eb3a59.yaml new file mode 100644 index 000000000..e40507d12 --- /dev/null +++ b/releasenotes/notes/no-start-task-manager-56773f3ea5eb3a59.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixed a regression in the new `TaskManager` code which caused programs that + were passing in a `TaskManager` that they had been running `start` on to + fail due to a double call. From 89e7d77e3ac1bec50f7c2b70b8f64a66d3167365 Mon Sep 17 00:00:00 2001 From: Maxim Babushkin Date: Sun, 21 Oct 2018 15:39:31 +0300 Subject: [PATCH 2249/3836] Add vnic_type to create_port valid kwargs Add the ability to specify port_type for created and updated port. Change-Id: I6a228340d39df252014342fff6aacf46824e5bd6 --- openstack/cloud/openstackcloud.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 518f3788c..8680d53c2 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -8378,7 +8378,7 @@ def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, @_utils.valid_kwargs('name', 'admin_state_up', 'mac_address', 'fixed_ips', 'subnet_id', 'ip_address', 'security_groups', 'allowed_address_pairs', 'extra_dhcp_opts', - 'device_owner', 'device_id') + 'device_owner', 'device_id', 'binding:vnic_type') def create_port(self, network_id, **kwargs): """Create a port @@ -8427,6 +8427,7 @@ def create_port(self, network_id, **kwargs): For example, a DHCP agent. (Optional) :param device_id: The ID of the device that uses this port. For example, a virtual server. (Optional) + :param binding vnic_type: The type of the created port. (Optional) :returns: a ``munch.Munch`` describing the created port. @@ -8442,7 +8443,8 @@ def create_port(self, network_id, **kwargs): @_utils.valid_kwargs('name', 'admin_state_up', 'fixed_ips', 'security_groups', 'allowed_address_pairs', - 'extra_dhcp_opts', 'device_owner', 'device_id') + 'extra_dhcp_opts', 'device_owner', 'device_id', + 'binding:vnic_type') def update_port(self, name_or_id, **kwargs): """Update a port @@ -8488,6 +8490,7 @@ def update_port(self, name_or_id, **kwargs): :param device_owner: The ID of the entity that uses this port. For example, a DHCP agent. (Optional) :param device_id: The ID of the resource this port is attached to. + :param binding vnic_type: The type of the created port. (Optional) :returns: a ``munch.Munch`` describing the updated port. From 09b10cfecb7db8c0917169c05fbe7b782fe1d823 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 22 Sep 2018 07:59:04 -0500 Subject: [PATCH 2250/3836] Add support for per-service rate limits Rate limits on the server-side are per-service, but the rate limit in the TaskManager is a single rate. Add support for a dict of rate limits, keyed by service-type. The primary user interface should be passing rate to the Connection constructor. That takes calls-per-second for both scalar and dict versions of its rate parameter. Depends-On: https://review.openstack.org/612168/ Change-Id: If0ff77b43adc1f6f0bb1e7c08908930a95508b31 --- openstack/_adapter.py | 12 +++++- openstack/config/cloud_region.py | 26 +++++++++++- openstack/connection.py | 10 ++++- openstack/task_manager.py | 40 ++++++++++++++----- openstack/tests/unit/base.py | 2 + .../tests/unit/cloud/test_task_manager.py | 19 +++++++++ openstack/tests/unit/test_connection.py | 16 ++++++++ ...ient-side-rate-limit-ddb82df7cb92091c.yaml | 8 ++++ 8 files changed, 118 insertions(+), 15 deletions(-) create mode 100644 releasenotes/notes/expose-client-side-rate-limit-ddb82df7cb92091c.yaml diff --git a/openstack/_adapter.py b/openstack/_adapter.py index dfe2e3691..7f5da392b 100644 --- a/openstack/_adapter.py +++ b/openstack/_adapter.py @@ -116,11 +116,18 @@ class OpenStackSDKAdapter(adapter.Adapter): This allows using the nodepool MultiThreaded Rate Limiting TaskManager. """ - def __init__(self, session=None, task_manager=None, *args, **kwargs): + def __init__( + self, session=None, + task_manager=None, + rate_limit=None, concurrency=None, + *args, **kwargs): super(OpenStackSDKAdapter, self).__init__( session=session, *args, **kwargs) if not task_manager: - task_manager = _task_manager.TaskManager(name=self.service_type) + task_manager = _task_manager.TaskManager( + name=self.service_type, + rate=rate_limit, + workers=concurrency) task_manager.start() self.task_manager = task_manager @@ -143,6 +150,7 @@ def request( ret = self.task_manager.submit_function( request_method, run_async=True, name=name, connect_retries=connect_retries, raise_exc=raise_exc, + tag=self.service_type, **kwargs) if run_async: return ret diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 1bb421d84..7568673bf 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -15,7 +15,6 @@ import copy import warnings -from keystoneauth1 import adapter from keystoneauth1 import discover import keystoneauth1.exceptions.catalog from keystoneauth1 import session as ks_session @@ -23,6 +22,7 @@ import requestsexceptions from six.moves import urllib +from openstack import _adapter from openstack import version as openstack_version from openstack import _log from openstack.config import _util @@ -247,6 +247,17 @@ def _get_config( value = converter(value) return value + def _get_service_config(self, key, service_type): + config_dict = self.config.get(key) + if not config_dict: + return None + if not isinstance(config_dict, dict): + return config_dict + + for st in self._service_type_manager.get_all_types(service_type): + if st in config_dict: + return config_dict[st] + def get_interface(self, service_type=None): return self._get_config( 'interface', service_type, fallback_to_unprefixed=True) @@ -438,7 +449,8 @@ def get_all_version_data(self, service_type): return interface_versions.get(service_type, []) def get_session_client( - self, service_type, version=None, constructor=adapter.Adapter, + self, service_type, version=None, + constructor=_adapter.OpenStackSDKAdapter, **kwargs): """Return a prepped keystoneauth Adapter for a given service. @@ -498,6 +510,8 @@ def get_session_client( max_version=max_api_version, endpoint_override=endpoint_override, default_microversion=version_request.default_microversion, + rate_limit=self.get_rate_limit(service_type), + concurrency=self.get_concurrency(service_type), **kwargs) if version_request.default_microversion: default_microversion = version_request.default_microversion @@ -724,3 +738,11 @@ def get_client_config(self, name=None, defaults=None): def get_password_callback(self): return self._password_callback + + def get_rate_limit(self, service_type=None): + return self._get_service_config( + 'rate_limit', service_type=service_type) + + def get_concurrency(self, service_type=None): + return self._get_service_config( + 'concurrency', service_type=service_type) diff --git a/openstack/connection.py b/openstack/connection.py index 945283598..ad094319e 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -220,6 +220,7 @@ def __init__(self, cloud=None, config=None, session=None, strict=False, use_direct_get=False, task_manager=None, + rate_limit=None, **kwargs): """Create a connection to a cloud. @@ -262,6 +263,12 @@ def __init__(self, cloud=None, config=None, session=None, Defaults to None which causes a direct-action Task Manager to be used. :type manager: :class:`~openstack.task_manager.TaskManager` + :param rate_limit: + Client-side rate limit, expressed in calls per second. The + parameter can either be a single float, or it can be a dict with + keys as service-type and values as floats expressing the calls + per second for that service. Defaults to None, which means no + rate-limiting is performed. :param kwargs: If a config is not provided, the rest of the parameters provided are assumed to be arguments to be passed to the CloudRegion contructor. @@ -294,7 +301,8 @@ def __init__(self, cloud=None, config=None, session=None, self.task_manager = task_manager else: self.task_manager = _task_manager.TaskManager( - self.config.full_name) + self.config.full_name, + rate=rate_limit) self.task_manager.start() self._session = None diff --git a/openstack/task_manager.py b/openstack/task_manager.py index 6a58bb06a..37c5ed1ee 100644 --- a/openstack/task_manager.py +++ b/openstack/task_manager.py @@ -45,7 +45,9 @@ class Task(object): the main payload at execution time. """ - def __init__(self, main=None, name=None, run_async=False, *args, **kwargs): + def __init__( + self, main=None, name=None, run_async=False, + tag=None, *args, **kwargs): self._exception = None self._traceback = None self._result = None @@ -56,6 +58,7 @@ def __init__(self, main=None, name=None, run_async=False, *args, **kwargs): self.args = args self.kwargs = kwargs self.name = name or type(self).__name__ + self.tag = tag def main(self): return self._main(*self.args, **self.kwargs) @@ -103,12 +106,22 @@ def __init__(self, name, rate=None, log=_log, workers=5, **kwargs): self.daemon = True self.queue = queue.Queue() self._running = True - if rate is not None: - rate = float(rate) - self.rate = rate + if isinstance(rate, dict): + self._waits = {} + for (k, v) in rate.items(): + if v: + self._waits[k] = 1.0 / v + else: + if rate: + self._waits = {None: 1.0 / rate} + else: + self._waits = {} self._thread = threading.Thread(name=name, target=self.run) self._thread.daemon = True + def _get_wait(self, tag): + return self._waits.get(tag, self._waits.get(None)) + @property def executor(self): if not self._executor: @@ -129,7 +142,7 @@ def join(self): self._thread.join() def run(self): - last_ts = 0 + last_ts_dict = {} try: while True: task = self.queue.get() @@ -137,12 +150,15 @@ def run(self): if not self._running: break continue - if self.rate: + wait = self._get_wait(task.tag) + if wait: + last_ts = last_ts_dict.get(task.tag, 0) while True: delta = time.time() - last_ts - if delta >= self.rate: + if delta >= wait: break - time.sleep(self.rate - delta) + time.sleep(wait - delta) + last_ts_dict[task.tag] = time.time() self._log.debug( "TaskManager {name} queue size: {size})".format( name=self.name, @@ -171,12 +187,14 @@ def submit_task(self, task): return task.wait() def submit_function( - self, method, name=None, run_async=False, *args, **kwargs): + self, method, name=None, run_async=False, tag=None, + *args, **kwargs): """ Allows submitting an arbitrary method for work. :param method: Callable to run in the TaskManager. :param str name: Name to use for the generated Task object. :param bool run_async: Whether to run this task async or not. + :param str tag: Named rate-limiting context for the task. :param args: positional arguments to pass to the method when it runs. :param kwargs: keyword arguments to pass to the method when it runs. """ @@ -185,10 +203,12 @@ def submit_function( self.executor.submit, method, *args, **kwargs) task = Task( main=payload, name=name, - run_async=run_async) + run_async=run_async, + tag=tag) else: task = Task( main=method, name=name, + tag=tag, *args, **kwargs) return self.submit_task(task) diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 8062486bf..993346772 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -124,6 +124,8 @@ def _nosleep(seconds): self.strict_cloud = openstack.connection.Connection( config=self.cloud_config, strict=True) + self.addCleanup(self.cloud.task_manager.stop) + self.addCleanup(self.strict_cloud.task_manager.stop) # FIXME(notmorgan): Convert the uri_registry, discovery.json, and # use of keystone_v3/v2 to a proper fixtures.Fixture. For now this diff --git a/openstack/tests/unit/cloud/test_task_manager.py b/openstack/tests/unit/cloud/test_task_manager.py index 6c4b19c0c..9f471a208 100644 --- a/openstack/tests/unit/cloud/test_task_manager.py +++ b/openstack/tests/unit/cloud/test_task_manager.py @@ -63,6 +63,25 @@ def main(self): return set([1, 2]) +class TestRateTransforms(base.TestCase): + + def test_rate_parameter_scalar(self): + manager = task_manager.TaskManager(name='test', rate=0.1234) + self.assertEqual(1 / 0.1234, manager._get_wait('compute')) + self.assertEqual(1 / 0.1234, manager._get_wait(None)) + + def test_rate_parameter_dict(self): + manager = task_manager.TaskManager( + name='test', + rate={ + 'compute': 20, + 'network': 10, + }) + self.assertEqual(1 / 20, manager._get_wait('compute')) + self.assertEqual(1 / 10, manager._get_wait('network')) + self.assertIsNone(manager._get_wait('object-store')) + + class TestTaskManager(base.TestCase): def setUp(self): diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index a14e5af2d..2605525d7 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -84,6 +84,22 @@ def test_session_provided(self): self.assertEqual(mock_session, conn.session) self.assertEqual('auth.example.com', conn.config.name) + def test_task_manager_rate_scalar(self): + conn = connection.Connection(cloud='sample', rate_limit=20) + self.assertEqual(1 / 20, conn.task_manager._get_wait('object-store')) + self.assertEqual(1 / 20, conn.task_manager._get_wait(None)) + + def test_task_manager_rate_dict(self): + conn = connection.Connection( + cloud='sample', + rate_limit={ + 'compute': 20, + 'network': 10, + }) + self.assertEqual(1 / 20, conn.task_manager._get_wait('compute')) + self.assertEqual(1 / 10, conn.task_manager._get_wait('network')) + self.assertIsNone(conn.task_manager._get_wait('object-store')) + def test_create_session(self): conn = connection.Connection(cloud='sample') self.assertIsNotNone(conn) diff --git a/releasenotes/notes/expose-client-side-rate-limit-ddb82df7cb92091c.yaml b/releasenotes/notes/expose-client-side-rate-limit-ddb82df7cb92091c.yaml new file mode 100644 index 000000000..3d7b503f3 --- /dev/null +++ b/releasenotes/notes/expose-client-side-rate-limit-ddb82df7cb92091c.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Client-side rate limiting is now directly exposed via ``rate_limit`` + and ``concurrency`` parameters. A single value can be given that applies + to all services, or a dict of service-type and value if different + client-side rate or concurrency limits should be used for different + services. From f44e9c658eefd354d4672d28621e0e4403582a10 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 22 Oct 2018 09:19:33 -0500 Subject: [PATCH 2251/3836] Stop running shade tests We updated shade to not be a subclass of openstacksdk and instead just be a frozen maint-mode library. We don't need to co-gate with it anymore then, as sdk changes should not have impact on shade. Change-Id: If808a3e6b94e93c5b5934da8c91e06d8b6ded1b9 --- .zuul.yaml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index d08f38b08..31394e229 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -8,7 +8,6 @@ # openstacksdk in required-projects so that osc and keystoneauth # can add the job as well required-projects: - - openstack-infra/shade - openstack/keystoneauth - openstack/openstacksdk - openstack/os-client-config @@ -36,8 +35,6 @@ # make sure stable branches of openstacksdk never get cloned by other # people, since stable branches of openstacksdk are, well, not actually # things. - - name: openstack-infra/shade - override-branch: master - name: openstack/openstacksdk override-branch: master - name: openstack/os-client-config @@ -182,7 +179,6 @@ Run openstacksdk functional tests with tips of library dependencies against a master devstack. required-projects: - - openstack-infra/shade - openstack/keystoneauth - openstack/openstacksdk - openstack/os-client-config @@ -226,7 +222,7 @@ name: openstacksdk-functional-devstack-senlin parent: openstacksdk-functional-devstack description: | - Run shade functional tests against a master devstack with senlin + Run openstacksdk functional tests against a master devstack with senlin required-projects: - openstack/senlin vars: @@ -372,8 +368,6 @@ - osc-tox-unit-tips - publish-openstack-docs-pti - release-notes-jobs-python3 - - shade-functional-tips - - shade-tox-tips check: jobs: - openstacksdk-ansible-devel-functional-devstack: From 473e7d816819e19e19b382c5ffdc3f4a83d2b6fc Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 22 Oct 2018 18:30:23 -0500 Subject: [PATCH 2252/3836] Use python3 format syntax for citycloud We don't use % substitutions, we use .format() substitutions. Change-Id: Ia340d4fa3e72d379ea1ef2d6f71dc60fcdf5beb5 --- openstack/config/vendors/citycloud.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/config/vendors/citycloud.json b/openstack/config/vendors/citycloud.json index eee22a852..057b5100c 100644 --- a/openstack/config/vendors/citycloud.json +++ b/openstack/config/vendors/citycloud.json @@ -2,7 +2,7 @@ "name": "citycloud", "profile": { "auth": { - "auth_url": "https://%(region_name)s.citycloud.com:5000/v3/" + "auth_url": "https://{region_name}.citycloud.com:5000/v3/" }, "regions": [ "Buf1", From c080f5a2afc4485cfc16c0e51c730043db2e3ec5 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 24 Oct 2018 20:59:55 +0200 Subject: [PATCH 2253/3836] Don't pass disk_format or container_format to image task upload In task upload we update the image properties after importing the image. Attempting to set disk_format/container_format at that point is not the right choice in life. Depends-On: https://review.openstack.org/613438 Change-Id: I3b086e83514a71cea0bb4119d75c48c153099141 --- openstack/cloud/openstackcloud.py | 5 +++-- openstack/tests/unit/cloud/test_image.py | 1 + releasenotes/notes/fix-image-task-ae79502dd5c7ecba.yaml | 6 ++++++ 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/fix-image-task-ae79502dd5c7ecba.yaml diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 518f3788c..86d914c84 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -4973,12 +4973,13 @@ def _upload_image_task( self, name, filename, wait, timeout, meta, **image_kwargs): - properties = image_kwargs['properties'] + properties = image_kwargs.pop('properties', {}) md5 = properties[self._IMAGE_MD5_KEY] sha256 = properties[self._IMAGE_SHA256_KEY] container = properties[self._IMAGE_OBJECT_KEY].split('/', 1)[0] - properties = image_kwargs.pop('properties', {}) image_kwargs.update(properties) + image_kwargs.pop('disk_format', None) + image_kwargs.pop('container_format', None) self.create_container(container) self.create_object( diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index e6927a516..feba4d2c3 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -487,6 +487,7 @@ def test_create_image_task(self): self.cloud.create_image( self.image_name, self.imagefile.name, wait=True, timeout=1, + disk_format='vhd', container_format='ovf', is_public=False, container=self.container_name) self.assert_calls() diff --git a/releasenotes/notes/fix-image-task-ae79502dd5c7ecba.yaml b/releasenotes/notes/fix-image-task-ae79502dd5c7ecba.yaml new file mode 100644 index 000000000..8a6513b3c --- /dev/null +++ b/releasenotes/notes/fix-image-task-ae79502dd5c7ecba.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixed a regression in image upload when the cloud uses the task + upload method. A refactor led to attempting to update the disk_format + and container_format values after the image had been imported. From dd5f0f68274df4106902aa71a3f882d70c673dab Mon Sep 17 00:00:00 2001 From: Ian Wienand Date: Fri, 26 Oct 2018 14:47:44 +1100 Subject: [PATCH 2254/3836] Call pre/post run task calls from TaskManager.submit_task() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since I33325fb5be21264df0a68ceef2202ab7875f63ec, the task.run() call in TaskManager.run_task() is now an asynchronous submission to the threadpool, rather than a synchronous call to the task's actual function. This means taking the elapsed_time around this call is no longer an indication of the task's runtime, but always comes out as just a few μs for the insertion. Move the pre and post calls into TaskManager.submit_task() where the elapsed_time will reflect the time between insertion into the queue and the wait() return of its result. Update documentation for pre/post tasks, and add test-cases. Depends-On: https://review.openstack.org/613438 Change-Id: I8617ab2895d1544a6902ae5a3d6a97b87bfd2ec9 --- openstack/task_manager.py | 32 +++++++++++++++---- .../tests/unit/cloud/test_task_manager.py | 23 +++++++++++++ .../fix-task-timing-048afea680adc62e.yaml | 5 +++ 3 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/fix-task-timing-048afea680adc62e.yaml diff --git a/openstack/task_manager.py b/openstack/task_manager.py index 6a58bb06a..259d0ab7c 100644 --- a/openstack/task_manager.py +++ b/openstack/task_manager.py @@ -167,8 +167,14 @@ def submit_task(self, task): raise exceptions.TaskManagerStopped( "TaskManager {name} is no longer running".format( name=self.name)) + self.pre_run_task(task) + start = time.time() self.queue.put(task) - return task.wait() + ret = task.wait() + end = time.time() + dt = end - start + self.post_run_task(dt, task) + return ret def submit_function( self, method, name=None, run_async=False, *args, **kwargs): @@ -204,6 +210,13 @@ def submit_function_async(self, method, name=None, *args, **kwargs): method, name=name, run_async=True, *args, **kwargs) def pre_run_task(self, task): + '''Callback when task enters the task queue + + :param task: the task + + Intended to be overridden by child classes to track task + progress. + ''' self._log.debug( "Manager %s running task %s", self.name, task.name) @@ -213,14 +226,21 @@ def run_task(self, task): # code is designed so that caller of submit_task (which may be # in a different thread than this run_task) gets the # exception. - self.pre_run_task(task) - start = time.time() + # + # Note all threads go through the threadpool, so this is an + # async call. submit_task will wait() for the final result. task.run() - end = time.time() - dt = end - start - self.post_run_task(dt, task) def post_run_task(self, elapsed_time, task): + '''Callback at task completion + + :param float elapsed_time: time in seconds between task entering + queue and finishing + :param task: the task + + This function is intended to be overridden by child classes to + monitor task runtimes. + ''' self._log.debug( "Manager %s ran task %s in %ss", self.name, task.name, elapsed_time) diff --git a/openstack/tests/unit/cloud/test_task_manager.py b/openstack/tests/unit/cloud/test_task_manager.py index 6c4b19c0c..854b90a66 100644 --- a/openstack/tests/unit/cloud/test_task_manager.py +++ b/openstack/tests/unit/cloud/test_task_manager.py @@ -17,6 +17,7 @@ import fixtures import mock import threading +import time from six.moves import queue @@ -105,6 +106,28 @@ def test_async(self, mock_submit): self.manager.submit_function(set, run_async=True) self.assertTrue(mock_submit.called) + @mock.patch.object(task_manager.TaskManager, 'post_run_task') + @mock.patch.object(task_manager.TaskManager, 'pre_run_task') + def test_pre_post_calls(self, mock_pre, mock_post): + self.manager.submit_function(lambda: None) + mock_pre.assert_called_once() + mock_post.assert_called_once() + + @mock.patch.object(task_manager.TaskManager, 'post_run_task') + @mock.patch.object(task_manager.TaskManager, 'pre_run_task') + def test_validate_timing(self, mock_pre, mock_post): + # Note the unit test setup has mocked out time.sleep() and + # done a * 0.0001, and the test should be under the 5 + # second timeout. Thus with below, we should see at + # *least* a 1 second pause running the task. + self.manager.submit_function(lambda: time.sleep(10000)) + + mock_pre.assert_called_once() + mock_post.assert_called_once() + + args, kwargs = mock_post.call_args_list[0] + self.assertTrue(args[0] > 1.0) + class ThreadingTaskManager(task_manager.TaskManager): """A subclass of TaskManager which exercises the thread-shifting diff --git a/releasenotes/notes/fix-task-timing-048afea680adc62e.yaml b/releasenotes/notes/fix-task-timing-048afea680adc62e.yaml new file mode 100644 index 000000000..ef9e219e9 --- /dev/null +++ b/releasenotes/notes/fix-task-timing-048afea680adc62e.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fix a regression where the ``TaskManager.post_run_task`` ``elapsed_time`` + argument was not reflecting the time taken by the actual task. From 9804985fc7c7c5e95cdc0125ec079297b2f6e777 Mon Sep 17 00:00:00 2001 From: Ian Wienand Date: Fri, 26 Oct 2018 15:23:28 +1100 Subject: [PATCH 2255/3836] Add doc depends to tox releasenotes environment When I run "tox -e releasenotes" it fails because it doesn't install any of the sphinx or reno tools. Add doc dependencies. Change-Id: Ia8b864fbbe02f0963881d3a393b1a5935a3f16e9 --- tox.ini | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 676f6f34d..ff31ca10c 100644 --- a/tox.ini +++ b/tox.ini @@ -84,8 +84,10 @@ deps = commands = sphinx-build -W -d doc/build/doctrees -b html doc/source/ doc/build/html [testenv:releasenotes] -usedevelop = False -skip_install = True +deps = + -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} + -r{toxinidir}/requirements.txt + -r{toxinidir}/doc/requirements.txt commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [flake8] From 702b93d8dfe8870b0310126c51e1edf36bd4b2aa Mon Sep 17 00:00:00 2001 From: Sean McGinnis Date: Fri, 26 Oct 2018 13:53:25 -0500 Subject: [PATCH 2256/3836] Fix incorrect use of flake8:noqa Adding the comment flake8:noqa in a file will skip linting the entire file. Most of the time, the intent was just to skip individual lines to handle exception cases. This gets rid of the "noqa" usage where it was used incorrectly and fixes a few legitimate errors that were being hidden by the entire file being skipped. The behavior is change in flake8 to handle this better, which will result in pep8 job failures if these are not fixes first. See more information in the 3.6.0 release notes: http://flake8.pycqa.org/en/latest/release-notes/3.6.0.html#features Change-Id: I331b851310f62d6be925203e1f36cd67737996be Signed-off-by: Sean McGinnis --- openstack/tests/fakes.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/openstack/tests/fakes.py b/openstack/tests/fakes.py index d224811da..a8e8f7f1d 100644 --- a/openstack/tests/fakes.py +++ b/openstack/tests/fakes.py @@ -33,7 +33,13 @@ p=PROJECT_ID) NO_MD5 = '93b885adfe0da089cdf634904fd59f71' NO_SHA256 = '6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d' -FAKE_PUBLIC_KEY = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCkF3MX59OrlBs3dH5CU7lNmvpbrgZxSpyGjlnE8Flkirnc/Up22lpjznoxqeoTAwTW034k7Dz6aYIrZGmQwe2TkE084yqvlj45Dkyoj95fW/sZacm0cZNuL69EObEGHdprfGJQajrpz22NQoCD8TFB8Wv+8om9NH9Le6s+WPe98WC77KLw8qgfQsbIey+JawPWl4O67ZdL5xrypuRjfIPWjgy/VH85IXg/Z/GONZ2nxHgSShMkwqSFECAC5L3PHB+0+/12M/iikdatFSVGjpuHvkLOs3oe7m6HlOfluSJ85BzLWBbvva93qkGmLg4ZAc8rPh2O+YIsBUHNLLMM/oQp Generated-by-Nova\n" # flake8: noqa +FAKE_PUBLIC_KEY = ( + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCkF3MX59OrlBs3dH5CU7lNmvpbrgZxSpyGj" + "lnE8Flkirnc/Up22lpjznoxqeoTAwTW034k7Dz6aYIrZGmQwe2TkE084yqvlj45Dkyoj95fW/" + "sZacm0cZNuL69EObEGHdprfGJQajrpz22NQoCD8TFB8Wv+8om9NH9Le6s+WPe98WC77KLw8qg" + "fQsbIey+JawPWl4O67ZdL5xrypuRjfIPWjgy/VH85IXg/Z/GONZ2nxHgSShMkwqSFECAC5L3P" + "HB+0+/12M/iikdatFSVGjpuHvkLOs3oe7m6HlOfluSJ85BzLWBbvva93qkGmLg4ZAc8rPh2O+" + "YIsBUHNLLMM/oQp Generated-by-Nova\n") def make_fake_flavor(flavor_id, name, ram=100, disk=1600, vcpus=24): @@ -250,11 +256,12 @@ def make_fake_machine(machine_name, machine_id=None): id=machine_id, name=machine_name)) + def make_fake_port(address, node_id=None, port_id=None): if not node_id: - node_id = uuid.uuid4().hex + node_id = uuid.uuid4().hex if not port_id: - port_id = uuid.uuid4().hex + port_id = uuid.uuid4().hex return meta.obj_to_munch(FakeMachinePort( id=port_id, address=address, @@ -379,6 +386,7 @@ def __init__(self, id, address, node_id): self.address = address self.node_uuid = node_id + def make_fake_neutron_security_group( id, name, description, rules, project_id=None): if not rules: From f5570b737ab50289d49b0f1c3c995f19bbd4f321 Mon Sep 17 00:00:00 2001 From: Yuval Shalev Date: Sat, 20 Oct 2018 14:03:23 +0300 Subject: [PATCH 2257/3836] Added assign function to identity v3 proxy In order to fix the bug of not be able to use the assign role to user in project context, I've added this function to the identity v3 proxy. Story: 2002115 Change-Id: Ia0619ca2738b2adb3b7406bdb1891905a2179a62 --- openstack/identity/v3/_proxy.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index ae0e3012d..1d5e3c660 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -1114,3 +1114,18 @@ def delete_limit(self, limit, ignore_missing=True): """ self._delete(limit.Limit, limit, ignore_missing=ignore_missing) + + def assign_project_role_to_user(self, project, user, role): + """Assign role to user on a project + + :param project: Either the ID of a project or a + :class:`~openstack.identity.v3.project.Project` + instance. + :param user: Either the ID of a user or a + :class:`~openstack.identity.v3.user.User` instance. + :param role: Either the ID of a role or a + :class:`~openstack.identity.v3.role.Role` instance. + :return: ``None`` + """ + project = self._get_resource(_project.Project, project) + project.assign_role_to_user(self, user, role) From 7aaa683cd94b551d60f40b11a9b4ae7770b4f867 Mon Sep 17 00:00:00 2001 From: Maxim Babushkin Date: Sat, 27 Oct 2018 23:20:42 +0300 Subject: [PATCH 2258/3836] Add port_security_enabled to create_port valid kwargs Add the ability to specify port_security_enabled boolean for created and updated port. Change-Id: I9c2c8cbc4d4c5b25f458b2bcb263038d2a6de597 --- openstack/cloud/openstackcloud.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index aa4e55f3b..8d4dc7364 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -8379,7 +8379,8 @@ def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, @_utils.valid_kwargs('name', 'admin_state_up', 'mac_address', 'fixed_ips', 'subnet_id', 'ip_address', 'security_groups', 'allowed_address_pairs', 'extra_dhcp_opts', - 'device_owner', 'device_id', 'binding:vnic_type') + 'device_owner', 'device_id', 'binding:vnic_type', + 'port_security_enabled') def create_port(self, network_id, **kwargs): """Create a port @@ -8429,6 +8430,8 @@ def create_port(self, network_id, **kwargs): :param device_id: The ID of the device that uses this port. For example, a virtual server. (Optional) :param binding vnic_type: The type of the created port. (Optional) + :param port_security_enabled: The security port state created on + the network. (Optional) :returns: a ``munch.Munch`` describing the created port. @@ -8445,7 +8448,7 @@ def create_port(self, network_id, **kwargs): @_utils.valid_kwargs('name', 'admin_state_up', 'fixed_ips', 'security_groups', 'allowed_address_pairs', 'extra_dhcp_opts', 'device_owner', 'device_id', - 'binding:vnic_type') + 'binding:vnic_type', 'port_security_enabled') def update_port(self, name_or_id, **kwargs): """Update a port @@ -8492,6 +8495,8 @@ def update_port(self, name_or_id, **kwargs): For example, a DHCP agent. (Optional) :param device_id: The ID of the resource this port is attached to. :param binding vnic_type: The type of the created port. (Optional) + :param port_security_enabled: The security port state created on + the network. (Optional) :returns: a ``munch.Munch`` describing the updated port. From 5225369c5d65bf567e158e069dc3239977845146 Mon Sep 17 00:00:00 2001 From: Daniel Speichert Date: Wed, 10 Oct 2018 10:08:26 -0400 Subject: [PATCH 2259/3836] Added basic CRUD functionality around Host Aggregates "Actions" on Host Aggregates not implemented. Change-Id: I0f0de45989956c85659d53c585c4e3f33d42cd86 --- openstack/compute/v2/_proxy.py | 67 +++++++++++++++++++ openstack/compute/v2/aggregate.py | 38 +++++++++++ .../tests/unit/compute/v2/test_aggregate.py | 50 ++++++++++++++ .../add-aggregates-fc563e237755112e.yaml | 5 ++ 4 files changed, 160 insertions(+) create mode 100644 openstack/compute/v2/aggregate.py create mode 100644 openstack/tests/unit/compute/v2/test_aggregate.py create mode 100644 releasenotes/notes/add-aggregates-fc563e237755112e.yaml diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 20c7ebffb..c3efefcb3 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.compute.v2 import aggregate as _aggregate from openstack.compute.v2 import availability_zone from openstack.compute.v2 import extension from openstack.compute.v2 import flavor as _flavor @@ -120,6 +121,72 @@ def flavors(self, details=True, **query): flv = _flavor.FlavorDetail if details else _flavor.Flavor return self._list(flv, paginated=True, **query) + def aggregates(self): + """Return a generator of aggregate + + :returns: A generator of aggregate + :rtype: class: `~openstack.compute.v2.aggregate.Aggregate` + """ + aggregate = _aggregate.Aggregate + + return self._list(aggregate, paginated=False) + + def get_aggregate(self, aggregate): + """Get a single host aggregate + + :param image: The value can be the ID of an aggregate or a + :class:`~openstack.compute.v2.aggregate.Aggregate` + instance. + + :returns: One :class:`~openstack.compute.v2.aggregate.Aggregate` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_aggregate.Aggregate, aggregate) + + def create_aggregate(self, **attrs): + """Create a new host aggregate from attributes + + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.compute.v2.aggregate.Aggregate`, + comprised of the properties on the Aggregate class. + + :returns: The results of aggregate creation + :rtype: :class:`~openstack.compute.v2.aggregate.Aggregate` + """ + return self._create(_aggregate.Aggregate, **attrs) + + def update_aggregate(self, aggregate, **attrs): + """Update a host aggregate + + :param server: Either the ID of a host aggregate or a + :class:`~openstack.compute.v2.aggregate.Aggregate` + instance. + :attrs kwargs: The attributes to update on the aggregate represented + by ``aggregate``. + + :returns: The updated aggregate + :rtype: :class:`~openstack.compute.v2.aggregate.Aggregate` + """ + return self._update(_aggregate.Aggregate, aggregate, **attrs) + + def delete_aggregate(self, aggregate, ignore_missing=True): + """Delete a host aggregate + + :param keypair: The value can be either the ID of an aggregate or a + :class:`~openstack.compute.v2.aggregate.Aggregate` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the aggregate does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent aggregate. + + :returns: ``None`` + """ + self._delete(_aggregate.Aggregate, aggregate, + ignore_missing=ignore_missing) + def delete_image(self, image, ignore_missing=True): """Delete an image diff --git a/openstack/compute/v2/aggregate.py b/openstack/compute/v2/aggregate.py new file mode 100644 index 000000000..3a9b0c7d9 --- /dev/null +++ b/openstack/compute/v2/aggregate.py @@ -0,0 +1,38 @@ +# 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 resource + + +class Aggregate(resource.Resource): + resource_key = 'aggregate' + resources_key = 'aggregates' + base_path = '/os-aggregates' + + # capabilities + allow_create = True + allow_fetch = True + allow_delete = True + allow_list = True + + # Properties + #: Availability zone of aggregate + availability_zone = resource.Body('availability_zone') + #: Deleted? + deleted = resource.Body('deleted') + #: Name of aggregate + name = resource.Body('name') + #: Hosts + hosts = resource.Body('hosts') + #: Metadata + metadata = resource.Body('metadata') diff --git a/openstack/tests/unit/compute/v2/test_aggregate.py b/openstack/tests/unit/compute/v2/test_aggregate.py new file mode 100644 index 000000000..76ff19bd8 --- /dev/null +++ b/openstack/tests/unit/compute/v2/test_aggregate.py @@ -0,0 +1,50 @@ +# 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.tests.unit import base + +from openstack.compute.v2 import aggregate + +EXAMPLE = { + "name": "m-family", + "availability_zone": None, + "deleted": False, + "created_at": "2018-07-06T14:58:16.000000", + "updated_at": None, + "hosts": ["oscomp-m001", "oscomp-m002", "oscomp-m003"], + "deleted_at": None, + "id": 4, + "metadata": {"type": "public", "family": "m-family"} +} + + +class TestAggregate(base.TestCase): + + def test_basic(self): + sot = aggregate.Aggregate() + self.assertEqual('aggregate', sot.resource_key) + self.assertEqual('aggregates', sot.resources_key) + self.assertEqual('/os-aggregates', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = aggregate.Aggregate(**EXAMPLE) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['availability_zone'], sot.availability_zone) + self.assertEqual(EXAMPLE['deleted'], sot.deleted) + self.assertEqual(EXAMPLE['hosts'], sot.hosts) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertDictEqual(EXAMPLE['metadata'], sot.metadata) diff --git a/releasenotes/notes/add-aggregates-fc563e237755112e.yaml b/releasenotes/notes/add-aggregates-fc563e237755112e.yaml new file mode 100644 index 000000000..81733146b --- /dev/null +++ b/releasenotes/notes/add-aggregates-fc563e237755112e.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Basic CRUD functionality was added on Host Aggregates. Actions are not + implemented yet (adding/removing hosts from Host Aggregates). From ac1935b18217407efffd29d5b00c9b6f24321f3e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 29 Oct 2018 11:20:11 -0500 Subject: [PATCH 2260/3836] Add close method to shutdown threadpool There are contexts where people need to ensure a threadpool shuts down. Add a close method that can be used to ensure releasing of resources. Change-Id: I70d55a203c3104f70e3837db9601a1ebfe6fbd1f --- openstack/connection.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openstack/connection.py b/openstack/connection.py index ad094319e..d4968dc0f 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -367,3 +367,7 @@ def authorize(self): return self.session.get_token() except keystoneauth1.exceptions.ClientException as e: raise exceptions.raise_from_response(e.response) + + def close(self): + """Release any resources held open.""" + self.task_manager.stop() From fdcdaebfd3c7319adc56c7cb35d12b976967efdb Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 29 Oct 2018 11:31:01 -0500 Subject: [PATCH 2261/3836] Make Connection a context manager For people who are wanting to make sure Connection resources are cleaned up, add __enter__ and __exit__ methods so that one can do: with Connection() as conn: conn.be_awesome() and be sure that things are cleaned properly. Change-Id: I077dd09de7859c8e81c8090c847ecd5d72c4318e --- openstack/connection.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openstack/connection.py b/openstack/connection.py index d4968dc0f..73631d06e 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -371,3 +371,9 @@ def authorize(self): def close(self): """Release any resources held open.""" self.task_manager.stop() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() From 60d931f4c7fb4b0250aab71114b60f219fefa853 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 1 Jan 2018 11:50:54 -0600 Subject: [PATCH 2262/3836] Use sdk for list_servers As the next baby step in combining shade and sdk code, have list_servers use conn.compute.servers() on the backend. Change-Id: I4e87fcc69fcffeed3720b878518cf02ccd30a3dc --- openstack/cloud/_normalize.py | 3 ++- openstack/cloud/openstackcloud.py | 18 +++++------------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/openstack/cloud/_normalize.py b/openstack/cloud/_normalize.py index d8a1d59ce..740c52301 100644 --- a/openstack/cloud/_normalize.py +++ b/openstack/cloud/_normalize.py @@ -480,7 +480,8 @@ def _normalize_server(self, server): server, 'os-extended-volumes:volumes_attached', [], self.strict_mode) - config_drive = server.pop('config_drive', False) + config_drive = server.pop( + 'has_config_drive', server.pop('config_drive', False)) ret['has_config_drive'] = _to_bool(config_drive) host_id = server.pop('hostId', None) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 518f3788c..7c47e61b8 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -2124,19 +2124,11 @@ def list_servers(self, detailed=False, all_projects=False, bare=False, def _list_servers(self, detailed=False, all_projects=False, bare=False, filters=None): - error_msg = "Error fetching server list on {cloud}:{region}:".format( - cloud=self.name, - region=self.config.region_name) - - params = filters or {} - if all_projects: - params['all_tenants'] = True - data = _adapter._json_response( - self.compute.get( - '/servers/detail', params=params), - error_message=error_msg) - servers = self._normalize_servers( - self._get_and_munchify('servers', data)) + filters = filters or {} + servers = [ + self._normalize_server(server.to_dict()) + for server in self.compute.servers( + all_projects=all_projects, **filters)] return [ self._expand_server(server, detailed, bare) for server in servers From d42811c405dd22823ba01c35bfc3bdcc227df231 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 29 Oct 2018 14:21:42 -0500 Subject: [PATCH 2263/3836] Fix latest flake8 issues The latest flake8 checks strings for escape chars, and also has a new and better rule about line breaks before binary operators - which is what knuth says should happen. The escape char check was catching some asterisks in docstrings that weren't actually providing much, since we already list type as "kwargs". Just remove them. Install latest flake8 so we can gate on this. Change-Id: I89088adcc6f4ff5a894df5e677ae9b28a58edd9a --- examples/connect.py | 1 + openstack/_adapter.py | 1 - openstack/baremetal/v1/node.py | 22 ++++++------ openstack/block_storage/v2/_proxy.py | 4 +-- openstack/cloud/_heat/template_format.py | 2 ++ openstack/cloud/_heat/template_utils.py | 4 +-- openstack/cloud/_utils.py | 18 +++++----- openstack/cloud/meta.py | 5 +-- openstack/cloud/openstackcloud.py | 44 ++++++++++++------------ openstack/clustering/v1/_proxy.py | 24 ++++++------- openstack/compute/v2/_proxy.py | 8 ++--- openstack/config/loader.py | 12 +++---- openstack/database/v1/_proxy.py | 8 ++--- openstack/exceptions.py | 2 ++ openstack/identity/v2/_proxy.py | 6 ++-- openstack/identity/v3/_proxy.py | 30 ++++++++-------- openstack/image/v1/_proxy.py | 4 +-- openstack/image/v2/_proxy.py | 2 +- openstack/instance_ha/v1/_proxy.py | 6 ++-- openstack/key_manager/v1/_proxy.py | 6 ++-- openstack/message/v2/_proxy.py | 6 ++-- openstack/network/v2/_proxy.py | 26 +++++++------- openstack/object_store/v1/_proxy.py | 2 +- openstack/orchestration/v1/_proxy.py | 6 ++-- openstack/task_manager.py | 2 +- openstack/tests/fakes.py | 2 ++ openstack/tests/functional/base.py | 11 +++--- openstack/tests/unit/cloud/test_meta.py | 1 + openstack/tests/unit/test_exceptions.py | 4 +-- openstack/workflow/v2/_proxy.py | 4 +-- tox.ini | 1 + 31 files changed, 143 insertions(+), 131 deletions(-) diff --git a/examples/connect.py b/examples/connect.py index d5cce1c09..fe6ebd026 100644 --- a/examples/connect.py +++ b/examples/connect.py @@ -45,6 +45,7 @@ def __init__(self, cloud_name='devstack-admin', debug=False): def _get_resource_value(resource_key, default): return config.get_extra_config('example').get(resource_key, default) + SERVER_NAME = 'openstacksdk-example' IMAGE_NAME = _get_resource_value('image_name', 'cirros-0.3.5-x86_64-disk') FLAVOR_NAME = _get_resource_value('flavor_name', 'm1.small') diff --git a/openstack/_adapter.py b/openstack/_adapter.py index 7f5da392b..18b4e7dd9 100644 --- a/openstack/_adapter.py +++ b/openstack/_adapter.py @@ -22,7 +22,6 @@ JSONDecodeError = ValueError from six.moves import urllib - from keystoneauth1 import adapter from openstack import exceptions diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index d8a5bc18a..4b6a655ae 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -239,16 +239,16 @@ def create(self, session, *args, **kwargs): self._body.clean(only={'provision_state'}) super(Node, self).create(session, *args, **kwargs) - if (self.provision_state == 'enroll' and - expected_provision_state != 'enroll'): + if (self.provision_state == 'enroll' + and expected_provision_state != 'enroll'): self.set_provision_state(session, 'manage', wait=True) - if (self.provision_state == 'manageable' and - expected_provision_state == 'available'): + if (self.provision_state == 'manageable' + and expected_provision_state == 'available'): self.set_provision_state(session, 'provide', wait=True) - if (self.provision_state == 'available' and - expected_provision_state == 'manageable'): + if (self.provision_state == 'available' + and expected_provision_state == 'manageable'): self.set_provision_state(session, 'manage', wait=True) return self @@ -422,9 +422,9 @@ def _check_state_reached(self, session, expected_state, a failure state is reached. """ # NOTE(dtantsur): microversion 1.2 changed None to available - if (self.provision_state == expected_state or - (expected_state == 'available' and - self.provision_state is None)): + if (self.provision_state == expected_state + or (expected_state == 'available' + and self.provision_state is None)): return True elif not abort_on_failed_state: return False @@ -437,8 +437,8 @@ def _check_state_reached(self, session, expected_state, 'error': self.last_error}) # Special case: a failure state for "manage" transition can be # "enroll" - elif (expected_state == 'manageable' and - self.provision_state == 'enroll' and self.last_error): + elif (expected_state == 'manageable' + and self.provision_state == 'enroll' and self.last_error): raise exceptions.SDKException( "Node %(node)s could not reach state manageable: " "failed to verify management credentials; " diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index ece8b2609..3a5dc461f 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -41,7 +41,7 @@ def snapshots(self, details=True, **query): objects will be returned. The default, ``True``, will cause :class:`~openstack.block_storage.v2.snapshot.SnapshotDetail` objects to be returned. - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the snapshots being returned. Available parameters include: * name: Name of the snapshot as a string. @@ -150,7 +150,7 @@ def volumes(self, details=True, **query): will be returned. The default, ``True``, will cause :class:`~openstack.block_storage.v2.volume.VolumeDetail` objects to be returned. - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the volumes being returned. Available parameters include: * name: Name of the volume as a string. diff --git a/openstack/cloud/_heat/template_format.py b/openstack/cloud/_heat/template_format.py index cf37ee528..9b95ef24e 100644 --- a/openstack/cloud/_heat/template_format.py +++ b/openstack/cloud/_heat/template_format.py @@ -27,6 +27,8 @@ def _construct_yaml_str(self, node): # Override the default string handling function # to always return unicode objects return self.construct_scalar(node) + + HeatYamlLoader.add_constructor(u'tag:yaml.org,2002:str', _construct_yaml_str) # Unquoted dates like 2013-05-23 in yaml files get loaded as objects of type # datetime.data which causes problems in API layer when being processed by diff --git a/openstack/cloud/_heat/template_utils.py b/openstack/cloud/_heat/template_utils.py index c56b76ea5..1e1d3fa18 100644 --- a/openstack/cloud/_heat/template_utils.py +++ b/openstack/cloud/_heat/template_utils.py @@ -79,8 +79,8 @@ def ignore_if(key, value): return True if not isinstance(value, six.string_types): return True - if (key == 'type' and - not value.endswith(('.yaml', '.template'))): + if (key == 'type' + and not value.endswith(('.yaml', '.template'))): return True return False diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index 1f139f48a..10a8f1ac7 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -113,8 +113,8 @@ def _filter_list(data, name_or_id, filters): e_id = _make_unicode(e.get('id', None)) e_name = _make_unicode(e.get('name', None)) - if ((e_id and e_id == name_or_id) or - (e_name and e_name == name_or_id)): + if ((e_id and e_id == name_or_id) + or (e_name and e_name == name_or_id)): identifier_matches.append(e) else: # Only try fnmatch if we don't match exactly @@ -123,8 +123,8 @@ def _filter_list(data, name_or_id, filters): # so that we log the bad pattern bad_pattern = True continue - if ((e_id and fn_reg.match(e_id)) or - (e_name and fn_reg.match(e_name))): + if ((e_id and fn_reg.match(e_id)) + or (e_name and fn_reg.match(e_name))): identifier_matches.append(e) if not identifier_matches and bad_pattern: log.debug("Bad pattern passed to fnmatch", exc_info=True) @@ -187,8 +187,8 @@ def _get_entity(cloud, resource, name_or_id, filters, **kwargs): # an additional call, it's simple enough to test to see if we got an # object and just short-circuit return it. - if (hasattr(name_or_id, 'id') or - (isinstance(name_or_id, dict) and 'id' in name_or_id)): + if (hasattr(name_or_id, 'id') + or (isinstance(name_or_id, dict) and 'id' in name_or_id)): return name_or_id # If a uuid is passed short-circuit it calling the @@ -528,8 +528,8 @@ def _call_client_and_retry(client, url, retry_on=None, try: ret_val = client(url, **kwargs) except exc.OpenStackCloudHTTPError as e: - if (retry_on is not None and - e.response.status_code in retry_on): + if (retry_on is not None + and e.response.status_code in retry_on): log.debug('Received retryable error {err}, waiting ' '{wait} seconds to retry', { 'err': e.response.status_code, @@ -570,7 +570,7 @@ def parse_range(value): if value is None: return None - range_exp = re.match('(<|>|<=|>=){0,1}(\d+)$', value) + range_exp = re.match(r'(<|>|<=|>=){0,1}(\d+)$', value) if range_exp is None: return None diff --git a/openstack/cloud/meta.py b/openstack/cloud/meta.py index 8c13d0ec7..1312d00d9 100644 --- a/openstack/cloud/meta.py +++ b/openstack/cloud/meta.py @@ -404,8 +404,9 @@ def _get_supplemental_addresses(cloud, server): try: # Don't bother doing this before the server is active, it's a waste # of an API call while polling for a server to come up - if (cloud.has_service('network') and cloud._has_floating_ips() and - server['status'] == 'ACTIVE'): + if (cloud.has_service('network') + and cloud._has_floating_ips() + and server['status'] == 'ACTIVE'): for port in cloud.search_ports( filters=dict(device_id=server['id'])): # This SHOULD return one and only one FIP - but doing it as a diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index f267b99ab..12bf74f55 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -1377,8 +1377,8 @@ def get_flavor_by_ram(self, ram, include=None, get_extra=True): """ flavors = self.list_flavors(get_extra=get_extra) for flavor in sorted(flavors, key=operator.itemgetter('ram')): - if (flavor['ram'] >= ram and - (not include or include in flavor['name'])): + if (flavor['ram'] >= ram + and (not include or include in flavor['name'])): return flavor raise exc.OpenStackCloudException( "Could not find a flavor with {ram} and '{include}'".format( @@ -2402,38 +2402,38 @@ def _set_interesting_networks(self): or network['id'] in self._external_ipv4_names): external_ipv4_networks.append(network) elif ((('router:external' in network - and network['router:external']) or - network.get('provider:physical_network')) and - network['name'] not in self._internal_ipv4_names and - network['id'] not in self._internal_ipv4_names): + and network['router:external']) + or network.get('provider:physical_network')) + and network['name'] not in self._internal_ipv4_names + and network['id'] not in self._internal_ipv4_names): external_ipv4_networks.append(network) # Internal networks if (network['name'] in self._internal_ipv4_names or network['id'] in self._internal_ipv4_names): internal_ipv4_networks.append(network) - elif (not network.get('router:external', False) and - not network.get('provider:physical_network') and - network['name'] not in self._external_ipv4_names and - network['id'] not in self._external_ipv4_names): + elif (not network.get('router:external', False) + and not network.get('provider:physical_network') + and network['name'] not in self._external_ipv4_names + and network['id'] not in self._external_ipv4_names): internal_ipv4_networks.append(network) # External networks if (network['name'] in self._external_ipv6_names or network['id'] in self._external_ipv6_names): external_ipv6_networks.append(network) - elif (network.get('router:external') and - network['name'] not in self._internal_ipv6_names and - network['id'] not in self._internal_ipv6_names): + elif (network.get('router:external') + and network['name'] not in self._internal_ipv6_names + and network['id'] not in self._internal_ipv6_names): external_ipv6_networks.append(network) # Internal networks if (network['name'] in self._internal_ipv6_names or network['id'] in self._internal_ipv6_names): internal_ipv6_networks.append(network) - elif (not network.get('router:external', False) and - network['name'] not in self._external_ipv6_names and - network['id'] not in self._external_ipv6_names): + elif (not network.get('router:external', False) + and network['name'] not in self._external_ipv6_names + and network['id'] not in self._external_ipv6_names): internal_ipv6_networks.append(network) # External Floating IPv4 networks @@ -2612,8 +2612,8 @@ def get_external_networks(self): """ self._find_interesting_networks() return list( - set(self._external_ipv4_networks) | - set(self._external_ipv6_networks)) + set(self._external_ipv4_networks) + | set(self._external_ipv6_networks)) def get_internal_networks(self): """Return the networks that are configured to not route northbound. @@ -2625,8 +2625,8 @@ def get_internal_networks(self): """ self._find_interesting_networks() return list( - set(self._internal_ipv4_networks) | - set(self._internal_ipv6_networks)) + set(self._internal_ipv4_networks) + | set(self._internal_ipv6_networks)) def get_external_ipv4_networks(self): """Return the networks that are configured to route northbound. @@ -9744,8 +9744,8 @@ def register_machine(self, nics, wait=False, timeout=3600, "Timeout waiting for reservation to clear " "before setting provide state"): machine = self.get_machine(machine['uuid']) - if (machine['reservation'] is None and - machine['provision_state'] is not 'enroll'): + if (machine['reservation'] is None + and machine['provision_state'] != 'enroll'): # NOTE(TheJulia): In this case, the node has # has moved on from the previous state and is # likely not being verified, as no lock is diff --git a/openstack/clustering/v1/_proxy.py b/openstack/clustering/v1/_proxy.py index e78d68bdf..c6cc8db5f 100644 --- a/openstack/clustering/v1/_proxy.py +++ b/openstack/clustering/v1/_proxy.py @@ -134,7 +134,7 @@ def get_profile(self, profile): def profiles(self, **query): """Retrieve a generator of profiles. - :param kwargs \*\*query: Optional query parameters to be sent to + :param kwargs query: Optional query parameters to be sent to restrict the profiles to be returned. Available parameters include: * name: The name of a profile. @@ -247,7 +247,7 @@ def get_cluster(self, cluster): def clusters(self, **query): """Retrieve a generator of clusters. - :param kwargs \*\*query: Optional query parameters to be sent to + :param kwargs query: Optional query parameters to be sent to restrict the clusters to be returned. Available parameters include: * name: The name of a cluster. @@ -302,7 +302,7 @@ def remove_nodes_from_cluster(self, cluster, nodes, **params): :param cluster: Either the name or the ID of the cluster, or an instance of :class:`~openstack.clustering.v1.cluster.Cluster`. :param nodes: List of nodes to be removed from the cluster. - :param kwargs \*\*params: Optional query parameters to be sent to + :param kwargs params: Optional query parameters to be sent to restrict the nodes to be returned. Available parameters include: * destroy_after_deletion: A boolean value indicating whether the @@ -364,7 +364,7 @@ def resize_cluster(self, cluster, **params): :param cluster: Either the name or the ID of the cluster, or an instance of :class:`~openstack.clustering.v1.cluster.Cluster`. - :param dict \*\*params: A dictionary providing the parameters for the + :param dict params: A dictionary providing the parameters for the resize action. :returns: A dict containing the action initiated by this operation. """ @@ -380,7 +380,7 @@ def attach_policy_to_cluster(self, cluster, policy, **params): :param cluster: Either the name or the ID of the cluster, or an instance of :class:`~openstack.clustering.v1.cluster.Cluster`. :param policy: Either the name or the ID of a policy. - :param dict \*\*params: A dictionary containing the properties for the + :param dict params: A dictionary containing the properties for the policy to be attached. :returns: A dict containing the action initiated by this operation. """ @@ -410,7 +410,7 @@ def update_cluster_policy(self, cluster, policy, **params): :param cluster: Either the name or the ID of the cluster, or an instance of :class:`~openstack.clustering.v1.cluster.Cluster`. :param policy: Either the name or the ID of a policy. - :param dict \*\*params: A dictionary containing the new properties for + :param dict params: A dictionary containing the new properties for the policy. :returns: A dict containing the action initiated by this operation. """ @@ -544,7 +544,7 @@ def get_node(self, node, details=False): def nodes(self, **query): """Retrieve a generator of nodes. - :param kwargs \*\*query: Optional query parameters to be sent to + :param kwargs query: Optional query parameters to be sent to restrict the nodes to be returned. Available parameters include: * cluster_id: A string including the name or ID of a cluster to @@ -708,7 +708,7 @@ def get_policy(self, policy): def policies(self, **query): """Retrieve a generator of policies. - :param kwargs \*\*query: Optional query parameters to be sent to + :param kwargs query: Optional query parameters to be sent to restrict the policies to be returned. Available parameters include: * name: The name of a policy. @@ -760,7 +760,7 @@ def cluster_policies(self, cluster, **query): :param cluster: The value can be the name or ID of a cluster or a :class:`~openstack.clustering.v1.cluster.Cluster` instance. - :param kwargs \*\*query: Optional query parameters to be sent to + :param kwargs query: Optional query parameters to be sent to restrict the policies to be returned. Available parameters include: * enabled: A boolean value indicating whether the policy is @@ -858,7 +858,7 @@ def get_receiver(self, receiver): def receivers(self, **query): """Retrieve a generator of receivers. - :param kwargs \*\*query: Optional query parameters for restricting the + :param kwargs query: Optional query parameters for restricting the receivers to be returned. Available parameters include: * name: The name of a receiver object. @@ -891,7 +891,7 @@ def get_action(self, action): def actions(self, **query): """Retrieve a generator of actions. - :param kwargs \*\*query: Optional query parameters to be sent to + :param kwargs query: Optional query parameters to be sent to restrict the actions to be returned. Available parameters include: * name: name of action for query. @@ -929,7 +929,7 @@ def get_event(self, event): def events(self, **query): """Retrieve a generator of events. - :param kwargs \*\*query: Optional query parameters to be sent to + :param kwargs query: Optional query parameters to be sent to restrict the events to be returned. Available parameters include: * obj_name: name string of the object associated with an event. diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index b0a14ede8..ce57489fa 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -112,7 +112,7 @@ def flavors(self, details=True, **query): :class:`~openstack.compute.v2.flavor.FlavorDetail` objects, otherwise :class:`~openstack.compute.v2.flavor.Flavor`. *Default: ``True``* - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the flavors being returned. :returns: A generator of flavor objects @@ -168,7 +168,7 @@ def images(self, details=True, **query): :class:`~openstack.compute.v2.image.ImageDetail` objects, otherwise :class:`~openstack.compute.v2.image.Image`. *Default: ``True``* - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of image objects @@ -379,7 +379,7 @@ def servers(self, details=True, all_projects=False, **query): will be returned. The default, ``True``, will cause :class:`~openstack.compute.v2.server.ServerDetail` instances to be returned. - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the servers being returned. Available parameters include: * changes_since: A time/date stamp for when the server last changed @@ -1077,7 +1077,7 @@ def get_server_group(self, server_group): def server_groups(self, **query): """Return a generator of server groups - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of ServerGroup objects diff --git a/openstack/config/loader.py b/openstack/config/loader.py index d38cc4bf6..030fc029b 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -919,9 +919,9 @@ def _validate_auth_correctly(self, config, loader): def option_prompt(self, config, p_opt): """Prompt user for option that requires a value""" if ( - getattr(p_opt, 'prompt', None) is not None and - p_opt.dest not in config['auth'] and - self._pw_callback is not None + getattr(p_opt, 'prompt', None) is not None + and p_opt.dest not in config['auth'] + and self._pw_callback is not None ): config['auth'][p_opt.dest] = self._pw_callback(p_opt.prompt) return config @@ -948,9 +948,9 @@ def magic_fixes(self, config): """Perform the set of magic argument fixups""" # Infer token plugin if a token was given - if (('auth' in config and 'token' in config['auth']) or - ('auth_token' in config and config['auth_token']) or - ('token' in config and config['token'])): + if (('auth' in config and 'token' in config['auth']) + or ('auth_token' in config and config['auth_token']) + or ('token' in config and config['token'])): config.setdefault('token', config.pop('auth_token', None)) # These backwards compat values are only set via argparse. If it's diff --git a/openstack/database/v1/_proxy.py b/openstack/database/v1/_proxy.py index 826e19297..a21804da6 100644 --- a/openstack/database/v1/_proxy.py +++ b/openstack/database/v1/_proxy.py @@ -81,7 +81,7 @@ def databases(self, instance, **query): :param instance: This can be either the ID of an instance or a :class:`~openstack.database.v1.instance.Instance` instance that the interface belongs to. - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of database objects @@ -137,7 +137,7 @@ def get_flavor(self, flavor): def flavors(self, **query): """Return a generator of flavors - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of flavor objects @@ -203,7 +203,7 @@ def get_instance(self, instance): def instances(self, **query): """Return a generator of instances - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of instance objects @@ -283,7 +283,7 @@ def users(self, instance, **query): :param instance: This can be either the ID of an instance or a :class:`~openstack.database.v1.instance.Instance` - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of user objects diff --git a/openstack/exceptions.py b/openstack/exceptions.py index 8dbe70e85..fdbe11d2d 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -29,6 +29,8 @@ def __init__(self, message=None, extra_data=None): self.message = self.__class__.__name__ if message is None else message self.extra_data = extra_data super(SDKException, self).__init__(self.message) + + OpenStackCloudException = SDKException diff --git a/openstack/identity/v2/_proxy.py b/openstack/identity/v2/_proxy.py index 5c2175355..97560c12e 100644 --- a/openstack/identity/v2/_proxy.py +++ b/openstack/identity/v2/_proxy.py @@ -96,7 +96,7 @@ def get_role(self, role): def roles(self, **query): """Retrieve a generator of roles - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of role instances. @@ -173,7 +173,7 @@ def get_tenant(self, tenant): def tenants(self, **query): """Retrieve a generator of tenants - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of tenant instances. @@ -250,7 +250,7 @@ def get_user(self, user): def users(self, **query): """Retrieve a generator of users - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of user instances. diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index ae0e3012d..50472fd8b 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -96,7 +96,7 @@ def get_credential(self, credential): def credentials(self, **query): """Retrieve a generator of credentials - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of credentials instances. @@ -174,7 +174,7 @@ def get_domain(self, domain): def domains(self, **query): """Retrieve a generator of domains - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of domain instances. @@ -254,7 +254,7 @@ def get_endpoint(self, endpoint): def endpoints(self, **query): """Retrieve a generator of endpoints - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of endpoint instances. @@ -334,7 +334,7 @@ def get_group(self, group): def groups(self, **query): """Retrieve a generator of groups - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of group instances. @@ -412,7 +412,7 @@ def get_policy(self, policy): def policies(self, **query): """Retrieve a generator of policies - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of policy instances. @@ -490,7 +490,7 @@ def get_project(self, project): def projects(self, **query): """Retrieve a generator of projects - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of project instances. @@ -505,7 +505,7 @@ def user_projects(self, user, **query): :param user: Either the user id or an instance of :class:`~openstack.identity.v3.user.User` - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of project instances. @@ -584,7 +584,7 @@ def get_service(self, service): def services(self, **query): """Retrieve a generator of services - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of service instances. @@ -662,7 +662,7 @@ def get_user(self, user): def users(self, **query): """Retrieve a generator of users - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of user instances. @@ -740,7 +740,7 @@ def get_trust(self, trust): def trusts(self, **query): """Retrieve a generator of trusts - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of trust instances. @@ -805,7 +805,7 @@ def get_region(self, region): def regions(self, **query): """Retrieve a generator of regions - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the regions being returned. :returns: A generator of region instances. @@ -883,7 +883,7 @@ def get_role(self, role): def roles(self, **query): """Retrieve a generator of roles - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. The options are: domain_id, name. :return: A generator of role instances. @@ -964,7 +964,7 @@ def role_assignments_filter(self, domain=None, project=None, group=None, def role_assignments(self, **query): """Retrieve a generator of role assignments - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. The options are: group_id, role_id, scope_domain_id, scope_project_id, user_id, include_names, @@ -978,7 +978,7 @@ def role_assignments(self, **query): def registered_limits(self, **query): """Retrieve a generator of registered_limits - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the registered_limits being returned. :returns: A generator of registered_limits instances. @@ -1052,7 +1052,7 @@ def delete_registered_limit(self, registered_limit, ignore_missing=True): def limits(self, **query): """Retrieve a generator of limits - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the limits being returned. :returns: A generator of limits instances. diff --git a/openstack/image/v1/_proxy.py b/openstack/image/v1/_proxy.py index 472107b16..8e00ea851 100644 --- a/openstack/image/v1/_proxy.py +++ b/openstack/image/v1/_proxy.py @@ -72,8 +72,8 @@ def get_image(self, image): def images(self, **query): """Return a generator of images - :param kwargs \*\*query: Optional query parameters to be sent to limit - the resources being returned. + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. :returns: A generator of image objects :rtype: :class:`~openstack.image.v1.image.Image` diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 3b2d65f3c..9968367c1 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -138,7 +138,7 @@ def get_image(self, image): def images(self, **query): """Return a generator of images - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of image objects diff --git a/openstack/instance_ha/v1/_proxy.py b/openstack/instance_ha/v1/_proxy.py index e283e33ec..2022ea980 100644 --- a/openstack/instance_ha/v1/_proxy.py +++ b/openstack/instance_ha/v1/_proxy.py @@ -29,7 +29,7 @@ class Proxy(proxy.Proxy): def notifications(self, **query): """Return a generator of notifications. - :param kwargs \*\*query: Optional query parameters to be sent to + :param kwargs query: Optional query parameters to be sent to limit the notifications being returned. :returns: A generator of notifications """ @@ -67,7 +67,7 @@ def create_notification(self, **attrs): def segments(self, **query): """Return a generator of segments. - :param kwargs \*\*query: Optional query parameters to be sent to + :param kwargs query: Optional query parameters to be sent to limit the segments being returned. :returns: A generator of segments """ @@ -132,7 +132,7 @@ def hosts(self, segment_id, **query): """Return a generator of hosts. :param segment_id: The ID of a failover segment. - :param kwargs \*\*query: Optional query parameters to be sent to + :param kwargs query: Optional query parameters to be sent to limit the hosts being returned. :returns: A generator of hosts diff --git a/openstack/key_manager/v1/_proxy.py b/openstack/key_manager/v1/_proxy.py index c54bafeae..3945f4caa 100644 --- a/openstack/key_manager/v1/_proxy.py +++ b/openstack/key_manager/v1/_proxy.py @@ -78,7 +78,7 @@ def get_container(self, container): def containers(self, **query): """Return a generator of containers - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of container objects @@ -158,7 +158,7 @@ def get_order(self, order): def orders(self, **query): """Return a generator of orders - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of order objects @@ -239,7 +239,7 @@ def get_secret(self, secret): def secrets(self, **query): """Return a generator of secrets - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of secret objects diff --git a/openstack/message/v2/_proxy.py b/openstack/message/v2/_proxy.py index 2d8dae961..1606235ce 100644 --- a/openstack/message/v2/_proxy.py +++ b/openstack/message/v2/_proxy.py @@ -47,7 +47,7 @@ def get_queue(self, queue): def queues(self, **query): """Retrieve a generator of queues - :param kwargs \*\*query: Optional query parameters to be sent to + :param kwargs query: Optional query parameters to be sent to restrict the queues to be returned. Available parameters include: * limit: Requests at most the specified number of items be @@ -93,7 +93,7 @@ def messages(self, queue_name, **query): """Retrieve a generator of messages :param queue_name: The name of target queue to query messages from. - :param kwargs \*\*query: Optional query parameters to be sent to + :param kwargs query: Optional query parameters to be sent to restrict the messages to be returned. Available parameters include: * limit: Requests at most the specified number of items be @@ -170,7 +170,7 @@ def subscriptions(self, queue_name, **query): """Retrieve a generator of subscriptions :param queue_name: The name of target queue to subscribe on. - :param kwargs \*\*query: Optional query parameters to be sent to + :param kwargs query: Optional query parameters to be sent to restrict the subscriptions to be returned. Available parameters include: diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 77cc26dde..d7d2af176 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -214,7 +214,7 @@ def dhcp_agent_hosting_networks(self, agent, **query): :param agent: Either the agent id of an instance of :class:`~openstack.network.v2.network_agent.Agent` - :param query: kwargs \*\*query: Optional query parameters to be sent + :param query: kwargs query: Optional query parameters to be sent to limit the resources being returned. :return: A generator of networks """ @@ -1136,7 +1136,7 @@ def get_network(self, network): def networks(self, **query): """Return a generator of networks - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. Available parameters include: * ``description``: The network description. @@ -1210,7 +1210,7 @@ def get_network_ip_availability(self, network): def network_ip_availabilities(self, **query): """Return a generator of network ip availabilities - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. Available parameters include: * ``ip_version``: IP version of the network @@ -1504,7 +1504,7 @@ def get_port(self, port): def ports(self, **query): """Return a generator of ports - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. Available parameters include: * ``description``: The port description. @@ -1647,7 +1647,7 @@ def qos_bandwidth_limit_rules(self, qos_policy, **query): :param qos_policy: The value can be the ID of the QoS policy that the rule belongs or a :class:`~openstack.network.v2. qos_policy.QoSPolicy` instance. - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of bandwidth limit rule objects :rtype: :class:`~openstack.network.v2.qos_bandwidth_limit_rule. @@ -1770,7 +1770,7 @@ def qos_dscp_marking_rules(self, qos_policy, **query): :param qos_policy: The value can be the ID of the QoS policy that the rule belongs or a :class:`~openstack.network.v2. qos_policy.QoSPolicy` instance. - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of QoS DSCP marking rule objects :rtype: :class:`~openstack.network.v2.qos_dscp_marking_rule. @@ -1893,7 +1893,7 @@ def qos_minimum_bandwidth_rules(self, qos_policy, **query): :param qos_policy: The value can be the ID of the QoS policy that the rule belongs or a :class:`~openstack.network.v2. qos_policy.QoSPolicy` instance. - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of minimum bandwidth rule objects :rtype: :class:`~openstack.network.v2.qos_minimum_bandwidth_rule. @@ -2385,7 +2385,7 @@ def routers_hosting_l3_agents(self, router, **query): :param router: Either the router id or an instance of :class:`~openstack.network.v2.router.Router` - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources returned :returns: A generator of Router L3 Agents @@ -2400,7 +2400,7 @@ def agent_hosted_routers(self, agent, **query): :param agent: Either the agent id of an instance of :class:`~openstack.network.v2.network_agent.Agent` - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources returned :returns: A generator of routers @@ -2939,7 +2939,7 @@ def get_security_group_rule(self, security_group_rule): def security_group_rules(self, **query): """Return a generator of security group rules - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. Available parameters include: * ``description``: The security group rule description @@ -3019,7 +3019,7 @@ def get_segment(self, segment): def segments(self, **query): """Return a generator of segments - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. Available parameters include: * ``description``: The segment description @@ -3051,7 +3051,7 @@ def update_segment(self, segment, **attrs): def service_providers(self, **query): """Return a generator of service providers - :param kwargs \*\* query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of service provider objects @@ -3308,7 +3308,7 @@ def get_subnet_pool(self, subnet_pool): def subnet_pools(self, **query): """Return a generator of subnet pools - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. Available parameters include: * ``address_scope_id``: Subnet pool address scope ID diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index fef003eb5..629a5f733 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -150,7 +150,7 @@ def objects(self, container, **query): that you want to retrieve objects from. :type container: :class:`~openstack.object_store.v1.container.Container` - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :rtype: A generator of diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index ccf9790fc..4378353e1 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -59,7 +59,7 @@ def find_stack(self, name_or_id, ignore_missing=True): def stacks(self, **query): """Return a generator of stacks - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of stack objects @@ -84,7 +84,7 @@ def update_stack(self, stack, **attrs): :param stack: The value can be the ID of a stack or a :class:`~openstack.orchestration.v1.stack.Stack` instance. - :param kwargs \*\*attrs: The attributes to update on the stack + :param kwargs attrs: The attributes to update on the stack represented by ``value``. :returns: The updated stack @@ -191,7 +191,7 @@ def resources(self, stack, **query): :param stack: This can be a stack object, or the name of a stack for which the resources are to be listed. - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of resource objects if the stack exists and diff --git a/openstack/task_manager.py b/openstack/task_manager.py index 37c5ed1ee..8d2bec8a5 100644 --- a/openstack/task_manager.py +++ b/openstack/task_manager.py @@ -90,7 +90,7 @@ def run(self): # Retry one time if we get a retriable connection failure try: self.done(self.main()) - except keystoneauth1.exceptions.RetriableConnectionFailure as e: + except keystoneauth1.exceptions.RetriableConnectionFailure: self.done(self.main()) except Exception as e: self.exception(e, sys.exc_info()[2]) diff --git a/openstack/tests/fakes.py b/openstack/tests/fakes.py index a8e8f7f1d..08d07568a 100644 --- a/openstack/tests/fakes.py +++ b/openstack/tests/fakes.py @@ -64,6 +64,8 @@ def make_fake_flavor(flavor_id, name, ram=100, disk=1600, vcpus=24): u'swap': u'', u'vcpus': vcpus } + + FAKE_FLAVOR = make_fake_flavor(FLAVOR_ID, 'vanilla') FAKE_CHOCOLATE_FLAVOR = make_fake_flavor( CHOCOLATE_FLAVOR_ID, 'chocolate', ram=200) diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index b6f38c3f5..215673499 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -38,6 +38,7 @@ def _disable_keep_alive(conn): sess = conn.config.get_session() sess.keep_alive = False + IMAGE_NAME = _get_resource_value('image_name', 'cirros-0.3.5-x86_64-disk') FLAVOR_NAME = _get_resource_value('flavor_name', 'm1.small') @@ -129,10 +130,12 @@ def setUp(self): if not min_microversion: return - if not (data.min_microversion and data.max_microversion and - discover.version_between(data.min_microversion, - data.max_microversion, - min_microversion)): + if not (data.min_microversion + and data.max_microversion + and discover.version_between( + data.min_microversion, + data.max_microversion, + min_microversion)): self.skipTest('Service {service_type} does not provide ' 'microversion {ver}'.format( service_type=service_type, diff --git a/openstack/tests/unit/cloud/test_meta.py b/openstack/tests/unit/cloud/test_meta.py index f7e82894f..f6100d939 100644 --- a/openstack/tests/unit/cloud/test_meta.py +++ b/openstack/tests/unit/cloud/test_meta.py @@ -79,6 +79,7 @@ def list_server_security_groups(self, server): def get_default_network(self): return None + standard_fake_server = fakes.make_fake_server( server_id='test-id-0', name='test-id-0', diff --git a/openstack/tests/unit/test_exceptions.py b/openstack/tests/unit/test_exceptions.py index 63b15b91e..fad45d145 100644 --- a/openstack/tests/unit/test_exceptions.py +++ b/openstack/tests/unit/test_exceptions.py @@ -22,8 +22,8 @@ class Test_Exception(base.TestCase): def test_method_not_supported(self): exc = exceptions.MethodNotSupported(self.__class__, 'list') - expected = ('The list method is not supported for ' + - 'openstack.tests.unit.test_exceptions.Test_Exception') + expected = ('The list method is not supported for ' + + 'openstack.tests.unit.test_exceptions.Test_Exception') self.assertEqual(expected, str(exc)) diff --git a/openstack/workflow/v2/_proxy.py b/openstack/workflow/v2/_proxy.py index 0e4a51333..f2b9f0c53 100644 --- a/openstack/workflow/v2/_proxy.py +++ b/openstack/workflow/v2/_proxy.py @@ -44,7 +44,7 @@ def get_workflow(self, *attrs): def workflows(self, **query): """Retrieve a generator of workflows - :param kwargs \*\*query: Optional query parameters to be sent to + :param kwargs query: Optional query parameters to be sent to restrict the workflows to be returned. Available parameters include: @@ -120,7 +120,7 @@ def get_execution(self, *attrs): def executions(self, **query): """Retrieve a generator of executions - :param kwargs \*\*query: Optional query parameters to be sent to + :param kwargs query: Optional query parameters to be sent to restrict the executions to be returned. Available parameters include: diff --git a/tox.ini b/tox.ini index 676f6f34d..6670d2938 100644 --- a/tox.ini +++ b/tox.ini @@ -40,6 +40,7 @@ skip_install = True deps = -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} doc8 + flake8 hacking pygments readme From 61468af1241a838bfa4b3436c6cbf59411a598a6 Mon Sep 17 00:00:00 2001 From: Ian Wienand Date: Tue, 30 Oct 2018 15:33:05 +1100 Subject: [PATCH 2264/3836] Make delete_unattached_floating_ips return a count This modifies delete_unattached_floating_ips to return either a count of how many floating IPs were cleaned, or False if none were cleaned. Although this is an API change, from searching both codesearch and google it seems very likely that nodepool only user of this. However, since a positive integer value will evaluate True anyway extant code should continue to work. The neturon test-case is updated with an extra floating-ip to make sure the count works. The nova test-case is updated to ensure it returns False to indicate nothing was done. Sort-of-Needed-By: https://review.openstack.org/614074 Change-Id: I7bd709bd83f352c58203c767779acbe66ecfc10e --- openstack/cloud/openstackcloud.py | 4 ++-- .../unit/cloud/test_floating_ip_neutron.py | 24 +++++++++++++++++-- .../tests/unit/cloud/test_floating_ip_nova.py | 2 +- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index f267b99ab..d912ce8a0 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -6163,7 +6163,7 @@ def delete_unattached_floating_ips(self, retry=1): A value of 0 will also cause no checking of results to occur. - :returns: True if Floating IPs have been deleted, False if not + :returns: Number of Floating IPs deleted, False if none :raises: ``OpenStackCloudException``, on operation error. """ @@ -6173,7 +6173,7 @@ def delete_unattached_floating_ips(self, retry=1): if not ip['attached']: processed.append(self.delete_floating_ip( floating_ip_id=ip['id'], retry=retry)) - return all(processed) if processed else False + return len(processed) if all(processed) else False def _attach_ip_to_server( self, server, floating_ip, diff --git a/openstack/tests/unit/cloud/test_floating_ip_neutron.py b/openstack/tests/unit/cloud/test_floating_ip_neutron.py index bf4871f2c..94c06bdc0 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_neutron.py +++ b/openstack/tests/unit/cloud/test_floating_ip_neutron.py @@ -928,6 +928,14 @@ def test_cleanup_floating_ips(self): "network": "this-is-a-net-or-pool-id", "port_id": None, "status": "ACTIVE" + }, { + "id": "this-is-a-second-floating-ip-id", + "fixed_ip_address": None, + "internal_network": None, + "floating_ip_address": "203.0.113.30", + "network": "this-is-a-net-or-pool-id", + "port_id": None, + "status": "ACTIVE" }, { "id": "this-is-an-attached-floating-ip-id", "fixed_ip_address": None, @@ -949,12 +957,24 @@ def test_cleanup_floating_ips(self): append=['v2.0', 'floatingips/{0}.json'.format( floating_ips[0]['id'])]), json={}), + # First IP has been deleted now, return just the second + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips.json']), + json={'floatingips': floating_ips[1:]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'floatingips/{0}.json'.format( + floating_ips[1]['id'])]), + json={}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'floatingips.json']), - json={'floatingips': [floating_ips[1]]}), + json={'floatingips': [floating_ips[2]]}), ]) - self.cloud.delete_unattached_floating_ips() + cleaned_up = self.cloud.delete_unattached_floating_ips() + self.assertEqual(cleaned_up, 2) self.assert_calls() def test_create_floating_ip_no_port(self): diff --git a/openstack/tests/unit/cloud/test_floating_ip_nova.py b/openstack/tests/unit/cloud/test_floating_ip_nova.py index 1ca9a68a3..7d49e19d8 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_nova.py +++ b/openstack/tests/unit/cloud/test_floating_ip_nova.py @@ -318,4 +318,4 @@ def test_add_ip_from_pool(self): def test_cleanup_floating_ips(self): # This should not call anything because it's unsafe on nova. - self.cloud.delete_unattached_floating_ips() + self.assertFalse(self.cloud.delete_unattached_floating_ips()) From 965775622d5d38958b90cc6e4a0898c4f06bed6d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 30 Oct 2018 08:39:47 -0500 Subject: [PATCH 2265/3836] Filter ports in list_ports when batching is in effect When caching is in effect, we always fetch ports in a raw list with no push-down filters. However, people calling list_ports with the filters argument should expect it to work whether push-down conditions are used or not. Wrap the return in a filter list so that we apply the filters client-side in that case. Story: 2004207 Task: 27716 Change-Id: Ieabb3193c320bd9c0e569e2e7cef983529a6fbdb --- openstack/cloud/openstackcloud.py | 9 +++++---- openstack/tests/unit/cloud/test_caching.py | 20 +++++++++++++++++++ openstack/tests/unit/cloud/test_port.py | 12 +++++++++++ .../unit/fixtures/clouds/clouds_cache.yaml | 1 + 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index f267b99ab..34954297b 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -1702,8 +1702,6 @@ def list_ports(self, filters=None): if filters and self._PORT_AGE == 0: return self._list_ports(filters) - # Translate None from search interface to empty {} for kwargs below - filters = {} if (time.time() - self._ports_time) >= self._PORT_AGE: # Since we're using cached data anyway, we don't need to # have more than one thread actually submit the list @@ -1716,11 +1714,14 @@ def list_ports(self, filters=None): if self._ports_lock.acquire(first_run): try: if not (first_run and self._ports is not None): - self._ports = self._list_ports(filters) + self._ports = self._list_ports({}) self._ports_time = time.time() finally: self._ports_lock.release() - return self._ports + # Wrap the return with filter_list so that if filters were passed + # but we were batching/caching and thus always fetching the whole + # list from the cloud, we still return a filtered list. + return _utils._filter_list(self._ports, None, filters or {}) def _list_ports(self, filters): resp = self.network.get("/ports.json", params=filters) diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py index 54ed32542..70172c242 100644 --- a/openstack/tests/unit/cloud/test_caching.py +++ b/openstack/tests/unit/cloud/test_caching.py @@ -21,6 +21,7 @@ from openstack import exceptions from openstack.tests import fakes from openstack.tests.unit import base +from openstack.tests.unit.cloud import test_port # Mock out the gettext function so that the task schema can be copypasta @@ -534,6 +535,25 @@ def test_cache_no_cloud_name(self): ], self.cloud.list_images()) + def test_list_ports_filtered(self): + down_port = test_port.TestPort.mock_neutron_port_create_rep['port'] + active_port = down_port.copy() + active_port['status'] = 'ACTIVE' + # We're testing to make sure a query string isn't passed when we're + # caching, but that the results are still filtered. + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json']), + json={'ports': [ + down_port, + active_port, + ]}), + ]) + ports = self.cloud.list_ports(filters={'status': 'DOWN'}) + self.assertItemsEqual([down_port], ports) + self.assert_calls() + class TestCacheIgnoresQueuedStatus(base.TestCase): diff --git a/openstack/tests/unit/cloud/test_port.py b/openstack/tests/unit/cloud/test_port.py index e7092674b..53412660c 100644 --- a/openstack/tests/unit/cloud/test_port.py +++ b/openstack/tests/unit/cloud/test_port.py @@ -241,6 +241,18 @@ def test_list_ports(self): self.assertItemsEqual(self.mock_neutron_port_list_rep['ports'], ports) self.assert_calls() + def test_list_ports_filtered(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports.json'], + qs_elements=['status=DOWN']), + json=self.mock_neutron_port_list_rep) + ]) + ports = self.cloud.list_ports(filters={'status': 'DOWN'}) + self.assertItemsEqual(self.mock_neutron_port_list_rep['ports'], ports) + self.assert_calls() + def test_list_ports_exception(self): self.register_uris([ dict(method='GET', diff --git a/openstack/tests/unit/fixtures/clouds/clouds_cache.yaml b/openstack/tests/unit/fixtures/clouds/clouds_cache.yaml index eb01d37ec..21fb137a9 100644 --- a/openstack/tests/unit/fixtures/clouds/clouds_cache.yaml +++ b/openstack/tests/unit/fixtures/clouds/clouds_cache.yaml @@ -3,6 +3,7 @@ cache: class: dogpile.cache.memory expiration: server: 1 + port: 1 clouds: _test_cloud_: auth: From 49cf6f914afc1fe7277080443fc60bee5c2ff338 Mon Sep 17 00:00:00 2001 From: Sean McGinnis Date: Tue, 30 Oct 2018 10:38:41 -0500 Subject: [PATCH 2266/3836] Remove setup.py check from pep8 job Using "python setup.py check -r -s" method of checking the package has been deprecated with the new recommendation to build the sdist and wheel, then running "twine check" against the output. Luckily, there is already a job that covers this that only runs when the README, setup.py, or setup.cfg files change, making running this in the pep8 job redundant. This covered by the test-release-openstack-python3 that is defined in the publish-to-pypi-python3 template. More details can be found in this mailing list post: http://lists.openstack.org/pipermail/openstack-dev/2018-October/136136.html Change-Id: I9c760887d90869a44af62f4a4c4c09d3599f6047 Signed-off-by: Sean McGinnis --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index ff31ca10c..6d241d235 100644 --- a/tox.ini +++ b/tox.ini @@ -45,7 +45,6 @@ deps = readme commands = doc8 doc/source - python setup.py check -r -s flake8 [testenv:venv] From 6fef03c7ce71fb8d5aa8f81d0fa04c3fd7f51a79 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 30 Oct 2018 09:47:14 -0500 Subject: [PATCH 2267/3836] Apply list filter fix to servers and floating ips too Servers and floating ips use the same batching/caching logic that ports to - and suffer from the same issue in their push-down code. Let's fix them too. Change-Id: Ia5c40ef333d3084ec026ef1ddbe41aa7c64da610 --- openstack/cloud/openstackcloud.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 34954297b..898c71da5 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -2101,6 +2101,16 @@ def list_servers(self, detailed=False, all_projects=False, bare=False, :returns: A list of server ``munch.Munch``. """ + # If pushdown filters are specified and we do not have batched caching + # enabled, bypass local caching and push down the filters. + if filters and self._SERVER_AGE == 0: + return self._list_servers( + detailed=detailed, + all_projects=all_projects, + bare=bare, + filters=filters, + ) + if (time.time() - self._servers_time) >= self._SERVER_AGE: # Since we're using cached data anyway, we don't need to # have more than one thread actually submit the list @@ -2116,12 +2126,14 @@ def list_servers(self, detailed=False, all_projects=False, bare=False, self._servers = self._list_servers( detailed=detailed, all_projects=all_projects, - bare=bare, - filters=filters) + bare=bare) self._servers_time = time.time() finally: self._servers_lock.release() - return self._servers + # Wrap the return with filter_list so that if filters were passed + # but we were batching/caching and thus always fetching the whole + # list from the cloud, we still return a filtered list. + return _utils._filter_list(self._servers, None, filters) def _list_servers(self, detailed=False, all_projects=False, bare=False, filters=None): @@ -2333,7 +2345,10 @@ def list_floating_ips(self, filters=None): self._floating_ips_time = time.time() finally: self._floating_ips_lock.release() - return self._floating_ips + # Wrap the return with filter_list so that if filters were passed + # but we were batching/caching and thus always fetching the whole + # list from the cloud, we still return a filtered list. + return _utils._filter_list(self._floating_ips, None, filters) def _neutron_list_floating_ips(self, filters=None): if not filters: From d33109b19347682891b705c1b0b65db76b2df96d Mon Sep 17 00:00:00 2001 From: Logan V Date: Thu, 4 Oct 2018 11:56:23 -0500 Subject: [PATCH 2268/3836] Add networks to Limestone vendor Add the network configurations to the Limestone vendor config Change-Id: I1f7c0b10e391c59edae17acbc06da239f953a0a0 --- .../config/vendors/limestonenetworks.json | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/openstack/config/vendors/limestonenetworks.json b/openstack/config/vendors/limestonenetworks.json index 659a95b9f..dcef5ae19 100644 --- a/openstack/config/vendors/limestonenetworks.json +++ b/openstack/config/vendors/limestonenetworks.json @@ -10,6 +10,27 @@ ], "identity_api_version": "3", "image_format": "raw", - "volume_api_version": "3" + "volume_api_version": "3", + "networks": [ + { + "name": "Public Internet", + "routes_externally": true, + "default_interface": true, + "nat_source": true + }, + { + "name": "DDoS Protected", + "routes_externally": true + }, + { + "name": "Private Network (10.0.0.0/8 only)", + "routes_externally": false + }, + { + "name": "Private Network (Floating Public)", + "routes_externally": false, + "nat_destination": true + } + ] } } From ff917980a692b01d8c9787de7d4a78363f4bba5d Mon Sep 17 00:00:00 2001 From: brandonzhao Date: Wed, 31 Oct 2018 16:57:23 +0800 Subject: [PATCH 2269/3836] Fix the conflict of urlparse between python2 and python3 Change-Id: I9f44ac8bb475da7cfd3ad9d593fdb7a84e5d1395 --- tools/keystone_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/keystone_version.py b/tools/keystone_version.py index 418505c6b..663e6c878 100644 --- a/tools/keystone_version.py +++ b/tools/keystone_version.py @@ -16,7 +16,7 @@ import openstack.config import pprint import sys -import urlparse +from six.moves.urllib import parse as urlparse def print_versions(r): From a084fe78bcdc17aacb20dc3226ca071f32069561 Mon Sep 17 00:00:00 2001 From: Corey Wright Date: Wed, 31 Oct 2018 11:33:25 -0500 Subject: [PATCH 2270/3836] Fix bugs in debugging with Tox Fix PDB-based test debugging using Tox `debug` environment, ie `tox -e debug `. * Tell `oslo_test_helper` what directory holds test. * Fixes `ImportError: Start directory is not importable: './openstacksdk/tests'` * See https://docs.openstack.org/oslotest/rocky/user/features.html#update-tox-ini * Effectively disable test timeouts when debugging by setting timeout to 1 year. * Fixes `fixtures._fixtures.timeout.TimeoutException` during PDB prompt. Story: 2004225 Task: 27744 Change-Id: I40fea330e6b6dba78927de66bfe643b9c9a3e5e6 --- tox.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 6d241d235..705a82bdb 100644 --- a/tox.ini +++ b/tox.ini @@ -51,10 +51,12 @@ commands = commands = {posargs} [testenv:debug] +# allow 1 year, or 31536000 seconds, to debug a test before it times out +setenv = OS_TEST_TIMEOUT=31536000 whitelist_externals = find commands = find . -type f -name "*.pyc" -delete - oslo_debug_helper {posargs} + oslo_debug_helper -t openstack/tests {posargs} [testenv:cover] setenv = From dbaf360a37023a26e3eeffb8fdffae07d1855228 Mon Sep 17 00:00:00 2001 From: Corey Wright Date: Wed, 31 Oct 2018 20:03:03 -0500 Subject: [PATCH 2271/3836] Explicit set capabilities in VolumeDetail and SnapshotDetail VolumeDetail and SnapshotDetail classes inherit the capability attributes of their parent classes, ie Volume and Snapshot, but their `base_path` only supports listing instances, unlike their parent classes, so explicitly set all capability attributes to `False` except for `allow_list`. Story: 2004226 Task: 27746 Change-Id: I001f19093bcf6e5eb18c15d7e1dab8c0f723e04b --- openstack/block_storage/v2/snapshot.py | 7 +++++++ openstack/block_storage/v2/volume.py | 7 +++++++ openstack/tests/unit/block_storage/v2/test_snapshot.py | 5 +++++ openstack/tests/unit/block_storage/v2/test_volume.py | 5 +++++ 4 files changed, 24 insertions(+) diff --git a/openstack/block_storage/v2/snapshot.py b/openstack/block_storage/v2/snapshot.py index 4962014cf..120b50b50 100644 --- a/openstack/block_storage/v2/snapshot.py +++ b/openstack/block_storage/v2/snapshot.py @@ -57,6 +57,13 @@ class SnapshotDetail(Snapshot): base_path = "/snapshots/detail" + # capabilities + allow_fetch = False + allow_create = False + allow_delete = False + allow_commit = False + allow_list = True + #: The percentage of completeness the snapshot is currently at. progress = resource.Body("os-extended-snapshot-attributes:progress") #: The project ID this snapshot is associated with. diff --git a/openstack/block_storage/v2/volume.py b/openstack/block_storage/v2/volume.py index c2dfe4380..28ed870ff 100644 --- a/openstack/block_storage/v2/volume.py +++ b/openstack/block_storage/v2/volume.py @@ -94,6 +94,13 @@ class VolumeDetail(Volume): base_path = "/volumes/detail" + # capabilities + allow_fetch = False + allow_create = False + allow_delete = False + allow_commit = False + allow_list = True + #: The volume's current back-end. host = resource.Body("os-vol-host-attr:host") #: The project ID associated with current back-end. diff --git a/openstack/tests/unit/block_storage/v2/test_snapshot.py b/openstack/tests/unit/block_storage/v2/test_snapshot.py index e502f8b70..4910b6db7 100644 --- a/openstack/tests/unit/block_storage/v2/test_snapshot.py +++ b/openstack/tests/unit/block_storage/v2/test_snapshot.py @@ -77,6 +77,11 @@ def test_basic(self): sot = snapshot.SnapshotDetail(DETAILED_SNAPSHOT) self.assertIsInstance(sot, snapshot.Snapshot) self.assertEqual("/snapshots/detail", sot.base_path) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_create) + self.assertFalse(sot.allow_delete) + self.assertTrue(sot.allow_list) def test_create_detailed(self): sot = snapshot.SnapshotDetail(**DETAILED_SNAPSHOT) diff --git a/openstack/tests/unit/block_storage/v2/test_volume.py b/openstack/tests/unit/block_storage/v2/test_volume.py index 935941f68..da20309c8 100644 --- a/openstack/tests/unit/block_storage/v2/test_volume.py +++ b/openstack/tests/unit/block_storage/v2/test_volume.py @@ -126,6 +126,11 @@ def test_basic(self): sot = volume.VolumeDetail(VOLUME_DETAIL) self.assertIsInstance(sot, volume.Volume) self.assertEqual("/volumes/detail", sot.base_path) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_create) + self.assertFalse(sot.allow_delete) + self.assertTrue(sot.allow_list) def test_create(self): sot = volume.VolumeDetail(**VOLUME_DETAIL) From 999ff0eb7fbd13f49892b542e25d21102ec37c6c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 1 Nov 2018 10:00:17 -0500 Subject: [PATCH 2272/3836] Remove mocking workaround from adapter We had a workaround in the adapter for places where we were mocking internals of things. We're not longer doing that and are instead appropriately using requests-mock, so we no longer need the workaround. Change-Id: I4c9b6e784752f9ca2d1009553003913bad1761d0 --- openstack/_adapter.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/openstack/_adapter.py b/openstack/_adapter.py index 18b4e7dd9..bb301b187 100644 --- a/openstack/_adapter.py +++ b/openstack/_adapter.py @@ -135,13 +135,7 @@ def request( self, url, method, run_async=False, error_message=None, raise_exc=False, connect_retries=1, *args, **kwargs): name_parts = _extract_name(url, self.service_type) - # TODO(mordred) This if is in service of unit tests that are making - # calls without a service_type. It should be fixable once we shift - # to requests-mock and stop mocking internals. - if self.service_type: - name = '.'.join([self.service_type, method] + name_parts) - else: - name = '.'.join([method] + name_parts) + name = '.'.join([self.service_type, method] + name_parts) request_method = functools.partial( super(OpenStackSDKAdapter, self).request, url, method) From 3755456cbd521cdc7a1924b9d089f5a065e56ce5 Mon Sep 17 00:00:00 2001 From: Vieri <15050873171@163.com> Date: Fri, 2 Nov 2018 06:53:51 +0000 Subject: [PATCH 2273/3836] Update min tox version to 2.0 The commands used by constraints need at least tox 2.0. Update to reflect reality, which should help with local running of constraints targets. Change-Id: I9b4c39a0d33fabadb1595389281466ba5d2453b2 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 4710892ca..c6d500e53 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -minversion = 1.6 +minversion = 2.0 envlist = py35,py36,py27,pep8 skipsdist = True From 4a345e20761ebef4c614a1353adf59fa26a309e8 Mon Sep 17 00:00:00 2001 From: Nguyen Hai Truong Date: Sun, 4 Nov 2018 17:47:24 +0700 Subject: [PATCH 2274/3836] [Trivial Fix] Correct spelling error of "bandwidth" Small modification to correct spelling mistake. Change-Id: Ib0ba0c8594f5ffb4a7aa4ebb3923ce537fbf2550 --- openstack/cloud/openstackcloud.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 37aaa74c5..fe5c09456 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -3672,7 +3672,7 @@ def search_qos_bandwidth_limit_rules(self, policy_name_or_id, rule_id=None, return _utils._filter_list(rules, rule_id, filters) def list_qos_bandwidth_limit_rules(self, policy_name_or_id, filters=None): - """List all available QoS bandwith limit rules. + """List all available QoS bandwidth limit rules. :param string policy_name_or_id: Name or ID of the QoS policy from from rules should be listed. @@ -3702,7 +3702,7 @@ def list_qos_bandwidth_limit_rules(self, policy_name_or_id, filters=None): params=filters) data = _adapter._json_response( resp, - error_message="Error fetching QoS bandwith limit rules from " + error_message="Error fetching QoS bandwidth limit rules from " "{policy}".format(policy=policy['id'])) return self._get_and_munchify('bandwidth_limit_rules', data) @@ -3732,7 +3732,7 @@ def get_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): format(policy_id=policy['id'], rule_id=rule_id)) data = _adapter._json_response( resp, - error_message="Error fetching QoS bandwith limit rule {rule_id} " + error_message="Error fetching QoS bandwidth limit rule {rule_id} " "from {policy}".format(rule_id=rule_id, policy=policy['id'])) return self._get_and_munchify('bandwidth_limit_rule', data) @@ -4070,7 +4070,7 @@ def search_qos_minimum_bandwidth_rules(self, policy_name_or_id, def list_qos_minimum_bandwidth_rules(self, policy_name_or_id, filters=None): - """List all available QoS minimum bandwith rules. + """List all available QoS minimum bandwidth rules. :param string policy_name_or_id: Name or ID of the QoS policy from from rules should be listed. @@ -4100,7 +4100,7 @@ def list_qos_minimum_bandwidth_rules(self, policy_name_or_id, params=filters) data = _adapter._json_response( resp, - error_message="Error fetching QoS minimum bandwith rules from " + error_message="Error fetching QoS minimum bandwidth rules from " "{policy}".format(policy=policy['id'])) return self._get_and_munchify('minimum_bandwidth_rules', data) @@ -4130,9 +4130,9 @@ def get_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): format(policy_id=policy['id'], rule_id=rule_id)) data = _adapter._json_response( resp, - error_message="Error fetching QoS minimum_bandwith rule {rule_id} " - "from {policy}".format(rule_id=rule_id, - policy=policy['id'])) + error_message="Error fetching QoS minimum_bandwidth rule {rule_id}" + " from {policy}".format(rule_id=rule_id, + policy=policy['id'])) return self._get_and_munchify('minimum_bandwidth_rule', data) @_utils.valid_kwargs("direction") From 0b625424a25095acaf499a7060eb7e4121874683 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 1 Nov 2018 10:53:56 -0500 Subject: [PATCH 2275/3836] Shift swift segment async code out of adapter Rather than having a run_async parameter to adapter which impacts what the request method returns, do the concurrent.futures work in the segment upload code itself. This is one of the steps towards shifting to the keystoneauth-based rate limiting. Change-Id: I0ed6cd566f4c450fedbe14712fc8f3712d736f6a --- openstack/_adapter.py | 18 +++------- openstack/cloud/openstackcloud.py | 55 +++++++++++++++++++++++++++---- 2 files changed, 53 insertions(+), 20 deletions(-) diff --git a/openstack/_adapter.py b/openstack/_adapter.py index bb301b187..6f9f341e3 100644 --- a/openstack/_adapter.py +++ b/openstack/_adapter.py @@ -132,7 +132,7 @@ def __init__( self.task_manager = task_manager def request( - self, url, method, run_async=False, error_message=None, + self, url, method, error_message=None, raise_exc=False, connect_retries=1, *args, **kwargs): name_parts = _extract_name(url, self.service_type) name = '.'.join([self.service_type, method] + name_parts) @@ -145,10 +145,7 @@ def request( connect_retries=connect_retries, raise_exc=raise_exc, tag=self.service_type, **kwargs) - if run_async: - return ret - else: - return ret.result() + return ret.result() def _version_matches(self, version): api_version = self.get_api_major_version() @@ -160,11 +157,6 @@ def _version_matches(self, version): class ShadeAdapter(OpenStackSDKAdapter): """Wrapper for shade methods that expect json unpacking.""" - def request(self, url, method, - run_async=False, error_message=None, **kwargs): - response = super(ShadeAdapter, self).request( - url, method, run_async=run_async, **kwargs) - if run_async: - return response - else: - return _json_response(response, error_message=error_message) + def request(self, url, method, error_message=None, **kwargs): + response = super(ShadeAdapter, self).request(url, method, **kwargs) + return _json_response(response, error_message=error_message) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 37aaa74c5..9444aef09 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -12,6 +12,7 @@ import base64 import collections +import concurrent.futures import copy import datetime import functools @@ -51,7 +52,6 @@ from openstack.cloud import _utils import openstack.config import openstack.config.defaults -from openstack import task_manager from openstack import utils # Rackspace returns this for intermittent import errors @@ -259,6 +259,8 @@ def invalidate(self): self._container_cache = dict() self._file_hash_cache = dict() + self.__pool_executor = None + self._raw_clients = {} self._local_ipv6 = ( @@ -7828,6 +7830,42 @@ def _add_etag_to_manifest(self, segment_results, manifest): if entry['path'] == '/{name}'.format(name=name): entry['etag'] = result.headers['Etag'] + @property + def _pool_executor(self): + if not self.__pool_executor: + # TODO(mordred) Make this configurable - and probably use Futurist + # instead of concurrent.futures so that people using Eventlet will + # be happier. + self.__pool_executor = concurrent.futures.ThreadPoolExecutor( + max_workers=5) + return self.__pool_executor + + def _wait_for_futures(self, futures, raise_on_error=True): + '''Collect results or failures from a list of running future tasks.''' + + results = [] + retries = [] + + # Check on each result as its thread finishes + for completed in concurrent.futures.as_completed(futures): + try: + result = completed.result() + exceptions.raise_from_response(result) + results.append(result) + except (keystoneauth1.exceptions.RetriableConnectionFailure, + exceptions.HttpException) as e: + error_text = "Exception processing async task: {}".format( + str(e)) + if raise_on_error: + self.log.exception(error_text) + raise + else: + self.log.debug(error_text) + # If we get an exception, put the result into a list so we + # can try again + retries.append(completed.result()) + return results, retries + def _upload_large_object( self, endpoint, filename, headers, file_size, segment_size, use_slo): @@ -7851,8 +7889,10 @@ def _upload_large_object( # Schedule the segments for upload for name, segment in segments.items(): # Async call to put - schedules execution and returns a future - segment_future = self._object_store_client.put( - name, headers=headers, data=segment, run_async=True) + segment_future = self._pool_executor.submit( + self.object_store.put, + name, headers=headers, data=segment, + raise_exc=False) segment_futures.append(segment_future) # TODO(mordred) Collect etags from results to add to this manifest # dict. Then sort the list of dicts by path. @@ -7861,7 +7901,7 @@ def _upload_large_object( size_bytes=segment.length)) # Try once and collect failed results to retry - segment_results, retry_results = task_manager.wait_for_futures( + segment_results, retry_results = self._wait_for_futures( segment_futures, raise_on_error=False) self._add_etag_to_manifest(segment_results, manifest) @@ -7872,14 +7912,15 @@ def _upload_large_object( segment = segments[name] segment.seek(0) # Async call to put - schedules execution and returns a future - segment_future = self._object_store_client.put( - name, headers=headers, data=segment, run_async=True) + segment_future = self._pool_executor.submit( + self.object_store.put, + name, headers=headers, data=segment) # TODO(mordred) Collect etags from results to add to this manifest # dict. Then sort the list of dicts by path. retry_futures.append(segment_future) # If any segments fail the second time, just throw the error - segment_results, retry_results = task_manager.wait_for_futures( + segment_results, retry_results = self._wait_for_futures( retry_futures, raise_on_error=True) self._add_etag_to_manifest(segment_results, manifest) From 8ba8a7f90366356c1b742a88386731c6de143232 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 1 Nov 2018 11:00:15 -0500 Subject: [PATCH 2276/3836] Remove unused Task classes We haven't used these in a while, but they got missed in the patch that removed their use. Change-Id: Ic1ce458942b131aa1c72ee1a85d0688d1883a5cc --- openstack/cloud/_tasks.py | 108 -------------------------------------- 1 file changed, 108 deletions(-) delete mode 100644 openstack/cloud/_tasks.py diff --git a/openstack/cloud/_tasks.py b/openstack/cloud/_tasks.py deleted file mode 100644 index ee65566c6..000000000 --- a/openstack/cloud/_tasks.py +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright (c) 2015 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. - -from openstack import task_manager - - -class IronicTask(task_manager.Task): - - def __init__(self, client, **kwargs): - super(IronicTask, self).__init__(**kwargs) - self.client = client - - -class MachineCreate(IronicTask): - def main(self): - return self.client.ironic_client.node.create(*self.args, **self.kwargs) - - -class MachineDelete(IronicTask): - def main(self): - return self.client.ironic_client.node.delete(*self.args, **self.kwargs) - - -class MachinePatch(IronicTask): - def main(self): - return self.client.ironic_client.node.update(*self.args, **self.kwargs) - - -class MachinePortGet(IronicTask): - def main(self): - return self.client.ironic_client.port.get(*self.args, **self.kwargs) - - -class MachinePortGetByAddress(IronicTask): - def main(self): - return self.client.ironic_client.port.get_by_address( - *self.args, **self.kwargs) - - -class MachinePortCreate(IronicTask): - def main(self): - return self.client.ironic_client.port.create(*self.args, **self.kwargs) - - -class MachinePortDelete(IronicTask): - def main(self): - return self.client.ironic_client.port.delete(*self.args, **self.kwargs) - - -class MachinePortList(IronicTask): - def main(self): - return self.client.ironic_client.port.list() - - -class MachineNodeGet(IronicTask): - def main(self): - return self.client.ironic_client.node.get(*self.args, **self.kwargs) - - -class MachineNodeList(IronicTask): - def main(self): - return self.client.ironic_client.node.list(*self.args, **self.kwargs) - - -class MachineNodePortList(IronicTask): - def main(self): - return self.client.ironic_client.node.list_ports( - *self.args, **self.kwargs) - - -class MachineNodeUpdate(IronicTask): - def main(self): - return self.client.ironic_client.node.update(*self.args, **self.kwargs) - - -class MachineNodeValidate(IronicTask): - def main(self): - return self.client.ironic_client.node.validate( - *self.args, **self.kwargs) - - -class MachineSetMaintenance(IronicTask): - def main(self): - return self.client.ironic_client.node.set_maintenance( - *self.args, **self.kwargs) - - -class MachineSetPower(IronicTask): - def main(self): - return self.client.ironic_client.node.set_power_state( - *self.args, **self.kwargs) - - -class MachineSetProvision(IronicTask): - def main(self): - return self.client.ironic_client.node.set_provision_state( - *self.args, **self.kwargs) From 46ca87b8e8d08d96a039b9fd956fdc5e1a3632df Mon Sep 17 00:00:00 2001 From: Ian Wienand Date: Mon, 5 Nov 2018 19:59:17 +1100 Subject: [PATCH 2277/3836] Document "insecure" flag I received a clouds.yaml with "insecure: True" which I had not seen before. There's no explicit mention of this in the documentation, but due to a name clash in the example in the reference it initially led me to believe it was a section of the config file. So firstly make the cloud names more verbose to illustrate what's going on, and add a mention of the "insecure" flag, but suggest using "verify". Similarly in the unit tests use more explicit cloud names to make it obvious what's going on. Add a test that ensures "insecure=True" implies "verify=False". Change-Id: I56fe7460431442ad4ca264e32d2494068cf7f05e --- doc/source/user/config/configuration.rst | 21 +++++++++-- openstack/tests/unit/test_connection.py | 48 +++++++++++++++--------- 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/doc/source/user/config/configuration.rst b/doc/source/user/config/configuration.rst index dc3b9000b..1cdd0ec10 100644 --- a/doc/source/user/config/configuration.rst +++ b/doc/source/user/config/configuration.rst @@ -184,15 +184,28 @@ its file location needs to be set via `key`. # clouds.yaml clouds: - secure: - auth: ... + regular-secure-cloud: + auth: + auth_url: https://signed.cert.domain:5000 + ... + unknown-ca-with-client-cert-secure-cloud: + auth: + auth_url: https://unknown.ca.but.secure.domain:5000 + ... key: /home/myhome/client-cert.key cert: /home/myhome/client-cert.crt cacert: /home/myhome/ca.crt - insecure: - auth: ... + self-signed-insecure-cloud: + auth: + auth_url: https://self.signed.cert.domain:5000 + ... verify: False +Note for parity with ``openstack`` command-line options the `insecure` +boolean is also recognised (with the opposite semantics to `verify`; +i.e. `True` ignores certificate failures). This should be considered +deprecated for `verify`. + Cache Settings -------------- diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index 2605525d7..84e54b37b 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -29,14 +29,14 @@ CLOUD_CONFIG = """ clouds: - sample: + sample-cloud: region_name: RegionOne auth: auth_url: {auth_url} username: {username} password: {password} project_name: {project} - insecure: + insecure-cloud: auth: auth_url: {auth_url} username: {username} @@ -44,7 +44,14 @@ project_name: {project} cacert: {cacert} verify: False - cacert: + insecure-cloud-alternative-format: + auth: + auth_url: {auth_url} + username: {username} + password: {password} + project_name: {project} + insecure: True + cacert-cloud: auth: auth_url: {auth_url} username: {username} @@ -73,7 +80,7 @@ def setUp(self): self.use_keystone_v2() def test_other_parameters(self): - conn = connection.Connection(cloud='sample', cert='cert') + conn = connection.Connection(cloud='sample-cloud', cert='cert') self.assertEqual(conn.session.cert, 'cert') def test_session_provided(self): @@ -85,13 +92,13 @@ def test_session_provided(self): self.assertEqual('auth.example.com', conn.config.name) def test_task_manager_rate_scalar(self): - conn = connection.Connection(cloud='sample', rate_limit=20) + conn = connection.Connection(cloud='sample-cloud', rate_limit=20) self.assertEqual(1 / 20, conn.task_manager._get_wait('object-store')) self.assertEqual(1 / 20, conn.task_manager._get_wait(None)) def test_task_manager_rate_dict(self): conn = connection.Connection( - cloud='sample', + cloud='sample-cloud', rate_limit={ 'compute': 20, 'network': 10, @@ -101,7 +108,7 @@ def test_task_manager_rate_dict(self): self.assertIsNone(conn.task_manager._get_wait('object-store')) def test_create_session(self): - conn = connection.Connection(cloud='sample') + conn = connection.Connection(cloud='sample-cloud') self.assertIsNotNone(conn) # TODO(mordred) Rework this - we need to provide requests-mock # entries for each of the proxies below @@ -127,27 +134,27 @@ def test_create_session(self): # conn.workflow.__class__.__module__) def test_create_connection_version_param_default(self): - c1 = connection.Connection(cloud='sample') + c1 = connection.Connection(cloud='sample-cloud') conn = connection.Connection(session=c1.session) self.assertEqual('openstack.identity.v3._proxy', conn.identity.__class__.__module__) def test_create_connection_version_param_string(self): - c1 = connection.Connection(cloud='sample') + c1 = connection.Connection(cloud='sample-cloud') conn = connection.Connection( session=c1.session, identity_api_version='2') self.assertEqual('openstack.identity.v2._proxy', conn.identity.__class__.__module__) def test_create_connection_version_param_int(self): - c1 = connection.Connection(cloud='sample') + c1 = connection.Connection(cloud='sample-cloud') conn = connection.Connection( session=c1.session, identity_api_version=3) self.assertEqual('openstack.identity.v3._proxy', conn.identity.__class__.__module__) def test_create_connection_version_param_bogus(self): - c1 = connection.Connection(cloud='sample') + c1 = connection.Connection(cloud='sample-cloud') conn = connection.Connection( session=c1.session, identity_api_version='red') # TODO(mordred) This is obviously silly behavior @@ -155,7 +162,8 @@ def test_create_connection_version_param_bogus(self): conn.identity.__class__.__module__) def test_from_config_given_config(self): - cloud_region = openstack.config.OpenStackConfig().get_one("sample") + cloud_region = (openstack.config.OpenStackConfig(). + get_one("sample-cloud")) sot = connection.from_config(config=cloud_region) @@ -169,7 +177,7 @@ def test_from_config_given_config(self): sot.config.config['auth']['project_name']) def test_from_config_given_cloud(self): - sot = connection.from_config(cloud="sample") + sot = connection.from_config(cloud="sample-cloud") self.assertEqual(CONFIG_USERNAME, sot.config.config['auth']['username']) @@ -181,7 +189,8 @@ def test_from_config_given_cloud(self): sot.config.config['auth']['project_name']) def test_from_config_given_cloud_config(self): - cloud_region = openstack.config.OpenStackConfig().get_one("sample") + cloud_region = (openstack.config.OpenStackConfig(). + get_one("sample-cloud")) sot = connection.from_config(cloud_config=cloud_region) @@ -195,7 +204,7 @@ def test_from_config_given_cloud_config(self): sot.config.config['auth']['project_name']) def test_from_config_given_cloud_name(self): - sot = connection.from_config(cloud_name="sample") + sot = connection.from_config(cloud_name="sample-cloud") self.assertEqual(CONFIG_USERNAME, sot.config.config['auth']['username']) @@ -207,12 +216,17 @@ def test_from_config_given_cloud_name(self): sot.config.config['auth']['project_name']) def test_from_config_verify(self): - sot = connection.from_config(cloud="insecure") + sot = connection.from_config(cloud="insecure-cloud") self.assertFalse(sot.session.verify) - sot = connection.from_config(cloud="cacert") + sot = connection.from_config(cloud="cacert-cloud") self.assertEqual(CONFIG_CACERT, sot.session.verify) + def test_from_config_insecure(self): + # Ensure that the "insecure=True" flag implies "verify=False" + sot = connection.from_config("insecure-cloud-alternative-format") + self.assertFalse(sot.session.verify) + class TestNetworkConnection(base.TestCase): # We need to do the neutron adapter test differently because it needs From ca636b92e3fcea71c14dc15fdcf4937ee138ab96 Mon Sep 17 00:00:00 2001 From: yatin Date: Mon, 5 Nov 2018 17:38:47 +0530 Subject: [PATCH 2278/3836] Test python2 with py27 py3 tests were running with py27. Add ignore_basepython_conflict to tox.ini so that the basepython of python3 doesn't override the py27 env. Change-Id: I3ff782fccbc7ffd50f703a006f63b321e673a879 --- openstack/tests/unit/cloud/test_task_manager.py | 4 ++-- openstack/tests/unit/test_connection.py | 8 ++++---- tox.ini | 3 ++- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/openstack/tests/unit/cloud/test_task_manager.py b/openstack/tests/unit/cloud/test_task_manager.py index fd8907199..89fc2dfce 100644 --- a/openstack/tests/unit/cloud/test_task_manager.py +++ b/openstack/tests/unit/cloud/test_task_manager.py @@ -78,8 +78,8 @@ def test_rate_parameter_dict(self): 'compute': 20, 'network': 10, }) - self.assertEqual(1 / 20, manager._get_wait('compute')) - self.assertEqual(1 / 10, manager._get_wait('network')) + self.assertEqual(1.0 / 20, manager._get_wait('compute')) + self.assertEqual(1.0 / 10, manager._get_wait('network')) self.assertIsNone(manager._get_wait('object-store')) diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index 84e54b37b..936b11b01 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -93,8 +93,8 @@ def test_session_provided(self): def test_task_manager_rate_scalar(self): conn = connection.Connection(cloud='sample-cloud', rate_limit=20) - self.assertEqual(1 / 20, conn.task_manager._get_wait('object-store')) - self.assertEqual(1 / 20, conn.task_manager._get_wait(None)) + self.assertEqual(1.0 / 20, conn.task_manager._get_wait('object-store')) + self.assertEqual(1.0 / 20, conn.task_manager._get_wait(None)) def test_task_manager_rate_dict(self): conn = connection.Connection( @@ -103,8 +103,8 @@ def test_task_manager_rate_dict(self): 'compute': 20, 'network': 10, }) - self.assertEqual(1 / 20, conn.task_manager._get_wait('compute')) - self.assertEqual(1 / 10, conn.task_manager._get_wait('network')) + self.assertEqual(1.0 / 20, conn.task_manager._get_wait('compute')) + self.assertEqual(1.0 / 10, conn.task_manager._get_wait('network')) self.assertIsNone(conn.task_manager._get_wait('object-store')) def test_create_session(self): diff --git a/tox.ini b/tox.ini index c6d500e53..ad02bfe85 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,8 @@ [tox] -minversion = 2.0 +minversion = 3.1 envlist = py35,py36,py27,pep8 skipsdist = True +ignore_basepython_conflict = True [testenv] usedevelop = True From e0b417623d501c69666492ceefade20b136d24ae Mon Sep 17 00:00:00 2001 From: Ian Wienand Date: Wed, 7 Nov 2018 16:04:05 +1100 Subject: [PATCH 2279/3836] Add a __main__ handler, version command Recently I've been querying what versions of openstacksdk are installed in various environments, it would be handy to have a quick way to see what's installed. This adds a __main__ handler and a single command "version" to print the full version (with git reference). Change-Id: I690f2987740c804735879985d19752c31490c7c9 --- doc/source/user/index.rst | 4 ++ openstack/__main__.py | 40 +++++++++++++++++++ .../version-command-70c37dd7f880e9ae.yaml | 4 ++ 3 files changed, 48 insertions(+) create mode 100644 openstack/__main__.py create mode 100644 releasenotes/notes/version-command-70c37dd7f880e9ae.yaml diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index 8e66c897b..0dcbb3a50 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -13,6 +13,10 @@ The OpenStack SDK is available on $ pip install openstacksdk +To check the installed version you can call the module with :: + + $ python -m openstack version + .. _user_guides: User Guides diff --git a/openstack/__main__.py b/openstack/__main__.py new file mode 100644 index 000000000..30d775de9 --- /dev/null +++ b/openstack/__main__.py @@ -0,0 +1,40 @@ +# Copyright (c) 2018 Red Hat, 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 argparse +import sys + +import pbr.version + + +def show_version(args): + print("OpenstackSDK Version %s" % + pbr.version.VersionInfo('openstacksdk').version_string_with_vcs()) + + +parser = argparse.ArgumentParser(description="Openstack SDK") +subparsers = parser.add_subparsers(title='commands', + dest='command') + +cmd_version = subparsers.add_parser('version', + help='show Openstack SDK version') +cmd_version.set_defaults(func=show_version) + +args = parser.parse_args() + +if not args.command: + parser.print_help() + sys.exit(1) + +args.func(args) diff --git a/releasenotes/notes/version-command-70c37dd7f880e9ae.yaml b/releasenotes/notes/version-command-70c37dd7f880e9ae.yaml new file mode 100644 index 000000000..db9b5d672 --- /dev/null +++ b/releasenotes/notes/version-command-70c37dd7f880e9ae.yaml @@ -0,0 +1,4 @@ +--- +features: + - The installed version can now be quickly checked with ``python -m + openstack version``. From 92eb2edd76e91b6029cd4348fde342cd7bb4c1e7 Mon Sep 17 00:00:00 2001 From: Duc Truong Date: Mon, 5 Nov 2018 16:49:24 -0800 Subject: [PATCH 2280/3836] Add wait functions to orchestration proxy Senlin uses wait_for_status and wait_for_delete that were previously provided in openstack.proxy.Proxy. This adds the functions removed from openstack.proxy.Proxy to the orchestration proxy. This change also reenables the orchestration functional test. Change-Id: I70b478669951237bc290c66d4ae160ec21c655ac --- openstack/orchestration/v1/_proxy.py | 42 +++++++++++++++++++ .../functional/orchestration/v1/test_stack.py | 10 +++-- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index 4378353e1..7b0423621 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -20,6 +20,7 @@ from openstack.orchestration.v1 import stack_template as _stack_template from openstack.orchestration.v1 import template as _template from openstack import proxy +from openstack import resource class Proxy(proxy.Proxy): @@ -360,3 +361,44 @@ def validate_template(self, template, environment=None, template_url=None, return tmpl.validate(self, template, environment=environment, template_url=template_url, ignore_errors=ignore_errors) + + def wait_for_status(self, res, status='ACTIVE', failures=None, + interval=2, wait=120): + """Wait for a resource to be in a particular status. + + :param res: The resource to wait on to reach the specified status. + The resource must have a ``status`` attribute. + :type resource: A :class:`~openstack.resource.Resource` object. + :param status: Desired status. + :param failures: Statuses that would be interpreted as failures. + :type failures: :py:class:`list` + :param interval: Number of seconds to wait before to consecutive + checks. Default to 2. + :param wait: Maximum number of seconds to wait before the change. + Default to 120. + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to the desired status failed to occur in specified seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + has transited to one of the failure statuses. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute. + """ + failures = [] if failures is None else failures + return resource.wait_for_status( + self, res, status, failures, interval, wait) + + def wait_for_delete(self, res, interval=2, wait=120): + """Wait for a resource to be deleted. + + :param res: The resource to wait on to be deleted. + :type resource: A :class:`~openstack.resource.Resource` object. + :param interval: Number of seconds to wait before to consecutive + checks. Default to 2. + :param wait: Maximum number of seconds to wait before the change. + Default to 120. + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete(self, res, interval, wait) diff --git a/openstack/tests/functional/orchestration/v1/test_stack.py b/openstack/tests/functional/orchestration/v1/test_stack.py index 76ae4cf7d..47920bd54 100644 --- a/openstack/tests/functional/orchestration/v1/test_stack.py +++ b/openstack/tests/functional/orchestration/v1/test_stack.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import yaml + from openstack import exceptions from openstack.orchestration.v1 import stack from openstack.tests.functional import base @@ -26,9 +28,6 @@ class TestStack(base.BaseFunctionalTest): def setUp(self): super(TestStack, self).setUp() - self.skipTest( - 'Orchestration functional tests disabled:' - ' https://storyboard.openstack.org/#!/story/1525005') self.require_service('orchestration') if self.conn.compute.find_keypair(self.NAME) is None: @@ -36,7 +35,10 @@ def setUp(self): image = next(self.conn.image.images()) tname = "openstack/tests/functional/orchestration/v1/hello_world.yaml" with open(tname) as f: - template = f.read() + template = yaml.safe_load(f) + # TODO(mordred) Fix the need for this. We have better support in + # the shade layer. + template['heat_template_version'] = '2013-05-23' self.network, self.subnet = test_network.create_network( self.conn, self.NAME, From a7f35beb1dd87ef34b29459571634471d0dabeca Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 9 Nov 2018 10:25:03 -0600 Subject: [PATCH 2281/3836] Fix neutron endpoint mangling If the endpoint has a v2.0 on it, we were double mangling it. Fix the issue and add a test. Change-Id: Ia73ea9ae15038a1aba3b46528d0c21f2ac028ec0 Story: 2004310 Task: 27877 --- openstack/config/cloud_region.py | 2 +- .../unit/fixtures/catalog-v3-suffix.json | 211 ++++++++++++++++++ openstack/tests/unit/test_connection.py | 14 ++ ...on-endpoint-mangling-a9dd89dd09bc71ec.yaml | 5 + 4 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 openstack/tests/unit/fixtures/catalog-v3-suffix.json create mode 100644 releasenotes/notes/fix-neutron-endpoint-mangling-a9dd89dd09bc71ec.yaml diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 7568673bf..e12b91628 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -492,7 +492,7 @@ def get_session_client( region_name=self.region_name, ) network_endpoint = network_adapter.get_endpoint() - if not network_endpoint.rstrip().rsplit('/')[1] == 'v2.0': + if not network_endpoint.rstrip().rsplit('/')[-1] == 'v2.0': if not network_endpoint.endswith('/'): network_endpoint += '/' network_endpoint = urllib.parse.urljoin( diff --git a/openstack/tests/unit/fixtures/catalog-v3-suffix.json b/openstack/tests/unit/fixtures/catalog-v3-suffix.json new file mode 100644 index 000000000..a2a2633bc --- /dev/null +++ b/openstack/tests/unit/fixtures/catalog-v3-suffix.json @@ -0,0 +1,211 @@ +{ + "token": { + "audit_ids": [ + "Rvn7eHkiSeOwucBIPaKdYA" + ], + "catalog": [ + { + "endpoints": [ + { + "id": "32466f357f3545248c47471ca51b0d3a", + "interface": "public", + "region": "RegionOne", + "url": "https://compute.example.com/v2.1/" + } + ], + "name": "nova", + "type": "compute" + }, + { + "endpoints": [ + { + "id": "1e875ca2225b408bbf3520a1b8e1a537", + "interface": "public", + "region": "RegionOne", + "url": "https://volume.example.com/v2/1c36b64c840a42cd9e9b931a369337f0" + } + ], + "name": "cinderv2", + "type": "volumev2" + }, + { + "endpoints": [ + { + "id": "5a64de3c4a614d8d8f8d1ba3dee5f45f", + "interface": "public", + "region": "RegionOne", + "url": "https://image.example.com" + } + ], + "name": "glance", + "type": "image" + }, + { + "endpoints": [ + { + "id": "3d15fdfc7d424f3c8923324417e1a3d1", + "interface": "public", + "region": "RegionOne", + "url": "https://volume.example.com/v1/1c36b64c840a42cd9e9b931a369337f0" + } + ], + "name": "cinder", + "type": "volume" + }, + { + "endpoints": [ + { + "id": "4deb4d0504a044a395d4480741ba628c", + "interface": "public", + "region": "RegionOne", + "url": "https://identity.example.com" + }, + { + "id": "012322eeedcd459edabb4933021112bc", + "interface": "admin", + "region": "RegionOne", + "url": "https://identity.example.com" + } + ], + "endpoints_links": [], + "name": "keystone", + "type": "identity" + }, + { + "endpoints": [ + { + "id": "4deb4d0504a044a395d4480741ba628d", + "interface": "public", + "region": "RegionOne", + "url": "https://network.example.com/v2.0" + } + ], + "endpoints_links": [], + "name": "neutron", + "type": "network" + }, + { + "endpoints": [ + { + "id": "4deb4d0504a044a395d4480741ba628e", + "interface": "public", + "region": "RegionOne", + "url": "https://container-infra.example.com/v1" + } + ], + "endpoints_links": [], + "name": "magnum", + "type": "container-infra" + }, + { + "endpoints": [ + { + "id": "4deb4d0504a044a395d4480741ba628c", + "interface": "public", + "region": "RegionOne", + "url": "https://object-store.example.com/v1/1c36b64c840a42cd9e9b931a369337f0" + } + ], + "endpoints_links": [], + "name": "swift", + "type": "object-store" + }, + { + "endpoints": [ + { + "id": "652f0612744042bfbb8a8bb2c777a16d", + "interface": "public", + "region": "RegionOne", + "url": "https://bare-metal.example.com/" + } + ], + "endpoints_links": [], + "name": "ironic", + "type": "baremetal" + }, + { + "endpoints": [ + { + "id": "4deb4d0504a044a395d4480741ba628c", + "interface": "public", + "region": "RegionOne", + "url": "https://orchestration.example.com/v1/1c36b64c840a42cd9e9b931a369337f0" + } + ], + "endpoints_links": [], + "name": "heat", + "type": "orchestration" + }, + { + "endpoints": [ + { + "id": "10c76ffd2b744a67950ed1365190d353", + "interface": "public", + "region": "RegionOne", + "url": "https://placement.example.com" + } + ], + "endpoints_links": [], + "name": "placement", + "type": "placement" + }, + { + "endpoints": [ + { + "id": "10c76ffd2b744a67950ed1365190d352", + "interface": "public", + "region": "RegionOne", + "url": "https://dns.example.com" + } + ], + "endpoints_links": [], + "name": "designate", + "type": "dns" + }, + { + "endpoints": [ + { + "id": "4deb4d0504a044a395d4480741ba624z", + "interface": "public", + "region": "RegionOne", + "url": "https://clustering.example.com" + } + ], + "endpoints_links": [], + "name": "senlin", + "type": "clustering" + } + ], + "expires_at": "9999-12-31T23:59:59Z", + "issued_at": "2016-12-17T14:25:05.000000Z", + "methods": [ + "password" + ], + "project": { + "domain": { + "id": "default", + "name": "default" + }, + "id": "1c36b64c840a42cd9e9b931a369337f0", + "name": "Default Project" + }, + "roles": [ + { + "id": "9fe2ff9ee4384b1894a90878d3e92bab", + "name": "_member_" + }, + { + "id": "37071fc082e14c2284c32a2761f71c63", + "name": "swiftoperator" + } + ], + "user": { + "domain": { + "id": "default", + "name": "default" + }, + "id": "c17534835f8f42bf98fc367e0bf35e09", + "name": "mordred" + } + } +} diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index 936b11b01..10904a5a1 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -229,6 +229,20 @@ def test_from_config_insecure(self): class TestNetworkConnection(base.TestCase): + + # Verify that if the catalog has the suffix we don't mess things up. + def test_network_proxy(self): + self.use_keystone_v3(catalog='catalog-v3-suffix.json') + self.assertEqual( + 'openstack.network.v2._proxy', + self.conn.network.__class__.__module__) + self.assert_calls() + self.assertEqual( + "https://network.example.com/v2.0", + self.conn.network.get_endpoint()) + + +class TestNetworkConnectionSuffix(base.TestCase): # We need to do the neutron adapter test differently because it needs # to actually get a catalog. diff --git a/releasenotes/notes/fix-neutron-endpoint-mangling-a9dd89dd09bc71ec.yaml b/releasenotes/notes/fix-neutron-endpoint-mangling-a9dd89dd09bc71ec.yaml new file mode 100644 index 000000000..0f4a2d1c7 --- /dev/null +++ b/releasenotes/notes/fix-neutron-endpoint-mangling-a9dd89dd09bc71ec.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixed incorrect neutron endpoint mangling for the cases when the catalog + contains a versioned neutron endpoint. From cdef4806cea1de48ed200d92cf0b217cb738c6f3 Mon Sep 17 00:00:00 2001 From: zhouxinyong Date: Wed, 14 Nov 2018 02:06:15 +0800 Subject: [PATCH 2282/3836] Advancing the protocal of the website to HTTPS in compute.rst. Change-Id: I4d7692790acaed8c7ebaa4905e4645a54db1c22c --- doc/source/user/guides/compute.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/user/guides/compute.rst b/doc/source/user/guides/compute.rst index 3915bcee4..a31295bf9 100644 --- a/doc/source/user/guides/compute.rst +++ b/doc/source/user/guides/compute.rst @@ -83,7 +83,7 @@ for it to become active. Full example: `compute resource create`_ -.. _compute resource list: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/compute/list.py -.. _network resource list: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/network/list.py -.. _compute resource create: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/compute/create.py +.. _compute resource list: https://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/compute/list.py +.. _network resource list: https://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/network/list.py +.. _compute resource create: https://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/compute/create.py .. _public–key cryptography: https://en.wikipedia.org/wiki/Public-key_cryptography From 6f49b8dcc77be0c63d3dce70058ff307bfe47e1a Mon Sep 17 00:00:00 2001 From: fyx Date: Wed, 14 Nov 2018 01:42:08 +0100 Subject: [PATCH 2283/3836] new auth_url for ELASTX Change-Id: I86c39b8919a196b8b9118a49b5b341e9d495487d --- doc/source/user/config/vendor-support.rst | 5 +++-- openstack/config/vendors/elastx.json | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/source/user/config/vendor-support.rst b/doc/source/user/config/vendor-support.rst index 4143da248..87832c7c0 100644 --- a/doc/source/user/config/vendor-support.rst +++ b/doc/source/user/config/vendor-support.rst @@ -141,14 +141,15 @@ eu-de Germany ELASTX ------ -https://ops.elastx.net:5000/v2.0 +https://ops.elastx.cloud:5000/v3 ============== ================ Region Name Location ============== ================ -regionOne Stockholm, SE +se-sto Stockholm, SE ============== ================ +* Identity API Version is 3 * Public IPv4 is provided via NAT with Neutron Floating IP Enter Cloud Suite diff --git a/openstack/config/vendors/elastx.json b/openstack/config/vendors/elastx.json index 676c00290..b6d208b93 100644 --- a/openstack/config/vendors/elastx.json +++ b/openstack/config/vendors/elastx.json @@ -2,7 +2,7 @@ "name": "elastx", "profile": { "auth": { - "auth_url": "https://ops.elastx.net:5000" + "auth_url": "https://ops.elastx.cloud:5000/v3" }, "identity_api_version": "3", "region_name": "se-sto" From b450b456a1d69f214d3766036a38247c852a63f5 Mon Sep 17 00:00:00 2001 From: Tobias Urdin Date: Thu, 15 Nov 2018 16:35:35 +0100 Subject: [PATCH 2284/3836] Fix some spelling in documentation Change-Id: I7923fa5a9dcbc612312589d830061fbb78816d74 --- openstack/cloud/openstackcloud.py | 52 +++++++++++++++---------------- openstack/connection.py | 4 +-- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index dc271e1fb..818dc556a 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -10269,7 +10269,7 @@ def create_service(self, name, enabled=True, **kwargs): - description: :raises: ``OpenStackCloudException`` if something goes wrong during the - openstack API call. + OpenStack API call. """ type_ = kwargs.pop('type', None) @@ -10328,7 +10328,7 @@ def list_services(self): :returns: a list of ``munch.Munch`` containing the services description :raises: ``OpenStackCloudException`` if something goes wrong during the - openstack API call. + OpenStack API call. """ if self._is_client_version('identity', 2): url, key = '/OS-KSADM/services', 'OS-KSADM:services' @@ -10350,7 +10350,7 @@ def search_services(self, name_or_id=None, filters=None): :returns: a list of ``munch.Munch`` containing the services description :raises: ``OpenStackCloudException`` if something goes wrong during the - openstack API call. + OpenStack API call. """ services = self.list_services() return _utils._filter_list(services, name_or_id, filters) @@ -10370,7 +10370,7 @@ def get_service(self, name_or_id, filters=None): - description: :raises: ``OpenStackCloudException`` if something goes wrong during the - openstack API call or if multiple matches are found. + OpenStack API call or if multiple matches are found. """ return _utils._get_entity(self, 'service', name_or_id, filters) @@ -10382,7 +10382,7 @@ def delete_service(self, name_or_id): :returns: True if delete succeeded, False otherwise. :raises: ``OpenStackCloudException`` if something goes wrong during - the openstack API call + the OpenStack API call """ service = self.get_service(name_or_id=name_or_id) if service is None: @@ -10422,7 +10422,7 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, :returns: a list of ``munch.Munch`` containing the endpoint description :raises: OpenStackCloudException if the service cannot be found or if - something goes wrong during the openstack API call. + something goes wrong during the OpenStack API call. """ public_url = kwargs.pop('public_url', None) internal_url = kwargs.pop('internal_url', None) @@ -10531,7 +10531,7 @@ def list_endpoints(self): :returns: a list of ``munch.Munch`` containing the endpoint description :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. + the OpenStack API call. """ # Force admin interface if v2.0 is in use v2 = self._is_client_version('identity', 2) @@ -10559,7 +10559,7 @@ def search_endpoints(self, id=None, filters=None): - admin_url: (optional) :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. + the OpenStack API call. """ # NOTE(SamYaple): With keystone v3 we can filter directly via the # the keystone api, but since the return of all the endpoints even in @@ -10593,7 +10593,7 @@ def delete_endpoint(self, id): :returns: True if delete succeeded, False otherwise. :raises: ``OpenStackCloudException`` if something goes wrong during - the openstack API call. + the OpenStack API call. """ endpoint = self.get_endpoint(id=id) if endpoint is None: @@ -10666,7 +10666,7 @@ def delete_domain(self, domain_id=None, name_or_id=None): :returns: True if delete succeeded, False otherwise. :raises: ``OpenStackCloudException`` if something goes wrong during - the openstack API call. + the OpenStack API call. """ if domain_id is None: if name_or_id is None: @@ -10694,7 +10694,7 @@ def list_domains(self, **filters): :returns: a list of ``munch.Munch`` containing the domain description. :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. + the OpenStack API call. """ data = self._identity_client.get( '/domains', params=filters, error_message="Failed to list domains") @@ -10715,7 +10715,7 @@ def search_domains(self, filters=None, name_or_id=None): - description: :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. + the OpenStack API call. """ if filters is None: filters = {} @@ -10741,7 +10741,7 @@ def get_domain(self, domain_id=None, name_or_id=None, filters=None): - description: :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. + the OpenStack API call. """ if domain_id is None: # NOTE(SamYaple): search_domains() has filters and name_or_id @@ -10769,7 +10769,7 @@ def list_groups(self, **kwargs): :returns: A list of ``munch.Munch`` containing the group description. :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. + the OpenStack API call. """ data = self._identity_client.get( '/groups', params=kwargs, error_message="Failed to list groups") @@ -10786,7 +10786,7 @@ def search_groups(self, name_or_id=None, filters=None, **kwargs): :returns: A list of ``munch.Munch`` containing the group description. :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. + the OpenStack API call. """ groups = self.list_groups(**kwargs) return _utils._filter_list(groups, name_or_id, filters) @@ -10802,7 +10802,7 @@ def get_group(self, name_or_id, filters=None, **kwargs): :returns: A ``munch.Munch`` containing the group description. :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. + the OpenStack API call. """ return _utils._get_entity(self, 'group', name_or_id, filters, **kwargs) @@ -10816,7 +10816,7 @@ def create_group(self, name, description, domain=None): :returns: A ``munch.Munch`` containing the group description. :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. + the OpenStack API call. """ group_ref = {'name': name} if description: @@ -10849,7 +10849,7 @@ def update_group(self, name_or_id, name=None, description=None, :returns: A ``munch.Munch`` containing the group description. :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. + the OpenStack API call. """ self.list_groups.invalidate(self) group = self.get_group(name_or_id, **kwargs) @@ -10882,7 +10882,7 @@ def delete_group(self, name_or_id, **kwargs): :returns: True if delete succeeded, False otherwise. :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. + the OpenStack API call. """ group = self.get_group(name_or_id, **kwargs) if group is None: @@ -10906,7 +10906,7 @@ def list_roles(self, **kwargs): :returns: a list of ``munch.Munch`` containing the role description. :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. + the OpenStack API call. """ v2 = self._is_client_version('identity', 2) url = '/OS-KSADM/roles' if v2 else '/roles' @@ -10930,7 +10930,7 @@ def search_roles(self, name_or_id=None, filters=None, **kwargs): - description: :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. + the OpenStack API call. """ roles = self.list_roles(**kwargs) return _utils._filter_list(roles, name_or_id, filters) @@ -10951,7 +10951,7 @@ def get_role(self, name_or_id, filters=None, **kwargs): - description: :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. + the OpenStack API call. """ return _utils._get_entity(self, 'role', name_or_id, filters, **kwargs) @@ -11036,7 +11036,7 @@ def list_role_assignments(self, filters=None): - project|domain: :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. + the OpenStack API call. """ # NOTE(samueldmq): although 'include_names' is a valid query parameter # in the keystone v3 list role assignments API, it would have NO effect @@ -11271,7 +11271,7 @@ def delete_role(self, name_or_id, **kwargs): :returns: True if delete succeeded, False otherwise. :raises: ``OpenStackCloudException`` if something goes wrong during - the openstack API call. + the OpenStack API call. """ role = self.get_role(name_or_id, **kwargs) if role is None: @@ -11501,7 +11501,7 @@ def search_aggregates(self, name_or_id=None, filters=None): :returns: a list of dicts containing the aggregates :raises: ``OpenStackCloudException``: if something goes wrong during - the openstack API call. + the OpenStack API call. """ aggregates = self.list_aggregates() return _utils._filter_list(aggregates, name_or_id, filters) @@ -12563,7 +12563,7 @@ def create_firewall_rule(self, **kwargs): :param bool shared: Visibility to other projects. Defaults to False. :param source_firewall_group_id: ID of source firewall group. - :param source_ip_address: IPv4-, IPv6 adress or CIDR. + :param source_ip_address: IPv4-, IPv6 address or CIDR. :param source_port: Port or port range (e.g. 80:90) :raises: BadRequestException if parameters are malformed :return: created firewall rule diff --git a/openstack/connection.py b/openstack/connection.py index 73631d06e..47f3de8ed 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -47,7 +47,7 @@ If the application in question is a command line application that should also accept command line arguments, an `argparse.Namespace` can be passed to :func:`openstack.connect` that will have relevant arguments added to it and -then subsequently consumed by the construtor: +then subsequently consumed by the constructor: .. code-block:: python @@ -271,7 +271,7 @@ def __init__(self, cloud=None, config=None, session=None, rate-limiting is performed. :param kwargs: If a config is not provided, the rest of the parameters provided are assumed to be arguments to be passed to the - CloudRegion contructor. + CloudRegion constructor. """ self.config = config self._extra_services = {} From 37a1decac136dff98149050e871dc7e159caa2e5 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 16 Nov 2018 10:20:56 +0100 Subject: [PATCH 2285/3836] implement block-storage backup resource implement Backup resource with respective functionality of the block-storage.v2 Change-Id: Ie8676bba91fd2236b7f04b3f4d0e72d79a3f3925 --- doc/source/user/proxies/block_storage.rst | 11 ++ .../user/resources/block_storage/index.rst | 1 + .../resources/block_storage/v2/backup.rst | 21 +++ openstack/block_storage/v2/_proxy.py | 103 +++++++++++++++ openstack/block_storage/v2/backup.py | 100 +++++++++++++++ .../block_storage/v2/test_backup.py | 68 ++++++++++ .../unit/block_storage/v2/test_backup.py | 121 ++++++++++++++++++ .../tests/unit/block_storage/v2/test_proxy.py | 72 +++++++++++ ...block-storage-backup-5886e91fd6e423bf.yaml | 3 + 9 files changed, 500 insertions(+) create mode 100644 doc/source/user/resources/block_storage/v2/backup.rst create mode 100644 openstack/block_storage/v2/backup.py create mode 100644 openstack/tests/functional/block_storage/v2/test_backup.py create mode 100644 openstack/tests/unit/block_storage/v2/test_backup.py create mode 100644 releasenotes/notes/block-storage-backup-5886e91fd6e423bf.yaml diff --git a/doc/source/user/proxies/block_storage.rst b/doc/source/user/proxies/block_storage.rst index cd4e79204..e8fb8fac4 100644 --- a/doc/source/user/proxies/block_storage.rst +++ b/doc/source/user/proxies/block_storage.rst @@ -22,6 +22,17 @@ Volume Operations .. automethod:: openstack.block_storage.v2._proxy.Proxy.get_volume .. automethod:: openstack.block_storage.v2._proxy.Proxy.volumes +Backup Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v2._proxy.Proxy + + .. automethod:: openstack.block_storage.v2._proxy.Proxy.create_backup + .. automethod:: openstack.block_storage.v2._proxy.Proxy.delete_backup + .. automethod:: openstack.block_storage.v2._proxy.Proxy.get_backup + .. automethod:: openstack.block_storage.v2._proxy.Proxy.backups + .. automethod:: openstack.block_storage.v2._proxy.Proxy.restore_backup + Type Operations ^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/block_storage/index.rst b/doc/source/user/resources/block_storage/index.rst index e4a249416..923162185 100644 --- a/doc/source/user/resources/block_storage/index.rst +++ b/doc/source/user/resources/block_storage/index.rst @@ -4,6 +4,7 @@ Block Storage Resources .. toctree:: :maxdepth: 1 + v2/backup v2/snapshot v2/type v2/volume diff --git a/doc/source/user/resources/block_storage/v2/backup.rst b/doc/source/user/resources/block_storage/v2/backup.rst new file mode 100644 index 000000000..5c56b480e --- /dev/null +++ b/doc/source/user/resources/block_storage/v2/backup.rst @@ -0,0 +1,21 @@ +openstack.block_storage.v2.backup +================================= + +.. automodule:: openstack.block_storage.v2.backup + +The Backup Class +---------------- + +The ``Backup`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v2.backup.Backup + :members: + +The BackupDetail Class +---------------------- + +The ``BackupDetail`` class inherits from +:class:`~openstack.block_storage.v2.backup.Backup`. + +.. autoclass:: openstack.block_storage.v2.backup.BackupDetail + :members: diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index ece8b2609..a6f26417b 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -10,10 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.block_storage.v2 import backup as _backup from openstack.block_storage.v2 import snapshot as _snapshot from openstack.block_storage.v2 import stats as _stats from openstack.block_storage.v2 import type as _type from openstack.block_storage.v2 import volume as _volume +from openstack import exceptions from openstack import proxy from openstack import resource @@ -209,6 +211,107 @@ def backend_pools(self): """ return self._list(_stats.Pools, paginated=False) + def backups(self, details=True, **query): + """Retrieve a generator of backups + + :param bool details: When set to ``False`` + :class:`~openstack.block_storage.v2.backup.Backup` objects + will be returned. The default, ``True``, will cause + :class:`~openstack.block_storage.v2.backup.BackupDetail` + objects to be returned. + :param dict query: Optional query parameters to be sent to limit the + resources being returned: + + * offset: pagination marker + * limit: pagination limit + * sort_key: Sorts by an attribute. A valid value is + name, status, container_format, disk_format, size, id, + created_at, or updated_at. Default is created_at. + The API uses the natural sorting direction of the + sort_key attribute value. + * sort_dir: Sorts by one or more sets of attribute and sort + direction combinations. If you omit the sort direction + in a set, default is desc. + + :returns: A generator of backup objects. + """ + if not self._connection.has_service('object-store'): + raise exceptions.SDKException( + 'Object-store service is required for block-store backups' + ) + backup = _backup.BackupDetail if details else _backup.Backup + return self._list(backup, paginated=True, **query) + + def get_backup(self, backup): + """Get a backup + + :param backup: The value can be the ID of a backup + or a :class:`~openstack.block_storage.v2.backup.Backup` + instance. + + :returns: Backup instance + :rtype: :class:`~openstack.block_storage.v2.backup.Backup` + """ + if not self._connection.has_service('object-store'): + raise exceptions.SDKException( + 'Object-store service is required for block-store backups' + ) + return self._get(_backup.Backup, backup) + + def create_backup(self, **attrs): + """Create a new Backup from attributes with native API + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.block_storage.v2.backup.Backup` + comprised of the properties on the Backup class. + + :returns: The results of Backup creation + :rtype: :class:`~openstack.block_storage.v2.backup.Backup` + """ + if not self._connection.has_service('object-store'): + raise exceptions.SDKException( + 'Object-store service is required for block-store backups' + ) + return self._create(_backup.Backup, **attrs) + + def delete_backup(self, backup, ignore_missing=True): + """Delete a CloudBackup + + :param backup: The value can be the ID of a backup or a + :class:`~openstack.block_storage.v2.backup.Backup` instance + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the zone does not exist. + When set to ``True``, no exception will be set when attempting to + delete a nonexistent zone. + + :returns: ``None`` + """ + if not self._connection.has_service('object-store'): + raise exceptions.SDKException( + 'Object-store service is required for block-store backups' + ) + self._delete(_backup.Backup, backup, + ignore_missing=ignore_missing) + + def restore_backup(self, backup, volume_id, name): + """Restore a Backup to volume + + :param backup: The value can be the ID of a backup or a + :class:`~openstack.block_storage.v2.backup.Backup` instance + :param volume_id: The ID of the volume to restore the backup to. + :param name: The name for new volume creation to restore. + + :returns: Updated backup instance + :rtype: :class:`~openstack.block_storage.v2.backup.Backup` + """ + if not self._connection.has_service('object-store'): + raise exceptions.SDKException( + 'Object-store service is required for block-store backups' + ) + backup = self._get_resource(_backup.Backup, backup) + return backup.restore(self, volume_id=volume_id, name=name) + def wait_for_status(self, res, status='ACTIVE', failures=None, interval=2, wait=120): """Wait for a resource to be in a particular status. diff --git a/openstack/block_storage/v2/backup.py b/openstack/block_storage/v2/backup.py new file mode 100644 index 000000000..8b3c1f82f --- /dev/null +++ b/openstack/block_storage/v2/backup.py @@ -0,0 +1,100 @@ +# 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 resource +from openstack import utils + + +class Backup(resource.Resource): + """Volume Backup""" + resource_key = "backup" + resources_key = "backups" + base_path = "/backups" + + _query_mapping = resource.QueryParameters( + 'all_tenants', 'limit', 'marker', + 'sort_key', 'sort_dir') + + # capabilities + allow_fetch = True + allow_create = True + allow_delete = True + allow_list = True + allow_get = True + + #: Properties + #: backup availability zone + availability_zone = resource.Body("availability_zone") + #: The container backup in + container = resource.Body("container") + #: The date and time when the resource was created. + created_at = resource.Body("created_at") + #: data timestamp + #: The time when the data on the volume was first saved. + #: If it is a backup from volume, it will be the same as created_at + #: for a backup. If it is a backup from a snapshot, + #: it will be the same as created_at for the snapshot. + data_timestamp = resource.Body('data_timestamp') + #: backup description + description = resource.Body("description") + #: Backup fail reason + fail_reason = resource.Body("fail_reason") + #: Force backup + force = resource.Body("force", type=bool) + #: has_dependent_backups + #: If this value is true, there are other backups depending on this backup. + has_dependent_backups = resource.Body('has_dependent_backups', type=bool) + #: Indicates whether the backup mode is incremental. + #: If this value is true, the backup mode is incremental. + #: If this value is false, the backup mode is full. + is_incremental = resource.Body("is_incremental", type=bool) + #: A list of links associated with this volume. *Type: list* + links = resource.Body("links", type=list) + #: backup name + name = resource.Body("name") + #: backup object count + object_count = resource.Body("object_count", type=int) + #: The size of the volume, in gibibytes (GiB). + size = resource.Body("size", type=int) + #: The UUID of the source volume snapshot. + snapshot_id = resource.Body("snapshot_id") + #: backup status + #: values: creating, available, deleting, error, restoring, error_restoring + status = resource.Body("status") + #: The date and time when the resource was updated. + updated_at = resource.Body("updated_at") + #: The UUID of the volume. + volume_id = resource.Body("volume_id") + + def restore(self, session, volume_id=None, name=None): + """Restore current backup to volume + + :param session: openstack session + :param volume_id: The ID of the volume to restore the backup to. + :param name: The name for new volume creation to restore. + :return: + """ + url = utils.urljoin(self.base_path, self.id, "restore") + body = {"restore": {"volume_id": volume_id, "name": name}} + response = session.post(url, + json=body) + self._translate_response(response) + return self + + +class BackupDetail(Backup): + """Volume Backup with Details""" + base_path = "/backups/detail" + + # capabilities + allow_list = True + + #: Properties diff --git a/openstack/tests/functional/block_storage/v2/test_backup.py b/openstack/tests/functional/block_storage/v2/test_backup.py new file mode 100644 index 000000000..08914673d --- /dev/null +++ b/openstack/tests/functional/block_storage/v2/test_backup.py @@ -0,0 +1,68 @@ +# 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.block_storage.v2 import volume as _volume +from openstack.block_storage.v2 import backup as _backup +from openstack.tests.functional import base + + +class TestBackup(base.BaseFunctionalTest): + + def setUp(self): + super(TestBackup, self).setUp() + + if not self.conn.has_service('object-store'): + self.skipTest('Object service is requred, but not available') + + self.VOLUME_NAME = self.getUniqueString() + self.VOLUME_ID = None + self.BACKUP_NAME = self.getUniqueString() + self.BACKUP_ID = None + + volume = self.conn.block_storage.create_volume( + name=self.VOLUME_NAME, + size=1) + self.conn.block_storage.wait_for_status( + volume, + status='available', + failures=['error'], + interval=5, + wait=300) + assert isinstance(volume, _volume.Volume) + self.VOLUME_ID = volume.id + + backup = self.conn.block_storage.create_backup( + name=self.BACKUP_NAME, + volume_id=volume.id) + self.conn.block_storage.wait_for_status( + backup, + status='available', + failures=['error'], + interval=5, + wait=300) + assert isinstance(backup, _backup.Backup) + self.assertEqual(self.BACKUP_NAME, backup.name) + self.BACKUP_ID = backup.id + + def tearDown(self): + sot = self.conn.block_storage.delete_backup( + self.BACKUP_ID, + ignore_missing=False) + sot = self.conn.block_storage.delete_volume( + self.VOLUME_ID, + ignore_missing=False) + self.assertIsNone(sot) + super(TestBackup, self).tearDown() + + def test_get(self): + sot = self.conn.block_storage.get_backup(self.BACKUP_ID) + self.assertEqual(self.BACKUP_NAME, sot.name) diff --git a/openstack/tests/unit/block_storage/v2/test_backup.py b/openstack/tests/unit/block_storage/v2/test_backup.py new file mode 100644 index 000000000..cc429d1b9 --- /dev/null +++ b/openstack/tests/unit/block_storage/v2/test_backup.py @@ -0,0 +1,121 @@ +# 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 import adapter + +from openstack.tests.unit import base + +from openstack.block_storage.v2 import backup + +FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff" + +BACKUP = { + "availability_zone": "az1", + "container": "volumebackups", + "created_at": "2018-04-02T10:35:27.000000", + "updated_at": "2018-04-03T10:35:27.000000", + "description": 'description', + "fail_reason": 'fail reason', + "id": FAKE_ID, + "name": "backup001", + "object_count": 22, + "size": 1, + "status": "available", + "volume_id": "e5185058-943a-4cb4-96d9-72c184c337d6", + "is_incremental": True, + "has_dependent_backups": False +} + +DETAILS = { +} + +BACKUP_DETAIL = copy.copy(BACKUP) +BACKUP_DETAIL.update(DETAILS) + + +class TestBackup(base.TestCase): + + def setUp(self): + super(TestBackup, self).setUp() + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.get = mock.Mock() + self.sess.default_microversion = mock.Mock(return_value='') + + def test_basic(self): + sot = backup.Backup(BACKUP) + self.assertEqual("backup", sot.resource_key) + self.assertEqual("backups", sot.resources_key) + self.assertEqual("/backups", sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_fetch) + + self.assertDictEqual( + { + "all_tenants": "all_tenants", + "limit": "limit", + "marker": "marker", + "sort_dir": "sort_dir", + "sort_key": "sort_key" + }, + sot._query_mapping._mapping + ) + + def test_create(self): + sot = backup.Backup(**BACKUP) + self.assertEqual(BACKUP["id"], sot.id) + self.assertEqual(BACKUP["name"], sot.name) + self.assertEqual(BACKUP["status"], sot.status) + self.assertEqual(BACKUP["container"], sot.container) + self.assertEqual(BACKUP["availability_zone"], sot.availability_zone) + self.assertEqual(BACKUP["created_at"], sot.created_at) + self.assertEqual(BACKUP["updated_at"], sot.updated_at) + self.assertEqual(BACKUP["description"], sot.description) + self.assertEqual(BACKUP["fail_reason"], sot.fail_reason) + self.assertEqual(BACKUP["volume_id"], sot.volume_id) + self.assertEqual(BACKUP["object_count"], sot.object_count) + self.assertEqual(BACKUP["is_incremental"], sot.is_incremental) + self.assertEqual(BACKUP["size"], sot.size) + self.assertEqual(BACKUP["has_dependent_backups"], + sot.has_dependent_backups) + + +class TestBackupDetail(base.TestCase): + + def test_basic(self): + sot = backup.BackupDetail(BACKUP_DETAIL) + self.assertIsInstance(sot, backup.Backup) + self.assertEqual("/backups/detail", sot.base_path) + + def test_create(self): + sot = backup.Backup(**BACKUP_DETAIL) + self.assertEqual(BACKUP_DETAIL["id"], sot.id) + self.assertEqual(BACKUP_DETAIL["name"], sot.name) + self.assertEqual(BACKUP_DETAIL["status"], sot.status) + self.assertEqual(BACKUP_DETAIL["container"], sot.container) + self.assertEqual(BACKUP_DETAIL["availability_zone"], + sot.availability_zone) + self.assertEqual(BACKUP_DETAIL["created_at"], sot.created_at) + self.assertEqual(BACKUP_DETAIL["updated_at"], sot.updated_at) + self.assertEqual(BACKUP_DETAIL["description"], sot.description) + self.assertEqual(BACKUP_DETAIL["fail_reason"], sot.fail_reason) + self.assertEqual(BACKUP_DETAIL["volume_id"], sot.volume_id) + self.assertEqual(BACKUP_DETAIL["object_count"], sot.object_count) + self.assertEqual(BACKUP_DETAIL["is_incremental"], sot.is_incremental) + self.assertEqual(BACKUP_DETAIL["size"], sot.size) + self.assertEqual(BACKUP_DETAIL["has_dependent_backups"], + sot.has_dependent_backups) diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index 53a46773e..a9af7afea 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -9,8 +9,12 @@ # 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 openstack import exceptions from openstack.block_storage.v2 import _proxy +from openstack.block_storage.v2 import backup from openstack.block_storage.v2 import snapshot from openstack.block_storage.v2 import stats from openstack.block_storage.v2 import type @@ -97,3 +101,71 @@ def test_volume_extend(self): def test_backend_pools(self): self.verify_list(self.proxy.backend_pools, stats.Pools, paginated=False) + + def test_backups_detailed(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self.verify_list(self.proxy.backups, backup.BackupDetail, + paginated=True, + method_kwargs={"details": True, "query": 1}, + expected_kwargs={"query": 1}) + + def test_backups_not_detailed(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self.verify_list(self.proxy.backups, backup.Backup, + paginated=True, + method_kwargs={"details": False, "query": 1}, + expected_kwargs={"query": 1}) + + def test_backup_get(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self.verify_get(self.proxy.get_backup, backup.Backup) + + def test_backup_delete(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self.verify_delete(self.proxy.delete_backup, backup.Backup, False) + + def test_backup_delete_ignore(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self.verify_delete(self.proxy.delete_backup, backup.Backup, True) + + def test_backup_create_attrs(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self.verify_create(self.proxy.create_backup, backup.Backup) + + def test_backup_restore(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self._verify2( + 'openstack.block_storage.v2.backup.Backup.restore', + self.proxy.restore_backup, + method_args=['volume_id'], + method_kwargs={'volume_id': 'vol_id', 'name': 'name'}, + expected_args=[self.proxy], + expected_kwargs={'volume_id': 'vol_id', 'name': 'name'} + ) + + def test_backup_no_swift(self): + """Ensure proxy method raises exception if swift is not available + """ + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=False) + self.assertRaises( + exceptions.SDKException, + self.proxy.restore_backup, + 'backup', + 'volume_id', + 'name') diff --git a/releasenotes/notes/block-storage-backup-5886e91fd6e423bf.yaml b/releasenotes/notes/block-storage-backup-5886e91fd6e423bf.yaml new file mode 100644 index 000000000..d6f82506b --- /dev/null +++ b/releasenotes/notes/block-storage-backup-5886e91fd6e423bf.yaml @@ -0,0 +1,3 @@ +--- +features: + - Implement block-storage.v2 Backup resource with restore functionality. From 40322bac041a471e2b775e494f1042fd1fbc777a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 16 Nov 2018 04:18:32 -0600 Subject: [PATCH 2286/3836] Register proxy directly in add_service When adding an unknown service to Connection, the existing code was adding the service's ServiceDescription. Unfortunately, adding a Descriptor at runtime doesn't actually work and the added instance winds up being unusable. As add_service is an active call, we can assume that the person calling it actually wants to use the service, so we don't need to hid it behind an on-demand descriptor. Run _make_proxy directly and attach the resulting proxy to the Connection. Change-Id: I02aa1ebbd11bea33712a3cb82600abc1aed2a65a --- openstack/connection.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openstack/connection.py b/openstack/connection.py index 73631d06e..e8fdc9e4d 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -344,10 +344,11 @@ class contained in # we get an adapter. if isinstance(service, six.string_types): service = service_description.ServiceDescription(service) + service_proxy = service._make_proxy(self) # Register the proxy class with every known alias for attr_name in service.all_types: - setattr(self, attr_name.replace('-', '_'), service) + setattr(self, attr_name.replace('-', '_'), service_proxy) def authorize(self): """Authorize this Connection From 4d48309361c3247e491b86e622cb4d1ed03db297 Mon Sep 17 00:00:00 2001 From: zhufl Date: Mon, 19 Nov 2018 15:38:58 +0800 Subject: [PATCH 2287/3836] Add missing seperator between words This is to add missing seperator between words, usually in log messages. Change-Id: I7ca516a0a38f66489284452810977e49338832f5 --- openstack/cloud/openstackcloud.py | 2 +- openstack/config/cloud_region.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index dc271e1fb..3e94b49aa 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -9730,7 +9730,7 @@ def register_machine(self, nics, wait=False, timeout=3600, try: port_url = '/ports/{uuid}'.format(uuid=uuid) # NOTE(TheJulia): Added in hope that it is logged. - port_msg = ('Failed to delete port {port} for node' + port_msg = ('Failed to delete port {port} for node ' '{node}').format(port=uuid, node=machine['uuid']) self._baremetal_client.delete(port_url, diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index e12b91628..e96cabe32 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -595,7 +595,7 @@ def get_session_endpoint( if not endpoint: self.log.warning( "Keystone catalog entry not found (" - "service_type=%s,service_name=%s" + "service_type=%s,service_name=%s," "interface=%s,region_name=%s)", service_type, service_name, From 5df573b6c2910a56af4a80704e41a87e2bc7dca6 Mon Sep 17 00:00:00 2001 From: wangxiyuan Date: Thu, 1 Nov 2018 16:14:52 +0800 Subject: [PATCH 2288/3836] Add missing properties for role * Domain_id is the base property for role. * Description was added to role recently[1]. Add the support to openstacksdk. [1]: I230af9cc833af13064636b5d9a7ce6334c3f6e9a Change-Id: I700413c7f166c57af75a69ad65947b396ae290f9 --- openstack/identity/v3/role.py | 4 ++++ openstack/tests/unit/identity/v3/test_role.py | 4 ++++ .../notes/update-role-property-b16e902e913c7b25.yaml | 5 +++++ 3 files changed, 13 insertions(+) create mode 100644 releasenotes/notes/update-role-property-b16e902e913c7b25.yaml diff --git a/openstack/identity/v3/role.py b/openstack/identity/v3/role.py index cb3518bbe..76bda233e 100644 --- a/openstack/identity/v3/role.py +++ b/openstack/identity/v3/role.py @@ -32,5 +32,9 @@ class Role(resource.Resource): # Properties #: Unique role name, within the owning domain. *Type: string* name = resource.Body('name') + #: User-facing description of the role. *Type: string* + description = resource.Body('description') + #: References the domain ID which owns the role. *Type: string* + domain_id = resource.Body('domain_id') #: The links for the service resource. links = resource.Body('links') diff --git a/openstack/tests/unit/identity/v3/test_role.py b/openstack/tests/unit/identity/v3/test_role.py index 4a67d6dc4..d45f646e1 100644 --- a/openstack/tests/unit/identity/v3/test_role.py +++ b/openstack/tests/unit/identity/v3/test_role.py @@ -19,6 +19,8 @@ 'id': IDENTIFIER, 'links': {'self': 'http://example.com/user1'}, 'name': '2', + 'description': 'test description for role', + 'domain_id': 'default' } @@ -50,3 +52,5 @@ def test_make_it(self): self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['links'], sot.links) self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['domain_id'], sot.domain_id) diff --git a/releasenotes/notes/update-role-property-b16e902e913c7b25.yaml b/releasenotes/notes/update-role-property-b16e902e913c7b25.yaml new file mode 100644 index 000000000..c35cec03f --- /dev/null +++ b/releasenotes/notes/update-role-property-b16e902e913c7b25.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added the newly supported ``description`` parameter and the missing + ``domain_id`` parameter to ``Role`` resource. From 5c5478d774b8b6abb196d3ed27331a07efb98c88 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Thu, 22 Nov 2018 15:00:45 +0100 Subject: [PATCH 2289/3836] Change approach to detailed listings of baremetal resources The current approach with separate ResourceDetail classes suffers from serious flows and is barely usable. For example for nodes: * get_node() returns complete Node objects * nodes() returns incomplete Node objects * nodes(details=True) returns NodeDetail objects, which are exactly like complete Node objects, except that you cannot do anything with them: updating, listing VIFs, etc. This is confusing and essentially requires the caller to issue a redundant get_node call after listing detailed nodes. This change consolidates everything into Node (Port, etc) objects and deprecates NodeDetail (PortDetail, etc). I would highly recommend the same approach taken for other ResourceDetail objects. Change-Id: Idedee2b394e2ed412a69880d4bc9315737e1c3ed --- openstack/baremetal/v1/_common.py | 29 ++++++++ openstack/baremetal/v1/_proxy.py | 14 ++-- openstack/baremetal/v1/chassis.py | 17 +---- openstack/baremetal/v1/node.py | 23 +----- openstack/baremetal/v1/port.py | 23 +----- openstack/baremetal/v1/port_group.py | 20 +---- openstack/resource.py | 11 ++- .../baremetal/test_baremetal_node.py | 14 ++++ .../baremetal/test_baremetal_port.py | 11 +++ .../baremetal/test_baremetal_port_group.py | 11 +++ .../tests/unit/baremetal/v1/test_chassis.py | 24 ------ .../tests/unit/baremetal/v1/test_node.py | 52 ------------- .../tests/unit/baremetal/v1/test_port.py | 29 -------- .../unit/baremetal/v1/test_port_group.py | 29 -------- .../tests/unit/baremetal/v1/test_proxy.py | 73 +++++++++++-------- .../baremetal-details-09b27fba82111cfb.yaml | 12 +++ 16 files changed, 145 insertions(+), 247 deletions(-) create mode 100644 releasenotes/notes/baremetal-details-09b27fba82111cfb.yaml diff --git a/openstack/baremetal/v1/_common.py b/openstack/baremetal/v1/_common.py index 4be2bd47e..874a0752e 100644 --- a/openstack/baremetal/v1/_common.py +++ b/openstack/baremetal/v1/_common.py @@ -55,3 +55,32 @@ VIF_VERSION = '1.28' """API version in which the VIF operations were introduced.""" + + +class ListMixin(object): + + @classmethod + def list(cls, session, details=False, **params): + """This method is a generator which yields resource objects. + + This resource object list generator handles pagination and takes query + params for response filtering. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param bool details: Whether to return detailed node records + :param dict params: These keyword arguments are passed through the + :meth:`~openstack.resource.QueryParameter._transpose` method + to find if any of them match expected query parameters to be + sent in the *params* argument to + :meth:`~keystoneauth1.adapter.Adapter.get`. + + :return: A generator of :class:`openstack.resource.Resource` objects. + :raises: :exc:`~openstack.exceptions.InvalidResourceQuery` if query + contains invalid params. + """ + base_path = cls.base_path + if details: + base_path += '/detail' + return super(ListMixin, cls).list(session, paginated=True, + base_path=base_path, **params) diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 2b5d970af..7d395edd3 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -61,8 +61,7 @@ def chassis(self, details=False, **query): :returns: A generator of chassis instances. """ - cls = _chassis.ChassisDetail if details else _chassis.Chassis - return self._list(cls, paginated=True, **query) + return _chassis.Chassis.list(self, details=details, **query) def create_chassis(self, **attrs): """Create a new chassis from attributes. @@ -198,10 +197,9 @@ def nodes(self, details=False, **query): direction of the server attribute that is provided as the ``sort_key``. - :returns: A generator of node instances. + :returns: A generator of :class:`~openstack.baremetal.v1.node.Node` """ - cls = _node.NodeDetail if details else _node.Node - return self._list(cls, paginated=True, **query) + return _node.Node.list(self, details=details, **query) def create_node(self, **attrs): """Create a new node from attributes. @@ -442,8 +440,7 @@ def ports(self, details=False, **query): :returns: A generator of port instances. """ - cls = _port.PortDetail if details else _port.Port - return self._list(cls, paginated=True, **query) + return _port.Port.list(self, details=details, **query) def create_port(self, **attrs): """Create a new port from attributes. @@ -555,8 +552,7 @@ def port_groups(self, details=False, **query): :returns: A generator of port group instances. """ - cls = _portgroup.PortGroupDetail if details else _portgroup.PortGroup - return self._list(cls, paginated=True, **query) + return _portgroup.PortGroup.list(self, details=details, **query) def create_port_group(self, **attrs): """Create a new portgroup from attributes. diff --git a/openstack/baremetal/v1/chassis.py b/openstack/baremetal/v1/chassis.py index eb9842f5b..2d5b2e28c 100644 --- a/openstack/baremetal/v1/chassis.py +++ b/openstack/baremetal/v1/chassis.py @@ -10,10 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.baremetal.v1 import _common from openstack import resource -class Chassis(resource.Resource): +class Chassis(_common.ListMixin, resource.Resource): resources_key = 'chassis' base_path = '/chassis' @@ -47,16 +48,4 @@ class Chassis(resource.Resource): updated_at = resource.Body('updated_at') -class ChassisDetail(Chassis): - - base_path = '/chassis/detail' - - # capabilities - allow_create = False - allow_fetch = False - allow_commit = False - allow_delete = False - allow_list = True - - #: The UUID for the chassis - id = resource.Body('uuid', alternate_id=True) +ChassisDetail = Chassis diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 4b6a655ae..ee374d2c1 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -34,7 +34,7 @@ def __init__(self, result, reason): self.reason = reason -class Node(resource.Resource): +class Node(_common.ListMixin, resource.Resource): resources_key = 'nodes' base_path = '/nodes' @@ -589,23 +589,4 @@ def validate(self, session, required=('boot', 'deploy', 'power')): for key, value in result.items()} -class NodeDetail(Node): - - base_path = '/nodes/detail' - - # capabilities - allow_create = False - allow_fetch = False - allow_commit = False - allow_delete = False - allow_list = True - - _query_mapping = resource.QueryParameters( - 'associated', 'conductor_group', 'driver', 'fault', - 'provision_state', 'resource_class', - instance_id='instance_uuid', - is_maintenance='maintenance', - ) - - #: The UUID of the node resource. - id = resource.Body("uuid", alternate_id=True) +NodeDetail = Node diff --git a/openstack/baremetal/v1/port.py b/openstack/baremetal/v1/port.py index c05f1f7a5..07dc2cbee 100644 --- a/openstack/baremetal/v1/port.py +++ b/openstack/baremetal/v1/port.py @@ -10,10 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.baremetal.v1 import _common from openstack import resource -class Port(resource.Resource): +class Port(_common.ListMixin, resource.Resource): resources_key = 'ports' base_path = '/ports' @@ -29,6 +30,7 @@ class Port(resource.Resource): _query_mapping = resource.QueryParameters( 'address', 'fields', 'node', 'portgroup', + node_id='node_uuid', ) # The physical_network field introduced in 1.34 @@ -67,21 +69,4 @@ class Port(resource.Resource): updated_at = resource.Body('updated_at') -class PortDetail(Port): - - base_path = '/ports/detail' - - # capabilities - allow_create = False - allow_fetch = False - allow_commit = False - allow_delete = False - allow_list = True - - _query_mapping = resource.QueryParameters( - 'address', 'fields', 'node', 'portgroup', - node_id='node_uuid', - ) - - #: The UUID of the port - id = resource.Body('uuid', alternate_id=True) +PortDetail = Port diff --git a/openstack/baremetal/v1/port_group.py b/openstack/baremetal/v1/port_group.py index 6e587fe2d..32d70ea5b 100644 --- a/openstack/baremetal/v1/port_group.py +++ b/openstack/baremetal/v1/port_group.py @@ -10,10 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.baremetal.v1 import _common from openstack import resource -class PortGroup(resource.Resource): +class PortGroup(_common.ListMixin, resource.Resource): resources_key = 'portgroups' base_path = '/portgroups' @@ -66,19 +67,4 @@ class PortGroup(resource.Resource): updated_at = resource.Body('updated_at') -class PortGroupDetail(PortGroup): - - base_path = '/portgroups/detail' - - allow_create = False - allow_fetch = False - allow_commit = False - allow_delete = False - allow_list = True - - _query_mapping = resource.QueryParameters( - 'node', 'address', - ) - - #: The UUID for the portgroup - id = resource.Body('uuid', alternate_id=True) +PortGroupDetail = PortGroup diff --git a/openstack/resource.py b/openstack/resource.py index dd645679a..ab57ed161 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1206,7 +1206,7 @@ def delete(self, session, error_message=None): return self @classmethod - def list(cls, session, paginated=False, **params): + def list(cls, session, paginated=False, base_path=None, **params): """This method is a generator which yields resource objects. This resource object list generator handles pagination and takes query @@ -1220,6 +1220,9 @@ def list(cls, session, paginated=False, **params): **When paginated is False only one page of data will be returned regardless of the API's support of pagination.** + :param str base_path: Base part of the URI for listing resources, if + different from + :data:`~openstack.resource.Resource.base_path`. :param dict params: These keyword arguments are passed through the :meth:`~openstack.resource.QueryParamter._transpose` method to find if any of them match expected query parameters to be @@ -1241,9 +1244,11 @@ def list(cls, session, paginated=False, **params): session = cls._get_session(session) microversion = cls._get_microversion_for_list(session) - cls._query_mapping._validate(params, base_path=cls.base_path) + if base_path is None: + base_path = cls.base_path + cls._query_mapping._validate(params, base_path=base_path) query_params = cls._query_mapping._transpose(params) - uri = cls.base_path % params + uri = base_path % params limit = query_params.get('limit') diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index 60910951c..b76d20490 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -86,6 +86,20 @@ def test_node_update_by_name(self): node = self.conn.baremetal.get_node('node-name') self.assertIsNone(node.instance_id) + def test_node_list_update_delete(self): + self.create_node(name='node-name', extra={'foo': 'bar'}) + node = next(n for n in + self.conn.baremetal.nodes(details=True, + provision_state='available', + is_maintenance=False, + associated=False) + if n.name == 'node-name') + self.assertEqual(node.extra, {'foo': 'bar'}) + + # This test checks that resources returned from listing are usable + self.conn.baremetal.update_node(node, extra={'foo': 42}) + self.conn.baremetal.delete_node(node, ignore_missing=False) + def test_node_create_in_enroll_provide(self): node = self.create_node(provision_state='enroll') self.node_id = node.id diff --git a/openstack/tests/functional/baremetal/test_baremetal_port.py b/openstack/tests/functional/baremetal/test_baremetal_port.py index f7abde8b6..ce95fa1d7 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_port.py +++ b/openstack/tests/functional/baremetal/test_baremetal_port.py @@ -53,6 +53,17 @@ def test_port_list(self): ports = self.conn.baremetal.ports(node='test-node') self.assertEqual([p.id for p in ports], [port1.id]) + def test_port_list_update_delete(self): + self.create_port(address='11:22:33:44:55:66', node_id=self.node.id, + extra={'foo': 'bar'}) + port = next(self.conn.baremetal.ports(details=True, + address='11:22:33:44:55:66')) + self.assertEqual(port.extra, {'foo': 'bar'}) + + # This test checks that resources returned from listing are usable + self.conn.baremetal.update_port(port, extra={'foo': 42}) + self.conn.baremetal.delete_port(port, ignore_missing=False) + def test_port_update(self): port = self.create_port(address='11:22:33:44:55:66') port.address = '66:55:44:33:22:11' diff --git a/openstack/tests/functional/baremetal/test_baremetal_port_group.py b/openstack/tests/functional/baremetal/test_baremetal_port_group.py index 71f2f0b14..08afccaf4 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_port_group.py +++ b/openstack/tests/functional/baremetal/test_baremetal_port_group.py @@ -51,6 +51,17 @@ def test_port_list(self): pgs = self.conn.baremetal.port_groups(node='test-node') self.assertEqual([p.id for p in pgs], [pg1.id]) + def test_port_list_update_delete(self): + self.create_port_group(address='11:22:33:44:55:66', + extra={'foo': 'bar'}) + port_group = next(self.conn.baremetal.port_groups( + details=True, address='11:22:33:44:55:66')) + self.assertEqual(port_group.extra, {'foo': 'bar'}) + + # This test checks that resources returned from listing are usable + self.conn.baremetal.update_port_group(port_group, extra={'foo': 42}) + self.conn.baremetal.delete_port_group(port_group, ignore_missing=False) + def test_port_group_update(self): port_group = self.create_port_group() port_group.extra = {'answer': 42} diff --git a/openstack/tests/unit/baremetal/v1/test_chassis.py b/openstack/tests/unit/baremetal/v1/test_chassis.py index 4c52f01de..ca31a07b5 100644 --- a/openstack/tests/unit/baremetal/v1/test_chassis.py +++ b/openstack/tests/unit/baremetal/v1/test_chassis.py @@ -66,27 +66,3 @@ def test_instantiate(self): self.assertEqual(FAKE['links'], sot.links) self.assertEqual(FAKE['nodes'], sot.nodes) self.assertEqual(FAKE['updated_at'], sot.updated_at) - - -class TestChassisDetail(base.TestCase): - - def test_basic(self): - sot = chassis.ChassisDetail() - self.assertIsNone(sot.resource_key) - self.assertEqual('chassis', sot.resources_key) - self.assertEqual('/chassis/detail', sot.base_path) - self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_fetch) - self.assertFalse(sot.allow_commit) - self.assertFalse(sot.allow_delete) - self.assertTrue(sot.allow_list) - - def test_instantiate(self): - sot = chassis.ChassisDetail(**FAKE) - self.assertEqual(FAKE['uuid'], sot.id) - self.assertEqual(FAKE['created_at'], sot.created_at) - self.assertEqual(FAKE['description'], sot.description) - self.assertEqual(FAKE['extra'], sot.extra) - self.assertEqual(FAKE['links'], sot.links) - self.assertEqual(FAKE['nodes'], sot.nodes) - self.assertEqual(FAKE['updated_at'], sot.updated_at) diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 03cfd5ad1..71aed2e08 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -153,58 +153,6 @@ def test_normalize_provision_state(self): self.assertEqual('available', sot.provision_state) -class TestNodeDetail(base.TestCase): - - def test_basic(self): - sot = node.NodeDetail() - self.assertIsNone(sot.resource_key) - self.assertEqual('nodes', sot.resources_key) - self.assertEqual('/nodes/detail', sot.base_path) - self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_fetch) - self.assertFalse(sot.allow_commit) - self.assertFalse(sot.allow_delete) - self.assertTrue(sot.allow_list) - - def test_instantiate(self): - sot = node.NodeDetail(**FAKE) - - self.assertEqual(FAKE['uuid'], sot.id) - self.assertEqual(FAKE['name'], sot.name) - - self.assertEqual(FAKE['chassis_uuid'], sot.chassis_id) - self.assertEqual(FAKE['clean_step'], sot.clean_step) - self.assertEqual(FAKE['created_at'], sot.created_at) - self.assertEqual(FAKE['driver'], sot.driver) - self.assertEqual(FAKE['driver_info'], sot.driver_info) - self.assertEqual(FAKE['driver_internal_info'], - sot.driver_internal_info) - self.assertEqual(FAKE['extra'], sot.extra) - self.assertEqual(FAKE['instance_info'], sot.instance_info) - self.assertEqual(FAKE['instance_uuid'], sot.instance_id) - self.assertEqual(FAKE['console_enabled'], sot.is_console_enabled) - self.assertEqual(FAKE['maintenance'], sot.is_maintenance) - self.assertEqual(FAKE['last_error'], sot.last_error) - self.assertEqual(FAKE['links'], sot.links) - self.assertEqual(FAKE['maintenance_reason'], sot.maintenance_reason) - self.assertEqual(FAKE['name'], sot.name) - self.assertEqual(FAKE['network_interface'], sot.network_interface) - self.assertEqual(FAKE['ports'], sot.ports) - self.assertEqual(FAKE['portgroups'], sot.port_groups) - self.assertEqual(FAKE['power_state'], sot.power_state) - self.assertEqual(FAKE['properties'], sot.properties) - self.assertEqual(FAKE['provision_state'], sot.provision_state) - self.assertEqual(FAKE['raid_config'], sot.raid_config) - self.assertEqual(FAKE['reservation'], sot.reservation) - self.assertEqual(FAKE['resource_class'], sot.resource_class) - self.assertEqual(FAKE['states'], sot.states) - self.assertEqual(FAKE['target_provision_state'], - sot.target_provision_state) - self.assertEqual(FAKE['target_power_state'], sot.target_power_state) - self.assertEqual(FAKE['target_raid_config'], sot.target_raid_config) - self.assertEqual(FAKE['updated_at'], sot.updated_at) - - @mock.patch('time.sleep', lambda _t: None) @mock.patch.object(node.Node, 'fetch', autospec=True) class TestNodeWaitForProvisionState(base.TestCase): diff --git a/openstack/tests/unit/baremetal/v1/test_port.py b/openstack/tests/unit/baremetal/v1/test_port.py index 0ab7eae1a..c5d80304a 100644 --- a/openstack/tests/unit/baremetal/v1/test_port.py +++ b/openstack/tests/unit/baremetal/v1/test_port.py @@ -70,32 +70,3 @@ def test_instantiate(self): self.assertEqual(FAKE['portgroup_uuid'], sot.port_group_id) self.assertEqual(FAKE['pxe_enabled'], sot.is_pxe_enabled) self.assertEqual(FAKE['updated_at'], sot.updated_at) - - -class TestPortDetail(base.TestCase): - - def test_basic(self): - sot = port.PortDetail() - self.assertIsNone(sot.resource_key) - self.assertEqual('ports', sot.resources_key) - self.assertEqual('/ports/detail', sot.base_path) - self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_fetch) - self.assertFalse(sot.allow_commit) - self.assertFalse(sot.allow_delete) - self.assertTrue(sot.allow_list) - - def test_instantiate(self): - sot = port.PortDetail(**FAKE) - self.assertEqual(FAKE['uuid'], sot.id) - self.assertEqual(FAKE['address'], sot.address) - self.assertEqual(FAKE['created_at'], sot.created_at) - self.assertEqual(FAKE['extra'], sot.extra) - self.assertEqual(FAKE['internal_info'], sot.internal_info) - self.assertEqual(FAKE['links'], sot.links) - self.assertEqual(FAKE['local_link_connection'], - sot.local_link_connection) - self.assertEqual(FAKE['node_uuid'], sot.node_id) - self.assertEqual(FAKE['portgroup_uuid'], sot.port_group_id) - self.assertEqual(FAKE['pxe_enabled'], sot.is_pxe_enabled) - self.assertEqual(FAKE['updated_at'], sot.updated_at) diff --git a/openstack/tests/unit/baremetal/v1/test_port_group.py b/openstack/tests/unit/baremetal/v1/test_port_group.py index 81fb69a60..6b57faa25 100644 --- a/openstack/tests/unit/baremetal/v1/test_port_group.py +++ b/openstack/tests/unit/baremetal/v1/test_port_group.py @@ -75,32 +75,3 @@ def test_instantiate(self): self.assertEqual(FAKE['standalone_ports_supported'], sot.is_standalone_ports_supported) self.assertEqual(FAKE['updated_at'], sot.updated_at) - - -class TestPortGroupDetail(base.TestCase): - - def test_basic(self): - sot = port_group.PortGroupDetail() - self.assertIsNone(sot.resource_key) - self.assertEqual('portgroups', sot.resources_key) - self.assertEqual('/portgroups/detail', sot.base_path) - self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_fetch) - self.assertFalse(sot.allow_commit) - self.assertFalse(sot.allow_delete) - self.assertTrue(sot.allow_list) - - def test_instantiate(self): - sot = port_group.PortGroupDetail(**FAKE) - self.assertEqual(FAKE['uuid'], sot.id) - self.assertEqual(FAKE['address'], sot.address) - self.assertEqual(FAKE['created_at'], sot.created_at) - self.assertEqual(FAKE['extra'], sot.extra) - self.assertEqual(FAKE['internal_info'], sot.internal_info) - self.assertEqual(FAKE['links'], sot.links) - self.assertEqual(FAKE['name'], sot.name) - self.assertEqual(FAKE['node_uuid'], sot.node_id) - self.assertEqual(FAKE['ports'], sot.ports) - self.assertEqual(FAKE['standalone_ports_supported'], - sot.is_standalone_ports_supported) - self.assertEqual(FAKE['updated_at'], sot.updated_at) diff --git a/openstack/tests/unit/baremetal/v1/test_proxy.py b/openstack/tests/unit/baremetal/v1/test_proxy.py index e52791fcc..45a685f74 100644 --- a/openstack/tests/unit/baremetal/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal/v1/test_proxy.py @@ -17,6 +17,7 @@ from openstack.baremetal.v1 import driver from openstack.baremetal.v1 import node from openstack.baremetal.v1 import port +from openstack.baremetal.v1 import port_group from openstack import exceptions from openstack.tests.unit import base from openstack.tests.unit import test_proxy_base @@ -34,17 +35,17 @@ def test_drivers(self): def test_get_driver(self): self.verify_get(self.proxy.get_driver, driver.Driver) - def test_chassis_detailed(self): - self.verify_list(self.proxy.chassis, chassis.ChassisDetail, - paginated=True, - method_kwargs={"details": True, "query": 1}, - expected_kwargs={"query": 1}) + @mock.patch.object(chassis.Chassis, 'list') + def test_chassis_detailed(self, mock_list): + result = self.proxy.chassis(details=True, query=1) + self.assertIs(result, mock_list.return_value) + mock_list.assert_called_once_with(self.proxy, details=True, query=1) - def test_chassis_not_detailed(self): - self.verify_list(self.proxy.chassis, chassis.Chassis, - paginated=True, - method_kwargs={"details": False, "query": 1}, - expected_kwargs={"query": 1}) + @mock.patch.object(chassis.Chassis, 'list') + def test_chassis_not_detailed(self, mock_list): + result = self.proxy.chassis(query=1) + self.assertIs(result, mock_list.return_value) + mock_list.assert_called_once_with(self.proxy, details=False, query=1) def test_create_chassis(self): self.verify_create(self.proxy.create_chassis, chassis.Chassis) @@ -64,17 +65,17 @@ def test_delete_chassis(self): def test_delete_chassis_ignore(self): self.verify_delete(self.proxy.delete_chassis, chassis.Chassis, True) - def test_nodes_detailed(self): - self.verify_list(self.proxy.nodes, node.NodeDetail, - paginated=True, - method_kwargs={"details": True, "query": 1}, - expected_kwargs={"query": 1}) + @mock.patch.object(node.Node, 'list') + def test_nodes_detailed(self, mock_list): + result = self.proxy.nodes(details=True, query=1) + self.assertIs(result, mock_list.return_value) + mock_list.assert_called_once_with(self.proxy, details=True, query=1) - def test_nodes_not_detailed(self): - self.verify_list(self.proxy.nodes, node.Node, - paginated=True, - method_kwargs={"details": False, "query": 1}, - expected_kwargs={"query": 1}) + @mock.patch.object(node.Node, 'list') + def test_nodes_not_detailed(self, mock_list): + result = self.proxy.nodes(query=1) + self.assertIs(result, mock_list.return_value) + mock_list.assert_called_once_with(self.proxy, details=False, query=1) def test_create_node(self): self.verify_create(self.proxy.create_node, node.Node) @@ -106,17 +107,17 @@ def test_delete_node(self): def test_delete_node_ignore(self): self.verify_delete(self.proxy.delete_node, node.Node, True) - def test_ports_detailed(self): - self.verify_list(self.proxy.ports, port.PortDetail, - paginated=True, - method_kwargs={"details": True, "query": 1}, - expected_kwargs={"query": 1}) + @mock.patch.object(port.Port, 'list') + def test_ports_detailed(self, mock_list): + result = self.proxy.ports(details=True, query=1) + self.assertIs(result, mock_list.return_value) + mock_list.assert_called_once_with(self.proxy, details=True, query=1) - def test_ports_not_detailed(self): - self.verify_list(self.proxy.ports, port.Port, - paginated=True, - method_kwargs={"details": False, "query": 1}, - expected_kwargs={"query": 1}) + @mock.patch.object(port.Port, 'list') + def test_ports_not_detailed(self, mock_list): + result = self.proxy.ports(query=1) + self.assertIs(result, mock_list.return_value) + mock_list.assert_called_once_with(self.proxy, details=False, query=1) def test_create_port(self): self.verify_create(self.proxy.create_port, port.Port) @@ -136,6 +137,18 @@ def test_delete_port(self): def test_delete_port_ignore(self): self.verify_delete(self.proxy.delete_port, port.Port, True) + @mock.patch.object(port_group.PortGroup, 'list') + def test_port_groups_detailed(self, mock_list): + result = self.proxy.port_groups(details=True, query=1) + self.assertIs(result, mock_list.return_value) + mock_list.assert_called_once_with(self.proxy, details=True, query=1) + + @mock.patch.object(port_group.PortGroup, 'list') + def test_port_groups_not_detailed(self, mock_list): + result = self.proxy.port_groups(query=1) + self.assertIs(result, mock_list.return_value) + mock_list.assert_called_once_with(self.proxy, details=False, query=1) + @mock.patch('time.sleep', lambda _sec: None) @mock.patch.object(_proxy.Proxy, 'get_node', autospec=True) diff --git a/releasenotes/notes/baremetal-details-09b27fba82111cfb.yaml b/releasenotes/notes/baremetal-details-09b27fba82111cfb.yaml new file mode 100644 index 000000000..f54d0dbd1 --- /dev/null +++ b/releasenotes/notes/baremetal-details-09b27fba82111cfb.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + The objects returned by baremetal detailed listing functions + (``connection.baremetal.{nodes,ports,chassis,port_groups}``) are now + fully functional, e.g. can be directly updated or deleted. +deprecations: + - | + The following baremetal resource classes are no longer used and will be + removed in a future release: ``NodeDetail``, ``PortDetail``, + ``ChassisDetail`` and ``PortGroupDetail``. The regular ``Node``, ``Port``, + ``Chassis`` and ``PortGroup`` are now used instead. From 84116d6125b94426084c0eb7f5d07c0160998f36 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 23 Nov 2018 10:21:06 +0100 Subject: [PATCH 2290/3836] Make timeouts in functional tests configurable functional tests are often failing due to timeouts in `wait_for` functions. It forces us to trigger a "recheck". Let's make it configurable instead. A top level timeout can be set with `OPENSTACKSDK_FUNC_TEST_TIMEOUT` env and defaults to 300. Func tests for each service might override this var (i.e. for heat `OPENSTACKSDK_FUNC_TEST_TIMEOUT_ORCHESTRATE`). OPENSTACKSDK_FUNC_TEST_TIMEOUT_LOAD_BALANCER is set to 600 (in tox) to respect the value previously used in the code. Change-Id: I7bdcd67e858a1f2c43cb131ebdf549da070fbb4a --- openstack/tests/functional/base.py | 9 ++ .../tests/functional/block_storage/v2/base.py | 25 +++++ .../block_storage/v2/test_snapshot.py | 10 +- .../functional/block_storage/v2/test_type.py | 4 +- .../block_storage/v2/test_volume.py | 7 +- .../functional/clustering/test_cluster.py | 15 ++- openstack/tests/functional/compute/base.py | 25 +++++ .../functional/compute/v2/test_server.py | 8 +- .../load_balancer/v2/test_load_balancer.py | 95 +++++++++++++------ .../functional/orchestration/v1/test_stack.py | 13 ++- tox.ini | 5 +- 11 files changed, 168 insertions(+), 48 deletions(-) create mode 100644 openstack/tests/functional/block_storage/v2/base.py create mode 100644 openstack/tests/functional/compute/base.py diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index 215673499..911d2ebe0 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -45,6 +45,15 @@ def _disable_keep_alive(conn): class BaseFunctionalTest(base.TestCase): + @classmethod + def setUpClass(cls): + super(BaseFunctionalTest, cls).setUpClass() + # Defines default timeout for wait_for methods used + # in the functional tests + cls._wait_for_timeout = int(os.getenv( + 'OPENSTACKSDK_FUNC_TEST_TIMEOUT', + 300)) + def setUp(self): super(BaseFunctionalTest, self).setUp() self.conn = connection.Connection(config=TEST_CLOUD_REGION) diff --git a/openstack/tests/functional/block_storage/v2/base.py b/openstack/tests/functional/block_storage/v2/base.py new file mode 100644 index 000000000..e5f2694c5 --- /dev/null +++ b/openstack/tests/functional/block_storage/v2/base.py @@ -0,0 +1,25 @@ +# 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 + +from openstack.tests.functional import base + + +class BaseBlockStorageTest(base.BaseFunctionalTest): + + @classmethod + def setUpClass(cls): + super(BaseBlockStorageTest, cls).setUpClass() + cls._wait_for_timeout = int(os.getenv( + 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_BLOCK_STORAGE', + cls._wait_for_timeout)) diff --git a/openstack/tests/functional/block_storage/v2/test_snapshot.py b/openstack/tests/functional/block_storage/v2/test_snapshot.py index af898488d..8cf693f7f 100644 --- a/openstack/tests/functional/block_storage/v2/test_snapshot.py +++ b/openstack/tests/functional/block_storage/v2/test_snapshot.py @@ -13,10 +13,10 @@ from openstack.block_storage.v2 import snapshot as _snapshot from openstack.block_storage.v2 import volume as _volume -from openstack.tests.functional import base +from openstack.tests.functional.block_storage.v2 import base -class TestSnapshot(base.BaseFunctionalTest): +class TestSnapshot(base.BaseBlockStorageTest): def setUp(self): super(TestSnapshot, self).setUp() @@ -34,7 +34,7 @@ def setUp(self): status='available', failures=['error'], interval=2, - wait=120) + wait=self._wait_for_timeout) assert isinstance(volume, _volume.Volume) self.assertEqual(self.VOLUME_NAME, volume.name) self.VOLUME_ID = volume.id @@ -46,7 +46,7 @@ def setUp(self): status='available', failures=['error'], interval=2, - wait=120) + wait=self._wait_for_timeout) assert isinstance(snapshot, _snapshot.Snapshot) self.assertEqual(self.SNAPSHOT_NAME, snapshot.name) self.SNAPSHOT_ID = snapshot.id @@ -56,7 +56,7 @@ def tearDown(self): sot = self.conn.block_storage.delete_snapshot( snapshot, ignore_missing=False) self.conn.block_storage.wait_for_delete( - snapshot, interval=2, wait=120) + snapshot, interval=2, wait=self._wait_for_timeout) self.assertIsNone(sot) sot = self.conn.block_storage.delete_volume( self.VOLUME_ID, ignore_missing=False) diff --git a/openstack/tests/functional/block_storage/v2/test_type.py b/openstack/tests/functional/block_storage/v2/test_type.py index 46d2b8720..b983a0507 100644 --- a/openstack/tests/functional/block_storage/v2/test_type.py +++ b/openstack/tests/functional/block_storage/v2/test_type.py @@ -12,10 +12,10 @@ from openstack.block_storage.v2 import type as _type -from openstack.tests.functional import base +from openstack.tests.functional.block_storage.v2 import base -class TestType(base.BaseFunctionalTest): +class TestType(base.BaseBlockStorageTest): def setUp(self): super(TestType, self).setUp() diff --git a/openstack/tests/functional/block_storage/v2/test_volume.py b/openstack/tests/functional/block_storage/v2/test_volume.py index 803d858b0..405accd62 100644 --- a/openstack/tests/functional/block_storage/v2/test_volume.py +++ b/openstack/tests/functional/block_storage/v2/test_volume.py @@ -10,12 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. - from openstack.block_storage.v2 import volume as _volume -from openstack.tests.functional import base +from openstack.tests.functional.block_storage.v2 import base -class TestVolume(base.BaseFunctionalTest): +class TestVolume(base.BaseBlockStorageTest): def setUp(self): super(TestVolume, self).setUp() @@ -31,7 +30,7 @@ def setUp(self): status='available', failures=['error'], interval=2, - wait=120) + wait=self._wait_for_timeout) assert isinstance(volume, _volume.Volume) self.assertEqual(self.VOLUME_NAME, volume.name) self.VOLUME_ID = volume.id diff --git a/openstack/tests/functional/clustering/test_cluster.py b/openstack/tests/functional/clustering/test_cluster.py index a58e17840..e118a7873 100644 --- a/openstack/tests/functional/clustering/test_cluster.py +++ b/openstack/tests/functional/clustering/test_cluster.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import os import time from openstack.clustering.v1 import cluster @@ -19,6 +20,13 @@ class TestCluster(base.BaseFunctionalTest): + @classmethod + def setUpClass(cls): + super(TestCluster, cls).setUpClass() + cls._wait_for_timeout = int(os.getenv( + 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_CLUSTER', + cls._wait_for_timeout)) + def setUp(self): super(TestCluster, self).setUp() self.require_service('clustering') @@ -56,12 +64,15 @@ def setUp(self): } self.cluster = self.conn.clustering.create_cluster(**cluster_spec) - self.conn.clustering.wait_for_status(self.cluster, 'ACTIVE') + self.conn.clustering.wait_for_status( + self.cluster, 'ACTIVE', + wait=self._wait_for_timeout) assert isinstance(self.cluster, cluster.Cluster) def tearDown(self): self.conn.clustering.delete_cluster(self.cluster.id) - self.conn.clustering.wait_for_delete(self.cluster) + self.conn.clustering.wait_for_delete(self.cluster, + wait=self._wait_for_timeout) test_network.delete_network(self.conn, self.network, self.subnet) diff --git a/openstack/tests/functional/compute/base.py b/openstack/tests/functional/compute/base.py new file mode 100644 index 000000000..12b86fad1 --- /dev/null +++ b/openstack/tests/functional/compute/base.py @@ -0,0 +1,25 @@ +# 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 + +from openstack.tests.functional import base + + +class BaseComputeTest(base.BaseFunctionalTest): + + @classmethod + def setUpClass(cls): + super(BaseComputeTest, cls).setUpClass() + cls._wait_for_timeout = int(os.getenv( + 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_COMPUTE', + cls._wait_for_timeout)) diff --git a/openstack/tests/functional/compute/v2/test_server.py b/openstack/tests/functional/compute/v2/test_server.py index 1d618513b..5f8da5533 100644 --- a/openstack/tests/functional/compute/v2/test_server.py +++ b/openstack/tests/functional/compute/v2/test_server.py @@ -12,10 +12,11 @@ from openstack.compute.v2 import server from openstack.tests.functional import base +from openstack.tests.functional.compute import base as ft_base from openstack.tests.functional.network.v2 import test_network -class TestServer(base.BaseFunctionalTest): +class TestServer(ft_base.BaseComputeTest): def setUp(self): super(TestServer, self).setUp() @@ -38,7 +39,7 @@ def setUp(self): sot = self.conn.compute.create_server( name=self.NAME, flavor_id=flavor.id, image_id=image.id, networks=[{"uuid": self.network.id}]) - self.conn.compute.wait_for_server(sot) + self.conn.compute.wait_for_server(sot, wait=self._wait_for_timeout) assert isinstance(sot, server.Server) self.assertEqual(self.NAME, sot.name) self.server = sot @@ -47,7 +48,8 @@ def tearDown(self): sot = self.conn.compute.delete_server(self.server.id) self.assertIsNone(sot) # Need to wait for the stack to go away before network delete - self.conn.compute.wait_for_delete(self.server) + self.conn.compute.wait_for_delete(self.server, + wait=self._wait_for_timeout) test_network.delete_network(self.conn, self.network, self.subnet) super(TestServer, self).tearDown() diff --git a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py index ad93ea26e..e2bab7fcb 100644 --- a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py +++ b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import os + from openstack.load_balancer.v2 import health_monitor from openstack.load_balancer.v2 import l7_policy from openstack.load_balancer.v2 import l7_rule @@ -45,6 +47,13 @@ class TestLoadBalancer(base.BaseFunctionalTest): L7RULE_TYPE = 'HOST_NAME' L7RULE_VALUE = 'example' + @classmethod + def setUpClass(cls): + super(TestLoadBalancer, cls).setUpClass() + cls._wait_for_timeout = int(os.getenv( + 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_LOAD_BALANCER', + cls._wait_for_timeout)) + # TODO(shade): Creating load balancers can be slow on some hosts due to # nova instance boot times (up to ten minutes). This used to # use setUpClass, but that's a whole other pile of bad, so @@ -70,8 +79,9 @@ def setUp(self): self.assertEqual(self.LB_NAME, test_lb.name) # Wait for the LB to go ACTIVE. On non-virtualization enabled hosts # it can take nova up to ten minutes to boot a VM. - self.conn.load_balancer.wait_for_load_balancer(test_lb.id, interval=1, - wait=600) + self.conn.load_balancer.wait_for_load_balancer( + test_lb.id, interval=1, + wait=self._wait_for_timeout) self.LB_ID = test_lb.id test_listener = self.conn.load_balancer.create_listener( @@ -80,7 +90,8 @@ def setUp(self): assert isinstance(test_listener, listener.Listener) self.assertEqual(self.LISTENER_NAME, test_listener.name) self.LISTENER_ID = test_listener.id - self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) + self.conn.load_balancer.wait_for_load_balancer( + self.LB_ID, wait=self._wait_for_timeout) test_pool = self.conn.load_balancer.create_pool( name=self.POOL_NAME, protocol=self.PROTOCOL, @@ -88,7 +99,8 @@ def setUp(self): assert isinstance(test_pool, pool.Pool) self.assertEqual(self.POOL_NAME, test_pool.name) self.POOL_ID = test_pool.id - self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) + self.conn.load_balancer.wait_for_load_balancer( + self.LB_ID, wait=self._wait_for_timeout) test_member = self.conn.load_balancer.create_member( pool=self.POOL_ID, name=self.MEMBER_NAME, @@ -97,7 +109,8 @@ def setUp(self): assert isinstance(test_member, member.Member) self.assertEqual(self.MEMBER_NAME, test_member.name) self.MEMBER_ID = test_member.id - self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) + self.conn.load_balancer.wait_for_load_balancer( + self.LB_ID, wait=self._wait_for_timeout) test_hm = self.conn.load_balancer.create_health_monitor( pool_id=self.POOL_ID, name=self.HM_NAME, delay=self.DELAY, @@ -106,7 +119,8 @@ def setUp(self): assert isinstance(test_hm, health_monitor.HealthMonitor) self.assertEqual(self.HM_NAME, test_hm.name) self.HM_ID = test_hm.id - self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) + self.conn.load_balancer.wait_for_load_balancer( + self.LB_ID, wait=self._wait_for_timeout) test_l7policy = self.conn.load_balancer.create_l7_policy( listener_id=self.LISTENER_ID, name=self.L7POLICY_NAME, @@ -114,7 +128,8 @@ def setUp(self): assert isinstance(test_l7policy, l7_policy.L7Policy) self.assertEqual(self.L7POLICY_NAME, test_l7policy.name) self.L7POLICY_ID = test_l7policy.id - self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) + self.conn.load_balancer.wait_for_load_balancer( + self.LB_ID, wait=self._wait_for_timeout) test_l7rule = self.conn.load_balancer.create_l7_rule( l7_policy=self.L7POLICY_ID, compare_type=self.COMPARE_TYPE, @@ -122,34 +137,42 @@ def setUp(self): assert isinstance(test_l7rule, l7_rule.L7Rule) self.assertEqual(self.COMPARE_TYPE, test_l7rule.compare_type) self.L7RULE_ID = test_l7rule.id - self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) + self.conn.load_balancer.wait_for_load_balancer( + self.LB_ID, wait=self._wait_for_timeout) def tearDown(self): self.conn.load_balancer.get_load_balancer(self.LB_ID) - self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) + self.conn.load_balancer.wait_for_load_balancer( + self.LB_ID, wait=self._wait_for_timeout) self.conn.load_balancer.delete_l7_rule( self.L7RULE_ID, l7_policy=self.L7POLICY_ID, ignore_missing=False) - self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) + self.conn.load_balancer.wait_for_load_balancer( + self.LB_ID, wait=self._wait_for_timeout) self.conn.load_balancer.delete_l7_policy( self.L7POLICY_ID, ignore_missing=False) - self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) + self.conn.load_balancer.wait_for_load_balancer( + self.LB_ID, wait=self._wait_for_timeout) self.conn.load_balancer.delete_health_monitor( self.HM_ID, ignore_missing=False) - self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) + self.conn.load_balancer.wait_for_load_balancer( + self.LB_ID, wait=self._wait_for_timeout) self.conn.load_balancer.delete_member( self.MEMBER_ID, self.POOL_ID, ignore_missing=False) - self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) + self.conn.load_balancer.wait_for_load_balancer( + self.LB_ID, wait=self._wait_for_timeout) self.conn.load_balancer.delete_pool(self.POOL_ID, ignore_missing=False) - self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) + self.conn.load_balancer.wait_for_load_balancer( + self.LB_ID, wait=self._wait_for_timeout) self.conn.load_balancer.delete_listener(self.LISTENER_ID, ignore_missing=False) - self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) + self.conn.load_balancer.wait_for_load_balancer( + self.LB_ID, wait=self._wait_for_timeout) self.conn.load_balancer.delete_load_balancer( self.LB_ID, ignore_missing=False) @@ -172,13 +195,15 @@ def test_lb_list(self): def test_lb_update(self): self.conn.load_balancer.update_load_balancer( self.LB_ID, name=self.UPDATE_NAME) - self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) + self.conn.load_balancer.wait_for_load_balancer( + self.LB_ID, wait=self._wait_for_timeout) test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) self.assertEqual(self.UPDATE_NAME, test_lb.name) self.conn.load_balancer.update_load_balancer( self.LB_ID, name=self.LB_NAME) - self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) + self.conn.load_balancer.wait_for_load_balancer( + self.LB_ID, wait=self._wait_for_timeout) test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) self.assertEqual(self.LB_NAME, test_lb.name) @@ -203,13 +228,15 @@ def test_listener_update(self): self.conn.load_balancer.update_listener( self.LISTENER_ID, name=self.UPDATE_NAME) - self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) + self.conn.load_balancer.wait_for_load_balancer( + self.LB_ID, wait=self._wait_for_timeout) test_listener = self.conn.load_balancer.get_listener(self.LISTENER_ID) self.assertEqual(self.UPDATE_NAME, test_listener.name) self.conn.load_balancer.update_listener( self.LISTENER_ID, name=self.LISTENER_NAME) - self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) + self.conn.load_balancer.wait_for_load_balancer( + self.LB_ID, wait=self._wait_for_timeout) test_listener = self.conn.load_balancer.get_listener(self.LISTENER_ID) self.assertEqual(self.LISTENER_NAME, test_listener.name) @@ -232,13 +259,15 @@ def test_pool_update(self): self.conn.load_balancer.update_pool(self.POOL_ID, name=self.UPDATE_NAME) - self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) + self.conn.load_balancer.wait_for_load_balancer( + self.LB_ID, wait=self._wait_for_timeout) test_pool = self.conn.load_balancer.get_pool(self.POOL_ID) self.assertEqual(self.UPDATE_NAME, test_pool.name) self.conn.load_balancer.update_pool(self.POOL_ID, name=self.POOL_NAME) - self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) + self.conn.load_balancer.wait_for_load_balancer( + self.LB_ID, wait=self._wait_for_timeout) test_pool = self.conn.load_balancer.get_pool(self.POOL_ID) self.assertEqual(self.POOL_NAME, test_pool.name) @@ -266,14 +295,16 @@ def test_member_update(self): self.conn.load_balancer.update_member(self.MEMBER_ID, self.POOL_ID, name=self.UPDATE_NAME) - self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) + self.conn.load_balancer.wait_for_load_balancer( + self.LB_ID, wait=self._wait_for_timeout) test_member = self.conn.load_balancer.get_member(self.MEMBER_ID, self.POOL_ID) self.assertEqual(self.UPDATE_NAME, test_member.name) self.conn.load_balancer.update_member(self.MEMBER_ID, self.POOL_ID, name=self.MEMBER_NAME) - self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) + self.conn.load_balancer.wait_for_load_balancer( + self.LB_ID, wait=self._wait_for_timeout) test_member = self.conn.load_balancer.get_member(self.MEMBER_ID, self.POOL_ID) self.assertEqual(self.MEMBER_NAME, test_member.name) @@ -300,13 +331,15 @@ def test_health_monitor_update(self): self.conn.load_balancer.update_health_monitor(self.HM_ID, name=self.UPDATE_NAME) - self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) + self.conn.load_balancer.wait_for_load_balancer( + self.LB_ID, wait=self._wait_for_timeout) test_hm = self.conn.load_balancer.get_health_monitor(self.HM_ID) self.assertEqual(self.UPDATE_NAME, test_hm.name) self.conn.load_balancer.update_health_monitor(self.HM_ID, name=self.HM_NAME) - self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) + self.conn.load_balancer.wait_for_load_balancer( + self.LB_ID, wait=self._wait_for_timeout) test_hm = self.conn.load_balancer.get_health_monitor(self.HM_ID) self.assertEqual(self.HM_NAME, test_hm.name) @@ -331,14 +364,16 @@ def test_l7_policy_update(self): self.conn.load_balancer.update_l7_policy( self.L7POLICY_ID, name=self.UPDATE_NAME) - self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) + self.conn.load_balancer.wait_for_load_balancer( + self.LB_ID, wait=self._wait_for_timeout) test_l7_policy = self.conn.load_balancer.get_l7_policy( self.L7POLICY_ID) self.assertEqual(self.UPDATE_NAME, test_l7_policy.name) self.conn.load_balancer.update_l7_policy(self.L7POLICY_ID, name=self.L7POLICY_NAME) - self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) + self.conn.load_balancer.wait_for_load_balancer( + self.LB_ID, wait=self._wait_for_timeout) test_l7_policy = self.conn.load_balancer.get_l7_policy( self.L7POLICY_ID) self.assertEqual(self.L7POLICY_NAME, test_l7_policy.name) @@ -368,7 +403,8 @@ def test_l7_rule_update(self): self.conn.load_balancer.update_l7_rule(self.L7RULE_ID, l7_policy=self.L7POLICY_ID, rule_value=self.UPDATE_NAME) - self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) + self.conn.load_balancer.wait_for_load_balancer( + self.LB_ID, wait=self._wait_for_timeout) test_l7_rule = self.conn.load_balancer.get_l7_rule( self.L7RULE_ID, l7_policy=self.L7POLICY_ID) self.assertEqual(self.UPDATE_NAME, test_l7_rule.rule_value) @@ -376,7 +412,8 @@ def test_l7_rule_update(self): self.conn.load_balancer.update_l7_rule(self.L7RULE_ID, l7_policy=self.L7POLICY_ID, rule_value=self.L7RULE_VALUE) - self.conn.load_balancer.wait_for_load_balancer(self.LB_ID) + self.conn.load_balancer.wait_for_load_balancer( + self.LB_ID, wait=self._wait_for_timeout) test_l7_rule = self.conn.load_balancer.get_l7_rule( self.L7RULE_ID, l7_policy=self.L7POLICY_ID,) self.assertEqual(self.L7RULE_VALUE, test_l7_rule.rule_value) diff --git a/openstack/tests/functional/orchestration/v1/test_stack.py b/openstack/tests/functional/orchestration/v1/test_stack.py index 47920bd54..67ef21537 100644 --- a/openstack/tests/functional/orchestration/v1/test_stack.py +++ b/openstack/tests/functional/orchestration/v1/test_stack.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import os import yaml from openstack import exceptions @@ -26,6 +27,13 @@ class TestStack(base.BaseFunctionalTest): subnet = None cidr = '10.99.99.0/16' + @classmethod + def setUpClass(cls): + super(TestStack, cls).setUpClass() + cls._wait_for_timeout = int(os.getenv( + 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_ORCHESTRATION', + cls._wait_for_timeout)) + def setUp(self): super(TestStack, self).setUp() self.require_service('orchestration') @@ -58,7 +66,8 @@ def setUp(self): self.stack = sot self.assertEqual(self.NAME, sot.name) self.conn.orchestration.wait_for_status( - sot, status='CREATE_COMPLETE', failures=['CREATE_FAILED']) + sot, status='CREATE_COMPLETE', failures=['CREATE_FAILED'], + wait=self._wait_for_timeout) def tearDown(self): self.conn.orchestration.delete_stack(self.stack, ignore_missing=False) @@ -66,7 +75,7 @@ def tearDown(self): # Need to wait for the stack to go away before network delete try: self.conn.orchestration.wait_for_status( - self.stack, 'DELETE_COMPLETE') + self.stack, 'DELETE_COMPLETE', wait=self._wait_for_timeout) except exceptions.ResourceNotFound: pass test_network.delete_network(self.conn, self.network, self.subnet) diff --git a/tox.ini b/tox.ini index ad02bfe85..7a525cd68 100644 --- a/tox.ini +++ b/tox.ini @@ -29,9 +29,12 @@ commands = stestr --test-path ./openstack/tests/examples run {posargs} stestr slowest [testenv:functional] +# Some jobs (especially heat) takes longer, therefore increase default timeout +# This timeout should not be smaller, than the longest individual timeout setenv = {[testenv]setenv} - OS_TEST_TIMEOUT=120 + OS_TEST_TIMEOUT=600 + OPENSTACKSDK_FUNC_TEST_TIMEOUT_LOAD_BALANCER=600 commands = stestr --test-path ./openstack/tests/functional/{env:OPENSTACKSDK_TESTS_SUBDIR:} run --serial {posargs} stestr slowest From 7b14c1e6fdafe634af8519615f78516a95fc9409 Mon Sep 17 00:00:00 2001 From: Pooja Jadhav Date: Sat, 11 Aug 2018 18:49:54 +0530 Subject: [PATCH 2291/3836] Add functional tests for masakari This patch adds minimal functional tests to check the behavior of supported APIs for Segment and Host by masakari. Also added new CI job to run these newly added functional test cases. Note: Functional test cases related to ``POST/GET /notifications`` APIs will be added in Masakari. Change-Id: I55fed23a1ceb259e9a13f3ed9e4b6d462330bcb4 --- .zuul.yaml | 21 ++++++ .../tests/functional/instance_ha/__init__.py | 0 .../tests/functional/instance_ha/test_host.py | 73 +++++++++++++++++++ .../functional/instance_ha/test_segment.py | 43 +++++++++++ 4 files changed, 137 insertions(+) create mode 100644 openstack/tests/functional/instance_ha/__init__.py create mode 100644 openstack/tests/functional/instance_ha/test_host.py create mode 100644 openstack/tests/functional/instance_ha/test_segment.py diff --git a/.zuul.yaml b/.zuul.yaml index 31394e229..eebbd6945 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -345,6 +345,25 @@ test_matrix_branch: master tox_install_siblings: true +- job: + name: openstacksdk-functional-devstack-masakari + parent: openstacksdk-functional-devstack-minimum + description: | + Run openstacksdk functional tests against a master devstack with masakari + required-projects: + - openstack/masakari + vars: + devstack_plugins: + masakari: https://git.openstack.org/openstack/masakari + devstack_services: + masakari-api: true + masakari-engine: true + tox_environment: + OPENSTACKSDK_HAS_MASAKARI: 1 + OPENSTACKSDK_TESTS_SUBDIR: instance_ha + zuul_copy_output: + '{{ devstack_base_dir }}/masakari-logs': logs + - project-template: name: openstacksdk-functional-tips check: @@ -379,6 +398,8 @@ - openstacksdk-functional-devstack-senlin - openstacksdk-functional-devstack-magnum: voting: false + - openstacksdk-functional-devstack-masakari: + voting: false - openstacksdk-functional-devstack-ironic: voting: false - openstacksdk-functional-devstack-python2 diff --git a/openstack/tests/functional/instance_ha/__init__.py b/openstack/tests/functional/instance_ha/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/functional/instance_ha/test_host.py b/openstack/tests/functional/instance_ha/test_host.py new file mode 100644 index 000000000..23bfb0a92 --- /dev/null +++ b/openstack/tests/functional/instance_ha/test_host.py @@ -0,0 +1,73 @@ +# Copyright (C) 2018 NTT DATA +# 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 unittest + +from openstack import connection +from openstack.cloud.openstackcloud import _OpenStackCloudMixin +from openstack.tests.functional import base + +HYPERVISORS = [] + + +def hypervisors(): + global HYPERVISORS + if HYPERVISORS: + return True + HYPERVISORS = _OpenStackCloudMixin.list_hypervisors( + connection.from_config(cloud_name=base.TEST_CLOUD_NAME)) + return bool(HYPERVISORS) + + +class TestHost(base.BaseFunctionalTest): + + def setUp(self): + super(TestHost, self).setUp() + self.require_service('instance-ha') + self.NAME = self.getUniqueString() + + if not hypervisors(): + self.skipTest("Skip TestHost as there are no hypervisors " + "configured in nova") + + # Create segment + self.segment = self.conn.ha.create_segment( + name=self.NAME, recovery_method='auto', + service_type='COMPUTE') + + # Create valid host + self.NAME = HYPERVISORS[0]['hypervisor_hostname'] + self.host = self.conn.ha.create_host( + segment_id=self.segment.uuid, name=self.NAME, type='COMPUTE', + control_attributes='SSH') + + # Delete host + self.addCleanup(self.conn.ha.delete_host, self.segment.uuid, + self.host.uuid) + # Delete segment + self.addCleanup(self.conn.ha.delete_segment, self.segment.uuid) + + def test_list(self): + names = [o.name for o in self.conn.ha.hosts( + self.segment.uuid, failover_segment_id=self.segment.uuid, + type='COMPUTE')] + self.assertIn(self.NAME, names) + + def test_update(self): + updated_host = self.conn.ha.update_host(self.host['uuid'], + segment_id=self.segment.uuid, + on_maintenance='True') + get_host = self.conn.ha.get_host(updated_host.uuid, + updated_host.segment_id) + self.assertEqual(True, get_host.on_maintenance) diff --git a/openstack/tests/functional/instance_ha/test_segment.py b/openstack/tests/functional/instance_ha/test_segment.py new file mode 100644 index 000000000..15d863778 --- /dev/null +++ b/openstack/tests/functional/instance_ha/test_segment.py @@ -0,0 +1,43 @@ +# Copyright (C) 2018 NTT DATA +# 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 openstack.tests.functional import base + + +class TestSegment(base.BaseFunctionalTest): + + def setUp(self): + super(TestSegment, self).setUp() + self.require_service('instance-ha') + self.NAME = self.getUniqueString() + + # Create segment + self.segment = self.conn.ha.create_segment( + name=self.NAME, recovery_method='auto', + service_type='COMPUTE') + + # Delete segment + self.addCleanup(self.conn.ha.delete_segment, self.segment['uuid']) + + def test_list(self): + names = [o.name for o in self.conn.ha.segments( + recovery_method='auto')] + self.assertIn(self.NAME, names) + + def test_update(self): + updated_segment = self.conn.ha.update_segment(self.segment['uuid'], + name='UPDATED-NAME') + get_updated_segment = self.conn.ha.get_segment(updated_segment.uuid) + self.assertEqual('UPDATED-NAME', get_updated_segment.name) From 8f97fb767cb89f284e371c52abfc9b84da4602a8 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 23 Nov 2018 10:32:52 +0100 Subject: [PATCH 2292/3836] Check result of server metadata operation An accidental failure in one of the functional tests showed, that response of metadata change was not checked. In that particular example nova returned 500 for one of the metadata deletes, but the testcase has not recognized this and went further, what resulted in a later asserEquals failure. So what we need to do here is to check the response of the metadata operation and raise an exception if it was not successful. Change-Id: I37bc96531eb37f9f850d5531354bd4db17973406 --- openstack/compute/v2/metadata.py | 3 ++ .../tests/unit/compute/v2/test_metadata.py | 41 ++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/openstack/compute/v2/metadata.py b/openstack/compute/v2/metadata.py index 5fbb7e2c5..e585871f7 100644 --- a/openstack/compute/v2/metadata.py +++ b/openstack/compute/v2/metadata.py @@ -12,6 +12,7 @@ import six +from openstack import exceptions from openstack import utils @@ -48,6 +49,8 @@ def _metadata(self, method, key=None, clear=False, delete=False, response = method(url, headers=headers, **kwargs) + # ensure Nova API has not returned us an error + exceptions.raise_from_response(response) # DELETE doesn't return a JSON body while everything else does. return response.json() if not delete else None diff --git a/openstack/tests/unit/compute/v2/test_metadata.py b/openstack/tests/unit/compute/v2/test_metadata.py index 6924b93e9..0339b78e2 100644 --- a/openstack/tests/unit/compute/v2/test_metadata.py +++ b/openstack/tests/unit/compute/v2/test_metadata.py @@ -11,6 +11,7 @@ # under the License. import mock +from openstack import exceptions from openstack.tests.unit import base from openstack.compute.v2 import server @@ -41,6 +42,7 @@ def test_get_all_metadata_ServerDetail(self): def _test_get_all_metadata(self, sot): response = mock.Mock() + response.status_code = 200 response.json.return_value = self.metadata_result sess = mock.Mock() sess.get.return_value = response @@ -54,6 +56,7 @@ def _test_get_all_metadata(self, sot): def test_set_metadata(self): response = mock.Mock() + response.status_code = 200 response.json.return_value = self.metadata_result sess = mock.Mock() sess.post.return_value = response @@ -71,7 +74,9 @@ def test_set_metadata(self): def test_delete_metadata(self): sess = mock.Mock() - sess.delete.return_value = None + response = mock.Mock() + response.status_code = 200 + sess.delete.return_value = response sot = server.Server(id=IDENTIFIER) @@ -83,3 +88,37 @@ def test_delete_metadata(self): "servers/IDENTIFIER/metadata/" + key, headers={"Accept": ""}, ) + + def test_delete_metadata_error(self): + sess = mock.Mock() + response = mock.Mock() + response.status_code = 400 + response.content = None + sess.delete.return_value = response + + sot = server.Server(id=IDENTIFIER) + + key = "hey" + + self.assertRaises( + exceptions.BadRequestException, + sot.delete_metadata, + sess, + [key]) + + def test_set_metadata_error(self): + sess = mock.Mock() + response = mock.Mock() + response.status_code = 400 + response.content = None + sess.post.return_value = response + + sot = server.Server(id=IDENTIFIER) + + set_meta = {"lol": "rofl"} + + self.assertRaises( + exceptions.BadRequestException, + sot.set_metadata, + sess, + **set_meta) From bcc9965a43a21be17fc86cd9c20cb81a848294dd Mon Sep 17 00:00:00 2001 From: zhangdebo Date: Sat, 24 Nov 2018 10:07:46 +0800 Subject: [PATCH 2293/3836] Update link address for vendor support Change-Id: Ib3c47bd64593f559c2c2dfd7cae6853e1e64f737 --- doc/source/user/multi-cloud-demo.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/user/multi-cloud-demo.rst b/doc/source/user/multi-cloud-demo.rst index de529d598..1dd1852a0 100644 --- a/doc/source/user/multi-cloud-demo.rst +++ b/doc/source/user/multi-cloud-demo.rst @@ -221,7 +221,7 @@ Simple example of a clouds.yaml * Config for a named `cloud` "my-citycloud" * Reference a well-known "named" profile: `citycloud` * `os-client-config` has a built-in list of profiles at - https://docs.openstack.org/os-client-config/latest/user/vendor-support.html + https://docs.openstack.org/openstacksdk/latest/user/config/vendor-support.html * Vendor profiles contain various advanced config * `cloud` name can match `profile` name (using different names for clarity) From 1d1b5dae7d879dc9d95e6a15a69a86440122e538 Mon Sep 17 00:00:00 2001 From: Lars Kellogg-Stedman Date: Sun, 25 Nov 2018 21:36:55 -0500 Subject: [PATCH 2294/3836] do not force interface=admin for identity api v3 In some places we were forcing interface=admin for identity api v3 connections, which is not necessary and can cause unexpected problems when the caller was explicitly trying to interact with the public or internal api endpoints. Story: #2004422 Task: #28074 Change-Id: Ib142be68ad7c2acce3a5daee4cb9aea141a4dc71 --- openstack/cloud/openstackcloud.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 6a24efb80..445cd9ff1 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -10318,7 +10318,7 @@ def update_service(self, name_or_id, **kwargs): msg = 'Error in updating service {service}'.format(service=name_or_id) data = self._identity_client.patch( '{url}/{id}'.format(url=url, id=service['id']), json={key: kwargs}, - endpoint_filter={'interface': 'admin'}, error_message=msg) + error_message=msg) service = self._get_and_munchify(key, data) return _utils.normalize_keystone_services([service])[0] @@ -10332,10 +10332,13 @@ def list_services(self): """ if self._is_client_version('identity', 2): url, key = '/OS-KSADM/services', 'OS-KSADM:services' + endpoint_filter = {'interface': 'admin'} else: url, key = '/services', 'services' + endpoint_filter = {} + data = self._identity_client.get( - url, endpoint_filter={'interface': 'admin'}, + url, endpoint_filter=endpoint_filter, error_message="Failed to list services") services = self._get_and_munchify(key, data) return _utils.normalize_keystone_services(services) @@ -10391,13 +10394,15 @@ def delete_service(self, name_or_id): if self._is_client_version('identity', 2): url = '/OS-KSADM/services' + endpoint_filter = {'interface': 'admin'} else: url = '/services' + endpoint_filter = {} error_msg = 'Failed to delete service {id}'.format(id=service['id']) self._identity_client.delete( '{url}/{id}'.format(url=url, id=service['id']), - endpoint_filter={'interface': 'admin'}, error_message=error_msg) + endpoint_filter=endpoint_filter, error_message=error_msg) return True From 9b29b8871998141c81964d7942b2681b42fd60af Mon Sep 17 00:00:00 2001 From: Slawek Kaplonski Date: Thu, 8 Nov 2018 23:51:27 +0100 Subject: [PATCH 2295/3836] Add CRUD methods for Neutron Port Forwarding Floating IP port forwarding API was implemented in Neutron in Rocky. This patch adds support for it in SDK. Depends-On: https://review.openstack.org/617045 Change-Id: Ib3f8b45e1534198f1e03223c20348fb604a91a59 --- .zuul.yaml | 1 + openstack/network/v2/_proxy.py | 120 ++++++++++++ openstack/network/v2/port_forwarding.py | 42 +++++ .../network/v2/test_port_forwarding.py | 175 ++++++++++++++++++ .../unit/network/v2/test_port_forwarding.py | 52 ++++++ 5 files changed, 390 insertions(+) create mode 100644 openstack/network/v2/port_forwarding.py create mode 100644 openstack/tests/functional/network/v2/test_port_forwarding.py create mode 100644 openstack/tests/unit/network/v2/test_port_forwarding.py diff --git a/.zuul.yaml b/.zuul.yaml index 31394e229..eff7fb97c 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -107,6 +107,7 @@ devstack_services: neutron-qos: true neutron-trunk: true + neutron-port-forwarding: true - job: name: openstacksdk-functional-devstack-networking diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index d7d2af176..145e37a55 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -32,6 +32,7 @@ from openstack.network.v2 import pool as _pool from openstack.network.v2 import pool_member as _pool_member from openstack.network.v2 import port as _port +from openstack.network.v2 import port_forwarding as _port_forwarding from openstack.network.v2 import qos_bandwidth_limit_rule as \ _qos_bandwidth_limit_rule from openstack.network.v2 import qos_dscp_marking_rule as \ @@ -589,6 +590,125 @@ def update_ip(self, floating_ip, **attrs): """ return self._update(_floating_ip.FloatingIP, floating_ip, **attrs) + def create_port_forwarding(self, **attrs): + """Create a new floating ip port forwarding from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.network.v2.port_forwarding.PortForwarding`, + comprised of the properties on the PortForwarding class. + + :returns: The results of port forwarding creation + :rtype: :class:`~openstack.network.v2.port_forwarding.PortForwarding` + """ + return self._create(_port_forwarding.PortForwarding, **attrs) + + def get_port_forwarding(self, port_forwarding, floating_ip): + """Get a single port forwarding + + :param port_forwarding: The value can be the ID of a port forwarding + or a :class:`~openstack.network.v2.port_forwarding.PortForwarding` + instance. + :param floating_ip: The value can be the ID of a Floating IP or a + :class:`~openstack.network.v2.floating_ip.FloatingIP` + instance. + + :returns: One + :class:`~openstack.network.v2.port_forwarding.PortForwarding` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + floating_ip = self._get_resource(_floating_ip.FloatingIP, floating_ip) + return self._get(_port_forwarding.PortForwarding, port_forwarding, + floatingip_id=floating_ip.id) + + def find_port_forwarding(self, pf_id, floating_ip, ignore_missing=True, + **args): + """Find a single port forwarding + + :param pf_id: The ID of a port forwarding. + :param floating_ip: The value can be the ID of a Floating IP or a + :class:`~openstack.network.v2.floating_ip.FloatingIP` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. + :returns: + One :class:`~openstack.network.v2.port_forwarding.PortForwarding` + or None + """ + floating_ip = self._get_resource(_floating_ip.FloatingIP, floating_ip) + return self._find(_port_forwarding.PortForwarding, pf_id, + floatingip_id=floating_ip.id, + ignore_missing=ignore_missing, **args) + + def delete_port_forwarding(self, port_forwarding, floating_ip, + ignore_missing=True): + """Delete a port forwarding + + :param port_forwarding: The value can be the ID of a port forwarding + or a :class:`~openstack.network.v2.port_forwarding.PortForwarding` + instance. + :param floating_ip: The value can be the ID of a Floating IP or a + :class:`~openstack.network.v2.floating_ip.FloatingIP` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the floating ip does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent ip. + + :returns: ``None`` + """ + fip = self._get_resource(_floating_ip.FloatingIP, floating_ip) + self._delete(_port_forwarding.PortForwarding, port_forwarding, + floatingip_id=fip.id, + ignore_missing=ignore_missing) + + def port_forwardings(self, floating_ip, **query): + """Return a generator of port forwardings + + :param floating_ip: The value can be the ID of a Floating IP or a + :class:`~openstack.network.v2.floating_ip.FloatingIP` + instance. + :param dict query: Optional query parameters to be sent to limit + the resources being returned. Valid parameters are: + + * ``internal_port_id``: The ID of internal port. + * ``internal_ip_address``: The internal IP address + * ``internal_port``: The internal TCP/UDP/other port number + * ``external_port``: The external TCP/UDP/other port number + * ``protocol``: TCP/UDP/other protocol + + :returns: A generator of port forwarding objects + :rtype: :class:`~openstack.network.v2.port_forwarding.PortForwarding` + """ + fip = self._get_resource(_floating_ip.FloatingIP, floating_ip) + return self._list(_port_forwarding.PortForwarding, paginated=False, + floatingip_id=fip.id, **query) + + def update_port_forwarding(self, port_forwarding, floating_ip, **attrs): + """Update a port forwarding + + :param port_forwarding: The value can be the ID of a port forwarding + or a :class:`~openstack.network.v2.port_forwarding.PortForwarding` + instance. + :param floating_ip: The value can be the ID of a Floating IP or a + :class:`~openstack.network.v2.floating_ip.FloatingIP` + instance. + :param dict attrs: The attributes to update on the ip represented + by ``value``. + + :returns: The updated port_forwarding + :rtype: :class:`~openstack.network.v2.port_forwarding.PortForwarding` + """ + fip = self._get_resource(_floating_ip.FloatingIP, floating_ip) + return self._update(_port_forwarding.PortForwarding, + port_forwarding, floatingip_id=fip.id, **attrs) + def create_health_monitor(self, **attrs): """Create a new health monitor from attributes diff --git a/openstack/network/v2/port_forwarding.py b/openstack/network/v2/port_forwarding.py new file mode 100644 index 000000000..ee4004841 --- /dev/null +++ b/openstack/network/v2/port_forwarding.py @@ -0,0 +1,42 @@ +# 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 resource + + +class PortForwarding(resource.Resource): + name_attribute = "floating_ip_port_forwarding" + resource_name = "port forwarding" + resource_key = 'port_forwarding' + resources_key = 'port_forwardings' + base_path = '/floatingips/%(floatingip_id)s/port_forwardings' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + # Properties + #: The ID of Floating IP address + floatingip_id = resource.URI('floatingip_id') + #: The ID of internal port + internal_port_id = resource.Body('internal_port_id') + #: The internal IP address + internal_ip_address = resource.Body('internal_ip_address') + #: The internal TCP/UDP/other port number + internal_port = resource.Body('internal_port', type=int) + #: The external TCP/UDP/other port number + external_port = resource.Body('external_port', type=int) + #: The protocol + protocol = resource.Body('protocol') diff --git a/openstack/tests/functional/network/v2/test_port_forwarding.py b/openstack/tests/functional/network/v2/test_port_forwarding.py new file mode 100644 index 000000000..1e39cd679 --- /dev/null +++ b/openstack/tests/functional/network/v2/test_port_forwarding.py @@ -0,0 +1,175 @@ +# 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.network.v2 import floating_ip +from openstack.network.v2 import network +from openstack.network.v2 import port_forwarding as _port_forwarding +from openstack.network.v2 import port +from openstack.network.v2 import router +from openstack.network.v2 import subnet +from openstack.tests.functional import base + + +class TestPortForwarding(base.BaseFunctionalTest): + + IPV4 = 4 + FIP_ID = None + EXT_CIDR = "10.100.0.0/24" + INT_CIDR = "10.101.0.0/24" + EXT_NET_ID = None + INT_NET_ID = None + EXT_SUB_ID = None + INT_SUB_ID = None + ROT_ID = None + + INTERNAL_PORT_ID = None + INTERNAL_IP_ADDRESS = None + INTERNAL_PORT = 8080 + EXTERNAL_PORT = 80 + PROTOCOL = "tcp" + + def setUp(self): + super(TestPortForwarding, self).setUp() + + if not self.conn.network.find_extension('floating-ip-port-forwarding'): + self.skipTest('Floating IP Port Forwarding extension disabled') + + self.ROT_NAME = self.getUniqueString() + self.EXT_NET_NAME = self.getUniqueString() + self.EXT_SUB_NAME = self.getUniqueString() + self.INT_NET_NAME = self.getUniqueString() + self.INT_SUB_NAME = self.getUniqueString() + # Create Exeternal Network + args = {'router:external': True} + net = self._create_network(self.EXT_NET_NAME, **args) + self.EXT_NET_ID = net.id + sub = self._create_subnet( + self.EXT_SUB_NAME, self.EXT_NET_ID, self.EXT_CIDR) + self.EXT_SUB_ID = sub.id + # Create Internal Network + net = self._create_network(self.INT_NET_NAME) + self.INT_NET_ID = net.id + sub = self._create_subnet( + self.INT_SUB_NAME, self.INT_NET_ID, self.INT_CIDR) + self.INT_SUB_ID = sub.id + # Create Router + args = {'external_gateway_info': {'network_id': self.EXT_NET_ID}} + sot = self.conn.network.create_router(name=self.ROT_NAME, **args) + assert isinstance(sot, router.Router) + self.assertEqual(self.ROT_NAME, sot.name) + self.ROT_ID = sot.id + self.ROT = sot + # Add Router's Interface to Internal Network + sot = self.ROT.add_interface( + self.conn.network, subnet_id=self.INT_SUB_ID) + self.assertEqual(sot['subnet_id'], self.INT_SUB_ID) + # Create Port in Internal Network + prt = self.conn.network.create_port(network_id=self.INT_NET_ID) + assert isinstance(prt, port.Port) + self.INTERNAL_PORT_ID = prt.id + self.INTERNAL_IP_ADDRESS = prt.fixed_ips[0]['ip_address'] + # Create Floating IP. + fip = self.conn.network.create_ip( + floating_network_id=self.EXT_NET_ID) + assert isinstance(fip, floating_ip.FloatingIP) + self.FIP_ID = fip.id + # Create Port Forwarding + pf = self.conn.network.create_port_forwarding( + floatingip_id=self.FIP_ID, + internal_port_id=self.INTERNAL_PORT_ID, + internal_ip_address=self.INTERNAL_IP_ADDRESS, + internal_port=self.INTERNAL_PORT, + external_port=self.EXTERNAL_PORT, + protocol=self.PROTOCOL) + assert isinstance(pf, _port_forwarding.PortForwarding) + self.PF = pf + + def tearDown(self): + sot = self.conn.network.delete_port_forwarding( + self.PF, self.FIP_ID, ignore_missing=False) + self.assertIsNone(sot) + sot = self.conn.network.delete_ip(self.FIP_ID, ignore_missing=False) + self.assertIsNone(sot) + sot = self.conn.network.delete_port( + self.INTERNAL_PORT_ID, ignore_missing=False) + self.assertIsNone(sot) + sot = self.ROT.remove_interface( + self.conn.network, subnet_id=self.INT_SUB_ID) + self.assertEqual(sot['subnet_id'], self.INT_SUB_ID) + sot = self.conn.network.delete_router( + self.ROT_ID, ignore_missing=False) + self.assertIsNone(sot) + sot = self.conn.network.delete_subnet( + self.EXT_SUB_ID, ignore_missing=False) + self.assertIsNone(sot) + sot = self.conn.network.delete_network( + self.EXT_NET_ID, ignore_missing=False) + self.assertIsNone(sot) + sot = self.conn.network.delete_subnet( + self.INT_SUB_ID, ignore_missing=False) + self.assertIsNone(sot) + sot = self.conn.network.delete_network( + self.INT_NET_ID, ignore_missing=False) + self.assertIsNone(sot) + super(TestPortForwarding, self).tearDown() + + def _create_network(self, name, **args): + self.name = name + net = self.conn.network.create_network(name=name, **args) + assert isinstance(net, network.Network) + self.assertEqual(self.name, net.name) + return net + + def _create_subnet(self, name, net_id, cidr): + self.name = name + self.net_id = net_id + self.cidr = cidr + sub = self.conn.network.create_subnet( + name=self.name, + ip_version=self.IPV4, + network_id=self.net_id, + cidr=self.cidr) + assert isinstance(sub, subnet.Subnet) + self.assertEqual(self.name, sub.name) + return sub + + def test_find(self): + sot = self.conn.network.find_port_forwarding( + self.PF.id, self.FIP_ID) + self.assertEqual(self.INTERNAL_PORT_ID, sot.internal_port_id) + self.assertEqual(self.INTERNAL_IP_ADDRESS, sot.internal_ip_address) + self.assertEqual(self.INTERNAL_PORT, sot.internal_port) + self.assertEqual(self.EXTERNAL_PORT, sot.external_port) + self.assertEqual(self.PROTOCOL, sot.protocol) + + def test_get(self): + sot = self.conn.network.get_port_forwarding( + self.PF, self.FIP_ID) + self.assertEqual(self.INTERNAL_PORT_ID, sot.internal_port_id) + self.assertEqual(self.INTERNAL_IP_ADDRESS, sot.internal_ip_address) + self.assertEqual(self.INTERNAL_PORT, sot.internal_port) + self.assertEqual(self.EXTERNAL_PORT, sot.external_port) + self.assertEqual(self.PROTOCOL, sot.protocol) + + def test_list(self): + pf_ids = [o.id for o in + self.conn.network.port_forwardings(self.FIP_ID)] + self.assertIn(self.PF.id, pf_ids) + + def test_update(self): + NEW_EXTERNAL_PORT = 90 + sot = self.conn.network.update_port_forwarding( + self.PF.id, + self.FIP_ID, + external_port=NEW_EXTERNAL_PORT) + self.assertEqual(NEW_EXTERNAL_PORT, sot.external_port) diff --git a/openstack/tests/unit/network/v2/test_port_forwarding.py b/openstack/tests/unit/network/v2/test_port_forwarding.py new file mode 100644 index 000000000..18a3397df --- /dev/null +++ b/openstack/tests/unit/network/v2/test_port_forwarding.py @@ -0,0 +1,52 @@ +# 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.tests.unit import base + +from openstack.network.v2 import port_forwarding + +EXAMPLE = { + 'id': 'pf_id', + 'protocol': 'tcp', + 'internal_ip_address': '1.2.3.4', + 'floatingip_id': 'floating-ip-uuid', + 'internal_port': 80, + 'internal_port_id': 'internal-port-uuid', + 'external_port': 8080, +} + + +class TestFloatingIP(base.TestCase): + + def test_basic(self): + sot = port_forwarding.PortForwarding() + self.assertEqual('port_forwarding', sot.resource_key) + self.assertEqual('port_forwardings', sot.resources_key) + self.assertEqual( + '/floatingips/%(floatingip_id)s/port_forwardings', + sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = port_forwarding.PortForwarding(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['floatingip_id'], sot.floatingip_id) + self.assertEqual(EXAMPLE['protocol'], sot.protocol) + self.assertEqual(EXAMPLE['internal_ip_address'], + sot.internal_ip_address) + self.assertEqual(EXAMPLE['internal_port'], sot.internal_port) + self.assertEqual(EXAMPLE['internal_port_id'], sot.internal_port_id) + self.assertEqual(EXAMPLE['external_port'], sot.external_port) From 1da367b53746ace13d90cf491ea25827b8369a3e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 7 Nov 2018 09:26:31 -0600 Subject: [PATCH 2296/3836] Slightly refactor vendor profile loading In prep for the next patch which will add support for url-based vendor profiles, rearrange a few bits of the vendor profile loading code. This should make the next patch easier to read. Change-Id: I3b18678895006249c0e958944c88f09a365daee0 --- openstack/config/loader.py | 58 ++++++++++++++-------------- openstack/config/vendors/__init__.py | 27 +++++++------ 2 files changed, 46 insertions(+), 39 deletions(-) diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 030fc029b..e5ac28a75 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -448,35 +448,37 @@ def _expand_vendor_profile(self, name, cloud, our_cloud): # Expand a profile if it exists. 'cloud' is an old confusing name # for this. profile_name = our_cloud.get('profile', our_cloud.get('cloud', None)) - if profile_name and profile_name != self.envvar_key: - if 'cloud' in our_cloud: - warnings.warn( - "{0} use the keyword 'cloud' to reference a known " - "vendor profile. This has been deprecated in favor of the " - "'profile' keyword.".format(self.config_filename)) - vendor_filename, vendor_file = self._load_vendor_file() - if vendor_file and profile_name in vendor_file['public-clouds']: - _auth_update(cloud, vendor_file['public-clouds'][profile_name]) + if not profile_name or profile_name == self.envvar_key: + return + if 'cloud' in our_cloud: + warnings.warn( + "{0} uses the keyword 'cloud' to reference a known " + "vendor profile. This has been deprecated in favor of the " + "'profile' keyword.".format(self.config_filename)) + + vendor_filename, vendor_file = self._load_vendor_file() + if vendor_file and profile_name in vendor_file['public-clouds']: + _auth_update(cloud, vendor_file['public-clouds'][profile_name]) + else: + profile_data = vendors.get_profile(profile_name) + if profile_data: + status = profile_data.pop('status', 'active') + message = profile_data.pop('message', '') + if status == 'deprecated': + warnings.warn( + "{profile_name} is deprecated: {message}".format( + profile_name=profile_name, message=message)) + elif status == 'shutdown': + raise exceptions.ConfigException( + "{profile_name} references a cloud that no longer" + " exists: {message}".format( + profile_name=profile_name, message=message)) + _auth_update(cloud, profile_data) else: - profile_data = vendors.get_profile(profile_name) - if profile_data: - status = profile_data.pop('status', 'active') - message = profile_data.pop('message', '') - if status == 'deprecated': - warnings.warn( - "{profile_name} is deprecated: {message}".format( - profile_name=profile_name, message=message)) - elif status == 'shutdown': - raise exceptions.ConfigException( - "{profile_name} references a cloud that no longer" - " exists: {message}".format( - profile_name=profile_name, message=message)) - _auth_update(cloud, profile_data) - else: - # Can't find the requested vendor config, go about business - warnings.warn("Couldn't find the vendor profile '{0}', for" - " the cloud '{1}'".format(profile_name, - name)) + # Can't find the requested vendor config, go about business + warnings.warn("Couldn't find the vendor profile '{0}', for" + " the cloud '{1}'".format(profile_name, + name)) def _project_scoped(self, cloud): return ('project_id' in cloud or 'project_name' in cloud diff --git a/openstack/config/vendors/__init__.py b/openstack/config/vendors/__init__.py index 3e1d20a5a..f46a64351 100644 --- a/openstack/config/vendors/__init__.py +++ b/openstack/config/vendors/__init__.py @@ -18,20 +18,25 @@ import yaml -_vendors_path = os.path.dirname(os.path.realpath(__file__)) -_vendor_defaults = None +_VENDORS_PATH = os.path.dirname(os.path.realpath(__file__)) +_VENDOR_DEFAULTS = {} -def get_profile(profile_name): - global _vendor_defaults - if _vendor_defaults is None: - _vendor_defaults = {} - for vendor in glob.glob(os.path.join(_vendors_path, '*.yaml')): +def _get_vendor_defaults(): + global _VENDOR_DEFAULTS + if not _VENDOR_DEFAULTS: + for vendor in glob.glob(os.path.join(_VENDORS_PATH, '*.yaml')): with open(vendor, 'r') as f: vendor_data = yaml.safe_load(f) - _vendor_defaults[vendor_data['name']] = vendor_data['profile'] - for vendor in glob.glob(os.path.join(_vendors_path, '*.json')): + _VENDOR_DEFAULTS[vendor_data['name']] = vendor_data['profile'] + for vendor in glob.glob(os.path.join(_VENDORS_PATH, '*.json')): with open(vendor, 'r') as f: vendor_data = json.load(f) - _vendor_defaults[vendor_data['name']] = vendor_data['profile'] - return _vendor_defaults.get(profile_name) + _VENDOR_DEFAULTS[vendor_data['name']] = vendor_data['profile'] + return _VENDOR_DEFAULTS + + +def get_profile(profile_name): + vendor_defaults = _get_vendor_defaults() + if profile_name in vendor_defaults: + return vendor_defaults[profile_name].copy() From 3d08643c43f9a23209636d14ae7ef41f5c8a22b7 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 7 Nov 2018 09:49:07 -0600 Subject: [PATCH 2297/3836] Support remote vendor profiles Maintaining vendor profile information inside of the sdk is great and all, but it winds up being problematic from a release management perspective since the data should always be current but people may not be in a position to upgrade their version of openstacksdk. It's also a less pleasing experience for people running or using clouds that the openstacksdk developers don't know about. RFC 5785 defines a scheme for serving data at well known URL locations. Use it to allow specifying a profile by URL instead of by name. For instance, for the cloud Example, a user could list profile: https://example.com and openstacksdk will fetch the profile from https://example.com/.well-known/openstack/api. It should be noted that sub-urls are not allowed, so it MUST be served off of a root domain. (That is, https://example.com/cloud is not allowed by the RFC) Clouds are not required to serve one of these. Change-Id: I884f62b35da5f29aa6e72e2dde9b8ec3ef48ad60 --- doc/source/user/config/configuration.rst | 5 ++- openstack/config/loader.py | 5 +++ openstack/config/vendors/__init__.py | 40 +++++++++++++++++++ openstack/config/vendors/vexxhost.json | 14 +------ openstack/tests/unit/config/test_config.py | 33 +++++++++++++++ .../remote-profile-100218d08b25019d.yaml | 7 ++++ 6 files changed, 89 insertions(+), 15 deletions(-) create mode 100644 releasenotes/notes/remote-profile-100218d08b25019d.yaml diff --git a/doc/source/user/config/configuration.rst b/doc/source/user/config/configuration.rst index 1cdd0ec10..a76ab1f73 100644 --- a/doc/source/user/config/configuration.rst +++ b/doc/source/user/config/configuration.rst @@ -84,7 +84,7 @@ An example config file is probably helpful: clouds: mtvexx: - profile: vexxhost + profile: https://vexxhost.com auth: username: mordred@inaugust.com password: XXXXXXXXX @@ -111,7 +111,8 @@ An example config file is probably helpful: You may note a few things. First, since `auth_url` settings are silly and embarrassingly ugly, known cloud vendor profile information is included and -may be referenced by name. One of the benefits of that is that `auth_url` +may be referenced by name or by base URL to the cloud in question if the +cloud serves a vendor profile. One of the benefits of that is that `auth_url` isn't the only thing the vendor defaults contain. For instance, since Rackspace lists `rax:database` as the service type for trove, `openstacksdk` knows that so that you don't have to. In case the cloud vendor profile is not diff --git a/openstack/config/loader.py b/openstack/config/loader.py index e5ac28a75..1451c6282 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -462,6 +462,11 @@ def _expand_vendor_profile(self, name, cloud, our_cloud): else: profile_data = vendors.get_profile(profile_name) if profile_data: + nested_profile = profile_data.pop('profile', None) + if nested_profile: + nested_profile_data = vendors.get_profile(nested_profile) + if nested_profile_data: + profile_data = nested_profile_data status = profile_data.pop('status', 'active') message = profile_data.pop('message', '') if status == 'deprecated': diff --git a/openstack/config/vendors/__init__.py b/openstack/config/vendors/__init__.py index f46a64351..bbe81b4ea 100644 --- a/openstack/config/vendors/__init__.py +++ b/openstack/config/vendors/__init__.py @@ -16,10 +16,16 @@ import json import os +from six.moves import urllib +import requests import yaml +from openstack.config import _util +from openstack import exceptions + _VENDORS_PATH = os.path.dirname(os.path.realpath(__file__)) _VENDOR_DEFAULTS = {} +_WELL_KNOWN_PATH = "{scheme}://{netloc}/.well-known/openstack/api" def _get_vendor_defaults(): @@ -40,3 +46,37 @@ def get_profile(profile_name): vendor_defaults = _get_vendor_defaults() if profile_name in vendor_defaults: return vendor_defaults[profile_name].copy() + profile_url = urllib.parse.urlparse(profile_name) + if not profile_url.netloc: + # This isn't a url, and we already don't have it. + return + well_known_url = _WELL_KNOWN_PATH.format( + scheme=profile_url.scheme, + netloc=profile_url.netloc, + ) + response = requests.get(well_known_url) + if not response.ok: + raise exceptions.ConfigException( + "{profile_name} is a remote profile that could not be fetched:" + " ({status_code) {reason}".format( + profile_name=profile_name, + status_code=response.status_code, + reason=response.reason)) + vendor_defaults[profile_name] = None + return + vendor_data = response.json() + name = vendor_data['name'] + # Merge named and url cloud config, but make named config override the + # config from the cloud so that we can supply local overrides if needed. + profile = _util.merge_clouds( + vendor_data['profile'], + vendor_defaults.get(name, {})) + # If there is (or was) a profile listed in a named config profile, it + # might still be here. We just merged in content from a URL though, so + # pop the key to prevent doing it again in the future. + profile.pop('profile', None) + # Save the data under both names so we don't reprocess this, no matter + # how we're called. + vendor_defaults[profile_name] = profile + vendor_defaults[name] = profile + return profile diff --git a/openstack/config/vendors/vexxhost.json b/openstack/config/vendors/vexxhost.json index 6ec6fd5c3..2f846068c 100644 --- a/openstack/config/vendors/vexxhost.json +++ b/openstack/config/vendors/vexxhost.json @@ -1,18 +1,6 @@ { "name": "vexxhost", "profile": { - "auth_type": "v3password", - "auth": { - "auth_url": "https://auth.vexxhost.net/v3" - }, - "regions": [ - "ca-ymq-1", - "sjc1" - ], - "dns_api_version": "1", - "identity_api_version": "3", - "image_format": "raw", - "floating_ip_source": "None", - "requires_floating_ip": false + "profile": "https://vexxhost.com" } } diff --git a/openstack/tests/unit/config/test_config.py b/openstack/tests/unit/config/test_config.py index bb134ddfe..2cd2702c1 100644 --- a/openstack/tests/unit/config/test_config.py +++ b/openstack/tests/unit/config/test_config.py @@ -96,6 +96,39 @@ def test_get_one_default_cloud_from_file(self): cc = c.get_one() self.assertEqual(cc.name, 'single') + def test_remote_profile(self): + single_conf = base._write_yaml({ + 'clouds': { + 'remote': { + 'profile': 'https://example.com', + 'auth': { + 'username': 'testuser', + 'password': 'testpass', + 'project_name': 'testproject', + }, + 'region_name': 'test-region', + } + } + }) + self.register_uris([ + dict(method='GET', + uri='https://example.com/.well-known/openstack/api', + json={ + "name": "example", + "profile": { + "auth": { + "auth_url": "https://auth.example.com/v3", + } + } + }), + ]) + + c = config.OpenStackConfig(config_files=[single_conf]) + cc = c.get_one(cloud='remote') + self.assertEqual(cc.name, 'remote') + self.assertEqual(cc.auth['auth_url'], 'https://auth.example.com/v3') + self.assertEqual(cc.auth['username'], 'testuser') + def test_get_one_auth_defaults(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml]) cc = c.get_one(cloud='_test-cloud_', auth={'username': 'user'}) diff --git a/releasenotes/notes/remote-profile-100218d08b25019d.yaml b/releasenotes/notes/remote-profile-100218d08b25019d.yaml new file mode 100644 index 000000000..5cfe09d6c --- /dev/null +++ b/releasenotes/notes/remote-profile-100218d08b25019d.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Vendor profiles can now be fetched from an RFC 5785 compliant URL on a + cloud, namely, ``https://example.com/.well-known/openstack/api``. A cloud + can manage their own vendor profile and serve it from that URL, allowing + a user to simply list ``https://example.com`` as the profile name. From e25e97720135dc3f59647758363edef24d6b8639 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 30 Nov 2018 09:26:02 +0100 Subject: [PATCH 2298/3836] Refactor tag support align different services to comply the same `tag` standard: https://specs.openstack.org/openstack/api-wg/guidelines/tags.html Currently not all services support tags, but we can prepare SDK for that. There are some changes/issues to know: - image.add_tag and image.remove_tag return the image entity - server.query_params for tags are renamed to be compliant with rest of services (the ones from network are reused) - cinder does support only setting of the tags, but not getting (API limitation) As of now the only services with tagging API available: - compute.server - network.{net, subnet, ip, port, qos_policy, router, sg, sg_rule, subnet_pool, trunk} - image.image - identity.project Change-Id: I7368c48baf414af325dad2b0579b094c803321bf --- openstack/compute/v2/server.py | 9 +- openstack/identity/v3/project.py | 3 +- openstack/image/v2/image.py | 12 +- openstack/network/v2/floating_ip.py | 5 +- openstack/network/v2/network.py | 5 +- openstack/network/v2/port.py | 5 +- openstack/network/v2/qos_policy.py | 5 +- openstack/network/v2/router.py | 5 +- openstack/network/v2/security_group.py | 5 +- openstack/network/v2/security_group_rule.py | 3 +- openstack/network/v2/subnet.py | 5 +- openstack/network/v2/subnet_pool.py | 5 +- openstack/network/v2/tag.py | 35 ---- openstack/network/v2/trunk.py | 3 +- openstack/resource.py | 113 ++++++++++++ .../tests/unit/compute/v2/test_server.py | 4 +- .../tests/unit/identity/v3/test_project.py | 4 + openstack/tests/unit/image/v2/test_image.py | 6 +- openstack/tests/unit/network/v2/test_tag.py | 56 ------ openstack/tests/unit/test_resource.py | 163 ++++++++++++++++++ 20 files changed, 313 insertions(+), 138 deletions(-) delete mode 100644 openstack/network/v2/tag.py delete mode 100644 openstack/tests/unit/network/v2/test_tag.py diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index c60a7962c..11294fb62 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -15,7 +15,7 @@ from openstack import utils -class Server(resource.Resource, metadata.MetadataMixin): +class Server(resource.Resource, metadata.MetadataMixin, resource.TagMixin): resource_key = 'server' resources_key = 'servers' base_path = '/servers' @@ -33,14 +33,13 @@ class Server(resource.Resource, metadata.MetadataMixin): "sort_key", "sort_dir", "reservation_id", "tags", "project_id", - tags_any="tags-any", - not_tags="not-tags", - not_tags_any="not-tags-any", is_deleted="deleted", ipv4_address="ip", ipv6_address="ip6", changes_since="changes-since", - all_projects="all_tenants") + all_projects="all_tenants", + **resource.TagMixin._tag_query_parameters + ) #: A list of dictionaries holding links relevant to this server. links = resource.Body('links') diff --git a/openstack/identity/v3/project.py b/openstack/identity/v3/project.py index 7f11cae17..5f5a741c2 100644 --- a/openstack/identity/v3/project.py +++ b/openstack/identity/v3/project.py @@ -14,7 +14,7 @@ from openstack import utils -class Project(resource.Resource): +class Project(resource.Resource, resource.TagMixin): resource_key = 'project' resources_key = 'projects' base_path = '/projects' @@ -33,6 +33,7 @@ class Project(resource.Resource): 'name', 'parent_id', is_enabled='enabled', + **resource.TagMixin._tag_query_parameters ) # Properties diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index 271f82d98..7889bc1d2 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -20,7 +20,7 @@ _logger = _log.setup_logging('openstack') -class Image(resource.Resource): +class Image(resource.Resource, resource.TagMixin): resources_key = 'images' base_path = '/images' @@ -232,16 +232,6 @@ def reactivate(self, session): """ self._action(session, "reactivate") - def add_tag(self, session, tag): - """Add a tag to an image""" - url = utils.urljoin(self.base_path, self.id, 'tags', tag) - session.put(url,) - - def remove_tag(self, session, tag): - """Remove a tag from an image""" - url = utils.urljoin(self.base_path, self.id, 'tags', tag) - session.delete(url,) - def upload(self, session): """Upload data into an existing image""" url = utils.urljoin(self.base_path, self.id, 'file') diff --git a/openstack/network/v2/floating_ip.py b/openstack/network/v2/floating_ip.py index 1511fb8d2..68a563fed 100644 --- a/openstack/network/v2/floating_ip.py +++ b/openstack/network/v2/floating_ip.py @@ -10,11 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network.v2 import tag from openstack import resource -class FloatingIP(resource.Resource, tag.TagMixin): +class FloatingIP(resource.Resource, resource.TagMixin): name_attribute = "floating_ip_address" resource_name = "floating ip" resource_key = 'floatingip' @@ -33,7 +32,7 @@ class FloatingIP(resource.Resource, tag.TagMixin): 'floating_ip_address', 'floating_network_id', 'port_id', 'router_id', 'status', 'subnet_id', project_id='tenant_id', - **tag.TagMixin._tag_query_parameters) + **resource.TagMixin._tag_query_parameters) # Properties #: Timestamp at which the floating IP was created. diff --git a/openstack/network/v2/network.py b/openstack/network/v2/network.py index 3ba20e2ed..3339c1f9e 100644 --- a/openstack/network/v2/network.py +++ b/openstack/network/v2/network.py @@ -10,11 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network.v2 import tag from openstack import resource -class Network(resource.Resource, tag.TagMixin): +class Network(resource.Resource, resource.TagMixin): resource_key = 'network' resources_key = 'networks' base_path = '/networks' @@ -39,7 +38,7 @@ class Network(resource.Resource, tag.TagMixin): provider_network_type='provider:network_type', provider_physical_network='provider:physical_network', provider_segmentation_id='provider:segmentation_id', - **tag.TagMixin._tag_query_parameters + **resource.TagMixin._tag_query_parameters ) # Properties diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index e3290aef2..7ad723b83 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -10,11 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network.v2 import tag from openstack import resource -class Port(resource.Resource, tag.TagMixin): +class Port(resource.Resource, resource.TagMixin): resource_key = 'port' resources_key = 'ports' base_path = '/ports' @@ -35,7 +34,7 @@ class Port(resource.Resource, tag.TagMixin): is_admin_state_up='admin_state_up', is_port_security_enabled='port_security_enabled', project_id='tenant_id', - **tag.TagMixin._tag_query_parameters + **resource.TagMixin._tag_query_parameters ) # Properties diff --git a/openstack/network/v2/qos_policy.py b/openstack/network/v2/qos_policy.py index 7efa40efc..162de8ed6 100644 --- a/openstack/network/v2/qos_policy.py +++ b/openstack/network/v2/qos_policy.py @@ -10,12 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network.v2 import tag from openstack import resource from openstack import utils -class QoSPolicy(resource.Resource, tag.TagMixin): +class QoSPolicy(resource.Resource, resource.TagMixin): resource_key = 'policy' resources_key = 'policies' base_path = '/qos/policies' @@ -31,7 +30,7 @@ class QoSPolicy(resource.Resource, tag.TagMixin): 'name', 'description', 'is_default', project_id='tenant_id', is_shared='shared', - **tag.TagMixin._tag_query_parameters + **resource.TagMixin._tag_query_parameters ) # Properties diff --git a/openstack/network/v2/router.py b/openstack/network/v2/router.py index 73db5b697..afe0f104b 100644 --- a/openstack/network/v2/router.py +++ b/openstack/network/v2/router.py @@ -10,12 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network.v2 import tag from openstack import resource from openstack import utils -class Router(resource.Resource, tag.TagMixin): +class Router(resource.Resource, resource.TagMixin): resource_key = 'router' resources_key = 'routers' base_path = '/routers' @@ -34,7 +33,7 @@ class Router(resource.Resource, tag.TagMixin): is_distributed='distributed', is_ha='ha', project_id='tenant_id', - **tag.TagMixin._tag_query_parameters + **resource.TagMixin._tag_query_parameters ) # Properties diff --git a/openstack/network/v2/security_group.py b/openstack/network/v2/security_group.py index e80b96c92..e4677c92e 100644 --- a/openstack/network/v2/security_group.py +++ b/openstack/network/v2/security_group.py @@ -10,11 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network.v2 import tag from openstack import resource -class SecurityGroup(resource.Resource, tag.TagMixin): +class SecurityGroup(resource.Resource, resource.TagMixin): resource_key = 'security_group' resources_key = 'security_groups' base_path = '/security-groups' @@ -29,7 +28,7 @@ class SecurityGroup(resource.Resource, tag.TagMixin): _query_mapping = resource.QueryParameters( 'description', 'name', project_id='tenant_id', - **tag.TagMixin._tag_query_parameters + **resource.TagMixin._tag_query_parameters ) # Properties diff --git a/openstack/network/v2/security_group_rule.py b/openstack/network/v2/security_group_rule.py index 54de7fdd2..4f10fbdb8 100644 --- a/openstack/network/v2/security_group_rule.py +++ b/openstack/network/v2/security_group_rule.py @@ -13,7 +13,7 @@ from openstack import resource -class SecurityGroupRule(resource.Resource): +class SecurityGroupRule(resource.Resource, resource.TagMixin): resource_key = 'security_group_rule' resources_key = 'security_group_rules' base_path = '/security-group-rules' @@ -30,6 +30,7 @@ class SecurityGroupRule(resource.Resource): 'remote_group_id', 'security_group_id', ether_type='ethertype', project_id='tenant_id', + **resource.TagMixin._tag_query_parameters ) diff --git a/openstack/network/v2/subnet.py b/openstack/network/v2/subnet.py index 4c9078acb..64eca90ce 100644 --- a/openstack/network/v2/subnet.py +++ b/openstack/network/v2/subnet.py @@ -10,11 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network.v2 import tag from openstack import resource -class Subnet(resource.Resource, tag.TagMixin): +class Subnet(resource.Resource, resource.TagMixin): resource_key = 'subnet' resources_key = 'subnets' base_path = '/subnets' @@ -35,7 +34,7 @@ class Subnet(resource.Resource, tag.TagMixin): project_id='tenant_id', subnet_pool_id='subnetpool_id', use_default_subnet_pool='use_default_subnetpool', - **tag.TagMixin._tag_query_parameters + **resource.TagMixin._tag_query_parameters ) # Properties diff --git a/openstack/network/v2/subnet_pool.py b/openstack/network/v2/subnet_pool.py index e7d186dd6..953aa44ca 100644 --- a/openstack/network/v2/subnet_pool.py +++ b/openstack/network/v2/subnet_pool.py @@ -10,11 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network.v2 import tag from openstack import resource -class SubnetPool(resource.Resource, tag.TagMixin): +class SubnetPool(resource.Resource, resource.TagMixin): resource_key = 'subnetpool' resources_key = 'subnetpools' base_path = '/subnetpools' @@ -31,7 +30,7 @@ class SubnetPool(resource.Resource, tag.TagMixin): 'name', is_shared='shared', project_id='tenant_id', - **tag.TagMixin._tag_query_parameters + **resource.TagMixin._tag_query_parameters ) # Properties diff --git a/openstack/network/v2/tag.py b/openstack/network/v2/tag.py deleted file mode 100644 index f59717b28..000000000 --- a/openstack/network/v2/tag.py +++ /dev/null @@ -1,35 +0,0 @@ -# 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 resource -from openstack import utils - - -class TagMixin(object): - - _tag_query_parameters = { - 'tags': 'tags', - 'any_tags': 'tags-any', - 'not_tags': 'not-tags', - 'not_any_tags': 'not-tags-any', - } - - #: A list of associated tags - #: *Type: list of tag strings* - tags = resource.Body('tags', type=list, default=[]) - - def set_tags(self, session, tags): - url = utils.urljoin(self.base_path, self.id, 'tags') - session.put(url, - json={'tags': tags}) - self._body.attributes.update({'tags': tags}) - return self diff --git a/openstack/network/v2/trunk.py b/openstack/network/v2/trunk.py index 0168f9933..db672bd65 100644 --- a/openstack/network/v2/trunk.py +++ b/openstack/network/v2/trunk.py @@ -14,7 +14,7 @@ from openstack import utils -class Trunk(resource.Resource): +class Trunk(resource.Resource, resource.TagMixin): resource_key = 'trunk' resources_key = 'trunks' base_path = '/trunks' @@ -30,6 +30,7 @@ class Trunk(resource.Resource): 'name', 'description', 'port_id', 'status', 'sub_ports', project_id='tenant_id', is_admin_state_up='admin_state_up', + **resource.TagMixin._tag_query_parameters ) # Properties diff --git a/openstack/resource.py b/openstack/resource.py index dd645679a..2527bbd91 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1413,6 +1413,119 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): "No %s found for %s" % (cls.__name__, name_or_id)) +class TagMixin(object): + + _tag_query_parameters = { + 'tags': 'tags', + 'any_tags': 'tags-any', + 'not_tags': 'not-tags', + 'not_any_tags': 'not-tags-any', + } + + #: A list of associated tags + #: *Type: list of tag strings* + tags = Body('tags', type=list, default=[]) + + def fetch_tags(self, session): + """Lists tags set on the entity. + + :param session: The session to use for making this request. + :return: The list with tags attached to the entity + """ + url = utils.urljoin(self.base_path, self.id, 'tags') + session = self._get_session(session) + response = session.get(url) + exceptions.raise_from_response(response) + # NOTE(gtema): since this is a common method + # we can't rely on the resource_key, because tags are returned + # without resource_key. Do parse response here + json = response.json() + if 'tags' in json: + self._body.attributes.update({'tags': json['tags']}) + return self + + def set_tags(self, session, tags=[]): + """Sets/Replaces all tags on the resource. + + :param session: The session to use for making this request. + :param list tags: List with tags to be set on the resource + """ + url = utils.urljoin(self.base_path, self.id, 'tags') + session = self._get_session(session) + response = session.put(url, json={'tags': tags}) + exceptions.raise_from_response(response) + self._body.attributes.update({'tags': tags}) + return self + + def remove_all_tags(self, session): + """Removes all tags on the entity. + + :param session: The session to use for making this request. + """ + url = utils.urljoin(self.base_path, self.id, 'tags') + session = self._get_session(session) + response = session.delete(url) + exceptions.raise_from_response(response) + self._body.attributes.update({'tags': []}) + return self + + def check_tag(self, session, tag): + """Checks if tag exists on the entity. + + If the tag does not exist a 404 will be returned + + :param session: The session to use for making this request. + :param tag: The tag as a string. + """ + url = utils.urljoin(self.base_path, self.id, 'tags', tag) + session = self._get_session(session) + response = session.get(url) + exceptions.raise_from_response(response, + error_message='Tag does not exist') + return self + + def add_tag(self, session, tag): + """Adds a single tag to the resource. + + :param session: The session to use for making this request. + :param tag: The tag as a string. + """ + url = utils.urljoin(self.base_path, self.id, 'tags', tag) + session = self._get_session(session) + response = session.put(url) + exceptions.raise_from_response(response) + # we do not want to update tags directly + tags = self.tags + tags.append(tag) + self._body.attributes.update({ + 'tags': tags + }) + return self + + def remove_tag(self, session, tag): + """Removes a single tag from the specified server. + + :param session: The session to use for making this request. + :param tag: The tag as a string. + """ + url = utils.urljoin(self.base_path, self.id, 'tags', tag) + session = self._get_session(session) + response = session.delete(url) + exceptions.raise_from_response(response) + # we do not want to update tags directly + tags = self.tags + try: + # NOTE(gtema): if tags were not fetched, but request suceeded + # it is ok. Just ensure tag does not exist locally + tags.remove(tag) + except ValueError: + pass # do nothing! + self._body.attributes.update({ + 'tags': tags + }) + return self + + def _normalize_status(status): if status is not None: status = status.lower() diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index a39e937c0..b95b0ede0 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -93,9 +93,9 @@ def test_basic(self): "reservation_id": "reservation_id", "project_id": "project_id", "tags": "tags", - "tags_any": "tags-any", + "any_tags": "tags-any", "not_tags": "not-tags", - "not_tags_any": "not-tags-any", + "not_any_tags": "not-tags-any", "is_deleted": "deleted", "ipv4_address": "ip", "ipv6_address": "ip6", diff --git a/openstack/tests/unit/identity/v3/test_project.py b/openstack/tests/unit/identity/v3/test_project.py index 6f2c1d145..95c0ad22d 100644 --- a/openstack/tests/unit/identity/v3/test_project.py +++ b/openstack/tests/unit/identity/v3/test_project.py @@ -49,6 +49,10 @@ def test_basic(self): 'is_enabled': 'enabled', 'limit': 'limit', 'marker': 'marker', + 'tags': 'tags', + 'any_tags': 'tags-any', + 'not_tags': 'not-tags', + 'not_any_tags': 'not-tags-any', }, sot._query_mapping._mapping) diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 84ce95619..f9097db73 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -103,6 +103,8 @@ def setUp(self): self.resp.json = mock.Mock(return_value=self.resp.body) self.sess = mock.Mock(spec=adapter.Adapter) self.sess.post = mock.Mock(return_value=self.resp) + self.sess.put = mock.Mock(return_value=FakeResponse({})) + self.sess.delete = mock.Mock(return_value=FakeResponse({})) self.sess.default_microversion = None self.sess.retriable_status_codes = None @@ -197,7 +199,7 @@ def test_add_tag(self): sot = image.Image(**EXAMPLE) tag = "lol" - self.assertIsNone(sot.add_tag(self.sess, tag)) + sot.add_tag(self.sess, tag) self.sess.put.assert_called_with( 'images/IDENTIFIER/tags/%s' % tag, ) @@ -206,7 +208,7 @@ def test_remove_tag(self): sot = image.Image(**EXAMPLE) tag = "lol" - self.assertIsNone(sot.remove_tag(self.sess, tag)) + sot.remove_tag(self.sess, tag) self.sess.delete.assert_called_with( 'images/IDENTIFIER/tags/%s' % tag, ) diff --git a/openstack/tests/unit/network/v2/test_tag.py b/openstack/tests/unit/network/v2/test_tag.py deleted file mode 100644 index fdfb8d51d..000000000 --- a/openstack/tests/unit/network/v2/test_tag.py +++ /dev/null @@ -1,56 +0,0 @@ -# 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 inspect -import mock -from openstack.tests.unit import base - -from openstack.network.v2 import network -import openstack.network.v2 as network_resources -from openstack.network.v2.tag import TagMixin - -ID = 'IDENTIFIER' - - -class TestTag(base.TestCase): - - @staticmethod - def _create_network_resource(tags=None): - tags = tags or [] - return network.Network(id=ID, name='test-net', tags=tags) - - def test_tags_attribute(self): - net = self._create_network_resource() - self.assertTrue(hasattr(net, 'tags')) - self.assertIsInstance(net.tags, list) - - def test_set_tags(self): - net = self._create_network_resource() - sess = mock.Mock() - result = net.set_tags(sess, ['blue', 'green']) - # Check tags attribute is updated - self.assertEqual(['blue', 'green'], net.tags) - # Check the passed resource is returned - self.assertEqual(net, result) - url = 'networks/' + ID + '/tags' - sess.put.assert_called_once_with(url, - json={'tags': ['blue', 'green']}) - - def test_tagged_resource_always_created_with_empty_tag_list(self): - for _, module in inspect.getmembers(network_resources, - inspect.ismodule): - for _, resource in inspect.getmembers(module, inspect.isclass): - if issubclass(resource, TagMixin) and resource != TagMixin: - x_resource = resource.new( - id="%s_ID" % resource.resource_key.upper()) - self.assertIsNotNone(x_resource.tags) - self.assertEqual(x_resource.tags, list()) diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 2719caf4a..f7c4138dc 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -2215,3 +2215,166 @@ def test_none(self, mock_get_ver): self.res._assert_microversion_for, self.session, 'fetch', '1.6') mock_get_ver.assert_called_once_with(self.res, self.session, 'fetch') + + +class TestTagMixin(base.TestCase): + + def setUp(self): + super(TestTagMixin, self).setUp() + + self.service_name = "service" + self.base_path = "base_path" + + class Test(resource.Resource, resource.TagMixin): + service = self.service_name + base_path = self.base_path + resources_key = 'resources' + allow_create = True + allow_fetch = True + allow_head = True + allow_commit = True + allow_delete = True + allow_list = True + + self.test_class = Test + + self.request = mock.Mock(spec=resource._Request) + self.request.url = "uri" + self.request.body = "body" + self.request.headers = "headers" + + self.response = FakeResponse({}) + + self.sot = Test.new(id="id", tags=[]) + self.sot._prepare_request = mock.Mock(return_value=self.request) + self.sot._translate_response = mock.Mock() + + self.session = mock.Mock(spec=adapter.Adapter) + self.session.get = mock.Mock(return_value=self.response) + self.session.put = mock.Mock(return_value=self.response) + self.session.delete = mock.Mock(return_value=self.response) + + def test_tags_attribute(self): + res = self.sot + self.assertTrue(hasattr(res, 'tags')) + self.assertIsInstance(res.tags, list) + + def test_fetch_tags(self): + res = self.sot + sess = self.session + + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.links = {} + mock_response.json.return_value = {'tags': ['blue1', 'green1']} + + sess.get.side_effect = [mock_response] + + result = res.fetch_tags(sess) + # Check tags attribute is updated + self.assertEqual(['blue1', 'green1'], res.tags) + # Check the passed resource is returned + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/tags' + sess.get.assert_called_once_with(url) + + def test_set_tags(self): + res = self.sot + sess = self.session + + # Set some initial value to check rewrite + res.tags.extend(['blue_old', 'green_old']) + + result = res.set_tags(sess, ['blue', 'green']) + # Check tags attribute is updated + self.assertEqual(['blue', 'green'], res.tags) + # Check the passed resource is returned + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/tags' + sess.put.assert_called_once_with( + url, + json={'tags': ['blue', 'green']} + ) + + def test_remove_all_tags(self): + res = self.sot + sess = self.session + + # Set some initial value to check removal + res.tags.extend(['blue_old', 'green_old']) + + result = res.remove_all_tags(sess) + # Check tags attribute is updated + self.assertEqual([], res.tags) + # Check the passed resource is returned + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/tags' + sess.delete.assert_called_once_with(url) + + def test_remove_single_tag(self): + res = self.sot + sess = self.session + + res.tags.extend(['blue', 'dummy']) + + result = res.remove_tag(sess, 'dummy') + # Check tags attribute is updated + self.assertEqual(['blue'], res.tags) + # Check the passed resource is returned + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/tags/dummy' + sess.delete.assert_called_once_with(url) + + def test_check_tag_exists(self): + res = self.sot + sess = self.session + + sess.get.side_effect = [FakeResponse(None, 202)] + + result = res.check_tag(sess, 'blue') + # Check tags attribute is updated + self.assertEqual([], res.tags) + # Check the passed resource is returned + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/tags/blue' + sess.get.assert_called_once_with(url) + + def test_check_tag_not_exists(self): + res = self.sot + sess = self.session + + mock_response = mock.Mock() + mock_response.status_code = 404 + mock_response.links = {} + mock_response.content = None + + sess.get.side_effect = [mock_response] + + # ensure we get 404 + self.assertRaises( + exceptions.NotFoundException, + res.check_tag, + sess, + 'dummy', + ) + + def test_add_tag(self): + res = self.sot + sess = self.session + + # Set some initial value to check add + res.tags.extend(['blue', 'green']) + + result = res.add_tag(sess, 'lila') + # Check tags attribute is updated + self.assertEqual(['blue', 'green', 'lila'], res.tags) + # Check the passed resource is returned + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/tags/lila' + sess.put.assert_called_once_with(url) + + def test_tagged_resource_always_created_with_empty_tag_list(self): + res = self.sot + + self.assertIsNotNone(res.tags) + self.assertEqual(res.tags, list()) From 8d71f764e4f9712464306a489ef601c502bf220d Mon Sep 17 00:00:00 2001 From: Lars Kellogg-Stedman Date: Fri, 16 Nov 2018 07:43:41 -0500 Subject: [PATCH 2299/3836] syntax errors and undefined exceptions in service_description.py openstack/service_description.py had several fatal errors: - The _make_proxy method attempted to reference a variable named api_version when no such variable was in scope. - There was a syntax error in the associated format string. - There were multiple references to an UnsupportedVersion exception when no such exception exists. This commit corrects all of the above. Change-Id: If7826913883c34d0a1b09efef1d7f74c90bccdee --- openstack/service_description.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/openstack/service_description.py b/openstack/service_description.py index 0e669b182..5abc14b00 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -118,7 +118,7 @@ def _make_proxy(self, instance): " have direct passthrough REST capabilities.".format( version=version_string, service_type=self.service_type), - category=exceptions.UnsupportedVersionWarning) + category=exceptions.UnsupportedServiceVersion) elif endpoint_override and self.supported_versions: temp_adapter = config.get_session_client( self.service_type @@ -133,14 +133,14 @@ def _make_proxy(self, instance): ) else: warnings.warn( - "Service {service_type) has an endpoint override set" + "Service {service_type} has an endpoint override set" " but the version discovered at that endpoint, {version}" " is not supported by openstacksdk. The resulting Proxy" " object will only have direct passthrough REST" " capabilities.".format( version=api_version, service_type=self.service_type), - category=exceptions.UnsupportedVersionWarning) + category=exceptions.UnsupportedServiceVersion) if proxy_obj: @@ -193,12 +193,11 @@ def _make_proxy(self, instance): # service catalog that also doesn't have any useful # version discovery? warnings.warn( - "Service {service_type) has no discoverable version." + "Service {service_type} has no discoverable version." " The resulting Proxy object will only have direct" " passthrough REST capabilities.".format( - version=api_version, service_type=self.service_type), - category=exceptions.UnsupportedVersionWarning) + category=exceptions.UnsupportedServiceVersion) return temp_adapter proxy_class = self.supported_versions.get(str(found_version[0])) if not proxy_class: From 9939c93011ae176dac94aadcff81aa6e4cae36ed Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 30 Nov 2018 17:25:51 -0600 Subject: [PATCH 2300/3836] Transform server with munch before normalizing The latest release of openstacksdk broke an interface where server.flavor.id had previously worked, not server.flavor is a dict. munch.Munch munches subdicts as well, so when ret['flavor'] = server.pop('flavor') is run with just a pure dict, we now wind up with just a pure dict in the server.flavor field. Run server.to_dict() through a munch constructor so that sub-dicts are properly munch now. As a followup, once we're returning Resource objects here, flavor should wind up being an openstack.compute.v2.flavor.Flavor - so that the interface remains the same. Change-Id: I600ed8c518af9c3a441902a6921f508ff20a079d --- openstack/cloud/openstackcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 509f56496..4e06fa108 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -2141,7 +2141,7 @@ def _list_servers(self, detailed=False, all_projects=False, bare=False, filters=None): filters = filters or {} servers = [ - self._normalize_server(server.to_dict()) + self._normalize_server(munch.Munch(server.to_dict())) for server in self.compute.servers( all_projects=all_projects, **filters)] return [ From 120a9d1d03d71aff76bda9a33b85d12ec25e2094 Mon Sep 17 00:00:00 2001 From: Yang JianFeng Date: Thu, 9 Aug 2018 08:54:55 +0000 Subject: [PATCH 2301/3836] Add support for octavia's resuorces quota This patch adds some methods for octavia quota. Story: 2003379 Task: 24454 Change-Id: Ia9ef9f9ecd5b47224794766a400c40682447671a --- openstack/load_balancer/v2/_proxy.py | 66 +++++++++++++++ openstack/load_balancer/v2/quota.py | 62 ++++++++++++++ .../load_balancer/v2/test_load_balancer.py | 32 ++++++++ .../tests/unit/load_balancer/test_proxy.py | 22 +++++ .../tests/unit/load_balancer/test_quota.py | 81 +++++++++++++++++++ 5 files changed, 263 insertions(+) create mode 100644 openstack/load_balancer/v2/quota.py create mode 100644 openstack/tests/unit/load_balancer/test_quota.py diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index ab843e5f5..625edcb90 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -17,6 +17,7 @@ from openstack.load_balancer.v2 import load_balancer as _lb from openstack.load_balancer.v2 import member as _member from openstack.load_balancer.v2 import pool as _pool +from openstack.load_balancer.v2 import quota as _quota from openstack import proxy from openstack import resource @@ -680,3 +681,68 @@ def update_l7_rule(self, l7rule, l7_policy, **attrs): l7policyobj = self._get_resource(_l7policy.L7Policy, l7_policy) return self._update(_l7rule.L7Rule, l7rule, l7policy_id=l7policyobj.id, **attrs) + + def quotas(self, **query): + """Return a generator of quotas + + :param dict query: Optional query parameters to be sent to limit + the resources being returned. Currently no query + parameter is supported. + + :returns: A generator of quota objects + :rtype: :class:`~openstack.load_balancer.v2.quota.Quota` + """ + return self._list(_quota.Quota, paginated=False, **query) + + def get_quota(self, quota): + """Get a quota + + :param quota: The value can be the ID of a quota or a + :class:`~openstack.load_balancer.v2.quota.Quota` + instance. The ID of a quota is the same as the project + ID for the quota. + + :returns: One :class:`~openstack.load_balancer.v2.quota.Quota` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_quota.Quota, quota) + + def update_quota(self, quota, **attrs): + """Update a quota + + :param quota: Either the ID of a quota or a + :class:`~openstack.load_balancer.v2.quota.Quota` + instance. The ID of a quota is the same as the + project ID for the quota. + :param dict attrs: The attributes to update on the quota represented + by ``quota``. + + :returns: The updated quota + :rtype: :class:`~openstack.load_balancer.v2.quota.Quota` + """ + return self._update(_quota.Quota, quota, **attrs) + + def get_quota_default(self): + """Get a default quota + + :returns: One :class:`~openstack.load_balancer.v2.quota.QuotaDefault` + """ + return self._get(_quota.QuotaDefault, requires_id=False) + + def delete_quota(self, quota, ignore_missing=True): + """Delete a quota (i.e. reset to the default quota) + + :param quota: The value can be either the ID of a quota or a + :class:`~openstack.load_balancer.v2.quota.Quota` + instance. The ID of a quota is the same as the + project ID for the quota. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when quota does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent quota. + + :returns: ``None`` + """ + self._delete(_quota.Quota, quota, ignore_missing=ignore_missing) diff --git a/openstack/load_balancer/v2/quota.py b/openstack/load_balancer/v2/quota.py new file mode 100644 index 000000000..77799f7d1 --- /dev/null +++ b/openstack/load_balancer/v2/quota.py @@ -0,0 +1,62 @@ +# Copyright (c) 2018 China Telecom Corporation +# 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 openstack import resource + + +class Quota(resource.Resource): + resource_key = 'quota' + resources_key = 'quotas' + base_path = '/lbaas/quotas' + + # capabilities + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + # Properties + #: The maximum amount of load balancers you can have. *Type: int* + load_balancers = resource.Body('load_balancer', type=int) + #: The maximum amount of listeners you can create. *Type: int* + listeners = resource.Body('listener', type=int) + #: The maximum amount of pools you can create. *Type: int* + pools = resource.Body('pool', type=int) + #: The maximum amount of health monitors you can create. *Type: int* + health_monitors = resource.Body('health_monitor', type=int) + #: The maximum amount of members you can create. *Type: int* + members = resource.Body('member', type=int) + #: The ID of the project this quota is associated with. + project_id = resource.Body('project_id', alternate_id=True) + + def _prepare_request(self, requires_id=True, prepend_key=False): + _request = super(Quota, self)._prepare_request(requires_id, + prepend_key) + if self.resource_key in _request.body: + _body = _request.body[self.resource_key] + else: + _body = _request.body + if 'id' in _body: + del _body['id'] + return _request + + +class QuotaDefault(Quota): + base_path = '/lbaas/quotas/defaults' + + allow_retrieve = True + allow_commit = False + allow_delete = False + allow_list = False diff --git a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py index e2bab7fcb..f8f30aac7 100644 --- a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py +++ b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py @@ -19,6 +19,7 @@ from openstack.load_balancer.v2 import load_balancer from openstack.load_balancer.v2 import member from openstack.load_balancer.v2 import pool +from openstack.load_balancer.v2 import quota from openstack.tests.functional import base @@ -72,6 +73,14 @@ def setUp(self): subnets = list(self.conn.network.subnets()) self.VIP_SUBNET_ID = subnets[0].id self.PROJECT_ID = self.conn.session.get_project_id() + test_quota = self.conn.load_balancer.update_quota( + self.PROJECT_ID, **{'load_balancer': 100, + 'pool': 100, + 'listener': 100, + 'health_monitor': 100, + 'member': 100}) + assert isinstance(test_quota, quota.Quota) + self.assertEqual(self.PROJECT_ID, test_quota.id) test_lb = self.conn.load_balancer.create_load_balancer( name=self.LB_NAME, vip_subnet_id=self.VIP_SUBNET_ID, project_id=self.PROJECT_ID) @@ -145,6 +154,9 @@ def tearDown(self): self.conn.load_balancer.wait_for_load_balancer( self.LB_ID, wait=self._wait_for_timeout) + self.conn.load_balancer.delete_quota(self.PROJECT_ID, + ignore_missing=False) + self.conn.load_balancer.delete_l7_rule( self.L7RULE_ID, l7_policy=self.L7POLICY_ID, ignore_missing=False) self.conn.load_balancer.wait_for_load_balancer( @@ -417,3 +429,23 @@ def test_l7_rule_update(self): test_l7_rule = self.conn.load_balancer.get_l7_rule( self.L7RULE_ID, l7_policy=self.L7POLICY_ID,) self.assertEqual(self.L7RULE_VALUE, test_l7_rule.rule_value) + + def test_quota_list(self): + for qot in self.conn.load_balancer.quotas(): + self.assertIsNotNone(qot.project_id) + + def test_quota_get(self): + test_quota = self.conn.load_balancer.get_quota(self.PROJECT_ID) + self.assertEqual(self.PROJECT_ID, test_quota.id) + + def test_quota_update(self): + attrs = {'load_balancer': 12345, 'pool': 67890} + for project_quota in self.conn.load_balancer.quotas(): + self.conn.load_balancer.update_quota(project_quota, **attrs) + new_quota = self.conn.load_balancer.get_quota( + project_quota.project_id) + self.assertEqual(12345, new_quota.load_balancers) + self.assertEqual(67890, new_quota.pools) + + def test_default_quota(self): + self.conn.load_balancer.get_quota_default() diff --git a/openstack/tests/unit/load_balancer/test_proxy.py b/openstack/tests/unit/load_balancer/test_proxy.py index d2c84e155..143f691d2 100644 --- a/openstack/tests/unit/load_balancer/test_proxy.py +++ b/openstack/tests/unit/load_balancer/test_proxy.py @@ -21,6 +21,7 @@ from openstack.load_balancer.v2 import load_balancer as lb from openstack.load_balancer.v2 import member from openstack.load_balancer.v2 import pool +from openstack.load_balancer.v2 import quota from openstack import proxy as proxy_base from openstack.tests.unit import test_proxy_base @@ -268,3 +269,24 @@ def test_l7_rule_update(self): method_args=["RULE", self.L7_POLICY_ID], expected_args=[l7_rule.L7Rule, "RULE"], expected_kwargs={"l7policy_id": self.L7_POLICY_ID}) + + def test_quotas(self): + self.verify_list(self.proxy.quotas, quota.Quota, paginated=False) + + def test_quota_get(self): + self.verify_get(self.proxy.get_quota, quota.Quota) + + def test_quota_update(self): + self.verify_update(self.proxy.update_quota, quota.Quota) + + def test_quota_default_get(self): + self._verify2("openstack.proxy.Proxy._get", + self.proxy.get_quota_default, + expected_args=[quota.QuotaDefault], + expected_kwargs={'requires_id': False}) + + def test_quota_delete(self): + self.verify_delete(self.proxy.delete_quota, quota.Quota, False) + + def test_quota_delete_ignore(self): + self.verify_delete(self.proxy.delete_quota, quota.Quota, True) diff --git a/openstack/tests/unit/load_balancer/test_quota.py b/openstack/tests/unit/load_balancer/test_quota.py new file mode 100644 index 000000000..c9b01aeef --- /dev/null +++ b/openstack/tests/unit/load_balancer/test_quota.py @@ -0,0 +1,81 @@ +# Copyright (c) 2018 China Telecom Corporation +# 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 openstack.tests.unit import base + +from openstack.load_balancer.v2 import quota + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'load_balancer': 1, + 'listener': 2, + 'pool': 3, + 'health_monitor': 4, + 'member': 5, + 'project_id': 6, +} + + +class TestQuota(base.TestCase): + + def test_basic(self): + sot = quota.Quota() + self.assertEqual('quota', sot.resource_key) + self.assertEqual('quotas', sot.resources_key) + self.assertEqual('/lbaas/quotas', sot.base_path) + self.assertFalse(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = quota.Quota(**EXAMPLE) + self.assertEqual(EXAMPLE['load_balancer'], sot.load_balancers) + self.assertEqual(EXAMPLE['listener'], sot.listeners) + self.assertEqual(EXAMPLE['pool'], sot.pools) + self.assertEqual(EXAMPLE['health_monitor'], sot.health_monitors) + self.assertEqual(EXAMPLE['member'], sot.members) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + + def test_prepare_request(self): + body = {'id': 'ABCDEFGH', 'load_balancer': '12345'} + quota_obj = quota.Quota(**body) + response = quota_obj._prepare_request() + self.assertNotIn('id', response) + + +class TestQuotaDefault(base.TestCase): + + def test_basic(self): + sot = quota.QuotaDefault() + self.assertEqual('quota', sot.resource_key) + self.assertEqual('quotas', sot.resources_key) + self.assertEqual('/lbaas/quotas/defaults', sot.base_path) + self.assertFalse(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_delete) + self.assertFalse(sot.allow_list) + self.assertTrue(sot.allow_retrieve) + + def test_make_it(self): + sot = quota.Quota(**EXAMPLE) + self.assertEqual(EXAMPLE['load_balancer'], sot.load_balancers) + self.assertEqual(EXAMPLE['listener'], sot.listeners) + self.assertEqual(EXAMPLE['pool'], sot.pools) + self.assertEqual(EXAMPLE['health_monitor'], sot.health_monitors) + self.assertEqual(EXAMPLE['member'], sot.members) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) From 3ed8f0e9fae854979559763fe2e37d9d33fbabd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A9ri=20Le=20Bouder?= Date: Sat, 1 Dec 2018 17:00:04 -0500 Subject: [PATCH 2302/3836] object_store: exposes the prefix parameter The prefix parameter can be used to filter the list of containers or objects. Without it, its mostly impossible to handle container with large amount of objects. Change-Id: Id39cedd3d7a7e402e325ac476d0c80bb42aa4a4d --- openstack/cloud/openstackcloud.py | 14 +++++++++----- openstack/object_store/v1/container.py | 4 ++++ openstack/object_store/v1/obj.py | 4 ++++ .../tests/functional/cloud/test_object.py | 6 ++++++ openstack/tests/unit/cloud/test_object.py | 19 +++++++++++++++++++ 5 files changed, 42 insertions(+), 5 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 4e06fa108..595fc848d 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -7412,7 +7412,7 @@ def delete_server_group(self, name_or_id): return True - def list_containers(self, full_listing=True): + def list_containers(self, full_listing=True, prefix=None): """List containers. :param full_listing: Ignored. Present for backwards compat @@ -7421,7 +7421,8 @@ def list_containers(self, full_listing=True): :raises: OpenStackCloudException on operation error. """ - response = self.object_store.get('/', params=dict(format='json')) + params = dict(format='json', prefix=prefix) + response = self.object_store.get('/', params=params) return self._get_and_munchify(None, _adapter._json_response(response)) def search_containers(self, name=None, filters=None): @@ -7971,18 +7972,21 @@ def update_object(self, container, name, metadata=None, **headers): container=container, object=name), headers=headers) - def list_objects(self, container, full_listing=True): + def list_objects(self, container, full_listing=True, prefix=None): """List objects. :param container: Name of the container to list objects in. :param full_listing: Ignored. Present for backwards compat + :param string prefix: + only objects with this prefix will be returned. + (optional) :returns: list of Munch of the objects :raises: OpenStackCloudException on operation error. """ - data = self._object_store_client.get( - container, params=dict(format='json')) + params = dict(format='json', prefix=prefix) + data = self._object_store_client.get(container, params=params) return self._get_and_munchify(None, data) def search_objects(self, container, name=None, filters=None): diff --git a/openstack/object_store/v1/container.py b/openstack/object_store/v1/container.py index 8f90c730a..d45bcd864 100644 --- a/openstack/object_store/v1/container.py +++ b/openstack/object_store/v1/container.py @@ -37,6 +37,10 @@ class Container(_base.BaseResource): allow_list = True allow_head = True + _query_mapping = resource.QueryParameters( + 'prefix', + ) + # Container body data (when id=None) #: The name of the container. name = resource.Body("name", alternate_id=True, alias='id') diff --git a/openstack/object_store/v1/obj.py b/openstack/object_store/v1/obj.py index e77588f0d..41daa9d28 100644 --- a/openstack/object_store/v1/obj.py +++ b/openstack/object_store/v1/obj.py @@ -39,6 +39,10 @@ class Object(_base.BaseResource): allow_list = True allow_head = True + _query_mapping = resource.QueryParameters( + 'prefix', + ) + # Data to be passed during a POST call to create an object on the server. # TODO(mordred) Make a base class BaseDataResource that can be used here # and with glance images that has standard overrides for dealing with diff --git a/openstack/tests/functional/cloud/test_object.py b/openstack/tests/functional/cloud/test_object.py index 1f17013c0..fab28d36f 100644 --- a/openstack/tests/functional/cloud/test_object.py +++ b/openstack/tests/functional/cloud/test_object.py @@ -42,6 +42,8 @@ def test_create_object(self): self.user_cloud.create_container(container_name) self.assertEqual(container_name, self.user_cloud.list_containers()[0]['name']) + self.assertEqual([], + self.user_cloud.list_containers(prefix='somethin')) sizes = ( (64 * 1024, 1), # 64K, one segment (64 * 1024, 5) # 64MB, 5 segments @@ -90,6 +92,10 @@ def test_create_object(self): self.assertEqual( name, self.user_cloud.list_objects(container_name)[0]['name']) + self.assertEqual( + [], + self.user_cloud.list_objects(container_name, + prefix='abc')) self.assertTrue( self.user_cloud.delete_object(container_name, name)) self.assertEqual([], self.user_cloud.list_objects(container_name)) diff --git a/openstack/tests/unit/cloud/test_object.py b/openstack/tests/unit/cloud/test_object.py index 762dd7412..90654547c 100644 --- a/openstack/tests/unit/cloud/test_object.py +++ b/openstack/tests/unit/cloud/test_object.py @@ -282,6 +282,25 @@ def test_list_objects(self): self.assert_calls() self.assertEqual(objects, ret) + def test_list_objects_with_prefix(self): + endpoint = '{endpoint}?format=json&prefix=test'.format( + endpoint=self.container_endpoint) + + objects = [{ + u'bytes': 20304400896, + u'last_modified': u'2016-12-15T13:34:13.650090', + u'hash': u'daaf9ed2106d09bba96cf193d866445e', + u'name': self.object, + u'content_type': u'application/octet-stream'}] + + self.register_uris([dict(method='GET', uri=endpoint, complete_qs=True, + json=objects)]) + + ret = self.cloud.list_objects(self.container, prefix='test') + + self.assert_calls() + self.assertEqual(objects, ret) + def test_list_objects_exception(self): endpoint = '{endpoint}?format=json'.format( endpoint=self.container_endpoint) From 41ccc0731ef1758774d66aa608c6e6505bc301a2 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 30 Nov 2018 17:25:51 -0600 Subject: [PATCH 2303/3836] Move server munch transformation into normalize So that we ensure we call it everywhere, do the munchification of the initial server object in the normalize function. Change-Id: I04953df06036b631ebfd8f251de57d94f76c8827 --- openstack/cloud/_normalize.py | 3 ++- openstack/cloud/openstackcloud.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openstack/cloud/_normalize.py b/openstack/cloud/_normalize.py index 740c52301..92e58d5a5 100644 --- a/openstack/cloud/_normalize.py +++ b/openstack/cloud/_normalize.py @@ -451,7 +451,8 @@ def _normalize_servers(self, servers): def _normalize_server(self, server): ret = munch.Munch() # Copy incoming server because of shared dicts in unittests - server = server.copy() + # Wrap the copy in munch so that sub-dicts are properly munched + server = munch.Munch(server) self._remove_novaclient_artifacts(server) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 366a3b5b2..799a9e522 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -2141,7 +2141,7 @@ def _list_servers(self, detailed=False, all_projects=False, bare=False, filters=None): filters = filters or {} servers = [ - self._normalize_server(munch.Munch(server.to_dict())) + self._normalize_server(server.to_dict()) for server in self.compute.servers( all_projects=all_projects, **filters)] return [ From 08dfc169dc7f1fcc4133937f4986091fb141c957 Mon Sep 17 00:00:00 2001 From: Tino Schmeier Date: Mon, 3 Dec 2018 16:57:20 +0100 Subject: [PATCH 2304/3836] Support non-public volume types - added query-parameters to filter for non-public volume-types. Needs ?is_public=None - added is_public-property to support non-public-volume-types Change-Id: I3f6cdf3fc4c97584a28c9254b74985d8ecde4719 --- openstack/block_storage/v2/_proxy.py | 4 ++-- openstack/block_storage/v2/type.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 1e5b8d32c..f981d0d92 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -98,12 +98,12 @@ def get_type(self, type): """ return self._get(_type.Type, type) - def types(self): + def types(self, **query): """Retrieve a generator of volume types :returns: A generator of volume type objects. """ - return self._list(_type.Type, paginated=False) + return self._list(_type.Type, paginated=False, **query) def create_type(self, **attrs): """Create a new type from attributes diff --git a/openstack/block_storage/v2/type.py b/openstack/block_storage/v2/type.py index db0726051..7e3c81aae 100644 --- a/openstack/block_storage/v2/type.py +++ b/openstack/block_storage/v2/type.py @@ -24,6 +24,8 @@ class Type(resource.Resource): allow_delete = True allow_list = True + _query_mapping = resource.QueryParameters("is_public") + # Properties #: A ID representing this type. id = resource.Body("id") @@ -31,3 +33,5 @@ class Type(resource.Resource): name = resource.Body("name") #: A dict of extra specifications. "capabilities" is a usual key. extra_specs = resource.Body("extra_specs", type=dict) + #: a private volume-type. *Type: bool* + is_public = resource.Body('os-volume-type-access:is_public', type=bool) From 65fd4a9442108ed81996022014fbd3ff35ae075f Mon Sep 17 00:00:00 2001 From: qingszhao Date: Tue, 4 Dec 2018 17:19:41 +0000 Subject: [PATCH 2305/3836] Change openstack-dev to openstack-discuss Mailinglists have been updated. Openstack-discuss replaces openstack-dev. Change-Id: I57ae41d3349c26c660a3a208cb590295f1a7e375 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index fd51be71d..45a1c03e6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,7 +4,7 @@ summary = An SDK for building applications to work with OpenStack description-file = README.rst author = OpenStack -author-email = openstack-dev@lists.openstack.org +author-email = openstack-discuss@lists.openstack.org home-page = http://developer.openstack.org/sdks/python/openstacksdk/ classifier = Environment :: OpenStack From 51182f3ec8224c3edda78d0853853bec6c1db16d Mon Sep 17 00:00:00 2001 From: Nate Johnston Date: Tue, 4 Dec 2018 12:51:52 -0500 Subject: [PATCH 2306/3836] Replace neutron-grenade job with grenade-py3 Since the grenade-py3 job is now in the neutron check and gate queues, the neutron team would like to eliminate the neutron-grenade job as it look like it duplicates the functionality of grenade-py3. This also means the grenade job is now py3-compliant. Change-Id: I83e359974630e2d955bb93574d460b5a136927df Needed-By: https://review.openstack.org/620357 --- .zuul.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 02c757439..cfefe8ca8 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -406,7 +406,7 @@ - openstacksdk-functional-devstack-python2 - osc-functional-devstack-tips: voting: false - - neutron-grenade + - grenade-py3 - nodepool-functional-py35-src - bifrost-integration-tinyipa-ubuntu-xenial gate: @@ -415,6 +415,6 @@ - openstacksdk-functional-devstack-python2 - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-senlin - - neutron-grenade + - grenade-py3 - nodepool-functional-py35-src - bifrost-integration-tinyipa-ubuntu-xenial From d6527b89dad07d79ab7c518b1addba5d5681b66a Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 5 Dec 2018 11:53:48 +0100 Subject: [PATCH 2307/3836] block_storage.backup func tests to use configurable timeout a followup on change 619008 to make timeouts in functional tests configurable Change-Id: I62d743f0d4e85ca19b83f676fdf00ad1e78689ef --- .../tests/functional/block_storage/v2/test_backup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openstack/tests/functional/block_storage/v2/test_backup.py b/openstack/tests/functional/block_storage/v2/test_backup.py index 08914673d..1e17688df 100644 --- a/openstack/tests/functional/block_storage/v2/test_backup.py +++ b/openstack/tests/functional/block_storage/v2/test_backup.py @@ -12,10 +12,10 @@ from openstack.block_storage.v2 import volume as _volume from openstack.block_storage.v2 import backup as _backup -from openstack.tests.functional import base +from openstack.tests.functional.block_storage.v2 import base -class TestBackup(base.BaseFunctionalTest): +class TestBackup(base.BaseBlockStorageTest): def setUp(self): super(TestBackup, self).setUp() @@ -36,7 +36,7 @@ def setUp(self): status='available', failures=['error'], interval=5, - wait=300) + wait=self._wait_for_timeout) assert isinstance(volume, _volume.Volume) self.VOLUME_ID = volume.id @@ -48,7 +48,7 @@ def setUp(self): status='available', failures=['error'], interval=5, - wait=300) + wait=self._wait_for_timeout) assert isinstance(backup, _backup.Backup) self.assertEqual(self.BACKUP_NAME, backup.name) self.BACKUP_ID = backup.id From 0c6ede931e5d7c7be5b4be4217bd5f06a5d74d6b Mon Sep 17 00:00:00 2001 From: melissaml Date: Wed, 5 Dec 2018 20:20:55 +0800 Subject: [PATCH 2308/3836] Change openstack-dev to openstack-discuss Mailinglists have been updated. Openstack-discuss replaces openstack-dev. Change-Id: Iea68921e947d86b6d786ced806373879c998c9fa --- doc/source/contributor/index.rst | 2 +- doc/source/user/multi-cloud-demo.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst index a4f7f32c1..1e871335b 100644 --- a/doc/source/contributor/index.rst +++ b/doc/source/contributor/index.rst @@ -36,7 +36,7 @@ as occasional talk about SDKs created for languages outside of Python. Email ***** -The `openstack-dev `_ +The `openstack-discuss `_ mailing list fields questions of all types on OpenStack. Using the ``[sdk]`` filter to begin your email subject will ensure that the message gets to SDK developers. diff --git a/doc/source/user/multi-cloud-demo.rst b/doc/source/user/multi-cloud-demo.rst index 1dd1852a0..c4fb90f4f 100644 --- a/doc/source/user/multi-cloud-demo.rst +++ b/doc/source/user/multi-cloud-demo.rst @@ -46,7 +46,7 @@ shade is Free Software ====================== * https://git.openstack.org/cgit/openstack-infra/shade -* openstack-dev@lists.openstack.org +* openstack-discuss@lists.openstack.org * #openstack-shade on freenode This talk is Free Software, too From 1ce656218a667595861eb1bdefeb5dad09a43e05 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 5 Dec 2018 14:45:41 +0100 Subject: [PATCH 2309/3836] Fix query parameters of network.port_forwarding A port forwarding resource was recently add, but a proper query_parametes were missed. Add them according to https://developer.openstack.org/api-ref/network/v2/index.html?expanded=list-floating-ip-port-forwardings-detail#floating-ips-port-forwarding Change-Id: I8696496a71a57d578297ec2cfd58b0cabe88cc12 --- openstack/network/v2/_proxy.py | 2 -- openstack/network/v2/port_forwarding.py | 4 ++++ openstack/tests/unit/network/v2/test_port_forwarding.py | 7 +++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 145e37a55..c7dd6e4a3 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -678,8 +678,6 @@ def port_forwardings(self, floating_ip, **query): the resources being returned. Valid parameters are: * ``internal_port_id``: The ID of internal port. - * ``internal_ip_address``: The internal IP address - * ``internal_port``: The internal TCP/UDP/other port number * ``external_port``: The external TCP/UDP/other port number * ``protocol``: TCP/UDP/other protocol diff --git a/openstack/network/v2/port_forwarding.py b/openstack/network/v2/port_forwarding.py index ee4004841..854126009 100644 --- a/openstack/network/v2/port_forwarding.py +++ b/openstack/network/v2/port_forwarding.py @@ -27,6 +27,10 @@ class PortForwarding(resource.Resource): allow_delete = True allow_list = True + _query_mapping = resource.QueryParameters( + 'internal_port_id', 'external_port', 'protocol' + ) + # Properties #: The ID of Floating IP address floatingip_id = resource.URI('floatingip_id') diff --git a/openstack/tests/unit/network/v2/test_port_forwarding.py b/openstack/tests/unit/network/v2/test_port_forwarding.py index 18a3397df..c5a03489c 100644 --- a/openstack/tests/unit/network/v2/test_port_forwarding.py +++ b/openstack/tests/unit/network/v2/test_port_forwarding.py @@ -40,6 +40,13 @@ def test_basic(self): self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) + self.assertDictEqual({'internal_port_id': 'internal_port_id', + 'external_port': 'external_port', + 'limit': 'limit', + 'marker': 'marker', + 'protocol': 'protocol'}, + sot._query_mapping._mapping) + def test_make_it(self): sot = port_forwarding.PortForwarding(**EXAMPLE) self.assertEqual(EXAMPLE['id'], sot.id) From d027626d7faf92d44afc9b624bd4ccea35bec96a Mon Sep 17 00:00:00 2001 From: Hongbin Lu Date: Fri, 27 Jul 2018 17:02:56 -0400 Subject: [PATCH 2310/3836] Add propagate_uplink_status to port Neutron patch: https://review.openstack.org/#/c/571899/ Change-Id: Ie1c8c5229a86a1c5fa9afe84ff201aafedf63d6f Related-Bug: #1722720 --- openstack/network/v2/port.py | 3 +++ openstack/tests/unit/network/v2/test_port.py | 3 +++ ...agate_uplink_status-to-port-0152d476c65979e3.yaml | 12 ++++++++++++ 3 files changed, 18 insertions(+) create mode 100644 releasenotes/notes/add-propagate_uplink_status-to-port-0152d476c65979e3.yaml diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index e3290aef2..fba887d33 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -103,6 +103,9 @@ class Port(resource.Resource, tag.TagMixin): #: The ID of the project who owns the network. Only administrative #: users can specify a project ID other than their own. project_id = resource.Body('tenant_id') + #: Whether to propagate uplink status of the port. *Type: bool* + propagate_uplink_status = resource.Body('propagate_uplink_status', + type=bool) #: The ID of the QoS policy attached to the port. qos_policy_id = resource.Body('qos_policy_id') #: Revision number of the port. *Type: int* diff --git a/openstack/tests/unit/network/v2/test_port.py b/openstack/tests/unit/network/v2/test_port.py index f2c798ddf..c9c8686a4 100644 --- a/openstack/tests/unit/network/v2/test_port.py +++ b/openstack/tests/unit/network/v2/test_port.py @@ -39,6 +39,7 @@ 'network_id': '18', 'port_security_enabled': True, 'qos_policy_id': '21', + 'propagate_uplink_status': False, 'revision_number': 22, 'security_groups': ['23'], 'status': '25', @@ -121,6 +122,8 @@ def test_make_it(self): self.assertEqual(EXAMPLE['network_id'], sot.network_id) self.assertTrue(sot.is_port_security_enabled) self.assertEqual(EXAMPLE['qos_policy_id'], sot.qos_policy_id) + self.assertEqual(EXAMPLE['propagate_uplink_status'], + sot.propagate_uplink_status) self.assertEqual(EXAMPLE['revision_number'], sot.revision_number) self.assertEqual(EXAMPLE['security_groups'], sot.security_group_ids) self.assertEqual(EXAMPLE['status'], sot.status) diff --git a/releasenotes/notes/add-propagate_uplink_status-to-port-0152d476c65979e3.yaml b/releasenotes/notes/add-propagate_uplink_status-to-port-0152d476c65979e3.yaml new file mode 100644 index 000000000..28bf68160 --- /dev/null +++ b/releasenotes/notes/add-propagate_uplink_status-to-port-0152d476c65979e3.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + Add ``propagate_uplink_status`` attribute to ``port`` resource. + Users can set this attribute to ``True`` or ``False``. + If it is set to ``True``, uplink status propagation is enabled. + Otherwise, it is disabled. + Neutron server needs to have the API extension + ``uplink-status-propagation`` in order to support this feature. + This feature can be used in SRIOV scenario, in which users + enable uplink status propagation of the SRIOV port + so that the link status of the VF will follow the PF. From f06b60f4f9dc0a62bd06fc32c20da20ed6e57a80 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 6 Dec 2018 20:37:12 +0000 Subject: [PATCH 2311/3836] Deal with double-normalization of host_id We need to generalize this a bit more, but when we pass a server.to_dict() from the resource layer into cloud's normalize, things go sideways. Change-Id: I926811d43c0bde15fa2178fdc35285cb5ad5eddf --- openstack/cloud/_normalize.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/cloud/_normalize.py b/openstack/cloud/_normalize.py index 92e58d5a5..92ef152bf 100644 --- a/openstack/cloud/_normalize.py +++ b/openstack/cloud/_normalize.py @@ -485,7 +485,7 @@ def _normalize_server(self, server): 'has_config_drive', server.pop('config_drive', False)) ret['has_config_drive'] = _to_bool(config_drive) - host_id = server.pop('hostId', None) + host_id = server.pop('hostId', server.pop('host_id', None)) ret['host_id'] = host_id ret['progress'] = _pop_int(server, 'progress') From 79dff76bef9a7495bf76046cb569dec1439efbb0 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Wed, 5 Dec 2018 20:49:00 +0100 Subject: [PATCH 2312/3836] Fix requesting specific fields from ironic Currently we try sending a Python dict in the query, which obviously does not work. Convert fields to a comma-separated list first. For chassis specify that the required API version 1.8 is supported. Change-Id: Ie1c3230f4fd14a59237a55bab2e91f04d50529a7 --- openstack/baremetal/v1/_common.py | 7 ++++ openstack/baremetal/v1/chassis.py | 5 ++- openstack/baremetal/v1/node.py | 3 +- openstack/baremetal/v1/port.py | 3 +- openstack/baremetal/v1/port_group.py | 3 +- openstack/resource.py | 37 +++++++++++++++---- .../baremetal/test_baremetal_chassis.py | 12 ++++++ .../baremetal/test_baremetal_node.py | 12 ++++++ .../baremetal/test_baremetal_port.py | 13 +++++++ .../baremetal/test_baremetal_port_group.py | 8 ++++ openstack/tests/unit/test_resource.py | 19 ++++++++-- .../baremetal-fields-1f6fbcd8bd1ea2aa.yaml | 4 ++ 12 files changed, 110 insertions(+), 16 deletions(-) create mode 100644 releasenotes/notes/baremetal-fields-1f6fbcd8bd1ea2aa.yaml diff --git a/openstack/baremetal/v1/_common.py b/openstack/baremetal/v1/_common.py index 874a0752e..0c29e50ec 100644 --- a/openstack/baremetal/v1/_common.py +++ b/openstack/baremetal/v1/_common.py @@ -84,3 +84,10 @@ def list(cls, session, details=False, **params): base_path += '/detail' return super(ListMixin, cls).list(session, paginated=True, base_path=base_path, **params) + + +def comma_separated_list(value): + if value is None: + return None + else: + return ','.join(value) diff --git a/openstack/baremetal/v1/chassis.py b/openstack/baremetal/v1/chassis.py index 2d5b2e28c..953b1fd6b 100644 --- a/openstack/baremetal/v1/chassis.py +++ b/openstack/baremetal/v1/chassis.py @@ -19,6 +19,9 @@ class Chassis(_common.ListMixin, resource.Resource): resources_key = 'chassis' base_path = '/chassis' + # Specifying fields became possible in 1.8. + _max_microversion = '1.8' + # capabilities allow_create = True allow_fetch = True @@ -29,7 +32,7 @@ class Chassis(_common.ListMixin, resource.Resource): commit_jsonpatch = True _query_mapping = resource.QueryParameters( - 'fields' + fields={'name': 'fields', 'type': _common.comma_separated_list}, ) #: Timestamp at which the chassis was created. diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index ee374d2c1..80a93842a 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -49,8 +49,9 @@ class Node(_common.ListMixin, resource.Resource): commit_jsonpatch = True _query_mapping = resource.QueryParameters( - 'associated', 'conductor_group', 'driver', 'fault', 'fields', + 'associated', 'conductor_group', 'driver', 'fault', 'provision_state', 'resource_class', + fields={'name': 'fields', 'type': _common.comma_separated_list}, instance_id='instance_uuid', is_maintenance='maintenance', ) diff --git a/openstack/baremetal/v1/port.py b/openstack/baremetal/v1/port.py index 07dc2cbee..f77e70249 100644 --- a/openstack/baremetal/v1/port.py +++ b/openstack/baremetal/v1/port.py @@ -29,7 +29,8 @@ class Port(_common.ListMixin, resource.Resource): commit_jsonpatch = True _query_mapping = resource.QueryParameters( - 'address', 'fields', 'node', 'portgroup', + 'address', 'node', 'portgroup', + fields={'name': 'fields', 'type': _common.comma_separated_list}, node_id='node_uuid', ) diff --git a/openstack/baremetal/v1/port_group.py b/openstack/baremetal/v1/port_group.py index 32d70ea5b..a6419fed8 100644 --- a/openstack/baremetal/v1/port_group.py +++ b/openstack/baremetal/v1/port_group.py @@ -29,7 +29,8 @@ class PortGroup(_common.ListMixin, resource.Resource): commit_jsonpatch = True _query_mapping = resource.QueryParameters( - 'node', 'address', 'fields', + 'node', 'address', + fields={'name': 'fields', 'type': _common.comma_separated_list}, ) # The mode and properties field introduced in 1.26. diff --git a/openstack/resource.py b/openstack/resource.py index ab57ed161..fc46729d5 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -273,13 +273,18 @@ def __init__(self, *names, **mappings): :param mappings: Key-value pairs where the key is the client-side name we'll accept here and the value is the name - the server expects, e.g, changes_since=changes-since + the server expects, e.g, changes_since=changes-since. + Additionally, a value can be a dict with optional keys + name - server-side name, + type - callable to convert from client to server + representation. By default, both limit and marker are included in the initial mapping as they're the most common query parameters used for listing resources. """ self._mapping = {"limit": "limit", "marker": "marker"} - self._mapping.update(dict({name: name for name in names}, **mappings)) + self._mapping.update({name: name for name in names}) + self._mapping.update(mappings) def _validate(self, query, base_path=None): """Check that supplied query keys match known query mappings @@ -290,7 +295,9 @@ def _validate(self, query, base_path=None): the resource. """ expected_params = list(self._mapping.keys()) - expected_params += self._mapping.values() + expected_params.extend( + value['name'] if isinstance(value, dict) else value + for value in self._mapping.values()) if base_path: expected_params += utils.get_string_format_keys(base_path) @@ -312,11 +319,25 @@ def _transpose(self, query): server side name. """ result = {} - for key, value in self._mapping.items(): - if key in query: - result[value] = query[key] - elif value in query: - result[value] = query[value] + for client_side, server_side in self._mapping.items(): + if isinstance(server_side, dict): + name = server_side['name'] + type_ = server_side.get('type') + else: + name = server_side + type_ = None + + if client_side in query: + value = query[client_side] + elif name in query: + value = query[name] + else: + continue + + if type_ is not None: + result[name] = type_(value) + else: + result[name] = value return result diff --git a/openstack/tests/functional/baremetal/test_baremetal_chassis.py b/openstack/tests/functional/baremetal/test_baremetal_chassis.py index 2947e0bb6..185f2d5c6 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_chassis.py +++ b/openstack/tests/functional/baremetal/test_baremetal_chassis.py @@ -49,3 +49,15 @@ def test_chassis_negative_non_existing(self): ignore_missing=False) self.assertIsNone(self.conn.baremetal.find_chassis(uuid)) self.assertIsNone(self.conn.baremetal.delete_chassis(uuid)) + + +class TestBareMetalChassisFields(base.BaseBaremetalTest): + + min_microversion = '1.8' + + def test_chassis_fields(self): + self.create_chassis(description='something') + result = self.conn.baremetal.chassis(fields=['uuid', 'extra']) + for ch in result: + self.assertIsNotNone(ch.id) + self.assertIsNone(ch.description) diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index b76d20490..1c24d3f5c 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -142,6 +142,18 @@ def test_node_negative_non_existing(self): self.assertIsNone(self.conn.baremetal.delete_node(uuid)) +class TestBareMetalNodeFields(base.BaseBaremetalTest): + + min_microversion = '1.8' + + def test_node_fields(self): + self.create_node() + result = self.conn.baremetal.nodes(fields=['uuid', 'name']) + for item in result: + self.assertIsNotNone(item.id) + self.assertIsNone(item.driver) + + class TestBareMetalVif(base.BaseBaremetalTest): min_microversion = '1.28' diff --git a/openstack/tests/functional/baremetal/test_baremetal_port.py b/openstack/tests/functional/baremetal/test_baremetal_port.py index ce95fa1d7..4dafaef74 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_port.py +++ b/openstack/tests/functional/baremetal/test_baremetal_port.py @@ -92,3 +92,16 @@ def test_port_negative_non_existing(self): pxe_enabled=True) self.assertIsNone(self.conn.baremetal.find_port(uuid)) self.assertIsNone(self.conn.baremetal.delete_port(uuid)) + + +class TestBareMetalPortFields(base.BaseBaremetalTest): + + min_microversion = '1.8' + + def test_port_fields(self): + self.create_node() + self.create_port(address='11:22:33:44:55:66') + result = self.conn.baremetal.ports(fields=['uuid']) + for item in result: + self.assertIsNotNone(item.id) + self.assertIsNone(item.address) diff --git a/openstack/tests/functional/baremetal/test_baremetal_port_group.py b/openstack/tests/functional/baremetal/test_baremetal_port_group.py index 08afccaf4..24b8b3d75 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_port_group.py +++ b/openstack/tests/functional/baremetal/test_baremetal_port_group.py @@ -84,3 +84,11 @@ def test_port_group_negative_non_existing(self): ignore_missing=False) self.assertIsNone(self.conn.baremetal.find_port_group(uuid)) self.assertIsNone(self.conn.baremetal.delete_port_group(uuid)) + + def test_port_group_fields(self): + self.create_node() + self.create_port_group(address='11:22:33:44:55:66') + result = self.conn.baremetal.port_groups(fields=['uuid', 'name']) + for item in result: + self.assertIsNotNone(item.id) + self.assertIsNone(item.address) diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 2719caf4a..33a2116f9 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -360,32 +360,43 @@ class TestQueryParameters(base.TestCase): def test_create(self): location = "location" - mapping = {"first_name": "first-name"} + mapping = {"first_name": "first-name", + "second_name": {"name": "second-name"}, + "third_name": {"name": "third", "type": int}} sot = resource.QueryParameters(location, **mapping) self.assertEqual({"location": "location", "first_name": "first-name", + "second_name": {"name": "second-name"}, + "third_name": {"name": "third", "type": int}, "limit": "limit", "marker": "marker"}, sot._mapping) def test_transpose_unmapped(self): location = "location" - mapping = {"first_name": "first-name"} + mapping = {"first_name": "first-name", + "pet_name": {"name": "pet"}, + "answer": {"name": "answer", "type": int}} sot = resource.QueryParameters(location, **mapping) result = sot._transpose({"location": "Brooklyn", "first_name": "Brian", + "pet_name": "Meow", + "answer": "42", "last_name": "Curtin"}) # last_name isn't mapped and shouldn't be included - self.assertEqual({"location": "Brooklyn", "first-name": "Brian"}, + self.assertEqual({"location": "Brooklyn", "first-name": "Brian", + "pet": "Meow", "answer": 42}, result) def test_transpose_not_in_query(self): location = "location" - mapping = {"first_name": "first-name"} + mapping = {"first_name": "first-name", + "pet_name": {"name": "pet"}, + "answer": {"name": "answer", "type": int}} sot = resource.QueryParameters(location, **mapping) result = sot._transpose({"location": "Brooklyn"}) diff --git a/releasenotes/notes/baremetal-fields-1f6fbcd8bd1ea2aa.yaml b/releasenotes/notes/baremetal-fields-1f6fbcd8bd1ea2aa.yaml new file mode 100644 index 000000000..133117623 --- /dev/null +++ b/releasenotes/notes/baremetal-fields-1f6fbcd8bd1ea2aa.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - | + Fixes specifying fields when listing bare metal resources. From 5a1983a42f2e53a2342de5cb58529f11d9964c45 Mon Sep 17 00:00:00 2001 From: Yuval Shalev Date: Mon, 3 Dec 2018 22:33:53 +0200 Subject: [PATCH 2313/3836] Add host aggregate missing functions Added add and remove host from aggregate. Also added set metadata to aggregate Change-Id: I8a6c082a200abefeb1f64e06b75763a866d1b895 --- openstack/compute/v2/_proxy.py | 41 +++++++++++++++++ openstack/compute/v2/aggregate.py | 26 +++++++++++ .../tests/unit/compute/v2/test_aggregate.py | 44 +++++++++++++++++++ 3 files changed, 111 insertions(+) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index db3b35efa..7d4a9fbab 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -187,6 +187,47 @@ def delete_aggregate(self, aggregate, ignore_missing=True): self._delete(_aggregate.Aggregate, aggregate, ignore_missing=ignore_missing) + def add_host_to_aggregate(self, aggregate, host): + """Adds a host to an aggregate + + :param aggregate: Either the ID of a aggregate or a + :class:`~openstack.compute.v2.aggregate.Aggregate` + instance. + :param str host: The host to add to the aggregate + + :returns: One :class:`~openstack.compute.v2.aggregate.Aggregate` + """ + aggregate = self._get_resource(_aggregate.Aggregate, aggregate) + return aggregate.add_host(self, host) + + def remove_host_from_aggregate(self, aggregate, host): + """Removes a host from an aggregate + + :param aggregate: Either the ID of a aggregate or a + :class:`~openstack.compute.v2.aggregate.Aggregate` + instance. + :param str host: The host to remove from the aggregate + + :returns: One :class:`~openstack.compute.v2.aggregate.Aggregate` + """ + aggregate = self._get_resource(_aggregate.Aggregate, aggregate) + return aggregate.remove_host(self, host) + + def set_aggregate_metadata(self, aggregate, metadata): + """Creates or replaces metadata for an aggregate + + :param aggregate: Either the ID of a aggregate or a + :class:`~openstack.compute.v2.aggregate.Aggregate` + instance. + :param dict metadata: Metadata key and value pairs. The maximum + size for each metadata key and value pair + is 255 bytes. + + :returns: One :class:`~openstack.compute.v2.aggregate.Aggregate` + """ + aggregate = self._get_resource(_aggregate.Aggregate, aggregate) + return aggregate.set_metadata(self, metadata) + def delete_image(self, image, ignore_missing=True): """Delete an image diff --git a/openstack/compute/v2/aggregate.py b/openstack/compute/v2/aggregate.py index 3a9b0c7d9..0de479492 100644 --- a/openstack/compute/v2/aggregate.py +++ b/openstack/compute/v2/aggregate.py @@ -12,6 +12,7 @@ from openstack import resource +from openstack import utils class Aggregate(resource.Resource): @@ -36,3 +37,28 @@ class Aggregate(resource.Resource): hosts = resource.Body('hosts') #: Metadata metadata = resource.Body('metadata') + + def _action(self, session, body, microversion=None): + """Preform aggregate actions given the message body.""" + url = utils.urljoin(self.base_path, self.id, 'action') + headers = {'Accept': ''} + response = session.post( + url, json=body, headers=headers, microversion=microversion) + aggregate = Aggregate() + aggregate._translate_response(response=response) + return aggregate + + def add_host(self, session, host): + """Adds a host to an aggregate.""" + body = {'add_host': {'host': host}} + return self._action(session, body) + + def remove_host(self, session, host): + """Removes a host from an aggregate.""" + body = {'remove_host': {'host': host}} + return self._action(session, body) + + def set_metadata(self, session, metadata): + """Creates or replaces metadata for an aggregate.""" + body = {'set_metadata': {'metadata': metadata}} + return self._action(session, body) diff --git a/openstack/tests/unit/compute/v2/test_aggregate.py b/openstack/tests/unit/compute/v2/test_aggregate.py index 76ff19bd8..e2ad9aa1b 100644 --- a/openstack/tests/unit/compute/v2/test_aggregate.py +++ b/openstack/tests/unit/compute/v2/test_aggregate.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import mock from openstack.tests.unit import base from openstack.compute.v2 import aggregate @@ -29,6 +30,16 @@ class TestAggregate(base.TestCase): + def setUp(self): + super(TestAggregate, self).setUp() + self.resp = mock.Mock() + self.resp.body = EXAMPLE.copy() + self.resp.json = mock.Mock(return_value=self.resp.body) + self.resp.status_code = 200 + self.resp.headers = {'Accept': ''} + self.sess = mock.Mock() + self.sess.post = mock.Mock(return_value=self.resp) + def test_basic(self): sot = aggregate.Aggregate() self.assertEqual('aggregate', sot.resource_key) @@ -48,3 +59,36 @@ def test_make_it(self): self.assertEqual(EXAMPLE['hosts'], sot.hosts) self.assertEqual(EXAMPLE['id'], sot.id) self.assertDictEqual(EXAMPLE['metadata'], sot.metadata) + + def test_add_host(self): + sot = aggregate.Aggregate(**EXAMPLE) + + sot.add_host(self.sess, 'host1') + + url = 'os-aggregates/4/action' + body = {"add_host": {"host": "host1"}} + headers = {'Accept': ''} + self.sess.post.assert_called_with( + url, json=body, headers=headers, microversion=None) + + def test_remove_host(self): + sot = aggregate.Aggregate(**EXAMPLE) + + sot.remove_host(self.sess, 'host1') + + url = 'os-aggregates/4/action' + body = {"remove_host": {"host": "host1"}} + headers = {'Accept': ''} + self.sess.post.assert_called_with( + url, json=body, headers=headers, microversion=None) + + def test_set_metadata(self): + sot = aggregate.Aggregate(**EXAMPLE) + + sot.set_metadata(self.sess, {"key: value"}) + + url = 'os-aggregates/4/action' + body = {"set_metadata": {"metadata": {"key: value"}}} + headers = {'Accept': ''} + self.sess.post.assert_called_with( + url, json=body, headers=headers, microversion=None) From 1bceca8b98c051dc477c5c43b7f30812e18acb22 Mon Sep 17 00:00:00 2001 From: melissaml Date: Fri, 14 Dec 2018 20:52:54 +0800 Subject: [PATCH 2314/3836] Change openstack-dev to openstack-discuss Mailinglists have been updated. Openstack-discuss replaces openstack-dev. Change-Id: I7daa0112dba3445f4901005006153784ed4dff9f --- CONTRIBUTING.rst | 2 +- README.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 873294e88..db02b70fb 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -36,7 +36,7 @@ Bug tracker https://storyboard.openstack.org/#!/project/openstack/openstacksdk Mailing list (prefix subjects with ``[sdk]`` for faster responses) - http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-dev + http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-discuss Code Hosting https://git.openstack.org/cgit/openstack/openstacksdk diff --git a/README.rst b/README.rst index ea5ada641..da434d546 100644 --- a/README.rst +++ b/README.rst @@ -159,5 +159,5 @@ Links * `Code Review `_ * `Documentation `_ * `PyPI `_ -* `Mailing list `_ +* `Mailing list `_ * `Release Notes `_ From fd61b546796af5847a92774b064eb21e2f0a22e1 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 11 Dec 2018 15:25:34 -0500 Subject: [PATCH 2315/3836] Avoid dogpile.cache 0.7.0 Latest release of dogpile.cache breaks our method of calling a bound function in the decorators. For more details see story or https://github.com/sqlalchemy/dogpile.cache/issues/140 Pin while we work on a solution. Additionally, the cirros version was updated to 0.3.6 in devsatck with Id2f20ebafdd78c2dadf81b8f80f22e7bd6db7755. Update the hard-coded versions in the functional tests (this should be modified to be more version agnostic in a future change) Story: 2004605 Depends-On: https://review.openstack.org/624697 Change-Id: Id2df8a05ef45041bf55a2671f7d06e7b88cc79d6 --- examples/connect.py | 2 +- openstack/tests/functional/base.py | 2 +- .../tests/functional/cloud/test_clustering.py | 38 +++++++++---------- requirements.txt | 2 +- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/examples/connect.py b/examples/connect.py index fe6ebd026..b4e76cef2 100644 --- a/examples/connect.py +++ b/examples/connect.py @@ -47,7 +47,7 @@ def _get_resource_value(resource_key, default): SERVER_NAME = 'openstacksdk-example' -IMAGE_NAME = _get_resource_value('image_name', 'cirros-0.3.5-x86_64-disk') +IMAGE_NAME = _get_resource_value('image_name', 'cirros-0.3.6-x86_64-disk') FLAVOR_NAME = _get_resource_value('flavor_name', 'm1.small') NETWORK_NAME = _get_resource_value('network_name', 'private') KEYPAIR_NAME = _get_resource_value('keypair_name', 'openstacksdk-example') diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index 911d2ebe0..115242615 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -39,7 +39,7 @@ def _disable_keep_alive(conn): sess.keep_alive = False -IMAGE_NAME = _get_resource_value('image_name', 'cirros-0.3.5-x86_64-disk') +IMAGE_NAME = _get_resource_value('image_name', 'cirros-0.3.6-x86_64-disk') FLAVOR_NAME = _get_resource_value('flavor_name', 'm1.small') diff --git a/openstack/tests/functional/cloud/test_clustering.py b/openstack/tests/functional/cloud/test_clustering.py index 4825e6142..07c3a7444 100644 --- a/openstack/tests/functional/cloud/test_clustering.py +++ b/openstack/tests/functional/cloud/test_clustering.py @@ -116,7 +116,7 @@ def test_create_profile(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.5-x86_64-disk", + "image": "cirros-0.3.6-x86_64-disk", "networks": [ { "network": "private" @@ -146,7 +146,7 @@ def test_create_cluster(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.5-x86_64-disk", + "image": "cirros-0.3.6-x86_64-disk", "networks": [ { "network": "private" @@ -190,7 +190,7 @@ def test_get_cluster_by_id(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.5-x86_64-disk", + "image": "cirros-0.3.6-x86_64-disk", "networks": [ { "network": "private" @@ -233,7 +233,7 @@ def test_update_cluster(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.5-x86_64-disk", + "image": "cirros-0.3.6-x86_64-disk", "networks": [ { "network": "private" @@ -319,7 +319,7 @@ def test_attach_policy_to_cluster(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.5-x86_64-disk", + "image": "cirros-0.3.6-x86_64-disk", "networks": [ { "network": "private" @@ -393,7 +393,7 @@ def test_detach_policy_from_cluster(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.5-x86_64-disk", + "image": "cirros-0.3.6-x86_64-disk", "networks": [ { "network": "private" @@ -475,7 +475,7 @@ def test_get_policy_on_cluster_by_id(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.5-x86_64-disk", + "image": "cirros-0.3.6-x86_64-disk", "networks": [ { "network": "private" @@ -568,7 +568,7 @@ def test_list_policies_on_cluster(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.5-x86_64-disk", + "image": "cirros-0.3.6-x86_64-disk", "networks": [ { "network": "private" @@ -657,7 +657,7 @@ def test_create_cluster_receiver(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.5-x86_64-disk", + "image": "cirros-0.3.6-x86_64-disk", "networks": [ { "network": "private" @@ -714,7 +714,7 @@ def test_list_cluster_receivers(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.5-x86_64-disk", + "image": "cirros-0.3.6-x86_64-disk", "networks": [ { "network": "private" @@ -776,7 +776,7 @@ def test_delete_cluster(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.5-x86_64-disk", + "image": "cirros-0.3.6-x86_64-disk", "networks": [ { "network": "private" @@ -864,7 +864,7 @@ def test_list_clusters(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.5-x86_64-disk", + "image": "cirros-0.3.6-x86_64-disk", "networks": [ { "network": "private" @@ -915,7 +915,7 @@ def test_update_policy_on_cluster(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.5-x86_64-disk", + "image": "cirros-0.3.6-x86_64-disk", "networks": [ { "network": "private" @@ -1019,7 +1019,7 @@ def test_list_cluster_profiles(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.5-x86_64-disk", + "image": "cirros-0.3.6-x86_64-disk", "networks": [ { "network": "private" @@ -1057,7 +1057,7 @@ def test_get_cluster_profile_by_id(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.5-x86_64-disk", + "image": "cirros-0.3.6-x86_64-disk", "networks": [ { "network": "private" @@ -1095,7 +1095,7 @@ def test_update_cluster_profile(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.5-x86_64-disk", + "image": "cirros-0.3.6-x86_64-disk", "networks": [ { "network": "private" @@ -1131,7 +1131,7 @@ def test_delete_cluster_profile(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.5-x86_64-disk", + "image": "cirros-0.3.6-x86_64-disk", "networks": [ { "network": "private" @@ -1298,7 +1298,7 @@ def test_get_cluster_receiver_by_id(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.5-x86_64-disk", + "image": "cirros-0.3.6-x86_64-disk", "networks": [ { "network": "private" @@ -1357,7 +1357,7 @@ def test_update_cluster_receiver(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.5-x86_64-disk", + "image": "cirros-0.3.6-x86_64-disk", "networks": [ { "network": "private" diff --git a/requirements.txt b/requirements.txt index 78f758519..87ed65edb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,5 +18,5 @@ futures>=3.0.0;python_version=='2.7' or python_version=='2.6' # BSD iso8601>=0.1.11 # MIT netifaces>=0.10.4 # MIT -dogpile.cache>=0.6.2 # BSD +dogpile.cache>=0.6.2,<0.7.0 # BSD cryptography>=2.1 # BSD/Apache-2.0 From 9675c1fb0b065017a0fadf75f52d8193cce7dd24 Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Fri, 14 Dec 2018 14:46:40 -0800 Subject: [PATCH 2316/3836] Fix dogpile.cache 0.7.0 interaction Due to the change in behavior in dogpile.cache > 0.7.0 where bound methods can no longer be passed to the cache_on_arguments decorator, openstackSDK now explicitly provides a non-method wrapper for all elements passed to cache_on_arguments. This is handled via an explicit no-op decorator. functools.wraps is used to preserve data from the original method. Needed-By: https://review.openstack.org/#/c/624993 Change-Id: Ied27fa1e834d145246815afcb67c59d48669ffb2 Story: 2004605 Task: 28502 --- openstack/cloud/_utils.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index 10a8f1ac7..45131c604 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -14,6 +14,7 @@ import contextlib import fnmatch +import functools import inspect import jmespath import munch @@ -378,6 +379,18 @@ def func_wrapper(func, *args, **kwargs): return func_wrapper +def _func_wrap(f): + # NOTE(morgan): This extra wrapper is intended to eliminate ever + # passing a bound method to dogpile.cache's cache_on_arguments. In + # 0.7.0 and later it is impossible to pass bound methods to the + # decorator. This was introduced when utilizing the decorate module in + # lieu of a direct wrap implementation. + @functools.wraps(f) + def inner(*args, **kwargs): + return f(*args, **kwargs) + return inner + + def cache_on_arguments(*cache_on_args, **cache_on_kwargs): _cache_name = cache_on_kwargs.pop('resource', None) @@ -385,7 +398,7 @@ def _inner_cache_on_arguments(func): def _cache_decorator(obj, *args, **kwargs): the_method = obj._get_cache(_cache_name).cache_on_arguments( *cache_on_args, **cache_on_kwargs)( - func.__get__(obj, type(obj))) + _func_wrap(func.__get__(obj, type(obj)))) return the_method(*args, **kwargs) def invalidate(obj, *args, **kwargs): From dd482fef8b0a9c695fd187d9a60f5ccb012adaad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rio=20Santos?= Date: Tue, 18 Dec 2018 13:49:50 +0000 Subject: [PATCH 2317/3836] Adds kwargs support when creating a Neutron subnet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change removes the parameter limitation exposed via openstacksdk for Neutron subnet creation. This way, any key value passed via `create_subnet` method is included in Neutron's API call. The use case is that some third party Neutron plugins actually extend the API and this would allow us to still use openstacksdk to pass those parameters. Change-Id: I7a5468217e7d74b507ceb2ae95b4bbc613ddaba2 Signed-off-by: Mário Santos --- openstack/cloud/openstackcloud.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 0069fd061..c8b23c5d2 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -8182,7 +8182,7 @@ def create_subnet(self, network_name_or_id, cidr=None, ip_version=4, gateway_ip=None, disable_gateway_ip=False, dns_nameservers=None, host_routes=None, ipv6_ra_mode=None, ipv6_address_mode=None, - use_default_subnetpool=False): + use_default_subnetpool=False, **kwargs): """Create a subnet on a specified network. :param string network_name_or_id: @@ -8248,6 +8248,7 @@ def create_subnet(self, network_name_or_id, cidr=None, ip_version=4, Use the default subnetpool for ``ip_version`` to obtain a CIDR. It is required to pass ``None`` to the ``cidr`` argument when enabling this option. + :param kwargs: Key value pairs to be passed to the Neutron API. :returns: The new subnet object. :raises: OpenStackCloudException on operation error. @@ -8286,11 +8287,11 @@ def create_subnet(self, network_name_or_id, cidr=None, ip_version=4, # The body of the neutron message for the subnet we wish to create. # This includes attributes that are required or have defaults. - subnet = { + subnet = dict({ 'network_id': network['id'], 'ip_version': ip_version, - 'enable_dhcp': enable_dhcp - } + 'enable_dhcp': enable_dhcp, + }, **kwargs) # Add optional attributes to the message. if cidr: From ca2045b829162461bdf804793dd5ec481a2117d7 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Tue, 11 Dec 2018 17:51:00 +0100 Subject: [PATCH 2318/3836] Import code for building ironic-compatible configdrives Building a correct configdrive may not be trivial. We have the code in ironicclient to do it, which was later cargo-culted to metalsmith. This is a cleaned up version of it that does not carry any assumptions about the first boot software (cloud-init vs anything). Change-Id: Iedc07e8f86bdc4561b150bf03a7cb59522ef7616 --- doc/source/user/proxies/baremetal.rst | 9 ++ openstack/baremetal/configdrive.py | 116 ++++++++++++++++++ openstack/baremetal/v1/_proxy.py | 3 +- openstack/baremetal/v1/node.py | 3 +- .../tests/unit/baremetal/test_configdrive.py | 48 ++++++++ 5 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 openstack/baremetal/configdrive.py create mode 100644 openstack/tests/unit/baremetal/test_configdrive.py diff --git a/doc/source/user/proxies/baremetal.rst b/doc/source/user/proxies/baremetal.rst index 6bc3ed572..fd37b61b0 100644 --- a/doc/source/user/proxies/baremetal.rst +++ b/doc/source/user/proxies/baremetal.rst @@ -74,3 +74,12 @@ VIF Operations .. automethod:: openstack.baremetal.v1._proxy.Proxy.attach_vif_to_node .. automethod:: openstack.baremetal.v1._proxy.Proxy.detach_vif_from_node .. automethod:: openstack.baremetal.v1._proxy.Proxy.list_node_vifs + +Utilities +--------- + +Building config drives +^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: openstack.baremetal.configdrive + :members: diff --git a/openstack/baremetal/configdrive.py b/openstack/baremetal/configdrive.py new file mode 100644 index 000000000..1fee2847f --- /dev/null +++ b/openstack/baremetal/configdrive.py @@ -0,0 +1,116 @@ +# 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. + +"""Helpers for building configdrive compatible with the Bare Metal service.""" + +import base64 +import contextlib +import gzip +import json +import os +import shutil +import subprocess +import tempfile + +import six + + +@contextlib.contextmanager +def populate_directory(metadata, user_data, versions=None): + """Populate a directory with configdrive files. + + :param dict metadata: Metadata. + :param bytes user_data: Vendor-specific user data. + :param versions: List of metadata versions to support. + :return: a context manager yielding a directory with files + """ + d = tempfile.mkdtemp() + versions = versions or ('2012-08-10', 'latest') + try: + for version in versions: + subdir = os.path.join(d, 'openstack', version) + if not os.path.exists(subdir): + os.makedirs(subdir) + + with open(os.path.join(subdir, 'meta_data.json'), 'w') as fp: + json.dump(metadata, fp) + + if user_data: + with open(os.path.join(subdir, 'user_data'), 'wb') as fp: + fp.write(user_data) + + yield d + finally: + shutil.rmtree(d) + + +def build(metadata, user_data, versions=None): + """Make a configdrive compatible with the Bare Metal service. + + Requires the genisoimage utility to be available. + + :param dict metadata: Metadata. + :param user_data: Vendor-specific user data. + :param versions: List of metadata versions to support. + :return: configdrive contents as a base64-encoded string. + """ + with populate_directory(metadata, user_data, versions) as path: + return pack(path) + + +def pack(path): + """Pack a directory with files into a Bare Metal service configdrive. + + Creates an ISO image with the files and label "config-2". + + :param str path: Path to directory with files + :return: configdrive contents as a base64-encoded string. + """ + with tempfile.NamedTemporaryFile() as tmpfile: + try: + p = subprocess.Popen(['genisoimage', + '-o', tmpfile.name, + '-ldots', '-allow-lowercase', + '-allow-multidot', '-l', + '-publisher', 'metalsmith', + '-quiet', '-J', + '-r', '-V', 'config-2', + path], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + except OSError as e: + raise RuntimeError( + 'Error generating the configdrive. Make sure the ' + '"genisoimage" tool is installed. Error: %s') % e + + stdout, stderr = p.communicate() + if p.returncode != 0: + raise RuntimeError( + 'Error generating the configdrive.' + 'Stdout: "%(stdout)s". Stderr: "%(stderr)s"' % + {'stdout': stdout, 'stderr': stderr}) + + tmpfile.seek(0) + + with tempfile.NamedTemporaryFile() as tmpzipfile: + with gzip.GzipFile(fileobj=tmpzipfile, mode='wb') as gz_file: + shutil.copyfileobj(tmpfile, gz_file) + + tmpzipfile.seek(0) + cd = base64.b64encode(tmpzipfile.read()) + + # NOTE(dtantsur): Ironic expects configdrive to be a string, but base64 + # returns bytes on Python 3. + if not isinstance(cd, six.string_types): + cd = cd.decode('utf-8') + + return cd diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 7d395edd3..59d2635ec 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -270,7 +270,8 @@ def set_node_provision_state(self, node, target, config_drive=None, :param target: Provisioning action, e.g. ``active``, ``provide``. See the Bare Metal service documentation for available actions. :param config_drive: Config drive to pass to the node, only valid - for ``active` and ``rebuild`` targets. + for ``active` and ``rebuild`` targets. You can use functions from + :mod:`openstack.baremetal.configdrive` to build it. :param clean_steps: Clean steps to execute, only valid for ``clean`` target. :param rescue_password: Password for the rescue operation, only valid diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 80a93842a..edfb6e9c0 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -267,7 +267,8 @@ def set_provision_state(self, session, target, config_drive=None, :param target: Provisioning action, e.g. ``active``, ``provide``. See the Bare Metal service documentation for available actions. :param config_drive: Config drive to pass to the node, only valid - for ``active` and ``rebuild`` targets. + for ``active` and ``rebuild`` targets. You can use functions from + :mod:`openstack.baremetal.configdrive` to build it. :param clean_steps: Clean steps to execute, only valid for ``clean`` target. :param rescue_password: Password for the rescue operation, only valid diff --git a/openstack/tests/unit/baremetal/test_configdrive.py b/openstack/tests/unit/baremetal/test_configdrive.py new file mode 100644 index 000000000..f6765d038 --- /dev/null +++ b/openstack/tests/unit/baremetal/test_configdrive.py @@ -0,0 +1,48 @@ +# Copyright 2018 Red Hat, 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 json +import os + +import testtools + +from openstack.baremetal import configdrive + + +class TestPopulateDirectory(testtools.TestCase): + def _check(self, metadata, user_data=None): + with configdrive.populate_directory(metadata, user_data) as d: + for version in ('2012-08-10', 'latest'): + with open(os.path.join(d, 'openstack', version, + 'meta_data.json')) as fp: + actual_metadata = json.load(fp) + + self.assertEqual(metadata, actual_metadata) + user_data_file = os.path.join(d, 'openstack', version, + 'user_data') + if user_data is None: + self.assertFalse(os.path.exists(user_data_file)) + else: + with open(user_data_file, 'rb') as fp: + self.assertEqual(user_data, fp.read()) + + # Clean up in __exit__ + self.assertFalse(os.path.exists(d)) + + def test_without_user_data(self): + self._check({'foo': 42}) + + def test_with_user_data(self): + self._check({'foo': 42}, b'I am user data') From c71acc62f4c6334ade67bbd0d1f614a9c921e157 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 19 Dec 2018 00:41:11 +0000 Subject: [PATCH 2319/3836] Turn off unneeded devstack services Hopefully this will help with memory pressure, but we don't need these installed or running for sdk tests. Change-Id: Iad90847609a9c3069159b4bf62de7eec6b5cbe39 --- .zuul.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.zuul.yaml b/.zuul.yaml index cfefe8ca8..8d9871b45 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -43,6 +43,13 @@ vars: devstack_localrc: Q_ML2_PLUGIN_EXT_DRIVERS: qos,port_security + devstack_services: + # sdk doesn't need vnc access + n-cauth: false + n-novnc: false + # sdk testing uses config drive only + n-api-meta: false + q-meta: false tox_environment: # Do we really need to set this? It's cargo culted PYTHONUNBUFFERED: 'true' From 5d7e149c1ad853e1ad881c0e4213d211f53e2657 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 18 Dec 2018 14:13:12 +0000 Subject: [PATCH 2320/3836] Properly munch for resource sub-dicts In the shade layer, we expect object notation to work for sub-dicts. When we're using underlying resource objects and translating them to munch then putting them through normalize (Which is temporary during transition) we're losing the munchified sub-dicts. Update to_dict in openstack/resource to be able to provide munches instead of dicts so that the recursive transform is complete. Add a test for server that makes sure we're getting what we need. A followup patch that should come that sets original_names to false in the to_munch call, which will need an update to the normalize function to deal with new incoming name. Change-Id: I3df806fe0db7ddf8d93546d64780fc979f38e78f --- doc/source/user/model.rst | 7 +++ openstack/cloud/_normalize.py | 19 +++++++- openstack/cloud/openstackcloud.py | 5 ++- openstack/resource.py | 28 ++++++++---- openstack/tests/unit/cloud/test_normalize.py | 45 +++++++++++++++++-- openstack/tests/unit/test_resource.py | 42 +++++++++++++++++ .../munch-sub-dict-e1619c71c26879cb.yaml | 5 +++ 7 files changed, 136 insertions(+), 15 deletions(-) create mode 100644 releasenotes/notes/munch-sub-dict-e1619c71c26879cb.yaml diff --git a/doc/source/user/model.rst b/doc/source/user/model.rst index 62fa748ef..f6e7b4fbb 100644 --- a/doc/source/user/model.rst +++ b/doc/source/user/model.rst @@ -226,6 +226,13 @@ A Server from Nova launched_at=str() or None, terminated_at=str() or None, task_state=str() or None, + block_device_mapping=dict() or None, + instance_name=str() or None, + hypervisor_name=str() or None, + tags=list(), + personality=str() or None, + scheduler_hints=str() or None, + user_data=str() or None, properties=dict()) ComputeLimits diff --git a/openstack/cloud/_normalize.py b/openstack/cloud/_normalize.py index 92ef152bf..17ee787ed 100644 --- a/openstack/cloud/_normalize.py +++ b/openstack/cloud/_normalize.py @@ -41,12 +41,14 @@ 'key_name', 'metadata', 'networks', + 'personality', 'private_v4', 'public_v4', 'public_v6', 'status', 'updated', 'user_id', + 'tags', ) _KEYPAIR_FIELDS = ( @@ -461,18 +463,28 @@ def _normalize_server(self, server): server['flavor'].pop('links', None) ret['flavor'] = server.pop('flavor') + # From original_names from sdk + server.pop('flavorRef', None) # OpenStack can return image as a string when you've booted # from volume if str(server['image']) != server['image']: server['image'].pop('links', None) ret['image'] = server.pop('image') + # From original_names from sdk + server.pop('imageRef', None) + # From original_names from sdk + ret['block_device_mapping'] = server.pop('block_device_mapping_v2', {}) project_id = server.pop('tenant_id', '') project_id = server.pop('project_id', project_id) az = _pop_or_get( server, 'OS-EXT-AZ:availability_zone', None, self.strict_mode) + # the server resource has this already, but it's missing az info + # from the resource. + # TODO(mordred) Fix server resource to set az in the location + server.pop('location', None) ret['location'] = self._get_current_location( project_id=project_id, zone=az) @@ -498,7 +510,12 @@ def _normalize_server(self, server): 'OS-EXT-STS:task_state', 'OS-EXT-STS:vm_state', 'OS-SRV-USG:launched_at', - 'OS-SRV-USG:terminated_at'): + 'OS-SRV-USG:terminated_at', + 'OS-EXT-SRV-ATTR:hypervisor_hostname', + 'OS-EXT-SRV-ATTR:instance_name', + 'OS-EXT-SRV-ATTR:user_data', + 'OS-SCH-HNT:scheduler_hints', + ): short_key = key.split(':')[1] ret[short_key] = _pop_or_get(server, key, None, self.strict_mode) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 0069fd061..86a132a3c 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -2141,7 +2141,10 @@ def _list_servers(self, detailed=False, all_projects=False, bare=False, filters=None): filters = filters or {} servers = [ - self._normalize_server(server.to_dict()) + # TODO(mordred) Add original_names=False here and update the + # normalize file for server. Then, just remove the normalize call + # and the to_munch call. + self._normalize_server(server._to_munch()) for server in self.compute.servers( all_projects=all_projects, **filters)] return [ diff --git a/openstack/resource.py b/openstack/resource.py index e0dfe02f3..b1c5cc2af 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -600,8 +600,7 @@ def _collect_attrs(self, attrs): # TODO(mordred) We should make a Location Resource and add it here # instead of just the dict. if self._connection: - computed['location'] = munch.unmunchify( - self._connection._openstackcloud.current_location) + computed['location'] = self._connection.current_location return body, header, uri, computed @@ -786,7 +785,7 @@ def _from_munch(cls, obj, synchronized=True, connection=None): return cls(_synchronized=synchronized, connection=connection, **obj) def to_dict(self, body=True, headers=True, computed=True, - ignore_none=False, original_names=False): + ignore_none=False, original_names=False, _to_munch=False): """Return a dictionary of this resource's contents :param bool body: Include the :class:`~openstack.resource.Body` @@ -800,11 +799,16 @@ def to_dict(self, body=True, headers=True, computed=True, attributes that the server hasn't returned. :param bool original_names: When True, use attribute names as they were received from the server. + :param bool _to_munch: For internal use only. Converts to `munch.Munch` + instead of dict. :return: A dictionary of key/value pairs where keys are named as they exist as attributes of this class. """ - mapping = {} + if _to_munch: + mapping = munch.Munch() + else: + mapping = {} components = [] if body: @@ -840,12 +844,17 @@ def to_dict(self, body=True, headers=True, computed=True, if ignore_none and value is None: continue if isinstance(value, Resource): - mapping[key] = value.to_dict() + mapping[key] = value.to_dict(_to_munch=_to_munch) + elif isinstance(value, dict) and _to_munch: + mapping[key] = munch.Munch(value) elif value and isinstance(value, list): converted = [] for raw in value: if isinstance(raw, Resource): - converted.append(raw.to_dict()) + converted.append( + raw.to_dict(_to_munch=_to_munch)) + elif isinstance(raw, dict) and _to_munch: + converted.append(munch.Munch(raw)) else: converted.append(raw) mapping[key] = converted @@ -858,10 +867,11 @@ def to_dict(self, body=True, headers=True, computed=True, # Make the munch copy method use to_dict copy = to_dict - def _to_munch(self): + def _to_munch(self, original_names=True): """Convert this resource into a Munch compatible with shade.""" - return munch.Munch(self.to_dict(body=True, headers=False, - original_names=True)) + return self.to_dict( + body=True, headers=False, + original_names=original_names, _to_munch=True) def _prepare_request_body(self, patch, prepend_key): if patch: diff --git a/openstack/tests/unit/cloud/test_normalize.py b/openstack/tests/unit/cloud/test_normalize.py index 890e79f4f..c26189f16 100644 --- a/openstack/tests/unit/cloud/test_normalize.py +++ b/openstack/tests/unit/cloud/test_normalize.py @@ -13,6 +13,7 @@ import mock import fixtures +from openstack.compute.v2 import server as server_resource from openstack.tests.unit import base RAW_SERVER_DICT = { @@ -557,8 +558,18 @@ def test_normalize_glance_images_strict(self): self.assertEqual(sorted(expected.keys()), sorted(retval.keys())) self.assertEqual(expected, retval) + def _assert_server_munch_attributes(self, raw, server): + self.assertEqual(server.flavor.id, raw['flavor']['id']) + self.assertEqual(server.image.id, raw['image']['id']) + self.assertEqual(server.metadata.group, raw['metadata']['group']) + self.assertEqual( + server.security_groups[0].name, + raw['security_groups'][0]['name']) + def test_normalize_servers_strict(self): - raw_server = RAW_SERVER_DICT.copy() + res = server_resource.Server( + connection=self.strict_cloud, + **RAW_SERVER_DICT) expected = { 'accessIPv4': u'', 'accessIPv6': u'', @@ -574,15 +585,18 @@ def test_normalize_servers_strict(self): u'addr': u'162.253.54.192', u'version': 4}]}, 'adminPass': None, + 'block_device_mapping': None, 'created': u'2015-08-01T19:52:16Z', 'created_at': u'2015-08-01T19:52:16Z', 'disk_config': u'MANUAL', 'flavor': {u'id': u'bbcb7eb5-5c8d-498f-9d7e-307c575d3566'}, 'has_config_drive': True, 'host_id': u'bd37', + 'hypervisor_hostname': None, 'id': u'811c5197-dba7-4d3a-a3f6-68ca5328b9a7', 'image': {u'id': u'69c99b45-cd53-49de-afdc-f24789eb8f83'}, 'interface_ip': u'', + 'instance_name': None, 'key_name': u'mordred', 'launched_at': u'2015-08-01T19:52:02.000000', 'location': { @@ -600,31 +614,42 @@ def test_normalize_servers_strict(self): u'public': [ u'2604:e100:1:0:f816:3eff:fe9f:463e', u'162.253.54.192']}, + 'personality': None, 'power_state': 1, 'private_v4': None, 'progress': 0, 'properties': {}, 'public_v4': None, 'public_v6': None, + 'scheduler_hints': None, 'security_groups': [{u'name': u'default'}], 'status': u'ACTIVE', + 'tags': [], 'task_state': None, 'terminated_at': None, 'updated': u'2016-10-15T15:49:29Z', + 'user_data': None, 'user_id': u'e9b21dc437d149858faee0898fb08e92', 'vm_state': u'active', 'volumes': []} - retval = self.strict_cloud._normalize_server(raw_server) + retval = self.strict_cloud._normalize_server(res._to_munch()) + self._assert_server_munch_attributes(res, retval) self.assertEqual(expected, retval) def test_normalize_servers_normal(self): - raw_server = RAW_SERVER_DICT.copy() + res = server_resource.Server( + connection=self.cloud, + **RAW_SERVER_DICT) expected = { 'OS-DCF:diskConfig': u'MANUAL', 'OS-EXT-AZ:availability_zone': u'ca-ymq-2', + 'OS-EXT-SRV-ATTR:hypervisor_hostname': None, + 'OS-EXT-SRV-ATTR:instance_name': None, + 'OS-EXT-SRV-ATTR:user_data': None, 'OS-EXT-STS:power_state': 1, 'OS-EXT-STS:task_state': None, 'OS-EXT-STS:vm_state': u'active', + 'OS-SCH-HNT:scheduler_hints': None, 'OS-SRV-USG:launched_at': u'2015-08-01T19:52:02.000000', 'OS-SRV-USG:terminated_at': None, 'accessIPv4': u'', @@ -642,6 +667,7 @@ def test_normalize_servers_normal(self): u'version': 4}]}, 'adminPass': None, 'az': u'ca-ymq-2', + 'block_device_mapping': None, 'cloud': '_test_cloud_', 'config_drive': u'True', 'created': u'2015-08-01T19:52:16Z', @@ -653,7 +679,9 @@ def test_normalize_servers_normal(self): 'host_id': u'bd37', 'id': u'811c5197-dba7-4d3a-a3f6-68ca5328b9a7', 'image': {u'id': u'69c99b45-cd53-49de-afdc-f24789eb8f83'}, + 'instance_name': None, 'interface_ip': '', + 'hypervisor_hostname': None, 'key_name': u'mordred', 'launched_at': u'2015-08-01T19:52:02.000000', 'location': { @@ -672,6 +700,7 @@ def test_normalize_servers_normal(self): u'2604:e100:1:0:f816:3eff:fe9f:463e', u'162.253.54.192']}, 'os-extended-volumes:volumes_attached': [], + 'personality': None, 'power_state': 1, 'private_v4': None, 'progress': 0, @@ -679,25 +708,33 @@ def test_normalize_servers_normal(self): 'properties': { 'OS-DCF:diskConfig': u'MANUAL', 'OS-EXT-AZ:availability_zone': u'ca-ymq-2', + 'OS-EXT-SRV-ATTR:hypervisor_hostname': None, + 'OS-EXT-SRV-ATTR:instance_name': None, + 'OS-EXT-SRV-ATTR:user_data': None, 'OS-EXT-STS:power_state': 1, 'OS-EXT-STS:task_state': None, 'OS-EXT-STS:vm_state': u'active', + 'OS-SCH-HNT:scheduler_hints': None, 'OS-SRV-USG:launched_at': u'2015-08-01T19:52:02.000000', 'OS-SRV-USG:terminated_at': None, 'os-extended-volumes:volumes_attached': []}, 'public_v4': None, 'public_v6': None, 'region': u'RegionOne', + 'scheduler_hints': None, 'security_groups': [{u'name': u'default'}], 'status': u'ACTIVE', + 'tags': [], 'task_state': None, 'tenant_id': u'db92b20496ae4fbda850a689ea9d563f', 'terminated_at': None, 'updated': u'2016-10-15T15:49:29Z', + 'user_data': None, 'user_id': u'e9b21dc437d149858faee0898fb08e92', 'vm_state': u'active', 'volumes': []} - retval = self.cloud._normalize_server(raw_server) + retval = self.cloud._normalize_server(res._to_munch()) + self._assert_server_munch_attributes(res, retval) self.assertEqual(expected, retval) def test_normalize_secgroups_strict(self): diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 5bc8c5713..8dafcc08f 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -683,6 +683,48 @@ class Test(resource.Resource): } self.assertEqual(expected, res.to_dict()) + def test_to_dict_nested(self): + + class Test(resource.Resource): + foo = resource.Header('foo') + bar = resource.Body('bar') + a_list = resource.Body('a_list') + + class Sub(resource.Resource): + sub = resource.Body('foo') + + sub = Sub(id='ANOTHER_ID', foo='bar') + + res = Test( + id='FAKE_ID', + bar=sub, + a_list=[sub]) + + expected = { + 'id': 'FAKE_ID', + 'name': None, + 'location': None, + 'foo': None, + 'bar': { + 'id': 'ANOTHER_ID', + 'name': None, + 'sub': 'bar', + 'location': None, + }, + 'a_list': [{ + 'id': 'ANOTHER_ID', + 'name': None, + 'sub': 'bar', + 'location': None, + }], + } + self.assertEqual(expected, res.to_dict()) + a_munch = res.to_dict(_to_munch=True) + self.assertEqual(a_munch.bar.id, 'ANOTHER_ID') + self.assertEqual(a_munch.bar.sub, 'bar') + self.assertEqual(a_munch.a_list[0].id, 'ANOTHER_ID') + self.assertEqual(a_munch.a_list[0].sub, 'bar') + def test_to_dict_no_body(self): class Test(resource.Resource): diff --git a/releasenotes/notes/munch-sub-dict-e1619c71c26879cb.yaml b/releasenotes/notes/munch-sub-dict-e1619c71c26879cb.yaml new file mode 100644 index 000000000..2fc59a248 --- /dev/null +++ b/releasenotes/notes/munch-sub-dict-e1619c71c26879cb.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixed a regression with sub-dicts of server objects + were not usable with object notation. From 416ea741c0df5db144e4f2bef70f7d265101fee3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 19 Dec 2018 19:08:50 +0000 Subject: [PATCH 2321/3836] Start using direct REST in normalize tests This is too synthetic and we're missing things. Switch to requests_mock. In doing this, remove the extra creation of extra connection objects in the base test. Reorganize the normalize test file so that they just drop in a strict_cloud flag and get the cloud created properly the first time. Change-Id: I86436b439cbc4b236b806af7eed195b198ce54e8 --- openstack/tests/unit/base.py | 16 +- openstack/tests/unit/cloud/test_normalize.py | 582 ++++++++++--------- 2 files changed, 300 insertions(+), 298 deletions(-) diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 993346772..365f883fd 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -82,6 +82,8 @@ class TestCase(base.TestCase): + strict_cloud = False + def setUp(self, cloud_config_fixture='clouds.yaml'): """Run before each test method to initialize test environment.""" @@ -111,21 +113,10 @@ def _nosleep(seconds): vendor.write(b'{}') vendor.close() - test_cloud = os.environ.get('OPENSTACKSDK_OS_CLOUD', '_test_cloud_') self.config = occ.OpenStackConfig( config_files=[config.name], vendor_files=[vendor.name], secure_files=['non-existant']) - self.cloud_config = self.config.get_one( - cloud=test_cloud, validate=False) - self.cloud = openstack.connection.Connection( - config=self.cloud_config, - strict=False) - self.strict_cloud = openstack.connection.Connection( - config=self.cloud_config, - strict=True) - self.addCleanup(self.cloud.task_manager.stop) - self.addCleanup(self.strict_cloud.task_manager.stop) # FIXME(notmorgan): Convert the uri_registry, discovery.json, and # use of keystone_v3/v2 to a proper fixtures.Fixture. For now this @@ -446,8 +437,9 @@ def _make_test_cloud(self, cloud_name='_test_cloud_', **kwargs): self.cloud_config = self.config.get_one( cloud=test_cloud, validate=True, **kwargs) self.conn = openstack.connection.Connection( - config=self.cloud_config) + config=self.cloud_config, strict=self.strict_cloud) self.cloud = self.conn + self.addCleanup(self.cloud.task_manager.stop) def get_glance_discovery_mock_dict( self, diff --git a/openstack/tests/unit/cloud/test_normalize.py b/openstack/tests/unit/cloud/test_normalize.py index c26189f16..fc95c10c3 100644 --- a/openstack/tests/unit/cloud/test_normalize.py +++ b/openstack/tests/unit/cloud/test_normalize.py @@ -10,9 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import mock -import fixtures - from openstack.compute.v2 import server as server_resource from openstack.tests.unit import base @@ -180,15 +177,16 @@ 'vcpus': 8} -# TODO(shade) Convert this to TestCase -class TestUtils(base.TestCase): +def _assert_server_munch_attributes(testcase, raw, server): + testcase.assertEqual(server.flavor.id, raw['flavor']['id']) + testcase.assertEqual(server.image.id, raw['image']['id']) + testcase.assertEqual(server.metadata.group, raw['metadata']['group']) + testcase.assertEqual( + server.security_groups[0].name, + raw['security_groups'][0]['name']) - def setUp(self): - super(TestUtils, self).setUp() - self.session_fixture = self.useFixture(fixtures.MonkeyPatch( - 'openstack.config.cloud_region.CloudRegion.get_session', - mock.Mock())) +class TestNormalize(base.TestCase): def test_normalize_flavors(self): raw_flavor = RAW_FLAVOR_DICT.copy() @@ -216,7 +214,7 @@ def test_normalize_flavors(self): 'project': { 'domain_id': None, 'domain_name': 'default', - 'id': mock.ANY, + 'id': '1c36b64c840a42cd9e9b931a369337f0', 'name': 'admin'}, 'region_name': u'RegionOne', 'zone': None}, @@ -236,38 +234,6 @@ def test_normalize_flavors(self): retval = self.cloud._normalize_flavor(raw_flavor) self.assertEqual(expected, retval) - def test_normalize_flavors_strict(self): - raw_flavor = RAW_FLAVOR_DICT.copy() - expected = { - 'disk': 40, - 'ephemeral': 80, - 'extra_specs': { - u'class': u'performance1', - u'disk_io_index': u'40', - u'number_of_data_disks': u'1', - u'policy_class': u'performance_flavor', - u'resize_policy_class': u'performance_flavor'}, - 'id': u'performance1-8', - 'is_disabled': False, - 'is_public': False, - 'location': { - 'cloud': '_test_cloud_', - 'project': { - 'domain_id': None, - 'domain_name': 'default', - 'id': mock.ANY, - 'name': 'admin'}, - 'region_name': u'RegionOne', - 'zone': None}, - 'name': u'8 GB Performance', - 'properties': {}, - 'ram': 8192, - 'rxtx_factor': 1600.0, - 'swap': 0, - 'vcpus': 8} - retval = self.strict_cloud._normalize_flavor(raw_flavor) - self.assertEqual(expected, retval) - def test_normalize_nova_images(self): raw_image = RAW_NOVA_IMAGE_DICT.copy() expected = { @@ -302,7 +268,7 @@ def test_normalize_nova_images(self): 'project': { 'domain_id': None, 'domain_name': 'default', - 'id': mock.ANY, + 'id': '1c36b64c840a42cd9e9b931a369337f0', 'name': 'admin'}, 'region_name': u'RegionOne', 'zone': None}, @@ -361,60 +327,6 @@ def test_normalize_nova_images(self): retval = self.cloud._normalize_image(raw_image) self.assertEqual(expected, retval) - def test_normalize_nova_images_strict(self): - raw_image = RAW_NOVA_IMAGE_DICT.copy() - expected = { - 'checksum': None, - 'container_format': None, - 'created_at': '2015-02-15T22:58:45Z', - 'direct_url': None, - 'disk_format': None, - 'file': None, - 'id': u'f2868d7c-63e1-4974-a64d-8670a86df21e', - 'is_protected': False, - 'is_public': False, - 'location': { - 'cloud': '_test_cloud_', - 'project': { - 'domain_id': None, - 'domain_name': 'default', - 'id': mock.ANY, - 'name': 'admin'}, - 'region_name': u'RegionOne', - 'zone': None}, - 'locations': [], - 'min_disk': 20, - 'min_ram': 0, - 'name': u'Test Monty Ubuntu', - 'owner': None, - 'properties': { - u'auto_disk_config': u'False', - u'com.rackspace__1__build_core': u'1', - u'com.rackspace__1__build_managed': u'1', - u'com.rackspace__1__build_rackconnect': u'1', - u'com.rackspace__1__options': u'0', - u'com.rackspace__1__source': u'import', - u'com.rackspace__1__visible_core': u'1', - u'com.rackspace__1__visible_managed': u'1', - u'com.rackspace__1__visible_rackconnect': u'1', - u'image_type': u'import', - u'org.openstack__1__architecture': u'x64', - u'os_type': u'linux', - u'user_id': u'156284', - u'vm_mode': u'hvm', - u'xenapi_use_agent': u'False', - 'OS-DCF:diskConfig': u'MANUAL', - 'progress': 100}, - 'size': 323004185, - 'status': u'active', - 'tags': [], - 'updated_at': u'2015-02-15T23:04:34Z', - 'virtual_size': 0, - 'visibility': 'private'} - retval = self.strict_cloud._normalize_image(raw_image) - self.assertEqual(sorted(expected.keys()), sorted(retval.keys())) - self.assertEqual(expected, retval) - def test_normalize_glance_images(self): raw_image = RAW_GLANCE_IMAGE_DICT.copy() expected = { @@ -505,137 +417,6 @@ def test_normalize_glance_images(self): retval = self.cloud._normalize_image(raw_image) self.assertEqual(expected, retval) - def test_normalize_glance_images_strict(self): - raw_image = RAW_GLANCE_IMAGE_DICT.copy() - expected = { - 'checksum': u'774f48af604ab1ec319093234c5c0019', - 'container_format': u'ovf', - 'created_at': u'2015-02-15T22:58:45Z', - 'direct_url': None, - 'disk_format': u'vhd', - 'file': u'/v2/images/f2868d7c-63e1-4974-a64d-8670a86df21e/file', - 'id': u'f2868d7c-63e1-4974-a64d-8670a86df21e', - 'is_protected': False, - 'is_public': False, - 'location': { - 'cloud': '_test_cloud_', - 'project': { - 'domain_id': None, - 'domain_name': None, - 'id': u'610275', - 'name': None}, - 'region_name': u'RegionOne', - 'zone': None}, - 'locations': [], - 'min_disk': 20, - 'min_ram': 0, - 'name': u'Test Monty Ubuntu', - 'owner': u'610275', - 'properties': { - u'auto_disk_config': u'False', - u'com.rackspace__1__build_core': u'1', - u'com.rackspace__1__build_managed': u'1', - u'com.rackspace__1__build_rackconnect': u'1', - u'com.rackspace__1__options': u'0', - u'com.rackspace__1__source': u'import', - u'com.rackspace__1__visible_core': u'1', - u'com.rackspace__1__visible_managed': u'1', - u'com.rackspace__1__visible_rackconnect': u'1', - u'image_type': u'import', - u'org.openstack__1__architecture': u'x64', - u'os_type': u'linux', - u'schema': u'/v2/schemas/image', - u'user_id': u'156284', - u'vm_mode': u'hvm', - u'xenapi_use_agent': u'False'}, - 'size': 323004185, - 'status': u'active', - 'tags': [], - 'updated_at': u'2015-02-15T23:04:34Z', - 'virtual_size': 0, - 'visibility': 'private'} - retval = self.strict_cloud._normalize_image(raw_image) - self.assertEqual(sorted(expected.keys()), sorted(retval.keys())) - self.assertEqual(expected, retval) - - def _assert_server_munch_attributes(self, raw, server): - self.assertEqual(server.flavor.id, raw['flavor']['id']) - self.assertEqual(server.image.id, raw['image']['id']) - self.assertEqual(server.metadata.group, raw['metadata']['group']) - self.assertEqual( - server.security_groups[0].name, - raw['security_groups'][0]['name']) - - def test_normalize_servers_strict(self): - res = server_resource.Server( - connection=self.strict_cloud, - **RAW_SERVER_DICT) - expected = { - 'accessIPv4': u'', - 'accessIPv6': u'', - 'addresses': { - u'public': [{ - u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:9f:46:3e', - u'OS-EXT-IPS:type': u'fixed', - u'addr': u'2604:e100:1:0:f816:3eff:fe9f:463e', - u'version': 6 - }, { - u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:9f:46:3e', - u'OS-EXT-IPS:type': u'fixed', - u'addr': u'162.253.54.192', - u'version': 4}]}, - 'adminPass': None, - 'block_device_mapping': None, - 'created': u'2015-08-01T19:52:16Z', - 'created_at': u'2015-08-01T19:52:16Z', - 'disk_config': u'MANUAL', - 'flavor': {u'id': u'bbcb7eb5-5c8d-498f-9d7e-307c575d3566'}, - 'has_config_drive': True, - 'host_id': u'bd37', - 'hypervisor_hostname': None, - 'id': u'811c5197-dba7-4d3a-a3f6-68ca5328b9a7', - 'image': {u'id': u'69c99b45-cd53-49de-afdc-f24789eb8f83'}, - 'interface_ip': u'', - 'instance_name': None, - 'key_name': u'mordred', - 'launched_at': u'2015-08-01T19:52:02.000000', - 'location': { - 'cloud': '_test_cloud_', - 'project': { - 'domain_id': None, - 'domain_name': None, - 'id': u'db92b20496ae4fbda850a689ea9d563f', - 'name': None}, - 'region_name': u'RegionOne', - 'zone': u'ca-ymq-2'}, - 'metadata': {u'group': u'irc', u'groups': u'irc,enabled'}, - 'name': u'mordred-irc', - 'networks': { - u'public': [ - u'2604:e100:1:0:f816:3eff:fe9f:463e', - u'162.253.54.192']}, - 'personality': None, - 'power_state': 1, - 'private_v4': None, - 'progress': 0, - 'properties': {}, - 'public_v4': None, - 'public_v6': None, - 'scheduler_hints': None, - 'security_groups': [{u'name': u'default'}], - 'status': u'ACTIVE', - 'tags': [], - 'task_state': None, - 'terminated_at': None, - 'updated': u'2016-10-15T15:49:29Z', - 'user_data': None, - 'user_id': u'e9b21dc437d149858faee0898fb08e92', - 'vm_state': u'active', - 'volumes': []} - retval = self.strict_cloud._normalize_server(res._to_munch()) - self._assert_server_munch_attributes(res, retval) - self.assertEqual(expected, retval) - def test_normalize_servers_normal(self): res = server_resource.Server( connection=self.cloud, @@ -734,53 +515,7 @@ def test_normalize_servers_normal(self): 'vm_state': u'active', 'volumes': []} retval = self.cloud._normalize_server(res._to_munch()) - self._assert_server_munch_attributes(res, retval) - self.assertEqual(expected, retval) - - def test_normalize_secgroups_strict(self): - nova_secgroup = dict( - id='abc123', - name='nova_secgroup', - description='A Nova security group', - rules=[ - dict(id='123', from_port=80, to_port=81, ip_protocol='tcp', - ip_range={'cidr': '0.0.0.0/0'}, parent_group_id='xyz123') - ] - ) - - expected = dict( - id='abc123', - name='nova_secgroup', - description='A Nova security group', - properties={}, - location=dict( - region_name='RegionOne', - zone=None, - project=dict( - domain_name='default', - id=mock.ANY, - domain_id=None, - name='admin'), - cloud='_test_cloud_'), - security_group_rules=[ - dict(id='123', direction='ingress', ethertype='IPv4', - port_range_min=80, port_range_max=81, protocol='tcp', - remote_ip_prefix='0.0.0.0/0', security_group_id='xyz123', - properties={}, - remote_group_id=None, - location=dict( - region_name='RegionOne', - zone=None, - project=dict( - domain_name='default', - id=mock.ANY, - domain_id=None, - name='admin'), - cloud='_test_cloud_')) - ] - ) - - retval = self.strict_cloud._normalize_secgroup(nova_secgroup) + _assert_server_munch_attributes(self, res, retval) self.assertEqual(expected, retval) def test_normalize_secgroups(self): @@ -806,7 +541,7 @@ def test_normalize_secgroups(self): zone=None, project=dict( domain_name='default', - id=mock.ANY, + id='1c36b64c840a42cd9e9b931a369337f0', domain_id=None, name='admin'), cloud='_test_cloud_'), @@ -823,7 +558,7 @@ def test_normalize_secgroups(self): zone=None, project=dict( domain_name='default', - id=mock.ANY, + id='1c36b64c840a42cd9e9b931a369337f0', domain_id=None, name='admin'), cloud='_test_cloud_')) @@ -864,7 +599,7 @@ def test_normalize_secgroup_rules(self): zone=None, project=dict( domain_name='default', - id=mock.ANY, + id='1c36b64c840a42cd9e9b931a369337f0', domain_id=None, name='admin'), cloud='_test_cloud_')) @@ -882,6 +617,12 @@ def test_normalize_volumes_v1(self): status='in-use', created_at='2015-08-27T09:49:58-05:00', ) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'block-storage', 'public', append=['volumes', 'detail']), + json={'volumes': [vol]}), + ]) expected = { 'attachments': [], 'availability_zone': None, @@ -902,7 +643,7 @@ def test_normalize_volumes_v1(self): 'project': { 'domain_id': None, 'domain_name': 'default', - 'id': mock.ANY, + 'id': '1c36b64c840a42cd9e9b931a369337f0', 'name': 'admin'}, 'region_name': u'RegionOne', 'zone': None}, @@ -921,7 +662,7 @@ def test_normalize_volumes_v1(self): 'updated_at': None, 'volume_type': None, } - retval = self.cloud._normalize_volume(vol) + retval = self.cloud.list_volumes(vol)[0] self.assertEqual(expected, retval) def test_normalize_volumes_v2(self): @@ -982,7 +723,276 @@ def test_normalize_volumes_v2(self): retval = self.cloud._normalize_volume(vol) self.assertEqual(expected, retval) - def test_normalize_volumes_v1_strict(self): + +class TestStrictNormalize(base.TestCase): + + strict_cloud = True + + def setUp(self): + super(TestStrictNormalize, self).setUp() + self.assertTrue(self.cloud.strict_mode) + + def test_normalize_flavors(self): + raw_flavor = RAW_FLAVOR_DICT.copy() + expected = { + 'disk': 40, + 'ephemeral': 80, + 'extra_specs': { + u'class': u'performance1', + u'disk_io_index': u'40', + u'number_of_data_disks': u'1', + u'policy_class': u'performance_flavor', + u'resize_policy_class': u'performance_flavor'}, + 'id': u'performance1-8', + 'is_disabled': False, + 'is_public': False, + 'location': { + 'cloud': '_test_cloud_', + 'project': { + 'domain_id': None, + 'domain_name': 'default', + 'id': u'1c36b64c840a42cd9e9b931a369337f0', + 'name': 'admin'}, + 'region_name': u'RegionOne', + 'zone': None}, + 'name': u'8 GB Performance', + 'properties': {}, + 'ram': 8192, + 'rxtx_factor': 1600.0, + 'swap': 0, + 'vcpus': 8} + retval = self.cloud._normalize_flavor(raw_flavor) + self.assertEqual(expected, retval) + + def test_normalize_nova_images(self): + raw_image = RAW_NOVA_IMAGE_DICT.copy() + expected = { + 'checksum': None, + 'container_format': None, + 'created_at': '2015-02-15T22:58:45Z', + 'direct_url': None, + 'disk_format': None, + 'file': None, + 'id': u'f2868d7c-63e1-4974-a64d-8670a86df21e', + 'is_protected': False, + 'is_public': False, + 'location': { + 'cloud': '_test_cloud_', + 'project': { + 'domain_id': None, + 'domain_name': 'default', + 'id': u'1c36b64c840a42cd9e9b931a369337f0', + 'name': 'admin'}, + 'region_name': u'RegionOne', + 'zone': None}, + 'locations': [], + 'min_disk': 20, + 'min_ram': 0, + 'name': u'Test Monty Ubuntu', + 'owner': None, + 'properties': { + u'auto_disk_config': u'False', + u'com.rackspace__1__build_core': u'1', + u'com.rackspace__1__build_managed': u'1', + u'com.rackspace__1__build_rackconnect': u'1', + u'com.rackspace__1__options': u'0', + u'com.rackspace__1__source': u'import', + u'com.rackspace__1__visible_core': u'1', + u'com.rackspace__1__visible_managed': u'1', + u'com.rackspace__1__visible_rackconnect': u'1', + u'image_type': u'import', + u'org.openstack__1__architecture': u'x64', + u'os_type': u'linux', + u'user_id': u'156284', + u'vm_mode': u'hvm', + u'xenapi_use_agent': u'False', + 'OS-DCF:diskConfig': u'MANUAL', + 'progress': 100}, + 'size': 323004185, + 'status': u'active', + 'tags': [], + 'updated_at': u'2015-02-15T23:04:34Z', + 'virtual_size': 0, + 'visibility': 'private'} + retval = self.cloud._normalize_image(raw_image) + self.assertEqual(sorted(expected.keys()), sorted(retval.keys())) + self.assertEqual(expected, retval) + + def test_normalize_glance_images(self): + raw_image = RAW_GLANCE_IMAGE_DICT.copy() + expected = { + 'checksum': u'774f48af604ab1ec319093234c5c0019', + 'container_format': u'ovf', + 'created_at': u'2015-02-15T22:58:45Z', + 'direct_url': None, + 'disk_format': u'vhd', + 'file': u'/v2/images/f2868d7c-63e1-4974-a64d-8670a86df21e/file', + 'id': u'f2868d7c-63e1-4974-a64d-8670a86df21e', + 'is_protected': False, + 'is_public': False, + 'location': { + 'cloud': '_test_cloud_', + 'project': { + 'domain_id': None, + 'domain_name': None, + 'id': u'610275', + 'name': None}, + 'region_name': u'RegionOne', + 'zone': None}, + 'locations': [], + 'min_disk': 20, + 'min_ram': 0, + 'name': u'Test Monty Ubuntu', + 'owner': u'610275', + 'properties': { + u'auto_disk_config': u'False', + u'com.rackspace__1__build_core': u'1', + u'com.rackspace__1__build_managed': u'1', + u'com.rackspace__1__build_rackconnect': u'1', + u'com.rackspace__1__options': u'0', + u'com.rackspace__1__source': u'import', + u'com.rackspace__1__visible_core': u'1', + u'com.rackspace__1__visible_managed': u'1', + u'com.rackspace__1__visible_rackconnect': u'1', + u'image_type': u'import', + u'org.openstack__1__architecture': u'x64', + u'os_type': u'linux', + u'schema': u'/v2/schemas/image', + u'user_id': u'156284', + u'vm_mode': u'hvm', + u'xenapi_use_agent': u'False'}, + 'size': 323004185, + 'status': u'active', + 'tags': [], + 'updated_at': u'2015-02-15T23:04:34Z', + 'virtual_size': 0, + 'visibility': 'private'} + retval = self.cloud._normalize_image(raw_image) + self.assertEqual(sorted(expected.keys()), sorted(retval.keys())) + self.assertEqual(expected, retval) + + def test_normalize_servers(self): + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [RAW_SERVER_DICT]}), + ]) + expected = { + 'accessIPv4': u'', + 'accessIPv6': u'', + 'addresses': { + u'public': [{ + u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:9f:46:3e', + u'OS-EXT-IPS:type': u'fixed', + u'addr': u'2604:e100:1:0:f816:3eff:fe9f:463e', + u'version': 6 + }, { + u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:9f:46:3e', + u'OS-EXT-IPS:type': u'fixed', + u'addr': u'162.253.54.192', + u'version': 4}]}, + 'adminPass': None, + 'block_device_mapping': None, + 'created': u'2015-08-01T19:52:16Z', + 'created_at': u'2015-08-01T19:52:16Z', + 'disk_config': u'MANUAL', + 'flavor': {u'id': u'bbcb7eb5-5c8d-498f-9d7e-307c575d3566'}, + 'has_config_drive': True, + 'host_id': u'bd37', + 'hypervisor_hostname': None, + 'id': u'811c5197-dba7-4d3a-a3f6-68ca5328b9a7', + 'image': {u'id': u'69c99b45-cd53-49de-afdc-f24789eb8f83'}, + 'interface_ip': u'', + 'instance_name': None, + 'key_name': u'mordred', + 'launched_at': u'2015-08-01T19:52:02.000000', + 'location': { + 'cloud': '_test_cloud_', + 'project': { + 'domain_id': None, + 'domain_name': None, + 'id': u'db92b20496ae4fbda850a689ea9d563f', + 'name': None}, + 'region_name': u'RegionOne', + 'zone': u'ca-ymq-2'}, + 'metadata': {u'group': u'irc', u'groups': u'irc,enabled'}, + 'name': u'mordred-irc', + 'networks': { + u'public': [ + u'2604:e100:1:0:f816:3eff:fe9f:463e', + u'162.253.54.192']}, + 'personality': None, + 'power_state': 1, + 'private_v4': None, + 'progress': 0, + 'properties': {}, + 'public_v4': None, + 'public_v6': None, + 'scheduler_hints': None, + 'security_groups': [{u'name': u'default'}], + 'status': u'ACTIVE', + 'tags': [], + 'task_state': None, + 'terminated_at': None, + 'updated': u'2016-10-15T15:49:29Z', + 'user_data': None, + 'user_id': u'e9b21dc437d149858faee0898fb08e92', + 'vm_state': u'active', + 'volumes': []} + self.cloud.strict_mode = True + retval = self.cloud.list_servers(bare=True)[0] + _assert_server_munch_attributes(self, expected, retval) + self.assertEqual(expected, retval) + + def test_normalize_secgroups(self): + nova_secgroup = dict( + id='abc123', + name='nova_secgroup', + description='A Nova security group', + rules=[ + dict(id='123', from_port=80, to_port=81, ip_protocol='tcp', + ip_range={'cidr': '0.0.0.0/0'}, parent_group_id='xyz123') + ] + ) + + expected = dict( + id='abc123', + name='nova_secgroup', + description='A Nova security group', + properties={}, + location=dict( + region_name='RegionOne', + zone=None, + project=dict( + domain_name='default', + id='1c36b64c840a42cd9e9b931a369337f0', + domain_id=None, + name='admin'), + cloud='_test_cloud_'), + security_group_rules=[ + dict(id='123', direction='ingress', ethertype='IPv4', + port_range_min=80, port_range_max=81, protocol='tcp', + remote_ip_prefix='0.0.0.0/0', security_group_id='xyz123', + properties={}, + remote_group_id=None, + location=dict( + region_name='RegionOne', + zone=None, + project=dict( + domain_name='default', + id='1c36b64c840a42cd9e9b931a369337f0', + domain_id=None, + name='admin'), + cloud='_test_cloud_')) + ] + ) + + retval = self.cloud._normalize_secgroup(nova_secgroup) + self.assertEqual(expected, retval) + + def test_normalize_volumes_v1(self): vol = dict( id='55db9e89-9cb4-4202-af88-d8c4a174998e', display_name='test', @@ -1007,7 +1017,7 @@ def test_normalize_volumes_v1_strict(self): 'project': { 'domain_id': None, 'domain_name': 'default', - 'id': mock.ANY, + 'id': '1c36b64c840a42cd9e9b931a369337f0', 'name': 'admin'}, 'region_name': u'RegionOne', 'zone': None}, @@ -1025,10 +1035,10 @@ def test_normalize_volumes_v1_strict(self): 'updated_at': None, 'volume_type': None, } - retval = self.strict_cloud._normalize_volume(vol) + retval = self.cloud._normalize_volume(vol) self.assertEqual(expected, retval) - def test_normalize_volumes_v2_strict(self): + def test_normalize_volumes_v2(self): vol = dict( id='55db9e89-9cb4-4202-af88-d8c4a174998e', name='test', @@ -1073,5 +1083,5 @@ def test_normalize_volumes_v2_strict(self): 'updated_at': None, 'volume_type': None, } - retval = self.strict_cloud._normalize_volume(vol) + retval = self.cloud._normalize_volume(vol) self.assertEqual(expected, retval) From e904cb6f4bc418c99366814eee68a4183a045c39 Mon Sep 17 00:00:00 2001 From: Thomas Bechtold Date: Thu, 13 Dec 2018 22:04:38 +0100 Subject: [PATCH 2322/3836] Drop self.conn from base.TestCase It's the same variable as self.cloud so just use that one in all tests. Change-Id: Ie2a526c420e7156888931832b55c5b1619d20452 --- openstack/tests/unit/base.py | 3 +-- .../tests/unit/object_store/v1/test_container.py | 8 ++++---- openstack/tests/unit/object_store/v1/test_obj.py | 4 ++-- openstack/tests/unit/object_store/v1/test_proxy.py | 4 ++-- openstack/tests/unit/test_connection.py | 12 ++++++------ 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 365f883fd..4ef8693e9 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -436,9 +436,8 @@ def _make_test_cloud(self, cloud_name='_test_cloud_', **kwargs): test_cloud = os.environ.get('OPENSTACKSDK_OS_CLOUD', cloud_name) self.cloud_config = self.config.get_one( cloud=test_cloud, validate=True, **kwargs) - self.conn = openstack.connection.Connection( + self.cloud = openstack.connection.Connection( config=self.cloud_config, strict=self.strict_cloud) - self.cloud = self.conn self.addCleanup(self.cloud.task_manager.stop) def get_glance_discovery_mock_dict( diff --git a/openstack/tests/unit/object_store/v1/test_container.py b/openstack/tests/unit/object_store/v1/test_container.py index 5d53e6532..1821160bc 100644 --- a/openstack/tests/unit/object_store/v1/test_container.py +++ b/openstack/tests/unit/object_store/v1/test_container.py @@ -21,7 +21,7 @@ class TestContainer(base.TestCase): def setUp(self): super(TestContainer, self).setUp() self.container = self.getUniqueString() - self.endpoint = self.conn.object_store.get_endpoint() + '/' + self.endpoint = self.cloud.object_store.get_endpoint() + '/' self.container_endpoint = '{endpoint}{container}'.format( endpoint=self.endpoint, container=self.container) @@ -118,7 +118,7 @@ def test_list(self): json=containers) ]) - response = container.Container.list(self.conn.object_store) + response = container.Container.list(self.cloud.object_store) self.assertEqual(len(containers), len(list(response))) for index, item in enumerate(response): @@ -143,7 +143,7 @@ def _test_create_update(self, sot, sot_call, sess_method): json=self.body, validate=dict(headers=headers)), ]) - sot_call(self.conn.object_store) + sot_call(self.cloud.object_store) self.assert_calls() @@ -199,7 +199,7 @@ def _test_no_headers(self, sot, sot_call, sess_method): headers=headers, json=data)) ]) - sot_call(self.conn.object_store) + sot_call(self.cloud.object_store) def test_create_no_headers(self): sot = container.Container.new(name=self.container) diff --git a/openstack/tests/unit/object_store/v1/test_obj.py b/openstack/tests/unit/object_store/v1/test_obj.py index f417bf7ce..5b5c58b49 100644 --- a/openstack/tests/unit/object_store/v1/test_obj.py +++ b/openstack/tests/unit/object_store/v1/test_obj.py @@ -121,7 +121,7 @@ def test_download(self): # the up-conversion works properly. sot.if_match = self.headers['Etag'] - rv = sot.download(self.conn.object_store) + rv = sot.download(self.cloud.object_store) self.assertEqual(self.the_data, rv) @@ -139,7 +139,7 @@ def _test_create(self, method, data): headers=sent_headers)) ]) - rv = sot.create(self.conn.object_store) + rv = sot.create(self.cloud.object_store) self.assertEqual(rv.etag, self.headers['Etag']) self.assert_calls() diff --git a/openstack/tests/unit/object_store/v1/test_proxy.py b/openstack/tests/unit/object_store/v1/test_proxy.py index f6634046a..8c56328ec 100644 --- a/openstack/tests/unit/object_store/v1/test_proxy.py +++ b/openstack/tests/unit/object_store/v1/test_proxy.py @@ -280,7 +280,7 @@ def setUp(self): content=self.the_data)]) def test_download(self): - data = self.conn.object_store.download_object( + data = self.cloud.object_store.download_object( self.object, container=self.container) self.assertEqual(data, self.the_data) @@ -288,7 +288,7 @@ def test_download(self): def test_stream(self): chunk_size = 2 - for index, chunk in enumerate(self.conn.object_store.stream_object( + for index, chunk in enumerate(self.cloud.object_store.stream_object( self.object, container=self.container, chunk_size=chunk_size)): chunk_len = len(chunk) diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index 10904a5a1..f27ce67b9 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -235,11 +235,11 @@ def test_network_proxy(self): self.use_keystone_v3(catalog='catalog-v3-suffix.json') self.assertEqual( 'openstack.network.v2._proxy', - self.conn.network.__class__.__module__) + self.cloud.network.__class__.__module__) self.assert_calls() self.assertEqual( "https://network.example.com/v2.0", - self.conn.network.get_endpoint()) + self.cloud.network.get_endpoint()) class TestNetworkConnectionSuffix(base.TestCase): @@ -249,21 +249,21 @@ class TestNetworkConnectionSuffix(base.TestCase): def test_network_proxy(self): self.assertEqual( 'openstack.network.v2._proxy', - self.conn.network.__class__.__module__) + self.cloud.network.__class__.__module__) self.assert_calls() self.assertEqual( "https://network.example.com/v2.0", - self.conn.network.get_endpoint()) + self.cloud.network.get_endpoint()) class TestAuthorize(base.TestCase): def test_authorize_works(self): - res = self.conn.authorize() + res = self.cloud.authorize() self.assertEqual('KeystoneToken-1', res) def test_authorize_failure(self): self.use_broken_keystone() self.assertRaises(openstack.exceptions.HttpException, - self.conn.authorize) + self.cloud.authorize) From 8274409c9e79a0ea40e61fdd5a97c326a6f94ecc Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 20 Dec 2018 16:21:42 +0100 Subject: [PATCH 2323/3836] Add possibility to override base_path for resource operations We currently have multiple places, where we inherit resource just to be able to request it's Details or so under different url. With 619594 a change has been introduced to improve this by having possibility to override base_path for a single resource operation. This was done only for list operations. However there are also cases when such possibility would be nice for create/update/etc operations. One example is dry-run for heat, where the endpoint receives "/preview" part. This is currently implemented by creating additional resource StackPreview with different base_path. For create it might be ok, but dry-run stack update would require another url. With this change a possibility to override base_path for individual operation on resource object is added. It can be given to proxy function, which would pass it to resource and use it during URI calculation. Change-Id: Id8c8212249cb985d2e47eb1d4fb23ebf19b3871b --- openstack/compute/v2/keypair.py | 2 +- openstack/compute/v2/limits.py | 6 +- openstack/compute/v2/server.py | 3 +- openstack/compute/v2/server_ip.py | 2 +- openstack/database/v1/user.py | 3 +- openstack/identity/v2/extension.py | 2 +- openstack/identity/version.py | 2 +- openstack/image/v2/image.py | 5 +- openstack/key_manager/v1/secret.py | 6 +- openstack/load_balancer/v2/quota.py | 6 +- openstack/message/v2/claim.py | 17 +++-- openstack/message/v2/message.py | 8 ++- openstack/message/v2/queue.py | 13 ++-- openstack/message/v2/subscription.py | 10 +-- openstack/network/v2/quota.py | 3 +- openstack/object_store/v1/container.py | 4 +- openstack/object_store/v1/obj.py | 4 +- openstack/orchestration/v1/software_config.py | 5 +- .../orchestration/v1/software_deployment.py | 8 +-- openstack/orchestration/v1/stack.py | 13 ++-- openstack/orchestration/v1/stack_files.py | 4 +- openstack/proxy.py | 38 ++++++++--- openstack/resource.py | 45 ++++++++---- openstack/tests/unit/message/v2/test_proxy.py | 6 +- .../tests/unit/orchestration/v1/test_stack.py | 6 +- openstack/tests/unit/test_proxy.py | 63 ++++++++++++++--- openstack/tests/unit/test_proxy_base.py | 20 +++++- openstack/tests/unit/test_resource.py | 68 ++++++++++++++++--- openstack/workflow/v2/execution.py | 5 +- openstack/workflow/v2/workflow.py | 5 +- 30 files changed, 278 insertions(+), 104 deletions(-) diff --git a/openstack/compute/v2/keypair.py b/openstack/compute/v2/keypair.py index 51109632c..1db894f81 100644 --- a/openstack/compute/v2/keypair.py +++ b/openstack/compute/v2/keypair.py @@ -51,7 +51,7 @@ def _consume_attrs(self, mapping, attrs): return super(Keypair, self)._consume_attrs(mapping, attrs) @classmethod - def list(cls, session, paginated=False): + def list(cls, session, paginated=False, base_path=None): resp = session.get(cls.base_path, headers={"Accept": "application/json"}) resp = resp.json() diff --git a/openstack/compute/v2/limits.py b/openstack/compute/v2/limits.py index b08b29219..0f019897f 100644 --- a/openstack/compute/v2/limits.py +++ b/openstack/compute/v2/limits.py @@ -76,7 +76,8 @@ class Limits(resource.Resource): absolute = resource.Body("absolute", type=AbsoluteLimits) rate = resource.Body("rate", type=list, list_type=RateLimit) - def fetch(self, session, requires_id=False, error_message=None): + def fetch(self, session, requires_id=False, error_message=None, + base_path=None): """Get the Limits resource. :param session: The session to use for making this request. @@ -88,4 +89,5 @@ def fetch(self, session, requires_id=False, error_message=None): # TODO(mordred) We shouldn't have to subclass just to declare # requires_id = False. return super(Limits, self).fetch( - session=session, requires_id=False, error_message=error_message) + session=session, requires_id=False, error_message=error_message, + base_path=base_path) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 11294fb62..a9db33f9a 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -138,7 +138,8 @@ class Server(resource.Resource, metadata.MetadataMixin, resource.TagMixin): #: only. instance_name = resource.Body('OS-EXT-SRV-ATTR:instance_name') - def _prepare_request(self, requires_id=True, prepend_key=True): + def _prepare_request(self, requires_id=True, prepend_key=True, + base_path=None): request = super(Server, self)._prepare_request(requires_id=requires_id, prepend_key=prepend_key) diff --git a/openstack/compute/v2/server_ip.py b/openstack/compute/v2/server_ip.py index bc868fb6c..d3b9dd236 100644 --- a/openstack/compute/v2/server_ip.py +++ b/openstack/compute/v2/server_ip.py @@ -33,7 +33,7 @@ class ServerIP(resource.Resource): @classmethod def list(cls, session, paginated=False, server_id=None, - network_label=None, **params): + network_label=None, base_path=None, **params): url = cls.base_path % {"server_id": server_id} if network_label is not None: diff --git a/openstack/database/v1/user.py b/openstack/database/v1/user.py index c3a0d3d6e..a18f0b898 100644 --- a/openstack/database/v1/user.py +++ b/openstack/database/v1/user.py @@ -34,7 +34,8 @@ class User(resource.Resource): #: The password of the user password = resource.Body('password') - def _prepare_request(self, requires_id=True, prepend_key=True): + def _prepare_request(self, requires_id=True, prepend_key=True, + base_path=None): """Prepare a request for the database service's create call User.create calls require the resources_key. diff --git a/openstack/identity/v2/extension.py b/openstack/identity/v2/extension.py index 153d6df91..5f95a499d 100644 --- a/openstack/identity/v2/extension.py +++ b/openstack/identity/v2/extension.py @@ -43,7 +43,7 @@ class Extension(resource.Resource): updated_at = resource.Body('updated') @classmethod - def list(cls, session, paginated=False, **params): + def list(cls, session, paginated=False, base_path=None, **params): resp = session.get(cls.base_path, params=params) resp = resp.json() diff --git a/openstack/identity/version.py b/openstack/identity/version.py index 3ad27ee86..710328055 100644 --- a/openstack/identity/version.py +++ b/openstack/identity/version.py @@ -27,7 +27,7 @@ class Version(resource.Resource): updated = resource.Body('updated') @classmethod - def list(cls, session, paginated=False, **params): + def list(cls, session, paginated=False, base_path=None, **params): resp = session.get(cls.base_path, params=params) resp = resp.json() diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index 7889bc1d2..0c121b11d 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -278,10 +278,11 @@ def download(self, session, stream=False): return resp.content def _prepare_request(self, requires_id=None, prepend_key=False, - patch=False): + patch=False, base_path=None): request = super(Image, self)._prepare_request(requires_id=requires_id, prepend_key=prepend_key, - patch=patch) + patch=patch, + base_path=base_path) if patch: headers = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', diff --git a/openstack/key_manager/v1/secret.py b/openstack/key_manager/v1/secret.py index 4e2be6c46..7cb8c23f9 100644 --- a/openstack/key_manager/v1/secret.py +++ b/openstack/key_manager/v1/secret.py @@ -77,8 +77,10 @@ class Secret(resource.Resource): #: (required if payload is encoded) payload_content_encoding = resource.Body('payload_content_encoding') - def fetch(self, session, requires_id=True, error_message=None): - request = self._prepare_request(requires_id=requires_id) + def fetch(self, session, requires_id=True, + base_path=None, error_message=None): + request = self._prepare_request(requires_id=requires_id, + base_path=base_path) response = session.get(request.url).json() diff --git a/openstack/load_balancer/v2/quota.py b/openstack/load_balancer/v2/quota.py index 77799f7d1..a14f62812 100644 --- a/openstack/load_balancer/v2/quota.py +++ b/openstack/load_balancer/v2/quota.py @@ -41,9 +41,11 @@ class Quota(resource.Resource): #: The ID of the project this quota is associated with. project_id = resource.Body('project_id', alternate_id=True) - def _prepare_request(self, requires_id=True, prepend_key=False): + def _prepare_request(self, requires_id=True, + base_path=None, prepend_key=False): _request = super(Quota, self)._prepare_request(requires_id, - prepend_key) + prepend_key, + base_path=base_path) if self.resource_key in _request.body: _body = _request.body[self.resource_key] else: diff --git a/openstack/message/v2/claim.py b/openstack/message/v2/claim.py index f02e51748..ba9075338 100644 --- a/openstack/message/v2/claim.py +++ b/openstack/message/v2/claim.py @@ -61,9 +61,10 @@ def _translate_response(self, response, has_body=True): # Extract claim ID from location self.id = self.location.split("claims/")[1] - def create(self, session, prepend_key=False): + def create(self, session, prepend_key=False, base_path=None): request = self._prepare_request(requires_id=False, - prepend_key=prepend_key) + prepend_key=prepend_key, + base_path=base_path) headers = { "Client-ID": self.client_id or str(uuid.uuid4()), "X-PROJECT-ID": self.project_id or session.get_project_id() @@ -80,8 +81,10 @@ def create(self, session, prepend_key=False): return self - def fetch(self, session, requires_id=True, error_message=None): - request = self._prepare_request(requires_id=requires_id) + def fetch(self, session, requires_id=True, + base_path=None, error_message=None): + request = self._prepare_request(requires_id=requires_id, + base_path=base_path) headers = { "Client-ID": self.client_id or str(uuid.uuid4()), "X-PROJECT-ID": self.project_id or session.get_project_id() @@ -94,8 +97,10 @@ def fetch(self, session, requires_id=True, error_message=None): return self - def commit(self, session, prepend_key=False, has_body=False): - request = self._prepare_request(prepend_key=prepend_key) + def commit(self, session, prepend_key=False, has_body=False, + base_path=None): + request = self._prepare_request(prepend_key=prepend_key, + base_path=base_path) headers = { "Client-ID": self.client_id or str(uuid.uuid4()), "X-PROJECT-ID": self.project_id or session.get_project_id() diff --git a/openstack/message/v2/message.py b/openstack/message/v2/message.py index 5f7acb090..a8818ace8 100644 --- a/openstack/message/v2/message.py +++ b/openstack/message/v2/message.py @@ -67,7 +67,7 @@ def post(self, session, messages): return response.json()['resources'] @classmethod - def list(cls, session, paginated=True, **params): + def list(cls, session, paginated=True, base_path=None, **params): """This method is a generator which yields message objects. This is almost the copy of list method of resource.Resource class. @@ -107,8 +107,10 @@ def list(cls, session, paginated=True, **params): query_params["limit"] = yielded query_params["marker"] = new_marker - def fetch(self, session, requires_id=True, error_message=None): - request = self._prepare_request(requires_id=requires_id) + def fetch(self, session, requires_id=True, + base_path=None, error_message=None): + request = self._prepare_request(requires_id=requires_id, + base_path=base_path) headers = { "Client-ID": self.client_id or str(uuid.uuid4()), "X-PROJECT-ID": self.project_id or session.get_project_id() diff --git a/openstack/message/v2/queue.py b/openstack/message/v2/queue.py index 7901f1959..7b65bb881 100644 --- a/openstack/message/v2/queue.py +++ b/openstack/message/v2/queue.py @@ -50,9 +50,10 @@ class Queue(resource.Resource): #: in case keystone auth is not enabled in Zaqar service. project_id = resource.Header("X-PROJECT-ID") - def create(self, session, prepend_key=True): + def create(self, session, prepend_key=True, base_path=None): request = self._prepare_request(requires_id=True, - prepend_key=prepend_key) + prepend_key=prepend_key, + base_path=None) headers = { "Client-ID": self.client_id or str(uuid.uuid4()), "X-PROJECT-ID": self.project_id or session.get_project_id() @@ -65,7 +66,7 @@ def create(self, session, prepend_key=True): return self @classmethod - def list(cls, session, paginated=False, **params): + def list(cls, session, paginated=False, base_path=None, **params): """This method is a generator which yields queue objects. This is almost the copy of list method of resource.Resource class. @@ -105,8 +106,10 @@ def list(cls, session, paginated=False, **params): query_params["limit"] = yielded query_params["marker"] = new_marker - def fetch(self, session, requires_id=True, error_message=None): - request = self._prepare_request(requires_id=requires_id) + def fetch(self, session, requires_id=True, + base_path=None, error_message=None): + request = self._prepare_request(requires_id=requires_id, + base_path=base_path) headers = { "Client-ID": self.client_id or str(uuid.uuid4()), "X-PROJECT-ID": self.project_id or session.get_project_id() diff --git a/openstack/message/v2/subscription.py b/openstack/message/v2/subscription.py index 7372951b8..b3960c0fd 100644 --- a/openstack/message/v2/subscription.py +++ b/openstack/message/v2/subscription.py @@ -58,7 +58,7 @@ class Subscription(resource.Resource): #: authentication is not enabled in Zaqar service. project_id = resource.Header("X-PROJECT-ID") - def create(self, session, prepend_key=True): + def create(self, session, prepend_key=True, base_path=None): request = self._prepare_request(requires_id=False, prepend_key=prepend_key) headers = { @@ -73,7 +73,7 @@ def create(self, session, prepend_key=True): return self @classmethod - def list(cls, session, paginated=True, **params): + def list(cls, session, paginated=True, base_path=None, **params): """This method is a generator which yields subscription objects. This is almost the copy of list method of resource.Resource class. @@ -113,8 +113,10 @@ def list(cls, session, paginated=True, **params): query_params["limit"] = yielded query_params["marker"] = new_marker - def fetch(self, session, requires_id=True, error_message=None): - request = self._prepare_request(requires_id=requires_id) + def fetch(self, session, requires_id=True, + base_path=None, error_message=None): + request = self._prepare_request(requires_id=requires_id, + base_path=base_path) headers = { "Client-ID": self.client_id or str(uuid.uuid4()), "X-PROJECT-ID": self.project_id or session.get_project_id() diff --git a/openstack/network/v2/quota.py b/openstack/network/v2/quota.py index eab5aa79b..87432718c 100644 --- a/openstack/network/v2/quota.py +++ b/openstack/network/v2/quota.py @@ -56,7 +56,8 @@ class Quota(resource.Resource): #: The maximum amount of security groups you can create. *Type: int* security_groups = resource.Body('security_group', type=int) - def _prepare_request(self, requires_id=True, prepend_key=False): + def _prepare_request(self, requires_id=True, prepend_key=False, + base_path=None): _request = super(Quota, self)._prepare_request(requires_id, prepend_key) if self.resource_key in _request.body: diff --git a/openstack/object_store/v1/container.py b/openstack/object_store/v1/container.py index d45bcd864..04c0d45e4 100644 --- a/openstack/object_store/v1/container.py +++ b/openstack/object_store/v1/container.py @@ -109,7 +109,7 @@ def new(cls, **kwargs): kwargs.setdefault('name', name) return Container(_synchronized=True, **kwargs) - def create(self, session, prepend_key=True): + def create(self, session, prepend_key=True, base_path=None): """Create a remote resource based on this instance. :param session: The session to use for making this request. @@ -123,7 +123,7 @@ def create(self, session, prepend_key=True): :data:`Resource.allow_create` is not set to ``True``. """ request = self._prepare_request( - requires_id=True, prepend_key=prepend_key) + requires_id=True, prepend_key=prepend_key, base_path=base_path) response = session.put( request.url, json=request.body, headers=request.headers) diff --git a/openstack/object_store/v1/obj.py b/openstack/object_store/v1/obj.py index 41daa9d28..2ef16529f 100644 --- a/openstack/object_store/v1/obj.py +++ b/openstack/object_store/v1/obj.py @@ -285,8 +285,8 @@ def stream(self, session, error_message=None, chunk_size=1024): session, error_message=error_message, stream=True) return response.iter_content(chunk_size, decode_unicode=False) - def create(self, session): - request = self._prepare_request() + def create(self, session, base_path=None): + request = self._prepare_request(base_path=base_path) request.headers['Accept'] = '' response = session.put( diff --git a/openstack/orchestration/v1/software_config.py b/openstack/orchestration/v1/software_config.py index af59e8989..6107068d5 100644 --- a/openstack/orchestration/v1/software_config.py +++ b/openstack/orchestration/v1/software_config.py @@ -45,7 +45,8 @@ class SoftwareConfig(resource.Resource): #: produces. outputs = resource.Body('outputs') - def create(self, session): + def create(self, session, base_path=None): # This overrides the default behavior of resource creation because # heat doesn't accept resource_key in its request. - return super(SoftwareConfig, self).create(session, prepend_key=False) + return super(SoftwareConfig, self).create(session, prepend_key=False, + base_path=base_path) diff --git a/openstack/orchestration/v1/software_deployment.py b/openstack/orchestration/v1/software_deployment.py index 5ac670973..2c091b342 100644 --- a/openstack/orchestration/v1/software_deployment.py +++ b/openstack/orchestration/v1/software_deployment.py @@ -49,14 +49,14 @@ class SoftwareDeployment(resource.Resource): #: The date and time when the software deployment resource was created. updated_at = resource.Body('updated_time') - def create(self, session): + def create(self, session, base_path=None): # This overrides the default behavior of resource creation because # heat doesn't accept resource_key in its request. return super(SoftwareDeployment, self).create( - session, prepend_key=False) + session, prepend_key=False, base_path=base_path) - def commit(self, session): + def commit(self, session, base_path=None): # This overrides the default behavior of resource creation because # heat doesn't accept resource_key in its request. return super(SoftwareDeployment, self).commit( - session, prepend_key=False) + session, prepend_key=False, base_path=base_path) diff --git a/openstack/orchestration/v1/stack.py b/openstack/orchestration/v1/stack.py index 97bb6e3f6..c2e464293 100644 --- a/openstack/orchestration/v1/stack.py +++ b/openstack/orchestration/v1/stack.py @@ -74,16 +74,17 @@ class Stack(resource.Resource): #: The ID of the user project created for this stack. user_project_id = resource.Body('stack_user_project_id') - def create(self, session): + def create(self, session, base_path=None): # This overrides the default behavior of resource creation because # heat doesn't accept resource_key in its request. - return super(Stack, self).create(session, prepend_key=False) + return super(Stack, self).create(session, prepend_key=False, + base_path=base_path) - def commit(self, session): + def commit(self, session, base_path=None): # This overrides the default behavior of resource creation because # heat doesn't accept resource_key in its request. return super(Stack, self).commit(session, prepend_key=False, - has_body=False) + has_body=False, base_path=None) def _action(self, session, body): """Perform stack actions""" @@ -94,10 +95,12 @@ def _action(self, session, body): def check(self, session): return self._action(session, {'check': ''}) - def fetch(self, session, requires_id=True, error_message=None): + def fetch(self, session, requires_id=True, + base_path=None, error_message=None): stk = super(Stack, self).fetch( session, requires_id=requires_id, + base_path=base_path, error_message=error_message) if stk and stk.status in ['DELETE_COMPLETE', 'ADOPT_COMPLETE']: raise exceptions.ResourceNotFound( diff --git a/openstack/orchestration/v1/stack_files.py b/openstack/orchestration/v1/stack_files.py index faa4c942b..98b28aa37 100644 --- a/openstack/orchestration/v1/stack_files.py +++ b/openstack/orchestration/v1/stack_files.py @@ -34,9 +34,9 @@ class StackFiles(resource.Resource): # Backwards compat stack_id = id - def fetch(self, session): + def fetch(self, session, base_path=None): # The stack files response contains a map of filenames and file # contents. - request = self._prepare_request(requires_id=False) + request = self._prepare_request(requires_id=False, base_path=base_path) resp = session.get(request.url) return resp.json() diff --git a/openstack/proxy.py b/openstack/proxy.py index 94a43480f..339f5dc93 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -161,7 +161,7 @@ def _delete(self, resource_type, value, ignore_missing=True, **attrs): return rv @_check_resource(strict=False) - def _update(self, resource_type, value, **attrs): + def _update(self, resource_type, value, base_path=None, **attrs): """Update a resource :param resource_type: The type of resource to update. @@ -169,6 +169,9 @@ def _update(self, resource_type, value, **attrs): :param value: The resource to update. This must either be a :class:`~openstack.resource.Resource` or an id that corresponds to a resource. + :param str base_path: Base part of the URI for updating resources, if + different from + :data:`~openstack.resource.Resource.base_path`. :param dict attrs: Attributes to be passed onto the :meth:`~openstack.resource.Resource.update` method to be updated. These should correspond @@ -180,13 +183,16 @@ def _update(self, resource_type, value, **attrs): :rtype: :class:`~openstack.resource.Resource` """ res = self._get_resource(resource_type, value, **attrs) - return res.commit(self) + return res.commit(self, base_path=base_path) - def _create(self, resource_type, **attrs): + def _create(self, resource_type, base_path=None, **attrs): """Create a resource from attributes :param resource_type: The type of resource to create. :type resource_type: :class:`~openstack.resource.Resource` + :param str base_path: Base part of the URI for creating resources, if + different from + :data:`~openstack.resource.Resource.base_path`. :param path_args: A dict containing arguments for forming the request URL, if needed. :param dict attrs: Attributes to be passed onto the @@ -200,10 +206,11 @@ def _create(self, resource_type, **attrs): :rtype: :class:`~openstack.resource.Resource` """ res = resource_type.new(**attrs) - return res.create(self) + return res.create(self, base_path=base_path) @_check_resource(strict=False) - def _get(self, resource_type, value=None, requires_id=True, **attrs): + def _get(self, resource_type, value=None, requires_id=True, + base_path=None, **attrs): """Fetch a resource :param resource_type: The type of resource to get. @@ -211,6 +218,9 @@ def _get(self, resource_type, value=None, requires_id=True, **attrs): :param value: The value to get. Can be either the ID of a resource or a :class:`~openstack.resource.Resource` subclass. + :param str base_path: Base part of the URI for fetching resources, if + different from + :data:`~openstack.resource.Resource.base_path`. :param dict attrs: Attributes to be passed onto the :meth:`~openstack.resource.Resource.get` method. These should correspond @@ -224,11 +234,12 @@ def _get(self, resource_type, value=None, requires_id=True, **attrs): res = self._get_resource(resource_type, value, **attrs) return res.fetch( - self, requires_id=requires_id, + self, requires_id=requires_id, base_path=base_path, error_message="No {resource_type} found for {value}".format( resource_type=resource_type.__name__, value=value)) - def _list(self, resource_type, value=None, paginated=False, **attrs): + def _list(self, resource_type, value=None, + paginated=False, base_path=None, **attrs): """List a resource :param resource_type: The type of resource to delete. This should @@ -241,6 +252,9 @@ def _list(self, resource_type, value=None, paginated=False, **attrs): to be returned in one response. When set to ``True``, the resource supports data being returned across multiple pages. + :param str base_path: Base part of the URI for listing resources, if + different from + :data:`~openstack.resource.Resource.base_path`. :param dict attrs: Attributes to be passed onto the :meth:`~openstack.resource.Resource.list` method. These should correspond to either :class:`~openstack.resource.URI` values @@ -252,9 +266,10 @@ def _list(self, resource_type, value=None, paginated=False, **attrs): the ``resource_type``. """ res = self._get_resource(resource_type, value, **attrs) - return res.list(self, paginated=paginated, **attrs) + return res.list(self, paginated=paginated, + base_path=base_path, **attrs) - def _head(self, resource_type, value=None, **attrs): + def _head(self, resource_type, value=None, base_path=None, **attrs): """Retrieve a resource's header :param resource_type: The type of resource to retrieve. @@ -263,6 +278,9 @@ def _head(self, resource_type, value=None, **attrs): for. Can be either the ID of a resource, a :class:`~openstack.resource.Resource` subclass, or ``None``. + :param str base_path: Base part of the URI for heading resources, if + different from + :data:`~openstack.resource.Resource.base_path`. :param dict attrs: Attributes to be passed onto the :meth:`~openstack.resource.Resource.head` method. These should correspond to @@ -272,4 +290,4 @@ def _head(self, resource_type, value=None, **attrs): :rtype: :class:`~openstack.resource.Resource` """ res = self._get_resource(resource_type, value, **attrs) - return res.head(self) + return res.head(self, base_path=base_path) diff --git a/openstack/resource.py b/openstack/resource.py index b1c5cc2af..a0313ce95 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -884,7 +884,7 @@ def _prepare_request_body(self, patch, prepend_key): return body def _prepare_request(self, requires_id=None, prepend_key=False, - patch=False): + patch=False, base_path=None): """Prepare a request to be sent to the server Create operations don't require an ID, but all others do, @@ -912,7 +912,9 @@ def _prepare_request(self, requires_id=None, prepend_key=False, else: headers[k] = str(v) - uri = self.base_path % self._uri.attributes + if base_path is None: + base_path = self.base_path + uri = base_path % self._uri.attributes if requires_id: if self.id is None: raise exceptions.InvalidRequest( @@ -1047,7 +1049,7 @@ def _raise(message): return actual - def create(self, session, prepend_key=True): + def create(self, session, prepend_key=True, base_path=None): """Create a remote resource based on this instance. :param session: The session to use for making this request. @@ -1055,7 +1057,9 @@ def create(self, session, prepend_key=True): :param prepend_key: A boolean indicating whether the resource_key should be prepended in a resource creation request. Default to True. - + :param str base_path: Base part of the URI for creating resources, if + different from + :data:`~openstack.resource.Resource.base_path`. :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if :data:`Resource.allow_create` is not set to ``True``. @@ -1067,13 +1071,15 @@ def create(self, session, prepend_key=True): microversion = self._get_microversion_for(session, 'create') if self.create_method == 'PUT': request = self._prepare_request(requires_id=True, - prepend_key=prepend_key) + prepend_key=prepend_key, + base_path=base_path) response = session.put(request.url, json=request.body, headers=request.headers, microversion=microversion) elif self.create_method == 'POST': request = self._prepare_request(requires_id=False, - prepend_key=prepend_key) + prepend_key=prepend_key, + base_path=base_path) response = session.post(request.url, json=request.body, headers=request.headers, microversion=microversion) @@ -1085,13 +1091,19 @@ def create(self, session, prepend_key=True): self._translate_response(response) return self - def fetch(self, session, requires_id=True, error_message=None): + def fetch(self, session, requires_id=True, + base_path=None, error_message=None): """Get a remote resource based on this instance. :param session: The session to use for making this request. :type session: :class:`~keystoneauth1.adapter.Adapter` :param boolean requires_id: A boolean indicating whether resource ID should be part of the requested URI. + :param str base_path: Base part of the URI for fetching resources, if + different from + :data:`~openstack.resource.Resource.base_path`. + :param str error_message: An Error message to be returned if + requested object does not exist. :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if :data:`Resource.allow_fetch` is not set to ``True``. @@ -1101,7 +1113,8 @@ def fetch(self, session, requires_id=True, error_message=None): if not self.allow_fetch: raise exceptions.MethodNotSupported(self, "fetch") - request = self._prepare_request(requires_id=requires_id) + request = self._prepare_request(requires_id=requires_id, + base_path=base_path) session = self._get_session(session) microversion = self._get_microversion_for(session, 'fetch') response = session.get(request.url, microversion=microversion) @@ -1113,11 +1126,14 @@ def fetch(self, session, requires_id=True, error_message=None): self._translate_response(response, **kwargs) return self - def head(self, session): + def head(self, session, base_path=None): """Get headers from a remote resource based on this instance. :param session: The session to use for making this request. :type session: :class:`~keystoneauth1.adapter.Adapter` + :param str base_path: Base part of the URI for fetching resources, if + different from + :data:`~openstack.resource.Resource.base_path`. :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if @@ -1128,7 +1144,7 @@ def head(self, session): if not self.allow_head: raise exceptions.MethodNotSupported(self, "head") - request = self._prepare_request() + request = self._prepare_request(base_path=base_path) session = self._get_session(session) microversion = self._get_microversion_for(session, 'fetch') @@ -1141,7 +1157,7 @@ def head(self, session): return self def commit(self, session, prepend_key=True, has_body=True, - retry_on_conflict=None): + retry_on_conflict=None, base_path=None): """Commit the state of the instance to the remote resource. :param session: The session to use for making this request. @@ -1152,6 +1168,9 @@ def commit(self, session, prepend_key=True, has_body=True, :param bool retry_on_conflict: Whether to enable retries on HTTP CONFLICT (409). Value of ``None`` leaves the `Adapter` defaults. + :param str base_path: Base part of the URI for modifying resources, if + different from + :data:`~openstack.resource.Resource.base_path`. :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if @@ -1173,7 +1192,9 @@ def commit(self, session, prepend_key=True, has_body=True, if self.commit_jsonpatch: kwargs['patch'] = True - request = self._prepare_request(prepend_key=prepend_key, **kwargs) + request = self._prepare_request(prepend_key=prepend_key, + base_path=base_path, + **kwargs) session = self._get_session(session) kwargs = {} diff --git a/openstack/tests/unit/message/v2/test_proxy.py b/openstack/tests/unit/message/v2/test_proxy.py index fd9fb839c..f9ea86f71 100644 --- a/openstack/tests/unit/message/v2/test_proxy.py +++ b/openstack/tests/unit/message/v2/test_proxy.py @@ -130,7 +130,8 @@ def test_message_delete_ignore(self, mock_get_resource): def test_subscription_create(self): self._verify("openstack.message.v2.subscription.Subscription.create", self.proxy.create_subscription, - method_args=["test_queue"]) + method_args=["test_queue"], + expected_kwargs={"base_path": None}) @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_subscription_get(self, mock_get_resource): @@ -175,7 +176,8 @@ def test_subscription_delete_ignore(self, mock_get_resource): def test_claim_create(self): self._verify("openstack.message.v2.claim.Claim.create", self.proxy.create_claim, - method_args=["test_queue"]) + method_args=["test_queue"], + expected_kwargs={"base_path": None}) def test_claim_get(self): self._verify2("openstack.proxy.Proxy._get", diff --git a/openstack/tests/unit/orchestration/v1/test_stack.py b/openstack/tests/unit/orchestration/v1/test_stack.py index c25084096..967265ce0 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack.py +++ b/openstack/tests/unit/orchestration/v1/test_stack.py @@ -93,7 +93,8 @@ def test_create(self, mock_create): res = sot.create(sess) - mock_create.assert_called_once_with(sess, prepend_key=False) + mock_create.assert_called_once_with(sess, prepend_key=False, + base_path=None) self.assertEqual(mock_create.return_value, res) @mock.patch.object(resource.Resource, 'commit') @@ -104,7 +105,8 @@ def test_commit(self, mock_commit): res = sot.commit(sess) mock_commit.assert_called_once_with(sess, prepend_key=False, - has_body=False) + has_body=False, + base_path=None) self.assertEqual(mock_commit.return_value, res) def test_check(self): diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index c03759d07..297a06a0a 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -254,13 +254,22 @@ def test_update_resource(self): self.assertEqual(rv, self.fake_result) self.res._update.assert_called_once_with(**self.attrs) - self.res.commit.assert_called_once_with(self.sot) + self.res.commit.assert_called_once_with(self.sot, base_path=None) + + def test_update_resource_override_base_path(self): + base_path = 'dummy' + rv = self.sot._update(UpdateableResource, self.res, + base_path=base_path, **self.attrs) + + self.assertEqual(rv, self.fake_result) + self.res._update.assert_called_once_with(**self.attrs) + self.res.commit.assert_called_once_with(self.sot, base_path=base_path) def test_update_id(self): rv = self.sot._update(UpdateableResource, self.fake_id, **self.attrs) self.assertEqual(rv, self.fake_result) - self.res.commit.assert_called_once_with(self.sot) + self.res.commit.assert_called_once_with(self.sot, base_path=None) class TestProxyCreate(base.TestCase): @@ -284,7 +293,18 @@ def test_create_attributes(self): self.assertEqual(rv, self.fake_result) CreateableResource.new.assert_called_once_with(**attrs) - self.res.create.assert_called_once_with(self.sot) + self.res.create.assert_called_once_with(self.sot, base_path=None) + + def test_create_attributes_override_base_path(self): + CreateableResource.new = mock.Mock(return_value=self.res) + + base_path = 'dummy' + attrs = {"x": 1, "y": 2, "z": 3} + rv = self.sot._create(CreateableResource, base_path=base_path, **attrs) + + self.assertEqual(rv, self.fake_result) + CreateableResource.new.assert_called_once_with(**attrs) + self.res.create.assert_called_once_with(self.sot, base_path=base_path) class TestProxyGet(base.TestCase): @@ -309,6 +329,7 @@ def test_get_resource(self): self.res.fetch.assert_called_with( self.sot, requires_id=True, + base_path=None, error_message=mock.ANY) self.assertEqual(rv, self.fake_result) @@ -318,7 +339,7 @@ def test_get_resource_with_args(self): self.res._update.assert_called_once_with(**args) self.res.fetch.assert_called_with( - self.sot, requires_id=True, + self.sot, requires_id=True, base_path=None, error_message=mock.ANY) self.assertEqual(rv, self.fake_result) @@ -327,7 +348,18 @@ def test_get_id(self): RetrieveableResource.new.assert_called_with(id=self.fake_id) self.res.fetch.assert_called_with( - self.sot, requires_id=True, + self.sot, requires_id=True, base_path=None, + error_message=mock.ANY) + self.assertEqual(rv, self.fake_result) + + def test_get_base_path(self): + base_path = 'dummy' + rv = self.sot._get(RetrieveableResource, self.fake_id, + base_path=base_path) + + RetrieveableResource.new.assert_called_with(id=self.fake_id) + self.res.fetch.assert_called_with( + self.sot, requires_id=True, base_path=base_path, error_message=mock.ANY) self.assertEqual(rv, self.fake_result) @@ -354,12 +386,13 @@ def setUp(self): ListableResource.list = mock.Mock() ListableResource.list.return_value = self.fake_response - def _test_list(self, paginated): - rv = self.sot._list(ListableResource, paginated=paginated, **self.args) + def _test_list(self, paginated, base_path=None): + rv = self.sot._list(ListableResource, paginated=paginated, + base_path=base_path, **self.args) self.assertEqual(self.fake_response, rv) ListableResource.list.assert_called_once_with( - self.sot, paginated=paginated, **self.args) + self.sot, paginated=paginated, base_path=base_path, **self.args) def test_list_paginated(self): self._test_list(True) @@ -367,6 +400,9 @@ def test_list_paginated(self): def test_list_non_paginated(self): self._test_list(False) + def test_list_override_base_path(self): + self._test_list(False, base_path='dummy') + class TestProxyHead(base.TestCase): @@ -388,12 +424,19 @@ def setUp(self): def test_head_resource(self): rv = self.sot._head(HeadableResource, self.res) - self.res.head.assert_called_with(self.sot) + self.res.head.assert_called_with(self.sot, base_path=None) + self.assertEqual(rv, self.fake_result) + + def test_head_resource_base_path(self): + base_path = 'dummy' + rv = self.sot._head(HeadableResource, self.res, base_path=base_path) + + self.res.head.assert_called_with(self.sot, base_path=base_path) self.assertEqual(rv, self.fake_result) def test_head_id(self): rv = self.sot._head(HeadableResource, self.fake_id) HeadableResource.new.assert_called_with(id=self.fake_id) - self.res.head.assert_called_with(self.sot) + self.res.head.assert_called_with(self.sot, base_path=None) self.assertEqual(rv, self.fake_result) diff --git a/openstack/tests/unit/test_proxy_base.py b/openstack/tests/unit/test_proxy_base.py index c2c0dab97..849597d6c 100644 --- a/openstack/tests/unit/test_proxy_base.py +++ b/openstack/tests/unit/test_proxy_base.py @@ -76,7 +76,17 @@ def _verify2(self, mock_method, test_method, else: self.assertEqual(expected_result, test_method(*method_args, **method_kwargs)) - mocked.assert_called_with(*expected_args, **expected_kwargs) + # Check how the mock was called in detail + (called_args, called_kwargs) = mocked.call_args + self.assertEqual(list(called_args), expected_args) + base_path = expected_kwargs.get('base_path', None) + # NOTE(gtema): if base_path is not in epected_kwargs or empty + # exclude it from the comparison, since some methods might + # still invoke method with None value + if not base_path: + expected_kwargs.pop('base_path', None) + called_kwargs.pop('base_path', None) + self.assertDictEqual(called_kwargs, expected_kwargs) else: self.assertEqual(expected_result, test_method()) mocked.assert_called_with(test_method.__self__) @@ -87,7 +97,9 @@ def verify_create(self, test_method, resource_type, the_kwargs = {"x": 1, "y": 2, "z": 3} method_kwargs = kwargs.pop("method_kwargs", the_kwargs) expected_args = [resource_type] - expected_kwargs = kwargs.pop("expected_kwargs", the_kwargs) + # Default the_kwargs should be copied, since we might need to extend it + expected_kwargs = kwargs.pop("expected_kwargs", the_kwargs.copy()) + expected_kwargs["base_path"] = kwargs.pop("base_path", None) self._verify2(mock_method, test_method, expected_result=expected_result, @@ -147,6 +159,7 @@ def verify_get_overrided(self, proxy, resource_type, patch_target): proxy._get(resource_type) res.fetch.assert_called_once_with( proxy, requires_id=True, + base_path=None, error_message=mock.ANY) def verify_head(self, test_method, resource_type, @@ -216,7 +229,8 @@ def verify_update(self, test_method, resource_type, value=None, method_kwargs = kwargs.pop("method_kwargs", {}) method_kwargs.update({"x": 1, "y": 2, "z": 3}) expected_args = kwargs.pop("expected_args", ["resource_or_id"]) - expected_kwargs = method_kwargs.copy() + expected_kwargs = kwargs.pop("expected_kwargs", method_kwargs.copy()) + expected_kwargs["base_path"] = kwargs.pop("base_path", None) self._add_path_args_for_verify(path_args, method_args, expected_kwargs, value=value) diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 8dafcc08f..f46e9ea68 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1076,16 +1076,18 @@ class Test(resource.Resource): self.session.get_endpoint_data.return_value = self.endpoint_data def _test_create(self, cls, requires_id=False, prepend_key=False, - microversion=None): + microversion=None, base_path=None): id = "id" if requires_id else None sot = cls(id=id) sot._prepare_request = mock.Mock(return_value=self.request) sot._translate_response = mock.Mock() - result = sot.create(self.session, prepend_key=prepend_key) + result = sot.create(self.session, prepend_key=prepend_key, + base_path=base_path) sot._prepare_request.assert_called_once_with( - requires_id=requires_id, prepend_key=prepend_key) + requires_id=requires_id, prepend_key=prepend_key, + base_path=base_path) if requires_id: self.session.put.assert_called_once_with( self.request.url, @@ -1130,10 +1132,21 @@ class Test(resource.Resource): self._test_create(Test, requires_id=False, prepend_key=True) + def test_post_create_base_path(self): + class Test(resource.Resource): + service = self.service_name + base_path = self.base_path + allow_create = True + create_method = 'POST' + + self._test_create(Test, requires_id=False, prepend_key=True, + base_path='dummy') + def test_fetch(self): result = self.sot.fetch(self.session) - self.sot._prepare_request.assert_called_once_with(requires_id=True) + self.sot._prepare_request.assert_called_once_with( + requires_id=True, base_path=None) self.session.get.assert_called_once_with( self.request.url, microversion=None) @@ -1154,7 +1167,8 @@ class Test(resource.Resource): result = sot.fetch(self.session) - sot._prepare_request.assert_called_once_with(requires_id=True) + sot._prepare_request.assert_called_once_with( + requires_id=True, base_path=None) self.session.get.assert_called_once_with( self.request.url, microversion='1.42') @@ -1165,7 +1179,20 @@ class Test(resource.Resource): def test_fetch_not_requires_id(self): result = self.sot.fetch(self.session, False) - self.sot._prepare_request.assert_called_once_with(requires_id=False) + self.sot._prepare_request.assert_called_once_with( + requires_id=False, base_path=None) + self.session.get.assert_called_once_with( + self.request.url, microversion=None) + + self.sot._translate_response.assert_called_once_with(self.response) + self.assertEqual(result, self.sot) + + def test_fetch_base_path(self): + result = self.sot.fetch(self.session, False, base_path='dummy') + + self.sot._prepare_request.assert_called_once_with( + requires_id=False, + base_path='dummy') self.session.get.assert_called_once_with( self.request.url, microversion=None) @@ -1175,7 +1202,21 @@ def test_fetch_not_requires_id(self): def test_head(self): result = self.sot.head(self.session) - self.sot._prepare_request.assert_called_once_with() + self.sot._prepare_request.assert_called_once_with(base_path=None) + self.session.head.assert_called_once_with( + self.request.url, + headers={"Accept": ""}, + microversion=None) + + self.assertIsNone(self.sot.microversion) + self.sot._translate_response.assert_called_once_with( + self.response, has_body=False) + self.assertEqual(result, self.sot) + + def test_head_base_path(self): + result = self.sot.head(self.session, base_path='dummy') + + self.sot._prepare_request.assert_called_once_with(base_path='dummy') self.session.head.assert_called_once_with( self.request.url, headers={"Accept": ""}, @@ -1199,7 +1240,7 @@ class Test(resource.Resource): result = sot.head(self.session) - sot._prepare_request.assert_called_once_with() + sot._prepare_request.assert_called_once_with(base_path=None) self.session.head.assert_called_once_with( self.request.url, headers={"Accept": ""}, @@ -1212,7 +1253,7 @@ class Test(resource.Resource): def _test_commit(self, commit_method='PUT', prepend_key=True, has_body=True, microversion=None, - commit_args=None, expected_args=None): + commit_args=None, expected_args=None, base_path=None): self.sot.commit_method = commit_method # Need to make sot look dirty so we can attempt an update @@ -1220,10 +1261,11 @@ def _test_commit(self, commit_method='PUT', prepend_key=True, self.sot._body.dirty = mock.Mock(return_value={"x": "y"}) self.sot.commit(self.session, prepend_key=prepend_key, - has_body=has_body, **(commit_args or {})) + has_body=has_body, base_path=base_path, + **(commit_args or {})) self.sot._prepare_request.assert_called_once_with( - prepend_key=prepend_key) + prepend_key=prepend_key, base_path=base_path) if commit_method == 'PATCH': self.session.patch.assert_called_once_with( @@ -1252,6 +1294,10 @@ def test_commit_patch(self): self._test_commit( commit_method='PATCH', prepend_key=False, has_body=False) + def test_commit_base_path(self): + self._test_commit(commit_method='PUT', prepend_key=True, has_body=True, + base_path='dummy') + def test_commit_patch_retry_on_conflict(self): self._test_commit( commit_method='PATCH', diff --git a/openstack/workflow/v2/execution.py b/openstack/workflow/v2/execution.py index 6a317841c..5e8787ba9 100644 --- a/openstack/workflow/v2/execution.py +++ b/openstack/workflow/v2/execution.py @@ -50,9 +50,10 @@ class Execution(resource.Resource): #: The time at which the Execution was updated updated_at = resource.Body("updated_at") - def create(self, session, prepend_key=True): + def create(self, session, prepend_key=True, base_path=None): request = self._prepare_request(requires_id=False, - prepend_key=prepend_key) + prepend_key=prepend_key, + base_path=base_path) request_body = request.body["execution"] response = session.post(request.url, diff --git a/openstack/workflow/v2/workflow.py b/openstack/workflow/v2/workflow.py index 6be931340..e196c19b4 100644 --- a/openstack/workflow/v2/workflow.py +++ b/openstack/workflow/v2/workflow.py @@ -46,9 +46,10 @@ class Workflow(resource.Resource): #: The time at which the workflow was created updated_at = resource.Body("updated_at") - def create(self, session, prepend_key=True): + def create(self, session, prepend_key=True, base_path=None): request = self._prepare_request(requires_id=False, - prepend_key=prepend_key) + prepend_key=prepend_key, + base_path=base_path) headers = { "Content-Type": 'text/plain' From aea3ea2ad1fe16831997e6a360ad55b53884bb14 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 4 Dec 2018 15:50:27 +0100 Subject: [PATCH 2324/3836] Rework orchestration to add update preview With a slight rework of basic resource and proxy a change can be done in orchestrate to throw away useless StackPreview class and add possibility to create/update stack with preview (dry-run) using a single regular Stack class. Additionally stack.abandon is added Task: 28128 Change-Id: I9a0a2a389be04a5cbcc3dd085ef830c58af3c1d0 --- openstack/orchestration/v1/_proxy.py | 25 ++- openstack/orchestration/v1/stack.py | 52 +++++- .../tests/unit/orchestration/v1/test_proxy.py | 24 ++- .../tests/unit/orchestration/v1/test_stack.py | 148 ++++++++++++++++-- 4 files changed, 222 insertions(+), 27 deletions(-) diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index 7b0423621..237f78bce 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -28,10 +28,8 @@ class Proxy(proxy.Proxy): def create_stack(self, preview=False, **attrs): """Create a new stack from attributes - :param bool preview: When ``True``, returns - an :class:`~openstack.orchestration.v1.stack.StackPreview` object, - otherwise an :class:`~openstack.orchestration.v1.stack.Stack` - object. + :param bool preview: When ``True``, a preview endpoint will be used to + verify the template *Default: ``False``* :param dict attrs: Keyword arguments which will be used to create a :class:`~openstack.orchestration.v1.stack.Stack`, @@ -40,8 +38,8 @@ def create_stack(self, preview=False, **attrs): :returns: The results of stack creation :rtype: :class:`~openstack.orchestration.v1.stack.Stack` """ - res_type = _stack.StackPreview if preview else _stack.Stack - return self._create(res_type, **attrs) + base_path = None if not preview else '/stacks/preview' + return self._create(_stack.Stack, base_path=base_path, **attrs) def find_stack(self, name_or_id, ignore_missing=True): """Find a single stack @@ -80,7 +78,7 @@ def get_stack(self, stack): """ return self._get(_stack.Stack, stack) - def update_stack(self, stack, **attrs): + def update_stack(self, stack, preview=False, **attrs): """Update a stack :param stack: The value can be the ID of a stack or a @@ -93,7 +91,8 @@ def update_stack(self, stack, **attrs): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._update(_stack.Stack, stack, **attrs) + res = self._get_resource(_stack.Stack, stack, **attrs) + return res.update(self, preview) def delete_stack(self, stack, ignore_missing=True): """Delete a stack @@ -128,6 +127,16 @@ def check_stack(self, stack): stk_obj.check(self) + def abandon_stack(self, stack): + """Abandon a stack's without deleting it's resources + + :param stack: The value can be either the ID of a stack or an instance + of :class:`~openstack.orchestration.v1.stack.Stack`. + :returns: ``None`` + """ + res = self._get_resource(_stack.Stack, stack) + return res.abandon(self) + def get_stack_template(self, stack): """Get template used by a stack diff --git a/openstack/orchestration/v1/stack.py b/openstack/orchestration/v1/stack.py index c2e464293..8f31dbee8 100644 --- a/openstack/orchestration/v1/stack.py +++ b/openstack/orchestration/v1/stack.py @@ -29,6 +29,9 @@ class Stack(resource.Resource): allow_delete = True # Properties + #: A list of resource objects that will be added if a stack update + # is performed. + added = resource.Body('added') #: Placeholder for AWS compatible template listing capabilities #: required by the stack. capabilities = resource.Body('capabilities') @@ -36,6 +39,9 @@ class Stack(resource.Resource): created_at = resource.Body('creation_time') #: A text description of the stack. description = resource.Body('description') + #: A list of resource objects that will be deleted if a stack + #: update is performed. + deleted = resource.Body('deleted', type=list) #: Whether the stack will support a rollback operation on stack #: create/update failures. *Type: bool* is_rollback_disabled = resource.Body('disable_rollback', type=bool) @@ -43,6 +49,7 @@ class Stack(resource.Resource): links = resource.Body('links') #: Name of the stack. name = resource.Body('stack_name') + stack_name = resource.URI('stack_name') #: Placeholder for future extensions where stack related events #: can be published. notification_topics = resource.Body('notification_topics') @@ -54,6 +61,9 @@ class Stack(resource.Resource): parameters = resource.Body('parameters', type=dict) #: The ID of the parent stack if any parent_id = resource.Body('parent') + #: A list of resource objects that will be replaced if a stack update + #: is performed. + replaced = resource.Body('replaced') #: A string representation of the stack status, e.g. ``CREATE_COMPLETE``. status = resource.Body('stack_status') #: A text explaining how the stack transits to its current status. @@ -69,6 +79,12 @@ class Stack(resource.Resource): template_url = resource.Body('template_url') #: Stack operation timeout in minutes. timeout_mins = resource.Body('timeout_mins') + #: A list of resource objects that will remain unchanged if a stack + #: update is performed. + unchanged = resource.Body('unchanged') + #: A list of resource objects that will have their properties updated + #: in place if a stack update is performed. + updated = resource.Body('updated') #: Timestamp of last update on the stack. updated_at = resource.Body('updated_time') #: The ID of the user project created for this stack. @@ -86,6 +102,27 @@ def commit(self, session, base_path=None): return super(Stack, self).commit(session, prepend_key=False, has_body=False, base_path=None) + def update(self, session, preview=False): + # This overrides the default behavior of resource update because + # we need to use other endpoint for update preview. + request = self._prepare_request( + prepend_key=False, + base_path='/stacks/%(stack_name)s/' % {'stack_name': self.name}) + + microversion = self._get_microversion_for(session, 'commit') + + request_url = request.url + if preview: + request_url = utils.urljoin(request_url, 'preview') + + response = session.put( + request_url, json=request.body, headers=request.headers, + microversion=microversion) + + self.microversion = microversion + self._translate_response(response, has_body=True) + return self + def _action(self, session, body): """Perform stack actions""" url = utils.urljoin(self.base_path, self._get_id(self), 'actions') @@ -95,6 +132,12 @@ def _action(self, session, body): def check(self, session): return self._action(session, {'check': ''}) + def abandon(self, session): + url = utils.urljoin(self.base_path, self.name, + self._get_id(self), 'abandon') + resp = session.delete(url) + return resp.json() + def fetch(self, session, requires_id=True, base_path=None, error_message=None): stk = super(Stack, self).fetch( @@ -108,11 +151,4 @@ def fetch(self, session, requires_id=True, return stk -class StackPreview(Stack): - base_path = '/stacks/preview' - - allow_create = True - allow_list = False - allow_fetch = False - allow_commit = False - allow_delete = False +StackPreview = Stack diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index 0fc39487b..c6cc44359 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -36,7 +36,7 @@ def test_create_stack(self): def test_create_stack_preview(self): method_kwargs = {"preview": True, "x": 1, "y": 2, "z": 3} - self.verify_create(self.proxy.create_stack, stack.StackPreview, + self.verify_create(self.proxy.create_stack, stack.Stack, method_kwargs=method_kwargs) def test_find_stack(self): @@ -52,7 +52,27 @@ def test_get_stack(self): 'openstack.orchestration.v1.stack.Stack') def test_update_stack(self): - self.verify_update(self.proxy.update_stack, stack.Stack) + self._verify2('openstack.orchestration.v1.stack.Stack.update', + self.proxy.update_stack, + expected_result='result', + method_args=['stack'], + method_kwargs={'preview': False}, + expected_args=[self.proxy, False]) + + def test_update_stack_preview(self): + self._verify2('openstack.orchestration.v1.stack.Stack.update', + self.proxy.update_stack, + expected_result='result', + method_args=['stack'], + method_kwargs={'preview': True}, + expected_args=[self.proxy, True]) + + def test_abandon_stack(self): + self._verify2('openstack.orchestration.v1.stack.Stack.abandon', + self.proxy.abandon_stack, + expected_result='result', + method_args=['stack'], + expected_args=[self.proxy]) def test_delete_stack(self): self.verify_delete(self.proxy.delete_stack, stack.Stack, False) diff --git a/openstack/tests/unit/orchestration/v1/test_stack.py b/openstack/tests/unit/orchestration/v1/test_stack.py index 967265ce0..698d877a7 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack.py +++ b/openstack/tests/unit/orchestration/v1/test_stack.py @@ -49,6 +49,73 @@ 'href': 'stacks/%s/%s' % (FAKE_NAME, FAKE_ID), 'rel': 'self'}]} } +FAKE_UPDATE_PREVIEW_RESPONSE = { + 'unchanged': [ + { + 'updated_time': 'datetime', + 'resource_name': '', + 'physical_resource_id': '{resource id or ''}', + 'resource_action': 'CREATE', + 'resource_status': 'COMPLETE', + 'resource_status_reason': '', + 'resource_type': 'restype', + 'stack_identity': '{stack_id}', + 'stack_name': '{stack_name}' + } + ], + 'updated': [ + { + 'updated_time': 'datetime', + 'resource_name': '', + 'physical_resource_id': '{resource id or ''}', + 'resource_action': 'CREATE', + 'resource_status': 'COMPLETE', + 'resource_status_reason': '', + 'resource_type': 'restype', + 'stack_identity': '{stack_id}', + 'stack_name': '{stack_name}' + } + ], + 'replaced': [ + { + 'updated_time': 'datetime', + 'resource_name': '', + 'physical_resource_id': '{resource id or ''}', + 'resource_action': 'CREATE', + 'resource_status': 'COMPLETE', + 'resource_status_reason': '', + 'resource_type': 'restype', + 'stack_identity': '{stack_id}', + 'stack_name': '{stack_name}' + } + ], + 'added': [ + { + 'updated_time': 'datetime', + 'resource_name': '', + 'physical_resource_id': '{resource id or ''}', + 'resource_action': 'CREATE', + 'resource_status': 'COMPLETE', + 'resource_status_reason': '', + 'resource_type': 'restype', + 'stack_identity': '{stack_id}', + 'stack_name': '{stack_name}' + } + ], + 'deleted': [ + { + 'updated_time': 'datetime', + 'resource_name': '', + 'physical_resource_id': '{resource id or ''}', + 'resource_action': 'CREATE', + 'resource_status': 'COMPLETE', + 'resource_status_reason': '', + 'resource_type': 'restype', + 'stack_identity': '{stack_id}', + 'stack_name': '{stack_name}' + } + ] +} class TestStack(base.TestCase): @@ -138,15 +205,78 @@ def test_fetch(self, mock_fetch): self.assertEqual('No stack found for %s' % FAKE_ID, six.text_type(ex)) + def test_abandon(self): + sess = mock.Mock() + sess.default_microversion = None -class TestStackPreview(base.TestCase): + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.headers = {} + mock_response.json.return_value = {} + sess.delete = mock.Mock(return_value=mock_response) + sot = stack.Stack(**FAKE) - def test_basic(self): - sot = stack.StackPreview() + sot.abandon(sess) - self.assertEqual('/stacks/preview', sot.base_path) - self.assertTrue(sot.allow_create) - self.assertFalse(sot.allow_list) - self.assertFalse(sot.allow_fetch) - self.assertFalse(sot.allow_commit) - self.assertFalse(sot.allow_delete) + sess.delete.assert_called_with( + 'stacks/%s/%s/abandon' % (FAKE_NAME, FAKE_ID), + + ) + + def test_update(self): + sess = mock.Mock() + sess.default_microversion = None + + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.headers = {} + mock_response.json.return_value = {} + sess.put = mock.Mock(return_value=mock_response) + sot = stack.Stack(**FAKE) + body = sot._body.dirty.copy() + + sot.update(sess) + + sess.put.assert_called_with( + 'stacks/%s/%s' % (FAKE_NAME, FAKE_ID), + headers={}, + microversion=None, + json=body + ) + + def test_update_preview(self): + sess = mock.Mock() + sess.default_microversion = None + + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.headers = {} + mock_response.json.return_value = FAKE_UPDATE_PREVIEW_RESPONSE.copy() + sess.put = mock.Mock(return_value=mock_response) + sot = stack.Stack(**FAKE) + body = sot._body.dirty.copy() + + ret = sot.update(sess, preview=True) + + sess.put.assert_called_with( + 'stacks/%s/%s/preview' % (FAKE_NAME, FAKE_ID), + headers={}, + microversion=None, + json=body + ) + + self.assertEqual( + FAKE_UPDATE_PREVIEW_RESPONSE['added'], + ret.added) + self.assertEqual( + FAKE_UPDATE_PREVIEW_RESPONSE['deleted'], + ret.deleted) + self.assertEqual( + FAKE_UPDATE_PREVIEW_RESPONSE['replaced'], + ret.replaced) + self.assertEqual( + FAKE_UPDATE_PREVIEW_RESPONSE['unchanged'], + ret.unchanged) + self.assertEqual( + FAKE_UPDATE_PREVIEW_RESPONSE['updated'], + ret.updated) From b25e4b8be57e582cb133be310f2789271bc1e8d4 Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Tue, 18 Dec 2018 02:30:28 +0000 Subject: [PATCH 2325/3836] Unpin dogpile.cache Unpin dogpile.cache, we can use 0.7.0 and later now. Story: 2004605 Task: 28502 Needed-By: https://review.openstack.org/#/c/624993 Change-Id: I1cbf4897f8f55376a85a8f760b683ebb778c5bb9 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 87ed65edb..78f758519 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,5 +18,5 @@ futures>=3.0.0;python_version=='2.7' or python_version=='2.6' # BSD iso8601>=0.1.11 # MIT netifaces>=0.10.4 # MIT -dogpile.cache>=0.6.2,<0.7.0 # BSD +dogpile.cache>=0.6.2 # BSD cryptography>=2.1 # BSD/Apache-2.0 From 1f3a65032f5f5c21b796a0c5c9cd9024bbe6dfe3 Mon Sep 17 00:00:00 2001 From: wangqiangbj Date: Tue, 25 Dec 2018 15:47:14 +0800 Subject: [PATCH 2326/3836] fix typos Change-Id: I6b353b5144ffdd57bbcb1e289c941c1d589f91b0 --- openstack/tests/functional/network/v2/test_agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/tests/functional/network/v2/test_agent.py b/openstack/tests/functional/network/v2/test_agent.py index da1d3ce91..7e5c35bfa 100644 --- a/openstack/tests/functional/network/v2/test_agent.py +++ b/openstack/tests/functional/network/v2/test_agent.py @@ -19,7 +19,7 @@ class TestAgent(base.BaseFunctionalTest): AGENT = None - DESC = 'test descrition' + DESC = 'test description' def validate_uuid(self, s): try: From 0bebd378f73914f400748ec17272a78fbce4cfe7 Mon Sep 17 00:00:00 2001 From: lijunjie Date: Thu, 27 Dec 2018 15:44:27 +0800 Subject: [PATCH 2327/3836] Fix the misspelling of "configuration" Change-Id: Id93739b568397cedac5a468b77915e71b0743056 --- openstack/orchestration/v1/software_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/orchestration/v1/software_config.py b/openstack/orchestration/v1/software_config.py index af59e8989..7d1df265f 100644 --- a/openstack/orchestration/v1/software_config.py +++ b/openstack/orchestration/v1/software_config.py @@ -38,7 +38,7 @@ class SoftwareConfig(resource.Resource): inputs = resource.Body('inputs') #: Name of the software config. name = resource.Body('name') - #: A string that contains options that are specific to the configuraiton + #: A string that contains options that are specific to the configuration #: management tool that this resource uses. options = resource.Body('options') #: A list of schemas each representing an output this software config From aa82e3bba9892faf2e750f861aa6e157b2503a3e Mon Sep 17 00:00:00 2001 From: Johannes Kulik Date: Mon, 7 Jan 2019 15:08:06 +0100 Subject: [PATCH 2328/3836] Fix pagination key detection The assignment of the first detection test is wrongly written as a boolean comparison. Change-Id: Ib3dc07c47b45fba54f0be727e9991f4669007184 --- openstack/resource.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/resource.py b/openstack/resource.py index b1c5cc2af..10e3dc4a2 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1342,7 +1342,7 @@ def _get_next_link(cls, uri, response, data, marker, limit, total_yielded): if not pagination_key and 'links' in data: # api-wg guidelines are for a links dict in the main body - pagination_key == 'links' + pagination_key = 'links' if not pagination_key and cls.resources_key: # Nova has a {key}_links dict in the main body pagination_key = '{key}_links'.format(key=cls.resources_key) From e18899d19fa3b138bb287d8a5aa0e3f3ce37c95d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 8 Jan 2019 12:15:19 +0000 Subject: [PATCH 2329/3836] Skip block storage v2 functional tests for a minute There is something very sad going on with v2, discovery and block-storage. It needs to be fixed, but at the moment it's proving hard to track down and the failures are blocking other fixes. Skip the four tests for now while we work on it. Change-Id: I0a91089309547d56d0e51392ec94829422be4064 --- openstack/tests/functional/block_storage/v2/base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openstack/tests/functional/block_storage/v2/base.py b/openstack/tests/functional/block_storage/v2/base.py index e5f2694c5..4c0f77917 100644 --- a/openstack/tests/functional/block_storage/v2/base.py +++ b/openstack/tests/functional/block_storage/v2/base.py @@ -23,3 +23,7 @@ def setUpClass(cls): cls._wait_for_timeout = int(os.getenv( 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_BLOCK_STORAGE', cls._wait_for_timeout)) + + def setUp(self): + super(BaseBlockStorageTest, self).setUp() + self.skipTest('block-storage v2 functional tests broken') From 76ccd2c83628782e2c66a2994fc3a3bbc124c321 Mon Sep 17 00:00:00 2001 From: "b.haleblian" Date: Tue, 8 Jan 2019 00:56:49 +0100 Subject: [PATCH 2330/3836] implement identity v3 Proxy "unassign_project_role_from_user" identity/v3/_proxy.py lacks the reverse method of "assign_project_role_to_user" although this method is implemented in Project class. With this patchset call to conn.identity.unassign_project_role_from_user(project,user,role) works Fix no known issue; just adds functionnality Change-Id: I066fcb378cb3b3449bbe12b6186caaf63432c2af --- openstack/identity/v3/_proxy.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index ca7eefe0f..142e3c462 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -1129,3 +1129,18 @@ def assign_project_role_to_user(self, project, user, role): """ project = self._get_resource(_project.Project, project) project.assign_role_to_user(self, user, role) + + def unassign_project_role_from_user(self, project, user, role): + """Unassign role from user on a project + + :param project: Either the ID of a project or a + :class:`~openstack.identity.v3.project.Project` + instance. + :param user: Either the ID of a user or a + :class:`~openstack.identity.v3.user.User` instance. + :param role: Either the ID of a role or a + :class:`~openstack.identity.v3.role.Role` instance. + :return: ``None`` + """ + project = self._get_resource(_project.Project, project) + project.unassign_role_from_user(self, user, role) From 7027c17b32733c93c6b2e27b7de1944a44b2cbd7 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 7 Jan 2019 13:45:08 +0000 Subject: [PATCH 2331/3836] Skip v2 block-storage tests when service is not found We have skips in for the cloud functional tests. Add the same skips for the resource layer tests. Since the resource layer is tied to version 2, put in an explicit config override in the setUp to set block_storage_api_version. Shift block_storage v2 tests to use self.user_cloud and not self.conn. In doing so, test_type turns out to need admin privs, so use self.op_cloud for it. Change-Id: I8b231e7b5e0ebad6751702d9f0cb85e3d2192ac5 --- .../tests/functional/block_storage/v2/base.py | 6 +++++- .../functional/block_storage/v2/test_backup.py | 16 ++++++++-------- .../block_storage/v2/test_snapshot.py | 18 +++++++++--------- .../functional/block_storage/v2/test_type.py | 7 ++++--- .../functional/block_storage/v2/test_volume.py | 11 +++++++---- 5 files changed, 33 insertions(+), 25 deletions(-) diff --git a/openstack/tests/functional/block_storage/v2/base.py b/openstack/tests/functional/block_storage/v2/base.py index 4c0f77917..5475d1e14 100644 --- a/openstack/tests/functional/block_storage/v2/base.py +++ b/openstack/tests/functional/block_storage/v2/base.py @@ -26,4 +26,8 @@ def setUpClass(cls): def setUp(self): super(BaseBlockStorageTest, self).setUp() - self.skipTest('block-storage v2 functional tests broken') + self._set_user_cloud(block_storage_api_version='2') + self._set_operator_cloud(block_storage_api_version='2') + + if not self.user_cloud.has_service('block-storage'): + self.skipTest('block-storage service not supported by cloud') diff --git a/openstack/tests/functional/block_storage/v2/test_backup.py b/openstack/tests/functional/block_storage/v2/test_backup.py index 1e17688df..7f2670f8d 100644 --- a/openstack/tests/functional/block_storage/v2/test_backup.py +++ b/openstack/tests/functional/block_storage/v2/test_backup.py @@ -20,7 +20,7 @@ class TestBackup(base.BaseBlockStorageTest): def setUp(self): super(TestBackup, self).setUp() - if not self.conn.has_service('object-store'): + if not self.user_cloud.has_service('object-store'): self.skipTest('Object service is requred, but not available') self.VOLUME_NAME = self.getUniqueString() @@ -28,10 +28,10 @@ def setUp(self): self.BACKUP_NAME = self.getUniqueString() self.BACKUP_ID = None - volume = self.conn.block_storage.create_volume( + volume = self.user_cloud.block_storage.create_volume( name=self.VOLUME_NAME, size=1) - self.conn.block_storage.wait_for_status( + self.user_cloud.block_storage.wait_for_status( volume, status='available', failures=['error'], @@ -40,10 +40,10 @@ def setUp(self): assert isinstance(volume, _volume.Volume) self.VOLUME_ID = volume.id - backup = self.conn.block_storage.create_backup( + backup = self.user_cloud.block_storage.create_backup( name=self.BACKUP_NAME, volume_id=volume.id) - self.conn.block_storage.wait_for_status( + self.user_cloud.block_storage.wait_for_status( backup, status='available', failures=['error'], @@ -54,15 +54,15 @@ def setUp(self): self.BACKUP_ID = backup.id def tearDown(self): - sot = self.conn.block_storage.delete_backup( + sot = self.user_cloud.block_storage.delete_backup( self.BACKUP_ID, ignore_missing=False) - sot = self.conn.block_storage.delete_volume( + sot = self.user_cloud.block_storage.delete_volume( self.VOLUME_ID, ignore_missing=False) self.assertIsNone(sot) super(TestBackup, self).tearDown() def test_get(self): - sot = self.conn.block_storage.get_backup(self.BACKUP_ID) + sot = self.user_cloud.block_storage.get_backup(self.BACKUP_ID) self.assertEqual(self.BACKUP_NAME, sot.name) diff --git a/openstack/tests/functional/block_storage/v2/test_snapshot.py b/openstack/tests/functional/block_storage/v2/test_snapshot.py index 8cf693f7f..18a7a77ab 100644 --- a/openstack/tests/functional/block_storage/v2/test_snapshot.py +++ b/openstack/tests/functional/block_storage/v2/test_snapshot.py @@ -26,10 +26,10 @@ def setUp(self): self.VOLUME_NAME = self.getUniqueString() self.VOLUME_ID = None - volume = self.conn.block_storage.create_volume( + volume = self.user_cloud.block_storage.create_volume( name=self.VOLUME_NAME, size=1) - self.conn.block_storage.wait_for_status( + self.user_cloud.block_storage.wait_for_status( volume, status='available', failures=['error'], @@ -38,10 +38,10 @@ def setUp(self): assert isinstance(volume, _volume.Volume) self.assertEqual(self.VOLUME_NAME, volume.name) self.VOLUME_ID = volume.id - snapshot = self.conn.block_storage.create_snapshot( + snapshot = self.user_cloud.block_storage.create_snapshot( name=self.SNAPSHOT_NAME, volume_id=self.VOLUME_ID) - self.conn.block_storage.wait_for_status( + self.user_cloud.block_storage.wait_for_status( snapshot, status='available', failures=['error'], @@ -52,17 +52,17 @@ def setUp(self): self.SNAPSHOT_ID = snapshot.id def tearDown(self): - snapshot = self.conn.block_storage.get_snapshot(self.SNAPSHOT_ID) - sot = self.conn.block_storage.delete_snapshot( + snapshot = self.user_cloud.block_storage.get_snapshot(self.SNAPSHOT_ID) + sot = self.user_cloud.block_storage.delete_snapshot( snapshot, ignore_missing=False) - self.conn.block_storage.wait_for_delete( + self.user_cloud.block_storage.wait_for_delete( snapshot, interval=2, wait=self._wait_for_timeout) self.assertIsNone(sot) - sot = self.conn.block_storage.delete_volume( + sot = self.user_cloud.block_storage.delete_volume( self.VOLUME_ID, ignore_missing=False) self.assertIsNone(sot) super(TestSnapshot, self).tearDown() def test_get(self): - sot = self.conn.block_storage.get_snapshot(self.SNAPSHOT_ID) + sot = self.user_cloud.block_storage.get_snapshot(self.SNAPSHOT_ID) self.assertEqual(self.SNAPSHOT_NAME, sot.name) diff --git a/openstack/tests/functional/block_storage/v2/test_type.py b/openstack/tests/functional/block_storage/v2/test_type.py index b983a0507..ce2052f43 100644 --- a/openstack/tests/functional/block_storage/v2/test_type.py +++ b/openstack/tests/functional/block_storage/v2/test_type.py @@ -23,17 +23,18 @@ def setUp(self): self.TYPE_NAME = self.getUniqueString() self.TYPE_ID = None - sot = self.conn.block_storage.create_type(name=self.TYPE_NAME) + sot = self.operator_cloud.block_storage.create_type( + name=self.TYPE_NAME) assert isinstance(sot, _type.Type) self.assertEqual(self.TYPE_NAME, sot.name) self.TYPE_ID = sot.id def tearDown(self): - sot = self.conn.block_storage.delete_type( + sot = self.operator_cloud.block_storage.delete_type( self.TYPE_ID, ignore_missing=False) self.assertIsNone(sot) super(TestType, self).tearDown() def test_get(self): - sot = self.conn.block_storage.get_type(self.TYPE_ID) + sot = self.operator_cloud.block_storage.get_type(self.TYPE_ID) self.assertEqual(self.TYPE_NAME, sot.name) diff --git a/openstack/tests/functional/block_storage/v2/test_volume.py b/openstack/tests/functional/block_storage/v2/test_volume.py index 405accd62..67ac7627b 100644 --- a/openstack/tests/functional/block_storage/v2/test_volume.py +++ b/openstack/tests/functional/block_storage/v2/test_volume.py @@ -19,13 +19,16 @@ class TestVolume(base.BaseBlockStorageTest): def setUp(self): super(TestVolume, self).setUp() + if not self.user_cloud.has_service('block-storage'): + self.skipTest('block-storage service not supported by cloud') + self.VOLUME_NAME = self.getUniqueString() self.VOLUME_ID = None - volume = self.conn.block_storage.create_volume( + volume = self.user_cloud.block_storage.create_volume( name=self.VOLUME_NAME, size=1) - self.conn.block_storage.wait_for_status( + self.user_cloud.block_storage.wait_for_status( volume, status='available', failures=['error'], @@ -36,12 +39,12 @@ def setUp(self): self.VOLUME_ID = volume.id def tearDown(self): - sot = self.conn.block_storage.delete_volume( + sot = self.user_cloud.block_storage.delete_volume( self.VOLUME_ID, ignore_missing=False) self.assertIsNone(sot) super(TestVolume, self).tearDown() def test_get(self): - sot = self.conn.block_storage.get_volume(self.VOLUME_ID) + sot = self.user_cloud.block_storage.get_volume(self.VOLUME_ID) self.assertEqual(self.VOLUME_NAME, sot.name) From 0465884d4322fa706a3439f1ed5cba53214ca980 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 9 Jan 2019 14:25:43 +0000 Subject: [PATCH 2332/3836] Stop running grenade-py3 It is not testing openstacksdk patches, so running it is a waste of resources. Change-Id: I8be6e0c10516224370bec452a87e8a27b5d8fa96 --- .zuul.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 8d9871b45..70b1dd055 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -413,7 +413,6 @@ - openstacksdk-functional-devstack-python2 - osc-functional-devstack-tips: voting: false - - grenade-py3 - nodepool-functional-py35-src - bifrost-integration-tinyipa-ubuntu-xenial gate: @@ -422,6 +421,5 @@ - openstacksdk-functional-devstack-python2 - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-senlin - - grenade-py3 - nodepool-functional-py35-src - bifrost-integration-tinyipa-ubuntu-xenial From 35d57b9c8652e0e79a7729392121d095f9e6ea47 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 10 Jan 2019 14:43:04 +0000 Subject: [PATCH 2333/3836] Restrict inventory test to devstack-admin There are multiple different cloud entries in clouds.yaml now. That means that doing an inventory across all configured clouds gets weird, especially when we're tossing system scoped accounts in there. Change-Id: I12e834996052dae0cd07e5e5092906dd1e8a4f81 --- openstack/tests/functional/cloud/test_inventory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/tests/functional/cloud/test_inventory.py b/openstack/tests/functional/cloud/test_inventory.py index ff3ff3a50..bfe8b11f2 100644 --- a/openstack/tests/functional/cloud/test_inventory.py +++ b/openstack/tests/functional/cloud/test_inventory.py @@ -30,7 +30,7 @@ def setUp(self): super(TestInventory, self).setUp() # This needs to use an admin account, otherwise a public IP # is not allocated from devstack. - self.inventory = inventory.OpenStackInventory() + self.inventory = inventory.OpenStackInventory(cloud='devstack-admin') self.server_name = self.getUniqueString('inventory') self.flavor = pick_flavor( self.user_cloud.list_flavors(get_extra=False)) From 642c0fc937a1e84cbfa4037ef73f8f8daf6e48c0 Mon Sep 17 00:00:00 2001 From: Lajos Katona Date: Thu, 10 Jan 2019 15:53:29 +0100 Subject: [PATCH 2334/3836] Add port property: port-resource-request With the introduction of port-resource-request some ports have a new attribute: resource_request, make openstacksdk know about it. Details: https://developer.openstack.org/api-ref/network/v2/ \ index.html?expanded=show-port-details-detail#ports Change-Id: I47494ddc7df9308f8eead8bd36a194e59b781199 Partial-Bug: #1578989 See-Also: https://review.openstack.org/502306 (nova spec) See-Also: https://review.openstack.org/508149 (neutron spec) --- openstack/network/v2/port.py | 4 ++++ openstack/tests/unit/network/v2/test_port.py | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index 4eda995b3..94dd80911 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -107,6 +107,10 @@ class Port(resource.Resource, resource.TagMixin): type=bool) #: The ID of the QoS policy attached to the port. qos_policy_id = resource.Body('qos_policy_id') + #: Read-only. The port-resource-request exposes Placement resources + # (i.e.: minimum-bandwidth) and traits (i.e.: vnic-type, physnet) + # requested by a port to Nova and Placement. + resource_request = resource.Body('resource_request', type=dict) #: Revision number of the port. *Type: int* revision_number = resource.Body('revision_number', type=int) #: The IDs of any attached security groups. diff --git a/openstack/tests/unit/network/v2/test_port.py b/openstack/tests/unit/network/v2/test_port.py index c9c8686a4..7c78a2b7e 100644 --- a/openstack/tests/unit/network/v2/test_port.py +++ b/openstack/tests/unit/network/v2/test_port.py @@ -40,6 +40,12 @@ 'port_security_enabled': True, 'qos_policy_id': '21', 'propagate_uplink_status': False, + 'resource_request': { + 'required': [ + 'CUSTOM_PHYSNET_PUBLIC', 'CUSTOM_VNIC_TYPE_NORMAL'], + 'resources': { + 'NET_BW_EGR_KILOBIT_PER_SEC': 1, + 'NET_BW_IGR_KILOBIT_PER_SEC': 2}}, 'revision_number': 22, 'security_groups': ['23'], 'status': '25', @@ -124,6 +130,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['qos_policy_id'], sot.qos_policy_id) self.assertEqual(EXAMPLE['propagate_uplink_status'], sot.propagate_uplink_status) + self.assertEqual(EXAMPLE['resource_request'], sot.resource_request) self.assertEqual(EXAMPLE['revision_number'], sot.revision_number) self.assertEqual(EXAMPLE['security_groups'], sot.security_group_ids) self.assertEqual(EXAMPLE['status'], sot.status) From 76326b19cc9a5550e209eb1ae1f489656e714193 Mon Sep 17 00:00:00 2001 From: Kailun Qin Date: Sat, 15 Dec 2018 23:21:54 +0800 Subject: [PATCH 2335/3836] Add network segment range resource Add network segment range resource in support of network segment range management. This patch set includes the following: - Network segment range resource - Proxy CRUD interfaces for the network segment range resource - Documentation updates - Unit tests - Function tests (currently skipped) Co-authored-by: Allain Legacy Partially-implements: blueprint network-segment-range-management Change-Id: I62ddf40387fd589e788ac45235fbe6bdc6612ad8 --- doc/source/user/resources/network/index.rst | 1 + .../network/v2/network_segment_range.rst | 13 ++ openstack/network/v2/_proxy.py | 118 ++++++++++++++++++ openstack/network/v2/network_segment_range.py | 71 +++++++++++ .../network/v2/test_network_segment_range.py | 99 +++++++++++++++ .../network/v2/test_network_segment_range.py | 64 ++++++++++ openstack/tests/unit/network/v2/test_proxy.py | 30 +++++ 7 files changed, 396 insertions(+) create mode 100644 doc/source/user/resources/network/v2/network_segment_range.rst create mode 100644 openstack/network/v2/network_segment_range.py create mode 100644 openstack/tests/functional/network/v2/test_network_segment_range.py create mode 100644 openstack/tests/unit/network/v2/test_network_segment_range.py diff --git a/doc/source/user/resources/network/index.rst b/doc/source/user/resources/network/index.rst index 21da8cf9f..9011b0c90 100644 --- a/doc/source/user/resources/network/index.rst +++ b/doc/source/user/resources/network/index.rst @@ -18,6 +18,7 @@ Network Resources v2/metering_label_rule v2/network v2/network_ip_availability + v2/network_segment_range v2/pool v2/pool_member v2/port diff --git a/doc/source/user/resources/network/v2/network_segment_range.rst b/doc/source/user/resources/network/v2/network_segment_range.rst new file mode 100644 index 000000000..1f2c55ddf --- /dev/null +++ b/doc/source/user/resources/network/v2/network_segment_range.rst @@ -0,0 +1,13 @@ +openstack.network.v2.network_segment_range +========================================== + +.. automodule:: openstack.network.v2.network_segment_range + +The NetworkSegmentRange Class +----------------------------- + +The ``NetworkSegmentRange`` class inherits from :class:`~openstack.resource +.Resource`. + +.. autoclass:: openstack.network.v2.network_segment_range.NetworkSegmentRange + :members: diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index c7dd6e4a3..bc0966be8 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -29,6 +29,8 @@ from openstack.network.v2 import metering_label_rule as _metering_label_rule from openstack.network.v2 import network as _network from openstack.network.v2 import network_ip_availability +from openstack.network.v2 import network_segment_range as \ + _network_segment_range from openstack.network.v2 import pool as _pool from openstack.network.v2 import pool_member as _pool_member from openstack.network.v2 import port as _port @@ -1345,6 +1347,122 @@ def network_ip_availabilities(self, **query): return self._list(network_ip_availability.NetworkIPAvailability, paginated=False, **query) + def create_network_segment_range(self, **attrs): + """Create a new network segment range from attributes + + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.network.v2. + network_segment_range.NetworkSegmentRange`, + comprised of the properties on the + NetworkSegmentRange class. + + :returns: The results of network segment range creation + :rtype: :class:`~openstack.network.v2.network_segment_range + .NetworkSegmentRange` + """ + return self._create(_network_segment_range.NetworkSegmentRange, + **attrs) + + def delete_network_segment_range(self, network_segment_range, + ignore_missing=True): + """Delete a network segment range + + :param network_segment_range: The value can be either the ID of a + network segment range or a + :class:`~openstack.network.v2.network_segment_range. + NetworkSegmentRange` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the network segment range does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent network segment range. + + :returns: ``None`` + """ + self._delete(_network_segment_range.NetworkSegmentRange, + network_segment_range, ignore_missing=ignore_missing) + + def find_network_segment_range(self, name_or_id, ignore_missing=True, + **args): + """Find a single network segment range + + :param name_or_id: The name or ID of a network segment range. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. + :returns: One :class:`~openstack.network.v2.network_segment_range + .NetworkSegmentRange` or None + """ + return self._find(_network_segment_range.NetworkSegmentRange, + name_or_id, ignore_missing=ignore_missing, **args) + + def get_network_segment_range(self, network_segment_range): + """Get a single network segment range + + :param network_segment_range: The value can be the ID of a network + segment range or a :class:`~openstack.network.v2. + network_segment_range.NetworkSegmentRange` instance. + + :returns: One :class:`~openstack.network.v2._network_segment_range. + NetworkSegmentRange` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_network_segment_range.NetworkSegmentRange, + network_segment_range) + + def network_segment_ranges(self, **query): + """Return a generator of network segment ranges + + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. Available parameters include: + + * ``name``: Name of the segments + * ``default``: The network segment range is loaded from the host + configuration file. + * ``shared``: The network segment range is shared with other + projects + * ``project_id``: ID of the project that owns the network + segment range + * ``network_type``: Network type for the network segment ranges + * ``physical_network``: Physical network name for the network + segment ranges + * ``minimum``: Minimum segmentation ID for the network segment + ranges + * ``maximum``: Maximum Segmentation ID for the network segment + ranges + * ``used``: Mapping of which segmentation ID in the range is + used by which tenant + * ``available``: List of available segmentation IDs in this + network segment range + + :returns: A generator of network segment range objects + :rtype: :class:`~openstack.network.v2._network_segment_range. + NetworkSegmentRange` + """ + return self._list(_network_segment_range.NetworkSegmentRange, + paginated=False, **query) + + def update_network_segment_range(self, network_segment_range, **attrs): + """Update a network segment range + + :param network_segment_range: Either the id of a network segment range + or a :class:`~openstack.network.v2._network_segment_range. + NetworkSegmentRange` instance. + :attrs kwargs: The attributes to update on the network segment range + represented by ``value``. + + :returns: The updated network segment range + :rtype: :class:`~openstack.network.v2._network_segment_range. + NetworkSegmentRange` + """ + return self._update(_network_segment_range.NetworkSegmentRange, + network_segment_range, **attrs) + def create_pool(self, **attrs): """Create a new pool from attributes diff --git a/openstack/network/v2/network_segment_range.py b/openstack/network/v2/network_segment_range.py new file mode 100644 index 000000000..2512f242d --- /dev/null +++ b/openstack/network/v2/network_segment_range.py @@ -0,0 +1,71 @@ +# Copyright (c) 2018, Intel Corporation. +# 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 openstack import resource + + +class NetworkSegmentRange(resource.Resource): + resource_key = 'network_segment_range' + resources_key = 'network_segment_ranges' + base_path = '/network_segment_ranges' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + _query_mapping = resource.QueryParameters( + 'name', 'default', 'shared', 'project_id', + 'network_type', 'physical_network', 'minimum', 'maximum', + 'used', 'available' + ) + + # Properties + #: The network segment range name. + name = resource.Body('name') + #: The network segment range is loaded from the host configuration file. + #: *Type: bool* + default = resource.Body('default', type=bool) + #: The network segment range is shared with other projects. + #: *Type: bool* + shared = resource.Body('shared', type=bool) + #: The ID of the project associated with this network segment range. + project_id = resource.Body('project_id') + #: The type of network associated with this network segment range, such as + #: ``geneve``, ``gre``, ``vlan`` or ``vxlan``. + network_type = resource.Body('network_type') + #: The name of the physical network associated with this network segment + #: range. + physical_network = resource.Body('physical_network') + #: The minimum segmentation ID for this network segment range. The + #: network type defines the segmentation model, VLAN ID for ``vlan`` + #: network type and tunnel ID for ``geneve``, ``gre`` and ``vxlan`` + #: network types. + #: *Type: int* + minimum = resource.Body('minimum', type=int) + #: The maximum segmentation ID for this network segment range. The + #: network type defines the segmentation model, VLAN ID for ``vlan`` + #: network type and tunnel ID for ``geneve``, ``gre`` and ``vxlan`` + #: network types. + #: *Type: int* + maximum = resource.Body('maximum', type=int) + #: Mapping of which segmentation ID in the range is used by which tenant. + #: *Type: dict* + used = resource.Body('used', type=dict) + #: List of available segmentation IDs in this network segment range. + #: *Type: list* + available = resource.Body('available', type=list) diff --git a/openstack/tests/functional/network/v2/test_network_segment_range.py b/openstack/tests/functional/network/v2/test_network_segment_range.py new file mode 100644 index 000000000..d626d72f0 --- /dev/null +++ b/openstack/tests/functional/network/v2/test_network_segment_range.py @@ -0,0 +1,99 @@ +# Copyright (c) 2018, Intel Corporation. +# 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 openstack.network.v2 import network_segment_range +from openstack.tests.functional import base + + +class TestNetworkSegmentRange(base.BaseFunctionalTest): + + NETWORK_SEGMENT_RANGE_ID = None + NAME = 'test_name' + DEFAULT = False + SHARED = False + PROJECT_ID = '2018' + NETWORK_TYPE = 'vlan' + PHYSICAL_NETWORK = 'phys_net' + MINIMUM = 100 + MAXIMUM = 200 + + def setUp(self): + super(TestNetworkSegmentRange, self).setUp() + + # NOTE(kailun): The network segment range extension is not yet enabled + # by default. + # Skip the tests if not enabled. + if not self.conn.network.find_extension('network-segment-range'): + self.skipTest('Network Segment Range extension disabled') + + test_seg_range = self.conn.network.create_network_segment_range( + name=self.NAME, + default=self.DEFAULT, + shared=self.SHARED, + project_id=self.PROJECT_ID, + network_type=self.NETWORK_TYPE, + physical_network=self.PHYSICAL_NETWORK, + minimum=self.MINIMUM, + maximum=self.MAXIMUM, + ) + self.assertIsInstance(test_seg_range, + network_segment_range.NetworkSegmentRange) + self.NETWORK_SEGMENT_RANGE_ID = test_seg_range.id + self.assertEqual(self.NAME, test_seg_range.name) + self.assertEqual(self.DEFAULT, test_seg_range.default) + self.assertEqual(self.SHARED, test_seg_range.shared) + self.assertEqual(self.PROJECT_ID, test_seg_range.project_id) + self.assertEqual(self.NETWORK_TYPE, test_seg_range.network_type) + self.assertEqual(self.PHYSICAL_NETWORK, + test_seg_range.physical_network) + self.assertEqual(self.MINIMUM, test_seg_range.minimum) + self.assertEqual(self.MAXIMUM, test_seg_range.maximum) + + def tearDown(self): + super(TestNetworkSegmentRange, self).tearDown() + + def test_create_delete(self): + del_test_seg_range = self.conn.network.delete_network_segment_range( + self.NETWORK_SEGMENT_RANGE_ID) + self.assertIsNone(del_test_seg_range) + + def test_find(self): + test_seg_range = self.conn.network.find_network_segment_range( + self.NETWORK_SEGMENT_RANGE_ID) + self.assertEqual(self.NETWORK_SEGMENT_RANGE_ID, test_seg_range.id) + + def test_get(self): + test_seg_range = self.conn.network.get_network_segment_range( + self.NETWORK_SEGMENT_RANGE_ID) + self.assertEqual(self.NETWORK_SEGMENT_RANGE_ID, test_seg_range.id) + self.assertEqual(self.NAME, test_seg_range.name) + self.assertEqual(self.DEFAULT, test_seg_range.default) + self.assertEqual(self.SHARED, test_seg_range.shared) + self.assertEqual(self.PROJECT_ID, test_seg_range.project_id) + self.assertEqual(self.NETWORK_TYPE, test_seg_range.network_type) + self.assertEqual(self.PHYSICAL_NETWORK, + test_seg_range.physical_network) + self.assertEqual(self.MINIMUM, test_seg_range.minimum) + self.assertEqual(self.MAXIMUM, test_seg_range.maximum) + + def test_list(self): + ids = [o.id for o in self.conn.network.network_segment_ranges( + name=None)] + self.assertIn(self.NETWORK_SEGMENT_RANGE_ID, ids) + + def test_update(self): + update_seg_range = self.conn.network.update_segment( + self.NETWORK_SEGMENT_RANGE_ID, name='update_test_name') + self.assertEqual('update_test_name', update_seg_range.name) diff --git a/openstack/tests/unit/network/v2/test_network_segment_range.py b/openstack/tests/unit/network/v2/test_network_segment_range.py new file mode 100644 index 000000000..9a76cc234 --- /dev/null +++ b/openstack/tests/unit/network/v2/test_network_segment_range.py @@ -0,0 +1,64 @@ +# Copyright (c) 2018, Intel Corporation. +# 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 openstack.tests.unit import base + +from openstack.network.v2 import network_segment_range + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'id': IDENTIFIER, + 'name': '1', + 'default': False, + 'shared': False, + 'project_id': '2', + 'network_type': '3', + 'physical_network': '4', + 'minimum': 5, + 'maximum': 6, + 'used': {}, + 'available': [], +} + + +class TestNetworkSegmentRange(base.TestCase): + + def test_basic(self): + test_seg_range = network_segment_range.NetworkSegmentRange() + self.assertEqual('network_segment_range', test_seg_range.resource_key) + self.assertEqual('network_segment_ranges', + test_seg_range.resources_key) + self.assertEqual('/network_segment_ranges', test_seg_range.base_path) + + self.assertTrue(test_seg_range.allow_create) + self.assertTrue(test_seg_range.allow_fetch) + self.assertTrue(test_seg_range.allow_commit) + self.assertTrue(test_seg_range.allow_delete) + self.assertTrue(test_seg_range.allow_list) + + def test_make_it(self): + test_seg_range = network_segment_range.NetworkSegmentRange(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], test_seg_range.id) + self.assertEqual(EXAMPLE['name'], test_seg_range.name) + self.assertEqual(EXAMPLE['default'], test_seg_range.default) + self.assertEqual(EXAMPLE['shared'], test_seg_range.shared) + self.assertEqual(EXAMPLE['project_id'], test_seg_range.project_id) + self.assertEqual(EXAMPLE['network_type'], test_seg_range.network_type) + self.assertEqual(EXAMPLE['physical_network'], + test_seg_range.physical_network) + self.assertEqual(EXAMPLE['minimum'], test_seg_range.minimum) + self.assertEqual(EXAMPLE['maximum'], test_seg_range.maximum) + self.assertEqual(EXAMPLE['used'], test_seg_range.used) + self.assertEqual(EXAMPLE['available'], test_seg_range.available) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 5f5bef458..2a7d98be2 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -32,6 +32,7 @@ from openstack.network.v2 import metering_label_rule from openstack.network.v2 import network from openstack.network.v2 import network_ip_availability +from openstack.network.v2 import network_segment_range from openstack.network.v2 import pool from openstack.network.v2 import pool_member from openstack.network.v2 import port @@ -962,6 +963,35 @@ def test_firewall_rule_update(self): self.verify_update(self.proxy.update_firewall_rule, firewall_rule.FirewallRule) + def test_network_segment_range_create_attrs(self): + self.verify_create(self.proxy.create_network_segment_range, + network_segment_range.NetworkSegmentRange) + + def test_network_segment_range_delete(self): + self.verify_delete(self.proxy.delete_network_segment_range, + network_segment_range.NetworkSegmentRange, False) + + def test_network_segment_range_delete_ignore(self): + self.verify_delete(self.proxy.delete_network_segment_range, + network_segment_range.NetworkSegmentRange, True) + + def test_network_segment_range_find(self): + self.verify_find(self.proxy.find_network_segment_range, + network_segment_range.NetworkSegmentRange) + + def test_network_segment_range_get(self): + self.verify_get(self.proxy.get_network_segment_range, + network_segment_range.NetworkSegmentRange) + + def test_network_segment_ranges(self): + self.verify_list(self.proxy.network_segment_ranges, + network_segment_range.NetworkSegmentRange, + paginated=False) + + def test_network_segment_range_update(self): + self.verify_update(self.proxy.update_network_segment_range, + network_segment_range.NetworkSegmentRange) + def test_security_group_create_attrs(self): self.verify_create(self.proxy.create_security_group, security_group.SecurityGroup) From 9cb9fc8905ba9ed155d277d205c6c794b565410e Mon Sep 17 00:00:00 2001 From: "B.Haleblian" Date: Thu, 10 Jan 2019 00:38:43 +0100 Subject: [PATCH 2336/3836] Bug : identity v3 Proxy role assignments only support instances. Fix: these methods now conform to their docstring. params Role and User can be either string ID or instance Change-Id: I3fa0a4dce45515d28fd0b2a7362c30b1081aa69f --- openstack/identity/v3/_proxy.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 142e3c462..d35a326ff 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -1128,6 +1128,8 @@ def assign_project_role_to_user(self, project, user, role): :return: ``None`` """ project = self._get_resource(_project.Project, project) + user = self._get_resource(_user.User, user) + role = self._get_resource(_role.Role, role) project.assign_role_to_user(self, user, role) def unassign_project_role_from_user(self, project, user, role): @@ -1143,4 +1145,6 @@ def unassign_project_role_from_user(self, project, user, role): :return: ``None`` """ project = self._get_resource(_project.Project, project) + user = self._get_resource(_user.User, user) + role = self._get_resource(_role.Role, role) project.unassign_role_from_user(self, user, role) From 161f5123bfe2707e33961e3c0b8b74b52828af69 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 19 Dec 2018 19:43:00 +0000 Subject: [PATCH 2337/3836] Compute location properly in server The shade layer pulls the az and project_id info out of the server record and feeds it into the location field. This is because the location field should be where the resource is, and the existing token should only be a fallback. This isn't happening in the resource layer, so start doing that. As a result of digging, make sure we always pass in the connection object to the Resources when we make them. And stop creating a bare resource in order to use the 'new' class method. It's a class method, we don't need to instantiate. Change-Id: Ic8850cd2863e2c806464b92cde2c749f6cc01a91 --- openstack/cloud/_normalize.py | 9 +++-- openstack/compute/v2/server.py | 8 ++++ openstack/proxy.py | 30 ++++++++++---- openstack/resource.py | 25 +++++------- openstack/tests/unit/cloud/test_fwaas.py | 37 ++++++++++++----- .../tests/unit/network/v2/test_floating_ip.py | 9 +++-- openstack/tests/unit/test_proxy.py | 40 ++++++++++++++----- openstack/tests/unit/test_resource.py | 27 ++++++++----- ...tion-server-resource-af77fdab5d35d421.yaml | 6 +++ 9 files changed, 130 insertions(+), 61 deletions(-) create mode 100644 releasenotes/notes/location-server-resource-af77fdab5d35d421.yaml diff --git a/openstack/cloud/_normalize.py b/openstack/cloud/_normalize.py index 17ee787ed..ea7687fa7 100644 --- a/openstack/cloud/_normalize.py +++ b/openstack/cloud/_normalize.py @@ -483,10 +483,11 @@ def _normalize_server(self, server): server, 'OS-EXT-AZ:availability_zone', None, self.strict_mode) # the server resource has this already, but it's missing az info # from the resource. - # TODO(mordred) Fix server resource to set az in the location - server.pop('location', None) - ret['location'] = self._get_current_location( - project_id=project_id, zone=az) + # TODO(mordred) create_server is still normalizing servers that aren't + # from the resource layer. + ret['location'] = server.pop( + 'location', self._get_current_location( + project_id=project_id, zone=az)) # Ensure volumes is always in the server dict, even if empty ret['volumes'] = _pop_or_get( diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index a9db33f9a..85fc71e46 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -138,6 +138,14 @@ class Server(resource.Resource, metadata.MetadataMixin, resource.TagMixin): #: only. instance_name = resource.Body('OS-EXT-SRV-ATTR:instance_name') + def __init__(self, *args, **kwargs): + super(Server, self).__init__(*args, **kwargs) + + if self._connection: + self.location = self._connection._get_current_location( + project_id=self.project_id, + zone=self.availability_zone) + def _prepare_request(self, requires_id=True, prepend_key=True, base_path=None): request = super(Server, self)._prepare_request(requires_id=requires_id, diff --git a/openstack/proxy.py b/openstack/proxy.py index 339f5dc93..c003473d6 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -56,6 +56,17 @@ def __init__(self, *args, **kwargs): self.retriable_status_codes) super(Proxy, self).__init__(*args, **kwargs) + def _get_connection(self): + """Get the Connection object associated with this Proxy. + + When the Session is created, a reference to the Connection is attached + to the ``_sdk_connection`` attribute. We also add a reference to it + directly on ourselves. Use one of them. + """ + return getattr( + self, '_connection', getattr( + self.session, '_sdk_connection', None)) + def _get_resource(self, resource_type, value, **attrs): """Get a resource object to work on @@ -69,16 +80,19 @@ class if using an existing instance, or ``munch.Munch``, :param path_args: A dict containing arguments for forming the request URL, if needed. """ + conn = self._get_connection() if value is None: # Create a bare resource - res = resource_type.new(**attrs) + res = resource_type.new(connection=conn, **attrs) elif (isinstance(value, dict) and not isinstance(value, resource.Resource)): - res = resource_type._from_munch(value) + res = resource_type._from_munch( + value, connection=conn) res._update(**attrs) elif not isinstance(value, resource_type): # Create from an ID - res = resource_type.new(id=value, **attrs) + res = resource_type.new( + id=value, connection=conn, **attrs) else: # An existing resource instance res = value @@ -205,7 +219,8 @@ def _create(self, resource_type, base_path=None, **attrs): :returns: The result of the ``create`` :rtype: :class:`~openstack.resource.Resource` """ - res = resource_type.new(**attrs) + conn = self._get_connection() + res = resource_type.new(connection=conn, **attrs) return res.create(self, base_path=base_path) @_check_resource(strict=False) @@ -265,9 +280,10 @@ def _list(self, resource_type, value=None, :class:`~openstack.resource.Resource` that doesn't match the ``resource_type``. """ - res = self._get_resource(resource_type, value, **attrs) - return res.list(self, paginated=paginated, - base_path=base_path, **attrs) + return resource_type.list( + self, paginated=paginated, + base_path=base_path, + **attrs) def _head(self, resource_type, value=None, base_path=None, **attrs): """Retrieve a resource's header diff --git a/openstack/resource.py b/openstack/resource.py index 8ecc39e55..912b9fb60 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -765,11 +765,7 @@ def existing(cls, connection=None, **kwargs): :param dict kwargs: Each of the named arguments will be set as attributes on the resulting Resource object. """ - res = cls(_synchronized=True, **kwargs) - # TODO(shade) Done as a second call rather than a constructor param - # because otherwise the mocking in the tests goes nuts. - res._connection = connection - return res + return cls(_synchronized=True, connection=connection, **kwargs) @classmethod def _from_munch(cls, obj, synchronized=True, connection=None): @@ -1338,11 +1334,10 @@ def list(cls, session, paginated=False, base_path=None, **params): # argument and is practically a reserved word. raw_resource.pop("self", None) - value = cls.existing(microversion=microversion, **raw_resource) - # TODO(shade) Done as a second call rather than a constructor - # param because otherwise the mocking in the tests goes nuts. - if hasattr(session, '_sdk_connection'): - value._connection = session._sdk_connection + value = cls.existing( + microversion=microversion, + connection=session._get_connection(), + **raw_resource) marker = value.id yield value total_yielded += 1 @@ -1447,13 +1442,13 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): :raises: :class:`openstack.exceptions.ResourceNotFound` if nothing is found and ignore_missing is ``False``. """ + session = cls._get_session(session) # Try to short-circuit by looking directly for a matching ID. try: - match = cls.existing(id=name_or_id, **params) - # TODO(shade) Done as a second call rather than a constructor - # param because otherwise the mocking in the tests goes nuts. - if hasattr(session, '_sdk_connection'): - match._connection = session._sdk_connection + match = cls.existing( + id=name_or_id, + connection=session._get_connection(), + **params) return match.fetch(session) except exceptions.NotFoundException: pass diff --git a/openstack/tests/unit/cloud/test_fwaas.py b/openstack/tests/unit/cloud/test_fwaas.py index abfc3a498..4ad163d3c 100644 --- a/openstack/tests/unit/cloud/test_fwaas.py +++ b/openstack/tests/unit/cloud/test_fwaas.py @@ -48,9 +48,10 @@ class TestFirewallRule(FirewallTestCase): mock_firewall_rule = None def setUp(self, cloud_config_fixture='clouds.yaml'): + super(TestFirewallRule, self).setUp() self.mock_firewall_rule = FirewallRule( + connection=self.cloud, **self._mock_firewall_rule_attrs).to_dict() - super(TestFirewallRule, self).setUp() def test_create_firewall_rule(self): # attributes that are passed to the tested function @@ -260,9 +261,10 @@ class TestFirewallPolicy(FirewallTestCase): mock_firewall_policy = None def setUp(self, cloud_config_fixture='clouds.yaml'): + super(TestFirewallPolicy, self).setUp() self.mock_firewall_policy = FirewallPolicy( + connection=self.cloud, **self._mock_firewall_policy_attrs).to_dict() - super(TestFirewallPolicy, self).setUp() def test_create_firewall_policy(self): # attributes that are passed to the tested method @@ -272,7 +274,7 @@ def test_create_firewall_policy(self): # policy that is returned by the POST request created_attrs = deepcopy(self._mock_firewall_policy_attrs) created_attrs['firewall_rules'][0] = TestFirewallRule.firewall_rule_id - created_policy = FirewallPolicy(**created_attrs) + created_policy = FirewallPolicy(connection=self.cloud, **created_attrs) # attributes used to validate the request inside register_uris() validate_attrs = deepcopy(created_attrs) @@ -421,7 +423,9 @@ def test_list_firewall_policies(self): self.mock_firewall_policy.copy(), self.mock_firewall_policy.copy()]}) ]) - policy = FirewallPolicy(**self.mock_firewall_policy) + policy = FirewallPolicy( + connection=self.cloud, + **self.mock_firewall_policy) self.assertListEqual(self.cloud.list_firewall_policies(), [policy, policy]) self.assert_calls() @@ -434,12 +438,16 @@ def test_list_firewall_policies_filters(self): json={'firewall_policies': [ self.mock_firewall_policy]}) ]) - self.assertListEqual(self.cloud.list_firewall_policies(filters), - [FirewallPolicy(**self.mock_firewall_policy)]) + self.assertListEqual( + self.cloud.list_firewall_policies(filters), [ + FirewallPolicy( + connection=self.cloud, + **self.mock_firewall_policy)]) self.assert_calls() def test_update_firewall_policy(self): lookup_rule = FirewallRule( + connection=self.cloud, **TestFirewallRule._mock_firewall_rule_attrs).to_dict() params = {'firewall_rules': [lookup_rule['id']], 'description': 'updated!'} @@ -520,7 +528,9 @@ def test_update_firewall_policy_filters(self): self.cloud.network.find_firewall_policy = _find def test_insert_rule_into_policy(self): - rule0 = FirewallRule(**TestFirewallRule._mock_firewall_rule_attrs) + rule0 = FirewallRule( + connection=self.cloud, + **TestFirewallRule._mock_firewall_rule_attrs) _rule1_attrs = deepcopy( TestFirewallRule._mock_firewall_rule_attrs) @@ -834,15 +844,19 @@ class TestFirewallGroup(FirewallTestCase): mock_returned_firewall_rule = None def setUp(self, cloud_config_fixture='clouds.yaml'): + super(TestFirewallGroup, self).setUp() self.mock_egress_policy = FirewallPolicy( + connection=self.cloud, **self._mock_egress_policy_attrs).to_dict() self.mock_ingress_policy = FirewallPolicy( + connection=self.cloud, **self._mock_ingress_policy_attrs).to_dict() self.mock_firewall_group = FirewallGroup( + connection=self.cloud, **self._mock_firewall_group_attrs).to_dict() self.mock_returned_firewall_group = FirewallGroup( + connection=self.cloud, **self._mock_returned_firewall_group_attrs).to_dict() - super(TestFirewallGroup, self).setUp() def test_create_firewall_group(self): create_group_attrs = self._mock_firewall_group_attrs.copy() @@ -901,7 +915,10 @@ def test_create_firewall_group_compact(self): ]) r = self.cloud.create_firewall_group(**firewall_group) self.assertDictEqual( - FirewallGroup(**created_firewall).to_dict(), r.to_dict()) + FirewallGroup( + connection=self.cloud, + **created_firewall).to_dict(), + r.to_dict()) self.assert_calls() def test_delete_firewall_group(self): @@ -1010,7 +1027,7 @@ def test_list_firewall_groups(self): uri=self._make_mock_url('firewall_groups'), json={'firewall_groups': [returned_attrs, returned_attrs]}) ]) - group = FirewallGroup(**returned_attrs) + group = FirewallGroup(connection=self.cloud, **returned_attrs) self.assertListEqual([group, group], self.cloud.list_firewall_groups()) self.assert_calls() diff --git a/openstack/tests/unit/network/v2/test_floating_ip.py b/openstack/tests/unit/network/v2/test_floating_ip.py index 77cc2002e..544520f69 100644 --- a/openstack/tests/unit/network/v2/test_floating_ip.py +++ b/openstack/tests/unit/network/v2/test_floating_ip.py @@ -10,11 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -from keystoneauth1 import adapter import mock -from openstack.tests.unit import base +from openstack import proxy from openstack.network.v2 import floating_ip +from openstack.tests.unit import base IDENTIFIER = 'IDENTIFIER' EXAMPLE = { @@ -73,9 +73,10 @@ def test_make_it(self): self.assertEqual(EXAMPLE['tags'], sot.tags) def test_find_available(self): - mock_session = mock.Mock(spec=adapter.Adapter) + mock_session = mock.Mock(spec=proxy.Proxy) mock_session.get_filter = mock.Mock(return_value={}) mock_session.default_microversion = None + mock_session.session = self.cloud.session data = {'id': 'one', 'floating_ip_address': '10.0.0.1'} fake_response = mock.Mock() body = {floating_ip.FloatingIP.resources_key: [data]} @@ -93,7 +94,7 @@ def test_find_available(self): microversion=None) def test_find_available_nada(self): - mock_session = mock.Mock(spec=adapter.Adapter) + mock_session = mock.Mock(spec=proxy.Proxy) mock_session.default_microversion = None fake_response = mock.Mock() body = {floating_ip.FloatingIP.resources_key: []} diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index 297a06a0a..abf6d1cfb 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -54,7 +54,10 @@ def method(self, expected_type, value): self.sot = mock.Mock() self.sot.method = method - self.fake_proxy = proxy.Proxy("session") + self.session = mock.Mock() + self.session._sdk_connection = self.cloud + self.fake_proxy = proxy.Proxy(self.session) + self.fake_proxy._connection = self.cloud def _test_correct(self, value): decorated = proxy._check_resource(strict=False)(self.sot.method) @@ -119,7 +122,7 @@ def test__get_resource_new(self): result = self.fake_proxy._get_resource(fake_type, None, **attrs) - fake_type.new.assert_called_with(**attrs) + fake_type.new.assert_called_with(connection=self.cloud, **attrs) self.assertEqual(value, result) def test__get_resource_from_id(self): @@ -143,7 +146,8 @@ def new(cls, **kwargs): result = self.fake_proxy._get_resource(Fake, id, **attrs) - self.assertDictEqual(dict(id=id, **attrs), Fake.call) + self.assertDictEqual( + dict(id=id, connection=mock.ANY, **attrs), Fake.call) self.assertEqual(value, result) def test__get_resource_from_resource(self): @@ -169,7 +173,7 @@ def test__get_resource_from_munch(self): result = self.fake_proxy._get_resource(cls, m, **attrs) - cls._from_munch.assert_called_once_with(m) + cls._from_munch.assert_called_once_with(m, connection=self.cloud) res._update.assert_called_once_with(**attrs) self.assertEqual(result, res) @@ -180,6 +184,7 @@ def setUp(self): super(TestProxyDelete, self).setUp() self.session = mock.Mock() + self.session._sdk_connection = self.cloud self.fake_id = 1 self.res = mock.Mock(spec=DeleteableResource) @@ -187,6 +192,7 @@ def setUp(self): self.res.delete = mock.Mock() self.sot = proxy.Proxy(self.session) + self.sot._connection = self.cloud DeleteableResource.new = mock.Mock(return_value=self.res) def test_delete(self): @@ -194,7 +200,8 @@ def test_delete(self): self.res.delete.assert_called_with(self.sot) self.sot._delete(DeleteableResource, self.fake_id) - DeleteableResource.new.assert_called_with(id=self.fake_id) + DeleteableResource.new.assert_called_with( + connection=self.cloud, id=self.fake_id) self.res.delete.assert_called_with(self.sot) # Delete generally doesn't return anything, so we will normally @@ -244,6 +251,7 @@ def setUp(self): self.res.commit = mock.Mock(return_value=self.fake_result) self.sot = proxy.Proxy(self.session) + self.sot._connection = self.cloud self.attrs = {"x": 1, "y": 2, "z": 3} @@ -278,12 +286,14 @@ def setUp(self): super(TestProxyCreate, self).setUp() self.session = mock.Mock() + self.session._sdk_connection = self.cloud self.fake_result = "fake_result" self.res = mock.Mock(spec=CreateableResource) self.res.create = mock.Mock(return_value=self.fake_result) self.sot = proxy.Proxy(self.session) + self.sot._connection = self.cloud def test_create_attributes(self): CreateableResource.new = mock.Mock(return_value=self.res) @@ -292,7 +302,8 @@ def test_create_attributes(self): rv = self.sot._create(CreateableResource, **attrs) self.assertEqual(rv, self.fake_result) - CreateableResource.new.assert_called_once_with(**attrs) + CreateableResource.new.assert_called_once_with( + connection=self.cloud, **attrs) self.res.create.assert_called_once_with(self.sot, base_path=None) def test_create_attributes_override_base_path(self): @@ -303,7 +314,8 @@ def test_create_attributes_override_base_path(self): rv = self.sot._create(CreateableResource, base_path=base_path, **attrs) self.assertEqual(rv, self.fake_result) - CreateableResource.new.assert_called_once_with(**attrs) + CreateableResource.new.assert_called_once_with( + connection=self.cloud, **attrs) self.res.create.assert_called_once_with(self.sot, base_path=base_path) @@ -313,6 +325,7 @@ def setUp(self): super(TestProxyGet, self).setUp() self.session = mock.Mock() + self.session._sdk_connection = self.cloud self.fake_id = 1 self.fake_name = "fake_name" @@ -322,6 +335,7 @@ def setUp(self): self.res.fetch = mock.Mock(return_value=self.fake_result) self.sot = proxy.Proxy(self.session) + self.sot._connection = self.cloud RetrieveableResource.new = mock.Mock(return_value=self.res) def test_get_resource(self): @@ -346,7 +360,8 @@ def test_get_resource_with_args(self): def test_get_id(self): rv = self.sot._get(RetrieveableResource, self.fake_id) - RetrieveableResource.new.assert_called_with(id=self.fake_id) + RetrieveableResource.new.assert_called_with( + connection=self.cloud, id=self.fake_id) self.res.fetch.assert_called_with( self.sot, requires_id=True, base_path=None, error_message=mock.ANY) @@ -357,7 +372,8 @@ def test_get_base_path(self): rv = self.sot._get(RetrieveableResource, self.fake_id, base_path=base_path) - RetrieveableResource.new.assert_called_with(id=self.fake_id) + RetrieveableResource.new.assert_called_with( + connection=self.cloud, id=self.fake_id) self.res.fetch.assert_called_with( self.sot, requires_id=True, base_path=base_path, error_message=mock.ANY) @@ -383,6 +399,7 @@ def setUp(self): self.fake_response = [resource.Resource()] self.sot = proxy.Proxy(self.session) + self.sot._connection = self.cloud ListableResource.list = mock.Mock() ListableResource.list.return_value = self.fake_response @@ -410,6 +427,7 @@ def setUp(self): super(TestProxyHead, self).setUp() self.session = mock.Mock() + self.session._sdk_connection = self.cloud self.fake_id = 1 self.fake_name = "fake_name" @@ -419,6 +437,7 @@ def setUp(self): self.res.head = mock.Mock(return_value=self.fake_result) self.sot = proxy.Proxy(self.session) + self.sot._connection = self.cloud HeadableResource.new = mock.Mock(return_value=self.res) def test_head_resource(self): @@ -437,6 +456,7 @@ def test_head_resource_base_path(self): def test_head_id(self): rv = self.sot._head(HeadableResource, self.fake_id) - HeadableResource.new.assert_called_with(id=self.fake_id) + HeadableResource.new.assert_called_with( + connection=self.cloud, id=self.fake_id) self.res.head.assert_called_with(self.sot, base_path=None) self.assertEqual(rv, self.fake_result) diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index f46e9ea68..f1719f79c 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1068,6 +1068,8 @@ class Test(resource.Resource): self.session.post = mock.Mock(return_value=self.response) self.session.delete = mock.Mock(return_value=self.response) self.session.head = mock.Mock(return_value=self.response) + self.session.session = self.session + self.session._get_connection = mock.Mock(return_value=self.cloud) self.session.default_microversion = None self.session.retriable_status_codes = None @@ -2048,20 +2050,23 @@ def existing(cls, **kwargs): mock_match.fetch.return_value = value return mock_match - result = Test.find("session", "name") + result = Test.find(self.cloud.compute, "name") self.assertEqual(result, value) def test_no_match_raise(self): self.assertRaises(exceptions.ResourceNotFound, self.no_results.find, - "session", "name", ignore_missing=False) + self.cloud.compute, "name", ignore_missing=False) def test_no_match_return(self): self.assertIsNone( - self.no_results.find("session", "name", ignore_missing=True)) + self.no_results.find( + self.cloud.compute, "name", ignore_missing=True)) def test_find_result(self): - self.assertEqual(self.result, self.one_result.find("session", "name")) + self.assertEqual( + self.result, + self.one_result.find(self.cloud.compute, "name")) def test_match_empty_results(self): self.assertIsNone(resource.Resource._get_one_match("name", [])) @@ -2126,7 +2131,7 @@ def test_immediate_status(self): res.status = status result = resource.wait_for_status( - "session", res, status, "failures", "interval", "wait") + self.cloud.compute, res, status, "failures", "interval", "wait") self.assertTrue(result, res) @@ -2136,7 +2141,7 @@ def test_immediate_status_case(self): res.status = status result = resource.wait_for_status( - "session", res, 'lOling', "failures", "interval", "wait") + self.cloud.compute, res, 'lOling', "failures", "interval", "wait") self.assertTrue(result, res) @@ -2146,7 +2151,7 @@ def test_immediate_status_different_attribute(self): res.mood = status result = resource.wait_for_status( - "session", res, status, "failures", "interval", "wait", + self.cloud.compute, res, status, "failures", "interval", "wait", attribute='mood') self.assertTrue(result, res) @@ -2236,7 +2241,7 @@ def test_timeout(self): self.assertRaises(exceptions.ResourceTimeout, resource.wait_for_status, - "session", res, status, None, 0.01, 0.1) + self.cloud.compute, res, status, None, 0.01, 0.1) def test_no_sleep(self): res = mock.Mock() @@ -2245,7 +2250,7 @@ def test_no_sleep(self): self.assertRaises(exceptions.ResourceTimeout, resource.wait_for_status, - "session", res, "status", None, 0, -1) + self.cloud.compute, res, "status", None, 0, -1) class TestWaitForDelete(base.TestCase): @@ -2259,7 +2264,7 @@ def test_success(self): None, None, exceptions.ResourceNotFound('Not Found', response)] - result = resource.wait_for_delete("session", res, 1, 3) + result = resource.wait_for_delete(self.cloud.compute, res, 1, 3) self.assertEqual(result, res) @@ -2271,7 +2276,7 @@ def test_timeout(self): self.assertRaises( exceptions.ResourceTimeout, resource.wait_for_delete, - "session", res, 0.1, 0.3) + self.cloud.compute, res, 0.1, 0.3) @mock.patch.object(resource.Resource, '_get_microversion_for', autospec=True) diff --git a/releasenotes/notes/location-server-resource-af77fdab5d35d421.yaml b/releasenotes/notes/location-server-resource-af77fdab5d35d421.yaml new file mode 100644 index 000000000..d548881d8 --- /dev/null +++ b/releasenotes/notes/location-server-resource-af77fdab5d35d421.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Corrected the location property on the ``Server`` resource to + use the ``project_id`` from the remote resource rather than the + information from the token of the user. From 47f92065905dc50ff7167cef77f2611c2ff3d488 Mon Sep 17 00:00:00 2001 From: "B.Haleblian" Date: Sat, 12 Jan 2019 02:15:50 +0100 Subject: [PATCH 2338/3836] Fix/Add : Identity V3 validate user role HTTP status handling in Project class is now correct: - 204 if head() returns OK (User has Role) --> True - else --> False Add this validate functionality to Identity v3 Proxy Change-Id: I12862b229a462182bd303e8a0b20fb326e6b9bff --- openstack/identity/v3/_proxy.py | 17 +++++++++++++++++ openstack/identity/v3/project.py | 4 ++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index d35a326ff..66ba8dbd9 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -1148,3 +1148,20 @@ def unassign_project_role_from_user(self, project, user, role): user = self._get_resource(_user.User, user) role = self._get_resource(_role.Role, role) project.unassign_role_from_user(self, user, role) + + def validate_user_has_role(self, project, user, role): + """Validates that a user has a role on a project + + :param project: Either the ID of a project or a + :class:`~openstack.identity.v3.project.Project` + instance. + :param user: Either the ID of a user or a + :class:`~openstack.identity.v3.user.User` instance. + :param role: Either the ID of a role or a + :class:`~openstack.identity.v3.role.Role` instance. + :returns: True if user has role in project + """ + project = self._get_resource(_project.Project, project) + user = self._get_resource(_user.User, user) + role = self._get_resource(_role.Role, role) + return project.validate_user_has_role(self, user, role) diff --git a/openstack/identity/v3/project.py b/openstack/identity/v3/project.py index 5f5a741c2..19fa8dc9d 100644 --- a/openstack/identity/v3/project.py +++ b/openstack/identity/v3/project.py @@ -73,7 +73,7 @@ def validate_user_has_role(self, session, user, role): url = utils.urljoin(self.base_path, self.id, 'users', user.id, 'roles', role.id) resp = session.head(url,) - if resp.status_code == 201: + if resp.status_code == 204: return True return False @@ -100,7 +100,7 @@ def validate_group_has_role(self, session, group, role): url = utils.urljoin(self.base_path, self.id, 'groups', group.id, 'roles', role.id) resp = session.head(url,) - if resp.status_code == 201: + if resp.status_code == 204: return True return False From 4dfd5a85b4379c5ff3566fe14676ea07948bbd58 Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Sun, 13 Jan 2019 15:01:50 +0000 Subject: [PATCH 2339/3836] Add block_storage v3 API support Existing block_storage v2 API methods were duplicated to v3 API. v3 API is a superset of v2 API. All tests are passing with only modifications to the change the module API version from 2 --> 3. Change-Id: I8cbabfb547a6b33e45af4fc779c786a23d441c91 --- .../block_storage/block_storage_service.py | 6 +- openstack/block_storage/v3/__init__.py | 0 openstack/block_storage/v3/_proxy.py | 354 ++++++++++++++++++ openstack/block_storage/v3/backup.py | 100 +++++ openstack/block_storage/v3/snapshot.py | 70 ++++ openstack/block_storage/v3/stats.py | 31 ++ openstack/block_storage/v3/type.py | 37 ++ openstack/block_storage/v3/volume.py | 125 +++++++ .../functional/block_storage/v3/__init__.py | 0 .../tests/functional/block_storage/v3/base.py | 33 ++ .../block_storage/v3/test_backup.py | 68 ++++ .../block_storage/v3/test_snapshot.py | 68 ++++ .../functional/block_storage/v3/test_type.py | 40 ++ .../block_storage/v3/test_volume.py | 50 +++ .../tests/unit/block_storage/v3/__init__.py | 0 .../unit/block_storage/v3/test_backup.py | 121 ++++++ .../tests/unit/block_storage/v3/test_proxy.py | 171 +++++++++ .../unit/block_storage/v3/test_snapshot.py | 94 +++++ .../tests/unit/block_storage/v3/test_type.py | 48 +++ .../unit/block_storage/v3/test_volume.py | 153 ++++++++ .../block-storage-v3-9798d584d088c048.yaml | 4 + 21 files changed, 1571 insertions(+), 2 deletions(-) create mode 100644 openstack/block_storage/v3/__init__.py create mode 100644 openstack/block_storage/v3/_proxy.py create mode 100644 openstack/block_storage/v3/backup.py create mode 100644 openstack/block_storage/v3/snapshot.py create mode 100644 openstack/block_storage/v3/stats.py create mode 100644 openstack/block_storage/v3/type.py create mode 100644 openstack/block_storage/v3/volume.py create mode 100644 openstack/tests/functional/block_storage/v3/__init__.py create mode 100644 openstack/tests/functional/block_storage/v3/base.py create mode 100644 openstack/tests/functional/block_storage/v3/test_backup.py create mode 100644 openstack/tests/functional/block_storage/v3/test_snapshot.py create mode 100644 openstack/tests/functional/block_storage/v3/test_type.py create mode 100644 openstack/tests/functional/block_storage/v3/test_volume.py create mode 100644 openstack/tests/unit/block_storage/v3/__init__.py create mode 100644 openstack/tests/unit/block_storage/v3/test_backup.py create mode 100644 openstack/tests/unit/block_storage/v3/test_proxy.py create mode 100644 openstack/tests/unit/block_storage/v3/test_snapshot.py create mode 100644 openstack/tests/unit/block_storage/v3/test_type.py create mode 100644 openstack/tests/unit/block_storage/v3/test_volume.py create mode 100644 releasenotes/notes/block-storage-v3-9798d584d088c048.yaml diff --git a/openstack/block_storage/block_storage_service.py b/openstack/block_storage/block_storage_service.py index a4906298a..5ab0ca409 100644 --- a/openstack/block_storage/block_storage_service.py +++ b/openstack/block_storage/block_storage_service.py @@ -10,7 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.block_storage.v2 import _proxy +from openstack.block_storage.v2 import _proxy as _v2_proxy +from openstack.block_storage.v3 import _proxy as _v3_proxy from openstack import service_description @@ -18,5 +19,6 @@ class BlockStorageService(service_description.ServiceDescription): """The block storage service.""" supported_versions = { - '2': _proxy.Proxy, + '3': _v3_proxy.Proxy, + '2': _v2_proxy.Proxy, } diff --git a/openstack/block_storage/v3/__init__.py b/openstack/block_storage/v3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py new file mode 100644 index 000000000..62f82272f --- /dev/null +++ b/openstack/block_storage/v3/_proxy.py @@ -0,0 +1,354 @@ +# 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.block_storage.v3 import backup as _backup +from openstack.block_storage.v3 import snapshot as _snapshot +from openstack.block_storage.v3 import stats as _stats +from openstack.block_storage.v3 import type as _type +from openstack.block_storage.v3 import volume as _volume +from openstack import exceptions +from openstack import proxy +from openstack import resource + + +class Proxy(proxy.Proxy): + + def get_snapshot(self, snapshot): + """Get a single snapshot + + :param snapshot: The value can be the ID of a snapshot or a + :class:`~openstack.volume.v3.snapshot.Snapshot` + instance. + + :returns: One :class:`~openstack.volume.v3.snapshot.Snapshot` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_snapshot.Snapshot, snapshot) + + def snapshots(self, details=True, **query): + """Retrieve a generator of snapshots + + :param bool details: When set to ``False`` + :class:`~openstack.block_storage.v3.snapshot.Snapshot` + objects will be returned. The default, ``True``, will cause + :class:`~openstack.block_storage.v3.snapshot.SnapshotDetail` + objects to be returned. + :param kwargs query: Optional query parameters to be sent to limit + the snapshots being returned. Available parameters include: + + * name: Name of the snapshot as a string. + * all_projects: Whether return the snapshots in all projects. + * volume_id: volume id of a snapshot. + * status: Value of the status of the snapshot so that you can + filter on "available" for example. + + :returns: A generator of snapshot objects. + """ + snapshot = _snapshot.SnapshotDetail if details else _snapshot.Snapshot + return self._list(snapshot, paginated=True, **query) + + def create_snapshot(self, **attrs): + """Create a new snapshot from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.volume.v3.snapshot.Snapshot`, + comprised of the properties on the Snapshot class. + + :returns: The results of snapshot creation + :rtype: :class:`~openstack.volume.v3.snapshot.Snapshot` + """ + return self._create(_snapshot.Snapshot, **attrs) + + def delete_snapshot(self, snapshot, ignore_missing=True): + """Delete a snapshot + + :param snapshot: The value can be either the ID of a snapshot or a + :class:`~openstack.volume.v3.snapshot.Snapshot` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the snapshot does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent snapshot. + + :returns: ``None`` + """ + self._delete(_snapshot.Snapshot, snapshot, + ignore_missing=ignore_missing) + + def get_type(self, type): + """Get a single type + + :param type: The value can be the ID of a type or a + :class:`~openstack.volume.v3.type.Type` instance. + + :returns: One :class:`~openstack.volume.v3.type.Type` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_type.Type, type) + + def types(self, **query): + """Retrieve a generator of volume types + + :returns: A generator of volume type objects. + """ + return self._list(_type.Type, paginated=False, **query) + + def create_type(self, **attrs): + """Create a new type from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.volume.v3.type.Type`, + comprised of the properties on the Type class. + + :returns: The results of type creation + :rtype: :class:`~openstack.volume.v3.type.Type` + """ + return self._create(_type.Type, **attrs) + + def delete_type(self, type, ignore_missing=True): + """Delete a type + + :param type: The value can be either the ID of a type or a + :class:`~openstack.volume.v3.type.Type` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the type does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent type. + + :returns: ``None`` + """ + self._delete(_type.Type, type, ignore_missing=ignore_missing) + + def get_volume(self, volume): + """Get a single volume + + :param volume: The value can be the ID of a volume or a + :class:`~openstack.volume.v3.volume.Volume` instance. + + :returns: One :class:`~openstack.volume.v3.volume.Volume` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_volume.Volume, volume) + + def volumes(self, details=True, **query): + """Retrieve a generator of volumes + + :param bool details: When set to ``False`` + :class:`~openstack.block_storage.v3.volume.Volume` objects + will be returned. The default, ``True``, will cause + :class:`~openstack.block_storage.v3.volume.VolumeDetail` + objects to be returned. + :param kwargs query: Optional query parameters to be sent to limit + the volumes being returned. Available parameters include: + + * name: Name of the volume as a string. + * all_projects: Whether return the volumes in all projects + * status: Value of the status of the volume so that you can filter + on "available" for example. + + :returns: A generator of volume objects. + """ + volume = _volume.VolumeDetail if details else _volume.Volume + return self._list(volume, paginated=True, **query) + + def create_volume(self, **attrs): + """Create a new volume from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.volume.v3.volume.Volume`, + comprised of the properties on the Volume class. + + :returns: The results of volume creation + :rtype: :class:`~openstack.volume.v3.volume.Volume` + """ + return self._create(_volume.Volume, **attrs) + + def delete_volume(self, volume, ignore_missing=True): + """Delete a volume + + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.volume.v3.volume.Volume` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the volume does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent volume. + + :returns: ``None`` + """ + self._delete(_volume.Volume, volume, ignore_missing=ignore_missing) + + def extend_volume(self, volume, size): + """Extend a volume + + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.volume.v3.volume.Volume` instance. + :param size: New volume size + + :returns: None + """ + volume = self._get_resource(_volume.Volume, volume) + volume.extend(self, size) + + def backend_pools(self): + """Returns a generator of cinder Back-end storage pools + + :returns A generator of cinder Back-end storage pools objects + """ + return self._list(_stats.Pools, paginated=False) + + def backups(self, details=True, **query): + """Retrieve a generator of backups + + :param bool details: When set to ``False`` + :class:`~openstack.block_storage.v3.backup.Backup` objects + will be returned. The default, ``True``, will cause + :class:`~openstack.block_storage.v3.backup.BackupDetail` + objects to be returned. + :param dict query: Optional query parameters to be sent to limit the + resources being returned: + + * offset: pagination marker + * limit: pagination limit + * sort_key: Sorts by an attribute. A valid value is + name, status, container_format, disk_format, size, id, + created_at, or updated_at. Default is created_at. + The API uses the natural sorting direction of the + sort_key attribute value. + * sort_dir: Sorts by one or more sets of attribute and sort + direction combinations. If you omit the sort direction + in a set, default is desc. + + :returns: A generator of backup objects. + """ + if not self._connection.has_service('object-store'): + raise exceptions.SDKException( + 'Object-store service is required for block-store backups' + ) + backup = _backup.BackupDetail if details else _backup.Backup + return self._list(backup, paginated=True, **query) + + def get_backup(self, backup): + """Get a backup + + :param backup: The value can be the ID of a backup + or a :class:`~openstack.block_storage.v3.backup.Backup` + instance. + + :returns: Backup instance + :rtype: :class:`~openstack.block_storage.v3.backup.Backup` + """ + if not self._connection.has_service('object-store'): + raise exceptions.SDKException( + 'Object-store service is required for block-store backups' + ) + return self._get(_backup.Backup, backup) + + def create_backup(self, **attrs): + """Create a new Backup from attributes with native API + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.block_storage.v3.backup.Backup` + comprised of the properties on the Backup class. + + :returns: The results of Backup creation + :rtype: :class:`~openstack.block_storage.v3.backup.Backup` + """ + if not self._connection.has_service('object-store'): + raise exceptions.SDKException( + 'Object-store service is required for block-store backups' + ) + return self._create(_backup.Backup, **attrs) + + def delete_backup(self, backup, ignore_missing=True): + """Delete a CloudBackup + + :param backup: The value can be the ID of a backup or a + :class:`~openstack.block_storage.v3.backup.Backup` instance + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the zone does not exist. + When set to ``True``, no exception will be set when attempting to + delete a nonexistent zone. + + :returns: ``None`` + """ + if not self._connection.has_service('object-store'): + raise exceptions.SDKException( + 'Object-store service is required for block-store backups' + ) + self._delete(_backup.Backup, backup, + ignore_missing=ignore_missing) + + def restore_backup(self, backup, volume_id, name): + """Restore a Backup to volume + + :param backup: The value can be the ID of a backup or a + :class:`~openstack.block_storage.v3.backup.Backup` instance + :param volume_id: The ID of the volume to restore the backup to. + :param name: The name for new volume creation to restore. + + :returns: Updated backup instance + :rtype: :class:`~openstack.block_storage.v3.backup.Backup` + """ + if not self._connection.has_service('object-store'): + raise exceptions.SDKException( + 'Object-store service is required for block-store backups' + ) + backup = self._get_resource(_backup.Backup, backup) + return backup.restore(self, volume_id=volume_id, name=name) + + def wait_for_status(self, res, status='ACTIVE', failures=None, + interval=2, wait=120): + """Wait for a resource to be in a particular status. + + :param res: The resource to wait on to reach the specified status. + The resource must have a ``status`` attribute. + :type resource: A :class:`~openstack.resource.Resource` object. + :param status: Desired status. + :param failures: Statuses that would be interpreted as failures. + :type failures: :py:class:`list` + :param interval: Number of seconds to wait before to consecutive + checks. Default to 2. + :param wait: Maximum number of seconds to wait before the change. + Default to 120. + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to the desired status failed to occur in specified seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + has transited to one of the failure statuses. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute. + """ + failures = ['Error'] if failures is None else failures + return resource.wait_for_status( + self, res, status, failures, interval, wait) + + def wait_for_delete(self, res, interval=2, wait=120): + """Wait for a resource to be deleted. + + :param res: The resource to wait on to be deleted. + :type resource: A :class:`~openstack.resource.Resource` object. + :param interval: Number of seconds to wait before to consecutive + checks. Default to 2. + :param wait: Maximum number of seconds to wait before the change. + Default to 120. + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete(self, res, interval, wait) diff --git a/openstack/block_storage/v3/backup.py b/openstack/block_storage/v3/backup.py new file mode 100644 index 000000000..8b3c1f82f --- /dev/null +++ b/openstack/block_storage/v3/backup.py @@ -0,0 +1,100 @@ +# 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 resource +from openstack import utils + + +class Backup(resource.Resource): + """Volume Backup""" + resource_key = "backup" + resources_key = "backups" + base_path = "/backups" + + _query_mapping = resource.QueryParameters( + 'all_tenants', 'limit', 'marker', + 'sort_key', 'sort_dir') + + # capabilities + allow_fetch = True + allow_create = True + allow_delete = True + allow_list = True + allow_get = True + + #: Properties + #: backup availability zone + availability_zone = resource.Body("availability_zone") + #: The container backup in + container = resource.Body("container") + #: The date and time when the resource was created. + created_at = resource.Body("created_at") + #: data timestamp + #: The time when the data on the volume was first saved. + #: If it is a backup from volume, it will be the same as created_at + #: for a backup. If it is a backup from a snapshot, + #: it will be the same as created_at for the snapshot. + data_timestamp = resource.Body('data_timestamp') + #: backup description + description = resource.Body("description") + #: Backup fail reason + fail_reason = resource.Body("fail_reason") + #: Force backup + force = resource.Body("force", type=bool) + #: has_dependent_backups + #: If this value is true, there are other backups depending on this backup. + has_dependent_backups = resource.Body('has_dependent_backups', type=bool) + #: Indicates whether the backup mode is incremental. + #: If this value is true, the backup mode is incremental. + #: If this value is false, the backup mode is full. + is_incremental = resource.Body("is_incremental", type=bool) + #: A list of links associated with this volume. *Type: list* + links = resource.Body("links", type=list) + #: backup name + name = resource.Body("name") + #: backup object count + object_count = resource.Body("object_count", type=int) + #: The size of the volume, in gibibytes (GiB). + size = resource.Body("size", type=int) + #: The UUID of the source volume snapshot. + snapshot_id = resource.Body("snapshot_id") + #: backup status + #: values: creating, available, deleting, error, restoring, error_restoring + status = resource.Body("status") + #: The date and time when the resource was updated. + updated_at = resource.Body("updated_at") + #: The UUID of the volume. + volume_id = resource.Body("volume_id") + + def restore(self, session, volume_id=None, name=None): + """Restore current backup to volume + + :param session: openstack session + :param volume_id: The ID of the volume to restore the backup to. + :param name: The name for new volume creation to restore. + :return: + """ + url = utils.urljoin(self.base_path, self.id, "restore") + body = {"restore": {"volume_id": volume_id, "name": name}} + response = session.post(url, + json=body) + self._translate_response(response) + return self + + +class BackupDetail(Backup): + """Volume Backup with Details""" + base_path = "/backups/detail" + + # capabilities + allow_list = True + + #: Properties diff --git a/openstack/block_storage/v3/snapshot.py b/openstack/block_storage/v3/snapshot.py new file mode 100644 index 000000000..120b50b50 --- /dev/null +++ b/openstack/block_storage/v3/snapshot.py @@ -0,0 +1,70 @@ +# 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 format +from openstack import resource + + +class Snapshot(resource.Resource): + resource_key = "snapshot" + resources_key = "snapshots" + base_path = "/snapshots" + + _query_mapping = resource.QueryParameters( + 'name', 'status', 'volume_id', all_projects='all_tenants') + + # capabilities + allow_fetch = True + allow_create = True + allow_delete = True + allow_commit = True + allow_list = True + + # Properties + #: A ID representing this snapshot. + id = resource.Body("id") + #: Name of the snapshot. Default is None. + name = resource.Body("name") + + #: The current status of this snapshot. Potential values are creating, + #: available, deleting, error, and error_deleting. + status = resource.Body("status") + #: Description of snapshot. Default is None. + description = resource.Body("description") + #: The timestamp of this snapshot creation. + created_at = resource.Body("created_at") + #: Metadata associated with this snapshot. + metadata = resource.Body("metadata", type=dict) + #: The ID of the volume this snapshot was taken of. + volume_id = resource.Body("volume_id") + #: The size of the volume, in GBs. + size = resource.Body("size", type=int) + #: Indicate whether to create snapshot, even if the volume is attached. + #: Default is ``False``. *Type: bool* + is_forced = resource.Body("force", type=format.BoolStr) + + +class SnapshotDetail(Snapshot): + + base_path = "/snapshots/detail" + + # capabilities + allow_fetch = False + allow_create = False + allow_delete = False + allow_commit = False + allow_list = True + + #: The percentage of completeness the snapshot is currently at. + progress = resource.Body("os-extended-snapshot-attributes:progress") + #: The project ID this snapshot is associated with. + project_id = resource.Body("os-extended-snapshot-attributes:project_id") diff --git a/openstack/block_storage/v3/stats.py b/openstack/block_storage/v3/stats.py new file mode 100644 index 000000000..dcd2e2945 --- /dev/null +++ b/openstack/block_storage/v3/stats.py @@ -0,0 +1,31 @@ +# 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 resource + + +class Pools(resource.Resource): + resource_key = "pool" + resources_key = "pools" + base_path = "/scheduler-stats/get_pools?detail=True" + + # capabilities + allow_fetch = False + allow_create = False + allow_delete = False + allow_list = True + + # Properties + #: The Cinder name for the pool + name = resource.Body("name") + #: returns a dict with information about the pool + capabilities = resource.Body("capabilities", type=dict) diff --git a/openstack/block_storage/v3/type.py b/openstack/block_storage/v3/type.py new file mode 100644 index 000000000..7e3c81aae --- /dev/null +++ b/openstack/block_storage/v3/type.py @@ -0,0 +1,37 @@ +# 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 resource + + +class Type(resource.Resource): + resource_key = "volume_type" + resources_key = "volume_types" + base_path = "/types" + + # capabilities + allow_fetch = True + allow_create = True + allow_delete = True + allow_list = True + + _query_mapping = resource.QueryParameters("is_public") + + # Properties + #: A ID representing this type. + id = resource.Body("id") + #: Name of the type. + name = resource.Body("name") + #: A dict of extra specifications. "capabilities" is a usual key. + extra_specs = resource.Body("extra_specs", type=dict) + #: a private volume-type. *Type: bool* + is_public = resource.Body('os-volume-type-access:is_public', type=bool) diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py new file mode 100644 index 000000000..28ed870ff --- /dev/null +++ b/openstack/block_storage/v3/volume.py @@ -0,0 +1,125 @@ +# 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 format +from openstack import resource +from openstack import utils + + +class Volume(resource.Resource): + resource_key = "volume" + resources_key = "volumes" + base_path = "/volumes" + + _query_mapping = resource.QueryParameters( + 'name', 'status', 'project_id', all_projects='all_tenants') + + # capabilities + allow_fetch = True + allow_create = True + allow_delete = True + allow_commit = True + allow_list = True + + # Properties + #: A ID representing this volume. + id = resource.Body("id") + #: The name of this volume. + name = resource.Body("name") + #: A list of links associated with this volume. *Type: list* + links = resource.Body("links", type=list) + + #: The availability zone. + availability_zone = resource.Body("availability_zone") + #: To create a volume from an existing volume, specify the ID of + #: the existing volume. If specified, the volume is created with + #: same size of the source volume. + source_volume_id = resource.Body("source_volid") + #: The volume description. + description = resource.Body("description") + #: To create a volume from an existing snapshot, specify the ID of + #: the existing volume snapshot. If specified, the volume is created + #: in same availability zone and with same size of the snapshot. + snapshot_id = resource.Body("snapshot_id") + #: The size of the volume, in GBs. *Type: int* + size = resource.Body("size", type=int) + #: The ID of the image from which you want to create the volume. + #: Required to create a bootable volume. + image_id = resource.Body("imageRef") + #: The name of the associated volume type. + volume_type = resource.Body("volume_type") + #: Enables or disables the bootable attribute. You can boot an + #: instance from a bootable volume. *Type: bool* + is_bootable = resource.Body("bootable", type=format.BoolStr) + #: One or more metadata key and value pairs to associate with the volume. + metadata = resource.Body("metadata") + #: One or more metadata key and value pairs about image + volume_image_metadata = resource.Body("volume_image_metadata") + + #: One of the following values: creating, available, attaching, in-use + #: deleting, error, error_deleting, backing-up, restoring-backup, + #: error_restoring. For details on these statuses, see the + #: Block Storage API documentation. + status = resource.Body("status") + #: TODO(briancurtin): This is currently undocumented in the API. + attachments = resource.Body("attachments") + #: The timestamp of this volume creation. + created_at = resource.Body("created_at") + + def _action(self, session, body): + """Preform volume actions given the message body.""" + # NOTE: This is using Volume.base_path instead of self.base_path + # as both Volume and VolumeDetail instances can be acted on, but + # the URL used is sans any additional /detail/ part. + url = utils.urljoin(Volume.base_path, self.id, 'action') + headers = {'Accept': ''} + return session.post(url, json=body, headers=headers) + + def extend(self, session, size): + """Extend a volume size.""" + body = {'os-extend': {'new_size': size}} + self._action(session, body) + + +class VolumeDetail(Volume): + + base_path = "/volumes/detail" + + # capabilities + allow_fetch = False + allow_create = False + allow_delete = False + allow_commit = False + allow_list = True + + #: The volume's current back-end. + host = resource.Body("os-vol-host-attr:host") + #: The project ID associated with current back-end. + project_id = resource.Body("os-vol-tenant-attr:tenant_id") + #: The status of this volume's migration (None means that a migration + #: is not currently in progress). + migration_status = resource.Body("os-vol-mig-status-attr:migstat") + #: The volume ID that this volume's name on the back-end is based on. + migration_id = resource.Body("os-vol-mig-status-attr:name_id") + #: Status of replication on this volume. + replication_status = resource.Body("replication_status") + #: Extended replication status on this volume. + extended_replication_status = resource.Body( + "os-volume-replication:extended_status") + #: ID of the consistency group. + consistency_group_id = resource.Body("consistencygroup_id") + #: Data set by the replication driver + replication_driver_data = resource.Body( + "os-volume-replication:driver_data") + #: ``True`` if this volume is encrypted, ``False`` if not. + #: *Type: bool* + is_encrypted = resource.Body("encrypted", type=format.BoolStr) diff --git a/openstack/tests/functional/block_storage/v3/__init__.py b/openstack/tests/functional/block_storage/v3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/functional/block_storage/v3/base.py b/openstack/tests/functional/block_storage/v3/base.py new file mode 100644 index 000000000..5f98a3cdc --- /dev/null +++ b/openstack/tests/functional/block_storage/v3/base.py @@ -0,0 +1,33 @@ +# 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 + +from openstack.tests.functional import base + + +class BaseBlockStorageTest(base.BaseFunctionalTest): + + @classmethod + def setUpClass(cls): + super(BaseBlockStorageTest, cls).setUpClass() + cls._wait_for_timeout = int(os.getenv( + 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_BLOCK_STORAGE', + cls._wait_for_timeout)) + + def setUp(self): + super(BaseBlockStorageTest, self).setUp() + self._set_user_cloud(block_storage_api_version='3') + self._set_operator_cloud(block_storage_api_version='3') + + if not self.user_cloud.has_service('block-storage'): + self.skipTest('block-storage service not supported by cloud') diff --git a/openstack/tests/functional/block_storage/v3/test_backup.py b/openstack/tests/functional/block_storage/v3/test_backup.py new file mode 100644 index 000000000..d27fff2f6 --- /dev/null +++ b/openstack/tests/functional/block_storage/v3/test_backup.py @@ -0,0 +1,68 @@ +# 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.block_storage.v3 import volume as _volume +from openstack.block_storage.v3 import backup as _backup +from openstack.tests.functional.block_storage.v3 import base + + +class TestBackup(base.BaseBlockStorageTest): + + def setUp(self): + super(TestBackup, self).setUp() + + if not self.user_cloud.has_service('object-store'): + self.skipTest('Object service is requred, but not available') + + self.VOLUME_NAME = self.getUniqueString() + self.VOLUME_ID = None + self.BACKUP_NAME = self.getUniqueString() + self.BACKUP_ID = None + + volume = self.user_cloud.block_storage.create_volume( + name=self.VOLUME_NAME, + size=1) + self.user_cloud.block_storage.wait_for_status( + volume, + status='available', + failures=['error'], + interval=5, + wait=self._wait_for_timeout) + assert isinstance(volume, _volume.Volume) + self.VOLUME_ID = volume.id + + backup = self.user_cloud.block_storage.create_backup( + name=self.BACKUP_NAME, + volume_id=volume.id) + self.user_cloud.block_storage.wait_for_status( + backup, + status='available', + failures=['error'], + interval=5, + wait=self._wait_for_timeout) + assert isinstance(backup, _backup.Backup) + self.assertEqual(self.BACKUP_NAME, backup.name) + self.BACKUP_ID = backup.id + + def tearDown(self): + sot = self.user_cloud.block_storage.delete_backup( + self.BACKUP_ID, + ignore_missing=False) + sot = self.user_cloud.block_storage.delete_volume( + self.VOLUME_ID, + ignore_missing=False) + self.assertIsNone(sot) + super(TestBackup, self).tearDown() + + def test_get(self): + sot = self.user_cloud.block_storage.get_backup(self.BACKUP_ID) + self.assertEqual(self.BACKUP_NAME, sot.name) diff --git a/openstack/tests/functional/block_storage/v3/test_snapshot.py b/openstack/tests/functional/block_storage/v3/test_snapshot.py new file mode 100644 index 000000000..2ffc1a6d0 --- /dev/null +++ b/openstack/tests/functional/block_storage/v3/test_snapshot.py @@ -0,0 +1,68 @@ +# 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.block_storage.v3 import snapshot as _snapshot +from openstack.block_storage.v3 import volume as _volume +from openstack.tests.functional.block_storage.v3 import base + + +class TestSnapshot(base.BaseBlockStorageTest): + + def setUp(self): + super(TestSnapshot, self).setUp() + + self.SNAPSHOT_NAME = self.getUniqueString() + self.SNAPSHOT_ID = None + self.VOLUME_NAME = self.getUniqueString() + self.VOLUME_ID = None + + volume = self.user_cloud.block_storage.create_volume( + name=self.VOLUME_NAME, + size=1) + self.user_cloud.block_storage.wait_for_status( + volume, + status='available', + failures=['error'], + interval=2, + wait=self._wait_for_timeout) + assert isinstance(volume, _volume.Volume) + self.assertEqual(self.VOLUME_NAME, volume.name) + self.VOLUME_ID = volume.id + snapshot = self.user_cloud.block_storage.create_snapshot( + name=self.SNAPSHOT_NAME, + volume_id=self.VOLUME_ID) + self.user_cloud.block_storage.wait_for_status( + snapshot, + status='available', + failures=['error'], + interval=2, + wait=self._wait_for_timeout) + assert isinstance(snapshot, _snapshot.Snapshot) + self.assertEqual(self.SNAPSHOT_NAME, snapshot.name) + self.SNAPSHOT_ID = snapshot.id + + def tearDown(self): + snapshot = self.user_cloud.block_storage.get_snapshot(self.SNAPSHOT_ID) + sot = self.user_cloud.block_storage.delete_snapshot( + snapshot, ignore_missing=False) + self.user_cloud.block_storage.wait_for_delete( + snapshot, interval=2, wait=self._wait_for_timeout) + self.assertIsNone(sot) + sot = self.user_cloud.block_storage.delete_volume( + self.VOLUME_ID, ignore_missing=False) + self.assertIsNone(sot) + super(TestSnapshot, self).tearDown() + + def test_get(self): + sot = self.user_cloud.block_storage.get_snapshot(self.SNAPSHOT_ID) + self.assertEqual(self.SNAPSHOT_NAME, sot.name) diff --git a/openstack/tests/functional/block_storage/v3/test_type.py b/openstack/tests/functional/block_storage/v3/test_type.py new file mode 100644 index 000000000..c86e15930 --- /dev/null +++ b/openstack/tests/functional/block_storage/v3/test_type.py @@ -0,0 +1,40 @@ +# 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.block_storage.v3 import type as _type +from openstack.tests.functional.block_storage.v3 import base + + +class TestType(base.BaseBlockStorageTest): + + def setUp(self): + super(TestType, self).setUp() + + self.TYPE_NAME = self.getUniqueString() + self.TYPE_ID = None + + sot = self.operator_cloud.block_storage.create_type( + name=self.TYPE_NAME) + assert isinstance(sot, _type.Type) + self.assertEqual(self.TYPE_NAME, sot.name) + self.TYPE_ID = sot.id + + def tearDown(self): + sot = self.operator_cloud.block_storage.delete_type( + self.TYPE_ID, ignore_missing=False) + self.assertIsNone(sot) + super(TestType, self).tearDown() + + def test_get(self): + sot = self.operator_cloud.block_storage.get_type(self.TYPE_ID) + self.assertEqual(self.TYPE_NAME, sot.name) diff --git a/openstack/tests/functional/block_storage/v3/test_volume.py b/openstack/tests/functional/block_storage/v3/test_volume.py new file mode 100644 index 000000000..155461aca --- /dev/null +++ b/openstack/tests/functional/block_storage/v3/test_volume.py @@ -0,0 +1,50 @@ +# 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.block_storage.v3 import volume as _volume +from openstack.tests.functional.block_storage.v3 import base + + +class TestVolume(base.BaseBlockStorageTest): + + def setUp(self): + super(TestVolume, self).setUp() + + if not self.user_cloud.has_service('block-storage'): + self.skipTest('block-storage service not supported by cloud') + + self.VOLUME_NAME = self.getUniqueString() + self.VOLUME_ID = None + + volume = self.user_cloud.block_storage.create_volume( + name=self.VOLUME_NAME, + size=1) + self.user_cloud.block_storage.wait_for_status( + volume, + status='available', + failures=['error'], + interval=2, + wait=self._wait_for_timeout) + assert isinstance(volume, _volume.Volume) + self.assertEqual(self.VOLUME_NAME, volume.name) + self.VOLUME_ID = volume.id + + def tearDown(self): + sot = self.user_cloud.block_storage.delete_volume( + self.VOLUME_ID, + ignore_missing=False) + self.assertIsNone(sot) + super(TestVolume, self).tearDown() + + def test_get(self): + sot = self.user_cloud.block_storage.get_volume(self.VOLUME_ID) + self.assertEqual(self.VOLUME_NAME, sot.name) diff --git a/openstack/tests/unit/block_storage/v3/__init__.py b/openstack/tests/unit/block_storage/v3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/unit/block_storage/v3/test_backup.py b/openstack/tests/unit/block_storage/v3/test_backup.py new file mode 100644 index 000000000..509d1ac24 --- /dev/null +++ b/openstack/tests/unit/block_storage/v3/test_backup.py @@ -0,0 +1,121 @@ +# 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 import adapter + +from openstack.tests.unit import base + +from openstack.block_storage.v3 import backup + +FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff" + +BACKUP = { + "availability_zone": "az1", + "container": "volumebackups", + "created_at": "2018-04-02T10:35:27.000000", + "updated_at": "2018-04-03T10:35:27.000000", + "description": 'description', + "fail_reason": 'fail reason', + "id": FAKE_ID, + "name": "backup001", + "object_count": 22, + "size": 1, + "status": "available", + "volume_id": "e5185058-943a-4cb4-96d9-72c184c337d6", + "is_incremental": True, + "has_dependent_backups": False +} + +DETAILS = { +} + +BACKUP_DETAIL = copy.copy(BACKUP) +BACKUP_DETAIL.update(DETAILS) + + +class TestBackup(base.TestCase): + + def setUp(self): + super(TestBackup, self).setUp() + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.get = mock.Mock() + self.sess.default_microversion = mock.Mock(return_value='') + + def test_basic(self): + sot = backup.Backup(BACKUP) + self.assertEqual("backup", sot.resource_key) + self.assertEqual("backups", sot.resources_key) + self.assertEqual("/backups", sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_fetch) + + self.assertDictEqual( + { + "all_tenants": "all_tenants", + "limit": "limit", + "marker": "marker", + "sort_dir": "sort_dir", + "sort_key": "sort_key" + }, + sot._query_mapping._mapping + ) + + def test_create(self): + sot = backup.Backup(**BACKUP) + self.assertEqual(BACKUP["id"], sot.id) + self.assertEqual(BACKUP["name"], sot.name) + self.assertEqual(BACKUP["status"], sot.status) + self.assertEqual(BACKUP["container"], sot.container) + self.assertEqual(BACKUP["availability_zone"], sot.availability_zone) + self.assertEqual(BACKUP["created_at"], sot.created_at) + self.assertEqual(BACKUP["updated_at"], sot.updated_at) + self.assertEqual(BACKUP["description"], sot.description) + self.assertEqual(BACKUP["fail_reason"], sot.fail_reason) + self.assertEqual(BACKUP["volume_id"], sot.volume_id) + self.assertEqual(BACKUP["object_count"], sot.object_count) + self.assertEqual(BACKUP["is_incremental"], sot.is_incremental) + self.assertEqual(BACKUP["size"], sot.size) + self.assertEqual(BACKUP["has_dependent_backups"], + sot.has_dependent_backups) + + +class TestBackupDetail(base.TestCase): + + def test_basic(self): + sot = backup.BackupDetail(BACKUP_DETAIL) + self.assertIsInstance(sot, backup.Backup) + self.assertEqual("/backups/detail", sot.base_path) + + def test_create(self): + sot = backup.Backup(**BACKUP_DETAIL) + self.assertEqual(BACKUP_DETAIL["id"], sot.id) + self.assertEqual(BACKUP_DETAIL["name"], sot.name) + self.assertEqual(BACKUP_DETAIL["status"], sot.status) + self.assertEqual(BACKUP_DETAIL["container"], sot.container) + self.assertEqual(BACKUP_DETAIL["availability_zone"], + sot.availability_zone) + self.assertEqual(BACKUP_DETAIL["created_at"], sot.created_at) + self.assertEqual(BACKUP_DETAIL["updated_at"], sot.updated_at) + self.assertEqual(BACKUP_DETAIL["description"], sot.description) + self.assertEqual(BACKUP_DETAIL["fail_reason"], sot.fail_reason) + self.assertEqual(BACKUP_DETAIL["volume_id"], sot.volume_id) + self.assertEqual(BACKUP_DETAIL["object_count"], sot.object_count) + self.assertEqual(BACKUP_DETAIL["is_incremental"], sot.is_incremental) + self.assertEqual(BACKUP_DETAIL["size"], sot.size) + self.assertEqual(BACKUP_DETAIL["has_dependent_backups"], + sot.has_dependent_backups) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py new file mode 100644 index 000000000..528203054 --- /dev/null +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -0,0 +1,171 @@ +# 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 openstack import exceptions + +from openstack.block_storage.v3 import _proxy +from openstack.block_storage.v3 import backup +from openstack.block_storage.v3 import snapshot +from openstack.block_storage.v3 import stats +from openstack.block_storage.v3 import type +from openstack.block_storage.v3 import volume +from openstack.tests.unit import test_proxy_base + + +class TestVolumeProxy(test_proxy_base.TestProxyBase): + def setUp(self): + super(TestVolumeProxy, self).setUp() + self.proxy = _proxy.Proxy(self.session) + + def test_snapshot_get(self): + self.verify_get(self.proxy.get_snapshot, snapshot.Snapshot) + + def test_snapshots_detailed(self): + self.verify_list(self.proxy.snapshots, snapshot.SnapshotDetail, + paginated=True, + method_kwargs={"details": True, "query": 1}, + expected_kwargs={"query": 1}) + + def test_snapshots_not_detailed(self): + self.verify_list(self.proxy.snapshots, snapshot.Snapshot, + paginated=True, + method_kwargs={"details": False, "query": 1}, + expected_kwargs={"query": 1}) + + def test_snapshot_create_attrs(self): + self.verify_create(self.proxy.create_snapshot, snapshot.Snapshot) + + def test_snapshot_delete(self): + self.verify_delete(self.proxy.delete_snapshot, + snapshot.Snapshot, False) + + def test_snapshot_delete_ignore(self): + self.verify_delete(self.proxy.delete_snapshot, + snapshot.Snapshot, True) + + def test_type_get(self): + self.verify_get(self.proxy.get_type, type.Type) + + def test_types(self): + self.verify_list(self.proxy.types, type.Type, paginated=False) + + def test_type_create_attrs(self): + self.verify_create(self.proxy.create_type, type.Type) + + def test_type_delete(self): + self.verify_delete(self.proxy.delete_type, type.Type, False) + + def test_type_delete_ignore(self): + self.verify_delete(self.proxy.delete_type, type.Type, True) + + def test_volume_get(self): + self.verify_get(self.proxy.get_volume, volume.Volume) + + def test_volumes_detailed(self): + self.verify_list(self.proxy.volumes, volume.VolumeDetail, + paginated=True, + method_kwargs={"details": True, "query": 1}, + expected_kwargs={"query": 1}) + + def test_volumes_not_detailed(self): + self.verify_list(self.proxy.volumes, volume.Volume, + paginated=True, + method_kwargs={"details": False, "query": 1}, + expected_kwargs={"query": 1}) + + def test_volume_create_attrs(self): + self.verify_create(self.proxy.create_volume, volume.Volume) + + def test_volume_delete(self): + self.verify_delete(self.proxy.delete_volume, volume.Volume, False) + + def test_volume_delete_ignore(self): + self.verify_delete(self.proxy.delete_volume, volume.Volume, True) + + def test_volume_extend(self): + self._verify("openstack.block_storage.v3.volume.Volume.extend", + self.proxy.extend_volume, + method_args=["value", "new-size"], + expected_args=["new-size"]) + + def test_backend_pools(self): + self.verify_list(self.proxy.backend_pools, stats.Pools, + paginated=False) + + def test_backups_detailed(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self.verify_list(self.proxy.backups, backup.BackupDetail, + paginated=True, + method_kwargs={"details": True, "query": 1}, + expected_kwargs={"query": 1}) + + def test_backups_not_detailed(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self.verify_list(self.proxy.backups, backup.Backup, + paginated=True, + method_kwargs={"details": False, "query": 1}, + expected_kwargs={"query": 1}) + + def test_backup_get(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self.verify_get(self.proxy.get_backup, backup.Backup) + + def test_backup_delete(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self.verify_delete(self.proxy.delete_backup, backup.Backup, False) + + def test_backup_delete_ignore(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self.verify_delete(self.proxy.delete_backup, backup.Backup, True) + + def test_backup_create_attrs(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self.verify_create(self.proxy.create_backup, backup.Backup) + + def test_backup_restore(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self._verify2( + 'openstack.block_storage.v3.backup.Backup.restore', + self.proxy.restore_backup, + method_args=['volume_id'], + method_kwargs={'volume_id': 'vol_id', 'name': 'name'}, + expected_args=[self.proxy], + expected_kwargs={'volume_id': 'vol_id', 'name': 'name'} + ) + + def test_backup_no_swift(self): + """Ensure proxy method raises exception if swift is not available + """ + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=False) + self.assertRaises( + exceptions.SDKException, + self.proxy.restore_backup, + 'backup', + 'volume_id', + 'name') diff --git a/openstack/tests/unit/block_storage/v3/test_snapshot.py b/openstack/tests/unit/block_storage/v3/test_snapshot.py new file mode 100644 index 000000000..f55816807 --- /dev/null +++ b/openstack/tests/unit/block_storage/v3/test_snapshot.py @@ -0,0 +1,94 @@ +# 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.tests.unit import base + +from openstack.block_storage.v3 import snapshot + +FAKE_ID = "ffa9bc5e-1172-4021-acaf-cdcd78a9584d" + +SNAPSHOT = { + "status": "creating", + "description": "Daily backup", + "created_at": "2015-03-09T12:14:57.233772", + "metadata": {}, + "volume_id": "5aa119a8-d25b-45a7-8d1b-88e127885635", + "size": 1, + "id": FAKE_ID, + "name": "snap-001", + "force": "true", +} + +DETAILS = { + "os-extended-snapshot-attributes:progress": "100%", + "os-extended-snapshot-attributes:project_id": + "0c2eba2c5af04d3f9e9d0d410b371fde" +} + +DETAILED_SNAPSHOT = SNAPSHOT.copy() +DETAILED_SNAPSHOT.update(**DETAILS) + + +class TestSnapshot(base.TestCase): + + def test_basic(self): + sot = snapshot.Snapshot(SNAPSHOT) + self.assertEqual("snapshot", sot.resource_key) + self.assertEqual("snapshots", sot.resources_key) + self.assertEqual("/snapshots", sot.base_path) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + self.assertDictEqual({"name": "name", + "status": "status", + "all_projects": "all_tenants", + "volume_id": "volume_id", + "limit": "limit", + "marker": "marker"}, + sot._query_mapping._mapping) + + def test_create_basic(self): + sot = snapshot.Snapshot(**SNAPSHOT) + self.assertEqual(SNAPSHOT["id"], sot.id) + self.assertEqual(SNAPSHOT["status"], sot.status) + self.assertEqual(SNAPSHOT["created_at"], sot.created_at) + self.assertEqual(SNAPSHOT["metadata"], sot.metadata) + self.assertEqual(SNAPSHOT["volume_id"], sot.volume_id) + self.assertEqual(SNAPSHOT["size"], sot.size) + self.assertEqual(SNAPSHOT["name"], sot.name) + self.assertTrue(sot.is_forced) + + +class TestSnapshotDetail(base.TestCase): + + def test_basic(self): + sot = snapshot.SnapshotDetail(DETAILED_SNAPSHOT) + self.assertIsInstance(sot, snapshot.Snapshot) + self.assertEqual("/snapshots/detail", sot.base_path) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_create) + self.assertFalse(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_create_detailed(self): + sot = snapshot.SnapshotDetail(**DETAILED_SNAPSHOT) + + self.assertEqual( + DETAILED_SNAPSHOT["os-extended-snapshot-attributes:progress"], + sot.progress) + self.assertEqual( + DETAILED_SNAPSHOT["os-extended-snapshot-attributes:project_id"], + sot.project_id) diff --git a/openstack/tests/unit/block_storage/v3/test_type.py b/openstack/tests/unit/block_storage/v3/test_type.py new file mode 100644 index 000000000..5bdcff167 --- /dev/null +++ b/openstack/tests/unit/block_storage/v3/test_type.py @@ -0,0 +1,48 @@ +# 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.tests.unit import base + +from openstack.block_storage.v3 import type + +FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff" +TYPE = { + "extra_specs": { + "capabilities": "gpu" + }, + "id": FAKE_ID, + "name": "SSD" +} + + +class TestType(base.TestCase): + + def test_basic(self): + sot = type.Type(**TYPE) + self.assertEqual("volume_type", sot.resource_key) + self.assertEqual("volume_types", sot.resources_key) + self.assertEqual("/types", sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertFalse(sot.allow_commit) + + def test_new(self): + sot = type.Type.new(id=FAKE_ID) + self.assertEqual(FAKE_ID, sot.id) + + def test_create(self): + sot = type.Type(**TYPE) + self.assertEqual(TYPE["id"], sot.id) + self.assertEqual(TYPE["extra_specs"], sot.extra_specs) + self.assertEqual(TYPE["name"], sot.name) diff --git a/openstack/tests/unit/block_storage/v3/test_volume.py b/openstack/tests/unit/block_storage/v3/test_volume.py new file mode 100644 index 000000000..6519dbe94 --- /dev/null +++ b/openstack/tests/unit/block_storage/v3/test_volume.py @@ -0,0 +1,153 @@ +# 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 openstack.tests.unit import base + +from openstack.block_storage.v3 import volume + +FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff" +IMAGE_METADATA = { + 'container_format': 'bare', + 'min_ram': '64', 'disk_format': u'qcow2', + 'image_name': 'TestVM', + 'image_id': '625d4f2c-cf67-4af3-afb6-c7220f766947', + 'checksum': '64d7c1cd2b6f60c92c14662941cb7913', + 'min_disk': '0', u'size': '13167616' +} + +VOLUME = { + "status": "creating", + "name": "my_volume", + "attachments": [], + "availability_zone": "nova", + "bootable": "false", + "created_at": "2015-03-09T12:14:57.233772", + "description": "something", + "volume_type": "some_type", + "snapshot_id": "93c2e2aa-7744-4fd6-a31a-80c4726b08d7", + "source_volid": None, + "imageRef": "some_image", + "metadata": {}, + "volume_image_metadata": IMAGE_METADATA, + "id": FAKE_ID, + "size": 10 +} + +DETAILS = { + "os-vol-host-attr:host": "127.0.0.1", + "os-vol-tenant-attr:tenant_id": "some tenant", + "os-vol-mig-status-attr:migstat": "done", + "os-vol-mig-status-attr:name_id": "93c2e2aa-7744-4fd6-a31a-80c4726b08d7", + "replication_status": "nah", + "os-volume-replication:extended_status": "really nah", + "consistencygroup_id": "123asf-asdf123", + "os-volume-replication:driver_data": "ahasadfasdfasdfasdfsdf", + "snapshot_id": "93c2e2aa-7744-4fd6-a31a-80c4726b08d7", + "encrypted": "false", +} + +VOLUME_DETAIL = copy.copy(VOLUME) +VOLUME_DETAIL.update(DETAILS) + + +class TestVolume(base.TestCase): + + def setUp(self): + super(TestVolume, self).setUp() + self.resp = mock.Mock() + self.resp.body = None + self.resp.json = mock.Mock(return_value=self.resp.body) + self.sess = mock.Mock() + self.sess.post = mock.Mock(return_value=self.resp) + + def test_basic(self): + sot = volume.Volume(VOLUME) + self.assertEqual("volume", sot.resource_key) + self.assertEqual("volumes", sot.resources_key) + self.assertEqual("/volumes", sot.base_path) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + self.assertDictEqual({"name": "name", + "status": "status", + "all_projects": "all_tenants", + "project_id": "project_id", + "limit": "limit", + "marker": "marker"}, + sot._query_mapping._mapping) + + def test_create(self): + sot = volume.Volume(**VOLUME) + self.assertEqual(VOLUME["id"], sot.id) + self.assertEqual(VOLUME["status"], sot.status) + self.assertEqual(VOLUME["attachments"], sot.attachments) + self.assertEqual(VOLUME["availability_zone"], sot.availability_zone) + self.assertFalse(sot.is_bootable) + self.assertEqual(VOLUME["created_at"], sot.created_at) + self.assertEqual(VOLUME["description"], sot.description) + self.assertEqual(VOLUME["volume_type"], sot.volume_type) + self.assertEqual(VOLUME["snapshot_id"], sot.snapshot_id) + self.assertEqual(VOLUME["source_volid"], sot.source_volume_id) + self.assertEqual(VOLUME["metadata"], sot.metadata) + self.assertEqual(VOLUME["volume_image_metadata"], + sot.volume_image_metadata) + self.assertEqual(VOLUME["size"], sot.size) + self.assertEqual(VOLUME["imageRef"], sot.image_id) + + def test_extend(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.extend(self.sess, '20')) + + url = 'volumes/%s/action' % FAKE_ID + body = {"os-extend": {"new_size": "20"}} + headers = {'Accept': ''} + self.sess.post.assert_called_with(url, json=body, headers=headers) + + +class TestVolumeDetail(base.TestCase): + + def test_basic(self): + sot = volume.VolumeDetail(VOLUME_DETAIL) + self.assertIsInstance(sot, volume.Volume) + self.assertEqual("/volumes/detail", sot.base_path) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_create) + self.assertFalse(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_create(self): + sot = volume.VolumeDetail(**VOLUME_DETAIL) + self.assertEqual(VOLUME_DETAIL["os-vol-host-attr:host"], sot.host) + self.assertEqual(VOLUME_DETAIL["os-vol-tenant-attr:tenant_id"], + sot.project_id) + self.assertEqual(VOLUME_DETAIL["os-vol-mig-status-attr:migstat"], + sot.migration_status) + self.assertEqual(VOLUME_DETAIL["os-vol-mig-status-attr:name_id"], + sot.migration_id) + self.assertEqual(VOLUME_DETAIL["replication_status"], + sot.replication_status) + self.assertEqual( + VOLUME_DETAIL["os-volume-replication:extended_status"], + sot.extended_replication_status) + self.assertEqual(VOLUME_DETAIL["consistencygroup_id"], + sot.consistency_group_id) + self.assertEqual(VOLUME_DETAIL["os-volume-replication:driver_data"], + sot.replication_driver_data) + self.assertFalse(sot.is_encrypted) diff --git a/releasenotes/notes/block-storage-v3-9798d584d088c048.yaml b/releasenotes/notes/block-storage-v3-9798d584d088c048.yaml new file mode 100644 index 000000000..0ce997f49 --- /dev/null +++ b/releasenotes/notes/block-storage-v3-9798d584d088c048.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Added support for block storage v3. From 9021d5114e82e272d67ba365bd09b1c607971230 Mon Sep 17 00:00:00 2001 From: "B.Haleblian" Date: Sat, 12 Jan 2019 16:49:36 +0100 Subject: [PATCH 2340/3836] implement identity v3 Proxy group role management identity/v3/_proxy.py handles CVD of project group roles assignments this patchset proxies following Project class methods - unassign_project_role_from_group(project,group,role) - assign_project_role_to_group(project,group,role) - validate_group_has_role(project,group,role) Fix no known issue; just adds functionality Change-Id: I48855a112bb2c46c3aeeb00255b4d8479f0750da --- openstack/identity/v3/_proxy.py | 51 +++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 66ba8dbd9..f922ba587 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -1165,3 +1165,54 @@ def validate_user_has_role(self, project, user, role): user = self._get_resource(_user.User, user) role = self._get_resource(_role.Role, role) return project.validate_user_has_role(self, user, role) + + def assign_project_role_to_group(self, project, group, role): + """Assign role to group on a project + + :param project: Either the ID of a project or a + :class:`~openstack.identity.v3.project.Project` + instance. + :param group: Either the ID of a group or a + :class:`~openstack.identity.v3.group.Group` instance. + :param role: Either the ID of a role or a + :class:`~openstack.identity.v3.role.Role` instance. + :return: ``None`` + """ + project = self._get_resource(_project.Project, project) + group = self._get_resource(_group.Group, group) + role = self._get_resource(_role.Role, role) + project.assign_role_to_group(self, group, role) + + def unassign_project_role_from_group(self, project, group, role): + """Unassign role from group on a project + + :param project: Either the ID of a project or a + :class:`~openstack.identity.v3.project.Project` + instance. + :param group: Either the ID of a group or a + :class:`~openstack.identity.v3.group.Group` instance. + :param role: Either the ID of a role or a + :class:`~openstack.identity.v3.role.Role` instance. + :return: ``None`` + """ + project = self._get_resource(_project.Project, project) + group = self._get_resource(_group.Group, group) + role = self._get_resource(_role.Role, role) + project.unassign_role_from_group(self, group, role) + + def validate_group_has_role(self, project, group, role): + """Validates that a group has a role on a project + + :param project: Either the ID of a project or a + :class:`~openstack.identity.v3.project.Project` + instance. + :param group: Either the ID of a group or a + :class:`~openstack.identity.v3.group.Group` instance. + :param role: Either the ID of a role or a + :class:`~openstack.identity.v3.role.Role` instance. + :returns: True if group has role in project + """ + project = self._get_resource(_project.Project, project) + group = self._get_resource(_group.Group, group) + role = self._get_resource(_role.Role, role) + return project.validate_group_has_role(self, group, role) From ec588200ccde39958e4e3349086251c1ed47c184 Mon Sep 17 00:00:00 2001 From: Yuval Shalev Date: Sun, 13 Jan 2019 21:38:11 +0200 Subject: [PATCH 2341/3836] Fix for not released thread in service_description The thread that is created in the 'temp_adapter' is never released, we should use task_manager.stop() to release unnecessary resources. If not released each connection to service will create a threadpool that is not detected by the garbage collector. Story: 28885 Change-Id: I2f237b7b8908d0106c2ad0f1b4b2fbf585c86465 --- openstack/service_description.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openstack/service_description.py b/openstack/service_description.py index 5abc14b00..bab08562b 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -199,6 +199,7 @@ def _make_proxy(self, instance): service_type=self.service_type), category=exceptions.UnsupportedServiceVersion) return temp_adapter + temp_adapter.task_manager.stop() proxy_class = self.supported_versions.get(str(found_version[0])) if not proxy_class: proxy_class = proxy.Proxy From 01b0dec9ae4308c1d11429908df0f4262a1e7fbc Mon Sep 17 00:00:00 2001 From: Yuval Shalev Date: Mon, 14 Jan 2019 14:45:35 +0200 Subject: [PATCH 2342/3836] Fix for not released thread in get_session_client The thread that is created in the 'network_adapter' is never released, we should use task_manager.stop() to release unnecessary resources. If not released each connection to service will create a threadpool that is not detected by the garbage collector. Change-Id: Iefb2fc55caf38eed5f79aec3622c21e80be5936b Story: 28885 --- openstack/config/cloud_region.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index e96cabe32..0d763d96c 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -492,6 +492,7 @@ def get_session_client( region_name=self.region_name, ) network_endpoint = network_adapter.get_endpoint() + network_adapter.task_manager.stop() if not network_endpoint.rstrip().rsplit('/')[-1] == 'v2.0': if not network_endpoint.endswith('/'): network_endpoint += '/' From b98c66d69a936e763d7d3a98f60294ce3a2fdd74 Mon Sep 17 00:00:00 2001 From: "B.Haleblian" Date: Mon, 14 Jan 2019 15:21:01 +0100 Subject: [PATCH 2343/3836] Document "Role Assignment Operations" Change-Id: I254f0c695b9f43e78653721b4534232f759bde6e --- doc/source/user/proxies/identity_v3.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/doc/source/user/proxies/identity_v3.rst b/doc/source/user/proxies/identity_v3.rst index e2c5b125e..7a81270fa 100644 --- a/doc/source/user/proxies/identity_v3.rst +++ b/doc/source/user/proxies/identity_v3.rst @@ -107,8 +107,21 @@ Role Operations .. automethod:: openstack.identity.v3._proxy.Proxy.get_role .. automethod:: openstack.identity.v3._proxy.Proxy.find_role .. automethod:: openstack.identity.v3._proxy.Proxy.roles + +Role Assignment Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.identity.v3._proxy.Proxy + .. automethod:: openstack.identity.v3._proxy.Proxy.role_assignments .. automethod:: openstack.identity.v3._proxy.Proxy.role_assignments_filter + .. automethod:: openstack.identity.v3._proxy.Proxy.assign_project_role_to_user + .. automethod:: openstack.identity.v3._proxy.Proxy.unassign_project_role_from_user + .. automethod:: openstack.identity.v3._proxy.Proxy.validate_user_has_role + .. automethod:: openstack.identity.v3._proxy.Proxy.assign_project_role_to_group + .. automethod:: openstack.identity.v3._proxy.Proxy.unassign_project_role_from_group + .. automethod:: openstack.identity.v3._proxy.Proxy.validate_group_has_role + Service Operations ^^^^^^^^^^^^^^^^^^ From 3b022e8d70e7e4a10da009f1c3a1615090bf2786 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Fri, 18 Jan 2019 18:15:19 +0100 Subject: [PATCH 2344/3836] Return retries on HTTP CONFLICT to baremetal.attach_vif_to_node Unfortunately, it is very easy to hit the "node locked" error when retries are disabled, so add retries to this call as well. Change-Id: I076cf2537a20687932aca9fd85358bf02882b736 --- openstack/baremetal/v1/_proxy.py | 8 ++++++-- openstack/baremetal/v1/node.py | 13 +++++++++---- openstack/tests/unit/baremetal/v1/test_node.py | 10 +++++++++- .../notes/baremetal-retries-804f553b4e22b3bf.yaml | 8 ++++++++ 4 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/baremetal-retries-804f553b4e22b3bf.yaml diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 59d2635ec..ae10c54dd 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -632,7 +632,7 @@ def delete_port_group(self, port_group, ignore_missing=True): return self._delete(_portgroup.PortGroup, port_group, ignore_missing=ignore_missing) - def attach_vif_to_node(self, node, vif_id): + def attach_vif_to_node(self, node, vif_id, retry_on_conflict=True): """Attach a VIF to the node. The exact form of the VIF ID depends on the network interface used by @@ -643,12 +643,16 @@ def attach_vif_to_node(self, node, vif_id): :param node: The value can be either the name or ID of a node or a :class:`~openstack.baremetal.v1.node.Node` instance. :param string vif_id: Backend-specific VIF ID. + :param retry_on_conflict: Whether to retry HTTP CONFLICT errors. + This can happen when either the VIF is already used on a node or + the node is locked. Since the latter happens more often, the + default value is True. :return: ``None`` :raises: :exc:`~openstack.exceptions.NotSupported` if the server does not support the VIF API. """ res = self._get_resource(_node.Node, node) - res.attach_vif(self, vif_id) + res.attach_vif(self, vif_id, retry_on_conflict=retry_on_conflict) def detach_vif_from_node(self, node, vif_id, ignore_missing=True): """Detach a VIF from the node. diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index edfb6e9c0..9c2356ebc 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -447,7 +447,7 @@ def _check_state_reached(self, session, expected_state, "the last error is %(error)s" % {'node': self.id, 'error': self.last_error}) - def attach_vif(self, session, vif_id): + def attach_vif(self, session, vif_id, retry_on_conflict=True): """Attach a VIF to the node. The exact form of the VIF ID depends on the network interface used by @@ -458,6 +458,10 @@ def attach_vif(self, session, vif_id): :param session: The session to use for making this request. :type session: :class:`~keystoneauth1.adapter.Adapter` :param string vif_id: Backend-specific VIF ID. + :param retry_on_conflict: Whether to retry HTTP CONFLICT errors. + This can happen when either the VIF is already used on a node or + the node is locked. Since the latter happens more often, the + default value is True. :return: ``None`` :raises: :exc:`~openstack.exceptions.NotSupported` if the server does not support the VIF API. @@ -470,12 +474,13 @@ def attach_vif(self, session, vif_id): request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'vifs') body = {'id': vif_id} + retriable_status_codes = _common.RETRIABLE_STATUS_CODES + if not retry_on_conflict: + retriable_status_codes = set(retriable_status_codes) - {409} response = session.post( request.url, json=body, headers=request.headers, microversion=version, - # NOTE(dtantsur): do not retry CONFLICT, it's a valid status code - # in this API when the VIF is already attached to another node. - retriable_status_codes=[503]) + retriable_status_codes=retriable_status_codes) msg = ("Failed to attach VIF {vif} to bare metal node {node}" .format(node=self.id, vif=vif_id)) diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 71aed2e08..466e6a7e4 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -342,7 +342,15 @@ def test_attach_vif(self): self.session.post.assert_called_once_with( 'nodes/%s/vifs' % self.node.id, json={'id': self.vif_id}, headers=mock.ANY, microversion='1.28', - retriable_status_codes=[503]) + retriable_status_codes=[409, 503]) + + def test_attach_vif_no_retries(self): + self.assertIsNone(self.node.attach_vif(self.session, self.vif_id, + retry_on_conflict=False)) + self.session.post.assert_called_once_with( + 'nodes/%s/vifs' % self.node.id, json={'id': self.vif_id}, + headers=mock.ANY, microversion='1.28', + retriable_status_codes={503}) def test_detach_vif_existing(self): self.assertTrue(self.node.detach_vif(self.session, self.vif_id)) diff --git a/releasenotes/notes/baremetal-retries-804f553b4e22b3bf.yaml b/releasenotes/notes/baremetal-retries-804f553b4e22b3bf.yaml new file mode 100644 index 000000000..b54dc1f19 --- /dev/null +++ b/releasenotes/notes/baremetal-retries-804f553b4e22b3bf.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Changes the ``baremetal.attach_vif_to_node`` call to retry HTTP CONFLICT + by default. While it's a valid error code when a VIF is already attached + to a node, the same code is also used when the target node is locked. + The latter happens more often, so the retries are now on by default and + can be disabled by setting ``retry_on_conflict`` to ``False``. From 45b22aa890f4adcf3d8860ca7bcc1aa6db3c52d9 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Fri, 18 Jan 2019 18:25:02 +0100 Subject: [PATCH 2345/3836] Add baremetal Node fields from versions 1.47 - 1.49 Change-Id: I585c4c7bb5d13a5e98a84b58d8d8014115b7179c --- openstack/baremetal/v1/node.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index edfb6e9c0..88a04cbe8 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -56,14 +56,17 @@ class Node(_common.ListMixin, resource.Resource): is_maintenance='maintenance', ) - # The conductor_group field introduced in 1.46 (Rocky). - _max_microversion = '1.46' + # The conductor field introduced in 1.49 (Stein). + _max_microversion = '1.49' # Properties #: The UUID of the chassis associated wit this node. Can be empty or None. chassis_id = resource.Body("chassis_uuid") #: The current clean step. clean_step = resource.Body("clean_step") + #: Hostname of the conductor currently handling this ndoe. Added in API + # microversion 1.49. + conductor = resource.Body("conductor") #: Conductor group this node is managed by. Added in API microversion 1.46. conductor_group = resource.Body("conductor_group") #: Timestamp at which the node was last updated. @@ -91,11 +94,16 @@ class Node(_common.ListMixin, resource.Resource): instance_info = resource.Body("instance_info") #: UUID of the nova instance associated with this node. instance_id = resource.Body("instance_uuid") + #: Override enabling of automated cleaning. Added in API microversion 1.47. + is_automated_clean_enabled = resource.Body("automated_clean", type=bool) #: Whether console access is enabled on this node. is_console_enabled = resource.Body("console_enabled", type=bool) #: Whether node is currently in "maintenance mode". Nodes put into #: maintenance mode are removed from the available resource pool. is_maintenance = resource.Body("maintenance", type=bool) + # Whether the node is protected from undeploying. Added in API microversion + # 1.48. + is_protected = resource.Body("protected", type=bool) #: Any error from the most recent transaction that started but failed to #: finish. last_error = resource.Body("last_error") @@ -118,6 +126,8 @@ class Node(_common.ListMixin, resource.Resource): #: Physical characteristics of the node. Content populated by the service #: during inspection. properties = resource.Body("properties", type=dict) + # The reason why this node is protected. Added in API microversion 1.48. + protected_reason = resource.Body("protected_reason") #: The current provisioning state of the node. provision_state = resource.Body("provision_state") #: The current RAID configuration of the node. From ce4d1d4da1e46842c1fe68cc00231cbd846fa643 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Tue, 22 Jan 2019 09:59:07 +0100 Subject: [PATCH 2346/3836] Fixed incorrect exception raising in configdrive generation When genisoimage is missing, we raise an exception with incorrect formatting. This patch fixes it and adds relevant unit tests. Change-Id: I628fa334f424c5f703c2e4941d5b4cb71ca089e3 --- openstack/baremetal/configdrive.py | 2 +- .../tests/unit/baremetal/test_configdrive.py | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/openstack/baremetal/configdrive.py b/openstack/baremetal/configdrive.py index 1fee2847f..504d332a2 100644 --- a/openstack/baremetal/configdrive.py +++ b/openstack/baremetal/configdrive.py @@ -90,7 +90,7 @@ def pack(path): except OSError as e: raise RuntimeError( 'Error generating the configdrive. Make sure the ' - '"genisoimage" tool is installed. Error: %s') % e + '"genisoimage" tool is installed. Error: %s' % e) stdout, stderr = p.communicate() if p.returncode != 0: diff --git a/openstack/tests/unit/baremetal/test_configdrive.py b/openstack/tests/unit/baremetal/test_configdrive.py index f6765d038..846e05c57 100644 --- a/openstack/tests/unit/baremetal/test_configdrive.py +++ b/openstack/tests/unit/baremetal/test_configdrive.py @@ -16,6 +16,8 @@ import json import os +import mock +import six import testtools from openstack.baremetal import configdrive @@ -46,3 +48,25 @@ def test_without_user_data(self): def test_with_user_data(self): self._check({'foo': 42}, b'I am user data') + + +@mock.patch('subprocess.Popen', autospec=True) +class TestPack(testtools.TestCase): + + def test_no_genisoimage(self, mock_popen): + mock_popen.side_effect = OSError + self.assertRaisesRegex(RuntimeError, "genisoimage", + configdrive.pack, "/fake") + + def test_genisoimage_fails(self, mock_popen): + mock_popen.return_value.communicate.return_value = "", "BOOM" + mock_popen.return_value.returncode = 1 + self.assertRaisesRegex(RuntimeError, "BOOM", + configdrive.pack, "/fake") + + def test_success(self, mock_popen): + mock_popen.return_value.communicate.return_value = "", "" + mock_popen.return_value.returncode = 0 + result = configdrive.pack("/fake") + # Make sure the result is string on all python versions + self.assertIsInstance(result, six.string_types) From 601769e8385bdda06c6102c0d565d672fd793e6f Mon Sep 17 00:00:00 2001 From: Jens Harbott Date: Tue, 22 Jan 2019 11:14:48 +0000 Subject: [PATCH 2347/3836] Update cirros version for functional tests Devstack has updated from 0.3.6 to 0.4.0, since we still hardcode things, we need to follow them. [0] https://review.openstack.org/521825 Change-Id: I5ed1bb70f6f14f79a31b7c98eedfbe2956044036 --- examples/connect.py | 2 +- openstack/tests/functional/base.py | 2 +- .../tests/functional/cloud/test_clustering.py | 38 +++++++++---------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/examples/connect.py b/examples/connect.py index b4e76cef2..d2730de9c 100644 --- a/examples/connect.py +++ b/examples/connect.py @@ -47,7 +47,7 @@ def _get_resource_value(resource_key, default): SERVER_NAME = 'openstacksdk-example' -IMAGE_NAME = _get_resource_value('image_name', 'cirros-0.3.6-x86_64-disk') +IMAGE_NAME = _get_resource_value('image_name', 'cirros-0.4.0-x86_64-disk') FLAVOR_NAME = _get_resource_value('flavor_name', 'm1.small') NETWORK_NAME = _get_resource_value('network_name', 'private') KEYPAIR_NAME = _get_resource_value('keypair_name', 'openstacksdk-example') diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index 115242615..603125f21 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -39,7 +39,7 @@ def _disable_keep_alive(conn): sess.keep_alive = False -IMAGE_NAME = _get_resource_value('image_name', 'cirros-0.3.6-x86_64-disk') +IMAGE_NAME = _get_resource_value('image_name', 'cirros-0.4.0-x86_64-disk') FLAVOR_NAME = _get_resource_value('flavor_name', 'm1.small') diff --git a/openstack/tests/functional/cloud/test_clustering.py b/openstack/tests/functional/cloud/test_clustering.py index 07c3a7444..9c3f775c2 100644 --- a/openstack/tests/functional/cloud/test_clustering.py +++ b/openstack/tests/functional/cloud/test_clustering.py @@ -116,7 +116,7 @@ def test_create_profile(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.6-x86_64-disk", + "image": "cirros-0.4.0-x86_64-disk", "networks": [ { "network": "private" @@ -146,7 +146,7 @@ def test_create_cluster(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.6-x86_64-disk", + "image": "cirros-0.4.0-x86_64-disk", "networks": [ { "network": "private" @@ -190,7 +190,7 @@ def test_get_cluster_by_id(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.6-x86_64-disk", + "image": "cirros-0.4.0-x86_64-disk", "networks": [ { "network": "private" @@ -233,7 +233,7 @@ def test_update_cluster(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.6-x86_64-disk", + "image": "cirros-0.4.0-x86_64-disk", "networks": [ { "network": "private" @@ -319,7 +319,7 @@ def test_attach_policy_to_cluster(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.6-x86_64-disk", + "image": "cirros-0.4.0-x86_64-disk", "networks": [ { "network": "private" @@ -393,7 +393,7 @@ def test_detach_policy_from_cluster(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.6-x86_64-disk", + "image": "cirros-0.4.0-x86_64-disk", "networks": [ { "network": "private" @@ -475,7 +475,7 @@ def test_get_policy_on_cluster_by_id(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.6-x86_64-disk", + "image": "cirros-0.4.0-x86_64-disk", "networks": [ { "network": "private" @@ -568,7 +568,7 @@ def test_list_policies_on_cluster(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.6-x86_64-disk", + "image": "cirros-0.4.0-x86_64-disk", "networks": [ { "network": "private" @@ -657,7 +657,7 @@ def test_create_cluster_receiver(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.6-x86_64-disk", + "image": "cirros-0.4.0-x86_64-disk", "networks": [ { "network": "private" @@ -714,7 +714,7 @@ def test_list_cluster_receivers(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.6-x86_64-disk", + "image": "cirros-0.4.0-x86_64-disk", "networks": [ { "network": "private" @@ -776,7 +776,7 @@ def test_delete_cluster(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.6-x86_64-disk", + "image": "cirros-0.4.0-x86_64-disk", "networks": [ { "network": "private" @@ -864,7 +864,7 @@ def test_list_clusters(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.6-x86_64-disk", + "image": "cirros-0.4.0-x86_64-disk", "networks": [ { "network": "private" @@ -915,7 +915,7 @@ def test_update_policy_on_cluster(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.6-x86_64-disk", + "image": "cirros-0.4.0-x86_64-disk", "networks": [ { "network": "private" @@ -1019,7 +1019,7 @@ def test_list_cluster_profiles(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.6-x86_64-disk", + "image": "cirros-0.4.0-x86_64-disk", "networks": [ { "network": "private" @@ -1057,7 +1057,7 @@ def test_get_cluster_profile_by_id(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.6-x86_64-disk", + "image": "cirros-0.4.0-x86_64-disk", "networks": [ { "network": "private" @@ -1095,7 +1095,7 @@ def test_update_cluster_profile(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.6-x86_64-disk", + "image": "cirros-0.4.0-x86_64-disk", "networks": [ { "network": "private" @@ -1131,7 +1131,7 @@ def test_delete_cluster_profile(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.6-x86_64-disk", + "image": "cirros-0.4.0-x86_64-disk", "networks": [ { "network": "private" @@ -1298,7 +1298,7 @@ def test_get_cluster_receiver_by_id(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.6-x86_64-disk", + "image": "cirros-0.4.0-x86_64-disk", "networks": [ { "network": "private" @@ -1357,7 +1357,7 @@ def test_update_cluster_receiver(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.3.6-x86_64-disk", + "image": "cirros-0.4.0-x86_64-disk", "networks": [ { "network": "private" From 52425850a2ae8eb2373482ad980373314d0bde64 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Tue, 22 Jan 2019 17:07:32 +0100 Subject: [PATCH 2348/3836] Add a non-voting job with metalsmith metalsmith is now exercising neutron, glance and ironic via openstacksdk, so it adds useful coverage. Change-Id: Iaf872c520d224366d48c34e31bae618acb502968 --- .zuul.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.zuul.yaml b/.zuul.yaml index 70b1dd055..03dfb665f 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -372,6 +372,12 @@ zuul_copy_output: '{{ devstack_base_dir }}/masakari-logs': logs +- job: + name: metalsmith-integration-openstacksdk-src + parent: metalsmith-integration-glance-netboot-cirros-direct + required-projects: + - openstack/openstacksdk + - project-template: name: openstacksdk-functional-tips check: @@ -415,6 +421,8 @@ voting: false - nodepool-functional-py35-src - bifrost-integration-tinyipa-ubuntu-xenial + - metalsmith-integration-openstacksdk-src: + voting: false gate: jobs: - openstacksdk-functional-devstack From 0f526df9596c15d22581c19e5e59d65572156caf Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 23 Jan 2019 16:28:36 +0100 Subject: [PATCH 2349/3836] use overriden base_path in remaining CRUD operations This is a follow-up of https://review.openstack.org/#/c/621153/ Change-Id: I0ec00864ccf9524f0cfea8d4b16b619c0fef4c59 --- openstack/compute/v2/keypair.py | 6 +++++- openstack/compute/v2/server.py | 3 ++- openstack/compute/v2/server_ip.py | 6 +++++- openstack/database/v1/user.py | 5 ++++- openstack/identity/v2/extension.py | 6 +++++- openstack/identity/version.py | 6 +++++- openstack/message/v2/message.py | 6 +++++- openstack/message/v2/queue.py | 6 +++++- openstack/message/v2/subscription.py | 9 +++++++-- 9 files changed, 43 insertions(+), 10 deletions(-) diff --git a/openstack/compute/v2/keypair.py b/openstack/compute/v2/keypair.py index 1db894f81..607aed774 100644 --- a/openstack/compute/v2/keypair.py +++ b/openstack/compute/v2/keypair.py @@ -52,7 +52,11 @@ def _consume_attrs(self, mapping, attrs): @classmethod def list(cls, session, paginated=False, base_path=None): - resp = session.get(cls.base_path, + + if base_path is None: + base_path = cls.base_path + + resp = session.get(base_path, headers={"Accept": "application/json"}) resp = resp.json() resp = resp[cls.resources_key] diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index a9db33f9a..32eeec53f 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -141,7 +141,8 @@ class Server(resource.Resource, metadata.MetadataMixin, resource.TagMixin): def _prepare_request(self, requires_id=True, prepend_key=True, base_path=None): request = super(Server, self)._prepare_request(requires_id=requires_id, - prepend_key=prepend_key) + prepend_key=prepend_key, + base_path=base_path) server_body = request.body[self.resource_key] diff --git a/openstack/compute/v2/server_ip.py b/openstack/compute/v2/server_ip.py index d3b9dd236..710437793 100644 --- a/openstack/compute/v2/server_ip.py +++ b/openstack/compute/v2/server_ip.py @@ -34,7 +34,11 @@ class ServerIP(resource.Resource): @classmethod def list(cls, session, paginated=False, server_id=None, network_label=None, base_path=None, **params): - url = cls.base_path % {"server_id": server_id} + + if base_path is None: + base_path = cls.base_path + + url = base_path % {"server_id": server_id} if network_label is not None: url = utils.urljoin(url, network_label) diff --git a/openstack/database/v1/user.py b/openstack/database/v1/user.py index a18f0b898..b9bd4fe14 100644 --- a/openstack/database/v1/user.py +++ b/openstack/database/v1/user.py @@ -43,7 +43,10 @@ def _prepare_request(self, requires_id=True, prepend_key=True, """ body = {self.resources_key: self._body.dirty} - uri = self.base_path % self._uri.attributes + if base_path is None: + base_path = self.base_path + + uri = base_path % self._uri.attributes uri = utils.urljoin(uri, self.id) return resource._Request(uri, body, None) diff --git a/openstack/identity/v2/extension.py b/openstack/identity/v2/extension.py index 5f95a499d..93f71ad95 100644 --- a/openstack/identity/v2/extension.py +++ b/openstack/identity/v2/extension.py @@ -44,7 +44,11 @@ class Extension(resource.Resource): @classmethod def list(cls, session, paginated=False, base_path=None, **params): - resp = session.get(cls.base_path, + + if base_path is None: + base_path = cls.base_path + + resp = session.get(base_path, params=params) resp = resp.json() for data in resp[cls.resources_key]['values']: diff --git a/openstack/identity/version.py b/openstack/identity/version.py index 710328055..ea1575f81 100644 --- a/openstack/identity/version.py +++ b/openstack/identity/version.py @@ -28,7 +28,11 @@ class Version(resource.Resource): @classmethod def list(cls, session, paginated=False, base_path=None, **params): - resp = session.get(cls.base_path, + + if base_path is None: + base_path = cls.base_path + + resp = session.get(base_path, params=params) resp = resp.json() for data in resp[cls.resources_key]['values']: diff --git a/openstack/message/v2/message.py b/openstack/message/v2/message.py index a8818ace8..8b8d59385 100644 --- a/openstack/message/v2/message.py +++ b/openstack/message/v2/message.py @@ -75,7 +75,11 @@ def list(cls, session, paginated=True, base_path=None, **params): and `X-PROJECT-ID` fields which are required by Zaqar v2 API. """ more_data = True - uri = cls.base_path % params + + if base_path is None: + base_path = cls.base_path + + uri = base_path % params headers = { "Client-ID": params.get('client_id', None) or str(uuid.uuid4()), "X-PROJECT-ID": params.get('project_id', None diff --git a/openstack/message/v2/queue.py b/openstack/message/v2/queue.py index 7b65bb881..7d580e486 100644 --- a/openstack/message/v2/queue.py +++ b/openstack/message/v2/queue.py @@ -75,7 +75,11 @@ def list(cls, session, paginated=False, base_path=None, **params): """ more_data = True query_params = cls._query_mapping._transpose(params) - uri = cls.base_path % params + + if base_path is None: + base_path = cls.base_path + + uri = base_path % params headers = { "Client-ID": params.get('client_id', None) or str(uuid.uuid4()), "X-PROJECT-ID": params.get('project_id', None diff --git a/openstack/message/v2/subscription.py b/openstack/message/v2/subscription.py index b3960c0fd..3c6322869 100644 --- a/openstack/message/v2/subscription.py +++ b/openstack/message/v2/subscription.py @@ -60,7 +60,8 @@ class Subscription(resource.Resource): def create(self, session, prepend_key=True, base_path=None): request = self._prepare_request(requires_id=False, - prepend_key=prepend_key) + prepend_key=prepend_key, + base_path=base_path) headers = { "Client-ID": self.client_id or str(uuid.uuid4()), "X-PROJECT-ID": self.project_id or session.get_project_id() @@ -81,7 +82,11 @@ def list(cls, session, paginated=True, base_path=None, **params): and `X-PROJECT-ID` fields which are required by Zaqar v2 API. """ more_data = True - uri = cls.base_path % params + + if base_path is None: + base_path = cls.base_path + + uri = base_path % params headers = { "Client-ID": params.get('client_id', None) or str(uuid.uuid4()), "X-PROJECT-ID": params.get('project_id', None From b72f3e1f8adcee8b5f1d7c4fa7544af40232bd16 Mon Sep 17 00:00:00 2001 From: Johannes Kulik Date: Wed, 23 Jan 2019 15:36:11 +0100 Subject: [PATCH 2350/3836] Support dict of links in pagination detection Some responses, e.g. by keystone (http://git.openstack.org/cgit/openstack/keystone/tree/api-ref/source/v3/samples/admin/groups-list-response.json?h=stable/rocky#n2) contain a dict in the `links` key. We convert such a dict to a list of dicts, because that's the format we expect. Change-Id: Ic211dc4ff6aed81e0993c177b5e63928bf92fe89 --- openstack/resource.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openstack/resource.py b/openstack/resource.py index 8ecc39e55..b0c673d94 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1369,6 +1369,9 @@ def _get_next_link(cls, uri, response, data, marker, limit, total_yielded): pagination_key = '{key}_links'.format(key=cls.resources_key) if pagination_key: links = data.get(pagination_key, {}) + # keystone might return a dict + if isinstance(links, dict): + links = ({k: v} for k, v in six.iteritems(links)) for item in links: if item.get('rel') == 'next' and 'href' in item: next_link = item['href'] From b668add5a9fab0c759a2f863ebf5a60c8c9ee168 Mon Sep 17 00:00:00 2001 From: James Denton Date: Wed, 7 Mar 2018 13:25:31 -0500 Subject: [PATCH 2351/3836] Adds prefixlen to the request body when creating subnets The python-openstack client uses the OpenStack SDK to interface with Neutron, which strips the prefix length from the client request when creating a subnet from a subnet pool. This patch adds support for prefixlen when creating a subnet. Partial-Bug: 1754062 Change-Id: I87762c55ea7b24eefbd91fc159cf57a5566ce5f4 --- openstack/cloud/openstackcloud.py | 6 +- openstack/network/v2/subnet.py | 2 + .../v2/test_subnet_from_subnet_pool.py | 80 +++++++++++++++++++ openstack/tests/unit/cloud/test_subnet.py | 52 ++++++++++++ 4 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 openstack/tests/functional/network/v2/test_subnet_from_subnet_pool.py diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 871ab6501..68c913d02 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -8185,7 +8185,7 @@ def create_subnet(self, network_name_or_id, cidr=None, ip_version=4, gateway_ip=None, disable_gateway_ip=False, dns_nameservers=None, host_routes=None, ipv6_ra_mode=None, ipv6_address_mode=None, - use_default_subnetpool=False, **kwargs): + prefixlen=None, use_default_subnetpool=False, **kwargs): """Create a subnet on a specified network. :param string network_name_or_id: @@ -8247,6 +8247,8 @@ def create_subnet(self, network_name_or_id, cidr=None, ip_version=4, :param string ipv6_address_mode: IPv6 address mode. Valid values are: 'dhcpv6-stateful', 'dhcpv6-stateless', or 'slaac'. + :param string prefixlen: + The prefix length to use for subnet allocation from a subnet pool. :param bool use_default_subnetpool: Use the default subnetpool for ``ip_version`` to obtain a CIDR. It is required to pass ``None`` to the ``cidr`` argument when enabling @@ -8317,6 +8319,8 @@ def create_subnet(self, network_name_or_id, cidr=None, ip_version=4, subnet['ipv6_ra_mode'] = ipv6_ra_mode if ipv6_address_mode: subnet['ipv6_address_mode'] = ipv6_address_mode + if prefixlen: + subnet['prefixlen'] = prefixlen if use_default_subnetpool: subnet['use_default_subnetpool'] = True diff --git a/openstack/network/v2/subnet.py b/openstack/network/v2/subnet.py index 64eca90ce..c7f2b8bce 100644 --- a/openstack/network/v2/subnet.py +++ b/openstack/network/v2/subnet.py @@ -69,6 +69,8 @@ class Subnet(resource.Resource, resource.TagMixin): name = resource.Body('name') #: The ID of the attached network. network_id = resource.Body('network_id') + #: The prefix length to use for subnet allocation from a subnet pool + prefix_length = resource.Body('prefixlen') #: The ID of the project this subnet is associated with. project_id = resource.Body('tenant_id') #: Revision number of the subnet. *Type: int* diff --git a/openstack/tests/functional/network/v2/test_subnet_from_subnet_pool.py b/openstack/tests/functional/network/v2/test_subnet_from_subnet_pool.py new file mode 100644 index 000000000..c03dce637 --- /dev/null +++ b/openstack/tests/functional/network/v2/test_subnet_from_subnet_pool.py @@ -0,0 +1,80 @@ +# 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.network.v2 import network +from openstack.network.v2 import subnet +from openstack.network.v2 import subnet_pool +from openstack.tests.functional import base + + +class TestSubnetFromSubnetPool(base.BaseFunctionalTest): + + IPV4 = 4 + CIDR = "10.100.0.0/28" + MINIMUM_PREFIX_LENGTH = 8 + DEFAULT_PREFIX_LENGTH = 24 + MAXIMUM_PREFIX_LENGTH = 32 + SUBNET_PREFIX_LENGTH = 28 + IP_VERSION = 4 + PREFIXES = ['10.100.0.0/24'] + NET_ID = None + SUB_ID = None + SUB_POOL_ID = None + + def setUp(self): + super(TestSubnetFromSubnetPool, self).setUp() + self.NET_NAME = self.getUniqueString() + self.SUB_NAME = self.getUniqueString() + self.SUB_POOL_NAME = self.getUniqueString() + + sub_pool = self.conn.network.create_subnet_pool( + name=self.SUB_POOL_NAME, + min_prefixlen=self.MINIMUM_PREFIX_LENGTH, + default_prefixlen=self.DEFAULT_PREFIX_LENGTH, + max_prefixlen=self.MAXIMUM_PREFIX_LENGTH, + prefixes=self.PREFIXES) + self.assertIsInstance(sub_pool, subnet_pool.SubnetPool) + self.assertEqual(self.SUB_POOL_NAME, sub_pool.name) + self.SUB_POOL_ID = sub_pool.id + net = self.conn.network.create_network(name=self.NET_NAME) + self.assertIsInstance(net, network.Network) + self.assertEqual(self.NET_NAME, net.name) + self.NET_ID = net.id + sub = self.conn.network.create_subnet( + name=self.SUB_NAME, + ip_version=self.IPV4, + network_id=self.NET_ID, + prefixlen=self.SUBNET_PREFIX_LENGTH, + subnetpool_id=self.SUB_POOL_ID) + self.assertIsInstance(sub, subnet.Subnet) + self.assertEqual(self.SUB_NAME, sub.name) + self.SUB_ID = sub.id + + def tearDown(self): + sot = self.conn.network.delete_subnet(self.SUB_ID) + self.assertIsNone(sot) + sot = self.conn.network.delete_network( + self.NET_ID, ignore_missing=False) + self.assertIsNone(sot) + sot = self.conn.network.delete_subnet_pool(self.SUB_POOL_ID) + self.assertIsNone(sot) + super(TestSubnetFromSubnetPool, self).tearDown() + + def test_get(self): + sot = self.conn.network.get_subnet(self.SUB_ID) + self.assertEqual(self.SUB_NAME, sot.name) + self.assertEqual(self.SUB_ID, sot.id) + self.assertEqual(self.CIDR, sot.cidr) + self.assertEqual(self.IPV4, sot.ip_version) + self.assertEqual("10.100.0.1", sot.gateway_ip) + self.assertTrue(sot.is_dhcp_enabled) diff --git a/openstack/tests/unit/cloud/test_subnet.py b/openstack/tests/unit/cloud/test_subnet.py index 2cafa056c..b811b28a7 100644 --- a/openstack/tests/unit/cloud/test_subnet.py +++ b/openstack/tests/unit/cloud/test_subnet.py @@ -26,6 +26,8 @@ class TestSubnet(base.TestCase): subnet_name = 'subnet_name' subnet_id = '1f1696eb-7f47-47f6-835c-4889bff88604' subnet_cidr = '192.168.199.0/24' + subnetpool_cidr = '172.16.0.0/28' + prefix_length = 28 mock_network_rep = { 'id': '881d1bb7-a663-44c0-8f9f-ee2765b74486', @@ -57,6 +59,13 @@ class TestSubnet(base.TestCase): 'tags': [] } + mock_subnetpool_rep = { + 'id': 'f49a1319-423a-4ee6-ba54-1d95a4f6cc68', + 'prefixes': [ + '172.16.0.0/16' + ] + } + def test_get_subnet(self): self.register_uris([ dict(method='GET', @@ -263,6 +272,49 @@ def test_create_subnet_non_unique_network(self): self.network_name, self.subnet_cidr) self.assert_calls() + def test_create_subnet_from_subnetpool_with_prefixlen(self): + pool = [{'start': '172.16.0.2', 'end': '172.16.0.15'}] + id = '143296eb-7f47-4755-835c-488123475604' + gateway = '172.16.0.1' + dns = ['8.8.8.8'] + routes = [{"destination": "0.0.0.0/0", "nexthop": "123.456.78.9"}] + mock_subnet_rep = copy.copy(self.mock_subnet_rep) + mock_subnet_rep['allocation_pools'] = pool + mock_subnet_rep['dns_nameservers'] = dns + mock_subnet_rep['host_routes'] = routes + mock_subnet_rep['gateway_ip'] = gateway + mock_subnet_rep['subnetpool_id'] = self.mock_subnetpool_rep['id'] + mock_subnet_rep['cidr'] = self.subnetpool_cidr + mock_subnet_rep['id'] = id + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks.json']), + json={'networks': [self.mock_network_rep]}), + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets.json']), + json={'subnet': mock_subnet_rep}, + validate=dict( + json={'subnet': { + 'enable_dhcp': False, + 'ip_version': 4, + 'network_id': self.mock_network_rep['id'], + 'allocation_pools': pool, + 'dns_nameservers': dns, + 'use_default_subnetpool': True, + 'prefixlen': self.prefix_length, + 'host_routes': routes}})) + ]) + subnet = self.cloud.create_subnet(self.network_name, + allocation_pools=pool, + dns_nameservers=dns, + use_default_subnetpool=True, + prefixlen=self.prefix_length, + host_routes=routes) + self.assertDictEqual(mock_subnet_rep, subnet) + self.assert_calls() + def test_delete_subnet(self): self.register_uris([ dict(method='GET', From c7bbaf32f3e450d233516f4a2e36077089be41a1 Mon Sep 17 00:00:00 2001 From: Bernard Cafarelli Date: Mon, 28 Jan 2019 11:05:22 +0100 Subject: [PATCH 2352/3836] Fixes for Unicode characters in python 2 requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit urljoin() util function uses str() which will trigger an exception on Unicode characters in python 2, use string type from six to support both versions Furthermore, HttpException would trigger an exception when logging an error, as __unicode__() may call super().__str__() with Unicode characters. Replace the call with similar code (returning the message) Sample command to trigger both errors in current code: openstack port delete does_not_exist™ Change-Id: Iedc47fa74e89f70e6f5e846f8a564bf1db40228a Story: 2004356 Task: 29066 --- openstack/exceptions.py | 2 +- openstack/tests/unit/test_exceptions.py | 10 ++++++++++ openstack/tests/unit/test_utils.py | 13 +++++++++++++ openstack/utils.py | 4 +++- 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/openstack/exceptions.py b/openstack/exceptions.py index fdbe11d2d..8fa9637d9 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -94,7 +94,7 @@ def __unicode__(self): # and we should use it. If it is 'Error', then we should construct a # better message from the information we do have. if not self.url or self.message == 'Error': - return super(HttpException, self).__str__() + return self.message if self.url: remote_error = "{source} Error for url: {url}".format( source=self.source, url=self.url) diff --git a/openstack/tests/unit/test_exceptions.py b/openstack/tests/unit/test_exceptions.py index fad45d145..1a95edecd 100644 --- a/openstack/tests/unit/test_exceptions.py +++ b/openstack/tests/unit/test_exceptions.py @@ -1,3 +1,5 @@ +# -*- 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 @@ -60,6 +62,14 @@ def test_http_status(self): self.assertEqual(self.message, exc.message) self.assertEqual(http_status, exc.status_code) + def test_unicode_message(self): + unicode_message = u"Event: No item found for does_not_exist©" + http_exception = exceptions.HttpException(message=unicode_message) + try: + http_exception.__unicode__() + except Exception: + self.fail("HttpException unicode message error") + class TestRaiseFromResponse(base.TestCase): diff --git a/openstack/tests/unit/test_utils.py b/openstack/tests/unit/test_utils.py index 42821906c..022923982 100644 --- a/openstack/tests/unit/test_utils.py +++ b/openstack/tests/unit/test_utils.py @@ -1,3 +1,5 @@ +# -*- 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 @@ -115,6 +117,17 @@ def test_with_none(self): result = utils.urljoin(root, *leaves) self.assertEqual(result, "http://www.example.com/foo/") + def test_unicode_strings(self): + root = "http://www.example.com" + leaves = u"ascii", u"extra_chars-™" + + try: + result = utils.urljoin(root, *leaves) + except Exception: + self.fail("urljoin failed on unicode strings") + + self.assertEqual(result, u"http://www.example.com/ascii/extra_chars-™") + class TestMaximumSupportedMicroversion(base.TestCase): def setUp(self): diff --git a/openstack/utils.py b/openstack/utils.py index a3c8c908d..efab5cce5 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -13,6 +13,8 @@ import string import time +import six + import keystoneauth1 from keystoneauth1 import discover @@ -27,7 +29,7 @@ def urljoin(*args): like /path this should be joined to http://host/path as it is an anchored link. We generally won't care about that in client. """ - return '/'.join(str(a or '').strip('/') for a in args) + return '/'.join(six.text_type(a or '').strip('/') for a in args) def iterate_timeout(timeout, message, wait=2): From 03f3847f74849ff272c2b33ca2575b38513d48b5 Mon Sep 17 00:00:00 2001 From: Ding Baojian Date: Tue, 15 Jan 2019 01:15:55 +0800 Subject: [PATCH 2353/3836] Fix raise create_server and attach to a network given a net-name param Task: 28888 Story: 2004769 Change-Id: I8e0e6a80445ace0e62c04c9b69156dcd350ba89e --- openstack/cloud/openstackcloud.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 871ab6501..52d7c1b62 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -6971,11 +6971,12 @@ def create_server( # If there's a net-id, ignore net-name nic.pop('net-name', None) elif 'net-name' in nic: - nic_net = self.get_network(nic['net-name']) + net_name = nic.pop('net-name') + nic_net = self.get_network(net_name) if not nic_net: raise exc.OpenStackCloudException( "Requested network {net} could not be found.".format( - net=nic['net-name'])) + net=net_name)) net['uuid'] = nic_net['id'] for ip_key in ('v4-fixed-ip', 'v6-fixed-ip', 'fixed_ip'): fixed_ip = nic.pop(ip_key, None) From 6482783ea9a39bc680a94cc47fa3a151044c4161 Mon Sep 17 00:00:00 2001 From: Johannes Kulik Date: Tue, 8 Jan 2019 08:33:13 +0100 Subject: [PATCH 2354/3836] Use pagination detection by default This cleans up all explicitly set ``paginated`` parameters, as they should not be needed anymore, since pagination detection is used automatically. The parameter ``paginated`` still stays available to have a means for disabling a service's misbehaving pagination implementations. Change-Id: I0f3b2cd58c5e856b12478667d07912f813b0f9da --- openstack/baremetal/v1/_proxy.py | 2 +- openstack/block_storage/v2/_proxy.py | 10 +- openstack/block_storage/v3/_proxy.py | 10 +- openstack/clustering/v1/_proxy.py | 28 ++--- openstack/compute/v2/_proxy.py | 26 ++--- openstack/database/v1/_proxy.py | 10 +- openstack/identity/v2/_proxy.py | 8 +- openstack/identity/v3/_proxy.py | 42 ++++---- openstack/image/v1/_proxy.py | 2 +- openstack/image/v2/_proxy.py | 5 +- openstack/instance_ha/v1/_proxy.py | 7 +- openstack/key_manager/v1/_proxy.py | 6 +- openstack/load_balancer/v2/_proxy.py | 18 ++-- openstack/message/v2/_proxy.py | 6 +- openstack/network/v2/_proxy.py | 102 ++++++++---------- openstack/orchestration/v1/_proxy.py | 10 +- openstack/proxy.py | 2 +- openstack/resource.py | 2 +- .../tests/unit/baremetal/v1/test_proxy.py | 2 +- .../tests/unit/block_storage/v2/test_proxy.py | 11 +- .../tests/unit/block_storage/v3/test_proxy.py | 11 +- .../tests/unit/clustering/v1/test_proxy.py | 20 +--- openstack/tests/unit/compute/v2/test_proxy.py | 31 ++---- .../tests/unit/database/v1/test_proxy.py | 10 +- .../tests/unit/identity/v2/test_proxy.py | 2 +- .../tests/unit/identity/v3/test_proxy.py | 25 ++--- openstack/tests/unit/image/v1/test_proxy.py | 2 +- openstack/tests/unit/image/v2/test_proxy.py | 4 +- .../tests/unit/key_manager/v1/test_proxy.py | 7 +- .../tests/unit/load_balancer/test_proxy.py | 19 ++-- openstack/tests/unit/message/v2/test_proxy.py | 6 +- openstack/tests/unit/network/v2/test_proxy.py | 95 ++++++---------- .../tests/unit/orchestration/v1/test_proxy.py | 11 +- openstack/tests/unit/test_proxy_base.py | 8 +- openstack/tests/unit/test_resource.py | 1 + openstack/tests/unit/workflow/test_proxy.py | 6 +- openstack/workflow/v2/_proxy.py | 4 +- 37 files changed, 230 insertions(+), 341 deletions(-) diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index ae10c54dd..7924c269b 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -143,7 +143,7 @@ def drivers(self, details=False): # 1.30. Thus we do not send any value if not needed. if details: kwargs['details'] = True - return self._list(_driver.Driver, paginated=False, **kwargs) + return self._list(_driver.Driver, **kwargs) def get_driver(self, driver): """Get a specific driver. diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index f981d0d92..8abb34cc1 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -55,7 +55,7 @@ def snapshots(self, details=True, **query): :returns: A generator of snapshot objects. """ snapshot = _snapshot.SnapshotDetail if details else _snapshot.Snapshot - return self._list(snapshot, paginated=True, **query) + return self._list(snapshot, **query) def create_snapshot(self, **attrs): """Create a new snapshot from attributes @@ -103,7 +103,7 @@ def types(self, **query): :returns: A generator of volume type objects. """ - return self._list(_type.Type, paginated=False, **query) + return self._list(_type.Type, **query) def create_type(self, **attrs): """Create a new type from attributes @@ -163,7 +163,7 @@ def volumes(self, details=True, **query): :returns: A generator of volume objects. """ volume = _volume.VolumeDetail if details else _volume.Volume - return self._list(volume, paginated=True, **query) + return self._list(volume, **query) def create_volume(self, **attrs): """Create a new volume from attributes @@ -209,7 +209,7 @@ def backend_pools(self): :returns A generator of cinder Back-end storage pools objects """ - return self._list(_stats.Pools, paginated=False) + return self._list(_stats.Pools) def backups(self, details=True, **query): """Retrieve a generator of backups @@ -240,7 +240,7 @@ def backups(self, details=True, **query): 'Object-store service is required for block-store backups' ) backup = _backup.BackupDetail if details else _backup.Backup - return self._list(backup, paginated=True, **query) + return self._list(backup, **query) def get_backup(self, backup): """Get a backup diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 62f82272f..29b9740e9 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -55,7 +55,7 @@ def snapshots(self, details=True, **query): :returns: A generator of snapshot objects. """ snapshot = _snapshot.SnapshotDetail if details else _snapshot.Snapshot - return self._list(snapshot, paginated=True, **query) + return self._list(snapshot, **query) def create_snapshot(self, **attrs): """Create a new snapshot from attributes @@ -103,7 +103,7 @@ def types(self, **query): :returns: A generator of volume type objects. """ - return self._list(_type.Type, paginated=False, **query) + return self._list(_type.Type, **query) def create_type(self, **attrs): """Create a new type from attributes @@ -163,7 +163,7 @@ def volumes(self, details=True, **query): :returns: A generator of volume objects. """ volume = _volume.VolumeDetail if details else _volume.Volume - return self._list(volume, paginated=True, **query) + return self._list(volume, **query) def create_volume(self, **attrs): """Create a new volume from attributes @@ -209,7 +209,7 @@ def backend_pools(self): :returns A generator of cinder Back-end storage pools objects """ - return self._list(_stats.Pools, paginated=False) + return self._list(_stats.Pools) def backups(self, details=True, **query): """Retrieve a generator of backups @@ -240,7 +240,7 @@ def backups(self, details=True, **query): 'Object-store service is required for block-store backups' ) backup = _backup.BackupDetail if details else _backup.Backup - return self._list(backup, paginated=True, **query) + return self._list(backup, **query) def get_backup(self, backup): """Get a backup diff --git a/openstack/clustering/v1/_proxy.py b/openstack/clustering/v1/_proxy.py index c6cc8db5f..10e7b66f2 100644 --- a/openstack/clustering/v1/_proxy.py +++ b/openstack/clustering/v1/_proxy.py @@ -42,7 +42,7 @@ def profile_types(self, **query): :returns: A generator of objects that are of type :class:`~openstack.clustering.v1.profile_type.ProfileType` """ - return self._list(_profile_type.ProfileType, paginated=False, **query) + return self._list(_profile_type.ProfileType, **query) def get_profile_type(self, profile_type): """Get the details about a profile type. @@ -63,7 +63,7 @@ def policy_types(self, **query): :returns: A generator of objects that are of type :class:`~openstack.clustering.v1.policy_type.PolicyType` """ - return self._list(_policy_type.PolicyType, paginated=False, **query) + return self._list(_policy_type.PolicyType, **query) def get_policy_type(self, policy_type): """Get the details about a policy type. @@ -156,7 +156,7 @@ def profiles(self, **query): :returns: A generator of profile instances. """ - return self._list(_profile.Profile, paginated=True, **query) + return self._list(_profile.Profile, **query) def update_profile(self, profile, **attrs): """Update a profile. @@ -267,7 +267,7 @@ def clusters(self, **query): :returns: A generator of cluster instances. """ - return self._list(_cluster.Cluster, paginated=True, **query) + return self._list(_cluster.Cluster, **query) def update_cluster(self, cluster, **attrs): """Update a cluster. @@ -429,8 +429,8 @@ def collect_cluster_attrs(self, cluster, path): :returns: A dictionary containing the list of attribute values. """ - return self._list(_cluster_attr.ClusterAttr, paginated=False, - cluster_id=cluster, path=path) + return self._list(_cluster_attr.ClusterAttr, cluster_id=cluster, + path=path) def check_cluster(self, cluster, **params): """Check a cluster. @@ -565,7 +565,7 @@ def nodes(self, **query): :returns: A generator of node instances. """ - return self._list(_node.Node, paginated=True, **query) + return self._list(_node.Node, **query) def update_node(self, node, **attrs): """Update a node. @@ -728,7 +728,7 @@ def policies(self, **query): :returns: A generator of policy instances. """ - return self._list(_policy.Policy, paginated=True, **query) + return self._list(_policy.Policy, **query) def update_policy(self, policy, **attrs): """Update a policy. @@ -768,8 +768,8 @@ def cluster_policies(self, cluster, **query): :returns: A generator of cluster-policy binding instances. """ cluster_id = resource.Resource._get_id(cluster) - return self._list(_cluster_policy.ClusterPolicy, paginated=False, - cluster_id=cluster_id, **query) + return self._list(_cluster_policy.ClusterPolicy, cluster_id=cluster_id, + **query) def get_cluster_policy(self, cluster_policy, cluster): """Get a cluster-policy binding. @@ -873,7 +873,7 @@ def receivers(self, **query): :returns: A generator of receiver instances. """ - return self._list(_receiver.Receiver, paginated=True, **query) + return self._list(_receiver.Receiver, **query) def get_action(self, action): """Get a single action. @@ -911,7 +911,7 @@ def actions(self, **query): :returns: A generator of action instances. """ - return self._list(_action.Action, paginated=True, **query) + return self._list(_action.Action, **query) def get_event(self, event): """Get a single event. @@ -954,7 +954,7 @@ def events(self, **query): :returns: A generator of event instances. """ - return self._list(_event.Event, paginated=True, **query) + return self._list(_event.Event, **query) def wait_for_status(self, res, status, failures=None, interval=2, wait=120): @@ -1003,7 +1003,7 @@ def services(self, **query): :returns: A generator of objects that are of type :class:`~openstack.clustering.v1.service.Service` """ - return self._list(_service.Service, paginated=False, **query) + return self._list(_service.Service, **query) def list_profile_type_operations(self, profile_type): """Get the operation about a profile type. diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 7d4a9fbab..f9eccb001 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -51,7 +51,7 @@ def extensions(self): :returns: A generator of extension instances. :rtype: :class:`~openstack.compute.v2.extension.Extension` """ - return self._list(extension.Extension, paginated=True) + return self._list(extension.Extension) def find_flavor(self, name_or_id, ignore_missing=True): """Find a single flavor @@ -119,7 +119,7 @@ def flavors(self, details=True, **query): :returns: A generator of flavor objects """ flv = _flavor.FlavorDetail if details else _flavor.Flavor - return self._list(flv, paginated=True, **query) + return self._list(flv, **query) def aggregates(self): """Return a generator of aggregate @@ -129,7 +129,7 @@ def aggregates(self): """ aggregate = _aggregate.Aggregate - return self._list(aggregate, paginated=False) + return self._list(aggregate) def get_aggregate(self, aggregate): """Get a single host aggregate @@ -282,7 +282,7 @@ def images(self, details=True, **query): :returns: A generator of image objects """ img = _image.ImageDetail if details else _image.Image - return self._list(img, paginated=True, **query) + return self._list(img, **query) def _get_base_resource(self, res, base): # Metadata calls for Image and Server can work for both those @@ -408,7 +408,7 @@ def keypairs(self): :returns: A generator of keypair objects :rtype: :class:`~openstack.compute.v2.keypair.Keypair` """ - return self._list(_keypair.Keypair, paginated=False) + return self._list(_keypair.Keypair) def get_limits(self): """Retrieve limits that are applied to the project's account @@ -521,7 +521,7 @@ def servers(self, details=True, all_projects=False, **query): if all_projects: query['all_projects'] = True srv = _server.ServerDetail if details else _server.Server - return self._list(srv, paginated=True, **query) + return self._list(srv, **query) def update_server(self, server, **attrs): """Update a server @@ -1033,7 +1033,7 @@ def server_interfaces(self, server): :rtype: :class:`~openstack.compute.v2.server_interface.ServerInterface` """ server_id = resource.Resource._get_id(server) - return self._list(_server_interface.ServerInterface, paginated=False, + return self._list(_server_interface.ServerInterface, server_id=server_id) def server_ips(self, server, network_label=None): @@ -1048,7 +1048,7 @@ def server_ips(self, server, network_label=None): :rtype: :class:`~openstack.compute.v2.server_ip.ServerIP` """ server_id = resource.Resource._get_id(server) - return self._list(server_ip.ServerIP, paginated=False, + return self._list(server_ip.ServerIP, server_id=server_id, network_label=network_label) def availability_zones(self, details=False): @@ -1067,7 +1067,7 @@ def availability_zones(self, details=False): else: az = availability_zone.AvailabilityZone - return self._list(az, paginated=False) + return self._list(az) def get_server_metadata(self, server): """Return a dictionary of metadata for a server @@ -1191,7 +1191,7 @@ def server_groups(self, **query): :returns: A generator of ServerGroup objects :rtype: :class:`~openstack.compute.v2.server_group.ServerGroup` """ - return self._list(_server_group.ServerGroup, paginated=False, **query) + return self._list(_server_group.ServerGroup, **query) def hypervisors(self, details=False): """Return a generator of hypervisor @@ -1209,7 +1209,7 @@ def hypervisors(self, details=False): else: hypervisor = _hypervisor.Hypervisor - return self._list(hypervisor, paginated=False) + return self._list(hypervisor) def find_hypervisor(self, name_or_id, ignore_missing=True): """Find a hypervisor from name or id to get the corresponding info @@ -1288,7 +1288,7 @@ def services(self): :rtype: class: `~openstack.compute.v2.service.Service` """ - return self._list(_service.Service, paginated=False) + return self._list(_service.Service) def create_volume_attachment(self, server, **attrs): """Create a new volume attachment from attributes @@ -1410,7 +1410,7 @@ def volume_attachments(self, server): :class:`~openstack.compute.v2.volume_attachment.VolumeAttachment` """ server_id = resource.Resource._get_id(server) - return self._list(_volume_attachment.VolumeAttachment, paginated=False, + return self._list(_volume_attachment.VolumeAttachment, server_id=server_id) def migrate_server(self, server): diff --git a/openstack/database/v1/_proxy.py b/openstack/database/v1/_proxy.py index a21804da6..f5684506a 100644 --- a/openstack/database/v1/_proxy.py +++ b/openstack/database/v1/_proxy.py @@ -88,8 +88,7 @@ def databases(self, instance, **query): :rtype: :class:`~openstack.database.v1.database.Database` """ instance = self._get_resource(_instance.Instance, instance) - return self._list(_database.Database, paginated=False, - instance_id=instance.id, **query) + return self._list(_database.Database, instance_id=instance.id, **query) def get_database(self, database, instance=None): """Get a single database @@ -143,7 +142,7 @@ def flavors(self, **query): :returns: A generator of flavor objects :rtype: :class:`~openstack.database.v1.flavor.Flavor` """ - return self._list(_flavor.Flavor, paginated=False, **query) + return self._list(_flavor.Flavor, **query) def create_instance(self, **attrs): """Create a new instance from attributes @@ -209,7 +208,7 @@ def instances(self, **query): :returns: A generator of instance objects :rtype: :class:`~openstack.database.v1.instance.Instance` """ - return self._list(_instance.Instance, paginated=False, **query) + return self._list(_instance.Instance, **query) def update_instance(self, instance, **attrs): """Update a instance @@ -290,8 +289,7 @@ def users(self, instance, **query): :rtype: :class:`~openstack.database.v1.user.User` """ instance = self._get_resource(_instance.Instance, instance) - return self._list(_user.User, instance_id=instance.id, - paginated=False, **query) + return self._list(_user.User, instance_id=instance.id, **query) def get_user(self, user, instance=None): """Get a single user diff --git a/openstack/identity/v2/_proxy.py b/openstack/identity/v2/_proxy.py index 97560c12e..f1097f4b3 100644 --- a/openstack/identity/v2/_proxy.py +++ b/openstack/identity/v2/_proxy.py @@ -25,7 +25,7 @@ def extensions(self): :returns: A generator of extension instances. :rtype: :class:`~openstack.identity.v2.extension.Extension` """ - return self._list(_extension.Extension, paginated=False) + return self._list(_extension.Extension) def get_extension(self, extension): """Get a single extension @@ -102,7 +102,7 @@ def roles(self, **query): :returns: A generator of role instances. :rtype: :class:`~openstack.identity.v2.role.Role` """ - return self._list(_role.Role, paginated=False, **query) + return self._list(_role.Role, **query) def update_role(self, role, **attrs): """Update a role @@ -179,7 +179,7 @@ def tenants(self, **query): :returns: A generator of tenant instances. :rtype: :class:`~openstack.identity.v2.tenant.Tenant` """ - return self._list(_tenant.Tenant, paginated=True, **query) + return self._list(_tenant.Tenant, **query) def update_tenant(self, tenant, **attrs): """Update a tenant @@ -256,7 +256,7 @@ def users(self, **query): :returns: A generator of user instances. :rtype: :class:`~openstack.identity.v2.user.User` """ - return self._list(_user.User, paginated=False, **query) + return self._list(_user.User, **query) def update_user(self, user, **attrs): """Update a user diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index f922ba587..39286cbbd 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -103,7 +103,7 @@ def credentials(self, **query): :rtype: :class:`~openstack.identity.v3.credential.Credential` """ # TODO(briancurtin): This is paginated but requires base list changes. - return self._list(_credential.Credential, paginated=False, **query) + return self._list(_credential.Credential, **query) def update_credential(self, credential, **attrs): """Update a credential @@ -181,7 +181,7 @@ def domains(self, **query): :rtype: :class:`~openstack.identity.v3.domain.Domain` """ # TODO(briancurtin): This is paginated but requires base list changes. - return self._list(_domain.Domain, paginated=False, **query) + return self._list(_domain.Domain, **query) def update_domain(self, domain, **attrs): """Update a domain @@ -261,7 +261,7 @@ def endpoints(self, **query): :rtype: :class:`~openstack.identity.v3.endpoint.Endpoint` """ # TODO(briancurtin): This is paginated but requires base list changes. - return self._list(_endpoint.Endpoint, paginated=False, **query) + return self._list(_endpoint.Endpoint, **query) def update_endpoint(self, endpoint, **attrs): """Update a endpoint @@ -341,7 +341,7 @@ def groups(self, **query): :rtype: :class:`~openstack.identity.v3.group.Group` """ # TODO(briancurtin): This is paginated but requires base list changes. - return self._list(_group.Group, paginated=False, **query) + return self._list(_group.Group, **query) def update_group(self, group, **attrs): """Update a group @@ -419,7 +419,7 @@ def policies(self, **query): :rtype: :class:`~openstack.identity.v3.policy.Policy` """ # TODO(briancurtin): This is paginated but requires base list changes. - return self._list(_policy.Policy, paginated=False, **query) + return self._list(_policy.Policy, **query) def update_policy(self, policy, **attrs): """Update a policy @@ -497,7 +497,7 @@ def projects(self, **query): :rtype: :class:`~openstack.identity.v3.project.Project` """ # TODO(briancurtin): This is paginated but requires base list changes. - return self._list(_project.Project, paginated=False, **query) + return self._list(_project.Project, **query) def user_projects(self, user, **query): """Retrieve a generator of projects to which the user has authorization @@ -512,8 +512,7 @@ def user_projects(self, user, **query): :rtype: :class:`~openstack.identity.v3.project.UserProject` """ user = self._get_resource(_user.User, user) - return self._list(_project.UserProject, paginated=True, - user_id=user.id, **query) + return self._list(_project.UserProject, user_id=user.id, **query) def update_project(self, project, **attrs): """Update a project @@ -591,7 +590,7 @@ def services(self, **query): :rtype: :class:`~openstack.identity.v3.service.Service` """ # TODO(briancurtin): This is paginated but requires base list changes. - return self._list(_service.Service, paginated=False, **query) + return self._list(_service.Service, **query) def update_service(self, service, **attrs): """Update a service @@ -669,7 +668,7 @@ def users(self, **query): :rtype: :class:`~openstack.identity.v3.user.User` """ # TODO(briancurtin): This is paginated but requires base list changes. - return self._list(_user.User, paginated=False, **query) + return self._list(_user.User, **query) def update_user(self, user, **attrs): """Update a user @@ -747,7 +746,7 @@ def trusts(self, **query): :rtype: :class:`~openstack.identity.v3.trust.Trust` """ # TODO(briancurtin): This is paginated but requires base list changes. - return self._list(_trust.Trust, paginated=False, **query) + return self._list(_trust.Trust, **query) def create_region(self, **attrs): """Create a new region from attributes @@ -812,7 +811,7 @@ def regions(self, **query): :rtype: :class:`~openstack.identity.v3.region.Region` """ # TODO(briancurtin): This is paginated but requires base list changes. - return self._list(_region.Region, paginated=False, **query) + return self._list(_region.Region, **query) def update_region(self, region, **attrs): """Update a region @@ -889,7 +888,7 @@ def roles(self, **query): :return: A generator of role instances. :rtype: :class:`~openstack.identity.v3.role.Role` """ - return self._list(_role.Role, paginated=False, **query) + return self._list(_role.Role, **query) def update_role(self, role, **attrs): """Update a role @@ -942,24 +941,24 @@ def role_assignments_filter(self, domain=None, project=None, group=None, group = self._get_resource(_group.Group, group) return self._list( _role_domain_group_assignment.RoleDomainGroupAssignment, - paginated=False, domain_id=domain.id, group_id=group.id) + domain_id=domain.id, group_id=group.id) else: user = self._get_resource(_user.User, user) return self._list( _role_domain_user_assignment.RoleDomainUserAssignment, - paginated=False, domain_id=domain.id, user_id=user.id) + domain_id=domain.id, user_id=user.id) else: project = self._get_resource(_project.Project, project) if group: group = self._get_resource(_group.Group, group) return self._list( _role_project_group_assignment.RoleProjectGroupAssignment, - paginated=False, project_id=project.id, group_id=group.id) + project_id=project.id, group_id=group.id) else: user = self._get_resource(_user.User, user) return self._list( _role_project_user_assignment.RoleProjectUserAssignment, - paginated=False, project_id=project.id, user_id=user.id) + project_id=project.id, user_id=user.id) def role_assignments(self, **query): """Retrieve a generator of role assignments @@ -972,8 +971,7 @@ def role_assignments(self, **query): :return: :class:`~openstack.identity.v3.role_assignment.RoleAssignment` """ - return self._list(_role_assignment.RoleAssignment, - paginated=False, **query) + return self._list(_role_assignment.RoleAssignment, **query) def registered_limits(self, **query): """Retrieve a generator of registered_limits @@ -985,8 +983,7 @@ def registered_limits(self, **query): :rtype: :class: `~openstack.identity.v3.registered_limit.RegisteredLimit` """ - return self._list(_registered_limit.RegisteredLimit, paginated=False, - **query) + return self._list(_registered_limit.RegisteredLimit, **query) def get_registered_limit(self, registered_limit): """Get a single registered_limit @@ -1058,8 +1055,7 @@ def limits(self, **query): :returns: A generator of limits instances. :rtype: :class:`~openstack.identity.v3.limit.Limit` """ - return self._list(_limit.Limit, paginated=False, - **query) + return self._list(_limit.Limit, **query) def get_limit(self, limit): """Get a single limit diff --git a/openstack/image/v1/_proxy.py b/openstack/image/v1/_proxy.py index 8e00ea851..c3b1d0234 100644 --- a/openstack/image/v1/_proxy.py +++ b/openstack/image/v1/_proxy.py @@ -78,7 +78,7 @@ def images(self, **query): :returns: A generator of image objects :rtype: :class:`~openstack.image.v1.image.Image` """ - return self._list(_image.Image, paginated=True, **query) + return self._list(_image.Image, **query) def update_image(self, image, **attrs): """Update a image diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 9968367c1..fb5ebb499 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -144,7 +144,7 @@ def images(self, **query): :returns: A generator of image objects :rtype: :class:`~openstack.image.v2.image.Image` """ - return self._list(_image.Image, paginated=True, **query) + return self._list(_image.Image, **query) def update_image(self, image, **attrs): """Update a image @@ -287,8 +287,7 @@ def members(self, image): :rtype: :class:`~openstack.image.v2.member.Member` """ image_id = resource.Resource._get_id(image) - return self._list(_member.Member, paginated=False, - image_id=image_id) + return self._list(_member.Member, image_id=image_id) def update_member(self, member, image, **attrs): """Update the member of an image diff --git a/openstack/instance_ha/v1/_proxy.py b/openstack/instance_ha/v1/_proxy.py index 2022ea980..28701e553 100644 --- a/openstack/instance_ha/v1/_proxy.py +++ b/openstack/instance_ha/v1/_proxy.py @@ -33,7 +33,7 @@ def notifications(self, **query): limit the notifications being returned. :returns: A generator of notifications """ - return self._list(_notification.Notification, paginated=False, **query) + return self._list(_notification.Notification, **query) def get_notification(self, notification): """Get a single notification. @@ -71,7 +71,7 @@ def segments(self, **query): limit the segments being returned. :returns: A generator of segments """ - return self._list(_segment.Segment, paginated=False, **query) + return self._list(_segment.Segment, **query) def get_segment(self, segment): """Get a single segment. @@ -137,8 +137,7 @@ def hosts(self, segment_id, **query): :returns: A generator of hosts """ - return self._list(_host.Host, segment_id=segment_id, paginated=False, - **query) + return self._list(_host.Host, segment_id=segment_id, **query) def create_host(self, segment_id, **attrs): """Create a new host. diff --git a/openstack/key_manager/v1/_proxy.py b/openstack/key_manager/v1/_proxy.py index 3945f4caa..222ad37be 100644 --- a/openstack/key_manager/v1/_proxy.py +++ b/openstack/key_manager/v1/_proxy.py @@ -84,7 +84,7 @@ def containers(self, **query): :returns: A generator of container objects :rtype: :class:`~openstack.key_manager.v1.container.Container` """ - return self._list(_container.Container, paginated=False, **query) + return self._list(_container.Container, **query) def update_container(self, container, **attrs): """Update a container @@ -164,7 +164,7 @@ def orders(self, **query): :returns: A generator of order objects :rtype: :class:`~openstack.key_manager.v1.order.Order` """ - return self._list(_order.Order, paginated=False, **query) + return self._list(_order.Order, **query) def update_order(self, order, **attrs): """Update a order @@ -245,7 +245,7 @@ def secrets(self, **query): :returns: A generator of secret objects :rtype: :class:`~openstack.key_manager.v1.secret.Secret` """ - return self._list(_secret.Secret, paginated=False, **query) + return self._list(_secret.Secret, **query) def update_secret(self, secret, **attrs): """Update a secret diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index 625edcb90..e8ea2c892 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -55,7 +55,7 @@ def load_balancers(self, **query): :returns: A generator of load balancer instances """ - return self._list(_lb.LoadBalancer, paginated=True, **query) + return self._list(_lb.LoadBalancer, **query) def delete_load_balancer(self, load_balancer, ignore_missing=True, cascade=False): @@ -180,7 +180,7 @@ def listeners(self, **query): :returns: A generator of listener objects :rtype: :class:`~openstack.load_balancer.v2.listener.Listener` """ - return self._list(_listener.Listener, paginated=True, **query) + return self._list(_listener.Listener, **query) def update_listener(self, listener, **attrs): """Update a listener @@ -227,7 +227,7 @@ def pools(self, **query): :returns: A generator of Pool instances """ - return self._list(_pool.Pool, paginated=True, **query) + return self._list(_pool.Pool, **query) def delete_pool(self, pool, ignore_missing=True): """Delete a pool @@ -364,8 +364,7 @@ def members(self, pool, **query): :rtype: :class:`~openstack.load_balancer.v2.member.Member` """ poolobj = self._get_resource(_pool.Pool, pool) - return self._list(_member.Member, paginated=True, - pool_id=poolobj.id, **query) + return self._list(_member.Member, pool_id=poolobj.id, **query) def update_member(self, member, pool, **attrs): """Update a member @@ -451,7 +450,7 @@ def health_monitors(self, **query): :returns: A generator of health monitor instances """ - return self._list(_hm.HealthMonitor, paginated=True, **query) + return self._list(_hm.HealthMonitor, **query) def delete_health_monitor(self, healthmonitor, ignore_missing=True): """Delete a health monitor @@ -554,7 +553,7 @@ def l7_policies(self, **query): :returns: A generator of l7policy objects :rtype: :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` """ - return self._list(_l7policy.L7Policy, paginated=True, **query) + return self._list(_l7policy.L7Policy, **query) def update_l7_policy(self, l7_policy, **attrs): """Update a l7policy @@ -660,8 +659,7 @@ def l7_rules(self, l7_policy, **query): :rtype: :class:`~openstack.load_balancer.v2.l7_rule.L7Rule` """ l7policyobj = self._get_resource(_l7policy.L7Policy, l7_policy) - return self._list(_l7rule.L7Rule, paginated=True, - l7policy_id=l7policyobj.id, **query) + return self._list(_l7rule.L7Rule, l7policy_id=l7policyobj.id, **query) def update_l7_rule(self, l7rule, l7_policy, **attrs): """Update a l7rule @@ -692,7 +690,7 @@ def quotas(self, **query): :returns: A generator of quota objects :rtype: :class:`~openstack.load_balancer.v2.quota.Quota` """ - return self._list(_quota.Quota, paginated=False, **query) + return self._list(_quota.Quota, **query) def get_quota(self, quota): """Get a quota diff --git a/openstack/message/v2/_proxy.py b/openstack/message/v2/_proxy.py index 1606235ce..365b73a85 100644 --- a/openstack/message/v2/_proxy.py +++ b/openstack/message/v2/_proxy.py @@ -59,7 +59,7 @@ def queues(self, **query): :returns: A generator of queue instances. """ - return self._list(_queue.Queue, paginated=True, **query) + return self._list(_queue.Queue, **query) def delete_queue(self, value, ignore_missing=True): """Delete a queue @@ -110,7 +110,7 @@ def messages(self, queue_name, **query): :returns: A generator of message instances. """ query["queue_name"] = queue_name - return self._list(_message.Message, paginated=True, **query) + return self._list(_message.Message, **query) def get_message(self, queue_name, message): """Get a message @@ -184,7 +184,7 @@ def subscriptions(self, queue_name, **query): :returns: A generator of subscription instances. """ query["queue_name"] = queue_name - return self._list(_subscription.Subscription, paginated=True, **query) + return self._list(_subscription.Subscription, **query) def get_subscription(self, queue_name, subscription): """Get a subscription diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index bc0966be8..9e0caab24 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -133,8 +133,7 @@ def address_scopes(self, **query): :returns: A generator of address scope objects :rtype: :class:`~openstack.network.v2.address_scope.AddressScope` """ - return self._list(_address_scope.AddressScope, paginated=False, - **query) + return self._list(_address_scope.AddressScope, **query) def update_address_scope(self, address_scope, **attrs): """Update an address scope @@ -169,7 +168,7 @@ def agents(self, **query): :returns: A generator of agents :rtype: :class:`~openstack.network.v2.agent.Agent` """ - return self._list(_agent.Agent, paginated=False, **query) + return self._list(_agent.Agent, **query) def delete_agent(self, agent, ignore_missing=True): """Delete a network agent @@ -222,7 +221,7 @@ def dhcp_agent_hosting_networks(self, agent, **query): :return: A generator of networks """ agent_obj = self._get_resource(_agent.Agent, agent) - return self._list(_network.DHCPAgentHostingNetwork, paginated=False, + return self._list(_network.DHCPAgentHostingNetwork, agent_id=agent_obj.id, **query) def add_dhcp_agent_to_network(self, agent, network): @@ -259,8 +258,8 @@ def network_hosting_dhcp_agents(self, network, **query): :return: A generator of hosted DHCP agents """ net = self._get_resource(_network.Network, network) - return self._list(_agent.NetworkHostingDHCPAgent, paginated=False, - network_id=net.id, **query) + return self._list(_agent.NetworkHostingDHCPAgent, network_id=net.id, + **query) def get_auto_allocated_topology(self, project=None): """Get the auto-allocated topology of a given tenant @@ -330,7 +329,7 @@ def availability_zones(self, **query): :rtype: :class:`~openstack.network.v2.availability_zone.AvailabilityZone` """ - return self._list(availability_zone.AvailabilityZone, paginated=False) + return self._list(availability_zone.AvailabilityZone) def find_extension(self, name_or_id, ignore_missing=True, **args): """Find a single extension @@ -359,7 +358,7 @@ def extensions(self, **query): :returns: A generator of extension objects :rtype: :class:`~openstack.network.v2.extension.Extension` """ - return self._list(extension.Extension, paginated=False, **query) + return self._list(extension.Extension, **query) def create_flavor(self, **attrs): """Create a new network service flavor from attributes @@ -447,7 +446,7 @@ def flavors(self, **query): :returns: A generator of flavor objects :rtype: :class:`~openstack.network.v2.flavor.Flavor` """ - return self._list(_flavor.Flavor, paginated=True, **query) + return self._list(_flavor.Flavor, **query) def associate_flavor_with_service_profile(self, flavor, service_profile): """Associate network flavor with service profile. @@ -576,7 +575,7 @@ def ips(self, **query): :returns: A generator of floating IP objects :rtype: :class:`~openstack.network.v2.floating_ip.FloatingIP` """ - return self._list(_floating_ip.FloatingIP, paginated=False, **query) + return self._list(_floating_ip.FloatingIP, **query) def update_ip(self, floating_ip, **attrs): """Update a ip @@ -687,7 +686,7 @@ def port_forwardings(self, floating_ip, **query): :rtype: :class:`~openstack.network.v2.port_forwarding.PortForwarding` """ fip = self._get_resource(_floating_ip.FloatingIP, floating_ip) - return self._list(_port_forwarding.PortForwarding, paginated=False, + return self._list(_port_forwarding.PortForwarding, floatingip_id=fip.id, **query) def update_port_forwarding(self, port_forwarding, floating_ip, **attrs): @@ -796,8 +795,7 @@ def health_monitors(self, **query): :returns: A generator of health monitor objects :rtype: :class:`~openstack.network.v2.health_monitor.HealthMonitor` """ - return self._list(_health_monitor.HealthMonitor, paginated=False, - **query) + return self._list(_health_monitor.HealthMonitor, **query) def update_health_monitor(self, health_monitor, **attrs): """Update a health monitor @@ -892,7 +890,7 @@ def listeners(self, **query): :returns: A generator of listener objects :rtype: :class:`~openstack.network.v2.listener.Listener` """ - return self._list(_listener.Listener, paginated=False, **query) + return self._list(_listener.Listener, **query) def update_listener(self, listener, **attrs): """Update a listener @@ -976,8 +974,7 @@ def load_balancers(self, **query): :returns: A generator of load balancer objects :rtype: :class:`~openstack.network.v2.load_balancer.LoadBalancer` """ - return self._list(_load_balancer.LoadBalancer, paginated=False, - **query) + return self._list(_load_balancer.LoadBalancer, **query) def update_load_balancer(self, load_balancer, **attrs): """Update a load balancer @@ -1071,8 +1068,7 @@ def metering_labels(self, **query): :returns: A generator of metering label objects :rtype: :class:`~openstack.network.v2.metering_label.MeteringLabel` """ - return self._list(_metering_label.MeteringLabel, paginated=False, - **query) + return self._list(_metering_label.MeteringLabel, **query) def update_metering_label(self, metering_label, **attrs): """Update a metering label @@ -1176,8 +1172,7 @@ def metering_label_rules(self, **query): :rtype: :class:`~openstack.network.v2.metering_label_rule. MeteringLabelRule` """ - return self._list(_metering_label_rule.MeteringLabelRule, - paginated=False, **query) + return self._list(_metering_label_rule.MeteringLabelRule, **query) def update_metering_label_rule(self, metering_label_rule, **attrs): """Update a metering label rule @@ -1279,7 +1274,7 @@ def networks(self, **query): :returns: A generator of network objects :rtype: :class:`~openstack.network.v2.network.Network` """ - return self._list(_network.Network, paginated=False, **query) + return self._list(_network.Network, **query) def update_network(self, network, **attrs): """Update a network @@ -1345,7 +1340,7 @@ def network_ip_availabilities(self, **query): NetworkIPAvailability` """ return self._list(network_ip_availability.NetworkIPAvailability, - paginated=False, **query) + **query) def create_network_segment_range(self, **attrs): """Create a new network segment range from attributes @@ -1444,8 +1439,7 @@ def network_segment_ranges(self, **query): :rtype: :class:`~openstack.network.v2._network_segment_range. NetworkSegmentRange` """ - return self._list(_network_segment_range.NetworkSegmentRange, - paginated=False, **query) + return self._list(_network_segment_range.NetworkSegmentRange, **query) def update_network_segment_range(self, network_segment_range, **attrs): """Update a network segment range @@ -1542,7 +1536,7 @@ def pools(self, **query): :returns: A generator of pool objects :rtype: :class:`~openstack.network.v2.pool.Pool` """ - return self._list(_pool.Pool, paginated=False, **query) + return self._list(_pool.Pool, **query) def update_pool(self, pool, **attrs): """Update a pool @@ -1660,8 +1654,7 @@ def pool_members(self, pool, **query): :rtype: :class:`~openstack.network.v2.pool_member.PoolMember` """ poolobj = self._get_resource(_pool.Pool, pool) - return self._list(_pool_member.PoolMember, paginated=False, - pool_id=poolobj.id, **query) + return self._list(_pool_member.PoolMember, pool_id=poolobj.id, **query) def update_pool_member(self, pool_member, pool, **attrs): """Update a pool member @@ -1759,7 +1752,7 @@ def ports(self, **query): :returns: A generator of port objects :rtype: :class:`~openstack.network.v2.port.Port` """ - return self._list(_port.Port, paginated=False, **query) + return self._list(_port.Port, **query) def update_port(self, port, **attrs): """Update a port @@ -1891,7 +1884,7 @@ def qos_bandwidth_limit_rules(self, qos_policy, **query): """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._list(_qos_bandwidth_limit_rule.QoSBandwidthLimitRule, - paginated=False, qos_policy_id=policy.id, **query) + qos_policy_id=policy.id, **query) def update_qos_bandwidth_limit_rule(self, qos_rule, qos_policy, **attrs): @@ -2014,7 +2007,7 @@ def qos_dscp_marking_rules(self, qos_policy, **query): """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._list(_qos_dscp_marking_rule.QoSDSCPMarkingRule, - paginated=False, qos_policy_id=policy.id, **query) + qos_policy_id=policy.id, **query) def update_qos_dscp_marking_rule(self, qos_rule, qos_policy, **attrs): """Update a QoS DSCP marking rule @@ -2137,7 +2130,7 @@ def qos_minimum_bandwidth_rules(self, qos_policy, **query): """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._list(_qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, - paginated=False, qos_policy_id=policy.id, **query) + qos_policy_id=policy.id, **query) def update_qos_minimum_bandwidth_rule(self, qos_rule, qos_policy, **attrs): @@ -2236,7 +2229,7 @@ def qos_policies(self, **query): :returns: A generator of QoS policy objects :rtype: :class:`~openstack.network.v2.qos_policy.QoSPolicy` """ - return self._list(_qos_policy.QoSPolicy, paginated=False, **query) + return self._list(_qos_policy.QoSPolicy, **query) def update_qos_policy(self, qos_policy, **attrs): """Update a QoS policy @@ -2293,7 +2286,7 @@ def qos_rule_types(self, **query): :returns: A generator of QoS rule type objects :rtype: :class:`~openstack.network.v2.qos_rule_type.QoSRuleType` """ - return self._list(_qos_rule_type.QoSRuleType, paginated=False, **query) + return self._list(_qos_rule_type.QoSRuleType, **query) def delete_quota(self, quota, ignore_missing=True): """Delete a quota (i.e. reset to the default quota) @@ -2360,7 +2353,7 @@ def quotas(self, **query): :returns: A generator of quota objects :rtype: :class:`~openstack.network.v2.quota.Quota` """ - return self._list(_quota.Quota, paginated=False, **query) + return self._list(_quota.Quota, **query) def update_quota(self, quota, **attrs): """Update a quota @@ -2450,7 +2443,7 @@ def rbac_policies(self, **query): :returns: A generator of rbac objects :rtype: :class:`~openstack.network.v2.rbac_policy.RBACPolicy` """ - return self._list(_rbac_policy.RBACPolicy, paginated=False, **query) + return self._list(_rbac_policy.RBACPolicy, **query) def update_rbac_policy(self, rbac_policy, **attrs): """Update a RBAC policy @@ -2539,7 +2532,7 @@ def routers(self, **query): :returns: A generator of router objects :rtype: :class:`~openstack.network.v2.router.Router` """ - return self._list(_router.Router, paginated=False, **query) + return self._list(_router.Router, **query) def update_router(self, router, **attrs): """Update a router @@ -2628,8 +2621,7 @@ def routers_hosting_l3_agents(self, router, **query): :rtype: :class:`~openstack.network.v2.router.RouterL3Agents` """ router = self._get_resource(_router.Router, router) - return self._list(_agent.RouterL3Agent, paginated=False, - router_id=router.id, **query) + return self._list(_agent.RouterL3Agent, router_id=router.id, **query) def agent_hosted_routers(self, agent, **query): """Return a generator of routers hosted by a L3 agent @@ -2643,8 +2635,7 @@ def agent_hosted_routers(self, agent, **query): :rtype: :class:`~openstack.network.v2.agent.L3AgentRouters` """ agent = self._get_resource(_agent.Agent, agent) - return self._list(_router.L3AgentRouter, paginated=False, - agent_id=agent.id, **query) + return self._list(_router.L3AgentRouter, agent_id=agent.id, **query) def add_router_to_agent(self, agent, router): """Add router to L3 agent @@ -2755,8 +2746,7 @@ def firewall_groups(self, **query): :returns: A generator of firewall group objects """ - return self._list(_firewall_group.FirewallGroup, - paginated=False, **query) + return self._list(_firewall_group.FirewallGroup, **query) def update_firewall_group(self, firewall_group, **attrs): """Update a firewall group @@ -2851,8 +2841,7 @@ def firewall_policies(self, **query): :returns: A generator of firewall policy objects """ - return self._list(_firewall_policy.FirewallPolicy, - paginated=False, **query) + return self._list(_firewall_policy.FirewallPolicy, **query) def update_firewall_policy(self, firewall_policy, **attrs): """Update a firewall policy @@ -2994,8 +2983,7 @@ def firewall_rules(self, **query): :returns: A generator of firewall rule objects """ - return self._list(_firewall_rule.FirewallRule, - paginated=False, **query) + return self._list(_firewall_rule.FirewallRule, **query) def update_firewall_rule(self, firewall_rule, **attrs): """Update a firewall rule @@ -3087,8 +3075,7 @@ def security_groups(self, **query): :returns: A generator of security group objects :rtype: :class:`~openstack.network.v2.security_group.SecurityGroup` """ - return self._list(_security_group.SecurityGroup, paginated=False, - **query) + return self._list(_security_group.SecurityGroup, **query) def update_security_group(self, security_group, **attrs): """Update a security group @@ -3192,8 +3179,7 @@ def security_group_rules(self, **query): :rtype: :class:`~openstack.network.v2.security_group_rule. SecurityGroupRule` """ - return self._list(_security_group_rule.SecurityGroupRule, - paginated=False, **query) + return self._list(_security_group_rule.SecurityGroupRule, **query) def create_segment(self, **attrs): """Create a new segment from attributes @@ -3268,7 +3254,7 @@ def segments(self, **query): :returns: A generator of segment objects :rtype: :class:`~openstack.network.v2.segment.Segment` """ - return self._list(_segment.Segment, paginated=False, **query) + return self._list(_segment.Segment, **query) def update_segment(self, segment, **attrs): """Update a segment @@ -3294,8 +3280,7 @@ def service_providers(self, **query): :rtype: :class:`~openstack.network.v2.service_provider.ServiceProvider` """ - return self._list(_service_provider.ServiceProvider, - paginated=False, **query) + return self._list(_service_provider.ServiceProvider, **query) def create_service_profile(self, **attrs): """Create a new network service flavor profile from attributes @@ -3374,8 +3359,7 @@ def service_profiles(self, **query): :returns: A generator of service profile objects :rtype: :class:`~openstack.network.v2.service_profile.ServiceProfile` """ - return self._list(_service_profile.ServiceProfile, paginated=True, - **query) + return self._list(_service_profile.ServiceProfile, **query) def update_service_profile(self, service_profile, **attrs): """Update a network flavor service profile @@ -3469,7 +3453,7 @@ def subnets(self, **query): :returns: A generator of subnet objects :rtype: :class:`~openstack.network.v2.subnet.Subnet` """ - return self._list(_subnet.Subnet, paginated=False, **query) + return self._list(_subnet.Subnet, **query) def update_subnet(self, subnet, **attrs): """Update a subnet @@ -3558,7 +3542,7 @@ def subnet_pools(self, **query): :returns: A generator of subnet pool objects :rtype: :class:`~openstack.network.v2.subnet_pool.SubnetPool` """ - return self._list(_subnet_pool.SubnetPool, paginated=False, **query) + return self._list(_subnet_pool.SubnetPool, **query) def update_subnet_pool(self, subnet_pool, **attrs): """Update a subnet pool @@ -3658,7 +3642,7 @@ def trunks(self, **query): :returns: A generator of trunk objects :rtype: :class:`~openstack.network.v2.trunk.trunk` """ - return self._list(_trunk.Trunk, paginated=False, **query) + return self._list(_trunk.Trunk, **query) def update_trunk(self, trunk, **attrs): """Update a trunk @@ -3782,7 +3766,7 @@ def vpn_services(self, **query): :returns: A generator of vpn service objects :rtype: :class:`~openstack.network.v2.vpn_service.VPNService` """ - return self._list(_vpn_service.VPNService, paginated=False, **query) + return self._list(_vpn_service.VPNService, **query) def update_vpn_service(self, vpn_service, **attrs): """Update a vpn service diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index 237f78bce..02d725524 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -64,7 +64,7 @@ def stacks(self, **query): :returns: A generator of stack objects :rtype: :class:`~openstack.orchestration.v1.stack.Stack` """ - return self._list(_stack.Stack, paginated=False, **query) + return self._list(_stack.Stack, **query) def get_stack(self, stack): """Get a single stack @@ -218,8 +218,8 @@ def resources(self, stack, **query): else: obj = self._find(_stack.Stack, stack, ignore_missing=False) - return self._list(_resource.Resource, paginated=False, - stack_name=obj.name, stack_id=obj.id, **query) + return self._list(_resource.Resource, stack_name=obj.name, + stack_id=obj.id, **query) def create_software_config(self, **attrs): """Create a new software config from attributes @@ -243,7 +243,7 @@ def software_configs(self, **query): :rtype: :class:`~openstack.orchestration.v1.software_config.\ SoftwareConfig` """ - return self._list(_sc.SoftwareConfig, paginated=True, **query) + return self._list(_sc.SoftwareConfig, **query) def get_software_config(self, software_config): """Get details about a specific software config. @@ -295,7 +295,7 @@ def software_deployments(self, **query): :rtype: :class:`~openstack.orchestration.v1.software_deployment.\ SoftwareDeployment` """ - return self._list(_sd.SoftwareDeployment, paginated=False, **query) + return self._list(_sd.SoftwareDeployment, **query) def get_software_deployment(self, software_deployment): """Get details about a specific software deployment resource diff --git a/openstack/proxy.py b/openstack/proxy.py index c003473d6..a3e7364a8 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -254,7 +254,7 @@ def _get(self, resource_type, value=None, requires_id=True, resource_type=resource_type.__name__, value=value)) def _list(self, resource_type, value=None, - paginated=False, base_path=None, **attrs): + paginated=True, base_path=None, **attrs): """List a resource :param resource_type: The type of resource to delete. This should diff --git a/openstack/resource.py b/openstack/resource.py index c54fff6ba..a3f55fb95 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1254,7 +1254,7 @@ def delete(self, session, error_message=None): return self @classmethod - def list(cls, session, paginated=False, base_path=None, **params): + def list(cls, session, paginated=True, base_path=None, **params): """This method is a generator which yields resource objects. This resource object list generator handles pagination and takes query diff --git a/openstack/tests/unit/baremetal/v1/test_proxy.py b/openstack/tests/unit/baremetal/v1/test_proxy.py index 45a685f74..0ab402785 100644 --- a/openstack/tests/unit/baremetal/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal/v1/test_proxy.py @@ -30,7 +30,7 @@ def setUp(self): self.proxy = _proxy.Proxy(self.session) def test_drivers(self): - self.verify_list(self.proxy.drivers, driver.Driver, paginated=False) + self.verify_list(self.proxy.drivers, driver.Driver) def test_get_driver(self): self.verify_get(self.proxy.get_driver, driver.Driver) diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index a9af7afea..1e0a7d061 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -32,13 +32,11 @@ def test_snapshot_get(self): def test_snapshots_detailed(self): self.verify_list(self.proxy.snapshots, snapshot.SnapshotDetail, - paginated=True, method_kwargs={"details": True, "query": 1}, expected_kwargs={"query": 1}) def test_snapshots_not_detailed(self): self.verify_list(self.proxy.snapshots, snapshot.Snapshot, - paginated=True, method_kwargs={"details": False, "query": 1}, expected_kwargs={"query": 1}) @@ -57,7 +55,7 @@ def test_type_get(self): self.verify_get(self.proxy.get_type, type.Type) def test_types(self): - self.verify_list(self.proxy.types, type.Type, paginated=False) + self.verify_list(self.proxy.types, type.Type) def test_type_create_attrs(self): self.verify_create(self.proxy.create_type, type.Type) @@ -73,13 +71,11 @@ def test_volume_get(self): def test_volumes_detailed(self): self.verify_list(self.proxy.volumes, volume.VolumeDetail, - paginated=True, method_kwargs={"details": True, "query": 1}, expected_kwargs={"query": 1}) def test_volumes_not_detailed(self): self.verify_list(self.proxy.volumes, volume.Volume, - paginated=True, method_kwargs={"details": False, "query": 1}, expected_kwargs={"query": 1}) @@ -99,15 +95,13 @@ def test_volume_extend(self): expected_args=["new-size"]) def test_backend_pools(self): - self.verify_list(self.proxy.backend_pools, stats.Pools, - paginated=False) + self.verify_list(self.proxy.backend_pools, stats.Pools) def test_backups_detailed(self): # NOTE: mock has_service self.proxy._connection = mock.Mock() self.proxy._connection.has_service = mock.Mock(return_value=True) self.verify_list(self.proxy.backups, backup.BackupDetail, - paginated=True, method_kwargs={"details": True, "query": 1}, expected_kwargs={"query": 1}) @@ -116,7 +110,6 @@ def test_backups_not_detailed(self): self.proxy._connection = mock.Mock() self.proxy._connection.has_service = mock.Mock(return_value=True) self.verify_list(self.proxy.backups, backup.Backup, - paginated=True, method_kwargs={"details": False, "query": 1}, expected_kwargs={"query": 1}) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 528203054..f0057f7de 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -32,13 +32,11 @@ def test_snapshot_get(self): def test_snapshots_detailed(self): self.verify_list(self.proxy.snapshots, snapshot.SnapshotDetail, - paginated=True, method_kwargs={"details": True, "query": 1}, expected_kwargs={"query": 1}) def test_snapshots_not_detailed(self): self.verify_list(self.proxy.snapshots, snapshot.Snapshot, - paginated=True, method_kwargs={"details": False, "query": 1}, expected_kwargs={"query": 1}) @@ -57,7 +55,7 @@ def test_type_get(self): self.verify_get(self.proxy.get_type, type.Type) def test_types(self): - self.verify_list(self.proxy.types, type.Type, paginated=False) + self.verify_list(self.proxy.types, type.Type) def test_type_create_attrs(self): self.verify_create(self.proxy.create_type, type.Type) @@ -73,13 +71,11 @@ def test_volume_get(self): def test_volumes_detailed(self): self.verify_list(self.proxy.volumes, volume.VolumeDetail, - paginated=True, method_kwargs={"details": True, "query": 1}, expected_kwargs={"query": 1}) def test_volumes_not_detailed(self): self.verify_list(self.proxy.volumes, volume.Volume, - paginated=True, method_kwargs={"details": False, "query": 1}, expected_kwargs={"query": 1}) @@ -99,15 +95,13 @@ def test_volume_extend(self): expected_args=["new-size"]) def test_backend_pools(self): - self.verify_list(self.proxy.backend_pools, stats.Pools, - paginated=False) + self.verify_list(self.proxy.backend_pools, stats.Pools) def test_backups_detailed(self): # NOTE: mock has_service self.proxy._connection = mock.Mock() self.proxy._connection.has_service = mock.Mock(return_value=True) self.verify_list(self.proxy.backups, backup.BackupDetail, - paginated=True, method_kwargs={"details": True, "query": 1}, expected_kwargs={"query": 1}) @@ -116,7 +110,6 @@ def test_backups_not_detailed(self): self.proxy._connection = mock.Mock() self.proxy._connection.has_service = mock.Mock(return_value=True) self.verify_list(self.proxy.backups, backup.Backup, - paginated=True, method_kwargs={"details": False, "query": 1}, expected_kwargs={"query": 1}) diff --git a/openstack/tests/unit/clustering/v1/test_proxy.py b/openstack/tests/unit/clustering/v1/test_proxy.py index 8095b1156..c42e02e02 100644 --- a/openstack/tests/unit/clustering/v1/test_proxy.py +++ b/openstack/tests/unit/clustering/v1/test_proxy.py @@ -42,16 +42,14 @@ def test_build_info_get(self): def test_profile_types(self): self.verify_list(self.proxy.profile_types, - profile_type.ProfileType, - paginated=False) + profile_type.ProfileType) def test_profile_type_get(self): self.verify_get(self.proxy.get_profile_type, profile_type.ProfileType) def test_policy_types(self): - self.verify_list(self.proxy.policy_types, policy_type.PolicyType, - paginated=False) + self.verify_list(self.proxy.policy_types, policy_type.PolicyType) def test_policy_type_get(self): self.verify_get(self.proxy.get_policy_type, policy_type.PolicyType) @@ -77,7 +75,6 @@ def test_profile_get(self): def test_profiles(self): self.verify_list(self.proxy.profiles, profile.Profile, - paginated=True, method_kwargs={'limit': 2}, expected_kwargs={'limit': 2}) @@ -106,7 +103,6 @@ def test_cluster_get(self): def test_clusters(self): self.verify_list(self.proxy.clusters, cluster.Cluster, - paginated=True, method_kwargs={'limit': 2}, expected_kwargs={'limit': 2}) @@ -115,8 +111,7 @@ def test_cluster_update(self): def test_services(self): self.verify_list(self.proxy.services, - service.Service, - paginated=False) + service.Service) @mock.patch.object(proxy_base.Proxy, '_find') def test_resize_cluster(self, mock_find): @@ -140,7 +135,7 @@ def test_resize_cluster_with_obj(self): def test_collect_cluster_attrs(self): self.verify_list(self.proxy.collect_cluster_attrs, - cluster_attr.ClusterAttr, paginated=False, + cluster_attr.ClusterAttr, method_args=['FAKE_ID', 'path.to.attr'], expected_kwargs={'cluster_id': 'FAKE_ID', 'path': 'path.to.attr'}) @@ -194,7 +189,6 @@ def test_node_get_with_details(self): def test_nodes(self): self.verify_list(self.proxy.nodes, node.Node, - paginated=True, method_kwargs={'limit': 2}, expected_kwargs={'limit': 2}) @@ -261,7 +255,6 @@ def test_policy_get(self): def test_policies(self): self.verify_list(self.proxy.policies, policy.Policy, - paginated=True, method_kwargs={'limit': 2}, expected_kwargs={'limit': 2}) @@ -271,7 +264,7 @@ def test_policy_update(self): def test_cluster_policies(self): self.verify_list(self.proxy.cluster_policies, cluster_policy.ClusterPolicy, - paginated=False, method_args=["FAKE_CLUSTER"], + method_args=["FAKE_CLUSTER"], expected_kwargs={"cluster_id": "FAKE_CLUSTER"}) def test_get_cluster_policy(self): @@ -324,7 +317,6 @@ def test_receiver_get(self): def test_receivers(self): self.verify_list(self.proxy.receivers, receiver.Receiver, - paginated=True, method_kwargs={'limit': 2}, expected_kwargs={'limit': 2}) @@ -333,7 +325,6 @@ def test_action_get(self): def test_actions(self): self.verify_list(self.proxy.actions, action.Action, - paginated=True, method_kwargs={'limit': 2}, expected_kwargs={'limit': 2}) @@ -342,7 +333,6 @@ def test_event_get(self): def test_events(self): self.verify_list(self.proxy.events, event.Event, - paginated=True, method_kwargs={'limit': 2}, expected_kwargs={'limit': 2}) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 8c41f956d..6b0cac5be 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -35,8 +35,7 @@ def test_extension_find(self): self.verify_find(self.proxy.find_extension, extension.Extension) def test_extensions(self): - self.verify_list_no_kwargs(self.proxy.extensions, extension.Extension, - paginated=True) + self.verify_list_no_kwargs(self.proxy.extensions, extension.Extension) def test_flavor_create(self): self.verify_create(self.proxy.create_flavor, flavor.Flavor) @@ -55,13 +54,11 @@ def test_flavor_get(self): def test_flavors_detailed(self): self.verify_list(self.proxy.flavors, flavor.FlavorDetail, - paginated=True, method_kwargs={"details": True, "query": 1}, expected_kwargs={"query": 1}) def test_flavors_not_detailed(self): self.verify_list(self.proxy.flavors, flavor.Flavor, - paginated=True, method_kwargs={"details": False, "query": 1}, expected_kwargs={"query": 1}) @@ -79,13 +76,11 @@ def test_image_get(self): def test_images_detailed(self): self.verify_list(self.proxy.images, image.ImageDetail, - paginated=True, method_kwargs={"details": True, "query": 1}, expected_kwargs={"query": 1}) def test_images_not_detailed(self): self.verify_list(self.proxy.images, image.Image, - paginated=True, method_kwargs={"details": False, "query": 1}, expected_kwargs={"query": 1}) @@ -105,8 +100,7 @@ def test_keypair_get(self): self.verify_get(self.proxy.get_keypair, keypair.Keypair) def test_keypairs(self): - self.verify_list_no_kwargs(self.proxy.keypairs, keypair.Keypair, - paginated=False) + self.verify_list_no_kwargs(self.proxy.keypairs, keypair.Keypair) def test_limits_get(self): self.verify_get(self.proxy.get_limits, limits.Limits, value=[]) @@ -183,19 +177,19 @@ def test_server_interface_get(self): def test_server_interfaces(self): self.verify_list(self.proxy.server_interfaces, server_interface.ServerInterface, - paginated=False, method_args=["test_id"], + method_args=["test_id"], expected_kwargs={"server_id": "test_id"}) def test_server_ips_with_network_label(self): self.verify_list(self.proxy.server_ips, server_ip.ServerIP, - paginated=False, method_args=["test_id"], + method_args=["test_id"], method_kwargs={"network_label": "test_label"}, expected_kwargs={"server_id": "test_id", "network_label": "test_label"}) def test_server_ips_without_network_label(self): self.verify_list(self.proxy.server_ips, server_ip.ServerIP, - paginated=False, method_args=["test_id"], + method_args=["test_id"], expected_kwargs={"server_id": "test_id", "network_label": None}) @@ -221,18 +215,15 @@ def test_server_get(self): def test_servers_detailed(self): self.verify_list(self.proxy.servers, server.ServerDetail, - paginated=True, method_kwargs={"details": True, "changes_since": 1, "image": 2}, expected_kwargs={"changes_since": 1, "image": 2}) def test_servers_not_detailed(self): self.verify_list(self.proxy.servers, server.Server, - paginated=True, method_kwargs={"details": False, "changes_since": 1, "image": 2}, - expected_kwargs={"paginated": True, - "changes_since": 1, "image": 2}) + expected_kwargs={"changes_since": 1, "image": 2}) def test_server_update(self): self.verify_update(self.proxy.update_server, server.Server) @@ -421,13 +412,11 @@ def test_get_server_output(self): def test_availability_zones_not_detailed(self): self.verify_list(self.proxy.availability_zones, az.AvailabilityZone, - paginated=False, method_kwargs={"details": False}) def test_availability_zones_detailed(self): self.verify_list(self.proxy.availability_zones, az.AvailabilityZoneDetail, - paginated=False, method_kwargs={"details": True}) def test_get_all_server_metadata(self): @@ -479,17 +468,14 @@ def test_server_group_get(self): server_group.ServerGroup) def test_server_groups(self): - self.verify_list(self.proxy.server_groups, server_group.ServerGroup, - paginated=False) + self.verify_list(self.proxy.server_groups, server_group.ServerGroup) def test_hypervisors_not_detailed(self): self.verify_list(self.proxy.hypervisors, hypervisor.Hypervisor, - paginated=False, method_kwargs={"details": False}) def test_hypervisors_detailed(self): self.verify_list(self.proxy.hypervisors, hypervisor.HypervisorDetail, - paginated=False, method_kwargs={"details": True}) def test_find_hypervisor(self): @@ -502,8 +488,7 @@ def test_get_hypervisor(self): def test_services(self): self.verify_list_no_kwargs(self.proxy.services, - service.Service, - paginated=False) + service.Service) def test_enable_service(self): self._verify('openstack.compute.v2.service.Service.enable', diff --git a/openstack/tests/unit/database/v1/test_proxy.py b/openstack/tests/unit/database/v1/test_proxy.py index bbb51a9cb..5fa4776fc 100644 --- a/openstack/tests/unit/database/v1/test_proxy.py +++ b/openstack/tests/unit/database/v1/test_proxy.py @@ -50,7 +50,7 @@ def test_database_find(self): def test_databases(self): self.verify_list(self.proxy.databases, database.Database, - paginated=False, method_args=["id"], + method_args=["id"], expected_kwargs={"instance_id": "id"}) def test_database_get(self): @@ -63,8 +63,7 @@ def test_flavor_get(self): self.verify_get(self.proxy.get_flavor, flavor.Flavor) def test_flavors(self): - self.verify_list(self.proxy.flavors, flavor.Flavor, - paginated=False) + self.verify_list(self.proxy.flavors, flavor.Flavor) def test_instance_create_attrs(self): self.verify_create(self.proxy.create_instance, instance.Instance) @@ -84,8 +83,7 @@ def test_instance_get(self): self.verify_get(self.proxy.get_instance, instance.Instance) def test_instances(self): - self.verify_list(self.proxy.instances, instance.Instance, - paginated=False) + self.verify_list(self.proxy.instances, instance.Instance) def test_instance_update(self): self.verify_update(self.proxy.update_instance, instance.Instance) @@ -114,7 +112,7 @@ def test_user_find(self): "ignore_missing": True}) def test_users(self): - self.verify_list(self.proxy.users, user.User, paginated=False, + self.verify_list(self.proxy.users, user.User, method_args=["test_instance"], expected_kwargs={"instance_id": "test_instance"}) diff --git a/openstack/tests/unit/identity/v2/test_proxy.py b/openstack/tests/unit/identity/v2/test_proxy.py index 6e8ba830a..0a08fd641 100644 --- a/openstack/tests/unit/identity/v2/test_proxy.py +++ b/openstack/tests/unit/identity/v2/test_proxy.py @@ -59,7 +59,7 @@ def test_tenant_get(self): self.verify_get(self.proxy.get_tenant, tenant.Tenant) def test_tenants(self): - self.verify_list(self.proxy.tenants, tenant.Tenant, paginated=True) + self.verify_list(self.proxy.tenants, tenant.Tenant) def test_tenant_update(self): self.verify_update(self.proxy.update_tenant, tenant.Tenant) diff --git a/openstack/tests/unit/identity/v3/test_proxy.py b/openstack/tests/unit/identity/v3/test_proxy.py index 028b322e1..57a49a4aa 100644 --- a/openstack/tests/unit/identity/v3/test_proxy.py +++ b/openstack/tests/unit/identity/v3/test_proxy.py @@ -53,8 +53,7 @@ def test_credential_get(self): self.verify_get(self.proxy.get_credential, credential.Credential) def test_credentials(self): - self.verify_list(self.proxy.credentials, credential.Credential, - paginated=False) + self.verify_list(self.proxy.credentials, credential.Credential) def test_credential_update(self): self.verify_update(self.proxy.update_credential, credential.Credential) @@ -75,7 +74,7 @@ def test_domain_get(self): self.verify_get(self.proxy.get_domain, domain.Domain) def test_domains(self): - self.verify_list(self.proxy.domains, domain.Domain, paginated=False) + self.verify_list(self.proxy.domains, domain.Domain) def test_domain_update(self): self.verify_update(self.proxy.update_domain, domain.Domain) @@ -98,8 +97,7 @@ def test_endpoint_get(self): self.verify_get(self.proxy.get_endpoint, endpoint.Endpoint) def test_endpoints(self): - self.verify_list(self.proxy.endpoints, endpoint.Endpoint, - paginated=False) + self.verify_list(self.proxy.endpoints, endpoint.Endpoint) def test_endpoint_update(self): self.verify_update(self.proxy.update_endpoint, endpoint.Endpoint) @@ -120,7 +118,7 @@ def test_group_get(self): self.verify_get(self.proxy.get_group, group.Group) def test_groups(self): - self.verify_list(self.proxy.groups, group.Group, paginated=False) + self.verify_list(self.proxy.groups, group.Group) def test_group_update(self): self.verify_update(self.proxy.update_group, group.Group) @@ -141,7 +139,7 @@ def test_policy_get(self): self.verify_get(self.proxy.get_policy, policy.Policy) def test_policies(self): - self.verify_list(self.proxy.policies, policy.Policy, paginated=False) + self.verify_list(self.proxy.policies, policy.Policy) def test_policy_update(self): self.verify_update(self.proxy.update_policy, policy.Policy) @@ -162,13 +160,12 @@ def test_project_get(self): self.verify_get(self.proxy.get_project, project.Project) def test_projects(self): - self.verify_list(self.proxy.projects, project.Project, paginated=False) + self.verify_list(self.proxy.projects, project.Project) def test_user_projects(self): self.verify_list( self.proxy.user_projects, project.UserProject, - paginated=True, method_kwargs={'user': USER_ID}, expected_kwargs={'user_id': USER_ID} ) @@ -192,7 +189,7 @@ def test_service_get(self): self.verify_get(self.proxy.get_service, service.Service) def test_services(self): - self.verify_list(self.proxy.services, service.Service, paginated=False) + self.verify_list(self.proxy.services, service.Service) def test_service_update(self): self.verify_update(self.proxy.update_service, service.Service) @@ -213,7 +210,7 @@ def test_user_get(self): self.verify_get(self.proxy.get_user, user.User) def test_users(self): - self.verify_list(self.proxy.users, user.User, paginated=False) + self.verify_list(self.proxy.users, user.User) def test_user_update(self): self.verify_update(self.proxy.update_user, user.User) @@ -234,7 +231,7 @@ def test_trust_get(self): self.verify_get(self.proxy.get_trust, trust.Trust) def test_trusts(self): - self.verify_list(self.proxy.trusts, trust.Trust, paginated=False) + self.verify_list(self.proxy.trusts, trust.Trust) def test_region_create_attrs(self): self.verify_create(self.proxy.create_region, region.Region) @@ -252,7 +249,7 @@ def test_region_get(self): self.verify_get(self.proxy.get_region, region.Region) def test_regions(self): - self.verify_list(self.proxy.regions, region.Region, paginated=False) + self.verify_list(self.proxy.regions, region.Region) def test_region_update(self): self.verify_update(self.proxy.update_region, region.Region) @@ -273,7 +270,7 @@ def test_role_get(self): self.verify_get(self.proxy.get_role, role.Role) def test_roles(self): - self.verify_list(self.proxy.roles, role.Role, paginated=False) + self.verify_list(self.proxy.roles, role.Role) def test_role_update(self): self.verify_update(self.proxy.update_role, role.Role) diff --git a/openstack/tests/unit/image/v1/test_proxy.py b/openstack/tests/unit/image/v1/test_proxy.py index 741770028..c5cc5419c 100644 --- a/openstack/tests/unit/image/v1/test_proxy.py +++ b/openstack/tests/unit/image/v1/test_proxy.py @@ -36,7 +36,7 @@ def test_image_get(self): self.verify_get(self.proxy.get_image, image.Image) def test_images(self): - self.verify_list(self.proxy.images, image.Image, paginated=True) + self.verify_list(self.proxy.images, image.Image) def test_image_update(self): self.verify_update(self.proxy.update_image, image.Image) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 14cff8271..e0dc10954 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -74,7 +74,7 @@ def test_image_get(self): self.verify_get(self.proxy.get_image, image.Image) def test_images(self): - self.verify_list(self.proxy.images, image.Image, paginated=True) + self.verify_list(self.proxy.images, image.Image) def test_add_tag(self): self._verify("openstack.image.v2.image.Image.add_tag", @@ -151,6 +151,6 @@ def test_member_find(self): 'image_id': 'image_id'}) def test_members(self): - self.verify_list(self.proxy.members, member.Member, paginated=False, + self.verify_list(self.proxy.members, member.Member, method_args=('image_1',), expected_kwargs={'image_id': 'image_1'}) diff --git a/openstack/tests/unit/key_manager/v1/test_proxy.py b/openstack/tests/unit/key_manager/v1/test_proxy.py index 400e7150a..e9ad2b35f 100644 --- a/openstack/tests/unit/key_manager/v1/test_proxy.py +++ b/openstack/tests/unit/key_manager/v1/test_proxy.py @@ -40,8 +40,7 @@ def test_container_get(self): self.verify_get(self.proxy.get_container, container.Container) def test_containers(self): - self.verify_list(self.proxy.containers, container.Container, - paginated=False) + self.verify_list(self.proxy.containers, container.Container) def test_container_update(self): self.verify_update(self.proxy.update_container, container.Container) @@ -62,7 +61,7 @@ def test_order_get(self): self.verify_get(self.proxy.get_order, order.Order) def test_orders(self): - self.verify_list(self.proxy.orders, order.Order, paginated=False) + self.verify_list(self.proxy.orders, order.Order) def test_order_update(self): self.verify_update(self.proxy.update_order, order.Order) @@ -86,7 +85,7 @@ def test_secret_get(self): 'openstack.key_manager.v1.secret.Secret') def test_secrets(self): - self.verify_list(self.proxy.secrets, secret.Secret, paginated=False) + self.verify_list(self.proxy.secrets, secret.Secret) def test_secret_update(self): self.verify_update(self.proxy.update_secret, secret.Secret) diff --git a/openstack/tests/unit/load_balancer/test_proxy.py b/openstack/tests/unit/load_balancer/test_proxy.py index 143f691d2..94a3c07a3 100644 --- a/openstack/tests/unit/load_balancer/test_proxy.py +++ b/openstack/tests/unit/load_balancer/test_proxy.py @@ -37,8 +37,7 @@ def setUp(self): def test_load_balancers(self): self.verify_list(self.proxy.load_balancers, - lb.LoadBalancer, - paginated=True) + lb.LoadBalancer) def test_load_balancer_get(self): self.verify_get(self.proxy.get_load_balancer, @@ -90,8 +89,7 @@ def test_load_balancer_update(self): def test_listeners(self): self.verify_list(self.proxy.listeners, - listener.Listener, - paginated=True) + listener.Listener) def test_listener_get(self): self.verify_get(self.proxy.get_listener, @@ -115,8 +113,7 @@ def test_listener_update(self): def test_pools(self): self.verify_list(self.proxy.pools, - pool.Pool, - paginated=True) + pool.Pool) def test_pool_get(self): self.verify_get(self.proxy.get_pool, @@ -141,7 +138,6 @@ def test_pool_update(self): def test_members(self): self.verify_list(self.proxy.members, member.Member, - paginated=True, method_kwargs={'pool': self.POOL_ID}, expected_kwargs={'pool_id': self.POOL_ID}) @@ -181,8 +177,7 @@ def test_member_update(self): def test_health_monitors(self): self.verify_list(self.proxy.health_monitors, - health_monitor.HealthMonitor, - paginated=True) + health_monitor.HealthMonitor) def test_health_monitor_get(self): self.verify_get(self.proxy.get_health_monitor, @@ -206,8 +201,7 @@ def test_health_monitor_update(self): def test_l7_policies(self): self.verify_list(self.proxy.l7_policies, - l7_policy.L7Policy, - paginated=True) + l7_policy.L7Policy) def test_l7_policy_get(self): self.verify_get(self.proxy.get_l7_policy, @@ -232,7 +226,6 @@ def test_l7_policy_update(self): def test_l7_rules(self): self.verify_list(self.proxy.l7_rules, l7_rule.L7Rule, - paginated=True, method_kwargs={'l7_policy': self.L7_POLICY_ID}, expected_kwargs={'l7policy_id': self.L7_POLICY_ID}) @@ -271,7 +264,7 @@ def test_l7_rule_update(self): expected_kwargs={"l7policy_id": self.L7_POLICY_ID}) def test_quotas(self): - self.verify_list(self.proxy.quotas, quota.Quota, paginated=False) + self.verify_list(self.proxy.quotas, quota.Quota) def test_quota_get(self): self.verify_get(self.proxy.get_quota, quota.Quota) diff --git a/openstack/tests/unit/message/v2/test_proxy.py b/openstack/tests/unit/message/v2/test_proxy.py index f9ea86f71..fea9f9c84 100644 --- a/openstack/tests/unit/message/v2/test_proxy.py +++ b/openstack/tests/unit/message/v2/test_proxy.py @@ -38,7 +38,7 @@ def test_queue_get(self): 'openstack.message.v2.queue.Queue') def test_queues(self): - self.verify_list(self.proxy.queues, queue.Queue, paginated=True) + self.verify_list(self.proxy.queues, queue.Queue) def test_queue_delete(self): self.verify_delete(self.proxy.delete_queue, queue.Queue, False) @@ -73,7 +73,7 @@ def test_message_get(self, mock_get_resource): def test_messages(self): self.verify_list(self.proxy.messages, message.Message, - paginated=True, method_args=["test_queue"], + method_args=["test_queue"], expected_kwargs={"queue_name": "test_queue"}) @mock.patch.object(proxy_base.Proxy, '_get_resource') @@ -150,7 +150,7 @@ def test_subscription_get(self, mock_get_resource): def test_subscriptions(self): self.verify_list(self.proxy.subscriptions, subscription.Subscription, - paginated=True, method_args=["test_queue"], + method_args=["test_queue"], expected_kwargs={"queue_name": "test_queue"}) @mock.patch.object(proxy_base.Proxy, '_get_resource') diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 2a7d98be2..446e843c6 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -92,8 +92,7 @@ def test_address_scope_get(self): def test_address_scopes(self): self.verify_list(self.proxy.address_scopes, - address_scope.AddressScope, - paginated=False) + address_scope.AddressScope) def test_address_scope_update(self): self.verify_update(self.proxy.update_address_scope, @@ -106,22 +105,19 @@ def test_agent_get(self): self.verify_get(self.proxy.get_agent, agent.Agent) def test_agents(self): - self.verify_list(self.proxy.agents, agent.Agent, - paginated=False) + self.verify_list(self.proxy.agents, agent.Agent) def test_agent_update(self): self.verify_update(self.proxy.update_agent, agent.Agent) def test_availability_zones(self): self.verify_list_no_kwargs(self.proxy.availability_zones, - availability_zone.AvailabilityZone, - paginated=False) + availability_zone.AvailabilityZone) def test_dhcp_agent_hosting_networks(self): self.verify_list( self.proxy.dhcp_agent_hosting_networks, network.DHCPAgentHostingNetwork, - paginated=False, method_kwargs={'agent': AGENT_ID}, expected_kwargs={'agent_id': AGENT_ID} ) @@ -130,7 +126,6 @@ def test_network_hosting_dhcp_agents(self): self.verify_list( self.proxy.network_hosting_dhcp_agents, agent.NetworkHostingDHCPAgent, - paginated=False, method_kwargs={'network': NETWORK_ID}, expected_kwargs={'network_id': NETWORK_ID} ) @@ -139,8 +134,7 @@ def test_extension_find(self): self.verify_find(self.proxy.find_extension, extension.Extension) def test_extensions(self): - self.verify_list(self.proxy.extensions, extension.Extension, - paginated=False) + self.verify_list(self.proxy.extensions, extension.Extension) def test_floating_ip_create_attrs(self): self.verify_create(self.proxy.create_ip, floating_ip.FloatingIP) @@ -160,8 +154,7 @@ def test_floating_ip_get(self): self.verify_get(self.proxy.get_ip, floating_ip.FloatingIP) def test_ips(self): - self.verify_list(self.proxy.ips, floating_ip.FloatingIP, - paginated=False) + self.verify_list(self.proxy.ips, floating_ip.FloatingIP) def test_floating_ip_update(self): self.verify_update(self.proxy.update_ip, floating_ip.FloatingIP) @@ -188,8 +181,7 @@ def test_health_monitor_get(self): def test_health_monitors(self): self.verify_list(self.proxy.health_monitors, - health_monitor.HealthMonitor, - paginated=False) + health_monitor.HealthMonitor) def test_health_monitor_update(self): self.verify_update(self.proxy.update_health_monitor, @@ -213,8 +205,7 @@ def test_listener_get(self): self.verify_get(self.proxy.get_listener, listener.Listener) def test_listeners(self): - self.verify_list(self.proxy.listeners, listener.Listener, - paginated=False) + self.verify_list(self.proxy.listeners, listener.Listener) def test_listener_update(self): self.verify_update(self.proxy.update_listener, listener.Listener) @@ -241,8 +232,7 @@ def test_load_balancer_get(self): def test_load_balancers(self): self.verify_list(self.proxy.load_balancers, - load_balancer.LoadBalancer, - paginated=False) + load_balancer.LoadBalancer) def test_load_balancer_update(self): self.verify_update(self.proxy.update_load_balancer, @@ -270,8 +260,7 @@ def test_metering_label_get(self): def test_metering_labels(self): self.verify_list(self.proxy.metering_labels, - metering_label.MeteringLabel, - paginated=False) + metering_label.MeteringLabel) def test_metering_label_update(self): self.verify_update(self.proxy.update_metering_label, @@ -299,8 +288,7 @@ def test_metering_label_rule_get(self): def test_metering_label_rules(self): self.verify_list(self.proxy.metering_label_rules, - metering_label_rule.MeteringLabelRule, - paginated=False) + metering_label_rule.MeteringLabelRule) def test_metering_label_rule_update(self): self.verify_update(self.proxy.update_metering_label_rule, @@ -331,8 +319,7 @@ def test_network_get(self): self.verify_get(self.proxy.get_network, network.Network) def test_networks(self): - self.verify_list(self.proxy.networks, network.Network, - paginated=False) + self.verify_list(self.proxy.networks, network.Network) def test_network_update(self): self.verify_update(self.proxy.update_network, network.Network) @@ -353,8 +340,7 @@ def test_flavor_update(self): self.verify_update(self.proxy.update_flavor, flavor.Flavor) def test_flavors(self): - self.verify_list(self.proxy.flavors, flavor.Flavor, - paginated=True) + self.verify_list(self.proxy.flavors, flavor.Flavor) def test_service_profile_create_attrs(self): self.verify_create(self.proxy.create_service_profile, @@ -374,7 +360,7 @@ def test_service_profile_get(self): def test_service_profiles(self): self.verify_list(self.proxy.service_profiles, - service_profile.ServiceProfile, paginated=True) + service_profile.ServiceProfile) def test_service_profile_update(self): self.verify_update(self.proxy.update_service_profile, @@ -425,7 +411,7 @@ def test_pool_member_get(self): def test_pool_members(self): self.verify_list(self.proxy.pool_members, pool_member.PoolMember, - paginated=False, method_args=["test_id"], + method_args=["test_id"], expected_kwargs={"pool_id": "test_id"}) def test_pool_member_update(self): @@ -451,7 +437,7 @@ def test_pool_get(self): self.verify_get(self.proxy.get_pool, pool.Pool) def test_pools(self): - self.verify_list(self.proxy.pools, pool.Pool, paginated=False) + self.verify_list(self.proxy.pools, pool.Pool) def test_pool_update(self): self.verify_update(self.proxy.update_pool, pool.Pool) @@ -472,7 +458,7 @@ def test_port_get(self): self.verify_get(self.proxy.get_port, port.Port) def test_ports(self): - self.verify_list(self.proxy.ports, port.Port, paginated=False) + self.verify_list(self.proxy.ports, port.Port) def test_port_update(self): self.verify_update(self.proxy.update_port, port.Port) @@ -520,7 +506,6 @@ def test_qos_bandwidth_limit_rules(self): self.verify_list( self.proxy.qos_bandwidth_limit_rules, qos_bandwidth_limit_rule.QoSBandwidthLimitRule, - paginated=False, method_kwargs={'qos_policy': QOS_POLICY_ID}, expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) @@ -578,7 +563,6 @@ def test_qos_dscp_marking_rules(self): self.verify_list( self.proxy.qos_dscp_marking_rules, qos_dscp_marking_rule.QoSDSCPMarkingRule, - paginated=False, method_kwargs={'qos_policy': QOS_POLICY_ID}, expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) @@ -637,7 +621,6 @@ def test_qos_minimum_bandwidth_rules(self): self.verify_list( self.proxy.qos_minimum_bandwidth_rules, qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, - paginated=False, method_kwargs={'qos_policy': QOS_POLICY_ID}, expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) @@ -671,8 +654,7 @@ def test_qos_policy_get(self): self.verify_get(self.proxy.get_qos_policy, qos_policy.QoSPolicy) def test_qos_policies(self): - self.verify_list(self.proxy.qos_policies, qos_policy.QoSPolicy, - paginated=False) + self.verify_list(self.proxy.qos_policies, qos_policy.QoSPolicy) def test_qos_policy_update(self): self.verify_update(self.proxy.update_qos_policy, qos_policy.QoSPolicy) @@ -686,8 +668,7 @@ def test_qos_rule_type_get(self): qos_rule_type.QoSRuleType) def test_qos_rule_types(self): - self.verify_list(self.proxy.qos_rule_types, qos_rule_type.QoSRuleType, - paginated=False) + self.verify_list(self.proxy.qos_rule_types, qos_rule_type.QoSRuleType) def test_quota_delete(self): self.verify_delete(self.proxy.delete_quota, quota.Quota, False) @@ -724,7 +705,7 @@ def test_quota_default_get(self, mock_get): mock_get.assert_called_once_with(quota.Quota, 'QUOTA_ID') def test_quotas(self): - self.verify_list(self.proxy.quotas, quota.Quota, paginated=False) + self.verify_list(self.proxy.quotas, quota.Quota) def test_quota_update(self): self.verify_update(self.proxy.update_quota, quota.Quota) @@ -748,8 +729,7 @@ def test_rbac_policy_get(self): self.verify_get(self.proxy.get_rbac_policy, rbac_policy.RBACPolicy) def test_rbac_policies(self): - self.verify_list(self.proxy.rbac_policies, - rbac_policy.RBACPolicy, paginated=False) + self.verify_list(self.proxy.rbac_policies, rbac_policy.RBACPolicy) def test_rbac_policy_update(self): self.verify_update(self.proxy.update_rbac_policy, @@ -771,7 +751,7 @@ def test_router_get(self): self.verify_get(self.proxy.get_router, router.Router) def test_routers(self): - self.verify_list(self.proxy.routers, router.Router, paginated=False) + self.verify_list(self.proxy.routers, router.Router) def test_router_update(self): self.verify_update(self.proxy.update_router, router.Router) @@ -862,7 +842,6 @@ def test_router_hosting_l3_agents_list(self): self.verify_list( self.proxy.routers_hosting_l3_agents, agent.RouterL3Agent, - paginated=False, method_kwargs={'router': ROUTER_ID}, expected_kwargs={'router_id': ROUTER_ID}, ) @@ -871,7 +850,6 @@ def test_agent_hosted_routers_list(self): self.verify_list( self.proxy.agent_hosted_routers, router.L3AgentRouter, - paginated=False, method_kwargs={'agent': AGENT_ID}, expected_kwargs={'agent_id': AGENT_ID}, ) @@ -898,8 +876,7 @@ def test_firewall_group_get(self): def test_firewall_groups(self): self.verify_list(self.proxy.firewall_groups, - firewall_group.FirewallGroup, - paginated=False) + firewall_group.FirewallGroup) def test_firewall_group_update(self): self.verify_update(self.proxy.update_firewall_group, @@ -927,8 +904,7 @@ def test_firewall_policy_get(self): def test_firewall_policies(self): self.verify_list(self.proxy.firewall_policies, - firewall_policy.FirewallPolicy, - paginated=False) + firewall_policy.FirewallPolicy) def test_firewall_policy_update(self): self.verify_update(self.proxy.update_firewall_policy, @@ -956,8 +932,7 @@ def test_firewall_rule_get(self): def test_firewall_rules(self): self.verify_list(self.proxy.firewall_rules, - firewall_rule.FirewallRule, - paginated=False) + firewall_rule.FirewallRule) def test_firewall_rule_update(self): self.verify_update(self.proxy.update_firewall_rule, @@ -985,8 +960,7 @@ def test_network_segment_range_get(self): def test_network_segment_ranges(self): self.verify_list(self.proxy.network_segment_ranges, - network_segment_range.NetworkSegmentRange, - paginated=False) + network_segment_range.NetworkSegmentRange) def test_network_segment_range_update(self): self.verify_update(self.proxy.update_network_segment_range, @@ -1014,8 +988,7 @@ def test_security_group_get(self): def test_security_groups(self): self.verify_list(self.proxy.security_groups, - security_group.SecurityGroup, - paginated=False) + security_group.SecurityGroup) def test_security_group_update(self): self.verify_update(self.proxy.update_security_group, @@ -1043,8 +1016,7 @@ def test_security_group_rule_get(self): def test_security_group_rules(self): self.verify_list(self.proxy.security_group_rules, - security_group_rule.SecurityGroupRule, - paginated=False) + security_group_rule.SecurityGroupRule) def test_segment_create_attrs(self): self.verify_create(self.proxy.create_segment, segment.Segment) @@ -1062,7 +1034,7 @@ def test_segment_get(self): self.verify_get(self.proxy.get_segment, segment.Segment) def test_segments(self): - self.verify_list(self.proxy.segments, segment.Segment, paginated=False) + self.verify_list(self.proxy.segments, segment.Segment) def test_segment_update(self): self.verify_update(self.proxy.update_segment, segment.Segment) @@ -1083,7 +1055,7 @@ def test_subnet_get(self): self.verify_get(self.proxy.get_subnet, subnet.Subnet) def test_subnets(self): - self.verify_list(self.proxy.subnets, subnet.Subnet, paginated=False) + self.verify_list(self.proxy.subnets, subnet.Subnet) def test_subnet_update(self): self.verify_update(self.proxy.update_subnet, subnet.Subnet) @@ -1110,8 +1082,7 @@ def test_subnet_pool_get(self): def test_subnet_pools(self): self.verify_list(self.proxy.subnet_pools, - subnet_pool.SubnetPool, - paginated=False) + subnet_pool.SubnetPool) def test_subnet_pool_update(self): self.verify_update(self.proxy.update_subnet_pool, @@ -1137,8 +1108,7 @@ def test_vpn_service_get(self): self.verify_get(self.proxy.get_vpn_service, vpn_service.VPNService) def test_vpn_services(self): - self.verify_list(self.proxy.vpn_services, vpn_service.VPNService, - paginated=False) + self.verify_list(self.proxy.vpn_services, vpn_service.VPNService) def test_vpn_service_update(self): self.verify_update(self.proxy.update_vpn_service, @@ -1146,8 +1116,7 @@ def test_vpn_service_update(self): def test_service_provider(self): self.verify_list(self.proxy.service_providers, - service_provider.ServiceProvider, - paginated=False) + service_provider.ServiceProvider) def test_auto_allocated_topology_get(self): self.verify_get(self.proxy.get_auto_allocated_topology, diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index c6cc44359..87597bfce 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -43,7 +43,7 @@ def test_find_stack(self): self.verify_find(self.proxy.find_stack, stack.Stack) def test_stacks(self): - self.verify_list(self.proxy.stacks, stack.Stack, paginated=False) + self.verify_list(self.proxy.stacks, stack.Stack) def test_get_stack(self): self.verify_get(self.proxy.get_stack, stack.Stack) @@ -195,7 +195,7 @@ def test_resources_with_stack_object(self, mock_find): stk = stack.Stack(id=stack_id, name=stack_name) self.verify_list(self.proxy.resources, resource.Resource, - paginated=False, method_args=[stk], + method_args=[stk], expected_kwargs={'stack_name': stack_name, 'stack_id': stack_id}) @@ -209,7 +209,7 @@ def test_resources_with_stack_name(self, mock_find): mock_find.return_value = stk self.verify_list(self.proxy.resources, resource.Resource, - paginated=False, method_args=[stack_id], + method_args=[stack_id], expected_kwargs={'stack_name': stack_name, 'stack_id': stack_id}) @@ -232,8 +232,7 @@ def test_create_software_config(self): sc.SoftwareConfig) def test_software_configs(self): - self.verify_list(self.proxy.software_configs, sc.SoftwareConfig, - paginated=True) + self.verify_list(self.proxy.software_configs, sc.SoftwareConfig) def test_get_software_config(self): self.verify_get(self.proxy.get_software_config, sc.SoftwareConfig) @@ -250,7 +249,7 @@ def test_create_software_deployment(self): def test_software_deployments(self): self.verify_list(self.proxy.software_deployments, - sd.SoftwareDeployment, paginated=False) + sd.SoftwareDeployment) def test_get_software_deployment(self): self.verify_get(self.proxy.get_software_deployment, diff --git a/openstack/tests/unit/test_proxy_base.py b/openstack/tests/unit/test_proxy_base.py index 849597d6c..544dac59d 100644 --- a/openstack/tests/unit/test_proxy_base.py +++ b/openstack/tests/unit/test_proxy_base.py @@ -200,11 +200,12 @@ def verify_find(self, test_method, resource_type, value=None, expected_result="result", **kwargs) - def verify_list(self, test_method, resource_type, paginated=False, + def verify_list(self, test_method, resource_type, mock_method="openstack.proxy.Proxy._list", **kwargs): expected_kwargs = kwargs.pop("expected_kwargs", {}) - expected_kwargs.update({"paginated": paginated}) + if 'paginated' in kwargs: + expected_kwargs.update({"paginated": kwargs['paginated']}) method_kwargs = kwargs.pop("method_kwargs", {}) self._verify2(mock_method, test_method, method_kwargs=method_kwargs, @@ -214,12 +215,11 @@ def verify_list(self, test_method, resource_type, paginated=False, **kwargs) def verify_list_no_kwargs(self, test_method, resource_type, - paginated=False, mock_method="openstack.proxy.Proxy._list"): self._verify2(mock_method, test_method, method_kwargs={}, expected_args=[resource_type], - expected_kwargs={"paginated": paginated}, + expected_kwargs={}, expected_result=["result"]) def verify_update(self, test_method, resource_type, value=None, diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index f1719f79c..ee98777f0 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1441,6 +1441,7 @@ class Test(self.test_class): mock_response = mock.Mock() mock_response.status_code = 200 mock_response.json.return_value = {key: [{"id": id_value}]} + mock_response.links = [] self.session.get.return_value = mock_response diff --git a/openstack/tests/unit/workflow/test_proxy.py b/openstack/tests/unit/workflow/test_proxy.py index b17d1d67b..2f6ce967e 100644 --- a/openstack/tests/unit/workflow/test_proxy.py +++ b/openstack/tests/unit/workflow/test_proxy.py @@ -23,13 +23,11 @@ def setUp(self): def test_workflows(self): self.verify_list(self.proxy.workflows, - workflow.Workflow, - paginated=True) + workflow.Workflow) def test_executions(self): self.verify_list(self.proxy.executions, - execution.Execution, - paginated=True) + execution.Execution) def test_workflow_get(self): self.verify_get(self.proxy.get_workflow, diff --git a/openstack/workflow/v2/_proxy.py b/openstack/workflow/v2/_proxy.py index f2b9f0c53..e0e5e5ced 100644 --- a/openstack/workflow/v2/_proxy.py +++ b/openstack/workflow/v2/_proxy.py @@ -57,7 +57,7 @@ def workflows(self, **query): :returns: A generator of workflow instances. """ - return self._list(_workflow.Workflow, paginated=True, **query) + return self._list(_workflow.Workflow, **query) def delete_workflow(self, value, ignore_missing=True): """Delete a workflow @@ -133,7 +133,7 @@ def executions(self, **query): :returns: A generator of execution instances. """ - return self._list(_execution.Execution, paginated=True, **query) + return self._list(_execution.Execution, **query) def delete_execution(self, value, ignore_missing=True): """Delete an execution From b64990055caa0a96efbe94c2395d6813b74d2cb7 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 30 Jan 2019 17:11:44 +0100 Subject: [PATCH 2355/3836] fix typo a small typo (missing "c" in "cloud") Change-Id: I5748ea9b51aae6dc330856f5635993cded59545a --- examples/connect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/connect.py b/examples/connect.py index d2730de9c..c8ce89550 100644 --- a/examples/connect.py +++ b/examples/connect.py @@ -25,7 +25,7 @@ openstack.enable_logging(True, stream=sys.stdout) -#: Defines the OpenStack Config loud key in your config file, +#: Defines the OpenStack Config cloud key in your config file, #: typically in $HOME/.config/openstack/clouds.yaml. That configuration #: will determine where the examples will be run and what resource defaults #: will be used to run the examples. From e740da6088f93967759a2ab8ec29ccc8e1a06aa4 Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Wed, 30 Jan 2019 08:15:10 -0800 Subject: [PATCH 2356/3836] Fix indentation for new pycodestyle E117 There was an over indented method in openstackcloud.py#L3099 [1] that the new pycodestyle E117[2] is catching[3]. This patch corrects the indentation to allow pep8/flake8 to pass. [1] https://github.com/openstack/openstacksdk/blob/master/openstack/cloud/ \ openstackcloud.py#L3099 [2] https://github.com/PyCQA/pycodestyle/blob/master/CHANGES.txt#L9 [3] http://logs.openstack.org/56/633856/2/check/openstack-tox-pep8/5c57397/ \ job-output.txt.gz#_2019-01-30_15_27_16_765029 Change-Id: I20fd50195ca6aa6ba67ba15e7ba1155acb18d4eb --- openstack/cloud/openstackcloud.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 9ddf30cb8..398218124 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -3096,10 +3096,10 @@ def get_server_console(self, server, length=None): return "" def _get_server_console_output(self, server_id, length=None): - data = _adapter._json_response(self.compute.post( - '/servers/{server_id}/action'.format(server_id=server_id), - json={'os-getConsoleOutput': {'length': length}})) - return self._get_and_munchify('output', data) + data = _adapter._json_response(self.compute.post( + '/servers/{server_id}/action'.format(server_id=server_id), + json={'os-getConsoleOutput': {'length': length}})) + return self._get_and_munchify('output', data) def get_server( self, name_or_id=None, filters=None, detailed=False, bare=False, From 2327519e67d0ea567524380cab77d8e6302c3398 Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Tue, 29 Jan 2019 16:08:46 -0800 Subject: [PATCH 2357/3836] Adds tags support for Octavia (load_balancer) This patch adds "tags" support to the Octavia (load_balancer) objects. It adds the "redirect_prefix" to L7 policy. It corrects the "flavor" load balancer field that never existed, to be "flavor_id". Finally, it adds unit testing for the query parameters. Change-Id: I5271183e2838eac9e943b488af5e6795e1ece4d0 --- openstack/load_balancer/v2/health_monitor.py | 3 +- openstack/load_balancer/v2/l7_policy.py | 8 +++-- openstack/load_balancer/v2/l7_rule.py | 4 +-- openstack/load_balancer/v2/listener.py | 3 +- openstack/load_balancer/v2/load_balancer.py | 11 ++++--- openstack/load_balancer/v2/member.py | 3 +- openstack/load_balancer/v2/pool.py | 5 +-- .../unit/load_balancer/test_health_monitor.py | 27 ++++++++++++++++ .../tests/unit/load_balancer/test_l7policy.py | 26 ++++++++++++++++ .../tests/unit/load_balancer/test_l7rule.py | 23 ++++++++++++++ .../tests/unit/load_balancer/test_listener.py | 31 +++++++++++++++++++ .../unit/load_balancer/test_load_balancer.py | 27 ++++++++++++++-- .../tests/unit/load_balancer/test_member.py | 25 +++++++++++++++ .../tests/unit/load_balancer/test_pool.py | 24 ++++++++++++++ ...octavia-tags-support-1c1cf94184e6ebb7.yaml | 10 ++++++ 15 files changed, 214 insertions(+), 16 deletions(-) create mode 100644 releasenotes/notes/add-octavia-tags-support-1c1cf94184e6ebb7.yaml diff --git a/openstack/load_balancer/v2/health_monitor.py b/openstack/load_balancer/v2/health_monitor.py index 4cca0262d..01f5fbee1 100644 --- a/openstack/load_balancer/v2/health_monitor.py +++ b/openstack/load_balancer/v2/health_monitor.py @@ -13,7 +13,7 @@ from openstack import resource -class HealthMonitor(resource.Resource): +class HealthMonitor(resource.Resource, resource.TagMixin): resource_key = 'healthmonitor' resources_key = 'healthmonitors' base_path = '/lbaas/healthmonitors' @@ -30,6 +30,7 @@ class HealthMonitor(resource.Resource): 'http_method', 'max_retries', 'max_retries_down', 'pool_id', 'provisioning_status', 'operating_status', 'timeout', 'project_id', 'type', 'url_path', is_admin_state_up='admin_state_up', + **resource.TagMixin._tag_query_parameters ) #: Properties diff --git a/openstack/load_balancer/v2/l7_policy.py b/openstack/load_balancer/v2/l7_policy.py index 099b176da..4b099dc22 100644 --- a/openstack/load_balancer/v2/l7_policy.py +++ b/openstack/load_balancer/v2/l7_policy.py @@ -13,7 +13,7 @@ from openstack import resource -class L7Policy(resource.Resource): +class L7Policy(resource.Resource, resource.TagMixin): resource_key = 'l7policy' resources_key = 'l7policies' base_path = '/lbaas/l7policies' @@ -28,7 +28,9 @@ class L7Policy(resource.Resource): _query_mapping = resource.QueryParameters( 'action', 'description', 'listener_id', 'name', 'position', 'redirect_pool_id', 'redirect_url', 'provisioning_status', - 'operating_status', is_admin_state_up='admin_state_up', + 'operating_status', 'redirect_prefix', 'project_id', + is_admin_state_up='admin_state_up', + **resource.TagMixin._tag_query_parameters ) #: Properties @@ -54,6 +56,8 @@ class L7Policy(resource.Resource): provisioning_status = resource.Body('provisioning_status') #: The ID of the pool to which the requests will be redirected redirect_pool_id = resource.Body('redirect_pool_id') + #: The URL prefix to which the requests should be redirected + redirect_prefix = resource.Body('redirect_prefix') #: The URL to which the requests should be redirected redirect_url = resource.Body('redirect_url') #: The list of L7Rules associated with the l7policy diff --git a/openstack/load_balancer/v2/l7_rule.py b/openstack/load_balancer/v2/l7_rule.py index be5c118f5..ee8400e75 100644 --- a/openstack/load_balancer/v2/l7_rule.py +++ b/openstack/load_balancer/v2/l7_rule.py @@ -13,7 +13,7 @@ from openstack import resource -class L7Rule(resource.Resource): +class L7Rule(resource.Resource, resource.TagMixin): resource_key = 'rule' resources_key = 'rules' base_path = '/lbaas/l7policies/%(l7policy_id)s/rules' @@ -29,7 +29,7 @@ class L7Rule(resource.Resource): 'compare_type', 'created_at', 'invert', 'key', 'project_id', 'provisioning_status', 'type', 'updated_at', 'rule_value', 'operating_status', is_admin_state_up='admin_state_up', - l7_policy_id='l7policy_id', + l7_policy_id='l7policy_id', **resource.TagMixin._tag_query_parameters ) #: Properties diff --git a/openstack/load_balancer/v2/listener.py b/openstack/load_balancer/v2/listener.py index 6ea5d74bb..e55bcde48 100644 --- a/openstack/load_balancer/v2/listener.py +++ b/openstack/load_balancer/v2/listener.py @@ -13,7 +13,7 @@ from openstack import resource -class Listener(resource.Resource): +class Listener(resource.Resource, resource.TagMixin): resource_key = 'listener' resources_key = 'listeners' base_path = '/lbaas/listeners' @@ -33,6 +33,7 @@ class Listener(resource.Resource): 'timeout_client_data', 'timeout_member_connect', 'timeout_member_data', 'timeout_tcp_inspect', is_admin_state_up='admin_state_up', + **resource.TagMixin._tag_query_parameters ) # Properties diff --git a/openstack/load_balancer/v2/load_balancer.py b/openstack/load_balancer/v2/load_balancer.py index 9b36f7e5f..3c8379338 100644 --- a/openstack/load_balancer/v2/load_balancer.py +++ b/openstack/load_balancer/v2/load_balancer.py @@ -13,7 +13,7 @@ from openstack import resource -class LoadBalancer(resource.Resource): +class LoadBalancer(resource.Resource, resource.TagMixin): resource_key = 'loadbalancer' resources_key = 'loadbalancers' base_path = '/lbaas/loadbalancers' @@ -26,10 +26,11 @@ class LoadBalancer(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'description', 'flavor', 'name', 'project_id', 'provider', + 'description', 'flavor_id', 'name', 'project_id', 'provider', 'vip_address', 'vip_network_id', 'vip_port_id', 'vip_subnet_id', 'vip_qos_policy_id', 'provisioning_status', 'operating_status', - is_admin_state_up='admin_state_up' + is_admin_state_up='admin_state_up', + **resource.TagMixin._tag_query_parameters ) #: Properties @@ -39,8 +40,8 @@ class LoadBalancer(resource.Resource): created_at = resource.Body('created_at') #: The load balancer description description = resource.Body('description') - #: The load balancer flavor - flavor = resource.Body('flavor') + #: The load balancer flavor ID + flavor_id = resource.Body('flavor_id') #: List of listeners associated with this load balancer listeners = resource.Body('listeners', type=list) #: The load balancer name diff --git a/openstack/load_balancer/v2/member.py b/openstack/load_balancer/v2/member.py index f67ad3b74..3f4201be7 100644 --- a/openstack/load_balancer/v2/member.py +++ b/openstack/load_balancer/v2/member.py @@ -13,7 +13,7 @@ from openstack import resource -class Member(resource.Resource): +class Member(resource.Resource, resource.TagMixin): resource_key = 'member' resources_key = 'members' base_path = '/lbaas/pools/%(pool_id)s/members' @@ -30,6 +30,7 @@ class Member(resource.Resource): 'created_at', 'updated_at', 'provisioning_status', 'operating_status', 'project_id', 'monitor_address', 'monitor_port', 'backup', is_admin_state_up='admin_state_up', + **resource.TagMixin._tag_query_parameters ) # Properties diff --git a/openstack/load_balancer/v2/pool.py b/openstack/load_balancer/v2/pool.py index 9bbc13bf0..8794e9ce1 100644 --- a/openstack/load_balancer/v2/pool.py +++ b/openstack/load_balancer/v2/pool.py @@ -13,7 +13,7 @@ from openstack import resource -class Pool(resource.Resource): +class Pool(resource.Resource, resource.TagMixin): resource_key = 'pool' resources_key = 'pools' base_path = '/lbaas/pools' @@ -29,7 +29,8 @@ class Pool(resource.Resource): 'health_monitor_id', 'lb_algorithm', 'listener_id', 'loadbalancer_id', 'description', 'name', 'project_id', 'protocol', 'created_at', 'updated_at', 'provisioning_status', 'operating_status', - is_admin_state_up='admin_state_up' + is_admin_state_up='admin_state_up', + **resource.TagMixin._tag_query_parameters ) #: Properties diff --git a/openstack/tests/unit/load_balancer/test_health_monitor.py b/openstack/tests/unit/load_balancer/test_health_monitor.py index 710a8e79b..7bfd72c33 100644 --- a/openstack/tests/unit/load_balancer/test_health_monitor.py +++ b/openstack/tests/unit/load_balancer/test_health_monitor.py @@ -72,3 +72,30 @@ def test_make_it(self): self.assertEqual(EXAMPLE['type'], test_hm.type) self.assertEqual(EXAMPLE['updated_at'], test_hm.updated_at) self.assertEqual(EXAMPLE['url_path'], test_hm.url_path) + + self.assertDictEqual( + {'limit': 'limit', + 'marker': 'marker', + 'created_at': 'created_at', + 'updated_at': 'updated_at', + 'name': 'name', + 'project_id': 'project_id', + 'tags': 'tags', + 'any_tags': 'tags-any', + 'not_tags': 'not-tags', + 'not_any_tags': 'not-tags-any', + 'operating_status': 'operating_status', + 'provisioning_status': 'provisioning_status', + 'is_admin_state_up': 'admin_state_up', + + 'delay': 'delay', + 'expected_codes': 'expected_codes', + 'http_method': 'http_method', + 'max_retries': 'max_retries', + 'max_retries_down': 'max_retries_down', + 'pool_id': 'pool_id', + 'timeout': 'timeout', + 'type': 'type', + 'url_path': 'url_path' + }, + test_hm._query_mapping._mapping) diff --git a/openstack/tests/unit/load_balancer/test_l7policy.py b/openstack/tests/unit/load_balancer/test_l7policy.py index f8001ad69..a3194f93c 100644 --- a/openstack/tests/unit/load_balancer/test_l7policy.py +++ b/openstack/tests/unit/load_balancer/test_l7policy.py @@ -28,6 +28,7 @@ 'project_id': uuid.uuid4(), 'provisioning_status': 'ACTIVE', 'redirect_pool_id': uuid.uuid4(), + 'redirect_prefix': 'https://www.example.com', 'redirect_url': '/test_url', 'rules': [{'id': uuid.uuid4()}], 'updated_at': '2017-07-17T12:16:57.233772', @@ -64,6 +65,31 @@ def test_make_it(self): test_l7_policy.provisioning_status) self.assertEqual(EXAMPLE['redirect_pool_id'], test_l7_policy.redirect_pool_id) + self.assertEqual(EXAMPLE['redirect_prefix'], + test_l7_policy.redirect_prefix) self.assertEqual(EXAMPLE['redirect_url'], test_l7_policy.redirect_url) self.assertEqual(EXAMPLE['rules'], test_l7_policy.rules) self.assertEqual(EXAMPLE['updated_at'], test_l7_policy.updated_at) + + self.assertDictEqual( + {'limit': 'limit', + 'marker': 'marker', + 'name': 'name', + 'description': 'description', + 'project_id': 'project_id', + 'tags': 'tags', + 'any_tags': 'tags-any', + 'not_tags': 'not-tags', + 'not_any_tags': 'not-tags-any', + 'operating_status': 'operating_status', + 'provisioning_status': 'provisioning_status', + 'is_admin_state_up': 'admin_state_up', + + 'action': 'action', + 'listener_id': 'listener_id', + 'position': 'position', + 'redirect_pool_id': 'redirect_pool_id', + 'redirect_url': 'redirect_url', + 'redirect_prefix': 'redirect_prefix' + }, + test_l7_policy._query_mapping._mapping) diff --git a/openstack/tests/unit/load_balancer/test_l7rule.py b/openstack/tests/unit/load_balancer/test_l7rule.py index c67615a5c..3d93703f2 100644 --- a/openstack/tests/unit/load_balancer/test_l7rule.py +++ b/openstack/tests/unit/load_balancer/test_l7rule.py @@ -63,3 +63,26 @@ def test_make_it(self): self.assertEqual(EXAMPLE['type'], test_l7rule.type) self.assertEqual(EXAMPLE['updated_at'], test_l7rule.updated_at) self.assertEqual(EXAMPLE['value'], test_l7rule.rule_value) + + self.assertDictEqual( + {'limit': 'limit', + 'marker': 'marker', + 'created_at': 'created_at', + 'updated_at': 'updated_at', + 'project_id': 'project_id', + 'tags': 'tags', + 'any_tags': 'tags-any', + 'not_tags': 'not-tags', + 'not_any_tags': 'not-tags-any', + 'operating_status': 'operating_status', + 'provisioning_status': 'provisioning_status', + 'is_admin_state_up': 'admin_state_up', + + 'compare_type': 'compare_type', + 'invert': 'invert', + 'key': 'key', + 'type': 'type', + 'rule_value': 'rule_value', + 'l7_policy_id': 'l7policy_id' + }, + test_l7rule._query_mapping._mapping) diff --git a/openstack/tests/unit/load_balancer/test_listener.py b/openstack/tests/unit/load_balancer/test_listener.py index 93aad261b..5ea719d87 100644 --- a/openstack/tests/unit/load_balancer/test_listener.py +++ b/openstack/tests/unit/load_balancer/test_listener.py @@ -93,3 +93,34 @@ def test_make_it(self): test_listener.timeout_member_data) self.assertEqual(EXAMPLE['timeout_tcp_inspect'], test_listener.timeout_tcp_inspect) + + self.assertDictEqual( + {'limit': 'limit', + 'marker': 'marker', + 'created_at': 'created_at', + 'updated_at': 'updated_at', + 'description': 'description', + 'name': 'name', + 'project_id': 'project_id', + 'tags': 'tags', + 'any_tags': 'tags-any', + 'not_tags': 'not-tags', + 'not_any_tags': 'not-tags-any', + 'operating_status': 'operating_status', + 'provisioning_status': 'provisioning_status', + 'is_admin_state_up': 'admin_state_up', + + 'connection_limit': 'connection_limit', + 'default_pool_id': 'default_pool_id', + 'default_tls_container_ref': 'default_tls_container_ref', + 'sni_container_refs': 'sni_container_refs', + 'insert_headers': 'insert_headers', + 'load_balancer_id': 'load_balancer_id', + 'protocol': 'protocol', + 'protocol_port': 'protocol_port', + 'timeout_client_data': 'timeout_client_data', + 'timeout_member_connect': 'timeout_member_connect', + 'timeout_member_data': 'timeout_member_data', + 'timeout_tcp_inspect': 'timeout_tcp_inspect', + }, + test_listener._query_mapping._mapping) diff --git a/openstack/tests/unit/load_balancer/test_load_balancer.py b/openstack/tests/unit/load_balancer/test_load_balancer.py index 4c2ae6d59..427ad8184 100644 --- a/openstack/tests/unit/load_balancer/test_load_balancer.py +++ b/openstack/tests/unit/load_balancer/test_load_balancer.py @@ -21,7 +21,7 @@ 'admin_state_up': True, 'created_at': '2017-07-17T12:14:57.233772', 'description': 'fake_description', - 'flavor': uuid.uuid4(), + 'flavor_id': uuid.uuid4(), 'id': IDENTIFIER, 'listeners': [{'id', uuid.uuid4()}], 'name': 'test_load_balancer', @@ -59,7 +59,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['created_at'], test_load_balancer.created_at), self.assertEqual(EXAMPLE['description'], test_load_balancer.description) - self.assertEqual(EXAMPLE['flavor'], test_load_balancer.flavor) + self.assertEqual(EXAMPLE['flavor_id'], test_load_balancer.flavor_id) self.assertEqual(EXAMPLE['id'], test_load_balancer.id) self.assertEqual(EXAMPLE['listeners'], test_load_balancer.listeners) self.assertEqual(EXAMPLE['name'], test_load_balancer.name) @@ -82,6 +82,29 @@ def test_make_it(self): self.assertEqual(EXAMPLE['vip_qos_policy_id'], test_load_balancer.vip_qos_policy_id) + self.assertDictEqual( + {'limit': 'limit', + 'marker': 'marker', + 'description': 'description', + 'flavor_id': 'flavor_id', + 'name': 'name', + 'project_id': 'project_id', + 'provider': 'provider', + 'operating_status': 'operating_status', + 'provisioning_status': 'provisioning_status', + 'is_admin_state_up': 'admin_state_up', + 'vip_address': 'vip_address', + 'vip_network_id': 'vip_network_id', + 'vip_port_id': 'vip_port_id', + 'vip_subnet_id': 'vip_subnet_id', + 'vip_qos_policy_id': 'vip_qos_policy_id', + 'tags': 'tags', + 'any_tags': 'tags-any', + 'not_tags': 'not-tags', + 'not_any_tags': 'not-tags-any', + }, + test_load_balancer._query_mapping._mapping) + def test_delete_non_cascade(self): sess = mock.Mock() resp = mock.Mock() diff --git a/openstack/tests/unit/load_balancer/test_member.py b/openstack/tests/unit/load_balancer/test_member.py index 4efcadc40..a99a2858d 100644 --- a/openstack/tests/unit/load_balancer/test_member.py +++ b/openstack/tests/unit/load_balancer/test_member.py @@ -61,3 +61,28 @@ def test_make_it(self): self.assertEqual(EXAMPLE['subnet_id'], test_member.subnet_id) self.assertEqual(EXAMPLE['weight'], test_member.weight) self.assertFalse(test_member.backup) + + self.assertDictEqual( + {'limit': 'limit', + 'marker': 'marker', + 'created_at': 'created_at', + 'updated_at': 'updated_at', + 'name': 'name', + 'project_id': 'project_id', + 'tags': 'tags', + 'any_tags': 'tags-any', + 'not_tags': 'not-tags', + 'not_any_tags': 'not-tags-any', + 'operating_status': 'operating_status', + 'provisioning_status': 'provisioning_status', + 'is_admin_state_up': 'admin_state_up', + + 'address': 'address', + 'protocol_port': 'protocol_port', + 'subnet_id': 'subnet_id', + 'weight': 'weight', + 'monitor_address': 'monitor_address', + 'monitor_port': 'monitor_port', + 'backup': 'backup' + }, + test_member._query_mapping._mapping) diff --git a/openstack/tests/unit/load_balancer/test_pool.py b/openstack/tests/unit/load_balancer/test_pool.py index 5465798fe..efecf5b2d 100644 --- a/openstack/tests/unit/load_balancer/test_pool.py +++ b/openstack/tests/unit/load_balancer/test_pool.py @@ -81,3 +81,27 @@ def test_make_it(self): self.assertEqual(EXAMPLE['health_monitor_id'], test_pool.health_monitor_id) self.assertEqual(EXAMPLE['members'], test_pool.members) + + self.assertDictEqual( + {'limit': 'limit', + 'marker': 'marker', + 'created_at': 'created_at', + 'updated_at': 'updated_at', + 'description': 'description', + 'name': 'name', + 'project_id': 'project_id', + 'tags': 'tags', + 'any_tags': 'tags-any', + 'not_tags': 'not-tags', + 'not_any_tags': 'not-tags-any', + 'operating_status': 'operating_status', + 'provisioning_status': 'provisioning_status', + 'is_admin_state_up': 'admin_state_up', + + 'health_monitor_id': 'health_monitor_id', + 'lb_algorithm': 'lb_algorithm', + 'listener_id': 'listener_id', + 'loadbalancer_id': 'loadbalancer_id', + 'protocol': 'protocol', + }, + test_pool._query_mapping._mapping) diff --git a/releasenotes/notes/add-octavia-tags-support-1c1cf94184e6ebb7.yaml b/releasenotes/notes/add-octavia-tags-support-1c1cf94184e6ebb7.yaml new file mode 100644 index 000000000..5eb3c85e8 --- /dev/null +++ b/releasenotes/notes/add-octavia-tags-support-1c1cf94184e6ebb7.yaml @@ -0,0 +1,10 @@ +--- +features: + - Add tags support for the Octavia (load_balancer) objects. + - | + Added support for the Octavia (load_balancer) L7 Policy "redirect_prefix" + capability. +fixes: + - | + Fixed the Octavia (load_balancer) load balancer objects to have + "flavor_id" instead of the nonexistent "flavor" field. From f31930a57633066de3ea96902f6cdc58fd1682b0 Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Tue, 29 Jan 2019 17:39:58 -0800 Subject: [PATCH 2358/3836] Added Octavia load balancer and listener stats This patch adds Octavia (load_balancer) load balancer and listener get statistics methods. Change-Id: I69ddcece685e928cb1244483998f9be76a7ce9cf --- doc/source/user/proxies/load_balancer_v2.rst | 2 + openstack/load_balancer/v2/_proxy.py | 26 +++++++++++++ openstack/load_balancer/v2/listener.py | 26 +++++++++++++ openstack/load_balancer/v2/load_balancer.py | 26 +++++++++++++ .../load_balancer/v2/test_load_balancer.py | 18 +++++++++ .../tests/unit/load_balancer/test_listener.py | 35 +++++++++++++++++ .../unit/load_balancer/test_load_balancer.py | 39 ++++++++++++++++++- .../tests/unit/load_balancer/test_proxy.py | 18 +++++++++ ...ia-lb-listener-stats-1538cc6e4f734353.yaml | 4 ++ 9 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/add-octavia-lb-listener-stats-1538cc6e4f734353.yaml diff --git a/doc/source/user/proxies/load_balancer_v2.rst b/doc/source/user/proxies/load_balancer_v2.rst index 0ad940691..520274948 100644 --- a/doc/source/user/proxies/load_balancer_v2.rst +++ b/doc/source/user/proxies/load_balancer_v2.rst @@ -19,6 +19,7 @@ Load Balancer Operations .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_load_balancer .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_load_balancer .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_load_balancer + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_load_balancer_statistics .. automethod:: openstack.load_balancer.v2._proxy.Proxy.load_balancers .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_load_balancer @@ -31,6 +32,7 @@ Listener Operations .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_listener .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_listener .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_listener + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_listener_statistics .. automethod:: openstack.load_balancer.v2._proxy.Proxy.listeners .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_listener diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index e8ea2c892..bc0fb9034 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -50,6 +50,17 @@ def get_load_balancer(self, *attrs): """ return self._get(_lb.LoadBalancer, *attrs) + def get_load_balancer_statistics(self, name_or_id): + """Get the load balancer statistics + + :param name_or_id: The name or ID of a load balancer + + :returns: One :class:`~openstack.load_balancer.v2.load_balancer. + LoadBalancerStats` + """ + return self._get(_lb.LoadBalancerStats, lb_id=name_or_id, + requires_id=False) + def load_balancers(self, **query): """Retrieve a generator of load balancers @@ -172,6 +183,21 @@ def get_listener(self, listener): """ return self._get(_listener.Listener, listener) + def get_listener_statistics(self, listener): + """Get the listener statistics + + :param listener: The value can be the ID of a listener or a + :class:`~openstack.load_balancer.v2.listener.Listener` + instance. + + :returns: One :class:`~openstack.load_balancer.v2.listener. + ListenerStats` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_listener.ListenerStats, listener_id=listener, + requires_id=False) + def listeners(self, **query): """Return a generator of listeners diff --git a/openstack/load_balancer/v2/listener.py b/openstack/load_balancer/v2/listener.py index e55bcde48..894a93523 100644 --- a/openstack/load_balancer/v2/listener.py +++ b/openstack/load_balancer/v2/listener.py @@ -89,3 +89,29 @@ class Listener(resource.Resource, resource.TagMixin): #: Time, in milliseconds, to wait for additional TCP packets for content #: inspection. timeout_tcp_inspect = resource.Body('timeout_tcp_inspect', type=int) + + +class ListenerStats(resource.Resource): + resource_key = 'stats' + base_path = '/lbaas/listeners/%(listener_id)s/stats' + + # capabilities + allow_create = False + allow_fetch = True + allow_commit = False + allow_delete = False + allow_list = False + + # Properties + #: The ID of the listener. + listener_id = resource.URI('listener_id') + #: The currently active connections. + active_connections = resource.Body('active_connections', type=int) + #: The total bytes received. + bytes_in = resource.Body('bytes_in', type=int) + #: The total bytes sent. + bytes_out = resource.Body('bytes_out', type=int) + #: The total requests that were unable to be fulfilled. + request_errors = resource.Body('request_errors', type=int) + #: The total connections handled. + total_connections = resource.Body('total_connections', type=int) diff --git a/openstack/load_balancer/v2/load_balancer.py b/openstack/load_balancer/v2/load_balancer.py index 3c8379338..9d7cc407c 100644 --- a/openstack/load_balancer/v2/load_balancer.py +++ b/openstack/load_balancer/v2/load_balancer.py @@ -87,3 +87,29 @@ def delete(self, session, error_message=None): self._translate_response(response, has_body=False, error_message=error_message) return self + + +class LoadBalancerStats(resource.Resource): + resource_key = 'stats' + base_path = '/lbaas/loadbalancers/%(lb_id)s/stats' + + # capabilities + allow_create = False + allow_fetch = True + allow_commit = False + allow_delete = False + allow_list = False + + # Properties + #: The ID of the load balancer. + lb_id = resource.URI('lb_id') + #: The currently active connections. + active_connections = resource.Body('active_connections', type=int) + #: The total bytes received. + bytes_in = resource.Body('bytes_in', type=int) + #: The total bytes sent. + bytes_out = resource.Body('bytes_out', type=int) + #: The total requests that were unable to be fulfilled. + request_errors = resource.Body('request_errors', type=int) + #: The total connections handled. + total_connections = resource.Body('total_connections', type=int) diff --git a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py index f8f30aac7..9a12c564b 100644 --- a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py +++ b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py @@ -200,6 +200,15 @@ def test_lb_get(self): self.assertEqual(self.LB_ID, test_lb.id) self.assertEqual(self.VIP_SUBNET_ID, test_lb.vip_subnet_id) + def test_lb_get_stats(self): + test_lb_stats = self.conn.load_balancer.get_load_balancer_statistics( + self.LB_ID) + self.assertEqual(0, test_lb_stats.active_connections) + self.assertEqual(0, test_lb_stats.bytes_in) + self.assertEqual(0, test_lb_stats.bytes_out) + self.assertEqual(0, test_lb_stats.request_errors) + self.assertEqual(0, test_lb_stats.total_connections) + def test_lb_list(self): names = [lb.name for lb in self.conn.load_balancer.load_balancers()] self.assertIn(self.LB_NAME, names) @@ -231,6 +240,15 @@ def test_listener_get(self): self.assertEqual(self.PROTOCOL, test_listener.protocol) self.assertEqual(self.PROTOCOL_PORT, test_listener.protocol_port) + def test_listener_get_stats(self): + test_listener_stats = self.conn.load_balancer.get_listener_statistics( + self.LISTENER_ID) + self.assertEqual(0, test_listener_stats.active_connections) + self.assertEqual(0, test_listener_stats.bytes_in) + self.assertEqual(0, test_listener_stats.bytes_out) + self.assertEqual(0, test_listener_stats.request_errors) + self.assertEqual(0, test_listener_stats.total_connections) + def test_listener_list(self): names = [ls.name for ls in self.conn.load_balancer.listeners()] self.assertIn(self.LISTENER_NAME, names) diff --git a/openstack/tests/unit/load_balancer/test_listener.py b/openstack/tests/unit/load_balancer/test_listener.py index 5ea719d87..a0b254116 100644 --- a/openstack/tests/unit/load_balancer/test_listener.py +++ b/openstack/tests/unit/load_balancer/test_listener.py @@ -42,6 +42,14 @@ 'timeout_tcp_inspect': 0, } +EXAMPLE_STATS = { + 'active_connections': 1, + 'bytes_in': 2, + 'bytes_out': 3, + 'request_errors': 4, + 'total_connections': 5 +} + class TestListener(base.TestCase): @@ -124,3 +132,30 @@ def test_make_it(self): 'timeout_tcp_inspect': 'timeout_tcp_inspect', }, test_listener._query_mapping._mapping) + + +class TestListenerStats(base.TestCase): + + def test_basic(self): + test_listener = listener.ListenerStats() + self.assertEqual('stats', test_listener.resource_key) + self.assertEqual('/lbaas/listeners/%(listener_id)s/stats', + test_listener.base_path) + self.assertFalse(test_listener.allow_create) + self.assertTrue(test_listener.allow_fetch) + self.assertFalse(test_listener.allow_delete) + self.assertFalse(test_listener.allow_list) + self.assertFalse(test_listener.allow_commit) + + def test_make_it(self): + test_listener = listener.ListenerStats(**EXAMPLE_STATS) + self.assertEqual(EXAMPLE_STATS['active_connections'], + test_listener.active_connections) + self.assertEqual(EXAMPLE_STATS['bytes_in'], + test_listener.bytes_in) + self.assertEqual(EXAMPLE_STATS['bytes_out'], + test_listener.bytes_out) + self.assertEqual(EXAMPLE_STATS['request_errors'], + test_listener.request_errors) + self.assertEqual(EXAMPLE_STATS['total_connections'], + test_listener.total_connections) diff --git a/openstack/tests/unit/load_balancer/test_load_balancer.py b/openstack/tests/unit/load_balancer/test_load_balancer.py index 427ad8184..adab6706d 100644 --- a/openstack/tests/unit/load_balancer/test_load_balancer.py +++ b/openstack/tests/unit/load_balancer/test_load_balancer.py @@ -38,6 +38,14 @@ 'vip_qos_policy_id': uuid.uuid4(), } +EXAMPLE_STATS = { + 'active_connections': 1, + 'bytes_in': 2, + 'bytes_out': 3, + 'request_errors': 4, + 'total_connections': 5 +} + class TestLoadBalancer(base.TestCase): @@ -56,7 +64,7 @@ def test_basic(self): def test_make_it(self): test_load_balancer = load_balancer.LoadBalancer(**EXAMPLE) self.assertTrue(test_load_balancer.is_admin_state_up) - self.assertEqual(EXAMPLE['created_at'], test_load_balancer.created_at), + self.assertEqual(EXAMPLE['created_at'], test_load_balancer.created_at) self.assertEqual(EXAMPLE['description'], test_load_balancer.description) self.assertEqual(EXAMPLE['flavor_id'], test_load_balancer.flavor_id) @@ -70,7 +78,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['provider'], test_load_balancer.provider) self.assertEqual(EXAMPLE['provisioning_status'], test_load_balancer.provisioning_status) - self.assertEqual(EXAMPLE['updated_at'], test_load_balancer.updated_at), + self.assertEqual(EXAMPLE['updated_at'], test_load_balancer.updated_at) self.assertEqual(EXAMPLE['vip_address'], test_load_balancer.vip_address) self.assertEqual(EXAMPLE['vip_network_id'], @@ -152,3 +160,30 @@ def test_delete_cascade(self): error_message=None, has_body=False, ) + + +class TestLoadBalancerStats(base.TestCase): + + def test_basic(self): + test_load_balancer = load_balancer.LoadBalancerStats() + self.assertEqual('stats', test_load_balancer.resource_key) + self.assertEqual('/lbaas/loadbalancers/%(lb_id)s/stats', + test_load_balancer.base_path) + self.assertFalse(test_load_balancer.allow_create) + self.assertTrue(test_load_balancer.allow_fetch) + self.assertFalse(test_load_balancer.allow_delete) + self.assertFalse(test_load_balancer.allow_list) + self.assertFalse(test_load_balancer.allow_commit) + + def test_make_it(self): + test_load_balancer = load_balancer.LoadBalancerStats(**EXAMPLE_STATS) + self.assertEqual(EXAMPLE_STATS['active_connections'], + test_load_balancer.active_connections) + self.assertEqual(EXAMPLE_STATS['bytes_in'], + test_load_balancer.bytes_in) + self.assertEqual(EXAMPLE_STATS['bytes_out'], + test_load_balancer.bytes_out) + self.assertEqual(EXAMPLE_STATS['request_errors'], + test_load_balancer.request_errors) + self.assertEqual(EXAMPLE_STATS['total_connections'], + test_load_balancer.total_connections) diff --git a/openstack/tests/unit/load_balancer/test_proxy.py b/openstack/tests/unit/load_balancer/test_proxy.py index 94a3c07a3..a7bd1f838 100644 --- a/openstack/tests/unit/load_balancer/test_proxy.py +++ b/openstack/tests/unit/load_balancer/test_proxy.py @@ -28,6 +28,8 @@ class TestLoadBalancerProxy(test_proxy_base.TestProxyBase): + LB_ID = uuid.uuid4() + LISTENER_ID = uuid.uuid4() POOL_ID = uuid.uuid4() L7_POLICY_ID = uuid.uuid4() @@ -43,6 +45,14 @@ def test_load_balancer_get(self): self.verify_get(self.proxy.get_load_balancer, lb.LoadBalancer) + def test_load_balancer_stats_get(self): + self.verify_get(self.proxy.get_load_balancer_statistics, + lb.LoadBalancerStats, + value=[self.LB_ID], + expected_args=[lb.LoadBalancerStats], + expected_kwargs={'lb_id': self.LB_ID, + 'requires_id': False}) + def test_load_balancer_create(self): self.verify_create(self.proxy.create_load_balancer, lb.LoadBalancer) @@ -95,6 +105,14 @@ def test_listener_get(self): self.verify_get(self.proxy.get_listener, listener.Listener) + def test_listener_stats_get(self): + self.verify_get(self.proxy.get_listener_statistics, + listener.ListenerStats, + value=[self.LISTENER_ID], + expected_args=[listener.ListenerStats], + expected_kwargs={'listener_id': self.LISTENER_ID, + 'requires_id': False}) + def test_listener_create(self): self.verify_create(self.proxy.create_listener, listener.Listener) diff --git a/releasenotes/notes/add-octavia-lb-listener-stats-1538cc6e4f734353.yaml b/releasenotes/notes/add-octavia-lb-listener-stats-1538cc6e4f734353.yaml new file mode 100644 index 000000000..8fb7f9f09 --- /dev/null +++ b/releasenotes/notes/add-octavia-lb-listener-stats-1538cc6e4f734353.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Added load balancer and listener get statistics methods. From 3dcc7955b9af7185d9365d1457360525d66c7bac Mon Sep 17 00:00:00 2001 From: Duc Truong Date: Thu, 17 Jan 2019 01:04:20 +0000 Subject: [PATCH 2359/3836] Fix resource deletion in clustering The addition of the global Location setting for Resource object broke clustering because it was relying on the Location header to find the Action object associated with the delete action. To fix that, instead of exposing that location on a location property, construct an Action object with the id pulled from the location header in the delete method. This way there is an object with a status property already. fetch will need to be called on the Action returned to fill in status information - but since wait_for_status and wait_for_delete do that already, it should work with those systems as expected. Change-Id: Ifa44aacc4b4719b73e59d27ed0fcd35130358608 --- openstack/clustering/v1/_async_resource.py | 45 +++++++++++++++++++ openstack/clustering/v1/cluster.py | 9 ++-- openstack/clustering/v1/node.py | 9 ++-- openstack/resource.py | 24 ++++++---- .../functional/clustering/test_cluster.py | 35 +++++++++++++-- .../tests/unit/clustering/v1/test_cluster.py | 5 ++- .../tests/unit/clustering/v1/test_node.py | 5 ++- ...ng-resource-deletion-bed869ba47c2aac1.yaml | 13 ++++++ 8 files changed, 121 insertions(+), 24 deletions(-) create mode 100644 openstack/clustering/v1/_async_resource.py create mode 100644 releasenotes/notes/clustering-resource-deletion-bed869ba47c2aac1.yaml diff --git a/openstack/clustering/v1/_async_resource.py b/openstack/clustering/v1/_async_resource.py new file mode 100644 index 000000000..cd01ac3fe --- /dev/null +++ b/openstack/clustering/v1/_async_resource.py @@ -0,0 +1,45 @@ +# 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 import resource + +from openstack.clustering.v1 import action as _action + + +class AsyncResource(resource.Resource): + + def delete(self, session, error_message=None): + """Delete the remote resource based on this instance. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + + :return: An :class:`~openstack.clustering.v1.action.Action` + instance. The ``fetch`` method will need to be used + to populate the `Action` with status information. + :raises: :exc:`~openstack.exceptions.MethodNotSupported` if + :data:`Resource.allow_commit` is not set to ``True``. + :raises: :exc:`~openstack.exceptions.ResourceNotFound` if + the resource was not found. + """ + response = self._raw_delete(session) + return self._delete_response(response, error_message) + + def _delete_response(self, response, error_message=None): + exceptions.raise_from_response(response, error_message=error_message) + location = response.headers['Location'] + action_id = location.split('/')[-1] + action = _action.Action.existing( + id=action_id, + connection=self._connection) + return action diff --git a/openstack/clustering/v1/cluster.py b/openstack/clustering/v1/cluster.py index 26e22cd2f..e545080c4 100644 --- a/openstack/clustering/v1/cluster.py +++ b/openstack/clustering/v1/cluster.py @@ -13,8 +13,10 @@ from openstack import resource from openstack import utils +from openstack.clustering.v1 import _async_resource -class Cluster(resource.Resource): + +class Cluster(_async_resource.AsyncResource): resource_key = 'cluster' resources_key = 'clusters' base_path = '/clusters' @@ -184,6 +186,5 @@ def force_delete(self, session): """Force delete a cluster.""" body = {'force': True} url = utils.urljoin(self.base_path, self.id) - resp = session.delete(url, json=body) - self._translate_response(resp, has_body=False) - return self + response = session.delete(url, json=body) + return self._delete_response(response) diff --git a/openstack/clustering/v1/node.py b/openstack/clustering/v1/node.py index cf9fe05e2..909ff6e95 100644 --- a/openstack/clustering/v1/node.py +++ b/openstack/clustering/v1/node.py @@ -13,8 +13,10 @@ from openstack import resource from openstack import utils +from openstack.clustering.v1 import _async_resource -class Node(resource.Resource): + +class Node(_async_resource.AsyncResource): resource_key = 'node' resources_key = 'nodes' base_path = '/nodes' @@ -158,9 +160,8 @@ def force_delete(self, session): """Force delete a node.""" body = {'force': True} url = utils.urljoin(self.base_path, self.id) - resp = session.delete(url, json=body) - self._translate_response(resp, has_body=False) - return self + response = session.delete(url, json=body) + return self._delete_response(response) class NodeDetail(Node): diff --git a/openstack/resource.py b/openstack/resource.py index 8ecc39e55..97d7f3a46 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -415,6 +415,7 @@ class Resource(dict): _uri = None _computed = None _original_body = None + _delete_response_class = None def __init__(self, _synchronized=False, connection=None, **attrs): """The base resource @@ -1240,16 +1241,8 @@ def delete(self, session, error_message=None): :raises: :exc:`~openstack.exceptions.ResourceNotFound` if the resource was not found. """ - if not self.allow_delete: - raise exceptions.MethodNotSupported(self, "delete") - request = self._prepare_request() - session = self._get_session(session) - microversion = self._get_microversion_for(session, 'delete') - - response = session.delete(request.url, - headers={"Accept": ""}, - microversion=microversion) + response = self._raw_delete(session) kwargs = {} if error_message: kwargs['error_message'] = error_message @@ -1257,6 +1250,19 @@ def delete(self, session, error_message=None): self._translate_response(response, has_body=False, **kwargs) return self + def _raw_delete(self, session): + if not self.allow_delete: + raise exceptions.MethodNotSupported(self, "delete") + + request = self._prepare_request() + session = self._get_session(session) + microversion = self._get_microversion_for(session, 'delete') + + return session.delete( + request.url, + headers={"Accept": ""}, + microversion=microversion) + @classmethod def list(cls, session, paginated=False, base_path=None, **params): """This method is a generator which yields resource objects. diff --git a/openstack/tests/functional/clustering/test_cluster.py b/openstack/tests/functional/clustering/test_cluster.py index e118a7873..e964d37d4 100644 --- a/openstack/tests/functional/clustering/test_cluster.py +++ b/openstack/tests/functional/clustering/test_cluster.py @@ -70,9 +70,10 @@ def setUp(self): assert isinstance(self.cluster, cluster.Cluster) def tearDown(self): - self.conn.clustering.delete_cluster(self.cluster.id) - self.conn.clustering.wait_for_delete(self.cluster, - wait=self._wait_for_timeout) + if self.cluster: + self.conn.clustering.delete_cluster(self.cluster.id) + self.conn.clustering.wait_for_delete( + self.cluster, wait=self._wait_for_timeout) test_network.delete_network(self.conn, self.network, self.subnet) @@ -100,3 +101,31 @@ def test_update(self): time.sleep(2) sot = self.conn.clustering.get_cluster(self.cluster) self.assertEqual(new_cluster_name, sot.name) + + def test_delete(self): + cluster_delete_action = self.conn.clustering.delete_cluster( + self.cluster.id) + + self.conn.clustering.wait_for_delete(self.cluster, + wait=self._wait_for_timeout) + + action = self.conn.clustering.get_action(cluster_delete_action.id) + self.assertEqual(action.target_id, self.cluster.id) + self.assertEqual(action.action, 'CLUSTER_DELETE') + self.assertEqual(action.status, 'SUCCEEDED') + + self.cluster = None + + def test_force_delete(self): + cluster_delete_action = self.conn.clustering.delete_cluster( + self.cluster.id, False, True) + + self.conn.clustering.wait_for_delete(self.cluster, + wait=self._wait_for_timeout) + + action = self.conn.clustering.get_action(cluster_delete_action.id) + self.assertEqual(action.target_id, self.cluster.id) + self.assertEqual(action.action, 'CLUSTER_DELETE') + self.assertEqual(action.status, 'SUCCEEDED') + + self.cluster = None diff --git a/openstack/tests/unit/clustering/v1/test_cluster.py b/openstack/tests/unit/clustering/v1/test_cluster.py index 2d0a22ada..ac6eab76f 100644 --- a/openstack/tests/unit/clustering/v1/test_cluster.py +++ b/openstack/tests/unit/clustering/v1/test_cluster.py @@ -311,14 +311,15 @@ def test_force_delete(self): sot = cluster.Cluster(**FAKE) resp = mock.Mock() - resp.headers = {} + fake_action_id = 'f1de9847-2382-4272-8e73-cab0bc194663' + resp.headers = {'Location': fake_action_id} resp.json = mock.Mock(return_value={"foo": "bar"}) resp.status_code = 200 sess = mock.Mock() sess.delete = mock.Mock(return_value=resp) res = sot.force_delete(sess) - self.assertEqual(sot, res) + self.assertEqual(fake_action_id, res.id) url = 'clusters/%s' % sot.id body = {'force': True} sess.delete.assert_called_once_with(url, json=body) diff --git a/openstack/tests/unit/clustering/v1/test_node.py b/openstack/tests/unit/clustering/v1/test_node.py index c2838a093..328065788 100644 --- a/openstack/tests/unit/clustering/v1/test_node.py +++ b/openstack/tests/unit/clustering/v1/test_node.py @@ -142,14 +142,15 @@ def test_force_delete(self): sot = node.Node(**FAKE) resp = mock.Mock() - resp.headers = {} + fake_action_id = 'f1de9847-2382-4272-8e73-cab0bc194663' + resp.headers = {'Location': fake_action_id} resp.json = mock.Mock(return_value={"foo": "bar"}) resp.status_code = 200 sess = mock.Mock() sess.delete = mock.Mock(return_value=resp) res = sot.force_delete(sess) - self.assertEqual(sot, res) + self.assertEqual(fake_action_id, res.id) url = 'nodes/%s' % sot.id body = {'force': True} sess.delete.assert_called_once_with(url, json=body) diff --git a/releasenotes/notes/clustering-resource-deletion-bed869ba47c2aac1.yaml b/releasenotes/notes/clustering-resource-deletion-bed869ba47c2aac1.yaml new file mode 100644 index 000000000..877a57171 --- /dev/null +++ b/releasenotes/notes/clustering-resource-deletion-bed869ba47c2aac1.yaml @@ -0,0 +1,13 @@ +--- +fixes: + - | + Fixed a regression in deleting Node and Cluster resources + in clustering caused by the addition of the ``location`` + property to all resource objects. Previously the delete + calls had directly returned the ``location`` field + returned in the headers from the clustering service pointing + to an Action resource that could be fetched to get status + on the delete operation. The delete calls now return an + Action resource directly that is correctly constructed + so that ``wait_for_status`` and ``wait_for_deleted`` + work as expected. From 41d6d33339d430a5ed033837dd844a36d251129c Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Wed, 30 Jan 2019 08:45:49 -0800 Subject: [PATCH 2360/3836] Add Octavia (load_balancer) load balancer failover This patch adds a Octavia (load_balancer) load balancer failover method. It also corrects the resource documentation for the Octavia stats resource classes. Depends-On: https://review.openstack.org/634344 Change-Id: I7384351a5b59b2d68d6a45c543449815adfe52f2 --- doc/source/user/proxies/load_balancer_v2.rst | 1 + .../resources/load_balancer/v2/listener.rst | 9 +++++ .../load_balancer/v2/load_balancer.rst | 18 +++++++++ openstack/load_balancer/v2/_proxy.py | 9 +++++ openstack/load_balancer/v2/load_balancer.py | 37 ++++++++++++++++++- .../load_balancer/v2/test_load_balancer.py | 7 ++++ .../unit/load_balancer/test_load_balancer.py | 13 +++++++ .../tests/unit/load_balancer/test_proxy.py | 7 ++++ ...-octavia-lb-failover-9a34c9577d78ad34.yaml | 4 ++ 9 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-octavia-lb-failover-9a34c9577d78ad34.yaml diff --git a/doc/source/user/proxies/load_balancer_v2.rst b/doc/source/user/proxies/load_balancer_v2.rst index 520274948..d60756010 100644 --- a/doc/source/user/proxies/load_balancer_v2.rst +++ b/doc/source/user/proxies/load_balancer_v2.rst @@ -22,6 +22,7 @@ Load Balancer Operations .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_load_balancer_statistics .. automethod:: openstack.load_balancer.v2._proxy.Proxy.load_balancers .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_load_balancer + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.failover_load_balancer Listener Operations ^^^^^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/load_balancer/v2/listener.rst b/doc/source/user/resources/load_balancer/v2/listener.rst index 099caf7d0..b94bc73b2 100644 --- a/doc/source/user/resources/load_balancer/v2/listener.rst +++ b/doc/source/user/resources/load_balancer/v2/listener.rst @@ -10,3 +10,12 @@ The ``Listener`` class inherits from :class:`~openstack.resource.Resource`. .. autoclass:: openstack.load_balancer.v2.listener.Listener :members: + +The ListenerStats Class +----------------------- + +The ``ListenerStats`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.load_balancer.v2.listener.ListenerStats + :members: diff --git a/doc/source/user/resources/load_balancer/v2/load_balancer.rst b/doc/source/user/resources/load_balancer/v2/load_balancer.rst index 0622c7cb4..9e1456029 100644 --- a/doc/source/user/resources/load_balancer/v2/load_balancer.rst +++ b/doc/source/user/resources/load_balancer/v2/load_balancer.rst @@ -10,3 +10,21 @@ The ``LoadBalancer`` class inherits from :class:`~openstack.resource.Resource`. .. autoclass:: openstack.load_balancer.v2.load_balancer.LoadBalancer :members: + +The LoadBalancerStats Class +--------------------------- + +The ``LoadBalancerStats`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.load_balancer.v2.load_balancer.LoadBalancerStats + :members: + +The LoadBalancerFailover Class +------------------------------ + +The ``LoadBalancerFailover`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.load_balancer.v2.load_balancer.LoadBalancerFailover + :members: diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index bc0fb9034..db4b74675 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -126,6 +126,15 @@ def wait_for_load_balancer(self, name_or_id, status='ACTIVE', return resource.wait_for_status(self, lb, status, failures, interval, wait, attribute='provisioning_status') + def failover_load_balancer(self, name_or_id, **attrs): + """Failover a load balancer + + :param name_or_id: The name or ID of a load balancer + + :returns: ``None`` + """ + return self._update(_lb.LoadBalancerFailover, lb_id=name_or_id) + def create_listener(self, **attrs): """Create a new listener from attributes diff --git a/openstack/load_balancer/v2/load_balancer.py b/openstack/load_balancer/v2/load_balancer.py index 9d7cc407c..77f8cb61a 100644 --- a/openstack/load_balancer/v2/load_balancer.py +++ b/openstack/load_balancer/v2/load_balancer.py @@ -33,7 +33,7 @@ class LoadBalancer(resource.Resource, resource.TagMixin): **resource.TagMixin._tag_query_parameters ) - #: Properties + # Properties #: The administrative state of the load balancer *Type: bool* is_admin_state_up = resource.Body('admin_state_up', type=bool) #: Timestamp when the load balancer was created @@ -113,3 +113,38 @@ class LoadBalancerStats(resource.Resource): request_errors = resource.Body('request_errors', type=int) #: The total connections handled. total_connections = resource.Body('total_connections', type=int) + + +class LoadBalancerFailover(resource.Resource): + base_path = '/lbaas/loadbalancers/%(lb_id)s/failover' + + # capabilities + allow_create = False + allow_fetch = False + allow_commit = True + allow_delete = False + allow_list = False + + requires_id = False + + # Properties + #: The ID of the load balancer. + lb_id = resource.URI('lb_id') + + # The parent commit method assumes there is a header or body change, + # which we do not have here. The default _update code path also has no + # way to pass has_body into this function, so overriding the method here. + def commit(self, session, base_path=None): + kwargs = {} + request = self._prepare_request(prepend_key=False, + base_path=base_path, + **kwargs) + session = self._get_session(session) + kwargs = {} + microversion = self._get_microversion_for(session, 'commit') + response = session.put(request.url, json=request.body, + headers=request.headers, + microversion=microversion, **kwargs) + self.microversion = microversion + self._translate_response(response, has_body=False) + return self diff --git a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py index 9a12c564b..a06b8e820 100644 --- a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py +++ b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py @@ -228,6 +228,13 @@ def test_lb_update(self): test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) self.assertEqual(self.LB_NAME, test_lb.name) + def test_lb_failover(self): + self.conn.load_balancer.failover_load_balancer(self.LB_ID) + self.conn.load_balancer.wait_for_load_balancer( + self.LB_ID, wait=self._wait_for_timeout) + test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) + self.assertEqual(self.LB_NAME, test_lb.name) + def test_listener_find(self): test_listener = self.conn.load_balancer.find_listener( self.LISTENER_NAME) diff --git a/openstack/tests/unit/load_balancer/test_load_balancer.py b/openstack/tests/unit/load_balancer/test_load_balancer.py index adab6706d..ef0c44f60 100644 --- a/openstack/tests/unit/load_balancer/test_load_balancer.py +++ b/openstack/tests/unit/load_balancer/test_load_balancer.py @@ -187,3 +187,16 @@ def test_make_it(self): test_load_balancer.request_errors) self.assertEqual(EXAMPLE_STATS['total_connections'], test_load_balancer.total_connections) + + +class TestLoadBalancerFailover(base.TestCase): + + def test_basic(self): + test_load_balancer = load_balancer.LoadBalancerFailover() + self.assertEqual('/lbaas/loadbalancers/%(lb_id)s/failover', + test_load_balancer.base_path) + self.assertFalse(test_load_balancer.allow_create) + self.assertFalse(test_load_balancer.allow_fetch) + self.assertFalse(test_load_balancer.allow_delete) + self.assertFalse(test_load_balancer.allow_list) + self.assertTrue(test_load_balancer.allow_commit) diff --git a/openstack/tests/unit/load_balancer/test_proxy.py b/openstack/tests/unit/load_balancer/test_proxy.py index a7bd1f838..222645b27 100644 --- a/openstack/tests/unit/load_balancer/test_proxy.py +++ b/openstack/tests/unit/load_balancer/test_proxy.py @@ -97,6 +97,13 @@ def test_load_balancer_update(self): self.verify_update(self.proxy.update_load_balancer, lb.LoadBalancer) + def test_load_balancer_failover(self): + self.verify_update(self.proxy.failover_load_balancer, + lb.LoadBalancerFailover, + value=[self.LB_ID], + expected_args=[], + expected_kwargs={'lb_id': self.LB_ID}) + def test_listeners(self): self.verify_list(self.proxy.listeners, listener.Listener) diff --git a/releasenotes/notes/add-octavia-lb-failover-9a34c9577d78ad34.yaml b/releasenotes/notes/add-octavia-lb-failover-9a34c9577d78ad34.yaml new file mode 100644 index 000000000..d87404ed9 --- /dev/null +++ b/releasenotes/notes/add-octavia-lb-failover-9a34c9577d78ad34.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Added Octavia (load_balancer) load balancer failover. From b27f0c579e53e81095c2b3fe353d047192afef89 Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Fri, 1 Feb 2019 12:20:04 -0800 Subject: [PATCH 2361/3836] Add Octavia (load_balancer) provider API support This patch adds the Octavia (load_balancer) provider API support. Specifically: providers and provider_flavor_capabilities Change-Id: Ieb8e2d2fc592d69eca9c8cf36f726a35db1cda22 --- doc/source/user/proxies/load_balancer_v2.rst | 8 ++ .../user/resources/load_balancer/index.rst | 1 + .../resources/load_balancer/v2/provider.rst | 21 ++++++ openstack/load_balancer/v2/_proxy.py | 16 ++++ openstack/load_balancer/v2/provider.py | 57 +++++++++++++++ .../load_balancer/v2/test_load_balancer.py | 14 ++++ .../tests/unit/load_balancer/test_provider.py | 73 +++++++++++++++++++ .../tests/unit/load_balancer/test_proxy.py | 11 +++ ...alancer-provider-api-08bcfb72ddf5b247.yaml | 4 + 9 files changed, 205 insertions(+) create mode 100644 doc/source/user/resources/load_balancer/v2/provider.rst create mode 100644 openstack/load_balancer/v2/provider.py create mode 100644 openstack/tests/unit/load_balancer/test_provider.py create mode 100644 releasenotes/notes/add-load-balancer-provider-api-08bcfb72ddf5b247.yaml diff --git a/doc/source/user/proxies/load_balancer_v2.rst b/doc/source/user/proxies/load_balancer_v2.rst index d60756010..cb87b2469 100644 --- a/doc/source/user/proxies/load_balancer_v2.rst +++ b/doc/source/user/proxies/load_balancer_v2.rst @@ -96,3 +96,11 @@ L7 Rule Operations .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_l7_rule .. automethod:: openstack.load_balancer.v2._proxy.Proxy.l7_rules .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_l7_rule + +Provider Operations +^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.load_balancer.v2._proxy.Proxy + + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.providers + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.provider_flavor_capabilities diff --git a/doc/source/user/resources/load_balancer/index.rst b/doc/source/user/resources/load_balancer/index.rst index 873f6d130..c85f68860 100644 --- a/doc/source/user/resources/load_balancer/index.rst +++ b/doc/source/user/resources/load_balancer/index.rst @@ -11,3 +11,4 @@ Load Balancer Resources v2/health_monitor v2/l7_policy v2/l7_rule + v2/provider diff --git a/doc/source/user/resources/load_balancer/v2/provider.rst b/doc/source/user/resources/load_balancer/v2/provider.rst new file mode 100644 index 000000000..b9c5af91a --- /dev/null +++ b/doc/source/user/resources/load_balancer/v2/provider.rst @@ -0,0 +1,21 @@ +openstack.load_balancer.v2.provider +=================================== + +.. automodule:: openstack.load_balancer.v2.provider + +The Provider Class +------------------ + +The ``Provider`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.load_balancer.v2.provider.Provider + :members: + +The Provider Flavor Capabilities Class +-------------------------------------- + +The ``ProviderFlavorCapabilities`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.load_balancer.v2.provider.ProviderFlavorCapabilities + :members: diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index db4b74675..42aee4129 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -17,6 +17,7 @@ from openstack.load_balancer.v2 import load_balancer as _lb from openstack.load_balancer.v2 import member as _member from openstack.load_balancer.v2 import pool as _pool +from openstack.load_balancer.v2 import provider as _provider from openstack.load_balancer.v2 import quota as _quota from openstack import proxy from openstack import resource @@ -779,3 +780,18 @@ def delete_quota(self, quota, ignore_missing=True): :returns: ``None`` """ self._delete(_quota.Quota, quota, ignore_missing=ignore_missing) + + def providers(self, **query): + """Retrieve a generator of providers + + :returns: A generator of providers instances + """ + return self._list(_provider.Provider, **query) + + def provider_flavor_capabilities(self, provider, **query): + """Retrieve a generator of provider flavor capabilities + + :returns: A generator of provider flavor capabilities instances + """ + return self._list(_provider.ProviderFlavorCapabilities, + provider=provider, **query) diff --git a/openstack/load_balancer/v2/provider.py b/openstack/load_balancer/v2/provider.py new file mode 100644 index 000000000..5ff6e0c6e --- /dev/null +++ b/openstack/load_balancer/v2/provider.py @@ -0,0 +1,57 @@ +# Copyright 2019 Rackspace, US 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. + +from openstack import resource + + +class Provider(resource.Resource): + resources_key = 'providers' + base_path = '/lbaas/providers' + + # capabilities + allow_create = False + allow_fetch = False + allow_commit = False + allow_delete = False + allow_list = True + + _query_mapping = resource.QueryParameters('description', 'name') + + # Properties + #: The provider name. + name = resource.Body('name') + #: The provider description. + description = resource.Body('description') + + +class ProviderFlavorCapabilities(resource.Resource): + resources_key = 'flavor_capabilities' + base_path = '/lbaas/providers/%(provider)s/flavor_capabilities' + + # capabilities + allow_create = False + allow_fetch = False + allow_commit = False + allow_delete = False + allow_list = True + + _query_mapping = resource.QueryParameters('description', 'name') + + # Properties + #: The provider name to query. + provider = resource.URI('provider') + #: The provider name. + name = resource.Body('name') + #: The provider description. + description = resource.Body('description') diff --git a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py index a06b8e820..71d65b6a9 100644 --- a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py +++ b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py @@ -47,6 +47,7 @@ class TestLoadBalancer(base.BaseFunctionalTest): COMPARE_TYPE = 'CONTAINS' L7RULE_TYPE = 'HOST_NAME' L7RULE_VALUE = 'example' + AMPHORA = 'amphora' @classmethod def setUpClass(cls): @@ -474,3 +475,16 @@ def test_quota_update(self): def test_default_quota(self): self.conn.load_balancer.get_quota_default() + + def test_providers(self): + providers = self.conn.load_balancer.providers() + # Make sure our default provider is in the list + self.assertTrue( + any(prov['name'] == self.AMPHORA for prov in providers)) + + def test_provider_flavor_capabilities(self): + capabilities = self.conn.load_balancer.provider_flavor_capabilities( + self.AMPHORA) + # Make sure a known capability is in the default provider + self.assertTrue(any( + cap['name'] == 'loadbalancer_topology' for cap in capabilities)) diff --git a/openstack/tests/unit/load_balancer/test_provider.py b/openstack/tests/unit/load_balancer/test_provider.py new file mode 100644 index 000000000..59b8f4308 --- /dev/null +++ b/openstack/tests/unit/load_balancer/test_provider.py @@ -0,0 +1,73 @@ +# Copyright 2019 Rackspace, US 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. + +from openstack.tests.unit import base + +from openstack.load_balancer.v2 import provider + +EXAMPLE = { + 'name': 'best', + 'description': 'The best provider' +} + + +class TestProvider(base.TestCase): + + def test_basic(self): + test_provider = provider.Provider() + self.assertEqual('providers', test_provider.resources_key) + self.assertEqual('/lbaas/providers', test_provider.base_path) + self.assertFalse(test_provider.allow_create) + self.assertFalse(test_provider.allow_fetch) + self.assertFalse(test_provider.allow_commit) + self.assertFalse(test_provider.allow_delete) + self.assertTrue(test_provider.allow_list) + + def test_make_it(self): + test_provider = provider.Provider(**EXAMPLE) + self.assertEqual(EXAMPLE['name'], test_provider.name) + self.assertEqual(EXAMPLE['description'], test_provider.description) + + self.assertDictEqual( + {'limit': 'limit', + 'marker': 'marker', + 'name': 'name', + 'description': 'description'}, + test_provider._query_mapping._mapping) + + +class TestProviderFlavorCapabilities(base.TestCase): + + def test_basic(self): + test_flav_cap = provider.ProviderFlavorCapabilities() + self.assertEqual('flavor_capabilities', test_flav_cap.resources_key) + self.assertEqual('/lbaas/providers/%(provider)s/flavor_capabilities', + test_flav_cap.base_path) + self.assertFalse(test_flav_cap.allow_create) + self.assertFalse(test_flav_cap.allow_fetch) + self.assertFalse(test_flav_cap.allow_commit) + self.assertFalse(test_flav_cap.allow_delete) + self.assertTrue(test_flav_cap.allow_list) + + def test_make_it(self): + test_flav_cap = provider.ProviderFlavorCapabilities(**EXAMPLE) + self.assertEqual(EXAMPLE['name'], test_flav_cap.name) + self.assertEqual(EXAMPLE['description'], test_flav_cap.description) + + self.assertDictEqual( + {'limit': 'limit', + 'marker': 'marker', + 'name': 'name', + 'description': 'description'}, + test_flav_cap._query_mapping._mapping) diff --git a/openstack/tests/unit/load_balancer/test_proxy.py b/openstack/tests/unit/load_balancer/test_proxy.py index 222645b27..d2d044038 100644 --- a/openstack/tests/unit/load_balancer/test_proxy.py +++ b/openstack/tests/unit/load_balancer/test_proxy.py @@ -21,6 +21,7 @@ from openstack.load_balancer.v2 import load_balancer as lb from openstack.load_balancer.v2 import member from openstack.load_balancer.v2 import pool +from openstack.load_balancer.v2 import provider from openstack.load_balancer.v2 import quota from openstack import proxy as proxy_base from openstack.tests.unit import test_proxy_base @@ -32,6 +33,7 @@ class TestLoadBalancerProxy(test_proxy_base.TestProxyBase): LISTENER_ID = uuid.uuid4() POOL_ID = uuid.uuid4() L7_POLICY_ID = uuid.uuid4() + AMPHORA = 'amphora' def setUp(self): super(TestLoadBalancerProxy, self).setUp() @@ -308,3 +310,12 @@ def test_quota_delete(self): def test_quota_delete_ignore(self): self.verify_delete(self.proxy.delete_quota, quota.Quota, True) + + def test_providers(self): + self.verify_list(self.proxy.providers, provider.Provider) + + def test_provider_flavor_capabilities(self): + self.verify_list(self.proxy.provider_flavor_capabilities, + provider.ProviderFlavorCapabilities, + method_args=[self.AMPHORA], + expected_kwargs={'provider': self.AMPHORA}) diff --git a/releasenotes/notes/add-load-balancer-provider-api-08bcfb72ddf5b247.yaml b/releasenotes/notes/add-load-balancer-provider-api-08bcfb72ddf5b247.yaml new file mode 100644 index 000000000..906e9026a --- /dev/null +++ b/releasenotes/notes/add-load-balancer-provider-api-08bcfb72ddf5b247.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Adds Octavia (load_balancer) support for the providers APIs. From 1b884947bc7d1be4500cf515abe8528ba2af0bef Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Fri, 1 Feb 2019 14:36:29 -0800 Subject: [PATCH 2362/3836] Add Octavia (load_balancer) flavor profile API This patch adds the Octavia (load_balancer) flavor profile API support. Change-Id: I1358d89b07917726451bf311c1323e57f381f853 --- doc/source/user/proxies/load_balancer_v2.rst | 12 +++ .../user/resources/load_balancer/index.rst | 1 + .../load_balancer/v2/flavor_profile.rst | 13 +++ openstack/load_balancer/v2/_proxy.py | 83 +++++++++++++++++++ openstack/load_balancer/v2/flavor_profile.py | 42 ++++++++++ .../load_balancer/v2/test_load_balancer.py | 45 ++++++++++ .../unit/load_balancer/test_flavor_profile.py | 55 ++++++++++++ .../tests/unit/load_balancer/test_proxy.py | 25 ++++++ ...r-flavor-profile-api-e5a15157563eb75f.yaml | 4 + 9 files changed, 280 insertions(+) create mode 100644 doc/source/user/resources/load_balancer/v2/flavor_profile.rst create mode 100644 openstack/load_balancer/v2/flavor_profile.py create mode 100644 openstack/tests/unit/load_balancer/test_flavor_profile.py create mode 100644 releasenotes/notes/add-load-balancer-flavor-profile-api-e5a15157563eb75f.yaml diff --git a/doc/source/user/proxies/load_balancer_v2.rst b/doc/source/user/proxies/load_balancer_v2.rst index cb87b2469..65b5afa71 100644 --- a/doc/source/user/proxies/load_balancer_v2.rst +++ b/doc/source/user/proxies/load_balancer_v2.rst @@ -104,3 +104,15 @@ Provider Operations .. automethod:: openstack.load_balancer.v2._proxy.Proxy.providers .. automethod:: openstack.load_balancer.v2._proxy.Proxy.provider_flavor_capabilities + +Flavor Profile Operations +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.load_balancer.v2._proxy.Proxy + + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.create_flavor_profile + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_flavor_profile + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.flavor_profiles + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_flavor_profile + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_flavor_profile + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_flavor_profile diff --git a/doc/source/user/resources/load_balancer/index.rst b/doc/source/user/resources/load_balancer/index.rst index c85f68860..e06159c25 100644 --- a/doc/source/user/resources/load_balancer/index.rst +++ b/doc/source/user/resources/load_balancer/index.rst @@ -12,3 +12,4 @@ Load Balancer Resources v2/l7_policy v2/l7_rule v2/provider + v2/flavor_profile diff --git a/doc/source/user/resources/load_balancer/v2/flavor_profile.rst b/doc/source/user/resources/load_balancer/v2/flavor_profile.rst new file mode 100644 index 000000000..8a702f9a9 --- /dev/null +++ b/doc/source/user/resources/load_balancer/v2/flavor_profile.rst @@ -0,0 +1,13 @@ +openstack.load_balancer.v2.flavor_profile +========================================= + +.. automodule:: openstack.load_balancer.v2.flavor_profile + +The FlavorProfile Class +----------------------- + +The ``FlavorProfile`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.load_balancer.v2.flavor_profile.FlavorProfile + :members: diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index 42aee4129..6741a0243 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.load_balancer.v2 import flavor_profile as _flavor_profile from openstack.load_balancer.v2 import health_monitor as _hm from openstack.load_balancer.v2 import l7_policy as _l7policy from openstack.load_balancer.v2 import l7_rule as _l7rule @@ -795,3 +796,85 @@ def provider_flavor_capabilities(self, provider, **query): """ return self._list(_provider.ProviderFlavorCapabilities, provider=provider, **query) + + def create_flavor_profile(self, **attrs): + """Create a new flavor profile from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.load_balancer.v2. + flavor_profile.FlavorProfile`, + comprised of the properties on the + FlavorProfile class. + + :returns: The results of profile creation creation + :rtype: :class:`~openstack.load_balancer.v2.flavor_profile. + FlavorProfile` + """ + return self._create(_flavor_profile.FlavorProfile, **attrs) + + def get_flavor_profile(self, *attrs): + """Get a flavor profile + + :param flavor_profile: The value can be the name of a flavor profile + or :class:`~openstack.load_balancer.v2.flavor_profile. + FlavorProfile` instance. + + :returns: One + :class:`~openstack.load_balancer.v2.flavor_profile.FlavorProfile` + """ + return self._get(_flavor_profile.FlavorProfile, *attrs) + + def flavor_profiles(self, **query): + """Retrieve a generator of flavor profiles + + :returns: A generator of flavor profiles instances + """ + return self._list(_flavor_profile.FlavorProfile, **query) + + def delete_flavor_profile(self, flavor_profile, ignore_missing=True): + """Delete a flavor profile + + :param flavor_profile: The flavor_profile can be either the name or a + :class:`~openstack.load_balancer.v2.flavor_profile.FlavorProfile` + instance + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the flavor profile does not exist. + When set to ``True``, no exception will be set when attempting to + delete a nonexistent load balancer. + + :returns: ``None`` + """ + self._delete(_flavor_profile.FlavorProfile, flavor_profile, + ignore_missing=ignore_missing) + + def find_flavor_profile(self, name_or_id, ignore_missing=True): + """Find a single flavor profile + + :param name_or_id: The name or ID of a flavor profile + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the flavor profile does not exist. + When set to ``True``, no exception will be set when attempting + to delete a nonexistent flavor profile. + + :returns: ``None`` + """ + return self._find(_flavor_profile.FlavorProfile, name_or_id, + ignore_missing=ignore_missing) + + def update_flavor_profile(self, flavor_profile, **attrs): + """Update a flavor profile + + :param flavor_profile: The flavor_profile can be either the name or a + :class:`~openstack.load_balancer.v2.flavor_profile.FlavorProfile` + instance + :param dict attrs: The attributes to update on the flavor profile + represented by ``flavor_profile``. + + :returns: The updated flavor profile + :rtype: :class:`~openstack.load_balancer.v2.flavor_profile. + FlavorProfile` + """ + return self._update(_flavor_profile.FlavorProfile, flavor_profile, + **attrs) diff --git a/openstack/load_balancer/v2/flavor_profile.py b/openstack/load_balancer/v2/flavor_profile.py new file mode 100644 index 000000000..bdb503170 --- /dev/null +++ b/openstack/load_balancer/v2/flavor_profile.py @@ -0,0 +1,42 @@ +# Copyright 2019 Rackspace, US 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. + +from openstack import resource + + +class FlavorProfile(resource.Resource): + resource_key = 'flavorprofile' + resources_key = 'flavorprofiles' + base_path = '/lbaas/flavorprofiles' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + _query_mapping = resource.QueryParameters( + 'id', 'name', 'provider_name', 'flavor_data' + ) + + # Properties + #: The ID of the flavor profile. + id = resource.Body('id') + #: The name of the flavor profile. + name = resource.Body('name') + #: The provider this flavor profile is for. + provider_name = resource.Body('provider_name') + #: The JSON string containing the flavor metadata. + flavor_data = resource.Body('flavor_data') diff --git a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py index 71d65b6a9..08a340477 100644 --- a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py +++ b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py @@ -12,6 +12,7 @@ import os +from openstack.load_balancer.v2 import flavor_profile from openstack.load_balancer.v2 import health_monitor from openstack.load_balancer.v2 import l7_policy from openstack.load_balancer.v2 import l7_rule @@ -33,6 +34,7 @@ class TestLoadBalancer(base.BaseFunctionalTest): POOL_ID = None VIP_SUBNET_ID = None PROJECT_ID = None + FLAVOR_PROFILE_ID = None PROTOCOL = 'HTTP' PROTOCOL_PORT = 80 LB_ALGORITHM = 'ROUND_ROBIN' @@ -48,6 +50,7 @@ class TestLoadBalancer(base.BaseFunctionalTest): L7RULE_TYPE = 'HOST_NAME' L7RULE_VALUE = 'example' AMPHORA = 'amphora' + FLAVOR_DATA = '{"loadbalancer_topology": "SINGLE"}' @classmethod def setUpClass(cls): @@ -71,6 +74,7 @@ def setUp(self): self.MEMBER_NAME = self.getUniqueString() self.POOL_NAME = self.getUniqueString() self.UPDATE_NAME = self.getUniqueString() + self.FLAVOR_PROFILE_NAME = self.getUniqueString() subnets = list(self.conn.network.subnets()) self.VIP_SUBNET_ID = subnets[0].id self.PROJECT_ID = self.conn.session.get_project_id() @@ -82,6 +86,14 @@ def setUp(self): 'member': 100}) assert isinstance(test_quota, quota.Quota) self.assertEqual(self.PROJECT_ID, test_quota.id) + + test_profile = self.conn.load_balancer.create_flavor_profile( + name=self.FLAVOR_PROFILE_NAME, provider_name=self.AMPHORA, + flavor_data=self.FLAVOR_DATA) + assert isinstance(test_profile, flavor_profile.FlavorProfile) + self.assertEqual(self.FLAVOR_PROFILE_NAME, test_profile.name) + self.FLAVOR_PROFILE_ID = test_profile.id + test_lb = self.conn.load_balancer.create_load_balancer( name=self.LB_NAME, vip_subnet_id=self.VIP_SUBNET_ID, project_id=self.PROJECT_ID) @@ -191,6 +203,9 @@ def tearDown(self): self.LB_ID, ignore_missing=False) super(TestLoadBalancer, self).tearDown() + self.conn.load_balancer.delete_flavor_profile(self.FLAVOR_PROFILE_ID, + ignore_missing=False) + def test_lb_find(self): test_lb = self.conn.load_balancer.find_load_balancer(self.LB_NAME) self.assertEqual(self.LB_ID, test_lb.id) @@ -488,3 +503,33 @@ def test_provider_flavor_capabilities(self): # Make sure a known capability is in the default provider self.assertTrue(any( cap['name'] == 'loadbalancer_topology' for cap in capabilities)) + + def test_flavor_profile_find(self): + test_profile = self.conn.load_balancer.find_flavor_profile( + self.FLAVOR_PROFILE_NAME) + self.assertEqual(self.FLAVOR_PROFILE_ID, test_profile.id) + + def test_flavor_profile_get(self): + test_flavor_profile = self.conn.load_balancer.get_flavor_profile( + self.FLAVOR_PROFILE_ID) + self.assertEqual(self.FLAVOR_PROFILE_NAME, test_flavor_profile.name) + self.assertEqual(self.FLAVOR_PROFILE_ID, test_flavor_profile.id) + self.assertEqual(self.AMPHORA, test_flavor_profile.provider_name) + self.assertEqual(self.FLAVOR_DATA, test_flavor_profile.flavor_data) + + def test_flavor_profile_list(self): + names = [fv.name for fv in self.conn.load_balancer.flavor_profiles()] + self.assertIn(self.FLAVOR_PROFILE_NAME, names) + + def test_flavor_profile_update(self): + self.conn.load_balancer.update_flavor_profile( + self.FLAVOR_PROFILE_ID, name=self.UPDATE_NAME) + test_flavor_profile = self.conn.load_balancer.get_flavor_profile( + self.FLAVOR_PROFILE_ID) + self.assertEqual(self.UPDATE_NAME, test_flavor_profile.name) + + self.conn.load_balancer.update_flavor_profile( + self.FLAVOR_PROFILE_ID, name=self.FLAVOR_PROFILE_NAME) + test_flavor_profile = self.conn.load_balancer.get_flavor_profile( + self.FLAVOR_PROFILE_ID) + self.assertEqual(self.FLAVOR_PROFILE_NAME, test_flavor_profile.name) diff --git a/openstack/tests/unit/load_balancer/test_flavor_profile.py b/openstack/tests/unit/load_balancer/test_flavor_profile.py new file mode 100644 index 000000000..7b7ea43c4 --- /dev/null +++ b/openstack/tests/unit/load_balancer/test_flavor_profile.py @@ -0,0 +1,55 @@ +# Copyright 2019 Rackspace, US 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. + +from openstack.tests.unit import base +import uuid + +from openstack.load_balancer.v2 import flavor_profile + +IDENTIFIER = uuid.uuid4() +EXAMPLE = { + 'id': IDENTIFIER, + 'name': 'acidic', + 'provider_name': 'best', + 'flavor_data': '{"loadbalancer_topology": "SINGLE"}'} + + +class TestFlavorProfile(base.TestCase): + + def test_basic(self): + test_profile = flavor_profile.FlavorProfile() + self.assertEqual('flavorprofile', test_profile.resource_key) + self.assertEqual('flavorprofiles', test_profile.resources_key) + self.assertEqual('/lbaas/flavorprofiles', test_profile.base_path) + self.assertTrue(test_profile.allow_create) + self.assertTrue(test_profile.allow_fetch) + self.assertTrue(test_profile.allow_commit) + self.assertTrue(test_profile.allow_delete) + self.assertTrue(test_profile.allow_list) + + def test_make_it(self): + test_profile = flavor_profile.FlavorProfile(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], test_profile.id) + self.assertEqual(EXAMPLE['name'], test_profile.name) + self.assertEqual(EXAMPLE['provider_name'], test_profile.provider_name) + self.assertEqual(EXAMPLE['flavor_data'], test_profile.flavor_data) + + self.assertDictEqual( + {'limit': 'limit', + 'marker': 'marker', + 'id': 'id', + 'name': 'name', + 'provider_name': 'provider_name', + 'flavor_data': 'flavor_data'}, + test_profile._query_mapping._mapping) diff --git a/openstack/tests/unit/load_balancer/test_proxy.py b/openstack/tests/unit/load_balancer/test_proxy.py index d2d044038..c257009ab 100644 --- a/openstack/tests/unit/load_balancer/test_proxy.py +++ b/openstack/tests/unit/load_balancer/test_proxy.py @@ -14,6 +14,7 @@ import mock from openstack.load_balancer.v2 import _proxy +from openstack.load_balancer.v2 import flavor_profile from openstack.load_balancer.v2 import health_monitor from openstack.load_balancer.v2 import l7_policy from openstack.load_balancer.v2 import l7_rule @@ -319,3 +320,27 @@ def test_provider_flavor_capabilities(self): provider.ProviderFlavorCapabilities, method_args=[self.AMPHORA], expected_kwargs={'provider': self.AMPHORA}) + + def test_flavor_profiles(self): + self.verify_list(self.proxy.flavor_profiles, + flavor_profile.FlavorProfile) + + def test_flavor_profile_get(self): + self.verify_get(self.proxy.get_flavor_profile, + flavor_profile.FlavorProfile) + + def test_flavor_profile_create(self): + self.verify_create(self.proxy.create_flavor_profile, + flavor_profile.FlavorProfile) + + def test_flavor_profile_delete(self): + self.verify_delete(self.proxy.delete_flavor_profile, + flavor_profile.FlavorProfile, True) + + def test_flavor_profile_find(self): + self.verify_find(self.proxy.find_flavor_profile, + flavor_profile.FlavorProfile) + + def test_flavor_profile_update(self): + self.verify_update(self.proxy.update_flavor_profile, + flavor_profile.FlavorProfile) diff --git a/releasenotes/notes/add-load-balancer-flavor-profile-api-e5a15157563eb75f.yaml b/releasenotes/notes/add-load-balancer-flavor-profile-api-e5a15157563eb75f.yaml new file mode 100644 index 000000000..1d674cc51 --- /dev/null +++ b/releasenotes/notes/add-load-balancer-flavor-profile-api-e5a15157563eb75f.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Adds Octavia (load_balancer) support for the flavor profile APIs. From 85f1e4c7f18e010a24f82d86c26c42257a5c392c Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Fri, 1 Feb 2019 16:41:11 -0800 Subject: [PATCH 2363/3836] Add Octavia (load_balancer) flavor API This patch adds the Octavia (load_balancer) flavor API support. Change-Id: If786149c9d411b715740f893b6263b1bb47ba54f --- doc/source/user/proxies/load_balancer_v2.rst | 12 +++ .../user/resources/load_balancer/index.rst | 1 + .../resources/load_balancer/v2/flavor.rst | 12 +++ openstack/load_balancer/v2/_proxy.py | 77 ++++++++++++++++++- openstack/load_balancer/v2/flavor.py | 44 +++++++++++ .../load_balancer/v2/test_load_balancer.py | 40 ++++++++++ .../tests/unit/load_balancer/test_flavor.py | 60 +++++++++++++++ .../tests/unit/load_balancer/test_proxy.py | 19 +++++ ...-balancer-flavor-api-d2598e30347a19fc.yaml | 4 + 9 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 doc/source/user/resources/load_balancer/v2/flavor.rst create mode 100644 openstack/load_balancer/v2/flavor.py create mode 100644 openstack/tests/unit/load_balancer/test_flavor.py create mode 100644 releasenotes/notes/add-load-balancer-flavor-api-d2598e30347a19fc.yaml diff --git a/doc/source/user/proxies/load_balancer_v2.rst b/doc/source/user/proxies/load_balancer_v2.rst index 65b5afa71..ceecfcfd3 100644 --- a/doc/source/user/proxies/load_balancer_v2.rst +++ b/doc/source/user/proxies/load_balancer_v2.rst @@ -116,3 +116,15 @@ Flavor Profile Operations .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_flavor_profile .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_flavor_profile .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_flavor_profile + +Flavor Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.load_balancer.v2._proxy.Proxy + + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.create_flavor + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_flavor + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.flavors + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_flavor + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_flavor + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_flavor diff --git a/doc/source/user/resources/load_balancer/index.rst b/doc/source/user/resources/load_balancer/index.rst index e06159c25..dbb8226f2 100644 --- a/doc/source/user/resources/load_balancer/index.rst +++ b/doc/source/user/resources/load_balancer/index.rst @@ -13,3 +13,4 @@ Load Balancer Resources v2/l7_rule v2/provider v2/flavor_profile + v2/flavor diff --git a/doc/source/user/resources/load_balancer/v2/flavor.rst b/doc/source/user/resources/load_balancer/v2/flavor.rst new file mode 100644 index 000000000..57b97ba0b --- /dev/null +++ b/doc/source/user/resources/load_balancer/v2/flavor.rst @@ -0,0 +1,12 @@ +openstack.load_balancer.v2.flavor +================================= + +.. automodule:: openstack.load_balancer.v2.flavor + +The Flavor Class +---------------- + +The ``Flavor`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.load_balancer.v2.flavor.Flavor + :members: diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index 6741a0243..edf42be30 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.load_balancer.v2 import flavor as _flavor from openstack.load_balancer.v2 import flavor_profile as _flavor_profile from openstack.load_balancer.v2 import health_monitor as _hm from openstack.load_balancer.v2 import l7_policy as _l7policy @@ -841,7 +842,7 @@ def delete_flavor_profile(self, flavor_profile, ignore_missing=True): :class:`~openstack.exceptions.ResourceNotFound` will be raised when the flavor profile does not exist. When set to ``True``, no exception will be set when attempting to - delete a nonexistent load balancer. + delete a nonexistent flavor profile. :returns: ``None`` """ @@ -878,3 +879,77 @@ def update_flavor_profile(self, flavor_profile, **attrs): """ return self._update(_flavor_profile.FlavorProfile, flavor_profile, **attrs) + + def create_flavor(self, **attrs): + """Create a new flavor from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.load_balancer.v2. + flavor.Flavor`, comprised of the properties on the + Flavorclass. + + :returns: The results of flavor creation creation + :rtype: :class:`~openstack.load_balancer.v2.flavor.Flavor` + """ + return self._create(_flavor.Flavor, **attrs) + + def get_flavor(self, *attrs): + """Get a flavor + + :param flavor: The value can be the name of a flavor + or :class:`~openstack.load_balancer.v2.flavor.Flavor` instance. + + :returns: One + :class:`~openstack.load_balancer.v2.flavor.Flavor` + """ + return self._get(_flavor.Flavor, *attrs) + + def flavors(self, **query): + """Retrieve a generator of flavors + + :returns: A generator of flavor instances + """ + return self._list(_flavor.Flavor, **query) + + def delete_flavor(self, flavor, ignore_missing=True): + """Delete a flavor + + :param flavor: The flavorcan be either the name or a + :class:`~openstack.load_balancer.v2.flavor.Flavor` instance + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the flavor does not exist. + When set to ``True``, no exception will be set when attempting to + delete a nonexistent flavor. + + :returns: ``None`` + """ + self._delete(_flavor.Flavor, flavor, ignore_missing=ignore_missing) + + def find_flavor(self, name_or_id, ignore_missing=True): + """Find a single flavor + + :param name_or_id: The name or ID of a flavor + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the flavor does not exist. + When set to ``True``, no exception will be set when attempting + to delete a nonexistent flavor. + + :returns: ``None`` + """ + return self._find(_flavor.Flavor, name_or_id, + ignore_missing=ignore_missing) + + def update_flavor(self, flavor, **attrs): + """Update a flavor + + :param flavor: The flavor can be either the name or a + :class:`~openstack.load_balancer.v2.flavor.Flavor` instance + :param dict attrs: The attributes to update on the flavor + represented by ``flavor``. + + :returns: The updated flavor + :rtype: :class:`~openstack.load_balancer.v2.flavor.Flavor` + """ + return self._update(_flavor.Flavor, flavor, **attrs) diff --git a/openstack/load_balancer/v2/flavor.py b/openstack/load_balancer/v2/flavor.py new file mode 100644 index 000000000..799d97578 --- /dev/null +++ b/openstack/load_balancer/v2/flavor.py @@ -0,0 +1,44 @@ +# Copyright 2019 Rackspace, US 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. + +from openstack import resource + + +class Flavor(resource.Resource): + resource_key = 'flavor' + resources_key = 'flavors' + base_path = '/lbaas/flavors' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + _query_mapping = resource.QueryParameters( + 'id', 'name', 'description', 'flavor_profile_id', is_enabled='enabled' + ) + + # Properties + #: The ID of the flavor. + id = resource.Body('id') + #: The name of the flavor. + name = resource.Body('name') + #: The flavor description. + description = resource.Body('description') + #: The associated flavor profile ID + flavor_profile_id = resource.Body('flavor_profile_id') + #: Whether the flavor is enabled for use or not. + is_enabled = resource.Body('enabled') diff --git a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py index 08a340477..74d55b30a 100644 --- a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py +++ b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py @@ -12,6 +12,7 @@ import os +from openstack.load_balancer.v2 import flavor from openstack.load_balancer.v2 import flavor_profile from openstack.load_balancer.v2 import health_monitor from openstack.load_balancer.v2 import l7_policy @@ -35,6 +36,7 @@ class TestLoadBalancer(base.BaseFunctionalTest): VIP_SUBNET_ID = None PROJECT_ID = None FLAVOR_PROFILE_ID = None + FLAVOR_ID = None PROTOCOL = 'HTTP' PROTOCOL_PORT = 80 LB_ALGORITHM = 'ROUND_ROBIN' @@ -51,6 +53,7 @@ class TestLoadBalancer(base.BaseFunctionalTest): L7RULE_VALUE = 'example' AMPHORA = 'amphora' FLAVOR_DATA = '{"loadbalancer_topology": "SINGLE"}' + DESCRIPTION = 'Test description' @classmethod def setUpClass(cls): @@ -75,6 +78,7 @@ def setUp(self): self.POOL_NAME = self.getUniqueString() self.UPDATE_NAME = self.getUniqueString() self.FLAVOR_PROFILE_NAME = self.getUniqueString() + self.FLAVOR_NAME = self.getUniqueString() subnets = list(self.conn.network.subnets()) self.VIP_SUBNET_ID = subnets[0].id self.PROJECT_ID = self.conn.session.get_project_id() @@ -94,6 +98,13 @@ def setUp(self): self.assertEqual(self.FLAVOR_PROFILE_NAME, test_profile.name) self.FLAVOR_PROFILE_ID = test_profile.id + test_flavor = self.conn.load_balancer.create_flavor( + name=self.FLAVOR_NAME, flavor_profile_id=self.FLAVOR_PROFILE_ID, + is_enabled=True, description=self.DESCRIPTION) + assert isinstance(test_flavor, flavor.Flavor) + self.assertEqual(self.FLAVOR_NAME, test_flavor.name) + self.FLAVOR_ID = test_flavor.id + test_lb = self.conn.load_balancer.create_load_balancer( name=self.LB_NAME, vip_subnet_id=self.VIP_SUBNET_ID, project_id=self.PROJECT_ID) @@ -203,6 +214,9 @@ def tearDown(self): self.LB_ID, ignore_missing=False) super(TestLoadBalancer, self).tearDown() + self.conn.load_balancer.delete_flavor(self.FLAVOR_ID, + ignore_missing=False) + self.conn.load_balancer.delete_flavor_profile(self.FLAVOR_PROFILE_ID, ignore_missing=False) @@ -533,3 +547,29 @@ def test_flavor_profile_update(self): test_flavor_profile = self.conn.load_balancer.get_flavor_profile( self.FLAVOR_PROFILE_ID) self.assertEqual(self.FLAVOR_PROFILE_NAME, test_flavor_profile.name) + + def test_flavor_find(self): + test_flavor = self.conn.load_balancer.find_flavor(self.FLAVOR_NAME) + self.assertEqual(self.FLAVOR_ID, test_flavor.id) + + def test_flavor_get(self): + test_flavor = self.conn.load_balancer.get_flavor(self.FLAVOR_ID) + self.assertEqual(self.FLAVOR_NAME, test_flavor.name) + self.assertEqual(self.FLAVOR_ID, test_flavor.id) + self.assertEqual(self.DESCRIPTION, test_flavor.description) + self.assertEqual(self.FLAVOR_PROFILE_ID, test_flavor.flavor_profile_id) + + def test_flavor_list(self): + names = [fv.name for fv in self.conn.load_balancer.flavors()] + self.assertIn(self.FLAVOR_NAME, names) + + def test_flavor_update(self): + self.conn.load_balancer.update_flavor( + self.FLAVOR_ID, name=self.UPDATE_NAME) + test_flavor = self.conn.load_balancer.get_flavor(self.FLAVOR_ID) + self.assertEqual(self.UPDATE_NAME, test_flavor.name) + + self.conn.load_balancer.update_flavor( + self.FLAVOR_ID, name=self.FLAVOR_NAME) + test_flavor = self.conn.load_balancer.get_flavor(self.FLAVOR_ID) + self.assertEqual(self.FLAVOR_NAME, test_flavor.name) diff --git a/openstack/tests/unit/load_balancer/test_flavor.py b/openstack/tests/unit/load_balancer/test_flavor.py new file mode 100644 index 000000000..5d5069a75 --- /dev/null +++ b/openstack/tests/unit/load_balancer/test_flavor.py @@ -0,0 +1,60 @@ +# Copyright 2019 Rackspace, US 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. + +from openstack.tests.unit import base +import uuid + +from openstack.load_balancer.v2 import flavor + +IDENTIFIER = uuid.uuid4() +FLAVOR_PROFILE_ID = uuid.uuid4() +EXAMPLE = { + 'id': IDENTIFIER, + 'name': 'strawberry', + 'description': 'tasty', + 'is_enabled': False, + 'flavor_profile_id': FLAVOR_PROFILE_ID} + + +class TestFlavor(base.TestCase): + + def test_basic(self): + test_flavor = flavor.Flavor() + self.assertEqual('flavor', test_flavor.resource_key) + self.assertEqual('flavors', test_flavor.resources_key) + self.assertEqual('/lbaas/flavors', test_flavor.base_path) + self.assertTrue(test_flavor.allow_create) + self.assertTrue(test_flavor.allow_fetch) + self.assertTrue(test_flavor.allow_commit) + self.assertTrue(test_flavor.allow_delete) + self.assertTrue(test_flavor.allow_list) + + def test_make_it(self): + test_flavor = flavor.Flavor(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], test_flavor.id) + self.assertEqual(EXAMPLE['name'], test_flavor.name) + self.assertEqual(EXAMPLE['description'], test_flavor.description) + self.assertFalse(test_flavor.is_enabled) + self.assertEqual(EXAMPLE['flavor_profile_id'], + test_flavor.flavor_profile_id) + + self.assertDictEqual( + {'limit': 'limit', + 'marker': 'marker', + 'id': 'id', + 'name': 'name', + 'description': 'description', + 'is_enabled': 'enabled', + 'flavor_profile_id': 'flavor_profile_id'}, + test_flavor._query_mapping._mapping) diff --git a/openstack/tests/unit/load_balancer/test_proxy.py b/openstack/tests/unit/load_balancer/test_proxy.py index c257009ab..fafc9d10e 100644 --- a/openstack/tests/unit/load_balancer/test_proxy.py +++ b/openstack/tests/unit/load_balancer/test_proxy.py @@ -14,6 +14,7 @@ import mock from openstack.load_balancer.v2 import _proxy +from openstack.load_balancer.v2 import flavor from openstack.load_balancer.v2 import flavor_profile from openstack.load_balancer.v2 import health_monitor from openstack.load_balancer.v2 import l7_policy @@ -344,3 +345,21 @@ def test_flavor_profile_find(self): def test_flavor_profile_update(self): self.verify_update(self.proxy.update_flavor_profile, flavor_profile.FlavorProfile) + + def test_flavors(self): + self.verify_list(self.proxy.flavors, flavor.Flavor) + + def test_flavor_get(self): + self.verify_get(self.proxy.get_flavor, flavor.Flavor) + + def test_flavor_create(self): + self.verify_create(self.proxy.create_flavor, flavor.Flavor) + + def test_flavor_delete(self): + self.verify_delete(self.proxy.delete_flavor, flavor.Flavor, True) + + def test_flavor_find(self): + self.verify_find(self.proxy.find_flavor, flavor.Flavor) + + def test_flavor_update(self): + self.verify_update(self.proxy.update_flavor, flavor.Flavor) diff --git a/releasenotes/notes/add-load-balancer-flavor-api-d2598e30347a19fc.yaml b/releasenotes/notes/add-load-balancer-flavor-api-d2598e30347a19fc.yaml new file mode 100644 index 000000000..462acc135 --- /dev/null +++ b/releasenotes/notes/add-load-balancer-flavor-api-d2598e30347a19fc.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Adds Octavia (load_balancer) support for the flavor APIs. From 1249d5fd26fe87cdf85451ce34858f4dd3c945b2 Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Fri, 1 Feb 2019 19:12:03 -0800 Subject: [PATCH 2364/3836] Add Octavia (load_balancer) amphora API This patch adds the Octavia (load_balancer) amphora API support. Depends-On: https://review.openstack.org/#/c/632842/ Change-Id: Id5a2ab45c2600a52415387b81369d371b6182578 --- doc/source/user/proxies/load_balancer_v2.rst | 11 ++ .../user/resources/load_balancer/index.rst | 1 + .../resources/load_balancer/v2/amphora.rst | 30 ++++ openstack/load_balancer/v2/_proxy.py | 52 ++++++ openstack/load_balancer/v2/amphora.py | 148 ++++++++++++++++++ .../load_balancer/v2/test_load_balancer.py | 27 ++++ .../tests/unit/load_balancer/test_amphora.py | 142 +++++++++++++++++ .../tests/unit/load_balancer/test_proxy.py | 25 +++ ...-octavia-amphora-api-7f3586f6a4f31de4.yaml | 4 + 9 files changed, 440 insertions(+) create mode 100644 doc/source/user/resources/load_balancer/v2/amphora.rst create mode 100644 openstack/load_balancer/v2/amphora.py create mode 100644 openstack/tests/unit/load_balancer/test_amphora.py create mode 100644 releasenotes/notes/add-octavia-amphora-api-7f3586f6a4f31de4.yaml diff --git a/doc/source/user/proxies/load_balancer_v2.rst b/doc/source/user/proxies/load_balancer_v2.rst index ceecfcfd3..81856454a 100644 --- a/doc/source/user/proxies/load_balancer_v2.rst +++ b/doc/source/user/proxies/load_balancer_v2.rst @@ -128,3 +128,14 @@ Flavor Operations .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_flavor .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_flavor .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_flavor + +Amphora Operations +^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.load_balancer.v2._proxy.Proxy + + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.amphorae + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_amphora + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_amphora + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.configure_amphora + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.failover_amphora diff --git a/doc/source/user/resources/load_balancer/index.rst b/doc/source/user/resources/load_balancer/index.rst index dbb8226f2..171146ff8 100644 --- a/doc/source/user/resources/load_balancer/index.rst +++ b/doc/source/user/resources/load_balancer/index.rst @@ -14,3 +14,4 @@ Load Balancer Resources v2/provider v2/flavor_profile v2/flavor + v2/amphora diff --git a/doc/source/user/resources/load_balancer/v2/amphora.rst b/doc/source/user/resources/load_balancer/v2/amphora.rst new file mode 100644 index 000000000..c89d1ee86 --- /dev/null +++ b/doc/source/user/resources/load_balancer/v2/amphora.rst @@ -0,0 +1,30 @@ +openstack.load_balancer.v2.amphora +================================== + +.. automodule:: openstack.load_balancer.v2.amphora + +The Amphora Class +----------------- + +The ``Amphora`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.load_balancer.v2.amphora.Amphora + :members: + +The AmphoraConfig Class +----------------------- + +The ``AmphoraConfig`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.load_balancer.v2.amphora.AmphoraConfig + :members: + +The AmphoraFailover Class +------------------------- + +The ``AmphoraFailover`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.load_balancer.v2.amphora.AmphoraFailover + :members: diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index edf42be30..6b1f8c7e2 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.load_balancer.v2 import amphora as _amphora from openstack.load_balancer.v2 import flavor as _flavor from openstack.load_balancer.v2 import flavor_profile as _flavor_profile from openstack.load_balancer.v2 import health_monitor as _hm @@ -953,3 +954,54 @@ def update_flavor(self, flavor, **attrs): :rtype: :class:`~openstack.load_balancer.v2.flavor.Flavor` """ return self._update(_flavor.Flavor, flavor, **attrs) + + def amphorae(self, **query): + """Retrieve a generator of amphorae + + :returns: A generator of amphora instances + """ + return self._list(_amphora.Amphora, **query) + + def get_amphora(self, *attrs): + """Get a amphora + + :param amphora: The value can be the ID of an amphora + or :class:`~openstack.load_balancer.v2.amphora.Amphora` instance. + + :returns: One + :class:`~openstack.load_balancer.v2.amphora.Amphora` + """ + return self._get(_amphora.Amphora, *attrs) + + def find_amphora(self, amphora_id, ignore_missing=True): + """Find a single amphora + + :param amphora_id: The ID of a amphora + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the amphora does not exist. + When set to ``True``, no exception will be set when attempting + to find a nonexistent amphora. + + :returns: ``None`` + """ + return self._find(_amphora.Amphora, amphora_id, + ignore_missing=ignore_missing) + + def configure_amphora(self, amphora_id, **attrs): + """Update the configuration of an amphora agent + + :param amphora_id: The ID of an amphora + + :returns: ``None`` + """ + return self._update(_amphora.AmphoraConfig, amphora_id=amphora_id) + + def failover_amphora(self, amphora_id, **attrs): + """Failover an amphora + + :param amphora_id: The ID of an amphora + + :returns: ``None`` + """ + return self._update(_amphora.AmphoraFailover, amphora_id=amphora_id) diff --git a/openstack/load_balancer/v2/amphora.py b/openstack/load_balancer/v2/amphora.py new file mode 100644 index 000000000..bedae8333 --- /dev/null +++ b/openstack/load_balancer/v2/amphora.py @@ -0,0 +1,148 @@ +# Copyright 2019 Rackspace, US 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. + +from openstack import resource + + +class Amphora(resource.Resource): + resource_key = 'amphora' + resources_key = 'amphorae' + base_path = '/octavia/amphorae' + + # capabilities + allow_create = False + allow_fetch = True + allow_commit = False + allow_delete = False + allow_list = True + + _query_mapping = resource.QueryParameters( + 'id', 'loadbalancer_id', 'compute_id', 'lb_network_ip', 'vrrp_ip', + 'ha_ip', 'vrrp_port_id', 'ha_port_id', 'cert_expiration', 'cert_busy', + 'role', 'status', 'vrrp_interface', 'vrrp_id', 'vrrp_priority', + 'cached_zone', 'created_at', 'updated_at', 'image_id', 'image_id' + ) + + # Properties + #: The ID of the amphora. + id = resource.Body('id') + #: The ID of the load balancer. + loadbalancer_id = resource.Body('loadbalancer_id') + #: The ID of the amphora resource in the compute system. + compute_id = resource.Body('compute_id') + #: The management IP of the amphora. + lb_network_ip = resource.Body('lb_network_ip') + #: The address of the vrrp port on the amphora. + vrrp_ip = resource.Body('vrrp_ip') + #: The IP address of the Virtual IP (VIP). + ha_ip = resource.Body('ha_ip') + #: The vrrp port's ID in the networking system. + vrrp_port_id = resource.Body('vrrp_port_id') + #: The ID of the Virtual IP (VIP) port. + ha_port_id = resource.Body('ha_port_id') + #: The date the certificate for the amphora expires. + cert_expiration = resource.Body('cert_expiration') + #: Whether the certificate is in the process of being replaced. + cert_busy = resource.Body('cert_busy') + #: The role configured for the amphora. One of STANDALONE, MASTER, BACKUP. + role = resource.Body('role') + #: The status of the amphora. One of: BOOTING, ALLOCATED, READY, + #: PENDING_CREATE, PENDING_DELETE, DELETED, ERROR. + status = resource.Body('status') + #: The bound interface name of the vrrp port on the amphora. + vrrp_interface = resource.Body('vrrp_interface') + #: The vrrp group's ID for the amphora. + vrrp_id = resource.Body('vrrp_id') + #: The priority of the amphora in the vrrp group. + vrrp_priority = resource.Body('vrrp_priority') + #: The availability zone of a compute instance, cached at create time. + cached_zone = resource.Body('cached_zone') + #: The UTC date and timestamp when the resource was created. + created_at = resource.Body('created_at') + #: The UTC date and timestamp when the resource was last updated. + updated_at = resource.Body('updated_at') + #: The ID of the glance image used for the amphora. + image_id = resource.Body('image_id') + #: The ID of the compute flavor used for the amphora. + compute_flavor = resource.Body('compute_flavor') + + +class AmphoraConfig(resource.Resource): + base_path = '/octavia/amphorae/%(amphora_id)s/config' + + # capabilities + allow_create = False + allow_fetch = False + allow_commit = True + allow_delete = False + allow_list = False + + requires_id = False + + # Properties + #: The ID of the amphora. + amphora_id = resource.URI('amphora_id') + + # The parent commit method assumes there is a header or body change, + # which we do not have here. The default _update code path also has no + # way to pass has_body into this function, so overriding the method here. + def commit(self, session, base_path=None): + kwargs = {} + request = self._prepare_request(prepend_key=False, + base_path=base_path, + **kwargs) + session = self._get_session(session) + kwargs = {} + microversion = self._get_microversion_for(session, 'commit') + response = session.put(request.url, json=request.body, + headers=request.headers, + microversion=microversion, **kwargs) + self.microversion = microversion + self._translate_response(response, has_body=False) + return self + + +class AmphoraFailover(resource.Resource): + base_path = '/octavia/amphorae/%(amphora_id)s/failover' + + # capabilities + allow_create = False + allow_fetch = False + allow_commit = True + allow_delete = False + allow_list = False + + requires_id = False + + # Properties + #: The ID of the amphora. + amphora_id = resource.URI('amphora_id') + + # The parent commit method assumes there is a header or body change, + # which we do not have here. The default _update code path also has no + # way to pass has_body into this function, so overriding the method here. + def commit(self, session, base_path=None): + kwargs = {} + request = self._prepare_request(prepend_key=False, + base_path=base_path, + **kwargs) + session = self._get_session(session) + kwargs = {} + microversion = self._get_microversion_for(session, 'commit') + response = session.put(request.url, json=request.body, + headers=request.headers, + microversion=microversion, **kwargs) + self.microversion = microversion + self._translate_response(response, has_body=False) + return self diff --git a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py index 74d55b30a..a9b984653 100644 --- a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py +++ b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py @@ -37,6 +37,7 @@ class TestLoadBalancer(base.BaseFunctionalTest): PROJECT_ID = None FLAVOR_PROFILE_ID = None FLAVOR_ID = None + AMPHORA_ID = None PROTOCOL = 'HTTP' PROTOCOL_PORT = 80 LB_ALGORITHM = 'ROUND_ROBIN' @@ -117,6 +118,10 @@ def setUp(self): wait=self._wait_for_timeout) self.LB_ID = test_lb.id + amphorae = self.conn.load_balancer.amphorae(loadbalancer_id=self.LB_ID) + for amp in amphorae: + self.AMPHORA_ID = amp.id + test_listener = self.conn.load_balancer.create_listener( name=self.LISTENER_NAME, protocol=self.PROTOCOL, protocol_port=self.PROTOCOL_PORT, loadbalancer_id=self.LB_ID) @@ -573,3 +578,25 @@ def test_flavor_update(self): self.FLAVOR_ID, name=self.FLAVOR_NAME) test_flavor = self.conn.load_balancer.get_flavor(self.FLAVOR_ID) self.assertEqual(self.FLAVOR_NAME, test_flavor.name) + + def test_amphora_list(self): + amp_ids = [amp.id for amp in self.conn.load_balancer.amphorae()] + self.assertIn(self.AMPHORA_ID, amp_ids) + + def test_amphora_find(self): + test_amphora = self.conn.load_balancer.find_amphora(self.AMPHORA_ID) + self.assertEqual(self.AMPHORA_ID, test_amphora.id) + + def test_amphora_get(self): + test_amphora = self.conn.load_balancer.get_amphora(self.AMPHORA_ID) + self.assertEqual(self.AMPHORA_ID, test_amphora.id) + + def test_amphora_configure(self): + self.conn.load_balancer.configure_amphora(self.AMPHORA_ID) + test_amp = self.conn.load_balancer.get_amphora(self.AMPHORA_ID) + self.assertEqual(self.AMPHORA_ID, test_amp.id) + + def test_amphora_failover(self): + self.conn.load_balancer.failover_amphora(self.AMPHORA_ID) + test_amp = self.conn.load_balancer.get_amphora(self.AMPHORA_ID) + self.assertEqual(self.AMPHORA_ID, test_amp.id) diff --git a/openstack/tests/unit/load_balancer/test_amphora.py b/openstack/tests/unit/load_balancer/test_amphora.py new file mode 100644 index 000000000..03d44f81d --- /dev/null +++ b/openstack/tests/unit/load_balancer/test_amphora.py @@ -0,0 +1,142 @@ +# Copyright 2019 Rackspace, US 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. + +from openstack.tests.unit import base +import uuid + +from openstack.load_balancer.v2 import amphora + +IDENTIFIER = uuid.uuid4() +LB_ID = uuid.uuid4() +LISTENER_ID = uuid.uuid4() +COMPUTE_ID = uuid.uuid4() +VRRP_PORT_ID = uuid.uuid4() +HA_PORT_ID = uuid.uuid4() +IMAGE_ID = uuid.uuid4() +COMPUTE_FLAVOR = uuid.uuid4() +AMPHORA_ID = uuid.uuid4() + +EXAMPLE = { + 'id': IDENTIFIER, + 'loadbalancer_id': LB_ID, + 'compute_id': COMPUTE_ID, + 'lb_network_ip': '192.168.1.2', + 'vrrp_ip': '192.168.1.5', + 'ha_ip': '192.168.1.10', + 'vrrp_port_id': VRRP_PORT_ID, + 'ha_port_id': HA_PORT_ID, + 'cert_expiration': '2019-09-19 00:34:51', + 'cert_busy': 0, + 'role': 'MASTER', + 'status': 'ALLOCATED', + 'vrrp_interface': 'eth1', + 'vrrp_id': 1, + 'vrrp_priority': 100, + 'cached_zone': 'zone1', + 'created_at': '2017-05-10T18:14:44', + 'updated_at': '2017-05-10T23:08:12', + 'image_id': IMAGE_ID, + 'compute_flavor': COMPUTE_FLAVOR +} + + +class TestAmphora(base.TestCase): + + def test_basic(self): + test_amphora = amphora.Amphora() + self.assertEqual('amphora', test_amphora.resource_key) + self.assertEqual('amphorae', test_amphora.resources_key) + self.assertEqual('/octavia/amphorae', test_amphora.base_path) + self.assertFalse(test_amphora.allow_create) + self.assertTrue(test_amphora.allow_fetch) + self.assertFalse(test_amphora.allow_commit) + self.assertFalse(test_amphora.allow_delete) + self.assertTrue(test_amphora.allow_list) + + def test_make_it(self): + test_amphora = amphora.Amphora(**EXAMPLE) + self.assertEqual(IDENTIFIER, test_amphora.id) + self.assertEqual(LB_ID, test_amphora.loadbalancer_id) + self.assertEqual(COMPUTE_ID, test_amphora.compute_id) + self.assertEqual(EXAMPLE['lb_network_ip'], test_amphora.lb_network_ip) + self.assertEqual(EXAMPLE['vrrp_ip'], test_amphora.vrrp_ip) + self.assertEqual(EXAMPLE['ha_ip'], test_amphora.ha_ip) + self.assertEqual(VRRP_PORT_ID, test_amphora.vrrp_port_id) + self.assertEqual(HA_PORT_ID, test_amphora.ha_port_id) + self.assertEqual(EXAMPLE['cert_expiration'], + test_amphora.cert_expiration) + self.assertEqual(EXAMPLE['cert_busy'], test_amphora.cert_busy) + self.assertEqual(EXAMPLE['role'], test_amphora.role) + self.assertEqual(EXAMPLE['status'], test_amphora.status) + self.assertEqual(EXAMPLE['vrrp_interface'], + test_amphora.vrrp_interface) + self.assertEqual(EXAMPLE['vrrp_id'], test_amphora.vrrp_id) + self.assertEqual(EXAMPLE['vrrp_priority'], test_amphora.vrrp_priority) + self.assertEqual(EXAMPLE['cached_zone'], test_amphora.cached_zone) + self.assertEqual(EXAMPLE['created_at'], test_amphora.created_at) + self.assertEqual(EXAMPLE['updated_at'], test_amphora.updated_at) + self.assertEqual(IMAGE_ID, test_amphora.image_id) + self.assertEqual(COMPUTE_FLAVOR, test_amphora.compute_flavor) + + self.assertDictEqual( + {'limit': 'limit', + 'marker': 'marker', + 'id': 'id', + 'loadbalancer_id': 'loadbalancer_id', + 'compute_id': 'compute_id', + 'lb_network_ip': 'lb_network_ip', + 'vrrp_ip': 'vrrp_ip', + 'ha_ip': 'ha_ip', + 'vrrp_port_id': 'vrrp_port_id', + 'ha_port_id': 'ha_port_id', + 'cert_expiration': 'cert_expiration', + 'cert_busy': 'cert_busy', + 'role': 'role', + 'status': 'status', + 'vrrp_interface': 'vrrp_interface', + 'vrrp_id': 'vrrp_id', + 'vrrp_priority': 'vrrp_priority', + 'cached_zone': 'cached_zone', + 'created_at': 'created_at', + 'updated_at': 'updated_at', + 'image_id': 'image_id', + 'image_id': 'image_id' + }, + test_amphora._query_mapping._mapping) + + +class TestAmphoraConfig(base.TestCase): + + def test_basic(self): + test_amp_config = amphora.AmphoraConfig() + self.assertEqual('/octavia/amphorae/%(amphora_id)s/config', + test_amp_config.base_path) + self.assertFalse(test_amp_config.allow_create) + self.assertFalse(test_amp_config.allow_fetch) + self.assertTrue(test_amp_config.allow_commit) + self.assertFalse(test_amp_config.allow_delete) + self.assertFalse(test_amp_config.allow_list) + + +class TestAmphoraFailover(base.TestCase): + + def test_basic(self): + test_amp_failover = amphora.AmphoraFailover() + self.assertEqual('/octavia/amphorae/%(amphora_id)s/failover', + test_amp_failover.base_path) + self.assertFalse(test_amp_failover.allow_create) + self.assertFalse(test_amp_failover.allow_fetch) + self.assertTrue(test_amp_failover.allow_commit) + self.assertFalse(test_amp_failover.allow_delete) + self.assertFalse(test_amp_failover.allow_list) diff --git a/openstack/tests/unit/load_balancer/test_proxy.py b/openstack/tests/unit/load_balancer/test_proxy.py index fafc9d10e..0f90b6a66 100644 --- a/openstack/tests/unit/load_balancer/test_proxy.py +++ b/openstack/tests/unit/load_balancer/test_proxy.py @@ -14,6 +14,7 @@ import mock from openstack.load_balancer.v2 import _proxy +from openstack.load_balancer.v2 import amphora from openstack.load_balancer.v2 import flavor from openstack.load_balancer.v2 import flavor_profile from openstack.load_balancer.v2 import health_monitor @@ -36,6 +37,7 @@ class TestLoadBalancerProxy(test_proxy_base.TestProxyBase): POOL_ID = uuid.uuid4() L7_POLICY_ID = uuid.uuid4() AMPHORA = 'amphora' + AMPHORA_ID = uuid.uuid4() def setUp(self): super(TestLoadBalancerProxy, self).setUp() @@ -363,3 +365,26 @@ def test_flavor_find(self): def test_flavor_update(self): self.verify_update(self.proxy.update_flavor, flavor.Flavor) + + def test_amphorae(self): + self.verify_list(self.proxy.amphorae, amphora.Amphora) + + def test_amphora_get(self): + self.verify_get(self.proxy.get_amphora, amphora.Amphora) + + def test_amphora_find(self): + self.verify_find(self.proxy.find_amphora, amphora.Amphora) + + def test_amphora_configure(self): + self.verify_update(self.proxy.configure_amphora, + amphora.AmphoraConfig, + value=[self.AMPHORA_ID], + expected_args=[], + expected_kwargs={'amphora_id': self.AMPHORA_ID}) + + def test_amphora_failover(self): + self.verify_update(self.proxy.failover_amphora, + amphora.AmphoraFailover, + value=[self.AMPHORA_ID], + expected_args=[], + expected_kwargs={'amphora_id': self.AMPHORA_ID}) diff --git a/releasenotes/notes/add-octavia-amphora-api-7f3586f6a4f31de4.yaml b/releasenotes/notes/add-octavia-amphora-api-7f3586f6a4f31de4.yaml new file mode 100644 index 000000000..af9a5b409 --- /dev/null +++ b/releasenotes/notes/add-octavia-amphora-api-7f3586f6a4f31de4.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Adds Octavia (load_balancer) support for the amphora APIs. From 96c823c99f747530c505f97157a55b5de123f820 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 2 Feb 2019 13:36:56 +0000 Subject: [PATCH 2365/3836] Add support for bodyless commits Add a flag that can be used for things like the load balancer action objects that don't have a body but are just a URL that needs to be hit. Change-Id: I32b3f6a92b7ee67d7092ce833f881613d6d46867 --- openstack/load_balancer/v2/amphora.py | 38 +++++---------------- openstack/load_balancer/v2/load_balancer.py | 19 +++-------- openstack/resource.py | 9 ++++- 3 files changed, 20 insertions(+), 46 deletions(-) diff --git a/openstack/load_balancer/v2/amphora.py b/openstack/load_balancer/v2/amphora.py index bedae8333..f3e02b2f9 100644 --- a/openstack/load_balancer/v2/amphora.py +++ b/openstack/load_balancer/v2/amphora.py @@ -87,6 +87,7 @@ class AmphoraConfig(resource.Resource): allow_commit = True allow_delete = False allow_list = False + allow_empty_commit = True requires_id = False @@ -94,23 +95,11 @@ class AmphoraConfig(resource.Resource): #: The ID of the amphora. amphora_id = resource.URI('amphora_id') - # The parent commit method assumes there is a header or body change, - # which we do not have here. The default _update code path also has no + # The default _update code path also has no # way to pass has_body into this function, so overriding the method here. def commit(self, session, base_path=None): - kwargs = {} - request = self._prepare_request(prepend_key=False, - base_path=base_path, - **kwargs) - session = self._get_session(session) - kwargs = {} - microversion = self._get_microversion_for(session, 'commit') - response = session.put(request.url, json=request.body, - headers=request.headers, - microversion=microversion, **kwargs) - self.microversion = microversion - self._translate_response(response, has_body=False) - return self + return super(AmphoraConfig, self).commit( + session, base_path=base_path, has_body=False) class AmphoraFailover(resource.Resource): @@ -122,6 +111,7 @@ class AmphoraFailover(resource.Resource): allow_commit = True allow_delete = False allow_list = False + allow_empty_commit = True requires_id = False @@ -129,20 +119,8 @@ class AmphoraFailover(resource.Resource): #: The ID of the amphora. amphora_id = resource.URI('amphora_id') - # The parent commit method assumes there is a header or body change, - # which we do not have here. The default _update code path also has no + # The default _update code path also has no # way to pass has_body into this function, so overriding the method here. def commit(self, session, base_path=None): - kwargs = {} - request = self._prepare_request(prepend_key=False, - base_path=base_path, - **kwargs) - session = self._get_session(session) - kwargs = {} - microversion = self._get_microversion_for(session, 'commit') - response = session.put(request.url, json=request.body, - headers=request.headers, - microversion=microversion, **kwargs) - self.microversion = microversion - self._translate_response(response, has_body=False) - return self + return super(AmphoraFailover, self).commit( + session, base_path=base_path, has_body=False) diff --git a/openstack/load_balancer/v2/load_balancer.py b/openstack/load_balancer/v2/load_balancer.py index 77f8cb61a..b6cf805f3 100644 --- a/openstack/load_balancer/v2/load_balancer.py +++ b/openstack/load_balancer/v2/load_balancer.py @@ -124,6 +124,7 @@ class LoadBalancerFailover(resource.Resource): allow_commit = True allow_delete = False allow_list = False + allow_empty_commit = True requires_id = False @@ -131,20 +132,8 @@ class LoadBalancerFailover(resource.Resource): #: The ID of the load balancer. lb_id = resource.URI('lb_id') - # The parent commit method assumes there is a header or body change, - # which we do not have here. The default _update code path also has no + # The default _update code path also has no # way to pass has_body into this function, so overriding the method here. def commit(self, session, base_path=None): - kwargs = {} - request = self._prepare_request(prepend_key=False, - base_path=base_path, - **kwargs) - session = self._get_session(session) - kwargs = {} - microversion = self._get_microversion_for(session, 'commit') - response = session.put(request.url, json=request.body, - headers=request.headers, - microversion=microversion, **kwargs) - self.microversion = microversion - self._translate_response(response, has_body=False) - return self + return super(LoadBalancerFailover, self).commit( + session, base_path=base_path, has_body=False) diff --git a/openstack/resource.py b/openstack/resource.py index a3f55fb95..129b3d05e 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -392,6 +392,9 @@ class Resource(dict): # OSC no longer checks for allow_get allow_get = True + #: Commits happen without header or body being dirty. + allow_empty_commit = False + #: Method for committing a resource (PUT, PATCH, POST) commit_method = "PUT" #: Method for creating a resource (POST, PUT) @@ -1176,7 +1179,11 @@ def commit(self, session, prepend_key=True, has_body=True, self._body._dirty.discard("id") # Only try to update if we actually have anything to commit. - if not any([self._body.dirty, self._header.dirty]): + if not any([ + self._body.dirty, + self._header.dirty, + self.allow_empty_commit, + ]): return self if not self.allow_commit: From dc092757d1fdf56806abc0a8c45f2021c6cc5771 Mon Sep 17 00:00:00 2001 From: Yuval Shalev Date: Mon, 4 Feb 2019 00:01:12 +0200 Subject: [PATCH 2366/3836] Added server diagnostics This patch add get server diagnostics method. Change-Id: I1f79db9faab4694b251465b456365a18ed94dfa7 --- openstack/compute/v2/_proxy.py | 18 ++++ openstack/compute/v2/server_diagnostics.py | 52 +++++++++++ .../compute/v2/test_server_diagnostics.py | 89 +++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 openstack/compute/v2/server_diagnostics.py create mode 100644 openstack/tests/unit/compute/v2/test_server_diagnostics.py diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index f9eccb001..10727dd2c 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -19,6 +19,7 @@ from openstack.compute.v2 import keypair as _keypair from openstack.compute.v2 import limits from openstack.compute.v2 import server as _server +from openstack.compute.v2 import server_diagnostics as _server_diagnostics from openstack.compute.v2 import server_group as _server_group from openstack.compute.v2 import server_interface as _server_interface from openstack.compute.v2 import server_ip @@ -1466,3 +1467,20 @@ def wait_for_delete(self, res, interval=2, wait=120): to delete failed to occur in the specified seconds. """ return resource.wait_for_delete(self, res, interval, wait) + + def get_server_diagnostics(self, server): + """Get a single server diagnostics + + :param server: This parameter need to be specified when ServerInterface + ID is given as value. It can be either the ID of a + server or a :class:`~openstack.compute.v2.server.Server` + instance that the interface belongs to. + + :returns: One + :class:`~openstack.compute.v2.server_diagnostics.ServerDiagnostics` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + server_id = self._get_resource(_server.Server, server).id + return self._get(_server_diagnostics.ServerDiagnostics, + server_id=server_id, requires_id=False) diff --git a/openstack/compute/v2/server_diagnostics.py b/openstack/compute/v2/server_diagnostics.py new file mode 100644 index 000000000..24a8501ad --- /dev/null +++ b/openstack/compute/v2/server_diagnostics.py @@ -0,0 +1,52 @@ +# 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 resource + + +class ServerDiagnostics(resource.Resource): + resource_key = 'diagnostics' + base_path = '/servers/%(server_id)s/diagnostics' + + # capabilities + allow_fetch = True + + requires_id = False + + #: Indicates whether or not a config drive was used for this server. + has_config_drive = resource.Body('config_drive') + #: The current state of the VM. + state = resource.Body('state') + #: The driver on which the VM is running. + driver = resource.Body('driver') + #: The hypervisor on which the VM is running. + hypervisor = resource.Body('hypervisor') + #: The hypervisor OS. + hypervisor_os = resource.Body('hypervisor_os') + #: The amount of time in seconds that the VM has been running. + uptime = resource.URI('uptime') + #: The number of vCPUs. + num_cpus = resource.URI('num_cpus') + #: The number of disks. + num_disks = resource.URI('num_disks') + #: The number of vNICs. + num_nics = resource.URI('num_nics') + #: The dictionary with information about VM memory usage. + memory_details = resource.URI('memory_details') + #: The list of dictionaries with detailed information about VM CPUs. + cpu_details = resource.URI('cpu_details') + #: The list of dictionaries with detailed information about VM disks. + disk_details = resource.URI('disk_details') + #: The list of dictionaries with detailed information about VM NICs. + nic_details = resource.URI('nic_details') + #: The ID for the server. + server_id = resource.URI('server_id') diff --git a/openstack/tests/unit/compute/v2/test_server_diagnostics.py b/openstack/tests/unit/compute/v2/test_server_diagnostics.py new file mode 100644 index 000000000..51309fe7e --- /dev/null +++ b/openstack/tests/unit/compute/v2/test_server_diagnostics.py @@ -0,0 +1,89 @@ +# 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.tests.unit import base + +from openstack.compute.v2 import server_diagnostics + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + "config_drive": True, + "cpu_details": [ + { + "id": 0, + "time": 17300000000, + "utilisation": 15 + } + ], + "disk_details": [ + { + "errors_count": 1, + "read_bytes": 262144, + "read_requests": 112, + "write_bytes": 5778432, + "write_requests": 488 + } + ], + "driver": "libvirt", + "hypervisor": "kvm", + "hypervisor_os": "ubuntu", + "memory_details": { + "maximum": 524288, + "used": 0 + }, + "nic_details": [ + { + "mac_address": "01:23:45:67:89:ab", + "rx_drop": 200, + "rx_errors": 100, + "rx_octets": 2070139, + "rx_packets": 26701, + "rx_rate": 300, + "tx_drop": 500, + "tx_errors": 400, + "tx_octets": 140208, + "tx_packets": 662, + "tx_rate": 600 + } + ], + "num_cpus": 1, + "num_disks": 1, + "num_nics": 1, + "state": "running", + "uptime": 46664 +} + + +class TestServerInterface(base.TestCase): + + def test_basic(self): + sot = server_diagnostics.ServerDiagnostics() + self.assertEqual('diagnostics', sot.resource_key) + self.assertEqual('/servers/%(server_id)s/diagnostics', sot.base_path) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.requires_id) + + def test_make_it(self): + sot = server_diagnostics.ServerDiagnostics(**EXAMPLE) + self.assertEqual(EXAMPLE['config_drive'], sot.has_config_drive) + self.assertEqual(EXAMPLE['cpu_details'], sot.cpu_details) + self.assertEqual(EXAMPLE['disk_details'], sot.disk_details) + self.assertEqual(EXAMPLE['driver'], sot.driver) + self.assertEqual(EXAMPLE['hypervisor'], sot.hypervisor) + self.assertEqual(EXAMPLE['hypervisor_os'], sot.hypervisor_os) + self.assertEqual(EXAMPLE['memory_details'], sot.memory_details) + self.assertEqual(EXAMPLE['nic_details'], sot.nic_details) + self.assertEqual(EXAMPLE['num_cpus'], sot.num_cpus) + self.assertEqual(EXAMPLE['num_disks'], sot.num_disks) + self.assertEqual(EXAMPLE['num_nics'], sot.num_nics) + self.assertEqual(EXAMPLE['state'], sot.state) + self.assertEqual(EXAMPLE['uptime'], sot.uptime) From 5a4ce4567b81081b37179e79edfa374b0862c6d5 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 2 Feb 2019 13:48:12 +0000 Subject: [PATCH 2367/3836] Make all resource locations process project_id All resources have a location property now that describes the location of the object in the cloud, but the majority of that information is taken directly from the connection that is in use. Some resources contain server-side project_id or availability zone information. When those exist, they should override the project_id implied by the connection. Change-Id: I48c116bf62d726be8d906c3d232e1ee203347b0a --- openstack/compute/v2/server.py | 8 -------- openstack/network/v2/firewall_policy.py | 1 + openstack/resource.py | 27 ++++++++++++++++++++++++- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index e7f6b5401..32eeec53f 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -138,14 +138,6 @@ class Server(resource.Resource, metadata.MetadataMixin, resource.TagMixin): #: only. instance_name = resource.Body('OS-EXT-SRV-ATTR:instance_name') - def __init__(self, *args, **kwargs): - super(Server, self).__init__(*args, **kwargs) - - if self._connection: - self.location = self._connection._get_current_location( - project_id=self.project_id, - zone=self.availability_zone) - def _prepare_request(self, requires_id=True, prepend_key=True, base_path=None): request = super(Server, self)._prepare_request(requires_id=requires_id, diff --git a/openstack/network/v2/firewall_policy.py b/openstack/network/v2/firewall_policy.py index aa4df63dd..bc4afe484 100644 --- a/openstack/network/v2/firewall_policy.py +++ b/openstack/network/v2/firewall_policy.py @@ -92,4 +92,5 @@ def _put_request(self, session, url, json_data): raise HttpException(message=message, response=resp) self._body.attributes.update(data) + self._update_location() return self diff --git a/openstack/resource.py b/openstack/resource.py index 98d204144..5cc5eb4e7 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -464,6 +464,8 @@ def __init__(self, _synchronized=False, connection=None, **attrs): else: self._original_body = {} + self._update_location() + # TODO(mordred) This is terrible, but is a hack at the moment to ensure # json.dumps works. The json library does basically if not obj: and # obj.items() ... but I think the if not obj: is short-circuiting down @@ -571,6 +573,7 @@ def _update(self, **attrs): self._header.update(header) self._uri.update(uri) self._computed.update(computed) + self._update_location() # TODO(mordred) This is terrible, but is a hack at the moment to ensure # json.dumps works. The json library does basically if not obj: and @@ -601,10 +604,31 @@ def _collect_attrs(self, attrs): # TODO(mordred) We should make a Location Resource and add it here # instead of just the dict. if self._connection: - computed['location'] = self._connection.current_location + computed.setdefault('location', self._connection.current_location) return body, header, uri, computed + def _update_location(self): + """Update location to include resource project/zone information. + + Location should describe the location of the resource. For some + resources, where the resource doesn't have any such baked-in notion + we assume the resource exists in the same project as the logged-in + user's token. + + However, if a resource contains a project_id, then that project is + where the resource lives, and the location should reflect that. + """ + if not self._connection: + return + kwargs = {} + if hasattr(self, 'project_id'): + kwargs['project_id'] = self.project_id + if hasattr(self, 'availability_zone'): + kwargs['zone'] = self.availability_zone + if kwargs: + self.location = self._connection._get_current_location(**kwargs) + def _compute_attributes(self, body, header, uri): """Compute additional attributes from the remote resource.""" return {} @@ -948,6 +972,7 @@ def _translate_response(self, response, has_body=None, error_message=None): headers = self._consume_header_attrs(response.headers) self._header.attributes.update(headers) self._header.clean() + self._update_location() @classmethod def _get_session(cls, session): From 2be7e0f6f791feda8a53c73e23d0cf04e9daa478 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 2 Feb 2019 15:26:51 +0000 Subject: [PATCH 2368/3836] Rename compute.service.zone to availability_zone While zone is less characters, availability_zone is what this is called across the rest of the codebase, and is what the location system looks for. This is an incompatibility to be taken care of pre-1.0 release. Change-Id: I9fc7ac2a32ac20f173554dcde80eb57713a92f7c --- openstack/compute/v2/service.py | 2 +- openstack/tests/unit/compute/v2/test_service.py | 2 +- .../notes/compute-service-zone-2b25ec705b0156c4.yaml | 7 +++++++ 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/compute-service-zone-2b25ec705b0156c4.yaml diff --git a/openstack/compute/v2/service.py b/openstack/compute/v2/service.py index f13cac1c4..d1361ffd8 100644 --- a/openstack/compute/v2/service.py +++ b/openstack/compute/v2/service.py @@ -37,7 +37,7 @@ class Service(resource.Resource): #: Host where service runs host = resource.Body('host') #: The availability zone of service - zone = resource.Body("zone") + availability_zone = resource.Body("zone") def _action(self, session, action, body): url = utils.urljoin(Service.base_path, action) diff --git a/openstack/tests/unit/compute/v2/test_service.py b/openstack/tests/unit/compute/v2/test_service.py index 159423505..5b72b6f52 100644 --- a/openstack/tests/unit/compute/v2/test_service.py +++ b/openstack/tests/unit/compute/v2/test_service.py @@ -51,7 +51,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['binary'], sot.binary) self.assertEqual(EXAMPLE['status'], sot.status) self.assertEqual(EXAMPLE['state'], sot.state) - self.assertEqual(EXAMPLE['zone'], sot.zone) + self.assertEqual(EXAMPLE['zone'], sot.availability_zone) self.assertEqual(EXAMPLE['id'], sot.id) def test_force_down(self): diff --git a/releasenotes/notes/compute-service-zone-2b25ec705b0156c4.yaml b/releasenotes/notes/compute-service-zone-2b25ec705b0156c4.yaml new file mode 100644 index 000000000..ea69aef6a --- /dev/null +++ b/releasenotes/notes/compute-service-zone-2b25ec705b0156c4.yaml @@ -0,0 +1,7 @@ +--- +upgrade: + - | + The ``zone`` attribute on compute ``Service`` objects + has been renamed to ``availability_zone`` to match all + of the other resources, and also to better integrate + with the ``Resource.location`` attribute. From 1dfd5cde93703e9b312a79094bc324506bae89c9 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 2 Feb 2019 14:46:12 +0000 Subject: [PATCH 2369/3836] Stop mocking method in fwaas test Found this while debugging the previous patch. Improve the unit test by not mocking the method but instead doing an additional requests_mock call. Change-Id: Ifbb5b57d2dbf501ad17ceacc4ee6237a1d21851e --- openstack/tests/unit/cloud/test_fwaas.py | 33 +++++++++++++----------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/openstack/tests/unit/cloud/test_fwaas.py b/openstack/tests/unit/cloud/test_fwaas.py index 4ad163d3c..bd8efdfe6 100644 --- a/openstack/tests/unit/cloud/test_fwaas.py +++ b/openstack/tests/unit/cloud/test_fwaas.py @@ -226,25 +226,28 @@ def test_update_firewall_rule_filters(self): filters = {'project_id': self.mock_firewall_rule['project_id']} updated = self.mock_firewall_rule.copy() updated.update(params) - _find = self.cloud.network.find_firewall_rule - self.cloud.network.find_firewall_rule = Mock( - return_value=self.mock_firewall_rule) + updated_dict = self._mock_firewall_rule_attrs.copy() + updated_dict.update(params) self.register_uris([ - dict(method='PUT', - uri=self._make_mock_url('firewall_rules', - self.firewall_rule_id), - json={'firewall_rule': updated}) + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_rules', self.firewall_rule_name), + json={'firewall_rule': self._mock_firewall_rule_attrs}), + dict( + method='PUT', + uri=self._make_mock_url( + 'firewall_rules', self.firewall_rule_id), + json={'firewall_rule': updated_dict}, + validate={ + 'json': {'firewall_rule': params}, + }) ]) - self.assertDictEqual(updated, - self.cloud.update_firewall_rule( - self.firewall_rule_name, filters, **params)) + updated_rule = self.cloud.update_firewall_rule( + self.firewall_rule_name, filters, **params) + self.assertDictEqual(updated, updated_rule) self.assert_calls() - self.cloud.network.find_firewall_rule.assert_called_once_with( - self.firewall_rule_name, ignore_missing=False, **filters) - # restore - self.cloud.network.find_firewall_rule = _find - class TestFirewallPolicy(FirewallTestCase): firewall_policy_id = '78d05d20-d406-41ec-819d-06b65c2684e4' From b7b7353e5541b5c18c5d487ac9b3cac5c1f3f36b Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Wed, 13 Feb 2019 12:43:12 +0100 Subject: [PATCH 2370/3836] baremetal: implement set_node_power_state in the proxy This call was implemented in the shade part, but not in the baremetal proxy. This change implements it, and makes the share part use it. As a side effect, soft power actions (from API 1.27) are now supported. Change-Id: I6f2f0aa7717c0f9423d6a3ddc163c4fc0d8152d0 --- doc/source/user/proxies/baremetal.rst | 1 + openstack/baremetal/v1/_proxy.py | 13 ++++++ openstack/baremetal/v1/node.py | 35 ++++++++++++++++ openstack/cloud/openstackcloud.py | 40 ++----------------- .../baremetal/test_baremetal_node.py | 13 ++++++ .../tests/unit/baremetal/v1/test_node.py | 28 +++++++++++++ 6 files changed, 93 insertions(+), 37 deletions(-) diff --git a/doc/source/user/proxies/baremetal.rst b/doc/source/user/proxies/baremetal.rst index fd37b61b0..1a6c212af 100644 --- a/doc/source/user/proxies/baremetal.rst +++ b/doc/source/user/proxies/baremetal.rst @@ -22,6 +22,7 @@ Node Operations .. automethod:: openstack.baremetal.v1._proxy.Proxy.get_node .. automethod:: openstack.baremetal.v1._proxy.Proxy.find_node .. automethod:: openstack.baremetal.v1._proxy.Proxy.nodes + .. automethod:: openstack.baremetal.v1._proxy.Proxy.set_node_power_state .. automethod:: openstack.baremetal.v1._proxy.Proxy.set_node_provision_state .. automethod:: openstack.baremetal.v1._proxy.Proxy.wait_for_nodes_provision_state .. automethod:: openstack.baremetal.v1._proxy.Proxy.wait_for_node_reservation diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 7924c269b..7280c6a5d 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -339,6 +339,19 @@ def wait_for_nodes_provision_state(self, nodes, expected_state, {'nodes': ', '.join(n.id for n in remaining), 'target': expected_state}) + def set_node_power_state(self, node, target): + """Run an action modifying node's power state. + + This call is asynchronous, it will return success as soon as the Bare + Metal service acknowledges the request. + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :param target: Target power state, e.g. "rebooting", "power on". + See the Bare Metal service documentation for available actions. + """ + self._get_resource(_node.Node, node).set_power_state(self, target) + def wait_for_node_reservation(self, node, timeout=None): """Wait for a lock on the node to be released. diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 1395b7866..6ab81c360 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -457,6 +457,41 @@ def _check_state_reached(self, session, expected_state, "the last error is %(error)s" % {'node': self.id, 'error': self.last_error}) + # TODO(dtantsur): waiting for power state + def set_power_state(self, session, target): + """Run an action modifying this node's power state. + + This call is asynchronous, it will return success as soon as the Bare + Metal service acknowledges the request. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param target: Target power state, e.g. "rebooting", "power on". + See the Bare Metal service documentation for available actions. + """ + session = self._get_session(session) + + if target.startswith("soft "): + version = '1.27' + else: + version = None + + version = utils.pick_microversion(session, version) + + # TODO(dtantsur): server timeout support + body = {'target': target} + + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'states', 'power') + response = session.put( + request.url, json=body, + headers=request.headers, microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + msg = ("Failed to set power state for bare metal node {node} " + "to {target}".format(node=self.id, target=target)) + exceptions.raise_from_response(response, error_message=msg) + def attach_vif(self, session, vif_id, retry_on_conflict=True): """Attach a VIF to the node. diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 398218124..16f5b6be1 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -10141,40 +10141,6 @@ def remove_machine_from_maintenance(self, name_or_id): """ self.set_machine_maintenance_state(name_or_id, False) - def _set_machine_power_state(self, name_or_id, state): - """Set machine power state to on or off - - This private method allows a user to turn power on or off to - a node via the Baremetal API. - - :params string name_or_id: A string representing the baremetal - node to have power turned to an "on" - state. - :params string state: A value of "on", "off", or "reboot" that is - passed to the baremetal API to be asserted to - the machine. In the case of the "reboot" state, - Ironic will return the host to the "on" state. - - :raises: OpenStackCloudException on operation error or. - - :returns: None - """ - msg = ("Error setting machine power state to {state} on node " - "{node}").format(state=state, node=name_or_id) - url = '/nodes/{name_or_id}/states/power'.format(name_or_id=name_or_id) - if 'reboot' in state: - desired_state = 'rebooting' - else: - desired_state = 'power {state}'.format(state=state) - payload = {'target': desired_state} - _utils._call_client_and_retry(self._baremetal_client.put, - url, - retry_on=[409, 503], - json=payload, - error_message=msg, - microversion="1.6") - return None - def set_machine_power_on(self, name_or_id): """Activate baremetal machine power @@ -10188,7 +10154,7 @@ def set_machine_power_on(self, name_or_id): :returns: None """ - self._set_machine_power_state(name_or_id, 'on') + self.baremetal.set_node_power_state(name_or_id, 'power on') def set_machine_power_off(self, name_or_id): """De-activate baremetal machine power @@ -10203,7 +10169,7 @@ def set_machine_power_off(self, name_or_id): :returns: """ - self._set_machine_power_state(name_or_id, 'off') + self.baremetal.set_node_power_state(name_or_id, 'power off') def set_machine_power_reboot(self, name_or_id): """De-activate baremetal machine power @@ -10220,7 +10186,7 @@ def set_machine_power_reboot(self, name_or_id): :returns: None """ - self._set_machine_power_state(name_or_id, 'reboot') + self.baremetal.set_node_power_state(name_or_id, 'rebooting') def activate_node(self, uuid, configdrive=None, wait=False, timeout=1200): diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index 1c24d3f5c..359cd9a07 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -117,6 +117,19 @@ def test_node_create_in_enroll_provide(self): wait=True) self.assertEqual(node.provision_state, 'available') + def test_node_power_state(self): + node = self.create_node() + self.assertIsNone(node.power_state) + + self.conn.baremetal.set_node_power_state(node, 'power on') + node = self.conn.baremetal.get_node(node.id) + # Fake nodes react immediately to power requests. + self.assertEqual('power on', node.power_state) + + self.conn.baremetal.set_node_power_state(node, 'power off') + node = self.conn.baremetal.get_node(node.id) + self.assertEqual('power off', node.power_state) + def test_node_validate(self): node = self.create_node() # Fake hardware passes validation for all interfaces diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 466e6a7e4..e8a7a2f8b 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -487,3 +487,31 @@ def test_timeout(self, mock_fetch): self.node.wait_for_reservation, self.session, timeout=0.001) mock_fetch.assert_called_with(self.node, self.session) + + +@mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) +class TestNodeSetPowerState(base.TestCase): + + def setUp(self): + super(TestNodeSetPowerState, self).setUp() + self.node = node.Node(**FAKE) + self.session = mock.Mock(spec=adapter.Adapter, + default_microversion=None) + + def test_power_on(self): + self.node.set_power_state(self.session, 'power on') + self.session.put.assert_called_once_with( + 'nodes/%s/states/power' % FAKE['uuid'], + json={'target': 'power on'}, + headers=mock.ANY, + microversion=None, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + def test_soft_power_on(self): + self.node.set_power_state(self.session, 'soft power off') + self.session.put.assert_called_once_with( + 'nodes/%s/states/power' % FAKE['uuid'], + json={'target': 'soft power off'}, + headers=mock.ANY, + microversion='1.27', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) From e8ccfee5fadae7877966bd5b2ada6910f95fc3af Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Wed, 13 Feb 2019 11:45:11 +0100 Subject: [PATCH 2371/3836] baremetal: support for allocation API Change-Id: Ie47a903430d6b54740152676d897b5f8759e36c2 Story: #2004341 Task: #28029 --- doc/source/user/proxies/baremetal.rst | 10 ++ doc/source/user/resources/baremetal/index.rst | 1 + .../resources/baremetal/v1/allocation.rst | 12 ++ openstack/baremetal/v1/_proxy.py | 92 +++++++++++++++ openstack/baremetal/v1/allocation.py | 98 ++++++++++++++++ openstack/baremetal/v1/node.py | 7 +- openstack/tests/functional/baremetal/base.py | 7 ++ .../baremetal/test_baremetal_allocation.py | 110 ++++++++++++++++++ .../unit/baremetal/v1/test_allocation.py | 109 +++++++++++++++++ .../tests/unit/baremetal/v1/test_proxy.py | 15 +++ .../allocation-api-04f6b3b7a0ccc850.yaml | 4 + 11 files changed, 463 insertions(+), 2 deletions(-) create mode 100644 doc/source/user/resources/baremetal/v1/allocation.rst create mode 100644 openstack/baremetal/v1/allocation.py create mode 100644 openstack/tests/functional/baremetal/test_baremetal_allocation.py create mode 100644 openstack/tests/unit/baremetal/v1/test_allocation.py create mode 100644 releasenotes/notes/allocation-api-04f6b3b7a0ccc850.yaml diff --git a/doc/source/user/proxies/baremetal.rst b/doc/source/user/proxies/baremetal.rst index 1a6c212af..cace96a7a 100644 --- a/doc/source/user/proxies/baremetal.rst +++ b/doc/source/user/proxies/baremetal.rst @@ -76,6 +76,16 @@ VIF Operations .. automethod:: openstack.baremetal.v1._proxy.Proxy.detach_vif_from_node .. automethod:: openstack.baremetal.v1._proxy.Proxy.list_node_vifs +Allocation Operations +^^^^^^^^^^^^^^^^^^^^^ +.. autoclass:: openstack.baremetal.v1._proxy.Proxy + + .. automethod:: openstack.baremetal.v1._proxy.Proxy.create_allocation + .. automethod:: openstack.baremetal.v1._proxy.Proxy.delete_allocation + .. automethod:: openstack.baremetal.v1._proxy.Proxy.get_allocation + .. automethod:: openstack.baremetal.v1._proxy.Proxy.allocations + .. automethod:: openstack.baremetal.v1._proxy.Proxy.wait_for_allocation + Utilities --------- diff --git a/doc/source/user/resources/baremetal/index.rst b/doc/source/user/resources/baremetal/index.rst index 123fc5e4a..3f7c56fa0 100644 --- a/doc/source/user/resources/baremetal/index.rst +++ b/doc/source/user/resources/baremetal/index.rst @@ -9,3 +9,4 @@ Baremetal Resources v1/node v1/port v1/port_group + v1/allocation diff --git a/doc/source/user/resources/baremetal/v1/allocation.rst b/doc/source/user/resources/baremetal/v1/allocation.rst new file mode 100644 index 000000000..013518083 --- /dev/null +++ b/doc/source/user/resources/baremetal/v1/allocation.rst @@ -0,0 +1,12 @@ +openstack.baremetal.v1.Allocation +================================= + +.. automodule:: openstack.baremetal.v1.allocation + +The Allocation Class +-------------------- + +The ``Allocation`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.baremetal.v1.allocation.Allocation + :members: diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 7280c6a5d..dd9d65aed 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -12,6 +12,7 @@ from openstack import _log from openstack.baremetal.v1 import _common +from openstack.baremetal.v1 import allocation as _allocation from openstack.baremetal.v1 import chassis as _chassis from openstack.baremetal.v1 import driver as _driver from openstack.baremetal.v1 import node as _node @@ -703,3 +704,94 @@ def list_node_vifs(self, node): """ res = self._get_resource(_node.Node, node) return res.list_vifs(self) + + def allocations(self, **query): + """Retrieve a generator of allocations. + + :param dict query: Optional query parameters to be sent to restrict + the allocation to be returned. Available parameters include: + + * ``fields``: A list containing one or more fields to be returned + in the response. This may lead to some performance gain + because other fields of the resource are not refreshed. + * ``limit``: Requests at most the specified number of items be + returned from the query. + * ``marker``: Specifies the ID of the last-seen allocation. Use the + ``limit`` parameter to make an initial limited request and + use the ID of the last-seen allocation from the response as + the ``marker`` value in a subsequent limited request. + * ``sort_dir``: Sorts the response by the requested sort direction. + A valid value is ``asc`` (ascending) or ``desc`` + (descending). Default is ``asc``. You can specify multiple + pairs of sort key and sort direction query parameters. If + you omit the sort direction in a pair, the API uses the + natural sorting direction of the server attribute that is + provided as the ``sort_key``. + * ``sort_key``: Sorts the response by the this attribute value. + Default is ``id``. You can specify multiple pairs of sort + key and sort direction query parameters. If you omit the + sort direction in a pair, the API uses the natural sorting + direction of the server attribute that is provided as the + ``sort_key``. + + :returns: A generator of allocation instances. + """ + return _allocation.Allocation.list(self, **query) + + def create_allocation(self, **attrs): + """Create a new allocation from attributes. + + :param dict attrs: Keyword arguments that will be used to create a + :class:`~openstack.baremetal.v1.allocation.Allocation`. + + :returns: The results of allocation creation. + :rtype: :class:`~openstack.baremetal.v1.allocation.Allocation`. + """ + return self._create(_allocation.Allocation, **attrs) + + def get_allocation(self, allocation): + """Get a specific allocation. + + :param allocation: The value can be the name or ID of an allocation or + a :class:`~openstack.baremetal.v1.allocation.Allocation` instance. + + :returns: One :class:`~openstack.baremetal.v1.allocation.Allocation` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + allocation matching the name or ID could be found. + """ + return self._get(_allocation.Allocation, allocation) + + def delete_allocation(self, allocation, ignore_missing=True): + """Delete an allocation. + + :param allocation: The value can be the name or ID of an allocation or + a :class:`~openstack.baremetal.v1.allocation.Allocation` instance. + :param bool ignore_missing: When set to ``False``, an exception + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the allocation could not be found. When set to ``True``, no + exception will be raised when attempting to delete a non-existent + allocation. + + :returns: The instance of the allocation which was deleted. + :rtype: :class:`~openstack.baremetal.v1.allocation.Allocation`. + """ + return self._delete(_allocation.Allocation, allocation, + ignore_missing=ignore_missing) + + def wait_for_allocation(self, allocation, timeout=None, + ignore_error=False): + """Wait for the allocation to become active. + + :param allocation: The value can be the name or ID of an allocation or + a :class:`~openstack.baremetal.v1.allocation.Allocation` instance. + :param timeout: How much (in seconds) to wait for the allocation. + The value of ``None`` (the default) means no client-side timeout. + :param ignore_error: If ``True``, this call will raise an exception + if the allocation reaches the ``error`` state. Otherwise the error + state is considered successful and the call returns. + + :returns: The instance of the allocation. + :rtype: :class:`~openstack.baremetal.v1.allocation.Allocation`. + """ + res = self._get_resource(_allocation.Allocation, allocation) + return res.wait(self, timeout=timeout, ignore_error=ignore_error) diff --git a/openstack/baremetal/v1/allocation.py b/openstack/baremetal/v1/allocation.py new file mode 100644 index 000000000..27ac55181 --- /dev/null +++ b/openstack/baremetal/v1/allocation.py @@ -0,0 +1,98 @@ +# 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 _log +from openstack.baremetal.v1 import _common +from openstack import exceptions +from openstack import resource +from openstack import utils + + +_logger = _log.setup_logging('openstack') + + +class Allocation(_common.ListMixin, resource.Resource): + + resources_key = 'allocations' + base_path = '/allocations' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = False + allow_delete = True + allow_list = True + + _query_mapping = resource.QueryParameters( + 'node', 'resource_class', 'state', + fields={'name': 'fields', 'type': _common.comma_separated_list}, + ) + + # The allocation API introduced in 1.52. + _max_microversion = '1.52' + + #: The candidate nodes for this allocation. + candidate_nodes = resource.Body('candidate_nodes', type=list) + #: Timestamp at which the allocation was created. + created_at = resource.Body('created_at') + #: A set of one or more arbitrary metadata key and value pairs. + extra = resource.Body('extra', type=dict) + #: The UUID for the allocation. + id = resource.Body('uuid', alternate_id=True) + #: The last error for the allocation. + last_error = resource.Body("last_error") + #: A list of relative links, including the self and bookmark links. + links = resource.Body('links', type=list) + #: The name of the allocation. + name = resource.Body('name') + #: UUID of the node this allocation belongs to. + node_id = resource.Body('node_uuid') + #: The requested resource class. + resource_class = resource.Body('resource_class') + #: The state of the allocation. + state = resource.Body('state') + #: The requested traits. + traits = resource.Body('traits', type=list) + #: Timestamp at which the allocation was last updated. + updated_at = resource.Body('updated_at') + + def wait(self, session, timeout=None, ignore_error=False): + """Wait for the allocation to become active. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param timeout: How much (in seconds) to wait for the allocation. + The value of ``None`` (the default) means no client-side timeout. + :param ignore_error: If ``True``, this call will raise an exception + if the allocation reaches the ``error`` state. Otherwise the error + state is considered successful and the call returns. + + :return: This :class:`Allocation` instance. + """ + if self.state == 'active': + return self + + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for the allocation %s" % self.id): + self.fetch(session) + + if self.state == 'error' and not ignore_error: + raise exceptions.SDKException( + "Allocation %(allocation)s failed: %(error)s" % + {'allocation': self.id, 'error': self.last_error}) + elif self.state != 'allocating': + return self + + _logger.debug('Still waiting for the allocation %(allocation)s ' + 'to become active, the current state is %(state)s', + {'allocation': self.id, 'state': self.state}) diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 6ab81c360..59d3b8785 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -56,10 +56,13 @@ class Node(_common.ListMixin, resource.Resource): is_maintenance='maintenance', ) - # The conductor field introduced in 1.49 (Stein). - _max_microversion = '1.49' + # The allocation_uuid field introduced in 1.52 (Stein). + _max_microversion = '1.52' # Properties + #: The UUID of the allocation associated with this node. Added in API + #: microversion 1.52. + allocation_id = resource.Body("allocation_uuid") #: The UUID of the chassis associated wit this node. Can be empty or None. chassis_id = resource.Body("chassis_uuid") #: The current clean step. diff --git a/openstack/tests/functional/baremetal/base.py b/openstack/tests/functional/baremetal/base.py index 4faf913a0..77796096a 100644 --- a/openstack/tests/functional/baremetal/base.py +++ b/openstack/tests/functional/baremetal/base.py @@ -23,6 +23,13 @@ def setUp(self): self.require_service('baremetal', min_microversion=self.min_microversion) + def create_allocation(self, **kwargs): + allocation = self.conn.baremetal.create_allocation(**kwargs) + self.addCleanup( + lambda: self.conn.baremetal.delete_allocation(allocation.id, + ignore_missing=True)) + return allocation + def create_chassis(self, **kwargs): chassis = self.conn.baremetal.create_chassis(**kwargs) self.addCleanup( diff --git a/openstack/tests/functional/baremetal/test_baremetal_allocation.py b/openstack/tests/functional/baremetal/test_baremetal_allocation.py new file mode 100644 index 000000000..1f36985bc --- /dev/null +++ b/openstack/tests/functional/baremetal/test_baremetal_allocation.py @@ -0,0 +1,110 @@ +# 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 random + +from openstack import exceptions +from openstack.tests.functional.baremetal import base + + +class TestBareMetalAllocation(base.BaseBaremetalTest): + + min_microversion = '1.52' + + def setUp(self): + super(TestBareMetalAllocation, self).setUp() + # NOTE(dtantsur): generate a unique resource class to prevent parallel + # tests from clashing. + self.resource_class = 'baremetal-%d' % random.randrange(1024) + self.node = self._create_available_node() + + def _create_available_node(self): + node = self.create_node(resource_class=self.resource_class) + self.conn.baremetal.set_node_provision_state(node, 'manage', + wait=True) + self.conn.baremetal.set_node_provision_state(node, 'provide', + wait=True) + # Make sure the node has non-empty power state by forcing power off. + self.conn.baremetal.set_node_power_state(node, 'power off') + self.addCleanup( + lambda: self.conn.baremetal.update_node(node.id, + instance_id=None)) + return node + + def test_allocation_create_get_delete(self): + allocation = self.create_allocation(resource_class=self.resource_class) + self.assertEqual('allocating', allocation.state) + self.assertIsNone(allocation.node_id) + self.assertIsNone(allocation.last_error) + + loaded = self.conn.baremetal.wait_for_allocation(allocation) + self.assertEqual(loaded.id, allocation.id) + self.assertEqual('active', allocation.state) + self.assertEqual(self.node.id, allocation.node_id) + self.assertIsNone(allocation.last_error) + + node = self.conn.baremetal.get_node(self.node.id) + self.assertEqual(allocation.id, node.allocation_id) + + self.conn.baremetal.delete_allocation(allocation, ignore_missing=False) + self.assertRaises(exceptions.ResourceNotFound, + self.conn.baremetal.get_allocation, allocation.id) + + def test_allocation_list(self): + allocation1 = self.create_allocation( + resource_class=self.resource_class) + allocation2 = self.create_allocation( + resource_class=self.resource_class + '-fail') + + self.conn.baremetal.wait_for_allocation(allocation1) + self.conn.baremetal.wait_for_allocation(allocation2, ignore_error=True) + + allocations = self.conn.baremetal.allocations() + self.assertEqual({p.id for p in allocations}, + {allocation1.id, allocation2.id}) + + allocations = self.conn.baremetal.allocations(state='active') + self.assertEqual([p.id for p in allocations], [allocation1.id]) + + allocations = self.conn.baremetal.allocations(node=self.node.id) + self.assertEqual([p.id for p in allocations], [allocation1.id]) + + allocations = self.conn.baremetal.allocations( + resource_class=self.resource_class + '-fail') + self.assertEqual([p.id for p in allocations], [allocation2.id]) + + def test_allocation_negative_failure(self): + allocation = self.create_allocation( + resource_class=self.resource_class + '-fail') + self.assertRaises(exceptions.SDKException, + self.conn.baremetal.wait_for_allocation, + allocation) + + allocation = self.conn.baremetal.get_allocation(allocation.id) + self.assertEqual('error', allocation.state) + self.assertIn(self.resource_class + '-fail', allocation.last_error) + + def test_allocation_negative_non_existing(self): + uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" + self.assertRaises(exceptions.ResourceNotFound, + self.conn.baremetal.get_allocation, uuid) + self.assertRaises(exceptions.ResourceNotFound, + self.conn.baremetal.delete_allocation, uuid, + ignore_missing=False) + self.assertIsNone(self.conn.baremetal.delete_allocation(uuid)) + + def test_allocation_fields(self): + self.create_allocation(resource_class=self.resource_class) + result = self.conn.baremetal.allocations(fields=['uuid']) + for item in result: + self.assertIsNotNone(item.id) + self.assertIsNone(item.resource_class) diff --git a/openstack/tests/unit/baremetal/v1/test_allocation.py b/openstack/tests/unit/baremetal/v1/test_allocation.py new file mode 100644 index 000000000..b2a82ec99 --- /dev/null +++ b/openstack/tests/unit/baremetal/v1/test_allocation.py @@ -0,0 +1,109 @@ +# 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 keystoneauth1 import adapter +import mock + +from openstack.baremetal.v1 import allocation +from openstack import exceptions +from openstack.tests.unit import base + +FAKE = { + "candidate_nodes": [], + "created_at": "2016-08-18T22:28:48.165105+00:00", + "extra": {}, + "last_error": None, + "links": [ + { + "href": "http://127.0.0.1:6385/v1/allocations/", + "rel": "self" + }, + { + "href": "http://127.0.0.1:6385/allocations/", + "rel": "bookmark" + } + ], + "name": "test_allocation", + "node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d", + "resource_class": "baremetal", + "state": "active", + "traits": [], + "updated_at": None, + "uuid": "e43c722c-248e-4c6e-8ce8-0d8ff129387a", +} + + +class TestAllocation(base.TestCase): + + def test_basic(self): + sot = allocation.Allocation() + self.assertIsNone(sot.resource_key) + self.assertEqual('allocations', sot.resources_key) + self.assertEqual('/allocations', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_instantiate(self): + sot = allocation.Allocation(**FAKE) + self.assertEqual(FAKE['candidate_nodes'], sot.candidate_nodes) + self.assertEqual(FAKE['created_at'], sot.created_at) + self.assertEqual(FAKE['extra'], sot.extra) + self.assertEqual(FAKE['last_error'], sot.last_error) + self.assertEqual(FAKE['links'], sot.links) + self.assertEqual(FAKE['name'], sot.name) + self.assertEqual(FAKE['node_uuid'], sot.node_id) + self.assertEqual(FAKE['resource_class'], sot.resource_class) + self.assertEqual(FAKE['state'], sot.state) + self.assertEqual(FAKE['traits'], sot.traits) + self.assertEqual(FAKE['updated_at'], sot.updated_at) + self.assertEqual(FAKE['uuid'], sot.id) + + +@mock.patch('time.sleep', lambda _t: None) +@mock.patch.object(allocation.Allocation, 'fetch', autospec=True) +class TestWaitForAllocation(base.TestCase): + + def setUp(self): + super(TestWaitForAllocation, self).setUp() + self.session = mock.Mock(spec=adapter.Adapter) + self.session.default_microversion = '1.52' + self.fake = dict(FAKE, state='allocating', node_uuid=None) + self.allocation = allocation.Allocation(**self.fake) + + def test_already_active(self, mock_fetch): + self.allocation.state = 'active' + allocation = self.allocation.wait(None) + self.assertIs(allocation, self.allocation) + self.assertFalse(mock_fetch.called) + + def test_wait(self, mock_fetch): + marker = [False] # mutable object to modify in the closure + + def _side_effect(allocation, session): + if marker[0]: + self.allocation.state = 'active' + self.allocation.node_id = FAKE['node_uuid'] + else: + marker[0] = True + + mock_fetch.side_effect = _side_effect + allocation = self.allocation.wait(self.session) + self.assertIs(allocation, self.allocation) + self.assertEqual(2, mock_fetch.call_count) + + def test_timeout(self, mock_fetch): + self.assertRaises(exceptions.ResourceTimeout, + self.allocation.wait, self.session, timeout=0.001) + mock_fetch.assert_called_with(self.allocation, self.session) diff --git a/openstack/tests/unit/baremetal/v1/test_proxy.py b/openstack/tests/unit/baremetal/v1/test_proxy.py index 0ab402785..cd6c65d53 100644 --- a/openstack/tests/unit/baremetal/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal/v1/test_proxy.py @@ -13,6 +13,7 @@ import mock from openstack.baremetal.v1 import _proxy +from openstack.baremetal.v1 import allocation from openstack.baremetal.v1 import chassis from openstack.baremetal.v1 import driver from openstack.baremetal.v1 import node @@ -149,6 +150,20 @@ def test_port_groups_not_detailed(self, mock_list): self.assertIs(result, mock_list.return_value) mock_list.assert_called_once_with(self.proxy, details=False, query=1) + def test_create_allocation(self): + self.verify_create(self.proxy.create_allocation, allocation.Allocation) + + def test_get_allocation(self): + self.verify_get(self.proxy.get_allocation, allocation.Allocation) + + def test_delete_allocation(self): + self.verify_delete(self.proxy.delete_allocation, allocation.Allocation, + False) + + def test_delete_allocation_ignore(self): + self.verify_delete(self.proxy.delete_allocation, allocation.Allocation, + True) + @mock.patch('time.sleep', lambda _sec: None) @mock.patch.object(_proxy.Proxy, 'get_node', autospec=True) diff --git a/releasenotes/notes/allocation-api-04f6b3b7a0ccc850.yaml b/releasenotes/notes/allocation-api-04f6b3b7a0ccc850.yaml new file mode 100644 index 000000000..8ca573f13 --- /dev/null +++ b/releasenotes/notes/allocation-api-04f6b3b7a0ccc850.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Adds support for the baremetal allocation API. From 68b4dc6d67693385e190d84a51869e026a5d3ae1 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 18 Feb 2019 14:31:10 +0100 Subject: [PATCH 2372/3836] Add image.schema resource Add support for fetching schema in image service Change-Id: I1039ec04f83bb2761727a5dbc56ae8e942fb62b0 --- openstack/image/v2/_proxy.py | 41 +++++++++++ openstack/image/v2/schema.py | 25 +++++++ .../tests/functional/image/v2/test_image.py | 16 +++++ openstack/tests/unit/image/v2/test_proxy.py | 29 ++++++++ openstack/tests/unit/image/v2/test_schema.py | 72 +++++++++++++++++++ .../add-image-schema-9c07c2789490718a.yaml | 3 + 6 files changed, 186 insertions(+) create mode 100644 openstack/image/v2/schema.py create mode 100644 openstack/tests/unit/image/v2/test_schema.py create mode 100644 releasenotes/notes/add-image-schema-9c07c2789490718a.yaml diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index fb5ebb499..598115f5f 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -13,6 +13,7 @@ from openstack import exceptions from openstack.image.v2 import image as _image from openstack.image.v2 import member as _member +from openstack.image.v2 import schema as _schema from openstack import proxy from openstack import resource @@ -307,3 +308,43 @@ def update_member(self, member, image, **attrs): image_id = resource.Resource._get_id(image) return self._update(_member.Member, member_id=member_id, image_id=image_id, **attrs) + + def get_images_schema(self): + """Get images schema + + :returns: One :class:`~openstack.image.v2.schema.Schema` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_schema.Schema, requires_id=False, + base_path='/schemas/images') + + def get_image_schema(self): + """Get single image schema + + :returns: One :class:`~openstack.image.v2.schema.Schema` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_schema.Schema, requires_id=False, + base_path='/schemas/image') + + def get_members_schema(self): + """Get image members schema + + :returns: One :class:`~openstack.image.v2.schema.Schema` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_schema.Schema, requires_id=False, + base_path='/schemas/members') + + def get_member_schema(self): + """Get image member schema + + :returns: One :class:`~openstack.image.v2.schema.Schema` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_schema.Schema, requires_id=False, + base_path='/schemas/member') diff --git a/openstack/image/v2/schema.py b/openstack/image/v2/schema.py new file mode 100644 index 000000000..f67e00437 --- /dev/null +++ b/openstack/image/v2/schema.py @@ -0,0 +1,25 @@ +# 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 resource + + +class Schema(resource.Resource): + base_path = '/schemas' + + # capabilities + allow_fetch = True + + #: Additional properties + additional_properties = resource.Body('additionalProperties', type=dict) + #: Schema properties + properties = resource.Body('properties', type=dict) diff --git a/openstack/tests/functional/image/v2/test_image.py b/openstack/tests/functional/image/v2/test_image.py index a2501f93d..11a39e009 100644 --- a/openstack/tests/functional/image/v2/test_image.py +++ b/openstack/tests/functional/image/v2/test_image.py @@ -45,3 +45,19 @@ def setUp(self): def test_get_image(self): img2 = self.conn.image.get_image(self.img) self.assertEqual(self.img, img2) + + def test_get_images_schema(self): + schema = self.conn.image.get_images_schema() + self.assertIsNotNone(schema) + + def test_get_image_schema(self): + schema = self.conn.image.get_image_schema() + self.assertIsNotNone(schema) + + def test_get_members_schema(self): + schema = self.conn.image.get_members_schema() + self.assertIsNotNone(schema) + + def test_get_member_schema(self): + schema = self.conn.image.get_member_schema() + self.assertIsNotNone(schema) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index e0dc10954..19bf6f860 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -16,6 +16,7 @@ from openstack.image.v2 import _proxy from openstack.image.v2 import image from openstack.image.v2 import member +from openstack.image.v2 import schema from openstack.tests.unit.image.v2 import test_image as fake_image from openstack.tests.unit import test_proxy_base @@ -154,3 +155,31 @@ def test_members(self): self.verify_list(self.proxy.members, member.Member, method_args=('image_1',), expected_kwargs={'image_id': 'image_1'}) + + def test_images_schema_get(self): + self._verify2("openstack.proxy.Proxy._get", + self.proxy.get_images_schema, + expected_args=[schema.Schema], + expected_kwargs={'base_path': '/schemas/images', + 'requires_id': False}) + + def test_image_schema_get(self): + self._verify2("openstack.proxy.Proxy._get", + self.proxy.get_image_schema, + expected_args=[schema.Schema], + expected_kwargs={'base_path': '/schemas/image', + 'requires_id': False}) + + def test_members_schema_get(self): + self._verify2("openstack.proxy.Proxy._get", + self.proxy.get_members_schema, + expected_args=[schema.Schema], + expected_kwargs={'base_path': '/schemas/members', + 'requires_id': False}) + + def test_member_schema_get(self): + self._verify2("openstack.proxy.Proxy._get", + self.proxy.get_member_schema, + expected_args=[schema.Schema], + expected_kwargs={'base_path': '/schemas/member', + 'requires_id': False}) diff --git a/openstack/tests/unit/image/v2/test_schema.py b/openstack/tests/unit/image/v2/test_schema.py new file mode 100644 index 000000000..88e0823cf --- /dev/null +++ b/openstack/tests/unit/image/v2/test_schema.py @@ -0,0 +1,72 @@ +# 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.tests.unit import base + +from openstack.image.v2 import schema + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'additionalProperties': { + 'type': 'string' + }, + 'links': [ + { + 'href': '{self}', + 'rel': 'self' + }, + { + 'href': '{file}', + 'rel': 'enclosure' + }, + { + 'href': '{schema}', + 'rel': 'describedby' + } + ], + 'name': 'image', + 'properties': { + 'architecture': { + 'description': 'Operating system architecture', + 'is_base': False, + 'type': 'string' + }, + 'visibility': { + 'description': 'Scope of image accessibility', + 'enum': [ + 'public', + 'private' + ], + 'type': 'string' + } + } +} + + +class TestSchema(base.TestCase): + def test_basic(self): + sot = schema.Schema() + self.assertIsNone(sot.resource_key) + self.assertIsNone(sot.resources_key) + self.assertEqual('/schemas', sot.base_path) + self.assertFalse(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_delete) + self.assertFalse(sot.allow_list) + + def test_make_it(self): + sot = schema.Schema(**EXAMPLE) + self.assertEqual(EXAMPLE['properties'], sot.properties) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['additionalProperties'], + sot.additional_properties) diff --git a/releasenotes/notes/add-image-schema-9c07c2789490718a.yaml b/releasenotes/notes/add-image-schema-9c07c2789490718a.yaml new file mode 100644 index 000000000..3217f998d --- /dev/null +++ b/releasenotes/notes/add-image-schema-9c07c2789490718a.yaml @@ -0,0 +1,3 @@ +--- +features: + - Add support for schema resource in image service. From b748628db19d2ea198fe1ce3e3595d31d355686a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 18 Feb 2019 19:48:43 +0000 Subject: [PATCH 2373/3836] Make sure we pick flavors with disk Modern nova disallows booting vms on flavors with disk: 0 unless they are volume backed. Change-Id: I527a607ba71d027daa9ac2553275f849fce1244a --- openstack/tests/functional/cloud/util.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openstack/tests/functional/cloud/util.py b/openstack/tests/functional/cloud/util.py index fef67190f..d16bfd8c1 100644 --- a/openstack/tests/functional/cloud/util.py +++ b/openstack/tests/functional/cloud/util.py @@ -39,4 +39,5 @@ def pick_flavor(flavors): for flavor in sorted( flavors, key=operator.attrgetter('ram')): - return flavor + if flavor.disk: + return flavor From 11a5952933119e9878875b0e9c9395ae29c511af Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 19 Feb 2019 21:20:06 +0100 Subject: [PATCH 2374/3836] Add image attributes from v2.7 Add new image attributes added with APIv2.7: - os_hidden (hide from list) - os_hash_algo - os_hash_value Change-Id: I1ab56bff65b28ad0b441b598f2c15e97d87df6d7 --- openstack/image/v2/image.py | 18 ++++++++++--- openstack/tests/unit/image/v2/test_image.py | 26 +++++++++++++++++++ ...add-image-attributes-05b820a85cd09806.yaml | 3 +++ 3 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/add-image-attributes-05b820a85cd09806.yaml diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index 0c121b11d..e58a7b33f 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -36,10 +36,11 @@ class Image(resource.Resource, resource.TagMixin): _query_mapping = resource.QueryParameters( "name", "visibility", "member_status", "owner", - "status", "size_min", - "size_max", "sort_key", - "sort_dir", "sort", "tag", - "created_at", "updated_at") + "status", "size_min", "size_max", + "protected", "is_hidden", + "sort_key", "sort_dir", "sort", "tag", + "created_at", "updated_at", + is_hidden="os_hidden") # NOTE: Do not add "self" support here. If you've used Python before, # you know that self, while not being a reserved word, has special @@ -69,9 +70,18 @@ class Image(resource.Resource, resource.TagMixin): #: disk image. Virtual appliance vendors have different formats #: for laying out the information contained in a VM disk image. disk_format = resource.Body('disk_format') + #: This field controls whether an image is displayed in the default + #: image-list response + is_hidden = resource.Body('os_hidden', type=bool) #: Defines whether the image can be deleted. #: *Type: bool* is_protected = resource.Body('protected', type=bool) + #: The algorithm used to compute a secure hash of the image data + #: for this image + hash_algo = resource.Body('os_hash_algo') + #: The hexdigest of the secure hash of the image data computed using + #: the algorithm whose name is the value of the os_hash_algo property. + hash_value = resource.Body('os_hash_value') #: The minimum disk size in GB that is required to boot the image. min_disk = resource.Body('min_disk') #: The minimum amount of RAM in MB that is required to boot the image. diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index f9097db73..7f506a1fe 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -36,6 +36,9 @@ 'status': '8', 'tags': ['g', 'h', 'i'], 'updated_at': '2015-03-09T12:15:57.233772', + 'os_hash_algo': 'sha512', + 'os_hash_value': '073b4523583784fbe01daff81eba092a262ec3', + 'os_hidden': False, 'virtual_size': '10', 'visibility': '11', 'location': '12', @@ -119,6 +122,26 @@ def test_basic(self): self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) + self.assertDictEqual({'created_at': 'created_at', + 'is_hidden': 'os_hidden', + 'limit': 'limit', + 'marker': 'marker', + 'member_status': 'member_status', + 'name': 'name', + 'owner': 'owner', + 'protected': 'protected', + 'size_max': 'size_max', + 'size_min': 'size_min', + 'sort': 'sort', + 'sort_dir': 'sort_dir', + 'sort_key': 'sort_key', + 'status': 'status', + 'tag': 'tag', + 'updated_at': 'updated_at', + 'visibility': 'visibility' + }, + sot._query_mapping._mapping) + def test_make_it(self): sot = image.Image(**EXAMPLE) self.assertEqual(IDENTIFIER, sot.id) @@ -134,6 +157,9 @@ def test_make_it(self): self.assertEqual(EXAMPLE['status'], sot.status) self.assertEqual(EXAMPLE['tags'], sot.tags) self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) + self.assertEqual(EXAMPLE['os_hash_algo'], sot.hash_algo) + self.assertEqual(EXAMPLE['os_hash_value'], sot.hash_value) + self.assertEqual(EXAMPLE['os_hidden'], sot.is_hidden) self.assertEqual(EXAMPLE['virtual_size'], sot.virtual_size) self.assertEqual(EXAMPLE['visibility'], sot.visibility) self.assertEqual(EXAMPLE['size'], sot.size) diff --git a/releasenotes/notes/add-image-attributes-05b820a85cd09806.yaml b/releasenotes/notes/add-image-attributes-05b820a85cd09806.yaml new file mode 100644 index 000000000..6507b9892 --- /dev/null +++ b/releasenotes/notes/add-image-attributes-05b820a85cd09806.yaml @@ -0,0 +1,3 @@ +--- +features: + - Add image attributes is_hidden, hash_algo, hash_value From 1c3bf976b6ff8e3ea72afcb219071f2c038aec7d Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Wed, 20 Feb 2019 12:35:17 +0100 Subject: [PATCH 2375/3836] Fix minor issues in the baremetal proxy docs * Corrected indentation that caused lists to have definitions lists inside of them. * Chassis and ports do not have names, stop mentioning them. * Small wording fixes. Potential problems to fix in the future: * find_chassis and find_port probably work incorrectly because these resources do not have names. * the fields arguments to get_port and get_port_group probably do not work this way. Change-Id: Id7ab627ac08f986142d1755543d602d878e0f53e --- openstack/baremetal/v1/_proxy.py | 235 ++++++++++++++++--------------- 1 file changed, 118 insertions(+), 117 deletions(-) diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index dd9d65aed..77cc77a32 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -38,27 +38,27 @@ def chassis(self, details=False, **query): restrict the chassis to be returned. Available parameters include: * ``fields``: A list containing one or more fields to be returned - in the response. This may lead to some performance gain - because other fields of the resource are not refreshed. + in the response. This may lead to some performance gain + because other fields of the resource are not refreshed. * ``limit``: Requests at most the specified number of items be - returned from the query. + returned from the query. * ``marker``: Specifies the ID of the last-seen chassis. Use the - ``limit`` parameter to make an initial limited request and - use the ID of the last-seen chassis from the response as - the ``marker`` value in a subsequent limited request. + ``limit`` parameter to make an initial limited request and + use the ID of the last-seen chassis from the response as + the ``marker`` value in a subsequent limited request. * ``sort_dir``: Sorts the response by the requested sort direction. - A valid value is ``asc`` (ascending) or ``desc`` - (descending). Default is ``asc``. You can specify multiple - pairs of sort key and sort direction query parameters. If - you omit the sort direction in a pair, the API uses the - natural sorting direction of the server attribute that is - provided as the ``sort_key``. + A valid value is ``asc`` (ascending) or ``desc`` + (descending). Default is ``asc``. You can specify multiple + pairs of sort key and sort direction query parameters. If + you omit the sort direction in a pair, the API uses the + natural sorting direction of the server attribute that is + provided as the ``sort_key``. * ``sort_key``: Sorts the response by the this attribute value. - Default is ``id``. You can specify multiple pairs of sort - key and sort direction query parameters. If you omit the - sort direction in a pair, the API uses the natural sorting - direction of the server attribute that is provided as the - ``sort_key``. + Default is ``id``. You can specify multiple pairs of sort + key and sort direction query parameters. If you omit the + sort direction in a pair, the API uses the natural sorting + direction of the server attribute that is provided as the + ``sort_key``. :returns: A generator of chassis instances. """ @@ -68,8 +68,7 @@ def create_chassis(self, **attrs): """Create a new chassis from attributes. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.baremetal.v1.chassis.Chassis`, it comprised - of the properties on the ``Chassis`` class. + :class:`~openstack.baremetal.v1.chassis.Chassis`. :returns: The results of chassis creation. :rtype: :class:`~openstack.baremetal.v1.chassis.Chassis`. @@ -79,7 +78,7 @@ def create_chassis(self, **attrs): def find_chassis(self, name_or_id, ignore_missing=True): """Find a single chassis. - :param str name_or_id: The name or ID of a chassis. + :param str name_or_id: The ID of a chassis. :param bool ignore_missing: When set to ``False``, an exception of :class:`~openstack.exceptions.ResourceNotFound` will be raised when the chassis does not exist. When set to `True``, None will @@ -93,7 +92,7 @@ def find_chassis(self, name_or_id, ignore_missing=True): def get_chassis(self, chassis): """Get a specific chassis. - :param chassis: The value can be the name or ID of a chassis or a + :param chassis: The value can be the ID of a chassis or a :class:`~openstack.baremetal.v1.chassis.Chassis` instance. :returns: One :class:`~openstack.baremetal.v1.chassis.Chassis` @@ -105,7 +104,7 @@ def get_chassis(self, chassis): def update_chassis(self, chassis, **attrs): """Update a chassis. - :param chassis: Either the name or the ID of a chassis, or an instance + :param chassis: Either the ID of a chassis, or an instance of :class:`~openstack.baremetal.v1.chassis.Chassis`. :param dict attrs: The attributes to update on the chassis represented by the ``chassis`` parameter. @@ -118,7 +117,7 @@ def update_chassis(self, chassis, **attrs): def delete_chassis(self, chassis, ignore_missing=True): """Delete a chassis. - :param chassis: The value can be either the name or ID of a chassis or + :param chassis: The value can be either the ID of a chassis or a :class:`~openstack.baremetal.v1.chassis.Chassis` instance. :param bool ignore_missing: When set to ``False``, an exception :class:`~openstack.exceptions.ResourceNotFound` will be raised @@ -167,36 +166,41 @@ def nodes(self, details=False, **query): the nodes returned. Available parameters include: * ``associated``: Only return those which are, or are not, - associated with an ``instance_id``. + associated with an ``instance_id``. + * ``conductor_group``: Only return those in the specified + ``conductor_group``. * ``driver``: Only return those with the specified ``driver``. + * ``fault``: Only return those with the specified fault type. * ``fields``: A list containing one or more fields to be returned - in the response. This may lead to some performance gain - because other fields of the resource are not refreshed. + in the response. This may lead to some performance gain + because other fields of the resource are not refreshed. * ``instance_id``: Only return the node with this specific instance - UUID or an empty set if not found. + UUID or an empty set if not found. * ``is_maintenance``: Only return those with ``maintenance`` set to - ``True`` or ``False``. + ``True`` or ``False``. * ``limit``: Requests at most the specified number of nodes be - returned from the query. + returned from the query. * ``marker``: Specifies the ID of the last-seen node. Use the - ``limit`` parameter to make an initial limited request and - use the ID of the last-seen node from the response as - the ``marker`` value in a subsequent limited request. + ``limit`` parameter to make an initial limited request and + use the ID of the last-seen node from the response as + the ``marker`` value in a subsequent limited request. * ``provision_state``: Only return those nodes with the specified - ``provision_state``. + ``provision_state``. + * ``resource_class``: Only return those with the specified + ``resource_class``. * ``sort_dir``: Sorts the response by the requested sort direction. - A valid value is ``asc`` (ascending) or ``desc`` - (descending). Default is ``asc``. You can specify multiple - pairs of sort key and sort direction query parameters. If - you omit the sort direction in a pair, the API uses the - natural sorting direction of the server attribute that is - provided as the ``sort_key``. + A valid value is ``asc`` (ascending) or ``desc`` + (descending). Default is ``asc``. You can specify multiple + pairs of sort key and sort direction query parameters. If + you omit the sort direction in a pair, the API uses the + natural sorting direction of the server attribute that is + provided as the ``sort_key``. * ``sort_key``: Sorts the response by the this attribute value. - Default is ``id``. You can specify multiple pairs of sort - key and sort direction query parameters. If you omit the - sort direction in a pair, the API uses the natural sorting - direction of the server attribute that is provided as the - ``sort_key``. + Default is ``id``. You can specify multiple pairs of sort + key and sort direction query parameters. If you omit the + sort direction in a pair, the API uses the natural sorting + direction of the server attribute that is provided as the + ``sort_key``. :returns: A generator of :class:`~openstack.baremetal.v1.node.Node` """ @@ -206,8 +210,7 @@ def create_node(self, **attrs): """Create a new node from attributes. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.baremetal.v1.node.Node`, it comprised - of the properties on the ``Node`` class. + :class:`~openstack.baremetal.v1.node.Node`. :returns: The results of node creation. :rtype: :class:`~openstack.baremetal.v1.node.Node`. @@ -231,7 +234,7 @@ def find_node(self, name_or_id, ignore_missing=True): def get_node(self, node): """Get a specific node. - :param node: The value can be the name or ID of a chassis or a + :param node: The value can be the name or ID of a node or a :class:`~openstack.baremetal.v1.node.Node` instance. :returns: One :class:`~openstack.baremetal.v1.node.Node` @@ -421,37 +424,37 @@ def ports(self, details=False, **query): the ports returned. Available parameters include: * ``address``: Only return ports with the specified physical - hardware address, typically a MAC address. + hardware address, typically a MAC address. * ``driver``: Only return those with the specified ``driver``. * ``fields``: A list containing one or more fields to be returned - in the response. This may lead to some performance gain - because other fields of the resource are not refreshed. + in the response. This may lead to some performance gain + because other fields of the resource are not refreshed. * ``limit``: Requests at most the specified number of ports be - returned from the query. + returned from the query. * ``marker``: Specifies the ID of the last-seen port. Use the - ``limit`` parameter to make an initial limited request and - use the ID of the last-seen port from the response as - the ``marker`` value in a subsequent limited request. + ``limit`` parameter to make an initial limited request and + use the ID of the last-seen port from the response as + the ``marker`` value in a subsequent limited request. * ``node``:only return the ones associated with this specific node - (name or UUID), or an empty set if not found. + (name or UUID), or an empty set if not found. * ``node_id``:only return the ones associated with this specific - node UUID, or an empty set if not found. + node UUID, or an empty set if not found. * ``portgroup``: only return the ports associated with this - specific Portgroup (name or UUID), or an empty set if not - found. Added in API microversion 1.24. + specific Portgroup (name or UUID), or an empty set if not + found. Added in API microversion 1.24. * ``sort_dir``: Sorts the response by the requested sort direction. - A valid value is ``asc`` (ascending) or ``desc`` - (descending). Default is ``asc``. You can specify multiple - pairs of sort key and sort direction query parameters. If - you omit the sort direction in a pair, the API uses the - natural sorting direction of the server attribute that is - provided as the ``sort_key``. + A valid value is ``asc`` (ascending) or ``desc`` + (descending). Default is ``asc``. You can specify multiple + pairs of sort key and sort direction query parameters. If + you omit the sort direction in a pair, the API uses the + natural sorting direction of the server attribute that is + provided as the ``sort_key``. * ``sort_key``: Sorts the response by the this attribute value. - Default is ``id``. You can specify multiple pairs of sort - key and sort direction query parameters. If you omit the - sort direction in a pair, the API uses the natural sorting - direction of the server attribute that is provided as the - ``sort_key``. + Default is ``id``. You can specify multiple pairs of sort + key and sort direction query parameters. If you omit the + sort direction in a pair, the API uses the natural sorting + direction of the server attribute that is provided as the + ``sort_key``. :returns: A generator of port instances. """ @@ -461,8 +464,7 @@ def create_port(self, **attrs): """Create a new port from attributes. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.baremetal.v1.port.Port`, it comprises of the - properties on the ``Port`` class. + :class:`~openstack.baremetal.v1.port.Port`. :returns: The results of port creation. :rtype: :class:`~openstack.baremetal.v1.port.Port`. @@ -472,7 +474,7 @@ def create_port(self, **attrs): def find_port(self, name_or_id, ignore_missing=True): """Find a single port. - :param str name_or_id: The name or ID of a port. + :param str name_or_id: The ID of a port. :param bool ignore_missing: When set to ``False``, an exception of :class:`~openstack.exceptions.ResourceNotFound` will be raised when the port does not exist. When set to `True``, None will @@ -486,14 +488,14 @@ def find_port(self, name_or_id, ignore_missing=True): def get_port(self, port, **query): """Get a specific port. - :param port: The value can be the name or ID of a chassis or a + :param port: The value can be the ID of a port or a :class:`~openstack.baremetal.v1.port.Port` instance. :param dict query: Optional query parameters to be sent to restrict the port properties returned. Available parameters include: * ``fields``: A list containing one or more fields to be returned - in the response. This may lead to some performance gain - because other fields of the resource are not refreshed. + in the response. This may lead to some performance gain + because other fields of the resource are not refreshed. :returns: One :class:`~openstack.baremetal.v1.port.Port` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no @@ -504,7 +506,7 @@ def get_port(self, port, **query): def update_port(self, port, **attrs): """Update a port. - :param chassis: Either the name or the ID of a port or an instance + :param chassis: Either the ID of a port or an instance of :class:`~openstack.baremetal.v1.port.Port`. :param dict attrs: The attributes to update on the port represented by the ``port`` parameter. @@ -517,7 +519,7 @@ def update_port(self, port, **attrs): def delete_port(self, port, ignore_missing=True): """Delete a port. - :param port: The value can be either the name or ID of a port or + :param port: The value can be either the ID of a port or a :class:`~openstack.baremetal.v1.port.Port` instance. :param bool ignore_missing: When set to ``False``, an exception :class:`~openstack.exceptions.ResourceNotFound` will be raised @@ -539,31 +541,31 @@ def port_groups(self, details=False, **query): the port groups returned. Available parameters include: * ``address``: Only return portgroups with the specified physical - hardware address, typically a MAC address. + hardware address, typically a MAC address. * ``fields``: A list containing one or more fields to be returned - in the response. This may lead to some performance gain - because other fields of the resource are not refreshed. + in the response. This may lead to some performance gain + because other fields of the resource are not refreshed. * ``limit``: Requests at most the specified number of portgroups - returned from the query. + returned from the query. * ``marker``: Specifies the ID of the last-seen portgroup. Use the - ``limit`` parameter to make an initial limited request and - use the ID of the last-seen portgroup from the response as - the ``marker`` value in a subsequent limited request. + ``limit`` parameter to make an initial limited request and + use the ID of the last-seen portgroup from the response as + the ``marker`` value in a subsequent limited request. * ``node``:only return the ones associated with this specific node - (name or UUID), or an empty set if not found. + (name or UUID), or an empty set if not found. * ``sort_dir``: Sorts the response by the requested sort direction. - A valid value is ``asc`` (ascending) or ``desc`` - (descending). Default is ``asc``. You can specify multiple - pairs of sort key and sort direction query parameters. If - you omit the sort direction in a pair, the API uses the - natural sorting direction of the server attribute that is - provided as the ``sort_key``. + A valid value is ``asc`` (ascending) or ``desc`` + (descending). Default is ``asc``. You can specify multiple + pairs of sort key and sort direction query parameters. If + you omit the sort direction in a pair, the API uses the + natural sorting direction of the server attribute that is + provided as the ``sort_key``. * ``sort_key``: Sorts the response by the this attribute value. - Default is ``id``. You can specify multiple pairs of sort - key and sort direction query parameters. If you omit the - sort direction in a pair, the API uses the natural sorting - direction of the server attribute that is provided as the - ``sort_key``. + Default is ``id``. You can specify multiple pairs of sort + key and sort direction query parameters. If you omit the + sort direction in a pair, the API uses the natural sorting + direction of the server attribute that is provided as the + ``sort_key``. :returns: A generator of port group instances. """ @@ -573,8 +575,7 @@ def create_port_group(self, **attrs): """Create a new portgroup from attributes. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.baremetal.v1.port_group.PortGroup`, it - comprises of the properties on the ``PortGroup`` class. + :class:`~openstack.baremetal.v1.port_group.PortGroup`. :returns: The results of portgroup creation. :rtype: :class:`~openstack.baremetal.v1.port_group.PortGroup`. @@ -604,8 +605,8 @@ def get_port_group(self, port_group, **query): the port group properties returned. Available parameters include: * ``fields``: A list containing one or more fields to be returned - in the response. This may lead to some performance gain - because other fields of the resource are not refreshed. + in the response. This may lead to some performance gain + because other fields of the resource are not refreshed. :returns: One :class:`~openstack.baremetal.v1.port_group.PortGroup` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no @@ -712,27 +713,27 @@ def allocations(self, **query): the allocation to be returned. Available parameters include: * ``fields``: A list containing one or more fields to be returned - in the response. This may lead to some performance gain - because other fields of the resource are not refreshed. + in the response. This may lead to some performance gain + because other fields of the resource are not refreshed. * ``limit``: Requests at most the specified number of items be - returned from the query. + returned from the query. * ``marker``: Specifies the ID of the last-seen allocation. Use the - ``limit`` parameter to make an initial limited request and - use the ID of the last-seen allocation from the response as - the ``marker`` value in a subsequent limited request. + ``limit`` parameter to make an initial limited request and + use the ID of the last-seen allocation from the response as + the ``marker`` value in a subsequent limited request. * ``sort_dir``: Sorts the response by the requested sort direction. - A valid value is ``asc`` (ascending) or ``desc`` - (descending). Default is ``asc``. You can specify multiple - pairs of sort key and sort direction query parameters. If - you omit the sort direction in a pair, the API uses the - natural sorting direction of the server attribute that is - provided as the ``sort_key``. + A valid value is ``asc`` (ascending) or ``desc`` + (descending). Default is ``asc``. You can specify multiple + pairs of sort key and sort direction query parameters. If + you omit the sort direction in a pair, the API uses the + natural sorting direction of the server attribute that is + provided as the ``sort_key``. * ``sort_key``: Sorts the response by the this attribute value. - Default is ``id``. You can specify multiple pairs of sort - key and sort direction query parameters. If you omit the - sort direction in a pair, the API uses the natural sorting - direction of the server attribute that is provided as the - ``sort_key``. + Default is ``id``. You can specify multiple pairs of sort + key and sort direction query parameters. If you omit the + sort direction in a pair, the API uses the natural sorting + direction of the server attribute that is provided as the + ``sort_key``. :returns: A generator of allocation instances. """ From 35ccf211977a745138ddfdcf629a22ea787eb677 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 21 Feb 2019 12:56:00 +0100 Subject: [PATCH 2376/3836] handle "paginated" argument in test_list properly If any proxy test is calling test_proxy_base.test_list(..., paginated=False) we put paginated into expected_kwargs, but keep it in kwargs. Remove it from there Change-Id: I1f9ea83bdc2868f26cf8583cf090ed79f1c8ba94 --- openstack/tests/unit/test_proxy_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openstack/tests/unit/test_proxy_base.py b/openstack/tests/unit/test_proxy_base.py index 544dac59d..7b4bf6cdc 100644 --- a/openstack/tests/unit/test_proxy_base.py +++ b/openstack/tests/unit/test_proxy_base.py @@ -80,7 +80,7 @@ def _verify2(self, mock_method, test_method, (called_args, called_kwargs) = mocked.call_args self.assertEqual(list(called_args), expected_args) base_path = expected_kwargs.get('base_path', None) - # NOTE(gtema): if base_path is not in epected_kwargs or empty + # NOTE(gtema): if base_path is not in expected_kwargs or empty # exclude it from the comparison, since some methods might # still invoke method with None value if not base_path: @@ -205,7 +205,7 @@ def verify_list(self, test_method, resource_type, **kwargs): expected_kwargs = kwargs.pop("expected_kwargs", {}) if 'paginated' in kwargs: - expected_kwargs.update({"paginated": kwargs['paginated']}) + expected_kwargs.update({"paginated": kwargs.pop('paginated')}) method_kwargs = kwargs.pop("method_kwargs", {}) self._verify2(mock_method, test_method, method_kwargs=method_kwargs, From 41f8ac16d03d30ca45fb1d2e28cc01c75d8ebf04 Mon Sep 17 00:00:00 2001 From: Paul Belanger Date: Thu, 21 Feb 2019 20:12:50 -0500 Subject: [PATCH 2377/3836] Fix syntax error with exception handling This would results in: ValueError: unexpected '{' in field name Change-Id: I0bb0327c854abbd9dc02818d900706905130efae Signed-off-by: Paul Belanger --- openstack/config/vendors/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/config/vendors/__init__.py b/openstack/config/vendors/__init__.py index bbe81b4ea..41badf54b 100644 --- a/openstack/config/vendors/__init__.py +++ b/openstack/config/vendors/__init__.py @@ -58,7 +58,7 @@ def get_profile(profile_name): if not response.ok: raise exceptions.ConfigException( "{profile_name} is a remote profile that could not be fetched:" - " ({status_code) {reason}".format( + " {status_code} {reason}".format( profile_name=profile_name, status_code=response.status_code, reason=response.reason)) From d0f9e1b31424b691183f16604efb81aba5b99adb Mon Sep 17 00:00:00 2001 From: Sergii Golovatiuk Date: Mon, 25 Feb 2019 14:02:36 +0100 Subject: [PATCH 2378/3836] Add missing py37 and corrected default envlist. Change-Id: I408c179e66a58f80972727ad270126319d7ded0b --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 7a525cd68..63be5d510 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 3.1 -envlist = py35,py36,py27,pep8 +envlist = pep8,py37,py36,py35,py27 skipsdist = True ignore_basepython_conflict = True From 3c0f44f62d1bdca3fecfda1ccd8a716a28f18881 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Mon, 25 Feb 2019 17:26:09 +0100 Subject: [PATCH 2379/3836] baremetal: support network_data when building configdrive This change adds support for network_data.json as defined in: https://specs.openstack.org/openstack/nova-specs/specs/liberty/implemented/metadata-service-network-info.html Also make user_data options, since it is actually optional. Change-Id: I28b7152e47bb6648b63d9216fbcdad22c1649888 Story: #2005083 --- openstack/baremetal/configdrive.py | 12 ++++++++++-- .../tests/unit/baremetal/test_configdrive.py | 18 ++++++++++++++++-- .../notes/network-data-deb5772edc111428.yaml | 6 ++++++ 3 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/network-data-deb5772edc111428.yaml diff --git a/openstack/baremetal/configdrive.py b/openstack/baremetal/configdrive.py index 504d332a2..abbebf5a3 100644 --- a/openstack/baremetal/configdrive.py +++ b/openstack/baremetal/configdrive.py @@ -25,12 +25,14 @@ @contextlib.contextmanager -def populate_directory(metadata, user_data, versions=None): +def populate_directory(metadata, user_data=None, versions=None, + network_data=None): """Populate a directory with configdrive files. :param dict metadata: Metadata. :param bytes user_data: Vendor-specific user data. :param versions: List of metadata versions to support. + :param dict network_data: Networking configuration. :return: a context manager yielding a directory with files """ d = tempfile.mkdtemp() @@ -44,6 +46,11 @@ def populate_directory(metadata, user_data, versions=None): with open(os.path.join(subdir, 'meta_data.json'), 'w') as fp: json.dump(metadata, fp) + if network_data: + with open(os.path.join(subdir, 'network_data.json'), + 'w') as fp: + json.dump(network_data, fp) + if user_data: with open(os.path.join(subdir, 'user_data'), 'wb') as fp: fp.write(user_data) @@ -53,7 +60,7 @@ def populate_directory(metadata, user_data, versions=None): shutil.rmtree(d) -def build(metadata, user_data, versions=None): +def build(metadata, user_data=None, versions=None, network_data=None): """Make a configdrive compatible with the Bare Metal service. Requires the genisoimage utility to be available. @@ -61,6 +68,7 @@ def build(metadata, user_data, versions=None): :param dict metadata: Metadata. :param user_data: Vendor-specific user data. :param versions: List of metadata versions to support. + :param dict network_data: Networking configuration. :return: configdrive contents as a base64-encoded string. """ with populate_directory(metadata, user_data, versions) as path: diff --git a/openstack/tests/unit/baremetal/test_configdrive.py b/openstack/tests/unit/baremetal/test_configdrive.py index 846e05c57..72cade0d5 100644 --- a/openstack/tests/unit/baremetal/test_configdrive.py +++ b/openstack/tests/unit/baremetal/test_configdrive.py @@ -24,16 +24,27 @@ class TestPopulateDirectory(testtools.TestCase): - def _check(self, metadata, user_data=None): - with configdrive.populate_directory(metadata, user_data) as d: + def _check(self, metadata, user_data=None, network_data=None): + with configdrive.populate_directory(metadata, + user_data=user_data, + network_data=network_data) as d: for version in ('2012-08-10', 'latest'): with open(os.path.join(d, 'openstack', version, 'meta_data.json')) as fp: actual_metadata = json.load(fp) self.assertEqual(metadata, actual_metadata) + network_data_file = os.path.join(d, 'openstack', version, + 'network_data.json') user_data_file = os.path.join(d, 'openstack', version, 'user_data') + + if network_data is None: + self.assertFalse(os.path.exists(network_data_file)) + else: + with open(network_data_file) as fp: + self.assertEqual(network_data, json.load(fp)) + if user_data is None: self.assertFalse(os.path.exists(user_data_file)) else: @@ -49,6 +60,9 @@ def test_without_user_data(self): def test_with_user_data(self): self._check({'foo': 42}, b'I am user data') + def test_with_network_data(self): + self._check({'foo': 42}, network_data={'networks': {}}) + @mock.patch('subprocess.Popen', autospec=True) class TestPack(testtools.TestCase): diff --git a/releasenotes/notes/network-data-deb5772edc111428.yaml b/releasenotes/notes/network-data-deb5772edc111428.yaml new file mode 100644 index 000000000..3cd9e2dbd --- /dev/null +++ b/releasenotes/notes/network-data-deb5772edc111428.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Adds support for `network_data + `_ + when building baremetal configdrives. From 4417a7e646ec6e76df80d147ac71c235dca39e5a Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 19 Feb 2019 18:34:44 +0100 Subject: [PATCH 2380/3836] Add image.task resource This change implements a Task resource of the image service with a wait_for_task method to wait for task to reach certain (normally 'success') status Change-Id: Ib74bb59bb06b6753720fc5047af1168c3cd66898 --- doc/source/user/proxies/image_v2.rst | 10 +++ doc/source/user/resources/image/index.rst | 1 + doc/source/user/resources/image/v2/task.rst | 12 ++++ openstack/image/v2/_proxy.py | 62 ++++++++++++++++ openstack/image/v2/task.py | 50 +++++++++++++ .../tests/functional/image/v2/test_image.py | 4 ++ openstack/tests/unit/image/v2/test_proxy.py | 17 +++++ openstack/tests/unit/image/v2/test_task.py | 71 +++++++++++++++++++ 8 files changed, 227 insertions(+) create mode 100644 doc/source/user/resources/image/v2/task.rst create mode 100644 openstack/image/v2/task.py create mode 100644 openstack/tests/unit/image/v2/test_task.py diff --git a/doc/source/user/proxies/image_v2.rst b/doc/source/user/proxies/image_v2.rst index 121e2cc7a..4e108948d 100644 --- a/doc/source/user/proxies/image_v2.rst +++ b/doc/source/user/proxies/image_v2.rst @@ -40,3 +40,13 @@ Member Operations .. automethod:: openstack.image.v2._proxy.Proxy.get_member .. automethod:: openstack.image.v2._proxy.Proxy.find_member .. automethod:: openstack.image.v2._proxy.Proxy.members + +Task Operations +^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.image.v2._proxy.Proxy + + .. automethod:: openstack.image.v2._proxy.Proxy.tasks + .. automethod:: openstack.image.v2._proxy.Proxy.create_task + .. automethod:: openstack.image.v2._proxy.Proxy.get_task + .. automethod:: openstack.image.v2._proxy.Proxy.wait_for_task diff --git a/doc/source/user/resources/image/index.rst b/doc/source/user/resources/image/index.rst index 2696ab1e1..dcafdcd3b 100644 --- a/doc/source/user/resources/image/index.rst +++ b/doc/source/user/resources/image/index.rst @@ -14,3 +14,4 @@ Image v2 Resources v2/image v2/member + v2/task diff --git a/doc/source/user/resources/image/v2/task.rst b/doc/source/user/resources/image/v2/task.rst new file mode 100644 index 000000000..3e6652e99 --- /dev/null +++ b/doc/source/user/resources/image/v2/task.rst @@ -0,0 +1,12 @@ +openstack.image.v2.task +======================= + +.. automodule:: openstack.image.v2.task + +The Task Class +-------------- + +The ``Task`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.image.v2.task.Task + :members: diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 598115f5f..2c3c48489 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -14,6 +14,7 @@ from openstack.image.v2 import image as _image from openstack.image.v2 import member as _member from openstack.image.v2 import schema as _schema +from openstack.image.v2 import task as _task from openstack import proxy from openstack import resource @@ -348,3 +349,64 @@ def get_member_schema(self): """ return self._get(_schema.Schema, requires_id=False, base_path='/schemas/member') + + def tasks(self, **query): + """Return a generator of tasks + + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. + + :returns: A generator of task objects + :rtype: :class:`~openstack.image.v2.task.Task` + """ + return self._list(_task.Task, **query) + + def get_task(self, task): + """Get task details + + :param task: The value can be the ID of a task or a + :class:`~openstack.image.v2.task.Task` instance. + + :returns: One :class:`~openstack.image.v2.task.Task` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_task.Task, task) + + def create_task(self, **attrs): + """Create a new task from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.image.v2.task.Task`, + comprised of the properties on the Task class. + + :returns: The results of task creation + :rtype: :class:`~openstack.image.v2.task.Task` + """ + return self._create(_task.Task, **attrs) + + def wait_for_task(self, task, status='success', failures=None, + interval=2, wait=120): + """Wait for a task to be in a particular status. + + :param task: The resource to wait on to reach the specified status. + The resource must have a ``status`` attribute. + :type resource: A :class:`~openstack.resource.Resource` object. + :param status: Desired status. + :param failures: Statuses that would be interpreted as failures. + :type failures: :py:class:`list` + :param interval: Number of seconds to wait before to consecutive + checks. Default to 2. + :param wait: Maximum number of seconds to wait before the change. + Default to 120. + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to the desired status failed to occur in specified seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + has transited to one of the failure statuses. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute. + """ + failures = ['failure'] if failures is None else failures + return resource.wait_for_status( + self, task, status, failures, interval, wait) diff --git a/openstack/image/v2/task.py b/openstack/image/v2/task.py new file mode 100644 index 000000000..9c14b848d --- /dev/null +++ b/openstack/image/v2/task.py @@ -0,0 +1,50 @@ +# 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 resource + + +class Task(resource.Resource): + resources_key = 'tasks' + base_path = '/tasks' + + # capabilities + allow_create = True + allow_fetch = True + allow_list = True + + _query_mapping = resource.QueryParameters( + 'type', 'status', 'sort_dir', 'sort_key' + ) + + #: The date and time when the task was created. + created_at = resource.Body('created_at') + #: The date and time when the task is subject to removal. + expires_at = resource.Body('expires_at') + #: A JSON object specifying the input parameters to the task. + input = resource.Body('input') + #: Human-readable text, possibly an empty string, usually displayed + #: in an error situation to provide more information about what + #: has occurred. + message = resource.Body('message') + #: The ID of the owner, or project, of the task. + owner_id = resource.Body('owner') + #: A JSON object specifying the outcome of the task. + result = resource.Body('result') + #: The URL for schema of the task. + schema = resource.Body('schema') + #: The status of the task. + status = resource.Body('status') + #: The type of task represented by this content. + type = resource.Body('type') + #: The date and time when the task was updated. + updated_at = resource.Body('updated_at') diff --git a/openstack/tests/functional/image/v2/test_image.py b/openstack/tests/functional/image/v2/test_image.py index 11a39e009..badd31fc5 100644 --- a/openstack/tests/functional/image/v2/test_image.py +++ b/openstack/tests/functional/image/v2/test_image.py @@ -61,3 +61,7 @@ def test_get_members_schema(self): def test_get_member_schema(self): schema = self.conn.image.get_member_schema() self.assertIsNotNone(schema) + + def test_list_tasks(self): + tasks = self.conn.image.tasks() + self.assertIsNotNone(tasks) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 19bf6f860..5af325436 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -17,6 +17,7 @@ from openstack.image.v2 import image from openstack.image.v2 import member from openstack.image.v2 import schema +from openstack.image.v2 import task from openstack.tests.unit.image.v2 import test_image as fake_image from openstack.tests.unit import test_proxy_base @@ -183,3 +184,19 @@ def test_member_schema_get(self): expected_args=[schema.Schema], expected_kwargs={'base_path': '/schemas/member', 'requires_id': False}) + + def test_task_get(self): + self.verify_get(self.proxy.get_task, task.Task) + + def test_tasks(self): + self.verify_list(self.proxy.tasks, task.Task) + + def test_task_create(self): + self.verify_create(self.proxy.create_task, task.Task) + + def test_task_wait_for(self): + value = task.Task(id='1234') + self.verify_wait_for_status( + self.proxy.wait_for_task, + method_args=[value], + expected_args=[value, 'success', ['failure'], 2, 120]) diff --git a/openstack/tests/unit/image/v2/test_task.py b/openstack/tests/unit/image/v2/test_task.py new file mode 100644 index 000000000..f137f0611 --- /dev/null +++ b/openstack/tests/unit/image/v2/test_task.py @@ -0,0 +1,71 @@ +# 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.tests.unit import base + +from openstack.image.v2 import task + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'created_at': '2016-06-24T14:40:19Z', + 'id': IDENTIFIER, + 'input': { + 'image_properties': { + 'container_format': 'ovf', + 'disk_format': 'vhd' + }, + 'import_from': 'http://example.com', + 'import_from_format': 'qcow2' + }, + 'message': 'message', + 'owner': 'fa6c8c1600f4444281658a23ee6da8e8', + 'result': 'some result', + 'schema': '/v2/schemas/task', + 'status': 'processing', + 'type': 'import', + 'updated_at': '2016-06-24T14:40:20Z' +} + + +class TestTask(base.TestCase): + def test_basic(self): + sot = task.Task() + self.assertIsNone(sot.resource_key) + self.assertEqual('tasks', sot.resources_key) + self.assertEqual('/tasks', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_delete) + self.assertTrue(sot.allow_list) + + self.assertDictEqual({'limit': 'limit', + 'marker': 'marker', + 'sort_dir': 'sort_dir', + 'sort_key': 'sort_key', + 'status': 'status', + 'type': 'type', + }, + sot._query_mapping._mapping) + + def test_make_it(self): + sot = task.Task(**EXAMPLE) + self.assertEqual(IDENTIFIER, sot.id) + self.assertEqual(EXAMPLE['created_at'], sot.created_at) + self.assertEqual(EXAMPLE['input'], sot.input) + self.assertEqual(EXAMPLE['message'], sot.message) + self.assertEqual(EXAMPLE['owner'], sot.owner_id) + self.assertEqual(EXAMPLE['result'], sot.result) + self.assertEqual(EXAMPLE['schema'], sot.schema) + self.assertEqual(EXAMPLE['status'], sot.status) + self.assertEqual(EXAMPLE['type'], sot.type) + self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) From d8fafcfe52f2646ccdaa169f21bb7e04e8e3ed1e Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Tue, 26 Feb 2019 19:27:32 +0000 Subject: [PATCH 2381/3836] Use mock context in test_fwaas Instead of mocking objects and restoring the original value at the end of the test, this patch uses the mock context. The original implementation is prone to errors. If the test doesn't finish correctly, the original object is never restored. However inside a mock context, if the test exits prematurely, the original object is restored. Change-Id: Ia333230b906470e27da326b7365049f79b7dcf6a --- openstack/tests/unit/cloud/test_fwaas.py | 233 ++++++++++------------- 1 file changed, 100 insertions(+), 133 deletions(-) diff --git a/openstack/tests/unit/cloud/test_fwaas.py b/openstack/tests/unit/cloud/test_fwaas.py index bd8efdfe6..c383c4b6e 100644 --- a/openstack/tests/unit/cloud/test_fwaas.py +++ b/openstack/tests/unit/cloud/test_fwaas.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. from copy import deepcopy -from mock import Mock +import mock from openstack import exceptions from openstack.network.v2.firewall_group import FirewallGroup @@ -121,8 +121,6 @@ def test_delete_firewall_rule_filters(self): self.assert_calls() def test_delete_firewall_rule_not_found(self): - _delete = self.cloud.network.delete_firewall_rule - _log = self.cloud.log.debug self.register_uris([ dict(method='GET', # short-circuit uri=self._make_mock_url('firewall_rules', @@ -132,18 +130,14 @@ def test_delete_firewall_rule_not_found(self): uri=self._make_mock_url('firewall_rules'), json={'firewall_rules': []}) ]) - self.cloud.network.delete_firewall_rule = Mock() - self.cloud.log.debug = Mock() - self.assertFalse( - self.cloud.delete_firewall_rule(self.firewall_rule_name)) - - self.cloud.network.delete_firewall_rule.assert_not_called() - self.cloud.log.debug.assert_called_once() + with mock.patch.object(self.cloud.network, 'delete_firewall_rule'), \ + mock.patch.object(self.cloud.log, 'debug'): + self.assertFalse( + self.cloud.delete_firewall_rule(self.firewall_rule_name)) - # restore methods - self.cloud.network.delete_firewall_rule = _delete - self.cloud.log.debug = _log + self.cloud.network.delete_firewall_rule.assert_not_called() + self.cloud.log.debug.assert_called_once() def test_delete_firewall_multiple_matches(self): self.register_uris([ @@ -305,8 +299,6 @@ def test_create_firewall_policy(self): def test_create_firewall_policy_rule_not_found(self): posted_policy = deepcopy(self._mock_firewall_policy_attrs) del posted_policy['id'] - _create = self.cloud.network.create_firewall_policy - self.cloud.network.create_firewall_policy = Mock() self.register_uris([ dict(method='GET', # short-circuit uri=self._make_mock_url('firewall_rules', @@ -316,16 +308,15 @@ def test_create_firewall_policy_rule_not_found(self): uri=self._make_mock_url('firewall_rules'), json={'firewall_rules': []}) ]) - self.assertRaises(exceptions.ResourceNotFound, - self.cloud.create_firewall_policy, **posted_policy) - self.cloud.network.create_firewall_policy.assert_not_called() - self.assert_calls() - # restore - self.cloud.network.create_firewall_policy = _create + + with mock.patch.object(self.cloud.network, 'create_firewall_policy'): + self.assertRaises(exceptions.ResourceNotFound, + self.cloud.create_firewall_policy, + **posted_policy) + self.cloud.network.create_firewall_policy.assert_not_called() + self.assert_calls() def test_delete_firewall_policy(self): - _log = self.cloud.log.debug - self.cloud.log.debug = Mock() self.register_uris([ dict(method='GET', # short-circuit uri=self._make_mock_url('firewall_policies', @@ -339,37 +330,32 @@ def test_delete_firewall_policy(self): self.firewall_policy_id), json={}, status_code=204) ]) - self.assertTrue( - self.cloud.delete_firewall_policy(self.firewall_policy_name)) - self.assert_calls() - self.cloud.log.debug.assert_not_called() - # restore - self.cloud.log.debug = _log + with mock.patch.object(self.cloud.log, 'debug'): + self.assertTrue( + self.cloud.delete_firewall_policy(self.firewall_policy_name)) + self.assert_calls() + self.cloud.log.debug.assert_not_called() def test_delete_firewall_policy_filters(self): filters = {'project_id': self.mock_firewall_policy['project_id']} - _find = self.cloud.network.find_firewall_policy - _log = self.cloud.log.debug - self.cloud.log.debug = Mock() - self.cloud.network.find_firewall_policy = Mock( - return_value=self.mock_firewall_policy) self.register_uris([ dict(method='DELETE', uri=self._make_mock_url('firewall_policies', self.firewall_policy_id), json={}, status_code=204) ]) - self.assertTrue( - self.cloud.delete_firewall_policy(self.firewall_policy_name, - filters)) - self.assert_calls() - self.cloud.network.find_firewall_policy.assert_called_once_with( - self.firewall_policy_name, ignore_missing=False, **filters) - self.cloud.log.debug.assert_not_called() - # restore - self.cloud.network.find_firewall_policy = _find - self.cloud.log.debug = _log + + with mock.patch.object(self.cloud.network, 'find_firewall_policy', + return_value=self.mock_firewall_policy), \ + mock.patch.object(self.cloud.log, 'debug'): + self.assertTrue( + self.cloud.delete_firewall_policy(self.firewall_policy_name, + filters)) + self.assert_calls() + self.cloud.network.find_firewall_policy.assert_called_once_with( + self.firewall_policy_name, ignore_missing=False, **filters) + self.cloud.log.debug.assert_not_called() def test_delete_firewall_policy_not_found(self): self.register_uris([ @@ -381,14 +367,12 @@ def test_delete_firewall_policy_not_found(self): uri=self._make_mock_url('firewall_policies'), json={'firewall_policies': []}) ]) - _log = self.cloud.log.debug - self.cloud.log.debug = Mock() - self.assertFalse( - self.cloud.delete_firewall_policy(self.firewall_policy_name)) - self.assert_calls() - self.cloud.log.debug.assert_called_once() - # restore - self.cloud.log.debug = _log + + with mock.patch.object(self.cloud.log, 'debug'): + self.assertFalse( + self.cloud.delete_firewall_policy(self.firewall_policy_name)) + self.assert_calls() + self.cloud.log.debug.assert_called_once() def test_get_firewall_policy(self): self.register_uris([ @@ -510,10 +494,6 @@ def test_update_firewall_policy_filters(self): updated_policy = deepcopy(self.mock_firewall_policy) updated_policy.update(params) - _find = self.cloud.network.find_firewall_policy - self.cloud.network.find_firewall_policy = Mock( - return_value=deepcopy(self.mock_firewall_policy)) - self.register_uris([ dict(method='PUT', uri=self._make_mock_url('firewall_policies', @@ -521,14 +501,17 @@ def test_update_firewall_policy_filters(self): json={'firewall_policy': updated_policy}, validate=dict(json={'firewall_policy': params})), ]) - self.assertDictEqual(updated_policy, - self.cloud.update_firewall_policy( - self.firewall_policy_name, filters, **params)) - self.assert_calls() - self.cloud.network.find_firewall_policy.assert_called_once_with( - self.firewall_policy_name, ignore_missing=False, **filters) - # restore - self.cloud.network.find_firewall_policy = _find + + with mock.patch.object(self.cloud.network, 'find_firewall_policy', + return_value=deepcopy( + self.mock_firewall_policy)): + self.assertDictEqual( + updated_policy, + self.cloud.update_firewall_policy(self.firewall_policy_name, + filters, **params)) + self.assert_calls() + self.cloud.network.find_firewall_policy.assert_called_once_with( + self.firewall_policy_name, ignore_missing=False, **filters) def test_insert_rule_into_policy(self): rule0 = FirewallRule( @@ -638,8 +621,6 @@ def test_insert_rule_into_policy_compact(self): def test_insert_rule_into_policy_not_found(self): policy_name = 'bogus_policy' - _find_rule = self.cloud.network.find_firewall_rule - self.cloud.network.find_firewall_rule = Mock() self.register_uris([ dict(method='GET', # short-circuit uri=self._make_mock_url('firewall_policies', policy_name), @@ -648,13 +629,13 @@ def test_insert_rule_into_policy_not_found(self): uri=self._make_mock_url('firewall_policies'), json={'firewall_policies': []}) ]) - self.assertRaises(exceptions.ResourceNotFound, - self.cloud.insert_rule_into_policy, - policy_name, 'bogus_rule') - self.assert_calls() - self.cloud.network.find_firewall_rule.assert_not_called() - # restore - self.cloud.network.find_firewall_rule = _find_rule + + with mock.patch.object(self.cloud.network, 'find_firewall_rule'): + self.assertRaises(exceptions.ResourceNotFound, + self.cloud.insert_rule_into_policy, + policy_name, 'bogus_rule') + self.assert_calls() + self.cloud.network.find_firewall_rule.assert_not_called() def test_insert_rule_into_policy_rule_not_found(self): rule_name = 'unknown_rule' @@ -676,8 +657,6 @@ def test_insert_rule_into_policy_rule_not_found(self): self.assert_calls() def test_insert_rule_into_policy_already_associated(self): - _log = self.cloud.log.debug - self.cloud.log.debug = Mock() rule = FirewallRule( **TestFirewallRule._mock_firewall_rule_attrs).to_dict() policy = deepcopy(self.mock_firewall_policy) @@ -691,12 +670,12 @@ def test_insert_rule_into_policy_already_associated(self): uri=self._make_mock_url('firewall_rules', rule['id']), json={'firewall_rule': rule}) ]) - r = self.cloud.insert_rule_into_policy(policy['id'], rule['id']) - self.assertDictEqual(policy, r.to_dict()) - self.assert_calls() - self.cloud.log.debug.assert_called() - # restore - self.cloud.log.debug = _log + + with mock.patch.object(self.cloud.log, 'debug'): + r = self.cloud.insert_rule_into_policy(policy['id'], rule['id']) + self.assertDictEqual(policy, r.to_dict()) + self.assert_calls() + self.cloud.log.debug.assert_called() def test_remove_rule_from_policy(self): policy_name = self.firewall_policy_name @@ -734,8 +713,6 @@ def test_remove_rule_from_policy(self): self.assert_calls() def test_remove_rule_from_policy_not_found(self): - _find_rule = self.cloud.network.find_firewall_rule - self.cloud.network.find_firewall_rule = Mock() self.register_uris([ dict(method='GET', # short-circuit uri=self._make_mock_url('firewall_policies', @@ -745,14 +722,14 @@ def test_remove_rule_from_policy_not_found(self): uri=self._make_mock_url('firewall_policies'), json={'firewall_policies': []}) ]) - self.assertRaises(exceptions.ResourceNotFound, - self.cloud.remove_rule_from_policy, - self.firewall_policy_name, - TestFirewallRule.firewall_rule_name) - self.assert_calls() - self.cloud.network.find_firewall_rule.assert_not_called() - # restore - self.cloud.network.find_firewall_rule = _find_rule + + with mock.patch.object(self.cloud.network, 'find_firewall_rule'): + self.assertRaises(exceptions.ResourceNotFound, + self.cloud.remove_rule_from_policy, + self.firewall_policy_name, + TestFirewallRule.firewall_rule_name) + self.assert_calls() + self.cloud.network.find_firewall_rule.assert_not_called() def test_remove_rule_from_policy_rule_not_found(self): retrieved_policy = deepcopy(self.mock_firewall_policy) @@ -782,10 +759,6 @@ def test_remove_rule_from_policy_not_associated(self): policy = deepcopy(self.mock_firewall_policy) del policy['firewall_rules'][0] - _log = self.cloud.log.debug - _remove = self.cloud.network.remove_rule_from_policy - self.cloud.log.debug = Mock() - self.cloud.network.remove_rule_from_policy = Mock() self.register_uris([ dict(method='GET', uri=self._make_mock_url('firewall_policies', policy['id']), @@ -794,14 +767,14 @@ def test_remove_rule_from_policy_not_associated(self): uri=self._make_mock_url('firewall_rules', rule['id']), json={'firewall_rule': rule}) ]) - r = self.cloud.remove_rule_from_policy(policy['id'], rule['id']) - self.assertDictEqual(policy, r.to_dict()) - self.assert_calls() - self.cloud.log.debug.assert_called_once() - self.cloud.network.remove_rule_from_policy.assert_not_called() - # restore - self.cloud.log.debug = _log - self.cloud.network.remove_rule_from_policy = _remove + + with mock.patch.object(self.cloud.network, 'remove_rule_from_policy'),\ + mock.patch.object(self.cloud.log, 'debug'): + r = self.cloud.remove_rule_from_policy(policy['id'], rule['id']) + self.assertDictEqual(policy, r.to_dict()) + self.assert_calls() + self.cloud.log.debug.assert_called_once() + self.cloud.network.remove_rule_from_policy.assert_not_called() class TestFirewallGroup(FirewallTestCase): @@ -945,24 +918,22 @@ def test_delete_firewall_group(self): def test_delete_firewall_group_filters(self): filters = {'project_id': self.mock_firewall_group['project_id']} - _find = self.cloud.network.find_firewall_group - self.cloud.network.find_firewall_group = Mock( - return_value=deepcopy(self.mock_firewall_group)) self.register_uris([ dict(method='DELETE', uri=self._make_mock_url('firewall_groups', self.firewall_group_id), status_code=204) ]) - self.assertTrue( - self.cloud.delete_firewall_group(self.firewall_group_name, - filters)) - self.assert_calls() - self.cloud.network.find_firewall_group.assert_called_once_with( - self.firewall_group_name, ignore_missing=False, **filters) - # restore - self.cloud.network.find_firewall_group = _find + with mock.patch.object(self.cloud.network, 'find_firewall_group', + return_value=deepcopy( + self.mock_firewall_group)): + self.assertTrue( + self.cloud.delete_firewall_group(self.firewall_group_name, + filters)) + self.assert_calls() + self.cloud.network.find_firewall_group.assert_called_once_with( + self.firewall_group_name, ignore_missing=False, **filters) def test_delete_firewall_group_not_found(self): self.register_uris([ @@ -974,14 +945,12 @@ def test_delete_firewall_group_not_found(self): uri=self._make_mock_url('firewall_groups'), json={'firewall_groups': []}) ]) - _log = self.cloud.log.debug - self.cloud.log.debug = Mock() - self.assertFalse( - self.cloud.delete_firewall_group(self.firewall_group_name)) - self.assert_calls() - self.cloud.log.debug.assert_called_once() - # restore - self.cloud.log.debug = _log + + with mock.patch.object(self.cloud.log, 'debug'): + self.assertFalse( + self.cloud.delete_firewall_group(self.firewall_group_name)) + self.assert_calls() + self.cloud.log.debug.assert_called_once() def test_get_firewall_group(self): returned_group = deepcopy(self.mock_returned_firewall_group) @@ -1123,9 +1092,6 @@ def test_update_firewall_group_compact(self): def test_update_firewall_group_filters(self): filters = {'project_id': self.mock_firewall_group['project_id']} - _find = self.cloud.network.find_firewall_group - self.cloud.network.find_firewall_group = Mock( - return_value=deepcopy(self.mock_firewall_group)) params = {'description': 'updated again!'} updated_group = deepcopy(self.mock_returned_firewall_group) self.register_uris([ @@ -1135,15 +1101,16 @@ def test_update_firewall_group_filters(self): json={'firewall_group': updated_group}, validate=dict(json={'firewall_group': params})) ]) - r = self.cloud.update_firewall_group(self.firewall_group_name, filters, - **params) - self.assertDictEqual(updated_group, r.to_dict()) - self.assert_calls() - self.cloud.network.find_firewall_group.assert_called_once_with( - self.firewall_group_name, ignore_missing=False, **filters) - # restore - self.cloud.network.find_firewall_group = _find + with mock.patch.object(self.cloud.network, 'find_firewall_group', + return_value=deepcopy( + self.mock_firewall_group)): + r = self.cloud.update_firewall_group(self.firewall_group_name, + filters, **params) + self.assertDictEqual(updated_group, r.to_dict()) + self.assert_calls() + self.cloud.network.find_firewall_group.assert_called_once_with( + self.firewall_group_name, ignore_missing=False, **filters) def test_update_firewall_group_unset_policies(self): transformed_params = {'ingress_firewall_policy_id': None, From 2742e28ee4c96a284d1a0a9691a38fd51d52bb1a Mon Sep 17 00:00:00 2001 From: Lajos Katona Date: Wed, 6 Feb 2019 12:42:03 +0100 Subject: [PATCH 2382/3836] Add agent property: resources-synced Agents supporting the guaranteed minimum bandwidth feature need to share their resource view with neutron-server and in turn with Placement. The success of this synchronization represented by the agent attribute resources-synced. Change-Id: I764a50884d8f23bbcad25825676c897107ed8f3f Depends-On: https://review.openstack.org/630999 Partial-Bug: #1578989 See-Also: https://review.openstack.org/502306 (nova spec) See-Also: https://review.openstack.org/508149 (neutron spec) --- openstack/network/v2/agent.py | 9 +++++++++ openstack/tests/unit/network/v2/test_agent.py | 2 ++ 2 files changed, 11 insertions(+) diff --git a/openstack/network/v2/agent.py b/openstack/network/v2/agent.py index b6a6097fe..09054d540 100644 --- a/openstack/network/v2/agent.py +++ b/openstack/network/v2/agent.py @@ -57,6 +57,15 @@ class Agent(resource.Resource): #: Whether or not the network agent is alive. #: *Type: bool* is_alive = resource.Body('alive', type=bool) + #: Whether or not the agent is succesffully synced towards placement. + #: Agents supporting the guaranteed minimum bandwidth feature share their + #: resource view with neutron-server and neutron-server share this view + #: with placement, resources_synced represents the success of the latter. + #: The value None means no resource view synchronization to Placement was + #: attempted. true / false values signify the success of the last + #: synchronization attempt. + #: *Type: bool* + resources_synced = resource.Body('resources_synced', type=bool) #: Timestamp when the network agent was last started. started_at = resource.Body('started_at') #: The messaging queue topic the network agent subscribes to. diff --git a/openstack/tests/unit/network/v2/test_agent.py b/openstack/tests/unit/network/v2/test_agent.py index f21880470..19316dea0 100644 --- a/openstack/tests/unit/network/v2/test_agent.py +++ b/openstack/tests/unit/network/v2/test_agent.py @@ -28,6 +28,7 @@ 'heartbeat_timestamp': '2016-08-09T12:14:57.233772', 'host': 'test-host', 'id': IDENTIFIER, + 'resources_synced': False, 'started_at': '2016-07-09T12:14:57.233772', 'topic': 'test-topic', 'ha_state': 'active' @@ -61,6 +62,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['heartbeat_timestamp'], sot.last_heartbeat_at) self.assertEqual(EXAMPLE['host'], sot.host) self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['resources_synced'], sot.resources_synced) self.assertEqual(EXAMPLE['started_at'], sot.started_at) self.assertEqual(EXAMPLE['topic'], sot.topic) self.assertEqual(EXAMPLE['ha_state'], sot.ha_state) From 5e09afc80e6bffdb8b63b51cc6b76efdcbf9481b Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 28 Feb 2019 12:12:04 +0100 Subject: [PATCH 2383/3836] Tweak find_image method to search in hidden images With Image v2.7 a possibility of hiding and image is being added (an attribute 'os_hidden'). After an image is being hidden the only possibility to find image by name is to do a list with `os_hidden=True` to list hidden images. So what we do here is search as usual, and if nothing found try also to search in hidden images (list of hidden images and grep by name) Change-Id: I237da3e7e6b5d23010d8b26a3fcbfabaddeb5f31 --- openstack/image/v2/image.py | 22 +++++++++++++ openstack/tests/unit/image/v2/test_image.py | 34 ++++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index e58a7b33f..8005ac13b 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -301,3 +301,25 @@ def _prepare_request(self, requires_id=None, prepend_key=False, request.headers.update(headers) return request + + @classmethod + def find(cls, session, name_or_id, ignore_missing=True, **params): + # Do a regular search first (ignoring missing) + result = super(Image, cls).find(session, name_or_id, True, + **params) + + if result: + return result + else: + # Search also in hidden images + params['is_hidden'] = True + data = cls.list(session, **params) + + result = cls._get_one_match(name_or_id, data) + if result is not None: + return result + + if ignore_missing: + return None + raise exceptions.ResourceNotFound( + "No %s found for %s" % (cls.__name__, name_or_id)) diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 7f506a1fe..ebdbd8899 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -86,12 +86,16 @@ class FakeResponse(object): - def __init__(self, response, status_code=200, headers=None): + def __init__(self, response, status_code=200, headers=None, reason=None): self.body = response self.content = response self.status_code = status_code headers = headers if headers else {'content-type': 'application/json'} self.headers = requests.structures.CaseInsensitiveDict(headers) + if reason: + self.reason = reason + # for the sake of "list" response faking + self.links = [] def json(self): return self.body @@ -108,6 +112,7 @@ def setUp(self): self.sess.post = mock.Mock(return_value=self.resp) self.sess.put = mock.Mock(return_value=FakeResponse({})) self.sess.delete = mock.Mock(return_value=FakeResponse({})) + self.sess.fetch = mock.Mock(return_value=FakeResponse({})) self.sess.default_microversion = None self.sess.retriable_status_codes = None @@ -366,3 +371,30 @@ def test_image_update(self): self.assertEqual( sorted(value, key=operator.itemgetter('value')), sorted(call_kwargs['json'], key=operator.itemgetter('value'))) + + def test_image_find(self): + sot = image.Image() + + self.sess._get_connection = mock.Mock(return_value=self.cloud) + self.sess.get.side_effect = [ + # First fetch by name + FakeResponse(None, 404, headers={}, reason='dummy'), + # Then list with no results + FakeResponse({'images': []}), + # And finally new list of hidden images with one searched + FakeResponse({'images': [EXAMPLE]}) + + ] + + result = sot.find(self.sess, EXAMPLE['name']) + + self.sess.get.assert_has_calls([ + mock.call('images/' + EXAMPLE['name'], microversion=None), + mock.call('/images', headers={'Accept': 'application/json'}, + microversion=None, params={}), + mock.call('/images', headers={'Accept': 'application/json'}, + microversion=None, params={'os_hidden': True}) + ]) + + self.assertIsInstance(result, image.Image) + self.assertEqual(IDENTIFIER, result.id) From 232553daf7b30972d8e135b502a5e24b8bd8cc09 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 25 Feb 2019 11:56:02 +0100 Subject: [PATCH 2384/3836] Move image methods to sdk image proxy We have excellent image uploading code - people should get to use it whether they use the abstraction layer or not. The sdk is version specific, so we can split v1 and v2 using those classes. Make a base class for both proxies so that we can define a general interface and handle some of the argument normalization and processing. NOTE: This is very unfinished. The proxy methods should be transformed to using the Resource layer. There are many places where calls back in to the Connection haven't had self._connection pre-pended to them. The wait logic needs to be reworked. We should make a v2.ImageTask resource (I think) with a wait method - and a v2.Image with a wait method so that we can have a proxy wait_for_image method that will work fully for put and task. Then we should remove the wait loops from the shade layer and have it call self.image.wait_for_image(image) if wait/timeout have been passed. At the end of this, create_image in shade should basically be: if volume: self.block_storage.create_image() else: self.image.create_image() if wait: self.image.wait_for_image(wait, timeout) This is also a straw man for a general approach to shifting important logic into the sdk layer so that it can be shared, but also keep things like the wait/timeout and "call image or block-storage api calls" in shade. The block_storage.create_image is going to be interesting - because it realy needs to return an Image resource. I think the existing code is racey/buggy - because for not-wait it returns get_image(image_id) - but I'm pretty sure that can't possibly be guaranteed to exist that instant. However, with Image resource we can just create a blank Image object with image_id filled in, and that blank object can be used as a parameter to wait_for_image. Change-Id: Idfeb25e8d6b20d7f5ea218aaf05af9a52fb1cfb8 --- openstack/block_storage/_base_proxy.py | 50 +++ openstack/block_storage/v2/_proxy.py | 4 +- openstack/block_storage/v3/_proxy.py | 4 +- openstack/cloud/openstackcloud.py | 379 +----------------- openstack/image/_base_proxy.py | 180 +++++++++ openstack/image/v1/_proxy.py | 71 +++- openstack/image/v2/_proxy.py | 225 ++++++++++- openstack/tests/unit/base.py | 9 + openstack/tests/unit/cloud/test_image.py | 2 + .../unit/fixtures/block-storage-version.json | 28 ++ 10 files changed, 578 insertions(+), 374 deletions(-) create mode 100644 openstack/block_storage/_base_proxy.py create mode 100644 openstack/image/_base_proxy.py create mode 100644 openstack/tests/unit/fixtures/block-storage-version.json diff --git a/openstack/block_storage/_base_proxy.py b/openstack/block_storage/_base_proxy.py new file mode 100644 index 000000000..97b8b4b9c --- /dev/null +++ b/openstack/block_storage/_base_proxy.py @@ -0,0 +1,50 @@ +# 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 six + +from openstack import exceptions +from openstack import proxy + + +class BaseBlockStorageProxy(six.with_metaclass(abc.ABCMeta, proxy.Proxy)): + + def create_image( + self, name, volume, allow_duplicates, + container_format, disk_format, wait, timeout): + if not disk_format: + disk_format = self._connection.config.config['image_format'] + if not container_format: + # https://docs.openstack.org/image-guide/image-formats.html + container_format = 'bare' + + if 'id' in volume: + volume_id = volume['id'] + else: + volume_obj = self.get_volume(volume) + if not volume_obj: + raise exceptions.SDKException( + "Volume {volume} given to create_image could" + " not be found".format(volume=volume)) + volume_id = volume_obj['id'] + data = self.post( + '/volumes/{id}/action'.format(id=volume_id), + json={ + 'os-volume_upload_image': { + 'force': allow_duplicates, + 'image_name': name, + 'container_format': container_format, + 'disk_format': disk_format}}) + response = self._connection._get_and_munchify( + 'os-volume_upload_image', data) + return self._connection.image._existing_image(id=response['image_id']) diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 8abb34cc1..57095afc7 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -10,17 +10,17 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.block_storage import _base_proxy from openstack.block_storage.v2 import backup as _backup from openstack.block_storage.v2 import snapshot as _snapshot from openstack.block_storage.v2 import stats as _stats from openstack.block_storage.v2 import type as _type from openstack.block_storage.v2 import volume as _volume from openstack import exceptions -from openstack import proxy from openstack import resource -class Proxy(proxy.Proxy): +class Proxy(_base_proxy.BaseBlockStorageProxy): def get_snapshot(self, snapshot): """Get a single snapshot diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 29b9740e9..4295f9d41 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -10,17 +10,17 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.block_storage import _base_proxy from openstack.block_storage.v3 import backup as _backup from openstack.block_storage.v3 import snapshot as _snapshot from openstack.block_storage.v3 import stats as _stats from openstack.block_storage.v3 import type as _type from openstack.block_storage.v3 import volume as _volume from openstack import exceptions -from openstack import proxy from openstack import resource -class Proxy(proxy.Proxy): +class Proxy(_base_proxy.BaseBlockStorageProxy): def get_snapshot(self, snapshot): """Get a single snapshot diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 16f5b6be1..450ef4d9a 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -54,8 +54,6 @@ import openstack.config.defaults from openstack import utils -# Rackspace returns this for intermittent import errors -IMAGE_ERROR_396 = "Image cannot be imported. Error code: '396'" DEFAULT_OBJECT_SEGMENT_SIZE = 1073741824 # 1GB # This halves the current default for Swift DEFAULT_MAX_FILE_SIZE = (5 * 1024 * 1024 * 1024 + 2) / 2 @@ -4738,84 +4736,22 @@ def create_image( :raises: OpenStackCloudException if there are problems uploading """ - if container is None: - container = self._OBJECT_AUTOCREATE_CONTAINER - if not meta: - meta = {} - - if not disk_format: - disk_format = self.config.config['image_format'] - if not container_format: - # https://docs.openstack.org/image-guide/image-formats.html - container_format = 'bare' - if volume: - if 'id' in volume: - volume_id = volume['id'] - else: - volume_obj = self.get_volume(volume) - if not volume_obj: - raise exc.OpenStackCloudException( - "Volume {volume} given to create_image could" - " not be foud".format(volume=volume)) - volume_id = volume_obj['id'] - return self._upload_image_from_volume( - name=name, volume_id=volume_id, + image = self.block_storage.create_image( + name=name, volume=volume, allow_duplicates=allow_duplicates, container_format=container_format, disk_format=disk_format, wait=wait, timeout=timeout) - - # If there is no filename, see if name is actually the filename - if not filename: - name, filename = self._get_name_and_filename(name) - if not (md5 or sha256): - (md5, sha256) = self._get_file_hashes(filename) - if allow_duplicates: - current_image = None - else: - current_image = self.get_image(name) - if current_image: - md5_key = current_image.get( - self._IMAGE_MD5_KEY, - current_image.get(self._SHADE_IMAGE_MD5_KEY, '')) - sha256_key = current_image.get( - self._IMAGE_SHA256_KEY, - current_image.get(self._SHADE_IMAGE_SHA256_KEY, '')) - up_to_date = self._hashes_up_to_date( - md5=md5, sha256=sha256, - md5_key=md5_key, sha256_key=sha256_key) - if up_to_date: - self.log.debug( - "image %(name)s exists and is up to date", - {'name': name}) - return current_image - kwargs[self._IMAGE_MD5_KEY] = md5 or '' - kwargs[self._IMAGE_SHA256_KEY] = sha256 or '' - kwargs[self._IMAGE_OBJECT_KEY] = '/'.join([container, name]) - - if disable_vendor_agent: - kwargs.update(self.config.config['disable_vendor_agent']) - - # If a user used the v1 calling format, they will have - # passed a dict called properties along - properties = kwargs.pop('properties', {}) - kwargs.update(properties) - image_kwargs = dict(properties=kwargs) - if disk_format: - image_kwargs['disk_format'] = disk_format - if container_format: - image_kwargs['container_format'] = container_format - - if self._is_client_version('image', 2): - image = self._upload_image_v2( - name, filename, - wait=wait, timeout=timeout, - meta=meta, **image_kwargs) else: - image = self._upload_image_v1( - name, filename, + image = self.image.create_image( + name, filename=filename, + container=container, + md5=sha256, sha256=sha256, + disk_format=disk_format, container_format=container_format, + disable_vendor_agent=disable_vendor_agent, wait=wait, timeout=timeout, - meta=meta, **image_kwargs) + allow_duplicates=allow_duplicates, meta=meta, **kwargs) + self._get_cache(None).invalidate() if not wait: return image @@ -4832,300 +4768,11 @@ def create_image( self.delete_image(image.id, wait=True) raise - def _upload_image_v2( - self, name, filename=None, - wait=False, timeout=3600, - meta=None, **kwargs): - # We can never have nice things. Glance v1 took "is_public" as a - # boolean. Glance v2 takes "visibility". If the user gives us - # is_public, we know what they mean. If they give us visibility, they - # know that they mean. - if 'is_public' in kwargs['properties']: - is_public = kwargs['properties'].pop('is_public') - if is_public: - kwargs['visibility'] = 'public' - else: - kwargs['visibility'] = 'private' - - try: - # This makes me want to die inside - if self.image_api_use_tasks: - return self._upload_image_task( - name, filename, - wait=wait, timeout=timeout, - meta=meta, **kwargs) - else: - return self._upload_image_put_v2( - name, filename, meta=meta, - **kwargs) - except exc.OpenStackCloudException: - self.log.debug("Image creation failed", exc_info=True) - raise - except Exception as e: - raise exc.OpenStackCloudException( - "Image creation failed: {message}".format(message=str(e))) - - def _make_v2_image_params(self, meta, properties): - ret = {} - for k, v in iter(properties.items()): - if k in ('min_disk', 'min_ram', 'size', 'virtual_size'): - ret[k] = int(v) - elif k == 'protected': - ret[k] = v - else: - if v is None: - ret[k] = None - else: - ret[k] = str(v) - ret.update(meta) - return ret - - def _upload_image_from_volume( - self, name, volume_id, allow_duplicates, - container_format, disk_format, wait, timeout): - data = self._volume_client.post( - '/volumes/{id}/action'.format(id=volume_id), - json={ - 'os-volume_upload_image': { - 'force': allow_duplicates, - 'image_name': name, - 'container_format': container_format, - 'disk_format': disk_format}}) - response = self._get_and_munchify('os-volume_upload_image', data) - - if not wait: - return self.get_image(response['image_id']) - try: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for the image to finish."): - image_obj = self.get_image(response['image_id']) - if image_obj and image_obj.status not in ('queued', 'saving'): - return image_obj - except exc.OpenStackCloudTimeout: - self.log.debug( - "Timeout waiting for image to become ready. Deleting.") - self.delete_image(response['image_id'], wait=True) - raise - - def _upload_image_put_v2(self, name, filename, meta, **image_kwargs): - image_data = open(filename, 'rb') - - properties = image_kwargs.pop('properties', {}) - - image_kwargs.update(self._make_v2_image_params(meta, properties)) - image_kwargs['name'] = name - - data = self._image_client.post('/images', json=image_kwargs) - image = self._get_and_munchify(key=None, data=data) - - try: - self._image_client.put( - '/images/{id}/file'.format(id=image.id), - headers={'Content-Type': 'application/octet-stream'}, - data=image_data) - - except Exception: - self.log.debug("Deleting failed upload of image %s", name) - try: - self._image_client.delete( - '/images/{id}'.format(id=image.id)) - except exc.OpenStackCloudHTTPError: - # We're just trying to clean up - if it doesn't work - shrug - self.log.debug( - "Failed deleting image after we failed uploading it.", - exc_info=True) - raise - - return self._normalize_image(image) - - def _upload_image_v1( - self, name, filename, - wait=False, timeout=3600, - meta=None, **image_kwargs): - # NOTE(mordred) wait and timeout parameters are unused, but - # are present for ease at calling site. - image_data = open(filename, 'rb') - image_kwargs['properties'].update(meta) - image_kwargs['name'] = name - - image = self._get_and_munchify( - 'image', - self._image_client.post('/images', json=image_kwargs)) - checksum = image_kwargs['properties'].get(self._IMAGE_MD5_KEY, '') - - try: - # Let us all take a brief moment to be grateful that this - # is not actually how OpenStack APIs work anymore - headers = { - 'x-glance-registry-purge-props': 'false', - } - if checksum: - headers['x-image-meta-checksum'] = checksum - - image = self._get_and_munchify( - 'image', - self._image_client.put( - '/images/{id}'.format(id=image.id), - headers=headers, data=image_data)) - - except exc.OpenStackCloudHTTPError: - self.log.debug("Deleting failed upload of image %s", name) - try: - self._image_client.delete( - '/images/{id}'.format(id=image.id)) - except exc.OpenStackCloudHTTPError: - # We're just trying to clean up - if it doesn't work - shrug - self.log.debug( - "Failed deleting image after we failed uploading it.", - exc_info=True) - raise - return self._normalize_image(image) - - def _upload_image_task( - self, name, filename, - wait, timeout, meta, **image_kwargs): - - properties = image_kwargs.pop('properties', {}) - md5 = properties[self._IMAGE_MD5_KEY] - sha256 = properties[self._IMAGE_SHA256_KEY] - container = properties[self._IMAGE_OBJECT_KEY].split('/', 1)[0] - image_kwargs.update(properties) - image_kwargs.pop('disk_format', None) - image_kwargs.pop('container_format', None) - - self.create_container(container) - self.create_object( - container, name, filename, - md5=md5, sha256=sha256, - metadata={self._OBJECT_AUTOCREATE_KEY: 'true'}, - **{'content-type': 'application/octet-stream'}) - # TODO(mordred): Can we do something similar to what nodepool does - # using glance properties to not delete then upload but instead make a - # new "good" image and then mark the old one as "bad" - task_args = dict( - type='import', input=dict( - import_from='{container}/{name}'.format( - container=container, name=name), - image_properties=dict(name=name))) - data = self._image_client.post('/tasks', json=task_args) - glance_task = self._get_and_munchify(key=None, data=data) - self.list_images.invalidate(self) - if wait: - start = time.time() - image_id = None - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for the image to import."): - try: - if image_id is None: - status = self._image_client.get( - '/tasks/{id}'.format(id=glance_task.id)) - except exc.OpenStackCloudHTTPError as e: - if e.response.status_code == 503: - # Clear the exception so that it doesn't linger - # and get reported as an Inner Exception later - _utils._exc_clear() - # Intermittent failure - catch and try again - continue - raise - - if status['status'] == 'success': - image_id = status['result']['image_id'] - try: - image = self.get_image(image_id) - except exc.OpenStackCloudHTTPError as e: - if e.response.status_code == 503: - # Clear the exception so that it doesn't linger - # and get reported as an Inner Exception later - _utils._exc_clear() - # Intermittent failure - catch and try again - continue - raise - if image is None: - continue - self.update_image_properties( - image=image, meta=meta, **image_kwargs) - self.log.debug( - "Image Task %s imported %s in %s", - glance_task.id, image_id, (time.time() - start)) - # Clean up after ourselves. The object we created is not - # needed after the import is done. - self.delete_object(container, name) - return self.get_image(image_id) - elif status['status'] == 'failure': - if status['message'] == IMAGE_ERROR_396: - glance_task = self._image_client.post( - '/tasks', data=task_args) - self.list_images.invalidate(self) - else: - # Clean up after ourselves. The image did not import - # and this isn't a 'just retry' error - glance didn't - # like the content. So we don't want to keep it for - # next time. - self.delete_object(container, name) - raise exc.OpenStackCloudException( - "Image creation failed: {message}".format( - message=status['message']), - extra_data=status) - else: - return glance_task - def update_image_properties( self, image=None, name_or_id=None, meta=None, **properties): - if image is None: - image = self.get_image(name_or_id) - - if not meta: - meta = {} - - img_props = {} - for k, v in iter(properties.items()): - if v and k in ['ramdisk', 'kernel']: - v = self.get_image_id(v) - k = '{0}_id'.format(k) - img_props[k] = v - - # This makes me want to die inside - if self._is_client_version('image', 2): - return self._update_image_properties_v2(image, meta, img_props) - else: - return self._update_image_properties_v1(image, meta, img_props) - - def _update_image_properties_v2(self, image, meta, properties): - img_props = image.properties.copy() - for k, v in iter(self._make_v2_image_params(meta, properties).items()): - if image.get(k, None) != v: - img_props[k] = v - if not img_props: - return False - headers = { - 'Content-Type': 'application/openstack-images-v2.1-json-patch'} - patch = sorted(list(jsonpatch.JsonPatch.from_diff( - image.properties, img_props)), key=operator.itemgetter('value')) - - # No need to fire an API call if there is an empty patch - if patch: - self._image_client.patch( - '/images/{id}'.format(id=image.id), - headers=headers, - data=json.dumps(patch)) - - self.list_images.invalidate(self) - return True - - def _update_image_properties_v1(self, image, meta, properties): - properties.update(meta) - img_props = {} - for k, v in iter(properties.items()): - if image.properties.get(k, None) != v: - img_props['x-image-meta-{key}'.format(key=k)] = v - if not img_props: - return False - self._image_client.put( - '/images/{id}'.format(id=image.id), headers=img_props) - self.list_images.invalidate(self) - return True + image = image or name_or_id + return self.image.update_image_properties( + image=image, meta=meta, **properties) def create_volume( self, size, diff --git a/openstack/image/_base_proxy.py b/openstack/image/_base_proxy.py new file mode 100644 index 000000000..ec97ba9ad --- /dev/null +++ b/openstack/image/_base_proxy.py @@ -0,0 +1,180 @@ +# 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 six + +from openstack import proxy + + +class BaseImageProxy(six.with_metaclass(abc.ABCMeta, proxy.Proxy)): + + def create_image( + self, name, filename=None, + container=None, + md5=None, sha256=None, + disk_format=None, container_format=None, + disable_vendor_agent=True, + allow_duplicates=False, meta=None, + wait=False, timeout=3600, + **kwargs): + """Upload an image. + + :param str name: Name of the image to create. If it is a pathname + of an image, the name will be constructed from the + extensionless basename of the path. + :param str filename: The path to the file to upload, if needed. + (optional, defaults to None) + :param str container: Name of the container in swift where images + should be uploaded for import if the cloud + requires such a thing. (optiona, defaults to + 'images') + :param str md5: md5 sum of the image file. If not given, an md5 will + be calculated. + :param str sha256: sha256 sum of the image file. If not given, an md5 + will be calculated. + :param str disk_format: The disk format the image is in. (optional, + defaults to the os-client-config config value + for this cloud) + :param str container_format: The container format the image is in. + (optional, defaults to the + os-client-config config value for this + cloud) + :param bool disable_vendor_agent: Whether or not to append metadata + flags to the image to inform the + cloud in question to not expect a + vendor agent to be runing. + (optional, defaults to True) + :param allow_duplicates: If true, skips checks that enforce unique + image name. (optional, defaults to False) + :param meta: A dict of key/value pairs to use for metadata that + bypasses automatic type conversion. + :param bool wait: If true, waits for image to be created. Defaults to + true - however, be aware that one of the upload + methods is always synchronous. + :param timeout: Seconds to wait for image creation. None is forever. + + Additional kwargs will be passed to the image creation as additional + metadata for the image and will have all values converted to string + except for min_disk, min_ram, size and virtual_size which will be + converted to int. + + If you are sure you have all of your data types correct or have an + advanced need to be explicit, use meta. If you are just a normal + consumer, using kwargs is likely the right choice. + + If a value is in meta and kwargs, meta wins. + + :returns: A ``munch.Munch`` of the Image object + + :raises: OpenStackCloudException if there are problems uploading + """ + if container is None: + container = self._connection._OBJECT_AUTOCREATE_CONTAINER + if not meta: + meta = {} + + if not disk_format: + disk_format = self._connection.config.config['image_format'] + if not container_format: + # https://docs.openstack.org/image-guide/image-formats.html + container_format = 'bare' + + # If there is no filename, see if name is actually the filename + if not filename: + name, filename = self._connection._get_name_and_filename(name) + if not (md5 or sha256): + (md5, sha256) = self._connection._get_file_hashes(filename) + if allow_duplicates: + current_image = None + else: + current_image = self._connection.get_image(name) + if current_image: + md5_key = current_image.get( + self._connection._IMAGE_MD5_KEY, + current_image.get( + self._connection._SHADE_IMAGE_MD5_KEY, '')) + sha256_key = current_image.get( + self._connection._IMAGE_SHA256_KEY, + current_image.get( + self._connection._SHADE_IMAGE_SHA256_KEY, '')) + up_to_date = self._connection._hashes_up_to_date( + md5=md5, sha256=sha256, + md5_key=md5_key, sha256_key=sha256_key) + if up_to_date: + self._connection.log.debug( + "image %(name)s exists and is up to date", + {'name': name}) + return current_image + kwargs[self._connection._IMAGE_MD5_KEY] = md5 or '' + kwargs[self._connection._IMAGE_SHA256_KEY] = sha256 or '' + kwargs[self._connection._IMAGE_OBJECT_KEY] = '/'.join( + [container, name]) + + if disable_vendor_agent: + kwargs.update( + self._connection.config.config['disable_vendor_agent']) + + # If a user used the v1 calling format, they will have + # passed a dict called properties along + properties = kwargs.pop('properties', {}) + kwargs.update(properties) + image_kwargs = dict(properties=kwargs) + if disk_format: + image_kwargs['disk_format'] = disk_format + if container_format: + image_kwargs['container_format'] = container_format + + image = self._upload_image( + name, filename, + wait=wait, timeout=timeout, + meta=meta, **image_kwargs) + self._connection._get_cache(None).invalidate() + return image + + @abc.abstractmethod + def _upload_image(self, name, filename, meta, **image_kwargs): + pass + + @abc.abstractmethod + def _update_image_properties(self, image, meta, properties): + pass + + def update_image_properties( + self, image=None, meta=None, **kwargs): + """ + Update the properties of an existing image. + + :param image: Name or id of an image or an Image object. + :param meta: A dict of key/value pairs to use for metadata that + bypasses automatic type conversion. + + Additional kwargs will be passed to the image creation as additional + metadata for the image and will have all values converted to string + except for min_disk, min_ram, size and virtual_size which will be + converted to int. + """ + + if image is None: + image = self._connection.get_image(image) + + if not meta: + meta = {} + + img_props = {} + for k, v in iter(kwargs.items()): + if v and k in ['ramdisk', 'kernel']: + v = self._connection.get_image_id(v) + k = '{0}_id'.format(k) + img_props[k] = v + + return self._update_image_properties(image, meta, img_props) diff --git a/openstack/image/v1/_proxy.py b/openstack/image/v1/_proxy.py index c3b1d0234..782641b76 100644 --- a/openstack/image/v1/_proxy.py +++ b/openstack/image/v1/_proxy.py @@ -9,16 +9,23 @@ # 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 warnings +from openstack.cloud import exc +from openstack.image import _base_proxy from openstack.image.v1 import image as _image -from openstack import proxy -class Proxy(proxy.Proxy): +class Proxy(_base_proxy.BaseImageProxy): def upload_image(self, **attrs): """Upload a new image from attributes + .. warning: + This method is deprecated - and also doesn't work very well. + Please stop using it immediately and switch to + `create_image`. + :param dict attrs: Keyword arguments which will be used to create a :class:`~openstack.image.v1.image.Image`, comprised of the properties on the Image class. @@ -26,8 +33,68 @@ def upload_image(self, **attrs): :returns: The results of image creation :rtype: :class:`~openstack.image.v1.image.Image` """ + warnings.warn("upload_image is deprecated. Use create_image instead.") return self._create(_image.Image, **attrs) + def _upload_image( + self, name, filename, meta, wait, timeout, **image_kwargs): + # NOTE(mordred) wait and timeout parameters are unused, but + # are present for ease at calling site. + image_data = open(filename, 'rb') + image_kwargs['properties'].update(meta) + image_kwargs['name'] = name + + # TODO(mordred) Convert this to use image Resource + image = self._connection._get_and_munchify( + 'image', + self.post('/images', json=image_kwargs)) + checksum = image_kwargs['properties'].get( + self._connection._IMAGE_MD5_KEY, '') + + try: + # Let us all take a brief moment to be grateful that this + # is not actually how OpenStack APIs work anymore + headers = { + 'x-glance-registry-purge-props': 'false', + } + if checksum: + headers['x-image-meta-checksum'] = checksum + + image = self._connection._get_and_munchify( + 'image', + self.put( + '/images/{id}'.format(id=image.id), + headers=headers, data=image_data)) + + except exc.OpenStackCloudHTTPError: + self._connection.log.debug( + "Deleting failed upload of image %s", name) + try: + self.delete('/images/{id}'.format(id=image.id)) + except exc.OpenStackCloudHTTPError: + # We're just trying to clean up - if it doesn't work - shrug + self._connection.log.warning( + "Failed deleting image after we failed uploading it.", + exc_info=True) + raise + return self._connection._normalize_image(image) + + def _update_image_properties(self, image, meta, properties): + properties.update(meta) + img_props = {} + for k, v in iter(properties.items()): + if image.properties.get(k, None) != v: + img_props['x-image-meta-{key}'.format(key=k)] = v + if not img_props: + return False + self.put( + '/images/{id}'.format(id=image.id), headers=img_props) + self._connection.list_images.invalidate(self._connection) + return True + + def _existing_image(self, **kwargs): + return _image.Image.existing(connection=self._connection, **kwargs) + def delete_image(self, image, ignore_missing=True): """Delete an image diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 2c3c48489..134df0a01 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -10,21 +10,39 @@ # License for the specific language governing permissions and limitations # under the License. +import json +import jsonpatch +import operator +import time +import warnings + +from openstack.cloud import exc +from openstack.cloud import _utils from openstack import exceptions +from openstack.image import _base_proxy from openstack.image.v2 import image as _image from openstack.image.v2 import member as _member from openstack.image.v2 import schema as _schema from openstack.image.v2 import task as _task -from openstack import proxy from openstack import resource +from openstack import utils +# Rackspace returns this for intermittent import errors +_IMAGE_ERROR_396 = "Image cannot be imported. Error code: '396'" +_INT_PROPERTIES = ('min_disk', 'min_ram', 'size', 'virtual_size') -class Proxy(proxy.Proxy): + +class Proxy(_base_proxy.BaseImageProxy): def upload_image(self, container_format=None, disk_format=None, data=None, **attrs): """Upload a new image from attributes + .. warning: + This method is deprecated - and also doesn't work very well. + Please stop using it immediately and switch to + `create_image`. + :param container_format: Format of the container. A valid value is ami, ari, aki, bare, ovf, ova, or docker. @@ -38,6 +56,7 @@ def upload_image(self, container_format=None, disk_format=None, :returns: The results of image creation :rtype: :class:`~openstack.image.v2.image.Image` """ + warnings.warn("upload_image is deprecated. Use create_image instead.") # container_format and disk_format are required to be set # on the image by the time upload_image is called, but they're not # required by the _create call. Enforce them here so that we don't @@ -62,6 +81,208 @@ def upload_image(self, container_format=None, disk_format=None, return img + def _upload_image( + self, name, filename=None, + meta=None, **kwargs): + # We can never have nice things. Glance v1 took "is_public" as a + # boolean. Glance v2 takes "visibility". If the user gives us + # is_public, we know what they mean. If they give us visibility, they + # know that they mean. + if 'is_public' in kwargs['properties']: + is_public = kwargs['properties'].pop('is_public') + if is_public: + kwargs['visibility'] = 'public' + else: + kwargs['visibility'] = 'private' + + try: + # This makes me want to die inside + if self._connection.image_api_use_tasks: + return self._upload_image_task( + name, filename, + meta=meta, **kwargs) + else: + return self._upload_image_put( + name, filename, meta=meta, + **kwargs) + except exc.OpenStackCloudException: + self._connection.log.debug("Image creation failed", exc_info=True) + raise + except Exception as e: + raise exc.OpenStackCloudException( + "Image creation failed: {message}".format(message=str(e))) + + def _make_v2_image_params(self, meta, properties): + ret = {} + for k, v in iter(properties.items()): + if k in _INT_PROPERTIES: + ret[k] = int(v) + elif k == 'protected': + ret[k] = v + else: + if v is None: + ret[k] = None + else: + ret[k] = str(v) + ret.update(meta) + return ret + + def _upload_image_put( + self, name, filename, meta, wait, timeout, **image_kwargs): + image_data = open(filename, 'rb') + + properties = image_kwargs.pop('properties', {}) + + image_kwargs.update(self._make_v2_image_params(meta, properties)) + image_kwargs['name'] = name + + data = self.post('/images', json=image_kwargs) + image = self._connection._get_and_munchify(key=None, data=data) + + try: + response = self.put( + '/images/{id}/file'.format(id=image.id), + headers={'Content-Type': 'application/octet-stream'}, + data=image_data) + exceptions.raise_from_response(response) + except Exception: + self._connection.log.debug( + "Deleting failed upload of image %s", name) + try: + response = self.delete( + '/images/{id}'.format(id=image.id)) + exceptions.raise_from_response(response) + except exc.OpenStackCloudHTTPError: + # We're just trying to clean up - if it doesn't work - shrug + self._connection.log.warning( + "Failed deleting image after we failed uploading it.", + exc_info=True) + raise + + return self._connection._normalize_image(image) + + def _upload_image_task( + self, name, filename, + wait, timeout, meta, **image_kwargs): + + if not self._connection.has_service('object-store'): + raise exc.OpenStackCloudException( + "The cloud {cloud} is configured to use tasks for image" + " upload, but no object-store service is available." + " Aborting.".format(cloud=self._connection.config.name)) + properties = image_kwargs.pop('properties', {}) + md5 = properties[self._connection._IMAGE_MD5_KEY] + sha256 = properties[self._connection._IMAGE_SHA256_KEY] + container = properties[ + self._connection._IMAGE_OBJECT_KEY].split('/', 1)[0] + image_kwargs.update(properties) + image_kwargs.pop('disk_format', None) + image_kwargs.pop('container_format', None) + + self._connection.create_container(container) + self._connection.create_object( + container, name, filename, + md5=md5, sha256=sha256, + metadata={self._connection._OBJECT_AUTOCREATE_KEY: 'true'}, + **{'content-type': 'application/octet-stream'}) + # TODO(mordred): Can we do something similar to what nodepool does + # using glance properties to not delete then upload but instead make a + # new "good" image and then mark the old one as "bad" + task_args = dict( + type='import', input=dict( + import_from='{container}/{name}'.format( + container=container, name=name), + image_properties=dict(name=name))) + data = self.post('/tasks', json=task_args) + glance_task = self._connection._get_and_munchify(key=None, data=data) + self._connection.list_images.invalidate(self) + if wait: + start = time.time() + image_id = None + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for the image to import."): + try: + if image_id is None: + response = self.get( + '/tasks/{id}'.format(id=glance_task.id)) + status = self._connection._get_and_munchify( + key=None, data=response) + + except exc.OpenStackCloudHTTPError as e: + if e.response.status_code == 503: + # Clear the exception so that it doesn't linger + # and get reported as an Inner Exception later + _utils._exc_clear() + # Intermittent failure - catch and try again + continue + raise + + if status['status'] == 'success': + image_id = status['result']['image_id'] + try: + image = self._connection.get_image(image_id) + except exc.OpenStackCloudHTTPError as e: + if e.response.status_code == 503: + # Clear the exception so that it doesn't linger + # and get reported as an Inner Exception later + _utils._exc_clear() + # Intermittent failure - catch and try again + continue + raise + if image is None: + continue + self.update_image_properties( + image=image, meta=meta, **image_kwargs) + self._connection.log.debug( + "Image Task %s imported %s in %s", + glance_task.id, image_id, (time.time() - start)) + # Clean up after ourselves. The object we created is not + # needed after the import is done. + self._connection.delete_object(container, name) + return self._connection.get_image(image_id) + elif status['status'] == 'failure': + if status['message'] == _IMAGE_ERROR_396: + glance_task = self.post('/tasks', data=task_args) + self._connection.list_images.invalidate(self) + else: + # Clean up after ourselves. The image did not import + # and this isn't a 'just retry' error - glance didn't + # like the content. So we don't want to keep it for + # next time. + self._connection.delete_object(container, name) + raise exc.OpenStackCloudException( + "Image creation failed: {message}".format( + message=status['message']), + extra_data=status) + else: + return glance_task + + def _update_image_properties(self, image, meta, properties): + img_props = image.properties.copy() + for k, v in iter(self._make_v2_image_params(meta, properties).items()): + if image.get(k, None) != v: + img_props[k] = v + if not img_props: + return False + headers = { + 'Content-Type': 'application/openstack-images-v2.1-json-patch'} + patch = sorted(list(jsonpatch.JsonPatch.from_diff( + image.properties, img_props)), key=operator.itemgetter('value')) + + # No need to fire an API call if there is an empty patch + if patch: + self.patch( + '/images/{id}'.format(id=image.id), + headers=headers, + data=json.dumps(patch)) + + self._connection.list_images.invalidate(self._connection) + return True + + def _existing_image(self, **kwargs): + return _image.Image.existing(connection=self._connection, **kwargs) + def download_image(self, image, stream=False): """Download an image diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 4ef8693e9..cee34d786 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -440,6 +440,15 @@ def _make_test_cloud(self, cloud_name='_test_cloud_', **kwargs): config=self.cloud_config, strict=self.strict_cloud) self.addCleanup(self.cloud.task_manager.stop) + def get_cinder_discovery_mock_dict( + self, + block_storage_version_json='block-storage-version.json', + block_storage_discovery_url='https://volume.example.com/'): + discovery_fixture = os.path.join( + self.fixtures_directory, block_storage_version_json) + return dict(method='GET', uri=block_storage_discovery_url, + text=open(discovery_fixture, 'r').read()) + def get_glance_discovery_mock_dict( self, image_version_json='image-version.json', diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index feba4d2c3..c50cdf689 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -986,6 +986,7 @@ def setUp(self): def test_create_image_volume(self): self.register_uris([ + self.get_cinder_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'volumev2', append=['volumes', self.volume_id, 'action']), @@ -1017,6 +1018,7 @@ def test_create_image_volume(self): def test_create_image_volume_duplicate(self): self.register_uris([ + self.get_cinder_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'volumev2', append=['volumes', self.volume_id, 'action']), diff --git a/openstack/tests/unit/fixtures/block-storage-version.json b/openstack/tests/unit/fixtures/block-storage-version.json new file mode 100644 index 000000000..e75ddfd44 --- /dev/null +++ b/openstack/tests/unit/fixtures/block-storage-version.json @@ -0,0 +1,28 @@ +{ + "versions": [ + { + "status": "CURRENT", + "updated": "2017-02-25T12:00:00Z", + "links": [ + { + "href": "https://docs.openstack.org/", + "type": "text/html", + "rel": "describedby" + }, + { + "href": "https://volume.example.com/v2/", + "rel": "self" + } + ], + "min_version": "", + "version": "", + "media-types": [ + { + "base": "application/json", + "type": "application/vnd.openstack.volume+json;version=2" + } + ], + "id": "v2.0" + } + ] +} From c2b75dc6eaf5707072d7470114d86bb5d94aecba Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 27 Feb 2019 14:28:39 +0000 Subject: [PATCH 2385/3836] Use retriable_status_codes in image upload This is supported down at the keystoneauth layer. Use it. Change-Id: I3dfdc885a1c7989eb17244242c816a0a55aaea4a --- openstack/config/defaults.json | 1 + openstack/image/_base_proxy.py | 2 ++ openstack/image/v2/_proxy.py | 32 ++++++-------------------------- 3 files changed, 9 insertions(+), 26 deletions(-) diff --git a/openstack/config/defaults.json b/openstack/config/defaults.json index 508932271..3eb5213c7 100644 --- a/openstack/config/defaults.json +++ b/openstack/config/defaults.json @@ -1,6 +1,7 @@ { "auth_type": "password", "baremetal_status_code_retries": 5, + "image_status_code_retries": 5, "disable_vendor_agent": {}, "interface": "public", "floating_ip_source": "neutron", diff --git a/openstack/image/_base_proxy.py b/openstack/image/_base_proxy.py index ec97ba9ad..3ae1624bd 100644 --- a/openstack/image/_base_proxy.py +++ b/openstack/image/_base_proxy.py @@ -18,6 +18,8 @@ class BaseImageProxy(six.with_metaclass(abc.ABCMeta, proxy.Proxy)): + retriable_status_codes = [503] + def create_image( self, name, filename=None, container=None, diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 134df0a01..e64af6387 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -17,7 +17,6 @@ import warnings from openstack.cloud import exc -from openstack.cloud import _utils from openstack import exceptions from openstack.image import _base_proxy from openstack.image.v2 import image as _image @@ -202,34 +201,15 @@ def _upload_image_task( for count in utils.iterate_timeout( timeout, "Timeout waiting for the image to import."): - try: - if image_id is None: - response = self.get( - '/tasks/{id}'.format(id=glance_task.id)) - status = self._connection._get_and_munchify( - key=None, data=response) - - except exc.OpenStackCloudHTTPError as e: - if e.response.status_code == 503: - # Clear the exception so that it doesn't linger - # and get reported as an Inner Exception later - _utils._exc_clear() - # Intermittent failure - catch and try again - continue - raise + if image_id is None: + response = self.get( + '/tasks/{id}'.format(id=glance_task.id)) + status = self._connection._get_and_munchify( + key=None, data=response) if status['status'] == 'success': image_id = status['result']['image_id'] - try: - image = self._connection.get_image(image_id) - except exc.OpenStackCloudHTTPError as e: - if e.response.status_code == 503: - # Clear the exception so that it doesn't linger - # and get reported as an Inner Exception later - _utils._exc_clear() - # Intermittent failure - catch and try again - continue - raise + image = self._connection.get_image(image_id) if image is None: continue self.update_image_properties( From de8a882ce79ff10ef1f0fb1e5ffe6be87923709a Mon Sep 17 00:00:00 2001 From: Victor Coutellier Date: Mon, 25 Feb 2019 15:53:00 +0100 Subject: [PATCH 2386/3836] Add glance image import support Interoperable image import process are introduced in the Image API v2.6. It mainly allow image importing from an external url and let Image Service download it by itself without sending binary data at image creation. This commit will add a method import_image in both Image resource and proxy. I also add a simple create_image proxy function for creating an image without uploading it's data, as it is not required in glance API. Change-Id: Idee9bea3f1f5db412e0ecd2105adff316aef4c4b Story: 2005085 Task: 29671 --- doc/source/user/guides/image.rst | 16 ++++++++ doc/source/user/proxies/image_v2.rst | 2 + examples/image/import.py | 38 +++++++++++++++++++ openstack/image/v2/_proxy.py | 31 ++++++++++++++- openstack/image/v2/image.py | 12 ++++++ openstack/tests/unit/image/v2/test_image.py | 17 +++++++++ openstack/tests/unit/image/v2/test_proxy.py | 19 +++++++++- ...image_import_support-6cea2e7d7a781071.yaml | 7 ++++ 8 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 examples/image/import.py create mode 100644 releasenotes/notes/add_image_import_support-6cea2e7d7a781071.yaml diff --git a/doc/source/user/guides/image.rst b/doc/source/user/guides/image.rst index 154f9d076..42e32118c 100644 --- a/doc/source/user/guides/image.rst +++ b/doc/source/user/guides/image.rst @@ -32,6 +32,20 @@ Create an image by uploading its data and setting its attributes. Full example: `image resource create`_ +Create Image via interoperable image import process +--------------------------------------------------- + +Create an image then use interoperable image import process to download data +from a web URL. + +For more information about the image import process, please check +`interoperable image import`_ + +.. literalinclude:: ../examples/image/import.py + :pyobject: import_image + +Full example: `image resource import`_ + .. _download_image-stream-true: Downloading an Image with stream=True @@ -76,6 +90,8 @@ Delete an image. Full example: `image resource delete`_ .. _image resource create: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/image/create.py +.. _image resource import: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/image/import.py .. _image resource delete: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/image/delete.py .. _image resource list: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/image/list.py .. _image resource download: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/image/download.py +.. _interoperable image import: https://docs.openstack.org/glance/latest/admin/interoperable-image-import.html diff --git a/doc/source/user/proxies/image_v2.rst b/doc/source/user/proxies/image_v2.rst index 4e108948d..2cf235daf 100644 --- a/doc/source/user/proxies/image_v2.rst +++ b/doc/source/user/proxies/image_v2.rst @@ -17,6 +17,8 @@ Image Operations .. autoclass:: openstack.image.v2._proxy.Proxy + .. automethod:: openstack.image.v2._proxy.Proxy.create_image + .. automethod:: openstack.image.v2._proxy.Proxy.import_image .. automethod:: openstack.image.v2._proxy.Proxy.upload_image .. automethod:: openstack.image.v2._proxy.Proxy.download_image .. automethod:: openstack.image.v2._proxy.Proxy.update_image diff --git a/examples/image/import.py b/examples/image/import.py new file mode 100644 index 000000000..ec9c6c67e --- /dev/null +++ b/examples/image/import.py @@ -0,0 +1,38 @@ +# 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 examples.connect import EXAMPLE_IMAGE_NAME + +""" +Create resources with the Image service. + +For a full guide see +http://developer.openstack.org/sdks/python/openstacksdk/user/guides/image.html +""" + + +def import_image(conn): + print("Import Image:") + + # Url where glance can download the image + uri = 'https://download.cirros-cloud.net/0.4.0/' \ + 'cirros-0.4.0-x86_64-disk.img' + + # Build the image attributes and import the image. + image_attrs = { + 'name': EXAMPLE_IMAGE_NAME, + 'disk_format': 'qcow2', + 'container_format': 'bare', + 'visibility': 'public', + } + image = conn.image.create_image(**image_attrs) + conn.image.import_image(image, method="web-download", uri=uri) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index e64af6387..9f77ec821 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -33,9 +33,38 @@ class Proxy(_base_proxy.BaseImageProxy): + def import_image(self, image, method='glance-direct', uri=None): + """Import data to an existing image + + Interoperable image import process are introduced in the Image API + v2.6. It mainly allow image importing from an external url and let + Image Service download it by itself without sending binary data at + image creation. + + :param image: The value can be the ID of a image or a + :class:`~openstack.image.v2.image.Image` instance. + :param method: Method to use for importing the image. + A valid value is glance-direct or web-download. + :param uri: Required only if using the web-download import method. + This url is where the data is made available to the Image + service. + + :returns: None + """ + image = self._get_resource(_image.Image, image) + + # as for the standard image upload function, container_format and + # disk_format are required for using image import process + if not all([image.container_format, image.disk_format]): + raise exceptions.InvalidRequest( + "Both container_format and disk_format are required for" + " importing an image") + + image.import_image(self, method=method, uri=uri) + def upload_image(self, container_format=None, disk_format=None, data=None, **attrs): - """Upload a new image from attributes + """Create and upload a new image from attributes .. warning: This method is deprecated - and also doesn't work very well. diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index e58a7b33f..1c7b5845c 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -249,6 +249,18 @@ def upload(self, session): headers={"Content-Type": "application/octet-stream", "Accept": ""}) + def import_image(self, session, method='glance-direct', uri=None): + """Import Image via interoperable image import process""" + url = utils.urljoin(self.base_path, self.id, 'import') + json = {'method': {'name': method}} + if uri: + if method == 'web-download': + json['method']['uri'] = uri + else: + raise exceptions.InvalidRequest('URI is only supported with ' + 'method: "web-download"') + session.post(url, json=json) + def download(self, session, stream=False): """Download the data contained in an image""" # TODO(briancurtin): This method should probably offload the get diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 7f506a1fe..5028e4f28 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -239,6 +239,23 @@ def test_remove_tag(self): 'images/IDENTIFIER/tags/%s' % tag, ) + def test_import_image(self): + sot = image.Image(**EXAMPLE) + json = {"method": {"name": "web-download", "uri": "such-a-good-uri"}} + sot.import_image(self.sess, "web-download", "such-a-good-uri") + self.sess.post.assert_called_with( + 'images/IDENTIFIER/import', + json=json + ) + + def test_import_image_with_uri_not_web_download(self): + sot = image.Image(**EXAMPLE) + self.assertRaises(exceptions.InvalidRequest, + sot.import_image, + self.sess, + "glance-direct", + "such-a-good-uri") + def test_upload(self): sot = image.Image(**EXAMPLE) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 5af325436..1d097a7cb 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -29,11 +29,26 @@ def setUp(self): super(TestImageProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) - def test_image_create_no_args(self): + def test_image_import_no_required_attrs(self): + # container_format and disk_format are required attrs of the image + existing_image = image.Image(id="id") + self.assertRaises(exceptions.InvalidRequest, + self.proxy.import_image, + existing_image) + + def test_image_import(self): + original_image = image.Image(**EXAMPLE) + self._verify("openstack.image.v2.image.Image.import_image", + self.proxy.import_image, + method_args=[original_image, "method", "uri"], + expected_kwargs={"method": "method", + "uri": "uri"}) + + def test_image_upload_no_args(self): # container_format and disk_format are required args self.assertRaises(exceptions.InvalidRequest, self.proxy.upload_image) - def test_image_create(self): + def test_image_upload(self): # NOTE: This doesn't use any of the base class verify methods # because it ends up making two separate calls to complete the # operation. diff --git a/releasenotes/notes/add_image_import_support-6cea2e7d7a781071.yaml b/releasenotes/notes/add_image_import_support-6cea2e7d7a781071.yaml new file mode 100644 index 000000000..22b35ce6b --- /dev/null +++ b/releasenotes/notes/add_image_import_support-6cea2e7d7a781071.yaml @@ -0,0 +1,7 @@ +--- +features: + - Add ability to create image without upload data at the same time + - Add support for interoperable image import process as introduced in the + Image API v2.6 at [1] + + [1]https://developer.openstack.org/api-ref/image/v2/index.html#interoperable-image-import From 96eae95e3b74b688695d1c25de6b172de3e39a11 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 26 Feb 2019 14:32:12 +0100 Subject: [PATCH 2387/3836] Add image tasks schema methods A follow-up of https://review.openstack.org/#/c/637540/ to bring tasks schema methods to the image proxy and add all schema methods to proxy docs Release-note is inherited here (no need for a separate RN) Change-Id: I5486d4955f4a8c04088f2b7d5466c0e45bec18a8 --- doc/source/user/proxies/image_v2.rst | 12 ++++++++++++ openstack/image/v2/_proxy.py | 20 ++++++++++++++++++++ openstack/tests/unit/image/v2/test_proxy.py | 14 ++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/doc/source/user/proxies/image_v2.rst b/doc/source/user/proxies/image_v2.rst index 2cf235daf..9b8a9b9d9 100644 --- a/doc/source/user/proxies/image_v2.rst +++ b/doc/source/user/proxies/image_v2.rst @@ -52,3 +52,15 @@ Task Operations .. automethod:: openstack.image.v2._proxy.Proxy.create_task .. automethod:: openstack.image.v2._proxy.Proxy.get_task .. automethod:: openstack.image.v2._proxy.Proxy.wait_for_task + +Schema Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.image.v2._proxy.Proxy + + .. automethod:: openstack.image.v2._proxy.Proxy.get_images_schema + .. automethod:: openstack.image.v2._proxy.Proxy.get_image_schema + .. automethod:: openstack.image.v2._proxy.Proxy.get_members_schema + .. automethod:: openstack.image.v2._proxy.Proxy.get_member_schema + .. automethod:: openstack.image.v2._proxy.Proxy.get_tasks_schema + .. automethod:: openstack.image.v2._proxy.Proxy.get_task_schema diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 9f77ec821..359905731 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -640,3 +640,23 @@ def wait_for_task(self, task, status='success', failures=None, failures = ['failure'] if failures is None else failures return resource.wait_for_status( self, task, status, failures, interval, wait) + + def get_tasks_schema(self): + """Get image tasks schema + + :returns: One :class:`~openstack.image.v2.schema.Schema` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_schema.Schema, requires_id=False, + base_path='/schemas/tasks') + + def get_task_schema(self): + """Get image task schema + + :returns: One :class:`~openstack.image.v2.schema.Schema` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_schema.Schema, requires_id=False, + base_path='/schemas/task') diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 1d097a7cb..ff905e074 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -215,3 +215,17 @@ def test_task_wait_for(self): self.proxy.wait_for_task, method_args=[value], expected_args=[value, 'success', ['failure'], 2, 120]) + + def test_tasks_schema_get(self): + self._verify2("openstack.proxy.Proxy._get", + self.proxy.get_tasks_schema, + expected_args=[schema.Schema], + expected_kwargs={'base_path': '/schemas/tasks', + 'requires_id': False}) + + def test_task_schema_get(self): + self._verify2("openstack.proxy.Proxy._get", + self.proxy.get_task_schema, + expected_args=[schema.Schema], + expected_kwargs={'base_path': '/schemas/task', + 'requires_id': False}) From e9e1bbe9ce0d537d7f1e520954a695def1393893 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 20 Feb 2019 18:37:22 +0100 Subject: [PATCH 2388/3836] Add image.service_info resources Add service info discovery resources for image service: - import (import constraints) - store (supported image stores) Change-Id: I978c156f41386ce05feed9d6b3e92a7a675b70c9 --- doc/source/user/proxies/image_v2.rst | 8 +++ doc/source/user/resources/image/index.rst | 1 + .../user/resources/image/v2/service_info.rst | 20 ++++++ openstack/image/v2/_proxy.py | 18 +++++ openstack/image/v2/service_info.py | 36 ++++++++++ openstack/tests/unit/image/v2/test_proxy.py | 12 ++++ .../tests/unit/image/v2/test_service_info.py | 68 +++++++++++++++++++ ...d-image-service-info-90d6063b5ba0735d.yaml | 3 + 8 files changed, 166 insertions(+) create mode 100644 doc/source/user/resources/image/v2/service_info.rst create mode 100644 openstack/image/v2/service_info.py create mode 100644 openstack/tests/unit/image/v2/test_service_info.py create mode 100644 releasenotes/notes/add-image-service-info-90d6063b5ba0735d.yaml diff --git a/doc/source/user/proxies/image_v2.rst b/doc/source/user/proxies/image_v2.rst index 9b8a9b9d9..97c3cc65d 100644 --- a/doc/source/user/proxies/image_v2.rst +++ b/doc/source/user/proxies/image_v2.rst @@ -64,3 +64,11 @@ Schema Operations .. automethod:: openstack.image.v2._proxy.Proxy.get_member_schema .. automethod:: openstack.image.v2._proxy.Proxy.get_tasks_schema .. automethod:: openstack.image.v2._proxy.Proxy.get_task_schema + +Service Info Discovery Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.image.v2._proxy.Proxy + + .. automethod:: openstack.image.v2._proxy.Proxy.stores + .. automethod:: openstack.image.v2._proxy.Proxy.get_import_info diff --git a/doc/source/user/resources/image/index.rst b/doc/source/user/resources/image/index.rst index dcafdcd3b..4e09af0e2 100644 --- a/doc/source/user/resources/image/index.rst +++ b/doc/source/user/resources/image/index.rst @@ -15,3 +15,4 @@ Image v2 Resources v2/image v2/member v2/task + v2/service_info diff --git a/doc/source/user/resources/image/v2/service_info.rst b/doc/source/user/resources/image/v2/service_info.rst new file mode 100644 index 000000000..92bae8988 --- /dev/null +++ b/doc/source/user/resources/image/v2/service_info.rst @@ -0,0 +1,20 @@ +openstack.image.v2.service_info +=============================== + +.. automodule:: openstack.image.v2.service_info + +The Store Class +---------------- + +The ``Store`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.image.v2.service_info.Store + :members: + +The Import Info Class +--------------------- + +The ``Import`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.image.v2.service_info.Import + :members: diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 359905731..360843260 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -23,6 +23,7 @@ from openstack.image.v2 import member as _member from openstack.image.v2 import schema as _schema from openstack.image.v2 import task as _task +from openstack.image.v2 import service_info as _si from openstack import resource from openstack import utils @@ -660,3 +661,20 @@ def get_task_schema(self): """ return self._get(_schema.Schema, requires_id=False, base_path='/schemas/task') + + def stores(self, **query): + """Return a generator of supported image stores + + :returns: A generator of store objects + :rtype: :class:`~openstack.image.v2.service_info.Store` + """ + return self._list(_si.Store, **query) + + def get_import_info(self): + """Get a info about image constraints + + :returns: One :class:`~openstack.image.v2.service_info.Import` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_si.Import, require_id=False) diff --git a/openstack/image/v2/service_info.py b/openstack/image/v2/service_info.py new file mode 100644 index 000000000..733e8a34b --- /dev/null +++ b/openstack/image/v2/service_info.py @@ -0,0 +1,36 @@ +# 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 resource + + +class Import(resource.Resource): + base_path = '/info/import' + + # capabilities + allow_fetch = True + + #: import methods + import_methods = resource.Body('import-methods', type=dict) + + +class Store(resource.Resource): + resources_key = 'stores' + base_path = '/info/stores' + + # capabilities + allow_list = True + + #: Description of the store + description = resource.Body('description') + #: default + is_default = resource.Body('default', type=bool) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index ff905e074..0690206ca 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -18,6 +18,7 @@ from openstack.image.v2 import member from openstack.image.v2 import schema from openstack.image.v2 import task +from openstack.image.v2 import service_info as si from openstack.tests.unit.image.v2 import test_image as fake_image from openstack.tests.unit import test_proxy_base @@ -229,3 +230,14 @@ def test_task_schema_get(self): expected_args=[schema.Schema], expected_kwargs={'base_path': '/schemas/task', 'requires_id': False}) + + def test_stores(self): + self.verify_list(self.proxy.stores, si.Store) + + def test_import_info(self): + self._verify2("openstack.proxy.Proxy._get", + self.proxy.get_import_info, + method_args=[], + method_kwargs={}, + expected_args=[si.Import], + expected_kwargs={'require_id': False}) diff --git a/openstack/tests/unit/image/v2/test_service_info.py b/openstack/tests/unit/image/v2/test_service_info.py new file mode 100644 index 000000000..6d8c85f92 --- /dev/null +++ b/openstack/tests/unit/image/v2/test_service_info.py @@ -0,0 +1,68 @@ +# 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.tests.unit import base + +from openstack.image.v2 import service_info as si + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE_IMPORT = { + 'import-methods': { + 'description': 'Import methods available.', + 'type': 'array', + 'value': [ + 'glance-direct', + 'web-download' + ] + } +} +EXAMPLE_STORE = { + 'id': 'fast', + 'description': 'Fast access to rbd store', + 'default': True +} + + +class TestStore(base.TestCase): + def test_basic(self): + sot = si.Store() + self.assertIsNone(sot.resource_key) + self.assertEqual('stores', sot.resources_key) + self.assertEqual('/info/stores', sot.base_path) + self.assertFalse(sot.allow_create) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = si.Store(**EXAMPLE_STORE) + self.assertEqual(EXAMPLE_STORE['id'], sot.id) + self.assertEqual(EXAMPLE_STORE['description'], sot.description) + self.assertEqual(EXAMPLE_STORE['default'], sot.is_default) + + +class TestImport(base.TestCase): + def test_basic(self): + sot = si.Import() + self.assertIsNone(sot.resource_key) + self.assertIsNone(sot.resources_key) + self.assertEqual('/info/import', sot.base_path) + self.assertFalse(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_delete) + self.assertFalse(sot.allow_list) + + def test_make_it(self): + sot = si.Import(**EXAMPLE_IMPORT) + self.assertEqual(EXAMPLE_IMPORT['import-methods'], sot.import_methods) diff --git a/releasenotes/notes/add-image-service-info-90d6063b5ba0735d.yaml b/releasenotes/notes/add-image-service-info-90d6063b5ba0735d.yaml new file mode 100644 index 000000000..ea84d0d09 --- /dev/null +++ b/releasenotes/notes/add-image-service-info-90d6063b5ba0735d.yaml @@ -0,0 +1,3 @@ +--- +features: + - Add image service info discovery (import constraints and supported stores) From a49f2056b2500ea7e23fe1b5c30bf5b29cf8933f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 1 Mar 2019 13:38:31 +0000 Subject: [PATCH 2389/3836] Get rid of setUpClass and block it for forever setUpClass is evil. It lures you in to a sense of complacency thinking that reality exists and makes sense. But it's all just a cruel joke designed to increase your personal karmic debt. In order that we might collectively one day reach nirvana, remove all senseless and misleading instances of setUpClass. Then add local hacking checks to prevent the introduction of such scourge ever again. Change-Id: Ifd43958bf47981aedad639bef61769ddb37618d3 --- HACKING.rst | 10 ++++ openstack/_hacking.py | 44 ++++++++++++++ openstack/tests/functional/base.py | 15 +++-- .../tests/functional/block_storage/v2/base.py | 9 +-- .../tests/functional/block_storage/v3/base.py | 9 +-- .../functional/block_store/v2/test_stats.py | 9 ++- .../functional/clustering/test_cluster.py | 8 +-- openstack/tests/functional/compute/base.py | 9 +-- .../load_balancer/v2/test_load_balancer.py | 9 +-- .../functional/orchestration/v1/test_stack.py | 8 +-- openstack/tests/unit/test_hacking.py | 60 +++++++++++++++++++ tox.ini | 12 ++-- 12 files changed, 137 insertions(+), 65 deletions(-) create mode 100644 openstack/_hacking.py create mode 100644 openstack/tests/unit/test_hacking.py diff --git a/HACKING.rst b/HACKING.rst index bda11bcbd..9179bd7c0 100644 --- a/HACKING.rst +++ b/HACKING.rst @@ -51,3 +51,13 @@ in a way that protects against local environment. Test cases should use requests-mock to mock out HTTP interactions rather than using mock to mock out object access. + +Don't Use setUpClass +-------------------- + +setUpClass looks like it runs once for the class. In parallel test execution +environments though, it runs once per execution context. This makes reasoning +about when it is going to actually run and what is going to happen extremely +difficult and can produce hard to debug test issues. + +Don't ever use it. It makes baby pandas cry. diff --git a/openstack/_hacking.py b/openstack/_hacking.py new file mode 100644 index 000000000..94952630c --- /dev/null +++ b/openstack/_hacking.py @@ -0,0 +1,44 @@ +# Copyright (c) 2019, Red Hat, 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 re + +""" +Guidelines for writing new hacking checks + + - Use only for openstacksdk specific tests. OpenStack general tests + should be submitted to the common 'hacking' module. + - Pick numbers in the range O3xx. Find the current test with + the highest allocated number and then pick the next value. + - Keep the test method code in the source file ordered based + on the O3xx value. + - List the new rule in the top level HACKING.rst file + - Add test cases for each new rule to nova/tests/unit/test_hacking.py + +""" + +SETUPCLASS_RE = re.compile(r"def setUpClass\(") + + +def assert_no_setupclass(logical_line): + """Check for use of setUpClass + + O300 + """ + if SETUPCLASS_RE.match(logical_line): + yield (0, "O300: setUpClass not allowed") + + +def factory(register): + register(assert_no_setupclass) diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index 603125f21..43900c99a 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -45,14 +45,7 @@ def _disable_keep_alive(conn): class BaseFunctionalTest(base.TestCase): - @classmethod - def setUpClass(cls): - super(BaseFunctionalTest, cls).setUpClass() - # Defines default timeout for wait_for methods used - # in the functional tests - cls._wait_for_timeout = int(os.getenv( - 'OPENSTACKSDK_FUNC_TEST_TIMEOUT', - 300)) + _wait_for_timeout_key = '' def setUp(self): super(BaseFunctionalTest, self).setUp() @@ -70,6 +63,12 @@ def setUp(self): self.identity_version = \ self.operator_cloud.config.get_api_version('identity') + # Defines default timeout for wait_for methods used + # in the functional tests + self._wait_for_timeout = int( + os.getenv(self._wait_for_timeout_key, os.getenv( + 'OPENSTACKSDK_FUNC_TEST_TIMEOUT', 300))) + def _set_user_cloud(self, **kwargs): user_config = self.config.get_one( cloud=self._demo_name, **kwargs) diff --git a/openstack/tests/functional/block_storage/v2/base.py b/openstack/tests/functional/block_storage/v2/base.py index 5475d1e14..4d7f0c099 100644 --- a/openstack/tests/functional/block_storage/v2/base.py +++ b/openstack/tests/functional/block_storage/v2/base.py @@ -10,19 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -import os - from openstack.tests.functional import base class BaseBlockStorageTest(base.BaseFunctionalTest): - @classmethod - def setUpClass(cls): - super(BaseBlockStorageTest, cls).setUpClass() - cls._wait_for_timeout = int(os.getenv( - 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_BLOCK_STORAGE', - cls._wait_for_timeout)) + _wait_for_timeout_key = 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_BLOCK_STORAGE' def setUp(self): super(BaseBlockStorageTest, self).setUp() diff --git a/openstack/tests/functional/block_storage/v3/base.py b/openstack/tests/functional/block_storage/v3/base.py index 5f98a3cdc..adab8dba8 100644 --- a/openstack/tests/functional/block_storage/v3/base.py +++ b/openstack/tests/functional/block_storage/v3/base.py @@ -10,19 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -import os - from openstack.tests.functional import base class BaseBlockStorageTest(base.BaseFunctionalTest): - @classmethod - def setUpClass(cls): - super(BaseBlockStorageTest, cls).setUpClass() - cls._wait_for_timeout = int(os.getenv( - 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_BLOCK_STORAGE', - cls._wait_for_timeout)) + _wait_for_timeout_key = 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_BLOCK_STORAGE' def setUp(self): super(BaseBlockStorageTest, self).setUp() diff --git a/openstack/tests/functional/block_store/v2/test_stats.py b/openstack/tests/functional/block_store/v2/test_stats.py index 581f6e33e..ef64d49a7 100644 --- a/openstack/tests/functional/block_store/v2/test_stats.py +++ b/openstack/tests/functional/block_store/v2/test_stats.py @@ -17,12 +17,11 @@ class TestStats(base.BaseFunctionalTest): - @classmethod - def setUpClass(cls): - super(TestStats, cls).setUpClass() - sot = cls.conn.block_storage.backend_pools() + def setUp(self): + super(TestStats, self).setUp() + sot = self.conn.block_storage.backend_pools() for pool in sot: - assert isinstance(pool, _stats.Pools) + self.assertIsInstance(pool, _stats.Pools) def test_list(self): capList = ['volume_backend_name', 'storage_protocol', diff --git a/openstack/tests/functional/clustering/test_cluster.py b/openstack/tests/functional/clustering/test_cluster.py index e964d37d4..4f03e9f21 100644 --- a/openstack/tests/functional/clustering/test_cluster.py +++ b/openstack/tests/functional/clustering/test_cluster.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import os import time from openstack.clustering.v1 import cluster @@ -20,12 +19,7 @@ class TestCluster(base.BaseFunctionalTest): - @classmethod - def setUpClass(cls): - super(TestCluster, cls).setUpClass() - cls._wait_for_timeout = int(os.getenv( - 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_CLUSTER', - cls._wait_for_timeout)) + _wait_for_timeout_key = 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_CLUSTER' def setUp(self): super(TestCluster, self).setUp() diff --git a/openstack/tests/functional/compute/base.py b/openstack/tests/functional/compute/base.py index 12b86fad1..bfeabd521 100644 --- a/openstack/tests/functional/compute/base.py +++ b/openstack/tests/functional/compute/base.py @@ -10,16 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. -import os - from openstack.tests.functional import base class BaseComputeTest(base.BaseFunctionalTest): - @classmethod - def setUpClass(cls): - super(BaseComputeTest, cls).setUpClass() - cls._wait_for_timeout = int(os.getenv( - 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_COMPUTE', - cls._wait_for_timeout)) + _wait_for_timeout_key = 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_COMPUTE' diff --git a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py index a9b984653..daae3b619 100644 --- a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py +++ b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import os - from openstack.load_balancer.v2 import flavor from openstack.load_balancer.v2 import flavor_profile from openstack.load_balancer.v2 import health_monitor @@ -56,12 +54,7 @@ class TestLoadBalancer(base.BaseFunctionalTest): FLAVOR_DATA = '{"loadbalancer_topology": "SINGLE"}' DESCRIPTION = 'Test description' - @classmethod - def setUpClass(cls): - super(TestLoadBalancer, cls).setUpClass() - cls._wait_for_timeout = int(os.getenv( - 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_LOAD_BALANCER', - cls._wait_for_timeout)) + _wait_for_timeout_key = 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_LOAD_BALANCER' # TODO(shade): Creating load balancers can be slow on some hosts due to # nova instance boot times (up to ten minutes). This used to diff --git a/openstack/tests/functional/orchestration/v1/test_stack.py b/openstack/tests/functional/orchestration/v1/test_stack.py index 67ef21537..afc08c523 100644 --- a/openstack/tests/functional/orchestration/v1/test_stack.py +++ b/openstack/tests/functional/orchestration/v1/test_stack.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import os import yaml from openstack import exceptions @@ -27,12 +26,7 @@ class TestStack(base.BaseFunctionalTest): subnet = None cidr = '10.99.99.0/16' - @classmethod - def setUpClass(cls): - super(TestStack, cls).setUpClass() - cls._wait_for_timeout = int(os.getenv( - 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_ORCHESTRATION', - cls._wait_for_timeout)) + _wait_for_timeout_key = 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_ORCHESTRATION' def setUp(self): super(TestStack, self).setUp() diff --git a/openstack/tests/unit/test_hacking.py b/openstack/tests/unit/test_hacking.py new file mode 100644 index 000000000..e6da4f58d --- /dev/null +++ b/openstack/tests/unit/test_hacking.py @@ -0,0 +1,60 @@ +# Copyright 2019 Red Hat, 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. + +from openstack import _hacking +from openstack.tests.unit import base + + +class HackingTestCase(base.TestCase): + """This class tests the hacking checks in openstack._hacking.checks. + + It works by passing strings to the check methods like the pep8/flake8 + parser would. The parser loops over each line in the file and then passes + the parameters to the check method. The parameter names in the check method + dictate what type of object is passed to the check method. + + The parameter types are:: + + logical_line: A processed line with the following modifications: + - Multi-line statements converted to a single line. + - Stripped left and right. + - Contents of strings replaced with "xxx" of same length. + - Comments removed. + physical_line: Raw line of text from the input file. + lines: a list of the raw lines from the input file + tokens: the tokens that contribute to this logical line + line_number: line number in the input file + total_lines: number of lines in the input file + blank_lines: blank lines before this one + indent_char: indentation character in this file (" " or "\t") + indent_level: indentation (with tabs expanded to multiples of 8) + previous_indent_level: indentation on previous line + previous_logical: previous logical line + filename: Path of the file being run through pep8 + + When running a test on a check method the return will be False/None if + there is no violation in the sample input. If there is an error a tuple is + returned with a position in the line, and a message. So to check the result + just assertTrue if the check is expected to fail and assertFalse if it + should pass. + """ + def test_assert_no_setupclass(self): + self.assertEqual(len(list(_hacking.assert_no_setupclass( + "def setUpClass(cls)"))), 1) + + self.assertEqual(len(list(_hacking.assert_no_setupclass( + "# setUpClass is evil"))), 0) + + self.assertEqual(len(list(_hacking.assert_no_setupclass( + "def setUpClassyDrinkingLocation(cls)"))), 0) diff --git a/tox.ini b/tox.ini index 63be5d510..20e755222 100644 --- a/tox.ini +++ b/tox.ini @@ -39,18 +39,18 @@ commands = stestr --test-path ./openstack/tests/functional/{env:OPENSTACKSDK_TES stestr slowest [testenv:pep8] -usedevelop = False -skip_install = True deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} - doc8 - flake8 + {[testenv]deps} hacking + doc8 pygments readme commands = - doc8 doc/source flake8 + doc8 doc/source + +[hacking] +local-check-factory = openstack._hacking.factory [testenv:venv] commands = {posargs} From 8d14cbaa8892de16cb1dc250d40a9374f5b8de9c Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Sat, 2 Mar 2019 09:09:56 +0100 Subject: [PATCH 2390/3836] Add DNS support Add support for the DNS service. There are still some unimplemented resources (tld, pools, limits, tsigkey, blacklist, quota, service_status). They would be added in the follow-up, since this change is already very big for a review. Change-Id: I4acab7c4cace63b4ea7d719854e159bd41d49039 --- doc/source/user/guides/dns.rst | 18 + doc/source/user/index.rst | 3 + doc/source/user/proxies/dns.rst | 81 +++ doc/source/user/resources/dns/index.rst | 12 + .../user/resources/dns/v2/floating_ip.rst | 12 + .../user/resources/dns/v2/recordset.rst | 12 + doc/source/user/resources/dns/v2/zone.rst | 12 + .../user/resources/dns/v2/zone_export.rst | 12 + .../user/resources/dns/v2/zone_import.rst | 12 + .../user/resources/dns/v2/zone_transfer.rst | 20 + examples/dns/__init__.py | 0 examples/dns/list.py | 24 + openstack/dns/__init__.py | 0 openstack/dns/dns_service.py | 22 + openstack/dns/v2/__init__.py | 0 openstack/dns/v2/_proxy.py | 494 ++++++++++++++++++ openstack/dns/v2/floating_ip.py | 40 ++ openstack/dns/v2/recordset.py | 64 +++ openstack/dns/v2/zone.py | 96 ++++ openstack/dns/v2/zone_export.py | 86 +++ openstack/dns/v2/zone_import.py | 86 +++ openstack/dns/v2/zone_transfer.py | 72 +++ openstack/tests/functional/dns/__init__.py | 0 openstack/tests/functional/dns/v2/__init__.py | 0 .../tests/functional/dns/v2/test_zone.py | 52 ++ openstack/tests/unit/dns/__init__.py | 0 openstack/tests/unit/dns/v2/__init__.py | 0 .../tests/unit/dns/v2/test_floating_ip.py | 56 ++ openstack/tests/unit/dns/v2/test_proxy.py | 197 +++++++ openstack/tests/unit/dns/v2/test_recordset.py | 69 +++ openstack/tests/unit/dns/v2/test_zone.py | 87 +++ .../tests/unit/dns/v2/test_zone_export.py | 80 +++ .../tests/unit/dns/v2/test_zone_import.py | 79 +++ .../tests/unit/dns/v2/test_zone_transfer.py | 103 ++++ openstack/tests/unit/test_proxy_base.py | 1 + .../notes/add-dns-606cc018e01d40fa.yaml | 5 + 36 files changed, 1907 insertions(+) create mode 100644 doc/source/user/guides/dns.rst create mode 100644 doc/source/user/proxies/dns.rst create mode 100644 doc/source/user/resources/dns/index.rst create mode 100644 doc/source/user/resources/dns/v2/floating_ip.rst create mode 100644 doc/source/user/resources/dns/v2/recordset.rst create mode 100644 doc/source/user/resources/dns/v2/zone.rst create mode 100644 doc/source/user/resources/dns/v2/zone_export.rst create mode 100644 doc/source/user/resources/dns/v2/zone_import.rst create mode 100644 doc/source/user/resources/dns/v2/zone_transfer.rst create mode 100644 examples/dns/__init__.py create mode 100644 examples/dns/list.py create mode 100644 openstack/dns/__init__.py create mode 100644 openstack/dns/dns_service.py create mode 100644 openstack/dns/v2/__init__.py create mode 100644 openstack/dns/v2/_proxy.py create mode 100644 openstack/dns/v2/floating_ip.py create mode 100644 openstack/dns/v2/recordset.py create mode 100644 openstack/dns/v2/zone.py create mode 100644 openstack/dns/v2/zone_export.py create mode 100644 openstack/dns/v2/zone_import.py create mode 100644 openstack/dns/v2/zone_transfer.py create mode 100644 openstack/tests/functional/dns/__init__.py create mode 100644 openstack/tests/functional/dns/v2/__init__.py create mode 100644 openstack/tests/functional/dns/v2/test_zone.py create mode 100644 openstack/tests/unit/dns/__init__.py create mode 100644 openstack/tests/unit/dns/v2/__init__.py create mode 100644 openstack/tests/unit/dns/v2/test_floating_ip.py create mode 100644 openstack/tests/unit/dns/v2/test_proxy.py create mode 100644 openstack/tests/unit/dns/v2/test_recordset.py create mode 100644 openstack/tests/unit/dns/v2/test_zone.py create mode 100644 openstack/tests/unit/dns/v2/test_zone_export.py create mode 100644 openstack/tests/unit/dns/v2/test_zone_import.py create mode 100644 openstack/tests/unit/dns/v2/test_zone_transfer.py create mode 100644 releasenotes/notes/add-dns-606cc018e01d40fa.yaml diff --git a/doc/source/user/guides/dns.rst b/doc/source/user/guides/dns.rst new file mode 100644 index 000000000..f2ba4fbcf --- /dev/null +++ b/doc/source/user/guides/dns.rst @@ -0,0 +1,18 @@ +Using OpenStack DNS +=================== + +Before working with the DNS service, you'll need to create a connection +to your OpenStack cloud by following the :doc:`connect` user guide. This will +provide you with the ``conn`` variable used in the examples below. + +.. TODO(gtema): Implement this guide + +List Zones +---------- + +.. literalinclude:: ../examples/dns/list.py + :pyobject: list_zones + +Full example: `dns resource list`_ + +.. _dns resource list: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/dns/list.py diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index 0dcbb3a50..4fa8a02f6 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -39,6 +39,7 @@ approach, this is where you'll want to begin. Clustering Compute Database + DNS Identity Image Key Manager @@ -98,6 +99,7 @@ control which services can be used. Clustering Compute Database + DNS Identity v2 Identity v3 Image v1 @@ -130,6 +132,7 @@ The following services have exposed *Resource* classes. Clustering Compute Database + DNS Identity Image Key Management diff --git a/doc/source/user/proxies/dns.rst b/doc/source/user/proxies/dns.rst new file mode 100644 index 000000000..91a63de2b --- /dev/null +++ b/doc/source/user/proxies/dns.rst @@ -0,0 +1,81 @@ +DNS API +======= + +For details on how to use dns, see :doc:`/user/guides/dns` + +.. automodule:: openstack.dns.v2._proxy + +The DNS Class +------------- + +The dns high-level interface is available through the ``dns`` +member of a :class:`~openstack.connection.Connection` object. The +``dns`` member will only be added if the service is detected. + +DNS Zone Operations +^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.dns.v2._proxy.Proxy + + .. automethod:: openstack.dns.v2._proxy.Proxy.create_zone + .. automethod:: openstack.dns.v2._proxy.Proxy.delete_zone + .. automethod:: openstack.dns.v2._proxy.Proxy.get_zone + .. automethod:: openstack.dns.v2._proxy.Proxy.find_zone + .. automethod:: openstack.dns.v2._proxy.Proxy.zones + .. automethod:: openstack.dns.v2._proxy.Proxy.abandon_zone + .. automethod:: openstack.dns.v2._proxy.Proxy.xfr_zone + +Recordset Operations +^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.dns.v2._proxy.Proxy + + .. automethod:: openstack.dns.v2._proxy.Proxy.create_recordset + .. automethod:: openstack.dns.v2._proxy.Proxy.update_recordset + .. automethod:: openstack.dns.v2._proxy.Proxy.get_recordset + .. automethod:: openstack.dns.v2._proxy.Proxy.delete_recordset + .. automethod:: openstack.dns.v2._proxy.Proxy.recordsets + +Zone Import Operations +^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.dns.v2._proxy.Proxy + + .. automethod:: openstack.dns.v2._proxy.Proxy.zone_imports + .. automethod:: openstack.dns.v2._proxy.Proxy.create_zone_import + .. automethod:: openstack.dns.v2._proxy.Proxy.get_zone_import + .. automethod:: openstack.dns.v2._proxy.Proxy.delete_zone_import + +Zone Export Operations +^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.dns.v2._proxy.Proxy + + .. automethod:: openstack.dns.v2._proxy.Proxy.zone_exports + .. automethod:: openstack.dns.v2._proxy.Proxy.create_zone_export + .. automethod:: openstack.dns.v2._proxy.Proxy.get_zone_export + .. automethod:: openstack.dns.v2._proxy.Proxy.get_zone_export_text + .. automethod:: openstack.dns.v2._proxy.Proxy.delete_zone_export + +FloatingIP Operations +^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.dns.v2._proxy.Proxy + + .. automethod:: openstack.dns.v2._proxy.Proxy.floating_ips + .. automethod:: openstack.dns.v2._proxy.Proxy.get_floating_ip + .. automethod:: openstack.dns.v2._proxy.Proxy.update_floating_ip + +Zone Transfer Operations +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.dns.v2._proxy.Proxy + + .. automethod:: openstack.dns.v2._proxy.Proxy.zone_transfer_requests + .. automethod:: openstack.dns.v2._proxy.Proxy.get_zone_transfer_request + .. automethod:: openstack.dns.v2._proxy.Proxy.create_zone_transfer_request + .. automethod:: openstack.dns.v2._proxy.Proxy.update_zone_transfer_request + .. automethod:: openstack.dns.v2._proxy.Proxy.delete_zone_transfer_request + .. automethod:: openstack.dns.v2._proxy.Proxy.zone_transfer_accepts + .. automethod:: openstack.dns.v2._proxy.Proxy.get_zone_transfer_accept + .. automethod:: openstack.dns.v2._proxy.Proxy.create_zone_transfer_accept diff --git a/doc/source/user/resources/dns/index.rst b/doc/source/user/resources/dns/index.rst new file mode 100644 index 000000000..a8d0c9360 --- /dev/null +++ b/doc/source/user/resources/dns/index.rst @@ -0,0 +1,12 @@ +DNS Resources +============= + +.. toctree:: + :maxdepth: 1 + + v2/zone + v2/zone_transfer + v2/zone_export + v2/zone_import + v2/floating_ip + v2/recordset diff --git a/doc/source/user/resources/dns/v2/floating_ip.rst b/doc/source/user/resources/dns/v2/floating_ip.rst new file mode 100644 index 000000000..d616e71a9 --- /dev/null +++ b/doc/source/user/resources/dns/v2/floating_ip.rst @@ -0,0 +1,12 @@ +openstack.dns.v2.floating_ip +============================ + +.. automodule:: openstack.dns.v2.floating_ip + +The FloatingIP Class +-------------------- + +The ``DNS`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.dns.v2.floating_ip.FloatingIP + :members: diff --git a/doc/source/user/resources/dns/v2/recordset.rst b/doc/source/user/resources/dns/v2/recordset.rst new file mode 100644 index 000000000..c02302f2d --- /dev/null +++ b/doc/source/user/resources/dns/v2/recordset.rst @@ -0,0 +1,12 @@ +openstack.dns.v2.recordset +========================== + +.. automodule:: openstack.dns.v2.recordset + +The Recordset Class +------------------- + +The ``DNS`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.dns.v2.recordset.Recordset + :members: diff --git a/doc/source/user/resources/dns/v2/zone.rst b/doc/source/user/resources/dns/v2/zone.rst new file mode 100644 index 000000000..634bd8f3f --- /dev/null +++ b/doc/source/user/resources/dns/v2/zone.rst @@ -0,0 +1,12 @@ +openstack.dns.v2.zone +============================== + +.. automodule:: openstack.dns.v2.zone + +The Zone Class +-------------- + +The ``DNS`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.dns.v2.zone.Zone + :members: diff --git a/doc/source/user/resources/dns/v2/zone_export.rst b/doc/source/user/resources/dns/v2/zone_export.rst new file mode 100644 index 000000000..2c2baa3ee --- /dev/null +++ b/doc/source/user/resources/dns/v2/zone_export.rst @@ -0,0 +1,12 @@ +openstack.dns.v2.zone_export +============================ + +.. automodule:: openstack.dns.v2.zone_export + +The ZoneExport Class +-------------------- + +The ``DNS`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.dns.v2.zone_export.ZoneExport + :members: diff --git a/doc/source/user/resources/dns/v2/zone_import.rst b/doc/source/user/resources/dns/v2/zone_import.rst new file mode 100644 index 000000000..5836f539d --- /dev/null +++ b/doc/source/user/resources/dns/v2/zone_import.rst @@ -0,0 +1,12 @@ +openstack.dns.v2.zone_import +============================ + +.. automodule:: openstack.dns.v2.zone_import + +The ZoneImport Class +-------------------- + +The ``DNS`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.dns.v2.zone_import.ZoneImport + :members: diff --git a/doc/source/user/resources/dns/v2/zone_transfer.rst b/doc/source/user/resources/dns/v2/zone_transfer.rst new file mode 100644 index 000000000..9f5c2c4c4 --- /dev/null +++ b/doc/source/user/resources/dns/v2/zone_transfer.rst @@ -0,0 +1,20 @@ +openstack.dns.v2.zone_transfer +============================== + +.. automodule:: openstack.dns.v2.zone_transfer + +The ZoneTransferRequest Class +----------------------------- + +The ``DNS`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.dns.v2.zone_transfer.ZoneTransferRequest + :members: + +The ZoneTransferAccept Class +---------------------------- + +The ``DNS`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.dns.v2.zone_transfer.ZoneTransferAccept + :members: diff --git a/examples/dns/__init__.py b/examples/dns/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/dns/list.py b/examples/dns/list.py new file mode 100644 index 000000000..47024801e --- /dev/null +++ b/examples/dns/list.py @@ -0,0 +1,24 @@ +# 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. + +""" +List resources from the DNS service. + +For a full guide see TODO(gtema):link to docs on developer.openstack.org +""" + + +def list_zones(conn): + print("List Zones:") + + for zone in conn.dns.zones(): + print(zone) diff --git a/openstack/dns/__init__.py b/openstack/dns/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/dns/dns_service.py b/openstack/dns/dns_service.py new file mode 100644 index 000000000..6fa162b57 --- /dev/null +++ b/openstack/dns/dns_service.py @@ -0,0 +1,22 @@ +# 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.dns.v2 import _proxy +from openstack import service_description + + +class DnsService(service_description.ServiceDescription): + """The DNS service.""" + + supported_versions = { + '2': _proxy.Proxy, + } diff --git a/openstack/dns/v2/__init__.py b/openstack/dns/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py new file mode 100644 index 000000000..7e36b0c5e --- /dev/null +++ b/openstack/dns/v2/_proxy.py @@ -0,0 +1,494 @@ +# 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 proxy +from openstack.dns.v2 import recordset as _rs +from openstack.dns.v2 import zone as _zone +from openstack.dns.v2 import zone_import as _zone_import +from openstack.dns.v2 import zone_export as _zone_export +from openstack.dns.v2 import zone_transfer as _zone_transfer +from openstack.dns.v2 import floating_ip as _fip + + +class Proxy(proxy.Proxy): + + # ======== Zones ======== + def zones(self, **query): + """Retrieve a generator of zones + + :param dict query: Optional query parameters to be sent to limit the + resources being returned. + + * `name`: Zone Name field. + * `type`: Zone Type field. + * `email`: Zone email field. + * `status`: Status of the zone. + * `ttl`: TTL field filter.abs + * `description`: Zone description field filter. + + :returns: A generator of zone + :class:`~openstack.dns.v2.zone.Zone` instances. + """ + return self._list(_zone.Zone, **query) + + def create_zone(self, **attrs): + """Create a new zone from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.dns.v2.zone.Zone`, + comprised of the properties on the Zone class. + :returns: The results of zone creation. + :rtype: :class:`~openstack.dns.v2.zone.Zone` + """ + return self._create(_zone.Zone, prepend_key=False, **attrs) + + def get_zone(self, zone): + """Get a zone + + :param zone: The value can be the ID of a zone + or a :class:`~openstack.dns.v2.zone.Zone` instance. + :returns: Zone instance. + :rtype: :class:`~openstack.dns.v2.zone.Zone` + """ + return self._get(_zone.Zone, zone) + + def delete_zone(self, zone, ignore_missing=True): + """Delete a zone + + :param zone: The value can be the ID of a zone + or a :class:`~openstack.dns.v2.zone.Zone` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the zone does not exist. + When set to ``True``, no exception will be set when attempting to + delete a nonexistent zone. + + :returns: Zone been deleted + :rtype: :class:`~openstack.dns.v2.zone.Zone` + """ + return self._delete(_zone.Zone, zone, ignore_missing=ignore_missing) + + def update_zone(self, zone, **attrs): + """Update zone attributes + + :param zone: The id or an instance of + :class:`~openstack.dns.v2.zone.Zone`. + :param dict attrs: attributes for update on + :class:`~openstack.dns.v2.zone.Zone`. + + :rtype: :class:`~openstack.dns.v2.zone.Zone` + """ + return self._update(_zone.Zone, zone, **attrs) + + def find_zone(self, name_or_id, ignore_missing=True): + """Find a single zone + + :param name_or_id: The name or ID of a zone + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the zone does not exist. + When set to ``True``, no exception will be set when attempting + to delete a nonexistent zone. + + :returns: :class:`~openstack.dns.v2.zone.Zone` + """ + return self._find(_zone.Zone, name_or_id, + ignore_missing=ignore_missing) + + def abandon_zone(self, zone, **attrs): + """Abandon Zone + + :param zone: The value can be the ID of a zone to be abandoned + or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance. + + :returns: None + """ + zone = self._get_resource(_zone.Zone, zone) + + return zone.abandon(self) + + def xfr_zone(self, zone, **attrs): + """Trigger update of secondary Zone + + :param zone: The value can be the ID of a zone to be abandoned + or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance. + + :returns: None + """ + zone = self._get_resource(_zone.Zone, zone) + return zone.xfr(self) + + # ======== Recordsets ======== + def recordsets(self, zone=None, **query): + """Retrieve a generator of recordsets + + :param zone: The optional value can be the ID of a zone + or a :class:`~openstack.dns.v2.zone.Zone` instance. If it is not + given all recordsets for all zones of the tenant would be + retrieved + :param dict query: Optional query parameters to be sent to limit the + resources being returned. + + * `name`: Recordset Name field. + * `type`: Type field. + * `status`: Status of the recordset. + * `ttl`: TTL field filter. + * `description`: Recordset description field filter. + + :returns: A generator of zone + (:class:`~openstack.dns.v2.recordset.Recordset`) instances + """ + base_path = None + if not zone: + base_path = '/recordsets' + else: + zone = self._get_resource(_zone.Zone, zone) + query.update({'zone_id': zone.id}) + return self._list(_rs.Recordset, base_path=base_path, **query) + + def create_recordset(self, zone, **attrs): + """Create a new recordset in the zone + + :param zone: The value can be the ID of a zone + or a :class:`~openstack.dns.v2.zone.Zone` instance. + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.dns.v2.recordset.Recordset`, + comprised of the properties on the Recordset class. + :returns: The results of zone creation + :rtype: :class:`~openstack.dns.v2.recordset.Recordset` + """ + zone = self._get_resource(_zone.Zone, zone) + attrs.update({'zone_id': zone.id}) + return self._create(_rs.Recordset, prepend_key=False, **attrs) + + def update_recordset(self, recordset, **attrs): + """Update Recordset attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.dns.v2.recordset.Recordset`, + comprised of the properties on the Recordset class. + :returns: The results of zone creation + :rtype: :class:`~openstack.dns.v2.recordset.Recordset` + """ + return self._update(_rs.Recordset, recordset, **attrs) + + def get_recordset(self, recordset, zone): + """Get a recordset + + :param zone: The value can be the ID of a zone + or a :class:`~openstack.dns.v2.zone.Zone` instance. + :param recordset: The value can be the ID of a recordset + or a :class:`~openstack.dns.v2.recordset.Recordset` instance. + :returns: Recordset instance + :rtype: :class:`~openstack.dns.v2.recordset.Recordset` + """ + zone = self._get_resource(_zone.Zone, zone) + return self._get(_rs.Recordset, recordset, zone_id=zone.id) + + def delete_recordset(self, recordset, zone=None, ignore_missing=True): + """Delete a zone + + :param recordset: The value can be the ID of a recordset + or a :class:`~openstack.dns.v2.recordset.Recordset` + instance. + :param zone: The value can be the ID of a zone + or a :class:`~openstack.dns.v2.zone.Zone` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the zone does not exist. When set to ``True``, no exception will + be set when attempting to delete a nonexistent zone. + + :returns: Recordset instance been deleted + :rtype: :class:`~openstack.dns.v2.recordset.Recordset` + """ + if zone: + zone = self._get_resource(_zone.Zone, zone) + recordset = self._get( + _rs.Recordset, recordset, zone_id=zone.id) + return self._delete(_rs.Recordset, recordset, + ignore_missing=ignore_missing) + + # ======== Zone Imports ======== + def zone_imports(self, **query): + """Retrieve a generator of zone imports + + :param dict query: Optional query parameters to be sent to limit the + resources being returned. + + * `zone_id`: Zone I field. + * `message`: Message field. + * `status`: Status of the zone import record. + + :returns: A generator of zone + :class:`~openstack.dns.v2.zone_import.ZoneImport` instances. + """ + return self._list(_zone_import.ZoneImport, **query) + + def create_zone_import(self, **attrs): + """Create a new zone import from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.dns.v2.zone_import.ZoneImport`, + comprised of the properties on the ZoneImport class. + :returns: The results of zone creation. + :rtype: :class:`~openstack.dns.v2.zone_import.ZoneImport` + """ + return self._create(_zone_import.ZoneImport, prepend_key=False, + **attrs) + + def get_zone_import(self, zone_import): + """Get a zone import record + + :param zone: The value can be the ID of a zone import + or a :class:`~openstack.dns.v2.zone_import.ZoneImport` instance. + :returns: ZoneImport instance. + :rtype: :class:`~openstack.dns.v2.zone_import.ZoneImport` + """ + return self._get(_zone_import.ZoneImport, zone_import) + + def delete_zone_import(self, zone_import, ignore_missing=True): + """Delete a zone import + + :param zone_import: The value can be the ID of a zone import + or a :class:`~openstack.dns.v2.zone_import.ZoneImport` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the zone does not exist. + When set to ``True``, no exception will be set when attempting to + delete a nonexistent zone. + + :returns: None + """ + return self._delete(_zone_import.ZoneImport, zone_import, + ignore_missing=ignore_missing) + + # ======== Zone Exports ======== + def zone_exports(self, **query): + """Retrieve a generator of zone exports + + :param dict query: Optional query parameters to be sent to limit the + resources being returned. + + * `zone_id`: Zone I field. + * `message`: Message field. + * `status`: Status of the zone import record. + + :returns: A generator of zone + :class:`~openstack.dns.v2.zone_export.ZoneExport` instances. + """ + return self._list(_zone_export.ZoneExport, **query) + + def create_zone_export(self, zone, **attrs): + """Create a new zone export from attributes + + :param zone: The value can be the ID of a zone to be exported + or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance. + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.dns.v2.zone_export.ZoneExport`, + comprised of the properties on the ZoneExport class. + :returns: The results of zone creation. + :rtype: :class:`~openstack.dns.v2.zone_export.ZoneExport` + """ + zone = self._get_resource(_zone.Zone, zone) + return self._create(_zone_export.ZoneExport, + base_path='/zones/%(zone_id)s/tasks/export', + prepend_key=False, + zone_id=zone.id, + **attrs) + + def get_zone_export(self, zone_export): + """Get a zone export record + + :param zone: The value can be the ID of a zone import + or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance. + :returns: ZoneExport instance. + :rtype: :class:`~openstack.dns.v2.zone_export.ZoneExport` + """ + return self._get(_zone_export.ZoneExport, zone_export) + + def get_zone_export_text(self, zone_export): + """Get a zone export record as text + + :param zone: The value can be the ID of a zone import + or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance. + :returns: ZoneExport instance. + :rtype: :class:`~openstack.dns.v2.zone_export.ZoneExport` + """ + return self._get(_zone_export.ZoneExport, zone_export, + base_path='/zones/tasks/export/%(id)s/export') + + def delete_zone_export(self, zone_export, ignore_missing=True): + """Delete a zone export + + :param zone_export: The value can be the ID of a zone import + or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the zone does not exist. + When set to ``True``, no exception will be set when attempting to + delete a nonexistent zone. + + :returns: None + """ + return self._delete(_zone_export.ZoneExport, zone_export, + ignore_missing=ignore_missing) + + # ======== FloatingIPs ======== + def floating_ips(self, **query): + """Retrieve a generator of recordsets + + :param dict query: Optional query parameters to be sent to limit the + resources being returned. + + * `name`: Recordset Name field. + * `type`: Type field. + * `status`: Status of the recordset. + * `ttl`: TTL field filter. + * `description`: Recordset description field filter. + + :returns: A generator of floatingips + (:class:`~openstack.dns.v2.floating_ip.FloatingIP`) instances + """ + return self._list(_fip.FloatingIP, **query) + + def get_floating_ip(self, floating_ip): + """Get a Floating IP + + :param floating_ip: The value can be the ID of a floating ip + or a :class:`~openstack.dns.v2.floating_ip.FloatingIP` instance. + The ID is in format "region_name:floatingip_id" + :returns: FloatingIP instance. + :rtype: :class:`~openstack.dns.v2.floating_ip.FloatingIP` + """ + return self._get(_fip.FloatingIP, floating_ip) + + def update_floating_ip(self, floating_ip, **attrs): + """Update floating ip attributes + + :param floating_ip: The id or an instance of + :class:`~openstack.dns.v2.fip.FloatingIP`. + :param dict attrs: attributes for update on + :class:`~openstack.dns.v2.fip.FloatingIP`. + + :rtype: :class:`~openstack.dns.v2.fip.FloatingIP` + """ + return self._update(_fip.FloatingIP, floating_ip, **attrs) + + # ======== Zone Transfer ======== + def zone_transfer_requests(self, **query): + """Retrieve a generator of zone transfer requests + + :param dict query: Optional query parameters to be sent to limit the + resources being returned. + + * `status`: Status of the recordset. + + :returns: A generator of transfer requests + (:class:`~openstack.dns.v2.zone_transfer.ZoneTransferRequest`) + instances + """ + return self._list(_zone_transfer.ZoneTransferRequest, **query) + + def get_zone_transfer_request(self, request): + """Get a ZoneTransfer Request info + + :param request: The value can be the ID of a transfer request + or a :class:`~openstack.dns.v2.zone_transfer.ZoneTransferRequest` + instance. + :returns: Zone transfer request instance. + :rtype: :class:`~openstack.dns.v2.zone_transfer.ZoneTransferRequest` + """ + return self._get(_zone_transfer.ZoneTransferRequest, request) + + def create_zone_transfer_request(self, zone, **attrs): + """Create a new ZoneTransfer Request from attributes + + :param zone: The value can be the ID of a zone to be transferred + or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance. + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.dns.v2.zone_transfer.ZoneTransferRequest`, + comprised of the properties on the ZoneTransferRequest class. + :returns: The results of zone transfer request creation. + :rtype: :class:`~openstack.dns.v2.zone_transfer.ZoneTransferRequest` + """ + zone = self._get_resource(_zone.Zone, zone) + return self._create( + _zone_transfer.ZoneTransferRequest, + base_path='/zones/%(zone_id)s/tasks/transfer_requests', + prepend_key=False, + zone_id=zone.id, + **attrs) + + def update_zone_transfer_request(self, request, **attrs): + """Update ZoneTransfer Request attributes + + :param floating_ip: The id or an instance of + :class:`~openstack.dns.v2.zone_transfer.ZoneTransferRequest`. + :param dict attrs: attributes for update on + :class:`~openstack.dns.v2.zone_transfer.ZoneTransferRequest`. + + :rtype: :class:`~openstack.dns.v2.zone_transfer.ZoneTransferRequest` + """ + return self._update(_zone_transfer.ZoneTransferRequest, + request, **attrs) + + def delete_zone_transfer_request(self, request, ignore_missing=True): + """Delete a ZoneTransfer Request + + :param request: The value can be the ID of a zone transfer request + or a :class:`~openstack.dns.v2.zone_transfer.ZoneTransferRequest` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the zone does not exist. + When set to ``True``, no exception will be set when attempting to + delete a nonexistent zone. + + :returns: None + """ + return self._delete(_zone_transfer.ZoneTransferRequest, request, + ignore_missing=ignore_missing) + + def zone_transfer_accepts(self, **query): + """Retrieve a generator of zone transfer accepts + + :param dict query: Optional query parameters to be sent to limit the + resources being returned. + + * `status`: Status of the recordset. + + :returns: A generator of transfer accepts + (:class:`~openstack.dns.v2.zone_transfer.ZoneTransferAccept`) + instances + """ + return self._list(_zone_transfer.ZoneTransferAccept, **query) + + def get_zone_transfer_accept(self, accept): + """Get a ZoneTransfer Accept info + + :param request: The value can be the ID of a transfer accept + or a :class:`~openstack.dns.v2.zone_transfer.ZoneTransferAccept` + instance. + :returns: Zone transfer request instance. + :rtype: :class:`~openstack.dns.v2.zone_transfer.ZoneTransferAccept` + """ + return self._get(_zone_transfer.ZoneTransferAccept, accept) + + def create_zone_transfer_accept(self, **attrs): + """Create a new ZoneTransfer Accept from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.dns.v2.zone_transfer.ZoneTransferAccept`, + comprised of the properties on the ZoneTransferAccept class. + :returns: The results of zone transfer request creation. + :rtype: :class:`~openstack.dns.v2.zone_transfer.ZoneTransferAccept` + """ + return self._create(_zone_transfer.ZoneTransferAccept, **attrs) diff --git a/openstack/dns/v2/floating_ip.py b/openstack/dns/v2/floating_ip.py new file mode 100644 index 000000000..f6b4eec97 --- /dev/null +++ b/openstack/dns/v2/floating_ip.py @@ -0,0 +1,40 @@ +# 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 import resource + + +class FloatingIP(resource.Resource): + """DNS Floating IP Resource""" + resource_key = '' + resources_key = 'floatingips' + base_path = '/reverse/floatingips' + + # capabilities + allow_fetch = True + allow_commit = True + allow_list = True + commit_method = "PATCH" + + #: Properties + #: current action in progress on the resource + action = resource.Body('action') + #: The floatingip address for this PTR record + address = resource.Body('address') + #: Description for this PTR record + description = resource.Body('description') + #: Domain name for this PTR record + ptrdname = resource.Body('ptrdname') + #: status of the resource + status = resource.Body('status') + #: Time to live for this PTR record + ttl = resource.Body('ttl', type=int) diff --git a/openstack/dns/v2/recordset.py b/openstack/dns/v2/recordset.py new file mode 100644 index 000000000..949b25d7e --- /dev/null +++ b/openstack/dns/v2/recordset.py @@ -0,0 +1,64 @@ +# 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 import resource + + +class Recordset(resource.Resource): + """DNS Recordset Resource""" + resource_key = 'recordset' + resources_key = 'recordsets' + base_path = '/zones/%(zone_id)s/recordsets' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + _query_mapping = resource.QueryParameters( + 'name', 'type', 'ttl', 'data', 'status', 'description', + 'limit', 'marker') + + #: Properties + #: current action in progress on the resource + action = resource.Body('action') + #: Timestamp when the zone was created + created_at = resource.Body('create_at') + #: Recordset description + description = resource.Body('description') + #: Links contains a `self` pertaining to this zone or a `next` pertaining + #: to next page + links = resource.Body('links', type=dict) + #: DNS Name of the recordset + name = resource.Body('name') + #: ID of the project which the recordset belongs to + project_id = resource.Body('project_id') + #: DNS record value list + records = resource.Body('records', type=list) + #: Recordset status + #: Valid values include: `PENDING_CREATE`, `ACTIVE`,`PENDING_DELETE`, + #: `ERROR` + status = resource.Body('status') + #: Time to live, default 300, available value 300-2147483647 (seconds) + ttl = resource.Body('ttl', type=int) + #: DNS type of the recordset + #: Valid values include `A`, `AAAA`, `MX`, `CNAME`, `TXT`, `NS`, + #: `SSHFP`, `SPF`, `SRV`, `PTR` + type = resource.Body('type') + #: Timestamp when the zone was last updated + updated_at = resource.Body('updated_at') + #: The id of the Zone which this recordset belongs to + zone_id = resource.URI('zone_id') + #: The name of the Zone which this recordset belongs to + zone_name = resource.Body('zone_name') diff --git a/openstack/dns/v2/zone.py b/openstack/dns/v2/zone.py new file mode 100644 index 000000000..737792bcf --- /dev/null +++ b/openstack/dns/v2/zone.py @@ -0,0 +1,96 @@ +# 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 import resource +from openstack import utils + + +class Zone(resource.Resource): + """DNS ZONE Resource""" + resources_key = 'zones' + base_path = '/zones' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + commit_method = "PATCH" + + _query_mapping = resource.QueryParameters( + 'name', 'type', 'email', 'status', 'description', 'ttl', + 'limit', 'marker' + ) + + #: Properties + #: current action in progress on the resource + action = resource.Body('action') + #: Attributes + #: Key:Value pairs of information about this zone, and the pool the user + #: would like to place the zone in. This information can be used by the + #: scheduler to place zones on the correct pool. + attributes = resource.Body('attributes', type=dict) + #: Timestamp when the zone was created + created_at = resource.Body('created_at') + #: Zone description + #: *Type: str* + description = resource.Body('description') + #: The administrator email of this zone + #: *Type: str* + email = resource.Body('email') + #: Links contains a `self` pertaining to this zone or a `next` pertaining + #: to next page + links = resource.Body('links', type=dict) + #: The master list for slaver server to fetch DNS + masters = resource.Body('masters', type=list) + #: Zone name + name = resource.Body('name') + #: The pool which manages the zone, assigned by system + pool_id = resource.Body('pool_id') + #: The project id which the zone belongs to + project_id = resource.Body('project_id') + #: Serial number in the SOA record set in the zone, + #: which identifies the change on the primary DNS server + #: *Type: int* + serial = resource.Body('serial', type=int) + #: Zone status + #: Valid values include `PENDING_CREATE`, `ACTIVE`, + #: `PENDING_DELETE`, `ERROR` + status = resource.Body('status') + #: SOA TTL time, unit is seconds, default 300, TTL range 300-2147483647 + #: *Type: int* + ttl = resource.Body('ttl', type=int) + #: Zone type, + #: Valid values include `PRIMARY`, `SECONDARY` + #: *Type: str* + type = resource.Body('type') + #: Timestamp when the zone was last updated + updated_at = resource.Body('updated_at') + + def _action(self, session, action, body): + """Preform actions given the message body. + + """ + url = utils.urljoin(self.base_path, self.id, 'tasks', action) + response = session.post( + url, + json=body) + exceptions.raise_from_response(response) + return response + + def abandon(self, session): + self._action(session, 'abandon', None) + + def xfr(self, session): + self._action(session, 'xfr', None) diff --git a/openstack/dns/v2/zone_export.py b/openstack/dns/v2/zone_export.py new file mode 100644 index 000000000..6cd5c9232 --- /dev/null +++ b/openstack/dns/v2/zone_export.py @@ -0,0 +1,86 @@ +# 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 import resource + + +class ZoneExport(resource.Resource): + """DNS Zone Exports Resource""" + resource_key = '' + resources_key = 'exports' + base_path = '/zones/tasks/export' + + # capabilities + allow_create = True + allow_fetch = True + allow_delete = True + allow_list = True + + _query_mapping = resource.QueryParameters( + 'zone_id', 'message', 'status' + ) + + #: Properties + #: Timestamp when the zone was created + created_at = resource.Body('created_at') + #: Links contains a `self` pertaining to this zone or a `next` pertaining + #: to next page + links = resource.Body('links', type=dict) + #: Message + message = resource.Body('message') + #: Returns the total_count of resources matching this filter + metadata = resource.Body('metadata', type=list) + #: The project id which the zone belongs to + project_id = resource.Body('project_id') + #: Current status of the zone export + status = resource.Body('status') + #: Timestamp when the zone was last updated + updated_at = resource.Body('updated_at') + #: Version of the resource + version = resource.Body('version', type=int) + #: ID for the zone that was created by this export + zone_id = resource.Body('zone_id') + + def create(self, session, prepend_key=True, base_path=None): + """Create a remote resource based on this instance. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param prepend_key: A boolean indicating whether the resource_key + should be prepended in a resource creation + request. Default to True. + :param str base_path: Base part of the URI for creating resources, if + different from + :data:`~openstack.resource.Resource.base_path`. + :return: This :class:`Resource` instance. + :raises: :exc:`~openstack.exceptions.MethodNotSupported` if + :data:`Resource.allow_create` is not set to ``True``. + """ + if not self.allow_create: + raise exceptions.MethodNotSupported(self, "create") + + session = self._get_session(session) + microversion = self._get_microversion_for(session, 'create') + # Create ZoneExport requires empty body + # skip _prepare_request completely, since we need just empty body + request = resource._Request( + self.base_path, + None, + None + ) + response = session.post(request.url, + json=request.body, headers=request.headers, + microversion=microversion) + + self.microversion = microversion + self._translate_response(response) + return self diff --git a/openstack/dns/v2/zone_import.py b/openstack/dns/v2/zone_import.py new file mode 100644 index 000000000..5a18bdcc7 --- /dev/null +++ b/openstack/dns/v2/zone_import.py @@ -0,0 +1,86 @@ +# 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 import resource + + +class ZoneImport(resource.Resource): + """DNS Zone Import Resource""" + resource_key = '' + resources_key = 'imports' + base_path = '/zones/tasks/import' + + # capabilities + allow_create = True + allow_fetch = True + allow_delete = True + allow_list = True + + _query_mapping = resource.QueryParameters( + 'zone_id', 'message', 'status' + ) + + #: Properties + #: Timestamp when the zone was created + created_at = resource.Body('created_at') + #: Links contains a `self` pertaining to this zone or a `next` pertaining + #: to next page + links = resource.Body('links', type=dict) + #: Message + message = resource.Body('message') + #: Returns the total_count of resources matching this filter + metadata = resource.Body('metadata', type=list) + #: The project id which the zone belongs to + project_id = resource.Body('project_id') + #: Current status of the zone import + status = resource.Body('status') + #: Timestamp when the zone was last updated + updated_at = resource.Body('updated_at') + #: Version of the resource + version = resource.Body('version', type=int) + #: ID for the zone that was created by this import + zone_id = resource.Body('zone_id') + + def create(self, session, prepend_key=True, base_path=None): + """Create a remote resource based on this instance. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param prepend_key: A boolean indicating whether the resource_key + should be prepended in a resource creation + request. Default to True. + :param str base_path: Base part of the URI for creating resources, if + different from + :data:`~openstack.resource.Resource.base_path`. + :return: This :class:`Resource` instance. + :raises: :exc:`~openstack.exceptions.MethodNotSupported` if + :data:`Resource.allow_create` is not set to ``True``. + """ + if not self.allow_create: + raise exceptions.MethodNotSupported(self, "create") + + session = self._get_session(session) + microversion = self._get_microversion_for(session, 'create') + # Create ZoneImport requires empty body and 'text/dns' as content-type + # skip _prepare_request completely, since we need just empty body + request = resource._Request( + self.base_path, + None, + {'content-type': 'text/dns'} + ) + response = session.post(request.url, + json=request.body, headers=request.headers, + microversion=microversion) + + self.microversion = microversion + self._translate_response(response) + return self diff --git a/openstack/dns/v2/zone_transfer.py b/openstack/dns/v2/zone_transfer.py new file mode 100644 index 000000000..2bd95fe3c --- /dev/null +++ b/openstack/dns/v2/zone_transfer.py @@ -0,0 +1,72 @@ +# 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 resource + + +class ZoneTransferBase(resource.Resource): + """DNS Zone Transfer Request/Accept Base Resource""" + + _query_mapping = resource.QueryParameters( + 'status' + ) + + #: Properties + #: Timestamp when the resource was created + created_at = resource.Body('created_at') + #: Key that is used as part of the zone transfer accept process. + #: This is only shown to the creator, and must be communicated out of band. + key = resource.Body('key') + #: The project id which the zone belongs to + project_id = resource.Body('project_id') + #: Current status of the zone import + status = resource.Body('status') + #: Timestamp when the resource was last updated + updated_at = resource.Body('updated_at') + #: Version of the resource + version = resource.Body('version', type=int) + #: ID for the zone that is being exported + zone_id = resource.Body('zone_id') + + +class ZoneTransferRequest(ZoneTransferBase): + """DNS Zone Transfer Request Resource""" + base_path = '/zones/tasks/transfer_requests' + resources_key = 'transfer_requests' + + # capabilities + allow_create = True + allow_fetch = True + allow_delete = True + allow_list = True + allow_commit = True + + #: Description + description = resource.Body('description') + #: A project ID that the request will be limited to. + #: No other project will be allowed to accept this request. + target_project_id = resource.Body('target_project_id') + #: Name for the zone that is being exported + zone_name = resource.Body('zone_name') + + +class ZoneTransferAccept(ZoneTransferBase): + """DNS Zone Transfer Accept Resource""" + base_path = '/zones/tasks/transfer_accepts' + resources_key = 'transfer_accepts' + + # capabilities + allow_create = True + allow_fetch = True + allow_list = True + + #: Name for the zone that is being exported + zone_transfer_request_id = resource.Body('zone_transfer_request_id') diff --git a/openstack/tests/functional/dns/__init__.py b/openstack/tests/functional/dns/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/functional/dns/v2/__init__.py b/openstack/tests/functional/dns/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/functional/dns/v2/test_zone.py b/openstack/tests/functional/dns/v2/test_zone.py new file mode 100644 index 000000000..2d4527cc8 --- /dev/null +++ b/openstack/tests/functional/dns/v2/test_zone.py @@ -0,0 +1,52 @@ +# 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 random + +from openstack import connection +from openstack.tests.functional import base + + +class TestZone(base.BaseFunctionalTest): + + def setUp(self): + super(TestZone, self).setUp() + self.require_service('dns') + + self.conn = connection.from_config(cloud_name=base.TEST_CLOUD_NAME) + + # Note: zone deletion is not an immediate operation, so each time + # chose a new zone name for a test + # getUniqueString is not guaranteed to return unique string between + # different tests of the same class. + self.ZONE_NAME = 'example-{0}.org.'.format(random.randint(1, 100)) + + self.zone = self.conn.dns.create_zone( + name=self.ZONE_NAME, + email='joe@example.org', + type='PRIMARY', + ttl=7200, + description='example zone' + ) + self.addCleanup(self.conn.dns.delete_zone, self.zone) + + def tearDown(self): + if self.zone: + self.conn.dns.delete_zone(self.zone) + super(TestZone, self).tearDown() + + def test_get_zone(self): + zone = self.conn.dns.get_zone(self.zone) + self.assertEqual(self.zone, zone) + + def test_list_zones(self): + names = [f.name for f in self.conn.dns.zones()] + self.assertIn(self.ZONE_NAME, names) diff --git a/openstack/tests/unit/dns/__init__.py b/openstack/tests/unit/dns/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/unit/dns/v2/__init__.py b/openstack/tests/unit/dns/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/unit/dns/v2/test_floating_ip.py b/openstack/tests/unit/dns/v2/test_floating_ip.py new file mode 100644 index 000000000..412e453c9 --- /dev/null +++ b/openstack/tests/unit/dns/v2/test_floating_ip.py @@ -0,0 +1,56 @@ +# 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.tests.unit import base + +from openstack.dns.v2 import floating_ip as fip + + +IDENTIFIER = 'RegionOne:id' +EXAMPLE = { + 'status': 'PENDING', + 'ptrdname': 'smtp.example.com.', + 'description': 'This is a floating ip for 127.0.0.1', + 'links': { + 'self': 'dummylink/reverse/floatingips/RegionOne:id' + }, + 'ttl': 600, + 'address': '172.24.4.10', + 'action': 'CREATE', + 'id': IDENTIFIER +} + + +class TestFloatingIP(base.TestCase): + + def test_basic(self): + sot = fip.FloatingIP() + self.assertEqual('', sot.resource_key) + self.assertEqual('floatingips', sot.resources_key) + self.assertEqual('/reverse/floatingips', sot.base_path) + self.assertTrue(sot.allow_list) + self.assertFalse(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertFalse(sot.allow_delete) + + self.assertEqual('PATCH', sot.commit_method) + + def test_make_it(self): + sot = fip.FloatingIP(**EXAMPLE) + self.assertEqual(IDENTIFIER, sot.id) + self.assertEqual(EXAMPLE['ptrdname'], sot.ptrdname) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['ttl'], sot.ttl) + self.assertEqual(EXAMPLE['address'], sot.address) + self.assertEqual(EXAMPLE['action'], sot.action) + self.assertEqual(EXAMPLE['status'], sot.status) diff --git a/openstack/tests/unit/dns/v2/test_proxy.py b/openstack/tests/unit/dns/v2/test_proxy.py new file mode 100644 index 000000000..9814ea59d --- /dev/null +++ b/openstack/tests/unit/dns/v2/test_proxy.py @@ -0,0 +1,197 @@ +# 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.dns.v2 import _proxy +from openstack.dns.v2 import zone +from openstack.dns.v2 import zone_import +from openstack.dns.v2 import zone_export +from openstack.dns.v2 import zone_transfer +from openstack.dns.v2 import recordset +from openstack.dns.v2 import floating_ip +from openstack.tests.unit import test_proxy_base + + +class TestDnsProxy(test_proxy_base.TestProxyBase): + def setUp(self): + super(TestDnsProxy, self).setUp() + self.proxy = _proxy.Proxy(self.session) + + +class TestDnsZone(TestDnsProxy): + def test_zone_create(self): + self.verify_create(self.proxy.create_zone, zone.Zone, + method_kwargs={'name': 'id'}, + expected_kwargs={'name': 'id', + 'prepend_key': False}) + + def test_zone_delete(self): + self.verify_delete(self.proxy.delete_zone, + zone.Zone, True) + + def test_zone_find(self): + self.verify_find(self.proxy.find_zone, zone.Zone) + + def test_zone_get(self): + self.verify_get(self.proxy.get_zone, zone.Zone) + + def test_zones(self): + self.verify_list(self.proxy.zones, zone.Zone) + + def test_zone_update(self): + self.verify_update(self.proxy.update_zone, zone.Zone) + + def test_zone_abandon(self): + self._verify("openstack.dns.v2.zone.Zone.abandon", + self.proxy.abandon_zone, + method_args=[{'zone': 'id'}]) + + def test_zone_xfr(self): + self._verify("openstack.dns.v2.zone.Zone.xfr", + self.proxy.xfr_zone, + method_args=[{'zone': 'id'}]) + + +class TestDnsRecordset(TestDnsProxy): + def test_recordset_create(self): + self.verify_create(self.proxy.create_recordset, recordset.Recordset, + method_kwargs={'zone': 'id'}, + expected_kwargs={'zone_id': 'id', + 'prepend_key': False}) + + def test_recordset_delete(self): + self.verify_delete(self.proxy.delete_recordset, + recordset.Recordset, True) + + def test_recordset_update(self): + self.verify_update(self.proxy.update_recordset, recordset.Recordset) + + def test_recordset_get(self): + self.verify_get(self.proxy.get_recordset, recordset.Recordset, + method_kwargs={'zone': 'zid'}, + expected_kwargs={'zone_id': 'zid'} + ) + + def test_recordsets(self): + self.verify_list(self.proxy.recordsets, recordset.Recordset, + base_path='/recordsets') + + def test_recordsets_zone(self): + self.verify_list(self.proxy.recordsets, recordset.Recordset, + method_kwargs={'zone': 'zid'}, + expected_kwargs={'zone_id': 'zid'}) + + +class TestDnsFloatIP(TestDnsProxy): + def test_floating_ips(self): + self.verify_list(self.proxy.floating_ips, floating_ip.FloatingIP) + + def test_floating_ip_get(self): + self.verify_get(self.proxy.get_floating_ip, floating_ip.FloatingIP) + + def test_floating_ip_update(self): + self.verify_update(self.proxy.update_floating_ip, + floating_ip.FloatingIP) + + def test_zone_create(self): + self.verify_create(self.proxy.create_zone, zone.Zone, + method_kwargs={'name': 'id'}, + expected_kwargs={'name': 'id', + 'prepend_key': False}) + + +class TestDnsZoneImport(TestDnsProxy): + def test_zone_import_delete(self): + self.verify_delete(self.proxy.delete_zone_import, + zone_import.ZoneImport, True) + + def test_zone_import_get(self): + self.verify_get(self.proxy.get_zone_import, zone_import.ZoneImport) + + def test_zone_imports(self): + self.verify_list(self.proxy.zone_imports, zone_import.ZoneImport) + + def test_zone_import_create(self): + self.verify_create(self.proxy.create_zone_import, + zone_import.ZoneImport, + method_kwargs={'name': 'id'}, + expected_kwargs={'name': 'id', + 'prepend_key': False}) + + +class TestDnsZoneExport(TestDnsProxy): + def test_zone_export_delete(self): + self.verify_delete(self.proxy.delete_zone_export, + zone_export.ZoneExport, True) + + def test_zone_export_get(self): + self.verify_get(self.proxy.get_zone_export, zone_export.ZoneExport) + + def test_zone_export_get_text(self): + self.verify_get(self.proxy.get_zone_export_text, + zone_export.ZoneExport, + value=[{'id': 'zone_export_id_value'}], + expected_kwargs={ + 'base_path': '/zones/tasks/export/%(id)s/export' + }) + + def test_zone_exports(self): + self.verify_list(self.proxy.zone_exports, zone_export.ZoneExport) + + def test_zone_export_create(self): + self.verify_create(self.proxy.create_zone_export, + zone_export.ZoneExport, + method_args=[{'id': 'zone_id_value'}], + method_kwargs={'name': 'id'}, + expected_kwargs={'name': 'id', + 'zone_id': 'zone_id_value', + 'prepend_key': False}) + + +class TestDnsZoneTransferRequest(TestDnsProxy): + def test_zone_transfer_request_delete(self): + self.verify_delete(self.proxy.delete_zone_transfer_request, + zone_transfer.ZoneTransferRequest, True) + + def test_zone_transfer_request_get(self): + self.verify_get(self.proxy.get_zone_transfer_request, + zone_transfer.ZoneTransferRequest) + + def test_zone_transfer_requests(self): + self.verify_list(self.proxy.zone_transfer_requests, + zone_transfer.ZoneTransferRequest) + + def test_zone_transfer_request_create(self): + self.verify_create(self.proxy.create_zone_transfer_request, + zone_transfer.ZoneTransferRequest, + method_args=[{'id': 'zone_id_value'}], + method_kwargs={'name': 'id'}, + expected_kwargs={'name': 'id', + 'zone_id': 'zone_id_value', + 'prepend_key': False}) + + def test_zone_transfer_request_update(self): + self.verify_update(self.proxy.update_zone_transfer_request, + zone_transfer.ZoneTransferRequest) + + +class TestDnsZoneTransferAccept(TestDnsProxy): + def test_zone_transfer_accept_get(self): + self.verify_get(self.proxy.get_zone_transfer_accept, + zone_transfer.ZoneTransferAccept) + + def test_zone_transfer_accepts(self): + self.verify_list(self.proxy.zone_transfer_accepts, + zone_transfer.ZoneTransferAccept) + + def test_zone_transfer_accept_create(self): + self.verify_create(self.proxy.create_zone_transfer_accept, + zone_transfer.ZoneTransferAccept) diff --git a/openstack/tests/unit/dns/v2/test_recordset.py b/openstack/tests/unit/dns/v2/test_recordset.py new file mode 100644 index 000000000..700a5148a --- /dev/null +++ b/openstack/tests/unit/dns/v2/test_recordset.py @@ -0,0 +1,69 @@ +# 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.tests.unit import base + +from openstack.dns.v2 import recordset + + +IDENTIFIER = 'NAME' +EXAMPLE = { + 'description': 'This is an example record set.', + 'updated_at': None, + 'records': [ + '10.1.0.2' + ], + 'ttl': 3600, + 'id': IDENTIFIER, + 'name': 'example.org.', + 'project_id': '4335d1f0-f793-11e2-b778-0800200c9a66', + 'zone_id': '2150b1bf-dee2-4221-9d85-11f7886fb15f', + 'zone_name': 'example.com.', + 'created_at': '2014-10-24T19:59:44.000000', + 'version': 1, + 'type': 'A', + 'status': 'ACTIVE', + 'action': 'NONE' +} + + +class TestRecordset(base.TestCase): + + def test_basic(self): + sot = recordset.Recordset() + self.assertEqual('recordset', sot.resource_key) + self.assertEqual('recordsets', sot.resources_key) + self.assertEqual('/zones/%(zone_id)s/recordsets', sot.base_path) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + + self.assertDictEqual({'data': 'data', + 'description': 'description', + 'limit': 'limit', + 'marker': 'marker', + 'name': 'name', + 'status': 'status', + 'ttl': 'ttl', + 'type': 'type'}, + sot._query_mapping._mapping) + + def test_make_it(self): + sot = recordset.Recordset(**EXAMPLE) + self.assertEqual(IDENTIFIER, sot.id) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['ttl'], sot.ttl) + self.assertEqual(EXAMPLE['type'], sot.type) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['status'], sot.status) diff --git a/openstack/tests/unit/dns/v2/test_zone.py b/openstack/tests/unit/dns/v2/test_zone.py new file mode 100644 index 000000000..5b2daafdf --- /dev/null +++ b/openstack/tests/unit/dns/v2/test_zone.py @@ -0,0 +1,87 @@ +# 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 keystoneauth1 import adapter +import mock + +from openstack.tests.unit import base + +from openstack.dns.v2 import zone + + +IDENTIFIER = 'NAME' +EXAMPLE = { + 'attributes': { + 'tier': 'gold', 'ha': 'true' + }, + 'id': IDENTIFIER, + 'name': 'test.org', + 'email': 'joe@example.org', + 'type': 'PRIMARY', + 'ttl': 7200, + 'description': 'This is an example zone.', + 'status': 'ACTIVE' +} + + +class TestZone(base.TestCase): + + def setUp(self): + super(TestZone, self).setUp() + self.resp = mock.Mock() + self.resp.body = None + self.resp.json = mock.Mock(return_value=self.resp.body) + self.resp.status_code = 200 + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.post = mock.Mock(return_value=self.resp) + self.sess.default_microversion = None + + def test_basic(self): + sot = zone.Zone() + self.assertEqual(None, sot.resource_key) + self.assertEqual('zones', sot.resources_key) + self.assertEqual('/zones', sot.base_path) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + + self.assertEqual('PATCH', sot.commit_method) + + self.assertDictEqual({'description': 'description', + 'email': 'email', + 'limit': 'limit', + 'marker': 'marker', + 'name': 'name', + 'status': 'status', + 'ttl': 'ttl', + 'type': 'type'}, + sot._query_mapping._mapping) + + def test_make_it(self): + sot = zone.Zone(**EXAMPLE) + self.assertEqual(IDENTIFIER, sot.id) + self.assertEqual(EXAMPLE['email'], sot.email) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['ttl'], sot.ttl) + self.assertEqual(EXAMPLE['type'], sot.type) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['status'], sot.status) + + def test_abandon(self): + sot = zone.Zone(**EXAMPLE) + self.assertIsNone(sot.abandon(self.sess)) + self.sess.post.assert_called_with( + 'zones/NAME/tasks/abandon', + json=None + ) diff --git a/openstack/tests/unit/dns/v2/test_zone_export.py b/openstack/tests/unit/dns/v2/test_zone_export.py new file mode 100644 index 000000000..5b7876298 --- /dev/null +++ b/openstack/tests/unit/dns/v2/test_zone_export.py @@ -0,0 +1,80 @@ +# 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 keystoneauth1 import adapter +from openstack.tests.unit import base + +from openstack.dns.v2 import zone_export + + +IDENTIFIER = '074e805e-fe87-4cbb-b10b-21a06e215d41' +EXAMPLE = { + 'status': 'COMPLETE', + 'zone_id': '6625198b-d67d-47dc-8d29-f90bd60f3ac4', + 'links': { + 'self': 'http://127.0.0.1:9001/v2/zones/tasks/exports/074e805e-f', + 'href': 'http://127.0.0.1:9001/v2/zones/6625198b-d67d-' + }, + 'created_at': '2015-05-08T15:43:42.000000', + 'updated_at': '2015-05-08T15:43:43.000000', + 'version': 2, + 'location': 'designate://v2/zones/tasks/exports/8ec17fe1/export', + 'message': 'example.com. exported', + 'project_id': 'noauth-project', + 'id': IDENTIFIER +} + + +@mock.patch.object(zone_export.ZoneExport, '_translate_response', mock.Mock()) +class TestZoneExport(base.TestCase): + + def test_basic(self): + sot = zone_export.ZoneExport() + self.assertEqual('', sot.resource_key) + self.assertEqual('exports', sot.resources_key) + self.assertEqual('/zones/tasks/export', sot.base_path) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertTrue(sot.allow_delete) + + self.assertDictEqual({'limit': 'limit', + 'marker': 'marker', + 'message': 'message', + 'status': 'status', + 'zone_id': 'zone_id'}, + sot._query_mapping._mapping) + + def test_make_it(self): + sot = zone_export.ZoneExport(**EXAMPLE) + self.assertEqual(IDENTIFIER, sot.id) + self.assertEqual(EXAMPLE['created_at'], sot.created_at) + self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) + self.assertEqual(EXAMPLE['version'], sot.version) + self.assertEqual(EXAMPLE['message'], sot.message) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + self.assertEqual(EXAMPLE['status'], sot.status) + self.assertEqual(EXAMPLE['zone_id'], sot.zone_id) + + def test_create(self): + sot = zone_export.ZoneExport() + response = mock.Mock() + response.json = mock.Mock(return_value='') + self.session = mock.Mock(spec=adapter.Adapter) + self.session.default_microversion = '1.1' + + sot.create(self.session) + self.session.post.assert_called_once_with( + mock.ANY, json=None, + headers=None, + microversion=self.session.default_microversion) diff --git a/openstack/tests/unit/dns/v2/test_zone_import.py b/openstack/tests/unit/dns/v2/test_zone_import.py new file mode 100644 index 000000000..808830338 --- /dev/null +++ b/openstack/tests/unit/dns/v2/test_zone_import.py @@ -0,0 +1,79 @@ +# 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 keystoneauth1 import adapter +from openstack.tests.unit import base + +from openstack.dns.v2 import zone_import + + +IDENTIFIER = '074e805e-fe87-4cbb-b10b-21a06e215d41' +EXAMPLE = { + 'status': 'COMPLETE', + 'zone_id': '6625198b-d67d-47dc-8d29-f90bd60f3ac4', + 'links': { + 'self': 'http://127.0.0.1:9001/v2/zones/tasks/imports/074e805e-f', + 'href': 'http://127.0.0.1:9001/v2/zones/6625198b-d67d-' + }, + 'created_at': '2015-05-08T15:43:42.000000', + 'updated_at': '2015-05-08T15:43:43.000000', + 'version': 2, + 'message': 'example.com. imported', + 'project_id': 'noauth-project', + 'id': IDENTIFIER +} + + +@mock.patch.object(zone_import.ZoneImport, '_translate_response', mock.Mock()) +class TestZoneImport(base.TestCase): + + def test_basic(self): + sot = zone_import.ZoneImport() + self.assertEqual('', sot.resource_key) + self.assertEqual('imports', sot.resources_key) + self.assertEqual('/zones/tasks/import', sot.base_path) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertTrue(sot.allow_delete) + + self.assertDictEqual({'limit': 'limit', + 'marker': 'marker', + 'message': 'message', + 'status': 'status', + 'zone_id': 'zone_id'}, + sot._query_mapping._mapping) + + def test_make_it(self): + sot = zone_import.ZoneImport(**EXAMPLE) + self.assertEqual(IDENTIFIER, sot.id) + self.assertEqual(EXAMPLE['created_at'], sot.created_at) + self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) + self.assertEqual(EXAMPLE['version'], sot.version) + self.assertEqual(EXAMPLE['message'], sot.message) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + self.assertEqual(EXAMPLE['status'], sot.status) + self.assertEqual(EXAMPLE['zone_id'], sot.zone_id) + + def test_create(self): + sot = zone_import.ZoneImport() + response = mock.Mock() + response.json = mock.Mock(return_value='') + self.session = mock.Mock(spec=adapter.Adapter) + self.session.default_microversion = '1.1' + + sot.create(self.session) + self.session.post.assert_called_once_with( + mock.ANY, json=None, + headers={'content-type': 'text/dns'}, + microversion=self.session.default_microversion) diff --git a/openstack/tests/unit/dns/v2/test_zone_transfer.py b/openstack/tests/unit/dns/v2/test_zone_transfer.py new file mode 100644 index 000000000..7064834fe --- /dev/null +++ b/openstack/tests/unit/dns/v2/test_zone_transfer.py @@ -0,0 +1,103 @@ +# 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.tests.unit import base + +from openstack.dns.v2 import zone_transfer + + +IDENTIFIER = '074e805e-fe87-4cbb-b10b-21a06e215d41' +EXAMPLE_REQUEST = { + 'created_at': '2014-07-17T20:34:40.882579', + 'description': 'some description', + 'id': IDENTIFIER, + 'key': '9Z2R50Y0', + 'project_id': '1', + 'status': 'ACTIVE', + 'target_project_id': '123456', + 'updated_at': None, + 'zone_id': '6b78734a-aef1-45cd-9708-8eb3c2d26ff8', + 'zone_name': 'qa.dev.example.com.', +} +EXAMPLE_ACCEPT = { + 'status': 'COMPLETE', + 'zone_id': 'b4542f5a-f1ea-4ec1-b850-52db9dc3f465', + 'created_at': '2016-06-22 06:13:55', + 'updated_at': 'null', + 'key': 'FUGXMZ5N', + 'project_id': '2e43de7ce3504a8fb90a45382532c37e', + 'id': IDENTIFIER, + 'zone_transfer_request_id': '794fdf58-6e1d-41da-8b2d-16b6d10c8827' +} + + +class TestZoneTransferRequest(base.TestCase): + + def test_basic(self): + sot = zone_transfer.ZoneTransferRequest() + # self.assertEqual('', sot.resource_key) + self.assertEqual('transfer_requests', sot.resources_key) + self.assertEqual('/zones/tasks/transfer_requests', sot.base_path) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + + self.assertDictEqual({'limit': 'limit', + 'marker': 'marker', + 'status': 'status'}, + sot._query_mapping._mapping) + + def test_make_it(self): + sot = zone_transfer.ZoneTransferRequest(**EXAMPLE_REQUEST) + self.assertEqual(IDENTIFIER, sot.id) + self.assertEqual(EXAMPLE_REQUEST['created_at'], sot.created_at) + self.assertEqual(EXAMPLE_REQUEST['updated_at'], sot.updated_at) + self.assertEqual(EXAMPLE_REQUEST['description'], sot.description) + self.assertEqual(EXAMPLE_REQUEST['key'], sot.key) + self.assertEqual(EXAMPLE_REQUEST['project_id'], sot.project_id) + self.assertEqual(EXAMPLE_REQUEST['status'], sot.status) + self.assertEqual(EXAMPLE_REQUEST['target_project_id'], + sot.target_project_id) + self.assertEqual(EXAMPLE_REQUEST['zone_id'], sot.zone_id) + self.assertEqual(EXAMPLE_REQUEST['zone_name'], sot.zone_name) + + +class TestZoneTransferAccept(base.TestCase): + + def test_basic(self): + sot = zone_transfer.ZoneTransferAccept() + # self.assertEqual('', sot.resource_key) + self.assertEqual('transfer_accepts', sot.resources_key) + self.assertEqual('/zones/tasks/transfer_accepts', sot.base_path) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_delete) + + self.assertDictEqual({'limit': 'limit', + 'marker': 'marker', + 'status': 'status'}, + sot._query_mapping._mapping) + + def test_make_it(self): + sot = zone_transfer.ZoneTransferAccept(**EXAMPLE_ACCEPT) + self.assertEqual(IDENTIFIER, sot.id) + self.assertEqual(EXAMPLE_ACCEPT['created_at'], sot.created_at) + self.assertEqual(EXAMPLE_ACCEPT['updated_at'], sot.updated_at) + self.assertEqual(EXAMPLE_ACCEPT['key'], sot.key) + self.assertEqual(EXAMPLE_ACCEPT['project_id'], sot.project_id) + self.assertEqual(EXAMPLE_ACCEPT['status'], sot.status) + self.assertEqual(EXAMPLE_ACCEPT['zone_id'], sot.zone_id) + self.assertEqual(EXAMPLE_ACCEPT['zone_transfer_request_id'], + sot.zone_transfer_request_id) diff --git a/openstack/tests/unit/test_proxy_base.py b/openstack/tests/unit/test_proxy_base.py index 7b4bf6cdc..e8e1eb834 100644 --- a/openstack/tests/unit/test_proxy_base.py +++ b/openstack/tests/unit/test_proxy_base.py @@ -207,6 +207,7 @@ def verify_list(self, test_method, resource_type, if 'paginated' in kwargs: expected_kwargs.update({"paginated": kwargs.pop('paginated')}) method_kwargs = kwargs.pop("method_kwargs", {}) + expected_kwargs["base_path"] = kwargs.pop("base_path", None) self._verify2(mock_method, test_method, method_kwargs=method_kwargs, expected_args=[resource_type], diff --git a/releasenotes/notes/add-dns-606cc018e01d40fa.yaml b/releasenotes/notes/add-dns-606cc018e01d40fa.yaml new file mode 100644 index 000000000..dcaab35dc --- /dev/null +++ b/releasenotes/notes/add-dns-606cc018e01d40fa.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Adds support for `dns + `_ service. From c9b60f2b8634c7f8c4254e158cc921202538db04 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Sat, 2 Mar 2019 09:04:45 +0100 Subject: [PATCH 2391/3836] Replace TaskManager with a keystoneauth concurrency We've added concurrency and rate-limiting controls to keystoneauth. That means we don't need to do them in openstacksdk. Depends-On: https://review.openstack.org/#/c/640389/ Change-Id: I5d7bd12606785365d2f5b5b52ec7a2316459b68f --- doc/source/user/guides/logging.rst | 8 - lower-constraints.txt | 2 +- openstack/_adapter.py | 33 +-- openstack/cloud/openstackcloud.py | 3 - openstack/config/cloud_region.py | 1 - openstack/connection.py | 22 +- openstack/service_description.py | 5 - openstack/tests/unit/base.py | 1 - .../tests/unit/cloud/test_task_manager.py | 227 ------------------ openstack/tests/unit/test_connection.py | 16 -- requirements.txt | 2 +- 11 files changed, 13 insertions(+), 307 deletions(-) delete mode 100644 openstack/tests/unit/cloud/test_task_manager.py diff --git a/doc/source/user/guides/logging.rst b/doc/source/user/guides/logging.rst index 6eb4da4a5..6c8a27eee 100644 --- a/doc/source/user/guides/logging.rst +++ b/doc/source/user/guides/logging.rst @@ -65,14 +65,6 @@ openstack.config Issues pertaining to configuration are logged to the ``openstack.config`` logger. -openstack.task_manager - `openstacksdk` uses a Task Manager to perform remote calls. The - ``openstack.task_manager`` logger emits messages at the start and end - of each Task announcing what it is going to run and then what it ran and - how long it took. Logging ``openstack.task_manager`` is a good way to - get a trace of external actions `openstacksdk` is taking without full - `HTTP Tracing`_. - openstack.iterate_timeout When `openstacksdk` needs to poll a resource, it does so in a loop that waits between iterations and ultimately times out. The diff --git a/lower-constraints.txt b/lower-constraints.txt index 6eb10512e..99965df90 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -13,7 +13,7 @@ jmespath==0.9.0 jsonpatch==1.16 jsonpointer==1.13 jsonschema==2.6.0 -keystoneauth1==3.11.0 +keystoneauth1==3.13.0 linecache2==1.0.0 mock==2.0.0 mox3==0.20.0 diff --git a/openstack/_adapter.py b/openstack/_adapter.py index 6f9f341e3..5f8d720eb 100644 --- a/openstack/_adapter.py +++ b/openstack/_adapter.py @@ -14,7 +14,6 @@ ''' Wrapper around keystoneauth Adapter to wrap calls in TaskManager ''' -import functools try: import simplejson JSONDecodeError = simplejson.scanner.JSONDecodeError @@ -25,7 +24,6 @@ from keystoneauth1 import adapter from openstack import exceptions -from openstack import task_manager as _task_manager def _extract_name(url, service_type=None): @@ -109,43 +107,22 @@ def _json_response(response, result_key=None, error_message=None): class OpenStackSDKAdapter(adapter.Adapter): - """Wrapper around keystoneauth1.adapter.Adapter. - - Uses task_manager to run tasks rather than executing them directly. - This allows using the nodepool MultiThreaded Rate Limiting TaskManager. - """ + """Wrapper around keystoneauth1.adapter.Adapter.""" def __init__( self, session=None, - task_manager=None, - rate_limit=None, concurrency=None, *args, **kwargs): super(OpenStackSDKAdapter, self).__init__( session=session, *args, **kwargs) - if not task_manager: - task_manager = _task_manager.TaskManager( - name=self.service_type, - rate=rate_limit, - workers=concurrency) - task_manager.start() - - self.task_manager = task_manager def request( self, url, method, error_message=None, raise_exc=False, connect_retries=1, *args, **kwargs): - name_parts = _extract_name(url, self.service_type) - name = '.'.join([self.service_type, method] + name_parts) - - request_method = functools.partial( - super(OpenStackSDKAdapter, self).request, url, method) - - ret = self.task_manager.submit_function( - request_method, run_async=True, name=name, - connect_retries=connect_retries, raise_exc=raise_exc, - tag=self.service_type, + response = super(OpenStackSDKAdapter, self).request( + url, method, + connect_retries=connect_retries, raise_exc=False, **kwargs) - return ret.result() + return response def _version_matches(self, version): api_version = self.get_api_major_version() diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 450ef4d9a..43c7753a2 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -452,7 +452,6 @@ def _get_versioned_client( version=config_major) adapter = _adapter.ShadeAdapter( session=self.session, - task_manager=self.task_manager, service_type=self.config.get_service_type(service_type), service_name=self.config.get_service_name(service_type), interface=self.config.get_interface(service_type), @@ -465,7 +464,6 @@ def _get_versioned_client( adapter = _adapter.ShadeAdapter( session=self.session, - task_manager=self.task_manager, service_type=self.config.get_service_type(service_type), service_name=self.config.get_service_name(service_type), interface=self.config.get_interface(service_type), @@ -504,7 +502,6 @@ def _get_raw_client( self, service_type, api_version=None, endpoint_override=None): return _adapter.ShadeAdapter( session=self.session, - task_manager=self.task_manager, service_type=self.config.get_service_type(service_type), service_name=self.config.get_service_name(service_type), interface=self.config.get_interface(service_type), diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 0d763d96c..e96cabe32 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -492,7 +492,6 @@ def get_session_client( region_name=self.region_name, ) network_endpoint = network_adapter.get_endpoint() - network_adapter.task_manager.stop() if not network_endpoint.rstrip().rsplit('/')[-1] == 'v2.0': if not network_endpoint.endswith('/'): network_endpoint += '/' diff --git a/openstack/connection.py b/openstack/connection.py index 27974fa9d..f208bf4be 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -168,7 +168,6 @@ from openstack.config import cloud_region from openstack import exceptions from openstack import service_description -from openstack import task_manager as _task_manager __all__ = [ 'from_config', @@ -259,10 +258,8 @@ def __init__(self, cloud=None, config=None, session=None, filtering instead of making list calls and filtering client-side. Default false. :param task_manager: - Task Manager to handle the execution of remote REST calls. - Defaults to None which causes a direct-action Task Manager to be - used. - :type manager: :class:`~openstack.task_manager.TaskManager` + Ignored. Exists for backwards compat during transition. Rate limit + parameters should be passed directly to the `rate_limit` parameter. :param rate_limit: Client-side rate limit, expressed in calls per second. The parameter can either be a single float, or it can be a dict with @@ -286,6 +283,7 @@ def __init__(self, cloud=None, config=None, session=None, app_name=app_name, app_version=app_version, load_yaml_config=False, load_envvars=False, + rate_limit=rate_limit, **kwargs) else: self.config = _config.get_cloud_region( @@ -293,18 +291,9 @@ def __init__(self, cloud=None, config=None, session=None, app_name=app_name, app_version=app_version, load_yaml_config=cloud is not None, load_envvars=cloud is not None, + rate_limit=rate_limit, **kwargs) - if task_manager: - # If a TaskManager was passed in, don't start it, assume it's - # under the control of the calling context. - self.task_manager = task_manager - else: - self.task_manager = _task_manager.TaskManager( - self.config.full_name, - rate=rate_limit) - self.task_manager.start() - self._session = None self._proxies = {} self.use_direct_get = use_direct_get @@ -371,7 +360,8 @@ def authorize(self): def close(self): """Release any resources held open.""" - self.task_manager.stop() + if self.__pool_executor: + self.__pool_executor.shutdown() def __enter__(self): return self diff --git a/openstack/service_description.py b/openstack/service_description.py index bab08562b..206affce8 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -108,7 +108,6 @@ def _make_proxy(self, instance): proxy_obj = config.get_session_client( self.service_type, constructor=proxy_class, - task_manager=instance.task_manager, ) else: warnings.warn( @@ -129,7 +128,6 @@ def _make_proxy(self, instance): proxy_obj = config.get_session_client( self.service_type, constructor=proxy_class, - task_manager=instance.task_manager, ) else: warnings.warn( @@ -165,7 +163,6 @@ def _make_proxy(self, instance): proxy_obj = config.get_session_client( self.service_type, constructor=proxy_class, - task_manager=instance.task_manager, ) return proxy_obj @@ -199,14 +196,12 @@ def _make_proxy(self, instance): service_type=self.service_type), category=exceptions.UnsupportedServiceVersion) return temp_adapter - temp_adapter.task_manager.stop() proxy_class = self.supported_versions.get(str(found_version[0])) if not proxy_class: proxy_class = proxy.Proxy return config.get_session_client( self.service_type, constructor=proxy_class, - task_manager=instance.task_manager, allow_version_hack=True, **version_kwargs ) diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index cee34d786..7d508ecd6 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -438,7 +438,6 @@ def _make_test_cloud(self, cloud_name='_test_cloud_', **kwargs): cloud=test_cloud, validate=True, **kwargs) self.cloud = openstack.connection.Connection( config=self.cloud_config, strict=self.strict_cloud) - self.addCleanup(self.cloud.task_manager.stop) def get_cinder_discovery_mock_dict( self, diff --git a/openstack/tests/unit/cloud/test_task_manager.py b/openstack/tests/unit/cloud/test_task_manager.py deleted file mode 100644 index 89fc2dfce..000000000 --- a/openstack/tests/unit/cloud/test_task_manager.py +++ /dev/null @@ -1,227 +0,0 @@ -# Copyright (c) 2015 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. - - -import concurrent.futures -import fixtures -import mock -import threading -import time - -from six.moves import queue - -from openstack import task_manager -from openstack.tests.unit import base - - -class TestException(Exception): - pass - - -class TaskTest(task_manager.Task): - def main(self): - raise TestException("This is a test exception") - - -class TaskTestGenerator(task_manager.Task): - def main(self): - yield 1 - - -class TaskTestInt(task_manager.Task): - def main(self): - return int(1) - - -class TaskTestFloat(task_manager.Task): - def main(self): - return float(2.0) - - -class TaskTestStr(task_manager.Task): - def main(self): - return "test" - - -class TaskTestBool(task_manager.Task): - def main(self): - return True - - -class TaskTestSet(task_manager.Task): - def main(self): - return set([1, 2]) - - -class TestRateTransforms(base.TestCase): - - def test_rate_parameter_scalar(self): - manager = task_manager.TaskManager(name='test', rate=0.1234) - self.assertEqual(1 / 0.1234, manager._get_wait('compute')) - self.assertEqual(1 / 0.1234, manager._get_wait(None)) - - def test_rate_parameter_dict(self): - manager = task_manager.TaskManager( - name='test', - rate={ - 'compute': 20, - 'network': 10, - }) - self.assertEqual(1.0 / 20, manager._get_wait('compute')) - self.assertEqual(1.0 / 10, manager._get_wait('network')) - self.assertIsNone(manager._get_wait('object-store')) - - -class TestTaskManager(base.TestCase): - - def setUp(self): - super(TestTaskManager, self).setUp() - self.manager = task_manager.TaskManager(name='test') - self.manager.start() - - def test_wait_re_raise(self): - """Test that Exceptions thrown in a Task is reraised correctly - - This test is aimed to six.reraise(), called in Task::wait(). - Specifically, we test if we get the same behaviour with all the - configured interpreters (e.g. py27, p35, ...) - """ - self.assertRaises(TestException, self.manager.submit_task, TaskTest()) - - def test_dont_munchify_int(self): - ret = self.manager.submit_task(TaskTestInt()) - self.assertIsInstance(ret, int) - - def test_dont_munchify_float(self): - ret = self.manager.submit_task(TaskTestFloat()) - self.assertIsInstance(ret, float) - - def test_dont_munchify_str(self): - ret = self.manager.submit_task(TaskTestStr()) - self.assertIsInstance(ret, str) - - def test_dont_munchify_bool(self): - ret = self.manager.submit_task(TaskTestBool()) - self.assertIsInstance(ret, bool) - - def test_dont_munchify_set(self): - ret = self.manager.submit_task(TaskTestSet()) - self.assertIsInstance(ret, set) - - @mock.patch.object(concurrent.futures.ThreadPoolExecutor, 'submit') - def test_async(self, mock_submit): - - self.manager.submit_function(set, run_async=True) - self.assertTrue(mock_submit.called) - - @mock.patch.object(task_manager.TaskManager, 'post_run_task') - @mock.patch.object(task_manager.TaskManager, 'pre_run_task') - def test_pre_post_calls(self, mock_pre, mock_post): - self.manager.submit_function(lambda: None) - mock_pre.assert_called_once() - mock_post.assert_called_once() - - @mock.patch.object(task_manager.TaskManager, 'post_run_task') - @mock.patch.object(task_manager.TaskManager, 'pre_run_task') - def test_validate_timing(self, mock_pre, mock_post): - # Note the unit test setup has mocked out time.sleep() and - # done a * 0.0001, and the test should be under the 5 - # second timeout. Thus with below, we should see at - # *least* a 1 second pause running the task. - self.manager.submit_function(lambda: time.sleep(10000)) - - mock_pre.assert_called_once() - mock_post.assert_called_once() - - args, kwargs = mock_post.call_args_list[0] - self.assertTrue(args[0] > 1.0) - - -class ThreadingTaskManager(task_manager.TaskManager): - """A subclass of TaskManager which exercises the thread-shifting - exception handling behavior.""" - - def __init__(self, *args, **kw): - super(ThreadingTaskManager, self).__init__( - *args, **kw) - self.queue = queue.Queue() - self._running = True - self._thread = threading.Thread(name=self.name, target=self.run) - self._thread.daemon = True - self.failed = False - - def start(self): - self._thread.start() - - def stop(self): - self._running = False - self.queue.put(None) - - def join(self): - self._thread.join() - - def run(self): - # No exception should ever cause this method to hit its - # exception handler. - try: - while True: - task = self.queue.get() - if not task: - if not self._running: - break - continue - self.run_task(task) - self.queue.task_done() - except Exception: - self.failed = True - raise - - def submit_task(self, task, raw=False): - # An important part of the exception-shifting feature is that - # this method should raise the exception. - self.queue.put(task) - return task.wait() - - -class ThreadingTaskManagerFixture(fixtures.Fixture): - def _setUp(self): - self.manager = ThreadingTaskManager(name='threading test') - self.manager.start() - self.addCleanup(self._cleanup) - - def _cleanup(self): - self.manager.stop() - self.manager.join() - - -class TestThreadingTaskManager(base.TestCase): - - def setUp(self): - super(TestThreadingTaskManager, self).setUp() - f = self.useFixture(ThreadingTaskManagerFixture()) - self.manager = f.manager - - def test_wait_re_raise(self): - """Test that Exceptions thrown in a Task is reraised correctly - - This test is aimed to six.reraise(), called in Task::wait(). - Specifically, we test if we get the same behaviour with all the - configured interpreters (e.g. py27, p35, ...) - """ - self.assertRaises(TestException, self.manager.submit_task, TaskTest()) - # Stop the manager and join the run thread to ensure the - # exception handler has run. - self.manager.stop() - self.manager.join() - self.assertFalse(self.manager.failed) diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index f27ce67b9..163275fd0 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -91,22 +91,6 @@ def test_session_provided(self): self.assertEqual(mock_session, conn.session) self.assertEqual('auth.example.com', conn.config.name) - def test_task_manager_rate_scalar(self): - conn = connection.Connection(cloud='sample-cloud', rate_limit=20) - self.assertEqual(1.0 / 20, conn.task_manager._get_wait('object-store')) - self.assertEqual(1.0 / 20, conn.task_manager._get_wait(None)) - - def test_task_manager_rate_dict(self): - conn = connection.Connection( - cloud='sample-cloud', - rate_limit={ - 'compute': 20, - 'network': 10, - }) - self.assertEqual(1.0 / 20, conn.task_manager._get_wait('compute')) - self.assertEqual(1.0 / 10, conn.task_manager._get_wait('network')) - self.assertIsNone(conn.task_manager._get_wait('object-store')) - def test_create_session(self): conn = connection.Connection(cloud='sample-cloud') self.assertIsNotNone(conn) diff --git a/requirements.txt b/requirements.txt index 78f758519..6181b5544 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ requestsexceptions>=1.2.0 # Apache-2.0 jsonpatch!=1.20,>=1.16 # BSD six>=1.10.0 # MIT os-service-types>=1.2.0 # Apache-2.0 -keystoneauth1>=3.11.0 # Apache-2.0 +keystoneauth1>=3.13.0 # Apache-2.0 munch>=2.1.0 # MIT decorator>=3.4.0 # BSD From b287655a5790be3172519db19859e4502dfc90a3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 4 Mar 2019 16:29:26 +0000 Subject: [PATCH 2392/3836] Make tox tips job actually run sdk tests When keystoneauth or os-client-config run this job, they'll run their own tests, not openstacksdk's. Whoops. Change-Id: Ia4a421dc4b2e8c3d682ebce2474a436490988363 --- .zuul.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.zuul.yaml b/.zuul.yaml index 03dfb665f..b409ed2b6 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -5,6 +5,7 @@ Run tox python 36 unittests against master of important libs vars: tox_install_siblings: true + zuul_work_dir: src/git.openstack.org/openstack/openstacksdk # openstacksdk in required-projects so that osc and keystoneauth # can add the job as well required-projects: From 371db82c6eafbd4b951b01530183836e2a17f7ed Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Thu, 7 Mar 2019 10:52:44 +0100 Subject: [PATCH 2393/3836] baremetal: support server-side configdrive building (API 1.56) Bare Metal API 1.56 allows passing a dictionary for configdrive. Change-Id: Ib245801ae326754a9ba319d29b0507cd759fdb4c Story: #2005083 Task: #29875 --- openstack/baremetal/v1/node.py | 12 +++-- .../tests/unit/baremetal/v1/test_node.py | 49 ++++++++++++++++++- .../notes/configdrive-f8ca9f94b2981db7.yaml | 5 ++ 3 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/configdrive-f8ca9f94b2981db7.yaml diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 59d3b8785..4a2471401 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -296,13 +296,17 @@ def set_provision_state(self, session, target, config_drive=None, """ session = self._get_session(session) + version = None if target in _common.PROVISIONING_VERSIONS: version = '1.%d' % _common.PROVISIONING_VERSIONS[target] - else: - if config_drive and target == 'rebuild': + + if config_drive: + # Some config drive actions require a higher version. + if isinstance(config_drive, dict): + version = '1.56' + elif target == 'rebuild': version = '1.35' - else: - version = None + version = utils.pick_microversion(session, version) body = {'target': target} diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index e8a7a2f8b..5db11397e 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -225,7 +225,54 @@ def setUp(self): default_microversion=None) def test_no_arguments(self): - self.node.set_provision_state(self.session, 'manage') + result = self.node.set_provision_state(self.session, 'active') + self.assertIs(result, self.node) + self.session.put.assert_called_once_with( + 'nodes/%s/states/provision' % self.node.id, + json={'target': 'active'}, + headers=mock.ANY, microversion=None, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + def test_manage(self): + result = self.node.set_provision_state(self.session, 'manage') + self.assertIs(result, self.node) + self.session.put.assert_called_once_with( + 'nodes/%s/states/provision' % self.node.id, + json={'target': 'manage'}, + headers=mock.ANY, microversion='1.4', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + def test_deploy_with_configdrive(self): + result = self.node.set_provision_state(self.session, 'active', + config_drive='abcd') + self.assertIs(result, self.node) + self.session.put.assert_called_once_with( + 'nodes/%s/states/provision' % self.node.id, + json={'target': 'active', 'configdrive': 'abcd'}, + headers=mock.ANY, microversion=None, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + def test_rebuild_with_configdrive(self): + result = self.node.set_provision_state(self.session, 'rebuild', + config_drive='abcd') + self.assertIs(result, self.node) + self.session.put.assert_called_once_with( + 'nodes/%s/states/provision' % self.node.id, + json={'target': 'rebuild', 'configdrive': 'abcd'}, + headers=mock.ANY, microversion='1.35', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + def test_configdrive_as_dict(self): + for target in ('rebuild', 'active'): + self.session.put.reset_mock() + result = self.node.set_provision_state( + self.session, target, config_drive={'user_data': 'abcd'}) + self.assertIs(result, self.node) + self.session.put.assert_called_once_with( + 'nodes/%s/states/provision' % self.node.id, + json={'target': target, 'configdrive': {'user_data': 'abcd'}}, + headers=mock.ANY, microversion='1.56', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) @mock.patch.object(node.Node, '_translate_response', mock.Mock()) diff --git a/releasenotes/notes/configdrive-f8ca9f94b2981db7.yaml b/releasenotes/notes/configdrive-f8ca9f94b2981db7.yaml new file mode 100644 index 000000000..4337a5424 --- /dev/null +++ b/releasenotes/notes/configdrive-f8ca9f94b2981db7.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Supports Bare Metal API version 1.56, which allows building a config drive + on the server side from a provided dictionary. From 1066c9385fec669a2fc71c5fb59f84743dbf2734 Mon Sep 17 00:00:00 2001 From: niraj singh Date: Thu, 28 Feb 2019 07:59:10 +0000 Subject: [PATCH 2394/3836] Add support to get recovery workflow details In microversion 1.1, Masakari supports to return ``recovery_workflow_details`` information of the notification in ``GET /notifications/{notification_id}`` API. Added ``recovery_workflow_details `` attribute to Notification class to read the recovery_workflow_details of the notification. Change-Id: I639fd38312c88522ac6dc628eb3b798066cca74d --- openstack/instance_ha/v1/notification.py | 25 +++++++++++++ .../unit/instance_ha/v1/test_notification.py | 37 ++++++++++++++++++- ...otification-resource-f7871acb6ffd46dc.yaml | 7 ++++ 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-new-field-progress-details-in-notification-resource-f7871acb6ffd46dc.yaml diff --git a/openstack/instance_ha/v1/notification.py b/openstack/instance_ha/v1/notification.py index 4cf538861..42dabe6ee 100644 --- a/openstack/instance_ha/v1/notification.py +++ b/openstack/instance_ha/v1/notification.py @@ -15,6 +15,27 @@ from openstack import resource +class ProgressDetailsItem(resource.Resource): + #: The timestamp of recovery workflow task. + timestamp = resource.Body("timestamp") + #: The message of recovery workflow task. + message = resource.Body("message") + #: The progress of recovery workflow task. + progress = resource.Body("progress") + + +class RecoveryWorkflowDetailItem(resource.Resource): + #: The progress of recovery workflow. + progress = resource.Body("progress") + #: The name of recovery workflow. + name = resource.Body("name") + #: The state of recovery workflow. + state = resource.Body("state") + #: The progress details of this recovery workflow. + progress_details = resource.Body( + "progress_details", type=list, list_type=ProgressDetailsItem) + + class Notification(resource.Resource): resource_key = "notification" resources_key = "notifications" @@ -56,6 +77,10 @@ class Notification(resource.Resource): payload = resource.Body("payload") #: The source host uuid of this notification. source_host_uuid = resource.Body("source_host_uuid") + #: The recovery workflow details of this notification. + recovery_workflow_details = resource.Body( + "recovery_workflow_details", + type=list, list_type=RecoveryWorkflowDetailItem) _query_mapping = resource.QueryParameters( "sort_key", "sort_dir", source_host_uuid="source_host_uuid", diff --git a/openstack/tests/unit/instance_ha/v1/test_notification.py b/openstack/tests/unit/instance_ha/v1/test_notification.py index c5e94a03f..d4fb269d4 100644 --- a/openstack/tests/unit/instance_ha/v1/test_notification.py +++ b/openstack/tests/unit/instance_ha/v1/test_notification.py @@ -23,6 +23,16 @@ "vir_domain_event": "STOPPED_FAILED", "event": "LIFECYCLE" } + +PROGRESS_DETAILS = [{"timestamp": "2019-02-28 07:21:33.291810", + "progress": 1.0, + "message": "Skipping recovery for process " + "nova-compute as it is already disabled"}] + +RECOVERY_WORKFLOW_DETAILS = [{"progress": 1.0, "state": "SUCCESS", + "name": "DisableComputeNodeTask", + "progress_details": PROGRESS_DETAILS}] + NOTIFICATION = { "id": FAKE_ID, "notification_uuid": FAKE_UUID, @@ -33,7 +43,8 @@ "status": "new", "generated_time": "2018-03-21T00:00:00.000000", "payload": PAYLOAD, - "source_host_uuid": FAKE_HOST_UUID + "source_host_uuid": FAKE_HOST_UUID, + "recovery_workflow_details": RECOVERY_WORKFLOW_DETAILS } @@ -62,6 +73,7 @@ def test_basic(self): def test_create(self): sot = notification.Notification(**NOTIFICATION) + rec_workflow_details = NOTIFICATION["recovery_workflow_details"][0] self.assertEqual(NOTIFICATION["id"], sot.id) self.assertEqual( NOTIFICATION["notification_uuid"], sot.notification_uuid) @@ -74,3 +86,26 @@ def test_create(self): self.assertEqual(NOTIFICATION["payload"], sot.payload) self.assertEqual( NOTIFICATION["source_host_uuid"], sot.source_host_uuid) + self.assertEqual(rec_workflow_details["name"], + sot.recovery_workflow_details[0].name) + self.assertEqual(rec_workflow_details["state"], + sot.recovery_workflow_details[0].state) + self.assertEqual(rec_workflow_details["progress"], + sot.recovery_workflow_details[0].progress) + self.assertEqual( + rec_workflow_details["progress_details"][0]['progress'], + sot.recovery_workflow_details[0].progress_details[0].progress) + self.assertEqual( + rec_workflow_details["progress_details"][0]['message'], + sot.recovery_workflow_details[0].progress_details[0].message) + self.assertEqual( + rec_workflow_details["progress_details"][0]['timestamp'], + sot.recovery_workflow_details[0].progress_details[0].timestamp) + self.assertIsInstance(sot.recovery_workflow_details, list) + self.assertIsInstance( + sot.recovery_workflow_details[0].progress_details, list) + self.assertIsInstance(sot.recovery_workflow_details[0], + notification.RecoveryWorkflowDetailItem) + self.assertIsInstance( + sot.recovery_workflow_details[0].progress_details[0], + notification.ProgressDetailsItem) diff --git a/releasenotes/notes/add-new-field-progress-details-in-notification-resource-f7871acb6ffd46dc.yaml b/releasenotes/notes/add-new-field-progress-details-in-notification-resource-f7871acb6ffd46dc.yaml new file mode 100644 index 000000000..f0470b45d --- /dev/null +++ b/releasenotes/notes/add-new-field-progress-details-in-notification-resource-f7871acb6ffd46dc.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + In microversion 1.1, Masakari returns ``recovery_workflow_details`` information + of the notification in ``GET /notifications/{notification_id}`` API. Added + ``recovery_workflow_details`` attribute to Notification class to read the + recovery_workflow_details of the notification. From baa702566dd9acf717e2307b7ee140e45a16c650 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 8 Mar 2019 18:32:08 +0100 Subject: [PATCH 2395/3836] Move object methods to object_store proxy This is a similar change to https://review.openstack.org/#/c/609684/ with the purpose to streamline the code between SDK/shade and give ability OSC to use the well tested logic. NOTE: This is not yet finished. The proxy methods should be transformed to using the Resource layer. This requires enabling custom meta headers of the object to avoid doing direct PUT and GET bypassing the resource (also doing too much endpoint calculation) Change-Id: I5d288c25797833a850b361af8f067923b00b8706 --- openstack/cloud/_normalize.py | 34 ++ openstack/cloud/openstackcloud.py | 297 ++------------- openstack/object_store/v1/_base.py | 7 +- openstack/object_store/v1/_proxy.py | 339 ++++++++++++++++-- openstack/object_store/v1/container.py | 2 +- openstack/object_store/v1/info.py | 75 ++++ openstack/object_store/v1/obj.py | 25 +- openstack/tests/unit/cloud/test__utils.py | 26 -- openstack/tests/unit/cloud/test_object.py | 5 +- .../tests/unit/object_store/v1/test_proxy.py | 40 ++- 10 files changed, 528 insertions(+), 322 deletions(-) create mode 100644 openstack/object_store/v1/info.py diff --git a/openstack/cloud/_normalize.py b/openstack/cloud/_normalize.py index ea7687fa7..598d982ad 100644 --- a/openstack/cloud/_normalize.py +++ b/openstack/cloud/_normalize.py @@ -1165,3 +1165,37 @@ def _normalize_role(self, role): location=self._get_identity_location(), properties={}, ) + + def _normalize_containers(self, containers): + """Normalize Swift Containers""" + ret = [] + for container in containers: + ret.append(self._normalize_container(container)) + return ret + + def _normalize_container(self, container): + """Normalize Swift Container.""" + + return munch.Munch( + name=container.get('name'), + bytes=container.get('bytes'), + count=container.get('count'), + ) + + def _normalize_objects(self, objects): + """Normalize Swift Objects""" + ret = [] + for object in objects: + ret.append(self._normalize_object(object)) + return ret + + def _normalize_object(self, object): + """Normalize Swift Object.""" + + return munch.Munch( + name=object.get('name'), + bytes=object.get('_bytes'), + content_type=object.get('content_type'), + hash=object.get('_hash'), + last_modified=object.get('_last_modified'), + ) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 43c7753a2..804be74aa 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -11,7 +11,6 @@ # limitations under the License. import base64 -import collections import concurrent.futures import copy import datetime @@ -19,7 +18,6 @@ import hashlib import ipaddress import iso8601 -import json import jsonpatch import operator import os @@ -36,7 +34,6 @@ import munch import requests.models import requestsexceptions -from six.moves import urllib import keystoneauth1.exceptions import keystoneauth1.session @@ -54,9 +51,6 @@ import openstack.config.defaults from openstack import utils -DEFAULT_OBJECT_SEGMENT_SIZE = 1073741824 # 1GB -# This halves the current default for Swift -DEFAULT_MAX_FILE_SIZE = (5 * 1024 * 1024 * 1024 + 2) / 2 DEFAULT_SERVER_AGE = 5 DEFAULT_PORT_AGE = 5 DEFAULT_FLOAT_AGE = 5 @@ -7070,8 +7064,8 @@ def list_containers(self, full_listing=True, prefix=None): :raises: OpenStackCloudException on operation error. """ params = dict(format='json', prefix=prefix) - response = self.object_store.get('/', params=params) - return self._get_and_munchify(None, _adapter._json_response(response)) + data = self.object_store.containers(**params) + return self._normalize_containers(self._get_and_munchify(None, data)) def search_containers(self, name=None, filters=None): """Search containers. @@ -7101,9 +7095,8 @@ def get_container(self, name, skip_cache=False): """ if skip_cache or name not in self._container_cache: try: - response = self.object_store.head(name) - exceptions.raise_from_response(response) - self._container_cache[name] = response.headers + response = self.object_store.get_container_metadata(name) + self._container_cache[name] = response except exc.OpenStackCloudHTTPError as e: if e.response.status_code == 404: return None @@ -7121,7 +7114,7 @@ def create_container(self, name, public=False): container = self.get_container(name) if container: return container - exceptions.raise_from_response(self.object_store.put(name)) + self.object_store.create_container(name=name) if public: self.set_container_access(name, 'public') return self.get_container(name, skip_cache=True) @@ -7132,7 +7125,7 @@ def delete_container(self, name): :param str name: Name of the container to delete. """ try: - exceptions.raise_from_response(self.object_store.delete(name)) + self.object_store.delete_container(name, ignore_missing=False) self._container_cache.pop(name, None) return True except exc.OpenStackCloudHTTPError as e: @@ -7161,8 +7154,11 @@ def update_container(self, name, headers): :param dict headers: Key/Value headers to set on the container. """ - exceptions.raise_from_response( - self.object_store.post(name, headers=headers)) + self.object_store.set_container_metadata( + name, + refresh=False, + **headers + ) def set_container_access(self, name, access): """Set the access control list on a container. @@ -7178,8 +7174,10 @@ def set_container_access(self, name, access): raise exc.OpenStackCloudException( "Invalid container access specified: %s. Must be one of %s" % (access, list(OBJECT_CONTAINER_ACLS.keys()))) - header = {'x-container-read': OBJECT_CONTAINER_ACLS[access]} - self.update_container(name, header) + self.object_store.set_container_metadata( + name, + refresh=False, + read_ACL=OBJECT_CONTAINER_ACLS[access]) def get_container_access(self, name): """Get the control list from a container. @@ -7189,7 +7187,7 @@ def get_container_access(self, name): container = self.get_container(name, skip_cache=True) if not container: raise exc.OpenStackCloudException("Container not found: %s" % name) - acl = container.get('x-container-read', '') + acl = container.read_ACL or '' for key, value in OBJECT_CONTAINER_ACLS.items(): # Convert to string for the comparison because swiftclient # returns byte values as bytes sometimes and apparently == @@ -7229,43 +7227,11 @@ def get_object_capabilities(self): The object-storage service publishes a set of capabilities that include metadata about maximum values and thresholds. """ - # The endpoint in the catalog has version and project-id in it - # To get capabilities, we have to disassemble and reassemble the URL - # This logic is taken from swiftclient - endpoint = urllib.parse.urlparse(self.object_store.get_endpoint()) - url = "{scheme}://{netloc}/info".format( - scheme=endpoint.scheme, netloc=endpoint.netloc) - - return _adapter._json_response(self.object_store.get(url)) + return self.object_store.get_info() def get_object_segment_size(self, segment_size): """Get a segment size that will work given capabilities""" - if segment_size is None: - segment_size = DEFAULT_OBJECT_SEGMENT_SIZE - min_segment_size = 0 - try: - caps = self.get_object_capabilities() - except exc.OpenStackCloudHTTPError as e: - if e.response.status_code in (404, 412): - # Clear the exception so that it doesn't linger - # and get reported as an Inner Exception later - _utils._exc_clear() - server_max_file_size = DEFAULT_MAX_FILE_SIZE - self.log.info( - "Swift capabilities not supported. " - "Using default max file size.") - else: - raise - else: - server_max_file_size = caps.get('swift', {}).get('max_file_size', - 0) - min_segment_size = caps.get('slo', {}).get('min_segment_size', 0) - - if segment_size > server_max_file_size: - return server_max_file_size - if segment_size < min_segment_size: - return min_segment_size - return segment_size + return self.object_store.get_object_segment_size(segment_size) def is_object_stale( self, container, name, filename, file_md5=None, file_sha256=None): @@ -7281,35 +7247,8 @@ def is_object_stale( Pre-calculated sha256 of the file contents. Defaults to None which means calculate locally. """ - metadata = self.get_object_metadata(container, name) - if not metadata: - self.log.debug( - "swift stale check, no object: {container}/{name}".format( - container=container, name=name)) - return True - - if not (file_md5 or file_sha256): - (file_md5, file_sha256) = self._get_file_hashes(filename) - md5_key = metadata.get( - self._OBJECT_MD5_KEY, metadata.get(self._SHADE_OBJECT_MD5_KEY, '')) - sha256_key = metadata.get( - self._OBJECT_SHA256_KEY, metadata.get( - self._SHADE_OBJECT_SHA256_KEY, '')) - up_to_date = self._hashes_up_to_date( - md5=file_md5, sha256=file_sha256, - md5_key=md5_key, sha256_key=sha256_key) - - if not up_to_date: - self.log.debug( - "swift checksum mismatch: " - " %(filename)s!=%(container)s/%(name)s", - {'filename': filename, 'container': container, 'name': name}) - return True - - self.log.debug( - "swift object up to date: %(container)s/%(name)s", - {'container': container, 'name': name}) - return False + return self.object_store.is_object_stale(container, name, filename, + file_md5, file_sha256) def create_directory_marker_object(self, container, name, **headers): """Create a zero-byte directory marker object @@ -7379,105 +7318,12 @@ def create_object( :raises: ``OpenStackCloudException`` on operation error. """ - if data is not None and filename: - raise ValueError( - "Both filename and data given. Please choose one.") - if data is not None and not name: - raise ValueError( - "name is a required parameter when data is given") - if data is not None and generate_checksums: - raise ValueError( - "checksums cannot be generated with data parameter") - if generate_checksums is None: - if data is not None: - generate_checksums = False - else: - generate_checksums = True - - if not metadata: - metadata = {} - - if not filename and data is None: - filename = name - - if generate_checksums and (md5 is None or sha256 is None): - (md5, sha256) = self._get_file_hashes(filename) - if md5: - headers[self._OBJECT_MD5_KEY] = md5 or '' - if sha256: - headers[self._OBJECT_SHA256_KEY] = sha256 or '' - for (k, v) in metadata.items(): - headers['x-object-meta-' + k] = v - - endpoint = '{container}/{name}'.format(container=container, name=name) - - if data is not None: - self.log.debug( - "swift uploading data to %(endpoint)s", - {'endpoint': endpoint}) - - return self._upload_object_data(endpoint, data, headers) - - # segment_size gets used as a step value in a range call, so needs - # to be an int - if segment_size: - segment_size = int(segment_size) - segment_size = self.get_object_segment_size(segment_size) - file_size = os.path.getsize(filename) - - if self.is_object_stale(container, name, filename, md5, sha256): - - self.log.debug( - "swift uploading %(filename)s to %(endpoint)s", - {'filename': filename, 'endpoint': endpoint}) - - if file_size <= segment_size: - self._upload_object(endpoint, filename, headers) - else: - self._upload_large_object( - endpoint, filename, headers, - file_size, segment_size, use_slo) - - def _upload_object_data(self, endpoint, data, headers): - return _adapter._json_response(self.object_store.put( - endpoint, headers=headers, data=data)) - - def _upload_object(self, endpoint, filename, headers): - return _adapter._json_response(self.object_store.put( - endpoint, headers=headers, data=open(filename, 'rb'))) - - def _get_file_segments(self, endpoint, filename, file_size, segment_size): - # Use an ordered dict here so that testing can replicate things - segments = collections.OrderedDict() - for (index, offset) in enumerate(range(0, file_size, segment_size)): - remaining = file_size - (index * segment_size) - segment = _utils.FileSegment( - filename, offset, - segment_size if segment_size < remaining else remaining) - name = '{endpoint}/{index:0>6}'.format( - endpoint=endpoint, index=index) - segments[name] = segment - return segments - - def _object_name_from_url(self, url): - '''Get container_name/object_name from the full URL called. - - Remove the Swift endpoint from the front of the URL, and remove - the leaving / that will leave behind.''' - endpoint = self.object_store.get_endpoint() - object_name = url.replace(endpoint, '') - if object_name.startswith('/'): - object_name = object_name[1:] - return object_name - - def _add_etag_to_manifest(self, segment_results, manifest): - for result in segment_results: - if 'Etag' not in result.headers: - continue - name = self._object_name_from_url(result.url) - for entry in manifest: - if entry['path'] == '/{name}'.format(name=name): - entry['etag'] = result.headers['Etag'] + return self.object_store.create_object( + container, name, + filename=filename, md5=md5, sha256=sha256, + segment_size=segment_size, use_slo=use_slo, metadata=metadata, + generate_checksums=generate_checksums, data=data, + **headers) @property def _pool_executor(self): @@ -7515,84 +7361,6 @@ def _wait_for_futures(self, futures, raise_on_error=True): retries.append(completed.result()) return results, retries - def _upload_large_object( - self, endpoint, filename, - headers, file_size, segment_size, use_slo): - # If the object is big, we need to break it up into segments that - # are no larger than segment_size, upload each of them individually - # and then upload a manifest object. The segments can be uploaded in - # parallel, so we'll use the async feature of the TaskManager. - - segment_futures = [] - segment_results = [] - retry_results = [] - retry_futures = [] - manifest = [] - - # Get an OrderedDict with keys being the swift location for the - # segment, the value a FileSegment file-like object that is a - # slice of the data for the segment. - segments = self._get_file_segments( - endpoint, filename, file_size, segment_size) - - # Schedule the segments for upload - for name, segment in segments.items(): - # Async call to put - schedules execution and returns a future - segment_future = self._pool_executor.submit( - self.object_store.put, - name, headers=headers, data=segment, - raise_exc=False) - segment_futures.append(segment_future) - # TODO(mordred) Collect etags from results to add to this manifest - # dict. Then sort the list of dicts by path. - manifest.append(dict( - path='/{name}'.format(name=name), - size_bytes=segment.length)) - - # Try once and collect failed results to retry - segment_results, retry_results = self._wait_for_futures( - segment_futures, raise_on_error=False) - - self._add_etag_to_manifest(segment_results, manifest) - - for result in retry_results: - # Grab the FileSegment for the failed upload so we can retry - name = self._object_name_from_url(result.url) - segment = segments[name] - segment.seek(0) - # Async call to put - schedules execution and returns a future - segment_future = self._pool_executor.submit( - self.object_store.put, - name, headers=headers, data=segment) - # TODO(mordred) Collect etags from results to add to this manifest - # dict. Then sort the list of dicts by path. - retry_futures.append(segment_future) - - # If any segments fail the second time, just throw the error - segment_results, retry_results = self._wait_for_futures( - retry_futures, raise_on_error=True) - - self._add_etag_to_manifest(segment_results, manifest) - - if use_slo: - return self._finish_large_object_slo(endpoint, headers, manifest) - else: - return self._finish_large_object_dlo(endpoint, headers) - - def _finish_large_object_slo(self, endpoint, headers, manifest): - # TODO(mordred) send an etag of the manifest, which is the md5sum - # of the concatenation of the etags of the results - headers = headers.copy() - return self._object_store_client.put( - endpoint, - params={'multipart-manifest': 'put'}, - headers=headers, data=json.dumps(manifest)) - - def _finish_large_object_dlo(self, endpoint, headers): - headers = headers.copy() - headers['X-Object-Manifest'] = endpoint - return self._object_store_client.put(endpoint, headers=headers) - def update_object(self, container, name, metadata=None, **headers): """Update the metadata of an object @@ -7633,9 +7401,8 @@ def list_objects(self, container, full_listing=True, prefix=None): :raises: OpenStackCloudException on operation error. """ - params = dict(format='json', prefix=prefix) - data = self._object_store_client.get(container, params=params) - return self._get_and_munchify(None, data) + data = self.object_store.objects(container, prefix=prefix) + return self._normalize_objects(self._get_and_munchify(None, data)) def search_objects(self, container, name=None, filters=None): """Search objects. @@ -7765,10 +7532,10 @@ def stream_object( :raises: OpenStackCloudException on operation error. """ try: - with self.get_object_raw( - container, obj, query_string=query_string) as response: - for ret in response.iter_content(chunk_size=resp_chunk_size): - yield ret + for ret in self.object_store.stream_object( + obj, container=container, + chunk_size=resp_chunk_size): + yield ret except exc.OpenStackCloudHTTPError as e: if e.response.status_code == 404: return diff --git a/openstack/object_store/v1/_base.py b/openstack/object_store/v1/_base.py index 21372406f..779f1e92d 100644 --- a/openstack/object_store/v1/_base.py +++ b/openstack/object_store/v1/_base.py @@ -41,14 +41,15 @@ def _calculate_headers(self, metadata): headers[header] = metadata[key] return headers - def set_metadata(self, session, metadata): + def set_metadata(self, session, metadata, refresh=True): request = self._prepare_request() response = session.post( request.url, headers=self._calculate_headers(metadata)) self._translate_response(response, has_body=False) - response = session.head(request.url) - self._translate_response(response, has_body=False) + if refresh: + response = session.head(request.url) + self._translate_response(response, has_body=False) return self def delete_metadata(self, session, keys): diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index 629a5f733..5221b303d 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -10,10 +10,22 @@ # License for the specific language governing permissions and limitations # under the License. +import collections +import os +import json + from openstack.object_store.v1 import account as _account from openstack.object_store.v1 import container as _container from openstack.object_store.v1 import obj as _obj +from openstack.object_store.v1 import info as _info +from openstack import _adapter +from openstack import exceptions +from openstack import _log from openstack import proxy +from openstack.cloud import _utils + +DEFAULT_OBJECT_SEGMENT_SIZE = 1073741824 # 1GB +DEFAULT_MAX_FILE_SIZE = (5 * 1024 * 1024 * 1024 + 2) / 2 class Proxy(proxy.Proxy): @@ -24,6 +36,8 @@ class Proxy(proxy.Proxy): Container = _container.Container Object = _obj.Object + log = _log.setup_logging('openstack') + def get_account_metadata(self): """Get metadata for this account. @@ -105,12 +119,13 @@ def get_container_metadata(self, container): """ return self._head(_container.Container, container) - def set_container_metadata(self, container, **metadata): + def set_container_metadata(self, container, refresh=True, **metadata): """Set metadata for a container. :param container: The value can be the name of a container or a :class:`~openstack.object_store.v1.container.Container` instance. + :param refresh: Flag to trigger refresh of container object re-fetch. :param kwargs metadata: Key/value pairs to be set as metadata on the container. Both custom and system metadata can be set. Custom metadata are keys @@ -128,7 +143,7 @@ def set_container_metadata(self, container, **metadata): - `sync_key` """ res = self._get_resource(_container.Container, container) - res.set_metadata(self, metadata) + res.set_metadata(self, metadata, refresh=refresh) return res def delete_container_metadata(self, container, keys): @@ -160,7 +175,7 @@ def objects(self, container, **query): for obj in self._list( _obj.Object, container=container, - paginated=True, **query): + paginated=True, format='json', **query): obj.container = container yield obj @@ -232,25 +247,110 @@ def stream_object(self, obj, container=None, chunk_size=1024, **attrs): _obj.Object, obj, container=container_name, **attrs) return obj.stream(self, chunk_size=chunk_size) - def create_object(self, container, name, **attrs): - """Upload a new object from attributes - - :param container: The value can be the name of a container or a - :class:`~openstack.object_store.v1.container.Container` - instance. - :param name: Name of the object to create. - :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.object_store.v1.obj.Object`, - comprised of the properties on the Object class. - - :returns: The results of object creation - :rtype: :class:`~openstack.object_store.v1.container.Container` + def create_object( + self, container, name, filename=None, + md5=None, sha256=None, segment_size=None, + use_slo=True, metadata=None, + generate_checksums=None, data=None, + **headers): + """Create a file object. + + Automatically uses large-object segments if needed. + + :param container: The name of the container to store the file in. + This container will be created if it does not exist already. + :param name: Name for the object within the container. + :param filename: The path to the local file whose contents will be + uploaded. Mutually exclusive with data. + :param data: The content to upload to the object. Mutually exclusive + with filename. + :param md5: A hexadecimal md5 of the file. (Optional), if it is known + and can be passed here, it will save repeating the expensive md5 + process. It is assumed to be accurate. + :param sha256: A hexadecimal sha256 of the file. (Optional) See md5. + :param segment_size: Break the uploaded object into segments of this + many bytes. (Optional) SDK will attempt to discover the maximum + value for this from the server if it is not specified, or will use + a reasonable default. + :param headers: These will be passed through to the object creation + API as HTTP Headers. + :param use_slo: If the object is large enough to need to be a Large + Object, use a static rather than dynamic object. Static Objects + will delete segment objects when the manifest object is deleted. + (optional, defaults to True) + :param generate_checksums: Whether to generate checksums on the client + side that get added to headers for later prevention of double + uploads of identical data. (optional, defaults to True) + :param metadata: This dict will get changed into headers that set + metadata of the object + + :raises: ``OpenStackCloudException`` on operation error. """ - # TODO(mordred) Add ability to stream data from a file - # TODO(mordred) Use create_object from OpenStackCloud + if data is not None and filename: + raise ValueError( + "Both filename and data given. Please choose one.") + if data is not None and not name: + raise ValueError( + "name is a required parameter when data is given") + if data is not None and generate_checksums: + raise ValueError( + "checksums cannot be generated with data parameter") + if generate_checksums is None: + if data is not None: + generate_checksums = False + else: + generate_checksums = True + + if not metadata: + metadata = {} + + if not filename and data is None: + filename = name + + if generate_checksums and (md5 is None or sha256 is None): + (md5, sha256) = self._connection._get_file_hashes(filename) + if md5: + headers[self._connection._OBJECT_MD5_KEY] = md5 or '' + if sha256: + headers[self._connection._OBJECT_SHA256_KEY] = sha256 or '' + for (k, v) in metadata.items(): + headers['x-object-meta-' + k] = v + container_name = self._get_container_name(container=container) - return self._create( - _obj.Object, container=container_name, name=name, **attrs) + endpoint = '{container}/{name}'.format(container=container_name, + name=name) + + if data is not None: + self.log.debug( + "swift uploading data to %(endpoint)s", + {'endpoint': endpoint}) + # TODO(gtema): custom headers need to be somehow injected + return self._create( + _obj.Object, container=container_name, + name=name, data=data, **headers) + + # segment_size gets used as a step value in a range call, so needs + # to be an int + if segment_size: + segment_size = int(segment_size) + segment_size = self.get_object_segment_size(segment_size) + file_size = os.path.getsize(filename) + + if self.is_object_stale(container_name, name, filename, md5, sha256): + + self._connection.log.debug( + "swift uploading %(filename)s to %(endpoint)s", + {'filename': filename, 'endpoint': endpoint}) + + if file_size <= segment_size: + # TODO(gtema): replace with regular resource put, but + # custom headers need to be somehow injected + self._upload_object(endpoint, filename, headers) + else: + self._upload_large_object( + endpoint, filename, headers, + file_size, segment_size, use_slo) + # Backwards compat upload_object = create_object @@ -341,3 +441,202 @@ def delete_object_metadata(self, obj, container=None, keys=None): res = self._get_resource(_obj.Object, obj, container=container_name) res.delete_metadata(self, keys) return res + + def is_object_stale( + self, container, name, filename, file_md5=None, file_sha256=None): + """Check to see if an object matches the hashes of a file. + + :param container: Name of the container. + :param name: Name of the object. + :param filename: Path to the file. + :param file_md5: + Pre-calculated md5 of the file contents. Defaults to None which + means calculate locally. + :param file_sha256: + Pre-calculated sha256 of the file contents. Defaults to None which + means calculate locally. + """ + metadata = self._connection.get_object_metadata(container, name) + if not metadata: + self._connection.log.debug( + "swift stale check, no object: {container}/{name}".format( + container=container, name=name)) + return True + + if not (file_md5 or file_sha256): + (file_md5, file_sha256) = \ + self._connection._get_file_hashes(filename) + md5_key = metadata.get( + self._connection._OBJECT_MD5_KEY, + metadata.get(self._connection._SHADE_OBJECT_MD5_KEY, '')) + sha256_key = metadata.get( + self._connection._OBJECT_SHA256_KEY, metadata.get( + self._connection._SHADE_OBJECT_SHA256_KEY, '')) + up_to_date = self._connection._hashes_up_to_date( + md5=file_md5, sha256=file_sha256, + md5_key=md5_key, sha256_key=sha256_key) + + if not up_to_date: + self._connection.log.debug( + "swift checksum mismatch: " + " %(filename)s!=%(container)s/%(name)s", + {'filename': filename, 'container': container, 'name': name}) + return True + + self._connection.log.debug( + "swift object up to date: %(container)s/%(name)s", + {'container': container, 'name': name}) + return False + + def _upload_large_object( + self, endpoint, filename, + headers, file_size, segment_size, use_slo): + # If the object is big, we need to break it up into segments that + # are no larger than segment_size, upload each of them individually + # and then upload a manifest object. The segments can be uploaded in + # parallel, so we'll use the async feature of the TaskManager. + + segment_futures = [] + segment_results = [] + retry_results = [] + retry_futures = [] + manifest = [] + + # Get an OrderedDict with keys being the swift location for the + # segment, the value a FileSegment file-like object that is a + # slice of the data for the segment. + segments = self._get_file_segments( + endpoint, filename, file_size, segment_size) + + # Schedule the segments for upload + for name, segment in segments.items(): + # Async call to put - schedules execution and returns a future + segment_future = self._connection._pool_executor.submit( + self.put, + name, headers=headers, data=segment, + raise_exc=False) + segment_futures.append(segment_future) + # TODO(mordred) Collect etags from results to add to this manifest + # dict. Then sort the list of dicts by path. + manifest.append(dict( + path='/{name}'.format(name=name), + size_bytes=segment.length)) + + # Try once and collect failed results to retry + segment_results, retry_results = self._connection._wait_for_futures( + segment_futures, raise_on_error=False) + + self._add_etag_to_manifest(segment_results, manifest) + + for result in retry_results: + # Grab the FileSegment for the failed upload so we can retry + name = self._object_name_from_url(result.url) + segment = segments[name] + segment.seek(0) + # Async call to put - schedules execution and returns a future + segment_future = self._connection._pool_executor.submit( + self.put, + name, headers=headers, data=segment) + # TODO(mordred) Collect etags from results to add to this manifest + # dict. Then sort the list of dicts by path. + retry_futures.append(segment_future) + + # If any segments fail the second time, just throw the error + segment_results, retry_results = self._connection._wait_for_futures( + retry_futures, raise_on_error=True) + + self._add_etag_to_manifest(segment_results, manifest) + + if use_slo: + return self._finish_large_object_slo(endpoint, headers, manifest) + else: + return self._finish_large_object_dlo(endpoint, headers) + + def _finish_large_object_slo(self, endpoint, headers, manifest): + # TODO(mordred) send an etag of the manifest, which is the md5sum + # of the concatenation of the etags of the results + headers = headers.copy() + return self.put( + endpoint, + params={'multipart-manifest': 'put'}, + headers=headers, data=json.dumps(manifest)) + + def _finish_large_object_dlo(self, endpoint, headers): + headers = headers.copy() + headers['X-Object-Manifest'] = endpoint + return self.put(endpoint, headers=headers) + + def _upload_object(self, endpoint, filename, headers): + with open(filename, 'rb') as dt: + return _adapter._json_response(self.put( + endpoint, headers=headers, data=dt)) + + def _get_file_segments(self, endpoint, filename, file_size, segment_size): + # Use an ordered dict here so that testing can replicate things + segments = collections.OrderedDict() + for (index, offset) in enumerate(range(0, file_size, segment_size)): + remaining = file_size - (index * segment_size) + segment = _utils.FileSegment( + filename, offset, + segment_size if segment_size < remaining else remaining) + name = '{endpoint}/{index:0>6}'.format( + endpoint=endpoint, index=index) + segments[name] = segment + return segments + + def get_object_segment_size(self, segment_size): + """Get a segment size that will work given capabilities""" + if segment_size is None: + segment_size = DEFAULT_OBJECT_SEGMENT_SIZE + min_segment_size = 0 + try: + # caps = self.get_object_capabilities() + caps = self.get_info() + except exceptions.SDKException as e: + if e.response.status_code in (404, 412): + # Clear the exception so that it doesn't linger + # and get reported as an Inner Exception later + _utils._exc_clear() + server_max_file_size = DEFAULT_MAX_FILE_SIZE + self._connection.log.info( + "Swift capabilities not supported. " + "Using default max file size.") + else: + raise + else: + server_max_file_size = caps.swift.get('max_file_size', 0) + min_segment_size = caps.slo.get('min_segment_size', 0) + + if segment_size > server_max_file_size: + return server_max_file_size + if segment_size < min_segment_size: + return min_segment_size + return segment_size + + def _object_name_from_url(self, url): + '''Get container_name/object_name from the full URL called. + + Remove the Swift endpoint from the front of the URL, and remove + the leaving / that will leave behind.''' + endpoint = self.get_endpoint() + object_name = url.replace(endpoint, '') + if object_name.startswith('/'): + object_name = object_name[1:] + return object_name + + def _add_etag_to_manifest(self, segment_results, manifest): + for result in segment_results: + if 'Etag' not in result.headers: + continue + name = self._object_name_from_url(result.url) + for entry in manifest: + if entry['path'] == '/{name}'.format(name=name): + entry['etag'] = result.headers['Etag'] + + def get_info(self): + """Get infomation about the object-storage service + + The object-storage service publishes a set of capabilities that + include metadata about maximum values and thresholds. + """ + return self._get(_info.Info) diff --git a/openstack/object_store/v1/container.py b/openstack/object_store/v1/container.py index 04c0d45e4..1275b4a54 100644 --- a/openstack/object_store/v1/container.py +++ b/openstack/object_store/v1/container.py @@ -38,7 +38,7 @@ class Container(_base.BaseResource): allow_head = True _query_mapping = resource.QueryParameters( - 'prefix', + 'prefix', 'format' ) # Container body data (when id=None) diff --git a/openstack/object_store/v1/info.py b/openstack/object_store/v1/info.py new file mode 100644 index 000000000..f5fbde39b --- /dev/null +++ b/openstack/object_store/v1/info.py @@ -0,0 +1,75 @@ +# 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 import resource + +from six.moves import urllib + + +class Info(resource.Resource): + + base_path = "/info" + + allow_fetch = True + + _query_mapping = resource.QueryParameters( + 'swiftinfo_sig', 'swiftinfo_expires' + ) + + # Properties + swift = resource.Body("swift", type=dict) + slo = resource.Body("slo", type=dict) + staticweb = resource.Body("staticweb", type=dict) + tempurl = resource.Body("tempurl", type=dict) + + def fetch(self, session, requires_id=False, + base_path=None, error_message=None): + """Get a remote resource based on this instance. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param boolean requires_id: A boolean indicating whether resource ID + should be part of the requested URI. + :param str base_path: Base part of the URI for fetching resources, if + different from + :data:`~openstack.resource.Resource.base_path`. + :param str error_message: An Error message to be returned if + requested object does not exist. + :return: This :class:`Resource` instance. + :raises: :exc:`~openstack.exceptions.MethodNotSupported` if + :data:`Resource.allow_fetch` is not set to ``True``. + :raises: :exc:`~openstack.exceptions.ResourceNotFound` if + the resource was not found. + """ + if not self.allow_fetch: + raise exceptions.MethodNotSupported(self, "fetch") + + # The endpoint in the catalog has version and project-id in it + # To get capabilities, we have to disassemble and reassemble the URL + # This logic is taken from swiftclient + + session = self._get_session(session) + endpoint = urllib.parse.urlparse(session.get_endpoint()) + url = "{scheme}://{netloc}/info".format( + scheme=endpoint.scheme, netloc=endpoint.netloc) + + microversion = self._get_microversion_for(session, 'fetch') + response = session.get(url, microversion=microversion) + kwargs = {} + if error_message: + kwargs['error_message'] = error_message + + self.microversion = microversion + self._translate_response(response, **kwargs) + return self diff --git a/openstack/object_store/v1/obj.py b/openstack/object_store/v1/obj.py index 2ef16529f..b2c6d8bf0 100644 --- a/openstack/object_store/v1/obj.py +++ b/openstack/object_store/v1/obj.py @@ -40,7 +40,7 @@ class Object(_base.BaseResource): allow_head = True _query_mapping = resource.QueryParameters( - 'prefix', + 'prefix', 'format' ) # Data to be passed during a POST call to create an object on the server. @@ -295,3 +295,26 @@ def create(self, session, base_path=None): headers=request.headers) self._translate_response(response, has_body=False) return self + + def _raw_delete(self, session): + if not self.allow_delete: + raise exceptions.MethodNotSupported(self, "delete") + + request = self._prepare_request() + session = self._get_session(session) + microversion = self._get_microversion_for(session, 'delete') + + if self.is_static_large_object is None: + # Fetch metadata to determine SLO flag + self.head(session) + + headers = { + 'Accept': "" + } + if self.is_static_large_object: + headers['multipart-manifest'] = 'delete' + + return session.delete( + request.url, + headers=headers, + microversion=microversion) diff --git a/openstack/tests/unit/cloud/test__utils.py b/openstack/tests/unit/cloud/test__utils.py index bcb04dc12..2aea81158 100644 --- a/openstack/tests/unit/cloud/test__utils.py +++ b/openstack/tests/unit/cloud/test__utils.py @@ -12,9 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. -import random -import string -import tempfile from uuid import uuid4 import mock @@ -298,29 +295,6 @@ def test_range_filter_invalid_op(self): ): _utils.range_filter(RANGE_DATA, "key1", "<>100") - def test_file_segment(self): - file_size = 4200 - content = ''.join(random.SystemRandom().choice( - string.ascii_uppercase + string.digits) - for _ in range(file_size)).encode('latin-1') - self.imagefile = tempfile.NamedTemporaryFile(delete=False) - self.imagefile.write(content) - self.imagefile.close() - - segments = self.cloud._get_file_segments( - endpoint='test_container/test_image', - filename=self.imagefile.name, - file_size=file_size, - segment_size=1000) - self.assertEqual(len(segments), 5) - segment_content = b'' - for (index, (name, segment)) in enumerate(segments.items()): - self.assertEqual( - 'test_container/test_image/{index:0>6}'.format(index=index), - name) - segment_content += segment.read() - self.assertEqual(content, segment_content) - def test_get_entity_pass_object(self): obj = mock.Mock(id=uuid4().hex) self.cloud.use_direct_get = True diff --git a/openstack/tests/unit/cloud/test_object.py b/openstack/tests/unit/cloud/test_object.py index 90654547c..6eea2eb3d 100644 --- a/openstack/tests/unit/cloud/test_object.py +++ b/openstack/tests/unit/cloud/test_object.py @@ -20,6 +20,7 @@ import openstack.cloud.openstackcloud as oc_oc from openstack.cloud import exc from openstack.tests.unit import base +from openstack.object_store.v1 import _proxy class BaseTestObject(base.TestCase): @@ -443,7 +444,7 @@ def test_get_object_segment_size_http_404(self): self.register_uris([ dict(method='GET', uri='https://object-store.example.com/info', status_code=404, reason='Not Found')]) - self.assertEqual(oc_oc.DEFAULT_OBJECT_SEGMENT_SIZE, + self.assertEqual(_proxy.DEFAULT_OBJECT_SEGMENT_SIZE, self.cloud.get_object_segment_size(None)) self.assert_calls() @@ -452,7 +453,7 @@ def test_get_object_segment_size_http_412(self): dict(method='GET', uri='https://object-store.example.com/info', status_code=412, reason='Precondition failed')]) self.assertEqual( - oc_oc.DEFAULT_OBJECT_SEGMENT_SIZE, + _proxy.DEFAULT_OBJECT_SEGMENT_SIZE, self.cloud.get_object_segment_size(None)) self.assert_calls() diff --git a/openstack/tests/unit/object_store/v1/test_proxy.py b/openstack/tests/unit/object_store/v1/test_proxy.py index 8c56328ec..4d4a2ba48 100644 --- a/openstack/tests/unit/object_store/v1/test_proxy.py +++ b/openstack/tests/unit/object_store/v1/test_proxy.py @@ -10,17 +10,20 @@ # License for the specific language governing permissions and limitations # under the License. +import random import six +import string +import tempfile from openstack.object_store.v1 import _proxy from openstack.object_store.v1 import account from openstack.object_store.v1 import container from openstack.object_store.v1 import obj from openstack.tests.unit.cloud import test_object as base_test_object -from openstack.tests.unit import test_proxy_base2 +from openstack.tests.unit import test_proxy_base -class TestObjectStoreProxy(test_proxy_base2.TestProxyBase): +class TestObjectStoreProxy(test_proxy_base.TestProxyBase): kwargs_to_path_args = False @@ -51,8 +54,12 @@ def test_container_create_attrs(self): expected_kwargs={'name': 'container_name', "x": 1, "y": 2, "z": 3}) def test_object_metadata_get(self): - self.verify_head(self.proxy.get_object_metadata, obj.Object, - value="object", container="container") + self._verify2("openstack.proxy.Proxy._head", + self.proxy.get_object_metadata, + method_args=['object'], + method_kwargs={'container': 'container'}, + expected_args=[obj.Object, 'object'], + expected_kwargs={'container': 'container'}) def _test_object_delete(self, ignore): expected_kwargs = { @@ -303,3 +310,28 @@ class Test_copy_object(TestObjectStoreProxy): def test_copy_object(self): self.assertRaises(NotImplementedError, self.proxy.copy_object) + + +class Test_utils(TestObjectStoreProxy): + def test_file_segment(self): + file_size = 4200 + content = ''.join(random.SystemRandom().choice( + string.ascii_uppercase + string.digits) + for _ in range(file_size)).encode('latin-1') + self.imagefile = tempfile.NamedTemporaryFile(delete=False) + self.imagefile.write(content) + self.imagefile.close() + + segments = self.proxy._get_file_segments( + endpoint='test_container/test_image', + filename=self.imagefile.name, + file_size=file_size, + segment_size=1000) + self.assertEqual(len(segments), 5) + segment_content = b'' + for (index, (name, segment)) in enumerate(segments.items()): + self.assertEqual( + 'test_container/test_image/{index:0>6}'.format(index=index), + name) + segment_content += segment.read() + self.assertEqual(content, segment_content) From 0eaf2c95d31a1608000f23464d907d37f668c220 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Sat, 9 Mar 2019 11:12:38 +0100 Subject: [PATCH 2396/3836] Deprecate VolumeDetail and BackupDetail classes We have introduced altering base_path for operations on resources. Volumes and Backups were not switched to this new method and were still relying on returning inherited classes VolumeDetail and BackupDetail. This change allow doing any supported operation on a resource returned by list immediately without any conversion or re-fetch. Change-Id: Ia095c53f1d04f1c119c6a6f8a38c5bfd60dc8a67 --- .../resources/block_storage/v2/backup.rst | 9 --- .../resources/block_storage/v2/volume.rst | 9 --- openstack/block_storage/v2/_proxy.py | 24 +++----- openstack/block_storage/v2/backup.py | 9 +-- openstack/block_storage/v2/volume.py | 43 ++++++------- openstack/block_storage/v3/_proxy.py | 22 +++---- openstack/block_storage/v3/backup.py | 9 +-- openstack/block_storage/v3/volume.py | 43 ++++++------- .../unit/block_storage/v2/test_backup.py | 34 ----------- .../tests/unit/block_storage/v2/test_proxy.py | 10 +-- .../unit/block_storage/v2/test_volume.py | 61 ++++++------------- .../unit/block_storage/v3/test_backup.py | 34 ----------- .../tests/unit/block_storage/v3/test_proxy.py | 10 +-- .../unit/block_storage/v3/test_volume.py | 61 ++++++------------- ...tore-details-classes-158ab1f46655320a.yaml | 6 ++ 15 files changed, 111 insertions(+), 273 deletions(-) create mode 100644 releasenotes/notes/remove-block-store-details-classes-158ab1f46655320a.yaml diff --git a/doc/source/user/resources/block_storage/v2/backup.rst b/doc/source/user/resources/block_storage/v2/backup.rst index 5c56b480e..a291fa5c0 100644 --- a/doc/source/user/resources/block_storage/v2/backup.rst +++ b/doc/source/user/resources/block_storage/v2/backup.rst @@ -10,12 +10,3 @@ The ``Backup`` class inherits from :class:`~openstack.resource.Resource`. .. autoclass:: openstack.block_storage.v2.backup.Backup :members: - -The BackupDetail Class ----------------------- - -The ``BackupDetail`` class inherits from -:class:`~openstack.block_storage.v2.backup.Backup`. - -.. autoclass:: openstack.block_storage.v2.backup.BackupDetail - :members: diff --git a/doc/source/user/resources/block_storage/v2/volume.rst b/doc/source/user/resources/block_storage/v2/volume.rst index 499f585ae..ba4b9db57 100644 --- a/doc/source/user/resources/block_storage/v2/volume.rst +++ b/doc/source/user/resources/block_storage/v2/volume.rst @@ -10,12 +10,3 @@ The ``Volume`` class inherits from :class:`~openstack.resource.Resource`. .. autoclass:: openstack.block_storage.v2.volume.Volume :members: - -The VolumeDetail Class ----------------------- - -The ``VolumeDetail`` class inherits from -:class:`~openstack.block_storage.v2.volume.Volume`. - -.. autoclass:: openstack.block_storage.v2.volume.VolumeDetail - :members: diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 57095afc7..653366d2a 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -147,11 +147,9 @@ def get_volume(self, volume): def volumes(self, details=True, **query): """Retrieve a generator of volumes - :param bool details: When set to ``False`` - :class:`~openstack.block_storage.v2.volume.Volume` objects - will be returned. The default, ``True``, will cause - :class:`~openstack.block_storage.v2.volume.VolumeDetail` - objects to be returned. + :param bool details: When set to ``False`` no extended attributes + will be returned. The default, ``True``, will cause objects with + additional attributes to be returned. :param kwargs query: Optional query parameters to be sent to limit the volumes being returned. Available parameters include: @@ -162,8 +160,8 @@ def volumes(self, details=True, **query): :returns: A generator of volume objects. """ - volume = _volume.VolumeDetail if details else _volume.Volume - return self._list(volume, **query) + base_path = '/volumes/detail' if details else None + return self._list(_volume.Volume, base_path=base_path, **query) def create_volume(self, **attrs): """Create a new volume from attributes @@ -214,11 +212,9 @@ def backend_pools(self): def backups(self, details=True, **query): """Retrieve a generator of backups - :param bool details: When set to ``False`` - :class:`~openstack.block_storage.v2.backup.Backup` objects - will be returned. The default, ``True``, will cause - :class:`~openstack.block_storage.v2.backup.BackupDetail` - objects to be returned. + :param bool details: When set to ``False`` no additional details will + be returned. The default, ``True``, will cause objects with + additional attributes to be returned. :param dict query: Optional query parameters to be sent to limit the resources being returned: @@ -239,8 +235,8 @@ def backups(self, details=True, **query): raise exceptions.SDKException( 'Object-store service is required for block-store backups' ) - backup = _backup.BackupDetail if details else _backup.Backup - return self._list(backup, **query) + base_path = '/backups/detail' if details else None + return self._list(_backup.Backup, base_path=base_path, **query) def get_backup(self, backup): """Get a backup diff --git a/openstack/block_storage/v2/backup.py b/openstack/block_storage/v2/backup.py index 8b3c1f82f..ee3d53163 100644 --- a/openstack/block_storage/v2/backup.py +++ b/openstack/block_storage/v2/backup.py @@ -90,11 +90,4 @@ def restore(self, session, volume_id=None, name=None): return self -class BackupDetail(Backup): - """Volume Backup with Details""" - base_path = "/backups/detail" - - # capabilities - allow_list = True - - #: Properties +BackupDetail = Backup diff --git a/openstack/block_storage/v2/volume.py b/openstack/block_storage/v2/volume.py index 28ed870ff..f1f1542a3 100644 --- a/openstack/block_storage/v2/volume.py +++ b/openstack/block_storage/v2/volume.py @@ -75,32 +75,6 @@ class Volume(resource.Resource): #: The timestamp of this volume creation. created_at = resource.Body("created_at") - def _action(self, session, body): - """Preform volume actions given the message body.""" - # NOTE: This is using Volume.base_path instead of self.base_path - # as both Volume and VolumeDetail instances can be acted on, but - # the URL used is sans any additional /detail/ part. - url = utils.urljoin(Volume.base_path, self.id, 'action') - headers = {'Accept': ''} - return session.post(url, json=body, headers=headers) - - def extend(self, session, size): - """Extend a volume size.""" - body = {'os-extend': {'new_size': size}} - self._action(session, body) - - -class VolumeDetail(Volume): - - base_path = "/volumes/detail" - - # capabilities - allow_fetch = False - allow_create = False - allow_delete = False - allow_commit = False - allow_list = True - #: The volume's current back-end. host = resource.Body("os-vol-host-attr:host") #: The project ID associated with current back-end. @@ -123,3 +97,20 @@ class VolumeDetail(Volume): #: ``True`` if this volume is encrypted, ``False`` if not. #: *Type: bool* is_encrypted = resource.Body("encrypted", type=format.BoolStr) + + def _action(self, session, body): + """Preform volume actions given the message body.""" + # NOTE: This is using Volume.base_path instead of self.base_path + # as both Volume and VolumeDetail instances can be acted on, but + # the URL used is sans any additional /detail/ part. + url = utils.urljoin(Volume.base_path, self.id, 'action') + headers = {'Accept': ''} + return session.post(url, json=body, headers=headers) + + def extend(self, session, size): + """Extend a volume size.""" + body = {'os-extend': {'new_size': size}} + self._action(session, body) + + +VolumeDetail = Volume diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 4295f9d41..6590b2fc3 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -147,11 +147,9 @@ def get_volume(self, volume): def volumes(self, details=True, **query): """Retrieve a generator of volumes - :param bool details: When set to ``False`` - :class:`~openstack.block_storage.v3.volume.Volume` objects - will be returned. The default, ``True``, will cause - :class:`~openstack.block_storage.v3.volume.VolumeDetail` - objects to be returned. + :param bool details: When set to ``False`` no extended attributes + will be returned. The default, ``True``, will cause objects with + additional attributes to be returned. :param kwargs query: Optional query parameters to be sent to limit the volumes being returned. Available parameters include: @@ -162,8 +160,8 @@ def volumes(self, details=True, **query): :returns: A generator of volume objects. """ - volume = _volume.VolumeDetail if details else _volume.Volume - return self._list(volume, **query) + base_path = '/volumes/detail' if details else None + return self._list(_volume.Volume, base_path=base_path, **query) def create_volume(self, **attrs): """Create a new volume from attributes @@ -215,10 +213,8 @@ def backups(self, details=True, **query): """Retrieve a generator of backups :param bool details: When set to ``False`` - :class:`~openstack.block_storage.v3.backup.Backup` objects - will be returned. The default, ``True``, will cause - :class:`~openstack.block_storage.v3.backup.BackupDetail` - objects to be returned. + no additional details will be returned. The default, ``True``, + will cause objects with additional attributes to be returned. :param dict query: Optional query parameters to be sent to limit the resources being returned: @@ -239,8 +235,8 @@ def backups(self, details=True, **query): raise exceptions.SDKException( 'Object-store service is required for block-store backups' ) - backup = _backup.BackupDetail if details else _backup.Backup - return self._list(backup, **query) + base_path = '/backups/detail' if details else None + return self._list(_backup.Backup, base_path=base_path, **query) def get_backup(self, backup): """Get a backup diff --git a/openstack/block_storage/v3/backup.py b/openstack/block_storage/v3/backup.py index 8b3c1f82f..ee3d53163 100644 --- a/openstack/block_storage/v3/backup.py +++ b/openstack/block_storage/v3/backup.py @@ -90,11 +90,4 @@ def restore(self, session, volume_id=None, name=None): return self -class BackupDetail(Backup): - """Volume Backup with Details""" - base_path = "/backups/detail" - - # capabilities - allow_list = True - - #: Properties +BackupDetail = Backup diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index 28ed870ff..f1f1542a3 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -75,32 +75,6 @@ class Volume(resource.Resource): #: The timestamp of this volume creation. created_at = resource.Body("created_at") - def _action(self, session, body): - """Preform volume actions given the message body.""" - # NOTE: This is using Volume.base_path instead of self.base_path - # as both Volume and VolumeDetail instances can be acted on, but - # the URL used is sans any additional /detail/ part. - url = utils.urljoin(Volume.base_path, self.id, 'action') - headers = {'Accept': ''} - return session.post(url, json=body, headers=headers) - - def extend(self, session, size): - """Extend a volume size.""" - body = {'os-extend': {'new_size': size}} - self._action(session, body) - - -class VolumeDetail(Volume): - - base_path = "/volumes/detail" - - # capabilities - allow_fetch = False - allow_create = False - allow_delete = False - allow_commit = False - allow_list = True - #: The volume's current back-end. host = resource.Body("os-vol-host-attr:host") #: The project ID associated with current back-end. @@ -123,3 +97,20 @@ class VolumeDetail(Volume): #: ``True`` if this volume is encrypted, ``False`` if not. #: *Type: bool* is_encrypted = resource.Body("encrypted", type=format.BoolStr) + + def _action(self, session, body): + """Preform volume actions given the message body.""" + # NOTE: This is using Volume.base_path instead of self.base_path + # as both Volume and VolumeDetail instances can be acted on, but + # the URL used is sans any additional /detail/ part. + url = utils.urljoin(Volume.base_path, self.id, 'action') + headers = {'Accept': ''} + return session.post(url, json=body, headers=headers) + + def extend(self, session, size): + """Extend a volume size.""" + body = {'os-extend': {'new_size': size}} + self._action(session, body) + + +VolumeDetail = Volume diff --git a/openstack/tests/unit/block_storage/v2/test_backup.py b/openstack/tests/unit/block_storage/v2/test_backup.py index cc429d1b9..58512bfc5 100644 --- a/openstack/tests/unit/block_storage/v2/test_backup.py +++ b/openstack/tests/unit/block_storage/v2/test_backup.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import copy import mock from keystoneauth1 import adapter @@ -38,12 +37,6 @@ "has_dependent_backups": False } -DETAILS = { -} - -BACKUP_DETAIL = copy.copy(BACKUP) -BACKUP_DETAIL.update(DETAILS) - class TestBackup(base.TestCase): @@ -92,30 +85,3 @@ def test_create(self): self.assertEqual(BACKUP["size"], sot.size) self.assertEqual(BACKUP["has_dependent_backups"], sot.has_dependent_backups) - - -class TestBackupDetail(base.TestCase): - - def test_basic(self): - sot = backup.BackupDetail(BACKUP_DETAIL) - self.assertIsInstance(sot, backup.Backup) - self.assertEqual("/backups/detail", sot.base_path) - - def test_create(self): - sot = backup.Backup(**BACKUP_DETAIL) - self.assertEqual(BACKUP_DETAIL["id"], sot.id) - self.assertEqual(BACKUP_DETAIL["name"], sot.name) - self.assertEqual(BACKUP_DETAIL["status"], sot.status) - self.assertEqual(BACKUP_DETAIL["container"], sot.container) - self.assertEqual(BACKUP_DETAIL["availability_zone"], - sot.availability_zone) - self.assertEqual(BACKUP_DETAIL["created_at"], sot.created_at) - self.assertEqual(BACKUP_DETAIL["updated_at"], sot.updated_at) - self.assertEqual(BACKUP_DETAIL["description"], sot.description) - self.assertEqual(BACKUP_DETAIL["fail_reason"], sot.fail_reason) - self.assertEqual(BACKUP_DETAIL["volume_id"], sot.volume_id) - self.assertEqual(BACKUP_DETAIL["object_count"], sot.object_count) - self.assertEqual(BACKUP_DETAIL["is_incremental"], sot.is_incremental) - self.assertEqual(BACKUP_DETAIL["size"], sot.size) - self.assertEqual(BACKUP_DETAIL["has_dependent_backups"], - sot.has_dependent_backups) diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index 1e0a7d061..44267ec2c 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -70,9 +70,10 @@ def test_volume_get(self): self.verify_get(self.proxy.get_volume, volume.Volume) def test_volumes_detailed(self): - self.verify_list(self.proxy.volumes, volume.VolumeDetail, + self.verify_list(self.proxy.volumes, volume.Volume, method_kwargs={"details": True, "query": 1}, - expected_kwargs={"query": 1}) + expected_kwargs={"query": 1, + "base_path": "/volumes/detail"}) def test_volumes_not_detailed(self): self.verify_list(self.proxy.volumes, volume.Volume, @@ -101,9 +102,10 @@ def test_backups_detailed(self): # NOTE: mock has_service self.proxy._connection = mock.Mock() self.proxy._connection.has_service = mock.Mock(return_value=True) - self.verify_list(self.proxy.backups, backup.BackupDetail, + self.verify_list(self.proxy.backups, backup.Backup, method_kwargs={"details": True, "query": 1}, - expected_kwargs={"query": 1}) + expected_kwargs={"query": 1, + "base_path": "/backups/detail"}) def test_backups_not_detailed(self): # NOTE: mock has_service diff --git a/openstack/tests/unit/block_storage/v2/test_volume.py b/openstack/tests/unit/block_storage/v2/test_volume.py index da20309c8..d19755981 100644 --- a/openstack/tests/unit/block_storage/v2/test_volume.py +++ b/openstack/tests/unit/block_storage/v2/test_volume.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import copy import mock from openstack.tests.unit import base @@ -42,10 +41,7 @@ "metadata": {}, "volume_image_metadata": IMAGE_METADATA, "id": FAKE_ID, - "size": 10 -} - -DETAILS = { + "size": 10, "os-vol-host-attr:host": "127.0.0.1", "os-vol-tenant-attr:tenant_id": "some tenant", "os-vol-mig-status-attr:migstat": "done", @@ -55,12 +51,9 @@ "consistencygroup_id": "123asf-asdf123", "os-volume-replication:driver_data": "ahasadfasdfasdfasdfsdf", "snapshot_id": "93c2e2aa-7744-4fd6-a31a-80c4726b08d7", - "encrypted": "false", + "encrypted": "false" } -VOLUME_DETAIL = copy.copy(VOLUME) -VOLUME_DETAIL.update(DETAILS) - class TestVolume(base.TestCase): @@ -108,6 +101,23 @@ def test_create(self): sot.volume_image_metadata) self.assertEqual(VOLUME["size"], sot.size) self.assertEqual(VOLUME["imageRef"], sot.image_id) + self.assertEqual(VOLUME["os-vol-host-attr:host"], sot.host) + self.assertEqual(VOLUME["os-vol-tenant-attr:tenant_id"], + sot.project_id) + self.assertEqual(VOLUME["os-vol-mig-status-attr:migstat"], + sot.migration_status) + self.assertEqual(VOLUME["os-vol-mig-status-attr:name_id"], + sot.migration_id) + self.assertEqual(VOLUME["replication_status"], + sot.replication_status) + self.assertEqual( + VOLUME["os-volume-replication:extended_status"], + sot.extended_replication_status) + self.assertEqual(VOLUME["consistencygroup_id"], + sot.consistency_group_id) + self.assertEqual(VOLUME["os-volume-replication:driver_data"], + sot.replication_driver_data) + self.assertFalse(sot.is_encrypted) def test_extend(self): sot = volume.Volume(**VOLUME) @@ -118,36 +128,3 @@ def test_extend(self): body = {"os-extend": {"new_size": "20"}} headers = {'Accept': ''} self.sess.post.assert_called_with(url, json=body, headers=headers) - - -class TestVolumeDetail(base.TestCase): - - def test_basic(self): - sot = volume.VolumeDetail(VOLUME_DETAIL) - self.assertIsInstance(sot, volume.Volume) - self.assertEqual("/volumes/detail", sot.base_path) - self.assertFalse(sot.allow_fetch) - self.assertFalse(sot.allow_commit) - self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_delete) - self.assertTrue(sot.allow_list) - - def test_create(self): - sot = volume.VolumeDetail(**VOLUME_DETAIL) - self.assertEqual(VOLUME_DETAIL["os-vol-host-attr:host"], sot.host) - self.assertEqual(VOLUME_DETAIL["os-vol-tenant-attr:tenant_id"], - sot.project_id) - self.assertEqual(VOLUME_DETAIL["os-vol-mig-status-attr:migstat"], - sot.migration_status) - self.assertEqual(VOLUME_DETAIL["os-vol-mig-status-attr:name_id"], - sot.migration_id) - self.assertEqual(VOLUME_DETAIL["replication_status"], - sot.replication_status) - self.assertEqual( - VOLUME_DETAIL["os-volume-replication:extended_status"], - sot.extended_replication_status) - self.assertEqual(VOLUME_DETAIL["consistencygroup_id"], - sot.consistency_group_id) - self.assertEqual(VOLUME_DETAIL["os-volume-replication:driver_data"], - sot.replication_driver_data) - self.assertFalse(sot.is_encrypted) diff --git a/openstack/tests/unit/block_storage/v3/test_backup.py b/openstack/tests/unit/block_storage/v3/test_backup.py index 509d1ac24..4d206c452 100644 --- a/openstack/tests/unit/block_storage/v3/test_backup.py +++ b/openstack/tests/unit/block_storage/v3/test_backup.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import copy import mock from keystoneauth1 import adapter @@ -38,12 +37,6 @@ "has_dependent_backups": False } -DETAILS = { -} - -BACKUP_DETAIL = copy.copy(BACKUP) -BACKUP_DETAIL.update(DETAILS) - class TestBackup(base.TestCase): @@ -92,30 +85,3 @@ def test_create(self): self.assertEqual(BACKUP["size"], sot.size) self.assertEqual(BACKUP["has_dependent_backups"], sot.has_dependent_backups) - - -class TestBackupDetail(base.TestCase): - - def test_basic(self): - sot = backup.BackupDetail(BACKUP_DETAIL) - self.assertIsInstance(sot, backup.Backup) - self.assertEqual("/backups/detail", sot.base_path) - - def test_create(self): - sot = backup.Backup(**BACKUP_DETAIL) - self.assertEqual(BACKUP_DETAIL["id"], sot.id) - self.assertEqual(BACKUP_DETAIL["name"], sot.name) - self.assertEqual(BACKUP_DETAIL["status"], sot.status) - self.assertEqual(BACKUP_DETAIL["container"], sot.container) - self.assertEqual(BACKUP_DETAIL["availability_zone"], - sot.availability_zone) - self.assertEqual(BACKUP_DETAIL["created_at"], sot.created_at) - self.assertEqual(BACKUP_DETAIL["updated_at"], sot.updated_at) - self.assertEqual(BACKUP_DETAIL["description"], sot.description) - self.assertEqual(BACKUP_DETAIL["fail_reason"], sot.fail_reason) - self.assertEqual(BACKUP_DETAIL["volume_id"], sot.volume_id) - self.assertEqual(BACKUP_DETAIL["object_count"], sot.object_count) - self.assertEqual(BACKUP_DETAIL["is_incremental"], sot.is_incremental) - self.assertEqual(BACKUP_DETAIL["size"], sot.size) - self.assertEqual(BACKUP_DETAIL["has_dependent_backups"], - sot.has_dependent_backups) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index f0057f7de..12a4c77c0 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -70,9 +70,10 @@ def test_volume_get(self): self.verify_get(self.proxy.get_volume, volume.Volume) def test_volumes_detailed(self): - self.verify_list(self.proxy.volumes, volume.VolumeDetail, + self.verify_list(self.proxy.volumes, volume.Volume, method_kwargs={"details": True, "query": 1}, - expected_kwargs={"query": 1}) + expected_kwargs={"query": 1, + "base_path": "/volumes/detail"}) def test_volumes_not_detailed(self): self.verify_list(self.proxy.volumes, volume.Volume, @@ -101,9 +102,10 @@ def test_backups_detailed(self): # NOTE: mock has_service self.proxy._connection = mock.Mock() self.proxy._connection.has_service = mock.Mock(return_value=True) - self.verify_list(self.proxy.backups, backup.BackupDetail, + self.verify_list(self.proxy.backups, backup.Backup, method_kwargs={"details": True, "query": 1}, - expected_kwargs={"query": 1}) + expected_kwargs={"query": 1, + "base_path": "/backups/detail"}) def test_backups_not_detailed(self): # NOTE: mock has_service diff --git a/openstack/tests/unit/block_storage/v3/test_volume.py b/openstack/tests/unit/block_storage/v3/test_volume.py index 6519dbe94..e16f39aa6 100644 --- a/openstack/tests/unit/block_storage/v3/test_volume.py +++ b/openstack/tests/unit/block_storage/v3/test_volume.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import copy import mock from openstack.tests.unit import base @@ -42,10 +41,7 @@ "metadata": {}, "volume_image_metadata": IMAGE_METADATA, "id": FAKE_ID, - "size": 10 -} - -DETAILS = { + "size": 10, "os-vol-host-attr:host": "127.0.0.1", "os-vol-tenant-attr:tenant_id": "some tenant", "os-vol-mig-status-attr:migstat": "done", @@ -55,12 +51,9 @@ "consistencygroup_id": "123asf-asdf123", "os-volume-replication:driver_data": "ahasadfasdfasdfasdfsdf", "snapshot_id": "93c2e2aa-7744-4fd6-a31a-80c4726b08d7", - "encrypted": "false", + "encrypted": "false" } -VOLUME_DETAIL = copy.copy(VOLUME) -VOLUME_DETAIL.update(DETAILS) - class TestVolume(base.TestCase): @@ -108,6 +101,23 @@ def test_create(self): sot.volume_image_metadata) self.assertEqual(VOLUME["size"], sot.size) self.assertEqual(VOLUME["imageRef"], sot.image_id) + self.assertEqual(VOLUME["os-vol-host-attr:host"], sot.host) + self.assertEqual(VOLUME["os-vol-tenant-attr:tenant_id"], + sot.project_id) + self.assertEqual(VOLUME["os-vol-mig-status-attr:migstat"], + sot.migration_status) + self.assertEqual(VOLUME["os-vol-mig-status-attr:name_id"], + sot.migration_id) + self.assertEqual(VOLUME["replication_status"], + sot.replication_status) + self.assertEqual( + VOLUME["os-volume-replication:extended_status"], + sot.extended_replication_status) + self.assertEqual(VOLUME["consistencygroup_id"], + sot.consistency_group_id) + self.assertEqual(VOLUME["os-volume-replication:driver_data"], + sot.replication_driver_data) + self.assertFalse(sot.is_encrypted) def test_extend(self): sot = volume.Volume(**VOLUME) @@ -118,36 +128,3 @@ def test_extend(self): body = {"os-extend": {"new_size": "20"}} headers = {'Accept': ''} self.sess.post.assert_called_with(url, json=body, headers=headers) - - -class TestVolumeDetail(base.TestCase): - - def test_basic(self): - sot = volume.VolumeDetail(VOLUME_DETAIL) - self.assertIsInstance(sot, volume.Volume) - self.assertEqual("/volumes/detail", sot.base_path) - self.assertFalse(sot.allow_fetch) - self.assertFalse(sot.allow_commit) - self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_delete) - self.assertTrue(sot.allow_list) - - def test_create(self): - sot = volume.VolumeDetail(**VOLUME_DETAIL) - self.assertEqual(VOLUME_DETAIL["os-vol-host-attr:host"], sot.host) - self.assertEqual(VOLUME_DETAIL["os-vol-tenant-attr:tenant_id"], - sot.project_id) - self.assertEqual(VOLUME_DETAIL["os-vol-mig-status-attr:migstat"], - sot.migration_status) - self.assertEqual(VOLUME_DETAIL["os-vol-mig-status-attr:name_id"], - sot.migration_id) - self.assertEqual(VOLUME_DETAIL["replication_status"], - sot.replication_status) - self.assertEqual( - VOLUME_DETAIL["os-volume-replication:extended_status"], - sot.extended_replication_status) - self.assertEqual(VOLUME_DETAIL["consistencygroup_id"], - sot.consistency_group_id) - self.assertEqual(VOLUME_DETAIL["os-volume-replication:driver_data"], - sot.replication_driver_data) - self.assertFalse(sot.is_encrypted) diff --git a/releasenotes/notes/remove-block-store-details-classes-158ab1f46655320a.yaml b/releasenotes/notes/remove-block-store-details-classes-158ab1f46655320a.yaml new file mode 100644 index 000000000..b53b6b182 --- /dev/null +++ b/releasenotes/notes/remove-block-store-details-classes-158ab1f46655320a.yaml @@ -0,0 +1,6 @@ +--- +deprecations: + - | + Requesting volumes or backups with details from block_storage will return + objects of classes Volume and Backup correspondingly, instead + of VolumeDetail and BackupDetail. From bb8b4bf5a56fda34cd9ea6debbc730fe7a9db8fe Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 9 Mar 2019 13:25:36 +0000 Subject: [PATCH 2397/3836] Swap human-facing links to use opendev.org While we should not start using this for cloning things in the gate yet, the opendev.org links provide for a nicer browsing experience already even though the service is in beta. Go ahead and swap our browsing links. While in there, remove some masakari references to github links to the source code. Masakari now publishes api docs to developer.openstack.org so there is no need to point people to source code for api docs. Change-Id: I14afa3476f9832ef433ec744e888348fc9a359e3 --- CONTRIBUTING.rst | 2 +- doc/source/contributor/local.conf | 2 +- doc/source/contributor/setup.rst | 9 +++------ doc/source/contributor/testing.rst | 6 +++--- doc/source/user/guides/baremetal.rst | 4 ++-- doc/source/user/guides/clustering/action.rst | 2 +- doc/source/user/guides/clustering/cluster.rst | 2 +- doc/source/user/guides/clustering/event.rst | 2 +- doc/source/user/guides/clustering/node.rst | 2 +- doc/source/user/guides/clustering/policy.rst | 2 +- doc/source/user/guides/clustering/policy_type.rst | 2 +- doc/source/user/guides/clustering/profile.rst | 2 +- doc/source/user/guides/clustering/profile_type.rst | 2 +- doc/source/user/guides/clustering/receiver.rst | 2 +- doc/source/user/guides/compute.rst | 6 +++--- doc/source/user/guides/connect.rst | 2 +- doc/source/user/guides/connect_from_config.rst | 2 +- doc/source/user/guides/identity.rst | 2 +- doc/source/user/guides/image.rst | 10 +++++----- doc/source/user/guides/network.rst | 8 ++++---- doc/source/user/multi-cloud-demo.rst | 2 +- openstack/config/schema.json | 2 +- openstack/config/vendor-schema.json | 2 +- openstack/instance_ha/v1/host.py | 5 ----- openstack/instance_ha/v1/notification.py | 5 ----- openstack/instance_ha/v1/segment.py | 5 ----- .../notes/removed-glanceclient-105c7fba9481b9be.yaml | 2 +- 27 files changed, 38 insertions(+), 56 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index db02b70fb..9cd75cae1 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -39,7 +39,7 @@ Mailing list (prefix subjects with ``[sdk]`` for faster responses) http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-discuss Code Hosting - https://git.openstack.org/cgit/openstack/openstacksdk + https://opendev.org/openstack/openstacksdk Code Review https://review.openstack.org/#/q/status:open+project:openstack/openstacksdk,n,z diff --git a/doc/source/contributor/local.conf b/doc/source/contributor/local.conf index 2ce95caae..3b66d569f 100644 --- a/doc/source/contributor/local.conf +++ b/doc/source/contributor/local.conf @@ -9,7 +9,7 @@ SWIFT_HASH=DEVSTACK_PASSWORD # Configure the stable OpenStack branches used by DevStack # For stable branches see -# http://git.openstack.org/cgit/openstack-dev/devstack/refs/ +# http://opendev.org/openstack-dev/devstack CINDER_BRANCH=stable/OPENSTACK_VERSION CEILOMETER_BRANCH=stable/OPENSTACK_VERSION GLANCE_BRANCH=stable/OPENSTACK_VERSION diff --git a/doc/source/contributor/setup.rst b/doc/source/contributor/setup.rst index 7aee35416..8275da02d 100644 --- a/doc/source/contributor/setup.rst +++ b/doc/source/contributor/setup.rst @@ -88,13 +88,10 @@ Getting the Source Code for details on how to use the continuous integration and code review systems that we use. -The canonical Git repository is hosted on openstack.org at -http://git.openstack.org/cgit/openstack/openstacksdk/, with a -mirror on GitHub at https://github.com/openstack/openstacksdk. -Because of how Git works, you can create a local clone from either of those, -or your own personal fork.:: +The canonical Git repository is hosted on opendev.org at +http://opendev.org/openstack/openstacksdk/:: - (sdk3)$ git clone https://git.openstack.org/openstack/openstacksdk.git + (sdk3)$ git clone https://opendev.org/openstack/openstacksdk (sdk3)$ cd openstacksdk Installing Dependencies diff --git a/doc/source/contributor/testing.rst b/doc/source/contributor/testing.rst index 3d2d02713..c72946c9b 100644 --- a/doc/source/contributor/testing.rst +++ b/doc/source/contributor/testing.rst @@ -57,14 +57,14 @@ This is the ``local.conf`` file we use to configure DevStack. Replace ``DEVSTACK_PASSWORD`` with a password of your choice. -Replace ``OPENSTACK_VERSION`` with a `stable branch `_ +Replace ``OPENSTACK_VERSION`` with a `stable branch `_ of OpenStack (without the ``stable/`` prefix on the branch name). os-client-config **************** To connect the functional tests to an OpenStack cloud we use -`os-client-config `_. +`os-client-config `_. To setup os-client-config create a ``clouds.yaml`` file in the root of your source checkout. @@ -113,7 +113,7 @@ public or private OpenStack cloud that you can run the tests against. In practice, this means that the tests should initially be run against a stable branch of `DevStack `_. And like the functional tests, the examples tests connect to an OpenStack cloud -using `os-client-config `_. +using `os-client-config `_. See the functional tests instructions for information on setting up DevStack and os-client-config. diff --git a/doc/source/user/guides/baremetal.rst b/doc/source/user/guides/baremetal.rst index 7189511a1..c32553125 100644 --- a/doc/source/user/guides/baremetal.rst +++ b/doc/source/user/guides/baremetal.rst @@ -59,6 +59,6 @@ for deployment. Full example: `baremetal provisioning`_ -.. _baremetal resource list: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/baremetal/list.py -.. _baremetal provisioning: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/baremetal/provisioning.py +.. _baremetal resource list: http://opendev.org/openstack/openstacksdk/src/branch/master/examples/baremetal/list.py +.. _baremetal provisioning: http://opendev.org/openstack/openstacksdk/src/branch/master/examples/baremetal/provisioning.py .. _Bare Metal service states documentation: https://docs.openstack.org/ironic/latest/contributor/states.html diff --git a/doc/source/user/guides/clustering/action.rst b/doc/source/user/guides/clustering/action.rst index 2cf661bb5..1be15eca3 100644 --- a/doc/source/user/guides/clustering/action.rst +++ b/doc/source/user/guides/clustering/action.rst @@ -44,4 +44,4 @@ To get a action based on its name or ID: .. literalinclude:: ../../examples/clustering/action.py :pyobject: get_action -.. _manage action: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/clustering/action.py +.. _manage action: https://opendev.org/openstack/openstacksdk/src/branch/master/examples/clustering/action.py diff --git a/doc/source/user/guides/clustering/cluster.rst b/doc/source/user/guides/clustering/cluster.rst index 280de5bab..6fd798668 100644 --- a/doc/source/user/guides/clustering/cluster.rst +++ b/doc/source/user/guides/clustering/cluster.rst @@ -189,5 +189,5 @@ To restore a specified cluster, members in the cluster will be checked. :pyobject: recover_cluster -.. _manage cluster: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/clustering/cluster.py +.. _manage cluster: https://opendev.org/openstack/openstacksdk/src/branch/master/examples/clustering/cluster.py diff --git a/doc/source/user/guides/clustering/event.rst b/doc/source/user/guides/clustering/event.rst index 80896f76f..da958e318 100644 --- a/doc/source/user/guides/clustering/event.rst +++ b/doc/source/user/guides/clustering/event.rst @@ -44,4 +44,4 @@ To get a event based on its name or ID: .. literalinclude:: ../../examples/clustering/event.py :pyobject: get_event -.. _manage event: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/clustering/event.py +.. _manage event: https://opendev.org/openstack/openstacksdk/src/branch/master/examples/clustering/event.py diff --git a/doc/source/user/guides/clustering/node.rst b/doc/source/user/guides/clustering/node.rst index 3d68396bf..99b59939d 100644 --- a/doc/source/user/guides/clustering/node.rst +++ b/doc/source/user/guides/clustering/node.rst @@ -117,4 +117,4 @@ To restore a specified node. .. literalinclude:: ../../examples/clustering/node.py :pyobject: recover_node -.. _manage node: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/clustering/node.py +.. _manage node: https://opendev.org/openstack/openstacksdk/src/branch/master/examples/clustering/node.py diff --git a/doc/source/user/guides/clustering/policy.rst b/doc/source/user/guides/clustering/policy.rst index 5bdcbb61c..bf282b214 100644 --- a/doc/source/user/guides/clustering/policy.rst +++ b/doc/source/user/guides/clustering/policy.rst @@ -99,4 +99,4 @@ still in use, you will get an error message. :pyobject: delete_policy -.. _manage policy: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/clustering/policy.py +.. _manage policy: https://opendev.org/openstack/openstacksdk/src/branch/master/examples/clustering/policy.py diff --git a/doc/source/user/guides/clustering/policy_type.rst b/doc/source/user/guides/clustering/policy_type.rst index d294028e1..eb7bc623c 100644 --- a/doc/source/user/guides/clustering/policy_type.rst +++ b/doc/source/user/guides/clustering/policy_type.rst @@ -42,4 +42,4 @@ it. Full example: `manage policy type`_ -.. _manage policy type: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/clustering/policy_type.py +.. _manage policy type: https://opendev.org/openstack/openstacksdk/src/branch/master/examples/clustering/policy_type.py diff --git a/doc/source/user/guides/clustering/profile.rst b/doc/source/user/guides/clustering/profile.rst index 228fa4232..79137844d 100644 --- a/doc/source/user/guides/clustering/profile.rst +++ b/doc/source/user/guides/clustering/profile.rst @@ -102,4 +102,4 @@ still in use, you will get an error message. :pyobject: delete_profile -.. _manage profile: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/clustering/profile.py +.. _manage profile: https://opendev.org/openstack/openstacksdk/src/branch/master/examples/clustering/profile.py diff --git a/doc/source/user/guides/clustering/profile_type.rst b/doc/source/user/guides/clustering/profile_type.rst index 2a15d19e9..4f8d3645c 100644 --- a/doc/source/user/guides/clustering/profile_type.rst +++ b/doc/source/user/guides/clustering/profile_type.rst @@ -41,4 +41,4 @@ To get the details about a profile type, you need to provide the name of it. Full example: `manage profile type`_ -.. _manage profile type: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/clustering/profile_type.py +.. _manage profile type: https://opendev.org/openstack/openstacksdk/src/branch/master/examples/clustering/profile_type.py diff --git a/doc/source/user/guides/clustering/receiver.rst b/doc/source/user/guides/clustering/receiver.rst index 587b37c18..c3c71dce0 100644 --- a/doc/source/user/guides/clustering/receiver.rst +++ b/doc/source/user/guides/clustering/receiver.rst @@ -97,4 +97,4 @@ use, you will get an error message. :pyobject: delete_receiver -.. _manage receiver: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/clustering/receiver.py +.. _manage receiver: https://opendev.org/openstack/openstacksdk/src/branch/master/examples/clustering/receiver.py diff --git a/doc/source/user/guides/compute.rst b/doc/source/user/guides/compute.rst index a31295bf9..bac8a106d 100644 --- a/doc/source/user/guides/compute.rst +++ b/doc/source/user/guides/compute.rst @@ -83,7 +83,7 @@ for it to become active. Full example: `compute resource create`_ -.. _compute resource list: https://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/compute/list.py -.. _network resource list: https://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/network/list.py -.. _compute resource create: https://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/compute/create.py +.. _compute resource list: https://opendev.org/openstack/openstacksdk/src/branch/master/examples/compute/list.py +.. _network resource list: https://opendev.org/openstack/openstacksdk/src/branch/master/examples/network/list.py +.. _compute resource create: https://opendev.org/openstack/openstacksdk/src/branch/master/examples/compute/create.py .. _public–key cryptography: https://en.wikipedia.org/wiki/Public-key_cryptography diff --git a/doc/source/user/guides/connect.rst b/doc/source/user/guides/connect.rst index bcf15fe02..5630bbcd8 100644 --- a/doc/source/user/guides/connect.rst +++ b/doc/source/user/guides/connect.rst @@ -18,7 +18,7 @@ To create a :class:`~openstack.connection.Connection` instance, use the .. literalinclude:: ../examples/connect.py :pyobject: create_connection -Full example at `connect.py `_ +Full example at `connect.py `_ .. note:: To enable logging, see the :doc:`logging` user guide. diff --git a/doc/source/user/guides/connect_from_config.rst b/doc/source/user/guides/connect_from_config.rst index dfa489ca6..0e0b4572e 100644 --- a/doc/source/user/guides/connect_from_config.rst +++ b/doc/source/user/guides/connect_from_config.rst @@ -6,7 +6,7 @@ In order to work with an OpenStack cloud you first need to create a :class:`~openstack.connection.Connection` can be created in 3 ways, using the class itself (see :doc:`connect`), a file, or environment variables as illustrated below. The SDK uses -`os-client-config `_ +`os-client-config `_ to handle the configuration. Create Connection From A File diff --git a/doc/source/user/guides/identity.rst b/doc/source/user/guides/identity.rst index 5041f7a3a..9d9444144 100644 --- a/doc/source/user/guides/identity.rst +++ b/doc/source/user/guides/identity.rst @@ -108,4 +108,4 @@ sub-regions with a region to make a tree-like structured hierarchy. Full example: `identity resource list`_ -.. _identity resource list: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/identity/list.py +.. _identity resource list: https://opendev.org/openstack/openstacksdk/src/branch/master/examples/identity/list.py diff --git a/doc/source/user/guides/image.rst b/doc/source/user/guides/image.rst index 42e32118c..9065a4931 100644 --- a/doc/source/user/guides/image.rst +++ b/doc/source/user/guides/image.rst @@ -89,9 +89,9 @@ Delete an image. Full example: `image resource delete`_ -.. _image resource create: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/image/create.py -.. _image resource import: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/image/import.py -.. _image resource delete: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/image/delete.py -.. _image resource list: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/image/list.py -.. _image resource download: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/image/download.py +.. _image resource create: https://opendev.org/openstack/openstacksdk/src/branch/master/examples/image/create.py +.. _image resource import: https://opendev.org/openstack/openstacksdk/src/branch/master/examples/image/import.py +.. _image resource delete: https://opendev.org/openstack/openstacksdk/src/branch/master/examples/image/delete.py +.. _image resource list: https://opendev.org/openstack/openstacksdk/src/branch/master/examples/image/list.py +.. _image resource download: https://opendev.org/openstack/openstacksdk/src/branch/master/examples/image/download.py .. _interoperable image import: https://docs.openstack.org/glance/latest/admin/interoperable-image-import.html diff --git a/doc/source/user/guides/network.rst b/doc/source/user/guides/network.rst index e824e9b55..fc5f4ac0f 100644 --- a/doc/source/user/guides/network.rst +++ b/doc/source/user/guides/network.rst @@ -136,7 +136,7 @@ Delete a project network and its subnets. Full example: `network resource delete`_ -.. _network resource create: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/network/create.py -.. _network resource delete: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/network/delete.py -.. _network resource list: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/network/list.py -.. _network security group create: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/network/security_group_rules.py +.. _network resource create: https://opendev.org/openstack/openstacksdk/src/branch/master/examples/network/create.py +.. _network resource delete: https://opendev.org/openstack/openstacksdk/src/branch/master/examples/network/delete.py +.. _network resource list: https://opendev.org/openstack/openstacksdk/src/branch/master/examples/network/list.py +.. _network security group create: https://opendev.org/openstack/openstacksdk/src/branch/master/examples/network/security_group_rules.py diff --git a/doc/source/user/multi-cloud-demo.rst b/doc/source/user/multi-cloud-demo.rst index c4fb90f4f..87a3dcfb9 100644 --- a/doc/source/user/multi-cloud-demo.rst +++ b/doc/source/user/multi-cloud-demo.rst @@ -45,7 +45,7 @@ What are we going to talk about? shade is Free Software ====================== -* https://git.openstack.org/cgit/openstack-infra/shade +* https://opendev.org/openstack-infra/shade * openstack-discuss@lists.openstack.org * #openstack-shade on freenode diff --git a/openstack/config/schema.json b/openstack/config/schema.json index cd430d061..7ea7d050a 100644 --- a/openstack/config/schema.json +++ b/openstack/config/schema.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "id": "https://git.openstack.org/cgit/openstack/cloud-data/plain/schema.json#", + "id": "https://opendev.org/openstack/openstacksdk/raw/branch/master/openstack/config/schema.json", "type": "object", "properties": { "auth_type": { diff --git a/openstack/config/vendor-schema.json b/openstack/config/vendor-schema.json index 5847ae545..ba671023a 100644 --- a/openstack/config/vendor-schema.json +++ b/openstack/config/vendor-schema.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "id": "https://git.openstack.org/cgit/openstack/cloud-data/plain/vendor-schema.json#", + "id": "https://opendev.org/openstack/openstacksdk/raw/branch/master/openstack/config/vendor-schema.json#", "type": "object", "properties": { "name": { diff --git a/openstack/instance_ha/v1/host.py b/openstack/instance_ha/v1/host.py index a505a648d..6ee6a4b4f 100644 --- a/openstack/instance_ha/v1/host.py +++ b/openstack/instance_ha/v1/host.py @@ -32,11 +32,6 @@ class Host(resource.Resource): allow_commit = True allow_delete = True - # Properties - # Refer "https://github.com/openstack/masakari/blob/ - # master/masakari/api/openstack/ha/schemas/hosts.py" - # for properties of host API - #: A ID of representing this host id = resource.URI("id") #: A Uuid of representing this host diff --git a/openstack/instance_ha/v1/notification.py b/openstack/instance_ha/v1/notification.py index 42dabe6ee..c83ded92a 100644 --- a/openstack/instance_ha/v1/notification.py +++ b/openstack/instance_ha/v1/notification.py @@ -51,11 +51,6 @@ class Notification(resource.Resource): allow_commit = False allow_delete = False - # Properties - # Refer "https://github.com/openstack/masakari/tree/ - # master/masakari/api/openstack/ha/schemas/notificaions.py" - # for properties of notifications API - #: A ID of representing this notification. id = resource.Body("id") #: A Uuid of representing this notification. diff --git a/openstack/instance_ha/v1/segment.py b/openstack/instance_ha/v1/segment.py index a2668f71c..c13b01e29 100644 --- a/openstack/instance_ha/v1/segment.py +++ b/openstack/instance_ha/v1/segment.py @@ -32,11 +32,6 @@ class Segment(resource.Resource): allow_commit = True allow_delete = True - # Properties - # Refer "https://github.com/openstack/masakari/tree/ - # master/masakari/api/openstack/ha/schemas" - # for properties of each API - #: A ID of representing this segment. id = resource.Body("id") #: A Uuid of representing this segment. diff --git a/releasenotes/notes/removed-glanceclient-105c7fba9481b9be.yaml b/releasenotes/notes/removed-glanceclient-105c7fba9481b9be.yaml index dc633522d..b926f4b44 100644 --- a/releasenotes/notes/removed-glanceclient-105c7fba9481b9be.yaml +++ b/releasenotes/notes/removed-glanceclient-105c7fba9481b9be.yaml @@ -9,7 +9,7 @@ prelude: > ``openstack.auth.base.BaseAuthPlugin`` classes are no more. Profile has been replace by ``openstack.config.cloud_region.CloudRegion`` from `os-client-config - `_ + `_ ``openstack.auth.base.BaseAuthPlugin`` has been replaced with the Auth plugins from keystoneauth. From 334f8acbd26cb95a50a92fe433aa9c591debcc87 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 9 Mar 2019 13:32:04 +0000 Subject: [PATCH 2398/3836] Remove outdated devstack section from docs This is linking to a very wrong local.conf. The real state of the world is vastly different and more complicated. Change-Id: I2a269a6e0dce4e637408cefd0d7e7fa12eabb3bb --- doc/source/contributor/local.conf | 60 ------------------------------ doc/source/contributor/testing.rst | 18 --------- 2 files changed, 78 deletions(-) delete mode 100644 doc/source/contributor/local.conf diff --git a/doc/source/contributor/local.conf b/doc/source/contributor/local.conf deleted file mode 100644 index 3b66d569f..000000000 --- a/doc/source/contributor/local.conf +++ /dev/null @@ -1,60 +0,0 @@ -[[local|localrc]] -# Configure passwords and the Swift Hash -MYSQL_PASSWORD=DEVSTACK_PASSWORD -RABBIT_PASSWORD=DEVSTACK_PASSWORD -SERVICE_TOKEN=DEVSTACK_PASSWORD -ADMIN_PASSWORD=DEVSTACK_PASSWORD -SERVICE_PASSWORD=DEVSTACK_PASSWORD -SWIFT_HASH=DEVSTACK_PASSWORD - -# Configure the stable OpenStack branches used by DevStack -# For stable branches see -# http://opendev.org/openstack-dev/devstack -CINDER_BRANCH=stable/OPENSTACK_VERSION -CEILOMETER_BRANCH=stable/OPENSTACK_VERSION -GLANCE_BRANCH=stable/OPENSTACK_VERSION -HEAT_BRANCH=stable/OPENSTACK_VERSION -HORIZON_BRANCH=stable/OPENSTACK_VERSION -KEYSTONE_BRANCH=stable/OPENSTACK_VERSION -NEUTRON_BRANCH=stable/OPENSTACK_VERSION -NOVA_BRANCH=stable/OPENSTACK_VERSION -SWIFT_BRANCH=stable/OPENSTACK_VERSION -ZAQAR_BRANCH=stable/OPENSTACK_VERSION - -# Enable Swift -enable_service s-proxy -enable_service s-object -enable_service s-container -enable_service s-account - -# Disable Nova Network and enable Neutron -disable_service n-net -enable_service q-svc -enable_service q-agt -enable_service q-dhcp -enable_service q-l3 -enable_service q-meta -enable_service q-metering - -# Enable Zaqar -enable_plugin zaqar https://github.com/openstack/zaqar -enable_service zaqar-server - -# Enable Heat -enable_service h-eng -enable_service h-api -enable_service h-api-cfn -enable_service h-api-cw - -# Automatically download and register a VM image that Heat can launch -# For more information on Heat and DevStack see -# https://docs.openstack.org/heat/latest/getting_started/on_devstack.html -IMAGE_URL_SITE="http://download.fedoraproject.org" -IMAGE_URL_PATH="/pub/fedora/linux/releases/25/CloudImages/x86_64/images/" -IMAGE_URL_FILE="Fedora-Cloud-Base-25-1.3.x86_64.qcow2" -IMAGE_URLS+=","$IMAGE_URL_SITE$IMAGE_URL_PATH$IMAGE_URL_FILE - -# Logging -LOGDAYS=1 -LOGFILE=/opt/stack/logs/stack.sh.log -LOGDIR=/opt/stack/logs diff --git a/doc/source/contributor/testing.rst b/doc/source/contributor/testing.rst index c72946c9b..c45b88049 100644 --- a/doc/source/contributor/testing.rst +++ b/doc/source/contributor/testing.rst @@ -42,24 +42,6 @@ public clouds but first and foremost they must be run against OpenStack. In practice, this means that the tests should initially be run against a stable branch of `DevStack `_. -DevStack -******** - -There are many ways to run and configure DevStack. The link above will show -you how to run DevStack a number of ways. You'll need to choose a method -you're familiar with and can run in your environment. Wherever DevStack is -running, we need to make sure that openstacksdk contributors are -using the same configuration. - -This is the ``local.conf`` file we use to configure DevStack. - -.. literalinclude:: local.conf - -Replace ``DEVSTACK_PASSWORD`` with a password of your choice. - -Replace ``OPENSTACK_VERSION`` with a `stable branch `_ -of OpenStack (without the ``stable/`` prefix on the branch name). - os-client-config **************** From ddd8d84cef2f70e694a29c59873a6bcbd2404298 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Sat, 9 Mar 2019 16:50:55 +0100 Subject: [PATCH 2399/3836] Deprecate ServerDetails class We have recently introduced possibility to alter base_path for operations on the resource and shifted some of the services to use it. For compute.servers(details) we still use an inherited class with modified base_path. Remove this class and instead alter the base_path for the list servers operation. This also has an advantage, that we get a list of servers with details and are immediately able to execute operations on the instance without any conversion or re-fetch by id. Change-Id: I4c4649ee1390676f3ccc6bf19e4b11b1964b5aaa --- openstack/compute/v2/_proxy.py | 10 ++++------ openstack/compute/v2/server.py | 10 +--------- openstack/tests/unit/compute/v2/test_proxy.py | 5 +++-- openstack/tests/unit/compute/v2/test_server.py | 11 ----------- ...emove-serverdetails-resource-f66cb278b224627d.yaml | 5 +++++ 5 files changed, 13 insertions(+), 28 deletions(-) create mode 100644 releasenotes/notes/remove-serverdetails-resource-f66cb278b224627d.yaml diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 10727dd2c..215aad203 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -484,10 +484,8 @@ def servers(self, details=True, all_projects=False, **query): """Retrieve a generator of servers :param bool details: When set to ``False`` - :class:`~openstack.compute.v2.server.Server` instances - will be returned. The default, ``True``, will cause - :class:`~openstack.compute.v2.server.ServerDetail` - instances to be returned. + instances with only basic data will be returned. The default, + ``True``, will cause instances with full data to be returned. :param kwargs query: Optional query parameters to be sent to limit the servers being returned. Available parameters include: @@ -521,8 +519,8 @@ def servers(self, details=True, all_projects=False, **query): """ if all_projects: query['all_projects'] = True - srv = _server.ServerDetail if details else _server.Server - return self._list(srv, **query) + base_path = '/servers/detail' if details else None + return self._list(_server.Server, base_path=base_path, **query) def update_server(self, server, **attrs): """Update a server diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 32eeec53f..1ed715876 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -440,12 +440,4 @@ def _live_migrate(self, session, host, force, block_migration, session, {'os-migrateLive': body}, microversion=microversion) -class ServerDetail(Server): - base_path = '/servers/detail' - - # capabilities - allow_create = False - allow_fetch = False - allow_commit = False - allow_delete = False - allow_list = True +ServerDetail = Server diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 6b0cac5be..b9b8694f3 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -214,10 +214,11 @@ def test_server_get(self): self.verify_get(self.proxy.get_server, server.Server) def test_servers_detailed(self): - self.verify_list(self.proxy.servers, server.ServerDetail, + self.verify_list(self.proxy.servers, server.Server, method_kwargs={"details": True, "changes_since": 1, "image": 2}, - expected_kwargs={"changes_since": 1, "image": 2}) + expected_kwargs={"changes_since": 1, "image": 2, + "base_path": "/servers/detail"}) def test_servers_not_detailed(self): self.verify_list(self.proxy.servers, server.Server, diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index b95b0ede0..f43934420 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -149,17 +149,6 @@ def test_make_it(self): sot.scheduler_hints) self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:user_data'], sot.user_data) - def test_detail(self): - sot = server.ServerDetail() - self.assertEqual('server', sot.resource_key) - self.assertEqual('servers', sot.resources_key) - self.assertEqual('/servers/detail', sot.base_path) - self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_fetch) - self.assertFalse(sot.allow_commit) - self.assertFalse(sot.allow_delete) - self.assertTrue(sot.allow_list) - def test__prepare_server(self): zone = 1 data = 2 diff --git a/releasenotes/notes/remove-serverdetails-resource-f66cb278b224627d.yaml b/releasenotes/notes/remove-serverdetails-resource-f66cb278b224627d.yaml new file mode 100644 index 000000000..4f50e5587 --- /dev/null +++ b/releasenotes/notes/remove-serverdetails-resource-f66cb278b224627d.yaml @@ -0,0 +1,5 @@ +--- +deprecations: + - | + Listing servers with details `servers(details=True)` will return + instances of the Server class instead of ServerDetails. From cf583ccace017858f7d33a531b3df8af5338cc51 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Sat, 9 Mar 2019 17:25:16 +0100 Subject: [PATCH 2400/3836] Fix some typos Change-Id: Ief188ad290cbf6e00be8742d27e99a0af42a07d9 --- HACKING.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HACKING.rst b/HACKING.rst index 9179bd7c0..cf66c368c 100644 --- a/HACKING.rst +++ b/HACKING.rst @@ -15,7 +15,7 @@ Visual indentation looks like this: return_value = self.some_method(arg1, arg1, arg3, arg4) -Visual indentation makes refactoring the code base unneccesarily hard. +Visual indentation makes refactoring the code base unnecessarily hard. Instead of visual indentation, use this: @@ -27,7 +27,7 @@ Instead of visual indentation, use this: That way, if some_method ever needs to be renamed, the only line that needs to be touched is the line with some_method. -Additionaly, if you need to line break at the top of a block, please indent +Additionally, if you need to line break at the top of a block, please indent the continuation line an additional 4 spaces, like this: .. code-block:: python From 8748f4a4f8c92e117f37f280c3d8549ca6f6661d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 2 Mar 2019 15:42:00 +0000 Subject: [PATCH 2401/3836] Move pep8 requirements in to test-requirements We had these in tox.ini before to streamline pep8 (it didn't need to actually install openstacksdk) But now that we're doing local hacking checks, we have to install the code, which means this optimization is bong. Put things back in test-requirements like sane people. While doing that, remove readme, which isn't in openstack/requirements and just let doc8 check it. Change-Id: Id1860a1df33610ad7072c13ef9ee61395b785b94 --- lower-constraints.txt | 2 ++ test-requirements.txt | 4 ++++ tox.ini | 8 +------- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index 6eb10512e..901caee09 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -2,6 +2,7 @@ appdirs==1.3.0 coverage==4.0 cryptography==2.1 decorator==3.4.0 +doc8==0.8.0 dogpile.cache==0.6.2 extras==1.0.0 fixtures==3.0.0 @@ -23,6 +24,7 @@ os-client-config==1.28.0 os-service-types==1.2.0 oslotest==3.2.0 pbr==2.0.0 +Pygments==2.2.0 python-mimeparse==1.6.0 python-subunit==1.0.0 PyYAML==3.12 diff --git a/test-requirements.txt b/test-requirements.txt index 54ba19d09..bbc69b07f 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,6 +1,8 @@ # 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>=1.0,<1.2 # Apache-2.0 + coverage!=4.4,>=4.0 # Apache-2.0 extras>=1.0.0 # MIT fixtures>=3.0.0 # Apache-2.0/BSD @@ -13,3 +15,5 @@ stestr>=1.0.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=2.2.0 # MIT +doc8>=0.8.0 # Apache-2.0 +Pygments>=2.2.0 # BSD license diff --git a/tox.ini b/tox.ini index 20e755222..726004b65 100644 --- a/tox.ini +++ b/tox.ini @@ -39,15 +39,9 @@ commands = stestr --test-path ./openstack/tests/functional/{env:OPENSTACKSDK_TES stestr slowest [testenv:pep8] -deps = - {[testenv]deps} - hacking - doc8 - pygments - readme commands = flake8 - doc8 doc/source + doc8 doc/source README.rst [hacking] local-check-factory = openstack._hacking.factory From feae8da9bf7194d6294cf79b26311b71ed88f742 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 10 Mar 2019 22:16:36 +0000 Subject: [PATCH 2402/3836] Revert "Replace TaskManager with a keystoneauth concurrency" We need to cut a quick release for masakari and this is a bit much to have in a quick release. This reverts commit c9b60f2b8634c7f8c4254e158cc921202538db04. Change-Id: I4771ee4136e54f22979e159932f36040397d4d4c --- doc/source/user/guides/logging.rst | 8 + lower-constraints.txt | 2 +- openstack/_adapter.py | 33 ++- openstack/cloud/openstackcloud.py | 3 + openstack/config/cloud_region.py | 1 + openstack/connection.py | 22 +- openstack/service_description.py | 5 + openstack/tests/unit/base.py | 1 + .../tests/unit/cloud/test_task_manager.py | 227 ++++++++++++++++++ openstack/tests/unit/test_connection.py | 16 ++ requirements.txt | 2 +- 11 files changed, 307 insertions(+), 13 deletions(-) create mode 100644 openstack/tests/unit/cloud/test_task_manager.py diff --git a/doc/source/user/guides/logging.rst b/doc/source/user/guides/logging.rst index 6c8a27eee..6eb4da4a5 100644 --- a/doc/source/user/guides/logging.rst +++ b/doc/source/user/guides/logging.rst @@ -65,6 +65,14 @@ openstack.config Issues pertaining to configuration are logged to the ``openstack.config`` logger. +openstack.task_manager + `openstacksdk` uses a Task Manager to perform remote calls. The + ``openstack.task_manager`` logger emits messages at the start and end + of each Task announcing what it is going to run and then what it ran and + how long it took. Logging ``openstack.task_manager`` is a good way to + get a trace of external actions `openstacksdk` is taking without full + `HTTP Tracing`_. + openstack.iterate_timeout When `openstacksdk` needs to poll a resource, it does so in a loop that waits between iterations and ultimately times out. The diff --git a/lower-constraints.txt b/lower-constraints.txt index 99965df90..6eb10512e 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -13,7 +13,7 @@ jmespath==0.9.0 jsonpatch==1.16 jsonpointer==1.13 jsonschema==2.6.0 -keystoneauth1==3.13.0 +keystoneauth1==3.11.0 linecache2==1.0.0 mock==2.0.0 mox3==0.20.0 diff --git a/openstack/_adapter.py b/openstack/_adapter.py index 5f8d720eb..6f9f341e3 100644 --- a/openstack/_adapter.py +++ b/openstack/_adapter.py @@ -14,6 +14,7 @@ ''' Wrapper around keystoneauth Adapter to wrap calls in TaskManager ''' +import functools try: import simplejson JSONDecodeError = simplejson.scanner.JSONDecodeError @@ -24,6 +25,7 @@ from keystoneauth1 import adapter from openstack import exceptions +from openstack import task_manager as _task_manager def _extract_name(url, service_type=None): @@ -107,22 +109,43 @@ def _json_response(response, result_key=None, error_message=None): class OpenStackSDKAdapter(adapter.Adapter): - """Wrapper around keystoneauth1.adapter.Adapter.""" + """Wrapper around keystoneauth1.adapter.Adapter. + + Uses task_manager to run tasks rather than executing them directly. + This allows using the nodepool MultiThreaded Rate Limiting TaskManager. + """ def __init__( self, session=None, + task_manager=None, + rate_limit=None, concurrency=None, *args, **kwargs): super(OpenStackSDKAdapter, self).__init__( session=session, *args, **kwargs) + if not task_manager: + task_manager = _task_manager.TaskManager( + name=self.service_type, + rate=rate_limit, + workers=concurrency) + task_manager.start() + + self.task_manager = task_manager def request( self, url, method, error_message=None, raise_exc=False, connect_retries=1, *args, **kwargs): - response = super(OpenStackSDKAdapter, self).request( - url, method, - connect_retries=connect_retries, raise_exc=False, + name_parts = _extract_name(url, self.service_type) + name = '.'.join([self.service_type, method] + name_parts) + + request_method = functools.partial( + super(OpenStackSDKAdapter, self).request, url, method) + + ret = self.task_manager.submit_function( + request_method, run_async=True, name=name, + connect_retries=connect_retries, raise_exc=raise_exc, + tag=self.service_type, **kwargs) - return response + return ret.result() def _version_matches(self, version): api_version = self.get_api_major_version() diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 43c7753a2..450ef4d9a 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -452,6 +452,7 @@ def _get_versioned_client( version=config_major) adapter = _adapter.ShadeAdapter( session=self.session, + task_manager=self.task_manager, service_type=self.config.get_service_type(service_type), service_name=self.config.get_service_name(service_type), interface=self.config.get_interface(service_type), @@ -464,6 +465,7 @@ def _get_versioned_client( adapter = _adapter.ShadeAdapter( session=self.session, + task_manager=self.task_manager, service_type=self.config.get_service_type(service_type), service_name=self.config.get_service_name(service_type), interface=self.config.get_interface(service_type), @@ -502,6 +504,7 @@ def _get_raw_client( self, service_type, api_version=None, endpoint_override=None): return _adapter.ShadeAdapter( session=self.session, + task_manager=self.task_manager, service_type=self.config.get_service_type(service_type), service_name=self.config.get_service_name(service_type), interface=self.config.get_interface(service_type), diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index e96cabe32..0d763d96c 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -492,6 +492,7 @@ def get_session_client( region_name=self.region_name, ) network_endpoint = network_adapter.get_endpoint() + network_adapter.task_manager.stop() if not network_endpoint.rstrip().rsplit('/')[-1] == 'v2.0': if not network_endpoint.endswith('/'): network_endpoint += '/' diff --git a/openstack/connection.py b/openstack/connection.py index f208bf4be..27974fa9d 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -168,6 +168,7 @@ from openstack.config import cloud_region from openstack import exceptions from openstack import service_description +from openstack import task_manager as _task_manager __all__ = [ 'from_config', @@ -258,8 +259,10 @@ def __init__(self, cloud=None, config=None, session=None, filtering instead of making list calls and filtering client-side. Default false. :param task_manager: - Ignored. Exists for backwards compat during transition. Rate limit - parameters should be passed directly to the `rate_limit` parameter. + Task Manager to handle the execution of remote REST calls. + Defaults to None which causes a direct-action Task Manager to be + used. + :type manager: :class:`~openstack.task_manager.TaskManager` :param rate_limit: Client-side rate limit, expressed in calls per second. The parameter can either be a single float, or it can be a dict with @@ -283,7 +286,6 @@ def __init__(self, cloud=None, config=None, session=None, app_name=app_name, app_version=app_version, load_yaml_config=False, load_envvars=False, - rate_limit=rate_limit, **kwargs) else: self.config = _config.get_cloud_region( @@ -291,9 +293,18 @@ def __init__(self, cloud=None, config=None, session=None, app_name=app_name, app_version=app_version, load_yaml_config=cloud is not None, load_envvars=cloud is not None, - rate_limit=rate_limit, **kwargs) + if task_manager: + # If a TaskManager was passed in, don't start it, assume it's + # under the control of the calling context. + self.task_manager = task_manager + else: + self.task_manager = _task_manager.TaskManager( + self.config.full_name, + rate=rate_limit) + self.task_manager.start() + self._session = None self._proxies = {} self.use_direct_get = use_direct_get @@ -360,8 +371,7 @@ def authorize(self): def close(self): """Release any resources held open.""" - if self.__pool_executor: - self.__pool_executor.shutdown() + self.task_manager.stop() def __enter__(self): return self diff --git a/openstack/service_description.py b/openstack/service_description.py index 206affce8..bab08562b 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -108,6 +108,7 @@ def _make_proxy(self, instance): proxy_obj = config.get_session_client( self.service_type, constructor=proxy_class, + task_manager=instance.task_manager, ) else: warnings.warn( @@ -128,6 +129,7 @@ def _make_proxy(self, instance): proxy_obj = config.get_session_client( self.service_type, constructor=proxy_class, + task_manager=instance.task_manager, ) else: warnings.warn( @@ -163,6 +165,7 @@ def _make_proxy(self, instance): proxy_obj = config.get_session_client( self.service_type, constructor=proxy_class, + task_manager=instance.task_manager, ) return proxy_obj @@ -196,12 +199,14 @@ def _make_proxy(self, instance): service_type=self.service_type), category=exceptions.UnsupportedServiceVersion) return temp_adapter + temp_adapter.task_manager.stop() proxy_class = self.supported_versions.get(str(found_version[0])) if not proxy_class: proxy_class = proxy.Proxy return config.get_session_client( self.service_type, constructor=proxy_class, + task_manager=instance.task_manager, allow_version_hack=True, **version_kwargs ) diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 7d508ecd6..cee34d786 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -438,6 +438,7 @@ def _make_test_cloud(self, cloud_name='_test_cloud_', **kwargs): cloud=test_cloud, validate=True, **kwargs) self.cloud = openstack.connection.Connection( config=self.cloud_config, strict=self.strict_cloud) + self.addCleanup(self.cloud.task_manager.stop) def get_cinder_discovery_mock_dict( self, diff --git a/openstack/tests/unit/cloud/test_task_manager.py b/openstack/tests/unit/cloud/test_task_manager.py new file mode 100644 index 000000000..89fc2dfce --- /dev/null +++ b/openstack/tests/unit/cloud/test_task_manager.py @@ -0,0 +1,227 @@ +# Copyright (c) 2015 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. + + +import concurrent.futures +import fixtures +import mock +import threading +import time + +from six.moves import queue + +from openstack import task_manager +from openstack.tests.unit import base + + +class TestException(Exception): + pass + + +class TaskTest(task_manager.Task): + def main(self): + raise TestException("This is a test exception") + + +class TaskTestGenerator(task_manager.Task): + def main(self): + yield 1 + + +class TaskTestInt(task_manager.Task): + def main(self): + return int(1) + + +class TaskTestFloat(task_manager.Task): + def main(self): + return float(2.0) + + +class TaskTestStr(task_manager.Task): + def main(self): + return "test" + + +class TaskTestBool(task_manager.Task): + def main(self): + return True + + +class TaskTestSet(task_manager.Task): + def main(self): + return set([1, 2]) + + +class TestRateTransforms(base.TestCase): + + def test_rate_parameter_scalar(self): + manager = task_manager.TaskManager(name='test', rate=0.1234) + self.assertEqual(1 / 0.1234, manager._get_wait('compute')) + self.assertEqual(1 / 0.1234, manager._get_wait(None)) + + def test_rate_parameter_dict(self): + manager = task_manager.TaskManager( + name='test', + rate={ + 'compute': 20, + 'network': 10, + }) + self.assertEqual(1.0 / 20, manager._get_wait('compute')) + self.assertEqual(1.0 / 10, manager._get_wait('network')) + self.assertIsNone(manager._get_wait('object-store')) + + +class TestTaskManager(base.TestCase): + + def setUp(self): + super(TestTaskManager, self).setUp() + self.manager = task_manager.TaskManager(name='test') + self.manager.start() + + def test_wait_re_raise(self): + """Test that Exceptions thrown in a Task is reraised correctly + + This test is aimed to six.reraise(), called in Task::wait(). + Specifically, we test if we get the same behaviour with all the + configured interpreters (e.g. py27, p35, ...) + """ + self.assertRaises(TestException, self.manager.submit_task, TaskTest()) + + def test_dont_munchify_int(self): + ret = self.manager.submit_task(TaskTestInt()) + self.assertIsInstance(ret, int) + + def test_dont_munchify_float(self): + ret = self.manager.submit_task(TaskTestFloat()) + self.assertIsInstance(ret, float) + + def test_dont_munchify_str(self): + ret = self.manager.submit_task(TaskTestStr()) + self.assertIsInstance(ret, str) + + def test_dont_munchify_bool(self): + ret = self.manager.submit_task(TaskTestBool()) + self.assertIsInstance(ret, bool) + + def test_dont_munchify_set(self): + ret = self.manager.submit_task(TaskTestSet()) + self.assertIsInstance(ret, set) + + @mock.patch.object(concurrent.futures.ThreadPoolExecutor, 'submit') + def test_async(self, mock_submit): + + self.manager.submit_function(set, run_async=True) + self.assertTrue(mock_submit.called) + + @mock.patch.object(task_manager.TaskManager, 'post_run_task') + @mock.patch.object(task_manager.TaskManager, 'pre_run_task') + def test_pre_post_calls(self, mock_pre, mock_post): + self.manager.submit_function(lambda: None) + mock_pre.assert_called_once() + mock_post.assert_called_once() + + @mock.patch.object(task_manager.TaskManager, 'post_run_task') + @mock.patch.object(task_manager.TaskManager, 'pre_run_task') + def test_validate_timing(self, mock_pre, mock_post): + # Note the unit test setup has mocked out time.sleep() and + # done a * 0.0001, and the test should be under the 5 + # second timeout. Thus with below, we should see at + # *least* a 1 second pause running the task. + self.manager.submit_function(lambda: time.sleep(10000)) + + mock_pre.assert_called_once() + mock_post.assert_called_once() + + args, kwargs = mock_post.call_args_list[0] + self.assertTrue(args[0] > 1.0) + + +class ThreadingTaskManager(task_manager.TaskManager): + """A subclass of TaskManager which exercises the thread-shifting + exception handling behavior.""" + + def __init__(self, *args, **kw): + super(ThreadingTaskManager, self).__init__( + *args, **kw) + self.queue = queue.Queue() + self._running = True + self._thread = threading.Thread(name=self.name, target=self.run) + self._thread.daemon = True + self.failed = False + + def start(self): + self._thread.start() + + def stop(self): + self._running = False + self.queue.put(None) + + def join(self): + self._thread.join() + + def run(self): + # No exception should ever cause this method to hit its + # exception handler. + try: + while True: + task = self.queue.get() + if not task: + if not self._running: + break + continue + self.run_task(task) + self.queue.task_done() + except Exception: + self.failed = True + raise + + def submit_task(self, task, raw=False): + # An important part of the exception-shifting feature is that + # this method should raise the exception. + self.queue.put(task) + return task.wait() + + +class ThreadingTaskManagerFixture(fixtures.Fixture): + def _setUp(self): + self.manager = ThreadingTaskManager(name='threading test') + self.manager.start() + self.addCleanup(self._cleanup) + + def _cleanup(self): + self.manager.stop() + self.manager.join() + + +class TestThreadingTaskManager(base.TestCase): + + def setUp(self): + super(TestThreadingTaskManager, self).setUp() + f = self.useFixture(ThreadingTaskManagerFixture()) + self.manager = f.manager + + def test_wait_re_raise(self): + """Test that Exceptions thrown in a Task is reraised correctly + + This test is aimed to six.reraise(), called in Task::wait(). + Specifically, we test if we get the same behaviour with all the + configured interpreters (e.g. py27, p35, ...) + """ + self.assertRaises(TestException, self.manager.submit_task, TaskTest()) + # Stop the manager and join the run thread to ensure the + # exception handler has run. + self.manager.stop() + self.manager.join() + self.assertFalse(self.manager.failed) diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index 163275fd0..f27ce67b9 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -91,6 +91,22 @@ def test_session_provided(self): self.assertEqual(mock_session, conn.session) self.assertEqual('auth.example.com', conn.config.name) + def test_task_manager_rate_scalar(self): + conn = connection.Connection(cloud='sample-cloud', rate_limit=20) + self.assertEqual(1.0 / 20, conn.task_manager._get_wait('object-store')) + self.assertEqual(1.0 / 20, conn.task_manager._get_wait(None)) + + def test_task_manager_rate_dict(self): + conn = connection.Connection( + cloud='sample-cloud', + rate_limit={ + 'compute': 20, + 'network': 10, + }) + self.assertEqual(1.0 / 20, conn.task_manager._get_wait('compute')) + self.assertEqual(1.0 / 10, conn.task_manager._get_wait('network')) + self.assertIsNone(conn.task_manager._get_wait('object-store')) + def test_create_session(self): conn = connection.Connection(cloud='sample-cloud') self.assertIsNotNone(conn) diff --git a/requirements.txt b/requirements.txt index 6181b5544..78f758519 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ requestsexceptions>=1.2.0 # Apache-2.0 jsonpatch!=1.20,>=1.16 # BSD six>=1.10.0 # MIT os-service-types>=1.2.0 # Apache-2.0 -keystoneauth1>=3.13.0 # Apache-2.0 +keystoneauth1>=3.11.0 # Apache-2.0 munch>=2.1.0 # MIT decorator>=3.4.0 # BSD From e7a9b52998f23d7d7af99ecd9adb0554102f9604 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 10 Mar 2019 22:17:19 +0000 Subject: [PATCH 2403/3836] Revert "Revert "Replace TaskManager with a keystoneauth concurrency"" This reverts commit feae8da9bf7194d6294cf79b26311b71ed88f742. Change-Id: I6f85cc58a404063a262cf7338c827cde61493b43 --- doc/source/user/guides/logging.rst | 8 - lower-constraints.txt | 2 +- openstack/_adapter.py | 33 +-- openstack/cloud/openstackcloud.py | 3 - openstack/config/cloud_region.py | 1 - openstack/connection.py | 22 +- openstack/service_description.py | 5 - openstack/tests/unit/base.py | 1 - .../tests/unit/cloud/test_task_manager.py | 227 ------------------ openstack/tests/unit/test_connection.py | 16 -- requirements.txt | 2 +- 11 files changed, 13 insertions(+), 307 deletions(-) delete mode 100644 openstack/tests/unit/cloud/test_task_manager.py diff --git a/doc/source/user/guides/logging.rst b/doc/source/user/guides/logging.rst index 6eb4da4a5..6c8a27eee 100644 --- a/doc/source/user/guides/logging.rst +++ b/doc/source/user/guides/logging.rst @@ -65,14 +65,6 @@ openstack.config Issues pertaining to configuration are logged to the ``openstack.config`` logger. -openstack.task_manager - `openstacksdk` uses a Task Manager to perform remote calls. The - ``openstack.task_manager`` logger emits messages at the start and end - of each Task announcing what it is going to run and then what it ran and - how long it took. Logging ``openstack.task_manager`` is a good way to - get a trace of external actions `openstacksdk` is taking without full - `HTTP Tracing`_. - openstack.iterate_timeout When `openstacksdk` needs to poll a resource, it does so in a loop that waits between iterations and ultimately times out. The diff --git a/lower-constraints.txt b/lower-constraints.txt index 6eb10512e..99965df90 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -13,7 +13,7 @@ jmespath==0.9.0 jsonpatch==1.16 jsonpointer==1.13 jsonschema==2.6.0 -keystoneauth1==3.11.0 +keystoneauth1==3.13.0 linecache2==1.0.0 mock==2.0.0 mox3==0.20.0 diff --git a/openstack/_adapter.py b/openstack/_adapter.py index 6f9f341e3..5f8d720eb 100644 --- a/openstack/_adapter.py +++ b/openstack/_adapter.py @@ -14,7 +14,6 @@ ''' Wrapper around keystoneauth Adapter to wrap calls in TaskManager ''' -import functools try: import simplejson JSONDecodeError = simplejson.scanner.JSONDecodeError @@ -25,7 +24,6 @@ from keystoneauth1 import adapter from openstack import exceptions -from openstack import task_manager as _task_manager def _extract_name(url, service_type=None): @@ -109,43 +107,22 @@ def _json_response(response, result_key=None, error_message=None): class OpenStackSDKAdapter(adapter.Adapter): - """Wrapper around keystoneauth1.adapter.Adapter. - - Uses task_manager to run tasks rather than executing them directly. - This allows using the nodepool MultiThreaded Rate Limiting TaskManager. - """ + """Wrapper around keystoneauth1.adapter.Adapter.""" def __init__( self, session=None, - task_manager=None, - rate_limit=None, concurrency=None, *args, **kwargs): super(OpenStackSDKAdapter, self).__init__( session=session, *args, **kwargs) - if not task_manager: - task_manager = _task_manager.TaskManager( - name=self.service_type, - rate=rate_limit, - workers=concurrency) - task_manager.start() - - self.task_manager = task_manager def request( self, url, method, error_message=None, raise_exc=False, connect_retries=1, *args, **kwargs): - name_parts = _extract_name(url, self.service_type) - name = '.'.join([self.service_type, method] + name_parts) - - request_method = functools.partial( - super(OpenStackSDKAdapter, self).request, url, method) - - ret = self.task_manager.submit_function( - request_method, run_async=True, name=name, - connect_retries=connect_retries, raise_exc=raise_exc, - tag=self.service_type, + response = super(OpenStackSDKAdapter, self).request( + url, method, + connect_retries=connect_retries, raise_exc=False, **kwargs) - return ret.result() + return response def _version_matches(self, version): api_version = self.get_api_major_version() diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 450ef4d9a..43c7753a2 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -452,7 +452,6 @@ def _get_versioned_client( version=config_major) adapter = _adapter.ShadeAdapter( session=self.session, - task_manager=self.task_manager, service_type=self.config.get_service_type(service_type), service_name=self.config.get_service_name(service_type), interface=self.config.get_interface(service_type), @@ -465,7 +464,6 @@ def _get_versioned_client( adapter = _adapter.ShadeAdapter( session=self.session, - task_manager=self.task_manager, service_type=self.config.get_service_type(service_type), service_name=self.config.get_service_name(service_type), interface=self.config.get_interface(service_type), @@ -504,7 +502,6 @@ def _get_raw_client( self, service_type, api_version=None, endpoint_override=None): return _adapter.ShadeAdapter( session=self.session, - task_manager=self.task_manager, service_type=self.config.get_service_type(service_type), service_name=self.config.get_service_name(service_type), interface=self.config.get_interface(service_type), diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 0d763d96c..e96cabe32 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -492,7 +492,6 @@ def get_session_client( region_name=self.region_name, ) network_endpoint = network_adapter.get_endpoint() - network_adapter.task_manager.stop() if not network_endpoint.rstrip().rsplit('/')[-1] == 'v2.0': if not network_endpoint.endswith('/'): network_endpoint += '/' diff --git a/openstack/connection.py b/openstack/connection.py index 27974fa9d..f208bf4be 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -168,7 +168,6 @@ from openstack.config import cloud_region from openstack import exceptions from openstack import service_description -from openstack import task_manager as _task_manager __all__ = [ 'from_config', @@ -259,10 +258,8 @@ def __init__(self, cloud=None, config=None, session=None, filtering instead of making list calls and filtering client-side. Default false. :param task_manager: - Task Manager to handle the execution of remote REST calls. - Defaults to None which causes a direct-action Task Manager to be - used. - :type manager: :class:`~openstack.task_manager.TaskManager` + Ignored. Exists for backwards compat during transition. Rate limit + parameters should be passed directly to the `rate_limit` parameter. :param rate_limit: Client-side rate limit, expressed in calls per second. The parameter can either be a single float, or it can be a dict with @@ -286,6 +283,7 @@ def __init__(self, cloud=None, config=None, session=None, app_name=app_name, app_version=app_version, load_yaml_config=False, load_envvars=False, + rate_limit=rate_limit, **kwargs) else: self.config = _config.get_cloud_region( @@ -293,18 +291,9 @@ def __init__(self, cloud=None, config=None, session=None, app_name=app_name, app_version=app_version, load_yaml_config=cloud is not None, load_envvars=cloud is not None, + rate_limit=rate_limit, **kwargs) - if task_manager: - # If a TaskManager was passed in, don't start it, assume it's - # under the control of the calling context. - self.task_manager = task_manager - else: - self.task_manager = _task_manager.TaskManager( - self.config.full_name, - rate=rate_limit) - self.task_manager.start() - self._session = None self._proxies = {} self.use_direct_get = use_direct_get @@ -371,7 +360,8 @@ def authorize(self): def close(self): """Release any resources held open.""" - self.task_manager.stop() + if self.__pool_executor: + self.__pool_executor.shutdown() def __enter__(self): return self diff --git a/openstack/service_description.py b/openstack/service_description.py index bab08562b..206affce8 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -108,7 +108,6 @@ def _make_proxy(self, instance): proxy_obj = config.get_session_client( self.service_type, constructor=proxy_class, - task_manager=instance.task_manager, ) else: warnings.warn( @@ -129,7 +128,6 @@ def _make_proxy(self, instance): proxy_obj = config.get_session_client( self.service_type, constructor=proxy_class, - task_manager=instance.task_manager, ) else: warnings.warn( @@ -165,7 +163,6 @@ def _make_proxy(self, instance): proxy_obj = config.get_session_client( self.service_type, constructor=proxy_class, - task_manager=instance.task_manager, ) return proxy_obj @@ -199,14 +196,12 @@ def _make_proxy(self, instance): service_type=self.service_type), category=exceptions.UnsupportedServiceVersion) return temp_adapter - temp_adapter.task_manager.stop() proxy_class = self.supported_versions.get(str(found_version[0])) if not proxy_class: proxy_class = proxy.Proxy return config.get_session_client( self.service_type, constructor=proxy_class, - task_manager=instance.task_manager, allow_version_hack=True, **version_kwargs ) diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index cee34d786..7d508ecd6 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -438,7 +438,6 @@ def _make_test_cloud(self, cloud_name='_test_cloud_', **kwargs): cloud=test_cloud, validate=True, **kwargs) self.cloud = openstack.connection.Connection( config=self.cloud_config, strict=self.strict_cloud) - self.addCleanup(self.cloud.task_manager.stop) def get_cinder_discovery_mock_dict( self, diff --git a/openstack/tests/unit/cloud/test_task_manager.py b/openstack/tests/unit/cloud/test_task_manager.py deleted file mode 100644 index 89fc2dfce..000000000 --- a/openstack/tests/unit/cloud/test_task_manager.py +++ /dev/null @@ -1,227 +0,0 @@ -# Copyright (c) 2015 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. - - -import concurrent.futures -import fixtures -import mock -import threading -import time - -from six.moves import queue - -from openstack import task_manager -from openstack.tests.unit import base - - -class TestException(Exception): - pass - - -class TaskTest(task_manager.Task): - def main(self): - raise TestException("This is a test exception") - - -class TaskTestGenerator(task_manager.Task): - def main(self): - yield 1 - - -class TaskTestInt(task_manager.Task): - def main(self): - return int(1) - - -class TaskTestFloat(task_manager.Task): - def main(self): - return float(2.0) - - -class TaskTestStr(task_manager.Task): - def main(self): - return "test" - - -class TaskTestBool(task_manager.Task): - def main(self): - return True - - -class TaskTestSet(task_manager.Task): - def main(self): - return set([1, 2]) - - -class TestRateTransforms(base.TestCase): - - def test_rate_parameter_scalar(self): - manager = task_manager.TaskManager(name='test', rate=0.1234) - self.assertEqual(1 / 0.1234, manager._get_wait('compute')) - self.assertEqual(1 / 0.1234, manager._get_wait(None)) - - def test_rate_parameter_dict(self): - manager = task_manager.TaskManager( - name='test', - rate={ - 'compute': 20, - 'network': 10, - }) - self.assertEqual(1.0 / 20, manager._get_wait('compute')) - self.assertEqual(1.0 / 10, manager._get_wait('network')) - self.assertIsNone(manager._get_wait('object-store')) - - -class TestTaskManager(base.TestCase): - - def setUp(self): - super(TestTaskManager, self).setUp() - self.manager = task_manager.TaskManager(name='test') - self.manager.start() - - def test_wait_re_raise(self): - """Test that Exceptions thrown in a Task is reraised correctly - - This test is aimed to six.reraise(), called in Task::wait(). - Specifically, we test if we get the same behaviour with all the - configured interpreters (e.g. py27, p35, ...) - """ - self.assertRaises(TestException, self.manager.submit_task, TaskTest()) - - def test_dont_munchify_int(self): - ret = self.manager.submit_task(TaskTestInt()) - self.assertIsInstance(ret, int) - - def test_dont_munchify_float(self): - ret = self.manager.submit_task(TaskTestFloat()) - self.assertIsInstance(ret, float) - - def test_dont_munchify_str(self): - ret = self.manager.submit_task(TaskTestStr()) - self.assertIsInstance(ret, str) - - def test_dont_munchify_bool(self): - ret = self.manager.submit_task(TaskTestBool()) - self.assertIsInstance(ret, bool) - - def test_dont_munchify_set(self): - ret = self.manager.submit_task(TaskTestSet()) - self.assertIsInstance(ret, set) - - @mock.patch.object(concurrent.futures.ThreadPoolExecutor, 'submit') - def test_async(self, mock_submit): - - self.manager.submit_function(set, run_async=True) - self.assertTrue(mock_submit.called) - - @mock.patch.object(task_manager.TaskManager, 'post_run_task') - @mock.patch.object(task_manager.TaskManager, 'pre_run_task') - def test_pre_post_calls(self, mock_pre, mock_post): - self.manager.submit_function(lambda: None) - mock_pre.assert_called_once() - mock_post.assert_called_once() - - @mock.patch.object(task_manager.TaskManager, 'post_run_task') - @mock.patch.object(task_manager.TaskManager, 'pre_run_task') - def test_validate_timing(self, mock_pre, mock_post): - # Note the unit test setup has mocked out time.sleep() and - # done a * 0.0001, and the test should be under the 5 - # second timeout. Thus with below, we should see at - # *least* a 1 second pause running the task. - self.manager.submit_function(lambda: time.sleep(10000)) - - mock_pre.assert_called_once() - mock_post.assert_called_once() - - args, kwargs = mock_post.call_args_list[0] - self.assertTrue(args[0] > 1.0) - - -class ThreadingTaskManager(task_manager.TaskManager): - """A subclass of TaskManager which exercises the thread-shifting - exception handling behavior.""" - - def __init__(self, *args, **kw): - super(ThreadingTaskManager, self).__init__( - *args, **kw) - self.queue = queue.Queue() - self._running = True - self._thread = threading.Thread(name=self.name, target=self.run) - self._thread.daemon = True - self.failed = False - - def start(self): - self._thread.start() - - def stop(self): - self._running = False - self.queue.put(None) - - def join(self): - self._thread.join() - - def run(self): - # No exception should ever cause this method to hit its - # exception handler. - try: - while True: - task = self.queue.get() - if not task: - if not self._running: - break - continue - self.run_task(task) - self.queue.task_done() - except Exception: - self.failed = True - raise - - def submit_task(self, task, raw=False): - # An important part of the exception-shifting feature is that - # this method should raise the exception. - self.queue.put(task) - return task.wait() - - -class ThreadingTaskManagerFixture(fixtures.Fixture): - def _setUp(self): - self.manager = ThreadingTaskManager(name='threading test') - self.manager.start() - self.addCleanup(self._cleanup) - - def _cleanup(self): - self.manager.stop() - self.manager.join() - - -class TestThreadingTaskManager(base.TestCase): - - def setUp(self): - super(TestThreadingTaskManager, self).setUp() - f = self.useFixture(ThreadingTaskManagerFixture()) - self.manager = f.manager - - def test_wait_re_raise(self): - """Test that Exceptions thrown in a Task is reraised correctly - - This test is aimed to six.reraise(), called in Task::wait(). - Specifically, we test if we get the same behaviour with all the - configured interpreters (e.g. py27, p35, ...) - """ - self.assertRaises(TestException, self.manager.submit_task, TaskTest()) - # Stop the manager and join the run thread to ensure the - # exception handler has run. - self.manager.stop() - self.manager.join() - self.assertFalse(self.manager.failed) diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index f27ce67b9..163275fd0 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -91,22 +91,6 @@ def test_session_provided(self): self.assertEqual(mock_session, conn.session) self.assertEqual('auth.example.com', conn.config.name) - def test_task_manager_rate_scalar(self): - conn = connection.Connection(cloud='sample-cloud', rate_limit=20) - self.assertEqual(1.0 / 20, conn.task_manager._get_wait('object-store')) - self.assertEqual(1.0 / 20, conn.task_manager._get_wait(None)) - - def test_task_manager_rate_dict(self): - conn = connection.Connection( - cloud='sample-cloud', - rate_limit={ - 'compute': 20, - 'network': 10, - }) - self.assertEqual(1.0 / 20, conn.task_manager._get_wait('compute')) - self.assertEqual(1.0 / 10, conn.task_manager._get_wait('network')) - self.assertIsNone(conn.task_manager._get_wait('object-store')) - def test_create_session(self): conn = connection.Connection(cloud='sample-cloud') self.assertIsNotNone(conn) diff --git a/requirements.txt b/requirements.txt index 78f758519..6181b5544 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ requestsexceptions>=1.2.0 # Apache-2.0 jsonpatch!=1.20,>=1.16 # BSD six>=1.10.0 # MIT os-service-types>=1.2.0 # Apache-2.0 -keystoneauth1>=3.11.0 # Apache-2.0 +keystoneauth1>=3.13.0 # Apache-2.0 munch>=2.1.0 # MIT decorator>=3.4.0 # BSD From b548c0eecfa3fea62dd7fca04d2b352faedc5d29 Mon Sep 17 00:00:00 2001 From: Jakub Jursa Date: Fri, 7 Dec 2018 16:43:34 +0100 Subject: [PATCH 2404/3836] added support for binding:profile parameter in create_port/update_port Change-Id: Ie6629f33dabfe07269e862c566255cf714a3f4c5 --- openstack/cloud/openstackcloud.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 450ef4d9a..5462cb449 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -8089,7 +8089,7 @@ def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, 'subnet_id', 'ip_address', 'security_groups', 'allowed_address_pairs', 'extra_dhcp_opts', 'device_owner', 'device_id', 'binding:vnic_type', - 'port_security_enabled') + 'binding:profile', 'port_security_enabled') def create_port(self, network_id, **kwargs): """Create a port @@ -8157,7 +8157,8 @@ def create_port(self, network_id, **kwargs): @_utils.valid_kwargs('name', 'admin_state_up', 'fixed_ips', 'security_groups', 'allowed_address_pairs', 'extra_dhcp_opts', 'device_owner', 'device_id', - 'binding:vnic_type', 'port_security_enabled') + 'binding:vnic_type', 'binding:profile', + 'port_security_enabled') def update_port(self, name_or_id, **kwargs): """Update a port From f9b0911166ebd5b184fe23b43cca216d8ce35687 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 9 Mar 2019 13:47:53 +0000 Subject: [PATCH 2405/3836] Collapse OpenStackSDKAdapter into Proxy We have two subclasses of keystoneauth1.adapter.Adapter. We never use OpenStackSDKAdapter directly for anything. Collapse it in to Proxy. Change-Id: Ia0034348d80804e31867349b0939b37fb2b8f21f --- doc/source/contributor/coding.rst | 2 +- doc/source/contributor/layout.rst | 6 - openstack/_adapter.py | 139 ------------- openstack/cloud/openstackcloud.py | 190 +++++++++--------- openstack/config/cloud_region.py | 4 +- openstack/object_store/v1/_proxy.py | 3 +- openstack/proxy.py | 115 ++++++++++- openstack/service_description.py | 12 +- .../functional/cloud/test_floating_ip.py | 4 +- openstack/tests/unit/test__adapter.py | 38 ---- openstack/tests/unit/test_proxy.py | 23 +++ 11 files changed, 239 insertions(+), 297 deletions(-) delete mode 100644 openstack/_adapter.py delete mode 100644 openstack/tests/unit/test__adapter.py diff --git a/doc/source/contributor/coding.rst b/doc/source/contributor/coding.rst index 14ab10ff8..67e9ea3eb 100644 --- a/doc/source/contributor/coding.rst +++ b/doc/source/contributor/coding.rst @@ -63,7 +63,7 @@ Returned Resources ------------------ Complex objects returned to the caller must be a `munch.Munch` type. The -`openstack._adapter.ShadeAdapter` class makes resources into `munch.Munch`. +`openstack.proxy._ShadeAdapter` class makes resources into `munch.Munch`. All objects should be normalized. It is shade's purpose in life to make OpenStack consistent for end users, and this means not trusting the clouds diff --git a/doc/source/contributor/layout.rst b/doc/source/contributor/layout.rst index 3605f7ec6..0656ee1a2 100644 --- a/doc/source/contributor/layout.rst +++ b/doc/source/contributor/layout.rst @@ -56,18 +56,12 @@ Each service implements a ``Proxy`` class based on service's ``Proxy`` exists in ``openstack/compute/v2/_proxy.py``. The :class:`~openstack.proxy.Proxy` class is based on -:class:`~openstack._adapter.OpenStackSDKAdapter` which is in turn based on :class:`~keystoneauth1.adapter.Adapter`. .. autoclass:: openstack.proxy.Proxy :members: :show-inheritance: -.. autoclass:: openstack._adapter.OpenStackSDKAdapter - :members: - :inherited-members: - :show-inheritance: - Each service's ``Proxy`` provides a higher-level interface for users to work with via a :class:`~openstack.connection.Connection` instance. diff --git a/openstack/_adapter.py b/openstack/_adapter.py deleted file mode 100644 index 5f8d720eb..000000000 --- a/openstack/_adapter.py +++ /dev/null @@ -1,139 +0,0 @@ -# Copyright (c) 2016 Red Hat, 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. - -''' Wrapper around keystoneauth Adapter to wrap calls in TaskManager ''' - -try: - import simplejson - JSONDecodeError = simplejson.scanner.JSONDecodeError -except ImportError: - JSONDecodeError = ValueError - -from six.moves import urllib -from keystoneauth1 import adapter - -from openstack import exceptions - - -def _extract_name(url, service_type=None): - '''Produce a key name to use in logging/metrics from the URL path. - - We want to be able to logic/metric sane general things, so we pull - the url apart to generate names. The function returns a list because - there are two different ways in which the elements want to be combined - below (one for logging, one for statsd) - - Some examples are likely useful: - - /servers -> ['servers'] - /servers/{id} -> ['servers'] - /servers/{id}/os-security-groups -> ['servers', 'os-security-groups'] - /v2.0/networks.json -> ['networks'] - ''' - - url_path = urllib.parse.urlparse(url).path.strip() - # Remove / from the beginning to keep the list indexes of interesting - # things consistent - if url_path.startswith('/'): - url_path = url_path[1:] - - # Special case for neutron, which puts .json on the end of urls - if url_path.endswith('.json'): - url_path = url_path[:-len('.json')] - - url_parts = url_path.split('/') - if url_parts[-1] == 'detail': - # Special case detail calls - # GET /servers/detail - # returns ['servers', 'detail'] - name_parts = url_parts[-2:] - else: - # Strip leading version piece so that - # GET /v2.0/networks - # returns ['networks'] - if url_parts[0] in ('v1', 'v2', 'v2.0'): - url_parts = url_parts[1:] - name_parts = [] - # Pull out every other URL portion - so that - # GET /servers/{id}/os-security-groups - # returns ['servers', 'os-security-groups'] - for idx in range(0, len(url_parts)): - if not idx % 2 and url_parts[idx]: - name_parts.append(url_parts[idx]) - - # Keystone Token fetching is a special case, so we name it "tokens" - if url_path.endswith('tokens'): - name_parts = ['tokens'] - - # Getting the root of an endpoint is doing version discovery - if not name_parts: - if service_type == 'object-store': - name_parts = ['account'] - else: - name_parts = ['discovery'] - - # Strip out anything that's empty or None - return [part for part in name_parts if part] - - -def _json_response(response, result_key=None, error_message=None): - """Temporary method to use to bridge from ShadeAdapter to SDK calls.""" - exceptions.raise_from_response(response, error_message=error_message) - - if not response.content: - # This doesn't have any content - return response - - # Some REST calls do not return json content. Don't decode it. - if 'application/json' not in response.headers.get('Content-Type'): - return response - - try: - result_json = response.json() - except JSONDecodeError: - return response - return result_json - - -class OpenStackSDKAdapter(adapter.Adapter): - """Wrapper around keystoneauth1.adapter.Adapter.""" - - def __init__( - self, session=None, - *args, **kwargs): - super(OpenStackSDKAdapter, self).__init__( - session=session, *args, **kwargs) - - def request( - self, url, method, error_message=None, - raise_exc=False, connect_retries=1, *args, **kwargs): - response = super(OpenStackSDKAdapter, self).request( - url, method, - connect_retries=connect_retries, raise_exc=False, - **kwargs) - return response - - def _version_matches(self, version): - api_version = self.get_api_major_version() - if api_version: - return api_version[0] == version - return False - - -class ShadeAdapter(OpenStackSDKAdapter): - """Wrapper for shade methods that expect json unpacking.""" - - def request(self, url, method, error_message=None, **kwargs): - response = super(ShadeAdapter, self).request(url, method, **kwargs) - return _json_response(response, error_message=error_message) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 804be74aa..54e0939d6 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -38,7 +38,6 @@ import keystoneauth1.exceptions import keystoneauth1.session -from openstack import _adapter from openstack import _log from openstack import exceptions from openstack.cloud import exc @@ -49,6 +48,7 @@ from openstack.cloud import _utils import openstack.config import openstack.config.defaults +from openstack import proxy from openstack import utils DEFAULT_SERVER_AGE = 5 @@ -444,7 +444,7 @@ def _get_versioned_client( request_min_version = config_version request_max_version = '{version}.latest'.format( version=config_major) - adapter = _adapter.ShadeAdapter( + adapter = proxy._ShadeAdapter( session=self.session, service_type=self.config.get_service_type(service_type), service_name=self.config.get_service_name(service_type), @@ -456,7 +456,7 @@ def _get_versioned_client( if adapter.get_endpoint(): return adapter - adapter = _adapter.ShadeAdapter( + adapter = proxy._ShadeAdapter( session=self.session, service_type=self.config.get_service_type(service_type), service_name=self.config.get_service_name(service_type), @@ -494,7 +494,7 @@ def _get_versioned_client( # object. def _get_raw_client( self, service_type, api_version=None, endpoint_override=None): - return _adapter.ShadeAdapter( + return proxy._ShadeAdapter( session=self.session, service_type=self.config.get_service_type(service_type), service_name=self.config.get_service_name(service_type), @@ -802,7 +802,7 @@ def _get_and_munchify(self, key, data): to fail. """ if isinstance(data, requests.models.Response): - data = _adapter._json_response(data) + data = proxy._json_response(data) return meta.get_and_munchify(key, data) @_utils.cache_on_arguments() @@ -1416,7 +1416,7 @@ def has_service(self, service_key): @_utils.cache_on_arguments() def _nova_extensions(self): extensions = set() - data = _adapter._json_response( + data = proxy._json_response( self.compute.get('/extensions'), error_message="Error fetching extension list for nova") @@ -1435,7 +1435,7 @@ def search_keypairs(self, name_or_id=None, filters=None): def _neutron_extensions(self): extensions = set() resp = self.network.get('/extensions.json') - data = _adapter._json_response( + data = proxy._json_response( resp, error_message="Error fetching extension list for neutron") for extension in self._get_and_munchify('extensions', data): @@ -1633,7 +1633,7 @@ def list_keypairs(self): :returns: A list of ``munch.Munch`` containing keypair info. """ - data = _adapter._json_response( + data = proxy._json_response( self.compute.get('/os-keypairs'), error_message="Error fetching keypair list") return self._normalize_keypairs([ @@ -1663,7 +1663,7 @@ def list_routers(self, filters=None): if not filters: filters = {} resp = self.network.get("/routers.json", params=filters) - data = _adapter._json_response( + data = proxy._json_response( resp, error_message="Error fetching router list") return self._get_and_munchify('routers', data) @@ -1716,7 +1716,7 @@ def list_ports(self, filters=None): def _list_ports(self, filters): resp = self.network.get("/ports.json", params=filters) - data = _adapter._json_response( + data = proxy._json_response( resp, error_message="Error fetching port list") return self._get_and_munchify('ports', data) @@ -1736,7 +1736,7 @@ def list_qos_rule_types(self, filters=None): if not filters: filters = {} resp = self.network.get("/qos/rule-types.json", params=filters) - data = _adapter._json_response( + data = proxy._json_response( resp, error_message="Error fetching QoS rule types list") return self._get_and_munchify('rule_types', data) @@ -1761,7 +1761,7 @@ def get_qos_rule_type_details(self, rule_type, filters=None): resp = self.network.get( "/qos/rule-types/{rule_type}.json".format(rule_type=rule_type)) - data = _adapter._json_response( + data = proxy._json_response( resp, error_message="Error fetching QoS details of {rule_type} " "rule type".format(rule_type=rule_type)) @@ -1781,7 +1781,7 @@ def list_qos_policies(self, filters=None): if not filters: filters = {} resp = self.network.get("/qos/policies.json", params=filters) - data = _adapter._json_response( + data = proxy._json_response( resp, error_message="Error fetching QoS policies list") return self._get_and_munchify('policies', data) @@ -1867,7 +1867,7 @@ def list_availability_zone_names(self, unavailable=False): list could not be fetched. """ try: - data = _adapter._json_response( + data = proxy._json_response( self.compute.get('/os-availability-zone')) except exc.OpenStackCloudHTTPError: self.log.debug( @@ -1892,7 +1892,7 @@ def list_flavors(self, get_extra=False): :returns: A list of flavor ``munch.Munch``. """ - data = _adapter._json_response( + data = proxy._json_response( self.compute.get( '/flavors/detail', params=dict(is_public='None')), error_message="Error fetching flavor list") @@ -1904,7 +1904,7 @@ def list_flavors(self, get_extra=False): endpoint = "/flavors/{id}/os-extra_specs".format( id=flavor.id) try: - data = _adapter._json_response( + data = proxy._json_response( self.compute.get(endpoint), error_message="Error fetching flavor extra specs") flavor.extra_specs = self._get_and_munchify( @@ -1941,7 +1941,7 @@ def list_server_security_groups(self, server): if not self._has_secgroups(): return [] - data = _adapter._json_response( + data = proxy._json_response( self.compute.get( '/servers/{server_id}/os-security-groups'.format( server_id=server['id']))) @@ -1998,7 +1998,7 @@ def add_server_security_groups(self, server, security_groups): return False for sg in security_groups: - _adapter._json_response(self.compute.post( + proxy._json_response(self.compute.post( '/servers/%s/action' % server['id'], json={'addSecurityGroup': {'name': sg.name}})) @@ -2026,7 +2026,7 @@ def remove_server_security_groups(self, server, security_groups): for sg in security_groups: try: - _adapter._json_response(self.compute.post( + proxy._json_response(self.compute.post( '/servers/%s/action' % server['id'], json={'removeSecurityGroup': {'name': sg.name}})) @@ -2062,7 +2062,7 @@ def list_security_groups(self, filters=None): if self._use_neutron_secgroups(): # Neutron returns dicts, so no need to convert objects here. resp = self.network.get('/security-groups.json', params=filters) - data = _adapter._json_response( + data = proxy._json_response( resp, error_message="Error fetching security group list") return self._normalize_secgroups( @@ -2070,7 +2070,7 @@ def list_security_groups(self, filters=None): # Handle nova security groups else: - data = _adapter._json_response(self.compute.get( + data = proxy._json_response(self.compute.get( '/os-security-groups', params=filters)) return self._normalize_secgroups( self._get_and_munchify('security_groups', data)) @@ -2147,7 +2147,7 @@ def list_server_groups(self): :returns: A list of server group dicts. """ - data = _adapter._json_response( + data = proxy._json_response( self.compute.get('/os-server-groups'), error_message="Error fetching server group list") return self._get_and_munchify('server_groups', data) @@ -2174,7 +2174,7 @@ def get_compute_limits(self, name_or_id=None): error_msg = "{msg} for the project: {project} ".format( msg=error_msg, project=name_or_id) - data = _adapter._json_response( + data = proxy._json_response( self.compute.get('/limits', params=params)) limits = self._get_and_munchify('limits', data) return self._normalize_compute_limits(limits, project_id=project_id) @@ -2209,7 +2209,7 @@ def list_images(self, filter_deleted=True, show_all=False): except keystoneauth1.exceptions.catalog.EndpointNotFound: # We didn't have glance, let's try nova # If this doesn't work - we just let the exception propagate - response = _adapter._json_response( + response = proxy._json_response( self.compute.get('/images/detail')) while 'next' in response: image_list.extend(meta.obj_list_to_munch(response['images'])) @@ -2250,7 +2250,7 @@ def list_floating_ip_pools(self): raise exc.OpenStackCloudUnavailableExtension( 'Floating IP pools extension is not available on target cloud') - data = _adapter._json_response( + data = proxy._json_response( self.compute.get('os-floating-ip-pools'), error_message="Error fetching floating IP pool list") pools = self._get_and_munchify('floating_ip_pools', data) @@ -2344,7 +2344,7 @@ def _neutron_list_floating_ips(self, filters=None): def _nova_list_floating_ips(self): try: - data = _adapter._json_response( + data = proxy._json_response( self.compute.get('/os-floating-ips')) except exc.OpenStackCloudURINotFound: return [] @@ -2746,7 +2746,7 @@ def get_network_by_id(self, id): :returns: A network ``munch.Munch``. """ resp = self.network.get('/networks/{id}'.format(id=id)) - data = _adapter._json_response( + data = proxy._json_response( resp, error_message="Error getting network with ID {id}".format(id=id) ) @@ -2807,7 +2807,7 @@ def get_subnet_by_id(self, id): :returns: A subnet ``munch.Munch``. """ resp = self.network.get('/subnets/{id}'.format(id=id)) - data = _adapter._json_response( + data = proxy._json_response( resp, error_message="Error getting subnet with ID {id}".format(id=id) ) @@ -2846,7 +2846,7 @@ def get_port_by_id(self, id): :returns: A port ``munch.Munch``. """ resp = self.network.get('/ports/{id}'.format(id=id)) - data = _adapter._json_response( + data = proxy._json_response( resp, error_message="Error getting port with ID {id}".format(id=id) ) @@ -2985,7 +2985,7 @@ def get_flavor_by_id(self, id, get_extra=False): specs. :returns: A flavor ``munch.Munch``. """ - data = _adapter._json_response( + data = proxy._json_response( self.compute.get('/flavors/{id}'.format(id=id)), error_message="Error getting flavor with ID {id}".format(id=id) ) @@ -2996,7 +2996,7 @@ def get_flavor_by_id(self, id, get_extra=False): endpoint = "/flavors/{id}/os-extra_specs".format( id=flavor.id) try: - data = _adapter._json_response( + data = proxy._json_response( self.compute.get(endpoint), error_message="Error fetching flavor extra specs") flavor.extra_specs = self._get_and_munchify( @@ -3049,9 +3049,9 @@ def get_security_group_by_id(self, id): " ID {id}".format(id=id)) if self._use_neutron_secgroups(): resp = self.network.get('/security-groups/{id}'.format(id=id)) - data = _adapter._json_response(resp, error_message=error_message) + data = proxy._json_response(resp, error_message=error_message) else: - data = _adapter._json_response( + data = proxy._json_response( self.compute.get( '/os-security-groups/{id}'.format(id=id)), error_message=error_message) @@ -3085,7 +3085,7 @@ def get_server_console(self, server, length=None): return "" def _get_server_console_output(self, server_id, length=None): - data = _adapter._json_response(self.compute.post( + data = proxy._json_response(self.compute.post( '/servers/{server_id}/action'.format(server_id=server_id), json={'os-getConsoleOutput': {'length': length}})) return self._get_and_munchify('output', data) @@ -3138,7 +3138,7 @@ def _expand_server(self, server, detailed, bare): return meta.add_server_interfaces(self, server) def get_server_by_id(self, id): - data = _adapter._json_response( + data = proxy._json_response( self.compute.get('/servers/{id}'.format(id=id))) server = self._get_and_munchify('server', data) return meta.add_server_interfaces(self, self._normalize_server(server)) @@ -3291,13 +3291,13 @@ def get_floating_ip_by_id(self, id): error_message = "Error getting floating ip with ID {id}".format(id=id) if self._use_neutron_floating(): - data = _adapter._json_response( + data = proxy._json_response( self.network.get('/floatingips/{id}'.format(id=id)), error_message=error_message) return self._normalize_floating_ip( self._get_and_munchify('floatingip', data)) else: - data = _adapter._json_response( + data = proxy._json_response( self.compute.get('/os-floating-ips/{id}'.format(id=id)), error_message=error_message) return self._normalize_floating_ip( @@ -3353,7 +3353,7 @@ def create_keypair(self, name, public_key=None): } if public_key: keypair['public_key'] = public_key - data = _adapter._json_response( + data = proxy._json_response( self.compute.post( '/os-keypairs', json={'keypair': keypair}), @@ -3371,7 +3371,7 @@ def delete_keypair(self, name): :raises: OpenStackCloudException on operation error. """ try: - _adapter._json_response(self.compute.delete( + proxy._json_response(self.compute.delete( '/os-keypairs/{name}'.format(name=name))) except exc.OpenStackCloudURINotFound: self.log.debug("Keypair %s not found for deleting", name) @@ -3517,7 +3517,7 @@ def update_network(self, name_or_id, **kwargs): raise exc.OpenStackCloudException( "Network %s not found." % name_or_id) - data = _adapter._json_response(self.network.put( + data = proxy._json_response(self.network.put( "/networks/{net_id}.json".format(net_id=network.id), json={"network": kwargs}), error_message="Error updating network {0}".format(name_or_id)) @@ -3694,7 +3694,7 @@ def list_qos_bandwidth_limit_rules(self, policy_name_or_id, filters=None): "/qos/policies/{policy_id}/bandwidth_limit_rules.json".format( policy_id=policy['id']), params=filters) - data = _adapter._json_response( + data = proxy._json_response( resp, error_message="Error fetching QoS bandwidth limit rules from " "{policy}".format(policy=policy['id'])) @@ -3724,7 +3724,7 @@ def get_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): resp = self.network.get( "/qos/policies/{policy_id}/bandwidth_limit_rules/{rule_id}.json". format(policy_id=policy['id'], rule_id=rule_id)) - data = _adapter._json_response( + data = proxy._json_response( resp, error_message="Error fetching QoS bandwidth limit rule {rule_id} " "from {policy}".format(rule_id=rule_id, @@ -3903,7 +3903,7 @@ def list_qos_dscp_marking_rules(self, policy_name_or_id, filters=None): "/qos/policies/{policy_id}/dscp_marking_rules.json".format( policy_id=policy['id']), params=filters) - data = _adapter._json_response( + data = proxy._json_response( resp, error_message="Error fetching QoS DSCP marking rules from " "{policy}".format(policy=policy['id'])) @@ -3933,7 +3933,7 @@ def get_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): resp = self.network.get( "/qos/policies/{policy_id}/dscp_marking_rules/{rule_id}.json". format(policy_id=policy['id'], rule_id=rule_id)) - data = _adapter._json_response( + data = proxy._json_response( resp, error_message="Error fetching QoS DSCP marking rule {rule_id} " "from {policy}".format(rule_id=rule_id, @@ -4092,7 +4092,7 @@ def list_qos_minimum_bandwidth_rules(self, policy_name_or_id, "/qos/policies/{policy_id}/minimum_bandwidth_rules.json".format( policy_id=policy['id']), params=filters) - data = _adapter._json_response( + data = proxy._json_response( resp, error_message="Error fetching QoS minimum bandwidth rules from " "{policy}".format(policy=policy['id'])) @@ -4122,7 +4122,7 @@ def get_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): resp = self.network.get( "/qos/policies/{policy_id}/minimum_bandwidth_rules/{rule_id}.json". format(policy_id=policy['id'], rule_id=rule_id)) - data = _adapter._json_response( + data = proxy._json_response( resp, error_message="Error fetching QoS minimum_bandwidth rule {rule_id}" " from {policy}".format(rule_id=rule_id, @@ -4271,7 +4271,7 @@ def add_router_interface(self, router, subnet_id=None, port_id=None): if port_id: json_body['port_id'] = port_id - return _adapter._json_response( + return proxy._json_response( self.network.put( "/routers/{router_id}/add_router_interface.json".format( router_id=router['id']), @@ -4395,7 +4395,7 @@ def create_router(self, name=None, admin_state_up=True, 'target cloud') router['availability_zone_hints'] = availability_zone_hints - data = _adapter._json_response( + data = proxy._json_response( self.network.post("/routers.json", json={"router": router}), error_message="Error creating router {0}".format(name)) return self._get_and_munchify('router', data) @@ -4464,7 +4464,7 @@ def update_router(self, name_or_id, name=None, admin_state_up=None, resp = self.network.put( "/routers/{router_id}.json".format(router_id=curr_router['id']), json={"router": router}) - data = _adapter._json_response( + data = proxy._json_response( resp, error_message="Error updating router {0}".format(name_or_id)) return self._get_and_munchify('router', data) @@ -4542,7 +4542,7 @@ def create_image_snapshot( "Server {server} could not be found and therefore" " could not be snapshotted.".format(server=server)) server = server_obj - response = _adapter._json_response( + response = proxy._json_response( self.compute.post( '/servers/{server_id}/action'.format(server_id=server['id']), json={ @@ -4998,7 +4998,7 @@ def detach_volume(self, server, volume, wait=True, timeout=None): :raises: OpenStackCloudException on operation error. """ - _adapter._json_response(self.compute.delete( + proxy._json_response(self.compute.delete( '/servers/{server_id}/os-volume_attachments/{volume_id}'.format( server_id=server['id'], volume_id=volume['id'])), error_message=( @@ -5065,7 +5065,7 @@ def attach_volume(self, server, volume, device=None, payload = {'volumeId': volume['id']} if device: payload['device'] = device - data = _adapter._json_response( + data = proxy._json_response( self.compute.post( '/servers/{server_id}/os-volume_attachments'.format( server_id=server['id']), @@ -5713,11 +5713,11 @@ def _nova_create_floating_ip(self, pool=None): "unable to find a floating ip pool") pool = pools[0]['name'] - data = _adapter._json_response(self.compute.post( + data = proxy._json_response(self.compute.post( '/os-floating-ips', json=dict(pool=pool))) pool_ip = self._get_and_munchify('floating_ip', data) # TODO(mordred) Remove this - it's just for compat - data = _adapter._json_response( + data = proxy._json_response( self.compute.get('/os-floating-ips/{id}'.format( id=pool_ip['id']))) return self._get_and_munchify('floating_ip', data) @@ -5774,7 +5774,7 @@ def _delete_floating_ip(self, floating_ip_id): def _neutron_delete_floating_ip(self, floating_ip_id): try: - _adapter._json_response(self.network.delete( + proxy._json_response(self.network.delete( "/floatingips/{fip_id}.json".format(fip_id=floating_ip_id), error_message="unable to delete floating IP")) except exc.OpenStackCloudResourceNotFound: @@ -5787,7 +5787,7 @@ def _neutron_delete_floating_ip(self, floating_ip_id): def _nova_delete_floating_ip(self, floating_ip_id): try: - _adapter._json_response( + proxy._json_response( self.compute.delete( '/os-floating-ips/{id}'.format(id=floating_ip_id)), error_message='Unable to delete floating IP {fip_id}'.format( @@ -6015,7 +6015,7 @@ def _neutron_attach_ip_to_server( if fixed_address is not None: floating_ip_args['fixed_ip_address'] = fixed_address - return _adapter._json_response( + return proxy._json_response( self.network.put( "/floatingips/{fip_id}.json".format(fip_id=floating_ip['id']), json={'floatingip': floating_ip_args}), @@ -6038,7 +6038,7 @@ def _nova_attach_ip_to_server(self, server_id, floating_ip_id, } if fixed_address: body['fixed_address'] = fixed_address - return _adapter._json_response( + return proxy._json_response( self.compute.post( '/servers/{server_id}/action'.format(server_id=server_id), json=dict(addFloatingIp=body)), @@ -6091,7 +6091,7 @@ def _nova_detach_ip_from_server(self, server_id, floating_ip_id): "unable to find floating IP {0}".format(floating_ip_id)) error_message = "Error detaching IP {ip} from instance {id}".format( ip=floating_ip_id, id=server_id) - return _adapter._json_response( + return proxy._json_response( self.compute.post( '/servers/{server_id}/action'.format(server_id=server_id), json=dict(removeFloatingIp=dict( @@ -6667,7 +6667,7 @@ def create_server( if 'block_device_mapping_v2' in kwargs: endpoint = '/os-volumes_boot' with _utils.shade_exceptions("Error in creating instance"): - data = _adapter._json_response( + data = proxy._json_response( self.compute.post(endpoint, json=server_json)) server = self._get_and_munchify('server', data) admin_pass = server.get('adminPass') or kwargs.get('admin_pass') @@ -6788,7 +6788,7 @@ def rebuild_server(self, server_id, image_id, admin_pass=None, if admin_pass: kwargs['adminPass'] = admin_pass - data = _adapter._json_response( + data = proxy._json_response( self.compute.post( '/servers/{server_id}/action'.format(server_id=server_id), json={'rebuild': kwargs}), @@ -6838,7 +6838,7 @@ def set_server_metadata(self, name_or_id, metadata): raise exc.OpenStackCloudException( 'Invalid Server {server}'.format(server=name_or_id)) - _adapter._json_response( + proxy._json_response( self.compute.post( '/servers/{server_id}/metadata'.format(server_id=server['id']), json={'metadata': metadata}), @@ -6862,7 +6862,7 @@ def delete_server_metadata(self, name_or_id, metadata_keys): for key in metadata_keys: error_message = 'Error deleting metadata {key} on {server}'.format( key=key, server=name_or_id) - _adapter._json_response( + proxy._json_response( self.compute.delete( '/servers/{server_id}/metadata/{key}'.format( server_id=server['id'], @@ -6936,7 +6936,7 @@ def _delete_server( self._delete_server_floating_ips(server, delete_ip_retry) try: - _adapter._json_response( + proxy._json_response( self.compute.delete( '/servers/{id}'.format(id=server['id'])), error_message="Error in deleting server") @@ -7001,7 +7001,7 @@ def update_server(self, name_or_id, detailed=False, bare=False, **kwargs): raise exc.OpenStackCloudException( "failed to find server '{server}'".format(server=name_or_id)) - data = _adapter._json_response( + data = proxy._json_response( self.compute.put( '/servers/{server_id}'.format(server_id=server['id']), json={'server': kwargs}), @@ -7020,7 +7020,7 @@ def create_server_group(self, name, policies): :raises: OpenStackCloudException on operation error. """ - data = _adapter._json_response( + data = proxy._json_response( self.compute.post( '/os-server-groups', json={ @@ -7046,7 +7046,7 @@ def delete_server_group(self, name_or_id): name_or_id) return False - _adapter._json_response( + proxy._json_response( self.compute.delete( '/os-server-groups/{id}'.format(id=server_group['id'])), error_message="Error deleting server group {name}".format( @@ -7912,7 +7912,7 @@ def create_port(self, network_id, **kwargs): """ kwargs['network_id'] = network_id - data = _adapter._json_response( + data = proxy._json_response( self.network.post("/ports.json", json={'port': kwargs}), error_message="Error creating port for network {0}".format( network_id)) @@ -7980,7 +7980,7 @@ def update_port(self, name_or_id, **kwargs): raise exc.OpenStackCloudException( "failed to find port '{port}'".format(port=name_or_id)) - data = _adapter._json_response( + data = proxy._json_response( self.network.put( "/ports/{port_id}.json".format(port_id=port['id']), json={"port": kwargs}), @@ -8037,13 +8037,13 @@ def create_security_group(self, name, description, project_id=None): if project_id is not None: security_group_json['security_group']['tenant_id'] = project_id if self._use_neutron_secgroups(): - data = _adapter._json_response( + data = proxy._json_response( self.network.post( '/security-groups.json', json=security_group_json), error_message="Error creating security group {0}".format(name)) else: - data = _adapter._json_response(self.compute.post( + data = proxy._json_response(self.compute.post( '/os-security-groups', json=security_group_json)) return self._normalize_secgroup( self._get_and_munchify('security_group', data)) @@ -8084,7 +8084,7 @@ def delete_security_group(self, name_or_id): return True else: - _adapter._json_response(self.compute.delete( + proxy._json_response(self.compute.delete( '/os-security-groups/{id}'.format(id=secgroup['id']))) return True @@ -8113,7 +8113,7 @@ def update_security_group(self, name_or_id, **kwargs): "Security group %s not found." % name_or_id) if self._use_neutron_secgroups(): - data = _adapter._json_response( + data = proxy._json_response( self.network.put( '/security-groups/{sg_id}.json'.format(sg_id=group['id']), json={'security_group': kwargs}), @@ -8122,7 +8122,7 @@ def update_security_group(self, name_or_id, **kwargs): else: for key in ('name', 'description'): kwargs.setdefault(key, group[key]) - data = _adapter._json_response( + data = proxy._json_response( self.compute.put( '/os-security-groups/{id}'.format(id=group['id']), json={'security_group': kwargs})) @@ -8213,7 +8213,7 @@ def create_security_group_rule(self, if project_id is not None: rule_def['tenant_id'] = project_id - data = _adapter._json_response( + data = proxy._json_response( self.network.post( '/security-group-rules.json', json={'security_group_rule': rule_def}), @@ -8259,7 +8259,7 @@ def create_security_group_rule(self, if project_id is not None: security_group_rule_dict[ 'security_group_rule']['tenant_id'] = project_id - data = _adapter._json_response( + data = proxy._json_response( self.compute.post( '/os-security-group-rules', json=security_group_rule_dict @@ -10495,7 +10495,7 @@ def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", } if flavorid == 'auto': payload['id'] = None - data = _adapter._json_response(self.compute.post( + data = proxy._json_response(self.compute.post( '/flavors', json=dict(flavor=payload))) @@ -10517,7 +10517,7 @@ def delete_flavor(self, name_or_id): "Flavor %s not found for deleting", name_or_id) return False - _adapter._json_response( + proxy._json_response( self.compute.delete( '/flavors/{id}'.format(id=flavor['id'])), error_message="Unable to delete flavor {name}".format( @@ -10534,7 +10534,7 @@ def set_flavor_specs(self, flavor_id, extra_specs): :raises: OpenStackCloudException on operation error. :raises: OpenStackCloudResourceNotFound if flavor ID is not found. """ - _adapter._json_response( + proxy._json_response( self.compute.post( "/flavors/{id}/os-extra_specs".format(id=flavor_id), json=dict(extra_specs=extra_specs)), @@ -10550,7 +10550,7 @@ def unset_flavor_specs(self, flavor_id, keys): :raises: OpenStackCloudResourceNotFound if flavor ID is not found. """ for key in keys: - _adapter._json_response( + proxy._json_response( self.compute.delete( "/flavors/{id}/os-extra_specs/{key}".format( id=flavor_id, key=key)), @@ -10566,7 +10566,7 @@ def _mod_flavor_access(self, action, flavor_id, project_id): access = {'tenant': project_id} access_key = '{action}TenantAccess'.format(action=action) - _adapter._json_response( + proxy._json_response( self.compute.post(endpoint, json={access_key: access})) def add_flavor_access(self, flavor_id, project_id): @@ -10598,7 +10598,7 @@ def list_flavor_access(self, flavor_id): :raises: OpenStackCloudException on operation error. """ - data = _adapter._json_response( + data = proxy._json_response( self.compute.get( '/flavors/{id}/os-flavor-access'.format(id=flavor_id)), error_message=( @@ -10882,7 +10882,7 @@ def list_hypervisors(self): :returns: A list of hypervisor ``munch.Munch``. """ - data = _adapter._json_response( + data = proxy._json_response( self.compute.get('/os-hypervisors/detail'), error_message="Error fetching hypervisor list") return self._get_and_munchify('hypervisors', data) @@ -10907,7 +10907,7 @@ def list_aggregates(self): :returns: A list of aggregate dicts. """ - data = _adapter._json_response( + data = proxy._json_response( self.compute.get('/os-aggregates'), error_message="Error fetching aggregate list") return self._get_and_munchify('aggregates', data) @@ -10943,7 +10943,7 @@ def create_aggregate(self, name, availability_zone=None): :raises: OpenStackCloudException on operation error. """ - data = _adapter._json_response( + data = proxy._json_response( self.compute.post( '/os-aggregates', json={'aggregate': { @@ -10971,7 +10971,7 @@ def update_aggregate(self, name_or_id, **kwargs): raise exc.OpenStackCloudException( "Host aggregate %s not found." % name_or_id) - data = _adapter._json_response( + data = proxy._json_response( self.compute.put( '/os-aggregates/{id}'.format(id=aggregate['id']), json={'aggregate': kwargs}), @@ -10993,7 +10993,7 @@ def delete_aggregate(self, name_or_id): self.log.debug("Aggregate %s not found for deleting", name_or_id) return False - return _adapter._json_response( + return proxy._json_response( self.compute.delete( '/os-aggregates/{id}'.format(id=aggregate['id'])), error_message="Error deleting aggregate {name}".format( @@ -11020,7 +11020,7 @@ def set_aggregate_metadata(self, name_or_id, metadata): err_msg = "Unable to set metadata for host aggregate {name}".format( name=name_or_id) - data = _adapter._json_response( + data = proxy._json_response( self.compute.post( '/os-aggregates/{id}/action'.format(id=aggregate['id']), json={'set_metadata': {'metadata': metadata}}), @@ -11043,7 +11043,7 @@ def add_host_to_aggregate(self, name_or_id, host_name): err_msg = "Unable to add host {host} to aggregate {name}".format( host=host_name, name=name_or_id) - return _adapter._json_response( + return proxy._json_response( self.compute.post( '/os-aggregates/{id}/action'.format(id=aggregate['id']), json={'add_host': {'host': host_name}}), @@ -11065,7 +11065,7 @@ def remove_host_from_aggregate(self, name_or_id, host_name): err_msg = "Unable to remove host {host} to aggregate {name}".format( host=host_name, name=name_or_id) - return _adapter._json_response( + return proxy._json_response( self.compute.post( '/os-aggregates/{id}/action'.format(id=aggregate['id']), json={'remove_host': {'host': host_name}}), @@ -11157,7 +11157,7 @@ def set_compute_quotas(self, name_or_id, **kwargs): # if key in quota.VOLUME_QUOTAS} kwargs['force'] = True - _adapter._json_response( + proxy._json_response( self.compute.put( '/os-quota-sets/{project}'.format(project=proj.id), json={'quota_set': kwargs}), @@ -11174,7 +11174,7 @@ def get_compute_quotas(self, name_or_id): proj = self.get_project(name_or_id) if not proj: raise exc.OpenStackCloudException("project does not exist") - data = _adapter._json_response( + data = proxy._json_response( self.compute.get( '/os-quota-sets/{project}'.format(project=proj.id))) return self._get_and_munchify('quota_set', data) @@ -11191,7 +11191,7 @@ def delete_compute_quotas(self, name_or_id): proj = self.get_project(name_or_id) if not proj: raise exc.OpenStackCloudException("project does not exist") - return _adapter._json_response( + return proxy._json_response( self.compute.delete( '/os-quota-sets/{project}'.format(project=proj.id))) @@ -11251,7 +11251,7 @@ def parse_datetime_for_nova(date): raise exc.OpenStackCloudException( "project does not exist: {}".format(name=proj.id)) - data = _adapter._json_response( + data = proxy._json_response( self.compute.get( '/os-simple-tenant-usage/{project}'.format(project=proj.id), params=dict(start=start.isoformat(), end=end.isoformat())), @@ -11352,7 +11352,7 @@ def get_network_quotas(self, name_or_id, details=False): if details: url = url + "/details" url = url + ".json" - data = _adapter._json_response( + data = proxy._json_response( self.network.get(url), error_message=("Error fetching Neutron's quota for " "project {0}".format(proj.id))) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index e96cabe32..50c13bc04 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -22,12 +22,12 @@ import requestsexceptions from six.moves import urllib -from openstack import _adapter from openstack import version as openstack_version from openstack import _log from openstack.config import _util from openstack.config import defaults as config_defaults from openstack import exceptions +from openstack import proxy def _make_key(key, service_type): @@ -450,7 +450,7 @@ def get_all_version_data(self, service_type): def get_session_client( self, service_type, version=None, - constructor=_adapter.OpenStackSDKAdapter, + constructor=proxy.Proxy, **kwargs): """Return a prepped keystoneauth Adapter for a given service. diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index 5221b303d..dc4800ebd 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -18,7 +18,6 @@ from openstack.object_store.v1 import container as _container from openstack.object_store.v1 import obj as _obj from openstack.object_store.v1 import info as _info -from openstack import _adapter from openstack import exceptions from openstack import _log from openstack import proxy @@ -568,7 +567,7 @@ def _finish_large_object_dlo(self, endpoint, headers): def _upload_object(self, endpoint, filename, headers): with open(filename, 'rb') as dt: - return _adapter._json_response(self.put( + return proxy._json_response(self.put( endpoint, headers=headers, data=dt)) def _get_file_segments(self, endpoint, filename, file_size, segment_size): diff --git a/openstack/proxy.py b/openstack/proxy.py index a3e7364a8..5bc23f85e 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -10,11 +10,80 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import _adapter +try: + import simplejson + JSONDecodeError = simplejson.scanner.JSONDecodeError +except ImportError: + JSONDecodeError = ValueError +from six.moves import urllib + +from keystoneauth1 import adapter + from openstack import exceptions from openstack import resource +def _extract_name(url, service_type=None): + '''Produce a key name to use in logging/metrics from the URL path. + + We want to be able to logic/metric sane general things, so we pull + the url apart to generate names. The function returns a list because + there are two different ways in which the elements want to be combined + below (one for logging, one for statsd) + + Some examples are likely useful: + + /servers -> ['servers'] + /servers/{id} -> ['servers'] + /servers/{id}/os-security-groups -> ['servers', 'os-security-groups'] + /v2.0/networks.json -> ['networks'] + ''' + + url_path = urllib.parse.urlparse(url).path.strip() + # Remove / from the beginning to keep the list indexes of interesting + # things consistent + if url_path.startswith('/'): + url_path = url_path[1:] + + # Special case for neutron, which puts .json on the end of urls + if url_path.endswith('.json'): + url_path = url_path[:-len('.json')] + + url_parts = url_path.split('/') + if url_parts[-1] == 'detail': + # Special case detail calls + # GET /servers/detail + # returns ['servers', 'detail'] + name_parts = url_parts[-2:] + else: + # Strip leading version piece so that + # GET /v2.0/networks + # returns ['networks'] + if url_parts[0] in ('v1', 'v2', 'v2.0'): + url_parts = url_parts[1:] + name_parts = [] + # Pull out every other URL portion - so that + # GET /servers/{id}/os-security-groups + # returns ['servers', 'os-security-groups'] + for idx in range(0, len(url_parts)): + if not idx % 2 and url_parts[idx]: + name_parts.append(url_parts[idx]) + + # Keystone Token fetching is a special case, so we name it "tokens" + if url_path.endswith('tokens'): + name_parts = ['tokens'] + + # Getting the root of an endpoint is doing version discovery + if not name_parts: + if service_type == 'object-store': + name_parts = ['account'] + else: + name_parts = ['discovery'] + + # Strip out anything that's empty or None + return [part for part in name_parts if part] + + # The _check_resource decorator is used on Proxy methods to ensure that # the `actual` argument is in fact the type of the `expected` argument. # It does so under two cases: @@ -39,7 +108,7 @@ def check(self, expected, actual=None, *args, **kwargs): return wrap -class Proxy(_adapter.OpenStackSDKAdapter): +class Proxy(adapter.Adapter): """Represents a service.""" retriable_status_codes = None @@ -56,6 +125,21 @@ def __init__(self, *args, **kwargs): self.retriable_status_codes) super(Proxy, self).__init__(*args, **kwargs) + def request( + self, url, method, error_message=None, + raise_exc=False, connect_retries=1, *args, **kwargs): + response = super(Proxy, self).request( + url, method, + connect_retries=connect_retries, raise_exc=False, + **kwargs) + return response + + def _version_matches(self, version): + api_version = self.get_api_major_version() + if api_version: + return api_version[0] == version + return False + def _get_connection(self): """Get the Connection object associated with this Proxy. @@ -307,3 +391,30 @@ def _head(self, resource_type, value=None, base_path=None, **attrs): """ res = self._get_resource(resource_type, value, **attrs) return res.head(self, base_path=base_path) + + +def _json_response(response, result_key=None, error_message=None): + """Temporary method to use to bridge from ShadeAdapter to SDK calls.""" + exceptions.raise_from_response(response, error_message=error_message) + + if not response.content: + # This doesn't have any content + return response + + # Some REST calls do not return json content. Don't decode it. + if 'application/json' not in response.headers.get('Content-Type'): + return response + + try: + result_json = response.json() + except JSONDecodeError: + return response + return result_json + + +class _ShadeAdapter(Proxy): + """Wrapper for shade methods that expect json unpacking.""" + + def request(self, url, method, error_message=None, **kwargs): + response = super(_ShadeAdapter, self).request(url, method, **kwargs) + return _json_response(response, error_message=error_message) diff --git a/openstack/service_description.py b/openstack/service_description.py index 206affce8..bfe46a7f5 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -17,7 +17,6 @@ from openstack import _log from openstack import exceptions -from openstack import proxy __all__ = [ 'ServiceDescription', @@ -55,11 +54,6 @@ def __init__(self, service_type, supported_versions=None, aliases=None): :param string service_type: service_type to look for in the keystone catalog - :param proxy.Proxy proxy_class: - subclass of :class:`~openstack.proxy.Proxy` implementing - an interface for this service. Defaults to - :class:`~openstack.proxy.Proxy` which provides REST operations - but no additional features. :param list aliases: Optional list of aliases, if there is more than one name that might be used to register the service in the catalog. @@ -179,7 +173,6 @@ def _make_proxy(self, instance): temp_adapter = config.get_session_client( self.service_type, - constructor=proxy.Proxy, allow_version_hack=True, **version_kwargs ) @@ -197,11 +190,10 @@ def _make_proxy(self, instance): category=exceptions.UnsupportedServiceVersion) return temp_adapter proxy_class = self.supported_versions.get(str(found_version[0])) - if not proxy_class: - proxy_class = proxy.Proxy + if proxy_class: + version_kwargs['constructor'] = proxy_class return config.get_session_client( self.service_type, - constructor=proxy_class, allow_version_hack=True, **version_kwargs ) diff --git a/openstack/tests/functional/cloud/test_floating_ip.py b/openstack/tests/functional/cloud/test_floating_ip.py index 2be3bcea7..89a0ed4cb 100644 --- a/openstack/tests/functional/cloud/test_floating_ip.py +++ b/openstack/tests/functional/cloud/test_floating_ip.py @@ -25,9 +25,9 @@ from testtools import content -from openstack import _adapter from openstack.cloud import meta from openstack.cloud.exc import OpenStackCloudException +from openstack import proxy from openstack.tests.functional import base from openstack.tests.functional.cloud.util import pick_flavor from openstack import utils @@ -175,7 +175,7 @@ def _setup_networks(self): self.user_cloud.list_networks()))) else: # Find network names for nova-net - data = _adapter._json_response( + data = proxy._json_response( self.user_cloud._conn.compute.get('/os-tenant-networks')) nets = meta.get_and_munchify('networks', data) self.addDetail( diff --git a/openstack/tests/unit/test__adapter.py b/openstack/tests/unit/test__adapter.py deleted file mode 100644 index 67cbbf744..000000000 --- a/openstack/tests/unit/test__adapter.py +++ /dev/null @@ -1,38 +0,0 @@ -# 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 testscenarios import load_tests_apply_scenarios as load_tests # noqa - -from openstack import _adapter -from openstack.tests.unit import base - - -class TestExtractName(base.TestCase): - - scenarios = [ - ('slash_servers_bare', dict(url='/servers', parts=['servers'])), - ('slash_servers_arg', dict(url='/servers/1', parts=['servers'])), - ('servers_bare', dict(url='servers', parts=['servers'])), - ('servers_arg', dict(url='servers/1', parts=['servers'])), - ('networks_bare', dict(url='/v2.0/networks', parts=['networks'])), - ('networks_arg', dict(url='/v2.0/networks/1', parts=['networks'])), - ('tokens', dict(url='/v3/tokens', parts=['tokens'])), - ('discovery', dict(url='/', parts=['discovery'])), - ('secgroups', dict( - url='/servers/1/os-security-groups', - parts=['servers', 'os-security-groups'])), - ] - - def test_extract_name(self): - - results = _adapter._extract_name(self.url) - self.assertEqual(self.parts, results) diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index abf6d1cfb..bb56d9f5c 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -9,6 +9,7 @@ # 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 testscenarios import load_tests_apply_scenarios as load_tests # noqa import mock import munch @@ -460,3 +461,25 @@ def test_head_id(self): connection=self.cloud, id=self.fake_id) self.res.head.assert_called_with(self.sot, base_path=None) self.assertEqual(rv, self.fake_result) + + +class TestExtractName(base.TestCase): + + scenarios = [ + ('slash_servers_bare', dict(url='/servers', parts=['servers'])), + ('slash_servers_arg', dict(url='/servers/1', parts=['servers'])), + ('servers_bare', dict(url='servers', parts=['servers'])), + ('servers_arg', dict(url='servers/1', parts=['servers'])), + ('networks_bare', dict(url='/v2.0/networks', parts=['networks'])), + ('networks_arg', dict(url='/v2.0/networks/1', parts=['networks'])), + ('tokens', dict(url='/v3/tokens', parts=['tokens'])), + ('discovery', dict(url='/', parts=['discovery'])), + ('secgroups', dict( + url='/servers/1/os-security-groups', + parts=['servers', 'os-security-groups'])), + ] + + def test_extract_name(self): + + results = proxy._extract_name(self.url) + self.assertEqual(self.parts, results) From c8b96cddd3d65b9b79788d93e72fe499f07ffae0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 1 Nov 2018 12:54:43 -0500 Subject: [PATCH 2406/3836] Collect request stats This is a thing that nodepool has been doing for ages. With the upcoming changes to remove the task manager, the mechanism it has been using to put activity in the right place isn't going to be available anymore. But also, people using openstacksdk from within a service might also want to be able to do the same logging. This improves upon the old method as well, as it uses the history in the response object to get and report on all of the calls made as part of a request. This will catch things that do auto retries. While we're in there, add support for reporting to prometheus instead. The prometheus support does not read from config, and does not run an http service, since openstacksdk is a library. It is expected that an application that uses openstacksdk and wants request stats collected will pass a prometheus_client.CollectorRegistry to collector_registry. Change-Id: I7218179dd5f0c068a52a4704b2ce1a0942fdc0d1 --- lower-constraints.txt | 2 + openstack/cloud/openstackcloud.py | 4 + openstack/config/cloud_region.py | 81 +++++- openstack/config/loader.py | 22 +- openstack/proxy.py | 45 ++- openstack/tests/unit/test_stats.py | 268 ++++++++++++++++++ .../notes/request-stats-9d70480bebbdb4d6.yaml | 5 + test-requirements.txt | 2 + 8 files changed, 424 insertions(+), 5 deletions(-) create mode 100644 openstack/tests/unit/test_stats.py create mode 100644 releasenotes/notes/request-stats-9d70480bebbdb4d6.yaml diff --git a/lower-constraints.txt b/lower-constraints.txt index 3de103693..93758a7e4 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -24,6 +24,7 @@ os-client-config==1.28.0 os-service-types==1.2.0 oslotest==3.2.0 pbr==2.0.0 +prometheus-client==0.4.2 Pygments==2.2.0 python-mimeparse==1.6.0 python-subunit==1.0.0 @@ -32,6 +33,7 @@ requests==2.18.0 requests-mock==1.2.0 requestsexceptions==1.2.0 six==1.10.0 +statsd==3.3.0 stestr==1.0.0 stevedore==1.20.0 testrepository==0.0.18 diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 54e0939d6..027fe0373 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -451,6 +451,10 @@ def _get_versioned_client( interface=self.config.get_interface(service_type), endpoint_override=self.config.get_endpoint(service_type), region_name=self.config.region_name, + statsd_prefix=self.config.get_statsd_prefix(), + statsd_client=self.config.get_statsd_client(), + prometheus_counter=self.config.get_prometheus_counter(), + prometheus_histogram=self.config.get_prometheus_histogram(), min_version=request_min_version, max_version=request_max_version) if adapter.get_endpoint(): diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 50c13bc04..8bb208346 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -21,6 +21,15 @@ import os_service_types import requestsexceptions from six.moves import urllib +try: + import statsd +except ImportError: + statsd = None +try: + import prometheus_client +except ImportError: + prometheus_client = None + from openstack import version as openstack_version from openstack import _log @@ -96,7 +105,9 @@ def __init__(self, name=None, region_name=None, config=None, discovery_cache=None, extra_config=None, cache_expiration_time=0, cache_expirations=None, cache_path=None, cache_class='dogpile.cache.null', - cache_arguments=None, password_callback=None): + cache_arguments=None, password_callback=None, + statsd_host=None, statsd_port=None, statsd_prefix=None, + collector_registry=None): self._name = name self.region_name = region_name self.config = _util.normalize_keys(config) @@ -116,6 +127,11 @@ def __init__(self, name=None, region_name=None, config=None, self._cache_class = cache_class self._cache_arguments = cache_arguments self._password_callback = password_callback + self._statsd_host = statsd_host + self._statsd_port = statsd_port + self._statsd_prefix = statsd_prefix + self._statsd_client = None + self._collector_registry = collector_registry self._service_type_manager = os_service_types.ServiceTypes() @@ -471,6 +487,11 @@ def get_session_client( self.get_connect_retries(service_type)) kwargs.setdefault('status_code_retries', self.get_status_code_retries(service_type)) + kwargs.setdefault('statsd_prefix', self.get_statsd_prefix()) + kwargs.setdefault('statsd_client', self.get_statsd_client()) + kwargs.setdefault('prometheus_counter', self.get_prometheus_counter()) + kwargs.setdefault( + 'prometheus_histogram', self.get_prometheus_histogram()) endpoint_override = self.get_endpoint(service_type) version = version_request.version min_api_version = ( @@ -746,3 +767,61 @@ def get_rate_limit(self, service_type=None): def get_concurrency(self, service_type=None): return self._get_service_config( 'concurrency', service_type=service_type) + + def get_statsd_client(self): + if not statsd: + return None + statsd_args = {} + if self._statsd_host: + statsd_args['host'] = self._statsd_host + if self._statsd_port: + statsd_args['port'] = self._statsd_port + if statsd_args: + return statsd.StatsClient(**statsd_args) + else: + return None + + def get_statsd_prefix(self): + return self._statsd_prefix or 'openstack.api' + + def get_prometheus_registry(self): + if not self._collector_registry and prometheus_client: + self._collector_registry = prometheus_client.REGISTRY + return self._collector_registry + + def get_prometheus_histogram(self): + registry = self.get_prometheus_registry() + if not registry or not prometheus_client: + return + # We have to hide a reference to the histogram on the registry + # object, because it's collectors must be singletons for a given + # registry but register at creation time. + hist = getattr(registry, '_openstacksdk_histogram', None) + if not hist: + hist = prometheus_client.Histogram( + 'openstack_http_response_time', + 'Time taken for an http response to an OpenStack service', + labelnames=[ + 'method', 'endpoint', 'service_type', 'status_code' + ], + registry=registry, + ) + registry._openstacksdk_histogram = hist + return hist + + def get_prometheus_counter(self): + registry = self.get_prometheus_registry() + if not registry or not prometheus_client: + return + counter = getattr(registry, '_openstacksdk_counter', None) + if not counter: + counter = prometheus_client.Counter( + 'openstack_http_requests', + 'Number of HTTP requests made to an OpenStack service', + labelnames=[ + 'method', 'endpoint', 'service_type', 'status_code' + ], + registry=registry, + ) + registry._openstacksdk_counter = counter + return counter diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 1b5540d24..42f830fa1 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -140,7 +140,9 @@ def __init__(self, config_files=None, vendor_files=None, envvar_prefix=None, secure_files=None, pw_func=None, session_constructor=None, app_name=None, app_version=None, - load_yaml_config=True, load_envvars=True): + load_yaml_config=True, load_envvars=True, + statsd_host=None, statsd_port=None, + statsd_prefix=None): self.log = _log.setup_logging('openstack.config') self._session_constructor = session_constructor self._app_name = app_name @@ -276,6 +278,21 @@ def __init__(self, config_files=None, vendor_files=None, self._cache_expirations = cache_settings.get( 'expiration', self._cache_expirations) + if load_yaml_config: + statsd_config = self.cloud_config.get('statsd', {}) + statsd_host = statsd_host or statsd_config.get('host') + statsd_port = statsd_port or statsd_config.get('port') + statsd_prefix = statsd_prefix or statsd_config.get('prefix') + + if load_envvars: + statsd_host = statsd_host or os.environ.get('STATSD_HOST') + statsd_port = statsd_port or os.environ.get('STATSD_PORT') + statsd_prefix = statsd_prefix or os.environ.get('STATSD_PREFIX') + + self._statsd_host = statsd_host + self._statsd_port = statsd_port + self._statsd_prefix = statsd_prefix + # Flag location to hold the peeked value of an argparse timeout value self._argv_timeout = False @@ -1091,6 +1108,9 @@ def get_one( cache_class=self._cache_class, cache_arguments=self._cache_arguments, password_callback=self._pw_callback, + statsd_host=self._statsd_host, + statsd_port=self._statsd_port, + statsd_prefix=self._statsd_prefix, ) # TODO(mordred) Backwards compat for OSC transition get_one_cloud = get_one diff --git a/openstack/proxy.py b/openstack/proxy.py index 5bc23f85e..8144e771f 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -59,7 +59,9 @@ def _extract_name(url, service_type=None): # Strip leading version piece so that # GET /v2.0/networks # returns ['networks'] - if url_parts[0] in ('v1', 'v2', 'v2.0'): + if (url_parts[0] + and url_parts[0][0] == 'v' + and url_parts[0][1] and url_parts[0][1].isdigit()): url_parts = url_parts[1:] name_parts = [] # Pull out every other URL portion - so that @@ -118,12 +120,21 @@ class Proxy(adapter.Adapter): ``_status_code_retries``. """ - def __init__(self, *args, **kwargs): + def __init__( + self, + session, + statsd_client=None, statsd_prefix=None, + prometheus_counter=None, prometheus_histogram=None, + *args, **kwargs): # NOTE(dtantsur): keystoneauth defaults retriable_status_codes to None, # override it with a class-level value. kwargs.setdefault('retriable_status_codes', self.retriable_status_codes) - super(Proxy, self).__init__(*args, **kwargs) + super(Proxy, self).__init__(session=session, *args, **kwargs) + self._statsd_client = statsd_client + self._statsd_prefix = statsd_prefix + self._prometheus_counter = prometheus_counter + self._prometheus_histogram = prometheus_histogram def request( self, url, method, error_message=None, @@ -132,8 +143,36 @@ def request( url, method, connect_retries=connect_retries, raise_exc=False, **kwargs) + for h in response.history: + self._report_stats(h) + self._report_stats(response) return response + def _report_stats(self, response): + if self._statsd_client: + self._report_stats_statsd(response) + if self._prometheus_counter and self._prometheus_histogram: + self._report_stats_prometheus(response) + + def _report_stats_statsd(self, response): + name_parts = _extract_name(response.request.url, self.service_type) + key = '.'.join( + [self._statsd_prefix, self.service_type, response.request.method] + + name_parts) + self._statsd_client.timing(key, int(response.elapsed.seconds * 1000)) + self._statsd_client.incr(key) + + def _report_stats_prometheus(self, response): + labels = dict( + method=response.request.method, + endpoint=response.request.url, + service_type=self.service_type, + status_code=response.status_code, + ) + self._prometheus_counter.labels(**labels).inc() + self._prometheus_histogram.labels(**labels).observe( + response.elapsed.seconds) + def _version_matches(self, version): api_version = self.get_api_major_version() if api_version: diff --git a/openstack/tests/unit/test_stats.py b/openstack/tests/unit/test_stats.py new file mode 100644 index 000000000..d8c88c299 --- /dev/null +++ b/openstack/tests/unit/test_stats.py @@ -0,0 +1,268 @@ +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# Copyright 2014 OpenStack Foundation +# Copyright 2018 Red Hat, 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 itertools +import os +import pprint +import threading +import time +import select +import socket + +import fixtures +import prometheus_client +import testtools.content + +from openstack.tests.unit import base + + +class StatsdFixture(fixtures.Fixture): + def _setUp(self): + self.running = True + self.thread = threading.Thread(target=self.run) + self.thread.daemon = True + self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.sock.bind(('', 0)) + self.port = self.sock.getsockname()[1] + self.wake_read, self.wake_write = os.pipe() + self.stats = [] + self.thread.start() + self.addCleanup(self._cleanup) + + def run(self): + while self.running: + poll = select.poll() + poll.register(self.sock, select.POLLIN) + poll.register(self.wake_read, select.POLLIN) + ret = poll.poll() + for (fd, event) in ret: + if fd == self.sock.fileno(): + data = self.sock.recvfrom(1024) + if not data: + return + self.stats.append(data[0]) + if fd == self.wake_read: + return + + def _cleanup(self): + self.running = False + os.write(self.wake_write, b'1\n') + self.thread.join() + + +class TestStats(base.TestCase): + + def setUp(self): + self.statsd = StatsdFixture() + self.useFixture(self.statsd) + # note, use 127.0.0.1 rather than localhost to avoid getting ipv6 + # see: https://github.com/jsocol/pystatsd/issues/61 + self.useFixture( + fixtures.EnvironmentVariable('STATSD_HOST', '127.0.0.1')) + self.useFixture( + fixtures.EnvironmentVariable('STATSD_PORT', str(self.statsd.port))) + + self.add_info_on_exception('statsd_content', self.statsd.stats) + # Set up the above things before the super setup so that we have the + # environment variables set when the Connection is created. + super(TestStats, self).setUp() + + self._registry = prometheus_client.CollectorRegistry() + self.cloud.config._collector_registry = self._registry + self.addOnException(self._add_prometheus_samples) + + def _add_prometheus_samples(self, exc_info): + samples = [] + for metric in self._registry.collect(): + for s in metric.samples: + samples.append(s) + self.addDetail( + 'prometheus_samples', + testtools.content.text_content(pprint.pformat(samples))) + + def assert_reported_stat(self, key, value=None, kind=None): + """Check statsd output + + Check statsd return values. A ``value`` should specify a + ``kind``, however a ``kind`` may be specified without a + ``value`` for a generic match. Leave both empy to just check + for key presence. + + :arg str key: The statsd key + :arg str value: The expected value of the metric ``key`` + :arg str kind: The expected type of the metric ``key`` For example + + - ``c`` counter + - ``g`` gauge + - ``ms`` timing + - ``s`` set + """ + + self.assertIsNotNone(self.statsd) + + if value: + self.assertNotEqual(kind, None) + + start = time.time() + while time.time() < (start + 1): + # Note our fake statsd just queues up results in a queue. + # We just keep going through them until we find one that + # matches, or fail out. If statsd pipelines are used, + # large single packets are sent with stats separated by + # newlines; thus we first flatten the stats out into + # single entries. + stats = itertools.chain.from_iterable( + [s.decode('utf-8').split('\n') for s in self.statsd.stats]) + for stat in stats: + k, v = stat.split(':') + if key == k: + if kind is None: + # key with no qualifiers is found + return True + + s_value, s_kind = v.split('|') + + # if no kind match, look for other keys + if kind != s_kind: + continue + + if value: + # special-case value|ms because statsd can turn + # timing results into float of indeterminate + # length, hence foiling string matching. + if kind == 'ms': + if float(value) == float(s_value): + return True + if value == s_value: + return True + # otherwise keep looking for other matches + continue + + # this key matches + return True + time.sleep(0.1) + + raise Exception("Key %s not found in reported stats" % key) + + def assert_prometheus_stat(self, name, value, labels=None): + sample_value = self._registry.get_sample_value(name, labels) + self.assertEqual(sample_value, value) + + def test_list_projects(self): + + mock_uri = self.get_mock_url( + service_type='identity', interface='admin', resource='projects', + base_url_append='v3') + + self.register_uris([ + dict(method='GET', uri=mock_uri, status_code=200, + json={'projects': []})]) + + self.cloud.list_projects() + self.assert_calls() + + self.assert_reported_stat( + 'openstack.api.identity.GET.projects', value='1', kind='c') + self.assert_prometheus_stat( + 'openstack_http_requests_total', 1, dict( + service_type='identity', + endpoint=mock_uri, + method='GET', + status_code='200')) + + def test_projects(self): + mock_uri = self.get_mock_url( + service_type='identity', interface='admin', resource='projects', + base_url_append='v3') + + self.register_uris([ + dict(method='GET', uri=mock_uri, status_code=200, + json={'projects': []})]) + + list(self.cloud.identity.projects()) + self.assert_calls() + + self.assert_reported_stat( + 'openstack.api.identity.GET.projects', value='1', kind='c') + self.assert_prometheus_stat( + 'openstack_http_requests_total', 1, dict( + service_type='identity', + endpoint=mock_uri, + method='GET', + status_code='200')) + + def test_servers(self): + + mock_uri = 'https://compute.example.com/v2.1/servers/detail' + + self.register_uris([ + dict(method='GET', uri=mock_uri, status_code=200, + json={'servers': []})]) + + list(self.cloud.compute.servers()) + self.assert_calls() + + self.assert_reported_stat( + 'openstack.api.compute.GET.servers.detail', value='1', kind='c') + self.assert_prometheus_stat( + 'openstack_http_requests_total', 1, dict( + service_type='compute', + endpoint=mock_uri, + method='GET', + status_code='200')) + + def test_servers_no_detail(self): + + mock_uri = 'https://compute.example.com/v2.1/servers' + + self.register_uris([ + dict(method='GET', uri=mock_uri, status_code=200, + json={'servers': []})]) + + self.cloud.compute.get('/servers') + self.assert_calls() + + self.assert_reported_stat( + 'openstack.api.compute.GET.servers', value='1', kind='c') + self.assert_prometheus_stat( + 'openstack_http_requests_total', 1, dict( + service_type='compute', + endpoint=mock_uri, + method='GET', + status_code='200')) + + +class TestNoStats(base.TestCase): + + def setUp(self): + super(TestNoStats, self).setUp() + self.statsd = StatsdFixture() + self.useFixture(self.statsd) + + def test_no_stats(self): + + mock_uri = self.get_mock_url( + service_type='identity', interface='admin', resource='projects', + base_url_append='v3') + + self.register_uris([ + dict(method='GET', uri=mock_uri, status_code=200, + json={'projects': []})]) + + self.cloud.identity._statsd_client = None + list(self.cloud.identity.projects()) + self.assert_calls() + self.assertEqual([], self.statsd.stats) diff --git a/releasenotes/notes/request-stats-9d70480bebbdb4d6.yaml b/releasenotes/notes/request-stats-9d70480bebbdb4d6.yaml new file mode 100644 index 000000000..04748cae6 --- /dev/null +++ b/releasenotes/notes/request-stats-9d70480bebbdb4d6.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added support for collecting and reporting stats on calls made to + statsd and prometheus. diff --git a/test-requirements.txt b/test-requirements.txt index bbc69b07f..82ccae1a3 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,9 +8,11 @@ extras>=1.0.0 # MIT fixtures>=3.0.0 # Apache-2.0/BSD jsonschema<3.0.0,>=2.6.0 # MIT mock>=2.0.0 # BSD +prometheus-client>=0.4.2 # Apache-2.0 python-subunit>=1.0.0 # Apache-2.0/BSD oslotest>=3.2.0 # Apache-2.0 requests-mock>=1.2.0 # Apache-2.0 +statsd>=3.3.0 stestr>=1.0.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD From 55235c5c22679dbb78ca48485d5fa0b2d89e9002 Mon Sep 17 00:00:00 2001 From: Ian Wienand Date: Tue, 12 Mar 2019 05:40:12 +0000 Subject: [PATCH 2407/3836] Replace openstack.org git:// URLs with https:// This is a mechanically generated change to replace openstack.org git:// URLs with https:// equivalents. This is in aid of a planned future move of the git hosting infrastructure to a self-hosted instance of gitea (https://gitea.io), which does not support the git wire protocol at this stage. This review should result in no functional change. Change-Id: I366b7a32aeedbd76d49c763b31ff4ec0b049190e Story: #2004627 Task: #29701 --- .zuul.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index b409ed2b6..6d0dcf814 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -265,7 +265,7 @@ IRONIC_VM_LOG_DIR: '{{ devstack_base_dir }}/ironic-bm-logs' IRONIC_VM_SPECS_RAM: 384 devstack_plugins: - ironic: git://git.openstack.org/openstack/ironic + ironic: https://git.openstack.org/openstack/ironic devstack_services: c-api: false c-bak: false From 72504d7f5b4be4a49a0e380b36a5c208ca11611d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 13 Mar 2019 12:35:37 +0000 Subject: [PATCH 2408/3836] Use auth_url as identity endpoint when not project scoped There are a set of actions in keystone that can be performed without a project scope, but the current discovery code will try to find the identity endpoint in the catalog. Use the auth_url for identity_endpoint_override when there is either no project info or system-scope declaration. Change-Id: Ibab4b2af2ca71fd9bd388829afcf9062431739ec --- openstack/config/cloud_region.py | 15 ++++++++++++++- openstack/tests/unit/config/test_config.py | 18 ++++++++++++++++++ .../identity-auth-url-f3ae8ef22d2bcab6.yaml | 7 +++++++ 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/identity-auth-url-f3ae8ef22d2bcab6.yaml diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 8bb208346..7c5a42f4f 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -38,6 +38,12 @@ from openstack import exceptions from openstack import proxy +SCOPE_KEYS = { + 'domain_id', 'domain_name', + 'project_id', 'project_name', + 'system_scope' +} + def _make_key(key, service_type): if not service_type: @@ -313,6 +319,7 @@ def get_service_name(self, service_type): return self._get_config('service_name', service_type) def get_endpoint(self, service_type): + auth = self.config.get('auth', {}) value = self._get_config('endpoint_override', service_type) if not value: value = self._get_config('endpoint', service_type) @@ -320,7 +327,13 @@ def get_endpoint(self, service_type): # If endpoint is given and we're using the none auth type, # then the endpoint value is the endpoint_override for every # service. - value = self.config.get('auth', {}).get('endpoint') + value = auth.get('endpoint') + if (not value and service_type == 'identity' + and SCOPE_KEYS.isdisjoint(set(auth.keys()))): + # There are a small number of unscoped identity operations. + # Specifically, looking up a list of projects/domains/system to + # scope to. + value = auth.get('auth_url') return value def get_connect_retries(self, service_type): diff --git a/openstack/tests/unit/config/test_config.py b/openstack/tests/unit/config/test_config.py index aa63b9a81..5a83c2931 100644 --- a/openstack/tests/unit/config/test_config.py +++ b/openstack/tests/unit/config/test_config.py @@ -168,6 +168,23 @@ def test_get_one_with_domain_id(self): self.assertNotIn('domain-id', cc.auth) self.assertNotIn('domain_id', cc) + def test_get_one_unscoped_identity(self): + single_conf = base._write_yaml({ + 'clouds': { + 'unscoped': { + 'auth': { + 'auth_url': 'http://example.com/v2', + 'username': 'testuser', + 'password': 'testpass', + }, + } + } + }) + c = config.OpenStackConfig(config_files=[single_conf], + vendor_files=[self.vendor_yaml]) + cc = c.get_one() + self.assertEqual('http://example.com/v2', cc.get_endpoint('identity')) + def test_get_one_domain_scoped(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) @@ -175,6 +192,7 @@ def test_get_one_domain_scoped(self): self.assertEqual('12345', cc.auth['domain_id']) self.assertNotIn('user_domain_id', cc.auth) self.assertNotIn('project_domain_id', cc.auth) + self.assertIsNone(cc.get_endpoint('identity')) def test_get_one_infer_user_domain(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], diff --git a/releasenotes/notes/identity-auth-url-f3ae8ef22d2bcab6.yaml b/releasenotes/notes/identity-auth-url-f3ae8ef22d2bcab6.yaml new file mode 100644 index 000000000..3009b0947 --- /dev/null +++ b/releasenotes/notes/identity-auth-url-f3ae8ef22d2bcab6.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + The ``auth_url`` will be used for the default value of + ``identity_endpoint_override`` in the absence of project or system-scope + information. This should simplify some actions such as listing available + projects. From bde5543390591d7229a981caaf7f111f96347283 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Fri, 15 Mar 2019 17:33:33 +0100 Subject: [PATCH 2409/3836] baremetal: implement the correct update of the maintenance_reason field This field has to be updated throw a separate API endpoint. Implement new calls for it, and integrate it seemlessly into Node.commit(). Change-Id: I474194fd06247f2aac63de9260ddb258ccd51467 --- doc/source/user/proxies/baremetal.rst | 2 + openstack/baremetal/v1/_proxy.py | 21 ++++ openstack/baremetal/v1/node.py | 68 +++++++++- openstack/cloud/openstackcloud.py | 13 +- openstack/resource.py | 20 ++- .../baremetal/test_baremetal_node.py | 78 ++++++++++++ .../tests/unit/baremetal/v1/test_node.py | 119 ++++++++++++++++++ .../tests/unit/cloud/test_baremetal_node.py | 18 +++ ...aremetal-maintenance-5cb95c6d898d4d72.yaml | 4 + 9 files changed, 327 insertions(+), 16 deletions(-) create mode 100644 releasenotes/notes/baremetal-maintenance-5cb95c6d898d4d72.yaml diff --git a/doc/source/user/proxies/baremetal.rst b/doc/source/user/proxies/baremetal.rst index cace96a7a..d877f400a 100644 --- a/doc/source/user/proxies/baremetal.rst +++ b/doc/source/user/proxies/baremetal.rst @@ -27,6 +27,8 @@ Node Operations .. automethod:: openstack.baremetal.v1._proxy.Proxy.wait_for_nodes_provision_state .. automethod:: openstack.baremetal.v1._proxy.Proxy.wait_for_node_reservation .. automethod:: openstack.baremetal.v1._proxy.Proxy.validate_node + .. automethod:: openstack.baremetal.v1._proxy.Proxy.set_node_maintenance + .. automethod:: openstack.baremetal.v1._proxy.Proxy.unset_node_maintenance Port Operations ^^^^^^^^^^^^^^^ diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 77cc77a32..e24b2cf43 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -399,6 +399,27 @@ def validate_node(self, node, required=('boot', 'deploy', 'power')): res = self._get_resource(_node.Node, node) return res.validate(self, required=required) + def set_node_maintenance(self, node, reason=None): + """Enable maintenance mode on the node. + + :param node: The value can be either the name or ID of a node or + a :class:`~openstack.baremetal.v1.node.Node` instance. + :param reason: Optional reason for maintenance. + :return: This :class:`Node` instance. + """ + res = self._get_resource(_node.Node, node) + return res.set_maintenance(self, reason) + + def unset_node_maintenance(self, node): + """Disable maintenance mode on the node. + + :param node: The value can be either the name or ID of a node or + a :class:`~openstack.baremetal.v1.node.Node` instance. + :return: This :class:`Node` instance. + """ + res = self._get_resource(_node.Node, node) + return res.unset_maintenance(self) + def delete_node(self, node, ignore_missing=True): """Delete a node. diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 4a2471401..21c41d3e3 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -250,7 +250,7 @@ def create(self, session, *args, **kwargs): "state %s" % expected_provision_state) # Ironic cannot set provision_state itself, so marking it as unchanged - self._body.clean(only={'provision_state'}) + self._clean_body_attrs({'provision_state'}) super(Node, self).create(session, *args, **kwargs) if (self.provision_state == 'enroll' @@ -267,6 +267,39 @@ def create(self, session, *args, **kwargs): return self + def commit(self, session, *args, **kwargs): + """Commit the state of the instance to the remote resource. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + + :return: This :class:`Node` instance. + """ + # These fields have to be set through separate API. + if ('maintenance_reason' in self._body.dirty + or 'maintenance' in self._body.dirty): + if not self.is_maintenance and self.maintenance_reason: + if 'maintenance' in self._body.dirty: + self.maintenance_reason = None + else: + raise ValueError('Maintenance reason cannot be set when ' + 'maintenance is False') + if self.is_maintenance: + self._do_maintenance_action( + session, 'put', {'reason': self.maintenance_reason}) + else: + # This corresponds to setting maintenance=False and + # maintenance_reason=None in the same request. + self._do_maintenance_action(session, 'delete') + + self._clean_body_attrs({'maintenance', 'maintenance_reason'}) + if not self.requires_commit: + # Other fields are not updated, re-fetch the node to reflect + # the new status. + return self.fetch(session) + + return super(Node, self).commit(session, *args, **kwargs) + def set_provision_state(self, session, target, config_drive=None, clean_steps=None, rescue_password=None, wait=False, timeout=None): @@ -647,5 +680,38 @@ def validate(self, session, required=('boot', 'deploy', 'power')): return {key: ValidationResult(value.get('result'), value.get('reason')) for key, value in result.items()} + def set_maintenance(self, session, reason=None): + """Enable maintenance mode on the node. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param reason: Optional reason for maintenance. + :return: This :class:`Node` instance. + """ + self._do_maintenance_action(session, 'put', {'reason': reason}) + return self.fetch(session) + + def unset_maintenance(self, session): + """Disable maintenance mode on the node. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :return: This :class:`Node` instance. + """ + self._do_maintenance_action(session, 'delete') + return self.fetch(session) + + def _do_maintenance_action(self, session, verb, body=None): + session = self._get_session(session) + version = self._get_microversion_for(session, 'commit') + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'maintenance') + response = getattr(session, verb)( + request.url, json=body, + headers=request.headers, microversion=version) + msg = ("Failed to change maintenance mode for node {node}" + .format(node=self.id)) + exceptions.raise_from_response(response, error_message=msg) + NodeDetail = Node diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 54e0939d6..9e112acad 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -9523,17 +9523,10 @@ def set_machine_maintenance_state( :returns: None """ - msg = ("Error setting machine maintenance state to {state} on node " - "{node}").format(state=state, node=name_or_id) - url = '/nodes/{name_or_id}/maintenance'.format(name_or_id=name_or_id) if state: - payload = {'reason': reason} - self._baremetal_client.put(url, - json=payload, - error_message=msg) + self.baremetal.set_node_maintenance(name_or_id, reason) else: - self._baremetal_client.delete(url, error_message=msg) - return None + self.baremetal.unset_node_maintenance(name_or_id) def remove_machine_from_maintenance(self, name_or_id): """Remove Baremetal Machine from Maintenance State @@ -9550,7 +9543,7 @@ def remove_machine_from_maintenance(self, name_or_id): :returns: None """ - self.set_machine_maintenance_state(name_or_id, False) + self.baremetal.unset_node_maintenance(name_or_id) def set_machine_power_on(self, name_or_id): """Activate baremetal machine power diff --git a/openstack/resource.py b/openstack/resource.py index 718037020..acd227301 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -695,6 +695,14 @@ def _consume_attrs(self, mapping, attrs): return relevant_attrs + def _clean_body_attrs(self, attrs): + """Mark the attributes as up-to-date.""" + self._body.clean(only=attrs) + if self.commit_jsonpatch: + for attr in attrs: + if attr in self._body: + self._original_body[attr] = self._body[attr] + @classmethod def _get_mapping(cls, component): """Return a dict of attributes of a given component on the class""" @@ -1181,6 +1189,12 @@ def head(self, session, base_path=None): self._translate_response(response, has_body=False) return self + @property + def requires_commit(self): + """Whether the next commit() call will do anything.""" + return (self._body.dirty or self._header.dirty + or self.allow_empty_commit) + def commit(self, session, prepend_key=True, has_body=True, retry_on_conflict=None, base_path=None): """Commit the state of the instance to the remote resource. @@ -1205,11 +1219,7 @@ def commit(self, session, prepend_key=True, has_body=True, self._body._dirty.discard("id") # Only try to update if we actually have anything to commit. - if not any([ - self._body.dirty, - self._header.dirty, - self.allow_empty_commit, - ]): + if not self.requires_commit: return self if not self.allow_commit: diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index 359cd9a07..c38fbf52a 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -154,6 +154,84 @@ def test_node_negative_non_existing(self): self.assertIsNone(self.conn.baremetal.find_node(uuid)) self.assertIsNone(self.conn.baremetal.delete_node(uuid)) + def test_maintenance(self): + reason = "Prepating for taking over the world" + + node = self.create_node() + self.assertFalse(node.is_maintenance) + self.assertIsNone(node.maintenance_reason) + + # Initial setting without the reason + node = self.conn.baremetal.set_node_maintenance(node) + self.assertTrue(node.is_maintenance) + self.assertIsNone(node.maintenance_reason) + + # Updating the reason later + node = self.conn.baremetal.set_node_maintenance(node, reason) + self.assertTrue(node.is_maintenance) + self.assertEqual(reason, node.maintenance_reason) + + # Removing the reason later + node = self.conn.baremetal.set_node_maintenance(node) + self.assertTrue(node.is_maintenance) + self.assertIsNone(node.maintenance_reason) + + # Unsetting maintenance + node = self.conn.baremetal.unset_node_maintenance(node) + self.assertFalse(node.is_maintenance) + self.assertIsNone(node.maintenance_reason) + + # Initial setting with the reason + node = self.conn.baremetal.set_node_maintenance(node, reason) + self.assertTrue(node.is_maintenance) + self.assertEqual(reason, node.maintenance_reason) + + def test_maintenance_via_update(self): + reason = "Prepating for taking over the world" + + node = self.create_node() + + # Initial setting without the reason + node = self.conn.baremetal.update_node(node, is_maintenance=True) + self.assertTrue(node.is_maintenance) + self.assertIsNone(node.maintenance_reason) + + # Make sure the change has effect on the remote side. + node = self.conn.baremetal.get_node(node.id) + self.assertTrue(node.is_maintenance) + self.assertIsNone(node.maintenance_reason) + + # Updating the reason later + node = self.conn.baremetal.update_node(node, maintenance_reason=reason) + self.assertTrue(node.is_maintenance) + self.assertEqual(reason, node.maintenance_reason) + + # Make sure the change has effect on the remote side. + node = self.conn.baremetal.get_node(node.id) + self.assertTrue(node.is_maintenance) + self.assertEqual(reason, node.maintenance_reason) + + # Unsetting maintenance + node = self.conn.baremetal.update_node(node, is_maintenance=False) + self.assertFalse(node.is_maintenance) + self.assertIsNone(node.maintenance_reason) + + # Make sure the change has effect on the remote side. + node = self.conn.baremetal.get_node(node.id) + self.assertFalse(node.is_maintenance) + self.assertIsNone(node.maintenance_reason) + + # Initial setting with the reason + node = self.conn.baremetal.update_node(node, is_maintenance=True, + maintenance_reason=reason) + self.assertTrue(node.is_maintenance) + self.assertEqual(reason, node.maintenance_reason) + + # Make sure the change has effect on the remote side. + node = self.conn.baremetal.get_node(node.id) + self.assertTrue(node.is_maintenance) + self.assertEqual(reason, node.maintenance_reason) + class TestBareMetalNodeFields(base.BaseBaremetalTest): diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 5db11397e..765250ef7 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -562,3 +562,122 @@ def test_soft_power_on(self): headers=mock.ANY, microversion='1.27', retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + +@mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) +@mock.patch.object(node.Node, '_translate_response', mock.Mock()) +@mock.patch.object(node.Node, '_get_session', lambda self, x: x) +class TestNodeMaintenance(base.TestCase): + + def setUp(self): + super(TestNodeMaintenance, self).setUp() + self.node = node.Node.existing(**FAKE) + self.session = mock.Mock(spec=adapter.Adapter, + default_microversion='1.1', + retriable_status_codes=None) + + def test_set(self): + self.node.set_maintenance(self.session) + self.session.put.assert_called_once_with( + 'nodes/%s/maintenance' % self.node.id, + json={'reason': None}, + headers=mock.ANY, + microversion=mock.ANY) + + def test_set_with_reason(self): + self.node.set_maintenance(self.session, 'No work on Monday') + self.session.put.assert_called_once_with( + 'nodes/%s/maintenance' % self.node.id, + json={'reason': 'No work on Monday'}, + headers=mock.ANY, + microversion=mock.ANY) + + def test_unset(self): + self.node.unset_maintenance(self.session) + self.session.delete.assert_called_once_with( + 'nodes/%s/maintenance' % self.node.id, + json=None, + headers=mock.ANY, + microversion=mock.ANY) + + def test_set_via_update(self): + self.node.is_maintenance = True + self.node.commit(self.session) + self.session.put.assert_called_once_with( + 'nodes/%s/maintenance' % self.node.id, + json={'reason': None}, + headers=mock.ANY, + microversion=mock.ANY) + + self.assertFalse(self.session.patch.called) + + def test_set_with_reason_via_update(self): + self.node.is_maintenance = True + self.node.maintenance_reason = 'No work on Monday' + self.node.commit(self.session) + self.session.put.assert_called_once_with( + 'nodes/%s/maintenance' % self.node.id, + json={'reason': 'No work on Monday'}, + headers=mock.ANY, + microversion=mock.ANY) + self.assertFalse(self.session.patch.called) + + def test_set_with_other_fields(self): + self.node.is_maintenance = True + self.node.name = 'lazy-3000' + self.node.commit(self.session) + self.session.put.assert_called_once_with( + 'nodes/%s/maintenance' % self.node.id, + json={'reason': None}, + headers=mock.ANY, + microversion=mock.ANY) + + self.session.patch.assert_called_once_with( + 'nodes/%s' % self.node.id, + json=[{'path': '/name', 'op': 'replace', 'value': 'lazy-3000'}], + headers=mock.ANY, + microversion=mock.ANY) + + def test_set_with_reason_and_other_fields(self): + self.node.is_maintenance = True + self.node.maintenance_reason = 'No work on Monday' + self.node.name = 'lazy-3000' + self.node.commit(self.session) + self.session.put.assert_called_once_with( + 'nodes/%s/maintenance' % self.node.id, + json={'reason': 'No work on Monday'}, + headers=mock.ANY, + microversion=mock.ANY) + + self.session.patch.assert_called_once_with( + 'nodes/%s' % self.node.id, + json=[{'path': '/name', 'op': 'replace', 'value': 'lazy-3000'}], + headers=mock.ANY, + microversion=mock.ANY) + + def test_no_reason_without_maintenance(self): + self.node.maintenance_reason = 'Can I?' + self.assertRaises(ValueError, self.node.commit, self.session) + self.assertFalse(self.session.put.called) + self.assertFalse(self.session.patch.called) + + def test_set_unset_maintenance(self): + self.node.is_maintenance = True + self.node.maintenance_reason = 'No work on Monday' + self.node.commit(self.session) + + self.session.put.assert_called_once_with( + 'nodes/%s/maintenance' % self.node.id, + json={'reason': 'No work on Monday'}, + headers=mock.ANY, + microversion=mock.ANY) + + self.node.is_maintenance = False + self.node.commit(self.session) + self.assertIsNone(self.node.maintenance_reason) + + self.session.delete.assert_called_once_with( + 'nodes/%s/maintenance' % self.node.id, + json=None, + headers=mock.ANY, + microversion=mock.ANY) diff --git a/openstack/tests/unit/cloud/test_baremetal_node.py b/openstack/tests/unit/cloud/test_baremetal_node.py index e40262897..61882ea48 100644 --- a/openstack/tests/unit/cloud/test_baremetal_node.py +++ b/openstack/tests/unit/cloud/test_baremetal_node.py @@ -592,6 +592,12 @@ def test_set_machine_maintenace_state(self): append=[self.fake_baremetal_node['uuid'], 'maintenance']), validate=dict(json={'reason': 'no reason'})), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), ]) self.cloud.set_machine_maintenance_state( self.fake_baremetal_node['uuid'], True, reason='no reason') @@ -606,6 +612,12 @@ def test_set_machine_maintenace_state_false(self): resource='nodes', append=[self.fake_baremetal_node['uuid'], 'maintenance'])), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), ]) self.cloud.set_machine_maintenance_state( self.fake_baremetal_node['uuid'], False) @@ -620,6 +632,12 @@ def test_remove_machine_from_maintenance(self): resource='nodes', append=[self.fake_baremetal_node['uuid'], 'maintenance'])), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']]), + json=self.fake_baremetal_node), ]) self.cloud.remove_machine_from_maintenance( self.fake_baremetal_node['uuid']) diff --git a/releasenotes/notes/baremetal-maintenance-5cb95c6d898d4d72.yaml b/releasenotes/notes/baremetal-maintenance-5cb95c6d898d4d72.yaml new file mode 100644 index 000000000..4a309b9fd --- /dev/null +++ b/releasenotes/notes/baremetal-maintenance-5cb95c6d898d4d72.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Implements updating the baremetal Node's ``maintenance_reason``. From 16f2dbe3b08d6feb1438a59f6b0a82e46ea00fe3 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Fri, 15 Mar 2019 16:51:50 -0500 Subject: [PATCH 2410/3836] Add proxy API reference to baremetal user guide Today was my first day with the openstacksdk docs, and it took me a while to find the baremetal proxy API reference [1] after banging my head against the examples in the user guide [2]. This commit adds a link to [1] at the top of [2] and makes clear that the latter is just an exemplary subset of what can be done. [1] https://docs.openstack.org/openstacksdk/latest/user/proxies/baremetal.html [2] https://docs.openstack.org/openstacksdk/latest/user/guides/baremetal.html Change-Id: If3e62da2f037b44cb057856e6566d68ef16ded8e --- doc/source/user/guides/baremetal.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/source/user/guides/baremetal.rst b/doc/source/user/guides/baremetal.rst index c32553125..cbfd9665e 100644 --- a/doc/source/user/guides/baremetal.rst +++ b/doc/source/user/guides/baremetal.rst @@ -11,6 +11,9 @@ below. The primary resource of the Bare Metal service is the **node**. +Below are a few usage examples. For a reference to all the available methods, +see :doc:`/user/proxies/baremetal`. + CRUD operations ~~~~~~~~~~~~~~~ From 6dcf92fca14545e3e28f967fa7d2cda849251ffd Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 15 Mar 2019 16:42:56 +0100 Subject: [PATCH 2411/3836] Add unit tests for connection.add_service Change-Id: Ifb488d9a49c988e9c21ff2d8e0a0ad7abbb28618 --- openstack/tests/unit/fake/__init__.py | 0 openstack/tests/unit/fake/fake_service.py | 24 +++++++ openstack/tests/unit/fake/v1/__init__.py | 0 openstack/tests/unit/fake/v1/_proxy.py | 20 ++++++ openstack/tests/unit/fake/v1/fake.py | 35 +++++++++ openstack/tests/unit/fake/v2/__init__.py | 0 openstack/tests/unit/fake/v2/_proxy.py | 20 ++++++ openstack/tests/unit/fake/v2/fake.py | 35 +++++++++ .../unit/fixtures/catalog-v3-fake-v1.json | 71 +++++++++++++++++++ .../unit/fixtures/catalog-v3-fake-v2.json | 71 +++++++++++++++++++ openstack/tests/unit/test_connection.py | 54 ++++++++++++++ 11 files changed, 330 insertions(+) create mode 100644 openstack/tests/unit/fake/__init__.py create mode 100644 openstack/tests/unit/fake/fake_service.py create mode 100644 openstack/tests/unit/fake/v1/__init__.py create mode 100644 openstack/tests/unit/fake/v1/_proxy.py create mode 100644 openstack/tests/unit/fake/v1/fake.py create mode 100644 openstack/tests/unit/fake/v2/__init__.py create mode 100644 openstack/tests/unit/fake/v2/_proxy.py create mode 100644 openstack/tests/unit/fake/v2/fake.py create mode 100644 openstack/tests/unit/fixtures/catalog-v3-fake-v1.json create mode 100644 openstack/tests/unit/fixtures/catalog-v3-fake-v2.json diff --git a/openstack/tests/unit/fake/__init__.py b/openstack/tests/unit/fake/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/unit/fake/fake_service.py b/openstack/tests/unit/fake/fake_service.py new file mode 100644 index 000000000..8440b52b8 --- /dev/null +++ b/openstack/tests/unit/fake/fake_service.py @@ -0,0 +1,24 @@ +# 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 service_description +from openstack.tests.unit.fake.v1 import _proxy as _proxy_1 +from openstack.tests.unit.fake.v2 import _proxy as _proxy_2 + + +class FakeService(service_description.ServiceDescription): + """The fake service.""" + + supported_versions = { + '1': _proxy_1.Proxy, + '2': _proxy_2.Proxy, + } diff --git a/openstack/tests/unit/fake/v1/__init__.py b/openstack/tests/unit/fake/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/unit/fake/v1/_proxy.py b/openstack/tests/unit/fake/v1/_proxy.py new file mode 100644 index 000000000..98a05119e --- /dev/null +++ b/openstack/tests/unit/fake/v1/_proxy.py @@ -0,0 +1,20 @@ +# 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 proxy + + +class Proxy(proxy.Proxy): + + skip_discovery = True + + def dummy(self): + return True diff --git a/openstack/tests/unit/fake/v1/fake.py b/openstack/tests/unit/fake/v1/fake.py new file mode 100644 index 000000000..901be4a39 --- /dev/null +++ b/openstack/tests/unit/fake/v1/fake.py @@ -0,0 +1,35 @@ +# 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 resource + + +class Fake(resource.Resource): + resource_key = "resource" + resources_key = "resources" + base_path = "/fake" + + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + allow_head = True + + #: The transaction date and time. + timestamp = resource.Header("x-timestamp") + #: The name of this resource. + name = resource.Body("name", alternate_id=True) + #: The value of the resource. Also available in headers. + value = resource.Body("value", alias="x-resource-value") + #: Is this resource cool? If so, set it to True. + #: This is a multi-line comment about cool stuff. + cool = resource.Body("cool", type=bool) diff --git a/openstack/tests/unit/fake/v2/__init__.py b/openstack/tests/unit/fake/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/unit/fake/v2/_proxy.py b/openstack/tests/unit/fake/v2/_proxy.py new file mode 100644 index 000000000..003c72d29 --- /dev/null +++ b/openstack/tests/unit/fake/v2/_proxy.py @@ -0,0 +1,20 @@ +# 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 proxy + + +class Proxy(proxy.Proxy): + + skip_discovery = True + + def dummy(self): + return False diff --git a/openstack/tests/unit/fake/v2/fake.py b/openstack/tests/unit/fake/v2/fake.py new file mode 100644 index 000000000..901be4a39 --- /dev/null +++ b/openstack/tests/unit/fake/v2/fake.py @@ -0,0 +1,35 @@ +# 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 resource + + +class Fake(resource.Resource): + resource_key = "resource" + resources_key = "resources" + base_path = "/fake" + + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + allow_head = True + + #: The transaction date and time. + timestamp = resource.Header("x-timestamp") + #: The name of this resource. + name = resource.Body("name", alternate_id=True) + #: The value of the resource. Also available in headers. + value = resource.Body("value", alias="x-resource-value") + #: Is this resource cool? If so, set it to True. + #: This is a multi-line comment about cool stuff. + cool = resource.Body("cool", type=bool) diff --git a/openstack/tests/unit/fixtures/catalog-v3-fake-v1.json b/openstack/tests/unit/fixtures/catalog-v3-fake-v1.json new file mode 100644 index 000000000..c32617577 --- /dev/null +++ b/openstack/tests/unit/fixtures/catalog-v3-fake-v1.json @@ -0,0 +1,71 @@ +{ + "token": { + "audit_ids": [ + "Rvn7eHkiSeOwucBIPaKdYA" + ], + "catalog": [ + { + "endpoints": [ + { + "id": "4deb4d0504a044a395d4480741ba628c", + "interface": "public", + "region": "RegionOne", + "url": "https://identity.example.com" + }, + { + "id": "012322eeedcd459edabb4933021112bc", + "interface": "admin", + "region": "RegionOne", + "url": "https://identity.example.com" + } + ], + "endpoints_links": [], + "name": "keystone", + "type": "identity" + }, + { + "endpoints": [ + { + "id": "1e875ca2225b408bbf3520a1b8e1a537", + "interface": "public", + "region": "RegionOne", + "url": "https://fake.example.com/v1/1c36b64c840a42cd9e9b931a369337f0" + } + ], + "name": "fake_service", + "type": "fake" + } + ], + "expires_at": "9999-12-31T23:59:59Z", + "issued_at": "2016-12-17T14:25:05.000000Z", + "methods": [ + "password" + ], + "project": { + "domain": { + "id": "default", + "name": "default" + }, + "id": "1c36b64c840a42cd9e9b931a369337f0", + "name": "Default Project" + }, + "roles": [ + { + "id": "9fe2ff9ee4384b1894a90878d3e92bab", + "name": "_member_" + }, + { + "id": "37071fc082e14c2284c32a2761f71c63", + "name": "swiftoperator" + } + ], + "user": { + "domain": { + "id": "default", + "name": "default" + }, + "id": "c17534835f8f42bf98fc367e0bf35e09", + "name": "mordred" + } + } +} diff --git a/openstack/tests/unit/fixtures/catalog-v3-fake-v2.json b/openstack/tests/unit/fixtures/catalog-v3-fake-v2.json new file mode 100644 index 000000000..9ce0b7f50 --- /dev/null +++ b/openstack/tests/unit/fixtures/catalog-v3-fake-v2.json @@ -0,0 +1,71 @@ +{ + "token": { + "audit_ids": [ + "Rvn7eHkiSeOwucBIPaKdYA" + ], + "catalog": [ + { + "endpoints": [ + { + "id": "4deb4d0504a044a395d4480741ba628c", + "interface": "public", + "region": "RegionOne", + "url": "https://identity.example.com" + }, + { + "id": "012322eeedcd459edabb4933021112bc", + "interface": "admin", + "region": "RegionOne", + "url": "https://identity.example.com" + } + ], + "endpoints_links": [], + "name": "keystone", + "type": "identity" + }, + { + "endpoints": [ + { + "id": "1e875ca2225b408bbf3520a1b8e1a537", + "interface": "public", + "region": "RegionOne", + "url": "https://fake.example.com/v2/1c36b64c840a42cd9e9b931a369337f0" + } + ], + "name": "fake_service", + "type": "fake" + } + ], + "expires_at": "9999-12-31T23:59:59Z", + "issued_at": "2016-12-17T14:25:05.000000Z", + "methods": [ + "password" + ], + "project": { + "domain": { + "id": "default", + "name": "default" + }, + "id": "1c36b64c840a42cd9e9b931a369337f0", + "name": "Default Project" + }, + "roles": [ + { + "id": "9fe2ff9ee4384b1894a90878d3e92bab", + "name": "_member_" + }, + { + "id": "37071fc082e14c2284c32a2761f71c63", + "name": "swiftoperator" + } + ], + "user": { + "domain": { + "id": "default", + "name": "default" + }, + "id": "c17534835f8f42bf98fc367e0bf35e09", + "name": "mordred" + } + } +} diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index 163275fd0..e9b6691d9 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -19,6 +19,7 @@ from openstack import connection import openstack.config from openstack.tests.unit import base +from openstack.tests.unit.fake import fake_service CONFIG_AUTH_URL = "https://identity.example.com/" @@ -251,3 +252,56 @@ def test_authorize_failure(self): self.assertRaises(openstack.exceptions.HttpException, self.cloud.authorize) + + +class TestNewService(base.TestCase): + + def test_add_service_v1(self): + self.use_keystone_v3(catalog='catalog-v3-fake-v1.json') + conn = self.cloud + + self.register_uris([ + dict(method='GET', + uri='https://fake.example.com', + status_code=404), + dict(method='GET', + uri='https://fake.example.com/v1/', + status_code=404), + dict(method='GET', + uri=self.get_mock_url('fake'), + status_code=404), + ]) + + service = fake_service.FakeService('fake') + + conn.add_service(service) + + self.assertEqual( + 'openstack.tests.unit.fake.v1._proxy', + conn.fake.__class__.__module__) + self.assertTrue(conn.fake.dummy()) + + def test_add_service_v2(self): + self.use_keystone_v3(catalog='catalog-v3-fake-v2.json') + conn = self.cloud + + self.register_uris([ + dict(method='GET', + uri='https://fake.example.com', + status_code=404), + dict(method='GET', + uri='https://fake.example.com/v2/', + status_code=404), + dict(method='GET', + uri=self.get_mock_url('fake'), + status_code=404), + ]) + + service = fake_service.FakeService('fake') + + conn.add_service(service) + + self.assertEqual( + 'openstack.tests.unit.fake.v2._proxy', + conn.fake.__class__.__module__) + self.assertFalse(conn.fake.dummy()) From e8622646e42a89f0585ef6c5691694a86bf2ea67 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 18 Mar 2019 12:03:02 +0100 Subject: [PATCH 2412/3836] Create runtime descriptor for new service Instead of explicitly creating proxy for any new service added with connection.add_service fill a "run-time" descriptor. This avoids going into service discovery for each new service (when there are multiple of those added in a "init"-like function the overall start time increases heavily. Change-Id: I114ff0ee170244978a1949dc9c7258f373023ab6 --- openstack/connection.py | 14 +++++++++++--- openstack/tests/unit/test_connection.py | 11 +++++++---- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/openstack/connection.py b/openstack/connection.py index f208bf4be..f76a9b765 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -333,11 +333,19 @@ class contained in # we get an adapter. if isinstance(service, six.string_types): service = service_description.ServiceDescription(service) - service_proxy = service._make_proxy(self) - # Register the proxy class with every known alias + # Directly invoke descriptor of the ServiceDescription + def getter(self): + return service.__get__(self, service) + + # Register the ServiceDescription class (as property) + # with every known alias for a "runtime descriptor" for attr_name in service.all_types: - setattr(self, attr_name.replace('-', '_'), service_proxy) + setattr( + self.__class__, + attr_name.replace('-', '_'), + property(fget=getter) + ) def authorize(self): """Authorize this Connection diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index e9b6691d9..ce2320cc6 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -260,6 +260,13 @@ def test_add_service_v1(self): self.use_keystone_v3(catalog='catalog-v3-fake-v1.json') conn = self.cloud + service = fake_service.FakeService('fake') + + conn.add_service(service) + + # Ensure no discovery calls made + self.assertEqual(0, len(self.adapter.request_history)) + self.register_uris([ dict(method='GET', uri='https://fake.example.com', @@ -272,10 +279,6 @@ def test_add_service_v1(self): status_code=404), ]) - service = fake_service.FakeService('fake') - - conn.add_service(service) - self.assertEqual( 'openstack.tests.unit.fake.v1._proxy', conn.fake.__class__.__module__) From 4b89bc2592e10ac43d0727ed72168605d56596b3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 18 Mar 2019 14:17:31 +0000 Subject: [PATCH 2413/3836] Fix wait_for_server docstring There is no 'res' parameter. While we're in there, fix the indentation. Change-Id: Ib9d13ec18177bf2fb436a42c81b5776ad8248818 --- openstack/compute/v2/_proxy.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 215aad203..31183d14c 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -930,16 +930,20 @@ def wait_for_server(self, server, status='ACTIVE', failures=None, interval=2, wait=120): """Wait for a server to be in a particular status. - :param res: The resource to wait on to reach the specified status. - The resource must have a ``status`` attribute. - :type resource: A :class:`~openstack.resource.Resource` object. + :param server: + The :class:`~openstack.compute.v2.server.Server` to wait on + to reach the specified status. + :type server: :class:`~openstack.compute.v2.server.Server`: :param status: Desired status. - :param failures: Statuses that would be interpreted as failures. + :param failures: + Statuses that would be interpreted as failures. :type failures: :py:class:`list` - :param interval: Number of seconds to wait before to consecutive - checks. Default to 2. - :param wait: Maximum number of seconds to wait before the change. - Default to 120. + :param int interval: + Number of seconds to wait before to consecutive checks. + Default to 2. + :param int wait: + Maximum number of seconds to wait before the change. + Default to 120. :returns: The resource is returned on success. :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition to the desired status failed to occur in specified seconds. From 82aaf1908787705c49950c08967efa041d870f4f Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Mon, 18 Mar 2019 14:41:17 +0000 Subject: [PATCH 2414/3836] Update master for stable/stein Add file to the reno documentation build to show release notes for stable/stein. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/stein. Change-Id: Ia93abe9f76b52dd101ec90bef09416d202740173 Sem-Ver: feature --- releasenotes/source/index.rst | 1 + releasenotes/source/stein.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/stein.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index dba663344..b397ae01b 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ :maxdepth: 1 unreleased + stein rocky queens pike diff --git a/releasenotes/source/stein.rst b/releasenotes/source/stein.rst new file mode 100644 index 000000000..efaceb667 --- /dev/null +++ b/releasenotes/source/stein.rst @@ -0,0 +1,6 @@ +=================================== + Stein Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/stein From b3723bed9dd2b336c63aa9bcf5a3e1ce08e2e290 Mon Sep 17 00:00:00 2001 From: Sahid Orentino Ferdjaoui Date: Mon, 25 Mar 2019 11:01:00 +0000 Subject: [PATCH 2415/3836] add python 3.7 unit test job See ML discussion here [1] for context. [1] http://lists.openstack.org/pipermail/openstack-dev/2018-October/135626.html Change-Id: If472ec316c5f5aaee15aab4d72964f806d3efff7 Signed-off-by: Sahid Orentino Ferdjaoui --- .zuul.yaml | 1 + openstack/tests/unit/test_resource.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index 6d0dcf814..ee9dea9f6 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -396,6 +396,7 @@ - openstack-lower-constraints-jobs - openstack-python-jobs - openstack-python36-jobs + - openstack-python37-jobs - openstacksdk-functional-tips - openstacksdk-tox-tips - os-client-config-tox-tips diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index ee98777f0..7b44a60d3 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1901,7 +1901,10 @@ def test_list_multi_page_inferred_additional(self): microversion=None) # Ensure we're done after those three items - self.assertRaises(StopIteration, next, results) + # In python3.7, PEP 479 is enabled for all code, and StopIteration + # raised directly from code is turned into a RuntimeError. + # Something about how mock is implemented triggers that here. + self.assertRaises((StopIteration, RuntimeError), next, results) # Ensure we only made two calls to get this done self.assertEqual(3, len(self.session.get.call_args_list)) From 1a4ed826b9768ff53630607c95349186ab344ad8 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 26 Mar 2019 16:26:48 +0100 Subject: [PATCH 2416/3836] Split OpenStackCloud into reasonable pieces With over 12k lines of code in openstackcloud class it is hardly manageable. Split methods on a service base into corresponding Mixings. It still make sense to think how individual mixins are structured (it is not very nice, when function in an object use property of self, which is not defined in the class, but exists only in a resulting mixed object). Initialization of individual Mixins in the connection should be also reviewed Change-Id: I81050d66e3a2dc0997e3e7620ff2c4e1891bfeb7 --- openstack/cloud/_baremetal.py | 718 + openstack/cloud/_block_storage.py | 870 ++ openstack/cloud/_clustering.py | 567 + openstack/cloud/_coe.py | 425 + openstack/cloud/_compute.py | 1917 +++ openstack/cloud/_dns.py | 296 + openstack/cloud/_floating_ip.py | 1169 ++ openstack/cloud/_identity.py | 1534 ++ openstack/cloud/_image.py | 432 + openstack/cloud/_network.py | 2566 ++++ openstack/cloud/_network_common.py | 370 + openstack/cloud/_object_store.py | 837 ++ openstack/cloud/_orchestration.py | 275 + openstack/cloud/_security_group.py | 387 + openstack/cloud/openstackcloud.py | 11781 +--------------- openstack/connection.py | 47 +- .../tests/functional/instance_ha/test_host.py | 3 +- .../tests/unit/cloud/test_create_server.py | 14 +- .../unit/cloud/test_floating_ip_common.py | 34 +- openstack/tests/unit/cloud/test_meta.py | 42 +- openstack/tests/unit/cloud/test_shade.py | 5 +- 21 files changed, 12515 insertions(+), 11774 deletions(-) create mode 100644 openstack/cloud/_baremetal.py create mode 100644 openstack/cloud/_block_storage.py create mode 100644 openstack/cloud/_clustering.py create mode 100644 openstack/cloud/_coe.py create mode 100644 openstack/cloud/_compute.py create mode 100644 openstack/cloud/_dns.py create mode 100644 openstack/cloud/_floating_ip.py create mode 100644 openstack/cloud/_identity.py create mode 100644 openstack/cloud/_image.py create mode 100644 openstack/cloud/_network.py create mode 100644 openstack/cloud/_network_common.py create mode 100644 openstack/cloud/_object_store.py create mode 100644 openstack/cloud/_orchestration.py create mode 100644 openstack/cloud/_security_group.py diff --git a/openstack/cloud/_baremetal.py b/openstack/cloud/_baremetal.py new file mode 100644 index 000000000..99d1126cb --- /dev/null +++ b/openstack/cloud/_baremetal.py @@ -0,0 +1,718 @@ +# 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 types so that we can reference ListType in sphinx param declarations. +# We can't just use list, because sphinx gets confused by +# openstack.resource.Resource.list and openstack.resource2.Resource.list +import jsonpatch +import types # noqa +import warnings + +from openstack.cloud import exc +from openstack.cloud import _normalize +from openstack.cloud import _utils +from openstack import utils + + +class BaremetalCloudMixin(_normalize.Normalizer): + + @property + def _baremetal_client(self): + if 'baremetal' not in self._raw_clients: + client = self._get_raw_client('baremetal') + # Do this to force version discovery. We need to do that, because + # the endpoint-override trick we do for neutron because + # ironicclient just appends a /v1 won't work and will break + # keystoneauth - because ironic's versioned discovery endpoint + # is non-compliant and doesn't return an actual version dict. + client = self._get_versioned_client( + 'baremetal', min_version=1, max_version='1.latest') + self._raw_clients['baremetal'] = client + return self._raw_clients['baremetal'] + + def list_nics(self): + """Return a list of all bare metal ports.""" + return [nic._to_munch() for nic in self.baremetal.ports(details=True)] + + def list_nics_for_machine(self, uuid): + """Returns a list of ports present on the machine node. + + :param uuid: String representing machine UUID value in + order to identify the machine. + :returns: A list of ports. + """ + # TODO(dtantsur): support node names here. + return [nic._to_munch() + for nic in self.baremetal.ports(details=True, node_id=uuid)] + + def get_nic_by_mac(self, mac): + """Get bare metal NIC by its hardware address (usually MAC).""" + results = [nic._to_munch() + for nic in self.baremetal.ports(address=mac, details=True)] + try: + return results[0] + except IndexError: + return None + + def list_machines(self): + """List Machines. + + :returns: list of ``munch.Munch`` representing machines. + """ + return [self._normalize_machine(node._to_munch()) + for node in self.baremetal.nodes()] + + def get_machine(self, name_or_id): + """Get Machine by name or uuid + + Search the baremetal host out by utilizing the supplied id value + which can consist of a name or UUID. + + :param name_or_id: A node name or UUID that will be looked up. + + :returns: ``munch.Munch`` representing the node found or None if no + nodes are found. + """ + try: + return self._normalize_machine( + self.baremetal.get_node(name_or_id)._to_munch()) + except exc.OpenStackCloudResourceNotFound: + return None + + def get_machine_by_mac(self, mac): + """Get machine by port MAC address + + :param mac: Port MAC address to query in order to return a node. + + :returns: ``munch.Munch`` representing the node found or None + if the node is not found. + """ + nic = self.get_nic_by_mac(mac) + if nic is None: + return None + else: + return self.get_machine(nic['node_uuid']) + + def inspect_machine(self, name_or_id, wait=False, timeout=3600): + """Inspect a Barmetal machine + + Engages the Ironic node inspection behavior in order to collect + metadata about the baremetal machine. + + :param name_or_id: String representing machine name or UUID value in + order to identify the machine. + + :param wait: Boolean value controlling if the method is to wait for + the desired state to be reached or a failure to occur. + + :param timeout: Integer value, defautling to 3600 seconds, for the$ + wait state to reach completion. + + :returns: ``munch.Munch`` representing the current state of the machine + upon exit of the method. + """ + + return_to_available = False + + node = self.baremetal.get_node(name_or_id) + + # NOTE(TheJulia): If in available state, we can do this. However, + # we need to to move the machine back to manageable first. + if node.provision_state == 'available': + if node.instance_id: + raise exc.OpenStackCloudException( + "Refusing to inspect available machine %(node)s " + "which is associated with an instance " + "(instance_uuid %(inst)s)" % + {'node': node.id, 'inst': node.instance_id}) + + return_to_available = True + # NOTE(TheJulia): Changing available machine to managedable state + # and due to state transitions we need to until that transition has + # completed. + node = self.baremetal.set_node_provision_state(node, 'manage', + wait=True, + timeout=timeout) + + if node.provision_state not in ('manageable', 'inspect failed'): + raise exc.OpenStackCloudException( + "Machine %(node)s must be in 'manageable', 'inspect failed' " + "or 'available' provision state to start inspection, the " + "current state is %(state)s" % + {'node': node.id, 'state': node.provision_state}) + + node = self.baremetal.set_node_provision_state(node, 'inspect', + wait=True, + timeout=timeout) + + if return_to_available: + node = self.baremetal.set_node_provision_state(node, 'provide', + wait=True, + timeout=timeout) + + return node._to_munch() + + def register_machine(self, nics, wait=False, timeout=3600, + lock_timeout=600, **kwargs): + """Register Baremetal with Ironic + + Allows for the registration of Baremetal nodes with Ironic + and population of pertinant node information or configuration + to be passed to the Ironic API for the node. + + This method also creates ports for a list of MAC addresses passed + in to be utilized for boot and potentially network configuration. + + If a failure is detected creating the network ports, any ports + created are deleted, and the node is removed from Ironic. + + :param nics: + An array of MAC addresses that represent the + network interfaces for the node to be created. + + Example:: + + [ + {'mac': 'aa:bb:cc:dd:ee:01'}, + {'mac': 'aa:bb:cc:dd:ee:02'} + ] + + :param wait: Boolean value, defaulting to false, to wait for the + node to reach the available state where the node can be + provisioned. It must be noted, when set to false, the + method will still wait for locks to clear before sending + the next required command. + + :param timeout: Integer value, defautling to 3600 seconds, for the + wait state to reach completion. + + :param lock_timeout: Integer value, defaulting to 600 seconds, for + locks to clear. + + :param kwargs: Key value pairs to be passed to the Ironic API, + including uuid, name, chassis_uuid, driver_info, + parameters. + + :raises: OpenStackCloudException on operation error. + + :returns: Returns a ``munch.Munch`` representing the new + baremetal node. + """ + + msg = ("Baremetal machine node failed to be created.") + port_msg = ("Baremetal machine port failed to be created.") + + url = '/nodes' + # TODO(TheJulia): At some point we need to figure out how to + # handle data across when the requestor is defining newer items + # with the older api. + machine = self._baremetal_client.post(url, + json=kwargs, + error_message=msg, + microversion="1.6") + + created_nics = [] + try: + for row in nics: + payload = {'address': row['mac'], + 'node_uuid': machine['uuid']} + nic = self._baremetal_client.post('/ports', + json=payload, + error_message=port_msg) + created_nics.append(nic['uuid']) + + except Exception as e: + self.log.debug("ironic NIC registration failed", exc_info=True) + # TODO(mordred) Handle failures here + try: + for uuid in created_nics: + try: + port_url = '/ports/{uuid}'.format(uuid=uuid) + # NOTE(TheJulia): Added in hope that it is logged. + port_msg = ('Failed to delete port {port} for node ' + '{node}').format(port=uuid, + node=machine['uuid']) + self._baremetal_client.delete(port_url, + error_message=port_msg) + except Exception: + pass + finally: + version = "1.6" + msg = "Baremetal machine failed to be deleted." + url = '/nodes/{node_id}'.format( + node_id=machine['uuid']) + self._baremetal_client.delete(url, + error_message=msg, + microversion=version) + raise exc.OpenStackCloudException( + "Error registering NICs with the baremetal service: %s" + % str(e)) + + with _utils.shade_exceptions( + "Error transitioning node to available state"): + if wait: + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for node transition to " + "available state"): + + machine = self.get_machine(machine['uuid']) + + # Note(TheJulia): Per the Ironic state code, a node + # that fails returns to enroll state, which means a failed + # node cannot be determined at this point in time. + if machine['provision_state'] in ['enroll']: + self.node_set_provision_state( + machine['uuid'], 'manage') + elif machine['provision_state'] in ['manageable']: + self.node_set_provision_state( + machine['uuid'], 'provide') + elif machine['last_error'] is not None: + raise exc.OpenStackCloudException( + "Machine encountered a failure: %s" + % machine['last_error']) + + # Note(TheJulia): Earlier versions of Ironic default to + # None and later versions default to available up until + # the introduction of enroll state. + # Note(TheJulia): The node will transition through + # cleaning if it is enabled, and we will wait for + # completion. + elif machine['provision_state'] in ['available', None]: + break + + else: + if machine['provision_state'] in ['enroll']: + self.node_set_provision_state(machine['uuid'], 'manage') + # Note(TheJulia): We need to wait for the lock to clear + # before we attempt to set the machine into provide state + # which allows for the transition to available. + for count in utils.iterate_timeout( + lock_timeout, + "Timeout waiting for reservation to clear " + "before setting provide state"): + machine = self.get_machine(machine['uuid']) + if (machine['reservation'] is None + and machine['provision_state'] != 'enroll'): + # NOTE(TheJulia): In this case, the node has + # has moved on from the previous state and is + # likely not being verified, as no lock is + # present on the node. + self.node_set_provision_state( + machine['uuid'], 'provide') + machine = self.get_machine(machine['uuid']) + break + + elif machine['provision_state'] in [ + 'cleaning', + 'available']: + break + + elif machine['last_error'] is not None: + raise exc.OpenStackCloudException( + "Machine encountered a failure: %s" + % machine['last_error']) + if not isinstance(machine, str): + return self._normalize_machine(machine) + else: + return machine + + def unregister_machine(self, nics, uuid, wait=False, timeout=600): + """Unregister Baremetal from Ironic + + Removes entries for Network Interfaces and baremetal nodes + from an Ironic API + + :param nics: An array of strings that consist of MAC addresses + to be removed. + :param string uuid: The UUID of the node to be deleted. + + :param wait: Boolean value, defaults to false, if to block the method + upon the final step of unregistering the machine. + + :param timeout: Integer value, representing seconds with a default + value of 600, which controls the maximum amount of + time to block the method's completion on. + + :raises: OpenStackCloudException on operation failure. + """ + + machine = self.get_machine(uuid) + invalid_states = ['active', 'cleaning', 'clean wait', 'clean failed'] + if machine['provision_state'] in invalid_states: + raise exc.OpenStackCloudException( + "Error unregistering node '%s' due to current provision " + "state '%s'" % (uuid, machine['provision_state'])) + + # NOTE(TheJulia) There is a high possibility of a lock being present + # if the machine was just moved through the state machine. This was + # previously concealed by exception retry logic that detected the + # failure, and resubitted the request in python-ironicclient. + try: + self.wait_for_baremetal_node_lock(machine, timeout=timeout) + except exc.OpenStackCloudException as e: + raise exc.OpenStackCloudException( + "Error unregistering node '%s': Exception occured while" + " waiting to be able to proceed: %s" % (machine['uuid'], e)) + + for nic in nics: + port_msg = ("Error removing NIC {nic} from baremetal API for " + "node {uuid}").format(nic=nic, uuid=uuid) + port_url = '/ports/detail?address={mac}'.format(mac=nic['mac']) + port = self._baremetal_client.get(port_url, microversion=1.6, + error_message=port_msg) + port_url = '/ports/{uuid}'.format(uuid=port['ports'][0]['uuid']) + _utils._call_client_and_retry(self._baremetal_client.delete, + port_url, retry_on=[409, 503], + error_message=port_msg) + + with _utils.shade_exceptions( + "Error unregistering machine {node_id} from the baremetal " + "API".format(node_id=uuid)): + + # NOTE(TheJulia): While this should not matter microversion wise, + # ironic assumes all calls without an explicit microversion to be + # version 1.0. Ironic expects to deprecate support for older + # microversions in future releases, as such, we explicitly set + # the version to what we have been using with the client library.. + version = "1.6" + msg = "Baremetal machine failed to be deleted" + url = '/nodes/{node_id}'.format( + node_id=uuid) + _utils._call_client_and_retry(self._baremetal_client.delete, + url, retry_on=[409, 503], + error_message=msg, + microversion=version) + + if wait: + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for machine to be deleted"): + if not self.get_machine(uuid): + break + + def patch_machine(self, name_or_id, patch): + """Patch Machine Information + + This method allows for an interface to manipulate node entries + within Ironic. + + :param string name_or_id: A machine name or UUID to be updated. + :param patch: + The JSON Patch document is a list of dictonary objects + that comply with RFC 6902 which can be found at + https://tools.ietf.org/html/rfc6902. + + Example patch construction:: + + patch=[] + patch.append({ + 'op': 'remove', + 'path': '/instance_info' + }) + patch.append({ + 'op': 'replace', + 'path': '/name', + 'value': 'newname' + }) + patch.append({ + 'op': 'add', + 'path': '/driver_info/username', + 'value': 'administrator' + }) + + :raises: OpenStackCloudException on operation error. + + :returns: ``munch.Munch`` representing the newly updated node. + """ + node = self.baremetal.get_node(name_or_id) + microversion = node._get_microversion_for(self._baremetal_client, + 'commit') + msg = ("Error updating machine via patch operation on node " + "{node}".format(node=name_or_id)) + url = '/nodes/{node_id}'.format(node_id=node.id) + return self._normalize_machine( + self._baremetal_client.patch(url, + json=patch, + microversion=microversion, + error_message=msg)) + + def update_machine(self, name_or_id, **attrs): + """Update a machine with new configuration information + + A user-friendly method to perform updates of a machine, in whole or + part. + + :param string name_or_id: A machine name or UUID to be updated. + :param attrs: Attributes to updated on the machine. + + :raises: OpenStackCloudException on operation error. + + :returns: ``munch.Munch`` containing a machine sub-dictonary consisting + of the updated data returned from the API update operation, + and a list named changes which contains all of the API paths + that received updates. + """ + machine = self.get_machine(name_or_id) + if not machine: + raise exc.OpenStackCloudException( + "Machine update failed to find Machine: %s. " % name_or_id) + + new_config = dict(machine, **attrs) + + try: + patch = jsonpatch.JsonPatch.from_diff(machine, new_config) + except Exception as e: + raise exc.OpenStackCloudException( + "Machine update failed - Error generating JSON patch object " + "for submission to the API. Machine: %s Error: %s" + % (name_or_id, e)) + + if not patch: + return dict( + node=machine, + changes=None + ) + + change_list = [change['path'] for change in patch] + node = self.baremetal.update_node(machine, **attrs) + return dict( + node=self._normalize_machine(node._to_munch()), + changes=change_list + ) + + def attach_port_to_machine(self, name_or_id, port_name_or_id): + """Attach a virtual port to the bare metal machine. + + :param string name_or_id: A machine name or UUID. + :param string port_name_or_id: A port name or UUID. + Note that this is a Network service port, not a bare metal NIC. + :return: Nothing. + """ + machine = self.get_machine(name_or_id) + port = self.get_port(port_name_or_id) + self.baremetal.attach_vif_to_node(machine, port['id']) + + def detach_port_from_machine(self, name_or_id, port_name_or_id): + """Detach a virtual port from the bare metal machine. + + :param string name_or_id: A machine name or UUID. + :param string port_name_or_id: A port name or UUID. + Note that this is a Network service port, not a bare metal NIC. + :return: Nothing. + """ + machine = self.get_machine(name_or_id) + port = self.get_port(port_name_or_id) + self.baremetal.detach_vif_from_node(machine, port['id']) + + def list_ports_attached_to_machine(self, name_or_id): + """List virtual ports attached to the bare metal machine. + + :param string name_or_id: A machine name or UUID. + :returns: List of ``munch.Munch`` representing the ports. + """ + machine = self.get_machine(name_or_id) + vif_ids = self.baremetal.list_node_vifs(machine) + return [self.get_port(vif) for vif in vif_ids] + + def validate_machine(self, name_or_id, for_deploy=True): + """Validate parameters of the machine. + + :param string name_or_id: The Name or UUID value representing the + baremetal node. + :param bool for_deploy: If ``True``, validate readiness for deployment, + otherwise validate only the power management + properties. + :raises: :exc:`~openstack.exceptions.ValidationException` + """ + if for_deploy: + ifaces = ('boot', 'deploy', 'management', 'power') + else: + ifaces = ('power',) + self.baremetal.validate_node(name_or_id, required=ifaces) + + def validate_node(self, uuid): + warnings.warn('validate_node is deprecated, please use ' + 'validate_machine instead', DeprecationWarning) + self.baremetal.validate_node(uuid) + + def node_set_provision_state(self, + name_or_id, + state, + configdrive=None, + wait=False, + timeout=3600): + """Set Node Provision State + + Enables a user to provision a Machine and optionally define a + config drive to be utilized. + + :param string name_or_id: The Name or UUID value representing the + baremetal node. + :param string state: The desired provision state for the + baremetal node. + :param string configdrive: An optional URL or file or path + representing the configdrive. In the + case of a directory, the client API + will create a properly formatted + configuration drive file and post the + file contents to the API for + deployment. + :param boolean wait: A boolean value, defaulted to false, to control + if the method will wait for the desire end state + to be reached before returning. + :param integer timeout: Integer value, defaulting to 3600 seconds, + representing the amount of time to wait for + the desire end state to be reached. + + :raises: OpenStackCloudException on operation error. + + :returns: ``munch.Munch`` representing the current state of the machine + upon exit of the method. + """ + node = self.baremetal.set_node_provision_state( + name_or_id, target=state, config_drive=configdrive, + wait=wait, timeout=timeout) + return node._to_munch() + + def set_machine_maintenance_state( + self, + name_or_id, + state=True, + reason=None): + """Set Baremetal Machine Maintenance State + + Sets Baremetal maintenance state and maintenance reason. + + :param string name_or_id: The Name or UUID value representing the + baremetal node. + :param boolean state: The desired state of the node. True being in + maintenance where as False means the machine + is not in maintenance mode. This value + defaults to True if not explicitly set. + :param string reason: An optional freeform string that is supplied to + the baremetal API to allow for notation as to why + the node is in maintenance state. + + :raises: OpenStackCloudException on operation error. + + :returns: None + """ + if state: + self.baremetal.set_node_maintenance(name_or_id, reason) + else: + self.baremetal.unset_node_maintenance(name_or_id) + + def remove_machine_from_maintenance(self, name_or_id): + """Remove Baremetal Machine from Maintenance State + + Similarly to set_machine_maintenance_state, this method + removes a machine from maintenance state. It must be noted + that this method simpily calls set_machine_maintenace_state + for the name_or_id requested and sets the state to False. + + :param string name_or_id: The Name or UUID value representing the + baremetal node. + + :raises: OpenStackCloudException on operation error. + + :returns: None + """ + self.baremetal.unset_node_maintenance(name_or_id) + + def set_machine_power_on(self, name_or_id): + """Activate baremetal machine power + + This is a method that sets the node power state to "on". + + :params string name_or_id: A string representing the baremetal + node to have power turned to an "on" + state. + + :raises: OpenStackCloudException on operation error. + + :returns: None + """ + self.baremetal.set_node_power_state(name_or_id, 'power on') + + def set_machine_power_off(self, name_or_id): + """De-activate baremetal machine power + + This is a method that sets the node power state to "off". + + :params string name_or_id: A string representing the baremetal + node to have power turned to an "off" + state. + + :raises: OpenStackCloudException on operation error. + + :returns: + """ + self.baremetal.set_node_power_state(name_or_id, 'power off') + + def set_machine_power_reboot(self, name_or_id): + """De-activate baremetal machine power + + This is a method that sets the node power state to "reboot", which + in essence changes the machine power state to "off", and that back + to "on". + + :params string name_or_id: A string representing the baremetal + node to have power turned to an "off" + state. + + :raises: OpenStackCloudException on operation error. + + :returns: None + """ + self.baremetal.set_node_power_state(name_or_id, 'rebooting') + + def activate_node(self, uuid, configdrive=None, + wait=False, timeout=1200): + self.node_set_provision_state( + uuid, 'active', configdrive, wait=wait, timeout=timeout) + + def deactivate_node(self, uuid, wait=False, + timeout=1200): + self.node_set_provision_state( + uuid, 'deleted', wait=wait, timeout=timeout) + + def set_node_instance_info(self, uuid, patch): + msg = ("Error updating machine via patch operation on node " + "{node}".format(node=uuid)) + url = '/nodes/{node_id}'.format(node_id=uuid) + return self._baremetal_client.patch(url, + json=patch, + error_message=msg) + + def purge_node_instance_info(self, uuid): + patch = [] + patch.append({'op': 'remove', 'path': '/instance_info'}) + msg = ("Error updating machine via patch operation on node " + "{node}".format(node=uuid)) + url = '/nodes/{node_id}'.format(node_id=uuid) + return self._baremetal_client.patch(url, + json=patch, + error_message=msg) + + def wait_for_baremetal_node_lock(self, node, timeout=30): + """Wait for a baremetal node to have no lock. + + DEPRECATED, use ``wait_for_node_reservation`` on the `baremetal` proxy. + + :raises: OpenStackCloudException upon client failure. + :returns: None + """ + warnings.warn("The wait_for_baremetal_node_lock call is deprecated " + "in favor of wait_for_node_reservation on the baremetal " + "proxy", DeprecationWarning) + self.baremetal.wait_for_node_reservation(node, timeout) diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py new file mode 100644 index 000000000..d0ede10f8 --- /dev/null +++ b/openstack/cloud/_block_storage.py @@ -0,0 +1,870 @@ +# 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 types so that we can reference ListType in sphinx param declarations. +# We can't just use list, because sphinx gets confused by +# openstack.resource.Resource.list and openstack.resource2.Resource.list +import types # noqa +import warnings + +from openstack.cloud import exc +from openstack.cloud import _normalize +from openstack.cloud import _utils +from openstack import proxy +from openstack import utils + + +def _no_pending_volumes(volumes): + """If there are any volumes not in a steady state, don't cache""" + for volume in volumes: + if volume['status'] not in ('available', 'error', 'in-use'): + return False + return True + + +class BlockStorageCloudMixin(_normalize.Normalizer): + + @property + def _volume_client(self): + if 'block-storage' not in self._raw_clients: + client = self._get_raw_client('block-storage') + self._raw_clients['block-storage'] = client + return self._raw_clients['block-storage'] + + @_utils.cache_on_arguments(should_cache_fn=_no_pending_volumes) + def list_volumes(self, cache=True): + """List all available volumes. + + :returns: A list of volume ``munch.Munch``. + + """ + def _list(data): + volumes.extend(data.get('volumes', [])) + endpoint = None + for l in data.get('volumes_links', []): + if 'rel' in l and 'next' == l['rel']: + endpoint = l['href'] + break + if endpoint: + try: + _list(self._volume_client.get(endpoint)) + except exc.OpenStackCloudURINotFound: + # Catch and re-raise here because we are making recursive + # calls and we just have context for the log here + self.log.debug( + "While listing volumes, could not find next link" + " {link}.".format(link=data)) + raise + + if not cache: + warnings.warn('cache argument to list_volumes is deprecated. Use ' + 'invalidate instead.') + + # Fetching paginated volumes can fails for several reasons, if + # something goes wrong we'll have to start fetching volumes from + # scratch + attempts = 5 + for _ in range(attempts): + volumes = [] + data = self._volume_client.get('/volumes/detail') + if 'volumes_links' not in data: + # no pagination needed + volumes.extend(data.get('volumes', [])) + break + + try: + _list(data) + break + except exc.OpenStackCloudURINotFound: + pass + else: + self.log.debug( + "List volumes failed to retrieve all volumes after" + " {attempts} attempts. Returning what we found.".format( + attempts=attempts)) + # list volumes didn't complete succesfully so just return what + # we found + return self._normalize_volumes( + self._get_and_munchify(key=None, data=volumes)) + + @_utils.cache_on_arguments() + def list_volume_types(self, get_extra=True): + """List all available volume types. + + :returns: A list of volume ``munch.Munch``. + + """ + data = self._volume_client.get( + '/types', + params=dict(is_public='None'), + error_message='Error fetching volume_type list') + return self._normalize_volume_types( + self._get_and_munchify('volume_types', data)) + + def get_volume(self, name_or_id, filters=None): + """Get a volume by name or ID. + + :param name_or_id: Name or ID of the volume. + :param filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: A volume ``munch.Munch`` or None if no matching volume is + found. + + """ + return _utils._get_entity(self, 'volume', name_or_id, filters) + + def get_volume_by_id(self, id): + """ Get a volume by ID + + :param id: ID of the volume. + :returns: A volume ``munch.Munch``. + """ + data = self._volume_client.get( + '/volumes/{id}'.format(id=id), + error_message="Error getting volume with ID {id}".format(id=id) + ) + volume = self._normalize_volume( + self._get_and_munchify('volume', data)) + + return volume + + def get_volume_type(self, name_or_id, filters=None): + """Get a volume type by name or ID. + + :param name_or_id: Name or ID of the volume. + :param filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: A volume ``munch.Munch`` or None if no matching volume is + found. + + """ + return _utils._get_entity( + self, 'volume_type', name_or_id, filters) + + def create_volume( + self, size, + wait=True, timeout=None, image=None, bootable=None, **kwargs): + """Create a volume. + + :param size: Size, in GB of the volume to create. + :param name: (optional) Name for the volume. + :param description: (optional) Name for the volume. + :param wait: If true, waits for volume to be created. + :param timeout: Seconds to wait for volume creation. None is forever. + :param image: (optional) Image name, ID or object from which to create + the volume + :param bootable: (optional) Make this volume bootable. If set, wait + will also be set to true. + :param kwargs: Keyword arguments as expected for cinder client. + + :returns: The created volume object. + + :raises: OpenStackCloudTimeout if wait time exceeded. + :raises: OpenStackCloudException on operation error. + """ + if bootable is not None: + wait = True + + if image: + image_obj = self.get_image(image) + if not image_obj: + raise exc.OpenStackCloudException( + "Image {image} was requested as the basis for a new" + " volume, but was not found on the cloud".format( + image=image)) + kwargs['imageRef'] = image_obj['id'] + kwargs = self._get_volume_kwargs(kwargs) + kwargs['size'] = size + payload = dict(volume=kwargs) + if 'scheduler_hints' in kwargs: + payload['OS-SCH-HNT:scheduler_hints'] = kwargs.pop( + 'scheduler_hints', None) + data = self._volume_client.post( + '/volumes', + json=dict(payload), + error_message='Error in creating volume') + volume = self._get_and_munchify('volume', data) + self.list_volumes.invalidate(self) + + if volume['status'] == 'error': + raise exc.OpenStackCloudException("Error in creating volume") + + if wait: + vol_id = volume['id'] + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for the volume to be available."): + volume = self.get_volume(vol_id) + + if not volume: + continue + + if volume['status'] == 'available': + if bootable is not None: + self.set_volume_bootable(volume, bootable=bootable) + # no need to re-fetch to update the flag, just set it. + volume['bootable'] = bootable + return volume + + if volume['status'] == 'error': + raise exc.OpenStackCloudException("Error creating volume") + + return self._normalize_volume(volume) + + def update_volume(self, name_or_id, **kwargs): + kwargs = self._get_volume_kwargs(kwargs) + + volume = self.get_volume(name_or_id) + if not volume: + raise exc.OpenStackCloudException( + "Volume %s not found." % name_or_id) + + data = self._volume_client.put( + '/volumes/{volume_id}'.format(volume_id=volume.id), + json=dict({'volume': kwargs}), + error_message='Error updating volume') + + self.list_volumes.invalidate(self) + + return self._normalize_volume(self._get_and_munchify('volume', data)) + + def set_volume_bootable(self, name_or_id, bootable=True): + """Set a volume's bootable flag. + + :param name_or_id: Name, unique ID of the volume or a volume dict. + :param bool bootable: Whether the volume should be bootable. + (Defaults to True) + + :raises: OpenStackCloudTimeout if wait time exceeded. + :raises: OpenStackCloudException on operation error. + """ + + volume = self.get_volume(name_or_id) + + if not volume: + raise exc.OpenStackCloudException( + "Volume {name_or_id} does not exist".format( + name_or_id=name_or_id)) + + self._volume_client.post( + 'volumes/{id}/action'.format(id=volume['id']), + json={'os-set_bootable': {'bootable': bootable}}, + error_message="Error setting bootable on volume {volume}".format( + volume=volume['id']) + ) + + def delete_volume(self, name_or_id=None, wait=True, timeout=None, + force=False): + """Delete a volume. + + :param name_or_id: Name or unique ID of the volume. + :param wait: If true, waits for volume to be deleted. + :param timeout: Seconds to wait for volume deletion. None is forever. + :param force: Force delete volume even if the volume is in deleting + or error_deleting state. + + :raises: OpenStackCloudTimeout if wait time exceeded. + :raises: OpenStackCloudException on operation error. + """ + + self.list_volumes.invalidate(self) + volume = self.get_volume(name_or_id) + + if not volume: + self.log.debug( + "Volume %(name_or_id)s does not exist", + {'name_or_id': name_or_id}, + exc_info=True) + return False + + with _utils.shade_exceptions("Error in deleting volume"): + try: + if force: + self._volume_client.post( + 'volumes/{id}/action'.format(id=volume['id']), + json={'os-force_delete': None}) + else: + self._volume_client.delete( + 'volumes/{id}'.format(id=volume['id'])) + except exc.OpenStackCloudURINotFound: + self.log.debug( + "Volume {id} not found when deleting. Ignoring.".format( + id=volume['id'])) + return False + + self.list_volumes.invalidate(self) + if wait: + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for the volume to be deleted."): + + if not self.get_volume(volume['id']): + break + + return True + + def get_volumes(self, server, cache=True): + volumes = [] + for volume in self.list_volumes(cache=cache): + for attach in volume['attachments']: + if attach['server_id'] == server['id']: + volumes.append(volume) + return volumes + + def get_volume_limits(self, name_or_id=None): + """ Get volume limits for a project + + :param name_or_id: (optional) project name or ID to get limits for + if different from the current project + :raises: OpenStackCloudException if it's not a valid project + + :returns: Munch object with the limits + """ + params = {} + project_id = None + error_msg = "Failed to get limits" + if name_or_id: + + proj = self.get_project(name_or_id) + if not proj: + raise exc.OpenStackCloudException("project does not exist") + project_id = proj.id + params['tenant_id'] = project_id + error_msg = "{msg} for the project: {project} ".format( + msg=error_msg, project=name_or_id) + + data = self._volume_client.get('/limits', params=params) + limits = self._get_and_munchify('limits', data) + return limits + + def get_volume_id(self, name_or_id): + volume = self.get_volume(name_or_id) + if volume: + return volume['id'] + return None + + def volume_exists(self, name_or_id): + return self.get_volume(name_or_id) is not None + + def get_volume_attach_device(self, volume, server_id): + """Return the device name a volume is attached to for a server. + + This can also be used to verify if a volume is attached to + a particular server. + + :param volume: Volume dict + :param server_id: ID of server to check + + :returns: Device name if attached, None if volume is not attached. + """ + for attach in volume['attachments']: + if server_id == attach['server_id']: + return attach['device'] + return None + + def detach_volume(self, server, volume, wait=True, timeout=None): + """Detach a volume from a server. + + :param server: The server dict to detach from. + :param volume: The volume dict to detach. + :param wait: If true, waits for volume to be detached. + :param timeout: Seconds to wait for volume detachment. None is forever. + + :raises: OpenStackCloudTimeout if wait time exceeded. + :raises: OpenStackCloudException on operation error. + """ + + proxy._json_response(self.compute.delete( + '/servers/{server_id}/os-volume_attachments/{volume_id}'.format( + server_id=server['id'], volume_id=volume['id'])), + error_message=( + "Error detaching volume {volume} from server {server}".format( + volume=volume['id'], server=server['id']))) + + if wait: + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for volume %s to detach." % volume['id']): + try: + vol = self.get_volume(volume['id']) + except Exception: + self.log.debug( + "Error getting volume info %s", volume['id'], + exc_info=True) + continue + + if vol['status'] == 'available': + return + + if vol['status'] == 'error': + raise exc.OpenStackCloudException( + "Error in detaching volume %s" % volume['id'] + ) + + def attach_volume(self, server, volume, device=None, + wait=True, timeout=None): + """Attach a volume to a server. + + This will attach a volume, described by the passed in volume + dict (as returned by get_volume()), to the server described by + the passed in server dict (as returned by get_server()) on the + named device on the server. + + If the volume is already attached to the server, or generally not + available, then an exception is raised. To re-attach to a server, + but under a different device, the user must detach it first. + + :param server: The server dict to attach to. + :param volume: The volume dict to attach. + :param device: The device name where the volume will attach. + :param wait: If true, waits for volume to be attached. + :param timeout: Seconds to wait for volume attachment. None is forever. + + :returns: a volume attachment object. + + :raises: OpenStackCloudTimeout if wait time exceeded. + :raises: OpenStackCloudException on operation error. + """ + dev = self.get_volume_attach_device(volume, server['id']) + if dev: + raise exc.OpenStackCloudException( + "Volume %s already attached to server %s on device %s" + % (volume['id'], server['id'], dev) + ) + + if volume['status'] != 'available': + raise exc.OpenStackCloudException( + "Volume %s is not available. Status is '%s'" + % (volume['id'], volume['status']) + ) + + payload = {'volumeId': volume['id']} + if device: + payload['device'] = device + data = proxy._json_response( + self.compute.post( + '/servers/{server_id}/os-volume_attachments'.format( + server_id=server['id']), + json=dict(volumeAttachment=payload)), + error_message="Error attaching volume {volume_id} to server " + "{server_id}".format(volume_id=volume['id'], + server_id=server['id'])) + + if wait: + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for volume %s to attach." % volume['id']): + try: + self.list_volumes.invalidate(self) + vol = self.get_volume(volume['id']) + except Exception: + self.log.debug( + "Error getting volume info %s", volume['id'], + exc_info=True) + continue + + if self.get_volume_attach_device(vol, server['id']): + break + + # TODO(Shrews) check to see if a volume can be in error status + # and also attached. If so, we should move this + # above the get_volume_attach_device call + if vol['status'] == 'error': + raise exc.OpenStackCloudException( + "Error in attaching volume %s" % volume['id'] + ) + return self._normalize_volume_attachment( + self._get_and_munchify('volumeAttachment', data)) + + def _get_volume_kwargs(self, kwargs): + name = kwargs.pop('name', kwargs.pop('display_name', None)) + description = kwargs.pop('description', + kwargs.pop('display_description', None)) + if name: + if self._is_client_version('volume', 2): + kwargs['name'] = name + else: + kwargs['display_name'] = name + if description: + if self._is_client_version('volume', 2): + kwargs['description'] = description + else: + kwargs['display_description'] = description + return kwargs + + @_utils.valid_kwargs('name', 'display_name', + 'description', 'display_description') + def create_volume_snapshot(self, volume_id, force=False, + wait=True, timeout=None, **kwargs): + """Create a volume. + + :param volume_id: the ID of the volume to snapshot. + :param force: If set to True the snapshot will be created even if the + volume is attached to an instance, if False it will not + :param name: name of the snapshot, one will be generated if one is + not provided + :param description: description of the snapshot, one will be generated + if one is not provided + :param wait: If true, waits for volume snapshot to be created. + :param timeout: Seconds to wait for volume snapshot creation. None is + forever. + + :returns: The created volume object. + + :raises: OpenStackCloudTimeout if wait time exceeded. + :raises: OpenStackCloudException on operation error. + """ + + kwargs = self._get_volume_kwargs(kwargs) + payload = {'volume_id': volume_id, 'force': force} + payload.update(kwargs) + data = self._volume_client.post( + '/snapshots', + json=dict(snapshot=payload), + error_message="Error creating snapshot of volume " + "{volume_id}".format(volume_id=volume_id)) + snapshot = self._get_and_munchify('snapshot', data) + if wait: + snapshot_id = snapshot['id'] + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for the volume snapshot to be available." + ): + snapshot = self.get_volume_snapshot_by_id(snapshot_id) + + if snapshot['status'] == 'available': + break + + if snapshot['status'] == 'error': + raise exc.OpenStackCloudException( + "Error in creating volume snapshot") + + # TODO(mordred) need to normalize snapshots. We were normalizing them + # as volumes, which is an error. They need to be normalized as + # volume snapshots, which are completely different objects + return snapshot + + def get_volume_snapshot_by_id(self, snapshot_id): + """Takes a snapshot_id and gets a dict of the snapshot + that maches that ID. + + Note: This is more efficient than get_volume_snapshot. + + param: snapshot_id: ID of the volume snapshot. + + """ + data = self._volume_client.get( + '/snapshots/{snapshot_id}'.format(snapshot_id=snapshot_id), + error_message="Error getting snapshot " + "{snapshot_id}".format(snapshot_id=snapshot_id)) + return self._normalize_volume( + self._get_and_munchify('snapshot', data)) + + def get_volume_snapshot(self, name_or_id, filters=None): + """Get a volume by name or ID. + + :param name_or_id: Name or ID of the volume snapshot. + :param filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: A volume ``munch.Munch`` or None if no matching volume is + found. + """ + return _utils._get_entity(self, 'volume_snapshot', name_or_id, + filters) + + def create_volume_backup(self, volume_id, name=None, description=None, + force=False, wait=True, timeout=None): + """Create a volume backup. + + :param volume_id: the ID of the volume to backup. + :param name: name of the backup, one will be generated if one is + not provided + :param description: description of the backup, one will be generated + if one is not provided + :param force: If set to True the backup will be created even if the + volume is attached to an instance, if False it will not + :param wait: If true, waits for volume backup to be created. + :param timeout: Seconds to wait for volume backup creation. None is + forever. + + :returns: The created volume backup object. + + :raises: OpenStackCloudTimeout if wait time exceeded. + :raises: OpenStackCloudException on operation error. + """ + payload = { + 'name': name, + 'volume_id': volume_id, + 'description': description, + 'force': force, + } + + data = self._volume_client.post( + '/backups', json=dict(backup=payload), + error_message="Error creating backup of volume " + "{volume_id}".format(volume_id=volume_id)) + backup = self._get_and_munchify('backup', data) + + if wait: + backup_id = backup['id'] + msg = ("Timeout waiting for the volume backup {} to be " + "available".format(backup_id)) + for _ in utils.iterate_timeout(timeout, msg): + backup = self.get_volume_backup(backup_id) + + if backup['status'] == 'available': + break + + if backup['status'] == 'error': + raise exc.OpenStackCloudException( + "Error in creating volume backup {id}".format( + id=backup_id)) + + return backup + + def get_volume_backup(self, name_or_id, filters=None): + """Get a volume backup by name or ID. + + :returns: A backup ``munch.Munch`` or None if no matching backup is + found. + """ + return _utils._get_entity(self, 'volume_backup', name_or_id, + filters) + + def list_volume_snapshots(self, detailed=True, search_opts=None): + """List all volume snapshots. + + :returns: A list of volume snapshots ``munch.Munch``. + + """ + endpoint = '/snapshots/detail' if detailed else '/snapshots' + data = self._volume_client.get( + endpoint, + params=search_opts, + error_message="Error getting a list of snapshots") + return self._get_and_munchify('snapshots', data) + + def list_volume_backups(self, detailed=True, search_opts=None): + """ + List all volume backups. + + :param bool detailed: Also list details for each entry + :param dict search_opts: Search options + A dictionary of meta data to use for further filtering. Example:: + + { + 'name': 'my-volume-backup', + 'status': 'available', + 'volume_id': 'e126044c-7b4c-43be-a32a-c9cbbc9ddb56', + 'all_tenants': 1 + } + + :returns: A list of volume backups ``munch.Munch``. + """ + endpoint = '/backups/detail' if detailed else '/backups' + data = self._volume_client.get( + endpoint, params=search_opts, + error_message="Error getting a list of backups") + return self._get_and_munchify('backups', data) + + def delete_volume_backup(self, name_or_id=None, force=False, wait=False, + timeout=None): + """Delete a volume backup. + + :param name_or_id: Name or unique ID of the volume backup. + :param force: Allow delete in state other than error or available. + :param wait: If true, waits for volume backup to be deleted. + :param timeout: Seconds to wait for volume backup deletion. None is + forever. + + :raises: OpenStackCloudTimeout if wait time exceeded. + :raises: OpenStackCloudException on operation error. + """ + + volume_backup = self.get_volume_backup(name_or_id) + + if not volume_backup: + return False + + msg = "Error in deleting volume backup" + if force: + self._volume_client.post( + '/backups/{backup_id}/action'.format( + backup_id=volume_backup['id']), + json={'os-force_delete': None}, + error_message=msg) + else: + self._volume_client.delete( + '/backups/{backup_id}'.format( + backup_id=volume_backup['id']), + error_message=msg) + if wait: + msg = "Timeout waiting for the volume backup to be deleted." + for count in utils.iterate_timeout(timeout, msg): + if not self.get_volume_backup(volume_backup['id']): + break + + return True + + def delete_volume_snapshot(self, name_or_id=None, wait=False, + timeout=None): + """Delete a volume snapshot. + + :param name_or_id: Name or unique ID of the volume snapshot. + :param wait: If true, waits for volume snapshot to be deleted. + :param timeout: Seconds to wait for volume snapshot deletion. None is + forever. + + :raises: OpenStackCloudTimeout if wait time exceeded. + :raises: OpenStackCloudException on operation error. + """ + + volumesnapshot = self.get_volume_snapshot(name_or_id) + + if not volumesnapshot: + return False + + self._volume_client.delete( + '/snapshots/{snapshot_id}'.format( + snapshot_id=volumesnapshot['id']), + error_message="Error in deleting volume snapshot") + + if wait: + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for the volume snapshot to be deleted."): + if not self.get_volume_snapshot(volumesnapshot['id']): + break + + return True + + def search_volumes(self, name_or_id=None, filters=None): + volumes = self.list_volumes() + return _utils._filter_list( + volumes, name_or_id, filters) + + def search_volume_snapshots(self, name_or_id=None, filters=None): + volumesnapshots = self.list_volume_snapshots() + return _utils._filter_list( + volumesnapshots, name_or_id, filters) + + def search_volume_backups(self, name_or_id=None, filters=None): + volume_backups = self.list_volume_backups() + return _utils._filter_list( + volume_backups, name_or_id, filters) + + def search_volume_types( + self, name_or_id=None, filters=None, get_extra=True): + volume_types = self.list_volume_types(get_extra=get_extra) + return _utils._filter_list(volume_types, name_or_id, filters) + + def get_volume_type_access(self, name_or_id): + """Return a list of volume_type_access. + + :param name_or_id: Name or ID of the volume type. + + :raises: OpenStackCloudException on operation error. + """ + volume_type = self.get_volume_type(name_or_id) + if not volume_type: + raise exc.OpenStackCloudException( + "VolumeType not found: %s" % name_or_id) + + data = self._volume_client.get( + '/types/{id}/os-volume-type-access'.format(id=volume_type.id), + error_message="Unable to get volume type access" + " {name}".format(name=name_or_id)) + return self._normalize_volume_type_accesses( + self._get_and_munchify('volume_type_access', data)) + + def add_volume_type_access(self, name_or_id, project_id): + """Grant access on a volume_type to a project. + + :param name_or_id: ID or name of a volume_type + :param project_id: A project id + + NOTE: the call works even if the project does not exist. + + :raises: OpenStackCloudException on operation error. + """ + volume_type = self.get_volume_type(name_or_id) + if not volume_type: + raise exc.OpenStackCloudException( + "VolumeType not found: %s" % name_or_id) + with _utils.shade_exceptions(): + payload = {'project': project_id} + self._volume_client.post( + '/types/{id}/action'.format(id=volume_type.id), + json=dict(addProjectAccess=payload), + error_message="Unable to authorize {project} " + "to use volume type {name}".format( + name=name_or_id, project=project_id)) + + def remove_volume_type_access(self, name_or_id, project_id): + """Revoke access on a volume_type to a project. + + :param name_or_id: ID or name of a volume_type + :param project_id: A project id + + :raises: OpenStackCloudException on operation error. + """ + volume_type = self.get_volume_type(name_or_id) + if not volume_type: + raise exc.OpenStackCloudException( + "VolumeType not found: %s" % name_or_id) + with _utils.shade_exceptions(): + payload = {'project': project_id} + self._volume_client.post( + '/types/{id}/action'.format(id=volume_type.id), + json=dict(removeProjectAccess=payload), + error_message="Unable to revoke {project} " + "to use volume type {name}".format( + name=name_or_id, project=project_id)) diff --git a/openstack/cloud/_clustering.py b/openstack/cloud/_clustering.py new file mode 100644 index 000000000..ca45befde --- /dev/null +++ b/openstack/cloud/_clustering.py @@ -0,0 +1,567 @@ +# 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 types so that we can reference ListType in sphinx param declarations. +# We can't just use list, because sphinx gets confused by +# openstack.resource.Resource.list and openstack.resource2.Resource.list +import types # noqa + +from openstack.cloud import exc +from openstack.cloud import _normalize +from openstack.cloud import _utils +from openstack import utils + + +class ClusteringCloudMixin(_normalize.Normalizer): + + @property + def _clustering_client(self): + if 'clustering' not in self._raw_clients: + clustering_client = self._get_versioned_client( + 'clustering', min_version=1, max_version='1.latest') + self._raw_clients['clustering'] = clustering_client + return self._raw_clients['clustering'] + + def create_cluster(self, name, profile, config=None, desired_capacity=0, + max_size=None, metadata=None, min_size=None, + timeout=None): + profile = self.get_cluster_profile(profile) + profile_id = profile['id'] + body = { + 'desired_capacity': desired_capacity, + 'name': name, + 'profile_id': profile_id + } + + if config is not None: + body['config'] = config + + if max_size is not None: + body['max_size'] = max_size + + if metadata is not None: + body['metadata'] = metadata + + if min_size is not None: + body['min_size'] = min_size + + if timeout is not None: + body['timeout'] = timeout + + data = self._clustering_client.post( + '/clusters', json={'cluster': body}, + error_message="Error creating cluster {name}".format(name=name)) + + return self._get_and_munchify(key=None, data=data) + + def set_cluster_metadata(self, name_or_id, metadata): + cluster = self.get_cluster(name_or_id) + if not cluster: + raise exc.OpenStackCloudException( + 'Invalid Cluster {cluster}'.format(cluster=name_or_id)) + + self._clustering_client.post( + '/clusters/{cluster_id}/metadata'.format(cluster_id=cluster['id']), + json={'metadata': metadata}, + error_message='Error updating cluster metadata') + + def get_cluster_by_id(self, cluster_id): + try: + data = self._clustering_client.get( + "/clusters/{cluster_id}".format(cluster_id=cluster_id), + error_message="Error fetching cluster {name}".format( + name=cluster_id)) + return self._get_and_munchify('cluster', data) + except Exception: + return None + + def get_cluster(self, name_or_id, filters=None): + return _utils._get_entity( + cloud=self, resource='cluster', + name_or_id=name_or_id, filters=filters) + + def update_cluster(self, name_or_id, new_name=None, + profile_name_or_id=None, config=None, metadata=None, + timeout=None, profile_only=False): + old_cluster = self.get_cluster(name_or_id) + if old_cluster is None: + raise exc.OpenStackCloudException( + 'Invalid Cluster {cluster}'.format(cluster=name_or_id)) + cluster = { + 'profile_only': profile_only + } + + if config is not None: + cluster['config'] = config + + if metadata is not None: + cluster['metadata'] = metadata + + if profile_name_or_id is not None: + profile = self.get_cluster_profile(profile_name_or_id) + if profile is None: + raise exc.OpenStackCloudException( + 'Invalid Cluster Profile {profile}'.format( + profile=profile_name_or_id)) + cluster['profile_id'] = profile.id + + if timeout is not None: + cluster['timeout'] = timeout + + if new_name is not None: + cluster['name'] = new_name + + data = self._clustering_client.patch( + "/clusters/{cluster_id}".format(cluster_id=old_cluster['id']), + json={'cluster': cluster}, + error_message="Error updating cluster " + "{name}".format(name=name_or_id)) + + return self._get_and_munchify(key=None, data=data) + + def delete_cluster(self, name_or_id): + cluster = self.get_cluster(name_or_id) + if cluster is None: + self.log.debug("Cluster %s not found for deleting", name_or_id) + return False + + for policy in self.list_policies_on_cluster(name_or_id): + detach_policy = self.get_cluster_policy_by_id( + policy['policy_id']) + self.detach_policy_from_cluster(cluster, detach_policy) + + for receiver in self.list_cluster_receivers(): + if cluster["id"] == receiver["cluster_id"]: + self.delete_cluster_receiver(receiver["id"], wait=True) + + self._clustering_client.delete( + "/clusters/{cluster_id}".format(cluster_id=name_or_id), + error_message="Error deleting cluster {name}".format( + name=name_or_id)) + + return True + + def search_clusters(self, name_or_id=None, filters=None): + clusters = self.list_clusters() + return _utils._filter_list(clusters, name_or_id, filters) + + def list_clusters(self): + try: + data = self._clustering_client.get( + '/clusters', + error_message="Error fetching clusters") + return self._get_and_munchify('clusters', data) + except exc.OpenStackCloudURINotFound as e: + self.log.debug(str(e), exc_info=True) + return [] + + def attach_policy_to_cluster(self, name_or_id, policy_name_or_id, + is_enabled): + cluster = self.get_cluster(name_or_id) + policy = self.get_cluster_policy(policy_name_or_id) + if cluster is None: + raise exc.OpenStackCloudException( + 'Cluster {cluster} not found for attaching'.format( + cluster=name_or_id)) + + if policy is None: + raise exc.OpenStackCloudException( + 'Policy {policy} not found for attaching'.format( + policy=policy_name_or_id)) + + body = { + 'policy_id': policy['id'], + 'enabled': is_enabled + } + + self._clustering_client.post( + "/clusters/{cluster_id}/actions".format(cluster_id=cluster['id']), + error_message="Error attaching policy {policy} to cluster " + "{cluster}".format( + policy=policy['id'], + cluster=cluster['id']), + json={'policy_attach': body}) + + return True + + def detach_policy_from_cluster( + self, name_or_id, policy_name_or_id, wait=False, timeout=3600): + cluster = self.get_cluster(name_or_id) + policy = self.get_cluster_policy(policy_name_or_id) + if cluster is None: + raise exc.OpenStackCloudException( + 'Cluster {cluster} not found for detaching'.format( + cluster=name_or_id)) + + if policy is None: + raise exc.OpenStackCloudException( + 'Policy {policy} not found for detaching'.format( + policy=policy_name_or_id)) + + body = {'policy_id': policy['id']} + self._clustering_client.post( + "/clusters/{cluster_id}/actions".format(cluster_id=cluster['id']), + error_message="Error detaching policy {policy} from cluster " + "{cluster}".format( + policy=policy['id'], + cluster=cluster['id']), + json={'policy_detach': body}) + + if not wait: + return True + + value = [] + + for count in utils.iterate_timeout( + timeout, "Timeout waiting for cluster policy to detach"): + + # TODO(bjjohnson) This logic will wait until there are no policies. + # Since we're detaching a specific policy, checking to make sure + # that policy is not in the list of policies would be better. + policy_status = self.get_cluster_by_id(cluster['id'])['policies'] + + if policy_status == value: + break + return True + + def update_policy_on_cluster(self, name_or_id, policy_name_or_id, + is_enabled): + cluster = self.get_cluster(name_or_id) + policy = self.get_cluster_policy(policy_name_or_id) + if cluster is None: + raise exc.OpenStackCloudException( + 'Cluster {cluster} not found for updating'.format( + cluster=name_or_id)) + + if policy is None: + raise exc.OpenStackCloudException( + 'Policy {policy} not found for updating'.format( + policy=policy_name_or_id)) + + body = { + 'policy_id': policy['id'], + 'enabled': is_enabled + } + self._clustering_client.post( + "/clusters/{cluster_id}/actions".format(cluster_id=cluster['id']), + error_message="Error updating policy {policy} on cluster " + "{cluster}".format( + policy=policy['id'], + cluster=cluster['id']), + json={'policy_update': body}) + + return True + + def get_policy_on_cluster(self, name_or_id, policy_name_or_id): + try: + policy = self._clustering_client.get( + "/clusters/{cluster_id}/policies/{policy_id}".format( + cluster_id=name_or_id, policy_id=policy_name_or_id), + error_message="Error fetching policy " + "{name}".format(name=policy_name_or_id)) + return self._get_and_munchify('cluster_policy', policy) + except Exception: + return False + + def list_policies_on_cluster(self, name_or_id): + endpoint = "/clusters/{cluster_id}/policies".format( + cluster_id=name_or_id) + try: + data = self._clustering_client.get( + endpoint, + error_message="Error fetching cluster policies") + except exc.OpenStackCloudURINotFound as e: + self.log.debug(str(e), exc_info=True) + return [] + return self._get_and_munchify('cluster_policies', data) + + def create_cluster_profile(self, name, spec, metadata=None): + profile = { + 'name': name, + 'spec': spec + } + + if metadata is not None: + profile['metadata'] = metadata + + data = self._clustering_client.post( + '/profiles', json={'profile': profile}, + error_message="Error creating profile {name}".format(name=name)) + + return self._get_and_munchify('profile', data) + + def set_cluster_profile_metadata(self, name_or_id, metadata): + profile = self.get_cluster_profile(name_or_id) + if not profile: + raise exc.OpenStackCloudException( + 'Invalid Profile {profile}'.format(profile=name_or_id)) + + self._clustering_client.post( + '/profiles/{profile_id}/metadata'.format(profile_id=profile['id']), + json={'metadata': metadata}, + error_message='Error updating profile metadata') + + def search_cluster_profiles(self, name_or_id=None, filters=None): + cluster_profiles = self.list_cluster_profiles() + return _utils._filter_list(cluster_profiles, name_or_id, filters) + + def list_cluster_profiles(self): + try: + data = self._clustering_client.get( + '/profiles', + error_message="Error fetching profiles") + except exc.OpenStackCloudURINotFound as e: + self.log.debug(str(e), exc_info=True) + return [] + return self._get_and_munchify('profiles', data) + + def get_cluster_profile_by_id(self, profile_id): + try: + data = self._clustering_client.get( + "/profiles/{profile_id}".format(profile_id=profile_id), + error_message="Error fetching profile {name}".format( + name=profile_id)) + return self._get_and_munchify('profile', data) + except exc.OpenStackCloudURINotFound as e: + self.log.debug(str(e), exc_info=True) + return None + + def get_cluster_profile(self, name_or_id, filters=None): + return _utils._get_entity(self, 'cluster_profile', name_or_id, filters) + + def delete_cluster_profile(self, name_or_id): + profile = self.get_cluster_profile(name_or_id) + if profile is None: + self.log.debug("Profile %s not found for deleting", name_or_id) + return False + + for cluster in self.list_clusters(): + if (name_or_id, profile.id) in cluster.items(): + self.log.debug( + "Profile %s is being used by cluster %s, won't delete", + name_or_id, cluster.name) + return False + + self._clustering_client.delete( + "/profiles/{profile_id}".format(profile_id=profile['id']), + error_message="Error deleting profile " + "{name}".format(name=name_or_id)) + + return True + + def update_cluster_profile(self, name_or_id, metadata=None, new_name=None): + old_profile = self.get_cluster_profile(name_or_id) + if not old_profile: + raise exc.OpenStackCloudException( + 'Invalid Profile {profile}'.format(profile=name_or_id)) + + profile = {} + + if metadata is not None: + profile['metadata'] = metadata + + if new_name is not None: + profile['name'] = new_name + + data = self._clustering_client.patch( + "/profiles/{profile_id}".format(profile_id=old_profile.id), + json={'profile': profile}, + error_message="Error updating profile {name}".format( + name=name_or_id)) + + return self._get_and_munchify(key=None, data=data) + + def create_cluster_policy(self, name, spec): + policy = { + 'name': name, + 'spec': spec + } + + data = self._clustering_client.post( + '/policies', json={'policy': policy}, + error_message="Error creating policy {name}".format( + name=policy['name'])) + return self._get_and_munchify('policy', data) + + def search_cluster_policies(self, name_or_id=None, filters=None): + cluster_policies = self.list_cluster_policies() + return _utils._filter_list(cluster_policies, name_or_id, filters) + + def list_cluster_policies(self): + endpoint = "/policies" + try: + data = self._clustering_client.get( + endpoint, + error_message="Error fetching cluster policies") + except exc.OpenStackCloudURINotFound as e: + self.log.debug(str(e), exc_info=True) + return [] + return self._get_and_munchify('policies', data) + + def get_cluster_policy_by_id(self, policy_id): + try: + data = self._clustering_client.get( + "/policies/{policy_id}".format(policy_id=policy_id), + error_message="Error fetching policy {name}".format( + name=policy_id)) + return self._get_and_munchify('policy', data) + except exc.OpenStackCloudURINotFound as e: + self.log.debug(str(e), exc_info=True) + return None + + def get_cluster_policy(self, name_or_id, filters=None): + return _utils._get_entity( + self, 'cluster_policie', name_or_id, filters) + + def delete_cluster_policy(self, name_or_id): + policy = self.get_cluster_policy_by_id(name_or_id) + if policy is None: + self.log.debug("Policy %s not found for deleting", name_or_id) + return False + + for cluster in self.list_clusters(): + if (name_or_id, policy.id) in cluster.items(): + self.log.debug( + "Policy %s is being used by cluster %s, won't delete", + name_or_id, cluster.name) + return False + + self._clustering_client.delete( + "/policies/{policy_id}".format(policy_id=name_or_id), + error_message="Error deleting policy " + "{name}".format(name=name_or_id)) + + return True + + def update_cluster_policy(self, name_or_id, new_name): + old_policy = self.get_cluster_policy(name_or_id) + if not old_policy: + raise exc.OpenStackCloudException( + 'Invalid Policy {policy}'.format(policy=name_or_id)) + policy = {'name': new_name} + + data = self._clustering_client.patch( + "/policies/{policy_id}".format(policy_id=old_policy.id), + json={'policy': policy}, + error_message="Error updating policy " + "{name}".format(name=name_or_id)) + return self._get_and_munchify(key=None, data=data) + + def create_cluster_receiver(self, name, receiver_type, + cluster_name_or_id=None, action=None, + actor=None, params=None): + cluster = self.get_cluster(cluster_name_or_id) + if cluster is None: + raise exc.OpenStackCloudException( + 'Invalid cluster {cluster}'.format(cluster=cluster_name_or_id)) + + receiver = { + 'name': name, + 'type': receiver_type + } + + if cluster_name_or_id is not None: + receiver['cluster_id'] = cluster.id + + if action is not None: + receiver['action'] = action + + if actor is not None: + receiver['actor'] = actor + + if params is not None: + receiver['params'] = params + + data = self._clustering_client.post( + '/receivers', json={'receiver': receiver}, + error_message="Error creating receiver {name}".format(name=name)) + return self._get_and_munchify('receiver', data) + + def search_cluster_receivers(self, name_or_id=None, filters=None): + cluster_receivers = self.list_cluster_receivers() + return _utils._filter_list(cluster_receivers, name_or_id, filters) + + def list_cluster_receivers(self): + try: + data = self._clustering_client.get( + '/receivers', + error_message="Error fetching receivers") + except exc.OpenStackCloudURINotFound as e: + self.log.debug(str(e), exc_info=True) + return [] + return self._get_and_munchify('receivers', data) + + def get_cluster_receiver_by_id(self, receiver_id): + try: + data = self._clustering_client.get( + "/receivers/{receiver_id}".format(receiver_id=receiver_id), + error_message="Error fetching receiver {name}".format( + name=receiver_id)) + return self._get_and_munchify('receiver', data) + except exc.OpenStackCloudURINotFound as e: + self.log.debug(str(e), exc_info=True) + return None + + def get_cluster_receiver(self, name_or_id, filters=None): + return _utils._get_entity( + self, 'cluster_receiver', name_or_id, filters) + + def delete_cluster_receiver(self, name_or_id, wait=False, timeout=3600): + receiver = self.get_cluster_receiver(name_or_id) + if receiver is None: + self.log.debug("Receiver %s not found for deleting", name_or_id) + return False + + receiver_id = receiver['id'] + + self._clustering_client.delete( + "/receivers/{receiver_id}".format(receiver_id=receiver_id), + error_message="Error deleting receiver {name}".format( + name=name_or_id)) + + if not wait: + return True + + for count in utils.iterate_timeout( + timeout, "Timeout waiting for cluster receiver to delete"): + + receiver = self.get_cluster_receiver_by_id(receiver_id) + + if not receiver: + break + + return True + + def update_cluster_receiver(self, name_or_id, new_name=None, action=None, + params=None): + old_receiver = self.get_cluster_receiver(name_or_id) + if old_receiver is None: + raise exc.OpenStackCloudException( + 'Invalid receiver {receiver}'.format(receiver=name_or_id)) + + receiver = {} + + if new_name is not None: + receiver['name'] = new_name + + if action is not None: + receiver['action'] = action + + if params is not None: + receiver['params'] = params + + data = self._clustering_client.patch( + "/receivers/{receiver_id}".format(receiver_id=old_receiver.id), + json={'receiver': receiver}, + error_message="Error updating receiver {name}".format( + name=name_or_id)) + return self._get_and_munchify(key=None, data=data) diff --git a/openstack/cloud/_coe.py b/openstack/cloud/_coe.py new file mode 100644 index 000000000..0a025d3ea --- /dev/null +++ b/openstack/cloud/_coe.py @@ -0,0 +1,425 @@ +# 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 types so that we can reference ListType in sphinx param declarations. +# We can't just use list, because sphinx gets confused by +# openstack.resource.Resource.list and openstack.resource2.Resource.list +import types # noqa + +from openstack.cloud import exc +from openstack.cloud import _normalize +from openstack.cloud import _utils + + +class CoeCloudMixin(_normalize.Normalizer): + + @property + def _container_infra_client(self): + if 'container-infra' not in self._raw_clients: + self._raw_clients['container-infra'] = self._get_raw_client( + 'container-infra') + return self._raw_clients['container-infra'] + + @_utils.cache_on_arguments() + def list_coe_clusters(self): + """List COE(Ccontainer Orchestration Engine) cluster. + + :returns: a list of dicts containing the cluster. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + with _utils.shade_exceptions("Error fetching cluster list"): + data = self._container_infra_client.get('/clusters') + return self._normalize_coe_clusters( + self._get_and_munchify('clusters', data)) + + def search_coe_clusters( + self, name_or_id=None, filters=None): + """Search COE cluster. + + :param name_or_id: cluster name or ID. + :param filters: a dict containing additional filters to use. + :param detail: a boolean to control if we need summarized or + detailed output. + + :returns: a list of dict containing the cluster + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + coe_clusters = self.list_coe_clusters() + return _utils._filter_list( + coe_clusters, name_or_id, filters) + + def get_coe_cluster(self, name_or_id, filters=None): + """Get a COE cluster by name or ID. + + :param name_or_id: Name or ID of the cluster. + :param filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: A cluster dict or None if no matching cluster is found. + """ + return _utils._get_entity(self, 'coe_cluster', name_or_id, + filters=filters) + + def create_coe_cluster( + self, name, cluster_template_id, **kwargs): + """Create a COE cluster based on given cluster template. + + :param string name: Name of the cluster. + :param string image_id: ID of the cluster template to use. + + Other arguments will be passed in kwargs. + + :returns: a dict containing the cluster description + + :raises: ``OpenStackCloudException`` if something goes wrong during + the OpenStack API call + """ + error_message = ("Error creating cluster of name" + " {cluster_name}".format(cluster_name=name)) + with _utils.shade_exceptions(error_message): + body = kwargs.copy() + body['name'] = name + body['cluster_template_id'] = cluster_template_id + + cluster = self._container_infra_client.post( + '/clusters', json=body) + + self.list_coe_clusters.invalidate(self) + return cluster + + def delete_coe_cluster(self, name_or_id): + """Delete a COE cluster. + + :param name_or_id: Name or unique ID of the cluster. + :returns: True if the delete succeeded, False if the + cluster was not found. + + :raises: OpenStackCloudException on operation error. + """ + + cluster = self.get_coe_cluster(name_or_id) + + if not cluster: + self.log.debug( + "COE Cluster %(name_or_id)s does not exist", + {'name_or_id': name_or_id}, + exc_info=True) + return False + + with _utils.shade_exceptions("Error in deleting COE cluster"): + self._container_infra_client.delete( + '/clusters/{id}'.format(id=cluster['id'])) + self.list_coe_clusters.invalidate(self) + + return True + + @_utils.valid_kwargs('node_count') + def update_coe_cluster(self, name_or_id, operation, **kwargs): + """Update a COE cluster. + + :param name_or_id: Name or ID of the COE cluster being updated. + :param operation: Operation to perform - add, remove, replace. + + Other arguments will be passed with kwargs. + + :returns: a dict representing the updated cluster. + + :raises: OpenStackCloudException on operation error. + """ + self.list_coe_clusters.invalidate(self) + cluster = self.get_coe_cluster(name_or_id) + if not cluster: + raise exc.OpenStackCloudException( + "COE cluster %s not found." % name_or_id) + + if operation not in ['add', 'replace', 'remove']: + raise TypeError( + "%s operation not in 'add', 'replace', 'remove'" % operation) + + patches = _utils.generate_patches_from_kwargs(operation, **kwargs) + # No need to fire an API call if there is an empty patch + if not patches: + return cluster + + with _utils.shade_exceptions( + "Error updating COE cluster {0}".format(name_or_id)): + self._container_infra_client.patch( + '/clusters/{id}'.format(id=cluster['id']), + json=patches) + + new_cluster = self.get_coe_cluster(name_or_id) + return new_cluster + + def get_coe_cluster_certificate(self, cluster_id): + """Get details about the CA certificate for a cluster by name or ID. + + :param cluster_id: ID of the cluster. + + :returns: Details about the CA certificate for the given cluster. + """ + msg = ("Error fetching CA cert for the cluster {cluster_id}".format( + cluster_id=cluster_id)) + url = "/certificates/{cluster_id}".format(cluster_id=cluster_id) + data = self._container_infra_client.get(url, + error_message=msg) + + return self._get_and_munchify(key=None, data=data) + + def sign_coe_cluster_certificate(self, cluster_id, csr): + """Sign client key and generate the CA certificate for a cluster + + :param cluster_id: UUID of the cluster. + :param csr: Certificate Signing Request (CSR) for authenticating + client key.The CSR will be used by Magnum to generate + a signed certificate that client will use to communicate + with the cluster. + + :returns: a dict representing the signed certs. + + :raises: OpenStackCloudException on operation error. + """ + error_message = ("Error signing certs for cluster" + " {cluster_id}".format(cluster_id=cluster_id)) + with _utils.shade_exceptions(error_message): + body = {} + body['cluster_uuid'] = cluster_id + body['csr'] = csr + + certs = self._container_infra_client.post( + '/certificates', json=body) + + return self._get_and_munchify(key=None, data=certs) + + @_utils.cache_on_arguments() + def list_cluster_templates(self, detail=False): + """List cluster templates. + + :param bool detail. Ignored. Included for backwards compat. + ClusterTemplates are always returned with full details. + + :returns: a list of dicts containing the cluster template details. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + with _utils.shade_exceptions("Error fetching cluster template list"): + try: + data = self._container_infra_client.get('/clustertemplates') + # NOTE(flwang): Magnum adds /clustertemplates and /cluster + # to deprecate /baymodels and /bay since Newton release. So + # we're using a small tag to indicate if current + # cloud has those two new API endpoints. + self._container_infra_client._has_magnum_after_newton = True + return self._normalize_cluster_templates( + self._get_and_munchify('clustertemplates', data)) + except exc.OpenStackCloudURINotFound: + data = self._container_infra_client.get('/baymodels/detail') + return self._normalize_cluster_templates( + self._get_and_munchify('baymodels', data)) + list_baymodels = list_cluster_templates + list_coe_cluster_templates = list_cluster_templates + + def search_cluster_templates( + self, name_or_id=None, filters=None, detail=False): + """Search cluster templates. + + :param name_or_id: cluster template name or ID. + :param filters: a dict containing additional filters to use. + :param detail: a boolean to control if we need summarized or + detailed output. + + :returns: a list of dict containing the cluster templates + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + cluster_templates = self.list_cluster_templates(detail=detail) + return _utils._filter_list( + cluster_templates, name_or_id, filters) + search_baymodels = search_cluster_templates + search_coe_cluster_templates = search_cluster_templates + + def get_cluster_template(self, name_or_id, filters=None, detail=False): + """Get a cluster template by name or ID. + + :param name_or_id: Name or ID of the cluster template. + :param filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: A cluster template dict or None if no matching + cluster template is found. + """ + return _utils._get_entity(self, 'cluster_template', name_or_id, + filters=filters, detail=detail) + get_baymodel = get_cluster_template + get_coe_cluster_template = get_cluster_template + + def create_cluster_template( + self, name, image_id=None, keypair_id=None, coe=None, **kwargs): + """Create a cluster template. + + :param string name: Name of the cluster template. + :param string image_id: Name or ID of the image to use. + :param string keypair_id: Name or ID of the keypair to use. + :param string coe: Name of the coe for the cluster template. + + Other arguments will be passed in kwargs. + + :returns: a dict containing the cluster template description + + :raises: ``OpenStackCloudException`` if something goes wrong during + the OpenStack API call + """ + error_message = ("Error creating cluster template of name" + " {cluster_template_name}".format( + cluster_template_name=name)) + with _utils.shade_exceptions(error_message): + body = kwargs.copy() + body['name'] = name + body['image_id'] = image_id + body['keypair_id'] = keypair_id + body['coe'] = coe + + try: + cluster_template = self._container_infra_client.post( + '/clustertemplates', json=body) + self._container_infra_client._has_magnum_after_newton = True + except exc.OpenStackCloudURINotFound: + cluster_template = self._container_infra_client.post( + '/baymodels', json=body) + + self.list_cluster_templates.invalidate(self) + return cluster_template + create_baymodel = create_cluster_template + create_coe_cluster_template = create_cluster_template + + def delete_cluster_template(self, name_or_id): + """Delete a cluster template. + + :param name_or_id: Name or unique ID of the cluster template. + :returns: True if the delete succeeded, False if the + cluster template was not found. + + :raises: OpenStackCloudException on operation error. + """ + + cluster_template = self.get_cluster_template(name_or_id) + + if not cluster_template: + self.log.debug( + "Cluster template %(name_or_id)s does not exist", + {'name_or_id': name_or_id}, + exc_info=True) + return False + + with _utils.shade_exceptions("Error in deleting cluster template"): + if getattr(self._container_infra_client, + '_has_magnum_after_newton', False): + self._container_infra_client.delete( + '/clustertemplates/{id}'.format(id=cluster_template['id'])) + else: + self._container_infra_client.delete( + '/baymodels/{id}'.format(id=cluster_template['id'])) + self.list_cluster_templates.invalidate(self) + + return True + delete_baymodel = delete_cluster_template + delete_coe_cluster_template = delete_cluster_template + + @_utils.valid_kwargs('name', 'image_id', 'flavor_id', 'master_flavor_id', + 'keypair_id', 'external_network_id', 'fixed_network', + 'dns_nameserver', 'docker_volume_size', 'labels', + 'coe', 'http_proxy', 'https_proxy', 'no_proxy', + 'network_driver', 'tls_disabled', 'public', + 'registry_enabled', 'volume_driver') + def update_cluster_template(self, name_or_id, operation, **kwargs): + """Update a cluster template. + + :param name_or_id: Name or ID of the cluster template being updated. + :param operation: Operation to perform - add, remove, replace. + + Other arguments will be passed with kwargs. + + :returns: a dict representing the updated cluster template. + + :raises: OpenStackCloudException on operation error. + """ + self.list_cluster_templates.invalidate(self) + cluster_template = self.get_cluster_template(name_or_id) + if not cluster_template: + raise exc.OpenStackCloudException( + "Cluster template %s not found." % name_or_id) + + if operation not in ['add', 'replace', 'remove']: + raise TypeError( + "%s operation not in 'add', 'replace', 'remove'" % operation) + + patches = _utils.generate_patches_from_kwargs(operation, **kwargs) + # No need to fire an API call if there is an empty patch + if not patches: + return cluster_template + + with _utils.shade_exceptions( + "Error updating cluster template {0}".format(name_or_id)): + if getattr(self._container_infra_client, + '_has_magnum_after_newton', False): + self._container_infra_client.patch( + '/clustertemplates/{id}'.format(id=cluster_template['id']), + json=patches) + else: + self._container_infra_client.patch( + '/baymodels/{id}'.format(id=cluster_template['id']), + json=patches) + + new_cluster_template = self.get_cluster_template(name_or_id) + return new_cluster_template + update_baymodel = update_cluster_template + update_coe_cluster_template = update_cluster_template + + def list_magnum_services(self): + """List all Magnum services. + :returns: a list of dicts containing the service details. + + :raises: OpenStackCloudException on operation error. + """ + with _utils.shade_exceptions("Error fetching Magnum services list"): + data = self._container_infra_client.get('/mservices') + return self._normalize_magnum_services( + self._get_and_munchify('mservices', data)) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py new file mode 100644 index 000000000..522a5d542 --- /dev/null +++ b/openstack/cloud/_compute.py @@ -0,0 +1,1917 @@ +# 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 types so that we can reference ListType in sphinx param declarations. +# We can't just use list, because sphinx gets confused by +# openstack.resource.Resource.list and openstack.resource2.Resource.list +import base64 +import datetime +import functools +import iso8601 +import operator +import six +import threading +import time +import types # noqa + +from openstack.cloud import exc +from openstack.cloud import meta +from openstack.cloud import _normalize +from openstack.cloud import _utils +from openstack import proxy +from openstack import utils + + +class ComputeCloudMixin(_normalize.Normalizer): + + def __init__(self): + self._servers = None + self._servers_time = 0 + self._servers_lock = threading.Lock() + + def get_flavor_name(self, flavor_id): + flavor = self.get_flavor(flavor_id, get_extra=False) + if flavor: + return flavor['name'] + return None + + def get_flavor_by_ram(self, ram, include=None, get_extra=True): + """Get a flavor based on amount of RAM available. + + Finds the flavor with the least amount of RAM that is at least + as much as the specified amount. If `include` is given, further + filter based on matching flavor name. + + :param int ram: Minimum amount of RAM. + :param string include: If given, will return a flavor whose name + contains this string as a substring. + """ + flavors = self.list_flavors(get_extra=get_extra) + for flavor in sorted(flavors, key=operator.itemgetter('ram')): + if (flavor['ram'] >= ram + and (not include or include in flavor['name'])): + return flavor + raise exc.OpenStackCloudException( + "Could not find a flavor with {ram} and '{include}'".format( + ram=ram, include=include)) + + @_utils.cache_on_arguments() + def _nova_extensions(self): + extensions = set() + data = proxy._json_response( + self.compute.get('/extensions'), + error_message="Error fetching extension list for nova") + + for extension in self._get_and_munchify('extensions', data): + extensions.add(extension['alias']) + return extensions + + def _has_nova_extension(self, extension_name): + return extension_name in self._nova_extensions() + + def search_keypairs(self, name_or_id=None, filters=None): + keypairs = self.list_keypairs() + return _utils._filter_list(keypairs, name_or_id, filters) + + def search_flavors(self, name_or_id=None, filters=None, get_extra=True): + flavors = self.list_flavors(get_extra=get_extra) + return _utils._filter_list(flavors, name_or_id, filters) + + def search_security_groups(self, name_or_id=None, filters=None): + # `filters` could be a dict or a jmespath (str) + groups = self.list_security_groups( + filters=filters if isinstance(filters, dict) else None + ) + return _utils._filter_list(groups, name_or_id, filters) + + def search_servers( + self, name_or_id=None, filters=None, detailed=False, + all_projects=False, bare=False): + servers = self.list_servers( + detailed=detailed, all_projects=all_projects, bare=bare) + return _utils._filter_list(servers, name_or_id, filters) + + def search_server_groups(self, name_or_id=None, filters=None): + """Seach server groups. + + :param name: server group name or ID. + :param filters: a dict containing additional filters to use. + + :returns: a list of dicts containing the server groups + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + server_groups = self.list_server_groups() + return _utils._filter_list(server_groups, name_or_id, filters) + + def list_keypairs(self): + """List all available keypairs. + + :returns: A list of ``munch.Munch`` containing keypair info. + + """ + data = proxy._json_response( + self.compute.get('/os-keypairs'), + error_message="Error fetching keypair list") + return self._normalize_keypairs([ + k['keypair'] for k in self._get_and_munchify('keypairs', data)]) + + @_utils.cache_on_arguments() + def list_availability_zone_names(self, unavailable=False): + """List names of availability zones. + + :param bool unavailable: Whether or not to include unavailable zones + in the output. Defaults to False. + + :returns: A list of availability zone names, or an empty list if the + list could not be fetched. + """ + try: + data = proxy._json_response( + self.compute.get('/os-availability-zone')) + except exc.OpenStackCloudHTTPError: + self.log.debug( + "Availability zone list could not be fetched", + exc_info=True) + return [] + zones = self._get_and_munchify('availabilityZoneInfo', data) + ret = [] + for zone in zones: + if zone['zoneState']['available'] or unavailable: + ret.append(zone['zoneName']) + return ret + + @_utils.cache_on_arguments() + def list_flavors(self, get_extra=False): + """List all available flavors. + + :param get_extra: Whether or not to fetch extra specs for each flavor. + Defaults to True. Default behavior value can be + overridden in clouds.yaml by setting + openstack.cloud.get_extra_specs to False. + :returns: A list of flavor ``munch.Munch``. + + """ + data = proxy._json_response( + self.compute.get( + '/flavors/detail', params=dict(is_public='None')), + error_message="Error fetching flavor list") + flavors = self._normalize_flavors( + self._get_and_munchify('flavors', data)) + + for flavor in flavors: + if not flavor.extra_specs and get_extra: + endpoint = "/flavors/{id}/os-extra_specs".format( + id=flavor.id) + try: + data = proxy._json_response( + self.compute.get(endpoint), + error_message="Error fetching flavor extra specs") + flavor.extra_specs = self._get_and_munchify( + 'extra_specs', data) + except exc.OpenStackCloudHTTPError as e: + flavor.extra_specs = {} + self.log.debug( + 'Fetching extra specs for flavor failed:' + ' %(msg)s', {'msg': str(e)}) + + return flavors + + def list_server_security_groups(self, server): + """List all security groups associated with the given server. + + :returns: A list of security group ``munch.Munch``. + """ + + # Don't even try if we're a cloud that doesn't have them + if not self._has_secgroups(): + return [] + + data = proxy._json_response( + self.compute.get( + '/servers/{server_id}/os-security-groups'.format( + server_id=server['id']))) + return self._normalize_secgroups( + self._get_and_munchify('security_groups', data)) + + def _get_server_security_groups(self, server, security_groups): + if not self._has_secgroups(): + raise exc.OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + + if not isinstance(server, dict): + server = self.get_server(server, bare=True) + + if server is None: + self.log.debug('Server %s not found', server) + return None, None + + if not isinstance(security_groups, (list, tuple)): + security_groups = [security_groups] + + sec_group_objs = [] + + for sg in security_groups: + if not isinstance(sg, dict): + sg = self.get_security_group(sg) + + if sg is None: + self.log.debug('Security group %s not found for adding', + sg) + + return None, None + + sec_group_objs.append(sg) + + return server, sec_group_objs + + def add_server_security_groups(self, server, security_groups): + """Add security groups to a server. + + Add existing security groups to an existing server. If the security + groups are already present on the server this will continue unaffected. + + :returns: False if server or security groups are undefined, True + otherwise. + + :raises: ``OpenStackCloudException``, on operation error. + """ + server, security_groups = self._get_server_security_groups( + server, security_groups) + + if not (server and security_groups): + return False + + for sg in security_groups: + proxy._json_response(self.compute.post( + '/servers/%s/action' % server['id'], + json={'addSecurityGroup': {'name': sg.name}})) + + return True + + def remove_server_security_groups(self, server, security_groups): + """Remove security groups from a server + + Remove existing security groups from an existing server. If the + security groups are not present on the server this will continue + unaffected. + + :returns: False if server or security groups are undefined, True + otherwise. + + :raises: ``OpenStackCloudException``, on operation error. + """ + server, security_groups = self._get_server_security_groups( + server, security_groups) + + if not (server and security_groups): + return False + + ret = True + + for sg in security_groups: + try: + proxy._json_response(self.compute.post( + '/servers/%s/action' % server['id'], + json={'removeSecurityGroup': {'name': sg.name}})) + + except exc.OpenStackCloudURINotFound: + # NOTE(jamielennox): Is this ok? If we remove something that + # isn't present should we just conclude job done or is that an + # error? Nova returns ok if you try to add a group twice. + self.log.debug( + "The security group %s was not present on server %s so " + "no action was performed", sg.name, server.name) + ret = False + + return ret + + def list_servers(self, detailed=False, all_projects=False, bare=False, + filters=None): + """List all available servers. + + :param detailed: Whether or not to add detailed additional information. + Defaults to False. + :param all_projects: Whether to list servers from all projects or just + the current auth scoped project. + :param bare: Whether to skip adding any additional information to the + server record. Defaults to False, meaning the addresses + dict will be populated as needed from neutron. Setting + to True implies detailed = False. + :param filters: Additional query parameters passed to the API server. + + :returns: A list of server ``munch.Munch``. + + """ + # If pushdown filters are specified and we do not have batched caching + # enabled, bypass local caching and push down the filters. + if filters and self._SERVER_AGE == 0: + return self._list_servers( + detailed=detailed, + all_projects=all_projects, + bare=bare, + filters=filters, + ) + + if (time.time() - self._servers_time) >= self._SERVER_AGE: + # Since we're using cached data anyway, we don't need to + # have more than one thread actually submit the list + # servers task. Let the first one submit it while holding + # a lock, and the non-blocking acquire method will cause + # subsequent threads to just skip this and use the old + # data until it succeeds. + # Initially when we never got data, block to retrieve some data. + first_run = self._servers is None + if self._servers_lock.acquire(first_run): + try: + if not (first_run and self._servers is not None): + self._servers = self._list_servers( + detailed=detailed, + all_projects=all_projects, + bare=bare) + self._servers_time = time.time() + finally: + self._servers_lock.release() + # Wrap the return with filter_list so that if filters were passed + # but we were batching/caching and thus always fetching the whole + # list from the cloud, we still return a filtered list. + return _utils._filter_list(self._servers, None, filters) + + def _list_servers(self, detailed=False, all_projects=False, bare=False, + filters=None): + filters = filters or {} + servers = [ + # TODO(mordred) Add original_names=False here and update the + # normalize file for server. Then, just remove the normalize call + # and the to_munch call. + self._normalize_server(server._to_munch()) + for server in self.compute.servers( + all_projects=all_projects, **filters)] + return [ + self._expand_server(server, detailed, bare) + for server in servers + ] + + def list_server_groups(self): + """List all available server groups. + + :returns: A list of server group dicts. + + """ + data = proxy._json_response( + self.compute.get('/os-server-groups'), + error_message="Error fetching server group list") + return self._get_and_munchify('server_groups', data) + + def get_compute_limits(self, name_or_id=None): + """ Get compute limits for a project + + :param name_or_id: (optional) project name or ID to get limits for + if different from the current project + :raises: OpenStackCloudException if it's not a valid project + + :returns: Munch object with the limits + """ + params = {} + project_id = None + error_msg = "Failed to get limits" + if name_or_id: + + proj = self.get_project(name_or_id) + if not proj: + raise exc.OpenStackCloudException("project does not exist") + project_id = proj.id + params['tenant_id'] = project_id + error_msg = "{msg} for the project: {project} ".format( + msg=error_msg, project=name_or_id) + + data = proxy._json_response( + self.compute.get('/limits', params=params)) + limits = self._get_and_munchify('limits', data) + return self._normalize_compute_limits(limits, project_id=project_id) + + def get_keypair(self, name_or_id, filters=None): + """Get a keypair by name or ID. + + :param name_or_id: Name or ID of the keypair. + :param filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: A keypair ``munch.Munch`` or None if no matching keypair is + found. + """ + return _utils._get_entity(self, 'keypair', name_or_id, filters) + + def get_flavor(self, name_or_id, filters=None, get_extra=True): + """Get a flavor by name or ID. + + :param name_or_id: Name or ID of the flavor. + :param filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :param get_extra: + Whether or not the list_flavors call should get the extra flavor + specs. + + :returns: A flavor ``munch.Munch`` or None if no matching flavor is + found. + + """ + search_func = functools.partial( + self.search_flavors, get_extra=get_extra) + return _utils._get_entity(self, search_func, name_or_id, filters) + + def get_flavor_by_id(self, id, get_extra=False): + """ Get a flavor by ID + + :param id: ID of the flavor. + :param get_extra: + Whether or not the list_flavors call should get the extra flavor + specs. + :returns: A flavor ``munch.Munch``. + """ + data = proxy._json_response( + self.compute.get('/flavors/{id}'.format(id=id)), + error_message="Error getting flavor with ID {id}".format(id=id) + ) + flavor = self._normalize_flavor( + self._get_and_munchify('flavor', data)) + + if not flavor.extra_specs and get_extra: + endpoint = "/flavors/{id}/os-extra_specs".format( + id=flavor.id) + try: + data = proxy._json_response( + self.compute.get(endpoint), + error_message="Error fetching flavor extra specs") + flavor.extra_specs = self._get_and_munchify( + 'extra_specs', data) + except exc.OpenStackCloudHTTPError as e: + flavor.extra_specs = {} + self.log.debug( + 'Fetching extra specs for flavor failed:' + ' %(msg)s', {'msg': str(e)}) + + return flavor + + def get_server_console(self, server, length=None): + """Get the console log for a server. + + :param server: The server to fetch the console log for. Can be either + a server dict or the Name or ID of the server. + :param int length: The number of lines you would like to retrieve from + the end of the log. (optional, defaults to all) + + :returns: A string containing the text of the console log or an + empty string if the cloud does not support console logs. + :raises: OpenStackCloudException if an invalid server argument is given + or if something else unforseen happens + """ + + if not isinstance(server, dict): + server = self.get_server(server, bare=True) + + if not server: + raise exc.OpenStackCloudException( + "Console log requested for invalid server") + + try: + return self._get_server_console_output(server['id'], length) + except exc.OpenStackCloudBadRequest: + return "" + + def _get_server_console_output(self, server_id, length=None): + data = proxy._json_response(self.compute.post( + '/servers/{server_id}/action'.format(server_id=server_id), + json={'os-getConsoleOutput': {'length': length}})) + return self._get_and_munchify('output', data) + + def get_server( + self, name_or_id=None, filters=None, detailed=False, bare=False, + all_projects=False): + """Get a server by name or ID. + + :param name_or_id: Name or ID of the server. + :param filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :param detailed: Whether or not to add detailed additional information. + Defaults to False. + :param bare: Whether to skip adding any additional information to the + server record. Defaults to False, meaning the addresses + dict will be populated as needed from neutron. Setting + to True implies detailed = False. + :param all_projects: Whether to get server from all projects or just + the current auth scoped project. + + :returns: A server ``munch.Munch`` or None if no matching server is + found. + + """ + searchfunc = functools.partial(self.search_servers, + detailed=detailed, bare=True, + all_projects=all_projects) + server = _utils._get_entity(self, searchfunc, name_or_id, filters) + return self._expand_server(server, detailed, bare) + + def _expand_server(self, server, detailed, bare): + if bare or not server: + return server + elif detailed: + return meta.get_hostvars_from_server(self, server) + else: + return meta.add_server_interfaces(self, server) + + def get_server_by_id(self, id): + data = proxy._json_response( + self.compute.get('/servers/{id}'.format(id=id))) + server = self._get_and_munchify('server', data) + return meta.add_server_interfaces(self, self._normalize_server(server)) + + def get_server_group(self, name_or_id=None, filters=None): + """Get a server group by name or ID. + + :param name_or_id: Name or ID of the server group. + :param filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'policy': 'affinity', + } + + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: A server groups dict or None if no matching server group + is found. + + """ + return _utils._get_entity(self, 'server_group', name_or_id, + filters) + + def create_keypair(self, name, public_key=None): + """Create a new keypair. + + :param name: Name of the keypair being created. + :param public_key: Public key for the new keypair. + + :raises: OpenStackCloudException on operation error. + """ + keypair = { + 'name': name, + } + if public_key: + keypair['public_key'] = public_key + data = proxy._json_response( + self.compute.post( + '/os-keypairs', + json={'keypair': keypair}), + error_message="Unable to create keypair {name}".format(name=name)) + return self._normalize_keypair( + self._get_and_munchify('keypair', data)) + + def delete_keypair(self, name): + """Delete a keypair. + + :param name: Name of the keypair to delete. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException on operation error. + """ + try: + proxy._json_response(self.compute.delete( + '/os-keypairs/{name}'.format(name=name))) + except exc.OpenStackCloudURINotFound: + self.log.debug("Keypair %s not found for deleting", name) + return False + return True + + def create_image_snapshot( + self, name, server, wait=False, timeout=3600, **metadata): + """Create an image by snapshotting an existing server. + + ..note:: + On most clouds this is a cold snapshot - meaning that the server + in question will be shutdown before taking the snapshot. It is + possible that it's a live snapshot - but there is no way to know + as a user, so caveat emptor. + + :param name: Name of the image to be created + :param server: Server name or ID or dict representing the server + to be snapshotted + :param wait: If true, waits for image to be created. + :param timeout: Seconds to wait for image creation. None is forever. + :param metadata: Metadata to give newly-created image entity + + :returns: A ``munch.Munch`` of the Image object + + :raises: OpenStackCloudException if there are problems uploading + """ + if not isinstance(server, dict): + server_obj = self.get_server(server, bare=True) + if not server_obj: + raise exc.OpenStackCloudException( + "Server {server} could not be found and therefore" + " could not be snapshotted.".format(server=server)) + server = server_obj + response = proxy._json_response( + self.compute.post( + '/servers/{server_id}/action'.format(server_id=server['id']), + json={ + "createImage": { + "name": name, + "metadata": metadata, + } + })) + # You won't believe it - wait, who am I kidding - of course you will! + # Nova returns the URL of the image created in the Location + # header of the response. (what?) But, even better, the URL it responds + # with has a very good chance of being wrong (it is built from + # nova.conf values that point to internal API servers in any cloud + # large enough to have both public and internal endpoints. + # However, nobody has ever noticed this because novaclient doesn't + # actually use that URL - it extracts the id from the end of + # the url, then returns the id. This leads us to question: + # a) why Nova is going to return a value in a header + # b) why it's going to return data that probably broken + # c) indeed the very nature of the fabric of reality + # Although it fills us with existential dread, we have no choice but + # to follow suit like a lemming being forced over a cliff by evil + # producers from Disney. + # TODO(mordred) Update this to consume json microversion when it is + # available. + # blueprint:remove-create-image-location-header-response + image_id = response.headers['Location'].rsplit('/', 1)[1] + self.list_images.invalidate(self) + image = self.get_image(image_id) + + if not wait: + return image + return self.wait_for_image(image, timeout=timeout) + + def get_server_id(self, name_or_id): + server = self.get_server(name_or_id, bare=True) + if server: + return server['id'] + return None + + def get_server_private_ip(self, server): + return meta.get_server_private_ip(server, self) + + def get_server_public_ip(self, server): + return meta.get_server_external_ipv4(self, server) + + def get_server_meta(self, server): + # TODO(mordred) remove once ansible has moved to Inventory interface + server_vars = meta.get_hostvars_from_server(self, server) + groups = meta.get_groups_from_server(self, server, server_vars) + return dict(server_vars=server_vars, groups=groups) + + @_utils.valid_kwargs( + 'meta', 'files', 'userdata', + 'reservation_id', 'return_raw', 'min_count', + 'max_count', 'security_groups', 'key_name', + 'availability_zone', 'block_device_mapping', + 'block_device_mapping_v2', 'nics', 'scheduler_hints', + 'config_drive', 'admin_pass', 'disk_config') + def create_server( + self, name, image=None, flavor=None, + auto_ip=True, ips=None, ip_pool=None, + root_volume=None, terminate_volume=False, + wait=False, timeout=180, reuse_ips=True, + network=None, boot_from_volume=False, volume_size='50', + boot_volume=None, volumes=None, nat_destination=None, + group=None, + **kwargs): + """Create a virtual server instance. + + :param name: Something to name the server. + :param image: Image dict, name or ID to boot with. image is required + unless boot_volume is given. + :param flavor: Flavor dict, name or ID to boot onto. + :param auto_ip: Whether to take actions to find a routable IP for + the server. (defaults to True) + :param ips: List of IPs to attach to the server (defaults to None) + :param ip_pool: Name of the network or floating IP pool to get an + address from. (defaults to None) + :param root_volume: Name or ID of a volume to boot from + (defaults to None - deprecated, use boot_volume) + :param boot_volume: Name or ID of a volume to boot from + (defaults to None) + :param terminate_volume: If booting from a volume, whether it should + be deleted when the server is destroyed. + (defaults to False) + :param volumes: (optional) A list of volumes to attach to the server + :param meta: (optional) A dict of arbitrary key/value metadata to + store for this server. Both keys and values must be + <=255 characters. + :param files: (optional, deprecated) A dict of files to overwrite + on the server upon boot. Keys are file names (i.e. + ``/etc/passwd``) and values + are the file contents (either as a string or as a + file-like object). A maximum of five entries is allowed, + and each file must be 10k or less. + :param reservation_id: a UUID for the set of servers being requested. + :param min_count: (optional extension) The minimum number of + servers to launch. + :param max_count: (optional extension) The maximum number of + servers to launch. + :param security_groups: A list of security group names + :param userdata: user data to pass to be exposed by the metadata + server this can be a file type object as well or a + string. + :param key_name: (optional extension) name of previously created + keypair to inject into the instance. + :param availability_zone: Name of the availability zone for instance + placement. + :param block_device_mapping: (optional) A dict of block + device mappings for this server. + :param block_device_mapping_v2: (optional) A dict of block + device mappings for this server. + :param nics: (optional extension) an ordered list of nics to be + added to this server, with information about + connected networks, fixed IPs, port etc. + :param scheduler_hints: (optional extension) arbitrary key-value pairs + specified by the client to help boot an instance + :param config_drive: (optional extension) value for config drive + either boolean, or volume-id + :param disk_config: (optional extension) control how the disk is + partitioned when the server is created. possible + values are 'AUTO' or 'MANUAL'. + :param admin_pass: (optional extension) add a user supplied admin + password. + :param wait: (optional) Wait for the address to appear as assigned + to the server. Defaults to False. + :param timeout: (optional) Seconds to wait, defaults to 60. + See the ``wait`` parameter. + :param reuse_ips: (optional) Whether to attempt to reuse pre-existing + floating ips should a floating IP be + needed (defaults to True) + :param network: (optional) Network dict or name or ID to attach the + server to. Mutually exclusive with the nics parameter. + Can also be be a list of network names or IDs or + network dicts. + :param boot_from_volume: Whether to boot from volume. 'boot_volume' + implies True, but boot_from_volume=True with + no boot_volume is valid and will create a + volume from the image and use that. + :param volume_size: When booting an image from volume, how big should + the created volume be? Defaults to 50. + :param nat_destination: Which network should a created floating IP + be attached to, if it's not possible to + infer from the cloud's configuration. + (Optional, defaults to None) + :param group: ServerGroup dict, name or id to boot the server in. + If a group is provided in both scheduler_hints and in + the group param, the group param will win. + (Optional, defaults to None) + :returns: A ``munch.Munch`` representing the created server. + :raises: OpenStackCloudException on operation error. + """ + # TODO(shade) Image is optional but flavor is not - yet flavor comes + # after image in the argument list. Doh. + if not flavor: + raise TypeError( + "create_server() missing 1 required argument: 'flavor'") + if not image and not boot_volume: + raise TypeError( + "create_server() requires either 'image' or 'boot_volume'") + + server_json = {'server': kwargs} + + # TODO(mordred) Add support for description starting in 2.19 + security_groups = kwargs.get('security_groups', []) + if security_groups and not isinstance(kwargs['security_groups'], list): + security_groups = [security_groups] + if security_groups: + kwargs['security_groups'] = [] + for sec_group in security_groups: + kwargs['security_groups'].append(dict(name=sec_group)) + if 'userdata' in kwargs: + user_data = kwargs.pop('userdata') + if user_data: + kwargs['user_data'] = self._encode_server_userdata(user_data) + for (desired, given) in ( + ('OS-DCF:diskConfig', 'disk_config'), + ('config_drive', 'config_drive'), + ('key_name', 'key_name'), + ('metadata', 'meta'), + ('adminPass', 'admin_pass')): + value = kwargs.pop(given, None) + if value: + kwargs[desired] = value + + hints = kwargs.pop('scheduler_hints', {}) + if group: + group_obj = self.get_server_group(group) + if not group_obj: + raise exc.OpenStackCloudException( + "Server Group {group} was requested but was not found" + " on the cloud".format(group=group)) + hints['group'] = group_obj['id'] + if hints: + server_json['os:scheduler_hints'] = hints + kwargs.setdefault('max_count', kwargs.get('max_count', 1)) + kwargs.setdefault('min_count', kwargs.get('min_count', 1)) + + if 'nics' in kwargs and not isinstance(kwargs['nics'], list): + if isinstance(kwargs['nics'], dict): + # Be nice and help the user out + kwargs['nics'] = [kwargs['nics']] + else: + raise exc.OpenStackCloudException( + 'nics parameter to create_server takes a list of dicts.' + ' Got: {nics}'.format(nics=kwargs['nics'])) + + if network and ('nics' not in kwargs or not kwargs['nics']): + nics = [] + if not isinstance(network, list): + network = [network] + for net_name in network: + if isinstance(net_name, dict) and 'id' in net_name: + network_obj = net_name + else: + network_obj = self.get_network(name_or_id=net_name) + if not network_obj: + raise exc.OpenStackCloudException( + 'Network {network} is not a valid network in' + ' {cloud}:{region}'.format( + network=network, + cloud=self.name, region=self.config.region_name)) + nics.append({'net-id': network_obj['id']}) + + kwargs['nics'] = nics + if not network and ('nics' not in kwargs or not kwargs['nics']): + default_network = self.get_default_network() + if default_network: + kwargs['nics'] = [{'net-id': default_network['id']}] + + networks = [] + for nic in kwargs.pop('nics', []): + net = {} + if 'net-id' in nic: + # TODO(mordred) Make sure this is in uuid format + net['uuid'] = nic.pop('net-id') + # If there's a net-id, ignore net-name + nic.pop('net-name', None) + elif 'net-name' in nic: + net_name = nic.pop('net-name') + nic_net = self.get_network(net_name) + if not nic_net: + raise exc.OpenStackCloudException( + "Requested network {net} could not be found.".format( + net=net_name)) + net['uuid'] = nic_net['id'] + for ip_key in ('v4-fixed-ip', 'v6-fixed-ip', 'fixed_ip'): + fixed_ip = nic.pop(ip_key, None) + if fixed_ip and net.get('fixed_ip'): + raise exc.OpenStackCloudException( + "Only one of v4-fixed-ip, v6-fixed-ip or fixed_ip" + " may be given") + if fixed_ip: + net['fixed_ip'] = fixed_ip + # TODO(mordred) Add support for tag if server supports microversion + # 2.32-2.36 or >= 2.42 + for key in ('port', 'port-id'): + if key in nic: + net['port'] = nic.pop(key) + if nic: + raise exc.OpenStackCloudException( + "Additional unsupported keys given for server network" + " creation: {keys}".format(keys=nic.keys())) + networks.append(net) + if networks: + kwargs['networks'] = networks + + if image: + if isinstance(image, dict): + kwargs['imageRef'] = image['id'] + else: + kwargs['imageRef'] = self.get_image(image).id + if isinstance(flavor, dict): + kwargs['flavorRef'] = flavor['id'] + else: + kwargs['flavorRef'] = self.get_flavor(flavor, get_extra=False).id + + if volumes is None: + volumes = [] + + # nova cli calls this boot_volume. Let's be the same + if root_volume and not boot_volume: + boot_volume = root_volume + + kwargs = self._get_boot_from_volume_kwargs( + image=image, boot_from_volume=boot_from_volume, + boot_volume=boot_volume, volume_size=str(volume_size), + terminate_volume=terminate_volume, + volumes=volumes, kwargs=kwargs) + + kwargs['name'] = name + endpoint = '/servers' + # TODO(mordred) We're only testing this in functional tests. We need + # to add unit tests for this too. + if 'block_device_mapping_v2' in kwargs: + endpoint = '/os-volumes_boot' + with _utils.shade_exceptions("Error in creating instance"): + data = proxy._json_response( + self.compute.post(endpoint, json=server_json)) + server = self._get_and_munchify('server', data) + admin_pass = server.get('adminPass') or kwargs.get('admin_pass') + if not wait: + # This is a direct get call to skip the list_servers + # cache which has absolutely no chance of containing the + # new server. + # Only do this if we're not going to wait for the server + # to complete booting, because the only reason we do it + # is to get a server record that is the return value from + # get/list rather than the return value of create. If we're + # going to do the wait loop below, this is a waste of a call + server = self.get_server_by_id(server.id) + if server.status == 'ERROR': + raise exc.OpenStackCloudCreateException( + resource='server', resource_id=server.id) + + if wait: + server = self.wait_for_server( + server, + auto_ip=auto_ip, ips=ips, ip_pool=ip_pool, + reuse=reuse_ips, timeout=timeout, + nat_destination=nat_destination, + ) + + server.adminPass = admin_pass + return server + + def _get_boot_from_volume_kwargs( + self, image, boot_from_volume, boot_volume, volume_size, + terminate_volume, volumes, kwargs): + """Return block device mappings + + :param image: Image dict, name or id to boot with. + + """ + # TODO(mordred) We're only testing this in functional tests. We need + # to add unit tests for this too. + if boot_volume or boot_from_volume or volumes: + kwargs.setdefault('block_device_mapping_v2', []) + else: + return kwargs + + # If we have boot_from_volume but no root volume, then we're + # booting an image from volume + if boot_volume: + volume = self.get_volume(boot_volume) + if not volume: + raise exc.OpenStackCloudException( + 'Volume {boot_volume} is not a valid volume' + ' in {cloud}:{region}'.format( + boot_volume=boot_volume, + cloud=self.name, region=self.config.region_name)) + block_mapping = { + 'boot_index': '0', + 'delete_on_termination': terminate_volume, + 'destination_type': 'volume', + 'uuid': volume['id'], + 'source_type': 'volume', + } + kwargs['block_device_mapping_v2'].append(block_mapping) + kwargs['imageRef'] = '' + elif boot_from_volume: + + if isinstance(image, dict): + image_obj = image + else: + image_obj = self.get_image(image) + if not image_obj: + raise exc.OpenStackCloudException( + 'Image {image} is not a valid image in' + ' {cloud}:{region}'.format( + image=image, + cloud=self.name, region=self.config.region_name)) + + block_mapping = { + 'boot_index': '0', + 'delete_on_termination': terminate_volume, + 'destination_type': 'volume', + 'uuid': image_obj['id'], + 'source_type': 'image', + 'volume_size': volume_size, + } + kwargs['imageRef'] = '' + kwargs['block_device_mapping_v2'].append(block_mapping) + if volumes and kwargs['imageRef']: + # If we're attaching volumes on boot but booting from an image, + # we need to specify that in the BDM. + block_mapping = { + u'boot_index': 0, + u'delete_on_termination': True, + u'destination_type': u'local', + u'source_type': u'image', + u'uuid': kwargs['imageRef'], + } + kwargs['block_device_mapping_v2'].append(block_mapping) + for volume in volumes: + volume_obj = self.get_volume(volume) + if not volume_obj: + raise exc.OpenStackCloudException( + 'Volume {volume} is not a valid volume' + ' in {cloud}:{region}'.format( + volume=volume, + cloud=self.name, region=self.config.region_name)) + block_mapping = { + 'boot_index': '-1', + 'delete_on_termination': False, + 'destination_type': 'volume', + 'uuid': volume_obj['id'], + 'source_type': 'volume', + } + kwargs['block_device_mapping_v2'].append(block_mapping) + if boot_volume or boot_from_volume or volumes: + self.list_volumes.invalidate(self) + return kwargs + + def wait_for_server( + self, server, auto_ip=True, ips=None, ip_pool=None, + reuse=True, timeout=180, nat_destination=None): + """ + Wait for a server to reach ACTIVE status. + """ + server_id = server['id'] + timeout_message = "Timeout waiting for the server to come up." + start_time = time.time() + + # There is no point in iterating faster than the list_servers cache + for count in utils.iterate_timeout( + timeout, + timeout_message, + # if _SERVER_AGE is 0 we still want to wait a bit + # to be friendly with the server. + wait=self._SERVER_AGE or 2): + try: + # Use the get_server call so that the list_servers + # cache can be leveraged + server = self.get_server(server_id) + except Exception: + continue + if not server: + continue + + # We have more work to do, but the details of that are + # hidden from the user. So, calculate remaining timeout + # and pass it down into the IP stack. + remaining_timeout = timeout - int(time.time() - start_time) + if remaining_timeout <= 0: + raise exc.OpenStackCloudTimeout(timeout_message) + + server = self.get_active_server( + server=server, reuse=reuse, + auto_ip=auto_ip, ips=ips, ip_pool=ip_pool, + wait=True, timeout=remaining_timeout, + nat_destination=nat_destination) + + if server is not None and server['status'] == 'ACTIVE': + return server + + def get_active_server( + self, server, auto_ip=True, ips=None, ip_pool=None, + reuse=True, wait=False, timeout=180, nat_destination=None): + + if server['status'] == 'ERROR': + if 'fault' in server and 'message' in server['fault']: + raise exc.OpenStackCloudException( + "Error in creating the server." + " Compute service reports fault: {reason}".format( + reason=server['fault']['message']), + extra_data=dict(server=server)) + + raise exc.OpenStackCloudException( + "Error in creating the server", extra_data=dict(server=server)) + + if server['status'] == 'ACTIVE': + if 'addresses' in server and server['addresses']: + return self.add_ips_to_server( + server, auto_ip, ips, ip_pool, reuse=reuse, + nat_destination=nat_destination, + wait=wait, timeout=timeout) + + self.log.debug( + 'Server %(server)s reached ACTIVE state without' + ' being allocated an IP address.' + ' Deleting server.', {'server': server['id']}) + try: + self._delete_server( + server=server, wait=wait, timeout=timeout) + except Exception as e: + raise exc.OpenStackCloudException( + 'Server reached ACTIVE state without being' + ' allocated an IP address AND then could not' + ' be deleted: {0}'.format(e), + extra_data=dict(server=server)) + raise exc.OpenStackCloudException( + 'Server reached ACTIVE state without being' + ' allocated an IP address.', + extra_data=dict(server=server)) + return None + + def rebuild_server(self, server_id, image_id, admin_pass=None, + detailed=False, bare=False, + wait=False, timeout=180): + kwargs = {} + if image_id: + kwargs['imageRef'] = image_id + if admin_pass: + kwargs['adminPass'] = admin_pass + + data = proxy._json_response( + self.compute.post( + '/servers/{server_id}/action'.format(server_id=server_id), + json={'rebuild': kwargs}), + error_message="Error in rebuilding instance") + server = self._get_and_munchify('server', data) + if not wait: + return self._expand_server( + self._normalize_server(server), bare=bare, detailed=detailed) + + admin_pass = server.get('adminPass') or admin_pass + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for server {0} to " + "rebuild.".format(server_id), + wait=self._SERVER_AGE): + try: + server = self.get_server(server_id, bare=True) + except Exception: + continue + if not server: + continue + + if server['status'] == 'ERROR': + raise exc.OpenStackCloudException( + "Error in rebuilding the server", + extra_data=dict(server=server)) + + if server['status'] == 'ACTIVE': + server.adminPass = admin_pass + break + + return self._expand_server(server, detailed=detailed, bare=bare) + + def set_server_metadata(self, name_or_id, metadata): + """Set metadata in a server instance. + + :param str name_or_id: The name or ID of the server instance + to update. + :param dict metadata: A dictionary with the key=value pairs + to set in the server instance. It only updates the key=value + pairs provided. Existing ones will remain untouched. + + :raises: OpenStackCloudException on operation error. + """ + server = self.get_server(name_or_id, bare=True) + if not server: + raise exc.OpenStackCloudException( + 'Invalid Server {server}'.format(server=name_or_id)) + + proxy._json_response( + self.compute.post( + '/servers/{server_id}/metadata'.format(server_id=server['id']), + json={'metadata': metadata}), + error_message='Error updating server metadata') + + def delete_server_metadata(self, name_or_id, metadata_keys): + """Delete metadata from a server instance. + + :param str name_or_id: The name or ID of the server instance + to update. + :param metadata_keys: A list with the keys to be deleted + from the server instance. + + :raises: OpenStackCloudException on operation error. + """ + server = self.get_server(name_or_id, bare=True) + if not server: + raise exc.OpenStackCloudException( + 'Invalid Server {server}'.format(server=name_or_id)) + + for key in metadata_keys: + error_message = 'Error deleting metadata {key} on {server}'.format( + key=key, server=name_or_id) + proxy._json_response( + self.compute.delete( + '/servers/{server_id}/metadata/{key}'.format( + server_id=server['id'], + key=key)), + error_message=error_message) + + def delete_server( + self, name_or_id, wait=False, timeout=180, delete_ips=False, + delete_ip_retry=1): + """Delete a server instance. + + :param name_or_id: name or ID of the server to delete + :param bool wait: If true, waits for server to be deleted. + :param int timeout: Seconds to wait for server deletion. + :param bool delete_ips: If true, deletes any floating IPs + associated with the instance. + :param int delete_ip_retry: Number of times to retry deleting + any floating ips, should the first try be unsuccessful. + + :returns: True if delete succeeded, False otherwise if the + server does not exist. + + :raises: OpenStackCloudException on operation error. + """ + # If delete_ips is True, we need the server to not be bare. + server = self.get_server(name_or_id, bare=True) + if not server: + return False + + # This portion of the code is intentionally left as a separate + # private method in order to avoid an unnecessary API call to get + # a server we already have. + return self._delete_server( + server, wait=wait, timeout=timeout, delete_ips=delete_ips, + delete_ip_retry=delete_ip_retry) + + def _delete_server_floating_ips(self, server, delete_ip_retry): + # Does the server have floating ips in its + # addresses dict? If not, skip this. + server_floats = meta.find_nova_interfaces( + server['addresses'], ext_tag='floating') + for fip in server_floats: + try: + ip = self.get_floating_ip(id=None, filters={ + 'floating_ip_address': fip['addr']}) + except exc.OpenStackCloudURINotFound: + # We're deleting. If it doesn't exist - awesome + # NOTE(mordred) If the cloud is a nova FIP cloud but + # floating_ip_source is set to neutron, this + # can lead to a FIP leak. + continue + if not ip: + continue + deleted = self.delete_floating_ip( + ip['id'], retry=delete_ip_retry) + if not deleted: + raise exc.OpenStackCloudException( + "Tried to delete floating ip {floating_ip}" + " associated with server {id} but there was" + " an error deleting it. Not deleting server.".format( + floating_ip=ip['floating_ip_address'], + id=server['id'])) + + def _delete_server( + self, server, wait=False, timeout=180, delete_ips=False, + delete_ip_retry=1): + if not server: + return False + + if delete_ips and self._has_floating_ips(): + self._delete_server_floating_ips(server, delete_ip_retry) + + try: + proxy._json_response( + self.compute.delete( + '/servers/{id}'.format(id=server['id'])), + error_message="Error in deleting server") + except exc.OpenStackCloudURINotFound: + return False + except Exception: + raise + + if not wait: + return True + + # If the server has volume attachments, or if it has booted + # from volume, deleting it will change volume state so we will + # need to invalidate the cache. Avoid the extra API call if + # caching is not enabled. + reset_volume_cache = False + if (self.cache_enabled + and self.has_service('volume') + and self.get_volumes(server)): + reset_volume_cache = True + + for count in utils.iterate_timeout( + timeout, + "Timed out waiting for server to get deleted.", + # if _SERVER_AGE is 0 we still want to wait a bit + # to be friendly with the server. + wait=self._SERVER_AGE or 2): + with _utils.shade_exceptions("Error in deleting server"): + server = self.get_server(server['id'], bare=True) + if not server: + break + + if reset_volume_cache: + self.list_volumes.invalidate(self) + + # Reset the list servers cache time so that the next list server + # call gets a new list + self._servers_time = self._servers_time - self._SERVER_AGE + return True + + @_utils.valid_kwargs( + 'name', 'description') + def update_server(self, name_or_id, detailed=False, bare=False, **kwargs): + """Update a server. + + :param name_or_id: Name of the server to be updated. + :param detailed: Whether or not to add detailed additional information. + Defaults to False. + :param bare: Whether to skip adding any additional information to the + server record. Defaults to False, meaning the addresses + dict will be populated as needed from neutron. Setting + to True implies detailed = False. + :name: New name for the server + :description: New description for the server + + :returns: a dictionary representing the updated server. + + :raises: OpenStackCloudException on operation error. + """ + server = self.get_server(name_or_id=name_or_id, bare=True) + if server is None: + raise exc.OpenStackCloudException( + "failed to find server '{server}'".format(server=name_or_id)) + + data = proxy._json_response( + self.compute.put( + '/servers/{server_id}'.format(server_id=server['id']), + json={'server': kwargs}), + error_message="Error updating server {0}".format(name_or_id)) + server = self._normalize_server( + self._get_and_munchify('server', data)) + return self._expand_server(server, bare=bare, detailed=detailed) + + def create_server_group(self, name, policies): + """Create a new server group. + + :param name: Name of the server group being created + :param policies: List of policies for the server group. + + :returns: a dict representing the new server group. + + :raises: OpenStackCloudException on operation error. + """ + data = proxy._json_response( + self.compute.post( + '/os-server-groups', + json={ + 'server_group': { + 'name': name, + 'policies': policies}}), + error_message="Unable to create server group {name}".format( + name=name)) + return self._get_and_munchify('server_group', data) + + def delete_server_group(self, name_or_id): + """Delete a server group. + + :param name_or_id: Name or ID of the server group to delete + + :returns: True if delete succeeded, False otherwise + + :raises: OpenStackCloudException on operation error. + """ + server_group = self.get_server_group(name_or_id) + if not server_group: + self.log.debug("Server group %s not found for deleting", + name_or_id) + return False + + proxy._json_response( + self.compute.delete( + '/os-server-groups/{id}'.format(id=server_group['id'])), + error_message="Error deleting server group {name}".format( + name=name_or_id)) + + return True + + def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", + ephemeral=0, swap=0, rxtx_factor=1.0, is_public=True): + """Create a new flavor. + + :param name: Descriptive name of the flavor + :param ram: Memory in MB for the flavor + :param vcpus: Number of VCPUs for the flavor + :param disk: Size of local disk in GB + :param flavorid: ID for the flavor (optional) + :param ephemeral: Ephemeral space size in GB + :param swap: Swap space in MB + :param rxtx_factor: RX/TX factor + :param is_public: Make flavor accessible to the public + + :returns: A ``munch.Munch`` describing the new flavor. + + :raises: OpenStackCloudException on operation error. + """ + with _utils.shade_exceptions("Failed to create flavor {name}".format( + name=name)): + payload = { + 'disk': disk, + 'OS-FLV-EXT-DATA:ephemeral': ephemeral, + 'id': flavorid, + 'os-flavor-access:is_public': is_public, + 'name': name, + 'ram': ram, + 'rxtx_factor': rxtx_factor, + 'swap': swap, + 'vcpus': vcpus, + } + if flavorid == 'auto': + payload['id'] = None + data = proxy._json_response(self.compute.post( + '/flavors', + json=dict(flavor=payload))) + + return self._normalize_flavor( + self._get_and_munchify('flavor', data)) + + def delete_flavor(self, name_or_id): + """Delete a flavor + + :param name_or_id: ID or name of the flavor to delete. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException on operation error. + """ + flavor = self.get_flavor(name_or_id, get_extra=False) + if flavor is None: + self.log.debug( + "Flavor %s not found for deleting", name_or_id) + return False + + proxy._json_response( + self.compute.delete( + '/flavors/{id}'.format(id=flavor['id'])), + error_message="Unable to delete flavor {name}".format( + name=name_or_id)) + + return True + + def set_flavor_specs(self, flavor_id, extra_specs): + """Add extra specs to a flavor + + :param string flavor_id: ID of the flavor to update. + :param dict extra_specs: Dictionary of key-value pairs. + + :raises: OpenStackCloudException on operation error. + :raises: OpenStackCloudResourceNotFound if flavor ID is not found. + """ + proxy._json_response( + self.compute.post( + "/flavors/{id}/os-extra_specs".format(id=flavor_id), + json=dict(extra_specs=extra_specs)), + error_message="Unable to set flavor specs") + + def unset_flavor_specs(self, flavor_id, keys): + """Delete extra specs from a flavor + + :param string flavor_id: ID of the flavor to update. + :param keys: List of spec keys to delete. + + :raises: OpenStackCloudException on operation error. + :raises: OpenStackCloudResourceNotFound if flavor ID is not found. + """ + for key in keys: + proxy._json_response( + self.compute.delete( + "/flavors/{id}/os-extra_specs/{key}".format( + id=flavor_id, key=key)), + error_message="Unable to delete flavor spec {0}".format(key)) + + def _mod_flavor_access(self, action, flavor_id, project_id): + """Common method for adding and removing flavor access + """ + with _utils.shade_exceptions("Error trying to {action} access from " + "flavor ID {flavor}".format( + action=action, flavor=flavor_id)): + endpoint = '/flavors/{id}/action'.format(id=flavor_id) + access = {'tenant': project_id} + access_key = '{action}TenantAccess'.format(action=action) + + proxy._json_response( + self.compute.post(endpoint, json={access_key: access})) + + def add_flavor_access(self, flavor_id, project_id): + """Grant access to a private flavor for a project/tenant. + + :param string flavor_id: ID of the private flavor. + :param string project_id: ID of the project/tenant. + + :raises: OpenStackCloudException on operation error. + """ + self._mod_flavor_access('add', flavor_id, project_id) + + def remove_flavor_access(self, flavor_id, project_id): + """Revoke access from a private flavor for a project/tenant. + + :param string flavor_id: ID of the private flavor. + :param string project_id: ID of the project/tenant. + + :raises: OpenStackCloudException on operation error. + """ + self._mod_flavor_access('remove', flavor_id, project_id) + + def list_flavor_access(self, flavor_id): + """List access from a private flavor for a project/tenant. + + :param string flavor_id: ID of the private flavor. + + :returns: a list of ``munch.Munch`` containing the access description + + :raises: OpenStackCloudException on operation error. + """ + data = proxy._json_response( + self.compute.get( + '/flavors/{id}/os-flavor-access'.format(id=flavor_id)), + error_message=( + "Error trying to list access from flavorID {flavor}".format( + flavor=flavor_id))) + return _utils.normalize_flavor_accesses( + self._get_and_munchify('flavor_access', data)) + + def list_hypervisors(self): + """List all hypervisors + + :returns: A list of hypervisor ``munch.Munch``. + """ + + data = proxy._json_response( + self.compute.get('/os-hypervisors/detail'), + error_message="Error fetching hypervisor list") + return self._get_and_munchify('hypervisors', data) + + def search_aggregates(self, name_or_id=None, filters=None): + """Seach host aggregates. + + :param name: aggregate name or id. + :param filters: a dict containing additional filters to use. + + :returns: a list of dicts containing the aggregates + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + aggregates = self.list_aggregates() + return _utils._filter_list(aggregates, name_or_id, filters) + + def list_aggregates(self): + """List all available host aggregates. + + :returns: A list of aggregate dicts. + + """ + data = proxy._json_response( + self.compute.get('/os-aggregates'), + error_message="Error fetching aggregate list") + return self._get_and_munchify('aggregates', data) + + def get_aggregate(self, name_or_id, filters=None): + """Get an aggregate by name or ID. + + :param name_or_id: Name or ID of the aggregate. + :param dict filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'availability_zone': 'nova', + 'metadata': { + 'cpu_allocation_ratio': '1.0' + } + } + + :returns: An aggregate dict or None if no matching aggregate is + found. + + """ + return _utils._get_entity(self, 'aggregate', name_or_id, filters) + + def create_aggregate(self, name, availability_zone=None): + """Create a new host aggregate. + + :param name: Name of the host aggregate being created + :param availability_zone: Availability zone to assign hosts + + :returns: a dict representing the new host aggregate. + + :raises: OpenStackCloudException on operation error. + """ + data = proxy._json_response( + self.compute.post( + '/os-aggregates', + json={'aggregate': { + 'name': name, + 'availability_zone': availability_zone + }}), + error_message="Unable to create host aggregate {name}".format( + name=name)) + return self._get_and_munchify('aggregate', data) + + @_utils.valid_kwargs('name', 'availability_zone') + def update_aggregate(self, name_or_id, **kwargs): + """Update a host aggregate. + + :param name_or_id: Name or ID of the aggregate being updated. + :param name: New aggregate name + :param availability_zone: Availability zone to assign to hosts + + :returns: a dict representing the updated host aggregate. + + :raises: OpenStackCloudException on operation error. + """ + aggregate = self.get_aggregate(name_or_id) + if not aggregate: + raise exc.OpenStackCloudException( + "Host aggregate %s not found." % name_or_id) + + data = proxy._json_response( + self.compute.put( + '/os-aggregates/{id}'.format(id=aggregate['id']), + json={'aggregate': kwargs}), + error_message="Error updating aggregate {name}".format( + name=name_or_id)) + return self._get_and_munchify('aggregate', data) + + def delete_aggregate(self, name_or_id): + """Delete a host aggregate. + + :param name_or_id: Name or ID of the host aggregate to delete. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException on operation error. + """ + aggregate = self.get_aggregate(name_or_id) + if not aggregate: + self.log.debug("Aggregate %s not found for deleting", name_or_id) + return False + + return proxy._json_response( + self.compute.delete( + '/os-aggregates/{id}'.format(id=aggregate['id'])), + error_message="Error deleting aggregate {name}".format( + name=name_or_id)) + + return True + + def set_aggregate_metadata(self, name_or_id, metadata): + """Set aggregate metadata, replacing the existing metadata. + + :param name_or_id: Name of the host aggregate to update + :param metadata: Dict containing metadata to replace (Use + {'key': None} to remove a key) + + :returns: a dict representing the new host aggregate. + + :raises: OpenStackCloudException on operation error. + """ + aggregate = self.get_aggregate(name_or_id) + if not aggregate: + raise exc.OpenStackCloudException( + "Host aggregate %s not found." % name_or_id) + + err_msg = "Unable to set metadata for host aggregate {name}".format( + name=name_or_id) + + data = proxy._json_response( + self.compute.post( + '/os-aggregates/{id}/action'.format(id=aggregate['id']), + json={'set_metadata': {'metadata': metadata}}), + error_message=err_msg) + return self._get_and_munchify('aggregate', data) + + def add_host_to_aggregate(self, name_or_id, host_name): + """Add a host to an aggregate. + + :param name_or_id: Name or ID of the host aggregate. + :param host_name: Host to add. + + :raises: OpenStackCloudException on operation error. + """ + aggregate = self.get_aggregate(name_or_id) + if not aggregate: + raise exc.OpenStackCloudException( + "Host aggregate %s not found." % name_or_id) + + err_msg = "Unable to add host {host} to aggregate {name}".format( + host=host_name, name=name_or_id) + + return proxy._json_response( + self.compute.post( + '/os-aggregates/{id}/action'.format(id=aggregate['id']), + json={'add_host': {'host': host_name}}), + error_message=err_msg) + + def remove_host_from_aggregate(self, name_or_id, host_name): + """Remove a host from an aggregate. + + :param name_or_id: Name or ID of the host aggregate. + :param host_name: Host to remove. + + :raises: OpenStackCloudException on operation error. + """ + aggregate = self.get_aggregate(name_or_id) + if not aggregate: + raise exc.OpenStackCloudException( + "Host aggregate %s not found." % name_or_id) + + err_msg = "Unable to remove host {host} to aggregate {name}".format( + host=host_name, name=name_or_id) + + return proxy._json_response( + self.compute.post( + '/os-aggregates/{id}/action'.format(id=aggregate['id']), + json={'remove_host': {'host': host_name}}), + error_message=err_msg) + + def set_compute_quotas(self, name_or_id, **kwargs): + """ Set a quota in a project + + :param name_or_id: project name or id + :param kwargs: key/value pairs of quota name and quota value + + :raises: OpenStackCloudException if the resource to set the + quota does not exist. + """ + + proj = self.get_project(name_or_id) + if not proj: + raise exc.OpenStackCloudException("project does not exist") + + # compute_quotas = {key: val for key, val in kwargs.items() + # if key in quota.COMPUTE_QUOTAS} + # TODO(ghe): Manage volume and network quotas + # network_quotas = {key: val for key, val in kwargs.items() + # if key in quota.NETWORK_QUOTAS} + # volume_quotas = {key: val for key, val in kwargs.items() + # if key in quota.VOLUME_QUOTAS} + + kwargs['force'] = True + proxy._json_response( + self.compute.put( + '/os-quota-sets/{project}'.format(project=proj.id), + json={'quota_set': kwargs}), + error_message="No valid quota or resource") + + def get_compute_quotas(self, name_or_id): + """ Get quota for a project + + :param name_or_id: project name or id + :raises: OpenStackCloudException if it's not a valid project + + :returns: Munch object with the quotas + """ + proj = self.get_project(name_or_id) + if not proj: + raise exc.OpenStackCloudException("project does not exist") + data = proxy._json_response( + self.compute.get( + '/os-quota-sets/{project}'.format(project=proj.id))) + return self._get_and_munchify('quota_set', data) + + def delete_compute_quotas(self, name_or_id): + """ Delete quota for a project + + :param name_or_id: project name or id + :raises: OpenStackCloudException if it's not a valid project or the + nova client call failed + + :returns: dict with the quotas + """ + proj = self.get_project(name_or_id) + if not proj: + raise exc.OpenStackCloudException("project does not exist") + return proxy._json_response( + self.compute.delete( + '/os-quota-sets/{project}'.format(project=proj.id))) + + def get_compute_usage(self, name_or_id, start=None, end=None): + """ Get usage for a specific project + + :param name_or_id: project name or id + :param start: :class:`datetime.datetime` or string. Start date in UTC + Defaults to 2010-07-06T12:00:00Z (the date the OpenStack + project was started) + :param end: :class:`datetime.datetime` or string. End date in UTC. + Defaults to now + :raises: OpenStackCloudException if it's not a valid project + + :returns: Munch object with the usage + """ + + def parse_date(date): + try: + return iso8601.parse_date(date) + except iso8601.iso8601.ParseError: + # Yes. This is an exception mask. However,iso8601 is an + # implementation detail - and the error message is actually + # less informative. + raise exc.OpenStackCloudException( + "Date given, {date}, is invalid. Please pass in a date" + " string in ISO 8601 format -" + " YYYY-MM-DDTHH:MM:SS".format( + date=date)) + + def parse_datetime_for_nova(date): + # Must strip tzinfo from the date- it breaks Nova. Also, + # Nova is expecting this in UTC. If someone passes in an + # ISO8601 date string or a datetime with timzeone data attached, + # strip the timezone data but apply offset math first so that + # the user's well formed perfectly valid date will be used + # correctly. + offset = date.utcoffset() + if offset: + date = date - datetime.timedelta(hours=offset) + return date.replace(tzinfo=None) + + if not start: + start = parse_date('2010-07-06') + elif not isinstance(start, datetime.datetime): + start = parse_date(start) + if not end: + end = datetime.datetime.utcnow() + elif not isinstance(start, datetime.datetime): + end = parse_date(end) + + start = parse_datetime_for_nova(start) + end = parse_datetime_for_nova(end) + + proj = self.get_project(name_or_id) + if not proj: + raise exc.OpenStackCloudException( + "project does not exist: {}".format(name=proj.id)) + + data = proxy._json_response( + self.compute.get( + '/os-simple-tenant-usage/{project}'.format(project=proj.id), + params=dict(start=start.isoformat(), end=end.isoformat())), + error_message="Unable to get usage for project: {name}".format( + name=proj.id)) + return self._normalize_compute_usage( + self._get_and_munchify('tenant_usage', data)) + + def _encode_server_userdata(self, userdata): + if hasattr(userdata, 'read'): + userdata = userdata.read() + + if not isinstance(userdata, six.binary_type): + # If the userdata passed in is bytes, just send it unmodified + if not isinstance(userdata, six.string_types): + raise TypeError("%s can't be encoded" % type(userdata)) + # If it's not bytes, make it bytes + userdata = userdata.encode('utf-8', 'strict') + + # Once we have base64 bytes, make them into a utf-8 string for REST + return base64.b64encode(userdata).decode('utf-8') diff --git a/openstack/cloud/_dns.py b/openstack/cloud/_dns.py new file mode 100644 index 000000000..6796b3666 --- /dev/null +++ b/openstack/cloud/_dns.py @@ -0,0 +1,296 @@ +# 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 types so that we can reference ListType in sphinx param declarations. +# We can't just use list, because sphinx gets confused by +# openstack.resource.Resource.list and openstack.resource2.Resource.list +import types # noqa + +from openstack.cloud import exc +from openstack.cloud import _normalize +from openstack.cloud import _utils + + +class DnsCloudMixin(_normalize.Normalizer): + + @property + def _dns_client(self): + if 'dns' not in self._raw_clients: + dns_client = self._get_versioned_client( + 'dns', min_version=2, max_version='2.latest') + self._raw_clients['dns'] = dns_client + return self._raw_clients['dns'] + + def list_zones(self): + """List all available zones. + + :returns: A list of zones dicts. + + """ + data = self._dns_client.get( + "/zones", + error_message="Error fetching zones list") + return self._get_and_munchify('zones', data) + + def get_zone(self, name_or_id, filters=None): + """Get a zone by name or ID. + + :param name_or_id: Name or ID of the zone + :param filters: + A dictionary of meta data to use for further filtering + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: A zone dict or None if no matching zone is found. + + """ + return _utils._get_entity(self, 'zone', name_or_id, filters) + + def search_zones(self, name_or_id=None, filters=None): + zones = self.list_zones() + return _utils._filter_list(zones, name_or_id, filters) + + def create_zone(self, name, zone_type=None, email=None, description=None, + ttl=None, masters=None): + """Create a new zone. + + :param name: Name of the zone being created. + :param zone_type: Type of the zone (primary/secondary) + :param email: Email of the zone owner (only + applies if zone_type is primary) + :param description: Description of the zone + :param ttl: TTL (Time to live) value in seconds + :param masters: Master nameservers (only applies + if zone_type is secondary) + + :returns: a dict representing the created zone. + + :raises: OpenStackCloudException on operation error. + """ + + # We capitalize in case the user passes time in lowercase, as + # designate call expects PRIMARY/SECONDARY + if zone_type is not None: + zone_type = zone_type.upper() + if zone_type not in ('PRIMARY', 'SECONDARY'): + raise exc.OpenStackCloudException( + "Invalid type %s, valid choices are PRIMARY or SECONDARY" % + zone_type) + + zone = { + "name": name, + "email": email, + "description": description, + } + if ttl is not None: + zone["ttl"] = ttl + + if zone_type is not None: + zone["type"] = zone_type + + if masters is not None: + zone["masters"] = masters + + data = self._dns_client.post( + "/zones", json=zone, + error_message="Unable to create zone {name}".format(name=name)) + return self._get_and_munchify(key=None, data=data) + + @_utils.valid_kwargs('email', 'description', 'ttl', 'masters') + def update_zone(self, name_or_id, **kwargs): + """Update a zone. + + :param name_or_id: Name or ID of the zone being updated. + :param email: Email of the zone owner (only + applies if zone_type is primary) + :param description: Description of the zone + :param ttl: TTL (Time to live) value in seconds + :param masters: Master nameservers (only applies + if zone_type is secondary) + + :returns: a dict representing the updated zone. + + :raises: OpenStackCloudException on operation error. + """ + zone = self.get_zone(name_or_id) + if not zone: + raise exc.OpenStackCloudException( + "Zone %s not found." % name_or_id) + + data = self._dns_client.patch( + "/zones/{zone_id}".format(zone_id=zone['id']), json=kwargs, + error_message="Error updating zone {0}".format(name_or_id)) + return self._get_and_munchify(key=None, data=data) + + def delete_zone(self, name_or_id): + """Delete a zone. + + :param name_or_id: Name or ID of the zone being deleted. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException on operation error. + """ + + zone = self.get_zone(name_or_id) + if zone is None: + self.log.debug("Zone %s not found for deleting", name_or_id) + return False + + return self._dns_client.delete( + "/zones/{zone_id}".format(zone_id=zone['id']), + error_message="Error deleting zone {0}".format(name_or_id)) + + return True + + def list_recordsets(self, zone): + """List all available recordsets. + + :param zone: Name or ID of the zone managing the recordset + + :returns: A list of recordsets. + + """ + zone_obj = self.get_zone(zone) + if zone_obj is None: + raise exc.OpenStackCloudException( + "Zone %s not found." % zone) + return self._dns_client.get( + "/zones/{zone_id}/recordsets".format(zone_id=zone_obj['id']), + error_message="Error fetching recordsets list")['recordsets'] + + def get_recordset(self, zone, name_or_id): + """Get a recordset by name or ID. + + :param zone: Name or ID of the zone managing the recordset + :param name_or_id: Name or ID of the recordset + + :returns: A recordset dict or None if no matching recordset is + found. + + """ + zone_obj = self.get_zone(zone) + if zone_obj is None: + raise exc.OpenStackCloudException( + "Zone %s not found." % zone) + try: + return self._dns_client.get( + "/zones/{zone_id}/recordsets/{recordset_id}".format( + zone_id=zone_obj['id'], recordset_id=name_or_id), + error_message="Error fetching recordset") + except Exception: + return None + + def search_recordsets(self, zone, name_or_id=None, filters=None): + recordsets = self.list_recordsets(zone=zone) + return _utils._filter_list(recordsets, name_or_id, filters) + + def create_recordset(self, zone, name, recordset_type, records, + description=None, ttl=None): + """Create a recordset. + + :param zone: Name or ID of the zone managing the recordset + :param name: Name of the recordset + :param recordset_type: Type of the recordset + :param records: List of the recordset definitions + :param description: Description of the recordset + :param ttl: TTL value of the recordset + + :returns: a dict representing the created recordset. + + :raises: OpenStackCloudException on operation error. + + """ + zone_obj = self.get_zone(zone) + if zone_obj is None: + raise exc.OpenStackCloudException( + "Zone %s not found." % zone) + + # We capitalize the type in case the user sends in lowercase + recordset_type = recordset_type.upper() + + body = { + 'name': name, + 'type': recordset_type, + 'records': records + } + + if description: + body['description'] = description + + if ttl: + body['ttl'] = ttl + + return self._dns_client.post( + "/zones/{zone_id}/recordsets".format(zone_id=zone_obj['id']), + json=body, + error_message="Error creating recordset {name}".format(name=name)) + + @_utils.valid_kwargs('description', 'ttl', 'records') + def update_recordset(self, zone, name_or_id, **kwargs): + """Update a recordset. + + :param zone: Name or ID of the zone managing the recordset + :param name_or_id: Name or ID of the recordset being updated. + :param records: List of the recordset definitions + :param description: Description of the recordset + :param ttl: TTL (Time to live) value in seconds of the recordset + + :returns: a dict representing the updated recordset. + + :raises: OpenStackCloudException on operation error. + """ + zone_obj = self.get_zone(zone) + if zone_obj is None: + raise exc.OpenStackCloudException( + "Zone %s not found." % zone) + + recordset_obj = self.get_recordset(zone, name_or_id) + if recordset_obj is None: + raise exc.OpenStackCloudException( + "Recordset %s not found." % name_or_id) + + new_recordset = self._dns_client.put( + "/zones/{zone_id}/recordsets/{recordset_id}".format( + zone_id=zone_obj['id'], recordset_id=name_or_id), json=kwargs, + error_message="Error updating recordset {0}".format(name_or_id)) + + return new_recordset + + def delete_recordset(self, zone, name_or_id): + """Delete a recordset. + + :param zone: Name or ID of the zone managing the recordset. + :param name_or_id: Name or ID of the recordset being deleted. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException on operation error. + """ + + zone_obj = self.get_zone(zone) + if zone_obj is None: + self.log.debug("Zone %s not found for deleting", zone) + return False + + recordset = self.get_recordset(zone_obj['id'], name_or_id) + if recordset is None: + self.log.debug("Recordset %s not found for deleting", name_or_id) + return False + + self._dns_client.delete( + "/zones/{zone_id}/recordsets/{recordset_id}".format( + zone_id=zone_obj['id'], recordset_id=name_or_id), + error_message="Error deleting recordset {0}".format(name_or_id)) + + return True diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py new file mode 100644 index 000000000..5235fdffe --- /dev/null +++ b/openstack/cloud/_floating_ip.py @@ -0,0 +1,1169 @@ +# 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 types so that we can reference ListType in sphinx param declarations. +# We can't just use list, because sphinx gets confused by +# openstack.resource.Resource.list and openstack.resource2.Resource.list +import ipaddress +# import jsonpatch +import threading +import six +import time +import types # noqa + +from openstack.cloud import exc +from openstack.cloud import meta +from openstack.cloud import _normalize +from openstack.cloud import _utils +from openstack import exceptions +from openstack import proxy +from openstack import utils + +_CONFIG_DOC_URL = ( + "https://docs.openstack.org/openstacksdk/latest/" + "user/config/configuration.html") + + +class FloatingIPCloudMixin(_normalize.Normalizer): + + def __init__(self): + self.private = self.config.config.get('private', False) + + self._floating_ip_source = self.config.config.get( + 'floating_ip_source') + if self._floating_ip_source: + if self._floating_ip_source.lower() == 'none': + self._floating_ip_source = None + else: + self._floating_ip_source = self._floating_ip_source.lower() + + self._floating_ips = None + self._floating_ips_time = 0 + self._floating_ips_lock = threading.Lock() + + self._floating_network_by_router = None + self._floating_network_by_router_run = False + self._floating_network_by_router_lock = threading.Lock() + + def search_floating_ip_pools(self, name=None, filters=None): + pools = self.list_floating_ip_pools() + return _utils._filter_list(pools, name, filters) + + # With Neutron, there are some cases in which full server side filtering is + # not possible (e.g. nested attributes or list of objects) so we also need + # to use the client-side filtering + # The same goes for all neutron-related search/get methods! + def search_floating_ips(self, id=None, filters=None): + # `filters` could be a jmespath expression which Neutron server doesn't + # understand, obviously. + if self._use_neutron_floating() and isinstance(filters, dict): + filter_keys = ['router_id', 'status', 'tenant_id', 'project_id', + 'revision_number', 'description', + 'floating_network_id', 'fixed_ip_address', + 'floating_ip_address', 'port_id', 'sort_dir', + 'sort_key', 'tags', 'tags-any', 'not-tags', + 'not-tags-any', 'fields'] + neutron_filters = {k: v for k, v in filters.items() + if k in filter_keys} + kwargs = {'filters': neutron_filters} + else: + kwargs = {} + floating_ips = self.list_floating_ips(**kwargs) + return _utils._filter_list(floating_ips, id, filters) + + def _neutron_list_floating_ips(self, filters=None): + if not filters: + filters = {} + data = self.network.get('/floatingips.json', params=filters) + return self._get_and_munchify('floatingips', data) + + def _nova_list_floating_ips(self): + try: + data = proxy._json_response( + self.compute.get('/os-floating-ips')) + except exc.OpenStackCloudURINotFound: + return [] + return self._get_and_munchify('floating_ips', data) + + def get_floating_ip(self, id, filters=None): + """Get a floating IP by ID + + :param id: ID of the floating IP. + :param filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: A floating IP ``munch.Munch`` or None if no matching floating + IP is found. + + """ + return _utils._get_entity(self, 'floating_ip', id, filters) + + def _list_floating_ips(self, filters=None): + if self._use_neutron_floating(): + try: + return self._normalize_floating_ips( + self._neutron_list_floating_ips(filters)) + except exc.OpenStackCloudURINotFound as e: + # Nova-network don't support server-side floating ips + # filtering, so it's safer to return and empty list than + # to fallback to Nova which may return more results that + # expected. + if filters: + self.log.error( + "Neutron returned NotFound for floating IPs, which" + " means this cloud doesn't have neutron floating ips." + " shade can't fallback to trying Nova since nova" + " doesn't support server-side filtering when listing" + " floating ips and filters were given. If you do not" + " think shade should be attempting to list floating" + " ips on neutron, it is possible to control the" + " behavior by setting floating_ip_source to 'nova' or" + " None for cloud: %(cloud)s. If you are not already" + " using clouds.yaml to configure settings for your" + " cloud(s), and you want to configure this setting," + " you will need a clouds.yaml file. For more" + " information, please see %(doc_url)s", { + 'cloud': self.name, + 'doc_url': _CONFIG_DOC_URL, + } + ) + # We can't fallback to nova because we push-down filters. + # We got a 404 which means neutron doesn't exist. If the + # user + return [] + self.log.debug( + "Something went wrong talking to neutron API: " + "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) + # Fall-through, trying with Nova + else: + if filters: + raise ValueError( + "Nova-network don't support server-side floating ips " + "filtering. Use the search_floatting_ips method instead" + ) + + floating_ips = self._nova_list_floating_ips() + return self._normalize_floating_ips(floating_ips) + + def list_floating_ip_pools(self): + """List all available floating IP pools. + + NOTE: This function supports the nova-net view of the world. nova-net + has been deprecated, so it's highly recommended to switch to using + neutron. `get_external_ipv4_floating_networks` is what you should + almost certainly be using. + + :returns: A list of floating IP pool ``munch.Munch``. + + """ + if not self._has_nova_extension('os-floating-ip-pools'): + raise exc.OpenStackCloudUnavailableExtension( + 'Floating IP pools extension is not available on target cloud') + + data = proxy._json_response( + self.compute.get('os-floating-ip-pools'), + error_message="Error fetching floating IP pool list") + pools = self._get_and_munchify('floating_ip_pools', data) + return [{'name': p['name']} for p in pools] + + def list_floating_ips(self, filters=None): + """List all available floating IPs. + + :param filters: (optional) dict of filter conditions to push down + :returns: A list of floating IP ``munch.Munch``. + + """ + # If pushdown filters are specified and we do not have batched caching + # enabled, bypass local caching and push down the filters. + if filters and self._FLOAT_AGE == 0: + return self._list_floating_ips(filters) + + if (time.time() - self._floating_ips_time) >= self._FLOAT_AGE: + # Since we're using cached data anyway, we don't need to + # have more than one thread actually submit the list + # floating ips task. Let the first one submit it while holding + # a lock, and the non-blocking acquire method will cause + # subsequent threads to just skip this and use the old + # data until it succeeds. + # Initially when we never got data, block to retrieve some data. + first_run = self._floating_ips is None + if self._floating_ips_lock.acquire(first_run): + try: + if not (first_run and self._floating_ips is not None): + self._floating_ips = self._list_floating_ips() + self._floating_ips_time = time.time() + finally: + self._floating_ips_lock.release() + # Wrap the return with filter_list so that if filters were passed + # but we were batching/caching and thus always fetching the whole + # list from the cloud, we still return a filtered list. + return _utils._filter_list(self._floating_ips, None, filters) + + def get_floating_ip_by_id(self, id): + """ Get a floating ip by ID + + :param id: ID of the floating ip. + :returns: A floating ip ``munch.Munch``. + """ + error_message = "Error getting floating ip with ID {id}".format(id=id) + + if self._use_neutron_floating(): + data = proxy._json_response( + self.network.get('/floatingips/{id}'.format(id=id)), + error_message=error_message) + return self._normalize_floating_ip( + self._get_and_munchify('floatingip', data)) + else: + data = proxy._json_response( + self.compute.get('/os-floating-ips/{id}'.format(id=id)), + error_message=error_message) + return self._normalize_floating_ip( + self._get_and_munchify('floating_ip', data)) + + def _neutron_available_floating_ips( + self, network=None, project_id=None, server=None): + """Get a floating IP from a network. + + Return a list of available floating IPs or allocate a new one and + return it in a list of 1 element. + + :param network: A single network name or ID, or a list of them. + :param server: (server) Server the Floating IP is for + + :returns: a list of floating IP addresses. + + :raises: ``OpenStackCloudResourceNotFound``, if an external network + that meets the specified criteria cannot be found. + """ + if project_id is None: + # Make sure we are only listing floatingIPs allocated the current + # tenant. This is the default behaviour of Nova + project_id = self.current_project_id + + if network: + if isinstance(network, six.string_types): + network = [network] + + # Use given list to get first matching external network + floating_network_id = None + for net in network: + for ext_net in self.get_external_ipv4_floating_networks(): + if net in (ext_net['name'], ext_net['id']): + floating_network_id = ext_net['id'] + break + if floating_network_id: + break + + if floating_network_id is None: + raise exc.OpenStackCloudResourceNotFound( + "unable to find external network {net}".format( + net=network) + ) + else: + floating_network_id = self._get_floating_network_id() + + filters = { + 'port': None, + 'network': floating_network_id, + 'location': {'project': {'id': project_id}}, + } + + floating_ips = self._list_floating_ips() + available_ips = _utils._filter_list( + floating_ips, name_or_id=None, filters=filters) + if available_ips: + return available_ips + + # No available IP found or we didn't try + # allocate a new Floating IP + f_ip = self._neutron_create_floating_ip( + network_id=floating_network_id, server=server) + + return [f_ip] + + def _nova_available_floating_ips(self, pool=None): + """Get available floating IPs from a floating IP pool. + + Return a list of available floating IPs or allocate a new one and + return it in a list of 1 element. + + :param pool: Nova floating IP pool name. + + :returns: a list of floating IP addresses. + + :raises: ``OpenStackCloudResourceNotFound``, if a floating IP pool + is not specified and cannot be found. + """ + + with _utils.shade_exceptions( + "Unable to create floating IP in pool {pool}".format( + pool=pool)): + if pool is None: + pools = self.list_floating_ip_pools() + if not pools: + raise exc.OpenStackCloudResourceNotFound( + "unable to find a floating ip pool") + pool = pools[0]['name'] + + filters = { + 'instance_id': None, + 'pool': pool + } + + floating_ips = self._nova_list_floating_ips() + available_ips = _utils._filter_list( + floating_ips, name_or_id=None, filters=filters) + if available_ips: + return available_ips + + # No available IP found or we did not try. + # Allocate a new Floating IP + f_ip = self._nova_create_floating_ip(pool=pool) + + return [f_ip] + + def _find_floating_network_by_router(self): + """Find the network providing floating ips by looking at routers.""" + + if self._floating_network_by_router_lock.acquire( + not self._floating_network_by_router_run): + if self._floating_network_by_router_run: + self._floating_network_by_router_lock.release() + return self._floating_network_by_router + try: + for router in self.list_routers(): + if router['admin_state_up']: + network_id = router.get( + 'external_gateway_info', {}).get('network_id') + if network_id: + self._floating_network_by_router = network_id + finally: + self._floating_network_by_router_run = True + self._floating_network_by_router_lock.release() + return self._floating_network_by_router + + def available_floating_ip(self, network=None, server=None): + """Get a floating IP from a network or a pool. + + Return the first available floating IP or allocate a new one. + + :param network: Name or ID of the network. + :param server: Server the IP is for if known + + :returns: a (normalized) structure with a floating IP address + description. + """ + if self._use_neutron_floating(): + try: + f_ips = self._normalize_floating_ips( + self._neutron_available_floating_ips( + network=network, server=server)) + return f_ips[0] + except exc.OpenStackCloudURINotFound as e: + self.log.debug( + "Something went wrong talking to neutron API: " + "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) + # Fall-through, trying with Nova + + f_ips = self._normalize_floating_ips( + self._nova_available_floating_ips(pool=network) + ) + return f_ips[0] + + def _get_floating_network_id(self): + # Get first existing external IPv4 network + networks = self.get_external_ipv4_floating_networks() + if networks: + floating_network_id = networks[0]['id'] + else: + floating_network = self._find_floating_network_by_router() + if floating_network: + floating_network_id = floating_network + else: + raise exc.OpenStackCloudResourceNotFound( + "unable to find an external network") + return floating_network_id + + def create_floating_ip(self, network=None, server=None, + fixed_address=None, nat_destination=None, + port=None, wait=False, timeout=60): + """Allocate a new floating IP from a network or a pool. + + :param network: Name or ID of the network + that the floating IP should come from. + :param server: (optional) Server dict for the server to create + the IP for and to which it should be attached. + :param fixed_address: (optional) Fixed IP to attach the floating + ip to. + :param nat_destination: (optional) Name or ID of the network + that the fixed IP to attach the floating + IP to should be on. + :param port: (optional) The port ID that the floating IP should be + attached to. Specifying a port conflicts + with specifying a server, fixed_address or + nat_destination. + :param wait: (optional) Whether to wait for the IP to be active. + Defaults to False. Only applies if a server is + provided. + :param timeout: (optional) How long to wait for the IP to be active. + Defaults to 60. Only applies if a server is + provided. + + :returns: a floating IP address + + :raises: ``OpenStackCloudException``, on operation error. + """ + if self._use_neutron_floating(): + try: + return self._neutron_create_floating_ip( + network_name_or_id=network, server=server, + fixed_address=fixed_address, + nat_destination=nat_destination, + port=port, + wait=wait, timeout=timeout) + except exc.OpenStackCloudURINotFound as e: + self.log.debug( + "Something went wrong talking to neutron API: " + "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) + # Fall-through, trying with Nova + + if port: + raise exc.OpenStackCloudException( + "This cloud uses nova-network which does not support" + " arbitrary floating-ip/port mappings. Please nudge" + " your cloud provider to upgrade the networking stack" + " to neutron, or alternately provide the server," + " fixed_address and nat_destination arguments as appropriate") + # Else, we are using Nova network + f_ips = self._normalize_floating_ips( + [self._nova_create_floating_ip(pool=network)]) + return f_ips[0] + + def _submit_create_fip(self, kwargs): + # Split into a method to aid in test mocking + data = self.network.post( + "/floatingips.json", json={"floatingip": kwargs}) + return self._normalize_floating_ip( + self._get_and_munchify('floatingip', data)) + + def _neutron_create_floating_ip( + self, network_name_or_id=None, server=None, + fixed_address=None, nat_destination=None, + port=None, + wait=False, timeout=60, network_id=None): + + if not network_id: + if network_name_or_id: + network = self.get_network(network_name_or_id) + if not network: + raise exc.OpenStackCloudResourceNotFound( + "unable to find network for floating ips with ID " + "{0}".format(network_name_or_id)) + network_id = network['id'] + else: + network_id = self._get_floating_network_id() + kwargs = { + 'floating_network_id': network_id, + } + if not port: + if server: + (port_obj, fixed_ip_address) = self._nat_destination_port( + server, fixed_address=fixed_address, + nat_destination=nat_destination) + if port_obj: + port = port_obj['id'] + if fixed_ip_address: + kwargs['fixed_ip_address'] = fixed_ip_address + if port: + kwargs['port_id'] = port + + fip = self._submit_create_fip(kwargs) + fip_id = fip['id'] + + if port: + # The FIP is only going to become active in this context + # when we've attached it to something, which only occurs + # if we've provided a port as a parameter + if wait: + try: + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for the floating IP" + " to be ACTIVE", + wait=self._FLOAT_AGE): + fip = self.get_floating_ip(fip_id) + if fip and fip['status'] == 'ACTIVE': + break + except exc.OpenStackCloudTimeout: + self.log.error( + "Timed out on floating ip %(fip)s becoming active." + " Deleting", {'fip': fip_id}) + try: + self.delete_floating_ip(fip_id) + except Exception as e: + self.log.error( + "FIP LEAK: Attempted to delete floating ip " + "%(fip)s but received %(exc)s exception: " + "%(err)s", {'fip': fip_id, 'exc': e.__class__, + 'err': str(e)}) + raise + if fip['port_id'] != port: + if server: + raise exc.OpenStackCloudException( + "Attempted to create FIP on port {port} for server" + " {server} but FIP has port {port_id}".format( + port=port, port_id=fip['port_id'], + server=server['id'])) + else: + raise exc.OpenStackCloudException( + "Attempted to create FIP on port {port}" + " but something went wrong".format(port=port)) + return fip + + def _nova_create_floating_ip(self, pool=None): + with _utils.shade_exceptions( + "Unable to create floating IP in pool {pool}".format( + pool=pool)): + if pool is None: + pools = self.list_floating_ip_pools() + if not pools: + raise exc.OpenStackCloudResourceNotFound( + "unable to find a floating ip pool") + pool = pools[0]['name'] + + data = proxy._json_response(self.compute.post( + '/os-floating-ips', json=dict(pool=pool))) + pool_ip = self._get_and_munchify('floating_ip', data) + # TODO(mordred) Remove this - it's just for compat + data = proxy._json_response( + self.compute.get('/os-floating-ips/{id}'.format( + id=pool_ip['id']))) + return self._get_and_munchify('floating_ip', data) + + def delete_floating_ip(self, floating_ip_id, retry=1): + """Deallocate a floating IP from a project. + + :param floating_ip_id: a floating IP address ID. + :param retry: number of times to retry. Optional, defaults to 1, + which is in addition to the initial delete call. + A value of 0 will also cause no checking of results to + occur. + + :returns: True if the IP address has been deleted, False if the IP + address was not found. + + :raises: ``OpenStackCloudException``, on operation error. + """ + for count in range(0, max(0, retry) + 1): + result = self._delete_floating_ip(floating_ip_id) + + if (retry == 0) or not result: + return result + + # Wait for the cached floating ip list to be regenerated + if self._FLOAT_AGE: + time.sleep(self._FLOAT_AGE) + + # neutron sometimes returns success when deleting a floating + # ip. That's awesome. SO - verify that the delete actually + # worked. Some clouds will set the status to DOWN rather than + # deleting the IP immediately. This is, of course, a bit absurd. + f_ip = self.get_floating_ip(id=floating_ip_id) + if not f_ip or f_ip['status'] == 'DOWN': + return True + + raise exc.OpenStackCloudException( + "Attempted to delete Floating IP {ip} with ID {id} a total of" + " {retry} times. Although the cloud did not indicate any errors" + " the floating ip is still in existence. Aborting further" + " operations.".format( + id=floating_ip_id, ip=f_ip['floating_ip_address'], + retry=retry + 1)) + + def _delete_floating_ip(self, floating_ip_id): + if self._use_neutron_floating(): + try: + return self._neutron_delete_floating_ip(floating_ip_id) + except exc.OpenStackCloudURINotFound as e: + self.log.debug( + "Something went wrong talking to neutron API: " + "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) + return self._nova_delete_floating_ip(floating_ip_id) + + def _neutron_delete_floating_ip(self, floating_ip_id): + try: + proxy._json_response(self.network.delete( + "/floatingips/{fip_id}.json".format(fip_id=floating_ip_id), + error_message="unable to delete floating IP")) + except exc.OpenStackCloudResourceNotFound: + return False + except Exception as e: + raise exc.OpenStackCloudException( + "Unable to delete floating IP ID {fip_id}: {msg}".format( + fip_id=floating_ip_id, msg=str(e))) + return True + + def _nova_delete_floating_ip(self, floating_ip_id): + try: + proxy._json_response( + self.compute.delete( + '/os-floating-ips/{id}'.format(id=floating_ip_id)), + error_message='Unable to delete floating IP {fip_id}'.format( + fip_id=floating_ip_id)) + except exc.OpenStackCloudURINotFound: + return False + return True + + def delete_unattached_floating_ips(self, retry=1): + """Safely delete unattached floating ips. + + If the cloud can safely purge any unattached floating ips without + race conditions, do so. + + Safely here means a specific thing. It means that you are not running + this while another process that might do a two step create/attach + is running. You can safely run this method while another process + is creating servers and attaching floating IPs to them if either that + process is using add_auto_ip from shade, or is creating the floating + IPs by passing in a server to the create_floating_ip call. + + :param retry: number of times to retry. Optional, defaults to 1, + which is in addition to the initial delete call. + A value of 0 will also cause no checking of results to + occur. + + :returns: Number of Floating IPs deleted, False if none + + :raises: ``OpenStackCloudException``, on operation error. + """ + processed = [] + if self._use_neutron_floating(): + for ip in self.list_floating_ips(): + if not ip['attached']: + processed.append(self.delete_floating_ip( + floating_ip_id=ip['id'], retry=retry)) + return len(processed) if all(processed) else False + + def _attach_ip_to_server( + self, server, floating_ip, + fixed_address=None, wait=False, + timeout=60, skip_attach=False, nat_destination=None): + """Attach a floating IP to a server. + + :param server: Server dict + :param floating_ip: Floating IP dict to attach + :param fixed_address: (optional) fixed address to which attach the + floating IP to. + :param wait: (optional) Wait for the address to appear as assigned + to the server. Defaults to False. + :param timeout: (optional) Seconds to wait, defaults to 60. + See the ``wait`` parameter. + :param skip_attach: (optional) Skip the actual attach and just do + the wait. Defaults to False. + :param nat_destination: The fixed network the server's port for the + FIP to attach to will come from. + + :returns: The server ``munch.Munch`` + + :raises: OpenStackCloudException, on operation error. + """ + # Short circuit if we're asking to attach an IP that's already + # attached + ext_ip = meta.get_server_ip(server, ext_tag='floating', public=True) + if ext_ip == floating_ip['floating_ip_address']: + return server + + if self._use_neutron_floating(): + if not skip_attach: + try: + self._neutron_attach_ip_to_server( + server=server, floating_ip=floating_ip, + fixed_address=fixed_address, + nat_destination=nat_destination) + except exc.OpenStackCloudURINotFound as e: + self.log.debug( + "Something went wrong talking to neutron API: " + "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) + # Fall-through, trying with Nova + else: + # Nova network + self._nova_attach_ip_to_server( + server_id=server['id'], floating_ip_id=floating_ip['id'], + fixed_address=fixed_address) + + if wait: + # Wait for the address to be assigned to the server + server_id = server['id'] + for _ in utils.iterate_timeout( + timeout, + "Timeout waiting for the floating IP to be attached.", + wait=self._SERVER_AGE): + server = self.get_server(server_id) + ext_ip = meta.get_server_ip( + server, ext_tag='floating', public=True) + if ext_ip == floating_ip['floating_ip_address']: + return server + return server + + def _neutron_attach_ip_to_server( + self, server, floating_ip, fixed_address=None, + nat_destination=None): + + # Find an available port + (port, fixed_address) = self._nat_destination_port( + server, fixed_address=fixed_address, + nat_destination=nat_destination) + if not port: + raise exc.OpenStackCloudException( + "unable to find a port for server {0}".format( + server['id'])) + + floating_ip_args = {'port_id': port['id']} + if fixed_address is not None: + floating_ip_args['fixed_ip_address'] = fixed_address + + return proxy._json_response( + self.network.put( + "/floatingips/{fip_id}.json".format(fip_id=floating_ip['id']), + json={'floatingip': floating_ip_args}), + error_message=("Error attaching IP {ip} to " + "server {server_id}".format( + ip=floating_ip['id'], + server_id=server['id']))) + + def _nova_attach_ip_to_server(self, server_id, floating_ip_id, + fixed_address=None): + f_ip = self.get_floating_ip( + id=floating_ip_id) + if f_ip is None: + raise exc.OpenStackCloudException( + "unable to find floating IP {0}".format(floating_ip_id)) + error_message = "Error attaching IP {ip} to instance {id}".format( + ip=floating_ip_id, id=server_id) + body = { + 'address': f_ip['floating_ip_address'] + } + if fixed_address: + body['fixed_address'] = fixed_address + return proxy._json_response( + self.compute.post( + '/servers/{server_id}/action'.format(server_id=server_id), + json=dict(addFloatingIp=body)), + error_message=error_message) + + def detach_ip_from_server(self, server_id, floating_ip_id): + """Detach a floating IP from a server. + + :param server_id: ID of a server. + :param floating_ip_id: Id of the floating IP to detach. + + :returns: True if the IP has been detached, or False if the IP wasn't + attached to any server. + + :raises: ``OpenStackCloudException``, on operation error. + """ + if self._use_neutron_floating(): + try: + return self._neutron_detach_ip_from_server( + server_id=server_id, floating_ip_id=floating_ip_id) + except exc.OpenStackCloudURINotFound as e: + self.log.debug( + "Something went wrong talking to neutron API: " + "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) + # Fall-through, trying with Nova + + # Nova network + self._nova_detach_ip_from_server( + server_id=server_id, floating_ip_id=floating_ip_id) + + def _neutron_detach_ip_from_server(self, server_id, floating_ip_id): + f_ip = self.get_floating_ip(id=floating_ip_id) + if f_ip is None or not f_ip['attached']: + return False + exceptions.raise_from_response( + self.network.put( + "/floatingips/{fip_id}.json".format(fip_id=floating_ip_id), + json={"floatingip": {"port_id": None}}), + error_message=("Error detaching IP {ip} from " + "server {server_id}".format( + ip=floating_ip_id, server_id=server_id))) + + return True + + def _nova_detach_ip_from_server(self, server_id, floating_ip_id): + + f_ip = self.get_floating_ip(id=floating_ip_id) + if f_ip is None: + raise exc.OpenStackCloudException( + "unable to find floating IP {0}".format(floating_ip_id)) + error_message = "Error detaching IP {ip} from instance {id}".format( + ip=floating_ip_id, id=server_id) + return proxy._json_response( + self.compute.post( + '/servers/{server_id}/action'.format(server_id=server_id), + json=dict(removeFloatingIp=dict( + address=f_ip['floating_ip_address']))), + error_message=error_message) + + return True + + def _add_ip_from_pool( + self, server, network, fixed_address=None, reuse=True, + wait=False, timeout=60, nat_destination=None): + """Add a floating IP to a server from a given pool + + This method reuses available IPs, when possible, or allocate new IPs + to the current tenant. + The floating IP is attached to the given fixed address or to the + first server port/fixed address + + :param server: Server dict + :param network: Name or ID of the network. + :param fixed_address: a fixed address + :param reuse: Try to reuse existing ips. Defaults to True. + :param wait: (optional) Wait for the address to appear as assigned + to the server. Defaults to False. + :param timeout: (optional) Seconds to wait, defaults to 60. + See the ``wait`` parameter. + :param nat_destination: (optional) the name of the network of the + port to associate with the floating ip. + + :returns: the updated server ``munch.Munch`` + """ + if reuse: + f_ip = self.available_floating_ip(network=network) + else: + start_time = time.time() + f_ip = self.create_floating_ip( + server=server, + network=network, nat_destination=nat_destination, + wait=wait, timeout=timeout) + timeout = timeout - (time.time() - start_time) + # Wait for cache invalidation time so that we don't try + # to attach the FIP a second time below + time.sleep(self._SERVER_AGE) + server = self.get_server(server.id) + + # We run attach as a second call rather than in the create call + # because there are code flows where we will not have an attached + # FIP yet. However, even if it was attached in the create, we run + # the attach function below to get back the server dict refreshed + # with the FIP information. + return self._attach_ip_to_server( + server=server, floating_ip=f_ip, fixed_address=fixed_address, + wait=wait, timeout=timeout, nat_destination=nat_destination) + + def add_ip_list( + self, server, ips, wait=False, timeout=60, + fixed_address=None): + """Attach a list of IPs to a server. + + :param server: a server object + :param ips: list of floating IP addresses or a single address + :param wait: (optional) Wait for the address to appear as assigned + to the server. Defaults to False. + :param timeout: (optional) Seconds to wait, defaults to 60. + See the ``wait`` parameter. + :param fixed_address: (optional) Fixed address of the server to + attach the IP to + + :returns: The updated server ``munch.Munch`` + + :raises: ``OpenStackCloudException``, on operation error. + """ + if type(ips) == list: + ip = ips[0] + else: + ip = ips + f_ip = self.get_floating_ip( + id=None, filters={'floating_ip_address': ip}) + return self._attach_ip_to_server( + server=server, floating_ip=f_ip, wait=wait, timeout=timeout, + fixed_address=fixed_address) + + def add_auto_ip(self, server, wait=False, timeout=60, reuse=True): + """Add a floating IP to a server. + + This method is intended for basic usage. For advanced network + architecture (e.g. multiple external networks or servers with multiple + interfaces), use other floating IP methods. + + This method can reuse available IPs, or allocate new IPs to the current + project. + + :param server: a server dictionary. + :param reuse: Whether or not to attempt to reuse IPs, defaults + to True. + :param wait: (optional) Wait for the address to appear as assigned + to the server. Defaults to False. + :param timeout: (optional) Seconds to wait, defaults to 60. + See the ``wait`` parameter. + :param reuse: Try to reuse existing ips. Defaults to True. + + :returns: Floating IP address attached to server. + + """ + server = self._add_auto_ip( + server, wait=wait, timeout=timeout, reuse=reuse) + return server['interface_ip'] or None + + def _add_auto_ip(self, server, wait=False, timeout=60, reuse=True): + skip_attach = False + created = False + if reuse: + f_ip = self.available_floating_ip(server=server) + else: + start_time = time.time() + f_ip = self.create_floating_ip( + server=server, wait=wait, timeout=timeout) + timeout = timeout - (time.time() - start_time) + if server: + # This gets passed in for both nova and neutron + # but is only meaningful for the neutron logic branch + skip_attach = True + created = True + + try: + # We run attach as a second call rather than in the create call + # because there are code flows where we will not have an attached + # FIP yet. However, even if it was attached in the create, we run + # the attach function below to get back the server dict refreshed + # with the FIP information. + return self._attach_ip_to_server( + server=server, floating_ip=f_ip, wait=wait, timeout=timeout, + skip_attach=skip_attach) + except exc.OpenStackCloudTimeout: + if self._use_neutron_floating() and created: + # We are here because we created an IP on the port + # It failed. Delete so as not to leak an unmanaged + # resource + self.log.error( + "Timeout waiting for floating IP to become" + " active. Floating IP %(ip)s:%(id)s was created for" + " server %(server)s but is being deleted due to" + " activation failure.", { + 'ip': f_ip['floating_ip_address'], + 'id': f_ip['id'], + 'server': server['id']}) + try: + self.delete_floating_ip(f_ip['id']) + except Exception as e: + self.log.error( + "FIP LEAK: Attempted to delete floating ip " + "%(fip)s but received %(exc)s exception: %(err)s", + {'fip': f_ip['id'], 'exc': e.__class__, 'err': str(e)}) + raise e + raise + + def add_ips_to_server( + self, server, auto_ip=True, ips=None, ip_pool=None, + wait=False, timeout=60, reuse=True, fixed_address=None, + nat_destination=None): + if ip_pool: + server = self._add_ip_from_pool( + server, ip_pool, reuse=reuse, wait=wait, timeout=timeout, + fixed_address=fixed_address, nat_destination=nat_destination) + elif ips: + server = self.add_ip_list( + server, ips, wait=wait, timeout=timeout, + fixed_address=fixed_address) + elif auto_ip: + if self._needs_floating_ip(server, nat_destination): + server = self._add_auto_ip( + server, wait=wait, timeout=timeout, reuse=reuse) + return server + + def _needs_floating_ip(self, server, nat_destination): + """Figure out if auto_ip should add a floating ip to this server. + + If the server has a public_v4 it does not need a floating ip. + + If the server does not have a private_v4 it does not need a + floating ip. + + If self.private then the server does not need a floating ip. + + If the cloud runs nova, and the server has a private_v4 and not + a public_v4, then the server needs a floating ip. + + If the server has a private_v4 and no public_v4 and the cloud has + a network from which floating IPs come that is connected via a + router to the network from which the private_v4 address came, + then the server needs a floating ip. + + If the server has a private_v4 and no public_v4 and the cloud + does not have a network from which floating ips come, or it has + one but that network is not connected to the network from which + the server's private_v4 address came via a router, then the + server does not need a floating ip. + """ + if not self._has_floating_ips(): + return False + + if server['public_v4']: + return False + + if not server['private_v4']: + return False + + if self.private: + return False + + if not self.has_service('network'): + return True + + # No floating ip network - no FIPs + try: + self._get_floating_network_id() + except exc.OpenStackCloudException: + return False + + (port_obj, fixed_ip_address) = self._nat_destination_port( + server, nat_destination=nat_destination) + + if not port_obj or not fixed_ip_address: + return False + + return True + + def _nat_destination_port( + self, server, fixed_address=None, nat_destination=None): + """Returns server port that is on a nat_destination network + + Find a port attached to the server which is on a network which + has a subnet which can be the destination of NAT. Such a network + is referred to in shade as a "nat_destination" network. So this + then is a function which returns a port on such a network that is + associated with the given server. + + :param server: Server dict. + :param fixed_address: Fixed ip address of the port + :param nat_destination: Name or ID of the network of the port. + """ + # If we are caching port lists, we may not find the port for + # our server if the list is old. Try for at least 2 cache + # periods if that is the case. + if self._PORT_AGE: + timeout = self._PORT_AGE * 2 + else: + timeout = None + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for port to show up in list", + wait=self._PORT_AGE): + try: + port_filter = {'device_id': server['id']} + ports = self.search_ports(filters=port_filter) + break + except exc.OpenStackCloudTimeout: + ports = None + if not ports: + return (None, None) + port = None + if not fixed_address: + if len(ports) > 1: + if nat_destination: + nat_network = self.get_network(nat_destination) + if not nat_network: + raise exc.OpenStackCloudException( + 'NAT Destination {nat_destination} was configured' + ' but not found on the cloud. Please check your' + ' config and your cloud and try again.'.format( + nat_destination=nat_destination)) + else: + nat_network = self.get_nat_destination() + + if not nat_network: + raise exc.OpenStackCloudException( + 'Multiple ports were found for server {server}' + ' but none of the networks are a valid NAT' + ' destination, so it is impossible to add a' + ' floating IP. If you have a network that is a valid' + ' destination for NAT and we could not find it,' + ' please file a bug. But also configure the' + ' nat_destination property of the networks list in' + ' your clouds.yaml file. If you do not have a' + ' clouds.yaml file, please make one - your setup' + ' is complicated.'.format(server=server['id'])) + + maybe_ports = [] + for maybe_port in ports: + if maybe_port['network_id'] == nat_network['id']: + maybe_ports.append(maybe_port) + if not maybe_ports: + raise exc.OpenStackCloudException( + 'No port on server {server} was found matching' + ' your NAT destination network {dest}. Please ' + ' check your config'.format( + server=server['id'], dest=nat_network['name'])) + ports = maybe_ports + + # Select the most recent available IPv4 address + # To do this, sort the ports in reverse order by the created_at + # field which is a string containing an ISO DateTime (which + # thankfully sort properly) This way the most recent port created, + # if there are more than one, will be the arbitrary port we + # select. + for port in sorted( + ports, + key=lambda p: p.get('created_at', 0), + reverse=True): + for address in port.get('fixed_ips', list()): + try: + ip = ipaddress.ip_address(address['ip_address']) + except Exception: + continue + if ip.version == 4: + fixed_address = address['ip_address'] + return port, fixed_address + raise exc.OpenStackCloudException( + "unable to find a free fixed IPv4 address for server " + "{0}".format(server['id'])) + # unfortunately a port can have more than one fixed IP: + # we can't use the search_ports filtering for fixed_address as + # they are contained in a list. e.g. + # + # "fixed_ips": [ + # { + # "subnet_id": "008ba151-0b8c-4a67-98b5-0d2b87666062", + # "ip_address": "172.24.4.2" + # } + # ] + # + # Search fixed_address + for p in ports: + for fixed_ip in p['fixed_ips']: + if fixed_address == fixed_ip['ip_address']: + return (p, fixed_address) + return (None, None) + + def _has_floating_ips(self): + if not self._floating_ip_source: + return False + else: + return self._floating_ip_source in ('nova', 'neutron') + + def _use_neutron_floating(self): + return (self.has_service('network') + and self._floating_ip_source == 'neutron') diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py new file mode 100644 index 000000000..9e825cadf --- /dev/null +++ b/openstack/cloud/_identity.py @@ -0,0 +1,1534 @@ +# 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 types so that we can reference ListType in sphinx param declarations. +# We can't just use list, because sphinx gets confused by +# openstack.resource.Resource.list and openstack.resource2.Resource.list +import types # noqa + +import munch + +from openstack.cloud import exc +from openstack.cloud import _normalize +from openstack.cloud import _utils +from openstack import utils + + +class IdentityCloudMixin(_normalize.Normalizer): + + @property + def _identity_client(self): + if 'identity' not in self._raw_clients: + self._raw_clients['identity'] = self._get_versioned_client( + 'identity', min_version=2, max_version='3.latest') + return self._raw_clients['identity'] + + @_utils.cache_on_arguments() + def list_projects(self, domain_id=None, name_or_id=None, filters=None): + """List projects. + + With no parameters, returns a full listing of all visible projects. + + :param domain_id: domain ID to scope the searched projects. + :param name_or_id: project name or ID. + :param filters: a dict containing additional filters to use + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: a list of ``munch.Munch`` containing the projects + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + kwargs = dict( + filters=filters, + domain_id=domain_id) + if self._is_client_version('identity', 3): + kwargs['obj_name'] = 'project' + + pushdown, filters = _normalize._split_filters(**kwargs) + + try: + if self._is_client_version('identity', 3): + key = 'projects' + else: + key = 'tenants' + data = self._identity_client.get( + '/{endpoint}'.format(endpoint=key), params=pushdown) + projects = self._normalize_projects( + self._get_and_munchify(key, data)) + except Exception as e: + self.log.debug("Failed to list projects", exc_info=True) + raise exc.OpenStackCloudException(str(e)) + return _utils._filter_list(projects, name_or_id, filters) + + def search_projects(self, name_or_id=None, filters=None, domain_id=None): + '''Backwards compatibility method for search_projects + + search_projects originally had a parameter list that was name_or_id, + filters and list had domain_id first. This method exists in this form + to allow code written with positional parameter to still work. But + really, use keyword arguments. + ''' + return self.list_projects( + domain_id=domain_id, name_or_id=name_or_id, filters=filters) + + def get_project(self, name_or_id, filters=None, domain_id=None): + """Get exactly one project. + + :param name_or_id: project name or ID. + :param filters: a dict containing additional filters to use. + :param domain_id: domain ID (identity v3 only). + + :returns: a list of ``munch.Munch`` containing the project description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + return _utils._get_entity(self, 'project', name_or_id, filters, + domain_id=domain_id) + + @_utils.valid_kwargs('description') + def update_project(self, name_or_id, enabled=None, domain_id=None, + **kwargs): + with _utils.shade_exceptions( + "Error in updating project {project}".format( + project=name_or_id)): + proj = self.get_project(name_or_id, domain_id=domain_id) + if not proj: + raise exc.OpenStackCloudException( + "Project %s not found." % name_or_id) + if enabled is not None: + kwargs.update({'enabled': enabled}) + # NOTE(samueldmq): Current code only allow updates of description + # or enabled fields. + if self._is_client_version('identity', 3): + data = self._identity_client.patch( + '/projects/' + proj['id'], json={'project': kwargs}) + project = self._get_and_munchify('project', data) + else: + data = self._identity_client.post( + '/tenants/' + proj['id'], json={'tenant': kwargs}) + project = self._get_and_munchify('tenant', data) + project = self._normalize_project(project) + self.list_projects.invalidate(self) + return project + + def create_project( + self, name, description=None, domain_id=None, enabled=True): + """Create a project.""" + with _utils.shade_exceptions( + "Error in creating project {project}".format(project=name)): + project_ref = self._get_domain_id_param_dict(domain_id) + project_ref.update({'name': name, + 'description': description, + 'enabled': enabled}) + endpoint, key = ('tenants', 'tenant') + if self._is_client_version('identity', 3): + endpoint, key = ('projects', 'project') + data = self._identity_client.post( + '/{endpoint}'.format(endpoint=endpoint), + json={key: project_ref}) + project = self._normalize_project( + self._get_and_munchify(key, data)) + self.list_projects.invalidate(self) + return project + + def delete_project(self, name_or_id, domain_id=None): + """Delete a project. + + :param string name_or_id: Project name or ID. + :param string domain_id: Domain ID containing the project(identity v3 + only). + + :returns: True if delete succeeded, False if the project was not found. + + :raises: ``OpenStackCloudException`` if something goes wrong during + the OpenStack API call + """ + + with _utils.shade_exceptions( + "Error in deleting project {project}".format( + project=name_or_id)): + project = self.get_project(name_or_id, domain_id=domain_id) + if project is None: + self.log.debug( + "Project %s not found for deleting", name_or_id) + return False + + if self._is_client_version('identity', 3): + self._identity_client.delete('/projects/' + project['id']) + else: + self._identity_client.delete('/tenants/' + project['id']) + + return True + + @_utils.valid_kwargs('domain_id') + @_utils.cache_on_arguments() + def list_users(self, **kwargs): + """List users. + + :param domain_id: Domain ID. (v3) + + :returns: a list of ``munch.Munch`` containing the user description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + data = self._identity_client.get('/users', params=kwargs) + return _utils.normalize_users( + self._get_and_munchify('users', data)) + + @_utils.valid_kwargs('domain_id') + def search_users(self, name_or_id=None, filters=None, **kwargs): + """Search users. + + :param string name_or_id: user name or ID. + :param domain_id: Domain ID. (v3) + :param filters: a dict containing additional filters to use. + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: a list of ``munch.Munch`` containing the users + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + users = self.list_users(**kwargs) + return _utils._filter_list(users, name_or_id, filters) + + @_utils.valid_kwargs('domain_id') + def get_user(self, name_or_id, filters=None, **kwargs): + """Get exactly one user. + + :param string name_or_id: user name or ID. + :param domain_id: Domain ID. (v3) + :param filters: a dict containing additional filters to use. + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: a single ``munch.Munch`` containing the user description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + return _utils._get_entity(self, 'user', name_or_id, filters, **kwargs) + + def get_user_by_id(self, user_id, normalize=True): + """Get a user by ID. + + :param string user_id: user ID + :param bool normalize: Flag to control dict normalization + + :returns: a single ``munch.Munch`` containing the user description + """ + data = self._identity_client.get( + '/users/{user}'.format(user=user_id), + error_message="Error getting user with ID {user_id}".format( + user_id=user_id)) + + user = self._get_and_munchify('user', data) + if user and normalize: + user = _utils.normalize_users(user) + return user + + # NOTE(Shrews): Keystone v2 supports updating only name, email and enabled. + @_utils.valid_kwargs('name', 'email', 'enabled', 'domain_id', 'password', + 'description', 'default_project') + def update_user(self, name_or_id, **kwargs): + self.list_users.invalidate(self) + user_kwargs = {} + if 'domain_id' in kwargs and kwargs['domain_id']: + user_kwargs['domain_id'] = kwargs['domain_id'] + user = self.get_user(name_or_id, **user_kwargs) + + # TODO(mordred) When this changes to REST, force interface=admin + # in the adapter call if it's an admin force call (and figure out how + # to make that disctinction) + if self._is_client_version('identity', 2): + # Do not pass v3 args to a v2 keystone. + kwargs.pop('domain_id', None) + kwargs.pop('description', None) + kwargs.pop('default_project', None) + password = kwargs.pop('password', None) + if password is not None: + with _utils.shade_exceptions( + "Error updating password for {user}".format( + user=name_or_id)): + error_msg = "Error updating password for user {}".format( + name_or_id) + data = self._identity_client.put( + '/users/{u}/OS-KSADM/password'.format(u=user['id']), + json={'user': {'password': password}}, + error_message=error_msg) + + # Identity v2.0 implements PUT. v3 PATCH. Both work as PATCH. + data = self._identity_client.put( + '/users/{user}'.format(user=user['id']), json={'user': kwargs}, + error_message="Error in updating user {}".format(name_or_id)) + else: + # NOTE(samueldmq): now this is a REST call and domain_id is dropped + # if None. keystoneclient drops keys with None values. + if 'domain_id' in kwargs and kwargs['domain_id'] is None: + del kwargs['domain_id'] + data = self._identity_client.patch( + '/users/{user}'.format(user=user['id']), json={'user': kwargs}, + error_message="Error in updating user {}".format(name_or_id)) + + user = self._get_and_munchify('user', data) + self.list_users.invalidate(self) + return _utils.normalize_users([user])[0] + + def create_user( + self, name, password=None, email=None, default_project=None, + enabled=True, domain_id=None, description=None): + """Create a user.""" + params = self._get_identity_params(domain_id, default_project) + params.update({'name': name, 'password': password, 'email': email, + 'enabled': enabled}) + if self._is_client_version('identity', 3): + params['description'] = description + elif description is not None: + self.log.info( + "description parameter is not supported on Keystone v2") + + error_msg = "Error in creating user {user}".format(user=name) + data = self._identity_client.post('/users', json={'user': params}, + error_message=error_msg) + user = self._get_and_munchify('user', data) + + self.list_users.invalidate(self) + return _utils.normalize_users([user])[0] + + @_utils.valid_kwargs('domain_id') + def delete_user(self, name_or_id, **kwargs): + # TODO(mordred) Why are we invalidating at the TOP? + self.list_users.invalidate(self) + user = self.get_user(name_or_id, **kwargs) + if not user: + self.log.debug( + "User {0} not found for deleting".format(name_or_id)) + return False + + # TODO(mordred) Extra GET only needed to support keystoneclient. + # Can be removed as a follow-on. + user = self.get_user_by_id(user['id'], normalize=False) + self._identity_client.delete( + '/users/{user}'.format(user=user['id']), + error_message="Error in deleting user {user}".format( + user=name_or_id)) + + self.list_users.invalidate(self) + return True + + def _get_user_and_group(self, user_name_or_id, group_name_or_id): + user = self.get_user(user_name_or_id) + if not user: + raise exc.OpenStackCloudException( + 'User {user} not found'.format(user=user_name_or_id)) + + group = self.get_group(group_name_or_id) + if not group: + raise exc.OpenStackCloudException( + 'Group {user} not found'.format(user=group_name_or_id)) + + return (user, group) + + def add_user_to_group(self, name_or_id, group_name_or_id): + """Add a user to a group. + + :param string name_or_id: User name or ID + :param string group_name_or_id: Group name or ID + + :raises: ``OpenStackCloudException`` if something goes wrong during + the OpenStack API call + """ + user, group = self._get_user_and_group(name_or_id, group_name_or_id) + + error_msg = "Error adding user {user} to group {group}".format( + user=name_or_id, group=group_name_or_id) + self._identity_client.put( + '/groups/{g}/users/{u}'.format(g=group['id'], u=user['id']), + error_message=error_msg) + + def is_user_in_group(self, name_or_id, group_name_or_id): + """Check to see if a user is in a group. + + :param string name_or_id: User name or ID + :param string group_name_or_id: Group name or ID + + :returns: True if user is in the group, False otherwise + + :raises: ``OpenStackCloudException`` if something goes wrong during + the OpenStack API call + """ + user, group = self._get_user_and_group(name_or_id, group_name_or_id) + + try: + self._identity_client.head( + '/groups/{g}/users/{u}'.format(g=group['id'], u=user['id'])) + return True + except exc.OpenStackCloudURINotFound: + # NOTE(samueldmq): knowing this URI exists, let's interpret this as + # user not found in group rather than URI not found. + return False + + def remove_user_from_group(self, name_or_id, group_name_or_id): + """Remove a user from a group. + + :param string name_or_id: User name or ID + :param string group_name_or_id: Group name or ID + + :raises: ``OpenStackCloudException`` if something goes wrong during + the OpenStack API call + """ + user, group = self._get_user_and_group(name_or_id, group_name_or_id) + + error_msg = "Error removing user {user} from group {group}".format( + user=name_or_id, group=group_name_or_id) + self._identity_client.delete( + '/groups/{g}/users/{u}'.format(g=group['id'], u=user['id']), + error_message=error_msg) + + @_utils.valid_kwargs('type', 'service_type', 'description') + def create_service(self, name, enabled=True, **kwargs): + """Create a service. + + :param name: Service name. + :param type: Service type. (type or service_type required.) + :param service_type: Service type. (type or service_type required.) + :param description: Service description (optional). + :param enabled: Whether the service is enabled (v3 only) + + :returns: a ``munch.Munch`` containing the services description, + i.e. the following attributes:: + - id: + - name: + - type: + - service_type: + - description: + + :raises: ``OpenStackCloudException`` if something goes wrong during the + OpenStack API call. + + """ + type_ = kwargs.pop('type', None) + service_type = kwargs.pop('service_type', None) + + # TODO(mordred) When this changes to REST, force interface=admin + # in the adapter call + if self._is_client_version('identity', 2): + url, key = '/OS-KSADM/services', 'OS-KSADM:service' + kwargs['type'] = type_ or service_type + else: + url, key = '/services', 'service' + kwargs['type'] = type_ or service_type + kwargs['enabled'] = enabled + kwargs['name'] = name + + msg = 'Failed to create service {name}'.format(name=name) + data = self._identity_client.post( + url, json={key: kwargs}, error_message=msg) + service = self._get_and_munchify(key, data) + return _utils.normalize_keystone_services([service])[0] + + @_utils.valid_kwargs('name', 'enabled', 'type', 'service_type', + 'description') + def update_service(self, name_or_id, **kwargs): + # NOTE(SamYaple): Service updates are only available on v3 api + if self._is_client_version('identity', 2): + raise exc.OpenStackCloudUnavailableFeature( + 'Unavailable Feature: Service update requires Identity v3' + ) + + # NOTE(SamYaple): Keystone v3 only accepts 'type' but shade accepts + # both 'type' and 'service_type' with a preference + # towards 'type' + type_ = kwargs.pop('type', None) + service_type = kwargs.pop('service_type', None) + if type_ or service_type: + kwargs['type'] = type_ or service_type + + if self._is_client_version('identity', 2): + url, key = '/OS-KSADM/services', 'OS-KSADM:service' + else: + url, key = '/services', 'service' + + service = self.get_service(name_or_id) + msg = 'Error in updating service {service}'.format(service=name_or_id) + data = self._identity_client.patch( + '{url}/{id}'.format(url=url, id=service['id']), json={key: kwargs}, + error_message=msg) + service = self._get_and_munchify(key, data) + return _utils.normalize_keystone_services([service])[0] + + def list_services(self): + """List all Keystone services. + + :returns: a list of ``munch.Munch`` containing the services description + + :raises: ``OpenStackCloudException`` if something goes wrong during the + OpenStack API call. + """ + if self._is_client_version('identity', 2): + url, key = '/OS-KSADM/services', 'OS-KSADM:services' + endpoint_filter = {'interface': 'admin'} + else: + url, key = '/services', 'services' + endpoint_filter = {} + + data = self._identity_client.get( + url, endpoint_filter=endpoint_filter, + error_message="Failed to list services") + services = self._get_and_munchify(key, data) + return _utils.normalize_keystone_services(services) + + def search_services(self, name_or_id=None, filters=None): + """Search Keystone services. + + :param name_or_id: Name or id of the desired service. + :param filters: a dict containing additional filters to use. e.g. + {'type': 'network'}. + + :returns: a list of ``munch.Munch`` containing the services description + + :raises: ``OpenStackCloudException`` if something goes wrong during the + OpenStack API call. + """ + services = self.list_services() + return _utils._filter_list(services, name_or_id, filters) + + def get_service(self, name_or_id, filters=None): + """Get exactly one Keystone service. + + :param name_or_id: Name or id of the desired service. + :param filters: a dict containing additional filters to use. e.g. + {'type': 'network'} + + :returns: a ``munch.Munch`` containing the services description, + i.e. the following attributes:: + - id: + - name: + - type: + - description: + + :raises: ``OpenStackCloudException`` if something goes wrong during the + OpenStack API call or if multiple matches are found. + """ + return _utils._get_entity(self, 'service', name_or_id, filters) + + def delete_service(self, name_or_id): + """Delete a Keystone service. + + :param name_or_id: Service name or id. + + :returns: True if delete succeeded, False otherwise. + + :raises: ``OpenStackCloudException`` if something goes wrong during + the OpenStack API call + """ + service = self.get_service(name_or_id=name_or_id) + if service is None: + self.log.debug("Service %s not found for deleting", name_or_id) + return False + + if self._is_client_version('identity', 2): + url = '/OS-KSADM/services' + endpoint_filter = {'interface': 'admin'} + else: + url = '/services' + endpoint_filter = {} + + error_msg = 'Failed to delete service {id}'.format(id=service['id']) + self._identity_client.delete( + '{url}/{id}'.format(url=url, id=service['id']), + endpoint_filter=endpoint_filter, error_message=error_msg) + + return True + + @_utils.valid_kwargs('public_url', 'internal_url', 'admin_url') + def create_endpoint(self, service_name_or_id, url=None, interface=None, + region=None, enabled=True, **kwargs): + """Create a Keystone endpoint. + + :param service_name_or_id: Service name or id for this endpoint. + :param url: URL of the endpoint + :param interface: Interface type of the endpoint + :param public_url: Endpoint public URL. + :param internal_url: Endpoint internal URL. + :param admin_url: Endpoint admin URL. + :param region: Endpoint region. + :param enabled: Whether the endpoint is enabled + + NOTE: Both v2 (public_url, internal_url, admin_url) and v3 + (url, interface) calling semantics are supported. But + you can only use one of them at a time. + + :returns: a list of ``munch.Munch`` containing the endpoint description + + :raises: OpenStackCloudException if the service cannot be found or if + something goes wrong during the OpenStack API call. + """ + public_url = kwargs.pop('public_url', None) + internal_url = kwargs.pop('internal_url', None) + admin_url = kwargs.pop('admin_url', None) + + if (url or interface) and (public_url or internal_url or admin_url): + raise exc.OpenStackCloudException( + "create_endpoint takes either url and interface OR" + " public_url, internal_url, admin_url") + + service = self.get_service(name_or_id=service_name_or_id) + if service is None: + raise exc.OpenStackCloudException( + "service {service} not found".format( + service=service_name_or_id)) + + if self._is_client_version('identity', 2): + if url: + # v2.0 in use, v3-like arguments, one endpoint created + if interface != 'public': + raise exc.OpenStackCloudException( + "Error adding endpoint for service {service}." + " On a v2 cloud the url/interface API may only be" + " used for public url. Try using the public_url," + " internal_url, admin_url parameters instead of" + " url and interface".format( + service=service_name_or_id)) + endpoint_args = {'publicurl': url} + else: + # v2.0 in use, v2.0-like arguments, one endpoint created + endpoint_args = {} + if public_url: + endpoint_args.update({'publicurl': public_url}) + if internal_url: + endpoint_args.update({'internalurl': internal_url}) + if admin_url: + endpoint_args.update({'adminurl': admin_url}) + + # keystone v2.0 requires 'region' arg even if it is None + endpoint_args.update( + {'service_id': service['id'], 'region': region}) + + data = self._identity_client.post( + '/endpoints', json={'endpoint': endpoint_args}, + endpoint_filter={'interface': 'admin'}, + error_message=("Failed to create endpoint for service" + " {service}".format(service=service['name']))) + return [self._get_and_munchify('endpoint', data)] + else: + endpoints_args = [] + if url: + # v3 in use, v3-like arguments, one endpoint created + endpoints_args.append( + {'url': url, 'interface': interface, + 'service_id': service['id'], 'enabled': enabled, + 'region': region}) + else: + # v3 in use, v2.0-like arguments, one endpoint created for each + # interface url provided + endpoint_args = {'region': region, 'enabled': enabled, + 'service_id': service['id']} + if public_url: + endpoint_args.update({'url': public_url, + 'interface': 'public'}) + endpoints_args.append(endpoint_args.copy()) + if internal_url: + endpoint_args.update({'url': internal_url, + 'interface': 'internal'}) + endpoints_args.append(endpoint_args.copy()) + if admin_url: + endpoint_args.update({'url': admin_url, + 'interface': 'admin'}) + endpoints_args.append(endpoint_args.copy()) + + endpoints = [] + error_msg = ("Failed to create endpoint for service" + " {service}".format(service=service['name'])) + for args in endpoints_args: + data = self._identity_client.post( + '/endpoints', json={'endpoint': args}, + error_message=error_msg) + endpoints.append(self._get_and_munchify('endpoint', data)) + return endpoints + + @_utils.valid_kwargs('enabled', 'service_name_or_id', 'url', 'interface', + 'region') + def update_endpoint(self, endpoint_id, **kwargs): + # NOTE(SamYaple): Endpoint updates are only available on v3 api + if self._is_client_version('identity', 2): + raise exc.OpenStackCloudUnavailableFeature( + 'Unavailable Feature: Endpoint update' + ) + + service_name_or_id = kwargs.pop('service_name_or_id', None) + if service_name_or_id is not None: + kwargs['service_id'] = service_name_or_id + + data = self._identity_client.patch( + '/endpoints/{}'.format(endpoint_id), json={'endpoint': kwargs}, + error_message="Failed to update endpoint {}".format(endpoint_id)) + return self._get_and_munchify('endpoint', data) + + def list_endpoints(self): + """List Keystone endpoints. + + :returns: a list of ``munch.Munch`` containing the endpoint description + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + # Force admin interface if v2.0 is in use + v2 = self._is_client_version('identity', 2) + kwargs = {'endpoint_filter': {'interface': 'admin'}} if v2 else {} + + data = self._identity_client.get( + '/endpoints', error_message="Failed to list endpoints", **kwargs) + endpoints = self._get_and_munchify('endpoints', data) + + return endpoints + + def search_endpoints(self, id=None, filters=None): + """List Keystone endpoints. + + :param id: endpoint id. + :param filters: a dict containing additional filters to use. e.g. + {'region': 'region-a.geo-1'} + + :returns: a list of ``munch.Munch`` containing the endpoint + description. Each dict contains the following attributes:: + - id: + - region: + - public_url: + - internal_url: (optional) + - admin_url: (optional) + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + # NOTE(SamYaple): With keystone v3 we can filter directly via the + # the keystone api, but since the return of all the endpoints even in + # large environments is small, we can continue to filter in shade just + # like the v2 api. + endpoints = self.list_endpoints() + return _utils._filter_list(endpoints, id, filters) + + def get_endpoint(self, id, filters=None): + """Get exactly one Keystone endpoint. + + :param id: endpoint id. + :param filters: a dict containing additional filters to use. e.g. + {'region': 'region-a.geo-1'} + + :returns: a ``munch.Munch`` containing the endpoint description. + i.e. a ``munch.Munch`` containing the following attributes:: + - id: + - region: + - public_url: + - internal_url: (optional) + - admin_url: (optional) + """ + return _utils._get_entity(self, 'endpoint', id, filters) + + def delete_endpoint(self, id): + """Delete a Keystone endpoint. + + :param id: Id of the endpoint to delete. + + :returns: True if delete succeeded, False otherwise. + + :raises: ``OpenStackCloudException`` if something goes wrong during + the OpenStack API call. + """ + endpoint = self.get_endpoint(id=id) + if endpoint is None: + self.log.debug("Endpoint %s not found for deleting", id) + return False + + # Force admin interface if v2.0 is in use + v2 = self._is_client_version('identity', 2) + kwargs = {'endpoint_filter': {'interface': 'admin'}} if v2 else {} + + error_msg = "Failed to delete endpoint {id}".format(id=id) + self._identity_client.delete('/endpoints/{id}'.format(id=id), + error_message=error_msg, **kwargs) + + return True + + def create_domain(self, name, description=None, enabled=True): + """Create a domain. + + :param name: The name of the domain. + :param description: A description of the domain. + :param enabled: Is the domain enabled or not (default True). + + :returns: a ``munch.Munch`` containing the domain representation. + + :raise OpenStackCloudException: if the domain cannot be created. + """ + domain_ref = {'name': name, 'enabled': enabled} + if description is not None: + domain_ref['description'] = description + msg = 'Failed to create domain {name}'.format(name=name) + data = self._identity_client.post( + '/domains', json={'domain': domain_ref}, error_message=msg) + domain = self._get_and_munchify('domain', data) + return _utils.normalize_domains([domain])[0] + + def update_domain( + self, domain_id=None, name=None, description=None, + enabled=None, name_or_id=None): + if domain_id is None: + if name_or_id is None: + raise exc.OpenStackCloudException( + "You must pass either domain_id or name_or_id value" + ) + dom = self.get_domain(None, name_or_id) + if dom is None: + raise exc.OpenStackCloudException( + "Domain {0} not found for updating".format(name_or_id) + ) + domain_id = dom['id'] + + domain_ref = {} + domain_ref.update({'name': name} if name else {}) + domain_ref.update({'description': description} if description else {}) + domain_ref.update({'enabled': enabled} if enabled is not None else {}) + + error_msg = "Error in updating domain {id}".format(id=domain_id) + data = self._identity_client.patch( + '/domains/{id}'.format(id=domain_id), + json={'domain': domain_ref}, error_message=error_msg) + domain = self._get_and_munchify('domain', data) + return _utils.normalize_domains([domain])[0] + + def delete_domain(self, domain_id=None, name_or_id=None): + """Delete a domain. + + :param domain_id: ID of the domain to delete. + :param name_or_id: Name or ID of the domain to delete. + + :returns: True if delete succeeded, False otherwise. + + :raises: ``OpenStackCloudException`` if something goes wrong during + the OpenStack API call. + """ + if domain_id is None: + if name_or_id is None: + raise exc.OpenStackCloudException( + "You must pass either domain_id or name_or_id value" + ) + dom = self.get_domain(name_or_id=name_or_id) + if dom is None: + self.log.debug( + "Domain %s not found for deleting", name_or_id) + return False + domain_id = dom['id'] + + # A domain must be disabled before deleting + self.update_domain(domain_id, enabled=False) + error_msg = "Failed to delete domain {id}".format(id=domain_id) + self._identity_client.delete('/domains/{id}'.format(id=domain_id), + error_message=error_msg) + + return True + + def list_domains(self, **filters): + """List Keystone domains. + + :returns: a list of ``munch.Munch`` containing the domain description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + data = self._identity_client.get( + '/domains', params=filters, error_message="Failed to list domains") + domains = self._get_and_munchify('domains', data) + return _utils.normalize_domains(domains) + + def search_domains(self, filters=None, name_or_id=None): + """Search Keystone domains. + + :param name_or_id: domain name or id + :param dict filters: A dict containing additional filters to use. + Keys to search on are id, name, enabled and description. + + :returns: a list of ``munch.Munch`` containing the domain description. + Each ``munch.Munch`` contains the following attributes:: + - id: + - name: + - description: + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + if filters is None: + filters = {} + if name_or_id is not None: + domains = self.list_domains() + return _utils._filter_list(domains, name_or_id, filters) + else: + return self.list_domains(**filters) + + def get_domain(self, domain_id=None, name_or_id=None, filters=None): + """Get exactly one Keystone domain. + + :param domain_id: domain id. + :param name_or_id: domain name or id. + :param dict filters: A dict containing additional filters to use. + Keys to search on are id, name, enabled and description. + + :returns: a ``munch.Munch`` containing the domain description, or None + if not found. Each ``munch.Munch`` contains the following + attributes:: + - id: + - name: + - description: + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + if domain_id is None: + # NOTE(SamYaple): search_domains() has filters and name_or_id + # in the wrong positional order which prevents _get_entity from + # being able to return quickly if passing a domain object so we + # duplicate that logic here + if hasattr(name_or_id, 'id'): + return name_or_id + return _utils._get_entity(self, 'domain', filters, name_or_id) + else: + error_msg = 'Failed to get domain {id}'.format(id=domain_id) + data = self._identity_client.get( + '/domains/{id}'.format(id=domain_id), + error_message=error_msg) + domain = self._get_and_munchify('domain', data) + return _utils.normalize_domains([domain])[0] + + @_utils.valid_kwargs('domain_id') + @_utils.cache_on_arguments() + def list_groups(self, **kwargs): + """List Keystone Groups. + + :param domain_id: domain id. + + :returns: A list of ``munch.Munch`` containing the group description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + data = self._identity_client.get( + '/groups', params=kwargs, error_message="Failed to list groups") + return _utils.normalize_groups(self._get_and_munchify('groups', data)) + + @_utils.valid_kwargs('domain_id') + def search_groups(self, name_or_id=None, filters=None, **kwargs): + """Search Keystone groups. + + :param name: Group name or id. + :param filters: A dict containing additional filters to use. + :param domain_id: domain id. + + :returns: A list of ``munch.Munch`` containing the group description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + groups = self.list_groups(**kwargs) + return _utils._filter_list(groups, name_or_id, filters) + + @_utils.valid_kwargs('domain_id') + def get_group(self, name_or_id, filters=None, **kwargs): + """Get exactly one Keystone group. + + :param id: Group name or id. + :param filters: A dict containing additional filters to use. + :param domain_id: domain id. + + :returns: A ``munch.Munch`` containing the group description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + return _utils._get_entity(self, 'group', name_or_id, filters, **kwargs) + + def create_group(self, name, description, domain=None): + """Create a group. + + :param string name: Group name. + :param string description: Group description. + :param string domain: Domain name or ID for the group. + + :returns: A ``munch.Munch`` containing the group description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + group_ref = {'name': name} + if description: + group_ref['description'] = description + if domain: + dom = self.get_domain(domain) + if not dom: + raise exc.OpenStackCloudException( + "Creating group {group} failed: Invalid domain " + "{domain}".format(group=name, domain=domain) + ) + group_ref['domain_id'] = dom['id'] + + error_msg = "Error creating group {group}".format(group=name) + data = self._identity_client.post( + '/groups', json={'group': group_ref}, error_message=error_msg) + group = self._get_and_munchify('group', data) + self.list_groups.invalidate(self) + return _utils.normalize_groups([group])[0] + + @_utils.valid_kwargs('domain_id') + def update_group(self, name_or_id, name=None, description=None, + **kwargs): + """Update an existing group + + :param string name: New group name. + :param string description: New group description. + :param domain_id: domain id. + + :returns: A ``munch.Munch`` containing the group description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + self.list_groups.invalidate(self) + group = self.get_group(name_or_id, **kwargs) + if group is None: + raise exc.OpenStackCloudException( + "Group {0} not found for updating".format(name_or_id) + ) + + group_ref = {} + if name: + group_ref['name'] = name + if description: + group_ref['description'] = description + + error_msg = "Unable to update group {name}".format(name=name_or_id) + data = self._identity_client.patch( + '/groups/{id}'.format(id=group['id']), + json={'group': group_ref}, error_message=error_msg) + group = self._get_and_munchify('group', data) + self.list_groups.invalidate(self) + return _utils.normalize_groups([group])[0] + + @_utils.valid_kwargs('domain_id') + def delete_group(self, name_or_id, **kwargs): + """Delete a group + + :param name_or_id: ID or name of the group to delete. + :param domain_id: domain id. + + :returns: True if delete succeeded, False otherwise. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + group = self.get_group(name_or_id, **kwargs) + if group is None: + self.log.debug( + "Group %s not found for deleting", name_or_id) + return False + + error_msg = "Unable to delete group {name}".format(name=name_or_id) + self._identity_client.delete('/groups/{id}'.format(id=group['id']), + error_message=error_msg) + + self.list_groups.invalidate(self) + return True + + @_utils.valid_kwargs('domain_id') + def list_roles(self, **kwargs): + """List Keystone roles. + + :param domain_id: domain id for listing roles (v3) + + :returns: a list of ``munch.Munch`` containing the role description. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + v2 = self._is_client_version('identity', 2) + url = '/OS-KSADM/roles' if v2 else '/roles' + data = self._identity_client.get( + url, params=kwargs, error_message="Failed to list roles") + return self._normalize_roles(self._get_and_munchify('roles', data)) + + @_utils.valid_kwargs('domain_id') + def search_roles(self, name_or_id=None, filters=None, **kwargs): + """Seach Keystone roles. + + :param string name: role name or id. + :param dict filters: a dict containing additional filters to use. + :param domain_id: domain id (v3) + + :returns: a list of ``munch.Munch`` containing the role description. + Each ``munch.Munch`` contains the following attributes:: + + - id: + - name: + - description: + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + roles = self.list_roles(**kwargs) + return _utils._filter_list(roles, name_or_id, filters) + + @_utils.valid_kwargs('domain_id') + def get_role(self, name_or_id, filters=None, **kwargs): + """Get exactly one Keystone role. + + :param id: role name or id. + :param filters: a dict containing additional filters to use. + :param domain_id: domain id (v3) + + :returns: a single ``munch.Munch`` containing the role description. + Each ``munch.Munch`` contains the following attributes:: + + - id: + - name: + - description: + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + return _utils._get_entity(self, 'role', name_or_id, filters, **kwargs) + + def _keystone_v2_role_assignments(self, user, project=None, + role=None, **kwargs): + data = self._identity_client.get( + "/tenants/{tenant}/users/{user}/roles".format( + tenant=project, user=user), + error_message="Failed to list role assignments") + + roles = self._get_and_munchify('roles', data) + + ret = [] + for tmprole in roles: + if role is not None and role != tmprole.id: + continue + ret.append({ + 'role': { + 'id': tmprole.id + }, + 'scope': { + 'project': { + 'id': project, + } + }, + 'user': { + 'id': user, + } + }) + return ret + + def _keystone_v3_role_assignments(self, **filters): + # NOTE(samueldmq): different parameters have different representation + # patterns as query parameters in the call to the list role assignments + # API. The code below handles each set of patterns separately and + # renames the parameters names accordingly, ignoring 'effective', + # 'include_names' and 'include_subtree' whose do not need any renaming. + for k in ('group', 'role', 'user'): + if k in filters: + filters[k + '.id'] = filters[k] + del filters[k] + for k in ('project', 'domain'): + if k in filters: + filters['scope.' + k + '.id'] = filters[k] + del filters[k] + if 'os_inherit_extension_inherited_to' in filters: + filters['scope.OS-INHERIT:inherited_to'] = ( + filters['os_inherit_extension_inherited_to']) + del filters['os_inherit_extension_inherited_to'] + + data = self._identity_client.get( + '/role_assignments', params=filters, + error_message="Failed to list role assignments") + return self._get_and_munchify('role_assignments', data) + + def list_role_assignments(self, filters=None): + """List Keystone role assignments + + :param dict filters: Dict of filter conditions. Acceptable keys are: + + * 'user' (string) - User ID to be used as query filter. + * 'group' (string) - Group ID to be used as query filter. + * 'project' (string) - Project ID to be used as query filter. + * 'domain' (string) - Domain ID to be used as query filter. + * 'role' (string) - Role ID to be used as query filter. + * 'os_inherit_extension_inherited_to' (string) - Return inherited + role assignments for either 'projects' or 'domains' + * 'effective' (boolean) - Return effective role assignments. + * 'include_subtree' (boolean) - Include subtree + + 'user' and 'group' are mutually exclusive, as are 'domain' and + 'project'. + + NOTE: For keystone v2, only user, project, and role are used. + Project and user are both required in filters. + + :returns: a list of ``munch.Munch`` containing the role assignment + description. Contains the following attributes:: + + - id: + - user|group: + - project|domain: + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + # NOTE(samueldmq): although 'include_names' is a valid query parameter + # in the keystone v3 list role assignments API, it would have NO effect + # on shade due to normalization. It is not documented as an acceptable + # filter in the docs above per design! + + if not filters: + filters = {} + + # NOTE(samueldmq): the docs above say filters are *IDs*, though if + # munch.Munch objects are passed, this still works for backwards + # compatibility as keystoneclient allows either IDs or objects to be + # passed in. + # TODO(samueldmq): fix the docs above to advertise munch.Munch objects + # can be provided as parameters too + for k, v in filters.items(): + if isinstance(v, munch.Munch): + filters[k] = v['id'] + + if self._is_client_version('identity', 2): + if filters.get('project') is None or filters.get('user') is None: + raise exc.OpenStackCloudException( + "Must provide project and user for keystone v2" + ) + assignments = self._keystone_v2_role_assignments(**filters) + else: + assignments = self._keystone_v3_role_assignments(**filters) + + return _utils.normalize_role_assignments(assignments) + + @_utils.valid_kwargs('domain_id') + def create_role(self, name, **kwargs): + """Create a Keystone role. + + :param string name: The name of the role. + :param domain_id: domain id (v3) + + :returns: a ``munch.Munch`` containing the role description + + :raise OpenStackCloudException: if the role cannot be created + """ + v2 = self._is_client_version('identity', 2) + url = '/OS-KSADM/roles' if v2 else '/roles' + kwargs['name'] = name + msg = 'Failed to create role {name}'.format(name=name) + data = self._identity_client.post( + url, json={'role': kwargs}, error_message=msg) + role = self._get_and_munchify('role', data) + return self._normalize_role(role) + + @_utils.valid_kwargs('domain_id') + def update_role(self, name_or_id, name, **kwargs): + """Update a Keystone role. + + :param name_or_id: Name or id of the role to update + :param string name: The new role name + :param domain_id: domain id + + :returns: a ``munch.Munch`` containing the role description + + :raise OpenStackCloudException: if the role cannot be created + """ + if self._is_client_version('identity', 2): + raise exc.OpenStackCloudUnavailableFeature( + 'Unavailable Feature: Role update requires Identity v3' + ) + kwargs['name_or_id'] = name_or_id + role = self.get_role(**kwargs) + if role is None: + self.log.debug( + "Role %s not found for updating", name_or_id) + return False + msg = 'Failed to update role {name}'.format(name=name_or_id) + json_kwargs = {'role_id': role.id, 'role': {'name': name}} + data = self._identity_client.patch('/roles', error_message=msg, + json=json_kwargs) + role = self._get_and_munchify('role', data) + return self._normalize_role(role) + + @_utils.valid_kwargs('domain_id') + def delete_role(self, name_or_id, **kwargs): + """Delete a Keystone role. + + :param string id: Name or id of the role to delete. + :param domain_id: domain id (v3) + + :returns: True if delete succeeded, False otherwise. + + :raises: ``OpenStackCloudException`` if something goes wrong during + the OpenStack API call. + """ + role = self.get_role(name_or_id, **kwargs) + if role is None: + self.log.debug( + "Role %s not found for deleting", name_or_id) + return False + + v2 = self._is_client_version('identity', 2) + url = '{preffix}/{id}'.format( + preffix='/OS-KSADM/roles' if v2 else '/roles', id=role['id']) + error_msg = "Unable to delete role {name}".format(name=name_or_id) + self._identity_client.delete(url, error_message=error_msg) + + return True + + def _get_grant_revoke_params(self, role, user=None, group=None, + project=None, domain=None): + role = self.get_role(role) + if role is None: + return {} + data = {'role': role.id} + + # domain and group not available in keystone v2.0 + is_keystone_v2 = self._is_client_version('identity', 2) + + filters = {} + if not is_keystone_v2 and domain: + filters['domain_id'] = data['domain'] = \ + self.get_domain(domain)['id'] + + if user: + if domain: + data['user'] = self.get_user(user, + domain_id=filters['domain_id'], + filters=filters) + else: + data['user'] = self.get_user(user, filters=filters) + + if project: + # drop domain in favor of project + data.pop('domain', None) + data['project'] = self.get_project(project, filters=filters) + + if not is_keystone_v2 and group: + data['group'] = self.get_group(group, filters=filters) + + return data + + def grant_role(self, name_or_id, user=None, group=None, + project=None, domain=None, wait=False, timeout=60): + """Grant a role to a user. + + :param string name_or_id: The name or id of the role. + :param string user: The name or id of the user. + :param string group: The name or id of the group. (v3) + :param string project: The name or id of the project. + :param string domain: The id of the domain. (v3) + :param bool wait: Wait for role to be granted + :param int timeout: Timeout to wait for role to be granted + + NOTE: domain is a required argument when the grant is on a project, + user or group specified by name. In that situation, they are all + considered to be in that domain. If different domains are in use + in the same role grant, it is required to specify those by ID. + + NOTE: for wait and timeout, sometimes granting roles is not + instantaneous. + + NOTE: project is required for keystone v2 + + :returns: True if the role is assigned, otherwise False + + :raise OpenStackCloudException: if the role cannot be granted + """ + data = self._get_grant_revoke_params(name_or_id, user, group, + project, domain) + filters = data.copy() + if not data: + raise exc.OpenStackCloudException( + 'Role {0} not found.'.format(name_or_id)) + + if data.get('user') is not None and data.get('group') is not None: + raise exc.OpenStackCloudException( + 'Specify either a group or a user, not both') + if data.get('user') is None and data.get('group') is None: + raise exc.OpenStackCloudException( + 'Must specify either a user or a group') + if self._is_client_version('identity', 2) and \ + data.get('project') is None: + raise exc.OpenStackCloudException( + 'Must specify project for keystone v2') + + if self.list_role_assignments(filters=filters): + self.log.debug('Assignment already exists') + return False + + error_msg = "Error granting access to role: {0}".format(data) + if self._is_client_version('identity', 2): + # For v2.0, only tenant/project assignment is supported + url = "/tenants/{t}/users/{u}/roles/OS-KSADM/{r}".format( + t=data['project']['id'], u=data['user']['id'], r=data['role']) + + self._identity_client.put(url, error_message=error_msg, + endpoint_filter={'interface': 'admin'}) + else: + if data.get('project') is None and data.get('domain') is None: + raise exc.OpenStackCloudException( + 'Must specify either a domain or project') + + # For v3, figure out the assignment type and build the URL + if data.get('domain'): + url = "/domains/{}".format(data['domain']) + else: + url = "/projects/{}".format(data['project']['id']) + if data.get('group'): + url += "/groups/{}".format(data['group']['id']) + else: + url += "/users/{}".format(data['user']['id']) + url += "/roles/{}".format(data.get('role')) + + self._identity_client.put(url, error_message=error_msg) + + if wait: + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for role to be granted"): + if self.list_role_assignments(filters=filters): + break + return True + + def revoke_role(self, name_or_id, user=None, group=None, + project=None, domain=None, wait=False, timeout=60): + """Revoke a role from a user. + + :param string name_or_id: The name or id of the role. + :param string user: The name or id of the user. + :param string group: The name or id of the group. (v3) + :param string project: The name or id of the project. + :param string domain: The id of the domain. (v3) + :param bool wait: Wait for role to be revoked + :param int timeout: Timeout to wait for role to be revoked + + NOTE: for wait and timeout, sometimes revoking roles is not + instantaneous. + + NOTE: project is required for keystone v2 + + :returns: True if the role is revoke, otherwise False + + :raise OpenStackCloudException: if the role cannot be removed + """ + data = self._get_grant_revoke_params(name_or_id, user, group, + project, domain) + filters = data.copy() + + if not data: + raise exc.OpenStackCloudException( + 'Role {0} not found.'.format(name_or_id)) + + if data.get('user') is not None and data.get('group') is not None: + raise exc.OpenStackCloudException( + 'Specify either a group or a user, not both') + if data.get('user') is None and data.get('group') is None: + raise exc.OpenStackCloudException( + 'Must specify either a user or a group') + if self._is_client_version('identity', 2) and \ + data.get('project') is None: + raise exc.OpenStackCloudException( + 'Must specify project for keystone v2') + + if not self.list_role_assignments(filters=filters): + self.log.debug('Assignment does not exist') + return False + + error_msg = "Error revoking access to role: {0}".format(data) + if self._is_client_version('identity', 2): + # For v2.0, only tenant/project assignment is supported + url = "/tenants/{t}/users/{u}/roles/OS-KSADM/{r}".format( + t=data['project']['id'], u=data['user']['id'], r=data['role']) + + self._identity_client.delete( + url, error_message=error_msg, + endpoint_filter={'interface': 'admin'}) + else: + if data.get('project') is None and data.get('domain') is None: + raise exc.OpenStackCloudException( + 'Must specify either a domain or project') + + # For v3, figure out the assignment type and build the URL + if data.get('domain'): + url = "/domains/{}".format(data['domain']) + else: + url = "/projects/{}".format(data['project']['id']) + if data.get('group'): + url += "/groups/{}".format(data['group']['id']) + else: + url += "/users/{}".format(data['user']['id']) + url += "/roles/{}".format(data.get('role')) + + self._identity_client.delete(url, error_message=error_msg) + + if wait: + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for role to be revoked"): + if not self.list_role_assignments(filters=filters): + break + return True + + def _get_project_id_param_dict(self, name_or_id): + if name_or_id: + project = self.get_project(name_or_id) + if not project: + return {} + if self._is_client_version('identity', 3): + return {'default_project_id': project['id']} + else: + return {'tenant_id': project['id']} + else: + return {} + + def _get_domain_id_param_dict(self, domain_id): + """Get a useable domain.""" + + # Keystone v3 requires domains for user and project creation. v2 does + # not. However, keystone v2 does not allow user creation by non-admin + # users, so we can throw an error to the user that does not need to + # mention api versions + if self._is_client_version('identity', 3): + if not domain_id: + raise exc.OpenStackCloudException( + "User or project creation requires an explicit" + " domain_id argument.") + else: + return {'domain_id': domain_id} + else: + return {} + + def _get_identity_params(self, domain_id=None, project=None): + """Get the domain and project/tenant parameters if needed. + + keystone v2 and v3 are divergent enough that we need to pass or not + pass project or tenant_id or domain or nothing in a sane manner. + """ + ret = {} + ret.update(self._get_domain_id_param_dict(domain_id)) + ret.update(self._get_project_id_param_dict(project)) + return ret diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py new file mode 100644 index 000000000..7222a2e82 --- /dev/null +++ b/openstack/cloud/_image.py @@ -0,0 +1,432 @@ +# 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 types so that we can reference ListType in sphinx param declarations. +# We can't just use list, because sphinx gets confused by +# openstack.resource.Resource.list and openstack.resource2.Resource.list +import types # noqa + +import keystoneauth1.exceptions + +from openstack.cloud import exc +from openstack.cloud import meta +from openstack.cloud import _normalize +from openstack.cloud import _utils +from openstack import proxy +from openstack import utils + + +def _no_pending_images(images): + """If there are any images not in a steady state, don't cache""" + for image in images: + if image.status not in ('active', 'deleted', 'killed'): + return False + return True + + +class ImageCloudMixin(_normalize.Normalizer): + + def __init__(self): + self.image_api_use_tasks = self.config.config['image_api_use_tasks'] + + @property + def _raw_image_client(self): + if 'raw-image' not in self._raw_clients: + image_client = self._get_raw_client('image') + self._raw_clients['raw-image'] = image_client + return self._raw_clients['raw-image'] + + @property + def _image_client(self): + if 'image' not in self._raw_clients: + self._raw_clients['image'] = self._get_versioned_client( + 'image', min_version=1, max_version='2.latest') + return self._raw_clients['image'] + + def search_images(self, name_or_id=None, filters=None): + images = self.list_images() + return _utils._filter_list(images, name_or_id, filters) + + @_utils.cache_on_arguments(should_cache_fn=_no_pending_images) + def list_images(self, filter_deleted=True, show_all=False): + """Get available images. + + :param filter_deleted: Control whether deleted images are returned. + :param show_all: Show all images, including images that are shared + but not accepted. (By default in glance v2 shared image that + have not been accepted are not shown) show_all will override the + value of filter_deleted to False. + :returns: A list of glance images. + """ + if show_all: + filter_deleted = False + # First, try to actually get images from glance, it's more efficient + images = [] + params = {} + image_list = [] + try: + if self._is_client_version('image', 2): + endpoint = '/images' + if show_all: + params['member_status'] = 'all' + else: + endpoint = '/images/detail' + + response = self._image_client.get(endpoint, params=params) + + except keystoneauth1.exceptions.catalog.EndpointNotFound: + # We didn't have glance, let's try nova + # If this doesn't work - we just let the exception propagate + response = proxy._json_response( + self.compute.get('/images/detail')) + while 'next' in response: + image_list.extend(meta.obj_list_to_munch(response['images'])) + endpoint = response['next'] + # next links from glance have the version prefix. If the catalog + # has a versioned endpoint, then we can't append the next link to + # it. Strip the absolute prefix (/v1/ or /v2/ to turn it into + # a proper relative link. + if endpoint.startswith('/v'): + endpoint = endpoint[4:] + response = self._image_client.get(endpoint) + if 'images' in response: + image_list.extend(meta.obj_list_to_munch(response['images'])) + else: + image_list.extend(response) + + for image in image_list: + # The cloud might return DELETED for invalid images. + # While that's cute and all, that's an implementation detail. + if not filter_deleted: + images.append(image) + elif image.status.lower() != 'deleted': + images.append(image) + return self._normalize_images(images) + + def get_image(self, name_or_id, filters=None): + """Get an image by name or ID. + + :param name_or_id: Name or ID of the image. + :param filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: An image ``munch.Munch`` or None if no matching image + is found + + """ + return _utils._get_entity(self, 'image', name_or_id, filters) + + def get_image_by_id(self, id): + """ Get a image by ID + + :param id: ID of the image. + :returns: An image ``munch.Munch``. + """ + data = self._image_client.get( + '/images/{id}'.format(id=id), + error_message="Error getting image with ID {id}".format(id=id) + ) + key = 'image' if 'image' in data else None + image = self._normalize_image( + self._get_and_munchify(key, data)) + + return image + + def download_image( + self, name_or_id, output_path=None, output_file=None, + chunk_size=1024): + """Download an image by name or ID + + :param str name_or_id: Name or ID of the image. + :param output_path: the output path to write the image to. Either this + or output_file must be specified + :param output_file: a file object (or file-like object) to write the + image data to. Only write() will be called on this object. Either + this or output_path must be specified + :param int chunk_size: size in bytes to read from the wire and buffer + at one time. Defaults to 1024 + + :raises: OpenStackCloudException in the event download_image is called + without exactly one of either output_path or output_file + :raises: OpenStackCloudResourceNotFound if no images are found matching + the name or ID provided + """ + if output_path is None and output_file is None: + raise exc.OpenStackCloudException( + 'No output specified, an output path or file object' + ' is necessary to write the image data to') + elif output_path is not None and output_file is not None: + raise exc.OpenStackCloudException( + 'Both an output path and file object were provided,' + ' however only one can be used at once') + + image = self.search_images(name_or_id) + if len(image) == 0: + raise exc.OpenStackCloudResourceNotFound( + "No images with name or ID %s were found" % name_or_id, None) + if self._is_client_version('image', 2): + endpoint = '/images/{id}/file'.format(id=image[0]['id']) + else: + endpoint = '/images/{id}'.format(id=image[0]['id']) + + response = self._image_client.get(endpoint, stream=True) + + with _utils.shade_exceptions("Unable to download image"): + if output_path: + with open(output_path, 'wb') as fd: + for chunk in response.iter_content(chunk_size=chunk_size): + fd.write(chunk) + return + elif output_file: + for chunk in response.iter_content(chunk_size=chunk_size): + output_file.write(chunk) + return + + def get_image_exclude(self, name_or_id, exclude): + for image in self.search_images(name_or_id): + if exclude: + if exclude not in image.name: + return image + else: + return image + return None + + def get_image_name(self, image_id, exclude=None): + image = self.get_image_exclude(image_id, exclude) + if image: + return image.name + return None + + def get_image_id(self, image_name, exclude=None): + image = self.get_image_exclude(image_name, exclude) + if image: + return image.id + return None + + def wait_for_image(self, image, timeout=3600): + image_id = image['id'] + for count in utils.iterate_timeout( + timeout, "Timeout waiting for image to snapshot"): + self.list_images.invalidate(self) + image = self.get_image(image_id) + if not image: + continue + if image['status'] == 'active': + return image + elif image['status'] == 'error': + raise exc.OpenStackCloudException( + 'Image {image} hit error state'.format(image=image_id)) + + def delete_image( + self, name_or_id, wait=False, timeout=3600, delete_objects=True): + """Delete an existing image. + + :param name_or_id: Name of the image to be deleted. + :param wait: If True, waits for image to be deleted. + :param timeout: Seconds to wait for image deletion. None is forever. + :param delete_objects: If True, also deletes uploaded swift objects. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException if there are problems deleting. + """ + image = self.get_image(name_or_id) + if not image: + return False + self._image_client.delete( + '/images/{id}'.format(id=image.id), + error_message="Error in deleting image") + self.list_images.invalidate(self) + + # Task API means an image was uploaded to swift + if self.image_api_use_tasks and ( + self._IMAGE_OBJECT_KEY in image + or self._SHADE_IMAGE_OBJECT_KEY in image): + (container, objname) = image.get( + self._IMAGE_OBJECT_KEY, image.get( + self._SHADE_IMAGE_OBJECT_KEY)).split('/', 1) + self.delete_object(container=container, name=objname) + + if wait: + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for the image to be deleted."): + self._get_cache(None).invalidate() + if self.get_image(image.id) is None: + break + return True + + def create_image( + self, name, filename=None, + container=None, + md5=None, sha256=None, + disk_format=None, container_format=None, + disable_vendor_agent=True, + wait=False, timeout=3600, + allow_duplicates=False, meta=None, volume=None, **kwargs): + """Upload an image. + + :param str name: Name of the image to create. If it is a pathname + of an image, the name will be constructed from the + extensionless basename of the path. + :param str filename: The path to the file to upload, if needed. + (optional, defaults to None) + :param str container: Name of the container in swift where images + should be uploaded for import if the cloud + requires such a thing. (optiona, defaults to + 'images') + :param str md5: md5 sum of the image file. If not given, an md5 will + be calculated. + :param str sha256: sha256 sum of the image file. If not given, an md5 + will be calculated. + :param str disk_format: The disk format the image is in. (optional, + defaults to the os-client-config config value + for this cloud) + :param str container_format: The container format the image is in. + (optional, defaults to the + os-client-config config value for this + cloud) + :param bool disable_vendor_agent: Whether or not to append metadata + flags to the image to inform the + cloud in question to not expect a + vendor agent to be runing. + (optional, defaults to True) + :param bool wait: If true, waits for image to be created. Defaults to + true - however, be aware that one of the upload + methods is always synchronous. + :param timeout: Seconds to wait for image creation. None is forever. + :param allow_duplicates: If true, skips checks that enforce unique + image name. (optional, defaults to False) + :param meta: A dict of key/value pairs to use for metadata that + bypasses automatic type conversion. + :param volume: Name or ID or volume object of a volume to create an + image from. Mutually exclusive with (optional, defaults + to None) + + Additional kwargs will be passed to the image creation as additional + metadata for the image and will have all values converted to string + except for min_disk, min_ram, size and virtual_size which will be + converted to int. + + If you are sure you have all of your data types correct or have an + advanced need to be explicit, use meta. If you are just a normal + consumer, using kwargs is likely the right choice. + + If a value is in meta and kwargs, meta wins. + + :returns: A ``munch.Munch`` of the Image object + + :raises: OpenStackCloudException if there are problems uploading + """ + if volume: + image = self.block_storage.create_image( + name=name, volume=volume, + allow_duplicates=allow_duplicates, + container_format=container_format, disk_format=disk_format, + wait=wait, timeout=timeout) + else: + image = self.image.create_image( + name, filename=filename, + container=container, + md5=sha256, sha256=sha256, + disk_format=disk_format, container_format=container_format, + disable_vendor_agent=disable_vendor_agent, + wait=wait, timeout=timeout, + allow_duplicates=allow_duplicates, meta=meta, **kwargs) + + self._get_cache(None).invalidate() + if not wait: + return image + try: + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for the image to finish."): + image_obj = self.get_image(image.id) + if image_obj and image_obj.status not in ('queued', 'saving'): + return image_obj + except exc.OpenStackCloudTimeout: + self.log.debug( + "Timeout waiting for image to become ready. Deleting.") + self.delete_image(image.id, wait=True) + raise + + def update_image_properties( + self, image=None, name_or_id=None, meta=None, **properties): + image = image or name_or_id + return self.image.update_image_properties( + image=image, meta=meta, **properties) + + def set_volume_quotas(self, name_or_id, **kwargs): + """ Set a volume quota in a project + + :param name_or_id: project name or id + :param kwargs: key/value pairs of quota name and quota value + + :raises: OpenStackCloudException if the resource to set the + quota does not exist. + """ + + proj = self.get_project(name_or_id) + if not proj: + raise exc.OpenStackCloudException("project does not exist") + + kwargs['tenant_id'] = proj.id + self._volume_client.put( + '/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id), + json={'quota_set': kwargs}, + error_message="No valid quota or resource") + + def get_volume_quotas(self, name_or_id): + """ Get volume quotas for a project + + :param name_or_id: project name or id + :raises: OpenStackCloudException if it's not a valid project + + :returns: Munch object with the quotas + """ + proj = self.get_project(name_or_id) + if not proj: + raise exc.OpenStackCloudException("project does not exist") + + data = self._volume_client.get( + '/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id), + error_message="cinder client call failed") + return self._get_and_munchify('quota_set', data) + + def delete_volume_quotas(self, name_or_id): + """ Delete volume quotas for a project + + :param name_or_id: project name or id + :raises: OpenStackCloudException if it's not a valid project or the + cinder client call failed + + :returns: dict with the quotas + """ + proj = self.get_project(name_or_id) + if not proj: + raise exc.OpenStackCloudException("project does not exist") + + return self._volume_client.delete( + '/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id), + error_message="cinder client call failed") diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py new file mode 100644 index 000000000..b6b3b05ba --- /dev/null +++ b/openstack/cloud/_network.py @@ -0,0 +1,2566 @@ +# 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 types so that we can reference ListType in sphinx param declarations. +# We can't just use list, because sphinx gets confused by +# openstack.resource.Resource.list and openstack.resource2.Resource.list +import six +import time +import threading +import types # noqa + +from openstack.cloud import exc +from openstack.cloud import _normalize +from openstack.cloud import _utils +from openstack import exceptions +from openstack import proxy + + +class NetworkCloudMixin(_normalize.Normalizer): + + def __init__(self): + self._ports = None + self._ports_time = 0 + self._ports_lock = threading.Lock() + + @_utils.cache_on_arguments() + def _neutron_extensions(self): + extensions = set() + resp = self.network.get('/extensions.json') + data = proxy._json_response( + resp, + error_message="Error fetching extension list for neutron") + for extension in self._get_and_munchify('extensions', data): + extensions.add(extension['alias']) + return extensions + + def _has_neutron_extension(self, extension_alias): + return extension_alias in self._neutron_extensions() + + def search_networks(self, name_or_id=None, filters=None): + """Search networks + + :param name_or_id: Name or ID of the desired network. + :param filters: a dict containing additional filters to use. e.g. + {'router:external': True} + + :returns: a list of ``munch.Munch`` containing the network description. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + OpenStack API call. + """ + networks = self.list_networks( + filters if isinstance(filters, dict) else None) + return _utils._filter_list(networks, name_or_id, filters) + + def search_routers(self, name_or_id=None, filters=None): + """Search routers + + :param name_or_id: Name or ID of the desired router. + :param filters: a dict containing additional filters to use. e.g. + {'admin_state_up': True} + + :returns: a list of ``munch.Munch`` containing the router description. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + OpenStack API call. + """ + routers = self.list_routers( + filters if isinstance(filters, dict) else None) + return _utils._filter_list(routers, name_or_id, filters) + + def search_subnets(self, name_or_id=None, filters=None): + """Search subnets + + :param name_or_id: Name or ID of the desired subnet. + :param filters: a dict containing additional filters to use. e.g. + {'enable_dhcp': True} + + :returns: a list of ``munch.Munch`` containing the subnet description. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + OpenStack API call. + """ + subnets = self.list_subnets( + filters if isinstance(filters, dict) else None) + return _utils._filter_list(subnets, name_or_id, filters) + + def search_ports(self, name_or_id=None, filters=None): + """Search ports + + :param name_or_id: Name or ID of the desired port. + :param filters: a dict containing additional filters to use. e.g. + {'device_id': '2711c67a-b4a7-43dd-ace7-6187b791c3f0'} + + :returns: a list of ``munch.Munch`` containing the port description. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + OpenStack API call. + """ + # If port caching is enabled, do not push the filter down to + # neutron; get all the ports (potentially from the cache) and + # filter locally. + if self._PORT_AGE or isinstance(filters, str): + pushdown_filters = None + else: + pushdown_filters = filters + ports = self.list_ports(pushdown_filters) + return _utils._filter_list(ports, name_or_id, filters) + + def list_networks(self, filters=None): + """List all available networks. + + :param filters: (optional) dict of filter conditions to push down + :returns: A list of ``munch.Munch`` containing network info. + + """ + # Translate None from search interface to empty {} for kwargs below + if not filters: + filters = {} + data = self.network.get("/networks.json", params=filters) + return self._get_and_munchify('networks', data) + + def list_routers(self, filters=None): + """List all available routers. + + :param filters: (optional) dict of filter conditions to push down + :returns: A list of router ``munch.Munch``. + + """ + # Translate None from search interface to empty {} for kwargs below + if not filters: + filters = {} + resp = self.network.get("/routers.json", params=filters) + data = proxy._json_response( + resp, + error_message="Error fetching router list") + return self._get_and_munchify('routers', data) + + def list_subnets(self, filters=None): + """List all available subnets. + + :param filters: (optional) dict of filter conditions to push down + :returns: A list of subnet ``munch.Munch``. + + """ + # Translate None from search interface to empty {} for kwargs below + if not filters: + filters = {} + data = self.network.get("/subnets.json", params=filters) + return self._get_and_munchify('subnets', data) + + def list_ports(self, filters=None): + """List all available ports. + + :param filters: (optional) dict of filter conditions to push down + :returns: A list of port ``munch.Munch``. + + """ + # If pushdown filters are specified and we do not have batched caching + # enabled, bypass local caching and push down the filters. + if filters and self._PORT_AGE == 0: + return self._list_ports(filters) + + if (time.time() - self._ports_time) >= self._PORT_AGE: + # Since we're using cached data anyway, we don't need to + # have more than one thread actually submit the list + # ports task. Let the first one submit it while holding + # a lock, and the non-blocking acquire method will cause + # subsequent threads to just skip this and use the old + # data until it succeeds. + # Initially when we never got data, block to retrieve some data. + first_run = self._ports is None + if self._ports_lock.acquire(first_run): + try: + if not (first_run and self._ports is not None): + self._ports = self._list_ports({}) + self._ports_time = time.time() + finally: + self._ports_lock.release() + # Wrap the return with filter_list so that if filters were passed + # but we were batching/caching and thus always fetching the whole + # list from the cloud, we still return a filtered list. + return _utils._filter_list(self._ports, None, filters or {}) + + def _list_ports(self, filters): + resp = self.network.get("/ports.json", params=filters) + data = proxy._json_response( + resp, + error_message="Error fetching port list") + return self._get_and_munchify('ports', data) + + def get_qos_policy(self, name_or_id, filters=None): + """Get a QoS policy by name or ID. + + :param name_or_id: Name or ID of the policy. + :param filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: A policy ``munch.Munch`` or None if no matching network is + found. + + """ + return _utils._get_entity( + self, 'qos_policie', name_or_id, filters) + + def search_qos_policies(self, name_or_id=None, filters=None): + """Search QoS policies + + :param name_or_id: Name or ID of the desired policy. + :param filters: a dict containing additional filters to use. e.g. + {'shared': True} + + :returns: a list of ``munch.Munch`` containing the network description. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + OpenStack API call. + """ + policies = self.list_qos_policies(filters) + return _utils._filter_list(policies, name_or_id, filters) + + def list_qos_rule_types(self, filters=None): + """List all available QoS rule types. + + :param filters: (optional) dict of filter conditions to push down + :returns: A list of rule types ``munch.Munch``. + + """ + if not self._has_neutron_extension('qos'): + raise exc.OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + # Translate None from search interface to empty {} for kwargs below + if not filters: + filters = {} + resp = self.network.get("/qos/rule-types.json", params=filters) + data = proxy._json_response( + resp, + error_message="Error fetching QoS rule types list") + return self._get_and_munchify('rule_types', data) + + def get_qos_rule_type_details(self, rule_type, filters=None): + """Get a QoS rule type details by rule type name. + + :param string rule_type: Name of the QoS rule type. + + :returns: A rule type details ``munch.Munch`` or None if + no matching rule type is found. + + """ + if not self._has_neutron_extension('qos'): + raise exc.OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + if not self._has_neutron_extension('qos-rule-type-details'): + raise exc.OpenStackCloudUnavailableExtension( + 'qos-rule-type-details extension is not available ' + 'on target cloud') + + resp = self.network.get( + "/qos/rule-types/{rule_type}.json".format(rule_type=rule_type)) + data = proxy._json_response( + resp, + error_message="Error fetching QoS details of {rule_type} " + "rule type".format(rule_type=rule_type)) + return self._get_and_munchify('rule_type', data) + + def list_qos_policies(self, filters=None): + """List all available QoS policies. + + :param filters: (optional) dict of filter conditions to push down + :returns: A list of policies ``munch.Munch``. + + """ + if not self._has_neutron_extension('qos'): + raise exc.OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + # Translate None from search interface to empty {} for kwargs below + if not filters: + filters = {} + resp = self.network.get("/qos/policies.json", params=filters) + data = proxy._json_response( + resp, + error_message="Error fetching QoS policies list") + return self._get_and_munchify('policies', data) + + def get_network(self, name_or_id, filters=None): + """Get a network by name or ID. + + :param name_or_id: Name or ID of the network. + :param filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: A network ``munch.Munch`` or None if no matching network is + found. + + """ + return _utils._get_entity(self, 'network', name_or_id, filters) + + def get_network_by_id(self, id): + """ Get a network by ID + + :param id: ID of the network. + :returns: A network ``munch.Munch``. + """ + resp = self.network.get('/networks/{id}'.format(id=id)) + data = proxy._json_response( + resp, + error_message="Error getting network with ID {id}".format(id=id) + ) + network = self._get_and_munchify('network', data) + + return network + + def get_router(self, name_or_id, filters=None): + """Get a router by name or ID. + + :param name_or_id: Name or ID of the router. + :param filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: A router ``munch.Munch`` or None if no matching router is + found. + + """ + return _utils._get_entity(self, 'router', name_or_id, filters) + + def get_subnet(self, name_or_id, filters=None): + """Get a subnet by name or ID. + + :param name_or_id: Name or ID of the subnet. + :param filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + :returns: A subnet ``munch.Munch`` or None if no matching subnet is + found. + + """ + return _utils._get_entity(self, 'subnet', name_or_id, filters) + + def get_subnet_by_id(self, id): + """ Get a subnet by ID + + :param id: ID of the subnet. + :returns: A subnet ``munch.Munch``. + """ + resp = self.network.get('/subnets/{id}'.format(id=id)) + data = proxy._json_response( + resp, + error_message="Error getting subnet with ID {id}".format(id=id) + ) + subnet = self._get_and_munchify('subnet', data) + + return subnet + + def get_port(self, name_or_id, filters=None): + """Get a port by name or ID. + + :param name_or_id: Name or ID of the port. + :param filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: A port ``munch.Munch`` or None if no matching port is found. + + """ + return _utils._get_entity(self, 'port', name_or_id, filters) + + def get_port_by_id(self, id): + """ Get a port by ID + + :param id: ID of the port. + :returns: A port ``munch.Munch``. + """ + resp = self.network.get('/ports/{id}'.format(id=id)) + data = proxy._json_response( + resp, + error_message="Error getting port with ID {id}".format(id=id) + ) + port = self._get_and_munchify('port', data) + + return port + + def create_network(self, name, shared=False, admin_state_up=True, + external=False, provider=None, project_id=None, + availability_zone_hints=None, + port_security_enabled=None, + mtu_size=None): + """Create a network. + + :param string name: Name of the network being created. + :param bool shared: Set the network as shared. + :param bool admin_state_up: Set the network administrative state to up. + :param bool external: Whether this network is externally accessible. + :param dict provider: A dict of network provider options. Example:: + + { 'network_type': 'vlan', 'segmentation_id': 'vlan1' } + :param string project_id: Specify the project ID this network + will be created on (admin-only). + :param types.ListType availability_zone_hints: A list of availability + zone hints. + :param bool port_security_enabled: Enable / Disable port security + :param int mtu_size: maximum transmission unit value to address + fragmentation. Minimum value is 68 for IPv4, and 1280 for IPv6. + + :returns: The network object. + :raises: OpenStackCloudException on operation error. + """ + network = { + 'name': name, + 'admin_state_up': admin_state_up, + } + + if shared: + network['shared'] = shared + + if project_id is not None: + network['tenant_id'] = project_id + + if availability_zone_hints is not None: + if not isinstance(availability_zone_hints, list): + raise exc.OpenStackCloudException( + "Parameter 'availability_zone_hints' must be a list") + if not self._has_neutron_extension('network_availability_zone'): + raise exc.OpenStackCloudUnavailableExtension( + 'network_availability_zone extension is not available on ' + 'target cloud') + network['availability_zone_hints'] = availability_zone_hints + + if provider: + if not isinstance(provider, dict): + raise exc.OpenStackCloudException( + "Parameter 'provider' must be a dict") + # Only pass what we know + for attr in ('physical_network', 'network_type', + 'segmentation_id'): + if attr in provider: + arg = "provider:" + attr + network[arg] = provider[attr] + + # Do not send 'router:external' unless it is explicitly + # set since sending it *might* cause "Forbidden" errors in + # some situations. It defaults to False in the client, anyway. + if external: + network['router:external'] = True + + if port_security_enabled is not None: + if not isinstance(port_security_enabled, bool): + raise exc.OpenStackCloudException( + "Parameter 'port_security_enabled' must be a bool") + network['port_security_enabled'] = port_security_enabled + + if mtu_size: + if not isinstance(mtu_size, int): + raise exc.OpenStackCloudException( + "Parameter 'mtu_size' must be an integer.") + if not mtu_size >= 68: + raise exc.OpenStackCloudException( + "Parameter 'mtu_size' must be greater than 67.") + + network['mtu'] = mtu_size + + data = self.network.post("/networks.json", json={'network': network}) + + # Reset cache so the new network is picked up + self._reset_network_caches() + return self._get_and_munchify('network', data) + + @_utils.valid_kwargs("name", "shared", "admin_state_up", "external", + "provider", "mtu_size", "port_security_enabled") + def update_network(self, name_or_id, **kwargs): + """Update a network. + + :param string name_or_id: Name or ID of the network being updated. + :param string name: New name of the network. + :param bool shared: Set the network as shared. + :param bool admin_state_up: Set the network administrative state to up. + :param bool external: Whether this network is externally accessible. + :param dict provider: A dict of network provider options. Example:: + + { 'network_type': 'vlan', 'segmentation_id': 'vlan1' } + :param int mtu_size: New maximum transmission unit value to address + fragmentation. Minimum value is 68 for IPv4, and 1280 for IPv6. + :param bool port_security_enabled: Enable or disable port security. + + :returns: The updated network object. + :raises: OpenStackCloudException on operation error. + """ + if 'provider' in kwargs: + if not isinstance(kwargs['provider'], dict): + raise exc.OpenStackCloudException( + "Parameter 'provider' must be a dict") + # Only pass what we know + provider = {} + for key in kwargs['provider']: + if key in ('physical_network', 'network_type', + 'segmentation_id'): + provider['provider:' + key] = kwargs['provider'][key] + kwargs['provider'] = provider + + if 'external' in kwargs: + kwargs['router:external'] = kwargs.pop('external') + + if 'port_security_enabled' in kwargs: + if not isinstance(kwargs['port_security_enabled'], bool): + raise exc.OpenStackCloudException( + "Parameter 'port_security_enabled' must be a bool") + + if 'mtu_size' in kwargs: + if not isinstance(kwargs['mtu_size'], int): + raise exc.OpenStackCloudException( + "Parameter 'mtu_size' must be an integer.") + if kwargs['mtu_size'] < 68: + raise exc.OpenStackCloudException( + "Parameter 'mtu_size' must be greater than 67.") + kwargs['mtu'] = kwargs.pop('mtu_size') + + network = self.get_network(name_or_id) + if not network: + raise exc.OpenStackCloudException( + "Network %s not found." % name_or_id) + + data = proxy._json_response(self.network.put( + "/networks/{net_id}.json".format(net_id=network.id), + json={"network": kwargs}), + error_message="Error updating network {0}".format(name_or_id)) + + self._reset_network_caches() + + return self._get_and_munchify('network', data) + + def delete_network(self, name_or_id): + """Delete a network. + + :param name_or_id: Name or ID of the network being deleted. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException on operation error. + """ + network = self.get_network(name_or_id) + if not network: + self.log.debug("Network %s not found for deleting", name_or_id) + return False + + exceptions.raise_from_response(self.network.delete( + "/networks/{network_id}.json".format(network_id=network['id']))) + + # Reset cache so the deleted network is removed + self._reset_network_caches() + + return True + + def set_network_quotas(self, name_or_id, **kwargs): + """ Set a network quota in a project + + :param name_or_id: project name or id + :param kwargs: key/value pairs of quota name and quota value + + :raises: OpenStackCloudException if the resource to set the + quota does not exist. + """ + + proj = self.get_project(name_or_id) + if not proj: + raise exc.OpenStackCloudException("project does not exist") + + exceptions.raise_from_response( + self.network.put( + '/quotas/{project_id}.json'.format(project_id=proj.id), + json={'quota': kwargs}), + error_message=("Error setting Neutron's quota for " + "project {0}".format(proj.id))) + + def get_network_quotas(self, name_or_id, details=False): + """ Get network quotas for a project + + :param name_or_id: project name or id + :param details: if set to True it will return details about usage + of quotas by given project + :raises: OpenStackCloudException if it's not a valid project + + :returns: Munch object with the quotas + """ + proj = self.get_project(name_or_id) + if not proj: + raise exc.OpenStackCloudException("project does not exist") + url = '/quotas/{project_id}'.format(project_id=proj.id) + if details: + url = url + "/details" + url = url + ".json" + data = proxy._json_response( + self.network.get(url), + error_message=("Error fetching Neutron's quota for " + "project {0}".format(proj.id))) + return self._get_and_munchify('quota', data) + + def get_network_extensions(self): + """Get Cloud provided network extensions + + :returns: set of Neutron extension aliases + """ + return self._neutron_extensions() + + def delete_network_quotas(self, name_or_id): + """ Delete network quotas for a project + + :param name_or_id: project name or id + :raises: OpenStackCloudException if it's not a valid project or the + network client call failed + + :returns: dict with the quotas + """ + proj = self.get_project(name_or_id) + if not proj: + raise exc.OpenStackCloudException("project does not exist") + exceptions.raise_from_response( + self.network.delete( + '/quotas/{project_id}.json'.format(project_id=proj.id)), + error_message=("Error deleting Neutron's quota for " + "project {0}".format(proj.id))) + + @_utils.valid_kwargs( + 'action', 'description', 'destination_firewall_group_id', + 'destination_ip_address', 'destination_port', 'enabled', 'ip_version', + 'name', 'project_id', 'protocol', 'shared', 'source_firewall_group_id', + 'source_ip_address', 'source_port') + def create_firewall_rule(self, **kwargs): + """ + Creates firewall rule. + + :param action: Action performed on traffic. + Valid values: allow, deny + Defaults to deny. + :param description: Human-readable description. + :param destination_firewall_group_id: ID of destination firewall group. + :param destination_ip_address: IPv4-, IPv6 address or CIDR. + :param destination_port: Port or port range (e.g. 80:90) + :param bool enabled: Status of firewall rule. You can disable rules + without disassociating them from firewall + policies. Defaults to True. + :param int ip_version: IP Version. + Valid values: 4, 6 + Defaults to 4. + :param name: Human-readable name. + :param project_id: Project id. + :param protocol: IP protocol. + Valid values: icmp, tcp, udp, null + :param bool shared: Visibility to other projects. + Defaults to False. + :param source_firewall_group_id: ID of source firewall group. + :param source_ip_address: IPv4-, IPv6 address or CIDR. + :param source_port: Port or port range (e.g. 80:90) + :raises: BadRequestException if parameters are malformed + :return: created firewall rule + :rtype: FirewallRule + """ + return self.network.create_firewall_rule(**kwargs) + + def delete_firewall_rule(self, name_or_id, filters=None): + """ + Deletes firewall rule. + Prints debug message in case to-be-deleted resource was not found. + + :param name_or_id: firewall rule name or id + :param dict filters: optional filters + :raises: DuplicateResource on multiple matches + :return: True if resource is successfully deleted, False otherwise. + :rtype: bool + """ + if not filters: + filters = {} + try: + firewall_rule = self.network.find_firewall_rule( + name_or_id, ignore_missing=False, **filters) + self.network.delete_firewall_rule(firewall_rule, + ignore_missing=False) + except exceptions.ResourceNotFound: + self.log.debug('Firewall rule %s not found for deleting', + name_or_id) + return False + return True + + def get_firewall_rule(self, name_or_id, filters=None): + """ + Retrieves a single firewall rule. + + :param name_or_id: firewall rule name or id + :param dict filters: optional filters + :raises: DuplicateResource on multiple matches + :return: firewall rule dict or None if not found + :rtype: FirewallRule + """ + if not filters: + filters = {} + return self.network.find_firewall_rule(name_or_id, **filters) + + def list_firewall_rules(self, filters=None): + """ + Lists firewall rules. + + :param dict filters: optional filters + :return: list of firewall rules + :rtype: list[FirewallRule] + """ + if not filters: + filters = {} + return list(self.network.firewall_rules(**filters)) + + @_utils.valid_kwargs( + 'action', 'description', 'destination_firewall_group_id', + 'destination_ip_address', 'destination_port', 'enabled', 'ip_version', + 'name', 'project_id', 'protocol', 'shared', 'source_firewall_group_id', + 'source_ip_address', 'source_port') + def update_firewall_rule(self, name_or_id, filters=None, **kwargs): + """ + Updates firewall rule. + + :param name_or_id: firewall rule name or id + :param dict filters: optional filters + :param kwargs: firewall rule update parameters. + See create_firewall_rule docstring for valid parameters. + :raises: BadRequestException if parameters are malformed + :raises: NotFoundException if resource is not found + :return: updated firewall rule + :rtype: FirewallRule + """ + if not filters: + filters = {} + firewall_rule = self.network.find_firewall_rule( + name_or_id, ignore_missing=False, **filters) + + return self.network.update_firewall_rule(firewall_rule, **kwargs) + + def _get_firewall_rule_ids(self, name_or_id_list, filters=None): + """ + Takes a list of firewall rule name or ids, looks them up and returns + a list of firewall rule ids. + + Used by `create_firewall_policy` and `update_firewall_policy`. + + :param list[str] name_or_id_list: firewall rule name or id list + :param dict filters: optional filters + :raises: DuplicateResource on multiple matches + :raises: NotFoundException if resource is not found + :return: list of firewall rule ids + :rtype: list[str] + """ + if not filters: + filters = {} + ids_list = [] + for name_or_id in name_or_id_list: + ids_list.append(self.network.find_firewall_rule( + name_or_id, ignore_missing=False, **filters)['id']) + return ids_list + + @_utils.valid_kwargs('audited', 'description', 'firewall_rules', 'name', + 'project_id', 'shared') + def create_firewall_policy(self, **kwargs): + """ + Create firewall policy. + + :param bool audited: Status of audition of firewall policy. + Set to False each time the firewall policy or the + associated firewall rules are changed. + Has to be explicitly set to True. + :param description: Human-readable description. + :param list[str] firewall_rules: List of associated firewall rules. + :param name: Human-readable name. + :param project_id: Project id. + :param bool shared: Visibility to other projects. + Defaults to False. + :raises: BadRequestException if parameters are malformed + :raises: ResourceNotFound if a resource from firewall_list not found + :return: created firewall policy + :rtype: FirewallPolicy + """ + if 'firewall_rules' in kwargs: + kwargs['firewall_rules'] = self._get_firewall_rule_ids( + kwargs['firewall_rules']) + + return self.network.create_firewall_policy(**kwargs) + + def delete_firewall_policy(self, name_or_id, filters=None): + """ + Deletes firewall policy. + Prints debug message in case to-be-deleted resource was not found. + + :param name_or_id: firewall policy name or id + :param dict filters: optional filters + :raises: DuplicateResource on multiple matches + :return: True if resource is successfully deleted, False otherwise. + :rtype: bool + """ + if not filters: + filters = {} + try: + firewall_policy = self.network.find_firewall_policy( + name_or_id, ignore_missing=False, **filters) + self.network.delete_firewall_policy(firewall_policy, + ignore_missing=False) + except exceptions.ResourceNotFound: + self.log.debug('Firewall policy %s not found for deleting', + name_or_id) + return False + return True + + def get_firewall_policy(self, name_or_id, filters=None): + """ + Retrieves a single firewall policy. + + :param name_or_id: firewall policy name or id + :param dict filters: optional filters + :raises: DuplicateResource on multiple matches + :return: firewall policy or None if not found + :rtype: FirewallPolicy + """ + if not filters: + filters = {} + return self.network.find_firewall_policy(name_or_id, **filters) + + def list_firewall_policies(self, filters=None): + """ + Lists firewall policies. + + :param dict filters: optional filters + :return: list of firewall policies + :rtype: list[FirewallPolicy] + """ + if not filters: + filters = {} + return list(self.network.firewall_policies(**filters)) + + @_utils.valid_kwargs('audited', 'description', 'firewall_rules', 'name', + 'project_id', 'shared') + def update_firewall_policy(self, name_or_id, filters=None, **kwargs): + """ + Updates firewall policy. + + :param name_or_id: firewall policy name or id + :param dict filters: optional filters + :param kwargs: firewall policy update parameters + See create_firewall_policy docstring for valid parameters. + :raises: BadRequestException if parameters are malformed + :raises: DuplicateResource on multiple matches + :raises: ResourceNotFound if resource is not found + :return: updated firewall policy + :rtype: FirewallPolicy + """ + if not filters: + filters = {} + firewall_policy = self.network.find_firewall_policy( + name_or_id, ignore_missing=False, **filters) + + if 'firewall_rules' in kwargs: + kwargs['firewall_rules'] = self._get_firewall_rule_ids( + kwargs['firewall_rules']) + + return self.network.update_firewall_policy(firewall_policy, **kwargs) + + def insert_rule_into_policy(self, name_or_id, rule_name_or_id, + insert_after=None, insert_before=None, + filters=None): + """ + Adds firewall rule to the firewall_rules list of a firewall policy. + Short-circuits and returns the firewall policy early if the firewall + rule id is already present in the firewall_rules list. + This method doesn't do re-ordering. If you want to move a firewall rule + or or down the list, you have to remove and re-add it. + + :param name_or_id: firewall policy name or id + :param rule_name_or_id: firewall rule name or id + :param insert_after: rule name or id that should precede added rule + :param insert_before: rule name or id that should succeed added rule + :param dict filters: optional filters + :raises: DuplicateResource on multiple matches + :raises: ResourceNotFound if firewall policy or any of the firewall + rules (inserted, after, before) is not found. + :return: updated firewall policy + :rtype: FirewallPolicy + """ + if not filters: + filters = {} + firewall_policy = self.network.find_firewall_policy( + name_or_id, ignore_missing=False, **filters) + + firewall_rule = self.network.find_firewall_rule( + rule_name_or_id, ignore_missing=False) + # short-circuit if rule already in firewall_rules list + # the API can't do any re-ordering of existing rules + if firewall_rule['id'] in firewall_policy['firewall_rules']: + self.log.debug( + 'Firewall rule %s already associated with firewall policy %s', + rule_name_or_id, name_or_id) + return firewall_policy + + pos_params = {} + if insert_after is not None: + pos_params['insert_after'] = self.network.find_firewall_rule( + insert_after, ignore_missing=False)['id'] + + if insert_before is not None: + pos_params['insert_before'] = self.network.find_firewall_rule( + insert_before, ignore_missing=False)['id'] + + return self.network.insert_rule_into_policy(firewall_policy['id'], + firewall_rule['id'], + **pos_params) + + def remove_rule_from_policy(self, name_or_id, rule_name_or_id, + filters=None): + """ + Remove firewall rule from firewall policy's firewall_rules list. + Short-circuits and returns firewall policy early if firewall rule + is already absent from the firewall_rules list. + + :param name_or_id: firewall policy name or id + :param rule_name_or_id: firewall rule name or id + :param dict filters: optional filters + :raises: DuplicateResource on multiple matches + :raises: ResourceNotFound if firewall policy is not found + :return: updated firewall policy + :rtype: FirewallPolicy + """ + if not filters: + filters = {} + firewall_policy = self.network.find_firewall_policy( + name_or_id, ignore_missing=False, **filters) + + firewall_rule = self.network.find_firewall_rule(rule_name_or_id) + if not firewall_rule: + # short-circuit: if firewall rule is not found, + # return current firewall policy + self.log.debug('Firewall rule %s not found for removing', + rule_name_or_id) + return firewall_policy + + if firewall_rule['id'] not in firewall_policy['firewall_rules']: + # short-circuit: if firewall rule id is not associated, + # log it to debug and return current firewall policy + self.log.debug( + 'Firewall rule %s not associated with firewall policy %s', + rule_name_or_id, name_or_id) + return firewall_policy + + return self.network.remove_rule_from_policy(firewall_policy['id'], + firewall_rule['id']) + + @_utils.valid_kwargs( + 'admin_state_up', 'description', 'egress_firewall_policy', + 'ingress_firewall_policy', 'name', 'ports', 'project_id', 'shared') + def create_firewall_group(self, **kwargs): + """ + Creates firewall group. The keys egress_firewall_policy and + ingress_firewall_policy are looked up and mapped as + egress_firewall_policy_id and ingress_firewall_policy_id respectively. + Port name or ids list is transformed to port ids list before the POST + request. + + :param bool admin_state_up: State of firewall group. + Will block all traffic if set to False. + Defaults to True. + :param description: Human-readable description. + :param egress_firewall_policy: Name or id of egress firewall policy. + :param ingress_firewall_policy: Name or id of ingress firewall policy. + :param name: Human-readable name. + :param list[str] ports: List of associated ports (name or id) + :param project_id: Project id. + :param shared: Visibility to other projects. + Defaults to False. + :raises: BadRequestException if parameters are malformed + :raises: DuplicateResource on multiple matches + :raises: ResourceNotFound if (ingress-, egress-) firewall policy or + a port is not found. + :return: created firewall group + :rtype: FirewallGroup + """ + self._lookup_ingress_egress_firewall_policy_ids(kwargs) + if 'ports' in kwargs: + kwargs['ports'] = self._get_port_ids(kwargs['ports']) + return self.network.create_firewall_group(**kwargs) + + def delete_firewall_group(self, name_or_id, filters=None): + """ + Deletes firewall group. + Prints debug message in case to-be-deleted resource was not found. + + :param name_or_id: firewall group name or id + :param dict filters: optional filters + :raises: DuplicateResource on multiple matches + :return: True if resource is successfully deleted, False otherwise. + :rtype: bool + """ + if not filters: + filters = {} + try: + firewall_group = self.network.find_firewall_group( + name_or_id, ignore_missing=False, **filters) + self.network.delete_firewall_group(firewall_group, + ignore_missing=False) + except exceptions.ResourceNotFound: + self.log.debug('Firewall group %s not found for deleting', + name_or_id) + return False + return True + + def get_firewall_group(self, name_or_id, filters=None): + """ + Retrieves firewall group. + + :param name_or_id: firewall group name or id + :param dict filters: optional filters + :raises: DuplicateResource on multiple matches + :return: firewall group or None if not found + :rtype: FirewallGroup + """ + if not filters: + filters = {} + return self.network.find_firewall_group(name_or_id, **filters) + + def list_firewall_groups(self, filters=None): + """ + Lists firewall groups. + + :param dict filters: optional filters + :return: list of firewall groups + :rtype: list[FirewallGroup] + """ + if not filters: + filters = {} + return list(self.network.firewall_groups(**filters)) + + @_utils.valid_kwargs( + 'admin_state_up', 'description', 'egress_firewall_policy', + 'ingress_firewall_policy', 'name', 'ports', 'project_id', 'shared') + def update_firewall_group(self, name_or_id, filters=None, **kwargs): + """ + Updates firewall group. + To unset egress- or ingress firewall policy, set egress_firewall_policy + or ingress_firewall_policy to None. You can also set + egress_firewall_policy_id and ingress_firewall_policy_id directly, + which will skip the policy lookups. + + :param name_or_id: firewall group name or id + :param dict filters: optional filters + :param kwargs: firewall group update parameters + See create_firewall_group docstring for valid parameters. + :raises: BadRequestException if parameters are malformed + :raises: DuplicateResource on multiple matches + :raises: ResourceNotFound if firewall group, a firewall policy + (egress, ingress) or port is not found + :return: updated firewall group + :rtype: FirewallGroup + """ + if not filters: + filters = {} + firewall_group = self.network.find_firewall_group( + name_or_id, ignore_missing=False, **filters) + self._lookup_ingress_egress_firewall_policy_ids(kwargs) + + if 'ports' in kwargs: + kwargs['ports'] = self._get_port_ids(kwargs['ports']) + return self.network.update_firewall_group(firewall_group, **kwargs) + + def _lookup_ingress_egress_firewall_policy_ids(self, firewall_group): + """ + Transforms firewall_group dict IN-PLACE. Takes the value of the keys + egress_firewall_policy and ingress_firewall_policy, looks up the + policy ids and maps them to egress_firewall_policy_id and + ingress_firewall_policy_id. Old keys which were used for the lookup + are deleted. + + :param dict firewall_group: firewall group dict + :raises: DuplicateResource on multiple matches + :raises: ResourceNotFound if a firewall policy is not found + """ + for key in ('egress_firewall_policy', 'ingress_firewall_policy'): + if key not in firewall_group: + continue + if firewall_group[key] is None: + val = None + else: + val = self.network.find_firewall_policy( + firewall_group[key], ignore_missing=False)['id'] + firewall_group[key + '_id'] = val + del firewall_group[key] + + def list_security_groups(self, filters=None): + """List all available security groups. + + :param filters: (optional) dict of filter conditions to push down + :returns: A list of security group ``munch.Munch``. + + """ + # Security groups not supported + if not self._has_secgroups(): + raise exc.OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + + if not filters: + filters = {} + + data = [] + # Handle neutron security groups + if self._use_neutron_secgroups(): + # Neutron returns dicts, so no need to convert objects here. + resp = self.network.get('/security-groups.json', params=filters) + data = proxy._json_response( + resp, + error_message="Error fetching security group list") + return self._normalize_secgroups( + self._get_and_munchify('security_groups', data)) + + # Handle nova security groups + else: + data = proxy._json_response(self.compute.get( + '/os-security-groups', params=filters)) + return self._normalize_secgroups( + self._get_and_munchify('security_groups', data)) + + @_utils.valid_kwargs("name", "description", "shared", "default", + "project_id") + def create_qos_policy(self, **kwargs): + """Create a QoS policy. + + :param string name: Name of the QoS policy being created. + :param string description: Description of created QoS policy. + :param bool shared: Set the QoS policy as shared. + :param bool default: Set the QoS policy as default for project. + :param string project_id: Specify the project ID this QoS policy + will be created on (admin-only). + + :returns: The QoS policy object. + :raises: OpenStackCloudException on operation error. + """ + if not self._has_neutron_extension('qos'): + raise exc.OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + default = kwargs.pop("default", None) + if default is not None: + if self._has_neutron_extension('qos-default'): + kwargs['is_default'] = default + else: + self.log.debug("'qos-default' extension is not available on " + "target cloud") + + data = self.network.post("/qos/policies.json", json={'policy': kwargs}) + return self._get_and_munchify('policy', data) + + @_utils.valid_kwargs("name", "description", "shared", "default", + "project_id") + def update_qos_policy(self, name_or_id, **kwargs): + """Update an existing QoS policy. + + :param string name_or_id: + Name or ID of the QoS policy to update. + :param string policy_name: + The new name of the QoS policy. + :param string description: + The new description of the QoS policy. + :param bool shared: + If True, the QoS policy will be set as shared. + :param bool default: + If True, the QoS policy will be set as default for project. + + :returns: The updated QoS policy object. + :raises: OpenStackCloudException on operation error. + """ + if not self._has_neutron_extension('qos'): + raise exc.OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + default = kwargs.pop("default", None) + if default is not None: + if self._has_neutron_extension('qos-default'): + kwargs['is_default'] = default + else: + self.log.debug("'qos-default' extension is not available on " + "target cloud") + + if not kwargs: + self.log.debug("No QoS policy data to update") + return + + curr_policy = self.get_qos_policy(name_or_id) + if not curr_policy: + raise exc.OpenStackCloudException( + "QoS policy %s not found." % name_or_id) + + data = self.network.put( + "/qos/policies/{policy_id}.json".format( + policy_id=curr_policy['id']), + json={'policy': kwargs}) + return self._get_and_munchify('policy', data) + + def delete_qos_policy(self, name_or_id): + """Delete a QoS policy. + + :param name_or_id: Name or ID of the policy being deleted. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException on operation error. + """ + if not self._has_neutron_extension('qos'): + raise exc.OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + policy = self.get_qos_policy(name_or_id) + if not policy: + self.log.debug("QoS policy %s not found for deleting", name_or_id) + return False + + exceptions.raise_from_response(self.network.delete( + "/qos/policies/{policy_id}.json".format(policy_id=policy['id']))) + + return True + + def search_qos_bandwidth_limit_rules(self, policy_name_or_id, rule_id=None, + filters=None): + """Search QoS bandwidth limit rules + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rules should be associated. + :param string rule_id: ID of searched rule. + :param filters: a dict containing additional filters to use. e.g. + {'max_kbps': 1000} + + :returns: a list of ``munch.Munch`` containing the bandwidth limit + rule descriptions. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + OpenStack API call. + """ + rules = self.list_qos_bandwidth_limit_rules(policy_name_or_id, filters) + return _utils._filter_list(rules, rule_id, filters) + + def list_qos_bandwidth_limit_rules(self, policy_name_or_id, filters=None): + """List all available QoS bandwidth limit rules. + + :param string policy_name_or_id: Name or ID of the QoS policy from + from rules should be listed. + :param filters: (optional) dict of filter conditions to push down + :returns: A list of ``munch.Munch`` containing rule info. + + :raises: ``OpenStackCloudResourceNotFound`` if QoS policy will not be + found. + """ + if not self._has_neutron_extension('qos'): + raise exc.OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise exc.OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + # Translate None from search interface to empty {} for kwargs below + if not filters: + filters = {} + + resp = self.network.get( + "/qos/policies/{policy_id}/bandwidth_limit_rules.json".format( + policy_id=policy['id']), + params=filters) + data = proxy._json_response( + resp, + error_message="Error fetching QoS bandwidth limit rules from " + "{policy}".format(policy=policy['id'])) + return self._get_and_munchify('bandwidth_limit_rules', data) + + def get_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): + """Get a QoS bandwidth limit rule by name or ID. + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rule should be associated. + :param rule_id: ID of the rule. + + :returns: A bandwidth limit rule ``munch.Munch`` or None if + no matching rule is found. + + """ + if not self._has_neutron_extension('qos'): + raise exc.OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise exc.OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + resp = self.network.get( + "/qos/policies/{policy_id}/bandwidth_limit_rules/{rule_id}.json". + format(policy_id=policy['id'], rule_id=rule_id)) + data = proxy._json_response( + resp, + error_message="Error fetching QoS bandwidth limit rule {rule_id} " + "from {policy}".format(rule_id=rule_id, + policy=policy['id'])) + return self._get_and_munchify('bandwidth_limit_rule', data) + + @_utils.valid_kwargs("max_burst_kbps", "direction") + def create_qos_bandwidth_limit_rule(self, policy_name_or_id, max_kbps, + **kwargs): + """Create a QoS bandwidth limit rule. + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rule should be associated. + :param int max_kbps: Maximum bandwidth limit value + (in kilobits per second). + :param int max_burst_kbps: Maximum burst value (in kilobits). + :param string direction: Ingress or egress. + The direction in which the traffic will be limited. + + :returns: The QoS bandwidth limit rule. + :raises: OpenStackCloudException on operation error. + """ + if not self._has_neutron_extension('qos'): + raise exc.OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise exc.OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + if kwargs.get("direction") is not None: + if not self._has_neutron_extension('qos-bw-limit-direction'): + kwargs.pop("direction") + self.log.debug( + "'qos-bw-limit-direction' extension is not available on " + "target cloud") + + kwargs['max_kbps'] = max_kbps + data = self.network.post( + "/qos/policies/{policy_id}/bandwidth_limit_rules".format( + policy_id=policy['id']), + json={'bandwidth_limit_rule': kwargs}) + return self._get_and_munchify('bandwidth_limit_rule', data) + + @_utils.valid_kwargs("max_kbps", "max_burst_kbps", "direction") + def update_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id, + **kwargs): + """Update a QoS bandwidth limit rule. + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rule is associated. + :param string rule_id: ID of rule to update. + :param int max_kbps: Maximum bandwidth limit value + (in kilobits per second). + :param int max_burst_kbps: Maximum burst value (in kilobits). + :param string direction: Ingress or egress. + The direction in which the traffic will be limited. + + :returns: The updated QoS bandwidth limit rule. + :raises: OpenStackCloudException on operation error. + """ + if not self._has_neutron_extension('qos'): + raise exc.OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise exc.OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + if kwargs.get("direction") is not None: + if not self._has_neutron_extension('qos-bw-limit-direction'): + kwargs.pop("direction") + self.log.debug( + "'qos-bw-limit-direction' extension is not available on " + "target cloud") + + if not kwargs: + self.log.debug("No QoS bandwidth limit rule data to update") + return + + curr_rule = self.get_qos_bandwidth_limit_rule( + policy_name_or_id, rule_id) + if not curr_rule: + raise exc.OpenStackCloudException( + "QoS bandwidth_limit_rule {rule_id} not found in policy " + "{policy_id}".format(rule_id=rule_id, + policy_id=policy['id'])) + + data = self.network.put( + "/qos/policies/{policy_id}/bandwidth_limit_rules/{rule_id}.json". + format(policy_id=policy['id'], rule_id=rule_id), + json={'bandwidth_limit_rule': kwargs}) + return self._get_and_munchify('bandwidth_limit_rule', data) + + def delete_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): + """Delete a QoS bandwidth limit rule. + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rule is associated. + :param string rule_id: ID of rule to update. + + :raises: OpenStackCloudException on operation error. + """ + if not self._has_neutron_extension('qos'): + raise exc.OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise exc.OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + try: + exceptions.raise_from_response(self.network.delete( + "/qos/policies/{policy}/bandwidth_limit_rules/{rule}.json". + format(policy=policy['id'], rule=rule_id))) + except exc.OpenStackCloudURINotFound: + self.log.debug( + "QoS bandwidth limit rule {rule_id} not found in policy " + "{policy_id}. Ignoring.".format(rule_id=rule_id, + policy_id=policy['id'])) + return False + + return True + + def search_qos_dscp_marking_rules(self, policy_name_or_id, rule_id=None, + filters=None): + """Search QoS DSCP marking rules + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rules should be associated. + :param string rule_id: ID of searched rule. + :param filters: a dict containing additional filters to use. e.g. + {'dscp_mark': 32} + + :returns: a list of ``munch.Munch`` containing the dscp marking + rule descriptions. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + OpenStack API call. + """ + rules = self.list_qos_dscp_marking_rules(policy_name_or_id, filters) + return _utils._filter_list(rules, rule_id, filters) + + def list_qos_dscp_marking_rules(self, policy_name_or_id, filters=None): + """List all available QoS DSCP marking rules. + + :param string policy_name_or_id: Name or ID of the QoS policy from + from rules should be listed. + :param filters: (optional) dict of filter conditions to push down + :returns: A list of ``munch.Munch`` containing rule info. + + :raises: ``OpenStackCloudResourceNotFound`` if QoS policy will not be + found. + """ + if not self._has_neutron_extension('qos'): + raise exc.OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise exc.OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + # Translate None from search interface to empty {} for kwargs below + if not filters: + filters = {} + + resp = self.network.get( + "/qos/policies/{policy_id}/dscp_marking_rules.json".format( + policy_id=policy['id']), + params=filters) + data = proxy._json_response( + resp, + error_message="Error fetching QoS DSCP marking rules from " + "{policy}".format(policy=policy['id'])) + return self._get_and_munchify('dscp_marking_rules', data) + + def get_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): + """Get a QoS DSCP marking rule by name or ID. + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rule should be associated. + :param rule_id: ID of the rule. + + :returns: A bandwidth limit rule ``munch.Munch`` or None if + no matching rule is found. + + """ + if not self._has_neutron_extension('qos'): + raise exc.OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise exc.OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + resp = self.network.get( + "/qos/policies/{policy_id}/dscp_marking_rules/{rule_id}.json". + format(policy_id=policy['id'], rule_id=rule_id)) + data = proxy._json_response( + resp, + error_message="Error fetching QoS DSCP marking rule {rule_id} " + "from {policy}".format(rule_id=rule_id, + policy=policy['id'])) + return self._get_and_munchify('dscp_marking_rule', data) + + def create_qos_dscp_marking_rule(self, policy_name_or_id, dscp_mark): + """Create a QoS DSCP marking rule. + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rule should be associated. + :param int dscp_mark: DSCP mark value + + :returns: The QoS DSCP marking rule. + :raises: OpenStackCloudException on operation error. + """ + if not self._has_neutron_extension('qos'): + raise exc.OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise exc.OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + body = { + 'dscp_mark': dscp_mark + } + data = self.network.post( + "/qos/policies/{policy_id}/dscp_marking_rules".format( + policy_id=policy['id']), + json={'dscp_marking_rule': body}) + return self._get_and_munchify('dscp_marking_rule', data) + + @_utils.valid_kwargs("dscp_mark") + def update_qos_dscp_marking_rule(self, policy_name_or_id, rule_id, + **kwargs): + """Update a QoS DSCP marking rule. + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rule is associated. + :param string rule_id: ID of rule to update. + :param int dscp_mark: DSCP mark value + + :returns: The updated QoS bandwidth limit rule. + :raises: OpenStackCloudException on operation error. + """ + if not self._has_neutron_extension('qos'): + raise exc.OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise exc.OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + if not kwargs: + self.log.debug("No QoS DSCP marking rule data to update") + return + + curr_rule = self.get_qos_dscp_marking_rule( + policy_name_or_id, rule_id) + if not curr_rule: + raise exc.OpenStackCloudException( + "QoS dscp_marking_rule {rule_id} not found in policy " + "{policy_id}".format(rule_id=rule_id, + policy_id=policy['id'])) + + data = self.network.put( + "/qos/policies/{policy_id}/dscp_marking_rules/{rule_id}.json". + format(policy_id=policy['id'], rule_id=rule_id), + json={'dscp_marking_rule': kwargs}) + return self._get_and_munchify('dscp_marking_rule', data) + + def delete_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): + """Delete a QoS DSCP marking rule. + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rule is associated. + :param string rule_id: ID of rule to update. + + :raises: OpenStackCloudException on operation error. + """ + if not self._has_neutron_extension('qos'): + raise exc.OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise exc.OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + try: + exceptions.raise_from_response(self.network.delete( + "/qos/policies/{policy}/dscp_marking_rules/{rule}.json". + format(policy=policy['id'], rule=rule_id))) + except exc.OpenStackCloudURINotFound: + self.log.debug( + "QoS DSCP marking rule {rule_id} not found in policy " + "{policy_id}. Ignoring.".format(rule_id=rule_id, + policy_id=policy['id'])) + return False + + return True + + def search_qos_minimum_bandwidth_rules(self, policy_name_or_id, + rule_id=None, filters=None): + """Search QoS minimum bandwidth rules + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rules should be associated. + :param string rule_id: ID of searched rule. + :param filters: a dict containing additional filters to use. e.g. + {'min_kbps': 1000} + + :returns: a list of ``munch.Munch`` containing the bandwidth limit + rule descriptions. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + OpenStack API call. + """ + rules = self.list_qos_minimum_bandwidth_rules( + policy_name_or_id, filters) + return _utils._filter_list(rules, rule_id, filters) + + def list_qos_minimum_bandwidth_rules(self, policy_name_or_id, + filters=None): + """List all available QoS minimum bandwidth rules. + + :param string policy_name_or_id: Name or ID of the QoS policy from + from rules should be listed. + :param filters: (optional) dict of filter conditions to push down + :returns: A list of ``munch.Munch`` containing rule info. + + :raises: ``OpenStackCloudResourceNotFound`` if QoS policy will not be + found. + """ + if not self._has_neutron_extension('qos'): + raise exc.OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise exc.OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + # Translate None from search interface to empty {} for kwargs below + if not filters: + filters = {} + + resp = self.network.get( + "/qos/policies/{policy_id}/minimum_bandwidth_rules.json".format( + policy_id=policy['id']), + params=filters) + data = proxy._json_response( + resp, + error_message="Error fetching QoS minimum bandwidth rules from " + "{policy}".format(policy=policy['id'])) + return self._get_and_munchify('minimum_bandwidth_rules', data) + + def get_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): + """Get a QoS minimum bandwidth rule by name or ID. + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rule should be associated. + :param rule_id: ID of the rule. + + :returns: A bandwidth limit rule ``munch.Munch`` or None if + no matching rule is found. + + """ + if not self._has_neutron_extension('qos'): + raise exc.OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise exc.OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + resp = self.network.get( + "/qos/policies/{policy_id}/minimum_bandwidth_rules/{rule_id}.json". + format(policy_id=policy['id'], rule_id=rule_id)) + data = proxy._json_response( + resp, + error_message="Error fetching QoS minimum_bandwidth rule {rule_id}" + " from {policy}".format(rule_id=rule_id, + policy=policy['id'])) + return self._get_and_munchify('minimum_bandwidth_rule', data) + + @_utils.valid_kwargs("direction") + def create_qos_minimum_bandwidth_rule(self, policy_name_or_id, min_kbps, + **kwargs): + """Create a QoS minimum bandwidth limit rule. + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rule should be associated. + :param int min_kbps: Minimum bandwidth value (in kilobits per second). + :param string direction: Ingress or egress. + The direction in which the traffic will be available. + + :returns: The QoS minimum bandwidth rule. + :raises: OpenStackCloudException on operation error. + """ + if not self._has_neutron_extension('qos'): + raise exc.OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise exc.OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + kwargs['min_kbps'] = min_kbps + data = self.network.post( + "/qos/policies/{policy_id}/minimum_bandwidth_rules".format( + policy_id=policy['id']), + json={'minimum_bandwidth_rule': kwargs}) + return self._get_and_munchify('minimum_bandwidth_rule', data) + + @_utils.valid_kwargs("min_kbps", "direction") + def update_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id, + **kwargs): + """Update a QoS minimum bandwidth rule. + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rule is associated. + :param string rule_id: ID of rule to update. + :param int min_kbps: Minimum bandwidth value (in kilobits per second). + :param string direction: Ingress or egress. + The direction in which the traffic will be available. + + :returns: The updated QoS minimum bandwidth rule. + :raises: OpenStackCloudException on operation error. + """ + if not self._has_neutron_extension('qos'): + raise exc.OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise exc.OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + if not kwargs: + self.log.debug("No QoS minimum bandwidth rule data to update") + return + + curr_rule = self.get_qos_minimum_bandwidth_rule( + policy_name_or_id, rule_id) + if not curr_rule: + raise exc.OpenStackCloudException( + "QoS minimum_bandwidth_rule {rule_id} not found in policy " + "{policy_id}".format(rule_id=rule_id, + policy_id=policy['id'])) + + data = self.network.put( + "/qos/policies/{policy_id}/minimum_bandwidth_rules/{rule_id}.json". + format(policy_id=policy['id'], rule_id=rule_id), + json={'minimum_bandwidth_rule': kwargs}) + return self._get_and_munchify('minimum_bandwidth_rule', data) + + def delete_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): + """Delete a QoS minimum bandwidth rule. + + :param string policy_name_or_id: Name or ID of the QoS policy to which + rule is associated. + :param string rule_id: ID of rule to delete. + + :raises: OpenStackCloudException on operation error. + """ + if not self._has_neutron_extension('qos'): + raise exc.OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + + policy = self.get_qos_policy(policy_name_or_id) + if not policy: + raise exc.OpenStackCloudResourceNotFound( + "QoS policy {name_or_id} not Found.".format( + name_or_id=policy_name_or_id)) + + try: + exceptions.raise_from_response(self.network.delete( + "/qos/policies/{policy}/minimum_bandwidth_rules/{rule}.json". + format(policy=policy['id'], rule=rule_id))) + except exc.OpenStackCloudURINotFound: + self.log.debug( + "QoS minimum bandwidth rule {rule_id} not found in policy " + "{policy_id}. Ignoring.".format(rule_id=rule_id, + policy_id=policy['id'])) + return False + + return True + + def add_router_interface(self, router, subnet_id=None, port_id=None): + """Attach a subnet to an internal router interface. + + Either a subnet ID or port ID must be specified for the internal + interface. Supplying both will result in an error. + + :param dict router: The dict object of the router being changed + :param string subnet_id: The ID of the subnet to use for the interface + :param string port_id: The ID of the port to use for the interface + + :returns: A ``munch.Munch`` with the router ID (ID), + subnet ID (subnet_id), port ID (port_id) and tenant ID + (tenant_id). + + :raises: OpenStackCloudException on operation error. + """ + json_body = {} + if subnet_id: + json_body['subnet_id'] = subnet_id + if port_id: + json_body['port_id'] = port_id + + return proxy._json_response( + self.network.put( + "/routers/{router_id}/add_router_interface.json".format( + router_id=router['id']), + json=json_body), + error_message="Error attaching interface to router {0}".format( + router['id'])) + + def remove_router_interface(self, router, subnet_id=None, port_id=None): + """Detach a subnet from an internal router interface. + + At least one of subnet_id or port_id must be supplied. + + If you specify both subnet and port ID, the subnet ID must + correspond to the subnet ID of the first IP address on the port + specified by the port ID. Otherwise an error occurs. + + :param dict router: The dict object of the router being changed + :param string subnet_id: The ID of the subnet to use for the interface + :param string port_id: The ID of the port to use for the interface + + :returns: None on success + + :raises: OpenStackCloudException on operation error. + """ + json_body = {} + if subnet_id: + json_body['subnet_id'] = subnet_id + if port_id: + json_body['port_id'] = port_id + + if not json_body: + raise ValueError( + "At least one of subnet_id or port_id must be supplied.") + + exceptions.raise_from_response( + self.network.put( + "/routers/{router_id}/remove_router_interface.json".format( + router_id=router['id']), + json=json_body), + error_message="Error detaching interface from router {0}".format( + router['id'])) + + def list_router_interfaces(self, router, interface_type=None): + """List all interfaces for a router. + + :param dict router: A router dict object. + :param string interface_type: One of None, "internal", or "external". + Controls whether all, internal interfaces or external interfaces + are returned. + + :returns: A list of port ``munch.Munch`` objects. + """ + # Find only router interface and gateway ports, ignore L3 HA ports etc. + router_interfaces = self.search_ports(filters={ + 'device_id': router['id'], + 'device_owner': 'network:router_interface'} + ) + self.search_ports(filters={ + 'device_id': router['id'], + 'device_owner': 'network:router_interface_distributed'} + ) + self.search_ports(filters={ + 'device_id': router['id'], + 'device_owner': 'network:ha_router_replicated_interface'}) + router_gateways = self.search_ports(filters={ + 'device_id': router['id'], + 'device_owner': 'network:router_gateway'}) + ports = router_interfaces + router_gateways + + if interface_type: + if interface_type == 'internal': + return router_interfaces + if interface_type == 'external': + return router_gateways + return ports + + def create_router(self, name=None, admin_state_up=True, + ext_gateway_net_id=None, enable_snat=None, + ext_fixed_ips=None, project_id=None, + availability_zone_hints=None): + """Create a logical router. + + :param string name: The router name. + :param bool admin_state_up: The administrative state of the router. + :param string ext_gateway_net_id: Network ID for the external gateway. + :param bool enable_snat: Enable Source NAT (SNAT) attribute. + :param ext_fixed_ips: + List of dictionaries of desired IP and/or subnet on the + external network. Example:: + + [ + { + "subnet_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b", + "ip_address": "192.168.10.2" + } + ] + :param string project_id: Project ID for the router. + :param types.ListType availability_zone_hints: + A list of availability zone hints. + + :returns: The router object. + :raises: OpenStackCloudException on operation error. + """ + router = { + 'admin_state_up': admin_state_up + } + if project_id is not None: + router['tenant_id'] = project_id + if name: + router['name'] = name + ext_gw_info = self._build_external_gateway_info( + ext_gateway_net_id, enable_snat, ext_fixed_ips + ) + if ext_gw_info: + router['external_gateway_info'] = ext_gw_info + if availability_zone_hints is not None: + if not isinstance(availability_zone_hints, list): + raise exc.OpenStackCloudException( + "Parameter 'availability_zone_hints' must be a list") + if not self._has_neutron_extension('router_availability_zone'): + raise exc.OpenStackCloudUnavailableExtension( + 'router_availability_zone extension is not available on ' + 'target cloud') + router['availability_zone_hints'] = availability_zone_hints + + data = proxy._json_response( + self.network.post("/routers.json", json={"router": router}), + error_message="Error creating router {0}".format(name)) + return self._get_and_munchify('router', data) + + def update_router(self, name_or_id, name=None, admin_state_up=None, + ext_gateway_net_id=None, enable_snat=None, + ext_fixed_ips=None, routes=None): + """Update an existing logical router. + + :param string name_or_id: The name or UUID of the router to update. + :param string name: The new router name. + :param bool admin_state_up: The administrative state of the router. + :param string ext_gateway_net_id: + The network ID for the external gateway. + :param bool enable_snat: Enable Source NAT (SNAT) attribute. + :param ext_fixed_ips: + List of dictionaries of desired IP and/or subnet on the + external network. Example:: + + [ + { + "subnet_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b", + "ip_address": "192.168.10.2" + } + ] + :param list routes: + A list of dictionaries with destination and nexthop parameters. + Example:: + + [ + { + "destination": "179.24.1.0/24", + "nexthop": "172.24.3.99" + } + ] + :returns: The router object. + :raises: OpenStackCloudException on operation error. + """ + router = {} + if name: + router['name'] = name + if admin_state_up is not None: + router['admin_state_up'] = admin_state_up + ext_gw_info = self._build_external_gateway_info( + ext_gateway_net_id, enable_snat, ext_fixed_ips + ) + if ext_gw_info: + router['external_gateway_info'] = ext_gw_info + + if routes: + if self._has_neutron_extension('extraroute'): + router['routes'] = routes + else: + self.log.warn( + 'extra routes extension is not available on target cloud') + + if not router: + self.log.debug("No router data to update") + return + + curr_router = self.get_router(name_or_id) + if not curr_router: + raise exc.OpenStackCloudException( + "Router %s not found." % name_or_id) + + resp = self.network.put( + "/routers/{router_id}.json".format(router_id=curr_router['id']), + json={"router": router}) + data = proxy._json_response( + resp, + error_message="Error updating router {0}".format(name_or_id)) + return self._get_and_munchify('router', data) + + def delete_router(self, name_or_id): + """Delete a logical router. + + If a name, instead of a unique UUID, is supplied, it is possible + that we could find more than one matching router since names are + not required to be unique. An error will be raised in this case. + + :param name_or_id: Name or ID of the router being deleted. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException on operation error. + """ + router = self.get_router(name_or_id) + if not router: + self.log.debug("Router %s not found for deleting", name_or_id) + return False + + exceptions.raise_from_response(self.network.delete( + "/routers/{router_id}.json".format(router_id=router['id']), + error_message="Error deleting router {0}".format(name_or_id))) + + return True + + def create_subnet(self, network_name_or_id, cidr=None, ip_version=4, + enable_dhcp=False, subnet_name=None, tenant_id=None, + allocation_pools=None, + gateway_ip=None, disable_gateway_ip=False, + dns_nameservers=None, host_routes=None, + ipv6_ra_mode=None, ipv6_address_mode=None, + prefixlen=None, use_default_subnetpool=False, **kwargs): + """Create a subnet on a specified network. + + :param string network_name_or_id: + The unique name or ID of the attached network. If a non-unique + name is supplied, an exception is raised. + :param string cidr: + The CIDR. + :param int ip_version: + The IP version, which is 4 or 6. + :param bool enable_dhcp: + Set to ``True`` if DHCP is enabled and ``False`` if disabled. + Default is ``False``. + :param string subnet_name: + The name of the subnet. + :param string tenant_id: + The ID of the tenant who owns the network. Only administrative users + can specify a tenant ID other than their own. + :param allocation_pools: + A list of dictionaries of the start and end addresses for the + allocation pools. For example:: + + [ + { + "start": "192.168.199.2", + "end": "192.168.199.254" + } + ] + + :param string gateway_ip: + The gateway IP address. When you specify both allocation_pools and + gateway_ip, you must ensure that the gateway IP does not overlap + with the specified allocation pools. + :param bool disable_gateway_ip: + Set to ``True`` if gateway IP address is disabled and ``False`` if + enabled. It is not allowed with gateway_ip. + Default is ``False``. + :param dns_nameservers: + A list of DNS name servers for the subnet. For example:: + + [ "8.8.8.7", "8.8.8.8" ] + + :param host_routes: + A list of host route dictionaries for the subnet. For example:: + + [ + { + "destination": "0.0.0.0/0", + "nexthop": "123.456.78.9" + }, + { + "destination": "192.168.0.0/24", + "nexthop": "192.168.0.1" + } + ] + + :param string ipv6_ra_mode: + IPv6 Router Advertisement mode. Valid values are: 'dhcpv6-stateful', + 'dhcpv6-stateless', or 'slaac'. + :param string ipv6_address_mode: + IPv6 address mode. Valid values are: 'dhcpv6-stateful', + 'dhcpv6-stateless', or 'slaac'. + :param string prefixlen: + The prefix length to use for subnet allocation from a subnet pool. + :param bool use_default_subnetpool: + Use the default subnetpool for ``ip_version`` to obtain a CIDR. It + is required to pass ``None`` to the ``cidr`` argument when enabling + this option. + :param kwargs: Key value pairs to be passed to the Neutron API. + + :returns: The new subnet object. + :raises: OpenStackCloudException on operation error. + """ + + if tenant_id is not None: + filters = {'tenant_id': tenant_id} + else: + filters = None + + network = self.get_network(network_name_or_id, filters) + if not network: + raise exc.OpenStackCloudException( + "Network %s not found." % network_name_or_id) + + if disable_gateway_ip and gateway_ip: + raise exc.OpenStackCloudException( + 'arg:disable_gateway_ip is not allowed with arg:gateway_ip') + + if not cidr and not use_default_subnetpool: + raise exc.OpenStackCloudException( + 'arg:cidr is required when a subnetpool is not used') + + if cidr and use_default_subnetpool: + raise exc.OpenStackCloudException( + 'arg:cidr must be set to None when use_default_subnetpool == ' + 'True') + + # Be friendly on ip_version and allow strings + if isinstance(ip_version, six.string_types): + try: + ip_version = int(ip_version) + except ValueError: + raise exc.OpenStackCloudException( + 'ip_version must be an integer') + + # The body of the neutron message for the subnet we wish to create. + # This includes attributes that are required or have defaults. + subnet = dict({ + 'network_id': network['id'], + 'ip_version': ip_version, + 'enable_dhcp': enable_dhcp, + }, **kwargs) + + # Add optional attributes to the message. + if cidr: + subnet['cidr'] = cidr + if subnet_name: + subnet['name'] = subnet_name + if tenant_id: + subnet['tenant_id'] = tenant_id + if allocation_pools: + subnet['allocation_pools'] = allocation_pools + if gateway_ip: + subnet['gateway_ip'] = gateway_ip + if disable_gateway_ip: + subnet['gateway_ip'] = None + if dns_nameservers: + subnet['dns_nameservers'] = dns_nameservers + if host_routes: + subnet['host_routes'] = host_routes + if ipv6_ra_mode: + subnet['ipv6_ra_mode'] = ipv6_ra_mode + if ipv6_address_mode: + subnet['ipv6_address_mode'] = ipv6_address_mode + if prefixlen: + subnet['prefixlen'] = prefixlen + if use_default_subnetpool: + subnet['use_default_subnetpool'] = True + + response = self.network.post("/subnets.json", json={"subnet": subnet}) + + return self._get_and_munchify('subnet', response) + + def delete_subnet(self, name_or_id): + """Delete a subnet. + + If a name, instead of a unique UUID, is supplied, it is possible + that we could find more than one matching subnet since names are + not required to be unique. An error will be raised in this case. + + :param name_or_id: Name or ID of the subnet being deleted. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException on operation error. + """ + subnet = self.get_subnet(name_or_id) + if not subnet: + self.log.debug("Subnet %s not found for deleting", name_or_id) + return False + + exceptions.raise_from_response(self.network.delete( + "/subnets/{subnet_id}.json".format(subnet_id=subnet['id']))) + return True + + def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, + gateway_ip=None, disable_gateway_ip=None, + allocation_pools=None, dns_nameservers=None, + host_routes=None): + """Update an existing subnet. + + :param string name_or_id: + Name or ID of the subnet to update. + :param string subnet_name: + The new name of the subnet. + :param bool enable_dhcp: + Set to ``True`` if DHCP is enabled and ``False`` if disabled. + :param string gateway_ip: + The gateway IP address. When you specify both allocation_pools and + gateway_ip, you must ensure that the gateway IP does not overlap + with the specified allocation pools. + :param bool disable_gateway_ip: + Set to ``True`` if gateway IP address is disabled and ``False`` if + enabled. It is not allowed with gateway_ip. + Default is ``False``. + :param allocation_pools: + A list of dictionaries of the start and end addresses for the + allocation pools. For example:: + + [ + { + "start": "192.168.199.2", + "end": "192.168.199.254" + } + ] + + :param dns_nameservers: + A list of DNS name servers for the subnet. For example:: + + [ "8.8.8.7", "8.8.8.8" ] + + :param host_routes: + A list of host route dictionaries for the subnet. For example:: + + [ + { + "destination": "0.0.0.0/0", + "nexthop": "123.456.78.9" + }, + { + "destination": "192.168.0.0/24", + "nexthop": "192.168.0.1" + } + ] + + :returns: The updated subnet object. + :raises: OpenStackCloudException on operation error. + """ + subnet = {} + if subnet_name: + subnet['name'] = subnet_name + if enable_dhcp is not None: + subnet['enable_dhcp'] = enable_dhcp + if gateway_ip: + subnet['gateway_ip'] = gateway_ip + if disable_gateway_ip: + subnet['gateway_ip'] = None + if allocation_pools: + subnet['allocation_pools'] = allocation_pools + if dns_nameservers: + subnet['dns_nameservers'] = dns_nameservers + if host_routes: + subnet['host_routes'] = host_routes + + if not subnet: + self.log.debug("No subnet data to update") + return + + if disable_gateway_ip and gateway_ip: + raise exc.OpenStackCloudException( + 'arg:disable_gateway_ip is not allowed with arg:gateway_ip') + + curr_subnet = self.get_subnet(name_or_id) + if not curr_subnet: + raise exc.OpenStackCloudException( + "Subnet %s not found." % name_or_id) + + response = self.network.put( + "/subnets/{subnet_id}.json".format(subnet_id=curr_subnet['id']), + json={"subnet": subnet}) + return self._get_and_munchify('subnet', response) + + @_utils.valid_kwargs('name', 'admin_state_up', 'mac_address', 'fixed_ips', + 'subnet_id', 'ip_address', 'security_groups', + 'allowed_address_pairs', 'extra_dhcp_opts', + 'device_owner', 'device_id', 'binding:vnic_type', + 'binding:profile', 'port_security_enabled') + def create_port(self, network_id, **kwargs): + """Create a port + + :param network_id: The ID of the network. (Required) + :param name: A symbolic name for the port. (Optional) + :param admin_state_up: The administrative status of the port, + which is up (true, default) or down (false). (Optional) + :param mac_address: The MAC address. (Optional) + :param fixed_ips: List of ip_addresses and subnet_ids. See subnet_id + and ip_address. (Optional) + For example:: + + [ + { + "ip_address": "10.29.29.13", + "subnet_id": "a78484c4-c380-4b47-85aa-21c51a2d8cbd" + }, ... + ] + :param subnet_id: If you specify only a subnet ID, OpenStack Networking + allocates an available IP from that subnet to the port. (Optional) + If you specify both a subnet ID and an IP address, OpenStack + Networking tries to allocate the specified address to the port. + :param ip_address: If you specify both a subnet ID and an IP address, + OpenStack Networking tries to allocate the specified address to + the port. + :param security_groups: List of security group UUIDs. (Optional) + :param allowed_address_pairs: Allowed address pairs list (Optional) + For example:: + + [ + { + "ip_address": "23.23.23.1", + "mac_address": "fa:16:3e:c4:cd:3f" + }, ... + ] + :param extra_dhcp_opts: Extra DHCP options. (Optional). + For example:: + + [ + { + "opt_name": "opt name1", + "opt_value": "value1" + }, ... + ] + :param device_owner: The ID of the entity that uses this port. + For example, a DHCP agent. (Optional) + :param device_id: The ID of the device that uses this port. + For example, a virtual server. (Optional) + :param binding vnic_type: The type of the created port. (Optional) + :param port_security_enabled: The security port state created on + the network. (Optional) + + :returns: a ``munch.Munch`` describing the created port. + + :raises: ``OpenStackCloudException`` on operation error. + """ + kwargs['network_id'] = network_id + + data = proxy._json_response( + self.network.post("/ports.json", json={'port': kwargs}), + error_message="Error creating port for network {0}".format( + network_id)) + return self._get_and_munchify('port', data) + + @_utils.valid_kwargs('name', 'admin_state_up', 'fixed_ips', + 'security_groups', 'allowed_address_pairs', + 'extra_dhcp_opts', 'device_owner', 'device_id', + 'binding:vnic_type', 'binding:profile', + 'port_security_enabled') + def update_port(self, name_or_id, **kwargs): + """Update a port + + Note: to unset an attribute use None value. To leave an attribute + untouched just omit it. + + :param name_or_id: name or ID of the port to update. (Required) + :param name: A symbolic name for the port. (Optional) + :param admin_state_up: The administrative status of the port, + which is up (true) or down (false). (Optional) + :param fixed_ips: List of ip_addresses and subnet_ids. (Optional) + If you specify only a subnet ID, OpenStack Networking allocates + an available IP from that subnet to the port. + If you specify both a subnet ID and an IP address, OpenStack + Networking tries to allocate the specified address to the port. + For example:: + + [ + { + "ip_address": "10.29.29.13", + "subnet_id": "a78484c4-c380-4b47-85aa-21c51a2d8cbd" + }, ... + ] + :param security_groups: List of security group UUIDs. (Optional) + :param allowed_address_pairs: Allowed address pairs list (Optional) + For example:: + + [ + { + "ip_address": "23.23.23.1", + "mac_address": "fa:16:3e:c4:cd:3f" + }, ... + ] + :param extra_dhcp_opts: Extra DHCP options. (Optional). + For example:: + + [ + { + "opt_name": "opt name1", + "opt_value": "value1" + }, ... + ] + :param device_owner: The ID of the entity that uses this port. + For example, a DHCP agent. (Optional) + :param device_id: The ID of the resource this port is attached to. + :param binding vnic_type: The type of the created port. (Optional) + :param port_security_enabled: The security port state created on + the network. (Optional) + + :returns: a ``munch.Munch`` describing the updated port. + + :raises: OpenStackCloudException on operation error. + """ + port = self.get_port(name_or_id=name_or_id) + if port is None: + raise exc.OpenStackCloudException( + "failed to find port '{port}'".format(port=name_or_id)) + + data = proxy._json_response( + self.network.put( + "/ports/{port_id}.json".format(port_id=port['id']), + json={"port": kwargs}), + error_message="Error updating port {0}".format(name_or_id)) + return self._get_and_munchify('port', data) + + def delete_port(self, name_or_id): + """Delete a port + + :param name_or_id: ID or name of the port to delete. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException on operation error. + """ + port = self.get_port(name_or_id=name_or_id) + if port is None: + self.log.debug("Port %s not found for deleting", name_or_id) + return False + + exceptions.raise_from_response( + self.network.delete( + "/ports/{port_id}.json".format(port_id=port['id'])), + error_message="Error deleting port {0}".format(name_or_id)) + return True + + def _get_port_ids(self, name_or_id_list, filters=None): + """ + Takes a list of port names or ids, retrieves ports and returns a list + with port ids only. + + :param list[str] name_or_id_list: list of port names or ids + :param dict filters: optional filters + :raises: SDKException on multiple matches + :raises: ResourceNotFound if a port is not found + :return: list of port ids + :rtype: list[str] + """ + ids_list = [] + for name_or_id in name_or_id_list: + port = self.get_port(name_or_id, filters) + if not port: + raise exceptions.ResourceNotFound( + 'Port {id} not found'.format(id=name_or_id)) + ids_list.append(port['id']) + return ids_list + + def _build_external_gateway_info(self, ext_gateway_net_id, enable_snat, + ext_fixed_ips): + info = {} + if ext_gateway_net_id: + info['network_id'] = ext_gateway_net_id + # Only send enable_snat if it is explicitly set. + if enable_snat is not None: + info['enable_snat'] = enable_snat + if ext_fixed_ips: + info['external_fixed_ips'] = ext_fixed_ips + if info: + return info + return None diff --git a/openstack/cloud/_network_common.py b/openstack/cloud/_network_common.py new file mode 100644 index 000000000..2c2bdd724 --- /dev/null +++ b/openstack/cloud/_network_common.py @@ -0,0 +1,370 @@ +# 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 types so that we can reference ListType in sphinx param declarations. +# We can't just use list, because sphinx gets confused by +# openstack.resource.Resource.list and openstack.resource2.Resource.list +import threading +import types # noqa + +from openstack.cloud import exc +from openstack.cloud import _normalize + + +class NetworkCommonCloudMixin(_normalize.Normalizer): + """Shared networking functions used by FloatingIP, Network, Compute classes + """ + + def __init__(self): + self._external_ipv4_names = self.config.get_external_ipv4_networks() + self._internal_ipv4_names = self.config.get_internal_ipv4_networks() + self._external_ipv6_names = self.config.get_external_ipv6_networks() + self._internal_ipv6_names = self.config.get_internal_ipv6_networks() + self._nat_destination = self.config.get_nat_destination() + self._nat_source = self.config.get_nat_source() + self._default_network = self.config.get_default_network() + + self._use_external_network = self.config.config.get( + 'use_external_network', True) + self._use_internal_network = self.config.config.get( + 'use_internal_network', True) + + self._networks_lock = threading.Lock() + self._reset_network_caches() + + def use_external_network(self): + return self._use_external_network + + def use_internal_network(self): + return self._use_internal_network + + def _reset_network_caches(self): + # Variables to prevent us from going through the network finding + # logic again if we've done it once. This is different from just + # the cached value, since "None" is a valid value to find. + with self._networks_lock: + self._external_ipv4_networks = [] + self._external_ipv4_floating_networks = [] + self._internal_ipv4_networks = [] + self._external_ipv6_networks = [] + self._internal_ipv6_networks = [] + self._nat_destination_network = None + self._nat_source_network = None + self._default_network_network = None + self._network_list_stamp = False + + def _set_interesting_networks(self): + external_ipv4_networks = [] + external_ipv4_floating_networks = [] + internal_ipv4_networks = [] + external_ipv6_networks = [] + internal_ipv6_networks = [] + nat_destination = None + nat_source = None + default_network = None + + all_subnets = None + + # Filter locally because we have an or condition + try: + # TODO(mordred): Rackspace exposes neutron but it does not + # work. I think that overriding what the service catalog + # reports should be a thing os-client-config should handle + # in a vendor profile - but for now it does not. That means + # this search_networks can just totally fail. If it does + # though, that's fine, clearly the neutron introspection is + # not going to work. + all_networks = self.list_networks() + except exc.OpenStackCloudException: + self._network_list_stamp = True + return + + for network in all_networks: + + # External IPv4 networks + if (network['name'] in self._external_ipv4_names + or network['id'] in self._external_ipv4_names): + external_ipv4_networks.append(network) + elif ((('router:external' in network + and network['router:external']) + or network.get('provider:physical_network')) + and network['name'] not in self._internal_ipv4_names + and network['id'] not in self._internal_ipv4_names): + external_ipv4_networks.append(network) + + # Internal networks + if (network['name'] in self._internal_ipv4_names + or network['id'] in self._internal_ipv4_names): + internal_ipv4_networks.append(network) + elif (not network.get('router:external', False) + and not network.get('provider:physical_network') + and network['name'] not in self._external_ipv4_names + and network['id'] not in self._external_ipv4_names): + internal_ipv4_networks.append(network) + + # External networks + if (network['name'] in self._external_ipv6_names + or network['id'] in self._external_ipv6_names): + external_ipv6_networks.append(network) + elif (network.get('router:external') + and network['name'] not in self._internal_ipv6_names + and network['id'] not in self._internal_ipv6_names): + external_ipv6_networks.append(network) + + # Internal networks + if (network['name'] in self._internal_ipv6_names + or network['id'] in self._internal_ipv6_names): + internal_ipv6_networks.append(network) + elif (not network.get('router:external', False) + and network['name'] not in self._external_ipv6_names + and network['id'] not in self._external_ipv6_names): + internal_ipv6_networks.append(network) + + # External Floating IPv4 networks + if self._nat_source in ( + network['name'], network['id']): + if nat_source: + raise exc.OpenStackCloudException( + 'Multiple networks were found matching' + ' {nat_net} which is the network configured' + ' to be the NAT source. Please check your' + ' cloud resources. It is probably a good idea' + ' to configure this network by ID rather than' + ' by name.'.format( + nat_net=self._nat_source)) + external_ipv4_floating_networks.append(network) + nat_source = network + elif self._nat_source is None: + if network.get('router:external'): + external_ipv4_floating_networks.append(network) + nat_source = nat_source or network + + # NAT Destination + if self._nat_destination in ( + network['name'], network['id']): + if nat_destination: + raise exc.OpenStackCloudException( + 'Multiple networks were found matching' + ' {nat_net} which is the network configured' + ' to be the NAT destination. Please check your' + ' cloud resources. It is probably a good idea' + ' to configure this network by ID rather than' + ' by name.'.format( + nat_net=self._nat_destination)) + nat_destination = network + elif self._nat_destination is None: + # TODO(mordred) need a config value for floating + # ips for this cloud so that we can skip this + # No configured nat destination, we have to figured + # it out. + if all_subnets is None: + try: + all_subnets = self.list_subnets() + except exc.OpenStackCloudException: + # Thanks Rackspace broken neutron + all_subnets = [] + + for subnet in all_subnets: + # TODO(mordred) trap for detecting more than + # one network with a gateway_ip without a config + if ('gateway_ip' in subnet and subnet['gateway_ip'] + and network['id'] == subnet['network_id']): + nat_destination = network + break + + # Default network + if self._default_network in ( + network['name'], network['id']): + if default_network: + raise exc.OpenStackCloudException( + 'Multiple networks were found matching' + ' {default_net} which is the network' + ' configured to be the default interface' + ' network. Please check your cloud resources.' + ' It is probably a good idea' + ' to configure this network by ID rather than' + ' by name.'.format( + default_net=self._default_network)) + default_network = network + + # Validate config vs. reality + for net_name in self._external_ipv4_names: + if net_name not in [net['name'] for net in external_ipv4_networks]: + raise exc.OpenStackCloudException( + "Networks: {network} was provided for external IPv4" + " access and those networks could not be found".format( + network=net_name)) + + for net_name in self._internal_ipv4_names: + if net_name not in [net['name'] for net in internal_ipv4_networks]: + raise exc.OpenStackCloudException( + "Networks: {network} was provided for internal IPv4" + " access and those networks could not be found".format( + network=net_name)) + + for net_name in self._external_ipv6_names: + if net_name not in [net['name'] for net in external_ipv6_networks]: + raise exc.OpenStackCloudException( + "Networks: {network} was provided for external IPv6" + " access and those networks could not be found".format( + network=net_name)) + + for net_name in self._internal_ipv6_names: + if net_name not in [net['name'] for net in internal_ipv6_networks]: + raise exc.OpenStackCloudException( + "Networks: {network} was provided for internal IPv6" + " access and those networks could not be found".format( + network=net_name)) + + if self._nat_destination and not nat_destination: + raise exc.OpenStackCloudException( + 'Network {network} was configured to be the' + ' destination for inbound NAT but it could not be' + ' found'.format( + network=self._nat_destination)) + + if self._nat_source and not nat_source: + raise exc.OpenStackCloudException( + 'Network {network} was configured to be the' + ' source for inbound NAT but it could not be' + ' found'.format( + network=self._nat_source)) + + if self._default_network and not default_network: + raise exc.OpenStackCloudException( + 'Network {network} was configured to be the' + ' default network interface but it could not be' + ' found'.format( + network=self._default_network)) + + self._external_ipv4_networks = external_ipv4_networks + self._external_ipv4_floating_networks = external_ipv4_floating_networks + self._internal_ipv4_networks = internal_ipv4_networks + self._external_ipv6_networks = external_ipv6_networks + self._internal_ipv6_networks = internal_ipv6_networks + self._nat_destination_network = nat_destination + self._nat_source_network = nat_source + self._default_network_network = default_network + + def _find_interesting_networks(self): + if self._networks_lock.acquire(): + try: + if self._network_list_stamp: + return + if (not self._use_external_network + and not self._use_internal_network): + # Both have been flagged as skip - don't do a list + return + if not self.has_service('network'): + return + self._set_interesting_networks() + self._network_list_stamp = True + finally: + self._networks_lock.release() + + # def get_nat_destination(self): + # """Return the network that is configured to be the NAT destination. + # + # :returns: A network dict if one is found + # """ + # self._find_interesting_networks() + # return self._nat_destination_network + + def get_nat_source(self): + """Return the network that is configured to be the NAT destination. + + :returns: A network dict if one is found + """ + self._find_interesting_networks() + return self._nat_source_network + + def get_default_network(self): + """Return the network that is configured to be the default interface. + + :returns: A network dict if one is found + """ + self._find_interesting_networks() + return self._default_network_network + + def get_nat_destination(self): + """Return the network that is configured to be the NAT destination. + + :returns: A network dict if one is found + """ + self._find_interesting_networks() + return self._nat_destination_network + + def get_external_networks(self): + """Return the networks that are configured to route northbound. + + This should be avoided in favor of the specific ipv4/ipv6 method, + but is here for backwards compatibility. + + :returns: A list of network ``munch.Munch`` if one is found + """ + self._find_interesting_networks() + return list( + set(self._external_ipv4_networks) + | set(self._external_ipv6_networks)) + + def get_internal_networks(self): + """Return the networks that are configured to not route northbound. + + This should be avoided in favor of the specific ipv4/ipv6 method, + but is here for backwards compatibility. + + :returns: A list of network ``munch.Munch`` if one is found + """ + self._find_interesting_networks() + return list( + set(self._internal_ipv4_networks) + | set(self._internal_ipv6_networks)) + + def get_external_ipv4_networks(self): + """Return the networks that are configured to route northbound. + + :returns: A list of network ``munch.Munch`` if one is found + """ + self._find_interesting_networks() + return self._external_ipv4_networks + + def get_external_ipv4_floating_networks(self): + """Return the networks that are configured to route northbound. + + :returns: A list of network ``munch.Munch`` if one is found + """ + self._find_interesting_networks() + return self._external_ipv4_floating_networks + + def get_internal_ipv4_networks(self): + """Return the networks that are configured to not route northbound. + + :returns: A list of network ``munch.Munch`` if one is found + """ + self._find_interesting_networks() + return self._internal_ipv4_networks + + def get_external_ipv6_networks(self): + """Return the networks that are configured to route northbound. + + :returns: A list of network ``munch.Munch`` if one is found + """ + self._find_interesting_networks() + return self._external_ipv6_networks + + def get_internal_ipv6_networks(self): + """Return the networks that are configured to not route northbound. + + :returns: A list of network ``munch.Munch`` if one is found + """ + self._find_interesting_networks() + return self._internal_ipv6_networks diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py new file mode 100644 index 000000000..0fb255083 --- /dev/null +++ b/openstack/cloud/_object_store.py @@ -0,0 +1,837 @@ +# 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 types so that we can reference ListType in sphinx param declarations. +# We can't just use list, because sphinx gets confused by +# openstack.resource.Resource.list and openstack.resource2.Resource.list +import collections +import concurrent.futures +import hashlib +import json +import os +import six +import types # noqa + +from six.moves import urllib + +import keystoneauth1.exceptions + +from openstack.cloud import exc +from openstack.cloud import _normalize +from openstack.cloud import _utils +from openstack import exceptions +from openstack import proxy + + +DEFAULT_OBJECT_SEGMENT_SIZE = 1073741824 # 1GB +# This halves the current default for Swift +DEFAULT_MAX_FILE_SIZE = (5 * 1024 * 1024 * 1024 + 2) / 2 + + +OBJECT_CONTAINER_ACLS = { + 'public': '.r:*,.rlistings', + 'private': '', +} + + +class ObjectStoreCloudMixin(_normalize.Normalizer): + + def __init__(self): + self.__pool_executor = None + + @property + def _object_store_client(self): + if 'object-store' not in self._raw_clients: + raw_client = self._get_raw_client('object-store') + self._raw_clients['object-store'] = raw_client + return self._raw_clients['object-store'] + + @property + def _pool_executor(self): + if not self.__pool_executor: + # TODO(mordred) Make this configurable - and probably use Futurist + # instead of concurrent.futures so that people using Eventlet will + # be happier. + self.__pool_executor = concurrent.futures.ThreadPoolExecutor( + max_workers=5) + return self.__pool_executor + + def list_containers(self, full_listing=True, prefix=None): + """List containers. + + :param full_listing: Ignored. Present for backwards compat + + :returns: list of Munch of the container objects + + :raises: OpenStackCloudException on operation error. + """ + params = dict(format='json', prefix=prefix) + response = self.object_store.get('/', params=params) + return self._get_and_munchify(None, proxy._json_response(response)) + + def search_containers(self, name=None, filters=None): + """Search containers. + + :param string name: container name. + :param filters: a dict containing additional filters to use. + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: a list of ``munch.Munch`` containing the containers. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + containers = self.list_containers() + return _utils._filter_list(containers, name, filters) + + def get_container(self, name, skip_cache=False): + """Get metadata about a container. + + :param str name: + Name of the container to get metadata for. + :param bool skip_cache: + Ignore the cache of container metadata for this container.o + Defaults to ``False``. + """ + if skip_cache or name not in self._container_cache: + try: + response = self.object_store.head(name) + exceptions.raise_from_response(response) + self._container_cache[name] = response.headers + except exc.OpenStackCloudHTTPError as e: + if e.response.status_code == 404: + return None + raise + return self._container_cache[name] + + def create_container(self, name, public=False): + """Create an object-store container. + + :param str name: + Name of the container to create. + :param bool public: + Whether to set this container to be public. Defaults to ``False``. + """ + container = self.get_container(name) + if container: + return container + exceptions.raise_from_response(self.object_store.put(name)) + if public: + self.set_container_access(name, 'public') + return self.get_container(name, skip_cache=True) + + def delete_container(self, name): + """Delete an object-store container. + + :param str name: Name of the container to delete. + """ + try: + exceptions.raise_from_response(self.object_store.delete(name)) + self._container_cache.pop(name, None) + return True + except exc.OpenStackCloudHTTPError as e: + if e.response.status_code == 404: + return False + if e.response.status_code == 409: + raise exc.OpenStackCloudException( + 'Attempt to delete container {container} failed. The' + ' container is not empty. Please delete the objects' + ' inside it before deleting the container'.format( + container=name)) + raise + + def update_container(self, name, headers): + """Update the metadata in a container. + + :param str name: + Name of the container to create. + :param dict headers: + Key/Value headers to set on the container. + """ + """Update the metadata in a container. + + :param str name: + Name of the container to update. + :param dict headers: + Key/Value headers to set on the container. + """ + exceptions.raise_from_response( + self.object_store.post(name, headers=headers)) + + def set_container_access(self, name, access): + """Set the access control list on a container. + + :param str name: + Name of the container. + :param str access: + ACL string to set on the container. Can also be ``public`` + or ``private`` which will be translated into appropriate ACL + strings. + """ + if access not in OBJECT_CONTAINER_ACLS: + raise exc.OpenStackCloudException( + "Invalid container access specified: %s. Must be one of %s" + % (access, list(OBJECT_CONTAINER_ACLS.keys()))) + header = {'x-container-read': OBJECT_CONTAINER_ACLS[access]} + self.update_container(name, header) + + def get_container_access(self, name): + """Get the control list from a container. + + :param str name: Name of the container. + """ + container = self.get_container(name, skip_cache=True) + if not container: + raise exc.OpenStackCloudException("Container not found: %s" % name) + acl = container.get('x-container-read', '') + for key, value in OBJECT_CONTAINER_ACLS.items(): + # Convert to string for the comparison because swiftclient + # returns byte values as bytes sometimes and apparently == + # on bytes doesn't work like you'd think + if str(acl) == str(value): + return key + raise exc.OpenStackCloudException( + "Could not determine container access for ACL: %s." % acl) + + def _get_file_hashes(self, filename): + file_key = "{filename}:{mtime}".format( + filename=filename, + mtime=os.stat(filename).st_mtime) + if file_key not in self._file_hash_cache: + self.log.debug( + 'Calculating hashes for %(filename)s', {'filename': filename}) + md5 = hashlib.md5() + sha256 = hashlib.sha256() + with open(filename, 'rb') as file_obj: + for chunk in iter(lambda: file_obj.read(8192), b''): + md5.update(chunk) + sha256.update(chunk) + self._file_hash_cache[file_key] = dict( + md5=md5.hexdigest(), sha256=sha256.hexdigest()) + self.log.debug( + "Image file %(filename)s md5:%(md5)s sha256:%(sha256)s", + {'filename': filename, + 'md5': self._file_hash_cache[file_key]['md5'], + 'sha256': self._file_hash_cache[file_key]['sha256']}) + return (self._file_hash_cache[file_key]['md5'], + self._file_hash_cache[file_key]['sha256']) + + @_utils.cache_on_arguments() + def get_object_capabilities(self): + """Get infomation about the object-storage service + + The object-storage service publishes a set of capabilities that + include metadata about maximum values and thresholds. + """ + # The endpoint in the catalog has version and project-id in it + # To get capabilities, we have to disassemble and reassemble the URL + # This logic is taken from swiftclient + endpoint = urllib.parse.urlparse(self.object_store.get_endpoint()) + url = "{scheme}://{netloc}/info".format( + scheme=endpoint.scheme, netloc=endpoint.netloc) + + return proxy._json_response(self.object_store.get(url)) + + def get_object_segment_size(self, segment_size): + """Get a segment size that will work given capabilities""" + if segment_size is None: + segment_size = DEFAULT_OBJECT_SEGMENT_SIZE + min_segment_size = 0 + try: + caps = self.get_object_capabilities() + except exc.OpenStackCloudHTTPError as e: + if e.response.status_code in (404, 412): + # Clear the exception so that it doesn't linger + # and get reported as an Inner Exception later + _utils._exc_clear() + server_max_file_size = DEFAULT_MAX_FILE_SIZE + self.log.info( + "Swift capabilities not supported. " + "Using default max file size.") + else: + raise + else: + server_max_file_size = caps.get('swift', {}).get('max_file_size', + 0) + min_segment_size = caps.get('slo', {}).get('min_segment_size', 0) + + if segment_size > server_max_file_size: + return server_max_file_size + if segment_size < min_segment_size: + return min_segment_size + return segment_size + + def is_object_stale( + self, container, name, filename, file_md5=None, file_sha256=None): + """Check to see if an object matches the hashes of a file. + + :param container: Name of the container. + :param name: Name of the object. + :param filename: Path to the file. + :param file_md5: + Pre-calculated md5 of the file contents. Defaults to None which + means calculate locally. + :param file_sha256: + Pre-calculated sha256 of the file contents. Defaults to None which + means calculate locally. + """ + metadata = self.get_object_metadata(container, name) + if not metadata: + self.log.debug( + "swift stale check, no object: {container}/{name}".format( + container=container, name=name)) + return True + + if not (file_md5 or file_sha256): + (file_md5, file_sha256) = self._get_file_hashes(filename) + md5_key = metadata.get( + self._OBJECT_MD5_KEY, metadata.get(self._SHADE_OBJECT_MD5_KEY, '')) + sha256_key = metadata.get( + self._OBJECT_SHA256_KEY, metadata.get( + self._SHADE_OBJECT_SHA256_KEY, '')) + up_to_date = self._hashes_up_to_date( + md5=file_md5, sha256=file_sha256, + md5_key=md5_key, sha256_key=sha256_key) + + if not up_to_date: + self.log.debug( + "swift checksum mismatch: " + " %(filename)s!=%(container)s/%(name)s", + {'filename': filename, 'container': container, 'name': name}) + return True + + self.log.debug( + "swift object up to date: %(container)s/%(name)s", + {'container': container, 'name': name}) + return False + + def create_directory_marker_object(self, container, name, **headers): + """Create a zero-byte directory marker object + + .. note:: + + This method is not needed in most cases. Modern swift does not + require directory marker objects. However, some swift installs may + need these. + + When using swift Static Web and Web Listings to serve static content + one may need to create a zero-byte object to represent each + "directory". Doing so allows Web Listings to generate an index of the + objects inside of it, and allows Static Web to render index.html + "files" that are "inside" the directory. + + :param container: The name of the container. + :param name: Name for the directory marker object within the container. + :param headers: These will be passed through to the object creation + API as HTTP Headers. + """ + headers['content-type'] = 'application/directory' + + return self.create_object( + container, + name, + data='', + generate_checksums=False, + **headers) + + def create_object( + self, container, name, filename=None, + md5=None, sha256=None, segment_size=None, + use_slo=True, metadata=None, + generate_checksums=None, data=None, + **headers): + """Create a file object. + + Automatically uses large-object segments if needed. + + :param container: The name of the container to store the file in. + This container will be created if it does not exist already. + :param name: Name for the object within the container. + :param filename: The path to the local file whose contents will be + uploaded. Mutually exclusive with data. + :param data: The content to upload to the object. Mutually exclusive + with filename. + :param md5: A hexadecimal md5 of the file. (Optional), if it is known + and can be passed here, it will save repeating the expensive md5 + process. It is assumed to be accurate. + :param sha256: A hexadecimal sha256 of the file. (Optional) See md5. + :param segment_size: Break the uploaded object into segments of this + many bytes. (Optional) Shade will attempt to discover the maximum + value for this from the server if it is not specified, or will use + a reasonable default. + :param headers: These will be passed through to the object creation + API as HTTP Headers. + :param use_slo: If the object is large enough to need to be a Large + Object, use a static rather than dynamic object. Static Objects + will delete segment objects when the manifest object is deleted. + (optional, defaults to True) + :param generate_checksums: Whether to generate checksums on the client + side that get added to headers for later prevention of double + uploads of identical data. (optional, defaults to True) + :param metadata: This dict will get changed into headers that set + metadata of the object + + :raises: ``OpenStackCloudException`` on operation error. + """ + if data is not None and filename: + raise ValueError( + "Both filename and data given. Please choose one.") + if data is not None and not name: + raise ValueError( + "name is a required parameter when data is given") + if data is not None and generate_checksums: + raise ValueError( + "checksums cannot be generated with data parameter") + if generate_checksums is None: + if data is not None: + generate_checksums = False + else: + generate_checksums = True + + if not metadata: + metadata = {} + + if not filename and data is None: + filename = name + + if generate_checksums and (md5 is None or sha256 is None): + (md5, sha256) = self._get_file_hashes(filename) + if md5: + headers[self._OBJECT_MD5_KEY] = md5 or '' + if sha256: + headers[self._OBJECT_SHA256_KEY] = sha256 or '' + for (k, v) in metadata.items(): + headers['x-object-meta-' + k] = v + + endpoint = '{container}/{name}'.format(container=container, name=name) + + if data is not None: + self.log.debug( + "swift uploading data to %(endpoint)s", + {'endpoint': endpoint}) + + return self._upload_object_data(endpoint, data, headers) + + # segment_size gets used as a step value in a range call, so needs + # to be an int + if segment_size: + segment_size = int(segment_size) + segment_size = self.get_object_segment_size(segment_size) + file_size = os.path.getsize(filename) + + if self.is_object_stale(container, name, filename, md5, sha256): + + self.log.debug( + "swift uploading %(filename)s to %(endpoint)s", + {'filename': filename, 'endpoint': endpoint}) + + if file_size <= segment_size: + self._upload_object(endpoint, filename, headers) + else: + self._upload_large_object( + endpoint, filename, headers, + file_size, segment_size, use_slo) + + def _upload_object_data(self, endpoint, data, headers): + return proxy._json_response(self.object_store.put( + endpoint, headers=headers, data=data)) + + def _upload_object(self, endpoint, filename, headers): + return proxy._json_response(self.object_store.put( + endpoint, headers=headers, data=open(filename, 'rb'))) + + def _get_file_segments(self, endpoint, filename, file_size, segment_size): + # Use an ordered dict here so that testing can replicate things + segments = collections.OrderedDict() + for (index, offset) in enumerate(range(0, file_size, segment_size)): + remaining = file_size - (index * segment_size) + segment = _utils.FileSegment( + filename, offset, + segment_size if segment_size < remaining else remaining) + name = '{endpoint}/{index:0>6}'.format( + endpoint=endpoint, index=index) + segments[name] = segment + return segments + + def _object_name_from_url(self, url): + '''Get container_name/object_name from the full URL called. + + Remove the Swift endpoint from the front of the URL, and remove + the leaving / that will leave behind.''' + endpoint = self.object_store.get_endpoint() + object_name = url.replace(endpoint, '') + if object_name.startswith('/'): + object_name = object_name[1:] + return object_name + + def _add_etag_to_manifest(self, segment_results, manifest): + for result in segment_results: + if 'Etag' not in result.headers: + continue + name = self._object_name_from_url(result.url) + for entry in manifest: + if entry['path'] == '/{name}'.format(name=name): + entry['etag'] = result.headers['Etag'] + + def _upload_large_object( + self, endpoint, filename, + headers, file_size, segment_size, use_slo): + # If the object is big, we need to break it up into segments that + # are no larger than segment_size, upload each of them individually + # and then upload a manifest object. The segments can be uploaded in + # parallel, so we'll use the async feature of the TaskManager. + + segment_futures = [] + segment_results = [] + retry_results = [] + retry_futures = [] + manifest = [] + + # Get an OrderedDict with keys being the swift location for the + # segment, the value a FileSegment file-like object that is a + # slice of the data for the segment. + segments = self._get_file_segments( + endpoint, filename, file_size, segment_size) + + # Schedule the segments for upload + for name, segment in segments.items(): + # Async call to put - schedules execution and returns a future + segment_future = self._pool_executor.submit( + self.object_store.put, + name, headers=headers, data=segment, + raise_exc=False) + segment_futures.append(segment_future) + # TODO(mordred) Collect etags from results to add to this manifest + # dict. Then sort the list of dicts by path. + manifest.append(dict( + path='/{name}'.format(name=name), + size_bytes=segment.length)) + + # Try once and collect failed results to retry + segment_results, retry_results = self._wait_for_futures( + segment_futures, raise_on_error=False) + + self._add_etag_to_manifest(segment_results, manifest) + + for result in retry_results: + # Grab the FileSegment for the failed upload so we can retry + name = self._object_name_from_url(result.url) + segment = segments[name] + segment.seek(0) + # Async call to put - schedules execution and returns a future + segment_future = self._pool_executor.submit( + self.object_store.put, + name, headers=headers, data=segment) + # TODO(mordred) Collect etags from results to add to this manifest + # dict. Then sort the list of dicts by path. + retry_futures.append(segment_future) + + # If any segments fail the second time, just throw the error + segment_results, retry_results = self._wait_for_futures( + retry_futures, raise_on_error=True) + + self._add_etag_to_manifest(segment_results, manifest) + + if use_slo: + return self._finish_large_object_slo(endpoint, headers, manifest) + else: + return self._finish_large_object_dlo(endpoint, headers) + + def _finish_large_object_slo(self, endpoint, headers, manifest): + # TODO(mordred) send an etag of the manifest, which is the md5sum + # of the concatenation of the etags of the results + headers = headers.copy() + return self._object_store_client.put( + endpoint, + params={'multipart-manifest': 'put'}, + headers=headers, data=json.dumps(manifest)) + + def _finish_large_object_dlo(self, endpoint, headers): + headers = headers.copy() + headers['X-Object-Manifest'] = endpoint + return self._object_store_client.put(endpoint, headers=headers) + + def update_object(self, container, name, metadata=None, **headers): + """Update the metadata of an object + + :param container: The name of the container the object is in + :param name: Name for the object within the container. + :param metadata: This dict will get changed into headers that set + metadata of the object + :param headers: These will be passed through to the object update + API as HTTP Headers. + + :raises: ``OpenStackCloudException`` on operation error. + """ + if not metadata: + metadata = {} + + metadata_headers = {} + + for (k, v) in metadata.items(): + metadata_headers['x-object-meta-' + k] = v + + headers = dict(headers, **metadata_headers) + + return self._object_store_client.post( + '{container}/{object}'.format( + container=container, object=name), + headers=headers) + + def list_objects(self, container, full_listing=True, prefix=None): + """List objects. + + :param container: Name of the container to list objects in. + :param full_listing: Ignored. Present for backwards compat + :param string prefix: + only objects with this prefix will be returned. + (optional) + + :returns: list of Munch of the objects + + :raises: OpenStackCloudException on operation error. + """ + params = dict(format='json', prefix=prefix) + data = self._object_store_client.get(container, params=params) + return self._get_and_munchify(None, data) + + def search_objects(self, container, name=None, filters=None): + """Search objects. + + :param string name: object name. + :param filters: a dict containing additional filters to use. + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: a list of ``munch.Munch`` containing the objects. + + :raises: ``OpenStackCloudException``: if something goes wrong during + the OpenStack API call. + """ + objects = self.list_objects(container) + return _utils._filter_list(objects, name, filters) + + def delete_object(self, container, name, meta=None): + """Delete an object from a container. + + :param string container: Name of the container holding the object. + :param string name: Name of the object to delete. + :param dict meta: Metadata for the object in question. (optional, will + be fetched if not provided) + + :returns: True if delete succeeded, False if the object was not found. + + :raises: OpenStackCloudException on operation error. + """ + # TODO(mordred) DELETE for swift returns status in text/plain format + # like so: + # Number Deleted: 15 + # Number Not Found: 0 + # Response Body: + # Response Status: 200 OK + # Errors: + # We should ultimately do something with that + try: + if not meta: + meta = self.get_object_metadata(container, name) + if not meta: + return False + params = {} + if meta.get('X-Static-Large-Object', None) == 'True': + params['multipart-manifest'] = 'delete' + self._object_store_client.delete( + '{container}/{object}'.format( + container=container, object=name), + params=params) + return True + except exc.OpenStackCloudHTTPError: + return False + + def delete_autocreated_image_objects(self, container=None): + """Delete all objects autocreated for image uploads. + + This method should generally not be needed, as shade should clean up + the objects it uses for object-based image creation. If something + goes wrong and it is found that there are leaked objects, this method + can be used to delete any objects that shade has created on the user's + behalf in service of image uploads. + """ + if container is None: + container = self._OBJECT_AUTOCREATE_CONTAINER + # This method only makes sense on clouds that use tasks + if not self.image_api_use_tasks: + return False + + deleted = False + for obj in self.list_objects(container): + meta = self.get_object_metadata(container, obj['name']) + if meta.get( + self._OBJECT_AUTOCREATE_KEY, meta.get( + self._SHADE_OBJECT_AUTOCREATE_KEY)) == 'true': + if self.delete_object(container, obj['name'], meta): + deleted = True + return deleted + + def get_object_metadata(self, container, name): + try: + return self._object_store_client.head( + '{container}/{object}'.format( + container=container, object=name)).headers + except exc.OpenStackCloudException as e: + if e.response.status_code == 404: + return None + raise + + def get_object_raw(self, container, obj, query_string=None, stream=False): + """Get a raw response object for an object. + + :param string container: name of the container. + :param string obj: name of the object. + :param string query_string: + query args for uri. (delimiter, prefix, etc.) + :param bool stream: + Whether to stream the response or not. + + :returns: A `requests.Response` + :raises: OpenStackCloudException on operation error. + """ + endpoint = self._get_object_endpoint(container, obj, query_string) + return self._object_store_client.get(endpoint, stream=stream) + + def _get_object_endpoint(self, container, obj, query_string): + endpoint = '{container}/{object}'.format( + container=container, object=obj) + if query_string: + endpoint = '{endpoint}?{query_string}'.format( + endpoint=endpoint, query_string=query_string) + return endpoint + + def stream_object( + self, container, obj, query_string=None, resp_chunk_size=1024): + """Download the content via a streaming iterator. + + :param string container: name of the container. + :param string obj: name of the object. + :param string query_string: + query args for uri. (delimiter, prefix, etc.) + :param int resp_chunk_size: + chunk size of data to read. Only used if the results are + + :returns: + An iterator over the content or None if the object is not found. + :raises: OpenStackCloudException on operation error. + """ + try: + with self.get_object_raw( + container, obj, query_string=query_string) as response: + for ret in response.iter_content(chunk_size=resp_chunk_size): + yield ret + except exc.OpenStackCloudHTTPError as e: + if e.response.status_code == 404: + return + raise + + def get_object(self, container, obj, query_string=None, + resp_chunk_size=1024, outfile=None, stream=False): + """Get the headers and body of an object + + :param string container: name of the container. + :param string obj: name of the object. + :param string query_string: + query args for uri. (delimiter, prefix, etc.) + :param int resp_chunk_size: + chunk size of data to read. Only used if the results are + being written to a file or stream is True. + (optional, defaults to 1k) + :param outfile: + Write the object to a file instead of returning the contents. + If this option is given, body in the return tuple will be None. + outfile can either be a file path given as a string, or a + File like object. + + :returns: Tuple (headers, body) of the object, or None if the object + is not found (404). + :raises: OpenStackCloudException on operation error. + """ + # TODO(mordred) implement resp_chunk_size + endpoint = self._get_object_endpoint(container, obj, query_string) + try: + get_stream = (outfile is not None) + with self._object_store_client.get( + endpoint, stream=get_stream) as response: + response_headers = { + k.lower(): v for k, v in response.headers.items()} + if outfile: + if isinstance(outfile, six.string_types): + outfile_handle = open(outfile, 'wb') + else: + outfile_handle = outfile + for chunk in response.iter_content( + resp_chunk_size, decode_unicode=False): + outfile_handle.write(chunk) + if isinstance(outfile, six.string_types): + outfile_handle.close() + else: + outfile_handle.flush() + return (response_headers, None) + else: + return (response_headers, response.text) + except exc.OpenStackCloudHTTPError as e: + if e.response.status_code == 404: + return None + raise + + def _wait_for_futures(self, futures, raise_on_error=True): + '''Collect results or failures from a list of running future tasks.''' + + results = [] + retries = [] + + # Check on each result as its thread finishes + for completed in concurrent.futures.as_completed(futures): + try: + result = completed.result() + exceptions.raise_from_response(result) + results.append(result) + except (keystoneauth1.exceptions.RetriableConnectionFailure, + exceptions.HttpException) as e: + error_text = "Exception processing async task: {}".format( + str(e)) + if raise_on_error: + self.log.exception(error_text) + raise + else: + self.log.debug(error_text) + # If we get an exception, put the result into a list so we + # can try again + retries.append(completed.result()) + return results, retries + + def _hashes_up_to_date(self, md5, sha256, md5_key, sha256_key): + '''Compare md5 and sha256 hashes for being up to date + + md5 and sha256 are the current values. + md5_key and sha256_key are the previous values. + ''' + up_to_date = False + if md5 and md5_key == md5: + up_to_date = True + if sha256 and sha256_key == sha256: + up_to_date = True + if md5 and md5_key != md5: + up_to_date = False + if sha256 and sha256_key != sha256: + up_to_date = False + return up_to_date diff --git a/openstack/cloud/_orchestration.py b/openstack/cloud/_orchestration.py new file mode 100644 index 000000000..bb3343ad6 --- /dev/null +++ b/openstack/cloud/_orchestration.py @@ -0,0 +1,275 @@ +# 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 types so that we can reference ListType in sphinx param declarations. +# We can't just use list, because sphinx gets confused by +# openstack.resource.Resource.list and openstack.resource2.Resource.list +import types # noqa + +from openstack.cloud import exc +from openstack.cloud._heat import event_utils +from openstack.cloud._heat import template_utils +from openstack.cloud import _normalize +from openstack.cloud import _utils + + +def _no_pending_stacks(stacks): + """If there are any stacks not in a steady state, don't cache""" + for stack in stacks: + status = stack['stack_status'] + if '_COMPLETE' not in status and '_FAILED' not in status: + return False + return True + + +class OrchestrationCloudMixin(_normalize.Normalizer): + + @property + def _orchestration_client(self): + if 'orchestration' not in self._raw_clients: + raw_client = self._get_raw_client('orchestration') + self._raw_clients['orchestration'] = raw_client + return self._raw_clients['orchestration'] + + def get_template_contents( + self, template_file=None, template_url=None, + template_object=None, files=None): + try: + return template_utils.get_template_contents( + template_file=template_file, template_url=template_url, + template_object=template_object, files=files) + except Exception as e: + raise exc.OpenStackCloudException( + "Error in processing template files: %s" % str(e)) + + def create_stack( + self, name, tags=None, + template_file=None, template_url=None, + template_object=None, files=None, + rollback=True, + wait=False, timeout=3600, + environment_files=None, + **parameters): + """Create a stack. + + :param string name: Name of the stack. + :param tags: List of tag(s) of the stack. (optional) + :param string template_file: Path to the template. + :param string template_url: URL of template. + :param string template_object: URL to retrieve template object. + :param dict files: dict of additional file content to include. + :param boolean rollback: Enable rollback on create failure. + :param boolean wait: Whether to wait for the delete to finish. + :param int timeout: Stack create timeout in seconds. + :param environment_files: Paths to environment files to apply. + + Other arguments will be passed as stack parameters which will take + precedence over any parameters specified in the environments. + + Only one of template_file, template_url, template_object should be + specified. + + :returns: a dict containing the stack description + + :raises: ``OpenStackCloudException`` if something goes wrong during + the OpenStack API call + """ + envfiles, env = template_utils.process_multiple_environments_and_files( + env_paths=environment_files) + tpl_files, template = template_utils.get_template_contents( + template_file=template_file, + template_url=template_url, + template_object=template_object, + files=files) + params = dict( + stack_name=name, + tags=tags, + disable_rollback=not rollback, + parameters=parameters, + template=template, + files=dict(list(tpl_files.items()) + list(envfiles.items())), + environment=env, + timeout_mins=timeout // 60, + ) + self._orchestration_client.post('/stacks', json=params) + if wait: + event_utils.poll_for_events(self, stack_name=name, + action='CREATE') + return self.get_stack(name) + + def update_stack( + self, name_or_id, + template_file=None, template_url=None, + template_object=None, files=None, + rollback=True, + wait=False, timeout=3600, + environment_files=None, + **parameters): + """Update a stack. + + :param string name_or_id: Name or ID of the stack to update. + :param string template_file: Path to the template. + :param string template_url: URL of template. + :param string template_object: URL to retrieve template object. + :param dict files: dict of additional file content to include. + :param boolean rollback: Enable rollback on update failure. + :param boolean wait: Whether to wait for the delete to finish. + :param int timeout: Stack update timeout in seconds. + :param environment_files: Paths to environment files to apply. + + Other arguments will be passed as stack parameters which will take + precedence over any parameters specified in the environments. + + Only one of template_file, template_url, template_object should be + specified. + + :returns: a dict containing the stack description + + :raises: ``OpenStackCloudException`` if something goes wrong during + the OpenStack API calls + """ + envfiles, env = template_utils.process_multiple_environments_and_files( + env_paths=environment_files) + tpl_files, template = template_utils.get_template_contents( + template_file=template_file, + template_url=template_url, + template_object=template_object, + files=files) + params = dict( + disable_rollback=not rollback, + parameters=parameters, + template=template, + files=dict(list(tpl_files.items()) + list(envfiles.items())), + environment=env, + timeout_mins=timeout // 60, + ) + if wait: + # find the last event to use as the marker + events = event_utils.get_events( + self, name_or_id, event_args={'sort_dir': 'desc', 'limit': 1}) + marker = events[0].id if events else None + + self._orchestration_client.put( + '/stacks/{name_or_id}'.format(name_or_id=name_or_id), json=params) + if wait: + event_utils.poll_for_events(self, + name_or_id, + action='UPDATE', + marker=marker) + return self.get_stack(name_or_id) + + def delete_stack(self, name_or_id, wait=False): + """Delete a stack + + :param string name_or_id: Stack name or ID. + :param boolean wait: Whether to wait for the delete to finish + + :returns: True if delete succeeded, False if the stack was not found. + + :raises: ``OpenStackCloudException`` if something goes wrong during + the OpenStack API call + """ + stack = self.get_stack(name_or_id, resolve_outputs=False) + if stack is None: + self.log.debug("Stack %s not found for deleting", name_or_id) + return False + + if wait: + # find the last event to use as the marker + events = event_utils.get_events( + self, name_or_id, event_args={'sort_dir': 'desc', 'limit': 1}) + marker = events[0].id if events else None + + self._orchestration_client.delete( + '/stacks/{id}'.format(id=stack['id'])) + + if wait: + try: + event_utils.poll_for_events(self, + stack_name=name_or_id, + action='DELETE', + marker=marker) + except exc.OpenStackCloudHTTPError: + pass + stack = self.get_stack(name_or_id, resolve_outputs=False) + if stack and stack['stack_status'] == 'DELETE_FAILED': + raise exc.OpenStackCloudException( + "Failed to delete stack {id}: {reason}".format( + id=name_or_id, reason=stack['stack_status_reason'])) + + return True + + def search_stacks(self, name_or_id=None, filters=None): + """Search stacks. + + :param name_or_id: Name or ID of the desired stack. + :param filters: a dict containing additional filters to use. e.g. + {'stack_status': 'CREATE_COMPLETE'} + + :returns: a list of ``munch.Munch`` containing the stack description. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + OpenStack API call. + """ + stacks = self.list_stacks() + return _utils._filter_list(stacks, name_or_id, filters) + + @_utils.cache_on_arguments(should_cache_fn=_no_pending_stacks) + def list_stacks(self): + """List all stacks. + + :returns: a list of ``munch.Munch`` containing the stack description. + + :raises: ``OpenStackCloudException`` if something goes wrong during the + OpenStack API call. + """ + data = self._orchestration_client.get( + '/stacks', error_message="Error fetching stack list") + return self._normalize_stacks( + self._get_and_munchify('stacks', data)) + + def get_stack(self, name_or_id, filters=None, resolve_outputs=True): + """Get exactly one stack. + + :param name_or_id: Name or ID of the desired stack. + :param filters: a dict containing additional filters to use. e.g. + {'stack_status': 'CREATE_COMPLETE'} + :param resolve_outputs: If True, then outputs for this + stack will be resolved + + :returns: a ``munch.Munch`` containing the stack description + + :raises: ``OpenStackCloudException`` if something goes wrong during the + OpenStack API call or if multiple matches are found. + """ + + def _search_one_stack(name_or_id=None, filters=None): + # stack names are mandatory and enforced unique in the project + # so a StackGet can always be used for name or ID. + try: + url = '/stacks/{name_or_id}'.format(name_or_id=name_or_id) + if not resolve_outputs: + url = '{url}?resolve_outputs=False'.format(url=url) + data = self._orchestration_client.get( + url, + error_message="Error fetching stack") + stack = self._get_and_munchify('stack', data) + # Treat DELETE_COMPLETE stacks as a NotFound + if stack['stack_status'] == 'DELETE_COMPLETE': + return [] + except exc.OpenStackCloudURINotFound: + return [] + stack = self._normalize_stack(stack) + return _utils._filter_list([stack], name_or_id, filters) + + return _utils._get_entity( + self, _search_one_stack, name_or_id, filters) diff --git a/openstack/cloud/_security_group.py b/openstack/cloud/_security_group.py new file mode 100644 index 000000000..032769654 --- /dev/null +++ b/openstack/cloud/_security_group.py @@ -0,0 +1,387 @@ +# 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 types so that we can reference ListType in sphinx param declarations. +# We can't just use list, because sphinx gets confused by +# openstack.resource.Resource.list and openstack.resource2.Resource.list +# import jsonpatch +import types # noqa + +from openstack.cloud import exc +from openstack.cloud import _normalize +from openstack.cloud import _utils +from openstack import exceptions +from openstack import proxy + + +class SecurityGroupCloudMixin(_normalize.Normalizer): + + def __init__(self): + self.secgroup_source = self.config.config['secgroup_source'] + + def get_security_group(self, name_or_id, filters=None): + """Get a security group by name or ID. + + :param name_or_id: Name or ID of the security group. + :param filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: A security group ``munch.Munch`` or None if no matching + security group is found. + + """ + return _utils._get_entity( + self, 'security_group', name_or_id, filters) + + def get_security_group_by_id(self, id): + """ Get a security group by ID + + :param id: ID of the security group. + :returns: A security group ``munch.Munch``. + """ + if not self._has_secgroups(): + raise exc.OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + error_message = ("Error getting security group with" + " ID {id}".format(id=id)) + if self._use_neutron_secgroups(): + resp = self.network.get('/security-groups/{id}'.format(id=id)) + data = proxy._json_response(resp, error_message=error_message) + else: + data = proxy._json_response( + self.compute.get( + '/os-security-groups/{id}'.format(id=id)), + error_message=error_message) + return self._normalize_secgroup( + self._get_and_munchify('security_group', data)) + + def create_security_group(self, name, description, project_id=None): + """Create a new security group + + :param string name: A name for the security group. + :param string description: Describes the security group. + :param string project_id: + Specify the project ID this security group will be created + on (admin-only). + + :returns: A ``munch.Munch`` representing the new security group. + + :raises: OpenStackCloudException on operation error. + :raises: OpenStackCloudUnavailableFeature if security groups are + not supported on this cloud. + """ + + # Security groups not supported + if not self._has_secgroups(): + raise exc.OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + + data = [] + security_group_json = { + 'security_group': { + 'name': name, 'description': description + }} + if project_id is not None: + security_group_json['security_group']['tenant_id'] = project_id + if self._use_neutron_secgroups(): + data = proxy._json_response( + self.network.post( + '/security-groups.json', + json=security_group_json), + error_message="Error creating security group {0}".format(name)) + else: + data = proxy._json_response(self.compute.post( + '/os-security-groups', json=security_group_json)) + return self._normalize_secgroup( + self._get_and_munchify('security_group', data)) + + def delete_security_group(self, name_or_id): + """Delete a security group + + :param string name_or_id: The name or unique ID of the security group. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException on operation error. + :raises: OpenStackCloudUnavailableFeature if security groups are + not supported on this cloud. + """ + # Security groups not supported + if not self._has_secgroups(): + raise exc.OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + + # TODO(mordred): Let's come back and stop doing a GET before we do + # the delete. + secgroup = self.get_security_group(name_or_id) + if secgroup is None: + self.log.debug('Security group %s not found for deleting', + name_or_id) + return False + + if self._use_neutron_secgroups(): + exceptions.raise_from_response( + self.network.delete( + '/security-groups/{sg_id}.json'.format( + sg_id=secgroup['id'])), + error_message="Error deleting security group {0}".format( + name_or_id) + ) + return True + + else: + proxy._json_response(self.compute.delete( + '/os-security-groups/{id}'.format(id=secgroup['id']))) + return True + + @_utils.valid_kwargs('name', 'description') + def update_security_group(self, name_or_id, **kwargs): + """Update a security group + + :param string name_or_id: Name or ID of the security group to update. + :param string name: New name for the security group. + :param string description: New description for the security group. + + :returns: A ``munch.Munch`` describing the updated security group. + + :raises: OpenStackCloudException on operation error. + """ + # Security groups not supported + if not self._has_secgroups(): + raise exc.OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + + group = self.get_security_group(name_or_id) + + if group is None: + raise exc.OpenStackCloudException( + "Security group %s not found." % name_or_id) + + if self._use_neutron_secgroups(): + data = proxy._json_response( + self.network.put( + '/security-groups/{sg_id}.json'.format(sg_id=group['id']), + json={'security_group': kwargs}), + error_message="Error updating security group {0}".format( + name_or_id)) + else: + for key in ('name', 'description'): + kwargs.setdefault(key, group[key]) + data = proxy._json_response( + self.compute.put( + '/os-security-groups/{id}'.format(id=group['id']), + json={'security_group': kwargs})) + return self._normalize_secgroup( + self._get_and_munchify('security_group', data)) + + def create_security_group_rule(self, + secgroup_name_or_id, + port_range_min=None, + port_range_max=None, + protocol=None, + remote_ip_prefix=None, + remote_group_id=None, + direction='ingress', + ethertype='IPv4', + project_id=None): + """Create a new security group rule + + :param string secgroup_name_or_id: + The security group name or ID to associate with this security + group rule. If a non-unique group name is given, an exception + is raised. + :param int port_range_min: + The minimum port number in the range that is matched by the + security group rule. If the protocol is TCP or UDP, this value + must be less than or equal to the port_range_max attribute value. + If nova is used by the cloud provider for security groups, then + a value of None will be transformed to -1. + :param int port_range_max: + The maximum port number in the range that is matched by the + security group rule. The port_range_min attribute constrains the + port_range_max attribute. If nova is used by the cloud provider + for security groups, then a value of None will be transformed + to -1. + :param string protocol: + The protocol that is matched by the security group rule. Valid + values are None, tcp, udp, and icmp. + :param string remote_ip_prefix: + The remote IP prefix to be associated with this security group + rule. This attribute matches the specified IP prefix as the + source IP address of the IP packet. + :param string remote_group_id: + The remote group ID to be associated with this security group + rule. + :param string direction: + Ingress or egress: The direction in which the security group + rule is applied. For a compute instance, an ingress security + group rule is applied to incoming (ingress) traffic for that + instance. An egress rule is applied to traffic leaving the + instance. + :param string ethertype: + Must be IPv4 or IPv6, and addresses represented in CIDR must + match the ingress or egress rules. + :param string project_id: + Specify the project ID this security group will be created + on (admin-only). + + :returns: A ``munch.Munch`` representing the new security group rule. + + :raises: OpenStackCloudException on operation error. + """ + # Security groups not supported + if not self._has_secgroups(): + raise exc.OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + + secgroup = self.get_security_group(secgroup_name_or_id) + if not secgroup: + raise exc.OpenStackCloudException( + "Security group %s not found." % secgroup_name_or_id) + + if self._use_neutron_secgroups(): + # NOTE: Nova accepts -1 port numbers, but Neutron accepts None + # as the equivalent value. + rule_def = { + 'security_group_id': secgroup['id'], + 'port_range_min': + None if port_range_min == -1 else port_range_min, + 'port_range_max': + None if port_range_max == -1 else port_range_max, + 'protocol': protocol, + 'remote_ip_prefix': remote_ip_prefix, + 'remote_group_id': remote_group_id, + 'direction': direction, + 'ethertype': ethertype + } + if project_id is not None: + rule_def['tenant_id'] = project_id + + data = proxy._json_response( + self.network.post( + '/security-group-rules.json', + json={'security_group_rule': rule_def}), + error_message="Error creating security group rule") + else: + # NOTE: Neutron accepts None for protocol. Nova does not. + if protocol is None: + raise exc.OpenStackCloudException('Protocol must be specified') + + if direction == 'egress': + self.log.debug( + 'Rule creation failed: Nova does not support egress rules' + ) + raise exc.OpenStackCloudException( + 'No support for egress rules') + + # NOTE: Neutron accepts None for ports, but Nova requires -1 + # as the equivalent value for ICMP. + # + # For TCP/UDP, if both are None, Neutron allows this and Nova + # represents this as all ports (1-65535). Nova does not accept + # None values, so to hide this difference, we will automatically + # convert to the full port range. If only a single port value is + # specified, it will error as normal. + if protocol == 'icmp': + if port_range_min is None: + port_range_min = -1 + if port_range_max is None: + port_range_max = -1 + elif protocol in ['tcp', 'udp']: + if port_range_min is None and port_range_max is None: + port_range_min = 1 + port_range_max = 65535 + + security_group_rule_dict = dict(security_group_rule=dict( + parent_group_id=secgroup['id'], + ip_protocol=protocol, + from_port=port_range_min, + to_port=port_range_max, + cidr=remote_ip_prefix, + group_id=remote_group_id + )) + if project_id is not None: + security_group_rule_dict[ + 'security_group_rule']['tenant_id'] = project_id + data = proxy._json_response( + self.compute.post( + '/os-security-group-rules', + json=security_group_rule_dict + )) + return self._normalize_secgroup_rule( + self._get_and_munchify('security_group_rule', data)) + + def delete_security_group_rule(self, rule_id): + """Delete a security group rule + + :param string rule_id: The unique ID of the security group rule. + + :returns: True if delete succeeded, False otherwise. + + :raises: OpenStackCloudException on operation error. + :raises: OpenStackCloudUnavailableFeature if security groups are + not supported on this cloud. + """ + # Security groups not supported + if not self._has_secgroups(): + raise exc.OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + + if self._use_neutron_secgroups(): + try: + exceptions.raise_from_response( + self.network.delete( + '/security-group-rules/{sg_id}.json'.format( + sg_id=rule_id)), + error_message="Error deleting security group rule " + "{0}".format(rule_id)) + except exc.OpenStackCloudResourceNotFound: + return False + return True + + else: + try: + exceptions.raise_from_response( + self.compute.delete( + '/os-security-group-rules/{id}'.format(id=rule_id))) + except exc.OpenStackCloudResourceNotFound: + return False + + return True + + def _has_secgroups(self): + if not self.secgroup_source: + return False + else: + return self.secgroup_source.lower() in ('nova', 'neutron') + + def _use_neutron_secgroups(self): + return (self.has_service('network') + and self.secgroup_source == 'neutron') diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 83fdfcddb..8d65b0f00 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -9,21 +9,10 @@ # 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 base64 -import concurrent.futures import copy -import datetime import functools -import hashlib -import ipaddress -import iso8601 -import jsonpatch -import operator -import os import six -import threading -import time +# import threading # import types so that we can reference ListType in sphinx param declarations. # We can't just use list, because sphinx gets confused by # openstack.resource.Resource.list and openstack.resource2.Resource.list @@ -39,58 +28,28 @@ import keystoneauth1.session from openstack import _log -from openstack import exceptions +# from openstack import exceptions from openstack.cloud import exc -from openstack.cloud._heat import event_utils -from openstack.cloud._heat import template_utils -from openstack.cloud import _normalize +from openstack.cloud import _floating_ip +from openstack.cloud import _object_store +# from openstack.cloud import _normalize from openstack.cloud import meta from openstack.cloud import _utils import openstack.config -import openstack.config.defaults from openstack import proxy -from openstack import utils DEFAULT_SERVER_AGE = 5 DEFAULT_PORT_AGE = 5 DEFAULT_FLOAT_AGE = 5 -_CONFIG_DOC_URL = ( - "https://docs.openstack.org/openstacksdk/latest/" - "user/config/configuration.html") - - -OBJECT_CONTAINER_ACLS = { - 'public': '.r:*,.rlistings', - 'private': '', -} - - -def _no_pending_volumes(volumes): - """If there are any volumes not in a steady state, don't cache""" - for volume in volumes: - if volume['status'] not in ('available', 'error', 'in-use'): - return False - return True - - -def _no_pending_images(images): - """If there are any images not in a steady state, don't cache""" - for image in images: - if image.status not in ('active', 'deleted', 'killed'): - return False - return True - +_CONFIG_DOC_URL = _floating_ip._CONFIG_DOC_URL -def _no_pending_stacks(stacks): - """If there are any stacks not in a steady state, don't cache""" - for stack in stacks: - status = stack['stack_status'] - if '_COMPLETE' not in status and '_FAILED' not in status: - return False - return True +DEFAULT_OBJECT_SEGMENT_SIZE = _object_store.DEFAULT_OBJECT_SEGMENT_SIZE +# This halves the current default for Swift +DEFAULT_MAX_FILE_SIZE = _object_store.DEFAULT_MAX_FILE_SIZE +OBJECT_CONTAINER_ACLS = _object_store.OBJECT_CONTAINER_ACLS -class _OpenStackCloudMixin(_normalize.Normalizer): +class _OpenStackCloudMixin(object): """Represent a connection to an OpenStack Cloud. OpenStackCloud is the entry point for all cloud operations, regardless @@ -107,6 +66,7 @@ class _OpenStackCloudMixin(_normalize.Normalizer): _OBJECT_SHA256_KEY = 'x-object-meta-x-sdk-sha256' _OBJECT_AUTOCREATE_KEY = 'x-object-meta-x-sdk-autocreated' _OBJECT_AUTOCREATE_CONTAINER = 'images' + _IMAGE_MD5_KEY = 'owner_specified.openstack.md5' _IMAGE_SHA256_KEY = 'owner_specified.openstack.sha256' _IMAGE_OBJECT_KEY = 'owner_specified.openstack.object' @@ -125,36 +85,38 @@ class _OpenStackCloudMixin(_normalize.Normalizer): def __init__(self): + super(_OpenStackCloudMixin, self).__init__() + self.log = _log.setup_logging('openstack') self.name = self.config.name self.auth = self.config.get_auth_args() self.default_interface = self.config.get_interface() - self.private = self.config.config.get('private', False) - self.image_api_use_tasks = self.config.config['image_api_use_tasks'] - self.secgroup_source = self.config.config['secgroup_source'] + # self.private = self.config.config.get('private', False) + # self.image_api_use_tasks = self.config.config['image_api_use_tasks'] + # self.secgroup_source = self.config.config['secgroup_source'] self.force_ipv4 = self.config.force_ipv4 - self._external_ipv4_names = self.config.get_external_ipv4_networks() - self._internal_ipv4_names = self.config.get_internal_ipv4_networks() - self._external_ipv6_names = self.config.get_external_ipv6_networks() - self._internal_ipv6_names = self.config.get_internal_ipv6_networks() - self._nat_destination = self.config.get_nat_destination() - self._nat_source = self.config.get_nat_source() - self._default_network = self.config.get_default_network() - - self._floating_ip_source = self.config.config.get( - 'floating_ip_source') - if self._floating_ip_source: - if self._floating_ip_source.lower() == 'none': - self._floating_ip_source = None - else: - self._floating_ip_source = self._floating_ip_source.lower() - - self._use_external_network = self.config.config.get( - 'use_external_network', True) - self._use_internal_network = self.config.config.get( - 'use_internal_network', True) + # self._external_ipv4_names = self.config.get_external_ipv4_networks() + # self._internal_ipv4_names = self.config.get_internal_ipv4_networks() + # self._external_ipv6_names = self.config.get_external_ipv6_networks() + # self._internal_ipv6_names = self.config.get_internal_ipv6_networks() + # self._nat_destination = self.config.get_nat_destination() + # self._nat_source = self.config.get_nat_source() + # self._default_network = self.config.get_default_network() + + # self._floating_ip_source = self.config.config.get( + # 'floating_ip_source') + # if self._floating_ip_source: + # if self._floating_ip_source.lower() == 'none': + # self._floating_ip_source = None + # else: + # self._floating_ip_source = self._floating_ip_source.lower() + + # self._use_external_network = self.config.config.get( + # 'use_external_network', True) + # self._use_internal_network = self.config.config.get( + # 'use_internal_network', True) (self.verify, self.cert) = self.config.get_requests_verify_args() # Turn off urllib3 warnings about insecure certs if we have @@ -170,24 +132,24 @@ def __init__(self): self._disable_warnings = {} - self._servers = None - self._servers_time = 0 - self._servers_lock = threading.Lock() + # self._servers = None + # self._servers_time = 0 + # self._servers_lock = threading.Lock() - self._ports = None - self._ports_time = 0 - self._ports_lock = threading.Lock() + # self._ports = None + # self._ports_time = 0 + # self._ports_lock = threading.Lock() - self._floating_ips = None - self._floating_ips_time = 0 - self._floating_ips_lock = threading.Lock() - - self._floating_network_by_router = None - self._floating_network_by_router_run = False - self._floating_network_by_router_lock = threading.Lock() + # self._floating_ips = None + # self._floating_ips_time = 0 + # self._floating_ips_lock = threading.Lock() + # + # self._floating_network_by_router = None + # self._floating_network_by_router_run = False + # self._floating_network_by_router_lock = threading.Lock() - self._networks_lock = threading.Lock() - self._reset_network_caches() + # self._networks_lock = threading.Lock() + # self._reset_network_caches() cache_expiration_time = int(self.config.get_cache_expiration_time()) cache_class = self.config.get_cache_class() @@ -251,7 +213,7 @@ def invalidate(self): self._container_cache = dict() self._file_hash_cache = dict() - self.__pool_executor = None + # self.__pool_executor = None self._raw_clients = {} @@ -519,56 +481,12 @@ def _application_catalog_client(self): 'application-catalog') return self._raw_clients['application-catalog'] - @property - def _baremetal_client(self): - if 'baremetal' not in self._raw_clients: - client = self._get_raw_client('baremetal') - # Do this to force version discovery. We need to do that, because - # the endpoint-override trick we do for neutron because - # ironicclient just appends a /v1 won't work and will break - # keystoneauth - because ironic's versioned discovery endpoint - # is non-compliant and doesn't return an actual version dict. - client = self._get_versioned_client( - 'baremetal', min_version=1, max_version='1.latest') - self._raw_clients['baremetal'] = client - return self._raw_clients['baremetal'] - - @property - def _container_infra_client(self): - if 'container-infra' not in self._raw_clients: - self._raw_clients['container-infra'] = self._get_raw_client( - 'container-infra') - return self._raw_clients['container-infra'] - - @property - def _clustering_client(self): - if 'clustering' not in self._raw_clients: - clustering_client = self._get_versioned_client( - 'clustering', min_version=1, max_version='1.latest') - self._raw_clients['clustering'] = clustering_client - return self._raw_clients['clustering'] - @property def _database_client(self): if 'database' not in self._raw_clients: self._raw_clients['database'] = self._get_raw_client('database') return self._raw_clients['database'] - @property - def _dns_client(self): - if 'dns' not in self._raw_clients: - dns_client = self._get_versioned_client( - 'dns', min_version=2, max_version='2.latest') - self._raw_clients['dns'] = dns_client - return self._raw_clients['dns'] - - @property - def _identity_client(self): - if 'identity' not in self._raw_clients: - self._raw_clients['identity'] = self._get_versioned_client( - 'identity', min_version=2, max_version='3.latest') - return self._raw_clients['identity'] - @property def _raw_image_client(self): if 'raw-image' not in self._raw_clients: @@ -576,34 +494,6 @@ def _raw_image_client(self): self._raw_clients['raw-image'] = image_client return self._raw_clients['raw-image'] - @property - def _image_client(self): - if 'image' not in self._raw_clients: - self._raw_clients['image'] = self._get_versioned_client( - 'image', min_version=1, max_version='2.latest') - return self._raw_clients['image'] - - @property - def _object_store_client(self): - if 'object-store' not in self._raw_clients: - raw_client = self._get_raw_client('object-store') - self._raw_clients['object-store'] = raw_client - return self._raw_clients['object-store'] - - @property - def _orchestration_client(self): - if 'orchestration' not in self._raw_clients: - raw_client = self._get_raw_client('orchestration') - self._raw_clients['orchestration'] = raw_client - return self._raw_clients['orchestration'] - - @property - def _volume_client(self): - if 'block-storage' not in self._raw_clients: - client = self._get_raw_client('block-storage') - self._raw_clients['block-storage'] = client - return self._raw_clients['block-storage'] - def pprint(self, resource): """Wrapper around pprint that groks munch objects""" # import late since this is a utility function @@ -809,576 +699,12 @@ def _get_and_munchify(self, key, data): data = proxy._json_response(data) return meta.get_and_munchify(key, data) - @_utils.cache_on_arguments() - def list_projects(self, domain_id=None, name_or_id=None, filters=None): - """List projects. - - With no parameters, returns a full listing of all visible projects. - - :param domain_id: domain ID to scope the searched projects. - :param name_or_id: project name or ID. - :param filters: a dict containing additional filters to use - OR - A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - - :returns: a list of ``munch.Munch`` containing the projects - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. - """ - kwargs = dict( - filters=filters, - domain_id=domain_id) - if self._is_client_version('identity', 3): - kwargs['obj_name'] = 'project' - - pushdown, filters = _normalize._split_filters(**kwargs) - - try: - if self._is_client_version('identity', 3): - key = 'projects' - else: - key = 'tenants' - data = self._identity_client.get( - '/{endpoint}'.format(endpoint=key), params=pushdown) - projects = self._normalize_projects( - self._get_and_munchify(key, data)) - except Exception as e: - self.log.debug("Failed to list projects", exc_info=True) - raise exc.OpenStackCloudException(str(e)) - return _utils._filter_list(projects, name_or_id, filters) - - def search_projects(self, name_or_id=None, filters=None, domain_id=None): - '''Backwards compatibility method for search_projects - - search_projects originally had a parameter list that was name_or_id, - filters and list had domain_id first. This method exists in this form - to allow code written with positional parameter to still work. But - really, use keyword arguments. - ''' - return self.list_projects( - domain_id=domain_id, name_or_id=name_or_id, filters=filters) - - def get_project(self, name_or_id, filters=None, domain_id=None): - """Get exactly one project. - - :param name_or_id: project name or ID. - :param filters: a dict containing additional filters to use. - :param domain_id: domain ID (identity v3 only). - - :returns: a list of ``munch.Munch`` containing the project description. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. - """ - return _utils._get_entity(self, 'project', name_or_id, filters, - domain_id=domain_id) - - @_utils.valid_kwargs('description') - def update_project(self, name_or_id, enabled=None, domain_id=None, - **kwargs): - with _utils.shade_exceptions( - "Error in updating project {project}".format( - project=name_or_id)): - proj = self.get_project(name_or_id, domain_id=domain_id) - if not proj: - raise exc.OpenStackCloudException( - "Project %s not found." % name_or_id) - if enabled is not None: - kwargs.update({'enabled': enabled}) - # NOTE(samueldmq): Current code only allow updates of description - # or enabled fields. - if self._is_client_version('identity', 3): - data = self._identity_client.patch( - '/projects/' + proj['id'], json={'project': kwargs}) - project = self._get_and_munchify('project', data) - else: - data = self._identity_client.post( - '/tenants/' + proj['id'], json={'tenant': kwargs}) - project = self._get_and_munchify('tenant', data) - project = self._normalize_project(project) - self.list_projects.invalidate(self) - return project - - def create_project( - self, name, description=None, domain_id=None, enabled=True): - """Create a project.""" - with _utils.shade_exceptions( - "Error in creating project {project}".format(project=name)): - project_ref = self._get_domain_id_param_dict(domain_id) - project_ref.update({'name': name, - 'description': description, - 'enabled': enabled}) - endpoint, key = ('tenants', 'tenant') - if self._is_client_version('identity', 3): - endpoint, key = ('projects', 'project') - data = self._identity_client.post( - '/{endpoint}'.format(endpoint=endpoint), - json={key: project_ref}) - project = self._normalize_project( - self._get_and_munchify(key, data)) - self.list_projects.invalidate(self) - return project - - def delete_project(self, name_or_id, domain_id=None): - """Delete a project. - - :param string name_or_id: Project name or ID. - :param string domain_id: Domain ID containing the project(identity v3 - only). - - :returns: True if delete succeeded, False if the project was not found. - - :raises: ``OpenStackCloudException`` if something goes wrong during - the OpenStack API call - """ - - with _utils.shade_exceptions( - "Error in deleting project {project}".format( - project=name_or_id)): - project = self.get_project(name_or_id, domain_id=domain_id) - if project is None: - self.log.debug( - "Project %s not found for deleting", name_or_id) - return False - - if self._is_client_version('identity', 3): - self._identity_client.delete('/projects/' + project['id']) - else: - self._identity_client.delete('/tenants/' + project['id']) - - return True - - @_utils.valid_kwargs('domain_id') - @_utils.cache_on_arguments() - def list_users(self, **kwargs): - """List users. - - :param domain_id: Domain ID. (v3) - - :returns: a list of ``munch.Munch`` containing the user description. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. - """ - data = self._identity_client.get('/users', params=kwargs) - return _utils.normalize_users( - self._get_and_munchify('users', data)) - - @_utils.valid_kwargs('domain_id') - def search_users(self, name_or_id=None, filters=None, **kwargs): - """Search users. - - :param string name_or_id: user name or ID. - :param domain_id: Domain ID. (v3) - :param filters: a dict containing additional filters to use. - OR - A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - - :returns: a list of ``munch.Munch`` containing the users - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. - """ - users = self.list_users(**kwargs) - return _utils._filter_list(users, name_or_id, filters) - - @_utils.valid_kwargs('domain_id') - def get_user(self, name_or_id, filters=None, **kwargs): - """Get exactly one user. - - :param string name_or_id: user name or ID. - :param domain_id: Domain ID. (v3) - :param filters: a dict containing additional filters to use. - OR - A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - - :returns: a single ``munch.Munch`` containing the user description. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. - """ - return _utils._get_entity(self, 'user', name_or_id, filters, **kwargs) - - def get_user_by_id(self, user_id, normalize=True): - """Get a user by ID. - - :param string user_id: user ID - :param bool normalize: Flag to control dict normalization - - :returns: a single ``munch.Munch`` containing the user description - """ - data = self._identity_client.get( - '/users/{user}'.format(user=user_id), - error_message="Error getting user with ID {user_id}".format( - user_id=user_id)) - - user = self._get_and_munchify('user', data) - if user and normalize: - user = _utils.normalize_users(user) - return user - - # NOTE(Shrews): Keystone v2 supports updating only name, email and enabled. - @_utils.valid_kwargs('name', 'email', 'enabled', 'domain_id', 'password', - 'description', 'default_project') - def update_user(self, name_or_id, **kwargs): - self.list_users.invalidate(self) - user_kwargs = {} - if 'domain_id' in kwargs and kwargs['domain_id']: - user_kwargs['domain_id'] = kwargs['domain_id'] - user = self.get_user(name_or_id, **user_kwargs) - - # TODO(mordred) When this changes to REST, force interface=admin - # in the adapter call if it's an admin force call (and figure out how - # to make that disctinction) - if self._is_client_version('identity', 2): - # Do not pass v3 args to a v2 keystone. - kwargs.pop('domain_id', None) - kwargs.pop('description', None) - kwargs.pop('default_project', None) - password = kwargs.pop('password', None) - if password is not None: - with _utils.shade_exceptions( - "Error updating password for {user}".format( - user=name_or_id)): - error_msg = "Error updating password for user {}".format( - name_or_id) - data = self._identity_client.put( - '/users/{u}/OS-KSADM/password'.format(u=user['id']), - json={'user': {'password': password}}, - error_message=error_msg) - - # Identity v2.0 implements PUT. v3 PATCH. Both work as PATCH. - data = self._identity_client.put( - '/users/{user}'.format(user=user['id']), json={'user': kwargs}, - error_message="Error in updating user {}".format(name_or_id)) - else: - # NOTE(samueldmq): now this is a REST call and domain_id is dropped - # if None. keystoneclient drops keys with None values. - if 'domain_id' in kwargs and kwargs['domain_id'] is None: - del kwargs['domain_id'] - data = self._identity_client.patch( - '/users/{user}'.format(user=user['id']), json={'user': kwargs}, - error_message="Error in updating user {}".format(name_or_id)) - - user = self._get_and_munchify('user', data) - self.list_users.invalidate(self) - return _utils.normalize_users([user])[0] - - def create_user( - self, name, password=None, email=None, default_project=None, - enabled=True, domain_id=None, description=None): - """Create a user.""" - params = self._get_identity_params(domain_id, default_project) - params.update({'name': name, 'password': password, 'email': email, - 'enabled': enabled}) - if self._is_client_version('identity', 3): - params['description'] = description - elif description is not None: - self.log.info( - "description parameter is not supported on Keystone v2") - - error_msg = "Error in creating user {user}".format(user=name) - data = self._identity_client.post('/users', json={'user': params}, - error_message=error_msg) - user = self._get_and_munchify('user', data) - - self.list_users.invalidate(self) - return _utils.normalize_users([user])[0] - - @_utils.valid_kwargs('domain_id') - def delete_user(self, name_or_id, **kwargs): - # TODO(mordred) Why are we invalidating at the TOP? - self.list_users.invalidate(self) - user = self.get_user(name_or_id, **kwargs) - if not user: - self.log.debug( - "User {0} not found for deleting".format(name_or_id)) - return False - - # TODO(mordred) Extra GET only needed to support keystoneclient. - # Can be removed as a follow-on. - user = self.get_user_by_id(user['id'], normalize=False) - self._identity_client.delete( - '/users/{user}'.format(user=user['id']), - error_message="Error in deleting user {user}".format( - user=name_or_id)) - - self.list_users.invalidate(self) - return True - - def _get_user_and_group(self, user_name_or_id, group_name_or_id): - user = self.get_user(user_name_or_id) - if not user: - raise exc.OpenStackCloudException( - 'User {user} not found'.format(user=user_name_or_id)) - - group = self.get_group(group_name_or_id) - if not group: - raise exc.OpenStackCloudException( - 'Group {user} not found'.format(user=group_name_or_id)) - - return (user, group) - - def add_user_to_group(self, name_or_id, group_name_or_id): - """Add a user to a group. - - :param string name_or_id: User name or ID - :param string group_name_or_id: Group name or ID - - :raises: ``OpenStackCloudException`` if something goes wrong during - the OpenStack API call - """ - user, group = self._get_user_and_group(name_or_id, group_name_or_id) - - error_msg = "Error adding user {user} to group {group}".format( - user=name_or_id, group=group_name_or_id) - self._identity_client.put( - '/groups/{g}/users/{u}'.format(g=group['id'], u=user['id']), - error_message=error_msg) - - def is_user_in_group(self, name_or_id, group_name_or_id): - """Check to see if a user is in a group. - - :param string name_or_id: User name or ID - :param string group_name_or_id: Group name or ID - - :returns: True if user is in the group, False otherwise - - :raises: ``OpenStackCloudException`` if something goes wrong during - the OpenStack API call - """ - user, group = self._get_user_and_group(name_or_id, group_name_or_id) - - try: - self._identity_client.head( - '/groups/{g}/users/{u}'.format(g=group['id'], u=user['id'])) - return True - except exc.OpenStackCloudURINotFound: - # NOTE(samueldmq): knowing this URI exists, let's interpret this as - # user not found in group rather than URI not found. - return False - - def remove_user_from_group(self, name_or_id, group_name_or_id): - """Remove a user from a group. - - :param string name_or_id: User name or ID - :param string group_name_or_id: Group name or ID - - :raises: ``OpenStackCloudException`` if something goes wrong during - the OpenStack API call - """ - user, group = self._get_user_and_group(name_or_id, group_name_or_id) - - error_msg = "Error removing user {user} from group {group}".format( - user=name_or_id, group=group_name_or_id) - self._identity_client.delete( - '/groups/{g}/users/{u}'.format(g=group['id'], u=user['id']), - error_message=error_msg) - - def get_template_contents( - self, template_file=None, template_url=None, - template_object=None, files=None): - try: - return template_utils.get_template_contents( - template_file=template_file, template_url=template_url, - template_object=template_object, files=files) - except Exception as e: - raise exc.OpenStackCloudException( - "Error in processing template files: %s" % str(e)) - - def create_stack( - self, name, tags=None, - template_file=None, template_url=None, - template_object=None, files=None, - rollback=True, - wait=False, timeout=3600, - environment_files=None, - **parameters): - """Create a stack. - - :param string name: Name of the stack. - :param tags: List of tag(s) of the stack. (optional) - :param string template_file: Path to the template. - :param string template_url: URL of template. - :param string template_object: URL to retrieve template object. - :param dict files: dict of additional file content to include. - :param boolean rollback: Enable rollback on create failure. - :param boolean wait: Whether to wait for the delete to finish. - :param int timeout: Stack create timeout in seconds. - :param environment_files: Paths to environment files to apply. - - Other arguments will be passed as stack parameters which will take - precedence over any parameters specified in the environments. - - Only one of template_file, template_url, template_object should be - specified. - - :returns: a dict containing the stack description - - :raises: ``OpenStackCloudException`` if something goes wrong during - the OpenStack API call - """ - envfiles, env = template_utils.process_multiple_environments_and_files( - env_paths=environment_files) - tpl_files, template = template_utils.get_template_contents( - template_file=template_file, - template_url=template_url, - template_object=template_object, - files=files) - params = dict( - stack_name=name, - tags=tags, - disable_rollback=not rollback, - parameters=parameters, - template=template, - files=dict(list(tpl_files.items()) + list(envfiles.items())), - environment=env, - timeout_mins=timeout // 60, - ) - self._orchestration_client.post('/stacks', json=params) - if wait: - event_utils.poll_for_events(self, stack_name=name, - action='CREATE') - return self.get_stack(name) - - def update_stack( - self, name_or_id, - template_file=None, template_url=None, - template_object=None, files=None, - rollback=True, - wait=False, timeout=3600, - environment_files=None, - **parameters): - """Update a stack. - - :param string name_or_id: Name or ID of the stack to update. - :param string template_file: Path to the template. - :param string template_url: URL of template. - :param string template_object: URL to retrieve template object. - :param dict files: dict of additional file content to include. - :param boolean rollback: Enable rollback on update failure. - :param boolean wait: Whether to wait for the delete to finish. - :param int timeout: Stack update timeout in seconds. - :param environment_files: Paths to environment files to apply. - - Other arguments will be passed as stack parameters which will take - precedence over any parameters specified in the environments. - - Only one of template_file, template_url, template_object should be - specified. - - :returns: a dict containing the stack description - - :raises: ``OpenStackCloudException`` if something goes wrong during - the OpenStack API calls - """ - envfiles, env = template_utils.process_multiple_environments_and_files( - env_paths=environment_files) - tpl_files, template = template_utils.get_template_contents( - template_file=template_file, - template_url=template_url, - template_object=template_object, - files=files) - params = dict( - disable_rollback=not rollback, - parameters=parameters, - template=template, - files=dict(list(tpl_files.items()) + list(envfiles.items())), - environment=env, - timeout_mins=timeout // 60, - ) - if wait: - # find the last event to use as the marker - events = event_utils.get_events( - self, name_or_id, event_args={'sort_dir': 'desc', 'limit': 1}) - marker = events[0].id if events else None - - self._orchestration_client.put( - '/stacks/{name_or_id}'.format(name_or_id=name_or_id), json=params) - if wait: - event_utils.poll_for_events(self, - name_or_id, - action='UPDATE', - marker=marker) - return self.get_stack(name_or_id) - - def delete_stack(self, name_or_id, wait=False): - """Delete a stack - - :param string name_or_id: Stack name or ID. - :param boolean wait: Whether to wait for the delete to finish - - :returns: True if delete succeeded, False if the stack was not found. - - :raises: ``OpenStackCloudException`` if something goes wrong during - the OpenStack API call - """ - stack = self.get_stack(name_or_id, resolve_outputs=False) - if stack is None: - self.log.debug("Stack %s not found for deleting", name_or_id) - return False - - if wait: - # find the last event to use as the marker - events = event_utils.get_events( - self, name_or_id, event_args={'sort_dir': 'desc', 'limit': 1}) - marker = events[0].id if events else None - - self._orchestration_client.delete( - '/stacks/{id}'.format(id=stack['id'])) - - if wait: - try: - event_utils.poll_for_events(self, - stack_name=name_or_id, - action='DELETE', - marker=marker) - except exc.OpenStackCloudHTTPError: - pass - stack = self.get_stack(name_or_id, resolve_outputs=False) - if stack and stack['stack_status'] == 'DELETE_FAILED': - raise exc.OpenStackCloudException( - "Failed to delete stack {id}: {reason}".format( - id=name_or_id, reason=stack['stack_status_reason'])) - - return True - def get_name(self): return self.name def get_region(self): return self.config.region_name - def get_flavor_name(self, flavor_id): - flavor = self.get_flavor(flavor_id, get_extra=False) - if flavor: - return flavor['name'] - return None - - def get_flavor_by_ram(self, ram, include=None, get_extra=True): - """Get a flavor based on amount of RAM available. - - Finds the flavor with the least amount of RAM that is at least - as much as the specified amount. If `include` is given, further - filter based on matching flavor name. - - :param int ram: Minimum amount of RAM. - :param string include: If given, will return a flavor whose name - contains this string as a substring. - """ - flavors = self.list_flavors(get_extra=get_extra) - for flavor in sorted(flavors, key=operator.itemgetter('ram')): - if (flavor['ram'] >= ram - and (not include or include in flavor['name'])): - return flavor - raise exc.OpenStackCloudException( - "Could not find a flavor with {ram} and '{include}'".format( - ram=ram, include=include)) - def get_session_endpoint(self, service_key): try: return self.config.get_session_endpoint(service_key) @@ -1417,10998 +743,11 @@ def has_service(self, service_key): else: return False - @_utils.cache_on_arguments() - def _nova_extensions(self): - extensions = set() - data = proxy._json_response( - self.compute.get('/extensions'), - error_message="Error fetching extension list for nova") - - for extension in self._get_and_munchify('extensions', data): - extensions.add(extension['alias']) - return extensions - - def _has_nova_extension(self, extension_name): - return extension_name in self._nova_extensions() - - def search_keypairs(self, name_or_id=None, filters=None): - keypairs = self.list_keypairs() - return _utils._filter_list(keypairs, name_or_id, filters) - - @_utils.cache_on_arguments() - def _neutron_extensions(self): - extensions = set() - resp = self.network.get('/extensions.json') - data = proxy._json_response( - resp, - error_message="Error fetching extension list for neutron") - for extension in self._get_and_munchify('extensions', data): - extensions.add(extension['alias']) - return extensions - - def _has_neutron_extension(self, extension_alias): - return extension_alias in self._neutron_extensions() - - def search_networks(self, name_or_id=None, filters=None): - """Search networks - - :param name_or_id: Name or ID of the desired network. - :param filters: a dict containing additional filters to use. e.g. - {'router:external': True} - - :returns: a list of ``munch.Munch`` containing the network description. - - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call. - """ - networks = self.list_networks( - filters if isinstance(filters, dict) else None) - return _utils._filter_list(networks, name_or_id, filters) - - def search_routers(self, name_or_id=None, filters=None): - """Search routers - - :param name_or_id: Name or ID of the desired router. - :param filters: a dict containing additional filters to use. e.g. - {'admin_state_up': True} - - :returns: a list of ``munch.Munch`` containing the router description. - - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call. - """ - routers = self.list_routers( - filters if isinstance(filters, dict) else None) - return _utils._filter_list(routers, name_or_id, filters) - - def search_subnets(self, name_or_id=None, filters=None): - """Search subnets - - :param name_or_id: Name or ID of the desired subnet. - :param filters: a dict containing additional filters to use. e.g. - {'enable_dhcp': True} - - :returns: a list of ``munch.Munch`` containing the subnet description. - - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call. - """ - subnets = self.list_subnets( - filters if isinstance(filters, dict) else None) - return _utils._filter_list(subnets, name_or_id, filters) - - def search_ports(self, name_or_id=None, filters=None): - """Search ports - - :param name_or_id: Name or ID of the desired port. - :param filters: a dict containing additional filters to use. e.g. - {'device_id': '2711c67a-b4a7-43dd-ace7-6187b791c3f0'} - - :returns: a list of ``munch.Munch`` containing the port description. - - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call. - """ - # If port caching is enabled, do not push the filter down to - # neutron; get all the ports (potentially from the cache) and - # filter locally. - if self._PORT_AGE or isinstance(filters, str): - pushdown_filters = None - else: - pushdown_filters = filters - ports = self.list_ports(pushdown_filters) - return _utils._filter_list(ports, name_or_id, filters) - - def search_qos_policies(self, name_or_id=None, filters=None): - """Search QoS policies - - :param name_or_id: Name or ID of the desired policy. - :param filters: a dict containing additional filters to use. e.g. - {'shared': True} - - :returns: a list of ``munch.Munch`` containing the network description. - - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call. - """ - policies = self.list_qos_policies(filters) - return _utils._filter_list(policies, name_or_id, filters) - - def search_volumes(self, name_or_id=None, filters=None): - volumes = self.list_volumes() - return _utils._filter_list( - volumes, name_or_id, filters) - - def search_volume_snapshots(self, name_or_id=None, filters=None): - volumesnapshots = self.list_volume_snapshots() - return _utils._filter_list( - volumesnapshots, name_or_id, filters) - - def search_volume_backups(self, name_or_id=None, filters=None): - volume_backups = self.list_volume_backups() - return _utils._filter_list( - volume_backups, name_or_id, filters) - - def search_volume_types( - self, name_or_id=None, filters=None, get_extra=True): - volume_types = self.list_volume_types(get_extra=get_extra) - return _utils._filter_list(volume_types, name_or_id, filters) - - def search_flavors(self, name_or_id=None, filters=None, get_extra=True): - flavors = self.list_flavors(get_extra=get_extra) - return _utils._filter_list(flavors, name_or_id, filters) - - def search_security_groups(self, name_or_id=None, filters=None): - # `filters` could be a dict or a jmespath (str) - groups = self.list_security_groups( - filters=filters if isinstance(filters, dict) else None - ) - return _utils._filter_list(groups, name_or_id, filters) - - def search_servers( - self, name_or_id=None, filters=None, detailed=False, - all_projects=False, bare=False): - servers = self.list_servers( - detailed=detailed, all_projects=all_projects, bare=bare) - return _utils._filter_list(servers, name_or_id, filters) - - def search_server_groups(self, name_or_id=None, filters=None): - """Seach server groups. - - :param name: server group name or ID. - :param filters: a dict containing additional filters to use. - - :returns: a list of dicts containing the server groups - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. - """ - server_groups = self.list_server_groups() - return _utils._filter_list(server_groups, name_or_id, filters) - - def search_images(self, name_or_id=None, filters=None): - images = self.list_images() - return _utils._filter_list(images, name_or_id, filters) - - def search_floating_ip_pools(self, name=None, filters=None): - pools = self.list_floating_ip_pools() - return _utils._filter_list(pools, name, filters) - - # With Neutron, there are some cases in which full server side filtering is - # not possible (e.g. nested attributes or list of objects) so we also need - # to use the client-side filtering - # The same goes for all neutron-related search/get methods! - def search_floating_ips(self, id=None, filters=None): - # `filters` could be a jmespath expression which Neutron server doesn't - # understand, obviously. - if self._use_neutron_floating() and isinstance(filters, dict): - filter_keys = ['router_id', 'status', 'tenant_id', 'project_id', - 'revision_number', 'description', - 'floating_network_id', 'fixed_ip_address', - 'floating_ip_address', 'port_id', 'sort_dir', - 'sort_key', 'tags', 'tags-any', 'not-tags', - 'not-tags-any', 'fields'] - neutron_filters = {k: v for k, v in filters.items() - if k in filter_keys} - kwargs = {'filters': neutron_filters} - else: - kwargs = {} - floating_ips = self.list_floating_ips(**kwargs) - return _utils._filter_list(floating_ips, id, filters) - - def search_stacks(self, name_or_id=None, filters=None): - """Search stacks. - - :param name_or_id: Name or ID of the desired stack. - :param filters: a dict containing additional filters to use. e.g. - {'stack_status': 'CREATE_COMPLETE'} - - :returns: a list of ``munch.Munch`` containing the stack description. - - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call. - """ - stacks = self.list_stacks() - return _utils._filter_list(stacks, name_or_id, filters) - - def list_keypairs(self): - """List all available keypairs. - - :returns: A list of ``munch.Munch`` containing keypair info. - - """ - data = proxy._json_response( - self.compute.get('/os-keypairs'), - error_message="Error fetching keypair list") - return self._normalize_keypairs([ - k['keypair'] for k in self._get_and_munchify('keypairs', data)]) - - def list_networks(self, filters=None): - """List all available networks. - - :param filters: (optional) dict of filter conditions to push down - :returns: A list of ``munch.Munch`` containing network info. - - """ - # Translate None from search interface to empty {} for kwargs below - if not filters: - filters = {} - data = self.network.get("/networks.json", params=filters) - return self._get_and_munchify('networks', data) - - def list_routers(self, filters=None): - """List all available routers. - - :param filters: (optional) dict of filter conditions to push down - :returns: A list of router ``munch.Munch``. - - """ - # Translate None from search interface to empty {} for kwargs below - if not filters: - filters = {} - resp = self.network.get("/routers.json", params=filters) - data = proxy._json_response( - resp, - error_message="Error fetching router list") - return self._get_and_munchify('routers', data) - - def list_subnets(self, filters=None): - """List all available subnets. - - :param filters: (optional) dict of filter conditions to push down - :returns: A list of subnet ``munch.Munch``. - - """ - # Translate None from search interface to empty {} for kwargs below - if not filters: - filters = {} - data = self.network.get("/subnets.json", params=filters) - return self._get_and_munchify('subnets', data) - - def list_ports(self, filters=None): - """List all available ports. - - :param filters: (optional) dict of filter conditions to push down - :returns: A list of port ``munch.Munch``. - - """ - # If pushdown filters are specified and we do not have batched caching - # enabled, bypass local caching and push down the filters. - if filters and self._PORT_AGE == 0: - return self._list_ports(filters) - - if (time.time() - self._ports_time) >= self._PORT_AGE: - # Since we're using cached data anyway, we don't need to - # have more than one thread actually submit the list - # ports task. Let the first one submit it while holding - # a lock, and the non-blocking acquire method will cause - # subsequent threads to just skip this and use the old - # data until it succeeds. - # Initially when we never got data, block to retrieve some data. - first_run = self._ports is None - if self._ports_lock.acquire(first_run): - try: - if not (first_run and self._ports is not None): - self._ports = self._list_ports({}) - self._ports_time = time.time() - finally: - self._ports_lock.release() - # Wrap the return with filter_list so that if filters were passed - # but we were batching/caching and thus always fetching the whole - # list from the cloud, we still return a filtered list. - return _utils._filter_list(self._ports, None, filters or {}) - - def _list_ports(self, filters): - resp = self.network.get("/ports.json", params=filters) - data = proxy._json_response( - resp, - error_message="Error fetching port list") - return self._get_and_munchify('ports', data) - - def list_qos_rule_types(self, filters=None): - """List all available QoS rule types. - - :param filters: (optional) dict of filter conditions to push down - :returns: A list of rule types ``munch.Munch``. - - """ - if not self._has_neutron_extension('qos'): - raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') - - # Translate None from search interface to empty {} for kwargs below - if not filters: - filters = {} - resp = self.network.get("/qos/rule-types.json", params=filters) - data = proxy._json_response( - resp, - error_message="Error fetching QoS rule types list") - return self._get_and_munchify('rule_types', data) - - def get_qos_rule_type_details(self, rule_type, filters=None): - """Get a QoS rule type details by rule type name. - - :param string rule_type: Name of the QoS rule type. - - :returns: A rule type details ``munch.Munch`` or None if - no matching rule type is found. - - """ - if not self._has_neutron_extension('qos'): - raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') + def get_openstack_vars(self, server): + return meta.get_hostvars_from_server(self, server) - if not self._has_neutron_extension('qos-rule-type-details'): - raise exc.OpenStackCloudUnavailableExtension( - 'qos-rule-type-details extension is not available ' - 'on target cloud') - - resp = self.network.get( - "/qos/rule-types/{rule_type}.json".format(rule_type=rule_type)) - data = proxy._json_response( - resp, - error_message="Error fetching QoS details of {rule_type} " - "rule type".format(rule_type=rule_type)) - return self._get_and_munchify('rule_type', data) - - def list_qos_policies(self, filters=None): - """List all available QoS policies. - - :param filters: (optional) dict of filter conditions to push down - :returns: A list of policies ``munch.Munch``. - - """ - if not self._has_neutron_extension('qos'): - raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') - # Translate None from search interface to empty {} for kwargs below - if not filters: - filters = {} - resp = self.network.get("/qos/policies.json", params=filters) - data = proxy._json_response( - resp, - error_message="Error fetching QoS policies list") - return self._get_and_munchify('policies', data) - - @_utils.cache_on_arguments(should_cache_fn=_no_pending_volumes) - def list_volumes(self, cache=True): - """List all available volumes. - - :returns: A list of volume ``munch.Munch``. - - """ - def _list(data): - volumes.extend(data.get('volumes', [])) - endpoint = None - for l in data.get('volumes_links', []): - if 'rel' in l and 'next' == l['rel']: - endpoint = l['href'] - break - if endpoint: - try: - _list(self._volume_client.get(endpoint)) - except exc.OpenStackCloudURINotFound: - # Catch and re-raise here because we are making recursive - # calls and we just have context for the log here - self.log.debug( - "While listing volumes, could not find next link" - " {link}.".format(link=data)) - raise - - if not cache: - warnings.warn('cache argument to list_volumes is deprecated. Use ' - 'invalidate instead.') - - # Fetching paginated volumes can fails for several reasons, if - # something goes wrong we'll have to start fetching volumes from - # scratch - attempts = 5 - for _ in range(attempts): - volumes = [] - data = self._volume_client.get('/volumes/detail') - if 'volumes_links' not in data: - # no pagination needed - volumes.extend(data.get('volumes', [])) - break - - try: - _list(data) - break - except exc.OpenStackCloudURINotFound: - pass - else: - self.log.debug( - "List volumes failed to retrieve all volumes after" - " {attempts} attempts. Returning what we found.".format( - attempts=attempts)) - # list volumes didn't complete succesfully so just return what - # we found - return self._normalize_volumes( - self._get_and_munchify(key=None, data=volumes)) - - @_utils.cache_on_arguments() - def list_volume_types(self, get_extra=True): - """List all available volume types. - - :returns: A list of volume ``munch.Munch``. - - """ - data = self._volume_client.get( - '/types', - params=dict(is_public='None'), - error_message='Error fetching volume_type list') - return self._normalize_volume_types( - self._get_and_munchify('volume_types', data)) - - @_utils.cache_on_arguments() - def list_availability_zone_names(self, unavailable=False): - """List names of availability zones. - - :param bool unavailable: Whether or not to include unavailable zones - in the output. Defaults to False. - - :returns: A list of availability zone names, or an empty list if the - list could not be fetched. - """ - try: - data = proxy._json_response( - self.compute.get('/os-availability-zone')) - except exc.OpenStackCloudHTTPError: - self.log.debug( - "Availability zone list could not be fetched", - exc_info=True) - return [] - zones = self._get_and_munchify('availabilityZoneInfo', data) - ret = [] - for zone in zones: - if zone['zoneState']['available'] or unavailable: - ret.append(zone['zoneName']) - return ret - - @_utils.cache_on_arguments() - def list_flavors(self, get_extra=False): - """List all available flavors. - - :param get_extra: Whether or not to fetch extra specs for each flavor. - Defaults to True. Default behavior value can be - overridden in clouds.yaml by setting - openstack.cloud.get_extra_specs to False. - :returns: A list of flavor ``munch.Munch``. - - """ - data = proxy._json_response( - self.compute.get( - '/flavors/detail', params=dict(is_public='None')), - error_message="Error fetching flavor list") - flavors = self._normalize_flavors( - self._get_and_munchify('flavors', data)) - - for flavor in flavors: - if not flavor.extra_specs and get_extra: - endpoint = "/flavors/{id}/os-extra_specs".format( - id=flavor.id) - try: - data = proxy._json_response( - self.compute.get(endpoint), - error_message="Error fetching flavor extra specs") - flavor.extra_specs = self._get_and_munchify( - 'extra_specs', data) - except exc.OpenStackCloudHTTPError as e: - flavor.extra_specs = {} - self.log.debug( - 'Fetching extra specs for flavor failed:' - ' %(msg)s', {'msg': str(e)}) - - return flavors - - @_utils.cache_on_arguments(should_cache_fn=_no_pending_stacks) - def list_stacks(self): - """List all stacks. - - :returns: a list of ``munch.Munch`` containing the stack description. - - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call. - """ - data = self._orchestration_client.get( - '/stacks', error_message="Error fetching stack list") - return self._normalize_stacks( - self._get_and_munchify('stacks', data)) - - def list_server_security_groups(self, server): - """List all security groups associated with the given server. - - :returns: A list of security group ``munch.Munch``. - """ - - # Don't even try if we're a cloud that doesn't have them - if not self._has_secgroups(): - return [] - - data = proxy._json_response( - self.compute.get( - '/servers/{server_id}/os-security-groups'.format( - server_id=server['id']))) - return self._normalize_secgroups( - self._get_and_munchify('security_groups', data)) - - def _get_server_security_groups(self, server, security_groups): - if not self._has_secgroups(): - raise exc.OpenStackCloudUnavailableFeature( - "Unavailable feature: security groups" - ) - - if not isinstance(server, dict): - server = self.get_server(server, bare=True) - - if server is None: - self.log.debug('Server %s not found', server) - return None, None - - if not isinstance(security_groups, (list, tuple)): - security_groups = [security_groups] - - sec_group_objs = [] - - for sg in security_groups: - if not isinstance(sg, dict): - sg = self.get_security_group(sg) - - if sg is None: - self.log.debug('Security group %s not found for adding', - sg) - - return None, None - - sec_group_objs.append(sg) - - return server, sec_group_objs - - def add_server_security_groups(self, server, security_groups): - """Add security groups to a server. - - Add existing security groups to an existing server. If the security - groups are already present on the server this will continue unaffected. - - :returns: False if server or security groups are undefined, True - otherwise. - - :raises: ``OpenStackCloudException``, on operation error. - """ - server, security_groups = self._get_server_security_groups( - server, security_groups) - - if not (server and security_groups): - return False - - for sg in security_groups: - proxy._json_response(self.compute.post( - '/servers/%s/action' % server['id'], - json={'addSecurityGroup': {'name': sg.name}})) - - return True - - def remove_server_security_groups(self, server, security_groups): - """Remove security groups from a server - - Remove existing security groups from an existing server. If the - security groups are not present on the server this will continue - unaffected. - - :returns: False if server or security groups are undefined, True - otherwise. - - :raises: ``OpenStackCloudException``, on operation error. - """ - server, security_groups = self._get_server_security_groups( - server, security_groups) - - if not (server and security_groups): - return False - - ret = True - - for sg in security_groups: - try: - proxy._json_response(self.compute.post( - '/servers/%s/action' % server['id'], - json={'removeSecurityGroup': {'name': sg.name}})) - - except exc.OpenStackCloudURINotFound: - # NOTE(jamielennox): Is this ok? If we remove something that - # isn't present should we just conclude job done or is that an - # error? Nova returns ok if you try to add a group twice. - self.log.debug( - "The security group %s was not present on server %s so " - "no action was performed", sg.name, server.name) - ret = False - - return ret - - def list_security_groups(self, filters=None): - """List all available security groups. - - :param filters: (optional) dict of filter conditions to push down - :returns: A list of security group ``munch.Munch``. - - """ - # Security groups not supported - if not self._has_secgroups(): - raise exc.OpenStackCloudUnavailableFeature( - "Unavailable feature: security groups" - ) - - if not filters: - filters = {} - - data = [] - # Handle neutron security groups - if self._use_neutron_secgroups(): - # Neutron returns dicts, so no need to convert objects here. - resp = self.network.get('/security-groups.json', params=filters) - data = proxy._json_response( - resp, - error_message="Error fetching security group list") - return self._normalize_secgroups( - self._get_and_munchify('security_groups', data)) - - # Handle nova security groups - else: - data = proxy._json_response(self.compute.get( - '/os-security-groups', params=filters)) - return self._normalize_secgroups( - self._get_and_munchify('security_groups', data)) - - def list_servers(self, detailed=False, all_projects=False, bare=False, - filters=None): - """List all available servers. - - :param detailed: Whether or not to add detailed additional information. - Defaults to False. - :param all_projects: Whether to list servers from all projects or just - the current auth scoped project. - :param bare: Whether to skip adding any additional information to the - server record. Defaults to False, meaning the addresses - dict will be populated as needed from neutron. Setting - to True implies detailed = False. - :param filters: Additional query parameters passed to the API server. - - :returns: A list of server ``munch.Munch``. - - """ - # If pushdown filters are specified and we do not have batched caching - # enabled, bypass local caching and push down the filters. - if filters and self._SERVER_AGE == 0: - return self._list_servers( - detailed=detailed, - all_projects=all_projects, - bare=bare, - filters=filters, - ) - - if (time.time() - self._servers_time) >= self._SERVER_AGE: - # Since we're using cached data anyway, we don't need to - # have more than one thread actually submit the list - # servers task. Let the first one submit it while holding - # a lock, and the non-blocking acquire method will cause - # subsequent threads to just skip this and use the old - # data until it succeeds. - # Initially when we never got data, block to retrieve some data. - first_run = self._servers is None - if self._servers_lock.acquire(first_run): - try: - if not (first_run and self._servers is not None): - self._servers = self._list_servers( - detailed=detailed, - all_projects=all_projects, - bare=bare) - self._servers_time = time.time() - finally: - self._servers_lock.release() - # Wrap the return with filter_list so that if filters were passed - # but we were batching/caching and thus always fetching the whole - # list from the cloud, we still return a filtered list. - return _utils._filter_list(self._servers, None, filters) - - def _list_servers(self, detailed=False, all_projects=False, bare=False, - filters=None): - filters = filters or {} - servers = [ - # TODO(mordred) Add original_names=False here and update the - # normalize file for server. Then, just remove the normalize call - # and the to_munch call. - self._normalize_server(server._to_munch()) - for server in self.compute.servers( - all_projects=all_projects, **filters)] - return [ - self._expand_server(server, detailed, bare) - for server in servers - ] - - def list_server_groups(self): - """List all available server groups. - - :returns: A list of server group dicts. - - """ - data = proxy._json_response( - self.compute.get('/os-server-groups'), - error_message="Error fetching server group list") - return self._get_and_munchify('server_groups', data) - - def get_compute_limits(self, name_or_id=None): - """ Get compute limits for a project - - :param name_or_id: (optional) project name or ID to get limits for - if different from the current project - :raises: OpenStackCloudException if it's not a valid project - - :returns: Munch object with the limits - """ - params = {} - project_id = None - error_msg = "Failed to get limits" - if name_or_id: - - proj = self.get_project(name_or_id) - if not proj: - raise exc.OpenStackCloudException("project does not exist") - project_id = proj.id - params['tenant_id'] = project_id - error_msg = "{msg} for the project: {project} ".format( - msg=error_msg, project=name_or_id) - - data = proxy._json_response( - self.compute.get('/limits', params=params)) - limits = self._get_and_munchify('limits', data) - return self._normalize_compute_limits(limits, project_id=project_id) - - @_utils.cache_on_arguments(should_cache_fn=_no_pending_images) - def list_images(self, filter_deleted=True, show_all=False): - """Get available images. - - :param filter_deleted: Control whether deleted images are returned. - :param show_all: Show all images, including images that are shared - but not accepted. (By default in glance v2 shared image that - have not been accepted are not shown) show_all will override the - value of filter_deleted to False. - :returns: A list of glance images. - """ - if show_all: - filter_deleted = False - # First, try to actually get images from glance, it's more efficient - images = [] - params = {} - image_list = [] - try: - if self._is_client_version('image', 2): - endpoint = '/images' - if show_all: - params['member_status'] = 'all' - else: - endpoint = '/images/detail' - - response = self._image_client.get(endpoint, params=params) - - except keystoneauth1.exceptions.catalog.EndpointNotFound: - # We didn't have glance, let's try nova - # If this doesn't work - we just let the exception propagate - response = proxy._json_response( - self.compute.get('/images/detail')) - while 'next' in response: - image_list.extend(meta.obj_list_to_munch(response['images'])) - endpoint = response['next'] - # next links from glance have the version prefix. If the catalog - # has a versioned endpoint, then we can't append the next link to - # it. Strip the absolute prefix (/v1/ or /v2/ to turn it into - # a proper relative link. - if endpoint.startswith('/v'): - endpoint = endpoint[4:] - response = self._image_client.get(endpoint) - if 'images' in response: - image_list.extend(meta.obj_list_to_munch(response['images'])) - else: - image_list.extend(response) - - for image in image_list: - # The cloud might return DELETED for invalid images. - # While that's cute and all, that's an implementation detail. - if not filter_deleted: - images.append(image) - elif image.status.lower() != 'deleted': - images.append(image) - return self._normalize_images(images) - - def list_floating_ip_pools(self): - """List all available floating IP pools. - - NOTE: This function supports the nova-net view of the world. nova-net - has been deprecated, so it's highly recommended to switch to using - neutron. `get_external_ipv4_floating_networks` is what you should - almost certainly be using. - - :returns: A list of floating IP pool ``munch.Munch``. - - """ - if not self._has_nova_extension('os-floating-ip-pools'): - raise exc.OpenStackCloudUnavailableExtension( - 'Floating IP pools extension is not available on target cloud') - - data = proxy._json_response( - self.compute.get('os-floating-ip-pools'), - error_message="Error fetching floating IP pool list") - pools = self._get_and_munchify('floating_ip_pools', data) - return [{'name': p['name']} for p in pools] - - def _list_floating_ips(self, filters=None): - if self._use_neutron_floating(): - try: - return self._normalize_floating_ips( - self._neutron_list_floating_ips(filters)) - except exc.OpenStackCloudURINotFound as e: - # Nova-network don't support server-side floating ips - # filtering, so it's safer to return and empty list than - # to fallback to Nova which may return more results that - # expected. - if filters: - self.log.error( - "Neutron returned NotFound for floating IPs, which" - " means this cloud doesn't have neutron floating ips." - " shade can't fallback to trying Nova since nova" - " doesn't support server-side filtering when listing" - " floating ips and filters were given. If you do not" - " think shade should be attempting to list floating" - " ips on neutron, it is possible to control the" - " behavior by setting floating_ip_source to 'nova' or" - " None for cloud: %(cloud)s. If you are not already" - " using clouds.yaml to configure settings for your" - " cloud(s), and you want to configure this setting," - " you will need a clouds.yaml file. For more" - " information, please see %(doc_url)s", { - 'cloud': self.name, - 'doc_url': _CONFIG_DOC_URL, - } - ) - # We can't fallback to nova because we push-down filters. - # We got a 404 which means neutron doesn't exist. If the - # user - return [] - self.log.debug( - "Something went wrong talking to neutron API: " - "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) - # Fall-through, trying with Nova - else: - if filters: - raise ValueError( - "Nova-network don't support server-side floating ips " - "filtering. Use the search_floatting_ips method instead" - ) - - floating_ips = self._nova_list_floating_ips() - return self._normalize_floating_ips(floating_ips) - - def list_floating_ips(self, filters=None): - """List all available floating IPs. - - :param filters: (optional) dict of filter conditions to push down - :returns: A list of floating IP ``munch.Munch``. - - """ - # If pushdown filters are specified and we do not have batched caching - # enabled, bypass local caching and push down the filters. - if filters and self._FLOAT_AGE == 0: - return self._list_floating_ips(filters) - - if (time.time() - self._floating_ips_time) >= self._FLOAT_AGE: - # Since we're using cached data anyway, we don't need to - # have more than one thread actually submit the list - # floating ips task. Let the first one submit it while holding - # a lock, and the non-blocking acquire method will cause - # subsequent threads to just skip this and use the old - # data until it succeeds. - # Initially when we never got data, block to retrieve some data. - first_run = self._floating_ips is None - if self._floating_ips_lock.acquire(first_run): - try: - if not (first_run and self._floating_ips is not None): - self._floating_ips = self._list_floating_ips() - self._floating_ips_time = time.time() - finally: - self._floating_ips_lock.release() - # Wrap the return with filter_list so that if filters were passed - # but we were batching/caching and thus always fetching the whole - # list from the cloud, we still return a filtered list. - return _utils._filter_list(self._floating_ips, None, filters) - - def _neutron_list_floating_ips(self, filters=None): - if not filters: - filters = {} - data = self.network.get('/floatingips.json', params=filters) - return self._get_and_munchify('floatingips', data) - - def _nova_list_floating_ips(self): - try: - data = proxy._json_response( - self.compute.get('/os-floating-ips')) - except exc.OpenStackCloudURINotFound: - return [] - return self._get_and_munchify('floating_ips', data) - - def use_external_network(self): - return self._use_external_network - - def use_internal_network(self): - return self._use_internal_network - - def _reset_network_caches(self): - # Variables to prevent us from going through the network finding - # logic again if we've done it once. This is different from just - # the cached value, since "None" is a valid value to find. - with self._networks_lock: - self._external_ipv4_networks = [] - self._external_ipv4_floating_networks = [] - self._internal_ipv4_networks = [] - self._external_ipv6_networks = [] - self._internal_ipv6_networks = [] - self._nat_destination_network = None - self._nat_source_network = None - self._default_network_network = None - self._network_list_stamp = False - - def _set_interesting_networks(self): - external_ipv4_networks = [] - external_ipv4_floating_networks = [] - internal_ipv4_networks = [] - external_ipv6_networks = [] - internal_ipv6_networks = [] - nat_destination = None - nat_source = None - default_network = None - - all_subnets = None - - # Filter locally because we have an or condition - try: - # TODO(mordred): Rackspace exposes neutron but it does not - # work. I think that overriding what the service catalog - # reports should be a thing os-client-config should handle - # in a vendor profile - but for now it does not. That means - # this search_networks can just totally fail. If it does - # though, that's fine, clearly the neutron introspection is - # not going to work. - all_networks = self.list_networks() - except exc.OpenStackCloudException: - self._network_list_stamp = True - return - - for network in all_networks: - - # External IPv4 networks - if (network['name'] in self._external_ipv4_names - or network['id'] in self._external_ipv4_names): - external_ipv4_networks.append(network) - elif ((('router:external' in network - and network['router:external']) - or network.get('provider:physical_network')) - and network['name'] not in self._internal_ipv4_names - and network['id'] not in self._internal_ipv4_names): - external_ipv4_networks.append(network) - - # Internal networks - if (network['name'] in self._internal_ipv4_names - or network['id'] in self._internal_ipv4_names): - internal_ipv4_networks.append(network) - elif (not network.get('router:external', False) - and not network.get('provider:physical_network') - and network['name'] not in self._external_ipv4_names - and network['id'] not in self._external_ipv4_names): - internal_ipv4_networks.append(network) - - # External networks - if (network['name'] in self._external_ipv6_names - or network['id'] in self._external_ipv6_names): - external_ipv6_networks.append(network) - elif (network.get('router:external') - and network['name'] not in self._internal_ipv6_names - and network['id'] not in self._internal_ipv6_names): - external_ipv6_networks.append(network) - - # Internal networks - if (network['name'] in self._internal_ipv6_names - or network['id'] in self._internal_ipv6_names): - internal_ipv6_networks.append(network) - elif (not network.get('router:external', False) - and network['name'] not in self._external_ipv6_names - and network['id'] not in self._external_ipv6_names): - internal_ipv6_networks.append(network) - - # External Floating IPv4 networks - if self._nat_source in ( - network['name'], network['id']): - if nat_source: - raise exc.OpenStackCloudException( - 'Multiple networks were found matching' - ' {nat_net} which is the network configured' - ' to be the NAT source. Please check your' - ' cloud resources. It is probably a good idea' - ' to configure this network by ID rather than' - ' by name.'.format( - nat_net=self._nat_source)) - external_ipv4_floating_networks.append(network) - nat_source = network - elif self._nat_source is None: - if network.get('router:external'): - external_ipv4_floating_networks.append(network) - nat_source = nat_source or network - - # NAT Destination - if self._nat_destination in ( - network['name'], network['id']): - if nat_destination: - raise exc.OpenStackCloudException( - 'Multiple networks were found matching' - ' {nat_net} which is the network configured' - ' to be the NAT destination. Please check your' - ' cloud resources. It is probably a good idea' - ' to configure this network by ID rather than' - ' by name.'.format( - nat_net=self._nat_destination)) - nat_destination = network - elif self._nat_destination is None: - # TODO(mordred) need a config value for floating - # ips for this cloud so that we can skip this - # No configured nat destination, we have to figured - # it out. - if all_subnets is None: - try: - all_subnets = self.list_subnets() - except exc.OpenStackCloudException: - # Thanks Rackspace broken neutron - all_subnets = [] - - for subnet in all_subnets: - # TODO(mordred) trap for detecting more than - # one network with a gateway_ip without a config - if ('gateway_ip' in subnet and subnet['gateway_ip'] - and network['id'] == subnet['network_id']): - nat_destination = network - break - - # Default network - if self._default_network in ( - network['name'], network['id']): - if default_network: - raise exc.OpenStackCloudException( - 'Multiple networks were found matching' - ' {default_net} which is the network' - ' configured to be the default interface' - ' network. Please check your cloud resources.' - ' It is probably a good idea' - ' to configure this network by ID rather than' - ' by name.'.format( - default_net=self._default_network)) - default_network = network - - # Validate config vs. reality - for net_name in self._external_ipv4_names: - if net_name not in [net['name'] for net in external_ipv4_networks]: - raise exc.OpenStackCloudException( - "Networks: {network} was provided for external IPv4" - " access and those networks could not be found".format( - network=net_name)) - - for net_name in self._internal_ipv4_names: - if net_name not in [net['name'] for net in internal_ipv4_networks]: - raise exc.OpenStackCloudException( - "Networks: {network} was provided for internal IPv4" - " access and those networks could not be found".format( - network=net_name)) - - for net_name in self._external_ipv6_names: - if net_name not in [net['name'] for net in external_ipv6_networks]: - raise exc.OpenStackCloudException( - "Networks: {network} was provided for external IPv6" - " access and those networks could not be found".format( - network=net_name)) - - for net_name in self._internal_ipv6_names: - if net_name not in [net['name'] for net in internal_ipv6_networks]: - raise exc.OpenStackCloudException( - "Networks: {network} was provided for internal IPv6" - " access and those networks could not be found".format( - network=net_name)) - - if self._nat_destination and not nat_destination: - raise exc.OpenStackCloudException( - 'Network {network} was configured to be the' - ' destination for inbound NAT but it could not be' - ' found'.format( - network=self._nat_destination)) - - if self._nat_source and not nat_source: - raise exc.OpenStackCloudException( - 'Network {network} was configured to be the' - ' source for inbound NAT but it could not be' - ' found'.format( - network=self._nat_source)) - - if self._default_network and not default_network: - raise exc.OpenStackCloudException( - 'Network {network} was configured to be the' - ' default network interface but it could not be' - ' found'.format( - network=self._default_network)) - - self._external_ipv4_networks = external_ipv4_networks - self._external_ipv4_floating_networks = external_ipv4_floating_networks - self._internal_ipv4_networks = internal_ipv4_networks - self._external_ipv6_networks = external_ipv6_networks - self._internal_ipv6_networks = internal_ipv6_networks - self._nat_destination_network = nat_destination - self._nat_source_network = nat_source - self._default_network_network = default_network - - def _find_interesting_networks(self): - if self._networks_lock.acquire(): - try: - if self._network_list_stamp: - return - if (not self._use_external_network - and not self._use_internal_network): - # Both have been flagged as skip - don't do a list - return - if not self.has_service('network'): - return - self._set_interesting_networks() - self._network_list_stamp = True - finally: - self._networks_lock.release() - - def get_nat_destination(self): - """Return the network that is configured to be the NAT destination. - - :returns: A network dict if one is found - """ - self._find_interesting_networks() - return self._nat_destination_network - - def get_nat_source(self): - """Return the network that is configured to be the NAT destination. - - :returns: A network dict if one is found - """ - self._find_interesting_networks() - return self._nat_source_network - - def get_default_network(self): - """Return the network that is configured to be the default interface. - - :returns: A network dict if one is found - """ - self._find_interesting_networks() - return self._default_network_network - - def get_external_networks(self): - """Return the networks that are configured to route northbound. - - This should be avoided in favor of the specific ipv4/ipv6 method, - but is here for backwards compatibility. - - :returns: A list of network ``munch.Munch`` if one is found - """ - self._find_interesting_networks() - return list( - set(self._external_ipv4_networks) - | set(self._external_ipv6_networks)) - - def get_internal_networks(self): - """Return the networks that are configured to not route northbound. - - This should be avoided in favor of the specific ipv4/ipv6 method, - but is here for backwards compatibility. - - :returns: A list of network ``munch.Munch`` if one is found - """ - self._find_interesting_networks() - return list( - set(self._internal_ipv4_networks) - | set(self._internal_ipv6_networks)) - - def get_external_ipv4_networks(self): - """Return the networks that are configured to route northbound. - - :returns: A list of network ``munch.Munch`` if one is found - """ - self._find_interesting_networks() - return self._external_ipv4_networks - - def get_external_ipv4_floating_networks(self): - """Return the networks that are configured to route northbound. - - :returns: A list of network ``munch.Munch`` if one is found - """ - self._find_interesting_networks() - return self._external_ipv4_floating_networks - - def get_internal_ipv4_networks(self): - """Return the networks that are configured to not route northbound. - - :returns: A list of network ``munch.Munch`` if one is found - """ - self._find_interesting_networks() - return self._internal_ipv4_networks - - def get_external_ipv6_networks(self): - """Return the networks that are configured to route northbound. - - :returns: A list of network ``munch.Munch`` if one is found - """ - self._find_interesting_networks() - return self._external_ipv6_networks - - def get_internal_ipv6_networks(self): - """Return the networks that are configured to not route northbound. - - :returns: A list of network ``munch.Munch`` if one is found - """ - self._find_interesting_networks() - return self._internal_ipv6_networks - - def _has_floating_ips(self): - if not self._floating_ip_source: - return False - else: - return self._floating_ip_source in ('nova', 'neutron') - - def _use_neutron_floating(self): - return (self.has_service('network') - and self._floating_ip_source == 'neutron') - - def _has_secgroups(self): - if not self.secgroup_source: - return False - else: - return self.secgroup_source.lower() in ('nova', 'neutron') - - def _use_neutron_secgroups(self): - return (self.has_service('network') - and self.secgroup_source == 'neutron') - - def get_keypair(self, name_or_id, filters=None): - """Get a keypair by name or ID. - - :param name_or_id: Name or ID of the keypair. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - OR - A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - - :returns: A keypair ``munch.Munch`` or None if no matching keypair is - found. - """ - return _utils._get_entity(self, 'keypair', name_or_id, filters) - - def get_network(self, name_or_id, filters=None): - """Get a network by name or ID. - - :param name_or_id: Name or ID of the network. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - OR - A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - - :returns: A network ``munch.Munch`` or None if no matching network is - found. - - """ - return _utils._get_entity(self, 'network', name_or_id, filters) - - def get_network_by_id(self, id): - """ Get a network by ID - - :param id: ID of the network. - :returns: A network ``munch.Munch``. - """ - resp = self.network.get('/networks/{id}'.format(id=id)) - data = proxy._json_response( - resp, - error_message="Error getting network with ID {id}".format(id=id) - ) - network = self._get_and_munchify('network', data) - - return network - - def get_router(self, name_or_id, filters=None): - """Get a router by name or ID. - - :param name_or_id: Name or ID of the router. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - OR - A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - - :returns: A router ``munch.Munch`` or None if no matching router is - found. - - """ - return _utils._get_entity(self, 'router', name_or_id, filters) - - def get_subnet(self, name_or_id, filters=None): - """Get a subnet by name or ID. - - :param name_or_id: Name or ID of the subnet. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - :returns: A subnet ``munch.Munch`` or None if no matching subnet is - found. - - """ - return _utils._get_entity(self, 'subnet', name_or_id, filters) - - def get_subnet_by_id(self, id): - """ Get a subnet by ID - - :param id: ID of the subnet. - :returns: A subnet ``munch.Munch``. - """ - resp = self.network.get('/subnets/{id}'.format(id=id)) - data = proxy._json_response( - resp, - error_message="Error getting subnet with ID {id}".format(id=id) - ) - subnet = self._get_and_munchify('subnet', data) - - return subnet - - def get_port(self, name_or_id, filters=None): - """Get a port by name or ID. - - :param name_or_id: Name or ID of the port. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - OR - A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - - :returns: A port ``munch.Munch`` or None if no matching port is found. - - """ - return _utils._get_entity(self, 'port', name_or_id, filters) - - def get_port_by_id(self, id): - """ Get a port by ID - - :param id: ID of the port. - :returns: A port ``munch.Munch``. - """ - resp = self.network.get('/ports/{id}'.format(id=id)) - data = proxy._json_response( - resp, - error_message="Error getting port with ID {id}".format(id=id) - ) - port = self._get_and_munchify('port', data) - - return port - - def get_qos_policy(self, name_or_id, filters=None): - """Get a QoS policy by name or ID. - - :param name_or_id: Name or ID of the policy. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - OR - A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - - :returns: A policy ``munch.Munch`` or None if no matching network is - found. - - """ - return _utils._get_entity( - self, 'qos_policie', name_or_id, filters) - - def get_volume(self, name_or_id, filters=None): - """Get a volume by name or ID. - - :param name_or_id: Name or ID of the volume. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - OR - A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - - :returns: A volume ``munch.Munch`` or None if no matching volume is - found. - - """ - return _utils._get_entity(self, 'volume', name_or_id, filters) - - def get_volume_by_id(self, id): - """ Get a volume by ID - - :param id: ID of the volume. - :returns: A volume ``munch.Munch``. - """ - data = self._volume_client.get( - '/volumes/{id}'.format(id=id), - error_message="Error getting volume with ID {id}".format(id=id) - ) - volume = self._normalize_volume( - self._get_and_munchify('volume', data)) - - return volume - - def get_volume_type(self, name_or_id, filters=None): - """Get a volume type by name or ID. - - :param name_or_id: Name or ID of the volume. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - OR - A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - - :returns: A volume ``munch.Munch`` or None if no matching volume is - found. - - """ - return _utils._get_entity( - self, 'volume_type', name_or_id, filters) - - def get_flavor(self, name_or_id, filters=None, get_extra=True): - """Get a flavor by name or ID. - - :param name_or_id: Name or ID of the flavor. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - OR - A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - :param get_extra: - Whether or not the list_flavors call should get the extra flavor - specs. - - :returns: A flavor ``munch.Munch`` or None if no matching flavor is - found. - - """ - search_func = functools.partial( - self.search_flavors, get_extra=get_extra) - return _utils._get_entity(self, search_func, name_or_id, filters) - - def get_flavor_by_id(self, id, get_extra=False): - """ Get a flavor by ID - - :param id: ID of the flavor. - :param get_extra: - Whether or not the list_flavors call should get the extra flavor - specs. - :returns: A flavor ``munch.Munch``. - """ - data = proxy._json_response( - self.compute.get('/flavors/{id}'.format(id=id)), - error_message="Error getting flavor with ID {id}".format(id=id) - ) - flavor = self._normalize_flavor( - self._get_and_munchify('flavor', data)) - - if not flavor.extra_specs and get_extra: - endpoint = "/flavors/{id}/os-extra_specs".format( - id=flavor.id) - try: - data = proxy._json_response( - self.compute.get(endpoint), - error_message="Error fetching flavor extra specs") - flavor.extra_specs = self._get_and_munchify( - 'extra_specs', data) - except exc.OpenStackCloudHTTPError as e: - flavor.extra_specs = {} - self.log.debug( - 'Fetching extra specs for flavor failed:' - ' %(msg)s', {'msg': str(e)}) - - return flavor - - def get_security_group(self, name_or_id, filters=None): - """Get a security group by name or ID. - - :param name_or_id: Name or ID of the security group. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - OR - A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - - :returns: A security group ``munch.Munch`` or None if no matching - security group is found. - - """ - return _utils._get_entity( - self, 'security_group', name_or_id, filters) - - def get_security_group_by_id(self, id): - """ Get a security group by ID - - :param id: ID of the security group. - :returns: A security group ``munch.Munch``. - """ - if not self._has_secgroups(): - raise exc.OpenStackCloudUnavailableFeature( - "Unavailable feature: security groups" - ) - error_message = ("Error getting security group with" - " ID {id}".format(id=id)) - if self._use_neutron_secgroups(): - resp = self.network.get('/security-groups/{id}'.format(id=id)) - data = proxy._json_response(resp, error_message=error_message) - else: - data = proxy._json_response( - self.compute.get( - '/os-security-groups/{id}'.format(id=id)), - error_message=error_message) - return self._normalize_secgroup( - self._get_and_munchify('security_group', data)) - - def get_server_console(self, server, length=None): - """Get the console log for a server. - - :param server: The server to fetch the console log for. Can be either - a server dict or the Name or ID of the server. - :param int length: The number of lines you would like to retrieve from - the end of the log. (optional, defaults to all) - - :returns: A string containing the text of the console log or an - empty string if the cloud does not support console logs. - :raises: OpenStackCloudException if an invalid server argument is given - or if something else unforseen happens - """ - - if not isinstance(server, dict): - server = self.get_server(server, bare=True) - - if not server: - raise exc.OpenStackCloudException( - "Console log requested for invalid server") - - try: - return self._get_server_console_output(server['id'], length) - except exc.OpenStackCloudBadRequest: - return "" - - def _get_server_console_output(self, server_id, length=None): - data = proxy._json_response(self.compute.post( - '/servers/{server_id}/action'.format(server_id=server_id), - json={'os-getConsoleOutput': {'length': length}})) - return self._get_and_munchify('output', data) - - def get_server( - self, name_or_id=None, filters=None, detailed=False, bare=False, - all_projects=False): - """Get a server by name or ID. - - :param name_or_id: Name or ID of the server. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - OR - A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - :param detailed: Whether or not to add detailed additional information. - Defaults to False. - :param bare: Whether to skip adding any additional information to the - server record. Defaults to False, meaning the addresses - dict will be populated as needed from neutron. Setting - to True implies detailed = False. - :param all_projects: Whether to get server from all projects or just - the current auth scoped project. - - :returns: A server ``munch.Munch`` or None if no matching server is - found. - - """ - searchfunc = functools.partial(self.search_servers, - detailed=detailed, bare=True, - all_projects=all_projects) - server = _utils._get_entity(self, searchfunc, name_or_id, filters) - return self._expand_server(server, detailed, bare) - - def _expand_server(self, server, detailed, bare): - if bare or not server: - return server - elif detailed: - return meta.get_hostvars_from_server(self, server) - else: - return meta.add_server_interfaces(self, server) - - def get_server_by_id(self, id): - data = proxy._json_response( - self.compute.get('/servers/{id}'.format(id=id))) - server = self._get_and_munchify('server', data) - return meta.add_server_interfaces(self, self._normalize_server(server)) - - def get_server_group(self, name_or_id=None, filters=None): - """Get a server group by name or ID. - - :param name_or_id: Name or ID of the server group. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'policy': 'affinity', - } - - OR - A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - - :returns: A server groups dict or None if no matching server group - is found. - - """ - return _utils._get_entity(self, 'server_group', name_or_id, - filters) - - def get_image(self, name_or_id, filters=None): - """Get an image by name or ID. - - :param name_or_id: Name or ID of the image. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - OR - A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - - :returns: An image ``munch.Munch`` or None if no matching image - is found - - """ - return _utils._get_entity(self, 'image', name_or_id, filters) - - def get_image_by_id(self, id): - """ Get a image by ID - - :param id: ID of the image. - :returns: An image ``munch.Munch``. - """ - data = self._image_client.get( - '/images/{id}'.format(id=id), - error_message="Error getting image with ID {id}".format(id=id) - ) - key = 'image' if 'image' in data else None - image = self._normalize_image( - self._get_and_munchify(key, data)) - - return image - - def download_image( - self, name_or_id, output_path=None, output_file=None, - chunk_size=1024): - """Download an image by name or ID - - :param str name_or_id: Name or ID of the image. - :param output_path: the output path to write the image to. Either this - or output_file must be specified - :param output_file: a file object (or file-like object) to write the - image data to. Only write() will be called on this object. Either - this or output_path must be specified - :param int chunk_size: size in bytes to read from the wire and buffer - at one time. Defaults to 1024 - - :raises: OpenStackCloudException in the event download_image is called - without exactly one of either output_path or output_file - :raises: OpenStackCloudResourceNotFound if no images are found matching - the name or ID provided - """ - if output_path is None and output_file is None: - raise exc.OpenStackCloudException( - 'No output specified, an output path or file object' - ' is necessary to write the image data to') - elif output_path is not None and output_file is not None: - raise exc.OpenStackCloudException( - 'Both an output path and file object were provided,' - ' however only one can be used at once') - - image = self.search_images(name_or_id) - if len(image) == 0: - raise exc.OpenStackCloudResourceNotFound( - "No images with name or ID %s were found" % name_or_id, None) - if self._is_client_version('image', 2): - endpoint = '/images/{id}/file'.format(id=image[0]['id']) - else: - endpoint = '/images/{id}'.format(id=image[0]['id']) - - response = self._image_client.get(endpoint, stream=True) - - with _utils.shade_exceptions("Unable to download image"): - if output_path: - with open(output_path, 'wb') as fd: - for chunk in response.iter_content(chunk_size=chunk_size): - fd.write(chunk) - return - elif output_file: - for chunk in response.iter_content(chunk_size=chunk_size): - output_file.write(chunk) - return - - def get_floating_ip(self, id, filters=None): - """Get a floating IP by ID - - :param id: ID of the floating IP. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - OR - A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - - :returns: A floating IP ``munch.Munch`` or None if no matching floating - IP is found. - - """ - return _utils._get_entity(self, 'floating_ip', id, filters) - - def get_floating_ip_by_id(self, id): - """ Get a floating ip by ID - - :param id: ID of the floating ip. - :returns: A floating ip ``munch.Munch``. - """ - error_message = "Error getting floating ip with ID {id}".format(id=id) - - if self._use_neutron_floating(): - data = proxy._json_response( - self.network.get('/floatingips/{id}'.format(id=id)), - error_message=error_message) - return self._normalize_floating_ip( - self._get_and_munchify('floatingip', data)) - else: - data = proxy._json_response( - self.compute.get('/os-floating-ips/{id}'.format(id=id)), - error_message=error_message) - return self._normalize_floating_ip( - self._get_and_munchify('floating_ip', data)) - - def get_stack(self, name_or_id, filters=None, resolve_outputs=True): - """Get exactly one stack. - - :param name_or_id: Name or ID of the desired stack. - :param filters: a dict containing additional filters to use. e.g. - {'stack_status': 'CREATE_COMPLETE'} - :param resolve_outputs: If True, then outputs for this - stack will be resolved - - :returns: a ``munch.Munch`` containing the stack description - - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call or if multiple matches are found. - """ - - def _search_one_stack(name_or_id=None, filters=None): - # stack names are mandatory and enforced unique in the project - # so a StackGet can always be used for name or ID. - try: - url = '/stacks/{name_or_id}'.format(name_or_id=name_or_id) - if not resolve_outputs: - url = '{url}?resolve_outputs=False'.format(url=url) - data = self._orchestration_client.get( - url, - error_message="Error fetching stack") - stack = self._get_and_munchify('stack', data) - # Treat DELETE_COMPLETE stacks as a NotFound - if stack['stack_status'] == 'DELETE_COMPLETE': - return [] - except exc.OpenStackCloudURINotFound: - return [] - stack = self._normalize_stack(stack) - return _utils._filter_list([stack], name_or_id, filters) - - return _utils._get_entity( - self, _search_one_stack, name_or_id, filters) - - def create_keypair(self, name, public_key=None): - """Create a new keypair. - - :param name: Name of the keypair being created. - :param public_key: Public key for the new keypair. - - :raises: OpenStackCloudException on operation error. - """ - keypair = { - 'name': name, - } - if public_key: - keypair['public_key'] = public_key - data = proxy._json_response( - self.compute.post( - '/os-keypairs', - json={'keypair': keypair}), - error_message="Unable to create keypair {name}".format(name=name)) - return self._normalize_keypair( - self._get_and_munchify('keypair', data)) - - def delete_keypair(self, name): - """Delete a keypair. - - :param name: Name of the keypair to delete. - - :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. - """ - try: - proxy._json_response(self.compute.delete( - '/os-keypairs/{name}'.format(name=name))) - except exc.OpenStackCloudURINotFound: - self.log.debug("Keypair %s not found for deleting", name) - return False - return True - - def create_network(self, name, shared=False, admin_state_up=True, - external=False, provider=None, project_id=None, - availability_zone_hints=None, - port_security_enabled=None, - mtu_size=None): - """Create a network. - - :param string name: Name of the network being created. - :param bool shared: Set the network as shared. - :param bool admin_state_up: Set the network administrative state to up. - :param bool external: Whether this network is externally accessible. - :param dict provider: A dict of network provider options. Example:: - - { 'network_type': 'vlan', 'segmentation_id': 'vlan1' } - :param string project_id: Specify the project ID this network - will be created on (admin-only). - :param types.ListType availability_zone_hints: A list of availability - zone hints. - :param bool port_security_enabled: Enable / Disable port security - :param int mtu_size: maximum transmission unit value to address - fragmentation. Minimum value is 68 for IPv4, and 1280 for IPv6. - - :returns: The network object. - :raises: OpenStackCloudException on operation error. - """ - network = { - 'name': name, - 'admin_state_up': admin_state_up, - } - - if shared: - network['shared'] = shared - - if project_id is not None: - network['tenant_id'] = project_id - - if availability_zone_hints is not None: - if not isinstance(availability_zone_hints, list): - raise exc.OpenStackCloudException( - "Parameter 'availability_zone_hints' must be a list") - if not self._has_neutron_extension('network_availability_zone'): - raise exc.OpenStackCloudUnavailableExtension( - 'network_availability_zone extension is not available on ' - 'target cloud') - network['availability_zone_hints'] = availability_zone_hints - - if provider: - if not isinstance(provider, dict): - raise exc.OpenStackCloudException( - "Parameter 'provider' must be a dict") - # Only pass what we know - for attr in ('physical_network', 'network_type', - 'segmentation_id'): - if attr in provider: - arg = "provider:" + attr - network[arg] = provider[attr] - - # Do not send 'router:external' unless it is explicitly - # set since sending it *might* cause "Forbidden" errors in - # some situations. It defaults to False in the client, anyway. - if external: - network['router:external'] = True - - if port_security_enabled is not None: - if not isinstance(port_security_enabled, bool): - raise exc.OpenStackCloudException( - "Parameter 'port_security_enabled' must be a bool") - network['port_security_enabled'] = port_security_enabled - - if mtu_size: - if not isinstance(mtu_size, int): - raise exc.OpenStackCloudException( - "Parameter 'mtu_size' must be an integer.") - if not mtu_size >= 68: - raise exc.OpenStackCloudException( - "Parameter 'mtu_size' must be greater than 67.") - - network['mtu'] = mtu_size - - data = self.network.post("/networks.json", json={'network': network}) - - # Reset cache so the new network is picked up - self._reset_network_caches() - return self._get_and_munchify('network', data) - - @_utils.valid_kwargs("name", "shared", "admin_state_up", "external", - "provider", "mtu_size", "port_security_enabled") - def update_network(self, name_or_id, **kwargs): - """Update a network. - - :param string name_or_id: Name or ID of the network being updated. - :param string name: New name of the network. - :param bool shared: Set the network as shared. - :param bool admin_state_up: Set the network administrative state to up. - :param bool external: Whether this network is externally accessible. - :param dict provider: A dict of network provider options. Example:: - - { 'network_type': 'vlan', 'segmentation_id': 'vlan1' } - :param int mtu_size: New maximum transmission unit value to address - fragmentation. Minimum value is 68 for IPv4, and 1280 for IPv6. - :param bool port_security_enabled: Enable or disable port security. - - :returns: The updated network object. - :raises: OpenStackCloudException on operation error. - """ - if 'provider' in kwargs: - if not isinstance(kwargs['provider'], dict): - raise exc.OpenStackCloudException( - "Parameter 'provider' must be a dict") - # Only pass what we know - provider = {} - for key in kwargs['provider']: - if key in ('physical_network', 'network_type', - 'segmentation_id'): - provider['provider:' + key] = kwargs['provider'][key] - kwargs['provider'] = provider - - if 'external' in kwargs: - kwargs['router:external'] = kwargs.pop('external') - - if 'port_security_enabled' in kwargs: - if not isinstance(kwargs['port_security_enabled'], bool): - raise exc.OpenStackCloudException( - "Parameter 'port_security_enabled' must be a bool") - - if 'mtu_size' in kwargs: - if not isinstance(kwargs['mtu_size'], int): - raise exc.OpenStackCloudException( - "Parameter 'mtu_size' must be an integer.") - if kwargs['mtu_size'] < 68: - raise exc.OpenStackCloudException( - "Parameter 'mtu_size' must be greater than 67.") - kwargs['mtu'] = kwargs.pop('mtu_size') - - network = self.get_network(name_or_id) - if not network: - raise exc.OpenStackCloudException( - "Network %s not found." % name_or_id) - - data = proxy._json_response(self.network.put( - "/networks/{net_id}.json".format(net_id=network.id), - json={"network": kwargs}), - error_message="Error updating network {0}".format(name_or_id)) - - self._reset_network_caches() - - return self._get_and_munchify('network', data) - - def delete_network(self, name_or_id): - """Delete a network. - - :param name_or_id: Name or ID of the network being deleted. - - :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. - """ - network = self.get_network(name_or_id) - if not network: - self.log.debug("Network %s not found for deleting", name_or_id) - return False - - exceptions.raise_from_response(self.network.delete( - "/networks/{network_id}.json".format(network_id=network['id']))) - - # Reset cache so the deleted network is removed - self._reset_network_caches() - - return True - - @_utils.valid_kwargs("name", "description", "shared", "default", - "project_id") - def create_qos_policy(self, **kwargs): - """Create a QoS policy. - - :param string name: Name of the QoS policy being created. - :param string description: Description of created QoS policy. - :param bool shared: Set the QoS policy as shared. - :param bool default: Set the QoS policy as default for project. - :param string project_id: Specify the project ID this QoS policy - will be created on (admin-only). - - :returns: The QoS policy object. - :raises: OpenStackCloudException on operation error. - """ - if not self._has_neutron_extension('qos'): - raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') - - default = kwargs.pop("default", None) - if default is not None: - if self._has_neutron_extension('qos-default'): - kwargs['is_default'] = default - else: - self.log.debug("'qos-default' extension is not available on " - "target cloud") - - data = self.network.post("/qos/policies.json", json={'policy': kwargs}) - return self._get_and_munchify('policy', data) - - @_utils.valid_kwargs("name", "description", "shared", "default", - "project_id") - def update_qos_policy(self, name_or_id, **kwargs): - """Update an existing QoS policy. - - :param string name_or_id: - Name or ID of the QoS policy to update. - :param string policy_name: - The new name of the QoS policy. - :param string description: - The new description of the QoS policy. - :param bool shared: - If True, the QoS policy will be set as shared. - :param bool default: - If True, the QoS policy will be set as default for project. - - :returns: The updated QoS policy object. - :raises: OpenStackCloudException on operation error. - """ - if not self._has_neutron_extension('qos'): - raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') - - default = kwargs.pop("default", None) - if default is not None: - if self._has_neutron_extension('qos-default'): - kwargs['is_default'] = default - else: - self.log.debug("'qos-default' extension is not available on " - "target cloud") - - if not kwargs: - self.log.debug("No QoS policy data to update") - return - - curr_policy = self.get_qos_policy(name_or_id) - if not curr_policy: - raise exc.OpenStackCloudException( - "QoS policy %s not found." % name_or_id) - - data = self.network.put( - "/qos/policies/{policy_id}.json".format( - policy_id=curr_policy['id']), - json={'policy': kwargs}) - return self._get_and_munchify('policy', data) - - def delete_qos_policy(self, name_or_id): - """Delete a QoS policy. - - :param name_or_id: Name or ID of the policy being deleted. - - :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. - """ - if not self._has_neutron_extension('qos'): - raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') - policy = self.get_qos_policy(name_or_id) - if not policy: - self.log.debug("QoS policy %s not found for deleting", name_or_id) - return False - - exceptions.raise_from_response(self.network.delete( - "/qos/policies/{policy_id}.json".format(policy_id=policy['id']))) - - return True - - def search_qos_bandwidth_limit_rules(self, policy_name_or_id, rule_id=None, - filters=None): - """Search QoS bandwidth limit rules - - :param string policy_name_or_id: Name or ID of the QoS policy to which - rules should be associated. - :param string rule_id: ID of searched rule. - :param filters: a dict containing additional filters to use. e.g. - {'max_kbps': 1000} - - :returns: a list of ``munch.Munch`` containing the bandwidth limit - rule descriptions. - - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call. - """ - rules = self.list_qos_bandwidth_limit_rules(policy_name_or_id, filters) - return _utils._filter_list(rules, rule_id, filters) - - def list_qos_bandwidth_limit_rules(self, policy_name_or_id, filters=None): - """List all available QoS bandwidth limit rules. - - :param string policy_name_or_id: Name or ID of the QoS policy from - from rules should be listed. - :param filters: (optional) dict of filter conditions to push down - :returns: A list of ``munch.Munch`` containing rule info. - - :raises: ``OpenStackCloudResourceNotFound`` if QoS policy will not be - found. - """ - if not self._has_neutron_extension('qos'): - raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') - - policy = self.get_qos_policy(policy_name_or_id) - if not policy: - raise exc.OpenStackCloudResourceNotFound( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) - - # Translate None from search interface to empty {} for kwargs below - if not filters: - filters = {} - - resp = self.network.get( - "/qos/policies/{policy_id}/bandwidth_limit_rules.json".format( - policy_id=policy['id']), - params=filters) - data = proxy._json_response( - resp, - error_message="Error fetching QoS bandwidth limit rules from " - "{policy}".format(policy=policy['id'])) - return self._get_and_munchify('bandwidth_limit_rules', data) - - def get_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): - """Get a QoS bandwidth limit rule by name or ID. - - :param string policy_name_or_id: Name or ID of the QoS policy to which - rule should be associated. - :param rule_id: ID of the rule. - - :returns: A bandwidth limit rule ``munch.Munch`` or None if - no matching rule is found. - - """ - if not self._has_neutron_extension('qos'): - raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') - - policy = self.get_qos_policy(policy_name_or_id) - if not policy: - raise exc.OpenStackCloudResourceNotFound( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) - - resp = self.network.get( - "/qos/policies/{policy_id}/bandwidth_limit_rules/{rule_id}.json". - format(policy_id=policy['id'], rule_id=rule_id)) - data = proxy._json_response( - resp, - error_message="Error fetching QoS bandwidth limit rule {rule_id} " - "from {policy}".format(rule_id=rule_id, - policy=policy['id'])) - return self._get_and_munchify('bandwidth_limit_rule', data) - - @_utils.valid_kwargs("max_burst_kbps", "direction") - def create_qos_bandwidth_limit_rule(self, policy_name_or_id, max_kbps, - **kwargs): - """Create a QoS bandwidth limit rule. - - :param string policy_name_or_id: Name or ID of the QoS policy to which - rule should be associated. - :param int max_kbps: Maximum bandwidth limit value - (in kilobits per second). - :param int max_burst_kbps: Maximum burst value (in kilobits). - :param string direction: Ingress or egress. - The direction in which the traffic will be limited. - - :returns: The QoS bandwidth limit rule. - :raises: OpenStackCloudException on operation error. - """ - if not self._has_neutron_extension('qos'): - raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') - - policy = self.get_qos_policy(policy_name_or_id) - if not policy: - raise exc.OpenStackCloudResourceNotFound( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) - - if kwargs.get("direction") is not None: - if not self._has_neutron_extension('qos-bw-limit-direction'): - kwargs.pop("direction") - self.log.debug( - "'qos-bw-limit-direction' extension is not available on " - "target cloud") - - kwargs['max_kbps'] = max_kbps - data = self.network.post( - "/qos/policies/{policy_id}/bandwidth_limit_rules".format( - policy_id=policy['id']), - json={'bandwidth_limit_rule': kwargs}) - return self._get_and_munchify('bandwidth_limit_rule', data) - - @_utils.valid_kwargs("max_kbps", "max_burst_kbps", "direction") - def update_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id, - **kwargs): - """Update a QoS bandwidth limit rule. - - :param string policy_name_or_id: Name or ID of the QoS policy to which - rule is associated. - :param string rule_id: ID of rule to update. - :param int max_kbps: Maximum bandwidth limit value - (in kilobits per second). - :param int max_burst_kbps: Maximum burst value (in kilobits). - :param string direction: Ingress or egress. - The direction in which the traffic will be limited. - - :returns: The updated QoS bandwidth limit rule. - :raises: OpenStackCloudException on operation error. - """ - if not self._has_neutron_extension('qos'): - raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') - - policy = self.get_qos_policy(policy_name_or_id) - if not policy: - raise exc.OpenStackCloudResourceNotFound( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) - - if kwargs.get("direction") is not None: - if not self._has_neutron_extension('qos-bw-limit-direction'): - kwargs.pop("direction") - self.log.debug( - "'qos-bw-limit-direction' extension is not available on " - "target cloud") - - if not kwargs: - self.log.debug("No QoS bandwidth limit rule data to update") - return - - curr_rule = self.get_qos_bandwidth_limit_rule( - policy_name_or_id, rule_id) - if not curr_rule: - raise exc.OpenStackCloudException( - "QoS bandwidth_limit_rule {rule_id} not found in policy " - "{policy_id}".format(rule_id=rule_id, - policy_id=policy['id'])) - - data = self.network.put( - "/qos/policies/{policy_id}/bandwidth_limit_rules/{rule_id}.json". - format(policy_id=policy['id'], rule_id=rule_id), - json={'bandwidth_limit_rule': kwargs}) - return self._get_and_munchify('bandwidth_limit_rule', data) - - def delete_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): - """Delete a QoS bandwidth limit rule. - - :param string policy_name_or_id: Name or ID of the QoS policy to which - rule is associated. - :param string rule_id: ID of rule to update. - - :raises: OpenStackCloudException on operation error. - """ - if not self._has_neutron_extension('qos'): - raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') - - policy = self.get_qos_policy(policy_name_or_id) - if not policy: - raise exc.OpenStackCloudResourceNotFound( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) - - try: - exceptions.raise_from_response(self.network.delete( - "/qos/policies/{policy}/bandwidth_limit_rules/{rule}.json". - format(policy=policy['id'], rule=rule_id))) - except exc.OpenStackCloudURINotFound: - self.log.debug( - "QoS bandwidth limit rule {rule_id} not found in policy " - "{policy_id}. Ignoring.".format(rule_id=rule_id, - policy_id=policy['id'])) - return False - - return True - - def search_qos_dscp_marking_rules(self, policy_name_or_id, rule_id=None, - filters=None): - """Search QoS DSCP marking rules - - :param string policy_name_or_id: Name or ID of the QoS policy to which - rules should be associated. - :param string rule_id: ID of searched rule. - :param filters: a dict containing additional filters to use. e.g. - {'dscp_mark': 32} - - :returns: a list of ``munch.Munch`` containing the dscp marking - rule descriptions. - - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call. - """ - rules = self.list_qos_dscp_marking_rules(policy_name_or_id, filters) - return _utils._filter_list(rules, rule_id, filters) - - def list_qos_dscp_marking_rules(self, policy_name_or_id, filters=None): - """List all available QoS DSCP marking rules. - - :param string policy_name_or_id: Name or ID of the QoS policy from - from rules should be listed. - :param filters: (optional) dict of filter conditions to push down - :returns: A list of ``munch.Munch`` containing rule info. - - :raises: ``OpenStackCloudResourceNotFound`` if QoS policy will not be - found. - """ - if not self._has_neutron_extension('qos'): - raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') - - policy = self.get_qos_policy(policy_name_or_id) - if not policy: - raise exc.OpenStackCloudResourceNotFound( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) - - # Translate None from search interface to empty {} for kwargs below - if not filters: - filters = {} - - resp = self.network.get( - "/qos/policies/{policy_id}/dscp_marking_rules.json".format( - policy_id=policy['id']), - params=filters) - data = proxy._json_response( - resp, - error_message="Error fetching QoS DSCP marking rules from " - "{policy}".format(policy=policy['id'])) - return self._get_and_munchify('dscp_marking_rules', data) - - def get_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): - """Get a QoS DSCP marking rule by name or ID. - - :param string policy_name_or_id: Name or ID of the QoS policy to which - rule should be associated. - :param rule_id: ID of the rule. - - :returns: A bandwidth limit rule ``munch.Munch`` or None if - no matching rule is found. - - """ - if not self._has_neutron_extension('qos'): - raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') - - policy = self.get_qos_policy(policy_name_or_id) - if not policy: - raise exc.OpenStackCloudResourceNotFound( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) - - resp = self.network.get( - "/qos/policies/{policy_id}/dscp_marking_rules/{rule_id}.json". - format(policy_id=policy['id'], rule_id=rule_id)) - data = proxy._json_response( - resp, - error_message="Error fetching QoS DSCP marking rule {rule_id} " - "from {policy}".format(rule_id=rule_id, - policy=policy['id'])) - return self._get_and_munchify('dscp_marking_rule', data) - - def create_qos_dscp_marking_rule(self, policy_name_or_id, dscp_mark): - """Create a QoS DSCP marking rule. - - :param string policy_name_or_id: Name or ID of the QoS policy to which - rule should be associated. - :param int dscp_mark: DSCP mark value - - :returns: The QoS DSCP marking rule. - :raises: OpenStackCloudException on operation error. - """ - if not self._has_neutron_extension('qos'): - raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') - - policy = self.get_qos_policy(policy_name_or_id) - if not policy: - raise exc.OpenStackCloudResourceNotFound( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) - - body = { - 'dscp_mark': dscp_mark - } - data = self.network.post( - "/qos/policies/{policy_id}/dscp_marking_rules".format( - policy_id=policy['id']), - json={'dscp_marking_rule': body}) - return self._get_and_munchify('dscp_marking_rule', data) - - @_utils.valid_kwargs("dscp_mark") - def update_qos_dscp_marking_rule(self, policy_name_or_id, rule_id, - **kwargs): - """Update a QoS DSCP marking rule. - - :param string policy_name_or_id: Name or ID of the QoS policy to which - rule is associated. - :param string rule_id: ID of rule to update. - :param int dscp_mark: DSCP mark value - - :returns: The updated QoS bandwidth limit rule. - :raises: OpenStackCloudException on operation error. - """ - if not self._has_neutron_extension('qos'): - raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') - - policy = self.get_qos_policy(policy_name_or_id) - if not policy: - raise exc.OpenStackCloudResourceNotFound( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) - - if not kwargs: - self.log.debug("No QoS DSCP marking rule data to update") - return - - curr_rule = self.get_qos_dscp_marking_rule( - policy_name_or_id, rule_id) - if not curr_rule: - raise exc.OpenStackCloudException( - "QoS dscp_marking_rule {rule_id} not found in policy " - "{policy_id}".format(rule_id=rule_id, - policy_id=policy['id'])) - - data = self.network.put( - "/qos/policies/{policy_id}/dscp_marking_rules/{rule_id}.json". - format(policy_id=policy['id'], rule_id=rule_id), - json={'dscp_marking_rule': kwargs}) - return self._get_and_munchify('dscp_marking_rule', data) - - def delete_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): - """Delete a QoS DSCP marking rule. - - :param string policy_name_or_id: Name or ID of the QoS policy to which - rule is associated. - :param string rule_id: ID of rule to update. - - :raises: OpenStackCloudException on operation error. - """ - if not self._has_neutron_extension('qos'): - raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') - - policy = self.get_qos_policy(policy_name_or_id) - if not policy: - raise exc.OpenStackCloudResourceNotFound( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) - - try: - exceptions.raise_from_response(self.network.delete( - "/qos/policies/{policy}/dscp_marking_rules/{rule}.json". - format(policy=policy['id'], rule=rule_id))) - except exc.OpenStackCloudURINotFound: - self.log.debug( - "QoS DSCP marking rule {rule_id} not found in policy " - "{policy_id}. Ignoring.".format(rule_id=rule_id, - policy_id=policy['id'])) - return False - - return True - - def search_qos_minimum_bandwidth_rules(self, policy_name_or_id, - rule_id=None, filters=None): - """Search QoS minimum bandwidth rules - - :param string policy_name_or_id: Name or ID of the QoS policy to which - rules should be associated. - :param string rule_id: ID of searched rule. - :param filters: a dict containing additional filters to use. e.g. - {'min_kbps': 1000} - - :returns: a list of ``munch.Munch`` containing the bandwidth limit - rule descriptions. - - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call. - """ - rules = self.list_qos_minimum_bandwidth_rules( - policy_name_or_id, filters) - return _utils._filter_list(rules, rule_id, filters) - - def list_qos_minimum_bandwidth_rules(self, policy_name_or_id, - filters=None): - """List all available QoS minimum bandwidth rules. - - :param string policy_name_or_id: Name or ID of the QoS policy from - from rules should be listed. - :param filters: (optional) dict of filter conditions to push down - :returns: A list of ``munch.Munch`` containing rule info. - - :raises: ``OpenStackCloudResourceNotFound`` if QoS policy will not be - found. - """ - if not self._has_neutron_extension('qos'): - raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') - - policy = self.get_qos_policy(policy_name_or_id) - if not policy: - raise exc.OpenStackCloudResourceNotFound( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) - - # Translate None from search interface to empty {} for kwargs below - if not filters: - filters = {} - - resp = self.network.get( - "/qos/policies/{policy_id}/minimum_bandwidth_rules.json".format( - policy_id=policy['id']), - params=filters) - data = proxy._json_response( - resp, - error_message="Error fetching QoS minimum bandwidth rules from " - "{policy}".format(policy=policy['id'])) - return self._get_and_munchify('minimum_bandwidth_rules', data) - - def get_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): - """Get a QoS minimum bandwidth rule by name or ID. - - :param string policy_name_or_id: Name or ID of the QoS policy to which - rule should be associated. - :param rule_id: ID of the rule. - - :returns: A bandwidth limit rule ``munch.Munch`` or None if - no matching rule is found. - - """ - if not self._has_neutron_extension('qos'): - raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') - - policy = self.get_qos_policy(policy_name_or_id) - if not policy: - raise exc.OpenStackCloudResourceNotFound( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) - - resp = self.network.get( - "/qos/policies/{policy_id}/minimum_bandwidth_rules/{rule_id}.json". - format(policy_id=policy['id'], rule_id=rule_id)) - data = proxy._json_response( - resp, - error_message="Error fetching QoS minimum_bandwidth rule {rule_id}" - " from {policy}".format(rule_id=rule_id, - policy=policy['id'])) - return self._get_and_munchify('minimum_bandwidth_rule', data) - - @_utils.valid_kwargs("direction") - def create_qos_minimum_bandwidth_rule(self, policy_name_or_id, min_kbps, - **kwargs): - """Create a QoS minimum bandwidth limit rule. - - :param string policy_name_or_id: Name or ID of the QoS policy to which - rule should be associated. - :param int min_kbps: Minimum bandwidth value (in kilobits per second). - :param string direction: Ingress or egress. - The direction in which the traffic will be available. - - :returns: The QoS minimum bandwidth rule. - :raises: OpenStackCloudException on operation error. - """ - if not self._has_neutron_extension('qos'): - raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') - - policy = self.get_qos_policy(policy_name_or_id) - if not policy: - raise exc.OpenStackCloudResourceNotFound( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) - - kwargs['min_kbps'] = min_kbps - data = self.network.post( - "/qos/policies/{policy_id}/minimum_bandwidth_rules".format( - policy_id=policy['id']), - json={'minimum_bandwidth_rule': kwargs}) - return self._get_and_munchify('minimum_bandwidth_rule', data) - - @_utils.valid_kwargs("min_kbps", "direction") - def update_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id, - **kwargs): - """Update a QoS minimum bandwidth rule. - - :param string policy_name_or_id: Name or ID of the QoS policy to which - rule is associated. - :param string rule_id: ID of rule to update. - :param int min_kbps: Minimum bandwidth value (in kilobits per second). - :param string direction: Ingress or egress. - The direction in which the traffic will be available. - - :returns: The updated QoS minimum bandwidth rule. - :raises: OpenStackCloudException on operation error. - """ - if not self._has_neutron_extension('qos'): - raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') - - policy = self.get_qos_policy(policy_name_or_id) - if not policy: - raise exc.OpenStackCloudResourceNotFound( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) - - if not kwargs: - self.log.debug("No QoS minimum bandwidth rule data to update") - return - - curr_rule = self.get_qos_minimum_bandwidth_rule( - policy_name_or_id, rule_id) - if not curr_rule: - raise exc.OpenStackCloudException( - "QoS minimum_bandwidth_rule {rule_id} not found in policy " - "{policy_id}".format(rule_id=rule_id, - policy_id=policy['id'])) - - data = self.network.put( - "/qos/policies/{policy_id}/minimum_bandwidth_rules/{rule_id}.json". - format(policy_id=policy['id'], rule_id=rule_id), - json={'minimum_bandwidth_rule': kwargs}) - return self._get_and_munchify('minimum_bandwidth_rule', data) - - def delete_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): - """Delete a QoS minimum bandwidth rule. - - :param string policy_name_or_id: Name or ID of the QoS policy to which - rule is associated. - :param string rule_id: ID of rule to delete. - - :raises: OpenStackCloudException on operation error. - """ - if not self._has_neutron_extension('qos'): - raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') - - policy = self.get_qos_policy(policy_name_or_id) - if not policy: - raise exc.OpenStackCloudResourceNotFound( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) - - try: - exceptions.raise_from_response(self.network.delete( - "/qos/policies/{policy}/minimum_bandwidth_rules/{rule}.json". - format(policy=policy['id'], rule=rule_id))) - except exc.OpenStackCloudURINotFound: - self.log.debug( - "QoS minimum bandwidth rule {rule_id} not found in policy " - "{policy_id}. Ignoring.".format(rule_id=rule_id, - policy_id=policy['id'])) - return False - - return True - - def _build_external_gateway_info(self, ext_gateway_net_id, enable_snat, - ext_fixed_ips): - info = {} - if ext_gateway_net_id: - info['network_id'] = ext_gateway_net_id - # Only send enable_snat if it is explicitly set. - if enable_snat is not None: - info['enable_snat'] = enable_snat - if ext_fixed_ips: - info['external_fixed_ips'] = ext_fixed_ips - if info: - return info - return None - - def add_router_interface(self, router, subnet_id=None, port_id=None): - """Attach a subnet to an internal router interface. - - Either a subnet ID or port ID must be specified for the internal - interface. Supplying both will result in an error. - - :param dict router: The dict object of the router being changed - :param string subnet_id: The ID of the subnet to use for the interface - :param string port_id: The ID of the port to use for the interface - - :returns: A ``munch.Munch`` with the router ID (ID), - subnet ID (subnet_id), port ID (port_id) and tenant ID - (tenant_id). - - :raises: OpenStackCloudException on operation error. - """ - json_body = {} - if subnet_id: - json_body['subnet_id'] = subnet_id - if port_id: - json_body['port_id'] = port_id - - return proxy._json_response( - self.network.put( - "/routers/{router_id}/add_router_interface.json".format( - router_id=router['id']), - json=json_body), - error_message="Error attaching interface to router {0}".format( - router['id'])) - - def remove_router_interface(self, router, subnet_id=None, port_id=None): - """Detach a subnet from an internal router interface. - - At least one of subnet_id or port_id must be supplied. - - If you specify both subnet and port ID, the subnet ID must - correspond to the subnet ID of the first IP address on the port - specified by the port ID. Otherwise an error occurs. - - :param dict router: The dict object of the router being changed - :param string subnet_id: The ID of the subnet to use for the interface - :param string port_id: The ID of the port to use for the interface - - :returns: None on success - - :raises: OpenStackCloudException on operation error. - """ - json_body = {} - if subnet_id: - json_body['subnet_id'] = subnet_id - if port_id: - json_body['port_id'] = port_id - - if not json_body: - raise ValueError( - "At least one of subnet_id or port_id must be supplied.") - - exceptions.raise_from_response( - self.network.put( - "/routers/{router_id}/remove_router_interface.json".format( - router_id=router['id']), - json=json_body), - error_message="Error detaching interface from router {0}".format( - router['id'])) - - def list_router_interfaces(self, router, interface_type=None): - """List all interfaces for a router. - - :param dict router: A router dict object. - :param string interface_type: One of None, "internal", or "external". - Controls whether all, internal interfaces or external interfaces - are returned. - - :returns: A list of port ``munch.Munch`` objects. - """ - # Find only router interface and gateway ports, ignore L3 HA ports etc. - router_interfaces = self.search_ports(filters={ - 'device_id': router['id'], - 'device_owner': 'network:router_interface'} - ) + self.search_ports(filters={ - 'device_id': router['id'], - 'device_owner': 'network:router_interface_distributed'} - ) + self.search_ports(filters={ - 'device_id': router['id'], - 'device_owner': 'network:ha_router_replicated_interface'}) - router_gateways = self.search_ports(filters={ - 'device_id': router['id'], - 'device_owner': 'network:router_gateway'}) - ports = router_interfaces + router_gateways - - if interface_type: - if interface_type == 'internal': - return router_interfaces - if interface_type == 'external': - return router_gateways - return ports - - def create_router(self, name=None, admin_state_up=True, - ext_gateway_net_id=None, enable_snat=None, - ext_fixed_ips=None, project_id=None, - availability_zone_hints=None): - """Create a logical router. - - :param string name: The router name. - :param bool admin_state_up: The administrative state of the router. - :param string ext_gateway_net_id: Network ID for the external gateway. - :param bool enable_snat: Enable Source NAT (SNAT) attribute. - :param ext_fixed_ips: - List of dictionaries of desired IP and/or subnet on the - external network. Example:: - - [ - { - "subnet_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b", - "ip_address": "192.168.10.2" - } - ] - :param string project_id: Project ID for the router. - :param types.ListType availability_zone_hints: - A list of availability zone hints. - - :returns: The router object. - :raises: OpenStackCloudException on operation error. - """ - router = { - 'admin_state_up': admin_state_up - } - if project_id is not None: - router['tenant_id'] = project_id - if name: - router['name'] = name - ext_gw_info = self._build_external_gateway_info( - ext_gateway_net_id, enable_snat, ext_fixed_ips - ) - if ext_gw_info: - router['external_gateway_info'] = ext_gw_info - if availability_zone_hints is not None: - if not isinstance(availability_zone_hints, list): - raise exc.OpenStackCloudException( - "Parameter 'availability_zone_hints' must be a list") - if not self._has_neutron_extension('router_availability_zone'): - raise exc.OpenStackCloudUnavailableExtension( - 'router_availability_zone extension is not available on ' - 'target cloud') - router['availability_zone_hints'] = availability_zone_hints - - data = proxy._json_response( - self.network.post("/routers.json", json={"router": router}), - error_message="Error creating router {0}".format(name)) - return self._get_and_munchify('router', data) - - def update_router(self, name_or_id, name=None, admin_state_up=None, - ext_gateway_net_id=None, enable_snat=None, - ext_fixed_ips=None, routes=None): - """Update an existing logical router. - - :param string name_or_id: The name or UUID of the router to update. - :param string name: The new router name. - :param bool admin_state_up: The administrative state of the router. - :param string ext_gateway_net_id: - The network ID for the external gateway. - :param bool enable_snat: Enable Source NAT (SNAT) attribute. - :param ext_fixed_ips: - List of dictionaries of desired IP and/or subnet on the - external network. Example:: - - [ - { - "subnet_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b", - "ip_address": "192.168.10.2" - } - ] - :param list routes: - A list of dictionaries with destination and nexthop parameters. - Example:: - - [ - { - "destination": "179.24.1.0/24", - "nexthop": "172.24.3.99" - } - ] - :returns: The router object. - :raises: OpenStackCloudException on operation error. - """ - router = {} - if name: - router['name'] = name - if admin_state_up is not None: - router['admin_state_up'] = admin_state_up - ext_gw_info = self._build_external_gateway_info( - ext_gateway_net_id, enable_snat, ext_fixed_ips - ) - if ext_gw_info: - router['external_gateway_info'] = ext_gw_info - - if routes: - if self._has_neutron_extension('extraroute'): - router['routes'] = routes - else: - self.log.warn( - 'extra routes extension is not available on target cloud') - - if not router: - self.log.debug("No router data to update") - return - - curr_router = self.get_router(name_or_id) - if not curr_router: - raise exc.OpenStackCloudException( - "Router %s not found." % name_or_id) - - resp = self.network.put( - "/routers/{router_id}.json".format(router_id=curr_router['id']), - json={"router": router}) - data = proxy._json_response( - resp, - error_message="Error updating router {0}".format(name_or_id)) - return self._get_and_munchify('router', data) - - def delete_router(self, name_or_id): - """Delete a logical router. - - If a name, instead of a unique UUID, is supplied, it is possible - that we could find more than one matching router since names are - not required to be unique. An error will be raised in this case. - - :param name_or_id: Name or ID of the router being deleted. - - :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. - """ - router = self.get_router(name_or_id) - if not router: - self.log.debug("Router %s not found for deleting", name_or_id) - return False - - exceptions.raise_from_response(self.network.delete( - "/routers/{router_id}.json".format(router_id=router['id']), - error_message="Error deleting router {0}".format(name_or_id))) - - return True - - def get_image_exclude(self, name_or_id, exclude): - for image in self.search_images(name_or_id): - if exclude: - if exclude not in image.name: - return image - else: - return image - return None - - def get_image_name(self, image_id, exclude=None): - image = self.get_image_exclude(image_id, exclude) - if image: - return image.name - return None - - def get_image_id(self, image_name, exclude=None): - image = self.get_image_exclude(image_name, exclude) - if image: - return image.id - return None - - def create_image_snapshot( - self, name, server, wait=False, timeout=3600, **metadata): - """Create an image by snapshotting an existing server. - - ..note:: - On most clouds this is a cold snapshot - meaning that the server - in question will be shutdown before taking the snapshot. It is - possible that it's a live snapshot - but there is no way to know - as a user, so caveat emptor. - - :param name: Name of the image to be created - :param server: Server name or ID or dict representing the server - to be snapshotted - :param wait: If true, waits for image to be created. - :param timeout: Seconds to wait for image creation. None is forever. - :param metadata: Metadata to give newly-created image entity - - :returns: A ``munch.Munch`` of the Image object - - :raises: OpenStackCloudException if there are problems uploading - """ - if not isinstance(server, dict): - server_obj = self.get_server(server, bare=True) - if not server_obj: - raise exc.OpenStackCloudException( - "Server {server} could not be found and therefore" - " could not be snapshotted.".format(server=server)) - server = server_obj - response = proxy._json_response( - self.compute.post( - '/servers/{server_id}/action'.format(server_id=server['id']), - json={ - "createImage": { - "name": name, - "metadata": metadata, - } - })) - # You won't believe it - wait, who am I kidding - of course you will! - # Nova returns the URL of the image created in the Location - # header of the response. (what?) But, even better, the URL it responds - # with has a very good chance of being wrong (it is built from - # nova.conf values that point to internal API servers in any cloud - # large enough to have both public and internal endpoints. - # However, nobody has ever noticed this because novaclient doesn't - # actually use that URL - it extracts the id from the end of - # the url, then returns the id. This leads us to question: - # a) why Nova is going to return a value in a header - # b) why it's going to return data that probably broken - # c) indeed the very nature of the fabric of reality - # Although it fills us with existential dread, we have no choice but - # to follow suit like a lemming being forced over a cliff by evil - # producers from Disney. - # TODO(mordred) Update this to consume json microversion when it is - # available. - # blueprint:remove-create-image-location-header-response - image_id = response.headers['Location'].rsplit('/', 1)[1] - self.list_images.invalidate(self) - image = self.get_image(image_id) - - if not wait: - return image - return self.wait_for_image(image, timeout=timeout) - - def wait_for_image(self, image, timeout=3600): - image_id = image['id'] - for count in utils.iterate_timeout( - timeout, "Timeout waiting for image to snapshot"): - self.list_images.invalidate(self) - image = self.get_image(image_id) - if not image: - continue - if image['status'] == 'active': - return image - elif image['status'] == 'error': - raise exc.OpenStackCloudException( - 'Image {image} hit error state'.format(image=image_id)) - - def delete_image( - self, name_or_id, wait=False, timeout=3600, delete_objects=True): - """Delete an existing image. - - :param name_or_id: Name of the image to be deleted. - :param wait: If True, waits for image to be deleted. - :param timeout: Seconds to wait for image deletion. None is forever. - :param delete_objects: If True, also deletes uploaded swift objects. - - :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException if there are problems deleting. - """ - image = self.get_image(name_or_id) - if not image: - return False - self._image_client.delete( - '/images/{id}'.format(id=image.id), - error_message="Error in deleting image") - self.list_images.invalidate(self) - - # Task API means an image was uploaded to swift - if self.image_api_use_tasks and ( - self._IMAGE_OBJECT_KEY in image - or self._SHADE_IMAGE_OBJECT_KEY in image): - (container, objname) = image.get( - self._IMAGE_OBJECT_KEY, image.get( - self._SHADE_IMAGE_OBJECT_KEY)).split('/', 1) - self.delete_object(container=container, name=objname) - - if wait: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for the image to be deleted."): - self._get_cache(None).invalidate() - if self.get_image(image.id) is None: - break - return True - - def _get_name_and_filename(self, name): - # See if name points to an existing file - if os.path.exists(name): - # Neat. Easy enough - return (os.path.splitext(os.path.basename(name))[0], name) - - # Try appending the disk format - name_with_ext = '.'.join(( - name, self.config.config['image_format'])) - if os.path.exists(name_with_ext): - return (os.path.basename(name), name_with_ext) - - raise exc.OpenStackCloudException( - 'No filename parameter was given to create_image,' - ' and {name} was not the path to an existing file.' - ' Please provide either a path to an existing file' - ' or a name and a filename'.format(name=name)) - - def _hashes_up_to_date(self, md5, sha256, md5_key, sha256_key): - '''Compare md5 and sha256 hashes for being up to date - - md5 and sha256 are the current values. - md5_key and sha256_key are the previous values. - ''' - up_to_date = False - if md5 and md5_key == md5: - up_to_date = True - if sha256 and sha256_key == sha256: - up_to_date = True - if md5 and md5_key != md5: - up_to_date = False - if sha256 and sha256_key != sha256: - up_to_date = False - return up_to_date - - def create_image( - self, name, filename=None, - container=None, - md5=None, sha256=None, - disk_format=None, container_format=None, - disable_vendor_agent=True, - wait=False, timeout=3600, - allow_duplicates=False, meta=None, volume=None, **kwargs): - """Upload an image. - - :param str name: Name of the image to create. If it is a pathname - of an image, the name will be constructed from the - extensionless basename of the path. - :param str filename: The path to the file to upload, if needed. - (optional, defaults to None) - :param str container: Name of the container in swift where images - should be uploaded for import if the cloud - requires such a thing. (optiona, defaults to - 'images') - :param str md5: md5 sum of the image file. If not given, an md5 will - be calculated. - :param str sha256: sha256 sum of the image file. If not given, an md5 - will be calculated. - :param str disk_format: The disk format the image is in. (optional, - defaults to the os-client-config config value - for this cloud) - :param str container_format: The container format the image is in. - (optional, defaults to the - os-client-config config value for this - cloud) - :param bool disable_vendor_agent: Whether or not to append metadata - flags to the image to inform the - cloud in question to not expect a - vendor agent to be runing. - (optional, defaults to True) - :param bool wait: If true, waits for image to be created. Defaults to - true - however, be aware that one of the upload - methods is always synchronous. - :param timeout: Seconds to wait for image creation. None is forever. - :param allow_duplicates: If true, skips checks that enforce unique - image name. (optional, defaults to False) - :param meta: A dict of key/value pairs to use for metadata that - bypasses automatic type conversion. - :param volume: Name or ID or volume object of a volume to create an - image from. Mutually exclusive with (optional, defaults - to None) - - Additional kwargs will be passed to the image creation as additional - metadata for the image and will have all values converted to string - except for min_disk, min_ram, size and virtual_size which will be - converted to int. - - If you are sure you have all of your data types correct or have an - advanced need to be explicit, use meta. If you are just a normal - consumer, using kwargs is likely the right choice. - - If a value is in meta and kwargs, meta wins. - - :returns: A ``munch.Munch`` of the Image object - - :raises: OpenStackCloudException if there are problems uploading - """ - if volume: - image = self.block_storage.create_image( - name=name, volume=volume, - allow_duplicates=allow_duplicates, - container_format=container_format, disk_format=disk_format, - wait=wait, timeout=timeout) - else: - image = self.image.create_image( - name, filename=filename, - container=container, - md5=sha256, sha256=sha256, - disk_format=disk_format, container_format=container_format, - disable_vendor_agent=disable_vendor_agent, - wait=wait, timeout=timeout, - allow_duplicates=allow_duplicates, meta=meta, **kwargs) - - self._get_cache(None).invalidate() - if not wait: - return image - try: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for the image to finish."): - image_obj = self.get_image(image.id) - if image_obj and image_obj.status not in ('queued', 'saving'): - return image_obj - except exc.OpenStackCloudTimeout: - self.log.debug( - "Timeout waiting for image to become ready. Deleting.") - self.delete_image(image.id, wait=True) - raise - - def update_image_properties( - self, image=None, name_or_id=None, meta=None, **properties): - image = image or name_or_id - return self.image.update_image_properties( - image=image, meta=meta, **properties) - - def create_volume( - self, size, - wait=True, timeout=None, image=None, bootable=None, **kwargs): - """Create a volume. - - :param size: Size, in GB of the volume to create. - :param name: (optional) Name for the volume. - :param description: (optional) Name for the volume. - :param wait: If true, waits for volume to be created. - :param timeout: Seconds to wait for volume creation. None is forever. - :param image: (optional) Image name, ID or object from which to create - the volume - :param bootable: (optional) Make this volume bootable. If set, wait - will also be set to true. - :param kwargs: Keyword arguments as expected for cinder client. - - :returns: The created volume object. - - :raises: OpenStackCloudTimeout if wait time exceeded. - :raises: OpenStackCloudException on operation error. - """ - if bootable is not None: - wait = True - - if image: - image_obj = self.get_image(image) - if not image_obj: - raise exc.OpenStackCloudException( - "Image {image} was requested as the basis for a new" - " volume, but was not found on the cloud".format( - image=image)) - kwargs['imageRef'] = image_obj['id'] - kwargs = self._get_volume_kwargs(kwargs) - kwargs['size'] = size - payload = dict(volume=kwargs) - if 'scheduler_hints' in kwargs: - payload['OS-SCH-HNT:scheduler_hints'] = kwargs.pop( - 'scheduler_hints', None) - data = self._volume_client.post( - '/volumes', - json=dict(payload), - error_message='Error in creating volume') - volume = self._get_and_munchify('volume', data) - self.list_volumes.invalidate(self) - - if volume['status'] == 'error': - raise exc.OpenStackCloudException("Error in creating volume") - - if wait: - vol_id = volume['id'] - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for the volume to be available."): - volume = self.get_volume(vol_id) - - if not volume: - continue - - if volume['status'] == 'available': - if bootable is not None: - self.set_volume_bootable(volume, bootable=bootable) - # no need to re-fetch to update the flag, just set it. - volume['bootable'] = bootable - return volume - - if volume['status'] == 'error': - raise exc.OpenStackCloudException("Error creating volume") - - return self._normalize_volume(volume) - - def update_volume(self, name_or_id, **kwargs): - kwargs = self._get_volume_kwargs(kwargs) - - volume = self.get_volume(name_or_id) - if not volume: - raise exc.OpenStackCloudException( - "Volume %s not found." % name_or_id) - - data = self._volume_client.put( - '/volumes/{volume_id}'.format(volume_id=volume.id), - json=dict({'volume': kwargs}), - error_message='Error updating volume') - - self.list_volumes.invalidate(self) - - return self._normalize_volume(self._get_and_munchify('volume', data)) - - def set_volume_bootable(self, name_or_id, bootable=True): - """Set a volume's bootable flag. - - :param name_or_id: Name, unique ID of the volume or a volume dict. - :param bool bootable: Whether the volume should be bootable. - (Defaults to True) - - :raises: OpenStackCloudTimeout if wait time exceeded. - :raises: OpenStackCloudException on operation error. - """ - - volume = self.get_volume(name_or_id) - - if not volume: - raise exc.OpenStackCloudException( - "Volume {name_or_id} does not exist".format( - name_or_id=name_or_id)) - - self._volume_client.post( - 'volumes/{id}/action'.format(id=volume['id']), - json={'os-set_bootable': {'bootable': bootable}}, - error_message="Error setting bootable on volume {volume}".format( - volume=volume['id']) - ) - - def delete_volume(self, name_or_id=None, wait=True, timeout=None, - force=False): - """Delete a volume. - - :param name_or_id: Name or unique ID of the volume. - :param wait: If true, waits for volume to be deleted. - :param timeout: Seconds to wait for volume deletion. None is forever. - :param force: Force delete volume even if the volume is in deleting - or error_deleting state. - - :raises: OpenStackCloudTimeout if wait time exceeded. - :raises: OpenStackCloudException on operation error. - """ - - self.list_volumes.invalidate(self) - volume = self.get_volume(name_or_id) - - if not volume: - self.log.debug( - "Volume %(name_or_id)s does not exist", - {'name_or_id': name_or_id}, - exc_info=True) - return False - - with _utils.shade_exceptions("Error in deleting volume"): - try: - if force: - self._volume_client.post( - 'volumes/{id}/action'.format(id=volume['id']), - json={'os-force_delete': None}) - else: - self._volume_client.delete( - 'volumes/{id}'.format(id=volume['id'])) - except exc.OpenStackCloudURINotFound: - self.log.debug( - "Volume {id} not found when deleting. Ignoring.".format( - id=volume['id'])) - return False - - self.list_volumes.invalidate(self) - if wait: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for the volume to be deleted."): - - if not self.get_volume(volume['id']): - break - - return True - - def get_volumes(self, server, cache=True): - volumes = [] - for volume in self.list_volumes(cache=cache): - for attach in volume['attachments']: - if attach['server_id'] == server['id']: - volumes.append(volume) - return volumes - - def get_volume_limits(self, name_or_id=None): - """ Get volume limits for a project - - :param name_or_id: (optional) project name or ID to get limits for - if different from the current project - :raises: OpenStackCloudException if it's not a valid project - - :returns: Munch object with the limits - """ - params = {} - project_id = None - error_msg = "Failed to get limits" - if name_or_id: - - proj = self.get_project(name_or_id) - if not proj: - raise exc.OpenStackCloudException("project does not exist") - project_id = proj.id - params['tenant_id'] = project_id - error_msg = "{msg} for the project: {project} ".format( - msg=error_msg, project=name_or_id) - - data = self._volume_client.get('/limits', params=params) - limits = self._get_and_munchify('limits', data) - return limits - - def get_volume_id(self, name_or_id): - volume = self.get_volume(name_or_id) - if volume: - return volume['id'] - return None - - def volume_exists(self, name_or_id): - return self.get_volume(name_or_id) is not None - - def get_volume_attach_device(self, volume, server_id): - """Return the device name a volume is attached to for a server. - - This can also be used to verify if a volume is attached to - a particular server. - - :param volume: Volume dict - :param server_id: ID of server to check - - :returns: Device name if attached, None if volume is not attached. - """ - for attach in volume['attachments']: - if server_id == attach['server_id']: - return attach['device'] - return None - - def detach_volume(self, server, volume, wait=True, timeout=None): - """Detach a volume from a server. - - :param server: The server dict to detach from. - :param volume: The volume dict to detach. - :param wait: If true, waits for volume to be detached. - :param timeout: Seconds to wait for volume detachment. None is forever. - - :raises: OpenStackCloudTimeout if wait time exceeded. - :raises: OpenStackCloudException on operation error. - """ - - proxy._json_response(self.compute.delete( - '/servers/{server_id}/os-volume_attachments/{volume_id}'.format( - server_id=server['id'], volume_id=volume['id'])), - error_message=( - "Error detaching volume {volume} from server {server}".format( - volume=volume['id'], server=server['id']))) - - if wait: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for volume %s to detach." % volume['id']): - try: - vol = self.get_volume(volume['id']) - except Exception: - self.log.debug( - "Error getting volume info %s", volume['id'], - exc_info=True) - continue - - if vol['status'] == 'available': - return - - if vol['status'] == 'error': - raise exc.OpenStackCloudException( - "Error in detaching volume %s" % volume['id'] - ) - - def attach_volume(self, server, volume, device=None, - wait=True, timeout=None): - """Attach a volume to a server. - - This will attach a volume, described by the passed in volume - dict (as returned by get_volume()), to the server described by - the passed in server dict (as returned by get_server()) on the - named device on the server. - - If the volume is already attached to the server, or generally not - available, then an exception is raised. To re-attach to a server, - but under a different device, the user must detach it first. - - :param server: The server dict to attach to. - :param volume: The volume dict to attach. - :param device: The device name where the volume will attach. - :param wait: If true, waits for volume to be attached. - :param timeout: Seconds to wait for volume attachment. None is forever. - - :returns: a volume attachment object. - - :raises: OpenStackCloudTimeout if wait time exceeded. - :raises: OpenStackCloudException on operation error. - """ - dev = self.get_volume_attach_device(volume, server['id']) - if dev: - raise exc.OpenStackCloudException( - "Volume %s already attached to server %s on device %s" - % (volume['id'], server['id'], dev) - ) - - if volume['status'] != 'available': - raise exc.OpenStackCloudException( - "Volume %s is not available. Status is '%s'" - % (volume['id'], volume['status']) - ) - - payload = {'volumeId': volume['id']} - if device: - payload['device'] = device - data = proxy._json_response( - self.compute.post( - '/servers/{server_id}/os-volume_attachments'.format( - server_id=server['id']), - json=dict(volumeAttachment=payload)), - error_message="Error attaching volume {volume_id} to server " - "{server_id}".format(volume_id=volume['id'], - server_id=server['id'])) - - if wait: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for volume %s to attach." % volume['id']): - try: - self.list_volumes.invalidate(self) - vol = self.get_volume(volume['id']) - except Exception: - self.log.debug( - "Error getting volume info %s", volume['id'], - exc_info=True) - continue - - if self.get_volume_attach_device(vol, server['id']): - break - - # TODO(Shrews) check to see if a volume can be in error status - # and also attached. If so, we should move this - # above the get_volume_attach_device call - if vol['status'] == 'error': - raise exc.OpenStackCloudException( - "Error in attaching volume %s" % volume['id'] - ) - return self._normalize_volume_attachment( - self._get_and_munchify('volumeAttachment', data)) - - def _get_volume_kwargs(self, kwargs): - name = kwargs.pop('name', kwargs.pop('display_name', None)) - description = kwargs.pop('description', - kwargs.pop('display_description', None)) - if name: - if self._is_client_version('volume', 2): - kwargs['name'] = name - else: - kwargs['display_name'] = name - if description: - if self._is_client_version('volume', 2): - kwargs['description'] = description - else: - kwargs['display_description'] = description - return kwargs - - @_utils.valid_kwargs('name', 'display_name', - 'description', 'display_description') - def create_volume_snapshot(self, volume_id, force=False, - wait=True, timeout=None, **kwargs): - """Create a volume. - - :param volume_id: the ID of the volume to snapshot. - :param force: If set to True the snapshot will be created even if the - volume is attached to an instance, if False it will not - :param name: name of the snapshot, one will be generated if one is - not provided - :param description: description of the snapshot, one will be generated - if one is not provided - :param wait: If true, waits for volume snapshot to be created. - :param timeout: Seconds to wait for volume snapshot creation. None is - forever. - - :returns: The created volume object. - - :raises: OpenStackCloudTimeout if wait time exceeded. - :raises: OpenStackCloudException on operation error. - """ - - kwargs = self._get_volume_kwargs(kwargs) - payload = {'volume_id': volume_id, 'force': force} - payload.update(kwargs) - data = self._volume_client.post( - '/snapshots', - json=dict(snapshot=payload), - error_message="Error creating snapshot of volume " - "{volume_id}".format(volume_id=volume_id)) - snapshot = self._get_and_munchify('snapshot', data) - if wait: - snapshot_id = snapshot['id'] - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for the volume snapshot to be available." - ): - snapshot = self.get_volume_snapshot_by_id(snapshot_id) - - if snapshot['status'] == 'available': - break - - if snapshot['status'] == 'error': - raise exc.OpenStackCloudException( - "Error in creating volume snapshot") - - # TODO(mordred) need to normalize snapshots. We were normalizing them - # as volumes, which is an error. They need to be normalized as - # volume snapshots, which are completely different objects - return snapshot - - def get_volume_snapshot_by_id(self, snapshot_id): - """Takes a snapshot_id and gets a dict of the snapshot - that maches that ID. - - Note: This is more efficient than get_volume_snapshot. - - param: snapshot_id: ID of the volume snapshot. - - """ - data = self._volume_client.get( - '/snapshots/{snapshot_id}'.format(snapshot_id=snapshot_id), - error_message="Error getting snapshot " - "{snapshot_id}".format(snapshot_id=snapshot_id)) - return self._normalize_volume( - self._get_and_munchify('snapshot', data)) - - def get_volume_snapshot(self, name_or_id, filters=None): - """Get a volume by name or ID. - - :param name_or_id: Name or ID of the volume snapshot. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - OR - A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - - :returns: A volume ``munch.Munch`` or None if no matching volume is - found. - """ - return _utils._get_entity(self, 'volume_snapshot', name_or_id, - filters) - - def create_volume_backup(self, volume_id, name=None, description=None, - force=False, wait=True, timeout=None): - """Create a volume backup. - - :param volume_id: the ID of the volume to backup. - :param name: name of the backup, one will be generated if one is - not provided - :param description: description of the backup, one will be generated - if one is not provided - :param force: If set to True the backup will be created even if the - volume is attached to an instance, if False it will not - :param wait: If true, waits for volume backup to be created. - :param timeout: Seconds to wait for volume backup creation. None is - forever. - - :returns: The created volume backup object. - - :raises: OpenStackCloudTimeout if wait time exceeded. - :raises: OpenStackCloudException on operation error. - """ - payload = { - 'name': name, - 'volume_id': volume_id, - 'description': description, - 'force': force, - } - - data = self._volume_client.post( - '/backups', json=dict(backup=payload), - error_message="Error creating backup of volume " - "{volume_id}".format(volume_id=volume_id)) - backup = self._get_and_munchify('backup', data) - - if wait: - backup_id = backup['id'] - msg = ("Timeout waiting for the volume backup {} to be " - "available".format(backup_id)) - for _ in utils.iterate_timeout(timeout, msg): - backup = self.get_volume_backup(backup_id) - - if backup['status'] == 'available': - break - - if backup['status'] == 'error': - raise exc.OpenStackCloudException( - "Error in creating volume backup {id}".format( - id=backup_id)) - - return backup - - def get_volume_backup(self, name_or_id, filters=None): - """Get a volume backup by name or ID. - - :returns: A backup ``munch.Munch`` or None if no matching backup is - found. - """ - return _utils._get_entity(self, 'volume_backup', name_or_id, - filters) - - def list_volume_snapshots(self, detailed=True, search_opts=None): - """List all volume snapshots. - - :returns: A list of volume snapshots ``munch.Munch``. - - """ - endpoint = '/snapshots/detail' if detailed else '/snapshots' - data = self._volume_client.get( - endpoint, - params=search_opts, - error_message="Error getting a list of snapshots") - return self._get_and_munchify('snapshots', data) - - def list_volume_backups(self, detailed=True, search_opts=None): - """ - List all volume backups. - - :param bool detailed: Also list details for each entry - :param dict search_opts: Search options - A dictionary of meta data to use for further filtering. Example:: - - { - 'name': 'my-volume-backup', - 'status': 'available', - 'volume_id': 'e126044c-7b4c-43be-a32a-c9cbbc9ddb56', - 'all_tenants': 1 - } - - :returns: A list of volume backups ``munch.Munch``. - """ - endpoint = '/backups/detail' if detailed else '/backups' - data = self._volume_client.get( - endpoint, params=search_opts, - error_message="Error getting a list of backups") - return self._get_and_munchify('backups', data) - - def delete_volume_backup(self, name_or_id=None, force=False, wait=False, - timeout=None): - """Delete a volume backup. - - :param name_or_id: Name or unique ID of the volume backup. - :param force: Allow delete in state other than error or available. - :param wait: If true, waits for volume backup to be deleted. - :param timeout: Seconds to wait for volume backup deletion. None is - forever. - - :raises: OpenStackCloudTimeout if wait time exceeded. - :raises: OpenStackCloudException on operation error. - """ - - volume_backup = self.get_volume_backup(name_or_id) - - if not volume_backup: - return False - - msg = "Error in deleting volume backup" - if force: - self._volume_client.post( - '/backups/{backup_id}/action'.format( - backup_id=volume_backup['id']), - json={'os-force_delete': None}, - error_message=msg) - else: - self._volume_client.delete( - '/backups/{backup_id}'.format( - backup_id=volume_backup['id']), - error_message=msg) - if wait: - msg = "Timeout waiting for the volume backup to be deleted." - for count in utils.iterate_timeout(timeout, msg): - if not self.get_volume_backup(volume_backup['id']): - break - - return True - - def delete_volume_snapshot(self, name_or_id=None, wait=False, - timeout=None): - """Delete a volume snapshot. - - :param name_or_id: Name or unique ID of the volume snapshot. - :param wait: If true, waits for volume snapshot to be deleted. - :param timeout: Seconds to wait for volume snapshot deletion. None is - forever. - - :raises: OpenStackCloudTimeout if wait time exceeded. - :raises: OpenStackCloudException on operation error. - """ - - volumesnapshot = self.get_volume_snapshot(name_or_id) - - if not volumesnapshot: - return False - - self._volume_client.delete( - '/snapshots/{snapshot_id}'.format( - snapshot_id=volumesnapshot['id']), - error_message="Error in deleting volume snapshot") - - if wait: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for the volume snapshot to be deleted."): - if not self.get_volume_snapshot(volumesnapshot['id']): - break - - return True - - def get_server_id(self, name_or_id): - server = self.get_server(name_or_id, bare=True) - if server: - return server['id'] - return None - - def get_server_private_ip(self, server): - return meta.get_server_private_ip(server, self) - - def get_server_public_ip(self, server): - return meta.get_server_external_ipv4(self, server) - - def get_server_meta(self, server): - # TODO(mordred) remove once ansible has moved to Inventory interface - server_vars = meta.get_hostvars_from_server(self, server) - groups = meta.get_groups_from_server(self, server, server_vars) - return dict(server_vars=server_vars, groups=groups) - - def get_openstack_vars(self, server): - return meta.get_hostvars_from_server(self, server) - - def _expand_server_vars(self, server): - # Used by nodepool - # TODO(mordred) remove after these make it into what we - # actually want the API to be. - return meta.expand_server_vars(self, server) - - def _find_floating_network_by_router(self): - """Find the network providing floating ips by looking at routers.""" - - if self._floating_network_by_router_lock.acquire( - not self._floating_network_by_router_run): - if self._floating_network_by_router_run: - self._floating_network_by_router_lock.release() - return self._floating_network_by_router - try: - for router in self.list_routers(): - if router['admin_state_up']: - network_id = router.get( - 'external_gateway_info', {}).get('network_id') - if network_id: - self._floating_network_by_router = network_id - finally: - self._floating_network_by_router_run = True - self._floating_network_by_router_lock.release() - return self._floating_network_by_router - - def available_floating_ip(self, network=None, server=None): - """Get a floating IP from a network or a pool. - - Return the first available floating IP or allocate a new one. - - :param network: Name or ID of the network. - :param server: Server the IP is for if known - - :returns: a (normalized) structure with a floating IP address - description. - """ - if self._use_neutron_floating(): - try: - f_ips = self._normalize_floating_ips( - self._neutron_available_floating_ips( - network=network, server=server)) - return f_ips[0] - except exc.OpenStackCloudURINotFound as e: - self.log.debug( - "Something went wrong talking to neutron API: " - "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) - # Fall-through, trying with Nova - - f_ips = self._normalize_floating_ips( - self._nova_available_floating_ips(pool=network) - ) - return f_ips[0] - - def _get_floating_network_id(self): - # Get first existing external IPv4 network - networks = self.get_external_ipv4_floating_networks() - if networks: - floating_network_id = networks[0]['id'] - else: - floating_network = self._find_floating_network_by_router() - if floating_network: - floating_network_id = floating_network - else: - raise exc.OpenStackCloudResourceNotFound( - "unable to find an external network") - return floating_network_id - - def _neutron_available_floating_ips( - self, network=None, project_id=None, server=None): - """Get a floating IP from a network. - - Return a list of available floating IPs or allocate a new one and - return it in a list of 1 element. - - :param network: A single network name or ID, or a list of them. - :param server: (server) Server the Floating IP is for - - :returns: a list of floating IP addresses. - - :raises: ``OpenStackCloudResourceNotFound``, if an external network - that meets the specified criteria cannot be found. - """ - if project_id is None: - # Make sure we are only listing floatingIPs allocated the current - # tenant. This is the default behaviour of Nova - project_id = self.current_project_id - - if network: - if isinstance(network, six.string_types): - network = [network] - - # Use given list to get first matching external network - floating_network_id = None - for net in network: - for ext_net in self.get_external_ipv4_floating_networks(): - if net in (ext_net['name'], ext_net['id']): - floating_network_id = ext_net['id'] - break - if floating_network_id: - break - - if floating_network_id is None: - raise exc.OpenStackCloudResourceNotFound( - "unable to find external network {net}".format( - net=network) - ) - else: - floating_network_id = self._get_floating_network_id() - - filters = { - 'port': None, - 'network': floating_network_id, - 'location': {'project': {'id': project_id}}, - } - - floating_ips = self._list_floating_ips() - available_ips = _utils._filter_list( - floating_ips, name_or_id=None, filters=filters) - if available_ips: - return available_ips - - # No available IP found or we didn't try - # allocate a new Floating IP - f_ip = self._neutron_create_floating_ip( - network_id=floating_network_id, server=server) - - return [f_ip] - - def _nova_available_floating_ips(self, pool=None): - """Get available floating IPs from a floating IP pool. - - Return a list of available floating IPs or allocate a new one and - return it in a list of 1 element. - - :param pool: Nova floating IP pool name. - - :returns: a list of floating IP addresses. - - :raises: ``OpenStackCloudResourceNotFound``, if a floating IP pool - is not specified and cannot be found. - """ - - with _utils.shade_exceptions( - "Unable to create floating IP in pool {pool}".format( - pool=pool)): - if pool is None: - pools = self.list_floating_ip_pools() - if not pools: - raise exc.OpenStackCloudResourceNotFound( - "unable to find a floating ip pool") - pool = pools[0]['name'] - - filters = { - 'instance_id': None, - 'pool': pool - } - - floating_ips = self._nova_list_floating_ips() - available_ips = _utils._filter_list( - floating_ips, name_or_id=None, filters=filters) - if available_ips: - return available_ips - - # No available IP found or we did not try. - # Allocate a new Floating IP - f_ip = self._nova_create_floating_ip(pool=pool) - - return [f_ip] - - def create_floating_ip(self, network=None, server=None, - fixed_address=None, nat_destination=None, - port=None, wait=False, timeout=60): - """Allocate a new floating IP from a network or a pool. - - :param network: Name or ID of the network - that the floating IP should come from. - :param server: (optional) Server dict for the server to create - the IP for and to which it should be attached. - :param fixed_address: (optional) Fixed IP to attach the floating - ip to. - :param nat_destination: (optional) Name or ID of the network - that the fixed IP to attach the floating - IP to should be on. - :param port: (optional) The port ID that the floating IP should be - attached to. Specifying a port conflicts - with specifying a server, fixed_address or - nat_destination. - :param wait: (optional) Whether to wait for the IP to be active. - Defaults to False. Only applies if a server is - provided. - :param timeout: (optional) How long to wait for the IP to be active. - Defaults to 60. Only applies if a server is - provided. - - :returns: a floating IP address - - :raises: ``OpenStackCloudException``, on operation error. - """ - if self._use_neutron_floating(): - try: - return self._neutron_create_floating_ip( - network_name_or_id=network, server=server, - fixed_address=fixed_address, - nat_destination=nat_destination, - port=port, - wait=wait, timeout=timeout) - except exc.OpenStackCloudURINotFound as e: - self.log.debug( - "Something went wrong talking to neutron API: " - "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) - # Fall-through, trying with Nova - - if port: - raise exc.OpenStackCloudException( - "This cloud uses nova-network which does not support" - " arbitrary floating-ip/port mappings. Please nudge" - " your cloud provider to upgrade the networking stack" - " to neutron, or alternately provide the server," - " fixed_address and nat_destination arguments as appropriate") - # Else, we are using Nova network - f_ips = self._normalize_floating_ips( - [self._nova_create_floating_ip(pool=network)]) - return f_ips[0] - - def _submit_create_fip(self, kwargs): - # Split into a method to aid in test mocking - data = self.network.post( - "/floatingips.json", json={"floatingip": kwargs}) - return self._normalize_floating_ip( - self._get_and_munchify('floatingip', data)) - - def _neutron_create_floating_ip( - self, network_name_or_id=None, server=None, - fixed_address=None, nat_destination=None, - port=None, - wait=False, timeout=60, network_id=None): - - if not network_id: - if network_name_or_id: - network = self.get_network(network_name_or_id) - if not network: - raise exc.OpenStackCloudResourceNotFound( - "unable to find network for floating ips with ID " - "{0}".format(network_name_or_id)) - network_id = network['id'] - else: - network_id = self._get_floating_network_id() - kwargs = { - 'floating_network_id': network_id, - } - if not port: - if server: - (port_obj, fixed_ip_address) = self._nat_destination_port( - server, fixed_address=fixed_address, - nat_destination=nat_destination) - if port_obj: - port = port_obj['id'] - if fixed_ip_address: - kwargs['fixed_ip_address'] = fixed_ip_address - if port: - kwargs['port_id'] = port - - fip = self._submit_create_fip(kwargs) - fip_id = fip['id'] - - if port: - # The FIP is only going to become active in this context - # when we've attached it to something, which only occurs - # if we've provided a port as a parameter - if wait: - try: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for the floating IP" - " to be ACTIVE", - wait=self._FLOAT_AGE): - fip = self.get_floating_ip(fip_id) - if fip and fip['status'] == 'ACTIVE': - break - except exc.OpenStackCloudTimeout: - self.log.error( - "Timed out on floating ip %(fip)s becoming active." - " Deleting", {'fip': fip_id}) - try: - self.delete_floating_ip(fip_id) - except Exception as e: - self.log.error( - "FIP LEAK: Attempted to delete floating ip " - "%(fip)s but received %(exc)s exception: " - "%(err)s", {'fip': fip_id, 'exc': e.__class__, - 'err': str(e)}) - raise - if fip['port_id'] != port: - if server: - raise exc.OpenStackCloudException( - "Attempted to create FIP on port {port} for server" - " {server} but FIP has port {port_id}".format( - port=port, port_id=fip['port_id'], - server=server['id'])) - else: - raise exc.OpenStackCloudException( - "Attempted to create FIP on port {port}" - " but something went wrong".format(port=port)) - return fip - - def _nova_create_floating_ip(self, pool=None): - with _utils.shade_exceptions( - "Unable to create floating IP in pool {pool}".format( - pool=pool)): - if pool is None: - pools = self.list_floating_ip_pools() - if not pools: - raise exc.OpenStackCloudResourceNotFound( - "unable to find a floating ip pool") - pool = pools[0]['name'] - - data = proxy._json_response(self.compute.post( - '/os-floating-ips', json=dict(pool=pool))) - pool_ip = self._get_and_munchify('floating_ip', data) - # TODO(mordred) Remove this - it's just for compat - data = proxy._json_response( - self.compute.get('/os-floating-ips/{id}'.format( - id=pool_ip['id']))) - return self._get_and_munchify('floating_ip', data) - - def delete_floating_ip(self, floating_ip_id, retry=1): - """Deallocate a floating IP from a project. - - :param floating_ip_id: a floating IP address ID. - :param retry: number of times to retry. Optional, defaults to 1, - which is in addition to the initial delete call. - A value of 0 will also cause no checking of results to - occur. - - :returns: True if the IP address has been deleted, False if the IP - address was not found. - - :raises: ``OpenStackCloudException``, on operation error. - """ - for count in range(0, max(0, retry) + 1): - result = self._delete_floating_ip(floating_ip_id) - - if (retry == 0) or not result: - return result - - # Wait for the cached floating ip list to be regenerated - if self._FLOAT_AGE: - time.sleep(self._FLOAT_AGE) - - # neutron sometimes returns success when deleting a floating - # ip. That's awesome. SO - verify that the delete actually - # worked. Some clouds will set the status to DOWN rather than - # deleting the IP immediately. This is, of course, a bit absurd. - f_ip = self.get_floating_ip(id=floating_ip_id) - if not f_ip or f_ip['status'] == 'DOWN': - return True - - raise exc.OpenStackCloudException( - "Attempted to delete Floating IP {ip} with ID {id} a total of" - " {retry} times. Although the cloud did not indicate any errors" - " the floating ip is still in existence. Aborting further" - " operations.".format( - id=floating_ip_id, ip=f_ip['floating_ip_address'], - retry=retry + 1)) - - def _delete_floating_ip(self, floating_ip_id): - if self._use_neutron_floating(): - try: - return self._neutron_delete_floating_ip(floating_ip_id) - except exc.OpenStackCloudURINotFound as e: - self.log.debug( - "Something went wrong talking to neutron API: " - "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) - return self._nova_delete_floating_ip(floating_ip_id) - - def _neutron_delete_floating_ip(self, floating_ip_id): - try: - proxy._json_response(self.network.delete( - "/floatingips/{fip_id}.json".format(fip_id=floating_ip_id), - error_message="unable to delete floating IP")) - except exc.OpenStackCloudResourceNotFound: - return False - except Exception as e: - raise exc.OpenStackCloudException( - "Unable to delete floating IP ID {fip_id}: {msg}".format( - fip_id=floating_ip_id, msg=str(e))) - return True - - def _nova_delete_floating_ip(self, floating_ip_id): - try: - proxy._json_response( - self.compute.delete( - '/os-floating-ips/{id}'.format(id=floating_ip_id)), - error_message='Unable to delete floating IP {fip_id}'.format( - fip_id=floating_ip_id)) - except exc.OpenStackCloudURINotFound: - return False - return True - - def delete_unattached_floating_ips(self, retry=1): - """Safely delete unattached floating ips. - - If the cloud can safely purge any unattached floating ips without - race conditions, do so. - - Safely here means a specific thing. It means that you are not running - this while another process that might do a two step create/attach - is running. You can safely run this method while another process - is creating servers and attaching floating IPs to them if either that - process is using add_auto_ip from shade, or is creating the floating - IPs by passing in a server to the create_floating_ip call. - - :param retry: number of times to retry. Optional, defaults to 1, - which is in addition to the initial delete call. - A value of 0 will also cause no checking of results to - occur. - - :returns: Number of Floating IPs deleted, False if none - - :raises: ``OpenStackCloudException``, on operation error. - """ - processed = [] - if self._use_neutron_floating(): - for ip in self.list_floating_ips(): - if not ip['attached']: - processed.append(self.delete_floating_ip( - floating_ip_id=ip['id'], retry=retry)) - return len(processed) if all(processed) else False - - def _attach_ip_to_server( - self, server, floating_ip, - fixed_address=None, wait=False, - timeout=60, skip_attach=False, nat_destination=None): - """Attach a floating IP to a server. - - :param server: Server dict - :param floating_ip: Floating IP dict to attach - :param fixed_address: (optional) fixed address to which attach the - floating IP to. - :param wait: (optional) Wait for the address to appear as assigned - to the server. Defaults to False. - :param timeout: (optional) Seconds to wait, defaults to 60. - See the ``wait`` parameter. - :param skip_attach: (optional) Skip the actual attach and just do - the wait. Defaults to False. - :param nat_destination: The fixed network the server's port for the - FIP to attach to will come from. - - :returns: The server ``munch.Munch`` - - :raises: OpenStackCloudException, on operation error. - """ - # Short circuit if we're asking to attach an IP that's already - # attached - ext_ip = meta.get_server_ip(server, ext_tag='floating', public=True) - if ext_ip == floating_ip['floating_ip_address']: - return server - - if self._use_neutron_floating(): - if not skip_attach: - try: - self._neutron_attach_ip_to_server( - server=server, floating_ip=floating_ip, - fixed_address=fixed_address, - nat_destination=nat_destination) - except exc.OpenStackCloudURINotFound as e: - self.log.debug( - "Something went wrong talking to neutron API: " - "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) - # Fall-through, trying with Nova - else: - # Nova network - self._nova_attach_ip_to_server( - server_id=server['id'], floating_ip_id=floating_ip['id'], - fixed_address=fixed_address) - - if wait: - # Wait for the address to be assigned to the server - server_id = server['id'] - for _ in utils.iterate_timeout( - timeout, - "Timeout waiting for the floating IP to be attached.", - wait=self._SERVER_AGE): - server = self.get_server(server_id) - ext_ip = meta.get_server_ip( - server, ext_tag='floating', public=True) - if ext_ip == floating_ip['floating_ip_address']: - return server - return server - - def _nat_destination_port( - self, server, fixed_address=None, nat_destination=None): - """Returns server port that is on a nat_destination network - - Find a port attached to the server which is on a network which - has a subnet which can be the destination of NAT. Such a network - is referred to in shade as a "nat_destination" network. So this - then is a function which returns a port on such a network that is - associated with the given server. - - :param server: Server dict. - :param fixed_address: Fixed ip address of the port - :param nat_destination: Name or ID of the network of the port. - """ - # If we are caching port lists, we may not find the port for - # our server if the list is old. Try for at least 2 cache - # periods if that is the case. - if self._PORT_AGE: - timeout = self._PORT_AGE * 2 - else: - timeout = None - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for port to show up in list", - wait=self._PORT_AGE): - try: - port_filter = {'device_id': server['id']} - ports = self.search_ports(filters=port_filter) - break - except exc.OpenStackCloudTimeout: - ports = None - if not ports: - return (None, None) - port = None - if not fixed_address: - if len(ports) > 1: - if nat_destination: - nat_network = self.get_network(nat_destination) - if not nat_network: - raise exc.OpenStackCloudException( - 'NAT Destination {nat_destination} was configured' - ' but not found on the cloud. Please check your' - ' config and your cloud and try again.'.format( - nat_destination=nat_destination)) - else: - nat_network = self.get_nat_destination() - - if not nat_network: - raise exc.OpenStackCloudException( - 'Multiple ports were found for server {server}' - ' but none of the networks are a valid NAT' - ' destination, so it is impossible to add a' - ' floating IP. If you have a network that is a valid' - ' destination for NAT and we could not find it,' - ' please file a bug. But also configure the' - ' nat_destination property of the networks list in' - ' your clouds.yaml file. If you do not have a' - ' clouds.yaml file, please make one - your setup' - ' is complicated.'.format(server=server['id'])) - - maybe_ports = [] - for maybe_port in ports: - if maybe_port['network_id'] == nat_network['id']: - maybe_ports.append(maybe_port) - if not maybe_ports: - raise exc.OpenStackCloudException( - 'No port on server {server} was found matching' - ' your NAT destination network {dest}. Please ' - ' check your config'.format( - server=server['id'], dest=nat_network['name'])) - ports = maybe_ports - - # Select the most recent available IPv4 address - # To do this, sort the ports in reverse order by the created_at - # field which is a string containing an ISO DateTime (which - # thankfully sort properly) This way the most recent port created, - # if there are more than one, will be the arbitrary port we - # select. - for port in sorted( - ports, - key=lambda p: p.get('created_at', 0), - reverse=True): - for address in port.get('fixed_ips', list()): - try: - ip = ipaddress.ip_address(address['ip_address']) - except Exception: - continue - if ip.version == 4: - fixed_address = address['ip_address'] - return port, fixed_address - raise exc.OpenStackCloudException( - "unable to find a free fixed IPv4 address for server " - "{0}".format(server['id'])) - # unfortunately a port can have more than one fixed IP: - # we can't use the search_ports filtering for fixed_address as - # they are contained in a list. e.g. - # - # "fixed_ips": [ - # { - # "subnet_id": "008ba151-0b8c-4a67-98b5-0d2b87666062", - # "ip_address": "172.24.4.2" - # } - # ] - # - # Search fixed_address - for p in ports: - for fixed_ip in p['fixed_ips']: - if fixed_address == fixed_ip['ip_address']: - return (p, fixed_address) - return (None, None) - - def _neutron_attach_ip_to_server( - self, server, floating_ip, fixed_address=None, - nat_destination=None): - - # Find an available port - (port, fixed_address) = self._nat_destination_port( - server, fixed_address=fixed_address, - nat_destination=nat_destination) - if not port: - raise exc.OpenStackCloudException( - "unable to find a port for server {0}".format( - server['id'])) - - floating_ip_args = {'port_id': port['id']} - if fixed_address is not None: - floating_ip_args['fixed_ip_address'] = fixed_address - - return proxy._json_response( - self.network.put( - "/floatingips/{fip_id}.json".format(fip_id=floating_ip['id']), - json={'floatingip': floating_ip_args}), - error_message=("Error attaching IP {ip} to " - "server {server_id}".format( - ip=floating_ip['id'], - server_id=server['id']))) - - def _nova_attach_ip_to_server(self, server_id, floating_ip_id, - fixed_address=None): - f_ip = self.get_floating_ip( - id=floating_ip_id) - if f_ip is None: - raise exc.OpenStackCloudException( - "unable to find floating IP {0}".format(floating_ip_id)) - error_message = "Error attaching IP {ip} to instance {id}".format( - ip=floating_ip_id, id=server_id) - body = { - 'address': f_ip['floating_ip_address'] - } - if fixed_address: - body['fixed_address'] = fixed_address - return proxy._json_response( - self.compute.post( - '/servers/{server_id}/action'.format(server_id=server_id), - json=dict(addFloatingIp=body)), - error_message=error_message) - - def detach_ip_from_server(self, server_id, floating_ip_id): - """Detach a floating IP from a server. - - :param server_id: ID of a server. - :param floating_ip_id: Id of the floating IP to detach. - - :returns: True if the IP has been detached, or False if the IP wasn't - attached to any server. - - :raises: ``OpenStackCloudException``, on operation error. - """ - if self._use_neutron_floating(): - try: - return self._neutron_detach_ip_from_server( - server_id=server_id, floating_ip_id=floating_ip_id) - except exc.OpenStackCloudURINotFound as e: - self.log.debug( - "Something went wrong talking to neutron API: " - "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) - # Fall-through, trying with Nova - - # Nova network - self._nova_detach_ip_from_server( - server_id=server_id, floating_ip_id=floating_ip_id) - - def _neutron_detach_ip_from_server(self, server_id, floating_ip_id): - f_ip = self.get_floating_ip(id=floating_ip_id) - if f_ip is None or not f_ip['attached']: - return False - exceptions.raise_from_response( - self.network.put( - "/floatingips/{fip_id}.json".format(fip_id=floating_ip_id), - json={"floatingip": {"port_id": None}}), - error_message=("Error detaching IP {ip} from " - "server {server_id}".format( - ip=floating_ip_id, server_id=server_id))) - - return True - - def _nova_detach_ip_from_server(self, server_id, floating_ip_id): - - f_ip = self.get_floating_ip(id=floating_ip_id) - if f_ip is None: - raise exc.OpenStackCloudException( - "unable to find floating IP {0}".format(floating_ip_id)) - error_message = "Error detaching IP {ip} from instance {id}".format( - ip=floating_ip_id, id=server_id) - return proxy._json_response( - self.compute.post( - '/servers/{server_id}/action'.format(server_id=server_id), - json=dict(removeFloatingIp=dict( - address=f_ip['floating_ip_address']))), - error_message=error_message) - - return True - - def _add_ip_from_pool( - self, server, network, fixed_address=None, reuse=True, - wait=False, timeout=60, nat_destination=None): - """Add a floating IP to a server from a given pool - - This method reuses available IPs, when possible, or allocate new IPs - to the current tenant. - The floating IP is attached to the given fixed address or to the - first server port/fixed address - - :param server: Server dict - :param network: Name or ID of the network. - :param fixed_address: a fixed address - :param reuse: Try to reuse existing ips. Defaults to True. - :param wait: (optional) Wait for the address to appear as assigned - to the server. Defaults to False. - :param timeout: (optional) Seconds to wait, defaults to 60. - See the ``wait`` parameter. - :param nat_destination: (optional) the name of the network of the - port to associate with the floating ip. - - :returns: the updated server ``munch.Munch`` - """ - if reuse: - f_ip = self.available_floating_ip(network=network) - else: - start_time = time.time() - f_ip = self.create_floating_ip( - server=server, - network=network, nat_destination=nat_destination, - wait=wait, timeout=timeout) - timeout = timeout - (time.time() - start_time) - # Wait for cache invalidation time so that we don't try - # to attach the FIP a second time below - time.sleep(self._SERVER_AGE) - server = self.get_server(server.id) - - # We run attach as a second call rather than in the create call - # because there are code flows where we will not have an attached - # FIP yet. However, even if it was attached in the create, we run - # the attach function below to get back the server dict refreshed - # with the FIP information. - return self._attach_ip_to_server( - server=server, floating_ip=f_ip, fixed_address=fixed_address, - wait=wait, timeout=timeout, nat_destination=nat_destination) - - def add_ip_list( - self, server, ips, wait=False, timeout=60, - fixed_address=None): - """Attach a list of IPs to a server. - - :param server: a server object - :param ips: list of floating IP addresses or a single address - :param wait: (optional) Wait for the address to appear as assigned - to the server. Defaults to False. - :param timeout: (optional) Seconds to wait, defaults to 60. - See the ``wait`` parameter. - :param fixed_address: (optional) Fixed address of the server to - attach the IP to - - :returns: The updated server ``munch.Munch`` - - :raises: ``OpenStackCloudException``, on operation error. - """ - if type(ips) == list: - ip = ips[0] - else: - ip = ips - f_ip = self.get_floating_ip( - id=None, filters={'floating_ip_address': ip}) - return self._attach_ip_to_server( - server=server, floating_ip=f_ip, wait=wait, timeout=timeout, - fixed_address=fixed_address) - - def add_auto_ip(self, server, wait=False, timeout=60, reuse=True): - """Add a floating IP to a server. - - This method is intended for basic usage. For advanced network - architecture (e.g. multiple external networks or servers with multiple - interfaces), use other floating IP methods. - - This method can reuse available IPs, or allocate new IPs to the current - project. - - :param server: a server dictionary. - :param reuse: Whether or not to attempt to reuse IPs, defaults - to True. - :param wait: (optional) Wait for the address to appear as assigned - to the server. Defaults to False. - :param timeout: (optional) Seconds to wait, defaults to 60. - See the ``wait`` parameter. - :param reuse: Try to reuse existing ips. Defaults to True. - - :returns: Floating IP address attached to server. - - """ - server = self._add_auto_ip( - server, wait=wait, timeout=timeout, reuse=reuse) - return server['interface_ip'] or None - - def _add_auto_ip(self, server, wait=False, timeout=60, reuse=True): - skip_attach = False - created = False - if reuse: - f_ip = self.available_floating_ip(server=server) - else: - start_time = time.time() - f_ip = self.create_floating_ip( - server=server, wait=wait, timeout=timeout) - timeout = timeout - (time.time() - start_time) - if server: - # This gets passed in for both nova and neutron - # but is only meaningful for the neutron logic branch - skip_attach = True - created = True - - try: - # We run attach as a second call rather than in the create call - # because there are code flows where we will not have an attached - # FIP yet. However, even if it was attached in the create, we run - # the attach function below to get back the server dict refreshed - # with the FIP information. - return self._attach_ip_to_server( - server=server, floating_ip=f_ip, wait=wait, timeout=timeout, - skip_attach=skip_attach) - except exc.OpenStackCloudTimeout: - if self._use_neutron_floating() and created: - # We are here because we created an IP on the port - # It failed. Delete so as not to leak an unmanaged - # resource - self.log.error( - "Timeout waiting for floating IP to become" - " active. Floating IP %(ip)s:%(id)s was created for" - " server %(server)s but is being deleted due to" - " activation failure.", { - 'ip': f_ip['floating_ip_address'], - 'id': f_ip['id'], - 'server': server['id']}) - try: - self.delete_floating_ip(f_ip['id']) - except Exception as e: - self.log.error( - "FIP LEAK: Attempted to delete floating ip " - "%(fip)s but received %(exc)s exception: %(err)s", - {'fip': f_ip['id'], 'exc': e.__class__, 'err': str(e)}) - raise e - raise - - def add_ips_to_server( - self, server, auto_ip=True, ips=None, ip_pool=None, - wait=False, timeout=60, reuse=True, fixed_address=None, - nat_destination=None): - if ip_pool: - server = self._add_ip_from_pool( - server, ip_pool, reuse=reuse, wait=wait, timeout=timeout, - fixed_address=fixed_address, nat_destination=nat_destination) - elif ips: - server = self.add_ip_list( - server, ips, wait=wait, timeout=timeout, - fixed_address=fixed_address) - elif auto_ip: - if self._needs_floating_ip(server, nat_destination): - server = self._add_auto_ip( - server, wait=wait, timeout=timeout, reuse=reuse) - return server - - def _needs_floating_ip(self, server, nat_destination): - """Figure out if auto_ip should add a floating ip to this server. - - If the server has a public_v4 it does not need a floating ip. - - If the server does not have a private_v4 it does not need a - floating ip. - - If self.private then the server does not need a floating ip. - - If the cloud runs nova, and the server has a private_v4 and not - a public_v4, then the server needs a floating ip. - - If the server has a private_v4 and no public_v4 and the cloud has - a network from which floating IPs come that is connected via a - router to the network from which the private_v4 address came, - then the server needs a floating ip. - - If the server has a private_v4 and no public_v4 and the cloud - does not have a network from which floating ips come, or it has - one but that network is not connected to the network from which - the server's private_v4 address came via a router, then the - server does not need a floating ip. - """ - if not self._has_floating_ips(): - return False - - if server['public_v4']: - return False - - if not server['private_v4']: - return False - - if self.private: - return False - - if not self.has_service('network'): - return True - - # No floating ip network - no FIPs - try: - self._get_floating_network_id() - except exc.OpenStackCloudException: - return False - - (port_obj, fixed_ip_address) = self._nat_destination_port( - server, nat_destination=nat_destination) - - if not port_obj or not fixed_ip_address: - return False - - return True - - def _get_boot_from_volume_kwargs( - self, image, boot_from_volume, boot_volume, volume_size, - terminate_volume, volumes, kwargs): - """Return block device mappings - - :param image: Image dict, name or id to boot with. - - """ - # TODO(mordred) We're only testing this in functional tests. We need - # to add unit tests for this too. - if boot_volume or boot_from_volume or volumes: - kwargs.setdefault('block_device_mapping_v2', []) - else: - return kwargs - - # If we have boot_from_volume but no root volume, then we're - # booting an image from volume - if boot_volume: - volume = self.get_volume(boot_volume) - if not volume: - raise exc.OpenStackCloudException( - 'Volume {boot_volume} is not a valid volume' - ' in {cloud}:{region}'.format( - boot_volume=boot_volume, - cloud=self.name, region=self.config.region_name)) - block_mapping = { - 'boot_index': '0', - 'delete_on_termination': terminate_volume, - 'destination_type': 'volume', - 'uuid': volume['id'], - 'source_type': 'volume', - } - kwargs['block_device_mapping_v2'].append(block_mapping) - kwargs['imageRef'] = '' - elif boot_from_volume: - - if isinstance(image, dict): - image_obj = image - else: - image_obj = self.get_image(image) - if not image_obj: - raise exc.OpenStackCloudException( - 'Image {image} is not a valid image in' - ' {cloud}:{region}'.format( - image=image, - cloud=self.name, region=self.config.region_name)) - - block_mapping = { - 'boot_index': '0', - 'delete_on_termination': terminate_volume, - 'destination_type': 'volume', - 'uuid': image_obj['id'], - 'source_type': 'image', - 'volume_size': volume_size, - } - kwargs['imageRef'] = '' - kwargs['block_device_mapping_v2'].append(block_mapping) - if volumes and kwargs['imageRef']: - # If we're attaching volumes on boot but booting from an image, - # we need to specify that in the BDM. - block_mapping = { - u'boot_index': 0, - u'delete_on_termination': True, - u'destination_type': u'local', - u'source_type': u'image', - u'uuid': kwargs['imageRef'], - } - kwargs['block_device_mapping_v2'].append(block_mapping) - for volume in volumes: - volume_obj = self.get_volume(volume) - if not volume_obj: - raise exc.OpenStackCloudException( - 'Volume {volume} is not a valid volume' - ' in {cloud}:{region}'.format( - volume=volume, - cloud=self.name, region=self.config.region_name)) - block_mapping = { - 'boot_index': '-1', - 'delete_on_termination': False, - 'destination_type': 'volume', - 'uuid': volume_obj['id'], - 'source_type': 'volume', - } - kwargs['block_device_mapping_v2'].append(block_mapping) - if boot_volume or boot_from_volume or volumes: - self.list_volumes.invalidate(self) - return kwargs - - def _encode_server_userdata(self, userdata): - if hasattr(userdata, 'read'): - userdata = userdata.read() - - if not isinstance(userdata, six.binary_type): - # If the userdata passed in is bytes, just send it unmodified - if not isinstance(userdata, six.string_types): - raise TypeError("%s can't be encoded" % type(userdata)) - # If it's not bytes, make it bytes - userdata = userdata.encode('utf-8', 'strict') - - # Once we have base64 bytes, make them into a utf-8 string for REST - return base64.b64encode(userdata).decode('utf-8') - - @_utils.valid_kwargs( - 'meta', 'files', 'userdata', - 'reservation_id', 'return_raw', 'min_count', - 'max_count', 'security_groups', 'key_name', - 'availability_zone', 'block_device_mapping', - 'block_device_mapping_v2', 'nics', 'scheduler_hints', - 'config_drive', 'admin_pass', 'disk_config') - def create_server( - self, name, image=None, flavor=None, - auto_ip=True, ips=None, ip_pool=None, - root_volume=None, terminate_volume=False, - wait=False, timeout=180, reuse_ips=True, - network=None, boot_from_volume=False, volume_size='50', - boot_volume=None, volumes=None, nat_destination=None, - group=None, - **kwargs): - """Create a virtual server instance. - - :param name: Something to name the server. - :param image: Image dict, name or ID to boot with. image is required - unless boot_volume is given. - :param flavor: Flavor dict, name or ID to boot onto. - :param auto_ip: Whether to take actions to find a routable IP for - the server. (defaults to True) - :param ips: List of IPs to attach to the server (defaults to None) - :param ip_pool: Name of the network or floating IP pool to get an - address from. (defaults to None) - :param root_volume: Name or ID of a volume to boot from - (defaults to None - deprecated, use boot_volume) - :param boot_volume: Name or ID of a volume to boot from - (defaults to None) - :param terminate_volume: If booting from a volume, whether it should - be deleted when the server is destroyed. - (defaults to False) - :param volumes: (optional) A list of volumes to attach to the server - :param meta: (optional) A dict of arbitrary key/value metadata to - store for this server. Both keys and values must be - <=255 characters. - :param files: (optional, deprecated) A dict of files to overwrite - on the server upon boot. Keys are file names (i.e. - ``/etc/passwd``) and values - are the file contents (either as a string or as a - file-like object). A maximum of five entries is allowed, - and each file must be 10k or less. - :param reservation_id: a UUID for the set of servers being requested. - :param min_count: (optional extension) The minimum number of - servers to launch. - :param max_count: (optional extension) The maximum number of - servers to launch. - :param security_groups: A list of security group names - :param userdata: user data to pass to be exposed by the metadata - server this can be a file type object as well or a - string. - :param key_name: (optional extension) name of previously created - keypair to inject into the instance. - :param availability_zone: Name of the availability zone for instance - placement. - :param block_device_mapping: (optional) A dict of block - device mappings for this server. - :param block_device_mapping_v2: (optional) A dict of block - device mappings for this server. - :param nics: (optional extension) an ordered list of nics to be - added to this server, with information about - connected networks, fixed IPs, port etc. - :param scheduler_hints: (optional extension) arbitrary key-value pairs - specified by the client to help boot an instance - :param config_drive: (optional extension) value for config drive - either boolean, or volume-id - :param disk_config: (optional extension) control how the disk is - partitioned when the server is created. possible - values are 'AUTO' or 'MANUAL'. - :param admin_pass: (optional extension) add a user supplied admin - password. - :param wait: (optional) Wait for the address to appear as assigned - to the server. Defaults to False. - :param timeout: (optional) Seconds to wait, defaults to 60. - See the ``wait`` parameter. - :param reuse_ips: (optional) Whether to attempt to reuse pre-existing - floating ips should a floating IP be - needed (defaults to True) - :param network: (optional) Network dict or name or ID to attach the - server to. Mutually exclusive with the nics parameter. - Can also be be a list of network names or IDs or - network dicts. - :param boot_from_volume: Whether to boot from volume. 'boot_volume' - implies True, but boot_from_volume=True with - no boot_volume is valid and will create a - volume from the image and use that. - :param volume_size: When booting an image from volume, how big should - the created volume be? Defaults to 50. - :param nat_destination: Which network should a created floating IP - be attached to, if it's not possible to - infer from the cloud's configuration. - (Optional, defaults to None) - :param group: ServerGroup dict, name or id to boot the server in. - If a group is provided in both scheduler_hints and in - the group param, the group param will win. - (Optional, defaults to None) - :returns: A ``munch.Munch`` representing the created server. - :raises: OpenStackCloudException on operation error. - """ - # TODO(shade) Image is optional but flavor is not - yet flavor comes - # after image in the argument list. Doh. - if not flavor: - raise TypeError( - "create_server() missing 1 required argument: 'flavor'") - if not image and not boot_volume: - raise TypeError( - "create_server() requires either 'image' or 'boot_volume'") - - server_json = {'server': kwargs} - - # TODO(mordred) Add support for description starting in 2.19 - security_groups = kwargs.get('security_groups', []) - if security_groups and not isinstance(kwargs['security_groups'], list): - security_groups = [security_groups] - if security_groups: - kwargs['security_groups'] = [] - for sec_group in security_groups: - kwargs['security_groups'].append(dict(name=sec_group)) - if 'userdata' in kwargs: - user_data = kwargs.pop('userdata') - if user_data: - kwargs['user_data'] = self._encode_server_userdata(user_data) - for (desired, given) in ( - ('OS-DCF:diskConfig', 'disk_config'), - ('config_drive', 'config_drive'), - ('key_name', 'key_name'), - ('metadata', 'meta'), - ('adminPass', 'admin_pass')): - value = kwargs.pop(given, None) - if value: - kwargs[desired] = value - - hints = kwargs.pop('scheduler_hints', {}) - if group: - group_obj = self.get_server_group(group) - if not group_obj: - raise exc.OpenStackCloudException( - "Server Group {group} was requested but was not found" - " on the cloud".format(group=group)) - hints['group'] = group_obj['id'] - if hints: - server_json['os:scheduler_hints'] = hints - kwargs.setdefault('max_count', kwargs.get('max_count', 1)) - kwargs.setdefault('min_count', kwargs.get('min_count', 1)) - - if 'nics' in kwargs and not isinstance(kwargs['nics'], list): - if isinstance(kwargs['nics'], dict): - # Be nice and help the user out - kwargs['nics'] = [kwargs['nics']] - else: - raise exc.OpenStackCloudException( - 'nics parameter to create_server takes a list of dicts.' - ' Got: {nics}'.format(nics=kwargs['nics'])) - - if network and ('nics' not in kwargs or not kwargs['nics']): - nics = [] - if not isinstance(network, list): - network = [network] - for net_name in network: - if isinstance(net_name, dict) and 'id' in net_name: - network_obj = net_name - else: - network_obj = self.get_network(name_or_id=net_name) - if not network_obj: - raise exc.OpenStackCloudException( - 'Network {network} is not a valid network in' - ' {cloud}:{region}'.format( - network=network, - cloud=self.name, region=self.config.region_name)) - nics.append({'net-id': network_obj['id']}) - - kwargs['nics'] = nics - if not network and ('nics' not in kwargs or not kwargs['nics']): - default_network = self.get_default_network() - if default_network: - kwargs['nics'] = [{'net-id': default_network['id']}] - - networks = [] - for nic in kwargs.pop('nics', []): - net = {} - if 'net-id' in nic: - # TODO(mordred) Make sure this is in uuid format - net['uuid'] = nic.pop('net-id') - # If there's a net-id, ignore net-name - nic.pop('net-name', None) - elif 'net-name' in nic: - net_name = nic.pop('net-name') - nic_net = self.get_network(net_name) - if not nic_net: - raise exc.OpenStackCloudException( - "Requested network {net} could not be found.".format( - net=net_name)) - net['uuid'] = nic_net['id'] - for ip_key in ('v4-fixed-ip', 'v6-fixed-ip', 'fixed_ip'): - fixed_ip = nic.pop(ip_key, None) - if fixed_ip and net.get('fixed_ip'): - raise exc.OpenStackCloudException( - "Only one of v4-fixed-ip, v6-fixed-ip or fixed_ip" - " may be given") - if fixed_ip: - net['fixed_ip'] = fixed_ip - # TODO(mordred) Add support for tag if server supports microversion - # 2.32-2.36 or >= 2.42 - for key in ('port', 'port-id'): - if key in nic: - net['port'] = nic.pop(key) - if nic: - raise exc.OpenStackCloudException( - "Additional unsupported keys given for server network" - " creation: {keys}".format(keys=nic.keys())) - networks.append(net) - if networks: - kwargs['networks'] = networks - - if image: - if isinstance(image, dict): - kwargs['imageRef'] = image['id'] - else: - kwargs['imageRef'] = self.get_image(image).id - if isinstance(flavor, dict): - kwargs['flavorRef'] = flavor['id'] - else: - kwargs['flavorRef'] = self.get_flavor(flavor, get_extra=False).id - - if volumes is None: - volumes = [] - - # nova cli calls this boot_volume. Let's be the same - if root_volume and not boot_volume: - boot_volume = root_volume - - kwargs = self._get_boot_from_volume_kwargs( - image=image, boot_from_volume=boot_from_volume, - boot_volume=boot_volume, volume_size=str(volume_size), - terminate_volume=terminate_volume, - volumes=volumes, kwargs=kwargs) - - kwargs['name'] = name - endpoint = '/servers' - # TODO(mordred) We're only testing this in functional tests. We need - # to add unit tests for this too. - if 'block_device_mapping_v2' in kwargs: - endpoint = '/os-volumes_boot' - with _utils.shade_exceptions("Error in creating instance"): - data = proxy._json_response( - self.compute.post(endpoint, json=server_json)) - server = self._get_and_munchify('server', data) - admin_pass = server.get('adminPass') or kwargs.get('admin_pass') - if not wait: - # This is a direct get call to skip the list_servers - # cache which has absolutely no chance of containing the - # new server. - # Only do this if we're not going to wait for the server - # to complete booting, because the only reason we do it - # is to get a server record that is the return value from - # get/list rather than the return value of create. If we're - # going to do the wait loop below, this is a waste of a call - server = self.get_server_by_id(server.id) - if server.status == 'ERROR': - raise exc.OpenStackCloudCreateException( - resource='server', resource_id=server.id) - - if wait: - server = self.wait_for_server( - server, - auto_ip=auto_ip, ips=ips, ip_pool=ip_pool, - reuse=reuse_ips, timeout=timeout, - nat_destination=nat_destination, - ) - - server.adminPass = admin_pass - return server - - def wait_for_server( - self, server, auto_ip=True, ips=None, ip_pool=None, - reuse=True, timeout=180, nat_destination=None): - """ - Wait for a server to reach ACTIVE status. - """ - server_id = server['id'] - timeout_message = "Timeout waiting for the server to come up." - start_time = time.time() - - # There is no point in iterating faster than the list_servers cache - for count in utils.iterate_timeout( - timeout, - timeout_message, - # if _SERVER_AGE is 0 we still want to wait a bit - # to be friendly with the server. - wait=self._SERVER_AGE or 2): - try: - # Use the get_server call so that the list_servers - # cache can be leveraged - server = self.get_server(server_id) - except Exception: - continue - if not server: - continue - - # We have more work to do, but the details of that are - # hidden from the user. So, calculate remaining timeout - # and pass it down into the IP stack. - remaining_timeout = timeout - int(time.time() - start_time) - if remaining_timeout <= 0: - raise exc.OpenStackCloudTimeout(timeout_message) - - server = self.get_active_server( - server=server, reuse=reuse, - auto_ip=auto_ip, ips=ips, ip_pool=ip_pool, - wait=True, timeout=remaining_timeout, - nat_destination=nat_destination) - - if server is not None and server['status'] == 'ACTIVE': - return server - - def get_active_server( - self, server, auto_ip=True, ips=None, ip_pool=None, - reuse=True, wait=False, timeout=180, nat_destination=None): - - if server['status'] == 'ERROR': - if 'fault' in server and 'message' in server['fault']: - raise exc.OpenStackCloudException( - "Error in creating the server." - " Compute service reports fault: {reason}".format( - reason=server['fault']['message']), - extra_data=dict(server=server)) - - raise exc.OpenStackCloudException( - "Error in creating the server", extra_data=dict(server=server)) - - if server['status'] == 'ACTIVE': - if 'addresses' in server and server['addresses']: - return self.add_ips_to_server( - server, auto_ip, ips, ip_pool, reuse=reuse, - nat_destination=nat_destination, - wait=wait, timeout=timeout) - - self.log.debug( - 'Server %(server)s reached ACTIVE state without' - ' being allocated an IP address.' - ' Deleting server.', {'server': server['id']}) - try: - self._delete_server( - server=server, wait=wait, timeout=timeout) - except Exception as e: - raise exc.OpenStackCloudException( - 'Server reached ACTIVE state without being' - ' allocated an IP address AND then could not' - ' be deleted: {0}'.format(e), - extra_data=dict(server=server)) - raise exc.OpenStackCloudException( - 'Server reached ACTIVE state without being' - ' allocated an IP address.', - extra_data=dict(server=server)) - return None - - def rebuild_server(self, server_id, image_id, admin_pass=None, - detailed=False, bare=False, - wait=False, timeout=180): - kwargs = {} - if image_id: - kwargs['imageRef'] = image_id - if admin_pass: - kwargs['adminPass'] = admin_pass - - data = proxy._json_response( - self.compute.post( - '/servers/{server_id}/action'.format(server_id=server_id), - json={'rebuild': kwargs}), - error_message="Error in rebuilding instance") - server = self._get_and_munchify('server', data) - if not wait: - return self._expand_server( - self._normalize_server(server), bare=bare, detailed=detailed) - - admin_pass = server.get('adminPass') or admin_pass - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for server {0} to " - "rebuild.".format(server_id), - wait=self._SERVER_AGE): - try: - server = self.get_server(server_id, bare=True) - except Exception: - continue - if not server: - continue - - if server['status'] == 'ERROR': - raise exc.OpenStackCloudException( - "Error in rebuilding the server", - extra_data=dict(server=server)) - - if server['status'] == 'ACTIVE': - server.adminPass = admin_pass - break - - return self._expand_server(server, detailed=detailed, bare=bare) - - def set_server_metadata(self, name_or_id, metadata): - """Set metadata in a server instance. - - :param str name_or_id: The name or ID of the server instance - to update. - :param dict metadata: A dictionary with the key=value pairs - to set in the server instance. It only updates the key=value - pairs provided. Existing ones will remain untouched. - - :raises: OpenStackCloudException on operation error. - """ - server = self.get_server(name_or_id, bare=True) - if not server: - raise exc.OpenStackCloudException( - 'Invalid Server {server}'.format(server=name_or_id)) - - proxy._json_response( - self.compute.post( - '/servers/{server_id}/metadata'.format(server_id=server['id']), - json={'metadata': metadata}), - error_message='Error updating server metadata') - - def delete_server_metadata(self, name_or_id, metadata_keys): - """Delete metadata from a server instance. - - :param str name_or_id: The name or ID of the server instance - to update. - :param metadata_keys: A list with the keys to be deleted - from the server instance. - - :raises: OpenStackCloudException on operation error. - """ - server = self.get_server(name_or_id, bare=True) - if not server: - raise exc.OpenStackCloudException( - 'Invalid Server {server}'.format(server=name_or_id)) - - for key in metadata_keys: - error_message = 'Error deleting metadata {key} on {server}'.format( - key=key, server=name_or_id) - proxy._json_response( - self.compute.delete( - '/servers/{server_id}/metadata/{key}'.format( - server_id=server['id'], - key=key)), - error_message=error_message) - - def delete_server( - self, name_or_id, wait=False, timeout=180, delete_ips=False, - delete_ip_retry=1): - """Delete a server instance. - - :param name_or_id: name or ID of the server to delete - :param bool wait: If true, waits for server to be deleted. - :param int timeout: Seconds to wait for server deletion. - :param bool delete_ips: If true, deletes any floating IPs - associated with the instance. - :param int delete_ip_retry: Number of times to retry deleting - any floating ips, should the first try be unsuccessful. - - :returns: True if delete succeeded, False otherwise if the - server does not exist. - - :raises: OpenStackCloudException on operation error. - """ - # If delete_ips is True, we need the server to not be bare. - server = self.get_server(name_or_id, bare=True) - if not server: - return False - - # This portion of the code is intentionally left as a separate - # private method in order to avoid an unnecessary API call to get - # a server we already have. - return self._delete_server( - server, wait=wait, timeout=timeout, delete_ips=delete_ips, - delete_ip_retry=delete_ip_retry) - - def _delete_server_floating_ips(self, server, delete_ip_retry): - # Does the server have floating ips in its - # addresses dict? If not, skip this. - server_floats = meta.find_nova_interfaces( - server['addresses'], ext_tag='floating') - for fip in server_floats: - try: - ip = self.get_floating_ip(id=None, filters={ - 'floating_ip_address': fip['addr']}) - except exc.OpenStackCloudURINotFound: - # We're deleting. If it doesn't exist - awesome - # NOTE(mordred) If the cloud is a nova FIP cloud but - # floating_ip_source is set to neutron, this - # can lead to a FIP leak. - continue - if not ip: - continue - deleted = self.delete_floating_ip( - ip['id'], retry=delete_ip_retry) - if not deleted: - raise exc.OpenStackCloudException( - "Tried to delete floating ip {floating_ip}" - " associated with server {id} but there was" - " an error deleting it. Not deleting server.".format( - floating_ip=ip['floating_ip_address'], - id=server['id'])) - - def _delete_server( - self, server, wait=False, timeout=180, delete_ips=False, - delete_ip_retry=1): - if not server: - return False - - if delete_ips and self._has_floating_ips(): - self._delete_server_floating_ips(server, delete_ip_retry) - - try: - proxy._json_response( - self.compute.delete( - '/servers/{id}'.format(id=server['id'])), - error_message="Error in deleting server") - except exc.OpenStackCloudURINotFound: - return False - except Exception: - raise - - if not wait: - return True - - # If the server has volume attachments, or if it has booted - # from volume, deleting it will change volume state so we will - # need to invalidate the cache. Avoid the extra API call if - # caching is not enabled. - reset_volume_cache = False - if (self.cache_enabled - and self.has_service('volume') - and self.get_volumes(server)): - reset_volume_cache = True - - for count in utils.iterate_timeout( - timeout, - "Timed out waiting for server to get deleted.", - # if _SERVER_AGE is 0 we still want to wait a bit - # to be friendly with the server. - wait=self._SERVER_AGE or 2): - with _utils.shade_exceptions("Error in deleting server"): - server = self.get_server(server['id'], bare=True) - if not server: - break - - if reset_volume_cache: - self.list_volumes.invalidate(self) - - # Reset the list servers cache time so that the next list server - # call gets a new list - self._servers_time = self._servers_time - self._SERVER_AGE - return True - - @_utils.valid_kwargs( - 'name', 'description') - def update_server(self, name_or_id, detailed=False, bare=False, **kwargs): - """Update a server. - - :param name_or_id: Name of the server to be updated. - :param detailed: Whether or not to add detailed additional information. - Defaults to False. - :param bare: Whether to skip adding any additional information to the - server record. Defaults to False, meaning the addresses - dict will be populated as needed from neutron. Setting - to True implies detailed = False. - :name: New name for the server - :description: New description for the server - - :returns: a dictionary representing the updated server. - - :raises: OpenStackCloudException on operation error. - """ - server = self.get_server(name_or_id=name_or_id, bare=True) - if server is None: - raise exc.OpenStackCloudException( - "failed to find server '{server}'".format(server=name_or_id)) - - data = proxy._json_response( - self.compute.put( - '/servers/{server_id}'.format(server_id=server['id']), - json={'server': kwargs}), - error_message="Error updating server {0}".format(name_or_id)) - server = self._normalize_server( - self._get_and_munchify('server', data)) - return self._expand_server(server, bare=bare, detailed=detailed) - - def create_server_group(self, name, policies): - """Create a new server group. - - :param name: Name of the server group being created - :param policies: List of policies for the server group. - - :returns: a dict representing the new server group. - - :raises: OpenStackCloudException on operation error. - """ - data = proxy._json_response( - self.compute.post( - '/os-server-groups', - json={ - 'server_group': { - 'name': name, - 'policies': policies}}), - error_message="Unable to create server group {name}".format( - name=name)) - return self._get_and_munchify('server_group', data) - - def delete_server_group(self, name_or_id): - """Delete a server group. - - :param name_or_id: Name or ID of the server group to delete - - :returns: True if delete succeeded, False otherwise - - :raises: OpenStackCloudException on operation error. - """ - server_group = self.get_server_group(name_or_id) - if not server_group: - self.log.debug("Server group %s not found for deleting", - name_or_id) - return False - - proxy._json_response( - self.compute.delete( - '/os-server-groups/{id}'.format(id=server_group['id'])), - error_message="Error deleting server group {name}".format( - name=name_or_id)) - - return True - - def list_containers(self, full_listing=True, prefix=None): - """List containers. - - :param full_listing: Ignored. Present for backwards compat - - :returns: list of Munch of the container objects - - :raises: OpenStackCloudException on operation error. - """ - params = dict(format='json', prefix=prefix) - data = self.object_store.containers(**params) - return self._normalize_containers(self._get_and_munchify(None, data)) - - def search_containers(self, name=None, filters=None): - """Search containers. - - :param string name: container name. - :param filters: a dict containing additional filters to use. - OR - A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - - :returns: a list of ``munch.Munch`` containing the containers. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. - """ - containers = self.list_containers() - return _utils._filter_list(containers, name, filters) - - def get_container(self, name, skip_cache=False): - """Get metadata about a container. - - :param str name: - Name of the container to get metadata for. - :param bool skip_cache: - Ignore the cache of container metadata for this container.o - Defaults to ``False``. - """ - if skip_cache or name not in self._container_cache: - try: - response = self.object_store.get_container_metadata(name) - self._container_cache[name] = response - except exc.OpenStackCloudHTTPError as e: - if e.response.status_code == 404: - return None - raise - return self._container_cache[name] - - def create_container(self, name, public=False): - """Create an object-store container. - - :param str name: - Name of the container to create. - :param bool public: - Whether to set this container to be public. Defaults to ``False``. - """ - container = self.get_container(name) - if container: - return container - self.object_store.create_container(name=name) - if public: - self.set_container_access(name, 'public') - return self.get_container(name, skip_cache=True) - - def delete_container(self, name): - """Delete an object-store container. - - :param str name: Name of the container to delete. - """ - try: - self.object_store.delete_container(name, ignore_missing=False) - self._container_cache.pop(name, None) - return True - except exc.OpenStackCloudHTTPError as e: - if e.response.status_code == 404: - return False - if e.response.status_code == 409: - raise exc.OpenStackCloudException( - 'Attempt to delete container {container} failed. The' - ' container is not empty. Please delete the objects' - ' inside it before deleting the container'.format( - container=name)) - raise - - def update_container(self, name, headers): - """Update the metadata in a container. - - :param str name: - Name of the container to create. - :param dict headers: - Key/Value headers to set on the container. - """ - """Update the metadata in a container. - - :param str name: - Name of the container to update. - :param dict headers: - Key/Value headers to set on the container. - """ - self.object_store.set_container_metadata( - name, - refresh=False, - **headers - ) - - def set_container_access(self, name, access): - """Set the access control list on a container. - - :param str name: - Name of the container. - :param str access: - ACL string to set on the container. Can also be ``public`` - or ``private`` which will be translated into appropriate ACL - strings. - """ - if access not in OBJECT_CONTAINER_ACLS: - raise exc.OpenStackCloudException( - "Invalid container access specified: %s. Must be one of %s" - % (access, list(OBJECT_CONTAINER_ACLS.keys()))) - self.object_store.set_container_metadata( - name, - refresh=False, - read_ACL=OBJECT_CONTAINER_ACLS[access]) - - def get_container_access(self, name): - """Get the control list from a container. - - :param str name: Name of the container. - """ - container = self.get_container(name, skip_cache=True) - if not container: - raise exc.OpenStackCloudException("Container not found: %s" % name) - acl = container.read_ACL or '' - for key, value in OBJECT_CONTAINER_ACLS.items(): - # Convert to string for the comparison because swiftclient - # returns byte values as bytes sometimes and apparently == - # on bytes doesn't work like you'd think - if str(acl) == str(value): - return key - raise exc.OpenStackCloudException( - "Could not determine container access for ACL: %s." % acl) - - def _get_file_hashes(self, filename): - file_key = "{filename}:{mtime}".format( - filename=filename, - mtime=os.stat(filename).st_mtime) - if file_key not in self._file_hash_cache: - self.log.debug( - 'Calculating hashes for %(filename)s', {'filename': filename}) - md5 = hashlib.md5() - sha256 = hashlib.sha256() - with open(filename, 'rb') as file_obj: - for chunk in iter(lambda: file_obj.read(8192), b''): - md5.update(chunk) - sha256.update(chunk) - self._file_hash_cache[file_key] = dict( - md5=md5.hexdigest(), sha256=sha256.hexdigest()) - self.log.debug( - "Image file %(filename)s md5:%(md5)s sha256:%(sha256)s", - {'filename': filename, - 'md5': self._file_hash_cache[file_key]['md5'], - 'sha256': self._file_hash_cache[file_key]['sha256']}) - return (self._file_hash_cache[file_key]['md5'], - self._file_hash_cache[file_key]['sha256']) - - @_utils.cache_on_arguments() - def get_object_capabilities(self): - """Get infomation about the object-storage service - - The object-storage service publishes a set of capabilities that - include metadata about maximum values and thresholds. - """ - return self.object_store.get_info() - - def get_object_segment_size(self, segment_size): - """Get a segment size that will work given capabilities""" - return self.object_store.get_object_segment_size(segment_size) - - def is_object_stale( - self, container, name, filename, file_md5=None, file_sha256=None): - """Check to see if an object matches the hashes of a file. - - :param container: Name of the container. - :param name: Name of the object. - :param filename: Path to the file. - :param file_md5: - Pre-calculated md5 of the file contents. Defaults to None which - means calculate locally. - :param file_sha256: - Pre-calculated sha256 of the file contents. Defaults to None which - means calculate locally. - """ - return self.object_store.is_object_stale(container, name, filename, - file_md5, file_sha256) - - def create_directory_marker_object(self, container, name, **headers): - """Create a zero-byte directory marker object - - .. note:: - - This method is not needed in most cases. Modern swift does not - require directory marker objects. However, some swift installs may - need these. - - When using swift Static Web and Web Listings to serve static content - one may need to create a zero-byte object to represent each - "directory". Doing so allows Web Listings to generate an index of the - objects inside of it, and allows Static Web to render index.html - "files" that are "inside" the directory. - - :param container: The name of the container. - :param name: Name for the directory marker object within the container. - :param headers: These will be passed through to the object creation - API as HTTP Headers. - """ - headers['content-type'] = 'application/directory' - - return self.create_object( - container, - name, - data='', - generate_checksums=False, - **headers) - - def create_object( - self, container, name, filename=None, - md5=None, sha256=None, segment_size=None, - use_slo=True, metadata=None, - generate_checksums=None, data=None, - **headers): - """Create a file object. - - Automatically uses large-object segments if needed. - - :param container: The name of the container to store the file in. - This container will be created if it does not exist already. - :param name: Name for the object within the container. - :param filename: The path to the local file whose contents will be - uploaded. Mutually exclusive with data. - :param data: The content to upload to the object. Mutually exclusive - with filename. - :param md5: A hexadecimal md5 of the file. (Optional), if it is known - and can be passed here, it will save repeating the expensive md5 - process. It is assumed to be accurate. - :param sha256: A hexadecimal sha256 of the file. (Optional) See md5. - :param segment_size: Break the uploaded object into segments of this - many bytes. (Optional) Shade will attempt to discover the maximum - value for this from the server if it is not specified, or will use - a reasonable default. - :param headers: These will be passed through to the object creation - API as HTTP Headers. - :param use_slo: If the object is large enough to need to be a Large - Object, use a static rather than dynamic object. Static Objects - will delete segment objects when the manifest object is deleted. - (optional, defaults to True) - :param generate_checksums: Whether to generate checksums on the client - side that get added to headers for later prevention of double - uploads of identical data. (optional, defaults to True) - :param metadata: This dict will get changed into headers that set - metadata of the object - - :raises: ``OpenStackCloudException`` on operation error. - """ - return self.object_store.create_object( - container, name, - filename=filename, md5=md5, sha256=sha256, - segment_size=segment_size, use_slo=use_slo, metadata=metadata, - generate_checksums=generate_checksums, data=data, - **headers) - - @property - def _pool_executor(self): - if not self.__pool_executor: - # TODO(mordred) Make this configurable - and probably use Futurist - # instead of concurrent.futures so that people using Eventlet will - # be happier. - self.__pool_executor = concurrent.futures.ThreadPoolExecutor( - max_workers=5) - return self.__pool_executor - - def _wait_for_futures(self, futures, raise_on_error=True): - '''Collect results or failures from a list of running future tasks.''' - - results = [] - retries = [] - - # Check on each result as its thread finishes - for completed in concurrent.futures.as_completed(futures): - try: - result = completed.result() - exceptions.raise_from_response(result) - results.append(result) - except (keystoneauth1.exceptions.RetriableConnectionFailure, - exceptions.HttpException) as e: - error_text = "Exception processing async task: {}".format( - str(e)) - if raise_on_error: - self.log.exception(error_text) - raise - else: - self.log.debug(error_text) - # If we get an exception, put the result into a list so we - # can try again - retries.append(completed.result()) - return results, retries - - def update_object(self, container, name, metadata=None, **headers): - """Update the metadata of an object - - :param container: The name of the container the object is in - :param name: Name for the object within the container. - :param metadata: This dict will get changed into headers that set - metadata of the object - :param headers: These will be passed through to the object update - API as HTTP Headers. - - :raises: ``OpenStackCloudException`` on operation error. - """ - if not metadata: - metadata = {} - - metadata_headers = {} - - for (k, v) in metadata.items(): - metadata_headers['x-object-meta-' + k] = v - - headers = dict(headers, **metadata_headers) - - return self._object_store_client.post( - '{container}/{object}'.format( - container=container, object=name), - headers=headers) - - def list_objects(self, container, full_listing=True, prefix=None): - """List objects. - - :param container: Name of the container to list objects in. - :param full_listing: Ignored. Present for backwards compat - :param string prefix: - only objects with this prefix will be returned. - (optional) - - :returns: list of Munch of the objects - - :raises: OpenStackCloudException on operation error. - """ - data = self.object_store.objects(container, prefix=prefix) - return self._normalize_objects(self._get_and_munchify(None, data)) - - def search_objects(self, container, name=None, filters=None): - """Search objects. - - :param string name: object name. - :param filters: a dict containing additional filters to use. - OR - A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - - :returns: a list of ``munch.Munch`` containing the objects. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. - """ - objects = self.list_objects(container) - return _utils._filter_list(objects, name, filters) - - def delete_object(self, container, name, meta=None): - """Delete an object from a container. - - :param string container: Name of the container holding the object. - :param string name: Name of the object to delete. - :param dict meta: Metadata for the object in question. (optional, will - be fetched if not provided) - - :returns: True if delete succeeded, False if the object was not found. - - :raises: OpenStackCloudException on operation error. - """ - # TODO(mordred) DELETE for swift returns status in text/plain format - # like so: - # Number Deleted: 15 - # Number Not Found: 0 - # Response Body: - # Response Status: 200 OK - # Errors: - # We should ultimately do something with that - try: - if not meta: - meta = self.get_object_metadata(container, name) - if not meta: - return False - params = {} - if meta.get('X-Static-Large-Object', None) == 'True': - params['multipart-manifest'] = 'delete' - self._object_store_client.delete( - '{container}/{object}'.format( - container=container, object=name), - params=params) - return True - except exc.OpenStackCloudHTTPError: - return False - - def delete_autocreated_image_objects(self, container=None): - """Delete all objects autocreated for image uploads. - - This method should generally not be needed, as shade should clean up - the objects it uses for object-based image creation. If something - goes wrong and it is found that there are leaked objects, this method - can be used to delete any objects that shade has created on the user's - behalf in service of image uploads. - """ - if container is None: - container = self._OBJECT_AUTOCREATE_CONTAINER - # This method only makes sense on clouds that use tasks - if not self.image_api_use_tasks: - return False - - deleted = False - for obj in self.list_objects(container): - meta = self.get_object_metadata(container, obj['name']) - if meta.get( - self._OBJECT_AUTOCREATE_KEY, meta.get( - self._SHADE_OBJECT_AUTOCREATE_KEY)) == 'true': - if self.delete_object(container, obj['name'], meta): - deleted = True - return deleted - - def get_object_metadata(self, container, name): - try: - return self._object_store_client.head( - '{container}/{object}'.format( - container=container, object=name)).headers - except exc.OpenStackCloudException as e: - if e.response.status_code == 404: - return None - raise - - def get_object_raw(self, container, obj, query_string=None, stream=False): - """Get a raw response object for an object. - - :param string container: name of the container. - :param string obj: name of the object. - :param string query_string: - query args for uri. (delimiter, prefix, etc.) - :param bool stream: - Whether to stream the response or not. - - :returns: A `requests.Response` - :raises: OpenStackCloudException on operation error. - """ - endpoint = self._get_object_endpoint(container, obj, query_string) - return self._object_store_client.get(endpoint, stream=stream) - - def _get_object_endpoint(self, container, obj, query_string): - endpoint = '{container}/{object}'.format( - container=container, object=obj) - if query_string: - endpoint = '{endpoint}?{query_string}'.format( - endpoint=endpoint, query_string=query_string) - return endpoint - - def stream_object( - self, container, obj, query_string=None, resp_chunk_size=1024): - """Download the content via a streaming iterator. - - :param string container: name of the container. - :param string obj: name of the object. - :param string query_string: - query args for uri. (delimiter, prefix, etc.) - :param int resp_chunk_size: - chunk size of data to read. Only used if the results are - - :returns: - An iterator over the content or None if the object is not found. - :raises: OpenStackCloudException on operation error. - """ - try: - for ret in self.object_store.stream_object( - obj, container=container, - chunk_size=resp_chunk_size): - yield ret - except exc.OpenStackCloudHTTPError as e: - if e.response.status_code == 404: - return - raise - - def get_object(self, container, obj, query_string=None, - resp_chunk_size=1024, outfile=None, stream=False): - """Get the headers and body of an object - - :param string container: name of the container. - :param string obj: name of the object. - :param string query_string: - query args for uri. (delimiter, prefix, etc.) - :param int resp_chunk_size: - chunk size of data to read. Only used if the results are - being written to a file or stream is True. - (optional, defaults to 1k) - :param outfile: - Write the object to a file instead of returning the contents. - If this option is given, body in the return tuple will be None. - outfile can either be a file path given as a string, or a - File like object. - - :returns: Tuple (headers, body) of the object, or None if the object - is not found (404). - :raises: OpenStackCloudException on operation error. - """ - # TODO(mordred) implement resp_chunk_size - endpoint = self._get_object_endpoint(container, obj, query_string) - try: - get_stream = (outfile is not None) - with self._object_store_client.get( - endpoint, stream=get_stream) as response: - response_headers = { - k.lower(): v for k, v in response.headers.items()} - if outfile: - if isinstance(outfile, six.string_types): - outfile_handle = open(outfile, 'wb') - else: - outfile_handle = outfile - for chunk in response.iter_content( - resp_chunk_size, decode_unicode=False): - outfile_handle.write(chunk) - if isinstance(outfile, six.string_types): - outfile_handle.close() - else: - outfile_handle.flush() - return (response_headers, None) - else: - return (response_headers, response.text) - except exc.OpenStackCloudHTTPError as e: - if e.response.status_code == 404: - return None - raise - - def create_subnet(self, network_name_or_id, cidr=None, ip_version=4, - enable_dhcp=False, subnet_name=None, tenant_id=None, - allocation_pools=None, - gateway_ip=None, disable_gateway_ip=False, - dns_nameservers=None, host_routes=None, - ipv6_ra_mode=None, ipv6_address_mode=None, - prefixlen=None, use_default_subnetpool=False, **kwargs): - """Create a subnet on a specified network. - - :param string network_name_or_id: - The unique name or ID of the attached network. If a non-unique - name is supplied, an exception is raised. - :param string cidr: - The CIDR. - :param int ip_version: - The IP version, which is 4 or 6. - :param bool enable_dhcp: - Set to ``True`` if DHCP is enabled and ``False`` if disabled. - Default is ``False``. - :param string subnet_name: - The name of the subnet. - :param string tenant_id: - The ID of the tenant who owns the network. Only administrative users - can specify a tenant ID other than their own. - :param allocation_pools: - A list of dictionaries of the start and end addresses for the - allocation pools. For example:: - - [ - { - "start": "192.168.199.2", - "end": "192.168.199.254" - } - ] - - :param string gateway_ip: - The gateway IP address. When you specify both allocation_pools and - gateway_ip, you must ensure that the gateway IP does not overlap - with the specified allocation pools. - :param bool disable_gateway_ip: - Set to ``True`` if gateway IP address is disabled and ``False`` if - enabled. It is not allowed with gateway_ip. - Default is ``False``. - :param dns_nameservers: - A list of DNS name servers for the subnet. For example:: - - [ "8.8.8.7", "8.8.8.8" ] - - :param host_routes: - A list of host route dictionaries for the subnet. For example:: - - [ - { - "destination": "0.0.0.0/0", - "nexthop": "123.456.78.9" - }, - { - "destination": "192.168.0.0/24", - "nexthop": "192.168.0.1" - } - ] - - :param string ipv6_ra_mode: - IPv6 Router Advertisement mode. Valid values are: 'dhcpv6-stateful', - 'dhcpv6-stateless', or 'slaac'. - :param string ipv6_address_mode: - IPv6 address mode. Valid values are: 'dhcpv6-stateful', - 'dhcpv6-stateless', or 'slaac'. - :param string prefixlen: - The prefix length to use for subnet allocation from a subnet pool. - :param bool use_default_subnetpool: - Use the default subnetpool for ``ip_version`` to obtain a CIDR. It - is required to pass ``None`` to the ``cidr`` argument when enabling - this option. - :param kwargs: Key value pairs to be passed to the Neutron API. - - :returns: The new subnet object. - :raises: OpenStackCloudException on operation error. - """ - - if tenant_id is not None: - filters = {'tenant_id': tenant_id} - else: - filters = None - - network = self.get_network(network_name_or_id, filters) - if not network: - raise exc.OpenStackCloudException( - "Network %s not found." % network_name_or_id) - - if disable_gateway_ip and gateway_ip: - raise exc.OpenStackCloudException( - 'arg:disable_gateway_ip is not allowed with arg:gateway_ip') - - if not cidr and not use_default_subnetpool: - raise exc.OpenStackCloudException( - 'arg:cidr is required when a subnetpool is not used') - - if cidr and use_default_subnetpool: - raise exc.OpenStackCloudException( - 'arg:cidr must be set to None when use_default_subnetpool == ' - 'True') - - # Be friendly on ip_version and allow strings - if isinstance(ip_version, six.string_types): - try: - ip_version = int(ip_version) - except ValueError: - raise exc.OpenStackCloudException( - 'ip_version must be an integer') - - # The body of the neutron message for the subnet we wish to create. - # This includes attributes that are required or have defaults. - subnet = dict({ - 'network_id': network['id'], - 'ip_version': ip_version, - 'enable_dhcp': enable_dhcp, - }, **kwargs) - - # Add optional attributes to the message. - if cidr: - subnet['cidr'] = cidr - if subnet_name: - subnet['name'] = subnet_name - if tenant_id: - subnet['tenant_id'] = tenant_id - if allocation_pools: - subnet['allocation_pools'] = allocation_pools - if gateway_ip: - subnet['gateway_ip'] = gateway_ip - if disable_gateway_ip: - subnet['gateway_ip'] = None - if dns_nameservers: - subnet['dns_nameservers'] = dns_nameservers - if host_routes: - subnet['host_routes'] = host_routes - if ipv6_ra_mode: - subnet['ipv6_ra_mode'] = ipv6_ra_mode - if ipv6_address_mode: - subnet['ipv6_address_mode'] = ipv6_address_mode - if prefixlen: - subnet['prefixlen'] = prefixlen - if use_default_subnetpool: - subnet['use_default_subnetpool'] = True - - response = self.network.post("/subnets.json", json={"subnet": subnet}) - - return self._get_and_munchify('subnet', response) - - def delete_subnet(self, name_or_id): - """Delete a subnet. - - If a name, instead of a unique UUID, is supplied, it is possible - that we could find more than one matching subnet since names are - not required to be unique. An error will be raised in this case. - - :param name_or_id: Name or ID of the subnet being deleted. - - :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. - """ - subnet = self.get_subnet(name_or_id) - if not subnet: - self.log.debug("Subnet %s not found for deleting", name_or_id) - return False - - exceptions.raise_from_response(self.network.delete( - "/subnets/{subnet_id}.json".format(subnet_id=subnet['id']))) - return True - - def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, - gateway_ip=None, disable_gateway_ip=None, - allocation_pools=None, dns_nameservers=None, - host_routes=None): - """Update an existing subnet. - - :param string name_or_id: - Name or ID of the subnet to update. - :param string subnet_name: - The new name of the subnet. - :param bool enable_dhcp: - Set to ``True`` if DHCP is enabled and ``False`` if disabled. - :param string gateway_ip: - The gateway IP address. When you specify both allocation_pools and - gateway_ip, you must ensure that the gateway IP does not overlap - with the specified allocation pools. - :param bool disable_gateway_ip: - Set to ``True`` if gateway IP address is disabled and ``False`` if - enabled. It is not allowed with gateway_ip. - Default is ``False``. - :param allocation_pools: - A list of dictionaries of the start and end addresses for the - allocation pools. For example:: - - [ - { - "start": "192.168.199.2", - "end": "192.168.199.254" - } - ] - - :param dns_nameservers: - A list of DNS name servers for the subnet. For example:: - - [ "8.8.8.7", "8.8.8.8" ] - - :param host_routes: - A list of host route dictionaries for the subnet. For example:: - - [ - { - "destination": "0.0.0.0/0", - "nexthop": "123.456.78.9" - }, - { - "destination": "192.168.0.0/24", - "nexthop": "192.168.0.1" - } - ] - - :returns: The updated subnet object. - :raises: OpenStackCloudException on operation error. - """ - subnet = {} - if subnet_name: - subnet['name'] = subnet_name - if enable_dhcp is not None: - subnet['enable_dhcp'] = enable_dhcp - if gateway_ip: - subnet['gateway_ip'] = gateway_ip - if disable_gateway_ip: - subnet['gateway_ip'] = None - if allocation_pools: - subnet['allocation_pools'] = allocation_pools - if dns_nameservers: - subnet['dns_nameservers'] = dns_nameservers - if host_routes: - subnet['host_routes'] = host_routes - - if not subnet: - self.log.debug("No subnet data to update") - return - - if disable_gateway_ip and gateway_ip: - raise exc.OpenStackCloudException( - 'arg:disable_gateway_ip is not allowed with arg:gateway_ip') - - curr_subnet = self.get_subnet(name_or_id) - if not curr_subnet: - raise exc.OpenStackCloudException( - "Subnet %s not found." % name_or_id) - - response = self.network.put( - "/subnets/{subnet_id}.json".format(subnet_id=curr_subnet['id']), - json={"subnet": subnet}) - return self._get_and_munchify('subnet', response) - - @_utils.valid_kwargs('name', 'admin_state_up', 'mac_address', 'fixed_ips', - 'subnet_id', 'ip_address', 'security_groups', - 'allowed_address_pairs', 'extra_dhcp_opts', - 'device_owner', 'device_id', 'binding:vnic_type', - 'binding:profile', 'port_security_enabled') - def create_port(self, network_id, **kwargs): - """Create a port - - :param network_id: The ID of the network. (Required) - :param name: A symbolic name for the port. (Optional) - :param admin_state_up: The administrative status of the port, - which is up (true, default) or down (false). (Optional) - :param mac_address: The MAC address. (Optional) - :param fixed_ips: List of ip_addresses and subnet_ids. See subnet_id - and ip_address. (Optional) - For example:: - - [ - { - "ip_address": "10.29.29.13", - "subnet_id": "a78484c4-c380-4b47-85aa-21c51a2d8cbd" - }, ... - ] - :param subnet_id: If you specify only a subnet ID, OpenStack Networking - allocates an available IP from that subnet to the port. (Optional) - If you specify both a subnet ID and an IP address, OpenStack - Networking tries to allocate the specified address to the port. - :param ip_address: If you specify both a subnet ID and an IP address, - OpenStack Networking tries to allocate the specified address to - the port. - :param security_groups: List of security group UUIDs. (Optional) - :param allowed_address_pairs: Allowed address pairs list (Optional) - For example:: - - [ - { - "ip_address": "23.23.23.1", - "mac_address": "fa:16:3e:c4:cd:3f" - }, ... - ] - :param extra_dhcp_opts: Extra DHCP options. (Optional). - For example:: - - [ - { - "opt_name": "opt name1", - "opt_value": "value1" - }, ... - ] - :param device_owner: The ID of the entity that uses this port. - For example, a DHCP agent. (Optional) - :param device_id: The ID of the device that uses this port. - For example, a virtual server. (Optional) - :param binding vnic_type: The type of the created port. (Optional) - :param port_security_enabled: The security port state created on - the network. (Optional) - - :returns: a ``munch.Munch`` describing the created port. - - :raises: ``OpenStackCloudException`` on operation error. - """ - kwargs['network_id'] = network_id - - data = proxy._json_response( - self.network.post("/ports.json", json={'port': kwargs}), - error_message="Error creating port for network {0}".format( - network_id)) - return self._get_and_munchify('port', data) - - @_utils.valid_kwargs('name', 'admin_state_up', 'fixed_ips', - 'security_groups', 'allowed_address_pairs', - 'extra_dhcp_opts', 'device_owner', 'device_id', - 'binding:vnic_type', 'binding:profile', - 'port_security_enabled') - def update_port(self, name_or_id, **kwargs): - """Update a port - - Note: to unset an attribute use None value. To leave an attribute - untouched just omit it. - - :param name_or_id: name or ID of the port to update. (Required) - :param name: A symbolic name for the port. (Optional) - :param admin_state_up: The administrative status of the port, - which is up (true) or down (false). (Optional) - :param fixed_ips: List of ip_addresses and subnet_ids. (Optional) - If you specify only a subnet ID, OpenStack Networking allocates - an available IP from that subnet to the port. - If you specify both a subnet ID and an IP address, OpenStack - Networking tries to allocate the specified address to the port. - For example:: - - [ - { - "ip_address": "10.29.29.13", - "subnet_id": "a78484c4-c380-4b47-85aa-21c51a2d8cbd" - }, ... - ] - :param security_groups: List of security group UUIDs. (Optional) - :param allowed_address_pairs: Allowed address pairs list (Optional) - For example:: - - [ - { - "ip_address": "23.23.23.1", - "mac_address": "fa:16:3e:c4:cd:3f" - }, ... - ] - :param extra_dhcp_opts: Extra DHCP options. (Optional). - For example:: - - [ - { - "opt_name": "opt name1", - "opt_value": "value1" - }, ... - ] - :param device_owner: The ID of the entity that uses this port. - For example, a DHCP agent. (Optional) - :param device_id: The ID of the resource this port is attached to. - :param binding vnic_type: The type of the created port. (Optional) - :param port_security_enabled: The security port state created on - the network. (Optional) - - :returns: a ``munch.Munch`` describing the updated port. - - :raises: OpenStackCloudException on operation error. - """ - port = self.get_port(name_or_id=name_or_id) - if port is None: - raise exc.OpenStackCloudException( - "failed to find port '{port}'".format(port=name_or_id)) - - data = proxy._json_response( - self.network.put( - "/ports/{port_id}.json".format(port_id=port['id']), - json={"port": kwargs}), - error_message="Error updating port {0}".format(name_or_id)) - return self._get_and_munchify('port', data) - - def delete_port(self, name_or_id): - """Delete a port - - :param name_or_id: ID or name of the port to delete. - - :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. - """ - port = self.get_port(name_or_id=name_or_id) - if port is None: - self.log.debug("Port %s not found for deleting", name_or_id) - return False - - exceptions.raise_from_response( - self.network.delete( - "/ports/{port_id}.json".format(port_id=port['id'])), - error_message="Error deleting port {0}".format(name_or_id)) - return True - - def create_security_group(self, name, description, project_id=None): - """Create a new security group - - :param string name: A name for the security group. - :param string description: Describes the security group. - :param string project_id: - Specify the project ID this security group will be created - on (admin-only). - - :returns: A ``munch.Munch`` representing the new security group. - - :raises: OpenStackCloudException on operation error. - :raises: OpenStackCloudUnavailableFeature if security groups are - not supported on this cloud. - """ - - # Security groups not supported - if not self._has_secgroups(): - raise exc.OpenStackCloudUnavailableFeature( - "Unavailable feature: security groups" - ) - - data = [] - security_group_json = { - 'security_group': { - 'name': name, 'description': description - }} - if project_id is not None: - security_group_json['security_group']['tenant_id'] = project_id - if self._use_neutron_secgroups(): - data = proxy._json_response( - self.network.post( - '/security-groups.json', - json=security_group_json), - error_message="Error creating security group {0}".format(name)) - else: - data = proxy._json_response(self.compute.post( - '/os-security-groups', json=security_group_json)) - return self._normalize_secgroup( - self._get_and_munchify('security_group', data)) - - def delete_security_group(self, name_or_id): - """Delete a security group - - :param string name_or_id: The name or unique ID of the security group. - - :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. - :raises: OpenStackCloudUnavailableFeature if security groups are - not supported on this cloud. - """ - # Security groups not supported - if not self._has_secgroups(): - raise exc.OpenStackCloudUnavailableFeature( - "Unavailable feature: security groups" - ) - - # TODO(mordred): Let's come back and stop doing a GET before we do - # the delete. - secgroup = self.get_security_group(name_or_id) - if secgroup is None: - self.log.debug('Security group %s not found for deleting', - name_or_id) - return False - - if self._use_neutron_secgroups(): - exceptions.raise_from_response( - self.network.delete( - '/security-groups/{sg_id}.json'.format( - sg_id=secgroup['id'])), - error_message="Error deleting security group {0}".format( - name_or_id) - ) - return True - - else: - proxy._json_response(self.compute.delete( - '/os-security-groups/{id}'.format(id=secgroup['id']))) - return True - - @_utils.valid_kwargs('name', 'description') - def update_security_group(self, name_or_id, **kwargs): - """Update a security group - - :param string name_or_id: Name or ID of the security group to update. - :param string name: New name for the security group. - :param string description: New description for the security group. - - :returns: A ``munch.Munch`` describing the updated security group. - - :raises: OpenStackCloudException on operation error. - """ - # Security groups not supported - if not self._has_secgroups(): - raise exc.OpenStackCloudUnavailableFeature( - "Unavailable feature: security groups" - ) - - group = self.get_security_group(name_or_id) - - if group is None: - raise exc.OpenStackCloudException( - "Security group %s not found." % name_or_id) - - if self._use_neutron_secgroups(): - data = proxy._json_response( - self.network.put( - '/security-groups/{sg_id}.json'.format(sg_id=group['id']), - json={'security_group': kwargs}), - error_message="Error updating security group {0}".format( - name_or_id)) - else: - for key in ('name', 'description'): - kwargs.setdefault(key, group[key]) - data = proxy._json_response( - self.compute.put( - '/os-security-groups/{id}'.format(id=group['id']), - json={'security_group': kwargs})) - return self._normalize_secgroup( - self._get_and_munchify('security_group', data)) - - def create_security_group_rule(self, - secgroup_name_or_id, - port_range_min=None, - port_range_max=None, - protocol=None, - remote_ip_prefix=None, - remote_group_id=None, - direction='ingress', - ethertype='IPv4', - project_id=None): - """Create a new security group rule - - :param string secgroup_name_or_id: - The security group name or ID to associate with this security - group rule. If a non-unique group name is given, an exception - is raised. - :param int port_range_min: - The minimum port number in the range that is matched by the - security group rule. If the protocol is TCP or UDP, this value - must be less than or equal to the port_range_max attribute value. - If nova is used by the cloud provider for security groups, then - a value of None will be transformed to -1. - :param int port_range_max: - The maximum port number in the range that is matched by the - security group rule. The port_range_min attribute constrains the - port_range_max attribute. If nova is used by the cloud provider - for security groups, then a value of None will be transformed - to -1. - :param string protocol: - The protocol that is matched by the security group rule. Valid - values are None, tcp, udp, and icmp. - :param string remote_ip_prefix: - The remote IP prefix to be associated with this security group - rule. This attribute matches the specified IP prefix as the - source IP address of the IP packet. - :param string remote_group_id: - The remote group ID to be associated with this security group - rule. - :param string direction: - Ingress or egress: The direction in which the security group - rule is applied. For a compute instance, an ingress security - group rule is applied to incoming (ingress) traffic for that - instance. An egress rule is applied to traffic leaving the - instance. - :param string ethertype: - Must be IPv4 or IPv6, and addresses represented in CIDR must - match the ingress or egress rules. - :param string project_id: - Specify the project ID this security group will be created - on (admin-only). - - :returns: A ``munch.Munch`` representing the new security group rule. - - :raises: OpenStackCloudException on operation error. - """ - # Security groups not supported - if not self._has_secgroups(): - raise exc.OpenStackCloudUnavailableFeature( - "Unavailable feature: security groups" - ) - - secgroup = self.get_security_group(secgroup_name_or_id) - if not secgroup: - raise exc.OpenStackCloudException( - "Security group %s not found." % secgroup_name_or_id) - - if self._use_neutron_secgroups(): - # NOTE: Nova accepts -1 port numbers, but Neutron accepts None - # as the equivalent value. - rule_def = { - 'security_group_id': secgroup['id'], - 'port_range_min': - None if port_range_min == -1 else port_range_min, - 'port_range_max': - None if port_range_max == -1 else port_range_max, - 'protocol': protocol, - 'remote_ip_prefix': remote_ip_prefix, - 'remote_group_id': remote_group_id, - 'direction': direction, - 'ethertype': ethertype - } - if project_id is not None: - rule_def['tenant_id'] = project_id - - data = proxy._json_response( - self.network.post( - '/security-group-rules.json', - json={'security_group_rule': rule_def}), - error_message="Error creating security group rule") - else: - # NOTE: Neutron accepts None for protocol. Nova does not. - if protocol is None: - raise exc.OpenStackCloudException('Protocol must be specified') - - if direction == 'egress': - self.log.debug( - 'Rule creation failed: Nova does not support egress rules' - ) - raise exc.OpenStackCloudException( - 'No support for egress rules') - - # NOTE: Neutron accepts None for ports, but Nova requires -1 - # as the equivalent value for ICMP. - # - # For TCP/UDP, if both are None, Neutron allows this and Nova - # represents this as all ports (1-65535). Nova does not accept - # None values, so to hide this difference, we will automatically - # convert to the full port range. If only a single port value is - # specified, it will error as normal. - if protocol == 'icmp': - if port_range_min is None: - port_range_min = -1 - if port_range_max is None: - port_range_max = -1 - elif protocol in ['tcp', 'udp']: - if port_range_min is None and port_range_max is None: - port_range_min = 1 - port_range_max = 65535 - - security_group_rule_dict = dict(security_group_rule=dict( - parent_group_id=secgroup['id'], - ip_protocol=protocol, - from_port=port_range_min, - to_port=port_range_max, - cidr=remote_ip_prefix, - group_id=remote_group_id - )) - if project_id is not None: - security_group_rule_dict[ - 'security_group_rule']['tenant_id'] = project_id - data = proxy._json_response( - self.compute.post( - '/os-security-group-rules', - json=security_group_rule_dict - )) - return self._normalize_secgroup_rule( - self._get_and_munchify('security_group_rule', data)) - - def delete_security_group_rule(self, rule_id): - """Delete a security group rule - - :param string rule_id: The unique ID of the security group rule. - - :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. - :raises: OpenStackCloudUnavailableFeature if security groups are - not supported on this cloud. - """ - # Security groups not supported - if not self._has_secgroups(): - raise exc.OpenStackCloudUnavailableFeature( - "Unavailable feature: security groups" - ) - - if self._use_neutron_secgroups(): - try: - exceptions.raise_from_response( - self.network.delete( - '/security-group-rules/{sg_id}.json'.format( - sg_id=rule_id)), - error_message="Error deleting security group rule " - "{0}".format(rule_id)) - except exc.OpenStackCloudResourceNotFound: - return False - return True - - else: - try: - exceptions.raise_from_response( - self.compute.delete( - '/os-security-group-rules/{id}'.format(id=rule_id))) - except exc.OpenStackCloudResourceNotFound: - return False - - return True - - def list_zones(self): - """List all available zones. - - :returns: A list of zones dicts. - - """ - data = self._dns_client.get( - "/zones", - error_message="Error fetching zones list") - return self._get_and_munchify('zones', data) - - def get_zone(self, name_or_id, filters=None): - """Get a zone by name or ID. - - :param name_or_id: Name or ID of the zone - :param filters: - A dictionary of meta data to use for further filtering - OR - A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - - :returns: A zone dict or None if no matching zone is found. - - """ - return _utils._get_entity(self, 'zone', name_or_id, filters) - - def search_zones(self, name_or_id=None, filters=None): - zones = self.list_zones() - return _utils._filter_list(zones, name_or_id, filters) - - def create_zone(self, name, zone_type=None, email=None, description=None, - ttl=None, masters=None): - """Create a new zone. - - :param name: Name of the zone being created. - :param zone_type: Type of the zone (primary/secondary) - :param email: Email of the zone owner (only - applies if zone_type is primary) - :param description: Description of the zone - :param ttl: TTL (Time to live) value in seconds - :param masters: Master nameservers (only applies - if zone_type is secondary) - - :returns: a dict representing the created zone. - - :raises: OpenStackCloudException on operation error. - """ - - # We capitalize in case the user passes time in lowercase, as - # designate call expects PRIMARY/SECONDARY - if zone_type is not None: - zone_type = zone_type.upper() - if zone_type not in ('PRIMARY', 'SECONDARY'): - raise exc.OpenStackCloudException( - "Invalid type %s, valid choices are PRIMARY or SECONDARY" % - zone_type) - - zone = { - "name": name, - "email": email, - "description": description, - } - if ttl is not None: - zone["ttl"] = ttl - - if zone_type is not None: - zone["type"] = zone_type - - if masters is not None: - zone["masters"] = masters - - data = self._dns_client.post( - "/zones", json=zone, - error_message="Unable to create zone {name}".format(name=name)) - return self._get_and_munchify(key=None, data=data) - - @_utils.valid_kwargs('email', 'description', 'ttl', 'masters') - def update_zone(self, name_or_id, **kwargs): - """Update a zone. - - :param name_or_id: Name or ID of the zone being updated. - :param email: Email of the zone owner (only - applies if zone_type is primary) - :param description: Description of the zone - :param ttl: TTL (Time to live) value in seconds - :param masters: Master nameservers (only applies - if zone_type is secondary) - - :returns: a dict representing the updated zone. - - :raises: OpenStackCloudException on operation error. - """ - zone = self.get_zone(name_or_id) - if not zone: - raise exc.OpenStackCloudException( - "Zone %s not found." % name_or_id) - - data = self._dns_client.patch( - "/zones/{zone_id}".format(zone_id=zone['id']), json=kwargs, - error_message="Error updating zone {0}".format(name_or_id)) - return self._get_and_munchify(key=None, data=data) - - def delete_zone(self, name_or_id): - """Delete a zone. - - :param name_or_id: Name or ID of the zone being deleted. - - :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. - """ - - zone = self.get_zone(name_or_id) - if zone is None: - self.log.debug("Zone %s not found for deleting", name_or_id) - return False - - return self._dns_client.delete( - "/zones/{zone_id}".format(zone_id=zone['id']), - error_message="Error deleting zone {0}".format(name_or_id)) - - return True - - def list_recordsets(self, zone): - """List all available recordsets. - - :param zone: Name or ID of the zone managing the recordset - - :returns: A list of recordsets. - - """ - zone_obj = self.get_zone(zone) - if zone_obj is None: - raise exc.OpenStackCloudException( - "Zone %s not found." % zone) - return self._dns_client.get( - "/zones/{zone_id}/recordsets".format(zone_id=zone_obj['id']), - error_message="Error fetching recordsets list")['recordsets'] - - def get_recordset(self, zone, name_or_id): - """Get a recordset by name or ID. - - :param zone: Name or ID of the zone managing the recordset - :param name_or_id: Name or ID of the recordset - - :returns: A recordset dict or None if no matching recordset is - found. - - """ - zone_obj = self.get_zone(zone) - if zone_obj is None: - raise exc.OpenStackCloudException( - "Zone %s not found." % zone) - try: - return self._dns_client.get( - "/zones/{zone_id}/recordsets/{recordset_id}".format( - zone_id=zone_obj['id'], recordset_id=name_or_id), - error_message="Error fetching recordset") - except Exception: - return None - - def search_recordsets(self, zone, name_or_id=None, filters=None): - recordsets = self.list_recordsets(zone=zone) - return _utils._filter_list(recordsets, name_or_id, filters) - - def create_recordset(self, zone, name, recordset_type, records, - description=None, ttl=None): - """Create a recordset. - - :param zone: Name or ID of the zone managing the recordset - :param name: Name of the recordset - :param recordset_type: Type of the recordset - :param records: List of the recordset definitions - :param description: Description of the recordset - :param ttl: TTL value of the recordset - - :returns: a dict representing the created recordset. - - :raises: OpenStackCloudException on operation error. - - """ - zone_obj = self.get_zone(zone) - if zone_obj is None: - raise exc.OpenStackCloudException( - "Zone %s not found." % zone) - - # We capitalize the type in case the user sends in lowercase - recordset_type = recordset_type.upper() - - body = { - 'name': name, - 'type': recordset_type, - 'records': records - } - - if description: - body['description'] = description - - if ttl: - body['ttl'] = ttl - - return self._dns_client.post( - "/zones/{zone_id}/recordsets".format(zone_id=zone_obj['id']), - json=body, - error_message="Error creating recordset {name}".format(name=name)) - - @_utils.valid_kwargs('description', 'ttl', 'records') - def update_recordset(self, zone, name_or_id, **kwargs): - """Update a recordset. - - :param zone: Name or ID of the zone managing the recordset - :param name_or_id: Name or ID of the recordset being updated. - :param records: List of the recordset definitions - :param description: Description of the recordset - :param ttl: TTL (Time to live) value in seconds of the recordset - - :returns: a dict representing the updated recordset. - - :raises: OpenStackCloudException on operation error. - """ - zone_obj = self.get_zone(zone) - if zone_obj is None: - raise exc.OpenStackCloudException( - "Zone %s not found." % zone) - - recordset_obj = self.get_recordset(zone, name_or_id) - if recordset_obj is None: - raise exc.OpenStackCloudException( - "Recordset %s not found." % name_or_id) - - new_recordset = self._dns_client.put( - "/zones/{zone_id}/recordsets/{recordset_id}".format( - zone_id=zone_obj['id'], recordset_id=name_or_id), json=kwargs, - error_message="Error updating recordset {0}".format(name_or_id)) - - return new_recordset - - def delete_recordset(self, zone, name_or_id): - """Delete a recordset. - - :param zone: Name or ID of the zone managing the recordset. - :param name_or_id: Name or ID of the recordset being deleted. - - :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. - """ - - zone_obj = self.get_zone(zone) - if zone_obj is None: - self.log.debug("Zone %s not found for deleting", zone) - return False - - recordset = self.get_recordset(zone_obj['id'], name_or_id) - if recordset is None: - self.log.debug("Recordset %s not found for deleting", name_or_id) - return False - - self._dns_client.delete( - "/zones/{zone_id}/recordsets/{recordset_id}".format( - zone_id=zone_obj['id'], recordset_id=name_or_id), - error_message="Error deleting recordset {0}".format(name_or_id)) - - return True - - @_utils.cache_on_arguments() - def list_coe_clusters(self): - """List COE(Ccontainer Orchestration Engine) cluster. - - :returns: a list of dicts containing the cluster. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. - """ - with _utils.shade_exceptions("Error fetching cluster list"): - data = self._container_infra_client.get('/clusters') - return self._normalize_coe_clusters( - self._get_and_munchify('clusters', data)) - - def search_coe_clusters( - self, name_or_id=None, filters=None): - """Search COE cluster. - - :param name_or_id: cluster name or ID. - :param filters: a dict containing additional filters to use. - :param detail: a boolean to control if we need summarized or - detailed output. - - :returns: a list of dict containing the cluster - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. - """ - coe_clusters = self.list_coe_clusters() - return _utils._filter_list( - coe_clusters, name_or_id, filters) - - def get_coe_cluster(self, name_or_id, filters=None): - """Get a COE cluster by name or ID. - - :param name_or_id: Name or ID of the cluster. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - OR - A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - - :returns: A cluster dict or None if no matching cluster is found. - """ - return _utils._get_entity(self, 'coe_cluster', name_or_id, - filters=filters) - - def create_coe_cluster( - self, name, cluster_template_id, **kwargs): - """Create a COE cluster based on given cluster template. - - :param string name: Name of the cluster. - :param string image_id: ID of the cluster template to use. - - Other arguments will be passed in kwargs. - - :returns: a dict containing the cluster description - - :raises: ``OpenStackCloudException`` if something goes wrong during - the OpenStack API call - """ - error_message = ("Error creating cluster of name" - " {cluster_name}".format(cluster_name=name)) - with _utils.shade_exceptions(error_message): - body = kwargs.copy() - body['name'] = name - body['cluster_template_id'] = cluster_template_id - - cluster = self._container_infra_client.post( - '/clusters', json=body) - - self.list_coe_clusters.invalidate(self) - return cluster - - def delete_coe_cluster(self, name_or_id): - """Delete a COE cluster. - - :param name_or_id: Name or unique ID of the cluster. - :returns: True if the delete succeeded, False if the - cluster was not found. - - :raises: OpenStackCloudException on operation error. - """ - - cluster = self.get_coe_cluster(name_or_id) - - if not cluster: - self.log.debug( - "COE Cluster %(name_or_id)s does not exist", - {'name_or_id': name_or_id}, - exc_info=True) - return False - - with _utils.shade_exceptions("Error in deleting COE cluster"): - self._container_infra_client.delete( - '/clusters/{id}'.format(id=cluster['id'])) - self.list_coe_clusters.invalidate(self) - - return True - - @_utils.valid_kwargs('node_count') - def update_coe_cluster(self, name_or_id, operation, **kwargs): - """Update a COE cluster. - - :param name_or_id: Name or ID of the COE cluster being updated. - :param operation: Operation to perform - add, remove, replace. - - Other arguments will be passed with kwargs. - - :returns: a dict representing the updated cluster. - - :raises: OpenStackCloudException on operation error. - """ - self.list_coe_clusters.invalidate(self) - cluster = self.get_coe_cluster(name_or_id) - if not cluster: - raise exc.OpenStackCloudException( - "COE cluster %s not found." % name_or_id) - - if operation not in ['add', 'replace', 'remove']: - raise TypeError( - "%s operation not in 'add', 'replace', 'remove'" % operation) - - patches = _utils.generate_patches_from_kwargs(operation, **kwargs) - # No need to fire an API call if there is an empty patch - if not patches: - return cluster - - with _utils.shade_exceptions( - "Error updating COE cluster {0}".format(name_or_id)): - self._container_infra_client.patch( - '/clusters/{id}'.format(id=cluster['id']), - json=patches) - - new_cluster = self.get_coe_cluster(name_or_id) - return new_cluster - - def get_coe_cluster_certificate(self, cluster_id): - """Get details about the CA certificate for a cluster by name or ID. - - :param cluster_id: ID of the cluster. - - :returns: Details about the CA certificate for the given cluster. - """ - msg = ("Error fetching CA cert for the cluster {cluster_id}".format( - cluster_id=cluster_id)) - url = "/certificates/{cluster_id}".format(cluster_id=cluster_id) - data = self._container_infra_client.get(url, - error_message=msg) - - return self._get_and_munchify(key=None, data=data) - - def sign_coe_cluster_certificate(self, cluster_id, csr): - """Sign client key and generate the CA certificate for a cluster - - :param cluster_id: UUID of the cluster. - :param csr: Certificate Signing Request (CSR) for authenticating - client key.The CSR will be used by Magnum to generate - a signed certificate that client will use to communicate - with the cluster. - - :returns: a dict representing the signed certs. - - :raises: OpenStackCloudException on operation error. - """ - error_message = ("Error signing certs for cluster" - " {cluster_id}".format(cluster_id=cluster_id)) - with _utils.shade_exceptions(error_message): - body = {} - body['cluster_uuid'] = cluster_id - body['csr'] = csr - - certs = self._container_infra_client.post( - '/certificates', json=body) - - return self._get_and_munchify(key=None, data=certs) - - @_utils.cache_on_arguments() - def list_cluster_templates(self, detail=False): - """List cluster templates. - - :param bool detail. Ignored. Included for backwards compat. - ClusterTemplates are always returned with full details. - - :returns: a list of dicts containing the cluster template details. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. - """ - with _utils.shade_exceptions("Error fetching cluster template list"): - try: - data = self._container_infra_client.get('/clustertemplates') - # NOTE(flwang): Magnum adds /clustertemplates and /cluster - # to deprecate /baymodels and /bay since Newton release. So - # we're using a small tag to indicate if current - # cloud has those two new API endpoints. - self._container_infra_client._has_magnum_after_newton = True - return self._normalize_cluster_templates( - self._get_and_munchify('clustertemplates', data)) - except exc.OpenStackCloudURINotFound: - data = self._container_infra_client.get('/baymodels/detail') - return self._normalize_cluster_templates( - self._get_and_munchify('baymodels', data)) - list_baymodels = list_cluster_templates - list_coe_cluster_templates = list_cluster_templates - - def search_cluster_templates( - self, name_or_id=None, filters=None, detail=False): - """Search cluster templates. - - :param name_or_id: cluster template name or ID. - :param filters: a dict containing additional filters to use. - :param detail: a boolean to control if we need summarized or - detailed output. - - :returns: a list of dict containing the cluster templates - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. - """ - cluster_templates = self.list_cluster_templates(detail=detail) - return _utils._filter_list( - cluster_templates, name_or_id, filters) - search_baymodels = search_cluster_templates - search_coe_cluster_templates = search_cluster_templates - - def get_cluster_template(self, name_or_id, filters=None, detail=False): - """Get a cluster template by name or ID. - - :param name_or_id: Name or ID of the cluster template. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - OR - A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - - :returns: A cluster template dict or None if no matching - cluster template is found. - """ - return _utils._get_entity(self, 'cluster_template', name_or_id, - filters=filters, detail=detail) - get_baymodel = get_cluster_template - get_coe_cluster_template = get_cluster_template - - def create_cluster_template( - self, name, image_id=None, keypair_id=None, coe=None, **kwargs): - """Create a cluster template. - - :param string name: Name of the cluster template. - :param string image_id: Name or ID of the image to use. - :param string keypair_id: Name or ID of the keypair to use. - :param string coe: Name of the coe for the cluster template. - - Other arguments will be passed in kwargs. - - :returns: a dict containing the cluster template description - - :raises: ``OpenStackCloudException`` if something goes wrong during - the OpenStack API call - """ - error_message = ("Error creating cluster template of name" - " {cluster_template_name}".format( - cluster_template_name=name)) - with _utils.shade_exceptions(error_message): - body = kwargs.copy() - body['name'] = name - body['image_id'] = image_id - body['keypair_id'] = keypair_id - body['coe'] = coe - - try: - cluster_template = self._container_infra_client.post( - '/clustertemplates', json=body) - self._container_infra_client._has_magnum_after_newton = True - except exc.OpenStackCloudURINotFound: - cluster_template = self._container_infra_client.post( - '/baymodels', json=body) - - self.list_cluster_templates.invalidate(self) - return cluster_template - create_baymodel = create_cluster_template - create_coe_cluster_template = create_cluster_template - - def delete_cluster_template(self, name_or_id): - """Delete a cluster template. - - :param name_or_id: Name or unique ID of the cluster template. - :returns: True if the delete succeeded, False if the - cluster template was not found. - - :raises: OpenStackCloudException on operation error. - """ - - cluster_template = self.get_cluster_template(name_or_id) - - if not cluster_template: - self.log.debug( - "Cluster template %(name_or_id)s does not exist", - {'name_or_id': name_or_id}, - exc_info=True) - return False - - with _utils.shade_exceptions("Error in deleting cluster template"): - if getattr(self._container_infra_client, - '_has_magnum_after_newton', False): - self._container_infra_client.delete( - '/clustertemplates/{id}'.format(id=cluster_template['id'])) - else: - self._container_infra_client.delete( - '/baymodels/{id}'.format(id=cluster_template['id'])) - self.list_cluster_templates.invalidate(self) - - return True - delete_baymodel = delete_cluster_template - delete_coe_cluster_template = delete_cluster_template - - @_utils.valid_kwargs('name', 'image_id', 'flavor_id', 'master_flavor_id', - 'keypair_id', 'external_network_id', 'fixed_network', - 'dns_nameserver', 'docker_volume_size', 'labels', - 'coe', 'http_proxy', 'https_proxy', 'no_proxy', - 'network_driver', 'tls_disabled', 'public', - 'registry_enabled', 'volume_driver') - def update_cluster_template(self, name_or_id, operation, **kwargs): - """Update a cluster template. - - :param name_or_id: Name or ID of the cluster template being updated. - :param operation: Operation to perform - add, remove, replace. - - Other arguments will be passed with kwargs. - - :returns: a dict representing the updated cluster template. - - :raises: OpenStackCloudException on operation error. - """ - self.list_cluster_templates.invalidate(self) - cluster_template = self.get_cluster_template(name_or_id) - if not cluster_template: - raise exc.OpenStackCloudException( - "Cluster template %s not found." % name_or_id) - - if operation not in ['add', 'replace', 'remove']: - raise TypeError( - "%s operation not in 'add', 'replace', 'remove'" % operation) - - patches = _utils.generate_patches_from_kwargs(operation, **kwargs) - # No need to fire an API call if there is an empty patch - if not patches: - return cluster_template - - with _utils.shade_exceptions( - "Error updating cluster template {0}".format(name_or_id)): - if getattr(self._container_infra_client, - '_has_magnum_after_newton', False): - self._container_infra_client.patch( - '/clustertemplates/{id}'.format(id=cluster_template['id']), - json=patches) - else: - self._container_infra_client.patch( - '/baymodels/{id}'.format(id=cluster_template['id']), - json=patches) - - new_cluster_template = self.get_cluster_template(name_or_id) - return new_cluster_template - update_baymodel = update_cluster_template - update_coe_cluster_template = update_cluster_template - - def list_nics(self): - """Return a list of all bare metal ports.""" - return [nic._to_munch() for nic in self.baremetal.ports(details=True)] - - def list_nics_for_machine(self, uuid): - """Returns a list of ports present on the machine node. - - :param uuid: String representing machine UUID value in - order to identify the machine. - :returns: A list of ports. - """ - # TODO(dtantsur): support node names here. - return [nic._to_munch() - for nic in self.baremetal.ports(details=True, node_id=uuid)] - - def get_nic_by_mac(self, mac): - """Get bare metal NIC by its hardware address (usually MAC).""" - results = [nic._to_munch() - for nic in self.baremetal.ports(address=mac, details=True)] - try: - return results[0] - except IndexError: - return None - - def list_machines(self): - """List Machines. - - :returns: list of ``munch.Munch`` representing machines. - """ - return [self._normalize_machine(node._to_munch()) - for node in self.baremetal.nodes()] - - def get_machine(self, name_or_id): - """Get Machine by name or uuid - - Search the baremetal host out by utilizing the supplied id value - which can consist of a name or UUID. - - :param name_or_id: A node name or UUID that will be looked up. - - :returns: ``munch.Munch`` representing the node found or None if no - nodes are found. - """ - try: - return self._normalize_machine( - self.baremetal.get_node(name_or_id)._to_munch()) - except exc.OpenStackCloudResourceNotFound: - return None - - def get_machine_by_mac(self, mac): - """Get machine by port MAC address - - :param mac: Port MAC address to query in order to return a node. - - :returns: ``munch.Munch`` representing the node found or None - if the node is not found. - """ - nic = self.get_nic_by_mac(mac) - if nic is None: - return None - else: - return self.get_machine(nic['node_uuid']) - - def inspect_machine(self, name_or_id, wait=False, timeout=3600): - """Inspect a Barmetal machine - - Engages the Ironic node inspection behavior in order to collect - metadata about the baremetal machine. - - :param name_or_id: String representing machine name or UUID value in - order to identify the machine. - - :param wait: Boolean value controlling if the method is to wait for - the desired state to be reached or a failure to occur. - - :param timeout: Integer value, defautling to 3600 seconds, for the$ - wait state to reach completion. - - :returns: ``munch.Munch`` representing the current state of the machine - upon exit of the method. - """ - - return_to_available = False - - node = self.baremetal.get_node(name_or_id) - - # NOTE(TheJulia): If in available state, we can do this. However, - # we need to to move the machine back to manageable first. - if node.provision_state == 'available': - if node.instance_id: - raise exc.OpenStackCloudException( - "Refusing to inspect available machine %(node)s " - "which is associated with an instance " - "(instance_uuid %(inst)s)" % - {'node': node.id, 'inst': node.instance_id}) - - return_to_available = True - # NOTE(TheJulia): Changing available machine to managedable state - # and due to state transitions we need to until that transition has - # completed. - node = self.baremetal.set_node_provision_state(node, 'manage', - wait=True, - timeout=timeout) - - if node.provision_state not in ('manageable', 'inspect failed'): - raise exc.OpenStackCloudException( - "Machine %(node)s must be in 'manageable', 'inspect failed' " - "or 'available' provision state to start inspection, the " - "current state is %(state)s" % - {'node': node.id, 'state': node.provision_state}) - - node = self.baremetal.set_node_provision_state(node, 'inspect', - wait=True, - timeout=timeout) - - if return_to_available: - node = self.baremetal.set_node_provision_state(node, 'provide', - wait=True, - timeout=timeout) - - return node._to_munch() - - def register_machine(self, nics, wait=False, timeout=3600, - lock_timeout=600, **kwargs): - """Register Baremetal with Ironic - - Allows for the registration of Baremetal nodes with Ironic - and population of pertinant node information or configuration - to be passed to the Ironic API for the node. - - This method also creates ports for a list of MAC addresses passed - in to be utilized for boot and potentially network configuration. - - If a failure is detected creating the network ports, any ports - created are deleted, and the node is removed from Ironic. - - :param nics: - An array of MAC addresses that represent the - network interfaces for the node to be created. - - Example:: - - [ - {'mac': 'aa:bb:cc:dd:ee:01'}, - {'mac': 'aa:bb:cc:dd:ee:02'} - ] - - :param wait: Boolean value, defaulting to false, to wait for the - node to reach the available state where the node can be - provisioned. It must be noted, when set to false, the - method will still wait for locks to clear before sending - the next required command. - - :param timeout: Integer value, defautling to 3600 seconds, for the - wait state to reach completion. - - :param lock_timeout: Integer value, defaulting to 600 seconds, for - locks to clear. - - :param kwargs: Key value pairs to be passed to the Ironic API, - including uuid, name, chassis_uuid, driver_info, - parameters. - - :raises: OpenStackCloudException on operation error. - - :returns: Returns a ``munch.Munch`` representing the new - baremetal node. - """ - - msg = ("Baremetal machine node failed to be created.") - port_msg = ("Baremetal machine port failed to be created.") - - url = '/nodes' - # TODO(TheJulia): At some point we need to figure out how to - # handle data across when the requestor is defining newer items - # with the older api. - machine = self._baremetal_client.post(url, - json=kwargs, - error_message=msg, - microversion="1.6") - - created_nics = [] - try: - for row in nics: - payload = {'address': row['mac'], - 'node_uuid': machine['uuid']} - nic = self._baremetal_client.post('/ports', - json=payload, - error_message=port_msg) - created_nics.append(nic['uuid']) - - except Exception as e: - self.log.debug("ironic NIC registration failed", exc_info=True) - # TODO(mordred) Handle failures here - try: - for uuid in created_nics: - try: - port_url = '/ports/{uuid}'.format(uuid=uuid) - # NOTE(TheJulia): Added in hope that it is logged. - port_msg = ('Failed to delete port {port} for node ' - '{node}').format(port=uuid, - node=machine['uuid']) - self._baremetal_client.delete(port_url, - error_message=port_msg) - except Exception: - pass - finally: - version = "1.6" - msg = "Baremetal machine failed to be deleted." - url = '/nodes/{node_id}'.format( - node_id=machine['uuid']) - self._baremetal_client.delete(url, - error_message=msg, - microversion=version) - raise exc.OpenStackCloudException( - "Error registering NICs with the baremetal service: %s" - % str(e)) - - with _utils.shade_exceptions( - "Error transitioning node to available state"): - if wait: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for node transition to " - "available state"): - - machine = self.get_machine(machine['uuid']) - - # Note(TheJulia): Per the Ironic state code, a node - # that fails returns to enroll state, which means a failed - # node cannot be determined at this point in time. - if machine['provision_state'] in ['enroll']: - self.node_set_provision_state( - machine['uuid'], 'manage') - elif machine['provision_state'] in ['manageable']: - self.node_set_provision_state( - machine['uuid'], 'provide') - elif machine['last_error'] is not None: - raise exc.OpenStackCloudException( - "Machine encountered a failure: %s" - % machine['last_error']) - - # Note(TheJulia): Earlier versions of Ironic default to - # None and later versions default to available up until - # the introduction of enroll state. - # Note(TheJulia): The node will transition through - # cleaning if it is enabled, and we will wait for - # completion. - elif machine['provision_state'] in ['available', None]: - break - - else: - if machine['provision_state'] in ['enroll']: - self.node_set_provision_state(machine['uuid'], 'manage') - # Note(TheJulia): We need to wait for the lock to clear - # before we attempt to set the machine into provide state - # which allows for the transition to available. - for count in utils.iterate_timeout( - lock_timeout, - "Timeout waiting for reservation to clear " - "before setting provide state"): - machine = self.get_machine(machine['uuid']) - if (machine['reservation'] is None - and machine['provision_state'] != 'enroll'): - # NOTE(TheJulia): In this case, the node has - # has moved on from the previous state and is - # likely not being verified, as no lock is - # present on the node. - self.node_set_provision_state( - machine['uuid'], 'provide') - machine = self.get_machine(machine['uuid']) - break - - elif machine['provision_state'] in [ - 'cleaning', - 'available']: - break - - elif machine['last_error'] is not None: - raise exc.OpenStackCloudException( - "Machine encountered a failure: %s" - % machine['last_error']) - if not isinstance(machine, str): - return self._normalize_machine(machine) - else: - return machine - - def unregister_machine(self, nics, uuid, wait=False, timeout=600): - """Unregister Baremetal from Ironic - - Removes entries for Network Interfaces and baremetal nodes - from an Ironic API - - :param nics: An array of strings that consist of MAC addresses - to be removed. - :param string uuid: The UUID of the node to be deleted. - - :param wait: Boolean value, defaults to false, if to block the method - upon the final step of unregistering the machine. - - :param timeout: Integer value, representing seconds with a default - value of 600, which controls the maximum amount of - time to block the method's completion on. - - :raises: OpenStackCloudException on operation failure. - """ - - machine = self.get_machine(uuid) - invalid_states = ['active', 'cleaning', 'clean wait', 'clean failed'] - if machine['provision_state'] in invalid_states: - raise exc.OpenStackCloudException( - "Error unregistering node '%s' due to current provision " - "state '%s'" % (uuid, machine['provision_state'])) - - # NOTE(TheJulia) There is a high possibility of a lock being present - # if the machine was just moved through the state machine. This was - # previously concealed by exception retry logic that detected the - # failure, and resubitted the request in python-ironicclient. - try: - self.wait_for_baremetal_node_lock(machine, timeout=timeout) - except exc.OpenStackCloudException as e: - raise exc.OpenStackCloudException( - "Error unregistering node '%s': Exception occured while" - " waiting to be able to proceed: %s" % (machine['uuid'], e)) - - for nic in nics: - port_msg = ("Error removing NIC {nic} from baremetal API for " - "node {uuid}").format(nic=nic, uuid=uuid) - port_url = '/ports/detail?address={mac}'.format(mac=nic['mac']) - port = self._baremetal_client.get(port_url, microversion=1.6, - error_message=port_msg) - port_url = '/ports/{uuid}'.format(uuid=port['ports'][0]['uuid']) - _utils._call_client_and_retry(self._baremetal_client.delete, - port_url, retry_on=[409, 503], - error_message=port_msg) - - with _utils.shade_exceptions( - "Error unregistering machine {node_id} from the baremetal " - "API".format(node_id=uuid)): - - # NOTE(TheJulia): While this should not matter microversion wise, - # ironic assumes all calls without an explicit microversion to be - # version 1.0. Ironic expects to deprecate support for older - # microversions in future releases, as such, we explicitly set - # the version to what we have been using with the client library.. - version = "1.6" - msg = "Baremetal machine failed to be deleted" - url = '/nodes/{node_id}'.format( - node_id=uuid) - _utils._call_client_and_retry(self._baremetal_client.delete, - url, retry_on=[409, 503], - error_message=msg, - microversion=version) - - if wait: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for machine to be deleted"): - if not self.get_machine(uuid): - break - - def patch_machine(self, name_or_id, patch): - """Patch Machine Information - - This method allows for an interface to manipulate node entries - within Ironic. - - :param string name_or_id: A machine name or UUID to be updated. - :param patch: - The JSON Patch document is a list of dictonary objects - that comply with RFC 6902 which can be found at - https://tools.ietf.org/html/rfc6902. - - Example patch construction:: - - patch=[] - patch.append({ - 'op': 'remove', - 'path': '/instance_info' - }) - patch.append({ - 'op': 'replace', - 'path': '/name', - 'value': 'newname' - }) - patch.append({ - 'op': 'add', - 'path': '/driver_info/username', - 'value': 'administrator' - }) - - :raises: OpenStackCloudException on operation error. - - :returns: ``munch.Munch`` representing the newly updated node. - """ - node = self.baremetal.get_node(name_or_id) - microversion = node._get_microversion_for(self._baremetal_client, - 'commit') - msg = ("Error updating machine via patch operation on node " - "{node}".format(node=name_or_id)) - url = '/nodes/{node_id}'.format(node_id=node.id) - return self._normalize_machine( - self._baremetal_client.patch(url, - json=patch, - microversion=microversion, - error_message=msg)) - - def update_machine(self, name_or_id, **attrs): - """Update a machine with new configuration information - - A user-friendly method to perform updates of a machine, in whole or - part. - - :param string name_or_id: A machine name or UUID to be updated. - :param attrs: Attributes to updated on the machine. - - :raises: OpenStackCloudException on operation error. - - :returns: ``munch.Munch`` containing a machine sub-dictonary consisting - of the updated data returned from the API update operation, - and a list named changes which contains all of the API paths - that received updates. - """ - machine = self.get_machine(name_or_id) - if not machine: - raise exc.OpenStackCloudException( - "Machine update failed to find Machine: %s. " % name_or_id) - - new_config = dict(machine, **attrs) - - try: - patch = jsonpatch.JsonPatch.from_diff(machine, new_config) - except Exception as e: - raise exc.OpenStackCloudException( - "Machine update failed - Error generating JSON patch object " - "for submission to the API. Machine: %s Error: %s" - % (name_or_id, e)) - - if not patch: - return dict( - node=machine, - changes=None - ) - - change_list = [change['path'] for change in patch] - node = self.baremetal.update_node(machine, **attrs) - return dict( - node=self._normalize_machine(node._to_munch()), - changes=change_list - ) - - def attach_port_to_machine(self, name_or_id, port_name_or_id): - """Attach a virtual port to the bare metal machine. - - :param string name_or_id: A machine name or UUID. - :param string port_name_or_id: A port name or UUID. - Note that this is a Network service port, not a bare metal NIC. - :return: Nothing. - """ - machine = self.get_machine(name_or_id) - port = self.get_port(port_name_or_id) - self.baremetal.attach_vif_to_node(machine, port['id']) - - def detach_port_from_machine(self, name_or_id, port_name_or_id): - """Detach a virtual port from the bare metal machine. - - :param string name_or_id: A machine name or UUID. - :param string port_name_or_id: A port name or UUID. - Note that this is a Network service port, not a bare metal NIC. - :return: Nothing. - """ - machine = self.get_machine(name_or_id) - port = self.get_port(port_name_or_id) - self.baremetal.detach_vif_from_node(machine, port['id']) - - def list_ports_attached_to_machine(self, name_or_id): - """List virtual ports attached to the bare metal machine. - - :param string name_or_id: A machine name or UUID. - :returns: List of ``munch.Munch`` representing the ports. - """ - machine = self.get_machine(name_or_id) - vif_ids = self.baremetal.list_node_vifs(machine) - return [self.get_port(vif) for vif in vif_ids] - - def validate_machine(self, name_or_id, for_deploy=True): - """Validate parameters of the machine. - - :param string name_or_id: The Name or UUID value representing the - baremetal node. - :param bool for_deploy: If ``True``, validate readiness for deployment, - otherwise validate only the power management - properties. - :raises: :exc:`~openstack.exceptions.ValidationException` - """ - if for_deploy: - ifaces = ('boot', 'deploy', 'management', 'power') - else: - ifaces = ('power',) - self.baremetal.validate_node(name_or_id, required=ifaces) - - def validate_node(self, uuid): - warnings.warn('validate_node is deprecated, please use ' - 'validate_machine instead', DeprecationWarning) - self.baremetal.validate_node(uuid) - - def node_set_provision_state(self, - name_or_id, - state, - configdrive=None, - wait=False, - timeout=3600): - """Set Node Provision State - - Enables a user to provision a Machine and optionally define a - config drive to be utilized. - - :param string name_or_id: The Name or UUID value representing the - baremetal node. - :param string state: The desired provision state for the - baremetal node. - :param string configdrive: An optional URL or file or path - representing the configdrive. In the - case of a directory, the client API - will create a properly formatted - configuration drive file and post the - file contents to the API for - deployment. - :param boolean wait: A boolean value, defaulted to false, to control - if the method will wait for the desire end state - to be reached before returning. - :param integer timeout: Integer value, defaulting to 3600 seconds, - representing the amount of time to wait for - the desire end state to be reached. - - :raises: OpenStackCloudException on operation error. - - :returns: ``munch.Munch`` representing the current state of the machine - upon exit of the method. - """ - node = self.baremetal.set_node_provision_state( - name_or_id, target=state, config_drive=configdrive, - wait=wait, timeout=timeout) - return node._to_munch() - - def set_machine_maintenance_state( - self, - name_or_id, - state=True, - reason=None): - """Set Baremetal Machine Maintenance State - - Sets Baremetal maintenance state and maintenance reason. - - :param string name_or_id: The Name or UUID value representing the - baremetal node. - :param boolean state: The desired state of the node. True being in - maintenance where as False means the machine - is not in maintenance mode. This value - defaults to True if not explicitly set. - :param string reason: An optional freeform string that is supplied to - the baremetal API to allow for notation as to why - the node is in maintenance state. - - :raises: OpenStackCloudException on operation error. - - :returns: None - """ - if state: - self.baremetal.set_node_maintenance(name_or_id, reason) - else: - self.baremetal.unset_node_maintenance(name_or_id) - - def remove_machine_from_maintenance(self, name_or_id): - """Remove Baremetal Machine from Maintenance State - - Similarly to set_machine_maintenance_state, this method - removes a machine from maintenance state. It must be noted - that this method simpily calls set_machine_maintenace_state - for the name_or_id requested and sets the state to False. - - :param string name_or_id: The Name or UUID value representing the - baremetal node. - - :raises: OpenStackCloudException on operation error. - - :returns: None - """ - self.baremetal.unset_node_maintenance(name_or_id) - - def set_machine_power_on(self, name_or_id): - """Activate baremetal machine power - - This is a method that sets the node power state to "on". - - :params string name_or_id: A string representing the baremetal - node to have power turned to an "on" - state. - - :raises: OpenStackCloudException on operation error. - - :returns: None - """ - self.baremetal.set_node_power_state(name_or_id, 'power on') - - def set_machine_power_off(self, name_or_id): - """De-activate baremetal machine power - - This is a method that sets the node power state to "off". - - :params string name_or_id: A string representing the baremetal - node to have power turned to an "off" - state. - - :raises: OpenStackCloudException on operation error. - - :returns: - """ - self.baremetal.set_node_power_state(name_or_id, 'power off') - - def set_machine_power_reboot(self, name_or_id): - """De-activate baremetal machine power - - This is a method that sets the node power state to "reboot", which - in essence changes the machine power state to "off", and that back - to "on". - - :params string name_or_id: A string representing the baremetal - node to have power turned to an "off" - state. - - :raises: OpenStackCloudException on operation error. - - :returns: None - """ - self.baremetal.set_node_power_state(name_or_id, 'rebooting') - - def activate_node(self, uuid, configdrive=None, - wait=False, timeout=1200): - self.node_set_provision_state( - uuid, 'active', configdrive, wait=wait, timeout=timeout) - - def deactivate_node(self, uuid, wait=False, - timeout=1200): - self.node_set_provision_state( - uuid, 'deleted', wait=wait, timeout=timeout) - - def set_node_instance_info(self, uuid, patch): - msg = ("Error updating machine via patch operation on node " - "{node}".format(node=uuid)) - url = '/nodes/{node_id}'.format(node_id=uuid) - return self._baremetal_client.patch(url, - json=patch, - error_message=msg) - - def purge_node_instance_info(self, uuid): - patch = [] - patch.append({'op': 'remove', 'path': '/instance_info'}) - msg = ("Error updating machine via patch operation on node " - "{node}".format(node=uuid)) - url = '/nodes/{node_id}'.format(node_id=uuid) - return self._baremetal_client.patch(url, - json=patch, - error_message=msg) - - def wait_for_baremetal_node_lock(self, node, timeout=30): - """Wait for a baremetal node to have no lock. - - DEPRECATED, use ``wait_for_node_reservation`` on the `baremetal` proxy. - - :raises: OpenStackCloudException upon client failure. - :returns: None - """ - warnings.warn("The wait_for_baremetal_node_lock call is deprecated " - "in favor of wait_for_node_reservation on the baremetal " - "proxy", DeprecationWarning) - self.baremetal.wait_for_node_reservation(node, timeout) - - @_utils.valid_kwargs('type', 'service_type', 'description') - def create_service(self, name, enabled=True, **kwargs): - """Create a service. - - :param name: Service name. - :param type: Service type. (type or service_type required.) - :param service_type: Service type. (type or service_type required.) - :param description: Service description (optional). - :param enabled: Whether the service is enabled (v3 only) - - :returns: a ``munch.Munch`` containing the services description, - i.e. the following attributes:: - - id: - - name: - - type: - - service_type: - - description: - - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call. - - """ - type_ = kwargs.pop('type', None) - service_type = kwargs.pop('service_type', None) - - # TODO(mordred) When this changes to REST, force interface=admin - # in the adapter call - if self._is_client_version('identity', 2): - url, key = '/OS-KSADM/services', 'OS-KSADM:service' - kwargs['type'] = type_ or service_type - else: - url, key = '/services', 'service' - kwargs['type'] = type_ or service_type - kwargs['enabled'] = enabled - kwargs['name'] = name - - msg = 'Failed to create service {name}'.format(name=name) - data = self._identity_client.post( - url, json={key: kwargs}, error_message=msg) - service = self._get_and_munchify(key, data) - return _utils.normalize_keystone_services([service])[0] - - @_utils.valid_kwargs('name', 'enabled', 'type', 'service_type', - 'description') - def update_service(self, name_or_id, **kwargs): - # NOTE(SamYaple): Service updates are only available on v3 api - if self._is_client_version('identity', 2): - raise exc.OpenStackCloudUnavailableFeature( - 'Unavailable Feature: Service update requires Identity v3' - ) - - # NOTE(SamYaple): Keystone v3 only accepts 'type' but shade accepts - # both 'type' and 'service_type' with a preference - # towards 'type' - type_ = kwargs.pop('type', None) - service_type = kwargs.pop('service_type', None) - if type_ or service_type: - kwargs['type'] = type_ or service_type - - if self._is_client_version('identity', 2): - url, key = '/OS-KSADM/services', 'OS-KSADM:service' - else: - url, key = '/services', 'service' - - service = self.get_service(name_or_id) - msg = 'Error in updating service {service}'.format(service=name_or_id) - data = self._identity_client.patch( - '{url}/{id}'.format(url=url, id=service['id']), json={key: kwargs}, - error_message=msg) - service = self._get_and_munchify(key, data) - return _utils.normalize_keystone_services([service])[0] - - def list_services(self): - """List all Keystone services. - - :returns: a list of ``munch.Munch`` containing the services description - - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call. - """ - if self._is_client_version('identity', 2): - url, key = '/OS-KSADM/services', 'OS-KSADM:services' - endpoint_filter = {'interface': 'admin'} - else: - url, key = '/services', 'services' - endpoint_filter = {} - - data = self._identity_client.get( - url, endpoint_filter=endpoint_filter, - error_message="Failed to list services") - services = self._get_and_munchify(key, data) - return _utils.normalize_keystone_services(services) - - def search_services(self, name_or_id=None, filters=None): - """Search Keystone services. - - :param name_or_id: Name or id of the desired service. - :param filters: a dict containing additional filters to use. e.g. - {'type': 'network'}. - - :returns: a list of ``munch.Munch`` containing the services description - - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call. - """ - services = self.list_services() - return _utils._filter_list(services, name_or_id, filters) - - def get_service(self, name_or_id, filters=None): - """Get exactly one Keystone service. - - :param name_or_id: Name or id of the desired service. - :param filters: a dict containing additional filters to use. e.g. - {'type': 'network'} - - :returns: a ``munch.Munch`` containing the services description, - i.e. the following attributes:: - - id: - - name: - - type: - - description: - - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call or if multiple matches are found. - """ - return _utils._get_entity(self, 'service', name_or_id, filters) - - def delete_service(self, name_or_id): - """Delete a Keystone service. - - :param name_or_id: Service name or id. - - :returns: True if delete succeeded, False otherwise. - - :raises: ``OpenStackCloudException`` if something goes wrong during - the OpenStack API call - """ - service = self.get_service(name_or_id=name_or_id) - if service is None: - self.log.debug("Service %s not found for deleting", name_or_id) - return False - - if self._is_client_version('identity', 2): - url = '/OS-KSADM/services' - endpoint_filter = {'interface': 'admin'} - else: - url = '/services' - endpoint_filter = {} - - error_msg = 'Failed to delete service {id}'.format(id=service['id']) - self._identity_client.delete( - '{url}/{id}'.format(url=url, id=service['id']), - endpoint_filter=endpoint_filter, error_message=error_msg) - - return True - - @_utils.valid_kwargs('public_url', 'internal_url', 'admin_url') - def create_endpoint(self, service_name_or_id, url=None, interface=None, - region=None, enabled=True, **kwargs): - """Create a Keystone endpoint. - - :param service_name_or_id: Service name or id for this endpoint. - :param url: URL of the endpoint - :param interface: Interface type of the endpoint - :param public_url: Endpoint public URL. - :param internal_url: Endpoint internal URL. - :param admin_url: Endpoint admin URL. - :param region: Endpoint region. - :param enabled: Whether the endpoint is enabled - - NOTE: Both v2 (public_url, internal_url, admin_url) and v3 - (url, interface) calling semantics are supported. But - you can only use one of them at a time. - - :returns: a list of ``munch.Munch`` containing the endpoint description - - :raises: OpenStackCloudException if the service cannot be found or if - something goes wrong during the OpenStack API call. - """ - public_url = kwargs.pop('public_url', None) - internal_url = kwargs.pop('internal_url', None) - admin_url = kwargs.pop('admin_url', None) - - if (url or interface) and (public_url or internal_url or admin_url): - raise exc.OpenStackCloudException( - "create_endpoint takes either url and interface OR" - " public_url, internal_url, admin_url") - - service = self.get_service(name_or_id=service_name_or_id) - if service is None: - raise exc.OpenStackCloudException( - "service {service} not found".format( - service=service_name_or_id)) - - if self._is_client_version('identity', 2): - if url: - # v2.0 in use, v3-like arguments, one endpoint created - if interface != 'public': - raise exc.OpenStackCloudException( - "Error adding endpoint for service {service}." - " On a v2 cloud the url/interface API may only be" - " used for public url. Try using the public_url," - " internal_url, admin_url parameters instead of" - " url and interface".format( - service=service_name_or_id)) - endpoint_args = {'publicurl': url} - else: - # v2.0 in use, v2.0-like arguments, one endpoint created - endpoint_args = {} - if public_url: - endpoint_args.update({'publicurl': public_url}) - if internal_url: - endpoint_args.update({'internalurl': internal_url}) - if admin_url: - endpoint_args.update({'adminurl': admin_url}) - - # keystone v2.0 requires 'region' arg even if it is None - endpoint_args.update( - {'service_id': service['id'], 'region': region}) - - data = self._identity_client.post( - '/endpoints', json={'endpoint': endpoint_args}, - endpoint_filter={'interface': 'admin'}, - error_message=("Failed to create endpoint for service" - " {service}".format(service=service['name']))) - return [self._get_and_munchify('endpoint', data)] - else: - endpoints_args = [] - if url: - # v3 in use, v3-like arguments, one endpoint created - endpoints_args.append( - {'url': url, 'interface': interface, - 'service_id': service['id'], 'enabled': enabled, - 'region': region}) - else: - # v3 in use, v2.0-like arguments, one endpoint created for each - # interface url provided - endpoint_args = {'region': region, 'enabled': enabled, - 'service_id': service['id']} - if public_url: - endpoint_args.update({'url': public_url, - 'interface': 'public'}) - endpoints_args.append(endpoint_args.copy()) - if internal_url: - endpoint_args.update({'url': internal_url, - 'interface': 'internal'}) - endpoints_args.append(endpoint_args.copy()) - if admin_url: - endpoint_args.update({'url': admin_url, - 'interface': 'admin'}) - endpoints_args.append(endpoint_args.copy()) - - endpoints = [] - error_msg = ("Failed to create endpoint for service" - " {service}".format(service=service['name'])) - for args in endpoints_args: - data = self._identity_client.post( - '/endpoints', json={'endpoint': args}, - error_message=error_msg) - endpoints.append(self._get_and_munchify('endpoint', data)) - return endpoints - - @_utils.valid_kwargs('enabled', 'service_name_or_id', 'url', 'interface', - 'region') - def update_endpoint(self, endpoint_id, **kwargs): - # NOTE(SamYaple): Endpoint updates are only available on v3 api - if self._is_client_version('identity', 2): - raise exc.OpenStackCloudUnavailableFeature( - 'Unavailable Feature: Endpoint update' - ) - - service_name_or_id = kwargs.pop('service_name_or_id', None) - if service_name_or_id is not None: - kwargs['service_id'] = service_name_or_id - - data = self._identity_client.patch( - '/endpoints/{}'.format(endpoint_id), json={'endpoint': kwargs}, - error_message="Failed to update endpoint {}".format(endpoint_id)) - return self._get_and_munchify('endpoint', data) - - def list_endpoints(self): - """List Keystone endpoints. - - :returns: a list of ``munch.Munch`` containing the endpoint description - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. - """ - # Force admin interface if v2.0 is in use - v2 = self._is_client_version('identity', 2) - kwargs = {'endpoint_filter': {'interface': 'admin'}} if v2 else {} - - data = self._identity_client.get( - '/endpoints', error_message="Failed to list endpoints", **kwargs) - endpoints = self._get_and_munchify('endpoints', data) - - return endpoints - - def search_endpoints(self, id=None, filters=None): - """List Keystone endpoints. - - :param id: endpoint id. - :param filters: a dict containing additional filters to use. e.g. - {'region': 'region-a.geo-1'} - - :returns: a list of ``munch.Munch`` containing the endpoint - description. Each dict contains the following attributes:: - - id: - - region: - - public_url: - - internal_url: (optional) - - admin_url: (optional) - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. - """ - # NOTE(SamYaple): With keystone v3 we can filter directly via the - # the keystone api, but since the return of all the endpoints even in - # large environments is small, we can continue to filter in shade just - # like the v2 api. - endpoints = self.list_endpoints() - return _utils._filter_list(endpoints, id, filters) - - def get_endpoint(self, id, filters=None): - """Get exactly one Keystone endpoint. - - :param id: endpoint id. - :param filters: a dict containing additional filters to use. e.g. - {'region': 'region-a.geo-1'} - - :returns: a ``munch.Munch`` containing the endpoint description. - i.e. a ``munch.Munch`` containing the following attributes:: - - id: - - region: - - public_url: - - internal_url: (optional) - - admin_url: (optional) - """ - return _utils._get_entity(self, 'endpoint', id, filters) - - def delete_endpoint(self, id): - """Delete a Keystone endpoint. - - :param id: Id of the endpoint to delete. - - :returns: True if delete succeeded, False otherwise. - - :raises: ``OpenStackCloudException`` if something goes wrong during - the OpenStack API call. - """ - endpoint = self.get_endpoint(id=id) - if endpoint is None: - self.log.debug("Endpoint %s not found for deleting", id) - return False - - # Force admin interface if v2.0 is in use - v2 = self._is_client_version('identity', 2) - kwargs = {'endpoint_filter': {'interface': 'admin'}} if v2 else {} - - error_msg = "Failed to delete endpoint {id}".format(id=id) - self._identity_client.delete('/endpoints/{id}'.format(id=id), - error_message=error_msg, **kwargs) - - return True - - def create_domain(self, name, description=None, enabled=True): - """Create a domain. - - :param name: The name of the domain. - :param description: A description of the domain. - :param enabled: Is the domain enabled or not (default True). - - :returns: a ``munch.Munch`` containing the domain representation. - - :raise OpenStackCloudException: if the domain cannot be created. - """ - domain_ref = {'name': name, 'enabled': enabled} - if description is not None: - domain_ref['description'] = description - msg = 'Failed to create domain {name}'.format(name=name) - data = self._identity_client.post( - '/domains', json={'domain': domain_ref}, error_message=msg) - domain = self._get_and_munchify('domain', data) - return _utils.normalize_domains([domain])[0] - - def update_domain( - self, domain_id=None, name=None, description=None, - enabled=None, name_or_id=None): - if domain_id is None: - if name_or_id is None: - raise exc.OpenStackCloudException( - "You must pass either domain_id or name_or_id value" - ) - dom = self.get_domain(None, name_or_id) - if dom is None: - raise exc.OpenStackCloudException( - "Domain {0} not found for updating".format(name_or_id) - ) - domain_id = dom['id'] - - domain_ref = {} - domain_ref.update({'name': name} if name else {}) - domain_ref.update({'description': description} if description else {}) - domain_ref.update({'enabled': enabled} if enabled is not None else {}) - - error_msg = "Error in updating domain {id}".format(id=domain_id) - data = self._identity_client.patch( - '/domains/{id}'.format(id=domain_id), - json={'domain': domain_ref}, error_message=error_msg) - domain = self._get_and_munchify('domain', data) - return _utils.normalize_domains([domain])[0] - - def delete_domain(self, domain_id=None, name_or_id=None): - """Delete a domain. - - :param domain_id: ID of the domain to delete. - :param name_or_id: Name or ID of the domain to delete. - - :returns: True if delete succeeded, False otherwise. - - :raises: ``OpenStackCloudException`` if something goes wrong during - the OpenStack API call. - """ - if domain_id is None: - if name_or_id is None: - raise exc.OpenStackCloudException( - "You must pass either domain_id or name_or_id value" - ) - dom = self.get_domain(name_or_id=name_or_id) - if dom is None: - self.log.debug( - "Domain %s not found for deleting", name_or_id) - return False - domain_id = dom['id'] - - # A domain must be disabled before deleting - self.update_domain(domain_id, enabled=False) - error_msg = "Failed to delete domain {id}".format(id=domain_id) - self._identity_client.delete('/domains/{id}'.format(id=domain_id), - error_message=error_msg) - - return True - - def list_domains(self, **filters): - """List Keystone domains. - - :returns: a list of ``munch.Munch`` containing the domain description. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. - """ - data = self._identity_client.get( - '/domains', params=filters, error_message="Failed to list domains") - domains = self._get_and_munchify('domains', data) - return _utils.normalize_domains(domains) - - def search_domains(self, filters=None, name_or_id=None): - """Search Keystone domains. - - :param name_or_id: domain name or id - :param dict filters: A dict containing additional filters to use. - Keys to search on are id, name, enabled and description. - - :returns: a list of ``munch.Munch`` containing the domain description. - Each ``munch.Munch`` contains the following attributes:: - - id: - - name: - - description: - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. - """ - if filters is None: - filters = {} - if name_or_id is not None: - domains = self.list_domains() - return _utils._filter_list(domains, name_or_id, filters) - else: - return self.list_domains(**filters) - - def get_domain(self, domain_id=None, name_or_id=None, filters=None): - """Get exactly one Keystone domain. - - :param domain_id: domain id. - :param name_or_id: domain name or id. - :param dict filters: A dict containing additional filters to use. - Keys to search on are id, name, enabled and description. - - :returns: a ``munch.Munch`` containing the domain description, or None - if not found. Each ``munch.Munch`` contains the following - attributes:: - - id: - - name: - - description: - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. - """ - if domain_id is None: - # NOTE(SamYaple): search_domains() has filters and name_or_id - # in the wrong positional order which prevents _get_entity from - # being able to return quickly if passing a domain object so we - # duplicate that logic here - if hasattr(name_or_id, 'id'): - return name_or_id - return _utils._get_entity(self, 'domain', filters, name_or_id) - else: - error_msg = 'Failed to get domain {id}'.format(id=domain_id) - data = self._identity_client.get( - '/domains/{id}'.format(id=domain_id), - error_message=error_msg) - domain = self._get_and_munchify('domain', data) - return _utils.normalize_domains([domain])[0] - - @_utils.valid_kwargs('domain_id') - @_utils.cache_on_arguments() - def list_groups(self, **kwargs): - """List Keystone Groups. - - :param domain_id: domain id. - - :returns: A list of ``munch.Munch`` containing the group description. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. - """ - data = self._identity_client.get( - '/groups', params=kwargs, error_message="Failed to list groups") - return _utils.normalize_groups(self._get_and_munchify('groups', data)) - - @_utils.valid_kwargs('domain_id') - def search_groups(self, name_or_id=None, filters=None, **kwargs): - """Search Keystone groups. - - :param name: Group name or id. - :param filters: A dict containing additional filters to use. - :param domain_id: domain id. - - :returns: A list of ``munch.Munch`` containing the group description. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. - """ - groups = self.list_groups(**kwargs) - return _utils._filter_list(groups, name_or_id, filters) - - @_utils.valid_kwargs('domain_id') - def get_group(self, name_or_id, filters=None, **kwargs): - """Get exactly one Keystone group. - - :param id: Group name or id. - :param filters: A dict containing additional filters to use. - :param domain_id: domain id. - - :returns: A ``munch.Munch`` containing the group description. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. - """ - return _utils._get_entity(self, 'group', name_or_id, filters, **kwargs) - - def create_group(self, name, description, domain=None): - """Create a group. - - :param string name: Group name. - :param string description: Group description. - :param string domain: Domain name or ID for the group. - - :returns: A ``munch.Munch`` containing the group description. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. - """ - group_ref = {'name': name} - if description: - group_ref['description'] = description - if domain: - dom = self.get_domain(domain) - if not dom: - raise exc.OpenStackCloudException( - "Creating group {group} failed: Invalid domain " - "{domain}".format(group=name, domain=domain) - ) - group_ref['domain_id'] = dom['id'] - - error_msg = "Error creating group {group}".format(group=name) - data = self._identity_client.post( - '/groups', json={'group': group_ref}, error_message=error_msg) - group = self._get_and_munchify('group', data) - self.list_groups.invalidate(self) - return _utils.normalize_groups([group])[0] - - @_utils.valid_kwargs('domain_id') - def update_group(self, name_or_id, name=None, description=None, - **kwargs): - """Update an existing group - - :param string name: New group name. - :param string description: New group description. - :param domain_id: domain id. - - :returns: A ``munch.Munch`` containing the group description. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. - """ - self.list_groups.invalidate(self) - group = self.get_group(name_or_id, **kwargs) - if group is None: - raise exc.OpenStackCloudException( - "Group {0} not found for updating".format(name_or_id) - ) - - group_ref = {} - if name: - group_ref['name'] = name - if description: - group_ref['description'] = description - - error_msg = "Unable to update group {name}".format(name=name_or_id) - data = self._identity_client.patch( - '/groups/{id}'.format(id=group['id']), - json={'group': group_ref}, error_message=error_msg) - group = self._get_and_munchify('group', data) - self.list_groups.invalidate(self) - return _utils.normalize_groups([group])[0] - - @_utils.valid_kwargs('domain_id') - def delete_group(self, name_or_id, **kwargs): - """Delete a group - - :param name_or_id: ID or name of the group to delete. - :param domain_id: domain id. - - :returns: True if delete succeeded, False otherwise. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. - """ - group = self.get_group(name_or_id, **kwargs) - if group is None: - self.log.debug( - "Group %s not found for deleting", name_or_id) - return False - - error_msg = "Unable to delete group {name}".format(name=name_or_id) - self._identity_client.delete('/groups/{id}'.format(id=group['id']), - error_message=error_msg) - - self.list_groups.invalidate(self) - return True - - @_utils.valid_kwargs('domain_id') - def list_roles(self, **kwargs): - """List Keystone roles. - - :param domain_id: domain id for listing roles (v3) - - :returns: a list of ``munch.Munch`` containing the role description. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. - """ - v2 = self._is_client_version('identity', 2) - url = '/OS-KSADM/roles' if v2 else '/roles' - data = self._identity_client.get( - url, params=kwargs, error_message="Failed to list roles") - return self._normalize_roles(self._get_and_munchify('roles', data)) - - @_utils.valid_kwargs('domain_id') - def search_roles(self, name_or_id=None, filters=None, **kwargs): - """Seach Keystone roles. - - :param string name: role name or id. - :param dict filters: a dict containing additional filters to use. - :param domain_id: domain id (v3) - - :returns: a list of ``munch.Munch`` containing the role description. - Each ``munch.Munch`` contains the following attributes:: - - - id: - - name: - - description: - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. - """ - roles = self.list_roles(**kwargs) - return _utils._filter_list(roles, name_or_id, filters) - - @_utils.valid_kwargs('domain_id') - def get_role(self, name_or_id, filters=None, **kwargs): - """Get exactly one Keystone role. - - :param id: role name or id. - :param filters: a dict containing additional filters to use. - :param domain_id: domain id (v3) - - :returns: a single ``munch.Munch`` containing the role description. - Each ``munch.Munch`` contains the following attributes:: - - - id: - - name: - - description: - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. - """ - return _utils._get_entity(self, 'role', name_or_id, filters, **kwargs) - - def _keystone_v2_role_assignments(self, user, project=None, - role=None, **kwargs): - data = self._identity_client.get( - "/tenants/{tenant}/users/{user}/roles".format( - tenant=project, user=user), - error_message="Failed to list role assignments") - - roles = self._get_and_munchify('roles', data) - - ret = [] - for tmprole in roles: - if role is not None and role != tmprole.id: - continue - ret.append({ - 'role': { - 'id': tmprole.id - }, - 'scope': { - 'project': { - 'id': project, - } - }, - 'user': { - 'id': user, - } - }) - return ret - - def _keystone_v3_role_assignments(self, **filters): - # NOTE(samueldmq): different parameters have different representation - # patterns as query parameters in the call to the list role assignments - # API. The code below handles each set of patterns separately and - # renames the parameters names accordingly, ignoring 'effective', - # 'include_names' and 'include_subtree' whose do not need any renaming. - for k in ('group', 'role', 'user'): - if k in filters: - filters[k + '.id'] = filters[k] - del filters[k] - for k in ('project', 'domain'): - if k in filters: - filters['scope.' + k + '.id'] = filters[k] - del filters[k] - if 'os_inherit_extension_inherited_to' in filters: - filters['scope.OS-INHERIT:inherited_to'] = ( - filters['os_inherit_extension_inherited_to']) - del filters['os_inherit_extension_inherited_to'] - - data = self._identity_client.get( - '/role_assignments', params=filters, - error_message="Failed to list role assignments") - return self._get_and_munchify('role_assignments', data) - - def list_role_assignments(self, filters=None): - """List Keystone role assignments - - :param dict filters: Dict of filter conditions. Acceptable keys are: - - * 'user' (string) - User ID to be used as query filter. - * 'group' (string) - Group ID to be used as query filter. - * 'project' (string) - Project ID to be used as query filter. - * 'domain' (string) - Domain ID to be used as query filter. - * 'role' (string) - Role ID to be used as query filter. - * 'os_inherit_extension_inherited_to' (string) - Return inherited - role assignments for either 'projects' or 'domains' - * 'effective' (boolean) - Return effective role assignments. - * 'include_subtree' (boolean) - Include subtree - - 'user' and 'group' are mutually exclusive, as are 'domain' and - 'project'. - - NOTE: For keystone v2, only user, project, and role are used. - Project and user are both required in filters. - - :returns: a list of ``munch.Munch`` containing the role assignment - description. Contains the following attributes:: - - - id: - - user|group: - - project|domain: - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. - """ - # NOTE(samueldmq): although 'include_names' is a valid query parameter - # in the keystone v3 list role assignments API, it would have NO effect - # on shade due to normalization. It is not documented as an acceptable - # filter in the docs above per design! - - if not filters: - filters = {} - - # NOTE(samueldmq): the docs above say filters are *IDs*, though if - # munch.Munch objects are passed, this still works for backwards - # compatibility as keystoneclient allows either IDs or objects to be - # passed in. - # TODO(samueldmq): fix the docs above to advertise munch.Munch objects - # can be provided as parameters too - for k, v in filters.items(): - if isinstance(v, munch.Munch): - filters[k] = v['id'] - - if self._is_client_version('identity', 2): - if filters.get('project') is None or filters.get('user') is None: - raise exc.OpenStackCloudException( - "Must provide project and user for keystone v2" - ) - assignments = self._keystone_v2_role_assignments(**filters) - else: - assignments = self._keystone_v3_role_assignments(**filters) - - return _utils.normalize_role_assignments(assignments) - - def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", - ephemeral=0, swap=0, rxtx_factor=1.0, is_public=True): - """Create a new flavor. - - :param name: Descriptive name of the flavor - :param ram: Memory in MB for the flavor - :param vcpus: Number of VCPUs for the flavor - :param disk: Size of local disk in GB - :param flavorid: ID for the flavor (optional) - :param ephemeral: Ephemeral space size in GB - :param swap: Swap space in MB - :param rxtx_factor: RX/TX factor - :param is_public: Make flavor accessible to the public - - :returns: A ``munch.Munch`` describing the new flavor. - - :raises: OpenStackCloudException on operation error. - """ - with _utils.shade_exceptions("Failed to create flavor {name}".format( - name=name)): - payload = { - 'disk': disk, - 'OS-FLV-EXT-DATA:ephemeral': ephemeral, - 'id': flavorid, - 'os-flavor-access:is_public': is_public, - 'name': name, - 'ram': ram, - 'rxtx_factor': rxtx_factor, - 'swap': swap, - 'vcpus': vcpus, - } - if flavorid == 'auto': - payload['id'] = None - data = proxy._json_response(self.compute.post( - '/flavors', - json=dict(flavor=payload))) - - return self._normalize_flavor( - self._get_and_munchify('flavor', data)) - - def delete_flavor(self, name_or_id): - """Delete a flavor - - :param name_or_id: ID or name of the flavor to delete. - - :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. - """ - flavor = self.get_flavor(name_or_id, get_extra=False) - if flavor is None: - self.log.debug( - "Flavor %s not found for deleting", name_or_id) - return False - - proxy._json_response( - self.compute.delete( - '/flavors/{id}'.format(id=flavor['id'])), - error_message="Unable to delete flavor {name}".format( - name=name_or_id)) - - return True - - def set_flavor_specs(self, flavor_id, extra_specs): - """Add extra specs to a flavor - - :param string flavor_id: ID of the flavor to update. - :param dict extra_specs: Dictionary of key-value pairs. - - :raises: OpenStackCloudException on operation error. - :raises: OpenStackCloudResourceNotFound if flavor ID is not found. - """ - proxy._json_response( - self.compute.post( - "/flavors/{id}/os-extra_specs".format(id=flavor_id), - json=dict(extra_specs=extra_specs)), - error_message="Unable to set flavor specs") - - def unset_flavor_specs(self, flavor_id, keys): - """Delete extra specs from a flavor - - :param string flavor_id: ID of the flavor to update. - :param keys: List of spec keys to delete. - - :raises: OpenStackCloudException on operation error. - :raises: OpenStackCloudResourceNotFound if flavor ID is not found. - """ - for key in keys: - proxy._json_response( - self.compute.delete( - "/flavors/{id}/os-extra_specs/{key}".format( - id=flavor_id, key=key)), - error_message="Unable to delete flavor spec {0}".format(key)) - - def _mod_flavor_access(self, action, flavor_id, project_id): - """Common method for adding and removing flavor access - """ - with _utils.shade_exceptions("Error trying to {action} access from " - "flavor ID {flavor}".format( - action=action, flavor=flavor_id)): - endpoint = '/flavors/{id}/action'.format(id=flavor_id) - access = {'tenant': project_id} - access_key = '{action}TenantAccess'.format(action=action) - - proxy._json_response( - self.compute.post(endpoint, json={access_key: access})) - - def add_flavor_access(self, flavor_id, project_id): - """Grant access to a private flavor for a project/tenant. - - :param string flavor_id: ID of the private flavor. - :param string project_id: ID of the project/tenant. - - :raises: OpenStackCloudException on operation error. - """ - self._mod_flavor_access('add', flavor_id, project_id) - - def remove_flavor_access(self, flavor_id, project_id): - """Revoke access from a private flavor for a project/tenant. - - :param string flavor_id: ID of the private flavor. - :param string project_id: ID of the project/tenant. - - :raises: OpenStackCloudException on operation error. - """ - self._mod_flavor_access('remove', flavor_id, project_id) - - def list_flavor_access(self, flavor_id): - """List access from a private flavor for a project/tenant. - - :param string flavor_id: ID of the private flavor. - - :returns: a list of ``munch.Munch`` containing the access description - - :raises: OpenStackCloudException on operation error. - """ - data = proxy._json_response( - self.compute.get( - '/flavors/{id}/os-flavor-access'.format(id=flavor_id)), - error_message=( - "Error trying to list access from flavorID {flavor}".format( - flavor=flavor_id))) - return _utils.normalize_flavor_accesses( - self._get_and_munchify('flavor_access', data)) - - @_utils.valid_kwargs('domain_id') - def create_role(self, name, **kwargs): - """Create a Keystone role. - - :param string name: The name of the role. - :param domain_id: domain id (v3) - - :returns: a ``munch.Munch`` containing the role description - - :raise OpenStackCloudException: if the role cannot be created - """ - v2 = self._is_client_version('identity', 2) - url = '/OS-KSADM/roles' if v2 else '/roles' - kwargs['name'] = name - msg = 'Failed to create role {name}'.format(name=name) - data = self._identity_client.post( - url, json={'role': kwargs}, error_message=msg) - role = self._get_and_munchify('role', data) - return self._normalize_role(role) - - @_utils.valid_kwargs('domain_id') - def update_role(self, name_or_id, name, **kwargs): - """Update a Keystone role. - - :param name_or_id: Name or id of the role to update - :param string name: The new role name - :param domain_id: domain id - - :returns: a ``munch.Munch`` containing the role description - - :raise OpenStackCloudException: if the role cannot be created - """ - if self._is_client_version('identity', 2): - raise exc.OpenStackCloudUnavailableFeature( - 'Unavailable Feature: Role update requires Identity v3' - ) - kwargs['name_or_id'] = name_or_id - role = self.get_role(**kwargs) - if role is None: - self.log.debug( - "Role %s not found for updating", name_or_id) - return False - msg = 'Failed to update role {name}'.format(name=name_or_id) - json_kwargs = {'role_id': role.id, 'role': {'name': name}} - data = self._identity_client.patch('/roles', error_message=msg, - json=json_kwargs) - role = self._get_and_munchify('role', data) - return self._normalize_role(role) - - @_utils.valid_kwargs('domain_id') - def delete_role(self, name_or_id, **kwargs): - """Delete a Keystone role. - - :param string id: Name or id of the role to delete. - :param domain_id: domain id (v3) - - :returns: True if delete succeeded, False otherwise. - - :raises: ``OpenStackCloudException`` if something goes wrong during - the OpenStack API call. - """ - role = self.get_role(name_or_id, **kwargs) - if role is None: - self.log.debug( - "Role %s not found for deleting", name_or_id) - return False - - v2 = self._is_client_version('identity', 2) - url = '{preffix}/{id}'.format( - preffix='/OS-KSADM/roles' if v2 else '/roles', id=role['id']) - error_msg = "Unable to delete role {name}".format(name=name_or_id) - self._identity_client.delete(url, error_message=error_msg) - - return True - - def _get_grant_revoke_params(self, role, user=None, group=None, - project=None, domain=None): - role = self.get_role(role) - if role is None: - return {} - data = {'role': role.id} - - # domain and group not available in keystone v2.0 - is_keystone_v2 = self._is_client_version('identity', 2) - - filters = {} - if not is_keystone_v2 and domain: - filters['domain_id'] = data['domain'] = \ - self.get_domain(domain)['id'] - - if user: - if domain: - data['user'] = self.get_user(user, - domain_id=filters['domain_id'], - filters=filters) - else: - data['user'] = self.get_user(user, filters=filters) - - if project: - # drop domain in favor of project - data.pop('domain', None) - data['project'] = self.get_project(project, filters=filters) - - if not is_keystone_v2 and group: - data['group'] = self.get_group(group, filters=filters) - - return data - - def grant_role(self, name_or_id, user=None, group=None, - project=None, domain=None, wait=False, timeout=60): - """Grant a role to a user. - - :param string name_or_id: The name or id of the role. - :param string user: The name or id of the user. - :param string group: The name or id of the group. (v3) - :param string project: The name or id of the project. - :param string domain: The id of the domain. (v3) - :param bool wait: Wait for role to be granted - :param int timeout: Timeout to wait for role to be granted - - NOTE: domain is a required argument when the grant is on a project, - user or group specified by name. In that situation, they are all - considered to be in that domain. If different domains are in use - in the same role grant, it is required to specify those by ID. - - NOTE: for wait and timeout, sometimes granting roles is not - instantaneous. - - NOTE: project is required for keystone v2 - - :returns: True if the role is assigned, otherwise False - - :raise OpenStackCloudException: if the role cannot be granted - """ - data = self._get_grant_revoke_params(name_or_id, user, group, - project, domain) - filters = data.copy() - if not data: - raise exc.OpenStackCloudException( - 'Role {0} not found.'.format(name_or_id)) - - if data.get('user') is not None and data.get('group') is not None: - raise exc.OpenStackCloudException( - 'Specify either a group or a user, not both') - if data.get('user') is None and data.get('group') is None: - raise exc.OpenStackCloudException( - 'Must specify either a user or a group') - if self._is_client_version('identity', 2) and \ - data.get('project') is None: - raise exc.OpenStackCloudException( - 'Must specify project for keystone v2') - - if self.list_role_assignments(filters=filters): - self.log.debug('Assignment already exists') - return False - - error_msg = "Error granting access to role: {0}".format(data) - if self._is_client_version('identity', 2): - # For v2.0, only tenant/project assignment is supported - url = "/tenants/{t}/users/{u}/roles/OS-KSADM/{r}".format( - t=data['project']['id'], u=data['user']['id'], r=data['role']) - - self._identity_client.put(url, error_message=error_msg, - endpoint_filter={'interface': 'admin'}) - else: - if data.get('project') is None and data.get('domain') is None: - raise exc.OpenStackCloudException( - 'Must specify either a domain or project') - - # For v3, figure out the assignment type and build the URL - if data.get('domain'): - url = "/domains/{}".format(data['domain']) - else: - url = "/projects/{}".format(data['project']['id']) - if data.get('group'): - url += "/groups/{}".format(data['group']['id']) - else: - url += "/users/{}".format(data['user']['id']) - url += "/roles/{}".format(data.get('role')) - - self._identity_client.put(url, error_message=error_msg) - - if wait: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for role to be granted"): - if self.list_role_assignments(filters=filters): - break - return True - - def revoke_role(self, name_or_id, user=None, group=None, - project=None, domain=None, wait=False, timeout=60): - """Revoke a role from a user. - - :param string name_or_id: The name or id of the role. - :param string user: The name or id of the user. - :param string group: The name or id of the group. (v3) - :param string project: The name or id of the project. - :param string domain: The id of the domain. (v3) - :param bool wait: Wait for role to be revoked - :param int timeout: Timeout to wait for role to be revoked - - NOTE: for wait and timeout, sometimes revoking roles is not - instantaneous. - - NOTE: project is required for keystone v2 - - :returns: True if the role is revoke, otherwise False - - :raise OpenStackCloudException: if the role cannot be removed - """ - data = self._get_grant_revoke_params(name_or_id, user, group, - project, domain) - filters = data.copy() - - if not data: - raise exc.OpenStackCloudException( - 'Role {0} not found.'.format(name_or_id)) - - if data.get('user') is not None and data.get('group') is not None: - raise exc.OpenStackCloudException( - 'Specify either a group or a user, not both') - if data.get('user') is None and data.get('group') is None: - raise exc.OpenStackCloudException( - 'Must specify either a user or a group') - if self._is_client_version('identity', 2) and \ - data.get('project') is None: - raise exc.OpenStackCloudException( - 'Must specify project for keystone v2') - - if not self.list_role_assignments(filters=filters): - self.log.debug('Assignment does not exist') - return False - - error_msg = "Error revoking access to role: {0}".format(data) - if self._is_client_version('identity', 2): - # For v2.0, only tenant/project assignment is supported - url = "/tenants/{t}/users/{u}/roles/OS-KSADM/{r}".format( - t=data['project']['id'], u=data['user']['id'], r=data['role']) - - self._identity_client.delete( - url, error_message=error_msg, - endpoint_filter={'interface': 'admin'}) - else: - if data.get('project') is None and data.get('domain') is None: - raise exc.OpenStackCloudException( - 'Must specify either a domain or project') - - # For v3, figure out the assignment type and build the URL - if data.get('domain'): - url = "/domains/{}".format(data['domain']) - else: - url = "/projects/{}".format(data['project']['id']) - if data.get('group'): - url += "/groups/{}".format(data['group']['id']) - else: - url += "/users/{}".format(data['user']['id']) - url += "/roles/{}".format(data.get('role')) - - self._identity_client.delete(url, error_message=error_msg) - - if wait: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for role to be revoked"): - if not self.list_role_assignments(filters=filters): - break - return True - - def list_hypervisors(self): - """List all hypervisors - - :returns: A list of hypervisor ``munch.Munch``. - """ - - data = proxy._json_response( - self.compute.get('/os-hypervisors/detail'), - error_message="Error fetching hypervisor list") - return self._get_and_munchify('hypervisors', data) - - def search_aggregates(self, name_or_id=None, filters=None): - """Seach host aggregates. - - :param name: aggregate name or id. - :param filters: a dict containing additional filters to use. - - :returns: a list of dicts containing the aggregates - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. - """ - aggregates = self.list_aggregates() - return _utils._filter_list(aggregates, name_or_id, filters) - - def list_aggregates(self): - """List all available host aggregates. - - :returns: A list of aggregate dicts. - - """ - data = proxy._json_response( - self.compute.get('/os-aggregates'), - error_message="Error fetching aggregate list") - return self._get_and_munchify('aggregates', data) - - def get_aggregate(self, name_or_id, filters=None): - """Get an aggregate by name or ID. - - :param name_or_id: Name or ID of the aggregate. - :param dict filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'availability_zone': 'nova', - 'metadata': { - 'cpu_allocation_ratio': '1.0' - } - } - - :returns: An aggregate dict or None if no matching aggregate is - found. - - """ - return _utils._get_entity(self, 'aggregate', name_or_id, filters) - - def create_aggregate(self, name, availability_zone=None): - """Create a new host aggregate. - - :param name: Name of the host aggregate being created - :param availability_zone: Availability zone to assign hosts - - :returns: a dict representing the new host aggregate. - - :raises: OpenStackCloudException on operation error. - """ - data = proxy._json_response( - self.compute.post( - '/os-aggregates', - json={'aggregate': { - 'name': name, - 'availability_zone': availability_zone - }}), - error_message="Unable to create host aggregate {name}".format( - name=name)) - return self._get_and_munchify('aggregate', data) - - @_utils.valid_kwargs('name', 'availability_zone') - def update_aggregate(self, name_or_id, **kwargs): - """Update a host aggregate. - - :param name_or_id: Name or ID of the aggregate being updated. - :param name: New aggregate name - :param availability_zone: Availability zone to assign to hosts - - :returns: a dict representing the updated host aggregate. - - :raises: OpenStackCloudException on operation error. - """ - aggregate = self.get_aggregate(name_or_id) - if not aggregate: - raise exc.OpenStackCloudException( - "Host aggregate %s not found." % name_or_id) - - data = proxy._json_response( - self.compute.put( - '/os-aggregates/{id}'.format(id=aggregate['id']), - json={'aggregate': kwargs}), - error_message="Error updating aggregate {name}".format( - name=name_or_id)) - return self._get_and_munchify('aggregate', data) - - def delete_aggregate(self, name_or_id): - """Delete a host aggregate. - - :param name_or_id: Name or ID of the host aggregate to delete. - - :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. - """ - aggregate = self.get_aggregate(name_or_id) - if not aggregate: - self.log.debug("Aggregate %s not found for deleting", name_or_id) - return False - - return proxy._json_response( - self.compute.delete( - '/os-aggregates/{id}'.format(id=aggregate['id'])), - error_message="Error deleting aggregate {name}".format( - name=name_or_id)) - - return True - - def set_aggregate_metadata(self, name_or_id, metadata): - """Set aggregate metadata, replacing the existing metadata. - - :param name_or_id: Name of the host aggregate to update - :param metadata: Dict containing metadata to replace (Use - {'key': None} to remove a key) - - :returns: a dict representing the new host aggregate. - - :raises: OpenStackCloudException on operation error. - """ - aggregate = self.get_aggregate(name_or_id) - if not aggregate: - raise exc.OpenStackCloudException( - "Host aggregate %s not found." % name_or_id) - - err_msg = "Unable to set metadata for host aggregate {name}".format( - name=name_or_id) - - data = proxy._json_response( - self.compute.post( - '/os-aggregates/{id}/action'.format(id=aggregate['id']), - json={'set_metadata': {'metadata': metadata}}), - error_message=err_msg) - return self._get_and_munchify('aggregate', data) - - def add_host_to_aggregate(self, name_or_id, host_name): - """Add a host to an aggregate. - - :param name_or_id: Name or ID of the host aggregate. - :param host_name: Host to add. - - :raises: OpenStackCloudException on operation error. - """ - aggregate = self.get_aggregate(name_or_id) - if not aggregate: - raise exc.OpenStackCloudException( - "Host aggregate %s not found." % name_or_id) - - err_msg = "Unable to add host {host} to aggregate {name}".format( - host=host_name, name=name_or_id) - - return proxy._json_response( - self.compute.post( - '/os-aggregates/{id}/action'.format(id=aggregate['id']), - json={'add_host': {'host': host_name}}), - error_message=err_msg) - - def remove_host_from_aggregate(self, name_or_id, host_name): - """Remove a host from an aggregate. - - :param name_or_id: Name or ID of the host aggregate. - :param host_name: Host to remove. - - :raises: OpenStackCloudException on operation error. - """ - aggregate = self.get_aggregate(name_or_id) - if not aggregate: - raise exc.OpenStackCloudException( - "Host aggregate %s not found." % name_or_id) - - err_msg = "Unable to remove host {host} to aggregate {name}".format( - host=host_name, name=name_or_id) - - return proxy._json_response( - self.compute.post( - '/os-aggregates/{id}/action'.format(id=aggregate['id']), - json={'remove_host': {'host': host_name}}), - error_message=err_msg) - - def get_volume_type_access(self, name_or_id): - """Return a list of volume_type_access. - - :param name_or_id: Name or ID of the volume type. - - :raises: OpenStackCloudException on operation error. - """ - volume_type = self.get_volume_type(name_or_id) - if not volume_type: - raise exc.OpenStackCloudException( - "VolumeType not found: %s" % name_or_id) - - data = self._volume_client.get( - '/types/{id}/os-volume-type-access'.format(id=volume_type.id), - error_message="Unable to get volume type access" - " {name}".format(name=name_or_id)) - return self._normalize_volume_type_accesses( - self._get_and_munchify('volume_type_access', data)) - - def add_volume_type_access(self, name_or_id, project_id): - """Grant access on a volume_type to a project. - - :param name_or_id: ID or name of a volume_type - :param project_id: A project id - - NOTE: the call works even if the project does not exist. - - :raises: OpenStackCloudException on operation error. - """ - volume_type = self.get_volume_type(name_or_id) - if not volume_type: - raise exc.OpenStackCloudException( - "VolumeType not found: %s" % name_or_id) - with _utils.shade_exceptions(): - payload = {'project': project_id} - self._volume_client.post( - '/types/{id}/action'.format(id=volume_type.id), - json=dict(addProjectAccess=payload), - error_message="Unable to authorize {project} " - "to use volume type {name}".format( - name=name_or_id, project=project_id)) - - def remove_volume_type_access(self, name_or_id, project_id): - """Revoke access on a volume_type to a project. - - :param name_or_id: ID or name of a volume_type - :param project_id: A project id - - :raises: OpenStackCloudException on operation error. - """ - volume_type = self.get_volume_type(name_or_id) - if not volume_type: - raise exc.OpenStackCloudException( - "VolumeType not found: %s" % name_or_id) - with _utils.shade_exceptions(): - payload = {'project': project_id} - self._volume_client.post( - '/types/{id}/action'.format(id=volume_type.id), - json=dict(removeProjectAccess=payload), - error_message="Unable to revoke {project} " - "to use volume type {name}".format( - name=name_or_id, project=project_id)) - - def set_compute_quotas(self, name_or_id, **kwargs): - """ Set a quota in a project - - :param name_or_id: project name or id - :param kwargs: key/value pairs of quota name and quota value - - :raises: OpenStackCloudException if the resource to set the - quota does not exist. - """ - - proj = self.get_project(name_or_id) - if not proj: - raise exc.OpenStackCloudException("project does not exist") - - # compute_quotas = {key: val for key, val in kwargs.items() - # if key in quota.COMPUTE_QUOTAS} - # TODO(ghe): Manage volume and network quotas - # network_quotas = {key: val for key, val in kwargs.items() - # if key in quota.NETWORK_QUOTAS} - # volume_quotas = {key: val for key, val in kwargs.items() - # if key in quota.VOLUME_QUOTAS} - - kwargs['force'] = True - proxy._json_response( - self.compute.put( - '/os-quota-sets/{project}'.format(project=proj.id), - json={'quota_set': kwargs}), - error_message="No valid quota or resource") - - def get_compute_quotas(self, name_or_id): - """ Get quota for a project - - :param name_or_id: project name or id - :raises: OpenStackCloudException if it's not a valid project - - :returns: Munch object with the quotas - """ - proj = self.get_project(name_or_id) - if not proj: - raise exc.OpenStackCloudException("project does not exist") - data = proxy._json_response( - self.compute.get( - '/os-quota-sets/{project}'.format(project=proj.id))) - return self._get_and_munchify('quota_set', data) - - def delete_compute_quotas(self, name_or_id): - """ Delete quota for a project - - :param name_or_id: project name or id - :raises: OpenStackCloudException if it's not a valid project or the - nova client call failed - - :returns: dict with the quotas - """ - proj = self.get_project(name_or_id) - if not proj: - raise exc.OpenStackCloudException("project does not exist") - return proxy._json_response( - self.compute.delete( - '/os-quota-sets/{project}'.format(project=proj.id))) - - def get_compute_usage(self, name_or_id, start=None, end=None): - """ Get usage for a specific project - - :param name_or_id: project name or id - :param start: :class:`datetime.datetime` or string. Start date in UTC - Defaults to 2010-07-06T12:00:00Z (the date the OpenStack - project was started) - :param end: :class:`datetime.datetime` or string. End date in UTC. - Defaults to now - :raises: OpenStackCloudException if it's not a valid project - - :returns: Munch object with the usage - """ - - def parse_date(date): - try: - return iso8601.parse_date(date) - except iso8601.iso8601.ParseError: - # Yes. This is an exception mask. However,iso8601 is an - # implementation detail - and the error message is actually - # less informative. - raise exc.OpenStackCloudException( - "Date given, {date}, is invalid. Please pass in a date" - " string in ISO 8601 format -" - " YYYY-MM-DDTHH:MM:SS".format( - date=date)) - - def parse_datetime_for_nova(date): - # Must strip tzinfo from the date- it breaks Nova. Also, - # Nova is expecting this in UTC. If someone passes in an - # ISO8601 date string or a datetime with timzeone data attached, - # strip the timezone data but apply offset math first so that - # the user's well formed perfectly valid date will be used - # correctly. - offset = date.utcoffset() - if offset: - date = date - datetime.timedelta(hours=offset) - return date.replace(tzinfo=None) - - if not start: - start = parse_date('2010-07-06') - elif not isinstance(start, datetime.datetime): - start = parse_date(start) - if not end: - end = datetime.datetime.utcnow() - elif not isinstance(start, datetime.datetime): - end = parse_date(end) - - start = parse_datetime_for_nova(start) - end = parse_datetime_for_nova(end) - - proj = self.get_project(name_or_id) - if not proj: - raise exc.OpenStackCloudException( - "project does not exist: {}".format(name=proj.id)) - - data = proxy._json_response( - self.compute.get( - '/os-simple-tenant-usage/{project}'.format(project=proj.id), - params=dict(start=start.isoformat(), end=end.isoformat())), - error_message="Unable to get usage for project: {name}".format( - name=proj.id)) - return self._normalize_compute_usage( - self._get_and_munchify('tenant_usage', data)) - - def set_volume_quotas(self, name_or_id, **kwargs): - """ Set a volume quota in a project - - :param name_or_id: project name or id - :param kwargs: key/value pairs of quota name and quota value - - :raises: OpenStackCloudException if the resource to set the - quota does not exist. - """ - - proj = self.get_project(name_or_id) - if not proj: - raise exc.OpenStackCloudException("project does not exist") - - kwargs['tenant_id'] = proj.id - self._volume_client.put( - '/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id), - json={'quota_set': kwargs}, - error_message="No valid quota or resource") - - def get_volume_quotas(self, name_or_id): - """ Get volume quotas for a project - - :param name_or_id: project name or id - :raises: OpenStackCloudException if it's not a valid project - - :returns: Munch object with the quotas - """ - proj = self.get_project(name_or_id) - if not proj: - raise exc.OpenStackCloudException("project does not exist") - - data = self._volume_client.get( - '/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id), - error_message="cinder client call failed") - return self._get_and_munchify('quota_set', data) - - def delete_volume_quotas(self, name_or_id): - """ Delete volume quotas for a project - - :param name_or_id: project name or id - :raises: OpenStackCloudException if it's not a valid project or the - cinder client call failed - - :returns: dict with the quotas - """ - proj = self.get_project(name_or_id) - if not proj: - raise exc.OpenStackCloudException("project does not exist") - - return self._volume_client.delete( - '/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id), - error_message="cinder client call failed") - - def set_network_quotas(self, name_or_id, **kwargs): - """ Set a network quota in a project - - :param name_or_id: project name or id - :param kwargs: key/value pairs of quota name and quota value - - :raises: OpenStackCloudException if the resource to set the - quota does not exist. - """ - - proj = self.get_project(name_or_id) - if not proj: - raise exc.OpenStackCloudException("project does not exist") - - exceptions.raise_from_response( - self.network.put( - '/quotas/{project_id}.json'.format(project_id=proj.id), - json={'quota': kwargs}), - error_message=("Error setting Neutron's quota for " - "project {0}".format(proj.id))) - - def get_network_quotas(self, name_or_id, details=False): - """ Get network quotas for a project - - :param name_or_id: project name or id - :param details: if set to True it will return details about usage - of quotas by given project - :raises: OpenStackCloudException if it's not a valid project - - :returns: Munch object with the quotas - """ - proj = self.get_project(name_or_id) - if not proj: - raise exc.OpenStackCloudException("project does not exist") - url = '/quotas/{project_id}'.format(project_id=proj.id) - if details: - url = url + "/details" - url = url + ".json" - data = proxy._json_response( - self.network.get(url), - error_message=("Error fetching Neutron's quota for " - "project {0}".format(proj.id))) - return self._get_and_munchify('quota', data) - - def get_network_extensions(self): - """Get Cloud provided network extensions - - :returns: set of Neutron extension aliases - """ - return self._neutron_extensions() - - def delete_network_quotas(self, name_or_id): - """ Delete network quotas for a project - - :param name_or_id: project name or id - :raises: OpenStackCloudException if it's not a valid project or the - network client call failed - - :returns: dict with the quotas - """ - proj = self.get_project(name_or_id) - if not proj: - raise exc.OpenStackCloudException("project does not exist") - exceptions.raise_from_response( - self.network.delete( - '/quotas/{project_id}.json'.format(project_id=proj.id)), - error_message=("Error deleting Neutron's quota for " - "project {0}".format(proj.id))) - - def list_magnum_services(self): - """List all Magnum services. - :returns: a list of dicts containing the service details. - - :raises: OpenStackCloudException on operation error. - """ - with _utils.shade_exceptions("Error fetching Magnum services list"): - data = self._container_infra_client.get('/mservices') - return self._normalize_magnum_services( - self._get_and_munchify('mservices', data)) - - def create_cluster(self, name, profile, config=None, desired_capacity=0, - max_size=None, metadata=None, min_size=None, - timeout=None): - profile = self.get_cluster_profile(profile) - profile_id = profile['id'] - body = { - 'desired_capacity': desired_capacity, - 'name': name, - 'profile_id': profile_id - } - - if config is not None: - body['config'] = config - - if max_size is not None: - body['max_size'] = max_size - - if metadata is not None: - body['metadata'] = metadata - - if min_size is not None: - body['min_size'] = min_size - - if timeout is not None: - body['timeout'] = timeout - - data = self._clustering_client.post( - '/clusters', json={'cluster': body}, - error_message="Error creating cluster {name}".format(name=name)) - - return self._get_and_munchify(key=None, data=data) - - def set_cluster_metadata(self, name_or_id, metadata): - cluster = self.get_cluster(name_or_id) - if not cluster: - raise exc.OpenStackCloudException( - 'Invalid Cluster {cluster}'.format(cluster=name_or_id)) - - self._clustering_client.post( - '/clusters/{cluster_id}/metadata'.format(cluster_id=cluster['id']), - json={'metadata': metadata}, - error_message='Error updating cluster metadata') - - def get_cluster_by_id(self, cluster_id): - try: - data = self._clustering_client.get( - "/clusters/{cluster_id}".format(cluster_id=cluster_id), - error_message="Error fetching cluster {name}".format( - name=cluster_id)) - return self._get_and_munchify('cluster', data) - except Exception: - return None - - def get_cluster(self, name_or_id, filters=None): - return _utils._get_entity( - cloud=self, resource='cluster', - name_or_id=name_or_id, filters=filters) - - def update_cluster(self, name_or_id, new_name=None, - profile_name_or_id=None, config=None, metadata=None, - timeout=None, profile_only=False): - old_cluster = self.get_cluster(name_or_id) - if old_cluster is None: - raise exc.OpenStackCloudException( - 'Invalid Cluster {cluster}'.format(cluster=name_or_id)) - cluster = { - 'profile_only': profile_only - } - - if config is not None: - cluster['config'] = config - - if metadata is not None: - cluster['metadata'] = metadata - - if profile_name_or_id is not None: - profile = self.get_cluster_profile(profile_name_or_id) - if profile is None: - raise exc.OpenStackCloudException( - 'Invalid Cluster Profile {profile}'.format( - profile=profile_name_or_id)) - cluster['profile_id'] = profile.id - - if timeout is not None: - cluster['timeout'] = timeout - - if new_name is not None: - cluster['name'] = new_name - - data = self._clustering_client.patch( - "/clusters/{cluster_id}".format(cluster_id=old_cluster['id']), - json={'cluster': cluster}, - error_message="Error updating cluster " - "{name}".format(name=name_or_id)) - - return self._get_and_munchify(key=None, data=data) - - def delete_cluster(self, name_or_id): - cluster = self.get_cluster(name_or_id) - if cluster is None: - self.log.debug("Cluster %s not found for deleting", name_or_id) - return False - - for policy in self.list_policies_on_cluster(name_or_id): - detach_policy = self.get_cluster_policy_by_id( - policy['policy_id']) - self.detach_policy_from_cluster(cluster, detach_policy) - - for receiver in self.list_cluster_receivers(): - if cluster["id"] == receiver["cluster_id"]: - self.delete_cluster_receiver(receiver["id"], wait=True) - - self._clustering_client.delete( - "/clusters/{cluster_id}".format(cluster_id=name_or_id), - error_message="Error deleting cluster {name}".format( - name=name_or_id)) - - return True - - def search_clusters(self, name_or_id=None, filters=None): - clusters = self.list_clusters() - return _utils._filter_list(clusters, name_or_id, filters) - - def list_clusters(self): - try: - data = self._clustering_client.get( - '/clusters', - error_message="Error fetching clusters") - return self._get_and_munchify('clusters', data) - except exc.OpenStackCloudURINotFound as e: - self.log.debug(str(e), exc_info=True) - return [] - - def attach_policy_to_cluster(self, name_or_id, policy_name_or_id, - is_enabled): - cluster = self.get_cluster(name_or_id) - policy = self.get_cluster_policy(policy_name_or_id) - if cluster is None: - raise exc.OpenStackCloudException( - 'Cluster {cluster} not found for attaching'.format( - cluster=name_or_id)) - - if policy is None: - raise exc.OpenStackCloudException( - 'Policy {policy} not found for attaching'.format( - policy=policy_name_or_id)) - - body = { - 'policy_id': policy['id'], - 'enabled': is_enabled - } - - self._clustering_client.post( - "/clusters/{cluster_id}/actions".format(cluster_id=cluster['id']), - error_message="Error attaching policy {policy} to cluster " - "{cluster}".format( - policy=policy['id'], - cluster=cluster['id']), - json={'policy_attach': body}) - - return True - - def detach_policy_from_cluster( - self, name_or_id, policy_name_or_id, wait=False, timeout=3600): - cluster = self.get_cluster(name_or_id) - policy = self.get_cluster_policy(policy_name_or_id) - if cluster is None: - raise exc.OpenStackCloudException( - 'Cluster {cluster} not found for detaching'.format( - cluster=name_or_id)) - - if policy is None: - raise exc.OpenStackCloudException( - 'Policy {policy} not found for detaching'.format( - policy=policy_name_or_id)) - - body = {'policy_id': policy['id']} - self._clustering_client.post( - "/clusters/{cluster_id}/actions".format(cluster_id=cluster['id']), - error_message="Error detaching policy {policy} from cluster " - "{cluster}".format( - policy=policy['id'], - cluster=cluster['id']), - json={'policy_detach': body}) - - if not wait: - return True - - value = [] - - for count in utils.iterate_timeout( - timeout, "Timeout waiting for cluster policy to detach"): - - # TODO(bjjohnson) This logic will wait until there are no policies. - # Since we're detaching a specific policy, checking to make sure - # that policy is not in the list of policies would be better. - policy_status = self.get_cluster_by_id(cluster['id'])['policies'] - - if policy_status == value: - break - return True - - def update_policy_on_cluster(self, name_or_id, policy_name_or_id, - is_enabled): - cluster = self.get_cluster(name_or_id) - policy = self.get_cluster_policy(policy_name_or_id) - if cluster is None: - raise exc.OpenStackCloudException( - 'Cluster {cluster} not found for updating'.format( - cluster=name_or_id)) - - if policy is None: - raise exc.OpenStackCloudException( - 'Policy {policy} not found for updating'.format( - policy=policy_name_or_id)) - - body = { - 'policy_id': policy['id'], - 'enabled': is_enabled - } - self._clustering_client.post( - "/clusters/{cluster_id}/actions".format(cluster_id=cluster['id']), - error_message="Error updating policy {policy} on cluster " - "{cluster}".format( - policy=policy['id'], - cluster=cluster['id']), - json={'policy_update': body}) - - return True - - def get_policy_on_cluster(self, name_or_id, policy_name_or_id): - try: - policy = self._clustering_client.get( - "/clusters/{cluster_id}/policies/{policy_id}".format( - cluster_id=name_or_id, policy_id=policy_name_or_id), - error_message="Error fetching policy " - "{name}".format(name=policy_name_or_id)) - return self._get_and_munchify('cluster_policy', policy) - except Exception: - return False - - def list_policies_on_cluster(self, name_or_id): - endpoint = "/clusters/{cluster_id}/policies".format( - cluster_id=name_or_id) - try: - data = self._clustering_client.get( - endpoint, - error_message="Error fetching cluster policies") - except exc.OpenStackCloudURINotFound as e: - self.log.debug(str(e), exc_info=True) - return [] - return self._get_and_munchify('cluster_policies', data) - - def create_cluster_profile(self, name, spec, metadata=None): - profile = { - 'name': name, - 'spec': spec - } - - if metadata is not None: - profile['metadata'] = metadata - - data = self._clustering_client.post( - '/profiles', json={'profile': profile}, - error_message="Error creating profile {name}".format(name=name)) - - return self._get_and_munchify('profile', data) - - def set_cluster_profile_metadata(self, name_or_id, metadata): - profile = self.get_cluster_profile(name_or_id) - if not profile: - raise exc.OpenStackCloudException( - 'Invalid Profile {profile}'.format(profile=name_or_id)) - - self._clustering_client.post( - '/profiles/{profile_id}/metadata'.format(profile_id=profile['id']), - json={'metadata': metadata}, - error_message='Error updating profile metadata') - - def search_cluster_profiles(self, name_or_id=None, filters=None): - cluster_profiles = self.list_cluster_profiles() - return _utils._filter_list(cluster_profiles, name_or_id, filters) - - def list_cluster_profiles(self): - try: - data = self._clustering_client.get( - '/profiles', - error_message="Error fetching profiles") - except exc.OpenStackCloudURINotFound as e: - self.log.debug(str(e), exc_info=True) - return [] - return self._get_and_munchify('profiles', data) - - def get_cluster_profile_by_id(self, profile_id): - try: - data = self._clustering_client.get( - "/profiles/{profile_id}".format(profile_id=profile_id), - error_message="Error fetching profile {name}".format( - name=profile_id)) - return self._get_and_munchify('profile', data) - except exc.OpenStackCloudURINotFound as e: - self.log.debug(str(e), exc_info=True) - return None - - def get_cluster_profile(self, name_or_id, filters=None): - return _utils._get_entity(self, 'cluster_profile', name_or_id, filters) - - def delete_cluster_profile(self, name_or_id): - profile = self.get_cluster_profile(name_or_id) - if profile is None: - self.log.debug("Profile %s not found for deleting", name_or_id) - return False - - for cluster in self.list_clusters(): - if (name_or_id, profile.id) in cluster.items(): - self.log.debug( - "Profile %s is being used by cluster %s, won't delete", - name_or_id, cluster.name) - return False - - self._clustering_client.delete( - "/profiles/{profile_id}".format(profile_id=profile['id']), - error_message="Error deleting profile " - "{name}".format(name=name_or_id)) - - return True - - def update_cluster_profile(self, name_or_id, metadata=None, new_name=None): - old_profile = self.get_cluster_profile(name_or_id) - if not old_profile: - raise exc.OpenStackCloudException( - 'Invalid Profile {profile}'.format(profile=name_or_id)) - - profile = {} - - if metadata is not None: - profile['metadata'] = metadata - - if new_name is not None: - profile['name'] = new_name - - data = self._clustering_client.patch( - "/profiles/{profile_id}".format(profile_id=old_profile.id), - json={'profile': profile}, - error_message="Error updating profile {name}".format( - name=name_or_id)) - - return self._get_and_munchify(key=None, data=data) - - def create_cluster_policy(self, name, spec): - policy = { - 'name': name, - 'spec': spec - } - - data = self._clustering_client.post( - '/policies', json={'policy': policy}, - error_message="Error creating policy {name}".format( - name=policy['name'])) - return self._get_and_munchify('policy', data) - - def search_cluster_policies(self, name_or_id=None, filters=None): - cluster_policies = self.list_cluster_policies() - return _utils._filter_list(cluster_policies, name_or_id, filters) - - def list_cluster_policies(self): - endpoint = "/policies" - try: - data = self._clustering_client.get( - endpoint, - error_message="Error fetching cluster policies") - except exc.OpenStackCloudURINotFound as e: - self.log.debug(str(e), exc_info=True) - return [] - return self._get_and_munchify('policies', data) - - def get_cluster_policy_by_id(self, policy_id): - try: - data = self._clustering_client.get( - "/policies/{policy_id}".format(policy_id=policy_id), - error_message="Error fetching policy {name}".format( - name=policy_id)) - return self._get_and_munchify('policy', data) - except exc.OpenStackCloudURINotFound as e: - self.log.debug(str(e), exc_info=True) - return None - - def get_cluster_policy(self, name_or_id, filters=None): - return _utils._get_entity( - self, 'cluster_policie', name_or_id, filters) - - def delete_cluster_policy(self, name_or_id): - policy = self.get_cluster_policy_by_id(name_or_id) - if policy is None: - self.log.debug("Policy %s not found for deleting", name_or_id) - return False - - for cluster in self.list_clusters(): - if (name_or_id, policy.id) in cluster.items(): - self.log.debug( - "Policy %s is being used by cluster %s, won't delete", - name_or_id, cluster.name) - return False - - self._clustering_client.delete( - "/policies/{policy_id}".format(policy_id=name_or_id), - error_message="Error deleting policy " - "{name}".format(name=name_or_id)) - - return True - - def update_cluster_policy(self, name_or_id, new_name): - old_policy = self.get_cluster_policy(name_or_id) - if not old_policy: - raise exc.OpenStackCloudException( - 'Invalid Policy {policy}'.format(policy=name_or_id)) - policy = {'name': new_name} - - data = self._clustering_client.patch( - "/policies/{policy_id}".format(policy_id=old_policy.id), - json={'policy': policy}, - error_message="Error updating policy " - "{name}".format(name=name_or_id)) - return self._get_and_munchify(key=None, data=data) - - def create_cluster_receiver(self, name, receiver_type, - cluster_name_or_id=None, action=None, - actor=None, params=None): - cluster = self.get_cluster(cluster_name_or_id) - if cluster is None: - raise exc.OpenStackCloudException( - 'Invalid cluster {cluster}'.format(cluster=cluster_name_or_id)) - - receiver = { - 'name': name, - 'type': receiver_type - } - - if cluster_name_or_id is not None: - receiver['cluster_id'] = cluster.id - - if action is not None: - receiver['action'] = action - - if actor is not None: - receiver['actor'] = actor - - if params is not None: - receiver['params'] = params - - data = self._clustering_client.post( - '/receivers', json={'receiver': receiver}, - error_message="Error creating receiver {name}".format(name=name)) - return self._get_and_munchify('receiver', data) - - def search_cluster_receivers(self, name_or_id=None, filters=None): - cluster_receivers = self.list_cluster_receivers() - return _utils._filter_list(cluster_receivers, name_or_id, filters) - - def list_cluster_receivers(self): - try: - data = self._clustering_client.get( - '/receivers', - error_message="Error fetching receivers") - except exc.OpenStackCloudURINotFound as e: - self.log.debug(str(e), exc_info=True) - return [] - return self._get_and_munchify('receivers', data) - - def get_cluster_receiver_by_id(self, receiver_id): - try: - data = self._clustering_client.get( - "/receivers/{receiver_id}".format(receiver_id=receiver_id), - error_message="Error fetching receiver {name}".format( - name=receiver_id)) - return self._get_and_munchify('receiver', data) - except exc.OpenStackCloudURINotFound as e: - self.log.debug(str(e), exc_info=True) - return None - - def get_cluster_receiver(self, name_or_id, filters=None): - return _utils._get_entity( - self, 'cluster_receiver', name_or_id, filters) - - def delete_cluster_receiver(self, name_or_id, wait=False, timeout=3600): - receiver = self.get_cluster_receiver(name_or_id) - if receiver is None: - self.log.debug("Receiver %s not found for deleting", name_or_id) - return False - - receiver_id = receiver['id'] - - self._clustering_client.delete( - "/receivers/{receiver_id}".format(receiver_id=receiver_id), - error_message="Error deleting receiver {name}".format( - name=name_or_id)) - - if not wait: - return True - - for count in utils.iterate_timeout( - timeout, "Timeout waiting for cluster receiver to delete"): - - receiver = self.get_cluster_receiver_by_id(receiver_id) - - if not receiver: - break - - return True - - def update_cluster_receiver(self, name_or_id, new_name=None, action=None, - params=None): - old_receiver = self.get_cluster_receiver(name_or_id) - if old_receiver is None: - raise exc.OpenStackCloudException( - 'Invalid receiver {receiver}'.format(receiver=name_or_id)) - - receiver = {} - - if new_name is not None: - receiver['name'] = new_name - - if action is not None: - receiver['action'] = action - - if params is not None: - receiver['params'] = params - - data = self._clustering_client.patch( - "/receivers/{receiver_id}".format(receiver_id=old_receiver.id), - json={'receiver': receiver}, - error_message="Error updating receiver {name}".format( - name=name_or_id)) - return self._get_and_munchify(key=None, data=data) - - @_utils.valid_kwargs( - 'action', 'description', 'destination_firewall_group_id', - 'destination_ip_address', 'destination_port', 'enabled', 'ip_version', - 'name', 'project_id', 'protocol', 'shared', 'source_firewall_group_id', - 'source_ip_address', 'source_port') - def create_firewall_rule(self, **kwargs): - """ - Creates firewall rule. - - :param action: Action performed on traffic. - Valid values: allow, deny - Defaults to deny. - :param description: Human-readable description. - :param destination_firewall_group_id: ID of destination firewall group. - :param destination_ip_address: IPv4-, IPv6 address or CIDR. - :param destination_port: Port or port range (e.g. 80:90) - :param bool enabled: Status of firewall rule. You can disable rules - without disassociating them from firewall - policies. Defaults to True. - :param int ip_version: IP Version. - Valid values: 4, 6 - Defaults to 4. - :param name: Human-readable name. - :param project_id: Project id. - :param protocol: IP protocol. - Valid values: icmp, tcp, udp, null - :param bool shared: Visibility to other projects. - Defaults to False. - :param source_firewall_group_id: ID of source firewall group. - :param source_ip_address: IPv4-, IPv6 address or CIDR. - :param source_port: Port or port range (e.g. 80:90) - :raises: BadRequestException if parameters are malformed - :return: created firewall rule - :rtype: FirewallRule - """ - return self.network.create_firewall_rule(**kwargs) - - def delete_firewall_rule(self, name_or_id, filters=None): - """ - Deletes firewall rule. - Prints debug message in case to-be-deleted resource was not found. - - :param name_or_id: firewall rule name or id - :param dict filters: optional filters - :raises: DuplicateResource on multiple matches - :return: True if resource is successfully deleted, False otherwise. - :rtype: bool - """ - if not filters: - filters = {} - try: - firewall_rule = self.network.find_firewall_rule( - name_or_id, ignore_missing=False, **filters) - self.network.delete_firewall_rule(firewall_rule, - ignore_missing=False) - except exceptions.ResourceNotFound: - self.log.debug('Firewall rule %s not found for deleting', - name_or_id) - return False - return True - - def get_firewall_rule(self, name_or_id, filters=None): - """ - Retrieves a single firewall rule. - - :param name_or_id: firewall rule name or id - :param dict filters: optional filters - :raises: DuplicateResource on multiple matches - :return: firewall rule dict or None if not found - :rtype: FirewallRule - """ - if not filters: - filters = {} - return self.network.find_firewall_rule(name_or_id, **filters) - - def list_firewall_rules(self, filters=None): - """ - Lists firewall rules. - - :param dict filters: optional filters - :return: list of firewall rules - :rtype: list[FirewallRule] - """ - if not filters: - filters = {} - return list(self.network.firewall_rules(**filters)) - - @_utils.valid_kwargs( - 'action', 'description', 'destination_firewall_group_id', - 'destination_ip_address', 'destination_port', 'enabled', 'ip_version', - 'name', 'project_id', 'protocol', 'shared', 'source_firewall_group_id', - 'source_ip_address', 'source_port') - def update_firewall_rule(self, name_or_id, filters=None, **kwargs): - """ - Updates firewall rule. - - :param name_or_id: firewall rule name or id - :param dict filters: optional filters - :param kwargs: firewall rule update parameters. - See create_firewall_rule docstring for valid parameters. - :raises: BadRequestException if parameters are malformed - :raises: NotFoundException if resource is not found - :return: updated firewall rule - :rtype: FirewallRule - """ - if not filters: - filters = {} - firewall_rule = self.network.find_firewall_rule( - name_or_id, ignore_missing=False, **filters) - - return self.network.update_firewall_rule(firewall_rule, **kwargs) - - def _get_firewall_rule_ids(self, name_or_id_list, filters=None): - """ - Takes a list of firewall rule name or ids, looks them up and returns - a list of firewall rule ids. - - Used by `create_firewall_policy` and `update_firewall_policy`. - - :param list[str] name_or_id_list: firewall rule name or id list - :param dict filters: optional filters - :raises: DuplicateResource on multiple matches - :raises: NotFoundException if resource is not found - :return: list of firewall rule ids - :rtype: list[str] - """ - if not filters: - filters = {} - ids_list = [] - for name_or_id in name_or_id_list: - ids_list.append(self.network.find_firewall_rule( - name_or_id, ignore_missing=False, **filters)['id']) - return ids_list - - @_utils.valid_kwargs('audited', 'description', 'firewall_rules', 'name', - 'project_id', 'shared') - def create_firewall_policy(self, **kwargs): - """ - Create firewall policy. - - :param bool audited: Status of audition of firewall policy. - Set to False each time the firewall policy or the - associated firewall rules are changed. - Has to be explicitly set to True. - :param description: Human-readable description. - :param list[str] firewall_rules: List of associated firewall rules. - :param name: Human-readable name. - :param project_id: Project id. - :param bool shared: Visibility to other projects. - Defaults to False. - :raises: BadRequestException if parameters are malformed - :raises: ResourceNotFound if a resource from firewall_list not found - :return: created firewall policy - :rtype: FirewallPolicy - """ - if 'firewall_rules' in kwargs: - kwargs['firewall_rules'] = self._get_firewall_rule_ids( - kwargs['firewall_rules']) - - return self.network.create_firewall_policy(**kwargs) - - def delete_firewall_policy(self, name_or_id, filters=None): - """ - Deletes firewall policy. - Prints debug message in case to-be-deleted resource was not found. - - :param name_or_id: firewall policy name or id - :param dict filters: optional filters - :raises: DuplicateResource on multiple matches - :return: True if resource is successfully deleted, False otherwise. - :rtype: bool - """ - if not filters: - filters = {} - try: - firewall_policy = self.network.find_firewall_policy( - name_or_id, ignore_missing=False, **filters) - self.network.delete_firewall_policy(firewall_policy, - ignore_missing=False) - except exceptions.ResourceNotFound: - self.log.debug('Firewall policy %s not found for deleting', - name_or_id) - return False - return True - - def get_firewall_policy(self, name_or_id, filters=None): - """ - Retrieves a single firewall policy. - - :param name_or_id: firewall policy name or id - :param dict filters: optional filters - :raises: DuplicateResource on multiple matches - :return: firewall policy or None if not found - :rtype: FirewallPolicy - """ - if not filters: - filters = {} - return self.network.find_firewall_policy(name_or_id, **filters) - - def list_firewall_policies(self, filters=None): - """ - Lists firewall policies. - - :param dict filters: optional filters - :return: list of firewall policies - :rtype: list[FirewallPolicy] - """ - if not filters: - filters = {} - return list(self.network.firewall_policies(**filters)) - - @_utils.valid_kwargs('audited', 'description', 'firewall_rules', 'name', - 'project_id', 'shared') - def update_firewall_policy(self, name_or_id, filters=None, **kwargs): - """ - Updates firewall policy. - - :param name_or_id: firewall policy name or id - :param dict filters: optional filters - :param kwargs: firewall policy update parameters - See create_firewall_policy docstring for valid parameters. - :raises: BadRequestException if parameters are malformed - :raises: DuplicateResource on multiple matches - :raises: ResourceNotFound if resource is not found - :return: updated firewall policy - :rtype: FirewallPolicy - """ - if not filters: - filters = {} - firewall_policy = self.network.find_firewall_policy( - name_or_id, ignore_missing=False, **filters) - - if 'firewall_rules' in kwargs: - kwargs['firewall_rules'] = self._get_firewall_rule_ids( - kwargs['firewall_rules']) - - return self.network.update_firewall_policy(firewall_policy, **kwargs) - - def insert_rule_into_policy(self, name_or_id, rule_name_or_id, - insert_after=None, insert_before=None, - filters=None): - """ - Adds firewall rule to the firewall_rules list of a firewall policy. - Short-circuits and returns the firewall policy early if the firewall - rule id is already present in the firewall_rules list. - This method doesn't do re-ordering. If you want to move a firewall rule - or or down the list, you have to remove and re-add it. - - :param name_or_id: firewall policy name or id - :param rule_name_or_id: firewall rule name or id - :param insert_after: rule name or id that should precede added rule - :param insert_before: rule name or id that should succeed added rule - :param dict filters: optional filters - :raises: DuplicateResource on multiple matches - :raises: ResourceNotFound if firewall policy or any of the firewall - rules (inserted, after, before) is not found. - :return: updated firewall policy - :rtype: FirewallPolicy - """ - if not filters: - filters = {} - firewall_policy = self.network.find_firewall_policy( - name_or_id, ignore_missing=False, **filters) - - firewall_rule = self.network.find_firewall_rule( - rule_name_or_id, ignore_missing=False) - # short-circuit if rule already in firewall_rules list - # the API can't do any re-ordering of existing rules - if firewall_rule['id'] in firewall_policy['firewall_rules']: - self.log.debug( - 'Firewall rule %s already associated with firewall policy %s', - rule_name_or_id, name_or_id) - return firewall_policy - - pos_params = {} - if insert_after is not None: - pos_params['insert_after'] = self.network.find_firewall_rule( - insert_after, ignore_missing=False)['id'] - - if insert_before is not None: - pos_params['insert_before'] = self.network.find_firewall_rule( - insert_before, ignore_missing=False)['id'] - - return self.network.insert_rule_into_policy(firewall_policy['id'], - firewall_rule['id'], - **pos_params) - - def remove_rule_from_policy(self, name_or_id, rule_name_or_id, - filters=None): - """ - Remove firewall rule from firewall policy's firewall_rules list. - Short-circuits and returns firewall policy early if firewall rule - is already absent from the firewall_rules list. - - :param name_or_id: firewall policy name or id - :param rule_name_or_id: firewall rule name or id - :param dict filters: optional filters - :raises: DuplicateResource on multiple matches - :raises: ResourceNotFound if firewall policy is not found - :return: updated firewall policy - :rtype: FirewallPolicy - """ - if not filters: - filters = {} - firewall_policy = self.network.find_firewall_policy( - name_or_id, ignore_missing=False, **filters) - - firewall_rule = self.network.find_firewall_rule(rule_name_or_id) - if not firewall_rule: - # short-circuit: if firewall rule is not found, - # return current firewall policy - self.log.debug('Firewall rule %s not found for removing', - rule_name_or_id) - return firewall_policy - - if firewall_rule['id'] not in firewall_policy['firewall_rules']: - # short-circuit: if firewall rule id is not associated, - # log it to debug and return current firewall policy - self.log.debug( - 'Firewall rule %s not associated with firewall policy %s', - rule_name_or_id, name_or_id) - return firewall_policy - - return self.network.remove_rule_from_policy(firewall_policy['id'], - firewall_rule['id']) - - @_utils.valid_kwargs( - 'admin_state_up', 'description', 'egress_firewall_policy', - 'ingress_firewall_policy', 'name', 'ports', 'project_id', 'shared') - def create_firewall_group(self, **kwargs): - """ - Creates firewall group. The keys egress_firewall_policy and - ingress_firewall_policy are looked up and mapped as - egress_firewall_policy_id and ingress_firewall_policy_id respectively. - Port name or ids list is transformed to port ids list before the POST - request. - - :param bool admin_state_up: State of firewall group. - Will block all traffic if set to False. - Defaults to True. - :param description: Human-readable description. - :param egress_firewall_policy: Name or id of egress firewall policy. - :param ingress_firewall_policy: Name or id of ingress firewall policy. - :param name: Human-readable name. - :param list[str] ports: List of associated ports (name or id) - :param project_id: Project id. - :param shared: Visibility to other projects. - Defaults to False. - :raises: BadRequestException if parameters are malformed - :raises: DuplicateResource on multiple matches - :raises: ResourceNotFound if (ingress-, egress-) firewall policy or - a port is not found. - :return: created firewall group - :rtype: FirewallGroup - """ - self._lookup_ingress_egress_firewall_policy_ids(kwargs) - if 'ports' in kwargs: - kwargs['ports'] = self._get_port_ids(kwargs['ports']) - return self.network.create_firewall_group(**kwargs) - - def delete_firewall_group(self, name_or_id, filters=None): - """ - Deletes firewall group. - Prints debug message in case to-be-deleted resource was not found. - - :param name_or_id: firewall group name or id - :param dict filters: optional filters - :raises: DuplicateResource on multiple matches - :return: True if resource is successfully deleted, False otherwise. - :rtype: bool - """ - if not filters: - filters = {} - try: - firewall_group = self.network.find_firewall_group( - name_or_id, ignore_missing=False, **filters) - self.network.delete_firewall_group(firewall_group, - ignore_missing=False) - except exceptions.ResourceNotFound: - self.log.debug('Firewall group %s not found for deleting', - name_or_id) - return False - return True - - def get_firewall_group(self, name_or_id, filters=None): - """ - Retrieves firewall group. - - :param name_or_id: firewall group name or id - :param dict filters: optional filters - :raises: DuplicateResource on multiple matches - :return: firewall group or None if not found - :rtype: FirewallGroup - """ - if not filters: - filters = {} - return self.network.find_firewall_group(name_or_id, **filters) - - def list_firewall_groups(self, filters=None): - """ - Lists firewall groups. - - :param dict filters: optional filters - :return: list of firewall groups - :rtype: list[FirewallGroup] - """ - if not filters: - filters = {} - return list(self.network.firewall_groups(**filters)) - - @_utils.valid_kwargs( - 'admin_state_up', 'description', 'egress_firewall_policy', - 'ingress_firewall_policy', 'name', 'ports', 'project_id', 'shared') - def update_firewall_group(self, name_or_id, filters=None, **kwargs): - """ - Updates firewall group. - To unset egress- or ingress firewall policy, set egress_firewall_policy - or ingress_firewall_policy to None. You can also set - egress_firewall_policy_id and ingress_firewall_policy_id directly, - which will skip the policy lookups. - - :param name_or_id: firewall group name or id - :param dict filters: optional filters - :param kwargs: firewall group update parameters - See create_firewall_group docstring for valid parameters. - :raises: BadRequestException if parameters are malformed - :raises: DuplicateResource on multiple matches - :raises: ResourceNotFound if firewall group, a firewall policy - (egress, ingress) or port is not found - :return: updated firewall group - :rtype: FirewallGroup - """ - if not filters: - filters = {} - firewall_group = self.network.find_firewall_group( - name_or_id, ignore_missing=False, **filters) - self._lookup_ingress_egress_firewall_policy_ids(kwargs) - - if 'ports' in kwargs: - kwargs['ports'] = self._get_port_ids(kwargs['ports']) - return self.network.update_firewall_group(firewall_group, **kwargs) - - def _lookup_ingress_egress_firewall_policy_ids(self, firewall_group): - """ - Transforms firewall_group dict IN-PLACE. Takes the value of the keys - egress_firewall_policy and ingress_firewall_policy, looks up the - policy ids and maps them to egress_firewall_policy_id and - ingress_firewall_policy_id. Old keys which were used for the lookup - are deleted. - - :param dict firewall_group: firewall group dict - :raises: DuplicateResource on multiple matches - :raises: ResourceNotFound if a firewall policy is not found - """ - for key in ('egress_firewall_policy', 'ingress_firewall_policy'): - if key not in firewall_group: - continue - if firewall_group[key] is None: - val = None - else: - val = self.network.find_firewall_policy( - firewall_group[key], ignore_missing=False)['id'] - firewall_group[key + '_id'] = val - del firewall_group[key] - - def _get_port_ids(self, name_or_id_list, filters=None): - """ - Takes a list of port names or ids, retrieves ports and returns a list - with port ids only. - - :param list[str] name_or_id_list: list of port names or ids - :param dict filters: optional filters - :raises: SDKException on multiple matches - :raises: ResourceNotFound if a port is not found - :return: list of port ids - :rtype: list[str] - """ - ids_list = [] - for name_or_id in name_or_id_list: - port = self.get_port(name_or_id, filters) - if not port: - raise exceptions.ResourceNotFound( - 'Port {id} not found'.format(id=name_or_id)) - ids_list.append(port['id']) - return ids_list + def _expand_server_vars(self, server): + # Used by nodepool + # TODO(mordred) remove after these make it into what we + # actually want the API to be. + return meta.expand_server_vars(self, server) diff --git a/openstack/connection.py b/openstack/connection.py index f76a9b765..397c6db32 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -164,6 +164,20 @@ from openstack import _log from openstack._meta import connection as _meta from openstack.cloud import openstackcloud as _cloud +from openstack.cloud import _baremetal +from openstack.cloud import _block_storage +from openstack.cloud import _compute +from openstack.cloud import _clustering +from openstack.cloud import _coe +from openstack.cloud import _dns +from openstack.cloud import _floating_ip +from openstack.cloud import _identity +from openstack.cloud import _image +from openstack.cloud import _network +from openstack.cloud import _network_common +from openstack.cloud import _object_store +from openstack.cloud import _orchestration +from openstack.cloud import _security_group from openstack import config as _config from openstack.config import cloud_region from openstack import exceptions @@ -211,7 +225,22 @@ def from_config(cloud=None, config=None, options=None, **kwargs): class Connection(six.with_metaclass(_meta.ConnectionMeta, - _cloud._OpenStackCloudMixin)): + _cloud._OpenStackCloudMixin, + _baremetal.BaremetalCloudMixin, + _block_storage.BlockStorageCloudMixin, + _compute.ComputeCloudMixin, + _clustering.ClusteringCloudMixin, + _coe.CoeCloudMixin, + _dns.DnsCloudMixin, + _floating_ip.FloatingIPCloudMixin, + _identity.IdentityCloudMixin, + _image.ImageCloudMixin, + _network.NetworkCloudMixin, + _network_common.NetworkCommonCloudMixin, + _object_store.ObjectStoreCloudMixin, + _orchestration.OrchestrationCloudMixin, + _security_group.SecurityGroupCloudMixin + )): def __init__(self, cloud=None, config=None, session=None, app_name=None, app_version=None, @@ -298,9 +327,23 @@ def __init__(self, cloud=None, config=None, session=None, self._proxies = {} self.use_direct_get = use_direct_get self.strict_mode = strict - # Call the _OpenStackCloudMixin constructor while we work on + # Call the _*CloudMixin constructors while we work on # integrating things better. _cloud._OpenStackCloudMixin.__init__(self) + _baremetal.BaremetalCloudMixin.__init__(self) + _block_storage.BlockStorageCloudMixin.__init__(self) + _clustering.ClusteringCloudMixin.__init__(self) + _coe.CoeCloudMixin.__init__(self) + _compute.ComputeCloudMixin.__init__(self) + _dns.DnsCloudMixin.__init__(self) + _floating_ip.FloatingIPCloudMixin.__init__(self) + _identity.IdentityCloudMixin.__init__(self) + _image.ImageCloudMixin.__init__(self) + _network_common.NetworkCommonCloudMixin.__init__(self) + _network.NetworkCloudMixin.__init__(self) + _object_store.ObjectStoreCloudMixin.__init__(self) + _orchestration.OrchestrationCloudMixin.__init__(self) + _security_group.SecurityGroupCloudMixin.__init__(self) @property def session(self): diff --git a/openstack/tests/functional/instance_ha/test_host.py b/openstack/tests/functional/instance_ha/test_host.py index 23bfb0a92..1b03cb28a 100644 --- a/openstack/tests/functional/instance_ha/test_host.py +++ b/openstack/tests/functional/instance_ha/test_host.py @@ -15,7 +15,6 @@ # import unittest from openstack import connection -from openstack.cloud.openstackcloud import _OpenStackCloudMixin from openstack.tests.functional import base HYPERVISORS = [] @@ -25,7 +24,7 @@ def hypervisors(): global HYPERVISORS if HYPERVISORS: return True - HYPERVISORS = _OpenStackCloudMixin.list_hypervisors( + HYPERVISORS = connection.Connection.list_hypervisors( connection.from_config(cloud_name=base.TEST_CLOUD_NAME)) return bool(HYPERVISORS) diff --git a/openstack/tests/unit/cloud/test_create_server.py b/openstack/tests/unit/cloud/test_create_server.py index af43c2d72..35d25cd99 100644 --- a/openstack/tests/unit/cloud/test_create_server.py +++ b/openstack/tests/unit/cloud/test_create_server.py @@ -21,7 +21,7 @@ import mock -from openstack.cloud import openstackcloud +from openstack import connection from openstack.cloud import exc from openstack.cloud import meta from openstack.tests import fakes @@ -325,7 +325,7 @@ def test_create_server_with_admin_pass_no_wait(self): self.assert_calls() - @mock.patch.object(openstackcloud._OpenStackCloudMixin, "wait_for_server") + @mock.patch.object(connection.Connection, "wait_for_server") def test_create_server_with_admin_pass_wait(self, mock_wait): """ Test that a server with an admin_pass passed returns the password @@ -411,9 +411,8 @@ def test_create_server_user_data_base64(self): self.assert_calls() - @mock.patch.object( - openstackcloud._OpenStackCloudMixin, "get_active_server") - @mock.patch.object(openstackcloud._OpenStackCloudMixin, "get_server") + @mock.patch.object(connection.Connection, "get_active_server") + @mock.patch.object(connection.Connection, "get_server") def test_wait_for_server(self, mock_get_server, mock_get_active_server): """ Test that waiting for a server returns the server instance when @@ -447,7 +446,7 @@ def test_wait_for_server(self, mock_get_server, mock_get_active_server): self.assertEqual('ACTIVE', server['status']) - @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'wait_for_server') + @mock.patch.object(connection.Connection, 'wait_for_server') def test_create_server_wait(self, mock_wait): """ Test that create_server with a wait actually does the wait. @@ -484,8 +483,7 @@ def test_create_server_wait(self, mock_wait): ) self.assert_calls() - @mock.patch.object( - openstackcloud._OpenStackCloudMixin, 'add_ips_to_server') + @mock.patch.object(connection.Connection, 'add_ips_to_server') def test_create_server_no_addresses( self, mock_add_ips_to_server): """ diff --git a/openstack/tests/unit/cloud/test_floating_ip_common.py b/openstack/tests/unit/cloud/test_floating_ip_common.py index c17e81636..0103f284a 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_common.py +++ b/openstack/tests/unit/cloud/test_floating_ip_common.py @@ -21,17 +21,17 @@ from mock import patch +from openstack import connection from openstack.cloud import meta -from openstack.cloud import openstackcloud from openstack.tests import fakes from openstack.tests.unit import base class TestFloatingIP(base.TestCase): - @patch.object(openstackcloud._OpenStackCloudMixin, 'get_floating_ip') - @patch.object(openstackcloud._OpenStackCloudMixin, '_attach_ip_to_server') - @patch.object(openstackcloud._OpenStackCloudMixin, 'available_floating_ip') + @patch.object(connection.Connection, 'get_floating_ip') + @patch.object(connection.Connection, '_attach_ip_to_server') + @patch.object(connection.Connection, 'available_floating_ip') def test_add_auto_ip( self, mock_available_floating_ip, mock_attach_ip_to_server, mock_get_floating_ip): @@ -57,7 +57,7 @@ def test_add_auto_ip( timeout=60, wait=False, server=server_dict, floating_ip=floating_ip_dict, skip_attach=False) - @patch.object(openstackcloud._OpenStackCloudMixin, '_add_ip_from_pool') + @patch.object(connection.Connection, '_add_ip_from_pool') def test_add_ips_to_server_pool(self, mock_add_ip_from_pool): server_dict = fakes.make_fake_server( server_id='romeo', name='test-server', status="ACTIVE", @@ -70,9 +70,9 @@ def test_add_ips_to_server_pool(self, mock_add_ip_from_pool): server_dict, pool, reuse=True, wait=False, timeout=60, fixed_address=None, nat_destination=None) - @patch.object(openstackcloud._OpenStackCloudMixin, 'has_service') - @patch.object(openstackcloud._OpenStackCloudMixin, 'get_floating_ip') - @patch.object(openstackcloud._OpenStackCloudMixin, '_add_auto_ip') + @patch.object(connection.Connection, 'has_service') + @patch.object(connection.Connection, 'get_floating_ip') + @patch.object(connection.Connection, '_add_auto_ip') def test_add_ips_to_server_ipv6_only( self, mock_add_auto_ip, mock_get_floating_ip, @@ -109,9 +109,9 @@ def test_add_ips_to_server_ipv6_only( self.assertEqual( new_server['public_v6'], '2001:4800:7819:103:be76:4eff:fe05:8525') - @patch.object(openstackcloud._OpenStackCloudMixin, 'has_service') - @patch.object(openstackcloud._OpenStackCloudMixin, 'get_floating_ip') - @patch.object(openstackcloud._OpenStackCloudMixin, '_add_auto_ip') + @patch.object(connection.Connection, 'has_service') + @patch.object(connection.Connection, 'get_floating_ip') + @patch.object(connection.Connection, '_add_auto_ip') def test_add_ips_to_server_rackspace( self, mock_add_auto_ip, mock_get_floating_ip, @@ -145,9 +145,9 @@ def test_add_ips_to_server_rackspace( new_server['interface_ip'], '2001:4800:7819:103:be76:4eff:fe05:8525') - @patch.object(openstackcloud._OpenStackCloudMixin, 'has_service') - @patch.object(openstackcloud._OpenStackCloudMixin, 'get_floating_ip') - @patch.object(openstackcloud._OpenStackCloudMixin, '_add_auto_ip') + @patch.object(connection.Connection, 'has_service') + @patch.object(connection.Connection, 'get_floating_ip') + @patch.object(connection.Connection, '_add_auto_ip') def test_add_ips_to_server_rackspace_local_ipv4( self, mock_add_auto_ip, mock_get_floating_ip, @@ -179,7 +179,7 @@ def test_add_ips_to_server_rackspace_local_ipv4( mock_add_auto_ip.assert_not_called() self.assertEqual(new_server['interface_ip'], '104.130.246.91') - @patch.object(openstackcloud._OpenStackCloudMixin, 'add_ip_list') + @patch.object(connection.Connection, 'add_ip_list') def test_add_ips_to_server_ip_list(self, mock_add_ip_list): server_dict = fakes.make_fake_server( server_id='server-id', name='test-server', status="ACTIVE", @@ -191,8 +191,8 @@ def test_add_ips_to_server_ip_list(self, mock_add_ip_list): mock_add_ip_list.assert_called_with( server_dict, ips, wait=False, timeout=60, fixed_address=None) - @patch.object(openstackcloud._OpenStackCloudMixin, '_needs_floating_ip') - @patch.object(openstackcloud._OpenStackCloudMixin, '_add_auto_ip') + @patch.object(connection.Connection, '_needs_floating_ip') + @patch.object(connection.Connection, '_add_auto_ip') def test_add_ips_to_server_auto_ip( self, mock_add_auto_ip, mock_needs_floating_ip): server_dict = fakes.make_fake_server( diff --git a/openstack/tests/unit/cloud/test_meta.py b/openstack/tests/unit/cloud/test_meta.py index f6100d939..c1ee49946 100644 --- a/openstack/tests/unit/cloud/test_meta.py +++ b/openstack/tests/unit/cloud/test_meta.py @@ -14,7 +14,7 @@ import mock -from openstack.cloud import openstackcloud +from openstack import connection from openstack.cloud import meta from openstack.tests import fakes from openstack.tests.unit import base @@ -353,10 +353,10 @@ def test_get_server_multiple_private_ip(self): '10.0.0.101', meta.get_server_private_ip(srv, self.cloud)) self.assert_calls() - @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'has_service') - @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_volumes') - @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_image_name') - @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_flavor_name') + @mock.patch.object(connection.Connection, 'has_service') + @mock.patch.object(connection.Connection, 'get_volumes') + @mock.patch.object(connection.Connection, 'get_image_name') + @mock.patch.object(connection.Connection, 'get_flavor_name') def test_get_server_private_ip_devstack( self, mock_get_flavor_name, mock_get_image_name, @@ -418,9 +418,9 @@ def test_get_server_private_ip_devstack( self.assertEqual(PRIVATE_V4, srv['private_v4']) self.assert_calls() - @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_volumes') - @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_image_name') - @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_flavor_name') + @mock.patch.object(connection.Connection, 'get_volumes') + @mock.patch.object(connection.Connection, 'get_image_name') + @mock.patch.object(connection.Connection, 'get_flavor_name') def test_get_server_private_ip_no_fip( self, mock_get_flavor_name, mock_get_image_name, @@ -468,9 +468,9 @@ def test_get_server_private_ip_no_fip( self.assertEqual(PRIVATE_V4, srv['private_v4']) self.assert_calls() - @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_volumes') - @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_image_name') - @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_flavor_name') + @mock.patch.object(connection.Connection, 'get_volumes') + @mock.patch.object(connection.Connection, 'get_image_name') + @mock.patch.object(connection.Connection, 'get_flavor_name') def test_get_server_cloud_no_fips( self, mock_get_flavor_name, mock_get_image_name, @@ -516,10 +516,10 @@ def test_get_server_cloud_no_fips( self.assertEqual(PRIVATE_V4, srv['private_v4']) self.assert_calls() - @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'has_service') - @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_volumes') - @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_image_name') - @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_flavor_name') + @mock.patch.object(connection.Connection, 'has_service') + @mock.patch.object(connection.Connection, 'get_volumes') + @mock.patch.object(connection.Connection, 'get_image_name') + @mock.patch.object(connection.Connection, 'get_flavor_name') def test_get_server_cloud_missing_fips( self, mock_get_flavor_name, mock_get_image_name, @@ -585,9 +585,9 @@ def test_get_server_cloud_missing_fips( self.assertEqual(PUBLIC_V4, srv['public_v4']) self.assert_calls() - @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_volumes') - @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_image_name') - @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_flavor_name') + @mock.patch.object(connection.Connection, 'get_volumes') + @mock.patch.object(connection.Connection, 'get_image_name') + @mock.patch.object(connection.Connection, 'get_flavor_name') def test_get_server_cloud_rackspace_v6( self, mock_get_flavor_name, mock_get_image_name, mock_get_volumes): @@ -635,9 +635,9 @@ def test_get_server_cloud_rackspace_v6( "2001:4800:7819:103:be76:4eff:fe05:8525", srv['interface_ip']) self.assert_calls() - @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_volumes') - @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_image_name') - @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'get_flavor_name') + @mock.patch.object(connection.Connection, 'get_volumes') + @mock.patch.object(connection.Connection, 'get_image_name') + @mock.patch.object(connection.Connection, 'get_flavor_name') def test_get_server_cloud_osic_split( self, mock_get_flavor_name, mock_get_image_name, mock_get_volumes): diff --git a/openstack/tests/unit/cloud/test_shade.py b/openstack/tests/unit/cloud/test_shade.py index 5d95adbcb..c774cfa31 100644 --- a/openstack/tests/unit/cloud/test_shade.py +++ b/openstack/tests/unit/cloud/test_shade.py @@ -15,7 +15,6 @@ import testtools -from openstack.cloud import openstackcloud from openstack.cloud import exc from openstack import connection from openstack.tests import fakes @@ -63,7 +62,7 @@ def test_connect_as(self): # keystoneauth1.loading.base.BaseLoader.load_from_options self.cloud.connect_as(project_name='test_project') - @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'search_images') + @mock.patch.object(connection.Connection, 'search_images') def test_get_images(self, mock_search): image1 = dict(id='123', name='mickey') mock_search.return_value = [image1] @@ -71,7 +70,7 @@ def test_get_images(self, mock_search): self.assertIsNotNone(r) self.assertDictEqual(image1, r) - @mock.patch.object(openstackcloud._OpenStackCloudMixin, 'search_images') + @mock.patch.object(connection.Connection, 'search_images') def test_get_image_not_found(self, mock_search): mock_search.return_value = [] r = self.cloud.get_image('doesNotExist') From 7f2efadea78b4a23f73ce304c448354ebedc4ffa Mon Sep 17 00:00:00 2001 From: ITD27M01 Date: Wed, 27 Mar 2019 15:31:29 +0300 Subject: [PATCH 2417/3836] Adds missing "params" attribute for creating a Mistral workflow execution The v2 API of Mistral service have "params" attribute for creating workflow execution [1]. This attribute is a json object for workflow type-specific parameters. For example, the "env" (environment) dict can be used for installation specific inputs (dev/staging/etc..). We are using such env's to split environments but having the same logic in workflows definitions. [1] https://docs.openstack.org/mistral/latest/api/v2.html#executions Change-Id: Ic317b10c7b0053a8103ea2060491e437efc6898f Story: 2005232 Task: 30014 --- openstack/workflow/v2/execution.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openstack/workflow/v2/execution.py b/openstack/workflow/v2/execution.py index 5e8787ba9..9fa73c80c 100644 --- a/openstack/workflow/v2/execution.py +++ b/openstack/workflow/v2/execution.py @@ -43,6 +43,8 @@ class Execution(resource.Resource): #: A JSON structure containing workflow input values # TODO(briancurtin): type=dict input = resource.Body("input") + #: An optional JSON structure containing workflow type specific parameters + params = resource.Body("params") #: The output of the workflow output = resource.Body("output") #: The time at which the Execution was created From a0ec8ef7fdc7b0860f2922340655b5b730cd3123 Mon Sep 17 00:00:00 2001 From: Bharat Kunwar Date: Thu, 14 Mar 2019 00:16:30 +0000 Subject: [PATCH 2418/3836] Do not disregard tags when updating stacks At present, if tags are supplied to the update_stack function, it is completely disregarded while create_stack in fact allows it. This leads the Ansible module os_stack to also not be able to propagate tags properly. This is suboptimal therefore this patch aims to sort the issue out. Resolves: https://github.com/ansible/ansible/pull/53757 Change-Id: I59e7c47b8c01f96ec561b8bd153804bac5f21800 Story: 2005226 Task: 30008 --- openstack/cloud/_orchestration.py | 3 ++- openstack/tests/unit/cloud/test_stack.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/openstack/cloud/_orchestration.py b/openstack/cloud/_orchestration.py index bb3343ad6..95b01320b 100644 --- a/openstack/cloud/_orchestration.py +++ b/openstack/cloud/_orchestration.py @@ -110,7 +110,7 @@ def update_stack( self, name_or_id, template_file=None, template_url=None, template_object=None, files=None, - rollback=True, + rollback=True, tags=None, wait=False, timeout=3600, environment_files=None, **parameters): @@ -146,6 +146,7 @@ def update_stack( files=files) params = dict( disable_rollback=not rollback, + tags=tags, parameters=parameters, template=template, files=dict(list(tpl_files.items()) + list(envfiles.items())), diff --git a/openstack/tests/unit/cloud/test_stack.py b/openstack/tests/unit/cloud/test_stack.py index 08c856daa..20ce23773 100644 --- a/openstack/tests/unit/cloud/test_stack.py +++ b/openstack/tests/unit/cloud/test_stack.py @@ -425,6 +425,7 @@ def test_update_stack(self): 'environment': {}, 'files': {}, 'parameters': {}, + 'tags': self.stack_tag, 'template': fakes.FAKE_TEMPLATE_CONTENT, 'timeout_mins': 60})), dict( @@ -446,6 +447,7 @@ def test_update_stack(self): ]) self.cloud.update_stack( self.stack_name, + tags=self.stack_tag, template_file=test_template.name) self.assert_calls() @@ -479,6 +481,7 @@ def test_update_stack_wait(self): 'environment': {}, 'files': {}, 'parameters': {}, + 'tags': self.stack_tag, 'template': fakes.FAKE_TEMPLATE_CONTENT, 'timeout_mins': 60})), dict( @@ -512,6 +515,7 @@ def test_update_stack_wait(self): ]) self.cloud.update_stack( self.stack_name, + tags=self.stack_tag, template_file=test_template.name, wait=True) From 9ace77e69eaf6f687ede06abdfa56718cbed2c8f Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Tue, 26 Mar 2019 13:45:35 +0100 Subject: [PATCH 2419/3836] Make PATCH a first class operation and support it for baremetal The existing pattern of working with ironicclient is to use JSON patch to update resources. To simplify migration let us support it as well. Patching is also useful for granular updating of nested structures. Deprecate two openstackcloud calls that are meaningless wrappers on top of patch_machine/update_machine. Change-Id: Idffaf8947f51e5854461808d9d42c576640bec56 --- doc/source/user/proxies/baremetal.rst | 4 + openstack/baremetal/v1/_proxy.py | 62 ++++++++- openstack/baremetal/v1/chassis.py | 1 + openstack/baremetal/v1/node.py | 1 + openstack/baremetal/v1/port.py | 1 + openstack/baremetal/v1/port_group.py | 1 + openstack/cloud/_baremetal.py | 34 ++--- openstack/resource.py | 121 ++++++++++++++---- .../baremetal/test_baremetal_chassis.py | 10 ++ .../baremetal/test_baremetal_node.py | 29 +++++ .../baremetal/test_baremetal_port.py | 13 ++ .../baremetal/test_baremetal_port_group.py | 10 ++ .../tests/unit/cloud/test_baremetal_node.py | 5 - openstack/tests/unit/test_resource.py | 54 ++++++++ .../baremetal-patch-feebd96b1b92f3b9.yaml | 13 ++ 15 files changed, 304 insertions(+), 55 deletions(-) create mode 100644 releasenotes/notes/baremetal-patch-feebd96b1b92f3b9.yaml diff --git a/doc/source/user/proxies/baremetal.rst b/doc/source/user/proxies/baremetal.rst index d877f400a..7c740d92c 100644 --- a/doc/source/user/proxies/baremetal.rst +++ b/doc/source/user/proxies/baremetal.rst @@ -18,6 +18,7 @@ Node Operations .. automethod:: openstack.baremetal.v1._proxy.Proxy.create_node .. automethod:: openstack.baremetal.v1._proxy.Proxy.update_node + .. automethod:: openstack.baremetal.v1._proxy.Proxy.patch_node .. automethod:: openstack.baremetal.v1._proxy.Proxy.delete_node .. automethod:: openstack.baremetal.v1._proxy.Proxy.get_node .. automethod:: openstack.baremetal.v1._proxy.Proxy.find_node @@ -36,6 +37,7 @@ Port Operations .. automethod:: openstack.baremetal.v1._proxy.Proxy.create_port .. automethod:: openstack.baremetal.v1._proxy.Proxy.update_port + .. automethod:: openstack.baremetal.v1._proxy.Proxy.patch_port .. automethod:: openstack.baremetal.v1._proxy.Proxy.delete_port .. automethod:: openstack.baremetal.v1._proxy.Proxy.get_port .. automethod:: openstack.baremetal.v1._proxy.Proxy.find_port @@ -47,6 +49,7 @@ Port Group Operations .. automethod:: openstack.baremetal.v1._proxy.Proxy.create_port_group .. automethod:: openstack.baremetal.v1._proxy.Proxy.update_port_group + .. automethod:: openstack.baremetal.v1._proxy.Proxy.patch_port_group .. automethod:: openstack.baremetal.v1._proxy.Proxy.delete_port_group .. automethod:: openstack.baremetal.v1._proxy.Proxy.get_port_group .. automethod:: openstack.baremetal.v1._proxy.Proxy.find_port_group @@ -65,6 +68,7 @@ Chassis Operations .. automethod:: openstack.baremetal.v1._proxy.Proxy.create_chassis .. automethod:: openstack.baremetal.v1._proxy.Proxy.update_chassis + .. automethod:: openstack.baremetal.v1._proxy.Proxy.patch_chassis .. automethod:: openstack.baremetal.v1._proxy.Proxy.delete_chassis .. automethod:: openstack.baremetal.v1._proxy.Proxy.get_chassis .. automethod:: openstack.baremetal.v1._proxy.Proxy.find_chassis diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index e24b2cf43..2f95cc180 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -114,6 +114,18 @@ def update_chassis(self, chassis, **attrs): """ return self._update(_chassis.Chassis, chassis, **attrs) + def patch_chassis(self, chassis, patch): + """Apply a JSON patch to the chassis. + + :param chassis: The value can be the ID of a chassis or a + :class:`~openstack.baremetal.v1.chassis.Chassis` instance. + :param patch: JSON patch to apply. + + :returns: The updated chassis. + :rtype: :class:`~openstack.baremetal.v1.chassis.Chassis` + """ + return self._get_resource(_chassis.Chassis, chassis).patch(self, patch) + def delete_chassis(self, chassis, ignore_missing=True): """Delete a chassis. @@ -246,8 +258,8 @@ def get_node(self, node): def update_node(self, node, retry_on_conflict=True, **attrs): """Update a node. - :param chassis: Either the name or the ID of a node or an instance - of :class:`~openstack.baremetal.v1.node.Node`. + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. :param bool retry_on_conflict: Whether to retry HTTP CONFLICT error. Most of the time it can be retried, since it is caused by the node being locked. However, when setting ``instance_id``, this is @@ -261,6 +273,23 @@ def update_node(self, node, retry_on_conflict=True, **attrs): res = self._get_resource(_node.Node, node, **attrs) return res.commit(self, retry_on_conflict=retry_on_conflict) + def patch_node(self, node, patch, retry_on_conflict=True): + """Apply a JSON patch to the node. + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :param patch: JSON patch to apply. + :param bool retry_on_conflict: Whether to retry HTTP CONFLICT error. + Most of the time it can be retried, since it is caused by the node + being locked. However, when setting ``instance_id``, this is + a normal code and should not be retried. + + :returns: The updated node. + :rtype: :class:`~openstack.baremetal.v1.node.Node` + """ + res = self._get_resource(_node.Node, node) + return res.patch(self, patch, retry_on_conflict=retry_on_conflict) + def set_node_provision_state(self, node, target, config_drive=None, clean_steps=None, rescue_password=None, wait=False, timeout=None): @@ -527,7 +556,7 @@ def get_port(self, port, **query): def update_port(self, port, **attrs): """Update a port. - :param chassis: Either the ID of a port or an instance + :param port: Either the ID of a port or an instance of :class:`~openstack.baremetal.v1.port.Port`. :param dict attrs: The attributes to update on the port represented by the ``port`` parameter. @@ -537,6 +566,18 @@ def update_port(self, port, **attrs): """ return self._update(_port.Port, port, **attrs) + def patch_port(self, port, patch): + """Apply a JSON patch to the port. + + :param port: The value can be the ID of a port or a + :class:`~openstack.baremetal.v1.port.Port` instance. + :param patch: JSON patch to apply. + + :returns: The updated port. + :rtype: :class:`~openstack.baremetal.v1.port.Port` + """ + return self._get_resource(_port.Port, port).patch(self, patch) + def delete_port(self, port, ignore_missing=True): """Delete a port. @@ -638,7 +679,7 @@ def get_port_group(self, port_group, **query): def update_port_group(self, port_group, **attrs): """Update a port group. - :param chassis: Either the name or the ID of a port group or + :param port_group: Either the name or the ID of a port group or an instance of :class:`~openstack.baremetal.v1.port_group.PortGroup`. :param dict attrs: The attributes to update on the port group @@ -649,6 +690,19 @@ def update_port_group(self, port_group, **attrs): """ return self._update(_portgroup.PortGroup, port_group, **attrs) + def patch_port_group(self, port_group, patch): + """Apply a JSON patch to the port_group. + + :param port_group: The value can be the ID of a port group or a + :class:`~openstack.baremetal.v1.port_group.PortGroup` instance. + :param patch: JSON patch to apply. + + :returns: The updated port group. + :rtype: :class:`~openstack.baremetal.v1.port_group.PortGroup` + """ + res = self._get_resource(_portgroup.PortGroup, port_group) + return res.patch(self, patch) + def delete_port_group(self, port_group, ignore_missing=True): """Delete a port group. diff --git a/openstack/baremetal/v1/chassis.py b/openstack/baremetal/v1/chassis.py index 953b1fd6b..89ac5c571 100644 --- a/openstack/baremetal/v1/chassis.py +++ b/openstack/baremetal/v1/chassis.py @@ -28,6 +28,7 @@ class Chassis(_common.ListMixin, resource.Resource): allow_commit = True allow_delete = True allow_list = True + allow_patch = True commit_method = 'PATCH' commit_jsonpatch = True diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 21c41d3e3..ad2d3c74e 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -45,6 +45,7 @@ class Node(_common.ListMixin, resource.Resource): allow_commit = True allow_delete = True allow_list = True + allow_patch = True commit_method = 'PATCH' commit_jsonpatch = True diff --git a/openstack/baremetal/v1/port.py b/openstack/baremetal/v1/port.py index f77e70249..338c29e01 100644 --- a/openstack/baremetal/v1/port.py +++ b/openstack/baremetal/v1/port.py @@ -25,6 +25,7 @@ class Port(_common.ListMixin, resource.Resource): allow_commit = True allow_delete = True allow_list = True + allow_patch = True commit_method = 'PATCH' commit_jsonpatch = True diff --git a/openstack/baremetal/v1/port_group.py b/openstack/baremetal/v1/port_group.py index a6419fed8..939ad78bf 100644 --- a/openstack/baremetal/v1/port_group.py +++ b/openstack/baremetal/v1/port_group.py @@ -25,6 +25,7 @@ class PortGroup(_common.ListMixin, resource.Resource): allow_commit = True allow_delete = True allow_list = True + allow_patch = True commit_method = 'PATCH' commit_jsonpatch = True diff --git a/openstack/cloud/_baremetal.py b/openstack/cloud/_baremetal.py index 99d1126cb..6c05b8d7d 100644 --- a/openstack/cloud/_baremetal.py +++ b/openstack/cloud/_baremetal.py @@ -434,17 +434,8 @@ def patch_machine(self, name_or_id, patch): :returns: ``munch.Munch`` representing the newly updated node. """ - node = self.baremetal.get_node(name_or_id) - microversion = node._get_microversion_for(self._baremetal_client, - 'commit') - msg = ("Error updating machine via patch operation on node " - "{node}".format(node=name_or_id)) - url = '/nodes/{node_id}'.format(node_id=node.id) return self._normalize_machine( - self._baremetal_client.patch(url, - json=patch, - microversion=microversion, - error_message=msg)) + self.baremetal.patch_node(name_or_id, patch)) def update_machine(self, name_or_id, **attrs): """Update a machine with new configuration information @@ -687,22 +678,17 @@ def deactivate_node(self, uuid, wait=False, uuid, 'deleted', wait=wait, timeout=timeout) def set_node_instance_info(self, uuid, patch): - msg = ("Error updating machine via patch operation on node " - "{node}".format(node=uuid)) - url = '/nodes/{node_id}'.format(node_id=uuid) - return self._baremetal_client.patch(url, - json=patch, - error_message=msg) + warnings.warn("The set_node_instance_info call is deprecated, " + "use patch_machine or update_machine instead", + DeprecationWarning) + return self.patch_machine(uuid, patch) def purge_node_instance_info(self, uuid): - patch = [] - patch.append({'op': 'remove', 'path': '/instance_info'}) - msg = ("Error updating machine via patch operation on node " - "{node}".format(node=uuid)) - url = '/nodes/{node_id}'.format(node_id=uuid) - return self._baremetal_client.patch(url, - json=patch, - error_message=msg) + warnings.warn("The purge_node_instance_info call is deprecated, " + "use patch_machine or update_machine instead", + DeprecationWarning) + return self.patch_machine(uuid, + dict(path='/instance_info', op='remove')) def wait_for_baremetal_node_lock(self, node, timeout=30): """Wait for a baremetal node to have no lock. diff --git a/openstack/resource.py b/openstack/resource.py index acd227301..fb88d2087 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -387,6 +387,8 @@ class Resource(dict): allow_list = False #: Allow head operation for this resource. allow_head = False + #: Allow patch operation for this resource. + allow_patch = False # TODO(mordred) Unused - here for transition with OSC. Remove once # OSC no longer checks for allow_get @@ -455,7 +457,7 @@ def __init__(self, _synchronized=False, connection=None, **attrs): self._computed = _ComponentManager( attributes=computed, synchronized=_synchronized) - if self.commit_jsonpatch: + if self.commit_jsonpatch or self.allow_patch: # We need the original body to compare against if _synchronized: self._original_body = self._body.attributes.copy() @@ -698,7 +700,7 @@ def _consume_attrs(self, mapping, attrs): def _clean_body_attrs(self, attrs): """Mark the attributes as up-to-date.""" self._body.clean(only=attrs) - if self.commit_jsonpatch: + if self.commit_jsonpatch or self.allow_patch: for attr in attrs: if attr in self._body: self._original_body[attr] = self._body[attr] @@ -976,7 +978,7 @@ def _translate_response(self, response, has_body=None, error_message=None): body = self._consume_body_attrs(body) self._body.attributes.update(body) self._body.clean() - if self.commit_jsonpatch: + if self.commit_jsonpatch or self.allow_patch: # We need the original body to compare against self._original_body = body.copy() @@ -1037,11 +1039,11 @@ def _get_microversion_for(self, session, action): Subclasses can override this method if more complex logic is needed. :param session: :class`keystoneauth1.adapter.Adapter` - :param action: One of "fetch", "commit", "create", "delete". Unused in - the base implementation. + :param action: One of "fetch", "commit", "create", "delete", "patch". + Unused in the base implementation. :return: microversion as string or ``None`` """ - if action not in ('fetch', 'commit', 'create', 'delete'): + if action not in ('fetch', 'commit', 'create', 'delete', 'patch'): raise ValueError('Invalid action: %s' % action) return self._get_microversion_for_list(session) @@ -1234,6 +1236,14 @@ def commit(self, session, prepend_key=True, has_body=True, request = self._prepare_request(prepend_key=prepend_key, base_path=base_path, **kwargs) + microversion = self._get_microversion_for(session, 'commit') + + return self._commit(session, request, self.commit_method, microversion, + has_body=has_body, + retry_on_conflict=retry_on_conflict) + + def _commit(self, session, request, method, microversion, has_body=True, + retry_on_conflict=None): session = self._get_session(session) kwargs = {} @@ -1245,28 +1255,95 @@ def commit(self, session, prepend_key=True, has_body=True, # overriding it via an explicit retry_on_conflict=False. kwargs['retriable_status_codes'] = retriable_status_codes - {409} - microversion = self._get_microversion_for(session, 'commit') - - if self.commit_method == 'PATCH': - response = session.patch( - request.url, json=request.body, headers=request.headers, - microversion=microversion, **kwargs) - elif self.commit_method == 'POST': - response = session.post( - request.url, json=request.body, headers=request.headers, - microversion=microversion, **kwargs) - elif self.commit_method == 'PUT': - response = session.put( - request.url, json=request.body, headers=request.headers, - microversion=microversion, **kwargs) - else: + try: + call = getattr(session, method.lower()) + except AttributeError: raise exceptions.ResourceFailure( - msg="Invalid commit method: %s" % self.commit_method) + msg="Invalid commit method: %s" % method) + + response = call(request.url, json=request.body, + headers=request.headers, microversion=microversion, + **kwargs) self.microversion = microversion self._translate_response(response, has_body=has_body) return self + def _convert_patch(self, patch): + if not isinstance(patch, list): + patch = [patch] + + converted = [] + for item in patch: + try: + path = item['path'] + parts = path.lstrip('/').split('/', 1) + field = parts[0] + except (KeyError, IndexError): + raise ValueError("Malformed or missing path in %s" % item) + + try: + component = getattr(self.__class__, field) + except AttributeError: + server_field = field + else: + server_field = component.name + + if len(parts) > 1: + new_path = '/%s/%s' % (server_field, parts[1]) + else: + new_path = '/%s' % server_field + converted.append(dict(item, path=new_path)) + + return converted + + def patch(self, session, patch=None, prepend_key=True, has_body=True, + retry_on_conflict=None, base_path=None): + """Patch the remote resource. + + Allows modifying the resource by providing a list of JSON patches to + apply to it. The patches can use both the original (server-side) and + SDK field names. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param patch: Additional JSON patch as a list or one patch item. + If provided, it is applied on top of any changes to the + current resource. + :param prepend_key: A boolean indicating whether the resource_key + should be prepended in a resource update request. + Default to True. + :param bool retry_on_conflict: Whether to enable retries on HTTP + CONFLICT (409). Value of ``None`` leaves + the `Adapter` defaults. + :param str base_path: Base part of the URI for modifying resources, if + different from + :data:`~openstack.resource.Resource.base_path`. + + :return: This :class:`Resource` instance. + :raises: :exc:`~openstack.exceptions.MethodNotSupported` if + :data:`Resource.allow_patch` is not set to ``True``. + """ + # The id cannot be dirty for an commit + self._body._dirty.discard("id") + + # Only try to update if we actually have anything to commit. + if not patch and not self.requires_commit: + return self + + if not self.allow_patch: + raise exceptions.MethodNotSupported(self, "patch") + + request = self._prepare_request(prepend_key=prepend_key, + base_path=base_path, patch=True) + microversion = self._get_microversion_for(session, 'patch') + if patch: + request.body += self._convert_patch(patch) + + return self._commit(session, request, 'PATCH', microversion, + has_body=has_body, + retry_on_conflict=retry_on_conflict) + def delete(self, session, error_message=None): """Delete the remote resource based on this instance. diff --git a/openstack/tests/functional/baremetal/test_baremetal_chassis.py b/openstack/tests/functional/baremetal/test_baremetal_chassis.py index 185f2d5c6..0274eca3f 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_chassis.py +++ b/openstack/tests/functional/baremetal/test_baremetal_chassis.py @@ -37,6 +37,16 @@ def test_chassis_update(self): chassis = self.conn.baremetal.get_chassis(chassis.id) self.assertEqual({'answer': 42}, chassis.extra) + def test_chassis_patch(self): + chassis = self.create_chassis() + + chassis = self.conn.baremetal.patch_chassis( + chassis, dict(path='/extra/answer', op='add', value=42)) + self.assertEqual({'answer': 42}, chassis.extra) + + chassis = self.conn.baremetal.get_chassis(chassis.id) + self.assertEqual({'answer': 42}, chassis.extra) + def test_chassis_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" self.assertRaises(exceptions.ResourceNotFound, diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index c38fbf52a..37bdbacc1 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -86,6 +86,35 @@ def test_node_update_by_name(self): node = self.conn.baremetal.get_node('node-name') self.assertIsNone(node.instance_id) + def test_node_patch(self): + node = self.create_node(name='node-name', extra={'foo': 'bar'}) + node.name = 'new-name' + instance_uuid = str(uuid.uuid4()) + + node = self.conn.baremetal.patch_node( + node, + [dict(path='/instance_id', op='replace', value=instance_uuid), + dict(path='/extra/answer', op='add', value=42)]) + self.assertEqual('new-name', node.name) + self.assertEqual({'foo': 'bar', 'answer': 42}, node.extra) + self.assertEqual(instance_uuid, node.instance_id) + + node = self.conn.baremetal.get_node('new-name') + self.assertEqual('new-name', node.name) + self.assertEqual({'foo': 'bar', 'answer': 42}, node.extra) + self.assertEqual(instance_uuid, node.instance_id) + + node = self.conn.baremetal.patch_node( + node, + [dict(path='/instance_id', op='remove'), + dict(path='/extra/answer', op='remove')]) + self.assertIsNone(node.instance_id) + self.assertNotIn('answer', node.extra) + + node = self.conn.baremetal.get_node('new-name') + self.assertIsNone(node.instance_id) + self.assertNotIn('answer', node.extra) + def test_node_list_update_delete(self): self.create_node(name='node-name', extra={'foo': 'bar'}) node = next(n for n in diff --git a/openstack/tests/functional/baremetal/test_baremetal_port.py b/openstack/tests/functional/baremetal/test_baremetal_port.py index 4dafaef74..5c41c0e8e 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_port.py +++ b/openstack/tests/functional/baremetal/test_baremetal_port.py @@ -77,6 +77,19 @@ def test_port_update(self): self.assertEqual('66:55:44:33:22:11', port.address) self.assertEqual({'answer': 42}, port.extra) + def test_port_patch(self): + port = self.create_port(address='11:22:33:44:55:66') + port.address = '66:55:44:33:22:11' + + port = self.conn.baremetal.patch_port( + port, dict(path='/extra/answer', op='add', value=42)) + self.assertEqual('66:55:44:33:22:11', port.address) + self.assertEqual({'answer': 42}, port.extra) + + port = self.conn.baremetal.get_port(port.id) + self.assertEqual('66:55:44:33:22:11', port.address) + self.assertEqual({'answer': 42}, port.extra) + def test_port_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" self.assertRaises(exceptions.ResourceNotFound, diff --git a/openstack/tests/functional/baremetal/test_baremetal_port_group.py b/openstack/tests/functional/baremetal/test_baremetal_port_group.py index 24b8b3d75..bd74db5bd 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_port_group.py +++ b/openstack/tests/functional/baremetal/test_baremetal_port_group.py @@ -72,6 +72,16 @@ def test_port_group_update(self): port_group = self.conn.baremetal.get_port_group(port_group.id) self.assertEqual({'answer': 42}, port_group.extra) + def test_port_group_patch(self): + port_group = self.create_port_group() + + port_group = self.conn.baremetal.patch_port_group( + port_group, dict(path='/extra/answer', op='add', value=42)) + self.assertEqual({'answer': 42}, port_group.extra) + + port_group = self.conn.baremetal.get_port_group(port_group.id) + self.assertEqual({'answer': 42}, port_group.extra) + def test_port_group_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" self.assertRaises(exceptions.ResourceNotFound, diff --git a/openstack/tests/unit/cloud/test_baremetal_node.py b/openstack/tests/unit/cloud/test_baremetal_node.py index 61882ea48..f5aa18519 100644 --- a/openstack/tests/unit/cloud/test_baremetal_node.py +++ b/openstack/tests/unit/cloud/test_baremetal_node.py @@ -206,11 +206,6 @@ def test_patch_machine(self): 'path': '/instance_info'}] self.fake_baremetal_node['instance_info'] = {} self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), dict(method='PATCH', uri=self.get_mock_url( resource='nodes', diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 7b44a60d3..ffb84a420 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1336,6 +1336,60 @@ def test_commit_not_dirty(self): self.session.put.assert_not_called() + def test_patch_with_sdk_names(self): + class Test(resource.Resource): + allow_patch = True + + id = resource.Body('id') + attr = resource.Body('attr') + nested = resource.Body('renamed') + other = resource.Body('other') + + test_patch = [{'path': '/attr', 'op': 'replace', 'value': 'new'}, + {'path': '/nested/dog', 'op': 'remove'}, + {'path': '/nested/cat', 'op': 'add', 'value': 'meow'}] + expected = [{'path': '/attr', 'op': 'replace', 'value': 'new'}, + {'path': '/renamed/dog', 'op': 'remove'}, + {'path': '/renamed/cat', 'op': 'add', 'value': 'meow'}] + sot = Test.existing(id=1, attr=42, nested={'dog': 'bark'}) + sot.patch(self.session, test_patch) + self.session.patch.assert_called_once_with( + '/1', json=expected, headers=mock.ANY, microversion=None) + + def test_patch_with_server_names(self): + class Test(resource.Resource): + allow_patch = True + + id = resource.Body('id') + attr = resource.Body('attr') + nested = resource.Body('renamed') + other = resource.Body('other') + + test_patch = [{'path': '/attr', 'op': 'replace', 'value': 'new'}, + {'path': '/renamed/dog', 'op': 'remove'}, + {'path': '/renamed/cat', 'op': 'add', 'value': 'meow'}] + sot = Test.existing(id=1, attr=42, nested={'dog': 'bark'}) + sot.patch(self.session, test_patch) + self.session.patch.assert_called_once_with( + '/1', json=test_patch, headers=mock.ANY, microversion=None) + + def test_patch_with_changed_fields(self): + class Test(resource.Resource): + allow_patch = True + + attr = resource.Body('attr') + nested = resource.Body('renamed') + other = resource.Body('other') + + sot = Test.existing(id=1, attr=42, nested={'dog': 'bark'}) + sot.attr = 'new' + sot.patch(self.session, {'path': '/renamed/dog', 'op': 'remove'}) + + expected = [{'path': '/attr', 'op': 'replace', 'value': 'new'}, + {'path': '/renamed/dog', 'op': 'remove'}] + self.session.patch.assert_called_once_with( + '/1', json=expected, headers=mock.ANY, microversion=None) + def test_delete(self): result = self.sot.delete(self.session) diff --git a/releasenotes/notes/baremetal-patch-feebd96b1b92f3b9.yaml b/releasenotes/notes/baremetal-patch-feebd96b1b92f3b9.yaml new file mode 100644 index 000000000..bcf4edb3f --- /dev/null +++ b/releasenotes/notes/baremetal-patch-feebd96b1b92f3b9.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + Adds support for changing bare metal resources by providing a JSON patch. + Adds the following calls to the bare metal proxy: ``patch_node``, + ``patch_port``, ``patch_port_group`` and ``patch_chassis``. +deprecations: + - | + The ``set_node_instance_info`` call is deprecated, use ``patch_machine`` + with the same arguments instead. + - | + The ``purge_node_instance_info`` call is deprecated, use ``patch_machine`` + or ``update_machine`` instead. From 7f0a60871dae01b70c722cd3b31122c1cd4fcba4 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 28 Mar 2019 16:15:28 +0100 Subject: [PATCH 2420/3836] Cleanup split of openstackcloud A follow-up on 642218 with comments removal and moving few remaining methods to _compute Change-Id: Ic4922967485d48e6d0714daaac09a8184732b9ef --- openstack/cloud/_compute.py | 9 +++++ openstack/cloud/openstackcloud.py | 56 +------------------------------ 2 files changed, 10 insertions(+), 55 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 522a5d542..abd511fc9 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -1915,3 +1915,12 @@ def _encode_server_userdata(self, userdata): # Once we have base64 bytes, make them into a utf-8 string for REST return base64.b64encode(userdata).decode('utf-8') + + def get_openstack_vars(self, server): + return meta.get_hostvars_from_server(self, server) + + def _expand_server_vars(self, server): + # Used by nodepool + # TODO(mordred) remove after these make it into what we + # actually want the API to be. + return meta.expand_server_vars(self, server) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 8d65b0f00..010e51945 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -12,7 +12,6 @@ import copy import functools import six -# import threading # import types so that we can reference ListType in sphinx param declarations. # We can't just use list, because sphinx gets confused by # openstack.resource.Resource.list and openstack.resource2.Resource.list @@ -28,11 +27,9 @@ import keystoneauth1.session from openstack import _log -# from openstack import exceptions from openstack.cloud import exc from openstack.cloud import _floating_ip from openstack.cloud import _object_store -# from openstack.cloud import _normalize from openstack.cloud import meta from openstack.cloud import _utils import openstack.config @@ -92,33 +89,10 @@ def __init__(self): self.name = self.config.name self.auth = self.config.get_auth_args() self.default_interface = self.config.get_interface() - # self.private = self.config.config.get('private', False) - # self.image_api_use_tasks = self.config.config['image_api_use_tasks'] - # self.secgroup_source = self.config.config['secgroup_source'] self.force_ipv4 = self.config.force_ipv4 - # self._external_ipv4_names = self.config.get_external_ipv4_networks() - # self._internal_ipv4_names = self.config.get_internal_ipv4_networks() - # self._external_ipv6_names = self.config.get_external_ipv6_networks() - # self._internal_ipv6_names = self.config.get_internal_ipv6_networks() - # self._nat_destination = self.config.get_nat_destination() - # self._nat_source = self.config.get_nat_source() - # self._default_network = self.config.get_default_network() - - # self._floating_ip_source = self.config.config.get( - # 'floating_ip_source') - # if self._floating_ip_source: - # if self._floating_ip_source.lower() == 'none': - # self._floating_ip_source = None - # else: - # self._floating_ip_source = self._floating_ip_source.lower() - - # self._use_external_network = self.config.config.get( - # 'use_external_network', True) - # self._use_internal_network = self.config.config.get( - # 'use_internal_network', True) - (self.verify, self.cert) = self.config.get_requests_verify_args() + # Turn off urllib3 warnings about insecure certs if we have # explicitly configured requests to tell it we do not want # cert verification @@ -132,25 +106,6 @@ def __init__(self): self._disable_warnings = {} - # self._servers = None - # self._servers_time = 0 - # self._servers_lock = threading.Lock() - - # self._ports = None - # self._ports_time = 0 - # self._ports_lock = threading.Lock() - - # self._floating_ips = None - # self._floating_ips_time = 0 - # self._floating_ips_lock = threading.Lock() - # - # self._floating_network_by_router = None - # self._floating_network_by_router_run = False - # self._floating_network_by_router_lock = threading.Lock() - - # self._networks_lock = threading.Lock() - # self._reset_network_caches() - cache_expiration_time = int(self.config.get_cache_expiration_time()) cache_class = self.config.get_cache_class() cache_arguments = self.config.get_cache_arguments() @@ -742,12 +697,3 @@ def has_service(self, service_key): return True else: return False - - def get_openstack_vars(self, server): - return meta.get_hostvars_from_server(self, server) - - def _expand_server_vars(self, server): - # Used by nodepool - # TODO(mordred) remove after these make it into what we - # actually want the API to be. - return meta.expand_server_vars(self, server) From 3fe09b84e995d53ed17ba5f07e3a3767cfccae03 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 29 Mar 2019 15:35:26 +0100 Subject: [PATCH 2421/3836] Allow replacing service implementation Some clouds are offering their own incompatible implementation of system services. While with add_service we can add new implementation of the service, we can not replace system-ones. So let's allow deleting service to be able to have a custom service implementation in SDK. Change-Id: I55ef310d114702e5dc98a237fc1d8d130c09c5e1 --- openstack/service_description.py | 11 ++++++++++- openstack/tests/unit/test_connection.py | 26 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/openstack/service_description.py b/openstack/service_description.py index bfe46a7f5..617bf7b56 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -202,4 +202,13 @@ def __set__(self, instance, value): raise AttributeError('Service Descriptors cannot be set') def __delete__(self, instance): - raise AttributeError('Service Descriptors cannot be deleted') + # NOTE(gtema) Some clouds are not very fast (or interested at all) + # in bringing their changes upstream. If there are incompatible changes + # downstream we need to allow overriding default implementation by + # deleting service_type attribute of the connection and then + # "add_service" with new implementation. + # This is implemented explicitely not very comfortable to use + # to show how bad it is not to contribute changes back + for service_type in self.all_types: + if service_type in instance._proxies: + del instance._proxies[service_type] diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index ce2320cc6..ef4117f96 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -308,3 +308,29 @@ def test_add_service_v2(self): 'openstack.tests.unit.fake.v2._proxy', conn.fake.__class__.__module__) self.assertFalse(conn.fake.dummy()) + + def test_replace_system_service(self): + self.use_keystone_v3(catalog='catalog-v3-fake-v2.json') + conn = self.cloud + + # delete native dns service + delattr(conn, 'dns') + + self.register_uris([ + dict(method='GET', + uri='https://fake.example.com', + status_code=404), + dict(method='GET', + uri='https://fake.example.com/v2/', + status_code=404), + dict(method='GET', + uri=self.get_mock_url('fake'), + status_code=404), + ]) + + # add fake service with alias 'DNS' + service = fake_service.FakeService('fake', aliases=['dns']) + conn.add_service(service) + + # ensure dns service responds as we expect from replacement + self.assertFalse(conn.dns.dummy()) From fdd364ee81af166521abf5931f3c7db03ef0ebde Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 27 Feb 2019 17:43:03 +0000 Subject: [PATCH 2422/3836] Add support for generating form-post signatures swift has the capability of allowing limited access to upload objects via a pre-generated signature: https://docs.openstack.org/swift/latest/api/form_post_middleware.html Add methods to support setting keys as well as generating the timestamp and signature needed to use this. Change-Id: Iab2fb5c225d0c8e79a16130f2352de1efd6cad4b --- openstack/object_store/v1/_proxy.py | 123 ++++++++++++- openstack/object_store/v1/container.py | 6 + openstack/tests/unit/cloud/test_object.py | 174 ++++++++++++++++++ .../unit/object_store/v1/test_container.py | 2 + ...erate-form-signature-294ca46812f291d6.yaml | 5 + 5 files changed, 308 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/generate-form-signature-294ca46812f291d6.yaml diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index dc4800ebd..57637e8c6 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -9,10 +9,15 @@ # 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 collections -import os +from hashlib import sha1 +import hmac import json +import os +import time + +import six +from six.moves.urllib import parse from openstack.object_store.v1 import account as _account from openstack.object_store.v1 import container as _container @@ -639,3 +644,117 @@ def get_info(self): include metadata about maximum values and thresholds. """ return self._get(_info.Info) + + def set_account_temp_url_key(self, key, secondary=False): + """Set the temporary URL key for the account. + + :param key: + Text of the key to use. + :param bool secondary: + Whether this should set the secondary key. (defaults to False) + """ + header = 'Temp-URL-Key' + if secondary: + header += '-2' + + return self.set_account_metadata(**{header: key}) + + def set_container_temp_url_key(self, container, key, secondary=False): + """Set the temporary URL key for a container. + + :param container: + The value can be the name of a container or a + :class:`~openstack.object_store.v1.container.Container` instance. + :param key: + Text of the key to use. + :param bool secondary: + Whether this should set the secondary key. (defaults to False) + """ + header = 'Temp-URL-Key' + if secondary: + header += '-2' + + return self.set_container_metadata(container, **{header: key}) + + def get_temp_url_key(self, container=None): + """Get the best temporary url key for a given container. + + Will first try to return Temp-URL-Key-2 then Temp-URL-Key for + the container, and if neither exist, will attempt to return + Temp-URL-Key-2 then Temp-URL-Key for the account. If neither + exist, will return None. + + :param container: + The value can be the name of a container or a + :class:`~openstack.object_store.v1.container.Container` instance. + """ + temp_url_key = None + if container: + container_meta = self.get_container_metadata(container) + temp_url_key = (container_meta.meta_temp_url_key_2 + or container_meta.meta_temp_url_key) + if not temp_url_key: + account_meta = self.get_account_metadata() + temp_url_key = (account_meta.meta_temp_url_key_2 + or account_meta.meta_temp_url_key) + if temp_url_key and not isinstance(temp_url_key, six.binary_type): + temp_url_key = temp_url_key.encode('utf8') + return temp_url_key + + def generate_form_signature( + self, container, object_prefix, redirect_url, max_file_size, + max_upload_count, timeout, temp_url_key=None): + """Generate a signature for a FormPost upload. + + :param container: + The value can be the name of a container or a + :class:`~openstack.object_store.v1.container.Container` instance. + :param object_prefix: + Prefix to apply to limit all object names created using this + signature. + :param redirect_url: + The URL to redirect the browser to after the uploads have + completed. + :param max_file_size: + The maximum file size per file uploaded. + :param max_upload_count: + The maximum number of uploaded files allowed. + :param timeout: + The number of seconds from now to allow the form post to begin. + :param temp_url_key: + The X-Account-Meta-Temp-URL-Key for the account. Optional, if + omitted, the key will be fetched from the container or the account. + """ + max_file_size = int(max_file_size) + if max_file_size < 1: + raise exceptions.SDKException( + 'Please use a positive max_file_size value.') + max_upload_count = int(max_upload_count) + if max_upload_count < 1: + raise exceptions.SDKException( + 'Please use a positive max_upload_count value.') + if timeout < 1: + raise exceptions.SDKException( + 'Please use a positive value.') + expires = int(time.time() + int(timeout)) + if temp_url_key: + if not isinstance(temp_url_key, six.binary_type): + temp_url_key = temp_url_key.encode('utf8') + else: + temp_url_key = self.get_temp_url_key(container) + if not temp_url_key: + raise exceptions.SDKException( + 'temp_url_key was not given, nor was a temporary url key' + ' found for the account or the container.') + + res = self._get_resource(_container.Container, container) + endpoint = parse.urlparse(self.get_endpoint()) + path = '/'.join([endpoint.path, res.name, object_prefix]) + + data = '%s\n%s\n%s\n%s\n%s' % (path, redirect_url, max_file_size, + max_upload_count, expires) + if six.PY3: + data = data.encode('utf8') + sig = hmac.new(temp_url_key, data, sha1).hexdigest() + + return (expires, sig) diff --git a/openstack/object_store/v1/container.py b/openstack/object_store/v1/container.py index 1275b4a54..69f2e1a4e 100644 --- a/openstack/object_store/v1/container.py +++ b/openstack/object_store/v1/container.py @@ -98,6 +98,12 @@ class Container(_base.BaseResource): #: "If-None-Match: \*" header to query whether the server already #: has a copy of the object before any data is sent. if_none_match = resource.Header("if-none-match") + #: The secret key value for temporary URLs. If not set, + #: this header is not returned by this operation. + meta_temp_url_key = resource.Header("x-container-meta-temp-url-key") + #: A second secret key value for temporary URLs. If not set, + #: this header is not returned by this operation. + meta_temp_url_key_2 = resource.Header("x-container-meta-temp-url-key-2") @classmethod def new(cls, **kwargs): diff --git a/openstack/tests/unit/cloud/test_object.py b/openstack/tests/unit/cloud/test_object.py index 6eea2eb3d..7753670f1 100644 --- a/openstack/tests/unit/cloud/test_object.py +++ b/openstack/tests/unit/cloud/test_object.py @@ -14,11 +14,13 @@ import tempfile +import mock import testtools import openstack.cloud import openstack.cloud.openstackcloud as oc_oc from openstack.cloud import exc +from openstack import exceptions from openstack.tests.unit import base from openstack.object_store.v1 import _proxy @@ -264,6 +266,178 @@ def test_list_containers_exception(self): exc.OpenStackCloudException, self.cloud.list_containers) self.assert_calls() + @mock.patch('time.time', autospec=True) + def test_generate_form_signature_container_key(self, mock_time): + + mock_time.return_value = 12345 + + self.register_uris([ + dict(method='HEAD', uri=self.container_endpoint, + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'X-Container-Meta-Temp-Url-Key': 'amazingly-secure-key', + 'Content-Type': 'text/plain; charset=utf-8'}) + ]) + self.assertEqual( + (13345, '60731fb66d46c97cdcb79b6154363179c500b9d9'), + self.cloud.object_store.generate_form_signature( + self.container, + object_prefix='prefix/location', + redirect_url='https://example.com/location', + max_file_size=1024 * 1024 * 1024, + max_upload_count=10, timeout=1000, temp_url_key=None)) + self.assert_calls() + + @mock.patch('time.time', autospec=True) + def test_generate_form_signature_account_key(self, mock_time): + + mock_time.return_value = 12345 + + self.register_uris([ + dict(method='HEAD', uri=self.container_endpoint, + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}), + dict(method='HEAD', uri=self.endpoint + '/', + headers={ + 'X-Account-Meta-Temp-Url-Key': 'amazingly-secure-key'}), + ]) + self.assertEqual( + (13345, '3cb9bc83d5a4136421bb2c1f58b963740566646f'), + self.cloud.object_store.generate_form_signature( + self.container, + object_prefix='prefix/location', + redirect_url='https://example.com/location', + max_file_size=1024 * 1024 * 1024, + max_upload_count=10, timeout=1000, temp_url_key=None)) + self.assert_calls() + + @mock.patch('time.time') + def test_generate_form_signature_key_argument(self, mock_time): + + mock_time.return_value = 12345 + + self.assertEqual( + (13345, '1c283a05c6628274b732212d9a885265e6f67b63'), + self.cloud.object_store.generate_form_signature( + self.container, + object_prefix='prefix/location', + redirect_url='https://example.com/location', + max_file_size=1024 * 1024 * 1024, + max_upload_count=10, timeout=1000, + temp_url_key='amazingly-secure-key')) + self.assert_calls() + + def test_generate_form_signature_no_key(self): + + self.register_uris([ + dict(method='HEAD', uri=self.container_endpoint, + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8'}), + dict(method='HEAD', uri=self.endpoint + '/', + headers={}), + ]) + self.assertRaises( + exceptions.SDKException, + self.cloud.object_store.generate_form_signature, + self.container, + object_prefix='prefix/location', + redirect_url='https://example.com/location', + max_file_size=1024 * 1024 * 1024, + max_upload_count=10, timeout=1000, temp_url_key=None) + self.assert_calls() + + def test_set_account_temp_url_key(self): + + key = 'super-secure-key' + + self.register_uris([ + dict(method='POST', uri=self.endpoint + '/', + status_code=204, + validate=dict( + headers={ + 'x-account-meta-temp-url-key': key})), + dict(method='HEAD', uri=self.endpoint + '/', + headers={ + 'x-account-meta-temp-url-key': key}), + ]) + self.cloud.object_store.set_account_temp_url_key(key) + self.assert_calls() + + def test_set_account_temp_url_key_secondary(self): + + key = 'super-secure-key' + + self.register_uris([ + dict(method='POST', uri=self.endpoint + '/', + status_code=204, + validate=dict( + headers={ + 'x-account-meta-temp-url-key-2': key})), + dict(method='HEAD', uri=self.endpoint + '/', + headers={ + 'x-account-meta-temp-url-key-2': key}), + ]) + self.cloud.object_store.set_account_temp_url_key(key, secondary=True) + self.assert_calls() + + def test_set_container_temp_url_key(self): + + key = 'super-secure-key' + + self.register_uris([ + dict(method='POST', uri=self.container_endpoint, + status_code=204, + validate=dict( + headers={ + 'x-container-meta-temp-url-key': key})), + dict(method='HEAD', uri=self.container_endpoint, + headers={ + 'x-container-meta-temp-url-key': key}), + ]) + self.cloud.object_store.set_container_temp_url_key(self.container, key) + self.assert_calls() + + def test_set_container_temp_url_key_secondary(self): + + key = 'super-secure-key' + + self.register_uris([ + dict(method='POST', uri=self.container_endpoint, + status_code=204, + validate=dict( + headers={ + 'x-container-meta-temp-url-key-2': key})), + dict(method='HEAD', uri=self.container_endpoint, + headers={ + 'x-container-meta-temp-url-key-2': key}), + ]) + self.cloud.object_store.set_container_temp_url_key( + self.container, key, secondary=True) + self.assert_calls() + def test_list_objects(self): endpoint = '{endpoint}?format=json'.format( endpoint=self.container_endpoint) diff --git a/openstack/tests/unit/object_store/v1/test_container.py b/openstack/tests/unit/object_store/v1/test_container.py index 1821160bc..b88ce6bfd 100644 --- a/openstack/tests/unit/object_store/v1/test_container.py +++ b/openstack/tests/unit/object_store/v1/test_container.py @@ -184,6 +184,8 @@ def test_to_json(self): 'read_ACL': None, 'sync_key': None, 'sync_to': None, + 'meta_temp_url_key': None, + 'meta_temp_url_key_2': None, 'timestamp': None, 'versions_location': None, 'write_ACL': None, diff --git a/releasenotes/notes/generate-form-signature-294ca46812f291d6.yaml b/releasenotes/notes/generate-form-signature-294ca46812f291d6.yaml new file mode 100644 index 000000000..50289502d --- /dev/null +++ b/releasenotes/notes/generate-form-signature-294ca46812f291d6.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added methods to manage object store temp-url keys and + generate signatures needed for FormPost middleware. From af8888af722fa9c089c4b4e7e0c8b2780524688e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 28 Mar 2019 08:35:53 +0000 Subject: [PATCH 2423/3836] Move set_temp_url_key logic into resource objects These are resource-specific methods, so they should exist on the Resource objects. Move the methods there and make the proxy methods use them. Also, shift tests for them into the object_store tests. This puts them more where they should be - and puts requests_mock tests into those files, which is a good example for people. While we're in there, stop creating a Proxy with a Session and use the object_store Proxy that exists. Change-Id: I4ba4456ff044c6c2b0c4ab5faeaf3e3cfd77194f --- openstack/object_store/v1/_proxy.py | 14 +--- openstack/object_store/v1/account.py | 16 ++++ openstack/object_store/v1/container.py | 19 +++++ .../unit/object_store/v1/test_account.py | 38 +++++++++ .../unit/object_store/v1/test_container.py | 34 ++++++++ .../tests/unit/object_store/v1/test_proxy.py | 78 ++++++++++++++++++- 6 files changed, 185 insertions(+), 14 deletions(-) diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index 57637e8c6..c8d1eef66 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -653,11 +653,8 @@ def set_account_temp_url_key(self, key, secondary=False): :param bool secondary: Whether this should set the secondary key. (defaults to False) """ - header = 'Temp-URL-Key' - if secondary: - header += '-2' - - return self.set_account_metadata(**{header: key}) + account = self._get_resource(_account.Account, None) + account.set_temp_url_key(self, key, secondary) def set_container_temp_url_key(self, container, key, secondary=False): """Set the temporary URL key for a container. @@ -670,11 +667,8 @@ def set_container_temp_url_key(self, container, key, secondary=False): :param bool secondary: Whether this should set the secondary key. (defaults to False) """ - header = 'Temp-URL-Key' - if secondary: - header += '-2' - - return self.set_container_metadata(container, **{header: key}) + res = self._get_resource(_container.Container, container) + res.set_temp_url_key(self, key, secondary) def get_temp_url_key(self, container=None): """Get the best temporary url key for a given container. diff --git a/openstack/object_store/v1/account.py b/openstack/object_store/v1/account.py index 01dbd40cd..eb3496bfa 100644 --- a/openstack/object_store/v1/account.py +++ b/openstack/object_store/v1/account.py @@ -43,3 +43,19 @@ class Account(_base.BaseResource): has_body = False requires_id = False + + def set_temp_url_key(self, proxy, key, secondary=False): + """Set the temporary url key for the account. + + :param proxy: The proxy to use for making this request. + :type proxy: :class:`~openstack.proxy.Proxy` + :param key: + Text of the key to use. + :param bool secondary: + Whether this should set the secondary key. (defaults to False) + """ + header = 'Temp-URL-Key' + if secondary: + header += '-2' + + return self.set_metadata(proxy, {header: key}) diff --git a/openstack/object_store/v1/container.py b/openstack/object_store/v1/container.py index 69f2e1a4e..1f7a83f4a 100644 --- a/openstack/object_store/v1/container.py +++ b/openstack/object_store/v1/container.py @@ -135,3 +135,22 @@ def create(self, session, prepend_key=True, base_path=None): self._translate_response(response, has_body=False) return self + + def set_temp_url_key(self, proxy, key, secondary=False): + """Set the temporary url key for a container. + + :param proxy: The proxy to use for making this request. + :type proxy: :class:`~openstack.proxy.Proxy` + :param container: + The value can be the name of a container or a + :class:`~openstack.object_store.v1.container.Container` instance. + :param key: + Text of the key to use. + :param bool secondary: + Whether this should set the second key. (defaults to False) + """ + header = 'Temp-URL-Key' + if secondary: + header += '-2' + + return self.set_metadata(proxy, {header: key}) diff --git a/openstack/tests/unit/object_store/v1/test_account.py b/openstack/tests/unit/object_store/v1/test_account.py index 1616b835e..b415552ce 100644 --- a/openstack/tests/unit/object_store/v1/test_account.py +++ b/openstack/tests/unit/object_store/v1/test_account.py @@ -31,6 +31,10 @@ class TestAccount(base.TestCase): + def setUp(self): + super(TestAccount, self).setUp() + self.endpoint = self.cloud.object_store.get_endpoint() + '/' + def test_basic(self): sot = account.Account(**ACCOUNT_EXAMPLE) self.assertIsNone(sot.resources_key) @@ -53,3 +57,37 @@ def test_make_it(self): self.assertEqual(int(ACCOUNT_EXAMPLE['x-account-object-count']), sot.account_object_count) self.assertEqual(ACCOUNT_EXAMPLE['x-timestamp'], sot.timestamp) + + def test_set_temp_url_key(self): + sot = account.Account() + key = 'super-secure-key' + + self.register_uris([ + dict(method='POST', uri=self.endpoint, + status_code=204, + validate=dict( + headers={ + 'x-account-meta-temp-url-key': key})), + dict(method='HEAD', uri=self.endpoint, + headers={ + 'x-account-meta-temp-url-key': key}), + ]) + sot.set_temp_url_key(self.cloud.object_store, key) + self.assert_calls() + + def test_set_account_temp_url_key_second(self): + sot = account.Account() + key = 'super-secure-key' + + self.register_uris([ + dict(method='POST', uri=self.endpoint, + status_code=204, + validate=dict( + headers={ + 'x-account-meta-temp-url-key-2': key})), + dict(method='HEAD', uri=self.endpoint, + headers={ + 'x-account-meta-temp-url-key-2': key}), + ]) + sot.set_temp_url_key(self.cloud.object_store, key, secondary=True) + self.assert_calls() diff --git a/openstack/tests/unit/object_store/v1/test_container.py b/openstack/tests/unit/object_store/v1/test_container.py index b88ce6bfd..c752cd18b 100644 --- a/openstack/tests/unit/object_store/v1/test_container.py +++ b/openstack/tests/unit/object_store/v1/test_container.py @@ -212,3 +212,37 @@ def test_commit_no_headers(self): sot = container.Container.new(name=self.container) self._test_no_headers(sot, sot.commit, 'POST') self.assert_no_calls() + + def test_set_temp_url_key(self): + sot = container.Container.new(name=self.container) + key = self.getUniqueString() + + self.register_uris([ + dict(method='POST', uri=self.container_endpoint, + status_code=204, + validate=dict( + headers={ + 'x-container-meta-temp-url-key': key})), + dict(method='HEAD', uri=self.container_endpoint, + headers={ + 'x-container-meta-temp-url-key': key}), + ]) + sot.set_temp_url_key(self.cloud.object_store, key) + self.assert_calls() + + def test_set_temp_url_key_second(self): + sot = container.Container.new(name=self.container) + key = self.getUniqueString() + + self.register_uris([ + dict(method='POST', uri=self.container_endpoint, + status_code=204, + validate=dict( + headers={ + 'x-container-meta-temp-url-key-2': key})), + dict(method='HEAD', uri=self.container_endpoint, + headers={ + 'x-container-meta-temp-url-key-2': key}), + ]) + sot.set_temp_url_key(self.cloud.object_store, key, secondary=True) + self.assert_calls() diff --git a/openstack/tests/unit/object_store/v1/test_proxy.py b/openstack/tests/unit/object_store/v1/test_proxy.py index 4d4a2ba48..31a4c6769 100644 --- a/openstack/tests/unit/object_store/v1/test_proxy.py +++ b/openstack/tests/unit/object_store/v1/test_proxy.py @@ -15,7 +15,6 @@ import string import tempfile -from openstack.object_store.v1 import _proxy from openstack.object_store.v1 import account from openstack.object_store.v1 import container from openstack.object_store.v1 import obj @@ -29,7 +28,11 @@ class TestObjectStoreProxy(test_proxy_base.TestProxyBase): def setUp(self): super(TestObjectStoreProxy, self).setUp() - self.proxy = _proxy.Proxy(self.session) + self.proxy = self.cloud.object_store + self.container = self.getUniqueString() + self.endpoint = self.cloud.object_store.get_endpoint() + '/' + self.container_endpoint = '{endpoint}{container}'.format( + endpoint=self.endpoint, container=self.container) def test_account_metadata_get(self): self.verify_head(self.proxy.get_account_metadata, account.Account) @@ -100,12 +103,80 @@ def test_object_get(self): method_kwargs=kwargs, expected_kwargs=kwargs) + def test_set_temp_url_key(self): + + key = 'super-secure-key' + + self.register_uris([ + dict(method='POST', uri=self.endpoint, + status_code=204, + validate=dict( + headers={ + 'x-account-meta-temp-url-key': key})), + dict(method='HEAD', uri=self.endpoint, + headers={ + 'x-account-meta-temp-url-key': key}), + ]) + self.proxy.set_account_temp_url_key(key) + self.assert_calls() + + def test_set_account_temp_url_key_second(self): + + key = 'super-secure-key' + + self.register_uris([ + dict(method='POST', uri=self.endpoint, + status_code=204, + validate=dict( + headers={ + 'x-account-meta-temp-url-key-2': key})), + dict(method='HEAD', uri=self.endpoint, + headers={ + 'x-account-meta-temp-url-key-2': key}), + ]) + self.proxy.set_account_temp_url_key(key, secondary=True) + self.assert_calls() + + def test_set_container_temp_url_key(self): + + key = 'super-secure-key' + + self.register_uris([ + dict(method='POST', uri=self.container_endpoint, + status_code=204, + validate=dict( + headers={ + 'x-container-meta-temp-url-key': key})), + dict(method='HEAD', uri=self.container_endpoint, + headers={ + 'x-container-meta-temp-url-key': key}), + ]) + self.proxy.set_container_temp_url_key(self.container, key) + self.assert_calls() + + def test_set_container_temp_url_key_second(self): + + key = 'super-secure-key' + + self.register_uris([ + dict(method='POST', uri=self.container_endpoint, + status_code=204, + validate=dict( + headers={ + 'x-container-meta-temp-url-key-2': key})), + dict(method='HEAD', uri=self.container_endpoint, + headers={ + 'x-container-meta-temp-url-key-2': key}), + ]) + self.proxy.set_container_temp_url_key( + self.container, key, secondary=True) + self.assert_calls() + class Test_containers(TestObjectStoreProxy): def setUp(self): super(Test_containers, self).setUp() - self.proxy = _proxy.Proxy(self.session) self.containers_body = [] for i in range(3): @@ -180,7 +251,6 @@ class Test_objects(TestObjectStoreProxy): def setUp(self): super(Test_objects, self).setUp() - self.proxy = _proxy.Proxy(self.session) self.container_name = six.text_type("my_container") From 0aabfb0a852dc6ebb8a9f90da99084650ff97424 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 28 Mar 2019 09:03:23 +0000 Subject: [PATCH 2424/3836] Remove and rearrange object bonged proxy tests There are two test classes witha setUp method and all of their tests commented out. The commented tests pre-date the merge and are implemented using httpretty, which is not what we use around here. The fun part is though, since they subclass the proxy tests, they are not adding any value - BUT cause the proxy tests to run multiple times. Just remove them, since they're wasting energy. Also, there are a couple of tests in their own classes, which is similarly not the right design, as it causes all of the tests in the parent class to be run again. Move them into the main class. Change-Id: Ie328a3cd6e59cb9f40a57cd2c79bbd5890e1f2fe --- .../tests/unit/object_store/v1/test_proxy.py | 218 ++---------------- 1 file changed, 25 insertions(+), 193 deletions(-) diff --git a/openstack/tests/unit/object_store/v1/test_proxy.py b/openstack/tests/unit/object_store/v1/test_proxy.py index 31a4c6769..5b0bd818a 100644 --- a/openstack/tests/unit/object_store/v1/test_proxy.py +++ b/openstack/tests/unit/object_store/v1/test_proxy.py @@ -11,7 +11,6 @@ # under the License. import random -import six import string import tempfile @@ -172,173 +171,37 @@ def test_set_container_temp_url_key_second(self): self.container, key, secondary=True) self.assert_calls() + def test_copy_object(self): + self.assertRaises(NotImplementedError, self.proxy.copy_object) -class Test_containers(TestObjectStoreProxy): - - def setUp(self): - super(Test_containers, self).setUp() - - self.containers_body = [] - for i in range(3): - self.containers_body.append({six.text_type("name"): - six.text_type("container%d" % i)}) - -# @httpretty.activate -# def test_all_containers(self): -# self.stub_url(httpretty.GET, -# path=[container.Container.base_path], -# responses=[httpretty.Response( -# body=json.dumps(self.containers_body), -# status=200, content_type="application/json"), -# httpretty.Response(body=json.dumps([]), -# status=200, content_type="application/json")]) -# -# count = 0 -# for actual, expected in zip(self.proxy.containers(), -# self.containers_body): -# self.assertEqual(expected, actual) -# count += 1 -# self.assertEqual(len(self.containers_body), count) - -# @httpretty.activate -# def test_containers_limited(self): -# limit = len(self.containers_body) + 1 -# limit_param = "?limit=%d" % limit -# -# self.stub_url(httpretty.GET, -# path=[container.Container.base_path + limit_param], -# json=self.containers_body) -# -# count = 0 -# for actual, expected in zip(self.proxy.containers(limit=limit), -# self.containers_body): -# self.assertEqual(actual, expected) -# count += 1 -# -# self.assertEqual(len(self.containers_body), count) -# # Since we've chosen a limit larger than the body, only one request -# # should be made, so it should be the last one. -# self.assertIn(limit_param, httpretty.last_request().path) - -# @httpretty.activate -# def test_containers_with_marker(self): -# marker = six.text_type("container2") -# marker_param = "marker=%s" % marker -# -# self.stub_url(httpretty.GET, -# path=[container.Container.base_path + "?" + -# marker_param], -# json=self.containers_body) -# -# count = 0 -# for actual, expected in zip(self.proxy.containers(marker=marker), -# self.containers_body): -# # Make sure the marker made it into the actual request. -# self.assertIn(marker_param, httpretty.last_request().path) -# self.assertEqual(expected, actual) -# count += 1 -# -# self.assertEqual(len(self.containers_body), count) -# -# # Since we have to make one request beyond the end, because no -# # limit was provided, make sure the last container appears as -# # the marker in this last request. -# self.assertIn(self.containers_body[-1]["name"], -# httpretty.last_request().path) - - -class Test_objects(TestObjectStoreProxy): + def test_file_segment(self): + file_size = 4200 + content = ''.join(random.SystemRandom().choice( + string.ascii_uppercase + string.digits) + for _ in range(file_size)).encode('latin-1') + self.imagefile = tempfile.NamedTemporaryFile(delete=False) + self.imagefile.write(content) + self.imagefile.close() - def setUp(self): - super(Test_objects, self).setUp() - - self.container_name = six.text_type("my_container") - - self.objects_body = [] - for i in range(3): - self.objects_body.append({six.text_type("name"): - six.text_type("object%d" % i)}) - - # Returned object bodies have their container inserted. - self.returned_objects = [] - for ob in self.objects_body: - ob[six.text_type("container")] = self.container_name - self.returned_objects.append(ob) - self.assertEqual(len(self.objects_body), len(self.returned_objects)) - -# @httpretty.activate -# def test_all_objects(self): -# self.stub_url(httpretty.GET, -# path=[obj.Object.base_path % -# {"container": self.container_name}], -# responses=[httpretty.Response( -# body=json.dumps(self.objects_body), -# status=200, content_type="application/json"), -# httpretty.Response(body=json.dumps([]), -# status=200, content_type="application/json")]) -# -# count = 0 -# for actual, expected in zip(self.proxy.objects(self.container_name), -# self.returned_objects): -# self.assertEqual(expected, actual) -# count += 1 -# self.assertEqual(len(self.returned_objects), count) - -# @httpretty.activate -# def test_objects_limited(self): -# limit = len(self.objects_body) + 1 -# limit_param = "?limit=%d" % limit -# -# self.stub_url(httpretty.GET, -# path=[obj.Object.base_path % -# {"container": self.container_name} + limit_param], -# json=self.objects_body) -# -# count = 0 -# for actual, expected in zip(self.proxy.objects(self.container_name, -# limit=limit), -# self.returned_objects): -# self.assertEqual(expected, actual) -# count += 1 -# -# self.assertEqual(len(self.returned_objects), count) -# # Since we've chosen a limit larger than the body, only one request -# # should be made, so it should be the last one. -# self.assertIn(limit_param, httpretty.last_request().path) - -# @httpretty.activate -# def test_objects_with_marker(self): -# marker = six.text_type("object2") -# # marker_param = "marker=%s" % marker -# -# self.stub_url(httpretty.GET, -# path=[obj.Object.base_path % -# {"container": self.container_name} + "?" + -# marker_param], -# json=self.objects_body) -# -# count = 0 -# for actual, expected in zip(self.proxy.objects(self.container_name, -# marker=marker), -# self.returned_objects): -# # Make sure the marker made it into the actual request. -# self.assertIn(marker_param, httpretty.last_request().path) -# self.assertEqual(expected, actual) -# count += 1 -# -# self.assertEqual(len(self.returned_objects), count) -# -# # Since we have to make one request beyond the end, because no -# # limit was provided, make sure the last container appears as -# # the marker in this last request. -# self.assertIn(self.returned_objects[-1]["name"], -# httpretty.last_request().path) + segments = self.proxy._get_file_segments( + endpoint='test_container/test_image', + filename=self.imagefile.name, + file_size=file_size, + segment_size=1000) + self.assertEqual(len(segments), 5) + segment_content = b'' + for (index, (name, segment)) in enumerate(segments.items()): + self.assertEqual( + 'test_container/test_image/{index:0>6}'.format(index=index), + name) + segment_content += segment.read() + self.assertEqual(content, segment_content) -class Test_download_object(base_test_object.BaseTestObject): +class TestDownloadObject(base_test_object.BaseTestObject): def setUp(self): - super(Test_download_object, self).setUp() + super(TestDownloadObject, self).setUp() self.the_data = b'test body' self.register_uris([ dict(method='GET', uri=self.object_endpoint, @@ -374,34 +237,3 @@ def test_stream(self): self.assertLessEqual(chunk_len, chunk_size) self.assertEqual(chunk, self.the_data[start:end]) self.assert_calls() - - -class Test_copy_object(TestObjectStoreProxy): - - def test_copy_object(self): - self.assertRaises(NotImplementedError, self.proxy.copy_object) - - -class Test_utils(TestObjectStoreProxy): - def test_file_segment(self): - file_size = 4200 - content = ''.join(random.SystemRandom().choice( - string.ascii_uppercase + string.digits) - for _ in range(file_size)).encode('latin-1') - self.imagefile = tempfile.NamedTemporaryFile(delete=False) - self.imagefile.write(content) - self.imagefile.close() - - segments = self.proxy._get_file_segments( - endpoint='test_container/test_image', - filename=self.imagefile.name, - file_size=file_size, - segment_size=1000) - self.assertEqual(len(segments), 5) - segment_content = b'' - for (index, (name, segment)) in enumerate(segments.items()): - self.assertEqual( - 'test_container/test_image/{index:0>6}'.format(index=index), - name) - segment_content += segment.read() - self.assertEqual(content, segment_content) From 6c81f7362b5928ac99eae8f4d923fc64c40cf4cd Mon Sep 17 00:00:00 2001 From: Cao Xuan Hoang Date: Mon, 1 Apr 2019 16:22:29 +0700 Subject: [PATCH 2425/3836] Expose locked status for Server This patch exposes the property for the openstack.compute.v2.Server class, making them available for inspection. Change-Id: I53e45622f145971ef7efb220084419ed72c0a715 --- openstack/compute/v2/server.py | 2 ++ openstack/tests/unit/cloud/test_normalize.py | 7 ++++++- openstack/tests/unit/compute/v2/test_server.py | 4 +++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 1ed715876..d29fe6e70 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -137,6 +137,8 @@ class Server(resource.Resource, metadata.MetadataMixin, resource.TagMixin): #: instance name template. Appears in the response for administrative users #: only. instance_name = resource.Body('OS-EXT-SRV-ATTR:instance_name') + # The locked status of the server + is_locked = resource.Body('locked', type=bool) def _prepare_request(self, requires_id=True, prepend_key=True, base_path=None): diff --git a/openstack/tests/unit/cloud/test_normalize.py b/openstack/tests/unit/cloud/test_normalize.py index fc95c10c3..478bc3b6e 100644 --- a/openstack/tests/unit/cloud/test_normalize.py +++ b/openstack/tests/unit/cloud/test_normalize.py @@ -66,6 +66,7 @@ 'progress': 0, 'request_ids': [], 'security_groups': [{u'name': u'default'}], + 'locked': True, 'status': u'ACTIVE', 'tenant_id': u'db92b20496ae4fbda850a689ea9d563f', 'updated': u'2016-10-15T15:49:29Z', @@ -498,6 +499,7 @@ def test_normalize_servers_normal(self): 'OS-SCH-HNT:scheduler_hints': None, 'OS-SRV-USG:launched_at': u'2015-08-01T19:52:02.000000', 'OS-SRV-USG:terminated_at': None, + 'locked': True, 'os-extended-volumes:volumes_attached': []}, 'public_v4': None, 'public_v6': None, @@ -505,6 +507,7 @@ def test_normalize_servers_normal(self): 'scheduler_hints': None, 'security_groups': [{u'name': u'default'}], 'status': u'ACTIVE', + 'locked': True, 'tags': [], 'task_state': None, 'tenant_id': u'db92b20496ae4fbda850a689ea9d563f', @@ -927,7 +930,9 @@ def test_normalize_servers(self): 'power_state': 1, 'private_v4': None, 'progress': 0, - 'properties': {}, + 'properties': { + 'locked': True + }, 'public_v4': None, 'public_v6': None, 'scheduler_hints': None, diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index f43934420..a0ddbce51 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -54,7 +54,8 @@ 'OS-EXT-SRV-ATTR:hypervisor_hostname': 'hypervisor.example.com', 'OS-EXT-SRV-ATTR:instance_name': 'instance-00000001', 'OS-SCH-HNT:scheduler_hints': {'key': '30'}, - 'OS-EXT-SRV-ATTR:user_data': '31' + 'OS-EXT-SRV-ATTR:user_data': '31', + 'locked': True } @@ -148,6 +149,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['OS-SCH-HNT:scheduler_hints'], sot.scheduler_hints) self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:user_data'], sot.user_data) + self.assertEqual(EXAMPLE['locked'], sot.is_locked) def test__prepare_server(self): zone = 1 From 16afacbc61805568d21095b1d634f9dcce5eb0e4 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 3 Apr 2019 13:05:44 +0200 Subject: [PATCH 2426/3836] Move Orchestration methods into Proxy - Switch cloud.orchestration methods to consume proxy directly. - Add additional attributes to stack. - Add "query parameter" resolve_outputs to stack with and extend general _find/fetch method to be able to consume additional attributes (normally through overloading). Change-Id: I8c4cb174d8b2d7802e2e94ec907fdebfcdcc9859 --- openstack/cloud/_normalize.py | 16 ++- openstack/cloud/_orchestration.py | 82 +++++--------- .../_heat => orchestration/util}/__init__.py | 0 .../util}/environment_format.py | 2 +- .../util}/event_utils.py | 0 .../util}/template_format.py | 0 .../util}/template_utils.py | 14 +-- .../_heat => orchestration/util}/utils.py | 4 +- openstack/orchestration/v1/_proxy.py | 67 +++++++++-- openstack/orchestration/v1/stack.py | 107 ++++++++++++++++-- openstack/resource.py | 30 +++-- openstack/tests/fakes.py | 2 +- openstack/tests/unit/cloud/test_stack.py | 25 ++-- .../tests/unit/orchestration/v1/test_proxy.py | 31 ++++- .../tests/unit/orchestration/v1/test_stack.py | 41 +++++-- openstack/tests/unit/test_proxy_base.py | 2 +- 16 files changed, 301 insertions(+), 122 deletions(-) rename openstack/{cloud/_heat => orchestration/util}/__init__.py (100%) rename openstack/{cloud/_heat => orchestration/util}/environment_format.py (96%) rename openstack/{cloud/_heat => orchestration/util}/event_utils.py (100%) rename openstack/{cloud/_heat => orchestration/util}/template_format.py (100%) rename openstack/{cloud/_heat => orchestration/util}/template_utils.py (97%) rename openstack/{cloud/_heat => orchestration/util}/utils.py (95%) diff --git a/openstack/cloud/_normalize.py b/openstack/cloud/_normalize.py index 598d982ad..f4975cf2a 100644 --- a/openstack/cloud/_normalize.py +++ b/openstack/cloud/_normalize.py @@ -20,6 +20,8 @@ import munch import six +from openstack import resource + _IMAGE_FIELDS = ( 'checksum', 'container_format', @@ -1082,7 +1084,10 @@ def _normalize_stacks(self, stacks): def _normalize_stack(self, stack): """Normalize Heat Stack""" - stack = stack.copy() + if isinstance(stack, resource.Resource): + stack = stack.to_dict(ignore_none=True, original_names=True) + else: + stack = stack.copy() # Discard noise self._remove_novaclient_artifacts(stack) @@ -1092,7 +1097,10 @@ def _normalize_stack(self, stack): stack.pop('status', None) stack.pop('identifier', None) - stack_status = stack.pop('stack_status') + stack_status = None + + stack_status = stack.pop('stack_status', None) or \ + stack.pop('status', None) (action, status) = stack_status.split('_', 1) ret = munch.Munch( @@ -1121,13 +1129,13 @@ def _normalize_stack(self, stack): ('tempate_description', 'template_description'), ('timeout_mins', 'timeout_mins'), ('tags', 'tags')): - value = stack.pop(old_name, None) + value = stack.get(old_name, None) ret[new_name] = value if not self.strict_mode: ret[old_name] = value ret['identifier'] = '{name}/{id}'.format( name=ret['name'], id=ret['id']) - ret['properties'] = stack + # ret['properties'] = stack return ret def _normalize_machines(self, machines): diff --git a/openstack/cloud/_orchestration.py b/openstack/cloud/_orchestration.py index 95b01320b..bab1ed0ce 100644 --- a/openstack/cloud/_orchestration.py +++ b/openstack/cloud/_orchestration.py @@ -16,8 +16,7 @@ import types # noqa from openstack.cloud import exc -from openstack.cloud._heat import event_utils -from openstack.cloud._heat import template_utils +from openstack.orchestration.util import event_utils from openstack.cloud import _normalize from openstack.cloud import _utils @@ -43,13 +42,9 @@ def _orchestration_client(self): def get_template_contents( self, template_file=None, template_url=None, template_object=None, files=None): - try: - return template_utils.get_template_contents( - template_file=template_file, template_url=template_url, - template_object=template_object, files=files) - except Exception as e: - raise exc.OpenStackCloudException( - "Error in processing template files: %s" % str(e)) + return self.orchestration.get_template_contents( + template_file=template_file, template_url=template_url, + template_object=template_object, files=files) def create_stack( self, name, tags=None, @@ -83,24 +78,18 @@ def create_stack( :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call """ - envfiles, env = template_utils.process_multiple_environments_and_files( - env_paths=environment_files) - tpl_files, template = template_utils.get_template_contents( - template_file=template_file, - template_url=template_url, - template_object=template_object, - files=files) params = dict( - stack_name=name, tags=tags, - disable_rollback=not rollback, - parameters=parameters, - template=template, - files=dict(list(tpl_files.items()) + list(envfiles.items())), - environment=env, + is_rollback_disabled=not rollback, timeout_mins=timeout // 60, + parameters=parameters ) - self._orchestration_client.post('/stacks', json=params) + params.update(self.orchestration.read_env_and_templates( + template_file=template_file, template_url=template_url, + template_object=template_object, files=files, + environment_files=environment_files + )) + self.orchestration.create_stack(name=name, **params) if wait: event_utils.poll_for_events(self, stack_name=name, action='CREATE') @@ -137,30 +126,26 @@ def update_stack( :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API calls """ - envfiles, env = template_utils.process_multiple_environments_and_files( - env_paths=environment_files) - tpl_files, template = template_utils.get_template_contents( - template_file=template_file, - template_url=template_url, - template_object=template_object, - files=files) params = dict( - disable_rollback=not rollback, tags=tags, - parameters=parameters, - template=template, - files=dict(list(tpl_files.items()) + list(envfiles.items())), - environment=env, + is_rollback_disabled=not rollback, timeout_mins=timeout // 60, + parameters=parameters ) + params.update(self.orchestration.read_env_and_templates( + template_file=template_file, template_url=template_url, + template_object=template_object, files=files, + environment_files=environment_files + )) if wait: # find the last event to use as the marker events = event_utils.get_events( self, name_or_id, event_args={'sort_dir': 'desc', 'limit': 1}) marker = events[0].id if events else None - self._orchestration_client.put( - '/stacks/{name_or_id}'.format(name_or_id=name_or_id), json=params) + # Not to cause update of ID field pass stack as dict + self.orchestration.update_stack(stack={'id': name_or_id}, **params) + if wait: event_utils.poll_for_events(self, name_or_id, @@ -190,8 +175,7 @@ def delete_stack(self, name_or_id, wait=False): self, name_or_id, event_args={'sort_dir': 'desc', 'limit': 1}) marker = events[0].id if events else None - self._orchestration_client.delete( - '/stacks/{id}'.format(id=stack['id'])) + self.orchestration.delete_stack(stack) if wait: try: @@ -233,10 +217,8 @@ def list_stacks(self): :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ - data = self._orchestration_client.get( - '/stacks', error_message="Error fetching stack list") - return self._normalize_stacks( - self._get_and_munchify('stacks', data)) + data = self.orchestration.stacks() + return self._normalize_stacks(data) def get_stack(self, name_or_id, filters=None, resolve_outputs=True): """Get exactly one stack. @@ -257,15 +239,11 @@ def _search_one_stack(name_or_id=None, filters=None): # stack names are mandatory and enforced unique in the project # so a StackGet can always be used for name or ID. try: - url = '/stacks/{name_or_id}'.format(name_or_id=name_or_id) - if not resolve_outputs: - url = '{url}?resolve_outputs=False'.format(url=url) - data = self._orchestration_client.get( - url, - error_message="Error fetching stack") - stack = self._get_and_munchify('stack', data) - # Treat DELETE_COMPLETE stacks as a NotFound - if stack['stack_status'] == 'DELETE_COMPLETE': + stack = self.orchestration.find_stack( + name_or_id, + ignore_missing=False, + resolve_outputs=resolve_outputs) + if stack.status == 'DELETE_COMPLETE': return [] except exc.OpenStackCloudURINotFound: return [] diff --git a/openstack/cloud/_heat/__init__.py b/openstack/orchestration/util/__init__.py similarity index 100% rename from openstack/cloud/_heat/__init__.py rename to openstack/orchestration/util/__init__.py diff --git a/openstack/cloud/_heat/environment_format.py b/openstack/orchestration/util/environment_format.py similarity index 96% rename from openstack/cloud/_heat/environment_format.py rename to openstack/orchestration/util/environment_format.py index ac60715ae..8a9c9745c 100644 --- a/openstack/cloud/_heat/environment_format.py +++ b/openstack/orchestration/util/environment_format.py @@ -12,7 +12,7 @@ import yaml -from openstack.cloud._heat import template_format +from openstack.orchestration.util import template_format SECTIONS = ( diff --git a/openstack/cloud/_heat/event_utils.py b/openstack/orchestration/util/event_utils.py similarity index 100% rename from openstack/cloud/_heat/event_utils.py rename to openstack/orchestration/util/event_utils.py diff --git a/openstack/cloud/_heat/template_format.py b/openstack/orchestration/util/template_format.py similarity index 100% rename from openstack/cloud/_heat/template_format.py rename to openstack/orchestration/util/template_format.py diff --git a/openstack/cloud/_heat/template_utils.py b/openstack/orchestration/util/template_utils.py similarity index 97% rename from openstack/cloud/_heat/template_utils.py rename to openstack/orchestration/util/template_utils.py index 1e1d3fa18..8b0070c0d 100644 --- a/openstack/cloud/_heat/template_utils.py +++ b/openstack/orchestration/util/template_utils.py @@ -18,10 +18,10 @@ from six.moves.urllib import parse from six.moves.urllib import request -from openstack.cloud._heat import environment_format -from openstack.cloud._heat import template_format -from openstack.cloud._heat import utils -from openstack.cloud import exc +from openstack.orchestration.util import environment_format +from openstack.orchestration.util import template_format +from openstack.orchestration.util import utils +from openstack import exceptions def get_template_contents(template_file=None, template_url=None, @@ -46,12 +46,12 @@ def get_template_contents(template_file=None, template_url=None, elif existing: return {}, None else: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'Must provide one of template_file,' ' template_url or template_object') if not tpl: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'Could not fetch template from %s' % template_url) try: @@ -59,7 +59,7 @@ def get_template_contents(template_file=None, template_url=None, tpl = tpl.decode('utf-8') template = template_format.parse(tpl) except ValueError as e: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'Error parsing template %(url)s %(error)s' % {'url': template_url, 'error': e}) diff --git a/openstack/cloud/_heat/utils.py b/openstack/orchestration/util/utils.py similarity index 95% rename from openstack/cloud/_heat/utils.py rename to openstack/orchestration/util/utils.py index d977754ca..89a04b8eb 100644 --- a/openstack/cloud/_heat/utils.py +++ b/openstack/orchestration/util/utils.py @@ -20,7 +20,7 @@ from six.moves.urllib import parse from six.moves.urllib import request -from openstack.cloud import exc +from openstack import exceptions def base_url_for_url(url): @@ -41,7 +41,7 @@ def read_url_content(url): # TODO(mordred) Use requests content = request.urlopen(url).read() except error.URLError: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'Could not fetch contents for %s' % url) if content: diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index 02d725524..4ea3b0f5b 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import exceptions from openstack.orchestration.v1 import resource as _resource from openstack.orchestration.v1 import software_config as _sc from openstack.orchestration.v1 import software_deployment as _sd @@ -19,12 +18,51 @@ from openstack.orchestration.v1 import stack_files as _stack_files from openstack.orchestration.v1 import stack_template as _stack_template from openstack.orchestration.v1 import template as _template +from openstack.orchestration.util import template_utils +from openstack import exceptions from openstack import proxy from openstack import resource class Proxy(proxy.Proxy): + def read_env_and_templates(self, template_file=None, template_url=None, + template_object=None, files=None, + environment_files=None): + """Read templates and environment content and prepares + corresponding stack attributes + + :param string template_file: Path to the template. + :param string template_url: URL of template. + :param string template_object: URL to retrieve template object. + :param dict files: dict of additional file content to include. + :param environment_files: Paths to environment files to apply. + + :returns: Attributes dict to be set on the + :class:`~openstack.orchestration.v1.stack.Stack` + :rtype: dict + """ + stack_attrs = dict() + envfiles = None + tpl_files = None + if environment_files: + envfiles, env = \ + template_utils.process_multiple_environments_and_files( + env_paths=environment_files) + stack_attrs['environment'] = env + if template_file or template_url or template_object: + tpl_files, template = template_utils.get_template_contents( + template_file=template_file, + template_url=template_url, + template_object=template_object, + files=files) + stack_attrs['template'] = template + if tpl_files or envfiles: + stack_attrs['files'] = dict( + list(tpl_files.items()) + list(envfiles.items()) + ) + return stack_attrs + def create_stack(self, preview=False, **attrs): """Create a new stack from attributes @@ -32,16 +70,18 @@ def create_stack(self, preview=False, **attrs): verify the template *Default: ``False``* :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.orchestration.v1.stack.Stack`, - comprised of the properties on the Stack class. + a :class:`~openstack.orchestration.v1.stack.Stack`, + comprised of the properties on the Stack class. :returns: The results of stack creation :rtype: :class:`~openstack.orchestration.v1.stack.Stack` """ + base_path = None if not preview else '/stacks/preview' return self._create(_stack.Stack, base_path=base_path, **attrs) - def find_stack(self, name_or_id, ignore_missing=True): + def find_stack(self, name_or_id, + ignore_missing=True, resolve_outputs=True): """Find a single stack :param name_or_id: The name or ID of a stack. @@ -53,7 +93,8 @@ def find_stack(self, name_or_id, ignore_missing=True): :returns: One :class:`~openstack.orchestration.v1.stack.Stack` or None """ return self._find(_stack.Stack, name_or_id, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, + resolve_outputs=resolve_outputs) def stacks(self, **query): """Return a generator of stacks @@ -66,17 +107,18 @@ def stacks(self, **query): """ return self._list(_stack.Stack, **query) - def get_stack(self, stack): + def get_stack(self, stack, resolve_outputs=True): """Get a single stack :param stack: The value can be the ID of a stack or a :class:`~openstack.orchestration.v1.stack.Stack` instance. + :param resolve_outputs: Whether stack should contain outputs resolved. :returns: One :class:`~openstack.orchestration.v1.stack.Stack` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get(_stack.Stack, stack) + return self._get(_stack.Stack, stack, resolve_outputs=resolve_outputs) def update_stack(self, stack, preview=False, **attrs): """Update a stack @@ -411,3 +453,14 @@ def wait_for_delete(self, res, interval=2, wait=120): to delete failed to occur in the specified seconds. """ return resource.wait_for_delete(self, res, interval, wait) + + def get_template_contents( + self, template_file=None, template_url=None, + template_object=None, files=None): + try: + return template_utils.get_template_contents( + template_file=template_file, template_url=template_url, + template_object=template_object, files=files) + except Exception as e: + raise exceptions.SDKException( + "Error in processing template files: %s" % str(e)) diff --git a/openstack/orchestration/v1/stack.py b/openstack/orchestration/v1/stack.py index 8f31dbee8..adb577b01 100644 --- a/openstack/orchestration/v1/stack.py +++ b/openstack/orchestration/v1/stack.py @@ -28,6 +28,10 @@ class Stack(resource.Resource): allow_commit = True allow_delete = True + _query_mapping = resource.QueryParameters( + 'resolve_outputs' + ) + # Properties #: A list of resource objects that will be added if a stack update # is performed. @@ -42,6 +46,17 @@ class Stack(resource.Resource): #: A list of resource objects that will be deleted if a stack #: update is performed. deleted = resource.Body('deleted', type=list) + #: Timestamp of the stack deletion. + deleted_at = resource.Body('deletion_time') + #: A JSON environment for the stack. + environment = resource.Body('environment') + #: An ordered list of names for environment files found in the files dict. + environment_files = resource.Body('environment_files', type=list) + #: Additional files referenced in the template or the environment + files = resource.Body('files', type=dict) + #: Name of the container in swift that has child + #: templates and environment files. + files_container = resource.Body('files_container') #: Whether the stack will support a rollback operation on stack #: create/update failures. *Type: bool* is_rollback_disabled = resource.Body('disable_rollback', type=bool) @@ -105,9 +120,20 @@ def commit(self, session, base_path=None): def update(self, session, preview=False): # This overrides the default behavior of resource update because # we need to use other endpoint for update preview. + base_path = None + if self.name and self.id: + base_path = '/stacks/%(stack_name)s/%(stack_id)s' % { + 'stack_name': self.name, + 'stack_id': self.id} + elif self.name or self.id: + # We have only one of name/id. Do not try to build a stacks/NAME/ID + # path + base_path = '/stacks/%(stack_identity)s' % { + 'stack_identity': self.name or self.id} request = self._prepare_request( prepend_key=False, - base_path='/stacks/%(stack_name)s/' % {'stack_name': self.name}) + requires_id=False, + base_path=base_path) microversion = self._get_microversion_for(session, 'commit') @@ -139,16 +165,77 @@ def abandon(self, session): return resp.json() def fetch(self, session, requires_id=True, - base_path=None, error_message=None): - stk = super(Stack, self).fetch( - session, - requires_id=requires_id, - base_path=base_path, - error_message=error_message) - if stk and stk.status in ['DELETE_COMPLETE', 'ADOPT_COMPLETE']: + base_path=None, error_message=None, resolve_outputs=True): + + if not self.allow_fetch: + raise exceptions.MethodNotSupported(self, "fetch") + + request = self._prepare_request(requires_id=requires_id, + base_path=base_path) + # session = self._get_session(session) + microversion = self._get_microversion_for(session, 'fetch') + + # NOTE(gtema): would be nice to simply use QueryParameters, however + # Heat return 302 with parameters being set into URL and requests + # apply parameters again, what results in them being set doubled + if not resolve_outputs: + request.url = request.url + '?resolve_outputs=False' + response = session.get(request.url, microversion=microversion) + kwargs = {} + if error_message: + kwargs['error_message'] = error_message + + self.microversion = microversion + self._translate_response(response, **kwargs) + + if self and self.status in ['DELETE_COMPLETE', 'ADOPT_COMPLETE']: raise exceptions.ResourceNotFound( - "No stack found for %s" % stk.id) - return stk + "No stack found for %s" % self.id) + return self + + @classmethod + def find(cls, session, name_or_id, ignore_missing=True, **params): + """Find a resource by its name or id. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param name_or_id: This resource's identifier, if needed by + the request. The default is ``None``. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict params: Any additional parameters to be passed into + underlying methods, such as to + :meth:`~openstack.resource.Resource.existing` + in order to pass on URI parameters. + + :return: The :class:`Resource` object matching the given name or id + or None if nothing matches. + :raises: :class:`openstack.exceptions.DuplicateResource` if more + than one resource is found for this request. + :raises: :class:`openstack.exceptions.ResourceNotFound` if nothing + is found and ignore_missing is ``False``. + """ + session = cls._get_session(session) + # Try to short-circuit by looking directly for a matching ID. + try: + match = cls.existing( + id=name_or_id, + connection=session._get_connection(), + **params) + return match.fetch(session, **params) + except exceptions.NotFoundException: + pass + + # NOTE(gtema) we do not do list, since previous call has done this + # for us already + + if ignore_missing: + return None + raise exceptions.ResourceNotFound( + "No %s found for %s" % (cls.__name__, name_or_id)) StackPreview = Stack diff --git a/openstack/resource.py b/openstack/resource.py index fb88d2087..bf58cfc65 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -971,16 +971,21 @@ def _translate_response(self, response, has_body=None, error_message=None): has_body = self.has_body exceptions.raise_from_response(response, error_message=error_message) if has_body: - body = response.json() - if self.resource_key and self.resource_key in body: - body = body[self.resource_key] - - body = self._consume_body_attrs(body) - self._body.attributes.update(body) - self._body.clean() - if self.commit_jsonpatch or self.allow_patch: - # We need the original body to compare against - self._original_body = body.copy() + try: + body = response.json() + if self.resource_key and self.resource_key in body: + body = body[self.resource_key] + + body = self._consume_body_attrs(body) + self._body.attributes.update(body) + self._body.clean() + if self.commit_jsonpatch or self.allow_patch: + # We need the original body to compare against + self._original_body = body.copy() + except ValueError: + # Server returned not parse-able response (202, 204, etc) + # Do simply nothing + pass headers = self._consume_header_attrs(response.headers) self._header.attributes.update(headers) @@ -1127,7 +1132,7 @@ def create(self, session, prepend_key=True, base_path=None): return self def fetch(self, session, requires_id=True, - base_path=None, error_message=None): + base_path=None, error_message=None, **params): """Get a remote resource based on this instance. :param session: The session to use for making this request. @@ -1139,6 +1144,7 @@ def fetch(self, session, requires_id=True, :data:`~openstack.resource.Resource.base_path`. :param str error_message: An Error message to be returned if requested object does not exist. + :param dict params: Additional parameters that can be consumed. :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if :data:`Resource.allow_fetch` is not set to ``True``. @@ -1577,7 +1583,7 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): id=name_or_id, connection=session._get_connection(), **params) - return match.fetch(session) + return match.fetch(session, **params) except exceptions.NotFoundException: pass diff --git a/openstack/tests/fakes.py b/openstack/tests/fakes.py index 08d07568a..c0fee5774 100644 --- a/openstack/tests/fakes.py +++ b/openstack/tests/fakes.py @@ -21,7 +21,7 @@ import json import uuid -from openstack.cloud._heat import template_format +from openstack.orchestration.util import template_format from openstack.cloud import meta PROJECT_ID = '1c36b64c840a42cd9e9b931a369337f0' diff --git a/openstack/tests/unit/cloud/test_stack.py b/openstack/tests/unit/cloud/test_stack.py index 20ce23773..c6cce9b2d 100644 --- a/openstack/tests/unit/cloud/test_stack.py +++ b/openstack/tests/unit/cloud/test_stack.py @@ -15,10 +15,11 @@ import testtools import openstack.cloud -from openstack.cloud import meta from openstack.tests import fakes from openstack.tests.unit import base +from openstack.orchestration.v1 import stack + class TestStack(base.TestCase): @@ -44,7 +45,8 @@ def test_list_stacks(self): ]) stacks = self.cloud.list_stacks() self.assertEqual( - [f.toDict() for f in self.cloud._normalize_stacks(fake_stacks)], + [f.toDict() for f in self.cloud._normalize_stacks( + stack.Stack(**st) for st in fake_stacks)], [f.toDict() for f in stacks]) self.assert_calls() @@ -76,7 +78,8 @@ def test_search_stacks(self): ]) stacks = self.cloud.search_stacks() self.assertEqual( - self.cloud._normalize_stacks(meta.obj_list_to_munch(fake_stacks)), + self.cloud._normalize_stacks( + stack.Stack(**st) for st in fake_stacks), stacks) self.assert_calls() @@ -98,7 +101,7 @@ def test_search_stacks_filters(self): stacks = self.cloud.search_stacks(filters=filters) self.assertEqual( self.cloud._normalize_stacks( - meta.obj_list_to_munch(fake_stacks[1:])), + stack.Stack(**st) for st in fake_stacks[1:]), stacks) self.assert_calls() @@ -316,8 +319,6 @@ def test_create_stack(self): validate=dict( json={ 'disable_rollback': False, - 'environment': {}, - 'files': {}, 'parameters': {}, 'stack_name': self.stack_name, 'tags': self.stack_tag, @@ -364,8 +365,6 @@ def test_create_stack_wait(self): validate=dict( json={ 'disable_rollback': False, - 'environment': {}, - 'files': {}, 'parameters': {}, 'stack_name': self.stack_name, 'tags': self.stack_tag, @@ -422,12 +421,11 @@ def test_update_stack(self): validate=dict( json={ 'disable_rollback': False, - 'environment': {}, - 'files': {}, 'parameters': {}, 'tags': self.stack_tag, 'template': fakes.FAKE_TEMPLATE_CONTENT, - 'timeout_mins': 60})), + 'timeout_mins': 60}), + json={}), dict( method='GET', uri='{endpoint}/stacks/{name}'.format( @@ -478,12 +476,11 @@ def test_update_stack_wait(self): validate=dict( json={ 'disable_rollback': False, - 'environment': {}, - 'files': {}, 'parameters': {}, 'tags': self.stack_tag, 'template': fakes.FAKE_TEMPLATE_CONTENT, - 'timeout_mins': 60})), + 'timeout_mins': 60}), + json={}), dict( method='GET', uri='{endpoint}/stacks/{name}/events?{qs}'.format( diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index 87597bfce..834f69ba6 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -40,13 +40,40 @@ def test_create_stack_preview(self): method_kwargs=method_kwargs) def test_find_stack(self): - self.verify_find(self.proxy.find_stack, stack.Stack) + self.verify_find(self.proxy.find_stack, stack.Stack, + expected_kwargs={'resolve_outputs': True}) + # mock_method="openstack.proxy.Proxy._find" + # test_method=self.proxy.find_stack + # method_kwargs = { + # 'resolve_outputs': False, + # 'ignore_missing': False + # } + # method_args=["name_or_id"] + # self._verify2(mock_method, test_method, + # method_args=method_args, + # method_kwargs=method_kwargs, + # expected_args=[stack.Stack, "name_or_id"], + # expected_kwargs=method_kwargs, + # expected_result="result") + # + # method_kwargs = { + # 'resolve_outputs': True, + # 'ignore_missing': True + # } + # self._verify2(mock_method, test_method, + # method_args=method_args, + # method_kwargs=method_kwargs, + # expected_args=[stack.Stack, "name_or_id"], + # expected_kwargs=method_kwargs, + # expected_result="result") def test_stacks(self): self.verify_list(self.proxy.stacks, stack.Stack) def test_get_stack(self): - self.verify_get(self.proxy.get_stack, stack.Stack) + self.verify_get(self.proxy.get_stack, stack.Stack, + method_kwargs={'resolve_outputs': False}, + expected_kwargs={'resolve_outputs': False}) self.verify_get_overrided( self.proxy, stack.Stack, 'openstack.orchestration.v1.stack.Stack') diff --git a/openstack/tests/unit/orchestration/v1/test_stack.py b/openstack/tests/unit/orchestration/v1/test_stack.py index 698d877a7..38c8a412d 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack.py +++ b/openstack/tests/unit/orchestration/v1/test_stack.py @@ -13,6 +13,7 @@ import mock import six from openstack.tests.unit import base +from openstack.tests.unit import test_resource from openstack import exceptions from openstack.orchestration.v1 import stack @@ -24,8 +25,13 @@ FAKE = { 'capabilities': '1', 'creation_time': '2015-03-09T12:15:57.233772', + 'deletion_time': '2015-03-09T12:15:57.233772', 'description': '3', 'disable_rollback': True, + 'environment': {'var1': 'val1'}, + 'environment_files': [], + 'files': {'file1': 'content'}, + 'files_container': 'dummy_container', 'id': FAKE_ID, 'links': [{ 'href': 'stacks/%s/%s' % (FAKE_NAME, FAKE_ID), @@ -135,7 +141,12 @@ def test_make_it(self): sot = stack.Stack(**FAKE) self.assertEqual(FAKE['capabilities'], sot.capabilities) self.assertEqual(FAKE['creation_time'], sot.created_at) + self.assertEqual(FAKE['deletion_time'], sot.deleted_at) self.assertEqual(FAKE['description'], sot.description) + self.assertEqual(FAKE['environment'], sot.environment) + self.assertEqual(FAKE['environment_files'], sot.environment_files) + self.assertEqual(FAKE['files'], sot.files) + self.assertEqual(FAKE['files_container'], sot.files_container) self.assertTrue(sot.is_rollback_disabled) self.assertEqual(FAKE['id'], sot.id) self.assertEqual(FAKE['links'], sot.links) @@ -186,19 +197,31 @@ def test_check(self): sot._action.assert_called_with(sess, body) - @mock.patch.object(resource.Resource, 'fetch') - def test_fetch(self, mock_fetch): + def test_fetch(self): + sess = mock.Mock() + sess.default_microversion = None sot = stack.Stack(**FAKE) - deleted_stack = mock.Mock(id=FAKE_ID, status='DELETE_COMPLETE') - normal_stack = mock.Mock(status='CREATE_COMPLETE') - mock_fetch.side_effect = [ - normal_stack, + + sess.get = mock.Mock() + sess.get.side_effect = [ + test_resource.FakeResponse( + {'stack': {'stack_status': 'CREATE_COMPLETE'}}, 200), + test_resource.FakeResponse( + {'stack': {'stack_status': 'CREATE_COMPLETE'}}, 200), exceptions.ResourceNotFound(message='oops'), - deleted_stack, + test_resource.FakeResponse( + {'stack': {'stack_status': 'DELETE_COMPLETE'}}, 200) ] - self.assertEqual(normal_stack, sot.fetch(sess)) + self.assertEqual(sot, sot.fetch(sess)) + sess.get.assert_called_with( + 'stacks/{id}'.format(id=sot.id), + microversion=None) + sot.fetch(sess, resolve_outputs=False) + sess.get.assert_called_with( + 'stacks/{id}?resolve_outputs=False'.format(id=sot.id), + microversion=None) ex = self.assertRaises(exceptions.ResourceNotFound, sot.fetch, sess) self.assertEqual('oops', six.text_type(ex)) ex = self.assertRaises(exceptions.ResourceNotFound, sot.fetch, sess) @@ -238,7 +261,7 @@ def test_update(self): sot.update(sess) sess.put.assert_called_with( - 'stacks/%s/%s' % (FAKE_NAME, FAKE_ID), + '/stacks/%s/%s' % (FAKE_NAME, FAKE_ID), headers={}, microversion=None, json=body diff --git a/openstack/tests/unit/test_proxy_base.py b/openstack/tests/unit/test_proxy_base.py index e8e1eb834..4c293294f 100644 --- a/openstack/tests/unit/test_proxy_base.py +++ b/openstack/tests/unit/test_proxy_base.py @@ -177,7 +177,7 @@ def verify_find(self, test_method, resource_type, value=None, mock_method="openstack.proxy.Proxy._find", path_args=None, **kwargs): method_args = value or ["name_or_id"] - expected_kwargs = {} + expected_kwargs = kwargs.pop('expected_kwargs', {}) self._add_path_args_for_verify(path_args, method_args, expected_kwargs, value=value) From e9a5d45e5099e3bfc04aad7a6ae54d54f9990de5 Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Thu, 4 Apr 2019 15:59:54 -0700 Subject: [PATCH 2427/3836] Expand on a brief error message In this case, the remote server did not supply any information about the error. Tell the user this so it doesn't look like the SDK library is accidentally omitting anything. Change-Id: I8f4757d2075c7cb4708df7b387f02beb2d4e671d --- openstack/cloud/_compute.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index abd511fc9..a20e52942 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -1133,7 +1133,9 @@ def get_active_server( extra_data=dict(server=server)) raise exc.OpenStackCloudException( - "Error in creating the server", extra_data=dict(server=server)) + "Error in creating the server" + " (no further information available)", + extra_data=dict(server=server)) if server['status'] == 'ACTIVE': if 'addresses' in server and server['addresses']: From be753eacbbcdd0bcb2d80c71c77ca6b99341b4d4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 18 Feb 2019 15:04:06 +0000 Subject: [PATCH 2428/3836] Bail earlier on a version mismatch for a supported service When we can't find a version for a service we do know about, the behavior of falling through to an empty Proxy just leads to massive end-user confusion. Throw an error. Change-Id: I7625a2b076fcebe2d66c412503fe3e7681f26918 --- openstack/service_description.py | 10 +++- .../unit/fixtures/bad-glance-version.json | 15 ++++++ .../unit/fixtures/catalog-bogus-glance.json | 52 +++++++++++++++++++ openstack/tests/unit/test_missing_version.py | 40 ++++++++++++++ ...il-on-failed-service-cf299c37d5647b08.yaml | 6 +++ 5 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 openstack/tests/unit/fixtures/bad-glance-version.json create mode 100644 openstack/tests/unit/fixtures/catalog-bogus-glance.json create mode 100644 openstack/tests/unit/test_missing_version.py create mode 100644 releasenotes/notes/bail-on-failed-service-cf299c37d5647b08.yaml diff --git a/openstack/service_description.py b/openstack/service_description.py index bfe46a7f5..92446be18 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -177,7 +177,15 @@ def _make_proxy(self, instance): **version_kwargs ) found_version = temp_adapter.get_api_major_version() - if found_version is None: + if found_version is None and version_kwargs: + raise exceptions.NotSupported( + "The {service_type} service for {cloud}:{region_name}" + " exists but does not have any supported versions.".format( + service_type=self.service_type, + cloud=instance.name, + region_name=instance.config.region_name)) + proxy_class = self.supported_versions.get(str(found_version[0])) + if not proxy_class: # Maybe openstacksdk is being used for the passthrough # REST API proxy layer for an unknown service in the # service catalog that also doesn't have any useful diff --git a/openstack/tests/unit/fixtures/bad-glance-version.json b/openstack/tests/unit/fixtures/bad-glance-version.json new file mode 100644 index 000000000..0fd91011f --- /dev/null +++ b/openstack/tests/unit/fixtures/bad-glance-version.json @@ -0,0 +1,15 @@ +{ + "versions": [ + { + "status": "CURRENT", + "updated": "2013-07-23T11:33:21Z", + "links": [ + { + "href": "https://example.com/image/v7/", + "rel": "self" + } + ], + "id": "v7" + } + ] +} diff --git a/openstack/tests/unit/fixtures/catalog-bogus-glance.json b/openstack/tests/unit/fixtures/catalog-bogus-glance.json new file mode 100644 index 000000000..3a47d3f9e --- /dev/null +++ b/openstack/tests/unit/fixtures/catalog-bogus-glance.json @@ -0,0 +1,52 @@ +{ + "token": { + "audit_ids": [ + "Rvn7eHkiSeOwucBIPaKdYA" + ], + "catalog": [ + { + "endpoints": [ + { + "id": "32466f357f3545248c47471ca51b0d3a", + "interface": "public", + "region": "RegionOne", + "url": "https://example.com/image/" + } + ], + "name": "glance", + "type": "image" + } + ], + "expires_at": "9999-12-31T23:59:59Z", + "issued_at": "2016-12-17T14:25:05.000000Z", + "methods": [ + "password" + ], + "project": { + "domain": { + "id": "default", + "name": "default" + }, + "id": "1c36b64c840a42cd9e9b931a369337f0", + "name": "Default Project" + }, + "roles": [ + { + "id": "9fe2ff9ee4384b1894a90878d3e92bab", + "name": "_member_" + }, + { + "id": "37071fc082e14c2284c32a2761f71c63", + "name": "swiftoperator" + } + ], + "user": { + "domain": { + "id": "default", + "name": "default" + }, + "id": "c17534835f8f42bf98fc367e0bf35e09", + "name": "mordred" + } + } +} diff --git a/openstack/tests/unit/test_missing_version.py b/openstack/tests/unit/test_missing_version.py new file mode 100644 index 000000000..b1cdaa44e --- /dev/null +++ b/openstack/tests/unit/test_missing_version.py @@ -0,0 +1,40 @@ +# 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 testtools + +from openstack import exceptions +from openstack import proxy +from openstack.tests.unit import base + + +class TestMissingVersion(base.TestCase): + + def setUp(self): + super(TestMissingVersion, self).setUp() + self.use_keystone_v3(catalog='catalog-bogus-glance.json') + self.use_glance( + image_version_json='bad-glance-version.json', + image_discovery_url='https://example.com/image/') + + def test_unsupported_version(self): + + with testtools.ExpectedException(exceptions.NotSupported): + self.cloud.image.get('/') + + self.assert_calls() + + def test_unsupported_version_override(self): + self.cloud.config.config['image_api_version'] = '7' + self.assertTrue(isinstance(self.cloud.image, proxy.Proxy)) + + self.assert_calls() diff --git a/releasenotes/notes/bail-on-failed-service-cf299c37d5647b08.yaml b/releasenotes/notes/bail-on-failed-service-cf299c37d5647b08.yaml new file mode 100644 index 000000000..5f004f8cf --- /dev/null +++ b/releasenotes/notes/bail-on-failed-service-cf299c37d5647b08.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + When a known service cannot be resolved to a supported version, + an exception is now thrown instead of just returning a blank + Proxy object. This allows returning sane errors to users. From d1f880fc7842c453360a196e946bd556fc655cb3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 4 Mar 2019 18:47:26 +0000 Subject: [PATCH 2429/3836] Clarify error if no version can be found Otherwise this will blow up on the str(found_version[0]) on the next line. Change-Id: I53ab77332b0372bd0191331ff62d261ee75df0a6 --- openstack/service_description.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/openstack/service_description.py b/openstack/service_description.py index 92446be18..d319b686b 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -177,13 +177,21 @@ def _make_proxy(self, instance): **version_kwargs ) found_version = temp_adapter.get_api_major_version() - if found_version is None and version_kwargs: - raise exceptions.NotSupported( - "The {service_type} service for {cloud}:{region_name}" - " exists but does not have any supported versions.".format( - service_type=self.service_type, - cloud=instance.name, - region_name=instance.config.region_name)) + if found_version is None: + if version_kwargs: + raise exceptions.NotSupported( + "The {service_type} service for {cloud}:{region_name}" + " exists but does not have any supported versions.".format( + service_type=self.service_type, + cloud=instance.name, + region_name=instance.config.region_name)) + else: + raise exceptions.NotSupported( + "The {service_type} service for {cloud}:{region_name}" + " exists but no version was discoverable.".format( + service_type=self.service_type, + cloud=instance.name, + region_name=instance.config.region_name)) proxy_class = self.supported_versions.get(str(found_version[0])) if not proxy_class: # Maybe openstacksdk is being used for the passthrough From 8bbd44c9b5ad413de6384ae8fb096f20737ebf92 Mon Sep 17 00:00:00 2001 From: LIU Yulong Date: Tue, 28 Aug 2018 10:36:26 +0800 Subject: [PATCH 2430/3836] Add floating IP port forwarding related methods Closes-Bug: #1811352 Change-Id: Id78c91b0a66c8c04ab783fe4ba666a18844aed59 --- openstack/network/v2/_proxy.py | 117 ++++++++++++++++++ openstack/tests/unit/network/v2/test_proxy.py | 61 +++++++++ ...rtforwarding-methods-cffc14a6283cedfb.yaml | 4 + 3 files changed, 182 insertions(+) create mode 100644 releasenotes/notes/add-fip-portforwarding-methods-cffc14a6283cedfb.yaml diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 9e0caab24..36ad7755b 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -3780,3 +3780,120 @@ def update_vpn_service(self, vpn_service, **attrs): :rtype: :class:`~openstack.network.v2.vpn_service.VPNService` """ return self._update(_vpn_service.VPNService, vpn_service, **attrs) + + def create_floating_ip_port_forwarding(self, floating_ip, **attrs): + """Create a new floating ip port forwarding from attributes + + :param floating_ip: The value can be either the ID of a floating ip + or a :class:`~openstack.network.v2.floating_ip.FloatingIP` + instance. + :param dict attrs:Keyword arguments which will be used to create + a:class:`~openstack.network.v2.port_forwarding.PortForwarding`, + comprised of the properties on the PortForwarding class. + + :returns: The results of port forwarding creation + :rtype: :class:`~openstack.network.v2.port_forwarding.PortForwarding` + """ + floatingip = self._get_resource(_floating_ip.FloatingIP, floating_ip) + return self._create(_port_forwarding.PortForwarding, + floatingip_id=floatingip.id, **attrs) + + def delete_floating_ip_port_forwarding(self, floating_ip, port_forwarding, + ignore_missing=True): + """Delete a floating IP port forwarding. + + :param floating_ip: The value can be either the ID of a floating ip + or a :class:`~openstack.network.v2.floating_ip.FloatingIP` + instance. + :param port_forwarding: The value can be either the ID of a port + forwarding or a :class:`~openstack.network.v2. + port_forwarding.PortForwarding`instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the floating ip does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent ip. + + :returns: ``None`` + """ + floatingip = self._get_resource(_floating_ip.FloatingIP, floating_ip) + self._delete(_port_forwarding.PortForwarding, + port_forwarding, ignore_missing=ignore_missing, + floatingip_id=floatingip.id) + + def find_floating_ip_port_forwarding(self, floating_ip, port_forwarding_id, + ignore_missing=True, **args): + """Find a floating ip port forwarding + + :param floating_ip: The value can be the ID of the Floating IP that the + port forwarding belongs or a :class:`~openstack. + network.v2.floating_ip.FloatingIP` instance. + :param port_forwarding_id: The ID of a port forwarding. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. + :returns: One :class:`~openstack.network.v2.port_forwarding. + PortForwarding` or None + """ + floatingip = self._get_resource(_floating_ip.FloatingIP, floating_ip) + return self._find(_port_forwarding.PortForwarding, + port_forwarding_id, ignore_missing=ignore_missing, + floatingip_id=floatingip.id, **args) + + def get_floating_ip_port_forwarding(self, floating_ip, port_forwarding): + """Get a floating ip port forwarding + + :param floating_ip: The value can be the ID of the Floating IP that the + port forwarding belongs or a :class:`~openstack. + network.v2.floating_ip.FloatingIP` instance. + :param port_forwarding: The value can be the ID of a port forwarding + or a :class:`~openstack.network.v2. + port_forwarding.PortForwarding` instance. + :returns: One :class:`~openstack.network.v2.port_forwarding. + PortForwarding` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + floatingip = self._get_resource(_floating_ip.FloatingIP, floating_ip) + return self._get(_port_forwarding.PortForwarding, port_forwarding, + floatingip_id=floatingip.id) + + def floating_ip_port_forwardings(self, floating_ip, **query): + """Return a generator of floating ip port forwarding + + :param floating_ip: The value can be the ID of the Floating IP that the + port forwarding belongs or a :class:`~openstack. + network.v2.floating_ip.FloatingIP` instance. + :param kwargs \*\*query: Optional query parameters to be sent to limit + the resources being returned. + :returns: A generator of floating ip port forwarding objects + :rtype: :class:`~openstack.network.v2.port_forwarding. + PortForwarding` + """ + floatingip = self._get_resource(_floating_ip.FloatingIP, floating_ip) + return self._list(_port_forwarding.PortForwarding, + floatingip_id=floatingip.id, **query) + + def update_floating_ip_port_forwarding(self, floating_ip, port_forwarding, + **attrs): + """Update a floating ip port forwarding + + :param floating_ip: The value can be the ID of the Floating IP that the + port forwarding belongs or a :class:`~openstack. + network.v2.floating_ip.FloatingIP` instance. + :param port_forwarding: Either the id of a floating ip port forwarding + or a :class:`~openstack.network.v2. + port_forwarding.PortForwarding`instance. + :attrs kwargs: The attributes to update on the floating ip port + forwarding represented by ``value``. + + :returns: The updated floating ip port forwarding + :rtype: :class:`~openstack.network.v2.port_forwarding.PortForwarding` + """ + floatingip = self._get_resource(_floating_ip.FloatingIP, floating_ip) + return self._update(_port_forwarding.PortForwarding, port_forwarding, + floatingip_id=floatingip.id, **attrs) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 446e843c6..b93deedf0 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -36,6 +36,7 @@ from openstack.network.v2 import pool from openstack.network.v2 import pool_member from openstack.network.v2 import port +from openstack.network.v2 import port_forwarding from openstack.network.v2 import qos_bandwidth_limit_rule from openstack.network.v2 import qos_dscp_marking_rule from openstack.network.v2 import qos_minimum_bandwidth_rule @@ -61,6 +62,7 @@ NETWORK_ID = 'network-id-' + uuid.uuid4().hex AGENT_ID = 'agent-id-' + uuid.uuid4().hex ROUTER_ID = 'router-id-' + uuid.uuid4().hex +FIP_ID = 'fip-id-' + uuid.uuid4().hex class TestNetworkProxy(test_proxy_base.TestProxyBase): @@ -1156,3 +1158,62 @@ def test_set_tags_resource_without_tag_suport(self, mock_set_tags): self.proxy.set_tags, no_tag_resource, ['TAG1', 'TAG2']) self.assertEqual(0, mock_set_tags.call_count) + + def test_create_floating_ip_port_forwarding(self): + self.verify_create(self.proxy.create_floating_ip_port_forwarding, + port_forwarding.PortForwarding, + method_kwargs={'floating_ip': FIP_ID}, + expected_kwargs={'floatingip_id': FIP_ID}) + + def test_delete_floating_ip_port_forwarding(self): + self.verify_delete( + self.proxy.delete_floating_ip_port_forwarding, + port_forwarding.PortForwarding, + False, input_path_args=[FIP_ID, "resource_or_id"], + expected_path_args={'floatingip_id': FIP_ID},) + + def test_delete_floating_ip_port_forwarding_ignore(self): + self.verify_delete( + self.proxy.delete_floating_ip_port_forwarding, + port_forwarding.PortForwarding, + True, input_path_args=[FIP_ID, "resource_or_id"], + expected_path_args={'floatingip_id': FIP_ID}, ) + + def test_find_floating_ip_port_forwarding(self): + fip = floating_ip.FloatingIP.new(id=FIP_ID) + self._verify2('openstack.proxy.Proxy._find', + self.proxy.find_floating_ip_port_forwarding, + method_args=[fip, 'port_forwarding_id'], + expected_args=[ + port_forwarding.PortForwarding, + 'port_forwarding_id'], + expected_kwargs={'ignore_missing': True, + 'floatingip_id': FIP_ID}) + + def test_get_floating_ip_port_forwarding(self): + fip = floating_ip.FloatingIP.new(id=FIP_ID) + self._verify2('openstack.proxy.Proxy._get', + self.proxy.get_floating_ip_port_forwarding, + method_args=[fip, 'port_forwarding_id'], + expected_args=[ + port_forwarding.PortForwarding, + 'port_forwarding_id'], + expected_kwargs={'floatingip_id': FIP_ID}) + + def test_floating_ip_port_forwardings(self): + self.verify_list(self.proxy.floating_ip_port_forwardings, + port_forwarding.PortForwarding, + method_kwargs={'floating_ip': FIP_ID}, + expected_kwargs={'floatingip_id': FIP_ID}) + + def test_update_floating_ip_port_forwarding(self): + fip = floating_ip.FloatingIP.new(id=FIP_ID) + self._verify2('openstack.proxy.Proxy._update', + self.proxy.update_floating_ip_port_forwarding, + method_args=[fip, 'port_forwarding_id'], + method_kwargs={'foo': 'bar'}, + expected_args=[ + port_forwarding.PortForwarding, + 'port_forwarding_id'], + expected_kwargs={'floatingip_id': FIP_ID, + 'foo': 'bar'}) diff --git a/releasenotes/notes/add-fip-portforwarding-methods-cffc14a6283cedfb.yaml b/releasenotes/notes/add-fip-portforwarding-methods-cffc14a6283cedfb.yaml new file mode 100644 index 000000000..274e86a0e --- /dev/null +++ b/releasenotes/notes/add-fip-portforwarding-methods-cffc14a6283cedfb.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add floating IP Port Forwarding related methods. From 8fed470b09ac7db887ebdca38b369557b0b25f10 Mon Sep 17 00:00:00 2001 From: Thomas Bechtold Date: Tue, 9 Apr 2019 13:01:22 +0200 Subject: [PATCH 2431/3836] baremetal: Add support for mkisofs and xorrisofs for configdrive Currently, only "genisoimage" is supported. But "genisoimage" might not be available on all distros (like openSUSE or Debian). So add support for "mkisofs" and "xorrisofs" which luckily support the same command line parameters as "genisoimage". Change-Id: I720f25921f8e52f20a631f238a528dedf65a91c6 --- openstack/baremetal/configdrive.py | 37 ++++++++++++------- ...ve-mkisofs-xorrisofs-075db4d7d80e5a13.yaml | 8 ++++ 2 files changed, 32 insertions(+), 13 deletions(-) create mode 100644 releasenotes/notes/baremetal-configdrive-mkisofs-xorrisofs-075db4d7d80e5a13.yaml diff --git a/openstack/baremetal/configdrive.py b/openstack/baremetal/configdrive.py index abbebf5a3..b43d19b3c 100644 --- a/openstack/baremetal/configdrive.py +++ b/openstack/baremetal/configdrive.py @@ -84,21 +84,32 @@ def pack(path): :return: configdrive contents as a base64-encoded string. """ with tempfile.NamedTemporaryFile() as tmpfile: - try: - p = subprocess.Popen(['genisoimage', - '-o', tmpfile.name, - '-ldots', '-allow-lowercase', - '-allow-multidot', '-l', - '-publisher', 'metalsmith', - '-quiet', '-J', - '-r', '-V', 'config-2', - path], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - except OSError as e: + # NOTE(toabctl): Luckily, genisoimage, mkisofs and xorrisofs understand + # the same parameters which are currently used. + cmds = ['genisoimage', 'mkisofs', 'xorrisofs'] + for c in cmds: + try: + p = subprocess.Popen([c, + '-o', tmpfile.name, + '-ldots', '-allow-lowercase', + '-allow-multidot', '-l', + '-publisher', 'metalsmith', + '-quiet', '-J', + '-r', '-V', 'config-2', + path], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + except OSError as e: + error = e + else: + error = None + break + + if error: raise RuntimeError( 'Error generating the configdrive. Make sure the ' - '"genisoimage" tool is installed. Error: %s' % e) + '"genisoimage", "mkisofs" or "xorrisofs" tool is installed. ' + 'Error: %s' % error) stdout, stderr = p.communicate() if p.returncode != 0: diff --git a/releasenotes/notes/baremetal-configdrive-mkisofs-xorrisofs-075db4d7d80e5a13.yaml b/releasenotes/notes/baremetal-configdrive-mkisofs-xorrisofs-075db4d7d80e5a13.yaml new file mode 100644 index 000000000..008459e8d --- /dev/null +++ b/releasenotes/notes/baremetal-configdrive-mkisofs-xorrisofs-075db4d7d80e5a13.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + When generating a config drive for baremetal, "mkisofs" and "xorrisofs" + are now supported beside the already available "genisoimage" binary. + This is useful on environment where the "genisoimage" binary is not + available but "mkisofs" and/or "xorrisofs" are available. + From 0df7372b43c6ea2c15a6cc5e149d0fc139ab54ff Mon Sep 17 00:00:00 2001 From: Tobias Rydberg Date: Thu, 11 Apr 2019 11:58:02 +0000 Subject: [PATCH 2432/3836] Removing region La1 from the list of regions. Change-Id: Ib40de57580c09f496aee5a262a5127a71bcbc25e --- openstack/config/vendors/citycloud.json | 1 - 1 file changed, 1 deletion(-) diff --git a/openstack/config/vendors/citycloud.json b/openstack/config/vendors/citycloud.json index 057b5100c..b48978c27 100644 --- a/openstack/config/vendors/citycloud.json +++ b/openstack/config/vendors/citycloud.json @@ -6,7 +6,6 @@ }, "regions": [ "Buf1", - "La1", "Fra1", "Lon1", "Sto2", From 5c06d139c2a08a67cb6656b3196be3edcb9ef2c8 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 8 Apr 2019 12:05:31 +0000 Subject: [PATCH 2433/3836] Add logger to Proxy object It's good to be able to easily log things. Add a logger of the form openstack.{service-type}. This will allow conceptual filtering without too much heartburn. Change-Id: I091cb0997eec47d995b6522a57030005ca224cbb --- openstack/proxy.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openstack/proxy.py b/openstack/proxy.py index 8144e771f..9260222ae 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -19,6 +19,7 @@ from keystoneauth1 import adapter +from openstack import _log from openstack import exceptions from openstack import resource @@ -135,6 +136,11 @@ def __init__( self._statsd_prefix = statsd_prefix self._prometheus_counter = prometheus_counter self._prometheus_histogram = prometheus_histogram + if self.service_type: + log_name = 'openstack.{0}'.format(self.service_type) + else: + log_name = 'openstack' + self.log = _log.setup_logging(log_name) def request( self, url, method, error_message=None, From 842d3caab73f7993a1693e985ddbed06fe409e73 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 8 Apr 2019 12:06:38 +0000 Subject: [PATCH 2434/3836] Support microversion 2.61 for nova flavors 2.61 adds extra_specs, which is a nice to have. Change-Id: I43e88fee9ff41ed1eede4199dcab0910d19a51f7 --- openstack/compute/v2/flavor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openstack/compute/v2/flavor.py b/openstack/compute/v2/flavor.py index b68c726a5..1e6c65eb9 100644 --- a/openstack/compute/v2/flavor.py +++ b/openstack/compute/v2/flavor.py @@ -29,6 +29,9 @@ class Flavor(resource.Resource): min_disk="minDisk", min_ram="minRam") + # extra_specs introduced in 2.61 + _max_microversion = '2.61' + # Properties #: Links pertaining to this flavor. This is a list of dictionaries, #: each including keys ``href`` and ``rel``. @@ -54,6 +57,10 @@ class Flavor(resource.Resource): is_disabled = resource.Body('OS-FLV-DISABLED:disabled', type=bool) #: The bandwidth scaling factor this flavor receives on the network. rxtx_factor = resource.Body('rxtx_factor', type=float) + # TODO(mordred) extra_specs can historically also come from + # OS-FLV-WITH-EXT-SPECS:extra_specs. Do we care? + #: A dictionary of the flavor's extra-specs key-and-value pairs. + extra_specs = resource.Body('extra_specs', type=dict) class FlavorDetail(Flavor): From 1c05173395e59430f7ff8dc8bce4b2c422ac6531 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Mon, 15 Apr 2019 14:40:02 +0200 Subject: [PATCH 2435/3836] Try to fix the masakari CI job Change-Id: I49fa746c597e225ccba5af852b449068e2a4ede0 --- .zuul.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.zuul.yaml b/.zuul.yaml index ee9dea9f6..9a89154cc 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -361,6 +361,7 @@ Run openstacksdk functional tests against a master devstack with masakari required-projects: - openstack/masakari + - openstack/masakari-monitors vars: devstack_plugins: masakari: https://git.openstack.org/openstack/masakari From e8e99af4d371c2410a12b8b62e18b2982a9a8932 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 16 Apr 2019 13:36:00 +0000 Subject: [PATCH 2436/3836] Return None from get_server_by_id on 404 The interface for get_server_by_id SHOULD be None if not found like the rest of the cloud layer. At some point in the past that got broken. Fix it. Change-Id: Ibd4fdc22418679c3562faaabe8fe3d307e8e8283 --- openstack/cloud/_compute.py | 19 +++++++++++++++---- ...et-server-by-id-none-3e8538800fa09d82.yaml | 7 +++++++ 2 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/get-server-by-id-none-3e8538800fa09d82.yaml diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index a20e52942..a883ad3c7 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -27,6 +27,7 @@ from openstack.cloud import meta from openstack.cloud import _normalize from openstack.cloud import _utils +from openstack import exceptions from openstack import proxy from openstack import utils @@ -568,10 +569,20 @@ def _expand_server(self, server, detailed, bare): return meta.add_server_interfaces(self, server) def get_server_by_id(self, id): - data = proxy._json_response( - self.compute.get('/servers/{id}'.format(id=id))) - server = self._get_and_munchify('server', data) - return meta.add_server_interfaces(self, self._normalize_server(server)) + """Get a server by ID. + + :param id: ID of the server. + + :returns: A server dict or None if no matching server is found. + """ + try: + data = proxy._json_response( + self.compute.get('/servers/{id}'.format(id=id))) + server = self._get_and_munchify('server', data) + return meta.add_server_interfaces( + self, self._normalize_server(server)) + except exceptions.ResourceNotFound: + return None def get_server_group(self, name_or_id=None, filters=None): """Get a server group by name or ID. diff --git a/releasenotes/notes/get-server-by-id-none-3e8538800fa09d82.yaml b/releasenotes/notes/get-server-by-id-none-3e8538800fa09d82.yaml new file mode 100644 index 000000000..b3ef3fec3 --- /dev/null +++ b/releasenotes/notes/get-server-by-id-none-3e8538800fa09d82.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + The ``get_server_by_id`` method is supposed to return ``None`` if the + server in question can't be found, but a regression was introduced + causing it to raise ``ResourceNotFound`` instead. This has been corrected + and ``get_server_by_id`` returns ``None`` correctly again. From d8db601f212bb92c9b7542243d3b045f34607c7c Mon Sep 17 00:00:00 2001 From: Erik Olof Gunnar Andersson Date: Tue, 16 Apr 2019 20:21:16 -0700 Subject: [PATCH 2437/3836] Actually pass on network_data when building configdrive Change-Id: I75cfd10e1daf4590d064df531be57fe06363ccc5 --- openstack/baremetal/configdrive.py | 3 ++- releasenotes/notes/network-data-bd94e4a499ba3e0d.yaml | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/network-data-bd94e4a499ba3e0d.yaml diff --git a/openstack/baremetal/configdrive.py b/openstack/baremetal/configdrive.py index b43d19b3c..13348fe42 100644 --- a/openstack/baremetal/configdrive.py +++ b/openstack/baremetal/configdrive.py @@ -71,7 +71,8 @@ def build(metadata, user_data=None, versions=None, network_data=None): :param dict network_data: Networking configuration. :return: configdrive contents as a base64-encoded string. """ - with populate_directory(metadata, user_data, versions) as path: + with populate_directory(metadata, user_data, versions, + network_data) as path: return pack(path) diff --git a/releasenotes/notes/network-data-bd94e4a499ba3e0d.yaml b/releasenotes/notes/network-data-bd94e4a499ba3e0d.yaml new file mode 100644 index 000000000..22e5bd706 --- /dev/null +++ b/releasenotes/notes/network-data-bd94e4a499ba3e0d.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixes ``openstack.baremetal.configdrive.build`` to actually handle the + ``network_data`` argument. From 667fc55bf351a6a7155965d704c5b1cab4559c18 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 17 Apr 2019 11:17:48 +0200 Subject: [PATCH 2438/3836] Update baremetal to use proxy logger We added a logger to the Proxy object. Use it. Change-Id: Id84b845ca6400296514f86d4bf2fcc6dd66c9426 --- openstack/baremetal/v1/_proxy.py | 13 ++++------ openstack/baremetal/v1/allocation.py | 11 +++----- openstack/baremetal/v1/node.py | 25 +++++++++---------- .../unit/baremetal/v1/test_allocation.py | 1 + .../tests/unit/baremetal/v1/test_node.py | 2 ++ 5 files changed, 24 insertions(+), 28 deletions(-) diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 2f95cc180..ee655a055 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import _log from openstack.baremetal.v1 import _common from openstack.baremetal.v1 import allocation as _allocation from openstack.baremetal.v1 import chassis as _chassis @@ -22,9 +21,6 @@ from openstack import utils -_logger = _log.setup_logging('openstack') - - class Proxy(proxy.Proxy): retriable_status_codes = _common.RETRIABLE_STATUS_CODES @@ -367,10 +363,11 @@ def wait_for_nodes_provision_state(self, nodes, expected_state, if not remaining: return finished - _logger.debug('Still waiting for nodes %(nodes)s to reach state ' - '"%(target)s"', - {'nodes': ', '.join(n.id for n in remaining), - 'target': expected_state}) + self.log.debug( + 'Still waiting for nodes %(nodes)s to reach state ' + '"%(target)s"', + {'nodes': ', '.join(n.id for n in remaining), + 'target': expected_state}) def set_node_power_state(self, node, target): """Run an action modifying node's power state. diff --git a/openstack/baremetal/v1/allocation.py b/openstack/baremetal/v1/allocation.py index 27ac55181..f7a450c2b 100644 --- a/openstack/baremetal/v1/allocation.py +++ b/openstack/baremetal/v1/allocation.py @@ -10,16 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import _log from openstack.baremetal.v1 import _common from openstack import exceptions from openstack import resource from openstack import utils -_logger = _log.setup_logging('openstack') - - class Allocation(_common.ListMixin, resource.Resource): resources_key = 'allocations' @@ -93,6 +89,7 @@ def wait(self, session, timeout=None, ignore_error=False): elif self.state != 'allocating': return self - _logger.debug('Still waiting for the allocation %(allocation)s ' - 'to become active, the current state is %(state)s', - {'allocation': self.id, 'state': self.state}) + session.log.debug( + 'Still waiting for the allocation %(allocation)s ' + 'to become active, the current state is %(state)s', + {'allocation': self.id, 'state': self.state}) diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index ad2d3c74e..43f3d8acc 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -10,16 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import _log from openstack.baremetal.v1 import _common from openstack import exceptions from openstack import resource from openstack import utils -_logger = _log.setup_logging('openstack') - - class ValidationResult(object): """Result of a single interface validation. @@ -415,10 +411,11 @@ def wait_for_provision_state(self, session, expected_state, timeout=None, abort_on_failed_state): return self - _logger.debug('Still waiting for node %(node)s to reach state ' - '"%(target)s", the current state is "%(state)s"', - {'node': self.id, 'target': expected_state, - 'state': self.provision_state}) + session.log.debug( + 'Still waiting for node %(node)s to reach state ' + '"%(target)s", the current state is "%(state)s"', + {'node': self.id, 'target': expected_state, + 'state': self.provision_state}) def wait_for_reservation(self, session, timeout=None): """Wait for a lock on the node to be released. @@ -454,9 +451,10 @@ def wait_for_reservation(self, session, timeout=None): if self.reservation is None: return self - _logger.debug('Still waiting for the lock to be released on node ' - '%(node)s, currently locked by conductor %(host)s', - {'node': self.id, 'host': self.reservation}) + session.log.debug( + 'Still waiting for the lock to be released on node ' + '%(node)s, currently locked by conductor %(host)s', + {'node': self.id, 'host': self.reservation}) def _check_state_reached(self, session, expected_state, abort_on_failed_state=True): @@ -602,8 +600,9 @@ def detach_vif(self, session, vif_id, ignore_missing=True): retriable_status_codes=_common.RETRIABLE_STATUS_CODES) if ignore_missing and response.status_code == 400: - _logger.debug('VIF %(vif)s was already removed from node %(node)s', - {'vif': vif_id, 'node': self.id}) + session.log.debug( + 'VIF %(vif)s was already removed from node %(node)s', + {'vif': vif_id, 'node': self.id}) return False msg = ("Failed to detach VIF {vif} from bare metal node {node}" diff --git a/openstack/tests/unit/baremetal/v1/test_allocation.py b/openstack/tests/unit/baremetal/v1/test_allocation.py index b2a82ec99..c508cca68 100644 --- a/openstack/tests/unit/baremetal/v1/test_allocation.py +++ b/openstack/tests/unit/baremetal/v1/test_allocation.py @@ -79,6 +79,7 @@ def setUp(self): super(TestWaitForAllocation, self).setUp() self.session = mock.Mock(spec=adapter.Adapter) self.session.default_microversion = '1.52' + self.session.log = mock.Mock() self.fake = dict(FAKE, state='allocating', node_uuid=None) self.allocation = allocation.Allocation(**self.fake) diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 765250ef7..2d4764932 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -380,6 +380,7 @@ def setUp(self): super(TestNodeVif, self).setUp() self.session = mock.Mock(spec=adapter.Adapter) self.session.default_microversion = '1.28' + self.session.log = mock.Mock() self.node = node.Node(id='c29db401-b6a7-4530-af8e-20a720dee946', driver=FAKE['driver']) self.vif_id = '714bdf6d-2386-4b5e-bd0d-bc036f04b1ef' @@ -505,6 +506,7 @@ def setUp(self): super(TestNodeWaitForReservation, self).setUp() self.session = mock.Mock(spec=adapter.Adapter) self.session.default_microversion = '1.6' + self.session.log = mock.Mock() self.node = node.Node(**FAKE) def test_no_reservation(self, mock_fetch): From d6a53c9296622d1b574f57fe4bc502514dd14e31 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 18 Apr 2019 12:58:13 +0000 Subject: [PATCH 2439/3836] Remove now unused task_manager file We left this in place for the last release to allow nodepool to stop using it. That transition is done, so remove the unused file. Change-Id: I0316174e9c9fdf1f418e3728b5a96d46158edb97 --- openstack/task_manager.py | 290 -------------------------------------- 1 file changed, 290 deletions(-) delete mode 100644 openstack/task_manager.py diff --git a/openstack/task_manager.py b/openstack/task_manager.py deleted file mode 100644 index 2c353a523..000000000 --- a/openstack/task_manager.py +++ /dev/null @@ -1,290 +0,0 @@ -# Copyright (C) 2011-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 concurrent.futures -import functools -import sys -import threading -import time - -import keystoneauth1.exceptions -import six -from six.moves import queue - -import openstack._log -from openstack import exceptions - -_log = openstack._log.setup_logging('openstack.task_manager') - - -class Task(object): - """Represent a remote task to be performed on an OpenStack Cloud. - - Some consumers need to inject things like rate-limiting or auditing - around each external REST interaction. Task provides an interface - to encapsulate each such interaction. Also, although shade itself - operates normally in a single-threaded direct action manner, consuming - programs may provide a multi-threaded TaskManager themselves. For that - reason, Task uses threading events to ensure appropriate wait conditions. - These should be a no-op in single-threaded applications. - - A consumer is expected to overload the main method. - - :param dict kw: Any args that are expected to be passed to something in - the main payload at execution time. - """ - - def __init__( - self, main=None, name=None, run_async=False, - tag=None, *args, **kwargs): - self._exception = None - self._traceback = None - self._result = None - self._response = None - self._finished = threading.Event() - self._main = main - self._run_async = run_async - self.args = args - self.kwargs = kwargs - self.name = name or type(self).__name__ - self.tag = tag - - def main(self): - return self._main(*self.args, **self.kwargs) - - @property - def run_async(self): - return self._run_async - - def done(self, result): - self._result = result - self._finished.set() - - def exception(self, e, tb): - self._exception = e - self._traceback = tb - self._finished.set() - - def wait(self, raw=False): - self._finished.wait() - - if self._exception: - six.reraise(type(self._exception), self._exception, - self._traceback) - - return self._result - - def run(self): - try: - # Retry one time if we get a retriable connection failure - try: - self.done(self.main()) - except keystoneauth1.exceptions.RetriableConnectionFailure: - self.done(self.main()) - except Exception as e: - self.exception(e, sys.exc_info()[2]) - - -class TaskManager(object): - - def __init__(self, name, rate=None, log=_log, workers=5, **kwargs): - self.name = name - self._executor = None - self._log = log - self._workers = workers - self.daemon = True - self.queue = queue.Queue() - self._running = True - if isinstance(rate, dict): - self._waits = {} - for (k, v) in rate.items(): - if v: - self._waits[k] = 1.0 / v - else: - if rate: - self._waits = {None: 1.0 / rate} - else: - self._waits = {} - self._thread = threading.Thread(name=name, target=self.run) - self._thread.daemon = True - - def _get_wait(self, tag): - return self._waits.get(tag, self._waits.get(None)) - - @property - def executor(self): - if not self._executor: - self._executor = concurrent.futures.ThreadPoolExecutor( - max_workers=self._workers) - return self._executor - - def start(self): - self._thread.start() - - def stop(self): - self._running = False - self.queue.put(None) - if self._executor: - self._executor.shutdown() - - def join(self): - self._thread.join() - - def run(self): - last_ts_dict = {} - try: - while True: - task = self.queue.get() - if not task: - if not self._running: - break - continue - wait = self._get_wait(task.tag) - if wait: - last_ts = last_ts_dict.get(task.tag, 0) - while True: - delta = time.time() - last_ts - if delta >= wait: - break - time.sleep(wait - delta) - last_ts_dict[task.tag] = time.time() - self._log.debug( - "TaskManager {name} queue size: {size})".format( - name=self.name, - size=self.queue.qsize())) - self.run_task(task) - self.queue.task_done() - except Exception: - self._log.exception("TaskManager died") - raise - - def submit_task(self, task): - """Submit and execute the given task. - - :param task: The task to execute. - :param bool raw: If True, return the raw result as received from the - underlying client call. - - This method calls task.wait() so that it only returns when the - task is complete. - """ - if not self._running: - raise exceptions.TaskManagerStopped( - "TaskManager {name} is no longer running".format( - name=self.name)) - self.pre_run_task(task) - start = time.time() - self.queue.put(task) - ret = task.wait() - end = time.time() - dt = end - start - self.post_run_task(dt, task) - return ret - - def submit_function( - self, method, name=None, run_async=False, tag=None, - *args, **kwargs): - """ Allows submitting an arbitrary method for work. - - :param method: Callable to run in the TaskManager. - :param str name: Name to use for the generated Task object. - :param bool run_async: Whether to run this task async or not. - :param str tag: Named rate-limiting context for the task. - :param args: positional arguments to pass to the method when it runs. - :param kwargs: keyword arguments to pass to the method when it runs. - """ - if run_async: - payload = functools.partial( - self.executor.submit, method, *args, **kwargs) - task = Task( - main=payload, name=name, - run_async=run_async, - tag=tag) - else: - task = Task( - main=method, name=name, - tag=tag, - *args, **kwargs) - return self.submit_task(task) - - def submit_function_async(self, method, name=None, *args, **kwargs): - """ Allows submitting an arbitrary method for async work scheduling. - - :param method: Callable to run in the TaskManager. - :param str name: Name to use for the generated Task object. - :param args: positional arguments to pass to the method when it runs. - :param kwargs: keyword arguments to pass to the method when it runs. - """ - return self.submit_function( - method, name=name, run_async=True, *args, **kwargs) - - def pre_run_task(self, task): - '''Callback when task enters the task queue - - :param task: the task - - Intended to be overridden by child classes to track task - progress. - ''' - self._log.debug( - "Manager %s running task %s", self.name, task.name) - - def run_task(self, task): - # Never call task.wait() in the run_task call stack because we - # might be running in another thread. The exception-shifting - # code is designed so that caller of submit_task (which may be - # in a different thread than this run_task) gets the - # exception. - # - # Note all threads go through the threadpool, so this is an - # async call. submit_task will wait() for the final result. - task.run() - - def post_run_task(self, elapsed_time, task): - '''Callback at task completion - - :param float elapsed_time: time in seconds between task entering - queue and finishing - :param task: the task - - This function is intended to be overridden by child classes to - monitor task runtimes. - ''' - self._log.debug( - "Manager %s ran task %s in %ss", - self.name, task.name, elapsed_time) - - -def wait_for_futures(futures, raise_on_error=True, log=_log): - '''Collect results or failures from a list of running future tasks.''' - - results = [] - retries = [] - - # Check on each result as its thread finishes - for completed in concurrent.futures.as_completed(futures): - try: - result = completed.result() - exceptions.raise_from_response(result) - results.append(result) - except (keystoneauth1.exceptions.RetriableConnectionFailure, - exceptions.HttpException) as e: - log.exception( - "Exception processing async task: {e}".format(e=str(e))) - if raise_on_error: - raise - # If we get an exception, put the result into a list so we - # can try again - retries.append(completed.result()) - return results, retries From 598c994e11f840f7a9996e9526a3c906e66ed993 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Thu, 4 Apr 2019 16:52:31 +0200 Subject: [PATCH 2440/3836] Support for the baremetal introspection service This change adds support for the baremetal introspection (aka ironic-inspector) API. The initial patch includes starting, stopping introspection, retrieving introspection statuses and introspection data. Change-Id: I4b8448316b1b6f6777ed0044374175ed794937f1 --- doc/source/user/index.rst | 2 + .../user/proxies/baremetal_introspection.rst | 25 +++ .../baremetal_introspection/index.rst | 7 + .../v1/introspection.rst | 13 ++ openstack/baremetal_introspection/__init__.py | 0 .../baremetal_introspection_service.py | 22 +++ .../baremetal_introspection/v1/__init__.py | 0 .../baremetal_introspection/v1/_proxy.py | 130 ++++++++++++++++ .../v1/introspection.py | 131 ++++++++++++++++ openstack/config/defaults.json | 1 + openstack/resource.py | 19 ++- .../unit/baremetal_introspection/__init__.py | 0 .../baremetal_introspection/v1/__init__.py | 0 .../baremetal_introspection/v1/test_proxy.py | 142 ++++++++++++++++++ openstack/tests/unit/test_resource.py | 3 +- ...emetal-introspection-973351b3ee76309e.yaml | 4 + 16 files changed, 495 insertions(+), 4 deletions(-) create mode 100644 doc/source/user/proxies/baremetal_introspection.rst create mode 100644 doc/source/user/resources/baremetal_introspection/index.rst create mode 100644 doc/source/user/resources/baremetal_introspection/v1/introspection.rst create mode 100644 openstack/baremetal_introspection/__init__.py create mode 100644 openstack/baremetal_introspection/baremetal_introspection_service.py create mode 100644 openstack/baremetal_introspection/v1/__init__.py create mode 100644 openstack/baremetal_introspection/v1/_proxy.py create mode 100644 openstack/baremetal_introspection/v1/introspection.py create mode 100644 openstack/tests/unit/baremetal_introspection/__init__.py create mode 100644 openstack/tests/unit/baremetal_introspection/v1/__init__.py create mode 100644 openstack/tests/unit/baremetal_introspection/v1/test_proxy.py create mode 100644 releasenotes/notes/baremetal-introspection-973351b3ee76309e.yaml diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index 4fa8a02f6..1070284cc 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -95,6 +95,7 @@ control which services can be used. :maxdepth: 1 Baremetal + Baremetal Introspection Block Storage Clustering Compute @@ -128,6 +129,7 @@ The following services have exposed *Resource* classes. :maxdepth: 1 Baremetal + Baremetal Introspection Block Storage Clustering Compute diff --git a/doc/source/user/proxies/baremetal_introspection.rst b/doc/source/user/proxies/baremetal_introspection.rst new file mode 100644 index 000000000..d6d712b9e --- /dev/null +++ b/doc/source/user/proxies/baremetal_introspection.rst @@ -0,0 +1,25 @@ +Baremetal Introspection API +=========================== + +.. automodule:: openstack.baremetal_introspection.v1._proxy + +The Baremetal Introspection Proxy +--------------------------------- + +The baremetal introspection high-level interface is available through +the ``baremetal_introspection`` member of a +:class:`~openstack.connection.Connection` object. +The ``baremetal_introspection`` member will only be added if the service is +detected. + +Introspection Process Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.baremetal_introspection.v1._proxy.Proxy + + .. automethod:: openstack.baremetal_introspection.v1._proxy.Proxy.introspections + .. automethod:: openstack.baremetal_introspection.v1._proxy.Proxy.get_introspection + .. automethod:: openstack.baremetal_introspection.v1._proxy.Proxy.get_introspection_data + .. automethod:: openstack.baremetal_introspection.v1._proxy.Proxy.start_introspection + .. automethod:: openstack.baremetal_introspection.v1._proxy.Proxy.wait_for_introspection + .. automethod:: openstack.baremetal_introspection.v1._proxy.Proxy.abort_introspection diff --git a/doc/source/user/resources/baremetal_introspection/index.rst b/doc/source/user/resources/baremetal_introspection/index.rst new file mode 100644 index 000000000..4f1b371c7 --- /dev/null +++ b/doc/source/user/resources/baremetal_introspection/index.rst @@ -0,0 +1,7 @@ +Baremetal Introspection Resources +================================= + +.. toctree:: + :maxdepth: 1 + + v1/introspection diff --git a/doc/source/user/resources/baremetal_introspection/v1/introspection.rst b/doc/source/user/resources/baremetal_introspection/v1/introspection.rst new file mode 100644 index 000000000..6275e254b --- /dev/null +++ b/doc/source/user/resources/baremetal_introspection/v1/introspection.rst @@ -0,0 +1,13 @@ +openstack.baremetal_introspection.v1.Introspection +================================================== + +.. automodule:: openstack.baremetal_introspection.v1.introspection + +The Introspection Class +----------------------- + +The ``Introspection`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.baremetal_introspection.v1.introspection.Introspection + :members: diff --git a/openstack/baremetal_introspection/__init__.py b/openstack/baremetal_introspection/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/baremetal_introspection/baremetal_introspection_service.py b/openstack/baremetal_introspection/baremetal_introspection_service.py new file mode 100644 index 000000000..aec3adbab --- /dev/null +++ b/openstack/baremetal_introspection/baremetal_introspection_service.py @@ -0,0 +1,22 @@ +# 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 service_description +from openstack.baremetal_introspection.v1 import _proxy + + +class BaremetalIntrospectionService(service_description.ServiceDescription): + """The bare metal introspection service.""" + + supported_versions = { + '1': _proxy.Proxy, + } diff --git a/openstack/baremetal_introspection/v1/__init__.py b/openstack/baremetal_introspection/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/baremetal_introspection/v1/_proxy.py b/openstack/baremetal_introspection/v1/_proxy.py new file mode 100644 index 000000000..23b7ff2b8 --- /dev/null +++ b/openstack/baremetal_introspection/v1/_proxy.py @@ -0,0 +1,130 @@ +# 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 _log +from openstack.baremetal_introspection.v1 import introspection as _introspect +from openstack import exceptions +from openstack import proxy + + +_logger = _log.setup_logging('openstack') + + +class Proxy(proxy.Proxy): + + def introspections(self, **query): + """Retrieve a generator of introspection records. + + :param dict query: Optional query parameters to be sent to restrict + the records to be returned. Available parameters include: + + * ``fields``: A list containing one or more fields to be returned + in the response. This may lead to some performance gain + because other fields of the resource are not refreshed. + * ``limit``: Requests at most the specified number of items be + returned from the query. + * ``marker``: Specifies the ID of the last-seen introspection. Use + the ``limit`` parameter to make an initial limited request and + use the ID of the last-seen introspection from the response as + the ``marker`` value in a subsequent limited request. + * ``sort_dir``: Sorts the response by the requested sort direction. + A valid value is ``asc`` (ascending) or ``desc`` + (descending). Default is ``asc``. You can specify multiple + pairs of sort key and sort direction query parameters. If + you omit the sort direction in a pair, the API uses the + natural sorting direction of the server attribute that is + provided as the ``sort_key``. + * ``sort_key``: Sorts the response by the this attribute value. + Default is ``id``. You can specify multiple pairs of sort + key and sort direction query parameters. If you omit the + sort direction in a pair, the API uses the natural sorting + direction of the server attribute that is provided as the + ``sort_key``. + + :returns: A generator of :class:`~.introspection.Introspection` + objects + """ + return _introspect.Introspection.list(self, **query) + + def start_introspection(self, node): + """Create a new introspection from attributes. + + :param node: The value can be either the name or ID of a node or + a :class:`~openstack.baremetal.v1.node.Node` instance. + + :returns: :class:`~.introspection.Introspection` instance. + """ + return self._create(_introspect.Introspection, id=node) + + def get_introspection(self, introspection): + """Get a specific introspection. + + :param introspection: The value can be the name or ID of an + introspection (matching bare metal node name or ID) or + an :class:`~.introspection.Introspection` instance. + :returns: :class:`~.introspection.Introspection` instance. + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + introspection matching the name or ID could be found. + """ + return self._get(_introspect.Introspection, introspection) + + def get_introspection_data(self, introspection): + """Get introspection data. + + :param introspection: The value can be the name or ID of an + introspection (matching bare metal node name or ID) or + an :class:`~.introspection.Introspection` instance. + :returns: introspection data from the most recent successful run. + :rtype: dict + """ + res = self._get_resource(_introspect.Introspection, introspection) + return res.get_data(self) + + def abort_introspection(self, introspection, ignore_missing=True): + """Abort an introspection. + + Note that the introspection is not aborted immediately, you may use + `wait_for_introspection` with `ignore_error=True`. + + :param introspection: The value can be the name or ID of an + introspection (matching bare metal node name or ID) or + an :class:`~.introspection.Introspection` instance. + :param bool ignore_missing: When set to ``False``, an exception + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the introspection could not be found. When set to ``True``, no + exception will be raised when attempting to abort a non-existent + introspection. + :returns: nothing + """ + res = self._get_resource(_introspect.Introspection, introspection) + try: + res.abort(self) + except exceptions.ResourceNotFound: + if not ignore_missing: + raise + + def wait_for_introspection(self, introspection, timeout=None, + ignore_error=False): + """Wait for the introspection to finish. + + :param introspection: The value can be the name or ID of an + introspection (matching bare metal node name or ID) or + an :class:`~.introspection.Introspection` instance. + :param timeout: How much (in seconds) to wait for the introspection. + The value of ``None`` (the default) means no client-side timeout. + :param ignore_error: If ``True``, this call will raise an exception + if the introspection reaches the ``error`` state. Otherwise the + error state is considered successful and the call returns. + :returns: :class:`~.introspection.Introspection` instance. + """ + res = self._get_resource(_introspect.Introspection, introspection) + return res.wait(self, timeout=timeout, ignore_error=ignore_error) diff --git a/openstack/baremetal_introspection/v1/introspection.py b/openstack/baremetal_introspection/v1/introspection.py new file mode 100644 index 000000000..e560f38a5 --- /dev/null +++ b/openstack/baremetal_introspection/v1/introspection.py @@ -0,0 +1,131 @@ +# 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 _log +from openstack.baremetal.v1 import _common +from openstack import exceptions +from openstack import resource +from openstack import utils + + +_logger = _log.setup_logging('openstack') + + +class Introspection(resource.Resource): + + resources_key = 'introspection' + base_path = '/introspection' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = False + allow_delete = True + allow_list = True + + # created via POST with ID + create_method = 'POST' + create_requires_id = True + create_returns_body = False + + #: Timestamp at which the introspection was finished. + finished_at = resource.Body('finished_at') + #: The last error message (if any). + error = resource.Body('error') + #: The UUID of the introspection (matches the node UUID). + id = resource.Body('uuid', alternate_id=True) + #: Whether introspection is finished. + is_finished = resource.Body('finished', type=bool) + #: A list of relative links, including the self and bookmark links. + links = resource.Body('links', type=list) + #: Timestamp at which the introspection was started. + started_at = resource.Body('started_at') + #: The current introspection state. + state = resource.Body('state') + + def abort(self, session): + """Abort introspection. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + """ + if self.is_finished: + return + + session = self._get_session(session) + + version = self._get_microversion_for(session, 'delete') + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'abort') + response = session.post( + request.url, headers=request.headers, microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + msg = ("Failed to abort introspection for node {id}" + .format(id=self.id)) + exceptions.raise_from_response(response, error_message=msg) + + def get_data(self, session): + """Get introspection data. + + Note that the introspection data format is not stable and can vary + from environment to environment. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :returns: introspection data from the most recent successful run. + :rtype: dict + """ + session = self._get_session(session) + + version = self._get_microversion_for(session, 'fetch') + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'data') + response = session.get( + request.url, headers=request.headers, microversion=version) + msg = ("Failed to fetch introspection data for node {id}" + .format(id=self.id)) + exceptions.raise_from_response(response, error_message=msg) + return response.json() + + def wait(self, session, timeout=None, ignore_error=False): + """Wait for the node to reach the expected state. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param timeout: How much (in seconds) to wait for the introspection. + The value of ``None`` (the default) means no client-side timeout. + :param ignore_error: If ``True``, this call will raise an exception + if the introspection reaches the ``error`` state. Otherwise the + error state is considered successful and the call returns. + :return: This :class:`Introspection` instance. + """ + if self._check_state(ignore_error): + return self + + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for introspection on node %s" % self.id): + self.fetch(session) + if self._check_state(ignore_error): + return self + + _logger.debug('Still waiting for introspection of node %(node)s, ' + 'the current state is "%(state)s"', + {'node': self.id, 'state': self.state}) + + def _check_state(self, ignore_error): + if self.state == 'error' and not ignore_error: + raise exceptions.SDKException( + "Introspection of node %(node)s failed: %(error)s" % + {'node': self.id, 'error': self.error}) + else: + return self.is_finished diff --git a/openstack/config/defaults.json b/openstack/config/defaults.json index 3eb5213c7..2d154e3a9 100644 --- a/openstack/config/defaults.json +++ b/openstack/config/defaults.json @@ -1,6 +1,7 @@ { "auth_type": "password", "baremetal_status_code_retries": 5, + "baremetal_introspection_status_code_retries": 5, "image_status_code_retries": 5, "disable_vendor_agent": {}, "interface": "public", diff --git a/openstack/resource.py b/openstack/resource.py index fb88d2087..7b1007700 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -406,8 +406,12 @@ class Resource(dict): #: Do calls for this resource require an id requires_id = True + #: Whether create requires an ID (determined from method if None). + create_requires_id = None #: Do responses for this resource have bodies has_body = True + #: Does create returns a body (if False requires ID), defaults to has_body + create_returns_body = None #: Maximum microversion to use for getting/creating/updating the Resource _max_microversion = None @@ -1104,15 +1108,18 @@ def create(self, session, prepend_key=True, base_path=None): session = self._get_session(session) microversion = self._get_microversion_for(session, 'create') + requires_id = (self.create_requires_id + if self.create_requires_id is not None + else self.create_method == 'PUT') if self.create_method == 'PUT': - request = self._prepare_request(requires_id=True, + request = self._prepare_request(requires_id=requires_id, prepend_key=prepend_key, base_path=base_path) response = session.put(request.url, json=request.body, headers=request.headers, microversion=microversion) elif self.create_method == 'POST': - request = self._prepare_request(requires_id=False, + request = self._prepare_request(requires_id=requires_id, prepend_key=prepend_key, base_path=base_path) response = session.post(request.url, @@ -1122,8 +1129,14 @@ def create(self, session, prepend_key=True, base_path=None): raise exceptions.ResourceFailure( msg="Invalid create method: %s" % self.create_method) + has_body = (self.has_body if self.create_returns_body is None + else self.create_returns_body) self.microversion = microversion - self._translate_response(response) + self._translate_response(response, has_body=has_body) + # direct comparision to False since we need to rule out None + if self.has_body and self.create_returns_body is False: + # fetch the body if it's required but not returned by create + return self.fetch(session) return self def fetch(self, session, requires_id=True, diff --git a/openstack/tests/unit/baremetal_introspection/__init__.py b/openstack/tests/unit/baremetal_introspection/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/unit/baremetal_introspection/v1/__init__.py b/openstack/tests/unit/baremetal_introspection/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py b/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py new file mode 100644 index 000000000..92eb50eaf --- /dev/null +++ b/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py @@ -0,0 +1,142 @@ +# 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 keystoneauth1 import adapter + +from openstack.baremetal_introspection.v1 import _proxy +from openstack.baremetal_introspection.v1 import introspection +from openstack import exceptions +from openstack.tests.unit import base +from openstack.tests.unit import test_proxy_base + + +class TestBaremetalIntrospectionProxy(test_proxy_base.TestProxyBase): + + def setUp(self): + super(TestBaremetalIntrospectionProxy, self).setUp() + self.proxy = _proxy.Proxy(self.session) + + def test_create_introspection(self): + self.verify_create(self.proxy.start_introspection, + introspection.Introspection, + method_kwargs={'node': 'abcd'}, + expected_kwargs={'id': 'abcd'}) + + def test_get_introspection(self): + self.verify_get(self.proxy.get_introspection, + introspection.Introspection) + + +@mock.patch('time.sleep', lambda _sec: None) +@mock.patch.object(introspection.Introspection, 'fetch', autospec=True) +class TestWaitForIntrospection(base.TestCase): + + def setUp(self): + super(TestWaitForIntrospection, self).setUp() + self.session = mock.Mock(spec=adapter.Adapter) + self.proxy = _proxy.Proxy(self.session) + self.fake = {'state': 'waiting', 'error': None, 'finished': False} + self.introspection = introspection.Introspection(**self.fake) + + def test_already_finished(self, mock_fetch): + self.introspection.is_finished = True + self.introspection.state = 'finished' + result = self.proxy.wait_for_introspection(self.introspection) + self.assertIs(result, self.introspection) + self.assertFalse(mock_fetch.called) + + def test_wait(self, mock_fetch): + marker = [False] # mutable object to modify in the closure + + def _side_effect(allocation, session): + if marker[0]: + self.introspection.state = 'finished' + self.introspection.is_finished = True + else: + self.introspection.state = 'processing' + marker[0] = True + + mock_fetch.side_effect = _side_effect + result = self.proxy.wait_for_introspection(self.introspection) + self.assertIs(result, self.introspection) + self.assertEqual(2, mock_fetch.call_count) + + def test_timeout(self, mock_fetch): + self.assertRaises(exceptions.ResourceTimeout, + self.proxy.wait_for_introspection, + self.introspection, + timeout=0.001) + mock_fetch.assert_called_with(self.introspection, self.proxy) + + def test_failure(self, mock_fetch): + def _side_effect(allocation, session): + self.introspection.state = 'error' + self.introspection.is_finished = True + self.introspection.error = 'boom' + + mock_fetch.side_effect = _side_effect + self.assertRaisesRegex(exceptions.SDKException, 'boom', + self.proxy.wait_for_introspection, + self.introspection) + mock_fetch.assert_called_once_with(self.introspection, self.proxy) + + def test_failure_ignored(self, mock_fetch): + def _side_effect(allocation, session): + self.introspection.state = 'error' + self.introspection.is_finished = True + self.introspection.error = 'boom' + + mock_fetch.side_effect = _side_effect + result = self.proxy.wait_for_introspection(self.introspection, + ignore_error=True) + self.assertIs(result, self.introspection) + mock_fetch.assert_called_once_with(self.introspection, self.proxy) + + +@mock.patch.object(_proxy.Proxy, 'request', autospec=True) +class TestAbortIntrospection(base.TestCase): + + def setUp(self): + super(TestAbortIntrospection, self).setUp() + self.session = mock.Mock(spec=adapter.Adapter) + self.proxy = _proxy.Proxy(self.session) + self.fake = {'id': '1234', 'finished': False} + self.introspection = introspection.Introspection(**self.fake) + + def test_abort(self, mock_request): + mock_request.return_value.status_code = 202 + self.proxy.abort_introspection(self.introspection) + mock_request.assert_called_once_with( + self.proxy, 'introspection/1234/abort', 'POST', + headers=mock.ANY, microversion=mock.ANY, + retriable_status_codes=[409, 503]) + + +@mock.patch.object(_proxy.Proxy, 'request', autospec=True) +class TestGetData(base.TestCase): + + def setUp(self): + super(TestGetData, self).setUp() + self.session = mock.Mock(spec=adapter.Adapter) + self.proxy = _proxy.Proxy(self.session) + self.fake = {'id': '1234', 'finished': False} + self.introspection = introspection.Introspection(**self.fake) + + def test_get_data(self, mock_request): + mock_request.return_value.status_code = 200 + data = self.proxy.get_introspection_data(self.introspection) + mock_request.assert_called_once_with( + self.proxy, 'introspection/1234/data', 'GET', + headers=mock.ANY, microversion=mock.ANY) + self.assertIs(data, mock_request.return_value.json.return_value) diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index cf14a1bda..9042de7bf 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1120,7 +1120,8 @@ def _test_create(self, cls, requires_id=False, prepend_key=False, microversion=microversion) self.assertEqual(sot.microversion, microversion) - sot._translate_response.assert_called_once_with(self.response) + sot._translate_response.assert_called_once_with(self.response, + has_body=sot.has_body) self.assertEqual(result, sot) def test_put_create(self): diff --git a/releasenotes/notes/baremetal-introspection-973351b3ee76309e.yaml b/releasenotes/notes/baremetal-introspection-973351b3ee76309e.yaml new file mode 100644 index 000000000..7ab2885bc --- /dev/null +++ b/releasenotes/notes/baremetal-introspection-973351b3ee76309e.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Adds support for the bare metal introspection service. From f198347552b537a2a492692c1e43660d58b7dbf6 Mon Sep 17 00:00:00 2001 From: OpenDev Sysadmins Date: Fri, 19 Apr 2019 19:47:46 +0000 Subject: [PATCH 2441/3836] OpenDev Migration Patch This commit was bulk generated and pushed by the OpenDev sysadmins as a part of the Git hosting and code review systems migration detailed in these mailing list posts: http://lists.openstack.org/pipermail/openstack-discuss/2019-March/003603.html http://lists.openstack.org/pipermail/openstack-discuss/2019-April/004920.html Attempts have been made to correct repository namespaces and hostnames based on simple pattern matching, but it's possible some were updated incorrectly or missed entirely. Please reach out to us via the contact information listed at https://opendev.org/ with any questions you may have. --- .gitreview | 2 +- .zuul.yaml | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.gitreview b/.gitreview index 6b3a0e0b8..9e465c3ef 100644 --- a/.gitreview +++ b/.gitreview @@ -1,4 +1,4 @@ [gerrit] -host=review.openstack.org +host=review.opendev.org port=29418 project=openstack/openstacksdk.git diff --git a/.zuul.yaml b/.zuul.yaml index 9a89154cc..11a883440 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -5,7 +5,7 @@ Run tox python 36 unittests against master of important libs vars: tox_install_siblings: true - zuul_work_dir: src/git.openstack.org/openstack/openstacksdk + zuul_work_dir: src/opendev.org/openstack/openstacksdk # openstacksdk in required-projects so that osc and keystoneauth # can add the job as well required-projects: @@ -30,7 +30,7 @@ post-run: playbooks/devstack/post.yaml roles: # NOTE: We pull in roles from the tempest repo for stackviz processing. - - zuul: git.openstack.org/openstack/tempest + - zuul: opendev.org/openstack/tempest required-projects: # These jobs will DTRT when openstacksdk triggers them, but we want to # make sure stable branches of openstacksdk never get cloned by other @@ -65,7 +65,7 @@ tox_envlist: functional zuul_copy_output: '{{ ansible_user_dir }}/stackviz': logs - zuul_work_dir: src/git.openstack.org/openstack/openstacksdk + zuul_work_dir: src/opendev.org/openstack/openstacksdk - job: name: openstacksdk-functional-devstack-base @@ -74,7 +74,7 @@ Base job for devstack-based functional tests vars: devstack_plugins: - neutron: https://git.openstack.org/openstack/neutron + neutron: https://opendev.org/openstack/neutron devstack_local_conf: post-config: $CINDER_CONF: @@ -109,7 +109,7 @@ DISABLE_AMP_IMAGE_BUILD: true Q_SERVICE_PLUGIN_CLASSES: qos,trunk devstack_plugins: - heat: https://git.openstack.org/openstack/heat + heat: https://opendev.org/openstack/heat tox_environment: OPENSTACKSDK_HAS_HEAT: 1 devstack_services: @@ -148,9 +148,9 @@ devstack_localrc: Q_SERVICE_PLUGIN_CLASSES: qos,trunk,firewall_v2 devstack_plugins: - designate: https://git.openstack.org/openstack/designate - octavia: https://git.openstack.org/openstack/octavia - neutron-fwaas: https://git.openstack.org/openstack/neutron-fwaas + designate: https://opendev.org/openstack/designate + octavia: https://opendev.org/openstack/octavia + neutron-fwaas: https://opendev.org/openstack/neutron-fwaas devstack_services: designate: true octavia: true @@ -214,7 +214,7 @@ - openstack/python-magnumclient vars: devstack_plugins: - magnum: https://git.openstack.org/openstack/magnum + magnum: https://opendev.org/openstack/magnum devstack_localrc: MAGNUM_GUEST_IMAGE_URL: https://tarballs.openstack.org/magnum/images/fedora-atomic-f23-dib.qcow2 MAGNUM_IMAGE_NAME: fedora-atomic-f23-dib @@ -236,7 +236,7 @@ - openstack/senlin vars: devstack_plugins: - senlin: https://git.openstack.org/openstack/senlin + senlin: https://opendev.org/openstack/senlin devstack_services: s-account: false s-container: false @@ -265,7 +265,7 @@ IRONIC_VM_LOG_DIR: '{{ devstack_base_dir }}/ironic-bm-logs' IRONIC_VM_SPECS_RAM: 384 devstack_plugins: - ironic: https://git.openstack.org/openstack/ironic + ironic: https://opendev.org/openstack/ironic devstack_services: c-api: false c-bak: false @@ -324,7 +324,7 @@ override-checkout: devel - name: openstack/openstacksdk override-checkout: master - - name: openstack-dev/devstack + - name: openstack/devstack override-checkout: master vars: # test-matrix grabs branch from the zuul branch setting. If the job @@ -345,7 +345,7 @@ override-checkout: stable-2.6 - name: openstack/openstacksdk override-checkout: master - - name: openstack-dev/devstack + - name: openstack/devstack override-checkout: master vars: # test-matrix grabs branch from the zuul branch setting. If the job @@ -364,7 +364,7 @@ - openstack/masakari-monitors vars: devstack_plugins: - masakari: https://git.openstack.org/openstack/masakari + masakari: https://opendev.org/openstack/masakari devstack_services: masakari-api: true masakari-engine: true From 737bcb0eca52c5e6980607a5a5a3a1c3363a77fd Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 20 Apr 2019 20:39:17 +0000 Subject: [PATCH 2442/3836] Finish updating links to point to opendev The redirects work, but it's nicer to just use opendev links directly. Also, update shade, which is in openstack namespace now. Depends-On: https://review.opendev.org/654230 Depends-On: https://review.opendev.org/654233 Change-Id: Id559b79c2aefda50fa3ec0feedf6d8c52a687a75 --- CONTRIBUTING.rst | 2 +- README.rst | 2 +- devstack/plugin.sh | 2 +- doc/source/user/guides/dns.rst | 2 +- doc/source/user/multi-cloud-demo.rst | 2 +- playbooks/devstack/legacy-git.yaml | 4 ++-- tox.ini | 6 +++--- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 9cd75cae1..2478e7b07 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -42,4 +42,4 @@ Code Hosting https://opendev.org/openstack/openstacksdk Code Review - https://review.openstack.org/#/q/status:open+project:openstack/openstacksdk,n,z + https://review.opendev.org/#/q/status:open+project:openstack/openstacksdk,n,z diff --git a/README.rst b/README.rst index da434d546..b5c72b484 100644 --- a/README.rst +++ b/README.rst @@ -156,7 +156,7 @@ Links ===== * `Issue Tracker `_ -* `Code Review `_ +* `Code Review `_ * `Documentation `_ * `PyPI `_ * `Mailing list `_ diff --git a/devstack/plugin.sh b/devstack/plugin.sh index 0aacd6070..d1a53c15d 100644 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -3,7 +3,7 @@ # To enable openstacksdk in devstack add an entry to local.conf that looks like # # [[local|localrc]] -# enable_plugin openstacksdk https://git.openstack.org/openstack/openstacksdk +# enable_plugin openstacksdk https://opendev.org/openstack/openstacksdk function preinstall_openstacksdk { : diff --git a/doc/source/user/guides/dns.rst b/doc/source/user/guides/dns.rst index f2ba4fbcf..c2d268083 100644 --- a/doc/source/user/guides/dns.rst +++ b/doc/source/user/guides/dns.rst @@ -15,4 +15,4 @@ List Zones Full example: `dns resource list`_ -.. _dns resource list: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/dns/list.py +.. _dns resource list: https://opendev.org/openstack/openstacksdk/src/branch/master/examples/dns/list.py diff --git a/doc/source/user/multi-cloud-demo.rst b/doc/source/user/multi-cloud-demo.rst index 87a3dcfb9..c013fe7b5 100644 --- a/doc/source/user/multi-cloud-demo.rst +++ b/doc/source/user/multi-cloud-demo.rst @@ -45,7 +45,7 @@ What are we going to talk about? shade is Free Software ====================== -* https://opendev.org/openstack-infra/shade +* https://opendev.org/openstack/shade * openstack-discuss@lists.openstack.org * #openstack-shade on freenode diff --git a/playbooks/devstack/legacy-git.yaml b/playbooks/devstack/legacy-git.yaml index 5713daf72..96ba6d550 100644 --- a/playbooks/devstack/legacy-git.yaml +++ b/playbooks/devstack/legacy-git.yaml @@ -4,8 +4,8 @@ - name: Set openstacksdk libraries to master branch before functional tests command: git checkout master args: - chdir: "src/git.openstack.org/{{ item }}" + chdir: "src/opendev.org/{{ item }}" with_items: - - openstack-infra/shade + - openstack/shade - openstack/keystoneauth - openstack/os-client-config diff --git a/tox.ini b/tox.ini index 726004b65..11246b824 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,7 @@ setenv = OS_STDOUT_CAPTURE={env:OS_STDOUT_CAPTURE:true} OS_STDERR_CAPTURE={env:OS_STDERR_CAPTURE:true} deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} + -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/branch/master/upper-constraints.txt} -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt commands = stestr run {posargs} @@ -78,14 +78,14 @@ commands = {toxinidir}/extras/run-ansible-tests.sh -e {envdir} {posargs} [testenv:docs] deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} + -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/branch/master/upper-constraints.txt} -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = sphinx-build -W -d doc/build/doctrees -b html doc/source/ doc/build/html [testenv:releasenotes] deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} + -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/branch/master/upper-constraints.txt} -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html From 9fb79904a4c4e6fc3a035145cb4b71f86a6731e8 Mon Sep 17 00:00:00 2001 From: Daniel Speichert Date: Fri, 15 Mar 2019 16:15:18 -0400 Subject: [PATCH 2443/3836] Deduplicate next-page URL's query params This avoid HTTP 414 caused by endless addition of query params onto the next-page's URL that already contains them in case where the number of pages is high. Change-Id: I4f89e8e4837bb7c08c841e50070541038a2d2cc2 --- openstack/resource.py | 11 +++++++ openstack/tests/unit/test_resource.py | 47 +++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/openstack/resource.py b/openstack/resource.py index 718037020..92a6787b3 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1429,6 +1429,17 @@ def _get_next_link(cls, uri, response, data, marker, limit, total_yielded): if limit: params['limit'] = limit next_link = uri + + # Parse params from Link (next page URL) into params. + # This prevents duplication of query parameters that with large + # number of pages result in HTTP 414 error eventually. + if next_link: + parts = six.moves.urllib.parse.urlparse(next_link) + query_params = six.moves.urllib.parse.parse_qs(parts.query) + params.update(query_params) + next_link = six.moves.urllib.parse.urljoin(next_link, + parts.path) + # If we still have no link, and limit was given and is non-zero, # and the number of records yielded equals the limit, then the user # is playing pagination ball so we should go ahead and try once more. diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index ee98777f0..a87cd6115 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1534,6 +1534,53 @@ def test_list_response_paginated_with_links(self): self.assertEqual(2, len(self.session.get.call_args_list)) self.assertIsInstance(results[0], self.test_class) + def test_list_response_paginated_with_links_and_query(self): + q_limit = 1 + ids = [1, 2] + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.links = {} + mock_response.json.side_effect = [ + { + "resources": [{"id": ids[0]}], + "resources_links": [{ + "href": "https://example.com/next-url?limit=%d" % q_limit, + "rel": "next", + }] + }, { + "resources": [{"id": ids[1]}], + }, { + "resources": [], + }] + + self.session.get.return_value = mock_response + + class Test(self.test_class): + _query_mapping = resource.QueryParameters("limit") + + results = list(Test.list(self.session, paginated=True, limit=q_limit)) + + self.assertEqual(2, len(results)) + self.assertEqual(ids[0], results[0].id) + self.assertEqual(ids[1], results[1].id) + self.assertEqual( + mock.call('base_path', + headers={'Accept': 'application/json'}, params={ + 'limit': q_limit, + }, + microversion=None), + self.session.get.mock_calls[0]) + self.assertEqual( + mock.call('https://example.com/next-url', + headers={'Accept': 'application/json'}, params={ + 'limit': [str(q_limit)], + }, + microversion=None), + self.session.get.mock_calls[2]) + + self.assertEqual(3, len(self.session.get.call_args_list)) + self.assertIsInstance(results[0], self.test_class) + def test_list_response_paginated_with_microversions(self): class Test(resource.Resource): service = self.service_name From 769646c5c1a449d2c15e19e820e75d89f1414b69 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 25 Apr 2019 15:22:51 +0200 Subject: [PATCH 2444/3836] Update compute.server resource - Add current supported list filters - Add missing attributes (from newer microversions) - Sort attributes in the resource - Use example response from compute docs in unittest (not to miss new attributes) - add _max_microversion (and respective discovery calls in tests) Change-Id: Iad1b57aee0affa345a2812c2b504d00a60441f27 --- openstack/cloud/_normalize.py | 17 +- openstack/compute/v2/_proxy.py | 29 +-- openstack/compute/v2/server.py | 169 +++++++++------ openstack/tests/unit/cloud/test_caching.py | 1 + .../tests/unit/cloud/test_create_server.py | 3 + .../tests/unit/cloud/test_delete_server.py | 9 + .../unit/cloud/test_floating_ip_neutron.py | 1 + openstack/tests/unit/cloud/test_normalize.py | 46 ++++- .../tests/unit/cloud/test_rebuild_server.py | 4 + .../tests/unit/cloud/test_security_groups.py | 5 + .../tests/unit/cloud/test_server_console.py | 1 + .../unit/cloud/test_server_delete_metadata.py | 2 + .../unit/cloud/test_server_set_metadata.py | 2 + openstack/tests/unit/cloud/test_shade.py | 7 + .../tests/unit/cloud/test_update_server.py | 2 + .../tests/unit/compute/v2/test_server.py | 192 +++++++++++++----- .../tests/unit/config/test_from_session.py | 1 + openstack/tests/unit/test_stats.py | 1 + 18 files changed, 355 insertions(+), 137 deletions(-) diff --git a/openstack/cloud/_normalize.py b/openstack/cloud/_normalize.py index 598d982ad..6101d1028 100644 --- a/openstack/cloud/_normalize.py +++ b/openstack/cloud/_normalize.py @@ -38,6 +38,7 @@ 'addresses', 'adminPass', 'created', + 'description', 'key_name', 'metadata', 'networks', @@ -45,6 +46,7 @@ 'private_v4', 'public_v4', 'public_v6', + 'server_groups', 'status', 'updated', 'user_id', @@ -468,9 +470,11 @@ def _normalize_server(self, server): # OpenStack can return image as a string when you've booted # from volume - if str(server['image']) != server['image']: - server['image'].pop('links', None) - ret['image'] = server.pop('image') + image = server.pop('image', None) + if str(image) != image: + image = munch.Munch(id=image['id']) + + ret['image'] = image # From original_names from sdk server.pop('imageRef', None) # From original_names from sdk @@ -515,6 +519,13 @@ def _normalize_server(self, server): 'OS-EXT-SRV-ATTR:hypervisor_hostname', 'OS-EXT-SRV-ATTR:instance_name', 'OS-EXT-SRV-ATTR:user_data', + 'OS-EXT-SRV-ATTR:host', + 'OS-EXT-SRV-ATTR:hostname', + 'OS-EXT-SRV-ATTR:kernel_id', + 'OS-EXT-SRV-ATTR:launch_index', + 'OS-EXT-SRV-ATTR:ramdisk_id', + 'OS-EXT-SRV-ATTR:reservation_id', + 'OS-EXT-SRV-ATTR:root_device_name', 'OS-SCH-HNT:scheduler_hints', ): short_key = key.split(':')[1] diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 31183d14c..0e1434ea4 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -487,33 +487,8 @@ def servers(self, details=True, all_projects=False, **query): instances with only basic data will be returned. The default, ``True``, will cause instances with full data to be returned. :param kwargs query: Optional query parameters to be sent to limit - the servers being returned. Available parameters include: - - * changes_since: A time/date stamp for when the server last changed - status. - * image: An image resource or ID. - * flavor: A flavor resource or ID. - * name: Name of the server as a string. Can be queried with - regular expressions. The regular expression - ?name=bob returns both bob and bobb. If you must match on - only bob, you can use a regular expression that - matches the syntax of the underlying database server that - is implemented for Compute, such as MySQL or PostgreSQL. - * status: Value of the status of the server so that you can filter - on "ACTIVE" for example. - * host: Name of the host as a string. - * all_projects: Flag to request servers be returned from all - projects, not just the currently scoped one. - * limit: Requests a specified page size of returned items from the - query. Returns a number of items up to the specified - limit value. Use the limit parameter to make an initial - limited request and use the ID of the last-seen item from - the response as the marker parameter value in a subsequent - limited request. - * marker: Specifies the ID of the last-seen item. Use the limit - parameter to make an initial limited request and use the - ID of the last-seen item from the response as the marker - parameter value in a subsequent limited request. + the servers being returned. Available parameters can be seen + under https://developer.openstack.org/api-ref/compute/#list-servers :returns: A generator of server instances. """ diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index d29fe6e70..d7198f493 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -11,6 +11,7 @@ # under the License. from openstack.compute.v2 import metadata +from openstack.image.v2 import image from openstack import resource from openstack import utils @@ -28,19 +29,32 @@ class Server(resource.Resource, metadata.MetadataMixin, resource.TagMixin): allow_list = True _query_mapping = resource.QueryParameters( - "image", "flavor", "name", - "status", "host", + "auto_disk_config", "availability_zone", + "created_at", "description", "flavor", + "hostname", "image", "kernel_id", "key_name", + "launch_index", "launched_at", "locked_by", "name", + "node", "power_state", "progress", "project_id", "ramdisk_id", + "reservation_id", "root_device_name", + "status", "task_state", "terminated_at", "user_id", + "vm_state", "sort_key", "sort_dir", - "reservation_id", "tags", - "project_id", - is_deleted="deleted", + access_ipv4="access_ip_v4", + access_ipv6="access_ip_v6", + has_config_drive="config_drive", + deleted_only="deleted", + compute_host="host", + is_soft_deleted="soft_deleted", ipv4_address="ip", ipv6_address="ip6", changes_since="changes-since", + changes_before="changes-before", + id="uuid", all_projects="all_tenants", **resource.TagMixin._tag_query_parameters ) + _max_microversion = '2.72' + #: A list of dictionaries holding links relevant to this server. links = resource.Body('links') @@ -53,92 +67,131 @@ class Server(resource.Resource, metadata.MetadataMixin, resource.TagMixin): #: and ``version``, which is either 4 or 6 depending on the protocol #: of the IP address. *Type: dict* addresses = resource.Body('addresses', type=dict) + #: When a server is first created, it provides the administrator password. + admin_password = resource.Body('adminPass') + #: A list of an attached volumes. Each item in the list contains at least + #: an "id" key to identify the specific volumes. + attached_volumes = resource.Body( + 'os-extended-volumes:volumes_attached') + #: The name of the availability zone this server is a part of. + availability_zone = resource.Body('OS-EXT-AZ:availability_zone') + #: Enables fine grained control of the block device mapping for an + #: instance. This is typically used for booting servers from volumes. + block_device_mapping = resource.Body('block_device_mapping_v2') + #: Indicates whether or not a config drive was used for this server. + config_drive = resource.Body('config_drive') + #: The name of the compute host on which this instance is running. + #: Appears in the response for administrative users only. + compute_host = resource.Body('OS-EXT-SRV-ATTR:host') #: Timestamp of when the server was created. created_at = resource.Body('created') + #: The description of the server. Before microversion + #: 2.19 this was set to the server name. + description = resource.Body('description') + #: The disk configuration. Either AUTO or MANUAL. + disk_config = resource.Body('OS-DCF:diskConfig') #: The flavor reference, as a ID or full URL, for the flavor to use for #: this server. flavor_id = resource.Body('flavorRef') #: The flavor property as returned from server. + # TODO(gtema): replace with flavor.Flavor addressing flavor.original_name flavor = resource.Body('flavor', type=dict) + #: Indicates whether a configuration drive enables metadata injection. + #: Not all cloud providers enable this feature. + has_config_drive = resource.Body('config_drive') #: An ID representing the host of this server. host_id = resource.Body('hostId') + #: The host status. + host_status = resource.Body('host_status') + #: The hostname set on the instance when it is booted. + #: By default, it appears in the response for administrative users only. + hostname = resource.Body('OS-EXT-SRV-ATTR:hostname') + #: The hypervisor host name. Appears in the response for administrative + #: users only. + hypervisor_hostname = resource.Body('OS-EXT-SRV-ATTR:hypervisor_hostname') #: The image reference, as a ID or full URL, for the image to use for #: this server. image_id = resource.Body('imageRef') #: The image property as returned from server. - image = resource.Body('image', type=dict) + image = resource.Body('image', type=image.Image) + #: The instance name. The Compute API generates the instance name from the + #: instance name template. Appears in the response for administrative users + #: only. + instance_name = resource.Body('OS-EXT-SRV-ATTR:instance_name') + # The locked status of the server + is_locked = resource.Body('locked', type=bool) + #: The UUID of the kernel image when using an AMI. Will be null if not. + #: By default, it appears in the response for administrative users only. + kernel_id = resource.Body('OS-EXT-SRV-ATTR:kernel_id') + #: The name of an associated keypair + key_name = resource.Body('key_name') + #: When servers are launched via multiple create, this is the + #: sequence in which the servers were launched. By default, it + #: appears in the response for administrative users only. + launch_index = resource.Body('OS-EXT-SRV-ATTR:launch_index', type=int) + #: The timestamp when the server was launched. + launched_at = resource.Body('OS-SRV-USG:launched_at') #: Metadata stored for this server. *Type: dict* metadata = resource.Body('metadata', type=dict) + #: A networks object. Required parameter when there are multiple + #: networks defined for the tenant. When you do not specify the + #: networks parameter, the server attaches to the only network + #: created for the current tenant. + networks = resource.Body('networks') + #: The file path and contents, text only, to inject into the server at + #: launch. The maximum size of the file path data is 255 bytes. + #: The maximum limit is The number of allowed bytes in the decoded, + #: rather than encoded, data. + personality = resource.Body('personality') + #: The power state of this server. + power_state = resource.Body('OS-EXT-STS:power_state') #: While the server is building, this value represents the percentage #: of completion. Once it is completed, it will be 100. *Type: int* progress = resource.Body('progress', type=int) #: The ID of the project this server is associated with. project_id = resource.Body('tenant_id') + #: The UUID of the ramdisk image when using an AMI. Will be null if not. + #: By default, it appears in the response for administrative users only. + ramdisk_id = resource.Body('OS-EXT-SRV-ATTR:ramdisk_id') + #: The reservation id for the server. This is an id that can be + #: useful in tracking groups of servers created with multiple create, + #: that will all have the same reservation_id. By default, it appears + #: in the response for administrative users only. + reservation_id = resource.Body('OS-EXT-SRV-ATTR:reservation_id') + #: The root device name for the instance By default, it appears in the + #: response for administrative users only. + root_device_name = resource.Body('OS-EXT-SRV-ATTR:root_device_name') + #: The dictionary of data to send to the scheduler. + scheduler_hints = resource.Body('OS-SCH-HNT:scheduler_hints', type=dict) + #: A list of applicable security groups. Each group contains keys for + #: description, name, id, and rules. + security_groups = resource.Body('security_groups') + #: The UUIDs of the server groups to which the server belongs. + #: Currently this can contain at most one entry. + server_groups = resource.Body('server_groups', type=list, list_type=dict) #: The state this server is in. Valid values include ``ACTIVE``, #: ``BUILDING``, ``DELETED``, ``ERROR``, ``HARD_REBOOT``, ``PASSWORD``, #: ``PAUSED``, ``REBOOT``, ``REBUILD``, ``RESCUED``, ``RESIZED``, #: ``REVERT_RESIZE``, ``SHUTOFF``, ``SOFT_DELETED``, ``STOPPED``, #: ``SUSPENDED``, ``UNKNOWN``, or ``VERIFY_RESIZE``. status = resource.Body('status') - #: Timestamp of when this server was last updated. - updated_at = resource.Body('updated') - #: The ID of the owners of this server. - user_id = resource.Body('user_id') - #: The name of an associated keypair - key_name = resource.Body('key_name') - #: The disk configuration. Either AUTO or MANUAL. - disk_config = resource.Body('OS-DCF:diskConfig') - #: Indicates whether a configuration drive enables metadata injection. - #: Not all cloud providers enable this feature. - has_config_drive = resource.Body('config_drive') - #: The name of the availability zone this server is a part of. - availability_zone = resource.Body('OS-EXT-AZ:availability_zone') - #: The power state of this server. - power_state = resource.Body('OS-EXT-STS:power_state') #: The task state of this server. task_state = resource.Body('OS-EXT-STS:task_state') - #: The VM state of this server. - vm_state = resource.Body('OS-EXT-STS:vm_state') - #: A list of an attached volumes. Each item in the list contains at least - #: an "id" key to identify the specific volumes. - attached_volumes = resource.Body( - 'os-extended-volumes:volumes_attached') - #: The timestamp when the server was launched. - launched_at = resource.Body('OS-SRV-USG:launched_at') #: The timestamp when the server was terminated (if it has been). terminated_at = resource.Body('OS-SRV-USG:terminated_at') - #: A list of applicable security groups. Each group contains keys for - #: description, name, id, and rules. - security_groups = resource.Body('security_groups') - #: When a server is first created, it provides the administrator password. - admin_password = resource.Body('adminPass') - #: The file path and contents, text only, to inject into the server at - #: launch. The maximum size of the file path data is 255 bytes. - #: The maximum limit is The number of allowed bytes in the decoded, - #: rather than encoded, data. - personality = resource.Body('personality') + #: A list of trusted certificate IDs, that were used during image + #: signature verification to verify the signing certificate. + trusted_image_certificates = resource.Body( + 'trusted_image_certificates', type=list) + #: Timestamp of when this server was last updated. + updated_at = resource.Body('updated') #: Configuration information or scripts to use upon launch. #: Must be Base64 encoded. user_data = resource.Body('OS-EXT-SRV-ATTR:user_data') - #: Enables fine grained control of the block device mapping for an - #: instance. This is typically used for booting servers from volumes. - block_device_mapping = resource.Body('block_device_mapping_v2') - #: The dictionary of data to send to the scheduler. - scheduler_hints = resource.Body('OS-SCH-HNT:scheduler_hints', type=dict) - #: A networks object. Required parameter when there are multiple - #: networks defined for the tenant. When you do not specify the - #: networks parameter, the server attaches to the only network - #: created for the current tenant. - networks = resource.Body('networks') - #: The hypervisor host name. Appears in the response for administrative - #: users only. - hypervisor_hostname = resource.Body('OS-EXT-SRV-ATTR:hypervisor_hostname') - #: The instance name. The Compute API generates the instance name from the - #: instance name template. Appears in the response for administrative users - #: only. - instance_name = resource.Body('OS-EXT-SRV-ATTR:instance_name') - # The locked status of the server - is_locked = resource.Body('locked', type=bool) + #: The ID of the owners of this server. + user_id = resource.Body('user_id') + #: The VM state of this server. + vm_state = resource.Body('OS-EXT-STS:vm_state') def _prepare_request(self, requires_id=True, prepend_key=True, base_path=None): diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py index 70172c242..4e769a6cb 100644 --- a/openstack/tests/unit/cloud/test_caching.py +++ b/openstack/tests/unit/cloud/test_caching.py @@ -183,6 +183,7 @@ def test_list_servers_no_herd(self): self.cloud._SERVER_AGE = 2 fake_server = fakes.make_fake_server('1234', 'name') self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), diff --git a/openstack/tests/unit/cloud/test_create_server.py b/openstack/tests/unit/cloud/test_create_server.py index 35d25cd99..5be66d727 100644 --- a/openstack/tests/unit/cloud/test_create_server.py +++ b/openstack/tests/unit/cloud/test_create_server.py @@ -118,6 +118,7 @@ def test_create_server_wait_server_error(self): u'max_count': 1, u'min_count': 1, u'name': u'server-name'}})), + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -157,6 +158,7 @@ def test_create_server_with_timeout(self): u'max_count': 1, u'min_count': 1, u'name': u'server-name'}})), + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -509,6 +511,7 @@ def test_create_server_no_addresses( u'max_count': 1, u'min_count': 1, u'name': u'server-name'}})), + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), diff --git a/openstack/tests/unit/cloud/test_delete_server.py b/openstack/tests/unit/cloud/test_delete_server.py index 5ae11070e..b6424bfb5 100644 --- a/openstack/tests/unit/cloud/test_delete_server.py +++ b/openstack/tests/unit/cloud/test_delete_server.py @@ -31,6 +31,7 @@ def test_delete_server(self): """ server = fakes.make_fake_server('1234', 'daffy', 'ACTIVE') self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -48,6 +49,7 @@ def test_delete_server_already_gone(self): Test that we return immediately when server is already gone """ self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -59,6 +61,7 @@ def test_delete_server_already_gone(self): def test_delete_server_already_gone_wait(self): self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -73,6 +76,7 @@ def test_delete_server_wait_for_deleted(self): """ server = fakes.make_fake_server('9999', 'wily', 'ACTIVE') self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -99,6 +103,7 @@ def test_delete_server_fails(self): """ server = fakes.make_fake_server('1212', 'speedy', 'ACTIVE') self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -130,6 +135,7 @@ def fake_has_service(service_type): server = fakes.make_fake_server('1234', 'porky', 'ACTIVE') self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -150,6 +156,7 @@ def test_delete_server_delete_ips(self): fip_id = uuid.uuid4().hex self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -198,6 +205,7 @@ def test_delete_server_delete_ips_bad_neutron(self): server = fakes.make_fake_server('1234', 'porky', 'ACTIVE') self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -229,6 +237,7 @@ def test_delete_server_delete_fips_nova(self): server = fakes.make_fake_server('1234', 'porky', 'ACTIVE') self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), diff --git a/openstack/tests/unit/cloud/test_floating_ip_neutron.py b/openstack/tests/unit/cloud/test_floating_ip_neutron.py index 94c06bdc0..a7052b820 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_neutron.py +++ b/openstack/tests/unit/cloud/test_floating_ip_neutron.py @@ -482,6 +482,7 @@ def test_auto_ip_pool_no_reuse(self): "fixed_ip_address": "10.4.0.16", "port_id": "a767944e-057a-47d1-a669-824a21b8fb7b", }})), + self.get_nova_discovery_mock_dict(), dict(method='GET', uri='{endpoint}/servers/detail'.format( endpoint=fakes.COMPUTE_ENDPOINT), diff --git a/openstack/tests/unit/cloud/test_normalize.py b/openstack/tests/unit/cloud/test_normalize.py index 478bc3b6e..e6e434f90 100644 --- a/openstack/tests/unit/cloud/test_normalize.py +++ b/openstack/tests/unit/cloud/test_normalize.py @@ -425,8 +425,15 @@ def test_normalize_servers_normal(self): expected = { 'OS-DCF:diskConfig': u'MANUAL', 'OS-EXT-AZ:availability_zone': u'ca-ymq-2', + 'OS-EXT-SRV-ATTR:host': None, + 'OS-EXT-SRV-ATTR:hostname': None, 'OS-EXT-SRV-ATTR:hypervisor_hostname': None, 'OS-EXT-SRV-ATTR:instance_name': None, + 'OS-EXT-SRV-ATTR:kernel_id': None, + 'OS-EXT-SRV-ATTR:launch_index': None, + 'OS-EXT-SRV-ATTR:ramdisk_id': None, + 'OS-EXT-SRV-ATTR:reservation_id': None, + 'OS-EXT-SRV-ATTR:root_device_name': None, 'OS-EXT-SRV-ATTR:user_data': None, 'OS-EXT-STS:power_state': 1, 'OS-EXT-STS:task_state': None, @@ -454,17 +461,23 @@ def test_normalize_servers_normal(self): 'config_drive': u'True', 'created': u'2015-08-01T19:52:16Z', 'created_at': u'2015-08-01T19:52:16Z', + 'description': None, 'disk_config': u'MANUAL', 'flavor': {u'id': u'bbcb7eb5-5c8d-498f-9d7e-307c575d3566'}, 'has_config_drive': True, + 'host': None, 'hostId': u'bd37', 'host_id': u'bd37', + 'host_status': None, + 'hostname': None, + 'hypervisor_hostname': None, 'id': u'811c5197-dba7-4d3a-a3f6-68ca5328b9a7', 'image': {u'id': u'69c99b45-cd53-49de-afdc-f24789eb8f83'}, 'instance_name': None, 'interface_ip': '', - 'hypervisor_hostname': None, + 'kernel_id': None, 'key_name': u'mordred', + 'launch_index': None, 'launched_at': u'2015-08-01T19:52:02.000000', 'location': { 'cloud': '_test_cloud_', @@ -475,6 +488,7 @@ def test_normalize_servers_normal(self): 'name': None}, 'region_name': u'RegionOne', 'zone': u'ca-ymq-2'}, + 'locked': True, 'metadata': {u'group': u'irc', u'groups': u'irc,enabled'}, 'name': u'mordred-irc', 'networks': { @@ -490,8 +504,15 @@ def test_normalize_servers_normal(self): 'properties': { 'OS-DCF:diskConfig': u'MANUAL', 'OS-EXT-AZ:availability_zone': u'ca-ymq-2', + 'OS-EXT-SRV-ATTR:host': None, + 'OS-EXT-SRV-ATTR:hostname': None, 'OS-EXT-SRV-ATTR:hypervisor_hostname': None, 'OS-EXT-SRV-ATTR:instance_name': None, + 'OS-EXT-SRV-ATTR:kernel_id': None, + 'OS-EXT-SRV-ATTR:launch_index': None, + 'OS-EXT-SRV-ATTR:ramdisk_id': None, + 'OS-EXT-SRV-ATTR:reservation_id': None, + 'OS-EXT-SRV-ATTR:root_device_name': None, 'OS-EXT-SRV-ATTR:user_data': None, 'OS-EXT-STS:power_state': 1, 'OS-EXT-STS:task_state': None, @@ -499,19 +520,26 @@ def test_normalize_servers_normal(self): 'OS-SCH-HNT:scheduler_hints': None, 'OS-SRV-USG:launched_at': u'2015-08-01T19:52:02.000000', 'OS-SRV-USG:terminated_at': None, + 'host_status': None, 'locked': True, - 'os-extended-volumes:volumes_attached': []}, + 'os-extended-volumes:volumes_attached': [], + 'trusted_image_certificates': None}, 'public_v4': None, 'public_v6': None, + 'ramdisk_id': None, 'region': u'RegionOne', + 'reservation_id': None, + 'root_device_name': None, 'scheduler_hints': None, 'security_groups': [{u'name': u'default'}], + 'server_groups': None, 'status': u'ACTIVE', 'locked': True, 'tags': [], 'task_state': None, 'tenant_id': u'db92b20496ae4fbda850a689ea9d563f', 'terminated_at': None, + 'trusted_image_certificates': None, 'updated': u'2016-10-15T15:49:29Z', 'user_data': None, 'user_id': u'e9b21dc437d149858faee0898fb08e92', @@ -877,6 +905,7 @@ def test_normalize_glance_images(self): def test_normalize_servers(self): self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -900,16 +929,21 @@ def test_normalize_servers(self): 'block_device_mapping': None, 'created': u'2015-08-01T19:52:16Z', 'created_at': u'2015-08-01T19:52:16Z', + 'description': None, 'disk_config': u'MANUAL', 'flavor': {u'id': u'bbcb7eb5-5c8d-498f-9d7e-307c575d3566'}, 'has_config_drive': True, + 'host': None, 'host_id': u'bd37', + 'hostname': None, 'hypervisor_hostname': None, 'id': u'811c5197-dba7-4d3a-a3f6-68ca5328b9a7', 'image': {u'id': u'69c99b45-cd53-49de-afdc-f24789eb8f83'}, 'interface_ip': u'', 'instance_name': None, + 'kernel_id': None, 'key_name': u'mordred', + 'launch_index': None, 'launched_at': u'2015-08-01T19:52:02.000000', 'location': { 'cloud': '_test_cloud_', @@ -931,12 +965,18 @@ def test_normalize_servers(self): 'private_v4': None, 'progress': 0, 'properties': { - 'locked': True + 'host_status': None, + 'locked': True, + 'trusted_image_certificates': None }, 'public_v4': None, 'public_v6': None, + 'ramdisk_id': None, + 'reservation_id': None, + 'root_device_name': None, 'scheduler_hints': None, 'security_groups': [{u'name': u'default'}], + 'server_groups': None, 'status': u'ACTIVE', 'tags': [], 'task_state': None, diff --git a/openstack/tests/unit/cloud/test_rebuild_server.py b/openstack/tests/unit/cloud/test_rebuild_server.py index a72723be9..401fd43ee 100644 --- a/openstack/tests/unit/cloud/test_rebuild_server.py +++ b/openstack/tests/unit/cloud/test_rebuild_server.py @@ -79,6 +79,7 @@ def test_rebuild_server_server_error(self): json={ 'rebuild': { 'imageRef': 'a'}})), + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -105,6 +106,7 @@ def test_rebuild_server_timeout(self): json={ 'rebuild': { 'imageRef': 'a'}})), + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -194,6 +196,7 @@ def test_rebuild_server_with_admin_pass_wait(self): 'rebuild': { 'imageRef': 'a', 'adminPass': password}})), + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -231,6 +234,7 @@ def test_rebuild_server_wait(self): json={ 'rebuild': { 'imageRef': 'a'}})), + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), diff --git a/openstack/tests/unit/cloud/test_security_groups.py b/openstack/tests/unit/cloud/test_security_groups.py index d65f5ffac..3a27e70e8 100644 --- a/openstack/tests/unit/cloud/test_security_groups.py +++ b/openstack/tests/unit/cloud/test_security_groups.py @@ -609,6 +609,7 @@ def test_add_security_group_to_server_neutron(self): self.cloud.secgroup_source = 'neutron' self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', @@ -664,6 +665,7 @@ def test_remove_security_group_from_server_neutron(self): validate = {'removeSecurityGroup': {'name': 'neutron-sec-group'}} self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', @@ -691,6 +693,7 @@ def test_add_bad_security_group_to_server_nova(self): self.has_neutron = False self.cloud.secgroup_source = 'nova' self.register_uris([ + self.get_nova_discovery_mock_dict(), dict( method='GET', uri='{endpoint}/servers/detail'.format( @@ -717,6 +720,7 @@ def test_add_bad_security_group_to_server_neutron(self): self.cloud.secgroup_source = 'neutron' self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', @@ -737,6 +741,7 @@ def test_add_security_group_to_bad_server(self): fake_server = fakes.make_fake_server('1234', 'server-name', 'ACTIVE') self.register_uris([ + self.get_nova_discovery_mock_dict(), dict( method='GET', uri='{endpoint}/servers/detail'.format( diff --git a/openstack/tests/unit/cloud/test_server_console.py b/openstack/tests/unit/cloud/test_server_console.py index 6ff8476d1..48bc2d051 100644 --- a/openstack/tests/unit/cloud/test_server_console.py +++ b/openstack/tests/unit/cloud/test_server_console.py @@ -46,6 +46,7 @@ def test_get_server_console_dict(self): def test_get_server_console_name_or_id(self): self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri='{endpoint}/servers/detail'.format( endpoint=fakes.COMPUTE_ENDPOINT), diff --git a/openstack/tests/unit/cloud/test_server_delete_metadata.py b/openstack/tests/unit/cloud/test_server_delete_metadata.py index ab1d1ea52..ab0b94997 100644 --- a/openstack/tests/unit/cloud/test_server_delete_metadata.py +++ b/openstack/tests/unit/cloud/test_server_delete_metadata.py @@ -38,6 +38,7 @@ def test_server_delete_metadata_with_exception(self): Test that a missing metadata throws an exception. """ self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -58,6 +59,7 @@ def test_server_delete_metadata_with_exception(self): def test_server_delete_metadata(self): self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), diff --git a/openstack/tests/unit/cloud/test_server_set_metadata.py b/openstack/tests/unit/cloud/test_server_set_metadata.py index b777ea66d..d5f8bf211 100644 --- a/openstack/tests/unit/cloud/test_server_set_metadata.py +++ b/openstack/tests/unit/cloud/test_server_set_metadata.py @@ -35,6 +35,7 @@ def setUp(self): def test_server_set_metadata_with_exception(self): self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -57,6 +58,7 @@ def test_server_set_metadata_with_exception(self): def test_server_set_metadata(self): self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), diff --git a/openstack/tests/unit/cloud/test_shade.py b/openstack/tests/unit/cloud/test_shade.py index c774cfa31..3be490abd 100644 --- a/openstack/tests/unit/cloud/test_shade.py +++ b/openstack/tests/unit/cloud/test_shade.py @@ -81,6 +81,7 @@ def test_get_server(self): server2 = fakes.make_fake_server('345', 'mouse') self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -95,6 +96,7 @@ def test_get_server(self): def test_get_server_not_found(self): self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -108,6 +110,7 @@ def test_get_server_not_found(self): def test_list_servers_exception(self): self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -146,6 +149,7 @@ def test_list_servers(self): server_name = self.getUniqueString('name') fake_server = fakes.make_fake_server(server_id, server_name) self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -329,6 +333,7 @@ def test_list_server_private_ip(self): "name": "private-subnet-ipv4" }]} self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -353,6 +358,7 @@ def test_list_servers_all_projects(self): '''This test verifies that when list_servers is called with `all_projects=True` that it passes `all_tenants=True` to nova.''' self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail'], @@ -369,6 +375,7 @@ def test_list_servers_filters(self): '''This test verifies that when list_servers is called with `filters` dict that it passes it to nova.''' self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail'], diff --git a/openstack/tests/unit/cloud/test_update_server.py b/openstack/tests/unit/cloud/test_update_server.py index 252da3ce7..584c222d2 100644 --- a/openstack/tests/unit/cloud/test_update_server.py +++ b/openstack/tests/unit/cloud/test_update_server.py @@ -40,6 +40,7 @@ def test_update_server_with_update_exception(self): update_server. """ self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -65,6 +66,7 @@ def test_update_server_name(self): self.server_id, self.updated_server_name) self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index a0ddbce51..b00601cd5 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -14,48 +14,106 @@ import six from openstack.tests.unit import base +from openstack.image.v2 import image from openstack.compute.v2 import server IDENTIFIER = 'IDENTIFIER' EXAMPLE = { - 'accessIPv4': '1', - 'accessIPv6': '2', - 'addresses': {'region': '3'}, - 'config_drive': True, - 'created': '2015-03-09T12:14:57.233772', + 'OS-DCF:diskConfig': 'AUTO', + 'OS-EXT-AZ:availability_zone': 'us-west', + 'OS-EXT-SRV-ATTR:host': 'compute', + 'OS-EXT-SRV-ATTR:hostname': 'new-server-test', + 'OS-EXT-SRV-ATTR:hypervisor_hostname': 'fake-mini', + 'OS-EXT-SRV-ATTR:instance_name': 'instance-00000001', + 'OS-EXT-SRV-ATTR:kernel_id': '', + 'OS-EXT-SRV-ATTR:launch_index': 0, + 'OS-EXT-SRV-ATTR:ramdisk_id': '', + 'OS-EXT-SRV-ATTR:reservation_id': 'r-ov3q80zj', + 'OS-EXT-SRV-ATTR:root_device_name': '/dev/sda', + 'OS-EXT-SRV-ATTR:user_data': 'IyEvYmluL2Jhc2gKL2Jpbi9IHlvdSEiCg==', + 'OS-EXT-STS:power_state': 1, + 'OS-EXT-STS:task_state': None, + 'OS-EXT-STS:vm_state': 'active', + 'OS-SRV-USG:launched_at': '2017-02-14T19:23:59.895661', + 'OS-SRV-USG:terminated_at': '2015-03-09T12:15:57.233772', + 'OS-SCH-HNT:scheduler_hints': {'key': '30'}, + 'accessIPv4': '1.2.3.4', + 'accessIPv6': '80fe::', + 'adminPass': '27', + 'addresses': { + 'private': [ + { + 'OS-EXT-IPS-MAC:mac_addr': 'aa:bb:cc:dd:ee:ff', + 'OS-EXT-IPS:type': 'fixed', + 'addr': '192.168.0.3', + 'version': 4 + } + ] + }, + 'block_device_mapping_v2': {'key': '29'}, + 'config_drive': '', + 'created': '2017-02-14T19:23:58Z', + 'description': 'dummy', 'flavorRef': '5', - 'flavor': {'id': 'FLAVOR_ID', 'links': {}}, - 'hostId': '6', + 'flavor': { + 'disk': 1, + 'ephemeral': 0, + 'extra_specs': { + 'hw:cpu_policy': 'dedicated', + 'hw:mem_page_size': '2048' + }, + 'original_name': 'm1.tiny.specs', + 'ram': 512, + 'swap': 0, + 'vcpus': 1 + }, + 'hostId': '2091634baaccdc4c5a1d57069c833e402921df696b7f970791b12ec6', + 'host_status': 'UP', 'id': IDENTIFIER, 'imageRef': '8', - 'image': {'id': 'IMAGE_ID', 'links': {}}, - 'links': '9', - 'metadata': {'key': '10'}, + 'image': { + 'id': '70a599e0-31e7-49b7-b260-868f441e862b', + 'links': [ + { + 'href': 'http://openstack.example.com/images/70a599e0', + 'rel': 'bookmark' + } + ] + }, + 'key_name': 'dummy', + 'links': [ + { + 'href': 'http://openstack.example.com/v2.1/servers/9168b536', + 'rel': 'self' + }, + { + 'href': 'http://openstack.example.com/servers/9168b536', + 'rel': 'bookmark' + } + ], + 'locked': True, + 'metadata': { + 'My Server Name': 'Apache1' + }, + 'name': 'new-server-test', 'networks': 'auto', - 'name': '11', - 'progress': 12, - 'tenant_id': '13', - 'status': '14', - 'updated': '2015-03-09T12:15:57.233772', - 'user_id': '16', - 'key_name': '17', - 'OS-DCF:diskConfig': '18', - 'OS-EXT-AZ:availability_zone': '19', - 'OS-EXT-STS:power_state': '20', - 'OS-EXT-STS:task_state': '21', - 'OS-EXT-STS:vm_state': '22', - 'os-extended-volumes:volumes_attached': '23', - 'OS-SRV-USG:launched_at': '2015-03-09T12:15:57.233772', - 'OS-SRV-USG:terminated_at': '2015-03-09T12:15:57.233772', - 'security_groups': '26', - 'adminPass': '27', + 'os-extended-volumes:volumes_attached': [], 'personality': '28', - 'block_device_mapping_v2': {'key': '29'}, - 'OS-EXT-SRV-ATTR:hypervisor_hostname': 'hypervisor.example.com', - 'OS-EXT-SRV-ATTR:instance_name': 'instance-00000001', - 'OS-SCH-HNT:scheduler_hints': {'key': '30'}, - 'OS-EXT-SRV-ATTR:user_data': '31', - 'locked': True + 'progress': 0, + 'security_groups': [ + { + 'name': 'default' + } + ], + 'status': 'ACTIVE', + 'tags': [], + 'tenant_id': '6f70656e737461636b20342065766572', + 'trusted_image_certificates': [ + '0b5d2c72-12cc-4ba6-a8d7-3ff5cc1d8cb8', + '674736e3-f25c-405c-8362-bbf991e0ce0a' + ], + 'updated': '2017-02-14T19:24:00Z', + 'user_id': 'fake' } @@ -80,26 +138,51 @@ def test_basic(self): self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertDictEqual({"image": "image", - "flavor": "flavor", - "name": "name", - "status": "status", - "host": "host", - "all_projects": "all_tenants", + self.assertDictEqual({"access_ipv4": "access_ip_v4", + "access_ipv6": "access_ip_v6", + "auto_disk_config": "auto_disk_config", + "availability_zone": "availability_zone", + "changes_before": "changes-before", "changes_since": "changes-since", + "compute_host": "host", + "has_config_drive": "config_drive", + "created_at": "created_at", + "description": "description", + "flavor": "flavor", + "hostname": "hostname", + "image": "image", + "ipv4_address": "ip", + "ipv6_address": "ip6", + "id": "uuid", + "deleted_only": "deleted", + "is_soft_deleted": "soft_deleted", + "kernel_id": "kernel_id", + "key_name": "key_name", + "launch_index": "launch_index", + "launched_at": "launched_at", "limit": "limit", + "locked_by": "locked_by", "marker": "marker", - "sort_key": "sort_key", - "sort_dir": "sort_dir", - "reservation_id": "reservation_id", + "name": "name", + "node": "node", + "power_state": "power_state", + "progress": "progress", "project_id": "project_id", + "ramdisk_id": "ramdisk_id", + "reservation_id": "reservation_id", + "root_device_name": "root_device_name", + "sort_dir": "sort_dir", + "sort_key": "sort_key", + "status": "status", + "task_state": "task_state", + "terminated_at": "terminated_at", + "user_id": "user_id", + "vm_state": "vm_state", + "all_projects": "all_tenants", "tags": "tags", "any_tags": "tags-any", "not_tags": "not-tags", "not_any_tags": "not-tags-any", - "is_deleted": "deleted", - "ipv4_address": "ip", - "ipv6_address": "ip6", }, sot._query_mapping._mapping) @@ -113,9 +196,10 @@ def test_make_it(self): self.assertEqual(EXAMPLE['flavorRef'], sot.flavor_id) self.assertEqual(EXAMPLE['flavor'], sot.flavor) self.assertEqual(EXAMPLE['hostId'], sot.host_id) + self.assertEqual(EXAMPLE['host_status'], sot.host_status) self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['imageRef'], sot.image_id) - self.assertEqual(EXAMPLE['image'], sot.image) + self.assertEqual(image.Image(**EXAMPLE['image']), sot.image) self.assertEqual(EXAMPLE['links'], sot.links) self.assertEqual(EXAMPLE['metadata'], sot.metadata) self.assertEqual(EXAMPLE['networks'], sot.networks) @@ -142,14 +226,30 @@ def test_make_it(self): self.assertEqual(EXAMPLE['personality'], sot.personality) self.assertEqual(EXAMPLE['block_device_mapping_v2'], sot.block_device_mapping) + self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:host'], + sot.compute_host) + self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:hostname'], + sot.hostname) self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:hypervisor_hostname'], sot.hypervisor_hostname) self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:instance_name'], sot.instance_name) + self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:kernel_id'], + sot.kernel_id) + self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:launch_index'], + sot.launch_index) + self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:ramdisk_id'], + sot.ramdisk_id) + self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:reservation_id'], + sot.reservation_id) + self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:root_device_name'], + sot.root_device_name) self.assertEqual(EXAMPLE['OS-SCH-HNT:scheduler_hints'], sot.scheduler_hints) self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:user_data'], sot.user_data) self.assertEqual(EXAMPLE['locked'], sot.is_locked) + self.assertEqual(EXAMPLE['trusted_image_certificates'], + sot.trusted_image_certificates) def test__prepare_server(self): zone = 1 diff --git a/openstack/tests/unit/config/test_from_session.py b/openstack/tests/unit/config/test_from_session.py index 69f2d898c..e5599d915 100644 --- a/openstack/tests/unit/config/test_from_session.py +++ b/openstack/tests/unit/config/test_from_session.py @@ -41,6 +41,7 @@ def test_from_session(self): server_name = self.getUniqueString('name') fake_server = fakes.make_fake_server(server_id, server_name) self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), diff --git a/openstack/tests/unit/test_stats.py b/openstack/tests/unit/test_stats.py index d8c88c299..a556c1f14 100644 --- a/openstack/tests/unit/test_stats.py +++ b/openstack/tests/unit/test_stats.py @@ -209,6 +209,7 @@ def test_servers(self): mock_uri = 'https://compute.example.com/v2.1/servers/detail' self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=mock_uri, status_code=200, json={'servers': []})]) From 790fefbccdc6d8bc7d39ab11051776a17fdebaec Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Fri, 12 Apr 2019 15:24:22 +0000 Subject: [PATCH 2445/3836] Add "name" filter in "list" call when retrieving a single register Resource.find() function accepts both the ID or the name of a register. First tries to retrieve the register by calling the server API using the identifier given. By default, all OpenStack API will accept the ID of the register (which is unique). If this call fails, the function retrieves the list of all registers in the table. This last call is very inefficient if the number of elements is high. The bug description documents the time needed to retrieve a list of 1000 ports. Instead of this, this patch uses the filtering options available in the servers APIs. This filter will be applied in the database call and the number of registers returned will be limited to only those ones matching the "name" field. In case the API doesn't support this filtering parameter, a "InvalidResourceQuery" will be risen and the function will retrieve again the list of registers in the database without filtering by "name". Change-Id: I8bb6f46f66249aeae0649c8f6596f9269e3518a5 Related-Bug: #1779882 --- openstack/resource.py | 4 + openstack/tests/unit/cloud/test_fwaas.py | 100 ++++++++++++------ openstack/tests/unit/image/v2/test_image.py | 2 +- openstack/tests/unit/test_resource.py | 68 +++++++----- ...-find-filter-by-name-e647e5c507ff4b6c.yaml | 7 ++ 5 files changed, 120 insertions(+), 61 deletions(-) create mode 100644 releasenotes/notes/resource-find-filter-by-name-e647e5c507ff4b6c.yaml diff --git a/openstack/resource.py b/openstack/resource.py index bf58cfc65..7115a0518 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1587,6 +1587,10 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): except exceptions.NotFoundException: pass + if ('name' in cls._query_mapping._mapping.keys() + and 'name' not in params): + params['name'] = name_or_id + data = cls.list(session, **params) result = cls._get_one_match(name_or_id, data) diff --git a/openstack/tests/unit/cloud/test_fwaas.py b/openstack/tests/unit/cloud/test_fwaas.py index c383c4b6e..4abccb054 100644 --- a/openstack/tests/unit/cloud/test_fwaas.py +++ b/openstack/tests/unit/cloud/test_fwaas.py @@ -90,7 +90,8 @@ def test_delete_firewall_rule(self): self.firewall_rule_name), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_rules'), + uri=self._make_mock_url('firewall_rules', + name=self.firewall_rule_name), json={'firewall_rules': [self.mock_firewall_rule]}), dict(method='DELETE', uri=self._make_mock_url('firewall_rules', @@ -109,7 +110,9 @@ def test_delete_firewall_rule_filters(self): self.firewall_rule_name), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_rules', **filters), + uri=self._make_mock_url( + 'firewall_rules', + name=self.firewall_rule_name, **filters), json={'firewall_rules': [self.mock_firewall_rule]}, ), dict(method='DELETE', uri=self._make_mock_url('firewall_rules', @@ -146,7 +149,8 @@ def test_delete_firewall_multiple_matches(self): self.firewall_rule_name), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_rules'), + uri=self._make_mock_url('firewall_rules', + name=self.firewall_rule_name), json={'firewall_rules': [self.mock_firewall_rule, self.mock_firewall_rule]}) ]) @@ -162,7 +166,8 @@ def test_get_firewall_rule(self): self.firewall_rule_name), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_rules'), + uri=self._make_mock_url('firewall_rules', + name=self.firewall_rule_name), json={'firewall_rules': [self.mock_firewall_rule]}) ]) r = self.cloud.get_firewall_rule(self.firewall_rule_name) @@ -176,7 +181,7 @@ def test_get_firewall_rule_not_found(self): uri=self._make_mock_url('firewall_rules', name), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_rules'), + uri=self._make_mock_url('firewall_rules', name=name), json={'firewall_rules': []}) ]) self.assertIsNone(self.cloud.get_firewall_rule(name)) @@ -202,7 +207,8 @@ def test_update_firewall_rule(self): self.firewall_rule_name), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_rules'), + uri=self._make_mock_url('firewall_rules', + name=self.firewall_rule_name), json={'firewall_rules': [self.mock_firewall_rule]}), dict(method='PUT', uri=self._make_mock_url('firewall_rules', @@ -283,7 +289,9 @@ def test_create_firewall_policy(self): TestFirewallRule.firewall_rule_name), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_rules'), + uri=self._make_mock_url( + 'firewall_rules', + name=TestFirewallRule.firewall_rule_name), json={'firewall_rules': [ TestFirewallRule._mock_firewall_rule_attrs]}), dict(method='POST', @@ -305,7 +313,9 @@ def test_create_firewall_policy_rule_not_found(self): posted_policy['firewall_rules'][0]), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_rules'), + uri=self._make_mock_url( + 'firewall_rules', + name=posted_policy['firewall_rules'][0]), json={'firewall_rules': []}) ]) @@ -323,7 +333,8 @@ def test_delete_firewall_policy(self): self.firewall_policy_name), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_policies'), + uri=self._make_mock_url('firewall_policies', + name=self.firewall_policy_name), json={'firewall_policies': [self.mock_firewall_policy]}), dict(method='DELETE', uri=self._make_mock_url('firewall_policies', @@ -364,7 +375,8 @@ def test_delete_firewall_policy_not_found(self): self.firewall_policy_name), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_policies'), + uri=self._make_mock_url('firewall_policies', + name=self.firewall_policy_name), json={'firewall_policies': []}) ]) @@ -381,7 +393,8 @@ def test_get_firewall_policy(self): self.firewall_policy_name), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_policies'), + uri=self._make_mock_url('firewall_policies', + name=self.firewall_policy_name), json={'firewall_policies': [self.mock_firewall_policy]}) ]) self.assertDictEqual(self.mock_firewall_policy, @@ -396,7 +409,7 @@ def test_get_firewall_policy_not_found(self): uri=self._make_mock_url('firewall_policies', name), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_policies'), + uri=self._make_mock_url('firewall_policies', name=name), json={'firewall_policies': []}) ]) self.assertIsNone(self.cloud.get_firewall_policy(name)) @@ -448,7 +461,8 @@ def test_update_firewall_policy(self): self.firewall_policy_name), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_policies'), + uri=self._make_mock_url('firewall_policies', + name=self.firewall_policy_name), json={'firewall_policies': [retrieved_policy]}), dict(method='GET', uri=self._make_mock_url('firewall_rules', lookup_rule['id']), @@ -474,7 +488,8 @@ def test_update_firewall_policy_no_rules(self): self.firewall_policy_name), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_policies'), + uri=self._make_mock_url('firewall_policies', + name=self.firewall_policy_name), json={'firewall_policies': [ deepcopy(self.mock_firewall_policy)]}), dict(method='PUT', @@ -539,28 +554,29 @@ def test_insert_rule_into_policy(self): self.firewall_policy_name), status_code=404), dict(method='GET', # get policy - uri=self._make_mock_url('firewall_policies'), + uri=self._make_mock_url('firewall_policies', + name=self.firewall_policy_name), json={'firewall_policies': [retrieved_policy]}), dict(method='GET', # short-circuit uri=self._make_mock_url('firewall_rules', rule0['name']), status_code=404), dict(method='GET', # get rule to add - uri=self._make_mock_url('firewall_rules'), + uri=self._make_mock_url('firewall_rules', name=rule0['name']), json={'firewall_rules': [rule0]}), dict(method='GET', # short-circuit uri=self._make_mock_url('firewall_rules', rule1['name']), status_code=404), dict(method='GET', # get after rule - uri=self._make_mock_url('firewall_rules'), + uri=self._make_mock_url('firewall_rules', name=rule1['name']), json={'firewall_rules': [rule1]}), dict(method='GET', # short-circuit uri=self._make_mock_url('firewall_rules', rule2['name']), status_code=404), dict(method='GET', # get before rule - uri=self._make_mock_url('firewall_rules'), + uri=self._make_mock_url('firewall_rules', name=rule2['name']), json={'firewall_rules': [rule2]}), dict(method='PUT', # add rule @@ -595,14 +611,15 @@ def test_insert_rule_into_policy_compact(self): self.firewall_policy_name), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_policies'), + uri=self._make_mock_url('firewall_policies', + name=self.firewall_policy_name), json={'firewall_policies': [retrieved_policy]}), dict(method='GET', # short-circuit uri=self._make_mock_url('firewall_rules', rule['name']), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_rules'), + uri=self._make_mock_url('firewall_rules', name=rule['name']), json={'firewall_rules': [rule]}), dict(method='PUT', @@ -626,7 +643,8 @@ def test_insert_rule_into_policy_not_found(self): uri=self._make_mock_url('firewall_policies', policy_name), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_policies'), + uri=self._make_mock_url('firewall_policies', + name=policy_name), json={'firewall_policies': []}) ]) @@ -648,7 +666,7 @@ def test_insert_rule_into_policy_rule_not_found(self): uri=self._make_mock_url('firewall_rules', rule_name), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_rules'), + uri=self._make_mock_url('firewall_rules', name=rule_name), json={'firewall_rules': []}) ]) self.assertRaises(exceptions.ResourceNotFound, @@ -691,14 +709,15 @@ def test_remove_rule_from_policy(self): uri=self._make_mock_url('firewall_policies', policy_name), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_policies'), + uri=self._make_mock_url('firewall_policies', + name=policy_name), json={'firewall_policies': [retrieved_policy]}), dict(method='GET', # short-circuit uri=self._make_mock_url('firewall_rules', rule['name']), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_rules'), + uri=self._make_mock_url('firewall_rules', name=rule['name']), json={'firewall_rules': [rule]}), dict(method='PUT', @@ -719,7 +738,8 @@ def test_remove_rule_from_policy_not_found(self): self.firewall_policy_name), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_policies'), + uri=self._make_mock_url('firewall_policies', + name=self.firewall_policy_name), json={'firewall_policies': []}) ]) @@ -745,7 +765,7 @@ def test_remove_rule_from_policy_rule_not_found(self): rule['name']), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_rules'), + uri=self._make_mock_url('firewall_rules', name=rule['name']), json={'firewall_rules': []}) ]) r = self.cloud.remove_rule_from_policy(self.firewall_policy_id, @@ -845,7 +865,8 @@ def test_create_firewall_group(self): self.mock_egress_policy['name']), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_policies'), + uri=self._make_mock_url('firewall_policies', + name=self.mock_egress_policy['name']), json={'firewall_policies': [self.mock_egress_policy]}), dict(method='GET', # short-circuit @@ -853,7 +874,9 @@ def test_create_firewall_group(self): self.mock_ingress_policy['name']), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_policies'), + uri=self._make_mock_url( + 'firewall_policies', + name=self.mock_ingress_policy['name']), json={'firewall_policies': [self.mock_ingress_policy]}), dict(method='GET', @@ -904,7 +927,8 @@ def test_delete_firewall_group(self): self.firewall_group_name), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_groups'), + uri=self._make_mock_url('firewall_groups', + name=self.firewall_group_name), json={'firewall_groups': [ deepcopy(self.mock_returned_firewall_group)]}), dict(method='DELETE', @@ -942,7 +966,8 @@ def test_delete_firewall_group_not_found(self): self.firewall_group_name), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_groups'), + uri=self._make_mock_url('firewall_groups', + name=self.firewall_group_name), json={'firewall_groups': []}) ]) @@ -960,7 +985,8 @@ def test_get_firewall_group(self): self.firewall_group_name), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_groups'), + uri=self._make_mock_url('firewall_groups', + name=self.firewall_group_name), json={'firewall_groups': [returned_group]}) ]) self.assertDictEqual( @@ -975,7 +1001,7 @@ def test_get_firewall_group_not_found(self): uri=self._make_mock_url('firewall_groups', name), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_groups'), + uri=self._make_mock_url('firewall_groups', name=name), json={'firewall_groups': []}) ]) self.assertIsNone(self.cloud.get_firewall_group(name)) @@ -1025,7 +1051,8 @@ def test_update_firewall_group(self): self.firewall_group_name), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_groups'), + uri=self._make_mock_url('firewall_groups', + name=self.firewall_group_name), json={'firewall_groups': [returned_group]}), dict(method='GET', # short-circuit @@ -1033,7 +1060,8 @@ def test_update_firewall_group(self): self.mock_egress_policy['name']), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_policies'), + uri=self._make_mock_url('firewall_policies', + name=self.mock_egress_policy['name']), json={'firewall_policies': [ deepcopy(self.mock_egress_policy)]}), @@ -1042,7 +1070,9 @@ def test_update_firewall_group(self): self.mock_ingress_policy['name']), status_code=404), dict(method='GET', - uri=self._make_mock_url('firewall_policies'), + uri=self._make_mock_url( + 'firewall_policies', + name=self.mock_ingress_policy['name']), json={'firewall_policies': [ deepcopy(self.mock_ingress_policy)]}), diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index c5a327a46..7ea356a91 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -408,7 +408,7 @@ def test_image_find(self): self.sess.get.assert_has_calls([ mock.call('images/' + EXAMPLE['name'], microversion=None), mock.call('/images', headers={'Accept': 'application/json'}, - microversion=None, params={}), + microversion=None, params={'name': EXAMPLE['name']}), mock.call('/images', headers={'Accept': 'application/json'}, microversion=None, params={'os_hidden': True}) ]) diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index cf14a1bda..5c87c1e81 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -2082,38 +2082,42 @@ def test_list_multi_page_link_header(self): class TestResourceFind(base.TestCase): - def setUp(self): - super(TestResourceFind, self).setUp() + result = 1 - self.result = 1 + class Base(resource.Resource): - class Base(resource.Resource): + @classmethod + def existing(cls, **kwargs): + response = mock.Mock() + response.status_code = 404 + raise exceptions.ResourceNotFound( + 'Not Found', response=response) - @classmethod - def existing(cls, **kwargs): - response = mock.Mock() - response.status_code = 404 - raise exceptions.ResourceNotFound( - 'Not Found', response=response) + @classmethod + def list(cls, session, **params): + return None - @classmethod - def list(cls, session): - return None + class OneResult(Base): - class OneResult(Base): + @classmethod + def _get_one_match(cls, *args): + return TestResourceFind.result - @classmethod - def _get_one_match(cls, *args): - return self.result + class NoResults(Base): - class NoResults(Base): + @classmethod + def _get_one_match(cls, *args): + return None - @classmethod - def _get_one_match(cls, *args): - return None + class OneResultWithQueryParams(OneResult): + + _query_mapping = resource.QueryParameters('name') - self.no_results = NoResults - self.one_result = OneResult + def setUp(self): + super(TestResourceFind, self).setUp() + self.no_results = self.NoResults + self.one_result = self.OneResult + self.one_result_with_qparams = self.OneResultWithQueryParams def test_find_short_circuit(self): value = 1 @@ -2139,10 +2143,24 @@ def test_no_match_return(self): self.no_results.find( self.cloud.compute, "name", ignore_missing=True)) - def test_find_result(self): + def test_find_result_name_not_in_query_parameters(self): + with mock.patch.object(self.one_result, 'existing', + side_effect=self.OneResult.existing) \ + as mock_existing, \ + mock.patch.object(self.one_result, 'list', + side_effect=self.OneResult.list) \ + as mock_list: + self.assertEqual( + self.result, + self.one_result.find(self.cloud.compute, "name")) + mock_existing.assert_called_once_with(id='name', + connection=mock.ANY) + mock_list.assert_called_once_with(mock.ANY) + + def test_find_result_name_in_query_parameters(self): self.assertEqual( self.result, - self.one_result.find(self.cloud.compute, "name")) + self.one_result_with_qparams.find(self.cloud.compute, "name")) def test_match_empty_results(self): self.assertIsNone(resource.Resource._get_one_match("name", [])) diff --git a/releasenotes/notes/resource-find-filter-by-name-e647e5c507ff4b6c.yaml b/releasenotes/notes/resource-find-filter-by-name-e647e5c507ff4b6c.yaml new file mode 100644 index 000000000..f736cf93c --- /dev/null +++ b/releasenotes/notes/resource-find-filter-by-name-e647e5c507ff4b6c.yaml @@ -0,0 +1,7 @@ +--- +other: + - | + ``openstack.resource.Resource.find`` now can use the database back-end to + filter by name. If the resource class has "name" in the query parameters, + this function will add this filter parameter in the "list" command, instead + of retrieving the whole list and then manually filtering. From 83cd4f9d8c0273398e2e90b66ef82f8ce45d6192 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Thu, 25 Apr 2019 09:50:21 -0500 Subject: [PATCH 2446/3836] Uncap jsonschema We have jsonschema capped at a fairly old version. Other than some specific releases, it looks like keeping it below 3.0 was added in I943fd68b9fab3bce1764305a5058df5339470757 without really any explanation why. In order to update to a 3.x release we need to: 1. Remove the cap from global-requirements.txt (see Depends-On), leaving upper-constraints.txt at a 2.x release 2. Remove the cap from all consumers (this change) 3. Release a new version of consumers that are published to pypi 4. Update upper-constraints.txt with those new releases 5. Update jsonschema in upper-constraints.txt to a 3.X release (See: https://review.openstack.org/649789) 6. Test consumers with the change from 5. 7. [Optional] fix issues in consumers that arise from 6. 8. Merge the change from 5. Change-Id: I66ad793a52c657564ece35019430557a45edd3bf Co-Authored-by: Sean McGinnis Co-Authored-by: Tony Breeds --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 82ccae1a3..c32f63564 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -6,7 +6,7 @@ hacking>=1.0,<1.2 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 extras>=1.0.0 # MIT fixtures>=3.0.0 # Apache-2.0/BSD -jsonschema<3.0.0,>=2.6.0 # MIT +jsonschema>=2.6.0 # MIT mock>=2.0.0 # BSD prometheus-client>=0.4.2 # Apache-2.0 python-subunit>=1.0.0 # Apache-2.0/BSD From 21bb881d03d431995c654313db7ba29035b3bcb1 Mon Sep 17 00:00:00 2001 From: Guang Yee Date: Mon, 29 Apr 2019 15:29:42 -0700 Subject: [PATCH 2447/3836] fixing timing The parameter 'timing' got lost in translation in this stack. python-openstackclient -> osc-lib -> openstacksdk -> keystoneauth keystoneauth only understand 'collect_timing' while the others only conveys 'timing'. Therefore, we have to make the proper translation. Change-Id: I6c1182bb1d8c04791a573fb556197440360a2bb9 Story: #2005315 Task: #30222 --- openstack/config/cloud_region.py | 1 + .../tests/unit/config/test_cloud_config.py | 28 +++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 7c5a42f4f..877867dd5 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -395,6 +395,7 @@ def get_session(self): verify=verify, cert=cert, timeout=self.config.get('api_timeout'), + collect_timing=self.config.get('timing'), discovery_cache=self._discovery_cache) self.insert_user_agent() # Using old keystoneauth with new os-client-config fails if diff --git a/openstack/tests/unit/config/test_cloud_config.py b/openstack/tests/unit/config/test_cloud_config.py index df21ef2e5..5a8b9df2e 100644 --- a/openstack/tests/unit/config/test_cloud_config.py +++ b/openstack/tests/unit/config/test_cloud_config.py @@ -218,7 +218,8 @@ def test_get_session(self, mock_session): cc.get_session() mock_session.assert_called_with( auth=mock.ANY, - verify=True, cert=None, timeout=None, discovery_cache=None) + verify=True, cert=None, timeout=None, collect_timing=None, + discovery_cache=None) self.assertEqual( fake_session.additional_user_agent, [('openstacksdk', openstack_version.__version__)]) @@ -238,7 +239,8 @@ def test_get_session_with_app_name(self, mock_session): cc.get_session() mock_session.assert_called_with( auth=mock.ANY, - verify=True, cert=None, timeout=None, discovery_cache=None) + verify=True, cert=None, timeout=None, collect_timing=None, + discovery_cache=None) self.assertEqual(fake_session.app_name, "test_app") self.assertEqual(fake_session.app_version, "test_version") self.assertEqual( @@ -258,7 +260,27 @@ def test_get_session_with_timeout(self, mock_session): cc.get_session() mock_session.assert_called_with( auth=mock.ANY, - verify=True, cert=None, timeout=9, discovery_cache=None) + verify=True, cert=None, timeout=9, + collect_timing=None, discovery_cache=None) + self.assertEqual( + fake_session.additional_user_agent, + [('openstacksdk', openstack_version.__version__)]) + + @mock.patch.object(ksa_session, 'Session') + def test_get_session_with_timing(self, mock_session): + fake_session = mock.Mock() + fake_session.additional_user_agent = [] + mock_session.return_value = fake_session + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + config_dict['timing'] = True + cc = cloud_region.CloudRegion( + "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + cc.get_session() + mock_session.assert_called_with( + auth=mock.ANY, + verify=True, cert=None, timeout=None, + collect_timing=True, discovery_cache=None) self.assertEqual( fake_session.additional_user_agent, [('openstacksdk', openstack_version.__version__)]) From bc0cff52c002609f86b9046b01948d8a1be1fdf8 Mon Sep 17 00:00:00 2001 From: Maxime Guyot Date: Tue, 30 Apr 2019 21:47:15 -0600 Subject: [PATCH 2448/3836] Add support for all_tenants in OpenStackInventory This would allow for the Ansible inventory plugin to list servers from all tenants. Change-Id: I77c4d6e83cd0761c91609e3ae01a0db08c493b3b --- openstack/cloud/inventory.py | 6 +++-- openstack/tests/unit/cloud/test_inventory.py | 26 ++++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/openstack/cloud/inventory.py b/openstack/cloud/inventory.py index d62bef433..8905ce0a7 100644 --- a/openstack/cloud/inventory.py +++ b/openstack/cloud/inventory.py @@ -57,13 +57,15 @@ def __init__( for cloud in self.clouds: cloud._cache.invalidate() - def list_hosts(self, expand=True, fail_on_cloud_config=True): + def list_hosts(self, expand=True, fail_on_cloud_config=True, + all_projects=False): hostvars = [] for cloud in self.clouds: try: # Cycle on servers - for server in cloud.list_servers(detailed=expand): + for server in cloud.list_servers(detailed=expand, + all_projects=all_projects): hostvars.append(server) except exceptions.OpenStackCloudException: # Don't fail on one particular cloud as others may work diff --git a/openstack/tests/unit/cloud/test_inventory.py b/openstack/tests/unit/cloud/test_inventory.py index 339fa6695..d44864231 100644 --- a/openstack/tests/unit/cloud/test_inventory.py +++ b/openstack/tests/unit/cloud/test_inventory.py @@ -68,7 +68,8 @@ def test_list_hosts(self, mock_cloud, mock_config): ret = inv.list_hosts() - inv.clouds[0].list_servers.assert_called_once_with(detailed=True) + inv.clouds[0].list_servers.assert_called_once_with(detailed=True, + all_projects=False) self.assertFalse(inv.clouds[0].get_openstack_vars.called) self.assertEqual([server], ret) @@ -88,9 +89,30 @@ def test_list_hosts_no_detail(self, mock_cloud, mock_config): inv.list_hosts(expand=False) - inv.clouds[0].list_servers.assert_called_once_with(detailed=False) + inv.clouds[0].list_servers.assert_called_once_with(detailed=False, + all_projects=False) self.assertFalse(inv.clouds[0].get_openstack_vars.called) + @mock.patch("openstack.config.loader.OpenStackConfig") + @mock.patch("openstack.connection.Connection") + def test_list_hosts_all_projects(self, mock_cloud, mock_config): + mock_config.return_value.get_all.return_value = [{}] + + inv = inventory.OpenStackInventory() + + server = dict(id='server_id', name='server_name') + self.assertIsInstance(inv.clouds, list) + self.assertEqual(1, len(inv.clouds)) + inv.clouds[0].list_servers.return_value = [server] + inv.clouds[0].get_openstack_vars.return_value = server + + ret = inv.list_hosts(all_projects=True) + + inv.clouds[0].list_servers.assert_called_once_with(detailed=True, + all_projects=True) + self.assertFalse(inv.clouds[0].get_openstack_vars.called) + self.assertEqual([server], ret) + @mock.patch("openstack.config.loader.OpenStackConfig") @mock.patch("openstack.connection.Connection") def test_search_hosts(self, mock_cloud, mock_config): From f9483e5d95653906d6740a9a8ec0b715237f586e Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 15 Apr 2019 14:34:00 +0200 Subject: [PATCH 2449/3836] Skip attaching FloatingIP if it is already attached There are some flows in the cloud layer (reuse, but not found), which try to attach a freshly allocated (and attached) FIP. There is at least one cloud in the crowd, which explicitely forbids FIP attachment when it is already attached, what results in SDK and Ansible failure with auto_ip=True The test test_add_ip_refresh_timeout is being removed, since normally we should never end up this way (with reuse=False we wait for the FIP to be assigned during creation and we leave _add_auto_ip on a short-cut) Change-Id: If9a217981237d6f851ee470717da01fddd1b5ff1 --- openstack/cloud/_floating_ip.py | 11 +++- .../unit/cloud/test_floating_ip_neutron.py | 58 ++----------------- 2 files changed, 15 insertions(+), 54 deletions(-) diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index 5235fdffe..98068ad9b 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -691,6 +691,15 @@ def _attach_ip_to_server( # Short circuit if we're asking to attach an IP that's already # attached ext_ip = meta.get_server_ip(server, ext_tag='floating', public=True) + if not ext_ip and floating_ip['port_id']: + # When we came here from reuse_fip and created FIP it might be + # already attached, but the server info might be also + # old to check whether it belongs to us now, thus refresh + # the server data and try again. There are some clouds, which + # explicitely forbids FIP assign call if it is already assigned. + server = self.get_server_by_id(server['id']) + ext_ip = meta.get_server_ip(server, ext_tag='floating', + public=True) if ext_ip == floating_ip['floating_ip_address']: return server @@ -719,7 +728,7 @@ def _attach_ip_to_server( timeout, "Timeout waiting for the floating IP to be attached.", wait=self._SERVER_AGE): - server = self.get_server(server_id) + server = self.get_server_by_id(server_id) ext_ip = meta.get_server_ip( server, ext_tag='floating', public=True) if ext_ip == floating_ip['floating_ip_address']: diff --git a/openstack/tests/unit/cloud/test_floating_ip_neutron.py b/openstack/tests/unit/cloud/test_floating_ip_neutron.py index 94c06bdc0..d2f72f097 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_neutron.py +++ b/openstack/tests/unit/cloud/test_floating_ip_neutron.py @@ -768,7 +768,8 @@ def test_delete_floating_ip_not_found(self): self.assert_calls() def test_attach_ip_to_server(self): - fip = self.mock_floating_ip_list_rep['floatingips'][0] + fip = self.mock_floating_ip_list_rep['floatingips'][0].copy() + fip.update({'status': 'DOWN', 'port_id': None, 'router_id': None}) device_id = self.fake_server['id'] self.register_uris([ @@ -782,7 +783,8 @@ def test_attach_ip_to_server(self): 'network', 'public', append=['v2.0', 'floatingips/{0}.json'.format( fip['id'])]), - json={'floatingip': fip}, + json={'floatingip': + self.mock_floating_ip_list_rep['floatingips'][0]}, validate=dict( json={'floatingip': { 'port_id': self.mock_search_ports_rep[0]['id'], @@ -792,57 +794,7 @@ def test_attach_ip_to_server(self): self.cloud._attach_ip_to_server( server=self.fake_server, - floating_ip=self.floating_ip) - self.assert_calls() - - def test_add_ip_refresh_timeout(self): - device_id = self.fake_server['id'] - - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks.json']), - json={'networks': [self.mock_get_network_rep]}), - dict(method='GET', - uri='https://network.example.com/v2.0/subnets.json', - json={'subnets': []}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json'], - qs_elements=["device_id={0}".format(device_id)]), - json={'ports': self.mock_search_ports_rep}), - dict(method='POST', - uri='https://network.example.com/v2.0/floatingips.json', - json={'floatingip': self.floating_ip}, - validate=dict( - json={'floatingip': { - 'floating_network_id': 'my-network-id', - 'fixed_ip_address': self.mock_search_ports_rep[0][ - 'fixed_ips'][0]['ip_address'], - 'port_id': self.mock_search_ports_rep[0]['id']}})), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips.json']), - json={'floatingips': [self.floating_ip]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'floatingips/{0}.json'.format( - self.floating_ip['id'])]), - json={}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips.json']), - json={'floatingips': []}), - ]) - - self.assertRaises( - exc.OpenStackCloudTimeout, - self.cloud._add_auto_ip, - server=self.fake_server, - wait=True, timeout=0.01, - reuse=False) + floating_ip=self.cloud._normalize_floating_ip(fip)) self.assert_calls() def test_detach_ip_from_server(self): From 364b17e2f040da808f78497bc6c40156c999fd04 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 8 May 2019 13:16:42 +0000 Subject: [PATCH 2450/3836] Workaround older octavia version discovery Older octavia didn't have a working version discovery document, and python-octaviaclient just appends v2.0 to the endpoint like neutronclient did. Rather than getting ourselves worked up about it, just apply the same hack that we apply for neutronclient. Change-Id: Ib433eba2e4946d3db7fd9352f4c8f170193da72d --- openstack/config/cloud_region.py | 35 ++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 7c5a42f4f..7d2a829ee 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -477,6 +477,21 @@ def get_all_version_data(self, service_type): self.get_interface(service_type), {}) return interface_versions.get(service_type, []) + def _get_hardcoded_endpoint(self, service_type, constructor): + adapter = constructor( + session=self.get_session(), + service_type=self.get_service_type(service_type), + service_name=self.get_service_name(service_type), + interface=self.get_interface(service_type), + region_name=self.region_name, + ) + endpoint = adapter.get_endpoint() + if not endpoint.rstrip().rsplit('/')[-1] == 'v2.0': + if not endpoint.endswith('/'): + endpoint += '/' + endpoint = urllib.parse.urljoin(endpoint, 'v2.0') + return endpoint + def get_session_client( self, service_type, version=None, constructor=proxy.Proxy, @@ -511,27 +526,17 @@ def get_session_client( kwargs.pop('min_version', None) or version_request.min_api_version) max_api_version = ( kwargs.pop('max_version', None) or version_request.max_api_version) + # Older neutron has inaccessible discovery document. Nobody noticed # because neutronclient hard-codes an append of v2.0. YAY! - if service_type == 'network': + # Also, older octavia has a similar issue. + if service_type in ('network', 'load-balancer'): version = None min_api_version = None max_api_version = None if endpoint_override is None: - network_adapter = constructor( - session=self.get_session(), - service_type=self.get_service_type(service_type), - service_name=self.get_service_name(service_type), - interface=self.get_interface(service_type), - region_name=self.region_name, - ) - network_endpoint = network_adapter.get_endpoint() - if not network_endpoint.rstrip().rsplit('/')[-1] == 'v2.0': - if not network_endpoint.endswith('/'): - network_endpoint += '/' - network_endpoint = urllib.parse.urljoin( - network_endpoint, 'v2.0') - endpoint_override = network_endpoint + endpoint_override = self._get_hardcoded_endpoint( + service_type, constructor) client = constructor( session=self.get_session(), From 1e810595c618f5eac5241c479ec836e08e91770f Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 10 May 2019 18:34:34 +0200 Subject: [PATCH 2451/3836] Continue refactoring of the image Next refactoring monster with: - switch bunch of cloud._image methods to using image.proxy methods - Add new image.Image attribute - Fix image properties by adding global support for keeping "unknown" attributes under properties attr - turn back function lost in openstackcloud split (sadly not found by unittests) - add ability to create image without data required by OSC to switch to SDK - use the proxy logger Change-Id: I9d36d3a52370d6a1040362e1d6e762146df86258 --- openstack/cloud/_image.py | 72 +----- openstack/cloud/_normalize.py | 17 +- openstack/cloud/openstackcloud.py | 9 - openstack/image/_base_proxy.py | 112 ++++++--- openstack/image/v1/_proxy.py | 55 ++++- openstack/image/v1/image.py | 64 +++++ openstack/image/v2/_proxy.py | 239 +++++++++++-------- openstack/image/v2/image.py | 54 ++++- openstack/resource.py | 78 +++++- openstack/tests/fakes.py | 2 +- openstack/tests/unit/cloud/test_image.py | 116 +++++++-- openstack/tests/unit/cloud/test_normalize.py | 22 ++ openstack/tests/unit/image/v2/test_image.py | 39 ++- openstack/tests/unit/image/v2/test_proxy.py | 103 +++++++- openstack/tests/unit/test_resource.py | 148 ++++++++++++ 15 files changed, 857 insertions(+), 273 deletions(-) diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py index 7222a2e82..b3812d374 100644 --- a/openstack/cloud/_image.py +++ b/openstack/cloud/_image.py @@ -15,13 +15,9 @@ # openstack.resource.Resource.list and openstack.resource2.Resource.list import types # noqa -import keystoneauth1.exceptions - from openstack.cloud import exc -from openstack.cloud import meta from openstack.cloud import _normalize from openstack.cloud import _utils -from openstack import proxy from openstack import utils @@ -73,35 +69,10 @@ def list_images(self, filter_deleted=True, show_all=False): images = [] params = {} image_list = [] - try: - if self._is_client_version('image', 2): - endpoint = '/images' - if show_all: - params['member_status'] = 'all' - else: - endpoint = '/images/detail' - - response = self._image_client.get(endpoint, params=params) - - except keystoneauth1.exceptions.catalog.EndpointNotFound: - # We didn't have glance, let's try nova - # If this doesn't work - we just let the exception propagate - response = proxy._json_response( - self.compute.get('/images/detail')) - while 'next' in response: - image_list.extend(meta.obj_list_to_munch(response['images'])) - endpoint = response['next'] - # next links from glance have the version prefix. If the catalog - # has a versioned endpoint, then we can't append the next link to - # it. Strip the absolute prefix (/v1/ or /v2/ to turn it into - # a proper relative link. - if endpoint.startswith('/v'): - endpoint = endpoint[4:] - response = self._image_client.get(endpoint) - if 'images' in response: - image_list.extend(meta.obj_list_to_munch(response['images'])) - else: - image_list.extend(response) + if self._is_client_version('image', 2): + if show_all: + params['member_status'] = 'all' + image_list = list(self.image.images(**params)) for image in image_list: # The cloud might return DELETED for invalid images. @@ -143,13 +114,8 @@ def get_image_by_id(self, id): :param id: ID of the image. :returns: An image ``munch.Munch``. """ - data = self._image_client.get( - '/images/{id}'.format(id=id), - error_message="Error getting image with ID {id}".format(id=id) - ) - key = 'image' if 'image' in data else None image = self._normalize_image( - self._get_and_munchify(key, data)) + self.image.get_image(image={'id': id})) return image @@ -181,27 +147,14 @@ def download_image( 'Both an output path and file object were provided,' ' however only one can be used at once') - image = self.search_images(name_or_id) - if len(image) == 0: + image = self.image.find_image(name_or_id) + if not image: raise exc.OpenStackCloudResourceNotFound( "No images with name or ID %s were found" % name_or_id, None) - if self._is_client_version('image', 2): - endpoint = '/images/{id}/file'.format(id=image[0]['id']) - else: - endpoint = '/images/{id}'.format(id=image[0]['id']) - - response = self._image_client.get(endpoint, stream=True) - with _utils.shade_exceptions("Unable to download image"): - if output_path: - with open(output_path, 'wb') as fd: - for chunk in response.iter_content(chunk_size=chunk_size): - fd.write(chunk) - return - elif output_file: - for chunk in response.iter_content(chunk_size=chunk_size): - output_file.write(chunk) - return + return self.image.download_image( + image, output=output_file or output_path, + chunk_size=chunk_size) def get_image_exclude(self, name_or_id, exclude): for image in self.search_images(name_or_id): @@ -254,12 +207,11 @@ def delete_image( image = self.get_image(name_or_id) if not image: return False - self._image_client.delete( - '/images/{id}'.format(id=image.id), - error_message="Error in deleting image") + self.image.delete_image(image) self.list_images.invalidate(self) # Task API means an image was uploaded to swift + # TODO(gtema) does it make sense to move this into proxy? if self.image_api_use_tasks and ( self._IMAGE_OBJECT_KEY in image or self._SHADE_IMAGE_OBJECT_KEY in image): diff --git a/openstack/cloud/_normalize.py b/openstack/cloud/_normalize.py index d2cf29189..c5002247f 100644 --- a/openstack/cloud/_normalize.py +++ b/openstack/cloud/_normalize.py @@ -269,11 +269,18 @@ def _normalize_images(self, images): return ret def _normalize_image(self, image): - new_image = munch.Munch( - location=self._get_current_location(project_id=image.get('owner'))) + if isinstance(image, resource.Resource): + image = image.to_dict(ignore_none=True, original_names=True) + location = image.pop( + 'location', + self._get_current_location(project_id=image.get('owner'))) + else: + location = self._get_current_location( + project_id=image.get('owner')) + # This copy is to keep things from getting epically weird in tests + image = image.copy() - # This copy is to keep things from getting epically weird in tests - image = image.copy() + new_image = munch.Munch(location=location) # Discard noise self._remove_novaclient_artifacts(image) @@ -321,7 +328,7 @@ def _normalize_image(self, image): new_image['is_protected'] = protected new_image['locations'] = image.pop('locations', []) - metadata = image.pop('metadata', {}) + metadata = image.pop('metadata', {}) or {} for key, val in metadata.items(): properties.setdefault(key, val) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 010e51945..1468a14fd 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -64,21 +64,12 @@ class _OpenStackCloudMixin(object): _OBJECT_AUTOCREATE_KEY = 'x-object-meta-x-sdk-autocreated' _OBJECT_AUTOCREATE_CONTAINER = 'images' - _IMAGE_MD5_KEY = 'owner_specified.openstack.md5' - _IMAGE_SHA256_KEY = 'owner_specified.openstack.sha256' - _IMAGE_OBJECT_KEY = 'owner_specified.openstack.object' # NOTE(shade) shade keys were x-object-meta-x-shade-md5 - we need to check # those in freshness checks so that a shade->sdk transition # doesn't result in a re-upload _SHADE_OBJECT_MD5_KEY = 'x-object-meta-x-shade-md5' _SHADE_OBJECT_SHA256_KEY = 'x-object-meta-x-shade-sha256' _SHADE_OBJECT_AUTOCREATE_KEY = 'x-object-meta-x-shade-autocreated' - # NOTE(shade) shade keys were owner_specified.shade.md5 - we need to add - # those to freshness checks so that a shade->sdk transition - # doesn't result in a re-upload - _SHADE_IMAGE_MD5_KEY = 'owner_specified.shade.md5' - _SHADE_IMAGE_SHA256_KEY = 'owner_specified.shade.sha256' - _SHADE_IMAGE_OBJECT_KEY = 'owner_specified.shade.object' def __init__(self): diff --git a/openstack/image/_base_proxy.py b/openstack/image/_base_proxy.py index 3ae1624bd..93ee70446 100644 --- a/openstack/image/_base_proxy.py +++ b/openstack/image/_base_proxy.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. import abc +import os import six @@ -20,6 +21,17 @@ class BaseImageProxy(six.with_metaclass(abc.ABCMeta, proxy.Proxy)): retriable_status_codes = [503] + _IMAGE_MD5_KEY = 'owner_specified.openstack.md5' + _IMAGE_SHA256_KEY = 'owner_specified.openstack.sha256' + _IMAGE_OBJECT_KEY = 'owner_specified.openstack.object' + + # NOTE(shade) shade keys were owner_specified.shade.md5 - we need to add + # those to freshness checks so that a shade->sdk transition + # doesn't result in a re-upload + _SHADE_IMAGE_MD5_KEY = 'owner_specified.shade.md5' + _SHADE_IMAGE_SHA256_KEY = 'owner_specified.shade.sha256' + _SHADE_IMAGE_OBJECT_KEY = 'owner_specified.shade.object' + def create_image( self, name, filename=None, container=None, @@ -28,42 +40,42 @@ def create_image( disable_vendor_agent=True, allow_duplicates=False, meta=None, wait=False, timeout=3600, + validate_checksum=True, **kwargs): """Upload an image. :param str name: Name of the image to create. If it is a pathname - of an image, the name will be constructed from the - extensionless basename of the path. + of an image, the name will be constructed from the extensionless + basename of the path. :param str filename: The path to the file to upload, if needed. - (optional, defaults to None) + (optional, defaults to None) :param str container: Name of the container in swift where images - should be uploaded for import if the cloud - requires such a thing. (optiona, defaults to - 'images') + should be uploaded for import if the cloud requires such a thing. + (optional, defaults to 'images') :param str md5: md5 sum of the image file. If not given, an md5 will - be calculated. + be calculated. :param str sha256: sha256 sum of the image file. If not given, an md5 - will be calculated. + will be calculated. :param str disk_format: The disk format the image is in. (optional, - defaults to the os-client-config config value - for this cloud) + defaults to the os-client-config config value for this cloud) :param str container_format: The container format the image is in. - (optional, defaults to the - os-client-config config value for this - cloud) + (optional, defaults to the os-client-config config value for this + cloud) :param bool disable_vendor_agent: Whether or not to append metadata - flags to the image to inform the - cloud in question to not expect a - vendor agent to be runing. - (optional, defaults to True) + flags to the image to inform the cloud in question to not expect a + vendor agent to be runing. (optional, defaults to True) :param allow_duplicates: If true, skips checks that enforce unique - image name. (optional, defaults to False) + image name. (optional, defaults to False) :param meta: A dict of key/value pairs to use for metadata that - bypasses automatic type conversion. + bypasses automatic type conversion. :param bool wait: If true, waits for image to be created. Defaults to - true - however, be aware that one of the upload - methods is always synchronous. + true - however, be aware that one of the upload methods is always + synchronous. :param timeout: Seconds to wait for image creation. None is forever. + :param bool validate_checksum: If true and cloud returns checksum, + compares return value with the one calculated or passed into this + call. If value does not match - raises exception. Default is + 'false' Additional kwargs will be passed to the image creation as additional metadata for the image and will have all values converted to string @@ -78,7 +90,7 @@ def create_image( :returns: A ``munch.Munch`` of the Image object - :raises: OpenStackCloudException if there are problems uploading + :raises: SDKException if there are problems uploading """ if container is None: container = self._connection._OBJECT_AUTOCREATE_CONTAINER @@ -93,7 +105,8 @@ def create_image( # If there is no filename, see if name is actually the filename if not filename: - name, filename = self._connection._get_name_and_filename(name) + name, filename = self._get_name_and_filename( + name, self._connection.config.config['image_format']) if not (md5 or sha256): (md5, sha256) = self._connection._get_file_hashes(filename) if allow_duplicates: @@ -102,25 +115,19 @@ def create_image( current_image = self._connection.get_image(name) if current_image: md5_key = current_image.get( - self._connection._IMAGE_MD5_KEY, - current_image.get( - self._connection._SHADE_IMAGE_MD5_KEY, '')) + self._IMAGE_MD5_KEY, + current_image.get(self._SHADE_IMAGE_MD5_KEY, '')) sha256_key = current_image.get( - self._connection._IMAGE_SHA256_KEY, - current_image.get( - self._connection._SHADE_IMAGE_SHA256_KEY, '')) + self._IMAGE_SHA256_KEY, + current_image.get(self._SHADE_IMAGE_SHA256_KEY, '')) up_to_date = self._connection._hashes_up_to_date( md5=md5, sha256=sha256, md5_key=md5_key, sha256_key=sha256_key) if up_to_date: - self._connection.log.debug( + self.log.debug( "image %(name)s exists and is up to date", {'name': name}) return current_image - kwargs[self._connection._IMAGE_MD5_KEY] = md5 or '' - kwargs[self._connection._IMAGE_SHA256_KEY] = sha256 or '' - kwargs[self._connection._IMAGE_OBJECT_KEY] = '/'.join( - [container, name]) if disable_vendor_agent: kwargs.update( @@ -129,6 +136,10 @@ def create_image( # If a user used the v1 calling format, they will have # passed a dict called properties along properties = kwargs.pop('properties', {}) + properties[self._IMAGE_MD5_KEY] = md5 or '' + properties[self._IMAGE_SHA256_KEY] = sha256 or '' + properties[self._IMAGE_OBJECT_KEY] = '/'.join( + [container, name]) kwargs.update(properties) image_kwargs = dict(properties=kwargs) if disk_format: @@ -136,15 +147,25 @@ def create_image( if container_format: image_kwargs['container_format'] = container_format - image = self._upload_image( - name, filename, - wait=wait, timeout=timeout, - meta=meta, **image_kwargs) + if filename: + image = self._upload_image( + name, filename=filename, meta=meta, + wait=wait, timeout=timeout, + validate_checksum=validate_checksum, + **image_kwargs) + else: + image = self._create_image(**image_kwargs) self._connection._get_cache(None).invalidate() return image @abc.abstractmethod - def _upload_image(self, name, filename, meta, **image_kwargs): + def _create_image(self, name, **image_kwargs): + pass + + @abc.abstractmethod + def _upload_image(self, name, filename, meta, wait, timeout, + validate_checksum=True, + **image_kwargs): pass @abc.abstractmethod @@ -180,3 +201,16 @@ def update_image_properties( img_props[k] = v return self._update_image_properties(image, meta, img_props) + + def _get_name_and_filename(self, name, image_format): + # See if name points to an existing file + if os.path.exists(name): + # Neat. Easy enough + return (os.path.splitext(os.path.basename(name))[0], name) + + # Try appending the disk format + name_with_ext = '.'.join((name, image_format)) + if os.path.exists(name_with_ext): + return (os.path.basename(name), name_with_ext) + + return (name, None) diff --git a/openstack/image/v1/_proxy.py b/openstack/image/v1/_proxy.py index 782641b76..d7a0706a8 100644 --- a/openstack/image/v1/_proxy.py +++ b/openstack/image/v1/_proxy.py @@ -18,6 +18,11 @@ class Proxy(_base_proxy.BaseImageProxy): + def _create_image(self, **kwargs): + """Create image resource from attributes + """ + return self._create(_image.Image, **kwargs) + def upload_image(self, **attrs): """Upload a new image from attributes @@ -48,8 +53,7 @@ def _upload_image( image = self._connection._get_and_munchify( 'image', self.post('/images', json=image_kwargs)) - checksum = image_kwargs['properties'].get( - self._connection._IMAGE_MD5_KEY, '') + checksum = image_kwargs['properties'].get(self._IMAGE_MD5_KEY, '') try: # Let us all take a brief moment to be grateful that this @@ -67,13 +71,13 @@ def _upload_image( headers=headers, data=image_data)) except exc.OpenStackCloudHTTPError: - self._connection.log.debug( + self.log.debug( "Deleting failed upload of image %s", name) try: self.delete('/images/{id}'.format(id=image.id)) except exc.OpenStackCloudHTTPError: # We're just trying to clean up - if it doesn't work - shrug - self._connection.log.warning( + self.log.warning( "Failed deleting image after we failed uploading it.", exc_info=True) raise @@ -145,7 +149,7 @@ def images(self, **query): :returns: A generator of image objects :rtype: :class:`~openstack.image.v1.image.Image` """ - return self._list(_image.Image, **query) + return self._list(_image.Image, base_path='/images/detail', **query) def update_image(self, image, **attrs): """Update a image @@ -159,3 +163,44 @@ def update_image(self, image, **attrs): :rtype: :class:`~openstack.image.v1.image.Image` """ return self._update(_image.Image, image, **attrs) + + def download_image(self, image, stream=False, output=None, + chunk_size=1024): + """Download an image + + This will download an image to memory when ``stream=False``, or allow + streaming downloads using an iterator when ``stream=True``. + For examples of working with streamed responses, see + :ref:`download_image-stream-true`. + + :param image: The value can be either the ID of an image or a + :class:`~openstack.image.v2.image.Image` instance. + + :param bool stream: When ``True``, return a :class:`requests.Response` + instance allowing you to iterate over the + response data stream instead of storing its entire + contents in memory. See + :meth:`requests.Response.iter_content` for more + details. *NOTE*: If you do not consume + the entirety of the response you must explicitly + call :meth:`requests.Response.close` or otherwise + risk inefficiencies with the ``requests`` + library's handling of connections. + + + When ``False``, return the entire + contents of the response. + :param output: Either a file object or a path to store data into. + :param int chunk_size: size in bytes to read from the wire and buffer + at one time. Defaults to 1024 + + :returns: When output is not given - the bytes comprising the given + Image when stream is False, otherwise a :class:`requests.Response` + instance. When output is given - a + :class:`~openstack.image.v2.image.Image` instance. + """ + + image = self._get_resource(_image.Image, image) + + return image.download( + self, stream=stream, output=output, chunk_size=chunk_size) diff --git a/openstack/image/v1/image.py b/openstack/image/v1/image.py index bd060146c..6760ed51f 100644 --- a/openstack/image/v1/image.py +++ b/openstack/image/v1/image.py @@ -9,8 +9,13 @@ # 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 hashlib +import io +import six +from openstack import exceptions from openstack import resource +from openstack import utils class Image(resource.Resource): @@ -25,6 +30,10 @@ class Image(resource.Resource): allow_delete = True allow_list = True + # Store all unknown attributes under 'properties' in the object. + # Remotely they would be still in the resource root + _store_unknown_attrs_as_properties = True + #: Hash of the image data used. The Image service uses this value #: for verification. checksum = resource.Body('checksum') @@ -69,3 +78,58 @@ class Image(resource.Resource): status = resource.Body('status') #: The timestamp when this image was last updated. updated_at = resource.Body('updated_at') + + def download(self, session, stream=False, output=None, chunk_size=1024): + """Download the data contained in an image""" + # TODO(briancurtin): This method should probably offload the get + # operation into another thread or something of that nature. + url = utils.urljoin(self.base_path, self.id, 'file') + resp = session.get(url, stream=stream) + + # See the following bug report for details on why the checksum + # code may sometimes depend on a second GET call. + # https://storyboard.openstack.org/#!/story/1619675 + checksum = resp.headers.get("Content-MD5") + + if checksum is None: + # If we don't receive the Content-MD5 header with the download, + # make an additional call to get the image details and look at + # the checksum attribute. + details = self.fetch(session) + checksum = details.checksum + + if output: + try: + # In python 2 we might get StringIO - delete it as soon as + # py2 support is dropped + if isinstance(output, io.IOBase) \ + or isinstance(output, six.StringIO): + for chunk in resp.iter_content(chunk_size=chunk_size): + output.write(chunk) + else: + with open(output, 'wb') as fd: + for chunk in resp.iter_content( + chunk_size=chunk_size): + fd.write(chunk) + return resp + except Exception as e: + raise exceptions.SDKException( + "Unable to download image: %s" % e) + # if we are returning the repsonse object, ensure that it + # has the content-md5 header so that the caller doesn't + # need to jump through the same hoops through which we + # just jumped. + if stream: + resp.headers['content-md5'] = checksum + return resp + + if checksum is not None: + digest = hashlib.md5(resp.content).hexdigest() + if digest != checksum: + raise exceptions.InvalidResponse( + "checksum mismatch: %s != %s" % (checksum, digest)) + else: + session.log.warn( + "Unable to verify the integrity of image %s" % (self.id)) + + return resp diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 360843260..04965b372 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -10,13 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. -import json -import jsonpatch -import operator import time import warnings -from openstack.cloud import exc from openstack import exceptions from openstack.image import _base_proxy from openstack.image.v2 import image as _image @@ -34,6 +30,11 @@ class Proxy(_base_proxy.BaseImageProxy): + def _create_image(self, **kwargs): + """Create image resource from attributes + """ + return self._create(_image.Image, **kwargs) + def import_image(self, image, method='glance-direct', uri=None): """Import data to an existing image @@ -73,14 +74,13 @@ def upload_image(self, container_format=None, disk_format=None, `create_image`. :param container_format: Format of the container. - A valid value is ami, ari, aki, bare, - ovf, ova, or docker. + A valid value is ami, ari, aki, bare, ovf, ova, or docker. :param disk_format: The format of the disk. A valid value is ami, - ari, aki, vhd, vmdk, raw, qcow2, vdi, or iso. + ari, aki, vhd, vmdk, raw, qcow2, vdi, or iso. :param data: The data to be uploaded as an image. :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.image.v2.image.Image`, - comprised of the properties on the Image class. + a :class:`~openstack.image.v2.image.Image`, comprised of the + properties on the Image class. :returns: The results of image creation :rtype: :class:`~openstack.image.v2.image.Image` @@ -110,9 +110,9 @@ def upload_image(self, container_format=None, disk_format=None, return img - def _upload_image( - self, name, filename=None, - meta=None, **kwargs): + def _upload_image(self, name, filename=None, meta=None, + wait=False, timeout=None, validate_checksum=True, + **kwargs): # We can never have nice things. Glance v1 took "is_public" as a # boolean. Glance v2 takes "visibility". If the user gives us # is_public, we know what they mean. If they give us visibility, they @@ -128,17 +128,18 @@ def _upload_image( # This makes me want to die inside if self._connection.image_api_use_tasks: return self._upload_image_task( - name, filename, - meta=meta, **kwargs) + name, filename, meta=meta, + wait=wait, timeout=timeout, **kwargs) else: return self._upload_image_put( name, filename, meta=meta, + validate_checksum=validate_checksum, **kwargs) - except exc.OpenStackCloudException: - self._connection.log.debug("Image creation failed", exc_info=True) + except exceptions.SDKException: + self.log.debug("Image creation failed", exc_info=True) raise except Exception as e: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Image creation failed: {message}".format(message=str(e))) def _make_v2_image_params(self, meta, properties): @@ -157,7 +158,7 @@ def _make_v2_image_params(self, meta, properties): return ret def _upload_image_put( - self, name, filename, meta, wait, timeout, **image_kwargs): + self, name, filename, meta, validate_checksum, **image_kwargs): image_data = open(filename, 'rb') properties = image_kwargs.pop('properties', {}) @@ -165,46 +166,46 @@ def _upload_image_put( image_kwargs.update(self._make_v2_image_params(meta, properties)) image_kwargs['name'] = name - data = self.post('/images', json=image_kwargs) - image = self._connection._get_and_munchify(key=None, data=data) + image = self._create(_image.Image, **image_kwargs) + + image.data = image_data try: - response = self.put( - '/images/{id}/file'.format(id=image.id), - headers={'Content-Type': 'application/octet-stream'}, - data=image_data) + response = image.upload(self) exceptions.raise_from_response(response) + # image_kwargs are flat here + md5 = image_kwargs.get(self._IMAGE_MD5_KEY) + sha256 = image_kwargs.get(self._IMAGE_SHA256_KEY) + if validate_checksum and (md5 or sha256): + # Verify that the hash computed remotely matches the local + # value + data = image.fetch(self) + checksum = data.get('checksum') + if checksum: + valid = (checksum == md5 or checksum == sha256) + if not valid: + raise Exception('Image checksum verification failed') except Exception: - self._connection.log.debug( + self.log.debug( "Deleting failed upload of image %s", name) - try: - response = self.delete( - '/images/{id}'.format(id=image.id)) - exceptions.raise_from_response(response) - except exc.OpenStackCloudHTTPError: - # We're just trying to clean up - if it doesn't work - shrug - self._connection.log.warning( - "Failed deleting image after we failed uploading it.", - exc_info=True) + self.delete_image(image.id) raise - return self._connection._normalize_image(image) + return image def _upload_image_task( self, name, filename, wait, timeout, meta, **image_kwargs): if not self._connection.has_service('object-store'): - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "The cloud {cloud} is configured to use tasks for image" " upload, but no object-store service is available." " Aborting.".format(cloud=self._connection.config.name)) - properties = image_kwargs.pop('properties', {}) - md5 = properties[self._connection._IMAGE_MD5_KEY] - sha256 = properties[self._connection._IMAGE_SHA256_KEY] - container = properties[ - self._connection._IMAGE_OBJECT_KEY].split('/', 1)[0] - image_kwargs.update(properties) + properties = image_kwargs.get('properties', {}) + md5 = properties[self._IMAGE_MD5_KEY] + sha256 = properties[self._IMAGE_SHA256_KEY] + container = properties[self._IMAGE_OBJECT_KEY].split('/', 1)[0] image_kwargs.pop('disk_format', None) image_kwargs.pop('container_format', None) @@ -222,70 +223,61 @@ def _upload_image_task( import_from='{container}/{name}'.format( container=container, name=name), image_properties=dict(name=name))) - data = self.post('/tasks', json=task_args) - glance_task = self._connection._get_and_munchify(key=None, data=data) + + glance_task = self.create_task(**task_args) self._connection.list_images.invalidate(self) if wait: start = time.time() - image_id = None - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for the image to import."): - if image_id is None: - response = self.get( - '/tasks/{id}'.format(id=glance_task.id)) - status = self._connection._get_and_munchify( - key=None, data=response) - - if status['status'] == 'success': - image_id = status['result']['image_id'] - image = self._connection.get_image(image_id) - if image is None: - continue - self.update_image_properties( - image=image, meta=meta, **image_kwargs) - self._connection.log.debug( - "Image Task %s imported %s in %s", - glance_task.id, image_id, (time.time() - start)) - # Clean up after ourselves. The object we created is not - # needed after the import is done. - self._connection.delete_object(container, name) - return self._connection.get_image(image_id) - elif status['status'] == 'failure': - if status['message'] == _IMAGE_ERROR_396: - glance_task = self.post('/tasks', data=task_args) - self._connection.list_images.invalidate(self) - else: - # Clean up after ourselves. The image did not import - # and this isn't a 'just retry' error - glance didn't - # like the content. So we don't want to keep it for - # next time. - self._connection.delete_object(container, name) - raise exc.OpenStackCloudException( - "Image creation failed: {message}".format( - message=status['message']), - extra_data=status) + + try: + glance_task = self.wait_for_task( + task=glance_task, + status='success', + wait=timeout) + + image_id = glance_task.result['image_id'] + image = self.get_image(image_id) + # NOTE(gtema): Since we might move unknown attributes of + # the image under properties - merge current with update + # properties not to end up removing "existing" properties + props = image.properties.copy() + props.update(image_kwargs.pop('properties', {})) + image_kwargs['properties'] = props + + image = self.update_image(image, **image_kwargs) + self.log.debug( + "Image Task %s imported %s in %s", + glance_task.id, image_id, (time.time() - start)) + except exceptions.ResourceFailure as e: + glance_task = self.get_task(glance_task) + raise exceptions.SDKException( + "Image creation failed: {message}".format( + message=e.message), + extra_data=glance_task) + finally: + # Clean up after ourselves. The object we created is not + # needed after the import is done. + self._connection.delete_object(container, name) + self._connection.list_images.invalidate(self) + return image else: return glance_task def _update_image_properties(self, image, meta, properties): + if not isinstance(image, _image.Image): + # If we come here with a dict (cloud) - convert dict to real object + # to properly consume all properties (to calculate the diff). + # This currently happens from unittests. + image = _image.Image.existing(**image) img_props = image.properties.copy() + for k, v in iter(self._make_v2_image_params(meta, properties).items()): if image.get(k, None) != v: img_props[k] = v if not img_props: return False - headers = { - 'Content-Type': 'application/openstack-images-v2.1-json-patch'} - patch = sorted(list(jsonpatch.JsonPatch.from_diff( - image.properties, img_props)), key=operator.itemgetter('value')) - - # No need to fire an API call if there is an empty patch - if patch: - self.patch( - '/images/{id}'.format(id=image.id), - headers=headers, - data=json.dumps(patch)) + + self.update_image(image, **img_props) self._connection.list_images.invalidate(self._connection) return True @@ -293,7 +285,8 @@ def _update_image_properties(self, image, meta, properties): def _existing_image(self, **kwargs): return _image.Image.existing(connection=self._connection, **kwargs) - def download_image(self, image, stream=False): + def download_image(self, image, stream=False, output=None, + chunk_size=1024): """Download an image This will download an image to memory when ``stream=False``, or allow @@ -318,14 +311,20 @@ def download_image(self, image, stream=False): When ``False``, return the entire contents of the response. - - :returns: The bytes comprising the given Image when stream is - False, otherwise a :class:`requests.Response` - instance. + :param output: Either a file object or a path to store data into. + :param int chunk_size: size in bytes to read from the wire and buffer + at one time. Defaults to 1024 + + :returns: When output is not given - the bytes comprising the given + Image when stream is False, otherwise a :class:`requests.Response` + instance. When output is given - a + :class:`~openstack.image.v2.image.Image` instance. """ image = self._get_resource(_image.Image, image) - return image.download(self, stream=stream) + + return image.download( + self, stream=stream, output=output, chunk_size=chunk_size) def delete_image(self, image, ignore_missing=True): """Delete an image @@ -638,9 +637,45 @@ def wait_for_task(self, task, status='success', failures=None, :raises: :class:`~AttributeError` if the resource does not have a ``status`` attribute. """ - failures = ['failure'] if failures is None else failures - return resource.wait_for_status( - self, task, status, failures, interval, wait) + if failures is None: + failures = ['failure'] + else: + failures = [f.lower() for f in failures] + + if task.status.lower() == status.lower(): + return task + + name = "{res}:{id}".format(res=task.__class__.__name__, id=task.id) + msg = "Timeout waiting for {name} to transition to {status}".format( + name=name, status=status) + + for count in utils.iterate_timeout( + timeout=wait, + message=msg, + wait=interval): + task = task.fetch(self) + + if not task: + raise exceptions.ResourceFailure( + "{name} went away while waiting for {status}".format( + name=name, status=status)) + + new_status = task.status + normalized_status = new_status.lower() + if normalized_status == status.lower(): + return task + elif normalized_status in failures: + if task.message == _IMAGE_ERROR_396: + task_args = dict(input=task.input, type=task.type) + task = self.create_task(**task_args) + self.log.debug('Got error 396. Recreating task %s' % task) + else: + raise exceptions.ResourceFailure( + "{name} transitioned to failure state {status}".format( + name=name, status=new_status)) + + self.log.debug('Still waiting for resource %s to reach state %s, ' + 'current state is %s', name, status, new_status) def get_tasks_schema(self): """Get image tasks schema diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index bf4ca69d3..cc6a3236c 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -11,14 +11,13 @@ # under the License. import hashlib +import io +import six -from openstack import _log from openstack import exceptions from openstack import resource from openstack import utils -_logger = _log.setup_logging('openstack') - class Image(resource.Resource, resource.TagMixin): resources_key = 'images' @@ -33,6 +32,10 @@ class Image(resource.Resource, resource.TagMixin): commit_method = 'PATCH' commit_jsonpatch = True + # Store all unknown attributes under 'properties' in the object. + # Remotely they would be still in the resource root + _store_unknown_attrs_as_properties = True + _query_mapping = resource.QueryParameters( "name", "visibility", "member_status", "owner", @@ -135,7 +138,7 @@ class Image(resource.Resource, resource.TagMixin): architecture = resource.Body("architecture") #: The hypervisor type. Note that qemu is used for both QEMU and #: KVM hypervisor types. - hypervisor_type = resource.Body("hypervisor-type") + hypervisor_type = resource.Body("hypervisor_type") #: Optional property allows created servers to have a different bandwidth #: cap than that defined in the network they are attached to. instance_type_rxtx_factor = resource.Body( @@ -157,6 +160,8 @@ class Image(resource.Resource, resource.TagMixin): #: Secure Boot first examines software such as firmware and OS by #: their signature and only allows them to run if the signatures are valid. needs_secure_boot = resource.Body('os_secure_boot') + #: Time for graceful shutdown + os_shutdown_timeout = resource.Body('os_shutdown_timeout', type=int) #: The ID of image stored in the Image service that should be used as #: the ramdisk when booting an AMI-style image. ramdisk_id = resource.Body('ramdisk_id') @@ -172,6 +177,12 @@ class Image(resource.Resource, resource.TagMixin): #: Specifies the type of disk controller to attach disk devices to. #: One of scsi, virtio, uml, xen, ide, or usb. hw_disk_bus = resource.Body('hw_disk_bus') + #: Used to pin the virtual CPUs (vCPUs) of instances to the + #: host's physical CPU cores (pCPUs). + hw_cpu_policy = resource.Body('hw_cpu_policy') + #: Defines how hardware CPU threads in a simultaneous + #: multithreading-based (SMT) architecture be used. + hw_cpu_thread_policy = resource.Body('hw_cpu_thread_policy') #: Adds a random-number generator device to the image's instances. hw_rng_model = resource.Body('hw_rng_model') #: For libvirt: Enables booting an ARM system using the specified @@ -212,7 +223,7 @@ class Image(resource.Resource, resource.TagMixin): vmware_ostype = resource.Body('vmware_ostype') #: If true, the root partition on the disk is automatically resized #: before the instance boots. - has_auto_disk_config = resource.Body('auto_disk_config', type=bool) + has_auto_disk_config = resource.Body('auto_disk_config') #: The operating system installed on the image. os_type = resource.Body('os_type') #: The operating system admin username. @@ -221,6 +232,8 @@ class Image(resource.Resource, resource.TagMixin): hw_qemu_guest_agent = resource.Body('hw_qemu_guest_agent', type=bool) #: If true, require quiesce on snapshot via QEMU guest agent. os_require_quiesce = resource.Body('os_require_quiesce', type=bool) + #: The URL for the schema describing a virtual machine image. + schema = resource.Body('schema') def _action(self, session, action): """Call an action on an image ID.""" @@ -245,9 +258,9 @@ def reactivate(self, session): def upload(self, session): """Upload data into an existing image""" url = utils.urljoin(self.base_path, self.id, 'file') - session.put(url, data=self.data, - headers={"Content-Type": "application/octet-stream", - "Accept": ""}) + return session.put(url, data=self.data, + headers={"Content-Type": "application/octet-stream", + "Accept": ""}) def import_image(self, session, method='glance-direct', uri=None): """Import Image via interoperable image import process""" @@ -261,7 +274,7 @@ def import_image(self, session, method='glance-direct', uri=None): 'method: "web-download"') session.post(url, json=json) - def download(self, session, stream=False): + def download(self, session, stream=False, output=None, chunk_size=1024): """Download the data contained in an image""" # TODO(briancurtin): This method should probably offload the get # operation into another thread or something of that nature. @@ -271,7 +284,7 @@ def download(self, session, stream=False): # See the following bug report for details on why the checksum # code may sometimes depend on a second GET call. # https://storyboard.openstack.org/#!/story/1619675 - checksum = resp.headers.get("Content-MD5") + checksum = resp.headers.get('Content-MD5') if checksum is None: # If we don't receive the Content-MD5 header with the download, @@ -280,6 +293,23 @@ def download(self, session, stream=False): details = self.fetch(session) checksum = details.checksum + if output: + try: + # In python 2 we might get StringIO - delete it as soon as + # py2 support is dropped + if isinstance(output, io.IOBase) \ + or isinstance(output, six.StringIO): + for chunk in resp.iter_content(chunk_size=chunk_size): + output.write(chunk) + else: + with open(output, 'wb') as fd: + for chunk in resp.iter_content( + chunk_size=chunk_size): + fd.write(chunk) + return resp + except Exception as e: + raise exceptions.SDKException( + 'Unable to download image: %s' % e) # if we are returning the repsonse object, ensure that it # has the content-md5 header so that the caller doesn't # need to jump through the same hoops through which we @@ -294,10 +324,10 @@ def download(self, session, stream=False): raise exceptions.InvalidResponse( "checksum mismatch: %s != %s" % (checksum, digest)) else: - _logger.warn( + session.log.warn( "Unable to verify the integrity of image %s" % (self.id)) - return resp.content + return resp def _prepare_request(self, requires_id=None, prepend_key=False, patch=False, base_path=None): diff --git a/openstack/resource.py b/openstack/resource.py index bdb9c2092..b325e9054 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -35,6 +35,7 @@ class that represent a remote resource. The attributes that import itertools import jsonpatch +import operator from keystoneauth1 import adapter from keystoneauth1 import discover import munch @@ -425,6 +426,7 @@ class Resource(dict): _computed = None _original_body = None _delete_response_class = None + _store_unknown_attrs_as_properties = False def __init__(self, _synchronized=False, connection=None, **attrs): """The base resource @@ -445,9 +447,6 @@ def __init__(self, _synchronized=False, connection=None, **attrs): # items as they match up with any of the body, header, # or uri mappings. body, header, uri, computed = self._collect_attrs(attrs) - # TODO(briancurtin): at this point if attrs has anything left - # they're not being set anywhere. Log this? Raise exception? - # How strict should we be here? Should strict be an option? self._body = _ComponentManager( attributes=body, @@ -472,6 +471,13 @@ def __init__(self, _synchronized=False, connection=None, **attrs): } else: self._original_body = {} + if self._store_unknown_attrs_as_properties: + # When storing of unknown attributes is requested - ensure + # we have properties attribute (with type=None) + self._store_unknown_attrs_as_properties = ( + hasattr(self.__class__, 'properties') + and self.__class__.properties.type is None + ) self._update_location() @@ -504,7 +510,7 @@ def __eq__(self, comparand): self._body.attributes == comparand._body.attributes, self._header.attributes == comparand._header.attributes, self._uri.attributes == comparand._uri.attributes, - self._computed.attributes == comparand._computed.attributes, + self._computed.attributes == comparand._computed.attributes ]) def __getattribute__(self, name): @@ -602,6 +608,10 @@ def _collect_attrs(self, attrs): header = self._consume_header_attrs(attrs) uri = self._consume_uri_attrs(attrs) + if attrs and self._store_unknown_attrs_as_properties: + # Keep also remaining (unknown) attributes + body = self._pack_attrs_under_properties(body, attrs) + if any([body, header, uri]): attrs = self._compute_attributes(body, header, uri) @@ -911,12 +921,55 @@ def _to_munch(self, original_names=True): body=True, headers=False, original_names=original_names, _to_munch=True) + def _unpack_properties_to_resource_root(self, body): + if not body: + return + # We do not want to modify caller + body = body.copy() + props = body.pop('properties', {}) + if props and isinstance(props, dict): + # unpack dict of properties back to the root of the resource + body.update(props) + elif props and isinstance(props, str): + # A string value only - bring it back + body['properties'] = props + return body + + def _pack_attrs_under_properties(self, body, attrs): + props = body.get('properties', {}) + if not isinstance(props, dict): + props = {'properties': props} + props.update(attrs) + body['properties'] = props + return body + def _prepare_request_body(self, patch, prepend_key): if patch: - new = self._body.attributes - body = jsonpatch.make_patch(self._original_body, new).patch + if not self._store_unknown_attrs_as_properties: + # Default case + new = self._body.attributes + original_body = self._original_body + else: + new = self._unpack_properties_to_resource_root( + self._body.attributes) + original_body = self._unpack_properties_to_resource_root( + self._original_body) + + # NOTE(gtema) sort result, since we might need validate it in tests + body = sorted( + list(jsonpatch.make_patch( + original_body, + new).patch), + key=operator.itemgetter('path') + ) else: - body = self._body.dirty + if not self._store_unknown_attrs_as_properties: + # Default case + body = self._body.dirty + else: + body = self._unpack_properties_to_resource_root( + self._body.dirty) + if prepend_key and self.resource_key is not None: body = {self.resource_key: body} return body @@ -980,12 +1033,17 @@ def _translate_response(self, response, has_body=None, error_message=None): if self.resource_key and self.resource_key in body: body = body[self.resource_key] - body = self._consume_body_attrs(body) - self._body.attributes.update(body) + body_attrs = self._consume_body_attrs(body) + + if self._store_unknown_attrs_as_properties: + body_attrs = self._pack_attrs_under_properties( + body_attrs, body) + + self._body.attributes.update(body_attrs) self._body.clean() if self.commit_jsonpatch or self.allow_patch: # We need the original body to compare against - self._original_body = body.copy() + self._original_body = body_attrs.copy() except ValueError: # Server returned not parse-able response (202, 204, etc) # Do simply nothing diff --git a/openstack/tests/fakes.py b/openstack/tests/fakes.py index c0fee5774..9055075b4 100644 --- a/openstack/tests/fakes.py +++ b/openstack/tests/fakes.py @@ -225,7 +225,7 @@ def make_fake_image( u'image_state': u'available', u'container_format': u'bare', u'min_ram': 0, - u'ramdisk_id': None, + u'ramdisk_id': 'fake_ramdisk_id', u'updated_at': u'2016-02-10T05:05:02Z', u'file': '/v2/images/' + image_id + '/file', u'size': 3402170368, diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index c50cdf689..f972f7603 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -83,7 +83,15 @@ def test_download_image_two_outputs(self): def test_download_image_no_images_found(self): self.register_uris([ dict(method='GET', - uri='https://image.example.com/v2/images', + uri='https://image.example.com/v2/images/{name}'.format( + name=self.image_name), + status_code=404), + dict(method='GET', + uri='https://image.example.com/v2/images?name={name}'.format( + name=self.image_name), + json=dict(images=[])), + dict(method='GET', + uri='https://image.example.com/v2/images?os_hidden=True', json=dict(images=[]))]) self.assertRaises(exc.OpenStackCloudResourceNotFound, self.cloud.download_image, self.image_name, @@ -93,13 +101,21 @@ def test_download_image_no_images_found(self): def _register_image_mocks(self): self.register_uris([ dict(method='GET', - uri='https://image.example.com/v2/images', + uri='https://image.example.com/v2/images/{name}'.format( + name=self.image_name), + status_code=404), + dict(method='GET', + uri='https://image.example.com/v2/images?name={name}'.format( + name=self.image_name), json=self.fake_search_return), dict(method='GET', uri='https://image.example.com/v2/images/{id}/file'.format( id=self.image_id), content=self.output, - headers={'Content-Type': 'application/octet-stream'}) + headers={ + 'Content-Type': 'application/octet-stream', + 'Content-MD5': self.fake_image_dict['checksum'] + }) ]) def test_download_image_with_fd(self): @@ -147,7 +163,7 @@ def test_get_image_by_id(self): base_url_append='v2'), json=self.fake_image_dict) ]) - self.assertEqual( + self.assertDictEqual( self.cloud._normalize_image(self.fake_image_dict), self.cloud.get_image_by_id(self.image_id)) self.assert_calls() @@ -326,6 +342,12 @@ def test_create_image_put_v2(self): 'image', append=['images', self.image_id, 'file'], base_url_append='v2'), request_headers={'Content-Type': 'application/octet-stream'}), + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images', self.fake_image_dict['id']], + base_url_append='v2' + ), + json=self.fake_image_dict), dict(method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2'), @@ -411,7 +433,7 @@ def test_create_image_task(self): dict(method='POST', uri=self.get_mock_url( 'image', append=['tasks'], base_url_append='v2'), - json=args, + json={'id': task_id, 'status': 'processing'}, validate=dict( json=dict( type='import', input={ @@ -430,8 +452,9 @@ def test_create_image_task(self): json=args), dict(method='GET', uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - json={'images': [image_no_checksums]}), + 'image', append=['images', self.image_id], + base_url_append='v2'), + json=image_no_checksums), dict(method='PATCH', uri=self.get_mock_url( 'image', append=['images', self.image_id], @@ -447,10 +470,11 @@ def test_create_image_task(self): u'path': u'/owner_specified.openstack.md5'}, {u'op': u'add', u'value': fakes.NO_SHA256, u'path': u'/owner_specified.openstack.sha256'}], - key=operator.itemgetter('value')), + key=operator.itemgetter('path')), headers={ 'Content-Type': - 'application/openstack-images-v2.1-json-patch'}) + 'application/openstack-images-v2.1-json-patch'}), + json=self.fake_search_return ), dict(method='HEAD', uri='{endpoint}/{container}/{object}'.format( @@ -471,14 +495,6 @@ def test_create_image_task(self): uri='{endpoint}/{container}/{object}'.format( endpoint=endpoint, container=self.container_name, object=self.image_name)), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - json=self.fake_search_return), - # TODO(mordred) The task workflow results in an extra call - # in the upper level wait. We should be able to make this - # go away once we refactor a wait_for_image out in the next - # patch. dict(method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2'), @@ -634,7 +650,8 @@ def test_create_image_put_v1_bad_delete(self): 'owner_specified.openstack.sha256': fakes.NO_SHA256, 'owner_specified.openstack.object': 'images/{name}'.format( name=self.image_name), - 'is_public': False}} + 'is_public': False}, + 'validate_checksum': True} ret = args.copy() ret['id'] = self.image_id @@ -735,6 +752,52 @@ def test_create_image_put_v2_bad_delete(self): self.assert_calls() + def test_create_image_put_v2_wrong_checksum_delete(self): + self.cloud.image_api_use_tasks = False + + args = {'name': self.image_name, + 'container_format': 'bare', 'disk_format': 'qcow2', + 'owner_specified.openstack.md5': fakes.NO_MD5, + 'owner_specified.openstack.sha256': fakes.NO_SHA256, + 'owner_specified.openstack.object': 'images/{name}'.format( + name=self.image_name), + 'visibility': 'private'} + + ret = args.copy() + ret['id'] = self.image_id + ret['status'] = 'success' + ret['checksum'] = 'fake' + + self.register_uris([ + dict(method='GET', + uri='https://image.example.com/v2/images', + json={'images': []}), + dict(method='POST', + uri='https://image.example.com/v2/images', + json=ret, + validate=dict(json=args)), + dict(method='PUT', + uri='https://image.example.com/v2/images/{id}/file'.format( + id=self.image_id), + status_code=400, + validate=dict( + headers={ + 'Content-Type': 'application/octet-stream', + }, + )), + dict(method='DELETE', + uri='https://image.example.com/v2/images/{id}'.format( + id=self.image_id)), + ]) + + self.assertRaises( + exc.OpenStackCloudHTTPError, + self._call_create_image, + self.image_name, + md5='some_fake') + + self.assert_calls() + def test_create_image_put_bad_int(self): self.cloud.image_api_use_tasks = False @@ -784,6 +847,11 @@ def test_create_image_put_user_int(self): 'Content-Type': 'application/octet-stream', }, )), + dict(method='GET', + uri='https://image.example.com/v2/images/{id}'.format( + id=self.image_id + ), + json=ret), dict(method='GET', uri='https://image.example.com/v2/images', json={'images': [ret]}), @@ -810,6 +878,7 @@ def test_create_image_put_meta_int(self): ret = args.copy() ret['id'] = self.image_id ret['status'] = 'success' + ret['checksum'] = fakes.NO_MD5 self.register_uris([ dict(method='GET', @@ -827,6 +896,11 @@ def test_create_image_put_meta_int(self): 'Content-Type': 'application/octet-stream', }, )), + dict(method='GET', + uri='https://image.example.com/v2/images/{id}'.format( + id=self.image_id + ), + json=ret), dict(method='GET', uri='https://image.example.com/v2/images', json={'images': [ret]}), @@ -854,6 +928,7 @@ def test_create_image_put_protected(self): ret = args.copy() ret['id'] = self.image_id ret['status'] = 'success' + ret['checksum'] = fakes.NO_MD5 self.register_uris([ dict(method='GET', @@ -871,6 +946,11 @@ def test_create_image_put_protected(self): 'Content-Type': 'application/octet-stream', }, )), + dict(method='GET', + uri='https://image.example.com/v2/images/{id}'.format( + id=self.image_id + ), + json=ret), dict(method='GET', uri='https://image.example.com/v2/images', json={'images': [ret]}), diff --git a/openstack/tests/unit/cloud/test_normalize.py b/openstack/tests/unit/cloud/test_normalize.py index e6e434f90..03fcc188d 100644 --- a/openstack/tests/unit/cloud/test_normalize.py +++ b/openstack/tests/unit/cloud/test_normalize.py @@ -11,6 +11,7 @@ # under the License. from openstack.compute.v2 import server as server_resource +from openstack.image.v2 import image as image_resource from openstack.tests.unit import base RAW_SERVER_DICT = { @@ -94,6 +95,9 @@ u'name': u'Test Monty Ubuntu', u'org.openstack__1__architecture': u'x64', u'os_type': u'linux', + u'os_hash_algo': u'sha512', + u'os_hash_value': u'fake_hash', + u'os_hidden': False, u'owner': u'610275', u'protected': False, u'schema': u'/v2/schemas/image', @@ -373,6 +377,9 @@ def test_normalize_glance_images(self): u'com.rackspace__1__visible_rackconnect': u'1', u'image_type': u'import', u'org.openstack__1__architecture': u'x64', + u'os_hash_algo': u'sha512', + u'os_hash_value': u'fake_hash', + u'os_hidden': False, u'os_type': u'linux', u'schema': u'/v2/schemas/image', u'user_id': u'156284', @@ -384,6 +391,9 @@ def test_normalize_glance_images(self): 'min_ram': 0, 'name': u'Test Monty Ubuntu', u'org.openstack__1__architecture': u'x64', + u'os_hash_algo': u'sha512', + u'os_hash_value': u'fake_hash', + u'os_hidden': False, u'os_type': u'linux', 'owner': u'610275', 'properties': { @@ -398,6 +408,9 @@ def test_normalize_glance_images(self): u'com.rackspace__1__visible_rackconnect': u'1', u'image_type': u'import', u'org.openstack__1__architecture': u'x64', + u'os_hash_algo': u'sha512', + u'os_hash_value': u'fake_hash', + u'os_hidden': False, u'os_type': u'linux', u'schema': u'/v2/schemas/image', u'user_id': u'156284', @@ -418,6 +431,12 @@ def test_normalize_glance_images(self): retval = self.cloud._normalize_image(raw_image) self.assertEqual(expected, retval) + # Check normalization from Image resource + image = image_resource.Image.existing(**RAW_GLANCE_IMAGE_DICT) + + retval = self.cloud._normalize_image(image) + self.assertDictEqual(expected, retval) + def test_normalize_servers_normal(self): res = server_resource.Server( connection=self.cloud, @@ -888,6 +907,9 @@ def test_normalize_glance_images(self): u'image_type': u'import', u'org.openstack__1__architecture': u'x64', u'os_type': u'linux', + u'os_hash_algo': u'sha512', + u'os_hash_value': u'fake_hash', + u'os_hidden': False, u'schema': u'/v2/schemas/image', u'user_id': u'156284', u'vm_mode': u'hvm', diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 7ea356a91..281c89f4c 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -11,12 +11,15 @@ # under the License. import operator +import six +import tempfile from keystoneauth1 import adapter import mock import requests from openstack.tests.unit import base +from openstack import _log from openstack import exceptions from openstack.image.v2 import image @@ -50,7 +53,7 @@ 'url': '20', 'metadata': {'21': '22'}, 'architecture': '23', - 'hypervisor-type': '24', + 'hypervisor_type': '24', 'instance_type_rxtx_factor': 25.1, 'instance_uuid': '26', 'img_config_drive': '27', @@ -115,6 +118,7 @@ def setUp(self): self.sess.fetch = mock.Mock(return_value=FakeResponse({})) self.sess.default_microversion = None self.sess.retriable_status_codes = None + self.sess.log = _log.setup_logging('openstack') def test_basic(self): sot = image.Image() @@ -175,7 +179,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['url'], sot.url) self.assertEqual(EXAMPLE['metadata'], sot.metadata) self.assertEqual(EXAMPLE['architecture'], sot.architecture) - self.assertEqual(EXAMPLE['hypervisor-type'], sot.hypervisor_type) + self.assertEqual(EXAMPLE['hypervisor_type'], sot.hypervisor_type) self.assertEqual(EXAMPLE['instance_type_rxtx_factor'], sot.instance_type_rxtx_factor) self.assertEqual(EXAMPLE['instance_uuid'], sot.instance_uuid) @@ -264,7 +268,7 @@ def test_import_image_with_uri_not_web_download(self): def test_upload(self): sot = image.Image(**EXAMPLE) - self.assertIsNone(sot.upload(self.sess)) + self.assertIsNotNone(sot.upload(self.sess)) self.sess.put.assert_called_with('images/IDENTIFIER/file', data=sot.data, headers={"Content-Type": @@ -284,7 +288,7 @@ def test_download_checksum_match(self): self.sess.get.assert_called_with('images/IDENTIFIER/file', stream=False) - self.assertEqual(rv, resp.content) + self.assertEqual(rv, resp) def test_download_checksum_mismatch(self): sot = image.Image(**EXAMPLE) @@ -314,7 +318,7 @@ def test_download_no_checksum_header(self): stream=False), mock.call('images/IDENTIFIER', microversion=None)]) - self.assertEqual(rv, resp1.content) + self.assertEqual(rv, resp1) def test_download_no_checksum_at_all2(self): sot = image.Image(**EXAMPLE) @@ -340,7 +344,7 @@ def test_download_no_checksum_at_all2(self): stream=False), mock.call('images/IDENTIFIER', microversion=None)]) - self.assertEqual(rv, resp1.content) + self.assertEqual(rv, resp1) def test_download_stream(self): sot = image.Image(**EXAMPLE) @@ -356,6 +360,29 @@ def test_download_stream(self): self.assertEqual(rv, resp) + def test_image_download_output_fd(self): + output_file = six.BytesIO() + sot = image.Image(**EXAMPLE) + response = mock.Mock() + response.status_code = 200 + response.iter_content.return_value = [b'01', b'02'] + self.sess.get = mock.Mock(return_value=response) + sot.download(self.sess, output=output_file) + output_file.seek(0) + self.assertEqual(b'0102', output_file.read()) + + def test_image_download_output_file(self): + sot = image.Image(**EXAMPLE) + response = mock.Mock() + response.status_code = 200 + response.iter_content.return_value = [b'01', b'02'] + self.sess.get = mock.Mock(return_value=response) + + output_file = tempfile.NamedTemporaryFile() + sot.download(self.sess, output=output_file.name) + output_file.seek(0) + self.assertEqual(b'0102', output_file.read()) + def test_image_update(self): values = EXAMPLE.copy() del values['instance_uuid'] diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 0690206ca..617d55f70 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -11,6 +11,7 @@ # under the License. import mock +import requests from openstack import exceptions from openstack.image.v2 import _proxy @@ -25,6 +26,17 @@ EXAMPLE = fake_image.EXAMPLE +class FakeResponse(object): + def __init__(self, response, status_code=200, headers=None): + self.body = response + self.status_code = status_code + headers = headers if headers else {'content-type': 'application/json'} + self.headers = requests.structures.CaseInsensitiveDict(headers) + + def json(self): + return self.body + + class TestImageProxy(test_proxy_base.TestProxyBase): def setUp(self): super(TestImageProxy, self).setUp() @@ -68,6 +80,20 @@ def test_image_upload(self): created_image.upload.assert_called_with(self.proxy) self.assertEqual(rv, created_image) + def test_image_download(self): + original_image = image.Image(**EXAMPLE) + self._verify('openstack.image.v2.image.Image.download', + self.proxy.download_image, + method_args=[original_image], + method_kwargs={ + 'output': 'some_output', + 'chunk_size': 1, + 'stream': True + }, + expected_kwargs={'output': 'some_output', + 'chunk_size': 1, + 'stream': True}) + def test_image_delete(self): self.verify_delete(self.proxy.delete_image, image.Image, False) @@ -210,12 +236,77 @@ def test_tasks(self): def test_task_create(self): self.verify_create(self.proxy.create_task, task.Task) - def test_task_wait_for(self): - value = task.Task(id='1234') - self.verify_wait_for_status( - self.proxy.wait_for_task, - method_args=[value], - expected_args=[value, 'success', ['failure'], 2, 120]) + def test_wait_for_task_immediate_status(self): + status = 'success' + res = task.Task(id='1234', status=status) + + result = self.proxy.wait_for_task( + res, status, "failure", 0.01, 0.1) + + self.assertTrue(result, res) + + def test_wait_for_task_immediate_status_case(self): + status = "SUCcess" + res = task.Task(id='1234', status=status) + + result = self.proxy.wait_for_task( + res, status, "failure", 0.01, 0.1) + + self.assertTrue(result, res) + + def test_wait_for_task_error_396(self): + # Ensure we create a new task when we get 396 error + res = task.Task( + id='id', status='waiting', + type='some_type', input='some_input', result='some_result' + ) + + mock_fetch = mock.Mock() + mock_fetch.side_effect = [ + task.Task( + id='id', status='failure', + type='some_type', input='some_input', result='some_result', + message=_proxy._IMAGE_ERROR_396 + ), + task.Task(id='fake', status='waiting'), + task.Task(id='fake', status='success'), + ] + + self.proxy._create = mock.Mock() + self.proxy._create.side_effect = [ + task.Task(id='fake', status='success') + ] + + with mock.patch.object(task.Task, + 'fetch', mock_fetch): + + result = self.proxy.wait_for_task( + res, interval=0.01, wait=0.1) + + self.assertEqual('success', result.status) + + self.proxy._create.assert_called_with( + mock.ANY, + input=res.input, + type=res.type) + + def test_wait_for_task_wait(self): + res = task.Task(id='id', status='waiting') + + mock_fetch = mock.Mock() + mock_fetch.side_effect = [ + task.Task(id='id', status='waiting'), + task.Task(id='id', status='waiting'), + task.Task(id='id', status='success'), + ] + + with mock.patch.object(task.Task, + 'fetch', mock_fetch): + + result = self.proxy.wait_for_task( + res, interval=0.01, wait=0.1) + + self.assertEqual('success', result.status) def test_tasks_schema_get(self): self._verify2("openstack.proxy.Proxy._get", diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 9da2b1469..fe0f824ea 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1045,6 +1045,154 @@ class Test(resource.Resource): sot._body.dirty = mock.Mock(return_value={"x": "y"}) self.assertRaises(exceptions.MethodNotSupported, sot.commit, "") + def test_unknown_attrs_under_props_create(self): + class Test(resource.Resource): + properties = resource.Body("properties") + _store_unknown_attrs_as_properties = True + + sot = Test.new(**{ + 'dummy': 'value', + }) + self.assertDictEqual({'dummy': 'value'}, sot.properties) + self.assertDictEqual( + {'dummy': 'value'}, sot.to_dict()['properties'] + ) + self.assertDictEqual( + {'dummy': 'value'}, sot['properties'] + ) + self.assertEqual('value', sot['properties']['dummy']) + + sot = Test.new(**{ + 'dummy': 'value', + 'properties': 'a,b,c' + }) + self.assertDictEqual( + {'dummy': 'value', 'properties': 'a,b,c'}, + sot.properties + ) + self.assertDictEqual( + {'dummy': 'value', 'properties': 'a,b,c'}, + sot.to_dict()['properties'] + ) + + sot = Test.new(**{'properties': None}) + self.assertIsNone(sot.properties) + self.assertIsNone(sot.to_dict()['properties']) + + def test_unknown_attrs_not_stored(self): + class Test(resource.Resource): + properties = resource.Body("properties") + + sot = Test.new(**{ + 'dummy': 'value', + }) + self.assertIsNone(sot.properties) + + def test_unknown_attrs_not_stored1(self): + class Test(resource.Resource): + _store_unknown_attrs_as_properties = True + + sot = Test.new(**{ + 'dummy': 'value', + }) + self.assertRaises(KeyError, sot.__getitem__, 'properties') + + def test_unknown_attrs_under_props_set(self): + class Test(resource.Resource): + properties = resource.Body("properties") + _store_unknown_attrs_as_properties = True + + sot = Test.new(**{ + 'dummy': 'value', + }) + + sot['properties'] = {'dummy': 'new_value'} + self.assertEqual('new_value', sot['properties']['dummy']) + sot.properties = {'dummy': 'new_value1'} + self.assertEqual('new_value1', sot['properties']['dummy']) + + def test_unknown_attrs_prepare_request_unpacked(self): + class Test(resource.Resource): + properties = resource.Body("properties") + _store_unknown_attrs_as_properties = True + + # Unknown attribute given as root attribute + sot = Test.new(**{ + 'dummy': 'value', + 'properties': 'a,b,c' + }) + + request_body = sot._prepare_request(requires_id=False).body + self.assertEqual('value', request_body['dummy']) + self.assertEqual('a,b,c', request_body['properties']) + + # properties are already a dict + sot = Test.new(**{ + 'properties': { + 'properties': 'a,b,c', + 'dummy': 'value' + } + }) + + request_body = sot._prepare_request(requires_id=False).body + self.assertEqual('value', request_body['dummy']) + self.assertEqual('a,b,c', request_body['properties']) + + def test_unknown_attrs_prepare_request_no_unpack_dict(self): + # if props type is not None - ensure no unpacking is done + class Test(resource.Resource): + properties = resource.Body("properties", type=dict) + sot = Test.new(**{ + 'properties': { + 'properties': 'a,b,c', + 'dummy': 'value' + } + }) + + request_body = sot._prepare_request(requires_id=False).body + self.assertDictEqual( + {'dummy': 'value', 'properties': 'a,b,c'}, + request_body['properties']) + + def test_unknown_attrs_prepare_request_patch_unpacked(self): + class Test(resource.Resource): + properties = resource.Body("properties") + _store_unknown_attrs_as_properties = True + commit_jsonpatch = True + + sot = Test.existing(**{ + 'dummy': 'value', + 'properties': 'a,b,c' + }) + + sot._update(**{'properties': {'dummy': 'new_value'}}) + + request_body = sot._prepare_request(requires_id=False, patch=True).body + self.assertDictEqual( + { + u'path': u'/dummy', + u'value': u'new_value', + u'op': u'replace' + }, + request_body[0]) + + def test_unknown_attrs_under_props_translate_response(self): + class Test(resource.Resource): + properties = resource.Body("properties") + _store_unknown_attrs_as_properties = True + + body = {'dummy': 'value', 'properties': 'a,b,c'} + response = FakeResponse(body) + + sot = Test() + + sot._translate_response(response, has_body=True) + + self.assertDictEqual( + {'dummy': 'value', 'properties': 'a,b,c'}, + sot.properties + ) + class TestResourceActions(base.TestCase): From cf9922e885959fac4f986fabdefb4694facb6f09 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Sat, 11 May 2019 09:40:38 +0200 Subject: [PATCH 2452/3836] Extract image download method into a mixin This is the same code for v1 and v2. While we're in there, add checksum verification for when download is used with an output file. Change-Id: I35675fdbc29728b39ca76fc411f656e6234623a5 --- openstack/image/_download.py | 85 +++++++++++++++++++++ openstack/image/v1/image.py | 64 +--------------- openstack/image/v2/image.py | 63 +-------------- openstack/tests/fakes.py | 5 +- openstack/tests/unit/cloud/test_image.py | 7 +- openstack/tests/unit/image/v2/test_image.py | 22 +++++- 6 files changed, 116 insertions(+), 130 deletions(-) create mode 100644 openstack/image/_download.py diff --git a/openstack/image/_download.py b/openstack/image/_download.py new file mode 100644 index 000000000..83130e8bb --- /dev/null +++ b/openstack/image/_download.py @@ -0,0 +1,85 @@ +# 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 io +import hashlib +import six + +from openstack import exceptions +from openstack import utils + + +def _verify_checksum(md5, checksum): + if checksum: + digest = md5.hexdigest() + if digest != checksum: + raise exceptions.InvalidResponse( + "checksum mismatch: %s != %s" % (checksum, digest)) + + +class DownloadMixin(object): + + def download(self, session, stream=False, output=None, chunk_size=1024): + """Download the data contained in an image""" + # TODO(briancurtin): This method should probably offload the get + # operation into another thread or something of that nature. + url = utils.urljoin(self.base_path, self.id, 'file') + resp = session.get(url, stream=stream) + + # See the following bug report for details on why the checksum + # code may sometimes depend on a second GET call. + # https://storyboard.openstack.org/#!/story/1619675 + checksum = resp.headers.get("Content-MD5") + + if checksum is None: + # If we don't receive the Content-MD5 header with the download, + # make an additional call to get the image details and look at + # the checksum attribute. + details = self.fetch(session) + checksum = details.checksum + + md5 = hashlib.md5() + if output: + try: + # In python 2 we might get StringIO - delete it as soon as + # py2 support is dropped + if isinstance(output, io.IOBase) \ + or isinstance(output, six.StringIO): + for chunk in resp.iter_content(chunk_size=chunk_size): + output.write(chunk) + md5.update(chunk) + else: + with open(output, 'wb') as fd: + for chunk in resp.iter_content( + chunk_size=chunk_size): + fd.write(chunk) + md5.update(chunk) + _verify_checksum(md5, checksum) + + return resp + except Exception as e: + raise exceptions.SDKException( + "Unable to download image: %s" % e) + # if we are returning the repsonse object, ensure that it + # has the content-md5 header so that the caller doesn't + # need to jump through the same hoops through which we + # just jumped. + if stream: + resp.headers['content-md5'] = checksum + return resp + + if checksum is not None: + _verify_checksum(hashlib.md5(resp.content), checksum) + else: + session.log.warn( + "Unable to verify the integrity of image %s", (self.id)) + + return resp diff --git a/openstack/image/v1/image.py b/openstack/image/v1/image.py index 6760ed51f..a92c4362d 100644 --- a/openstack/image/v1/image.py +++ b/openstack/image/v1/image.py @@ -9,16 +9,11 @@ # 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 hashlib -import io -import six - -from openstack import exceptions +from openstack.image import _download from openstack import resource -from openstack import utils -class Image(resource.Resource): +class Image(resource.Resource, _download.DownloadMixin): resource_key = 'image' resources_key = 'images' base_path = '/images' @@ -78,58 +73,3 @@ class Image(resource.Resource): status = resource.Body('status') #: The timestamp when this image was last updated. updated_at = resource.Body('updated_at') - - def download(self, session, stream=False, output=None, chunk_size=1024): - """Download the data contained in an image""" - # TODO(briancurtin): This method should probably offload the get - # operation into another thread or something of that nature. - url = utils.urljoin(self.base_path, self.id, 'file') - resp = session.get(url, stream=stream) - - # See the following bug report for details on why the checksum - # code may sometimes depend on a second GET call. - # https://storyboard.openstack.org/#!/story/1619675 - checksum = resp.headers.get("Content-MD5") - - if checksum is None: - # If we don't receive the Content-MD5 header with the download, - # make an additional call to get the image details and look at - # the checksum attribute. - details = self.fetch(session) - checksum = details.checksum - - if output: - try: - # In python 2 we might get StringIO - delete it as soon as - # py2 support is dropped - if isinstance(output, io.IOBase) \ - or isinstance(output, six.StringIO): - for chunk in resp.iter_content(chunk_size=chunk_size): - output.write(chunk) - else: - with open(output, 'wb') as fd: - for chunk in resp.iter_content( - chunk_size=chunk_size): - fd.write(chunk) - return resp - except Exception as e: - raise exceptions.SDKException( - "Unable to download image: %s" % e) - # if we are returning the repsonse object, ensure that it - # has the content-md5 header so that the caller doesn't - # need to jump through the same hoops through which we - # just jumped. - if stream: - resp.headers['content-md5'] = checksum - return resp - - if checksum is not None: - digest = hashlib.md5(resp.content).hexdigest() - if digest != checksum: - raise exceptions.InvalidResponse( - "checksum mismatch: %s != %s" % (checksum, digest)) - else: - session.log.warn( - "Unable to verify the integrity of image %s" % (self.id)) - - return resp diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index cc6a3236c..50af9e8cf 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -9,17 +9,13 @@ # 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 hashlib -import io -import six - from openstack import exceptions +from openstack.image import _download from openstack import resource from openstack import utils -class Image(resource.Resource, resource.TagMixin): +class Image(resource.Resource, resource.TagMixin, _download.DownloadMixin): resources_key = 'images' base_path = '/images' @@ -274,61 +270,6 @@ def import_image(self, session, method='glance-direct', uri=None): 'method: "web-download"') session.post(url, json=json) - def download(self, session, stream=False, output=None, chunk_size=1024): - """Download the data contained in an image""" - # TODO(briancurtin): This method should probably offload the get - # operation into another thread or something of that nature. - url = utils.urljoin(self.base_path, self.id, 'file') - resp = session.get(url, stream=stream) - - # See the following bug report for details on why the checksum - # code may sometimes depend on a second GET call. - # https://storyboard.openstack.org/#!/story/1619675 - checksum = resp.headers.get('Content-MD5') - - if checksum is None: - # If we don't receive the Content-MD5 header with the download, - # make an additional call to get the image details and look at - # the checksum attribute. - details = self.fetch(session) - checksum = details.checksum - - if output: - try: - # In python 2 we might get StringIO - delete it as soon as - # py2 support is dropped - if isinstance(output, io.IOBase) \ - or isinstance(output, six.StringIO): - for chunk in resp.iter_content(chunk_size=chunk_size): - output.write(chunk) - else: - with open(output, 'wb') as fd: - for chunk in resp.iter_content( - chunk_size=chunk_size): - fd.write(chunk) - return resp - except Exception as e: - raise exceptions.SDKException( - 'Unable to download image: %s' % e) - # if we are returning the repsonse object, ensure that it - # has the content-md5 header so that the caller doesn't - # need to jump through the same hoops through which we - # just jumped. - if stream: - resp.headers['content-md5'] = checksum - return resp - - if checksum is not None: - digest = hashlib.md5(resp.content).hexdigest() - if digest != checksum: - raise exceptions.InvalidResponse( - "checksum mismatch: %s != %s" % (checksum, digest)) - else: - session.log.warn( - "Unable to verify the integrity of image %s" % (self.id)) - - return resp - def _prepare_request(self, requires_id=None, prepend_key=False, patch=False, base_path=None): request = super(Image, self)._prepare_request(requires_id=requires_id, diff --git a/openstack/tests/fakes.py b/openstack/tests/fakes.py index 9055075b4..4479968c3 100644 --- a/openstack/tests/fakes.py +++ b/openstack/tests/fakes.py @@ -220,7 +220,8 @@ def make_fake_stack_event( def make_fake_image( image_id=None, md5=NO_MD5, sha256=NO_SHA256, status='active', - image_name=u'fake_image'): + image_name=u'fake_image', + checksum=u'ee36e35a297980dee1b514de9803ec6d'): return { u'image_state': u'available', u'container_format': u'bare', @@ -242,7 +243,7 @@ def make_fake_image( u'min_disk': 40, u'virtual_size': None, u'name': image_name, - u'checksum': u'ee36e35a297980dee1b514de9803ec6d', + u'checksum': checksum, u'created_at': u'2016-02-10T05:03:11Z', u'owner_specified.openstack.md5': NO_MD5, u'owner_specified.openstack.sha256': NO_SHA256, diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index f972f7603..c90a28f5c 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -11,7 +11,7 @@ # 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 hashlib import operator import tempfile import uuid @@ -37,10 +37,11 @@ def setUp(self): self.imagefile = tempfile.NamedTemporaryFile(delete=False) self.imagefile.write(b'\0') self.imagefile.close() + self.output = uuid.uuid4().bytes self.fake_image_dict = fakes.make_fake_image( - image_id=self.image_id, image_name=self.image_name) + image_id=self.image_id, image_name=self.image_name, + checksum=hashlib.md5(self.output).hexdigest()) self.fake_search_return = {'images': [self.fake_image_dict]} - self.output = uuid.uuid4().bytes self.container_name = self.getUniqueString('container') diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 281c89f4c..7c429e48d 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -9,7 +9,7 @@ # 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 hashlib import operator import six import tempfile @@ -88,6 +88,13 @@ } +def calculate_md5_checksum(data): + checksum = hashlib.md5() + for chunk in data: + checksum.update(chunk) + return checksum.hexdigest() + + class FakeResponse(object): def __init__(self, response, status_code=200, headers=None, reason=None): self.body = response @@ -336,8 +343,11 @@ def test_download_no_checksum_at_all2(self): self.assertEqual(len(log.records), 1, "Too many warnings were logged") self.assertEqual( - "Unable to verify the integrity of image IDENTIFIER", + "Unable to verify the integrity of image %s", log.records[0].msg) + self.assertEqual( + (sot.id,), + log.records[0].args) self.sess.get.assert_has_calls( [mock.call('images/IDENTIFIER/file', @@ -366,6 +376,10 @@ def test_image_download_output_fd(self): response = mock.Mock() response.status_code = 200 response.iter_content.return_value = [b'01', b'02'] + response.headers = { + 'Content-MD5': + calculate_md5_checksum(response.iter_content.return_value) + } self.sess.get = mock.Mock(return_value=response) sot.download(self.sess, output=output_file) output_file.seek(0) @@ -376,6 +390,10 @@ def test_image_download_output_file(self): response = mock.Mock() response.status_code = 200 response.iter_content.return_value = [b'01', b'02'] + response.headers = { + 'Content-MD5': + calculate_md5_checksum(response.iter_content.return_value) + } self.sess.get = mock.Mock(return_value=response) output_file = tempfile.NamedTemporaryFile() From 51393509d1a4b8e29907e0d153a656f8f746ca20 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Sat, 11 May 2019 09:45:03 +0200 Subject: [PATCH 2453/3836] Replace use of log.warn with log.warning replace use of deprecated methods Change-Id: I20dce5eb88306d8292baac9d803cbb6e8ac0402f --- openstack/cloud/_network.py | 2 +- openstack/image/_download.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index b6b3b05ba..e4422a2d3 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -2069,7 +2069,7 @@ def update_router(self, name_or_id, name=None, admin_state_up=None, if self._has_neutron_extension('extraroute'): router['routes'] = routes else: - self.log.warn( + self.log.warning( 'extra routes extension is not available on target cloud') if not router: diff --git a/openstack/image/_download.py b/openstack/image/_download.py index 83130e8bb..d9176d24b 100644 --- a/openstack/image/_download.py +++ b/openstack/image/_download.py @@ -79,7 +79,7 @@ def download(self, session, stream=False, output=None, chunk_size=1024): if checksum is not None: _verify_checksum(hashlib.md5(resp.content), checksum) else: - session.log.warn( + session.log.warning( "Unable to verify the integrity of image %s", (self.id)) return resp From 75b0f292f88fde7c6f4789a2af918143fc11f087 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 13 May 2019 20:15:07 +0200 Subject: [PATCH 2454/3836] Add support for vendor hooks Add possibility to pass a hook in the vendor config, clouds-public.* or upon building a connection. This should be a string parameter - function name to be executed. This gives possibility to register new services of the cloud automatically or alter behavior of the present services. It would have not been necessary, if public clouds followed upstream-first aproach. While we are here fix warnings on not closed files in the test_json Change-Id: Ifd6c0847102af4f46e361dcb1a665829c77553b9 --- openstack/__init__.py | 4 +- openstack/config/loader.py | 3 +- openstack/config/schema.json | 5 ++ openstack/config/vendor-schema.json | 5 ++ openstack/config/vendors/otc.json | 6 +- openstack/connection.py | 26 ++++++ openstack/tests/unit/config/test_json.py | 17 ++-- openstack/tests/unit/test_connection.py | 89 +++++++++++++++++++ .../add_vendor_hook-e87b6afb7f215a30.yaml | 8 ++ 9 files changed, 153 insertions(+), 10 deletions(-) create mode 100644 releasenotes/notes/add_vendor_hook-e87b6afb7f215a30.yaml diff --git a/openstack/__init__.py b/openstack/__init__.py index 0bf4d0f3a..e7db55965 100644 --- a/openstack/__init__.py +++ b/openstack/__init__.py @@ -57,4 +57,6 @@ def connect( load_yaml_config=load_yaml_config, load_envvars=load_envvars, options=options, **kwargs) - return openstack.connection.Connection(config=cloud_region) + return openstack.connection.Connection( + config=cloud_region, + vendor_hook=kwargs.get('vendor_hook')) diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 42f830fa1..4ec47a3f5 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -481,7 +481,8 @@ def _expand_vendor_profile(self, name, cloud, our_cloud): "'profile' keyword.".format(self.config_filename)) vendor_filename, vendor_file = self._load_vendor_file() - if vendor_file and profile_name in vendor_file['public-clouds']: + if (vendor_file and 'public-clouds' in vendor_file + and profile_name in vendor_file['public-clouds']): _auth_update(cloud, vendor_file['public-clouds'][profile_name]) else: profile_data = vendors.get_profile(profile_name) diff --git a/openstack/config/schema.json b/openstack/config/schema.json index 7ea7d050a..ff07f0d99 100644 --- a/openstack/config/schema.json +++ b/openstack/config/schema.json @@ -104,6 +104,11 @@ "description": "Volume API Version", "default": "2", "type": "string" + }, + "vendor_hook": { + "name": "Hook for vendor customization", + "description": "A possibility for a vendor to alter connection object", + "type": "string" } }, "required": [ diff --git a/openstack/config/vendor-schema.json b/openstack/config/vendor-schema.json index ba671023a..be9ce5e6e 100644 --- a/openstack/config/vendor-schema.json +++ b/openstack/config/vendor-schema.json @@ -217,6 +217,11 @@ "name": "Baremetal API Version", "description": "Baremetal API Version", "type": "string" + }, + "vendor_hook": { + "name": "Hook for vendor customization", + "description": "A possibility for a vendor to alter connection object", + "type": "string" } } } diff --git a/openstack/config/vendors/otc.json b/openstack/config/vendors/otc.json index b0c1b116f..223d2892a 100644 --- a/openstack/config/vendors/otc.json +++ b/openstack/config/vendors/otc.json @@ -2,12 +2,14 @@ "name": "otc", "profile": { "auth": { - "auth_url": "https://iam.%(region_name)s.otc.t-systems.com/v3" + "auth_url": "https://iam.{region_name}.otc.t-systems.com/v3" }, "regions": [ "eu-de" ], "identity_api_version": "3", - "image_format": "vhd" + "interface": "public", + "image_format": "vhd", + "vendor_hook": "otcextensions.sdk:load" } } diff --git a/openstack/connection.py b/openstack/connection.py index 397c6db32..8a2d0b5a0 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -345,6 +345,32 @@ def __init__(self, cloud=None, config=None, session=None, _orchestration.OrchestrationCloudMixin.__init__(self) _security_group.SecurityGroupCloudMixin.__init__(self) + # Allow vendors to provide hooks. They will normally only receive a + # connection object and a responsible to register additional services + vendor_hook = kwargs.get('vendor_hook') + if not vendor_hook and 'vendor_hook' in self.config.config: + # Get the one from profile + vendor_hook = self.config.config.get('vendor_hook') + if vendor_hook: + try: + # NOTE(gtema): no class name in the hook, plain module:function + # Split string hook into module and function + try: + (package_name, function) = vendor_hook.rsplit(':') + + if package_name and function: + import pkg_resources + ep = pkg_resources.EntryPoint( + 'vendor_hook', package_name, attrs=(function,)) + hook = ep.resolve() + hook(self) + except ValueError: + self.log.warning('Hook should be in the entrypoint ' + 'module:attribute format') + except (ImportError, TypeError) as e: + self.log.warning('Configured hook %s cannot be executed: %s', + vendor_hook, e) + @property def session(self): if not self._session: diff --git a/openstack/tests/unit/config/test_json.py b/openstack/tests/unit/config/test_json.py index d41c509ca..7a43341e1 100644 --- a/openstack/tests/unit/config/test_json.py +++ b/openstack/tests/unit/config/test_json.py @@ -34,14 +34,16 @@ def test_defaults_valid_json(self): _schema_path = os.path.join( os.path.dirname(os.path.realpath(defaults.__file__)), 'schema.json') - schema = json.load(open(_schema_path, 'r')) + with open(_schema_path, 'r') as f: + schema = json.load(f) self.validator = jsonschema.Draft4Validator(schema) self.addOnException(self.json_diagnostics) self.filename = os.path.join( os.path.dirname(os.path.realpath(defaults.__file__)), 'defaults.json') - self.json_data = json.load(open(self.filename, 'r')) + with open(self.filename, 'r') as f: + self.json_data = json.load(f) self.assertTrue(self.validator.is_valid(self.json_data)) @@ -49,14 +51,17 @@ def test_vendors_valid_json(self): _schema_path = os.path.join( os.path.dirname(os.path.realpath(defaults.__file__)), 'vendor-schema.json') - schema = json.load(open(_schema_path, 'r')) - self.validator = jsonschema.Draft4Validator(schema) + with open(_schema_path, 'r') as f: + schema = json.load(f) + self.validator = jsonschema.Draft4Validator(schema) + self.addOnException(self.json_diagnostics) _vendors_path = os.path.join( os.path.dirname(os.path.realpath(defaults.__file__)), 'vendors') for self.filename in glob.glob(os.path.join(_vendors_path, '*.json')): - self.json_data = json.load(open(self.filename, 'r')) + with open(self.filename, 'r') as f: + self.json_data = json.load(f) - self.assertTrue(self.validator.is_valid(self.json_data)) + self.assertTrue(self.validator.is_valid(self.json_data)) diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index ef4117f96..69326d1d1 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -59,10 +59,37 @@ password: {password} project_name: {project} cacert: {cacert} + profiled-cloud: + profile: dummy + auth: + username: {username} + password: {password} + project_name: {project} + cacert: {cacert} """.format(auth_url=CONFIG_AUTH_URL, username=CONFIG_USERNAME, password=CONFIG_PASSWORD, project=CONFIG_PROJECT, cacert=CONFIG_CACERT) +VENDOR_CONFIG = """ +{{ + "name": "dummy", + "profile": {{ + "auth": {{ + "auth_url": "{auth_url}" + }}, + "vendor_hook": "openstack.tests.unit.test_connection:vendor_hook" + }} +}} +""".format(auth_url=CONFIG_AUTH_URL) + +PUBLIC_CLOUDS_YAML = """ +public-clouds: + dummy: + auth: + auth_url: {auth_url} + vendor_hook: openstack.tests.unit.test_connection:vendor_hook +""".format(auth_url=CONFIG_AUTH_URL) + class TestConnection(base.TestCase): @@ -334,3 +361,65 @@ def test_replace_system_service(self): # ensure dns service responds as we expect from replacement self.assertFalse(conn.dns.dummy()) + + +def vendor_hook(conn): + setattr(conn, 'test', 'test_val') + + +class TestVendorProfile(base.TestCase): + + def setUp(self): + super(TestVendorProfile, self).setUp() + # Create a temporary directory where our test config will live + # and insert it into the search path via OS_CLIENT_CONFIG_FILE. + config_dir = self.useFixture(fixtures.TempDir()).path + config_path = os.path.join(config_dir, "clouds.yaml") + public_clouds = os.path.join(config_dir, "clouds-public.yaml") + + with open(config_path, "w") as conf: + conf.write(CLOUD_CONFIG) + + with open(public_clouds, "w") as conf: + conf.write(PUBLIC_CLOUDS_YAML) + + self.useFixture(fixtures.EnvironmentVariable( + "OS_CLIENT_CONFIG_FILE", config_path)) + self.use_keystone_v2() + + self.config = openstack.config.loader.OpenStackConfig( + vendor_files=[public_clouds]) + + def test_conn_from_profile(self): + + self.cloud = self.config.get_one(cloud='profiled-cloud') + + conn = connection.Connection(config=self.cloud) + + self.assertIsNotNone(conn) + + def test_hook_from_profile(self): + + self.cloud = self.config.get_one(cloud='profiled-cloud') + + conn = connection.Connection(config=self.cloud) + + self.assertEqual('test_val', conn.test) + + def test_hook_from_connection_param(self): + + conn = connection.Connection( + cloud='sample-cloud', + vendor_hook='openstack.tests.unit.test_connection:vendor_hook' + ) + + self.assertEqual('test_val', conn.test) + + def test_hook_from_connection_ignore_missing(self): + + conn = connection.Connection( + cloud='sample-cloud', + vendor_hook='openstack.tests.unit.test_connection:missing' + ) + + self.assertIsNotNone(conn) diff --git a/releasenotes/notes/add_vendor_hook-e87b6afb7f215a30.yaml b/releasenotes/notes/add_vendor_hook-e87b6afb7f215a30.yaml new file mode 100644 index 000000000..1ef8c2b85 --- /dev/null +++ b/releasenotes/notes/add_vendor_hook-e87b6afb7f215a30.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Add possibility to automatically invoke vendor hooks. This can be done + either through extending profile (vendor_hook), or passing `vendor_hook` + parameter to the connection. The format of the vendor_hook is the same as + in the setuptools (module.name:function_name). The hook will get connection + as the only parameter. From cc51e34cf1f60c11f4f938efda62e33ae89d75d8 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 16 Apr 2019 17:42:50 +0200 Subject: [PATCH 2455/3836] Add image.stage methods Add support for staging (and completing the import) image data. Change-Id: Id865c7ffe5fff5d723074c22d0fd01d817ae932d --- doc/source/user/proxies/image_v2.rst | 1 + openstack/image/v2/_proxy.py | 30 +++++++++++++++++ openstack/image/v2/image.py | 12 +++++++ openstack/tests/unit/image/v2/test_image.py | 30 +++++++++++++---- openstack/tests/unit/image/v2/test_proxy.py | 32 +++++++++++++++++++ .../add-image-stage-1dbc3844a042fd26.yaml | 4 +++ 6 files changed, 103 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/add-image-stage-1dbc3844a042fd26.yaml diff --git a/doc/source/user/proxies/image_v2.rst b/doc/source/user/proxies/image_v2.rst index 97c3cc65d..295e72cf4 100644 --- a/doc/source/user/proxies/image_v2.rst +++ b/doc/source/user/proxies/image_v2.rst @@ -28,6 +28,7 @@ Image Operations .. automethod:: openstack.image.v2._proxy.Proxy.images .. automethod:: openstack.image.v2._proxy.Proxy.deactivate_image .. automethod:: openstack.image.v2._proxy.Proxy.reactivate_image + .. automethod:: openstack.image.v2._proxy.Proxy.stage_image .. automethod:: openstack.image.v2._proxy.Proxy.add_tag .. automethod:: openstack.image.v2._proxy.Proxy.remove_tag diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 04965b372..c033d0dc2 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -64,6 +64,36 @@ def import_image(self, image, method='glance-direct', uri=None): image.import_image(self, method=method, uri=uri) + def stage_image(self, image, filename=None, data=None): + """Stage binary image data + + :param image: The value can be the ID of a image or a + :class:`~openstack.image.v2.image.Image` instance. + :param filename: Optional name of the file to read data from. + :param data: Optional data to be uploaded as an image. + + :returns: The results of image creation + :rtype: :class:`~openstack.image.v2.image.Image` + """ + image = self._get_resource(_image.Image, image) + + if 'queued' != image.status: + raise exceptions.SDKException('Image stage is only possible for ' + 'images in the queued state.' + ' Current state is {status}' + .format(status=image.status)) + + if filename: + image.data = open(filename, 'rb') + elif data: + image.data = data + image.stage(self) + + # Stage does not return content, but updates the object + image.fetch(self) + + return image + def upload_image(self, container_format=None, disk_format=None, data=None, **attrs): """Create and upload a new image from attributes diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index cc6a3236c..84d4b79aa 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -262,6 +262,16 @@ def upload(self, session): headers={"Content-Type": "application/octet-stream", "Accept": ""}) + def stage(self, session): + """Stage binary image data into an existing image""" + url = utils.urljoin(self.base_path, self.id, 'stage') + response = session.put( + url, data=self.data, + headers={"Content-Type": "application/octet-stream", + "Accept": ""}) + self._translate_response(response, has_body=False) + return self + def import_image(self, session, method='glance-direct', uri=None): """Import Image via interoperable image import process""" url = utils.urljoin(self.base_path, self.id, 'import') @@ -269,6 +279,8 @@ def import_image(self, session, method='glance-direct', uri=None): if uri: if method == 'web-download': json['method']['uri'] = uri + elif method == 'glance-direct': + json['method']['uri'] = uri else: raise exceptions.InvalidRequest('URI is only supported with ' 'method: "web-download"') diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 281c89f4c..9e2c91040 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -92,6 +92,7 @@ class FakeResponse(object): def __init__(self, response, status_code=200, headers=None, reason=None): self.body = response self.content = response + self.text = response self.status_code = status_code headers = headers if headers else {'content-type': 'application/json'} self.headers = requests.structures.CaseInsensitiveDict(headers) @@ -115,7 +116,7 @@ def setUp(self): self.sess.post = mock.Mock(return_value=self.resp) self.sess.put = mock.Mock(return_value=FakeResponse({})) self.sess.delete = mock.Mock(return_value=FakeResponse({})) - self.sess.fetch = mock.Mock(return_value=FakeResponse({})) + self.sess.get = mock.Mock(return_value=FakeResponse({})) self.sess.default_microversion = None self.sess.retriable_status_codes = None self.sess.log = _log.setup_logging('openstack') @@ -259,11 +260,12 @@ def test_import_image(self): def test_import_image_with_uri_not_web_download(self): sot = image.Image(**EXAMPLE) - self.assertRaises(exceptions.InvalidRequest, - sot.import_image, - self.sess, - "glance-direct", - "such-a-good-uri") + + sot.import_image(self.sess, "glance-direct") + self.sess.post.assert_called_with( + 'images/IDENTIFIER/import', + json={"method": {"name": "glance-direct"}} + ) def test_upload(self): sot = image.Image(**EXAMPLE) @@ -275,6 +277,22 @@ def test_upload(self): "application/octet-stream", "Accept": ""}) + def test_stage(self): + sot = image.Image(**EXAMPLE) + + self.assertIsNotNone(sot.stage(self.sess)) + self.sess.put.assert_called_with('images/IDENTIFIER/stage', + data=sot.data, + headers={"Content-Type": + "application/octet-stream", + "Accept": ""}) + + def test_stage_error(self): + sot = image.Image(**EXAMPLE) + + self.sess.put.return_value = FakeResponse("dummy", status_code=400) + self.assertRaises(exceptions.SDKException, sot.stage, self.sess) + def test_download_checksum_match(self): sot = image.Image(**EXAMPLE) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 617d55f70..025e7069f 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -94,6 +94,38 @@ def test_image_download(self): 'chunk_size': 1, 'stream': True}) + @mock.patch("openstack.image.v2.image.Image.fetch") + def test_image_stage(self, mock_fetch): + img = image.Image(id="id", status="queued") + img.stage = mock.Mock() + + self.proxy.stage_image(image=img) + mock_fetch.assert_called() + img.stage.assert_called_with(self.proxy) + + @mock.patch("openstack.image.v2.image.Image.fetch") + def test_image_stage_with_data(self, mock_fetch): + img = image.Image(id="id", status="queued") + img.stage = mock.Mock() + mock_fetch.return_value = img + + rv = self.proxy.stage_image(image=img, data="data") + + img.stage.assert_called_with(self.proxy) + mock_fetch.assert_called() + self.assertEqual(rv.data, "data") + + def test_image_stage_wrong_status(self): + img = image.Image(id="id", status="active") + img.stage = mock.Mock() + + self.assertRaises( + exceptions.SDKException, + self.proxy.stage_image, + img, + "data" + ) + def test_image_delete(self): self.verify_delete(self.proxy.delete_image, image.Image, False) diff --git a/releasenotes/notes/add-image-stage-1dbc3844a042fd26.yaml b/releasenotes/notes/add-image-stage-1dbc3844a042fd26.yaml new file mode 100644 index 000000000..bde7506df --- /dev/null +++ b/releasenotes/notes/add-image-stage-1dbc3844a042fd26.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add support for staging image data. From e47d2561e4fe64f0ae92ac027f51da2ffd7636f3 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Tue, 14 May 2019 08:50:16 -0500 Subject: [PATCH 2456/3836] Link to baremetal API reference from patch_node Add a link in the patch_node docstring to the corresponding baremetal API operation, Update Node (PATCH /v1/nodes/{node_ident}). Change-Id: Id13b1d148c7200ce78787181aa3f376372f21df6 --- openstack/baremetal/v1/_proxy.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index ee655a055..e2e6c6dfc 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -280,6 +280,10 @@ def patch_node(self, node, patch, retry_on_conflict=True): being locked. However, when setting ``instance_id``, this is a normal code and should not be retried. + See `Update Node + `_ + for details. + :returns: The updated node. :rtype: :class:`~openstack.baremetal.v1.node.Node` """ From 5d949c7bf606c2dc2c2da71579f9e31b4f24267e Mon Sep 17 00:00:00 2001 From: zhangboye Date: Thu, 16 May 2019 13:55:53 +0800 Subject: [PATCH 2457/3836] Cap sphinx for py2 to match global requirements Change-Id: I1757faac7ba6993af3557a2e678ee0719395d134 --- doc/requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index d3fbf2210..d3f782bbd 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,7 +1,8 @@ # 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. -sphinx!=1.6.6,!=1.6.7,>=1.6.2 # BSD +sphinx!=1.6.6,!=1.6.7,>=1.6.2,<2.0.0;python_version=='2.7' # BSD +sphinx!=1.6.6,!=1.6.7,>=1.6.2;python_version>='3.4' # BSD docutils>=0.11 # OSI-Approved Open Source, Public Domain openstackdocstheme>=1.18.1 # Apache-2.0 beautifulsoup4>=4.6.0 # MIT From a3e846e2b9301f4ae725e97b21b4313bfcc81d10 Mon Sep 17 00:00:00 2001 From: Raimund Hook Date: Wed, 15 May 2019 20:21:53 +0100 Subject: [PATCH 2458/3836] Adding dns_domain parameter into create_network The Network class supports the dns_domain field. This change adds in the ability to create & update networks with the dns_domain set. Change-Id: Ifa072138b616d2a12696b27e10556f92542c05cb --- openstack/cloud/_network.py | 12 ++++++++++-- openstack/tests/unit/cloud/test_network.py | 3 ++- .../notes/dns-domain-parameter-d3acfc3287a9d632.yaml | 5 +++++ 3 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/dns-domain-parameter-d3acfc3287a9d632.yaml diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index e4422a2d3..7b0d28bbd 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -448,7 +448,7 @@ def create_network(self, name, shared=False, admin_state_up=True, external=False, provider=None, project_id=None, availability_zone_hints=None, port_security_enabled=None, - mtu_size=None): + mtu_size=None, dns_domain=None): """Create a network. :param string name: Name of the network being created. @@ -465,6 +465,8 @@ def create_network(self, name, shared=False, admin_state_up=True, :param bool port_security_enabled: Enable / Disable port security :param int mtu_size: maximum transmission unit value to address fragmentation. Minimum value is 68 for IPv4, and 1280 for IPv6. + :param string dns_domain: Specify the DNS domain associated with + this network. :returns: The network object. :raises: OpenStackCloudException on operation error. @@ -523,6 +525,9 @@ def create_network(self, name, shared=False, admin_state_up=True, network['mtu'] = mtu_size + if dns_domain: + network['dns_domain'] = dns_domain + data = self.network.post("/networks.json", json={'network': network}) # Reset cache so the new network is picked up @@ -530,7 +535,8 @@ def create_network(self, name, shared=False, admin_state_up=True, return self._get_and_munchify('network', data) @_utils.valid_kwargs("name", "shared", "admin_state_up", "external", - "provider", "mtu_size", "port_security_enabled") + "provider", "mtu_size", "port_security_enabled", + "dns_domain") def update_network(self, name_or_id, **kwargs): """Update a network. @@ -545,6 +551,8 @@ def update_network(self, name_or_id, **kwargs): :param int mtu_size: New maximum transmission unit value to address fragmentation. Minimum value is 68 for IPv4, and 1280 for IPv6. :param bool port_security_enabled: Enable or disable port security. + :param string dns_domain: Specify the DNS domain associated with + this network. :returns: The updated network object. :raises: OpenStackCloudException on operation error. diff --git a/openstack/tests/unit/cloud/test_network.py b/openstack/tests/unit/cloud/test_network.py index ca0b6073c..318d3f2e4 100644 --- a/openstack/tests/unit/cloud/test_network.py +++ b/openstack/tests/unit/cloud/test_network.py @@ -45,7 +45,8 @@ class TestNetwork(base.TestCase): 'admin_state_up': True, 'tenant_id': '861808a93da0484ea1767967c4df8a23', 'created_at': '2017-04-22T19:22:53Z', - 'mtu': 0 + 'mtu': 0, + 'dns_domain': 'sample.openstack.org.' } network_availability_zone_extension = { diff --git a/releasenotes/notes/dns-domain-parameter-d3acfc3287a9d632.yaml b/releasenotes/notes/dns-domain-parameter-d3acfc3287a9d632.yaml new file mode 100644 index 000000000..605cae2a5 --- /dev/null +++ b/releasenotes/notes/dns-domain-parameter-d3acfc3287a9d632.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added dns_domain parameter into the create_network and update_network + methods. From ac7fbea1a4d2a04ade90d8bf124e48735ce5a873 Mon Sep 17 00:00:00 2001 From: Dmitriy Rabotjagov Date: Wed, 22 May 2019 14:01:38 +0300 Subject: [PATCH 2459/3836] Add ability to provide qos_policy_id for port Neutron API supports qos_policy defenition during port creation and port object already contains qos_policy_id[1] So this patch extends port_update method with the ability to provide qos_policy_id in the addition to existing parameters. [1] https://opendev.org/openstack/openstacksdk/src/branch/master/openstack/network/v2/port.py#L109 Change-Id: Iad837fababa8d9d0c604637d326b1b1253f13c05 --- openstack/cloud/_network.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index 7b0d28bbd..e0f938b77 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -2383,7 +2383,8 @@ def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, 'subnet_id', 'ip_address', 'security_groups', 'allowed_address_pairs', 'extra_dhcp_opts', 'device_owner', 'device_id', 'binding:vnic_type', - 'binding:profile', 'port_security_enabled') + 'binding:profile', 'port_security_enabled', + 'qos_policy_id') def create_port(self, network_id, **kwargs): """Create a port @@ -2435,6 +2436,7 @@ def create_port(self, network_id, **kwargs): :param binding vnic_type: The type of the created port. (Optional) :param port_security_enabled: The security port state created on the network. (Optional) + :param qos_policy_id: The ID of the QoS policy to apply for port. :returns: a ``munch.Munch`` describing the created port. @@ -2452,7 +2454,7 @@ def create_port(self, network_id, **kwargs): 'security_groups', 'allowed_address_pairs', 'extra_dhcp_opts', 'device_owner', 'device_id', 'binding:vnic_type', 'binding:profile', - 'port_security_enabled') + 'port_security_enabled', 'qos_policy_id') def update_port(self, name_or_id, **kwargs): """Update a port @@ -2501,6 +2503,7 @@ def update_port(self, name_or_id, **kwargs): :param binding vnic_type: The type of the created port. (Optional) :param port_security_enabled: The security port state created on the network. (Optional) + :param qos_policy_id: The ID of the QoS policy to apply for port. :returns: a ``munch.Munch`` describing the updated port. From e4205085b335aa2c37e60cbfc476fe429f2ff062 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Wed, 22 May 2019 08:50:24 +0200 Subject: [PATCH 2460/3836] baremetal: allow updating name and extra fields of an allocation Support for this was introduced in Bare Metal API 1.57 (Train). Change-Id: I4885d9a710cd6697c241bb1962dbbe2741a049a1 Story: #2005126 Task: #29798 --- doc/source/user/proxies/baremetal.rst | 2 + openstack/baremetal/v1/_proxy.py | 26 +++++++ openstack/baremetal/v1/allocation.py | 9 ++- .../baremetal/test_baremetal_allocation.py | 73 ++++++++++++++++++- .../unit/baremetal/v1/test_allocation.py | 2 +- .../allocation-update-910c36c1290e5121.yaml | 4 + 6 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/allocation-update-910c36c1290e5121.yaml diff --git a/doc/source/user/proxies/baremetal.rst b/doc/source/user/proxies/baremetal.rst index 7c740d92c..0eb6012b7 100644 --- a/doc/source/user/proxies/baremetal.rst +++ b/doc/source/user/proxies/baremetal.rst @@ -87,6 +87,8 @@ Allocation Operations .. autoclass:: openstack.baremetal.v1._proxy.Proxy .. automethod:: openstack.baremetal.v1._proxy.Proxy.create_allocation + .. automethod:: openstack.baremetal.v1._proxy.Proxy.update_allocation + .. automethod:: openstack.baremetal.v1._proxy.Proxy.patch_allocation .. automethod:: openstack.baremetal.v1._proxy.Proxy.delete_allocation .. automethod:: openstack.baremetal.v1._proxy.Proxy.get_allocation .. automethod:: openstack.baremetal.v1._proxy.Proxy.allocations diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index e2e6c6dfc..842fd6241 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -838,6 +838,32 @@ def get_allocation(self, allocation): """ return self._get(_allocation.Allocation, allocation) + def update_allocation(self, allocation, **attrs): + """Update an allocation. + + :param allocation: The value can be the name or ID of an allocation or + a :class:`~openstack.baremetal.v1.allocation.Allocation` instance. + :param dict attrs: The attributes to update on the allocation + represented by the ``allocation`` parameter. + + :returns: The updated allocation. + :rtype: :class:`~openstack.baremetal.v1.allocation.Allocation` + """ + return self._update(_allocation.Allocation, allocation, **attrs) + + def patch_allocation(self, allocation, patch): + """Apply a JSON patch to the allocation. + + :param allocation: The value can be the name or ID of an allocation or + a :class:`~openstack.baremetal.v1.allocation.Allocation` instance. + :param patch: JSON patch to apply. + + :returns: The updated allocation. + :rtype: :class:`~openstack.baremetal.v1.allocation.Allocation` + """ + return self._get_resource(_allocation.Allocation, + allocation).patch(self, patch) + def delete_allocation(self, allocation, ignore_missing=True): """Delete an allocation. diff --git a/openstack/baremetal/v1/allocation.py b/openstack/baremetal/v1/allocation.py index f7a450c2b..c4cf48762 100644 --- a/openstack/baremetal/v1/allocation.py +++ b/openstack/baremetal/v1/allocation.py @@ -24,17 +24,20 @@ class Allocation(_common.ListMixin, resource.Resource): # capabilities allow_create = True allow_fetch = True - allow_commit = False + allow_commit = True allow_delete = True allow_list = True + allow_patch = True + commit_method = 'PATCH' + commit_jsonpatch = True _query_mapping = resource.QueryParameters( 'node', 'resource_class', 'state', fields={'name': 'fields', 'type': _common.comma_separated_list}, ) - # The allocation API introduced in 1.52. - _max_microversion = '1.52' + # Allocation update is available since 1.57 + _max_microversion = '1.57' #: The candidate nodes for this allocation. candidate_nodes = resource.Body('candidate_nodes', type=list) diff --git a/openstack/tests/functional/baremetal/test_baremetal_allocation.py b/openstack/tests/functional/baremetal/test_baremetal_allocation.py index 1f36985bc..9ffc52560 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_allocation.py +++ b/openstack/tests/functional/baremetal/test_baremetal_allocation.py @@ -16,12 +16,10 @@ from openstack.tests.functional.baremetal import base -class TestBareMetalAllocation(base.BaseBaremetalTest): - - min_microversion = '1.52' +class Base(base.BaseBaremetalTest): def setUp(self): - super(TestBareMetalAllocation, self).setUp() + super(Base, self).setUp() # NOTE(dtantsur): generate a unique resource class to prevent parallel # tests from clashing. self.resource_class = 'baremetal-%d' % random.randrange(1024) @@ -40,6 +38,11 @@ def _create_available_node(self): instance_id=None)) return node + +class TestBareMetalAllocation(Base): + + min_microversion = '1.52' + def test_allocation_create_get_delete(self): allocation = self.create_allocation(resource_class=self.resource_class) self.assertEqual('allocating', allocation.state) @@ -108,3 +111,65 @@ def test_allocation_fields(self): for item in result: self.assertIsNotNone(item.id) self.assertIsNone(item.resource_class) + + +class TestBareMetalAllocationUpdate(Base): + + min_microversion = '1.57' + + def test_allocation_update(self): + name = 'ossdk-name1' + + allocation = self.create_allocation(resource_class=self.resource_class) + allocation = self.conn.baremetal.wait_for_allocation(allocation) + self.assertEqual('active', allocation.state) + self.assertIsNone(allocation.last_error) + self.assertIsNone(allocation.name) + self.assertEqual({}, allocation.extra) + + allocation = self.conn.baremetal.update_allocation( + allocation, name=name, extra={'answer': 42}) + self.assertEqual(name, allocation.name) + self.assertEqual({'answer': 42}, allocation.extra) + + allocation = self.conn.baremetal.get_allocation(name) + self.assertEqual(name, allocation.name) + self.assertEqual({'answer': 42}, allocation.extra) + + self.conn.baremetal.delete_allocation(allocation, ignore_missing=False) + self.assertRaises(exceptions.ResourceNotFound, + self.conn.baremetal.get_allocation, allocation.id) + + def test_allocation_patch(self): + name = 'ossdk-name2' + + allocation = self.create_allocation(resource_class=self.resource_class) + allocation = self.conn.baremetal.wait_for_allocation(allocation) + self.assertEqual('active', allocation.state) + self.assertIsNone(allocation.last_error) + self.assertIsNone(allocation.name) + self.assertEqual({}, allocation.extra) + + allocation = self.conn.baremetal.patch_allocation( + allocation, [{'op': 'replace', 'path': '/name', 'value': name}, + {'op': 'add', 'path': '/extra/answer', 'value': 42}]) + self.assertEqual(name, allocation.name) + self.assertEqual({'answer': 42}, allocation.extra) + + allocation = self.conn.baremetal.get_allocation(name) + self.assertEqual(name, allocation.name) + self.assertEqual({'answer': 42}, allocation.extra) + + allocation = self.conn.baremetal.patch_allocation( + allocation, [{'op': 'remove', 'path': '/name'}, + {'op': 'remove', 'path': '/extra/answer'}]) + self.assertIsNone(allocation.name) + self.assertEqual({}, allocation.extra) + + allocation = self.conn.baremetal.get_allocation(allocation.id) + self.assertIsNone(allocation.name) + self.assertEqual({}, allocation.extra) + + self.conn.baremetal.delete_allocation(allocation, ignore_missing=False) + self.assertRaises(exceptions.ResourceNotFound, + self.conn.baremetal.get_allocation, allocation.id) diff --git a/openstack/tests/unit/baremetal/v1/test_allocation.py b/openstack/tests/unit/baremetal/v1/test_allocation.py index c508cca68..2146eed3d 100644 --- a/openstack/tests/unit/baremetal/v1/test_allocation.py +++ b/openstack/tests/unit/baremetal/v1/test_allocation.py @@ -51,7 +51,7 @@ def test_basic(self): self.assertEqual('/allocations', sot.base_path) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) - self.assertFalse(sot.allow_commit) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/releasenotes/notes/allocation-update-910c36c1290e5121.yaml b/releasenotes/notes/allocation-update-910c36c1290e5121.yaml new file mode 100644 index 000000000..6b9147a43 --- /dev/null +++ b/releasenotes/notes/allocation-update-910c36c1290e5121.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Allows updating ``name`` and ``extra`` fields of a baremetal allocation. From 9db14f6d78e021c8c78c81b8811284c6534c61ff Mon Sep 17 00:00:00 2001 From: Logan V Date: Thu, 23 May 2019 12:42:00 -0500 Subject: [PATCH 2461/3836] Update Limestone Networks vendor config - Change the config to yaml - Set the networks on a per-region basis since the "DDoS Protected" network is not yet available in the us-slc region, only us-dfw-1. Change-Id: Ie231f041accb83097e7585355f4174a57d74212e --- .../config/vendors/limestonenetworks.json | 36 ------------------- .../config/vendors/limestonenetworks.yaml | 36 +++++++++++++++++++ 2 files changed, 36 insertions(+), 36 deletions(-) delete mode 100644 openstack/config/vendors/limestonenetworks.json create mode 100644 openstack/config/vendors/limestonenetworks.yaml diff --git a/openstack/config/vendors/limestonenetworks.json b/openstack/config/vendors/limestonenetworks.json deleted file mode 100644 index dcef5ae19..000000000 --- a/openstack/config/vendors/limestonenetworks.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "limestonenetworks", - "profile": { - "auth": { - "auth_url": "https://auth.cloud.lstn.net:5000/v3" - }, - "regions": [ - "us-dfw-1", - "us-slc" - ], - "identity_api_version": "3", - "image_format": "raw", - "volume_api_version": "3", - "networks": [ - { - "name": "Public Internet", - "routes_externally": true, - "default_interface": true, - "nat_source": true - }, - { - "name": "DDoS Protected", - "routes_externally": true - }, - { - "name": "Private Network (10.0.0.0/8 only)", - "routes_externally": false - }, - { - "name": "Private Network (Floating Public)", - "routes_externally": false, - "nat_destination": true - } - ] - } - } diff --git a/openstack/config/vendors/limestonenetworks.yaml b/openstack/config/vendors/limestonenetworks.yaml new file mode 100644 index 000000000..d5b359fe5 --- /dev/null +++ b/openstack/config/vendors/limestonenetworks.yaml @@ -0,0 +1,36 @@ +--- + +name: limestonenetworks +profile: + auth: + auth_url: https://auth.cloud.lstn.net:5000/v3 + regions: + - name: us-dfw-1 + values: + networks: + - name: Public Internet + routes_externally: true + default_interface: true + nat_source: true + - name: DDoS Protected + routes_externally: true + - name: Private Network (10.0.0.0/8 only) + routes_externally: false + - name: Private Network (Floating Public) + routes_externally: false + nat_destination: true + - name: us-slc + values: + networks: + - name: Public Internet + routes_externally: true + default_interface: true + nat_source: true + - name: Private Network (10.0.0.0/8 only) + routes_externally: false + - name: Private Network (Floating Public) + routes_externally: false + nat_destination: true + identity_api_version: '3' + image_format: raw + volume_api_version: '3' From 38847204f939956cbef95fa77d605334bffff734 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 3 Jun 2019 09:45:29 +0200 Subject: [PATCH 2462/3836] Support skipping unknown QP In the cloud layer we receive multiple filters. We would like to pass as much filters as possible to the server to reduce response size. For this we need to know which QPs are supported by the given resource. Add support for passing everything given to the Resource.list function and silently skipping unknown parameters. This is not going to be a default behavior, but primarily used from cloud layer. Change-Id: I53854ddcb86b92acdb64fabeb1de4ea4907c4a21 --- openstack/resource.py | 50 ++++++++++++++++++--------- openstack/tests/unit/test_resource.py | 27 +++++++++++++++ 2 files changed, 61 insertions(+), 16 deletions(-) diff --git a/openstack/resource.py b/openstack/resource.py index b325e9054..538997fe2 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -287,13 +287,17 @@ def __init__(self, *names, **mappings): self._mapping.update({name: name for name in names}) self._mapping.update(mappings) - def _validate(self, query, base_path=None): + def _validate(self, query, base_path=None, allow_unknown_params=False): """Check that supplied query keys match known query mappings :param dict query: Collection of key-value pairs where each key is the - client-side parameter name or server side name. + client-side parameter name or server side name. :param base_path: Formatted python string of the base url path for - the resource. + the resource. + : param allow_unknown_params: Exclude query params not known by the + resource. + + :returns: Filtered collection of the supported QueryParameters """ expected_params = list(self._mapping.keys()) expected_params.extend( @@ -304,10 +308,18 @@ def _validate(self, query, base_path=None): expected_params += utils.get_string_format_keys(base_path) invalid_keys = set(query.keys()) - set(expected_params) - if invalid_keys: - raise exceptions.InvalidResourceQuery( - message="Invalid query params: %s" % ",".join(invalid_keys), - extra_data=invalid_keys) + if not invalid_keys: + return query + else: + if not allow_unknown_params: + raise exceptions.InvalidResourceQuery( + message="Invalid query params: %s" % + ",".join(invalid_keys), + extra_data=invalid_keys) + else: + known_keys = set(query.keys()).intersection( + set(expected_params)) + return {k: query[k] for k in known_keys} def _transpose(self, query): """Transpose the keys in query based on the mapping @@ -1456,7 +1468,8 @@ def _raw_delete(self, session): microversion=microversion) @classmethod - def list(cls, session, paginated=True, base_path=None, **params): + def list(cls, session, paginated=True, base_path=None, + allow_unknown_params=False, **params): """This method is a generator which yields resource objects. This resource object list generator handles pagination and takes query @@ -1465,14 +1478,17 @@ def list(cls, session, paginated=True, base_path=None, **params): :param session: The session to use for making this request. :type session: :class:`~keystoneauth1.adapter.Adapter` :param bool paginated: ``True`` if a GET to this resource returns - a paginated series of responses, or ``False`` - if a GET returns only one page of data. - **When paginated is False only one - page of data will be returned regardless - of the API's support of pagination.** + a paginated series of responses, or ``False`` + if a GET returns only one page of data. + **When paginated is False only one + page of data will be returned regardless + of the API's support of pagination.** :param str base_path: Base part of the URI for listing resources, if - different from - :data:`~openstack.resource.Resource.base_path`. + different from :data:`~openstack.resource.Resource.base_path`. + :param bool allow_unknown_params: ``True`` to accept, but discard + unknown query parameters. This allows getting list of 'filters' and + passing everything known to the server. ``False`` will result in + validation exception when unknown query parameters are passed. :param dict params: These keyword arguments are passed through the :meth:`~openstack.resource.QueryParamter._transpose` method to find if any of them match expected query parameters to be @@ -1496,7 +1512,9 @@ def list(cls, session, paginated=True, base_path=None, **params): if base_path is None: base_path = cls.base_path - cls._query_mapping._validate(params, base_path=base_path) + params = cls._query_mapping._validate( + params, base_path=base_path, + allow_unknown_params=allow_unknown_params) query_params = cls._query_mapping._transpose(params) uri = base_path % params diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index fe0f824ea..24ca91da8 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1927,6 +1927,33 @@ class Test(self.test_class): except exceptions.InvalidResourceQuery as err: self.assertEqual(str(err), 'Invalid query params: something_wrong') + def test_allow_invalid_list_params(self): + qp = "query param!" + qp_name = "query-param" + uri_param = "uri param!" + + mock_empty = mock.Mock() + mock_empty.status_code = 200 + mock_empty.links = {} + mock_empty.json.return_value = {"resources": []} + + self.session.get.side_effect = [mock_empty] + + class Test(self.test_class): + _query_mapping = resource.QueryParameters(query_param=qp_name) + base_path = "/%(something)s/blah" + something = resource.URI("something") + + list(Test.list(self.session, paginated=True, query_param=qp, + allow_unknown_params=True, something=uri_param, + something_wrong=True)) + self.session.get.assert_called_once_with( + "/{something}/blah".format(something=uri_param), + headers={'Accept': 'application/json'}, + microversion=None, + params={qp_name: qp} + ) + def test_values_as_list_params(self): id = 1 qp = "query param!" From de03f4f53a3fc55959ad1445111bd76c49fb7945 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 3 Jun 2019 13:44:41 +0200 Subject: [PATCH 2463/3836] Use Resource layer for compute AZ and Aggregates Change-Id: I72a02ade12667a797643998f1ee9d07a6889f369 --- openstack/cloud/_compute.py | 91 +++++-------------- openstack/compute/v2/_proxy.py | 18 ++-- openstack/compute/v2/aggregate.py | 1 + openstack/compute/v2/availability_zone.py | 3 +- openstack/tests/unit/cloud/test_aggregate.py | 4 - .../tests/unit/compute/v2/test_aggregate.py | 2 +- .../unit/compute/v2/test_availability_zone.py | 6 -- 7 files changed, 33 insertions(+), 92 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index a883ad3c7..d43cb9581 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -138,19 +138,17 @@ def list_availability_zone_names(self, unavailable=False): list could not be fetched. """ try: - data = proxy._json_response( - self.compute.get('/os-availability-zone')) - except exc.OpenStackCloudHTTPError: + zones = self.compute.availability_zones() + ret = [] + for zone in zones: + if zone.state['available'] or unavailable: + ret.append(zone.name) + return ret + except exceptions.SDKException: self.log.debug( "Availability zone list could not be fetched", exc_info=True) return [] - zones = self._get_and_munchify('availabilityZoneInfo', data) - ret = [] - for zone in zones: - if zone['zoneState']['available'] or unavailable: - ret.append(zone['zoneName']) - return ret @_utils.cache_on_arguments() def list_flavors(self, get_extra=False): @@ -1619,16 +1617,13 @@ def search_aggregates(self, name_or_id=None, filters=None): aggregates = self.list_aggregates() return _utils._filter_list(aggregates, name_or_id, filters) - def list_aggregates(self): + def list_aggregates(self, filters={}): """List all available host aggregates. :returns: A list of aggregate dicts. """ - data = proxy._json_response( - self.compute.get('/os-aggregates'), - error_message="Error fetching aggregate list") - return self._get_and_munchify('aggregates', data) + return self.compute.aggregates(allow_unknown_params=True, **filters) def get_aggregate(self, name_or_id, filters=None): """Get an aggregate by name or ID. @@ -1661,16 +1656,10 @@ def create_aggregate(self, name, availability_zone=None): :raises: OpenStackCloudException on operation error. """ - data = proxy._json_response( - self.compute.post( - '/os-aggregates', - json={'aggregate': { - 'name': name, - 'availability_zone': availability_zone - }}), - error_message="Unable to create host aggregate {name}".format( - name=name)) - return self._get_and_munchify('aggregate', data) + return self.compute.create_aggregate( + name=name, + availability_zone=availability_zone + ) @_utils.valid_kwargs('name', 'availability_zone') def update_aggregate(self, name_or_id, **kwargs): @@ -1685,17 +1674,7 @@ def update_aggregate(self, name_or_id, **kwargs): :raises: OpenStackCloudException on operation error. """ aggregate = self.get_aggregate(name_or_id) - if not aggregate: - raise exc.OpenStackCloudException( - "Host aggregate %s not found." % name_or_id) - - data = proxy._json_response( - self.compute.put( - '/os-aggregates/{id}'.format(id=aggregate['id']), - json={'aggregate': kwargs}), - error_message="Error updating aggregate {name}".format( - name=name_or_id)) - return self._get_and_munchify('aggregate', data) + return self.compute.update_aggregate(aggregate, **kwargs) def delete_aggregate(self, name_or_id): """Delete a host aggregate. @@ -1706,19 +1685,13 @@ def delete_aggregate(self, name_or_id): :raises: OpenStackCloudException on operation error. """ - aggregate = self.get_aggregate(name_or_id) - if not aggregate: + try: + self.compute.delete_aggregate(name_or_id, ignore_missing=False) + return True + except exceptions.ResourceNotFound: self.log.debug("Aggregate %s not found for deleting", name_or_id) return False - return proxy._json_response( - self.compute.delete( - '/os-aggregates/{id}'.format(id=aggregate['id'])), - error_message="Error deleting aggregate {name}".format( - name=name_or_id)) - - return True - def set_aggregate_metadata(self, name_or_id, metadata): """Set aggregate metadata, replacing the existing metadata. @@ -1735,15 +1708,7 @@ def set_aggregate_metadata(self, name_or_id, metadata): raise exc.OpenStackCloudException( "Host aggregate %s not found." % name_or_id) - err_msg = "Unable to set metadata for host aggregate {name}".format( - name=name_or_id) - - data = proxy._json_response( - self.compute.post( - '/os-aggregates/{id}/action'.format(id=aggregate['id']), - json={'set_metadata': {'metadata': metadata}}), - error_message=err_msg) - return self._get_and_munchify('aggregate', data) + return self.compute.set_aggregate_metadata(aggregate, metadata) def add_host_to_aggregate(self, name_or_id, host_name): """Add a host to an aggregate. @@ -1758,14 +1723,7 @@ def add_host_to_aggregate(self, name_or_id, host_name): raise exc.OpenStackCloudException( "Host aggregate %s not found." % name_or_id) - err_msg = "Unable to add host {host} to aggregate {name}".format( - host=host_name, name=name_or_id) - - return proxy._json_response( - self.compute.post( - '/os-aggregates/{id}/action'.format(id=aggregate['id']), - json={'add_host': {'host': host_name}}), - error_message=err_msg) + return self.compute.add_host_to_aggregate(aggregate, host_name) def remove_host_from_aggregate(self, name_or_id, host_name): """Remove a host from an aggregate. @@ -1780,14 +1738,7 @@ def remove_host_from_aggregate(self, name_or_id, host_name): raise exc.OpenStackCloudException( "Host aggregate %s not found." % name_or_id) - err_msg = "Unable to remove host {host} to aggregate {name}".format( - host=host_name, name=name_or_id) - - return proxy._json_response( - self.compute.post( - '/os-aggregates/{id}/action'.format(id=aggregate['id']), - json={'remove_host': {'host': host_name}}), - error_message=err_msg) + return self.compute.remove_host_from_aggregate(aggregate, host_name) def set_compute_quotas(self, name_or_id, **kwargs): """ Set a quota in a project diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 0e1434ea4..eb2e01923 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -122,15 +122,16 @@ def flavors(self, details=True, **query): flv = _flavor.FlavorDetail if details else _flavor.Flavor return self._list(flv, **query) - def aggregates(self): + def aggregates(self, **query): """Return a generator of aggregate + :param kwargs query: Optional query parameters to be sent to limit + the aggregates being returned. + :returns: A generator of aggregate :rtype: class: `~openstack.compute.v2.aggregate.Aggregate` """ - aggregate = _aggregate.Aggregate - - return self._list(aggregate) + return self._list(_aggregate.Aggregate, **query) def get_aggregate(self, aggregate): """Get a single host aggregate @@ -1040,12 +1041,11 @@ def availability_zones(self, details=False): :rtype: :class:`~openstack.compute.v2.availability_zone.\ AvailabilityZone` """ - if details: - az = availability_zone.AvailabilityZoneDetail - else: - az = availability_zone.AvailabilityZone + base_path = '/os-availability-zone/detail' if details else None - return self._list(az) + return self._list( + availability_zone.AvailabilityZone, + base_path=base_path) def get_server_metadata(self, server): """Return a dictionary of metadata for a server diff --git a/openstack/compute/v2/aggregate.py b/openstack/compute/v2/aggregate.py index 0de479492..ab0f05d0f 100644 --- a/openstack/compute/v2/aggregate.py +++ b/openstack/compute/v2/aggregate.py @@ -25,6 +25,7 @@ class Aggregate(resource.Resource): allow_fetch = True allow_delete = True allow_list = True + allow_commit = True # Properties #: Availability zone of aggregate diff --git a/openstack/compute/v2/availability_zone.py b/openstack/compute/v2/availability_zone.py index 0238dd435..aecfecb2d 100644 --- a/openstack/compute/v2/availability_zone.py +++ b/openstack/compute/v2/availability_zone.py @@ -29,5 +29,4 @@ class AvailabilityZone(resource.Resource): hosts = resource.Body('hosts') -class AvailabilityZoneDetail(AvailabilityZone): - base_path = '/os-availability-zone/detail' +AvailabilityZoneDetail = AvailabilityZone diff --git a/openstack/tests/unit/cloud/test_aggregate.py b/openstack/tests/unit/cloud/test_aggregate.py index 974aba727..f6718c2aa 100644 --- a/openstack/tests/unit/cloud/test_aggregate.py +++ b/openstack/tests/unit/cloud/test_aggregate.py @@ -69,10 +69,6 @@ def test_create_aggregate_with_az(self): def test_delete_aggregate(self): self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['os-aggregates']), - json={'aggregates': [self.fake_aggregate]}), dict(method='DELETE', uri=self.get_mock_url( 'compute', 'public', append=['os-aggregates', '1'])), diff --git a/openstack/tests/unit/compute/v2/test_aggregate.py b/openstack/tests/unit/compute/v2/test_aggregate.py index e2ad9aa1b..7c79f9d0b 100644 --- a/openstack/tests/unit/compute/v2/test_aggregate.py +++ b/openstack/tests/unit/compute/v2/test_aggregate.py @@ -47,7 +47,7 @@ def test_basic(self): self.assertEqual('/os-aggregates', sot.base_path) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) - self.assertFalse(sot.allow_commit) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/compute/v2/test_availability_zone.py b/openstack/tests/unit/compute/v2/test_availability_zone.py index f8d409366..af646a35e 100644 --- a/openstack/tests/unit/compute/v2/test_availability_zone.py +++ b/openstack/tests/unit/compute/v2/test_availability_zone.py @@ -31,12 +31,6 @@ def test_basic(self): self.assertEqual('/os-availability-zone', sot.base_path) self.assertTrue(sot.allow_list) - def test_basic_detail(self): - sot = az.AvailabilityZoneDetail() - self.assertEqual('availabilityZoneInfo', sot.resources_key) - self.assertEqual('/os-availability-zone/detail', sot.base_path) - self.assertTrue(sot.allow_list) - def test_make_basic(self): sot = az.AvailabilityZone(**BASIC_EXAMPLE) self.assertEqual(BASIC_EXAMPLE['id'], sot.id) From 10447761405c7a4bab263d2b30b80641f54a36be Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 3 Jun 2019 14:31:11 +0200 Subject: [PATCH 2464/3836] Use Resource layer for the compute Hypervsors and Images - add query params for Hypervisors - cleanup ImageDetail and HypervisorDetail - mark compute.images API as deprecated - use resource layer for hypervisors from cloud layer Change-Id: I64988e3b978b74bd095845d4ee0e5aab4f5acf95 --- openstack/cloud/_compute.py | 10 ++-- openstack/compute/v2/_proxy.py | 49 +++++++---------- openstack/compute/v2/hypervisor.py | 11 ++-- openstack/compute/v2/image.py | 7 +-- openstack/tests/unit/cloud/test_operator.py | 4 +- .../tests/unit/compute/v2/test_hypervisor.py | 15 +++--- openstack/tests/unit/compute/v2/test_image.py | 53 ++++++------------- 7 files changed, 56 insertions(+), 93 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index a883ad3c7..2064f29c0 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -1594,16 +1594,16 @@ def list_flavor_access(self, flavor_id): return _utils.normalize_flavor_accesses( self._get_and_munchify('flavor_access', data)) - def list_hypervisors(self): + def list_hypervisors(self, filters={}): """List all hypervisors :returns: A list of hypervisor ``munch.Munch``. """ - data = proxy._json_response( - self.compute.get('/os-hypervisors/detail'), - error_message="Error fetching hypervisor list") - return self._get_and_munchify('hypervisors', data) + return list(self.compute.hypervisors( + details=True, + allow_unknown_params=True, + **filters)) def search_aggregates(self, name_or_id=None, filters=None): """Seach host aggregates. diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 0e1434ea4..e1a93e7b8 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -9,6 +9,7 @@ # 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 warnings from openstack.compute.v2 import aggregate as _aggregate from openstack.compute.v2 import availability_zone @@ -274,16 +275,18 @@ def images(self, details=True, **query): """Return a generator of images :param bool details: When ``True``, returns - :class:`~openstack.compute.v2.image.ImageDetail` objects, - otherwise :class:`~openstack.compute.v2.image.Image`. + :class:`~openstack.compute.v2.image.Image` objects with all + available properties, otherwise only basic properties are returned. *Default: ``True``* :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of image objects """ - img = _image.ImageDetail if details else _image.Image - return self._list(img, **query) + warnings.warn('This API is deprecated and may disappear shortly', + DeprecationWarning) + base_path = '/images/detail' if details else None + return self._list(_image.Image, base_path=base_path, **query) def _get_base_resource(self, res, base): # Metadata calls for Image and Server can work for both those @@ -298,9 +301,7 @@ def get_image_metadata(self, image): """Return a dictionary of metadata for an image :param image: Either the ID of an image or a - :class:`~openstack.compute.v2.image.Image` or - :class:`~openstack.compute.v2.image.ImageDetail` - instance. + :class:`~openstack.compute.v2.image.Image` instance. :returns: A :class:`~openstack.compute.v2.image.Image` with only the image's metadata. All keys and values are Unicode text. @@ -315,9 +316,7 @@ def set_image_metadata(self, image, **metadata): """Update metadata for an image :param image: Either the ID of an image or a - :class:`~openstack.compute.v2.image.Image` or - :class:`~openstack.compute.v2.image.ImageDetail` - instance. + :class:`~openstack.compute.v2.image.Image` instance. :param kwargs metadata: Key/value pairs to be updated in the image's metadata. No other metadata is modified by this call. All keys and values are stored @@ -338,9 +337,7 @@ def delete_image_metadata(self, image, keys): Note: This method will do a HTTP DELETE request for every key in keys. :param image: Either the ID of an image or a - :class:`~openstack.compute.v2.image.Image` or - :class:`~openstack.compute.v2.image.ImageDetail` - instance. + :class:`~openstack.compute.v2.image.Image` instance. :param keys: The keys to delete. :rtype: ``None`` @@ -1068,9 +1065,7 @@ def set_server_metadata(self, server, **metadata): """Update metadata for a server :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` or - :class:`~openstack.compute.v2.server.ServerDetail` - instance. + :class:`~openstack.compute.v2.server.Server` instance. :param kwargs metadata: Key/value pairs to be updated in the server's metadata. No other metadata is modified by this call. All keys and values are stored @@ -1091,9 +1086,7 @@ def delete_server_metadata(self, server, keys): Note: This method will do a HTTP DELETE request for every key in keys. :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` or - :class:`~openstack.compute.v2.server.ServerDetail` - instance. + :class:`~openstack.compute.v2.server.Server` instance. :param keys: The keys to delete :rtype: ``None`` @@ -1171,23 +1164,19 @@ def server_groups(self, **query): """ return self._list(_server_group.ServerGroup, **query) - def hypervisors(self, details=False): + def hypervisors(self, details=False, **query): """Return a generator of hypervisor :param bool details: When set to the default, ``False``, - :class:`~openstack.compute.v2.hypervisor.Hypervisor` - instances will be returned. ``True`` will cause - :class:`~openstack.compute.v2.hypervisor.HypervisorDetail` - instances to be returned. + :class:`~openstack.compute.v2.hypervisor.Hypervisor` + instances will be returned with only basic information populated. + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. :returns: A generator of hypervisor :rtype: class: `~openstack.compute.v2.hypervisor.Hypervisor` """ - if details: - hypervisor = _hypervisor.HypervisorDetail - else: - hypervisor = _hypervisor.Hypervisor - - return self._list(hypervisor) + base_path = '/os-hypervisors/detail' if details else None + return self._list(_hypervisor.Hypervisor, base_path=base_path, **query) def find_hypervisor(self, name_or_id, ignore_missing=True): """Find a hypervisor from name or id to get the corresponding info diff --git a/openstack/compute/v2/hypervisor.py b/openstack/compute/v2/hypervisor.py index 50d5ae64c..fe07ca87e 100644 --- a/openstack/compute/v2/hypervisor.py +++ b/openstack/compute/v2/hypervisor.py @@ -23,6 +23,10 @@ class Hypervisor(resource.Resource): allow_fetch = True allow_list = True + _query_mapping = resource.QueryParameters( + 'hypervisor_hostname_pattern', 'with_servers' + ) + # Properties #: Status of hypervisor status = resource.Body('status') @@ -64,9 +68,4 @@ class Hypervisor(resource.Resource): disk_available = resource.Body("disk_available_least") -class HypervisorDetail(Hypervisor): - base_path = '/os-hypervisors/detail' - - # capabilities - allow_fetch = False - allow_list = True +HypervisorDetail = Hypervisor diff --git a/openstack/compute/v2/image.py b/openstack/compute/v2/image.py index ed6a1682b..74e351b05 100644 --- a/openstack/compute/v2/image.py +++ b/openstack/compute/v2/image.py @@ -55,9 +55,4 @@ class Image(resource.Resource, metadata.MetadataMixin): size = resource.Body('OS-EXT-IMG-SIZE:size', type=int) -class ImageDetail(Image): - base_path = '/images/detail' - - allow_fetch = False - allow_delete = False - allow_list = True +ImageDetail = Image diff --git a/openstack/tests/unit/cloud/test_operator.py b/openstack/tests/unit/cloud/test_operator.py index 1dee45e0d..230568560 100644 --- a/openstack/tests/unit/cloud/test_operator.py +++ b/openstack/tests/unit/cloud/test_operator.py @@ -130,7 +130,7 @@ def test_list_hypervisors(self): r = self.cloud.list_hypervisors() self.assertEqual(2, len(r)) - self.assertEqual('testserver1', r[0]['hypervisor_hostname']) - self.assertEqual('testserver2', r[1]['hypervisor_hostname']) + self.assertEqual('testserver1', r[0]['name']) + self.assertEqual('testserver2', r[1]['name']) self.assert_calls() diff --git a/openstack/tests/unit/compute/v2/test_hypervisor.py b/openstack/tests/unit/compute/v2/test_hypervisor.py index feb96b18e..2a27c9464 100644 --- a/openstack/tests/unit/compute/v2/test_hypervisor.py +++ b/openstack/tests/unit/compute/v2/test_hypervisor.py @@ -52,6 +52,13 @@ def test_basic(self): self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_list) + self.assertDictEqual({'hypervisor_hostname_pattern': + 'hypervisor_hostname_pattern', + 'limit': 'limit', + 'marker': 'marker', + 'with_servers': 'with_servers'}, + sot._query_mapping._mapping) + def test_make_it(self): sot = hypervisor.Hypervisor(**EXAMPLE) self.assertEqual(EXAMPLE['id'], sot.id) @@ -75,11 +82,3 @@ def test_make_it(self): self.assertEqual(EXAMPLE['disk_available_least'], sot.disk_available) self.assertEqual(EXAMPLE['local_gb'], sot.local_disk_size) self.assertEqual(EXAMPLE['free_ram_mb'], sot.memory_free) - - def test_detail(self): - sot = hypervisor.HypervisorDetail() - self.assertEqual('hypervisor', sot.resource_key) - self.assertEqual('hypervisors', sot.resources_key) - self.assertEqual('/os-hypervisors/detail', sot.base_path) - self.assertFalse(sot.allow_fetch) - self.assertTrue(sot.allow_list) diff --git a/openstack/tests/unit/compute/v2/test_image.py b/openstack/tests/unit/compute/v2/test_image.py index 59c101b50..eaa952222 100644 --- a/openstack/tests/unit/compute/v2/test_image.py +++ b/openstack/tests/unit/compute/v2/test_image.py @@ -15,13 +15,11 @@ from openstack.compute.v2 import image IDENTIFIER = 'IDENTIFIER' -BASIC_EXAMPLE = { + +EXAMPLE = { 'id': IDENTIFIER, 'links': '2', 'name': '3', -} - -DETAILS = { 'created': '2015-03-09T12:14:57.233772', 'metadata': {'key': '2'}, 'minDisk': 3, @@ -32,9 +30,6 @@ 'OS-EXT-IMG-SIZE:size': 8 } -DETAIL_EXAMPLE = BASIC_EXAMPLE.copy() -DETAIL_EXAMPLE.update(DETAILS) - class TestImage(base.TestCase): @@ -61,32 +56,18 @@ def test_basic(self): sot._query_mapping._mapping) def test_make_basic(self): - sot = image.Image(**BASIC_EXAMPLE) - self.assertEqual(BASIC_EXAMPLE['id'], sot.id) - self.assertEqual(BASIC_EXAMPLE['links'], sot.links) - self.assertEqual(BASIC_EXAMPLE['name'], sot.name) - - def test_detail(self): - sot = image.ImageDetail() - self.assertEqual('image', sot.resource_key) - self.assertEqual('images', sot.resources_key) - self.assertEqual('/images/detail', sot.base_path) - self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_fetch) - self.assertFalse(sot.allow_commit) - self.assertFalse(sot.allow_delete) - self.assertTrue(sot.allow_list) - - def test_make_detail(self): - sot = image.ImageDetail(**DETAIL_EXAMPLE) - self.assertEqual(DETAIL_EXAMPLE['created'], sot.created_at) - self.assertEqual(DETAIL_EXAMPLE['id'], sot.id) - self.assertEqual(DETAIL_EXAMPLE['links'], sot.links) - self.assertEqual(DETAIL_EXAMPLE['metadata'], sot.metadata) - self.assertEqual(DETAIL_EXAMPLE['minDisk'], sot.min_disk) - self.assertEqual(DETAIL_EXAMPLE['minRam'], sot.min_ram) - self.assertEqual(DETAIL_EXAMPLE['name'], sot.name) - self.assertEqual(DETAIL_EXAMPLE['progress'], sot.progress) - self.assertEqual(DETAIL_EXAMPLE['status'], sot.status) - self.assertEqual(DETAIL_EXAMPLE['updated'], sot.updated_at) - self.assertEqual(DETAIL_EXAMPLE['OS-EXT-IMG-SIZE:size'], sot.size) + sot = image.Image(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['links'], sot.links) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['created'], sot.created_at) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['links'], sot.links) + self.assertEqual(EXAMPLE['metadata'], sot.metadata) + self.assertEqual(EXAMPLE['minDisk'], sot.min_disk) + self.assertEqual(EXAMPLE['minRam'], sot.min_ram) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['progress'], sot.progress) + self.assertEqual(EXAMPLE['status'], sot.status) + self.assertEqual(EXAMPLE['updated'], sot.updated_at) + self.assertEqual(EXAMPLE['OS-EXT-IMG-SIZE:size'], sot.size) From 86ad9debd1cb08c2a2e53fd30502b020a0c3e4fe Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 15 Mar 2019 14:50:04 +0000 Subject: [PATCH 2465/3836] Make factory for a CloudRegion from CONF objects This commit enables SDK consumers using oslo.config for keystoneauth1 Adapter settings by introducing a method: openstack.config.cloud_region.from_conf This accepts: - An oslo.config ConfigOpts containing Adapter options in sections named according to project (e.g. [nova], not [compute]). Current behavior is to use defaults if no such section exists, which may not be what we want long term. - A Session. This is currently required - if unspecified, a ConfigException is raised - but in the future we probably want to support creating one (and an auth) from the conf. - Other kwargs to be passed to the CloudRegion constructor. The method returns a CloudRegion that can be used to create a Connection. Needed-By: blueprint openstacksdk-in-nova Co-Authored-By: Eric Fried Change-Id: I05fb4da39d2eefc91828ace02db2741b62a2cb0a --- lower-constraints.txt | 3 +- openstack/config/cloud_region.py | 55 ++++++ openstack/connection.py | 33 +++- openstack/tests/unit/config/test_from_conf.py | 165 ++++++++++++++++++ .../conf-object-ctr-c0e1da0a67dad841.yaml | 6 + requirements.txt | 2 +- test-requirements.txt | 1 + 7 files changed, 262 insertions(+), 3 deletions(-) create mode 100644 openstack/tests/unit/config/test_from_conf.py create mode 100644 releasenotes/notes/conf-object-ctr-c0e1da0a67dad841.yaml diff --git a/lower-constraints.txt b/lower-constraints.txt index 93758a7e4..affeed9e7 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -14,7 +14,7 @@ jmespath==0.9.0 jsonpatch==1.16 jsonpointer==1.13 jsonschema==2.6.0 -keystoneauth1==3.13.0 +keystoneauth1==3.14.0 linecache2==1.0.0 mock==2.0.0 mox3==0.20.0 @@ -22,6 +22,7 @@ munch==2.1.0 netifaces==0.10.4 os-client-config==1.28.0 os-service-types==1.2.0 +oslo.config==6.1.0 oslotest==3.2.0 pbr==2.0.0 prometheus-client==0.4.2 diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index c9d3682e8..044669fee 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -17,6 +17,7 @@ from keystoneauth1 import discover import keystoneauth1.exceptions.catalog +from keystoneauth1.loading import adapter as ks_load_adap from keystoneauth1 import session as ks_session import os_service_types import requestsexceptions @@ -45,6 +46,10 @@ } +# Sentinel for nonexistence +_ENOENT = object() + + def _make_key(key, service_type): if not service_type: return key @@ -98,6 +103,56 @@ def from_session(session, name=None, region_name=None, app_name=app_name, app_version=app_version) +def from_conf(conf, session=None, **kwargs): + """Create a CloudRegion from oslo.config ConfigOpts. + + :param oslo_config.cfg.ConfigOpts conf: + An oslo.config ConfigOpts containing keystoneauth1.Adapter options in + sections named according to project (e.g. [nova], not [compute]). + TODO: Current behavior is to use defaults if no such section exists, + which may not be what we want long term. + :param keystoneauth1.session.Session session: + An existing authenticated Session to use. This is currently required. + TODO: Load this (and auth) from the conf. + :param kwargs: + Additional keyword arguments to be passed directly to the CloudRegion + constructor. + :raise openstack.exceptions.ConfigException: + If session is not specified. + :return: + An openstack.config.cloud_region.CloudRegion. + """ + if not session: + # TODO(mordred) Fill this in - not needed for first stab with nova + raise exceptions.ConfigException("A Session must be supplied.") + config_dict = kwargs.pop('config', config_defaults.get_defaults()) + stm = os_service_types.ServiceTypes() + # TODO(mordred) Think about region_name here + region_name = kwargs.pop('region_name', None) + for st in stm.all_types_by_service_type: + project_name = stm.get_project_name(st) + if project_name not in conf: + continue + opt_dict = {} + # Populate opt_dict with (appropriately processed) Adapter conf opts + try: + ks_load_adap.process_conf_options(conf[project_name], opt_dict) + except Exception: + # NOTE(efried): This is for oslo_config.cfg.NoSuchOptError, but we + # don't want to drag in oslo.config just for that. + continue + # Load them into config_dict under keys prefixed by ${service_type}_ + for raw_name, opt_val in opt_dict.items(): + if raw_name == 'region_name': + region_name = opt_val + continue + config_name = '_'.join([st, raw_name]) + config_dict[config_name] = opt_val + return CloudRegion( + session=session, region_name=region_name, config=config_dict, + **kwargs) + + class CloudRegion(object): """The configuration for a Region of an OpenStack Cloud. diff --git a/openstack/connection.py b/openstack/connection.py index 8a2d0b5a0..f4fed899a 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -112,6 +112,27 @@ compute_api_version='2', identity_interface='internal') +From oslo.conf CONF object +-------------------------- + +For applications that have an oslo.config ``CONF`` object that has been +populated with ``keystoneauth1.loading.register_adapter_conf_options`` in +groups named by the OpenStack service's project name, it is possible to +construct a Connection with the ``CONF`` object and an authenticated Session. + +.. note:: + + This is primarily intended for use by OpenStack services to talk amongst + themselves. + +.. code-block:: python + + from openstack import connection + + conn = connection.Connection( + session=session, + oslo_config=CONF) + From existing CloudRegion ------------------------- @@ -249,6 +270,7 @@ def __init__(self, cloud=None, config=None, session=None, use_direct_get=False, task_manager=None, rate_limit=None, + oslo_conf=None, **kwargs): """Create a connection to a cloud. @@ -295,6 +317,11 @@ def __init__(self, cloud=None, config=None, session=None, keys as service-type and values as floats expressing the calls per second for that service. Defaults to None, which means no rate-limiting is performed. + :param oslo_conf: An oslo.config CONF object. + :type oslo_conf: :class:`~oslo_config.cfg.ConfigOpts` + An oslo.config ``CONF`` object that has been populated with + ``keystoneauth1.loading.register_adapter_conf_options`` in + groups named by the OpenStack service's project name. :param kwargs: If a config is not provided, the rest of the parameters provided are assumed to be arguments to be passed to the CloudRegion constructor. @@ -306,7 +333,11 @@ def __init__(self, cloud=None, config=None, session=None, self._extra_services[service.service_type] = service if not self.config: - if session: + if oslo_conf: + self.config = cloud_region.from_conf( + oslo_conf, session=session, app_name=app_name, + app_version=app_version) + elif session: self.config = cloud_region.from_session( session=session, app_name=app_name, app_version=app_version, diff --git a/openstack/tests/unit/config/test_from_conf.py b/openstack/tests/unit/config/test_from_conf.py new file mode 100644 index 000000000..7308b9397 --- /dev/null +++ b/openstack/tests/unit/config/test_from_conf.py @@ -0,0 +1,165 @@ +# 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 uuid + +from keystoneauth1 import exceptions as ks_exc +from keystoneauth1 import loading as ks_loading +from oslo_config import cfg + +from openstack.config import cloud_region +from openstack import connection +from openstack import exceptions +from openstack.tests import fakes +from openstack.tests.unit import base + + +class TestFromConf(base.TestCase): + + def setUp(self): + super(TestFromConf, self).setUp() + self.oslo_config_dict = { + # All defaults for nova + 'nova': {}, + # monasca not in the service catalog + 'monasca': {}, + # Overrides for heat + 'heat': { + 'region_name': 'SpecialRegion', + 'interface': 'internal', + 'endpoint_override': 'https://example.org:8888/heat/v2' + }, + } + + def _load_ks_cfg_opts(self): + conf = cfg.ConfigOpts() + for group, opts in self.oslo_config_dict.items(): + if opts is not None: + ks_loading.register_adapter_conf_options(conf, group) + for name, val in opts.items(): + conf.set_override(name, val, group=group) + return conf + + def _get_conn(self): + oslocfg = self._load_ks_cfg_opts() + # Throw name in here to prove **kwargs is working + config = cloud_region.from_conf( + oslocfg, session=self.cloud.session, name='from_conf.example.com') + self.assertEqual('from_conf.example.com', config.name) + + # TODO(efried): Currently region_name gets set to the last value seen + # in the config, which is nondeterministic and surely incorrect. + # Sometimes that's SpecialRegion, but some tests use the base fixtures + # which have no compute endpoint in SpecialRegion. Force override for + # now to make those tests work. + config.region_name = None + + return connection.Connection(config=config) + + def test_adapter_opts_set(self): + """Adapter opts specified in the conf.""" + conn = self._get_conn() + + discovery = { + "versions": { + "values": [ + {"status": "stable", + "updated": "2019-06-01T00:00:00Z", + "media-types": [{ + "base": "application/json", + "type": "application/vnd.openstack.heat-v2+json"}], + "id": "v2.0", + "links": [{ + "href": "https://example.org:8888/heat/v2", + "rel": "self"}] + }] + } + } + self.register_uris([ + dict(method='GET', + uri='https://example.org:8888/heat/v2', + json=discovery), + dict(method='GET', + uri='https://example.org:8888/heat/v2/foo', + json={'foo': {}}), + ]) + + adap = conn.orchestration + # TODO(efried): Fix this when region_name behaves correctly. + # self.assertEqual('SpecialRegion', adap.region_name) + self.assertEqual('orchestration', adap.service_type) + self.assertEqual('internal', adap.interface) + self.assertEqual('https://example.org:8888/heat/v2', + adap.endpoint_override) + + adap.get('/foo') + self.assert_calls() + + def test_default_adapter_opts(self): + """Adapter opts are registered, but all defaulting in conf.""" + conn = self._get_conn() + + # Nova has empty adapter config, so these default + adap = conn.compute + self.assertIsNone(adap.region_name) + self.assertEqual('compute', adap.service_type) + self.assertEqual('public', adap.interface) + self.assertIsNone(adap.endpoint_override) + + server_id = str(uuid.uuid4()) + server_name = self.getUniqueString('name') + fake_server = fakes.make_fake_server(server_id, server_name) + self.register_uris([ + self.get_nova_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [fake_server]}), + ]) + s = next(adap.servers()) + self.assertEqual(s.id, server_id) + self.assertEqual(s.name, server_name) + self.assert_calls() + + def test_no_adapter_opts(self): + """Adapter opts for service type not registered.""" + del self.oslo_config_dict['heat'] + conn = self._get_conn() + + # TODO(efried): This works, even though adapter opts are not + # registered. Should it? + adap = conn.orchestration + self.assertIsNone(adap.region_name) + self.assertEqual('orchestration', adap.service_type) + self.assertEqual('public', adap.interface) + self.assertIsNone(adap.endpoint_override) + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'orchestration', append=['foo']), + json={'foo': {}}) + ]) + adap.get('/foo') + self.assert_calls() + + def test_no_session(self): + # TODO(efried): Currently calling without a Session is not implemented. + self.assertRaises(exceptions.ConfigException, + cloud_region.from_conf, self._load_ks_cfg_opts()) + + def test_no_endpoint(self): + """Conf contains adapter opts, but service type not in catalog.""" + conn = self._get_conn() + # Monasca is not in the service catalog + self.assertRaises(ks_exc.catalog.EndpointNotFound, + getattr, conn, 'monitoring') diff --git a/releasenotes/notes/conf-object-ctr-c0e1da0a67dad841.yaml b/releasenotes/notes/conf-object-ctr-c0e1da0a67dad841.yaml new file mode 100644 index 000000000..60bb03594 --- /dev/null +++ b/releasenotes/notes/conf-object-ctr-c0e1da0a67dad841.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Added the ability to create a ``Connection`` from an ``oslo.config`` + ``CONF`` object. This is primarily intended to be used by OpenStack + services using SDK for inter-service communication. diff --git a/requirements.txt b/requirements.txt index 6181b5544..9062efb83 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ requestsexceptions>=1.2.0 # Apache-2.0 jsonpatch!=1.20,>=1.16 # BSD six>=1.10.0 # MIT os-service-types>=1.2.0 # Apache-2.0 -keystoneauth1>=3.13.0 # Apache-2.0 +keystoneauth1>=3.14.0 # Apache-2.0 munch>=2.1.0 # MIT decorator>=3.4.0 # BSD diff --git a/test-requirements.txt b/test-requirements.txt index c32f63564..feb8d781c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,6 +10,7 @@ jsonschema>=2.6.0 # MIT mock>=2.0.0 # BSD prometheus-client>=0.4.2 # Apache-2.0 python-subunit>=1.0.0 # Apache-2.0/BSD +oslo.config>=6.1.0 # Apache-2.0 oslotest>=3.2.0 # Apache-2.0 requests-mock>=1.2.0 # Apache-2.0 statsd>=3.3.0 From 5f0401a20609569de77d8d355cd43b36bed8d111 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Mon, 3 Jun 2019 16:31:36 -0500 Subject: [PATCH 2466/3836] Support Proxy-specific region_name Proxy (a subclass of keystoneauth1 Adapter) is set up to know about region_name. This is done so that consumers of cloud_region.from_conf will have their [$project]region_name setting honored rather than ignored. Previously, CloudRegion was set up to deal with a single region, masking (or rather overriding) each Proxy's notion of what its region_name should be. With this change, (service-specific) region_nameZ are honored by the CloudRegion constructor, as follows: - A service-specific region_name in the ``config`` dict (e.g. 'compute_region_name': 'SnowflakeComputeRegion') takes first precedence for that service. - If no service-specific region_name is present in the ``config`` dict, the value of the region_name kwarg to the CloudRegion constructor is used. (This kwarg should really go away, but is preserved for backward compatibility.) - If neither of the above is given, the value of the (unprefixed) 'region_name' key in the ``config`` dict is used. (This should really take precedence over the kwarg, but it's done this way for backward compatibility.) - If none of the above exist, None is used (the previous behavior, whatever that may mean). This change should hopefully be transparent to any existing usages of the CloudRegion constructor, as ${service}_region_name would have been ignored if present. However, if any existing usages had such a config setting present, it will no longer be ignored, resulting in potential RBB [1]. [1] That's "Revoked Bug Benefit" Change-Id: Ie253823a753d09d52e45df9d515fd22870c2d4c5 --- openstack/cloud/_compute.py | 13 ++-- openstack/cloud/_normalize.py | 5 +- openstack/cloud/meta.py | 4 +- openstack/cloud/openstackcloud.py | 15 ++-- openstack/config/cloud_region.py | 70 +++++++++++++------ openstack/service_description.py | 5 +- openstack/tests/unit/cloud/test_meta.py | 4 +- openstack/tests/unit/cloud/test_operator.py | 2 +- .../tests/unit/config/test_cloud_config.py | 49 +++++++++++++ openstack/tests/unit/config/test_from_conf.py | 10 +-- 10 files changed, 132 insertions(+), 45 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index a883ad3c7..942d76b04 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -39,6 +39,11 @@ def __init__(self): self._servers_time = 0 self._servers_lock = threading.Lock() + @property + def _compute_region(self): + # This is only used in exception messages. Can we get rid of it? + return self.config.get_region_name('compute') + def get_flavor_name(self, flavor_id): flavor = self.get_flavor(flavor_id, get_extra=False) if flavor: @@ -896,7 +901,7 @@ def create_server( 'Network {network} is not a valid network in' ' {cloud}:{region}'.format( network=network, - cloud=self.name, region=self.config.region_name)) + cloud=self.name, region=self._compute_region)) nics.append({'net-id': network_obj['id']}) kwargs['nics'] = nics @@ -1025,7 +1030,7 @@ def _get_boot_from_volume_kwargs( 'Volume {boot_volume} is not a valid volume' ' in {cloud}:{region}'.format( boot_volume=boot_volume, - cloud=self.name, region=self.config.region_name)) + cloud=self.name, region=self._compute_region)) block_mapping = { 'boot_index': '0', 'delete_on_termination': terminate_volume, @@ -1046,7 +1051,7 @@ def _get_boot_from_volume_kwargs( 'Image {image} is not a valid image in' ' {cloud}:{region}'.format( image=image, - cloud=self.name, region=self.config.region_name)) + cloud=self.name, region=self._compute_region)) block_mapping = { 'boot_index': '0', @@ -1076,7 +1081,7 @@ def _get_boot_from_volume_kwargs( 'Volume {volume} is not a valid volume' ' in {cloud}:{region}'.format( volume=volume, - cloud=self.name, region=self.config.region_name)) + cloud=self.name, region=self._compute_region)) block_mapping = { 'boot_index': '-1', 'delete_on_termination': False, diff --git a/openstack/cloud/_normalize.py b/openstack/cloud/_normalize.py index c5002247f..c5f9d760a 100644 --- a/openstack/cloud/_normalize.py +++ b/openstack/cloud/_normalize.py @@ -563,7 +563,10 @@ def _normalize_server(self, server): ret['config_drive'] = config_drive ret['project_id'] = project_id ret['tenant_id'] = project_id - ret['region'] = self.config.region_name + # TODO(efried): This is hardcoded to 'compute' because this method + # should only ever be used by the compute proxy. (That said, it + # doesn't appear to be used at all, so can we get rid of it?) + ret['region'] = self.config.get_region_name('compute') ret['cloud'] = self.config.name ret['az'] = az for key, val in ret['properties'].items(): diff --git a/openstack/cloud/meta.py b/openstack/cloud/meta.py index 1312d00d9..0ff6fb1dc 100644 --- a/openstack/cloud/meta.py +++ b/openstack/cloud/meta.py @@ -340,7 +340,9 @@ def _get_interface_ip(cloud, server): def get_groups_from_server(cloud, server, server_vars): groups = [] - region = cloud.config.region_name + # NOTE(efried): This is hardcoded to 'compute' because this method is only + # used from ComputeCloudMixin. + region = cloud.config.get_region_name('compute') cloud_name = cloud.name # Create a group for the cloud diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 1468a14fd..5f37ad929 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -358,7 +358,7 @@ def _get_versioned_client( service_name=self.config.get_service_name(service_type), interface=self.config.get_interface(service_type), endpoint_override=self.config.get_endpoint(service_type), - region_name=self.config.region_name, + region_name=self.config.get_region_name(service_type), statsd_prefix=self.config.get_statsd_prefix(), statsd_client=self.config.get_statsd_client(), prometheus_counter=self.config.get_prometheus_counter(), @@ -374,7 +374,7 @@ def _get_versioned_client( service_name=self.config.get_service_name(service_type), interface=self.config.get_interface(service_type), endpoint_override=self.config.get_endpoint(service_type), - region_name=self.config.region_name, + region_name=self.config.get_region_name(service_type), min_version=min_version, max_version=max_version) @@ -413,7 +413,7 @@ def _get_raw_client( interface=self.config.get_interface(service_type), endpoint_override=self.config.get_endpoint( service_type) or endpoint_override, - region_name=self.config.region_name) + region_name=self.config.get_region_name(service_type)) def _is_client_version(self, client, version): client_name = '_{client}_client'.format(client=client) @@ -529,7 +529,9 @@ def current_location(self): def _get_current_location(self, project_id=None, zone=None): return munch.Munch( cloud=self.name, - region_name=self.config.region_name, + # TODO(efried): This is wrong, but it only seems to be used in a + # repr; can we get rid of it? + region_name=self.config.get_region_name(), zone=zone, project=self._get_project_info(project_id), ) @@ -649,7 +651,8 @@ def get_name(self): return self.name def get_region(self): - return self.config.region_name + # TODO(efried): This seems to be unused. Can we get rid of it? + return self.config.get_region_name() def get_session_endpoint(self, service_key): try: @@ -666,7 +669,7 @@ def get_session_endpoint(self, service_key): " {error}".format( service=service_key, cloud=self.name, - region=self.config.region_name, + region=self.config.get_region_name(service_key), error=str(e))) return endpoint diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 044669fee..766a3b901 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -127,8 +127,6 @@ def from_conf(conf, session=None, **kwargs): raise exceptions.ConfigException("A Session must be supplied.") config_dict = kwargs.pop('config', config_defaults.get_defaults()) stm = os_service_types.ServiceTypes() - # TODO(mordred) Think about region_name here - region_name = kwargs.pop('region_name', None) for st in stm.all_types_by_service_type: project_name = stm.get_project_name(st) if project_name not in conf: @@ -143,14 +141,10 @@ def from_conf(conf, session=None, **kwargs): continue # Load them into config_dict under keys prefixed by ${service_type}_ for raw_name, opt_val in opt_dict.items(): - if raw_name == 'region_name': - region_name = opt_val - continue config_name = '_'.join([st, raw_name]) config_dict[config_name] = opt_val return CloudRegion( - session=session, region_name=region_name, config=config_dict, - **kwargs) + session=session, config=config_dict, **kwargs) class CloudRegion(object): @@ -158,6 +152,28 @@ class CloudRegion(object): A CloudRegion encapsulates the config information needed for connections to all of the services in a Region of a Cloud. + + TODO(efried): Doc the rest of the kwargs + + :param str region_name: + The default region name for all services in this CloudRegion. If + both ``region_name`` and ``config['region_name'] are specified, the + kwarg takes precedence. May be overridden for a given ${service} + via a ${service}_region_name key in the ``config`` dict. + :param dict config: + A dict of configuration values for the CloudRegion and its + services. The key for a ${config_option} for a specific ${service} + should be ${service}_${config_option}. For example, to configure + the endpoint_override for the block_storage service, the ``config`` + dict should contain:: + + 'block_storage_endpoint_override': 'http://...' + + To provide a default to be used if no service-specific override is + present, just use the unprefixed ${config_option} as the service + key, e.g.:: + + 'interface': 'public' """ def __init__(self, name=None, region_name=None, config=None, force_ipv4=False, auth_plugin=None, @@ -170,8 +186,12 @@ def __init__(self, name=None, region_name=None, config=None, statsd_host=None, statsd_port=None, statsd_prefix=None, collector_registry=None): self._name = name - self.region_name = region_name self.config = _util.normalize_keys(config) + # NOTE(efried): For backward compatibility: a) continue to accept the + # region_name kwarg; b) make it take precedence over (non-service_type- + # specific) region_name set in the config dict. + if region_name is not None: + self.config['region_name'] = region_name self._extra_config = extra_config or {} self.log = _log.setup_logging('openstack.config') self._force_ipv4 = force_ipv4 @@ -213,7 +233,8 @@ def __iter__(self): def __eq__(self, other): return ( self.name == other.name - and self.region_name == other.region_name + # Ew + and self.get_region_name() == other.get_region_name() and self.config == other.config) def __ne__(self, other): @@ -236,12 +257,13 @@ def full_name(self): Always returns a valid string. It will have name and region_name or just one of the two if only one is set, or else 'unknown'. """ - if self.name and self.region_name: - return ":".join([self.name, self.region_name]) - elif self.name and not self.region_name: + region_name = self.get_region_name() + if self.name and region_name: + return ":".join([self.name, region_name]) + elif self.name and not region_name: return self.name - elif not self.name and self.region_name: - return self.region_name + elif not self.name and region_name: + return region_name else: return 'unknown' @@ -335,6 +357,12 @@ def _get_service_config(self, key, service_type): if st in config_dict: return config_dict[st] + def get_region_name(self, service_type=None): + # If a region_name for the specific service_type is configured, use it; + # else use the one configured for the CloudRegion as a whole. + return self._get_config( + 'region_name', service_type, fallback_to_unprefixed=True) + def get_interface(self, service_type=None): return self._get_config( 'interface', service_type, fallback_to_unprefixed=True) @@ -523,12 +551,13 @@ def get_all_version_data(self, service_type): # Seriously. Don't think about the existential crisis # that is the next line. You'll wind up in cthulhu's lair. service_type = self.get_service_type(service_type) + region_name = self.get_region_name(service_type) versions = self.get_session().get_all_version_data( service_type=service_type, interface=self.get_interface(service_type), - region_name=self.region_name, + region_name=region_name, ) - region_versions = versions.get(self.region_name, {}) + region_versions = versions.get(region_name, {}) interface_versions = region_versions.get( self.get_interface(service_type), {}) return interface_versions.get(service_type, []) @@ -539,7 +568,7 @@ def _get_hardcoded_endpoint(self, service_type, constructor): service_type=self.get_service_type(service_type), service_name=self.get_service_name(service_type), interface=self.get_interface(service_type), - region_name=self.region_name, + region_name=self.get_region_name(service_type), ) endpoint = adapter.get_endpoint() if not endpoint.rstrip().rsplit('/')[-1] == 'v2.0': @@ -567,6 +596,7 @@ def get_session_client( """ version_request = self._get_version_request(service_type, version) + kwargs.setdefault('region_name', self.get_region_name(service_type)) kwargs.setdefault('connect_retries', self.get_connect_retries(service_type)) kwargs.setdefault('status_code_retries', @@ -599,7 +629,6 @@ def get_session_client( service_type=self.get_service_type(service_type), service_name=self.get_service_name(service_type), interface=self.get_interface(service_type), - region_name=self.region_name, version=version, min_version=min_api_version, max_version=max_api_version, @@ -666,6 +695,7 @@ def get_session_endpoint( if override_endpoint: return override_endpoint + region_name = self.get_region_name(service_type) service_name = self.get_service_name(service_type) interface = self.get_interface(service_type) session = self.get_session() @@ -680,7 +710,7 @@ def get_session_endpoint( # the request endpoint = session.get_endpoint( service_type=service_type, - region_name=self.region_name, + region_name=region_name, interface=interface, service_name=service_name, **version_kwargs @@ -695,7 +725,7 @@ def get_session_endpoint( service_type, service_name, interface, - self.region_name, + region_name, ) return endpoint diff --git a/openstack/service_description.py b/openstack/service_description.py index f154e92f8..d2292b4b4 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -178,20 +178,21 @@ def _make_proxy(self, instance): ) found_version = temp_adapter.get_api_major_version() if found_version is None: + region_name = instance.config.get_region_name(self.service_type) if version_kwargs: raise exceptions.NotSupported( "The {service_type} service for {cloud}:{region_name}" " exists but does not have any supported versions.".format( service_type=self.service_type, cloud=instance.name, - region_name=instance.config.region_name)) + region_name=region_name)) else: raise exceptions.NotSupported( "The {service_type} service for {cloud}:{region_name}" " exists but no version was discoverable.".format( service_type=self.service_type, cloud=instance.name, - region_name=instance.config.region_name)) + region_name=region_name)) proxy_class = self.supported_versions.get(str(found_version[0])) if not proxy_class: # Maybe openstacksdk is being used for the passthrough diff --git a/openstack/tests/unit/cloud/test_meta.py b/openstack/tests/unit/cloud/test_meta.py index c1ee49946..410d7cf71 100644 --- a/openstack/tests/unit/cloud/test_meta.py +++ b/openstack/tests/unit/cloud/test_meta.py @@ -25,7 +25,9 @@ class FakeConfig(object): - region_name = 'test-region' + def get_region_name(self, service_type=None): + # TODO(efried): Validate service_type? + return 'test-region' class FakeCloud(object): diff --git a/openstack/tests/unit/cloud/test_operator.py b/openstack/tests/unit/cloud/test_operator.py index 1dee45e0d..0c4c84fe2 100644 --- a/openstack/tests/unit/cloud/test_operator.py +++ b/openstack/tests/unit/cloud/test_operator.py @@ -74,7 +74,7 @@ def side_effect(*args, **kwargs): session_mock.get_endpoint.side_effect = side_effect get_session_mock.return_value = session_mock self.cloud.name = 'testcloud' - self.cloud.config.region_name = 'testregion' + self.cloud.config.config['region_name'] = 'testregion' with testtools.ExpectedException( exc.OpenStackCloudException, "Error getting image endpoint on testcloud:testregion:" diff --git a/openstack/tests/unit/config/test_cloud_config.py b/openstack/tests/unit/config/test_cloud_config.py index 5a8b9df2e..2bfcbcefa 100644 --- a/openstack/tests/unit/config/test_cloud_config.py +++ b/openstack/tests/unit/config/test_cloud_config.py @@ -181,6 +181,55 @@ def test_getters(self): self.assertEqual(1, cc.get_connect_retries('compute')) self.assertEqual(3, cc.get_connect_retries('baremetal')) + def test_get_region_name(self): + # TODO(efried): ddt this + + # No region_name kwarg, no regions specified in services dict + # (including the default). + cc = cloud_region.CloudRegion(config=fake_services_dict) + self.assertIsNone(cc.region_name) + self.assertIsNone(cc.get_region_name()) + self.assertIsNone(cc.get_region_name(service_type=None)) + self.assertIsNone(cc.get_region_name(service_type='compute')) + self.assertIsNone(cc.get_region_name(service_type='placement')) + + # Only region_name kwarg; it's returned for everything + cc = cloud_region.CloudRegion( + region_name='foo', config=fake_services_dict) + self.assertEqual('foo', cc.region_name) + self.assertEqual('foo', cc.get_region_name()) + self.assertEqual('foo', cc.get_region_name(service_type=None)) + self.assertEqual('foo', cc.get_region_name(service_type='compute')) + self.assertEqual('foo', cc.get_region_name(service_type='placement')) + + # No region_name kwarg; values (including default) show through from + # config dict + services_dict = dict( + fake_services_dict, + region_name='the-default', compute_region_name='compute-region') + cc = cloud_region.CloudRegion(config=services_dict) + self.assertEqual('the-default', cc.region_name) + self.assertEqual('the-default', cc.get_region_name()) + self.assertEqual('the-default', cc.get_region_name(service_type=None)) + self.assertEqual( + 'compute-region', cc.get_region_name(service_type='compute')) + self.assertEqual( + 'the-default', cc.get_region_name(service_type='placement')) + + # region_name kwarg overrides config dict default (for backward + # compatibility), but service-specific region_name takes precedence. + services_dict = dict( + fake_services_dict, + region_name='dict', compute_region_name='compute-region') + cc = cloud_region.CloudRegion( + region_name='kwarg', config=services_dict) + self.assertEqual('kwarg', cc.region_name) + self.assertEqual('kwarg', cc.get_region_name()) + self.assertEqual('kwarg', cc.get_region_name(service_type=None)) + self.assertEqual( + 'compute-region', cc.get_region_name(service_type='compute')) + self.assertEqual('kwarg', cc.get_region_name(service_type='placement')) + def test_aliases(self): services_dict = fake_services_dict.copy() services_dict['volume_api_version'] = 12 diff --git a/openstack/tests/unit/config/test_from_conf.py b/openstack/tests/unit/config/test_from_conf.py index 7308b9397..c60a48f84 100644 --- a/openstack/tests/unit/config/test_from_conf.py +++ b/openstack/tests/unit/config/test_from_conf.py @@ -56,13 +56,6 @@ def _get_conn(self): oslocfg, session=self.cloud.session, name='from_conf.example.com') self.assertEqual('from_conf.example.com', config.name) - # TODO(efried): Currently region_name gets set to the last value seen - # in the config, which is nondeterministic and surely incorrect. - # Sometimes that's SpecialRegion, but some tests use the base fixtures - # which have no compute endpoint in SpecialRegion. Force override for - # now to make those tests work. - config.region_name = None - return connection.Connection(config=config) def test_adapter_opts_set(self): @@ -94,8 +87,7 @@ def test_adapter_opts_set(self): ]) adap = conn.orchestration - # TODO(efried): Fix this when region_name behaves correctly. - # self.assertEqual('SpecialRegion', adap.region_name) + self.assertEqual('SpecialRegion', adap.region_name) self.assertEqual('orchestration', adap.service_type) self.assertEqual('internal', adap.interface) self.assertEqual('https://example.org:8888/heat/v2', From a5dfa85d4e7ff5bd5a40d37519670392f05ab6fe Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Tue, 4 Jun 2019 09:05:07 -0500 Subject: [PATCH 2467/3836] Get rid of unused _OpenStackCloudMixin.get_region This method was unused. Kill it. Change-Id: I2bd7c178ce321e408b10968183f8740196aa6903 --- openstack/cloud/openstackcloud.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 5f37ad929..899467799 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -650,10 +650,6 @@ def _get_and_munchify(self, key, data): def get_name(self): return self.name - def get_region(self): - # TODO(efried): This seems to be unused. Can we get rid of it? - return self.config.get_region_name() - def get_session_endpoint(self, service_key): try: return self.config.get_session_endpoint(service_key) From a7fbaba34cb2157403591b55f9ee8c9deca058e2 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 5 Jun 2019 09:12:05 +0200 Subject: [PATCH 2468/3836] Use Resource layer in cloud for SecurityGroups of server - add fetch_server_security_groups to the compute proxy - fix add/remove security_group of server to properly pass SG name and not ID according to docs - force usage of resource layer in cloud for respective methods Change-Id: I5c46169de436b9fe4df31a12f3a6246063a7d342 --- doc/source/user/proxies/compute.rst | 1 + openstack/cloud/_compute.py | 21 +- openstack/compute/v2/_proxy.py | 26 ++- openstack/compute/v2/server.py | 33 ++- openstack/tests/unit/cloud/test_meta.py | 197 +++++++++++------- .../tests/unit/cloud/test_security_groups.py | 10 +- openstack/tests/unit/compute/v2/test_proxy.py | 21 ++ .../tests/unit/compute/v2/test_server.py | 45 ++++ 8 files changed, 253 insertions(+), 101 deletions(-) diff --git a/doc/source/user/proxies/compute.rst b/doc/source/user/proxies/compute.rst index 0d29784f2..4ec84d13f 100644 --- a/doc/source/user/proxies/compute.rst +++ b/doc/source/user/proxies/compute.rst @@ -40,6 +40,7 @@ Network Actions .. automethod:: openstack.compute.v2._proxy.Proxy.remove_fixed_ip_from_server .. automethod:: openstack.compute.v2._proxy.Proxy.add_floating_ip_to_server .. automethod:: openstack.compute.v2._proxy.Proxy.remove_floating_ip_from_server + .. automethod:: openstack.compute.v2._proxy.Proxy.fetch_server_security_groups .. automethod:: openstack.compute.v2._proxy.Proxy.add_security_group_to_server .. automethod:: openstack.compute.v2._proxy.Proxy.remove_security_group_from_server diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index a883ad3c7..9a0d355f3 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -198,12 +198,11 @@ def list_server_security_groups(self, server): if not self._has_secgroups(): return [] - data = proxy._json_response( - self.compute.get( - '/servers/{server_id}/os-security-groups'.format( - server_id=server['id']))) - return self._normalize_secgroups( - self._get_and_munchify('security_groups', data)) + server = self.compute.get_server(server) + + server.fetch_security_groups(self.compute) + + return self._normalize_secgroups(server.security_groups) def _get_server_security_groups(self, server, security_groups): if not self._has_secgroups(): @@ -255,9 +254,7 @@ def add_server_security_groups(self, server, security_groups): return False for sg in security_groups: - proxy._json_response(self.compute.post( - '/servers/%s/action' % server['id'], - json={'addSecurityGroup': {'name': sg.name}})) + self.compute.add_security_group_to_server(server, sg) return True @@ -283,11 +280,9 @@ def remove_server_security_groups(self, server, security_groups): for sg in security_groups: try: - proxy._json_response(self.compute.post( - '/servers/%s/action' % server['id'], - json={'removeSecurityGroup': {'name': sg.name}})) + self.compute.remove_security_group_from_server(server, sg) - except exc.OpenStackCloudURINotFound: + except exceptions.ResourceNotFound: # NOTE(jamielennox): Is this ok? If we remove something that # isn't present should we just conclude job done or is that an # error? Nova returns ok if you try to add a group twice. diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 0e1434ea4..0fb493383 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -25,6 +25,7 @@ from openstack.compute.v2 import server_ip from openstack.compute.v2 import service as _service from openstack.compute.v2 import volume_attachment as _volume_attachment +from openstack.network.v2 import security_group as _sg from openstack import proxy from openstack import resource @@ -636,26 +637,37 @@ def create_server_image(self, server, name, metadata=None): server = self._get_resource(_server.Server, server) server.create_image(self, name, metadata) + def fetch_server_security_groups(self, server): + """Fetch security groups with details for a server. + + :param server: Either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance. + + :returns: updated :class:`~openstack.compute.v2.server.Server` instance + """ + server = self._get_resource(_server.Server, server) + return server.fetch_security_groups(self) + def add_security_group_to_server(self, server, security_group): """Add a security group to a server :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. - :param security_group: Either the ID of a security group or a + :class:`~openstack.compute.v2.server.Server` instance. + :param security_group: Either the ID, Name of a security group or a :class:`~openstack.network.v2.security_group.SecurityGroup` instance. :returns: None """ server = self._get_resource(_server.Server, server) - security_group_id = resource.Resource._get_id(security_group) - server.add_security_group(self, security_group_id) + security_group = self._get_resource(_sg.SecurityGroup, security_group) + server.add_security_group(self, security_group.name) def remove_security_group_from_server(self, server, security_group): """Remove a security group from a server :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :param security_group: Either the ID of a security group or a :class:`~openstack.network.v2.security_group.SecurityGroup` instance. @@ -663,8 +675,8 @@ def remove_security_group_from_server(self, server, security_group): :returns: None """ server = self._get_resource(_server.Server, server) - security_group_id = resource.Resource._get_id(security_group) - server.remove_security_group(self, security_group_id) + security_group = self._get_resource(_sg.SecurityGroup, security_group) + server.remove_security_group(self, security_group.name) def add_fixed_ip_to_server(self, server, network_id): """Adds a fixed IP address to a server instance. diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index d7198f493..99001e587 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -12,6 +12,7 @@ from openstack.compute.v2 import metadata from openstack.image.v2 import image +from openstack import exceptions from openstack import resource from openstack import utils @@ -165,7 +166,8 @@ class Server(resource.Resource, metadata.MetadataMixin, resource.TagMixin): scheduler_hints = resource.Body('OS-SCH-HNT:scheduler_hints', type=dict) #: A list of applicable security groups. Each group contains keys for #: description, name, id, and rules. - security_groups = resource.Body('security_groups') + security_groups = resource.Body('security_groups', + type=list, list_type=dict) #: The UUIDs of the server groups to which the server belongs. #: Currently this can contain at most one entry. server_groups = resource.Body('server_groups', type=list, list_type=dict) @@ -305,12 +307,12 @@ def create_image(self, session, name, metadata=None): body = {'createImage': action} self._action(session, body) - def add_security_group(self, session, security_group): - body = {"addSecurityGroup": {"name": security_group}} + def add_security_group(self, session, security_group_name): + body = {"addSecurityGroup": {"name": security_group_name}} self._action(session, body) - def remove_security_group(self, session, security_group): - body = {"removeSecurityGroup": {"name": security_group}} + def remove_security_group(self, session, security_group_name): + body = {"removeSecurityGroup": {"name": security_group_name}} self._action(session, body) def reset_state(self, session, state): @@ -494,5 +496,26 @@ def _live_migrate(self, session, host, force, block_migration, self._action( session, {'os-migrateLive': body}, microversion=microversion) + def fetch_security_groups(self, session): + """Fetch security groups of a server. + + :returns: Updated Server instance. + + """ + url = utils.urljoin(Server.base_path, self.id, 'os-security-groups') + + response = session.get(url) + + exceptions.raise_from_response(response) + + try: + data = response.json() + if 'security_groups' in data: + self.security_groups = data['security_groups'] + except ValueError: + pass + + return self + ServerDetail = Server diff --git a/openstack/tests/unit/cloud/test_meta.py b/openstack/tests/unit/cloud/test_meta.py index c1ee49946..484767465 100644 --- a/openstack/tests/unit/cloud/test_meta.py +++ b/openstack/tests/unit/cloud/test_meta.py @@ -367,6 +367,20 @@ def test_get_server_private_ip_devstack( mock_get_volumes.return_value = [] mock_has_service.return_value = True + fake_server = fakes.make_fake_server( + server_id='test-id', name='test-name', status='ACTIVE', + flavor={u'id': u'1'}, + image={ + 'name': u'cirros-0.3.4-x86_64-uec', + u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1'}, + addresses={u'test_pnztt_net': [{ + u'OS-EXT-IPS:type': u'fixed', + u'addr': PRIVATE_V4, + u'version': 4, + u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:ae:7d:42' + }]} + ) + self.register_uris([ dict(method='GET', uri=('https://network.example.com/v2.0/ports.json?' @@ -395,25 +409,19 @@ def test_get_server_private_ip_devstack( uri='https://network.example.com/v2.0/subnets.json', json={'subnets': SUBNETS_WITH_NAT}), + self.get_nova_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', fake_server['id']]), + json=fake_server), dict(method='GET', uri='{endpoint}/servers/test-id/os-security-groups'.format( endpoint=fakes.COMPUTE_ENDPOINT), json={'security_groups': []}) ]) - srv = self.cloud.get_openstack_vars(fakes.make_fake_server( - server_id='test-id', name='test-name', status='ACTIVE', - flavor={u'id': u'1'}, - image={ - 'name': u'cirros-0.3.4-x86_64-uec', - u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1'}, - addresses={u'test_pnztt_net': [{ - u'OS-EXT-IPS:type': u'fixed', - u'addr': PRIVATE_V4, - u'version': 4, - u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:ae:7d:42' - }]} - )) + srv = self.cloud.get_openstack_vars(fake_server) self.assertEqual(PRIVATE_V4, srv['private_v4']) self.assert_calls() @@ -431,6 +439,20 @@ def test_get_server_private_ip_no_fip( mock_get_flavor_name.return_value = 'm1.tiny' mock_get_volumes.return_value = [] + fake_server = fakes.make_fake_server( + server_id='test-id', name='test-name', status='ACTIVE', + flavor={u'id': u'1'}, + image={ + 'name': u'cirros-0.3.4-x86_64-uec', + u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1'}, + addresses={u'test_pnztt_net': [{ + u'OS-EXT-IPS:type': u'fixed', + u'addr': PRIVATE_V4, + u'version': 4, + u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:ae:7d:42' + }]} + ) + self.register_uris([ dict(method='GET', uri='https://network.example.com/v2.0/networks.json', @@ -445,25 +467,19 @@ def test_get_server_private_ip_no_fip( dict(method='GET', uri='https://network.example.com/v2.0/subnets.json', json={'subnets': SUBNETS_WITH_NAT}), + self.get_nova_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', fake_server['id']]), + json=fake_server), dict(method='GET', uri='{endpoint}/servers/test-id/os-security-groups'.format( endpoint=fakes.COMPUTE_ENDPOINT), json={'security_groups': []}) ]) - srv = self.cloud.get_openstack_vars(fakes.make_fake_server( - server_id='test-id', name='test-name', status='ACTIVE', - flavor={u'id': u'1'}, - image={ - 'name': u'cirros-0.3.4-x86_64-uec', - u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1'}, - addresses={u'test_pnztt_net': [{ - u'OS-EXT-IPS:type': u'fixed', - u'addr': PRIVATE_V4, - u'version': 4, - u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:ae:7d:42' - }]} - )) + srv = self.cloud.get_openstack_vars(fake_server) self.assertEqual(PRIVATE_V4, srv['private_v4']) self.assert_calls() @@ -479,6 +495,19 @@ def test_get_server_cloud_no_fips( mock_get_image_name.return_value = 'cirros-0.3.4-x86_64-uec' mock_get_flavor_name.return_value = 'm1.tiny' mock_get_volumes.return_value = [] + + fake_server = fakes.make_fake_server( + server_id='test-id', name='test-name', status='ACTIVE', + flavor={u'id': u'1'}, + image={ + 'name': u'cirros-0.3.4-x86_64-uec', + u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1'}, + addresses={u'test_pnztt_net': [{ + u'addr': PRIVATE_V4, + u'version': 4, + }]} + ) + self.register_uris([ dict(method='GET', uri='https://network.example.com/v2.0/networks.json', @@ -495,23 +524,19 @@ def test_get_server_cloud_no_fips( dict(method='GET', uri='https://network.example.com/v2.0/subnets.json', json={'subnets': SUBNETS_WITH_NAT}), + self.get_nova_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', fake_server['id']]), + json=fake_server), dict(method='GET', uri='{endpoint}/servers/test-id/os-security-groups'.format( endpoint=fakes.COMPUTE_ENDPOINT), json={'security_groups': []}) ]) - srv = self.cloud.get_openstack_vars(fakes.make_fake_server( - server_id='test-id', name='test-name', status='ACTIVE', - flavor={u'id': u'1'}, - image={ - 'name': u'cirros-0.3.4-x86_64-uec', - u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1'}, - addresses={u'test_pnztt_net': [{ - u'addr': PRIVATE_V4, - u'version': 4, - }]} - )) + srv = self.cloud.get_openstack_vars(fake_server) self.assertEqual(PRIVATE_V4, srv['private_v4']) self.assert_calls() @@ -529,7 +554,21 @@ def test_get_server_cloud_missing_fips( mock_get_volumes.return_value = [] mock_has_service.return_value = True + fake_server = fakes.make_fake_server( + server_id='test-id', name='test-name', status='ACTIVE', + flavor={u'id': u'1'}, + image={ + 'name': u'cirros-0.3.4-x86_64-uec', + u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1'}, + addresses={u'test_pnztt_net': [{ + u'addr': PRIVATE_V4, + u'version': 4, + 'OS-EXT-IPS-MAC:mac_addr': 'fa:16:3e:ae:7d:42', + }]} + ) + self.register_uris([ + # self.get_nova_discovery_mock_dict(), dict(method='GET', uri=('https://network.example.com/v2.0/ports.json?' 'device_id=test-id'), @@ -563,24 +602,19 @@ def test_get_server_cloud_missing_fips( dict(method='GET', uri='https://network.example.com/v2.0/subnets.json', json={'subnets': SUBNETS_WITH_NAT}), + self.get_nova_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', fake_server['id']]), + json=fake_server), dict(method='GET', uri='{endpoint}/servers/test-id/os-security-groups'.format( endpoint=fakes.COMPUTE_ENDPOINT), json={'security_groups': []}) ]) - srv = self.cloud.get_openstack_vars(fakes.make_fake_server( - server_id='test-id', name='test-name', status='ACTIVE', - flavor={u'id': u'1'}, - image={ - 'name': u'cirros-0.3.4-x86_64-uec', - u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1'}, - addresses={u'test_pnztt_net': [{ - u'addr': PRIVATE_V4, - u'version': 4, - 'OS-EXT-IPS-MAC:mac_addr': 'fa:16:3e:ae:7d:42', - }]} - )) + srv = self.cloud.get_openstack_vars(fake_server) self.assertEqual(PUBLIC_V4, srv['public_v4']) self.assert_calls() @@ -598,15 +632,7 @@ def test_get_server_cloud_rackspace_v6( mock_get_image_name.return_value = 'cirros-0.3.4-x86_64-uec' mock_get_flavor_name.return_value = 'm1.tiny' mock_get_volumes.return_value = [] - - self.register_uris([ - dict(method='GET', - uri='{endpoint}/servers/test-id/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_groups': []}) - ]) - - srv = self.cloud.get_openstack_vars(fakes.make_fake_server( + fake_server = fakes.make_fake_server( server_id='test-id', name='test-name', status='ACTIVE', flavor={u'id': u'1'}, image={ @@ -625,7 +651,22 @@ def test_get_server_cloud_rackspace_v6( 'version': 6 }] } - )) + ) + + self.register_uris([ + self.get_nova_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', fake_server['id']]), + json=fake_server), + dict(method='GET', + uri='{endpoint}/servers/test-id/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'security_groups': []}) + ]) + + srv = self.cloud.get_openstack_vars(fake_server) self.assertEqual("10.223.160.141", srv['private_v4']) self.assertEqual("104.130.246.91", srv['public_v4']) @@ -652,20 +693,7 @@ def test_get_server_cloud_osic_split( mock_get_flavor_name.return_value = 'm1.tiny' mock_get_volumes.return_value = [] - self.register_uris([ - dict(method='GET', - uri='https://network.example.com/v2.0/networks.json', - json={'networks': OSIC_NETWORKS}), - dict(method='GET', - uri='https://network.example.com/v2.0/subnets.json', - json={'subnets': OSIC_SUBNETS}), - dict(method='GET', - uri='{endpoint}/servers/test-id/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_groups': []}) - ]) - - srv = self.cloud.get_openstack_vars(fakes.make_fake_server( + fake_server = fakes.make_fake_server( server_id='test-id', name='test-name', status='ACTIVE', flavor={u'id': u'1'}, image={ @@ -684,7 +712,28 @@ def test_get_server_cloud_osic_split( 'version': 6 }] } - )) + ) + + self.register_uris([ + dict(method='GET', + uri='https://network.example.com/v2.0/networks.json', + json={'networks': OSIC_NETWORKS}), + dict(method='GET', + uri='https://network.example.com/v2.0/subnets.json', + json={'subnets': OSIC_SUBNETS}), + self.get_nova_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', fake_server['id']]), + json=fake_server), + dict(method='GET', + uri='{endpoint}/servers/test-id/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json={'security_groups': []}) + ]) + + srv = self.cloud.get_openstack_vars(fake_server) self.assertEqual("10.223.160.141", srv['private_v4']) self.assertEqual("104.130.246.91", srv['public_v4']) diff --git a/openstack/tests/unit/cloud/test_security_groups.py b/openstack/tests/unit/cloud/test_security_groups.py index 3a27e70e8..d17214b38 100644 --- a/openstack/tests/unit/cloud/test_security_groups.py +++ b/openstack/tests/unit/cloud/test_security_groups.py @@ -548,14 +548,20 @@ def test_nova_egress_security_group_rule(self): def test_list_server_security_groups_nova(self): self.has_neutron = False - server = dict(id='server_id') + server = fakes.make_fake_server('1234', 'server-name', 'ACTIVE') self.register_uris([ + self.get_nova_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', server['id']]), + json=server), dict( method='GET', uri='{endpoint}/servers/{id}/os-security-groups'.format( endpoint=fakes.COMPUTE_ENDPOINT, - id='server_id'), + id=server['id']), json={'security_groups': [nova_grp_dict]}), ]) groups = self.cloud.list_server_security_groups(server) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index b9b8694f3..17f282d83 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -515,3 +515,24 @@ def test_live_migrate_server(self): method_args=["value", "host1", False], expected_args=["host1"], expected_kwargs={'force': False, 'block_migration': None}) + + def test_fetch_security_groups(self): + self._verify( + 'openstack.compute.v2.server.Server.fetch_security_groups', + self.proxy.fetch_server_security_groups, + method_args=["value"], + expected_args=[]) + + def test_add_security_groups(self): + self._verify( + 'openstack.compute.v2.server.Server.add_security_group', + self.proxy.add_security_group_to_server, + method_args=["value", {'id': 'id', 'name': 'sg'}], + expected_args=['sg']) + + def test_remove_security_groups(self): + self._verify( + 'openstack.compute.v2.server.Server.remove_security_group', + self.proxy.remove_security_group_from_server, + method_args=["value", {'id': 'id', 'name': 'sg'}], + expected_args=['sg']) diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index b00601cd5..854867c37 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -883,3 +883,48 @@ class FakeEndpointData(object): headers = {'Accept': ''} self.sess.post.assert_called_with( url, json=body, headers=headers, microversion='2.30') + + def test_get_security_groups(self): + sot = server.Server(**EXAMPLE) + + response = mock.Mock() + + sgs = [{ + 'description': 'default', + 'id': 1, + 'name': 'default', + 'rules': [ + { + 'direction': 'egress', + 'ethertype': 'IPv6', + 'id': '3c0e45ff-adaf-4124-b083-bf390e5482ff', + 'port_range_max': None, + 'port_range_min': None, + 'protocol': None, + 'remote_group_id': None, + 'remote_ip_prefix': None, + 'security_group_id': '1', + 'project_id': 'e4f50856753b4dc6afee5fa6b9b6c550', + 'revision_number': 1, + 'tags': ['tag1,tag2'], + 'tenant_id': 'e4f50856753b4dc6afee5fa6b9b6c550', + 'created_at': '2018-03-19T19:16:56Z', + 'updated_at': '2018-03-19T19:16:56Z', + 'description': '' + } + ], + 'tenant_id': 'e4f50856753b4dc6afee5fa6b9b6c550' + }] + + response.status_code = 200 + response.json.return_value = { + 'security_groups': sgs + } + self.sess.get.return_value = response + + sot.fetch_security_groups(self.sess) + + url = 'servers/IDENTIFIER/os-security-groups' + self.sess.get.assert_called_with(url) + + self.assertEqual(sot.security_groups, sgs) From 40cc2cc564ca2a86da6f029e7e21abacf98ba1c2 Mon Sep 17 00:00:00 2001 From: Charlie Date: Thu, 6 Jun 2019 01:40:35 +1000 Subject: [PATCH 2469/3836] Support deleting all routes in update_router There was previously no way to remove all routes from a router with the update_router function because it only processed the routes if the statement 'if routes' evaluated to True, which an empty list did not. Modified the unit test for cloud/test_router so that it initialises the test router with routes, and confirms that they are removed. Change-Id: I9f31ad33eb500f59eb7634ea9e7e6502362a375b Story: 2005089 Task: 29685 --- openstack/cloud/_network.py | 6 +++-- openstack/tests/unit/cloud/test_router.py | 33 ++++++++++++++++++++--- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index e0f938b77..d36d90bba 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -2050,7 +2050,9 @@ def update_router(self, name_or_id, name=None, admin_state_up=None, } ] :param list routes: - A list of dictionaries with destination and nexthop parameters. + A list of dictionaries with destination and nexthop parameters. To + clear all routes pass an empty list ([]). + Example:: [ @@ -2073,7 +2075,7 @@ def update_router(self, name_or_id, name=None, admin_state_up=None, if ext_gw_info: router['external_gateway_info'] = ext_gw_info - if routes: + if routes is not None: if self._has_neutron_extension('extraroute'): router['routes'] = routes else: diff --git a/openstack/tests/unit/cloud/test_router.py b/openstack/tests/unit/cloud/test_router.py index 9ba183087..5a26d4943 100644 --- a/openstack/tests/unit/cloud/test_router.py +++ b/openstack/tests/unit/cloud/test_router.py @@ -38,7 +38,12 @@ class TestRouter(base.TestCase): 'id': router_id, 'name': router_name, 'project_id': u'861808a93da0484ea1767967c4df8a23', - 'routes': [], + 'routes': [ + { + "destination": "179.24.1.0/24", + "nexthop": "172.24.3.99" + } + ], 'status': u'ACTIVE', 'tenant_id': u'861808a93da0484ea1767967c4df8a23' } @@ -61,7 +66,17 @@ class TestRouter(base.TestCase): "name": "Router Availability Zone" } - enabled_neutron_extensions = [router_availability_zone_extension] + router_extraroute_extension = { + "alias": "extraroute", + "updated": "2015-01-01T10:00:00-00:00", + "description": "extra routes extension for router.", + "links": [], + "name": "Extra Routes" + } + + enabled_neutron_extensions = [ + router_availability_zone_extension, + router_extraroute_extension] def test_get_router(self): self.register_uris([ @@ -240,9 +255,18 @@ def test_remove_router_interface_missing_argument(self): def test_update_router(self): new_router_name = "mickey" + new_routes = [] expected_router_rep = copy.copy(self.mock_router_rep) expected_router_rep['name'] = new_router_name + expected_router_rep['routes'] = new_routes + # validate_calls() asserts that these requests are done in order, + # but the extensions call is only called if a non-None value is + # passed in 'routes' self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions.json']), + json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'routers.json']), @@ -254,10 +278,11 @@ def test_update_router(self): json={'router': expected_router_rep}, validate=dict( json={'router': { - 'name': new_router_name}})) + 'name': new_router_name, + 'routes': new_routes}})) ]) new_router = self.cloud.update_router( - self.router_id, name=new_router_name) + self.router_id, name=new_router_name, routes=new_routes) self.assertDictEqual(expected_router_rep, new_router) self.assert_calls() From 820790225cdcb078604c2ec6dab20bd6d2761496 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Wed, 5 Jun 2019 16:19:01 -0500 Subject: [PATCH 2470/3836] Handle oslo.config exceptions in from_conf In the following cases, we used to happily allow the Connection to the service, using default Adapter settings: - If the conf section for a given service is missing, or present but without ksa adapter opts registered. - If the conf section for a given service has bogus values. Now for these scenarios, we disable that service with a helpful reason. The service disabling is supported at a broader level: If someone sets has_{service_type} to false in their config, we remove the adapter for that service and replace it with something that throws errors when people try to use it. They can optionally set {service_type}_disabled_reason to make the message more informative. Co-Authored-By: Monty Taylor Change-Id: I3aa1f1633790e6e958bbc510ac5e5a11c0c27a9f --- lower-constraints.txt | 2 +- openstack/cloud/openstackcloud.py | 2 +- openstack/config/cloud_region.py | 59 +++++++++++++++++-- openstack/connection.py | 1 + openstack/exceptions.py | 4 ++ openstack/service_description.py | 17 ++++++ openstack/tests/unit/config/test_from_conf.py | 54 +++++++++++------ requirements.txt | 2 +- 8 files changed, 115 insertions(+), 26 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index affeed9e7..a8d5d7dfe 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -21,7 +21,7 @@ mox3==0.20.0 munch==2.1.0 netifaces==0.10.4 os-client-config==1.28.0 -os-service-types==1.2.0 +os-service-types==1.6.0 oslo.config==6.1.0 oslotest==3.2.0 pbr==2.0.0 diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 899467799..f48ce13e5 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -670,7 +670,7 @@ def get_session_endpoint(self, service_key): return endpoint def has_service(self, service_key): - if not self.config.config.get('has_%s' % service_key, True): + if not self.config.has_service(service_key): # TODO(mordred) add a stamp here so that we only report this once if not (service_key in self._disable_warnings and self._disable_warnings[service_key]): diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 766a3b901..99a85033a 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -39,6 +39,9 @@ from openstack import exceptions from openstack import proxy + +_logger = _log.setup_logging('openstack') + SCOPE_KEYS = { 'domain_id', 'domain_name', 'project_id', 'project_name', @@ -58,6 +61,15 @@ def _make_key(key, service_type): return "_".join([service_type, key]) +def _disable_service(config, service_type, reason=None): + service_type = service_type.lower().replace('-', '_') + key = 'has_{service_type}'.format(service_type=service_type) + config[key] = False + if reason: + d_key = _make_key('disabled_reason', service_type) + config[d_key] = reason + + def _get_implied_microversion(version): if not version: return @@ -130,18 +142,38 @@ def from_conf(conf, session=None, **kwargs): for st in stm.all_types_by_service_type: project_name = stm.get_project_name(st) if project_name not in conf: + _disable_service( + config_dict, st, + reason="No section for project '{project}' (service type " + "'{service_type}') was present in the config.".format( + project=project_name, service_type=st)) continue opt_dict = {} # Populate opt_dict with (appropriately processed) Adapter conf opts try: ks_load_adap.process_conf_options(conf[project_name], opt_dict) - except Exception: - # NOTE(efried): This is for oslo_config.cfg.NoSuchOptError, but we - # don't want to drag in oslo.config just for that. + except Exception as e: + # NOTE(efried): This is for (at least) a couple of scenarios: + # (1) oslo_config.cfg.NoSuchOptError when ksa adapter opts are not + # registered in this section. + # (2) TypeError, when opts are registered but bogus (e.g. + # 'interface' and 'valid_interfaces' are both present). + # We may want to consider (providing a kwarg giving the caller the + # option of) blowing up right away for (2) rather than letting them + # get all the way to the point of trying the service and having + # *that* blow up. + reason = ("Encountered an exception attempting to process config " + "for project '{project}' (service type " + "'{service_type}'): {exception}".format( + project=project_name, service_type=st, exception=e)) + _logger.warn("Disabling service '{service_type}'.".format( + service_type=st)) + _logger.warn(reason) + _disable_service(config_dict, st, reason=reason) continue # Load them into config_dict under keys prefixed by ${service_type}_ for raw_name, opt_val in opt_dict.items(): - config_name = '_'.join([st, raw_name]) + config_name = _make_key(raw_name, st) config_dict[config_name] = opt_val return CloudRegion( session=session, config=config_dict, **kwargs) @@ -929,3 +961,22 @@ def get_prometheus_counter(self): ) registry._openstacksdk_counter = counter return counter + + def has_service(self, service_type): + service_type = service_type.lower().replace('-', '_') + key = 'has_{service_type}'.format(service_type=service_type) + return self.config.get( + key, self._service_type_manager.is_official(service_type)) + + def disable_service(self, service_type, reason=None): + _disable_service(self.config, service_type, reason=reason) + + def enable_service(self, service_type): + service_type = service_type.lower().replace('-', '_') + key = 'has_{service_type}'.format(service_type=service_type) + self.config[key] = True + + def get_disabled_reason(self, service_type): + service_type = service_type.lower().replace('-', '_') + d_key = _make_key('disabled_reason', service_type) + return self.config.get(d_key) diff --git a/openstack/connection.py b/openstack/connection.py index f4fed899a..c79d84587 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -446,6 +446,7 @@ def getter(self): attr_name.replace('-', '_'), property(fget=getter) ) + self.config.enable_service(attr_name) def authorize(self): """Authorize this Connection diff --git a/openstack/exceptions.py b/openstack/exceptions.py index 8fa9637d9..a267c2c9f 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -253,3 +253,7 @@ class ValidationException(SDKException): class TaskManagerStopped(SDKException): """Operations were attempted on a stopped TaskManager.""" + + +class ServiceDisabledException(ConfigException): + """This service is disabled for reasons.""" diff --git a/openstack/service_description.py b/openstack/service_description.py index d2292b4b4..87acc909f 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -26,6 +26,18 @@ _service_type_manager = os_service_types.ServiceTypes() +class _ServiceDisabledProxyShim(object): + def __init__(self, service_type, reason): + self.service_type = service_type + self.reason = reason + + def __getattr__(self, item): + raise exceptions.ServiceDisabledException( + "Service '{service_type}' is disabled because its configuration " + "could not be loaded. {reason}".format( + service_type=self.service_type, reason=self.reason or '')) + + class ServiceDescription(object): #: Dictionary of supported versions and proxy classes for that version @@ -83,6 +95,11 @@ def _make_proxy(self, instance): """ config = instance.config + if not config.has_service(self.service_type): + return _ServiceDisabledProxyShim( + self.service_type, + config.get_disabled_reason(self.service_type)) + # First, check to see if we've got config that matches what we # understand in the SDK. version_string = config.get_api_version(self.service_type) diff --git a/openstack/tests/unit/config/test_from_conf.py b/openstack/tests/unit/config/test_from_conf.py index c60a48f84..4f83b5f10 100644 --- a/openstack/tests/unit/config/test_from_conf.py +++ b/openstack/tests/unit/config/test_from_conf.py @@ -30,8 +30,8 @@ def setUp(self): self.oslo_config_dict = { # All defaults for nova 'nova': {}, - # monasca not in the service catalog - 'monasca': {}, + # monasca-api not in the service catalog + 'monasca-api': {}, # Overrides for heat 'heat': { 'region_name': 'SpecialRegion', @@ -43,6 +43,7 @@ def setUp(self): def _load_ks_cfg_opts(self): conf = cfg.ConfigOpts() for group, opts in self.oslo_config_dict.items(): + conf.register_group(cfg.OptGroup(group)) if opts is not None: ks_loading.register_adapter_conf_options(conf, group) for name, val in opts.items(): @@ -122,27 +123,42 @@ def test_default_adapter_opts(self): self.assertEqual(s.name, server_name) self.assert_calls() - def test_no_adapter_opts(self): - """Adapter opts for service type not registered.""" - del self.oslo_config_dict['heat'] + def _test_missing_invalid_permutations(self, expected_reason): + # Do special things to self.oslo_config_dict['heat'] before calling + # this method. conn = self._get_conn() - # TODO(efried): This works, even though adapter opts are not - # registered. Should it? adap = conn.orchestration - self.assertIsNone(adap.region_name) - self.assertEqual('orchestration', adap.service_type) - self.assertEqual('public', adap.interface) - self.assertIsNone(adap.endpoint_override) + ex = self.assertRaises( + exceptions.ServiceDisabledException, getattr, adap, 'get') + self.assertIn("Service 'orchestration' is disabled because its " + "configuration could not be loaded.", ex.message) + self.assertIn(expected_reason, ex.message) + + def test_no_such_conf_section(self): + """No conf section (therefore no adapter opts) for service type.""" + del self.oslo_config_dict['heat'] + self._test_missing_invalid_permutations( + "No section for project 'heat' (service type 'orchestration') was " + "present in the config.") - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'orchestration', append=['foo']), - json={'foo': {}}) - ]) - adap.get('/foo') - self.assert_calls() + def test_no_adapter_opts(self): + """Conf section present, but opts for service type not registered.""" + self.oslo_config_dict['heat'] = None + self._test_missing_invalid_permutations( + "Encountered an exception attempting to process config for " + "project 'heat' (service type 'orchestration'): no such option") + + def test_invalid_adapter_opts(self): + """Adapter opts are bogus, in exception-raising ways.""" + self.oslo_config_dict['heat'] = { + 'interface': 'public', + 'valid_interfaces': 'private', + } + self._test_missing_invalid_permutations( + "Encountered an exception attempting to process config for " + "project 'heat' (service type 'orchestration'): interface and " + "valid_interfaces are mutually exclusive.") def test_no_session(self): # TODO(efried): Currently calling without a Session is not implemented. diff --git a/requirements.txt b/requirements.txt index 9062efb83..4e188856e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ appdirs>=1.3.0 # MIT License requestsexceptions>=1.2.0 # Apache-2.0 jsonpatch!=1.20,>=1.16 # BSD six>=1.10.0 # MIT -os-service-types>=1.2.0 # Apache-2.0 +os-service-types>=1.6.0 # Apache-2.0 keystoneauth1>=3.14.0 # Apache-2.0 munch>=2.1.0 # MIT From 879f7d4f12216710defc4cb5ffe0e9f2c00cda64 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Thu, 6 Jun 2019 09:28:48 -0500 Subject: [PATCH 2471/3836] Minor fixups from from_conf changes Addresses the following nonblocking nits from previous patches: - Consolidate warning logs when disabling service due to exception when processing oslo.config ksa settings [1]. - Move a TODO out of a docstring [2]. - Remove a now-redundant region_name comparison in CloudRegion.__eq__ [3]. - DRY the test_get_region_name unit test [4]. [1] https://review.opendev.org/#/c/663439/3/openstack/config/cloud_region.py@171 [2] https://review.opendev.org/#/c/662865/4/openstack/config/cloud_region.py@156 [3] https://review.opendev.org/#/c/662865/4/openstack/config/cloud_region.py@237 [4] https://review.opendev.org/#/c/662865/4/openstack/tests/unit/config/test_cloud_config.py@185 Change-Id: I1c140677347cf40db11e75f6a868356300d85071 --- openstack/config/cloud_region.py | 10 ++--- .../tests/unit/config/test_cloud_config.py | 37 +++++++------------ 2 files changed, 16 insertions(+), 31 deletions(-) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 99a85033a..abce4e97b 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -166,9 +166,8 @@ def from_conf(conf, session=None, **kwargs): "for project '{project}' (service type " "'{service_type}'): {exception}".format( project=project_name, service_type=st, exception=e)) - _logger.warn("Disabling service '{service_type}'.".format( - service_type=st)) - _logger.warn(reason) + _logger.warn("Disabling service '{service_type}': {reason}".format( + service_type=st, reason=reason)) _disable_service(config_dict, st, reason=reason) continue # Load them into config_dict under keys prefixed by ${service_type}_ @@ -180,13 +179,12 @@ def from_conf(conf, session=None, **kwargs): class CloudRegion(object): + # TODO(efried): Doc the rest of the kwargs """The configuration for a Region of an OpenStack Cloud. A CloudRegion encapsulates the config information needed for connections to all of the services in a Region of a Cloud. - TODO(efried): Doc the rest of the kwargs - :param str region_name: The default region name for all services in this CloudRegion. If both ``region_name`` and ``config['region_name'] are specified, the @@ -265,8 +263,6 @@ def __iter__(self): def __eq__(self, other): return ( self.name == other.name - # Ew - and self.get_region_name() == other.get_region_name() and self.config == other.config) def __ne__(self, other): diff --git a/openstack/tests/unit/config/test_cloud_config.py b/openstack/tests/unit/config/test_cloud_config.py index 2bfcbcefa..718450217 100644 --- a/openstack/tests/unit/config/test_cloud_config.py +++ b/openstack/tests/unit/config/test_cloud_config.py @@ -182,25 +182,25 @@ def test_getters(self): self.assertEqual(3, cc.get_connect_retries('baremetal')) def test_get_region_name(self): - # TODO(efried): ddt this + + def assert_region_name(default, compute): + self.assertEqual(default, cc.region_name) + self.assertEqual(default, cc.get_region_name()) + self.assertEqual(default, cc.get_region_name(service_type=None)) + self.assertEqual( + compute, cc.get_region_name(service_type='compute')) + self.assertEqual( + default, cc.get_region_name(service_type='placement')) # No region_name kwarg, no regions specified in services dict # (including the default). cc = cloud_region.CloudRegion(config=fake_services_dict) - self.assertIsNone(cc.region_name) - self.assertIsNone(cc.get_region_name()) - self.assertIsNone(cc.get_region_name(service_type=None)) - self.assertIsNone(cc.get_region_name(service_type='compute')) - self.assertIsNone(cc.get_region_name(service_type='placement')) + assert_region_name(None, None) # Only region_name kwarg; it's returned for everything cc = cloud_region.CloudRegion( region_name='foo', config=fake_services_dict) - self.assertEqual('foo', cc.region_name) - self.assertEqual('foo', cc.get_region_name()) - self.assertEqual('foo', cc.get_region_name(service_type=None)) - self.assertEqual('foo', cc.get_region_name(service_type='compute')) - self.assertEqual('foo', cc.get_region_name(service_type='placement')) + assert_region_name('foo', 'foo') # No region_name kwarg; values (including default) show through from # config dict @@ -208,13 +208,7 @@ def test_get_region_name(self): fake_services_dict, region_name='the-default', compute_region_name='compute-region') cc = cloud_region.CloudRegion(config=services_dict) - self.assertEqual('the-default', cc.region_name) - self.assertEqual('the-default', cc.get_region_name()) - self.assertEqual('the-default', cc.get_region_name(service_type=None)) - self.assertEqual( - 'compute-region', cc.get_region_name(service_type='compute')) - self.assertEqual( - 'the-default', cc.get_region_name(service_type='placement')) + assert_region_name('the-default', 'compute-region') # region_name kwarg overrides config dict default (for backward # compatibility), but service-specific region_name takes precedence. @@ -223,12 +217,7 @@ def test_get_region_name(self): region_name='dict', compute_region_name='compute-region') cc = cloud_region.CloudRegion( region_name='kwarg', config=services_dict) - self.assertEqual('kwarg', cc.region_name) - self.assertEqual('kwarg', cc.get_region_name()) - self.assertEqual('kwarg', cc.get_region_name(service_type=None)) - self.assertEqual( - 'compute-region', cc.get_region_name(service_type='compute')) - self.assertEqual('kwarg', cc.get_region_name(service_type='placement')) + assert_region_name('kwarg', 'compute-region') def test_aliases(self): services_dict = fake_services_dict.copy() From 3bac4abbda487347964bbadc4315b0693fd961b0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 6 Jun 2019 09:39:34 -0500 Subject: [PATCH 2472/3836] Add release note for new disable service functionality The behavior of disabling a service has changed slightly. Change-Id: If7ee3aed6696308d15932c31f2ffb14486b30889 --- .../notes/disable-service-39df96ef8a817785.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 releasenotes/notes/disable-service-39df96ef8a817785.yaml diff --git a/releasenotes/notes/disable-service-39df96ef8a817785.yaml b/releasenotes/notes/disable-service-39df96ef8a817785.yaml new file mode 100644 index 000000000..aab0d5824 --- /dev/null +++ b/releasenotes/notes/disable-service-39df96ef8a817785.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + ``has_{service_type}`` is a boolean config option that allows + asserting that a given service does not exist or should not be used + in a given cloud. Doing this will now cause the corresponding + service ``Proxy`` object to not be created and in its place is + an object that will throw exceptions if used. + - | + ``{service_type}_disabled_reason`` is a new string config option + that can be set to indicate a reason why a service has been disabled. + This string will be used in exceptions or log warnings emitted. From f7860861a12931d3fe139d80041ef9997a590316 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Thu, 6 Jun 2019 12:46:25 -0500 Subject: [PATCH 2473/3836] Pin to latest os-service-types OpenstackSDK should always be using the latest os-service-types. This commit bumps the lower bounds to the current latest version (1.7.0), and adds a canary test so that the CI will fail as soon as a new os-service-types version hits upper-constraints. Change-Id: Ie98ec9cfd58badc2b05a2a144d1d559d7ed6553b --- lower-constraints.txt | 2 +- openstack/tests/unit/test_utils.py | 11 +++++++++++ requirements.txt | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index a8d5d7dfe..60a322a5c 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -21,7 +21,7 @@ mox3==0.20.0 munch==2.1.0 netifaces==0.10.4 os-client-config==1.28.0 -os-service-types==1.6.0 +os-service-types==1.7.0 oslo.config==6.1.0 oslotest==3.2.0 pbr==2.0.0 diff --git a/openstack/tests/unit/test_utils.py b/openstack/tests/unit/test_utils.py index 022923982..c16cfe2dc 100644 --- a/openstack/tests/unit/test_utils.py +++ b/openstack/tests/unit/test_utils.py @@ -18,6 +18,7 @@ from openstack.tests.unit import base import fixtures +import os_service_types import openstack from openstack import utils @@ -157,3 +158,13 @@ def test_value_less_than_min(self): self.endpoint_data.min_microversion = '1.42' self.assertIsNone(utils.maximum_supported_microversion(self.adapter, '1.2')) + + +class TestOsServiceTypesVersion(base.TestCase): + def test_ost_version(self): + ost_version = '2019-05-01T19:53:21.498745' + self.assertEqual( + ost_version, os_service_types.ServiceTypes().version, + "This project must be pinned to the latest version of " + "os-service-types. Please bump requirements.txt and " + "lower-constraints.txt accordingly.") diff --git a/requirements.txt b/requirements.txt index 4e188856e..42da24b3c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ appdirs>=1.3.0 # MIT License requestsexceptions>=1.2.0 # Apache-2.0 jsonpatch!=1.20,>=1.16 # BSD six>=1.10.0 # MIT -os-service-types>=1.6.0 # Apache-2.0 +os-service-types>=1.7.0 # Apache-2.0 keystoneauth1>=3.14.0 # Apache-2.0 munch>=2.1.0 # MIT From 9cce6310940e2426dfcb92e0ecb4c82b93e7788a Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 7 Jun 2019 08:48:31 +0200 Subject: [PATCH 2474/3836] Use Resource layer for next compute methods - server_groups - server_console - create_image - server_metadata Change-Id: I26f3a22bf9d9e397c2a032cf2b66ec60d8040b51 --- openstack/cloud/_compute.py | 104 +++++------------- openstack/compute/v2/_proxy.py | 16 ++- openstack/compute/v2/server.py | 39 ++++++- openstack/compute/v2/server_group.py | 50 ++++++++- .../tests/unit/cloud/test_image_snapshot.py | 2 + .../tests/unit/cloud/test_server_console.py | 8 +- .../tests/unit/cloud/test_server_group.py | 2 + .../unit/cloud/test_server_set_metadata.py | 8 +- openstack/tests/unit/compute/v2/test_proxy.py | 28 +++++ .../tests/unit/compute/v2/test_server.py | 71 +++++++++++- 10 files changed, 234 insertions(+), 94 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 942d76b04..f757df7ba 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -363,7 +363,8 @@ def _list_servers(self, detailed=False, all_projects=False, bare=False, # and the to_munch call. self._normalize_server(server._to_munch()) for server in self.compute.servers( - all_projects=all_projects, **filters)] + all_projects=all_projects, allow_unknown_params=True, + **filters)] return [ self._expand_server(server, detailed, bare) for server in servers @@ -375,10 +376,7 @@ def list_server_groups(self): :returns: A list of server group dicts. """ - data = proxy._json_response( - self.compute.get('/os-server-groups'), - error_message="Error fetching server group list") - return self._get_and_munchify('server_groups', data) + return list(self.compute.server_groups()) def get_compute_limits(self, name_or_id=None): """ Get compute limits for a project @@ -521,10 +519,12 @@ def get_server_console(self, server, length=None): return "" def _get_server_console_output(self, server_id, length=None): - data = proxy._json_response(self.compute.post( - '/servers/{server_id}/action'.format(server_id=server_id), - json={'os-getConsoleOutput': {'length': length}})) - return self._get_and_munchify('output', data) + output = self.compute.get_server_console_output( + server=server_id, + length=length + ) + if 'output' in output: + return output['output'] def get_server( self, name_or_id=None, filters=None, detailed=False, bare=False, @@ -678,40 +678,9 @@ def create_image_snapshot( "Server {server} could not be found and therefore" " could not be snapshotted.".format(server=server)) server = server_obj - response = proxy._json_response( - self.compute.post( - '/servers/{server_id}/action'.format(server_id=server['id']), - json={ - "createImage": { - "name": name, - "metadata": metadata, - } - })) - # You won't believe it - wait, who am I kidding - of course you will! - # Nova returns the URL of the image created in the Location - # header of the response. (what?) But, even better, the URL it responds - # with has a very good chance of being wrong (it is built from - # nova.conf values that point to internal API servers in any cloud - # large enough to have both public and internal endpoints. - # However, nobody has ever noticed this because novaclient doesn't - # actually use that URL - it extracts the id from the end of - # the url, then returns the id. This leads us to question: - # a) why Nova is going to return a value in a header - # b) why it's going to return data that probably broken - # c) indeed the very nature of the fabric of reality - # Although it fills us with existential dread, we have no choice but - # to follow suit like a lemming being forced over a cliff by evil - # producers from Disney. - # TODO(mordred) Update this to consume json microversion when it is - # available. - # blueprint:remove-create-image-location-header-response - image_id = response.headers['Location'].rsplit('/', 1)[1] - self.list_images.invalidate(self) - image = self.get_image(image_id) - - if not wait: - return image - return self.wait_for_image(image, timeout=timeout) + image = self.compute.create_server_image( + server, name=name, metadata=metadata, wait=wait, timeout=timeout) + return image def get_server_id(self, name_or_id): server = self.get_server(name_or_id, bare=True) @@ -1100,6 +1069,9 @@ def wait_for_server( """ Wait for a server to reach ACTIVE status. """ + # server = self.compute.wait_for_server( + # server=server, interval=self._SERVER_AGE or 2, wait=timeout + # ) server_id = server['id'] timeout_message = "Timeout waiting for the server to come up." start_time = time.time() @@ -1238,11 +1210,7 @@ def set_server_metadata(self, name_or_id, metadata): raise exc.OpenStackCloudException( 'Invalid Server {server}'.format(server=name_or_id)) - proxy._json_response( - self.compute.post( - '/servers/{server_id}/metadata'.format(server_id=server['id']), - json={'metadata': metadata}), - error_message='Error updating server metadata') + self.compute.set_server_metadata(server=server.id, **metadata) def delete_server_metadata(self, name_or_id, metadata_keys): """Delete metadata from a server instance. @@ -1259,15 +1227,8 @@ def delete_server_metadata(self, name_or_id, metadata_keys): raise exc.OpenStackCloudException( 'Invalid Server {server}'.format(server=name_or_id)) - for key in metadata_keys: - error_message = 'Error deleting metadata {key} on {server}'.format( - key=key, server=name_or_id) - proxy._json_response( - self.compute.delete( - '/servers/{server_id}/metadata/{key}'.format( - server_id=server['id'], - key=key)), - error_message=error_message) + self.compute.delete_server_metadata(server=server.id, + keys=metadata_keys) def delete_server( self, name_or_id, wait=False, timeout=180, delete_ips=False, @@ -1410,7 +1371,7 @@ def update_server(self, name_or_id, detailed=False, bare=False, **kwargs): self._get_and_munchify('server', data)) return self._expand_server(server, bare=bare, detailed=detailed) - def create_server_group(self, name, policies): + def create_server_group(self, name, policies=[], policy=None): """Create a new server group. :param name: Name of the server group being created @@ -1420,16 +1381,16 @@ def create_server_group(self, name, policies): :raises: OpenStackCloudException on operation error. """ - data = proxy._json_response( - self.compute.post( - '/os-server-groups', - json={ - 'server_group': { - 'name': name, - 'policies': policies}}), - error_message="Unable to create server group {name}".format( - name=name)) - return self._get_and_munchify('server_group', data) + sg_attrs = { + 'name': name + } + if policies: + sg_attrs['policies'] = policies + if policy: + sg_attrs['policy'] = policy + return self.compute.create_server_group( + **sg_attrs + ) def delete_server_group(self, name_or_id): """Delete a server group. @@ -1446,12 +1407,7 @@ def delete_server_group(self, name_or_id): name_or_id) return False - proxy._json_response( - self.compute.delete( - '/os-server-groups/{id}'.format(id=server_group['id'])), - error_message="Error deleting server group {name}".format( - name=name_or_id)) - + self.compute.delete_server_group(server_group, ignore_missing=False) return True def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 0e1434ea4..35cbcf877 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -623,18 +623,26 @@ def revert_server_resize(self, server): server = self._get_resource(_server.Server, server) server.revert_resize(self) - def create_server_image(self, server, name, metadata=None): + def create_server_image(self, server, name, metadata=None, wait=False, + timeout=120): """Create an image from a server :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :param str name: The name of the image to be created. :param dict metadata: A dictionary of metadata to be set on the image. - :returns: None + :returns: :class:`~openstack.image.v2.image.Image` object. """ server = self._get_resource(_server.Server, server) - server.create_image(self, name, metadata) + image_id = server.create_image(self, name, metadata) + + self._connection.list_images.invalidate(self) + image = self._connection.get_image(image_id) + + if not wait: + return image + return self._connection.wait_for_image(image, timeout=timeout) def add_security_group_to_server(self, server, security_group): """Add a security group to a server diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index d7198f493..0ee457c0e 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -12,6 +12,7 @@ from openstack.compute.v2 import metadata from openstack.image.v2 import image +from openstack import exceptions from openstack import resource from openstack import utils @@ -233,8 +234,10 @@ def _action(self, session, body, microversion=None): # the URL used is sans any additional /detail/ part. url = utils.urljoin(Server.base_path, self.id, 'action') headers = {'Accept': ''} - return session.post( + response = session.post( url, json=body, headers=headers, microversion=microversion) + exceptions.raise_from_response(response) + return response def change_password(self, session, new_password): """Change the administrator password to the given password.""" @@ -303,7 +306,39 @@ def create_image(self, session, name, metadata=None): if metadata is not None: action['metadata'] = metadata body = {'createImage': action} - self._action(session, body) + + # You won't believe it - wait, who am I kidding - of course you will! + # Nova returns the URL of the image created in the Location + # header of the response. (what?) But, even better, the URL it responds + # with has a very good chance of being wrong (it is built from + # nova.conf values that point to internal API servers in any cloud + # large enough to have both public and internal endpoints. + # However, nobody has ever noticed this because novaclient doesn't + # actually use that URL - it extracts the id from the end of + # the url, then returns the id. This leads us to question: + # a) why Nova is going to return a value in a header + # b) why it's going to return data that probably broken + # c) indeed the very nature of the fabric of reality + # Although it fills us with existential dread, we have no choice but + # to follow suit like a lemming being forced over a cliff by evil + # producers from Disney. + microversion = None + if utils.supports_microversion(session, '2.45'): + microversion = '2.45' + response = self._action(session, body, microversion) + + body = None + try: + # There might be body, might be not + body = response.json() + except Exception: + pass + if body and 'image_id' in body: + image_id = body['image_id'] + else: + image_id = response.headers['Location'].rsplit('/', 1)[1] + + return image_id def add_security_group(self, session, security_group): body = {"addSecurityGroup": {"name": security_group}} diff --git a/openstack/compute/v2/server_group.py b/openstack/compute/v2/server_group.py index 4e3e4a4ba..78a577e9a 100644 --- a/openstack/compute/v2/server_group.py +++ b/openstack/compute/v2/server_group.py @@ -10,7 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import exceptions from openstack import resource +from openstack import utils class ServerGroup(resource.Resource): @@ -20,6 +22,8 @@ class ServerGroup(resource.Resource): _query_mapping = resource.QueryParameters("all_projects") + _max_microversion = '2.64' + # capabilities allow_create = True allow_fetch = True @@ -29,9 +33,53 @@ class ServerGroup(resource.Resource): # Properties #: A name identifying the server group name = resource.Body('name') - #: The list of policies supported by the server group + #: The list of policies supported by the server group (till 2.63) policies = resource.Body('policies') + #: The policy field represents the name of the policy (from 2.64) + policy = resource.Body('policy') #: The list of members in the server group member_ids = resource.Body('members') #: The metadata associated with the server group metadata = resource.Body('metadata') + #: The project ID who owns the server group. + project_id = resource.Body('project_id') + #: The rules field, which is a dict, can be applied to the policy + rules = resource.Body('rules', type=list, list_type=dict) + #: The user ID who owns the server group + user_id = resource.Body('user_id') + + def _get_microversion_for(self, session, action): + """Get microversion to use for the given action. + + The base version uses :meth:`_get_microversion_for_list`. + Subclasses can override this method if more complex logic is needed. + + :param session: :class`keystoneauth1.adapter.Adapter` + :param action: One of "fetch", "commit", "create", "delete", "patch". + Unused in the base implementation. + :return: microversion as string or ``None`` + """ + if action not in ('fetch', 'commit', 'create', 'delete', 'patch'): + raise ValueError('Invalid action: %s' % action) + + microversion = self._get_microversion_for_list(session) + if action == 'create': + # `policy` and `rules` are added with mv=2.64. In it also + # `policies` are removed. + if utils.supports_microversion(session, '2.64'): + if self.policies: + if not self.policy and isinstance(self.policies, list): + self.policy = self.policies[0] + self.policies = None + microversion = self._max_microversion + else: + if self.rules: + message = ("API version %s is required to set rules, but " + "it is not available.") % 2.64 + raise exceptions.NotSupported(message) + if self.policy: + if not self.policies: + self.policies = [self.policy] + self.policy = None + + return microversion diff --git a/openstack/tests/unit/cloud/test_image_snapshot.py b/openstack/tests/unit/cloud/test_image_snapshot.py index f6d4e261e..218ee8de2 100644 --- a/openstack/tests/unit/cloud/test_image_snapshot.py +++ b/openstack/tests/unit/cloud/test_image_snapshot.py @@ -33,6 +33,7 @@ def test_create_image_snapshot_wait_until_active_never_active(self): snapshot_name = 'test-snapshot' fake_image = fakes.make_fake_image(self.image_id, status='pending') self.register_uris([ + self.get_nova_discovery_mock_dict(), dict( method='POST', uri='{endpoint}/servers/{server_id}/action'.format( @@ -70,6 +71,7 @@ def test_create_image_snapshot_wait_active(self): pending_image = fakes.make_fake_image(self.image_id, status='pending') fake_image = fakes.make_fake_image(self.image_id) self.register_uris([ + self.get_nova_discovery_mock_dict(), dict( method='POST', uri='{endpoint}/servers/{server_id}/action'.format( diff --git a/openstack/tests/unit/cloud/test_server_console.py b/openstack/tests/unit/cloud/test_server_console.py index 48bc2d051..9c04174f9 100644 --- a/openstack/tests/unit/cloud/test_server_console.py +++ b/openstack/tests/unit/cloud/test_server_console.py @@ -36,11 +36,11 @@ def test_get_server_console_dict(self): id=self.server_id), json={"output": self.output}, validate=dict( - json={'os-getConsoleOutput': {'length': None}})) + json={'os-getConsoleOutput': {'length': 5}})) ]) self.assertEqual( - self.output, self.cloud.get_server_console(self.server)) + self.output, self.cloud.get_server_console(self.server, 5)) self.assert_calls() def test_get_server_console_name_or_id(self): @@ -57,7 +57,7 @@ def test_get_server_console_name_or_id(self): id=self.server_id), json={"output": self.output}, validate=dict( - json={'os-getConsoleOutput': {'length': None}})) + json={'os-getConsoleOutput': {}})) ]) self.assertEqual( @@ -74,7 +74,7 @@ def test_get_server_console_no_console(self): id=self.server_id), status_code=400, validate=dict( - json={'os-getConsoleOutput': {'length': None}})) + json={'os-getConsoleOutput': {}})) ]) self.assertEqual('', self.cloud.get_server_console(self.server)) diff --git a/openstack/tests/unit/cloud/test_server_group.py b/openstack/tests/unit/cloud/test_server_group.py index 88b14e2b4..8206b7320 100644 --- a/openstack/tests/unit/cloud/test_server_group.py +++ b/openstack/tests/unit/cloud/test_server_group.py @@ -30,6 +30,7 @@ def setUp(self): def test_create_server_group(self): self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['os-server-groups']), @@ -48,6 +49,7 @@ def test_create_server_group(self): def test_delete_server_group(self): self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['os-server-groups']), diff --git a/openstack/tests/unit/cloud/test_server_set_metadata.py b/openstack/tests/unit/cloud/test_server_set_metadata.py index d5f8bf211..72af0efcd 100644 --- a/openstack/tests/unit/cloud/test_server_set_metadata.py +++ b/openstack/tests/unit/cloud/test_server_set_metadata.py @@ -57,6 +57,7 @@ def test_server_set_metadata_with_exception(self): self.assert_calls() def test_server_set_metadata(self): + metadata = {'meta': 'data'} self.register_uris([ self.get_nova_discovery_mock_dict(), dict(method='GET', @@ -67,10 +68,11 @@ def test_server_set_metadata(self): uri=self.get_mock_url( 'compute', 'public', append=['servers', self.fake_server['id'], 'metadata']), - validate=dict(json={'metadata': {'meta': 'data'}}), - status_code=200), + validate=dict(json={'metadata': metadata}), + status_code=200, + json={'metadata': metadata}), ]) - self.cloud.set_server_metadata(self.server_id, {'meta': 'data'}) + self.cloud.set_server_metadata(self.server_id, metadata) self.assert_calls() diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index b9b8694f3..6575067c7 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -9,6 +9,7 @@ # 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 openstack.compute.v2 import _proxy from openstack.compute.v2 import availability_zone as az @@ -448,6 +449,33 @@ def test_delete_server_metadata(self): method_args=["value", "key"], expected_args=[self.proxy, "key"]) + def test_create_image(self): + metadata = {'k1': 'v1'} + with mock.patch('openstack.compute.v2.server.Server.create_image') \ + as ci_mock: + + ci_mock.return_value = 'image_id' + connection_mock = mock.Mock() + connection_mock.get_image = mock.Mock(return_value='image') + connection_mock.wait_for_image = mock.Mock() + self.proxy._connection = connection_mock + + rsp = self.proxy.create_server_image( + 'server', 'image_name', metadata, wait=True, timeout=1) + + ci_mock.assert_called_with( + self.proxy, + 'image_name', + metadata + ) + + self.proxy._connection.get_image.assert_called_with('image_id') + self.proxy._connection.wait_for_image.assert_called_with( + 'image', + timeout=1) + + self.assertEqual(connection_mock.wait_for_image(), rsp) + def test_server_group_create(self): self.verify_create(self.proxy.create_server_group, server_group.ServerGroup) diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index b00601cd5..569197165 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -124,6 +124,7 @@ def setUp(self): self.resp = mock.Mock() self.resp.body = None self.resp.json = mock.Mock(return_value=self.resp.body) + self.resp.status_code = 200 self.sess = mock.Mock() self.sess.post = mock.Mock(return_value=self.resp) @@ -395,30 +396,88 @@ def test_revert_resize(self): self.sess.post.assert_called_with( url, json=body, headers=headers, microversion=None) - def test_create_image(self): + def test_create_image_header(self): sot = server.Server(**EXAMPLE) name = 'noo' metadata = {'nu': 'image', 'created': 'today'} - self.assertIsNone(sot.create_image(self.sess, name, metadata)) - url = 'servers/IDENTIFIER/action' body = {"createImage": {'name': name, 'metadata': metadata}} headers = {'Accept': ''} + + rsp = mock.Mock() + rsp.json.return_value = None + rsp.headers = {'Location': 'dummy/dummy2'} + rsp.status_code = 200 + + self.sess.post.return_value = rsp + + self.endpoint_data = mock.Mock(spec=['min_microversion', + 'max_microversion'], + min_microversion=None, + max_microversion='2.44') + self.sess.get_endpoint_data.return_value = self.endpoint_data + + image_id = sot.create_image(self.sess, name, metadata) + self.sess.post.assert_called_with( url, json=body, headers=headers, microversion=None) - def test_create_image_minimal(self): + self.assertEqual('dummy2', image_id) + + def test_create_image_microver(self): sot = server.Server(**EXAMPLE) name = 'noo' + metadata = {'nu': 'image', 'created': 'today'} - self.assertIsNone(self.resp.body, sot.create_image(self.sess, name)) + url = 'servers/IDENTIFIER/action' + body = {"createImage": {'name': name, 'metadata': metadata}} + headers = {'Accept': ''} + + rsp = mock.Mock() + rsp.json.return_value = {'image_id': 'dummy3'} + rsp.headers = {'Location': 'dummy/dummy2'} + rsp.status_code = 200 + + self.sess.post.return_value = rsp + self.endpoint_data = mock.Mock(spec=['min_microversion', + 'max_microversion'], + min_microversion='2.1', + max_microversion='2.56') + self.sess.get_endpoint_data.return_value = self.endpoint_data + + image_id = sot.create_image(self.sess, name, metadata) + + self.sess.post.assert_called_with( + url, json=body, headers=headers, microversion='2.45') + + self.assertEqual('dummy3', image_id) + + def test_create_image_minimal(self): + sot = server.Server(**EXAMPLE) + name = 'noo' url = 'servers/IDENTIFIER/action' body = {"createImage": {'name': name}} headers = {'Accept': ''} + + rsp = mock.Mock() + rsp.json.return_value = None + rsp.headers = {'Location': 'dummy/dummy2'} + rsp.status_code = 200 + + self.sess.post.return_value = rsp + + self.endpoint_data = mock.Mock(spec=['min_microversion', + 'max_microversion'], + min_microversion='2.1', + max_microversion='2.56') + self.sess.get_endpoint_data.return_value = self.endpoint_data + + self.assertIsNone(self.resp.body, sot.create_image(self.sess, name)) + self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, microversion='2.45') def test_add_security_group(self): sot = server.Server(**EXAMPLE) From f4fa6fabba88d6a738153357bcce55974a00cb1b Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Fri, 7 Jun 2019 11:22:06 +0200 Subject: [PATCH 2475/3836] baremetal: raise more specific ResourceFailure in wait_for_* methods Raising a generic SDKException makes it harder to distinguish them from other failures. Also add missing docstrings. Change-Id: I576469b0bdb664512ce9d34d5217329f3fa62b64 --- openstack/baremetal/v1/_proxy.py | 6 ++++ openstack/baremetal/v1/allocation.py | 5 +++- openstack/baremetal/v1/node.py | 15 +++++++--- .../baremetal_introspection/v1/_proxy.py | 3 ++ .../v1/introspection.py | 5 +++- .../unit/baremetal/v1/test_allocation.py | 30 +++++++++++++++++++ .../tests/unit/baremetal/v1/test_node.py | 4 +-- .../baremetal_introspection/v1/test_proxy.py | 2 +- .../baremetal-wait-e4571cdb150b188a.yaml | 7 +++++ 9 files changed, 68 insertions(+), 9 deletions(-) create mode 100644 releasenotes/notes/baremetal-wait-e4571cdb150b188a.yaml diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 842fd6241..01754b3d7 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -344,6 +344,9 @@ def wait_for_nodes_provision_state(self, nodes, expected_state, :return: The list of :class:`~openstack.baremetal.v1.node.Node` instances that reached the requested state. + :raises: :class:`~openstack.exceptions.ResourceFailure` if a node + reaches an error state and ``abort_on_failed_state`` is ``True``. + :raises: :class:`~openstack.exceptions.ResourceTimeout` on timeout. """ log_nodes = ', '.join(n.id if isinstance(n, _node.Node) else n for n in nodes) @@ -895,6 +898,9 @@ def wait_for_allocation(self, allocation, timeout=None, :returns: The instance of the allocation. :rtype: :class:`~openstack.baremetal.v1.allocation.Allocation`. + :raises: :class:`~openstack.exceptions.ResourceFailure` if allocation + fails and ``ignore_error`` is ``False``. + :raises: :class:`~openstack.exceptions.ResourceTimeout` on timeout. """ res = self._get_resource(_allocation.Allocation, allocation) return res.wait(self, timeout=timeout, ignore_error=ignore_error) diff --git a/openstack/baremetal/v1/allocation.py b/openstack/baremetal/v1/allocation.py index c4cf48762..19be5133f 100644 --- a/openstack/baremetal/v1/allocation.py +++ b/openstack/baremetal/v1/allocation.py @@ -76,6 +76,9 @@ def wait(self, session, timeout=None, ignore_error=False): state is considered successful and the call returns. :return: This :class:`Allocation` instance. + :raises: :class:`~openstack.exceptions.ResourceFailure` if allocation + fails and ``ignore_error`` is ``False``. + :raises: :class:`~openstack.exceptions.ResourceTimeout` on timeout. """ if self.state == 'active': return self @@ -86,7 +89,7 @@ def wait(self, session, timeout=None, ignore_error=False): self.fetch(session) if self.state == 'error' and not ignore_error: - raise exceptions.SDKException( + raise exceptions.ResourceFailure( "Allocation %(allocation)s failed: %(error)s" % {'allocation': self.id, 'error': self.last_error}) elif self.state != 'allocating': diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 43f3d8acc..a02a00797 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -323,6 +323,10 @@ def set_provision_state(self, session, target, config_drive=None, :return: This :class:`Node` instance. :raises: ValueError if ``config_drive``, ``clean_steps`` or ``rescue_password`` are provided with an invalid ``target``. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the node + reaches an error state while waiting for the state. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if timeout + is reached while waiting for the state. """ session = self._get_session(session) @@ -400,6 +404,9 @@ def wait_for_provision_state(self, session, expected_state, timeout=None, ``manageable`` transition is ``enroll`` again. :return: This :class:`Node` instance. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the node + reaches an error state and ``abort_on_failed_state`` is ``True``. + :raises: :class:`~openstack.exceptions.ResourceTimeout` on timeout. """ for count in utils.iterate_timeout( timeout, @@ -469,8 +476,8 @@ def _check_state_reached(self, session, expected_state, ``manageable`` transition is ``enroll`` again. :return: ``True`` if the target state is reached - :raises: SDKException if ``abort_on_failed_state`` is ``True`` and - a failure state is reached. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the node + reaches an error state and ``abort_on_failed_state`` is ``True``. """ # NOTE(dtantsur): microversion 1.2 changed None to available if (self.provision_state == expected_state @@ -481,7 +488,7 @@ def _check_state_reached(self, session, expected_state, return False if self.provision_state.endswith(' failed'): - raise exceptions.SDKException( + raise exceptions.ResourceFailure( "Node %(node)s reached failure state \"%(state)s\"; " "the last error is %(error)s" % {'node': self.id, 'state': self.provision_state, @@ -490,7 +497,7 @@ def _check_state_reached(self, session, expected_state, # "enroll" elif (expected_state == 'manageable' and self.provision_state == 'enroll' and self.last_error): - raise exceptions.SDKException( + raise exceptions.ResourceFailure( "Node %(node)s could not reach state manageable: " "failed to verify management credentials; " "the last error is %(error)s" % diff --git a/openstack/baremetal_introspection/v1/_proxy.py b/openstack/baremetal_introspection/v1/_proxy.py index 23b7ff2b8..7ad0b0796 100644 --- a/openstack/baremetal_introspection/v1/_proxy.py +++ b/openstack/baremetal_introspection/v1/_proxy.py @@ -125,6 +125,9 @@ def wait_for_introspection(self, introspection, timeout=None, if the introspection reaches the ``error`` state. Otherwise the error state is considered successful and the call returns. :returns: :class:`~.introspection.Introspection` instance. + :raises: :class:`~openstack.exceptions.ResourceFailure` if + introspection fails and ``ignore_error`` is ``False``. + :raises: :class:`~openstack.exceptions.ResourceTimeout` on timeout. """ res = self._get_resource(_introspect.Introspection, introspection) return res.wait(self, timeout=timeout, ignore_error=ignore_error) diff --git a/openstack/baremetal_introspection/v1/introspection.py b/openstack/baremetal_introspection/v1/introspection.py index e560f38a5..88819f9f1 100644 --- a/openstack/baremetal_introspection/v1/introspection.py +++ b/openstack/baremetal_introspection/v1/introspection.py @@ -107,6 +107,9 @@ def wait(self, session, timeout=None, ignore_error=False): if the introspection reaches the ``error`` state. Otherwise the error state is considered successful and the call returns. :return: This :class:`Introspection` instance. + :raises: :class:`~openstack.exceptions.ResourceFailure` if + introspection fails and ``ignore_error`` is ``False``. + :raises: :class:`~openstack.exceptions.ResourceTimeout` on timeout. """ if self._check_state(ignore_error): return self @@ -124,7 +127,7 @@ def wait(self, session, timeout=None, ignore_error=False): def _check_state(self, ignore_error): if self.state == 'error' and not ignore_error: - raise exceptions.SDKException( + raise exceptions.ResourceFailure( "Introspection of node %(node)s failed: %(error)s" % {'node': self.id, 'error': self.error}) else: diff --git a/openstack/tests/unit/baremetal/v1/test_allocation.py b/openstack/tests/unit/baremetal/v1/test_allocation.py index 2146eed3d..5b42bb4ff 100644 --- a/openstack/tests/unit/baremetal/v1/test_allocation.py +++ b/openstack/tests/unit/baremetal/v1/test_allocation.py @@ -104,6 +104,36 @@ def _side_effect(allocation, session): self.assertIs(allocation, self.allocation) self.assertEqual(2, mock_fetch.call_count) + def test_failure(self, mock_fetch): + marker = [False] # mutable object to modify in the closure + + def _side_effect(allocation, session): + if marker[0]: + self.allocation.state = 'error' + self.allocation.last_error = 'boom!' + else: + marker[0] = True + + mock_fetch.side_effect = _side_effect + self.assertRaises(exceptions.ResourceFailure, + self.allocation.wait, self.session) + self.assertEqual(2, mock_fetch.call_count) + + def test_failure_ignored(self, mock_fetch): + marker = [False] # mutable object to modify in the closure + + def _side_effect(allocation, session): + if marker[0]: + self.allocation.state = 'error' + self.allocation.last_error = 'boom!' + else: + marker[0] = True + + mock_fetch.side_effect = _side_effect + allocation = self.allocation.wait(self.session, ignore_error=True) + self.assertIs(allocation, self.allocation) + self.assertEqual(2, mock_fetch.call_count) + def test_timeout(self, mock_fetch): self.assertRaises(exceptions.ResourceTimeout, self.allocation.wait, self.session, timeout=0.001) diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 2d4764932..3a8732fcd 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -178,7 +178,7 @@ def _get_side_effect(_self, session): mock_fetch.side_effect = _get_side_effect - self.assertRaisesRegex(exceptions.SDKException, + self.assertRaisesRegex(exceptions.ResourceFailure, 'failure state "deploy failed"', self.node.wait_for_provision_state, self.session, 'manageable') @@ -191,7 +191,7 @@ def _get_side_effect(_self, session): mock_fetch.side_effect = _get_side_effect - self.assertRaisesRegex(exceptions.SDKException, + self.assertRaisesRegex(exceptions.ResourceFailure, 'failed to verify management credentials', self.node.wait_for_provision_state, self.session, 'manageable') diff --git a/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py b/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py index 92eb50eaf..77f9c2ea5 100644 --- a/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py @@ -86,7 +86,7 @@ def _side_effect(allocation, session): self.introspection.error = 'boom' mock_fetch.side_effect = _side_effect - self.assertRaisesRegex(exceptions.SDKException, 'boom', + self.assertRaisesRegex(exceptions.ResourceFailure, 'boom', self.proxy.wait_for_introspection, self.introspection) mock_fetch.assert_called_once_with(self.introspection, self.proxy) diff --git a/releasenotes/notes/baremetal-wait-e4571cdb150b188a.yaml b/releasenotes/notes/baremetal-wait-e4571cdb150b188a.yaml new file mode 100644 index 000000000..c104a7fec --- /dev/null +++ b/releasenotes/notes/baremetal-wait-e4571cdb150b188a.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + The baremetal calls ``wait_for_nodes_provision_state``, + ``wait_for_allocation`` and the baremetal introspection call + ``wait_for_introspection`` now raise ``ResourceFailure`` on reaching + an error state instead of a generic ``SDKException``. From e096f990142de24ef9e1074f2d7b4600c9cdd190 Mon Sep 17 00:00:00 2001 From: Yves-Gwenael Bourhis Date: Fri, 7 Jun 2019 14:37:35 +0200 Subject: [PATCH 2476/3836] URL encode swift objects endpoints Swift containers and objects can contain any kind of characters. If a container or name contains special characters, openstacksdk was failing to access them. We need to url encode containers and objects when creating the url to access them. Change-Id: Ifbcde7f1c59bee16b4c133c3ff4ff69858c774ce Story: 2005828 Task: 33592 --- openstack/cloud/_object_store.py | 41 +++++++++++++++++++------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index 0fb255083..2b6139fe1 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -21,7 +21,7 @@ import six import types # noqa -from six.moves import urllib +from six.moves import urllib_parse import keystoneauth1.exceptions @@ -106,7 +106,9 @@ def get_container(self, name, skip_cache=False): """ if skip_cache or name not in self._container_cache: try: - response = self.object_store.head(name) + response = self.object_store.head( + self._get_object_endpoint(name) + ) exceptions.raise_from_response(response) self._container_cache[name] = response.headers except exc.OpenStackCloudHTTPError as e: @@ -126,7 +128,9 @@ def create_container(self, name, public=False): container = self.get_container(name) if container: return container - exceptions.raise_from_response(self.object_store.put(name)) + exceptions.raise_from_response(self.object_store.put( + self._get_object_endpoint(name) + )) if public: self.set_container_access(name, 'public') return self.get_container(name, skip_cache=True) @@ -137,7 +141,9 @@ def delete_container(self, name): :param str name: Name of the container to delete. """ try: - exceptions.raise_from_response(self.object_store.delete(name)) + exceptions.raise_from_response(self.object_store.delete( + self._get_object_endpoint(name) + )) self._container_cache.pop(name, None) return True except exc.OpenStackCloudHTTPError as e: @@ -167,7 +173,9 @@ def update_container(self, name, headers): Key/Value headers to set on the container. """ exceptions.raise_from_response( - self.object_store.post(name, headers=headers)) + self.object_store.post( + self._get_object_endpoint(name), headers=headers) + ) def set_container_access(self, name, access): """Set the access control list on a container. @@ -237,7 +245,7 @@ def get_object_capabilities(self): # The endpoint in the catalog has version and project-id in it # To get capabilities, we have to disassemble and reassemble the URL # This logic is taken from swiftclient - endpoint = urllib.parse.urlparse(self.object_store.get_endpoint()) + endpoint = urllib_parse.urlparse(self.object_store.get_endpoint()) url = "{scheme}://{netloc}/info".format( scheme=endpoint.scheme, netloc=endpoint.netloc) @@ -414,7 +422,7 @@ def create_object( for (k, v) in metadata.items(): headers['x-object-meta-' + k] = v - endpoint = '{container}/{name}'.format(container=container, name=name) + endpoint = self._get_object_endpoint(container, name) if data is not None: self.log.debug( @@ -585,8 +593,7 @@ def update_object(self, container, name, metadata=None, **headers): headers = dict(headers, **metadata_headers) return self._object_store_client.post( - '{container}/{object}'.format( - container=container, object=name), + self._get_object_endpoint(container, name), headers=headers) def list_objects(self, container, full_listing=True, prefix=None): @@ -652,8 +659,7 @@ def delete_object(self, container, name, meta=None): if meta.get('X-Static-Large-Object', None) == 'True': params['multipart-manifest'] = 'delete' self._object_store_client.delete( - '{container}/{object}'.format( - container=container, object=name), + self._get_object_endpoint(container, name), params=params) return True except exc.OpenStackCloudHTTPError: @@ -687,8 +693,7 @@ def delete_autocreated_image_objects(self, container=None): def get_object_metadata(self, container, name): try: return self._object_store_client.head( - '{container}/{object}'.format( - container=container, object=name)).headers + self._get_object_endpoint(container, name)).headers except exc.OpenStackCloudException as e: if e.response.status_code == 404: return None @@ -710,9 +715,13 @@ def get_object_raw(self, container, obj, query_string=None, stream=False): endpoint = self._get_object_endpoint(container, obj, query_string) return self._object_store_client.get(endpoint, stream=stream) - def _get_object_endpoint(self, container, obj, query_string): - endpoint = '{container}/{object}'.format( - container=container, object=obj) + def _get_object_endpoint(self, container, obj=None, query_string=None): + endpoint = urllib_parse.quote(container) + if obj: + endpoint = '{endpoint}/{object}'.format( + endpoint=endpoint, + object=urllib_parse.quote(obj) + ) if query_string: endpoint = '{endpoint}?{query_string}'.format( endpoint=endpoint, query_string=query_string) From 402c9f7361a89617c3fc198107a88d1066ae75f2 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Sat, 8 Jun 2019 10:46:16 +0200 Subject: [PATCH 2477/3836] Add access alias (aka) for the resource attributes In the compute.server we are currently exposing names, which are only "aliases" for known attributes (i.e. accessIPv4 vs access_ipv4). Since we want to drop the _normalization completely it is necessary to expose attributes under old used names (which are not really fitting naming convention of the resource attributes). So add a "aka" to the BaseComponent. While we are here - repair some of the resource accesses from the unittests (i.e. direct conversion to dict), - update local dict storage, so that json.dumps works on update resource. Actually also fix the existing problem in the code with that. - add _attributes_iterator to de-duplicate logic in Resource - what a wonderful bug - passing sha256 as md5 (affects nodepool heavily). Add also tests for that. Change-Id: I39edfc8fe4e4f6c216663c0e7602c9f1bec5cab4 --- openstack/cloud/_image.py | 2 +- openstack/resource.py | 142 ++++++++++++++--------- openstack/tests/fakes.py | 17 ++- openstack/tests/unit/cloud/test_image.py | 111 +++++++++++------- openstack/tests/unit/test_resource.py | 122 ++++++++++++++++++- 5 files changed, 291 insertions(+), 103 deletions(-) diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py index b3812d374..de8033bb7 100644 --- a/openstack/cloud/_image.py +++ b/openstack/cloud/_image.py @@ -301,7 +301,7 @@ def create_image( image = self.image.create_image( name, filename=filename, container=container, - md5=sha256, sha256=sha256, + md5=md5, sha256=sha256, disk_format=disk_format, container_format=container_format, disable_vendor_agent=disable_vendor_agent, wait=wait, timeout=timeout, diff --git a/openstack/resource.py b/openstack/resource.py index 538997fe2..f3d776464 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -88,7 +88,7 @@ class _BaseComponent(object): # The class to be used for mappings _map_cls = dict - def __init__(self, name, type=None, default=None, alias=None, + def __init__(self, name, type=None, default=None, alias=None, aka=None, alternate_id=False, list_type=None, coerce_to_default=False, **kwargs): """A typed descriptor for a component that makes up a Resource @@ -101,6 +101,7 @@ def __init__(self, name, type=None, default=None, alias=None, component to a string, __set__ will fail, for example. :param default: Typically None, but any other default can be set. :param alias: If set, alternative attribute on object to return. + :param aka: If set, additional name attribute would be available under. :param alternate_id: When `True`, this property is known internally as a value that can be sent with requests that require an ID but when `id` is @@ -121,6 +122,7 @@ def __init__(self, name, type=None, default=None, alias=None, else: self.default = default self.alias = alias + self.aka = aka self.alternate_id = alternate_id self.list_type = list_type self.coerce_to_default = coerce_to_default @@ -440,6 +442,9 @@ class Resource(dict): _delete_response_class = None _store_unknown_attrs_as_properties = False + # Placeholder for aliases as dict of {__alias__:__original} + _attr_aliases = {} + def __init__(self, _synchronized=False, connection=None, **attrs): """The base resource @@ -493,6 +498,11 @@ def __init__(self, _synchronized=False, connection=None, **attrs): self._update_location() + for attr, component in self._attributes_iterator(): + if component.aka: + # Register alias for the attribute (local name) + self._attr_aliases[component.aka] = attr + # TODO(mordred) This is terrible, but is a hack at the moment to ensure # json.dumps works. The json library does basically if not obj: and # obj.items() ... but I think the if not obj: is short-circuiting down @@ -500,6 +510,18 @@ def __init__(self, _synchronized=False, connection=None, **attrs): # always False even if we override __len__ or __bool__. dict.update(self, self.to_dict()) + @classmethod + def _attributes_iterator(cls, components=tuple([Body, Header])): + """Iterator over all Resource attributes + """ + # isinstance stricly requires this to be a tuple + # Since we're looking at class definitions we need to include + # subclasses, so check the whole MRO. + for klass in cls.__mro__: + for attr, component in klass.__dict__.items(): + if isinstance(component, components): + yield attr, component + def __repr__(self): pairs = [ "%s=%s" % (k, v if v is not None else 'None') @@ -541,7 +563,14 @@ def __getattribute__(self, name): except KeyError: return None else: - return object.__getattribute__(self, name) + try: + return object.__getattribute__(self, name) + except AttributeError as e: + if name in self._attr_aliases: + # Hmm - not found. But hey, the alias exists... + return object.__getattribute__( + self, self._attr_aliases[name]) + raise e def __getitem__(self, name): """Provide dictionary access for elements of the data model.""" @@ -549,6 +578,10 @@ def __getitem__(self, name): # behaves like its wrapped content. If we get it on the class, # it returns the BaseComponent itself, not the results of __get__. real_item = getattr(self.__class__, name, None) + if not real_item and name in self._attr_aliases: + # Not found? But we know an alias exists. + name = self._attr_aliases[name] + real_item = getattr(self.__class__, name, None) if isinstance(real_item, _BaseComponent): return getattr(self, name) raise KeyError(name) @@ -569,6 +602,23 @@ def __setitem__(self, name, value): cls=self.__class__.__name__, name=name)) + def _attributes(self, remote_names=False, components=None, + include_aliases=True): + """Generate list of supported attributes + """ + attributes = [] + + if not components: + components = tuple([Body, Header, Computed, URI]) + + for attr, component in self._attributes_iterator(components): + key = attr if not remote_names else component.name + attributes.append(key) + if include_aliases and component.aka: + attributes.append(component.aka) + + return attributes + def keys(self): # NOTE(mordred) In python2, dict.keys returns a list. In python3 it # returns a dict_keys view. For 2, we can return a list from the @@ -576,15 +626,9 @@ def keys(self): # It won't strictly speaking be an actual dict_keys, so it's possible # we may want to get more clever, but for now let's see how far this # will take us. - underlying_keys = itertools.chain( - self._body.attributes.keys(), - self._header.attributes.keys(), - self._uri.attributes.keys(), - self._computed.attributes.keys()) - if six.PY2: - return list(underlying_keys) - else: - return underlying_keys + # NOTE(gtema) For now let's return list of 'public' attributes and not + # remotes or "unknown" + return self._attributes() def _update(self, **attrs): """Given attributes, update them on this instance @@ -736,16 +780,12 @@ def _get_mapping(cls, component): """Return a dict of attributes of a given component on the class""" mapping = component._map_cls() ret = component._map_cls() - # Since we're looking at class definitions we need to include - # subclasses, so check the whole MRO. - for klass in cls.__mro__: - for key, value in klass.__dict__.items(): - if isinstance(value, component): - # Make sure base classes don't end up overwriting - # mappings we've found previously in subclasses. - if key not in mapping: - # Make it this way first, to get MRO stuff correct. - mapping[key] = value.name + for key, value in cls._attributes_iterator(component): + # Make sure base classes don't end up overwriting + # mappings we've found previously in subclasses. + if key not in mapping: + # Make it this way first, to get MRO stuff correct. + mapping[key] = value.name for k, v in mapping.items(): ret[v] = k return ret @@ -888,38 +928,35 @@ def to_dict(self, body=True, headers=True, computed=True, # but is slightly different in that we're looking at an instance # and we're mapping names on this class to their actual stored # values. - # Since we're looking at class definitions we need to include - # subclasses, so check the whole MRO. - for klass in self.__class__.__mro__: - for attr, component in klass.__dict__.items(): - if isinstance(component, components): - if original_names: - key = component.name + for attr, component in self._attributes_iterator(components): + if original_names: + key = component.name + else: + key = attr + for key in filter(None, (key, component.aka)): + # Make sure base classes don't end up overwriting + # mappings we've found previously in subclasses. + if key not in mapping: + value = getattr(self, attr, None) + if ignore_none and value is None: + continue + if isinstance(value, Resource): + mapping[key] = value.to_dict(_to_munch=_to_munch) + elif isinstance(value, dict) and _to_munch: + mapping[key] = munch.Munch(value) + elif value and isinstance(value, list): + converted = [] + for raw in value: + if isinstance(raw, Resource): + converted.append( + raw.to_dict(_to_munch=_to_munch)) + elif isinstance(raw, dict) and _to_munch: + converted.append(munch.Munch(raw)) + else: + converted.append(raw) + mapping[key] = converted else: - key = attr - # Make sure base classes don't end up overwriting - # mappings we've found previously in subclasses. - if key not in mapping: - value = getattr(self, attr, None) - if ignore_none and value is None: - continue - if isinstance(value, Resource): - mapping[key] = value.to_dict(_to_munch=_to_munch) - elif isinstance(value, dict) and _to_munch: - mapping[key] = munch.Munch(value) - elif value and isinstance(value, list): - converted = [] - for raw in value: - if isinstance(raw, Resource): - converted.append( - raw.to_dict(_to_munch=_to_munch)) - elif isinstance(raw, dict) and _to_munch: - converted.append(munch.Munch(raw)) - else: - converted.append(raw) - mapping[key] = converted - else: - mapping[key] = value + mapping[key] = value return mapping # Compatibility with the munch.Munch.toDict method @@ -1065,6 +1102,7 @@ def _translate_response(self, response, has_body=None, error_message=None): self._header.attributes.update(headers) self._header.clean() self._update_location() + dict.update(self, self.to_dict()) @classmethod def _get_session(cls, session): diff --git a/openstack/tests/fakes.py b/openstack/tests/fakes.py index 4479968c3..18129f14f 100644 --- a/openstack/tests/fakes.py +++ b/openstack/tests/fakes.py @@ -19,6 +19,7 @@ import datetime import json +import hashlib import uuid from openstack.orchestration.util import template_format @@ -221,7 +222,17 @@ def make_fake_stack_event( def make_fake_image( image_id=None, md5=NO_MD5, sha256=NO_SHA256, status='active', image_name=u'fake_image', + data=None, checksum=u'ee36e35a297980dee1b514de9803ec6d'): + if data: + md5 = hashlib.md5() + sha256 = hashlib.sha256() + with open(data, 'rb') as file_obj: + for chunk in iter(lambda: file_obj.read(8192), b''): + md5.update(chunk) + sha256.update(chunk) + md5 = md5.hexdigest() + sha256 = sha256.hexdigest() return { u'image_state': u'available', u'container_format': u'bare', @@ -243,10 +254,10 @@ def make_fake_image( u'min_disk': 40, u'virtual_size': None, u'name': image_name, - u'checksum': checksum, + u'checksum': md5 or checksum, u'created_at': u'2016-02-10T05:03:11Z', - u'owner_specified.openstack.md5': NO_MD5, - u'owner_specified.openstack.sha256': NO_SHA256, + u'owner_specified.openstack.md5': md5 or NO_MD5, + u'owner_specified.openstack.sha256': sha256 or NO_SHA256, u'owner_specified.openstack.object': 'images/{name}'.format( name=image_name), u'protected': False} diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index c90a28f5c..5789305b2 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -11,13 +11,13 @@ # 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 hashlib import operator import tempfile import uuid import six +from openstack import exceptions from openstack.cloud import exc from openstack.cloud import meta from openstack.tests import fakes @@ -35,12 +35,14 @@ def setUp(self): self.image_name = self.getUniqueString('image') self.object_name = u'images/{name}'.format(name=self.image_name) self.imagefile = tempfile.NamedTemporaryFile(delete=False) - self.imagefile.write(b'\0') + data = b'\2\0' + self.imagefile.write(data) self.imagefile.close() - self.output = uuid.uuid4().bytes + self.output = data self.fake_image_dict = fakes.make_fake_image( image_id=self.image_id, image_name=self.image_name, - checksum=hashlib.md5(self.output).hexdigest()) + data=self.imagefile.name + ) self.fake_search_return = {'images': [self.fake_image_dict]} self.container_name = self.getUniqueString('container') @@ -333,9 +335,13 @@ def test_create_image_put_v2(self): u'container_format': u'bare', u'disk_format': u'qcow2', u'name': self.image_name, - u'owner_specified.openstack.md5': fakes.NO_MD5, + u'owner_specified.openstack.md5': + self.fake_image_dict[ + 'owner_specified.openstack.md5'], u'owner_specified.openstack.object': self.object_name, - u'owner_specified.openstack.sha256': fakes.NO_SHA256, + u'owner_specified.openstack.sha256': + self.fake_image_dict[ + 'owner_specified.openstack.sha256'], u'visibility': u'private'}) ), dict(method='PUT', @@ -360,7 +366,8 @@ def test_create_image_put_v2(self): is_public=False) self.assert_calls() - self.assertEqual(self.adapter.request_history[5].text.read(), b'\x00') + self.assertEqual(self.adapter.request_history[5].text.read(), + self.output) def test_create_image_task(self): self.cloud.image_api_use_tasks = True @@ -428,8 +435,12 @@ def test_create_image_task(self): object=self.image_name), status_code=201, validate=dict( - headers={'x-object-meta-x-sdk-md5': fakes.NO_MD5, - 'x-object-meta-x-sdk-sha256': fakes.NO_SHA256}) + headers={'x-object-meta-x-sdk-md5': + self.fake_image_dict[ + 'owner_specified.openstack.md5'], + 'x-object-meta-x-sdk-sha256': + self.fake_image_dict[ + 'owner_specified.openstack.sha256']}) ), dict(method='POST', uri=self.get_mock_url( @@ -467,9 +478,13 @@ def test_create_image_task(self): container=self.container_name, object=self.image_name), u'path': u'/owner_specified.openstack.object'}, - {u'op': u'add', u'value': fakes.NO_MD5, + {u'op': u'add', u'value': + self.fake_image_dict[ + 'owner_specified.openstack.md5'], u'path': u'/owner_specified.openstack.md5'}, - {u'op': u'add', u'value': fakes.NO_SHA256, + {u'op': u'add', u'value': + self.fake_image_dict[ + 'owner_specified.openstack.sha256'], u'path': u'/owner_specified.openstack.sha256'}], key=operator.itemgetter('path')), headers={ @@ -486,8 +501,12 @@ def test_create_image_task(self): 'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1', 'Content-Length': '1290170880', 'Last-Modified': 'Tue, 14 Apr 2015 18:29:01 GMT', - 'X-Object-Meta-X-Sdk-Sha256': fakes.NO_SHA256, - 'X-Object-Meta-X-Sdk-Md5': fakes.NO_MD5, + 'X-Object-Meta-X-Sdk-Sha256': + self.fake_image_dict[ + 'owner_specified.openstack.sha256'], + 'X-Object-Meta-X-Sdk-Md5': + self.fake_image_dict[ + 'owner_specified.openstack.md5'], 'Date': 'Thu, 16 Nov 2017 15:24:30 GMT', 'Accept-Ranges': 'bytes', 'Content-Type': 'application/octet-stream', @@ -756,46 +775,56 @@ def test_create_image_put_v2_bad_delete(self): def test_create_image_put_v2_wrong_checksum_delete(self): self.cloud.image_api_use_tasks = False - args = {'name': self.image_name, - 'container_format': 'bare', 'disk_format': 'qcow2', - 'owner_specified.openstack.md5': fakes.NO_MD5, - 'owner_specified.openstack.sha256': fakes.NO_SHA256, - 'owner_specified.openstack.object': 'images/{name}'.format( - name=self.image_name), - 'visibility': 'private'} + fake_image = self.fake_image_dict - ret = args.copy() - ret['id'] = self.image_id - ret['status'] = 'success' - ret['checksum'] = 'fake' + fake_image['owner_specified.openstack.md5'] = 'a' + fake_image['owner_specified.openstack.sha256'] = 'b' self.register_uris([ dict(method='GET', - uri='https://image.example.com/v2/images', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), json={'images': []}), dict(method='POST', - uri='https://image.example.com/v2/images', - json=ret, - validate=dict(json=args)), - dict(method='PUT', - uri='https://image.example.com/v2/images/{id}/file'.format( - id=self.image_id), - status_code=400, + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), + json=self.fake_image_dict, validate=dict( - headers={ - 'Content-Type': 'application/octet-stream', - }, - )), + json={ + u'container_format': u'bare', + u'disk_format': u'qcow2', + u'name': self.image_name, + u'owner_specified.openstack.md5': + fake_image[ + 'owner_specified.openstack.md5'], + u'owner_specified.openstack.object': self.object_name, + u'owner_specified.openstack.sha256': + fake_image[ + 'owner_specified.openstack.sha256'], + u'visibility': u'private'}) + ), + dict(method='PUT', + uri=self.get_mock_url( + 'image', append=['images', self.image_id, 'file'], + base_url_append='v2'), + request_headers={'Content-Type': 'application/octet-stream'}), + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images', self.fake_image_dict['id']], + base_url_append='v2' + ), + json=fake_image), dict(method='DELETE', uri='https://image.example.com/v2/images/{id}'.format( - id=self.image_id)), + id=self.image_id)) ]) self.assertRaises( - exc.OpenStackCloudHTTPError, - self._call_create_image, - self.image_name, - md5='some_fake') + exceptions.SDKException, + self.cloud.create_image, + self.image_name, self.imagefile.name, + is_public=False, md5='a', sha256='b' + ) self.assert_calls() diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 24ca91da8..be9ec056e 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -11,6 +11,7 @@ # under the License. import itertools +import json from keystoneauth1 import adapter import mock @@ -54,11 +55,12 @@ def test_implementations(self): def test_creation(self): sot = resource._BaseComponent( - "name", type=int, default=1, alternate_id=True) + "name", type=int, default=1, alternate_id=True, aka="alias") self.assertEqual("name", sot.name) self.assertEqual(int, sot.type) self.assertEqual(1, sot.default) + self.assertEqual("alias", sot.aka) self.assertTrue(sot.alternate_id) def test_get_no_instance(self): @@ -684,11 +686,73 @@ def test__get_id_value(self): value = "id" self.assertEqual(value, resource.Resource._get_id(value)) + def test__attributes(self): + class Test(resource.Resource): + foo = resource.Header('foo') + bar = resource.Body('bar', aka='_bar') + bar_local = resource.Body('bar_remote') + + sot = Test() + + self.assertEqual( + sorted(['foo', 'bar', '_bar', 'bar_local', + 'id', 'name', 'location']), + sorted(sot._attributes()) + ) + + self.assertEqual( + sorted(['foo', 'bar', 'bar_local', 'id', 'name', 'location']), + sorted(sot._attributes(include_aliases=False)) + ) + + self.assertEqual( + sorted(['foo', 'bar', '_bar', 'bar_remote', + 'id', 'name', 'location']), + sorted(sot._attributes(remote_names=True)) + ) + + self.assertEqual( + sorted(['bar', '_bar', 'bar_local', 'id', 'name', 'location']), + sorted(sot._attributes( + components=tuple([resource.Body, resource.Computed]))) + ) + + self.assertEqual( + ('foo',), + tuple(sot._attributes(components=tuple([resource.Header]))) + ) + + def test__attributes_iterator(self): + class Parent(resource.Resource): + foo = resource.Header('foo') + bar = resource.Body('bar', aka='_bar') + + class Child(Parent): + foo1 = resource.Header('foo1') + bar1 = resource.Body('bar1') + + sot = Child() + expected = ['foo', 'bar', 'foo1', 'bar1'] + + for attr, component in sot._attributes_iterator(): + if attr in expected: + expected.remove(attr) + self.assertEqual([], expected) + + expected = ['foo', 'foo1'] + + # Check we iterate only over headers + for attr, component in sot._attributes_iterator( + components=tuple([resource.Header])): + if attr in expected: + expected.remove(attr) + self.assertEqual([], expected) + def test_to_dict(self): class Test(resource.Resource): foo = resource.Header('foo') - bar = resource.Body('bar') + bar = resource.Body('bar', aka='_bar') res = Test(id='FAKE_ID') @@ -697,7 +761,8 @@ class Test(resource.Resource): 'name': None, 'location': None, 'foo': None, - 'bar': None + 'bar': None, + '_bar': None } self.assertEqual(expected, res.to_dict()) @@ -791,17 +856,18 @@ def test_to_dict_with_mro(self): class Parent(resource.Resource): foo = resource.Header('foo') - bar = resource.Body('bar') + bar = resource.Body('bar', aka='_bar') class Child(Parent): foo_new = resource.Header('foo_baz_server') bar_new = resource.Body('bar_baz_server') - res = Child(id='FAKE_ID') + res = Child(id='FAKE_ID', bar='test') expected = { 'foo': None, - 'bar': None, + 'bar': 'test', + '_bar': 'test', 'foo_new': None, 'bar_new': None, 'id': 'FAKE_ID', @@ -810,6 +876,50 @@ class Child(Parent): } self.assertEqual(expected, res.to_dict()) + def test_json_dumps_from_resource(self): + class Test(resource.Resource): + foo = resource.Body('foo_remote') + + res = Test(foo='bar') + + expected = '{"foo": "bar", "id": null, "location": null, "name": null}' + + actual = json.dumps(res, sort_keys=True) + self.assertEqual(expected, actual) + + response = FakeResponse({ + 'foo': 'new_bar'}) + res._translate_response(response) + + expected = ('{"foo": "new_bar", "id": null, ' + '"location": null, "name": null}') + actual = json.dumps(res, sort_keys=True) + self.assertEqual(expected, actual) + + def test_access_by_aka(self): + class Test(resource.Resource): + foo = resource.Header('foo_remote', aka='foo_alias') + + res = Test(foo='bar', name='test') + + self.assertEqual('bar', res['foo_alias']) + self.assertEqual('bar', res.foo_alias) + self.assertTrue('foo' in res.keys()) + self.assertTrue('foo_alias' in res.keys()) + expected = munch.Munch({ + 'id': None, + 'name': 'test', + 'location': None, + 'foo': 'bar', + 'foo_alias': 'bar' + }) + actual = munch.Munch(res) + self.assertEqual(expected, actual) + self.assertEqual(expected, res.toDict()) + self.assertEqual(expected, res.to_dict()) + self.assertDictEqual(expected, res) + self.assertDictEqual(expected, dict(res)) + def test_to_dict_value_error(self): class Test(resource.Resource): From 9736d239d5bd3c02ad6f7d2c4847af4ce3f884a1 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 11 Jun 2019 11:35:15 +0200 Subject: [PATCH 2478/3836] Use Resource layer for network SecurityGroups Move SecurityGroups logic from the cloud layer to resource. Change-Id: I27d910dc717182a3f9879a1f3b99c1a0760a21ed --- openstack/cloud/_compute.py | 7 -- openstack/cloud/_network.py | 34 ------ openstack/cloud/_security_group.py | 100 +++++++++++------- openstack/network/v2/security_group.py | 8 +- openstack/network/v2/security_group_rule.py | 9 +- .../tests/unit/cloud/test_security_groups.py | 68 +++++++----- .../unit/network/v2/test_security_group.py | 20 +++- .../network/v2/test_security_group_rule.py | 27 ++++- 8 files changed, 159 insertions(+), 114 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index a883ad3c7..b7eceb825 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -87,13 +87,6 @@ def search_flavors(self, name_or_id=None, filters=None, get_extra=True): flavors = self.list_flavors(get_extra=get_extra) return _utils._filter_list(flavors, name_or_id, filters) - def search_security_groups(self, name_or_id=None, filters=None): - # `filters` could be a dict or a jmespath (str) - groups = self.list_security_groups( - filters=filters if isinstance(filters, dict) else None - ) - return _utils._filter_list(groups, name_or_id, filters) - def search_servers( self, name_or_id=None, filters=None, detailed=False, all_projects=False, bare=False): diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index e0f938b77..687d97c32 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -1156,40 +1156,6 @@ def _lookup_ingress_egress_firewall_policy_ids(self, firewall_group): firewall_group[key + '_id'] = val del firewall_group[key] - def list_security_groups(self, filters=None): - """List all available security groups. - - :param filters: (optional) dict of filter conditions to push down - :returns: A list of security group ``munch.Munch``. - - """ - # Security groups not supported - if not self._has_secgroups(): - raise exc.OpenStackCloudUnavailableFeature( - "Unavailable feature: security groups" - ) - - if not filters: - filters = {} - - data = [] - # Handle neutron security groups - if self._use_neutron_secgroups(): - # Neutron returns dicts, so no need to convert objects here. - resp = self.network.get('/security-groups.json', params=filters) - data = proxy._json_response( - resp, - error_message="Error fetching security group list") - return self._normalize_secgroups( - self._get_and_munchify('security_groups', data)) - - # Handle nova security groups - else: - data = proxy._json_response(self.compute.get( - '/os-security-groups', params=filters)) - return self._normalize_secgroups( - self._get_and_munchify('security_groups', data)) - @_utils.valid_kwargs("name", "description", "shared", "default", "project_id") def create_qos_policy(self, **kwargs): diff --git a/openstack/cloud/_security_group.py b/openstack/cloud/_security_group.py index 032769654..16fa73ad9 100644 --- a/openstack/cloud/_security_group.py +++ b/openstack/cloud/_security_group.py @@ -28,6 +28,45 @@ class SecurityGroupCloudMixin(_normalize.Normalizer): def __init__(self): self.secgroup_source = self.config.config['secgroup_source'] + def search_security_groups(self, name_or_id=None, filters=None): + # `filters` could be a dict or a jmespath (str) + groups = self.list_security_groups( + filters=filters if isinstance(filters, dict) else None + ) + return _utils._filter_list(groups, name_or_id, filters) + + def list_security_groups(self, filters=None): + """List all available security groups. + + :param filters: (optional) dict of filter conditions to push down + :returns: A list of security group ``munch.Munch``. + + """ + # Security groups not supported + if not self._has_secgroups(): + raise exc.OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + + if not filters: + filters = {} + + data = [] + # Handle neutron security groups + if self._use_neutron_secgroups(): + # pass filters dict to the list to filter as much as possible on + # the server side + return list( + self.network.security_groups(allow_unknown_params=True, + **filters)) + + # Handle nova security groups + else: + data = proxy._json_response(self.compute.get( + '/os-security-groups', params=filters)) + return self._normalize_secgroups( + self._get_and_munchify('security_groups', data)) + def get_security_group(self, name_or_id, filters=None): """Get a security group by name or ID. @@ -67,8 +106,7 @@ def get_security_group_by_id(self, id): error_message = ("Error getting security group with" " ID {id}".format(id=id)) if self._use_neutron_secgroups(): - resp = self.network.get('/security-groups/{id}'.format(id=id)) - data = proxy._json_response(resp, error_message=error_message) + return self.network.get_security_group(id) else: data = proxy._json_response( self.compute.get( @@ -101,20 +139,17 @@ def create_security_group(self, name, description, project_id=None): data = [] security_group_json = { - 'security_group': { - 'name': name, 'description': description - }} + 'name': name, 'description': description + } if project_id is not None: - security_group_json['security_group']['tenant_id'] = project_id + security_group_json['tenant_id'] = project_id if self._use_neutron_secgroups(): - data = proxy._json_response( - self.network.post( - '/security-groups.json', - json=security_group_json), - error_message="Error creating security group {0}".format(name)) + return self.network.create_security_group( + **security_group_json) else: data = proxy._json_response(self.compute.post( - '/os-security-groups', json=security_group_json)) + '/os-security-groups', + json={'security_group': security_group_json})) return self._normalize_secgroup( self._get_and_munchify('security_group', data)) @@ -144,13 +179,8 @@ def delete_security_group(self, name_or_id): return False if self._use_neutron_secgroups(): - exceptions.raise_from_response( - self.network.delete( - '/security-groups/{sg_id}.json'.format( - sg_id=secgroup['id'])), - error_message="Error deleting security group {0}".format( - name_or_id) - ) + self.network.delete_security_group( + secgroup['id'], ignore_missing=False) return True else: @@ -183,12 +213,10 @@ def update_security_group(self, name_or_id, **kwargs): "Security group %s not found." % name_or_id) if self._use_neutron_secgroups(): - data = proxy._json_response( - self.network.put( - '/security-groups/{sg_id}.json'.format(sg_id=group['id']), - json={'security_group': kwargs}), - error_message="Error updating security group {0}".format( - name_or_id)) + return self.network.update_security_group( + group['id'], + **kwargs + ) else: for key in ('name', 'description'): kwargs.setdefault(key, group[key]) @@ -281,13 +309,12 @@ def create_security_group_rule(self, 'ethertype': ethertype } if project_id is not None: + rule_def['project_id'] = project_id rule_def['tenant_id'] = project_id - data = proxy._json_response( - self.network.post( - '/security-group-rules.json', - json={'security_group_rule': rule_def}), - error_message="Error creating security group rule") + return self.network.create_security_group_rule( + **rule_def + ) else: # NOTE: Neutron accepts None for protocol. Nova does not. if protocol is None: @@ -355,15 +382,10 @@ def delete_security_group_rule(self, rule_id): ) if self._use_neutron_secgroups(): - try: - exceptions.raise_from_response( - self.network.delete( - '/security-group-rules/{sg_id}.json'.format( - sg_id=rule_id)), - error_message="Error deleting security group rule " - "{0}".format(rule_id)) - except exc.OpenStackCloudResourceNotFound: - return False + self.network.delete_security_group_rule( + rule_id, + ignore_missing=False + ) return True else: diff --git a/openstack/network/v2/security_group.py b/openstack/network/v2/security_group.py index e4677c92e..bb456e7f4 100644 --- a/openstack/network/v2/security_group.py +++ b/openstack/network/v2/security_group.py @@ -26,8 +26,8 @@ class SecurityGroup(resource.Resource, resource.TagMixin): allow_list = True _query_mapping = resource.QueryParameters( - 'description', 'name', - project_id='tenant_id', + 'description', 'name', 'project_id', 'tenant_id', 'revision_number', + 'sort_dir', 'sort_key', **resource.TagMixin._tag_query_parameters ) @@ -39,12 +39,14 @@ class SecurityGroup(resource.Resource, resource.TagMixin): #: The security group name. name = resource.Body('name') #: The ID of the project this security group is associated with. - project_id = resource.Body('tenant_id') + project_id = resource.Body('project_id') #: Revision number of the security group. *Type: int* revision_number = resource.Body('revision_number', type=int) #: A list of #: :class:`~openstack.network.v2.security_group_rule.SecurityGroupRule` #: objects. *Type: list* security_group_rules = resource.Body('security_group_rules', type=list) + #: The ID of the project this security group is associated with. + tenant_id = resource.Body('tenant_id') #: Timestamp when the security group was last updated. updated_at = resource.Body('updated_at') diff --git a/openstack/network/v2/security_group_rule.py b/openstack/network/v2/security_group_rule.py index 4f10fbdb8..391b97eb1 100644 --- a/openstack/network/v2/security_group_rule.py +++ b/openstack/network/v2/security_group_rule.py @@ -28,8 +28,11 @@ class SecurityGroupRule(resource.Resource, resource.TagMixin): _query_mapping = resource.QueryParameters( 'description', 'direction', 'protocol', 'remote_group_id', 'security_group_id', + 'port_range_max', 'port_range_min', + 'remote_ip_prefix', 'revision_number', + 'project_id', 'tenant_id', + 'sort_dir', 'sort_key', ether_type='ethertype', - project_id='tenant_id', **resource.TagMixin._tag_query_parameters ) @@ -58,7 +61,7 @@ class SecurityGroupRule(resource.Resource, resource.TagMixin): #: attribute. If the protocol is ICMP, this value must be an ICMP type. port_range_min = resource.Body('port_range_min', type=int) #: The ID of the project this security group rule is associated with. - project_id = resource.Body('tenant_id') + project_id = resource.Body('project_id') #: The protocol that is matched by the security group rule. #: Valid values are ``null``, ``tcp``, ``udp``, and ``icmp``. protocol = resource.Body('protocol') @@ -75,5 +78,7 @@ class SecurityGroupRule(resource.Resource, resource.TagMixin): revision_number = resource.Body('revision_number', type=int) #: The security group ID to associate with this security group rule. security_group_id = resource.Body('security_group_id') + #: The ID of the project this security group rule is associated with. + tenant_id = resource.Body('tenant_id') #: Timestamp when the security group rule was last updated. updated_at = resource.Body('updated_at') diff --git a/openstack/tests/unit/cloud/test_security_groups.py b/openstack/tests/unit/cloud/test_security_groups.py index 3a27e70e8..3836aa880 100644 --- a/openstack/tests/unit/cloud/test_security_groups.py +++ b/openstack/tests/unit/cloud/test_security_groups.py @@ -59,7 +59,7 @@ def test_list_security_groups_neutron(self): dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'security-groups.json'], + append=['v2.0', 'security-groups'], qs_elements=["project_id=%s" % project_id]), json={'security_groups': [neutron_grp_dict]}) ]) @@ -93,12 +93,13 @@ def test_delete_security_group_neutron(self): dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'security-groups.json']), + append=['v2.0', 'security-groups']), json={'security_groups': [neutron_grp_dict]}), dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'security-groups', '%s.json' % sg_id]), + append=['v2.0', 'security-groups', '%s' % sg_id]), + status_code=200, json={}) ]) self.assertTrue(self.cloud.delete_security_group('1')) @@ -126,7 +127,7 @@ def test_delete_security_group_neutron_not_found(self): dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'security-groups.json']), + append=['v2.0', 'security-groups']), json={'security_groups': [neutron_grp_dict]}) ]) self.assertFalse(self.cloud.delete_security_group('10')) @@ -163,7 +164,7 @@ def test_create_security_group_neutron(self): dict(method='POST', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'security-groups.json']), + append=['v2.0', 'security-groups']), json={'security_group': new_group}, validate=dict( json={'security_group': { @@ -194,7 +195,7 @@ def test_create_security_group_neutron_specific_tenant(self): dict(method='POST', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'security-groups.json']), + append=['v2.0', 'security-groups']), json={'security_group': new_group}, validate=dict( json={'security_group': { @@ -260,12 +261,12 @@ def test_update_security_group_neutron(self): dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'security-groups.json']), + append=['v2.0', 'security-groups']), json={'security_groups': [neutron_grp_dict]}), dict(method='PUT', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'security-groups', '%s.json' % sg_id]), + append=['v2.0', 'security-groups', '%s' % sg_id]), json={'security_group': update_return}, validate=dict(json={ 'security_group': {'name': new_name}})) @@ -321,28 +322,36 @@ def test_create_security_group_rule_neutron(self): expected_new_rule = copy.copy(expected_args) expected_new_rule['id'] = '1234' - expected_new_rule['tenant_id'] = '' + expected_new_rule['tenant_id'] = None expected_new_rule['project_id'] = expected_new_rule['tenant_id'] self.register_uris([ dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'security-groups.json']), + append=['v2.0', 'security-groups']), json={'security_groups': [neutron_grp_dict]}), dict(method='POST', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'security-group-rules.json']), + append=['v2.0', 'security-group-rules']), json={'security_group_rule': expected_new_rule}, validate=dict(json={ 'security_group_rule': expected_args})) ]) new_rule = self.cloud.create_security_group_rule( - secgroup_name_or_id=neutron_grp_dict['id'], **args) - # NOTE(slaweq): don't check location and properties in new rule - new_rule.pop("location") - new_rule.pop("properties") + secgroup_name_or_id=neutron_grp_dict['id'], **args).to_dict( + original_names=True + ) + # NOTE(gtema): don't check location and not relevant properties + # in new rule + new_rule.pop('created_at') + new_rule.pop('description') + new_rule.pop('location') + new_rule.pop('name') + new_rule.pop('revision_number') + new_rule.pop('tags') + new_rule.pop('updated_at') self.assertEqual(expected_new_rule, new_rule) self.assert_calls() @@ -363,31 +372,36 @@ def test_create_security_group_rule_neutron_specific_tenant(self): expected_args['port_range_min'] = None expected_args['security_group_id'] = neutron_grp_dict['id'] expected_args['tenant_id'] = expected_args['project_id'] - expected_args.pop('project_id') expected_new_rule = copy.copy(expected_args) expected_new_rule['id'] = '1234' - expected_new_rule['project_id'] = expected_new_rule['tenant_id'] self.register_uris([ dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'security-groups.json']), + append=['v2.0', 'security-groups']), json={'security_groups': [neutron_grp_dict]}), dict(method='POST', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'security-group-rules.json']), + append=['v2.0', 'security-group-rules']), json={'security_group_rule': expected_new_rule}, validate=dict(json={ 'security_group_rule': expected_args})) ]) new_rule = self.cloud.create_security_group_rule( - secgroup_name_or_id=neutron_grp_dict['id'], ** args) + secgroup_name_or_id=neutron_grp_dict['id'], ** args).to_dict( + original_names=True + ) # NOTE(slaweq): don't check location and properties in new rule - new_rule.pop("location") - new_rule.pop("properties") + new_rule.pop('created_at') + new_rule.pop('description') + new_rule.pop('location') + new_rule.pop('name') + new_rule.pop('revision_number') + new_rule.pop('tags') + new_rule.pop('updated_at') self.assertEqual(expected_new_rule, new_rule) self.assert_calls() @@ -477,7 +491,7 @@ def test_delete_security_group_rule_neutron(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'security-group-rules', - '%s.json' % rule_id]), + '%s' % rule_id]), json={}) ]) self.assertTrue(self.cloud.delete_security_group_rule(rule_id)) @@ -509,7 +523,7 @@ def test_delete_security_group_rule_not_found(self): dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'security-groups.json']), + append=['v2.0', 'security-groups']), json={'security_groups': [neutron_grp_dict]}) ]) self.assertFalse(self.cloud.delete_security_group(rule_id)) @@ -618,7 +632,7 @@ def test_add_security_group_to_server_neutron(self): dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'security-groups.json']), + append=['v2.0', 'security-groups']), json={'security_groups': [neutron_grp_dict]}), dict(method='POST', uri='%s/servers/%s/action' % (fakes.COMPUTE_ENDPOINT, '1234'), @@ -674,7 +688,7 @@ def test_remove_security_group_from_server_neutron(self): dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'security-groups.json']), + append=['v2.0', 'security-groups']), json={'security_groups': [neutron_grp_dict]}), dict(method='POST', uri='%s/servers/%s/action' % (fakes.COMPUTE_ENDPOINT, '1234'), @@ -729,7 +743,7 @@ def test_add_bad_security_group_to_server_neutron(self): dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'security-groups.json']), + append=['v2.0', 'security-groups']), json={'security_groups': [neutron_grp_dict]}) ]) self.assertFalse(self.cloud.add_server_security_groups( diff --git a/openstack/tests/unit/network/v2/test_security_group.py b/openstack/tests/unit/network/v2/test_security_group.py index 90cfdd1c1..de2e77cf8 100644 --- a/openstack/tests/unit/network/v2/test_security_group.py +++ b/openstack/tests/unit/network/v2/test_security_group.py @@ -57,6 +57,7 @@ 'revision_number': 3, 'security_group_rules': RULES, 'tenant_id': '4', + 'project_id': '4', 'updated_at': '2016-10-14T12:16:57.233772', 'tags': ['5'] } @@ -75,6 +76,22 @@ def test_basic(self): self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) + self.assertDictEqual({'any_tags': 'tags-any', + 'description': 'description', + 'limit': 'limit', + 'marker': 'marker', + 'name': 'name', + 'not_any_tags': 'not-tags-any', + 'not_tags': 'not-tags', + 'project_id': 'project_id', + 'revision_number': 'revision_number', + 'sort_dir': 'sort_dir', + 'sort_key': 'sort_key', + 'tags': 'tags', + 'tenant_id': 'tenant_id' + }, + sot._query_mapping._mapping) + def test_make_it(self): sot = security_group.SecurityGroup(**EXAMPLE) self.assertEqual(EXAMPLE['created_at'], sot.created_at) @@ -85,6 +102,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['security_group_rules'], sot.security_group_rules) self.assertEqual(dict, type(sot.security_group_rules[0])) - self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertEqual(EXAMPLE['tenant_id'], sot.tenant_id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) self.assertEqual(EXAMPLE['tags'], sot.tags) diff --git a/openstack/tests/unit/network/v2/test_security_group_rule.py b/openstack/tests/unit/network/v2/test_security_group_rule.py index c793c04ad..981c81227 100644 --- a/openstack/tests/unit/network/v2/test_security_group_rule.py +++ b/openstack/tests/unit/network/v2/test_security_group_rule.py @@ -29,6 +29,7 @@ 'revision_number': 9, 'security_group_id': '10', 'tenant_id': '11', + 'project_id': '11', 'updated_at': '12' } @@ -46,6 +47,29 @@ def test_basic(self): self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) + self.assertDictEqual({'any_tags': 'tags-any', + 'description': 'description', + 'direction': 'direction', + 'ether_type': 'ethertype', + 'limit': 'limit', + 'marker': 'marker', + 'not_any_tags': 'not-tags-any', + 'not_tags': 'not-tags', + 'port_range_max': 'port_range_max', + 'port_range_min': 'port_range_min', + 'project_id': 'project_id', + 'protocol': 'protocol', + 'remote_group_id': 'remote_group_id', + 'remote_ip_prefix': 'remote_ip_prefix', + 'revision_number': 'revision_number', + 'security_group_id': 'security_group_id', + 'sort_dir': 'sort_dir', + 'sort_key': 'sort_key', + 'tags': 'tags', + 'tenant_id': 'tenant_id' + }, + sot._query_mapping._mapping) + def test_make_it(self): sot = security_group_rule.SecurityGroupRule(**EXAMPLE) self.assertEqual(EXAMPLE['created_at'], sot.created_at) @@ -60,5 +84,6 @@ def test_make_it(self): self.assertEqual(EXAMPLE['remote_ip_prefix'], sot.remote_ip_prefix) self.assertEqual(EXAMPLE['revision_number'], sot.revision_number) self.assertEqual(EXAMPLE['security_group_id'], sot.security_group_id) - self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertEqual(EXAMPLE['tenant_id'], sot.tenant_id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) From fdf43169e35232e38f1c24b9f68bef0f85f67595 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 11 Jun 2019 08:19:19 -0500 Subject: [PATCH 2479/3836] Set xenapi_use_agent to "False" instead of false The correct remote value is "False". It's supposed to be a string. Set it that way. Story: #2005851 Task: #33637 Change-Id: I44b6fc9fe12422024392d60573862a646046564a --- openstack/config/vendors/rackspace.json | 2 +- releasenotes/notes/xenapi-use-agent-ecc33e520da81ffa.yaml | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/xenapi-use-agent-ecc33e520da81ffa.yaml diff --git a/openstack/config/vendors/rackspace.json b/openstack/config/vendors/rackspace.json index fd1cf89ca..00a454ff3 100644 --- a/openstack/config/vendors/rackspace.json +++ b/openstack/config/vendors/rackspace.json @@ -22,7 +22,7 @@ "block_storage_api_version": "1", "disable_vendor_agent": { "vm_mode": "hvm", - "xenapi_use_agent": false + "xenapi_use_agent": "False" }, "has_network": false } diff --git a/releasenotes/notes/xenapi-use-agent-ecc33e520da81ffa.yaml b/releasenotes/notes/xenapi-use-agent-ecc33e520da81ffa.yaml new file mode 100644 index 000000000..eeb3ed77d --- /dev/null +++ b/releasenotes/notes/xenapi-use-agent-ecc33e520da81ffa.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Updated the Rackspace vendor entry to use `"False"` for the value of + `xenapi_use_agent` instead of `false`, because that's what the remote + side expects. The recent update to use the Resource layer exposed + the incorrect setting causing image uploads to Rackspace to fail. From 15baef656ac56421a71e691982a70b218110f18d Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 14 Jun 2019 15:03:20 +0200 Subject: [PATCH 2480/3836] Use Resource layer for compute KeyPairs Move KeyPairs logic from the cloud layer to resource. Change-Id: I2c06a5c76d9b2fa872c1cbfab528ed805aa5063a --- openstack/cloud/_compute.py | 28 ++++++------- openstack/compute/v2/_proxy.py | 6 ++- openstack/compute/v2/keypair.py | 39 +++++++++++++------ openstack/tests/unit/cloud/test_keypair.py | 36 ++++++++++++++++- .../tests/unit/compute/v2/test_keypair.py | 21 +++++++++- 5 files changed, 97 insertions(+), 33 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index a883ad3c7..934f8c843 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -80,7 +80,9 @@ def _has_nova_extension(self, extension_name): return extension_name in self._nova_extensions() def search_keypairs(self, name_or_id=None, filters=None): - keypairs = self.list_keypairs() + keypairs = self.list_keypairs( + filters=filters if isinstance(filters, dict) else None + ) return _utils._filter_list(keypairs, name_or_id, filters) def search_flavors(self, name_or_id=None, filters=None, get_extra=True): @@ -115,17 +117,16 @@ def search_server_groups(self, name_or_id=None, filters=None): server_groups = self.list_server_groups() return _utils._filter_list(server_groups, name_or_id, filters) - def list_keypairs(self): + def list_keypairs(self, filters=None): """List all available keypairs. :returns: A list of ``munch.Munch`` containing keypair info. """ - data = proxy._json_response( - self.compute.get('/os-keypairs'), - error_message="Error fetching keypair list") - return self._normalize_keypairs([ - k['keypair'] for k in self._get_and_munchify('keypairs', data)]) + if not filters: + filters = {} + return list(self.compute.keypairs(allow_unknown_params=True, + **filters)) @_utils.cache_on_arguments() def list_availability_zone_names(self, unavailable=False): @@ -620,13 +621,7 @@ def create_keypair(self, name, public_key=None): } if public_key: keypair['public_key'] = public_key - data = proxy._json_response( - self.compute.post( - '/os-keypairs', - json={'keypair': keypair}), - error_message="Unable to create keypair {name}".format(name=name)) - return self._normalize_keypair( - self._get_and_munchify('keypair', data)) + return self.compute.create_keypair(**keypair) def delete_keypair(self, name): """Delete a keypair. @@ -638,9 +633,8 @@ def delete_keypair(self, name): :raises: OpenStackCloudException on operation error. """ try: - proxy._json_response(self.compute.delete( - '/os-keypairs/{name}'.format(name=name))) - except exc.OpenStackCloudURINotFound: + self.compute.delete_keypair(name, ignore_missing=False) + except exceptions.ResourceNotFound: self.log.debug("Keypair %s not found for deleting", name) return False return True diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 0e1434ea4..232169409 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -403,13 +403,15 @@ def find_keypair(self, name_or_id, ignore_missing=True): return self._find(_keypair.Keypair, name_or_id, ignore_missing=ignore_missing) - def keypairs(self): + def keypairs(self, **query): """Return a generator of keypairs + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. :returns: A generator of keypair objects :rtype: :class:`~openstack.compute.v2.keypair.Keypair` """ - return self._list(_keypair.Keypair) + return self._list(_keypair.Keypair, **query) def get_limits(self): """Retrieve limits that are applied to the project's account diff --git a/openstack/compute/v2/keypair.py b/openstack/compute/v2/keypair.py index 607aed774..f3886b637 100644 --- a/openstack/compute/v2/keypair.py +++ b/openstack/compute/v2/keypair.py @@ -18,6 +18,9 @@ class Keypair(resource.Resource): resources_key = 'keypairs' base_path = '/os-keypairs' + _query_mapping = resource.QueryParameters( + 'user_id') + # capabilities allow_create = True allow_fetch = True @@ -25,6 +28,10 @@ class Keypair(resource.Resource): allow_list = True # Properties + #: The date and time when the resource was created. + created_at = resource.Body('created_at') + #: A boolean indicates whether this keypair is deleted or not. + is_deleted = resource.Body('deleted', type=bool) #: The short fingerprint associated with the ``public_key`` for #: this keypair. fingerprint = resource.Body('fingerprint') @@ -42,6 +49,10 @@ class Keypair(resource.Resource): private_key = resource.Body('private_key') #: The SSH public key that is paired with the server. public_key = resource.Body('public_key') + #: The type of the keypair. + type = resource.Body('type', default='ssh') + #: The user_id for a keypair. + user_id = resource.Body('user_id') def _consume_attrs(self, mapping, attrs): # TODO(mordred) This should not be required. However, without doing @@ -51,16 +62,22 @@ def _consume_attrs(self, mapping, attrs): return super(Keypair, self)._consume_attrs(mapping, attrs) @classmethod - def list(cls, session, paginated=False, base_path=None): - - if base_path is None: - base_path = cls.base_path + def existing(cls, connection=None, **kwargs): + """Create an instance of an existing remote resource. - resp = session.get(base_path, - headers={"Accept": "application/json"}) - resp = resp.json() - resp = resp[cls.resources_key] + When creating the instance set the ``_synchronized`` parameter + of :class:`Resource` to ``True`` to indicate that it represents the + state of an existing server-side resource. As such, all attributes + passed in ``**kwargs`` are considered "clean", such that an immediate + :meth:`update` call would not generate a body of attributes to be + modified on the server. - for data in resp: - value = cls.existing(**data[cls.resource_key]) - yield value + :param dict kwargs: Each of the named arguments will be set as + attributes on the resulting Resource object. + """ + # Listing KPs return list with resource_key structure. Instead of + # overriding whole list just try to create object smart. + if cls.resource_key in kwargs: + args = kwargs.pop(cls.resource_key) + kwargs.update(**args) + return cls(_synchronized=True, connection=connection, **kwargs) diff --git a/openstack/tests/unit/cloud/test_keypair.py b/openstack/tests/unit/cloud/test_keypair.py index 7e84622d0..6dbad0ea7 100644 --- a/openstack/tests/unit/cloud/test_keypair.py +++ b/openstack/tests/unit/cloud/test_keypair.py @@ -38,7 +38,10 @@ def test_create_keypair(self): new_key = self.cloud.create_keypair( self.keyname, self.key['public_key']) - self.assertEqual(new_key, self.cloud._normalize_keypair(self.key)) + new_key_cmp = new_key.to_dict(ignore_none=True) + new_key_cmp.pop('location') + new_key_cmp.pop('id') + self.assertEqual(new_key_cmp, self.key) self.assert_calls() @@ -94,7 +97,36 @@ def test_list_keypairs(self): ]) keypairs = self.cloud.list_keypairs() - self.assertEqual(keypairs, self.cloud._normalize_keypairs([self.key])) + self.assertEqual(len(keypairs), 1) + self.assertEqual(keypairs[0].name, self.key['name']) + self.assert_calls() + + def test_list_keypairs_empty_filters(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-keypairs']), + json={'keypairs': [{'keypair': self.key}]}), + + ]) + keypairs = self.cloud.list_keypairs(filters=None) + self.assertEqual(len(keypairs), 1) + self.assertEqual(keypairs[0].name, self.key['name']) + self.assert_calls() + + def test_list_keypairs_notempty_filters(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-keypairs'], + qs_elements=['user_id=b']), + json={'keypairs': [{'keypair': self.key}]}), + + ]) + keypairs = self.cloud.list_keypairs( + filters={'user_id': 'b', 'fake': 'dummy'}) + self.assertEqual(len(keypairs), 1) + self.assertEqual(keypairs[0].name, self.key['name']) self.assert_calls() def test_list_keypairs_exception(self): diff --git a/openstack/tests/unit/compute/v2/test_keypair.py b/openstack/tests/unit/compute/v2/test_keypair.py index c058b5507..6c6e5e8a6 100644 --- a/openstack/tests/unit/compute/v2/test_keypair.py +++ b/openstack/tests/unit/compute/v2/test_keypair.py @@ -15,10 +15,14 @@ from openstack.compute.v2 import keypair EXAMPLE = { + 'created_at': 'some_time', + 'deleted': False, 'fingerprint': '1', 'name': '2', 'public_key': '3', - 'private_key': '3', + 'private_key': '4', + 'type': 'ssh', + 'user_id': '5' } @@ -35,9 +39,24 @@ def test_basic(self): self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) + self.assertDictEqual({'limit': 'limit', + 'marker': 'marker', + 'user_id': 'user_id'}, + sot._query_mapping._mapping) + def test_make_it(self): sot = keypair.Keypair(**EXAMPLE) + self.assertEqual(EXAMPLE['created_at'], sot.created_at) + self.assertEqual(EXAMPLE['deleted'], sot.is_deleted) self.assertEqual(EXAMPLE['fingerprint'], sot.fingerprint) self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['public_key'], sot.public_key) self.assertEqual(EXAMPLE['private_key'], sot.private_key) + self.assertEqual(EXAMPLE['type'], sot.type) + self.assertEqual(EXAMPLE['user_id'], sot.user_id) + + def test_make_it_defaults(self): + EXAMPLE_DEFAULT = EXAMPLE.copy() + EXAMPLE_DEFAULT.pop('type') + sot = keypair.Keypair(**EXAMPLE_DEFAULT) + self.assertEqual(EXAMPLE['type'], sot.type) From b94a8ce4a9d7de66736dccc791d72ba8f4d888f5 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Mon, 17 Jun 2019 13:17:18 +0200 Subject: [PATCH 2481/3836] from_conf: fix handling service names with dashes Services like ironic-inspector are likely to be represented as ironic_inspector in the configuration, so fall back to underscores if the variant with dashes is not found. Change-Id: I765a0722aa718ab8e430c3f8fc31c192091e9ca1 --- openstack/config/cloud_region.py | 16 ++++++---- openstack/tests/unit/config/test_from_conf.py | 29 +++++++++++++++++++ 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index abce4e97b..7728f2226 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -142,12 +142,16 @@ def from_conf(conf, session=None, **kwargs): for st in stm.all_types_by_service_type: project_name = stm.get_project_name(st) if project_name not in conf: - _disable_service( - config_dict, st, - reason="No section for project '{project}' (service type " - "'{service_type}') was present in the config.".format( - project=project_name, service_type=st)) - continue + if '-' in project_name: + project_name = project_name.replace('-', '_') + + if project_name not in conf: + _disable_service( + config_dict, st, + reason="No section for project '{project}' (service type " + "'{service_type}') was present in the config." + .format(project=project_name, service_type=st)) + continue opt_dict = {} # Populate opt_dict with (appropriately processed) Adapter conf opts try: diff --git a/openstack/tests/unit/config/test_from_conf.py b/openstack/tests/unit/config/test_from_conf.py index 4f83b5f10..1050e37f8 100644 --- a/openstack/tests/unit/config/test_from_conf.py +++ b/openstack/tests/unit/config/test_from_conf.py @@ -38,6 +38,10 @@ def setUp(self): 'interface': 'internal', 'endpoint_override': 'https://example.org:8888/heat/v2' }, + # test a service with dashes + 'ironic_inspector': { + 'endpoint_override': 'https://example.org:5050', + }, } def _load_ks_cfg_opts(self): @@ -123,6 +127,31 @@ def test_default_adapter_opts(self): self.assertEqual(s.name, server_name) self.assert_calls() + def test_name_with_dashes(self): + conn = self._get_conn() + + discovery = { + "versions": { + "values": [ + {"status": "stable", + "id": "v1", + "links": [{ + "href": "https://example.org:5050/v1", + "rel": "self"}] + }] + } + } + self.register_uris([ + dict(method='GET', + uri='https://example.org:5050', + json=discovery), + ]) + + adap = conn.baremetal_introspection + self.assertEqual('baremetal-introspection', adap.service_type) + self.assertEqual('public', adap.interface) + self.assertEqual('https://example.org:5050', adap.endpoint_override) + def _test_missing_invalid_permutations(self, expected_reason): # Do special things to self.oslo_config_dict['heat'] before calling # this method. From 245ebae563fe4440c3e96a4e9f8db90e72a42680 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 14 Jun 2019 21:29:28 +0200 Subject: [PATCH 2482/3836] Switch cloud layer to use proxy for DNS - use dns proxy in cloud layer - fix DNS pagination (not-standard - argh) - fix wrongly set resource_key for recordset - add create_recordset in functional tests of dns - streamline cloud tests of dns - add required find_recordset function to the proxy Change-Id: I28745475506848e0f205efd4427cbe2e9e81df17 Story: 2005749 Task: 33416 --- openstack/cloud/_dns.py | 138 +++---- openstack/dns/v2/_base.py | 105 +++++ openstack/dns/v2/_proxy.py | 21 +- openstack/dns/v2/floating_ip.py | 5 +- openstack/dns/v2/recordset.py | 6 +- openstack/dns/v2/zone.py | 4 +- openstack/dns/v2/zone_export.py | 4 +- openstack/dns/v2/zone_import.py | 4 +- openstack/dns/v2/zone_transfer.py | 4 +- .../tests/functional/dns/v2/test_zone.py | 11 + openstack/tests/unit/cloud/test_recordset.py | 372 ++++++++++++------ openstack/tests/unit/cloud/test_zone.py | 179 ++++++--- openstack/tests/unit/dns/v2/test_proxy.py | 23 +- openstack/tests/unit/dns/v2/test_recordset.py | 2 +- 14 files changed, 602 insertions(+), 276 deletions(-) create mode 100644 openstack/dns/v2/_base.py diff --git a/openstack/cloud/_dns.py b/openstack/cloud/_dns.py index 6796b3666..52449e770 100644 --- a/openstack/cloud/_dns.py +++ b/openstack/cloud/_dns.py @@ -15,6 +15,8 @@ # openstack.resource.Resource.list and openstack.resource2.Resource.list import types # noqa +from openstack import exceptions +from openstack import resource from openstack.cloud import exc from openstack.cloud import _normalize from openstack.cloud import _utils @@ -22,24 +24,16 @@ class DnsCloudMixin(_normalize.Normalizer): - @property - def _dns_client(self): - if 'dns' not in self._raw_clients: - dns_client = self._get_versioned_client( - 'dns', min_version=2, max_version='2.latest') - self._raw_clients['dns'] = dns_client - return self._raw_clients['dns'] - - def list_zones(self): + def list_zones(self, filters=None): """List all available zones. :returns: A list of zones dicts. """ - data = self._dns_client.get( - "/zones", - error_message="Error fetching zones list") - return self._get_and_munchify('zones', data) + if not filters: + filters = {} + return list(self.dns.zones(allow_unknown_params=True, + **filters)) def get_zone(self, name_or_id, filters=None): """Get a zone by name or ID. @@ -47,17 +41,20 @@ def get_zone(self, name_or_id, filters=None): :param name_or_id: Name or ID of the zone :param filters: A dictionary of meta data to use for further filtering - OR - A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A zone dict or None if no matching zone is found. """ - return _utils._get_entity(self, 'zone', name_or_id, filters) + if not filters: + filters = {} + zone = self.dns.find_zone( + name_or_id=name_or_id, ignore_missing=True, **filters) + if not zone: + return False + return zone def search_zones(self, name_or_id=None, filters=None): - zones = self.list_zones() + zones = self.list_zones(filters) return _utils._filter_list(zones, name_or_id, filters) def create_zone(self, name, zone_type=None, email=None, description=None, @@ -101,10 +98,12 @@ def create_zone(self, name, zone_type=None, email=None, description=None, if masters is not None: zone["masters"] = masters - data = self._dns_client.post( - "/zones", json=zone, - error_message="Unable to create zone {name}".format(name=name)) - return self._get_and_munchify(key=None, data=data) + try: + return self.dns.create_zone(**zone) + except exceptions.SDKException as e: + raise exc.OpenStackCloudException( + "Unable to create zone {name}".format(name=name) + ) @_utils.valid_kwargs('email', 'description', 'ttl', 'masters') def update_zone(self, name_or_id, **kwargs): @@ -127,10 +126,7 @@ def update_zone(self, name_or_id, **kwargs): raise exc.OpenStackCloudException( "Zone %s not found." % name_or_id) - data = self._dns_client.patch( - "/zones/{zone_id}".format(zone_id=zone['id']), json=kwargs, - error_message="Error updating zone {0}".format(name_or_id)) - return self._get_and_munchify(key=None, data=data) + return self.dns.update_zone(zone['id'], **kwargs) def delete_zone(self, name_or_id): """Delete a zone. @@ -142,54 +138,56 @@ def delete_zone(self, name_or_id): :raises: OpenStackCloudException on operation error. """ - zone = self.get_zone(name_or_id) - if zone is None: + zone = self.dns.find_zone(name_or_id) + if not zone: self.log.debug("Zone %s not found for deleting", name_or_id) return False - return self._dns_client.delete( - "/zones/{zone_id}".format(zone_id=zone['id']), - error_message="Error deleting zone {0}".format(name_or_id)) + self.dns.delete_zone(zone) return True def list_recordsets(self, zone): """List all available recordsets. - :param zone: Name or ID of the zone managing the recordset + :param zone: Name, ID or :class:`openstack.dns.v2.zone.Zone` instance + of the zone managing the recordset. :returns: A list of recordsets. """ - zone_obj = self.get_zone(zone) + if isinstance(zone, resource.Resource): + zone_obj = zone + else: + zone_obj = self.get_zone(zone) if zone_obj is None: raise exc.OpenStackCloudException( "Zone %s not found." % zone) - return self._dns_client.get( - "/zones/{zone_id}/recordsets".format(zone_id=zone_obj['id']), - error_message="Error fetching recordsets list")['recordsets'] + return list(self.dns.recordsets(zone_obj)) def get_recordset(self, zone, name_or_id): """Get a recordset by name or ID. - :param zone: Name or ID of the zone managing the recordset + :param zone: Name, ID or :class:`openstack.dns.v2.zone.Zone` instance + of the zone managing the recordset. :param name_or_id: Name or ID of the recordset - :returns: A recordset dict or None if no matching recordset is + :returns: A recordset dict or False if no matching recordset is found. """ - zone_obj = self.get_zone(zone) - if zone_obj is None: + if isinstance(zone, resource.Resource): + zone_obj = zone + else: + zone_obj = self.get_zone(zone) + if not zone_obj: raise exc.OpenStackCloudException( "Zone %s not found." % zone) try: - return self._dns_client.get( - "/zones/{zone_id}/recordsets/{recordset_id}".format( - zone_id=zone_obj['id'], recordset_id=name_or_id), - error_message="Error fetching recordset") + return self.dns.find_recordset( + zone=zone_obj, name_or_id=name_or_id, ignore_missing=False) except Exception: - return None + return False def search_recordsets(self, zone, name_or_id=None, filters=None): recordsets = self.list_recordsets(zone=zone) @@ -199,7 +197,8 @@ def create_recordset(self, zone, name, recordset_type, records, description=None, ttl=None): """Create a recordset. - :param zone: Name or ID of the zone managing the recordset + :param zone: Name, ID or :class:`openstack.dns.v2.zone.Zone` instance + of the zone managing the recordset. :param name: Name of the recordset :param recordset_type: Type of the recordset :param records: List of the recordset definitions @@ -211,8 +210,11 @@ def create_recordset(self, zone, name, recordset_type, records, :raises: OpenStackCloudException on operation error. """ - zone_obj = self.get_zone(zone) - if zone_obj is None: + if isinstance(zone, resource.Resource): + zone_obj = zone + else: + zone_obj = self.get_zone(zone) + if not zone_obj: raise exc.OpenStackCloudException( "Zone %s not found." % zone) @@ -231,16 +233,14 @@ def create_recordset(self, zone, name, recordset_type, records, if ttl: body['ttl'] = ttl - return self._dns_client.post( - "/zones/{zone_id}/recordsets".format(zone_id=zone_obj['id']), - json=body, - error_message="Error creating recordset {name}".format(name=name)) + return self.dns.create_recordset(zone=zone_obj, **body) @_utils.valid_kwargs('description', 'ttl', 'records') def update_recordset(self, zone, name_or_id, **kwargs): """Update a recordset. - :param zone: Name or ID of the zone managing the recordset + :param zone: Name, ID or :class:`openstack.dns.v2.zone.Zone` instance + of the zone managing the recordset. :param name_or_id: Name or ID of the recordset being updated. :param records: List of the recordset definitions :param description: Description of the recordset @@ -250,27 +250,21 @@ def update_recordset(self, zone, name_or_id, **kwargs): :raises: OpenStackCloudException on operation error. """ - zone_obj = self.get_zone(zone) - if zone_obj is None: - raise exc.OpenStackCloudException( - "Zone %s not found." % zone) - recordset_obj = self.get_recordset(zone, name_or_id) - if recordset_obj is None: + rs = self.get_recordset(zone, name_or_id) + if not rs: raise exc.OpenStackCloudException( "Recordset %s not found." % name_or_id) - new_recordset = self._dns_client.put( - "/zones/{zone_id}/recordsets/{recordset_id}".format( - zone_id=zone_obj['id'], recordset_id=name_or_id), json=kwargs, - error_message="Error updating recordset {0}".format(name_or_id)) + rs = self.dns.update_recordset(recordset=rs, **kwargs) - return new_recordset + return rs def delete_recordset(self, zone, name_or_id): """Delete a recordset. - :param zone: Name or ID of the zone managing the recordset. + :param zone: Name, ID or :class:`openstack.dns.v2.zone.Zone` instance + of the zone managing the recordset. :param name_or_id: Name or ID of the recordset being deleted. :returns: True if delete succeeded, False otherwise. @@ -278,19 +272,11 @@ def delete_recordset(self, zone, name_or_id): :raises: OpenStackCloudException on operation error. """ - zone_obj = self.get_zone(zone) - if zone_obj is None: - self.log.debug("Zone %s not found for deleting", zone) - return False - - recordset = self.get_recordset(zone_obj['id'], name_or_id) - if recordset is None: + recordset = self.get_recordset(zone, name_or_id) + if not recordset: self.log.debug("Recordset %s not found for deleting", name_or_id) return False - self._dns_client.delete( - "/zones/{zone_id}/recordsets/{recordset_id}".format( - zone_id=zone_obj['id'], recordset_id=name_or_id), - error_message="Error deleting recordset {0}".format(name_or_id)) + self.dns.delete_recordset(recordset, ignore_missing=False) return True diff --git a/openstack/dns/v2/_base.py b/openstack/dns/v2/_base.py new file mode 100644 index 000000000..40601a5f7 --- /dev/null +++ b/openstack/dns/v2/_base.py @@ -0,0 +1,105 @@ +# 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 + +from openstack import exceptions +from openstack import resource + + +class Resource(resource.Resource): + + @classmethod + def find(cls, session, name_or_id, ignore_missing=True, **params): + """Find a resource by its name or id. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param name_or_id: This resource's identifier, if needed by + the request. The default is ``None``. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict params: Any additional parameters to be passed into + underlying methods, such as to + :meth:`~openstack.resource.Resource.existing` + in order to pass on URI parameters. + + :return: The :class:`Resource` object matching the given name or id + or None if nothing matches. + :raises: :class:`openstack.exceptions.DuplicateResource` if more + than one resource is found for this request. + :raises: :class:`openstack.exceptions.ResourceNotFound` if nothing + is found and ignore_missing is ``False``. + """ + session = cls._get_session(session) + # Try to short-circuit by looking directly for a matching ID. + try: + match = cls.existing( + id=name_or_id, + connection=session._get_connection(), + **params) + return match.fetch(session, **params) + except exceptions.SDKException: + # DNS may return 400 when we try to do GET with name + pass + + if ('name' in cls._query_mapping._mapping.keys() + and 'name' not in params): + params['name'] = name_or_id + + data = cls.list(session, **params) + + result = cls._get_one_match(name_or_id, data) + if result is not None: + return result + + if ignore_missing: + return None + raise exceptions.ResourceNotFound( + "No %s found for %s" % (cls.__name__, name_or_id)) + + @classmethod + def _get_next_link(cls, uri, response, data, marker, limit, total_yielded): + next_link = None + params = {} + if isinstance(data, dict): + links = data.get('links') + if links: + next_link = links.get('next') + + total = data.get('metadata', {}).get('total_count') + if total: + # We have a kill switch + total_count = int(total) + if total_count <= total_yielded: + return None, params + + # Parse params from Link (next page URL) into params. + # This prevents duplication of query parameters that with large + # number of pages result in HTTP 414 error eventually. + if next_link: + parts = six.moves.urllib.parse.urlparse(next_link) + query_params = six.moves.urllib.parse.parse_qs(parts.query) + params.update(query_params) + next_link = six.moves.urllib.parse.urljoin(next_link, + parts.path) + + # If we still have no link, and limit was given and is non-zero, + # and the number of records yielded equals the limit, then the user + # is playing pagination ball so we should go ahead and try once more. + if not next_link and limit: + next_link = uri + params['marker'] = marker + params['limit'] = limit + return next_link, params diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py index 7e36b0c5e..ff357e927 100644 --- a/openstack/dns/v2/_proxy.py +++ b/openstack/dns/v2/_proxy.py @@ -88,7 +88,7 @@ def update_zone(self, zone, **attrs): """ return self._update(_zone.Zone, zone, **attrs) - def find_zone(self, name_or_id, ignore_missing=True): + def find_zone(self, name_or_id, ignore_missing=True, **attrs): """Find a single zone :param name_or_id: The name or ID of a zone @@ -216,6 +216,25 @@ def delete_recordset(self, recordset, zone=None, ignore_missing=True): return self._delete(_rs.Recordset, recordset, ignore_missing=ignore_missing) + def find_recordset(self, zone, name_or_id, ignore_missing=True, **attrs): + """Find a single recordset + + :param zone: The value can be the ID of a zone + or a :class:`~openstack.dns.v2.zone.Zone` instance. + :param name_or_id: The name or ID of a zone + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the zone does not exist. + When set to ``True``, no exception will be set when attempting + to delete a nonexistent zone. + + :returns: :class:`~openstack.dns.v2.recordset.Recordset` + """ + zone = self._get_resource(_zone.Zone, zone) + return self._find(_rs.Recordset, name_or_id, + ignore_missing=ignore_missing, zone_id=zone.id, + **attrs) + # ======== Zone Imports ======== def zone_imports(self, **query): """Retrieve a generator of zone imports diff --git a/openstack/dns/v2/floating_ip.py b/openstack/dns/v2/floating_ip.py index f6b4eec97..703b04570 100644 --- a/openstack/dns/v2/floating_ip.py +++ b/openstack/dns/v2/floating_ip.py @@ -9,11 +9,12 @@ # 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 import resource +from openstack.dns.v2 import _base -class FloatingIP(resource.Resource): + +class FloatingIP(_base.Resource): """DNS Floating IP Resource""" resource_key = '' resources_key = 'floatingips' diff --git a/openstack/dns/v2/recordset.py b/openstack/dns/v2/recordset.py index 949b25d7e..3ad5aace9 100644 --- a/openstack/dns/v2/recordset.py +++ b/openstack/dns/v2/recordset.py @@ -9,13 +9,13 @@ # 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 import resource +from openstack.dns.v2 import _base -class Recordset(resource.Resource): + +class Recordset(_base.Resource): """DNS Recordset Resource""" - resource_key = 'recordset' resources_key = 'recordsets' base_path = '/zones/%(zone_id)s/recordsets' diff --git a/openstack/dns/v2/zone.py b/openstack/dns/v2/zone.py index 737792bcf..ccee3015a 100644 --- a/openstack/dns/v2/zone.py +++ b/openstack/dns/v2/zone.py @@ -13,8 +13,10 @@ from openstack import resource from openstack import utils +from openstack.dns.v2 import _base -class Zone(resource.Resource): + +class Zone(_base.Resource): """DNS ZONE Resource""" resources_key = 'zones' base_path = '/zones' diff --git a/openstack/dns/v2/zone_export.py b/openstack/dns/v2/zone_export.py index 6cd5c9232..1d551235e 100644 --- a/openstack/dns/v2/zone_export.py +++ b/openstack/dns/v2/zone_export.py @@ -12,8 +12,10 @@ from openstack import exceptions from openstack import resource +from openstack.dns.v2 import _base -class ZoneExport(resource.Resource): + +class ZoneExport(_base.Resource): """DNS Zone Exports Resource""" resource_key = '' resources_key = 'exports' diff --git a/openstack/dns/v2/zone_import.py b/openstack/dns/v2/zone_import.py index 5a18bdcc7..1f642e00b 100644 --- a/openstack/dns/v2/zone_import.py +++ b/openstack/dns/v2/zone_import.py @@ -12,8 +12,10 @@ from openstack import exceptions from openstack import resource +from openstack.dns.v2 import _base -class ZoneImport(resource.Resource): + +class ZoneImport(_base.Resource): """DNS Zone Import Resource""" resource_key = '' resources_key = 'imports' diff --git a/openstack/dns/v2/zone_transfer.py b/openstack/dns/v2/zone_transfer.py index 2bd95fe3c..989b6f62f 100644 --- a/openstack/dns/v2/zone_transfer.py +++ b/openstack/dns/v2/zone_transfer.py @@ -11,8 +11,10 @@ # under the License. from openstack import resource +from openstack.dns.v2 import _base -class ZoneTransferBase(resource.Resource): + +class ZoneTransferBase(_base.Resource): """DNS Zone Transfer Request/Accept Base Resource""" _query_mapping = resource.QueryParameters( diff --git a/openstack/tests/functional/dns/v2/test_zone.py b/openstack/tests/functional/dns/v2/test_zone.py index 2d4527cc8..e38b48782 100644 --- a/openstack/tests/functional/dns/v2/test_zone.py +++ b/openstack/tests/functional/dns/v2/test_zone.py @@ -50,3 +50,14 @@ def test_get_zone(self): def test_list_zones(self): names = [f.name for f in self.conn.dns.zones()] self.assertIn(self.ZONE_NAME, names) + + def test_create_rs(self): + zone = self.conn.dns.get_zone(self.zone) + self.assertIsNotNone(self.conn.dns.create_recordset( + zone=zone, + name='www.{zone}'.format(zone=zone.name), + type='A', + description='Example zone rec', + ttl=3600, + records=['192.168.1.1'] + )) diff --git a/openstack/tests/unit/cloud/test_recordset.py b/openstack/tests/unit/cloud/test_recordset.py index 7b7aa1c8e..1d1424310 100644 --- a/openstack/tests/unit/cloud/test_recordset.py +++ b/openstack/tests/unit/cloud/test_recordset.py @@ -9,36 +9,28 @@ # 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 testtools - -import openstack.cloud +from openstack import exceptions from openstack.tests.unit import base +from openstack.tests.unit.cloud import test_zone -zone = { - 'id': '1', - 'name': 'example.net.', - 'type': 'PRIMARY', - 'email': 'test@example.net', - 'description': 'Example zone', - 'ttl': 3600, -} + +zone = test_zone.zone_dict recordset = { 'name': 'www.example.net.', 'type': 'A', - 'description': 'Example zone', + 'description': 'Example zone rec', 'ttl': 3600, - 'records': ['192.168.1.1'] + 'records': ['192.168.1.1'], + 'id': '1', + 'zone_id': zone['id'], + 'zone_name': zone['name'] } -recordset_zone = '1' -new_recordset = copy.copy(recordset) -new_recordset['id'] = '1' -new_recordset['zone'] = recordset_zone + +class RecordsetTestWrapper(test_zone.ZoneTestWrapper): + pass class TestRecordset(base.TestCase): @@ -47,43 +39,86 @@ def setUp(self): super(TestRecordset, self).setUp() self.use_designate() - def test_create_recordset(self): + def test_create_recordset_zoneid(self): + fake_zone = test_zone.ZoneTestWrapper(self, zone) + fake_rs = RecordsetTestWrapper(self, recordset) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['v2', 'zones', fake_zone['id']]), + json=fake_zone.get_get_response_json()), + dict(method='POST', + uri=self.get_mock_url( + 'dns', 'public', + append=['v2', 'zones', zone['id'], 'recordsets']), + json=fake_rs.get_create_response_json(), + validate=dict(json={ + "records": fake_rs['records'], + "type": fake_rs['type'], + "name": fake_rs['name'], + "description": fake_rs['description'], + "ttl": fake_rs['ttl'] + })), + ]) + rs = self.cloud.create_recordset( + zone=fake_zone['id'], + name=fake_rs['name'], + recordset_type=fake_rs['type'], + records=fake_rs['records'], + description=fake_rs['description'], + ttl=fake_rs['ttl']) + + fake_rs.cmp(rs) + self.assert_calls() + + def test_create_recordset_zonename(self): + fake_zone = test_zone.ZoneTestWrapper(self, zone) + fake_rs = RecordsetTestWrapper(self, recordset) self.register_uris([ + # try by directly dict(method='GET', uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones']), - json={ - "zones": [zone], - "links": {}, - "metadata": { - 'total_count': 1}}), + 'dns', 'public', + append=['v2', 'zones', fake_zone['name']]), + status_code=404), + # list with name + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['v2', 'zones'], + qs_elements=[ + 'name={name}'.format(name=fake_zone['name'])]), + json={'zones': [fake_zone.get_get_response_json()]}), dict(method='POST', uri=self.get_mock_url( 'dns', 'public', append=['v2', 'zones', zone['id'], 'recordsets']), - json=new_recordset, - validate=dict(json=recordset)), + json=fake_rs.get_create_response_json(), + validate=dict(json={ + "records": fake_rs['records'], + "type": fake_rs['type'], + "name": fake_rs['name'], + "description": fake_rs['description'], + "ttl": fake_rs['ttl'] + })), ]) rs = self.cloud.create_recordset( - zone=recordset_zone, - name=recordset['name'], - recordset_type=recordset['type'], - records=recordset['records'], - description=recordset['description'], - ttl=recordset['ttl']) - self.assertEqual(new_recordset, rs) + zone=fake_zone['name'], + name=fake_rs['name'], + recordset_type=fake_rs['type'], + records=fake_rs['records'], + description=fake_rs['description'], + ttl=fake_rs['ttl']) + + fake_rs.cmp(rs) self.assert_calls() def test_create_recordset_exception(self): + fake_zone = test_zone.ZoneTestWrapper(self, zone) self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones']), - json={ - "zones": [zone], - "links": {}, - "metadata": { - 'total_count': 1}}), + 'dns', 'public', append=['v2', 'zones', fake_zone['id']]), + json=fake_zone.get_get_response_json()), dict(method='POST', uri=self.get_mock_url( 'dns', 'public', @@ -94,149 +129,238 @@ def test_create_recordset_exception(self): 'records': ['192.168.1.2'], 'type': 'A'})), ]) - with testtools.ExpectedException( - openstack.cloud.exc.OpenStackCloudHTTPError, - "Error creating recordset www2.example.net." - ): - self.cloud.create_recordset('1', 'www2.example.net.', - 'a', ['192.168.1.2']) + + self.assertRaises( + exceptions.SDKException, + self.cloud.create_recordset, + fake_zone['id'], 'www2.example.net.', 'a', ['192.168.1.2'] + ) + self.assert_calls() def test_update_recordset(self): + fake_zone = test_zone.ZoneTestWrapper(self, zone) + fake_rs = RecordsetTestWrapper(self, recordset) new_ttl = 7200 - expected_recordset = { - 'name': recordset['name'], - 'records': recordset['records'], - 'type': recordset['type'] - } + expected_recordset = recordset.copy() + expected_recordset['ttl'] = new_ttl + updated_rs = RecordsetTestWrapper(self, expected_recordset) self.register_uris([ + # try by directly + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', + append=['v2', 'zones', fake_zone['name']]), + status_code=404), + # list with name dict(method='GET', uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones']), - json={ - "zones": [zone], - "links": {}, - "metadata": { - 'total_count': 1}}), + 'dns', 'public', append=['v2', 'zones'], + qs_elements=[ + 'name={name}'.format(name=fake_zone['name'])]), + json={'zones': [fake_zone.get_get_response_json()]}), + # try directly dict(method='GET', uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones']), - json={ - "zones": [zone], - "links": {}, - "metadata": { - 'total_count': 1}}), + 'dns', 'public', + append=['v2', 'zones', fake_zone['id'], + 'recordsets', fake_rs['name']]), + status_code=404), + # list with name dict(method='GET', uri=self.get_mock_url( 'dns', 'public', - append=['v2', 'zones', zone['id'], - 'recordsets', new_recordset['id']]), - json=new_recordset), + append=['v2', 'zones', fake_zone['id'], + 'recordsets'], + qs_elements=['name={name}'.format(name=fake_rs['name'])]), + json={'recordsets': [fake_rs.get_get_response_json()]}), + # update dict(method='PUT', uri=self.get_mock_url( 'dns', 'public', - append=['v2', 'zones', zone['id'], - 'recordsets', new_recordset['id']]), - json=expected_recordset, + append=['v2', 'zones', fake_zone['id'], + 'recordsets', fake_rs['id']]), + json=updated_rs.get_get_response_json(), validate=dict(json={'ttl': new_ttl})) ]) - updated_rs = self.cloud.update_recordset('1', '1', ttl=new_ttl) - self.assertEqual(expected_recordset, updated_rs) + res = self.cloud.update_recordset( + fake_zone['name'], fake_rs['name'], ttl=new_ttl) + + updated_rs.cmp(res) + self.assert_calls() + + def test_list_recordsets(self): + fake_zone = test_zone.ZoneTestWrapper(self, zone) + fake_rs = RecordsetTestWrapper(self, recordset) + self.register_uris([ + # try by directly + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', + append=['v2', 'zones', fake_zone['id']]), + json=fake_zone.get_get_response_json()), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', + append=['v2', 'zones', fake_zone['id'], 'recordsets']), + json={'recordsets': [fake_rs.get_get_response_json()], + 'links': { + 'next': self.get_mock_url( + 'dns', 'public', + append=['v2', 'zones', fake_zone['id'], + 'recordsets?limit=1&marker=asd']), + 'self': self.get_mock_url( + 'dns', 'public', + append=['v2', 'zones', fake_zone['id'], + 'recordsets?limit=1'])}, + 'metadata':{'total_count': 2}}), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', + append=['v2', 'zones', fake_zone['id'], 'recordsets'], + qs_elements=[ + 'limit=1', 'marker=asd']), + json={'recordsets': [fake_rs.get_get_response_json()]}), + ]) + res = self.cloud.list_recordsets(fake_zone['id']) + + self.assertEqual(2, len(res)) self.assert_calls() def test_delete_recordset(self): + fake_zone = test_zone.ZoneTestWrapper(self, zone) + fake_rs = RecordsetTestWrapper(self, recordset) self.register_uris([ + # try by directly dict(method='GET', uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones']), - json={ - "zones": [zone], - "links": {}, - "metadata": { - 'total_count': 1}}), + 'dns', 'public', + append=['v2', 'zones', fake_zone['name']]), + status_code=404), + # list with name dict(method='GET', uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones']), - json={ - "zones": [zone], - "links": {}, - "metadata": { - 'total_count': 1}}), + 'dns', 'public', append=['v2', 'zones'], + qs_elements=[ + 'name={name}'.format(name=fake_zone['name'])]), + json={'zones': [fake_zone.get_get_response_json()]}), + # try directly dict(method='GET', uri=self.get_mock_url( 'dns', 'public', - append=['v2', 'zones', zone['id'], - 'recordsets', new_recordset['id']]), - json=new_recordset), + append=['v2', 'zones', fake_zone['id'], + 'recordsets', fake_rs['name']]), + status_code=404), + # list with name + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', + append=['v2', 'zones', fake_zone['id'], + 'recordsets'], + qs_elements=[ + 'name={name}'.format(name=fake_rs['name'])]), + json={'recordsets': [fake_rs.get_get_response_json()]}), dict(method='DELETE', uri=self.get_mock_url( 'dns', 'public', append=['v2', 'zones', zone['id'], - 'recordsets', new_recordset['id']]), - json={}) + 'recordsets', fake_rs['id']]), + status_code=202) ]) - self.assertTrue(self.cloud.delete_recordset('1', '1')) + self.assertTrue( + self.cloud.delete_recordset(fake_zone['name'], fake_rs['name'])) self.assert_calls() def test_get_recordset_by_id(self): + fake_zone = test_zone.ZoneTestWrapper(self, zone) + fake_rs = RecordsetTestWrapper(self, recordset) self.register_uris([ + # try by directly dict(method='GET', uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones']), - json={ - "zones": [zone], - "links": {}, - "metadata": { - 'total_count': 1}}), + 'dns', 'public', + append=['v2', 'zones', fake_zone['name']]), + status_code=404), + # list with name + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['v2', 'zones'], + qs_elements=[ + 'name={name}'.format(name=fake_zone['name'])]), + json={'zones': [fake_zone.get_get_response_json()]}), + # try directly dict(method='GET', uri=self.get_mock_url( 'dns', 'public', - append=['v2', 'zones', '1', 'recordsets', '1']), - json=new_recordset), + append=['v2', 'zones', fake_zone['id'], + 'recordsets', fake_rs['id']]), + json=fake_rs.get_get_response_json()) ]) - recordset = self.cloud.get_recordset('1', '1') - self.assertEqual(recordset['id'], '1') + res = self.cloud.get_recordset(fake_zone['name'], fake_rs['id']) + fake_rs.cmp(res) self.assert_calls() def test_get_recordset_by_name(self): + fake_zone = test_zone.ZoneTestWrapper(self, zone) + fake_rs = RecordsetTestWrapper(self, recordset) self.register_uris([ + # try by directly + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', + append=['v2', 'zones', fake_zone['name']]), + status_code=404), + # list with name + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['v2', 'zones'], + qs_elements=[ + 'name={name}'.format(name=fake_zone['name'])]), + json={'zones': [fake_zone.get_get_response_json()]}), + # try directly dict(method='GET', uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones']), - json={ - "zones": [zone], - "links": {}, - "metadata": { - 'total_count': 1}}), + 'dns', 'public', + append=['v2', 'zones', fake_zone['id'], + 'recordsets', fake_rs['name']]), + status_code=404), + # list with name dict(method='GET', uri=self.get_mock_url( 'dns', 'public', - append=['v2', 'zones', '1', 'recordsets', - new_recordset['name']]), - json=new_recordset) + append=['v2', 'zones', fake_zone['id'], + 'recordsets'], + qs_elements=['name={name}'.format(name=fake_rs['name'])]), + json={'recordsets': [fake_rs.get_get_response_json()]}) ]) - recordset = self.cloud.get_recordset('1', new_recordset['name']) - self.assertEqual(new_recordset['name'], recordset['name']) + res = self.cloud.get_recordset(fake_zone['name'], fake_rs['name']) + fake_rs.cmp(res) self.assert_calls() def test_get_recordset_not_found_returns_false(self): - recordset_name = "www.nonexistingrecord.net." + fake_zone = test_zone.ZoneTestWrapper(self, zone) self.register_uris([ + # try by directly + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['v2', 'zones', fake_zone['id']]), + json=fake_zone.get_get_response_json()), + # try directly dict(method='GET', uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones']), - json={ - "zones": [zone], - "links": {}, - "metadata": { - 'total_count': 1}}), + 'dns', 'public', + append=['v2', 'zones', fake_zone['id'], + 'recordsets', 'fake']), + status_code=404), + # list with name dict(method='GET', uri=self.get_mock_url( 'dns', 'public', - append=['v2', 'zones', '1', 'recordsets', - recordset_name]), - json=[]) + append=['v2', 'zones', fake_zone['id'], + 'recordsets'], + qs_elements=['name=fake']), + json={'recordsets': []}) ]) - recordset = self.cloud.get_recordset('1', recordset_name) - self.assertFalse(recordset) + res = self.cloud.get_recordset(fake_zone['id'], 'fake') + self.assertFalse(res) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_zone.py b/openstack/tests/unit/cloud/test_zone.py index 193cc66d9..3bea3fb71 100644 --- a/openstack/tests/unit/cloud/test_zone.py +++ b/openstack/tests/unit/cloud/test_zone.py @@ -9,13 +9,12 @@ # 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 testtools -import openstack.cloud from openstack.tests.unit import base +from openstack import exceptions + zone_dict = { 'name': 'example.net.', @@ -23,10 +22,35 @@ 'email': 'test@example.net', 'description': 'Example zone', 'ttl': 3600, + 'id': '1' } -new_zone_dict = copy.copy(zone_dict) -new_zone_dict['id'] = '1' + +class ZoneTestWrapper(object): + + def __init__(self, ut, attrs): + self.remote_res = attrs + self.ut = ut + + def get_create_response_json(self): + return self.remote_res + + def get_get_response_json(self): + return self.remote_res + + def __getitem__(self, key): + """Dict access to be able to access properties easily + """ + return self.remote_res[key] + + def cmp(self, other): + ut = self.ut + me = self.remote_res + + for k, v in me.items(): + # Go over known attributes. We might of course compare others, + # but not necessary here + ut.assertEqual(v, other[k]) class TestZone(base.TestCase): @@ -36,13 +60,19 @@ def setUp(self): self.use_designate() def test_create_zone(self): + fake_zone = ZoneTestWrapper(self, zone_dict) self.register_uris([ dict(method='POST', uri=self.get_mock_url( 'dns', 'public', append=['v2', 'zones']), - json=new_zone_dict, - validate=dict( - json=zone_dict)) + json=fake_zone.get_create_response_json(), + validate=dict(json={ + 'description': zone_dict['description'], + 'email': zone_dict['email'], + 'name': zone_dict['name'], + 'ttl': zone_dict['ttl'], + 'type': 'PRIMARY' + })) ]) z = self.cloud.create_zone( name=zone_dict['name'], @@ -51,7 +81,7 @@ def test_create_zone(self): description=zone_dict['description'], ttl=zone_dict['ttl'], masters=None) - self.assertEqual(new_zone_dict, z) + fake_zone.cmp(z) self.assert_calls() def test_create_zone_exception(self): @@ -61,96 +91,127 @@ def test_create_zone_exception(self): 'dns', 'public', append=['v2', 'zones']), status_code=500) ]) - with testtools.ExpectedException( - openstack.cloud.exc.OpenStackCloudHTTPError, - "Unable to create zone example.net." - ): - self.cloud.create_zone('example.net.') + + self.assertRaises( + exceptions.SDKException, + self.cloud.create_zone, + 'example.net.' + ) self.assert_calls() def test_update_zone(self): + fake_zone = ZoneTestWrapper(self, zone_dict) new_ttl = 7200 - updated_zone = copy.copy(new_zone_dict) - updated_zone['ttl'] = new_ttl + updated_zone_dict = copy.copy(zone_dict) + updated_zone_dict['ttl'] = new_ttl + updated_zone = ZoneTestWrapper(self, updated_zone_dict) self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones']), - json={ - "zones": [new_zone_dict], - "links": {}, - "metadata": { - 'total_count': 1}}), + 'dns', 'public', append=['v2', 'zones', fake_zone['id']]), + json=fake_zone.get_get_response_json()), dict(method='PATCH', uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones', '1']), - json=updated_zone, - validate=dict( - json={"ttl": new_ttl})) + 'dns', 'public', append=['v2', 'zones', fake_zone['id']]), + json=updated_zone.get_get_response_json(), + validate=dict(json={"ttl": new_ttl})) ]) - z = self.cloud.update_zone('1', ttl=new_ttl) - self.assertEqual(updated_zone, z) + z = self.cloud.update_zone(fake_zone['id'], ttl=new_ttl) + updated_zone.cmp(z) self.assert_calls() def test_delete_zone(self): + fake_zone = ZoneTestWrapper(self, zone_dict) self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones']), - json={ - "zones": [new_zone_dict], - "links": {}, - "metadata": { - 'total_count': 1}}), + 'dns', 'public', append=['v2', 'zones', fake_zone['id']]), + json=fake_zone.get_get_response_json()), dict(method='DELETE', uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones', '1']), - json=new_zone_dict) + 'dns', 'public', append=['v2', 'zones', fake_zone['id']]), + status_code=202) ]) - self.assertTrue(self.cloud.delete_zone('1')) + self.assertTrue(self.cloud.delete_zone(fake_zone['id'])) self.assert_calls() def test_get_zone_by_id(self): + fake_zone = ZoneTestWrapper(self, zone_dict) self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones']), - json={ - "zones": [new_zone_dict], - "links": {}, - "metadata": { - 'total_count': 1}}) + 'dns', 'public', append=['v2', 'zones', fake_zone['id']]), + json=fake_zone.get_get_response_json()) ]) - zone = self.cloud.get_zone('1') - self.assertEqual(zone['id'], '1') + res = self.cloud.get_zone(fake_zone['id']) + + fake_zone.cmp(res) self.assert_calls() def test_get_zone_by_name(self): + fake_zone = ZoneTestWrapper(self, zone_dict) self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones']), - json={ - "zones": [new_zone_dict], - "links": {}, - "metadata": { - 'total_count': 1}}) + 'dns', 'public', + append=['v2', 'zones', fake_zone['name']]), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['v2', 'zones'], + qs_elements=[ + 'name={name}'.format(name=fake_zone['name'])]), + json={"zones": [fake_zone.get_get_response_json()]}) ]) - zone = self.cloud.get_zone('example.net.') - self.assertEqual(zone['name'], 'example.net.') + res = self.cloud.get_zone(fake_zone['name']) + fake_zone.cmp(res) self.assert_calls() def test_get_zone_not_found_returns_false(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones']), - json={ - "zones": [], - "links": {}, - "metadata": { - 'total_count': 1}}) + 'dns', 'public', + append=['v2', 'zones', 'nonexistingzone.net.']), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['v2', 'zones'], + qs_elements=['name=nonexistingzone.net.']), + json={"zones": []}) ]) zone = self.cloud.get_zone('nonexistingzone.net.') self.assertFalse(zone) self.assert_calls() + + def test_list_zones(self): + fake_zone = ZoneTestWrapper(self, zone_dict) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', + append=['v2', 'zones']), + json={'zones': [fake_zone.get_get_response_json()], + 'links': { + 'next': self.get_mock_url( + 'dns', 'public', + append=['v2', 'zones', + '?limit=1&marker=asd']), + 'self': self.get_mock_url( + 'dns', 'public', + append=['v2', 'zones', + '?limit=1'])}, + 'metadata':{'total_count': 2}}), + dict(method='GET', + uri=self.get_mock_url( + 'dns', 'public', + append=['v2', 'zones/'], + qs_elements=[ + 'limit=1', 'marker=asd']), + json={'zones': [fake_zone.get_get_response_json()]}), + ]) + res = self.cloud.list_zones() + + # updated_rs.cmp(res) + self.assertEqual(2, len(res)) + self.assert_calls() diff --git a/openstack/tests/unit/dns/v2/test_proxy.py b/openstack/tests/unit/dns/v2/test_proxy.py index 9814ea59d..5f6683cee 100644 --- a/openstack/tests/unit/dns/v2/test_proxy.py +++ b/openstack/tests/unit/dns/v2/test_proxy.py @@ -50,14 +50,16 @@ def test_zone_update(self): self.verify_update(self.proxy.update_zone, zone.Zone) def test_zone_abandon(self): - self._verify("openstack.dns.v2.zone.Zone.abandon", - self.proxy.abandon_zone, - method_args=[{'zone': 'id'}]) + self._verify2("openstack.dns.v2.zone.Zone.abandon", + self.proxy.abandon_zone, + method_args=[{'zone': 'id'}], + expected_args=[self.proxy]) def test_zone_xfr(self): - self._verify("openstack.dns.v2.zone.Zone.xfr", - self.proxy.xfr_zone, - method_args=[{'zone': 'id'}]) + self._verify2("openstack.dns.v2.zone.Zone.xfr", + self.proxy.xfr_zone, + method_args=[{'zone': 'id'}], + expected_args=[self.proxy]) class TestDnsRecordset(TestDnsProxy): @@ -89,6 +91,15 @@ def test_recordsets_zone(self): method_kwargs={'zone': 'zid'}, expected_kwargs={'zone_id': 'zid'}) + def test_recordset_find(self): + self._verify2("openstack.proxy.Proxy._find", + self.proxy.find_recordset, + method_args=['zone', 'rs'], + method_kwargs={}, + expected_args=[recordset.Recordset, 'rs'], + expected_kwargs={'ignore_missing': True, + 'zone_id': 'zone'}) + class TestDnsFloatIP(TestDnsProxy): def test_floating_ips(self): diff --git a/openstack/tests/unit/dns/v2/test_recordset.py b/openstack/tests/unit/dns/v2/test_recordset.py index 700a5148a..e53aa4938 100644 --- a/openstack/tests/unit/dns/v2/test_recordset.py +++ b/openstack/tests/unit/dns/v2/test_recordset.py @@ -40,7 +40,7 @@ class TestRecordset(base.TestCase): def test_basic(self): sot = recordset.Recordset() - self.assertEqual('recordset', sot.resource_key) + self.assertIsNone(sot.resource_key) self.assertEqual('recordsets', sot.resources_key) self.assertEqual('/zones/%(zone_id)s/recordsets', sot.base_path) self.assertTrue(sot.allow_list) From e8afa6878f7e9784d137cc817e46d621b34de154 Mon Sep 17 00:00:00 2001 From: Bogdan Dobrelya Date: Mon, 24 Jun 2019 16:27:19 +0200 Subject: [PATCH 2483/3836] Allow deeper levels of nesting for pdf builds Change the defaults for the max of a 5 of \begin...\end stanzas, up to a 10. Change-Id: I52ef17bd87cbcde7f9dc34e8b1bb874581d0917d Signed-off-by: Bogdan Dobrelya --- doc/source/conf.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/source/conf.py b/doc/source/conf.py index 905223cfb..99bea4c56 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -112,5 +112,8 @@ u'OpenStack Foundation', 'manual'), ] +# Allow deeper levels of nesting for \begin...\end stanzas +latex_elements = {'maxlistdepth': 10} + # Include both the class and __init__ docstrings when describing the class autoclass_content = "both" From acda2ce62d7aad3fefb1a269ee4f36d2521403a6 Mon Sep 17 00:00:00 2001 From: pengyuesheng Date: Wed, 26 Jun 2019 10:14:48 +0800 Subject: [PATCH 2484/3836] Add Python 3 Train unit tests This is a mechanically generated patch to ensure unit testing is in place for all of the Tested Runtimes for Train. See the Train python3-updates goal document for details: https://governance.openstack.org/tc/goals/train/python3-updates.html Change-Id: I3f92f9a98c86c1366cd4bc43c48615605ad2f1b4 --- .zuul.yaml | 3 +-- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 11a883440..dc5c8d3ac 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -396,8 +396,7 @@ - check-requirements - openstack-lower-constraints-jobs - openstack-python-jobs - - openstack-python36-jobs - - openstack-python37-jobs + - openstack-python3-train-jobs - openstacksdk-functional-tips - openstacksdk-tox-tips - os-client-config-tox-tips diff --git a/setup.cfg b/setup.cfg index 45a1c03e6..532962a52 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,8 +16,8 @@ classifier = Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 [files] packages = From 3f6043796defb5769aa77ec063fa8f7a06197a3f Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Thu, 27 Jun 2019 10:18:05 -0700 Subject: [PATCH 2485/3836] Replace nodepool func jobs These are new nodepool functional jobs designed to give more flexibility to the DIB/clean projects to define what they need to test. Otherwise, they behave very similarly to the current nodepool functional jobs. Depends-On: https://review.opendev.org/665023 Change-Id: I3b88debef5e92b2d2469952a5d12be8bf569102e --- .zuul.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 11a883440..6d9f3d3e9 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -422,7 +422,7 @@ - openstacksdk-functional-devstack-python2 - osc-functional-devstack-tips: voting: false - - nodepool-functional-py35-src + - nodepool-functional-openstack-src - bifrost-integration-tinyipa-ubuntu-xenial - metalsmith-integration-openstacksdk-src: voting: false @@ -432,5 +432,5 @@ - openstacksdk-functional-devstack-python2 - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-senlin - - nodepool-functional-py35-src + - nodepool-functional-openstack-src - bifrost-integration-tinyipa-ubuntu-xenial From 0a5bc189778b144b63286806c30e215c38dcc5fd Mon Sep 17 00:00:00 2001 From: zhufl Date: Wed, 3 Jul 2019 14:43:35 +0800 Subject: [PATCH 2486/3836] Fix invalid assert state In self.assertTrue("the_alt", Test._alternate_id()), assertEqual should be used. Change-Id: I29278908645e59b77607b71387a54ff9d0fa878e --- openstack/tests/unit/test_resource.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index be9ec056e..d51a2b560 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -634,7 +634,7 @@ def test__alternate_id(self): class Test(resource.Resource): alt = resource.Body("the_alt", alternate_id=True) - self.assertTrue("the_alt", Test._alternate_id()) + self.assertEqual("the_alt", Test._alternate_id()) value1 = "lol" sot = Test(alt=value1) From 9ad9da5411deff98956a90774cc1b4eaaa020c26 Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Fri, 5 Jul 2019 15:42:49 +0000 Subject: [PATCH 2487/3836] Increase randomness in ZONE creation Increase the randomness in the name generation, to avoid problems like in [1]. The "tearDown" definition is removed from "TestZone" class. This method is not needed, as long as "addCleanup" will always ensure the DNS zone is deleted. [1]http://logs.openstack.org/04/668304/7/check/openstacksdk-functional-devstack-networking/1416912/testr_results.html.gz Change-Id: Ibc130d57596b85310a7d53d40610a470777f9b84 --- openstack/tests/functional/dns/v2/test_zone.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/openstack/tests/functional/dns/v2/test_zone.py b/openstack/tests/functional/dns/v2/test_zone.py index 2d4527cc8..e164f0211 100644 --- a/openstack/tests/functional/dns/v2/test_zone.py +++ b/openstack/tests/functional/dns/v2/test_zone.py @@ -27,7 +27,7 @@ def setUp(self): # chose a new zone name for a test # getUniqueString is not guaranteed to return unique string between # different tests of the same class. - self.ZONE_NAME = 'example-{0}.org.'.format(random.randint(1, 100)) + self.ZONE_NAME = 'example-{0}.org.'.format(random.randint(1, 10000)) self.zone = self.conn.dns.create_zone( name=self.ZONE_NAME, @@ -38,11 +38,6 @@ def setUp(self): ) self.addCleanup(self.conn.dns.delete_zone, self.zone) - def tearDown(self): - if self.zone: - self.conn.dns.delete_zone(self.zone) - super(TestZone, self).tearDown() - def test_get_zone(self): zone = self.conn.dns.get_zone(self.zone) self.assertEqual(self.zone, zone) From 832e43689179bd355679f64fbbfe3b4c9ab5d2bf Mon Sep 17 00:00:00 2001 From: Corey Bryant Date: Fri, 5 Jul 2019 11:54:26 -0400 Subject: [PATCH 2488/3836] Add Python 3 Train unit tests This is a mechanically generated patch to ensure unit testing is in place for all of the Tested Runtimes for Train. See the Train python3-updates goal document for details: https://governance.openstack.org/tc/goals/train/python3-updates.html Change-Id: Ib55d9c890de62bdfe269390c5533144e46e92703 Story: #2005924 Task: #34233 --- .zuul.yaml | 3 +-- setup.cfg | 2 +- tox.ini | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 6d9f3d3e9..fb0cb070d 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -396,8 +396,7 @@ - check-requirements - openstack-lower-constraints-jobs - openstack-python-jobs - - openstack-python36-jobs - - openstack-python37-jobs + - openstack-python3-train-jobs - openstacksdk-functional-tips - openstacksdk-tox-tips - os-client-config-tox-tips diff --git a/setup.cfg b/setup.cfg index 45a1c03e6..532962a52 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,8 +16,8 @@ classifier = Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 [files] packages = diff --git a/tox.ini b/tox.ini index 11246b824..aab8ea678 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 3.1 -envlist = pep8,py37,py36,py35,py27 +envlist = pep8,py27,py37 skipsdist = True ignore_basepython_conflict = True From 2d406e7168c31345fe1d9722ffd8e031906920e4 Mon Sep 17 00:00:00 2001 From: Romil Gupta Date: Mon, 8 Jul 2019 13:14:23 +0000 Subject: [PATCH 2489/3836] Fix typo for subnet.py This 'slacc' should be 'slaac' We need to update the openstacksdk Api documentation as well: openstack.network.v2.subnet Change-Id: I1bccf5221a634493ac89477311daf89acd6ef99c --- openstack/network/v2/subnet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/network/v2/subnet.py b/openstack/network/v2/subnet.py index c7f2b8bce..a3c2d8842 100644 --- a/openstack/network/v2/subnet.py +++ b/openstack/network/v2/subnet.py @@ -57,7 +57,7 @@ class Subnet(resource.Resource, resource.TagMixin): #: *Type: int* ip_version = resource.Body('ip_version', type=int) #: The IPv6 address modes which are 'dhcpv6-stateful', 'dhcpv6-stateless' - #: or 'slacc'. + #: or 'slaac'. ipv6_address_mode = resource.Body('ipv6_address_mode') #: The IPv6 router advertisements modes which can be 'slaac', #: 'dhcpv6-stateful', 'dhcpv6-stateless'. From 983cc997ffc0a4cfaab65fc3dc0e6c1f9b26a321 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 9 Jul 2019 09:19:02 -0400 Subject: [PATCH 2490/3836] Return empty lists for resources if neutron doesn't exist It's the general design of the cloud layer to return empty resources rather than throw exceptions when a user tries to fetch a non-existent resource. Change-Id: I1c6ce28af767f466f349b51a47abe27c0dcdd3fb --- openstack/cloud/_network.py | 12 ++++++++++ openstack/tests/unit/cloud/test_shade.py | 24 ++++--------------- ...resources-empty-list-6aa760c01e7d97d7.yaml | 5 ++++ 3 files changed, 21 insertions(+), 20 deletions(-) create mode 100644 releasenotes/notes/list-network-resources-empty-list-6aa760c01e7d97d7.yaml diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index 3fd0e407e..9a58f34ad 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -123,6 +123,9 @@ def list_networks(self, filters=None): :returns: A list of ``munch.Munch`` containing network info. """ + # If the cloud is running nova-network, just return an empty list. + if not self.has_service('network'): + return [] # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} @@ -136,6 +139,9 @@ def list_routers(self, filters=None): :returns: A list of router ``munch.Munch``. """ + # If the cloud is running nova-network, just return an empty list. + if not self.has_service('network'): + return [] # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} @@ -152,6 +158,9 @@ def list_subnets(self, filters=None): :returns: A list of subnet ``munch.Munch``. """ + # If the cloud is running nova-network, just return an empty list. + if not self.has_service('network'): + return [] # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} @@ -192,6 +201,9 @@ def list_ports(self, filters=None): return _utils._filter_list(self._ports, None, filters or {}) def _list_ports(self, filters): + # If the cloud is running nova-network, just return an empty list. + if not self.has_service('network'): + return [] resp = self.network.get("/ports.json", params=filters) data = proxy._json_response( resp, diff --git a/openstack/tests/unit/cloud/test_shade.py b/openstack/tests/unit/cloud/test_shade.py index 3be490abd..d96b612b1 100644 --- a/openstack/tests/unit/cloud/test_shade.py +++ b/openstack/tests/unit/cloud/test_shade.py @@ -122,26 +122,10 @@ def test_list_servers_exception(self): self.assert_calls() - def test__neutron_exceptions_resource_not_found(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), - status_code=404) - ]) - self.assertRaises(exc.OpenStackCloudResourceNotFound, - self.cloud.list_networks) - self.assert_calls() - - def test__neutron_exceptions_url_not_found(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), - status_code=404) - ]) - self.assertRaises(exc.OpenStackCloudURINotFound, - self.cloud.list_networks) + def test_neutron_not_found(self): + self.use_nothing() + self.cloud.has_service = mock.Mock(return_value=False) + self.assertEqual([], self.cloud.list_networks()) self.assert_calls() def test_list_servers(self): diff --git a/releasenotes/notes/list-network-resources-empty-list-6aa760c01e7d97d7.yaml b/releasenotes/notes/list-network-resources-empty-list-6aa760c01e7d97d7.yaml new file mode 100644 index 000000000..2c7be2c72 --- /dev/null +++ b/releasenotes/notes/list-network-resources-empty-list-6aa760c01e7d97d7.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Basic networking list calls in the cloud layer been fixed to return + an empty list if neutron is not running. From b2df6b19cdc1bdaf558e338b0d4c8245adacfd10 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 10 Jul 2019 19:57:34 +0200 Subject: [PATCH 2491/3836] Repair masakari FT With switch to resource layer a wrong hypervisor attribute 'hypervisor_hostname' disappeared. Use a proper 'name' attribute instead. Change-Id: I1ce5f761fe49042e058d18371a7a2d0b0dcc75de --- openstack/tests/functional/instance_ha/test_host.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/tests/functional/instance_ha/test_host.py b/openstack/tests/functional/instance_ha/test_host.py index 1b03cb28a..f1f1a5372 100644 --- a/openstack/tests/functional/instance_ha/test_host.py +++ b/openstack/tests/functional/instance_ha/test_host.py @@ -46,7 +46,7 @@ def setUp(self): service_type='COMPUTE') # Create valid host - self.NAME = HYPERVISORS[0]['hypervisor_hostname'] + self.NAME = HYPERVISORS[0].name self.host = self.conn.ha.create_host( segment_id=self.segment.uuid, name=self.NAME, type='COMPUTE', control_attributes='SSH') From 2081befb9212a44442f0a3d4e7ec5d72aa152bb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Unbekandt?= Date: Thu, 11 Jul 2019 14:20:08 +0200 Subject: [PATCH 2492/3836] Can't create a metadata named key, clear, delete We now pass metadata as a dictionnary and not as keyword arguments since _metadata method already have 3 keywords arguments (key, clear and delete) which couldn't be used as metadata keys. Change-Id: Iee3773e4e9a4187bd1557f400aa966f80e3acb3c Story: 2005085 Task: 35787 --- openstack/compute/v2/metadata.py | 5 +++-- .../notes/metadata-key-name-bugfix-77612a825c5145d7.yaml | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/metadata-key-name-bugfix-77612a825c5145d7.yaml diff --git a/openstack/compute/v2/metadata.py b/openstack/compute/v2/metadata.py index e585871f7..b1f554d1e 100644 --- a/openstack/compute/v2/metadata.py +++ b/openstack/compute/v2/metadata.py @@ -19,7 +19,8 @@ class MetadataMixin(object): def _metadata(self, method, key=None, clear=False, delete=False, - **metadata): + metadata=None): + metadata = metadata or {} for k, v in metadata.items(): if not isinstance(v, six.string_types): raise ValueError("The value for %s (%s) must be " @@ -84,7 +85,7 @@ def set_metadata(self, session, **metadata): if not metadata: return dict() - result = self._metadata(session.post, **metadata) + result = self._metadata(session.post, metadata=metadata) return result["metadata"] def delete_metadata(self, session, keys): diff --git a/releasenotes/notes/metadata-key-name-bugfix-77612a825c5145d7.yaml b/releasenotes/notes/metadata-key-name-bugfix-77612a825c5145d7.yaml new file mode 100644 index 000000000..892fb85f2 --- /dev/null +++ b/releasenotes/notes/metadata-key-name-bugfix-77612a825c5145d7.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - Fixed a bug related to metadata's key name. An exception was + raised when setting it to "delete"," clear" or "key" From 15ae4e2c01aa62f4c6f5e8cd9ac0dd38de2b91d4 Mon Sep 17 00:00:00 2001 From: ZhaoBo Date: Tue, 25 Jun 2019 11:24:31 +0800 Subject: [PATCH 2493/3836] force specify project_id during create Sg_rule may cause issue I found we just specify tenant_id for creating SG, but force specify project_id for creating SG rules. This may cause compatible issue. So this patch keep SG rules same with SG creationi and test in upstream CI. This issue raises by https://review.opendev.org/#/c/662724/ , it seems a small nit that force to specify project_id to create SG_rule. But other resources, such as Network/SG just pass tenant_id. So it may cause some compatible issue, or keep the same like other resources at least. Change-Id: I2ce41dd0113c621c6cf6ab345aa649088a54e066 --- openstack/cloud/_security_group.py | 1 - openstack/tests/unit/cloud/test_security_groups.py | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/openstack/cloud/_security_group.py b/openstack/cloud/_security_group.py index 16fa73ad9..7142e69b1 100644 --- a/openstack/cloud/_security_group.py +++ b/openstack/cloud/_security_group.py @@ -309,7 +309,6 @@ def create_security_group_rule(self, 'ethertype': ethertype } if project_id is not None: - rule_def['project_id'] = project_id rule_def['tenant_id'] = project_id return self.network.create_security_group_rule( diff --git a/openstack/tests/unit/cloud/test_security_groups.py b/openstack/tests/unit/cloud/test_security_groups.py index a6d2838a9..3b62f79fa 100644 --- a/openstack/tests/unit/cloud/test_security_groups.py +++ b/openstack/tests/unit/cloud/test_security_groups.py @@ -372,9 +372,11 @@ def test_create_security_group_rule_neutron_specific_tenant(self): expected_args['port_range_min'] = None expected_args['security_group_id'] = neutron_grp_dict['id'] expected_args['tenant_id'] = expected_args['project_id'] + expected_args.pop('project_id') expected_new_rule = copy.copy(expected_args) expected_new_rule['id'] = '1234' + expected_new_rule['project_id'] = expected_new_rule['tenant_id'] self.register_uris([ dict(method='GET', From 24dd341e91707e64a7ae40001c7ded1c7de43e59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Unbekandt?= Date: Thu, 11 Jul 2019 15:55:05 +0200 Subject: [PATCH 2494/3836] Specify store when importing an image Add a new parameter (store) to import_image method. When the store is specified, the header 'X-Image-Meta-Store' is added to the request. Change-Id: I90f08ce108945606fa5bad12fd143268438630f9 Story: 2006203 Task: 35788 --- openstack/image/v2/_proxy.py | 11 +++++++++-- openstack/image/v2/image.py | 8 ++++++-- openstack/tests/unit/image/v2/test_image.py | 14 ++++++++++++++ openstack/tests/unit/image/v2/test_proxy.py | 2 +- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index c033d0dc2..60c2bfd30 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -35,7 +35,8 @@ def _create_image(self, **kwargs): """ return self._create(_image.Image, **kwargs) - def import_image(self, image, method='glance-direct', uri=None): + def import_image(self, image, method='glance-direct', uri=None, + store=None): """Import data to an existing image Interoperable image import process are introduced in the Image API @@ -50,10 +51,16 @@ def import_image(self, image, method='glance-direct', uri=None): :param uri: Required only if using the web-download import method. This url is where the data is made available to the Image service. + :param store: Used when enabled_backends is activated in glance + The value can be the id of a store or a + :class:`~openstack.image.v2.service_info.Store` + instance. :returns: None """ image = self._get_resource(_image.Image, image) + if store is not None: + store = self._get_resource(_si.Store, store) # as for the standard image upload function, container_format and # disk_format are required for using image import process @@ -62,7 +69,7 @@ def import_image(self, image, method='glance-direct', uri=None): "Both container_format and disk_format are required for" " importing an image") - image.import_image(self, method=method, uri=uri) + image.import_image(self, method=method, uri=uri, store=store) def stage_image(self, image, filename=None, data=None): """Stage binary image data diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index 1cea75743..13e9ff33c 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -268,7 +268,8 @@ def stage(self, session): self._translate_response(response, has_body=False) return self - def import_image(self, session, method='glance-direct', uri=None): + def import_image(self, session, method='glance-direct', uri=None, + store=None): """Import Image via interoperable image import process""" url = utils.urljoin(self.base_path, self.id, 'import') json = {'method': {'name': method}} @@ -280,7 +281,10 @@ def import_image(self, session, method='glance-direct', uri=None): else: raise exceptions.InvalidRequest('URI is only supported with ' 'method: "web-download"') - session.post(url, json=json) + headers = {} + if store is not None: + headers = {'X-Image-Meta-Store': store.id} + session.post(url, json=json, headers=headers) def _prepare_request(self, requires_id=None, prepend_key=False, patch=False, base_path=None): diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 2097417b9..8981db8f8 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -262,6 +262,7 @@ def test_import_image(self): sot.import_image(self.sess, "web-download", "such-a-good-uri") self.sess.post.assert_called_with( 'images/IDENTIFIER/import', + headers={}, json=json ) @@ -271,9 +272,22 @@ def test_import_image_with_uri_not_web_download(self): sot.import_image(self.sess, "glance-direct") self.sess.post.assert_called_with( 'images/IDENTIFIER/import', + headers={}, json={"method": {"name": "glance-direct"}} ) + def test_import_image_with_stores(self): + sot = image.Image(**EXAMPLE) + json = {"method": {"name": "web-download", "uri": "such-a-good-uri"}} + store = mock.MagicMock() + store.id = "ceph_1" + sot.import_image(self.sess, "web-download", "such-a-good-uri", store) + self.sess.post.assert_called_with( + 'images/IDENTIFIER/import', + headers={'X-Image-Meta-Store': 'ceph_1'}, + json=json + ) + def test_upload(self): sot = image.Image(**EXAMPLE) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 025e7069f..4bde92101 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -54,7 +54,7 @@ def test_image_import(self): self._verify("openstack.image.v2.image.Image.import_image", self.proxy.import_image, method_args=[original_image, "method", "uri"], - expected_kwargs={"method": "method", + expected_kwargs={"method": "method", "store": None, "uri": "uri"}) def test_image_upload_no_args(self): From 6a2668724551cd60589d018bd8962840e9f9a9a4 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Tue, 16 Jul 2019 18:19:10 +0200 Subject: [PATCH 2495/3836] Correct endpoint_override discovery for service with dashes in their type Currently when handling endpoint_override, we correctly discover the actual URL, but then we populate the wrong configuration option (with a dash instead of an underscore). This results in the unversioned URL being used for requests, breaking ironic-inspector with endpoint_override. Change-Id: Id8d68a8b544cb557cfbd3e38bee93bcf88f673ed --- openstack/service_description.py | 2 +- openstack/tests/unit/config/test_from_conf.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/openstack/service_description.py b/openstack/service_description.py index 87acc909f..ff27217e7 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -169,7 +169,7 @@ def _make_proxy(self, instance): # so that subsequent discovery calls don't get made incorrectly. if data.catalog_url != data.service_url: ep_key = '{service_type}_endpoint_override'.format( - service_type=self.service_type) + service_type=self.service_type.replace('-', '_')) config.config[ep_key] = data.service_url proxy_obj = config.get_session_client( self.service_type, diff --git a/openstack/tests/unit/config/test_from_conf.py b/openstack/tests/unit/config/test_from_conf.py index 1050e37f8..40e31f200 100644 --- a/openstack/tests/unit/config/test_from_conf.py +++ b/openstack/tests/unit/config/test_from_conf.py @@ -141,16 +141,25 @@ def test_name_with_dashes(self): }] } } + status = { + 'finished': True, + 'error': None + } self.register_uris([ dict(method='GET', uri='https://example.org:5050', json=discovery), + dict(method='GET', + uri='https://example.org:5050/v1/introspection/abcd', + json=status), ]) adap = conn.baremetal_introspection self.assertEqual('baremetal-introspection', adap.service_type) self.assertEqual('public', adap.interface) - self.assertEqual('https://example.org:5050', adap.endpoint_override) + self.assertEqual('https://example.org:5050/v1', adap.endpoint_override) + + self.assertTrue(adap.get_introspection('abcd').is_finished) def _test_missing_invalid_permutations(self, expected_reason): # Do special things to self.oslo_config_dict['heat'] before calling From fa5df8d5c9745e1fba2e28d8c32ce0c3b64576fb Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Mon, 22 Jul 2019 20:47:57 +0200 Subject: [PATCH 2496/3836] Update api-ref location The api documentation is now published on docs.openstack.org instead of developer.openstack.org. Update all links that are changed to the new location. Note that redirects will be set up as well but let's point now to the new location. For details, see: http://lists.openstack.org/pipermail/openstack-discuss/2019-July/007828.html Change-Id: I6724a292def5e0c392e42529116a43d39c990cb8 --- openstack/baremetal/v1/_proxy.py | 2 +- openstack/compute/v2/_proxy.py | 2 +- openstack/tests/fakes.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 01754b3d7..c79afa9f2 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -281,7 +281,7 @@ def patch_node(self, node, patch, retry_on_conflict=True): a normal code and should not be retried. See `Update Node - `_ + `_ for details. :returns: The updated node. diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 1aabbdfb9..bf1cd119f 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -487,7 +487,7 @@ def servers(self, details=True, all_projects=False, **query): ``True``, will cause instances with full data to be returned. :param kwargs query: Optional query parameters to be sent to limit the servers being returned. Available parameters can be seen - under https://developer.openstack.org/api-ref/compute/#list-servers + under https://docs.openstack.org/api-ref/compute/#list-servers :returns: A generator of server instances. """ diff --git a/openstack/tests/fakes.py b/openstack/tests/fakes.py index 18129f14f..fc3033072 100644 --- a/openstack/tests/fakes.py +++ b/openstack/tests/fakes.py @@ -159,7 +159,7 @@ def make_fake_server( def make_fake_keypair(name): # Note: this is literally taken from: - # https://developer.openstack.org/api-ref/compute/ + # https://docs.openstack.org/api-ref/compute/ return { "fingerprint": "7e:eb:ab:24:ba:d1:e1:88:ae:9a:fb:66:53:df:d3:bd", "name": name, From 6548a961b9a4a5e621eb238226d47b6632ca9188 Mon Sep 17 00:00:00 2001 From: Riccardo Pittau Date: Tue, 23 Jul 2019 02:01:52 +0200 Subject: [PATCH 2497/3836] Add set-boot-device to baremetal This patch adds the set-boot-device function to the baremetal module. Change-Id: Ia936cd41b87dd9422b00e96ee541f2c3842dc636 --- openstack/baremetal/v1/_proxy.py | 13 ++++++++++ openstack/baremetal/v1/node.py | 25 +++++++++++++++++++ .../tests/unit/baremetal/v1/test_node.py | 19 ++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 01754b3d7..8055cbddb 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -326,6 +326,19 @@ def set_node_provision_state(self, node, target, config_drive=None, rescue_password=rescue_password, wait=wait, timeout=timeout) + def set_node_boot_device(self, node, boot_device, persistent=False): + """Set node boot device + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :param boot_device: Boot device to assign to the node. + :param persistent: If the boot device change is maintained after node + reboot + :return: The updated :class:`~openstack.baremetal.v1.node.Node` + """ + res = self._get_resource(_node.Node, node) + return res.set_boot_device(self, boot_device, persistent=persistent) + def wait_for_nodes_provision_state(self, nodes, expected_state, timeout=None, abort_on_failed_state=True): diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index a02a00797..236103c7a 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -720,5 +720,30 @@ def _do_maintenance_action(self, session, verb, body=None): .format(node=self.id)) exceptions.raise_from_response(response, error_message=msg) + def set_boot_device(self, session, boot_device, persistent=False): + """Set node boot device + + :param session: The session to use for making this request. + :param boot_device: Boot device to assign to the node. + :param persistent: If the boot device change is maintained after node + reboot + :return: The updated :class:`~openstack.baremetal.v1.node.Node` + """ + session = self._get_session(session) + version = self._get_microversion_for(session, 'commit') + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'management', 'boot_device') + + body = {'boot_device': boot_device, 'persistent': persistent} + + response = session.put( + request.url, json=body, + headers=request.headers, microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + msg = ("Failed to set boot device for node {node}" + .format(node=self.id)) + exceptions.raise_from_response(response, error_message=msg) + NodeDetail = Node diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 3a8732fcd..1d6a2426f 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -683,3 +683,22 @@ def test_set_unset_maintenance(self): json=None, headers=mock.ANY, microversion=mock.ANY) + + +@mock.patch.object(node.Node, 'fetch', lambda self, session: self) +@mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) +class TestNodeSetBootDevice(base.TestCase): + + def setUp(self): + super(TestNodeSetBootDevice, self).setUp() + self.node = node.Node(**FAKE) + self.session = mock.Mock(spec=adapter.Adapter, + default_microversion='1.1') + + def test_node_set_boot_device(self): + self.node.set_boot_device(self.session, 'pxe', persistent=False) + self.session.put.assert_called_once_with( + 'nodes/%s/management/boot_device' % self.node.id, + json={'boot_device': 'pxe', 'persistent': False}, + headers=mock.ANY, microversion=mock.ANY, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) From e8534f582a258bb2a602f889b7672aac97a9d48a Mon Sep 17 00:00:00 2001 From: Mohammed Naser Date: Sat, 29 Dec 2018 13:34:40 -0500 Subject: [PATCH 2498/3836] docs: Add simplified CLI parser docs The documentation for using the built-in CLI parser are incorrect, this updates them for very functional and simple ones. Change-Id: I0cc17b46c85aadc1fd30c5a55021bfeb1f7366f0 --- doc/source/user/config/using.rst | 10 ++-------- examples/connect.py | 5 +---- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/doc/source/user/config/using.rst b/doc/source/user/config/using.rst index 7792989cf..2359de00c 100644 --- a/doc/source/user/config/using.rst +++ b/doc/source/user/config/using.rst @@ -45,14 +45,8 @@ with - as well as a consumption argument. .. code-block:: python import argparse - import sys - import openstack.config + import openstack - config = openstack.config.OpenStackConfig() parser = argparse.ArgumentParser() - config.register_argparse_arguments(parser, sys.argv) - - options = parser.parse_args() - - cloud_region = config.get_one(argparse=options) + cloud = openstack.connect(options=parser) diff --git a/examples/connect.py b/examples/connect.py index c8ce89550..ddf781a09 100644 --- a/examples/connect.py +++ b/examples/connect.py @@ -66,10 +66,7 @@ def create_connection_from_config(): def create_connection_from_args(): parser = argparse.ArgumentParser() - config = loader.OpenStackConfig() - config.register_argparse_arguments(parser, sys.argv[1:]) - args = parser.parse_args() - return openstack.connect(config=config.get_one(argparse=args)) + return openstack.connect(options=parser) def create_connection(auth_url, region, project_name, username, password): From 2fe2297ee83ca800250ff16f964c364408a1d5ec Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 24 Jul 2019 20:12:15 +0200 Subject: [PATCH 2499/3836] Fix image deletion with tasks_api enabled During image refactor some constants wandered into proxy. Due to the tests absence delete_image with tasks_api=True was not properly fixed. Get constants from proper place and add corresponding test. Change-Id: I64f29ade2da4740434f0812e8e514f0752510c71 --- openstack/cloud/_image.py | 8 ++-- openstack/tests/unit/cloud/test_image.py | 48 ++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py index de8033bb7..90872bc70 100644 --- a/openstack/cloud/_image.py +++ b/openstack/cloud/_image.py @@ -213,11 +213,11 @@ def delete_image( # Task API means an image was uploaded to swift # TODO(gtema) does it make sense to move this into proxy? if self.image_api_use_tasks and ( - self._IMAGE_OBJECT_KEY in image - or self._SHADE_IMAGE_OBJECT_KEY in image): + self.image._IMAGE_OBJECT_KEY in image + or self.image._SHADE_IMAGE_OBJECT_KEY in image): (container, objname) = image.get( - self._IMAGE_OBJECT_KEY, image.get( - self._SHADE_IMAGE_OBJECT_KEY)).split('/', 1) + self.image._IMAGE_OBJECT_KEY, image.get( + self.image._SHADE_IMAGE_OBJECT_KEY)).split('/', 1) self.delete_object(container=container, name=objname) if wait: diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 5789305b2..27f4662bb 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -536,6 +536,54 @@ def test_delete_autocreated_no_tasks(self): self.assertFalse(deleted) self.assert_calls() + def test_delete_image_task(self): + self.cloud.image_api_use_tasks = True + endpoint = self.cloud._object_store_client.get_endpoint() + + object_path = self.fake_image_dict['owner_specified.openstack.object'] + + image_no_checksums = self.fake_image_dict.copy() + del(image_no_checksums['owner_specified.openstack.md5']) + del(image_no_checksums['owner_specified.openstack.sha256']) + del(image_no_checksums['owner_specified.openstack.object']) + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), + json=self.fake_search_return), + dict(method='DELETE', + uri='https://image.example.com/v2/images/{id}'.format( + id=self.image_id)), + dict(method='HEAD', + uri='{endpoint}/{object}'.format( + endpoint=endpoint, + object=object_path), + headers={ + 'X-Timestamp': '1429036140.50253', + 'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1', + 'Content-Length': '1290170880', + 'Last-Modified': 'Tue, 14 Apr 2015 18:29:01 GMT', + 'X-Object-Meta-X-Sdk-Sha256': + self.fake_image_dict[ + 'owner_specified.openstack.sha256'], + 'X-Object-Meta-X-Sdk-Md5': + self.fake_image_dict[ + 'owner_specified.openstack.md5'], + 'Date': 'Thu, 16 Nov 2017 15:24:30 GMT', + 'Accept-Ranges': 'bytes', + 'Content-Type': 'application/octet-stream', + 'Etag': fakes.NO_MD5}), + dict(method='DELETE', + uri='{endpoint}/{object}'.format( + endpoint=endpoint, + object=object_path)), + ]) + + self.cloud.delete_image(self.image_id) + + self.assert_calls() + def test_delete_autocreated_image_objects(self): self.use_keystone_v3() self.cloud.image_api_use_tasks = True From becf30376822ae54d8d8e319559cb536164f9513 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 18 Jun 2019 09:49:33 +0200 Subject: [PATCH 2500/3836] Volume.Backup restore fixes for the volume backup restoration name or id (or both) is required. Do not force both to be set in v2. - Add restore unit tests - Add new properties in v3 and QP for v2 and v3 - Move test_stats that ended up in `block_store`. Heh? Change-Id: Ie118afe0116ced3580e52941e69cbc57bea2f7f8 --- openstack/block_storage/v2/backup.py | 17 ++++-- openstack/block_storage/v2/stats.py | 2 +- openstack/block_storage/v3/_proxy.py | 3 +- openstack/block_storage/v3/backup.py | 27 +++++++-- openstack/block_storage/v3/stats.py | 2 +- .../v2/test_stats.py | 16 ++--- .../unit/block_storage/v2/test_backup.py | 50 +++++++++++++++- .../v2/test_stats.py | 2 +- .../unit/block_storage/v3/test_backup.py | 59 ++++++++++++++++++- 9 files changed, 156 insertions(+), 22 deletions(-) rename openstack/tests/functional/{block_store => block_storage}/v2/test_stats.py (78%) rename openstack/tests/unit/{block_store => block_storage}/v2/test_stats.py (96%) diff --git a/openstack/block_storage/v2/backup.py b/openstack/block_storage/v2/backup.py index ee3d53163..61d53c62c 100644 --- a/openstack/block_storage/v2/backup.py +++ b/openstack/block_storage/v2/backup.py @@ -9,6 +9,7 @@ # 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 import resource from openstack import utils @@ -20,7 +21,8 @@ class Backup(resource.Resource): base_path = "/backups" _query_mapping = resource.QueryParameters( - 'all_tenants', 'limit', 'marker', + 'all_tenants', 'limit', 'marker', 'project_id', + 'name', 'status', 'volume_id', 'sort_key', 'sort_dir') # capabilities @@ -80,13 +82,20 @@ def restore(self, session, volume_id=None, name=None): :param session: openstack session :param volume_id: The ID of the volume to restore the backup to. :param name: The name for new volume creation to restore. - :return: + :return: Updated backup instance """ url = utils.urljoin(self.base_path, self.id, "restore") - body = {"restore": {"volume_id": volume_id, "name": name}} + body = {'restore': {}} + if volume_id: + body['restore']['volume_id'] = volume_id + if name: + body['restore']['name'] = name + if not (volume_id or name): + raise exceptions.SDKException('Either of `name` or `volume_id`' + ' must be specified.') response = session.post(url, json=body) - self._translate_response(response) + self._translate_response(response, has_body=False) return self diff --git a/openstack/block_storage/v2/stats.py b/openstack/block_storage/v2/stats.py index dcd2e2945..8f9a44224 100644 --- a/openstack/block_storage/v2/stats.py +++ b/openstack/block_storage/v2/stats.py @@ -14,7 +14,7 @@ class Pools(resource.Resource): - resource_key = "pool" + resource_key = "" resources_key = "pools" base_path = "/scheduler-stats/get_pools?detail=True" diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 6590b2fc3..11b72952d 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -228,6 +228,7 @@ def backups(self, details=True, **query): * sort_dir: Sorts by one or more sets of attribute and sort direction combinations. If you omit the sort direction in a set, default is desc. + * project_id: Project ID to query backups for. :returns: A generator of backup objects. """ @@ -290,7 +291,7 @@ def delete_backup(self, backup, ignore_missing=True): self._delete(_backup.Backup, backup, ignore_missing=ignore_missing) - def restore_backup(self, backup, volume_id, name): + def restore_backup(self, backup, volume_id=None, name=None): """Restore a Backup to volume :param backup: The value can be the ID of a backup or a diff --git a/openstack/block_storage/v3/backup.py b/openstack/block_storage/v3/backup.py index ee3d53163..2212cf629 100644 --- a/openstack/block_storage/v3/backup.py +++ b/openstack/block_storage/v3/backup.py @@ -9,6 +9,7 @@ # 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 import resource from openstack import utils @@ -19,8 +20,12 @@ class Backup(resource.Resource): resources_key = "backups" base_path = "/backups" + # TODO(gtema): Starting from ~3.31(3.45) Cinder seems to support also fuzzy + # search (name~, status~, volume_id~). But this is not documented + # officially and seem to require microversion be set _query_mapping = resource.QueryParameters( - 'all_tenants', 'limit', 'marker', + 'all_tenants', 'limit', 'marker', 'project_id', + 'name', 'status', 'volume_id', 'sort_key', 'sort_dir') # capabilities @@ -58,10 +63,15 @@ class Backup(resource.Resource): is_incremental = resource.Body("is_incremental", type=bool) #: A list of links associated with this volume. *Type: list* links = resource.Body("links", type=list) + #: The backup metadata. New in version 3.43 + metadata = resource.Body('metadata', type=dict) #: backup name name = resource.Body("name") #: backup object count object_count = resource.Body("object_count", type=int) + #: The UUID of the owning project. + #: New in version 3.18 + project_id = resource.Body('os-backup-project-attr:project_id') #: The size of the volume, in gibibytes (GiB). size = resource.Body("size", type=int) #: The UUID of the source volume snapshot. @@ -71,6 +81,8 @@ class Backup(resource.Resource): status = resource.Body("status") #: The date and time when the resource was updated. updated_at = resource.Body("updated_at") + #: The UUID of the project owner. New in 3.56 + user_id = resource.Body('user_id') #: The UUID of the volume. volume_id = resource.Body("volume_id") @@ -80,13 +92,20 @@ def restore(self, session, volume_id=None, name=None): :param session: openstack session :param volume_id: The ID of the volume to restore the backup to. :param name: The name for new volume creation to restore. - :return: + :return: Updated backup instance """ url = utils.urljoin(self.base_path, self.id, "restore") - body = {"restore": {"volume_id": volume_id, "name": name}} + body = {'restore': {}} + if volume_id: + body['restore']['volume_id'] = volume_id + if name: + body['restore']['name'] = name + if not (volume_id or name): + raise exceptions.SDKException('Either of `name` or `volume_id`' + ' must be specified.') response = session.post(url, json=body) - self._translate_response(response) + self._translate_response(response, has_body=False) return self diff --git a/openstack/block_storage/v3/stats.py b/openstack/block_storage/v3/stats.py index dcd2e2945..8f9a44224 100644 --- a/openstack/block_storage/v3/stats.py +++ b/openstack/block_storage/v3/stats.py @@ -14,7 +14,7 @@ class Pools(resource.Resource): - resource_key = "pool" + resource_key = "" resources_key = "pools" base_path = "/scheduler-stats/get_pools?detail=True" diff --git a/openstack/tests/functional/block_store/v2/test_stats.py b/openstack/tests/functional/block_storage/v2/test_stats.py similarity index 78% rename from openstack/tests/functional/block_store/v2/test_stats.py rename to openstack/tests/functional/block_storage/v2/test_stats.py index ef64d49a7..63175645f 100644 --- a/openstack/tests/functional/block_store/v2/test_stats.py +++ b/openstack/tests/functional/block_storage/v2/test_stats.py @@ -12,14 +12,15 @@ from openstack.block_storage.v2 import stats as _stats -from openstack.tests.functional import base +from openstack.tests.functional.block_storage.v2 import base -class TestStats(base.BaseFunctionalTest): +class TestStats(base.BaseBlockStorageTest): def setUp(self): super(TestStats, self).setUp() - sot = self.conn.block_storage.backend_pools() + + sot = self.operator_cloud.block_storage.backend_pools() for pool in sot: self.assertIsInstance(pool, _stats.Pools) @@ -35,10 +36,11 @@ def test_list(self): 'allocated_capacity_gb', 'reserved_percentage', 'location_info'] capList.sort() - pools = self.conn.block_storage.backend_pools() + pools = self.operator_cloud.block_storage.backend_pools() for pool in pools: caps = pool.capabilities - keys = caps.keys() - keys.sort() + keys = list(caps.keys()) assert isinstance(caps, dict) - self.assertListEqual(keys, capList) + # Check that we have at minimum listed capabilities + for cap in sorted(capList): + self.assertIn(cap, keys) diff --git a/openstack/tests/unit/block_storage/v2/test_backup.py b/openstack/tests/unit/block_storage/v2/test_backup.py index 58512bfc5..a25a03753 100644 --- a/openstack/tests/unit/block_storage/v2/test_backup.py +++ b/openstack/tests/unit/block_storage/v2/test_backup.py @@ -16,6 +16,7 @@ from openstack.tests.unit import base +from openstack import exceptions from openstack.block_storage.v2 import backup FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff" @@ -42,8 +43,15 @@ class TestBackup(base.TestCase): def setUp(self): super(TestBackup, self).setUp() + self.resp = mock.Mock() + self.resp.body = None + self.resp.json = mock.Mock(return_value=self.resp.body) + self.resp.headers = {} + self.resp.status_code = 202 + self.sess = mock.Mock(spec=adapter.Adapter) self.sess.get = mock.Mock() + self.sess.post = mock.Mock(return_value=self.resp) self.sess.default_microversion = mock.Mock(return_value='') def test_basic(self): @@ -62,8 +70,12 @@ def test_basic(self): "all_tenants": "all_tenants", "limit": "limit", "marker": "marker", + "name": "name", + "project_id": "project_id", "sort_dir": "sort_dir", - "sort_key": "sort_key" + "sort_key": "sort_key", + "status": "status", + "volume_id": "volume_id" }, sot._query_mapping._mapping ) @@ -85,3 +97,39 @@ def test_create(self): self.assertEqual(BACKUP["size"], sot.size) self.assertEqual(BACKUP["has_dependent_backups"], sot.has_dependent_backups) + + def test_restore(self): + sot = backup.Backup(**BACKUP) + + self.assertEqual(sot, sot.restore(self.sess, 'vol', 'name')) + + url = 'backups/%s/restore' % FAKE_ID + body = {"restore": {"volume_id": "vol", "name": "name"}} + self.sess.post.assert_called_with(url, json=body) + + def test_restore_name(self): + sot = backup.Backup(**BACKUP) + + self.assertEqual(sot, sot.restore(self.sess, name='name')) + + url = 'backups/%s/restore' % FAKE_ID + body = {"restore": {"name": "name"}} + self.sess.post.assert_called_with(url, json=body) + + def test_restore_vol_id(self): + sot = backup.Backup(**BACKUP) + + self.assertEqual(sot, sot.restore(self.sess, volume_id='vol')) + + url = 'backups/%s/restore' % FAKE_ID + body = {"restore": {"volume_id": "vol"}} + self.sess.post.assert_called_with(url, json=body) + + def test_restore_no_params(self): + sot = backup.Backup(**BACKUP) + + self.assertRaises( + exceptions.SDKException, + sot.restore, + self.sess + ) diff --git a/openstack/tests/unit/block_store/v2/test_stats.py b/openstack/tests/unit/block_storage/v2/test_stats.py similarity index 96% rename from openstack/tests/unit/block_store/v2/test_stats.py rename to openstack/tests/unit/block_storage/v2/test_stats.py index 729ec6370..53817a4a8 100644 --- a/openstack/tests/unit/block_store/v2/test_stats.py +++ b/openstack/tests/unit/block_storage/v2/test_stats.py @@ -35,7 +35,7 @@ def setUp(self): def test_basic(self): sot = stats.Pools(POOLS) - self.assertEqual("pool", sot.resource_key) + self.assertEqual("", sot.resource_key) self.assertEqual("pools", sot.resources_key) self.assertEqual("/scheduler-stats/get_pools?detail=True", sot.base_path) diff --git a/openstack/tests/unit/block_storage/v3/test_backup.py b/openstack/tests/unit/block_storage/v3/test_backup.py index 4d206c452..a2b0def81 100644 --- a/openstack/tests/unit/block_storage/v3/test_backup.py +++ b/openstack/tests/unit/block_storage/v3/test_backup.py @@ -16,6 +16,7 @@ from openstack.tests.unit import base +from openstack import exceptions from openstack.block_storage.v3 import backup FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff" @@ -34,7 +35,10 @@ "status": "available", "volume_id": "e5185058-943a-4cb4-96d9-72c184c337d6", "is_incremental": True, - "has_dependent_backups": False + "has_dependent_backups": False, + "os-backup-project-attr:project_id": "2c67a14be9314c5dae2ee6c4ec90cf0b", + "user_id": "515ba0dd59f84f25a6a084a45d8d93b2", + "metadata": {"key": "value"} } @@ -42,8 +46,15 @@ class TestBackup(base.TestCase): def setUp(self): super(TestBackup, self).setUp() + self.resp = mock.Mock() + self.resp.body = None + self.resp.json = mock.Mock(return_value=self.resp.body) + self.resp.headers = {} + self.resp.status_code = 202 + self.sess = mock.Mock(spec=adapter.Adapter) self.sess.get = mock.Mock() + self.sess.post = mock.Mock(return_value=self.resp) self.sess.default_microversion = mock.Mock(return_value='') def test_basic(self): @@ -62,8 +73,12 @@ def test_basic(self): "all_tenants": "all_tenants", "limit": "limit", "marker": "marker", + "name": "name", + "project_id": "project_id", "sort_dir": "sort_dir", - "sort_key": "sort_key" + "sort_key": "sort_key", + "status": "status", + "volume_id": "volume_id" }, sot._query_mapping._mapping ) @@ -85,3 +100,43 @@ def test_create(self): self.assertEqual(BACKUP["size"], sot.size) self.assertEqual(BACKUP["has_dependent_backups"], sot.has_dependent_backups) + self.assertEqual(BACKUP['os-backup-project-attr:project_id'], + sot.project_id) + self.assertEqual(BACKUP['metadata'], sot.metadata) + self.assertEqual(BACKUP['user_id'], sot.user_id) + + def test_restore(self): + sot = backup.Backup(**BACKUP) + + self.assertEqual(sot, sot.restore(self.sess, 'vol', 'name')) + + url = 'backups/%s/restore' % FAKE_ID + body = {"restore": {"volume_id": "vol", "name": "name"}} + self.sess.post.assert_called_with(url, json=body) + + def test_restore_name(self): + sot = backup.Backup(**BACKUP) + + self.assertEqual(sot, sot.restore(self.sess, name='name')) + + url = 'backups/%s/restore' % FAKE_ID + body = {"restore": {"name": "name"}} + self.sess.post.assert_called_with(url, json=body) + + def test_restore_vol_id(self): + sot = backup.Backup(**BACKUP) + + self.assertEqual(sot, sot.restore(self.sess, volume_id='vol')) + + url = 'backups/%s/restore' % FAKE_ID + body = {"restore": {"volume_id": "vol"}} + self.sess.post.assert_called_with(url, json=body) + + def test_restore_no_params(self): + sot = backup.Backup(**BACKUP) + + self.assertRaises( + exceptions.SDKException, + sot.restore, + self.sess + ) From 3c72ff38abf4feadf9e6b640035e336088c9a5e5 Mon Sep 17 00:00:00 2001 From: Amol Kahat Date: Thu, 25 Jul 2019 19:56:24 +0530 Subject: [PATCH 2501/3836] Added missing {user, project}_domain_name in Api In examples/connect.py, openstack.connect api call requires user_domain_name and project_domain_name as a required args which was missing lead to api failure while connecting to openstack cloud. It fixes the same. Story: 2006281 Task: 35984 Change-Id: I79d977282f4bc7699957a0275d251b75c2642cc2 Signed-off-by: Amol Kahat --- examples/connect.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/connect.py b/examples/connect.py index ddf781a09..5d24b2cb1 100644 --- a/examples/connect.py +++ b/examples/connect.py @@ -18,10 +18,10 @@ import argparse import os +import sys import openstack from openstack.config import loader -import sys openstack.enable_logging(True, stream=sys.stdout) @@ -69,14 +69,16 @@ def create_connection_from_args(): return openstack.connect(options=parser) -def create_connection(auth_url, region, project_name, username, password): - +def create_connection(auth_url, region, project_name, username, password, + user_domain, project_domain): return openstack.connect( auth_url=auth_url, project_name=project_name, username=username, password=password, region_name=region, + user_domain_name=user_domain, + project_domain_name=project_domain, app_name='examples', app_version='1.0', ) From f162b71cb84439dfeef27f84afc79d1d34c38a9b Mon Sep 17 00:00:00 2001 From: Riccardo Pittau Date: Tue, 23 Jul 2019 11:04:43 +0200 Subject: [PATCH 2502/3836] Add node traits support to baremetal This patch adds add-trait, remove-trait and set-traits methods to the baremetal module. Change-Id: I9c2dd35c1c5f823d8c482f0177e87d49024242a4 --- openstack/baremetal/v1/_proxy.py | 41 +++++++++ openstack/baremetal/v1/node.py | 84 +++++++++++++++++++ .../baremetal/test_baremetal_node.py | 46 ++++++++++ .../tests/unit/baremetal/v1/test_node.py | 45 ++++++++++ .../baremetal-traits-d1137318db33b8d1.yaml | 4 + 5 files changed, 220 insertions(+) create mode 100644 releasenotes/notes/baremetal-traits-d1137318db33b8d1.yaml diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 8673ef7e6..e15c367b6 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -917,3 +917,44 @@ def wait_for_allocation(self, allocation, timeout=None, """ res = self._get_resource(_allocation.Allocation, allocation) return res.wait(self, timeout=timeout, ignore_error=ignore_error) + + def add_node_trait(self, node, trait): + """Add a trait to a node. + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :param trait: trait to remove from the node. + :returns: The updated node + """ + res = self._get_resource(_node.Node, node) + return res.add_trait(self, trait) + + def remove_node_trait(self, node, trait, ignore_missing=True): + """Remove a trait from a node. + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :param trait: trait to remove from the node. + :param bool ignore_missing: When set to ``False``, an exception + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the trait could not be found. When set to ``True``, no + exception will be raised when attempting to delete a non-existent + trait. + :returns: The updated :class:`~openstack.baremetal.v1.node.Node` + """ + res = self._get_resource(_node.Node, node) + return res.remove_trait(self, trait, ignore_missing=ignore_missing) + + def set_node_traits(self, node, traits): + """Set traits for a node. + + Removes any existing traits and adds the traits passed in to this + method. + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :param traits: list of traits to add to the node. + :returns: The updated :class:`~openstack.baremetal.v1.node.Node` + """ + res = self._get_resource(_node.Node, node) + return res.set_traits(self, traits) diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 236103c7a..e32c2a5fc 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -745,5 +745,89 @@ def set_boot_device(self, session, boot_device, persistent=False): .format(node=self.id)) exceptions.raise_from_response(response, error_message=msg) + def add_trait(self, session, trait): + """Add a trait to a node. + + :param session: The session to use for making this request. + :param trait: The trait to add to the node. + :returns: The updated :class:`~openstack.baremetal.v1.node.Node` + """ + session = self._get_session(session) + version = utils.pick_microversion(session, '1.37') + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'traits', trait) + response = session.put( + request.url, json=None, + headers=request.headers, microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + msg = ("Failed to add trait {trait} for node {node}" + .format(trait=trait, node=self.id)) + exceptions.raise_from_response(response, error_message=msg) + + self.traits = list(set(self.traits or ()) | {trait}) + + def remove_trait(self, session, trait, ignore_missing=True): + """Remove a trait from a node. + + :param session: The session to use for making this request. + :param trait: The trait to remove from the node. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the trait does not exist. + Otherwise, ``False`` is returned. + :returns: The updated :class:`~openstack.baremetal.v1.node.Node` + """ + session = self._get_session(session) + version = utils.pick_microversion(session, '1.37') + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'traits', trait) + + response = session.delete( + request.url, headers=request.headers, microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + if ignore_missing or response.status_code == 400: + session.log.debug( + 'Trait %(trait)s was already removed from node %(node)s', + {'trait': trait, 'node': self.id}) + return False + + msg = ("Failed to remove trait {trait} from bare metal node {node}" + .format(node=self.id, trait=trait)) + exceptions.raise_from_response(response, error_message=msg) + + self.traits = list(set(self.traits) - {trait}) + + return True + + def set_traits(self, session, traits): + """Set traits for a node. + + Removes any existing traits and adds the traits passed in to this + method. + + :param session: The session to use for making this request. + :param traits: list of traits to add to the node. + :returns: The updated :class:`~openstack.baremetal.v1.node.Node` + """ + session = self._get_session(session) + version = utils.pick_microversion(session, '1.37') + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'traits') + + body = {'traits': traits} + + response = session.put( + request.url, json=body, + headers=request.headers, microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + msg = ("Failed to set traits for node {node}" + .format(node=self.id)) + exceptions.raise_from_response(response, error_message=msg) + + self.traits = traits + NodeDetail = Node diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index 37bdbacc1..2c727d977 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -303,3 +303,49 @@ def test_node_vif_negative(self): self.assertRaises(exceptions.ResourceNotFound, self.conn.baremetal.detach_vif_from_node, uuid, self.vif_id, ignore_missing=False) + + +class TestTraits(base.BaseBaremetalTest): + + min_microversion = '1.37' + + def setUp(self): + super(TestTraits, self).setUp() + self.node = self.create_node() + + def test_add_remove_node_trait(self): + node = self.conn.baremetal.get_node(self.node) + self.assertEqual([], node.traits) + + self.conn.baremetal.add_node_trait(self.node, 'CUSTOM_FAKE') + self.assertEqual(['CUSTOM_FAKE'], self.node.traits) + node = self.conn.baremetal.get_node(self.node) + self.assertEqual(['CUSTOM_FAKE'], node.traits) + + self.conn.baremetal.add_node_trait(self.node, 'CUSTOM_REAL') + self.assertEqual(['CUSTOM_FAKE', 'CUSTOM_REAL'], self.node.traits) + node = self.conn.baremetal.get_node(self.node) + self.assertEqual(['CUSTOM_FAKE', 'CUSTOM_REAL'], node.traits) + + self.conn.baremetal.remove_node_trait(node, 'CUSTOM_FAKE', + ignore_missing=False) + self.assertEqual(['CUSTOM_REAL'], self.node.traits) + node = self.conn.baremetal.get_node(self.node) + self.assertEqual(['CUSTOM_REAL'], node.traits) + + def test_set_node_traits(self): + node = self.conn.baremetal.get_node(self.node) + self.assertEqual([], node.traits) + + traits1 = ['CUSTOM_FAKE', 'CUSTOM_REAL'] + traits2 = ['CUSTOM_FOOBAR'] + + self.conn.baremetal.set_node_traits(self.node, traits1) + self.assertEqual(['CUSTOM_FAKE', 'CUSTOM_REAL'], self.node.traits) + node = self.conn.baremetal.get_node(self.node) + self.assertEqual(['CUSTOM_FAKE', 'CUSTOM_REAL'], node.traits) + + self.conn.baremetal.set_node_traits(self.node, traits2) + self.assertEqual(['CUSTOM_FOOBAR'], self.node.traits) + node = self.conn.baremetal.get_node(self.node) + self.assertEqual(['CUSTOM_FOOBAR'], node.traits) diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 1d6a2426f..234a7b25d 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -702,3 +702,48 @@ def test_node_set_boot_device(self): json={'boot_device': 'pxe', 'persistent': False}, headers=mock.ANY, microversion=mock.ANY, retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + +@mock.patch.object(node.Node, 'fetch', lambda self, session: self) +@mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) +class TestNodeTraits(base.TestCase): + + def setUp(self): + super(TestNodeTraits, self).setUp() + self.node = node.Node(**FAKE) + self.session = mock.Mock(spec=adapter.Adapter, + default_microversion='1.37') + self.session.log = mock.Mock() + + def test_node_add_trait(self): + self.node.add_trait(self.session, 'CUSTOM_FAKE') + self.session.put.assert_called_once_with( + 'nodes/%s/traits/%s' % (self.node.id, 'CUSTOM_FAKE'), + json=None, + headers=mock.ANY, microversion='1.37', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + def test_remove_trait(self): + self.node.remove_trait(self.session, 'CUSTOM_FAKE') + self.session.delete.assert_called_once_with( + 'nodes/%s/traits/%s' % (self.node.id, 'CUSTOM_FAKE'), + headers=mock.ANY, microversion='1.37', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + def test_remove_trait_missing(self): + self.session.delete.return_value.status_code = 400 + self.assertFalse(self.node.remove_trait(self.session, + 'CUSTOM_MISSING')) + self.session.delete.assert_called_once_with( + 'nodes/%s/traits/%s' % (self.node.id, 'CUSTOM_MISSING'), + headers=mock.ANY, microversion='1.37', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + def test_set_traits(self): + traits = ['CUSTOM_FAKE', 'CUSTOM_REAL', 'CUSTOM_MISSING'] + self.node.set_traits(self.session, traits) + self.session.put.assert_called_once_with( + 'nodes/%s/traits' % self.node.id, + json={'traits': ['CUSTOM_FAKE', 'CUSTOM_REAL', 'CUSTOM_MISSING']}, + headers=mock.ANY, microversion='1.37', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) diff --git a/releasenotes/notes/baremetal-traits-d1137318db33b8d1.yaml b/releasenotes/notes/baremetal-traits-d1137318db33b8d1.yaml new file mode 100644 index 000000000..7e706e611 --- /dev/null +++ b/releasenotes/notes/baremetal-traits-d1137318db33b8d1.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Implements add/remove/set traits API for bare metal nodes. From 7d25db261094d32c01e63f645ef4c8a7b195c431 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Tue, 30 Jul 2019 07:05:19 +0200 Subject: [PATCH 2503/3836] Update links The openstacksdk docs are published at docs.o.o, not developer.o.o since quite some time. Fix links to point to current version of docs. Add missing links, thus removing some TODOs. Change-Id: I5e3b4c70bc0875a1dfcc587b60a95258a61b4407 --- examples/clustering/action.py | 2 +- examples/clustering/cluster.py | 2 +- examples/clustering/event.py | 2 +- examples/clustering/node.py | 2 +- examples/clustering/policy.py | 2 +- examples/clustering/policy_type.py | 2 +- examples/clustering/profile.py | 2 +- examples/clustering/profile_type.py | 2 +- examples/clustering/receiver.py | 2 +- examples/compute/create.py | 3 ++- examples/compute/delete.py | 3 ++- examples/compute/find.py | 3 ++- examples/compute/list.py | 3 ++- examples/connect.py | 3 ++- examples/dns/list.py | 3 ++- examples/identity/list.py | 3 ++- examples/image/create.py | 2 +- examples/image/delete.py | 2 +- examples/image/download.py | 2 +- examples/image/import.py | 2 +- examples/image/list.py | 2 +- examples/network/create.py | 3 ++- examples/network/delete.py | 3 ++- examples/network/find.py | 3 ++- examples/network/list.py | 3 ++- examples/network/security_group_rules.py | 3 ++- setup.cfg | 2 +- 27 files changed, 39 insertions(+), 27 deletions(-) diff --git a/examples/clustering/action.py b/examples/clustering/action.py index cdbe7c646..ff0f0b4ed 100644 --- a/examples/clustering/action.py +++ b/examples/clustering/action.py @@ -14,7 +14,7 @@ Managing policies in the Cluster service. For a full guide see -https://developer.openstack.org/sdks/python/openstacksdk/user/guides/cluster.html +https://docs.openstack.org/openstacksdk/latest/user/guides/clustering.html """ ACTION_ID = "06ad259b-d6ab-4eb2-a0fa-fb144437eab1" diff --git a/examples/clustering/cluster.py b/examples/clustering/cluster.py index 0c49beb2a..60f06ae19 100644 --- a/examples/clustering/cluster.py +++ b/examples/clustering/cluster.py @@ -14,7 +14,7 @@ Managing policies in the Cluster service. For a full guide see -https://developer.openstack.org/sdks/python/openstacksdk/user/guides/cluster.html +https://docs.openstack.org/openstacksdk/latest/user/guides/clustering.html """ CLUSTER_NAME = "Test_Cluster" diff --git a/examples/clustering/event.py b/examples/clustering/event.py index e4f477bc4..e6f18807a 100644 --- a/examples/clustering/event.py +++ b/examples/clustering/event.py @@ -14,7 +14,7 @@ Managing policies in the Cluster service. For a full guide see -https://developer.openstack.org/sdks/python/openstacksdk/user/guides/cluster.html +https://docs.openstack.org/openstacksdk/latest/user/guides/clustering.html """ EVENT_ID = "5d982071-76c5-4733-bf35-b9e38a563c99" diff --git a/examples/clustering/node.py b/examples/clustering/node.py index 2ce8a0d99..4217a206e 100644 --- a/examples/clustering/node.py +++ b/examples/clustering/node.py @@ -14,7 +14,7 @@ Managing policies in the Cluster service. For a full guide see -https://developer.openstack.org/sdks/python/openstacksdk/user/guides/cluster.html +https://docs.openstack.org/openstacksdk/latest/user/guides/clustering.html """ NODE_NAME = 'Test_Node' diff --git a/examples/clustering/policy.py b/examples/clustering/policy.py index 2f4c3361b..328196fd6 100644 --- a/examples/clustering/policy.py +++ b/examples/clustering/policy.py @@ -14,7 +14,7 @@ Managing policies in the Cluster service. For a full guide see -https://developer.openstack.org/sdks/python/openstacksdk/user/guides/cluster.html +https://docs.openstack.org/openstacksdk/latest/user/guides/clustering.html """ diff --git a/examples/clustering/policy_type.py b/examples/clustering/policy_type.py index 2eb72f44a..a5618e418 100644 --- a/examples/clustering/policy_type.py +++ b/examples/clustering/policy_type.py @@ -14,7 +14,7 @@ Managing policy types in the Cluster service. For a full guide see -https://developer.openstack.org/sdks/python/openstacksdk/user/guides/cluster.html +https://docs.openstack.org/openstacksdk/latest/user/guides/clustering.html """ diff --git a/examples/clustering/profile.py b/examples/clustering/profile.py index ffaf3a6d2..d69fe1996 100644 --- a/examples/clustering/profile.py +++ b/examples/clustering/profile.py @@ -19,7 +19,7 @@ Managing profiles in the Cluster service. For a full guide see -https://developer.openstack.org/sdks/python/openstacksdk/user/guides/cluster.html +https://docs.openstack.org/openstacksdk/latest/user/guides/clustering.html """ diff --git a/examples/clustering/profile_type.py b/examples/clustering/profile_type.py index fa5403b83..ab84811d5 100644 --- a/examples/clustering/profile_type.py +++ b/examples/clustering/profile_type.py @@ -14,7 +14,7 @@ Managing profile types in the Cluster service. For a full guide see -https://developer.openstack.org/sdks/python/openstacksdk/user/guides/clustering.html +https://docs.openstack.org/openstacksdk/latest/user/guides/clustering.html """ diff --git a/examples/clustering/receiver.py b/examples/clustering/receiver.py index a0c374925..84c3febe3 100644 --- a/examples/clustering/receiver.py +++ b/examples/clustering/receiver.py @@ -14,7 +14,7 @@ Managing policies in the Cluster service. For a full guide see -https://developer.openstack.org/sdks/python/openstacksdk/user/guides/cluster.html +https://docs.openstack.org/openstacksdk/latest/user/guides/clustering.html """ FAKE_NAME = 'test_receiver' diff --git a/examples/compute/create.py b/examples/compute/create.py index c04d89814..342421f07 100644 --- a/examples/compute/create.py +++ b/examples/compute/create.py @@ -24,7 +24,8 @@ """ Create resources with the Compute service. -For a full guide see TODO(etoews):link to docs on developer.openstack.org +For a full guide see +https://docs.openstack.org/openstacksdk/latest/user/guides/compute.html """ diff --git a/examples/compute/delete.py b/examples/compute/delete.py index ba3443147..6fce1a3a2 100644 --- a/examples/compute/delete.py +++ b/examples/compute/delete.py @@ -20,7 +20,8 @@ """ Delete resources with the Compute service. -For a full guide see TODO(etoews):link to docs on developer.openstack.org +For a full guide see +https://docs.openstack.org/openstacksdk/latest/user/guides/compute.html """ diff --git a/examples/compute/find.py b/examples/compute/find.py index b009df922..64ca9e355 100644 --- a/examples/compute/find.py +++ b/examples/compute/find.py @@ -15,7 +15,8 @@ """ Find a resource from the Compute service. -For a full guide see TODO(etoews):link to docs on developer.openstack.org +For a full guide see +https://docs.openstack.org/openstacksdk/latest/user/guides/compute.html """ diff --git a/examples/compute/list.py b/examples/compute/list.py index 0886f8818..db53e81f3 100644 --- a/examples/compute/list.py +++ b/examples/compute/list.py @@ -13,7 +13,8 @@ """ List resources from the Compute service. -For a full guide see TODO(etoews):link to docs on developer.openstack.org +For a full guide see +https://docs.openstack.org/openstacksdk/latest/user/guides/compute.html """ diff --git a/examples/connect.py b/examples/connect.py index ddf781a09..4a4eaa92f 100644 --- a/examples/connect.py +++ b/examples/connect.py @@ -13,7 +13,8 @@ """ Connect to an OpenStack cloud. -For a full guide see TODO(etoews):link to docs on developer.openstack.org +For a full guide see +https://docs.openstack.org/openstacksdk/latest/user/guides/connect_from_config.html """ import argparse diff --git a/examples/dns/list.py b/examples/dns/list.py index 47024801e..50a5e8170 100644 --- a/examples/dns/list.py +++ b/examples/dns/list.py @@ -13,7 +13,8 @@ """ List resources from the DNS service. -For a full guide see TODO(gtema):link to docs on developer.openstack.org +For a full guide see +https://docs.openstack.org/openstacksdk/latest/user/guides/dns.html """ diff --git a/examples/identity/list.py b/examples/identity/list.py index fff73a50f..069480b45 100644 --- a/examples/identity/list.py +++ b/examples/identity/list.py @@ -13,7 +13,8 @@ """ List resources from the Identity service. -For a full guide see TODO(etoews):link to docs on developer.openstack.org +For a full guide see +https://docs.openstack.org/openstacksdk/latest/user/guides/identity.html """ diff --git a/examples/image/create.py b/examples/image/create.py index c3d64496b..a80fc33f1 100644 --- a/examples/image/create.py +++ b/examples/image/create.py @@ -16,7 +16,7 @@ Create resources with the Image service. For a full guide see -http://developer.openstack.org/sdks/python/openstacksdk/user/guides/image.html +https://docs.openstack.org/openstacksdk/latest/user/guides/image.html """ diff --git a/examples/image/delete.py b/examples/image/delete.py index 6344bf98a..b3eb290f0 100644 --- a/examples/image/delete.py +++ b/examples/image/delete.py @@ -16,7 +16,7 @@ Delete resources with the Image service. For a full guide see -http://developer.openstack.org/sdks/python/openstacksdk/user/guides/image.html +https://docs.openstack.org/openstacksdk/latest/user/guides/image.html """ diff --git a/examples/image/download.py b/examples/image/download.py index f1611116f..85d92b214 100644 --- a/examples/image/download.py +++ b/examples/image/download.py @@ -16,7 +16,7 @@ Download an image with the Image service. For a full guide see -http://developer.openstack.org/sdks/python/openstacksdk/user/guides/image.html +https://docs.openstack.org/openstacksdk/latest/user/guides/image.html """ diff --git a/examples/image/import.py b/examples/image/import.py index ec9c6c67e..277bb6349 100644 --- a/examples/image/import.py +++ b/examples/image/import.py @@ -16,7 +16,7 @@ Create resources with the Image service. For a full guide see -http://developer.openstack.org/sdks/python/openstacksdk/user/guides/image.html +https://docs.openstack.org/openstacksdk/latest/user/guides/image.html """ diff --git a/examples/image/list.py b/examples/image/list.py index 843e5f965..12eecbaa8 100644 --- a/examples/image/list.py +++ b/examples/image/list.py @@ -14,7 +14,7 @@ List resources from the Image service. For a full guide see -http://developer.openstack.org/sdks/python/openstacksdk/user/guides/image.html +https://docs.openstack.org/openstacksdk/latest/user/guides/image.html """ diff --git a/examples/network/create.py b/examples/network/create.py index f89387b8d..1342d7a98 100644 --- a/examples/network/create.py +++ b/examples/network/create.py @@ -13,7 +13,8 @@ """ Create resources with the Network service. -For a full guide see TODO(etoews):link to docs on developer.openstack.org +For a full guide see +https://docs.openstack.org/openstacksdk/latest/user/guides/network.html """ diff --git a/examples/network/delete.py b/examples/network/delete.py index 41e7a3bbd..720dfb5be 100644 --- a/examples/network/delete.py +++ b/examples/network/delete.py @@ -13,7 +13,8 @@ """ Delete resources with the Network service. -For a full guide see TODO(etoews):link to docs on developer.openstack.org +For a full guide see +https://docs.openstack.org/openstacksdk/latest/user/guides/network.html """ diff --git a/examples/network/find.py b/examples/network/find.py index 1d9005d13..95d3a118b 100644 --- a/examples/network/find.py +++ b/examples/network/find.py @@ -15,7 +15,8 @@ """ Find a resource from the Network service. -For a full guide see TODO(etoews):link to docs on developer.openstack.org +For a full guide see +https://docs.openstack.org/openstacksdk/latest/user/guides/network.html """ diff --git a/examples/network/list.py b/examples/network/list.py index ff85e1c27..2cedba5be 100644 --- a/examples/network/list.py +++ b/examples/network/list.py @@ -13,7 +13,8 @@ """ List resources from the Network service. -For a full guide see TODO(etoews):link to docs on developer.openstack.org +For a full guide see +https://docs.openstack.org/openstacksdk/latest/user/guides/network.html """ diff --git a/examples/network/security_group_rules.py b/examples/network/security_group_rules.py index b2cf42dda..b6c1533b5 100644 --- a/examples/network/security_group_rules.py +++ b/examples/network/security_group_rules.py @@ -13,7 +13,8 @@ """ Create resources with the Network service. -For a full guide see TODO(etoews):link to docs on developer.openstack.org +For a full guide see +https://docs.openstack.org/openstacksdk/latest/user/guides/network.html """ diff --git a/setup.cfg b/setup.cfg index 532962a52..45d9a23a6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,7 @@ description-file = README.rst author = OpenStack author-email = openstack-discuss@lists.openstack.org -home-page = http://developer.openstack.org/sdks/python/openstacksdk/ +home-page = https://docs.openstack.org/openstacksdk/ classifier = Environment :: OpenStack Intended Audience :: Information Technology From e702b020ca1b70a6866608c0c010f6efb6d3dae0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 1 Aug 2019 10:04:10 -0400 Subject: [PATCH 2504/3836] Fix README example for cloud layer This was showing 'import openstack.cloud' but then using openstack directly. Fix that. Then, reorganize a little bit. Change-Id: Iecb7cd02cc6648c6e78666da0d237fc28643d2ae --- README.rst | 69 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/README.rst b/README.rst index b5c72b484..cbd532db7 100644 --- a/README.rst +++ b/README.rst @@ -88,6 +88,47 @@ List servers using objects configured with the ``clouds.yaml`` file: for server in conn.compute.servers(): print(server.to_dict()) +Cloud Layer +=========== + +``openstacksdk`` contains a higher-level layer based on logical operations. + +.. code-block:: python + + import openstack + + # Initialize and turn on debug logging + openstack.enable_logging(debug=True) + + for server in conn.list_servers(): + print(server.to_dict()) + +The benefit is mostly seen in more complicated operations that take multiple +steps and where the steps vary across providers: + +.. code-block:: python + + import openstack + + # Initialize and turn on debug logging + openstack.enable_logging(debug=True) + + # Initialize connection + # Cloud configs are read with openstack.config + conn = openstack.connect(cloud='mordred') + + # Upload an image to the cloud + image = conn.create_image( + 'ubuntu-trusty', filename='ubuntu-trusty.qcow2', wait=True) + + # Find a flavor with at least 512M of RAM + flavor = conn.get_flavor_by_ram(512) + + # Boot a server, wait for it to boot, and then do whatever is needed + # to get a public ip for it. + conn.create_server( + 'my-server', image=image, flavor=flavor, wait=True, auto_ip=True) + openstack.config ================ @@ -124,34 +165,6 @@ in the following locations: More information at https://docs.openstack.org/openstacksdk/latest/user/config/configuration.html -openstack.cloud -=============== - -Create a server using objects configured with the ``clouds.yaml`` file: - -.. code-block:: python - - import openstack.cloud - - # Initialize and turn on debug logging - openstack.enable_logging(debug=True) - - # Initialize connection - # Cloud configs are read with openstack.config - conn = openstack.connect(cloud='mordred') - - # Upload an image to the cloud - image = conn.create_image( - 'ubuntu-trusty', filename='ubuntu-trusty.qcow2', wait=True) - - # Find a flavor with at least 512M of RAM - flavor = conn.get_flavor_by_ram(512) - - # Boot a server, wait for it to boot, and then do whatever is needed - # to get a public ip for it. - conn.create_server( - 'my-server', image=image, flavor=flavor, wait=True, auto_ip=True) - Links ===== From b98e7098c80aa10171db89edde3107acfbf31573 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 1 Aug 2019 10:14:21 -0400 Subject: [PATCH 2505/3836] Move the history lesson into the docs It's not really the most important thing now is it? Change-Id: I3a595eb774bf04a53e1dd09484058ed148b6c122 --- README.rst | 57 ++---------------------------- doc/source/contributor/history.rst | 48 +++++++++++++++++++++++++ doc/source/contributor/index.rst | 9 +++-- 3 files changed, 57 insertions(+), 57 deletions(-) create mode 100644 doc/source/contributor/history.rst diff --git a/README.rst b/README.rst index cbd532db7..3f01b050c 100644 --- a/README.rst +++ b/README.rst @@ -14,61 +14,8 @@ write an application that talks to clouds no matter what crazy choices the deployer has made in an attempt to be more hipster than their self-entitled narcissist peers, then the Cloud Abstraction layer is for you. -A Brief History ---------------- - -.. TODO(shade) This history section should move to the docs. We can put a - link to the published URL here in the README, but it's too long. - -openstacksdk started its life as three different libraries: shade, -os-client-config and python-openstacksdk. - -``shade`` started its life as some code inside of OpenStack Infra's `nodepool`_ -project, and as some code inside of the `Ansible OpenStack Modules`_. -Ansible had a bunch of different OpenStack related modules, and there was a -ton of duplicated code. Eventually, between refactoring that duplication into -an internal library, and adding the logic and features that the OpenStack Infra -team had developed to run client applications at scale, it turned out that we'd -written nine-tenths of what we'd need to have a standalone library. - -Because of its background from nodepool, shade contained abstractions to -work around deployment differences and is resource oriented rather than service -oriented. This allows a user to think about Security Groups without having to -know whether Security Groups are provided by Nova or Neutron on a given cloud. -On the other hand, as an interface that provides an abstraction, it deviates -from the published OpenStack REST API and adds its own opinions, which may not -get in the way of more advanced users with specific needs. - -``os-client-config`` was a library for collecting client configuration for -using an OpenStack cloud in a consistent and comprehensive manner, which -introduced the ``clouds.yaml`` file for expressing named cloud configurations. - -``python-openstacksdk`` was a library that exposed the OpenStack APIs to -developers in a consistent and predictable manner. - -After a while it became clear that there was value in both the high-level -layer that contains additional business logic and the lower-level SDK that -exposes services and their resources faithfully and consistently as Python -objects. - -Even with both of those layers, it is still beneficial at times to be able to -make direct REST calls and to do so with the same properly configured -`Session`_ from `python-requests`_. - -This led to the merge of the three projects. - -The original contents of the shade library have been moved into -``openstack.cloud`` and os-client-config has been moved in to -``openstack.config``. Future releases of shade will provide a thin -compatibility layer that subclasses the objects from ``openstack.cloud`` -and provides different argument defaults where needed for compatibility. -Similarly future releases of os-client-config will provide a compatibility -layer shim around ``openstack.config``. - -.. _nodepool: https://docs.openstack.org/infra/nodepool/ -.. _Ansible OpenStack Modules: http://docs.ansible.com/ansible/latest/list_of_cloud_modules.html#openstack -.. _Session: http://docs.python-requests.org/en/master/user/advanced/#session-objects -.. _python-requests: http://docs.python-requests.org/en/master/ +More information about its history can be found at +https://docs.openstack.org/openstacksdk/latest/contributor/history.html openstack ========= diff --git a/doc/source/contributor/history.rst b/doc/source/contributor/history.rst new file mode 100644 index 000000000..02a43f754 --- /dev/null +++ b/doc/source/contributor/history.rst @@ -0,0 +1,48 @@ +A Brief History +=============== + +openstacksdk started its life as three different libraries: shade, +os-client-config and python-openstacksdk. + +``shade`` started its life as some code inside of OpenStack Infra's `nodepool`_ +project, and as some code inside of the `Ansible OpenStack Modules`_. +Ansible had a bunch of different OpenStack related modules, and there was a +ton of duplicated code. Eventually, between refactoring that duplication into +an internal library, and adding the logic and features that the OpenStack Infra +team had developed to run client applications at scale, it turned out that we'd +written nine-tenths of what we'd need to have a standalone library. + +Because of its background from nodepool, shade contained abstractions to +work around deployment differences and is resource oriented rather than service +oriented. This allows a user to think about Security Groups without having to +know whether Security Groups are provided by Nova or Neutron on a given cloud. +On the other hand, as an interface that provides an abstraction, it deviates +from the published OpenStack REST API and adds its own opinions, which may not +get in the way of more advanced users with specific needs. + +``os-client-config`` was a library for collecting client configuration for +using an OpenStack cloud in a consistent and comprehensive manner, which +introduced the ``clouds.yaml`` file for expressing named cloud configurations. + +``python-openstacksdk`` was a library that exposed the OpenStack APIs to +developers in a consistent and predictable manner. + +After a while it became clear that there was value in both the high-level +layer that contains additional business logic and the lower-level SDK that +exposes services and their resources faithfully and consistently as Python +objects. + +Even with both of those layers, it is still beneficial at times to be able to +make direct REST calls and to do so with the same properly configured +`Session`_ from `python-requests`_. + +This led to the merge of the three projects. + +The original contents of the shade library have been moved into +``openstack.cloud`` and os-client-config has been moved in to +``openstack.config``. + +.. _nodepool: https://docs.openstack.org/infra/nodepool/ +.. _Ansible OpenStack Modules: http://docs.ansible.com/ansible/latest/list_of_cloud_modules.html#openstack +.. _Session: http://docs.python-requests.org/en/master/user/advanced/#session-objects +.. _python-requests: http://docs.python-requests.org/en/master/ diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst index 1e871335b..d9573fde1 100644 --- a/doc/source/contributor/index.rst +++ b/doc/source/contributor/index.rst @@ -10,8 +10,13 @@ About the Project The OpenStack SDK is a OpenStack project aimed at providing a complete software development kit for the programs which make up the OpenStack -community. It is a set of Python-based libraries, documentation, examples, -and tools released under the Apache 2 license. +community. It is a Python library with corresponding documentation, +examples, and tools released under the Apache 2 license. + +.. toctree:: + :maxdepth: 2 + + history Contribution Mechanics ---------------------- From 9a24ee966274d03fffc96bc058a82b449660e799 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 1 Aug 2019 16:30:36 -0400 Subject: [PATCH 2506/3836] Add CloudRegion helper method for arbitrary endpoints Sometimes you are faced with dealing with a non-standard service and you don't want to type c.session.auth.get_endpoint( c.session, service_type='example-service-type', region_name=c.config.region_name, interface=c.config.interface)) Add a helper method so you don't have to. Change-Id: I50d179abc528a884bae4b4af72936b634b26e93b --- openstack/config/cloud_region.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 7728f2226..df4d92790 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -451,6 +451,14 @@ def get_endpoint(self, service_type): value = auth.get('auth_url') return value + def get_endpoint_from_catalog(self, service_type): + session = self.get_session() + return session.auth.get_endpoint( + session, + service_type=service_type, + region_name=self.get_region_name(service_type), + interface=self.get_interface(service_type)) + def get_connect_retries(self, service_type): return self._get_config('connect_retries', service_type, fallback_to_unprefixed=True, From a739390c68e0b8fe9f84ddb9116c27dd65e7a331 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Fri, 2 Aug 2019 14:48:40 +0200 Subject: [PATCH 2507/3836] baremetal.configdrive: tolerate user_data as a string In theory user data is binary. But in practice many formats are textual, so it makes sense to accept strings as well. Change-Id: I2b5e09339480df081796d3066cd6ede6b72b0e1e --- openstack/baremetal/configdrive.py | 6 +++++- openstack/tests/unit/baremetal/test_configdrive.py | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/openstack/baremetal/configdrive.py b/openstack/baremetal/configdrive.py index 13348fe42..74e95e451 100644 --- a/openstack/baremetal/configdrive.py +++ b/openstack/baremetal/configdrive.py @@ -52,7 +52,11 @@ def populate_directory(metadata, user_data=None, versions=None, json.dump(network_data, fp) if user_data: - with open(os.path.join(subdir, 'user_data'), 'wb') as fp: + # Strictly speaking, user data is binary, but in many cases + # it's actually a text (cloud-init, ignition, etc). + flag = 't' if isinstance(user_data, six.text_type) else 'b' + with open(os.path.join(subdir, 'user_data'), + 'w%s' % flag) as fp: fp.write(user_data) yield d diff --git a/openstack/tests/unit/baremetal/test_configdrive.py b/openstack/tests/unit/baremetal/test_configdrive.py index 72cade0d5..709f3b124 100644 --- a/openstack/tests/unit/baremetal/test_configdrive.py +++ b/openstack/tests/unit/baremetal/test_configdrive.py @@ -48,6 +48,8 @@ def _check(self, metadata, user_data=None, network_data=None): if user_data is None: self.assertFalse(os.path.exists(user_data_file)) else: + if isinstance(user_data, six.text_type): + user_data = user_data.encode() with open(user_data_file, 'rb') as fp: self.assertEqual(user_data, fp.read()) @@ -60,6 +62,9 @@ def test_without_user_data(self): def test_with_user_data(self): self._check({'foo': 42}, b'I am user data') + def test_with_user_data_as_string(self): + self._check({'foo': 42}, u'I am user data') + def test_with_network_data(self): self._check({'foo': 42}, network_data={'networks': {}}) From 799bd0a773fb082c1a45262922d87630abc9bd30 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Fri, 2 Aug 2019 14:15:59 -0400 Subject: [PATCH 2508/3836] Fix DeprecationWarning for using logger.warn The logger.warn method is deprecated, use warning instead. https://docs.python.org/3.6/library/logging.html#logging.warning Change-Id: Id20ce8011aa0ac901a138132f7b1932c601e19ab Story: 2006330 Task: 36070 --- openstack/config/cloud_region.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 7728f2226..3ee9388d8 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -170,8 +170,8 @@ def from_conf(conf, session=None, **kwargs): "for project '{project}' (service type " "'{service_type}'): {exception}".format( project=project_name, service_type=st, exception=e)) - _logger.warn("Disabling service '{service_type}': {reason}".format( - service_type=st, reason=reason)) + _logger.warning("Disabling service '{service_type}': " + "{reason}".format(service_type=st, reason=reason)) _disable_service(config_dict, st, reason=reason) continue # Load them into config_dict under keys prefixed by ${service_type}_ From 4c0071916dea9031721bcb84cf87c202e269a424 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 2 Aug 2019 17:42:23 +0200 Subject: [PATCH 2509/3836] Enable ansible module test for keypair to check return data With a change in cloud layer to use proxy/resource layer for keypair Ansible got borked and is not capable in resolving data we return it back. Add test for it searching for the cause. Investigation resulted in the fact, that when invoked by Ansible native implementation of the Resoure.items was not suddifient under Py3, while under Py2 it is fine. An attempt to integrate all invoked functions into SDK from Ansible has not brought any success. Change-Id: I60dec9ba26176efc5b8ad8378b0ef414754a857c --- openstack/resource.py | 16 ++++++++++++++++ .../tests/ansible/roles/keypair/tasks/main.yml | 8 ++++++++ openstack/tests/unit/test_resource.py | 18 ++++++++++++++++++ tox.ini | 2 +- 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/openstack/resource.py b/openstack/resource.py index f3d776464..a6ab26d06 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -630,6 +630,22 @@ def keys(self): # remotes or "unknown" return self._attributes() + def items(self): + # This method is critically required for Ansible "jsonify" + # NOTE(gtema) For some reason when running from SDK itself the native + # implementation of the method is absolutely sifficient, when called + # from Ansible - the values are often empty. Even integrating all + # Ansible internal methods did not help to find the root cause. Another + # fact is that under Py2 everything is fine, while under Py3 it fails. + # There is currently no direct test for Ansible-SDK issue. It is tested + # implicitely in the keypair role for ansible module, where an assert + # verifies presence of attributes. + res = [] + for attr in self._attributes(): + # Append key, value tuple to result list + res.append((attr, self[attr])) + return res + def _update(self, **attrs): """Given attributes, update them on this instance diff --git a/openstack/tests/ansible/roles/keypair/tasks/main.yml b/openstack/tests/ansible/roles/keypair/tasks/main.yml index 53a856e2f..636bf1aca 100644 --- a/openstack/tests/ansible/roles/keypair/tasks/main.yml +++ b/openstack/tests/ansible/roles/keypair/tasks/main.yml @@ -4,6 +4,14 @@ cloud: "{{ cloud }}" name: "{{ keypair_name }}" state: present + register: + keypair + +# This assert verifies that Ansible is capable serializing data returned by SDK +- name: Ensure private key is returned + assert: + that: + - keypair.key.public_key is defined and keypair.key.public_key - name: Delete keypair (non-existing) os_keypair: diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index d51a2b560..d435a89ae 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -896,6 +896,24 @@ class Test(resource.Resource): actual = json.dumps(res, sort_keys=True) self.assertEqual(expected, actual) + def test_items(self): + class Test(resource.Resource): + foo = resource.Body('foo') + bar = resource.Body('bar') + foot = resource.Body('foot') + + data = { + 'foo': 'bar', + 'bar': 'foo\n', + 'foot': 'a:b:c:d' + } + + res = Test(**data) + for k, v in res.items(): + expected = data.get(k) + if expected: + self.assertEqual(v, expected) + def test_access_by_aka(self): class Test(resource.Resource): foo = resource.Header('foo_remote', aka='foo_alias') diff --git a/tox.ini b/tox.ini index aab8ea678..5322f1012 100644 --- a/tox.ini +++ b/tox.ini @@ -69,7 +69,7 @@ commands = [testenv:ansible] # Need to pass some env vars for the Ansible playbooks -basepython = {env:OPENSTACKSDK_TOX_PYTHON:python2} +basepython = {env:OPENSTACKSDK_TOX_PYTHON:python3} passenv = HOME USER ANSIBLE_VAR_* deps = {[testenv]deps} From 7c10ff6041f2f61309b0843062b841b90e2cc8d3 Mon Sep 17 00:00:00 2001 From: Vishakha Agarwal Date: Fri, 5 Jul 2019 16:40:04 +0530 Subject: [PATCH 2510/3836] Add application credential CRUD support This patch adds the client support of application credentials. Change-Id: Idac1b8f4610825ca7fdf62eec4f0453398e43346 --- openstack/identity/v3/_proxy.py | 107 ++++++++++++++++++ .../identity/v3/application_credential.py | 49 ++++++++ .../tests/functional/identity/__init__.py | 0 .../tests/functional/identity/v3/__init__.py | 0 .../v3/test_application_credential.py | 68 +++++++++++ .../v3/test_application_credential.py | 55 +++++++++ ...lication-credentials-abab9106dea10c11.yaml | 5 + 7 files changed, 284 insertions(+) create mode 100644 openstack/identity/v3/application_credential.py create mode 100644 openstack/tests/functional/identity/__init__.py create mode 100644 openstack/tests/functional/identity/v3/__init__.py create mode 100644 openstack/tests/functional/identity/v3/test_application_credential.py create mode 100644 openstack/tests/unit/identity/v3/test_application_credential.py create mode 100644 releasenotes/notes/add-application-credentials-abab9106dea10c11.yaml diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 39286cbbd..f0f20038f 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -11,6 +11,8 @@ # under the License. import openstack.exceptions as exception +from openstack.identity.v3 import application_credential as \ + _application_credential from openstack.identity.v3 import credential as _credential from openstack.identity.v3 import domain as _domain from openstack.identity.v3 import endpoint as _endpoint @@ -1212,3 +1214,108 @@ def validate_group_has_role(self, project, group, role): group = self._get_resource(_group.Group, group) role = self._get_resource(_role.Role, role) return project.validate_group_has_role(self, group, role) + + def application_credentials(self, user, **query): + """Retrieve a generator of application credentials + + :param user: Either the ID of a user or a + :class:`~openstack.identity.v3.user.User` instance. + + :param kwargs query: Optional query parameters to be sent to + application credential the resources being returned. + + :returns: A generator of application credentials instances. + :rtype: :class:`~openstack.identity.v3.application_credential. + ApplicationCredential` + """ + user = self._get_resource(_user.User, user) + return self._list(_application_credential.ApplicationCredential, + user_id=user.id, **query) + + def get_application_credential(self, user, application_credential): + """Get a single application credential + + :param user: Either the ID of a user or a + :class:`~openstack.identity.v3.user.User` instance. + + :param application_credential: The value can be the ID of a + application credential or a :class: + `~openstack.identity.v3.application_credential. + ApplicationCredential` instance. + + :returns: One :class:`~openstack.identity.v3.application_credential. + ApplicationCredential` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource can be found. + """ + user = self._get_resource(_user.User, user) + return self._get(_application_credential.ApplicationCredential, + application_credential, + user_id=user.id) + + def create_application_credential(self, user, name, **attrs): + """Create a new application credential from attributes + + :param user: Either the ID of a user or a + :class:`~openstack.identity.v3.user.User` instance. + :param name: The name of the application credential which is + unique to the user. + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.identity.v3.application_credential. + ApplicationCredential`, comprised of the properties on the + ApplicationCredential class. + + + :returns: The results of application credential creation. + :rtype: :class:`~openstack.identity.v3.application_credential. + ApplicationCredential` + """ + + user = self._get_resource(_user.User, user) + return self._create(_application_credential.ApplicationCredential, + name=name, + user_id=user.id, **attrs) + + def find_application_credential(self, user, name_or_id, + ignore_missing=True, **args): + """Find a single application credential + + :param user: Either the ID of a user or a + :class:`~openstack.identity.v3.user.User` instance. + :param name_or_id: The name or ID of a application credential. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + + :returns: One :class:`~openstack.identity.v3.application_credential. + ApplicationCredential` or None + """ + user = self._get_resource(_user.User, user) + return self._find(_application_credential.ApplicationCredential, + user_id=user.id, name_or_id=name_or_id, + ignore_missing=ignore_missing, **args) + + def delete_application_credential(self, user, application_credential, + ignore_missing=True): + """Delete a application credential + + :param user: Either the ID of a user or a + :class:`~openstack.identity.v3.user.User` instance. + :param application credential: The value can be either the ID of a + application credential or a :class: `~openstack.identity.v3. + application_credential.ApplicationCredential` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the application credential does not exist. When set to + ``True``, no exception will be thrown when attempting to delete + a nonexistent application credential. + + :returns: ``None`` + """ + user = self._get_resource(_user.User, user) + self._delete(_application_credential.ApplicationCredential, + application_credential, + user_id=user.id, + ignore_missing=ignore_missing) diff --git a/openstack/identity/v3/application_credential.py b/openstack/identity/v3/application_credential.py new file mode 100644 index 000000000..a3876502f --- /dev/null +++ b/openstack/identity/v3/application_credential.py @@ -0,0 +1,49 @@ +# 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 resource + + +class ApplicationCredential(resource.Resource): + resource_key = 'application_credential' + resources_key = 'application_credentials' + base_path = '/users/%(user_id)s/application_credentials' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + # Properties + #: User ID using application credential. *Type: string* + user_id = resource.URI('user_id') + #: User object using application credential. *Type: string* + user = resource.Body('user') + #: The links for the application credential resource. + links = resource.Body('links') + #: name of the user. *Type: string* + name = resource.Body('name') + #: secret that application credential will be created with, if any. + # *Type: string* + secret = resource.Body('secret') + #: description of application credential's purpose. *Type: string* + description = resource.Body('description') + #: expire time of application credential. *Type: string* + expires_at = resource.Body('expires_at') + #: roles of the user. *Type: list* + roles = resource.Body('roles') + #: restricts the application credential. *Type: boolean* + unrestricted = resource.Body('unrestricted', type=bool) + #: ID of project. *Type: string* + project_id = resource.Body('project_id') diff --git a/openstack/tests/functional/identity/__init__.py b/openstack/tests/functional/identity/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/functional/identity/v3/__init__.py b/openstack/tests/functional/identity/v3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/functional/identity/v3/test_application_credential.py b/openstack/tests/functional/identity/v3/test_application_credential.py new file mode 100644 index 000000000..21933a8b0 --- /dev/null +++ b/openstack/tests/functional/identity/v3/test_application_credential.py @@ -0,0 +1,68 @@ +# 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.tests.functional import base +from openstack import exceptions + + +class TestApplicationCredentials(base.BaseFunctionalTest): + + def setUp(self): + super(TestApplicationCredentials, self).setUp() + self.user_id = self.operator_cloud.current_user_id + + def _create_application_credentials(self): + app_creds = self.conn.identity.create_application_credential( + user=self.user_id, name='app_cred' + ) + self.addCleanup(self.conn.identity.delete_application_credential, + self.user_id, app_creds['id']) + return app_creds + + def test_create_application_credentials(self): + app_creds = self._create_application_credentials() + self.assertEqual(app_creds['user_id'], self.user_id) + + def test_get_application_credential(self): + app_creds = self._create_application_credentials() + app_cred = self.conn.identity.get_application_credential( + user=self.user_id, application_credential=app_creds['id'] + ) + self.assertEqual(app_cred['id'], app_creds['id']) + self.assertEqual(app_cred['user_id'], self.user_id) + + def test_application_credentials(self): + self._create_application_credentials() + app_creds = self.conn.identity.application_credentials( + user=self.user_id + ) + for app_cred in app_creds: + self.assertEqual(app_cred['user_id'], self.user_id) + + def test_find_application_credential(self): + app_creds = self._create_application_credentials() + app_cred = self.conn.identity.find_application_credential( + user=self.user_id, name_or_id=app_creds['id'] + ) + self.assertEqual(app_cred['id'], app_creds['id']) + self.assertEqual(app_cred['user_id'], self.user_id) + + def test_delete_application_credential(self): + app_creds = self._create_application_credentials() + self.conn.identity.delete_application_credential( + user=self.user_id, application_credential=app_creds['id'] + ) + self.assertRaises(exceptions.NotFoundException, + self.conn.identity.get_application_credential, + user=self.user_id, + application_credential=app_creds['id'] + ) diff --git a/openstack/tests/unit/identity/v3/test_application_credential.py b/openstack/tests/unit/identity/v3/test_application_credential.py new file mode 100644 index 000000000..e6cf3101d --- /dev/null +++ b/openstack/tests/unit/identity/v3/test_application_credential.py @@ -0,0 +1,55 @@ +# 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.tests.unit import base + +from openstack.identity.v3 import application_credential + +EXAMPLE = { + "user": { + "id": "8ac43bb0926245cead88676a96c750d3"}, + "name": 'monitoring', + "secret": 'rEaqvJka48mpv', + "roles": [ + {"name": "Reader"} + ], + "expires_at": '2018-02-27T18:30:59Z', + "description": "Application credential for monitoring", + "unrestricted": "False", + "project_id": "3", + "links": {"self": "http://example.com/v3/application_credential_1"} +} + + +class TestApplicationCredential(base.TestCase): + + def test_basic(self): + sot = application_credential.ApplicationCredential() + self.assertEqual('application_credential', sot.resource_key) + self.assertEqual('application_credentials', sot.resources_key) + self.assertEqual('/users/%(user_id)s/application_credentials', + sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = application_credential.ApplicationCredential(**EXAMPLE) + self.assertEqual(EXAMPLE['user'], sot.user) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['secret'], sot.secret) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['expires_at'], sot.expires_at) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + self.assertEqual(EXAMPLE['roles'], sot.roles) + self.assertEqual(EXAMPLE['links'], sot.links) diff --git a/releasenotes/notes/add-application-credentials-abab9106dea10c11.yaml b/releasenotes/notes/add-application-credentials-abab9106dea10c11.yaml new file mode 100644 index 000000000..9f4818303 --- /dev/null +++ b/releasenotes/notes/add-application-credentials-abab9106dea10c11.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added CRUD support for `application credentials + `_. From e81fe82fce18f2600bf865d1c4961596d90e25ad Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 5 Aug 2019 11:54:18 -0400 Subject: [PATCH 2511/3836] Fix bm tests: sort lists being compared These tests were randomly failing because they are not being sorted before being compared. Change-Id: Ib4e8820494deee0c11fe785abf420196989f389b --- .../tests/functional/baremetal/test_baremetal_node.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index 2c727d977..92a93dc8f 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -323,9 +323,11 @@ def test_add_remove_node_trait(self): self.assertEqual(['CUSTOM_FAKE'], node.traits) self.conn.baremetal.add_node_trait(self.node, 'CUSTOM_REAL') - self.assertEqual(['CUSTOM_FAKE', 'CUSTOM_REAL'], self.node.traits) + self.assertEqual(sorted(['CUSTOM_FAKE', 'CUSTOM_REAL']), + sorted(self.node.traits)) node = self.conn.baremetal.get_node(self.node) - self.assertEqual(['CUSTOM_FAKE', 'CUSTOM_REAL'], node.traits) + self.assertEqual(sorted(['CUSTOM_FAKE', 'CUSTOM_REAL']), + sorted(node.traits)) self.conn.baremetal.remove_node_trait(node, 'CUSTOM_FAKE', ignore_missing=False) @@ -341,9 +343,9 @@ def test_set_node_traits(self): traits2 = ['CUSTOM_FOOBAR'] self.conn.baremetal.set_node_traits(self.node, traits1) - self.assertEqual(['CUSTOM_FAKE', 'CUSTOM_REAL'], self.node.traits) + self.assertEqual(sorted(traits1), sorted(self.node.traits)) node = self.conn.baremetal.get_node(self.node) - self.assertEqual(['CUSTOM_FAKE', 'CUSTOM_REAL'], node.traits) + self.assertEqual(sorted(traits1), sorted(node.traits)) self.conn.baremetal.set_node_traits(self.node, traits2) self.assertEqual(['CUSTOM_FOOBAR'], self.node.traits) From 6cfd642591f65642e704ecc4e4a0b385292cc8ce Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Mon, 5 Aug 2019 13:58:27 -0500 Subject: [PATCH 2512/3836] Allow limiting Connection service_types from oslo.config Add a service_types kwarg to cloud_region.from_conf and Connection.__init__, accepting a list/set of service types. All other service types will be explicitly disabled, and we won't attempt to load their configs. Change-Id: I3d16d17caa2e8a58b7064c54e930468288aa6ff1 --- openstack/config/cloud_region.py | 17 +++- openstack/connection.py | 8 +- openstack/tests/unit/base.py | 31 ++++++- openstack/tests/unit/config/test_from_conf.py | 84 +++++++++---------- openstack/tests/unit/test_connection.py | 26 ++++++ 5 files changed, 119 insertions(+), 47 deletions(-) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index eaf7703e9..6844c5fe4 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -115,7 +115,7 @@ def from_session(session, name=None, region_name=None, app_name=app_name, app_version=app_version) -def from_conf(conf, session=None, **kwargs): +def from_conf(conf, session=None, service_types=None, **kwargs): """Create a CloudRegion from oslo.config ConfigOpts. :param oslo_config.cfg.ConfigOpts conf: @@ -126,6 +126,16 @@ def from_conf(conf, session=None, **kwargs): :param keystoneauth1.session.Session session: An existing authenticated Session to use. This is currently required. TODO: Load this (and auth) from the conf. + :param service_types: + A list/set of service types for which to look for and process config + opts. If None, all known service types are processed. Note that we will + not error if a supplied service type can not be processed successfully + (unless you try to use the proxy, of course). This tolerates uses where + the consuming code has paths for a given service, but those paths are + not exercised for given end user setups, and we do not want to generate + errors for e.g. missing/invalid conf sections in those cases. We also + don't check to make sure your service types are spelled correctly - + caveat implementor. :param kwargs: Additional keyword arguments to be passed directly to the CloudRegion constructor. @@ -140,6 +150,11 @@ def from_conf(conf, session=None, **kwargs): config_dict = kwargs.pop('config', config_defaults.get_defaults()) stm = os_service_types.ServiceTypes() for st in stm.all_types_by_service_type: + if service_types is not None and st not in service_types: + _disable_service( + config_dict, st, + reason="Not in the list of requested service_types.") + continue project_name = stm.get_project_name(st) if project_name not in conf: if '-' in project_name: diff --git a/openstack/connection.py b/openstack/connection.py index c79d84587..9d3f6bd83 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -271,6 +271,7 @@ def __init__(self, cloud=None, config=None, session=None, task_manager=None, rate_limit=None, oslo_conf=None, + service_types=None, **kwargs): """Create a connection to a cloud. @@ -322,6 +323,11 @@ def __init__(self, cloud=None, config=None, session=None, An oslo.config ``CONF`` object that has been populated with ``keystoneauth1.loading.register_adapter_conf_options`` in groups named by the OpenStack service's project name. + :param service_types: + A list/set of service types this Connection should support. All + other service types will be disabled (will error if used). + **Currently only supported in conjunction with the ``oslo_conf`` + kwarg.** :param kwargs: If a config is not provided, the rest of the parameters provided are assumed to be arguments to be passed to the CloudRegion constructor. @@ -336,7 +342,7 @@ def __init__(self, cloud=None, config=None, session=None, if oslo_conf: self.config = cloud_region.from_conf( oslo_conf, session=session, app_name=app_name, - app_version=app_version) + app_version=app_version, service_types=service_types) elif session: self.config = cloud_region.from_session( session=session, diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 7d508ecd6..be4edeef6 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -14,12 +14,14 @@ # under the License. import collections +import os import time import uuid import fixtures -import os +from keystoneauth1 import loading as ks_loading import openstack.config as occ +from oslo_config import cfg from requests import structures from requests_mock.contrib import fixture as rm_fixture from six.moves import urllib @@ -118,6 +120,23 @@ def _nosleep(seconds): vendor_files=[vendor.name], secure_files=['non-existant']) + self.oslo_config_dict = { + # All defaults for nova + 'nova': {}, + # monasca-api not in the service catalog + 'monasca-api': {}, + # Overrides for heat + 'heat': { + 'region_name': 'SpecialRegion', + 'interface': 'internal', + 'endpoint_override': 'https://example.org:8888/heat/v2' + }, + # test a service with dashes + 'ironic_inspector': { + 'endpoint_override': 'https://example.org:5050', + }, + } + # FIXME(notmorgan): Convert the uri_registry, discovery.json, and # use of keystone_v3/v2 to a proper fixtures.Fixture. For now this # is acceptable, but eventually this should become it's own fixture @@ -139,6 +158,16 @@ def _nosleep(seconds): self.use_keystone_v3() self.__register_uris_called = False + def _load_ks_cfg_opts(self): + conf = cfg.ConfigOpts() + for group, opts in self.oslo_config_dict.items(): + conf.register_group(cfg.OptGroup(group)) + if opts is not None: + ks_loading.register_adapter_conf_options(conf, group) + for name, val in opts.items(): + conf.set_override(name, val, group=group) + return conf + # TODO(shade) Update this to handle service type aliases def get_mock_url(self, service_type, interface='public', resource=None, append=None, base_url_append=None, diff --git a/openstack/tests/unit/config/test_from_conf.py b/openstack/tests/unit/config/test_from_conf.py index 40e31f200..49b8e4a68 100644 --- a/openstack/tests/unit/config/test_from_conf.py +++ b/openstack/tests/unit/config/test_from_conf.py @@ -13,8 +13,6 @@ import uuid from keystoneauth1 import exceptions as ks_exc -from keystoneauth1 import loading as ks_loading -from oslo_config import cfg from openstack.config import cloud_region from openstack import connection @@ -25,40 +23,12 @@ class TestFromConf(base.TestCase): - def setUp(self): - super(TestFromConf, self).setUp() - self.oslo_config_dict = { - # All defaults for nova - 'nova': {}, - # monasca-api not in the service catalog - 'monasca-api': {}, - # Overrides for heat - 'heat': { - 'region_name': 'SpecialRegion', - 'interface': 'internal', - 'endpoint_override': 'https://example.org:8888/heat/v2' - }, - # test a service with dashes - 'ironic_inspector': { - 'endpoint_override': 'https://example.org:5050', - }, - } - - def _load_ks_cfg_opts(self): - conf = cfg.ConfigOpts() - for group, opts in self.oslo_config_dict.items(): - conf.register_group(cfg.OptGroup(group)) - if opts is not None: - ks_loading.register_adapter_conf_options(conf, group) - for name, val in opts.items(): - conf.set_override(name, val, group=group) - return conf - - def _get_conn(self): + def _get_conn(self, **from_conf_kwargs): oslocfg = self._load_ks_cfg_opts() # Throw name in here to prove **kwargs is working config = cloud_region.from_conf( - oslocfg, session=self.cloud.session, name='from_conf.example.com') + oslocfg, session=self.cloud.session, name='from_conf.example.com', + **from_conf_kwargs) self.assertEqual('from_conf.example.com', config.name) return connection.Connection(config=config) @@ -161,39 +131,58 @@ def test_name_with_dashes(self): self.assertTrue(adap.get_introspection('abcd').is_finished) - def _test_missing_invalid_permutations(self, expected_reason): - # Do special things to self.oslo_config_dict['heat'] before calling - # this method. - conn = self._get_conn() - - adap = conn.orchestration + def assert_service_disabled(self, service_type, expected_reason, + **from_conf_kwargs): + conn = self._get_conn(**from_conf_kwargs) + # The _ServiceDisabledProxyShim loads up okay... + adap = getattr(conn, service_type) + # ...but freaks out if you try to use it. ex = self.assertRaises( exceptions.ServiceDisabledException, getattr, adap, 'get') - self.assertIn("Service 'orchestration' is disabled because its " - "configuration could not be loaded.", ex.message) + self.assertIn("Service '%s' is disabled because its configuration " + "could not be loaded." % service_type, ex.message) self.assertIn(expected_reason, ex.message) def test_no_such_conf_section(self): """No conf section (therefore no adapter opts) for service type.""" del self.oslo_config_dict['heat'] - self._test_missing_invalid_permutations( + self.assert_service_disabled( + 'orchestration', "No section for project 'heat' (service type 'orchestration') was " "present in the config.") + def test_no_such_conf_section_ignore_service_type(self): + """Ignore absent conf section if service type not requested.""" + del self.oslo_config_dict['heat'] + self.assert_service_disabled( + 'orchestration', "Not in the list of requested service_types.", + # 'orchestration' absent from this list + service_types=['compute']) + def test_no_adapter_opts(self): """Conf section present, but opts for service type not registered.""" self.oslo_config_dict['heat'] = None - self._test_missing_invalid_permutations( + self.assert_service_disabled( + 'orchestration', "Encountered an exception attempting to process config for " "project 'heat' (service type 'orchestration'): no such option") + def test_no_adapter_opts_ignore_service_type(self): + """Ignore unregistered conf section if service type not requested.""" + self.oslo_config_dict['heat'] = None + self.assert_service_disabled( + 'orchestration', "Not in the list of requested service_types.", + # 'orchestration' absent from this list + service_types=['compute']) + def test_invalid_adapter_opts(self): """Adapter opts are bogus, in exception-raising ways.""" self.oslo_config_dict['heat'] = { 'interface': 'public', 'valid_interfaces': 'private', } - self._test_missing_invalid_permutations( + self.assert_service_disabled( + 'orchestration', "Encountered an exception attempting to process config for " "project 'heat' (service type 'orchestration'): interface and " "valid_interfaces are mutually exclusive.") @@ -209,3 +198,10 @@ def test_no_endpoint(self): # Monasca is not in the service catalog self.assertRaises(ks_exc.catalog.EndpointNotFound, getattr, conn, 'monitoring') + + def test_no_endpoint_ignore_service_type(self): + """Bogus service type disabled if not in requested service_types.""" + self.assert_service_disabled( + 'monitoring', "Not in the list of requested service_types.", + # 'monitoring' absent from this list + service_types={'compute', 'orchestration', 'bogus'}) diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index 69326d1d1..0b287d525 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -18,6 +18,7 @@ from openstack import connection import openstack.config +from openstack import service_description from openstack.tests.unit import base from openstack.tests.unit.fake import fake_service @@ -240,6 +241,31 @@ def test_from_config_insecure(self): self.assertFalse(sot.session.verify) +class TestOsloConfig(TestConnection): + def test_from_conf(self): + c1 = connection.Connection(cloud='sample-cloud') + conn = connection.Connection( + session=c1.session, oslo_conf=self._load_ks_cfg_opts()) + # There was no config for keystone + self.assertIsInstance( + conn.identity, service_description._ServiceDisabledProxyShim) + # But nova was in there + self.assertEqual('openstack.compute.v2._proxy', + conn.compute.__class__.__module__) + + def test_from_conf_filter_service_types(self): + c1 = connection.Connection(cloud='sample-cloud') + conn = connection.Connection( + session=c1.session, oslo_conf=self._load_ks_cfg_opts(), + service_types={'orchestration', 'i-am-ignored'}) + # There was no config for keystone + self.assertIsInstance( + conn.identity, service_description._ServiceDisabledProxyShim) + # Nova was in there, but disabled because not requested + self.assertIsInstance( + conn.compute, service_description._ServiceDisabledProxyShim) + + class TestNetworkConnection(base.TestCase): # Verify that if the catalog has the suffix we don't mess things up. From 31bab62b398d0aee225569606180ed477157679b Mon Sep 17 00:00:00 2001 From: Christian Berendt Date: Tue, 6 Aug 2019 01:53:21 +0200 Subject: [PATCH 2513/3836] Update betacloud vendor entry * new auth_url * new location Change-Id: If068f0fa8fa2cd545c4f245adc83ad567f9d16ed --- doc/source/user/config/vendor-support.rst | 4 ++-- openstack/config/vendors/betacloud.json | 2 +- .../notes/vendor-update-betacloud-37dac22d8d91a3c5.yaml | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/vendor-update-betacloud-37dac22d8d91a3c5.yaml diff --git a/doc/source/user/config/vendor-support.rst b/doc/source/user/config/vendor-support.rst index 87832c7c0..4be56fd08 100644 --- a/doc/source/user/config/vendor-support.rst +++ b/doc/source/user/config/vendor-support.rst @@ -43,12 +43,12 @@ van1 Vancouver, BC Betacloud --------- -https://api-1.betacloud.io:5000 +https://api-1.betacloud.de:5000 ============== ================== Region Name Location ============== ================== -betacloud-1 Nuremberg, Germany +betacloud-1 Karlsruhe, Germany ============== ================== * Identity API Version is 3 diff --git a/openstack/config/vendors/betacloud.json b/openstack/config/vendors/betacloud.json index 348d7a9e1..87aeeeda5 100644 --- a/openstack/config/vendors/betacloud.json +++ b/openstack/config/vendors/betacloud.json @@ -2,7 +2,7 @@ "name": "betacloud", "profile": { "auth": { - "auth_url": "https://api-1.betacloud.io:5000" + "auth_url": "https://api-1.betacloud.de:5000" }, "regions": [ "betacloud-1" diff --git a/releasenotes/notes/vendor-update-betacloud-37dac22d8d91a3c5.yaml b/releasenotes/notes/vendor-update-betacloud-37dac22d8d91a3c5.yaml new file mode 100644 index 000000000..f2e249d19 --- /dev/null +++ b/releasenotes/notes/vendor-update-betacloud-37dac22d8d91a3c5.yaml @@ -0,0 +1,3 @@ +--- +other: + - Update betacloud region for Germany From 28d31345833b2af163fa6391258e72dd2b0df626 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 7 Aug 2019 09:16:12 -0400 Subject: [PATCH 2514/3836] Initalize pool_executor so close works The close method, called by the context manager, blows up if the Connection hasn't been used to upload any large swift objects, because the pool executor object hasn't been created. Initialize the variable so that the context manager works again. Change-Id: I2647b211a02e4fbc8f4850c420249398eaee9e74 --- openstack/connection.py | 1 + openstack/tests/unit/cloud/test_shade.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/openstack/connection.py b/openstack/connection.py index c79d84587..f8f1d256d 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -356,6 +356,7 @@ def __init__(self, cloud=None, config=None, session=None, self._session = None self._proxies = {} + self.__pool_executor = None self.use_direct_get = use_direct_get self.strict_mode = strict # Call the _*CloudMixin constructors while we work on diff --git a/openstack/tests/unit/cloud/test_shade.py b/openstack/tests/unit/cloud/test_shade.py index d96b612b1..cbf640487 100644 --- a/openstack/tests/unit/cloud/test_shade.py +++ b/openstack/tests/unit/cloud/test_shade.py @@ -62,6 +62,15 @@ def test_connect_as(self): # keystoneauth1.loading.base.BaseLoader.load_from_options self.cloud.connect_as(project_name='test_project') + def test_connect_as_context(self): + # Do initial auth/catalog steps + # TODO(mordred) This only tests the constructor steps. Discovery + # cache sharing is broken. We need to get discovery_cache option + # plumbed through + # keystoneauth1.loading.base.BaseLoader.load_from_options + with self.cloud.connect_as(project_name='test_project'): + pass + @mock.patch.object(connection.Connection, 'search_images') def test_get_images(self, mock_search): image1 = dict(id='123', name='mickey') From 1a4b61d51dfcf7666a32d1323c9e70e1706fa98e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 8 Aug 2019 07:24:35 -0400 Subject: [PATCH 2515/3836] Fix dns return values The dns layer started returning False instead of None when things aren't found. Fix it. Change-Id: I38fd8c38d6f0ff08effdc39afbdfa90fb87adf83 --- openstack/cloud/_dns.py | 6 +++--- releasenotes/notes/fix-dns-return-c810d5e6736322f1.yaml | 7 +++++++ 2 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/fix-dns-return-c810d5e6736322f1.yaml diff --git a/openstack/cloud/_dns.py b/openstack/cloud/_dns.py index 52449e770..b663b06c7 100644 --- a/openstack/cloud/_dns.py +++ b/openstack/cloud/_dns.py @@ -50,7 +50,7 @@ def get_zone(self, name_or_id, filters=None): zone = self.dns.find_zone( name_or_id=name_or_id, ignore_missing=True, **filters) if not zone: - return False + return None return zone def search_zones(self, name_or_id=None, filters=None): @@ -172,7 +172,7 @@ def get_recordset(self, zone, name_or_id): of the zone managing the recordset. :param name_or_id: Name or ID of the recordset - :returns: A recordset dict or False if no matching recordset is + :returns: A recordset dict or None if no matching recordset is found. """ @@ -187,7 +187,7 @@ def get_recordset(self, zone, name_or_id): return self.dns.find_recordset( zone=zone_obj, name_or_id=name_or_id, ignore_missing=False) except Exception: - return False + return None def search_recordsets(self, zone, name_or_id=None, filters=None): recordsets = self.list_recordsets(zone=zone) diff --git a/releasenotes/notes/fix-dns-return-c810d5e6736322f1.yaml b/releasenotes/notes/fix-dns-return-c810d5e6736322f1.yaml new file mode 100644 index 000000000..78b95dedd --- /dev/null +++ b/releasenotes/notes/fix-dns-return-c810d5e6736322f1.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixed issue where the dns methods were returning False instead of None + when resources were not found. + - | + Fixed jsonification under python3. From a7423f720fd4319e8abf0bf0abaaae8f9972142c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 7 Aug 2019 11:08:53 -0400 Subject: [PATCH 2516/3836] Skip most service_description for unknown services When we don't know anything about a service, we can know early in the process that we're never going to match a supported version. That means we can just directly and without malice or warnings return a REST adapter and be done with it. Change-Id: Ifd4f2fce59c76148807fb7390f68eaae179694c4 --- openstack/service_description.py | 20 ++++++++++++++++---- openstack/tests/unit/test_connection.py | 20 ++++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/openstack/service_description.py b/openstack/service_description.py index ff27217e7..965181a36 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -100,7 +100,19 @@ def _make_proxy(self, instance): self.service_type, config.get_disabled_reason(self.service_type)) - # First, check to see if we've got config that matches what we + # We don't know anything about this service, so the user is + # explicitly just using us for a passthrough REST adapter. + # Skip all the lower logic. + if not self.supported_versions: + temp_client = config.get_session_client( + self.service_type, + allow_version_hack=True, + ) + # trigger EndpointNotFound exception if this is bogus + temp_client.get_endpoint() + return temp_client + + # Check to see if we've got config that matches what we # understand in the SDK. version_string = config.get_api_version(self.service_type) endpoint_override = config.get_endpoint(self.service_type) @@ -111,7 +123,7 @@ def _make_proxy(self, instance): version_string = list(self.supported_versions)[0] proxy_obj = None - if endpoint_override and version_string and self.supported_versions: + if endpoint_override and version_string: # Both endpoint override and version_string are set, we don't # need to do discovery - just trust the user. proxy_class = self.supported_versions.get(version_string[0]) @@ -129,7 +141,7 @@ def _make_proxy(self, instance): version=version_string, service_type=self.service_type), category=exceptions.UnsupportedServiceVersion) - elif endpoint_override and self.supported_versions: + elif endpoint_override: temp_adapter = config.get_session_client( self.service_type ) @@ -181,7 +193,7 @@ def _make_proxy(self, instance): version_kwargs = {} if version_string: version_kwargs['version'] = version_string - elif self.supported_versions: + else: supported_versions = sorted([ int(f) for f in self.supported_versions]) version_kwargs['min_version'] = str(supported_versions[0]) diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index 69326d1d1..4aaacc1a1 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -15,8 +15,10 @@ import fixtures from keystoneauth1 import session import mock +from testtools import matchers from openstack import connection +from openstack import proxy import openstack.config from openstack.tests.unit import base from openstack.tests.unit.fake import fake_service @@ -145,6 +147,24 @@ def test_create_session(self): # self.assertEqual('openstack.workflow.v2._proxy', # conn.workflow.__class__.__module__) + def test_create_unknown_proxy(self): + self.register_uris([ + self.get_placement_discovery_mock_dict(), + ]) + + def closure(): + return self.cloud.placement + + self.assertThat( + closure, + matchers.Warnings(matchers.HasLength(0))) + + self.assertIsInstance( + self.cloud.placement, + proxy.Proxy) + + self.assert_calls() + def test_create_connection_version_param_default(self): c1 = connection.Connection(cloud='sample-cloud') conn = connection.Connection(session=c1.session) From 09cdcc002055c28a7ff6e2bb9fecc68af8fd52c1 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 9 Aug 2019 15:26:52 -0400 Subject: [PATCH 2517/3836] bug: avoid unnecessary object meta prefix Some hard coded values for object headers are already including the required 'x-object-meta-' prefix. In some cases, like the header for _OBJECT_AUTOCREATE_KEY, we were unnecessarily adding the prefix to the header name. Change-Id: I3940a8e93d0fc69244794a81b9d2c81d53b54986 --- openstack/cloud/_object_store.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index 2b6139fe1..d269d06f8 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -420,7 +420,10 @@ def create_object( if sha256: headers[self._OBJECT_SHA256_KEY] = sha256 or '' for (k, v) in metadata.items(): - headers['x-object-meta-' + k] = v + if not k.lower().startswith('x-object-meta-'): + headers['x-object-meta-' + k] = v + else: + headers[k] = v endpoint = self._get_object_endpoint(container, name) From 5f5353a791d65b81c468fb858f5857d1a203ea13 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 7 Aug 2019 11:07:31 -0400 Subject: [PATCH 2518/3836] Retry large object manifest upload Failure to upload the final manifest can leave the already uploaded segments lying around, unused and unloved. Remove those. Change-Id: I1756cfb3038c2312afedbc188161dd8d8459e4a7 --- openstack/cloud/_object_store.py | 59 ++++- openstack/tests/unit/cloud/test_object.py | 255 ++++++++++++++++++++++ 2 files changed, 303 insertions(+), 11 deletions(-) diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index d269d06f8..5e85bbd68 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -554,24 +554,55 @@ def _upload_large_object( self._add_etag_to_manifest(segment_results, manifest) - if use_slo: - return self._finish_large_object_slo(endpoint, headers, manifest) - else: - return self._finish_large_object_dlo(endpoint, headers) + # If the final manifest upload fails, remove the segments we've + # already uploaded. + try: + if use_slo: + return self._finish_large_object_slo(endpoint, headers, + manifest) + else: + return self._finish_large_object_dlo(endpoint, headers) + except Exception: + try: + segment_prefix = endpoint.split('/')[-1] + self.log.debug( + "Failed to upload large object manifest for %s. " + "Removing segment uploads.", segment_prefix) + self.delete_autocreated_image_objects( + segment_prefix=segment_prefix) + except Exception: + self.log.exception( + "Failed to cleanup image objects for %s:", + segment_prefix) + raise def _finish_large_object_slo(self, endpoint, headers, manifest): # TODO(mordred) send an etag of the manifest, which is the md5sum # of the concatenation of the etags of the results headers = headers.copy() - return self._object_store_client.put( - endpoint, - params={'multipart-manifest': 'put'}, - headers=headers, data=json.dumps(manifest)) + retries = 3 + while True: + try: + return self._object_store_client.put( + endpoint, + params={'multipart-manifest': 'put'}, + headers=headers, data=json.dumps(manifest)) + except Exception: + retries -= 1 + if retries == 0: + raise def _finish_large_object_dlo(self, endpoint, headers): headers = headers.copy() headers['X-Object-Manifest'] = endpoint - return self._object_store_client.put(endpoint, headers=headers) + retries = 3 + while True: + try: + return self._object_store_client.put(endpoint, headers=headers) + except Exception: + retries -= 1 + if retries == 0: + raise def update_object(self, container, name, metadata=None, **headers): """Update the metadata of an object @@ -668,7 +699,8 @@ def delete_object(self, container, name, meta=None): except exc.OpenStackCloudHTTPError: return False - def delete_autocreated_image_objects(self, container=None): + def delete_autocreated_image_objects(self, container=None, + segment_prefix=None): """Delete all objects autocreated for image uploads. This method should generally not be needed, as shade should clean up @@ -676,6 +708,11 @@ def delete_autocreated_image_objects(self, container=None): goes wrong and it is found that there are leaked objects, this method can be used to delete any objects that shade has created on the user's behalf in service of image uploads. + + :param str container: Name of the container. Defaults to 'images'. + :param str segment_prefix: Prefix for the image segment names to + delete. If not given, all image upload segments present are + deleted. """ if container is None: container = self._OBJECT_AUTOCREATE_CONTAINER @@ -684,7 +721,7 @@ def delete_autocreated_image_objects(self, container=None): return False deleted = False - for obj in self.list_objects(container): + for obj in self.list_objects(container, prefix=segment_prefix): meta = self.get_object_metadata(container, obj['name']) if meta.get( self._OBJECT_AUTOCREATE_KEY, meta.get( diff --git a/openstack/tests/unit/cloud/test_object.py b/openstack/tests/unit/cloud/test_object.py index 7753670f1..0d099b13b 100644 --- a/openstack/tests/unit/cloud/test_object.py +++ b/openstack/tests/unit/cloud/test_object.py @@ -841,6 +841,261 @@ def test_create_static_large_object(self): }, ], self.adapter.request_history[-1].json()) + def test_slo_manifest_retry(self): + """ + Uploading the SLO manifest file should be retried up to 3 times before + giving up. This test should succeed on the 3rd and final attempt. + """ + max_file_size = 25 + min_file_size = 1 + + uris_to_mock = [ + dict(method='GET', uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': max_file_size}, + slo={'min_segment_size': min_file_size})), + dict(method='HEAD', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=404) + ] + + uris_to_mock.extend([ + dict(method='PUT', + uri='{endpoint}/{container}/{object}/{index:0>6}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + index=index), + status_code=201, + headers=dict(Etag='etag{index}'.format(index=index))) + for index, offset in enumerate( + range(0, len(self.content), max_file_size)) + ]) + + # manifest file upload calls + uris_to_mock.extend([ + dict(method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=400, + validate=dict( + params={ + 'multipart-manifest', 'put' + }, + headers={ + 'x-object-meta-x-sdk-md5': self.md5, + 'x-object-meta-x-sdk-sha256': self.sha256, + })), + dict(method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=400, + validate=dict( + params={ + 'multipart-manifest', 'put' + }, + headers={ + 'x-object-meta-x-sdk-md5': self.md5, + 'x-object-meta-x-sdk-sha256': self.sha256, + })), + dict(method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=201, + validate=dict( + params={ + 'multipart-manifest', 'put' + }, + headers={ + 'x-object-meta-x-sdk-md5': self.md5, + 'x-object-meta-x-sdk-sha256': self.sha256, + })), + ]) + + self.register_uris(uris_to_mock) + + self.cloud.create_object( + container=self.container, name=self.object, + filename=self.object_file.name, use_slo=True) + + # After call 3, order become indeterminate because of thread pool + self.assert_calls(stop_after=3) + + for key, value in self.calls[-1]['headers'].items(): + self.assertEqual( + value, self.adapter.request_history[-1].headers[key], + 'header mismatch in manifest call') + + base_object = '/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object) + + self.assertEqual([ + { + 'path': "{base_object}/000000".format( + base_object=base_object), + 'size_bytes': 25, + 'etag': 'etag0', + }, + { + 'path': "{base_object}/000001".format( + base_object=base_object), + 'size_bytes': 25, + 'etag': 'etag1', + }, + { + 'path': "{base_object}/000002".format( + base_object=base_object), + 'size_bytes': 25, + 'etag': 'etag2', + }, + { + 'path': "{base_object}/000003".format( + base_object=base_object), + 'size_bytes': len(self.object) - 75, + 'etag': 'etag3', + }, + ], self.adapter.request_history[-1].json()) + + def test_slo_manifest_fail(self): + """ + Uploading the SLO manifest file should be retried up to 3 times before + giving up. This test fails all 3 attempts and should verify that we + delete uploaded segments that begin with the object prefix. + """ + max_file_size = 25 + min_file_size = 1 + + uris_to_mock = [ + dict(method='GET', uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': max_file_size}, + slo={'min_segment_size': min_file_size})), + dict(method='HEAD', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=404) + ] + + uris_to_mock.extend([ + dict(method='PUT', + uri='{endpoint}/{container}/{object}/{index:0>6}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + index=index), + status_code=201, + headers=dict(Etag='etag{index}'.format(index=index))) + for index, offset in enumerate( + range(0, len(self.content), max_file_size)) + ]) + + # manifest file upload calls + uris_to_mock.extend([ + dict(method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=400, + validate=dict( + params={ + 'multipart-manifest', 'put' + }, + headers={ + 'x-object-meta-x-sdk-md5': self.md5, + 'x-object-meta-x-sdk-sha256': self.sha256, + })), + dict(method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=400, + validate=dict( + params={ + 'multipart-manifest', 'put' + }, + headers={ + 'x-object-meta-x-sdk-md5': self.md5, + 'x-object-meta-x-sdk-sha256': self.sha256, + })), + dict(method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object=self.object), + status_code=400, + validate=dict( + params={ + 'multipart-manifest', 'put' + }, + headers={ + 'x-object-meta-x-sdk-md5': self.md5, + 'x-object-meta-x-sdk-sha256': self.sha256, + })), + ]) + + # Cleaning up image upload segments involves calling the + # delete_autocreated_image_objects() API method which will list + # objects (LIST), get the object metadata (HEAD), then delete the + # object (DELETE). + uris_to_mock.extend([ + dict(method='GET', + uri='{endpoint}/images?format=json&prefix={prefix}'.format( + endpoint=self.endpoint, + prefix=self.object), + complete_qs=True, + json=[{ + 'content_type': 'application/octet-stream', + 'bytes': 1437258240, + 'hash': '249219347276c331b87bf1ac2152d9af', + 'last_modified': '2015-02-16T17:50:05.289600', + 'name': self.object + }]), + + dict(method='HEAD', + uri='{endpoint}/images/{object}'.format( + endpoint=self.endpoint, + object=self.object), + headers={ + 'X-Timestamp': '1429036140.50253', + 'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1', + 'Content-Length': '1290170880', + 'Last-Modified': 'Tue, 14 Apr 2015 18:29:01 GMT', + 'x-object-meta-x-sdk-autocreated': 'true', + 'X-Object-Meta-X-Shade-Sha256': 'does not matter', + 'X-Object-Meta-X-Shade-Md5': 'does not matter', + 'Date': 'Thu, 16 Nov 2017 15:24:30 GMT', + 'Accept-Ranges': 'bytes', + 'Content-Type': 'application/octet-stream', + 'Etag': '249219347276c331b87bf1ac2152d9af', + }), + + dict(method='DELETE', + uri='{endpoint}/images/{object}'.format( + endpoint=self.endpoint, object=self.object)) + ]) + + self.register_uris(uris_to_mock) + + # image_api_use_tasks needs to be set to True in order for the API + # method delete_autocreated_image_objects() to do the cleanup. + self.cloud.image_api_use_tasks = True + + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.create_object, + container=self.container, name=self.object, + filename=self.object_file.name, use_slo=True) + + # After call 3, order become indeterminate because of thread pool + self.assert_calls(stop_after=3) + def test_object_segment_retry_failure(self): max_file_size = 25 From ad0a52033ccb031db5a90c127af8aa3e5046bbc6 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Mon, 12 Aug 2019 14:13:13 -0500 Subject: [PATCH 2519/3836] DRY test_connection An earlier change [1] wastefully subclassed TestConnection when all it needed was the setUp(). The subclass was redundantly running all the superclass's tests again. This commit factors out the setUp into a local base class and subclasses that base class from TestConnection and TestOsloConfig, cutting out 14 redundant tests. [1] I3d16d17caa2e8a58b7064c54e930468288aa6ff1 Change-Id: If65d7c23e07d9f232410a562e47f183d003a7dc0 --- openstack/tests/unit/test_connection.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index b3e4c51dd..5b7ae9a2b 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -94,10 +94,10 @@ """.format(auth_url=CONFIG_AUTH_URL) -class TestConnection(base.TestCase): +class _TestConnectionBase(base.TestCase): def setUp(self): - super(TestConnection, self).setUp() + super(_TestConnectionBase, self).setUp() # Create a temporary directory where our test config will live # and insert it into the search path via OS_CLIENT_CONFIG_FILE. config_dir = self.useFixture(fixtures.TempDir()).path @@ -110,6 +110,8 @@ def setUp(self): "OS_CLIENT_CONFIG_FILE", config_path)) self.use_keystone_v2() + +class TestConnection(_TestConnectionBase): def test_other_parameters(self): conn = connection.Connection(cloud='sample-cloud', cert='cert') self.assertEqual(conn.session.cert, 'cert') @@ -261,7 +263,7 @@ def test_from_config_insecure(self): self.assertFalse(sot.session.verify) -class TestOsloConfig(TestConnection): +class TestOsloConfig(_TestConnectionBase): def test_from_conf(self): c1 = connection.Connection(cloud='sample-cloud') conn = connection.Connection( From b30132030bf3fd403ded77d42f78c85711a1f944 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Mon, 12 Aug 2019 15:11:12 -0500 Subject: [PATCH 2520/3836] Minor refactor cleanup of _make_proxy Noticed we were doing a redundant lookup and conditional. Straight refactor for readability, no functional change. Change-Id: Ie5067a331b6f52d3e0480433c02199e2dd923ae6 --- openstack/service_description.py | 38 ++++++++++++++++---------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/openstack/service_description.py b/openstack/service_description.py index 965181a36..643184eff 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -223,26 +223,26 @@ def _make_proxy(self, instance): cloud=instance.name, region_name=region_name)) proxy_class = self.supported_versions.get(str(found_version[0])) - if not proxy_class: - # Maybe openstacksdk is being used for the passthrough - # REST API proxy layer for an unknown service in the - # service catalog that also doesn't have any useful - # version discovery? - warnings.warn( - "Service {service_type} has no discoverable version." - " The resulting Proxy object will only have direct" - " passthrough REST capabilities.".format( - service_type=self.service_type), - category=exceptions.UnsupportedServiceVersion) - return temp_adapter - proxy_class = self.supported_versions.get(str(found_version[0])) if proxy_class: - version_kwargs['constructor'] = proxy_class - return config.get_session_client( - self.service_type, - allow_version_hack=True, - **version_kwargs - ) + return config.get_session_client( + self.service_type, + allow_version_hack=True, + constructor=proxy_class, + **version_kwargs + ) + + # No proxy_class + # Maybe openstacksdk is being used for the passthrough + # REST API proxy layer for an unknown service in the + # service catalog that also doesn't have any useful + # version discovery? + warnings.warn( + "Service {service_type} has no discoverable version." + " The resulting Proxy object will only have direct" + " passthrough REST capabilities.".format( + service_type=self.service_type), + category=exceptions.UnsupportedServiceVersion) + return temp_adapter def __set__(self, instance, value): raise AttributeError('Service Descriptors cannot be set') From 57b634b0e0bcae2f5a937533ef4de1724623725c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 7 Aug 2019 09:32:21 -0400 Subject: [PATCH 2521/3836] Fix discovery cache sharing The intent is that connect_as shares the discovery cache so that users don't pay the double cost. Unfortunately, that's not actually what we were doing. There was also a mistaken thought that we should be sharing a Session, but we can't do that, because the Session has the Auth, and what we're doing is making a new Auth. Attach the discovery_cache to the new Session directly and all is good. Change-Id: I07a362ea710b01d3588b06e9d3e0f1ce7eb101d4 --- openstack/cloud/openstackcloud.py | 19 +++-------- openstack/tests/unit/base.py | 38 +++++++++++++-------- openstack/tests/unit/cloud/test_shade.py | 43 ++++++++++++++++++------ 3 files changed, 60 insertions(+), 40 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index f48ce13e5..62e4bcc16 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -226,21 +226,10 @@ def pop_keys(params, auth, name_key, id_key): for key, value in kwargs.items(): params['auth'][key] = value - # TODO(mordred) Replace this chunk with the next patch that allows - # passing a Session to CloudRegion. - # Closure to pass to OpenStackConfig to ensure the new cloud shares - # the Session with the current cloud. This will ensure that version - # discovery cache will be re-used. - def session_constructor(*args, **kwargs): - # We need to pass our current keystone session to the Session - # Constructor, otherwise the new auth plugin doesn't get used. - return keystoneauth1.session.Session( - session=self.session, - discovery_cache=self.config._discovery_cache) - - cloud_config = config.get_one( - session_constructor=session_constructor, - **params) + cloud_config = config.get_one(**params) + # Attach the discovery cache from the old session so we won't + # double discover. + cloud_config._discovery_cache = self.session._discovery_cache # Override the cloud name so that logging/location work right cloud_config._name = self.name cloud_config.config['profile'] = self.name diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index be4edeef6..3ee304c5b 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -420,24 +420,34 @@ def use_nothing(self): self.calls = [] self._uri_registry.clear() + def get_keystone_v3_token(self, catalog='catalog-v3.json'): + catalog_file = os.path.join(self.fixtures_directory, catalog) + with open(catalog_file, 'r') as tokens_file: + return dict( + method='POST', + uri='https://identity.example.com/v3/auth/tokens', + headers={ + 'X-Subject-Token': self.getUniqueString('KeystoneToken') + }, + text=tokens_file.read() + ) + + def get_keystone_v3_discovery(self): + with open(self.discovery_json, 'r') as discovery_file: + return dict( + method='GET', + uri='https://identity.example.com/', + text=discovery_file.read(), + ) + def use_keystone_v3(self, catalog='catalog-v3.json'): self.adapter = self.useFixture(rm_fixture.Fixture()) self.calls = [] self._uri_registry.clear() - with open(self.discovery_json, 'r') as discovery_file, \ - open(os.path.join( - self.fixtures_directory, catalog), 'r') as tokens_file: - self.__do_register_uris([ - dict(method='GET', uri='https://identity.example.com/', - text=discovery_file.read()), - dict(method='POST', - uri='https://identity.example.com/v3/auth/tokens', - headers={ - 'X-Subject-Token': - self.getUniqueString('KeystoneToken')}, - text=tokens_file.read() - ), - ]) + self.__do_register_uris([ + self.get_keystone_v3_discovery(), + self.get_keystone_v3_token(catalog), + ]) self._make_test_cloud(identity_api_version='3') def use_keystone_v2(self): diff --git a/openstack/tests/unit/cloud/test_shade.py b/openstack/tests/unit/cloud/test_shade.py index cbf640487..1cd777df3 100644 --- a/openstack/tests/unit/cloud/test_shade.py +++ b/openstack/tests/unit/cloud/test_shade.py @@ -56,20 +56,41 @@ def test_openstack_cloud(self): def test_connect_as(self): # Do initial auth/catalog steps - # TODO(mordred) This only tests the constructor steps. Discovery - # cache sharing is broken. We need to get discovery_cache option - # plumbed through - # keystoneauth1.loading.base.BaseLoader.load_from_options - self.cloud.connect_as(project_name='test_project') + # This should authenticate a second time, but should not + # need a second identity discovery + self.register_uris([ + self.get_keystone_v3_token(), + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': []}, + ), + ]) + + c2 = self.cloud.connect_as(project_name='test_project') + self.assertEqual(c2.list_servers(), []) + self.assert_calls() def test_connect_as_context(self): # Do initial auth/catalog steps - # TODO(mordred) This only tests the constructor steps. Discovery - # cache sharing is broken. We need to get discovery_cache option - # plumbed through - # keystoneauth1.loading.base.BaseLoader.load_from_options - with self.cloud.connect_as(project_name='test_project'): - pass + # This should authenticate a second time, but should not + # need a second identity discovery + self.register_uris([ + self.get_keystone_v3_token(), + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': []}, + ), + ]) + + with self.cloud.connect_as(project_name='test_project') as c2: + self.assertEqual(c2.list_servers(), []) + self.assert_calls() @mock.patch.object(connection.Connection, 'search_images') def test_get_images(self, mock_search): From 95bf14908e528c9817a73d5e854061d632fa9f3a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 6 Aug 2019 09:02:33 -0400 Subject: [PATCH 2522/3836] Add support for global_request_id Added support for setting global_request_id on a Connection. If done, this will cause all requests sent to send the request id header to the OpenStack services. Since Connection can otherwise be used multi-threaded, add a method global_request that returns a new Connection based on the old Connection but on which the new global_request_id has been set. Since a Connection can be used as a context manager, this also means the global_request method can be used in with statements. Change-Id: I70964cdd79741703c0b9b911b3b2f27c248130f0 --- lower-constraints.txt | 2 +- openstack/cloud/openstackcloud.py | 58 +++++++++++++++++-- openstack/connection.py | 6 ++ openstack/proxy.py | 9 ++- openstack/tests/unit/cloud/test_shade.py | 38 ++++++++++++ .../global-request-id-d7c0736f43929165.yaml | 11 ++++ requirements.txt | 2 +- 7 files changed, 118 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/global-request-id-d7c0736f43929165.yaml diff --git a/lower-constraints.txt b/lower-constraints.txt index 60a322a5c..675cfea56 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -14,7 +14,7 @@ jmespath==0.9.0 jsonpatch==1.16 jsonpointer==1.13 jsonschema==2.6.0 -keystoneauth1==3.14.0 +keystoneauth1==3.15.0 linecache2==1.0.0 mock==2.0.0 mox3==0.20.0 diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 62e4bcc16..235ddc624 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -33,6 +33,7 @@ from openstack.cloud import meta from openstack.cloud import _utils import openstack.config +from openstack.config import cloud_region as cloud_region_mod from openstack import proxy DEFAULT_SERVER_AGE = 5 @@ -226,16 +227,16 @@ def pop_keys(params, auth, name_key, id_key): for key, value in kwargs.items(): params['auth'][key] = value - cloud_config = config.get_one(**params) + cloud_region = config.get_one(**params) # Attach the discovery cache from the old session so we won't # double discover. - cloud_config._discovery_cache = self.session._discovery_cache + cloud_region._discovery_cache = self.session._discovery_cache # Override the cloud name so that logging/location work right - cloud_config._name = self.name - cloud_config.config['profile'] = self.name + cloud_region._name = self.name + cloud_region.config['profile'] = self.name # Use self.__class__ so that we return whatever this if, like if it's # a subclass in the case of shade wrapping sdk. - return self.__class__(config=cloud_config) + return self.__class__(config=cloud_region) def connect_as_project(self, project): """Make a new OpenStackCloud object with a new project. @@ -267,6 +268,53 @@ def connect_as_project(self, project): auth['project_name'] = project return self.connect_as(**auth) + def global_request(self, global_request_id): + """Make a new Connection object with a global request id set. + + Take the existing settings from the current Connection and construct a + new Connection object with the global_request_id overridden. + + .. code-block:: python + + from oslo_context import context + cloud = openstack.connect(cloud='example') + # Work normally + servers = cloud.list_servers() + cloud2 = cloud.global_request(context.generate_request_id()) + # cloud2 sends all requests with global_request_id set + servers = cloud2.list_servers() + + Additionally, this can be used as a context manager: + + .. code-block:: python + + from oslo_context import context + c = openstack.connect(cloud='example') + # Work normally + servers = c.list_servers() + with c.global_request(context.generate_request_id()) as c2: + # c2 sends all requests with global_request_id set + servers = c2.list_servers() + + :param global_request_id: The `global_request_id` to send. + """ + params = copy.deepcopy(self.config.config) + cloud_region = cloud_region_mod.from_session( + session=self.session, + app_name=self.config._app_name, + app_version=self.config._app_version, + discovery_cache=self.session._discovery_cache, + **params) + + # Override the cloud name so that logging/location work right + cloud_region._name = self.name + cloud_region.config['profile'] = self.name + # Use self.__class__ so that we return whatever this is, like if it's + # a subclass in the case of shade wrapping sdk. + new_conn = self.__class__(config=cloud_region) + new_conn.set_global_request_id(global_request_id) + return new_conn + def _make_cache(self, cache_class, expiration_time, arguments): return dogpile.cache.make_region( function_key_generator=self._make_cache_key diff --git a/openstack/connection.py b/openstack/connection.py index e91ecc7a8..5916b1868 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -272,6 +272,7 @@ def __init__(self, cloud=None, config=None, session=None, rate_limit=None, oslo_conf=None, service_types=None, + global_request_id=None, **kwargs): """Create a connection to a cloud. @@ -328,6 +329,7 @@ def __init__(self, cloud=None, config=None, session=None, other service types will be disabled (will error if used). **Currently only supported in conjunction with the ``oslo_conf`` kwarg.** + :param global_request_id: A Request-id to send with all interactions. :param kwargs: If a config is not provided, the rest of the parameters provided are assumed to be arguments to be passed to the CloudRegion constructor. @@ -363,6 +365,7 @@ def __init__(self, cloud=None, config=None, session=None, self._session = None self._proxies = {} self.__pool_executor = None + self._global_request_id = global_request_id self.use_direct_get = use_direct_get self.strict_mode = strict # Call the _*CloudMixin constructors while we work on @@ -479,6 +482,9 @@ def close(self): if self.__pool_executor: self.__pool_executor.shutdown() + def set_global_request_id(self, global_request_id): + self._global_request_id = global_request_id + def __enter__(self): return self diff --git a/openstack/proxy.py b/openstack/proxy.py index 9260222ae..924354baf 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -144,10 +144,17 @@ def __init__( def request( self, url, method, error_message=None, - raise_exc=False, connect_retries=1, *args, **kwargs): + raise_exc=False, connect_retries=1, + global_request_id=None, *args, **kwargs): + if not global_request_id: + conn = self._get_connection() + if conn: + # Per-request setting should take precedence + global_request_id = conn._global_request_id response = super(Proxy, self).request( url, method, connect_retries=connect_retries, raise_exc=False, + global_request_id=global_request_id, **kwargs) for h in response.history: self._report_stats(h) diff --git a/openstack/tests/unit/cloud/test_shade.py b/openstack/tests/unit/cloud/test_shade.py index 1cd777df3..bd16d684a 100644 --- a/openstack/tests/unit/cloud/test_shade.py +++ b/openstack/tests/unit/cloud/test_shade.py @@ -106,6 +106,44 @@ def test_get_image_not_found(self, mock_search): r = self.cloud.get_image('doesNotExist') self.assertIsNone(r) + def test_global_request_id(self): + request_id = uuid.uuid4().hex + self.register_uris([ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': []}, + validate=dict( + headers={'X-Openstack-Request-Id': request_id}), + ), + ]) + + cloud2 = self.cloud.global_request(request_id) + self.assertEqual([], cloud2.list_servers()) + + self.assert_calls() + + def test_global_request_id_context(self): + request_id = uuid.uuid4().hex + self.register_uris([ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': []}, + validate=dict( + headers={'X-Openstack-Request-Id': request_id}), + ), + ]) + + with self.cloud.global_request(request_id) as c2: + self.assertEqual([], c2.list_servers()) + + self.assert_calls() + def test_get_server(self): server1 = fakes.make_fake_server('123', 'mickey') server2 = fakes.make_fake_server('345', 'mouse') diff --git a/releasenotes/notes/global-request-id-d7c0736f43929165.yaml b/releasenotes/notes/global-request-id-d7c0736f43929165.yaml new file mode 100644 index 000000000..b2677a0c8 --- /dev/null +++ b/releasenotes/notes/global-request-id-d7c0736f43929165.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + Added support for setting ``global_request_id`` on a ``Connection``. + If done, this will cause all requests sent to send the request id + header to the OpenStack services. Since ``Connection`` can otherwise + be used multi-threaded, add a method ``global_request`` that returns + a new ``Connection`` based on the old ``Connection`` but on which + the new ``global_request_id`` has been set. Since a ``Connection`` + can be used as a context manager, this also means the ``global_request`` + method can be used in ``with`` statements. diff --git a/requirements.txt b/requirements.txt index 42da24b3c..7985b04bf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ requestsexceptions>=1.2.0 # Apache-2.0 jsonpatch!=1.20,>=1.16 # BSD six>=1.10.0 # MIT os-service-types>=1.7.0 # Apache-2.0 -keystoneauth1>=3.14.0 # Apache-2.0 +keystoneauth1>=3.15.0 # Apache-2.0 munch>=2.1.0 # MIT decorator>=3.4.0 # BSD From 0ea586038533ef47288aab206eca18f9c7bf1caf Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 7 Aug 2019 14:44:46 -0400 Subject: [PATCH 2523/3836] Validate that connect_as connects as the project We validate that a second keystone auth is made, but we don't currently validate that the second auth is to the correct thing. Add in a token payload validation, and have it set the project. This will get subsummed in a bit with some work to make a proper Fixture for the catalog/requests_mock stuff. But for now, this should work. Change-Id: I59c068c3a7e8e8d028b89da6a2f49e845d279473 --- openstack/tests/unit/base.py | 32 ++++++++++++++++++++++-- openstack/tests/unit/cloud/test_shade.py | 10 +++++--- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 3ee304c5b..9a439d451 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -420,7 +420,11 @@ def use_nothing(self): self.calls = [] self._uri_registry.clear() - def get_keystone_v3_token(self, catalog='catalog-v3.json'): + def get_keystone_v3_token( + self, + catalog='catalog-v3.json', + project_name='admin', + ): catalog_file = os.path.join(self.fixtures_directory, catalog) with open(catalog_file, 'r') as tokens_file: return dict( @@ -429,7 +433,31 @@ def get_keystone_v3_token(self, catalog='catalog-v3.json'): headers={ 'X-Subject-Token': self.getUniqueString('KeystoneToken') }, - text=tokens_file.read() + text=tokens_file.read(), + validate=dict(json={ + 'auth': { + 'identity': { + 'methods': ['password'], + 'password': { + 'user': { + 'domain': { + 'name': 'default', + }, + 'name': 'admin', + 'password': 'password' + } + } + }, + 'scope': { + 'project': { + 'domain': { + 'name': 'default' + }, + 'name': project_name + } + } + } + }), ) def get_keystone_v3_discovery(self): diff --git a/openstack/tests/unit/cloud/test_shade.py b/openstack/tests/unit/cloud/test_shade.py index bd16d684a..4bd0e4e9a 100644 --- a/openstack/tests/unit/cloud/test_shade.py +++ b/openstack/tests/unit/cloud/test_shade.py @@ -58,8 +58,9 @@ def test_connect_as(self): # Do initial auth/catalog steps # This should authenticate a second time, but should not # need a second identity discovery + project_name = 'test_project' self.register_uris([ - self.get_keystone_v3_token(), + self.get_keystone_v3_token(project_name=project_name), self.get_nova_discovery_mock_dict(), dict( method='GET', @@ -69,7 +70,7 @@ def test_connect_as(self): ), ]) - c2 = self.cloud.connect_as(project_name='test_project') + c2 = self.cloud.connect_as(project_name=project_name) self.assertEqual(c2.list_servers(), []) self.assert_calls() @@ -77,8 +78,9 @@ def test_connect_as_context(self): # Do initial auth/catalog steps # This should authenticate a second time, but should not # need a second identity discovery + project_name = 'test_project' self.register_uris([ - self.get_keystone_v3_token(), + self.get_keystone_v3_token(project_name=project_name), self.get_nova_discovery_mock_dict(), dict( method='GET', @@ -88,7 +90,7 @@ def test_connect_as_context(self): ), ]) - with self.cloud.connect_as(project_name='test_project') as c2: + with self.cloud.connect_as(project_name=project_name) as c2: self.assertEqual(c2.list_servers(), []) self.assert_calls() From a263dfb4ee558e69ad4d3ad30e2afe7d15429ca4 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Tue, 13 Aug 2019 12:24:00 +0200 Subject: [PATCH 2524/3836] Add support for fields in baremetal get_* resources Omitting Driver since it does not support fields. Fixes two issues that used to be hidden by ignored 'params' argument to Resource.fetch: * Finding/deleting firewall rules ignore filters * Incorrect invocation of fetch in DNS Change-Id: I1f0abaec823be327e0e12a090155f97bc59e3489 --- openstack/baremetal/v1/_proxy.py | 62 ++++++++++++------- openstack/dns/v2/_base.py | 2 +- openstack/resource.py | 3 +- .../baremetal/test_baremetal_allocation.py | 5 ++ .../baremetal/test_baremetal_node.py | 7 +++ .../baremetal/test_baremetal_port.py | 6 ++ .../baremetal/test_baremetal_port_group.py | 6 ++ .../tests/unit/baremetal/v1/test_proxy.py | 40 ++++++++++-- openstack/tests/unit/cloud/test_fwaas.py | 5 +- openstack/tests/unit/image/v2/test_image.py | 7 ++- openstack/tests/unit/test_resource.py | 20 ++++-- .../baremetal-fields-624546fa533a8287.yaml | 5 ++ 12 files changed, 131 insertions(+), 37 deletions(-) create mode 100644 releasenotes/notes/baremetal-fields-624546fa533a8287.yaml diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index e15c367b6..8a000a535 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -25,6 +25,29 @@ class Proxy(proxy.Proxy): retriable_status_codes = _common.RETRIABLE_STATUS_CODES + def _get_with_fields(self, resource_type, value, fields=None): + """Fetch a bare metal resource. + + :param resource_type: The type of resource to get. + :type resource_type: :class:`~openstack.resource.Resource` + :param value: The value to get. Can be either the ID of a + resource or a :class:`~openstack.resource.Resource` + subclass. + :param fields: Limit the resource fields to fetch. + + :returns: The result of the ``fetch`` + :rtype: :class:`~openstack.resource.Resource` + """ + res = self._get_resource(resource_type, value) + kwargs = {} + if fields: + kwargs['fields'] = _common.comma_separated_list(fields) + return res.fetch( + self, + error_message="No {resource_type} found for {value}".format( + resource_type=resource_type.__name__, value=value), + **kwargs) + def chassis(self, details=False, **query): """Retrieve a generator of chassis. @@ -85,17 +108,18 @@ def find_chassis(self, name_or_id, ignore_missing=True): return self._find(_chassis.Chassis, name_or_id, ignore_missing=ignore_missing) - def get_chassis(self, chassis): + def get_chassis(self, chassis, fields=None): """Get a specific chassis. :param chassis: The value can be the ID of a chassis or a :class:`~openstack.baremetal.v1.chassis.Chassis` instance. + :param fields: Limit the resource fields to fetch. :returns: One :class:`~openstack.baremetal.v1.chassis.Chassis` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no chassis matching the name or ID could be found. """ - return self._get(_chassis.Chassis, chassis) + return self._get_with_fields(_chassis.Chassis, chassis, fields=fields) def update_chassis(self, chassis, **attrs): """Update a chassis. @@ -239,17 +263,18 @@ def find_node(self, name_or_id, ignore_missing=True): return self._find(_node.Node, name_or_id, ignore_missing=ignore_missing) - def get_node(self, node): + def get_node(self, node, fields=None): """Get a specific node. :param node: The value can be the name or ID of a node or a :class:`~openstack.baremetal.v1.node.Node` instance. + :param fields: Limit the resource fields to fetch. :returns: One :class:`~openstack.baremetal.v1.node.Node` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no node matching the name or ID could be found. """ - return self._get(_node.Node, node) + return self._get_with_fields(_node.Node, node, fields=fields) def update_node(self, node, retry_on_conflict=True, **attrs): """Update a node. @@ -552,23 +577,18 @@ def find_port(self, name_or_id, ignore_missing=True): return self._find(_port.Port, name_or_id, ignore_missing=ignore_missing) - def get_port(self, port, **query): + def get_port(self, port, fields=None): """Get a specific port. :param port: The value can be the ID of a port or a :class:`~openstack.baremetal.v1.port.Port` instance. - :param dict query: Optional query parameters to be sent to restrict - the port properties returned. Available parameters include: - - * ``fields``: A list containing one or more fields to be returned - in the response. This may lead to some performance gain - because other fields of the resource are not refreshed. + :param fields: Limit the resource fields to fetch. :returns: One :class:`~openstack.baremetal.v1.port.Port` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no port matching the name or ID could be found. """ - return self._get(_port.Port, port, **query) + return self._get_with_fields(_port.Port, port, fields=fields) def update_port(self, port, **attrs): """Update a port. @@ -675,23 +695,19 @@ def find_port_group(self, name_or_id, ignore_missing=True): return self._find(_portgroup.PortGroup, name_or_id, ignore_missing=ignore_missing) - def get_port_group(self, port_group, **query): + def get_port_group(self, port_group, fields=None): """Get a specific port group. :param port_group: The value can be the name or ID of a chassis or a :class:`~openstack.baremetal.v1.port_group.PortGroup` instance. - :param dict query: Optional query parameters to be sent to restrict - the port group properties returned. Available parameters include: - - * ``fields``: A list containing one or more fields to be returned - in the response. This may lead to some performance gain - because other fields of the resource are not refreshed. + :param fields: Limit the resource fields to fetch. :returns: One :class:`~openstack.baremetal.v1.port_group.PortGroup` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no port group matching the name or ID could be found. """ - return self._get(_portgroup.PortGroup, port_group, **query) + return self._get_with_fields(_portgroup.PortGroup, port_group, + fields=fields) def update_port_group(self, port_group, **attrs): """Update a port group. @@ -842,17 +858,19 @@ def create_allocation(self, **attrs): """ return self._create(_allocation.Allocation, **attrs) - def get_allocation(self, allocation): + def get_allocation(self, allocation, fields=None): """Get a specific allocation. :param allocation: The value can be the name or ID of an allocation or a :class:`~openstack.baremetal.v1.allocation.Allocation` instance. + :param fields: Limit the resource fields to fetch. :returns: One :class:`~openstack.baremetal.v1.allocation.Allocation` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no allocation matching the name or ID could be found. """ - return self._get(_allocation.Allocation, allocation) + return self._get_with_fields(_allocation.Allocation, allocation, + fields=fields) def update_allocation(self, allocation, **attrs): """Update an allocation. diff --git a/openstack/dns/v2/_base.py b/openstack/dns/v2/_base.py index 40601a5f7..e2e3fa695 100644 --- a/openstack/dns/v2/_base.py +++ b/openstack/dns/v2/_base.py @@ -49,7 +49,7 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): id=name_or_id, connection=session._get_connection(), **params) - return match.fetch(session, **params) + return match.fetch(session) except exceptions.SDKException: # DNS may return 400 when we try to do GET with name pass diff --git a/openstack/resource.py b/openstack/resource.py index a6ab26d06..28106c271 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1295,7 +1295,8 @@ def fetch(self, session, requires_id=True, base_path=base_path) session = self._get_session(session) microversion = self._get_microversion_for(session, 'fetch') - response = session.get(request.url, microversion=microversion) + response = session.get(request.url, microversion=microversion, + params=params) kwargs = {} if error_message: kwargs['error_message'] = error_message diff --git a/openstack/tests/functional/baremetal/test_baremetal_allocation.py b/openstack/tests/functional/baremetal/test_baremetal_allocation.py index 9ffc52560..2f93285b5 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_allocation.py +++ b/openstack/tests/functional/baremetal/test_baremetal_allocation.py @@ -55,6 +55,11 @@ def test_allocation_create_get_delete(self): self.assertEqual(self.node.id, allocation.node_id) self.assertIsNone(allocation.last_error) + with_fields = self.conn.baremetal.get_allocation( + allocation.id, fields=['uuid', 'node_uuid']) + self.assertEqual(allocation.id, with_fields.id) + self.assertIsNone(with_fields.state) + node = self.conn.baremetal.get_node(self.node.id) self.assertEqual(allocation.id, node.allocation_id) diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index 92a93dc8f..a7a07be95 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -34,6 +34,13 @@ def test_node_create_get_delete(self): self.assertEqual(node.id, found.id) self.assertEqual(node.name, found.name) + with_fields = self.conn.baremetal.get_node('node-name', + fields=['uuid', 'driver']) + self.assertEqual(node.id, with_fields.id) + self.assertEqual(node.driver, with_fields.driver) + self.assertIsNone(with_fields.name) + self.assertIsNone(with_fields.provision_state) + nodes = self.conn.baremetal.nodes() self.assertIn(node.id, [n.id for n in nodes]) diff --git a/openstack/tests/functional/baremetal/test_baremetal_port.py b/openstack/tests/functional/baremetal/test_baremetal_port.py index 5c41c0e8e..2ac7fc050 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_port.py +++ b/openstack/tests/functional/baremetal/test_baremetal_port.py @@ -31,6 +31,12 @@ def test_port_create_get_delete(self): loaded = self.conn.baremetal.get_port(port.id) self.assertEqual(loaded.id, port.id) + self.assertIsNotNone(loaded.address) + + with_fields = self.conn.baremetal.get_port(port.id, + fields=['uuid', 'extra']) + self.assertEqual(port.id, with_fields.id) + self.assertIsNone(with_fields.address) self.conn.baremetal.delete_port(port, ignore_missing=False) self.assertRaises(exceptions.ResourceNotFound, diff --git a/openstack/tests/functional/baremetal/test_baremetal_port_group.py b/openstack/tests/functional/baremetal/test_baremetal_port_group.py index bd74db5bd..3e5eeb0c8 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_port_group.py +++ b/openstack/tests/functional/baremetal/test_baremetal_port_group.py @@ -28,6 +28,12 @@ def test_port_group_create_get_delete(self): loaded = self.conn.baremetal.get_port_group(port_group.id) self.assertEqual(loaded.id, port_group.id) + self.assertIsNotNone(loaded.node_id) + + with_fields = self.conn.baremetal.get_port_group( + port_group.id, fields=['uuid', 'extra']) + self.assertEqual(port_group.id, with_fields.id) + self.assertIsNone(with_fields.node_id) self.conn.baremetal.delete_port_group(port_group, ignore_missing=False) diff --git a/openstack/tests/unit/baremetal/v1/test_proxy.py b/openstack/tests/unit/baremetal/v1/test_proxy.py index cd6c65d53..f5902db97 100644 --- a/openstack/tests/unit/baremetal/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal/v1/test_proxy.py @@ -24,6 +24,9 @@ from openstack.tests.unit import test_proxy_base +_MOCK_METHOD = 'openstack.baremetal.v1._proxy.Proxy._get_with_fields' + + class TestBaremetalProxy(test_proxy_base.TestProxyBase): def setUp(self): @@ -55,7 +58,9 @@ def test_find_chassis(self): self.verify_find(self.proxy.find_chassis, chassis.Chassis) def test_get_chassis(self): - self.verify_get(self.proxy.get_chassis, chassis.Chassis) + self.verify_get(self.proxy.get_chassis, chassis.Chassis, + mock_method=_MOCK_METHOD, + expected_kwargs={'fields': None}) def test_update_chassis(self): self.verify_update(self.proxy.update_chassis, chassis.Chassis) @@ -85,7 +90,9 @@ def test_find_node(self): self.verify_find(self.proxy.find_node, node.Node) def test_get_node(self): - self.verify_get(self.proxy.get_node, node.Node) + self.verify_get(self.proxy.get_node, node.Node, + mock_method=_MOCK_METHOD, + expected_kwargs={'fields': None}) @mock.patch.object(node.Node, 'commit', autospec=True) def test_update_node(self, mock_commit): @@ -127,7 +134,9 @@ def test_find_port(self): self.verify_find(self.proxy.find_port, port.Port) def test_get_port(self): - self.verify_get(self.proxy.get_port, port.Port) + self.verify_get(self.proxy.get_port, port.Port, + mock_method=_MOCK_METHOD, + expected_kwargs={'fields': None}) def test_update_port(self): self.verify_update(self.proxy.update_port, port.Port) @@ -150,11 +159,18 @@ def test_port_groups_not_detailed(self, mock_list): self.assertIs(result, mock_list.return_value) mock_list.assert_called_once_with(self.proxy, details=False, query=1) + def test_get_port_group(self): + self.verify_get(self.proxy.get_port_group, port_group.PortGroup, + mock_method=_MOCK_METHOD, + expected_kwargs={'fields': None}) + def test_create_allocation(self): self.verify_create(self.proxy.create_allocation, allocation.Allocation) def test_get_allocation(self): - self.verify_get(self.proxy.get_allocation, allocation.Allocation) + self.verify_get(self.proxy.get_allocation, allocation.Allocation, + mock_method=_MOCK_METHOD, + expected_kwargs={'fields': None}) def test_delete_allocation(self): self.verify_delete(self.proxy.delete_allocation, allocation.Allocation, @@ -164,6 +180,22 @@ def test_delete_allocation_ignore(self): self.verify_delete(self.proxy.delete_allocation, allocation.Allocation, True) + @mock.patch.object(node.Node, 'fetch', autospec=True) + def test__get_with_fields_none(self, mock_fetch): + result = self.proxy._get_with_fields(node.Node, 'value') + self.assertIs(result, mock_fetch.return_value) + mock_fetch.assert_called_once_with(mock.ANY, self.proxy, + error_message=mock.ANY) + + @mock.patch.object(node.Node, 'fetch', autospec=True) + def test__get_with_fields(self, mock_fetch): + result = self.proxy._get_with_fields(node.Node, 'value', + fields=['a', 'b']) + self.assertIs(result, mock_fetch.return_value) + mock_fetch.assert_called_once_with(mock.ANY, self.proxy, + error_message=mock.ANY, + fields='a,b') + @mock.patch('time.sleep', lambda _sec: None) @mock.patch.object(_proxy.Proxy, 'get_node', autospec=True) diff --git a/openstack/tests/unit/cloud/test_fwaas.py b/openstack/tests/unit/cloud/test_fwaas.py index 4abccb054..ccf7d78e9 100644 --- a/openstack/tests/unit/cloud/test_fwaas.py +++ b/openstack/tests/unit/cloud/test_fwaas.py @@ -107,7 +107,8 @@ def test_delete_firewall_rule_filters(self): self.register_uris([ dict(method='GET', # short-circuit uri=self._make_mock_url('firewall_rules', - self.firewall_rule_name), + self.firewall_rule_name, + **filters), status_code=404), dict(method='GET', uri=self._make_mock_url( @@ -232,7 +233,7 @@ def test_update_firewall_rule_filters(self): dict( method='GET', uri=self._make_mock_url( - 'firewall_rules', self.firewall_rule_name), + 'firewall_rules', self.firewall_rule_name, **filters), json={'firewall_rule': self._mock_firewall_rule_attrs}), dict( method='PUT', diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 8981db8f8..8d0cafed9 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -355,7 +355,7 @@ def test_download_no_checksum_header(self): self.sess.get.assert_has_calls( [mock.call('images/IDENTIFIER/file', stream=False), - mock.call('images/IDENTIFIER', microversion=None)]) + mock.call('images/IDENTIFIER', microversion=None, params={})]) self.assertEqual(rv, resp1) @@ -384,7 +384,7 @@ def test_download_no_checksum_at_all2(self): self.sess.get.assert_has_calls( [mock.call('images/IDENTIFIER/file', stream=False), - mock.call('images/IDENTIFIER', microversion=None)]) + mock.call('images/IDENTIFIER', microversion=None, params={})]) self.assertEqual(rv, resp1) @@ -483,7 +483,8 @@ def test_image_find(self): result = sot.find(self.sess, EXAMPLE['name']) self.sess.get.assert_has_calls([ - mock.call('images/' + EXAMPLE['name'], microversion=None), + mock.call('images/' + EXAMPLE['name'], microversion=None, + params={}), mock.call('/images', headers={'Accept': 'application/json'}, microversion=None, params={'name': EXAMPLE['name']}), mock.call('/images', headers={'Accept': 'application/json'}, diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index d435a89ae..9a39fc21e 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1445,7 +1445,19 @@ def test_fetch(self): self.sot._prepare_request.assert_called_once_with( requires_id=True, base_path=None) self.session.get.assert_called_once_with( - self.request.url, microversion=None) + self.request.url, microversion=None, params={}) + + self.assertIsNone(self.sot.microversion) + self.sot._translate_response.assert_called_once_with(self.response) + self.assertEqual(result, self.sot) + + def test_fetch_with_params(self): + result = self.sot.fetch(self.session, fields='a,b') + + self.sot._prepare_request.assert_called_once_with( + requires_id=True, base_path=None) + self.session.get.assert_called_once_with( + self.request.url, microversion=None, params={'fields': 'a,b'}) self.assertIsNone(self.sot.microversion) self.sot._translate_response.assert_called_once_with(self.response) @@ -1467,7 +1479,7 @@ class Test(resource.Resource): sot._prepare_request.assert_called_once_with( requires_id=True, base_path=None) self.session.get.assert_called_once_with( - self.request.url, microversion='1.42') + self.request.url, microversion='1.42', params={}) self.assertEqual(sot.microversion, '1.42') sot._translate_response.assert_called_once_with(self.response) @@ -1479,7 +1491,7 @@ def test_fetch_not_requires_id(self): self.sot._prepare_request.assert_called_once_with( requires_id=False, base_path=None) self.session.get.assert_called_once_with( - self.request.url, microversion=None) + self.request.url, microversion=None, params={}) self.sot._translate_response.assert_called_once_with(self.response) self.assertEqual(result, self.sot) @@ -1491,7 +1503,7 @@ def test_fetch_base_path(self): requires_id=False, base_path='dummy') self.session.get.assert_called_once_with( - self.request.url, microversion=None) + self.request.url, microversion=None, params={}) self.sot._translate_response.assert_called_once_with(self.response) self.assertEqual(result, self.sot) diff --git a/releasenotes/notes/baremetal-fields-624546fa533a8287.yaml b/releasenotes/notes/baremetal-fields-624546fa533a8287.yaml new file mode 100644 index 000000000..053140d58 --- /dev/null +++ b/releasenotes/notes/baremetal-fields-624546fa533a8287.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Adds support for fetching specific fields when getting bare metal + `Node`, `Port`, `PortGroup`, `Chassis` and `Allocation` resources. From 6e7cbb96781fc326e86b77468d44d8ba89898f0b Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Tue, 13 Aug 2019 11:59:03 -0500 Subject: [PATCH 2525/3836] Bump keystoneauth1 minimum to 3.16.0 Because we should always be tracking to the latest ksa anyway, but also... ...to allow from_conf to to take advantage of retry config options introduced in [1] and [2]. [1] https://review.opendev.org/666287 [2] https://review.opendev.org/672930 Change-Id: I85669b12a6948ab5accd3d7df71dcbacce413e33 --- lower-constraints.txt | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index 675cfea56..b66d850be 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -14,7 +14,7 @@ jmespath==0.9.0 jsonpatch==1.16 jsonpointer==1.13 jsonschema==2.6.0 -keystoneauth1==3.15.0 +keystoneauth1==3.16.0 linecache2==1.0.0 mock==2.0.0 mox3==0.20.0 diff --git a/requirements.txt b/requirements.txt index 7985b04bf..961f57956 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ requestsexceptions>=1.2.0 # Apache-2.0 jsonpatch!=1.20,>=1.16 # BSD six>=1.10.0 # MIT os-service-types>=1.7.0 # Apache-2.0 -keystoneauth1>=3.15.0 # Apache-2.0 +keystoneauth1>=3.16.0 # Apache-2.0 munch>=2.1.0 # MIT decorator>=3.4.0 # BSD From 4b0c3c2fb6ea0e089c65cef9f2226f4326f9fe53 Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Wed, 14 Aug 2019 21:53:08 +0000 Subject: [PATCH 2526/3836] Add 'node' attribute to baremetal Allocation This attribute is only used for create_allocation. When it is specified, an allocation is created for an existing active node without going through the normal allocation steps. Change-Id: Ia0e54d7a38bdc50b163113c6435d49b33ced947d --- openstack/baremetal/v1/allocation.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openstack/baremetal/v1/allocation.py b/openstack/baremetal/v1/allocation.py index 19be5133f..40f57ee74 100644 --- a/openstack/baremetal/v1/allocation.py +++ b/openstack/baremetal/v1/allocation.py @@ -37,7 +37,8 @@ class Allocation(_common.ListMixin, resource.Resource): ) # Allocation update is available since 1.57 - _max_microversion = '1.57' + # Backfilling allocations is available since 1.58 + _max_microversion = '1.58' #: The candidate nodes for this allocation. candidate_nodes = resource.Body('candidate_nodes', type=list) @@ -53,6 +54,9 @@ class Allocation(_common.ListMixin, resource.Resource): links = resource.Body('links', type=list) #: The name of the allocation. name = resource.Body('name') + #: The node UUID or name to create the allocation against, + #: bypassing the normal allocation process. + node = resource.Body('node') #: UUID of the node this allocation belongs to. node_id = resource.Body('node_uuid') #: The requested resource class. From 8298a46e87cf84b69fc9e682516dfe52e1aa4eb2 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 15 Aug 2019 10:59:16 -0400 Subject: [PATCH 2527/3836] Add header to auto-delete image upload objects When using the tasks API, swift objects are used for creating the final image. Although we manually clean these up when everything goes right, it's possible when things go wrong that these objects can be left lying around. Let's use the 'x-delete-after' header on the objects to make sure they are cleaned up after a day. Change-Id: Ieb2c11490a6fd195941ca87724eae2a6f67d43ad --- openstack/image/v2/_proxy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 60c2bfd30..b539b6699 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -251,7 +251,8 @@ def _upload_image_task( container, name, filename, md5=md5, sha256=sha256, metadata={self._connection._OBJECT_AUTOCREATE_KEY: 'true'}, - **{'content-type': 'application/octet-stream'}) + **{'content-type': 'application/octet-stream', + 'x-delete-after': str(24 * 60 * 60)}) # TODO(mordred): Can we do something similar to what nodepool does # using glance properties to not delete then upload but instead make a # new "good" image and then mark the old one as "bad" From 1161fe360a27df02628fb00a2d9b17d3d725e7a2 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 15 Aug 2019 11:58:33 -0400 Subject: [PATCH 2528/3836] Avoid unnecessary object meta prefix in proxy This is a follow up to I3940a8e93d0fc69244794a81b9d2c81d53b54986 that fixed the bug in the non-proxy version of the code (i.e., shade), but not in the proxy version. Change-Id: I54659d41920c00fd0e43f4440ac0533c23b0d71a --- openstack/object_store/v1/_proxy.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index c8d1eef66..8ee1f755a 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -318,7 +318,10 @@ def create_object( if sha256: headers[self._connection._OBJECT_SHA256_KEY] = sha256 or '' for (k, v) in metadata.items(): - headers['x-object-meta-' + k] = v + if not k.lower().startswith('x-object-meta-'): + headers['x-object-meta-' + k] = v + else: + headers[k] = v container_name = self._get_container_name(container=container) endpoint = '{container}/{name}'.format(container=container_name, From 5ae0d72247f02c56c59a7fcb5b15e48539740d60 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Thu, 15 Aug 2019 17:59:44 -0500 Subject: [PATCH 2529/3836] Add strict_proxies option for Connection Add a strict_proxies kwarg to Connection, defaulting to False. When True, ServiceDescription will perform an extra check when creating a proxy. If it doesn't look like a valid connection is available, raise a new SDKException called ServiceDiscoveryException. Change-Id: I0b404d5744a4465d365780a4273aa8dc1cebeb14 Co-Authored-By: Monty Taylor --- openstack/connection.py | 12 +++ openstack/exceptions.py | 4 + openstack/service_description.py | 37 +++++++- openstack/tests/unit/base.py | 3 +- openstack/tests/unit/config/test_from_conf.py | 91 ++++++++++++++++++- .../strict-proxies-4a315f68f387ee89.yaml | 8 ++ 6 files changed, 149 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/strict-proxies-4a315f68f387ee89.yaml diff --git a/openstack/connection.py b/openstack/connection.py index 5916b1868..17a9ef158 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -273,6 +273,7 @@ def __init__(self, cloud=None, config=None, session=None, oslo_conf=None, service_types=None, global_request_id=None, + strict_proxies=False, **kwargs): """Create a connection to a cloud. @@ -330,12 +331,23 @@ def __init__(self, cloud=None, config=None, session=None, **Currently only supported in conjunction with the ``oslo_conf`` kwarg.** :param global_request_id: A Request-id to send with all interactions. + :param strict_proxies: + If True, check proxies on creation and raise + ServiceDiscoveryException if the service is unavailable. + :type strict_proxies: bool + Throw an ``openstack.exceptions.ServiceDiscoveryException`` if the + endpoint for a given service doesn't work. This is useful for + OpenStack services using sdk to talk to other OpenStack services + where it can be expected that the deployer config is correct and + errors should be reported immediately. + Default false. :param kwargs: If a config is not provided, the rest of the parameters provided are assumed to be arguments to be passed to the CloudRegion constructor. """ self.config = config self._extra_services = {} + self._strict_proxies = strict_proxies if extra_services: for service in extra_services: self._extra_services[service.service_type] = service diff --git a/openstack/exceptions.py b/openstack/exceptions.py index a267c2c9f..56fba3b5e 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -257,3 +257,7 @@ class TaskManagerStopped(SDKException): class ServiceDisabledException(ConfigException): """This service is disabled for reasons.""" + + +class ServiceDiscoveryException(SDKException): + """The service cannot be discovered.""" diff --git a/openstack/service_description.py b/openstack/service_description.py index 643184eff..c6cd11e3f 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -17,6 +17,7 @@ from openstack import _log from openstack import exceptions +from openstack import proxy as proxy_mod __all__ = [ 'ServiceDescription', @@ -83,10 +84,34 @@ def __get__(self, instance, owner): if instance is None: return self if self.service_type not in instance._proxies: - instance._proxies[self.service_type] = self._make_proxy(instance) - instance._proxies[self.service_type]._connection = instance + proxy = self._make_proxy(instance) + if not isinstance(proxy, _ServiceDisabledProxyShim): + # The keystone proxy has a method called get_endpoint + # that is about managing keystone endpoints. This is + # unfortunate. + endpoint = proxy_mod.Proxy.get_endpoint(proxy) + if instance._strict_proxies: + self._validate_proxy(proxy, endpoint) + proxy._connection = instance + instance._proxies[self.service_type] = proxy return instance._proxies[self.service_type] + def _validate_proxy(self, proxy, endpoint): + exc = None + service_url = getattr(proxy, 'skip_discovery', None) + try: + # Don't go too wild for e.g. swift + if service_url is None: + service_url = proxy.get_endpoint_data().service_url + except Exception as e: + exc = e + if exc or not endpoint or not service_url: + raise exceptions.ServiceDiscoveryException( + "Failed to create a working proxy for service {service_type}: " + "{message}".format( + service_type=self.service_type, + message=exc or "No valid endpoint was discoverable.")) + def _make_proxy(self, instance): """Create a Proxy for the service in question. @@ -108,8 +133,6 @@ def _make_proxy(self, instance): self.service_type, allow_version_hack=True, ) - # trigger EndpointNotFound exception if this is bogus - temp_client.get_endpoint() return temp_client # Check to see if we've got config that matches what we @@ -173,6 +196,12 @@ def _make_proxy(self, instance): return proxy_obj data = proxy_obj.get_endpoint_data() + if not data and instance._strict_proxies: + raise exceptions.ServiceDiscoveryException( + "Failed to create a working proxy for service " + "{service_type}: No endpoint data found.".format( + service_type=self.service_type)) + # If we've gotten here with a proxy object it means we have # an endpoint_override in place. If the catalog_url and # service_url don't match, which can happen if there is a diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 9a439d451..238d13b00 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -673,7 +673,8 @@ def __do_register_uris(self, uri_mock_list=None): if 'content-type' not in headers: headers[u'content-type'] = 'application/json' - to_mock['headers'] = headers + if 'exc' not in to_mock: + to_mock['headers'] = headers self.calls += [ dict( diff --git a/openstack/tests/unit/config/test_from_conf.py b/openstack/tests/unit/config/test_from_conf.py index 49b8e4a68..c9ab677cc 100644 --- a/openstack/tests/unit/config/test_from_conf.py +++ b/openstack/tests/unit/config/test_from_conf.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import requests.exceptions import uuid from keystoneauth1 import exceptions as ks_exc @@ -31,7 +32,7 @@ def _get_conn(self, **from_conf_kwargs): **from_conf_kwargs) self.assertEqual('from_conf.example.com', config.name) - return connection.Connection(config=config) + return connection.Connection(config=config, strict_proxies=True) def test_adapter_opts_set(self): """Adapter opts specified in the conf.""" @@ -75,6 +76,18 @@ def test_default_adapter_opts(self): """Adapter opts are registered, but all defaulting in conf.""" conn = self._get_conn() + server_id = str(uuid.uuid4()) + server_name = self.getUniqueString('name') + fake_server = fakes.make_fake_server(server_id, server_name) + + self.register_uris([ + self.get_nova_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail']), + json={'servers': [fake_server]}), + ]) + # Nova has empty adapter config, so these default adap = conn.compute self.assertIsNone(adap.region_name) @@ -82,16 +95,41 @@ def test_default_adapter_opts(self): self.assertEqual('public', adap.interface) self.assertIsNone(adap.endpoint_override) + s = next(adap.servers()) + self.assertEqual(s.id, server_id) + self.assertEqual(s.name, server_name) + self.assert_calls() + + def test_service_not_ready_catalog(self): + """Adapter opts are registered, but all defaulting in conf.""" + conn = self._get_conn() + server_id = str(uuid.uuid4()) server_name = self.getUniqueString('name') fake_server = fakes.make_fake_server(server_id, server_name) + self.register_uris([ + dict(method='GET', + uri='https://compute.example.com/v2.1/', + exc=requests.exceptions.ConnectionError), self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), json={'servers': [fake_server]}), ]) + + self.assertRaises( + exceptions.ServiceDiscoveryException, + getattr, conn, 'compute') + + # Nova has empty adapter config, so these default + adap = conn.compute + self.assertIsNone(adap.region_name) + self.assertEqual('compute', adap.service_type) + self.assertEqual('public', adap.interface) + self.assertIsNone(adap.endpoint_override) + s = next(adap.servers()) self.assertEqual(s.id, server_id) self.assertEqual(s.name, server_name) @@ -119,6 +157,11 @@ def test_name_with_dashes(self): dict(method='GET', uri='https://example.org:5050', json=discovery), + # strict-proxies means we're going to fetch the discovery + # doc from the versioned endpoint to verify it works. + dict(method='GET', + uri='https://example.org:5050/v1', + json=discovery), dict(method='GET', uri='https://example.org:5050/v1/introspection/abcd', json=status), @@ -131,6 +174,52 @@ def test_name_with_dashes(self): self.assertTrue(adap.get_introspection('abcd').is_finished) + def test_service_not_ready_endpoint_override(self): + conn = self._get_conn() + + discovery = { + "versions": { + "values": [ + {"status": "stable", + "id": "v1", + "links": [{ + "href": "https://example.org:5050/v1", + "rel": "self"}] + }] + } + } + status = { + 'finished': True, + 'error': None + } + self.register_uris([ + dict(method='GET', + uri='https://example.org:5050', + exc=requests.exceptions.ConnectTimeout), + dict(method='GET', + uri='https://example.org:5050', + json=discovery), + # strict-proxies means we're going to fetch the discovery + # doc from the versioned endpoint to verify it works. + dict(method='GET', + uri='https://example.org:5050/v1', + json=discovery), + dict(method='GET', + uri='https://example.org:5050/v1/introspection/abcd', + json=status), + ]) + + self.assertRaises( + exceptions.ServiceDiscoveryException, + getattr, conn, 'baremetal_introspection') + + adap = conn.baremetal_introspection + self.assertEqual('baremetal-introspection', adap.service_type) + self.assertEqual('public', adap.interface) + self.assertEqual('https://example.org:5050/v1', adap.endpoint_override) + + self.assertTrue(adap.get_introspection('abcd').is_finished) + def assert_service_disabled(self, service_type, expected_reason, **from_conf_kwargs): conn = self._get_conn(**from_conf_kwargs) diff --git a/releasenotes/notes/strict-proxies-4a315f68f387ee89.yaml b/releasenotes/notes/strict-proxies-4a315f68f387ee89.yaml new file mode 100644 index 000000000..ae2e11bc1 --- /dev/null +++ b/releasenotes/notes/strict-proxies-4a315f68f387ee89.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Added new option for Connection, ``strict_proxies``. When set to ``True``, + Connection will throw a ``ServiceDiscoveryException`` if the endpoint for + a given service doesn't work. This is useful for OpenStack services using + sdk to talk to other OpenStack services where it can be expected that the + deployer config is correct and errors should be reported immediately. From 658cd3a7f121e2ef2ca669732b150fca5f268876 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 7 Aug 2019 10:45:51 -0400 Subject: [PATCH 2530/3836] Rationalize endpoint_for and get_endpoint_from_catalog We have two methods in two different places which do almost the same thing. Make one use the other and then document what they do. Finally, make sure endpoint_for respects endpoint_override, since that would be the rational expectation at that point in the code. Add some tests. Change-Id: I4ff51373496c558342cfb54820eab4dddd195625 --- openstack/cloud/openstackcloud.py | 29 ++++++++++++++-- openstack/config/cloud_region.py | 33 +++++++++++++++---- openstack/tests/unit/cloud/test_shade.py | 15 +++++++++ .../tests/unit/config/test_cloud_config.py | 17 ++++++++++ openstack/tests/unit/fixtures/catalog-v3.json | 6 ++++ 5 files changed, 91 insertions(+), 9 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 235ddc624..dfc8af553 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -499,9 +499,32 @@ def _keystone_catalog(self): def service_catalog(self): return self._keystone_catalog.catalog - def endpoint_for(self, service_type, interface='public'): - return self._keystone_catalog.url_for( - service_type=service_type, interface=interface) + def endpoint_for(self, service_type, interface=None, region_name=None): + """Return the endpoint for a given service. + + Respects config values for Connection, including + ``*_endpoint_override``. For direct values from the catalog + regardless of overrides, see + :meth:`~openstack.config.cloud_region.CloudRegion.get_endpoint_from_catalog` + + :param service_type: Service Type of the endpoint to search for. + :param interface: + Interface of the endpoint to search for. Optional, defaults to + the configured value for interface for this Connection. + :param region_name: + Region Name of the endpoint to search for. Optional, defaults to + the configured value for region_name for this Connection. + + :returns: The endpoint of the service, or None if not found. + """ + + endpoint_override = self.config.get_endpoint(service_type) + if endpoint_override: + return endpoint_override + return self.config.get_endpoint_from_catalog( + service_type=service_type, + interface=interface, + region_name=region_name) @property def auth_token(self): diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 6844c5fe4..c9c626426 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -466,13 +466,34 @@ def get_endpoint(self, service_type): value = auth.get('auth_url') return value - def get_endpoint_from_catalog(self, service_type): + def get_endpoint_from_catalog( + self, service_type, interface=None, region_name=None): + """Return the endpoint for a given service as found in the catalog. + + For values respecting endpoint overrides, see + :meth:`~openstack.connection.Connection.endpoint_for` + + :param service_type: Service Type of the endpoint to search for. + :param interface: + Interface of the endpoint to search for. Optional, defaults to + the configured value for interface for this Connection. + :param region_name: + Region Name of the endpoint to search for. Optional, defaults to + the configured value for region_name for this Connection. + + :returns: The endpoint of the service, or None if not found. + """ + interface = interface or self.get_interface(service_type) + region_name = region_name or self.get_region_name(service_type) session = self.get_session() - return session.auth.get_endpoint( - session, - service_type=service_type, - region_name=self.get_region_name(service_type), - interface=self.get_interface(service_type)) + catalog = session.auth.get_access(session).service_catalog + try: + return catalog.url_for( + service_type=service_type, + interface=interface, + region_name=region_name) + except keystoneauth1.exceptions.catalog.EndpointNotFound: + return None def get_connect_retries(self, service_type): return self._get_config('connect_retries', service_type, diff --git a/openstack/tests/unit/cloud/test_shade.py b/openstack/tests/unit/cloud/test_shade.py index 4bd0e4e9a..5a9dfbbc9 100644 --- a/openstack/tests/unit/cloud/test_shade.py +++ b/openstack/tests/unit/cloud/test_shade.py @@ -54,6 +54,21 @@ def fake_has_service(*args, **kwargs): def test_openstack_cloud(self): self.assertIsInstance(self.cloud, connection.Connection) + def test_endpoint_for(self): + dns_override = 'https://override.dns.example.com' + self.cloud.config.config['dns_endpoint_override'] = dns_override + self.assertEqual( + 'https://compute.example.com/v2.1/', + self.cloud.endpoint_for('compute')) + self.assertEqual( + 'https://internal.compute.example.com/v2.1/', + self.cloud.endpoint_for('compute', interface='internal')) + self.assertIsNone( + self.cloud.endpoint_for('compute', region_name='unknown-region')) + self.assertEqual( + dns_override, + self.cloud.endpoint_for('dns')) + def test_connect_as(self): # Do initial auth/catalog steps # This should authenticate a second time, but should not diff --git a/openstack/tests/unit/config/test_cloud_config.py b/openstack/tests/unit/config/test_cloud_config.py index 718450217..5a0f03212 100644 --- a/openstack/tests/unit/config/test_cloud_config.py +++ b/openstack/tests/unit/config/test_cloud_config.py @@ -365,3 +365,20 @@ def test_session_endpoint_not_found(self, mock_get_session): cc = cloud_region.CloudRegion( "test1", "region-al", {}, auth_plugin=mock.Mock()) self.assertIsNone(cc.get_session_endpoint('notfound')) + + def test_get_endpoint_from_catalog(self): + dns_override = 'https://override.dns.example.com' + self.cloud.config.config['dns_endpoint_override'] = dns_override + self.assertEqual( + 'https://compute.example.com/v2.1/', + self.cloud.config.get_endpoint_from_catalog('compute')) + self.assertEqual( + 'https://internal.compute.example.com/v2.1/', + self.cloud.config.get_endpoint_from_catalog( + 'compute', interface='internal')) + self.assertIsNone( + self.cloud.config.get_endpoint_from_catalog( + 'compute', region_name='unknown-region')) + self.assertEqual( + 'https://dns.example.com', + self.cloud.config.get_endpoint_from_catalog('dns')) diff --git a/openstack/tests/unit/fixtures/catalog-v3.json b/openstack/tests/unit/fixtures/catalog-v3.json index 1df1dc196..40bc7bb39 100644 --- a/openstack/tests/unit/fixtures/catalog-v3.json +++ b/openstack/tests/unit/fixtures/catalog-v3.json @@ -11,6 +11,12 @@ "interface": "public", "region": "RegionOne", "url": "https://compute.example.com/v2.1/" + }, + { + "id": "32466f357f3545248c47471ca51b0d3b", + "interface": "internal", + "region": "RegionOne", + "url": "https://internal.compute.example.com/v2.1/" } ], "name": "nova", From 5534590861807b39ec935bedfe5cf749e42936c2 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 7 Aug 2019 15:48:50 -0400 Subject: [PATCH 2531/3836] Replace catalog-v3.json with keystoneauth fixture As a first step to having a reusable fixture, replace our use of catalog-v3.json with a fixture built on top of keystoneauth token fixtures. Make a catalog containing all of the openstack services. There are a bunch of places where we just have some things hardcoded that change with the auto-generation - like bare-metal to baremetal. Those are whatever. There are also places where the test code is requesting an admin url but in the old catalog the admin and non-admin were the same, so we were not catching that this was incorrect. Started to fix this, but it got hairy and it's only a keystone v2 thing which is on lifesupport. Make the admin and public urls the same just for ease of landing this patch. We can go back in and make them different and audit all the v2 keystone codepaths if anyone decides to care about keystone v2. There was a bunch of things hardcoded for block-storage which break when we actually use proxies, which in turn breaks when we try to properly use a better catalog fixture. As a followup, we should stop forcing block_storage_api_version=2 because that's absurd, but this is a big enough patch as it is. Also, volume quota calls were in the image file. Ooops. Change-Id: I308cd159a5b71c94511f86c9d46bdbc589580c6d --- openstack/cloud/_block_storage.py | 204 +++++++++++----- openstack/cloud/_image.py | 54 ----- openstack/cloud/openstackcloud.py | 3 +- openstack/fixture/__init__.py | 0 openstack/fixture/connection.py | 107 +++++++++ .../functional/cloud/test_volume_type.py | 4 +- openstack/tests/unit/base.py | 96 ++++---- openstack/tests/unit/cloud/test_caching.py | 6 +- .../unit/cloud/test_cluster_templates.py | 58 ++--- .../tests/unit/cloud/test_coe_clusters.py | 38 +-- .../cloud/test_coe_clusters_certificate.py | 15 +- .../unit/cloud/test_create_volume_snapshot.py | 4 + .../unit/cloud/test_delete_volume_snapshot.py | 4 + openstack/tests/unit/cloud/test_domains.py | 4 +- openstack/tests/unit/cloud/test_endpoints.py | 2 +- openstack/tests/unit/cloud/test_groups.py | 4 +- .../tests/unit/cloud/test_identity_roles.py | 2 +- openstack/tests/unit/cloud/test_image.py | 8 +- .../tests/unit/cloud/test_magnum_services.py | 4 +- openstack/tests/unit/cloud/test_meta.py | 3 +- openstack/tests/unit/cloud/test_normalize.py | 8 +- .../tests/unit/cloud/test_operator_noauth.py | 32 +-- openstack/tests/unit/cloud/test_project.py | 2 +- openstack/tests/unit/cloud/test_quotas.py | 3 + .../tests/unit/cloud/test_role_assignment.py | 2 +- openstack/tests/unit/cloud/test_services.py | 2 +- openstack/tests/unit/cloud/test_users.py | 2 +- openstack/tests/unit/cloud/test_volume.py | 16 +- .../tests/unit/cloud/test_volume_access.py | 4 + .../tests/unit/cloud/test_volume_backups.py | 4 + openstack/tests/unit/config/test_from_conf.py | 1 + openstack/tests/unit/fixtures/baremetal.json | 4 +- .../unit/fixtures/catalog-bogus-glance.json | 52 ----- openstack/tests/unit/fixtures/catalog-v2.json | 177 -------------- .../unit/fixtures/catalog-v3-fake-v1.json | 71 ------ .../unit/fixtures/catalog-v3-fake-v2.json | 71 ------ .../unit/fixtures/catalog-v3-suburl.json | 198 ---------------- .../unit/fixtures/catalog-v3-suffix.json | 211 ----------------- openstack/tests/unit/fixtures/catalog-v3.json | 217 ------------------ .../tests/unit/fixtures/clouds/clouds.yaml | 1 + openstack/tests/unit/test_connection.py | 33 ++- openstack/tests/unit/test_missing_version.py | 7 +- openstack/tests/unit/test_stats.py | 6 +- 43 files changed, 483 insertions(+), 1261 deletions(-) create mode 100644 openstack/fixture/__init__.py create mode 100644 openstack/fixture/connection.py delete mode 100644 openstack/tests/unit/fixtures/catalog-bogus-glance.json delete mode 100644 openstack/tests/unit/fixtures/catalog-v2.json delete mode 100644 openstack/tests/unit/fixtures/catalog-v3-fake-v1.json delete mode 100644 openstack/tests/unit/fixtures/catalog-v3-fake-v2.json delete mode 100644 openstack/tests/unit/fixtures/catalog-v3-suburl.json delete mode 100644 openstack/tests/unit/fixtures/catalog-v3-suffix.json delete mode 100644 openstack/tests/unit/fixtures/catalog-v3.json diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index d0ede10f8..f78cd6919 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -33,13 +33,6 @@ def _no_pending_volumes(volumes): class BlockStorageCloudMixin(_normalize.Normalizer): - @property - def _volume_client(self): - if 'block-storage' not in self._raw_clients: - client = self._get_raw_client('block-storage') - self._raw_clients['block-storage'] = client - return self._raw_clients['block-storage'] - @_utils.cache_on_arguments(should_cache_fn=_no_pending_volumes) def list_volumes(self, cache=True): """List all available volumes. @@ -56,7 +49,8 @@ def _list(data): break if endpoint: try: - _list(self._volume_client.get(endpoint)) + _list(proxy._json_response( + self.block_storage.get(endpoint))) except exc.OpenStackCloudURINotFound: # Catch and re-raise here because we are making recursive # calls and we just have context for the log here @@ -75,7 +69,8 @@ def _list(data): attempts = 5 for _ in range(attempts): volumes = [] - data = self._volume_client.get('/volumes/detail') + data = proxy._json_response( + self.block_storage.get('/volumes/detail')) if 'volumes_links' not in data: # no pagination needed volumes.extend(data.get('volumes', [])) @@ -103,9 +98,11 @@ def list_volume_types(self, get_extra=True): :returns: A list of volume ``munch.Munch``. """ - data = self._volume_client.get( + resp = self.block_storage.get( '/types', - params=dict(is_public='None'), + params=dict(is_public='None')) + data = proxy._json_response( + resp, error_message='Error fetching volume_type list') return self._normalize_volume_types( self._get_and_munchify('volume_types', data)) @@ -141,8 +138,9 @@ def get_volume_by_id(self, id): :param id: ID of the volume. :returns: A volume ``munch.Munch``. """ - data = self._volume_client.get( - '/volumes/{id}'.format(id=id), + resp = self.block_storage.get('/volumes/{id}'.format(id=id)) + data = proxy._json_response( + resp, error_message="Error getting volume with ID {id}".format(id=id) ) volume = self._normalize_volume( @@ -214,9 +212,11 @@ def create_volume( if 'scheduler_hints' in kwargs: payload['OS-SCH-HNT:scheduler_hints'] = kwargs.pop( 'scheduler_hints', None) - data = self._volume_client.post( + resp = self.block_storage.post( '/volumes', - json=dict(payload), + json=dict(payload)) + data = proxy._json_response( + resp, error_message='Error in creating volume') volume = self._get_and_munchify('volume', data) self.list_volumes.invalidate(self) @@ -254,9 +254,11 @@ def update_volume(self, name_or_id, **kwargs): raise exc.OpenStackCloudException( "Volume %s not found." % name_or_id) - data = self._volume_client.put( + resp = self.block_storage.put( '/volumes/{volume_id}'.format(volume_id=volume.id), - json=dict({'volume': kwargs}), + json=dict({'volume': kwargs})) + data = proxy._json_response( + resp, error_message='Error updating volume') self.list_volumes.invalidate(self) @@ -281,9 +283,11 @@ def set_volume_bootable(self, name_or_id, bootable=True): "Volume {name_or_id} does not exist".format( name_or_id=name_or_id)) - self._volume_client.post( + resp = self.block_storage.post( 'volumes/{id}/action'.format(id=volume['id']), - json={'os-set_bootable': {'bootable': bootable}}, + json={'os-set_bootable': {'bootable': bootable}}) + proxy._json_response( + resp, error_message="Error setting bootable on volume {volume}".format( volume=volume['id']) ) @@ -315,12 +319,12 @@ def delete_volume(self, name_or_id=None, wait=True, timeout=None, with _utils.shade_exceptions("Error in deleting volume"): try: if force: - self._volume_client.post( + proxy._json_response(self.block_storage.post( 'volumes/{id}/action'.format(id=volume['id']), - json={'os-force_delete': None}) + json={'os-force_delete': None})) else: - self._volume_client.delete( - 'volumes/{id}'.format(id=volume['id'])) + proxy._json_response(self.block_storage.delete( + 'volumes/{id}'.format(id=volume['id']))) except exc.OpenStackCloudURINotFound: self.log.debug( "Volume {id} not found when deleting. Ignoring.".format( @@ -368,7 +372,8 @@ def get_volume_limits(self, name_or_id=None): error_msg = "{msg} for the project: {project} ".format( msg=error_msg, project=name_or_id) - data = self._volume_client.get('/limits', params=params) + data = proxy._json_response( + self.block_storage.get('/limits', params=params)) limits = self._get_and_munchify('limits', data) return limits @@ -516,12 +521,12 @@ def _get_volume_kwargs(self, kwargs): description = kwargs.pop('description', kwargs.pop('display_description', None)) if name: - if self._is_client_version('volume', 2): + if self.block_storage._version_matches(2): kwargs['name'] = name else: kwargs['display_name'] = name if description: - if self._is_client_version('volume', 2): + if self.block_storage._version_matches(2): kwargs['description'] = description else: kwargs['display_description'] = description @@ -553,9 +558,11 @@ def create_volume_snapshot(self, volume_id, force=False, kwargs = self._get_volume_kwargs(kwargs) payload = {'volume_id': volume_id, 'force': force} payload.update(kwargs) - data = self._volume_client.post( + resp = self.block_storage.post( '/snapshots', - json=dict(snapshot=payload), + json=dict(snapshot=payload)) + data = proxy._json_response( + resp, error_message="Error creating snapshot of volume " "{volume_id}".format(volume_id=volume_id)) snapshot = self._get_and_munchify('snapshot', data) @@ -588,8 +595,10 @@ def get_volume_snapshot_by_id(self, snapshot_id): param: snapshot_id: ID of the volume snapshot. """ - data = self._volume_client.get( - '/snapshots/{snapshot_id}'.format(snapshot_id=snapshot_id), + resp = self.block_storage.get( + '/snapshots/{snapshot_id}'.format(snapshot_id=snapshot_id)) + data = proxy._json_response( + resp, error_message="Error getting snapshot " "{snapshot_id}".format(snapshot_id=snapshot_id)) return self._normalize_volume( @@ -647,8 +656,10 @@ def create_volume_backup(self, volume_id, name=None, description=None, 'force': force, } - data = self._volume_client.post( - '/backups', json=dict(backup=payload), + resp = self.block_storage.post( + '/backups', json=dict(backup=payload)) + data = proxy._json_response( + resp, error_message="Error creating backup of volume " "{volume_id}".format(volume_id=volume_id)) backup = self._get_and_munchify('backup', data) @@ -686,9 +697,11 @@ def list_volume_snapshots(self, detailed=True, search_opts=None): """ endpoint = '/snapshots/detail' if detailed else '/snapshots' - data = self._volume_client.get( + resp = self.block_storage.get( endpoint, - params=search_opts, + params=search_opts) + data = proxy._json_response( + resp, error_message="Error getting a list of snapshots") return self._get_and_munchify('snapshots', data) @@ -710,8 +723,10 @@ def list_volume_backups(self, detailed=True, search_opts=None): :returns: A list of volume backups ``munch.Munch``. """ endpoint = '/backups/detail' if detailed else '/backups' - data = self._volume_client.get( - endpoint, params=search_opts, + resp = self.block_storage.get( + endpoint, params=search_opts) + data = proxy._json_response( + resp, error_message="Error getting a list of backups") return self._get_and_munchify('backups', data) @@ -736,16 +751,15 @@ def delete_volume_backup(self, name_or_id=None, force=False, wait=False, msg = "Error in deleting volume backup" if force: - self._volume_client.post( + resp = self.block_storage.post( '/backups/{backup_id}/action'.format( backup_id=volume_backup['id']), - json={'os-force_delete': None}, - error_message=msg) + json={'os-force_delete': None}) else: - self._volume_client.delete( + resp = self.block_storage.delete( '/backups/{backup_id}'.format( - backup_id=volume_backup['id']), - error_message=msg) + backup_id=volume_backup['id'])) + proxy._json_response(resp, error_message=msg) if wait: msg = "Timeout waiting for the volume backup to be deleted." for count in utils.iterate_timeout(timeout, msg): @@ -772,9 +786,11 @@ def delete_volume_snapshot(self, name_or_id=None, wait=False, if not volumesnapshot: return False - self._volume_client.delete( + resp = self.block_storage.delete( '/snapshots/{snapshot_id}'.format( - snapshot_id=volumesnapshot['id']), + snapshot_id=volumesnapshot['id'])) + proxy._json_response( + resp, error_message="Error in deleting volume snapshot") if wait: @@ -818,8 +834,10 @@ def get_volume_type_access(self, name_or_id): raise exc.OpenStackCloudException( "VolumeType not found: %s" % name_or_id) - data = self._volume_client.get( - '/types/{id}/os-volume-type-access'.format(id=volume_type.id), + resp = self.block_storage.get( + '/types/{id}/os-volume-type-access'.format(id=volume_type.id)) + data = proxy._json_response( + resp, error_message="Unable to get volume type access" " {name}".format(name=name_or_id)) return self._normalize_volume_type_accesses( @@ -839,14 +857,15 @@ def add_volume_type_access(self, name_or_id, project_id): if not volume_type: raise exc.OpenStackCloudException( "VolumeType not found: %s" % name_or_id) - with _utils.shade_exceptions(): - payload = {'project': project_id} - self._volume_client.post( - '/types/{id}/action'.format(id=volume_type.id), - json=dict(addProjectAccess=payload), - error_message="Unable to authorize {project} " - "to use volume type {name}".format( - name=name_or_id, project=project_id)) + payload = {'project': project_id} + resp = self.block_storage.post( + '/types/{id}/action'.format(id=volume_type.id), + json=dict(addProjectAccess=payload)) + proxy._json_response( + resp, + error_message="Unable to authorize {project} " + "to use volume type {name}".format( + name=name_or_id, project=project_id)) def remove_volume_type_access(self, name_or_id, project_id): """Revoke access on a volume_type to a project. @@ -860,11 +879,72 @@ def remove_volume_type_access(self, name_or_id, project_id): if not volume_type: raise exc.OpenStackCloudException( "VolumeType not found: %s" % name_or_id) - with _utils.shade_exceptions(): - payload = {'project': project_id} - self._volume_client.post( - '/types/{id}/action'.format(id=volume_type.id), - json=dict(removeProjectAccess=payload), - error_message="Unable to revoke {project} " - "to use volume type {name}".format( - name=name_or_id, project=project_id)) + payload = {'project': project_id} + resp = self.block_storage.post( + '/types/{id}/action'.format(id=volume_type.id), + json=dict(removeProjectAccess=payload)) + proxy._json_response( + resp, + error_message="Unable to revoke {project} " + "to use volume type {name}".format( + name=name_or_id, project=project_id)) + + def set_volume_quotas(self, name_or_id, **kwargs): + """ Set a volume quota in a project + + :param name_or_id: project name or id + :param kwargs: key/value pairs of quota name and quota value + + :raises: OpenStackCloudException if the resource to set the + quota does not exist. + """ + + proj = self.get_project(name_or_id) + if not proj: + raise exc.OpenStackCloudException("project does not exist") + + kwargs['tenant_id'] = proj.id + resp = self.block_storage.put( + '/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id), + json={'quota_set': kwargs}) + proxy._json_response( + resp, + error_message="No valid quota or resource") + + def get_volume_quotas(self, name_or_id): + """ Get volume quotas for a project + + :param name_or_id: project name or id + :raises: OpenStackCloudException if it's not a valid project + + :returns: Munch object with the quotas + """ + proj = self.get_project(name_or_id) + if not proj: + raise exc.OpenStackCloudException("project does not exist") + + resp = self.block_storage.get( + '/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id)) + data = proxy._json_response( + resp, + error_message="cinder client call failed") + return self._get_and_munchify('quota_set', data) + + def delete_volume_quotas(self, name_or_id): + """ Delete volume quotas for a project + + :param name_or_id: project name or id + :raises: OpenStackCloudException if it's not a valid project or the + cinder client call failed + + :returns: dict with the quotas + """ + proj = self.get_project(name_or_id) + if not proj: + raise exc.OpenStackCloudException("project does not exist") + + resp = self.block_storage.delete( + '/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id)) + return proxy._json_response( + resp, + error_message="cinder client call failed") diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py index 90872bc70..53265d9e1 100644 --- a/openstack/cloud/_image.py +++ b/openstack/cloud/_image.py @@ -328,57 +328,3 @@ def update_image_properties( image = image or name_or_id return self.image.update_image_properties( image=image, meta=meta, **properties) - - def set_volume_quotas(self, name_or_id, **kwargs): - """ Set a volume quota in a project - - :param name_or_id: project name or id - :param kwargs: key/value pairs of quota name and quota value - - :raises: OpenStackCloudException if the resource to set the - quota does not exist. - """ - - proj = self.get_project(name_or_id) - if not proj: - raise exc.OpenStackCloudException("project does not exist") - - kwargs['tenant_id'] = proj.id - self._volume_client.put( - '/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id), - json={'quota_set': kwargs}, - error_message="No valid quota or resource") - - def get_volume_quotas(self, name_or_id): - """ Get volume quotas for a project - - :param name_or_id: project name or id - :raises: OpenStackCloudException if it's not a valid project - - :returns: Munch object with the quotas - """ - proj = self.get_project(name_or_id) - if not proj: - raise exc.OpenStackCloudException("project does not exist") - - data = self._volume_client.get( - '/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id), - error_message="cinder client call failed") - return self._get_and_munchify('quota_set', data) - - def delete_volume_quotas(self, name_or_id): - """ Delete volume quotas for a project - - :param name_or_id: project name or id - :raises: OpenStackCloudException if it's not a valid project or the - cinder client call failed - - :returns: dict with the quotas - """ - proj = self.get_project(name_or_id) - if not proj: - raise exc.OpenStackCloudException("project does not exist") - - return self._volume_client.delete( - '/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id), - error_message="cinder client call failed") diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index dfc8af553..8f59bc19b 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -453,7 +453,8 @@ def _get_raw_client( region_name=self.config.get_region_name(service_type)) def _is_client_version(self, client, version): - client_name = '_{client}_client'.format(client=client) + client_name = '_{client}_client'.format( + client=client.replace('-', '_')) client = getattr(self, client_name) return client._version_matches(version) diff --git a/openstack/fixture/__init__.py b/openstack/fixture/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/fixture/connection.py b/openstack/fixture/connection.py new file mode 100644 index 000000000..52ee4b12e --- /dev/null +++ b/openstack/fixture/connection.py @@ -0,0 +1,107 @@ +# Copyright 2019 Red Hat, 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 uuid + +import fixtures +from keystoneauth1.fixture import v2 +from keystoneauth1.fixture import v3 +import os_service_types + +_service_type_manager = os_service_types.ServiceTypes() + +_SUBURL_TEMPLATES = { + 'public': 'https://example.com/{service_type}', + 'internal': 'https://internal.example.com/{service_type}', + 'admin': 'https://example.com/{service_type}', +} +_ENDPOINT_TEMPLATES = { + 'public': 'https://{service_type}.example.com', + 'internal': 'https://internal.{service_type}.example.com', + 'admin': 'https://{service_type}.example.com', +} + + +class ConnectionFixture(fixtures.Fixture): + + _suffixes = { + 'baremetal': '/', + 'block-storage': '/{project_id}', + 'compute': '/v2.1/', + 'container-infrastructure-management': '/v1', + 'object-store': '/v1/{project_id}', + 'orchestration': '/v1/{project_id}', + 'volumev2': '/v2/{project_id}', + 'volumev3': '/v3/{project_id}', + } + + def __init__(self, suburl=False, project_id=None, *args, **kwargs): + super(ConnectionFixture, self).__init__(*args, **kwargs) + self._endpoint_templates = _ENDPOINT_TEMPLATES + if suburl: + self.use_suburl() + self.project_id = project_id or uuid.uuid4().hex.replace('-', '') + self.build_tokens() + + def use_suburl(self): + self._endpoint_templates = _SUBURL_TEMPLATES + + def _get_endpoint_templates(self, service_type, alias=None, v2=False): + templates = {} + for k, v in self._endpoint_templates.items(): + suffix = self._suffixes.get( + alias, self._suffixes.get(service_type, '')) + # For a keystone v2 catalog, we want to list the + # versioned endpoint in the catalog, because that's + # more likely how those were deployed. + if v2: + suffix = '/v2.0' + templates[k] = (v + suffix).format( + service_type=service_type, + project_id=self.project_id, + ) + return templates + + def _setUp(self): + pass + + def clear_tokens(self): + self.v2_token = v2.Token(tenant_id=self.project_id) + self.v3_token = v3.Token(project_id=self.project_id) + + def build_tokens(self): + self.clear_tokens() + for service in _service_type_manager.services: + service_type = service['service_type'] + if service_type == 'ec2-api': + continue + service_name = service['project'] + ets = self._get_endpoint_templates(service_type) + v3_svc = self.v3_token.add_service( + service_type, name=service_name) + v2_svc = self.v2_token.add_service( + service_type, name=service_name) + v3_svc.add_standard_endpoints(region='RegionOne', **ets) + if service_type == 'identity': + ets = self._get_endpoint_templates(service_type, v2=True) + v2_svc.add_endpoint(region='RegionOne', **ets) + for alias in service.get('aliases', []): + ets = self._get_endpoint_templates(service_type, alias=alias) + v3_svc = self.v3_token.add_service(alias, name=service_name) + v2_svc = self.v2_token.add_service(alias, name=service_name) + v3_svc.add_standard_endpoints(region='RegionOne', **ets) + v2_svc.add_endpoint(region='RegionOne', **ets) + + def _cleanup(self): + pass diff --git a/openstack/tests/functional/cloud/test_volume_type.py b/openstack/tests/functional/cloud/test_volume_type.py index 1eda9f8aa..ce0d553e5 100644 --- a/openstack/tests/functional/cloud/test_volume_type.py +++ b/openstack/tests/functional/cloud/test_volume_type.py @@ -38,13 +38,13 @@ def setUp(self): "name": 'test-volume-type', "description": None, "os-volume-type-access:is_public": False} - self.operator_cloud._volume_client.post( + self.operator_cloud.block_storage.post( '/types', json={'volume_type': volume_type}) def tearDown(self): ret = self.operator_cloud.get_volume_type('test-volume-type') if ret.get('id'): - self.operator_cloud._volume_client.delete( + self.operator_cloud.block_storage.delete( '/types/{volume_type_id}'.format(volume_type_id=ret.id)) super(TestVolumeType, self).tearDown() diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 9a439d451..78758eee9 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -29,6 +29,8 @@ import openstack.cloud import openstack.connection +from openstack.tests import fakes +from openstack.fixture import connection as os_fixture from openstack.tests import base @@ -101,6 +103,8 @@ def _nosleep(seconds): 'time.sleep', _nosleep)) self.fixtures_directory = 'openstack/tests/unit/fixtures' + self.os_fixture = self.useFixture( + os_fixture.ConnectionFixture(project_id=fakes.PROJECT_ID)) # Isolate openstack.config from test environment config = tempfile.NamedTemporaryFile(delete=False) @@ -422,45 +426,42 @@ def use_nothing(self): def get_keystone_v3_token( self, - catalog='catalog-v3.json', project_name='admin', ): - catalog_file = os.path.join(self.fixtures_directory, catalog) - with open(catalog_file, 'r') as tokens_file: - return dict( - method='POST', - uri='https://identity.example.com/v3/auth/tokens', - headers={ - 'X-Subject-Token': self.getUniqueString('KeystoneToken') - }, - text=tokens_file.read(), - validate=dict(json={ - 'auth': { - 'identity': { - 'methods': ['password'], - 'password': { - 'user': { - 'domain': { - 'name': 'default', - }, - 'name': 'admin', - 'password': 'password' - } - } - }, - 'scope': { - 'project': { + return dict( + method='POST', + uri='https://identity.example.com/v3/auth/tokens', + headers={ + 'X-Subject-Token': self.getUniqueString('KeystoneToken') + }, + json=self.os_fixture.v3_token, + validate=dict(json={ + 'auth': { + 'identity': { + 'methods': ['password'], + 'password': { + 'user': { 'domain': { - 'name': 'default' + 'name': 'default', }, - 'name': project_name + 'name': 'admin', + 'password': 'password' } } + }, + 'scope': { + 'project': { + 'domain': { + 'name': 'default' + }, + 'name': project_name + } } - }), - ) + } + }), + ) - def get_keystone_v3_discovery(self): + def get_keystone_discovery(self): with open(self.discovery_json, 'r') as discovery_file: return dict( method='GET', @@ -468,13 +469,13 @@ def get_keystone_v3_discovery(self): text=discovery_file.read(), ) - def use_keystone_v3(self, catalog='catalog-v3.json'): + def use_keystone_v3(self): self.adapter = self.useFixture(rm_fixture.Fixture()) self.calls = [] self._uri_registry.clear() self.__do_register_uris([ - self.get_keystone_v3_discovery(), - self.get_keystone_v3_token(catalog), + self.get_keystone_discovery(), + self.get_keystone_v3_token(), ]) self._make_test_cloud(identity_api_version='3') @@ -483,18 +484,13 @@ def use_keystone_v2(self): self.calls = [] self._uri_registry.clear() - with open(self.discovery_json, 'r') as discovery_file, \ - open(os.path.join( - self.fixtures_directory, - 'catalog-v2.json'), 'r') as tokens_file: - self.__do_register_uris([ - dict(method='GET', uri='https://identity.example.com/', - text=discovery_file.read()), - dict(method='POST', - uri='https://identity.example.com/v2.0/tokens', - text=tokens_file.read() - ), - ]) + self.__do_register_uris([ + self.get_keystone_discovery(), + dict(method='POST', + uri='https://identity.example.com/v2.0/tokens', + json=self.os_fixture.v2_token, + ), + ]) self._make_test_cloud(cloud_name='_test_cloud_v2_', identity_api_version='2.0') @@ -509,7 +505,7 @@ def _make_test_cloud(self, cloud_name='_test_cloud_', **kwargs): def get_cinder_discovery_mock_dict( self, block_storage_version_json='block-storage-version.json', - block_storage_discovery_url='https://volume.example.com/'): + block_storage_discovery_url='https://block-storage.example.com/'): discovery_fixture = os.path.join( self.fixtures_directory, block_storage_version_json) return dict(method='GET', uri=block_storage_discovery_url, @@ -551,7 +547,7 @@ def get_designate_discovery_mock_dict(self): def get_ironic_discovery_mock_dict(self): discovery_fixture = os.path.join( self.fixtures_directory, "baremetal.json") - return dict(method='GET', uri="https://bare-metal.example.com/", + return dict(method='GET', uri="https://baremetal.example.com/", text=open(discovery_fixture, 'r').read()) def get_senlin_discovery_mock_dict(self): @@ -580,6 +576,10 @@ def use_glance( self.get_glance_discovery_mock_dict( image_version_json, image_discovery_url)]) + def use_cinder(self): + self.__do_register_uris([ + self.get_cinder_discovery_mock_dict()]) + def use_placement(self): self.__do_register_uris([ self.get_placement_discovery_mock_dict()]) diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py index 4e769a6cb..f4e79ee65 100644 --- a/openstack/tests/unit/cloud/test_caching.py +++ b/openstack/tests/unit/cloud/test_caching.py @@ -117,7 +117,7 @@ def test_list_projects_v3(self): for p in project_list]} mock_uri = self.get_mock_url( - service_type='identity', interface='admin', resource='projects', + service_type='identity', resource='projects', base_url_append='v3') self.register_uris([ @@ -206,6 +206,7 @@ def test_list_volumes(self): 'Volume 2 Display Name') fake_volume2_dict = meta.obj_to_munch(fake_volume2) self.register_uris([ + self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', 'detail']), @@ -236,6 +237,7 @@ def test_list_volumes_creating_invalidates(self): 'Volume 2 Display Name') fake_volume2_dict = meta.obj_to_munch(fake_volume2) self.register_uris([ + self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', 'detail']), @@ -266,6 +268,7 @@ def now_deleting(request, context): fake_vol_avail['status'] = 'deleting' self.register_uris([ + self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', 'detail']), @@ -323,7 +326,6 @@ def test_list_users(self): dict(method='GET', uri=self.get_mock_url( service_type='identity', - interface='admin', resource='users', base_url_append='v3'), status_code=200, diff --git a/openstack/tests/unit/cloud/test_cluster_templates.py b/openstack/tests/unit/cloud/test_cluster_templates.py index 2e9f03be4..e0dc2c31b 100644 --- a/openstack/tests/unit/cloud/test_cluster_templates.py +++ b/openstack/tests/unit/cloud/test_cluster_templates.py @@ -51,16 +51,24 @@ class TestClusterTemplates(base.TestCase): + def get_mock_url( + self, + service_type='container-infrastructure-management', + base_url_append=None, append=None, resource=None): + return super(TestClusterTemplates, self).get_mock_url( + service_type=service_type, resource=resource, + append=append, base_url_append=base_url_append) + def test_list_cluster_templates_without_detail(self): self.register_uris([ dict( method='GET', - uri='https://container-infra.example.com/v1/clustertemplates', + uri=self.get_mock_url(resource='clustertemplates'), status_code=404), dict( method='GET', - uri='https://container-infra.example.com/v1/baymodels/detail', + uri=self.get_mock_url(resource='baymodels/detail'), json=dict(baymodels=[cluster_template_obj.toDict()]))]) cluster_templates_list = self.cloud.list_cluster_templates() self.assertEqual( @@ -72,11 +80,11 @@ def test_list_cluster_templates_with_detail(self): self.register_uris([ dict( method='GET', - uri='https://container-infra.example.com/v1/clustertemplates', + uri=self.get_mock_url(resource='clustertemplates'), status_code=404), dict( method='GET', - uri='https://container-infra.example.com/v1/baymodels/detail', + uri=self.get_mock_url(resource='baymodels/detail'), json=dict(baymodels=[cluster_template_obj.toDict()]))]) cluster_templates_list = self.cloud.list_cluster_templates(detail=True) self.assertEqual( @@ -88,11 +96,11 @@ def test_search_cluster_templates_by_name(self): self.register_uris([ dict( method='GET', - uri='https://container-infra.example.com/v1/clustertemplates', + uri=self.get_mock_url(resource='clustertemplates'), status_code=404), dict( method='GET', - uri='https://container-infra.example.com/v1/baymodels/detail', + uri=self.get_mock_url(resource='baymodels/detail'), json=dict(baymodels=[cluster_template_obj.toDict()]))]) cluster_templates = self.cloud.search_cluster_templates( @@ -107,11 +115,11 @@ def test_search_cluster_templates_not_found(self): self.register_uris([ dict( method='GET', - uri='https://container-infra.example.com/v1/clustertemplates', + uri=self.get_mock_url(resource='clustertemplates'), status_code=404), dict( method='GET', - uri='https://container-infra.example.com/v1/baymodels/detail', + uri=self.get_mock_url(resource='baymodels/detail'), json=dict(baymodels=[cluster_template_obj.toDict()]))]) cluster_templates = self.cloud.search_cluster_templates( @@ -124,11 +132,11 @@ def test_get_cluster_template(self): self.register_uris([ dict( method='GET', - uri='https://container-infra.example.com/v1/clustertemplates', + uri=self.get_mock_url(resource='clustertemplates'), status_code=404), dict( method='GET', - uri='https://container-infra.example.com/v1/baymodels/detail', + uri=self.get_mock_url(resource='baymodels/detail'), json=dict(baymodels=[cluster_template_obj.toDict()]))]) r = self.cloud.get_cluster_template('fake-cluster-template') @@ -141,11 +149,11 @@ def test_get_cluster_template_not_found(self): self.register_uris([ dict( method='GET', - uri='https://container-infra.example.com/v1/clustertemplates', + uri=self.get_mock_url(resource='clustertemplates'), status_code=404), dict( method='GET', - uri='https://container-infra.example.com/v1/baymodels/detail', + uri=self.get_mock_url(resource='baymodels/detail'), json=dict(baymodels=[]))]) r = self.cloud.get_cluster_template('doesNotExist') self.assertIsNone(r) @@ -155,11 +163,11 @@ def test_create_cluster_template(self): self.register_uris([ dict( method='POST', - uri='https://container-infra.example.com/v1/clustertemplates', + uri=self.get_mock_url(resource='clustertemplates'), status_code=404), dict( method='POST', - uri='https://container-infra.example.com/v1/baymodels', + uri=self.get_mock_url(resource='baymodels'), json=dict(baymodels=[cluster_template_obj.toDict()]), validate=dict(json={ 'coe': 'fake-coe', @@ -177,11 +185,11 @@ def test_create_cluster_template_exception(self): self.register_uris([ dict( method='POST', - uri='https://container-infra.example.com/v1/clustertemplates', + uri=self.get_mock_url(resource='clustertemplates'), status_code=404), dict( method='POST', - uri='https://container-infra.example.com/v1/baymodels', + uri=self.get_mock_url(resource='baymodels'), status_code=403)]) # TODO(mordred) requests here doens't give us a great story # for matching the old error message text. Investigate plumbing @@ -196,37 +204,35 @@ def test_create_cluster_template_exception(self): self.assert_calls() def test_delete_cluster_template(self): - uri = 'https://container-infra.example.com/v1/baymodels/fake-uuid' self.register_uris([ dict( method='GET', - uri='https://container-infra.example.com/v1/clustertemplates', + uri=self.get_mock_url(resource='clustertemplates'), status_code=404), dict( method='GET', - uri='https://container-infra.example.com/v1/baymodels/detail', + uri=self.get_mock_url(resource='baymodels/detail'), json=dict(baymodels=[cluster_template_obj.toDict()])), dict( method='DELETE', - uri=uri), + uri=self.get_mock_url(resource='baymodels/fake-uuid')), ]) self.cloud.delete_cluster_template('fake-uuid') self.assert_calls() def test_update_cluster_template(self): - uri = 'https://container-infra.example.com/v1/baymodels/fake-uuid' self.register_uris([ dict( method='GET', - uri='https://container-infra.example.com/v1/clustertemplates', + uri=self.get_mock_url(resource='clustertemplates'), status_code=404), dict( method='GET', - uri='https://container-infra.example.com/v1/baymodels/detail', + uri=self.get_mock_url(resource='baymodels/detail'), json=dict(baymodels=[cluster_template_obj.toDict()])), dict( method='PATCH', - uri=uri, + uri=self.get_mock_url(resource='baymodels/fake-uuid'), status_code=200, validate=dict( json=[{ @@ -237,7 +243,7 @@ def test_update_cluster_template(self): )), dict( method='GET', - uri='https://container-infra.example.com/v1/clustertemplates', + uri=self.get_mock_url(resource='clustertemplates'), # This json value is not meaningful to the test - it just has # to be valid. json=dict(baymodels=[cluster_template_obj.toDict()])), @@ -251,7 +257,7 @@ def test_get_coe_cluster_template(self): self.register_uris([ dict( method='GET', - uri='https://container-infra.example.com/v1/clustertemplates', + uri=self.get_mock_url(resource='clustertemplates'), json=dict(clustertemplates=[cluster_template_obj.toDict()]))]) r = self.cloud.get_coe_cluster_template('fake-cluster-template') diff --git a/openstack/tests/unit/cloud/test_coe_clusters.py b/openstack/tests/unit/cloud/test_coe_clusters.py index 1ef38cb4b..aff92a088 100644 --- a/openstack/tests/unit/cloud/test_coe_clusters.py +++ b/openstack/tests/unit/cloud/test_coe_clusters.py @@ -39,11 +39,19 @@ class TestCOEClusters(base.TestCase): + def get_mock_url( + self, + service_type='container-infrastructure-management', + base_url_append=None, append=None, resource=None): + return super(TestCOEClusters, self).get_mock_url( + service_type=service_type, resource=resource, + append=append, base_url_append=base_url_append) + def test_list_coe_clusters(self): self.register_uris([dict( method='GET', - uri='https://container-infra.example.com/v1/clusters', + uri=self.get_mock_url(resource='clusters'), json=dict(clusters=[coe_cluster_obj.toDict()]))]) cluster_list = self.cloud.list_coe_clusters() self.assertEqual( @@ -54,7 +62,7 @@ def test_list_coe_clusters(self): def test_create_coe_cluster(self): self.register_uris([dict( method='POST', - uri='https://container-infra.example.com/v1/clusters', + uri=self.get_mock_url(resource='clusters'), json=dict(baymodels=[coe_cluster_obj.toDict()]), validate=dict(json={ 'name': 'k8s', @@ -72,7 +80,7 @@ def test_create_coe_cluster(self): def test_search_coe_cluster_by_name(self): self.register_uris([dict( method='GET', - uri='https://container-infra.example.com/v1/clusters', + uri=self.get_mock_url(resource='clusters'), json=dict(clusters=[coe_cluster_obj.toDict()]))]) coe_clusters = self.cloud.search_coe_clusters( @@ -86,7 +94,7 @@ def test_search_coe_cluster_not_found(self): self.register_uris([dict( method='GET', - uri='https://container-infra.example.com/v1/clusters', + uri=self.get_mock_url(resource='clusters'), json=dict(clusters=[coe_cluster_obj.toDict()]))]) coe_clusters = self.cloud.search_coe_clusters( @@ -98,7 +106,7 @@ def test_search_coe_cluster_not_found(self): def test_get_coe_cluster(self): self.register_uris([dict( method='GET', - uri='https://container-infra.example.com/v1/clusters', + uri=self.get_mock_url(resource='clusters'), json=dict(clusters=[coe_cluster_obj.toDict()]))]) r = self.cloud.get_coe_cluster(coe_cluster_obj.name) @@ -110,38 +118,38 @@ def test_get_coe_cluster(self): def test_get_coe_cluster_not_found(self): self.register_uris([dict( method='GET', - uri='https://container-infra.example.com/v1/clusters', + uri=self.get_mock_url(resource='clusters'), json=dict(clusters=[]))]) r = self.cloud.get_coe_cluster('doesNotExist') self.assertIsNone(r) self.assert_calls() def test_delete_coe_cluster(self): - uri = ('https://container-infra.example.com/v1/clusters/%s' % - coe_cluster_obj.uuid) self.register_uris([ dict( method='GET', - uri='https://container-infra.example.com/v1/clusters', + uri=self.get_mock_url(resource='clusters'), json=dict(clusters=[coe_cluster_obj.toDict()])), dict( method='DELETE', - uri=uri), + uri=self.get_mock_url( + resource='clusters', + append=[coe_cluster_obj.uuid])), ]) self.cloud.delete_coe_cluster(coe_cluster_obj.uuid) self.assert_calls() def test_update_coe_cluster(self): - uri = ('https://container-infra.example.com/v1/clusters/%s' % - coe_cluster_obj.uuid) self.register_uris([ dict( method='GET', - uri='https://container-infra.example.com/v1/clusters', + uri=self.get_mock_url(resource='clusters'), json=dict(clusters=[coe_cluster_obj.toDict()])), dict( method='PATCH', - uri=uri, + uri=self.get_mock_url( + resource='clusters', + append=[coe_cluster_obj.uuid]), status_code=200, validate=dict( json=[{ @@ -152,7 +160,7 @@ def test_update_coe_cluster(self): )), dict( method='GET', - uri='https://container-infra.example.com/v1/clusters', + uri=self.get_mock_url(resource='clusters'), # This json value is not meaningful to the test - it just has # to be valid. json=dict(clusters=[coe_cluster_obj.toDict()])), diff --git a/openstack/tests/unit/cloud/test_coe_clusters_certificate.py b/openstack/tests/unit/cloud/test_coe_clusters_certificate.py index 88fe7fa37..8e448d549 100644 --- a/openstack/tests/unit/cloud/test_coe_clusters_certificate.py +++ b/openstack/tests/unit/cloud/test_coe_clusters_certificate.py @@ -34,11 +34,20 @@ class TestCOEClusters(base.TestCase): + def get_mock_url( + self, + service_type='container-infrastructure-management', + base_url_append=None, append=None, resource=None): + return super(TestCOEClusters, self).get_mock_url( + service_type=service_type, resource=resource, + append=append, base_url_append=base_url_append) + def test_get_coe_cluster_certificate(self): self.register_uris([dict( method='GET', - uri=('https://container-infra.example.com/v1/certificates/%s' % - coe_cluster_ca_obj.cluster_uuid), + uri=self.get_mock_url( + resource='certificates', + append=[coe_cluster_ca_obj.cluster_uuid]), json=coe_cluster_ca_obj) ]) ca_cert = self.cloud.get_coe_cluster_certificate( @@ -51,7 +60,7 @@ def test_get_coe_cluster_certificate(self): def test_sign_coe_cluster_certificate(self): self.register_uris([dict( method='POST', - uri='https://container-infra.example.com/v1/certificates', + uri=self.get_mock_url(resource='certificates'), json={"cluster_uuid": coe_cluster_signed_cert_obj.cluster_uuid, "csr": coe_cluster_signed_cert_obj.csr} )]) diff --git a/openstack/tests/unit/cloud/test_create_volume_snapshot.py b/openstack/tests/unit/cloud/test_create_volume_snapshot.py index 1986fdb12..bdca61e96 100644 --- a/openstack/tests/unit/cloud/test_create_volume_snapshot.py +++ b/openstack/tests/unit/cloud/test_create_volume_snapshot.py @@ -25,6 +25,10 @@ class TestCreateVolumeSnapshot(base.TestCase): + def setUp(self): + super(TestCreateVolumeSnapshot, self).setUp() + self.use_cinder() + def test_create_volume_snapshot_wait(self): """ Test that create_volume_snapshot with a wait returns the volume diff --git a/openstack/tests/unit/cloud/test_delete_volume_snapshot.py b/openstack/tests/unit/cloud/test_delete_volume_snapshot.py index f3504d6f9..0365b7945 100644 --- a/openstack/tests/unit/cloud/test_delete_volume_snapshot.py +++ b/openstack/tests/unit/cloud/test_delete_volume_snapshot.py @@ -25,6 +25,10 @@ class TestDeleteVolumeSnapshot(base.TestCase): + def setUp(self): + super(TestDeleteVolumeSnapshot, self).setUp() + self.use_cinder() + def test_delete_volume_snapshot(self): """ Test that delete_volume_snapshot without a wait returns True instance diff --git a/openstack/tests/unit/cloud/test_domains.py b/openstack/tests/unit/cloud/test_domains.py index babdbd395..f57b0514d 100644 --- a/openstack/tests/unit/cloud/test_domains.py +++ b/openstack/tests/unit/cloud/test_domains.py @@ -25,10 +25,10 @@ class TestDomains(base.TestCase): def get_mock_url(self, service_type='identity', - interface='admin', resource='domains', + resource='domains', append=None, base_url_append='v3'): return super(TestDomains, self).get_mock_url( - service_type=service_type, interface=interface, resource=resource, + service_type=service_type, resource=resource, append=append, base_url_append=base_url_append) def test_list_domains(self): diff --git a/openstack/tests/unit/cloud/test_endpoints.py b/openstack/tests/unit/cloud/test_endpoints.py index 74d3c5cb7..d05735d5e 100644 --- a/openstack/tests/unit/cloud/test_endpoints.py +++ b/openstack/tests/unit/cloud/test_endpoints.py @@ -29,7 +29,7 @@ class TestCloudEndpoints(base.TestCase): - def get_mock_url(self, service_type='identity', interface='admin', + def get_mock_url(self, service_type='identity', interface='public', resource='endpoints', append=None, base_url_append='v3'): return super(TestCloudEndpoints, self).get_mock_url( service_type, interface, resource, append, base_url_append) diff --git a/openstack/tests/unit/cloud/test_groups.py b/openstack/tests/unit/cloud/test_groups.py index a5f6c336e..52eb8ff68 100644 --- a/openstack/tests/unit/cloud/test_groups.py +++ b/openstack/tests/unit/cloud/test_groups.py @@ -20,10 +20,10 @@ def setUp(self, cloud_config_fixture='clouds.yaml'): cloud_config_fixture=cloud_config_fixture) self.addCleanup(self.assert_calls) - def get_mock_url(self, service_type='identity', interface='admin', + def get_mock_url(self, service_type='identity', interface='public', resource='groups', append=None, base_url_append='v3'): return super(TestGroups, self).get_mock_url( - service_type='identity', interface='admin', resource=resource, + service_type='identity', interface=interface, resource=resource, append=append, base_url_append=base_url_append) def test_list_groups(self): diff --git a/openstack/tests/unit/cloud/test_identity_roles.py b/openstack/tests/unit/cloud/test_identity_roles.py index 3409e7b63..89eeddc1a 100644 --- a/openstack/tests/unit/cloud/test_identity_roles.py +++ b/openstack/tests/unit/cloud/test_identity_roles.py @@ -36,7 +36,7 @@ class TestIdentityRoles(base.TestCase): - def get_mock_url(self, service_type='identity', interface='admin', + def get_mock_url(self, service_type='identity', interface='public', resource='roles', append=None, base_url_append='v3', qs_elements=None): return super(TestIdentityRoles, self).get_mock_url( diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 27f4662bb..78c56c4b4 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -24,9 +24,6 @@ from openstack.tests.unit import base -CINDER_URL = 'https://volume.example.com/v2/1c36b64c840a42cd9e9b931a369337f0' - - class BaseTestImage(base.TestCase): def setUp(self): @@ -1045,7 +1042,9 @@ class TestImageSuburl(BaseTestImage): def setUp(self): super(TestImageSuburl, self).setUp() - self.use_keystone_v3(catalog='catalog-v3-suburl.json') + self.os_fixture.use_suburl() + self.os_fixture.build_tokens() + self.use_keystone_v3() self.use_glance( image_version_json='image-version-suburl.json', image_discovery_url='https://example.com/image') @@ -1142,7 +1141,6 @@ def setUp(self): self.volume_id = str(uuid.uuid4()) def test_create_image_volume(self): - self.register_uris([ self.get_cinder_discovery_mock_dict(), dict(method='POST', diff --git a/openstack/tests/unit/cloud/test_magnum_services.py b/openstack/tests/unit/cloud/test_magnum_services.py index a410201a8..f5dee60e5 100644 --- a/openstack/tests/unit/cloud/test_magnum_services.py +++ b/openstack/tests/unit/cloud/test_magnum_services.py @@ -31,7 +31,9 @@ class TestMagnumServices(base.TestCase): def test_list_magnum_services(self): self.register_uris([dict( method='GET', - uri='https://container-infra.example.com/v1/mservices', + uri=self.get_mock_url( + service_type='container-infrastructure-management', + resource='mservices'), json=dict(mservices=[magnum_service_obj]))]) mservices_list = self.cloud.list_magnum_services() self.assertEqual( diff --git a/openstack/tests/unit/cloud/test_meta.py b/openstack/tests/unit/cloud/test_meta.py index e162e5eb8..0de6620d3 100644 --- a/openstack/tests/unit/cloud/test_meta.py +++ b/openstack/tests/unit/cloud/test_meta.py @@ -991,7 +991,8 @@ def test_basic_hostvars( self.assertIn('location', hostvars) self.assertEqual('_test_cloud_', hostvars['location']['cloud']) self.assertEqual('RegionOne', hostvars['location']['region_name']) - self.assertEqual('admin', hostvars['location']['project']['name']) + self.assertEqual( + fakes.PROJECT_ID, hostvars['location']['project']['id']) self.assertEqual("test-image-name", hostvars['image']['name']) self.assertEqual( standard_fake_server['image']['id'], hostvars['image']['id']) diff --git a/openstack/tests/unit/cloud/test_normalize.py b/openstack/tests/unit/cloud/test_normalize.py index 03fcc188d..228e09548 100644 --- a/openstack/tests/unit/cloud/test_normalize.py +++ b/openstack/tests/unit/cloud/test_normalize.py @@ -667,12 +667,6 @@ def test_normalize_volumes_v1(self): status='in-use', created_at='2015-08-27T09:49:58-05:00', ) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'block-storage', 'public', append=['volumes', 'detail']), - json={'volumes': [vol]}), - ]) expected = { 'attachments': [], 'availability_zone': None, @@ -712,7 +706,7 @@ def test_normalize_volumes_v1(self): 'updated_at': None, 'volume_type': None, } - retval = self.cloud.list_volumes(vol)[0] + retval = self.cloud._normalize_volume(vol) self.assertEqual(expected, retval) def test_normalize_volumes_v2(self): diff --git a/openstack/tests/unit/cloud/test_operator_noauth.py b/openstack/tests/unit/cloud/test_operator_noauth.py index d9faee222..1c098962a 100644 --- a/openstack/tests/unit/cloud/test_operator_noauth.py +++ b/openstack/tests/unit/cloud/test_operator_noauth.py @@ -37,7 +37,7 @@ def setUp(self): uri=self.get_mock_url( service_type='baremetal', base_url_append='v1'), json={'id': 'v1', - 'links': [{"href": "https://bare-metal.example.com/v1", + 'links': [{"href": "https://baremetal.example.com/v1", "rel": "self"}]}), dict(method='GET', uri=self.get_mock_url( @@ -58,7 +58,7 @@ def test_ironic_noauth_none_auth_type(self): # client library. self.cloud_noauth = openstack.connect( auth_type='none', - baremetal_endpoint_override="https://bare-metal.example.com/v1") + baremetal_endpoint_override="https://baremetal.example.com/v1") self.cloud_noauth.list_machines() @@ -73,11 +73,11 @@ def test_ironic_noauth_auth_endpoint(self): clouds: bifrost: auth_type: "none" - endpoint: https://bare-metal.example.com + endpoint: https://baremetal.example.com """ self.cloud_noauth = openstack.connect( auth_type='none', - endpoint='https://bare-metal.example.com/v1', + endpoint='https://baremetal.example.com/v1', ) self.cloud_noauth.list_machines() @@ -92,7 +92,7 @@ def test_ironic_noauth_admin_token_auth_type(self): self.cloud_noauth = openstack.connect( auth_type='admin_token', auth=dict( - endpoint='https://bare-metal.example.com/v1', + endpoint='https://baremetal.example.com/v1', token='ignored')) self.cloud_noauth.list_machines() @@ -118,7 +118,7 @@ def setUp(self): self._uri_registry.clear() self.register_uris([ dict(method='GET', - uri='https://bare-metal.example.com/', + uri='https://baremetal.example.com/', json={ "default_version": { "status": "CURRENT", @@ -126,7 +126,7 @@ def setUp(self): "version": "1.46", "id": "v1", "links": [{ - "href": "https://bare-metal.example.com/v1", + "href": "https://baremetal.example.com/v1", "rel": "self" }]}, "versions": [{ @@ -135,7 +135,7 @@ def setUp(self): "version": "1.46", "id": "v1", "links": [{ - "href": "https://bare-metal.example.com/v1", + "href": "https://baremetal.example.com/v1", "rel": "self" }]}], "name": "OpenStack Ironic API", @@ -150,21 +150,21 @@ def setUp(self): "type": "application/vnd.openstack.ironic.v1+json" }], "links": [{ - "href": "https://bare-metal.example.com/v1", + "href": "https://baremetal.example.com/v1", "rel": "self" }], "ports": [{ - "href": "https://bare-metal.example.com/v1/ports/", + "href": "https://baremetal.example.com/v1/ports/", "rel": "self" }, { - "href": "https://bare-metal.example.com/ports/", + "href": "https://baremetal.example.com/ports/", "rel": "bookmark" }], "nodes": [{ - "href": "https://bare-metal.example.com/v1/nodes/", + "href": "https://baremetal.example.com/v1/nodes/", "rel": "self" }, { - "href": "https://bare-metal.example.com/nodes/", + "href": "https://baremetal.example.com/nodes/", "rel": "bookmark" }], "id": "v1" @@ -188,7 +188,7 @@ def test_ironic_noauth_none_auth_type(self): # client library. self.cloud_noauth = openstack.connect( auth_type='none', - baremetal_endpoint_override="https://bare-metal.example.com") + baremetal_endpoint_override="https://baremetal.example.com") self.cloud_noauth.list_machines() @@ -203,11 +203,11 @@ def test_ironic_noauth_auth_endpoint(self): clouds: bifrost: auth_type: "none" - endpoint: https://bare-metal.example.com + endpoint: https://baremetal.example.com """ self.cloud_noauth = openstack.connect( auth_type='none', - endpoint='https://bare-metal.example.com/', + endpoint='https://baremetal.example.com/', ) self.cloud_noauth.list_machines() diff --git a/openstack/tests/unit/cloud/test_project.py b/openstack/tests/unit/cloud/test_project.py index 4bb337af0..02e534453 100644 --- a/openstack/tests/unit/cloud/test_project.py +++ b/openstack/tests/unit/cloud/test_project.py @@ -20,7 +20,7 @@ class TestProject(base.TestCase): - def get_mock_url(self, service_type='identity', interface='admin', + def get_mock_url(self, service_type='identity', interface='public', resource=None, append=None, base_url_append=None, v3=True): if v3 and resource is None: diff --git a/openstack/tests/unit/cloud/test_quotas.py b/openstack/tests/unit/cloud/test_quotas.py index ade487987..cb42290c1 100644 --- a/openstack/tests/unit/cloud/test_quotas.py +++ b/openstack/tests/unit/cloud/test_quotas.py @@ -109,6 +109,7 @@ def test_cinder_update_quotas(self): project = self.mock_for_keystone_projects(project_count=1, list_get=True)[0] self.register_uris([ + self.get_cinder_discovery_mock_dict(), dict(method='PUT', uri=self.get_mock_url( 'volumev2', 'public', @@ -125,6 +126,7 @@ def test_cinder_get_quotas(self): project = self.mock_for_keystone_projects(project_count=1, list_get=True)[0] self.register_uris([ + self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', @@ -137,6 +139,7 @@ def test_cinder_delete_quotas(self): project = self.mock_for_keystone_projects(project_count=1, list_get=True)[0] self.register_uris([ + self.get_cinder_discovery_mock_dict(), dict(method='DELETE', uri=self.get_mock_url( 'volumev2', 'public', diff --git a/openstack/tests/unit/cloud/test_role_assignment.py b/openstack/tests/unit/cloud/test_role_assignment.py index 10c3079df..2e4b89181 100644 --- a/openstack/tests/unit/cloud/test_role_assignment.py +++ b/openstack/tests/unit/cloud/test_role_assignment.py @@ -84,7 +84,7 @@ def setUp(self, cloud_config_fixture='clouds.yaml'): self.addCleanup(delattr, self, 'user_domain_assignment') self.addCleanup(delattr, self, 'group_domain_assignment') - def get_mock_url(self, service_type='identity', interface='admin', + def get_mock_url(self, service_type='identity', interface='public', resource='role_assignments', append=None, base_url_append='v3', qs_elements=None): return super(TestRoleAssignment, self).get_mock_url( diff --git a/openstack/tests/unit/cloud/test_services.py b/openstack/tests/unit/cloud/test_services.py index c120aaa8a..b718d57f5 100644 --- a/openstack/tests/unit/cloud/test_services.py +++ b/openstack/tests/unit/cloud/test_services.py @@ -30,7 +30,7 @@ class CloudServices(base.TestCase): def setUp(self, cloud_config_fixture='clouds.yaml'): super(CloudServices, self).setUp(cloud_config_fixture) - def get_mock_url(self, service_type='identity', interface='admin', + def get_mock_url(self, service_type='identity', interface='public', resource='services', append=None, base_url_append='v3'): return super(CloudServices, self).get_mock_url( diff --git a/openstack/tests/unit/cloud/test_users.py b/openstack/tests/unit/cloud/test_users.py index c3cd40cb5..44c28c9c3 100644 --- a/openstack/tests/unit/cloud/test_users.py +++ b/openstack/tests/unit/cloud/test_users.py @@ -25,7 +25,7 @@ def _get_keystone_mock_url(self, resource, append=None, v3=True): if v3: base_url_append = 'v3' return self.get_mock_url( - service_type='identity', interface='admin', resource=resource, + service_type='identity', resource=resource, append=append, base_url_append=base_url_append) def _get_user_list(self, user_data): diff --git a/openstack/tests/unit/cloud/test_volume.py b/openstack/tests/unit/cloud/test_volume.py index 72ce1a095..1c5735595 100644 --- a/openstack/tests/unit/cloud/test_volume.py +++ b/openstack/tests/unit/cloud/test_volume.py @@ -88,6 +88,7 @@ def test_attach_volume_wait(self): validate=dict(json={ 'volumeAttachment': { 'volumeId': vol['id']}})), + self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', 'detail']), @@ -120,6 +121,7 @@ def test_attach_volume_wait_error(self): validate=dict(json={ 'volumeAttachment': { 'volumeId': vol['id']}})), + self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', 'detail']), @@ -211,6 +213,7 @@ def test_detach_volume_wait(self): 'compute', 'public', append=['servers', server['id'], 'os-volume_attachments', volume.id])), + self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', 'detail']), @@ -233,6 +236,7 @@ def test_detach_volume_wait_error(self): 'compute', 'public', append=['servers', server['id'], 'os-volume_attachments', volume.id])), + self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', 'detail']), @@ -249,6 +253,7 @@ def test_delete_volume_deletes(self): 'name': '', 'attachments': []} volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) self.register_uris([ + self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', 'detail']), @@ -268,6 +273,7 @@ def test_delete_volume_gone_away(self): 'name': '', 'attachments': []} volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) self.register_uris([ + self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', 'detail']), @@ -284,6 +290,7 @@ def test_delete_volume_force(self): 'name': '', 'attachments': []} volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) self.register_uris([ + self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', 'detail']), @@ -306,6 +313,7 @@ def test_set_volume_bootable(self): 'name': '', 'attachments': []} volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) self.register_uris([ + self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', 'detail']), @@ -324,6 +332,7 @@ def test_set_volume_bootable_false(self): 'name': '', 'attachments': []} volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) self.register_uris([ + self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', 'detail']), @@ -341,6 +350,7 @@ def test_list_volumes_with_pagination(self): vol1 = meta.obj_to_munch(fakes.FakeVolume('01', 'available', 'vol1')) vol2 = meta.obj_to_munch(fakes.FakeVolume('02', 'available', 'vol2')) self.register_uris([ + self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', @@ -382,6 +392,7 @@ def test_list_volumes_with_pagination_next_link_fails_once(self): vol1 = meta.obj_to_munch(fakes.FakeVolume('01', 'available', 'vol1')) vol2 = meta.obj_to_munch(fakes.FakeVolume('02', 'available', 'vol2')) self.register_uris([ + self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', @@ -440,7 +451,7 @@ def test_list_volumes_with_pagination_next_link_fails_once(self): def test_list_volumes_with_pagination_next_link_fails_all_attempts(self): vol1 = meta.obj_to_munch(fakes.FakeVolume('01', 'available', 'vol1')) - uris = [] + uris = [self.get_cinder_discovery_mock_dict()] attempts = 5 for i in range(attempts): uris.extend([ @@ -474,6 +485,7 @@ def test_list_volumes_with_pagination_next_link_fails_all_attempts(self): def test_get_volume_by_id(self): vol1 = meta.obj_to_munch(fakes.FakeVolume('01', 'available', 'vol1')) self.register_uris([ + self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', @@ -489,6 +501,7 @@ def test_get_volume_by_id(self): def test_create_volume(self): vol1 = meta.obj_to_munch(fakes.FakeVolume('01', 'available', 'vol1')) self.register_uris([ + self.get_cinder_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'volumev2', 'public', append=['volumes']), @@ -511,6 +524,7 @@ def test_create_volume(self): def test_create_bootable_volume(self): vol1 = meta.obj_to_munch(fakes.FakeVolume('01', 'available', 'vol1')) self.register_uris([ + self.get_cinder_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'volumev2', 'public', append=['volumes']), diff --git a/openstack/tests/unit/cloud/test_volume_access.py b/openstack/tests/unit/cloud/test_volume_access.py index 02ecc4054..ec83ea4c5 100644 --- a/openstack/tests/unit/cloud/test_volume_access.py +++ b/openstack/tests/unit/cloud/test_volume_access.py @@ -20,6 +20,10 @@ class TestVolumeAccess(base.TestCase): + def setUp(self): + super(TestVolumeAccess, self).setUp() + self.use_cinder() + def test_list_volume_types(self): volume_type = dict( id='voltype01', description='volume type description', diff --git a/openstack/tests/unit/cloud/test_volume_backups.py b/openstack/tests/unit/cloud/test_volume_backups.py index 49479d31e..7bec56429 100644 --- a/openstack/tests/unit/cloud/test_volume_backups.py +++ b/openstack/tests/unit/cloud/test_volume_backups.py @@ -14,6 +14,10 @@ class TestVolumeBackups(base.TestCase): + def setUp(self): + super(TestVolumeBackups, self).setUp() + self.use_cinder() + def test_search_volume_backups(self): name = 'Volume1' vol1 = {'name': name, 'availability_zone': 'az1'} diff --git a/openstack/tests/unit/config/test_from_conf.py b/openstack/tests/unit/config/test_from_conf.py index 49b8e4a68..918661d94 100644 --- a/openstack/tests/unit/config/test_from_conf.py +++ b/openstack/tests/unit/config/test_from_conf.py @@ -194,6 +194,7 @@ def test_no_session(self): def test_no_endpoint(self): """Conf contains adapter opts, but service type not in catalog.""" + self.os_fixture.v3_token.remove_service('monitoring') conn = self._get_conn() # Monasca is not in the service catalog self.assertRaises(ks_exc.catalog.EndpointNotFound, diff --git a/openstack/tests/unit/fixtures/baremetal.json b/openstack/tests/unit/fixtures/baremetal.json index fa0a9e7a7..4712764fd 100644 --- a/openstack/tests/unit/fixtures/baremetal.json +++ b/openstack/tests/unit/fixtures/baremetal.json @@ -3,7 +3,7 @@ "id": "v1", "links": [ { - "href": "https://bare-metal.example.com/v1/", + "href": "https://baremetal.example.com/v1/", "rel": "self" } ], @@ -18,7 +18,7 @@ "id": "v1", "links": [ { - "href": "https://bare-metal.example.com/v1/", + "href": "https://baremetal.example.com/v1/", "rel": "self" } ], diff --git a/openstack/tests/unit/fixtures/catalog-bogus-glance.json b/openstack/tests/unit/fixtures/catalog-bogus-glance.json deleted file mode 100644 index 3a47d3f9e..000000000 --- a/openstack/tests/unit/fixtures/catalog-bogus-glance.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "token": { - "audit_ids": [ - "Rvn7eHkiSeOwucBIPaKdYA" - ], - "catalog": [ - { - "endpoints": [ - { - "id": "32466f357f3545248c47471ca51b0d3a", - "interface": "public", - "region": "RegionOne", - "url": "https://example.com/image/" - } - ], - "name": "glance", - "type": "image" - } - ], - "expires_at": "9999-12-31T23:59:59Z", - "issued_at": "2016-12-17T14:25:05.000000Z", - "methods": [ - "password" - ], - "project": { - "domain": { - "id": "default", - "name": "default" - }, - "id": "1c36b64c840a42cd9e9b931a369337f0", - "name": "Default Project" - }, - "roles": [ - { - "id": "9fe2ff9ee4384b1894a90878d3e92bab", - "name": "_member_" - }, - { - "id": "37071fc082e14c2284c32a2761f71c63", - "name": "swiftoperator" - } - ], - "user": { - "domain": { - "id": "default", - "name": "default" - }, - "id": "c17534835f8f42bf98fc367e0bf35e09", - "name": "mordred" - } - } -} diff --git a/openstack/tests/unit/fixtures/catalog-v2.json b/openstack/tests/unit/fixtures/catalog-v2.json deleted file mode 100644 index 54cc2b064..000000000 --- a/openstack/tests/unit/fixtures/catalog-v2.json +++ /dev/null @@ -1,177 +0,0 @@ -{ - "access": { - "token": { - "issued_at": "2016-04-14T10:09:58.014014Z", - "expires": "9999-12-31T23:59:59Z", - "id": "7fa3037ae2fe48ada8c626a51dc01ffd", - "tenant": { - "enabled": true, - "description": "Bootstrap project for initializing the cloud.", - "name": "admin", - "id": "1c36b64c840a42cd9e9b931a369337f0" - }, - "audit_ids": [ - "FgG3Q8T3Sh21r_7HyjHP8A" - ] - }, - "serviceCatalog": [ - { - "endpoints_links": [], - "endpoints": [ - { - "adminURL": "https://compute.example.com/v2.1/1c36b64c840a42cd9e9b931a369337f0", - "region": "RegionOne", - "publicURL": "https://compute.example.com/v2.1/1c36b64c840a42cd9e9b931a369337f0", - "internalURL": "https://compute.example.com/v2.1/1c36b64c840a42cd9e9b931a369337f0", - "id": "32466f357f3545248c47471ca51b0d3a" - } - ], - "type": "compute", - "name": "nova" - }, - { - "endpoints_links": [], - "endpoints": [ - { - "adminURL": "https://volume.example.com/v2/1c36b64c840a42cd9e9b931a369337f0", - "region": "RegionOne", - "publicURL": "https://volume.example.com/v2/1c36b64c840a42cd9e9b931a369337f0", - "internalURL": "https://volume.example.com/v2/1c36b64c840a42cd9e9b931a369337f0", - "id": "1e875ca2225b408bbf3520a1b8e1a537" - } - ], - "type": "volumev2", - "name": "cinderv2" - }, - { - "endpoints_links": [], - "endpoints": [ - { - "adminURL": "https://image.example.com/v2", - "region": "RegionOne", - "publicURL": "https://image.example.com/v2", - "internalURL": "https://image.example.com/v2", - "id": "5a64de3c4a614d8d8f8d1ba3dee5f45f" - } - ], - "type": "image", - "name": "glance" - }, - { - "endpoints_links": [], - "endpoints": [ - { - "adminURL": "https://volume.example.com/v1/1c36b64c840a42cd9e9b931a369337f0", - "region": "RegionOne", - "publicURL": "https://volume.example.com/v1/1c36b64c840a42cd9e9b931a369337f0", - "internalURL": "https://volume.example.com/v1/1c36b64c840a42cd9e9b931a369337f0", - "id": "3d15fdfc7d424f3c8923324417e1a3d1" - } - ], - "type": "volume", - "name": "cinder" - }, - { - "endpoints_links": [], - "endpoints": [ - { - "adminURL": "https://identity.example.com/v2.0", - "region": "RegionOne", - "publicURL": "https://identity.example.com/v2.0", - "internalURL": "https://identity.example.com/v2.0", - "id": "4deb4d0504a044a395d4480741ba628c" - } - ], - "type": "identity", - "name": "keystone" - }, - { - "endpoints_links": [], - "endpoints": [ - { - "adminURL": "https://network.example.com", - "region": "RegionOne", - "publicURL": "https://network.example.com", - "internalURL": "https://network.example.com", - "id": "4deb4d0504a044a395d4480741ba628d" - } - ], - "type": "network", - "name": "neutron" - }, - { - "endpoints_links": [], - "endpoints": [ - { - "adminURL": "https://object-store.example.com/v1/1c36b64c840a42cd9e9b931a369337f0", - "region": "RegionOne", - "publicURL": "https://object-store.example.com/v1/1c36b64c840a42cd9e9b931a369337f0", - "internalURL": "https://object-store.example.com/v1/1c36b64c840a42cd9e9b931a369337f0", - "id": "4deb4d0504a044a395d4480741ba628c" - } - ], - "type": "object-store", - "name": "swift" - }, - { - "endpoints_links": [], - "endpoints": [ - { - "adminURL": "https://placement.example.com", - "region": "RegionOne", - "publicURL": "https://placement.example.com", - "internalURL": "https://placement.example.com", - "id": "652f0612744042bfbb8a8bb2c777a16e" - } - ], - "type": "placement", - "name": "placement" - }, - { - "endpoints_links": [], - "endpoints": [ - { - "adminURL": "https://dns.example.com", - "region": "RegionOne", - "publicURL": "https://dns.example.com", - "internalURL": "https://dns.example.com", - "id": "652f0612744042bfbb8a8bb2c777a16d" - } - ], - "type": "dns", - "name": "designate" - }, - { - "endpoints_links": [], - "endpoints": [ - { - "adminURL": "https://clustering.example.com", - "region": "RegionOne", - "publicURL": "https://clustering.example.com", - "internalURL": "https://clustering.example.com", - "id": "4deb4d0504a044a395d4480741ba624z" - } - ], - "type": "clustering", - "name": "senlin" - } - ], - "user": { - "username": "dummy", - "roles_links": [], - "id": "71675f719c3343e8ac441cc28f396474", - "roles": [ - { - "name": "admin" - } - ], - "name": "admin" - }, - "metadata": { - "is_admin": 0, - "roles": [ - "6d813db50b6e4a1ababdbbb5a83c7de5" - ] - } - } -} diff --git a/openstack/tests/unit/fixtures/catalog-v3-fake-v1.json b/openstack/tests/unit/fixtures/catalog-v3-fake-v1.json deleted file mode 100644 index c32617577..000000000 --- a/openstack/tests/unit/fixtures/catalog-v3-fake-v1.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "token": { - "audit_ids": [ - "Rvn7eHkiSeOwucBIPaKdYA" - ], - "catalog": [ - { - "endpoints": [ - { - "id": "4deb4d0504a044a395d4480741ba628c", - "interface": "public", - "region": "RegionOne", - "url": "https://identity.example.com" - }, - { - "id": "012322eeedcd459edabb4933021112bc", - "interface": "admin", - "region": "RegionOne", - "url": "https://identity.example.com" - } - ], - "endpoints_links": [], - "name": "keystone", - "type": "identity" - }, - { - "endpoints": [ - { - "id": "1e875ca2225b408bbf3520a1b8e1a537", - "interface": "public", - "region": "RegionOne", - "url": "https://fake.example.com/v1/1c36b64c840a42cd9e9b931a369337f0" - } - ], - "name": "fake_service", - "type": "fake" - } - ], - "expires_at": "9999-12-31T23:59:59Z", - "issued_at": "2016-12-17T14:25:05.000000Z", - "methods": [ - "password" - ], - "project": { - "domain": { - "id": "default", - "name": "default" - }, - "id": "1c36b64c840a42cd9e9b931a369337f0", - "name": "Default Project" - }, - "roles": [ - { - "id": "9fe2ff9ee4384b1894a90878d3e92bab", - "name": "_member_" - }, - { - "id": "37071fc082e14c2284c32a2761f71c63", - "name": "swiftoperator" - } - ], - "user": { - "domain": { - "id": "default", - "name": "default" - }, - "id": "c17534835f8f42bf98fc367e0bf35e09", - "name": "mordred" - } - } -} diff --git a/openstack/tests/unit/fixtures/catalog-v3-fake-v2.json b/openstack/tests/unit/fixtures/catalog-v3-fake-v2.json deleted file mode 100644 index 9ce0b7f50..000000000 --- a/openstack/tests/unit/fixtures/catalog-v3-fake-v2.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "token": { - "audit_ids": [ - "Rvn7eHkiSeOwucBIPaKdYA" - ], - "catalog": [ - { - "endpoints": [ - { - "id": "4deb4d0504a044a395d4480741ba628c", - "interface": "public", - "region": "RegionOne", - "url": "https://identity.example.com" - }, - { - "id": "012322eeedcd459edabb4933021112bc", - "interface": "admin", - "region": "RegionOne", - "url": "https://identity.example.com" - } - ], - "endpoints_links": [], - "name": "keystone", - "type": "identity" - }, - { - "endpoints": [ - { - "id": "1e875ca2225b408bbf3520a1b8e1a537", - "interface": "public", - "region": "RegionOne", - "url": "https://fake.example.com/v2/1c36b64c840a42cd9e9b931a369337f0" - } - ], - "name": "fake_service", - "type": "fake" - } - ], - "expires_at": "9999-12-31T23:59:59Z", - "issued_at": "2016-12-17T14:25:05.000000Z", - "methods": [ - "password" - ], - "project": { - "domain": { - "id": "default", - "name": "default" - }, - "id": "1c36b64c840a42cd9e9b931a369337f0", - "name": "Default Project" - }, - "roles": [ - { - "id": "9fe2ff9ee4384b1894a90878d3e92bab", - "name": "_member_" - }, - { - "id": "37071fc082e14c2284c32a2761f71c63", - "name": "swiftoperator" - } - ], - "user": { - "domain": { - "id": "default", - "name": "default" - }, - "id": "c17534835f8f42bf98fc367e0bf35e09", - "name": "mordred" - } - } -} diff --git a/openstack/tests/unit/fixtures/catalog-v3-suburl.json b/openstack/tests/unit/fixtures/catalog-v3-suburl.json deleted file mode 100644 index ca2b68107..000000000 --- a/openstack/tests/unit/fixtures/catalog-v3-suburl.json +++ /dev/null @@ -1,198 +0,0 @@ -{ - "token": { - "audit_ids": [ - "Rvn7eHkiSeOwucBIPaKdYA" - ], - "catalog": [ - { - "endpoints": [ - { - "id": "32466f357f3545248c47471ca51b0d3a", - "interface": "public", - "region": "RegionOne", - "url": "https://example.com/compute/v2.1/" - } - ], - "name": "nova", - "type": "compute" - }, - { - "endpoints": [ - { - "id": "1e875ca2225b408bbf3520a1b8e1a537", - "interface": "public", - "region": "RegionOne", - "url": "https://example.com/volumev2/v2/1c36b64c840a42cd9e9b931a369337f0" - } - ], - "name": "cinderv2", - "type": "volumev2" - }, - { - "endpoints": [ - { - "id": "5a64de3c4a614d8d8f8d1ba3dee5f45f", - "interface": "public", - "region": "RegionOne", - "url": "https://example.com/image" - } - ], - "name": "glance", - "type": "image" - }, - { - "endpoints": [ - { - "id": "3d15fdfc7d424f3c8923324417e1a3d1", - "interface": "public", - "region": "RegionOne", - "url": "https://example.com/volume/v1/1c36b64c840a42cd9e9b931a369337f0" - } - ], - "name": "cinder", - "type": "volume" - }, - { - "endpoints": [ - { - "id": "4deb4d0504a044a395d4480741ba628c", - "interface": "public", - "region": "RegionOne", - "url": "https://identity.example.com" - }, - { - "id": "012322eeedcd459edabb4933021112bc", - "interface": "admin", - "region": "RegionOne", - "url": "https://example.com/identity" - } - ], - "endpoints_links": [], - "name": "keystone", - "type": "identity" - }, - { - "endpoints": [ - { - "id": "4deb4d0504a044a395d4480741ba628d", - "interface": "public", - "region": "RegionOne", - "url": "https://example.com/example" - } - ], - "endpoints_links": [], - "name": "neutron", - "type": "network" - }, - { - "endpoints": [ - { - "id": "4deb4d0504a044a395d4480741ba628e", - "interface": "public", - "region": "RegionOne", - "url": "https://example.com/container-infra/v1" - } - ], - "endpoints_links": [], - "name": "magnum", - "type": "container-infra" - }, - { - "endpoints": [ - { - "id": "4deb4d0504a044a395d4480741ba628c", - "interface": "public", - "region": "RegionOne", - "url": "https://example.com/object-store/v1/1c36b64c840a42cd9e9b931a369337f0" - } - ], - "endpoints_links": [], - "name": "swift", - "type": "object-store" - }, - { - "endpoints": [ - { - "id": "652f0612744042bfbb8a8bb2c777a16d", - "interface": "public", - "region": "RegionOne", - "url": "https://example.com/bare-metal" - } - ], - "endpoints_links": [], - "name": "ironic", - "type": "baremetal" - }, - { - "endpoints": [ - { - "id": "4deb4d0504a044a395d4480741ba628c", - "interface": "public", - "region": "RegionOne", - "url": "https://example.com/orchestration/v1/1c36b64c840a42cd9e9b931a369337f0" - } - ], - "endpoints_links": [], - "name": "heat", - "type": "orchestration" - }, - { - "endpoints": [ - { - "id": "10c76ffd2b744a67950ed1365190d352", - "interface": "public", - "region": "RegionOne", - "url": "https://example.com/dns" - } - ], - "endpoints_links": [], - "name": "designate", - "type": "dns" - }, - { - "endpoints": [ - { - "id": "4deb4d0504a044a395d4480741ba624z", - "interface": "public", - "region": "RegionOne", - "url": "https://example.com/clustering" - } - ], - "endpoint_links": [], - "name": "senlin", - "type": "clustering" - } - ], - "expires_at": "9999-12-31T23:59:59Z", - "issued_at": "2016-12-17T14:25:05.000000Z", - "methods": [ - "password" - ], - "project": { - "domain": { - "id": "default", - "name": "default" - }, - "id": "1c36b64c840a42cd9e9b931a369337f0", - "name": "Default Project" - }, - "roles": [ - { - "id": "9fe2ff9ee4384b1894a90878d3e92bab", - "name": "_member_" - }, - { - "id": "37071fc082e14c2284c32a2761f71c63", - "name": "swiftoperator" - } - ], - "user": { - "domain": { - "id": "default", - "name": "default" - }, - "id": "c17534835f8f42bf98fc367e0bf35e09", - "name": "mordred" - } - } -} diff --git a/openstack/tests/unit/fixtures/catalog-v3-suffix.json b/openstack/tests/unit/fixtures/catalog-v3-suffix.json deleted file mode 100644 index a2a2633bc..000000000 --- a/openstack/tests/unit/fixtures/catalog-v3-suffix.json +++ /dev/null @@ -1,211 +0,0 @@ -{ - "token": { - "audit_ids": [ - "Rvn7eHkiSeOwucBIPaKdYA" - ], - "catalog": [ - { - "endpoints": [ - { - "id": "32466f357f3545248c47471ca51b0d3a", - "interface": "public", - "region": "RegionOne", - "url": "https://compute.example.com/v2.1/" - } - ], - "name": "nova", - "type": "compute" - }, - { - "endpoints": [ - { - "id": "1e875ca2225b408bbf3520a1b8e1a537", - "interface": "public", - "region": "RegionOne", - "url": "https://volume.example.com/v2/1c36b64c840a42cd9e9b931a369337f0" - } - ], - "name": "cinderv2", - "type": "volumev2" - }, - { - "endpoints": [ - { - "id": "5a64de3c4a614d8d8f8d1ba3dee5f45f", - "interface": "public", - "region": "RegionOne", - "url": "https://image.example.com" - } - ], - "name": "glance", - "type": "image" - }, - { - "endpoints": [ - { - "id": "3d15fdfc7d424f3c8923324417e1a3d1", - "interface": "public", - "region": "RegionOne", - "url": "https://volume.example.com/v1/1c36b64c840a42cd9e9b931a369337f0" - } - ], - "name": "cinder", - "type": "volume" - }, - { - "endpoints": [ - { - "id": "4deb4d0504a044a395d4480741ba628c", - "interface": "public", - "region": "RegionOne", - "url": "https://identity.example.com" - }, - { - "id": "012322eeedcd459edabb4933021112bc", - "interface": "admin", - "region": "RegionOne", - "url": "https://identity.example.com" - } - ], - "endpoints_links": [], - "name": "keystone", - "type": "identity" - }, - { - "endpoints": [ - { - "id": "4deb4d0504a044a395d4480741ba628d", - "interface": "public", - "region": "RegionOne", - "url": "https://network.example.com/v2.0" - } - ], - "endpoints_links": [], - "name": "neutron", - "type": "network" - }, - { - "endpoints": [ - { - "id": "4deb4d0504a044a395d4480741ba628e", - "interface": "public", - "region": "RegionOne", - "url": "https://container-infra.example.com/v1" - } - ], - "endpoints_links": [], - "name": "magnum", - "type": "container-infra" - }, - { - "endpoints": [ - { - "id": "4deb4d0504a044a395d4480741ba628c", - "interface": "public", - "region": "RegionOne", - "url": "https://object-store.example.com/v1/1c36b64c840a42cd9e9b931a369337f0" - } - ], - "endpoints_links": [], - "name": "swift", - "type": "object-store" - }, - { - "endpoints": [ - { - "id": "652f0612744042bfbb8a8bb2c777a16d", - "interface": "public", - "region": "RegionOne", - "url": "https://bare-metal.example.com/" - } - ], - "endpoints_links": [], - "name": "ironic", - "type": "baremetal" - }, - { - "endpoints": [ - { - "id": "4deb4d0504a044a395d4480741ba628c", - "interface": "public", - "region": "RegionOne", - "url": "https://orchestration.example.com/v1/1c36b64c840a42cd9e9b931a369337f0" - } - ], - "endpoints_links": [], - "name": "heat", - "type": "orchestration" - }, - { - "endpoints": [ - { - "id": "10c76ffd2b744a67950ed1365190d353", - "interface": "public", - "region": "RegionOne", - "url": "https://placement.example.com" - } - ], - "endpoints_links": [], - "name": "placement", - "type": "placement" - }, - { - "endpoints": [ - { - "id": "10c76ffd2b744a67950ed1365190d352", - "interface": "public", - "region": "RegionOne", - "url": "https://dns.example.com" - } - ], - "endpoints_links": [], - "name": "designate", - "type": "dns" - }, - { - "endpoints": [ - { - "id": "4deb4d0504a044a395d4480741ba624z", - "interface": "public", - "region": "RegionOne", - "url": "https://clustering.example.com" - } - ], - "endpoints_links": [], - "name": "senlin", - "type": "clustering" - } - ], - "expires_at": "9999-12-31T23:59:59Z", - "issued_at": "2016-12-17T14:25:05.000000Z", - "methods": [ - "password" - ], - "project": { - "domain": { - "id": "default", - "name": "default" - }, - "id": "1c36b64c840a42cd9e9b931a369337f0", - "name": "Default Project" - }, - "roles": [ - { - "id": "9fe2ff9ee4384b1894a90878d3e92bab", - "name": "_member_" - }, - { - "id": "37071fc082e14c2284c32a2761f71c63", - "name": "swiftoperator" - } - ], - "user": { - "domain": { - "id": "default", - "name": "default" - }, - "id": "c17534835f8f42bf98fc367e0bf35e09", - "name": "mordred" - } - } -} diff --git a/openstack/tests/unit/fixtures/catalog-v3.json b/openstack/tests/unit/fixtures/catalog-v3.json deleted file mode 100644 index 40bc7bb39..000000000 --- a/openstack/tests/unit/fixtures/catalog-v3.json +++ /dev/null @@ -1,217 +0,0 @@ -{ - "token": { - "audit_ids": [ - "Rvn7eHkiSeOwucBIPaKdYA" - ], - "catalog": [ - { - "endpoints": [ - { - "id": "32466f357f3545248c47471ca51b0d3a", - "interface": "public", - "region": "RegionOne", - "url": "https://compute.example.com/v2.1/" - }, - { - "id": "32466f357f3545248c47471ca51b0d3b", - "interface": "internal", - "region": "RegionOne", - "url": "https://internal.compute.example.com/v2.1/" - } - ], - "name": "nova", - "type": "compute" - }, - { - "endpoints": [ - { - "id": "1e875ca2225b408bbf3520a1b8e1a537", - "interface": "public", - "region": "RegionOne", - "url": "https://volume.example.com/v2/1c36b64c840a42cd9e9b931a369337f0" - } - ], - "name": "cinderv2", - "type": "volumev2" - }, - { - "endpoints": [ - { - "id": "5a64de3c4a614d8d8f8d1ba3dee5f45f", - "interface": "public", - "region": "RegionOne", - "url": "https://image.example.com" - } - ], - "name": "glance", - "type": "image" - }, - { - "endpoints": [ - { - "id": "3d15fdfc7d424f3c8923324417e1a3d1", - "interface": "public", - "region": "RegionOne", - "url": "https://volume.example.com/v1/1c36b64c840a42cd9e9b931a369337f0" - } - ], - "name": "cinder", - "type": "volume" - }, - { - "endpoints": [ - { - "id": "4deb4d0504a044a395d4480741ba628c", - "interface": "public", - "region": "RegionOne", - "url": "https://identity.example.com" - }, - { - "id": "012322eeedcd459edabb4933021112bc", - "interface": "admin", - "region": "RegionOne", - "url": "https://identity.example.com" - } - ], - "endpoints_links": [], - "name": "keystone", - "type": "identity" - }, - { - "endpoints": [ - { - "id": "4deb4d0504a044a395d4480741ba628d", - "interface": "public", - "region": "RegionOne", - "url": "https://network.example.com" - } - ], - "endpoints_links": [], - "name": "neutron", - "type": "network" - }, - { - "endpoints": [ - { - "id": "4deb4d0504a044a395d4480741ba628e", - "interface": "public", - "region": "RegionOne", - "url": "https://container-infra.example.com/v1" - } - ], - "endpoints_links": [], - "name": "magnum", - "type": "container-infra" - }, - { - "endpoints": [ - { - "id": "4deb4d0504a044a395d4480741ba628c", - "interface": "public", - "region": "RegionOne", - "url": "https://object-store.example.com/v1/1c36b64c840a42cd9e9b931a369337f0" - } - ], - "endpoints_links": [], - "name": "swift", - "type": "object-store" - }, - { - "endpoints": [ - { - "id": "652f0612744042bfbb8a8bb2c777a16d", - "interface": "public", - "region": "RegionOne", - "url": "https://bare-metal.example.com/" - } - ], - "endpoints_links": [], - "name": "ironic", - "type": "baremetal" - }, - { - "endpoints": [ - { - "id": "4deb4d0504a044a395d4480741ba628c", - "interface": "public", - "region": "RegionOne", - "url": "https://orchestration.example.com/v1/1c36b64c840a42cd9e9b931a369337f0" - } - ], - "endpoints_links": [], - "name": "heat", - "type": "orchestration" - }, - { - "endpoints": [ - { - "id": "10c76ffd2b744a67950ed1365190d353", - "interface": "public", - "region": "RegionOne", - "url": "https://placement.example.com" - } - ], - "endpoints_links": [], - "name": "placement", - "type": "placement" - }, - { - "endpoints": [ - { - "id": "10c76ffd2b744a67950ed1365190d352", - "interface": "public", - "region": "RegionOne", - "url": "https://dns.example.com" - } - ], - "endpoints_links": [], - "name": "designate", - "type": "dns" - }, - { - "endpoints": [ - { - "id": "4deb4d0504a044a395d4480741ba624z", - "interface": "public", - "region": "RegionOne", - "url": "https://clustering.example.com" - } - ], - "endpoints_links": [], - "name": "senlin", - "type": "clustering" - } - ], - "expires_at": "9999-12-31T23:59:59Z", - "issued_at": "2016-12-17T14:25:05.000000Z", - "methods": [ - "password" - ], - "project": { - "domain": { - "id": "default", - "name": "default" - }, - "id": "1c36b64c840a42cd9e9b931a369337f0", - "name": "Default Project" - }, - "roles": [ - { - "id": "9fe2ff9ee4384b1894a90878d3e92bab", - "name": "_member_" - }, - { - "id": "37071fc082e14c2284c32a2761f71c63", - "name": "swiftoperator" - } - ], - "user": { - "domain": { - "id": "default", - "name": "default" - }, - "id": "c17534835f8f42bf98fc367e0bf35e09", - "name": "mordred" - } - } -} diff --git a/openstack/tests/unit/fixtures/clouds/clouds.yaml b/openstack/tests/unit/fixtures/clouds/clouds.yaml index ebd4cc0ba..99bae938f 100644 --- a/openstack/tests/unit/fixtures/clouds/clouds.yaml +++ b/openstack/tests/unit/fixtures/clouds/clouds.yaml @@ -7,6 +7,7 @@ clouds: username: admin user_domain_name: default project_domain_name: default + block_storage_api_version: 2 region_name: RegionOne _test_cloud_v2_: auth: diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index b3e4c51dd..721bc59a2 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -21,6 +21,7 @@ from openstack import proxy import openstack.config from openstack import service_description +from openstack.tests import fakes from openstack.tests.unit import base from openstack.tests.unit.fake import fake_service @@ -290,7 +291,13 @@ class TestNetworkConnection(base.TestCase): # Verify that if the catalog has the suffix we don't mess things up. def test_network_proxy(self): - self.use_keystone_v3(catalog='catalog-v3-suffix.json') + self.os_fixture.v3_token.remove_service('network') + svc = self.os_fixture.v3_token.add_service('network') + svc.add_endpoint( + interface='public', + url='https://network.example.com/v2.0', + region='RegionOne') + self.use_keystone_v3() self.assertEqual( 'openstack.network.v2._proxy', self.cloud.network.__class__.__module__) @@ -330,7 +337,13 @@ def test_authorize_failure(self): class TestNewService(base.TestCase): def test_add_service_v1(self): - self.use_keystone_v3(catalog='catalog-v3-fake-v1.json') + svc = self.os_fixture.v3_token.add_service('fake') + svc.add_endpoint( + interface='public', + region='RegionOne', + url='https://fake.example.com/v1/{0}'.format(fakes.PROJECT_ID), + ) + self.use_keystone_v3() conn = self.cloud service = fake_service.FakeService('fake') @@ -358,7 +371,13 @@ def test_add_service_v1(self): self.assertTrue(conn.fake.dummy()) def test_add_service_v2(self): - self.use_keystone_v3(catalog='catalog-v3-fake-v2.json') + svc = self.os_fixture.v3_token.add_service('fake') + svc.add_endpoint( + interface='public', + region='RegionOne', + url='https://fake.example.com/v2/{0}'.format(fakes.PROJECT_ID), + ) + self.use_keystone_v3() conn = self.cloud self.register_uris([ @@ -383,7 +402,13 @@ def test_add_service_v2(self): self.assertFalse(conn.fake.dummy()) def test_replace_system_service(self): - self.use_keystone_v3(catalog='catalog-v3-fake-v2.json') + svc = self.os_fixture.v3_token.add_service('fake') + svc.add_endpoint( + interface='public', + region='RegionOne', + url='https://fake.example.com/v2/{0}'.format(fakes.PROJECT_ID), + ) + self.use_keystone_v3() conn = self.cloud # delete native dns service diff --git a/openstack/tests/unit/test_missing_version.py b/openstack/tests/unit/test_missing_version.py index b1cdaa44e..6e1d5b975 100644 --- a/openstack/tests/unit/test_missing_version.py +++ b/openstack/tests/unit/test_missing_version.py @@ -21,7 +21,12 @@ class TestMissingVersion(base.TestCase): def setUp(self): super(TestMissingVersion, self).setUp() - self.use_keystone_v3(catalog='catalog-bogus-glance.json') + self.os_fixture.clear_tokens() + svc = self.os_fixture.v3_token.add_service('image') + svc.add_endpoint( + url='https://example.com/image/', + region='RegionOne', interface='public') + self.use_keystone_v3() self.use_glance( image_version_json='bad-glance-version.json', image_discovery_url='https://example.com/image/') diff --git a/openstack/tests/unit/test_stats.py b/openstack/tests/unit/test_stats.py index a556c1f14..801354545 100644 --- a/openstack/tests/unit/test_stats.py +++ b/openstack/tests/unit/test_stats.py @@ -164,7 +164,7 @@ def assert_prometheus_stat(self, name, value, labels=None): def test_list_projects(self): mock_uri = self.get_mock_url( - service_type='identity', interface='admin', resource='projects', + service_type='identity', resource='projects', base_url_append='v3') self.register_uris([ @@ -185,7 +185,7 @@ def test_list_projects(self): def test_projects(self): mock_uri = self.get_mock_url( - service_type='identity', interface='admin', resource='projects', + service_type='identity', resource='projects', base_url_append='v3') self.register_uris([ @@ -256,7 +256,7 @@ def setUp(self): def test_no_stats(self): mock_uri = self.get_mock_url( - service_type='identity', interface='admin', resource='projects', + service_type='identity', resource='projects', base_url_append='v3') self.register_uris([ From 5d554498f98aabddf7eebabb90e33370e7d3621f Mon Sep 17 00:00:00 2001 From: Maxim Babushkin Date: Thu, 8 Aug 2019 14:49:07 +0300 Subject: [PATCH 2532/3836] Add 'tag' support to compute with supported microversion A 'tag' could be specified for the interface when the nova microversion is 2.32-2.36 or >= 2.42. Add support to the 'tag' use. Change-Id: Ia9f95ef4ed285012e157d758dfbec69df4010350 --- openstack/cloud/_compute.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 8a8e85858..397681837 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -790,6 +790,7 @@ def create_server( raise TypeError( "create_server() requires either 'image' or 'boot_volume'") + microversion = None server_json = {'server': kwargs} # TODO(mordred) Add support for description starting in 2.19 @@ -883,11 +884,14 @@ def create_server( " may be given") if fixed_ip: net['fixed_ip'] = fixed_ip - # TODO(mordred) Add support for tag if server supports microversion - # 2.32-2.36 or >= 2.42 for key in ('port', 'port-id'): if key in nic: net['port'] = nic.pop(key) + # A tag supported only in server microversion 2.32-2.36 or >= 2.42 + # Bumping the version to 2.42 to support the 'tag' implementation + if 'tag' in nic: + microversion = utils.pick_microversion(self.compute, '2.42') + net['tag'] = nic.pop('tag') if nic: raise exc.OpenStackCloudException( "Additional unsupported keys given for server network" @@ -927,7 +931,8 @@ def create_server( endpoint = '/os-volumes_boot' with _utils.shade_exceptions("Error in creating instance"): data = proxy._json_response( - self.compute.post(endpoint, json=server_json)) + self.compute.post(endpoint, json=server_json, + microversion=microversion)) server = self._get_and_munchify('server', data) admin_pass = server.get('adminPass') or kwargs.get('admin_pass') if not wait: From af47dd4ab97355db007faa24da163618fbfe2bc4 Mon Sep 17 00:00:00 2001 From: Ghislain Bourgeois Date: Wed, 21 Aug 2019 08:53:46 -0400 Subject: [PATCH 2533/3836] Fix AttributeError bug when creating nested stacks Makes envfiles an empty dict instead of None. Also added a test that reproduces the issue without the fix. Task: 36321 Story: 2006424 Change-Id: Ia793195daad8b8a9c01f12de4df4d6f01033ad2b Signed-off-by: Ghislain Bourgeois --- openstack/orchestration/v1/_proxy.py | 2 +- .../unit/orchestration/v1/hello_world.yaml | 44 +++++++++++++++++++ .../unit/orchestration/v1/helloworld.txt | 1 + .../tests/unit/orchestration/v1/test_proxy.py | 8 ++++ 4 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 openstack/tests/unit/orchestration/v1/hello_world.yaml create mode 100644 openstack/tests/unit/orchestration/v1/helloworld.txt diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index 4ea3b0f5b..dbf61a7c8 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -43,7 +43,7 @@ def read_env_and_templates(self, template_file=None, template_url=None, :rtype: dict """ stack_attrs = dict() - envfiles = None + envfiles = dict() tpl_files = None if environment_files: envfiles, env = \ diff --git a/openstack/tests/unit/orchestration/v1/hello_world.yaml b/openstack/tests/unit/orchestration/v1/hello_world.yaml new file mode 100644 index 000000000..77651d203 --- /dev/null +++ b/openstack/tests/unit/orchestration/v1/hello_world.yaml @@ -0,0 +1,44 @@ +# +# Minimal HOT template defining a single compute server. +# +heat_template_version: 2013-05-23 + +description: > + Minimal HOT template for stack + +parameters: + key_name: + type: string + description: Name of an existing key pair to use for the server + constraints: + - custom_constraint: nova.keypair + flavor: + type: string + description: Flavor for the server to be created + default: m1.small + constraints: + - custom_constraint: nova.flavor + image: + type: string + description: Image ID or image name to use for the server + constraints: + - custom_constraint: glance.image + network: + type: string + description: Network used by the server + +resources: + server: + type: OS::Nova::Server + properties: + key_name: { get_param: key_name } + image: { get_param: image } + flavor: { get_param: flavor } + networks: [{network: {get_param: network} }] + metadata: + message: {get_file: helloworld.txt} + +outputs: + server_networks: + description: The networks of the deployed server + value: { get_attr: [server, networks] } diff --git a/openstack/tests/unit/orchestration/v1/helloworld.txt b/openstack/tests/unit/orchestration/v1/helloworld.txt new file mode 100644 index 000000000..e965047ad --- /dev/null +++ b/openstack/tests/unit/orchestration/v1/helloworld.txt @@ -0,0 +1 @@ +Hello diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index 834f69ba6..5b4fb85b6 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -306,6 +306,14 @@ def test_validate_template(self, mock_validate): ignore_errors=ignore_errors) self.assertEqual(mock_validate.return_value, res) + def test_validate_template_no_env(self): + tmpl = "openstack/tests/unit/orchestration/v1/hello_world.yaml" + + res = self.proxy.read_env_and_templates(tmpl) + + self.assertIsInstance(res, dict) + self.assertIsInstance(res["files"], dict) + def test_validate_template_invalid_request(self): err = self.assertRaises(exceptions.InvalidRequest, self.proxy.validate_template, From da45a449dc047e25a295069991b42f1e431e0f3c Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 7 Jun 2019 20:12:35 +0200 Subject: [PATCH 2534/3836] Rework statistics reporting - Add support for reporting statistics to InfluxDB - Move stats configuration under 'metrics' section of the yaml file - fix metric name build when url contains project id - report timing in milliseconds - add initial docs for metrics reporting - fix metric names in some weird cases - allow individual proxy to override metrics naming logic Change-Id: I76d2d78dc2f4c8cecbf89b8cc101c2bb1dec1a2b --- doc/source/user/guides/stats.rst | 59 ++++++ doc/source/user/index.rst | 1 + openstack/cloud/openstackcloud.py | 1 + openstack/config/cloud_region.py | 41 +++- openstack/config/loader.py | 29 ++- openstack/object_store/v1/_proxy.py | 32 +++ openstack/orchestration/v1/_proxy.py | 15 ++ openstack/proxy.py | 189 ++++++++++++------ .../tests/unit/object_store/v1/test_proxy.py | 13 ++ .../tests/unit/orchestration/v1/test_proxy.py | 31 +++ openstack/tests/unit/test_proxy.py | 11 +- .../add_influxdb_stats-665714d715302ad5.yaml | 4 + 12 files changed, 352 insertions(+), 74 deletions(-) create mode 100644 doc/source/user/guides/stats.rst create mode 100644 releasenotes/notes/add_influxdb_stats-665714d715302ad5.yaml diff --git a/doc/source/user/guides/stats.rst b/doc/source/user/guides/stats.rst new file mode 100644 index 000000000..d01ef8b4f --- /dev/null +++ b/doc/source/user/guides/stats.rst @@ -0,0 +1,59 @@ +==================== +Statistics reporting +==================== + +`openstacksdk` offers possibility to report statistics on individual API +requests/responses in different formats. `Statsd` allows reporting of the +response times in the statsd format. `InfluxDB` allows a more event-oriented +reporting of the same data. `Prometheus` reporting is a bit different and +requires the application using SDK to take care of the metrics exporting, while +`openstacksdk` prepares the metrics. + +Due to the nature of the `statsd` protocol lots of tools consuming the metrics +do the data aggregation and processing in the configurable time frame (mean +value calculation for a 1 minute time frame). For the case of periodic tasks +this might not be very useful. A better fit for using `openstacksdk` as a +library is an 'event'-recording, where duration of an individual request is +stored and all required calculations are done if necessary in the monitoring +system based required timeframe, or the data is simply shown as is with no +analytics. A `comparison +`_ article describes +differences in those approaches. + +Simple Usage +------------ + +To receive metrics add a following section to the config file (clouds.yaml): + +.. code-block:: yaml + + metrics: + statsd: + host: __statsd_server_host__ + port: __statsd_server_port__ + clouds: + .. + + +In order to enable InfluxDB reporting following configuration need to be done +in the `clouds.yaml` file + +.. code-block:: yaml + + metrics: + influxdb: + host: __influxdb_server_host__ + port: __influxdb_server_port__ + use_udp: __True|False__ + username: __influxdb_auth_username__ + password: __influxdb_auth_password__ + database: __influxdb_db_name__ + measurement: __influxdb_measurement_name__ + timeout: __infludb_requests_timeout__ + clouds: + .. + +Metrics will be reported only when corresponding client libraries ( +`statsd` for 'statsd' reporting, `influxdb` for influxdb reporting +correspondingly). When those libraries are not available reporting will be +silently ignored. diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index 1070284cc..9daef05f2 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -33,6 +33,7 @@ approach, this is where you'll want to begin. Connect to an OpenStack Cloud Connect to an OpenStack Cloud Using a Config File Logging + Statistics reporting Microversions Baremetal Block Storage diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index f48ce13e5..30818e25a 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -363,6 +363,7 @@ def _get_versioned_client( statsd_client=self.config.get_statsd_client(), prometheus_counter=self.config.get_prometheus_counter(), prometheus_histogram=self.config.get_prometheus_histogram(), + influxdb_client=self.config.get_influxdb_client(), min_version=request_min_version, max_version=request_max_version) if adapter.get_endpoint(): diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index eaf7703e9..04f5badc2 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -30,6 +30,10 @@ import prometheus_client except ImportError: prometheus_client = None +try: + import influxdb +except ImportError: + influxdb = None from openstack import version as openstack_version @@ -218,6 +222,7 @@ def __init__(self, name=None, region_name=None, config=None, cache_path=None, cache_class='dogpile.cache.null', cache_arguments=None, password_callback=None, statsd_host=None, statsd_port=None, statsd_prefix=None, + influxdb_config=None, collector_registry=None): self._name = name self.config = _util.normalize_keys(config) @@ -246,6 +251,8 @@ def __init__(self, name=None, region_name=None, config=None, self._statsd_port = statsd_port self._statsd_prefix = statsd_prefix self._statsd_client = None + self._influxdb_config = influxdb_config + self._influxdb_client = None self._collector_registry = collector_registry self._service_type_manager = os_service_types.ServiceTypes() @@ -646,6 +653,8 @@ def get_session_client( kwargs.setdefault('prometheus_counter', self.get_prometheus_counter()) kwargs.setdefault( 'prometheus_histogram', self.get_prometheus_histogram()) + kwargs.setdefault('influxdb_config', self._influxdb_config) + kwargs.setdefault('influxdb_client', self.get_influxdb_client()) endpoint_override = self.get_endpoint(service_type) version = version_request.version min_api_version = ( @@ -921,7 +930,11 @@ def get_statsd_client(self): if self._statsd_port: statsd_args['port'] = self._statsd_port if statsd_args: - return statsd.StatsClient(**statsd_args) + try: + return statsd.StatsClient(**statsd_args) + except Exception: + self.log.warning('Cannot establish connection to statsd') + return None else: return None @@ -988,3 +1001,29 @@ def get_disabled_reason(self, service_type): service_type = service_type.lower().replace('-', '_') d_key = _make_key('disabled_reason', service_type) return self.config.get(d_key) + + def get_influxdb_client(self): + influx_args = {} + if not self._influxdb_config: + return None + use_udp = bool(self._influxdb_config.get('use_udp', False)) + port = self._influxdb_config.get('port') + if use_udp: + influx_args['use_udp'] = True + if 'port' in self._influxdb_config: + if use_udp: + influx_args['udp_port'] = port + else: + influx_args['port'] = port + for key in ['host', 'username', 'password', 'database', 'timeout']: + if key in self._influxdb_config: + influx_args[key] = self._influxdb_config[key] + if influxdb and influx_args: + try: + return influxdb.InfluxDBClient(**influx_args) + except Exception: + self.log.warning('Cannot establish connection to InfluxDB') + else: + self.log.warning('InfluxDB configuration is present, ' + 'but no client library is found.') + return None diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 4ec47a3f5..09e916c37 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -142,7 +142,7 @@ def __init__(self, config_files=None, vendor_files=None, app_name=None, app_version=None, load_yaml_config=True, load_envvars=True, statsd_host=None, statsd_port=None, - statsd_prefix=None): + statsd_prefix=None, influxdb_config=None): self.log = _log.setup_logging('openstack.config') self._session_constructor = session_constructor self._app_name = app_name @@ -254,6 +254,7 @@ def __init__(self, config_files=None, vendor_files=None, self._cache_class = 'dogpile.cache.null' self._cache_arguments = {} self._cache_expirations = {} + self._influxdb_config = {} if 'cache' in self.cloud_config: cache_settings = _util.normalize_keys(self.cloud_config['cache']) @@ -279,11 +280,34 @@ def __init__(self, config_files=None, vendor_files=None, 'expiration', self._cache_expirations) if load_yaml_config: - statsd_config = self.cloud_config.get('statsd', {}) + metrics_config = self.cloud_config.get('metrics', {}) + statsd_config = metrics_config.get('statsd', {}) statsd_host = statsd_host or statsd_config.get('host') statsd_port = statsd_port or statsd_config.get('port') statsd_prefix = statsd_prefix or statsd_config.get('prefix') + influxdb_cfg = metrics_config.get('influxdb', {}) + # Parse InfluxDB configuration + if influxdb_config: + influxdb_cfg.update(influxdb_config) + if influxdb_cfg: + config = {} + if 'use_udp' in influxdb_cfg: + use_udp = influxdb_cfg['use_udp'] + if isinstance(use_udp, str): + use_udp = use_udp.lower() in ('true', 'yes', '1') + elif not isinstance(use_udp, bool): + use_udp = False + self.log.warning('InfluxDB.use_udp value type is not ' + 'supported. Use one of ' + '[true|false|yes|no|1|0]') + config['use_udp'] = use_udp + for key in ['host', 'port', 'username', 'password', 'database', + 'measurement', 'timeout']: + if key in influxdb_cfg: + config[key] = influxdb_cfg[key] + self._influxdb_config = config + if load_envvars: statsd_host = statsd_host or os.environ.get('STATSD_HOST') statsd_port = statsd_port or os.environ.get('STATSD_PORT') @@ -1112,6 +1136,7 @@ def get_one( statsd_host=self._statsd_host, statsd_port=self._statsd_port, statsd_prefix=self._statsd_prefix, + influxdb_config=self._influxdb_config, ) # TODO(mordred) Backwards compat for OSC transition get_one_cloud = get_one diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index c8d1eef66..201210825 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -42,6 +42,38 @@ class Proxy(proxy.Proxy): log = _log.setup_logging('openstack') + def _extract_name(self, url, service_type=None, project_id=None): + url_path = parse.urlparse(url).path.strip() + # Remove / from the beginning to keep the list indexes of interesting + # things consistent + if url_path.startswith('/'): + url_path = url_path[1:] + + # Split url into parts and exclude potential project_id in some urls + url_parts = [ + x for x in url_path.split('/') if ( + x != project_id + and ( + not project_id + or (project_id and x != 'AUTH_' + project_id) + )) + ] + # Strip leading version piece so that + # GET /v1/AUTH_xxx + # returns ['AUTH_xxx'] + if (url_parts[0] + and url_parts[0][0] == 'v' + and url_parts[0][1] and url_parts[0][1].isdigit()): + url_parts = url_parts[1:] + name_parts = self._extract_name_consume_url_parts(url_parts) + + # Getting the root of an endpoint is doing version discovery + if not name_parts: + name_parts = ['account'] + + # Strip out anything that's empty or None + return [part for part in name_parts if part] + def get_account_metadata(self): """Get metadata for this account. diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index 4ea3b0f5b..16fd3aaf7 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -26,6 +26,21 @@ class Proxy(proxy.Proxy): + def _extract_name_consume_url_parts(self, url_parts): + if (len(url_parts) == 3 and url_parts[0] == 'software_deployments' + and url_parts[1] == 'metadata'): + # Another nice example of totally different URL naming scheme, + # which we need to repair /software_deployment/metadata/server_id - + # just replace server_id with metadata to keep further logic + return ['software_deployment', 'metadata'] + if (url_parts[0] == 'stacks' and len(url_parts) > 2 + and not url_parts[2] in ['preview', 'resources']): + # orchestrate introduce having stack name and id part of the URL + # (/stacks/name/id/everything_else), so if on third position we + # have not a known part - discard it, not to brake further logic + del url_parts[2] + return super(Proxy, self)._extract_name_consume_url_parts(url_parts) + def read_env_and_templates(self, template_file=None, template_url=None, template_object=None, files=None, environment_files=None): diff --git a/openstack/proxy.py b/openstack/proxy.py index 9260222ae..52112d527 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -24,69 +24,6 @@ from openstack import resource -def _extract_name(url, service_type=None): - '''Produce a key name to use in logging/metrics from the URL path. - - We want to be able to logic/metric sane general things, so we pull - the url apart to generate names. The function returns a list because - there are two different ways in which the elements want to be combined - below (one for logging, one for statsd) - - Some examples are likely useful: - - /servers -> ['servers'] - /servers/{id} -> ['servers'] - /servers/{id}/os-security-groups -> ['servers', 'os-security-groups'] - /v2.0/networks.json -> ['networks'] - ''' - - url_path = urllib.parse.urlparse(url).path.strip() - # Remove / from the beginning to keep the list indexes of interesting - # things consistent - if url_path.startswith('/'): - url_path = url_path[1:] - - # Special case for neutron, which puts .json on the end of urls - if url_path.endswith('.json'): - url_path = url_path[:-len('.json')] - - url_parts = url_path.split('/') - if url_parts[-1] == 'detail': - # Special case detail calls - # GET /servers/detail - # returns ['servers', 'detail'] - name_parts = url_parts[-2:] - else: - # Strip leading version piece so that - # GET /v2.0/networks - # returns ['networks'] - if (url_parts[0] - and url_parts[0][0] == 'v' - and url_parts[0][1] and url_parts[0][1].isdigit()): - url_parts = url_parts[1:] - name_parts = [] - # Pull out every other URL portion - so that - # GET /servers/{id}/os-security-groups - # returns ['servers', 'os-security-groups'] - for idx in range(0, len(url_parts)): - if not idx % 2 and url_parts[idx]: - name_parts.append(url_parts[idx]) - - # Keystone Token fetching is a special case, so we name it "tokens" - if url_path.endswith('tokens'): - name_parts = ['tokens'] - - # Getting the root of an endpoint is doing version discovery - if not name_parts: - if service_type == 'object-store': - name_parts = ['account'] - else: - name_parts = ['discovery'] - - # Strip out anything that's empty or None - return [part for part in name_parts if part] - - # The _check_resource decorator is used on Proxy methods to ensure that # the `actual` argument is in fact the type of the `expected` argument. # It does so under two cases: @@ -126,6 +63,7 @@ def __init__( session, statsd_client=None, statsd_prefix=None, prometheus_counter=None, prometheus_histogram=None, + influxdb_config=None, influxdb_client=None, *args, **kwargs): # NOTE(dtantsur): keystoneauth defaults retriable_status_codes to None, # override it with a class-level value. @@ -136,6 +74,8 @@ def __init__( self._statsd_prefix = statsd_prefix self._prometheus_counter = prometheus_counter self._prometheus_histogram = prometheus_histogram + self._influxdb_client = influxdb_client + self._influxdb_config = influxdb_config if self.service_type: log_name = 'openstack.{0}'.format(self.service_type) else: @@ -154,18 +94,107 @@ def request( self._report_stats(response) return response + def _extract_name(self, url, service_type=None, project_id=None): + '''Produce a key name to use in logging/metrics from the URL path. + + We want to be able to logic/metric sane general things, so we pull + the url apart to generate names. The function returns a list because + there are two different ways in which the elements want to be combined + below (one for logging, one for statsd) + + Some examples are likely useful: + + /servers -> ['servers'] + /servers/{id} -> ['server'] + /servers/{id}/os-security-groups -> ['server', 'os-security-groups'] + /v2.0/networks.json -> ['networks'] + ''' + + url_path = urllib.parse.urlparse(url).path.strip() + # Remove / from the beginning to keep the list indexes of interesting + # things consistent + if url_path.startswith('/'): + url_path = url_path[1:] + + # Special case for neutron, which puts .json on the end of urls + if url_path.endswith('.json'): + url_path = url_path[:-len('.json')] + + # Split url into parts and exclude potential project_id in some urls + url_parts = [ + x for x in url_path.split('/') if ( + x != project_id + and ( + not project_id + or (project_id and x != 'AUTH_' + project_id) + )) + ] + if url_parts[-1] == 'detail': + # Special case detail calls + # GET /servers/detail + # returns ['servers', 'detail'] + name_parts = url_parts[-2:] + else: + # Strip leading version piece so that + # GET /v2.0/networks + # returns ['networks'] + if (url_parts[0] + and url_parts[0][0] == 'v' + and url_parts[0][1] and url_parts[0][1].isdigit()): + url_parts = url_parts[1:] + name_parts = self._extract_name_consume_url_parts(url_parts) + + # Keystone Token fetching is a special case, so we name it "tokens" + # NOTE(gtema): there is no metric triggered for regular authorization + # with openstack.connect(), since it bypassed SDK and goes directly to + # keystoneauth1. If you need to measure performance of the token + # fetching - trigger a separate call. + if url_path.endswith('tokens'): + name_parts = ['tokens'] + + if not name_parts: + name_parts = ['discovery'] + + # Strip out anything that's empty or None + return [part for part in name_parts if part] + + def _extract_name_consume_url_parts(self, url_parts): + """Pull out every other URL portion - so that + GET /servers/{id}/os-security-groups + returns ['server', 'os-security-groups'] + + """ + name_parts = [] + for idx in range(0, len(url_parts)): + if not idx % 2 and url_parts[idx]: + # If we are on first segment and it end with 's' stip this 's' + # to differentiate LIST and GET_BY_ID + if (len(url_parts) > idx + 1 + and url_parts[idx][-1] == 's' + and url_parts[idx][-2:] != 'is'): + name_parts.append(url_parts[idx][:-1]) + else: + name_parts.append(url_parts[idx]) + + return name_parts + def _report_stats(self, response): if self._statsd_client: self._report_stats_statsd(response) if self._prometheus_counter and self._prometheus_histogram: self._report_stats_prometheus(response) + if self._influxdb_client: + self._report_stats_influxdb(response) def _report_stats_statsd(self, response): - name_parts = _extract_name(response.request.url, self.service_type) + name_parts = self._extract_name(response.request.url, + self.service_type, + self.session.get_project_id()) key = '.'.join( [self._statsd_prefix, self.service_type, response.request.method] + name_parts) - self._statsd_client.timing(key, int(response.elapsed.seconds * 1000)) + self._statsd_client.timing(key, int( + response.elapsed.microseconds / 1000)) self._statsd_client.incr(key) def _report_stats_prometheus(self, response): @@ -177,7 +206,35 @@ def _report_stats_prometheus(self, response): ) self._prometheus_counter.labels(**labels).inc() self._prometheus_histogram.labels(**labels).observe( - response.elapsed.seconds) + response.elapsed.microseconds / 1000) + + def _report_stats_influxdb(self, response): + # NOTE(gtema): status_code is saved both as tag and field to give + # ability showing it as a value and not only as a legend. + # However Influx is not ok with having same name in tags and fields, + # therefore use different names. + data = [dict( + measurement=(self._influxdb_config.get('measurement', + 'openstack_api') + if self._influxdb_config else 'openstack_api'), + tags=dict( + method=response.request.method, + service_type=self.service_type, + status_code=response.status_code, + name='_'.join(self._extract_name( + response.request.url, self.service_type, + self.session.get_project_id()) + ) + ), + fields=dict( + duration=int(response.elapsed.microseconds / 1000), + status_code_val=int(response.status_code) + ) + )] + try: + self._influxdb_client.write_points(data) + except Exception: + self.log.exception('Error writing statistics to InfluxDB') def _version_matches(self, version): api_version = self.get_api_major_version() diff --git a/openstack/tests/unit/object_store/v1/test_proxy.py b/openstack/tests/unit/object_store/v1/test_proxy.py index 5b0bd818a..833cb527a 100644 --- a/openstack/tests/unit/object_store/v1/test_proxy.py +++ b/openstack/tests/unit/object_store/v1/test_proxy.py @@ -9,6 +9,7 @@ # 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 testscenarios import load_tests_apply_scenarios as load_tests # noqa import random import string @@ -237,3 +238,15 @@ def test_stream(self): self.assertLessEqual(chunk_len, chunk_size) self.assertEqual(chunk, self.the_data[start:end]) self.assert_calls() + + +class TestExtractName(TestObjectStoreProxy): + + scenarios = [ + ('discovery', dict(url='/', parts=['account'])) + ] + + def test_extract_name(self): + + results = self.proxy._extract_name(self.url) + self.assertEqual(self.parts, results) diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index 834f69ba6..d11c5e1db 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -9,6 +9,7 @@ # 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 testscenarios import load_tests_apply_scenarios as load_tests # noqa import mock import six @@ -312,3 +313,33 @@ def test_validate_template_invalid_request(self): None, template_url=None) self.assertEqual("'template_url' must be specified when template is " "None", six.text_type(err)) + + +class TestExtractName(TestOrchestrationProxy): + + scenarios = [ + ('stacks', dict(url='/stacks', parts=['stacks'])), + ('name_id', dict(url='/stacks/name/id', parts=['stack'])), + ('identity', dict(url='/stacks/id', parts=['stack'])), + ('preview', dict(url='/stacks/name/preview', + parts=['stack', 'preview'])), + ('stack_act', dict(url='/stacks/name/id/preview', + parts=['stack', 'preview'])), + ('stack_subres', dict(url='/stacks/name/id/resources', + parts=['stack', 'resources'])), + ('stack_subres_id', dict(url='/stacks/name/id/resources/id', + parts=['stack', 'resource'])), + ('stack_subres_id_act', + dict(url='/stacks/name/id/resources/id/action', + parts=['stack', 'resource', 'action'])), + ('event', + dict(url='/stacks/ignore/ignore/resources/ignore/events/id', + parts=['stack', 'resource', 'event'])), + ('sd_metadata', dict(url='/software_deployments/metadata/ignore', + parts=['software_deployment', 'metadata'])) + ] + + def test_extract_name(self): + + results = self.proxy._extract_name(self.url) + self.assertEqual(self.parts, results) diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index bb56d9f5c..42e239086 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -467,19 +467,20 @@ class TestExtractName(base.TestCase): scenarios = [ ('slash_servers_bare', dict(url='/servers', parts=['servers'])), - ('slash_servers_arg', dict(url='/servers/1', parts=['servers'])), + ('slash_servers_arg', dict(url='/servers/1', parts=['server'])), ('servers_bare', dict(url='servers', parts=['servers'])), - ('servers_arg', dict(url='servers/1', parts=['servers'])), + ('servers_arg', dict(url='servers/1', parts=['server'])), ('networks_bare', dict(url='/v2.0/networks', parts=['networks'])), - ('networks_arg', dict(url='/v2.0/networks/1', parts=['networks'])), + ('networks_arg', dict(url='/v2.0/networks/1', parts=['network'])), ('tokens', dict(url='/v3/tokens', parts=['tokens'])), ('discovery', dict(url='/', parts=['discovery'])), ('secgroups', dict( url='/servers/1/os-security-groups', - parts=['servers', 'os-security-groups'])), + parts=['server', 'os-security-groups'])), + ('bm_chassis', dict(url='/v1/chassis/id', parts=['chassis'])) ] def test_extract_name(self): - results = proxy._extract_name(self.url) + results = proxy.Proxy(mock.Mock())._extract_name(self.url) self.assertEqual(self.parts, results) diff --git a/releasenotes/notes/add_influxdb_stats-665714d715302ad5.yaml b/releasenotes/notes/add_influxdb_stats-665714d715302ad5.yaml new file mode 100644 index 000000000..f88ae1147 --- /dev/null +++ b/releasenotes/notes/add_influxdb_stats-665714d715302ad5.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add possibility to report API metrics into InfluxDB. From 3be1ca6d64b099f5e159518c9eaa8bd252b2b833 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 30 Aug 2019 09:15:30 +0200 Subject: [PATCH 2535/3836] Remove Accept header with empty value for HEAD and DELETE requests According to RFC 2616, 7231 If server does not support requested content type it should return 406 (not acceptable) RC. In case of HEAD or DELETE we are not interested in the body of the response, but we should not request missing content type. Either do not send this header at all, or at least send "*/*" instead of empty value. Any project can start following this spec at any time. P.S. Due to some funny patch in my cloud Swift is rejecting all requests (406) with "Accept: " header. Change-Id: Id443bec2cbe80b3c3d1ad2a7208edac0dde4b21b --- openstack/object_store/v1/obj.py | 1 - openstack/resource.py | 2 -- openstack/tests/unit/object_store/v1/test_obj.py | 2 +- openstack/tests/unit/test_resource.py | 5 ----- 4 files changed, 1 insertion(+), 9 deletions(-) diff --git a/openstack/object_store/v1/obj.py b/openstack/object_store/v1/obj.py index b2c6d8bf0..11b032497 100644 --- a/openstack/object_store/v1/obj.py +++ b/openstack/object_store/v1/obj.py @@ -287,7 +287,6 @@ def stream(self, session, error_message=None, chunk_size=1024): def create(self, session, base_path=None): request = self._prepare_request(base_path=base_path) - request.headers['Accept'] = '' response = session.put( request.url, diff --git a/openstack/resource.py b/openstack/resource.py index 28106c271..3eb2da725 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1328,7 +1328,6 @@ def head(self, session, base_path=None): session = self._get_session(session) microversion = self._get_microversion_for(session, 'fetch') response = session.head(request.url, - headers={"Accept": ""}, microversion=microversion) self.microversion = microversion @@ -1519,7 +1518,6 @@ def _raw_delete(self, session): return session.delete( request.url, - headers={"Accept": ""}, microversion=microversion) @classmethod diff --git a/openstack/tests/unit/object_store/v1/test_obj.py b/openstack/tests/unit/object_store/v1/test_obj.py index 5b5c58b49..c3e8135b2 100644 --- a/openstack/tests/unit/object_store/v1/test_obj.py +++ b/openstack/tests/unit/object_store/v1/test_obj.py @@ -131,7 +131,7 @@ def _test_create(self, method, data): sot = obj.Object.new(container=self.container, name=self.object, data=data) sot.is_newest = True - sent_headers = {"x-newest": 'True', "Accept": ""} + sent_headers = {"x-newest": 'True'} self.register_uris([ dict(method=method, uri=self.object_endpoint, headers=self.headers, diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 9a39fc21e..1f59cf7e8 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1514,7 +1514,6 @@ def test_head(self): self.sot._prepare_request.assert_called_once_with(base_path=None) self.session.head.assert_called_once_with( self.request.url, - headers={"Accept": ""}, microversion=None) self.assertIsNone(self.sot.microversion) @@ -1528,7 +1527,6 @@ def test_head_base_path(self): self.sot._prepare_request.assert_called_once_with(base_path='dummy') self.session.head.assert_called_once_with( self.request.url, - headers={"Accept": ""}, microversion=None) self.assertIsNone(self.sot.microversion) @@ -1552,7 +1550,6 @@ class Test(resource.Resource): sot._prepare_request.assert_called_once_with(base_path=None) self.session.head.assert_called_once_with( self.request.url, - headers={"Accept": ""}, microversion='1.42') self.assertEqual(sot.microversion, '1.42') @@ -1703,7 +1700,6 @@ def test_delete(self): self.sot._prepare_request.assert_called_once_with() self.session.delete.assert_called_once_with( self.request.url, - headers={"Accept": ""}, microversion=None) self.sot._translate_response.assert_called_once_with( @@ -1726,7 +1722,6 @@ class Test(resource.Resource): sot._prepare_request.assert_called_once_with() self.session.delete.assert_called_once_with( self.request.url, - headers={"Accept": ""}, microversion='1.42') sot._translate_response.assert_called_once_with( From b698c51d7bd3aa0708eca571568a9f02c5699242 Mon Sep 17 00:00:00 2001 From: Bo Tran Date: Mon, 26 Aug 2019 21:53:24 +0700 Subject: [PATCH 2536/3836] Add a fields meta_data to result of Senlin API Current, when I call API to list all events with description in here: https://docs.openstack.org/api-ref/clustering/?expanded=list-events-detail#list-events and in result maybe return event objects with meta_data field and this help useful for my works. But when i get events use python-senlinclient, i can't do that. So i want add to meta_data field to response when i use python-senlinclient. Thanks. Change-Id: I85d52027f6f20e9ec9a0ffa50dadaeed649b7d7f --- openstack/clustering/v1/event.py | 2 ++ openstack/tests/unit/clustering/v1/test_event.py | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/openstack/clustering/v1/event.py b/openstack/clustering/v1/event.py index cd556257c..d88b1c0d6 100644 --- a/openstack/clustering/v1/event.py +++ b/openstack/clustering/v1/event.py @@ -52,3 +52,5 @@ class Event(resource.Resource): #: A string description of the reason that brought the object into its #: current status. status_reason = resource.Body('status_reason') + #: The metadata of an event object. + meta_data = resource.Body('meta_data') diff --git a/openstack/tests/unit/clustering/v1/test_event.py b/openstack/tests/unit/clustering/v1/test_event.py index e52a44218..9a972580f 100644 --- a/openstack/tests/unit/clustering/v1/test_event.py +++ b/openstack/tests/unit/clustering/v1/test_event.py @@ -27,7 +27,13 @@ 'status': 'START', 'status_reason': 'The action was abandoned.', 'timestamp': '2016-10-10T12:46:36.000000', - 'user': '5e5bf8027826429c96af157f68dc9072' + 'user': '5e5bf8027826429c96af157f68dc9072', + 'meta_data': { + "action": { + "created_at": "2019-07-13T13:18:18Z", + "outputs": {} + } + } } @@ -58,3 +64,4 @@ def test_instantiate(self): self.assertEqual(FAKE['status_reason'], sot.status_reason) self.assertEqual(FAKE['timestamp'], sot.generated_at) self.assertEqual(FAKE['user'], sot.user_id) + self.assertEqual(FAKE['meta_data'], sot.meta_data) From 5f252e882ea77c8f135bbec5b905f3cade3de5f0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 31 Aug 2019 07:20:33 +0200 Subject: [PATCH 2537/3836] Strip two more accept headers from object-storage According to Tim Burke, the 'bytes' header isn't recognized by swift. The other empty accept continues to seem like a bad idea. Change-Id: I6cb9eda8b557118a4b624839d2d320448c5d3b0d --- openstack/object_store/v1/obj.py | 6 ++---- openstack/tests/unit/object_store/v1/test_obj.py | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/openstack/object_store/v1/obj.py b/openstack/object_store/v1/obj.py index 11b032497..116b8ccaf 100644 --- a/openstack/object_store/v1/obj.py +++ b/openstack/object_store/v1/obj.py @@ -269,7 +269,6 @@ def delete_metadata(self, session, keys): def _download(self, session, error_message=None, stream=False): request = self._prepare_request() - request.headers['Accept'] = 'bytes' response = session.get( request.url, headers=request.headers, stream=stream) @@ -307,9 +306,8 @@ def _raw_delete(self, session): # Fetch metadata to determine SLO flag self.head(session) - headers = { - 'Accept': "" - } + headers = {} + if self.is_static_large_object: headers['multipart-manifest'] = 'delete' diff --git a/openstack/tests/unit/object_store/v1/test_obj.py b/openstack/tests/unit/object_store/v1/test_obj.py index c3e8135b2..85642251e 100644 --- a/openstack/tests/unit/object_store/v1/test_obj.py +++ b/openstack/tests/unit/object_store/v1/test_obj.py @@ -105,7 +105,7 @@ def test_download(self): headers = { 'X-Newest': 'True', 'If-Match': self.headers['Etag'], - 'Accept': 'bytes' + 'Accept': '*/*' } self.register_uris([ dict(method='GET', uri=self.object_endpoint, From 363852f80d4e199792f5319d3cb1cdca0fdd7a7a Mon Sep 17 00:00:00 2001 From: "Ian Y. Choi" Date: Tue, 11 Sep 2018 12:56:43 -0500 Subject: [PATCH 2538/3836] Build PDF docs This commit adds a new tox target to build PDF documentation. It's a community goal[0] to have PDF docs available. Update conf.py for latex building. [0] https://governance.openstack.org/tc/goals/selected/train/pdf-doc-generation.html Co-authored-by: Bogdan Dobrelya Change-Id: I411bdabeaf70164ec1020abc2d22fcf1764f7c02 --- doc/requirements.txt | 1 + doc/source/conf.py | 8 ++++++-- tox.ini | 11 ++++++++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index d3f782bbd..3842af28b 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -7,3 +7,4 @@ docutils>=0.11 # OSI-Approved Open Source, Public Domain openstackdocstheme>=1.18.1 # Apache-2.0 beautifulsoup4>=4.6.0 # MIT reno>=2.5.0 # Apache-2.0 +sphinxcontrib-svg2pdfconverter>=0.1.0 # BSD diff --git a/doc/source/conf.py b/doc/source/conf.py index 99bea4c56..865b9a6bd 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -27,6 +27,7 @@ extensions = [ 'sphinx.ext.autodoc', 'openstackdocstheme', + 'sphinxcontrib.rsvgconverter', 'enforcer' ] @@ -107,13 +108,16 @@ # [howto/manual]). latex_documents = [ ('index', - '%s.tex' % project, - u'%s Documentation' % project, + 'doc-openstacksdk.tex', + u'OpenStackSDK Documentation', u'OpenStack Foundation', 'manual'), ] # Allow deeper levels of nesting for \begin...\end stanzas latex_elements = {'maxlistdepth': 10} +# Disable usage of xindy https://bugzilla.redhat.com/show_bug.cgi?id=1643664 +latex_use_xindy = False + # Include both the class and __init__ docstrings when describing the class autoclass_content = "both" diff --git a/tox.ini b/tox.ini index 5322f1012..b4ece8ba7 100644 --- a/tox.ini +++ b/tox.ini @@ -81,7 +81,16 @@ deps = -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/branch/master/upper-constraints.txt} -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt -commands = sphinx-build -W -d doc/build/doctrees -b html doc/source/ doc/build/html +commands = + sphinx-build -W -d doc/build/doctrees -b html doc/source/ doc/build/html + +[testenv:pdf-docs] +deps = {[testenv:docs]deps} +whitelist_externals = + make +commands = + sphinx-build -W -d doc/build/doctrees -b latex doc/source/ doc/build/pdf + make -C doc/build/pdf [testenv:releasenotes] deps = From d9b0159f51e0b6b84223eb27ceb8308ab460d3bf Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Wed, 4 Sep 2019 07:14:11 +0200 Subject: [PATCH 2539/3836] Cleanup doc/source/conf.py Remove old options that are part of openstackdocstheme default settings since at least version 1.20. Change-Id: I53d7d6267064f176a43a9cee51a0ee2b964e4a5c --- doc/requirements.txt | 2 +- doc/source/conf.py | 26 +------------------------- 2 files changed, 2 insertions(+), 26 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 3842af28b..c62786bc8 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -4,7 +4,7 @@ sphinx!=1.6.6,!=1.6.7,>=1.6.2,<2.0.0;python_version=='2.7' # BSD sphinx!=1.6.6,!=1.6.7,>=1.6.2;python_version>='3.4' # BSD docutils>=0.11 # OSI-Approved Open Source, Public Domain -openstackdocstheme>=1.18.1 # Apache-2.0 +openstackdocstheme>=1.20.0 # Apache-2.0 beautifulsoup4>=4.6.0 # MIT reno>=2.5.0 # Apache-2.0 sphinxcontrib-svg2pdfconverter>=0.1.0 # BSD diff --git a/doc/source/conf.py b/doc/source/conf.py index 865b9a6bd..d29b02ab6 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -15,8 +15,6 @@ import sys import warnings -import openstackdocstheme - sys.path.insert(0, os.path.abspath('../..')) sys.path.insert(0, os.path.abspath('.')) @@ -35,7 +33,6 @@ repository_name = 'openstack/openstacksdk' bug_project = '972' bug_tag = '' -html_last_updated_fmt = '%Y-%m-%d %H:%M' html_theme = 'openstackdocs' # TODO(shade) Set this to true once the build-openstack-sphinx-docs job is @@ -57,29 +54,8 @@ master_doc = 'index' # General information about the project. -project = u'openstacksdk' copyright = u'2017, Various members of the OpenStack Foundation' -# A few variables have to be set for the log-a-bug feature. -# gitsha: The SHA checksum of the bug description. Extracted from git log. -# bug_tag: Tag for categorizing the bug. Must be set manually. -# bug_project: Launchpad project to file bugs against. -# These variables are passed to the logabug code via html_context. -git_cmd = "/usr/bin/git log | head -n1 | cut -f2 -d' '" -try: - gitsha = os.popen(git_cmd).read().strip('\n') -except Exception: - warnings.warn("Can not get git sha.") - gitsha = "unknown" - -bug_tag = "docs" -pwd = os.getcwd() -# html_context allows us to pass arbitrary values into the html template -html_context = {"pwd": pwd, - "gitsha": gitsha, - "bug_tag": bug_tag, - "bug_project": bug_project} - # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True @@ -101,7 +77,7 @@ theme_include_auto_toc = False # Output file base name for HTML help builder. -htmlhelp_basename = '%sdoc' % project +htmlhelp_basename = 'openstacksdkdoc' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass From 4a6b5beb77ba2ed53af0cb54def88083cea22cc1 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Tue, 10 Sep 2019 13:55:36 +0200 Subject: [PATCH 2540/3836] baremetal-introspection: fix passing a Node to start_introspection Currently only a string ID is supported despite what the docstring claims. Change-Id: I59f8f85a31395c8a096cd507421cebb36d18198d --- openstack/baremetal_introspection/v1/_proxy.py | 4 +++- .../tests/unit/baremetal_introspection/v1/test_proxy.py | 7 +++++++ .../notes/introspection-node-6a3b7d55839ef82c.yaml | 4 ++++ 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/introspection-node-6a3b7d55839ef82c.yaml diff --git a/openstack/baremetal_introspection/v1/_proxy.py b/openstack/baremetal_introspection/v1/_proxy.py index 7ad0b0796..05b00964f 100644 --- a/openstack/baremetal_introspection/v1/_proxy.py +++ b/openstack/baremetal_introspection/v1/_proxy.py @@ -11,6 +11,7 @@ # under the License. from openstack import _log +from openstack.baremetal.v1 import node as _node from openstack.baremetal_introspection.v1 import introspection as _introspect from openstack import exceptions from openstack import proxy @@ -63,7 +64,8 @@ def start_introspection(self, node): :returns: :class:`~.introspection.Introspection` instance. """ - return self._create(_introspect.Introspection, id=node) + node = self._get_resource(_node.Node, node) + return self._create(_introspect.Introspection, id=node.id) def get_introspection(self, introspection): """Get a specific introspection. diff --git a/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py b/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py index 77f9c2ea5..272afad22 100644 --- a/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py @@ -14,6 +14,7 @@ from keystoneauth1 import adapter +from openstack.baremetal.v1 import node as _node from openstack.baremetal_introspection.v1 import _proxy from openstack.baremetal_introspection.v1 import introspection from openstack import exceptions @@ -33,6 +34,12 @@ def test_create_introspection(self): method_kwargs={'node': 'abcd'}, expected_kwargs={'id': 'abcd'}) + def test_create_introspection_with_node(self): + self.verify_create(self.proxy.start_introspection, + introspection.Introspection, + method_kwargs={'node': _node.Node(id='abcd')}, + expected_kwargs={'id': 'abcd'}) + def test_get_introspection(self): self.verify_get(self.proxy.get_introspection, introspection.Introspection) diff --git a/releasenotes/notes/introspection-node-6a3b7d55839ef82c.yaml b/releasenotes/notes/introspection-node-6a3b7d55839ef82c.yaml new file mode 100644 index 000000000..4638e8959 --- /dev/null +++ b/releasenotes/notes/introspection-node-6a3b7d55839ef82c.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - | + Fixes using a full `Node` object as an argument to `start_introspection`. From bf4a9c1e127eacca760558cd55ef7abd484c7ed6 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Tue, 10 Sep 2019 13:33:24 +0200 Subject: [PATCH 2541/3836] baremetal-introspection: add manage_boot argument to start_introspection Change-Id: Ia4240db1aacd658c3ad42eca2391fec880b4efd9 --- .../baremetal_introspection/v1/_proxy.py | 11 ++++- openstack/resource.py | 7 ++-- .../tests/unit/baremetal/v1/test_node.py | 15 ++++--- .../baremetal_introspection/v1/test_proxy.py | 40 +++++++++++++------ openstack/tests/unit/test_proxy_base.py | 2 +- openstack/tests/unit/test_resource.py | 29 ++++++++++++-- 6 files changed, 77 insertions(+), 27 deletions(-) diff --git a/openstack/baremetal_introspection/v1/_proxy.py b/openstack/baremetal_introspection/v1/_proxy.py index 05b00964f..f3db88ade 100644 --- a/openstack/baremetal_introspection/v1/_proxy.py +++ b/openstack/baremetal_introspection/v1/_proxy.py @@ -56,16 +56,23 @@ def introspections(self, **query): """ return _introspect.Introspection.list(self, **query) - def start_introspection(self, node): + def start_introspection(self, node, manage_boot=None): """Create a new introspection from attributes. :param node: The value can be either the name or ID of a node or a :class:`~openstack.baremetal.v1.node.Node` instance. + :param bool manage_boot: Whether to manage boot parameters for the + node. Defaults to the server default (which is `True`). :returns: :class:`~.introspection.Introspection` instance. """ node = self._get_resource(_node.Node, node) - return self._create(_introspect.Introspection, id=node.id) + res = _introspect.Introspection.new(connection=self._get_connection(), + id=node.id) + kwargs = {} + if manage_boot is not None: + kwargs['manage_boot'] = manage_boot + return res.create(self, **kwargs) def get_introspection(self, introspection): """Get a specific introspection. diff --git a/openstack/resource.py b/openstack/resource.py index 3eb2da725..64d6a197e 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1217,7 +1217,7 @@ def _raise(message): return actual - def create(self, session, prepend_key=True, base_path=None): + def create(self, session, prepend_key=True, base_path=None, **params): """Create a remote resource based on this instance. :param session: The session to use for making this request. @@ -1228,6 +1228,7 @@ def create(self, session, prepend_key=True, base_path=None): :param str base_path: Base part of the URI for creating resources, if different from :data:`~openstack.resource.Resource.base_path`. + :param dict params: Additional params to pass. :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if :data:`Resource.allow_create` is not set to ``True``. @@ -1246,14 +1247,14 @@ def create(self, session, prepend_key=True, base_path=None): base_path=base_path) response = session.put(request.url, json=request.body, headers=request.headers, - microversion=microversion) + microversion=microversion, params=params) elif self.create_method == 'POST': request = self._prepare_request(requires_id=requires_id, prepend_key=prepend_key, base_path=base_path) response = session.post(request.url, json=request.body, headers=request.headers, - microversion=microversion) + microversion=microversion, params=params) else: raise exceptions.ResourceFailure( msg="Invalid create method: %s" % self.create_method) diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 234a7b25d..72335d670 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -297,7 +297,8 @@ def test_available_old_version(self, mock_prov): self.assertIs(result, self.node) self.session.post.assert_called_once_with( mock.ANY, json={'driver': FAKE['driver']}, - headers=mock.ANY, microversion=self.session.default_microversion) + headers=mock.ANY, microversion=self.session.default_microversion, + params={}) self.assertFalse(mock_prov.called) def test_available_new_version(self, mock_prov): @@ -313,7 +314,8 @@ def _change_state(*args, **kwargs): self.assertIs(result, self.node) self.session.post.assert_called_once_with( mock.ANY, json={'driver': FAKE['driver']}, - headers=mock.ANY, microversion=self.session.default_microversion) + headers=mock.ANY, microversion=self.session.default_microversion, + params={}) mock_prov.assert_has_calls([ mock.call(self.node, self.session, 'manage', wait=True), mock.call(self.node, self.session, 'provide', wait=True) @@ -335,7 +337,8 @@ def test_enroll_new_version(self, mock_prov): self.assertIs(result, self.node) self.session.post.assert_called_once_with( mock.ANY, json={'driver': FAKE['driver']}, - headers=mock.ANY, microversion=self.session.default_microversion) + headers=mock.ANY, microversion=self.session.default_microversion, + params={}) self.assertFalse(mock_prov.called) def test_no_manageable_in_old_version(self, mock_prov): @@ -354,7 +357,8 @@ def test_manageable_old_version(self, mock_prov): self.assertIs(result, self.node) self.session.post.assert_called_once_with( mock.ANY, json={'driver': FAKE['driver']}, - headers=mock.ANY, microversion=self.session.default_microversion) + headers=mock.ANY, microversion=self.session.default_microversion, + params={}) mock_prov.assert_called_once_with(self.node, self.session, 'manage', wait=True) @@ -367,7 +371,8 @@ def test_manageable_new_version(self, mock_prov): self.assertIs(result, self.node) self.session.post.assert_called_once_with( mock.ANY, json={'driver': FAKE['driver']}, - headers=mock.ANY, microversion=self.session.default_microversion) + headers=mock.ANY, microversion=self.session.default_microversion, + params={}) mock_prov.assert_called_once_with(self.node, self.session, 'manage', wait=True) diff --git a/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py b/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py index 272afad22..43a6b8311 100644 --- a/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py @@ -22,24 +22,40 @@ from openstack.tests.unit import test_proxy_base +@mock.patch.object(introspection.Introspection, 'create', autospec=True) +class TestStartIntrospection(base.TestCase): + + def setUp(self): + super(TestStartIntrospection, self).setUp() + self.session = mock.Mock(spec=adapter.Adapter) + self.proxy = _proxy.Proxy(self.session) + + def test_create_introspection(self, mock_create): + self.proxy.start_introspection('abcd') + mock_create.assert_called_once_with(mock.ANY, self.proxy) + introspect = mock_create.call_args[0][0] + self.assertEqual('abcd', introspect.id) + + def test_create_introspection_with_node(self, mock_create): + self.proxy.start_introspection(_node.Node(id='abcd')) + mock_create.assert_called_once_with(mock.ANY, self.proxy) + introspect = mock_create.call_args[0][0] + self.assertEqual('abcd', introspect.id) + + def test_create_introspection_manage_boot(self, mock_create): + self.proxy.start_introspection('abcd', manage_boot=False) + mock_create.assert_called_once_with(mock.ANY, self.proxy, + manage_boot=False) + introspect = mock_create.call_args[0][0] + self.assertEqual('abcd', introspect.id) + + class TestBaremetalIntrospectionProxy(test_proxy_base.TestProxyBase): def setUp(self): super(TestBaremetalIntrospectionProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) - def test_create_introspection(self): - self.verify_create(self.proxy.start_introspection, - introspection.Introspection, - method_kwargs={'node': 'abcd'}, - expected_kwargs={'id': 'abcd'}) - - def test_create_introspection_with_node(self): - self.verify_create(self.proxy.start_introspection, - introspection.Introspection, - method_kwargs={'node': _node.Node(id='abcd')}, - expected_kwargs={'id': 'abcd'}) - def test_get_introspection(self): self.verify_get(self.proxy.get_introspection, introspection.Introspection) diff --git a/openstack/tests/unit/test_proxy_base.py b/openstack/tests/unit/test_proxy_base.py index 4c293294f..f4f520dd2 100644 --- a/openstack/tests/unit/test_proxy_base.py +++ b/openstack/tests/unit/test_proxy_base.py @@ -96,7 +96,7 @@ def verify_create(self, test_method, resource_type, expected_result="result", **kwargs): the_kwargs = {"x": 1, "y": 2, "z": 3} method_kwargs = kwargs.pop("method_kwargs", the_kwargs) - expected_args = [resource_type] + expected_args = kwargs.pop('expected_args', [resource_type]) # Default the_kwargs should be copied, since we might need to extend it expected_kwargs = kwargs.pop("expected_kwargs", the_kwargs.copy()) expected_kwargs["base_path"] = kwargs.pop("base_path", None) diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 1f59cf7e8..3b6581fcf 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1372,14 +1372,15 @@ class Test(resource.Resource): self.session.get_endpoint_data.return_value = self.endpoint_data def _test_create(self, cls, requires_id=False, prepend_key=False, - microversion=None, base_path=None): + microversion=None, base_path=None, params=None): id = "id" if requires_id else None sot = cls(id=id) sot._prepare_request = mock.Mock(return_value=self.request) sot._translate_response = mock.Mock() + params = params or {} result = sot.create(self.session, prepend_key=prepend_key, - base_path=base_path) + base_path=base_path, **params) sot._prepare_request.assert_called_once_with( requires_id=requires_id, prepend_key=prepend_key, @@ -1388,12 +1389,12 @@ def _test_create(self, cls, requires_id=False, prepend_key=False, self.session.put.assert_called_once_with( self.request.url, json=self.request.body, headers=self.request.headers, - microversion=microversion) + microversion=microversion, params=params) else: self.session.post.assert_called_once_with( self.request.url, json=self.request.body, headers=self.request.headers, - microversion=microversion) + microversion=microversion, params=params) self.assertEqual(sot.microversion, microversion) sot._translate_response.assert_called_once_with(self.response, @@ -1420,6 +1421,16 @@ class Test(resource.Resource): self._test_create(Test, requires_id=True, prepend_key=True, microversion='1.42') + def test_put_create_with_params(self): + class Test(resource.Resource): + service = self.service_name + base_path = self.base_path + allow_create = True + create_method = 'PUT' + + self._test_create(Test, requires_id=True, prepend_key=True, + params={'answer': 42}) + def test_post_create(self): class Test(resource.Resource): service = self.service_name @@ -1439,6 +1450,16 @@ class Test(resource.Resource): self._test_create(Test, requires_id=False, prepend_key=True, base_path='dummy') + def test_post_create_with_params(self): + class Test(resource.Resource): + service = self.service_name + base_path = self.base_path + allow_create = True + create_method = 'POST' + + self._test_create(Test, requires_id=False, prepend_key=True, + params={'answer': 42}) + def test_fetch(self): result = self.sot.fetch(self.session) From cdaa1045f976a16d748119c9f7d8ba034970edd7 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Fri, 6 Sep 2019 12:01:32 +0200 Subject: [PATCH 2542/3836] Properly convert baremetal fields to server-side values Currently we pass the 'fields' argument as it is. If a caller provides a client-side name instead of a server-side one (e.g. id instead of uuid), it results in a failure. This patch fixes it. Also stops requiring redundant 'name' for QueryParameters that are specified as a dictionary (like fields). Related-Bug: #1842989 Change-Id: Ie39067b6e9217eed63f5ae46331ab8a408f0ace6 --- openstack/baremetal/v1/_common.py | 15 +++++++++++ openstack/baremetal/v1/_proxy.py | 2 +- openstack/baremetal/v1/allocation.py | 2 +- openstack/baremetal/v1/chassis.py | 2 +- openstack/baremetal/v1/node.py | 2 +- openstack/baremetal/v1/port.py | 2 +- openstack/baremetal/v1/port_group.py | 2 +- openstack/message/v2/message.py | 2 +- openstack/message/v2/queue.py | 2 +- openstack/message/v2/subscription.py | 2 +- openstack/resource.py | 25 ++++++++++++++----- .../baremetal/test_baremetal_node.py | 8 +++--- .../baremetal/test_baremetal_port.py | 6 ++--- .../tests/unit/baremetal/v1/test_proxy.py | 24 +++++++++++++----- openstack/tests/unit/test_resource.py | 16 +++++++++--- ...metal-fields-convert-857b8804327f1e86.yaml | 5 ++++ 16 files changed, 86 insertions(+), 31 deletions(-) create mode 100644 releasenotes/notes/baremetal-fields-convert-857b8804327f1e86.yaml diff --git a/openstack/baremetal/v1/_common.py b/openstack/baremetal/v1/_common.py index 0c29e50ec..4217c2909 100644 --- a/openstack/baremetal/v1/_common.py +++ b/openstack/baremetal/v1/_common.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import resource + RETRIABLE_STATUS_CODES = [ # HTTP Conflict - happens if a node is locked @@ -91,3 +93,16 @@ def comma_separated_list(value): return None else: return ','.join(value) + + +def fields_type(value, resource_type): + if value is None: + return None + + resource_mapping = { + key: value.name + for key, value in resource_type.__dict__.items() + if isinstance(value, resource.Body) + } + + return comma_separated_list(resource_mapping.get(x, x) for x in value) diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 8a000a535..eea434227 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -41,7 +41,7 @@ def _get_with_fields(self, resource_type, value, fields=None): res = self._get_resource(resource_type, value) kwargs = {} if fields: - kwargs['fields'] = _common.comma_separated_list(fields) + kwargs['fields'] = _common.fields_type(fields, resource_type) return res.fetch( self, error_message="No {resource_type} found for {value}".format( diff --git a/openstack/baremetal/v1/allocation.py b/openstack/baremetal/v1/allocation.py index 40f57ee74..a1b8b728b 100644 --- a/openstack/baremetal/v1/allocation.py +++ b/openstack/baremetal/v1/allocation.py @@ -33,7 +33,7 @@ class Allocation(_common.ListMixin, resource.Resource): _query_mapping = resource.QueryParameters( 'node', 'resource_class', 'state', - fields={'name': 'fields', 'type': _common.comma_separated_list}, + fields={'type': _common.fields_type}, ) # Allocation update is available since 1.57 diff --git a/openstack/baremetal/v1/chassis.py b/openstack/baremetal/v1/chassis.py index 89ac5c571..635177e31 100644 --- a/openstack/baremetal/v1/chassis.py +++ b/openstack/baremetal/v1/chassis.py @@ -33,7 +33,7 @@ class Chassis(_common.ListMixin, resource.Resource): commit_jsonpatch = True _query_mapping = resource.QueryParameters( - fields={'name': 'fields', 'type': _common.comma_separated_list}, + fields={'type': _common.fields_type}, ) #: Timestamp at which the chassis was created. diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index e32c2a5fc..9b81937b6 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -48,7 +48,7 @@ class Node(_common.ListMixin, resource.Resource): _query_mapping = resource.QueryParameters( 'associated', 'conductor_group', 'driver', 'fault', 'provision_state', 'resource_class', - fields={'name': 'fields', 'type': _common.comma_separated_list}, + fields={'type': _common.fields_type}, instance_id='instance_uuid', is_maintenance='maintenance', ) diff --git a/openstack/baremetal/v1/port.py b/openstack/baremetal/v1/port.py index 338c29e01..a5a309526 100644 --- a/openstack/baremetal/v1/port.py +++ b/openstack/baremetal/v1/port.py @@ -31,7 +31,7 @@ class Port(_common.ListMixin, resource.Resource): _query_mapping = resource.QueryParameters( 'address', 'node', 'portgroup', - fields={'name': 'fields', 'type': _common.comma_separated_list}, + fields={'type': _common.fields_type}, node_id='node_uuid', ) diff --git a/openstack/baremetal/v1/port_group.py b/openstack/baremetal/v1/port_group.py index 939ad78bf..2fb55267b 100644 --- a/openstack/baremetal/v1/port_group.py +++ b/openstack/baremetal/v1/port_group.py @@ -31,7 +31,7 @@ class PortGroup(_common.ListMixin, resource.Resource): _query_mapping = resource.QueryParameters( 'node', 'address', - fields={'name': 'fields', 'type': _common.comma_separated_list}, + fields={'type': _common.fields_type}, ) # The mode and properties field introduced in 1.26. diff --git a/openstack/message/v2/message.py b/openstack/message/v2/message.py index 8b8d59385..e30d0f98d 100644 --- a/openstack/message/v2/message.py +++ b/openstack/message/v2/message.py @@ -86,7 +86,7 @@ def list(cls, session, paginated=True, base_path=None, **params): ) or session.get_project_id() } - query_params = cls._query_mapping._transpose(params) + query_params = cls._query_mapping._transpose(params, cls) while more_data: resp = session.get(uri, headers=headers, params=query_params) diff --git a/openstack/message/v2/queue.py b/openstack/message/v2/queue.py index 7d580e486..d6f60a8b9 100644 --- a/openstack/message/v2/queue.py +++ b/openstack/message/v2/queue.py @@ -74,7 +74,7 @@ def list(cls, session, paginated=False, base_path=None, **params): and `X-PROJECT-ID` fields which are required by Zaqar v2 API. """ more_data = True - query_params = cls._query_mapping._transpose(params) + query_params = cls._query_mapping._transpose(params, cls) if base_path is None: base_path = cls.base_path diff --git a/openstack/message/v2/subscription.py b/openstack/message/v2/subscription.py index 3c6322869..d9b129c49 100644 --- a/openstack/message/v2/subscription.py +++ b/openstack/message/v2/subscription.py @@ -93,7 +93,7 @@ def list(cls, session, paginated=True, base_path=None, **params): ) or session.get_project_id() } - query_params = cls._query_mapping._transpose(params) + query_params = cls._query_mapping._transpose(params, cls) while more_data: resp = session.get(uri, headers=headers, params=query_params) diff --git a/openstack/resource.py b/openstack/resource.py index 3eb2da725..9ef70b941 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -32,6 +32,7 @@ class that represent a remote resource. The attributes that """ import collections +import inspect import itertools import jsonpatch @@ -303,8 +304,8 @@ def _validate(self, query, base_path=None, allow_unknown_params=False): """ expected_params = list(self._mapping.keys()) expected_params.extend( - value['name'] if isinstance(value, dict) else value - for value in self._mapping.values()) + value.get('name', key) if isinstance(value, dict) else value + for key, value in self._mapping.items()) if base_path: expected_params += utils.get_string_format_keys(base_path) @@ -323,7 +324,7 @@ def _validate(self, query, base_path=None, allow_unknown_params=False): set(expected_params)) return {k: query[k] for k in known_keys} - def _transpose(self, query): + def _transpose(self, query, resource_type): """Transpose the keys in query based on the mapping If a query is supplied with its server side name, we will still use @@ -332,16 +333,25 @@ def _transpose(self, query): :param dict query: Collection of key-value pairs where each key is the client-side parameter name to be transposed to its server side name. + :param resource_type: Class of a resource. """ result = {} for client_side, server_side in self._mapping.items(): if isinstance(server_side, dict): - name = server_side['name'] + name = server_side.get('name', client_side) type_ = server_side.get('type') else: name = server_side type_ = None + # NOTE(dtantsur): a small hack to be compatible with both + # single-argument (like int) and double-argument type functions. + try: + provide_resource_type = ( + len(inspect.getargspec(type_).args) > 1) + except TypeError: + provide_resource_type = False + if client_side in query: value = query[client_side] elif name in query: @@ -350,7 +360,10 @@ def _transpose(self, query): continue if type_ is not None: - result[name] = type_(value) + if provide_resource_type: + result[name] = type_(value, resource_type) + else: + result[name] = type_(value) else: result[name] = value return result @@ -1568,7 +1581,7 @@ def list(cls, session, paginated=True, base_path=None, params = cls._query_mapping._validate( params, base_path=base_path, allow_unknown_params=allow_unknown_params) - query_params = cls._query_mapping._transpose(params) + query_params = cls._query_mapping._transpose(params, cls) uri = base_path % params limit = query_params.get('limit') diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index a7a07be95..a08f8e1f3 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -34,8 +34,9 @@ def test_node_create_get_delete(self): self.assertEqual(node.id, found.id) self.assertEqual(node.name, found.name) - with_fields = self.conn.baremetal.get_node('node-name', - fields=['uuid', 'driver']) + with_fields = self.conn.baremetal.get_node( + 'node-name', + fields=['uuid', 'driver', 'instance_id']) self.assertEqual(node.id, with_fields.id) self.assertEqual(node.driver, with_fields.driver) self.assertIsNone(with_fields.name) @@ -275,7 +276,8 @@ class TestBareMetalNodeFields(base.BaseBaremetalTest): def test_node_fields(self): self.create_node() - result = self.conn.baremetal.nodes(fields=['uuid', 'name']) + result = self.conn.baremetal.nodes( + fields=['uuid', 'name', 'instance_id']) for item in result: self.assertIsNotNone(item.id) self.assertIsNone(item.driver) diff --git a/openstack/tests/functional/baremetal/test_baremetal_port.py b/openstack/tests/functional/baremetal/test_baremetal_port.py index 2ac7fc050..ee2488f67 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_port.py +++ b/openstack/tests/functional/baremetal/test_baremetal_port.py @@ -33,8 +33,8 @@ def test_port_create_get_delete(self): self.assertEqual(loaded.id, port.id) self.assertIsNotNone(loaded.address) - with_fields = self.conn.baremetal.get_port(port.id, - fields=['uuid', 'extra']) + with_fields = self.conn.baremetal.get_port( + port.id, fields=['uuid', 'extra', 'node_id']) self.assertEqual(port.id, with_fields.id) self.assertIsNone(with_fields.address) @@ -120,7 +120,7 @@ class TestBareMetalPortFields(base.BaseBaremetalTest): def test_port_fields(self): self.create_node() self.create_port(address='11:22:33:44:55:66') - result = self.conn.baremetal.ports(fields=['uuid']) + result = self.conn.baremetal.ports(fields=['uuid', 'node_id']) for item in result: self.assertIsNotNone(item.id) self.assertIsNone(item.address) diff --git a/openstack/tests/unit/baremetal/v1/test_proxy.py b/openstack/tests/unit/baremetal/v1/test_proxy.py index f5902db97..ee3600d88 100644 --- a/openstack/tests/unit/baremetal/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal/v1/test_proxy.py @@ -188,13 +188,25 @@ def test__get_with_fields_none(self, mock_fetch): error_message=mock.ANY) @mock.patch.object(node.Node, 'fetch', autospec=True) - def test__get_with_fields(self, mock_fetch): - result = self.proxy._get_with_fields(node.Node, 'value', - fields=['a', 'b']) + def test__get_with_fields_node(self, mock_fetch): + result = self.proxy._get_with_fields( + # Mix of server-side and client-side fields + node.Node, 'value', fields=['maintenance', 'id', 'instance_id']) self.assertIs(result, mock_fetch.return_value) - mock_fetch.assert_called_once_with(mock.ANY, self.proxy, - error_message=mock.ANY, - fields='a,b') + mock_fetch.assert_called_once_with( + mock.ANY, self.proxy, error_message=mock.ANY, + # instance_id converted to server-side instance_uuid + fields='maintenance,uuid,instance_uuid') + + @mock.patch.object(port.Port, 'fetch', autospec=True) + def test__get_with_fields_port(self, mock_fetch): + result = self.proxy._get_with_fields( + port.Port, 'value', fields=['address', 'id', 'node_id']) + self.assertIs(result, mock_fetch.return_value) + mock_fetch.assert_called_once_with( + mock.ANY, self.proxy, error_message=mock.ANY, + # node_id converted to server-side node_uuid + fields='address,uuid,node_uuid') @mock.patch('time.sleep', lambda _sec: None) diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 1f59cf7e8..fdfff8f23 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -377,21 +377,28 @@ def test_create(self): sot._mapping) def test_transpose_unmapped(self): + def _type(value, rtype): + self.assertIs(rtype, mock.sentinel.resource_type) + return value * 10 + location = "location" mapping = {"first_name": "first-name", "pet_name": {"name": "pet"}, - "answer": {"name": "answer", "type": int}} + "answer": {"name": "answer", "type": int}, + "complex": {"type": _type}} sot = resource.QueryParameters(location, **mapping) result = sot._transpose({"location": "Brooklyn", "first_name": "Brian", "pet_name": "Meow", "answer": "42", - "last_name": "Curtin"}) + "last_name": "Curtin", + "complex": 1}, + mock.sentinel.resource_type) # last_name isn't mapped and shouldn't be included self.assertEqual({"location": "Brooklyn", "first-name": "Brian", - "pet": "Meow", "answer": 42}, + "pet": "Meow", "answer": 42, "complex": 10}, result) def test_transpose_not_in_query(self): @@ -401,7 +408,8 @@ def test_transpose_not_in_query(self): "answer": {"name": "answer", "type": int}} sot = resource.QueryParameters(location, **mapping) - result = sot._transpose({"location": "Brooklyn"}) + result = sot._transpose({"location": "Brooklyn"}, + mock.sentinel.resource_type) # first_name not being in the query shouldn't affect results self.assertEqual({"location": "Brooklyn"}, diff --git a/releasenotes/notes/baremetal-fields-convert-857b8804327f1e86.yaml b/releasenotes/notes/baremetal-fields-convert-857b8804327f1e86.yaml new file mode 100644 index 000000000..07fa11f84 --- /dev/null +++ b/releasenotes/notes/baremetal-fields-convert-857b8804327f1e86.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixes conversion of the bare metal ``fields`` argument from SDK to + server-side field names (e.g. ``instance_id`` to ``instance_uuid``). From cb240b1308768a51572864ee675c26cb87fbfe7a Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 16 Sep 2019 20:37:48 +0200 Subject: [PATCH 2543/3836] Fix image create with tags When passing tags to image create _make_v2_image_params will cast tags to string, while those should remain list. This is required to complete switch in OSC from glanceclient to SDK. Since we use TagMixin remove tags property from resource (it's anyway there). Change-Id: I4f068dfafe4bc85ddda6dbc716dffa0c316500f6 --- openstack/image/v2/_proxy.py | 3 ++- openstack/image/v2/image.py | 2 -- openstack/tests/unit/cloud/test_image.py | 4 +++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index b539b6699..9f9d56226 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -26,6 +26,7 @@ # Rackspace returns this for intermittent import errors _IMAGE_ERROR_396 = "Image cannot be imported. Error code: '396'" _INT_PROPERTIES = ('min_disk', 'min_ram', 'size', 'virtual_size') +_RAW_PROPERTIES = ('protected', 'tags') class Proxy(_base_proxy.BaseImageProxy): @@ -184,7 +185,7 @@ def _make_v2_image_params(self, meta, properties): for k, v in iter(properties.items()): if k in _INT_PROPERTIES: ret[k] = int(v) - elif k == 'protected': + elif k in _RAW_PROPERTIES: ret[k] = v else: if v is None: diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index 13e9ff33c..dd789d5ba 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -104,8 +104,6 @@ class Image(resource.Resource, resource.TagMixin, _download.DownloadMixin): store = resource.Body('store') #: The image status. status = resource.Body('status') - #: Tags, if any, that are associated with the image. - tags = resource.Body('tags') #: The date and time when the image was updated. updated_at = resource.Body('updated_at') #: The virtual size of the image. diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 78c56c4b4..12aec72fd 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -339,7 +339,8 @@ def test_create_image_put_v2(self): u'owner_specified.openstack.sha256': self.fake_image_dict[ 'owner_specified.openstack.sha256'], - u'visibility': u'private'}) + u'visibility': u'private', + u'tags': [u'tag1', u'tag2']}) ), dict(method='PUT', uri=self.get_mock_url( @@ -360,6 +361,7 @@ def test_create_image_put_v2(self): self.cloud.create_image( self.image_name, self.imagefile.name, wait=True, timeout=1, + tags=['tag1', 'tag2'], is_public=False) self.assert_calls() From a810707eb8904f7c9d91c8e125cec407be457ea0 Mon Sep 17 00:00:00 2001 From: inspurericzhang Date: Wed, 18 Sep 2019 14:32:11 +0800 Subject: [PATCH 2544/3836] fix "How To Contribute" url Change-Id: I73435f41b1aaf4575654889a24e27d80153e09e4 --- CONTRIBUTING.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 2478e7b07..be71810e2 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -21,7 +21,7 @@ Pull requests submitted through GitHub will be ignored. .. seealso:: - * https://wiki.openstack.org/wiki/HowToContribute + * https://wiki.openstack.org/wiki/How_To_Contribute * https://wiki.openstack.org/wiki/CLA .. _DeveloperWorkflow: https://docs.openstack.org/infra/manual/developers.html#development-workflow From c1dd675c8ca32f5e8c258b5629187c55bf5af4a0 Mon Sep 17 00:00:00 2001 From: zhurong Date: Thu, 19 Sep 2019 23:45:28 -0700 Subject: [PATCH 2545/3836] Fix the wrong doc use oslo_conf param Change-Id: I95e4515cae03718cf06c93f329b45f0816d07edf --- openstack/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/connection.py b/openstack/connection.py index 17a9ef158..61c954d18 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -131,7 +131,7 @@ conn = connection.Connection( session=session, - oslo_config=CONF) + oslo_conf=CONF) From existing CloudRegion ------------------------- From ce087e2c870a58af0b0dff8cb8c9860803a91b26 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 20 Sep 2019 16:26:43 +0000 Subject: [PATCH 2546/3836] Update master for stable/train Add file to the reno documentation build to show release notes for stable/train. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/train. Change-Id: I56ef38f228378e2dfd9665c0f06c2527864f5b40 Sem-Ver: feature --- releasenotes/source/index.rst | 1 + releasenotes/source/train.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/train.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index b397ae01b..67d709ac8 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ :maxdepth: 1 unreleased + train stein rocky queens diff --git a/releasenotes/source/train.rst b/releasenotes/source/train.rst new file mode 100644 index 000000000..583900393 --- /dev/null +++ b/releasenotes/source/train.rst @@ -0,0 +1,6 @@ +========================== +Train Series Release Notes +========================== + +.. release-notes:: + :branch: stable/train From eb589af1d06d9867cebc478efb13bab97e3274d9 Mon Sep 17 00:00:00 2001 From: pengyuesheng Date: Mon, 23 Sep 2019 16:10:45 +0800 Subject: [PATCH 2547/3836] Update the constraints url For more detail, see http://lists.openstack.org/pipermail/openstack-discuss/2019-May/006478.html Change-Id: I862f001e92526b332fc5610d426fb382f2ae6bae --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index b4ece8ba7..e6287e8dc 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,7 @@ setenv = OS_STDOUT_CAPTURE={env:OS_STDOUT_CAPTURE:true} OS_STDERR_CAPTURE={env:OS_STDERR_CAPTURE:true} deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/branch/master/upper-constraints.txt} + -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt commands = stestr run {posargs} @@ -78,7 +78,7 @@ commands = {toxinidir}/extras/run-ansible-tests.sh -e {envdir} {posargs} [testenv:docs] deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/branch/master/upper-constraints.txt} + -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = @@ -94,7 +94,7 @@ commands = [testenv:releasenotes] deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/branch/master/upper-constraints.txt} + -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html From b7613e1ea838bdf92f71a6c8b1a64da26d48b810 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Mon, 23 Sep 2019 14:34:52 +0200 Subject: [PATCH 2548/3836] Make the bifrost job non-voting Apparently, ironicclient 3.0.0 has broken no-auth support, in turn breaking ironic-inspector in bifrost. Solving it may require a new ironicclient release, so make the job non-voting for now. Change-Id: Ife2e5a1bbe1e55db1134b80074b2c93ae5eb29f3 --- .zuul.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index fb0cb070d..5a1a0e3cd 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -422,7 +422,8 @@ - osc-functional-devstack-tips: voting: false - nodepool-functional-openstack-src - - bifrost-integration-tinyipa-ubuntu-xenial + - bifrost-integration-tinyipa-ubuntu-xenial: + voting: false - metalsmith-integration-openstacksdk-src: voting: false gate: @@ -432,4 +433,3 @@ - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-senlin - nodepool-functional-openstack-src - - bifrost-integration-tinyipa-ubuntu-xenial From b54d03a4c045037ba19d9d3ff678dc5655e9dfc9 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Tue, 24 Sep 2019 08:41:49 -0500 Subject: [PATCH 2549/3836] Make proxy honor raise_exc in REST primitives Previously, invoking a REST primitive (.get(), .put(), etc.) on a proxy would ignore the raise_exc kwarg and always use raise_exc=False, causing any error coming from the actual service [1] to return a Response object even for responses with status >=400. With this change, the raise_exc kwarg is honored: when True, REST primitives with status >=400 will cause an appropriate ksa exception to be raised. [1] as opposed to lower level e.g. communication errors, which would still raise Change-Id: I463e9a63760a6e61827ba957dd9e5d23bd79f4e8 --- lower-constraints.txt | 1 + openstack/proxy.py | 2 +- openstack/tests/unit/test_placement_rest.py | 49 ++++++++++++++++----- test-requirements.txt | 1 + 4 files changed, 42 insertions(+), 11 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index b66d850be..10cae5083 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -1,6 +1,7 @@ appdirs==1.3.0 coverage==4.0 cryptography==2.1 +ddt==1.0.1 decorator==3.4.0 doc8==0.8.0 dogpile.cache==0.6.2 diff --git a/openstack/proxy.py b/openstack/proxy.py index 8849efb4c..724a816d6 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -93,7 +93,7 @@ def request( global_request_id = conn._global_request_id response = super(Proxy, self).request( url, method, - connect_retries=connect_retries, raise_exc=False, + connect_retries=connect_retries, raise_exc=raise_exc, global_request_id=global_request_id, **kwargs) for h in response.history: diff --git a/openstack/tests/unit/test_placement_rest.py b/openstack/tests/unit/test_placement_rest.py index e0c046166..b82b31d52 100644 --- a/openstack/tests/unit/test_placement_rest.py +++ b/openstack/tests/unit/test_placement_rest.py @@ -12,29 +12,58 @@ # License for the specific language governing permissions and limitations # under the License. +import ddt +from keystoneauth1 import exceptions + from openstack.tests.unit import base +@ddt.ddt class TestPlacementRest(base.TestCase): def setUp(self): super(TestPlacementRest, self).setUp() self.use_placement() - def test_discovery(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'placement', 'public', append=['allocation_candidates']), - json={}) - ]) - rs = self.cloud.placement.get('/allocation_candidates') - self.assertEqual(200, rs.status_code) + def _register_uris(self, status_code=None): + uri = dict( + method='GET', + uri=self.get_mock_url( + 'placement', 'public', append=['allocation_candidates']), + json={}) + if status_code is not None: + uri['status_code'] = status_code + self.register_uris([uri]) + + def _validate_resp(self, resp, status_code): + self.assertEqual(status_code, resp.status_code) self.assertEqual( 'https://placement.example.com/allocation_candidates', - rs.url) + resp.url) self.assert_calls() + @ddt.data({}, {'raise_exc': False}, {'raise_exc': True}) + def test_discovery(self, get_kwargs): + self._register_uris() + # Regardless of raise_exc, a <400 response doesn't raise + rs = self.cloud.placement.get('/allocation_candidates', **get_kwargs) + self._validate_resp(rs, 200) + + @ddt.data({}, {'raise_exc': False}) + def test_discovery_err(self, get_kwargs): + self._register_uris(status_code=500) + # >=400 doesn't raise by default or with explicit raise_exc=False + rs = self.cloud.placement.get('/allocation_candidates', **get_kwargs) + self._validate_resp(rs, 500) + + def test_discovery_exc(self): + self._register_uris(status_code=500) + # raise_exc=True raises a ksa exception appropriate to the status code + ex = self.assertRaises( + exceptions.InternalServerError, + self.cloud.placement.get, '/allocation_candidates', raise_exc=True) + self._validate_resp(ex.response, 500) + def test_microversion_discovery(self): self.assertEqual( (1, 17), diff --git a/test-requirements.txt b/test-requirements.txt index feb8d781c..ce8b81d83 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,6 +4,7 @@ hacking>=1.0,<1.2 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 +ddt>=1.0.1 # MIT extras>=1.0.0 # MIT fixtures>=3.0.0 # Apache-2.0/BSD jsonschema>=2.6.0 # MIT From 46d4d7337eafb0ecfcbfab1bd11de7bdb393849a Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Wed, 25 Sep 2019 14:26:58 +0200 Subject: [PATCH 2550/3836] Add a non-voting ironic-inspector job Both ironic and ironic-inspector rely on openstacksdk for x-project communication. The ironic-inspector-tempest job covers accessing ironic (from nova) and swift (from ironic and inspector). In Ussuri we hope to start using openstacksdk for accessing glance, neutron and cinder. Change-Id: I42bf7d31dcb0a53006c6773c8f208b8b5c8bac12 --- .zuul.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.zuul.yaml b/.zuul.yaml index 5a1a0e3cd..a4a812701 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -380,6 +380,12 @@ required-projects: - openstack/openstacksdk +- job: + name: ironic-inspector-tempest-openstacksdk-src + parent: ironic-inspector-tempest + required-projects: + - openstack/openstacksdk + - project-template: name: openstacksdk-functional-tips check: @@ -422,6 +428,9 @@ - osc-functional-devstack-tips: voting: false - nodepool-functional-openstack-src + # Ironic jobs, non-voting to avoid tight coupling + - ironic-inspector-tempest-openstacksdk-src: + voting: false - bifrost-integration-tinyipa-ubuntu-xenial: voting: false - metalsmith-integration-openstacksdk-src: From d1e1aa931cc92bb464b6499e4c25fec551d23e31 Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Mon, 23 Sep 2019 03:46:12 +0000 Subject: [PATCH 2551/3836] Support vendor data in configdrive building This change will allow a vendor_data dict to be supplied for configdrive building, resulting in that data being written to the vendor_data2.json file, which is what nova does[1] when building its configdrive. [1] https://www.madebymikal.com/nova-vendordata-deployment-an-excessively-detailed-guide/ Change-Id: I641a7d9e7214d0d4edbb9d9dd30b524fd282b242 Story: 2006597 Task: 36757 --- openstack/baremetal/configdrive.py | 14 +++++++++++--- .../tests/unit/baremetal/test_configdrive.py | 17 +++++++++++++++-- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/openstack/baremetal/configdrive.py b/openstack/baremetal/configdrive.py index 74e95e451..f189d5e6b 100644 --- a/openstack/baremetal/configdrive.py +++ b/openstack/baremetal/configdrive.py @@ -26,13 +26,14 @@ @contextlib.contextmanager def populate_directory(metadata, user_data=None, versions=None, - network_data=None): + network_data=None, vendor_data=None): """Populate a directory with configdrive files. :param dict metadata: Metadata. :param bytes user_data: Vendor-specific user data. :param versions: List of metadata versions to support. :param dict network_data: Networking configuration. + :param dict vendor_data: Extra supplied vendor data. :return: a context manager yielding a directory with files """ d = tempfile.mkdtemp() @@ -51,6 +52,11 @@ def populate_directory(metadata, user_data=None, versions=None, 'w') as fp: json.dump(network_data, fp) + if vendor_data: + with open(os.path.join(subdir, 'vendor_data2.json'), + 'w') as fp: + json.dump(vendor_data, fp) + if user_data: # Strictly speaking, user data is binary, but in many cases # it's actually a text (cloud-init, ignition, etc). @@ -64,7 +70,8 @@ def populate_directory(metadata, user_data=None, versions=None, shutil.rmtree(d) -def build(metadata, user_data=None, versions=None, network_data=None): +def build(metadata, user_data=None, versions=None, network_data=None, + vendor_data=None): """Make a configdrive compatible with the Bare Metal service. Requires the genisoimage utility to be available. @@ -73,10 +80,11 @@ def build(metadata, user_data=None, versions=None, network_data=None): :param user_data: Vendor-specific user data. :param versions: List of metadata versions to support. :param dict network_data: Networking configuration. + :param dict vendor_data: Extra supplied vendor data. :return: configdrive contents as a base64-encoded string. """ with populate_directory(metadata, user_data, versions, - network_data) as path: + network_data, vendor_data) as path: return pack(path) diff --git a/openstack/tests/unit/baremetal/test_configdrive.py b/openstack/tests/unit/baremetal/test_configdrive.py index 709f3b124..9b17963a4 100644 --- a/openstack/tests/unit/baremetal/test_configdrive.py +++ b/openstack/tests/unit/baremetal/test_configdrive.py @@ -24,10 +24,12 @@ class TestPopulateDirectory(testtools.TestCase): - def _check(self, metadata, user_data=None, network_data=None): + def _check(self, metadata, user_data=None, network_data=None, + vendor_data=None): with configdrive.populate_directory(metadata, user_data=user_data, - network_data=network_data) as d: + network_data=network_data, + vendor_data=vendor_data) as d: for version in ('2012-08-10', 'latest'): with open(os.path.join(d, 'openstack', version, 'meta_data.json')) as fp: @@ -38,6 +40,8 @@ def _check(self, metadata, user_data=None, network_data=None): 'network_data.json') user_data_file = os.path.join(d, 'openstack', version, 'user_data') + vendor_data_file = os.path.join(d, 'openstack', version, + 'vendor_data2.json') if network_data is None: self.assertFalse(os.path.exists(network_data_file)) @@ -45,6 +49,12 @@ def _check(self, metadata, user_data=None, network_data=None): with open(network_data_file) as fp: self.assertEqual(network_data, json.load(fp)) + if vendor_data is None: + self.assertFalse(os.path.exists(vendor_data_file)) + else: + with open(vendor_data_file) as fp: + self.assertEqual(vendor_data, json.load(fp)) + if user_data is None: self.assertFalse(os.path.exists(user_data_file)) else: @@ -68,6 +78,9 @@ def test_with_user_data_as_string(self): def test_with_network_data(self): self._check({'foo': 42}, network_data={'networks': {}}) + def test_with_vendor_data(self): + self._check({'foo': 42}, vendor_data={'foo': 'bar'}) + @mock.patch('subprocess.Popen', autospec=True) class TestPack(testtools.TestCase): From 5e927dce69b1db750bc346c366fd31c97e1e6fbf Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 1 Oct 2019 17:53:17 +0200 Subject: [PATCH 2552/3836] Use has_service in functional test's require_service Under the covers we want to use has_service, which respects the overall sdk infrastructure for skipping unnecessary discovery. Change-Id: Ib47bdc98ffa63ec70b89169574cb03057beecb42 --- openstack/tests/functional/base.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index 43900c99a..2c2a154a6 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -14,7 +14,6 @@ import openstack.config from keystoneauth1 import discover -from keystoneauth1 import exceptions as _exceptions from openstack import connection from openstack.tests import base @@ -114,8 +113,6 @@ def cleanup(): self.assertIsNone(result) self.addCleanup(cleanup) - # TODO(shade) Replace this with call to conn.has_service when we've merged - # the shade methods into Connection. def require_service(self, service_type, min_microversion=None, **kwargs): """Method to check whether a service exists @@ -128,16 +125,16 @@ def setUp(self): :returns: True if the service exists, otherwise False. """ - try: - data = self.conn.session.get_endpoint_data( - service_type=service_type, **kwargs) - except _exceptions.EndpointNotFound: + if not self.conn.has_service(service_type): self.skipTest('Service {service_type} not found in cloud'.format( service_type=service_type)) if not min_microversion: return + data = self.conn.session.get_endpoint_data( + service_type=service_type, **kwargs) + if not (data.min_microversion and data.max_microversion and discover.version_between( From c9fba05bf6231cc63d1b286d6c41852601c95920 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 4 Oct 2019 09:59:11 +0200 Subject: [PATCH 2553/3836] Use generated list of services instead of metaclass The metaclass makes it pretty impossible to do type annotations on anything. Turn the metaclass into a utility script and then generate the list of mappings. Change-Id: Iccd5ee34e364cd6f53c0a8934e20442c022844e8 --- openstack/_meta/__init__.py | 0 openstack/_meta/connection.py | 126 -------------------------------- openstack/_services_mixin.py | 131 ++++++++++++++++++++++++++++++++++ openstack/connection.py | 37 +++++----- tools/print-services.py | 128 +++++++++++++++++++++++++++++++++ tox.ini | 2 +- 6 files changed, 279 insertions(+), 145 deletions(-) delete mode 100644 openstack/_meta/__init__.py delete mode 100644 openstack/_meta/connection.py create mode 100644 openstack/_services_mixin.py create mode 100644 tools/print-services.py diff --git a/openstack/_meta/__init__.py b/openstack/_meta/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/openstack/_meta/connection.py b/openstack/_meta/connection.py deleted file mode 100644 index 61db44b0c..000000000 --- a/openstack/_meta/connection.py +++ /dev/null @@ -1,126 +0,0 @@ -# Copyright 2018 Red Hat, 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 importlib -import warnings - -import os_service_types - -from openstack import _log -from openstack import service_description - -_logger = _log.setup_logging('openstack') -_service_type_manager = os_service_types.ServiceTypes() -_DOC_TEMPLATE = ( - ":class:`{class_name}` for {service_type} aka {project}") -_PROXY_TEMPLATE = """Proxy for {service_type} aka {project} - -This proxy object could be an instance of -{class_doc_strings} -depending on client configuration and which version of the service is -found on remotely on the cloud. -""" - - -class ConnectionMeta(type): - def __new__(meta, name, bases, dct): - for service in _service_type_manager.services: - service_type = service['service_type'] - if service_type == 'ec2-api': - # NOTE(mordred) It doesn't make any sense to use ec2-api - # from openstacksdk. The credentials API calls are all calls - # on identity endpoints. - continue - desc_class = _find_service_description_class(service_type) - descriptor_args = {'service_type': service_type} - - if not desc_class.supported_versions: - doc = _DOC_TEMPLATE.format( - class_name="{service_type} Proxy".format( - service_type=service_type), - **service) - elif len(desc_class.supported_versions) == 1: - supported_version = list( - desc_class.supported_versions.keys())[0] - doc = _DOC_TEMPLATE.format( - class_name="{service_type} Proxy <{name}>".format( - service_type=service_type, name=supported_version), - **service) - else: - class_doc_strings = "\n".join([ - ":class:`{class_name}`".format( - class_name=proxy_class.__name__) - for proxy_class in desc_class.supported_versions.values()]) - doc = _PROXY_TEMPLATE.format( - class_doc_strings=class_doc_strings, **service) - descriptor = desc_class(**descriptor_args) - descriptor.__doc__ = doc - st = service_type.replace('-', '_') - dct[st] = descriptor - - # Register the descriptor class with every known alias. Don't - # add doc strings though - although they are supported, we don't - # want to give anybody any bad ideas. Making a second descriptor - # does not introduce runtime cost as the descriptors all use - # the same _proxies dict on the instance. - for alias_name in _get_aliases(st): - if alias_name[-1].isdigit(): - continue - alias_descriptor = desc_class(**descriptor_args) - dct[alias_name.replace('-', '_')] = alias_descriptor - return super(ConnectionMeta, meta).__new__(meta, name, bases, dct) - - -def _get_aliases(service_type, aliases=None): - # We make connection attributes for all official real type names - # and aliases. Three services have names they were called by in - # openstacksdk that are not covered by Service Types Authority aliases. - # Include them here - but take heed, no additional values should ever - # be added to this list. - # that were only used in openstacksdk resource naming. - LOCAL_ALIASES = { - 'baremetal': 'bare_metal', - 'block_storage': 'block_store', - 'clustering': 'cluster', - } - all_types = set(_service_type_manager.get_aliases(service_type)) - if aliases: - all_types.update(aliases) - if service_type in LOCAL_ALIASES: - all_types.add(LOCAL_ALIASES[service_type]) - return all_types - - -def _find_service_description_class(service_type): - package_name = 'openstack.{service_type}'.format( - service_type=service_type).replace('-', '_') - module_name = service_type.replace('-', '_') + '_service' - class_name = ''.join( - [part.capitalize() for part in module_name.split('_')]) - try: - import_name = '.'.join([package_name, module_name]) - service_description_module = importlib.import_module(import_name) - except ImportError as e: - # ImportWarning is ignored by default. This warning is here - # as an opt-in for people trying to figure out why something - # didn't work. - warnings.warn( - "Could not import {service_type} service description: {e}".format( - service_type=service_type, e=str(e)), - ImportWarning) - return service_description.ServiceDescription - # There are no cases in which we should have a module but not the class - # inside it. - service_description_class = getattr(service_description_module, class_name) - return service_description_class diff --git a/openstack/_services_mixin.py b/openstack/_services_mixin.py new file mode 100644 index 000000000..5f8fbedf6 --- /dev/null +++ b/openstack/_services_mixin.py @@ -0,0 +1,131 @@ +# Generated file, to change, run tools/print-services.py +from openstack import service_description +from openstack.baremetal import baremetal_service +from openstack.baremetal_introspection import baremetal_introspection_service +from openstack.block_storage import block_storage_service +from openstack.clustering import clustering_service +from openstack.compute import compute_service +from openstack.database import database_service +from openstack.dns import dns_service +from openstack.identity import identity_service +from openstack.image import image_service +from openstack.instance_ha import instance_ha_service +from openstack.key_manager import key_manager_service +from openstack.load_balancer import load_balancer_service +from openstack.message import message_service +from openstack.network import network_service +from openstack.object_store import object_store_service +from openstack.orchestration import orchestration_service +from openstack.workflow import workflow_service + + +class ServicesMixin(object): + + identity = identity_service.IdentityService(service_type='identity') + + compute = compute_service.ComputeService(service_type='compute') + + image = image_service.ImageService(service_type='image') + + load_balancer = load_balancer_service.LoadBalancerService(service_type='load-balancer') + + object_store = object_store_service.ObjectStoreService(service_type='object-store') + + clustering = clustering_service.ClusteringService(service_type='clustering') + resource_cluster = clustering + cluster = clustering + + data_processing = service_description.ServiceDescription(service_type='data-processing') + + baremetal = baremetal_service.BaremetalService(service_type='baremetal') + bare_metal = baremetal + + baremetal_introspection = baremetal_introspection_service.BaremetalIntrospectionService(service_type='baremetal-introspection') + + key_manager = key_manager_service.KeyManagerService(service_type='key-manager') + + resource_optimization = service_description.ServiceDescription(service_type='resource-optimization') + infra_optim = resource_optimization + + message = message_service.MessageService(service_type='message') + messaging = message + + application_catalog = service_description.ServiceDescription(service_type='application-catalog') + + container_infrastructure_management = service_description.ServiceDescription(service_type='container-infrastructure-management') + container_infrastructure = container_infrastructure_management + container_infra = container_infrastructure_management + + search = service_description.ServiceDescription(service_type='search') + + dns = dns_service.DnsService(service_type='dns') + + workflow = workflow_service.WorkflowService(service_type='workflow') + + rating = service_description.ServiceDescription(service_type='rating') + + operator_policy = service_description.ServiceDescription(service_type='operator-policy') + policy = operator_policy + + shared_file_system = service_description.ServiceDescription(service_type='shared-file-system') + share = shared_file_system + + data_protection_orchestration = service_description.ServiceDescription(service_type='data-protection-orchestration') + + orchestration = orchestration_service.OrchestrationService(service_type='orchestration') + + block_storage = block_storage_service.BlockStorageService(service_type='block-storage') + block_store = block_storage + volume = block_storage + + alarm = service_description.ServiceDescription(service_type='alarm') + alarming = alarm + + meter = service_description.ServiceDescription(service_type='meter') + metering = meter + telemetry = meter + + event = service_description.ServiceDescription(service_type='event') + events = event + + application_deployment = service_description.ServiceDescription(service_type='application-deployment') + application_deployment = application_deployment + + multi_region_network_automation = service_description.ServiceDescription(service_type='multi-region-network-automation') + tricircle = multi_region_network_automation + + database = database_service.DatabaseService(service_type='database') + + application_container = service_description.ServiceDescription(service_type='application-container') + container = application_container + + root_cause_analysis = service_description.ServiceDescription(service_type='root-cause-analysis') + rca = root_cause_analysis + + nfv_orchestration = service_description.ServiceDescription(service_type='nfv-orchestration') + + network = network_service.NetworkService(service_type='network') + + backup = service_description.ServiceDescription(service_type='backup') + + monitoring_logging = service_description.ServiceDescription(service_type='monitoring-logging') + monitoring_log_api = monitoring_logging + + monitoring = service_description.ServiceDescription(service_type='monitoring') + + monitoring_events = service_description.ServiceDescription(service_type='monitoring-events') + + placement = service_description.ServiceDescription(service_type='placement') + + instance_ha = instance_ha_service.InstanceHaService(service_type='instance-ha') + ha = instance_ha + + reservation = service_description.ServiceDescription(service_type='reservation') + + function_engine = service_description.ServiceDescription(service_type='function-engine') + + accelerator = service_description.ServiceDescription(service_type='accelerator') + + admin_logic = service_description.ServiceDescription(service_type='admin-logic') + registration = admin_logic + diff --git a/openstack/connection.py b/openstack/connection.py index 17a9ef158..2a14995b5 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -183,7 +183,7 @@ import six from openstack import _log -from openstack._meta import connection as _meta +from openstack import _services_mixin from openstack.cloud import openstackcloud as _cloud from openstack.cloud import _baremetal from openstack.cloud import _block_storage @@ -245,23 +245,24 @@ def from_config(cloud=None, config=None, options=None, **kwargs): return Connection(config=config) -class Connection(six.with_metaclass(_meta.ConnectionMeta, - _cloud._OpenStackCloudMixin, - _baremetal.BaremetalCloudMixin, - _block_storage.BlockStorageCloudMixin, - _compute.ComputeCloudMixin, - _clustering.ClusteringCloudMixin, - _coe.CoeCloudMixin, - _dns.DnsCloudMixin, - _floating_ip.FloatingIPCloudMixin, - _identity.IdentityCloudMixin, - _image.ImageCloudMixin, - _network.NetworkCloudMixin, - _network_common.NetworkCommonCloudMixin, - _object_store.ObjectStoreCloudMixin, - _orchestration.OrchestrationCloudMixin, - _security_group.SecurityGroupCloudMixin - )): +class Connection( + _services_mixin.ServicesMixin, + _cloud._OpenStackCloudMixin, + _baremetal.BaremetalCloudMixin, + _block_storage.BlockStorageCloudMixin, + _compute.ComputeCloudMixin, + _clustering.ClusteringCloudMixin, + _coe.CoeCloudMixin, + _dns.DnsCloudMixin, + _floating_ip.FloatingIPCloudMixin, + _identity.IdentityCloudMixin, + _image.ImageCloudMixin, + _network.NetworkCloudMixin, + _network_common.NetworkCommonCloudMixin, + _object_store.ObjectStoreCloudMixin, + _orchestration.OrchestrationCloudMixin, + _security_group.SecurityGroupCloudMixin +): def __init__(self, cloud=None, config=None, session=None, app_name=None, app_version=None, diff --git a/tools/print-services.py b/tools/print-services.py new file mode 100644 index 000000000..33872acd1 --- /dev/null +++ b/tools/print-services.py @@ -0,0 +1,128 @@ +# Copyright 2018 Red Hat, 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 importlib +import warnings + +import os_service_types + +from openstack import _log +from openstack import service_description + +_logger = _log.setup_logging('openstack') +_service_type_manager = os_service_types.ServiceTypes() + + +def make_names(): + imports = ['from openstack import service_description'] + services = [] + + for service in _service_type_manager.services: + service_type = service['service_type'] + if service_type == 'ec2-api': + # NOTE(mordred) It doesn't make any sense to use ec2-api + # from openstacksdk. The credentials API calls are all calls + # on identity endpoints. + continue + desc_class = _find_service_description_class(service_type) + + st = service_type.replace('-', '_') + + if desc_class.__module__ != 'openstack.service_description': + base_mod, dm = desc_class.__module__.rsplit('.', 1) + imports.append( + 'from {base_mod} import {dm}'.format( + base_mod=base_mod, + dm=dm)) + else: + dm = 'service_description' + + dc = desc_class.__name__ + services.append( + "{st} = {dm}.{dc}(service_type='{service_type}')".format( + st=st, dm=dm, dc=dc, service_type=service_type), + ) + + # Register the descriptor class with every known alias. Don't + # add doc strings though - although they are supported, we don't + # want to give anybody any bad ideas. Making a second descriptor + # does not introduce runtime cost as the descriptors all use + # the same _proxies dict on the instance. + for alias_name in _get_aliases(st): + if alias_name[-1].isdigit(): + continue + services.append( + '{alias_name} = {st}'.format( + alias_name=alias_name, + st=st)) + services.append('') + print("# Generated file, to change, run tools/print-services.py") + for imp in sorted(imports): + print(imp) + print('\n') + print("class ServicesMixin(object):\n") + for service in services: + if service: + print(" {service}".format(service=service)) + else: + print() + + +def _get_aliases(service_type, aliases=None): + # We make connection attributes for all official real type names + # and aliases. Three services have names they were called by in + # openstacksdk that are not covered by Service Types Authority aliases. + # Include them here - but take heed, no additional values should ever + # be added to this list. + # that were only used in openstacksdk resource naming. + LOCAL_ALIASES = { + 'baremetal': 'bare_metal', + 'block_storage': 'block_store', + 'clustering': 'cluster', + } + all_types = set(_service_type_manager.get_aliases(service_type)) + if aliases: + all_types.update(aliases) + if service_type in LOCAL_ALIASES: + all_types.add(LOCAL_ALIASES[service_type]) + all_aliases = set() + for alias in all_types: + all_aliases.add(alias.replace('-', '_')) + return all_aliases + + +def _find_service_description_class(service_type): + package_name = 'openstack.{service_type}'.format( + service_type=service_type).replace('-', '_') + module_name = service_type.replace('-', '_') + '_service' + class_name = ''.join( + [part.capitalize() for part in module_name.split('_')]) + try: + import_name = '.'.join([package_name, module_name]) + service_description_module = importlib.import_module(import_name) + except ImportError as e: + # ImportWarning is ignored by default. This warning is here + # as an opt-in for people trying to figure out why something + # didn't work. + warnings.warn( + "Could not import {service_type} service description: {e}".format( + service_type=service_type, e=str(e)), + ImportWarning) + return service_description.ServiceDescription + # There are no cases in which we should have a module but not the class + # inside it. + service_description_class = getattr(service_description_module, class_name) + return service_description_class + +make_names() diff --git a/tox.ini b/tox.ini index 5322f1012..c1b214803 100644 --- a/tox.ini +++ b/tox.ini @@ -101,7 +101,7 @@ commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasen # breaks should occur before the binary operator for readability. ignore = H306,H4,W503 show-source = True -exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build +exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,openstack/_services_mixin.py [doc8] extensions = .rst, .yaml From 7fcfeeb08f3d1ec2417492a7277dd86f01269011 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 3 Oct 2019 11:59:36 +0200 Subject: [PATCH 2554/3836] Start supporting type info This is super basic, but it's a starting point to us being able to start adding typing info. Adding it with python2 syntax for now, since we haven't dropped py2 yet. Change-Id: I4f9b95b02d43c81acb60d224584d60331f5a7784 --- openstack/__init__.py | 7 +++++-- openstack/py.typed | 0 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 openstack/py.typed diff --git a/openstack/__init__.py b/openstack/__init__.py index e7db55965..06aa5236d 100644 --- a/openstack/__init__.py +++ b/openstack/__init__.py @@ -24,10 +24,13 @@ def connect( cloud=None, - app_name=None, app_version=None, + app_name=None, # type: Optional[str] + app_version=None, # type: Optional[str] options=None, - load_yaml_config=True, load_envvars=True, + load_yaml_config=True, # type: bool + load_envvars=True, # type: bool **kwargs): + # type: (...) -> openstack.connection.Connection """Create a :class:`~openstack.connection.Connection` :param string cloud: diff --git a/openstack/py.typed b/openstack/py.typed new file mode 100644 index 000000000..e69de29bb From 9874fa81d7227b9ab41a5448bca55d1ac265f3e7 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 1 Oct 2019 18:22:57 +0200 Subject: [PATCH 2555/3836] Fix server for later microversion We updated the server record for 2.72 but missed some bits of inventory because we weren't actually gettting 2.72. Whoops. We need to clean the policy/policies fields in ServerGroup because otherwise they're considered dirty and None values are sent. Change-Id: Id1720774bc0f6f398d0739a466c7bd5c54182642 --- openstack/cloud/meta.py | 15 +++++++++++---- openstack/compute/v2/server_group.py | 4 ++-- .../tests/functional/cloud/test_inventory.py | 10 ++++++++-- .../fix-for-microversion-70cd686b6d6e3fd0.yaml | 14 ++++++++++++++ 4 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/fix-for-microversion-70cd686b6d6e3fd0.yaml diff --git a/openstack/cloud/meta.py b/openstack/cloud/meta.py index 0ff6fb1dc..a651d0365 100644 --- a/openstack/cloud/meta.py +++ b/openstack/cloud/meta.py @@ -484,10 +484,17 @@ def get_hostvars_from_server(cloud, server, mounts=None): """ server_vars = add_server_interfaces(cloud, server) - flavor_id = server['flavor']['id'] - flavor_name = cloud.get_flavor_name(flavor_id) - if flavor_name: - server_vars['flavor']['name'] = flavor_name + flavor_id = server['flavor'].get('id') + if flavor_id: + # In newer nova, the flavor record can be kept around for flavors + # that no longer exist. The id and name are not there. + flavor_name = cloud.get_flavor_name(flavor_id) + if flavor_name: + server_vars['flavor']['name'] = flavor_name + elif 'original_name' in server['flavor']: + # Users might be have code still expecting name. That name is in + # original_name. + server_vars['flavor']['name'] = server['flavor']['original_name'] expand_server_security_groups(cloud, server) diff --git a/openstack/compute/v2/server_group.py b/openstack/compute/v2/server_group.py index 78a577e9a..fe463504e 100644 --- a/openstack/compute/v2/server_group.py +++ b/openstack/compute/v2/server_group.py @@ -70,7 +70,7 @@ def _get_microversion_for(self, session, action): if self.policies: if not self.policy and isinstance(self.policies, list): self.policy = self.policies[0] - self.policies = None + self._body.clean(only={'policies'}) microversion = self._max_microversion else: if self.rules: @@ -80,6 +80,6 @@ def _get_microversion_for(self, session, action): if self.policy: if not self.policies: self.policies = [self.policy] - self.policy = None + self._body.clean(only={'policy'}) return microversion diff --git a/openstack/tests/functional/cloud/test_inventory.py b/openstack/tests/functional/cloud/test_inventory.py index bfe8b11f2..59e12aa59 100644 --- a/openstack/tests/functional/cloud/test_inventory.py +++ b/openstack/tests/functional/cloud/test_inventory.py @@ -49,12 +49,15 @@ def _cleanup_server(self): def _test_host_content(self, host): self.assertEqual(host['image']['id'], self.image.id) self.assertNotIn('links', host['image']) - self.assertEqual(host['flavor']['id'], self.flavor.id) + # TODO(mordred) Add this back wnen ksa releases + # self.assertNotIn('id', host['flavor']) self.assertNotIn('links', host['flavor']) self.assertNotIn('links', host) self.assertIsInstance(host['volumes'], list) self.assertIsInstance(host['metadata'], dict) self.assertIn('interface_ip', host) + # TODO(mordred) Add this back wnen ksa releases + # self.assertIn('ram', host['flavor']) def _test_expanded_host_content(self, host): self.assertEqual(host['image']['name'], self.image.name) @@ -81,9 +84,12 @@ def test_get_host_no_detail(self): self.assertEqual(host['image']['id'], self.image.id) self.assertNotIn('links', host['image']) self.assertNotIn('name', host['name']) - self.assertEqual(host['flavor']['id'], self.flavor.id) + # TODO(mordred) Add this back wnen ksa releases + # self.assertNotIn('id', host['flavor']) self.assertNotIn('links', host['flavor']) self.assertNotIn('name', host['flavor']) + # TODO(mordred) Add this back wnen ksa releases + # self.assertIn('ram', host['flavor']) host_found = False for host in self.inventory.list_hosts(expand=False): diff --git a/releasenotes/notes/fix-for-microversion-70cd686b6d6e3fd0.yaml b/releasenotes/notes/fix-for-microversion-70cd686b6d6e3fd0.yaml new file mode 100644 index 000000000..be402ef98 --- /dev/null +++ b/releasenotes/notes/fix-for-microversion-70cd686b6d6e3fd0.yaml @@ -0,0 +1,14 @@ +--- +fixes: + - | + In April 2019 the microversion support for the Server resource was increased + to ``2.72``. Unfortunately, due to an issue with version discovery documents, + this increase never actually became effective. A fix is coming in ``3.17.2`` of + ``keystoneauth`` which will unbreak version discovery and cause the microversion + support to start working. +upgrade: + - | + Due to the fix in microversion support in `keystoneauth`, Servers will be + fetched using microversion ``2.72``. Code that assumes the existence of a + ``flavor.id`` field in the Server record should be removed, as it does not exist + in new microversions and cannot be filled in behind the scenes. From d7233f43dce3bd8ec29f1f742afe0a9770dc017c Mon Sep 17 00:00:00 2001 From: melissaml Date: Fri, 18 Oct 2019 18:01:34 +0800 Subject: [PATCH 2556/3836] Bump the openstackdocstheme extension to 1.20 Some options are now automatically configured by the version 1.20: - project - html_last_updated_fmt - latex_engine - latex_elements - version - release. Change-Id: If8debe45c5d4891e2ba40c6fe0d837b27a944578 --- releasenotes/source/conf.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py index 47ae299b6..f2f9c186e 100644 --- a/releasenotes/source/conf.py +++ b/releasenotes/source/conf.py @@ -46,7 +46,6 @@ repository_name = 'openstack/openstacksdk' bug_project = '760' bug_tag = '' -html_last_updated_fmt = '%Y-%m-%d %H:%M' # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -61,7 +60,6 @@ master_doc = 'index' # General information about the project. -project = u'OpenStack SDK Release Notes' copyright = u'2017, Various members of the OpenStack Foundation' # Release notes are version independent. @@ -196,17 +194,6 @@ # -- 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]). From 27005c33253ab4dc2abc8246c00fd5cd36af2eb8 Mon Sep 17 00:00:00 2001 From: Jihad Dwidari Date: Tue, 15 Oct 2019 07:55:56 -0400 Subject: [PATCH 2557/3836] Fixes get_user when identity responses are paged Story: 2006720 The API already supports server side filtering on a username using the 'name' parameter https://docs.openstack.org/api-ref/identity/v3/index.html?expanded=list-users-detail#users I'm making use of that by doing the following 1- in _identity.get_user() I'm adding the var 'name_or_id' as the 'name' key to the **kwargs being passed... to _utils._get_entity _get_entity does not use the **kwargs except to pass to _identity.search_users method 2- _identity.search_users passes the **kwargs as API parameters Before In [1]: import openstack; cloud = openstack.connect() In [2]: domain = cloud.identity.find_domain('cisco') In [3]: cloud.get_user(name_or_id='jdwidari') In [4]: cloud.get_user(name_or_id='jdwidari', domain_id=domain.id) After In [1]: import openstack; cloud = openstack.connect() In [2]: domain = cloud.identity.find_domain('cisco') In [3]: cloud.get_user(name_or_id='jdwidari') In [4]: cloud.get_user(name_or_id='jdwidari', domain_id=domain.id) Out[4]: Munch({'id': '9ef350a7e6087436b2a005f24546e7e96a2d8c16cd34b0b368ae8eecb9a4f00a', 'email': 'jdwidari@cisco.com', 'name': 'jdwidari', 'username': None, 'default_project_id': None, 'domain_id': '31560833db2048e7bf6ce4c365c28d53', 'enabled': True, 'description': 'Jihad Dwidari'}) In [5]: updated the following unit tests - cloud.test_role_assignments - cloud.test_users Annotated some of the test_role_assignment tests to make it easier to understand which API calls are associated with which method calls. Change-Id: Icf68dceb3124ceaea0baa94bc018927dabba42cd --- openstack/cloud/_identity.py | 12 +- .../tests/unit/cloud/test_role_assignment.py | 230 +++++++++++++++--- openstack/tests/unit/cloud/test_users.py | 11 +- 3 files changed, 209 insertions(+), 44 deletions(-) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index 9e825cadf..e03a1ed6a 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -173,7 +173,7 @@ def delete_project(self, name_or_id, domain_id=None): return True - @_utils.valid_kwargs('domain_id') + @_utils.valid_kwargs('domain_id', 'name') @_utils.cache_on_arguments() def list_users(self, **kwargs): """List users. @@ -189,7 +189,7 @@ def list_users(self, **kwargs): return _utils.normalize_users( self._get_and_munchify('users', data)) - @_utils.valid_kwargs('domain_id') + @_utils.valid_kwargs('domain_id', 'name') def search_users(self, name_or_id=None, filters=None, **kwargs): """Search users. @@ -205,6 +205,11 @@ def search_users(self, name_or_id=None, filters=None, **kwargs): :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ + # NOTE(jdwidari) if name_or_id isn't UUID like then make use of server- + # side filter for user name https://bit.ly/2qh0Ijk + # especially important when using LDAP and using page to limit results + if name_or_id and not _utils._is_uuid_like(name_or_id): + kwargs['name'] = name_or_id users = self.list_users(**kwargs) return _utils._filter_list(users, name_or_id, filters) @@ -224,6 +229,9 @@ def get_user(self, name_or_id, filters=None, **kwargs): :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ + if not _utils._is_uuid_like(name_or_id): + kwargs['name'] = name_or_id + return _utils._get_entity(self, 'user', name_or_id, filters, **kwargs) def get_user_by_id(self, user_id, normalize=True): diff --git a/openstack/tests/unit/cloud/test_role_assignment.py b/openstack/tests/unit/cloud/test_role_assignment.py index 2e4b89181..d08165470 100644 --- a/openstack/tests/unit/cloud/test_role_assignment.py +++ b/openstack/tests/unit/cloud/test_role_assignment.py @@ -94,13 +94,17 @@ def get_mock_url(self, service_type='identity', interface='public', def test_grant_role_user_v2(self): self.use_keystone_v2() self.register_uris([ + # user name dict(method='GET', uri=self.get_mock_url(base_url_append='OS-KSADM', resource='roles'), status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(base_url_append=None, resource='users'), + uri=self.get_mock_url(base_url_append=None, + resource='users', + qs_elements=['name=%s' % + self.user_data.name]), status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -126,13 +130,15 @@ def test_grant_role_user_v2(self): append=[self.project_data_v2.project_id, 'users', self.user_data.user_id, 'roles', 'OS-KSADM', self.role_data.role_id])), + # user id dict(method='GET', uri=self.get_mock_url(base_url_append='OS-KSADM', resource='roles'), status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(base_url_append=None, resource='users'), + uri=self.get_mock_url(base_url_append=None, + resource='users'), status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -159,11 +165,13 @@ def test_grant_role_user_v2(self): 'OS-KSADM', self.role_data.role_id]), status_code=201) ]) + # user name self.assertTrue( self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, project=self.project_data.project_id)) + # user id self.assertTrue( self.cloud.grant_role( self.role_data.role_name, @@ -174,13 +182,17 @@ def test_grant_role_user_v2(self): def test_grant_role_user_project_v2(self): self.use_keystone_v2() self.register_uris([ + # role name and user name dict(method='GET', uri=self.get_mock_url(base_url_append='OS-KSADM', resource='roles'), status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(base_url_append=None, resource='users'), + uri=self.get_mock_url(base_url_append=None, + resource='users', + qs_elements=['name=%s' % + self.user_data.name]), status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -206,13 +218,15 @@ def test_grant_role_user_project_v2(self): 'users', self.user_data.user_id, 'roles', 'OS-KSADM', self.role_data.role_id]), status_code=201), + # role name and user id dict(method='GET', uri=self.get_mock_url(base_url_append='OS-KSADM', resource='roles'), status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(base_url_append=None, resource='users'), + uri=self.get_mock_url(base_url_append=None, + resource='users'), status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -238,13 +252,17 @@ def test_grant_role_user_project_v2(self): 'users', self.user_data.user_id, 'roles', 'OS-KSADM', self.role_data.role_id]), status_code=201), + # role id and user name dict(method='GET', uri=self.get_mock_url(base_url_append='OS-KSADM', resource='roles'), status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(base_url_append=None, resource='users'), + uri=self.get_mock_url(base_url_append=None, + resource='users', + qs_elements=['name=%s' % + self.user_data.name]), status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -271,13 +289,15 @@ def test_grant_role_user_project_v2(self): 'OS-KSADM', self.role_data.role_id]), status_code=201, ), + # role id and user id dict(method='GET', uri=self.get_mock_url(base_url_append='OS-KSADM', resource='roles'), status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(base_url_append=None, resource='users'), + uri=self.get_mock_url(base_url_append=None, + resource='users'), status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -304,21 +324,25 @@ def test_grant_role_user_project_v2(self): 'OS-KSADM', self.role_data.role_id]), status_code=201) ]) + # role name and user name self.assertTrue( self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, project=self.project_data_v2.project_id)) + # role name and user id self.assertTrue( self.cloud.grant_role( self.role_data.role_name, user=self.user_data.user_id, project=self.project_data_v2.project_id)) + # role id and user name self.assertTrue( self.cloud.grant_role( self.role_data.role_id, user=self.user_data.name, project=self.project_data_v2.project_id)) + # role id and user id self.assertTrue( self.cloud.grant_role( self.role_data.role_id, @@ -335,7 +359,10 @@ def test_grant_role_user_project_v2_exists(self): status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(base_url_append=None, resource='users'), + uri=self.get_mock_url(base_url_append=None, + resource='users', + qs_elements=['name=%s' % + self.user_data.name]), status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -362,12 +389,15 @@ def test_grant_role_user_project_v2_exists(self): def test_grant_role_user_project(self): self.register_uris([ + # user name dict(method='GET', uri=self.get_mock_url(resource='roles'), status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(resource='users'), + uri=self.get_mock_url(resource='users', + qs_elements=['name=%s' % + self.user_data.name]), status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -392,6 +422,7 @@ def test_grant_role_user_project(self): self.user_data.user_id, 'roles', self.role_data.role_id]), status_code=204), + # user id dict(method='GET', uri=self.get_mock_url(resource='roles'), status_code=200, @@ -423,11 +454,13 @@ def test_grant_role_user_project(self): self.role_data.role_id]), status_code=204), ]) + # user name self.assertTrue( self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, project=self.project_data.project_id)) + # user id self.assertTrue( self.cloud.grant_role( self.role_data.role_name, @@ -437,12 +470,15 @@ def test_grant_role_user_project(self): def test_grant_role_user_project_exists(self): self.register_uris([ + # user name dict(method='GET', uri=self.get_mock_url(resource='roles'), status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(resource='users'), + uri=self.get_mock_url(resource='users', + qs_elements=['name=%s' % + self.user_data.name]), status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -466,6 +502,7 @@ def test_grant_role_user_project_exists(self): scope_id=self.project_data.project_id, entity_type='user', entity_id=self.user_data.user_id)}), + # user id dict(method='GET', uri=self.get_mock_url(resource='roles'), status_code=200, @@ -496,10 +533,12 @@ def test_grant_role_user_project_exists(self): entity_type='user', entity_id=self.user_data.user_id)}), ]) + # user name self.assertFalse(self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, project=self.project_data.project_id)) + # user id self.assertFalse(self.cloud.grant_role( self.role_data.role_id, user=self.user_data.user_id, @@ -652,6 +691,7 @@ def test_grant_role_group_project_exists(self): def test_grant_role_user_domain(self): self.register_uris([ + # user name and domain id dict(method='GET', uri=self.get_mock_url(resource='roles'), status_code=200, @@ -665,7 +705,9 @@ def test_grant_role_user_domain(self): uri=self.get_mock_url(resource='users', qs_elements=['domain_id=%s' % self.domain_data. - domain_id]), + domain_id, + 'name=%s' % + self.user_data.name]), complete_qs=True, status_code=200, json={'users': [self.user_data.json_response['user']]}), @@ -687,6 +729,7 @@ def test_grant_role_user_domain(self): 'roles', self.role_data.role_id]), status_code=204), + # user id and domain id dict(method='GET', uri=self.get_mock_url(resource='roles'), status_code=200, @@ -722,6 +765,7 @@ def test_grant_role_user_domain(self): 'roles', self.role_data.role_id]), status_code=204), + # user name and domain name dict(method='GET', uri=self.get_mock_url(resource='roles'), status_code=200, @@ -735,7 +779,9 @@ def test_grant_role_user_domain(self): uri=self.get_mock_url(resource='users', qs_elements=['domain_id=%s' % self.domain_data. - domain_id]), + domain_id, + 'name=%s' % + self.user_data.name]), complete_qs=True, status_code=200, json={'users': [self.user_data.json_response['user']]}), @@ -757,6 +803,7 @@ def test_grant_role_user_domain(self): 'roles', self.role_data.role_id]), status_code=204), + # user id and domain name dict(method='GET', uri=self.get_mock_url(resource='roles'), status_code=200, @@ -793,18 +840,22 @@ def test_grant_role_user_domain(self): self.role_data.role_id]), status_code=204), ]) + # user name and domain id self.assertTrue(self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, domain=self.domain_data.domain_id)) + # user id and domain id self.assertTrue(self.cloud.grant_role( self.role_data.role_name, user=self.user_data.user_id, domain=self.domain_data.domain_id)) + # user name and domain name self.assertTrue(self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, domain=self.domain_data.domain_name)) + # user id and domain name self.assertTrue(self.cloud.grant_role( self.role_data.role_name, user=self.user_data.user_id, @@ -813,6 +864,7 @@ def test_grant_role_user_domain(self): def test_grant_role_user_domain_exists(self): self.register_uris([ + # user name and domain id dict(method='GET', uri=self.get_mock_url(resource='roles'), status_code=200, @@ -826,7 +878,9 @@ def test_grant_role_user_domain_exists(self): uri=self.get_mock_url(resource='users', qs_elements=['domain_id=%s' % self.domain_data. - domain_id]), + domain_id, + 'name=%s' % + self.user_data.name]), complete_qs=True, status_code=200, json={'users': [self.user_data.json_response['user']]}), @@ -846,6 +900,7 @@ def test_grant_role_user_domain_exists(self): scope_id=self.domain_data.domain_id, entity_type='user', entity_id=self.user_data.user_id)}), + # user id and domain id dict(method='GET', uri=self.get_mock_url(resource='roles'), status_code=200, @@ -879,6 +934,7 @@ def test_grant_role_user_domain_exists(self): scope_id=self.domain_data.domain_id, entity_type='user', entity_id=self.user_data.user_id)}), + # user name and domain name dict(method='GET', uri=self.get_mock_url(resource='roles'), status_code=200, @@ -892,7 +948,9 @@ def test_grant_role_user_domain_exists(self): uri=self.get_mock_url(resource='users', qs_elements=['domain_id=%s' % self.domain_data. - domain_id]), + domain_id, + 'name=%s' % + self.user_data.name]), complete_qs=True, status_code=200, json={'users': [self.user_data.json_response['user']]}), @@ -912,6 +970,7 @@ def test_grant_role_user_domain_exists(self): scope_id=self.domain_data.domain_id, entity_type='user', entity_id=self.user_data.user_id)}), + # user id and domain name dict(method='GET', uri=self.get_mock_url(resource='roles'), status_code=200, @@ -946,18 +1005,22 @@ def test_grant_role_user_domain_exists(self): entity_type='user', entity_id=self.user_data.user_id)}), ]) + # user name and domain id self.assertFalse(self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, domain=self.domain_data.domain_id)) + # user id and domain id self.assertFalse(self.cloud.grant_role( self.role_data.role_name, user=self.user_data.user_id, domain=self.domain_data.domain_id)) + # user name and domain name self.assertFalse(self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, domain=self.domain_data.domain_name)) + # user id and domain name self.assertFalse(self.cloud.grant_role( self.role_data.role_name, user=self.user_data.user_id, @@ -1249,6 +1312,7 @@ def test_grant_role_group_domain_exists(self): def test_revoke_role_user_v2(self): self.use_keystone_v2() self.register_uris([ + # user name dict(method='GET', uri=self.get_mock_url( base_url_append='OS-KSADM', @@ -1258,7 +1322,8 @@ def test_revoke_role_user_v2(self): dict(method='GET', uri=self.get_mock_url( base_url_append=None, - resource='users'), + resource='users', qs_elements=['name=%s' % + self.user_data.name]), status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -1286,6 +1351,7 @@ def test_revoke_role_user_v2(self): 'roles', 'OS-KSADM', self.role_data.role_id]), status_code=204), + # user id dict(method='GET', uri=self.get_mock_url( base_url_append='OS-KSADM', @@ -1324,10 +1390,12 @@ def test_revoke_role_user_v2(self): self.role_data.role_id]), status_code=204), ]) + # user name self.assertTrue(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name, project=self.project_data.project_id)) + # user id self.assertTrue(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.user_id, @@ -1337,13 +1405,17 @@ def test_revoke_role_user_v2(self): def test_revoke_role_user_project_v2(self): self.use_keystone_v2() self.register_uris([ + # role name and user name dict(method='GET', uri=self.get_mock_url(base_url_append='OS-KSADM', resource='roles'), status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(base_url_append=None, resource='users'), + uri=self.get_mock_url(base_url_append=None, + resource='users', + qs_elements=['name=%s' % + self.user_data.name]), status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -1362,13 +1434,15 @@ def test_revoke_role_user_project_v2(self): 'roles']), status_code=200, json={'roles': []}), + # role name and user id dict(method='GET', uri=self.get_mock_url(base_url_append='OS-KSADM', resource='roles'), status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(base_url_append=None, resource='users'), + uri=self.get_mock_url(base_url_append=None, + resource='users'), status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -1387,13 +1461,17 @@ def test_revoke_role_user_project_v2(self): 'roles']), status_code=200, json={'roles': []}), + # role id and user name dict(method='GET', uri=self.get_mock_url(base_url_append='OS-KSADM', resource='roles'), status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(base_url_append=None, resource='users'), + uri=self.get_mock_url(base_url_append=None, + resource='users', + qs_elements=['name=%s' % + self.user_data.name]), status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -1412,13 +1490,15 @@ def test_revoke_role_user_project_v2(self): 'roles']), status_code=200, json={'roles': []}), + # role id and user id dict(method='GET', uri=self.get_mock_url(base_url_append='OS-KSADM', resource='roles'), status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(base_url_append=None, resource='users'), + uri=self.get_mock_url(base_url_append=None, + resource='users'), status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -1438,18 +1518,22 @@ def test_revoke_role_user_project_v2(self): status_code=200, json={'roles': []}) ]) + # role name and user name self.assertFalse(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name, project=self.project_data.project_id)) + # role name and user id self.assertFalse(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.user_id, project=self.project_data.project_id)) + # role id and user name self.assertFalse(self.cloud.revoke_role( self.role_data.role_id, user=self.user_data.name, project=self.project_data.project_id)) + # role id and user id self.assertFalse(self.cloud.revoke_role( self.role_data.role_id, user=self.user_data.user_id, @@ -1465,7 +1549,10 @@ def test_revoke_role_user_project_v2_exists(self): status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(base_url_append=None, resource='users'), + uri=self.get_mock_url(base_url_append=None, + resource='users', + qs_elements=['name=%s' % + self.user_data.name]), status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -1502,12 +1589,15 @@ def test_revoke_role_user_project_v2_exists(self): def test_revoke_role_user_project(self): self.register_uris([ + # user name dict(method='GET', uri=self.get_mock_url(resource='roles'), status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(resource='users'), + uri=self.get_mock_url(resource='users', + qs_elements=['name=%s' % + self.user_data.name]), status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -1525,6 +1615,7 @@ def test_revoke_role_user_project(self): status_code=200, complete_qs=True, json={'role_assignments': []}), + # user id dict(method='GET', uri=self.get_mock_url(resource='roles'), status_code=200, @@ -1549,10 +1640,12 @@ def test_revoke_role_user_project(self): complete_qs=True, json={'role_assignments': []}), ]) + # user name self.assertFalse(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name, project=self.project_data.project_id)) + # user id self.assertFalse(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.user_id, @@ -1561,12 +1654,15 @@ def test_revoke_role_user_project(self): def test_revoke_role_user_project_exists(self): self.register_uris([ + # role name and user name dict(method='GET', uri=self.get_mock_url(resource='roles'), status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(resource='users'), + uri=self.get_mock_url(resource='users', + qs_elements=['name=%s' % + self.user_data.name]), status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -1597,6 +1693,7 @@ def test_revoke_role_user_project_exists(self): self.user_data.user_id, 'roles', self.role_data.role_id])), + # role id and user id dict(method='GET', uri=self.get_mock_url(resource='roles'), status_code=200, @@ -1634,10 +1731,12 @@ def test_revoke_role_user_project_exists(self): 'roles', self.role_data.role_id])), ]) + # role name and user name self.assertTrue(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name, project=self.project_data.project_id)) + # role id and user id self.assertTrue(self.cloud.revoke_role( self.role_data.role_id, user=self.user_data.user_id, @@ -1790,6 +1889,7 @@ def test_revoke_role_group_project_exists(self): def test_revoke_role_user_domain(self): self.register_uris([ + # user name and domain id dict(method='GET', uri=self.get_mock_url(resource='roles'), status_code=200, @@ -1803,7 +1903,9 @@ def test_revoke_role_user_domain(self): uri=self.get_mock_url(resource='users', qs_elements=['domain_id=%s' % self.domain_data. - domain_id]), + domain_id, + 'name=%s' % + self.user_data.name]), complete_qs=True, status_code=200, json={'users': [self.user_data.json_response['user']]}), @@ -1817,6 +1919,7 @@ def test_revoke_role_user_domain(self): status_code=200, complete_qs=True, json={'role_assignments': []}), + # user id and domain id dict(method='GET', uri=self.get_mock_url(resource='roles'), status_code=200, @@ -1844,6 +1947,7 @@ def test_revoke_role_user_domain(self): status_code=200, complete_qs=True, json={'role_assignments': []}), + # user name and domain name dict(method='GET', uri=self.get_mock_url(resource='roles'), status_code=200, @@ -1857,7 +1961,9 @@ def test_revoke_role_user_domain(self): uri=self.get_mock_url(resource='users', qs_elements=['domain_id=%s' % self.domain_data. - domain_id]), + domain_id, + 'name=%s' % + self.user_data.name]), complete_qs=True, status_code=200, json={'users': [self.user_data.json_response['user']]}), @@ -1871,6 +1977,7 @@ def test_revoke_role_user_domain(self): status_code=200, complete_qs=True, json={'role_assignments': []}), + # user id and domain name dict(method='GET', uri=self.get_mock_url(resource='roles'), status_code=200, @@ -1899,18 +2006,22 @@ def test_revoke_role_user_domain(self): complete_qs=True, json={'role_assignments': []}), ]) + # user name and domain id self.assertFalse(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name, domain=self.domain_data.domain_id)) + # user id and domain id self.assertFalse(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.user_id, domain=self.domain_data.domain_id)) + # user name and domain name self.assertFalse(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name, domain=self.domain_data.domain_name)) + # user id and domain name self.assertFalse(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.user_id, @@ -1919,6 +2030,7 @@ def test_revoke_role_user_domain(self): def test_revoke_role_user_domain_exists(self): self.register_uris([ + # user name and domain name dict(method='GET', uri=self.get_mock_url(resource='roles'), status_code=200, @@ -1932,7 +2044,9 @@ def test_revoke_role_user_domain_exists(self): uri=self.get_mock_url(resource='users', qs_elements=['domain_id=%s' % self.domain_data. - domain_id]), + domain_id, + 'name=%s' % + self.user_data.name]), complete_qs=True, status_code=200, json={'users': [self.user_data.json_response['user']]}), @@ -1959,6 +2073,7 @@ def test_revoke_role_user_domain_exists(self): self.user_data.user_id, 'roles', self.role_data.role_id])), + # user id and domain name dict(method='GET', uri=self.get_mock_url(resource='roles'), status_code=200, @@ -1999,6 +2114,7 @@ def test_revoke_role_user_domain_exists(self): self.user_data.user_id, 'roles', self.role_data.role_id])), + # user name and domain id dict(method='GET', uri=self.get_mock_url(resource='roles'), status_code=200, @@ -2012,7 +2128,9 @@ def test_revoke_role_user_domain_exists(self): uri=self.get_mock_url(resource='users', qs_elements=['domain_id=%s' % self.domain_data. - domain_id]), + domain_id, + 'name=%s' % + self.user_data.name]), complete_qs=True, status_code=200, json={'users': [self.user_data.json_response['user']]}), @@ -2039,6 +2157,7 @@ def test_revoke_role_user_domain_exists(self): self.user_data.user_id, 'roles', self.role_data.role_id])), + # user id and domain id dict(method='GET', uri=self.get_mock_url(resource='roles'), status_code=200, @@ -2080,18 +2199,22 @@ def test_revoke_role_user_domain_exists(self): 'roles', self.role_data.role_id])), ]) + # user name and domain name self.assertTrue(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name, domain=self.domain_data.domain_name)) + # user id and domain name self.assertTrue(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.user_id, domain=self.domain_data.domain_name)) + # user name and domain id self.assertTrue(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name, domain=self.domain_data.domain_id)) + # user id and domain id self.assertTrue(self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.user_id, @@ -2447,7 +2570,9 @@ def test_grant_no_user_or_group(self): status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(resource='users'), + uri=self.get_mock_url(resource='users', + qs_elements=['name=%s' % + self.user_data.name]), status_code=200, json={'users': []}) ]) @@ -2467,7 +2592,9 @@ def test_revoke_no_user_or_group(self): status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(resource='users'), + uri=self.get_mock_url(resource='users', + qs_elements=['name=%s' % + self.user_data.name]), status_code=200, json={'users': []}) ]) @@ -2487,7 +2614,9 @@ def test_grant_both_user_and_group(self): status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(resource='users'), + uri=self.get_mock_url(resource='users', + qs_elements=['name=%s' % + self.user_data.name]), status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -2512,7 +2641,9 @@ def test_revoke_both_user_and_group(self): status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(resource='users'), + uri=self.get_mock_url(resource='users', + qs_elements=['name=%s' % + self.user_data.name]), status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -2545,7 +2676,9 @@ def test_grant_both_project_and_domain(self): uri=self.get_mock_url(resource='users', qs_elements=['domain_id=%s' % self.domain_data. - domain_id]), + domain_id, + 'name=%s' % + self.user_data.name]), complete_qs=True, status_code=200, json={'users': [self.user_data.json_response['user']]}), @@ -2598,7 +2731,9 @@ def test_revoke_both_project_and_domain(self): uri=self.get_mock_url(resource='users', qs_elements=['domain_id=%s' % self.domain_data. - domain_id]), + domain_id, + 'name=%s' % + self.user_data.name]), complete_qs=True, status_code=200, json={'users': [self.user_data.json_response['user']]}), @@ -2648,7 +2783,9 @@ def test_grant_no_project_or_domain(self): status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(resource='users'), + uri=self.get_mock_url(resource='users', + qs_elements=['name=%s' % + self.user_data.name]), status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -2676,7 +2813,9 @@ def test_revoke_no_project_or_domain(self): status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(resource='users'), + uri=self.get_mock_url(resource='users', + qs_elements=['name=%s' % + self.user_data.name]), status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -2758,7 +2897,10 @@ def test_grant_role_user_project_v2_wait(self): status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(base_url_append=None, resource='users'), + uri=self.get_mock_url(base_url_append=None, + resource='users', + qs_elements=['name=%s' % + self.user_data.name]), status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -2817,7 +2959,9 @@ def test_grant_role_user_project_v2_wait_exception(self): json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', uri=self.get_mock_url(base_url_append=None, - resource='users'), + resource='users', + qs_elements=['name=%s' % + self.user_data.name]), status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -2872,7 +3016,10 @@ def test_revoke_role_user_project_v2_wait(self): status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(base_url_append=None, resource='users'), + uri=self.get_mock_url(base_url_append=None, + resource='users', + qs_elements=['name=%s' % + self.user_data.name]), status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -2927,7 +3074,10 @@ def test_revoke_role_user_project_v2_wait_exception(self): status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(base_url_append=None, resource='users'), + uri=self.get_mock_url(base_url_append=None, + resource='users', + qs_elements=['name=%s' % + self.user_data.name]), status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', @@ -2970,6 +3120,8 @@ def test_revoke_role_user_project_v2_wait_exception(self): 'Timeout waiting for role to be revoked' ): self.assertTrue(self.cloud.revoke_role( - self.role_data.role_name, user=self.user_data.name, - project=self.project_data.project_id, wait=True, timeout=0.01)) + self.role_data.role_name, + user=self.user_data.name, + project=self.project_data.project_id, + wait=True, timeout=0.01)) self.assert_calls(do_count=False) diff --git a/openstack/tests/unit/cloud/test_users.py b/openstack/tests/unit/cloud/test_users.py index 44c28c9c3..3c054e213 100644 --- a/openstack/tests/unit/cloud/test_users.py +++ b/openstack/tests/unit/cloud/test_users.py @@ -20,13 +20,15 @@ class TestUsers(base.TestCase): - def _get_keystone_mock_url(self, resource, append=None, v3=True): + def _get_keystone_mock_url(self, resource, append=None, v3=True, + qs_elements=None): base_url_append = None if v3: base_url_append = 'v3' return self.get_mock_url( service_type='identity', resource=resource, - append=append, base_url_append=base_url_append) + append=append, base_url_append=base_url_append, + qs_elements=qs_elements) def _get_user_list(self, user_data): uri = self._get_keystone_mock_url(resource='users') @@ -139,7 +141,10 @@ def test_delete_user(self): self.register_uris([ dict(method='GET', - uri=self._get_keystone_mock_url(resource='users'), + uri=self._get_keystone_mock_url(resource='users', + qs_elements=['name=%s' % + user_data. + name]), status_code=200, json=self._get_user_list(user_data)), dict(method='GET', uri=user_resource_uri, status_code=200, From 2a40b4480103cd3f4395b40cf7814c1883d06504 Mon Sep 17 00:00:00 2001 From: Duc Truong Date: Wed, 18 Sep 2019 04:51:03 +0000 Subject: [PATCH 2558/3836] Add clustering update_action Add update_action to support clustering action update API [1]. [1] https://docs.openstack.org/api-ref/clustering/#update-action Change-Id: I860cf08712bd5869f448eaab06eb6a637e0708fd --- openstack/clustering/v1/_proxy.py | 13 +++++++++++++ openstack/clustering/v1/action.py | 3 +++ openstack/tests/unit/clustering/v1/test_action.py | 1 + openstack/tests/unit/clustering/v1/test_proxy.py | 3 +++ 4 files changed, 20 insertions(+) diff --git a/openstack/clustering/v1/_proxy.py b/openstack/clustering/v1/_proxy.py index 10e7b66f2..1ece997b4 100644 --- a/openstack/clustering/v1/_proxy.py +++ b/openstack/clustering/v1/_proxy.py @@ -913,6 +913,19 @@ def actions(self, **query): """ return self._list(_action.Action, **query) + def update_action(self, action, **attrs): + """Update a profile. + + :param action: Either the ID of the action, or an + instance of :class:`~openstack.clustering.v1.action.Action`. + :param attrs: The attributes to update on the action represented by + the ``value`` parameter. + + :returns: The updated action. + :rtype: :class:`~openstack.clustering.v1.action.Action` + """ + return self._update(_action.Action, action, **attrs) + def get_event(self, event): """Get a single event. diff --git a/openstack/clustering/v1/action.py b/openstack/clustering/v1/action.py index 2bfdf9de0..afccbc2b2 100644 --- a/openstack/clustering/v1/action.py +++ b/openstack/clustering/v1/action.py @@ -22,6 +22,9 @@ class Action(resource.Resource): # Capabilities allow_list = True allow_fetch = True + allow_commit = True + + commit_method = 'PATCH' _query_mapping = resource.QueryParameters( 'name', 'action', 'status', 'sort', 'global_project', diff --git a/openstack/tests/unit/clustering/v1/test_action.py b/openstack/tests/unit/clustering/v1/test_action.py index 268202d25..376e6d67b 100644 --- a/openstack/tests/unit/clustering/v1/test_action.py +++ b/openstack/tests/unit/clustering/v1/test_action.py @@ -55,6 +55,7 @@ def test_basic(self): self.assertEqual('/actions', sot.base_path) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_commit) def test_instantiate(self): sot = action.Action(**FAKE) diff --git a/openstack/tests/unit/clustering/v1/test_proxy.py b/openstack/tests/unit/clustering/v1/test_proxy.py index c42e02e02..6f2ff3043 100644 --- a/openstack/tests/unit/clustering/v1/test_proxy.py +++ b/openstack/tests/unit/clustering/v1/test_proxy.py @@ -328,6 +328,9 @@ def test_actions(self): method_kwargs={'limit': 2}, expected_kwargs={'limit': 2}) + def test_action_update(self): + self.verify_update(self.proxy.update_action, action.Action) + def test_event_get(self): self.verify_get(self.proxy.get_event, event.Event) From b9e06def393e66b4d84a90d859f4e7eaae2017a8 Mon Sep 17 00:00:00 2001 From: Duc Truong Date: Wed, 23 Oct 2019 22:20:09 +0000 Subject: [PATCH 2559/3836] Add support for Node tainted field Add tainted field for Senlin nodes Depends-On: https://review.opendev.org/#/c/689606/ Change-Id: Iac034b7d8afe22a57de9f1fa9b37f3b2bf5fa0da --- openstack/clustering/v1/node.py | 2 ++ openstack/tests/unit/clustering/v1/test_node.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/openstack/clustering/v1/node.py b/openstack/clustering/v1/node.py index 909ff6e95..7dae01522 100644 --- a/openstack/clustering/v1/node.py +++ b/openstack/clustering/v1/node.py @@ -80,6 +80,8 @@ class Node(_async_resource.AsyncResource): details = resource.Body('details', type=dict) #: A map containing the dependency of nodes dependents = resource.Body('dependents', type=dict) + #: Whether the node is tainted. *Type: bool* + tainted = resource.Body('tainted', type=bool) def _action(self, session, body): """Procedure the invoke an action API. diff --git a/openstack/tests/unit/clustering/v1/test_node.py b/openstack/tests/unit/clustering/v1/test_node.py index 328065788..43b0fda51 100644 --- a/openstack/tests/unit/clustering/v1/test_node.py +++ b/openstack/tests/unit/clustering/v1/test_node.py @@ -34,6 +34,7 @@ 'created_at': '2015-10-10T12:46:36.000000', 'updated_at': '2016-10-10T12:46:36.000000', 'init_at': '2015-10-10T12:46:36.000000', + 'tainted': True } @@ -66,6 +67,7 @@ def test_instantiate(self): self.assertEqual(FAKE['created_at'], sot.created_at) self.assertEqual(FAKE['updated_at'], sot.updated_at) self.assertEqual(FAKE['dependents'], sot.dependents) + self.assertEqual(FAKE['tainted'], sot.tainted) def test_check(self): sot = node.Node(**FAKE) From 25674ccd456d1249ca112495061002727d9baac4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 4 Oct 2019 18:18:15 +0200 Subject: [PATCH 2560/3836] Re-add functional tests on flavor content A new keystoneauth release is out that fixes authenticated discovery, meaning we'll pick up new server microversion, which means we can start testing for such in our server functional test. Depends-On: https://review.opendev.org/690876 Change-Id: I51b7fd28f3469a5718057ece2f29f3655ef717e4 --- lower-constraints.txt | 2 +- openstack/tests/functional/cloud/test_inventory.py | 12 ++++-------- requirements.txt | 2 +- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index b66d850be..ad6273e66 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -14,7 +14,7 @@ jmespath==0.9.0 jsonpatch==1.16 jsonpointer==1.13 jsonschema==2.6.0 -keystoneauth1==3.16.0 +keystoneauth1==3.18.0 linecache2==1.0.0 mock==2.0.0 mox3==0.20.0 diff --git a/openstack/tests/functional/cloud/test_inventory.py b/openstack/tests/functional/cloud/test_inventory.py index 59e12aa59..5049aa978 100644 --- a/openstack/tests/functional/cloud/test_inventory.py +++ b/openstack/tests/functional/cloud/test_inventory.py @@ -49,15 +49,13 @@ def _cleanup_server(self): def _test_host_content(self, host): self.assertEqual(host['image']['id'], self.image.id) self.assertNotIn('links', host['image']) - # TODO(mordred) Add this back wnen ksa releases - # self.assertNotIn('id', host['flavor']) + self.assertNotIn('id', host['flavor']) self.assertNotIn('links', host['flavor']) self.assertNotIn('links', host) self.assertIsInstance(host['volumes'], list) self.assertIsInstance(host['metadata'], dict) self.assertIn('interface_ip', host) - # TODO(mordred) Add this back wnen ksa releases - # self.assertIn('ram', host['flavor']) + self.assertIn('ram', host['flavor']) def _test_expanded_host_content(self, host): self.assertEqual(host['image']['name'], self.image.name) @@ -84,12 +82,10 @@ def test_get_host_no_detail(self): self.assertEqual(host['image']['id'], self.image.id) self.assertNotIn('links', host['image']) self.assertNotIn('name', host['name']) - # TODO(mordred) Add this back wnen ksa releases - # self.assertNotIn('id', host['flavor']) + self.assertNotIn('id', host['flavor']) self.assertNotIn('links', host['flavor']) self.assertNotIn('name', host['flavor']) - # TODO(mordred) Add this back wnen ksa releases - # self.assertIn('ram', host['flavor']) + self.assertIn('ram', host['flavor']) host_found = False for host in self.inventory.list_hosts(expand=False): diff --git a/requirements.txt b/requirements.txt index 961f57956..5cfdf4cd0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ requestsexceptions>=1.2.0 # Apache-2.0 jsonpatch!=1.20,>=1.16 # BSD six>=1.10.0 # MIT os-service-types>=1.7.0 # Apache-2.0 -keystoneauth1>=3.16.0 # Apache-2.0 +keystoneauth1>=3.18.0 # Apache-2.0 munch>=2.1.0 # MIT decorator>=3.4.0 # BSD From c195e22499de927c4aefff8413ae6b31f0e3b52e Mon Sep 17 00:00:00 2001 From: xuanyandong Date: Fri, 25 Oct 2019 10:48:42 +0800 Subject: [PATCH 2561/3836] Switch to Ussuri jobs Change-Id: I0d4f66a4d1dda4f97343655c51722fc833e00727 --- .zuul.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index a4a812701..61a42e596 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -402,7 +402,7 @@ - check-requirements - openstack-lower-constraints-jobs - openstack-python-jobs - - openstack-python3-train-jobs + - openstack-python3-ussuri-jobs - openstacksdk-functional-tips - openstacksdk-tox-tips - os-client-config-tox-tips From 85579c7fb9286eace54a68f6cbb96438a58b417e Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Mon, 28 Oct 2019 09:42:18 +0100 Subject: [PATCH 2562/3836] CI: add ironic-python-agent-builder to the ironic job It is now a requirement of the devstack plugin. Change-Id: I37e2091d1844347f737c496decf3288af595beb6 --- .zuul.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.zuul.yaml b/.zuul.yaml index a4a812701..ca801fe20 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -253,6 +253,7 @@ Run openstacksdk functional tests against a master devstack with ironic required-projects: - openstack/ironic + - openstack/ironic-python-agent-builder vars: devstack_localrc: OVERRIDE_PUBLIC_BRIDGE_MTU: 1400 From 835ab9d1336c7878de762ca812f0cadec47df9e0 Mon Sep 17 00:00:00 2001 From: Riccardo Pittau Date: Mon, 28 Oct 2019 15:16:46 +0100 Subject: [PATCH 2563/3836] Increase dogpile version for Py3.7 compatibility The library dogpile.cache is not compatible with Python 3.7 until version 0.6.5 because of new reserved word 'async'. For more info please check: https://github.com/sqlalchemy/dogpile.cache/commit/3c4351c11f79e0c458dc281ee9745e29feee993b Change-Id: I851ed4ec14cb0a97c9acc38f77cdab6f269ed40c --- lower-constraints.txt | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index 7d59f8d38..7cdec6680 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -4,7 +4,7 @@ cryptography==2.1 ddt==1.0.1 decorator==3.4.0 doc8==0.8.0 -dogpile.cache==0.6.2 +dogpile.cache==0.6.5 extras==1.0.0 fixtures==3.0.0 future==0.16.0 diff --git a/requirements.txt b/requirements.txt index 5cfdf4cd0..90f88ae87 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,5 +18,5 @@ futures>=3.0.0;python_version=='2.7' or python_version=='2.6' # BSD iso8601>=0.1.11 # MIT netifaces>=0.10.4 # MIT -dogpile.cache>=0.6.2 # BSD +dogpile.cache>=0.6.5 # BSD cryptography>=2.1 # BSD/Apache-2.0 From 561c3a6831af2428a61dc21221322f68f053a1fb Mon Sep 17 00:00:00 2001 From: Thomas Bechtold Date: Thu, 31 Oct 2019 11:17:52 +0100 Subject: [PATCH 2564/3836] Increase test timeout for 2 tests in TestImageProxy class The tests TestImageProxy.test_wait_for_task_error_396 and TestImageProxy.test_wait_for_task_error_396 from openstack.tests.unit.image.v2.test_proxy are failing in some build environments with: openstack.exceptions.ResourceTimeout: Timeout waiting for Task:id to \ transition to success Increasing the timeout hopefully helps. Change-Id: I74714bfae1d488049d25701efcaad823b315631b --- openstack/tests/unit/image/v2/test_proxy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 4bde92101..76038ad15 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -313,7 +313,7 @@ def test_wait_for_task_error_396(self): 'fetch', mock_fetch): result = self.proxy.wait_for_task( - res, interval=0.01, wait=0.1) + res, interval=0.01, wait=0.5) self.assertEqual('success', result.status) @@ -336,7 +336,7 @@ def test_wait_for_task_wait(self): 'fetch', mock_fetch): result = self.proxy.wait_for_task( - res, interval=0.01, wait=0.1) + res, interval=0.01, wait=0.5) self.assertEqual('success', result.status) From 84137a1af2234dc9063c893ed7ec95f48c5e2119 Mon Sep 17 00:00:00 2001 From: gujin Date: Fri, 1 Nov 2019 15:06:50 +0800 Subject: [PATCH 2565/3836] tox: Keeping going with docs Sphinx 1.8 introduced [1] the '--keep-going' argument which, as its name suggests, keeps the build running when it encounters non-fatal errors. This is exceptionally useful in avoiding a continuous edit-build loop when undertaking large doc reworks where multiple errors may be introduced. [1] https://github.com/sphinx-doc/sphinx/commit/e3483e9b045 Change-Id: I1ed5fd8b965f4466a20d61d9acd84e7aa2bbcf57 --- doc/requirements.txt | 4 ++-- tox.ini | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index c62786bc8..efcc706d8 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,8 +1,8 @@ # 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. -sphinx!=1.6.6,!=1.6.7,>=1.6.2,<2.0.0;python_version=='2.7' # BSD -sphinx!=1.6.6,!=1.6.7,>=1.6.2;python_version>='3.4' # BSD +sphinx>=1.8.0,<2.0.0;python_version=='2.7' # BSD +sphinx>=1.8.0,!=2.1.0;python_version>='3.4' # BSD docutils>=0.11 # OSI-Approved Open Source, Public Domain openstackdocstheme>=1.20.0 # Apache-2.0 beautifulsoup4>=4.6.0 # MIT diff --git a/tox.ini b/tox.ini index eccdc470a..bee99b0ea 100644 --- a/tox.ini +++ b/tox.ini @@ -82,14 +82,14 @@ deps = -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = - sphinx-build -W -d doc/build/doctrees -b html doc/source/ doc/build/html + sphinx-build -W -d doc/build/doctrees --keep-going -b html doc/source/ doc/build/html [testenv:pdf-docs] deps = {[testenv:docs]deps} whitelist_externals = make commands = - sphinx-build -W -d doc/build/doctrees -b latex doc/source/ doc/build/pdf + sphinx-build -W -d doc/build/doctrees --keep-going -b latex doc/source/ doc/build/pdf make -C doc/build/pdf [testenv:releasenotes] @@ -97,7 +97,8 @@ deps = -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt -commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html +commands = + sphinx-build -a -E -W -d releasenotes/build/doctrees --keep-going -b html releasenotes/source releasenotes/build/html [flake8] # The following are ignored on purpose. It's not super worth it to fix them. From 28fcf6e313f431ffd411f7ef4cb73bdcb3e70484 Mon Sep 17 00:00:00 2001 From: Bence Romsics Date: Wed, 31 Jul 2019 16:15:49 +0200 Subject: [PATCH 2566/3836] Add router add/remove route operations Add methods to the SDK to call the two new member actions introduced by new Neutron extension: extraroute-atomic. Change-Id: I6417735b0fb784d500f9d33adca4fcd92cdf7966 Depends-On: https://review.opendev.org/670851 Partial-Bug: #1826396 (rfe) Related-Change: https://review.opendev.org/655680 (spec) --- doc/source/user/proxies/network.rst | 2 + openstack/network/v2/_proxy.py | 24 +++++++++++ openstack/network/v2/router.py | 43 +++++++++++++++++++ openstack/tests/unit/network/v2/test_proxy.py | 28 ++++++++++++ .../tests/unit/network/v2/test_router.py | 32 ++++++++++++++ ...er-extraroute-atomic-1a0c84c3fd90ceb1.yaml | 4 ++ 6 files changed, 133 insertions(+) create mode 100644 releasenotes/notes/router-extraroute-atomic-1a0c84c3fd90ceb1.yaml diff --git a/doc/source/user/proxies/network.rst b/doc/source/user/proxies/network.rst index e69f6cda1..5a2ea9a95 100644 --- a/doc/source/user/proxies/network.rst +++ b/doc/source/user/proxies/network.rst @@ -63,6 +63,8 @@ Router Operations .. automethod:: openstack.network.v2._proxy.Proxy.remove_gateway_from_router .. automethod:: openstack.network.v2._proxy.Proxy.add_interface_to_router .. automethod:: openstack.network.v2._proxy.Proxy.remove_interface_from_router + .. automethod:: openstack.network.v2._proxy.Proxy.add_extra_routes_to_router + .. automethod:: openstack.network.v2._proxy.Proxy.remove_extra_routes_from_router Floating IP Operations ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 36ad7755b..7b3229b89 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -2585,6 +2585,30 @@ def remove_interface_from_router(self, router, subnet_id=None, router = self._get_resource(_router.Router, router) return router.remove_interface(self, **body) + def add_extra_routes_to_router(self, router, body): + """Add extra routes to a router + + :param router: Either the router ID or an instance of + :class:`~openstack.network.v2.router.Router` + :param body: The request body as documented in the api-ref. + :returns: Router with updated extra routes + :rtype: :class: `~openstack.network.v2.router.Router` + """ + router = self._get_resource(_router.Router, router) + return router.add_extra_routes(self, body=body) + + def remove_extra_routes_from_router(self, router, body): + """Remove extra routes from a router + + :param router: Either the router ID or an instance of + :class:`~openstack.network.v2.router.Router` + :param body: The request body as documented in the api-ref. + :returns: Router with updated extra routes + :rtype: :class: `~openstack.network.v2.router.Router` + """ + router = self._get_resource(_router.Router, router) + return router.remove_extra_routes(self, body=body) + def add_gateway_to_router(self, router, **body): """Add Gateway to a router diff --git a/openstack/network/v2/router.py b/openstack/network/v2/router.py index afe0f104b..6abcac108 100644 --- a/openstack/network/v2/router.py +++ b/openstack/network/v2/router.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.exceptions import SDKException from openstack import resource from openstack import utils @@ -100,6 +101,48 @@ def remove_interface(self, session, **body): resp = session.put(url, json=body) return resp.json() + def _put(self, session, url, body): + resp = session.put(url, json=body) + if not resp.ok: + resp_body = resp.json() + message = None + if 'NeutronError' in resp_body: + message = resp_body['NeutronError']['message'] + raise SDKException(message=message) + return resp + + def add_extra_routes(self, session, body): + """Add extra routes to a logical router. + + :param session: The session to communicate through. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param dict body: The request body as documented in the api-ref. + + :returns: The response as a Router object with the added extra routes. + + :raises: :class:`~openstack.exceptions.SDKException` on error. + """ + url = utils.urljoin(self.base_path, self.id, 'add_extraroutes') + resp = self._put(session, url, body) + self._translate_response(resp) + return self + + def remove_extra_routes(self, session, body): + """Remove extra routes from a logical router. + + :param session: The session to communicate through. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param dict body: The request body as documented in the api-ref. + + :returns: The response as a Router object with the extra routes left. + + :raises: :class:`~openstack.exceptions.SDKException` on error. + """ + url = utils.urljoin(self.base_path, self.id, 'remove_extraroutes') + resp = self._put(session, url, body) + self._translate_response(resp) + return self + def add_gateway(self, session, **body): """Add an external gateway to a logical router. diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index b93deedf0..24961042b 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -814,6 +814,34 @@ def test_remove_interface_from_router_with_subnet(self, mock_remove, expected_kwargs={"subnet_id": "SUBNET"}) mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") + @mock.patch.object(proxy_base.Proxy, '_get_resource') + @mock.patch.object(router.Router, 'add_extra_routes') + def test_add_extra_routes_to_router( + self, mock_add_extra_routes, mock_get): + x_router = router.Router.new(id="ROUTER_ID") + mock_get.return_value = x_router + + self._verify("openstack.network.v2.router.Router.add_extra_routes", + self.proxy.add_extra_routes_to_router, + method_args=["FAKE_ROUTER"], + method_kwargs={"body": {"router": {"routes": []}}}, + expected_kwargs={"body": {"router": {"routes": []}}}) + mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") + + @mock.patch.object(proxy_base.Proxy, '_get_resource') + @mock.patch.object(router.Router, 'remove_extra_routes') + def test_remove_extra_routes_from_router( + self, mock_remove_extra_routes, mock_get): + x_router = router.Router.new(id="ROUTER_ID") + mock_get.return_value = x_router + + self._verify("openstack.network.v2.router.Router.remove_extra_routes", + self.proxy.remove_extra_routes_from_router, + method_args=["FAKE_ROUTER"], + method_kwargs={"body": {"router": {"routes": []}}}, + expected_kwargs={"body": {"router": {"routes": []}}}) + mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") + @mock.patch.object(proxy_base.Proxy, '_get_resource') @mock.patch.object(router.Router, 'add_gateway') def test_add_gateway_to_router(self, mock_add, mock_get): diff --git a/openstack/tests/unit/network/v2/test_router.py b/openstack/tests/unit/network/v2/test_router.py index aff651fc2..560d0ea42 100644 --- a/openstack/tests/unit/network/v2/test_router.py +++ b/openstack/tests/unit/network/v2/test_router.py @@ -173,6 +173,38 @@ def test_remove_interface_port(self): sess.put.assert_called_with(url, json=body) + def test_add_extra_routes(self): + r = router.Router(**EXAMPLE) + response = mock.Mock() + response.headers = {} + json_body = {'router': {}} + response.body = json_body + response.json = mock.Mock(return_value=response.body) + response.status_code = 200 + sess = mock.Mock() + sess.put = mock.Mock(return_value=response) + ret = r.add_extra_routes(sess, json_body) + self.assertIsInstance(ret, router.Router) + self.assertIsInstance(ret.routes, list) + url = 'routers/IDENTIFIER/add_extraroutes' + sess.put.assert_called_with(url, json=json_body) + + def test_remove_extra_routes(self): + r = router.Router(**EXAMPLE) + response = mock.Mock() + response.headers = {} + json_body = {'router': {}} + response.body = json_body + response.json = mock.Mock(return_value=response.body) + response.status_code = 200 + sess = mock.Mock() + sess.put = mock.Mock(return_value=response) + ret = r.remove_extra_routes(sess, json_body) + self.assertIsInstance(ret, router.Router) + self.assertIsInstance(ret.routes, list) + url = 'routers/IDENTIFIER/remove_extraroutes' + sess.put.assert_called_with(url, json=json_body) + def test_add_router_gateway(self): # Add gateway to a router sot = router.Router(**EXAMPLE_WITH_OPTIONAL) diff --git a/releasenotes/notes/router-extraroute-atomic-1a0c84c3fd90ceb1.yaml b/releasenotes/notes/router-extraroute-atomic-1a0c84c3fd90ceb1.yaml new file mode 100644 index 000000000..4edd5c9ff --- /dev/null +++ b/releasenotes/notes/router-extraroute-atomic-1a0c84c3fd90ceb1.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add support for methods of Neutron extension: ``extraroute-atomic``. From 3c6dae3a657f556adc7e34a22831261e66b1da39 Mon Sep 17 00:00:00 2001 From: Ruby Loo Date: Fri, 15 Nov 2019 22:00:26 +0000 Subject: [PATCH 2567/3836] baremetal node: 'error' is a failed state For baremetal.wait_for_provision_state(), if abort_on_failed_state is True, we need to abort if the node goes into the 'error' provision state (which happens if a failure occurs while trying to delete the BM node). Change-Id: I6f628787dcf458ff9149dbb3502e7beeded70d9e Story: #2006860 --- openstack/baremetal/v1/node.py | 3 ++- openstack/tests/unit/baremetal/v1/test_node.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 9b81937b6..df94a122e 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -487,7 +487,8 @@ def _check_state_reached(self, session, expected_state, elif not abort_on_failed_state: return False - if self.provision_state.endswith(' failed'): + if (self.provision_state.endswith(' failed') or + self.provision_state == 'error'): raise exceptions.ResourceFailure( "Node %(node)s reached failure state \"%(state)s\"; " "the last error is %(error)s" % diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 72335d670..54b857d10 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -183,6 +183,18 @@ def _get_side_effect(_self, session): self.node.wait_for_provision_state, self.session, 'manageable') + def test_failure_error(self, mock_fetch): + def _get_side_effect(_self, session): + self.node.provision_state = 'error' + self.assertIs(session, self.session) + + mock_fetch.side_effect = _get_side_effect + + self.assertRaisesRegex(exceptions.ResourceFailure, + 'failure state "error"', + self.node.wait_for_provision_state, + self.session, 'manageable') + def test_enroll_as_failure(self, mock_fetch): def _get_side_effect(_self, session): self.node.provision_state = 'enroll' From 0e8e36163716a53b4c1c2931408ced72951513be Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 23 Nov 2019 06:42:14 -0500 Subject: [PATCH 2568/3836] Keep connection backrefs with weakref.proxy We're storing references to the connection object on proxy objects and the senlin folks are seeing memory leaks. Storing them in weakrefs. Change-Id: Ie0cffc402747d070c1eae1d9b423a046936d917d --- openstack/connection.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openstack/connection.py b/openstack/connection.py index 2a14995b5..d77a81f9a 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -177,6 +177,7 @@ :ref:`service-proxies` documentation. """ import warnings +import weakref import keystoneauth1.exceptions import requestsexceptions @@ -432,7 +433,7 @@ def session(self): # Hide a reference to the connection on the session to help with # backwards compatibility for folks trying to just pass # conn.session to a Resource method's session argument. - self.session._sdk_connection = self + self.session._sdk_connection = weakref.proxy(self) return self._session def add_service(self, service): From 97375f21d049ec837792811e1befd57a26cba0da Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Tue, 3 Dec 2019 13:36:13 -0600 Subject: [PATCH 2569/3836] Fix .. note:: rendering in doc Missing space in a .. note:: made it not render properly. Fixed. Change-Id: I8ca5620be7ff5dd7b687fe0a2e3a3ce31ebd4cf2 --- doc/source/user/microversions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/user/microversions.rst b/doc/source/user/microversions.rst index 1b60f9df1..a4960876e 100644 --- a/doc/source/user/microversions.rst +++ b/doc/source/user/microversions.rst @@ -90,7 +90,7 @@ with the following rules in mind: model for those fields/features. It is ok for openstacksdk to not have something. - ..note:: + .. note:: openstacksdk does not currently have any sort of "experimental" opt-in API that would allow exposing things to a user that may not be supportable under the normal compatibility contract. If a conflict arises in the From 800cbf7cfd1d71ce9da742b3b88f362c0c75f1c0 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Tue, 3 Dec 2019 15:09:29 -0500 Subject: [PATCH 2570/3836] Update deps for tox venv target This makes the venv target use all of the requirements files including docs/requirements so you can create a release note using: $ tox -e venv -- reno new Change-Id: Ic6e53d3aa10d48d56af2886e9dee59c52a9a2994 --- tox.ini | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tox.ini b/tox.ini index eccdc470a..e19716434 100644 --- a/tox.ini +++ b/tox.ini @@ -47,6 +47,11 @@ commands = local-check-factory = openstack._hacking.factory [testenv:venv] +deps = + -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -r{toxinidir}/test-requirements.txt + -r{toxinidir}/requirements.txt + -r{toxinidir}/doc/requirements.txt commands = {posargs} [testenv:debug] From c585ed197fe2d07c5c0bdd399dee937486202a97 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Tue, 3 Dec 2019 15:10:27 -0500 Subject: [PATCH 2571/3836] Expose baremetal Node.owner The baremetal API node resource gained the 'owner' field in the 1.50 microversion. This adds it to the Node resource in the SDK. Change-Id: I129fa7f27ffdc82d591c4cf1f23eca46df50e1ba --- openstack/baremetal/v1/node.py | 3 +++ openstack/tests/unit/baremetal/v1/test_node.py | 2 ++ releasenotes/notes/node-owner-7f4b083ff9da8cce.yaml | 7 +++++++ 3 files changed, 12 insertions(+) create mode 100644 releasenotes/notes/node-owner-7f4b083ff9da8cce.yaml diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index df94a122e..2f4ff1fe1 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -60,6 +60,9 @@ class Node(_common.ListMixin, resource.Resource): #: The UUID of the allocation associated with this node. Added in API #: microversion 1.52. allocation_id = resource.Body("allocation_uuid") + #: A string or UUID of the tenant who owns the baremetal node. Added in API + #: microversion 1.50. + owner = resource.Body("owner") #: The UUID of the chassis associated wit this node. Can be empty or None. chassis_id = resource.Body("chassis_uuid") #: The current clean step. diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 54b857d10..2669eaf30 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -50,6 +50,7 @@ "maintenance_reason": None, "name": "test_node", "network_interface": "flat", + "owner": "4b7ed919-e4a6-4017-a081-43205c5b0b73", "portgroups": [ { "href": "http://127.0.0.1:6385/v1/nodes//portgroups", @@ -132,6 +133,7 @@ def test_instantiate(self): self.assertEqual(FAKE['maintenance_reason'], sot.maintenance_reason) self.assertEqual(FAKE['name'], sot.name) self.assertEqual(FAKE['network_interface'], sot.network_interface) + self.assertEqual(FAKE['owner'], sot.owner) self.assertEqual(FAKE['ports'], sot.ports) self.assertEqual(FAKE['portgroups'], sot.port_groups) self.assertEqual(FAKE['power_state'], sot.power_state) diff --git a/releasenotes/notes/node-owner-7f4b083ff9da8cce.yaml b/releasenotes/notes/node-owner-7f4b083ff9da8cce.yaml new file mode 100644 index 000000000..88fb5f3d0 --- /dev/null +++ b/releasenotes/notes/node-owner-7f4b083ff9da8cce.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + The ``openstack.baremetal.v1.Node`` resource now has an ``owner`` property + which was added in the baremetal API `microversion 1.50`_. + + .. _microversion 1.50: https://docs.openstack.org/ironic/latest/contributor/webapi-version-history.html#id7 From eafa75bd74357670740dc07634c225be5d446580 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 4 Dec 2019 14:54:09 -0500 Subject: [PATCH 2572/3836] Fix reno index list indent The whitespace was throwing off the formatting for the list. Change-Id: I6c54413aaf9fa6c7bc18a5de47c96c4ec84386a9 --- releasenotes/source/index.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 67d709ac8..5dac65b62 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -2,13 +2,13 @@ openstacksdk Release Notes ============================ - .. toctree:: - :maxdepth: 1 +.. toctree:: + :maxdepth: 1 - unreleased - train - stein - rocky - queens - pike - ocata + unreleased + train + stein + rocky + queens + pike + ocata From 5cac3f7240637eea934a172aee2db1a7e25dd7aa Mon Sep 17 00:00:00 2001 From: Thomas Bechtold Date: Tue, 5 Nov 2019 09:52:42 +0100 Subject: [PATCH 2573/3836] update OVH vendor entry - there is identity v3 support - there are a lot more regions available Change-Id: I75c2cca1812cd07817a3e4c6007fa8c18ab737ec --- doc/source/user/config/vendor-support.rst | 2 +- openstack/config/vendors/ovh.json | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/doc/source/user/config/vendor-support.rst b/doc/source/user/config/vendor-support.rst index 4be56fd08..b7a7977ab 100644 --- a/doc/source/user/config/vendor-support.rst +++ b/doc/source/user/config/vendor-support.rst @@ -217,7 +217,7 @@ us-slc Salt Lake City, UT OVH --- -https://auth.cloud.ovh.net/v2.0 +https://auth.cloud.ovh.net/v3 ============== ================ Region Name Location diff --git a/openstack/config/vendors/ovh.json b/openstack/config/vendors/ovh.json index f17dc2b68..8dfca6791 100644 --- a/openstack/config/vendors/ovh.json +++ b/openstack/config/vendors/ovh.json @@ -5,9 +5,21 @@ "auth_url": "https://auth.cloud.ovh.net/" }, "regions": [ + "BHS", "BHS1", + "BHS3", + "DE", + "DE1", + "GRA", "GRA1", - "SBG1" + "GRA5", + "SBG", + "SBG1", + "SBG5", + "UK", + "UK1", + "WAW", + "WAW1" ], "identity_api_version": "3", "floating_ip_source": "None" From fc0465ce750fd0bbdf455862b897e94e9788af10 Mon Sep 17 00:00:00 2001 From: Sagi Shnaidman Date: Mon, 9 Dec 2019 20:22:29 +0200 Subject: [PATCH 2574/3836] Remove duplicate job definition Change-Id: I38f45664e9d9027c8a25f6a2a215169ef39606ff --- .zuul.yaml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index d9bee9a34..af8310f17 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -304,15 +304,6 @@ vars: tox_envlist: ansible -- job: - name: openstacksdk-ansible-functional-devstack - parent: openstacksdk-functional-devstack - description: | - Run openstacksdk ansible functional tests against a master devstack - using released version of ansible. - vars: - tox_envlist: ansible - - job: name: openstacksdk-ansible-devel-functional-devstack parent: openstacksdk-ansible-functional-devstack From fb8267a86a695c6f21bc761ad49ac8f1fffd2251 Mon Sep 17 00:00:00 2001 From: Jens Harbott Date: Tue, 3 Sep 2019 17:30:43 +0000 Subject: [PATCH 2575/3836] Add dns_publish_fixed_ip attribute to subnets With the subnet_dns_publish_fixed_ip extension Neutron has added a new attribute to subnets, allowing to select whether DNS records should be published for fixed IPs from that subnet. Add support for this to the subnet resource. [0] https://bugs.launchpad.net/neutron/+bug/1784879 [1] https://review.opendev.org/662405 [2] https://review.opendev.org/662409 Change-Id: I5dd063f33e1efb5a3be85d5451bf57c96a799efc --- openstack/network/v2/subnet.py | 4 +++- openstack/tests/unit/network/v2/test_subnet.py | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/openstack/network/v2/subnet.py b/openstack/network/v2/subnet.py index a3c2d8842..2b1d3ad18 100644 --- a/openstack/network/v2/subnet.py +++ b/openstack/network/v2/subnet.py @@ -29,7 +29,7 @@ class Subnet(resource.Resource, resource.TagMixin): _query_mapping = resource.QueryParameters( 'cidr', 'description', 'gateway_ip', 'ip_version', 'ipv6_address_mode', 'ipv6_ra_mode', 'name', 'network_id', - 'segment_id', + 'segment_id', 'dns_publish_fixed_ip', is_dhcp_enabled='enable_dhcp', project_id='tenant_id', subnet_pool_id='subnetpool_id', @@ -49,6 +49,8 @@ class Subnet(resource.Resource, resource.TagMixin): description = resource.Body('description') #: A list of DNS nameservers. dns_nameservers = resource.Body('dns_nameservers', type=list) + #: Whether to publish DNS records for fixed IPs + dns_publish_fixed_ip = resource.Body('dns_publish_fixed_ip', type=bool) #: The gateway IP address. gateway_ip = resource.Body('gateway_ip') #: A list of host routes. diff --git a/openstack/tests/unit/network/v2/test_subnet.py b/openstack/tests/unit/network/v2/test_subnet.py index 051b3fc71..5f1cb0419 100644 --- a/openstack/tests/unit/network/v2/test_subnet.py +++ b/openstack/tests/unit/network/v2/test_subnet.py @@ -21,6 +21,7 @@ 'created_at': '3', 'description': '4', 'dns_nameservers': ['5'], + 'dns_publish_fixed_ip': True, 'enable_dhcp': True, 'gateway_ip': '6', 'host_routes': ['7'], @@ -60,6 +61,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['created_at'], sot.created_at) self.assertEqual(EXAMPLE['description'], sot.description) self.assertEqual(EXAMPLE['dns_nameservers'], sot.dns_nameservers) + self.assertTrue(sot.dns_publish_fixed_ip) self.assertTrue(sot.is_dhcp_enabled) self.assertEqual(EXAMPLE['gateway_ip'], sot.gateway_ip) self.assertEqual(EXAMPLE['host_routes'], sot.host_routes) From 57308719f00231c8d270c08c0f286cfc05a165e6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 15 Dec 2019 10:18:21 -0500 Subject: [PATCH 2576/3836] Remove python2 from project-template We're unwinding python2 testing. As part of doing that, remove python2 from our zuul project-template which other people are using to add tips jobs to their project. Change-Id: I7d4b3f54cfd09d6a39b707f4a9a0bf4b2b9bca69 --- .zuul.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index af8310f17..bcbc4b9d9 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -383,11 +383,9 @@ check: jobs: - openstacksdk-functional-devstack-tips - - openstacksdk-functional-devstack-tips-python2 gate: jobs: - openstacksdk-functional-devstack-tips - - openstacksdk-functional-devstack-tips-python2 - project: templates: From e9107c190b31fddf546407cb601763971b5bc703 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 15 Dec 2019 13:19:28 -0500 Subject: [PATCH 2577/3836] Stop supporting python2 We're peeling python2 from libraries now, so Stop running python2 versions of test jobs. Leaving removing the test jobs themselves for the moment as they're still used in other repos. Change-Id: Ic6d56e3636b757201d83649f075005ed3ae1e1a6 --- .zuul.yaml | 3 --- releasenotes/notes/drop-python27-b824f9ce51cb1ab7.yaml | 3 +++ setup.cfg | 2 -- tox.ini | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/drop-python27-b824f9ce51cb1ab7.yaml diff --git a/.zuul.yaml b/.zuul.yaml index bcbc4b9d9..62da50b92 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -391,7 +391,6 @@ templates: - check-requirements - openstack-lower-constraints-jobs - - openstack-python-jobs - openstack-python3-ussuri-jobs - openstacksdk-functional-tips - openstacksdk-tox-tips @@ -414,7 +413,6 @@ voting: false - openstacksdk-functional-devstack-ironic: voting: false - - openstacksdk-functional-devstack-python2 - osc-functional-devstack-tips: voting: false - nodepool-functional-openstack-src @@ -428,7 +426,6 @@ gate: jobs: - openstacksdk-functional-devstack - - openstacksdk-functional-devstack-python2 - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-senlin - nodepool-functional-openstack-src diff --git a/releasenotes/notes/drop-python27-b824f9ce51cb1ab7.yaml b/releasenotes/notes/drop-python27-b824f9ce51cb1ab7.yaml new file mode 100644 index 000000000..80f1f86f2 --- /dev/null +++ b/releasenotes/notes/drop-python27-b824f9ce51cb1ab7.yaml @@ -0,0 +1,3 @@ +--- +prelude: > + As of this release, python v2 is neither tested nor supported. diff --git a/setup.cfg b/setup.cfg index 45d9a23a6..9267c6ec8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,8 +13,6 @@ classifier = License :: OSI Approved :: Apache Software License Operating System :: POSIX :: Linux Programming Language :: Python - Programming Language :: Python :: 2 - Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 diff --git a/tox.ini b/tox.ini index 1e7d634b4..ad333313a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 3.1 -envlist = pep8,py27,py37 +envlist = pep8,py37 skipsdist = True ignore_basepython_conflict = True From 3bebbd385bcd2bd47c4fc0e351f4af72b1a320a3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 15 Dec 2019 13:22:10 -0500 Subject: [PATCH 2578/3836] Remove extra python2 test jobs Once these are not used elsewhere we can remove them here. Change-Id: I1e3ec3a39a43f08bd932312a6dd4616f8a973b36 --- .zuul.yaml | 19 ------------------- tox.ini | 3 +-- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 62da50b92..ed11daaae 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -172,15 +172,6 @@ OPENSTACKSDK_HAS_SWIFT: 0 OPENSTACKSDK_HAS_HEAT: 0 -- job: - name: openstacksdk-functional-devstack-python2 - parent: openstacksdk-functional-devstack - description: | - Run openstacksdk functional tests using python2 against a master devstack - vars: - tox_environment: - OPENSTACKSDK_TOX_PYTHON: python2 - - job: name: openstacksdk-functional-devstack-tips parent: openstacksdk-functional-devstack @@ -194,16 +185,6 @@ vars: tox_install_siblings: true -- job: - name: openstacksdk-functional-devstack-tips-python2 - parent: openstacksdk-functional-devstack-tips - description: | - Run openstacksdk functional tests with tips of library dependencies using - python2 against a master devstack. - vars: - tox_environment: - OPENSTACKSDK_TOX_PYTHON: python2 - - job: name: openstacksdk-functional-devstack-magnum parent: openstacksdk-functional-devstack diff --git a/tox.ini b/tox.ini index ad333313a..db99c9573 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ ignore_basepython_conflict = True usedevelop = True install_command = pip install {opts} {packages} passenv = OS_* OPENSTACKSDK_* -basepython = {env:OPENSTACKSDK_TOX_PYTHON:python3} +basepython = python3 setenv = VIRTUAL_ENV={envdir} LANG=en_US.UTF-8 @@ -74,7 +74,6 @@ commands = [testenv:ansible] # Need to pass some env vars for the Ansible playbooks -basepython = {env:OPENSTACKSDK_TOX_PYTHON:python3} passenv = HOME USER ANSIBLE_VAR_* deps = {[testenv]deps} From 94585b8114aed2bb0210285fa6ad2dd452283ea0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 15 Dec 2019 13:24:01 -0500 Subject: [PATCH 2579/3836] Switch stable ansible job to 2.8 Ansible 2.6 is EOL. Run tests with 2.8. Change-Id: I9695af073a855dd0284fc5e8161df79db09cf313 --- .zuul.yaml | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index ed11daaae..5139c5a16 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -327,6 +327,27 @@ test_matrix_branch: master tox_install_siblings: true +- job: + name: openstacksdk-ansible-stable-2.8-functional-devstack + parent: openstacksdk-ansible-functional-devstack + description: | + Run openstacksdk ansible functional tests against a master devstack + using git stable-2.8 branch version of ansible. + branches: ^(stable-2.8|master)$ + required-projects: + - name: github.com/ansible/ansible + override-checkout: stable-2.8 + - name: openstack/openstacksdk + override-checkout: master + - name: openstack/devstack + override-checkout: master + vars: + # test-matrix grabs branch from the zuul branch setting. If the job + # is triggered by ansible, that branch will be devel which doesn't + # make sense to devstack. Override so that we run the right thing. + test_matrix_branch: master + tox_install_siblings: true + - job: name: openstacksdk-functional-devstack-masakari parent: openstacksdk-functional-devstack-minimum @@ -383,7 +404,7 @@ jobs: - openstacksdk-ansible-devel-functional-devstack: voting: false - - openstacksdk-ansible-stable-2.6-functional-devstack: + - openstacksdk-ansible-stable-2.8-functional-devstack: voting: false - openstacksdk-functional-devstack - openstacksdk-functional-devstack-networking From 63fe02bf770b47d6001ad802be851765f1d1b862 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 13 Dec 2019 18:24:18 +0100 Subject: [PATCH 2580/3836] Support uploading image from data and stdin It might be useful to upload image from stdin. Disadvantage is, that it is not possible to calculate checksums and this will prohibit threaded image upload when using swift and tasks. Additionally fix checksum validation during image creation - when create_image is not called through cloud layer - the result might be different, when checksums of existing image match (due to call and return cloud.get_image). Fixing this requires also completing image v1 (proper find). Yeah, lots of tests are affected by that change. Required-by: https://review.opendev.org/#/c/650374 Change-Id: I709d8b48cb7867fd806e2f19781bb84739363843 --- openstack/cloud/_object_store.py | 22 ++- openstack/image/_base_proxy.py | 43 ++++-- openstack/image/v1/_proxy.py | 7 +- openstack/image/v1/image.py | 55 ++++++++ openstack/image/v2/_proxy.py | 17 ++- openstack/tests/unit/cloud/test_image.py | 133 +++++++++++++++--- openstack/tests/unit/image/v2/test_proxy.py | 106 ++++++++++++++ ...t_stdin_image_upload-305c04fb2daeb32c.yaml | 4 + 8 files changed, 341 insertions(+), 46 deletions(-) create mode 100644 releasenotes/notes/support_stdin_image_upload-305c04fb2daeb32c.yaml diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index 5e85bbd68..6bb6fc68d 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -219,14 +219,11 @@ def _get_file_hashes(self, filename): if file_key not in self._file_hash_cache: self.log.debug( 'Calculating hashes for %(filename)s', {'filename': filename}) - md5 = hashlib.md5() - sha256 = hashlib.sha256() + (md5, sha256) = (None, None) with open(filename, 'rb') as file_obj: - for chunk in iter(lambda: file_obj.read(8192), b''): - md5.update(chunk) - sha256.update(chunk) + (md5, sha256) = self._calculate_data_hashes(file_obj) self._file_hash_cache[file_key] = dict( - md5=md5.hexdigest(), sha256=sha256.hexdigest()) + md5=md5, sha256=sha256) self.log.debug( "Image file %(filename)s md5:%(md5)s sha256:%(sha256)s", {'filename': filename, @@ -235,6 +232,19 @@ def _get_file_hashes(self, filename): return (self._file_hash_cache[file_key]['md5'], self._file_hash_cache[file_key]['sha256']) + def _calculate_data_hashes(self, data): + md5 = hashlib.md5() + sha256 = hashlib.sha256() + + if hasattr(data, 'read'): + for chunk in iter(lambda: data.read(8192), b''): + md5.update(chunk) + sha256.update(chunk) + else: + md5.update(data) + sha256.update(data) + return (md5.hexdigest(), sha256.hexdigest()) + @_utils.cache_on_arguments() def get_object_capabilities(self): """Get infomation about the object-storage service diff --git a/openstack/image/_base_proxy.py b/openstack/image/_base_proxy.py index 93ee70446..499a2e983 100644 --- a/openstack/image/_base_proxy.py +++ b/openstack/image/_base_proxy.py @@ -14,6 +14,7 @@ import six +from openstack import exceptions from openstack import proxy @@ -40,7 +41,7 @@ def create_image( disable_vendor_agent=True, allow_duplicates=False, meta=None, wait=False, timeout=3600, - validate_checksum=True, + data=None, validate_checksum=True, **kwargs): """Upload an image. @@ -49,6 +50,8 @@ def create_image( basename of the path. :param str filename: The path to the file to upload, if needed. (optional, defaults to None) + :param data: Image data (string or file-like object). It is mutually + exclusive with filename :param str container: Name of the container in swift where images should be uploaded for import if the cloud requires such a thing. (optional, defaults to 'images') @@ -103,23 +106,34 @@ def create_image( # https://docs.openstack.org/image-guide/image-formats.html container_format = 'bare' + if data and filename: + raise exceptions.SDKException( + 'Passing filename and data simultaneously is not supported') # If there is no filename, see if name is actually the filename - if not filename: + if not filename and not data: name, filename = self._get_name_and_filename( name, self._connection.config.config['image_format']) - if not (md5 or sha256): - (md5, sha256) = self._connection._get_file_hashes(filename) + if validate_checksum and data and not isinstance(data, bytes): + raise exceptions.SDKException( + 'Validating checksum is not possible when data is not a ' + 'direct binary object') + if not (md5 or sha256) and validate_checksum: + if filename: + (md5, sha256) = self._connection._get_file_hashes(filename) + elif data and isinstance(data, bytes): + (md5, sha256) = self._connection._calculate_data_hashes(data) if allow_duplicates: current_image = None else: - current_image = self._connection.get_image(name) + current_image = self.find_image(name) if current_image: - md5_key = current_image.get( + props = current_image.get('properties', {}) + md5_key = props.get( self._IMAGE_MD5_KEY, - current_image.get(self._SHADE_IMAGE_MD5_KEY, '')) - sha256_key = current_image.get( + props.get(self._SHADE_IMAGE_MD5_KEY, '')) + sha256_key = props.get( self._IMAGE_SHA256_KEY, - current_image.get(self._SHADE_IMAGE_SHA256_KEY, '')) + props.get(self._SHADE_IMAGE_SHA256_KEY, '')) up_to_date = self._connection._hashes_up_to_date( md5=md5, sha256=sha256, md5_key=md5_key, sha256_key=sha256_key) @@ -128,6 +142,11 @@ def create_image( "image %(name)s exists and is up to date", {'name': name}) return current_image + else: + self.log.debug( + "image %(name)s exists, but contains different " + "checksums. Updating.", + {'name': name}) if disable_vendor_agent: kwargs.update( @@ -147,9 +166,9 @@ def create_image( if container_format: image_kwargs['container_format'] = container_format - if filename: + if filename or data: image = self._upload_image( - name, filename=filename, meta=meta, + name, filename=filename, data=data, meta=meta, wait=wait, timeout=timeout, validate_checksum=validate_checksum, **image_kwargs) @@ -163,7 +182,7 @@ def _create_image(self, name, **image_kwargs): pass @abc.abstractmethod - def _upload_image(self, name, filename, meta, wait, timeout, + def _upload_image(self, name, filename, data, meta, wait, timeout, validate_checksum=True, **image_kwargs): pass diff --git a/openstack/image/v1/_proxy.py b/openstack/image/v1/_proxy.py index d7a0706a8..b808b10f5 100644 --- a/openstack/image/v1/_proxy.py +++ b/openstack/image/v1/_proxy.py @@ -42,10 +42,13 @@ def upload_image(self, **attrs): return self._create(_image.Image, **attrs) def _upload_image( - self, name, filename, meta, wait, timeout, **image_kwargs): + self, name, filename, data, meta, wait, timeout, **image_kwargs): # NOTE(mordred) wait and timeout parameters are unused, but # are present for ease at calling site. - image_data = open(filename, 'rb') + if filename and not data: + image_data = open(filename, 'rb') + else: + image_data = data image_kwargs['properties'].update(meta) image_kwargs['name'] = name diff --git a/openstack/image/v1/image.py b/openstack/image/v1/image.py index a92c4362d..9fc7e7c08 100644 --- a/openstack/image/v1/image.py +++ b/openstack/image/v1/image.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. from openstack.image import _download +from openstack import exceptions from openstack import resource @@ -29,6 +30,11 @@ class Image(resource.Resource, _download.DownloadMixin): # Remotely they would be still in the resource root _store_unknown_attrs_as_properties = True + _query_mapping = resource.QueryParameters( + 'name', 'container_format', 'disk_format', + 'status', 'size_min', 'size_max' + ) + #: Hash of the image data used. The Image service uses this value #: for verification. checksum = resource.Body('checksum') @@ -73,3 +79,52 @@ class Image(resource.Resource, _download.DownloadMixin): status = resource.Body('status') #: The timestamp when this image was last updated. updated_at = resource.Body('updated_at') + + @classmethod + def find(cls, session, name_or_id, ignore_missing=True, **params): + """Find a resource by its name or id. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param name_or_id: This resource's identifier, if needed by + the request. The default is ``None``. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict params: Any additional parameters to be passed into + underlying methods, such as to + :meth:`~openstack.resource.Resource.existing` + in order to pass on URI parameters. + + :return: The :class:`Resource` object matching the given name or id + or None if nothing matches. + :raises: :class:`openstack.exceptions.DuplicateResource` if more + than one resource is found for this request. + :raises: :class:`openstack.exceptions.ResourceNotFound` if nothing + is found and ignore_missing is ``False``. + """ + session = cls._get_session(session) + # Try to short-circuit by looking directly for a matching ID. + try: + match = cls.existing( + id=name_or_id, + connection=session._get_connection(), + **params) + return match.fetch(session, **params) + except exceptions.NotFoundException: + pass + + params['name'] = name_or_id + + data = cls.list(session, base_path='/images/detail', **params) + + result = cls._get_one_match(name_or_id, data) + if result is not None: + return result + + if ignore_missing: + return None + raise exceptions.ResourceNotFound( + "No %s found for %s" % (cls.__name__, name_or_id)) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 9f9d56226..7961ef31d 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -148,7 +148,7 @@ def upload_image(self, container_format=None, disk_format=None, return img - def _upload_image(self, name, filename=None, meta=None, + def _upload_image(self, name, filename=None, data=None, meta=None, wait=False, timeout=None, validate_checksum=True, **kwargs): # We can never have nice things. Glance v1 took "is_public" as a @@ -166,11 +166,11 @@ def _upload_image(self, name, filename=None, meta=None, # This makes me want to die inside if self._connection.image_api_use_tasks: return self._upload_image_task( - name, filename, meta=meta, + name, filename, data=data, meta=meta, wait=wait, timeout=timeout, **kwargs) else: return self._upload_image_put( - name, filename, meta=meta, + name, filename, data=data, meta=meta, validate_checksum=validate_checksum, **kwargs) except exceptions.SDKException: @@ -196,8 +196,12 @@ def _make_v2_image_params(self, meta, properties): return ret def _upload_image_put( - self, name, filename, meta, validate_checksum, **image_kwargs): - image_data = open(filename, 'rb') + self, name, filename, data, meta, + validate_checksum, **image_kwargs): + if filename and not data: + image_data = open(filename, 'rb') + else: + image_data = data properties = image_kwargs.pop('properties', {}) @@ -232,7 +236,7 @@ def _upload_image_put( return image def _upload_image_task( - self, name, filename, + self, name, filename, data, wait, timeout, meta, **image_kwargs): if not self._connection.has_service('object-store'): @@ -251,6 +255,7 @@ def _upload_image_task( self._connection.create_object( container, name, filename, md5=md5, sha256=sha256, + data=data, metadata={self._connection._OBJECT_AUTOCREATE_KEY: 'true'}, **{'content-type': 'application/octet-stream', 'x-delete-after': str(24 * 60 * 60)}) diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 12aec72fd..e2c280ac0 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -321,7 +321,21 @@ def test_create_image_put_v2(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), + 'image', append=['images', self.image_name], + base_url_append='v2'), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], + base_url_append='v2', + qs_elements=['name=' + self.image_name]), + validate=dict(), + json={'images': []}), + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], + base_url_append='v2', + qs_elements=['os_hidden=True']), json={'images': []}), dict(method='POST', uri=self.get_mock_url( @@ -356,6 +370,7 @@ def test_create_image_put_v2(self): dict(method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2'), + complete_qs=True, json=self.fake_search_return) ]) @@ -365,7 +380,7 @@ def test_create_image_put_v2(self): is_public=False) self.assert_calls() - self.assertEqual(self.adapter.request_history[5].text.read(), + self.assertEqual(self.adapter.request_history[7].text.read(), self.output) def test_create_image_task(self): @@ -390,7 +405,21 @@ def test_create_image_task(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), + 'image', append=['images', self.image_name], + base_url_append='v2'), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], + base_url_append='v2', + qs_elements=['name=' + self.image_name]), + validate=dict(), + json={'images': []}), + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], + base_url_append='v2', + qs_elements=['os_hidden=True']), json={'images': []}), dict(method='HEAD', uri='{endpoint}/{container}'.format( @@ -517,6 +546,7 @@ def test_create_image_task(self): dict(method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2'), + complete_qs=True, json=self.fake_search_return) ]) @@ -686,7 +716,11 @@ def test_create_image_put_v1(self): self.register_uris([ dict(method='GET', - uri='https://image.example.com/v1/images/detail', + uri='https://image.example.com/v1/images/' + self.image_name, + status_code=404), + dict(method='GET', + uri='https://image.example.com/v1/images/detail?name=' + + self.image_name, json={'images': []}), dict(method='POST', uri='https://image.example.com/v1/images', @@ -726,7 +760,11 @@ def test_create_image_put_v1_bad_delete(self): self.register_uris([ dict(method='GET', - uri='https://image.example.com/v1/images/detail', + uri='https://image.example.com/v1/images/' + self.image_name, + status_code=404), + dict(method='GET', + uri='https://image.example.com/v1/images/detail?name=' + + self.image_name, json={'images': []}), dict(method='POST', uri='https://image.example.com/v1/images', @@ -792,7 +830,22 @@ def test_create_image_put_v2_bad_delete(self): self.register_uris([ dict(method='GET', - uri='https://image.example.com/v2/images', + uri=self.get_mock_url( + 'image', append=['images', self.image_name], + base_url_append='v2'), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], + base_url_append='v2', + qs_elements=['name=' + self.image_name]), + validate=dict(), + json={'images': []}), + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], + base_url_append='v2', + qs_elements=['os_hidden=True']), json={'images': []}), dict(method='POST', uri='https://image.example.com/v2/images', @@ -828,10 +881,6 @@ def test_create_image_put_v2_wrong_checksum_delete(self): fake_image['owner_specified.openstack.sha256'] = 'b' self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - json={'images': []}), dict(method='POST', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2'), @@ -870,7 +919,8 @@ def test_create_image_put_v2_wrong_checksum_delete(self): exceptions.SDKException, self.cloud.create_image, self.image_name, self.imagefile.name, - is_public=False, md5='a', sha256='b' + is_public=False, md5='a', sha256='b', + allow_duplicates=True ) self.assert_calls() @@ -878,15 +928,10 @@ def test_create_image_put_v2_wrong_checksum_delete(self): def test_create_image_put_bad_int(self): self.cloud.image_api_use_tasks = False - self.register_uris([ - dict(method='GET', - uri='https://image.example.com/v2/images', - json={'images': []}), - ]) - self.assertRaises( exc.OpenStackCloudException, self._call_create_image, self.image_name, + allow_duplicates=True, min_disk='fish', min_ram=0) self.assert_calls() @@ -910,7 +955,22 @@ def test_create_image_put_user_int(self): self.register_uris([ dict(method='GET', - uri='https://image.example.com/v2/images', + uri=self.get_mock_url( + 'image', append=['images', self.image_name], + base_url_append='v2'), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], + base_url_append='v2', + qs_elements=['name=' + self.image_name]), + validate=dict(), + json={'images': []}), + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], + base_url_append='v2', + qs_elements=['os_hidden=True']), json={'images': []}), dict(method='POST', uri='https://image.example.com/v2/images', @@ -931,6 +991,7 @@ def test_create_image_put_user_int(self): json=ret), dict(method='GET', uri='https://image.example.com/v2/images', + complete_qs=True, json={'images': [ret]}), ]) @@ -959,7 +1020,22 @@ def test_create_image_put_meta_int(self): self.register_uris([ dict(method='GET', - uri='https://image.example.com/v2/images', + uri=self.get_mock_url( + 'image', append=['images', self.image_name], + base_url_append='v2'), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], + base_url_append='v2', + qs_elements=['name=' + self.image_name]), + validate=dict(), + json={'images': []}), + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], + base_url_append='v2', + qs_elements=['os_hidden=True']), json={'images': []}), dict(method='POST', uri='https://image.example.com/v2/images', @@ -980,6 +1056,7 @@ def test_create_image_put_meta_int(self): json=ret), dict(method='GET', uri='https://image.example.com/v2/images', + complete_qs=True, json={'images': [ret]}), ]) @@ -1009,7 +1086,22 @@ def test_create_image_put_protected(self): self.register_uris([ dict(method='GET', - uri='https://image.example.com/v2/images', + uri=self.get_mock_url( + 'image', append=['images', self.image_name], + base_url_append='v2'), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], + base_url_append='v2', + qs_elements=['name=' + self.image_name]), + validate=dict(), + json={'images': []}), + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], + base_url_append='v2', + qs_elements=['os_hidden=True']), json={'images': []}), dict(method='POST', uri='https://image.example.com/v2/images', @@ -1030,6 +1122,7 @@ def test_create_image_put_protected(self): json=ret), dict(method='GET', uri='https://image.example.com/v2/images', + complete_qs=True, json={'images': [ret]}), ]) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 76038ad15..be7693f69 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -11,6 +11,7 @@ # under the License. import mock +import io import requests from openstack import exceptions @@ -41,6 +42,7 @@ class TestImageProxy(test_proxy_base.TestProxyBase): def setUp(self): super(TestImageProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) + self.proxy._connection = self.cloud def test_image_import_no_required_attrs(self): # container_format and disk_format are required attrs of the image @@ -57,6 +59,110 @@ def test_image_import(self): expected_kwargs={"method": "method", "store": None, "uri": "uri"}) + def test_image_create_conflict(self): + self.assertRaises( + exceptions.SDKException, self.proxy.create_image, + name='fake', filename='fake', data='fake', + container='bare', disk_format='raw' + ) + + def test_image_create_checksum_match(self): + fake_image = image.Image( + id="fake", properties={ + self.proxy._IMAGE_MD5_KEY: 'fake_md5', + self.proxy._IMAGE_SHA256_KEY: 'fake_sha256' + }) + self.proxy.find_image = mock.Mock(return_value=fake_image) + + self.proxy._upload_image = mock.Mock() + + res = self.proxy.create_image( + name='fake', + md5='fake_md5', sha256='fake_sha256' + ) + self.assertEqual(fake_image, res) + self.proxy._upload_image.assert_not_called() + + def test_image_create_checksum_mismatch(self): + fake_image = image.Image( + id="fake", properties={ + self.proxy._IMAGE_MD5_KEY: 'fake_md5', + self.proxy._IMAGE_SHA256_KEY: 'fake_sha256' + }) + self.proxy.find_image = mock.Mock(return_value=fake_image) + + self.proxy._upload_image = mock.Mock() + + self.proxy.create_image( + name='fake', data=b'fake', + md5='fake2_md5', sha256='fake2_sha256' + ) + self.proxy._upload_image.assert_called() + + def test_image_create_allow_duplicates_find_not_called(self): + self.proxy.find_image = mock.Mock() + + self.proxy._upload_image = mock.Mock() + + self.proxy.create_image( + name='fake', data=b'fake', allow_duplicates=True, + ) + + self.proxy.find_image.assert_not_called() + + def test_image_create_validate_checksum_data_binary(self): + """ Pass real data as binary""" + self.proxy.find_image = mock.Mock() + + self.proxy._upload_image = mock.Mock() + + self.proxy.create_image( + name='fake', data=b'fake', validate_checksum=True, + container='bare', disk_format='raw' + ) + + self.proxy.find_image.assert_called_with('fake') + + self.proxy._upload_image.assert_called_with( + 'fake', container_format='bare', disk_format='raw', + filename=None, data=b'fake', meta={}, + properties={ + self.proxy._IMAGE_MD5_KEY: '144c9defac04969c7bfad8efaa8ea194', + self.proxy._IMAGE_SHA256_KEY: 'b5d54c39e66671c9731b9f471e585' + 'd8262cd4f54963f0c93082d8dcf33' + '4d4c78', + self.proxy._IMAGE_OBJECT_KEY: 'bare/fake'}, + timeout=3600, validate_checksum=True, wait=False) + + def test_image_create_validate_checksum_data_not_binary(self): + self.assertRaises( + exceptions.SDKException, self.proxy.create_image, + name='fake', data=io.StringIO(), validate_checksum=True, + container='bare', disk_format='raw' + ) + + def test_image_create_data_binary(self): + """Pass binary file-like object""" + self.proxy.find_image = mock.Mock() + + self.proxy._upload_image = mock.Mock() + + data = io.BytesIO(b'\0\0') + + self.proxy.create_image( + name='fake', data=data, validate_checksum=False, + container='bare', disk_format='raw' + ) + + self.proxy._upload_image.assert_called_with( + 'fake', container_format='bare', disk_format='raw', + filename=None, data=data, meta={}, + properties={ + self.proxy._IMAGE_MD5_KEY: '', + self.proxy._IMAGE_SHA256_KEY: '', + self.proxy._IMAGE_OBJECT_KEY: 'bare/fake'}, + timeout=3600, validate_checksum=False, wait=False) + def test_image_upload_no_args(self): # container_format and disk_format are required args self.assertRaises(exceptions.InvalidRequest, self.proxy.upload_image) diff --git a/releasenotes/notes/support_stdin_image_upload-305c04fb2daeb32c.yaml b/releasenotes/notes/support_stdin_image_upload-305c04fb2daeb32c.yaml new file mode 100644 index 000000000..0d315e9f8 --- /dev/null +++ b/releasenotes/notes/support_stdin_image_upload-305c04fb2daeb32c.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add support for creating image from STDIN (i.e. from OSC). When creating from STDIN however, no checksum verification is possible, and thus validate_checksum must be also set to False. From 29de7af09f6d48cb89e6a2200a5abfc5a1c05335 Mon Sep 17 00:00:00 2001 From: Bo Tran Date: Wed, 18 Dec 2019 17:18:57 +0700 Subject: [PATCH 2581/3836] Add return cluster_id when query actions list With Senlin API version greater than 1.14. It will return cluster_id in each action object Change-Id: Icb64406aac362fe54a82463a9afb499233c09fe7 --- openstack/clustering/v1/action.py | 2 ++ openstack/tests/unit/clustering/v1/test_action.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/openstack/clustering/v1/action.py b/openstack/clustering/v1/action.py index afccbc2b2..280501ffd 100644 --- a/openstack/clustering/v1/action.py +++ b/openstack/clustering/v1/action.py @@ -72,3 +72,5 @@ class Action(resource.Resource): created_at = resource.Body('created_at') #: Timestamp when the action was last updated. updated_at = resource.Body('updated_at') + #: The ID of cluster which this action runs on. + cluster_id = resource.Body('cluster_id') diff --git a/openstack/tests/unit/clustering/v1/test_action.py b/openstack/tests/unit/clustering/v1/test_action.py index 376e6d67b..62e13188d 100644 --- a/openstack/tests/unit/clustering/v1/test_action.py +++ b/openstack/tests/unit/clustering/v1/test_action.py @@ -15,6 +15,7 @@ from openstack.clustering.v1 import action +FAKE_CLUSTER_ID = 'ffaed25e-46f5-4089-8e20-b3b4722fd597' FAKE_ID = '633bd3c6-520b-420f-8e6a-dc2a47022b53' FAKE_NAME = 'node_create_c3783474' @@ -40,6 +41,7 @@ 'depended_by': [], 'created_at': '2015-10-10T12:46:36.000000', 'updated_at': '2016-10-10T12:46:36.000000', + 'cluster_id': FAKE_CLUSTER_ID, } @@ -80,3 +82,4 @@ def test_instantiate(self): self.assertEqual(FAKE['depended_by'], sot.depended_by) self.assertEqual(FAKE['created_at'], sot.created_at) self.assertEqual(FAKE['updated_at'], sot.updated_at) + self.assertEqual(FAKE['cluster_id'], sot.cluster_id) From c0745ea4d1ae98ddfb36535453c81c964ef4863d Mon Sep 17 00:00:00 2001 From: Bence Romsics Date: Tue, 8 Oct 2019 14:56:35 +0200 Subject: [PATCH 2582/3836] Handle HTTP errors in add/remove router interface calls Change-Id: Ib1f880f75e511faaf8c798da22b9ca54dd441166 Story: #2006679 Task: #36955 --- openstack/network/v2/router.py | 28 ++++++++------- .../tests/unit/network/v2/test_router.py | 34 +++++++++++++++++++ 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/openstack/network/v2/router.py b/openstack/network/v2/router.py index 6abcac108..87edb27fc 100644 --- a/openstack/network/v2/router.py +++ b/openstack/network/v2/router.py @@ -75,6 +75,16 @@ class Router(resource.Resource, resource.TagMixin): #: Timestamp when the router was created. updated_at = resource.Body('updated_at') + def _put(self, session, url, body): + resp = session.put(url, json=body) + if not resp.ok: + resp_body = resp.json() + message = None + if 'NeutronError' in resp_body: + message = resp_body['NeutronError']['message'] + raise SDKException(message=message) + return resp + def add_interface(self, session, **body): """Add an internal interface to a logical router. @@ -83,9 +93,11 @@ def add_interface(self, session, **body): :param dict body: The body requested to be updated on the router :returns: The body of the response as a dictionary. + + :raises: :class:`~openstack.exceptions.SDKException` on error. """ url = utils.urljoin(self.base_path, self.id, 'add_router_interface') - resp = session.put(url, json=body) + resp = self._put(session, url, body) return resp.json() def remove_interface(self, session, **body): @@ -96,21 +108,13 @@ def remove_interface(self, session, **body): :param dict body: The body requested to be updated on the router :returns: The body of the response as a dictionary. + + :raises: :class:`~openstack.exceptions.SDKException` on error. """ url = utils.urljoin(self.base_path, self.id, 'remove_router_interface') - resp = session.put(url, json=body) + resp = self._put(session, url, body) return resp.json() - def _put(self, session, url, body): - resp = session.put(url, json=body) - if not resp.ok: - resp_body = resp.json() - message = None - if 'NeutronError' in resp_body: - message = resp_body['NeutronError']['message'] - raise SDKException(message=message) - return resp - def add_extra_routes(self, session, body): """Add extra routes to a logical router. diff --git a/openstack/tests/unit/network/v2/test_router.py b/openstack/tests/unit/network/v2/test_router.py index 560d0ea42..a5cd05326 100644 --- a/openstack/tests/unit/network/v2/test_router.py +++ b/openstack/tests/unit/network/v2/test_router.py @@ -11,6 +11,9 @@ # under the License. import mock +import testtools + +from openstack.exceptions import SDKException from openstack.tests.unit import base from openstack.network.v2 import router @@ -173,6 +176,37 @@ def test_remove_interface_port(self): sess.put.assert_called_with(url, json=body) + def test_add_interface_4xx(self): + # Neutron may return 4xx, we have to raise if that happens + sot = router.Router(**EXAMPLE) + response = mock.Mock() + msg = 'borked' + response.body = {'NeutronError': {'message': msg}} + response.json = mock.Mock(return_value=response.body) + response.ok = False + response.status_code = 409 + sess = mock.Mock() + sess.put = mock.Mock(return_value=response) + body = {'subnet_id': '3'} + with testtools.ExpectedException(SDKException, msg): + sot.add_interface(sess, **body) + + def test_remove_interface_4xx(self): + # Neutron may return 4xx for example if a router interface has + # extra routes referring to it as a nexthop + sot = router.Router(**EXAMPLE) + response = mock.Mock() + msg = 'borked' + response.body = {'NeutronError': {'message': msg}} + response.json = mock.Mock(return_value=response.body) + response.ok = False + response.status_code = 409 + sess = mock.Mock() + sess.put = mock.Mock(return_value=response) + body = {'subnet_id': '3'} + with testtools.ExpectedException(SDKException, msg): + sot.remove_interface(sess, **body) + def test_add_extra_routes(self): r = router.Router(**EXAMPLE) response = mock.Mock() From 8caa5ea86635274a84483905a216157db5c9ea3b Mon Sep 17 00:00:00 2001 From: zhufl Date: Mon, 6 Jan 2020 14:25:17 +0800 Subject: [PATCH 2583/3836] Fix duplicated words issue like "was not not found" This is to fix the duplicated words issue like "if the field was not not found in any elements". Change-Id: I7504354ec8b258cce0e3788c7d306d4ea2864ef4 --- doc/source/contributor/create/resource.rst | 3 +-- openstack/cloud/_baremetal.py | 2 +- openstack/cloud/_compute.py | 2 +- openstack/cloud/_network.py | 2 +- openstack/cloud/_utils.py | 4 ++-- openstack/clustering/v1/_proxy.py | 2 +- 6 files changed, 7 insertions(+), 8 deletions(-) diff --git a/doc/source/contributor/create/resource.rst b/doc/source/contributor/create/resource.rst index ffe5ab3b5..76b8954ea 100644 --- a/doc/source/contributor/create/resource.rst +++ b/doc/source/contributor/create/resource.rst @@ -150,8 +150,7 @@ built-in property objects, but they share only the name - they're not the same. Properties are set based on the contents of a response body or headers. Based on what your resource returns, you should set ``prop``\s to map -those those values to ones on your :class:`~openstack.resource.Resource` -object. +those values to ones on your :class:`~openstack.resource.Resource` object. *Line 22* sets a prop for ``timestamp`` , which will cause the ``Fake.timestamp`` attribute to contain the value returned in an diff --git a/openstack/cloud/_baremetal.py b/openstack/cloud/_baremetal.py index 6c05b8d7d..970767604 100644 --- a/openstack/cloud/_baremetal.py +++ b/openstack/cloud/_baremetal.py @@ -126,7 +126,7 @@ def inspect_machine(self, name_or_id, wait=False, timeout=3600): node = self.baremetal.get_node(name_or_id) # NOTE(TheJulia): If in available state, we can do this. However, - # we need to to move the machine back to manageable first. + # we need to move the machine back to manageable first. if node.provision_state == 'available': if node.instance_id: raise exc.OpenStackCloudException( diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 397681837..958c07822 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -762,7 +762,7 @@ def create_server( needed (defaults to True) :param network: (optional) Network dict or name or ID to attach the server to. Mutually exclusive with the nics parameter. - Can also be be a list of network names or IDs or + Can also be a list of network names or IDs or network dicts. :param boot_from_volume: Whether to boot from volume. 'boot_volume' implies True, but boot_from_volume=True with diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index 9a58f34ad..389d3e4c0 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -949,7 +949,7 @@ def insert_rule_into_policy(self, name_or_id, rule_name_or_id, Short-circuits and returns the firewall policy early if the firewall rule id is already present in the firewall_rules list. This method doesn't do re-ordering. If you want to move a firewall rule - or or down the list, you have to remove and re-add it. + or down the list, you have to remove and re-add it. :param name_or_id: firewall policy name or id :param rule_name_or_id: firewall rule name or id diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index 45131c604..4979774b6 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -452,7 +452,7 @@ def safe_dict_min(key, data): :param string key: The dictionary key to search for the minimum value. :param list data: List of dicts to use for the data set. - :returns: None if the field was not not found in any elements, or + :returns: None if the field was not found in any elements, or the minimum value for the field otherwise. """ min_value = None @@ -484,7 +484,7 @@ def safe_dict_max(key, data): :param string key: The dictionary key to search for the maximum value. :param list data: List of dicts to use for the data set. - :returns: None if the field was not not found in any elements, or + :returns: None if the field was not found in any elements, or the maximum value for the field otherwise. """ max_value = None diff --git a/openstack/clustering/v1/_proxy.py b/openstack/clustering/v1/_proxy.py index 1ece997b4..89dc7507c 100644 --- a/openstack/clustering/v1/_proxy.py +++ b/openstack/clustering/v1/_proxy.py @@ -619,7 +619,7 @@ def adopt_node(self, preview=False, **attrs): ``os.nova.sever-1.0``. * identity: (Required) A string including the name or ID of an OpenStack resource to be adopted as a Senlin node. - * name: (Optional) The name of of node to be created. Omitting + * name: (Optional) The name of node to be created. Omitting this parameter will have the node named automatically. * snapshot: (Optional) A boolean indicating whether a snapshot of the target resource should be created if possible. Default From 13c6bc2bd913b4a3b8ea048b95158ed9560fbf6a Mon Sep 17 00:00:00 2001 From: Riccardo Pittau Date: Fri, 18 Oct 2019 12:24:33 +0200 Subject: [PATCH 2584/3836] Add reset_interfaces argument to patch_node Ironic uses reset_interfaces option when patching a node to reset all the hardware interfaces of a node. This patch adds that argument to the patch_node method in the baremetal module and introduces a patch method under the node module to be able to evaluate the reset_interfaces parameter. Also, it modifies the _prepare_request method in resource to accept query parameters in urls and build the request uri keeping those into account. Increasing minimum version of mock to 3.0.0 to be able to use testing features not supported before that version, e.g. assert_called_once Change-Id: I8bca403df7d38a7ac1d066c5f1d7e2bff1deb054 --- lower-constraints.txt | 2 +- openstack/baremetal/v1/_proxy.py | 9 ++++- openstack/baremetal/v1/node.py | 34 +++++++++++++++++ openstack/resource.py | 6 ++- .../tests/unit/baremetal/v1/test_node.py | 37 +++++++++++++++++++ openstack/tests/unit/test_resource.py | 21 +++++++++++ test-requirements.txt | 2 +- 7 files changed, 106 insertions(+), 5 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index 7cdec6680..c4e6ec4d7 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -17,7 +17,7 @@ jsonpointer==1.13 jsonschema==2.6.0 keystoneauth1==3.18.0 linecache2==1.0.0 -mock==2.0.0 +mock==3.0.0 mox3==0.20.0 munch==2.1.0 netifaces==0.10.4 diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index eea434227..0e00b3914 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -294,12 +294,16 @@ def update_node(self, node, retry_on_conflict=True, **attrs): res = self._get_resource(_node.Node, node, **attrs) return res.commit(self, retry_on_conflict=retry_on_conflict) - def patch_node(self, node, patch, retry_on_conflict=True): + def patch_node(self, node, patch, reset_interfaces=None, + retry_on_conflict=True): """Apply a JSON patch to the node. :param node: The value can be the name or ID of a node or a :class:`~openstack.baremetal.v1.node.Node` instance. :param patch: JSON patch to apply. + :param bool reset_interfaces: whether to reset the node hardware + interfaces to their defaults. This works only when changing + drivers. Added in API microversion 1.45. :param bool retry_on_conflict: Whether to retry HTTP CONFLICT error. Most of the time it can be retried, since it is caused by the node being locked. However, when setting ``instance_id``, this is @@ -313,7 +317,8 @@ def patch_node(self, node, patch, retry_on_conflict=True): :rtype: :class:`~openstack.baremetal.v1.node.Node` """ res = self._get_resource(_node.Node, node) - return res.patch(self, patch, retry_on_conflict=retry_on_conflict) + return res.patch(self, patch, retry_on_conflict=retry_on_conflict, + reset_interfaces=reset_interfaces) def set_node_provision_state(self, node, target, config_drive=None, clean_steps=None, rescue_password=None, diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 2f4ff1fe1..6bb099c3a 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -833,5 +833,39 @@ def set_traits(self, session, traits): self.traits = traits + def patch(self, session, patch=None, prepend_key=True, has_body=True, + retry_on_conflict=None, base_path=None, reset_interfaces=None): + + if reset_interfaces is not None: + # The id cannot be dirty for an commit + self._body._dirty.discard("id") + + # Only try to update if we actually have anything to commit. + if not patch and not self.requires_commit: + return self + + if not self.allow_patch: + raise exceptions.MethodNotSupported(self, "patch") + + session = self._get_session(session) + microversion = utils.pick_microversion(session, '1.45') + params = [('reset_interfaces', reset_interfaces)] + + request = self._prepare_request(requires_id=True, + prepend_key=prepend_key, + base_path=base_path, patch=True, + params=params) + + if patch: + request.body += self._convert_patch(patch) + + return self._commit(session, request, 'PATCH', microversion, + has_body=has_body, + retry_on_conflict=retry_on_conflict) + + else: + return super(Node, self).patch(session, patch=patch, + retry_on_conflict=retry_on_conflict) + NodeDetail = Node diff --git a/openstack/resource.py b/openstack/resource.py index 59f38a4d2..fe266c6f5 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1053,7 +1053,7 @@ def _prepare_request_body(self, patch, prepend_key): return body def _prepare_request(self, requires_id=None, prepend_key=False, - patch=False, base_path=None): + patch=False, base_path=None, params=None): """Prepare a request to be sent to the server Create operations don't require an ID, but all others do, @@ -1091,6 +1091,10 @@ def _prepare_request(self, requires_id=None, prepend_key=False, uri = utils.urljoin(uri, self.id) + if params: + query_params = six.moves.urllib.parse.urlencode(params) + uri += '?' + query_params + return _Request(uri, body, headers) def _translate_response(self, response, has_body=None, error_message=None): diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 2669eaf30..a1a4dc04f 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -16,6 +16,7 @@ from openstack.baremetal.v1 import _common from openstack.baremetal.v1 import node from openstack import exceptions +from openstack import resource from openstack.tests.unit import base # NOTE: Sample data from api-ref doc @@ -766,3 +767,39 @@ def test_set_traits(self): json={'traits': ['CUSTOM_FAKE', 'CUSTOM_REAL', 'CUSTOM_MISSING']}, headers=mock.ANY, microversion='1.37', retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + +@mock.patch.object(resource.Resource, 'patch', autospec=True) +class TestNodePatch(base.TestCase): + + def setUp(self): + super(TestNodePatch, self).setUp() + self.node = node.Node(**FAKE) + self.session = mock.Mock(spec=adapter.Adapter, + default_microversion=None) + self.session.log = mock.Mock() + + def test_node_patch(self, mock_patch): + patch = {'path': 'test'} + self.node.patch(self.session, patch=patch) + mock_patch.assert_called_once() + kwargs = mock_patch.call_args.kwargs + self.assertEqual(kwargs['patch'], {'path': 'test'}) + + @mock.patch.object(resource.Resource, '_prepare_request', autospec=True) + @mock.patch.object(resource.Resource, '_commit', autospec=True) + def test_node_patch_reset_interfaces(self, mock__commit, mock_prepreq, + mock_patch): + patch = {'path': 'test'} + self.node.patch(self.session, patch=patch, retry_on_conflict=True, + reset_interfaces=True) + mock_prepreq.assert_called_once() + prepreq_kwargs = mock_prepreq.call_args.kwargs + self.assertEqual(prepreq_kwargs['params'], + [('reset_interfaces', True)]) + mock__commit.assert_called_once() + commit_args = mock__commit.call_args.args + commit_kwargs = mock__commit.call_args.kwargs + self.assertIn('1.45', commit_args) + self.assertEqual(commit_kwargs['retry_on_conflict'], True) + mock_patch.assert_not_called() diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 43e47e91b..ff29ba83d 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1104,6 +1104,27 @@ class Test(resource.Resource): self.assertEqual([{'op': 'add', 'path': '/x', 'value': 1}], result.body) + def test__prepare_request_with_patch_params(self): + class Test(resource.Resource): + commit_jsonpatch = True + base_path = "/something" + x = resource.Body("x") + y = resource.Body("y") + + the_id = "id" + sot = Test.existing(id=the_id, x=1, y=2) + sot.x = 3 + + params = [('foo', 'bar'), + ('life', 42)] + + result = sot._prepare_request(requires_id=True, patch=True, + params=params) + + self.assertEqual("something/id?foo=bar&life=42", result.url) + self.assertEqual([{'op': 'replace', 'path': '/x', 'value': 3}], + result.body) + def test__translate_response_no_body(self): class Test(resource.Resource): attr = resource.Header("attr") diff --git a/test-requirements.txt b/test-requirements.txt index ce8b81d83..bbd290487 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,7 +8,7 @@ ddt>=1.0.1 # MIT extras>=1.0.0 # MIT fixtures>=3.0.0 # Apache-2.0/BSD jsonschema>=2.6.0 # MIT -mock>=2.0.0 # BSD +mock>=3.0.0 # BSD prometheus-client>=0.4.2 # Apache-2.0 python-subunit>=1.0.0 # Apache-2.0/BSD oslo.config>=6.1.0 # Apache-2.0 From 16502c0ec3fcef7b5f022126f5e9773bd4318884 Mon Sep 17 00:00:00 2001 From: Adrian Turjak Date: Thu, 19 Dec 2019 15:36:54 +1300 Subject: [PATCH 2585/3836] Fix bug in object storage container creation setting metadata New object storage containers were not correctly setting valid metadata on creation. This was because the data wasn't being marked as dirty when a NEW container resource object was being created, so it never got sent with the initial creation request to the server. Change-Id: I12cd626e2475b384511b2993c2c7b593184e5138 --- openstack/object_store/v1/container.py | 4 ++-- openstack/tests/unit/object_store/v1/test_container.py | 8 ++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/openstack/object_store/v1/container.py b/openstack/object_store/v1/container.py index 1f7a83f4a..670a5db7b 100644 --- a/openstack/object_store/v1/container.py +++ b/openstack/object_store/v1/container.py @@ -113,7 +113,7 @@ def new(cls, **kwargs): name = kwargs.pop('id', None) if name: kwargs.setdefault('name', name) - return Container(_synchronized=True, **kwargs) + return cls(_synchronized=False, **kwargs) def create(self, session, prepend_key=True, base_path=None): """Create a remote resource based on this instance. @@ -131,7 +131,7 @@ def create(self, session, prepend_key=True, base_path=None): request = self._prepare_request( requires_id=True, prepend_key=prepend_key, base_path=base_path) response = session.put( - request.url, json=request.body, headers=request.headers) + request.url, headers=request.headers) self._translate_response(response, has_body=False) return self diff --git a/openstack/tests/unit/object_store/v1/test_container.py b/openstack/tests/unit/object_store/v1/test_container.py index c752cd18b..7f522aa65 100644 --- a/openstack/tests/unit/object_store/v1/test_container.py +++ b/openstack/tests/unit/object_store/v1/test_container.py @@ -193,13 +193,9 @@ def test_to_json(self): def _test_no_headers(self, sot, sot_call, sess_method): headers = {} - data = {} self.register_uris([ dict(method=sess_method, uri=self.container_endpoint, - json=self.body, - validate=dict( - headers=headers, - json=data)) + validate=dict(headers=headers)) ]) sot_call(self.cloud.object_store) @@ -211,7 +207,7 @@ def test_create_no_headers(self): def test_commit_no_headers(self): sot = container.Container.new(name=self.container) self._test_no_headers(sot, sot.commit, 'POST') - self.assert_no_calls() + self.assert_calls() def test_set_temp_url_key(self): sot = container.Container.new(name=self.container) From 999e38cf087cd3d1adcccde25448589966ba8591 Mon Sep 17 00:00:00 2001 From: Akihiro Motoki Date: Thu, 9 Jan 2020 19:51:04 +0900 Subject: [PATCH 2586/3836] Bump min version of decorator decorator.decorate decorator is only implemented in decorator 4.0.0. The current lower bound of decorator is 3.4.0 but it does not work with openstacksdk. It is found during a OSC change [1]. This commit bump the minimum version of decorator. It looks like that decorator 4.0.0 works but we now test with decorator 4.4.1 (the latest version), so I chose the latest version as the minimum version. [1] https://review.opendev.org/#/c/674325/ Change-Id: I7479a84ef19c7f1e881b9d56c12d2798340eb53b --- lower-constraints.txt | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index c4e6ec4d7..1f491875d 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -2,7 +2,7 @@ appdirs==1.3.0 coverage==4.0 cryptography==2.1 ddt==1.0.1 -decorator==3.4.0 +decorator==4.4.1 doc8==0.8.0 dogpile.cache==0.6.5 extras==1.0.0 diff --git a/requirements.txt b/requirements.txt index 90f88ae87..90a88c6e2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ os-service-types>=1.7.0 # Apache-2.0 keystoneauth1>=3.18.0 # Apache-2.0 munch>=2.1.0 # MIT -decorator>=3.4.0 # BSD +decorator>=4.4.1 # BSD jmespath>=0.9.0 # MIT ipaddress>=1.0.17;python_version<'3.3' # PSF futures>=3.0.0;python_version=='2.7' or python_version=='2.6' # BSD From 4d0d078e5e613a745f700867b8a08bf56eba28df Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 9 Jan 2020 07:50:09 -0600 Subject: [PATCH 2587/3836] Replace six.iteritems() with .items() 1. As mentioned in [1], we should avoid using six.iteritems to achieve iterators. We can use dict.items instead, as it will return iterators in PY3 as well. And dict.items/keys will more readable. 2. In py2, the performance about list should be negligible, see the link [2]. [1] https://wiki.openstack.org/wiki/Python3 [2] http://lists.openstack.org/pipermail/openstack-dev/2015-June/066391.html Change-Id: I9488135957934da0f3fc7a57b36ec58c269665bf --- openstack/resource.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/resource.py b/openstack/resource.py index fe266c6f5..1a88d8fd5 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1657,7 +1657,7 @@ def _get_next_link(cls, uri, response, data, marker, limit, total_yielded): links = data.get(pagination_key, {}) # keystone might return a dict if isinstance(links, dict): - links = ({k: v} for k, v in six.iteritems(links)) + links = ({k: v} for k, v in links.items()) for item in links: if item.get('rel') == 'next' and 'href' in item: next_link = item['href'] From 37ec175817f0484db74ffdc081da765659a7b9ab Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Wed, 6 Nov 2019 20:04:31 +0000 Subject: [PATCH 2588/3836] "qos_network_policy_id" attribute added to port resource Change-Id: I0dd2104e26e49119ad384d35318ac0f4d21a21ad Partial-Bug: #1851362 --- openstack/network/v2/port.py | 3 +++ openstack/tests/unit/network/v2/test_port.py | 3 +++ .../notes/qos-port-network-policy-cab43faa0f8bc036.yaml | 5 +++++ 3 files changed, 11 insertions(+) create mode 100644 releasenotes/notes/qos-port-network-policy-cab43faa0f8bc036.yaml diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index 94dd80911..1f96f4329 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -105,6 +105,9 @@ class Port(resource.Resource, resource.TagMixin): #: Whether to propagate uplink status of the port. *Type: bool* propagate_uplink_status = resource.Body('propagate_uplink_status', type=bool) + #: Read-only. The ID of the QoS policy attached to the network where the + # port is bound. + qos_network_policy_id = resource.Body('qos_network_policy_id') #: The ID of the QoS policy attached to the port. qos_policy_id = resource.Body('qos_policy_id') #: Read-only. The port-resource-request exposes Placement resources diff --git a/openstack/tests/unit/network/v2/test_port.py b/openstack/tests/unit/network/v2/test_port.py index 7c78a2b7e..de9b4643f 100644 --- a/openstack/tests/unit/network/v2/test_port.py +++ b/openstack/tests/unit/network/v2/test_port.py @@ -38,6 +38,7 @@ 'name': '17', 'network_id': '18', 'port_security_enabled': True, + 'qos_network_policy_id': '32', 'qos_policy_id': '21', 'propagate_uplink_status': False, 'resource_request': { @@ -127,6 +128,8 @@ def test_make_it(self): self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['network_id'], sot.network_id) self.assertTrue(sot.is_port_security_enabled) + self.assertEqual(EXAMPLE['qos_network_policy_id'], + sot.qos_network_policy_id) self.assertEqual(EXAMPLE['qos_policy_id'], sot.qos_policy_id) self.assertEqual(EXAMPLE['propagate_uplink_status'], sot.propagate_uplink_status) diff --git a/releasenotes/notes/qos-port-network-policy-cab43faa0f8bc036.yaml b/releasenotes/notes/qos-port-network-policy-cab43faa0f8bc036.yaml new file mode 100644 index 000000000..e472ddf48 --- /dev/null +++ b/releasenotes/notes/qos-port-network-policy-cab43faa0f8bc036.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + ``qos_network_policy_id`` attribute support has been added to the network + port resource From fcdb2924d4599f7b85315d29d43e71bf3509f9fe Mon Sep 17 00:00:00 2001 From: Carlos Goncalves Date: Fri, 17 Jan 2020 10:01:01 +0100 Subject: [PATCH 2589/3836] Add allowed_cidrs param to load balancer listener Parameter allowed_cidrs was added in Octavia API v2.12 (included in Train release). Change-Id: I043efce748d48bd1743ed3706cabda5e51494cfe --- openstack/load_balancer/v2/listener.py | 4 +++- openstack/tests/unit/load_balancer/test_listener.py | 3 +++ ...-allowed-cidrs-loadbalancer-listener-809e523a8bd6a7d5.yaml | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-support-allowed-cidrs-loadbalancer-listener-809e523a8bd6a7d5.yaml diff --git a/openstack/load_balancer/v2/listener.py b/openstack/load_balancer/v2/listener.py index 894a93523..88ccfef74 100644 --- a/openstack/load_balancer/v2/listener.py +++ b/openstack/load_balancer/v2/listener.py @@ -31,12 +31,14 @@ class Listener(resource.Resource, resource.TagMixin): 'created_at', 'updated_at', 'provisioning_status', 'operating_status', 'sni_container_refs', 'insert_headers', 'load_balancer_id', 'timeout_client_data', 'timeout_member_connect', - 'timeout_member_data', 'timeout_tcp_inspect', + 'timeout_member_data', 'timeout_tcp_inspect', 'allowed_cidrs', is_admin_state_up='admin_state_up', **resource.TagMixin._tag_query_parameters ) # Properties + #: List of IPv4 or IPv6 CIDRs. + allowed_cidrs = resource.Body('allowed_cidrs', type=list) #: The maximum number of connections permitted for this load balancer. #: Default is infinite. connection_limit = resource.Body('connection_limit') diff --git a/openstack/tests/unit/load_balancer/test_listener.py b/openstack/tests/unit/load_balancer/test_listener.py index a0b254116..299e61e94 100644 --- a/openstack/tests/unit/load_balancer/test_listener.py +++ b/openstack/tests/unit/load_balancer/test_listener.py @@ -18,6 +18,7 @@ IDENTIFIER = 'IDENTIFIER' EXAMPLE = { 'admin_state_up': True, + 'allowed_cidrs': ['192.168.1.0/24'], 'connection_limit': '2', 'default_pool_id': uuid.uuid4(), 'description': 'test description', @@ -67,6 +68,7 @@ def test_basic(self): def test_make_it(self): test_listener = listener.Listener(**EXAMPLE) self.assertTrue(test_listener.is_admin_state_up) + self.assertEqual(EXAMPLE['allowed_cidrs'], test_listener.allowed_cidrs) self.assertEqual(EXAMPLE['connection_limit'], test_listener.connection_limit) self.assertEqual(EXAMPLE['default_pool_id'], @@ -118,6 +120,7 @@ def test_make_it(self): 'provisioning_status': 'provisioning_status', 'is_admin_state_up': 'admin_state_up', + 'allowed_cidrs': 'allowed_cidrs', 'connection_limit': 'connection_limit', 'default_pool_id': 'default_pool_id', 'default_tls_container_ref': 'default_tls_container_ref', diff --git a/releasenotes/notes/add-support-allowed-cidrs-loadbalancer-listener-809e523a8bd6a7d5.yaml b/releasenotes/notes/add-support-allowed-cidrs-loadbalancer-listener-809e523a8bd6a7d5.yaml new file mode 100644 index 000000000..3d47aa397 --- /dev/null +++ b/releasenotes/notes/add-support-allowed-cidrs-loadbalancer-listener-809e523a8bd6a7d5.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added allowed_cidrs parameter into load balancer listener. From a5368985ac294416c74109d49e80d3b747da21a8 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Wed, 22 Jan 2020 08:03:22 +0100 Subject: [PATCH 2590/3836] Use the bifrost bionic CI job The Xenial one is non-functional because of the Python 2 deprecation. Change-Id: Ia11ffcf309786edb67c2b53d84795f7a842f85fb --- .zuul.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index 5139c5a16..39125db6a 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -421,7 +421,7 @@ # Ironic jobs, non-voting to avoid tight coupling - ironic-inspector-tempest-openstacksdk-src: voting: false - - bifrost-integration-tinyipa-ubuntu-xenial: + - bifrost-integration-tinyipa-ubuntu-bionic: voting: false - metalsmith-integration-openstacksdk-src: voting: false From 666381d6e45db8df707ff39a7ef8385cca172c3a Mon Sep 17 00:00:00 2001 From: Sagi Shnaidman Date: Mon, 20 Jan 2020 00:46:49 +0200 Subject: [PATCH 2591/3836] Fail a job for ansible modules with message In order prevent merging patches to Openstack Ansible modules via Github fail the CI job that runs there with appropriate message about movign the code. Remove all ansible bits Add a new job from collections to check changes. Depends-On: https://review.opendev.org/#/c/704332/ Change-Id: Id1aafe59775f7d59ea88e4ac9deef952abbdeda1 --- .zuul.yaml | 6 +- extras/run-ansible-tests.sh | 77 ++--------- openstack/tests/ansible/README.txt | 26 ---- .../tests/ansible/hooks/post_test_hook.sh | 40 ------ .../tests/ansible/roles/auth/tasks/main.yml | 6 - .../roles/client_config/tasks/main.yml | 7 - .../ansible/roles/group/defaults/main.yml | 1 - .../tests/ansible/roles/group/tasks/main.yml | 19 --- .../ansible/roles/image/defaults/main.yml | 1 - .../tests/ansible/roles/image/tasks/main.yml | 54 -------- .../ansible/roles/keypair/defaults/main.yml | 1 - .../ansible/roles/keypair/tasks/main.yml | 62 --------- .../roles/keystone_domain/defaults/main.yml | 1 - .../roles/keystone_domain/tasks/main.yml | 19 --- .../roles/keystone_role/defaults/main.yml | 1 - .../roles/keystone_role/tasks/main.yml | 12 -- .../ansible/roles/network/defaults/main.yml | 3 - .../ansible/roles/network/tasks/main.yml | 14 -- .../ansible/roles/nova_flavor/tasks/main.yml | 53 -------- .../tests/ansible/roles/object/tasks/main.yml | 37 ------ .../ansible/roles/port/defaults/main.yml | 6 - .../tests/ansible/roles/port/tasks/main.yml | 101 -------------- .../ansible/roles/router/defaults/main.yml | 3 - .../tests/ansible/roles/router/tasks/main.yml | 95 -------------- .../roles/security_group/defaults/main.yml | 1 - .../roles/security_group/tasks/main.yml | 123 ------------------ .../ansible/roles/server/defaults/main.yaml | 5 - .../tests/ansible/roles/server/tasks/main.yml | 92 ------------- .../ansible/roles/subnet/defaults/main.yml | 2 - .../tests/ansible/roles/subnet/tasks/main.yml | 43 ------ .../tests/ansible/roles/user/tasks/main.yml | 30 ----- .../ansible/roles/user_group/tasks/main.yml | 31 ----- .../tests/ansible/roles/volume/tasks/main.yml | 17 --- openstack/tests/ansible/run.yml | 26 ---- 34 files changed, 11 insertions(+), 1004 deletions(-) delete mode 100644 openstack/tests/ansible/README.txt delete mode 100755 openstack/tests/ansible/hooks/post_test_hook.sh delete mode 100644 openstack/tests/ansible/roles/auth/tasks/main.yml delete mode 100644 openstack/tests/ansible/roles/client_config/tasks/main.yml delete mode 100644 openstack/tests/ansible/roles/group/defaults/main.yml delete mode 100644 openstack/tests/ansible/roles/group/tasks/main.yml delete mode 100644 openstack/tests/ansible/roles/image/defaults/main.yml delete mode 100644 openstack/tests/ansible/roles/image/tasks/main.yml delete mode 100644 openstack/tests/ansible/roles/keypair/defaults/main.yml delete mode 100644 openstack/tests/ansible/roles/keypair/tasks/main.yml delete mode 100644 openstack/tests/ansible/roles/keystone_domain/defaults/main.yml delete mode 100644 openstack/tests/ansible/roles/keystone_domain/tasks/main.yml delete mode 100644 openstack/tests/ansible/roles/keystone_role/defaults/main.yml delete mode 100644 openstack/tests/ansible/roles/keystone_role/tasks/main.yml delete mode 100644 openstack/tests/ansible/roles/network/defaults/main.yml delete mode 100644 openstack/tests/ansible/roles/network/tasks/main.yml delete mode 100644 openstack/tests/ansible/roles/nova_flavor/tasks/main.yml delete mode 100644 openstack/tests/ansible/roles/object/tasks/main.yml delete mode 100644 openstack/tests/ansible/roles/port/defaults/main.yml delete mode 100644 openstack/tests/ansible/roles/port/tasks/main.yml delete mode 100644 openstack/tests/ansible/roles/router/defaults/main.yml delete mode 100644 openstack/tests/ansible/roles/router/tasks/main.yml delete mode 100644 openstack/tests/ansible/roles/security_group/defaults/main.yml delete mode 100644 openstack/tests/ansible/roles/security_group/tasks/main.yml delete mode 100644 openstack/tests/ansible/roles/server/defaults/main.yaml delete mode 100644 openstack/tests/ansible/roles/server/tasks/main.yml delete mode 100644 openstack/tests/ansible/roles/subnet/defaults/main.yml delete mode 100644 openstack/tests/ansible/roles/subnet/tasks/main.yml delete mode 100644 openstack/tests/ansible/roles/user/tasks/main.yml delete mode 100644 openstack/tests/ansible/roles/user_group/tasks/main.yml delete mode 100644 openstack/tests/ansible/roles/volume/tasks/main.yml delete mode 100644 openstack/tests/ansible/run.yml diff --git a/.zuul.yaml b/.zuul.yaml index 5139c5a16..0a05762ac 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -402,10 +402,6 @@ - release-notes-jobs-python3 check: jobs: - - openstacksdk-ansible-devel-functional-devstack: - voting: false - - openstacksdk-ansible-stable-2.8-functional-devstack: - voting: false - openstacksdk-functional-devstack - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-senlin @@ -425,6 +421,8 @@ voting: false - metalsmith-integration-openstacksdk-src: voting: false + - ansible-collections-openstack-functional-devstack: + voting: false gate: jobs: - openstacksdk-functional-devstack diff --git a/extras/run-ansible-tests.sh b/extras/run-ansible-tests.sh index 14ed166f2..e4ce80ea5 100755 --- a/extras/run-ansible-tests.sh +++ b/extras/run-ansible-tests.sh @@ -30,71 +30,12 @@ # run-ansible-tests.sh -e ansible -c cloudX auth keypair network ############################################################################# - -CLOUD="devstack-admin" -ENVDIR= -USE_DEV=0 - -while getopts "c:de:" opt -do - case $opt in - d) USE_DEV=1 ;; - c) CLOUD=${OPTARG} ;; - e) ENVDIR=${OPTARG} ;; - ?) echo "Invalid option: -${OPTARG}" - exit 1;; - esac -done - -if [ -z ${ENVDIR} ] -then - echo "Option -e is required" - exit 1 -fi - -shift $((OPTIND-1)) -TAGS=$( echo "$*" | tr ' ' , ) - -# We need to source the current tox environment so that Ansible will -# be setup for the correct python environment. -source $ENVDIR/bin/activate - -if [ ${USE_DEV} -eq 1 ] -then - if [ -d ${ENVDIR}/ansible ] - then - echo "Using existing Ansible source repo" - else - echo "Installing Ansible source repo at $ENVDIR" - git clone --recursive https://github.com/ansible/ansible.git ${ENVDIR}/ansible - fi - source $ENVDIR/ansible/hacking/env-setup -fi - -# Run the shade Ansible tests -tag_opt="" -if [ ! -z ${TAGS} ] -then - tag_opt="--tags ${TAGS}" -fi - -# Loop through all ANSIBLE_VAR_ environment variables to allow passing the further -for var in $(env | grep -e '^ANSIBLE_VAR_'); do - VAR_NAME=${var%%=*} # split variable name from value - ANSIBLE_VAR_NAME=${VAR_NAME#ANSIBLE_VAR_} # cut ANSIBLE_VAR_ prefix from variable name - ANSIBLE_VAR_NAME=${ANSIBLE_VAR_NAME,,} # lowercase ansible variable - ANSIBLE_VAR_VALUE=${!VAR_NAME} # Get the variable value - ANSIBLE_VARS+="${ANSIBLE_VAR_NAME}=${ANSIBLE_VAR_VALUE} " # concat variables -done - -# Until we have a module that lets us determine the image we want from -# within a playbook, we have to find the image here and pass it in. -# We use the openstack client instead of nova client since it can use clouds.yaml. -IMAGE=`openstack --os-cloud=${CLOUD} image list -f value -c Name | grep cirros | grep -v -e ramdisk -e kernel` -if [ $? -ne 0 ] -then - echo "Failed to find Cirros image" - exit 1 -fi - -ansible-playbook -vvv ./openstack/tests/ansible/run.yml -e "cloud=${CLOUD} image=${IMAGE} ${ANSIBLE_VARS}" ${tag_opt} +echo " + Thanks for submitting patch for Openstack Ansible modules! + We moved Openstack Ansible modules to Openstack repositories. + Next patches should be submitted not with Ansible Github but with + Openstack Gerrit: https://review.opendev.org/#/q/project:openstack/ansible-collections-openstack + Please submit your code there from now. + Thanks for your contribution and sorry for inconvienience. +" +exit 1 diff --git a/openstack/tests/ansible/README.txt b/openstack/tests/ansible/README.txt deleted file mode 100644 index 3931b4af9..000000000 --- a/openstack/tests/ansible/README.txt +++ /dev/null @@ -1,26 +0,0 @@ -This directory contains a testing infrastructure for the Ansible -OpenStack modules. You will need a clouds.yaml file in order to run -the tests. You must provide a value for the `cloud` variable for each -run (using the -e option) as a default is not currently provided. - -If you want to run these tests against devstack, it is easiest to use -the tox target. This assumes you have a devstack-admin cloud defined -in your clouds.yaml file that points to devstack. Some examples of -using tox: - - tox -e ansible - - tox -e ansible keypair security_group - -If you want to run these tests directly, or against different clouds, -then you'll need to use the ansible-playbook command that comes with -the Ansible distribution and feed it the run.yml playbook. Some examples: - - # Run all module tests against a provider - ansible-playbook run.yml -e "cloud=hp" - - # Run only the keypair and security_group tests - ansible-playbook run.yml -e "cloud=hp" --tags "keypair,security_group" - - # Run all tests except security_group - ansible-playbook run.yml -e "cloud=hp" --skip-tags "security_group" diff --git a/openstack/tests/ansible/hooks/post_test_hook.sh b/openstack/tests/ansible/hooks/post_test_hook.sh deleted file mode 100755 index bbda4af3b..000000000 --- a/openstack/tests/ansible/hooks/post_test_hook.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/sh - -# 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. - -# TODO(shade) Rework for Zuul v3 - -export OPENSTACKSDK_DIR="$BASE/new/openstacksdk" - -cd $OPENSTACKSDK_DIR -sudo chown -R jenkins:stack $OPENSTACKSDK_DIR - -echo "Running shade Ansible test suite" - -if [ ${OPENSTACKSDK_ANSIBLE_DEV:-0} -eq 1 ] -then - # Use the upstream development version of Ansible - set +e - sudo -E -H -u jenkins tox -eansible -- -d - EXIT_CODE=$? - set -e -else - # Use the release version of Ansible - set +e - sudo -E -H -u jenkins tox -eansible - EXIT_CODE=$? - set -e -fi - - -exit $EXIT_CODE diff --git a/openstack/tests/ansible/roles/auth/tasks/main.yml b/openstack/tests/ansible/roles/auth/tasks/main.yml deleted file mode 100644 index ca894e50a..000000000 --- a/openstack/tests/ansible/roles/auth/tasks/main.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -- name: Authenticate to the cloud - os_auth: - cloud={{ cloud }} - -- debug: var=service_catalog diff --git a/openstack/tests/ansible/roles/client_config/tasks/main.yml b/openstack/tests/ansible/roles/client_config/tasks/main.yml deleted file mode 100644 index 1506f6d69..000000000 --- a/openstack/tests/ansible/roles/client_config/tasks/main.yml +++ /dev/null @@ -1,7 +0,0 @@ ---- -- name: List all profiles - os_client_config: - register: list - -# WARNING: This will output sensitive authentication information!!!! -- debug: var=list diff --git a/openstack/tests/ansible/roles/group/defaults/main.yml b/openstack/tests/ansible/roles/group/defaults/main.yml deleted file mode 100644 index 361c01190..000000000 --- a/openstack/tests/ansible/roles/group/defaults/main.yml +++ /dev/null @@ -1 +0,0 @@ -group_name: ansible_group diff --git a/openstack/tests/ansible/roles/group/tasks/main.yml b/openstack/tests/ansible/roles/group/tasks/main.yml deleted file mode 100644 index 535ed4318..000000000 --- a/openstack/tests/ansible/roles/group/tasks/main.yml +++ /dev/null @@ -1,19 +0,0 @@ ---- -- name: Create group - os_group: - cloud: "{{ cloud }}" - state: present - name: "{{ group_name }}" - -- name: Update group - os_group: - cloud: "{{ cloud }}" - state: present - name: "{{ group_name }}" - description: "updated description" - -- name: Delete group - os_group: - cloud: "{{ cloud }}" - state: absent - name: "{{ group_name }}" diff --git a/openstack/tests/ansible/roles/image/defaults/main.yml b/openstack/tests/ansible/roles/image/defaults/main.yml deleted file mode 100644 index 13efe7144..000000000 --- a/openstack/tests/ansible/roles/image/defaults/main.yml +++ /dev/null @@ -1 +0,0 @@ -image_name: ansible_image diff --git a/openstack/tests/ansible/roles/image/tasks/main.yml b/openstack/tests/ansible/roles/image/tasks/main.yml deleted file mode 100644 index 587e887b8..000000000 --- a/openstack/tests/ansible/roles/image/tasks/main.yml +++ /dev/null @@ -1,54 +0,0 @@ ---- -- name: Create a test image file - shell: mktemp - register: tmp_file - -- name: Fill test image file to 1MB - shell: truncate -s 1048576 {{ tmp_file.stdout }} - -- name: Create raw image (defaults) - os_image: - cloud: "{{ cloud }}" - state: present - name: "{{ image_name }}" - filename: "{{ tmp_file.stdout }}" - disk_format: raw - register: image - -- debug: var=image - -- name: Delete raw image (defaults) - os_image: - cloud: "{{ cloud }}" - state: absent - name: "{{ image_name }}" - -- name: Create raw image (complex) - os_image: - cloud: "{{ cloud }}" - state: present - name: "{{ image_name }}" - filename: "{{ tmp_file.stdout }}" - disk_format: raw - is_public: True - min_disk: 10 - min_ram: 1024 - kernel: cirros-vmlinuz - ramdisk: cirros-initrd - properties: - cpu_arch: x86_64 - distro: ubuntu - register: image - -- debug: var=image - -- name: Delete raw image (complex) - os_image: - cloud: "{{ cloud }}" - state: absent - name: "{{ image_name }}" - -- name: Delete test image file - file: - name: "{{ tmp_file.stdout }}" - state: absent diff --git a/openstack/tests/ansible/roles/keypair/defaults/main.yml b/openstack/tests/ansible/roles/keypair/defaults/main.yml deleted file mode 100644 index 3956b56a2..000000000 --- a/openstack/tests/ansible/roles/keypair/defaults/main.yml +++ /dev/null @@ -1 +0,0 @@ -keypair_name: shade_keypair diff --git a/openstack/tests/ansible/roles/keypair/tasks/main.yml b/openstack/tests/ansible/roles/keypair/tasks/main.yml deleted file mode 100644 index 636bf1aca..000000000 --- a/openstack/tests/ansible/roles/keypair/tasks/main.yml +++ /dev/null @@ -1,62 +0,0 @@ ---- -- name: Create keypair (non-existing) - os_keypair: - cloud: "{{ cloud }}" - name: "{{ keypair_name }}" - state: present - register: - keypair - -# This assert verifies that Ansible is capable serializing data returned by SDK -- name: Ensure private key is returned - assert: - that: - - keypair.key.public_key is defined and keypair.key.public_key - -- name: Delete keypair (non-existing) - os_keypair: - cloud: "{{ cloud }}" - name: "{{ keypair_name }}" - state: absent - -- name: Generate test key file - user: - name: "{{ ansible_env.USER }}" - generate_ssh_key: yes - ssh_key_file: .ssh/shade_id_rsa - -- name: Create keypair (file) - os_keypair: - cloud: "{{ cloud }}" - name: "{{ keypair_name }}" - state: present - public_key_file: "{{ ansible_env.HOME }}/.ssh/shade_id_rsa.pub" - -- name: Delete keypair (file) - os_keypair: - cloud: "{{ cloud }}" - name: "{{ keypair_name }}" - state: absent - -- name: Create keypair (key) - os_keypair: - cloud: "{{ cloud }}" - name: "{{ keypair_name }}" - state: present - public_key: "{{ lookup('file', '~/.ssh/shade_id_rsa.pub') }}" - -- name: Delete keypair (key) - os_keypair: - cloud: "{{ cloud }}" - name: "{{ keypair_name }}" - state: absent - -- name: Delete test key pub file - file: - name: "{{ ansible_env.HOME }}/.ssh/shade_id_rsa.pub" - state: absent - -- name: Delete test key pvt file - file: - name: "{{ ansible_env.HOME }}/.ssh/shade_id_rsa" - state: absent diff --git a/openstack/tests/ansible/roles/keystone_domain/defaults/main.yml b/openstack/tests/ansible/roles/keystone_domain/defaults/main.yml deleted file mode 100644 index 049e7c378..000000000 --- a/openstack/tests/ansible/roles/keystone_domain/defaults/main.yml +++ /dev/null @@ -1 +0,0 @@ -domain_name: ansible_domain diff --git a/openstack/tests/ansible/roles/keystone_domain/tasks/main.yml b/openstack/tests/ansible/roles/keystone_domain/tasks/main.yml deleted file mode 100644 index d1ca1273b..000000000 --- a/openstack/tests/ansible/roles/keystone_domain/tasks/main.yml +++ /dev/null @@ -1,19 +0,0 @@ ---- -- name: Create keystone domain - os_keystone_domain: - cloud: "{{ cloud }}" - state: present - name: "{{ domain_name }}" - description: "test description" - -- name: Update keystone domain - os_keystone_domain: - cloud: "{{ cloud }}" - name: "{{ domain_name }}" - description: "updated description" - -- name: Delete keystone domain - os_keystone_domain: - cloud: "{{ cloud }}" - state: absent - name: "{{ domain_name }}" diff --git a/openstack/tests/ansible/roles/keystone_role/defaults/main.yml b/openstack/tests/ansible/roles/keystone_role/defaults/main.yml deleted file mode 100644 index d1ebe5d1c..000000000 --- a/openstack/tests/ansible/roles/keystone_role/defaults/main.yml +++ /dev/null @@ -1 +0,0 @@ -role_name: ansible_keystone_role diff --git a/openstack/tests/ansible/roles/keystone_role/tasks/main.yml b/openstack/tests/ansible/roles/keystone_role/tasks/main.yml deleted file mode 100644 index 110b4386b..000000000 --- a/openstack/tests/ansible/roles/keystone_role/tasks/main.yml +++ /dev/null @@ -1,12 +0,0 @@ ---- -- name: Create keystone role - os_keystone_role: - cloud: "{{ cloud }}" - state: present - name: "{{ role_name }}" - -- name: Delete keystone role - os_keystone_role: - cloud: "{{ cloud }}" - state: absent - name: "{{ role_name }}" diff --git a/openstack/tests/ansible/roles/network/defaults/main.yml b/openstack/tests/ansible/roles/network/defaults/main.yml deleted file mode 100644 index d5435ecb1..000000000 --- a/openstack/tests/ansible/roles/network/defaults/main.yml +++ /dev/null @@ -1,3 +0,0 @@ -network_name: shade_network -network_shared: false -network_external: false diff --git a/openstack/tests/ansible/roles/network/tasks/main.yml b/openstack/tests/ansible/roles/network/tasks/main.yml deleted file mode 100644 index 8a85c25cc..000000000 --- a/openstack/tests/ansible/roles/network/tasks/main.yml +++ /dev/null @@ -1,14 +0,0 @@ ---- -- name: Create network - os_network: - cloud: "{{ cloud }}" - name: "{{ network_name }}" - state: present - shared: "{{ network_shared }}" - external: "{{ network_external }}" - -- name: Delete network - os_network: - cloud: "{{ cloud }}" - name: "{{ network_name }}" - state: absent diff --git a/openstack/tests/ansible/roles/nova_flavor/tasks/main.yml b/openstack/tests/ansible/roles/nova_flavor/tasks/main.yml deleted file mode 100644 index c034bfc70..000000000 --- a/openstack/tests/ansible/roles/nova_flavor/tasks/main.yml +++ /dev/null @@ -1,53 +0,0 @@ ---- -- name: Create public flavor - os_nova_flavor: - cloud: "{{ cloud }}" - state: present - name: ansible_public_flavor - is_public: True - ram: 1024 - vcpus: 1 - disk: 10 - ephemeral: 10 - swap: 1 - flavorid: 12345 - -- name: Delete public flavor - os_nova_flavor: - cloud: "{{ cloud }}" - state: absent - name: ansible_public_flavor - -- name: Create private flavor - os_nova_flavor: - cloud: "{{ cloud }}" - state: present - name: ansible_private_flavor - is_public: False - ram: 1024 - vcpus: 1 - disk: 10 - ephemeral: 10 - swap: 1 - flavorid: 12345 - -- name: Delete private flavor - os_nova_flavor: - cloud: "{{ cloud }}" - state: absent - name: ansible_private_flavor - -- name: Create flavor (defaults) - os_nova_flavor: - cloud: "{{ cloud }}" - state: present - name: ansible_defaults_flavor - ram: 1024 - vcpus: 1 - disk: 10 - -- name: Delete flavor (defaults) - os_nova_flavor: - cloud: "{{ cloud }}" - state: absent - name: ansible_defaults_flavor diff --git a/openstack/tests/ansible/roles/object/tasks/main.yml b/openstack/tests/ansible/roles/object/tasks/main.yml deleted file mode 100644 index ae54b6ba2..000000000 --- a/openstack/tests/ansible/roles/object/tasks/main.yml +++ /dev/null @@ -1,37 +0,0 @@ ---- -- name: Create a test object file - shell: mktemp - register: tmp_file - -- name: Create container - os_object: - cloud: "{{ cloud }}" - state: present - container: ansible_container - container_access: private - -- name: Put object - os_object: - cloud: "{{ cloud }}" - state: present - name: ansible_object - filename: "{{ tmp_file.stdout }}" - container: ansible_container - -- name: Delete object - os_object: - cloud: "{{ cloud }}" - state: absent - name: ansible_object - container: ansible_container - -- name: Delete container - os_object: - cloud: "{{ cloud }}" - state: absent - container: ansible_container - -- name: Delete test object file - file: - name: "{{ tmp_file.stdout }}" - state: absent diff --git a/openstack/tests/ansible/roles/port/defaults/main.yml b/openstack/tests/ansible/roles/port/defaults/main.yml deleted file mode 100644 index de022001b..000000000 --- a/openstack/tests/ansible/roles/port/defaults/main.yml +++ /dev/null @@ -1,6 +0,0 @@ -network_name: ansible_port_network -network_external: true -subnet_name: ansible_port_subnet -port_name: ansible_port -secgroup_name: ansible_port_secgroup -no_security_groups: True diff --git a/openstack/tests/ansible/roles/port/tasks/main.yml b/openstack/tests/ansible/roles/port/tasks/main.yml deleted file mode 100644 index 1a39140e5..000000000 --- a/openstack/tests/ansible/roles/port/tasks/main.yml +++ /dev/null @@ -1,101 +0,0 @@ ---- -- name: Create network - os_network: - cloud: "{{ cloud }}" - state: present - name: "{{ network_name }}" - external: "{{ network_external }}" - -- name: Create subnet - os_subnet: - cloud: "{{ cloud }}" - state: present - name: "{{ subnet_name }}" - network_name: "{{ network_name }}" - cidr: 10.5.5.0/24 - -- name: Create port (no security group or default security group) - os_port: - cloud: "{{ cloud }}" - state: present - name: "{{ port_name }}" - network: "{{ network_name }}" - no_security_groups: "{{ no_security_groups }}" - fixed_ips: - - ip_address: 10.5.5.69 - register: port - -- debug: var=port - -- name: Delete port (no security group or default security group) - os_port: - cloud: "{{ cloud }}" - state: absent - name: "{{ port_name }}" - -- name: Create security group - os_security_group: - cloud: "{{ cloud }}" - state: present - name: "{{ secgroup_name }}" - description: Test group - -- name: Create port (with security group) - os_port: - cloud: "{{ cloud }}" - state: present - name: "{{ port_name }}" - network: "{{ network_name }}" - fixed_ips: - - ip_address: 10.5.5.69 - security_groups: - - "{{ secgroup_name }}" - register: port - -- debug: var=port - -- name: Delete port (with security group) - os_port: - cloud: "{{ cloud }}" - state: absent - name: "{{ port_name }}" - -- name: Create port (with allowed_address_pairs and extra_dhcp_opts) - os_port: - cloud: "{{ cloud }}" - state: present - name: "{{ port_name }}" - network: "{{ network_name }}" - no_security_groups: "{{ no_security_groups }}" - allowed_address_pairs: - - ip_address: 10.6.7.0/24 - extra_dhcp_opts: - - opt_name: "bootfile-name" - opt_value: "testfile.1" - register: port - -- debug: var=port - -- name: Delete port (with allowed_address_pairs and extra_dhcp_opts) - os_port: - cloud: "{{ cloud }}" - state: absent - name: "{{ port_name }}" - -- name: Delete security group - os_security_group: - cloud: "{{ cloud }}" - state: absent - name: "{{ secgroup_name }}" - -- name: Delete subnet - os_subnet: - cloud: "{{ cloud }}" - state: absent - name: "{{ subnet_name }}" - -- name: Delete network - os_network: - cloud: "{{ cloud }}" - state: absent - name: "{{ network_name }}" diff --git a/openstack/tests/ansible/roles/router/defaults/main.yml b/openstack/tests/ansible/roles/router/defaults/main.yml deleted file mode 100644 index f7d53933a..000000000 --- a/openstack/tests/ansible/roles/router/defaults/main.yml +++ /dev/null @@ -1,3 +0,0 @@ -external_network_name: ansible_external_net -network_external: true -router_name: ansible_router diff --git a/openstack/tests/ansible/roles/router/tasks/main.yml b/openstack/tests/ansible/roles/router/tasks/main.yml deleted file mode 100644 index 083d4f066..000000000 --- a/openstack/tests/ansible/roles/router/tasks/main.yml +++ /dev/null @@ -1,95 +0,0 @@ ---- -# Regular user operation -- name: Create internal network - os_network: - cloud: "{{ cloud }}" - state: present - name: "{{ network_name }}" - external: false - -- name: Create subnet1 - os_subnet: - cloud: "{{ cloud }}" - state: present - network_name: "{{ network_name }}" - name: shade_subnet1 - cidr: 10.7.7.0/24 - -- name: Create router - os_router: - cloud: "{{ cloud }}" - state: present - name: "{{ router_name }}" - -- name: Update router (add interface) - os_router: - cloud: "{{ cloud }}" - state: present - name: "{{ router_name }}" - interfaces: - - shade_subnet1 - -# Admin operation -- name: Create external network - os_network: - cloud: "{{ cloud }}" - state: present - name: "{{ external_network_name }}" - external: "{{ network_external }}" - when: - - network_external - -- name: Create subnet2 - os_subnet: - cloud: "{{ cloud }}" - state: present - network_name: "{{ external_network_name }}" - name: shade_subnet2 - cidr: 10.6.6.0/24 - when: - - network_external - -- name: Update router (add external gateway) - os_router: - cloud: "{{ cloud }}" - state: present - name: "{{ router_name }}" - network: "{{ external_network_name }}" - interfaces: - - shade_subnet1 - when: - - network_external - -- name: Delete router - os_router: - cloud: "{{ cloud }}" - state: absent - name: "{{ router_name }}" - -- name: Delete subnet1 - os_subnet: - cloud: "{{ cloud }}" - state: absent - name: shade_subnet1 - -- name: Delete subnet2 - os_subnet: - cloud: "{{ cloud }}" - state: absent - name: shade_subnet2 - when: - - network_external - -- name: Delete internal network - os_network: - cloud: "{{ cloud }}" - state: absent - name: "{{ network_name }}" - -- name: Delete external network - os_network: - cloud: "{{ cloud }}" - state: absent - name: "{{ external_network_name }}" - when: - - network_external diff --git a/openstack/tests/ansible/roles/security_group/defaults/main.yml b/openstack/tests/ansible/roles/security_group/defaults/main.yml deleted file mode 100644 index 00310dd10..000000000 --- a/openstack/tests/ansible/roles/security_group/defaults/main.yml +++ /dev/null @@ -1 +0,0 @@ -secgroup_name: shade_secgroup diff --git a/openstack/tests/ansible/roles/security_group/tasks/main.yml b/openstack/tests/ansible/roles/security_group/tasks/main.yml deleted file mode 100644 index ddc7e50cd..000000000 --- a/openstack/tests/ansible/roles/security_group/tasks/main.yml +++ /dev/null @@ -1,123 +0,0 @@ ---- -- name: Create security group - os_security_group: - cloud: "{{ cloud }}" - name: "{{ secgroup_name }}" - state: present - description: Created from Ansible playbook - -- name: Create empty ICMP rule - os_security_group_rule: - cloud: "{{ cloud }}" - security_group: "{{ secgroup_name }}" - state: present - protocol: icmp - remote_ip_prefix: 0.0.0.0/0 - -- name: Create -1 ICMP rule - os_security_group_rule: - cloud: "{{ cloud }}" - security_group: "{{ secgroup_name }}" - state: present - protocol: icmp - port_range_min: -1 - port_range_max: -1 - remote_ip_prefix: 0.0.0.0/0 - -- name: Create empty TCP rule - os_security_group_rule: - cloud: "{{ cloud }}" - security_group: "{{ secgroup_name }}" - state: present - protocol: tcp - remote_ip_prefix: 0.0.0.0/0 - -- name: Create empty UDP rule - os_security_group_rule: - cloud: "{{ cloud }}" - security_group: "{{ secgroup_name }}" - state: present - protocol: udp - remote_ip_prefix: 0.0.0.0/0 - -- name: Create HTTP rule - os_security_group_rule: - cloud: "{{ cloud }}" - security_group: "{{ secgroup_name }}" - state: present - protocol: tcp - port_range_min: 80 - port_range_max: 80 - remote_ip_prefix: 0.0.0.0/0 - -- name: Create egress rule - os_security_group_rule: - cloud: "{{ cloud }}" - security_group: "{{ secgroup_name }}" - state: present - protocol: tcp - port_range_min: 30000 - port_range_max: 30001 - remote_ip_prefix: 0.0.0.0/0 - direction: egress - -- name: Delete empty ICMP rule - os_security_group_rule: - cloud: "{{ cloud }}" - security_group: "{{ secgroup_name }}" - state: absent - protocol: icmp - remote_ip_prefix: 0.0.0.0/0 - -- name: Delete -1 ICMP rule - os_security_group_rule: - cloud: "{{ cloud }}" - security_group: "{{ secgroup_name }}" - state: absent - protocol: icmp - port_range_min: -1 - port_range_max: -1 - remote_ip_prefix: 0.0.0.0/0 - -- name: Delete empty TCP rule - os_security_group_rule: - cloud: "{{ cloud }}" - security_group: "{{ secgroup_name }}" - state: absent - protocol: tcp - remote_ip_prefix: 0.0.0.0/0 - -- name: Delete empty UDP rule - os_security_group_rule: - cloud: "{{ cloud }}" - security_group: "{{ secgroup_name }}" - state: absent - protocol: udp - remote_ip_prefix: 0.0.0.0/0 - -- name: Delete HTTP rule - os_security_group_rule: - cloud: "{{ cloud }}" - security_group: "{{ secgroup_name }}" - state: absent - protocol: tcp - port_range_min: 80 - port_range_max: 80 - remote_ip_prefix: 0.0.0.0/0 - -- name: Delete egress rule - os_security_group_rule: - cloud: "{{ cloud }}" - security_group: "{{ secgroup_name }}" - state: absent - protocol: tcp - port_range_min: 30000 - port_range_max: 30001 - remote_ip_prefix: 0.0.0.0/0 - direction: egress - -- name: Delete security group - os_security_group: - cloud: "{{ cloud }}" - name: "{{ secgroup_name }}" - state: absent diff --git a/openstack/tests/ansible/roles/server/defaults/main.yaml b/openstack/tests/ansible/roles/server/defaults/main.yaml deleted file mode 100644 index e3bd5f33b..000000000 --- a/openstack/tests/ansible/roles/server/defaults/main.yaml +++ /dev/null @@ -1,5 +0,0 @@ -server_network: private -server_name: ansible_server -flavor: m1.tiny -floating_ip_pool_name: public -boot_volume_size: 5 diff --git a/openstack/tests/ansible/roles/server/tasks/main.yml b/openstack/tests/ansible/roles/server/tasks/main.yml deleted file mode 100644 index ac0311554..000000000 --- a/openstack/tests/ansible/roles/server/tasks/main.yml +++ /dev/null @@ -1,92 +0,0 @@ ---- -- name: Create server with meta as CSV - os_server: - cloud: "{{ cloud }}" - state: present - name: "{{ server_name }}" - image: "{{ image }}" - flavor: "{{ flavor }}" - network: "{{ server_network }}" - auto_floating_ip: false - meta: "key1=value1,key2=value2" - wait: true - register: server - -- debug: var=server - -- name: Delete server with meta as CSV - os_server: - cloud: "{{ cloud }}" - state: absent - name: "{{ server_name }}" - wait: true - -- name: Create server with meta as dict - os_server: - cloud: "{{ cloud }}" - state: present - name: "{{ server_name }}" - image: "{{ image }}" - flavor: "{{ flavor }}" - auto_floating_ip: false - network: "{{ server_network }}" - meta: - key1: value1 - key2: value2 - wait: true - register: server - -- debug: var=server - -- name: Delete server with meta as dict - os_server: - cloud: "{{ cloud }}" - state: absent - name: "{{ server_name }}" - wait: true - -- name: Create server (FIP from pool/network) - os_server: - cloud: "{{ cloud }}" - state: present - name: "{{ server_name }}" - image: "{{ image }}" - flavor: "{{ flavor }}" - network: "{{ server_network }}" - floating_ip_pools: - - "{{ floating_ip_pool_name }}" - wait: true - register: server - -- debug: var=server - -- name: Delete server (FIP from pool/network) - os_server: - cloud: "{{ cloud }}" - state: absent - name: "{{ server_name }}" - wait: true - -- name: Create server from volume - os_server: - cloud: "{{ cloud }}" - state: present - name: "{{ server_name }}" - image: "{{ image }}" - flavor: "{{ flavor }}" - network: "{{ server_network }}" - auto_floating_ip: false - boot_from_volume: true - volume_size: "{{ boot_volume_size }}" - terminate_volume: true - wait: true - register: server - -- debug: var=server - -- name: Delete server with volume - os_server: - cloud: "{{ cloud }}" - state: absent - name: "{{ server_name }}" - wait: true diff --git a/openstack/tests/ansible/roles/subnet/defaults/main.yml b/openstack/tests/ansible/roles/subnet/defaults/main.yml deleted file mode 100644 index 5ccc85abc..000000000 --- a/openstack/tests/ansible/roles/subnet/defaults/main.yml +++ /dev/null @@ -1,2 +0,0 @@ -subnet_name: shade_subnet -enable_subnet_dhcp: false diff --git a/openstack/tests/ansible/roles/subnet/tasks/main.yml b/openstack/tests/ansible/roles/subnet/tasks/main.yml deleted file mode 100644 index a7ca490ad..000000000 --- a/openstack/tests/ansible/roles/subnet/tasks/main.yml +++ /dev/null @@ -1,43 +0,0 @@ ---- -- name: Create network {{ network_name }} - os_network: - cloud: "{{ cloud }}" - name: "{{ network_name }}" - state: present - -- name: Create subnet {{ subnet_name }} on network {{ network_name }} - os_subnet: - cloud: "{{ cloud }}" - network_name: "{{ network_name }}" - name: "{{ subnet_name }}" - state: present - enable_dhcp: "{{ enable_subnet_dhcp }}" - dns_nameservers: - - 8.8.8.7 - - 8.8.8.8 - cidr: 192.168.0.0/24 - gateway_ip: 192.168.0.1 - allocation_pool_start: 192.168.0.2 - allocation_pool_end: 192.168.0.254 - -- name: Update subnet - os_subnet: - cloud: "{{ cloud }}" - network_name: "{{ network_name }}" - name: "{{ subnet_name }}" - state: present - dns_nameservers: - - 8.8.8.7 - cidr: 192.168.0.0/24 - -- name: Delete subnet {{ subnet_name }} - os_subnet: - cloud: "{{ cloud }}" - name: "{{ subnet_name }}" - state: absent - -- name: Delete network {{ network_name }} - os_network: - cloud: "{{ cloud }}" - name: "{{ network_name }}" - state: absent diff --git a/openstack/tests/ansible/roles/user/tasks/main.yml b/openstack/tests/ansible/roles/user/tasks/main.yml deleted file mode 100644 index 6585ca582..000000000 --- a/openstack/tests/ansible/roles/user/tasks/main.yml +++ /dev/null @@ -1,30 +0,0 @@ ---- -- name: Create user - os_user: - cloud: "{{ cloud }}" - state: present - name: ansible_user - password: secret - email: ansible.user@nowhere.net - domain: default - default_project: demo - register: user - -- debug: var=user - -- name: Update user - os_user: - cloud: "{{ cloud }}" - state: present - name: ansible_user - password: secret - email: updated.ansible.user@nowhere.net - register: updateduser - -- debug: var=updateduser - -- name: Delete user - os_user: - cloud: "{{ cloud }}" - state: absent - name: ansible_user diff --git a/openstack/tests/ansible/roles/user_group/tasks/main.yml b/openstack/tests/ansible/roles/user_group/tasks/main.yml deleted file mode 100644 index a0074e2dc..000000000 --- a/openstack/tests/ansible/roles/user_group/tasks/main.yml +++ /dev/null @@ -1,31 +0,0 @@ ---- -- name: Create user - os_user: - cloud: "{{ cloud }}" - state: present - name: ansible_user - password: secret - email: ansible.user@nowhere.net - domain: default - default_project: demo - register: user - -- name: Assign user to nonadmins group - os_user_group: - cloud: "{{ cloud }}" - state: present - user: ansible_user - group: nonadmins - -- name: Remove user from nonadmins group - os_user_group: - cloud: "{{ cloud }}" - state: absent - user: ansible_user - group: nonadmins - -- name: Delete user - os_user: - cloud: "{{ cloud }}" - state: absent - name: ansible_user diff --git a/openstack/tests/ansible/roles/volume/tasks/main.yml b/openstack/tests/ansible/roles/volume/tasks/main.yml deleted file mode 100644 index 1479a0030..000000000 --- a/openstack/tests/ansible/roles/volume/tasks/main.yml +++ /dev/null @@ -1,17 +0,0 @@ ---- -- name: Create volume - os_volume: - cloud: "{{ cloud }}" - state: present - size: 1 - display_name: ansible_volume - display_description: Test volume - register: vol - -- debug: var=vol - -- name: Delete volume - os_volume: - cloud: "{{ cloud }}" - state: absent - display_name: ansible_volume diff --git a/openstack/tests/ansible/run.yml b/openstack/tests/ansible/run.yml deleted file mode 100644 index 9340ccd06..000000000 --- a/openstack/tests/ansible/run.yml +++ /dev/null @@ -1,26 +0,0 @@ ---- -- hosts: localhost - connection: local - gather_facts: true - - roles: - - { role: auth, tags: auth } - - { role: client_config, tags: client_config } - - { role: group, tags: group } - # TODO(mordred) Reenable this once the fixed os_image winds up in an - # upstream ansible release. - # - { role: image, tags: image } - - { role: keypair, tags: keypair } - - { role: keystone_domain, tags: keystone_domain } - - { role: keystone_role, tags: keystone_role } - - { role: network, tags: network } - - { role: nova_flavor, tags: nova_flavor } - - { role: object, tags: object } - - { role: port, tags: port } - - { role: router, tags: router } - - { role: security_group, tags: security_group } - - { role: server, tags: server } - - { role: subnet, tags: subnet } - - { role: user, tags: user } - - { role: user_group, tags: user_group } - - { role: volume, tags: volume } From 8afbfc7cd15a4bd8470cdcf41ee15a2a1d8e840a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dulko?= Date: Thu, 30 Jan 2020 13:15:28 +0100 Subject: [PATCH 2592/3836] Fix error handling on add/remove router iface calls Ib1f880f75e511faaf8c798da22b9ca54dd441166 was manually processing call results to raise SDKException. Instead of that exception.raise_from_response() should be used to make sure a correct exception class is thrown (e.g. NotFoundException on 404 results) and this commit fixes this. Change-Id: I76b33e6c870971cdf50938d1328445702c83b64a Story: #2006679 Task: #38575 --- openstack/network/v2/router.py | 9 ++------- openstack/tests/unit/network/v2/test_router.py | 16 +++++++++++----- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/openstack/network/v2/router.py b/openstack/network/v2/router.py index 87edb27fc..a7de75f8b 100644 --- a/openstack/network/v2/router.py +++ b/openstack/network/v2/router.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.exceptions import SDKException +from openstack import exceptions from openstack import resource from openstack import utils @@ -77,12 +77,7 @@ class Router(resource.Resource, resource.TagMixin): def _put(self, session, url, body): resp = session.put(url, json=body) - if not resp.ok: - resp_body = resp.json() - message = None - if 'NeutronError' in resp_body: - message = resp_body['NeutronError']['message'] - raise SDKException(message=message) + exceptions.raise_from_response(resp) return resp def add_interface(self, session, **body): diff --git a/openstack/tests/unit/network/v2/test_router.py b/openstack/tests/unit/network/v2/test_router.py index a5cd05326..bd959ea9a 100644 --- a/openstack/tests/unit/network/v2/test_router.py +++ b/openstack/tests/unit/network/v2/test_router.py @@ -13,7 +13,7 @@ import mock import testtools -from openstack.exceptions import SDKException +from openstack import exceptions from openstack.tests.unit import base from openstack.network.v2 import router @@ -121,6 +121,7 @@ def test_add_interface_subnet(self): response = mock.Mock() response.body = {"subnet_id": "3", "port_id": "2"} response.json = mock.Mock(return_value=response.body) + response.status_code = 200 sess = mock.Mock() sess.put = mock.Mock(return_value=response) body = {"subnet_id": "3"} @@ -136,6 +137,7 @@ def test_add_interface_port(self): response = mock.Mock() response.body = {"subnet_id": "3", "port_id": "3"} response.json = mock.Mock(return_value=response.body) + response.status_code = 200 sess = mock.Mock() sess.put = mock.Mock(return_value=response) @@ -152,6 +154,7 @@ def test_remove_interface_subnet(self): response = mock.Mock() response.body = {"subnet_id": "3", "port_id": "2"} response.json = mock.Mock(return_value=response.body) + response.status_code = 200 sess = mock.Mock() sess.put = mock.Mock(return_value=response) body = {"subnet_id": "3"} @@ -167,6 +170,7 @@ def test_remove_interface_port(self): response = mock.Mock() response.body = {"subnet_id": "3", "port_id": "3"} response.json = mock.Mock(return_value=response.body) + response.status_code = 200 sess = mock.Mock() sess.put = mock.Mock(return_value=response) body = {"network_id": 3, "enable_snat": True} @@ -180,15 +184,16 @@ def test_add_interface_4xx(self): # Neutron may return 4xx, we have to raise if that happens sot = router.Router(**EXAMPLE) response = mock.Mock() - msg = 'borked' + msg = '.*borked' response.body = {'NeutronError': {'message': msg}} response.json = mock.Mock(return_value=response.body) response.ok = False response.status_code = 409 + response.headers = {'content-type': 'application/json'} sess = mock.Mock() sess.put = mock.Mock(return_value=response) body = {'subnet_id': '3'} - with testtools.ExpectedException(SDKException, msg): + with testtools.ExpectedException(exceptions.ConflictException, msg): sot.add_interface(sess, **body) def test_remove_interface_4xx(self): @@ -196,15 +201,16 @@ def test_remove_interface_4xx(self): # extra routes referring to it as a nexthop sot = router.Router(**EXAMPLE) response = mock.Mock() - msg = 'borked' + msg = '.*borked' response.body = {'NeutronError': {'message': msg}} response.json = mock.Mock(return_value=response.body) response.ok = False response.status_code = 409 + response.headers = {'content-type': 'application/json'} sess = mock.Mock() sess.put = mock.Mock(return_value=response) body = {'subnet_id': '3'} - with testtools.ExpectedException(SDKException, msg): + with testtools.ExpectedException(exceptions.ConflictException, msg): sot.remove_interface(sess, **body) def test_add_extra_routes(self): From 9b7e02bebcb27cb916d6b46eed48a8e1edeadea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dulko?= Date: Thu, 30 Jan 2020 18:26:16 +0100 Subject: [PATCH 2593/3836] Fix error handling in network trunks operations It seems like operations defined in network.v2.trunk had no error handling or even ignored results from Neutron altogether. This meant that user was not able to say if his requests were successful or not. This commit fixes the issues by adding error handling through exceptions.raise_from_response and making sure that Trunk's body is updated with the actual result from Neutron instead of the info passed from user (seems like delete_subports() even updated the Trunk object actually *adding* the removed subports). Change-Id: Ia2f469690023e1d7e1c1e7935d6681a87f9433cd Story: #2007239 Task: #38579 --- openstack/network/v2/trunk.py | 12 ++++-- openstack/tests/unit/network/v2/test_trunk.py | 41 ++++++++++++++++++- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/openstack/network/v2/trunk.py b/openstack/network/v2/trunk.py index db672bd65..94fb2fda3 100644 --- a/openstack/network/v2/trunk.py +++ b/openstack/network/v2/trunk.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import exceptions from openstack import resource from openstack import utils @@ -54,18 +55,21 @@ class Trunk(resource.Resource, resource.TagMixin): def add_subports(self, session, subports): url = utils.urljoin('/trunks', self.id, 'add_subports') - session.put(url, json={'sub_ports': subports}) - self._body.attributes.update({'sub_ports': subports}) + resp = session.put(url, json={'sub_ports': subports}) + exceptions.raise_from_response(resp) + self._body.attributes.update(resp.json()) return self def delete_subports(self, session, subports): url = utils.urljoin('/trunks', self.id, 'remove_subports') - session.put(url, json={'sub_ports': subports}) - self._body.attributes.update({'sub_ports': subports}) + resp = session.put(url, json={'sub_ports': subports}) + exceptions.raise_from_response(resp) + self._body.attributes.update(resp.json()) return self def get_subports(self, session): url = utils.urljoin('/trunks', self.id, 'get_subports') resp = session.get(url) + exceptions.raise_from_response(resp) self._body.attributes.update(resp.json()) return resp.json() diff --git a/openstack/tests/unit/network/v2/test_trunk.py b/openstack/tests/unit/network/v2/test_trunk.py index 50467bca5..063298b85 100644 --- a/openstack/tests/unit/network/v2/test_trunk.py +++ b/openstack/tests/unit/network/v2/test_trunk.py @@ -10,9 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base +import mock +import testtools +from openstack import exceptions from openstack.network.v2 import trunk +from openstack.tests.unit import base EXAMPLE = { 'id': 'IDENTIFIER', @@ -31,7 +34,7 @@ } -class TestQoSPolicy(base.TestCase): +class TestTrunk(base.TestCase): def test_basic(self): sot = trunk.Trunk() @@ -54,3 +57,37 @@ def test_make_it(self): self.assertEqual(EXAMPLE['port_id'], sot.port_id) self.assertEqual(EXAMPLE['status'], sot.status) self.assertEqual(EXAMPLE['sub_ports'], sot.sub_ports) + + def test_add_subports_4xx(self): + # Neutron may return 4xx for example if a port does not exist + sot = trunk.Trunk(**EXAMPLE) + response = mock.Mock() + msg = '.*borked' + response.body = {'NeutronError': {'message': msg}} + response.json = mock.Mock(return_value=response.body) + response.ok = False + response.status_code = 404 + response.headers = {'content-type': 'application/json'} + sess = mock.Mock() + sess.put = mock.Mock(return_value=response) + subports = [{'port_id': 'abc', 'segmentation_id': '123', + 'segmentation_type': 'vlan'}] + with testtools.ExpectedException(exceptions.ResourceNotFound, msg): + sot.add_subports(sess, subports) + + def test_delete_subports_4xx(self): + # Neutron may return 4xx for example if a port does not exist + sot = trunk.Trunk(**EXAMPLE) + response = mock.Mock() + msg = '.*borked' + response.body = {'NeutronError': {'message': msg}} + response.json = mock.Mock(return_value=response.body) + response.ok = False + response.status_code = 404 + response.headers = {'content-type': 'application/json'} + sess = mock.Mock() + sess.put = mock.Mock(return_value=response) + subports = [{'port_id': 'abc', 'segmentation_id': '123', + 'segmentation_type': 'vlan'}] + with testtools.ExpectedException(exceptions.ResourceNotFound, msg): + sot.delete_subports(sess, subports) From 6fdc3241a0b561ea20f75666fae4cf84fdcd8548 Mon Sep 17 00:00:00 2001 From: Shogo Saito Date: Fri, 31 Jan 2020 17:28:02 +0900 Subject: [PATCH 2594/3836] Adding basic implementation for Accelerator(Cyborg) Firstly,let the client can work with some cyborg APIs. This patch supports some Cyborg APIs below. - devices API (v2) - only GET method - device_profiles API (v2) - accelerator_requests API (v2) - deployables API (v2) Accelerator in cloud layer supports create/delete/bind/unbind/list methods. Change-Id: Ibd7ea4dbd358b992f1114fd101ed0a4404156042 --- doc/source/enforcer.py | 3 +- doc/source/user/index.rst | 2 + doc/source/user/proxies/accelerator.rst | 51 +++ .../user/resources/accelerator/index.rst | 11 + .../accelerator/v2/accelerator_request.rst | 13 + .../resources/accelerator/v2/deployable.rst | 13 + .../user/resources/accelerator/v2/device.rst | 13 + .../accelerator/v2/device_profile.rst | 14 + openstack/_services_mixin.py | 3 +- openstack/accelerator/__init__.py | 0 openstack/accelerator/accelerator_service.py | 21 ++ openstack/accelerator/v2/__init__.py | 0 openstack/accelerator/v2/_proxy.py | 173 +++++++++ .../accelerator/v2/accelerator_request.py | 98 ++++++ openstack/accelerator/v2/deployable.py | 66 ++++ openstack/accelerator/v2/device.py | 44 +++ openstack/accelerator/v2/device_profile.py | 48 +++ openstack/accelerator/version.py | 27 ++ openstack/cloud/_accelerator.py | 154 ++++++++ openstack/connection.py | 3 + openstack/tests/unit/accelerator/__init__.py | 0 .../tests/unit/accelerator/test_version.py | 42 +++ .../tests/unit/accelerator/v2/__init__.py | 0 .../v2/test_accelerator_request.py | 59 ++++ .../unit/accelerator/v2/test_deployable.py | 52 +++ .../tests/unit/accelerator/v2/test_device.py | 53 +++ .../accelerator/v2/test_device_profile.py | 54 +++ .../tests/unit/accelerator/v2/test_proxy.py | 66 ++++ openstack/tests/unit/base.py | 15 + .../tests/unit/cloud/test_accelerator.py | 328 ++++++++++++++++++ .../tests/unit/fixtures/accelerator.json | 27 ++ .../add-cyborg-support-b9afca69f709c048.yaml | 3 + 32 files changed, 1454 insertions(+), 2 deletions(-) create mode 100644 doc/source/user/proxies/accelerator.rst create mode 100644 doc/source/user/resources/accelerator/index.rst create mode 100644 doc/source/user/resources/accelerator/v2/accelerator_request.rst create mode 100644 doc/source/user/resources/accelerator/v2/deployable.rst create mode 100644 doc/source/user/resources/accelerator/v2/device.rst create mode 100644 doc/source/user/resources/accelerator/v2/device_profile.rst create mode 100644 openstack/accelerator/__init__.py create mode 100644 openstack/accelerator/accelerator_service.py create mode 100644 openstack/accelerator/v2/__init__.py create mode 100644 openstack/accelerator/v2/_proxy.py create mode 100644 openstack/accelerator/v2/accelerator_request.py create mode 100644 openstack/accelerator/v2/deployable.py create mode 100644 openstack/accelerator/v2/device.py create mode 100644 openstack/accelerator/v2/device_profile.py create mode 100644 openstack/accelerator/version.py create mode 100644 openstack/cloud/_accelerator.py create mode 100644 openstack/tests/unit/accelerator/__init__.py create mode 100644 openstack/tests/unit/accelerator/test_version.py create mode 100644 openstack/tests/unit/accelerator/v2/__init__.py create mode 100644 openstack/tests/unit/accelerator/v2/test_accelerator_request.py create mode 100644 openstack/tests/unit/accelerator/v2/test_deployable.py create mode 100644 openstack/tests/unit/accelerator/v2/test_device.py create mode 100644 openstack/tests/unit/accelerator/v2/test_device_profile.py create mode 100644 openstack/tests/unit/accelerator/v2/test_proxy.py create mode 100644 openstack/tests/unit/cloud/test_accelerator.py create mode 100644 openstack/tests/unit/fixtures/accelerator.json create mode 100644 releasenotes/notes/add-cyborg-support-b9afca69f709c048.yaml diff --git a/doc/source/enforcer.py b/doc/source/enforcer.py index 6eea11e40..94fd8d3dc 100644 --- a/doc/source/enforcer.py +++ b/doc/source/enforcer.py @@ -33,7 +33,8 @@ class EnforcementError(errors.SphinxError): def get_proxy_methods(): """Return a set of public names on all proxies""" - names = ["openstack.baremetal.v1._proxy", + names = ["openstack.accelerator.v2._proxy", + "openstack.baremetal.v1._proxy", "openstack.clustering.v1._proxy", "openstack.block_storage.v2._proxy", "openstack.compute.v2._proxy", diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index 9daef05f2..9559f41de 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -95,6 +95,7 @@ control which services can be used. .. toctree:: :maxdepth: 1 + Accelerator Baremetal Baremetal Introspection Block Storage @@ -129,6 +130,7 @@ The following services have exposed *Resource* classes. .. toctree:: :maxdepth: 1 + Accelerator Baremetal Baremetal Introspection Block Storage diff --git a/doc/source/user/proxies/accelerator.rst b/doc/source/user/proxies/accelerator.rst new file mode 100644 index 000000000..b5688a518 --- /dev/null +++ b/doc/source/user/proxies/accelerator.rst @@ -0,0 +1,51 @@ +Accelerator API +=============== + +.. automodule:: openstack.accelerator.v2._proxy + +The Accelerator Class +--------------------- + +The accelerator high-level interface is available through the ``accelerator`` +member of a :class:`~openstack.connection.Connection` object. +The ``accelerator`` member will only be added if the service is detected. + + +Device Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.accelerator.v2._proxy.Proxy + + .. automethod:: openstack.accelerator.v2._proxy.Proxy.get_device + .. automethod:: openstack.accelerator.v2._proxy.Proxy.devices + +Deployable Operations +^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.accelerator.v2._proxy.Proxy + + .. automethod:: openstack.accelerator.v2._proxy.Proxy.update_deployable + .. automethod:: openstack.accelerator.v2._proxy.Proxy.get_deployable + .. automethod:: openstack.accelerator.v2._proxy.Proxy.deployables + +Device Profile Operations +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.accelerator.v2._proxy.Proxy + + .. automethod:: openstack.accelerator.v2._proxy.Proxy.create_device_profile + .. automethod:: openstack.accelerator.v2._proxy.Proxy.delete_device_profile + .. automethod:: openstack.accelerator.v2._proxy.Proxy.get_device_profile + .. automethod:: openstack.accelerator.v2._proxy.Proxy.device_profiles + +Accelerator Request Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.accelerator.v2._proxy.Proxy + + .. automethod:: openstack.accelerator.v2._proxy.Proxy.create_accelerator_request + .. automethod:: openstack.accelerator.v2._proxy.Proxy.delete_accelerator_request + .. automethod:: openstack.accelerator.v2._proxy.Proxy.get_accelerator_request + .. automethod:: openstack.accelerator.v2._proxy.Proxy.accelerator_requests + .. automethod:: openstack.accelerator.v2._proxy.Proxy.update_accelerator_request + diff --git a/doc/source/user/resources/accelerator/index.rst b/doc/source/user/resources/accelerator/index.rst new file mode 100644 index 000000000..5e09cf616 --- /dev/null +++ b/doc/source/user/resources/accelerator/index.rst @@ -0,0 +1,11 @@ +Accelerator v2 Resources +======================== + +.. toctree:: + :maxdepth: 1 + + v2/device + v2/deployable + v2/device_profile + v2/accelerator_request + diff --git a/doc/source/user/resources/accelerator/v2/accelerator_request.rst b/doc/source/user/resources/accelerator/v2/accelerator_request.rst new file mode 100644 index 000000000..172511407 --- /dev/null +++ b/doc/source/user/resources/accelerator/v2/accelerator_request.rst @@ -0,0 +1,13 @@ +openstack.accelerator.v2.accelerator_request +============================================ + +.. automodule:: openstack.accelerator.v2.accelerator_request + +The AcceleratorRequest Class +---------------------------- + +The ``AcceleratorRequest`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.accelerator.v2.accelerator_request.AcceleratorRequest + :members: diff --git a/doc/source/user/resources/accelerator/v2/deployable.rst b/doc/source/user/resources/accelerator/v2/deployable.rst new file mode 100644 index 000000000..383795c2a --- /dev/null +++ b/doc/source/user/resources/accelerator/v2/deployable.rst @@ -0,0 +1,13 @@ +openstack.accelerator.v2.deployable +============================================ + +.. automodule:: openstack.accelerator.v2.deployable + +The Deployable Class +-------------------- + +The ``Deployable`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.accelerator.v2.deployable.Deployable + :members: + diff --git a/doc/source/user/resources/accelerator/v2/device.rst b/doc/source/user/resources/accelerator/v2/device.rst new file mode 100644 index 000000000..943743546 --- /dev/null +++ b/doc/source/user/resources/accelerator/v2/device.rst @@ -0,0 +1,13 @@ +openstack.accelerator.v2.device +============================================ + +.. automodule:: openstack.accelerator.v2.device + +The Device Class +-------------------- + +The ``Device`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.accelerator.v2.device.Device + :members: + diff --git a/doc/source/user/resources/accelerator/v2/device_profile.rst b/doc/source/user/resources/accelerator/v2/device_profile.rst new file mode 100644 index 000000000..9849c7833 --- /dev/null +++ b/doc/source/user/resources/accelerator/v2/device_profile.rst @@ -0,0 +1,14 @@ +openstack.accelerator.v2.device_profile +============================================ + +.. automodule:: openstack.accelerator.v2.device_profile + +The DeviceProfile Class +----------------------- + +The ``DeviceProfile`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.accelerator.v2.device_profile.DeviceProfile + :members: + diff --git a/openstack/_services_mixin.py b/openstack/_services_mixin.py index 5f8fbedf6..eb58e83b8 100644 --- a/openstack/_services_mixin.py +++ b/openstack/_services_mixin.py @@ -1,5 +1,6 @@ # Generated file, to change, run tools/print-services.py from openstack import service_description +from openstack.accelerator import accelerator_service from openstack.baremetal import baremetal_service from openstack.baremetal_introspection import baremetal_introspection_service from openstack.block_storage import block_storage_service @@ -124,7 +125,7 @@ class ServicesMixin(object): function_engine = service_description.ServiceDescription(service_type='function-engine') - accelerator = service_description.ServiceDescription(service_type='accelerator') + accelerator = accelerator_service.AcceleratorService(service_type='accelerator') admin_logic = service_description.ServiceDescription(service_type='admin-logic') registration = admin_logic diff --git a/openstack/accelerator/__init__.py b/openstack/accelerator/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/accelerator/accelerator_service.py b/openstack/accelerator/accelerator_service.py new file mode 100644 index 000000000..9d7f1784a --- /dev/null +++ b/openstack/accelerator/accelerator_service.py @@ -0,0 +1,21 @@ +# 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 service_description +from openstack.accelerator.v2 import _proxy as _proxy_v2 + + +class AcceleratorService(service_description.ServiceDescription): + """The accelerator service.""" + supported_versions = { + '2': _proxy_v2.Proxy, + } diff --git a/openstack/accelerator/v2/__init__.py b/openstack/accelerator/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/accelerator/v2/_proxy.py b/openstack/accelerator/v2/_proxy.py new file mode 100644 index 000000000..8be5ac754 --- /dev/null +++ b/openstack/accelerator/v2/_proxy.py @@ -0,0 +1,173 @@ +# 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 proxy +from openstack.accelerator.v2 import deployable as _deployable +from openstack.accelerator.v2 import device as _device +from openstack.accelerator.v2 import device_profile as _device_profile +from openstack.accelerator.v2 import accelerator_request as _arq + + +class Proxy(proxy.Proxy): + + def deployables(self, **query): + """Retrieve a generator of deployables. + + :param kwargs query: Optional query parameters to be sent to + restrict the deployables to be returned. + :returns: A generator of deployable instances. + """ + return self._list(_deployable.Deployable, **query) + + def get_deployable(self, uuid, fields=None): + """Get a single deployable. + + :param uuid: The value can be the UUID of a deployable. + :returns: One :class:`~openstack.accelerator.v2.deployable.Deployable` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + deployable matching the criteria could be found. + """ + return self._get(_deployable.Deployable, uuid) + + def update_deployable(self, uuid, patch): + """Reconfig the FPGA with new bitstream. + + :param uuid: The value can be the UUID of a deployable + :param patch: The infomation of to reconfig. + :returns: The results of FPGA reconfig. + """ + return self._get_resource(_deployable.Deployable, + uuid).patch(self, patch) + + def devices(self, **query): + """Retrieve a generator of devices. + + :param kwargs query: Optional query parameters to be sent to + restrict the devices to be returned. Available parameters include: + * hostname: The hostname of the device. + * type: The type of the device. + * vendor: The vendor ID of the device. + * sort: A list of sorting keys separated by commas. Each sorting + key can optionally be attached with a sorting direction + modifier which can be ``asc`` or ``desc``. + * limit: Requests a specified size of returned items from the + query. Returns a number of items up to the specified limit + value. + * marker: Specifies the ID of the last-seen item. Use the limit + parameter to make an initial limited request and use the ID of + the last-seen item from the response as the marker parameter + value in a subsequent limited request. + :returns: A generator of device instances. + """ + return self._list(_device.Device, **query) + + def get_device(self, uuid, fields=None): + """Get a single device. + + :param uuid: The value can be the UUID of a device. + :returns: One :class:`~openstack.accelerator.v2.device.Device` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + deployable matching the criteria could be found. + """ + return self._get(_device.Device, uuid) + + def device_profiles(self, **query): + """Retrieve a generator of device profiles. + + :param kwargs query: Optional query parameters to be sent to + restrict the device profiles to be returned. + :returns: A generator of device profile instances. + """ + return self._list(_device_profile.DeviceProfile, **query) + + def create_device_profile(self, **attrs): + """Create a device_profiles. + + :param kwargs attrs: a list of device_profiles. + :returns: The list of created device profiles + """ + return self._create(_device_profile.DeviceProfile, **attrs) + + def delete_device_profile(self, name_or_id, ignore_missing=True): + """Delete an device profile + + :param name_or_id: The value can be either the ID of + an device profile. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the device profile does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent device profile. + :returns: ``None`` + """ + return self._delete(_device_profile.DeviceProfile, + name_or_id, ignore_missing=ignore_missing) + + def get_device_profile(self, uuid, fields=None): + """Get a single device profile. + + :param uuid: The value can be the UUID of a device profile. + :returns: One :class: + `~openstack.accelerator.v2.device_profile.DeviceProfile` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + device profile matching the criteria could be found. + """ + return self._get(_device_profile.DeviceProfile, uuid) + + def accelerator_requests(self, **query): + """Retrieve a generator of accelerator requests. + + :param kwargs query: Optional query parameters to be sent to + restrict the accelerator requests to be returned. + :returns: A generator of accelerator request instances. + """ + return self._list(_arq.AcceleratorRequest, **query) + + def create_accelerator_request(self, **attrs): + """Create an ARQs for a single device profile. + + :param kwargs attrs: request body. + """ + return self._create(_arq.AcceleratorRequest, **attrs) + + def delete_accelerator_request(self, name_or_id, ignore_missing=True): + """Delete an device profile + :param name_or_id: The value can be either the ID of + an accelerator request. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the device profile does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent accelerator request. + :returns: ``None`` + """ + return self._delete(_arq.AcceleratorRequest, name_or_id, + ignore_missing=ignore_missing) + + def get_accelerator_request(self, uuid, fields=None): + """Get a single accelerator request. + :param uuid: The value can be the UUID of a accelerator request. + :returns: One :class: + `~openstack.accelerator.v2.accelerator_request.AcceleratorRequest` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + accelerator request matching the criteria could be found. + """ + return self._get(_arq.AcceleratorRequest, uuid) + + def update_accelerator_request(self, uuid, properties): + """Bind/Unbind an accelerator to VM. + :param uuid: The uuid of the accelerator_request to be binded/unbinded. + :param properties: The info of VM + that will bind/unbind the accelerator. + :returns: True if bind/unbind succeeded, False otherwise. + """ + return self._get_resource(_arq.AcceleratorRequest, + uuid).patch(self, properties) diff --git a/openstack/accelerator/v2/accelerator_request.py b/openstack/accelerator/v2/accelerator_request.py new file mode 100644 index 000000000..b45bed453 --- /dev/null +++ b/openstack/accelerator/v2/accelerator_request.py @@ -0,0 +1,98 @@ +# 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 resource +from openstack import exceptions + + +class AcceleratorRequest(resource.Resource): + resource_key = 'arq' + resources_key = 'arqs' + base_path = '/accelerator_requests' + + # capabilities + allow_create = True + allow_fetch = True + allow_delete = True + allow_list = True + #: Allow patch operation for binding. + allow_patch = True + + #: The device address associated with this ARQ (if any) + attach_handle_info = resource.Body('attach_handle_info') + #: The type of attach handle (e.g. PCI, mdev...) + attach_handle_type = resource.Body('attach_handle_type') + #: The name of the device profile + device_profile_name = resource.Body('device_profile_name') + #: The id of the device profile group + device_profile_group_id = resource.Body('device_profile_group_id') + #: The UUID of the bound device RP (if any) + device_rp_uuid = resource.Body('device_rp_uuid') + #: The host name to which ARQ is bound. (if any) + hostname = resource.Body('hostname') + #: The UUID of the instance associated with this ARQ (if any) + instance_uuid = resource.Body('instance_uuid') + #: The state of the ARQ + state = resource.Body('state') + #: The UUID of the ARQ + uuid = resource.Body('uuid', alternate_id=True) + + def _convert_patch(self, patch): + # This overrides the default behavior of _convert_patch because + # the PATCH method consumes JSON, its key is the ARQ uuid + # and its value is an ordinary JSON patch. spec: + # https://specs.openstack.org/openstack/cyborg-specs/specs/train/approved/cyborg-api + + converted = super(AcceleratorRequest, self)._convert_patch(patch) + converted = {self.id: converted} + return converted + + def patch(self, session, patch=None, prepend_key=True, has_body=True, + retry_on_conflict=None, base_path=None): + # This overrides the default behavior of patch because + # the PATCH method consumes a dict rather than a list. spec: + # https://specs.openstack.org/openstack/cyborg-specs/specs/train/approved/cyborg-api + + # The id cannot be dirty for an commit + self._body._dirty.discard("id") + + # Only try to update if we actually have anything to commit. + if not patch and not self.requires_commit: + return self + + if not self.allow_patch: + raise exceptions.MethodNotSupported(self, "patch") + + request = self._prepare_request(prepend_key=prepend_key, + base_path=base_path, patch=True) + microversion = self._get_microversion_for(session, 'patch') + if patch: + request.body = self._convert_patch(patch) + + return self._commit(session, request, 'PATCH', microversion, + has_body=has_body, + retry_on_conflict=retry_on_conflict) + + def _consume_attrs(self, mapping, attrs): + # This overrides the default behavior of _consume_attrs because + # cyborg api returns an ARQ as list. spec: + # https://specs.openstack.org/openstack/cyborg-specs/specs/train/approved/cyborg-api + if isinstance(self, AcceleratorRequest): + if self.resources_key in attrs: + attrs = attrs[self.resources_key][0] + return super(AcceleratorRequest, self)._consume_attrs(mapping, attrs) + + def create(self, session, base_path=None): + # This overrides the default behavior of resource creation because + # cyborg doesn't accept resource_key in its request. + return super(AcceleratorRequest, self).create( + session, prepend_key=False, base_path=base_path) diff --git a/openstack/accelerator/v2/deployable.py b/openstack/accelerator/v2/deployable.py new file mode 100644 index 000000000..ebfee3197 --- /dev/null +++ b/openstack/accelerator/v2/deployable.py @@ -0,0 +1,66 @@ +# 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 import resource + + +class Deployable(resource.Resource): + resource_key = 'deployable' + resources_key = 'deployables' + base_path = '/deployables' + # capabilities + allow_create = False + allow_fetch = True + allow_commit = False + allow_delete = False + allow_list = True + allow_patch = True + #: The timestamp when this deployable was created. + created_at = resource.Body('created_at') + #: The device_id of the deployable. + device_id = resource.Body('device_id') + #: The UUID of the deployable. + id = resource.Body('uuid', alternate_id=True) + #: The name of the deployable. + name = resource.Body('name') + #: The num_accelerator of the deployable. + num_accelerators = resource.Body('num_accelerators') + #: The parent_id of the deployable. + parent_id = resource.Body('parent_id') + #: The root_id of the deployable. + root_id = resource.Body('root_id') + #: The timestamp when this deployable was updated. + updated_at = resource.Body('updated_at') + + def _commit(self, session, request, method, microversion, has_body=True, + retry_on_conflict=None): + session = self._get_session(session) + kwargs = {} + retriable_status_codes = set(session.retriable_status_codes or ()) + if retry_on_conflict: + kwargs['retriable_status_codes'] = retriable_status_codes | {409} + elif retry_on_conflict is not None and retriable_status_codes: + # The baremetal proxy defaults to retrying on conflict, allow + # overriding it via an explicit retry_on_conflict=False. + kwargs['retriable_status_codes'] = retriable_status_codes - {409} + try: + call = getattr(session, method.lower()) + except AttributeError: + raise exceptions.ResourceFailure( + msg="Invalid commit method: %s" % method) + request.url = request.url + "/program" + response = call(request.url, json=request.body, + headers=request.headers, microversion=microversion, + **kwargs) + self.microversion = microversion + self._translate_response(response, has_body=has_body) + return self diff --git a/openstack/accelerator/v2/device.py b/openstack/accelerator/v2/device.py new file mode 100644 index 000000000..73799669d --- /dev/null +++ b/openstack/accelerator/v2/device.py @@ -0,0 +1,44 @@ +# 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 resource + + +class Device(resource.Resource): + resource_key = 'device' + resources_key = 'devices' + base_path = '/devices' + # capabilities + allow_create = False + allow_fetch = True + allow_commit = False + allow_delete = False + allow_list = True + #: The timestamp when this device was created. + created_at = resource.Body('created_at') + #: The hostname of the device. + hostname = resource.Body('hostname') + #: The ID of the device. + id = resource.Body('id') + #: The model of the device. + model = resource.Body('model') + #: The std board information of the device. + std_board_info = resource.Body('std_board_info') + #: The type of the device. + type = resource.Body('type') + #: The timestamp when this device was updated. + updated_at = resource.Body('updated_at') + #: The UUID of the device. + uuid = resource.Body('uuid', alternate_id=True) + #: The vendor ID of the device. + vendor = resource.Body('vendor') + #: The vendor board information of the device. + vendor_board_info = resource.Body('vendor_board_info') diff --git a/openstack/accelerator/v2/device_profile.py b/openstack/accelerator/v2/device_profile.py new file mode 100644 index 000000000..a09f48167 --- /dev/null +++ b/openstack/accelerator/v2/device_profile.py @@ -0,0 +1,48 @@ +# 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 resource + + +class DeviceProfile(resource.Resource): + resource_key = 'device_profile' + resources_key = 'device_profiles' + base_path = '/device_profiles' + # capabilities + allow_create = True + allow_fetch = True + allow_commit = False + allow_delete = True + allow_list = True + + #: The timestamp when this device_profile was created. + created_at = resource.Body('created_at') + #: The groups of the device profile + groups = resource.Body('groups') + #: The name of the device profile + name = resource.Body('name') + #: The timestamp when this device_profile was updated. + updated_at = resource.Body('updated_at') + #: The uuid of the device profile + uuid = resource.Body('uuid', alternate_id=True) + + # TODO(s_shogo): This implementation only treat [ DeviceProfile ], and + # cannot treat multiple DeviceProfiles in list. + def _prepare_request_body(self, patch, prepend_key): + body = super(DeviceProfile, self)._prepare_request_body( + patch, prepend_key) + return [body] + + def create(self, session, base_path=None): + # This overrides the default behavior of resource creation because + # cyborg doesn't accept resource_key in its request. + return super(DeviceProfile, self).create( + session, prepend_key=False, base_path=base_path) diff --git a/openstack/accelerator/version.py b/openstack/accelerator/version.py new file mode 100644 index 000000000..692230a19 --- /dev/null +++ b/openstack/accelerator/version.py @@ -0,0 +1,27 @@ +# 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 resource + + +class Version(resource.Resource): + resource_key = 'version' + resources_key = 'versions' + base_path = '/' + + # capabilities + allow_list = True + + # Properties + links = resource.Body('links') + status = resource.Body('status') diff --git a/openstack/cloud/_accelerator.py b/openstack/cloud/_accelerator.py new file mode 100644 index 000000000..b28ac1f6b --- /dev/null +++ b/openstack/cloud/_accelerator.py @@ -0,0 +1,154 @@ +# 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 types so that we can reference ListType in sphinx param declarations. +# We can't just use list, because sphinx gets confused by +# openstack.resource.Resource.list and openstack.resource2.Resource.list + +from openstack.cloud import _normalize + + +class AcceleratorCloudMixin(_normalize.Normalizer): + + def list_deployables(self, filters=None): + """List all available deployables. + :param filters: (optional) dict of filter conditions to push down + :returns: A list of deployable info. + """ + + # Translate None from search interface to empty {} for kwargs below + if not filters: + filters = {} + return list(self.accelerator.deployables(**filters)) + + def list_devices(self, filters=None): + """List all devices. + :param filters: (optional) dict of filter conditions to push down + :returns: A list of device info. + """ + + # Translate None from search interface to empty {} for kwargs below + if not filters: + filters = {} + return list(self.accelerator.devices(**filters)) + + def list_device_profiles(self, filters=None): + """List all device_profiles. + :param filters: (optional) dict of filter conditions to push down + :returns: A list of device profile info. + """ + + # Translate None from search interface to empty {} for kwargs below + if not filters: + filters = {} + return list(self.accelerator.device_profiles(**filters)) + + def create_device_profile(self, attrs): + """Create a device_profile. + :param attrs: The info of device_profile to be created. + :returns: A ``munch.Munch`` of the created device_profile. + """ + + return self.accelerator.create_device_profile(**attrs) + + def delete_device_profile(self, name_or_id, filters): + """Delete a device_profile. + :param name_or_id: The Name(or uuid) of device_profile to be deleted. + :returns: True if delete succeeded, False otherwise. + """ + + device_profile = self.accelerator.get_device_profile( + name_or_id, + filters + ) + if device_profile is None: + self.log.debug( + "device_profile %s not found for deleting", + name_or_id + ) + return False + + self.accelerator.delete_device_profile(name_or_id=name_or_id) + + return True + + def list_accelerator_requests(self, filters=None): + """List all accelerator_requests. + :param filters: (optional) dict of filter conditions to push down + :returns: A list of accelerator request info. + """ + + # Translate None from search interface to empty {} for kwargs below + if not filters: + filters = {} + return list(self.accelerator.accelerator_requests(**filters)) + + def delete_accelerator_request(self, name_or_id, filters): + """Delete a accelerator_request. + :param name_or_id: The Name(or uuid) of accelerator_request. + :returns: True if delete succeeded, False otherwise. + """ + + accelerator_request = self.accelerator.get_accelerator_request( + name_or_id, + filters + ) + if accelerator_request is None: + self.log.debug( + "accelerator_request %s not found for deleting", + name_or_id + ) + return False + + self.accelerator.delete_accelerator_request(name_or_id=name_or_id) + + return True + + def create_accelerator_request(self, attrs): + """Create an accelerator_request. + :param attrs: The info of accelerator_request to be created. + :returns: A ``munch.Munch`` of the created accelerator_request. + """ + + return self.accelerator.create_accelerator_request(**attrs) + + def bind_accelerator_request(self, uuid, properties): + """Bind an accelerator to VM. + :param uuid: The uuid of the accelerator_request to be binded. + :param properties: The info of VM that will bind the accelerator. + :returns: True if bind succeeded, False otherwise. + """ + + accelerator_request = self.accelerator.get_accelerator_request(uuid) + if accelerator_request is None: + self.log.debug( + "accelerator_request %s not found for unbinding", uuid + ) + return False + + return self.accelerator.update_accelerator_request(uuid, properties) + + def unbind_accelerator_request(self, uuid, properties): + """Unbind an accelerator from VM. + :param uuid: The uuid of the accelerator_request to be unbinded. + :param properties: The info of VM that will unbind the accelerator. + :returns:True if unbind succeeded, False otherwise. + """ + + accelerator_request = self.accelerator.get_accelerator_request(uuid) + if accelerator_request is None: + self.log.debug( + "accelerator_request %s not found for unbinding", uuid + ) + return False + + return self.accelerator.update_accelerator_request(uuid, properties) diff --git a/openstack/connection.py b/openstack/connection.py index d77a81f9a..1fae88e40 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -186,6 +186,7 @@ from openstack import _log from openstack import _services_mixin from openstack.cloud import openstackcloud as _cloud +from openstack.cloud import _accelerator from openstack.cloud import _baremetal from openstack.cloud import _block_storage from openstack.cloud import _compute @@ -249,6 +250,7 @@ def from_config(cloud=None, config=None, options=None, **kwargs): class Connection( _services_mixin.ServicesMixin, _cloud._OpenStackCloudMixin, + _accelerator.AcceleratorCloudMixin, _baremetal.BaremetalCloudMixin, _block_storage.BlockStorageCloudMixin, _compute.ComputeCloudMixin, @@ -385,6 +387,7 @@ def __init__(self, cloud=None, config=None, session=None, # Call the _*CloudMixin constructors while we work on # integrating things better. _cloud._OpenStackCloudMixin.__init__(self) + _accelerator.AcceleratorCloudMixin.__init__(self) _baremetal.BaremetalCloudMixin.__init__(self) _block_storage.BlockStorageCloudMixin.__init__(self) _clustering.ClusteringCloudMixin.__init__(self) diff --git a/openstack/tests/unit/accelerator/__init__.py b/openstack/tests/unit/accelerator/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/unit/accelerator/test_version.py b/openstack/tests/unit/accelerator/test_version.py new file mode 100644 index 000000000..43d6378e7 --- /dev/null +++ b/openstack/tests/unit/accelerator/test_version.py @@ -0,0 +1,42 @@ +# 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.tests.unit import base + +from openstack.accelerator import version + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'id': IDENTIFIER, + 'links': '2', + 'status': '3', +} + + +class TestVersion(base.TestCase): + + def test_basic(self): + sot = version.Version() + self.assertEqual('version', sot.resource_key) + self.assertEqual('versions', sot.resources_key) + self.assertEqual('/', sot.base_path) + self.assertFalse(sot.allow_create) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = version.Version(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['links'], sot.links) + self.assertEqual(EXAMPLE['status'], sot.status) diff --git a/openstack/tests/unit/accelerator/v2/__init__.py b/openstack/tests/unit/accelerator/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/unit/accelerator/v2/test_accelerator_request.py b/openstack/tests/unit/accelerator/v2/test_accelerator_request.py new file mode 100644 index 000000000..8b36a717f --- /dev/null +++ b/openstack/tests/unit/accelerator/v2/test_accelerator_request.py @@ -0,0 +1,59 @@ +# 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.tests.unit import base + +from openstack.accelerator.v2 import accelerator_request as arq + +FAKE_ID = '0725b527-e51a-41df-ad22-adad5f4546ad' +FAKE_RP_UUID = 'f4b7fe6c-8ab4-4914-a113-547af022935b' +FAKE_INSTANCE_UUID = '1ce4a597-9836-4e02-bea1-a3a6cbe7b9f9' +FAKE_ATTACH_INFO_STR = '{"bus": "5e", '\ + '"device": "00", '\ + '"domain": "0000", '\ + '"function": "1"}' + +FAKE = { + 'uuid': FAKE_ID, + 'device_profile_name': 'fake-devprof', + 'device_profile_group_id': 0, + 'device_rp_uuid': FAKE_RP_UUID, + 'instance_uuid': FAKE_INSTANCE_UUID, + 'attach_handle_type': 'PCI', + 'attach_handle_info': FAKE_ATTACH_INFO_STR, +} + + +class TestAcceleratorRequest(base.TestCase): + + def test_basic(self): + sot = arq.AcceleratorRequest() + self.assertEqual('arq', sot.resource_key) + self.assertEqual('arqs', sot.resources_key) + self.assertEqual('/accelerator_requests', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_patch) + + def test_make_it(self): + sot = arq.AcceleratorRequest(**FAKE) + self.assertEqual(FAKE_ID, sot.uuid) + self.assertEqual(FAKE['device_profile_name'], sot.device_profile_name) + self.assertEqual(FAKE['device_profile_group_id'], + sot.device_profile_group_id) + self.assertEqual(FAKE_RP_UUID, sot.device_rp_uuid) + self.assertEqual(FAKE_INSTANCE_UUID, sot.instance_uuid) + self.assertEqual(FAKE['attach_handle_type'], sot.attach_handle_type) + self.assertEqual(FAKE_ATTACH_INFO_STR, sot.attach_handle_info) diff --git a/openstack/tests/unit/accelerator/v2/test_deployable.py b/openstack/tests/unit/accelerator/v2/test_deployable.py new file mode 100644 index 000000000..dcb0af554 --- /dev/null +++ b/openstack/tests/unit/accelerator/v2/test_deployable.py @@ -0,0 +1,52 @@ +# 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 uuid + +from openstack.tests.unit import base + +from openstack.accelerator.v2 import deployable + +EXAMPLE = { + 'uuid': uuid.uuid4(), + 'created_at': '2019-08-09T12:14:57.233772', + 'updated_at': '2019-08-09T12:15:57.233772', + 'parent_id': '1', + 'root_id': '1', + 'name': 'test_name', + 'num_accelerators': '1', + 'device_id': '1', +} + + +class TestDeployable(base.TestCase): + + def test_basic(self): + sot = deployable.Deployable() + self.assertEqual('deployable', sot.resource_key) + self.assertEqual('deployables', sot.resources_key) + self.assertEqual('/deployables', sot.base_path) + self.assertFalse(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = deployable.Deployable(**EXAMPLE) + self.assertEqual(EXAMPLE['uuid'], sot.id) + self.assertEqual(EXAMPLE['parent_id'], sot.parent_id) + self.assertEqual(EXAMPLE['root_id'], sot.root_id) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['num_accelerators'], sot.num_accelerators) + self.assertEqual(EXAMPLE['device_id'], sot.device_id) + self.assertEqual(EXAMPLE['created_at'], sot.created_at) + self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) diff --git a/openstack/tests/unit/accelerator/v2/test_device.py b/openstack/tests/unit/accelerator/v2/test_device.py new file mode 100644 index 000000000..22b17b336 --- /dev/null +++ b/openstack/tests/unit/accelerator/v2/test_device.py @@ -0,0 +1,53 @@ +# 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 uuid + +from openstack.tests.unit import base +from openstack.accelerator.v2 import device + +EXAMPLE = { + 'id': '1', + 'uuid': uuid.uuid4(), + 'created_at': '2019-08-09T12:14:57.233772', + 'updated_at': '2019-08-09T12:15:57.233772', + 'type': 'test_type', + 'vendor': '0x8086', + 'model': 'test_model', + 'std_board_info': '{"product_id": "0x09c4"}', + 'vendor_board_info': 'test_vb_info', +} + + +class TestDevice(base.TestCase): + + def test_basic(self): + sot = device.Device() + self.assertEqual('device', sot.resource_key) + self.assertEqual('devices', sot.resources_key) + self.assertEqual('/devices', sot.base_path) + self.assertFalse(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = device.Device(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['uuid'], sot.uuid) + self.assertEqual(EXAMPLE['type'], sot.type) + self.assertEqual(EXAMPLE['vendor'], sot.vendor) + self.assertEqual(EXAMPLE['model'], sot.model) + self.assertEqual(EXAMPLE['std_board_info'], sot.std_board_info) + self.assertEqual(EXAMPLE['vendor_board_info'], sot.vendor_board_info) + self.assertEqual(EXAMPLE['created_at'], sot.created_at) + self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) diff --git a/openstack/tests/unit/accelerator/v2/test_device_profile.py b/openstack/tests/unit/accelerator/v2/test_device_profile.py new file mode 100644 index 000000000..f1708713b --- /dev/null +++ b/openstack/tests/unit/accelerator/v2/test_device_profile.py @@ -0,0 +1,54 @@ +# 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.tests.unit import base + +from openstack.accelerator.v2 import device_profile + + +FAKE = { + "id": 1, + "uuid": u"a95e10ae-b3e3-4eab-a513-1afae6f17c51", + "name": u'afaas_example_1', + "groups": [ + {"resources:ACCELERATOR_FPGA": "1", + "trait:CUSTOM_FPGA_INTEL_PAC_ARRIA10": "required", + "trait:CUSTOM_FUNCTION_ID_3AFB": "required", + }, + {"resources:CUSTOM_ACCELERATOR_FOO": "2", + "resources:CUSTOM_MEMORY": "200", + "trait:CUSTOM_TRAIT_ALWAYS": "required", + } + ] +} + + +class TestDeviceProfile(base.TestCase): + + def test_basic(self): + sot = device_profile.DeviceProfile() + self.assertEqual('device_profile', sot.resource_key) + self.assertEqual('device_profiles', sot.resources_key) + self.assertEqual('/device_profiles', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertFalse(sot.allow_patch) + + def test_make_it(self): + sot = device_profile.DeviceProfile(**FAKE) + self.assertEqual(FAKE['id'], sot.id) + self.assertEqual(FAKE['uuid'], sot.uuid) + self.assertEqual(FAKE['name'], sot.name) + self.assertEqual(FAKE['groups'], sot.groups) diff --git a/openstack/tests/unit/accelerator/v2/test_proxy.py b/openstack/tests/unit/accelerator/v2/test_proxy.py new file mode 100644 index 000000000..b9fd45867 --- /dev/null +++ b/openstack/tests/unit/accelerator/v2/test_proxy.py @@ -0,0 +1,66 @@ +# 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.accelerator.v2 import _proxy +from openstack.accelerator.v2 import deployable +from openstack.accelerator.v2 import device_profile +from openstack.accelerator.v2 import accelerator_request +from openstack.tests.unit import test_proxy_base as test_proxy_base + + +class TestAcceleratorProxy(test_proxy_base.TestProxyBase): + def setUp(self): + super(TestAcceleratorProxy, self).setUp() + self.proxy = _proxy.Proxy(self.session) + + def test_list_deployables(self): + self.verify_list(self.proxy.deployables, deployable.Deployable) + + def test_list_device_profile(self): + self.verify_list(self.proxy.device_profiles, + device_profile.DeviceProfile) + + def test_create_device_profile(self): + self.verify_create(self.proxy.create_device_profile, + device_profile.DeviceProfile) + + def test_delete_device_profile(self): + self.verify_delete(self.proxy.delete_device_profile, + device_profile.DeviceProfile, False) + + def test_delete_device_profile_ignore(self): + self.verify_delete(self.proxy.delete_device_profile, + device_profile.DeviceProfile, True) + + def test_get_device_profile(self): + self.verify_get(self.proxy.get_device_profile, + device_profile.DeviceProfile) + + def test_list_accelerator_request(self): + self.verify_list(self.proxy.accelerator_requests, + accelerator_request.AcceleratorRequest) + + def test_create_accelerator_request(self): + self.verify_create(self.proxy.create_accelerator_request, + accelerator_request.AcceleratorRequest) + + def test_delete_accelerator_request(self): + self.verify_delete(self.proxy.delete_accelerator_request, + accelerator_request.AcceleratorRequest, False) + + def test_delete_accelerator_request_ignore(self): + self.verify_delete(self.proxy.delete_accelerator_request, + accelerator_request.AcceleratorRequest, True) + + def test_get_accelerator_request(self): + self.verify_get(self.proxy.get_accelerator_request, + accelerator_request.AcceleratorRequest) diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 9bb7a4d03..d39c265a9 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -564,6 +564,12 @@ def use_compute_discovery( compute_version_json, compute_discovery_url), ]) + def get_cyborg_discovery_mock_dict(self): + discovery_fixture = os.path.join( + self.fixtures_directory, "accelerator.json") + return dict(method='GET', uri="https://accelerator.example.com/", + text=open(discovery_fixture, 'r').read()) + def use_glance( self, image_version_json='image-version.json', image_discovery_url='https://image.example.com/'): @@ -611,6 +617,15 @@ def use_senlin(self): self.__do_register_uris([ self.get_senlin_discovery_mock_dict()]) + def use_cyborg(self): + # NOTE(s_shogo): This method is only meant to be used in "setUp" + # where the ordering of the url being registered is tightly controlled + # if the functionality of .use_cyborg is meant to be used during an + # actual test case, use .get_cyborg_discovery_mock and apply to the + # right location in the mock_uris when calling .register_uris + self.__do_register_uris([ + self.get_cyborg_discovery_mock_dict()]) + def register_uris(self, uri_mock_list=None): """Mock a list of URIs and responses via requests mock. diff --git a/openstack/tests/unit/cloud/test_accelerator.py b/openstack/tests/unit/cloud/test_accelerator.py new file mode 100644 index 000000000..5b09a56e8 --- /dev/null +++ b/openstack/tests/unit/cloud/test_accelerator.py @@ -0,0 +1,328 @@ +# 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.tests.unit import base +import copy +import uuid + +DEP_UUID = uuid.uuid4().hex +DEP_DICT = { + 'uuid': DEP_UUID, + 'name': 'dep_name', + 'parent_id': None, + 'root_id': 1, + 'num_accelerators': 4, + 'device_id': 0 +} + +DEV_UUID = uuid.uuid4().hex +DEV_DICT = { + 'id': 1, + 'uuid': DEV_UUID, + 'name': 'dev_name', + 'type': 'test_type', + 'vendor': '0x8086', + 'model': 'test_model', + 'std_board_info': '{"product_id": "0x09c4"}', + 'vendor_board_info': 'test_vb_info', +} + +DEV_PROF_UUID = uuid.uuid4().hex +DEV_PROF_GROUPS = [ + {"resources:ACCELERATOR_FPGA": "1", + "trait:CUSTOM_FPGA_INTEL_PAC_ARRIA10": "required", + "trait:CUSTOM_FUNCTION_ID_3AFB": "required", + }, + {"resources:CUSTOM_ACCELERATOR_FOO": "2", + "resources:CUSTOM_MEMORY": "200", + "trait:CUSTOM_TRAIT_ALWAYS": "required", + } +] +DEV_PROF_DICT = { + "id": 1, + "uuid": DEV_PROF_UUID, + "name": 'afaas_example_1', + "groups": DEV_PROF_GROUPS, +} + +NEW_DEV_PROF_DICT = copy.copy(DEV_PROF_DICT) + +ARQ_UUID = uuid.uuid4().hex +ARQ_DEV_RP_UUID = uuid.uuid4().hex +ARQ_INSTANCE_UUID = uuid.uuid4().hex +ARQ_ATTACH_INFO_STR = '{"bus": "5e", '\ + '"device": "00", '\ + '"domain": "0000", '\ + '"function": "1"}' +ARQ_DICT = { + 'uuid': ARQ_UUID, + 'hostname': 'test_hostname', + 'device_profile_name': 'fake-devprof', + 'device_profile_group_id': 0, + 'device_rp_uuid': ARQ_DEV_RP_UUID, + 'instance_uuid': ARQ_INSTANCE_UUID, + 'attach_handle_type': 'PCI', + 'attach_handle_info': ARQ_ATTACH_INFO_STR, +} + +NEW_ARQ_DICT = copy.copy(ARQ_DICT) + + +class TestAccelerator(base.TestCase): + def setUp(self): + super(TestAccelerator, self).setUp() + self.use_cyborg() + + def test_list_deployables(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'accelerator', + 'public', + append=['v2', 'deployables']), + json={'deployables': [DEP_DICT]} + ), + ]) + dep_list = self.cloud.list_deployables() + self.assertEqual(len(dep_list), 1) + self.assertEqual(dep_list[0].id, DEP_DICT['uuid']) + self.assertEqual(dep_list[0].name, DEP_DICT['name']) + self.assertEqual(dep_list[0].parent_id, DEP_DICT['parent_id']) + self.assertEqual(dep_list[0].root_id, DEP_DICT['root_id']) + self.assertEqual(dep_list[0].num_accelerators, + DEP_DICT['num_accelerators']) + self.assertEqual(dep_list[0].device_id, DEP_DICT['device_id']) + self.assert_calls() + + def test_list_devices(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'accelerator', + 'public', + append=['v2', 'devices']), + json={'devices': [DEV_DICT]} + ), + ]) + dev_list = self.cloud.list_devices() + self.assertEqual(len(dev_list), 1) + self.assertEqual(dev_list[0].id, DEV_DICT['id']) + self.assertEqual(dev_list[0].uuid, DEV_DICT['uuid']) + self.assertEqual(dev_list[0].name, DEV_DICT['name']) + self.assertEqual(dev_list[0].type, DEV_DICT['type']) + self.assertEqual(dev_list[0].vendor, DEV_DICT['vendor']) + self.assertEqual(dev_list[0].model, DEV_DICT['model']) + self.assertEqual(dev_list[0].std_board_info, + DEV_DICT['std_board_info']) + self.assertEqual(dev_list[0].vendor_board_info, + DEV_DICT['vendor_board_info']) + self.assert_calls() + + def test_list_device_profiles(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'accelerator', + 'public', + append=['v2', 'device_profiles']), + json={'device_profiles': [DEV_PROF_DICT]} + ), + ]) + dev_prof_list = self.cloud.list_device_profiles() + self.assertEqual(len(dev_prof_list), 1) + self.assertEqual(dev_prof_list[0].id, DEV_PROF_DICT['id']) + self.assertEqual(dev_prof_list[0].uuid, DEV_PROF_DICT['uuid']) + self.assertEqual(dev_prof_list[0].name, DEV_PROF_DICT['name']) + self.assertEqual(dev_prof_list[0].groups, DEV_PROF_DICT['groups']) + self.assert_calls() + + def test_create_device_profile(self): + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'accelerator', + 'public', + append=['v2', 'device_profiles']), + json=NEW_DEV_PROF_DICT) + ]) + + attrs = { + 'name': NEW_DEV_PROF_DICT['name'], + 'groups': NEW_DEV_PROF_DICT['groups'] + } + + self.assertTrue( + self.cloud.create_device_profile( + attrs + ) + ) + self.assert_calls() + + def test_delete_device_profile(self, filters=None): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'accelerator', + 'public', + append=['v2', 'device_profiles', DEV_PROF_DICT['name']]), + json={"device_profiles": [DEV_PROF_DICT]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'accelerator', + 'public', + append=['v2', 'device_profiles', DEV_PROF_DICT['name']]), + json=DEV_PROF_DICT) + + ]) + self.assertTrue( + self.cloud.delete_device_profile( + DEV_PROF_DICT['name'], + filters + ) + ) + self.assert_calls() + + def test_list_accelerator_requests(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'accelerator', + 'public', + append=['v2', 'accelerator_requests']), + json={'arqs': [ARQ_DICT]} + ), + ]) + arq_list = self.cloud.list_accelerator_requests() + self.assertEqual(len(arq_list), 1) + self.assertEqual(arq_list[0].uuid, ARQ_DICT['uuid']) + self.assertEqual(arq_list[0].device_profile_name, + ARQ_DICT['device_profile_name']) + self.assertEqual(arq_list[0].device_profile_group_id, + ARQ_DICT['device_profile_group_id']) + self.assertEqual(arq_list[0].device_rp_uuid, + ARQ_DICT['device_rp_uuid']) + self.assertEqual(arq_list[0].instance_uuid, + ARQ_DICT['instance_uuid']) + self.assertEqual(arq_list[0].attach_handle_type, + ARQ_DICT['attach_handle_type']) + self.assertEqual(arq_list[0].attach_handle_info, + ARQ_DICT['attach_handle_info']) + self.assert_calls() + + def test_create_accelerator_request(self): + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'accelerator', + 'public', + append=['v2', 'accelerator_requests']), + json=NEW_ARQ_DICT + ), + ]) + + attrs = { + 'device_profile_name': NEW_ARQ_DICT['device_profile_name'], + 'device_profile_group_id': NEW_ARQ_DICT['device_profile_group_id'] + } + + self.assertTrue( + self.cloud.create_accelerator_request( + attrs + ) + ) + self.assert_calls() + + def test_delete_accelerator_request(self, filters=None): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'accelerator', + 'public', + append=['v2', 'accelerator_requests', ARQ_DICT['uuid']]), + json={"accelerator_requests": [ARQ_DICT]}), + dict(method='DELETE', + uri=self.get_mock_url( + 'accelerator', + 'public', + append=['v2', 'accelerator_requests', ARQ_DICT['uuid']]), + json=ARQ_DICT) + + ]) + self.assertTrue( + self.cloud.delete_accelerator_request( + ARQ_DICT['uuid'], + filters + ) + ) + self.assert_calls() + + def test_bind_accelerator_request(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'accelerator', + 'public', + append=['v2', 'accelerator_requests', ARQ_DICT['uuid']]), + json={"accelerator_requests": [ARQ_DICT]}), + dict(method='PATCH', + uri=self.get_mock_url( + 'accelerator', + 'public', + append=['v2', 'accelerator_requests', ARQ_DICT['uuid']]), + json=ARQ_DICT) + ]) + properties = [{'path': '/hostname', + 'value': ARQ_DICT['hostname'], + 'op': 'add'}, + {'path': '/instance_uuid', + 'value': ARQ_DICT['instance_uuid'], + 'op': 'add'}, + {'path': '/device_rp_uuid', + 'value': ARQ_DICT['device_rp_uuid'], + 'op': 'add'}] + + self.assertTrue( + self.cloud.bind_accelerator_request( + ARQ_DICT['uuid'], properties + ) + ) + self.assert_calls() + + def test_unbind_accelerator_request(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'accelerator', + 'public', + append=['v2', 'accelerator_requests', ARQ_DICT['uuid']]), + json={"accelerator_requests": [ARQ_DICT]}), + dict(method='PATCH', + uri=self.get_mock_url( + 'accelerator', + 'public', + append=['v2', 'accelerator_requests', ARQ_DICT['uuid']]), + json=ARQ_DICT) + ]) + + properties = [{'path': '/hostname', + 'op': 'remove'}, + {'path': '/instance_uuid', + 'op': 'remove'}, + {'path': '/device_rp_uuid', + 'op': 'remove'}] + + self.assertTrue( + self.cloud.unbind_accelerator_request( + ARQ_DICT['uuid'], properties + ) + ) + self.assert_calls() diff --git a/openstack/tests/unit/fixtures/accelerator.json b/openstack/tests/unit/fixtures/accelerator.json new file mode 100644 index 000000000..bf2c04691 --- /dev/null +++ b/openstack/tests/unit/fixtures/accelerator.json @@ -0,0 +1,27 @@ +{ + "versions": [ + { + "id": "2.0", + "links": [ + { + "href": "/v2/", + "rel": "self" + }, + { + "href": "https://accelerator.example.com/api-ref/accelerator", + "rel": "help" + } + ], + "max_version": "2.0", + "media-types": [ + { + "base": "application/json", + "type": "application/vnd.openstack.accelerator-v1+json" + } + ], + "min_version": "2.0", + "status": "CURRENT", + "updated": "2019-09-01T00:00:00Z" + } + ] +} diff --git a/releasenotes/notes/add-cyborg-support-b9afca69f709c048.yaml b/releasenotes/notes/add-cyborg-support-b9afca69f709c048.yaml new file mode 100644 index 000000000..9688a5e24 --- /dev/null +++ b/releasenotes/notes/add-cyborg-support-b9afca69f709c048.yaml @@ -0,0 +1,3 @@ +--- +features: + - Add support for Cyborg(accelerator) From 36cda6086ca1c5cc5515bba9001a26285ca1e15c Mon Sep 17 00:00:00 2001 From: Tobias Rydberg Date: Wed, 12 Feb 2020 06:56:50 +0000 Subject: [PATCH 2595/3836] Change of auth url and regions * New dynamic auth url for all regions * Update of available regions Change-Id: I425f59b4f025aa97fe7318a7f30b41b0c5d89ce5 --- doc/source/user/config/vendor-support.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/source/user/config/vendor-support.rst b/doc/source/user/config/vendor-support.rst index b7a7977ab..bfc01e39d 100644 --- a/doc/source/user/config/vendor-support.rst +++ b/doc/source/user/config/vendor-support.rst @@ -76,17 +76,18 @@ nz_wlg_2 Wellington, NZ City Cloud ---------- -https://identity1.citycloud.com:5000/v3/ +https://%(region_name)s.citycloud.com:5000/v3/ ============== ================ Region Name Location ============== ================ Buf1 Buffalo, NY +dx1 Dubai, UAE Fra1 Frankfurt, DE Kna1 Karlskrona, SE -La1 Los Angeles, CA Lon1 London, UK Sto2 Stockholm, SE +tky1 Tokyo, JP ============== ================ * Identity API Version is 3 From ce3646fa2708cc36bda8b73b9ae0fbadf690c903 Mon Sep 17 00:00:00 2001 From: gryf Date: Mon, 17 Feb 2020 11:46:11 +0100 Subject: [PATCH 2596/3836] Add method for bulk creating objects. There are APIs (like Neutron) which provide a way for creating multiple objects at once using single request. This change add possibility for performing such requests using Resource objects. Change-Id: I7d7c540ed1bada37ebebbe305dc0e36f38cff071 --- openstack/resource.py | 77 +++++++++++++++ openstack/tests/unit/test_resource.py | 130 ++++++++++++++++++++++++++ 2 files changed, 207 insertions(+) diff --git a/openstack/resource.py b/openstack/resource.py index 1a88d8fd5..9260e9577 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1286,6 +1286,83 @@ def create(self, session, prepend_key=True, base_path=None, **params): return self.fetch(session) return self + @classmethod + def bulk_create(cls, session, data, prepend_key=True, base_path=None, + **params): + """Create multiple remote resources based on this class and data. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param data: list of dicts, which represent resources to create. + :param prepend_key: A boolean indicating whether the resource_key + should be prepended in a resource creation + request. Default to True. + :param str base_path: Base part of the URI for creating resources, if + different from + :data:`~openstack.resource.Resource.base_path`. + :param dict params: Additional params to pass. + + :return: A generator of :class:`Resource` objects. + :raises: :exc:`~openstack.exceptions.MethodNotSupported` if + :data:`Resource.allow_create` is not set to ``True``. + """ + if not cls.allow_create: + raise exceptions.MethodNotSupported(cls, "create") + + if not (data and isinstance(data, list) + and all([isinstance(x, dict) for x in data])): + raise ValueError('Invalid data passed: %s' % data) + + session = cls._get_session(session) + microversion = cls._get_microversion_for(cls, session, 'create') + requires_id = (cls.create_requires_id + if cls.create_requires_id is not None + else cls.create_method == 'PUT') + if cls.create_method == 'PUT': + method = session.put + elif cls.create_method == 'POST': + method = session.post + else: + raise exceptions.ResourceFailure( + msg="Invalid create method: %s" % cls.create_method) + + body = [] + resources = [] + for attrs in data: + # NOTE(gryf): we need to create resource objects, since + # _prepare_request only works on instances, not classes. + # Those objects will be used in case where request doesn't return + # JSON data representing created resource, and yet it's required + # to return newly created resource objects. + resource = cls.new(connection=session._get_connection(), **attrs) + resources.append(resource) + request = resource._prepare_request(requires_id=requires_id, + base_path=base_path) + body.append(request.body) + + if prepend_key: + body = {cls.resources_key: body} + + response = method(request.url, json=body, headers=request.headers, + microversion=microversion, params=params) + exceptions.raise_from_response(response) + data = response.json() + + if cls.resources_key: + data = data[cls.resources_key] + + if not isinstance(data, list): + data = [data] + + has_body = (cls.has_body if cls.create_returns_body is None + else cls.create_returns_body) + if has_body and cls.create_returns_body is False: + return (r.fetch(session) for r in resources) + else: + return (cls.existing(microversion=microversion, + connection=session._get_connection(), + **res_dict) for res_dict in data) + def fetch(self, session, requires_id=True, base_path=None, error_message=None, **params): """Get a remote resource based on this instance. diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index ff29ba83d..1e47d89af 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -2487,6 +2487,136 @@ def test_list_multi_page_link_header(self): # Ensure we only made two calls to get this done self.assertEqual(2, len(self.session.get.call_args_list)) + def test_bulk_create_invalid_data_passed(self): + class Test(resource.Resource): + service = self.service_name + base_path = self.base_path + create_method = 'POST' + allow_create = True + + Test._prepare_request = mock.Mock() + self.assertRaises(ValueError, Test.bulk_create, self.session, []) + self.assertRaises(ValueError, Test.bulk_create, self.session, None) + self.assertRaises(ValueError, Test.bulk_create, self.session, object) + self.assertRaises(ValueError, Test.bulk_create, self.session, {}) + self.assertRaises(ValueError, Test.bulk_create, self.session, "hi!") + self.assertRaises(ValueError, Test.bulk_create, self.session, ["hi!"]) + + def _test_bulk_create(self, cls, http_method, microversion=None, + base_path=None, **params): + req1 = mock.Mock() + req2 = mock.Mock() + req1.body = {'name': 'resource1'} + req2.body = {'name': 'resource2'} + req1.url = 'uri' + req2.url = 'uri' + req1.headers = 'headers' + req2.headers = 'headers' + + request_body = {"tests": [{'name': 'resource1', 'id': 'id1'}, + {'name': 'resource2', 'id': 'id2'}]} + + cls._prepare_request = mock.Mock(side_effect=[req1, req2]) + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.links = {} + mock_response.json.return_value = request_body + http_method.return_value = mock_response + + res = list(cls.bulk_create(self.session, [{'name': 'resource1'}, + {'name': 'resource2'}], + base_path=base_path, **params)) + + self.assertEqual(len(res), 2) + self.assertEqual(res[0].id, 'id1') + self.assertEqual(res[1].id, 'id2') + http_method.assert_called_once_with(self.request.url, + json={'tests': [req1.body, + req2.body]}, + headers=self.request.headers, + microversion=microversion, + params=params) + + def test_bulk_create_post(self): + class Test(resource.Resource): + service = self.service_name + base_path = self.base_path + create_method = 'POST' + allow_create = True + resources_key = 'tests' + + self._test_bulk_create(Test, self.session.post) + + def test_bulk_create_put(self): + class Test(resource.Resource): + service = self.service_name + base_path = self.base_path + create_method = 'PUT' + allow_create = True + resources_key = 'tests' + + self._test_bulk_create(Test, self.session.put) + + def test_bulk_create_with_params(self): + class Test(resource.Resource): + service = self.service_name + base_path = self.base_path + create_method = 'POST' + allow_create = True + resources_key = 'tests' + + self._test_bulk_create(Test, self.session.post, answer=42) + + def test_bulk_create_with_microversion(self): + class Test(resource.Resource): + service = self.service_name + base_path = self.base_path + create_method = 'POST' + allow_create = True + resources_key = 'tests' + _max_microversion = '1.42' + + self._test_bulk_create(Test, self.session.post, microversion='1.42') + + def test_bulk_create_with_base_path(self): + class Test(resource.Resource): + service = self.service_name + base_path = self.base_path + create_method = 'POST' + allow_create = True + resources_key = 'tests' + + self._test_bulk_create(Test, self.session.post, base_path='dummy') + + def test_bulk_create_fail(self): + class Test(resource.Resource): + service = self.service_name + base_path = self.base_path + create_method = 'POST' + allow_create = False + resources_key = 'tests' + + self.assertRaises(exceptions.MethodNotSupported, Test.bulk_create, + self.session, [{'name': 'name'}]) + + def test_bulk_create_fail_on_request(self): + class Test(resource.Resource): + service = self.service_name + base_path = self.base_path + create_method = 'POST' + allow_create = True + resources_key = 'tests' + + response = FakeResponse({}, status_code=409) + response.content = ('{"TestError": {"message": "Failed to parse ' + 'request. Required attribute \'foo\' not ' + 'specified", "type": "HTTPBadRequest", ' + '"detail": ""}}') + response.reason = 'Bad Request' + self.session.post.return_value = response + self.assertRaises(exceptions.ConflictException, Test.bulk_create, + self.session, [{'name': 'name'}]) + class TestResourceFind(base.TestCase): From cc711719625cd09e70c5fea53675b6c295a3a7cb Mon Sep 17 00:00:00 2001 From: gryf Date: Mon, 17 Feb 2020 12:59:50 +0100 Subject: [PATCH 2597/3836] Add bulk port create. Introducing new method "create_ports" for network module. It provides an ability for creating port objects using Neutron API in one API call. Change-Id: Ie4d6eadb4b59ec070dc4a3bc34e5c1d591bffc39 --- doc/source/user/proxies/network.rst | 1 + openstack/network/v2/_proxy.py | 12 +++++++ openstack/proxy.py | 20 +++++++++++ openstack/tests/unit/network/v2/test_proxy.py | 8 +++++ openstack/tests/unit/test_proxy.py | 33 +++++++++++++++++++ ...ulk-create-resources-12192ec9d76c7716.yaml | 5 +++ 6 files changed, 79 insertions(+) create mode 100644 releasenotes/notes/add-bulk-create-resources-12192ec9d76c7716.yaml diff --git a/doc/source/user/proxies/network.rst b/doc/source/user/proxies/network.rst index 5a2ea9a95..a9af0b922 100644 --- a/doc/source/user/proxies/network.rst +++ b/doc/source/user/proxies/network.rst @@ -38,6 +38,7 @@ Port Operations .. autoclass:: openstack.network.v2._proxy.Proxy .. automethod:: openstack.network.v2._proxy.Proxy.create_port + .. automethod:: openstack.network.v2._proxy.Proxy.create_ports .. automethod:: openstack.network.v2._proxy.Proxy.update_port .. automethod:: openstack.network.v2._proxy.Proxy.delete_port .. automethod:: openstack.network.v2._proxy.Proxy.get_port diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 7b3229b89..835945cf1 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -1687,6 +1687,18 @@ def create_port(self, **attrs): """ return self._create(_port.Port, **attrs) + def create_ports(self, data): + """Create ports from the list of attributes + + :param list data: List of dicts of attributes which will be used to + create a :class:`~openstack.network.v2.port.Port`, + comprised of the properties on the Port class. + + :returns: A generator of port objects + :rtype: :class:`~openstack.network.v2.port.Port` + """ + return self._bulk_create(_port.Port, data) + def delete_port(self, port, ignore_missing=True): """Delete a port diff --git a/openstack/proxy.py b/openstack/proxy.py index 724a816d6..2ea253038 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -416,6 +416,26 @@ def _create(self, resource_type, base_path=None, **attrs): res = resource_type.new(connection=conn, **attrs) return res.create(self, base_path=base_path) + def _bulk_create(self, resource_type, data, base_path=None): + """Create a resource from attributes + + :param resource_type: The type of resource to create. + :type resource_type: :class:`~openstack.resource.Resource` + :param list data: List of attributes dicts to be passed onto the + :meth:`~openstack.resource.Resource.create` + method to be created. These should correspond + to either :class:`~openstack.resource.Body` + or :class:`~openstack.resource.Header` + values on this resource. + :param str base_path: Base part of the URI for creating resources, if + different from + :data:`~openstack.resource.Resource.base_path`. + + :returns: A generator of Resource objects. + :rtype: :class:`~openstack.resource.Resource` + """ + return resource_type.bulk_create(self, data, base_path=base_path) + @_check_resource(strict=False) def _get(self, resource_type, value=None, requires_id=True, base_path=None, **attrs): diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 24961042b..f1037bf5e 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -465,6 +465,14 @@ def test_ports(self): def test_port_update(self): self.verify_update(self.proxy.update_port, port.Port) + @mock.patch('openstack.network.v2._proxy.Proxy._bulk_create') + def test_ports_create(self, bc): + data = mock.sentinel + + self.proxy.create_ports(data) + + bc.assert_called_once_with(port.Port, data) + def test_qos_bandwidth_limit_rule_create_attrs(self): self.verify_create( self.proxy.create_qos_bandwidth_limit_rule, diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index 42e239086..46c9ff8be 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -320,6 +320,39 @@ def test_create_attributes_override_base_path(self): self.res.create.assert_called_once_with(self.sot, base_path=base_path) +class TestProxyBulkCreate(base.TestCase): + + def setUp(self): + super(TestProxyBulkCreate, self).setUp() + + class Res(resource.Resource): + pass + + self.session = mock.Mock() + self.result = mock.sentinel + self.data = mock.Mock() + + self.sot = proxy.Proxy(self.session) + self.cls = Res + self.cls.bulk_create = mock.Mock(return_value=self.result) + + def test_bulk_create_attributes(self): + rv = self.sot._bulk_create(self.cls, self.data) + + self.assertEqual(rv, self.result) + self.cls.bulk_create.assert_called_once_with(self.sot, self.data, + base_path=None) + + def test_bulk_create_attributes_override_base_path(self): + base_path = 'dummy' + + rv = self.sot._bulk_create(self.cls, self.data, base_path=base_path) + + self.assertEqual(rv, self.result) + self.cls.bulk_create.assert_called_once_with(self.sot, self.data, + base_path=base_path) + + class TestProxyGet(base.TestCase): def setUp(self): diff --git a/releasenotes/notes/add-bulk-create-resources-12192ec9d76c7716.yaml b/releasenotes/notes/add-bulk-create-resources-12192ec9d76c7716.yaml new file mode 100644 index 000000000..896e17bdb --- /dev/null +++ b/releasenotes/notes/add-bulk-create-resources-12192ec9d76c7716.yaml @@ -0,0 +1,5 @@ +--- +features: + - Enabling Resource class for being able to create objects in bulk way. Add + first objects using that feature - Port, which now expose a proxy method + `create_ports` for creating multiple port objects at once. From 970a74e66d4d7b229351bf35c90aa82368ca336c Mon Sep 17 00:00:00 2001 From: Prashant Bhole Date: Mon, 17 Feb 2020 06:45:19 +0000 Subject: [PATCH 2598/3836] Fix: Set image name correctly if filename is not passed If filename is not passed to create_image method, the name paramter is ignored. This patch fixes this problem and adds required unit and functional tests. Change-Id: I191d15c2783a187c8201f0a18251734b3a5ddf81 Closes-Bug: #1863209 --- openstack/image/_base_proxy.py | 1 + openstack/tests/functional/cloud/test_image.py | 13 +++++++++++++ openstack/tests/unit/image/v2/test_proxy.py | 12 ++++++++++++ 3 files changed, 26 insertions(+) diff --git a/openstack/image/_base_proxy.py b/openstack/image/_base_proxy.py index 499a2e983..85055f95c 100644 --- a/openstack/image/_base_proxy.py +++ b/openstack/image/_base_proxy.py @@ -173,6 +173,7 @@ def create_image( validate_checksum=validate_checksum, **image_kwargs) else: + image_kwargs['name'] = name image = self._create_image(**image_kwargs) self._connection._get_cache(None).invalidate() return image diff --git a/openstack/tests/functional/cloud/test_image.py b/openstack/tests/functional/cloud/test_image.py index 08f416c9c..5331f5dca 100644 --- a/openstack/tests/functional/cloud/test_image.py +++ b/openstack/tests/functional/cloud/test_image.py @@ -149,6 +149,19 @@ def test_create_image_update_properties(self): finally: self.user_cloud.delete_image(image_name, wait=True) + def test_create_image_without_filename(self): + image_name = self.getUniqueString('image') + image = self.user_cloud.create_image( + name=image_name, + disk_format='raw', + container_format='bare', + min_disk=10, + min_ram=1024, + allow_duplicates=True, + wait=False) + self.assertEqual(image_name, image.name) + self.user_cloud.delete_image(image.id, wait=True) + def test_get_image_by_id(self): test_image = tempfile.NamedTemporaryFile(delete=False) test_image.write(b'\0' * 1024 * 1024) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index be7693f69..20e35d750 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -163,6 +163,18 @@ def test_image_create_data_binary(self): self.proxy._IMAGE_OBJECT_KEY: 'bare/fake'}, timeout=3600, validate_checksum=False, wait=False) + def test_image_create_without_filename(self): + self.proxy._create_image = mock.Mock() + + self.proxy.create_image( + allow_duplicates=True, + name='fake', disk_format="fake_dformat", + container_format="fake_cformat" + ) + self.proxy._create_image.assert_called_with( + container_format='fake_cformat', disk_format='fake_dformat', + name='fake', properties=mock.ANY) + def test_image_upload_no_args(self): # container_format and disk_format are required args self.assertRaises(exceptions.InvalidRequest, self.proxy.upload_image) From c2f2ffdd9f91773838060280747614b1691e7e01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dulko?= Date: Wed, 26 Feb 2020 14:07:19 +0100 Subject: [PATCH 2599/3836] Implement If-Match support for Neutron resources Neutron API supports using If-Match HTTP header to do compare-and-swap updates and deletes of several resources [1]. This feature is based on revision_number property. This commit implements that by adding the if_revision argument to supported update_* and delete_* resources. [1] https://docs.openstack.org/api-ref/network/v2/?expanded=list-routers-detail#revisions Change-Id: I5c4ff42796cc860c0a99a431cac84bb75a2d9236 --- openstack/compute/v2/server.py | 2 +- openstack/database/v1/user.py | 2 +- openstack/exceptions.py | 7 + openstack/image/v2/image.py | 2 +- openstack/load_balancer/v2/quota.py | 2 +- openstack/network/v2/_base.py | 27 +++ openstack/network/v2/_proxy.py | 109 +++++++++--- openstack/network/v2/floating_ip.py | 5 +- openstack/network/v2/network.py | 5 +- openstack/network/v2/port.py | 5 +- openstack/network/v2/quota.py | 2 +- openstack/network/v2/router.py | 3 +- openstack/network/v2/security_group.py | 5 +- openstack/network/v2/security_group_rule.py | 5 +- openstack/network/v2/subnet.py | 5 +- openstack/resource.py | 19 +- openstack/tests/unit/network/v2/test_proxy.py | 166 +++++++++++++++--- openstack/tests/unit/test_resource.py | 2 + 18 files changed, 290 insertions(+), 83 deletions(-) create mode 100644 openstack/network/v2/_base.py diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index c74f57e76..7064d8be8 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -196,7 +196,7 @@ class Server(resource.Resource, metadata.MetadataMixin, resource.TagMixin): vm_state = resource.Body('OS-EXT-STS:vm_state') def _prepare_request(self, requires_id=True, prepend_key=True, - base_path=None): + base_path=None, **kwargs): request = super(Server, self)._prepare_request(requires_id=requires_id, prepend_key=prepend_key, base_path=base_path) diff --git a/openstack/database/v1/user.py b/openstack/database/v1/user.py index b9bd4fe14..07e6fc72a 100644 --- a/openstack/database/v1/user.py +++ b/openstack/database/v1/user.py @@ -35,7 +35,7 @@ class User(resource.Resource): password = resource.Body('password') def _prepare_request(self, requires_id=True, prepend_key=True, - base_path=None): + base_path=None, **kwargs): """Prepare a request for the database service's create call User.create calls require the resources_key. diff --git a/openstack/exceptions.py b/openstack/exceptions.py index 56fba3b5e..a69d97729 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -121,6 +121,11 @@ class ConflictException(HttpException): pass +class PreconditionFailedException(HttpException): + """HTTP 412 Precondition Failed.""" + pass + + class MethodNotSupported(SDKException): """The resource does not support this operation type.""" def __init__(self, resource, method): @@ -192,6 +197,8 @@ def raise_from_response(response, error_message=None): cls = NotFoundException elif response.status_code == 400: cls = BadRequestException + elif response.status_code == 412: + cls = PreconditionFailedException else: cls = HttpException diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index dd789d5ba..3e0f223d2 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -285,7 +285,7 @@ def import_image(self, session, method='glance-direct', uri=None, session.post(url, json=json, headers=headers) def _prepare_request(self, requires_id=None, prepend_key=False, - patch=False, base_path=None): + patch=False, base_path=None, **kwargs): request = super(Image, self)._prepare_request(requires_id=requires_id, prepend_key=prepend_key, patch=patch, diff --git a/openstack/load_balancer/v2/quota.py b/openstack/load_balancer/v2/quota.py index a14f62812..8a0106970 100644 --- a/openstack/load_balancer/v2/quota.py +++ b/openstack/load_balancer/v2/quota.py @@ -42,7 +42,7 @@ class Quota(resource.Resource): project_id = resource.Body('project_id', alternate_id=True) def _prepare_request(self, requires_id=True, - base_path=None, prepend_key=False): + base_path=None, prepend_key=False, **kwargs): _request = super(Quota, self)._prepare_request(requires_id, prepend_key, base_path=base_path) diff --git a/openstack/network/v2/_base.py b/openstack/network/v2/_base.py new file mode 100644 index 000000000..147563d26 --- /dev/null +++ b/openstack/network/v2/_base.py @@ -0,0 +1,27 @@ +# 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 resource + + +class NetworkResource(resource.Resource): + #: Revision number of the resource. *Type: int* + revision_number = resource.Body('revision_number', type=int) + + def _prepare_request(self, requires_id=None, prepend_key=False, + patch=False, base_path=None, params=None, + if_revision=None, **kwargs): + req = super(NetworkResource, self)._prepare_request( + requires_id=requires_id, prepend_key=prepend_key, patch=patch, + base_path=base_path, params=params) + if if_revision is not None: + req.headers['If-Match'] = "revision_number=%d" % if_revision + return req diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 835945cf1..f6f69580c 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -60,6 +60,26 @@ class Proxy(proxy.Proxy): + @proxy._check_resource(strict=False) + def _update(self, resource_type, value, base_path=None, + if_revision=None, **attrs): + res = self._get_resource(resource_type, value, **attrs) + return res.commit(self, base_path=base_path, if_revision=if_revision) + + @proxy._check_resource(strict=False) + def _delete(self, resource_type, value, ignore_missing=True, + if_revision=None, **attrs): + res = self._get_resource(resource_type, value, **attrs) + + try: + rv = res.delete(self, if_revision=if_revision) + except exceptions.ResourceNotFound: + if ignore_missing: + return None + raise + + return rv + def create_address_scope(self, **attrs): """Create a new address scope from attributes @@ -497,7 +517,7 @@ def create_ip(self, **attrs): """ return self._create(_floating_ip.FloatingIP, **attrs) - def delete_ip(self, floating_ip, ignore_missing=True): + def delete_ip(self, floating_ip, ignore_missing=True, if_revision=None): """Delete a floating ip :param floating_ip: The value can be either the ID of a floating ip @@ -508,11 +528,13 @@ def delete_ip(self, floating_ip, ignore_missing=True): raised when the floating ip does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent ip. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. :returns: ``None`` """ self._delete(_floating_ip.FloatingIP, floating_ip, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, if_revision=if_revision) def find_available_ip(self): """Find an available IP @@ -577,19 +599,22 @@ def ips(self, **query): """ return self._list(_floating_ip.FloatingIP, **query) - def update_ip(self, floating_ip, **attrs): + def update_ip(self, floating_ip, if_revision=None, **attrs): """Update a ip :param floating_ip: Either the id of a ip or a :class:`~openstack.network.v2.floating_ip.FloatingIP` instance. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. :param dict attrs: The attributes to update on the ip represented by ``value``. :returns: The updated ip :rtype: :class:`~openstack.network.v2.floating_ip.FloatingIP` """ - return self._update(_floating_ip.FloatingIP, floating_ip, **attrs) + return self._update(_floating_ip.FloatingIP, floating_ip, + if_revision=if_revision, **attrs) def create_port_forwarding(self, **attrs): """Create a new floating ip port forwarding from attributes @@ -1203,7 +1228,7 @@ def create_network(self, **attrs): """ return self._create(_network.Network, **attrs) - def delete_network(self, network, ignore_missing=True): + def delete_network(self, network, ignore_missing=True, if_revision=None): """Delete a network :param network: @@ -1214,10 +1239,13 @@ def delete_network(self, network, ignore_missing=True): raised when the network does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent network. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. :returns: ``None`` """ - self._delete(_network.Network, network, ignore_missing=ignore_missing) + self._delete(_network.Network, network, ignore_missing=ignore_missing, + if_revision=if_revision) def find_network(self, name_or_id, ignore_missing=True, **args): """Find a single network @@ -1276,18 +1304,21 @@ def networks(self, **query): """ return self._list(_network.Network, **query) - def update_network(self, network, **attrs): + def update_network(self, network, if_revision=None, **attrs): """Update a network :param network: Either the id of a network or an instance of type :class:`~openstack.network.v2.network.Network`. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. :param dict attrs: The attributes to update on the network represented by ``network``. :returns: The updated network :rtype: :class:`~openstack.network.v2.network.Network` """ - return self._update(_network.Network, network, **attrs) + return self._update(_network.Network, network, if_revision=if_revision, + **attrs) def find_network_ip_availability(self, name_or_id, ignore_missing=True, **args): @@ -1699,7 +1730,7 @@ def create_ports(self, data): """ return self._bulk_create(_port.Port, data) - def delete_port(self, port, ignore_missing=True): + def delete_port(self, port, ignore_missing=True, if_revision=None): """Delete a port :param port: The value can be either the ID of a port or a @@ -1709,10 +1740,13 @@ def delete_port(self, port, ignore_missing=True): raised when the port does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent port. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. :returns: ``None`` """ - self._delete(_port.Port, port, ignore_missing=ignore_missing) + self._delete(_port.Port, port, ignore_missing=ignore_missing, + if_revision=if_revision) def find_port(self, name_or_id, ignore_missing=True, **args): """Find a single port @@ -1766,18 +1800,21 @@ def ports(self, **query): """ return self._list(_port.Port, **query) - def update_port(self, port, **attrs): + def update_port(self, port, if_revision=None, **attrs): """Update a port :param port: Either the id of a port or a :class:`~openstack.network.v2.port.Port` instance. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. :param dict attrs: The attributes to update on the port represented by ``port``. :returns: The updated port :rtype: :class:`~openstack.network.v2.port.Port` """ - return self._update(_port.Port, port, **attrs) + return self._update(_port.Port, port, if_revision=if_revision, + **attrs) def add_ip_to_port(self, port, ip): ip.port_id = port.id @@ -2482,7 +2519,7 @@ def create_router(self, **attrs): """ return self._create(_router.Router, **attrs) - def delete_router(self, router, ignore_missing=True): + def delete_router(self, router, ignore_missing=True, if_revision=None): """Delete a router :param router: The value can be either the ID of a router or a @@ -2492,10 +2529,13 @@ def delete_router(self, router, ignore_missing=True): raised when the router does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent router. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. :returns: ``None`` """ - self._delete(_router.Router, router, ignore_missing=ignore_missing) + self._delete(_router.Router, router, ignore_missing=ignore_missing, + if_revision=if_revision) def find_router(self, name_or_id, ignore_missing=True, **args): """Find a single router @@ -2546,18 +2586,21 @@ def routers(self, **query): """ return self._list(_router.Router, **query) - def update_router(self, router, **attrs): + def update_router(self, router, if_revision=None, **attrs): """Update a router :param router: Either the id of a router or a :class:`~openstack.network.v2.router.Router` instance. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. :param dict attrs: The attributes to update on the router represented by ``router``. :returns: The updated router :rtype: :class:`~openstack.network.v2.router.Router` """ - return self._update(_router.Router, router, **attrs) + return self._update(_router.Router, router, if_revision=if_revision, + **attrs) def add_interface_to_router(self, router, subnet_id=None, port_id=None): """Add Interface to a router @@ -3048,7 +3091,8 @@ def create_security_group(self, **attrs): """ return self._create(_security_group.SecurityGroup, **attrs) - def delete_security_group(self, security_group, ignore_missing=True): + def delete_security_group(self, security_group, ignore_missing=True, + if_revision=None): """Delete a security group :param security_group: @@ -3060,11 +3104,13 @@ def delete_security_group(self, security_group, ignore_missing=True): raised when the security group does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent security group. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. :returns: ``None`` """ self._delete(_security_group.SecurityGroup, security_group, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, if_revision=if_revision) def find_security_group(self, name_or_id, ignore_missing=True, **args): """Find a single security group @@ -3113,12 +3159,14 @@ def security_groups(self, **query): """ return self._list(_security_group.SecurityGroup, **query) - def update_security_group(self, security_group, **attrs): + def update_security_group(self, security_group, if_revision=None, **attrs): """Update a security group :param security_group: Either the id of a security group or a :class:`~openstack.network.v2.security_group.SecurityGroup` instance. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. :param dict attrs: The attributes to update on the security group represented by ``security_group``. @@ -3126,7 +3174,7 @@ def update_security_group(self, security_group, **attrs): :rtype: :class:`~openstack.network.v2.security_group.SecurityGroup` """ return self._update(_security_group.SecurityGroup, security_group, - **attrs) + if_revision=if_revision, **attrs) def create_security_group_rule(self, **attrs): """Create a new security group rule from attributes @@ -3143,7 +3191,7 @@ def create_security_group_rule(self, **attrs): return self._create(_security_group_rule.SecurityGroupRule, **attrs) def delete_security_group_rule(self, security_group_rule, - ignore_missing=True): + ignore_missing=True, if_revision=None): """Delete a security group rule :param security_group_rule: @@ -3155,11 +3203,14 @@ def delete_security_group_rule(self, security_group_rule, raised when the security group rule does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent security group rule. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. :returns: ``None`` """ self._delete(_security_group_rule.SecurityGroupRule, - security_group_rule, ignore_missing=ignore_missing) + security_group_rule, ignore_missing=ignore_missing, + if_revision=if_revision) def find_security_group_rule(self, name_or_id, ignore_missing=True, **args): @@ -3424,7 +3475,7 @@ def create_subnet(self, **attrs): """ return self._create(_subnet.Subnet, **attrs) - def delete_subnet(self, subnet, ignore_missing=True): + def delete_subnet(self, subnet, ignore_missing=True, if_revision=None): """Delete a subnet :param subnet: The value can be either the ID of a subnet or a @@ -3434,10 +3485,13 @@ def delete_subnet(self, subnet, ignore_missing=True): raised when the subnet does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent subnet. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. :returns: ``None`` """ - self._delete(_subnet.Subnet, subnet, ignore_missing=ignore_missing) + self._delete(_subnet.Subnet, subnet, ignore_missing=ignore_missing, + if_revision=if_revision) def find_subnet(self, name_or_id, ignore_missing=True, **args): """Find a single subnet @@ -3491,18 +3545,21 @@ def subnets(self, **query): """ return self._list(_subnet.Subnet, **query) - def update_subnet(self, subnet, **attrs): + def update_subnet(self, subnet, if_revision=None, **attrs): """Update a subnet :param subnet: Either the id of a subnet or a :class:`~openstack.network.v2.subnet.Subnet` instance. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. :param dict attrs: The attributes to update on the subnet represented by ``subnet``. :returns: The updated subnet :rtype: :class:`~openstack.network.v2.subnet.Subnet` """ - return self._update(_subnet.Subnet, subnet, **attrs) + return self._update(_subnet.Subnet, subnet, if_revision=if_revision, + **attrs) def create_subnet_pool(self, **attrs): """Create a new subnet pool from attributes diff --git a/openstack/network/v2/floating_ip.py b/openstack/network/v2/floating_ip.py index 68a563fed..2f94226f1 100644 --- a/openstack/network/v2/floating_ip.py +++ b/openstack/network/v2/floating_ip.py @@ -10,10 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import _base from openstack import resource -class FloatingIP(resource.Resource, resource.TagMixin): +class FloatingIP(_base.NetworkResource, resource.TagMixin): name_attribute = "floating_ip_address" resource_name = "floating ip" resource_key = 'floatingip' @@ -68,8 +69,6 @@ class FloatingIP(resource.Resource, resource.TagMixin): qos_policy_id = resource.Body('qos_policy_id') #: The ID of the project this floating IP is associated with. project_id = resource.Body('tenant_id') - #: Revision number of the floating IP. *Type: int* - revision_number = resource.Body('revision_number', type=int) #: The ID of an associated router. router_id = resource.Body('router_id') #: The floating IP status. Value is ``ACTIVE`` or ``DOWN``. diff --git a/openstack/network/v2/network.py b/openstack/network/v2/network.py index 3339c1f9e..884c52a86 100644 --- a/openstack/network/v2/network.py +++ b/openstack/network/v2/network.py @@ -10,10 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import _base from openstack import resource -class Network(resource.Resource, resource.TagMixin): +class Network(_base.NetworkResource, resource.TagMixin): resource_key = 'network' resources_key = 'networks' base_path = '/networks' @@ -97,8 +98,6 @@ class Network(resource.Resource, resource.TagMixin): provider_segmentation_id = resource.Body('provider:segmentation_id') #: The ID of the QoS policy attached to the port. qos_policy_id = resource.Body('qos_policy_id') - #: Revision number of the network. *Type: int* - revision_number = resource.Body('revision_number', type=int) #: A list of provider segment objects. #: Available for multiple provider extensions. segments = resource.Body('segments') diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index 1f96f4329..800b91fe5 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -10,10 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import _base from openstack import resource -class Port(resource.Resource, resource.TagMixin): +class Port(_base.NetworkResource, resource.TagMixin): resource_key = 'port' resources_key = 'ports' base_path = '/ports' @@ -114,8 +115,6 @@ class Port(resource.Resource, resource.TagMixin): # (i.e.: minimum-bandwidth) and traits (i.e.: vnic-type, physnet) # requested by a port to Nova and Placement. resource_request = resource.Body('resource_request', type=dict) - #: Revision number of the port. *Type: int* - revision_number = resource.Body('revision_number', type=int) #: The IDs of any attached security groups. #: *Type: list of strs of the security group IDs* security_group_ids = resource.Body('security_groups', type=list) diff --git a/openstack/network/v2/quota.py b/openstack/network/v2/quota.py index 87432718c..fbda19bc0 100644 --- a/openstack/network/v2/quota.py +++ b/openstack/network/v2/quota.py @@ -57,7 +57,7 @@ class Quota(resource.Resource): security_groups = resource.Body('security_group', type=int) def _prepare_request(self, requires_id=True, prepend_key=False, - base_path=None): + base_path=None, **kwargs): _request = super(Quota, self)._prepare_request(requires_id, prepend_key) if self.resource_key in _request.body: diff --git a/openstack/network/v2/router.py b/openstack/network/v2/router.py index a7de75f8b..b714b6e48 100644 --- a/openstack/network/v2/router.py +++ b/openstack/network/v2/router.py @@ -11,11 +11,12 @@ # under the License. from openstack import exceptions +from openstack.network.v2 import _base from openstack import resource from openstack import utils -class Router(resource.Resource, resource.TagMixin): +class Router(_base.NetworkResource, resource.TagMixin): resource_key = 'router' resources_key = 'routers' base_path = '/routers' diff --git a/openstack/network/v2/security_group.py b/openstack/network/v2/security_group.py index bb456e7f4..f4c60dc7a 100644 --- a/openstack/network/v2/security_group.py +++ b/openstack/network/v2/security_group.py @@ -10,10 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import _base from openstack import resource -class SecurityGroup(resource.Resource, resource.TagMixin): +class SecurityGroup(_base.NetworkResource, resource.TagMixin): resource_key = 'security_group' resources_key = 'security_groups' base_path = '/security-groups' @@ -40,8 +41,6 @@ class SecurityGroup(resource.Resource, resource.TagMixin): name = resource.Body('name') #: The ID of the project this security group is associated with. project_id = resource.Body('project_id') - #: Revision number of the security group. *Type: int* - revision_number = resource.Body('revision_number', type=int) #: A list of #: :class:`~openstack.network.v2.security_group_rule.SecurityGroupRule` #: objects. *Type: list* diff --git a/openstack/network/v2/security_group_rule.py b/openstack/network/v2/security_group_rule.py index 391b97eb1..398366e5c 100644 --- a/openstack/network/v2/security_group_rule.py +++ b/openstack/network/v2/security_group_rule.py @@ -10,10 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import _base from openstack import resource -class SecurityGroupRule(resource.Resource, resource.TagMixin): +class SecurityGroupRule(_base.NetworkResource, resource.TagMixin): resource_key = 'security_group_rule' resources_key = 'security_group_rules' base_path = '/security-group-rules' @@ -74,8 +75,6 @@ class SecurityGroupRule(resource.Resource, resource.TagMixin): #: in the request body. This attribute matches the specified IP prefix #: as the source IP address of the IP packet. remote_ip_prefix = resource.Body('remote_ip_prefix') - #: Revision number of the security group rule. *Type: int* - revision_number = resource.Body('revision_number', type=int) #: The security group ID to associate with this security group rule. security_group_id = resource.Body('security_group_id') #: The ID of the project this security group rule is associated with. diff --git a/openstack/network/v2/subnet.py b/openstack/network/v2/subnet.py index 2b1d3ad18..8ddbdbe4d 100644 --- a/openstack/network/v2/subnet.py +++ b/openstack/network/v2/subnet.py @@ -10,10 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import _base from openstack import resource -class Subnet(resource.Resource, resource.TagMixin): +class Subnet(_base.NetworkResource, resource.TagMixin): resource_key = 'subnet' resources_key = 'subnets' base_path = '/subnets' @@ -75,8 +76,6 @@ class Subnet(resource.Resource, resource.TagMixin): prefix_length = resource.Body('prefixlen') #: The ID of the project this subnet is associated with. project_id = resource.Body('tenant_id') - #: Revision number of the subnet. *Type: int* - revision_number = resource.Body('revision_number', type=int) #: The ID of the segment this subnet is associated with. segment_id = resource.Body('segment_id') #: Service types for this subnet diff --git a/openstack/resource.py b/openstack/resource.py index 9260e9577..6cefb5bd8 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1053,7 +1053,7 @@ def _prepare_request_body(self, patch, prepend_key): return body def _prepare_request(self, requires_id=None, prepend_key=False, - patch=False, base_path=None, params=None): + patch=False, base_path=None, params=None, **kwargs): """Prepare a request to be sent to the server Create operations don't require an ID, but all others do, @@ -1436,7 +1436,7 @@ def requires_commit(self): or self.allow_empty_commit) def commit(self, session, prepend_key=True, has_body=True, - retry_on_conflict=None, base_path=None): + retry_on_conflict=None, base_path=None, **kwargs): """Commit the state of the instance to the remote resource. :param session: The session to use for making this request. @@ -1450,6 +1450,8 @@ def commit(self, session, prepend_key=True, has_body=True, :param str base_path: Base part of the URI for modifying resources, if different from :data:`~openstack.resource.Resource.base_path`. + :param dict kwargs: Parameters that will be passed to + _prepare_request() :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if @@ -1467,7 +1469,6 @@ def commit(self, session, prepend_key=True, has_body=True, # Avoid providing patch unconditionally to avoid breaking subclasses # without it. - kwargs = {} if self.commit_jsonpatch: kwargs['patch'] = True @@ -1582,11 +1583,13 @@ def patch(self, session, patch=None, prepend_key=True, has_body=True, has_body=has_body, retry_on_conflict=retry_on_conflict) - def delete(self, session, error_message=None): + def delete(self, session, error_message=None, **kwargs): """Delete the remote resource based on this instance. :param session: The session to use for making this request. :type session: :class:`~keystoneauth1.adapter.Adapter` + :param dict kwargs: Parameters that will be passed to + _prepare_request() :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if @@ -1595,7 +1598,7 @@ def delete(self, session, error_message=None): the resource was not found. """ - response = self._raw_delete(session) + response = self._raw_delete(session, **kwargs) kwargs = {} if error_message: kwargs['error_message'] = error_message @@ -1603,16 +1606,16 @@ def delete(self, session, error_message=None): self._translate_response(response, has_body=False, **kwargs) return self - def _raw_delete(self, session): + def _raw_delete(self, session, **kwargs): if not self.allow_delete: raise exceptions.MethodNotSupported(self, "delete") - request = self._prepare_request() + request = self._prepare_request(**kwargs) session = self._get_session(session) microversion = self._get_microversion_for(session, 'delete') return session.delete( - request.url, + request.url, headers=request.headers, microversion=microversion) @classmethod diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index f1037bf5e..b5a5bc102 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -70,6 +70,25 @@ def setUp(self): super(TestNetworkProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) + def verify_update(self, test_method, resource_type, value=None, + mock_method="openstack.network.v2._proxy.Proxy._update", + expected_result="result", path_args=None, **kwargs): + super(TestNetworkProxy, self).verify_update( + test_method, resource_type, value=value, mock_method=mock_method, + expected_result=expected_result, path_args=path_args, **kwargs) + + def verify_delete(self, test_method, resource_type, ignore, + input_path_args=None, expected_path_args=None, + method_kwargs=None, expected_args=None, + expected_kwargs=None, + mock_method="openstack.network.v2._proxy.Proxy._delete"): + super(TestNetworkProxy, self).verify_delete( + test_method, resource_type, ignore, + input_path_args=input_path_args, + expected_path_args=expected_path_args, method_kwargs=method_kwargs, + expected_args=expected_args, expected_kwargs=expected_kwargs, + mock_method=mock_method) + def test_address_scope_create_attrs(self): self.verify_create(self.proxy.create_address_scope, address_scope.AddressScope) @@ -143,11 +162,16 @@ def test_floating_ip_create_attrs(self): def test_floating_ip_delete(self): self.verify_delete(self.proxy.delete_ip, floating_ip.FloatingIP, - False) + False, expected_kwargs={'if_revision': None}) def test_floating_ip_delete_ignore(self): self.verify_delete(self.proxy.delete_ip, floating_ip.FloatingIP, - True) + True, expected_kwargs={'if_revision': None}) + + def test_floating_ip_delete_if_revision(self): + self.verify_delete(self.proxy.delete_ip, floating_ip.FloatingIP, + True, method_kwargs={'if_revision': 42}, + expected_kwargs={'if_revision': 42}) def test_floating_ip_find(self): self.verify_find(self.proxy.find_ip, floating_ip.FloatingIP) @@ -159,7 +183,16 @@ def test_ips(self): self.verify_list(self.proxy.ips, floating_ip.FloatingIP) def test_floating_ip_update(self): - self.verify_update(self.proxy.update_ip, floating_ip.FloatingIP) + self.verify_update(self.proxy.update_ip, floating_ip.FloatingIP, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': None}) + + def test_floating_ip_update_if_revision(self): + self.verify_update(self.proxy.update_ip, floating_ip.FloatingIP, + method_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': 42}, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': 42}) def test_health_monitor_create_attrs(self): self.verify_create(self.proxy.create_health_monitor, @@ -300,10 +333,17 @@ def test_network_create_attrs(self): self.verify_create(self.proxy.create_network, network.Network) def test_network_delete(self): - self.verify_delete(self.proxy.delete_network, network.Network, False) + self.verify_delete(self.proxy.delete_network, network.Network, False, + expected_kwargs={'if_revision': None}) def test_network_delete_ignore(self): - self.verify_delete(self.proxy.delete_network, network.Network, True) + self.verify_delete(self.proxy.delete_network, network.Network, True, + expected_kwargs={'if_revision': None}) + + def test_network_delete_if_revision(self): + self.verify_delete(self.proxy.delete_network, network.Network, True, + method_kwargs={'if_revision': 42}, + expected_kwargs={'if_revision': 42}) def test_network_find(self): self.verify_find(self.proxy.find_network, network.Network) @@ -324,7 +364,16 @@ def test_networks(self): self.verify_list(self.proxy.networks, network.Network) def test_network_update(self): - self.verify_update(self.proxy.update_network, network.Network) + self.verify_update(self.proxy.update_network, network.Network, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': None}) + + def test_network_update_if_revision(self): + self.verify_update(self.proxy.update_network, network.Network, + method_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': 42}, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': 42}) def test_flavor_create_attrs(self): self.verify_create(self.proxy.create_flavor, flavor.Flavor) @@ -417,7 +466,7 @@ def test_pool_members(self): expected_kwargs={"pool_id": "test_id"}) def test_pool_member_update(self): - self._verify2("openstack.proxy.Proxy._update", + self._verify2("openstack.network.v2._proxy.Proxy._update", self.proxy.update_pool_member, method_args=["MEMBER", "POOL"], expected_args=[pool_member.PoolMember, "MEMBER"], @@ -448,10 +497,17 @@ def test_port_create_attrs(self): self.verify_create(self.proxy.create_port, port.Port) def test_port_delete(self): - self.verify_delete(self.proxy.delete_port, port.Port, False) + self.verify_delete(self.proxy.delete_port, port.Port, False, + expected_kwargs={'if_revision': None}) def test_port_delete_ignore(self): - self.verify_delete(self.proxy.delete_port, port.Port, True) + self.verify_delete(self.proxy.delete_port, port.Port, True, + expected_kwargs={'if_revision': None}) + + def test_port_delete_if_revision(self): + self.verify_delete(self.proxy.delete_port, port.Port, True, + method_kwargs={'if_revision': 42}, + expected_kwargs={'if_revision': 42}) def test_port_find(self): self.verify_find(self.proxy.find_port, port.Port) @@ -463,7 +519,16 @@ def test_ports(self): self.verify_list(self.proxy.ports, port.Port) def test_port_update(self): - self.verify_update(self.proxy.update_port, port.Port) + self.verify_update(self.proxy.update_port, port.Port, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': None}) + + def test_port_update_if_revision(self): + self.verify_update(self.proxy.update_port, port.Port, + method_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': 42}, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': 42}) @mock.patch('openstack.network.v2._proxy.Proxy._bulk_create') def test_ports_create(self, bc): @@ -521,7 +586,7 @@ def test_qos_bandwidth_limit_rules(self): def test_qos_bandwidth_limit_rule_update(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) - self._verify2('openstack.proxy.Proxy._update', + self._verify2('openstack.network.v2._proxy.Proxy._update', self.proxy.update_qos_bandwidth_limit_rule, method_args=['rule_id', policy], method_kwargs={'foo': 'bar'}, @@ -578,7 +643,7 @@ def test_qos_dscp_marking_rules(self): def test_qos_dscp_marking_rule_update(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) - self._verify2('openstack.proxy.Proxy._update', + self._verify2('openstack.network.v2._proxy.Proxy._update', self.proxy.update_qos_dscp_marking_rule, method_args=['rule_id', policy], method_kwargs={'foo': 'bar'}, @@ -636,7 +701,7 @@ def test_qos_minimum_bandwidth_rules(self): def test_qos_minimum_bandwidth_rule_update(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) - self._verify2('openstack.proxy.Proxy._update', + self._verify2('openstack.network.v2._proxy.Proxy._update', self.proxy.update_qos_minimum_bandwidth_rule, method_args=['rule_id', policy], method_kwargs={'foo': 'bar'}, @@ -749,10 +814,17 @@ def test_router_create_attrs(self): self.verify_create(self.proxy.create_router, router.Router) def test_router_delete(self): - self.verify_delete(self.proxy.delete_router, router.Router, False) + self.verify_delete(self.proxy.delete_router, router.Router, False, + expected_kwargs={'if_revision': None}) def test_router_delete_ignore(self): - self.verify_delete(self.proxy.delete_router, router.Router, True) + self.verify_delete(self.proxy.delete_router, router.Router, True, + expected_kwargs={'if_revision': None}) + + def test_router_delete_if_revision(self): + self.verify_delete(self.proxy.delete_router, router.Router, True, + method_kwargs={'if_revision': 42}, + expected_kwargs={'if_revision': 42}) def test_router_find(self): self.verify_find(self.proxy.find_router, router.Router) @@ -764,7 +836,16 @@ def test_routers(self): self.verify_list(self.proxy.routers, router.Router) def test_router_update(self): - self.verify_update(self.proxy.update_router, router.Router) + self.verify_update(self.proxy.update_router, router.Router, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': None}) + + def test_router_update_if_revision(self): + self.verify_update(self.proxy.update_router, router.Router, + method_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': 42}, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': 42}) @mock.patch.object(proxy_base.Proxy, '_get_resource') @mock.patch.object(router.Router, 'add_interface') @@ -1010,11 +1091,19 @@ def test_security_group_create_attrs(self): def test_security_group_delete(self): self.verify_delete(self.proxy.delete_security_group, - security_group.SecurityGroup, False) + security_group.SecurityGroup, False, + expected_kwargs={'if_revision': None}) def test_security_group_delete_ignore(self): self.verify_delete(self.proxy.delete_security_group, - security_group.SecurityGroup, True) + security_group.SecurityGroup, True, + expected_kwargs={'if_revision': None}) + + def test_security_group_delete_if_revision(self): + self.verify_delete(self.proxy.delete_security_group, + security_group.SecurityGroup, True, + method_kwargs={'if_revision': 42}, + expected_kwargs={'if_revision': 42}) def test_security_group_find(self): self.verify_find(self.proxy.find_security_group, @@ -1030,7 +1119,17 @@ def test_security_groups(self): def test_security_group_update(self): self.verify_update(self.proxy.update_security_group, - security_group.SecurityGroup) + security_group.SecurityGroup, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': None}) + + def test_security_group_update_if_revision(self): + self.verify_update(self.proxy.update_security_group, + security_group.SecurityGroup, + method_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': 42}, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': 42}) def test_security_group_rule_create_attrs(self): self.verify_create(self.proxy.create_security_group_rule, @@ -1038,11 +1137,19 @@ def test_security_group_rule_create_attrs(self): def test_security_group_rule_delete(self): self.verify_delete(self.proxy.delete_security_group_rule, - security_group_rule.SecurityGroupRule, False) + security_group_rule.SecurityGroupRule, False, + expected_kwargs={'if_revision': None}) def test_security_group_rule_delete_ignore(self): self.verify_delete(self.proxy.delete_security_group_rule, - security_group_rule.SecurityGroupRule, True) + security_group_rule.SecurityGroupRule, True, + expected_kwargs={'if_revision': None}) + + def test_security_group_rule_delete_if_revision(self): + self.verify_delete(self.proxy.delete_security_group_rule, + security_group_rule.SecurityGroupRule, True, + method_kwargs={'if_revision': 42}, + expected_kwargs={'if_revision': 42}) def test_security_group_rule_find(self): self.verify_find(self.proxy.find_security_group_rule, @@ -1081,10 +1188,17 @@ def test_subnet_create_attrs(self): self.verify_create(self.proxy.create_subnet, subnet.Subnet) def test_subnet_delete(self): - self.verify_delete(self.proxy.delete_subnet, subnet.Subnet, False) + self.verify_delete(self.proxy.delete_subnet, subnet.Subnet, False, + expected_kwargs={'if_revision': None}) def test_subnet_delete_ignore(self): - self.verify_delete(self.proxy.delete_subnet, subnet.Subnet, True) + self.verify_delete(self.proxy.delete_subnet, subnet.Subnet, True, + expected_kwargs={'if_revision': None}) + + def test_subnet_delete_if_revision(self): + self.verify_delete(self.proxy.delete_subnet, subnet.Subnet, True, + method_kwargs={'if_revision': 42}, + expected_kwargs={'if_revision': 42}) def test_subnet_find(self): self.verify_find(self.proxy.find_subnet, subnet.Subnet) @@ -1096,7 +1210,9 @@ def test_subnets(self): self.verify_list(self.proxy.subnets, subnet.Subnet) def test_subnet_update(self): - self.verify_update(self.proxy.update_subnet, subnet.Subnet) + self.verify_update(self.proxy.update_subnet, subnet.Subnet, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': None}) def test_subnet_pool_create_attrs(self): self.verify_create(self.proxy.create_subnet_pool, @@ -1244,7 +1360,7 @@ def test_floating_ip_port_forwardings(self): def test_update_floating_ip_port_forwarding(self): fip = floating_ip.FloatingIP.new(id=FIP_ID) - self._verify2('openstack.proxy.Proxy._update', + self._verify2('openstack.network.v2._proxy.Proxy._update', self.proxy.update_floating_ip_port_forwarding, method_args=[fip, 'port_forwarding_id'], method_kwargs={'foo': 'bar'}, diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 1e47d89af..989ab9984 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1750,6 +1750,7 @@ def test_delete(self): self.sot._prepare_request.assert_called_once_with() self.session.delete.assert_called_once_with( self.request.url, + headers='headers', microversion=None) self.sot._translate_response.assert_called_once_with( @@ -1772,6 +1773,7 @@ class Test(resource.Resource): sot._prepare_request.assert_called_once_with() self.session.delete.assert_called_once_with( self.request.url, + headers='headers', microversion='1.42') sot._translate_response.assert_called_once_with( From 7fbc27e3c0a42be1135ba536127f3a29694db1ab Mon Sep 17 00:00:00 2001 From: Adam Harwell Date: Fri, 28 Feb 2020 06:16:03 -0800 Subject: [PATCH 2600/3836] Add availability_zone param to load balancer Parameter availability_zone was added in Octavia API v2.14. Change-Id: I5f3d4eb1357f370a0fba99146825d39e30f4c89c --- openstack/load_balancer/v2/load_balancer.py | 4 +++- openstack/tests/unit/load_balancer/test_load_balancer.py | 4 ++++ ...pport-availability_zone-loadbalancer-a18aa1708d7859e2.yaml | 3 +++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-support-availability_zone-loadbalancer-a18aa1708d7859e2.yaml diff --git a/openstack/load_balancer/v2/load_balancer.py b/openstack/load_balancer/v2/load_balancer.py index b6cf805f3..5614fdc72 100644 --- a/openstack/load_balancer/v2/load_balancer.py +++ b/openstack/load_balancer/v2/load_balancer.py @@ -29,13 +29,15 @@ class LoadBalancer(resource.Resource, resource.TagMixin): 'description', 'flavor_id', 'name', 'project_id', 'provider', 'vip_address', 'vip_network_id', 'vip_port_id', 'vip_subnet_id', 'vip_qos_policy_id', 'provisioning_status', 'operating_status', - is_admin_state_up='admin_state_up', + 'availability_zone', is_admin_state_up='admin_state_up', **resource.TagMixin._tag_query_parameters ) # Properties #: The administrative state of the load balancer *Type: bool* is_admin_state_up = resource.Body('admin_state_up', type=bool) + #: Name of the target Octavia availability zone + availability_zone = resource.Body('availability_zone') #: Timestamp when the load balancer was created created_at = resource.Body('created_at') #: The load balancer description diff --git a/openstack/tests/unit/load_balancer/test_load_balancer.py b/openstack/tests/unit/load_balancer/test_load_balancer.py index ef0c44f60..c06713ecc 100644 --- a/openstack/tests/unit/load_balancer/test_load_balancer.py +++ b/openstack/tests/unit/load_balancer/test_load_balancer.py @@ -19,6 +19,7 @@ IDENTIFIER = 'IDENTIFIER' EXAMPLE = { 'admin_state_up': True, + 'availability_zone': 'my_fake_az', 'created_at': '2017-07-17T12:14:57.233772', 'description': 'fake_description', 'flavor_id': uuid.uuid4(), @@ -64,6 +65,8 @@ def test_basic(self): def test_make_it(self): test_load_balancer = load_balancer.LoadBalancer(**EXAMPLE) self.assertTrue(test_load_balancer.is_admin_state_up) + self.assertEqual(EXAMPLE['availability_zone'], + test_load_balancer.availability_zone) self.assertEqual(EXAMPLE['created_at'], test_load_balancer.created_at) self.assertEqual(EXAMPLE['description'], test_load_balancer.description) @@ -93,6 +96,7 @@ def test_make_it(self): self.assertDictEqual( {'limit': 'limit', 'marker': 'marker', + 'availability_zone': 'availability_zone', 'description': 'description', 'flavor_id': 'flavor_id', 'name': 'name', diff --git a/releasenotes/notes/add-support-availability_zone-loadbalancer-a18aa1708d7859e2.yaml b/releasenotes/notes/add-support-availability_zone-loadbalancer-a18aa1708d7859e2.yaml new file mode 100644 index 000000000..0a0efef26 --- /dev/null +++ b/releasenotes/notes/add-support-availability_zone-loadbalancer-a18aa1708d7859e2.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added availability_zone parameter into load balancer. \ No newline at end of file From 798b754fe56405232430acf910355cafd1560378 Mon Sep 17 00:00:00 2001 From: Lars Kellogg-Stedman Date: Mon, 2 Mar 2020 11:47:49 -0500 Subject: [PATCH 2601/3836] Include user_id attribute in volume information The v2 and v3 apis return a "user_id" attribute in the detail response, but this was missing from the corresponding classes. Change-Id: I3714f6dad7af0bb3a8e818f2e4afd52228d02948 --- openstack/block_storage/v2/volume.py | 2 ++ openstack/block_storage/v3/volume.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/openstack/block_storage/v2/volume.py b/openstack/block_storage/v2/volume.py index f1f1542a3..849351194 100644 --- a/openstack/block_storage/v2/volume.py +++ b/openstack/block_storage/v2/volume.py @@ -79,6 +79,8 @@ class Volume(resource.Resource): host = resource.Body("os-vol-host-attr:host") #: The project ID associated with current back-end. project_id = resource.Body("os-vol-tenant-attr:tenant_id") + #: The user ID associated with the volume + user_id = resource.Body("user_id") #: The status of this volume's migration (None means that a migration #: is not currently in progress). migration_status = resource.Body("os-vol-mig-status-attr:migstat") diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index f1f1542a3..849351194 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -79,6 +79,8 @@ class Volume(resource.Resource): host = resource.Body("os-vol-host-attr:host") #: The project ID associated with current back-end. project_id = resource.Body("os-vol-tenant-attr:tenant_id") + #: The user ID associated with the volume + user_id = resource.Body("user_id") #: The status of this volume's migration (None means that a migration #: is not currently in progress). migration_status = resource.Body("os-vol-mig-status-attr:migstat") From b3068002da8798e75d9c9a98fbc40fde1f68fdfe Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Mon, 2 Mar 2020 16:30:36 +0000 Subject: [PATCH 2602/3836] Include "fields" to "SecurityGroup" query parameters This new query parameter will allow to send a query sending the "fields" parameter. This "fields" parameter contains the needed OVO fields that need to be retrieved from the DB. As commented in the related bug, the OS client "list" command only prints five parameters, none of them the security group rules. In systems with a reasonable amount of security groups, skipping the unnecessary rule load can save a lot of time. Change-Id: Ifa5af28e76bf6017fc6cd1dbd998efbe0e1200bf Related-Bug: #1865223 --- openstack/network/v2/security_group.py | 4 ++-- openstack/tests/unit/network/v2/test_security_group.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openstack/network/v2/security_group.py b/openstack/network/v2/security_group.py index bb456e7f4..e0c9bc8b2 100644 --- a/openstack/network/v2/security_group.py +++ b/openstack/network/v2/security_group.py @@ -26,8 +26,8 @@ class SecurityGroup(resource.Resource, resource.TagMixin): allow_list = True _query_mapping = resource.QueryParameters( - 'description', 'name', 'project_id', 'tenant_id', 'revision_number', - 'sort_dir', 'sort_key', + 'description', 'fields', 'name', 'project_id', 'tenant_id', + 'revision_number', 'sort_dir', 'sort_key', **resource.TagMixin._tag_query_parameters ) diff --git a/openstack/tests/unit/network/v2/test_security_group.py b/openstack/tests/unit/network/v2/test_security_group.py index de2e77cf8..f91833093 100644 --- a/openstack/tests/unit/network/v2/test_security_group.py +++ b/openstack/tests/unit/network/v2/test_security_group.py @@ -78,6 +78,7 @@ def test_basic(self): self.assertDictEqual({'any_tags': 'tags-any', 'description': 'description', + 'fields': 'fields', 'limit': 'limit', 'marker': 'marker', 'name': 'name', From 14973754ceaac78d318a4aacf67bc43e8e1e00a7 Mon Sep 17 00:00:00 2001 From: pedro Date: Thu, 30 Jan 2020 11:58:01 -0300 Subject: [PATCH 2603/3836] Add description field to portforwarding NAT rules Add the `description` field to Floating IP Port Forwardings Depends-On: https://review.opendev.org/#/c/670930/ Change-Id: I38ebdc43800f8b5063af75f0e49ef687fb212b9f Implements: blueprint portforwarding-description Closes-Bug: #1850818 --- openstack/network/v2/port_forwarding.py | 2 ++ .../tests/functional/network/v2/test_port_forwarding.py | 6 +++++- openstack/tests/unit/network/v2/test_port_forwarding.py | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/openstack/network/v2/port_forwarding.py b/openstack/network/v2/port_forwarding.py index 854126009..26b6c2e4b 100644 --- a/openstack/network/v2/port_forwarding.py +++ b/openstack/network/v2/port_forwarding.py @@ -44,3 +44,5 @@ class PortForwarding(resource.Resource): external_port = resource.Body('external_port', type=int) #: The protocol protocol = resource.Body('protocol') + #: The description + description = resource.Body('description') diff --git a/openstack/tests/functional/network/v2/test_port_forwarding.py b/openstack/tests/functional/network/v2/test_port_forwarding.py index 1e39cd679..b1c2884ae 100644 --- a/openstack/tests/functional/network/v2/test_port_forwarding.py +++ b/openstack/tests/functional/network/v2/test_port_forwarding.py @@ -37,6 +37,7 @@ class TestPortForwarding(base.BaseFunctionalTest): INTERNAL_PORT = 8080 EXTERNAL_PORT = 80 PROTOCOL = "tcp" + DESCRIPTION = 'description' def setUp(self): super(TestPortForwarding, self).setUp() @@ -90,7 +91,8 @@ def setUp(self): internal_ip_address=self.INTERNAL_IP_ADDRESS, internal_port=self.INTERNAL_PORT, external_port=self.EXTERNAL_PORT, - protocol=self.PROTOCOL) + protocol=self.PROTOCOL, + description=self.DESCRIPTION) assert isinstance(pf, _port_forwarding.PortForwarding) self.PF = pf @@ -151,6 +153,7 @@ def test_find(self): self.assertEqual(self.INTERNAL_PORT, sot.internal_port) self.assertEqual(self.EXTERNAL_PORT, sot.external_port) self.assertEqual(self.PROTOCOL, sot.protocol) + self.assertEqual(self.DESCRIPTION, sot.description) def test_get(self): sot = self.conn.network.get_port_forwarding( @@ -160,6 +163,7 @@ def test_get(self): self.assertEqual(self.INTERNAL_PORT, sot.internal_port) self.assertEqual(self.EXTERNAL_PORT, sot.external_port) self.assertEqual(self.PROTOCOL, sot.protocol) + self.assertEqual(self.DESCRIPTION, sot.description) def test_list(self): pf_ids = [o.id for o in diff --git a/openstack/tests/unit/network/v2/test_port_forwarding.py b/openstack/tests/unit/network/v2/test_port_forwarding.py index c5a03489c..fd3fbfa7b 100644 --- a/openstack/tests/unit/network/v2/test_port_forwarding.py +++ b/openstack/tests/unit/network/v2/test_port_forwarding.py @@ -22,6 +22,7 @@ 'internal_port': 80, 'internal_port_id': 'internal-port-uuid', 'external_port': 8080, + 'description': 'description' } @@ -57,3 +58,4 @@ def test_make_it(self): self.assertEqual(EXAMPLE['internal_port'], sot.internal_port) self.assertEqual(EXAMPLE['internal_port_id'], sot.internal_port_id) self.assertEqual(EXAMPLE['external_port'], sot.external_port) + self.assertEqual(EXAMPLE['description'], sot.description) From 1d48f780065299597c3a33453877bd9ab1480b83 Mon Sep 17 00:00:00 2001 From: elajkat Date: Wed, 4 Mar 2020 14:39:05 +0100 Subject: [PATCH 2604/3836] Add port property: ip_allocation ip_allocation field is to let Nova know when deferred port binding is in effect. This field can be useful for the user/admin as well. Change-Id: Ia402e39739f8800e50a949f696b3a2877d3b793b --- openstack/network/v2/port.py | 3 +++ openstack/tests/unit/network/v2/test_port.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index 1f96f4329..cc837f36f 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -86,6 +86,9 @@ class Port(resource.Resource, resource.TagMixin): extra_dhcp_opts = resource.Body('extra_dhcp_opts', type=list) #: IP addresses for the port. Includes the IP address and subnet ID. fixed_ips = resource.Body('fixed_ips', type=list) + #: Read-only. The ip_allocation indicates when ports use deferred, + # immediate or no IP allocation. + ip_allocation = resource.Body('ip_allocation') #: The administrative state of the port, which is up ``True`` or #: down ``False``. *Type: bool* is_admin_state_up = resource.Body('admin_state_up', type=bool) diff --git a/openstack/tests/unit/network/v2/test_port.py b/openstack/tests/unit/network/v2/test_port.py index de9b4643f..728bdab96 100644 --- a/openstack/tests/unit/network/v2/test_port.py +++ b/openstack/tests/unit/network/v2/test_port.py @@ -34,6 +34,7 @@ 'extra_dhcp_opts': [{'13': 13}], 'fixed_ips': [{'14': '14'}], 'id': IDENTIFIER, + 'ip_allocation': 'immediate', 'mac_address': '16', 'name': '17', 'network_id': '18', @@ -124,6 +125,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['extra_dhcp_opts'], sot.extra_dhcp_opts) self.assertEqual(EXAMPLE['fixed_ips'], sot.fixed_ips) self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['ip_allocation'], sot.ip_allocation) self.assertEqual(EXAMPLE['mac_address'], sot.mac_address) self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['network_id'], sot.network_id) From 6ba111be29879cf53ff2874466ca82c53e87d47b Mon Sep 17 00:00:00 2001 From: Javier Pena Date: Wed, 4 Mar 2020 14:49:42 +0100 Subject: [PATCH 2605/3836] Replace assertItemsEqual with assertCountEqual assertItemsEqual was removed from Python's unittest.TestCase in Python 3.3 [1][2]. We have been able to use them since then, because testtools required unittest2, which still included it. With testtools removing Python 2.7 support [3][4], we will lose support for assertItemsEqual, so we should switch to use assertCountEqual. [1] - https://bugs.python.org/issue17866 [2] - https://hg.python.org/cpython/rev/d9921cb6e3cd [3] - https://github.com/testing-cabal/testtools/issues/286 [4] - https://github.com/testing-cabal/testtools/pull/277 Change-Id: I095a8aac336e5aa0fbfed64406e33b3b40ff42bc --- openstack/tests/unit/cloud/test_caching.py | 2 +- openstack/tests/unit/cloud/test_floating_ip_pool.py | 2 +- openstack/tests/unit/cloud/test_port.py | 4 ++-- openstack/tests/unit/config/test_config.py | 4 ++-- openstack/tests/unit/test_resource.py | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py index f4e79ee65..66345e27b 100644 --- a/openstack/tests/unit/cloud/test_caching.py +++ b/openstack/tests/unit/cloud/test_caching.py @@ -554,7 +554,7 @@ def test_list_ports_filtered(self): ]}), ]) ports = self.cloud.list_ports(filters={'status': 'DOWN'}) - self.assertItemsEqual([down_port], ports) + self.assertCountEqual([down_port], ports) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_floating_ip_pool.py b/openstack/tests/unit/cloud/test_floating_ip_pool.py index 6d5dbd3d0..ddda50010 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_pool.py +++ b/openstack/tests/unit/cloud/test_floating_ip_pool.py @@ -49,7 +49,7 @@ def test_list_floating_ip_pools(self): floating_ip_pools = self.cloud.list_floating_ip_pools() - self.assertItemsEqual(floating_ip_pools, self.pools) + self.assertCountEqual(floating_ip_pools, self.pools) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_port.py b/openstack/tests/unit/cloud/test_port.py index 53412660c..6067b55fe 100644 --- a/openstack/tests/unit/cloud/test_port.py +++ b/openstack/tests/unit/cloud/test_port.py @@ -238,7 +238,7 @@ def test_list_ports(self): json=self.mock_neutron_port_list_rep) ]) ports = self.cloud.list_ports() - self.assertItemsEqual(self.mock_neutron_port_list_rep['ports'], ports) + self.assertCountEqual(self.mock_neutron_port_list_rep['ports'], ports) self.assert_calls() def test_list_ports_filtered(self): @@ -250,7 +250,7 @@ def test_list_ports_filtered(self): json=self.mock_neutron_port_list_rep) ]) ports = self.cloud.list_ports(filters={'status': 'DOWN'}) - self.assertItemsEqual(self.mock_neutron_port_list_rep['ports'], ports) + self.assertCountEqual(self.mock_neutron_port_list_rep['ports'], ports) self.assert_calls() def test_list_ports_exception(self): diff --git a/openstack/tests/unit/config/test_config.py b/openstack/tests/unit/config/test_config.py index 5a83c2931..a3d35ff68 100644 --- a/openstack/tests/unit/config/test_config.py +++ b/openstack/tests/unit/config/test_config.py @@ -46,7 +46,7 @@ def test_get_all(self): cloud for cloud in base.USER_CONF['clouds'].keys() ] + ['_test_cloud_regions', '_test_cloud_regions'] configured_clouds = [cloud.name for cloud in clouds] - self.assertItemsEqual(user_clouds, configured_clouds) + self.assertCountEqual(user_clouds, configured_clouds) def test_get_all_clouds(self): # Ensure the alias is in place @@ -60,7 +60,7 @@ def test_get_all_clouds(self): cloud for cloud in base.USER_CONF['clouds'].keys() ] + ['_test_cloud_regions', '_test_cloud_regions'] configured_clouds = [cloud.name for cloud in clouds] - self.assertItemsEqual(user_clouds, configured_clouds) + self.assertCountEqual(user_clouds, configured_clouds) def test_get_one(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 1e47d89af..0ac4a3d4b 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -314,7 +314,7 @@ def test_delitem(self): def test_iter(self): attrs = {"key": "value"} sot = resource._ComponentManager(attributes=attrs) - self.assertItemsEqual(iter(attrs), sot.__iter__()) + self.assertCountEqual(iter(attrs), sot.__iter__()) def test_len(self): attrs = {"key": "value"} From 85a89791b384be50f1bbdf4669dccced7ddf8a38 Mon Sep 17 00:00:00 2001 From: Bharat Kunwar Date: Mon, 2 Mar 2020 16:39:00 +0000 Subject: [PATCH 2606/3836] Return uuid alias for coe_cluster in non strict mode At present, coe_cluster only returns the `id` and drops the `uuid` even when strict_mode is False. This is a problem for Ansible modules like os_coe_cluster which expects to find `uuid`. Additionally, add missing test cases for _normalize_coe_cluster and _normalize_cluster_template functions for strict and non-strict modes. Change-Id: I783958bfb4272c5736eb788de2074d0ba946baed Signed-off-by: Bharat Kunwar --- openstack/cloud/_normalize.py | 3 + openstack/tests/unit/cloud/test_normalize.py | 289 +++++++++++++++++++ 2 files changed, 292 insertions(+) diff --git a/openstack/cloud/_normalize.py b/openstack/cloud/_normalize.py index c5f9d760a..be9b5a6bb 100644 --- a/openstack/cloud/_normalize.py +++ b/openstack/cloud/_normalize.py @@ -982,6 +982,9 @@ def _normalize_coe_cluster(self, coe_cluster): location=self._get_current_location(), ) + if not self.strict_mode: + ret['uuid'] = c_id + for key in ( 'status', 'cluster_template_id', diff --git a/openstack/tests/unit/cloud/test_normalize.py b/openstack/tests/unit/cloud/test_normalize.py index 228e09548..86db56771 100644 --- a/openstack/tests/unit/cloud/test_normalize.py +++ b/openstack/tests/unit/cloud/test_normalize.py @@ -181,6 +181,90 @@ 'swap': u'', 'vcpus': 8} +RAW_COE_CLUSTER_TEMPLATE_DICT = { + "insecure_registry": "", + "labels": {}, + "updated_at": "", + "floating_ip_enabled": True, + "fixed_subnet": "", + "master_flavor_id": "ds2G", + "uuid": "7d4935d3-2bdc-4fb0-9e6d-ee4ac201d7f6", + "no_proxy": "", + "https_proxy": "", + "tls_disabled": False, + "keypair_id": "", + "public": False, + "http_proxy": "", + "docker_volume_size": "", + "server_type": "vm", + "external_network_id": "67ecffec-ba11-4698-b7a7-9b3cfd81054f", + "cluster_distro": "fedora-atomic", + "image_id": "Fedora-AtomicHost-29-20191126.0.x86_64", + "volume_driver": "cinder", + "registry_enabled": False, + "docker_storage_driver": "overlay2", + "apiserver_port": "", + "name": "k8s-fedora-atomic-flannel", + "created_at": "2020-02-27T17:16:55+00:00", + "network_driver": "flannel", + "fixed_network": "", + "coe": "kubernetes", + "flavor_id": "ds4G", + "master_lb_enabled": True, + "dns_nameserver": "", + "hidden": False +} + +RAW_COE_CLUSTER_DICT = { + "status": "CREATE_COMPLETE", + "health_status": "HEALTHY", + "cluster_template_id": "697e4b1a-33de-47cf-9181-d93bdfbe6aff", + "node_addresses": [ + "172.24.4.58" + ], + "uuid": "028f8287-5c12-4dae-bbf0-7b76b4d3612d", + "stack_id": "ce2e5b48-dfc9-4981-9fc5-36959ff08d12", + "status_reason": None, + "created_at": "2020-03-02T15:29:28+00:00", + "updated_at": "2020-03-02T15:34:58+00:00", + "coe_version": "v1.17.3", + "labels": { + "auto_healing_enabled": "true", + "auto_scaling_enabled": "true", + "autoscaler_tag": "v1.15.2", + "cloud_provider_tag": "v1.17.0", + "etcd_tag": "3.4.3", + "heat_container_agent_tag": "ussuri-dev", + "ingress_controller": "nginx", + "kube_tag": "v1.17.3", + "master_lb_floating_ip_enabled": "true", + "monitoring_enabled": "true", + "tiller_enabled": "true", + "tiller_tag": "v2.16.3", + "use_podman": "true" + }, + "faults": "", + "keypair": "default", + "api_address": "https://172.24.4.164:6443", + "master_addresses": [ + "172.24.4.70" + ], + "create_timeout": None, + "node_count": 1, + "discovery_url": "https://discovery.etcd.io/abc", + "master_count": 1, + "container_version": "1.12.6", + "name": "k8s", + "master_flavor_id": "ds2G", + "flavor_id": "ds4G", + "health_status_reason": { + "api": "ok", + "k8s-l36u5jjz5kvk-master-0.Ready": "True", + "k8s-l36u5jjz5kvk-node-0.Ready": "True", + }, + "project_id": "4e016477e7394decaf2cc158a7d9c75f" +} + def _assert_server_munch_attributes(testcase, raw, server): testcase.assertEqual(server.flavor.id, raw['flavor']['id']) @@ -767,6 +851,111 @@ def test_normalize_volumes_v2(self): retval = self.cloud._normalize_volume(vol) self.assertEqual(expected, retval) + def test_normalize_coe_cluster_template(self): + coe_cluster_template = RAW_COE_CLUSTER_TEMPLATE_DICT.copy() + expected = { + 'apiserver_port': '', + 'cluster_distro': 'fedora-atomic', + 'coe': 'kubernetes', + 'created_at': '2020-02-27T17:16:55+00:00', + 'dns_nameserver': '', + 'docker_volume_size': '', + 'external_network_id': '67ecffec-ba11-4698-b7a7-9b3cfd81054f', + 'fixed_network': '', + 'fixed_subnet': '', + 'flavor_id': 'ds4G', + 'floating_ip_enabled': True, + 'http_proxy': '', + 'https_proxy': '', + 'id': '7d4935d3-2bdc-4fb0-9e6d-ee4ac201d7f6', + 'image_id': 'Fedora-AtomicHost-29-20191126.0.x86_64', + 'insecure_registry': '', + 'is_public': False, + 'is_registry_enabled': False, + 'is_tls_disabled': False, + 'keypair_id': '', + 'labels': {}, + 'location': {'cloud': '_test_cloud_', + 'project': {'domain_id': None, + 'domain_name': 'default', + 'id': '1c36b64c840a42cd9e9b931a369337f0', + 'name': 'admin'}, + 'region_name': 'RegionOne', + 'zone': None}, + 'master_flavor_id': 'ds2G', + 'name': 'k8s-fedora-atomic-flannel', + 'network_driver': 'flannel', + 'no_proxy': '', + 'properties': {'docker_storage_driver': 'overlay2', + 'hidden': False, + 'master_lb_enabled': True}, + 'public': False, + 'registry_enabled': False, + 'server_type': 'vm', + 'tls_disabled': False, + 'updated_at': '', + 'uuid': '7d4935d3-2bdc-4fb0-9e6d-ee4ac201d7f6', + 'volume_driver': 'cinder', + } + retval = self.cloud._normalize_cluster_template(coe_cluster_template) + self.assertEqual(expected, retval) + + def test_normalize_coe_cluster(self): + coe_cluster = RAW_COE_CLUSTER_DICT.copy() + expected = { + 'cluster_template_id': '697e4b1a-33de-47cf-9181-d93bdfbe6aff', + 'create_timeout': None, + 'id': '028f8287-5c12-4dae-bbf0-7b76b4d3612d', + 'keypair': 'default', + 'location': {'cloud': '_test_cloud_', + 'project': {'domain_id': None, + 'domain_name': 'default', + 'id': '1c36b64c840a42cd9e9b931a369337f0', + 'name': 'admin'}, + 'region_name': 'RegionOne', + 'zone': None}, + 'master_count': 1, + 'name': 'k8s', + 'node_count': 1, + 'properties': {'api_address': 'https://172.24.4.164:6443', + 'coe_version': 'v1.17.3', + 'container_version': '1.12.6', + 'created_at': '2020-03-02T15:29:28+00:00', + 'discovery_url': 'https://discovery.etcd.io/abc', + 'faults': '', + 'flavor_id': 'ds4G', + 'health_status': 'HEALTHY', + 'health_status_reason': { + 'api': 'ok', + 'k8s-l36u5jjz5kvk-master-0.Ready': 'True', + 'k8s-l36u5jjz5kvk-node-0.Ready': 'True'}, + 'labels': { + 'auto_healing_enabled': 'true', + 'auto_scaling_enabled': 'true', + 'autoscaler_tag': 'v1.15.2', + 'cloud_provider_tag': 'v1.17.0', + 'etcd_tag': '3.4.3', + 'heat_container_agent_tag': 'ussuri-dev', + 'ingress_controller': 'nginx', + 'kube_tag': 'v1.17.3', + 'master_lb_floating_ip_enabled': 'true', + 'monitoring_enabled': 'true', + 'tiller_enabled': 'true', + 'tiller_tag': 'v2.16.3', + 'use_podman': 'true'}, + 'master_addresses': ['172.24.4.70'], + 'master_flavor_id': 'ds2G', + 'node_addresses': ['172.24.4.58'], + 'project_id': '4e016477e7394decaf2cc158a7d9c75f', + 'status_reason': None, + 'updated_at': '2020-03-02T15:34:58+00:00'}, + 'stack_id': 'ce2e5b48-dfc9-4981-9fc5-36959ff08d12', + 'status': 'CREATE_COMPLETE', + 'uuid': '028f8287-5c12-4dae-bbf0-7b76b4d3612d', + } + retval = self.cloud._normalize_coe_cluster(coe_cluster) + self.assertEqual(expected, retval) + class TestStrictNormalize(base.TestCase): @@ -1146,3 +1335,103 @@ def test_normalize_volumes_v2(self): } retval = self.cloud._normalize_volume(vol) self.assertEqual(expected, retval) + + def test_normalize_coe_cluster_template(self): + coe_cluster_template = RAW_COE_CLUSTER_TEMPLATE_DICT.copy() + expected = { + 'apiserver_port': '', + 'cluster_distro': 'fedora-atomic', + 'coe': 'kubernetes', + 'created_at': '2020-02-27T17:16:55+00:00', + 'dns_nameserver': '', + 'docker_volume_size': '', + 'external_network_id': '67ecffec-ba11-4698-b7a7-9b3cfd81054f', + 'fixed_network': '', + 'fixed_subnet': '', + 'flavor_id': 'ds4G', + 'http_proxy': '', + 'https_proxy': '', + 'id': '7d4935d3-2bdc-4fb0-9e6d-ee4ac201d7f6', + 'image_id': 'Fedora-AtomicHost-29-20191126.0.x86_64', + 'insecure_registry': '', + 'is_public': False, + 'is_registry_enabled': False, + 'is_tls_disabled': False, + 'keypair_id': '', + 'labels': {}, + 'location': {'cloud': '_test_cloud_', + 'project': {'domain_id': None, + 'domain_name': 'default', + 'id': '1c36b64c840a42cd9e9b931a369337f0', + 'name': 'admin'}, + 'region_name': 'RegionOne', + 'zone': None}, + 'master_flavor_id': 'ds2G', + 'name': 'k8s-fedora-atomic-flannel', + 'network_driver': 'flannel', + 'no_proxy': '', + 'properties': {'docker_storage_driver': 'overlay2', + 'hidden': False, + 'master_lb_enabled': True}, + 'server_type': 'vm', + 'updated_at': '', + 'volume_driver': 'cinder', + } + + retval = self.cloud._normalize_cluster_template(coe_cluster_template) + self.assertEqual(expected, retval) + + def test_normalize_coe_cluster(self): + coe_cluster = RAW_COE_CLUSTER_DICT.copy() + expected = { + 'cluster_template_id': '697e4b1a-33de-47cf-9181-d93bdfbe6aff', + 'create_timeout': None, + 'id': '028f8287-5c12-4dae-bbf0-7b76b4d3612d', + 'keypair': 'default', + 'location': {'cloud': '_test_cloud_', + 'project': {'domain_id': None, + 'domain_name': 'default', + 'id': '1c36b64c840a42cd9e9b931a369337f0', + 'name': 'admin'}, + 'region_name': 'RegionOne', + 'zone': None}, + 'master_count': 1, + 'name': 'k8s', + 'node_count': 1, + 'properties': {'api_address': 'https://172.24.4.164:6443', + 'coe_version': 'v1.17.3', + 'container_version': '1.12.6', + 'created_at': '2020-03-02T15:29:28+00:00', + 'discovery_url': 'https://discovery.etcd.io/abc', + 'faults': '', + 'flavor_id': 'ds4G', + 'health_status': 'HEALTHY', + 'health_status_reason': { + 'api': 'ok', + 'k8s-l36u5jjz5kvk-master-0.Ready': 'True', + 'k8s-l36u5jjz5kvk-node-0.Ready': 'True'}, + 'labels': { + 'auto_healing_enabled': 'true', + 'auto_scaling_enabled': 'true', + 'autoscaler_tag': 'v1.15.2', + 'cloud_provider_tag': 'v1.17.0', + 'etcd_tag': '3.4.3', + 'heat_container_agent_tag': 'ussuri-dev', + 'ingress_controller': 'nginx', + 'kube_tag': 'v1.17.3', + 'master_lb_floating_ip_enabled': 'true', + 'monitoring_enabled': 'true', + 'tiller_enabled': 'true', + 'tiller_tag': 'v2.16.3', + 'use_podman': 'true'}, + 'master_addresses': ['172.24.4.70'], + 'master_flavor_id': 'ds2G', + 'node_addresses': ['172.24.4.58'], + 'project_id': '4e016477e7394decaf2cc158a7d9c75f', + 'status_reason': None, + 'updated_at': '2020-03-02T15:34:58+00:00'}, + 'stack_id': 'ce2e5b48-dfc9-4981-9fc5-36959ff08d12', + 'status': 'CREATE_COMPLETE', + } + retval = self.cloud._normalize_coe_cluster(coe_cluster) + self.assertEqual(expected, retval) From 6f80554807ad2e08d3fe3be69522af0e6f8f3203 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 4 Mar 2020 14:10:27 -0600 Subject: [PATCH 2607/3836] Handle old status-less placement service Older versions of placement didn't include the status field in their version discovery documents. This was fixed in placement in https://review.opendev.org/575117, but there are clouds out there in the wild running this version and SDK bombs out trying to talk to them. Change-Id: I152f0c626b358328cedd404c8fc8d0bee46d2991 --- openstack/config/cloud_region.py | 12 +++++-- openstack/service_description.py | 27 +++++++++++++++- openstack/tests/unit/base.py | 9 +++--- .../tests/unit/fixtures/bad-placement.json | 10 ++++++ openstack/tests/unit/test_placement_rest.py | 32 +++++++++++++++++++ .../notes/old-placement-4b3c34abb8fe7b81.yaml | 6 ++++ 6 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 openstack/tests/unit/fixtures/bad-placement.json create mode 100644 releasenotes/notes/old-placement-4b3c34abb8fe7b81.yaml diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 73d05ae3d..b44c89afc 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -321,6 +321,10 @@ def full_name(self): else: return 'unknown' + def set_service_value(self, key, service_type, value): + key = _make_key(key, service_type) + self.config[key] = value + def set_session_constructor(self, session_constructor): """Sets the Session constructor.""" self._session_constructor = session_constructor @@ -645,7 +649,7 @@ def get_all_version_data(self, service_type): self.get_interface(service_type), {}) return interface_versions.get(service_type, []) - def _get_hardcoded_endpoint(self, service_type, constructor): + def _get_endpoint_from_catalog(self, service_type, constructor): adapter = constructor( session=self.get_session(), service_type=self.get_service_type(service_type), @@ -653,7 +657,11 @@ def _get_hardcoded_endpoint(self, service_type, constructor): interface=self.get_interface(service_type), region_name=self.get_region_name(service_type), ) - endpoint = adapter.get_endpoint() + return adapter.get_endpoint() + + def _get_hardcoded_endpoint(self, service_type, constructor): + endpoint = self._get_endpoint_from_catalog( + service_type, constructor) if not endpoint.rstrip().rsplit('/')[-1] == 'v2.0': if not endpoint.endswith('/'): endpoint += '/' diff --git a/openstack/service_description.py b/openstack/service_description.py index c6cd11e3f..2aad61a6d 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -89,13 +89,38 @@ def __get__(self, instance, owner): # The keystone proxy has a method called get_endpoint # that is about managing keystone endpoints. This is # unfortunate. - endpoint = proxy_mod.Proxy.get_endpoint(proxy) + try: + endpoint = proxy_mod.Proxy.get_endpoint(proxy) + except IndexError: + # It's best not to look to closely here. This is + # to support old placement. + # There was a time when it had no status entry + # in its version discovery doc (OY) In this case, + # no endpoints get through version discovery + # filtering. In order to deal with that, catch + # the IndexError thrown by keystoneauth and + # set an endpoint_override for the user to the + # url in the catalog and try again. + self._set_override_from_catalog(instance.config) + proxy = self._make_proxy(instance) + endpoint = proxy_mod.Proxy.get_endpoint(proxy) if instance._strict_proxies: self._validate_proxy(proxy, endpoint) proxy._connection = instance instance._proxies[self.service_type] = proxy return instance._proxies[self.service_type] + def _set_override_from_catalog(self, config): + override = config._get_endpoint_from_catalog( + self.service_type, + proxy_mod.Proxy, + ) + config.set_service_value( + 'endpoint_override', + self.service_type, + override, + ) + def _validate_proxy(self, proxy, endpoint): exc = None service_url = getattr(proxy, 'skip_discovery', None) diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 9bb7a4d03..dca787917 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -532,9 +532,10 @@ def get_nova_discovery_mock_dict( uri=compute_discovery_url, text=open(discovery_fixture, 'r').read()) - def get_placement_discovery_mock_dict(self): + def get_placement_discovery_mock_dict( + self, discovery_fixture='placement.json'): discovery_fixture = os.path.join( - self.fixtures_directory, "placement.json") + self.fixtures_directory, discovery_fixture) return dict(method='GET', uri="https://placement.example.com/", text=open(discovery_fixture, 'r').read()) @@ -580,9 +581,9 @@ def use_cinder(self): self.__do_register_uris([ self.get_cinder_discovery_mock_dict()]) - def use_placement(self): + def use_placement(self, **kwargs): self.__do_register_uris([ - self.get_placement_discovery_mock_dict()]) + self.get_placement_discovery_mock_dict(**kwargs)]) def use_designate(self): # NOTE(slaweq): This method is only meant to be used in "setUp" diff --git a/openstack/tests/unit/fixtures/bad-placement.json b/openstack/tests/unit/fixtures/bad-placement.json new file mode 100644 index 000000000..72f7fd716 --- /dev/null +++ b/openstack/tests/unit/fixtures/bad-placement.json @@ -0,0 +1,10 @@ +{ + "versions": [ + { + "id": "v1.0", + "links": [{"href": "", "rel": "self"}], + "max_version": "1.17", + "min_version": "1.0" + } + ] +} diff --git a/openstack/tests/unit/test_placement_rest.py b/openstack/tests/unit/test_placement_rest.py index b82b31d52..cd8104997 100644 --- a/openstack/tests/unit/test_placement_rest.py +++ b/openstack/tests/unit/test_placement_rest.py @@ -69,3 +69,35 @@ def test_microversion_discovery(self): (1, 17), self.cloud.placement.get_endpoint_data().max_microversion) self.assert_calls() + + +class TestBadPlacementRest(base.TestCase): + + def setUp(self): + super(TestBadPlacementRest, self).setUp() + # The bad-placement.json is for older placement that was + # missing the status field from its discovery doc. This + # lets us show that we can talk to such a placement. + self.use_placement(discovery_fixture='bad-placement.json') + + def _register_uris(self, status_code=None): + uri = dict( + method='GET', + uri=self.get_mock_url( + 'placement', 'public', append=['allocation_candidates']), + json={}) + if status_code is not None: + uri['status_code'] = status_code + self.register_uris([uri]) + + def _validate_resp(self, resp, status_code): + self.assertEqual(status_code, resp.status_code) + self.assertEqual( + 'https://placement.example.com/allocation_candidates', + resp.url) + self.assert_calls() + + def test_discovery(self): + self._register_uris() + rs = self.cloud.placement.get('/allocation_candidates') + self._validate_resp(rs, 200) diff --git a/releasenotes/notes/old-placement-4b3c34abb8fe7b81.yaml b/releasenotes/notes/old-placement-4b3c34abb8fe7b81.yaml new file mode 100644 index 000000000..402e87343 --- /dev/null +++ b/releasenotes/notes/old-placement-4b3c34abb8fe7b81.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Workaround an issue using openstacksdk with older versions of + the placement service that are missing a status field in + their version discovery doc. From b6a22e3749555ecf9fea762dfc4935e312f6d8bb Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 4 Mar 2020 12:54:57 -0600 Subject: [PATCH 2608/3836] Switch to futurist for concurrency As we're being used inside of services more and those services frequently use things like eventlet, allow people to pass in an Executor object from futurist to override the default ThreadPoolExecutor object. Change-Id: I6c04defc28998d49199383a6cc6d5f5611a99e25 --- lower-constraints.txt | 1 + openstack/cloud/_object_store.py | 13 ------------- openstack/connection.py | 16 +++++++++++++++- .../notes/futurist-b54b0f449d410997.yaml | 8 ++++++++ requirements.txt | 2 +- 5 files changed, 25 insertions(+), 15 deletions(-) create mode 100644 releasenotes/notes/futurist-b54b0f449d410997.yaml diff --git a/lower-constraints.txt b/lower-constraints.txt index 1f491875d..5760bc665 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -9,6 +9,7 @@ extras==1.0.0 fixtures==3.0.0 future==0.16.0 futures==3.0.0 +futurist==2.1.0 ipaddress==1.0.17 iso8601==0.1.11 jmespath==0.9.0 diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index 6bb6fc68d..c781e16c9 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -45,9 +45,6 @@ class ObjectStoreCloudMixin(_normalize.Normalizer): - def __init__(self): - self.__pool_executor = None - @property def _object_store_client(self): if 'object-store' not in self._raw_clients: @@ -55,16 +52,6 @@ def _object_store_client(self): self._raw_clients['object-store'] = raw_client return self._raw_clients['object-store'] - @property - def _pool_executor(self): - if not self.__pool_executor: - # TODO(mordred) Make this configurable - and probably use Futurist - # instead of concurrent.futures so that people using Eventlet will - # be happier. - self.__pool_executor = concurrent.futures.ThreadPoolExecutor( - max_workers=5) - return self.__pool_executor - def list_containers(self, full_listing=True, prefix=None): """List containers. diff --git a/openstack/connection.py b/openstack/connection.py index d77a81f9a..9747a9fb7 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -179,6 +179,7 @@ import warnings import weakref +import futurist import keystoneauth1.exceptions import requestsexceptions import six @@ -276,6 +277,7 @@ def __init__(self, cloud=None, config=None, session=None, service_types=None, global_request_id=None, strict_proxies=False, + pool_executor=None, **kwargs): """Create a connection to a cloud. @@ -343,6 +345,11 @@ def __init__(self, cloud=None, config=None, session=None, where it can be expected that the deployer config is correct and errors should be reported immediately. Default false. + :param pool_executor: + :type pool_executor: :class:`~futurist.Executor` + A futurist ``Executor`` object to be used for concurrent background + activities. Defaults to None in which case a ThreadPoolExecutor + will be created if needed. :param kwargs: If a config is not provided, the rest of the parameters provided are assumed to be arguments to be passed to the CloudRegion constructor. @@ -378,7 +385,7 @@ def __init__(self, cloud=None, config=None, session=None, self._session = None self._proxies = {} - self.__pool_executor = None + self.__pool_executor = pool_executor self._global_request_id = global_request_id self.use_direct_get = use_direct_get self.strict_mode = strict @@ -491,6 +498,13 @@ def authorize(self): except keystoneauth1.exceptions.ClientException as e: raise exceptions.raise_from_response(e.response) + @property + def _pool_executor(self): + if not self.__pool_executor: + self.__pool_executor = futurist.ThreadPoolExecutor( + max_workers=5) + return self.__pool_executor + def close(self): """Release any resources held open.""" if self.__pool_executor: diff --git a/releasenotes/notes/futurist-b54b0f449d410997.yaml b/releasenotes/notes/futurist-b54b0f449d410997.yaml new file mode 100644 index 000000000..1d1ca2095 --- /dev/null +++ b/releasenotes/notes/futurist-b54b0f449d410997.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Switched to the ``futurist`` library for managing background + concurrent tasks. Introduced a new ``pool_executor`` parameter + to `Connection` that allows passing any any futurist Executor + for cases where the default ``ThreadPoolExecutor`` would not + be appropriate. diff --git a/requirements.txt b/requirements.txt index 90a88c6e2..bda4eb9ff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ munch>=2.1.0 # MIT decorator>=4.4.1 # BSD jmespath>=0.9.0 # MIT ipaddress>=1.0.17;python_version<'3.3' # PSF -futures>=3.0.0;python_version=='2.7' or python_version=='2.6' # BSD +futurist>=2.1.0 # Apache-2.0 iso8601>=0.1.11 # MIT netifaces>=0.10.4 # MIT From a03f235b6cf160e60ef91c093440943f2bc9d893 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 5 Mar 2020 12:55:37 -0600 Subject: [PATCH 2609/3836] Fix service_type test for magnum in gate The official type is container-infrastructure-management, which is long but is the official type per service-types-authority. That means it's the key we need to use in has_service. Change-Id: If38b457509c1502a9c9a633f2c0668cba42d2997 --- openstack/tests/functional/cloud/test_cluster_templates.py | 4 +++- openstack/tests/functional/cloud/test_devstack.py | 5 ++++- openstack/tests/functional/cloud/test_magnum_services.py | 4 +++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/openstack/tests/functional/cloud/test_cluster_templates.py b/openstack/tests/functional/cloud/test_cluster_templates.py index bb8975473..729f25905 100644 --- a/openstack/tests/functional/cloud/test_cluster_templates.py +++ b/openstack/tests/functional/cloud/test_cluster_templates.py @@ -29,7 +29,9 @@ class TestClusterTemplate(base.BaseFunctionalTest): def setUp(self): super(TestClusterTemplate, self).setUp() - if not self.user_cloud.has_service('container-infra'): + if not self.user_cloud.has_service( + 'container-infrastructure-management' + ): self.skipTest('Container service not supported by cloud') self.ct = None self.ssh_directory = self.useFixture(fixtures.TempDir()).path diff --git a/openstack/tests/functional/cloud/test_devstack.py b/openstack/tests/functional/cloud/test_devstack.py index f6957fb09..1c72c6aa4 100644 --- a/openstack/tests/functional/cloud/test_devstack.py +++ b/openstack/tests/functional/cloud/test_devstack.py @@ -30,7 +30,10 @@ class TestDevstack(base.BaseFunctionalTest): scenarios = [ ('designate', dict(env='DESIGNATE', service='dns')), ('heat', dict(env='HEAT', service='orchestration')), - ('magnum', dict(env='MAGNUM', service='container-infra')), + ('magnum', dict( + env='MAGNUM', + service='container-infrastructure-management' + )), ('neutron', dict(env='NEUTRON', service='network')), ('octavia', dict(env='OCTAVIA', service='load-balancer')), ('swift', dict(env='SWIFT', service='object-store')), diff --git a/openstack/tests/functional/cloud/test_magnum_services.py b/openstack/tests/functional/cloud/test_magnum_services.py index f3ffd9e82..991b8aac2 100644 --- a/openstack/tests/functional/cloud/test_magnum_services.py +++ b/openstack/tests/functional/cloud/test_magnum_services.py @@ -24,7 +24,9 @@ class TestMagnumServices(base.BaseFunctionalTest): def setUp(self): super(TestMagnumServices, self).setUp() - if not self.operator_cloud.has_service('container-infra'): + if not self.operator_cloud.has_service( + 'container-infrastructure-management' + ): self.skipTest('Container service not supported by cloud') def test_magnum_services(self): From ce00e05aebe32d759eed298012883ebfded97558 Mon Sep 17 00:00:00 2001 From: Bharat Kunwar Date: Wed, 4 Mar 2020 17:21:11 +0000 Subject: [PATCH 2610/3836] Normalise create_coe_cluster{,_template} results At present, create_coe_cluster{,_template} return result that is not normalized which means that notably the `id` field is missing. This PS addresses this issue and the adjacent tests. Change-Id: I3d72304247dd60b3d761b730f020b8722702b6c1 Signed-off-by: Bharat Kunwar Depends-On: https://review.opendev.org/711533 --- openstack/cloud/_coe.py | 4 +-- openstack/cloud/_normalize.py | 3 ++- .../unit/cloud/test_cluster_templates.py | 22 ++++++++-------- .../tests/unit/cloud/test_coe_clusters.py | 25 ++++++++----------- 4 files changed, 26 insertions(+), 28 deletions(-) diff --git a/openstack/cloud/_coe.py b/openstack/cloud/_coe.py index 0a025d3ea..08d6a52a9 100644 --- a/openstack/cloud/_coe.py +++ b/openstack/cloud/_coe.py @@ -110,7 +110,7 @@ def create_coe_cluster( '/clusters', json=body) self.list_coe_clusters.invalidate(self) - return cluster + return self._normalize_coe_cluster(cluster) def delete_coe_cluster(self, name_or_id): """Delete a COE cluster. @@ -326,7 +326,7 @@ def create_cluster_template( '/baymodels', json=body) self.list_cluster_templates.invalidate(self) - return cluster_template + return self._normalize_cluster_template(cluster_template) create_baymodel = create_cluster_template create_coe_cluster_template = create_cluster_template diff --git a/openstack/cloud/_normalize.py b/openstack/cloud/_normalize.py index be9b5a6bb..f13673d56 100644 --- a/openstack/cloud/_normalize.py +++ b/openstack/cloud/_normalize.py @@ -994,7 +994,8 @@ def _normalize_coe_cluster(self, coe_cluster): 'create_timeout', 'node_count', 'name'): - ret[key] = coe_cluster.pop(key) + if key in coe_cluster: + ret[key] = coe_cluster.pop(key) ret['properties'] = coe_cluster return ret diff --git a/openstack/tests/unit/cloud/test_cluster_templates.py b/openstack/tests/unit/cloud/test_cluster_templates.py index e0dc2c31b..dfbea817b 100644 --- a/openstack/tests/unit/cloud/test_cluster_templates.py +++ b/openstack/tests/unit/cloud/test_cluster_templates.py @@ -160,6 +160,11 @@ def test_get_cluster_template_not_found(self): self.assert_calls() def test_create_cluster_template(self): + json_response = cluster_template_obj.toDict() + kwargs = dict(name=cluster_template_obj.name, + image_id=cluster_template_obj.image_id, + keypair_id=cluster_template_obj.keypair_id, + coe=cluster_template_obj.coe) self.register_uris([ dict( method='POST', @@ -168,17 +173,12 @@ def test_create_cluster_template(self): dict( method='POST', uri=self.get_mock_url(resource='baymodels'), - json=dict(baymodels=[cluster_template_obj.toDict()]), - validate=dict(json={ - 'coe': 'fake-coe', - 'image_id': 'fake-image', - 'keypair_id': 'fake-key', - 'name': 'fake-cluster-template'}),)]) - self.cloud.create_cluster_template( - name=cluster_template_obj.name, - image_id=cluster_template_obj.image_id, - keypair_id=cluster_template_obj.keypair_id, - coe=cluster_template_obj.coe) + json=json_response, + validate=dict(json=kwargs)), + ]) + expected = self.cloud._normalize_cluster_template(json_response) + response = self.cloud.create_cluster_template(**kwargs) + self.assertEqual(response, expected) self.assert_calls() def test_create_cluster_template_exception(self): diff --git a/openstack/tests/unit/cloud/test_coe_clusters.py b/openstack/tests/unit/cloud/test_coe_clusters.py index aff92a088..dd8402ebd 100644 --- a/openstack/tests/unit/cloud/test_coe_clusters.py +++ b/openstack/tests/unit/cloud/test_coe_clusters.py @@ -48,7 +48,6 @@ def get_mock_url( append=append, base_url_append=base_url_append) def test_list_coe_clusters(self): - self.register_uris([dict( method='GET', uri=self.get_mock_url(resource='clusters'), @@ -60,21 +59,20 @@ def test_list_coe_clusters(self): self.assert_calls() def test_create_coe_cluster(self): + json_response = dict(uuid=coe_cluster_obj.get('uuid')) + kwargs = dict(name=coe_cluster_obj.name, + cluster_template_id=coe_cluster_obj.cluster_template_id, + master_count=coe_cluster_obj.master_count, + node_count=coe_cluster_obj.node_count) self.register_uris([dict( method='POST', uri=self.get_mock_url(resource='clusters'), - json=dict(baymodels=[coe_cluster_obj.toDict()]), - validate=dict(json={ - 'name': 'k8s', - 'cluster_template_id': '0562d357-8641-4759-8fed-8173f02c9633', - 'master_count': 3, - 'node_count': 10}), - )]) - self.cloud.create_coe_cluster( - name=coe_cluster_obj.name, - cluster_template_id=coe_cluster_obj.cluster_template_id, - master_count=coe_cluster_obj.master_count, - node_count=coe_cluster_obj.node_count) + json=json_response, + validate=dict(json=kwargs)), + ]) + expected = self.cloud._normalize_coe_cluster(json_response) + response = self.cloud.create_coe_cluster(**kwargs) + self.assertEqual(response, expected) self.assert_calls() def test_search_coe_cluster_by_name(self): @@ -91,7 +89,6 @@ def test_search_coe_cluster_by_name(self): self.assert_calls() def test_search_coe_cluster_not_found(self): - self.register_uris([dict( method='GET', uri=self.get_mock_url(resource='clusters'), From 5a6395b683265784a1c43227170e383f4784c90e Mon Sep 17 00:00:00 2001 From: Riccardo Pittau Date: Tue, 3 Mar 2020 15:44:58 +0100 Subject: [PATCH 2611/3836] Add retired and retired_reason fields to baremetal node Recently ironic added the retired and retired_reason fields to the node definition. Adding them here as base to support marking nodes for retirement. Change-Id: I2a80c7653a3e55c89f892d0a90d8c40ea80a482b --- openstack/baremetal/v1/node.py | 10 +++- .../baremetal/test_baremetal_node.py | 56 +++++++++++++++++++ ...metal-retired-fields-f56a4632ad4797d7.yaml | 4 ++ 3 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/baremetal-retired-fields-f56a4632ad4797d7.yaml diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 6bb099c3a..868d6ad3f 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -53,8 +53,8 @@ class Node(_common.ListMixin, resource.Resource): is_maintenance='maintenance', ) - # The allocation_uuid field introduced in 1.52 (Stein). - _max_microversion = '1.52' + # The retired and retired_reason fields introduced in 1.61 (Ussuri). + _max_microversion = '1.61' # Properties #: The UUID of the allocation associated with this node. Added in API @@ -107,6 +107,9 @@ class Node(_common.ListMixin, resource.Resource): # Whether the node is protected from undeploying. Added in API microversion # 1.48. is_protected = resource.Body("protected", type=bool) + #: Whether the node is marked for retirement. Added in API microversion + #: 1.61. + is_retired = resource.Body("retired", type=bool) #: Any error from the most recent transaction that started but failed to #: finish. last_error = resource.Body("last_error") @@ -133,6 +136,9 @@ class Node(_common.ListMixin, resource.Resource): protected_reason = resource.Body("protected_reason") #: The current provisioning state of the node. provision_state = resource.Body("provision_state") + #: The reason why the node is marked for retirement. Added in API + #: microversion 1.61. + retired_reason = resource.Body("retired_reason") #: The current RAID configuration of the node. raid_config = resource.Body("raid_config") #: The name of an service conductor host which is holding a lock on this diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index a08f8e1f3..5a8552de4 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -269,6 +269,62 @@ def test_maintenance_via_update(self): self.assertTrue(node.is_maintenance) self.assertEqual(reason, node.maintenance_reason) + def test_retired(self): + reason = "I'm too old for this s...tuff!" + + node = self.create_node() + + # Set retired when node state available should fail! + self.assertRaises( + exceptions.ConflictException, + self.conn.baremetal.update_node, node, is_retired=True) + + # Set node state to manageable + self.conn.baremetal.set_node_provision_state(node, 'manage', + wait=True) + self.assertEqual(node.provision_state, 'manageable') + + # Set retired without reason + node = self.conn.baremetal.update_node(node, is_retired=True) + self.assertTrue(node.is_retired) + self.assertIsNone(node.retired_reason) + + # Verify set retired on server side + node = self.conn.baremetal.get_node(node.id) + self.assertTrue(node.is_retired) + self.assertIsNone(node.retired_reason) + + # Add the reason + node = self.conn.baremetal.update_node(node, retired_reason=reason) + self.assertTrue(node.is_retired) + self.assertEqual(reason, node.retired_reason) + + # Verify the reason on server side + node = self.conn.baremetal.get_node(node.id) + self.assertTrue(node.is_retired) + self.assertEqual(reason, node.retired_reason) + + # Unset retired + node = self.conn.baremetal.update_node(node, is_retired=False) + self.assertFalse(node.is_retired) + self.assertIsNone(node.retired_reason) + + # Verify on server side + node = self.conn.baremetal.get_node(node.id) + self.assertFalse(node.is_retired) + self.assertIsNone(node.retired_reason) + + # Set retired with reason + node = self.conn.baremetal.update_node(node, is_retired=True, + retired_reason=reason) + self.assertTrue(node.is_retired) + self.assertEqual(reason, node.retired_reason) + + # Verify on server side + node = self.conn.baremetal.get_node(node.id) + self.assertTrue(node.is_retired) + self.assertEqual(reason, node.retired_reason) + class TestBareMetalNodeFields(base.BaseBaremetalTest): diff --git a/releasenotes/notes/baremetal-retired-fields-f56a4632ad4797d7.yaml b/releasenotes/notes/baremetal-retired-fields-f56a4632ad4797d7.yaml new file mode 100644 index 000000000..115febd22 --- /dev/null +++ b/releasenotes/notes/baremetal-retired-fields-f56a4632ad4797d7.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Adds ``is_retired`` and ``retired_reason`` to the baremetal Node schema. \ No newline at end of file From cce94c51cf823232e7a0f684812befe6e114ca13 Mon Sep 17 00:00:00 2001 From: Riccardo Pittau Date: Tue, 3 Mar 2020 11:58:34 +0100 Subject: [PATCH 2612/3836] Extract check temp_url_key logic Move logic to check for the temp_url_key out of generate_from_signature. Change-Id: If36f139294780e7a1c3081ced3bb3ba291085227 --- openstack/object_store/v1/_proxy.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index bbad5e166..624399182 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -730,6 +730,18 @@ def get_temp_url_key(self, container=None): temp_url_key = temp_url_key.encode('utf8') return temp_url_key + def _check_temp_url_key(self, container=None, temp_url_key=None): + if temp_url_key: + if not isinstance(temp_url_key, six.binary_type): + temp_url_key = temp_url_key.encode('utf8') + else: + temp_url_key = self.get_temp_url_key(container) + if not temp_url_key: + raise exceptions.SDKException( + 'temp_url_key was not given, nor was a temporary url key' + ' found for the account or the container.') + return temp_url_key + def generate_form_signature( self, container, object_prefix, redirect_url, max_file_size, max_upload_count, timeout, temp_url_key=None): @@ -766,15 +778,9 @@ def generate_form_signature( raise exceptions.SDKException( 'Please use a positive value.') expires = int(time.time() + int(timeout)) - if temp_url_key: - if not isinstance(temp_url_key, six.binary_type): - temp_url_key = temp_url_key.encode('utf8') - else: - temp_url_key = self.get_temp_url_key(container) - if not temp_url_key: - raise exceptions.SDKException( - 'temp_url_key was not given, nor was a temporary url key' - ' found for the account or the container.') + + temp_url_key = self._check_temp_url_key(container=container, + temp_url_key=temp_url_key) res = self._get_resource(_container.Container, container) endpoint = parse.urlparse(self.get_endpoint()) From a0e68c318fff84303a78fd339e3fa7eec1e9b7b4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 10 Mar 2020 08:17:10 -0500 Subject: [PATCH 2613/3836] Fix aggregate functional test for id restriction Nova just landed a change that validates aggregate ids are ints. This had a side-effect of causing our aggregate test to fail, because we were sending a delete by name not id. It was failing open for us - the delete would have likely not done anything before. Change-Id: I1e151454f39d51db52cdc7a23dbdcbf9b28c9381 --- openstack/cloud/_compute.py | 10 ++++++++++ .../tests/functional/cloud/test_aggregate.py | 6 ++++-- openstack/tests/unit/cloud/test_aggregate.py | 18 ++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 958c07822..5b09d90a1 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -1633,6 +1633,16 @@ def delete_aggregate(self, name_or_id): :raises: OpenStackCloudException on operation error. """ + if ( + isinstance(name_or_id, six.string_types + (six.binary_type,)) + and not name_or_id.isdigit() + ): + aggregate = self.get_aggregate(name_or_id) + if not aggregate: + self.log.debug( + "Aggregate %s not found for deleting", name_or_id) + return False + name_or_id = aggregate.id try: self.compute.delete_aggregate(name_or_id, ignore_missing=False) return True diff --git a/openstack/tests/functional/cloud/test_aggregate.py b/openstack/tests/functional/cloud/test_aggregate.py index 0f06e3a8c..d7bcc0fd4 100644 --- a/openstack/tests/functional/cloud/test_aggregate.py +++ b/openstack/tests/functional/cloud/test_aggregate.py @@ -50,9 +50,11 @@ def test_aggregates(self): ) self.assertNotIn('key', aggregate['metadata']) - self.operator_cloud.delete_aggregate(aggregate_name) + # Validate that we can delete by name + self.assertTrue( + self.operator_cloud.delete_aggregate(aggregate_name)) def cleanup(self, aggregate_name): aggregate = self.operator_cloud.get_aggregate(aggregate_name) if aggregate: - self.operator_cloud.delete_aggregate(aggregate_name) + self.operator_cloud.delete_aggregate(aggregate['id']) diff --git a/openstack/tests/unit/cloud/test_aggregate.py b/openstack/tests/unit/cloud/test_aggregate.py index f6718c2aa..45975bbc1 100644 --- a/openstack/tests/unit/cloud/test_aggregate.py +++ b/openstack/tests/unit/cloud/test_aggregate.py @@ -78,6 +78,24 @@ def test_delete_aggregate(self): self.assert_calls() + def test_delete_aggregate_by_name(self): + self.register_uris([ + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates'] + ), + json={'aggregates': [self.fake_aggregate]}, + ), + dict(method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates', '1'])), + ]) + + self.assertTrue(self.cloud.delete_aggregate(self.aggregate_name)) + + self.assert_calls() + def test_update_aggregate_set_az(self): self.register_uris([ dict(method='GET', From 5db3323be1766c9f6774e17250a4f5021f8478b7 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Mon, 9 Mar 2020 17:50:16 +0100 Subject: [PATCH 2614/3836] Consistent normalization of Machine objects in the cloud layer Currently some functions call node._to_munch, some - _normalize_machine, some both. When only _normalize_machine is used, the machine's fields are not normalized to their server-side representation, breaking the os_ironic ansible module expecting node['uuid']. This change makes _normalize_machine call _to_munch to ensure that normalization always happens. All cloud calls are changed to use _normalize_machine and not _to_munch. Change-Id: Ic431f1340c017a24eafe07832da6e6c579fb1921 --- openstack/cloud/_baremetal.py | 11 +++++------ openstack/cloud/_normalize.py | 7 ++++--- openstack/tests/unit/cloud/test_baremetal_node.py | 6 ++++-- .../notes/normalize-machine-290d9f2a3b3a7ef0.yaml | 4 ++++ 4 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 releasenotes/notes/normalize-machine-290d9f2a3b3a7ef0.yaml diff --git a/openstack/cloud/_baremetal.py b/openstack/cloud/_baremetal.py index 970767604..b3921c55a 100644 --- a/openstack/cloud/_baremetal.py +++ b/openstack/cloud/_baremetal.py @@ -68,7 +68,7 @@ def list_machines(self): :returns: list of ``munch.Munch`` representing machines. """ - return [self._normalize_machine(node._to_munch()) + return [self._normalize_machine(node) for node in self.baremetal.nodes()] def get_machine(self, name_or_id): @@ -83,8 +83,7 @@ def get_machine(self, name_or_id): nodes are found. """ try: - return self._normalize_machine( - self.baremetal.get_node(name_or_id)._to_munch()) + return self._normalize_machine(self.baremetal.get_node(name_or_id)) except exc.OpenStackCloudResourceNotFound: return None @@ -159,7 +158,7 @@ def inspect_machine(self, name_or_id, wait=False, timeout=3600): wait=True, timeout=timeout) - return node._to_munch() + return self._normalize_machine(node) def register_machine(self, nics, wait=False, timeout=3600, lock_timeout=600, **kwargs): @@ -477,7 +476,7 @@ def update_machine(self, name_or_id, **attrs): change_list = [change['path'] for change in patch] node = self.baremetal.update_node(machine, **attrs) return dict( - node=self._normalize_machine(node._to_munch()), + node=self._normalize_machine(node), changes=change_list ) @@ -573,7 +572,7 @@ def node_set_provision_state(self, node = self.baremetal.set_node_provision_state( name_or_id, target=state, config_drive=configdrive, wait=wait, timeout=timeout) - return node._to_munch() + return self._normalize_machine(node) def set_machine_maintenance_state( self, diff --git a/openstack/cloud/_normalize.py b/openstack/cloud/_normalize.py index f13673d56..fc495fa90 100644 --- a/openstack/cloud/_normalize.py +++ b/openstack/cloud/_normalize.py @@ -1172,13 +1172,14 @@ def _normalize_machines(self, machines): def _normalize_machine(self, machine): """Normalize Ironic Machine""" - machine = machine.copy() + if isinstance(machine, resource.Resource): + machine = machine._to_munch() + else: + machine = machine.copy() # Discard noise self._remove_novaclient_artifacts(machine) - # TODO(mordred) Normalize this resource - return machine def _normalize_roles(self, roles): diff --git a/openstack/tests/unit/cloud/test_baremetal_node.py b/openstack/tests/unit/cloud/test_baremetal_node.py index f5aa18519..6c7905eed 100644 --- a/openstack/tests/unit/cloud/test_baremetal_node.py +++ b/openstack/tests/unit/cloud/test_baremetal_node.py @@ -213,8 +213,9 @@ def test_patch_machine(self): json=self.fake_baremetal_node, validate=dict(json=test_patch)), ]) - self.cloud.patch_machine( + result = self.cloud.patch_machine( self.fake_baremetal_node['uuid'], test_patch) + self.assertEqual(self.fake_baremetal_node['uuid'], result['uuid']) self.assert_calls() @@ -759,10 +760,11 @@ def test_node_set_provision_state(self): append=[self.fake_baremetal_node['uuid']]), json=self.fake_baremetal_node), ]) - self.cloud.node_set_provision_state( + result = self.cloud.node_set_provision_state( self.fake_baremetal_node['uuid'], 'active', configdrive='http://host/file') + self.assertEqual(self.fake_baremetal_node['uuid'], result['uuid']) self.assert_calls() diff --git a/releasenotes/notes/normalize-machine-290d9f2a3b3a7ef0.yaml b/releasenotes/notes/normalize-machine-290d9f2a3b3a7ef0.yaml new file mode 100644 index 000000000..04b36e4d8 --- /dev/null +++ b/releasenotes/notes/normalize-machine-290d9f2a3b3a7ef0.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - | + Fixes normalization of bare metal machines in the ``patch_machine`` call. From 51224f04e1c6af997996dee709b1fc478b56dc18 Mon Sep 17 00:00:00 2001 From: Tom Stappaerts Date: Tue, 3 Mar 2020 10:55:13 +0100 Subject: [PATCH 2615/3836] Support for stateless security groups Add support for stateful attribute of security groups, allowing a user to create security groups with stateful false. Change-Id: I380b2ab0fa4f81f676711d97138c6097c4790cd7 Blueprint: stateless-security-groups --- openstack/cloud/_normalize.py | 3 ++ openstack/cloud/_security_group.py | 8 +++- openstack/network/v2/security_group.py | 4 +- openstack/tests/fakes.py | 3 +- openstack/tests/unit/cloud/test_normalize.py | 9 ++++- .../tests/unit/cloud/test_security_groups.py | 40 ++++++++++++++++++- .../unit/network/v2/test_security_group.py | 4 +- ...teful-security-group-f32a78b9bbb49874.yaml | 4 ++ 8 files changed, 67 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/stateful-security-group-f32a78b9bbb49874.yaml diff --git a/openstack/cloud/_normalize.py b/openstack/cloud/_normalize.py index f13673d56..c44283be6 100644 --- a/openstack/cloud/_normalize.py +++ b/openstack/cloud/_normalize.py @@ -388,6 +388,9 @@ def _normalize_secgroup(self, group): ret['description'] = group.pop('description') ret['properties'] = group + if self._use_neutron_secgroups(): + ret['stateful'] = group.pop('stateful', True) + # Backwards compat with Neutron if not self.strict_mode: ret['tenant_id'] = project_id diff --git a/openstack/cloud/_security_group.py b/openstack/cloud/_security_group.py index 7142e69b1..1e31e659b 100644 --- a/openstack/cloud/_security_group.py +++ b/openstack/cloud/_security_group.py @@ -115,7 +115,8 @@ def get_security_group_by_id(self, id): return self._normalize_secgroup( self._get_and_munchify('security_group', data)) - def create_security_group(self, name, description, project_id=None): + def create_security_group(self, name, description, + project_id=None, stateful=None): """Create a new security group :param string name: A name for the security group. @@ -123,6 +124,7 @@ def create_security_group(self, name, description, project_id=None): :param string project_id: Specify the project ID this security group will be created on (admin-only). + :param string stateful: Whether the security group is stateful or not. :returns: A ``munch.Munch`` representing the new security group. @@ -141,6 +143,8 @@ def create_security_group(self, name, description, project_id=None): security_group_json = { 'name': name, 'description': description } + if stateful is not None: + security_group_json['stateful'] = stateful if project_id is not None: security_group_json['tenant_id'] = project_id if self._use_neutron_secgroups(): @@ -188,7 +192,7 @@ def delete_security_group(self, name_or_id): '/os-security-groups/{id}'.format(id=secgroup['id']))) return True - @_utils.valid_kwargs('name', 'description') + @_utils.valid_kwargs('name', 'description', 'stateful') def update_security_group(self, name_or_id, **kwargs): """Update a security group diff --git a/openstack/network/v2/security_group.py b/openstack/network/v2/security_group.py index 6b772f639..5c9cc2d97 100644 --- a/openstack/network/v2/security_group.py +++ b/openstack/network/v2/security_group.py @@ -27,7 +27,7 @@ class SecurityGroup(_base.NetworkResource, resource.TagMixin): allow_list = True _query_mapping = resource.QueryParameters( - 'description', 'fields', 'name', 'project_id', 'tenant_id', + 'description', 'fields', 'name', 'stateful', 'project_id', 'tenant_id', 'revision_number', 'sort_dir', 'sort_key', **resource.TagMixin._tag_query_parameters ) @@ -39,6 +39,8 @@ class SecurityGroup(_base.NetworkResource, resource.TagMixin): description = resource.Body('description') #: The security group name. name = resource.Body('name') + #: Whether the security group is stateful or not. + stateful = resource.Body('stateful') #: The ID of the project this security group is associated with. project_id = resource.Body('project_id') #: A list of diff --git a/openstack/tests/fakes.py b/openstack/tests/fakes.py index fc3033072..9fae3c7f5 100644 --- a/openstack/tests/fakes.py +++ b/openstack/tests/fakes.py @@ -402,7 +402,7 @@ def __init__(self, id, address, node_id): def make_fake_neutron_security_group( - id, name, description, rules, project_id=None): + id, name, description, rules, stateful=True, project_id=None): if not rules: rules = [] if not project_id: @@ -411,6 +411,7 @@ def make_fake_neutron_security_group( 'id': id, 'name': name, 'description': description, + 'stateful': stateful, 'project_id': project_id, 'tenant_id': project_id, 'security_group_rules': rules, diff --git a/openstack/tests/unit/cloud/test_normalize.py b/openstack/tests/unit/cloud/test_normalize.py index 86db56771..c0f9fbe5f 100644 --- a/openstack/tests/unit/cloud/test_normalize.py +++ b/openstack/tests/unit/cloud/test_normalize.py @@ -698,8 +698,11 @@ def test_normalize_secgroups(self): cloud='_test_cloud_')) ] ) - + # Set secgroup source to nova for this test as stateful parameter + # is only valid for neutron security groups. + self.cloud.secgroup_source = 'nova' retval = self.cloud._normalize_secgroup(nova_secgroup) + self.cloud.secgroup_source = 'neutron' self.assertEqual(expected, retval) def test_normalize_secgroups_negone_port(self): @@ -1239,7 +1242,11 @@ def test_normalize_secgroups(self): ] ) + # Set secgroup source to nova for this test as stateful parameter + # is only valid for neutron security groups. + self.cloud.secgroup_source = 'nova' retval = self.cloud._normalize_secgroup(nova_secgroup) + self.cloud.secgroup_source = 'neutron' self.assertEqual(expected, retval) def test_normalize_volumes_v1(self): diff --git a/openstack/tests/unit/cloud/test_security_groups.py b/openstack/tests/unit/cloud/test_security_groups.py index 3b62f79fa..152455514 100644 --- a/openstack/tests/unit/cloud/test_security_groups.py +++ b/openstack/tests/unit/cloud/test_security_groups.py @@ -176,6 +176,7 @@ def test_create_security_group_neutron(self): r = self.cloud.create_security_group(group_name, group_desc) self.assertEqual(group_name, r['name']) self.assertEqual(group_desc, r['description']) + self.assertEqual(True, r['stateful']) self.assert_calls() @@ -216,6 +217,37 @@ def test_create_security_group_neutron_specific_tenant(self): self.assert_calls() + def test_create_security_group_stateless_neutron(self): + self.cloud.secgroup_source = 'neutron' + group_name = self.getUniqueString() + group_desc = self.getUniqueString('description') + new_group = fakes.make_fake_neutron_security_group( + id='2', + name=group_name, + description=group_desc, + stateful=False, + rules=[]) + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'security-groups']), + json={'security_group': new_group}, + validate=dict( + json={'security_group': { + 'name': group_name, + 'description': group_desc, + 'stateful': False + }})) + ]) + + r = self.cloud.create_security_group(group_name, group_desc, + stateful=False) + self.assertEqual(group_name, r['name']) + self.assertEqual(group_desc, r['description']) + self.assertEqual(False, r['stateful']) + self.assert_calls() + def test_create_security_group_nova(self): group_name = self.getUniqueString() self.has_neutron = False @@ -257,6 +289,7 @@ def test_update_security_group_neutron(self): sg_id = neutron_grp_dict['id'] update_return = neutron_grp_dict.copy() update_return['name'] = new_name + update_return['stateful'] = False self.register_uris([ dict(method='GET', uri=self.get_mock_url( @@ -269,10 +302,12 @@ def test_update_security_group_neutron(self): append=['v2.0', 'security-groups', '%s' % sg_id]), json={'security_group': update_return}, validate=dict(json={ - 'security_group': {'name': new_name}})) + 'security_group': {'name': new_name, 'stateful': False}})) ]) - r = self.cloud.update_security_group(sg_id, name=new_name) + r = self.cloud.update_security_group(sg_id, name=new_name, + stateful=False) self.assertEqual(r['name'], new_name) + self.assertEqual(r['stateful'], False) self.assert_calls() def test_update_security_group_nova(self): @@ -793,6 +828,7 @@ def test_get_security_group_by_id_neutron(self): self.assertEqual(neutron_grp_dict['name'], ret_sg['name']) self.assertEqual(neutron_grp_dict['description'], ret_sg['description']) + self.assertEqual(neutron_grp_dict['stateful'], ret_sg['stateful']) self.assert_calls() def test_get_security_group_by_id_nova(self): diff --git a/openstack/tests/unit/network/v2/test_security_group.py b/openstack/tests/unit/network/v2/test_security_group.py index f91833093..82cda9845 100644 --- a/openstack/tests/unit/network/v2/test_security_group.py +++ b/openstack/tests/unit/network/v2/test_security_group.py @@ -54,6 +54,7 @@ 'description': '1', 'id': IDENTIFIER, 'name': '2', + 'stateful': True, 'revision_number': 3, 'security_group_rules': RULES, 'tenant_id': '4', @@ -89,7 +90,8 @@ def test_basic(self): 'sort_dir': 'sort_dir', 'sort_key': 'sort_key', 'tags': 'tags', - 'tenant_id': 'tenant_id' + 'tenant_id': 'tenant_id', + 'stateful': 'stateful', }, sot._query_mapping._mapping) diff --git a/releasenotes/notes/stateful-security-group-f32a78b9bbb49874.yaml b/releasenotes/notes/stateful-security-group-f32a78b9bbb49874.yaml new file mode 100644 index 000000000..d0d8945e5 --- /dev/null +++ b/releasenotes/notes/stateful-security-group-f32a78b9bbb49874.yaml @@ -0,0 +1,4 @@ +--- +features: + - New stateful parameter can be used in security group + From 1c34e2a205467e384718bec314faf2097f33af62 Mon Sep 17 00:00:00 2001 From: Bram Verschueren Date: Fri, 11 Oct 2019 20:52:18 +0200 Subject: [PATCH 2616/3836] Add support for additional volume backup options This adds support for creating incremental volume backups and volume backups from snapshot to the cloud layer. Change-Id: I8516b63289d18587a5e4d17216393e18d2460211 --- openstack/cloud/_block_storage.py | 8 +- .../functional/cloud/test_volume_backup.py | 35 ++++++ .../tests/unit/cloud/test_volume_backups.py | 111 ++++++++++++++++++ 3 files changed, 153 insertions(+), 1 deletion(-) diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index f78cd6919..8759e321c 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -630,7 +630,9 @@ def get_volume_snapshot(self, name_or_id, filters=None): filters) def create_volume_backup(self, volume_id, name=None, description=None, - force=False, wait=True, timeout=None): + force=False, wait=True, timeout=None, + incremental=False, snapshot_id=None): + """Create a volume backup. :param volume_id: the ID of the volume to backup. @@ -643,6 +645,8 @@ def create_volume_backup(self, volume_id, name=None, description=None, :param wait: If true, waits for volume backup to be created. :param timeout: Seconds to wait for volume backup creation. None is forever. + :param incremental: If set to true, the backup will be incremental. + :param snapshot_id: The UUID of the source snapshot to back up. :returns: The created volume backup object. @@ -654,6 +658,8 @@ def create_volume_backup(self, volume_id, name=None, description=None, 'volume_id': volume_id, 'description': description, 'force': force, + 'incremental': incremental, + 'snapshot_id': snapshot_id, } resp = self.block_storage.post( diff --git a/openstack/tests/functional/cloud/test_volume_backup.py b/openstack/tests/functional/cloud/test_volume_backup.py index 61eabfeae..c864f88b8 100644 --- a/openstack/tests/functional/cloud/test_volume_backup.py +++ b/openstack/tests/functional/cloud/test_volume_backup.py @@ -44,6 +44,41 @@ def test_create_get_delete_volume_backup(self): self.user_cloud.delete_volume_backup(backup['id'], wait=True) self.assertIsNone(self.user_cloud.get_volume_backup(backup['id'])) + def test_create_get_delete_volume_backup_from_snapshot(self): + volume = self.user_cloud.create_volume(size=1) + snapshot = self.user_cloud.create_volume_snapshot(volume['id']) + self.addCleanup(self.user_cloud.delete_volume, volume['id']) + self.addCleanup(self.user_cloud.delete_volume_snapshot, snapshot['id'], + wait=True) + + backup = self.user_cloud.create_volume_backup( + volume_id=volume['id'], snapshot_id=snapshot['id'], wait=True) + + backup = self.user_cloud.get_volume_backup(backup['id']) + self.assertEqual(backup['snapshot_id'], snapshot['id']) + + self.user_cloud.delete_volume_backup(backup['id'], wait=True) + self.assertIsNone(self.user_cloud.get_volume_backup(backup['id'])) + + def test_create_get_delete_incremental_volume_backup(self): + volume = self.user_cloud.create_volume(size=1) + self.addCleanup(self.user_cloud.delete_volume, volume['id']) + + full_backup = self.user_cloud.create_volume_backup( + volume_id=volume['id'], wait=True) + incr_backup = self.user_cloud.create_volume_backup( + volume_id=volume['id'], incremental=True, wait=True) + + full_backup = self.user_cloud.get_volume_backup(full_backup['id']) + incr_backup = self.user_cloud.get_volume_backup(incr_backup['id']) + self.assertEqual(full_backup['has_dependent_backups'], True) + self.assertEqual(incr_backup['is_incremental'], True) + + self.user_cloud.delete_volume_backup(incr_backup['id'], wait=True) + self.user_cloud.delete_volume_backup(full_backup['id'], wait=True) + self.assertIsNone(self.user_cloud.get_volume_backup(full_backup['id'])) + self.assertIsNone(self.user_cloud.get_volume_backup(incr_backup['id'])) + def test_list_volume_backups(self): vol1 = self.user_cloud.create_volume( display_name=self.getUniqueString(), size=1) diff --git a/openstack/tests/unit/cloud/test_volume_backups.py b/openstack/tests/unit/cloud/test_volume_backups.py index 7bec56429..5e8ff82bc 100644 --- a/openstack/tests/unit/cloud/test_volume_backups.py +++ b/openstack/tests/unit/cloud/test_volume_backups.py @@ -125,3 +125,114 @@ def test_delete_volume_backup_force(self): ]) self.cloud.delete_volume_backup(backup_id, True, True, 1) self.assert_calls() + + def test_create_volume_backup(self): + volume_id = '1234' + backup_name = 'bak1' + bak1 = { + 'id': '5678', + 'volume_id': volume_id, + 'status': 'available', + 'name': backup_name + } + + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['backups']), + json={'backup': bak1}, + validate=dict(json={ + 'backup': { + 'name': backup_name, + 'volume_id': volume_id, + 'description': None, + 'force': False, + 'snapshot_id': None, + 'incremental': False + } + })), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['backups', 'detail']), + json={"backups": [bak1]}), + + ]) + self.cloud.create_volume_backup(volume_id, name=backup_name) + self.assert_calls() + + def test_create_incremental_volume_backup(self): + volume_id = '1234' + backup_name = 'bak1' + bak1 = { + 'id': '5678', + 'volume_id': volume_id, + 'status': 'available', + 'name': backup_name + } + + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['backups']), + json={'backup': bak1}, + validate=dict(json={ + 'backup': { + 'name': backup_name, + 'volume_id': volume_id, + 'description': None, + 'force': False, + 'snapshot_id': None, + 'incremental': True + } + })), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['backups', 'detail']), + json={"backups": [bak1]}), + + ]) + self.cloud.create_volume_backup(volume_id, name=backup_name, + incremental=True) + self.assert_calls() + + def test_create_volume_backup_from_snapshot(self): + volume_id = '1234' + backup_name = 'bak1' + snapshot_id = '5678' + bak1 = { + 'id': '5678', + 'volume_id': volume_id, + 'status': 'available', + 'name': 'bak1' + } + + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['backups']), + json={'backup': bak1}, + validate=dict(json={ + 'backup': { + 'name': backup_name, + 'volume_id': volume_id, + 'description': None, + 'force': False, + 'snapshot_id': snapshot_id, + 'incremental': False + } + })), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['backups', 'detail']), + json={"backups": [bak1]}), + + ]) + self.cloud.create_volume_backup(volume_id, name=backup_name, + snapshot_id=snapshot_id) + self.assert_calls() From 9231c8723ae185842ce063ab7b3e72f0862c715e Mon Sep 17 00:00:00 2001 From: Riccardo Pittau Date: Mon, 9 Mar 2020 18:41:08 +0100 Subject: [PATCH 2617/3836] Set min version to test node retirement Splitting functional tests for node retirement to a different class to set minimum supported version. Change-Id: Ib9b6dba8f5fbb870faecf03ae0e49cfe814d31e0 --- openstack/tests/functional/baremetal/test_baremetal_node.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index 5a8552de4..e6616b16f 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -269,6 +269,11 @@ def test_maintenance_via_update(self): self.assertTrue(node.is_maintenance) self.assertEqual(reason, node.maintenance_reason) + +class TestNodeRetired(base.BaseBaremetalTest): + + min_microversion = '1.61' + def test_retired(self): reason = "I'm too old for this s...tuff!" From 2d844d775a722bf15243214a3c406f37a35e23c4 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Wed, 11 Mar 2020 11:29:26 +0100 Subject: [PATCH 2618/3836] Fix microversion negotiation in some bare metal node call Using utils.pick_microversion means that the result may be None, which is likely lower than a version negotiated for the resource. For example, when calling set_node_provision_state(, "provide"), it is determined that "provide" does not require a non-default microversion, so None is used, breaking using node name. This change switches set_node_provision_state, set_node_power_state and patch_node to _assert_microversion_for that takes into account the microversion negotiated for the resource. Change-Id: Ia81d8a39ca1c8407c689e7d128ace82071b52a01 --- openstack/baremetal/v1/_common.py | 9 +++++++++ openstack/baremetal/v1/node.py | 11 ++++++----- openstack/resource.py | 5 ++++- .../baremetal/test_baremetal_node.py | 19 +++++++++++++++++++ .../tests/unit/baremetal/v1/test_node.py | 7 +++++++ ...on-state-negotiation-0155b4d0e932054c.yaml | 12 ++++++++++++ 6 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/provision-state-negotiation-0155b4d0e932054c.yaml diff --git a/openstack/baremetal/v1/_common.py b/openstack/baremetal/v1/_common.py index 4217c2909..8fcf7b579 100644 --- a/openstack/baremetal/v1/_common.py +++ b/openstack/baremetal/v1/_common.py @@ -58,6 +58,15 @@ VIF_VERSION = '1.28' """API version in which the VIF operations were introduced.""" +CONFIG_DRIVE_REBUILD_VERSION = '1.35' +"""API version in which rebuild accepts a configdrive.""" + +RESET_INTERFACES_VERSION = '1.45' +"""API version in which the reset_interfaces parameter was introduced.""" + +CONFIG_DRIVE_DICT_VERSION = '1.56' +"""API version in which configdrive can be a dictionary.""" + class ListMixin(object): diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 868d6ad3f..7a515af8f 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -346,11 +346,11 @@ def set_provision_state(self, session, target, config_drive=None, if config_drive: # Some config drive actions require a higher version. if isinstance(config_drive, dict): - version = '1.56' + version = _common.CONFIG_DRIVE_DICT_VERSION elif target == 'rebuild': - version = '1.35' + version = _common.CONFIG_DRIVE_REBUILD_VERSION - version = utils.pick_microversion(session, version) + version = self._assert_microversion_for(session, 'commit', version) body = {'target': target} if config_drive: @@ -532,7 +532,7 @@ def set_power_state(self, session, target): else: version = None - version = utils.pick_microversion(session, version) + version = self._assert_microversion_for(session, 'commit', version) # TODO(dtantsur): server timeout support body = {'target': target} @@ -854,7 +854,8 @@ def patch(self, session, patch=None, prepend_key=True, has_body=True, raise exceptions.MethodNotSupported(self, "patch") session = self._get_session(session) - microversion = utils.pick_microversion(session, '1.45') + microversion = self._assert_microversion_for( + session, 'commit', _common.RESET_INTERFACES_VERSION) params = [('reset_interfaces', reset_interfaces)] request = self._prepare_request(requires_id=True, diff --git a/openstack/resource.py b/openstack/resource.py index 6cefb5bd8..f48ef2331 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1219,7 +1219,10 @@ def _raise(message): raise exceptions.NotSupported(message) actual = self._get_microversion_for(session, action) - if actual is None: + + if expected is None: + return actual + elif actual is None: message = ("API version %s is required, but the default " "version will be used.") % expected _raise(message) diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index 5a8552de4..88e6f8011 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import random import uuid from openstack import exceptions @@ -154,6 +155,24 @@ def test_node_create_in_enroll_provide(self): wait=True) self.assertEqual(node.provision_state, 'available') + def test_node_create_in_enroll_provide_by_name(self): + name = 'node-%d' % random.randint(0, 1000) + node = self.create_node(provision_state='enroll', name=name) + self.node_id = node.id + + self.assertEqual(node.driver, 'fake-hardware') + self.assertEqual(node.provision_state, 'enroll') + self.assertIsNone(node.power_state) + self.assertFalse(node.is_maintenance) + + node = self.conn.baremetal.set_node_provision_state(name, 'manage', + wait=True) + self.assertEqual(node.provision_state, 'manageable') + + node = self.conn.baremetal.set_node_provision_state(name, 'provide', + wait=True) + self.assertEqual(node.provision_state, 'available') + def test_node_power_state(self): node = self.create_node() self.assertIsNone(node.power_state) diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index a1a4dc04f..c2c1b5ab1 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -229,6 +229,11 @@ def _get_side_effect(_self, session): abort_on_failed_state=False) +def _fake_assert(self, session, action, expected, error_message=None): + return expected + + +@mock.patch.object(node.Node, '_assert_microversion_for', _fake_assert) @mock.patch.object(node.Node, 'fetch', lambda self, session: self) @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) class TestNodeSetProvisionState(base.TestCase): @@ -558,6 +563,7 @@ def test_timeout(self, mock_fetch): mock_fetch.assert_called_with(self.node, self.session) +@mock.patch.object(node.Node, '_assert_microversion_for', _fake_assert) @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) class TestNodeSetPowerState(base.TestCase): @@ -769,6 +775,7 @@ def test_set_traits(self): retriable_status_codes=_common.RETRIABLE_STATUS_CODES) +@mock.patch.object(node.Node, '_assert_microversion_for', _fake_assert) @mock.patch.object(resource.Resource, 'patch', autospec=True) class TestNodePatch(base.TestCase): diff --git a/releasenotes/notes/provision-state-negotiation-0155b4d0e932054c.yaml b/releasenotes/notes/provision-state-negotiation-0155b4d0e932054c.yaml new file mode 100644 index 000000000..3656cf9b3 --- /dev/null +++ b/releasenotes/notes/provision-state-negotiation-0155b4d0e932054c.yaml @@ -0,0 +1,12 @@ +--- +fixes: + - | + Fixes API version negotiation in the following bare metal node calls: + + * ``set_node_provision_state`` + * ``set_node_power_state`` + * ``patch_node`` + + Previously an unexpectingly low version could be negotiated, breaking + certain features, for example calling the ``provide`` provisioning action + with a node name. From 54a76faf72ffaf47f276c84010915ac855b298c0 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 11 Mar 2020 14:03:59 -0500 Subject: [PATCH 2619/3836] Rationalize examples and functional extra config loading This whole thing is a mess. examples is doing the right thing and using get_extra_config, but its docs are wrong. functional is reading config from a non-standard place but is the place that the examples docs incorrect say it should be read from. Update both places to read from the RIGHT location and update the docs as well. Make examples support setting a key to use and then set that key in tox so that we can just use the functional settings for both. Also, fix the clustering functional test so that it consumes the IMAGE_NAME discovered in functional.base. Change-Id: Ia9f150bbdd3825cb3e0d4282af3ca8f23dc1d888 --- doc/source/contributor/clouds.yaml | 8 ++-- examples/connect.py | 4 +- openstack/tests/functional/base.py | 7 ++-- .../tests/functional/cloud/test_clustering.py | 38 +++++++++---------- tox.ini | 1 + 5 files changed, 30 insertions(+), 28 deletions(-) diff --git a/doc/source/contributor/clouds.yaml b/doc/source/contributor/clouds.yaml index 04cd4aaf3..b92ba85b7 100644 --- a/doc/source/contributor/clouds.yaml +++ b/doc/source/contributor/clouds.yaml @@ -6,10 +6,6 @@ clouds: username: demo password: secrete project_name: demo - example: - image_name: fedora-20.x86_64 - flavor_name: m1.small - network_name: private rackspace: cloud: rackspace auth: @@ -17,3 +13,7 @@ clouds: password: joes-password project_name: 123123 region_name: IAD +example: + image_name: fedora-20.x86_64 + flavor_name: m1.small + network_name: private diff --git a/examples/connect.py b/examples/connect.py index 4a4eaa92f..1d4b86ad8 100644 --- a/examples/connect.py +++ b/examples/connect.py @@ -31,6 +31,7 @@ #: will determine where the examples will be run and what resource defaults #: will be used to run the examples. TEST_CLOUD = os.getenv('OS_TEST_CLOUD', 'devstack-admin') +EXAMPLE_CONFIG_KEY = os.getenv('OPENSTACKSDK_EXAMPLE_CONFIG_KEY', 'example') config = loader.OpenStackConfig() cloud = openstack.connect(cloud=TEST_CLOUD) @@ -44,7 +45,8 @@ def __init__(self, cloud_name='devstack-admin', debug=False): def _get_resource_value(resource_key, default): - return config.get_extra_config('example').get(resource_key, default) + return config.get_extra_config( + EXAMPLE_CONFIG_KEY).get(resource_key, default) SERVER_NAME = 'openstacksdk-example' diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index 2c2a154a6..1a19153ba 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -22,15 +22,14 @@ #: file, typically in $HOME/.config/openstack/clouds.yaml. That configuration #: will determine where the functional tests will be run and what resource #: defaults will be used to run the functional tests. +TEST_CONFIG = openstack.config.OpenStackConfig() TEST_CLOUD_NAME = os.getenv('OS_CLOUD', 'devstack-admin') TEST_CLOUD_REGION = openstack.config.get_cloud_region(cloud=TEST_CLOUD_NAME) def _get_resource_value(resource_key, default): - try: - return TEST_CLOUD_REGION.config['functional'][resource_key] - except KeyError: - return default + return TEST_CONFIG.get_extra_config( + 'functional').get(resource_key, default) def _disable_keep_alive(conn): diff --git a/openstack/tests/functional/cloud/test_clustering.py b/openstack/tests/functional/cloud/test_clustering.py index 9c3f775c2..75645b6da 100644 --- a/openstack/tests/functional/cloud/test_clustering.py +++ b/openstack/tests/functional/cloud/test_clustering.py @@ -116,7 +116,7 @@ def test_create_profile(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.4.0-x86_64-disk", + "image": base.IMAGE_NAME, "networks": [ { "network": "private" @@ -146,7 +146,7 @@ def test_create_cluster(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.4.0-x86_64-disk", + "image": base.IMAGE_NAME, "networks": [ { "network": "private" @@ -190,7 +190,7 @@ def test_get_cluster_by_id(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.4.0-x86_64-disk", + "image": base.IMAGE_NAME, "networks": [ { "network": "private" @@ -233,7 +233,7 @@ def test_update_cluster(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.4.0-x86_64-disk", + "image": base.IMAGE_NAME, "networks": [ { "network": "private" @@ -319,7 +319,7 @@ def test_attach_policy_to_cluster(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.4.0-x86_64-disk", + "image": base.IMAGE_NAME, "networks": [ { "network": "private" @@ -393,7 +393,7 @@ def test_detach_policy_from_cluster(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.4.0-x86_64-disk", + "image": base.IMAGE_NAME, "networks": [ { "network": "private" @@ -475,7 +475,7 @@ def test_get_policy_on_cluster_by_id(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.4.0-x86_64-disk", + "image": base.IMAGE_NAME, "networks": [ { "network": "private" @@ -568,7 +568,7 @@ def test_list_policies_on_cluster(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.4.0-x86_64-disk", + "image": base.IMAGE_NAME, "networks": [ { "network": "private" @@ -657,7 +657,7 @@ def test_create_cluster_receiver(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.4.0-x86_64-disk", + "image": base.IMAGE_NAME, "networks": [ { "network": "private" @@ -714,7 +714,7 @@ def test_list_cluster_receivers(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.4.0-x86_64-disk", + "image": base.IMAGE_NAME, "networks": [ { "network": "private" @@ -776,7 +776,7 @@ def test_delete_cluster(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.4.0-x86_64-disk", + "image": base.IMAGE_NAME, "networks": [ { "network": "private" @@ -864,7 +864,7 @@ def test_list_clusters(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.4.0-x86_64-disk", + "image": base.IMAGE_NAME, "networks": [ { "network": "private" @@ -915,7 +915,7 @@ def test_update_policy_on_cluster(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.4.0-x86_64-disk", + "image": base.IMAGE_NAME, "networks": [ { "network": "private" @@ -1019,7 +1019,7 @@ def test_list_cluster_profiles(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.4.0-x86_64-disk", + "image": base.IMAGE_NAME, "networks": [ { "network": "private" @@ -1057,7 +1057,7 @@ def test_get_cluster_profile_by_id(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.4.0-x86_64-disk", + "image": base.IMAGE_NAME, "networks": [ { "network": "private" @@ -1095,7 +1095,7 @@ def test_update_cluster_profile(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.4.0-x86_64-disk", + "image": base.IMAGE_NAME, "networks": [ { "network": "private" @@ -1131,7 +1131,7 @@ def test_delete_cluster_profile(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.4.0-x86_64-disk", + "image": base.IMAGE_NAME, "networks": [ { "network": "private" @@ -1298,7 +1298,7 @@ def test_get_cluster_receiver_by_id(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.4.0-x86_64-disk", + "image": base.IMAGE_NAME, "networks": [ { "network": "private" @@ -1357,7 +1357,7 @@ def test_update_cluster_receiver(self): spec = { "properties": { "flavor": "m1.tiny", - "image": "cirros-0.4.0-x86_64-disk", + "image": base.IMAGE_NAME, "networks": [ { "network": "private" diff --git a/tox.ini b/tox.ini index db99c9573..43283d415 100644 --- a/tox.ini +++ b/tox.ini @@ -35,6 +35,7 @@ setenv = {[testenv]setenv} OS_TEST_TIMEOUT=600 OPENSTACKSDK_FUNC_TEST_TIMEOUT_LOAD_BALANCER=600 + OPENSTACKSDK_EXAMPLE_CONFIG_KEY=functional commands = stestr --test-path ./openstack/tests/functional/{env:OPENSTACKSDK_TESTS_SUBDIR:} run --serial {posargs} stestr slowest From 00647dbb241563b98325b6fbc0c72fb01abecd83 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Thu, 12 Mar 2020 11:16:43 +0100 Subject: [PATCH 2620/3836] baremetal: fail-less mode for wait_for_nodes_provision_state This change adds a new `fail` flag to the call. If set to False, the call will return a namedtuple (success, failure, timeout) with lists of nodes. Timeout and failure exceptions will not be raised. Change-Id: Ie20193ce51fcd5ce3ffd479143225bd1a1e8c94a --- .../user/resources/baremetal/v1/node.rst | 7 ++ openstack/baremetal/v1/_proxy.py | 72 ++++++++++++------- openstack/baremetal/v1/node.py | 19 +++++ .../tests/unit/baremetal/v1/test_proxy.py | 54 ++++++++++++++ ...vision-state-no-fail-efa74dd39f687df8.yaml | 6 ++ 5 files changed, 133 insertions(+), 25 deletions(-) create mode 100644 releasenotes/notes/wait-provision-state-no-fail-efa74dd39f687df8.yaml diff --git a/doc/source/user/resources/baremetal/v1/node.rst b/doc/source/user/resources/baremetal/v1/node.rst index bf5f8a694..ebc5fb327 100644 --- a/doc/source/user/resources/baremetal/v1/node.rst +++ b/doc/source/user/resources/baremetal/v1/node.rst @@ -18,3 +18,10 @@ The ``ValidationResult`` class represents the result of a validation. .. autoclass:: openstack.baremetal.v1.node.ValidationResult :members: + +The WaitResult Class +^^^^^^^^^^^^^^^^^^^^ + +The ``WaitResult`` class represents the result of waiting for several nodes. + +.. autoclass:: openstack.baremetal.v1.node.WaitResult diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 0e00b3914..0c4e5b4e9 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -17,6 +17,7 @@ from openstack.baremetal.v1 import node as _node from openstack.baremetal.v1 import port as _port from openstack.baremetal.v1 import port_group as _portgroup +from openstack import exceptions from openstack import proxy from openstack import utils @@ -371,7 +372,8 @@ def set_node_boot_device(self, node, boot_device, persistent=False): def wait_for_nodes_provision_state(self, nodes, expected_state, timeout=None, - abort_on_failed_state=True): + abort_on_failed_state=True, + fail=True): """Wait for the nodes to reach the expected state. :param nodes: List of nodes - name, ID or @@ -384,9 +386,13 @@ def wait_for_nodes_provision_state(self, nodes, expected_state, if any node reaches a failure state which does not match the expected one. Note that the failure state for ``enroll`` -> ``manageable`` transition is ``enroll`` again. + :param fail: If set to ``False`` this call will not raise on timeouts + and provisioning failures. - :return: The list of :class:`~openstack.baremetal.v1.node.Node` - instances that reached the requested state. + :return: If `fail` is ``True`` (the default), the list of + :class:`~openstack.baremetal.v1.node.Node` instances that reached + the requested state. If `fail` is ``False``, a + :class:`~openstack.baremetal.v1.node.WaitResult` named tuple. :raises: :class:`~openstack.exceptions.ResourceFailure` if a node reaches an error state and ``abort_on_failed_state`` is ``True``. :raises: :class:`~openstack.exceptions.ResourceTimeout` on timeout. @@ -395,29 +401,45 @@ def wait_for_nodes_provision_state(self, nodes, expected_state, for n in nodes) finished = [] + failed = [] remaining = nodes - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for nodes %(nodes)s to reach " - "target state '%(state)s'" % {'nodes': log_nodes, - 'state': expected_state}): - nodes = [self.get_node(n) for n in remaining] - remaining = [] - for n in nodes: - if n._check_state_reached(self, expected_state, - abort_on_failed_state): - finished.append(n) - else: - remaining.append(n) - - if not remaining: - return finished - - self.log.debug( - 'Still waiting for nodes %(nodes)s to reach state ' - '"%(target)s"', - {'nodes': ', '.join(n.id for n in remaining), - 'target': expected_state}) + try: + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for nodes %(nodes)s to reach " + "target state '%(state)s'" % {'nodes': log_nodes, + 'state': expected_state}): + nodes = [self.get_node(n) for n in remaining] + remaining = [] + for n in nodes: + try: + if n._check_state_reached(self, expected_state, + abort_on_failed_state): + finished.append(n) + else: + remaining.append(n) + except exceptions.ResourceFailure: + if fail: + raise + else: + failed.append(n) + + if not remaining: + if fail: + return finished + else: + return _node.WaitResult(finished, failed, []) + + self.log.debug( + 'Still waiting for nodes %(nodes)s to reach state ' + '"%(target)s"', + {'nodes': ', '.join(n.id for n in remaining), + 'target': expected_state}) + except exceptions.ResourceTimeout: + if fail: + raise + else: + return _node.WaitResult(finished, failed, remaining) def set_node_power_state(self, node, target): """Run an action modifying node's power state. diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 7a515af8f..c2bd09d60 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import collections + from openstack.baremetal.v1 import _common from openstack import exceptions from openstack import resource @@ -30,6 +32,23 @@ def __init__(self, result, reason): self.reason = reason +class WaitResult(collections.namedtuple('WaitResult', + ['success', 'failure', 'timeout'])): + """A named tuple representing a result of waiting for several nodes. + + Each component is a list of :class:`~openstack.baremetal.v1.node.Node` + objects: + + :ivar ~.success: a list of :class:`~openstack.baremetal.v1.node.Node` + objects that reached the state. + :ivar ~.timeout: a list of :class:`~openstack.baremetal.v1.node.Node` + objects that reached timeout. + :ivar ~.failure: a list of :class:`~openstack.baremetal.v1.node.Node` + objects that hit a failure. + """ + __slots__ = () + + class Node(_common.ListMixin, resource.Resource): resources_key = 'nodes' diff --git a/openstack/tests/unit/baremetal/v1/test_proxy.py b/openstack/tests/unit/baremetal/v1/test_proxy.py index ee3600d88..b18bb921c 100644 --- a/openstack/tests/unit/baremetal/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal/v1/test_proxy.py @@ -235,6 +235,25 @@ def test_success(self, mock_get): n._check_state_reached.assert_called_once_with( self.proxy, 'fake state', True) + def test_success_no_fail(self, mock_get): + # two attempts, one node succeeds after the 1st + nodes = [mock.Mock(spec=node.Node, id=str(i)) + for i in range(3)] + for i, n in enumerate(nodes): + # 1st attempt on 1st node, 2nd attempt on 2nd node + n._check_state_reached.return_value = not (i % 2) + mock_get.side_effect = nodes + + result = self.proxy.wait_for_nodes_provision_state( + ['abcd', node.Node(id='1234')], 'fake state', fail=False) + self.assertEqual([nodes[0], nodes[2]], result.success) + self.assertEqual([], result.failure) + self.assertEqual([], result.timeout) + + for n in nodes: + n._check_state_reached.assert_called_once_with( + self.proxy, 'fake state', True) + def test_timeout(self, mock_get): mock_get.return_value._check_state_reached.return_value = False mock_get.return_value.id = '1234' @@ -245,3 +264,38 @@ def test_timeout(self, mock_get): timeout=0.001) mock_get.return_value._check_state_reached.assert_called_with( self.proxy, 'fake state', True) + + def test_timeout_no_fail(self, mock_get): + mock_get.return_value._check_state_reached.return_value = False + mock_get.return_value.id = '1234' + + result = self.proxy.wait_for_nodes_provision_state( + ['abcd'], 'fake state', timeout=0.001, fail=False) + mock_get.return_value._check_state_reached.assert_called_with( + self.proxy, 'fake state', True) + + self.assertEqual([], result.success) + self.assertEqual([mock_get.return_value], result.timeout) + self.assertEqual([], result.failure) + + def test_timeout_and_failures_not_fail(self, mock_get): + def _fake_get(_self, uuid): + result = mock.Mock() + result.id = uuid + if uuid == '1': + result._check_state_reached.return_value = True + elif uuid == '2': + result._check_state_reached.side_effect = \ + exceptions.ResourceFailure("boom") + else: + result._check_state_reached.return_value = False + return result + + mock_get.side_effect = _fake_get + + result = self.proxy.wait_for_nodes_provision_state( + ['1', '2', '3'], 'fake state', timeout=0.001, fail=False) + + self.assertEqual(['1'], [x.id for x in result.success]) + self.assertEqual(['3'], [x.id for x in result.timeout]) + self.assertEqual(['2'], [x.id for x in result.failure]) diff --git a/releasenotes/notes/wait-provision-state-no-fail-efa74dd39f687df8.yaml b/releasenotes/notes/wait-provision-state-no-fail-efa74dd39f687df8.yaml new file mode 100644 index 000000000..5c4fbca5a --- /dev/null +++ b/releasenotes/notes/wait-provision-state-no-fail-efa74dd39f687df8.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Adds an ability for the bare metal ``wait_for_nodes_provision_state`` call + to return an object with nodes that succeeded, failed or timed out instead + of raising an exception. From bc0e2605a457290e04c48ba17056fd7b64823800 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 4 Mar 2020 11:51:41 -0600 Subject: [PATCH 2621/3836] Set max_microversion to 2.53 for hypervisors If the cloud is new enough, the hypervisor id should be a UUID. Change-Id: I3abff10c018074834bcf834e697200db0530041a --- openstack/compute/v2/hypervisor.py | 3 + openstack/tests/unit/cloud/test_operator.py | 55 ++++++++++++++++--- .../unit/fixtures/old-compute-version.json | 30 ++++++++++ 3 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 openstack/tests/unit/fixtures/old-compute-version.json diff --git a/openstack/compute/v2/hypervisor.py b/openstack/compute/v2/hypervisor.py index fe07ca87e..9c18e7502 100644 --- a/openstack/compute/v2/hypervisor.py +++ b/openstack/compute/v2/hypervisor.py @@ -27,6 +27,9 @@ class Hypervisor(resource.Resource): 'hypervisor_hostname_pattern', 'with_servers' ) + # Hypervisor id is a UUID starting with 2.53 + _max_microversion = '2.53' + # Properties #: Status of hypervisor status = resource.Body('status') diff --git a/openstack/tests/unit/cloud/test_operator.py b/openstack/tests/unit/cloud/test_operator.py index e1f6bf457..15627a476 100644 --- a/openstack/tests/unit/cloud/test_operator.py +++ b/openstack/tests/unit/cloud/test_operator.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import uuid + import mock import testtools @@ -117,14 +119,53 @@ def test_has_service_yes(self, get_session_mock): def test_list_hypervisors(self): '''This test verifies that calling list_hypervisors results in a call to nova client.''' + uuid1 = uuid.uuid4().hex + uuid2 = uuid.uuid4().hex + self.use_compute_discovery() self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['os-hypervisors', 'detail']), - json={'hypervisors': [ - fakes.make_fake_hypervisor('1', 'testserver1'), - fakes.make_fake_hypervisor('2', 'testserver2'), - ]}), + dict( + method='GET', + uri='https://compute.example.com/v2.1/os-hypervisors/detail', + json={ + 'hypervisors': [ + fakes.make_fake_hypervisor(uuid1, 'testserver1'), + fakes.make_fake_hypervisor(uuid2, 'testserver2'), + ] + }, + validate={ + 'headers': { + 'OpenStack-API-Version': 'compute 2.53' + } + } + ), + ]) + + r = self.cloud.list_hypervisors() + + self.assertEqual(2, len(r)) + self.assertEqual('testserver1', r[0]['name']) + self.assertEqual(uuid1, r[0]['id']) + self.assertEqual('testserver2', r[1]['name']) + self.assertEqual(uuid2, r[1]['id']) + + self.assert_calls() + + def test_list_old_hypervisors(self): + '''This test verifies that calling list_hypervisors on a pre-2.53 cloud + calls the old version.''' + self.use_compute_discovery( + compute_version_json='old-compute-version.json') + self.register_uris([ + dict( + method='GET', + uri='https://compute.example.com/v2.1/os-hypervisors/detail', + json={ + 'hypervisors': [ + fakes.make_fake_hypervisor('1', 'testserver1'), + fakes.make_fake_hypervisor('2', 'testserver2'), + ] + } + ), ]) r = self.cloud.list_hypervisors() diff --git a/openstack/tests/unit/fixtures/old-compute-version.json b/openstack/tests/unit/fixtures/old-compute-version.json new file mode 100644 index 000000000..08cbfa95c --- /dev/null +++ b/openstack/tests/unit/fixtures/old-compute-version.json @@ -0,0 +1,30 @@ +{ + "versions": [ + { + "status": "SUPPORTED", + "updated": "2011-01-21T11:33:21Z", + "links": [ + { + "href": "https://compute.example.com/v2/", + "rel": "self" + } + ], + "min_version": "", + "version": "", + "id": "v2.0" + }, + { + "status": "CURRENT", + "updated": "2013-07-23T11:33:21Z", + "links": [ + { + "href": "https://compute.example.com/v2.1/", + "rel": "self" + } + ], + "min_version": "2.10", + "version": "2.50", + "id": "v2.1" + } + ] +} From 40802262981075b8a36b23c377eee01fee0c5f95 Mon Sep 17 00:00:00 2001 From: Riccardo Pittau Date: Fri, 6 Mar 2020 16:15:58 +0100 Subject: [PATCH 2622/3836] Import generate_temp_url from swiftclient The generate_temp_url function is used by other projects, for example ironic, to get temporary urls that grant unauthenticated access to the Swift object. This patch simply takes the code from swiftclient, adapts it for openstacksdk, and add it to the _proxy module in the object_store section, to grant the use of the function directly in openstacksdk. Change-Id: I2de2047ef9025ac0bd191a973f2744b01b904ca7 --- openstack/object_store/v1/_proxy.py | 135 +++++++++ .../tests/unit/object_store/v1/test_proxy.py | 284 ++++++++++++++++++ 2 files changed, 419 insertions(+) diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index 624399182..eba059e6a 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -9,6 +9,8 @@ # 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 calendar import timegm import collections from hashlib import sha1 import hmac @@ -30,6 +32,8 @@ DEFAULT_OBJECT_SEGMENT_SIZE = 1073741824 # 1GB DEFAULT_MAX_FILE_SIZE = (5 * 1024 * 1024 * 1024 + 2) / 2 +EXPIRES_ISO8601_FORMAT = '%Y-%m-%dT%H:%M:%SZ' +SHORT_EXPIRES_ISO8601_FORMAT = '%Y-%m-%d' class Proxy(proxy.Proxy): @@ -793,3 +797,134 @@ def generate_form_signature( sig = hmac.new(temp_url_key, data, sha1).hexdigest() return (expires, sig) + + def generate_temp_url( + self, path, seconds, method, absolute=False, prefix=False, + iso8601=False, ip_range=None, temp_url_key=None): + """Generates a temporary URL that gives unauthenticated access to the + Swift object. + + :param path: The full path to the Swift object or prefix if + a prefix-based temporary URL should be generated. Example: + /v1/AUTH_account/c/o or /v1/AUTH_account/c/prefix. + :param seconds: time in seconds or ISO 8601 timestamp. + If absolute is False and this is the string representation of an + integer, then this specifies the amount of time in seconds for + which the temporary URL will be valid. + If absolute is True then this specifies an absolute time at which + the temporary URL will expire. + :param method: A HTTP method, typically either GET or PUT, to allow + for this temporary URL. + :param absolute: if True then the seconds parameter is interpreted as a + Unix timestamp, if seconds represents an integer. + :param prefix: if True then a prefix-based temporary URL will be + generated. + :param iso8601: if True, a URL containing an ISO 8601 UTC timestamp + instead of a UNIX timestamp will be created. + :param ip_range: if a valid ip range, restricts the temporary URL to + the range of ips. + :param temp_url_key: + The X-Account-Meta-Temp-URL-Key for the account. Optional, if + omitted, the key will be fetched from the container or the account. + :raises ValueError: if timestamp or path is not in valid format. + :return: the path portion of a temporary URL + """ + try: + try: + timestamp = float(seconds) + except ValueError: + formats = ( + EXPIRES_ISO8601_FORMAT, + EXPIRES_ISO8601_FORMAT[:-1], + SHORT_EXPIRES_ISO8601_FORMAT) + for f in formats: + try: + t = time.strptime(seconds, f) + except ValueError: + t = None + else: + if f == EXPIRES_ISO8601_FORMAT: + timestamp = timegm(t) + else: + # Use local time if UTC designator is missing. + timestamp = int(time.mktime(t)) + + absolute = True + break + + if t is None: + raise ValueError() + else: + if not timestamp.is_integer(): + raise ValueError() + timestamp = int(timestamp) + if timestamp < 0: + raise ValueError() + except ValueError: + raise ValueError('time must either be a whole number ' + 'or in specific ISO 8601 format.') + + if isinstance(path, six.binary_type): + try: + path_for_body = path.decode('utf-8') + except UnicodeDecodeError: + raise ValueError('path must be representable as UTF-8') + else: + path_for_body = path + + parts = path_for_body.split('/', 4) + if len(parts) != 5 or parts[0] or not all( + parts[1:(4 if prefix else 5)]): + if prefix: + raise ValueError('path must at least contain /v1/a/c/') + else: + raise ValueError('path must be full path to an object' + ' e.g. /v1/a/c/o') + + standard_methods = ['GET', 'PUT', 'HEAD', 'POST', 'DELETE'] + if method.upper() not in standard_methods: + self.log.warning('Non default HTTP method %s for tempurl ' + 'specified, possibly an error', method.upper()) + + if not absolute: + expiration = int(time.time() + timestamp) + else: + expiration = timestamp + + hmac_parts = [method.upper(), str(expiration), + ('prefix:' if prefix else '') + path_for_body] + + if ip_range: + if isinstance(ip_range, six.binary_type): + try: + ip_range = ip_range.decode('utf-8') + except UnicodeDecodeError: + raise ValueError( + 'ip_range must be representable as UTF-8' + ) + hmac_parts.insert(0, "ip=%s" % ip_range) + + hmac_body = u'\n'.join(hmac_parts) + + temp_url_key = self._check_temp_url_key(temp_url_key=temp_url_key) + + sig = hmac.new(temp_url_key, hmac_body.encode('utf-8'), + sha1).hexdigest() + + if iso8601: + expiration = time.strftime( + EXPIRES_ISO8601_FORMAT, time.gmtime(expiration)) + + temp_url = u'{path}?temp_url_sig={sig}&temp_url_expires={exp}'.format( + path=path_for_body, sig=sig, exp=expiration) + + if ip_range: + temp_url += u'&temp_url_ip_range={}'.format(ip_range) + + if prefix: + temp_url += u'&temp_url_prefix={}'.format(parts[4]) + # Have return type match path from caller + if isinstance(path, six.binary_type): + return temp_url.encode('utf-8') + else: + return temp_url diff --git a/openstack/tests/unit/object_store/v1/test_proxy.py b/openstack/tests/unit/object_store/v1/test_proxy.py index 833cb527a..f3c10724d 100644 --- a/openstack/tests/unit/object_store/v1/test_proxy.py +++ b/openstack/tests/unit/object_store/v1/test_proxy.py @@ -11,9 +11,13 @@ # under the License. from testscenarios import load_tests_apply_scenarios as load_tests # noqa +from hashlib import sha1 +import mock import random +import six import string import tempfile +import time from openstack.object_store.v1 import account from openstack.object_store.v1 import container @@ -250,3 +254,283 @@ def test_extract_name(self): results = self.proxy._extract_name(self.url) self.assertEqual(self.parts, results) + + +class TestTempURL(TestObjectStoreProxy): + expires_iso8601_format = '%Y-%m-%dT%H:%M:%SZ' + short_expires_iso8601_format = '%Y-%m-%d' + time_errmsg = ('time must either be a whole number or in specific ' + 'ISO 8601 format.') + path_errmsg = 'path must be full path to an object e.g. /v1/a/c/o' + url = '/v1/AUTH_account/c/o' + seconds = 3600 + key = 'correcthorsebatterystaple' + method = 'GET' + expected_url = url + ('?temp_url_sig=temp_url_signature' + '&temp_url_expires=1400003600') + expected_body = '\n'.join([ + method, + '1400003600', + url, + ]).encode('utf-8') + + @mock.patch('hmac.HMAC') + @mock.patch('time.time', return_value=1400000000) + def test_generate_temp_url(self, time_mock, hmac_mock): + hmac_mock().hexdigest.return_value = 'temp_url_signature' + url = self.proxy.generate_temp_url( + self.url, self.seconds, self.method, temp_url_key=self.key) + key = self.key + if not isinstance(key, six.binary_type): + key = key.encode('utf-8') + self.assertEqual(url, self.expected_url) + self.assertEqual(hmac_mock.mock_calls, [ + mock.call(), + mock.call(key, self.expected_body, sha1), + mock.call().hexdigest(), + ]) + self.assertIsInstance(url, type(self.url)) + + @mock.patch('hmac.HMAC') + @mock.patch('time.time', return_value=1400000000) + def test_generate_temp_url_ip_range(self, time_mock, hmac_mock): + hmac_mock().hexdigest.return_value = 'temp_url_signature' + ip_ranges = [ + '1.2.3.4', '1.2.3.4/24', '2001:db8::', + b'1.2.3.4', b'1.2.3.4/24', b'2001:db8::', + ] + path = '/v1/AUTH_account/c/o/' + expected_url = path + ('?temp_url_sig=temp_url_signature' + '&temp_url_expires=1400003600' + '&temp_url_ip_range=') + for ip_range in ip_ranges: + hmac_mock.reset_mock() + url = self.proxy.generate_temp_url( + path, self.seconds, self.method, + temp_url_key=self.key, ip_range=ip_range) + key = self.key + if not isinstance(key, six.binary_type): + key = key.encode('utf-8') + + if isinstance(ip_range, six.binary_type): + ip_range_expected_url = ( + expected_url + ip_range.decode('utf-8') + ) + expected_body = '\n'.join([ + 'ip=' + ip_range.decode('utf-8'), + self.method, + '1400003600', + path, + ]).encode('utf-8') + else: + ip_range_expected_url = expected_url + ip_range + expected_body = '\n'.join([ + 'ip=' + ip_range, + self.method, + '1400003600', + path, + ]).encode('utf-8') + + self.assertEqual(url, ip_range_expected_url) + + self.assertEqual(hmac_mock.mock_calls, [ + mock.call(key, expected_body, sha1), + mock.call().hexdigest(), + ]) + self.assertIsInstance(url, type(path)) + + @mock.patch('hmac.HMAC') + def test_generate_temp_url_iso8601_argument(self, hmac_mock): + hmac_mock().hexdigest.return_value = 'temp_url_signature' + url = self.proxy.generate_temp_url( + self.url, '2014-05-13T17:53:20Z', self.method, + temp_url_key=self.key) + self.assertEqual(url, self.expected_url) + + # Don't care about absolute arg. + url = self.proxy.generate_temp_url(self.url, '2014-05-13T17:53:20Z', + self.method, + temp_url_key=self.key, + absolute=True) + self.assertEqual(url, self.expected_url) + + lt = time.localtime() + expires = time.strftime(self.expires_iso8601_format[:-1], lt) + + if not isinstance(self.expected_url, six.string_types): + expected_url = self.expected_url.replace( + b'1400003600', bytes(str(int(time.mktime(lt))), + encoding='ascii')) + else: + expected_url = self.expected_url.replace( + '1400003600', str(int(time.mktime(lt)))) + url = self.proxy.generate_temp_url(self.url, expires, + self.method, + temp_url_key=self.key) + self.assertEqual(url, expected_url) + + expires = time.strftime(self.short_expires_iso8601_format, lt) + lt = time.strptime(expires, self.short_expires_iso8601_format) + + if not isinstance(self.expected_url, six.string_types): + expected_url = self.expected_url.replace( + b'1400003600', bytes(str(int(time.mktime(lt))), + encoding='ascii')) + else: + expected_url = self.expected_url.replace( + '1400003600', str(int(time.mktime(lt)))) + url = self.proxy.generate_temp_url(self.url, expires, + self.method, + temp_url_key=self.key) + self.assertEqual(url, expected_url) + + @mock.patch('hmac.HMAC') + @mock.patch('time.time', return_value=1400000000) + def test_generate_temp_url_iso8601_output(self, time_mock, hmac_mock): + hmac_mock().hexdigest.return_value = 'temp_url_signature' + url = self.proxy.generate_temp_url(self.url, self.seconds, + self.method, + temp_url_key=self.key, + iso8601=True) + key = self.key + if not isinstance(key, six.binary_type): + key = key.encode('utf-8') + + expires = time.strftime(self.expires_iso8601_format, + time.gmtime(1400003600)) + if not isinstance(self.url, six.string_types): + self.assertTrue(url.endswith(bytes(expires, 'utf-8'))) + else: + self.assertTrue(url.endswith(expires)) + self.assertEqual(hmac_mock.mock_calls, [ + mock.call(), + mock.call(key, self.expected_body, sha1), + mock.call().hexdigest(), + ]) + self.assertIsInstance(url, type(self.url)) + + @mock.patch('hmac.HMAC') + @mock.patch('time.time', return_value=1400000000) + def test_generate_temp_url_prefix(self, time_mock, hmac_mock): + hmac_mock().hexdigest.return_value = 'temp_url_signature' + prefixes = ['', 'o', 'p0/p1/'] + for p in prefixes: + hmac_mock.reset_mock() + path = '/v1/AUTH_account/c/' + p + expected_url = path + ('?temp_url_sig=temp_url_signature' + '&temp_url_expires=1400003600' + '&temp_url_prefix=' + p) + expected_body = '\n'.join([ + self.method, + '1400003600', + 'prefix:' + path, + ]).encode('utf-8') + url = self.proxy.generate_temp_url( + path, self.seconds, self.method, prefix=True, + temp_url_key=self.key) + key = self.key + if not isinstance(key, six.binary_type): + key = key.encode('utf-8') + self.assertEqual(url, expected_url) + self.assertEqual(hmac_mock.mock_calls, [ + mock.call(key, expected_body, sha1), + mock.call().hexdigest(), + ]) + + self.assertIsInstance(url, type(path)) + + def test_generate_temp_url_invalid_path(self): + self.assertRaisesRegex( + ValueError, + 'path must be representable as UTF-8', + self.proxy.generate_temp_url, b'/v1/a/c/\xff', self.seconds, + self.method, temp_url_key=self.key) + + @mock.patch('hmac.HMAC.hexdigest', return_value="temp_url_signature") + def test_generate_absolute_expiry_temp_url(self, hmac_mock): + if isinstance(self.expected_url, six.binary_type): + expected_url = self.expected_url.replace( + b'1400003600', b'2146636800') + else: + expected_url = self.expected_url.replace( + u'1400003600', u'2146636800') + url = self.proxy.generate_temp_url( + self.url, 2146636800, self.method, absolute=True, + temp_url_key=self.key) + self.assertEqual(url, expected_url) + + def test_generate_temp_url_bad_time(self): + for bad_time in ['not_an_int', -1, 1.1, '-1', '1.1', '2015-05', + '2015-05-01T01:00']: + self.assertRaisesRegex( + ValueError, self.time_errmsg, + self.proxy.generate_temp_url, self.url, bad_time, + self.method, temp_url_key=self.key) + + def test_generate_temp_url_bad_path(self): + for bad_path in ['/v1/a/c', 'v1/a/c/o', 'blah/v1/a/c/o', '/v1//c/o', + '/v1/a/c/', '/v1/a/c']: + self.assertRaisesRegex( + ValueError, self.path_errmsg, + self.proxy.generate_temp_url, bad_path, 60, self.method, + temp_url_key=self.key) + + +class TestTempURLUnicodePathAndKey(TestTempURL): + url = u'/v1/\u00e4/c/\u00f3' + key = u'k\u00e9y' + expected_url = (u'%s?temp_url_sig=temp_url_signature' + u'&temp_url_expires=1400003600') % url + expected_body = u'\n'.join([ + u'GET', + u'1400003600', + url, + ]).encode('utf-8') + + +class TestTempURLUnicodePathBytesKey(TestTempURL): + url = u'/v1/\u00e4/c/\u00f3' + key = u'k\u00e9y'.encode('utf-8') + expected_url = (u'%s?temp_url_sig=temp_url_signature' + u'&temp_url_expires=1400003600') % url + expected_body = '\n'.join([ + u'GET', + u'1400003600', + url, + ]).encode('utf-8') + + +class TestTempURLBytesPathUnicodeKey(TestTempURL): + url = u'/v1/\u00e4/c/\u00f3'.encode('utf-8') + key = u'k\u00e9y' + expected_url = url + (b'?temp_url_sig=temp_url_signature' + b'&temp_url_expires=1400003600') + expected_body = b'\n'.join([ + b'GET', + b'1400003600', + url, + ]) + + +class TestTempURLBytesPathAndKey(TestTempURL): + url = u'/v1/\u00e4/c/\u00f3'.encode('utf-8') + key = u'k\u00e9y'.encode('utf-8') + expected_url = url + (b'?temp_url_sig=temp_url_signature' + b'&temp_url_expires=1400003600') + expected_body = b'\n'.join([ + b'GET', + b'1400003600', + url, + ]) + + +class TestTempURLBytesPathAndNonUtf8Key(TestTempURL): + url = u'/v1/\u00e4/c/\u00f3'.encode('utf-8') + key = b'k\xffy' + expected_url = url + (b'?temp_url_sig=temp_url_signature' + b'&temp_url_expires=1400003600') + expected_body = b'\n'.join([ + b'GET', + b'1400003600', + url, + ]) From 36fe79f93c562775621b2160ee337e4bf795cbd7 Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Tue, 17 Mar 2020 15:54:33 -0700 Subject: [PATCH 2623/3836] Add Octavia quota to the SDK docs The patch that added Octavia quota to the SDK forgot to update the documentation. This patch corrects that by adding the required section to the page. Change-Id: Icfb92c267a10c7526e91d678e00fc522058a92bc --- doc/source/user/proxies/load_balancer_v2.rst | 11 +++++++++++ doc/source/user/resources/load_balancer/index.rst | 1 + doc/source/user/resources/load_balancer/v2/quota.rst | 12 ++++++++++++ 3 files changed, 24 insertions(+) create mode 100644 doc/source/user/resources/load_balancer/v2/quota.rst diff --git a/doc/source/user/proxies/load_balancer_v2.rst b/doc/source/user/proxies/load_balancer_v2.rst index 81856454a..26e23881d 100644 --- a/doc/source/user/proxies/load_balancer_v2.rst +++ b/doc/source/user/proxies/load_balancer_v2.rst @@ -129,6 +129,17 @@ Flavor Operations .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_flavor .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_flavor +Quota Operations +^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.load_balancer.v2._proxy.Proxy + + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_quota + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_quota + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.quotas + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_quota + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_quota_default + Amphora Operations ^^^^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/load_balancer/index.rst b/doc/source/user/resources/load_balancer/index.rst index 171146ff8..c34a7783f 100644 --- a/doc/source/user/resources/load_balancer/index.rst +++ b/doc/source/user/resources/load_balancer/index.rst @@ -14,4 +14,5 @@ Load Balancer Resources v2/provider v2/flavor_profile v2/flavor + v2/quota v2/amphora diff --git a/doc/source/user/resources/load_balancer/v2/quota.rst b/doc/source/user/resources/load_balancer/v2/quota.rst new file mode 100644 index 000000000..1bf0335f2 --- /dev/null +++ b/doc/source/user/resources/load_balancer/v2/quota.rst @@ -0,0 +1,12 @@ +openstack.load_balancer.v2.quota +================================ + +.. automodule:: openstack.load_balancer.v2.quota + +The Quota Class +--------------- + +The ``Quota`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.load_balancer.v2.quota.Quota + :members: From d10d31b3686125fefd92f293bb9fc607926e2227 Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Tue, 17 Mar 2020 12:47:07 +0100 Subject: [PATCH 2624/3836] Add support for federation mappings Change-Id: I99cef69de1047dfbf31cf2ff5535bb734f5fa61e --- openstack/identity/v3/_proxy.py | 81 +++++++++++++++++++ openstack/identity/v3/mapping.py | 39 +++++++++ .../tests/unit/identity/v3/test_mapping.py | 50 ++++++++++++ 3 files changed, 170 insertions(+) create mode 100644 openstack/identity/v3/mapping.py create mode 100644 openstack/tests/unit/identity/v3/test_mapping.py diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index f0f20038f..461fbefb9 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -18,6 +18,7 @@ from openstack.identity.v3 import endpoint as _endpoint from openstack.identity.v3 import group as _group from openstack.identity.v3 import limit as _limit +from openstack.identity.v3 import mapping as _mapping from openstack.identity.v3 import policy as _policy from openstack.identity.v3 import project as _project from openstack.identity.v3 import region as _region @@ -1319,3 +1320,83 @@ def delete_application_credential(self, user, application_credential, application_credential, user_id=user.id, ignore_missing=ignore_missing) + + def create_mapping(self, **attrs): + """Create a new mapping from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.identity.v3.mapping.Mapping`, + comprised of the properties on the Mapping class. + + :returns: The results of mapping creation + :rtype: :class:`~openstack.identity.v3.mapping.Mapping` + """ + return self._create(_mapping.Mapping, **attrs) + + def delete_mapping(self, mapping, ignore_missing=True): + """Delete a mapping + + :param mapping: The ID of a mapping or a + :class:`~openstack.identity.v3.mapping.Mapping` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the mapping does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent mapping. + + :returns: ``None`` + """ + self._delete(_mapping.Mapping, mapping, ignore_missing=ignore_missing) + + def find_mapping(self, name_or_id, ignore_missing=True): + """Find a single mapping + + :param name_or_id: The name or ID of a mapping. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :returns: One :class:`~openstack.identity.v3.mapping.Mapping` or None + """ + return self._find(_mapping.Mapping, name_or_id, + ignore_missing=ignore_missing) + + def get_mapping(self, mapping): + """Get a single mapping + + :param mapping: The value can be the ID of a mapping or a + :class:`~openstack.identity.v3.mapping.Mapping` + instance. + + :returns: One :class:`~openstack.identity.v3.mapping.Mapping` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_mapping.Mapping, mapping) + + def mappings(self, **query): + """Retrieve a generator of mappings + + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. + + :returns: A generator of mapping instances. + :rtype: :class:`~openstack.identity.v3.mapping.Mapping` + """ + return self._list(_mapping.Mapping, **query) + + def update_mapping(self, mapping, **attrs): + """Update a mapping + + :param mapping: Either the ID of a mapping or a + :class:`~openstack.identity.v3.mapping.Mapping` + instance. + :attrs kwargs: The attributes to update on the mapping represented + by ``value``. + + :returns: The updated mapping + :rtype: :class:`~openstack.identity.v3.mapping.Mapping` + """ + return self._update(_mapping.Mapping, mapping, **attrs) diff --git a/openstack/identity/v3/mapping.py b/openstack/identity/v3/mapping.py new file mode 100644 index 000000000..a0327ef8d --- /dev/null +++ b/openstack/identity/v3/mapping.py @@ -0,0 +1,39 @@ +# 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 resource + + +class Mapping(resource.Resource): + resource_key = 'mapping' + resources_key = 'mappings' + base_path = '/OS-FEDERATION/mappings' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + create_method = 'PUT' + commit_method = 'PATCH' + + _query_mapping = resource.QueryParameters( + 'id', + ) + + # Properties + #: The rules of this mapping. *Type: list* + rules = resource.Body('rules', type=list) + + #: The identifier of the mapping. *Type: string* + name = resource.Body('id') diff --git a/openstack/tests/unit/identity/v3/test_mapping.py b/openstack/tests/unit/identity/v3/test_mapping.py new file mode 100644 index 000000000..41b07c7ba --- /dev/null +++ b/openstack/tests/unit/identity/v3/test_mapping.py @@ -0,0 +1,50 @@ +# 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.tests.unit import base + +from openstack.identity.v3 import mapping + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'id': IDENTIFIER, + 'rules': [{'local': [], 'remote': []}], +} + + +class TestMapping(base.TestCase): + + def test_basic(self): + sot = mapping.Mapping() + self.assertEqual('mapping', sot.resource_key) + self.assertEqual('mappings', sot.resources_key) + self.assertEqual('/OS-FEDERATION/mappings', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertEqual('PATCH', sot.commit_method) + self.assertEqual('PUT', sot.create_method) + + self.assertDictEqual( + { + 'id': 'id', + 'limit': 'limit', + 'marker': 'marker', + }, + sot._query_mapping._mapping) + + def test_make_it(self): + sot = mapping.Mapping(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['rules'], sot.rules) From 9e58f9d77df802c3d983ff8ea35a38c74eb6b2d9 Mon Sep 17 00:00:00 2001 From: Roman Dobosz Date: Fri, 20 Mar 2020 13:04:42 +0100 Subject: [PATCH 2625/3836] Add bulk creation of rules for Security Group. With new method _bulk_create in OpenStackSDK, now it is possible to use it for API methods, which supports creating multiple instances in single call. Neutron supports couple of resources to be created at once. In this change we propose to add multiple rules creation for a single security group. Change-Id: I5855a6faa706adeaf738a797af0048985bffe65f --- doc/source/user/proxies/network.rst | 1 + openstack/network/v2/_proxy.py | 15 +++++++++++++++ openstack/tests/unit/network/v2/test_proxy.py | 8 ++++++++ .../notes/add-sg-rules-bulk-f36a3e2326d74867.yaml | 5 +++++ 4 files changed, 29 insertions(+) create mode 100644 releasenotes/notes/add-sg-rules-bulk-f36a3e2326d74867.yaml diff --git a/doc/source/user/proxies/network.rst b/doc/source/user/proxies/network.rst index a9af0b922..3662d8fcc 100644 --- a/doc/source/user/proxies/network.rst +++ b/doc/source/user/proxies/network.rst @@ -124,6 +124,7 @@ Security Group Operations .. automethod:: openstack.network.v2._proxy.Proxy.security_groups .. automethod:: openstack.network.v2._proxy.Proxy.create_security_group_rule + .. automethod:: openstack.network.v2._proxy.Proxy.create_security_group_rules .. automethod:: openstack.network.v2._proxy.Proxy.delete_security_group_rule Availability Zone Operations diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index f6f69580c..f361ff868 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -3190,6 +3190,21 @@ def create_security_group_rule(self, **attrs): """ return self._create(_security_group_rule.SecurityGroupRule, **attrs) + def create_security_group_rules(self, data): + """Create new security group rules from the list of attributes + + :param list data: List of dicts of attributes which will be used to + create a :class:`~openstack.network.v2.\ + security_group_rule.SecurityGroupRule`, + comprised of the properties on the SecurityGroupRule + class. + + :returns: A generator of security group rule objects + :rtype: :class:`~openstack.network.v2.security_group_rule.\ + SecurityGroupRule` + """ + return self._bulk_create(_security_group_rule.SecurityGroupRule, data) + def delete_security_group_rule(self, security_group_rule, ignore_missing=True, if_revision=None): """Delete a security group rule diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index b5a5bc102..7678f4317 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -1163,6 +1163,14 @@ def test_security_group_rules(self): self.verify_list(self.proxy.security_group_rules, security_group_rule.SecurityGroupRule) + @mock.patch('openstack.network.v2._proxy.Proxy._bulk_create') + def test_security_group_rules_create(self, bc): + data = mock.sentinel + + self.proxy.create_security_group_rules(data) + + bc.assert_called_once_with(security_group_rule.SecurityGroupRule, data) + def test_segment_create_attrs(self): self.verify_create(self.proxy.create_segment, segment.Segment) diff --git a/releasenotes/notes/add-sg-rules-bulk-f36a3e2326d74867.yaml b/releasenotes/notes/add-sg-rules-bulk-f36a3e2326d74867.yaml new file mode 100644 index 000000000..4776d3366 --- /dev/null +++ b/releasenotes/notes/add-sg-rules-bulk-f36a3e2326d74867.yaml @@ -0,0 +1,5 @@ +--- +features: + - Added bulk create securtiy groups rules. With new proxy method + `create_security_group_rules` now it's possible to create multiple rules + for certain security group. From 994c97e0bfbc1ac693e54ff14d4121f68c620b0d Mon Sep 17 00:00:00 2001 From: Sagi Shnaidman Date: Sun, 22 Mar 2020 16:03:37 +0200 Subject: [PATCH 2626/3836] Run fetch-subunit-output role conditionally Jobs that inherit from base job don't always run unit tests or use subunit. This post playbook will fail for them, so make subinit role optional and set by default for true. For example disable it for ansible collection functional jobs: https://review.opendev.org/#/c/711471/ Change-Id: I50b1020c896d7d8a7d58e62de778bb8c2b6e970e --- playbooks/devstack/post.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/playbooks/devstack/post.yaml b/playbooks/devstack/post.yaml index 0b18f2357..14b50edab 100644 --- a/playbooks/devstack/post.yaml +++ b/playbooks/devstack/post.yaml @@ -1,5 +1,6 @@ - hosts: all roles: - fetch-tox-output - - fetch-subunit-output + - role: fetch-subunit-output + when: fetch_subunit|default(true)|bool - process-stackviz From 2c8a9181df9133b365fe8a1bd36af75c153e12a1 Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Fri, 20 Mar 2020 14:25:00 +0100 Subject: [PATCH 2627/3836] Add support for not including the ID in creation requests Some of the PUT APIs don't like it when the ID is included in both the URI and the Body Change-Id: Ia99c77b5e3734f645d97d61920273652bdadae7b --- openstack/resource.py | 6 +++++ openstack/tests/unit/test_resource.py | 36 ++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/openstack/resource.py b/openstack/resource.py index f48ef2331..f2a9ba598 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -436,6 +436,8 @@ class Resource(dict): requires_id = True #: Whether create requires an ID (determined from method if None). create_requires_id = None + #: Whether create should exclude ID in the body of the request. + create_exclude_id_from_body = False #: Do responses for this resource have bodies has_body = True #: Does create returns a body (if False requires ID), defaults to has_body @@ -1261,6 +1263,10 @@ def create(self, session, prepend_key=True, base_path=None, **params): requires_id = (self.create_requires_id if self.create_requires_id is not None else self.create_method == 'PUT') + + if self.create_exclude_id_from_body: + self._body._dirty.discard("id") + if self.create_method == 'PUT': request = self._prepare_request(requires_id=requires_id, prepend_key=prepend_key, diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 3fc701bdd..bc0605c18 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1045,6 +1045,25 @@ class Test(resource.Resource): self.assertEqual({"x": body_value, "id": the_id}, result.body) self.assertEqual({"y": header_value}, result.headers) + def test__prepare_request_with_id_marked_clean(self): + class Test(resource.Resource): + base_path = "/something" + body_attr = resource.Body("x") + header_attr = resource.Header("y") + + the_id = "id" + body_value = "body" + header_value = "header" + sot = Test(id=the_id, body_attr=body_value, header_attr=header_value, + _synchronized=False) + sot._body._dirty.discard("id") + + result = sot._prepare_request(requires_id=True) + + self.assertEqual("something/id", result.url) + self.assertEqual({"x": body_value}, result.body) + self.assertEqual({"y": header_value}, result.headers) + def test__prepare_request_missing_id(self): sot = resource.Resource(id=None) @@ -1401,7 +1420,8 @@ class Test(resource.Resource): self.session.get_endpoint_data.return_value = self.endpoint_data def _test_create(self, cls, requires_id=False, prepend_key=False, - microversion=None, base_path=None, params=None): + microversion=None, base_path=None, params=None, + id_marked_dirty=True): id = "id" if requires_id else None sot = cls(id=id) sot._prepare_request = mock.Mock(return_value=self.request) @@ -1411,6 +1431,9 @@ def _test_create(self, cls, requires_id=False, prepend_key=False, result = sot.create(self.session, prepend_key=prepend_key, base_path=base_path, **params) + id_is_dirty = ('id' in sot._body._dirty) + self.assertEqual(id_marked_dirty, id_is_dirty) + sot._prepare_request.assert_called_once_with( requires_id=requires_id, prepend_key=prepend_key, base_path=base_path) @@ -1439,6 +1462,17 @@ class Test(resource.Resource): self._test_create(Test, requires_id=True, prepend_key=True) + def test_put_create_exclude_id(self): + class Test(resource.Resource): + service = self.service_name + base_path = self.base_path + allow_create = True + create_method = 'PUT' + create_exclude_id_from_body = True + + self._test_create(Test, requires_id=True, prepend_key=True, + id_marked_dirty=False) + def test_put_create_with_microversion(self): class Test(resource.Resource): service = self.service_name From d4ddd5ce996096cb53a92df6fc4770440230b843 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 20 Dec 2019 19:27:55 +0100 Subject: [PATCH 2628/3836] Lay a foundation for the project cleanup During Berlin Summit 2018 it was decided to start implementation of the project cleanup in the SDK. This is a foundation for that with the required bits and few examples in the services to demonstrate the interface. In the followup changes following should be done: - review cleanup in service proxies (the ones implemented and not) - describe the feature in the documentation - start consuming this service from the OSC (need help deciding where should it be implemented - core OSC, separate project, OSC plugin in SDK, ...) Change-Id: Ie29d10475e8d1af1beea3cc5b45984f2be9236ef --- openstack/block_storage/v3/_proxy.py | 39 +++++++ openstack/cloud/openstackcloud.py | 54 +++++++++ openstack/compute/v2/_proxy.py | 20 ++++ openstack/config/cloud_region.py | 15 +++ openstack/connection.py | 2 +- openstack/dns/v2/_proxy.py | 11 ++ openstack/network/v2/_proxy.py | 46 ++++++++ openstack/orchestration/v1/_proxy.py | 18 +++ openstack/proxy.py | 16 +++ openstack/tests/functional/base.py | 9 ++ .../functional/cloud/test_project_cleanup.py | 86 ++++++++++++++ openstack/tests/unit/test_utils.py | 75 ++++++++++++ openstack/utils.py | 109 ++++++++++++++++++ .../add_project_cleanup-39c3517b25a5372e.yaml | 8 ++ tox.ini | 1 + 15 files changed, 508 insertions(+), 1 deletion(-) create mode 100644 openstack/tests/functional/cloud/test_project_cleanup.py create mode 100644 releasenotes/notes/add_project_cleanup-39c3517b25a5372e.yaml diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 11b72952d..1fe43d933 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -349,3 +349,42 @@ def wait_for_delete(self, res, interval=2, wait=120): to delete failed to occur in the specified seconds. """ return resource.wait_for_delete(self, res, interval, wait) + + def _get_cleanup_dependencies(self): + return { + 'block_storage': { + 'before': [] + } + } + + def _service_cleanup(self, dry_run=True, status_queue=None): + if self._connection.has_service('object-store'): + # Volume backups require object-store to be available, even for + # listing + for obj in self.backups(details=False): + if status_queue: + status_queue.put(obj) + if not dry_run: + self.delete_backup(obj) + + snapshots = [] + for obj in self.snapshots(details=False): + if status_queue: + snapshots.append(obj) + status_queue.put(obj) + if not dry_run: + self.delete_snapshot(obj) + + # Before deleting volumes need to wait for snapshots to be deleted + for obj in snapshots: + try: + self.wait_for_delete(obj) + except exceptions.SDKException: + # Well, did our best, still try further + pass + + for obj in self.volumes(details=False): + if status_queue: + status_queue.put(obj) + if not dry_run: + self.delete_volume(obj) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 390ca1503..ba37a0824 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -11,6 +11,7 @@ # limitations under the License. import copy import functools +import queue import six # import types so that we can reference ListType in sphinx param declarations. # We can't just use list, because sphinx gets confused by @@ -35,6 +36,7 @@ import openstack.config from openstack.config import cloud_region as cloud_region_mod from openstack import proxy +from openstack import utils DEFAULT_SERVER_AGE = 5 DEFAULT_PORT_AGE = 5 @@ -749,3 +751,55 @@ def has_service(self, service_key): return True else: return False + + def project_cleanup(self, dry_run=True, + wait_timeout=120, status_queue=None): + """Cleanup the project resources. + + Cleanup all resources in all services, which provide cleanup methods. + + :param bool dry_run: Cleanup or only list identified resources. + :param int wait_timeout: Maximum amount of time given to each service + to comlete the cleanup. + :param queue status_queue: a threading queue object used to get current + process status. The queue contain processed resources. + """ + dependencies = {} + get_dep_fn_name = '_get_cleanup_dependencies' + cleanup_fn_name = '_service_cleanup' + if not status_queue: + status_queue = queue.Queue() + for service in self.config.get_enabled_services(): + if hasattr(self, service): + proxy = getattr(self, service) + if (proxy + and hasattr(proxy, get_dep_fn_name) + and hasattr(proxy, cleanup_fn_name)): + deps = getattr(proxy, get_dep_fn_name)() + if deps: + dependencies.update(deps) + dep_graph = utils.TinyDAG() + for k, v in dependencies.items(): + dep_graph.add_node(k) + for dep in v['before']: + dep_graph.add_node(dep) + dep_graph.add_edge(k, dep) + + for service in dep_graph.walk(timeout=wait_timeout): + fn = None + if hasattr(self, service): + proxy = getattr(self, service) + cleanup_fn = getattr(proxy, cleanup_fn_name, None) + if cleanup_fn: + fn = functools.partial(cleanup_fn, dry_run=dry_run, + status_queue=status_queue) + if fn: + self._pool_executor.submit(cleanup_task, dep_graph, + service, fn) + else: + dep_graph.node_done(service) + + +def cleanup_task(graph, service, fn): + fn() + graph.node_done(service) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 0eef18930..a897509eb 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -1472,3 +1472,23 @@ def get_server_diagnostics(self, server): server_id = self._get_resource(_server.Server, server).id return self._get(_server_diagnostics.ServerDiagnostics, server_id=server_id, requires_id=False) + + def _get_cleanup_dependencies(self): + return { + 'compute': { + 'before': ['block_storage', 'network', 'identity'] + } + } + + def _service_cleanup(self, dry_run=True, status_queue=None): + for obj in self.servers(details=False): + self._service_cleanup_del_res(self.delete_server, + obj, + dry_run, + status_queue) + + for obj in self.keypairs(): + self._service_cleanup_del_res(self.delete_keypair, + obj, + dry_run, + status_queue) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index b44c89afc..95d6735e0 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -363,6 +363,21 @@ def get_services(self): services.append("_".join(key.split('_')[:-2])) return list(set(services)) + def get_enabled_services(self): + services = set() + + all_services = [k['service_type'] for k in + self._service_type_manager.services] + all_services.extend(k[4:] for k in + self.config.keys() if k.startswith('has_')) + + for srv in all_services: + ep = self.get_endpoint_from_catalog(srv) + if ep: + services.add(srv.replace('-', '_')) + + return services + def get_auth_args(self): return self.config.get('auth', {}) diff --git a/openstack/connection.py b/openstack/connection.py index 7937326f4..2ee65ec06 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -480,7 +480,7 @@ def getter(self): attr_name.replace('-', '_'), property(fget=getter) ) - self.config.enable_service(attr_name) + self.config.enable_service(service.service_type) def authorize(self): """Authorize this Connection diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py index ff357e927..610e9e830 100644 --- a/openstack/dns/v2/_proxy.py +++ b/openstack/dns/v2/_proxy.py @@ -511,3 +511,14 @@ def create_zone_transfer_accept(self, **attrs): :rtype: :class:`~openstack.dns.v2.zone_transfer.ZoneTransferAccept` """ return self._create(_zone_transfer.ZoneTransferAccept, **attrs) + + def _get_cleanup_dependencies(self): + # DNS may depend on floating ip + return { + 'dns': { + 'before': ['network'] + } + } + + def _service_cleanup(self, dry_run=True, status_queue=False): + pass diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index f6f69580c..caf7842c7 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -3990,3 +3990,49 @@ def update_floating_ip_port_forwarding(self, floating_ip, port_forwarding, floatingip = self._get_resource(_floating_ip.FloatingIP, floating_ip) return self._update(_port_forwarding.PortForwarding, port_forwarding, floatingip_id=floatingip.id, **attrs) + + def _get_cleanup_dependencies(self): + return { + 'network': { + 'before': ['identity'] + } + } + + def _service_cleanup(self, dry_run=True, status_queue=None): + for obj in self.ips(): + self._service_cleanup_del_res(self.delete_ip, obj, dry_run, + status_queue) + + for obj in self.security_groups(): + if obj.name != 'default': + self._service_cleanup_del_res( + self.delete_security_group, obj, + dry_run, status_queue) + + for port in self.ports(): + if port.device_owner in ['network:router_interface', + 'network:router_interface_distributed']: + if status_queue: + status_queue.put(obj) + if not dry_run: + try: + self.remove_interface_from_router( + router=port.device_id, + port_id=port.id) + except exceptions.SDKException: + self.log.error('Cannot delete object %s' % obj) + + for obj in self.routers(): + self._service_cleanup_del_res( + self.delete_router, obj, + dry_run, status_queue) + + for obj in self.subnets(): + self._service_cleanup_del_res( + self.delete_subnet, obj, + dry_run, status_queue) + + for obj in self.networks(): + self._service_cleanup_del_res( + self.delete_network, obj, + dry_run, status_queue) diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index b1eeaaf79..37bbaede2 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -479,3 +479,21 @@ def get_template_contents( except Exception as e: raise exceptions.SDKException( "Error in processing template files: %s" % str(e)) + + def _get_cleanup_dependencies(self): + return { + 'orchestration': { + 'before': ['compute', 'network', 'identity'] + } + } + + def _service_cleanup(self, dry_run=True, status_queue=None): + stacks = [] + for obj in self.stacks(): + stacks.append(obj) + self._project_cleanup_del_res( + self.delete_stack, obj, + dry_run, status_queue) + + for stack in stacks: + self.wait_for_delete(stack) diff --git a/openstack/proxy.py b/openstack/proxy.py index 2ea253038..6c1456ab9 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -521,6 +521,22 @@ def _head(self, resource_type, value=None, base_path=None, **attrs): res = self._get_resource(resource_type, value, **attrs) return res.head(self, base_path=base_path) + def _get_cleanup_dependencies(self): + return None + + def _service_cleanup(self, dry_run=True, status_queue=None): + return None + + def _service_cleanup_del_res(self, del_fn, obj, dry_run=True, + status_queue=None): + if status_queue: + status_queue.put(obj) + if not dry_run: + try: + del_fn(obj) + except exceptions.SDKException as e: + self.log.error('Cannot delete resource %s: %s', obj, str(e)) + def _json_response(response, result_key=None, error_message=None): """Temporary method to use to bridge from ShadeAdapter to SDK calls.""" diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index 1a19153ba..f3e4f2a67 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -51,6 +51,8 @@ def setUp(self): _disable_keep_alive(self.conn) self._demo_name = os.environ.get('OPENSTACKSDK_DEMO_CLOUD', 'devstack') + self._demo_name_alt = os.environ.get('OPENSTACKSDK_DEMO_CLOUD_ALT', + 'devstack-alt') self._op_name = os.environ.get( 'OPENSTACKSDK_OPERATOR_CLOUD', 'devstack-admin') @@ -73,6 +75,13 @@ def _set_user_cloud(self, **kwargs): self.user_cloud = connection.Connection(config=user_config) _disable_keep_alive(self.user_cloud) + # This cloud is used by the project_cleanup test, so you can't rely on + # it + user_config_alt = self.config.get_one( + cloud=self._demo_name_alt, **kwargs) + self.user_cloud_alt = connection.Connection(config=user_config_alt) + _disable_keep_alive(self.user_cloud_alt) + def _set_operator_cloud(self, **kwargs): operator_config = self.config.get_one( cloud=self._op_name, **kwargs) diff --git a/openstack/tests/functional/cloud/test_project_cleanup.py b/openstack/tests/functional/cloud/test_project_cleanup.py new file mode 100644 index 000000000..01a152cda --- /dev/null +++ b/openstack/tests/functional/cloud/test_project_cleanup.py @@ -0,0 +1,86 @@ +# 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_project_cleanup +---------------------------------- + +Functional tests for project cleanup methods. +""" +import queue + +from openstack.tests.functional import base + + +class TestProjectCleanup(base.BaseFunctionalTest): + + _wait_for_timeout_key = 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_CLEANUP' + + def setUp(self): + super(TestProjectCleanup, self).setUp() + self.conn = self.user_cloud_alt + self.network_name = self.getUniqueString('network') + + def _create_network_resources(self): + conn = self.conn + self.net = conn.network.create_network( + name=self.network_name, + ) + self.subnet = conn.network.create_subnet( + name=self.getUniqueString('subnet'), + network_id=self.net.id, + cidr='192.169.1.0/24', + ip_version=4, + ) + self.router = conn.network.create_router( + name=self.getUniqueString('router') + ) + conn.network.add_interface_to_router( + self.router.id, + subnet_id=self.subnet.id) + + def _test_cleanup(self): + self._create_network_resources() + status_queue = queue.Queue() + self.conn.project_cleanup( + dry_run=True, + wait_timeout=120, + status_queue=status_queue) + + objects = [] + while not status_queue.empty(): + objects.append(status_queue.get()) + + net_names = list(obj.name for obj in objects) + self.assertIn(self.network_name, net_names) + + # Ensure network still exists + net = self.conn.network.get_network(self.net.id) + self.assertEqual(net.name, self.net.name) + + self.conn.project_cleanup( + dry_run=False, + wait_timeout=600, + status_queue=status_queue) + + objects = [] + while not status_queue.empty(): + objects.append(status_queue.get()) + + nets = self.conn.network.networks() + net_names = list(obj.name for obj in nets) + # Since we might not have enough privs to drop all nets - ensure + # we do not have our known one + self.assertNotIn(self.network_name, net_names) + + def test_cleanup(self): + self._test_cleanup() diff --git a/openstack/tests/unit/test_utils.py b/openstack/tests/unit/test_utils.py index c16cfe2dc..570709222 100644 --- a/openstack/tests/unit/test_utils.py +++ b/openstack/tests/unit/test_utils.py @@ -17,10 +17,15 @@ import sys from openstack.tests.unit import base +import concurrent.futures + +import testtools + import fixtures import os_service_types import openstack +from openstack import exceptions from openstack import utils @@ -168,3 +173,73 @@ def test_ost_version(self): "This project must be pinned to the latest version of " "os-service-types. Please bump requirements.txt and " "lower-constraints.txt accordingly.") + + +class TestTinyDAG(base.TestCase): + test_graph = { + 'a': ['b', 'd', 'f'], + 'b': ['c', 'd'], + 'c': ['d'], + 'd': ['e'], + 'e': [], + 'f': ['e'], + 'g': ['e'] + } + + def _verify_order(self, test_graph, test_list): + for k, v in test_graph.items(): + for dep in v: + self.assertTrue(test_list.index(k) < test_list.index(dep)) + + def test_from_dict(self): + sot = utils.TinyDAG() + sot.from_dict(self.test_graph) + + def test_topological_sort(self): + sot = utils.TinyDAG() + sot.from_dict(self.test_graph) + sorted_list = sot.topological_sort() + self._verify_order(sot.graph, sorted_list) + self.assertEqual(len(self.test_graph.keys()), len(sorted_list)) + + def test_walk(self): + sot = utils.TinyDAG() + sot.from_dict(self.test_graph) + sorted_list = [] + for node in sot.walk(): + sorted_list.append(node) + sot.node_done(node) + self._verify_order(sot.graph, sorted_list) + self.assertEqual(len(self.test_graph.keys()), len(sorted_list)) + + def test_walk_parallel(self): + sot = utils.TinyDAG() + sot.from_dict(self.test_graph) + sorted_list = [] + with concurrent.futures.ThreadPoolExecutor(max_workers=15) as executor: + for node in sot.walk(timeout=1): + executor.submit(test_walker_fn, sot, node, sorted_list) + self._verify_order(sot.graph, sorted_list) + print(sorted_list) + self.assertEqual(len(self.test_graph.keys()), len(sorted_list)) + + def test_walk_raise(self): + sot = utils.TinyDAG() + sot.from_dict(self.test_graph) + bad_node = 'f' + with testtools.ExpectedException(exceptions.SDKException): + for node in sot.walk(timeout=1): + if node != bad_node: + sot.node_done(node) + + def test_add_node_after_edge(self): + sot = utils.TinyDAG() + sot.add_node('a') + sot.add_edge('a', 'b') + sot.add_node('a') + self.assertEqual(sot._graph['a'], set('b')) + + +def test_walker_fn(graph, node, lst): + lst.append(node) + graph.node_done(node) diff --git a/openstack/utils.py b/openstack/utils.py index efab5cce5..cca475de5 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -10,7 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +import queue import string +import threading import time import six @@ -190,3 +192,110 @@ def maximum_supported_microversion(adapter, client_maximum): result = min(client_max, server_max) return discover.version_to_string(result) + + +class TinyDAG(six.Iterator): + """Tiny DAG + + Bases on the Kahn's algorithm, and enables parallel visiting of the nodes + (parallel execution of the workflow items). + """ + + def __init__(self, data=None): + self._reset() + self._lock = threading.Lock() + if data and isinstance(data, dict): + self.from_dict(data) + + def _reset(self): + self._graph = dict() + self._wait_timeout = 120 + + @property + def graph(self): + """Get graph as adjacency dict + """ + return self._graph + + def add_node(self, node): + self._graph.setdefault(node, set()) + + def add_edge(self, u, v): + self._graph[u].add(v) + + def from_dict(self, data): + self._reset() + for k, v in data.items(): + self.add_node(k) + for dep in v: + self.add_edge(k, dep) + + def walk(self, timeout=None): + """Start the walking from the beginning. + """ + if timeout: + self._wait_timeout = timeout + return self + + def __iter__(self): + self._start_traverse() + return self + + def __next__(self): + # Start waiting if it is expected to get something + # (counting down from graph length to 0). + if (self._it_cnt > 0): + self._it_cnt -= 1 + try: + res = self._queue.get( + block=True, + timeout=self._wait_timeout) + return res + + except queue.Empty: + raise exceptions.SDKException('Timeout waiting for ' + 'cleanup task to complete') + else: + raise StopIteration + + def node_done(self, node): + """Mark node as "processed" and put following items into the queue""" + self._done.add(node) + + for v in self._graph[node]: + self._run_in_degree[v] -= 1 + if self._run_in_degree[v] == 0: + self._queue.put(v) + + def _start_traverse(self): + """Initialize graph traversing""" + self._run_in_degree = self._get_in_degree() + self._queue = queue.Queue() + self._done = set() + self._it_cnt = len(self._graph) + + for k, v in self._run_in_degree.items(): + if v == 0: + self._queue.put(k) + + def _get_in_degree(self): + """Calculate the in_degree (count incoming) for nodes""" + _in_degree = dict() + _in_degree = {u: 0 for u in self._graph.keys()} + for u in self._graph: + for v in self._graph[u]: + _in_degree[v] += 1 + + return _in_degree + + def topological_sort(self): + """Return the graph nodes in the topological order""" + result = [] + for node in self: + result.append(node) + self.node_done(node) + + return result + + def size(self): + return len(self._graph.keys()) diff --git a/releasenotes/notes/add_project_cleanup-39c3517b25a5372e.yaml b/releasenotes/notes/add_project_cleanup-39c3517b25a5372e.yaml new file mode 100644 index 000000000..f99c66fc9 --- /dev/null +++ b/releasenotes/notes/add_project_cleanup-39c3517b25a5372e.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Project cleanup functionality. It provides a single method in the + connection object, which calls cleanup method in all supported services + (both part of the SDK itself and all "imported" in the runtime or through + the vendor_hook functionality). Cleanup is working in multiple threads + where possible (no dependencies between services). diff --git a/tox.ini b/tox.ini index 43283d415..2072fcbf7 100644 --- a/tox.ini +++ b/tox.ini @@ -36,6 +36,7 @@ setenv = OS_TEST_TIMEOUT=600 OPENSTACKSDK_FUNC_TEST_TIMEOUT_LOAD_BALANCER=600 OPENSTACKSDK_EXAMPLE_CONFIG_KEY=functional + OPENSTACKSDK_FUNC_TEST_TIMEOUT_PROJECT_CLEANUP=1200 commands = stestr --test-path ./openstack/tests/functional/{env:OPENSTACKSDK_TESTS_SUBDIR:} run --serial {posargs} stestr slowest From d85c7eef001def20b37ae19bca0fdec234140522 Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Fri, 20 Mar 2020 14:45:16 +0100 Subject: [PATCH 2629/3836] Add support for Identity Providers Change-Id: I7f0ea852fd03ecca64e0387e5019335a4664aba6 Depends-On: https://review.opendev.org/714120 --- openstack/identity/v3/_proxy.py | 91 +++++++++++++++++++ openstack/identity/v3/identity_provider.py | 48 ++++++++++ .../identity/v3/test_identity_provider.py | 59 ++++++++++++ 3 files changed, 198 insertions(+) create mode 100644 openstack/identity/v3/identity_provider.py create mode 100644 openstack/tests/unit/identity/v3/test_identity_provider.py diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 461fbefb9..f7da56a15 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -17,6 +17,7 @@ from openstack.identity.v3 import domain as _domain from openstack.identity.v3 import endpoint as _endpoint from openstack.identity.v3 import group as _group +from openstack.identity.v3 import identity_provider as _identity_provider from openstack.identity.v3 import limit as _limit from openstack.identity.v3 import mapping as _mapping from openstack.identity.v3 import policy as _policy @@ -1400,3 +1401,93 @@ def update_mapping(self, mapping, **attrs): :rtype: :class:`~openstack.identity.v3.mapping.Mapping` """ return self._update(_mapping.Mapping, mapping, **attrs) + + def create_identity_provider(self, **attrs): + """Create a new identity provider from attributes + + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.identity.v3.identity_provider.IdentityProvider` + comprised of the properties on the IdentityProvider class. + + :returns: The results of identity provider creation + :rtype: + :class:`~openstack.identity.v3.identity_provider.IdentityProvider` + """ + return self._create(_identity_provider.IdentityProvider, **attrs) + + def delete_identity_provider(self, identity_provider, ignore_missing=True): + """Delete an identity provider + + :param mapping: The ID of an identity provoder or a + :class:`~openstack.identity.v3.identity_provider.IdentityProvider` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the identity provider does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent identity provider. + + :returns: ``None`` + """ + self._delete(_identity_provider.IdentityProvider, identity_provider, + ignore_missing=ignore_missing) + + def find_identity_provider(self, name_or_id, ignore_missing=True): + """Find a single identity provider + + :param name_or_id: The name or ID of an identity provider + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :returns: The details of an identity provider or None. + :rtype: + :class:`~openstack.identity.v3.identity_provider.IdentityProvider` + """ + return self._find(_identity_provider.IdentityProvider, name_or_id, + ignore_missing=ignore_missing) + + def get_identity_provider(self, identity_provider): + """Get a single mapping + + :param mapping: The value can be the ID of an identity provider or a + :class:`~openstack.identity.v3.identity_provider.IdentityProvider` + instance. + + :returns: The details of an identity provider. + :rtype: + :class:`~openstack.identity.v3.identity_provider.IdentityProvider` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_identity_provider.IdentityProvider, + identity_provider) + + def identity_providers(self, **query): + """Retrieve a generator of identity providers + + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. + + :returns: A generator of identity provider instances. + :rtype: + :class:`~openstack.identity.v3.identity_provider.IdentityProvider` + """ + return self._list(_identity_provider.IdentityProvider, **query) + + def update_identity_provider(self, identity_provider, **attrs): + """Update a mapping + + :param mapping: Either the ID of an identity provider or a + :class:`~openstack.identity.v3.identity_provider.IdentityProvider` + instance. + :attrs kwargs: The attributes to update on the identity_provider + represented by ``value``. + + :returns: The updated identity provider. + :rtype: + :class:`~openstack.identity.v3.identity_provider.IdentityProvider` + """ + return self._update(_identity_provider.IdentityProvider, + identity_provider, **attrs) diff --git a/openstack/identity/v3/identity_provider.py b/openstack/identity/v3/identity_provider.py new file mode 100644 index 000000000..6baea2551 --- /dev/null +++ b/openstack/identity/v3/identity_provider.py @@ -0,0 +1,48 @@ +# 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 resource + + +class IdentityProvider(resource.Resource): + resource_key = 'identity_provider' + resources_key = 'identity_providers' + base_path = '/OS-FEDERATION/identity_providers' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + create_method = 'PUT' + create_exclude_id_from_body = True + commit_method = 'PATCH' + + _query_mapping = resource.QueryParameters( + 'id', + is_enabled='enabled', + ) + + # Properties + #: The id of a domain associated with this identity provider. + # *Type: string* + domain_id = resource.Body('domain_id') + #: A description of this identity provider. *Type: string* + description = resource.Body('description') + #: If the identity provider is currently enabled. *Type: bool* + is_enabled = resource.Body('enabled', type=bool) + #: Remote IDs associated with the identity provider. *Type: list* + remote_ids = resource.Body('remote_ids', type=list) + + #: The identifier of the identity provider (read only). *Type: string* + name = resource.Body('id') diff --git a/openstack/tests/unit/identity/v3/test_identity_provider.py b/openstack/tests/unit/identity/v3/test_identity_provider.py new file mode 100644 index 000000000..c3c9c512f --- /dev/null +++ b/openstack/tests/unit/identity/v3/test_identity_provider.py @@ -0,0 +1,59 @@ +# 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.tests.unit import base + +from openstack.identity.v3 import identity_provider + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'id': IDENTIFIER, + 'domain_id': 'example_domain', + 'description': 'An example description', + 'is_enabled': True, + 'remote_ids': ['https://auth.example.com/auth/realms/ExampleRealm'], +} + + +class TestIdentityProvider(base.TestCase): + + def test_basic(self): + sot = identity_provider.IdentityProvider() + self.assertEqual('identity_provider', sot.resource_key) + self.assertEqual('identity_providers', sot.resources_key) + self.assertEqual('/OS-FEDERATION/identity_providers', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.create_exclude_id_from_body) + self.assertEqual('PATCH', sot.commit_method) + self.assertEqual('PUT', sot.create_method) + + self.assertDictEqual( + { + 'id': 'id', + 'limit': 'limit', + 'marker': 'marker', + 'is_enabled': 'enabled', + }, + sot._query_mapping._mapping) + + def test_make_it(self): + sot = identity_provider.IdentityProvider(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['id'], sot.name) + self.assertEqual(EXAMPLE['domain_id'], sot.domain_id) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['is_enabled'], sot.is_enabled) + self.assertEqual(EXAMPLE['remote_ids'], sot.remote_ids) From cbda44137dbea74a0c81458a1c38bac396588174 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 5 Mar 2020 08:56:41 -0600 Subject: [PATCH 2630/3836] Add ansible stable-2.9 job and run 2.8 and 2.9 We need to run master of openstacksdk against the stable ansible branches that have openstack modules. This partially reverts commit 666381d6e45db8df707ff39a7ef8385cca172c3a. We no longer are running jobs against devel of ansible, so we don't need to do the failure job. Change-Id: Ic783cfd564001a0bd00182725d26928ae6422c3f --- .zuul.yaml | 39 ++---- extras/run-ansible-tests.sh | 77 +++++++++-- openstack/tests/ansible/README.txt | 26 ++++ .../tests/ansible/hooks/post_test_hook.sh | 40 ++++++ .../tests/ansible/roles/auth/tasks/main.yml | 6 + .../roles/client_config/tasks/main.yml | 7 + .../ansible/roles/group/defaults/main.yml | 1 + .../tests/ansible/roles/group/tasks/main.yml | 19 +++ .../ansible/roles/image/defaults/main.yml | 1 + .../tests/ansible/roles/image/tasks/main.yml | 54 ++++++++ .../ansible/roles/keypair/defaults/main.yml | 1 + .../ansible/roles/keypair/tasks/main.yml | 62 +++++++++ .../roles/keystone_domain/defaults/main.yml | 1 + .../roles/keystone_domain/tasks/main.yml | 19 +++ .../roles/keystone_role/defaults/main.yml | 1 + .../roles/keystone_role/tasks/main.yml | 12 ++ .../ansible/roles/network/defaults/main.yml | 3 + .../ansible/roles/network/tasks/main.yml | 14 ++ .../ansible/roles/nova_flavor/tasks/main.yml | 53 ++++++++ .../tests/ansible/roles/object/tasks/main.yml | 37 ++++++ .../ansible/roles/port/defaults/main.yml | 6 + .../tests/ansible/roles/port/tasks/main.yml | 101 ++++++++++++++ .../ansible/roles/router/defaults/main.yml | 3 + .../tests/ansible/roles/router/tasks/main.yml | 95 ++++++++++++++ .../roles/security_group/defaults/main.yml | 1 + .../roles/security_group/tasks/main.yml | 123 ++++++++++++++++++ .../ansible/roles/server/defaults/main.yaml | 5 + .../tests/ansible/roles/server/tasks/main.yml | 92 +++++++++++++ .../ansible/roles/subnet/defaults/main.yml | 2 + .../tests/ansible/roles/subnet/tasks/main.yml | 43 ++++++ .../tests/ansible/roles/user/tasks/main.yml | 30 +++++ .../ansible/roles/user_group/tasks/main.yml | 31 +++++ .../tests/ansible/roles/volume/tasks/main.yml | 17 +++ openstack/tests/ansible/run.yml | 26 ++++ 34 files changed, 1010 insertions(+), 38 deletions(-) create mode 100644 openstack/tests/ansible/README.txt create mode 100755 openstack/tests/ansible/hooks/post_test_hook.sh create mode 100644 openstack/tests/ansible/roles/auth/tasks/main.yml create mode 100644 openstack/tests/ansible/roles/client_config/tasks/main.yml create mode 100644 openstack/tests/ansible/roles/group/defaults/main.yml create mode 100644 openstack/tests/ansible/roles/group/tasks/main.yml create mode 100644 openstack/tests/ansible/roles/image/defaults/main.yml create mode 100644 openstack/tests/ansible/roles/image/tasks/main.yml create mode 100644 openstack/tests/ansible/roles/keypair/defaults/main.yml create mode 100644 openstack/tests/ansible/roles/keypair/tasks/main.yml create mode 100644 openstack/tests/ansible/roles/keystone_domain/defaults/main.yml create mode 100644 openstack/tests/ansible/roles/keystone_domain/tasks/main.yml create mode 100644 openstack/tests/ansible/roles/keystone_role/defaults/main.yml create mode 100644 openstack/tests/ansible/roles/keystone_role/tasks/main.yml create mode 100644 openstack/tests/ansible/roles/network/defaults/main.yml create mode 100644 openstack/tests/ansible/roles/network/tasks/main.yml create mode 100644 openstack/tests/ansible/roles/nova_flavor/tasks/main.yml create mode 100644 openstack/tests/ansible/roles/object/tasks/main.yml create mode 100644 openstack/tests/ansible/roles/port/defaults/main.yml create mode 100644 openstack/tests/ansible/roles/port/tasks/main.yml create mode 100644 openstack/tests/ansible/roles/router/defaults/main.yml create mode 100644 openstack/tests/ansible/roles/router/tasks/main.yml create mode 100644 openstack/tests/ansible/roles/security_group/defaults/main.yml create mode 100644 openstack/tests/ansible/roles/security_group/tasks/main.yml create mode 100644 openstack/tests/ansible/roles/server/defaults/main.yaml create mode 100644 openstack/tests/ansible/roles/server/tasks/main.yml create mode 100644 openstack/tests/ansible/roles/subnet/defaults/main.yml create mode 100644 openstack/tests/ansible/roles/subnet/tasks/main.yml create mode 100644 openstack/tests/ansible/roles/user/tasks/main.yml create mode 100644 openstack/tests/ansible/roles/user_group/tasks/main.yml create mode 100644 openstack/tests/ansible/roles/volume/tasks/main.yml create mode 100644 openstack/tests/ansible/run.yml diff --git a/.zuul.yaml b/.zuul.yaml index 26a204990..282094cb8 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -286,36 +286,14 @@ tox_envlist: ansible - job: - name: openstacksdk-ansible-devel-functional-devstack - parent: openstacksdk-ansible-functional-devstack - description: | - Run openstacksdk ansible functional tests against a master devstack - using git devel branch version of ansible. - branches: ^(devel|master)$ - required-projects: - - name: github.com/ansible/ansible - override-checkout: devel - - name: openstack/openstacksdk - override-checkout: master - - name: openstack/devstack - override-checkout: master - vars: - # test-matrix grabs branch from the zuul branch setting. If the job - # is triggered by ansible, that branch will be devel which doesn't - # make sense to devstack. Override so that we run the right thing. - test_matrix_branch: master - tox_install_siblings: true - -- job: - name: openstacksdk-ansible-stable-2.6-functional-devstack + name: openstacksdk-ansible-stable-2.8-functional-devstack parent: openstacksdk-ansible-functional-devstack description: | Run openstacksdk ansible functional tests against a master devstack - using git stable-2.6 branch version of ansible. - branches: ^(stable-2.6|master)$ + using git stable-2.8 branch version of ansible. required-projects: - name: github.com/ansible/ansible - override-checkout: stable-2.6 + override-checkout: stable-2.8 - name: openstack/openstacksdk override-checkout: master - name: openstack/devstack @@ -328,15 +306,14 @@ tox_install_siblings: true - job: - name: openstacksdk-ansible-stable-2.8-functional-devstack + name: openstacksdk-ansible-stable-2.9-functional-devstack parent: openstacksdk-ansible-functional-devstack description: | Run openstacksdk ansible functional tests against a master devstack - using git stable-2.8 branch version of ansible. - branches: ^(stable-2.8|master)$ + using git stable-2.9 branch version of ansible. required-projects: - name: github.com/ansible/ansible - override-checkout: stable-2.8 + override-checkout: stable-2.9 - name: openstack/openstacksdk override-checkout: master - name: openstack/devstack @@ -423,6 +400,10 @@ voting: false - ansible-collections-openstack-functional-devstack: voting: false + - openstacksdk-ansible-stable-2.8-functional-devstack: + voting: false + - openstacksdk-ansible-stable-2.9-functional-devstack: + voting: false gate: jobs: - openstacksdk-functional-devstack diff --git a/extras/run-ansible-tests.sh b/extras/run-ansible-tests.sh index e4ce80ea5..14ed166f2 100755 --- a/extras/run-ansible-tests.sh +++ b/extras/run-ansible-tests.sh @@ -30,12 +30,71 @@ # run-ansible-tests.sh -e ansible -c cloudX auth keypair network ############################################################################# -echo " - Thanks for submitting patch for Openstack Ansible modules! - We moved Openstack Ansible modules to Openstack repositories. - Next patches should be submitted not with Ansible Github but with - Openstack Gerrit: https://review.opendev.org/#/q/project:openstack/ansible-collections-openstack - Please submit your code there from now. - Thanks for your contribution and sorry for inconvienience. -" -exit 1 + +CLOUD="devstack-admin" +ENVDIR= +USE_DEV=0 + +while getopts "c:de:" opt +do + case $opt in + d) USE_DEV=1 ;; + c) CLOUD=${OPTARG} ;; + e) ENVDIR=${OPTARG} ;; + ?) echo "Invalid option: -${OPTARG}" + exit 1;; + esac +done + +if [ -z ${ENVDIR} ] +then + echo "Option -e is required" + exit 1 +fi + +shift $((OPTIND-1)) +TAGS=$( echo "$*" | tr ' ' , ) + +# We need to source the current tox environment so that Ansible will +# be setup for the correct python environment. +source $ENVDIR/bin/activate + +if [ ${USE_DEV} -eq 1 ] +then + if [ -d ${ENVDIR}/ansible ] + then + echo "Using existing Ansible source repo" + else + echo "Installing Ansible source repo at $ENVDIR" + git clone --recursive https://github.com/ansible/ansible.git ${ENVDIR}/ansible + fi + source $ENVDIR/ansible/hacking/env-setup +fi + +# Run the shade Ansible tests +tag_opt="" +if [ ! -z ${TAGS} ] +then + tag_opt="--tags ${TAGS}" +fi + +# Loop through all ANSIBLE_VAR_ environment variables to allow passing the further +for var in $(env | grep -e '^ANSIBLE_VAR_'); do + VAR_NAME=${var%%=*} # split variable name from value + ANSIBLE_VAR_NAME=${VAR_NAME#ANSIBLE_VAR_} # cut ANSIBLE_VAR_ prefix from variable name + ANSIBLE_VAR_NAME=${ANSIBLE_VAR_NAME,,} # lowercase ansible variable + ANSIBLE_VAR_VALUE=${!VAR_NAME} # Get the variable value + ANSIBLE_VARS+="${ANSIBLE_VAR_NAME}=${ANSIBLE_VAR_VALUE} " # concat variables +done + +# Until we have a module that lets us determine the image we want from +# within a playbook, we have to find the image here and pass it in. +# We use the openstack client instead of nova client since it can use clouds.yaml. +IMAGE=`openstack --os-cloud=${CLOUD} image list -f value -c Name | grep cirros | grep -v -e ramdisk -e kernel` +if [ $? -ne 0 ] +then + echo "Failed to find Cirros image" + exit 1 +fi + +ansible-playbook -vvv ./openstack/tests/ansible/run.yml -e "cloud=${CLOUD} image=${IMAGE} ${ANSIBLE_VARS}" ${tag_opt} diff --git a/openstack/tests/ansible/README.txt b/openstack/tests/ansible/README.txt new file mode 100644 index 000000000..3931b4af9 --- /dev/null +++ b/openstack/tests/ansible/README.txt @@ -0,0 +1,26 @@ +This directory contains a testing infrastructure for the Ansible +OpenStack modules. You will need a clouds.yaml file in order to run +the tests. You must provide a value for the `cloud` variable for each +run (using the -e option) as a default is not currently provided. + +If you want to run these tests against devstack, it is easiest to use +the tox target. This assumes you have a devstack-admin cloud defined +in your clouds.yaml file that points to devstack. Some examples of +using tox: + + tox -e ansible + + tox -e ansible keypair security_group + +If you want to run these tests directly, or against different clouds, +then you'll need to use the ansible-playbook command that comes with +the Ansible distribution and feed it the run.yml playbook. Some examples: + + # Run all module tests against a provider + ansible-playbook run.yml -e "cloud=hp" + + # Run only the keypair and security_group tests + ansible-playbook run.yml -e "cloud=hp" --tags "keypair,security_group" + + # Run all tests except security_group + ansible-playbook run.yml -e "cloud=hp" --skip-tags "security_group" diff --git a/openstack/tests/ansible/hooks/post_test_hook.sh b/openstack/tests/ansible/hooks/post_test_hook.sh new file mode 100755 index 000000000..bbda4af3b --- /dev/null +++ b/openstack/tests/ansible/hooks/post_test_hook.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +# 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. + +# TODO(shade) Rework for Zuul v3 + +export OPENSTACKSDK_DIR="$BASE/new/openstacksdk" + +cd $OPENSTACKSDK_DIR +sudo chown -R jenkins:stack $OPENSTACKSDK_DIR + +echo "Running shade Ansible test suite" + +if [ ${OPENSTACKSDK_ANSIBLE_DEV:-0} -eq 1 ] +then + # Use the upstream development version of Ansible + set +e + sudo -E -H -u jenkins tox -eansible -- -d + EXIT_CODE=$? + set -e +else + # Use the release version of Ansible + set +e + sudo -E -H -u jenkins tox -eansible + EXIT_CODE=$? + set -e +fi + + +exit $EXIT_CODE diff --git a/openstack/tests/ansible/roles/auth/tasks/main.yml b/openstack/tests/ansible/roles/auth/tasks/main.yml new file mode 100644 index 000000000..ca894e50a --- /dev/null +++ b/openstack/tests/ansible/roles/auth/tasks/main.yml @@ -0,0 +1,6 @@ +--- +- name: Authenticate to the cloud + os_auth: + cloud={{ cloud }} + +- debug: var=service_catalog diff --git a/openstack/tests/ansible/roles/client_config/tasks/main.yml b/openstack/tests/ansible/roles/client_config/tasks/main.yml new file mode 100644 index 000000000..1506f6d69 --- /dev/null +++ b/openstack/tests/ansible/roles/client_config/tasks/main.yml @@ -0,0 +1,7 @@ +--- +- name: List all profiles + os_client_config: + register: list + +# WARNING: This will output sensitive authentication information!!!! +- debug: var=list diff --git a/openstack/tests/ansible/roles/group/defaults/main.yml b/openstack/tests/ansible/roles/group/defaults/main.yml new file mode 100644 index 000000000..361c01190 --- /dev/null +++ b/openstack/tests/ansible/roles/group/defaults/main.yml @@ -0,0 +1 @@ +group_name: ansible_group diff --git a/openstack/tests/ansible/roles/group/tasks/main.yml b/openstack/tests/ansible/roles/group/tasks/main.yml new file mode 100644 index 000000000..535ed4318 --- /dev/null +++ b/openstack/tests/ansible/roles/group/tasks/main.yml @@ -0,0 +1,19 @@ +--- +- name: Create group + os_group: + cloud: "{{ cloud }}" + state: present + name: "{{ group_name }}" + +- name: Update group + os_group: + cloud: "{{ cloud }}" + state: present + name: "{{ group_name }}" + description: "updated description" + +- name: Delete group + os_group: + cloud: "{{ cloud }}" + state: absent + name: "{{ group_name }}" diff --git a/openstack/tests/ansible/roles/image/defaults/main.yml b/openstack/tests/ansible/roles/image/defaults/main.yml new file mode 100644 index 000000000..13efe7144 --- /dev/null +++ b/openstack/tests/ansible/roles/image/defaults/main.yml @@ -0,0 +1 @@ +image_name: ansible_image diff --git a/openstack/tests/ansible/roles/image/tasks/main.yml b/openstack/tests/ansible/roles/image/tasks/main.yml new file mode 100644 index 000000000..587e887b8 --- /dev/null +++ b/openstack/tests/ansible/roles/image/tasks/main.yml @@ -0,0 +1,54 @@ +--- +- name: Create a test image file + shell: mktemp + register: tmp_file + +- name: Fill test image file to 1MB + shell: truncate -s 1048576 {{ tmp_file.stdout }} + +- name: Create raw image (defaults) + os_image: + cloud: "{{ cloud }}" + state: present + name: "{{ image_name }}" + filename: "{{ tmp_file.stdout }}" + disk_format: raw + register: image + +- debug: var=image + +- name: Delete raw image (defaults) + os_image: + cloud: "{{ cloud }}" + state: absent + name: "{{ image_name }}" + +- name: Create raw image (complex) + os_image: + cloud: "{{ cloud }}" + state: present + name: "{{ image_name }}" + filename: "{{ tmp_file.stdout }}" + disk_format: raw + is_public: True + min_disk: 10 + min_ram: 1024 + kernel: cirros-vmlinuz + ramdisk: cirros-initrd + properties: + cpu_arch: x86_64 + distro: ubuntu + register: image + +- debug: var=image + +- name: Delete raw image (complex) + os_image: + cloud: "{{ cloud }}" + state: absent + name: "{{ image_name }}" + +- name: Delete test image file + file: + name: "{{ tmp_file.stdout }}" + state: absent diff --git a/openstack/tests/ansible/roles/keypair/defaults/main.yml b/openstack/tests/ansible/roles/keypair/defaults/main.yml new file mode 100644 index 000000000..3956b56a2 --- /dev/null +++ b/openstack/tests/ansible/roles/keypair/defaults/main.yml @@ -0,0 +1 @@ +keypair_name: shade_keypair diff --git a/openstack/tests/ansible/roles/keypair/tasks/main.yml b/openstack/tests/ansible/roles/keypair/tasks/main.yml new file mode 100644 index 000000000..636bf1aca --- /dev/null +++ b/openstack/tests/ansible/roles/keypair/tasks/main.yml @@ -0,0 +1,62 @@ +--- +- name: Create keypair (non-existing) + os_keypair: + cloud: "{{ cloud }}" + name: "{{ keypair_name }}" + state: present + register: + keypair + +# This assert verifies that Ansible is capable serializing data returned by SDK +- name: Ensure private key is returned + assert: + that: + - keypair.key.public_key is defined and keypair.key.public_key + +- name: Delete keypair (non-existing) + os_keypair: + cloud: "{{ cloud }}" + name: "{{ keypair_name }}" + state: absent + +- name: Generate test key file + user: + name: "{{ ansible_env.USER }}" + generate_ssh_key: yes + ssh_key_file: .ssh/shade_id_rsa + +- name: Create keypair (file) + os_keypair: + cloud: "{{ cloud }}" + name: "{{ keypair_name }}" + state: present + public_key_file: "{{ ansible_env.HOME }}/.ssh/shade_id_rsa.pub" + +- name: Delete keypair (file) + os_keypair: + cloud: "{{ cloud }}" + name: "{{ keypair_name }}" + state: absent + +- name: Create keypair (key) + os_keypair: + cloud: "{{ cloud }}" + name: "{{ keypair_name }}" + state: present + public_key: "{{ lookup('file', '~/.ssh/shade_id_rsa.pub') }}" + +- name: Delete keypair (key) + os_keypair: + cloud: "{{ cloud }}" + name: "{{ keypair_name }}" + state: absent + +- name: Delete test key pub file + file: + name: "{{ ansible_env.HOME }}/.ssh/shade_id_rsa.pub" + state: absent + +- name: Delete test key pvt file + file: + name: "{{ ansible_env.HOME }}/.ssh/shade_id_rsa" + state: absent diff --git a/openstack/tests/ansible/roles/keystone_domain/defaults/main.yml b/openstack/tests/ansible/roles/keystone_domain/defaults/main.yml new file mode 100644 index 000000000..049e7c378 --- /dev/null +++ b/openstack/tests/ansible/roles/keystone_domain/defaults/main.yml @@ -0,0 +1 @@ +domain_name: ansible_domain diff --git a/openstack/tests/ansible/roles/keystone_domain/tasks/main.yml b/openstack/tests/ansible/roles/keystone_domain/tasks/main.yml new file mode 100644 index 000000000..d1ca1273b --- /dev/null +++ b/openstack/tests/ansible/roles/keystone_domain/tasks/main.yml @@ -0,0 +1,19 @@ +--- +- name: Create keystone domain + os_keystone_domain: + cloud: "{{ cloud }}" + state: present + name: "{{ domain_name }}" + description: "test description" + +- name: Update keystone domain + os_keystone_domain: + cloud: "{{ cloud }}" + name: "{{ domain_name }}" + description: "updated description" + +- name: Delete keystone domain + os_keystone_domain: + cloud: "{{ cloud }}" + state: absent + name: "{{ domain_name }}" diff --git a/openstack/tests/ansible/roles/keystone_role/defaults/main.yml b/openstack/tests/ansible/roles/keystone_role/defaults/main.yml new file mode 100644 index 000000000..d1ebe5d1c --- /dev/null +++ b/openstack/tests/ansible/roles/keystone_role/defaults/main.yml @@ -0,0 +1 @@ +role_name: ansible_keystone_role diff --git a/openstack/tests/ansible/roles/keystone_role/tasks/main.yml b/openstack/tests/ansible/roles/keystone_role/tasks/main.yml new file mode 100644 index 000000000..110b4386b --- /dev/null +++ b/openstack/tests/ansible/roles/keystone_role/tasks/main.yml @@ -0,0 +1,12 @@ +--- +- name: Create keystone role + os_keystone_role: + cloud: "{{ cloud }}" + state: present + name: "{{ role_name }}" + +- name: Delete keystone role + os_keystone_role: + cloud: "{{ cloud }}" + state: absent + name: "{{ role_name }}" diff --git a/openstack/tests/ansible/roles/network/defaults/main.yml b/openstack/tests/ansible/roles/network/defaults/main.yml new file mode 100644 index 000000000..d5435ecb1 --- /dev/null +++ b/openstack/tests/ansible/roles/network/defaults/main.yml @@ -0,0 +1,3 @@ +network_name: shade_network +network_shared: false +network_external: false diff --git a/openstack/tests/ansible/roles/network/tasks/main.yml b/openstack/tests/ansible/roles/network/tasks/main.yml new file mode 100644 index 000000000..8a85c25cc --- /dev/null +++ b/openstack/tests/ansible/roles/network/tasks/main.yml @@ -0,0 +1,14 @@ +--- +- name: Create network + os_network: + cloud: "{{ cloud }}" + name: "{{ network_name }}" + state: present + shared: "{{ network_shared }}" + external: "{{ network_external }}" + +- name: Delete network + os_network: + cloud: "{{ cloud }}" + name: "{{ network_name }}" + state: absent diff --git a/openstack/tests/ansible/roles/nova_flavor/tasks/main.yml b/openstack/tests/ansible/roles/nova_flavor/tasks/main.yml new file mode 100644 index 000000000..c034bfc70 --- /dev/null +++ b/openstack/tests/ansible/roles/nova_flavor/tasks/main.yml @@ -0,0 +1,53 @@ +--- +- name: Create public flavor + os_nova_flavor: + cloud: "{{ cloud }}" + state: present + name: ansible_public_flavor + is_public: True + ram: 1024 + vcpus: 1 + disk: 10 + ephemeral: 10 + swap: 1 + flavorid: 12345 + +- name: Delete public flavor + os_nova_flavor: + cloud: "{{ cloud }}" + state: absent + name: ansible_public_flavor + +- name: Create private flavor + os_nova_flavor: + cloud: "{{ cloud }}" + state: present + name: ansible_private_flavor + is_public: False + ram: 1024 + vcpus: 1 + disk: 10 + ephemeral: 10 + swap: 1 + flavorid: 12345 + +- name: Delete private flavor + os_nova_flavor: + cloud: "{{ cloud }}" + state: absent + name: ansible_private_flavor + +- name: Create flavor (defaults) + os_nova_flavor: + cloud: "{{ cloud }}" + state: present + name: ansible_defaults_flavor + ram: 1024 + vcpus: 1 + disk: 10 + +- name: Delete flavor (defaults) + os_nova_flavor: + cloud: "{{ cloud }}" + state: absent + name: ansible_defaults_flavor diff --git a/openstack/tests/ansible/roles/object/tasks/main.yml b/openstack/tests/ansible/roles/object/tasks/main.yml new file mode 100644 index 000000000..ae54b6ba2 --- /dev/null +++ b/openstack/tests/ansible/roles/object/tasks/main.yml @@ -0,0 +1,37 @@ +--- +- name: Create a test object file + shell: mktemp + register: tmp_file + +- name: Create container + os_object: + cloud: "{{ cloud }}" + state: present + container: ansible_container + container_access: private + +- name: Put object + os_object: + cloud: "{{ cloud }}" + state: present + name: ansible_object + filename: "{{ tmp_file.stdout }}" + container: ansible_container + +- name: Delete object + os_object: + cloud: "{{ cloud }}" + state: absent + name: ansible_object + container: ansible_container + +- name: Delete container + os_object: + cloud: "{{ cloud }}" + state: absent + container: ansible_container + +- name: Delete test object file + file: + name: "{{ tmp_file.stdout }}" + state: absent diff --git a/openstack/tests/ansible/roles/port/defaults/main.yml b/openstack/tests/ansible/roles/port/defaults/main.yml new file mode 100644 index 000000000..de022001b --- /dev/null +++ b/openstack/tests/ansible/roles/port/defaults/main.yml @@ -0,0 +1,6 @@ +network_name: ansible_port_network +network_external: true +subnet_name: ansible_port_subnet +port_name: ansible_port +secgroup_name: ansible_port_secgroup +no_security_groups: True diff --git a/openstack/tests/ansible/roles/port/tasks/main.yml b/openstack/tests/ansible/roles/port/tasks/main.yml new file mode 100644 index 000000000..1a39140e5 --- /dev/null +++ b/openstack/tests/ansible/roles/port/tasks/main.yml @@ -0,0 +1,101 @@ +--- +- name: Create network + os_network: + cloud: "{{ cloud }}" + state: present + name: "{{ network_name }}" + external: "{{ network_external }}" + +- name: Create subnet + os_subnet: + cloud: "{{ cloud }}" + state: present + name: "{{ subnet_name }}" + network_name: "{{ network_name }}" + cidr: 10.5.5.0/24 + +- name: Create port (no security group or default security group) + os_port: + cloud: "{{ cloud }}" + state: present + name: "{{ port_name }}" + network: "{{ network_name }}" + no_security_groups: "{{ no_security_groups }}" + fixed_ips: + - ip_address: 10.5.5.69 + register: port + +- debug: var=port + +- name: Delete port (no security group or default security group) + os_port: + cloud: "{{ cloud }}" + state: absent + name: "{{ port_name }}" + +- name: Create security group + os_security_group: + cloud: "{{ cloud }}" + state: present + name: "{{ secgroup_name }}" + description: Test group + +- name: Create port (with security group) + os_port: + cloud: "{{ cloud }}" + state: present + name: "{{ port_name }}" + network: "{{ network_name }}" + fixed_ips: + - ip_address: 10.5.5.69 + security_groups: + - "{{ secgroup_name }}" + register: port + +- debug: var=port + +- name: Delete port (with security group) + os_port: + cloud: "{{ cloud }}" + state: absent + name: "{{ port_name }}" + +- name: Create port (with allowed_address_pairs and extra_dhcp_opts) + os_port: + cloud: "{{ cloud }}" + state: present + name: "{{ port_name }}" + network: "{{ network_name }}" + no_security_groups: "{{ no_security_groups }}" + allowed_address_pairs: + - ip_address: 10.6.7.0/24 + extra_dhcp_opts: + - opt_name: "bootfile-name" + opt_value: "testfile.1" + register: port + +- debug: var=port + +- name: Delete port (with allowed_address_pairs and extra_dhcp_opts) + os_port: + cloud: "{{ cloud }}" + state: absent + name: "{{ port_name }}" + +- name: Delete security group + os_security_group: + cloud: "{{ cloud }}" + state: absent + name: "{{ secgroup_name }}" + +- name: Delete subnet + os_subnet: + cloud: "{{ cloud }}" + state: absent + name: "{{ subnet_name }}" + +- name: Delete network + os_network: + cloud: "{{ cloud }}" + state: absent + name: "{{ network_name }}" diff --git a/openstack/tests/ansible/roles/router/defaults/main.yml b/openstack/tests/ansible/roles/router/defaults/main.yml new file mode 100644 index 000000000..f7d53933a --- /dev/null +++ b/openstack/tests/ansible/roles/router/defaults/main.yml @@ -0,0 +1,3 @@ +external_network_name: ansible_external_net +network_external: true +router_name: ansible_router diff --git a/openstack/tests/ansible/roles/router/tasks/main.yml b/openstack/tests/ansible/roles/router/tasks/main.yml new file mode 100644 index 000000000..083d4f066 --- /dev/null +++ b/openstack/tests/ansible/roles/router/tasks/main.yml @@ -0,0 +1,95 @@ +--- +# Regular user operation +- name: Create internal network + os_network: + cloud: "{{ cloud }}" + state: present + name: "{{ network_name }}" + external: false + +- name: Create subnet1 + os_subnet: + cloud: "{{ cloud }}" + state: present + network_name: "{{ network_name }}" + name: shade_subnet1 + cidr: 10.7.7.0/24 + +- name: Create router + os_router: + cloud: "{{ cloud }}" + state: present + name: "{{ router_name }}" + +- name: Update router (add interface) + os_router: + cloud: "{{ cloud }}" + state: present + name: "{{ router_name }}" + interfaces: + - shade_subnet1 + +# Admin operation +- name: Create external network + os_network: + cloud: "{{ cloud }}" + state: present + name: "{{ external_network_name }}" + external: "{{ network_external }}" + when: + - network_external + +- name: Create subnet2 + os_subnet: + cloud: "{{ cloud }}" + state: present + network_name: "{{ external_network_name }}" + name: shade_subnet2 + cidr: 10.6.6.0/24 + when: + - network_external + +- name: Update router (add external gateway) + os_router: + cloud: "{{ cloud }}" + state: present + name: "{{ router_name }}" + network: "{{ external_network_name }}" + interfaces: + - shade_subnet1 + when: + - network_external + +- name: Delete router + os_router: + cloud: "{{ cloud }}" + state: absent + name: "{{ router_name }}" + +- name: Delete subnet1 + os_subnet: + cloud: "{{ cloud }}" + state: absent + name: shade_subnet1 + +- name: Delete subnet2 + os_subnet: + cloud: "{{ cloud }}" + state: absent + name: shade_subnet2 + when: + - network_external + +- name: Delete internal network + os_network: + cloud: "{{ cloud }}" + state: absent + name: "{{ network_name }}" + +- name: Delete external network + os_network: + cloud: "{{ cloud }}" + state: absent + name: "{{ external_network_name }}" + when: + - network_external diff --git a/openstack/tests/ansible/roles/security_group/defaults/main.yml b/openstack/tests/ansible/roles/security_group/defaults/main.yml new file mode 100644 index 000000000..00310dd10 --- /dev/null +++ b/openstack/tests/ansible/roles/security_group/defaults/main.yml @@ -0,0 +1 @@ +secgroup_name: shade_secgroup diff --git a/openstack/tests/ansible/roles/security_group/tasks/main.yml b/openstack/tests/ansible/roles/security_group/tasks/main.yml new file mode 100644 index 000000000..ddc7e50cd --- /dev/null +++ b/openstack/tests/ansible/roles/security_group/tasks/main.yml @@ -0,0 +1,123 @@ +--- +- name: Create security group + os_security_group: + cloud: "{{ cloud }}" + name: "{{ secgroup_name }}" + state: present + description: Created from Ansible playbook + +- name: Create empty ICMP rule + os_security_group_rule: + cloud: "{{ cloud }}" + security_group: "{{ secgroup_name }}" + state: present + protocol: icmp + remote_ip_prefix: 0.0.0.0/0 + +- name: Create -1 ICMP rule + os_security_group_rule: + cloud: "{{ cloud }}" + security_group: "{{ secgroup_name }}" + state: present + protocol: icmp + port_range_min: -1 + port_range_max: -1 + remote_ip_prefix: 0.0.0.0/0 + +- name: Create empty TCP rule + os_security_group_rule: + cloud: "{{ cloud }}" + security_group: "{{ secgroup_name }}" + state: present + protocol: tcp + remote_ip_prefix: 0.0.0.0/0 + +- name: Create empty UDP rule + os_security_group_rule: + cloud: "{{ cloud }}" + security_group: "{{ secgroup_name }}" + state: present + protocol: udp + remote_ip_prefix: 0.0.0.0/0 + +- name: Create HTTP rule + os_security_group_rule: + cloud: "{{ cloud }}" + security_group: "{{ secgroup_name }}" + state: present + protocol: tcp + port_range_min: 80 + port_range_max: 80 + remote_ip_prefix: 0.0.0.0/0 + +- name: Create egress rule + os_security_group_rule: + cloud: "{{ cloud }}" + security_group: "{{ secgroup_name }}" + state: present + protocol: tcp + port_range_min: 30000 + port_range_max: 30001 + remote_ip_prefix: 0.0.0.0/0 + direction: egress + +- name: Delete empty ICMP rule + os_security_group_rule: + cloud: "{{ cloud }}" + security_group: "{{ secgroup_name }}" + state: absent + protocol: icmp + remote_ip_prefix: 0.0.0.0/0 + +- name: Delete -1 ICMP rule + os_security_group_rule: + cloud: "{{ cloud }}" + security_group: "{{ secgroup_name }}" + state: absent + protocol: icmp + port_range_min: -1 + port_range_max: -1 + remote_ip_prefix: 0.0.0.0/0 + +- name: Delete empty TCP rule + os_security_group_rule: + cloud: "{{ cloud }}" + security_group: "{{ secgroup_name }}" + state: absent + protocol: tcp + remote_ip_prefix: 0.0.0.0/0 + +- name: Delete empty UDP rule + os_security_group_rule: + cloud: "{{ cloud }}" + security_group: "{{ secgroup_name }}" + state: absent + protocol: udp + remote_ip_prefix: 0.0.0.0/0 + +- name: Delete HTTP rule + os_security_group_rule: + cloud: "{{ cloud }}" + security_group: "{{ secgroup_name }}" + state: absent + protocol: tcp + port_range_min: 80 + port_range_max: 80 + remote_ip_prefix: 0.0.0.0/0 + +- name: Delete egress rule + os_security_group_rule: + cloud: "{{ cloud }}" + security_group: "{{ secgroup_name }}" + state: absent + protocol: tcp + port_range_min: 30000 + port_range_max: 30001 + remote_ip_prefix: 0.0.0.0/0 + direction: egress + +- name: Delete security group + os_security_group: + cloud: "{{ cloud }}" + name: "{{ secgroup_name }}" + state: absent diff --git a/openstack/tests/ansible/roles/server/defaults/main.yaml b/openstack/tests/ansible/roles/server/defaults/main.yaml new file mode 100644 index 000000000..e3bd5f33b --- /dev/null +++ b/openstack/tests/ansible/roles/server/defaults/main.yaml @@ -0,0 +1,5 @@ +server_network: private +server_name: ansible_server +flavor: m1.tiny +floating_ip_pool_name: public +boot_volume_size: 5 diff --git a/openstack/tests/ansible/roles/server/tasks/main.yml b/openstack/tests/ansible/roles/server/tasks/main.yml new file mode 100644 index 000000000..ac0311554 --- /dev/null +++ b/openstack/tests/ansible/roles/server/tasks/main.yml @@ -0,0 +1,92 @@ +--- +- name: Create server with meta as CSV + os_server: + cloud: "{{ cloud }}" + state: present + name: "{{ server_name }}" + image: "{{ image }}" + flavor: "{{ flavor }}" + network: "{{ server_network }}" + auto_floating_ip: false + meta: "key1=value1,key2=value2" + wait: true + register: server + +- debug: var=server + +- name: Delete server with meta as CSV + os_server: + cloud: "{{ cloud }}" + state: absent + name: "{{ server_name }}" + wait: true + +- name: Create server with meta as dict + os_server: + cloud: "{{ cloud }}" + state: present + name: "{{ server_name }}" + image: "{{ image }}" + flavor: "{{ flavor }}" + auto_floating_ip: false + network: "{{ server_network }}" + meta: + key1: value1 + key2: value2 + wait: true + register: server + +- debug: var=server + +- name: Delete server with meta as dict + os_server: + cloud: "{{ cloud }}" + state: absent + name: "{{ server_name }}" + wait: true + +- name: Create server (FIP from pool/network) + os_server: + cloud: "{{ cloud }}" + state: present + name: "{{ server_name }}" + image: "{{ image }}" + flavor: "{{ flavor }}" + network: "{{ server_network }}" + floating_ip_pools: + - "{{ floating_ip_pool_name }}" + wait: true + register: server + +- debug: var=server + +- name: Delete server (FIP from pool/network) + os_server: + cloud: "{{ cloud }}" + state: absent + name: "{{ server_name }}" + wait: true + +- name: Create server from volume + os_server: + cloud: "{{ cloud }}" + state: present + name: "{{ server_name }}" + image: "{{ image }}" + flavor: "{{ flavor }}" + network: "{{ server_network }}" + auto_floating_ip: false + boot_from_volume: true + volume_size: "{{ boot_volume_size }}" + terminate_volume: true + wait: true + register: server + +- debug: var=server + +- name: Delete server with volume + os_server: + cloud: "{{ cloud }}" + state: absent + name: "{{ server_name }}" + wait: true diff --git a/openstack/tests/ansible/roles/subnet/defaults/main.yml b/openstack/tests/ansible/roles/subnet/defaults/main.yml new file mode 100644 index 000000000..5ccc85abc --- /dev/null +++ b/openstack/tests/ansible/roles/subnet/defaults/main.yml @@ -0,0 +1,2 @@ +subnet_name: shade_subnet +enable_subnet_dhcp: false diff --git a/openstack/tests/ansible/roles/subnet/tasks/main.yml b/openstack/tests/ansible/roles/subnet/tasks/main.yml new file mode 100644 index 000000000..a7ca490ad --- /dev/null +++ b/openstack/tests/ansible/roles/subnet/tasks/main.yml @@ -0,0 +1,43 @@ +--- +- name: Create network {{ network_name }} + os_network: + cloud: "{{ cloud }}" + name: "{{ network_name }}" + state: present + +- name: Create subnet {{ subnet_name }} on network {{ network_name }} + os_subnet: + cloud: "{{ cloud }}" + network_name: "{{ network_name }}" + name: "{{ subnet_name }}" + state: present + enable_dhcp: "{{ enable_subnet_dhcp }}" + dns_nameservers: + - 8.8.8.7 + - 8.8.8.8 + cidr: 192.168.0.0/24 + gateway_ip: 192.168.0.1 + allocation_pool_start: 192.168.0.2 + allocation_pool_end: 192.168.0.254 + +- name: Update subnet + os_subnet: + cloud: "{{ cloud }}" + network_name: "{{ network_name }}" + name: "{{ subnet_name }}" + state: present + dns_nameservers: + - 8.8.8.7 + cidr: 192.168.0.0/24 + +- name: Delete subnet {{ subnet_name }} + os_subnet: + cloud: "{{ cloud }}" + name: "{{ subnet_name }}" + state: absent + +- name: Delete network {{ network_name }} + os_network: + cloud: "{{ cloud }}" + name: "{{ network_name }}" + state: absent diff --git a/openstack/tests/ansible/roles/user/tasks/main.yml b/openstack/tests/ansible/roles/user/tasks/main.yml new file mode 100644 index 000000000..6585ca582 --- /dev/null +++ b/openstack/tests/ansible/roles/user/tasks/main.yml @@ -0,0 +1,30 @@ +--- +- name: Create user + os_user: + cloud: "{{ cloud }}" + state: present + name: ansible_user + password: secret + email: ansible.user@nowhere.net + domain: default + default_project: demo + register: user + +- debug: var=user + +- name: Update user + os_user: + cloud: "{{ cloud }}" + state: present + name: ansible_user + password: secret + email: updated.ansible.user@nowhere.net + register: updateduser + +- debug: var=updateduser + +- name: Delete user + os_user: + cloud: "{{ cloud }}" + state: absent + name: ansible_user diff --git a/openstack/tests/ansible/roles/user_group/tasks/main.yml b/openstack/tests/ansible/roles/user_group/tasks/main.yml new file mode 100644 index 000000000..a0074e2dc --- /dev/null +++ b/openstack/tests/ansible/roles/user_group/tasks/main.yml @@ -0,0 +1,31 @@ +--- +- name: Create user + os_user: + cloud: "{{ cloud }}" + state: present + name: ansible_user + password: secret + email: ansible.user@nowhere.net + domain: default + default_project: demo + register: user + +- name: Assign user to nonadmins group + os_user_group: + cloud: "{{ cloud }}" + state: present + user: ansible_user + group: nonadmins + +- name: Remove user from nonadmins group + os_user_group: + cloud: "{{ cloud }}" + state: absent + user: ansible_user + group: nonadmins + +- name: Delete user + os_user: + cloud: "{{ cloud }}" + state: absent + name: ansible_user diff --git a/openstack/tests/ansible/roles/volume/tasks/main.yml b/openstack/tests/ansible/roles/volume/tasks/main.yml new file mode 100644 index 000000000..1479a0030 --- /dev/null +++ b/openstack/tests/ansible/roles/volume/tasks/main.yml @@ -0,0 +1,17 @@ +--- +- name: Create volume + os_volume: + cloud: "{{ cloud }}" + state: present + size: 1 + display_name: ansible_volume + display_description: Test volume + register: vol + +- debug: var=vol + +- name: Delete volume + os_volume: + cloud: "{{ cloud }}" + state: absent + display_name: ansible_volume diff --git a/openstack/tests/ansible/run.yml b/openstack/tests/ansible/run.yml new file mode 100644 index 000000000..9340ccd06 --- /dev/null +++ b/openstack/tests/ansible/run.yml @@ -0,0 +1,26 @@ +--- +- hosts: localhost + connection: local + gather_facts: true + + roles: + - { role: auth, tags: auth } + - { role: client_config, tags: client_config } + - { role: group, tags: group } + # TODO(mordred) Reenable this once the fixed os_image winds up in an + # upstream ansible release. + # - { role: image, tags: image } + - { role: keypair, tags: keypair } + - { role: keystone_domain, tags: keystone_domain } + - { role: keystone_role, tags: keystone_role } + - { role: network, tags: network } + - { role: nova_flavor, tags: nova_flavor } + - { role: object, tags: object } + - { role: port, tags: port } + - { role: router, tags: router } + - { role: security_group, tags: security_group } + - { role: server, tags: server } + - { role: subnet, tags: subnet } + - { role: user, tags: user } + - { role: user_group, tags: user_group } + - { role: volume, tags: volume } From c6c69fc474ad103ba162f551d27347b3786f9a25 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 24 Mar 2020 07:50:18 -0500 Subject: [PATCH 2631/3836] Update Rackspace vendor profile for cinder v2 Rackspace only has v1 in the catalog but run a cinder that supports v2. Since SDK doesn't have support for v1 at all, update our vendor config with an endpoint override for their block-storage service. As a special bit of hell, the url needs the project_id appended which we cannot do with our normal format substitutions, so put in YET ANOTHER Rackspace specific workaround logic. Story: 2007459 Task: 39146 Change-Id: I4e6485b936b6b7303b6463cbacd3bf98746fc5e1 --- doc/source/user/config/vendor-support.rst | 5 ++++- openstack/config/cloud_region.py | 12 ++++++++++++ openstack/config/vendors/rackspace.json | 3 ++- .../rackspace-block-storage-v2-fe0dd69b9e037599.yaml | 6 ++++++ 4 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/rackspace-block-storage-v2-fe0dd69b9e037599.yaml diff --git a/doc/source/user/config/vendor-support.rst b/doc/source/user/config/vendor-support.rst index bfc01e39d..15d48b338 100644 --- a/doc/source/user/config/vendor-support.rst +++ b/doc/source/user/config/vendor-support.rst @@ -258,7 +258,10 @@ SYD Sydney, NSW * Uploaded Images need properties to not use vendor agent:: :vm_mode: hvm :xenapi_use_agent: False -* Volume API Version is 1 +* Block Storage API Version is 2 +* The Block Storage API supports version 2 but only version 1 is in + the catalog. The Block Storage endpoint is + https://{region_name}.blockstorage.api.rackspacecloud.com/v2/{project_id} * While passwords are recommended for use, API keys do work as well. The `rackspaceauth` python package must be installed, and then the following can be added to clouds.yaml:: diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index b44c89afc..4857ba3fe 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -475,6 +475,18 @@ def get_endpoint(self, service_type): # Specifically, looking up a list of projects/domains/system to # scope to. value = auth.get('auth_url') + # Because of course. Seriously. + # We have to override the Rackspace block-storage endpoint because + # only v1 is in the catalog but the service actually does support + # v2. But the endpoint needs the project_id. + service_type = self._service_type_manager.get_service_type( + service_type) + if ( + value + and self.config.get('profile') == 'rackspace' + and service_type == 'block-storage' + ): + value = value + auth.get('project_id') return value def get_endpoint_from_catalog( diff --git a/openstack/config/vendors/rackspace.json b/openstack/config/vendors/rackspace.json index 00a454ff3..53db96284 100644 --- a/openstack/config/vendors/rackspace.json +++ b/openstack/config/vendors/rackspace.json @@ -19,7 +19,8 @@ "floating_ip_source": "None", "secgroup_source": "None", "requires_floating_ip": false, - "block_storage_api_version": "1", + "block_storage_endpoint_override": "https://{region_name}.blockstorage.api.rackspacecloud.com/v2/", + "block_storage_api_version": "2", "disable_vendor_agent": { "vm_mode": "hvm", "xenapi_use_agent": "False" diff --git a/releasenotes/notes/rackspace-block-storage-v2-fe0dd69b9e037599.yaml b/releasenotes/notes/rackspace-block-storage-v2-fe0dd69b9e037599.yaml new file mode 100644 index 000000000..e6b81f92f --- /dev/null +++ b/releasenotes/notes/rackspace-block-storage-v2-fe0dd69b9e037599.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + Rackspace Cloud's vendor profile has been updated to use v2 + of the Block Storage API. This introduces an endpoint override + for the service based on ``region_name`` and ``project_id``. From 49c3bba90609d28e2d2d86255ff07656c27ca64a Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Mon, 23 Mar 2020 10:32:40 +0100 Subject: [PATCH 2632/3836] Add support for Federation Protocols Change-Id: I6b60b8ce6f3650adff4732f06b212da787866183 --- openstack/identity/v3/_proxy.py | 148 ++++++++++++++++++ openstack/identity/v3/federation_protocol.py | 43 +++++ .../identity/v3/test_federation_protocol.py | 55 +++++++ 3 files changed, 246 insertions(+) create mode 100644 openstack/identity/v3/federation_protocol.py create mode 100644 openstack/tests/unit/identity/v3/test_federation_protocol.py diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index f7da56a15..88261adf6 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -16,6 +16,7 @@ from openstack.identity.v3 import credential as _credential from openstack.identity.v3 import domain as _domain from openstack.identity.v3 import endpoint as _endpoint +from openstack.identity.v3 import federation_protocol as _federation_protocol from openstack.identity.v3 import group as _group from openstack.identity.v3 import identity_provider as _identity_provider from openstack.identity.v3 import limit as _limit @@ -1322,6 +1323,153 @@ def delete_application_credential(self, user, application_credential, user_id=user.id, ignore_missing=ignore_missing) + def create_federation_protocol(self, idp_id, **attrs): + """Create a new federation protocol from attributes + + :param idp_id: The ID of the identity provider or a + :class:`~openstack.identity.v3.identity_provider.IdentityProvider` + representing the identity provider the protocol is to be + attached to. + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.identity.v3.federation_protocol. + FederationProtocol`, comprised of the properties on the + FederationProtocol class. + + :returns: The results of federation protocol creation + :rtype: :class:`~openstack.identity.v3.federation_protocol. + FederationProtocol` + """ + + idp_cls = _identity_provider.IdentityProvider + if isinstance(idp_id, idp_cls): + idp_id = idp_id.id + return self._create(_federation_protocol.FederationProtocol, + idp_id=idp_id, **attrs) + + def delete_federation_protocol(self, idp_id, protocol, + ignore_missing=True): + """Delete a federation protocol + + :param idp_id: The ID of the identity provider or a + :class:`~openstack.identity.v3.identity_provider.IdentityProvider` + representing the identity provider the protocol is attached to. + Can be None if protocol is a + :class:`~openstack.identity.v3.federation_protocol. + FederationProtocol` instance. + :param protocol: The ID of a federation protocol or a + :class:`~openstack.identity.v3.federation_protocol. + FederationProtocol` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the federation protocol does not exist. When set to + ``True``, no exception will be set when attempting to delete a + nonexistent federation protocol. + + :returns: ``None`` + """ + cls = _federation_protocol.FederationProtocol + if idp_id is None and isinstance(protocol, cls): + idp_id = protocol.idp_id + idp_cls = _identity_provider.IdentityProvider + if isinstance(idp_id, idp_cls): + idp_id = idp_id.id + self._delete(cls, protocol, + ignore_missing=ignore_missing, idp_id=idp_id) + + def find_federation_protocol(self, idp_id, protocol, + ignore_missing=True): + """Find a single federation protocol + + :param idp_id: The ID of the identity provider or a + :class:`~openstack.identity.v3.identity_provider.IdentityProvider` + representing the identity provider the protocol is attached to. + :param protocol: The name or ID of a federation protocol. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the resource does not exist. When set to ``True``, None will + be returned when attempting to find a nonexistent resource. + :returns: One federation protocol or None + :rtype: :class:`~openstack.identity.v3.federation_protocol. + FederationProtocol` + """ + idp_cls = _identity_provider.IdentityProvider + if isinstance(idp_id, idp_cls): + idp_id = idp_id.id + return self._find(_federation_protocol.FederationProtocol, protocol, + ignore_missing=ignore_missing, idp_id=idp_id) + + def get_federation_protocol(self, idp_id, protocol): + """Get a single federation protocol + + :param idp_id: The ID of the identity provider or a + :class:`~openstack.identity.v3.identity_provider.IdentityProvider` + representing the identity provider the protocol is attached to. + Can be None if protocol is a + :class:`~openstack.identity.v3.federation_protocol. + :param protocol: The value can be the ID of a federation protocol or a + :class:`~openstack.identity.v3.federation_protocol. + FederationProtocol` + instance. + + :returns: One federation protocol + :rtype: :class:`~openstack.identity.v3.federation_protocol. + FederationProtocol` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + cls = _federation_protocol.FederationProtocol + if idp_id is None and isinstance(protocol, cls): + idp_id = protocol.idp_id + idp_cls = _identity_provider.IdentityProvider + if isinstance(idp_id, idp_cls): + idp_id = idp_id.id + return self._get(cls, protocol, idp_id=idp_id) + + def federation_protocols(self, idp_id, **query): + """Retrieve a generator of federation protocols + + :param idp_id: The ID of the identity provider or a + :class:`~openstack.identity.v3.identity_provider.IdentityProvider` + representing the identity provider the protocol is attached to. + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. + + :returns: A generator of federation protocol instances. + :rtype: :class:`~openstack.identity.v3.federation_protocol. + FederationProtocol` + """ + idp_cls = _identity_provider.IdentityProvider + if isinstance(idp_id, idp_cls): + idp_id = idp_id.id + return self._list(_federation_protocol.FederationProtocol, + idp_id=idp_id, **query) + + def update_federation_protocol(self, idp_id, protocol, **attrs): + """Update a federation protocol + + :param idp_id: The ID of the identity provider or a + :class:`~openstack.identity.v3.identity_provider.IdentityProvider` + representing the identity provider the protocol is attached to. + Can be None if protocol is a + :class:`~openstack.identity.v3.federation_protocol. + :param protocol: Either the ID of a federation protocol or a + :class:`~openstack.identity.v3.federation_protocol. + FederationProtocol` instance. + :attrs kwargs: The attributes to update on the federation protocol + represented by ``value``. + + :returns: The updated federation protocol + :rtype: :class:`~openstack.identity.v3.federation_protocol. + FederationProtocol` + """ + cls = _federation_protocol.FederationProtocol + if (idp_id is None) and (isinstance(protocol, cls)): + idp_id = protocol.idp_id + idp_cls = _identity_provider.IdentityProvider + if isinstance(idp_id, idp_cls): + idp_id = idp_id.id + return self._update(cls, protocol, idp_id=idp_id, **attrs) + def create_mapping(self, **attrs): """Create a new mapping from attributes diff --git a/openstack/identity/v3/federation_protocol.py b/openstack/identity/v3/federation_protocol.py new file mode 100644 index 000000000..d9fe6f115 --- /dev/null +++ b/openstack/identity/v3/federation_protocol.py @@ -0,0 +1,43 @@ +# 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 resource + + +class FederationProtocol(resource.Resource): + resource_key = 'protocol' + resources_key = 'protocols' + base_path = '/OS-FEDERATION/identity_providers/%(idp_id)s/protocols' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + create_exclude_id_from_body = True + create_method = 'PUT' + commit_method = 'PATCH' + + _query_mapping = resource.QueryParameters( + 'id', + ) + + # Properties + #: name of the protocol (read only) *Type: string* + name = resource.Body('id') + #: The ID of the identity provider the protocol is attached to. + # *Type: string* + idp_id = resource.URI('idp_id') + #: The definition of the protocol + # *Type: dict* + mapping_id = resource.Body('mapping_id') diff --git a/openstack/tests/unit/identity/v3/test_federation_protocol.py b/openstack/tests/unit/identity/v3/test_federation_protocol.py new file mode 100644 index 000000000..9176ae6c0 --- /dev/null +++ b/openstack/tests/unit/identity/v3/test_federation_protocol.py @@ -0,0 +1,55 @@ +# 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.tests.unit import base + +from openstack.identity.v3 import federation_protocol + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'id': IDENTIFIER, + 'idp_id': 'example_idp', + 'mapping_id': 'example_mapping', +} + + +class TestFederationProtocol(base.TestCase): + + def test_basic(self): + sot = federation_protocol.FederationProtocol() + self.assertEqual('protocol', sot.resource_key) + self.assertEqual('protocols', sot.resources_key) + self.assertEqual( + '/OS-FEDERATION/identity_providers/%(idp_id)s/protocols', + sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.create_exclude_id_from_body) + self.assertEqual('PATCH', sot.commit_method) + self.assertEqual('PUT', sot.create_method) + + self.assertDictEqual( + { + 'id': 'id', + 'limit': 'limit', + 'marker': 'marker', + }, + sot._query_mapping._mapping) + + def test_make_it(self): + sot = federation_protocol.FederationProtocol(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['idp_id'], sot.idp_id) + self.assertEqual(EXAMPLE['mapping_id'], sot.mapping_id) From ad170452b3dbb7f44aeac4d6f83cfd81ca819a27 Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Wed, 25 Mar 2020 17:46:43 +0100 Subject: [PATCH 2633/3836] Add Release notes entry for Identity v3 IDP, Mapping and Protocol Change-Id: I2baea5f2bea4cd126725f5819eb33712675002be --- .../notes/added-federation-support-3b65e531e57211f5.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 releasenotes/notes/added-federation-support-3b65e531e57211f5.yaml diff --git a/releasenotes/notes/added-federation-support-3b65e531e57211f5.yaml b/releasenotes/notes/added-federation-support-3b65e531e57211f5.yaml new file mode 100644 index 000000000..8c7880e60 --- /dev/null +++ b/releasenotes/notes/added-federation-support-3b65e531e57211f5.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Adds support to create and manage Identity v3 Federation resources - + Specifically, Identity Providers, Mappings and Federation Protocols. From 6535d6eafa651a526451a3bd31215be8c44115a7 Mon Sep 17 00:00:00 2001 From: "Jesse Pretorius (odyssey4me)" Date: Wed, 25 Mar 2020 20:06:51 +0000 Subject: [PATCH 2634/3836] [tests] Improve devstack/post playbook efficiency By adjusting the syntax to the newer format, the fetch-subunit role will be skipped entirely, rather than run with all its tasks skipped. This gives us results sooner and burns less electricity, making the world a better place. :) Change-Id: I6ae5a63051d512eecb2a603af9d74ec139eb0877 --- playbooks/devstack/post.yaml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/playbooks/devstack/post.yaml b/playbooks/devstack/post.yaml index 14b50edab..c2ebc5074 100644 --- a/playbooks/devstack/post.yaml +++ b/playbooks/devstack/post.yaml @@ -1,6 +1,9 @@ - hosts: all - roles: - - fetch-tox-output - - role: fetch-subunit-output + tasks: + - include_role: + name: fetch-tox-output + - include_role: + name: fetch-subunit-output when: fetch_subunit|default(true)|bool - - process-stackviz + - include_role: + name: process-stackviz From 4efa8d5753479243b0279147b37a50a21934049a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 25 Mar 2020 15:24:10 -0500 Subject: [PATCH 2635/3836] Add unit test for rackspace block-storage workaround The previous patch added a workaround for block-storage on rackspace. Add a unit test for the project_id logic. Change-Id: I466bb7fa3ac4c627e34ee36fe6a417079bdb0a3f --- .../tests/unit/config/test_cloud_config.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/openstack/tests/unit/config/test_cloud_config.py b/openstack/tests/unit/config/test_cloud_config.py index 5a0f03212..0e1f8cc1e 100644 --- a/openstack/tests/unit/config/test_cloud_config.py +++ b/openstack/tests/unit/config/test_cloud_config.py @@ -181,6 +181,32 @@ def test_getters(self): self.assertEqual(1, cc.get_connect_retries('compute')) self.assertEqual(3, cc.get_connect_retries('baremetal')) + def test_rackspace_workaround(self): + # We're skipping loader here, so we have to expand relevant + # parts from the rackspace profile. The thing we're testing + # is that the project_id logic works. + cc = cloud_region.CloudRegion("test1", "DFW", { + 'profile': 'rackspace', + 'region_name': 'DFW', + 'auth': {'project_id': '123456'}, + 'block_storage_endpoint_override': 'https://example.com/v2/', + }) + self.assertEqual( + 'https://example.com/v2/123456', + cc.get_endpoint('block-storage') + ) + + def test_rackspace_workaround_only_rax(self): + cc = cloud_region.CloudRegion("test1", "DFW", { + 'region_name': 'DFW', + 'auth': {'project_id': '123456'}, + 'block_storage_endpoint_override': 'https://example.com/v2/', + }) + self.assertEqual( + 'https://example.com/v2/', + cc.get_endpoint('block-storage') + ) + def test_get_region_name(self): def assert_region_name(default, compute): From 8e5893f0ad074a3b82d7e8f5b8821695ee6ec15b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 26 Mar 2020 10:33:22 -0500 Subject: [PATCH 2636/3836] Revert "Switch to futurist for concurrency" This partially reverts commit b6a22e3749555ecf9fea762dfc4935e312f6d8bb. The addition of futurist as a hard depend broke python2, and at the same time didn't mark the release as having done so. In looking at the issue we realized that we don't actually need the hard depend. Change-Id: I2d874f618f5b3f66d49cd2e964c6e05655f22c0f --- .zuul.yaml | 2 -- lower-constraints.txt | 1 - openstack/connection.py | 4 ++-- releasenotes/notes/revert-futurist-34acc42fd3f0e7f3.yaml | 8 ++++++++ requirements.txt | 2 +- 5 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/revert-futurist-34acc42fd3f0e7f3.yaml diff --git a/.zuul.yaml b/.zuul.yaml index 26a204990..f44c4e0b3 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -413,7 +413,6 @@ voting: false - osc-functional-devstack-tips: voting: false - - nodepool-functional-openstack-src # Ironic jobs, non-voting to avoid tight coupling - ironic-inspector-tempest-openstacksdk-src: voting: false @@ -428,4 +427,3 @@ - openstacksdk-functional-devstack - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-senlin - - nodepool-functional-openstack-src diff --git a/lower-constraints.txt b/lower-constraints.txt index 5760bc665..1f491875d 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -9,7 +9,6 @@ extras==1.0.0 fixtures==3.0.0 future==0.16.0 futures==3.0.0 -futurist==2.1.0 ipaddress==1.0.17 iso8601==0.1.11 jmespath==0.9.0 diff --git a/openstack/connection.py b/openstack/connection.py index 2ee65ec06..1da50c601 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -179,7 +179,7 @@ import warnings import weakref -import futurist +import concurrent.futures import keystoneauth1.exceptions import requestsexceptions import six @@ -504,7 +504,7 @@ def authorize(self): @property def _pool_executor(self): if not self.__pool_executor: - self.__pool_executor = futurist.ThreadPoolExecutor( + self.__pool_executor = concurrent.futures.ThreadPoolExecutor( max_workers=5) return self.__pool_executor diff --git a/releasenotes/notes/revert-futurist-34acc42fd3f0e7f3.yaml b/releasenotes/notes/revert-futurist-34acc42fd3f0e7f3.yaml new file mode 100644 index 000000000..28712cd8f --- /dev/null +++ b/releasenotes/notes/revert-futurist-34acc42fd3f0e7f3.yaml @@ -0,0 +1,8 @@ +--- +upgrade: + - | + Removed the dependency on futurist, which isn't necessary. + Users can still pass futurist executors if they want, as + the API is the same, but if nothing is passed, + ``concurrent.futures.ThreadPoolExecutor`` will be used as + the default. diff --git a/requirements.txt b/requirements.txt index bda4eb9ff..90a88c6e2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ munch>=2.1.0 # MIT decorator>=4.4.1 # BSD jmespath>=0.9.0 # MIT ipaddress>=1.0.17;python_version<'3.3' # PSF -futurist>=2.1.0 # Apache-2.0 +futures>=3.0.0;python_version=='2.7' or python_version=='2.6' # BSD iso8601>=0.1.11 # MIT netifaces>=0.10.4 # MIT From ab2fcb753f6e25cb810d6d19a4c358fdafd4b096 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 26 Mar 2020 13:15:50 +0100 Subject: [PATCH 2637/3836] Change default image type in the OTC vendor profile VHD is there was some really weird old reason, and since qcow2 is supported as well - use it as default. Change-Id: Ib246503eba0112122cc874a5772eea05f651a69d --- openstack/config/vendors/otc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/config/vendors/otc.json b/openstack/config/vendors/otc.json index 223d2892a..c289de8aa 100644 --- a/openstack/config/vendors/otc.json +++ b/openstack/config/vendors/otc.json @@ -9,7 +9,7 @@ ], "identity_api_version": "3", "interface": "public", - "image_format": "vhd", + "image_format": "qcow2", "vendor_hook": "otcextensions.sdk:load" } } From 793cc2c37ae315a88b82e29e6e2f8ee904835784 Mon Sep 17 00:00:00 2001 From: Duc Truong Date: Thu, 26 Mar 2020 08:52:18 -0700 Subject: [PATCH 2638/3836] Remove kwargs validation for identity project updates Keystone supports setting custom properties when updating projects [1]. This change removes the check in openstacksdk cloud that prevents users from passing in custom kwargs when calling update_project. [1] https://opendev.org/openstack/keystone/src/branch/master/keystone/resource/schema.py#L76 Change-Id: I8b193d10b599c484447a7f5e0e0377276c6139d8 Story: 2007469 Task: 39157 --- openstack/cloud/_identity.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index e03a1ed6a..b7fa7442e 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -98,7 +98,6 @@ def get_project(self, name_or_id, filters=None, domain_id=None): return _utils._get_entity(self, 'project', name_or_id, filters, domain_id=domain_id) - @_utils.valid_kwargs('description') def update_project(self, name_or_id, enabled=None, domain_id=None, **kwargs): with _utils.shade_exceptions( From 185e55bd77563de3642756f7bdb97c944f8bcc42 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 26 Mar 2020 10:18:33 -0500 Subject: [PATCH 2639/3836] Add python-requires entry indicating 3.5 is required We require 3.5 now but we didn't add the metadata. Add it. While OpenStack broadly only supports 3.6, openstacksdk has a wider audience, including nodepool which still uses 3.5. So add the 3.5 unit tests back. Use lower-constraints for py35 unit tests because the upper-constraints have versions of things that have droppped py35 support already. Depends-On: https://review.opendev.org/#/c/715467/ Change-Id: I680e0bab5ba3af7be5e914ece64c6bd25d1f2191 --- .zuul.yaml | 1 + releasenotes/notes/python-3.5-629817cec092d528.yaml | 8 ++++++++ setup.cfg | 2 ++ tox.ini | 4 ++++ 4 files changed, 15 insertions(+) create mode 100644 releasenotes/notes/python-3.5-629817cec092d528.yaml diff --git a/.zuul.yaml b/.zuul.yaml index f44c4e0b3..9636c68be 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -393,6 +393,7 @@ templates: - check-requirements - openstack-lower-constraints-jobs + - openstack-python35-jobs - openstack-python3-ussuri-jobs - openstacksdk-functional-tips - openstacksdk-tox-tips diff --git a/releasenotes/notes/python-3.5-629817cec092d528.yaml b/releasenotes/notes/python-3.5-629817cec092d528.yaml new file mode 100644 index 000000000..472ef33f4 --- /dev/null +++ b/releasenotes/notes/python-3.5-629817cec092d528.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + openstacksdk does not test or support python2 as of 0.40, + but the releases have still accidentally worked (except for + 0.44 which was broken for python2). We're now explicitly + marking releases as requiring >= 3.5 so that things don't + attempt to install something that's bound to be broken. diff --git a/setup.cfg b/setup.cfg index 9267c6ec8..7fa5b3434 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,8 +14,10 @@ classifier = Operating System :: POSIX :: Linux Programming Language :: Python Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 +python-requires = >=3.5 [files] packages = diff --git a/tox.ini b/tox.ini index 2072fcbf7..825944cf0 100644 --- a/tox.ini +++ b/tox.ini @@ -127,3 +127,7 @@ deps = -c{toxinidir}/lower-constraints.txt -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt + +[testenv:py35] +basepython = python3.5 +deps = {[testenv:lower-constraints]deps} From e7237b468fb57b2e77147cbec429851ac97722df Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 27 Mar 2020 09:54:47 -0500 Subject: [PATCH 2640/3836] Turn off test log capture for now We're hitting memory pressure test failures on test nodes. Hypothesis is that we've got things logging too much and buffering to RAM. While we track that down, turn off auto-logging. Change-Id: Ie124f8dbd18600508a4a99ef91a5e0f5df00093c --- openstack/tests/base.py | 3 ++- tox.ini | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openstack/tests/base.py b/openstack/tests/base.py index b0d87a6b3..6f381a677 100644 --- a/openstack/tests/base.py +++ b/openstack/tests/base.py @@ -71,8 +71,9 @@ def setUp(self): logger.addHandler(handler) # Enable HTTP level tracing + # TODO(mordred) This is blowing out our memory we think logger = logging.getLogger('keystoneauth') - logger.setLevel(logging.DEBUG) + logger.setLevel(logging.INFO) logger.addHandler(handler) logger.propagate = False diff --git a/tox.ini b/tox.ini index 825944cf0..b55de9b3f 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,7 @@ setenv = LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=C - OS_LOG_CAPTURE={env:OS_LOG_CAPTURE:true} + OS_LOG_CAPTURE={env:OS_LOG_CAPTURE:false} OS_STDOUT_CAPTURE={env:OS_STDOUT_CAPTURE:true} OS_STDERR_CAPTURE={env:OS_STDERR_CAPTURE:true} deps = From 4c5cf2deea3f83101328f23b74ed75e1727f80c2 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Thu, 26 Mar 2020 21:53:16 +0100 Subject: [PATCH 2641/3836] Small cleanups after Python2 drop Remove requirements only needed by Python prior to version 3.5. Remove python 2.7 code from setup.py. Remove from setup.py ancient sections. Change-Id: Ie223b743a0f8def53874bfbbb5f9000ad26f030b --- doc/requirements.txt | 3 +-- requirements.txt | 2 -- setup.cfg | 12 ------------ setup.py | 8 -------- 4 files changed, 1 insertion(+), 24 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index efcc706d8..a1d8824e7 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,8 +1,7 @@ # 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. -sphinx>=1.8.0,<2.0.0;python_version=='2.7' # BSD -sphinx>=1.8.0,!=2.1.0;python_version>='3.4' # BSD +sphinx>=1.8.0,!=2.1.0 # BSD docutils>=0.11 # OSI-Approved Open Source, Public Domain openstackdocstheme>=1.20.0 # Apache-2.0 beautifulsoup4>=4.6.0 # MIT diff --git a/requirements.txt b/requirements.txt index 90a88c6e2..6d60847b4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,8 +13,6 @@ keystoneauth1>=3.18.0 # Apache-2.0 munch>=2.1.0 # MIT decorator>=4.4.1 # BSD jmespath>=0.9.0 # MIT -ipaddress>=1.0.17;python_version<'3.3' # PSF -futures>=3.0.0;python_version=='2.7' or python_version=='2.6' # BSD iso8601>=0.1.11 # MIT netifaces>=0.10.4 # MIT diff --git a/setup.cfg b/setup.cfg index 7fa5b3434..07dd21493 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,15 +27,3 @@ packages = [entry_points] console_scripts = openstack-inventory = openstack.cloud.cmd.inventory:main - -[build_sphinx] -source-dir = doc/source -build-dir = doc/build -all_files = 1 -warning-is-error = 1 - -[upload_sphinx] -upload-dir = doc/build/html - -[wheel] -universal = 1 diff --git a/setup.py b/setup.py index 566d84432..f63cc23c5 100644 --- a/setup.py +++ b/setup.py @@ -16,14 +16,6 @@ # 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) From d404a6021c0ae0da9d02707e5c8979e4875a825c Mon Sep 17 00:00:00 2001 From: Sam Morrison Date: Mon, 23 Mar 2020 13:02:11 +1100 Subject: [PATCH 2642/3836] Add availability zone and availability zone profile resources to load balancer Change-Id: I979a5b84492e73f02417004fb74cf0523d2f9a20 --- doc/source/user/proxies/load_balancer_v2.rst | 25 +++ .../user/resources/load_balancer/index.rst | 2 + .../load_balancer/v2/availability_zone.rst | 13 ++ .../v2/availability_zone_profile.rst | 13 ++ openstack/load_balancer/v2/_proxy.py | 178 ++++++++++++++++++ .../load_balancer/v2/availability_zone.py | 43 +++++ .../v2/availability_zone_profile.py | 41 ++++ .../load_balancer/v2/test_load_balancer.py | 114 ++++++++++- .../load_balancer/test_availability_zone.py | 59 ++++++ .../test_availability_zone_profile.py | 59 ++++++ .../tests/unit/load_balancer/test_proxy.py | 51 +++++ ...d-az-to-loadbalancer-da9bf1baaedc89a4.yaml | 5 + 12 files changed, 599 insertions(+), 4 deletions(-) create mode 100644 doc/source/user/resources/load_balancer/v2/availability_zone.rst create mode 100644 doc/source/user/resources/load_balancer/v2/availability_zone_profile.rst create mode 100644 openstack/load_balancer/v2/availability_zone.py create mode 100644 openstack/load_balancer/v2/availability_zone_profile.py create mode 100644 openstack/tests/unit/load_balancer/test_availability_zone.py create mode 100644 openstack/tests/unit/load_balancer/test_availability_zone_profile.py create mode 100644 releasenotes/notes/add-az-to-loadbalancer-da9bf1baaedc89a4.yaml diff --git a/doc/source/user/proxies/load_balancer_v2.rst b/doc/source/user/proxies/load_balancer_v2.rst index 26e23881d..ea91fd87f 100644 --- a/doc/source/user/proxies/load_balancer_v2.rst +++ b/doc/source/user/proxies/load_balancer_v2.rst @@ -150,3 +150,28 @@ Amphora Operations .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_amphora .. automethod:: openstack.load_balancer.v2._proxy.Proxy.configure_amphora .. automethod:: openstack.load_balancer.v2._proxy.Proxy.failover_amphora + +Availability Zone Profile Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.load_balancer.v2._proxy.Proxy + + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.create_availability_zone_profile + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_availability_zone_profile + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.availability_zone_profiles + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_availability_zone_profile + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_availability_zone_profile + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_availability_zone_profile + +Availability Zone Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.load_balancer.v2._proxy.Proxy + + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.create_availability_zone + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_availability_zone + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.availability_zones + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_availability_zone + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_availability_zone + .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_availability_zone + diff --git a/doc/source/user/resources/load_balancer/index.rst b/doc/source/user/resources/load_balancer/index.rst index c34a7783f..1eebf3787 100644 --- a/doc/source/user/resources/load_balancer/index.rst +++ b/doc/source/user/resources/load_balancer/index.rst @@ -16,3 +16,5 @@ Load Balancer Resources v2/flavor v2/quota v2/amphora + v2/availability_zone_profile + v2/availability_zone diff --git a/doc/source/user/resources/load_balancer/v2/availability_zone.rst b/doc/source/user/resources/load_balancer/v2/availability_zone.rst new file mode 100644 index 000000000..8f8889b50 --- /dev/null +++ b/doc/source/user/resources/load_balancer/v2/availability_zone.rst @@ -0,0 +1,13 @@ +openstack.load_balancer.v2.availability_zone +============================================ + +.. automodule:: openstack.load_balancer.v2.availability_zone + +The AvailabilityZone Class +-------------------------- + +The ``AvailabilityZone`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.load_balancer.v2.availability_zone.AvailabilityZone + :members: diff --git a/doc/source/user/resources/load_balancer/v2/availability_zone_profile.rst b/doc/source/user/resources/load_balancer/v2/availability_zone_profile.rst new file mode 100644 index 000000000..0aa2c6a3b --- /dev/null +++ b/doc/source/user/resources/load_balancer/v2/availability_zone_profile.rst @@ -0,0 +1,13 @@ +openstack.load_balancer.v2.availability_zone_profile +==================================================== + +.. automodule:: openstack.load_balancer.v2.availability_zone_profile + +The AvailabilityZoneProfile Class +--------------------------------- + +The ``AvailabilityZoneProfile`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.load_balancer.v2.availability_zone_profile.AvailabilityZoneProfile + :members: diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index 6b1f8c7e2..1b9ae05dd 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -11,6 +11,9 @@ # under the License. from openstack.load_balancer.v2 import amphora as _amphora +from openstack.load_balancer.v2 import availability_zone as _availability_zone +from openstack.load_balancer.v2 import availability_zone_profile as \ + _availability_zone_profile from openstack.load_balancer.v2 import flavor as _flavor from openstack.load_balancer.v2 import flavor_profile as _flavor_profile from openstack.load_balancer.v2 import health_monitor as _hm @@ -1005,3 +1008,178 @@ def failover_amphora(self, amphora_id, **attrs): :returns: ``None`` """ return self._update(_amphora.AmphoraFailover, amphora_id=amphora_id) + + def create_availability_zone_profile(self, **attrs): + """Create a new availability zone profile from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.load_balancer.v2. + availability_zone_profile.AvailabilityZoneProfile`, + comprised of the properties on the + AvailabilityZoneProfile class. + + :returns: The results of profile creation creation + :rtype: :class:`~openstack.load_balancer.v2.availability_zone_profile. + AvailabilityZoneProfile` + """ + return self._create(_availability_zone_profile.AvailabilityZoneProfile, + **attrs) + + def get_availability_zone_profile(self, *attrs): + """Get an availability zone profile + + :param availability_zone_profile: The value can be the name of an + availability_zone profile + or :class:`~openstack.load_balancer.v2.availability_zone_profile. + AvailabilityZoneProfile` instance. + + :returns: One + :class:`~openstack.load_balancer.v2.availability_zone_profile.AvailabilityZoneProfile` + """ + return self._get(_availability_zone_profile.AvailabilityZoneProfile, + *attrs) + + def availability_zone_profiles(self, **query): + """Retrieve a generator of availability zone profiles + + :returns: A generator of availability zone profiles instances + """ + return self._list(_availability_zone_profile.AvailabilityZoneProfile, + **query) + + def delete_availability_zone_profile(self, availability_zone_profile, + ignore_missing=True): + """Delete an availability zone profile + + :param availability_zone_profile: The availability_zone_profile can be + either the name or a + :class:`~openstack.load_balancer.v2.availability_zone_profile.AvailabilityZoneProfile` + instance + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the availability zone profile does not exist. + When set to ``True``, no exception will be set when attempting to + delete a nonexistent availability zone profile. + + :returns: ``None`` + """ + self._delete(_availability_zone_profile.AvailabilityZoneProfile, + availability_zone_profile, ignore_missing=ignore_missing) + + def find_availability_zone_profile(self, name_or_id, ignore_missing=True): + """Find a single availability zone profile + + :param name_or_id: The name or ID of a availability zone profile + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the availability zone profile does not exist. + When set to ``True``, no exception will be set when attempting + to delete a nonexistent availability zone profile. + + :returns: ``None`` + """ + return self._find(_availability_zone_profile.AvailabilityZoneProfile, + name_or_id, ignore_missing=ignore_missing) + + def update_availability_zone_profile(self, availability_zone_profile, + **attrs): + """Update an availability zone profile + + :param availability_zone_profile: The availability_zone_profile can be + either the name or a + :class:`~openstack.load_balancer.v2.availability_zone_profile.AvailabilityZoneProfile` + instance + :param dict attrs: The attributes to update on the availability_zone + profile represented by + ``availability_zone_profile``. + + :returns: The updated availability zone profile + :rtype: :class:`~openstack.load_balancer.v2.availability_zone_profile. + AvailabilityZoneProfile` + """ + return self._update(_availability_zone_profile.AvailabilityZoneProfile, + availability_zone_profile, **attrs) + + def create_availability_zone(self, **attrs): + """Create a new availability zone from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.load_balancer.v2. + availability_zone.AvailabilityZone`, comprised of + the properties on the AvailabilityZoneclass. + + :returns: The results of availability_zone creation creation + :rtype: + :class:`~openstack.load_balancer.v2.availability_zone.AvailabilityZone` + """ + return self._create(_availability_zone.AvailabilityZone, **attrs) + + def get_availability_zone(self, *attrs): + """Get an availability zone + + :param availability_zone: The value can be the name of a + availability_zone or + :class:`~openstack.load_balancer.v2.availability_zone.AvailabilityZone` + instance. + + :returns: One + :class:`~openstack.load_balancer.v2.availability_zone.AvailabilityZone` + """ + return self._get(_availability_zone.AvailabilityZone, *attrs) + + def availability_zones(self, **query): + """Retrieve a generator of availability zones + + :returns: A generator of availability zone instances + """ + return self._list(_availability_zone.AvailabilityZone, **query) + + def delete_availability_zone(self, availability_zone, ignore_missing=True): + """Delete an availability_zone + + :param availability_zone: The availability_zone can be either the name + or a + :class:`~openstack.load_balancer.v2.availability_zone.AvailabilityZone` + instance + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the availability zone does not exist. + When set to ``True``, no exception will be set when attempting to + delete a nonexistent availability zone. + + :returns: ``None`` + """ + self._delete(_availability_zone.AvailabilityZone, availability_zone, + ignore_missing=ignore_missing) + + def find_availability_zone(self, name_or_id, ignore_missing=True): + """Find a single availability zone + + :param name_or_id: The name or ID of a availability zone + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the availability zone does not exist. + When set to ``True``, no exception will be set when attempting + to delete a nonexistent availability zone. + + :returns: ``None`` + """ + return self._find(_availability_zone.AvailabilityZone, name_or_id, + ignore_missing=ignore_missing) + + def update_availability_zone(self, availability_zone, **attrs): + """Update an availability zone + + :param availability_zone: The availability_zone can be either the name + or a + :class:`~openstack.load_balancer.v2.availability_zone.AvailabilityZone` + instance + :param dict attrs: The attributes to update on the availability_zone + represented by ``availability_zone``. + + :returns: The updated availability_zone + :rtype: + :class:`~openstack.load_balancer.v2.availability_zone.AvailabilityZone` + """ + return self._update(_availability_zone.AvailabilityZone, + availability_zone, **attrs) diff --git a/openstack/load_balancer/v2/availability_zone.py b/openstack/load_balancer/v2/availability_zone.py new file mode 100644 index 000000000..9be7a4167 --- /dev/null +++ b/openstack/load_balancer/v2/availability_zone.py @@ -0,0 +1,43 @@ +# +# 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 resource + + +class AvailabilityZone(resource.Resource): + resource_key = 'availability_zone' + resources_key = 'availability_zones' + base_path = '/lbaas/availabilityzones' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + _query_mapping = resource.QueryParameters( + 'name', 'description', 'availability_zone_profile_id', + is_enabled='enabled' + ) + + # Properties + #: The name of the availability zone. + name = resource.Body('name') + #: The availability zone description. + description = resource.Body('description') + #: The associated availability zone profile ID + availability_zone_profile_id = resource.Body( + 'availability_zone_profile_id') + #: Whether the availability zone is enabled for use or not. + is_enabled = resource.Body('enabled') diff --git a/openstack/load_balancer/v2/availability_zone_profile.py b/openstack/load_balancer/v2/availability_zone_profile.py new file mode 100644 index 000000000..dbe72e616 --- /dev/null +++ b/openstack/load_balancer/v2/availability_zone_profile.py @@ -0,0 +1,41 @@ +# +# 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 resource + + +class AvailabilityZoneProfile(resource.Resource): + resource_key = 'availability_zone_profile' + resources_key = 'availability_zone_profiles' + base_path = '/lbaas/availabilityzoneprofiles' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + _query_mapping = resource.QueryParameters( + 'id', 'name', 'provider_name', 'availability_zone_data' + ) + + # Properties + #: The ID of the availability zone profile. + id = resource.Body('id') + #: The name of the availability zone profile. + name = resource.Body('name') + #: The provider this availability zone profile is for. + provider_name = resource.Body('provider_name') + #: The JSON string containing the availability zone metadata. + availability_zone_data = resource.Body('availability_zone_data') diff --git a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py index daae3b619..72d4d4bec 100644 --- a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py +++ b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.load_balancer.v2 import availability_zone +from openstack.load_balancer.v2 import availability_zone_profile from openstack.load_balancer.v2 import flavor from openstack.load_balancer.v2 import flavor_profile from openstack.load_balancer.v2 import health_monitor @@ -35,6 +37,7 @@ class TestLoadBalancer(base.BaseFunctionalTest): PROJECT_ID = None FLAVOR_PROFILE_ID = None FLAVOR_ID = None + AVAILABILITY_ZONE_PROFILE_ID = None AMPHORA_ID = None PROTOCOL = 'HTTP' PROTOCOL_PORT = 80 @@ -52,6 +55,7 @@ class TestLoadBalancer(base.BaseFunctionalTest): L7RULE_VALUE = 'example' AMPHORA = 'amphora' FLAVOR_DATA = '{"loadbalancer_topology": "SINGLE"}' + AVAILABILITY_ZONE_DATA = '{"compute_zone": "nova"}' DESCRIPTION = 'Test description' _wait_for_timeout_key = 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_LOAD_BALANCER' @@ -71,8 +75,11 @@ def setUp(self): self.MEMBER_NAME = self.getUniqueString() self.POOL_NAME = self.getUniqueString() self.UPDATE_NAME = self.getUniqueString() + self.UPDATE_DESCRIPTION = self.getUniqueString() self.FLAVOR_PROFILE_NAME = self.getUniqueString() self.FLAVOR_NAME = self.getUniqueString() + self.AVAILABILITY_ZONE_PROFILE_NAME = self.getUniqueString() + self.AVAILABILITY_ZONE_NAME = self.getUniqueString() subnets = list(self.conn.network.subnets()) self.VIP_SUBNET_ID = subnets[0].id self.PROJECT_ID = self.conn.session.get_project_id() @@ -85,12 +92,12 @@ def setUp(self): assert isinstance(test_quota, quota.Quota) self.assertEqual(self.PROJECT_ID, test_quota.id) - test_profile = self.conn.load_balancer.create_flavor_profile( + test_flavor_profile = self.conn.load_balancer.create_flavor_profile( name=self.FLAVOR_PROFILE_NAME, provider_name=self.AMPHORA, flavor_data=self.FLAVOR_DATA) - assert isinstance(test_profile, flavor_profile.FlavorProfile) - self.assertEqual(self.FLAVOR_PROFILE_NAME, test_profile.name) - self.FLAVOR_PROFILE_ID = test_profile.id + assert isinstance(test_flavor_profile, flavor_profile.FlavorProfile) + self.assertEqual(self.FLAVOR_PROFILE_NAME, test_flavor_profile.name) + self.FLAVOR_PROFILE_ID = test_flavor_profile.id test_flavor = self.conn.load_balancer.create_flavor( name=self.FLAVOR_NAME, flavor_profile_id=self.FLAVOR_PROFILE_ID, @@ -99,6 +106,24 @@ def setUp(self): self.assertEqual(self.FLAVOR_NAME, test_flavor.name) self.FLAVOR_ID = test_flavor.id + test_az_profile = \ + self.conn.load_balancer.create_availability_zone_profile( + name=self.AVAILABILITY_ZONE_PROFILE_NAME, + provider_name=self.AMPHORA, + availability_zone_data=self.AVAILABILITY_ZONE_DATA) + assert isinstance(test_az_profile, + availability_zone_profile.AvailabilityZoneProfile) + self.assertEqual(self.AVAILABILITY_ZONE_PROFILE_NAME, + test_az_profile.name) + self.AVAILABILITY_ZONE_PROFILE_ID = test_az_profile.id + + test_az = self.conn.load_balancer.create_availability_zone( + name=self.AVAILABILITY_ZONE_NAME, + availability_zone_profile_id=self.AVAILABILITY_ZONE_PROFILE_ID, + is_enabled=True, description=self.DESCRIPTION) + assert isinstance(test_az, availability_zone.AvailabilityZone) + self.assertEqual(self.AVAILABILITY_ZONE_NAME, test_az.name) + test_lb = self.conn.load_balancer.create_load_balancer( name=self.LB_NAME, vip_subnet_id=self.VIP_SUBNET_ID, project_id=self.PROJECT_ID) @@ -218,6 +243,12 @@ def tearDown(self): self.conn.load_balancer.delete_flavor_profile(self.FLAVOR_PROFILE_ID, ignore_missing=False) + self.conn.load_balancer.delete_availability_zone( + self.AVAILABILITY_ZONE_NAME, ignore_missing=False) + + self.conn.load_balancer.delete_availability_zone_profile( + self.AVAILABILITY_ZONE_PROFILE_ID, ignore_missing=False) + def test_lb_find(self): test_lb = self.conn.load_balancer.find_load_balancer(self.LB_NAME) self.assertEqual(self.LB_ID, test_lb.id) @@ -593,3 +624,78 @@ def test_amphora_failover(self): self.conn.load_balancer.failover_amphora(self.AMPHORA_ID) test_amp = self.conn.load_balancer.get_amphora(self.AMPHORA_ID) self.assertEqual(self.AMPHORA_ID, test_amp.id) + + def test_availability_zone_profile_find(self): + test_profile = self.conn.load_balancer.find_availability_zone_profile( + self.AVAILABILITY_ZONE_PROFILE_NAME) + self.assertEqual(self.AVAILABILITY_ZONE_PROFILE_ID, test_profile.id) + + def test_availability_zone_profile_get(self): + test_availability_zone_profile = \ + self.conn.load_balancer.get_availability_zone_profile( + self.AVAILABILITY_ZONE_PROFILE_ID) + self.assertEqual(self.AVAILABILITY_ZONE_PROFILE_NAME, + test_availability_zone_profile.name) + self.assertEqual(self.AVAILABILITY_ZONE_PROFILE_ID, + test_availability_zone_profile.id) + self.assertEqual(self.AMPHORA, + test_availability_zone_profile.provider_name) + self.assertEqual(self.AVAILABILITY_ZONE_DATA, + test_availability_zone_profile.availability_zone_data) + + def test_availability_zone_profile_list(self): + names = [az.name for az in + self.conn.load_balancer.availability_zone_profiles()] + self.assertIn(self.AVAILABILITY_ZONE_PROFILE_NAME, names) + + def test_availability_zone_profile_update(self): + self.conn.load_balancer.update_availability_zone_profile( + self.AVAILABILITY_ZONE_PROFILE_ID, name=self.UPDATE_NAME) + test_availability_zone_profile = \ + self.conn.load_balancer.get_availability_zone_profile( + self.AVAILABILITY_ZONE_PROFILE_ID) + self.assertEqual(self.UPDATE_NAME, test_availability_zone_profile.name) + + self.conn.load_balancer.update_availability_zone_profile( + self.AVAILABILITY_ZONE_PROFILE_ID, + name=self.AVAILABILITY_ZONE_PROFILE_NAME) + test_availability_zone_profile = \ + self.conn.load_balancer.get_availability_zone_profile( + self.AVAILABILITY_ZONE_PROFILE_ID) + self.assertEqual(self.AVAILABILITY_ZONE_PROFILE_NAME, + test_availability_zone_profile.name) + + def test_availability_zone_find(self): + test_availability_zone = \ + self.conn.load_balancer.find_availability_zone( + self.AVAILABILITY_ZONE_NAME) + self.assertEqual(self.AVAILABILITY_ZONE_NAME, + test_availability_zone.name) + + def test_availability_zone_get(self): + test_availability_zone = self.conn.load_balancer.get_availability_zone( + self.AVAILABILITY_ZONE_NAME) + self.assertEqual(self.AVAILABILITY_ZONE_NAME, + test_availability_zone.name) + self.assertEqual(self.DESCRIPTION, test_availability_zone.description) + self.assertEqual(self.AVAILABILITY_ZONE_PROFILE_ID, + test_availability_zone.availability_zone_profile_id) + + def test_availability_zone_list(self): + names = [az.name for az in + self.conn.load_balancer.availability_zones()] + self.assertIn(self.AVAILABILITY_ZONE_NAME, names) + + def test_availability_zone_update(self): + self.conn.load_balancer.update_availability_zone( + self.AVAILABILITY_ZONE_NAME, description=self.UPDATE_DESCRIPTION) + test_availability_zone = self.conn.load_balancer.get_availability_zone( + self.AVAILABILITY_ZONE_NAME) + self.assertEqual(self.UPDATE_DESCRIPTION, + test_availability_zone.description) + + self.conn.load_balancer.update_availability_zone( + self.AVAILABILITY_ZONE_NAME, description=self.DESCRIPTION) + test_availability_zone = self.conn.load_balancer.get_availability_zone( + self.AVAILABILITY_ZONE_NAME) + self.assertEqual(self.DESCRIPTION, test_availability_zone.description) diff --git a/openstack/tests/unit/load_balancer/test_availability_zone.py b/openstack/tests/unit/load_balancer/test_availability_zone.py new file mode 100644 index 000000000..3417ee734 --- /dev/null +++ b/openstack/tests/unit/load_balancer/test_availability_zone.py @@ -0,0 +1,59 @@ +# +# 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.tests.unit import base +import uuid + +from openstack.load_balancer.v2 import availability_zone + +AVAILABILITY_ZONE_PROFILE_ID = uuid.uuid4() +EXAMPLE = { + 'name': 'strawberry', + 'description': 'tasty', + 'is_enabled': False, + 'availability_zone_profile_id': AVAILABILITY_ZONE_PROFILE_ID} + + +class TestAvailabilityZone(base.TestCase): + + def test_basic(self): + test_availability_zone = availability_zone.AvailabilityZone() + self.assertEqual('availability_zone', + test_availability_zone.resource_key) + self.assertEqual('availability_zones', + test_availability_zone.resources_key) + self.assertEqual('/lbaas/availabilityzones', + test_availability_zone.base_path) + self.assertTrue(test_availability_zone.allow_create) + self.assertTrue(test_availability_zone.allow_fetch) + self.assertTrue(test_availability_zone.allow_commit) + self.assertTrue(test_availability_zone.allow_delete) + self.assertTrue(test_availability_zone.allow_list) + + def test_make_it(self): + test_availability_zone = availability_zone.AvailabilityZone(**EXAMPLE) + self.assertEqual(EXAMPLE['name'], test_availability_zone.name) + self.assertEqual(EXAMPLE['description'], + test_availability_zone.description) + self.assertFalse(test_availability_zone.is_enabled) + self.assertEqual(EXAMPLE['availability_zone_profile_id'], + test_availability_zone.availability_zone_profile_id) + + self.assertDictEqual( + {'limit': 'limit', + 'marker': 'marker', + 'name': 'name', + 'description': 'description', + 'is_enabled': 'enabled', + 'availability_zone_profile_id': 'availability_zone_profile_id'}, + test_availability_zone._query_mapping._mapping) diff --git a/openstack/tests/unit/load_balancer/test_availability_zone_profile.py b/openstack/tests/unit/load_balancer/test_availability_zone_profile.py new file mode 100644 index 000000000..82d1c0b9d --- /dev/null +++ b/openstack/tests/unit/load_balancer/test_availability_zone_profile.py @@ -0,0 +1,59 @@ +# +# 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.tests.unit import base +import uuid + +from openstack.load_balancer.v2 import availability_zone_profile + +IDENTIFIER = uuid.uuid4() +EXAMPLE = { + 'id': IDENTIFIER, + 'name': 'acidic', + 'provider_name': 'best', + 'availability_zone_data': '{"loadbalancer_topology": "SINGLE"}'} + + +class TestAvailabilityZoneProfile(base.TestCase): + + def test_basic(self): + test_profile = availability_zone_profile.AvailabilityZoneProfile() + self.assertEqual('availability_zone_profile', + test_profile.resource_key) + self.assertEqual('availability_zone_profiles', + test_profile.resources_key) + self.assertEqual('/lbaas/availabilityzoneprofiles', + test_profile.base_path) + self.assertTrue(test_profile.allow_create) + self.assertTrue(test_profile.allow_fetch) + self.assertTrue(test_profile.allow_commit) + self.assertTrue(test_profile.allow_delete) + self.assertTrue(test_profile.allow_list) + + def test_make_it(self): + test_profile = availability_zone_profile.AvailabilityZoneProfile( + **EXAMPLE) + self.assertEqual(EXAMPLE['id'], test_profile.id) + self.assertEqual(EXAMPLE['name'], test_profile.name) + self.assertEqual(EXAMPLE['provider_name'], test_profile.provider_name) + self.assertEqual(EXAMPLE['availability_zone_data'], + test_profile.availability_zone_data) + + self.assertDictEqual( + {'limit': 'limit', + 'marker': 'marker', + 'id': 'id', + 'name': 'name', + 'provider_name': 'provider_name', + 'availability_zone_data': 'availability_zone_data'}, + test_profile._query_mapping._mapping) diff --git a/openstack/tests/unit/load_balancer/test_proxy.py b/openstack/tests/unit/load_balancer/test_proxy.py index 0f90b6a66..954d47740 100644 --- a/openstack/tests/unit/load_balancer/test_proxy.py +++ b/openstack/tests/unit/load_balancer/test_proxy.py @@ -15,6 +15,8 @@ from openstack.load_balancer.v2 import _proxy from openstack.load_balancer.v2 import amphora +from openstack.load_balancer.v2 import availability_zone +from openstack.load_balancer.v2 import availability_zone_profile from openstack.load_balancer.v2 import flavor from openstack.load_balancer.v2 import flavor_profile from openstack.load_balancer.v2 import health_monitor @@ -388,3 +390,52 @@ def test_amphora_failover(self): value=[self.AMPHORA_ID], expected_args=[], expected_kwargs={'amphora_id': self.AMPHORA_ID}) + + def test_availability_zone_profiles(self): + self.verify_list(self.proxy.availability_zone_profiles, + availability_zone_profile.AvailabilityZoneProfile) + + def test_availability_zone_profile_get(self): + self.verify_get(self.proxy.get_availability_zone_profile, + availability_zone_profile.AvailabilityZoneProfile) + + def test_availability_zone_profile_create(self): + self.verify_create(self.proxy.create_availability_zone_profile, + availability_zone_profile.AvailabilityZoneProfile) + + def test_availability_zone_profile_delete(self): + self.verify_delete(self.proxy.delete_availability_zone_profile, + availability_zone_profile.AvailabilityZoneProfile, + True) + + def test_availability_zone_profile_find(self): + self.verify_find(self.proxy.find_availability_zone_profile, + availability_zone_profile.AvailabilityZoneProfile) + + def test_availability_zone_profile_update(self): + self.verify_update(self.proxy.update_availability_zone_profile, + availability_zone_profile.AvailabilityZoneProfile) + + def test_availability_zones(self): + self.verify_list(self.proxy.availability_zones, + availability_zone.AvailabilityZone) + + def test_availability_zone_get(self): + self.verify_get(self.proxy.get_availability_zone, + availability_zone.AvailabilityZone) + + def test_availability_zone_create(self): + self.verify_create(self.proxy.create_availability_zone, + availability_zone.AvailabilityZone) + + def test_availability_zone_delete(self): + self.verify_delete(self.proxy.delete_availability_zone, + availability_zone.AvailabilityZone, True) + + def test_availability_zone_find(self): + self.verify_find(self.proxy.find_availability_zone, + availability_zone.AvailabilityZone) + + def test_availability_zone_update(self): + self.verify_update(self.proxy.update_availability_zone, + availability_zone.AvailabilityZone) diff --git a/releasenotes/notes/add-az-to-loadbalancer-da9bf1baaedc89a4.yaml b/releasenotes/notes/add-az-to-loadbalancer-da9bf1baaedc89a4.yaml new file mode 100644 index 000000000..321d4a0f1 --- /dev/null +++ b/releasenotes/notes/add-az-to-loadbalancer-da9bf1baaedc89a4.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Adds Octavia (load_balancer) support for the availability zone and + availability zone profile APIs. From 01bfb89f3eafe04abf39eff7b1b1bcdfab8f12c4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 26 Mar 2020 10:27:45 -0500 Subject: [PATCH 2643/3836] Re-add nodepool functional test With the requires-python fix landed, we can re-add this test and wait for it to start passing again. Change-Id: Ia93a8685f7381e773e1139ca3f804ea354089eb3 --- .zuul.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.zuul.yaml b/.zuul.yaml index 9636c68be..1f5e161b8 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -403,6 +403,7 @@ - release-notes-jobs-python3 check: jobs: + - nodepool-functional-openstack-src - openstacksdk-functional-devstack - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-senlin @@ -425,6 +426,7 @@ voting: false gate: jobs: + - nodepool-functional-openstack-src - openstacksdk-functional-devstack - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-senlin From 8ca613e43fe9078676afbebf7ccf1eef9f1f3387 Mon Sep 17 00:00:00 2001 From: Sean McGinnis Date: Thu, 26 Mar 2020 05:40:28 -0500 Subject: [PATCH 2644/3836] Raise hacking to 2.x We have kept hacking capped below 1.2 for quite awhile. Newer versions of hacking pull in updated linters that have some good checks. We want to limit the maximum version of hacking installed since the global upper constraints does not do this for linters. Otherwise when a new release of hacking is available in the future, stable branches may suddenly be broken. Change-Id: I0e0ee8a169ae93f7efb2cda2b1d2458c1d49e46b Signed-off-by: Sean McGinnis --- openstack/__init__.py | 5 ++- openstack/baremetal/v1/node.py | 4 +- openstack/cloud/_dns.py | 2 +- openstack/cloud/_identity.py | 2 +- openstack/config/cloud_region.py | 2 +- openstack/network/v2/_proxy.py | 2 +- openstack/tests/unit/compute/v2/test_proxy.py | 42 +++++++++---------- test-requirements.txt | 2 +- tools/print-services.py | 1 + 9 files changed, 32 insertions(+), 30 deletions(-) diff --git a/openstack/__init__.py b/openstack/__init__.py index 06aa5236d..e7827c5d6 100644 --- a/openstack/__init__.py +++ b/openstack/__init__.py @@ -11,6 +11,7 @@ # 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 typing from openstack._log import enable_logging # noqa import openstack.config @@ -24,8 +25,8 @@ def connect( cloud=None, - app_name=None, # type: Optional[str] - app_version=None, # type: Optional[str] + app_name=None, # type: typing.Optional[str] + app_version=None, # type: typing.Optional[str] options=None, load_yaml_config=True, # type: bool load_envvars=True, # type: bool diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index c2bd09d60..5c034e723 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -515,8 +515,8 @@ def _check_state_reached(self, session, expected_state, elif not abort_on_failed_state: return False - if (self.provision_state.endswith(' failed') or - self.provision_state == 'error'): + if (self.provision_state.endswith(' failed') + or self.provision_state == 'error'): raise exceptions.ResourceFailure( "Node %(node)s reached failure state \"%(state)s\"; " "the last error is %(error)s" % diff --git a/openstack/cloud/_dns.py b/openstack/cloud/_dns.py index b663b06c7..8c4ce4427 100644 --- a/openstack/cloud/_dns.py +++ b/openstack/cloud/_dns.py @@ -100,7 +100,7 @@ def create_zone(self, name, zone_type=None, email=None, description=None, try: return self.dns.create_zone(**zone) - except exceptions.SDKException as e: + except exceptions.SDKException: raise exc.OpenStackCloudException( "Unable to create zone {name}".format(name=name) ) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index e03a1ed6a..af94c8a87 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -209,7 +209,7 @@ def search_users(self, name_or_id=None, filters=None, **kwargs): # side filter for user name https://bit.ly/2qh0Ijk # especially important when using LDAP and using page to limit results if name_or_id and not _utils._is_uuid_like(name_or_id): - kwargs['name'] = name_or_id + kwargs['name'] = name_or_id users = self.list_users(**kwargs) return _utils._filter_list(users, name_or_id, filters) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 6862230f8..96a126381 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -501,7 +501,7 @@ def get_endpoint(self, service_type): and self.config.get('profile') == 'rackspace' and service_type == 'block-storage' ): - value = value + auth.get('project_id') + value = value + auth.get('project_id') return value def get_endpoint_from_catalog( diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 68834a61c..1b4a40534 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -3976,7 +3976,7 @@ def floating_ip_port_forwardings(self, floating_ip, **query): :param floating_ip: The value can be the ID of the Floating IP that the port forwarding belongs or a :class:`~openstack. network.v2.floating_ip.FloatingIP` instance. - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs **query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of floating ip port forwarding objects :rtype: :class:`~openstack.network.v2.port_forwarding. diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 64d025c58..c1757c890 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -454,27 +454,27 @@ def test_create_image(self): with mock.patch('openstack.compute.v2.server.Server.create_image') \ as ci_mock: - ci_mock.return_value = 'image_id' - connection_mock = mock.Mock() - connection_mock.get_image = mock.Mock(return_value='image') - connection_mock.wait_for_image = mock.Mock() - self.proxy._connection = connection_mock - - rsp = self.proxy.create_server_image( - 'server', 'image_name', metadata, wait=True, timeout=1) - - ci_mock.assert_called_with( - self.proxy, - 'image_name', - metadata - ) - - self.proxy._connection.get_image.assert_called_with('image_id') - self.proxy._connection.wait_for_image.assert_called_with( - 'image', - timeout=1) - - self.assertEqual(connection_mock.wait_for_image(), rsp) + ci_mock.return_value = 'image_id' + connection_mock = mock.Mock() + connection_mock.get_image = mock.Mock(return_value='image') + connection_mock.wait_for_image = mock.Mock() + self.proxy._connection = connection_mock + + rsp = self.proxy.create_server_image( + 'server', 'image_name', metadata, wait=True, timeout=1) + + ci_mock.assert_called_with( + self.proxy, + 'image_name', + metadata + ) + + self.proxy._connection.get_image.assert_called_with('image_id') + self.proxy._connection.wait_for_image.assert_called_with( + 'image', + timeout=1) + + self.assertEqual(connection_mock.wait_for_image(), rsp) def test_server_group_create(self): self.verify_create(self.proxy.create_server_group, diff --git a/test-requirements.txt b/test-requirements.txt index bbd290487..ad440b8cc 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,7 @@ # 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>=1.0,<1.2 # Apache-2.0 +hacking>=2.0,<2.1.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 ddt>=1.0.1 # MIT diff --git a/tools/print-services.py b/tools/print-services.py index 33872acd1..b4c664957 100644 --- a/tools/print-services.py +++ b/tools/print-services.py @@ -125,4 +125,5 @@ def _find_service_description_class(service_type): service_description_class = getattr(service_description_module, class_name) return service_description_class + make_names() From a0d75007a8e3197b3bd2d879556553bbc8b0ffb6 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Sun, 29 Mar 2020 12:13:35 +0200 Subject: [PATCH 2645/3836] Update local hacking checks Update local hacking checks for new flake8. Change-Id: I1031badf36857a22efcd6ce6124f9db48bc18995 --- openstack/_hacking.py | 7 +++---- tox.ini | 8 +++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/openstack/_hacking.py b/openstack/_hacking.py index 94952630c..01b05adfc 100644 --- a/openstack/_hacking.py +++ b/openstack/_hacking.py @@ -14,6 +14,8 @@ import re +from hacking import core + """ Guidelines for writing new hacking checks @@ -31,6 +33,7 @@ SETUPCLASS_RE = re.compile(r"def setUpClass\(") +@core.flake8ext def assert_no_setupclass(logical_line): """Check for use of setUpClass @@ -38,7 +41,3 @@ def assert_no_setupclass(logical_line): """ if SETUPCLASS_RE.match(logical_line): yield (0, "O300: setUpClass not allowed") - - -def factory(register): - register(assert_no_setupclass) diff --git a/tox.ini b/tox.ini index b55de9b3f..3e6f1bfd0 100644 --- a/tox.ini +++ b/tox.ini @@ -45,9 +45,6 @@ commands = flake8 doc8 doc/source README.rst -[hacking] -local-check-factory = openstack._hacking.factory - [testenv:venv] deps = -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} @@ -119,6 +116,11 @@ ignore = H306,H4,W503 show-source = True exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,openstack/_services_mixin.py +[flake8:local-plugins] +extension = + O300 = _hacking:assert_no_setupclass +paths = ./openstack + [doc8] extensions = .rst, .yaml From 0a6be630a7ccaad7297a217d5b81b841f5fb9a7d Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Tue, 31 Mar 2020 13:43:39 +0200 Subject: [PATCH 2646/3836] Update to hacking 3.0 Hacking 3.0 was just released with minor changes, update to the new version. Change-Id: I9f242b5dace1714ea35f8c3ba39b42520fed3973 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index ad440b8cc..265f5cf57 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,7 @@ # 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>=2.0,<2.1.0 # Apache-2.0 +hacking>=3.0,<3.1.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 ddt>=1.0.1 # MIT From fc3b3d09efe23772520f1280b05e64522940b234 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 2 Apr 2020 17:09:24 +0200 Subject: [PATCH 2647/3836] Volume.backup API attr name fixes Creating backup requires "incremental" attribute, while GETs use "is_incremental". Fix that and add functional test for adding incremental backup. Change-Id: I9a4951132645756e81a618d84482614acf69ec39 --- openstack/block_storage/v2/backup.py | 58 +++++++++++++++++++ openstack/block_storage/v3/backup.py | 58 +++++++++++++++++++ .../block_storage/v3/test_backup.py | 22 ++++++- .../unit/block_storage/v2/test_backup.py | 38 +++++++++++- .../unit/block_storage/v3/test_backup.py | 38 +++++++++++- 5 files changed, 211 insertions(+), 3 deletions(-) diff --git a/openstack/block_storage/v2/backup.py b/openstack/block_storage/v2/backup.py index 61d53c62c..6d87c09aa 100644 --- a/openstack/block_storage/v2/backup.py +++ b/openstack/block_storage/v2/backup.py @@ -76,6 +76,64 @@ class Backup(resource.Resource): #: The UUID of the volume. volume_id = resource.Body("volume_id") + def create(self, session, prepend_key=True, base_path=None, **params): + """Create a remote resource based on this instance. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param prepend_key: A boolean indicating whether the resource_key + should be prepended in a resource creation + request. Default to True. + :param str base_path: Base part of the URI for creating resources, if + different from + :data:`~openstack.resource.Resource.base_path`. + :param dict params: Additional params to pass. + :return: This :class:`Resource` instance. + :raises: :exc:`~openstack.exceptions.MethodNotSupported` if + :data:`Resource.allow_create` is not set to ``True``. + """ + if not self.allow_create: + raise exceptions.MethodNotSupported(self, "create") + + session = self._get_session(session) + microversion = self._get_microversion_for(session, 'create') + requires_id = (self.create_requires_id + if self.create_requires_id is not None + else self.create_method == 'PUT') + + if self.create_exclude_id_from_body: + self._body._dirty.discard("id") + + if self.create_method == 'POST': + request = self._prepare_request(requires_id=requires_id, + prepend_key=prepend_key, + base_path=base_path) + # NOTE(gtema) this is a funny example of when attribute + # is called "incremental" on create, "is_incremental" on get + # and use of "alias" or "aka" is not working for such conflict, + # since our preferred attr name is exactly "is_incremental" + body = request.body + if 'is_incremental' in body['backup']: + body['backup']['incremental'] = \ + body['backup'].pop('is_incremental') + response = session.post(request.url, + json=request.body, headers=request.headers, + microversion=microversion, params=params) + else: + # Just for safety of the implementation (since PUT removed) + raise exceptions.ResourceFailure( + msg="Invalid create method: %s" % self.create_method) + + has_body = (self.has_body if self.create_returns_body is None + else self.create_returns_body) + self.microversion = microversion + self._translate_response(response, has_body=has_body) + # direct comparision to False since we need to rule out None + if self.has_body and self.create_returns_body is False: + # fetch the body if it's required but not returned by create + return self.fetch(session) + return self + def restore(self, session, volume_id=None, name=None): """Restore current backup to volume diff --git a/openstack/block_storage/v3/backup.py b/openstack/block_storage/v3/backup.py index 2212cf629..727ae535c 100644 --- a/openstack/block_storage/v3/backup.py +++ b/openstack/block_storage/v3/backup.py @@ -86,6 +86,64 @@ class Backup(resource.Resource): #: The UUID of the volume. volume_id = resource.Body("volume_id") + def create(self, session, prepend_key=True, base_path=None, **params): + """Create a remote resource based on this instance. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param prepend_key: A boolean indicating whether the resource_key + should be prepended in a resource creation + request. Default to True. + :param str base_path: Base part of the URI for creating resources, if + different from + :data:`~openstack.resource.Resource.base_path`. + :param dict params: Additional params to pass. + :return: This :class:`Resource` instance. + :raises: :exc:`~openstack.exceptions.MethodNotSupported` if + :data:`Resource.allow_create` is not set to ``True``. + """ + if not self.allow_create: + raise exceptions.MethodNotSupported(self, "create") + + session = self._get_session(session) + microversion = self._get_microversion_for(session, 'create') + requires_id = (self.create_requires_id + if self.create_requires_id is not None + else self.create_method == 'PUT') + + if self.create_exclude_id_from_body: + self._body._dirty.discard("id") + + if self.create_method == 'POST': + request = self._prepare_request(requires_id=requires_id, + prepend_key=prepend_key, + base_path=base_path) + # NOTE(gtema) this is a funny example of when attribute + # is called "incremental" on create, "is_incremental" on get + # and use of "alias" or "aka" is not working for such conflict, + # since our preferred attr name is exactly "is_incremental" + body = request.body + if 'is_incremental' in body['backup']: + body['backup']['incremental'] = \ + body['backup'].pop('is_incremental') + response = session.post(request.url, + json=request.body, headers=request.headers, + microversion=microversion, params=params) + else: + # Just for safety of the implementation (since PUT removed) + raise exceptions.ResourceFailure( + msg="Invalid create method: %s" % self.create_method) + + has_body = (self.has_body if self.create_returns_body is None + else self.create_returns_body) + self.microversion = microversion + self._translate_response(response, has_body=has_body) + # direct comparision to False since we need to rule out None + if self.has_body and self.create_returns_body is False: + # fetch the body if it's required but not returned by create + return self.fetch(session) + return self + def restore(self, session, volume_id=None, name=None): """Restore current backup to volume diff --git a/openstack/tests/functional/block_storage/v3/test_backup.py b/openstack/tests/functional/block_storage/v3/test_backup.py index d27fff2f6..444460bee 100644 --- a/openstack/tests/functional/block_storage/v3/test_backup.py +++ b/openstack/tests/functional/block_storage/v3/test_backup.py @@ -42,7 +42,8 @@ def setUp(self): backup = self.user_cloud.block_storage.create_backup( name=self.BACKUP_NAME, - volume_id=volume.id) + volume_id=volume.id, + is_incremental=False) self.user_cloud.block_storage.wait_for_status( backup, status='available', @@ -66,3 +67,22 @@ def tearDown(self): def test_get(self): sot = self.user_cloud.block_storage.get_backup(self.BACKUP_ID) self.assertEqual(self.BACKUP_NAME, sot.name) + self.assertEqual(False, sot.is_incremental) + + def test_create_incremental(self): + incremental_backup = self.user_cloud.block_storage.create_backup( + name=self.getUniqueString(), + volume_id=self.VOLUME_ID, + is_incremental=True) + self.user_cloud.block_storage.wait_for_status( + incremental_backup, + status='available', + failures=['error'], + interval=5, + wait=self._wait_for_timeout) + self.assertEqual(True, incremental_backup.is_incremental) + self.user_cloud.block_storage.delete_backup( + incremental_backup.id, + ignore_missing=False) + self.user_cloud.block_storage.wait_for_delete( + incremental_backup) diff --git a/openstack/tests/unit/block_storage/v2/test_backup.py b/openstack/tests/unit/block_storage/v2/test_backup.py index a25a03753..50605fc1e 100644 --- a/openstack/tests/unit/block_storage/v2/test_backup.py +++ b/openstack/tests/unit/block_storage/v2/test_backup.py @@ -52,7 +52,7 @@ def setUp(self): self.sess = mock.Mock(spec=adapter.Adapter) self.sess.get = mock.Mock() self.sess.post = mock.Mock(return_value=self.resp) - self.sess.default_microversion = mock.Mock(return_value='') + self.sess.default_microversion = None def test_basic(self): sot = backup.Backup(BACKUP) @@ -98,6 +98,42 @@ def test_create(self): self.assertEqual(BACKUP["has_dependent_backups"], sot.has_dependent_backups) + def test_create_incremental(self): + sot = backup.Backup(is_incremental=True) + sot2 = backup.Backup(is_incremental=False) + + create_response = mock.Mock() + create_response.status_code = 200 + create_response.json.return_value = {} + create_response.headers = {} + self.sess.post.return_value = create_response + + sot.create(self.sess) + self.sess.post.assert_called_with( + '/backups', + headers={}, + json={ + 'backup': { + 'incremental': True, + } + }, + microversion=None, + params={} + ) + + sot2.create(self.sess) + self.sess.post.assert_called_with( + '/backups', + headers={}, + json={ + 'backup': { + 'incremental': False, + } + }, + microversion=None, + params={} + ) + def test_restore(self): sot = backup.Backup(**BACKUP) diff --git a/openstack/tests/unit/block_storage/v3/test_backup.py b/openstack/tests/unit/block_storage/v3/test_backup.py index a2b0def81..4522a6464 100644 --- a/openstack/tests/unit/block_storage/v3/test_backup.py +++ b/openstack/tests/unit/block_storage/v3/test_backup.py @@ -55,7 +55,7 @@ def setUp(self): self.sess = mock.Mock(spec=adapter.Adapter) self.sess.get = mock.Mock() self.sess.post = mock.Mock(return_value=self.resp) - self.sess.default_microversion = mock.Mock(return_value='') + self.sess.default_microversion = None def test_basic(self): sot = backup.Backup(BACKUP) @@ -105,6 +105,42 @@ def test_create(self): self.assertEqual(BACKUP['metadata'], sot.metadata) self.assertEqual(BACKUP['user_id'], sot.user_id) + def test_create_incremental(self): + sot = backup.Backup(is_incremental=True) + sot2 = backup.Backup(is_incremental=False) + + create_response = mock.Mock() + create_response.status_code = 200 + create_response.json.return_value = {} + create_response.headers = {} + self.sess.post.return_value = create_response + + sot.create(self.sess) + self.sess.post.assert_called_with( + '/backups', + headers={}, + json={ + 'backup': { + 'incremental': True, + } + }, + microversion=None, + params={} + ) + + sot2.create(self.sess) + self.sess.post.assert_called_with( + '/backups', + headers={}, + json={ + 'backup': { + 'incremental': False, + } + }, + microversion=None, + params={} + ) + def test_restore(self): sot = backup.Backup(**BACKUP) From 767115c5009a421fd59bf7842ce5c011276afa50 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 3 Apr 2020 21:22:13 +0000 Subject: [PATCH 2648/3836] Update master for stable/ussuri Add file to the reno documentation build to show release notes for stable/ussuri. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/ussuri. Change-Id: Ib874927ce670ad387a3f86edfb8bbf278afc57a6 Sem-Ver: feature --- releasenotes/source/index.rst | 1 + releasenotes/source/ussuri.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/ussuri.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 5dac65b62..f169e76b3 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ :maxdepth: 1 unreleased + ussuri train stein rocky diff --git a/releasenotes/source/ussuri.rst b/releasenotes/source/ussuri.rst new file mode 100644 index 000000000..e21e50e0c --- /dev/null +++ b/releasenotes/source/ussuri.rst @@ -0,0 +1,6 @@ +=========================== +Ussuri Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/ussuri From 4f2e76122c86705e42203cb2f0595d79bd78cbfe Mon Sep 17 00:00:00 2001 From: LinPeiWen <591171850@qq.com> Date: Thu, 16 Apr 2020 05:01:28 -0400 Subject: [PATCH 2649/3836] OpenStack port decorator variables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OpenStack port binding_host_id attribute is modifiable, but an error will be reported when calling ‘def create_port’ of the OpenStack SDK, because the binding: host_id variable does not exist in ‘@_utils’ and the binding_host_id attribute cannot be changed. Change-Id: I56edd53f7cb4fa702922e3e34c24fdfed98083e4 --- openstack/cloud/_network.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index 389d3e4c0..7e243826a 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -2364,7 +2364,7 @@ def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, 'allowed_address_pairs', 'extra_dhcp_opts', 'device_owner', 'device_id', 'binding:vnic_type', 'binding:profile', 'port_security_enabled', - 'qos_policy_id') + 'qos_policy_id', 'binding:host_id') def create_port(self, network_id, **kwargs): """Create a port @@ -2434,7 +2434,8 @@ def create_port(self, network_id, **kwargs): 'security_groups', 'allowed_address_pairs', 'extra_dhcp_opts', 'device_owner', 'device_id', 'binding:vnic_type', 'binding:profile', - 'port_security_enabled', 'qos_policy_id') + 'port_security_enabled', 'qos_policy_id', + 'binding:host_id') def update_port(self, name_or_id, **kwargs): """Update a port From c3857c136cb3fcd24c92184ea7018b962b80798c Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Tue, 21 Apr 2020 12:20:09 +0200 Subject: [PATCH 2650/3836] Fix an unstable bare metal unit test Sometimes Node.id ends up being a Mock, breaking logging. Change-Id: I293d0e05151acc4207c5d565bd6a1439b1873f3b --- openstack/tests/unit/baremetal/v1/test_proxy.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openstack/tests/unit/baremetal/v1/test_proxy.py b/openstack/tests/unit/baremetal/v1/test_proxy.py index b18bb921c..9d5dfc99c 100644 --- a/openstack/tests/unit/baremetal/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal/v1/test_proxy.py @@ -279,12 +279,12 @@ def test_timeout_no_fail(self, mock_get): self.assertEqual([], result.failure) def test_timeout_and_failures_not_fail(self, mock_get): - def _fake_get(_self, uuid): + def _fake_get(_self, node): result = mock.Mock() - result.id = uuid - if uuid == '1': + result.id = getattr(node, 'id', node) + if result.id == '1': result._check_state_reached.return_value = True - elif uuid == '2': + elif result.id == '2': result._check_state_reached.side_effect = \ exceptions.ResourceFailure("boom") else: From f132edff2a79191335909d6760d560b0c8310946 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 21 Apr 2020 15:35:27 +0200 Subject: [PATCH 2651/3836] Update docs to work with newer Sphinx Change-Id: Ic15793329aa1f6027465eb362fcd0a0471d7b396 --- doc/source/user/proxies/accelerator.rst | 30 +- doc/source/user/proxies/baremetal.rst | 76 ++-- .../user/proxies/baremetal_introspection.rst | 10 +- doc/source/user/proxies/block_storage.rst | 33 +- doc/source/user/proxies/clustering.rst | 118 ++---- doc/source/user/proxies/compute.rst | 152 +++----- doc/source/user/proxies/database.rst | 33 +- doc/source/user/proxies/dns.rst | 57 +-- doc/source/user/proxies/identity_v2.rst | 33 +- doc/source/user/proxies/identity_v3.rst | 121 ++----- doc/source/user/proxies/image_v1.rst | 10 +- doc/source/user/proxies/image_v2.rst | 52 +-- doc/source/user/proxies/key_manager.rst | 30 +- doc/source/user/proxies/load_balancer_v2.rst | 138 +++---- doc/source/user/proxies/message_v2.rst | 29 +- doc/source/user/proxies/network.rst | 339 ++++++------------ doc/source/user/proxies/object_store.rst | 33 +- doc/source/user/proxies/orchestration.rst | 35 +- doc/source/user/proxies/workflow.rst | 18 +- 19 files changed, 417 insertions(+), 930 deletions(-) diff --git a/doc/source/user/proxies/accelerator.rst b/doc/source/user/proxies/accelerator.rst index b5688a518..38e4da488 100644 --- a/doc/source/user/proxies/accelerator.rst +++ b/doc/source/user/proxies/accelerator.rst @@ -15,37 +15,29 @@ Device Operations ^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.accelerator.v2._proxy.Proxy - - .. automethod:: openstack.accelerator.v2._proxy.Proxy.get_device - .. automethod:: openstack.accelerator.v2._proxy.Proxy.devices + :noindex: + :members: devices, get_device Deployable Operations ^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.accelerator.v2._proxy.Proxy - - .. automethod:: openstack.accelerator.v2._proxy.Proxy.update_deployable - .. automethod:: openstack.accelerator.v2._proxy.Proxy.get_deployable - .. automethod:: openstack.accelerator.v2._proxy.Proxy.deployables + :noindex: + :members: deployables, get_deployable, update_deployable Device Profile Operations ^^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.accelerator.v2._proxy.Proxy - - .. automethod:: openstack.accelerator.v2._proxy.Proxy.create_device_profile - .. automethod:: openstack.accelerator.v2._proxy.Proxy.delete_device_profile - .. automethod:: openstack.accelerator.v2._proxy.Proxy.get_device_profile - .. automethod:: openstack.accelerator.v2._proxy.Proxy.device_profiles + :noindex: + :members: device_profiles, get_device_profile, + create_device_profile, delete_device_profile Accelerator Request Operations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.accelerator.v2._proxy.Proxy - - .. automethod:: openstack.accelerator.v2._proxy.Proxy.create_accelerator_request - .. automethod:: openstack.accelerator.v2._proxy.Proxy.delete_accelerator_request - .. automethod:: openstack.accelerator.v2._proxy.Proxy.get_accelerator_request - .. automethod:: openstack.accelerator.v2._proxy.Proxy.accelerator_requests - .. automethod:: openstack.accelerator.v2._proxy.Proxy.update_accelerator_request - + :noindex: + :members: accelerator_requests, get_accelerator_request, + create_accelerator_request, delete_accelerator_request, + update_accelerator_request diff --git a/doc/source/user/proxies/baremetal.rst b/doc/source/user/proxies/baremetal.rst index 0eb6012b7..c38a72045 100644 --- a/doc/source/user/proxies/baremetal.rst +++ b/doc/source/user/proxies/baremetal.rst @@ -15,84 +15,51 @@ The ``baremetal`` member will only be added if the service is detected. Node Operations ^^^^^^^^^^^^^^^ .. autoclass:: openstack.baremetal.v1._proxy.Proxy - - .. automethod:: openstack.baremetal.v1._proxy.Proxy.create_node - .. automethod:: openstack.baremetal.v1._proxy.Proxy.update_node - .. automethod:: openstack.baremetal.v1._proxy.Proxy.patch_node - .. automethod:: openstack.baremetal.v1._proxy.Proxy.delete_node - .. automethod:: openstack.baremetal.v1._proxy.Proxy.get_node - .. automethod:: openstack.baremetal.v1._proxy.Proxy.find_node - .. automethod:: openstack.baremetal.v1._proxy.Proxy.nodes - .. automethod:: openstack.baremetal.v1._proxy.Proxy.set_node_power_state - .. automethod:: openstack.baremetal.v1._proxy.Proxy.set_node_provision_state - .. automethod:: openstack.baremetal.v1._proxy.Proxy.wait_for_nodes_provision_state - .. automethod:: openstack.baremetal.v1._proxy.Proxy.wait_for_node_reservation - .. automethod:: openstack.baremetal.v1._proxy.Proxy.validate_node - .. automethod:: openstack.baremetal.v1._proxy.Proxy.set_node_maintenance - .. automethod:: openstack.baremetal.v1._proxy.Proxy.unset_node_maintenance + :noindex: + :members: nodes, find_node, get_node, create_node, update_node, patch_node, delete_node, + validate_node, set_node_power_state, set_node_provision_state, + wait_for_nodes_provision_state, wait_for_node_reservation, + set_node_maintenance, unset_node_maintenance Port Operations ^^^^^^^^^^^^^^^ .. autoclass:: openstack.baremetal.v1._proxy.Proxy - - .. automethod:: openstack.baremetal.v1._proxy.Proxy.create_port - .. automethod:: openstack.baremetal.v1._proxy.Proxy.update_port - .. automethod:: openstack.baremetal.v1._proxy.Proxy.patch_port - .. automethod:: openstack.baremetal.v1._proxy.Proxy.delete_port - .. automethod:: openstack.baremetal.v1._proxy.Proxy.get_port - .. automethod:: openstack.baremetal.v1._proxy.Proxy.find_port - .. automethod:: openstack.baremetal.v1._proxy.Proxy.ports + :noindex: + :members: ports, find_port, get_port, create_port, update_port, delete_port, patch_port Port Group Operations ^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.baremetal.v1._proxy.Proxy - - .. automethod:: openstack.baremetal.v1._proxy.Proxy.create_port_group - .. automethod:: openstack.baremetal.v1._proxy.Proxy.update_port_group - .. automethod:: openstack.baremetal.v1._proxy.Proxy.patch_port_group - .. automethod:: openstack.baremetal.v1._proxy.Proxy.delete_port_group - .. automethod:: openstack.baremetal.v1._proxy.Proxy.get_port_group - .. automethod:: openstack.baremetal.v1._proxy.Proxy.find_port_group - .. automethod:: openstack.baremetal.v1._proxy.Proxy.port_groups + :noindex: + :members: port_groups, find_port_group, get_port_group, + create_port_group, update_port_group, delete_port_group, patch_port_group Driver Operations ^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.baremetal.v1._proxy.Proxy - - .. automethod:: openstack.baremetal.v1._proxy.Proxy.drivers - .. automethod:: openstack.baremetal.v1._proxy.Proxy.get_driver + :noindex: + :members: drivers, get_driver Chassis Operations ^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.baremetal.v1._proxy.Proxy - - .. automethod:: openstack.baremetal.v1._proxy.Proxy.create_chassis - .. automethod:: openstack.baremetal.v1._proxy.Proxy.update_chassis - .. automethod:: openstack.baremetal.v1._proxy.Proxy.patch_chassis - .. automethod:: openstack.baremetal.v1._proxy.Proxy.delete_chassis - .. automethod:: openstack.baremetal.v1._proxy.Proxy.get_chassis - .. automethod:: openstack.baremetal.v1._proxy.Proxy.find_chassis - .. automethod:: openstack.baremetal.v1._proxy.Proxy.chassis + :noindex: + :members: chassis, find_chassis, get_chassis, + create_chassis, update_chassis, patch_chassis, delete_chassis VIF Operations ^^^^^^^^^^^^^^ .. autoclass:: openstack.baremetal.v1._proxy.Proxy - - .. automethod:: openstack.baremetal.v1._proxy.Proxy.attach_vif_to_node - .. automethod:: openstack.baremetal.v1._proxy.Proxy.detach_vif_from_node - .. automethod:: openstack.baremetal.v1._proxy.Proxy.list_node_vifs + :noindex: + :members: list_node_vifs, attach_vif_to_node, detach_vif_from_node Allocation Operations ^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.baremetal.v1._proxy.Proxy - - .. automethod:: openstack.baremetal.v1._proxy.Proxy.create_allocation - .. automethod:: openstack.baremetal.v1._proxy.Proxy.update_allocation - .. automethod:: openstack.baremetal.v1._proxy.Proxy.patch_allocation - .. automethod:: openstack.baremetal.v1._proxy.Proxy.delete_allocation - .. automethod:: openstack.baremetal.v1._proxy.Proxy.get_allocation - .. automethod:: openstack.baremetal.v1._proxy.Proxy.allocations - .. automethod:: openstack.baremetal.v1._proxy.Proxy.wait_for_allocation + :noindex: + :members: allocations, get_allocation, create_allocation, + update_allocation, patch_allocation, delete_allocation, + wait_for_allocation Utilities --------- @@ -101,4 +68,5 @@ Building config drives ^^^^^^^^^^^^^^^^^^^^^^ .. automodule:: openstack.baremetal.configdrive + :noindex: :members: diff --git a/doc/source/user/proxies/baremetal_introspection.rst b/doc/source/user/proxies/baremetal_introspection.rst index d6d712b9e..b21aba0c4 100644 --- a/doc/source/user/proxies/baremetal_introspection.rst +++ b/doc/source/user/proxies/baremetal_introspection.rst @@ -16,10 +16,6 @@ Introspection Process Operations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.baremetal_introspection.v1._proxy.Proxy - - .. automethod:: openstack.baremetal_introspection.v1._proxy.Proxy.introspections - .. automethod:: openstack.baremetal_introspection.v1._proxy.Proxy.get_introspection - .. automethod:: openstack.baremetal_introspection.v1._proxy.Proxy.get_introspection_data - .. automethod:: openstack.baremetal_introspection.v1._proxy.Proxy.start_introspection - .. automethod:: openstack.baremetal_introspection.v1._proxy.Proxy.wait_for_introspection - .. automethod:: openstack.baremetal_introspection.v1._proxy.Proxy.abort_introspection + :noindex: + :members: introspections, get_introspection, get_introspection_data, + start_introspection, wait_for_introspection, abort_introspection diff --git a/doc/source/user/proxies/block_storage.rst b/doc/source/user/proxies/block_storage.rst index e8fb8fac4..d169611ca 100644 --- a/doc/source/user/proxies/block_storage.rst +++ b/doc/source/user/proxies/block_storage.rst @@ -16,46 +16,33 @@ Volume Operations ^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.block_storage.v2._proxy.Proxy - - .. automethod:: openstack.block_storage.v2._proxy.Proxy.create_volume - .. automethod:: openstack.block_storage.v2._proxy.Proxy.delete_volume - .. automethod:: openstack.block_storage.v2._proxy.Proxy.get_volume - .. automethod:: openstack.block_storage.v2._proxy.Proxy.volumes + :noindex: + :members: create_volume, delete_volume, get_volume, volumes Backup Operations ^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.block_storage.v2._proxy.Proxy - - .. automethod:: openstack.block_storage.v2._proxy.Proxy.create_backup - .. automethod:: openstack.block_storage.v2._proxy.Proxy.delete_backup - .. automethod:: openstack.block_storage.v2._proxy.Proxy.get_backup - .. automethod:: openstack.block_storage.v2._proxy.Proxy.backups - .. automethod:: openstack.block_storage.v2._proxy.Proxy.restore_backup + :noindex: + :members: create_backup, delete_backup, get_backup, backups, restore_backup Type Operations ^^^^^^^^^^^^^^^ .. autoclass:: openstack.block_storage.v2._proxy.Proxy - - .. automethod:: openstack.block_storage.v2._proxy.Proxy.create_type - .. automethod:: openstack.block_storage.v2._proxy.Proxy.delete_type - .. automethod:: openstack.block_storage.v2._proxy.Proxy.get_type - .. automethod:: openstack.block_storage.v2._proxy.Proxy.types + :noindex: + :members: create_type, delete_type, get_type, types Snapshot Operations ^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.block_storage.v2._proxy.Proxy - - .. automethod:: openstack.block_storage.v2._proxy.Proxy.create_snapshot - .. automethod:: openstack.block_storage.v2._proxy.Proxy.delete_snapshot - .. automethod:: openstack.block_storage.v2._proxy.Proxy.get_snapshot - .. automethod:: openstack.block_storage.v2._proxy.Proxy.snapshots + :noindex: + :members: create_snapshot, delete_snapshot, get_snapshot, snapshots Stats Operations ^^^^^^^^^^^^^^^^ .. autoclass:: openstack.block_storage.v2._proxy.Proxy - - .. automethod:: openstack.block_storage.v2._proxy.Proxy.backend_pools + :noindex: + :members: backend_pools diff --git a/doc/source/user/proxies/clustering.rst b/doc/source/user/proxies/clustering.rst index 26c409f56..46bf49753 100644 --- a/doc/source/user/proxies/clustering.rst +++ b/doc/source/user/proxies/clustering.rst @@ -15,151 +15,105 @@ Build Info Operations ^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.clustering.v1._proxy.Proxy - - .. automethod:: openstack.clustering.v1._proxy.Proxy.get_build_info + :noindex: + :members: get_build_info Profile Type Operations ^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.clustering.v1._proxy.Proxy - - .. automethod:: openstack.clustering.v1._proxy.Proxy.profile_types - .. automethod:: openstack.clustering.v1._proxy.Proxy.get_profile_type + :noindex: + :members: profile_types, get_profile_type Profile Operations ^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.clustering.v1._proxy.Proxy - - .. automethod:: openstack.clustering.v1._proxy.Proxy.create_profile - .. automethod:: openstack.clustering.v1._proxy.Proxy.update_profile - .. automethod:: openstack.clustering.v1._proxy.Proxy.delete_profile - .. automethod:: openstack.clustering.v1._proxy.Proxy.get_profile - .. automethod:: openstack.clustering.v1._proxy.Proxy.find_profile - .. automethod:: openstack.clustering.v1._proxy.Proxy.profiles - - .. automethod:: openstack.clustering.v1._proxy.Proxy.validate_profile + :noindex: + :members: create_profile, update_profile, delete_profile, get_profile, + find_profile, profiles, validate_profile Policy Type Operations ^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.clustering.v1._proxy.Proxy - - .. automethod:: openstack.clustering.v1._proxy.Proxy.policy_types - .. automethod:: openstack.clustering.v1._proxy.Proxy.get_policy_type + :noindex: + :members: policy_types, get_policy_type Policy Operations ^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.clustering.v1._proxy.Proxy + :noindex: + :members: create_policy, update_policy, delete_policy, get_policy, + find_policy, policies - .. automethod:: openstack.clustering.v1._proxy.Proxy.create_policy - .. automethod:: openstack.clustering.v1._proxy.Proxy.update_policy - .. automethod:: openstack.clustering.v1._proxy.Proxy.delete_policy - .. automethod:: openstack.clustering.v1._proxy.Proxy.get_policy - .. automethod:: openstack.clustering.v1._proxy.Proxy.find_policy - .. automethod:: openstack.clustering.v1._proxy.Proxy.policies - - .. automethod:: openstack.clustering.v1._proxy.Proxy.validate_policy +validate_policy Cluster Operations ^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.clustering.v1._proxy.Proxy - - .. automethod:: openstack.clustering.v1._proxy.Proxy.create_cluster - .. automethod:: openstack.clustering.v1._proxy.Proxy.update_cluster - .. automethod:: openstack.clustering.v1._proxy.Proxy.delete_cluster - .. automethod:: openstack.clustering.v1._proxy.Proxy.get_cluster - .. automethod:: openstack.clustering.v1._proxy.Proxy.find_cluster - .. automethod:: openstack.clustering.v1._proxy.Proxy.clusters - - .. automethod:: openstack.clustering.v1._proxy.Proxy.check_cluster - .. automethod:: openstack.clustering.v1._proxy.Proxy.recover_cluster - .. automethod:: openstack.clustering.v1._proxy.Proxy.resize_cluster - .. automethod:: openstack.clustering.v1._proxy.Proxy.scale_in_cluster - .. automethod:: openstack.clustering.v1._proxy.Proxy.scale_out_cluster - .. automethod:: openstack.clustering.v1._proxy.Proxy.collect_cluster_attrs - .. automethod:: openstack.clustering.v1._proxy.Proxy.perform_operation_on_cluster - - .. automethod:: openstack.clustering.v1._proxy.Proxy.add_nodes_to_cluster - .. automethod:: openstack.clustering.v1._proxy.Proxy.remove_nodes_from_cluster - .. automethod:: openstack.clustering.v1._proxy.Proxy.replace_nodes_in_cluster - - .. automethod:: openstack.clustering.v1._proxy.Proxy.attach_policy_to_cluster - .. automethod:: openstack.clustering.v1._proxy.Proxy.update_cluster_policy - .. automethod:: openstack.clustering.v1._proxy.Proxy.detach_policy_from_cluster - .. automethod:: openstack.clustering.v1._proxy.Proxy.get_cluster_policy - .. automethod:: openstack.clustering.v1._proxy.Proxy.cluster_policies - + :noindex: + :members: create_cluster, update_cluster, delete_cluster, get_cluster, + find_cluster, clusters, check_cluster, recover_cluster, + resize_cluster, scale_in_cluster, scale_out_cluster, + collect_cluster_attrs, perform_operation_on_cluster, + add_nodes_to_cluster, remove_nodes_from_cluster, + replace_nodes_in_cluster, attach_policy_to_cluster, + update_cluster_policy, detach_policy_from_cluster, + get_cluster_policy, cluster_policies Node Operations ^^^^^^^^^^^^^^^ .. autoclass:: openstack.clustering.v1._proxy.Proxy - - .. automethod:: openstack.clustering.v1._proxy.Proxy.create_node - .. automethod:: openstack.clustering.v1._proxy.Proxy.update_node - .. automethod:: openstack.clustering.v1._proxy.Proxy.delete_node - .. automethod:: openstack.clustering.v1._proxy.Proxy.get_node - .. automethod:: openstack.clustering.v1._proxy.Proxy.find_node - .. automethod:: openstack.clustering.v1._proxy.Proxy.nodes - - .. automethod:: openstack.clustering.v1._proxy.Proxy.check_node - .. automethod:: openstack.clustering.v1._proxy.Proxy.recover_node - .. automethod:: openstack.clustering.v1._proxy.Proxy.perform_operation_on_node - - .. automethod:: openstack.clustering.v1._proxy.Proxy.adopt_node + :noindex: + :members: create_node, update_node, delete_node, get_node, find_node, nodes, + check_node, recover_node, perform_operation_on_node, adopt_node Receiver Operations ^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.clustering.v1._proxy.Proxy - - .. automethod:: openstack.clustering.v1._proxy.Proxy.create_receiver - .. automethod:: openstack.clustering.v1._proxy.Proxy.update_receiver - .. automethod:: openstack.clustering.v1._proxy.Proxy.delete_receiver - .. automethod:: openstack.clustering.v1._proxy.Proxy.get_receiver - .. automethod:: openstack.clustering.v1._proxy.Proxy.find_receiver - .. automethod:: openstack.clustering.v1._proxy.Proxy.receivers + :noindex: + :members: create_receiver, update_receiver, delete_receiver, + get_receiver, find_receiver, receivers Action Operations ^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.clustering.v1._proxy.Proxy - - .. automethod:: openstack.clustering.v1._proxy.Proxy.get_action - .. automethod:: openstack.clustering.v1._proxy.Proxy.actions + :noindex: + :members: get_action, actions Event Operations ^^^^^^^^^^^^^^^^ .. autoclass:: openstack.clustering.v1._proxy.Proxy - - .. automethod:: openstack.clustering.v1._proxy.Proxy.get_event - .. automethod:: openstack.clustering.v1._proxy.Proxy.events + :noindex: + :members: get_event, events Helper Operations ^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.clustering.v1._proxy.Proxy - - .. automethod:: openstack.clustering.v1._proxy.Proxy.wait_for_delete - .. automethod:: openstack.clustering.v1._proxy.Proxy.wait_for_status + :noindex: + :members: wait_for_delete, wait_for_status Service Operations ^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.clustering.v1._proxy.Proxy - - .. automethod:: openstack.clustering.v1._proxy.Proxy.services + :noindex: + :members: services diff --git a/doc/source/user/proxies/compute.rst b/doc/source/user/proxies/compute.rst index 4ec84d13f..cc59d322c 100644 --- a/doc/source/user/proxies/compute.rst +++ b/doc/source/user/proxies/compute.rst @@ -17,180 +17,128 @@ Server Operations ^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.compute.v2._proxy.Proxy - - .. automethod:: openstack.compute.v2._proxy.Proxy.create_server - .. automethod:: openstack.compute.v2._proxy.Proxy.update_server - .. automethod:: openstack.compute.v2._proxy.Proxy.delete_server - .. automethod:: openstack.compute.v2._proxy.Proxy.get_server - .. automethod:: openstack.compute.v2._proxy.Proxy.find_server - .. automethod:: openstack.compute.v2._proxy.Proxy.servers - .. automethod:: openstack.compute.v2._proxy.Proxy.get_server_metadata - .. automethod:: openstack.compute.v2._proxy.Proxy.set_server_metadata - .. automethod:: openstack.compute.v2._proxy.Proxy.delete_server_metadata - .. automethod:: openstack.compute.v2._proxy.Proxy.wait_for_server - .. automethod:: openstack.compute.v2._proxy.Proxy.create_server_image - .. automethod:: openstack.compute.v2._proxy.Proxy.backup_server + :noindex: + :members: create_server, update_server, delete_server, get_server, + find_server, servers, get_server_metadata, set_server_metadata, + delete_server_metadata, wait_for_server, create_server_image, + backup_server Network Actions *************** .. autoclass:: openstack.compute.v2._proxy.Proxy - - .. automethod:: openstack.compute.v2._proxy.Proxy.add_fixed_ip_to_server - .. automethod:: openstack.compute.v2._proxy.Proxy.remove_fixed_ip_from_server - .. automethod:: openstack.compute.v2._proxy.Proxy.add_floating_ip_to_server - .. automethod:: openstack.compute.v2._proxy.Proxy.remove_floating_ip_from_server - .. automethod:: openstack.compute.v2._proxy.Proxy.fetch_server_security_groups - .. automethod:: openstack.compute.v2._proxy.Proxy.add_security_group_to_server - .. automethod:: openstack.compute.v2._proxy.Proxy.remove_security_group_from_server + :noindex: + :members: add_fixed_ip_to_server, remove_fixed_ip_from_server, + add_floating_ip_to_server, remove_floating_ip_from_server, + fetch_server_security_groups, add_security_group_to_server, + remove_security_group_from_server Starting, Stopping, etc. ************************ .. autoclass:: openstack.compute.v2._proxy.Proxy - - .. automethod:: openstack.compute.v2._proxy.Proxy.start_server - .. automethod:: openstack.compute.v2._proxy.Proxy.stop_server - .. automethod:: openstack.compute.v2._proxy.Proxy.suspend_server - .. automethod:: openstack.compute.v2._proxy.Proxy.resume_server - .. automethod:: openstack.compute.v2._proxy.Proxy.reboot_server - .. automethod:: openstack.compute.v2._proxy.Proxy.shelve_server - .. automethod:: openstack.compute.v2._proxy.Proxy.unshelve_server - .. automethod:: openstack.compute.v2._proxy.Proxy.lock_server - .. automethod:: openstack.compute.v2._proxy.Proxy.unlock_server - .. automethod:: openstack.compute.v2._proxy.Proxy.pause_server - .. automethod:: openstack.compute.v2._proxy.Proxy.unpause_server - .. automethod:: openstack.compute.v2._proxy.Proxy.rescue_server - .. automethod:: openstack.compute.v2._proxy.Proxy.unrescue_server - .. automethod:: openstack.compute.v2._proxy.Proxy.evacuate_server - .. automethod:: openstack.compute.v2._proxy.Proxy.migrate_server - .. automethod:: openstack.compute.v2._proxy.Proxy.get_server_console_output - .. automethod:: openstack.compute.v2._proxy.Proxy.live_migrate_server + :noindex: + :members: start_server, stop_server, suspend_server, resume_server, + reboot_server, shelve_server, unshelve_server, lock_server, + unlock_server, pause_server, unpause_server, rescue_server, + unrescue_server, evacuate_server, migrate_server, + get_server_console_output, live_migrate_server Modifying a Server ****************** .. autoclass:: openstack.compute.v2._proxy.Proxy - - .. automethod:: openstack.compute.v2._proxy.Proxy.resize_server - .. automethod:: openstack.compute.v2._proxy.Proxy.confirm_server_resize - .. automethod:: openstack.compute.v2._proxy.Proxy.revert_server_resize - .. automethod:: openstack.compute.v2._proxy.Proxy.rebuild_server - .. automethod:: openstack.compute.v2._proxy.Proxy.reset_server_state - .. automethod:: openstack.compute.v2._proxy.Proxy.change_server_password - .. automethod:: openstack.compute.v2._proxy.Proxy.get_server_password + :noindex: + :members: resize_server, confirm_server_resize, revert_server_resize, + rebuild_server, reset_server_state, change_server_password, + get_server_password Image Operations ^^^^^^^^^^^^^^^^ .. autoclass:: openstack.compute.v2._proxy.Proxy - - .. automethod:: openstack.compute.v2._proxy.Proxy.images - .. automethod:: openstack.compute.v2._proxy.Proxy.get_image - .. automethod:: openstack.compute.v2._proxy.Proxy.find_image - .. automethod:: openstack.compute.v2._proxy.Proxy.delete_image - .. automethod:: openstack.compute.v2._proxy.Proxy.get_image_metadata - .. automethod:: openstack.compute.v2._proxy.Proxy.set_image_metadata - .. automethod:: openstack.compute.v2._proxy.Proxy.delete_image_metadata + :noindex: + :members: images, get_image, find_image, delete_image, get_image_metadata, + set_image_metadata, delete_image_metadata Flavor Operations ^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.compute.v2._proxy.Proxy - - .. automethod:: openstack.compute.v2._proxy.Proxy.create_flavor - .. automethod:: openstack.compute.v2._proxy.Proxy.delete_flavor - .. automethod:: openstack.compute.v2._proxy.Proxy.get_flavor - .. automethod:: openstack.compute.v2._proxy.Proxy.find_flavor - .. automethod:: openstack.compute.v2._proxy.Proxy.flavors + :noindex: + :members: create_flavor, delete_flavor, get_flavor, find_flavor, flavors Service Operations ^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.compute.v2._proxy.Proxy - - .. automethod:: openstack.compute.v2._proxy.Proxy.services - .. automethod:: openstack.compute.v2._proxy.Proxy.enable_service - .. automethod:: openstack.compute.v2._proxy.Proxy.disable_service - .. automethod:: openstack.compute.v2._proxy.Proxy.force_service_down + :noindex: + :members: services, enable_service, disable_service, force_service_down Volume Attachment Operations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.compute.v2._proxy.Proxy - - .. automethod:: openstack.compute.v2._proxy.Proxy.create_volume_attachment - .. automethod:: openstack.compute.v2._proxy.Proxy.update_volume_attachment - .. automethod:: openstack.compute.v2._proxy.Proxy.delete_volume_attachment - .. automethod:: openstack.compute.v2._proxy.Proxy.get_volume_attachment - .. automethod:: openstack.compute.v2._proxy.Proxy.volume_attachments + :noindex: + :members: create_volume_attachment, update_volume_attachment, + delete_volume_attachment, get_volume_attachment, + volume_attachments Keypair Operations ^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.compute.v2._proxy.Proxy - - .. automethod:: openstack.compute.v2._proxy.Proxy.create_keypair - .. automethod:: openstack.compute.v2._proxy.Proxy.delete_keypair - .. automethod:: openstack.compute.v2._proxy.Proxy.get_keypair - .. automethod:: openstack.compute.v2._proxy.Proxy.find_keypair - .. automethod:: openstack.compute.v2._proxy.Proxy.keypairs + :noindex: + :members: create_keypair, delete_keypair, get_keypair, find_keypair, + keypairs Server IPs ^^^^^^^^^^ .. autoclass:: openstack.compute.v2._proxy.Proxy - - .. automethod:: openstack.compute.v2._proxy.Proxy.server_ips + :noindex: + :members: server_ips Server Group Operations ^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.compute.v2._proxy.Proxy - - .. automethod:: openstack.compute.v2._proxy.Proxy.create_server_group - .. automethod:: openstack.compute.v2._proxy.Proxy.delete_server_group - .. automethod:: openstack.compute.v2._proxy.Proxy.get_server_group - .. automethod:: openstack.compute.v2._proxy.Proxy.find_server_group - .. automethod:: openstack.compute.v2._proxy.Proxy.server_groups + :noindex: + :members: create_server_group, delete_server_group, get_server_group, + find_server_group, server_groups Server Interface Operations ^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.compute.v2._proxy.Proxy - - .. automethod:: openstack.compute.v2._proxy.Proxy.create_server_interface - .. automethod:: openstack.compute.v2._proxy.Proxy.delete_server_interface - .. automethod:: openstack.compute.v2._proxy.Proxy.get_server_interface - .. automethod:: openstack.compute.v2._proxy.Proxy.server_interfaces + :noindex: + :members: create_server_interface, delete_server_interface, + get_server_interface, server_interfaces, Availability Zone Operations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.compute.v2._proxy.Proxy - - .. automethod:: openstack.compute.v2._proxy.Proxy.availability_zones + :noindex: + :members: availability_zones Limits Operations ^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.compute.v2._proxy.Proxy - - .. automethod:: openstack.compute.v2._proxy.Proxy.get_limits + :noindex: + :members: get_limits Hypervisor Operations ^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.compute.v2._proxy.Proxy - - .. automethod:: openstack.compute.v2._proxy.Proxy.get_hypervisor - .. automethod:: openstack.compute.v2._proxy.Proxy.find_hypervisor - .. automethod:: openstack.compute.v2._proxy.Proxy.hypervisors + :noindex: + :members: get_hypervisor, find_hypervisor, hypervisors Extension Operations ^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.compute.v2._proxy.Proxy - - .. automethod:: openstack.compute.v2._proxy.Proxy.find_extension - .. automethod:: openstack.compute.v2._proxy.Proxy.extensions + :noindex: + :members: find_extension, extensions diff --git a/doc/source/user/proxies/database.rst b/doc/source/user/proxies/database.rst index 6a3cdb372..3752b88fb 100644 --- a/doc/source/user/proxies/database.rst +++ b/doc/source/user/proxies/database.rst @@ -16,41 +16,28 @@ Database Operations ^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.database.v1._proxy.Proxy - - .. automethod:: openstack.database.v1._proxy.Proxy.create_database - .. automethod:: openstack.database.v1._proxy.Proxy.delete_database - .. automethod:: openstack.database.v1._proxy.Proxy.get_database - .. automethod:: openstack.database.v1._proxy.Proxy.find_database - .. automethod:: openstack.database.v1._proxy.Proxy.databases + :noindex: + :members: create_database, delete_database, get_database, find_database, + databases Flavor Operations ^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.database.v1._proxy.Proxy - - .. automethod:: openstack.database.v1._proxy.Proxy.get_flavor - .. automethod:: openstack.database.v1._proxy.Proxy.find_flavor - .. automethod:: openstack.database.v1._proxy.Proxy.flavors + :noindex: + :members: get_flavor, find_flavor, flavors Instance Operations ^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.database.v1._proxy.Proxy - - .. automethod:: openstack.database.v1._proxy.Proxy.create_instance - .. automethod:: openstack.database.v1._proxy.Proxy.update_instance - .. automethod:: openstack.database.v1._proxy.Proxy.delete_instance - .. automethod:: openstack.database.v1._proxy.Proxy.get_instance - .. automethod:: openstack.database.v1._proxy.Proxy.find_instance - .. automethod:: openstack.database.v1._proxy.Proxy.instances + :noindex: + :members: create_instance, update_instance, delete_instance, get_instance, + find_instance, instances User Operations ^^^^^^^^^^^^^^^ .. autoclass:: openstack.database.v1._proxy.Proxy - - .. automethod:: openstack.database.v1._proxy.Proxy.create_user - .. automethod:: openstack.database.v1._proxy.Proxy.delete_user - .. automethod:: openstack.database.v1._proxy.Proxy.get_user - .. automethod:: openstack.database.v1._proxy.Proxy.find_user - .. automethod:: openstack.database.v1._proxy.Proxy.users + :noindex: + :members: create_user, delete_user, get_user, find_user, users diff --git a/doc/source/user/proxies/dns.rst b/doc/source/user/proxies/dns.rst index 91a63de2b..e85db5dbd 100644 --- a/doc/source/user/proxies/dns.rst +++ b/doc/source/user/proxies/dns.rst @@ -16,66 +16,47 @@ DNS Zone Operations ^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.dns.v2._proxy.Proxy - - .. automethod:: openstack.dns.v2._proxy.Proxy.create_zone - .. automethod:: openstack.dns.v2._proxy.Proxy.delete_zone - .. automethod:: openstack.dns.v2._proxy.Proxy.get_zone - .. automethod:: openstack.dns.v2._proxy.Proxy.find_zone - .. automethod:: openstack.dns.v2._proxy.Proxy.zones - .. automethod:: openstack.dns.v2._proxy.Proxy.abandon_zone - .. automethod:: openstack.dns.v2._proxy.Proxy.xfr_zone + :noindex: + :members: create_zone, delete_zone, get_zone, find_zone, zones, + abandon_zone, xfr_zone Recordset Operations ^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.dns.v2._proxy.Proxy - - .. automethod:: openstack.dns.v2._proxy.Proxy.create_recordset - .. automethod:: openstack.dns.v2._proxy.Proxy.update_recordset - .. automethod:: openstack.dns.v2._proxy.Proxy.get_recordset - .. automethod:: openstack.dns.v2._proxy.Proxy.delete_recordset - .. automethod:: openstack.dns.v2._proxy.Proxy.recordsets + :noindex: + :members: create_recordset, update_recordset, get_recordset, + delete_recordset, recordsets Zone Import Operations ^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.dns.v2._proxy.Proxy - - .. automethod:: openstack.dns.v2._proxy.Proxy.zone_imports - .. automethod:: openstack.dns.v2._proxy.Proxy.create_zone_import - .. automethod:: openstack.dns.v2._proxy.Proxy.get_zone_import - .. automethod:: openstack.dns.v2._proxy.Proxy.delete_zone_import + :noindex: + :members: zone_imports, create_zone_import, get_zone_import, + delete_zone_import Zone Export Operations ^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.dns.v2._proxy.Proxy - - .. automethod:: openstack.dns.v2._proxy.Proxy.zone_exports - .. automethod:: openstack.dns.v2._proxy.Proxy.create_zone_export - .. automethod:: openstack.dns.v2._proxy.Proxy.get_zone_export - .. automethod:: openstack.dns.v2._proxy.Proxy.get_zone_export_text - .. automethod:: openstack.dns.v2._proxy.Proxy.delete_zone_export + :noindex: + :members: zone_exports, create_zone_export, get_zone_export, + get_zone_export_text, delete_zone_export FloatingIP Operations ^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.dns.v2._proxy.Proxy - - .. automethod:: openstack.dns.v2._proxy.Proxy.floating_ips - .. automethod:: openstack.dns.v2._proxy.Proxy.get_floating_ip - .. automethod:: openstack.dns.v2._proxy.Proxy.update_floating_ip + :noindex: + :members: floating_ips, get_floating_ip, update_floating_ip Zone Transfer Operations ^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.dns.v2._proxy.Proxy - - .. automethod:: openstack.dns.v2._proxy.Proxy.zone_transfer_requests - .. automethod:: openstack.dns.v2._proxy.Proxy.get_zone_transfer_request - .. automethod:: openstack.dns.v2._proxy.Proxy.create_zone_transfer_request - .. automethod:: openstack.dns.v2._proxy.Proxy.update_zone_transfer_request - .. automethod:: openstack.dns.v2._proxy.Proxy.delete_zone_transfer_request - .. automethod:: openstack.dns.v2._proxy.Proxy.zone_transfer_accepts - .. automethod:: openstack.dns.v2._proxy.Proxy.get_zone_transfer_accept - .. automethod:: openstack.dns.v2._proxy.Proxy.create_zone_transfer_accept + :noindex: + :members: zone_transfer_requests, get_zone_transfer_request, + create_zone_transfer_request, update_zone_transfer_request, + delete_zone_transfer_request, zone_transfer_accepts, + get_zone_transfer_accept, create_zone_transfer_accept diff --git a/doc/source/user/proxies/identity_v2.rst b/doc/source/user/proxies/identity_v2.rst index 2bb5500a2..fac47fa2a 100644 --- a/doc/source/user/proxies/identity_v2.rst +++ b/doc/source/user/proxies/identity_v2.rst @@ -16,42 +16,27 @@ Extension Operations ^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.identity.v2._proxy.Proxy - - .. automethod:: openstack.identity.v2._proxy.Proxy.get_extension - .. automethod:: openstack.identity.v2._proxy.Proxy.extensions + :noindex: + :members: get_extension, extensions User Operations ^^^^^^^^^^^^^^^ .. autoclass:: openstack.identity.v2._proxy.Proxy - - .. automethod:: openstack.identity.v2._proxy.Proxy.create_user - .. automethod:: openstack.identity.v2._proxy.Proxy.update_user - .. automethod:: openstack.identity.v2._proxy.Proxy.delete_user - .. automethod:: openstack.identity.v2._proxy.Proxy.get_user - .. automethod:: openstack.identity.v2._proxy.Proxy.find_user - .. automethod:: openstack.identity.v2._proxy.Proxy.users + :noindex: + :members: create_user, update_user, delete_user, get_user, find_user, users Role Operations ^^^^^^^^^^^^^^^ .. autoclass:: openstack.identity.v2._proxy.Proxy - - .. automethod:: openstack.identity.v2._proxy.Proxy.create_role - .. automethod:: openstack.identity.v2._proxy.Proxy.update_role - .. automethod:: openstack.identity.v2._proxy.Proxy.delete_role - .. automethod:: openstack.identity.v2._proxy.Proxy.get_role - .. automethod:: openstack.identity.v2._proxy.Proxy.find_role - .. automethod:: openstack.identity.v2._proxy.Proxy.roles + :noindex: + :members: create_role, update_role, delete_role, get_role, find_role, roles Tenant Operations ^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.identity.v2._proxy.Proxy - - .. automethod:: openstack.identity.v2._proxy.Proxy.create_tenant - .. automethod:: openstack.identity.v2._proxy.Proxy.update_tenant - .. automethod:: openstack.identity.v2._proxy.Proxy.delete_tenant - .. automethod:: openstack.identity.v2._proxy.Proxy.get_tenant - .. automethod:: openstack.identity.v2._proxy.Proxy.find_tenant - .. automethod:: openstack.identity.v2._proxy.Proxy.tenants + :noindex: + :members: create_tenant, update_tenant, delete_tenant, get_tenant, + find_tenant, tenants diff --git a/doc/source/user/proxies/identity_v3.rst b/doc/source/user/proxies/identity_v3.rst index 7a81270fa..8153f6c09 100644 --- a/doc/source/user/proxies/identity_v3.rst +++ b/doc/source/user/proxies/identity_v3.rst @@ -16,144 +16,93 @@ Credential Operations ^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.identity.v3._proxy.Proxy - - .. automethod:: openstack.identity.v3._proxy.Proxy.create_credential - .. automethod:: openstack.identity.v3._proxy.Proxy.update_credential - .. automethod:: openstack.identity.v3._proxy.Proxy.delete_credential - .. automethod:: openstack.identity.v3._proxy.Proxy.get_credential - .. automethod:: openstack.identity.v3._proxy.Proxy.find_credential - .. automethod:: openstack.identity.v3._proxy.Proxy.credentials + :noindex: + :members: create_credential, update_credential, delete_credential, + get_credential, find_credential, credentials Domain Operations ^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.identity.v3._proxy.Proxy - - .. automethod:: openstack.identity.v3._proxy.Proxy.create_domain - .. automethod:: openstack.identity.v3._proxy.Proxy.update_domain - .. automethod:: openstack.identity.v3._proxy.Proxy.delete_domain - .. automethod:: openstack.identity.v3._proxy.Proxy.get_domain - .. automethod:: openstack.identity.v3._proxy.Proxy.find_domain - .. automethod:: openstack.identity.v3._proxy.Proxy.domains + :noindex: + :members: create_domain, update_domain, delete_domain, get_domain, + find_domain, domains Endpoint Operations ^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.identity.v3._proxy.Proxy - - .. automethod:: openstack.identity.v3._proxy.Proxy.create_endpoint - .. automethod:: openstack.identity.v3._proxy.Proxy.update_endpoint - .. automethod:: openstack.identity.v3._proxy.Proxy.delete_endpoint - .. automethod:: openstack.identity.v3._proxy.Proxy.get_endpoint - .. automethod:: openstack.identity.v3._proxy.Proxy.find_endpoint - .. automethod:: openstack.identity.v3._proxy.Proxy.endpoints + :noindex: + :members: create_endpoint, update_endpoint, delete_endpoint, get_endpoint, + find_endpoint, endpoints Group Operations ^^^^^^^^^^^^^^^^ .. autoclass:: openstack.identity.v3._proxy.Proxy - - .. automethod:: openstack.identity.v3._proxy.Proxy.create_group - .. automethod:: openstack.identity.v3._proxy.Proxy.update_group - .. automethod:: openstack.identity.v3._proxy.Proxy.delete_group - .. automethod:: openstack.identity.v3._proxy.Proxy.get_group - .. automethod:: openstack.identity.v3._proxy.Proxy.find_group - .. automethod:: openstack.identity.v3._proxy.Proxy.groups + :noindex: + :members: create_group, update_group, delete_group, get_group, find_group, + groups Policy Operations ^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.identity.v3._proxy.Proxy - - .. automethod:: openstack.identity.v3._proxy.Proxy.create_policy - .. automethod:: openstack.identity.v3._proxy.Proxy.update_policy - .. automethod:: openstack.identity.v3._proxy.Proxy.delete_policy - .. automethod:: openstack.identity.v3._proxy.Proxy.get_policy - .. automethod:: openstack.identity.v3._proxy.Proxy.find_policy - .. automethod:: openstack.identity.v3._proxy.Proxy.policies + :noindex: + :members: create_policy, update_policy, delete_policy, get_policy, + find_policy, policies Project Operations ^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.identity.v3._proxy.Proxy - - .. automethod:: openstack.identity.v3._proxy.Proxy.create_project - .. automethod:: openstack.identity.v3._proxy.Proxy.update_project - .. automethod:: openstack.identity.v3._proxy.Proxy.delete_project - .. automethod:: openstack.identity.v3._proxy.Proxy.get_project - .. automethod:: openstack.identity.v3._proxy.Proxy.find_project - .. automethod:: openstack.identity.v3._proxy.Proxy.projects + :noindex: + :members: create_project, update_project, delete_project, get_project, + find_project, projects Region Operations ^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.identity.v3._proxy.Proxy - - .. automethod:: openstack.identity.v3._proxy.Proxy.create_region - .. automethod:: openstack.identity.v3._proxy.Proxy.update_region - .. automethod:: openstack.identity.v3._proxy.Proxy.delete_region - .. automethod:: openstack.identity.v3._proxy.Proxy.get_region - .. automethod:: openstack.identity.v3._proxy.Proxy.find_region - .. automethod:: openstack.identity.v3._proxy.Proxy.regions + :noindex: + :members: create_region, update_region, delete_region, get_region, + find_region, regions Role Operations ^^^^^^^^^^^^^^^ .. autoclass:: openstack.identity.v3._proxy.Proxy - - .. automethod:: openstack.identity.v3._proxy.Proxy.create_role - .. automethod:: openstack.identity.v3._proxy.Proxy.update_role - .. automethod:: openstack.identity.v3._proxy.Proxy.delete_role - .. automethod:: openstack.identity.v3._proxy.Proxy.get_role - .. automethod:: openstack.identity.v3._proxy.Proxy.find_role - .. automethod:: openstack.identity.v3._proxy.Proxy.roles + :noindex: + :members: create_role, update_role, delete_role, get_role, find_role, roles Role Assignment Operations ^^^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.identity.v3._proxy.Proxy - - .. automethod:: openstack.identity.v3._proxy.Proxy.role_assignments - .. automethod:: openstack.identity.v3._proxy.Proxy.role_assignments_filter - .. automethod:: openstack.identity.v3._proxy.Proxy.assign_project_role_to_user - .. automethod:: openstack.identity.v3._proxy.Proxy.unassign_project_role_from_user - .. automethod:: openstack.identity.v3._proxy.Proxy.validate_user_has_role - .. automethod:: openstack.identity.v3._proxy.Proxy.assign_project_role_to_group - .. automethod:: openstack.identity.v3._proxy.Proxy.unassign_project_role_from_group - .. automethod:: openstack.identity.v3._proxy.Proxy.validate_group_has_role - + :noindex: + :members: role_assignments, role_assignments_filter, + assign_project_role_to_user, unassign_project_role_from_user, + validate_user_has_role, assign_project_role_to_group, + unassign_project_role_from_group, validate_group_has_role Service Operations ^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.identity.v3._proxy.Proxy - - .. automethod:: openstack.identity.v3._proxy.Proxy.create_service - .. automethod:: openstack.identity.v3._proxy.Proxy.update_service - .. automethod:: openstack.identity.v3._proxy.Proxy.delete_service - .. automethod:: openstack.identity.v3._proxy.Proxy.get_service - .. automethod:: openstack.identity.v3._proxy.Proxy.find_service - .. automethod:: openstack.identity.v3._proxy.Proxy.services + :noindex: + :members: create_service, update_service, delete_service, get_service, + find_service, services Trust Operations ^^^^^^^^^^^^^^^^ .. autoclass:: openstack.identity.v3._proxy.Proxy - - .. automethod:: openstack.identity.v3._proxy.Proxy.create_trust - .. automethod:: openstack.identity.v3._proxy.Proxy.delete_trust - .. automethod:: openstack.identity.v3._proxy.Proxy.get_trust - .. automethod:: openstack.identity.v3._proxy.Proxy.find_trust - .. automethod:: openstack.identity.v3._proxy.Proxy.trusts + :noindex: + :members: create_trust, delete_trust, get_trust, find_trust, trusts User Operations ^^^^^^^^^^^^^^^ .. autoclass:: openstack.identity.v3._proxy.Proxy - - .. automethod:: openstack.identity.v3._proxy.Proxy.create_user - .. automethod:: openstack.identity.v3._proxy.Proxy.update_user - .. automethod:: openstack.identity.v3._proxy.Proxy.delete_user - .. automethod:: openstack.identity.v3._proxy.Proxy.get_user - .. automethod:: openstack.identity.v3._proxy.Proxy.find_user - .. automethod:: openstack.identity.v3._proxy.Proxy.users + :noindex: + :members: create_user, update_user, delete_user, get_user, find_user, users diff --git a/doc/source/user/proxies/image_v1.rst b/doc/source/user/proxies/image_v1.rst index 7be486538..10911a3df 100644 --- a/doc/source/user/proxies/image_v1.rst +++ b/doc/source/user/proxies/image_v1.rst @@ -13,10 +13,6 @@ The image high-level interface is available through the ``image`` member of a only be added if the service is detected. .. autoclass:: openstack.image.v1._proxy.Proxy - - .. automethod:: openstack.image.v1._proxy.Proxy.upload_image - .. automethod:: openstack.image.v1._proxy.Proxy.update_image - .. automethod:: openstack.image.v1._proxy.Proxy.delete_image - .. automethod:: openstack.image.v1._proxy.Proxy.get_image - .. automethod:: openstack.image.v1._proxy.Proxy.find_image - .. automethod:: openstack.image.v1._proxy.Proxy.images + :noindex: + :members: upload_image, update_image, delete_image, get_image, find_image, + images diff --git a/doc/source/user/proxies/image_v2.rst b/doc/source/user/proxies/image_v2.rst index 295e72cf4..1496f1b8f 100644 --- a/doc/source/user/proxies/image_v2.rst +++ b/doc/source/user/proxies/image_v2.rst @@ -16,60 +16,38 @@ Image Operations ^^^^^^^^^^^^^^^^ .. autoclass:: openstack.image.v2._proxy.Proxy - - .. automethod:: openstack.image.v2._proxy.Proxy.create_image - .. automethod:: openstack.image.v2._proxy.Proxy.import_image - .. automethod:: openstack.image.v2._proxy.Proxy.upload_image - .. automethod:: openstack.image.v2._proxy.Proxy.download_image - .. automethod:: openstack.image.v2._proxy.Proxy.update_image - .. automethod:: openstack.image.v2._proxy.Proxy.delete_image - .. automethod:: openstack.image.v2._proxy.Proxy.get_image - .. automethod:: openstack.image.v2._proxy.Proxy.find_image - .. automethod:: openstack.image.v2._proxy.Proxy.images - .. automethod:: openstack.image.v2._proxy.Proxy.deactivate_image - .. automethod:: openstack.image.v2._proxy.Proxy.reactivate_image - .. automethod:: openstack.image.v2._proxy.Proxy.stage_image - .. automethod:: openstack.image.v2._proxy.Proxy.add_tag - .. automethod:: openstack.image.v2._proxy.Proxy.remove_tag + :noindex: + :members: create_image, import_image, upload_image, download_image, + update_image, delete_image, get_image, find_image, images, + deactivate_image, reactivate_image, stage_image, + add_tag, remove_tag Member Operations ^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.image.v2._proxy.Proxy - - .. automethod:: openstack.image.v2._proxy.Proxy.add_member - .. automethod:: openstack.image.v2._proxy.Proxy.remove_member - .. automethod:: openstack.image.v2._proxy.Proxy.update_member - .. automethod:: openstack.image.v2._proxy.Proxy.get_member - .. automethod:: openstack.image.v2._proxy.Proxy.find_member - .. automethod:: openstack.image.v2._proxy.Proxy.members + :noindex: + :members: add_member, remove_member, update_member, get_member, find_member, + members Task Operations ^^^^^^^^^^^^^^^ .. autoclass:: openstack.image.v2._proxy.Proxy - - .. automethod:: openstack.image.v2._proxy.Proxy.tasks - .. automethod:: openstack.image.v2._proxy.Proxy.create_task - .. automethod:: openstack.image.v2._proxy.Proxy.get_task - .. automethod:: openstack.image.v2._proxy.Proxy.wait_for_task + :noindex: + :members: tasks, create_task, get_task, wait_for_task Schema Operations ^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.image.v2._proxy.Proxy - - .. automethod:: openstack.image.v2._proxy.Proxy.get_images_schema - .. automethod:: openstack.image.v2._proxy.Proxy.get_image_schema - .. automethod:: openstack.image.v2._proxy.Proxy.get_members_schema - .. automethod:: openstack.image.v2._proxy.Proxy.get_member_schema - .. automethod:: openstack.image.v2._proxy.Proxy.get_tasks_schema - .. automethod:: openstack.image.v2._proxy.Proxy.get_task_schema + :noindex: + :members: get_images_schema, get_image_schema, get_members_schema, + get_member_schema, get_tasks_schema, get_task_schema Service Info Discovery Operations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.image.v2._proxy.Proxy - - .. automethod:: openstack.image.v2._proxy.Proxy.stores - .. automethod:: openstack.image.v2._proxy.Proxy.get_import_info + :noindex: + :members: stores, get_import_info diff --git a/doc/source/user/proxies/key_manager.rst b/doc/source/user/proxies/key_manager.rst index 291fb77fe..2b611e9dc 100644 --- a/doc/source/user/proxies/key_manager.rst +++ b/doc/source/user/proxies/key_manager.rst @@ -18,34 +18,22 @@ Secret Operations ^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.key_manager.v1._proxy.Proxy - - .. automethod:: openstack.key_manager.v1._proxy.Proxy.create_secret - .. automethod:: openstack.key_manager.v1._proxy.Proxy.update_secret - .. automethod:: openstack.key_manager.v1._proxy.Proxy.delete_secret - .. automethod:: openstack.key_manager.v1._proxy.Proxy.get_secret - .. automethod:: openstack.key_manager.v1._proxy.Proxy.find_secret - .. automethod:: openstack.key_manager.v1._proxy.Proxy.secrets + :noindex: + :members: create_secret, update_secret, delete_secret, get_secret, + find_secret, secrets Container Operations ^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.key_manager.v1._proxy.Proxy - - .. automethod:: openstack.key_manager.v1._proxy.Proxy.create_container - .. automethod:: openstack.key_manager.v1._proxy.Proxy.update_container - .. automethod:: openstack.key_manager.v1._proxy.Proxy.delete_container - .. automethod:: openstack.key_manager.v1._proxy.Proxy.get_container - .. automethod:: openstack.key_manager.v1._proxy.Proxy.find_container - .. automethod:: openstack.key_manager.v1._proxy.Proxy.containers + :noindex: + :members: create_container, update_container, delete_container, + get_container, find_container, containers Order Operations ^^^^^^^^^^^^^^^^ .. autoclass:: openstack.key_manager.v1._proxy.Proxy - - .. automethod:: openstack.key_manager.v1._proxy.Proxy.create_order - .. automethod:: openstack.key_manager.v1._proxy.Proxy.update_order - .. automethod:: openstack.key_manager.v1._proxy.Proxy.delete_order - .. automethod:: openstack.key_manager.v1._proxy.Proxy.get_order - .. automethod:: openstack.key_manager.v1._proxy.Proxy.find_order - .. automethod:: openstack.key_manager.v1._proxy.Proxy.orders + :noindex: + :members: create_order, update_order, delete_order, get_order, + find_order, orders diff --git a/doc/source/user/proxies/load_balancer_v2.rst b/doc/source/user/proxies/load_balancer_v2.rst index ea91fd87f..ab4d887d1 100644 --- a/doc/source/user/proxies/load_balancer_v2.rst +++ b/doc/source/user/proxies/load_balancer_v2.rst @@ -14,164 +14,110 @@ Load Balancer Operations ^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.load_balancer.v2._proxy.Proxy - - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.create_load_balancer - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_load_balancer - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_load_balancer - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_load_balancer - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_load_balancer_statistics - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.load_balancers - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_load_balancer - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.failover_load_balancer + :noindex: + :members: create_load_balancer, delete_load_balancer, find_load_balancer, + get_load_balancer, get_load_balancer_statistics, load_balancers, + update_load_balancer, failover_load_balancer Listener Operations ^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.load_balancer.v2._proxy.Proxy - - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.create_listener - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_listener - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_listener - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_listener - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_listener_statistics - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.listeners - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_listener + :noindex: + :members: create_listener, delete_listener, find_listener, get_listener, + get_listener_statistics, listeners, update_listener Pool Operations ^^^^^^^^^^^^^^^ .. autoclass:: openstack.load_balancer.v2._proxy.Proxy - - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.create_pool - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_pool - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_pool - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_pool - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.pools - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_pool + :noindex: + :members: create_pool, delete_pool, find_pool, get_pool, pools, update_pool Member Operations ^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.load_balancer.v2._proxy.Proxy - - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.create_member - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_member - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_member - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_member - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.members - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_member + :noindex: + :members: create_member, delete_member, find_member, get_member, members, + update_member Health Monitor Operations ^^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.load_balancer.v2._proxy.Proxy - - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.create_health_monitor - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_health_monitor - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_health_monitor - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_health_monitor - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.health_monitors - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_health_monitor + :noindex: + :members: create_health_monitor, delete_health_monitor, find_health_monitor, + get_health_monitor, health_monitors, update_health_monitor L7 Policy Operations ^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.load_balancer.v2._proxy.Proxy - - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.create_l7_policy - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_l7_policy - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_l7_policy - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_l7_policy - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.l7_policies - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_l7_policy + :noindex: + :members: create_l7_policy, delete_l7_policy, find_l7_policy, + get_l7_policy, l7_policies, update_l7_policy L7 Rule Operations ^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.load_balancer.v2._proxy.Proxy - - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.create_l7_rule - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_l7_rule - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_l7_rule - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_l7_rule - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.l7_rules - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_l7_rule + :noindex: + :members: create_l7_rule, delete_l7_rule, find_l7_rule, + get_l7_rule, l7_rules, update_l7_rule Provider Operations ^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.load_balancer.v2._proxy.Proxy - - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.providers - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.provider_flavor_capabilities + :noindex: + :members: providers, provider_flavor_capabilities Flavor Profile Operations ^^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.load_balancer.v2._proxy.Proxy - - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.create_flavor_profile - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_flavor_profile - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.flavor_profiles - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_flavor_profile - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_flavor_profile - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_flavor_profile + :noindex: + :members: create_flavor_profile, get_flavor_profile, flavor_profiles, + delete_flavor_profile, find_flavor_profile, update_flavor_profile Flavor Operations ^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.load_balancer.v2._proxy.Proxy - - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.create_flavor - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_flavor - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.flavors - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_flavor - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_flavor - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_flavor + :noindex: + :members: create_flavor, get_flavor, flavors, delete_flavor, + find_flavor, update_flavor Quota Operations ^^^^^^^^^^^^^^^^ .. autoclass:: openstack.load_balancer.v2._proxy.Proxy - - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_quota - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_quota - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.quotas - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_quota - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_quota_default + :noindex: + :members: update_quota, delete_quota, quotas, get_quota, get_quota_default Amphora Operations ^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.load_balancer.v2._proxy.Proxy - - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.amphorae - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_amphora - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_amphora - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.configure_amphora - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.failover_amphora + :noindex: + :members: amphorae, get_amphora, find_amphora, configure_amphora, + failover_amphora Availability Zone Profile Operations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.load_balancer.v2._proxy.Proxy - - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.create_availability_zone_profile - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_availability_zone_profile - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.availability_zone_profiles - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_availability_zone_profile - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_availability_zone_profile - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_availability_zone_profile + :noindex: + :members: create_availability_zone_profile, get_availability_zone_profile, + availability_zone_profiles, delete_availability_zone_profile, + find_availability_zone_profile, update_availability_zone_profile Availability Zone Operations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.load_balancer.v2._proxy.Proxy - - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.create_availability_zone - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.get_availability_zone - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.availability_zones - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.delete_availability_zone - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.find_availability_zone - .. automethod:: openstack.load_balancer.v2._proxy.Proxy.update_availability_zone - + :noindex: + :members: create_availability_zone, get_availability_zone, + availability_zones, delete_availability_zone, + find_availability_zone, update_availability_zone diff --git a/doc/source/user/proxies/message_v2.rst b/doc/source/user/proxies/message_v2.rst index dbf1f4778..361842c50 100644 --- a/doc/source/user/proxies/message_v2.rst +++ b/doc/source/user/proxies/message_v2.rst @@ -16,38 +16,27 @@ Message Operations ^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.message.v2._proxy.Proxy - - .. automethod:: openstack.message.v2._proxy.Proxy.post_message - .. automethod:: openstack.message.v2._proxy.Proxy.delete_message - .. automethod:: openstack.message.v2._proxy.Proxy.get_message - .. automethod:: openstack.message.v2._proxy.Proxy.messages + :noindex: + :members: post_message, delete_message, get_message, messages Queue Operations ^^^^^^^^^^^^^^^^ .. autoclass:: openstack.message.v2._proxy.Proxy - - .. automethod:: openstack.message.v2._proxy.Proxy.create_queue - .. automethod:: openstack.message.v2._proxy.Proxy.delete_queue - .. automethod:: openstack.message.v2._proxy.Proxy.get_queue - .. automethod:: openstack.message.v2._proxy.Proxy.queues + :noindex: + :members: create_queue, delete_queue, get_queue, queues Claim Operations ^^^^^^^^^^^^^^^^ .. autoclass:: openstack.message.v2._proxy.Proxy - - .. automethod:: openstack.message.v2._proxy.Proxy.create_claim - .. automethod:: openstack.message.v2._proxy.Proxy.update_claim - .. automethod:: openstack.message.v2._proxy.Proxy.delete_claim - .. automethod:: openstack.message.v2._proxy.Proxy.get_claim + :noindex: + :members: create_claim, update_claim, delete_claim, get_claim Subscription Operations ^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.message.v2._proxy.Proxy - - .. automethod:: openstack.message.v2._proxy.Proxy.create_subscription - .. automethod:: openstack.message.v2._proxy.Proxy.delete_subscription - .. automethod:: openstack.message.v2._proxy.Proxy.get_subscription - .. automethod:: openstack.message.v2._proxy.Proxy.subscriptions + :noindex: + :members: create_subscription, delete_subscription, get_subscription, + subscriptions diff --git a/doc/source/user/proxies/network.rst b/doc/source/user/proxies/network.rst index 3662d8fcc..960a49e64 100644 --- a/doc/source/user/proxies/network.rst +++ b/doc/source/user/proxies/network.rst @@ -16,355 +16,230 @@ Network Operations ^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.network.v2._proxy.Proxy - - .. automethod:: openstack.network.v2._proxy.Proxy.create_network - .. automethod:: openstack.network.v2._proxy.Proxy.update_network - .. automethod:: openstack.network.v2._proxy.Proxy.delete_network - .. automethod:: openstack.network.v2._proxy.Proxy.get_network - .. automethod:: openstack.network.v2._proxy.Proxy.find_network - .. automethod:: openstack.network.v2._proxy.Proxy.networks - - .. automethod:: openstack.network.v2._proxy.Proxy.get_network_ip_availability - .. automethod:: openstack.network.v2._proxy.Proxy.find_network_ip_availability - .. automethod:: openstack.network.v2._proxy.Proxy.network_ip_availabilities - - .. automethod:: openstack.network.v2._proxy.Proxy.add_dhcp_agent_to_network - .. automethod:: openstack.network.v2._proxy.Proxy.remove_dhcp_agent_from_network - .. automethod:: openstack.network.v2._proxy.Proxy.dhcp_agent_hosting_networks + :noindex: + :members: create_network, update_network, delete_network, get_network, + find_network, networks, get_network_ip_availability, + find_network_ip_availability, network_ip_availabilities, + add_dhcp_agent_to_network, remove_dhcp_agent_from_network, + dhcp_agent_hosting_networks, Port Operations ^^^^^^^^^^^^^^^ .. autoclass:: openstack.network.v2._proxy.Proxy - - .. automethod:: openstack.network.v2._proxy.Proxy.create_port - .. automethod:: openstack.network.v2._proxy.Proxy.create_ports - .. automethod:: openstack.network.v2._proxy.Proxy.update_port - .. automethod:: openstack.network.v2._proxy.Proxy.delete_port - .. automethod:: openstack.network.v2._proxy.Proxy.get_port - .. automethod:: openstack.network.v2._proxy.Proxy.find_port - .. automethod:: openstack.network.v2._proxy.Proxy.ports - - .. automethod:: openstack.network.v2._proxy.Proxy.add_ip_to_port - .. automethod:: openstack.network.v2._proxy.Proxy.remove_ip_from_port + :noindex: + :members: create_port, create_ports, update_port, delete_port, get_port, + find_port, ports, add_ip_to_port, remove_ip_from_port Router Operations ^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.network.v2._proxy.Proxy - - .. automethod:: openstack.network.v2._proxy.Proxy.create_router - .. automethod:: openstack.network.v2._proxy.Proxy.update_router - .. automethod:: openstack.network.v2._proxy.Proxy.delete_router - .. automethod:: openstack.network.v2._proxy.Proxy.get_router - .. automethod:: openstack.network.v2._proxy.Proxy.find_router - .. automethod:: openstack.network.v2._proxy.Proxy.routers - - .. automethod:: openstack.network.v2._proxy.Proxy.add_gateway_to_router - .. automethod:: openstack.network.v2._proxy.Proxy.remove_gateway_from_router - .. automethod:: openstack.network.v2._proxy.Proxy.add_interface_to_router - .. automethod:: openstack.network.v2._proxy.Proxy.remove_interface_from_router - .. automethod:: openstack.network.v2._proxy.Proxy.add_extra_routes_to_router - .. automethod:: openstack.network.v2._proxy.Proxy.remove_extra_routes_from_router + :noindex: + :members: create_router, update_router, delete_router, get_router, + find_router, routers, + add_gateway_to_router, remove_gateway_from_router, + add_interface_to_router, remove_interface_from_router, + add_extra_routes_to_router, remove_extra_routes_from_router Floating IP Operations ^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.network.v2._proxy.Proxy - - .. automethod:: openstack.network.v2._proxy.Proxy.create_ip - .. automethod:: openstack.network.v2._proxy.Proxy.update_ip - .. automethod:: openstack.network.v2._proxy.Proxy.delete_ip - .. automethod:: openstack.network.v2._proxy.Proxy.get_ip - .. automethod:: openstack.network.v2._proxy.Proxy.find_ip - .. automethod:: openstack.network.v2._proxy.Proxy.find_available_ip - .. automethod:: openstack.network.v2._proxy.Proxy.ips + :noindex: + :members: create_ip, update_ip, delete_ip, get_ip, find_ip, + find_available_ip, ips Pool Operations ^^^^^^^^^^^^^^^ .. autoclass:: openstack.network.v2._proxy.Proxy - - .. automethod:: openstack.network.v2._proxy.Proxy.create_pool - .. automethod:: openstack.network.v2._proxy.Proxy.update_pool - .. automethod:: openstack.network.v2._proxy.Proxy.delete_pool - .. automethod:: openstack.network.v2._proxy.Proxy.get_pool - .. automethod:: openstack.network.v2._proxy.Proxy.find_pool - .. automethod:: openstack.network.v2._proxy.Proxy.pools - - .. automethod:: openstack.network.v2._proxy.Proxy.create_pool_member - .. automethod:: openstack.network.v2._proxy.Proxy.update_pool_member - .. automethod:: openstack.network.v2._proxy.Proxy.delete_pool_member - .. automethod:: openstack.network.v2._proxy.Proxy.get_pool_member - .. automethod:: openstack.network.v2._proxy.Proxy.find_pool_member - .. automethod:: openstack.network.v2._proxy.Proxy.pool_members + :noindex: + :members: create_pool, update_pool, delete_pool, get_pool, find_pool, pools, + create_pool_member, update_pool_member, delete_pool_member, + get_pool_member, find_pool_member, pool_members Auto Allocated Topology Operations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.network.v2._proxy.Proxy - - .. automethod:: openstack.network.v2._proxy.Proxy.delete_auto_allocated_topology - .. automethod:: openstack.network.v2._proxy.Proxy.get_auto_allocated_topology - .. automethod:: openstack.network.v2._proxy.Proxy.validate_auto_allocated_topology + :noindex: + :members: delete_auto_allocated_topology, get_auto_allocated_topology, + validate_auto_allocated_topology Security Group Operations ^^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.network.v2._proxy.Proxy - - .. automethod:: openstack.network.v2._proxy.Proxy.create_security_group - .. automethod:: openstack.network.v2._proxy.Proxy.update_security_group - .. automethod:: openstack.network.v2._proxy.Proxy.delete_security_group - .. automethod:: openstack.network.v2._proxy.Proxy.get_security_group - .. automethod:: openstack.network.v2._proxy.Proxy.get_security_group_rule - .. automethod:: openstack.network.v2._proxy.Proxy.find_security_group - .. automethod:: openstack.network.v2._proxy.Proxy.find_security_group_rule - .. automethod:: openstack.network.v2._proxy.Proxy.security_group_rules - .. automethod:: openstack.network.v2._proxy.Proxy.security_groups - - .. automethod:: openstack.network.v2._proxy.Proxy.create_security_group_rule - .. automethod:: openstack.network.v2._proxy.Proxy.create_security_group_rules - .. automethod:: openstack.network.v2._proxy.Proxy.delete_security_group_rule + :noindex: + :members: create_security_group, update_security_group, + delete_security_group, get_security_group, + get_security_group_rule, find_security_group, + find_security_group_rule, security_group_rules, + security_groups, create_security_group_rule, + create_security_group_rules, delete_security_group_rule Availability Zone Operations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.network.v2._proxy.Proxy - - .. automethod:: openstack.network.v2._proxy.Proxy.availability_zones + :noindex: + :members: availability_zones Address Scope Operations ^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.network.v2._proxy.Proxy - - .. automethod:: openstack.network.v2._proxy.Proxy.create_address_scope - .. automethod:: openstack.network.v2._proxy.Proxy.update_address_scope - .. automethod:: openstack.network.v2._proxy.Proxy.delete_address_scope - .. automethod:: openstack.network.v2._proxy.Proxy.get_address_scope - .. automethod:: openstack.network.v2._proxy.Proxy.find_address_scope - .. automethod:: openstack.network.v2._proxy.Proxy.address_scopes + :noindex: + :members: create_address_scope, update_address_scope, delete_address_scope, + get_address_scope, find_address_scope, address_scopes Quota Operations ^^^^^^^^^^^^^^^^ .. autoclass:: openstack.network.v2._proxy.Proxy - - .. automethod:: openstack.network.v2._proxy.Proxy.update_quota - .. automethod:: openstack.network.v2._proxy.Proxy.delete_quota - .. automethod:: openstack.network.v2._proxy.Proxy.get_quota - .. automethod:: openstack.network.v2._proxy.Proxy.get_quota_default - .. automethod:: openstack.network.v2._proxy.Proxy.quotas + :noindex: + :members: update_quota, delete_quota, get_quota, get_quota_default, quotas QoS Operations ^^^^^^^^^^^^^^ .. autoclass:: openstack.network.v2._proxy.Proxy - - .. automethod:: openstack.network.v2._proxy.Proxy.create_qos_policy - .. automethod:: openstack.network.v2._proxy.Proxy.update_qos_policy - .. automethod:: openstack.network.v2._proxy.Proxy.delete_qos_policy - .. automethod:: openstack.network.v2._proxy.Proxy.get_qos_policy - .. automethod:: openstack.network.v2._proxy.Proxy.find_qos_policy - .. automethod:: openstack.network.v2._proxy.Proxy.qos_policies - .. automethod:: openstack.network.v2._proxy.Proxy.get_qos_rule_type - .. automethod:: openstack.network.v2._proxy.Proxy.find_qos_rule_type - .. automethod:: openstack.network.v2._proxy.Proxy.qos_rule_types - - .. automethod:: openstack.network.v2._proxy.Proxy.create_qos_minimum_bandwidth_rule - .. automethod:: openstack.network.v2._proxy.Proxy.update_qos_minimum_bandwidth_rule - .. automethod:: openstack.network.v2._proxy.Proxy.delete_qos_minimum_bandwidth_rule - .. automethod:: openstack.network.v2._proxy.Proxy.get_qos_minimum_bandwidth_rule - .. automethod:: openstack.network.v2._proxy.Proxy.find_qos_minimum_bandwidth_rule - .. automethod:: openstack.network.v2._proxy.Proxy.qos_minimum_bandwidth_rules - - .. automethod:: openstack.network.v2._proxy.Proxy.create_qos_bandwidth_limit_rule - .. automethod:: openstack.network.v2._proxy.Proxy.update_qos_bandwidth_limit_rule - .. automethod:: openstack.network.v2._proxy.Proxy.delete_qos_bandwidth_limit_rule - .. automethod:: openstack.network.v2._proxy.Proxy.get_qos_bandwidth_limit_rule - .. automethod:: openstack.network.v2._proxy.Proxy.find_qos_bandwidth_limit_rule - .. automethod:: openstack.network.v2._proxy.Proxy.qos_bandwidth_limit_rules - - .. automethod:: openstack.network.v2._proxy.Proxy.create_qos_dscp_marking_rule - .. automethod:: openstack.network.v2._proxy.Proxy.update_qos_dscp_marking_rule - .. automethod:: openstack.network.v2._proxy.Proxy.delete_qos_dscp_marking_rule - .. automethod:: openstack.network.v2._proxy.Proxy.get_qos_dscp_marking_rule - .. automethod:: openstack.network.v2._proxy.Proxy.find_qos_dscp_marking_rule - .. automethod:: openstack.network.v2._proxy.Proxy.qos_dscp_marking_rules + :noindex: + :members: create_qos_policy, update_qos_policy, delete_qos_policy, + get_qos_policy, find_qos_policy, qos_policies, get_qos_rule_type, + find_qos_rule_type, qos_rule_types, + create_qos_minimum_bandwidth_rule, + update_qos_minimum_bandwidth_rule, + delete_qos_minimum_bandwidth_rule, + get_qos_minimum_bandwidth_rule, + find_qos_minimum_bandwidth_rule, + qos_minimum_bandwidth_rules, + create_qos_bandwidth_limit_rule, + update_qos_bandwidth_limit_rule, + delete_qos_bandwidth_limit_rule, + get_qos_bandwidth_limit_rule, find_qos_bandwidth_limit_rule, + qos_bandwidth_limit_rules, + create_qos_dscp_marking_rule, update_qos_dscp_marking_rule, + delete_qos_dscp_marking_rule, get_qos_dscp_marking_rule, + find_qos_dscp_marking_rule, qos_dscp_marking_rules Agent Operations ^^^^^^^^^^^^^^^^ .. autoclass:: openstack.network.v2._proxy.Proxy - - .. automethod:: openstack.network.v2._proxy.Proxy.delete_agent - .. automethod:: openstack.network.v2._proxy.Proxy.update_agent - .. automethod:: openstack.network.v2._proxy.Proxy.get_agent - .. automethod:: openstack.network.v2._proxy.Proxy.agents - .. automethod:: openstack.network.v2._proxy.Proxy.agent_hosted_routers - .. automethod:: openstack.network.v2._proxy.Proxy.routers_hosting_l3_agents - .. automethod:: openstack.network.v2._proxy.Proxy.network_hosting_dhcp_agents - - .. automethod:: openstack.network.v2._proxy.Proxy.add_router_to_agent - .. automethod:: openstack.network.v2._proxy.Proxy.remove_router_from_agent + :noindex: + :members: delete_agent, update_agent, get_agent, agents, + agent_hosted_routers, routers_hosting_l3_agents, + network_hosting_dhcp_agents, add_router_to_agent, + remove_router_from_agent RBAC Operations ^^^^^^^^^^^^^^^ .. autoclass:: openstack.network.v2._proxy.Proxy - - .. automethod:: openstack.network.v2._proxy.Proxy.create_rbac_policy - .. automethod:: openstack.network.v2._proxy.Proxy.update_rbac_policy - .. automethod:: openstack.network.v2._proxy.Proxy.delete_rbac_policy - .. automethod:: openstack.network.v2._proxy.Proxy.get_rbac_policy - .. automethod:: openstack.network.v2._proxy.Proxy.find_rbac_policy - .. automethod:: openstack.network.v2._proxy.Proxy.rbac_policies + :noindex: + :members: create_rbac_policy, update_rbac_policy, delete_rbac_policy, + get_rbac_policy, find_rbac_policy, rbac_policies Listener Operations ^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.network.v2._proxy.Proxy - - .. automethod:: openstack.network.v2._proxy.Proxy.create_listener - .. automethod:: openstack.network.v2._proxy.Proxy.update_listener - .. automethod:: openstack.network.v2._proxy.Proxy.delete_listener - .. automethod:: openstack.network.v2._proxy.Proxy.get_listener - .. automethod:: openstack.network.v2._proxy.Proxy.find_listener - .. automethod:: openstack.network.v2._proxy.Proxy.listeners + :noindex: + :members: create_listener, update_listener, delete_listener, + get_listener, find_listener, listeners Subnet Operations ^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.network.v2._proxy.Proxy - - .. automethod:: openstack.network.v2._proxy.Proxy.create_subnet - .. automethod:: openstack.network.v2._proxy.Proxy.update_subnet - .. automethod:: openstack.network.v2._proxy.Proxy.delete_subnet - .. automethod:: openstack.network.v2._proxy.Proxy.get_subnet - .. automethod:: openstack.network.v2._proxy.Proxy.get_subnet_ports - .. automethod:: openstack.network.v2._proxy.Proxy.find_subnet - .. automethod:: openstack.network.v2._proxy.Proxy.subnets - - .. automethod:: openstack.network.v2._proxy.Proxy.create_subnet_pool - .. automethod:: openstack.network.v2._proxy.Proxy.update_subnet_pool - .. automethod:: openstack.network.v2._proxy.Proxy.delete_subnet_pool - .. automethod:: openstack.network.v2._proxy.Proxy.get_subnet_pool - .. automethod:: openstack.network.v2._proxy.Proxy.find_subnet_pool - .. automethod:: openstack.network.v2._proxy.Proxy.subnet_pools + :noindex: + :members: create_subnet, update_subnet, delete_subnet, get_subnet, + get_subnet_ports, find_subnet, subnets, create_subnet_pool, + update_subnet_pool, delete_subnet_pool, get_subnet_pool, + find_subnet_pool, subnet_pools Load Balancer Operations ^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.network.v2._proxy.Proxy - - .. automethod:: openstack.network.v2._proxy.Proxy.create_load_balancer - .. automethod:: openstack.network.v2._proxy.Proxy.update_load_balancer - .. automethod:: openstack.network.v2._proxy.Proxy.delete_load_balancer - .. automethod:: openstack.network.v2._proxy.Proxy.get_load_balancer - .. automethod:: openstack.network.v2._proxy.Proxy.find_load_balancer - .. automethod:: openstack.network.v2._proxy.Proxy.load_balancers + :noindex: + :members: create_load_balancer, update_load_balancer, delete_load_balancer, + get_load_balancer, find_load_balancer, load_balancers Health Monitor Operations ^^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.network.v2._proxy.Proxy - - .. automethod:: openstack.network.v2._proxy.Proxy.create_health_monitor - .. automethod:: openstack.network.v2._proxy.Proxy.update_health_monitor - .. automethod:: openstack.network.v2._proxy.Proxy.delete_health_monitor - .. automethod:: openstack.network.v2._proxy.Proxy.get_health_monitor - .. automethod:: openstack.network.v2._proxy.Proxy.find_health_monitor - .. automethod:: openstack.network.v2._proxy.Proxy.health_monitors + :noindex: + :members: create_health_monitor, update_health_monitor, + delete_health_monitor, get_health_monitor, find_health_monitor, + health_monitors Metering Label Operations ^^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.network.v2._proxy.Proxy - - .. automethod:: openstack.network.v2._proxy.Proxy.create_metering_label - .. automethod:: openstack.network.v2._proxy.Proxy.update_metering_label - .. automethod:: openstack.network.v2._proxy.Proxy.delete_metering_label - .. automethod:: openstack.network.v2._proxy.Proxy.get_metering_label - .. automethod:: openstack.network.v2._proxy.Proxy.find_metering_label - .. automethod:: openstack.network.v2._proxy.Proxy.metering_labels - - .. automethod:: openstack.network.v2._proxy.Proxy.create_metering_label_rule - .. automethod:: openstack.network.v2._proxy.Proxy.update_metering_label_rule - .. automethod:: openstack.network.v2._proxy.Proxy.delete_metering_label_rule - .. automethod:: openstack.network.v2._proxy.Proxy.get_metering_label_rule - .. automethod:: openstack.network.v2._proxy.Proxy.find_metering_label_rule - .. automethod:: openstack.network.v2._proxy.Proxy.metering_label_rules + :noindex: + :members: create_metering_label, update_metering_label, + delete_metering_label, get_metering_label, find_metering_label, + metering_labels, create_metering_label_rule, + update_metering_label_rule, delete_metering_label_rule, + get_metering_label_rule, find_metering_label_rule, + metering_label_rules Segment Operations ^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.network.v2._proxy.Proxy - - .. automethod:: openstack.network.v2._proxy.Proxy.create_segment - .. automethod:: openstack.network.v2._proxy.Proxy.update_segment - .. automethod:: openstack.network.v2._proxy.Proxy.delete_segment - .. automethod:: openstack.network.v2._proxy.Proxy.get_segment - .. automethod:: openstack.network.v2._proxy.Proxy.find_segment - .. automethod:: openstack.network.v2._proxy.Proxy.segments + :noindex: + :members: create_segment, update_segment, delete_segment, get_segment, + find_segment, segments Flavor Operations ^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.network.v2._proxy.Proxy - - .. automethod:: openstack.network.v2._proxy.Proxy.create_flavor - .. automethod:: openstack.network.v2._proxy.Proxy.update_flavor - .. automethod:: openstack.network.v2._proxy.Proxy.delete_flavor - .. automethod:: openstack.network.v2._proxy.Proxy.get_flavor - .. automethod:: openstack.network.v2._proxy.Proxy.find_flavor - .. automethod:: openstack.network.v2._proxy.Proxy.flavors + :noindex: + :members: create_flavor, update_flavor, delete_flavor, get_flavor, + find_flavor, flavors Service Profile Operations ^^^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.network.v2._proxy.Proxy - - .. automethod:: openstack.network.v2._proxy.Proxy.create_service_profile - .. automethod:: openstack.network.v2._proxy.Proxy.update_service_profile - .. automethod:: openstack.network.v2._proxy.Proxy.delete_service_profile - .. automethod:: openstack.network.v2._proxy.Proxy.get_service_profile - .. automethod:: openstack.network.v2._proxy.Proxy.find_service_profile - .. automethod:: openstack.network.v2._proxy.Proxy.service_profiles - - .. automethod:: openstack.network.v2._proxy.Proxy.associate_flavor_with_service_profile - .. automethod:: openstack.network.v2._proxy.Proxy.disassociate_flavor_from_service_profile + :noindex: + :members: create_service_profile, update_service_profile, + delete_service_profile, get_service_profile, find_service_profile, + service_profiles, associate_flavor_with_service_profile, + disassociate_flavor_from_service_profile Tag Operations ^^^^^^^^^^^^^^ .. autoclass:: openstack.network.v2._proxy.Proxy - - .. automethod:: openstack.network.v2._proxy.Proxy.set_tags + :noindex: + :members: set_tags VPN Operations ^^^^^^^^^^^^^^ .. autoclass:: openstack.network.v2._proxy.Proxy - - .. automethod:: openstack.network.v2._proxy.Proxy.create_vpn_service - .. automethod:: openstack.network.v2._proxy.Proxy.update_vpn_service - .. automethod:: openstack.network.v2._proxy.Proxy.delete_vpn_service - .. automethod:: openstack.network.v2._proxy.Proxy.get_vpn_service - .. automethod:: openstack.network.v2._proxy.Proxy.find_vpn_service - .. automethod:: openstack.network.v2._proxy.Proxy.vpn_services + :noindex: + :members: create_vpn_service, update_vpn_service, delete_vpn_service, + get_vpn_service, find_vpn_service, vpn_services Extension Operations ^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.network.v2._proxy.Proxy - - .. automethod:: openstack.network.v2._proxy.Proxy.find_extension - .. automethod:: openstack.network.v2._proxy.Proxy.extensions + :noindex: + :members: find_extension, extensions Service Provider Operations ^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.network.v2._proxy.Proxy - - .. automethod:: openstack.network.v2._proxy.Proxy.service_providers + :noindex: + :members: service_providers diff --git a/doc/source/user/proxies/object_store.rst b/doc/source/user/proxies/object_store.rst index ca6e28b34..fa286d8e3 100644 --- a/doc/source/user/proxies/object_store.rst +++ b/doc/source/user/proxies/object_store.rst @@ -15,36 +15,23 @@ Account Operations ^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.object_store.v1._proxy.Proxy - - .. automethod:: openstack.object_store.v1._proxy.Proxy.get_account_metadata - .. automethod:: openstack.object_store.v1._proxy.Proxy.set_account_metadata - .. automethod:: openstack.object_store.v1._proxy.Proxy.delete_account_metadata + :noindex: + :members: get_account_metadata, set_account_metadata, delete_account_metadata Container Operations ^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.object_store.v1._proxy.Proxy - - .. automethod:: openstack.object_store.v1._proxy.Proxy.create_container - .. automethod:: openstack.object_store.v1._proxy.Proxy.delete_container - .. automethod:: openstack.object_store.v1._proxy.Proxy.containers - - .. automethod:: openstack.object_store.v1._proxy.Proxy.get_container_metadata - .. automethod:: openstack.object_store.v1._proxy.Proxy.set_container_metadata - .. automethod:: openstack.object_store.v1._proxy.Proxy.delete_container_metadata + :noindex: + :members: create_container, delete_container, containers, + get_container_metadata, set_container_metadata, + delete_container_metadata Object Operations ^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.object_store.v1._proxy.Proxy - - .. automethod:: openstack.object_store.v1._proxy.Proxy.upload_object - .. automethod:: openstack.object_store.v1._proxy.Proxy.download_object - .. automethod:: openstack.object_store.v1._proxy.Proxy.copy_object - .. automethod:: openstack.object_store.v1._proxy.Proxy.delete_object - .. automethod:: openstack.object_store.v1._proxy.Proxy.get_object - .. automethod:: openstack.object_store.v1._proxy.Proxy.objects - - .. automethod:: openstack.object_store.v1._proxy.Proxy.get_object_metadata - .. automethod:: openstack.object_store.v1._proxy.Proxy.set_object_metadata - .. automethod:: openstack.object_store.v1._proxy.Proxy.delete_object_metadata + :noindex: + :members: upload_object, download_object, copy_object, delete_object, + get_object, objects, get_object_metadata, set_object_metadata, + delete_object_metadata diff --git a/doc/source/user/proxies/orchestration.rst b/doc/source/user/proxies/orchestration.rst index 9c2a9dbd9..93ee46fc3 100644 --- a/doc/source/user/proxies/orchestration.rst +++ b/doc/source/user/proxies/orchestration.rst @@ -17,37 +17,24 @@ Stack Operations ^^^^^^^^^^^^^^^^ .. autoclass:: openstack.orchestration.v1._proxy.Proxy - - .. automethod:: openstack.orchestration.v1._proxy.Proxy.create_stack - .. automethod:: openstack.orchestration.v1._proxy.Proxy.check_stack - .. automethod:: openstack.orchestration.v1._proxy.Proxy.update_stack - .. automethod:: openstack.orchestration.v1._proxy.Proxy.delete_stack - .. automethod:: openstack.orchestration.v1._proxy.Proxy.find_stack - .. automethod:: openstack.orchestration.v1._proxy.Proxy.get_stack - .. automethod:: openstack.orchestration.v1._proxy.Proxy.get_stack_environment - .. automethod:: openstack.orchestration.v1._proxy.Proxy.get_stack_files - .. automethod:: openstack.orchestration.v1._proxy.Proxy.get_stack_template - .. automethod:: openstack.orchestration.v1._proxy.Proxy.stacks - .. automethod:: openstack.orchestration.v1._proxy.Proxy.validate_template - .. automethod:: openstack.orchestration.v1._proxy.Proxy.resources + :noindex: + :members: create_stack, check_stack, update_stack, delete_stack, find_stack, + get_stack, get_stack_environment, get_stack_files, + get_stack_template, stacks, validate_template, resources Software Configuration Operations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.orchestration.v1._proxy.Proxy - - .. automethod:: openstack.orchestration.v1._proxy.Proxy.create_software_config - .. automethod:: openstack.orchestration.v1._proxy.Proxy.delete_software_config - .. automethod:: openstack.orchestration.v1._proxy.Proxy.get_software_config - .. automethod:: openstack.orchestration.v1._proxy.Proxy.software_configs + :noindex: + :members: create_software_config, delete_software_config, + get_software_config, software_configs Software Deployment Operations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.orchestration.v1._proxy.Proxy - - .. automethod:: openstack.orchestration.v1._proxy.Proxy.create_software_deployment - .. automethod:: openstack.orchestration.v1._proxy.Proxy.update_software_deployment - .. automethod:: openstack.orchestration.v1._proxy.Proxy.delete_software_deployment - .. automethod:: openstack.orchestration.v1._proxy.Proxy.get_software_deployment - .. automethod:: openstack.orchestration.v1._proxy.Proxy.software_deployments + :noindex: + :members: create_software_deployment, update_software_deployment, + delete_software_deployment, get_software_deployment, + software_deployments diff --git a/doc/source/user/proxies/workflow.rst b/doc/source/user/proxies/workflow.rst index 429d6d7ac..70fd18b9b 100644 --- a/doc/source/user/proxies/workflow.rst +++ b/doc/source/user/proxies/workflow.rst @@ -14,20 +14,14 @@ Workflow Operations ^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.workflow.v2._proxy.Proxy - - .. automethod:: openstack.workflow.v2._proxy.Proxy.create_workflow - .. automethod:: openstack.workflow.v2._proxy.Proxy.delete_workflow - .. automethod:: openstack.workflow.v2._proxy.Proxy.get_workflow - .. automethod:: openstack.workflow.v2._proxy.Proxy.find_workflow - .. automethod:: openstack.workflow.v2._proxy.Proxy.workflows + :noindex: + :members: create_workflow, delete_workflow, get_workflow, + find_workflow, workflows Execution Operations ^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.workflow.v2._proxy.Proxy - - .. automethod:: openstack.workflow.v2._proxy.Proxy.create_execution - .. automethod:: openstack.workflow.v2._proxy.Proxy.delete_execution - .. automethod:: openstack.workflow.v2._proxy.Proxy.get_execution - .. automethod:: openstack.workflow.v2._proxy.Proxy.find_execution - .. automethod:: openstack.workflow.v2._proxy.Proxy.executions + :noindex: + :members: create_execution, delete_execution, get_execution, + find_execution, executions From 04b55dab961ad5fe02d69890fde686dc659416e0 Mon Sep 17 00:00:00 2001 From: Noah Mickus Date: Mon, 20 Apr 2020 16:48:10 -0500 Subject: [PATCH 2652/3836] Add cipher list support for octavia Added a property "tls_ciphers" to pools.py and listeners.py for a storing a string of tls cipers in OpenSSL cipher string format. Story: 2006627 Task: 37190 Change-Id: Iaf1178cf2131f12f501318fa8dd2548b218132fc --- openstack/load_balancer/v2/listener.py | 3 +++ openstack/load_balancer/v2/pool.py | 3 +++ openstack/tests/unit/load_balancer/test_listener.py | 4 ++++ openstack/tests/unit/load_balancer/test_pool.py | 6 +++++- ...add-cipher-list-support-to-octavia-b6b2b0053ca6b184.yaml | 6 ++++++ 5 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-cipher-list-support-to-octavia-b6b2b0053ca6b184.yaml diff --git a/openstack/load_balancer/v2/listener.py b/openstack/load_balancer/v2/listener.py index 88ccfef74..455595a81 100644 --- a/openstack/load_balancer/v2/listener.py +++ b/openstack/load_balancer/v2/listener.py @@ -32,6 +32,7 @@ class Listener(resource.Resource, resource.TagMixin): 'sni_container_refs', 'insert_headers', 'load_balancer_id', 'timeout_client_data', 'timeout_member_connect', 'timeout_member_data', 'timeout_tcp_inspect', 'allowed_cidrs', + 'tls_ciphers', is_admin_state_up='admin_state_up', **resource.TagMixin._tag_query_parameters ) @@ -91,6 +92,8 @@ class Listener(resource.Resource, resource.TagMixin): #: Time, in milliseconds, to wait for additional TCP packets for content #: inspection. timeout_tcp_inspect = resource.Body('timeout_tcp_inspect', type=int) + #: Stores a cipher string in OpenSSL format. + tls_ciphers = resource.Body('tls_ciphers') class ListenerStats(resource.Resource): diff --git a/openstack/load_balancer/v2/pool.py b/openstack/load_balancer/v2/pool.py index 8794e9ce1..2766b381b 100644 --- a/openstack/load_balancer/v2/pool.py +++ b/openstack/load_balancer/v2/pool.py @@ -29,6 +29,7 @@ class Pool(resource.Resource, resource.TagMixin): 'health_monitor_id', 'lb_algorithm', 'listener_id', 'loadbalancer_id', 'description', 'name', 'project_id', 'protocol', 'created_at', 'updated_at', 'provisioning_status', 'operating_status', + 'tls_ciphers', is_admin_state_up='admin_state_up', **resource.TagMixin._tag_query_parameters ) @@ -64,6 +65,8 @@ class Pool(resource.Resource, resource.TagMixin): protocol = resource.Body('protocol') #: Provisioning status of the pool provisioning_status = resource.Body('provisioning_status') + #: Stores a string of cipher strings in OpenSSL format. + tls_ciphers = resource.Body('tls_ciphers') #: A JSON object specifying the session persistence for the pool. session_persistence = resource.Body('session_persistence', type=dict) #: Timestamp when the pool was updated diff --git a/openstack/tests/unit/load_balancer/test_listener.py b/openstack/tests/unit/load_balancer/test_listener.py index 299e61e94..6c34d34cc 100644 --- a/openstack/tests/unit/load_balancer/test_listener.py +++ b/openstack/tests/unit/load_balancer/test_listener.py @@ -41,6 +41,7 @@ 'timeout_member_connect': 5000, 'timeout_member_data': 50000, 'timeout_tcp_inspect': 0, + 'tls_ciphers': 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256' } EXAMPLE_STATS = { @@ -103,6 +104,8 @@ def test_make_it(self): test_listener.timeout_member_data) self.assertEqual(EXAMPLE['timeout_tcp_inspect'], test_listener.timeout_tcp_inspect) + self.assertEqual(EXAMPLE['tls_ciphers'], + test_listener.tls_ciphers) self.assertDictEqual( {'limit': 'limit', @@ -133,6 +136,7 @@ def test_make_it(self): 'timeout_member_connect': 'timeout_member_connect', 'timeout_member_data': 'timeout_member_data', 'timeout_tcp_inspect': 'timeout_tcp_inspect', + 'tls_ciphers': 'tls_ciphers', }, test_listener._query_mapping._mapping) diff --git a/openstack/tests/unit/load_balancer/test_pool.py b/openstack/tests/unit/load_balancer/test_pool.py index efecf5b2d..412449e9d 100644 --- a/openstack/tests/unit/load_balancer/test_pool.py +++ b/openstack/tests/unit/load_balancer/test_pool.py @@ -34,7 +34,8 @@ 'updated_at': '2017-07-17T12:16:57.233772', 'health_monitor': 'healthmonitor', 'health_monitor_id': uuid.uuid4(), - 'members': [{'id': uuid.uuid4()}] + 'members': [{'id': uuid.uuid4()}], + 'tls_ciphers': 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256' } @@ -81,6 +82,8 @@ def test_make_it(self): self.assertEqual(EXAMPLE['health_monitor_id'], test_pool.health_monitor_id) self.assertEqual(EXAMPLE['members'], test_pool.members) + self.assertEqual(EXAMPLE['tls_ciphers'], + test_pool.tls_ciphers) self.assertDictEqual( {'limit': 'limit', @@ -103,5 +106,6 @@ def test_make_it(self): 'listener_id': 'listener_id', 'loadbalancer_id': 'loadbalancer_id', 'protocol': 'protocol', + 'tls_ciphers': 'tls_ciphers', }, test_pool._query_mapping._mapping) diff --git a/releasenotes/notes/add-cipher-list-support-to-octavia-b6b2b0053ca6b184.yaml b/releasenotes/notes/add-cipher-list-support-to-octavia-b6b2b0053ca6b184.yaml new file mode 100644 index 000000000..e29879d5d --- /dev/null +++ b/releasenotes/notes/add-cipher-list-support-to-octavia-b6b2b0053ca6b184.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Added the ``tls_ciphers`` properties to listener.py + and pool.py for storing stings of tls ciphers in + OpenSSL cipher string format. From 09b739b11db42e6b94b2cd471216ed393950d5d6 Mon Sep 17 00:00:00 2001 From: Johannes Kulik Date: Thu, 7 May 2020 09:16:22 +0200 Subject: [PATCH 2653/3836] Add "id" to Port's query parameters According to the network v2 api-ref [1], it's possible to query ports by id, i.e. retrieve multiple ports we know the id of at once. This commit adds support for it. [1] https://docs.openstack.org/api-ref/network/v2/?expanded=list-ports-detail#list-ports Change-Id: I062b8b141e6200146c1f54d04649cf476887f179 --- openstack/network/v2/port.py | 5 +++-- openstack/tests/unit/network/v2/test_port.py | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index 6489983cd..6efcad0fb 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -30,8 +30,9 @@ class Port(_base.NetworkResource, resource.TagMixin): _query_mapping = resource.QueryParameters( 'binding:host_id', 'binding:profile', 'binding:vif_details', 'binding:vif_type', 'binding:vnic_type', - 'description', 'device_id', 'device_owner', 'fixed_ips', 'ip_address', - 'mac_address', 'name', 'network_id', 'status', 'subnet_id', + 'description', 'device_id', 'device_owner', 'fixed_ips', 'id', + 'ip_address', 'mac_address', 'name', 'network_id', 'status', + 'subnet_id', is_admin_state_up='admin_state_up', is_port_security_enabled='port_security_enabled', project_id='tenant_id', diff --git a/openstack/tests/unit/network/v2/test_port.py b/openstack/tests/unit/network/v2/test_port.py index 728bdab96..a40b46294 100644 --- a/openstack/tests/unit/network/v2/test_port.py +++ b/openstack/tests/unit/network/v2/test_port.py @@ -85,6 +85,7 @@ def test_basic(self): "device_id": "device_id", "device_owner": "device_owner", "fixed_ips": "fixed_ips", + "id": "id", "ip_address": "ip_address", "mac_address": "mac_address", "name": "name", From 98f4f24f4743e5253ee2f9a460fe64c13a620109 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 7 May 2020 16:45:42 +0200 Subject: [PATCH 2654/3836] Fix metric names in the object_store Under swift we have only endpoints/account/container/object resources, and regular scheme of the resource from url detection doesn't work. Change-Id: I03c7da8a579471c362e4124deeff43d67cdb986e --- openstack/object_store/v1/_proxy.py | 17 ++++++++++++----- .../tests/unit/object_store/v1/test_proxy.py | 11 +++++++++-- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index eba059e6a..df98358ad 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -69,14 +69,21 @@ def _extract_name(self, url, service_type=None, project_id=None): and url_parts[0][0] == 'v' and url_parts[0][1] and url_parts[0][1].isdigit()): url_parts = url_parts[1:] - name_parts = self._extract_name_consume_url_parts(url_parts) + + # Strip out anything that's empty or None + parts = [part for part in url_parts if part] # Getting the root of an endpoint is doing version discovery - if not name_parts: - name_parts = ['account'] + if not parts: + return ['account'] - # Strip out anything that's empty or None - return [part for part in name_parts if part] + if len(parts) == 1: + if 'endpoints' in parts: + return ['endpoints'] + else: + return ['container'] + else: + return ['object'] def get_account_metadata(self): """Get metadata for this account. diff --git a/openstack/tests/unit/object_store/v1/test_proxy.py b/openstack/tests/unit/object_store/v1/test_proxy.py index f3c10724d..510c11302 100644 --- a/openstack/tests/unit/object_store/v1/test_proxy.py +++ b/openstack/tests/unit/object_store/v1/test_proxy.py @@ -247,12 +247,19 @@ def test_stream(self): class TestExtractName(TestObjectStoreProxy): scenarios = [ - ('discovery', dict(url='/', parts=['account'])) + ('discovery', dict(url='/', parts=['account'])), + ('endpoints', dict(url='/endpoints', parts=['endpoints'])), + ('container', dict(url='/AUTH_123/container_name', + parts=['container'])), + ('object', dict(url='/container_name/object_name', + parts=['object'])), + ('object_long', dict(url='/v1/AUTH_123/cnt/path/deep/object_name', + parts=['object'])) ] def test_extract_name(self): - results = self.proxy._extract_name(self.url) + results = self.proxy._extract_name(self.url, project_id='123') self.assertEqual(self.parts, results) From d8be0e94d4b78c6fd4dd25888cffe113641f4176 Mon Sep 17 00:00:00 2001 From: Alex Schultz Date: Fri, 8 May 2020 09:29:06 -0600 Subject: [PATCH 2655/3836] Don't error if clouds.yaml is not readable There is a search path that we should check the next file if the first file we find is not readable. Change-Id: Ib638fe74210257f9175e28c1ccfff4493ef32873 Story: 2007645 Task: 39704 --- openstack/config/loader.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 09e916c37..7049ddd5b 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -17,6 +17,7 @@ import argparse as argparse_mod import collections import copy +import errno import json import os import re @@ -378,11 +379,17 @@ def _load_vendor_file(self): def _load_yaml_json_file(self, filelist): for path in filelist: if os.path.exists(path): - with open(path, 'r') as f: - if path.endswith('json'): - return path, json.load(f) - else: - return path, yaml.safe_load(f) + try: + with open(path, 'r') as f: + if path.endswith('json'): + return path, json.load(f) + else: + return path, yaml.safe_load(f) + except IOError as e: + if e.errno == errno.EACCES: + # Can't access file so let's continue to the next + # file + continue return (None, {}) def _expand_region_name(self, region_name): From 1b21b7bda1ead2d28568acea7fc0c0addaa913b0 Mon Sep 17 00:00:00 2001 From: Victor Coutellier Date: Sun, 10 May 2020 12:48:58 +0000 Subject: [PATCH 2656/3836] Add name query filter to keystone service Keystone actually support filtering services by name for a long time, but it was never documented in the api-ref, here is the patch for the keystone api documentation : https://review.opendev.org/#/c/726580/ We need to be able to use this filter in the SDK too. Change-Id: I634a0d867aa2b2f3495e8267488b12a2123a4f05 Story: 2007652 Task: 39716 --- openstack/identity/v3/service.py | 1 + openstack/tests/unit/identity/v3/test_service.py | 1 + 2 files changed, 2 insertions(+) diff --git a/openstack/identity/v3/service.py b/openstack/identity/v3/service.py index 982254ad4..d7a4a4bbf 100644 --- a/openstack/identity/v3/service.py +++ b/openstack/identity/v3/service.py @@ -27,6 +27,7 @@ class Service(resource.Resource): commit_method = 'PATCH' _query_mapping = resource.QueryParameters( + 'name', 'type', ) diff --git a/openstack/tests/unit/identity/v3/test_service.py b/openstack/tests/unit/identity/v3/test_service.py index a6da1fddd..a56924301 100644 --- a/openstack/tests/unit/identity/v3/test_service.py +++ b/openstack/tests/unit/identity/v3/test_service.py @@ -41,6 +41,7 @@ def test_basic(self): self.assertDictEqual( { + 'name': 'name', 'type': 'type', 'limit': 'limit', 'marker': 'marker', From fc7609f45e1047dd941a2129c50cd86b07d134ee Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 10 May 2020 08:19:03 -0500 Subject: [PATCH 2657/3836] Strip self from incoming glance properties We do this in list, but we should really also do it in translate response - because seriously this can never work. Change-Id: Ic97499b9fe1b563049da59899c34dbd810fe1509 --- openstack/resource.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openstack/resource.py b/openstack/resource.py index f2a9ba598..0634b9ecb 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1117,6 +1117,13 @@ def _translate_response(self, response, has_body=None, error_message=None): if self.resource_key and self.resource_key in body: body = body[self.resource_key] + # Do not allow keys called "self" through. Glance chose + # to name a key "self", so we need to pop it out because + # we can't send it through cls.existing and into the + # Resource initializer. "self" is already the first + # argument and is practically a reserved word. + body.pop("self", None) + body_attrs = self._consume_body_attrs(body) if self._store_unknown_attrs_as_properties: From e00c9ca2b14fb1a7b9233758aaa7cb4c95afb0ca Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 10 May 2020 08:06:16 -0500 Subject: [PATCH 2658/3836] Fix image owner field The field was owner in the original shade image normalize contract. It's also owner in glance, and in python-openstackclient. Unfortunately we've made a release with this set to owner_id, so what we need to do now is support this new owner_id thing else we'll break people. Change-Id: Ic074e4faed5cc391b8aff6b2e1a451f095d00024 --- openstack/image/v1/image.py | 4 +++- openstack/image/v2/image.py | 4 +++- openstack/tests/unit/image/v1/test_image.py | 1 + openstack/tests/unit/image/v2/test_image.py | 1 + 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/openstack/image/v1/image.py b/openstack/image/v1/image.py index 9fc7e7c08..c4905abfd 100644 --- a/openstack/image/v1/image.py +++ b/openstack/image/v1/image.py @@ -70,7 +70,9 @@ class Image(resource.Resource, _download.DownloadMixin): #: of images owned by others. name = resource.Body('name') #: The ID of the owner, or project, of the image. - owner_id = resource.Body('owner') + owner = resource.Body('owner', alias='owner_id') + #: The ID of the owner, or project, of the image. (backwards compat) + owner_id = resource.Body('owner', alias='owner') #: Properties, if any, that are associated with the image. properties = resource.Body('properties') #: The size of the image data, in bytes. diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index 3e0f223d2..a502e811f 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -88,7 +88,9 @@ class Image(resource.Resource, resource.TagMixin, _download.DownloadMixin): #: The name of the image. name = resource.Body('name') #: The ID of the owner, or project, of the image. - owner_id = resource.Body('owner') + owner = resource.Body('owner', alias='owner_id') + #: The ID of the owner, or project, of the image. (backwards compat) + owner_id = resource.Body('owner', alias='owner') # TODO(mordred) This is not how this works in v2. I mean, it's how it # should work, but it's not. We need to fix properties. They work right # in shade, so we can draw some logic from there. diff --git a/openstack/tests/unit/image/v1/test_image.py b/openstack/tests/unit/image/v1/test_image.py index 133c4232e..d593e727d 100644 --- a/openstack/tests/unit/image/v1/test_image.py +++ b/openstack/tests/unit/image/v1/test_image.py @@ -61,6 +61,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['min_disk'], sot.min_disk) self.assertEqual(EXAMPLE['min_ram'], sot.min_ram) self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['owner'], sot.owner) self.assertEqual(EXAMPLE['owner'], sot.owner_id) self.assertEqual(EXAMPLE['properties'], sot.properties) self.assertTrue(sot.is_protected) diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 8d0cafed9..7bcf3f559 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -168,6 +168,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['disk_format'], sot.disk_format) self.assertEqual(EXAMPLE['min_disk'], sot.min_disk) self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['owner'], sot.owner) self.assertEqual(EXAMPLE['owner'], sot.owner_id) self.assertEqual(EXAMPLE['properties'], sot.properties) self.assertFalse(sot.is_protected) From 9b253307a7973c51e2df05bfa0e288ea53b11698 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 29 Mar 2020 10:23:20 -0500 Subject: [PATCH 2659/3836] Remove some unneeded things from test-requirements We don't need to depend directly on python-subunit. In python3 we don't need safe_hasattr. We don't use testrepository. In lower constraints, os-client-config and mox3 are transitive depends that we do not care about so don't need to track lower pins for them. futures and ipaddress were removed from requirements but not from lc. Finally, remove future from lc and from __future__ since we're python3 only. Change-Id: Ic22079f78f77221e83986e5a835457dbefc3ded7 --- examples/baremetal/provisioning.py | 2 -- lower-constraints.txt | 9 --------- openstack/tests/unit/config/base.py | 3 +-- openstack/tests/unit/config/test_config.py | 3 +-- test-requirements.txt | 3 --- 5 files changed, 2 insertions(+), 18 deletions(-) diff --git a/examples/baremetal/provisioning.py b/examples/baremetal/provisioning.py index 36ff49e13..7eb758051 100644 --- a/examples/baremetal/provisioning.py +++ b/examples/baremetal/provisioning.py @@ -14,8 +14,6 @@ Operations with the provision state in the Bare Metal service. """ -from __future__ import print_function - def manage_and_inspect_node(conn, uuid): node = conn.baremetal.find_node(uuid) diff --git a/lower-constraints.txt b/lower-constraints.txt index 1f491875d..e86336cce 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -5,11 +5,7 @@ ddt==1.0.1 decorator==4.4.1 doc8==0.8.0 dogpile.cache==0.6.5 -extras==1.0.0 fixtures==3.0.0 -future==0.16.0 -futures==3.0.0 -ipaddress==1.0.17 iso8601==0.1.11 jmespath==0.9.0 jsonpatch==1.16 @@ -18,10 +14,8 @@ jsonschema==2.6.0 keystoneauth1==3.18.0 linecache2==1.0.0 mock==3.0.0 -mox3==0.20.0 munch==2.1.0 netifaces==0.10.4 -os-client-config==1.28.0 os-service-types==1.7.0 oslo.config==6.1.0 oslotest==3.2.0 @@ -37,9 +31,6 @@ requestsexceptions==1.2.0 six==1.10.0 statsd==3.3.0 stestr==1.0.0 -stevedore==1.20.0 -testrepository==0.0.18 testscenarios==0.4 testtools==2.2.0 traceback2==1.4.0 -unittest2==1.1.0 diff --git a/openstack/tests/unit/config/base.py b/openstack/tests/unit/config/base.py index 93dda054e..471cb5a6b 100644 --- a/openstack/tests/unit/config/base.py +++ b/openstack/tests/unit/config/base.py @@ -19,7 +19,6 @@ import os import tempfile -import extras import fixtures import yaml @@ -226,7 +225,7 @@ def setUp(self): def _assert_cloud_details(self, cc): self.assertIsInstance(cc, cloud_region.CloudRegion) - self.assertTrue(extras.safe_hasattr(cc, 'auth')) + self.assertTrue(hasattr(cc, 'auth')) self.assertIsInstance(cc.auth, dict) self.assertIsNone(cc.cloud) self.assertIn('username', cc.auth) diff --git a/openstack/tests/unit/config/test_config.py b/openstack/tests/unit/config/test_config.py index a3d35ff68..e0fb827cc 100644 --- a/openstack/tests/unit/config/test_config.py +++ b/openstack/tests/unit/config/test_config.py @@ -16,7 +16,6 @@ import copy import os -import extras import fixtures import testtools import yaml @@ -765,7 +764,7 @@ def test_get_one_no_yaml(self): # Not using assert_cloud_details because of cache settings which # are not present without the file self.assertIsInstance(cc, cloud_region.CloudRegion) - self.assertTrue(extras.safe_hasattr(cc, 'auth')) + self.assertTrue(hasattr(cc, 'auth')) self.assertIsInstance(cc.auth, dict) self.assertIsNone(cc.cloud) self.assertIn('username', cc.auth) diff --git a/test-requirements.txt b/test-requirements.txt index ad440b8cc..0544deede 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,18 +5,15 @@ hacking>=2.0,<2.1.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 ddt>=1.0.1 # MIT -extras>=1.0.0 # MIT fixtures>=3.0.0 # Apache-2.0/BSD jsonschema>=2.6.0 # MIT mock>=3.0.0 # BSD prometheus-client>=0.4.2 # Apache-2.0 -python-subunit>=1.0.0 # Apache-2.0/BSD oslo.config>=6.1.0 # Apache-2.0 oslotest>=3.2.0 # Apache-2.0 requests-mock>=1.2.0 # Apache-2.0 statsd>=3.3.0 stestr>=1.0.0 # Apache-2.0 -testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=2.2.0 # MIT doc8>=0.8.0 # Apache-2.0 From fe00460b9917c96cec58a792a8a77854fc64f7f4 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 29 Mar 2020 10:36:09 -0500 Subject: [PATCH 2660/3836] Remove uses of from six.moves We're python3 now. While we're at it, fix six.StringIO to io.StringIO (it matched the from six grep) and reorder imports where we're touching them anyway. Change-Id: I02c348d497bac4cb671ce1d9e8d11274b0ee8573 --- openstack/cloud/_object_store.py | 9 ++++----- openstack/config/cloud_region.py | 2 +- openstack/config/vendors/__init__.py | 2 +- openstack/key_manager/v1/_format.py | 2 +- openstack/object_store/v1/_proxy.py | 2 +- openstack/object_store/v1/info.py | 4 ++-- openstack/orchestration/util/template_utils.py | 4 ++-- openstack/orchestration/util/utils.py | 7 +++---- openstack/orchestration/v1/template.py | 2 +- openstack/proxy.py | 2 +- openstack/tests/base.py | 2 +- openstack/tests/unit/base.py | 4 ++-- tools/keystone_version.py | 5 +++-- 13 files changed, 23 insertions(+), 24 deletions(-) diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index c781e16c9..736d22b4d 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -20,8 +20,7 @@ import os import six import types # noqa - -from six.moves import urllib_parse +import urllib.parse import keystoneauth1.exceptions @@ -242,7 +241,7 @@ def get_object_capabilities(self): # The endpoint in the catalog has version and project-id in it # To get capabilities, we have to disassemble and reassemble the URL # This logic is taken from swiftclient - endpoint = urllib_parse.urlparse(self.object_store.get_endpoint()) + endpoint = urllib.parse.urlparse(self.object_store.get_endpoint()) url = "{scheme}://{netloc}/info".format( scheme=endpoint.scheme, netloc=endpoint.netloc) @@ -753,11 +752,11 @@ def get_object_raw(self, container, obj, query_string=None, stream=False): return self._object_store_client.get(endpoint, stream=stream) def _get_object_endpoint(self, container, obj=None, query_string=None): - endpoint = urllib_parse.quote(container) + endpoint = urllib.parse.quote(container) if obj: endpoint = '{endpoint}/{object}'.format( endpoint=endpoint, - object=urllib_parse.quote(obj) + object=urllib.parse.quote(obj) ) if query_string: endpoint = '{endpoint}?{query_string}'.format( diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 96a126381..41bcc9d5d 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -14,6 +14,7 @@ import copy import warnings +import urllib from keystoneauth1 import discover import keystoneauth1.exceptions.catalog @@ -21,7 +22,6 @@ from keystoneauth1 import session as ks_session import os_service_types import requestsexceptions -from six.moves import urllib try: import statsd except ImportError: diff --git a/openstack/config/vendors/__init__.py b/openstack/config/vendors/__init__.py index 41badf54b..0c0bbf80a 100644 --- a/openstack/config/vendors/__init__.py +++ b/openstack/config/vendors/__init__.py @@ -15,8 +15,8 @@ import glob import json import os +import urllib -from six.moves import urllib import requests import yaml diff --git a/openstack/key_manager/v1/_format.py b/openstack/key_manager/v1/_format.py index 34698a389..4ff41efde 100644 --- a/openstack/key_manager/v1/_format.py +++ b/openstack/key_manager/v1/_format.py @@ -12,7 +12,7 @@ from openstack import format -from six.moves.urllib import parse +from urllib import parse class HREFToUUID(format.Formatter): diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index eba059e6a..6a0b00dd3 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -17,9 +17,9 @@ import json import os import time +from urllib import parse import six -from six.moves.urllib import parse from openstack.object_store.v1 import account as _account from openstack.object_store.v1 import container as _container diff --git a/openstack/object_store/v1/info.py b/openstack/object_store/v1/info.py index f5fbde39b..3948b2140 100644 --- a/openstack/object_store/v1/info.py +++ b/openstack/object_store/v1/info.py @@ -11,11 +11,11 @@ # License for the specific language governing permissions and limitations # under the License. +import urllib + from openstack import exceptions from openstack import resource -from six.moves import urllib - class Info(resource.Resource): diff --git a/openstack/orchestration/util/template_utils.py b/openstack/orchestration/util/template_utils.py index 8b0070c0d..7b0ee6233 100644 --- a/openstack/orchestration/util/template_utils.py +++ b/openstack/orchestration/util/template_utils.py @@ -15,8 +15,8 @@ import collections import json import six -from six.moves.urllib import parse -from six.moves.urllib import request +from urllib import parse +from urllib import request from openstack.orchestration.util import environment_format from openstack.orchestration.util import template_format diff --git a/openstack/orchestration/util/utils.py b/openstack/orchestration/util/utils.py index 89a04b8eb..2c2b3d610 100644 --- a/openstack/orchestration/util/utils.py +++ b/openstack/orchestration/util/utils.py @@ -15,10 +15,9 @@ import base64 import os - -from six.moves.urllib import error -from six.moves.urllib import parse -from six.moves.urllib import request +from urllib import error +from urllib import parse +from urllib import request from openstack import exceptions diff --git a/openstack/orchestration/v1/template.py b/openstack/orchestration/v1/template.py index ab0bc5655..818e73992 100644 --- a/openstack/orchestration/v1/template.py +++ b/openstack/orchestration/v1/template.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from six.moves.urllib import parse +from urllib import parse from openstack import resource diff --git a/openstack/proxy.py b/openstack/proxy.py index 6c1456ab9..dbb246399 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -15,7 +15,7 @@ JSONDecodeError = simplejson.scanner.JSONDecodeError except ImportError: JSONDecodeError = ValueError -from six.moves import urllib +import urllib from keystoneauth1 import adapter diff --git a/openstack/tests/base.py b/openstack/tests/base.py index 6f381a677..5ee5ae688 100644 --- a/openstack/tests/base.py +++ b/openstack/tests/base.py @@ -17,11 +17,11 @@ import sys import fixtures +from io import StringIO import logging import munch from oslotest import base import pprint -from six import StringIO import testtools.content _TRUE_VALUES = ('true', '1', 'yes') diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index a4db4be69..d0eef6ed7 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -15,8 +15,10 @@ import collections import os +import tempfile import time import uuid +import urllib import fixtures from keystoneauth1 import loading as ks_loading @@ -24,8 +26,6 @@ from oslo_config import cfg from requests import structures from requests_mock.contrib import fixture as rm_fixture -from six.moves import urllib -import tempfile import openstack.cloud import openstack.connection diff --git a/tools/keystone_version.py b/tools/keystone_version.py index 663e6c878..df8fadcd2 100644 --- a/tools/keystone_version.py +++ b/tools/keystone_version.py @@ -13,10 +13,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import openstack.config import pprint import sys -from six.moves.urllib import parse as urlparse +from urllib import parse as urlparse + +import openstack.config def print_versions(r): From 2ddbf57ee6e556b0f938600be7bbf06c3a4e66ba Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 29 Mar 2020 10:43:43 -0500 Subject: [PATCH 2661/3836] Remove use of six We're python3 now, we don't need to use six. Change-Id: I7c1f0bc89838432aa0ce916beb462916c8763cd1 --- lower-constraints.txt | 1 - openstack/baremetal/configdrive.py | 6 ++---- openstack/block_storage/_base_proxy.py | 4 +--- openstack/cloud/_compute.py | 7 +++---- openstack/cloud/_floating_ip.py | 3 +-- openstack/cloud/_network.py | 3 +-- openstack/cloud/_normalize.py | 3 +-- openstack/cloud/_object_store.py | 8 ++------ openstack/cloud/_utils.py | 10 +--------- openstack/cloud/meta.py | 3 +-- openstack/cloud/openstackcloud.py | 3 +-- openstack/compute/v2/metadata.py | 3 +-- openstack/connection.py | 3 +-- openstack/dns/v2/_base.py | 9 ++++----- openstack/exceptions.py | 5 ++--- openstack/image/_base_proxy.py | 3 +-- openstack/image/_download.py | 6 +----- openstack/object_store/v1/_proxy.py | 18 ++++++------------ .../orchestration/util/template_utils.py | 7 +++---- openstack/resource.py | 11 +++++------ .../tests/functional/cloud/test_compute.py | 5 ++--- .../functional/cloud/test_floating_ip.py | 4 +--- .../functional/compute/v2/test_extension.py | 7 +++---- .../functional/compute/v2/test_flavor.py | 5 ++--- .../tests/functional/compute/v2/test_image.py | 3 +-- .../network/v2/test_availability_zone.py | 7 +++---- .../functional/network/v2/test_extension.py | 5 ++--- .../network/v2/test_qos_rule_type.py | 3 +-- .../tests/unit/baremetal/test_configdrive.py | 5 ++--- openstack/tests/unit/cloud/test_image.py | 7 +++---- .../tests/unit/compute/v2/test_server.py | 3 +-- openstack/tests/unit/image/v2/test_image.py | 4 ++-- .../tests/unit/object_store/v1/test_proxy.py | 19 +++++++++---------- .../tests/unit/orchestration/v1/test_proxy.py | 5 ++--- .../tests/unit/orchestration/v1/test_stack.py | 6 +++--- openstack/tests/unit/test_resource.py | 3 +-- openstack/utils.py | 6 ++---- requirements.txt | 1 - 38 files changed, 78 insertions(+), 136 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index e86336cce..dd0e02353 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -28,7 +28,6 @@ PyYAML==3.12 requests==2.18.0 requests-mock==1.2.0 requestsexceptions==1.2.0 -six==1.10.0 statsd==3.3.0 stestr==1.0.0 testscenarios==0.4 diff --git a/openstack/baremetal/configdrive.py b/openstack/baremetal/configdrive.py index f189d5e6b..e7e9e8892 100644 --- a/openstack/baremetal/configdrive.py +++ b/openstack/baremetal/configdrive.py @@ -21,8 +21,6 @@ import subprocess import tempfile -import six - @contextlib.contextmanager def populate_directory(metadata, user_data=None, versions=None, @@ -60,7 +58,7 @@ def populate_directory(metadata, user_data=None, versions=None, if user_data: # Strictly speaking, user data is binary, but in many cases # it's actually a text (cloud-init, ignition, etc). - flag = 't' if isinstance(user_data, six.text_type) else 'b' + flag = 't' if isinstance(user_data, str) else 'b' with open(os.path.join(subdir, 'user_data'), 'w%s' % flag) as fp: fp.write(user_data) @@ -142,7 +140,7 @@ def pack(path): # NOTE(dtantsur): Ironic expects configdrive to be a string, but base64 # returns bytes on Python 3. - if not isinstance(cd, six.string_types): + if not isinstance(cd, str): cd = cd.decode('utf-8') return cd diff --git a/openstack/block_storage/_base_proxy.py b/openstack/block_storage/_base_proxy.py index 97b8b4b9c..b1fb4c056 100644 --- a/openstack/block_storage/_base_proxy.py +++ b/openstack/block_storage/_base_proxy.py @@ -11,13 +11,11 @@ # under the License. import abc -import six - from openstack import exceptions from openstack import proxy -class BaseBlockStorageProxy(six.with_metaclass(abc.ABCMeta, proxy.Proxy)): +class BaseBlockStorageProxy(proxy.Proxy, metaclass=abc.ABCMeta): def create_image( self, name, volume, allow_duplicates, diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 5b09d90a1..f7d069b58 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -18,7 +18,6 @@ import functools import iso8601 import operator -import six import threading import time import types # noqa @@ -1634,7 +1633,7 @@ def delete_aggregate(self, name_or_id): :raises: OpenStackCloudException on operation error. """ if ( - isinstance(name_or_id, six.string_types + (six.binary_type,)) + isinstance(name_or_id, (str, bytes)) and not name_or_id.isdigit() ): aggregate = self.get_aggregate(name_or_id) @@ -1828,9 +1827,9 @@ def _encode_server_userdata(self, userdata): if hasattr(userdata, 'read'): userdata = userdata.read() - if not isinstance(userdata, six.binary_type): + if not isinstance(userdata, bytes): # If the userdata passed in is bytes, just send it unmodified - if not isinstance(userdata, six.string_types): + if not isinstance(userdata, str): raise TypeError("%s can't be encoded" % type(userdata)) # If it's not bytes, make it bytes userdata = userdata.encode('utf-8', 'strict') diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index 98068ad9b..1f2a2b3a7 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -16,7 +16,6 @@ import ipaddress # import jsonpatch import threading -import six import time import types # noqa @@ -262,7 +261,7 @@ def _neutron_available_floating_ips( project_id = self.current_project_id if network: - if isinstance(network, six.string_types): + if isinstance(network, str): network = [network] # Use given list to get first matching external network diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index 389d3e4c0..ab986672a 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -13,7 +13,6 @@ # import types so that we can reference ListType in sphinx param declarations. # We can't just use list, because sphinx gets confused by # openstack.resource.Resource.list and openstack.resource2.Resource.list -import six import time import threading import types # noqa @@ -2205,7 +2204,7 @@ def create_subnet(self, network_name_or_id, cidr=None, ip_version=4, 'True') # Be friendly on ip_version and allow strings - if isinstance(ip_version, six.string_types): + if isinstance(ip_version, str): try: ip_version = int(ip_version) except ValueError: diff --git a/openstack/cloud/_normalize.py b/openstack/cloud/_normalize.py index c5a8d069d..97a27fa0b 100644 --- a/openstack/cloud/_normalize.py +++ b/openstack/cloud/_normalize.py @@ -18,7 +18,6 @@ import datetime import munch -import six from openstack import resource @@ -117,7 +116,7 @@ def _split_filters(obj_name='', filters=None, **kwargs): def _to_bool(value): - if isinstance(value, six.string_types): + if isinstance(value, str): if not value: return False prospective = value.lower().capitalize() diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index 736d22b4d..ed3f88c28 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -18,7 +18,6 @@ import hashlib import json import os -import six import types # noqa import urllib.parse @@ -256,9 +255,6 @@ def get_object_segment_size(self, segment_size): caps = self.get_object_capabilities() except exc.OpenStackCloudHTTPError as e: if e.response.status_code in (404, 412): - # Clear the exception so that it doesn't linger - # and get reported as an Inner Exception later - _utils._exc_clear() server_max_file_size = DEFAULT_MAX_FILE_SIZE self.log.info( "Swift capabilities not supported. " @@ -819,14 +815,14 @@ def get_object(self, container, obj, query_string=None, response_headers = { k.lower(): v for k, v in response.headers.items()} if outfile: - if isinstance(outfile, six.string_types): + if isinstance(outfile, str): outfile_handle = open(outfile, 'wb') else: outfile_handle = outfile for chunk in response.iter_content( resp_chunk_size, decode_unicode=False): outfile_handle.write(chunk) - if isinstance(outfile, six.string_types): + if isinstance(outfile, str): outfile_handle.close() else: outfile_handle.flush() diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index 4979774b6..9a6a8cf0b 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -20,9 +20,7 @@ import munch import netifaces import re -import six import sre_constants -import sys import time import uuid @@ -35,12 +33,6 @@ _decorated_methods = [] -def _exc_clear(): - """Because sys.exc_clear is gone in py3 and is not in six.""" - if sys.version_info[0] == 2: - sys.exc_clear() - - def _make_unicode(input): """Turn an input into unicode unconditionally @@ -134,7 +126,7 @@ def _filter_list(data, name_or_id, filters): if not filters: return data - if isinstance(filters, six.string_types): + if isinstance(filters, str): return jmespath.search(filters, data) def _dict_filter(f, d): diff --git a/openstack/cloud/meta.py b/openstack/cloud/meta.py index a651d0365..874c69a6e 100644 --- a/openstack/cloud/meta.py +++ b/openstack/cloud/meta.py @@ -15,7 +15,6 @@ import munch import ipaddress -import six import socket from openstack import _log @@ -23,7 +22,7 @@ from openstack.cloud import exc -NON_CALLABLES = (six.string_types, bool, dict, int, float, list, type(None)) +NON_CALLABLES = (str, bool, dict, int, float, list, type(None)) def find_nova_interfaces(addresses, ext_tag=None, key_name=None, version=4, diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index ba37a0824..2e821238a 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -12,7 +12,6 @@ import copy import functools import queue -import six # import types so that we can reference ListType in sphinx param declarations. # We can't just use list, because sphinx gets confused by # openstack.resource.Resource.list and openstack.resource2.Resource.list @@ -351,7 +350,7 @@ def _get_cache(self, resource_name): def _get_major_version_id(self, version): if isinstance(version, int): return version - elif isinstance(version, six.string_types + (tuple,)): + elif isinstance(version, (str, tuple)): return int(version[0]) return version diff --git a/openstack/compute/v2/metadata.py b/openstack/compute/v2/metadata.py index b1f554d1e..906dc7213 100644 --- a/openstack/compute/v2/metadata.py +++ b/openstack/compute/v2/metadata.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import six from openstack import exceptions from openstack import utils @@ -22,7 +21,7 @@ def _metadata(self, method, key=None, clear=False, delete=False, metadata=None): metadata = metadata or {} for k, v in metadata.items(): - if not isinstance(v, six.string_types): + if not isinstance(v, str): raise ValueError("The value for %s (%s) must be " "a text string" % (k, v)) diff --git a/openstack/connection.py b/openstack/connection.py index 1da50c601..7643795ad 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -182,7 +182,6 @@ import concurrent.futures import keystoneauth1.exceptions import requestsexceptions -import six from openstack import _log from openstack import _services_mixin @@ -465,7 +464,7 @@ class contained in """ # If we don't have a proxy, just instantiate Proxy so that # we get an adapter. - if isinstance(service, six.string_types): + if isinstance(service, str): service = service_description.ServiceDescription(service) # Directly invoke descriptor of the ServiceDescription diff --git a/openstack/dns/v2/_base.py b/openstack/dns/v2/_base.py index e2e3fa695..ae930853b 100644 --- a/openstack/dns/v2/_base.py +++ b/openstack/dns/v2/_base.py @@ -9,7 +9,7 @@ # 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 urllib.parse from openstack import exceptions from openstack import resource @@ -89,11 +89,10 @@ def _get_next_link(cls, uri, response, data, marker, limit, total_yielded): # This prevents duplication of query parameters that with large # number of pages result in HTTP 414 error eventually. if next_link: - parts = six.moves.urllib.parse.urlparse(next_link) - query_params = six.moves.urllib.parse.parse_qs(parts.query) + parts = urllib.parse.urlparse(next_link) + query_params = urllib.parse.parse_qs(parts.query) params.update(query_params) - next_link = six.moves.urllib.parse.urljoin(next_link, - parts.path) + next_link = urllib.parse.urljoin(next_link, parts.path) # If we still have no link, and limit was given and is non-zero, # and the number of records yielded equals the limit, then the user diff --git a/openstack/exceptions.py b/openstack/exceptions.py index a69d97729..f449e86dd 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -20,7 +20,6 @@ import re from requests import exceptions as _rex -import six class SDKException(Exception): @@ -101,7 +100,7 @@ def __unicode__(self): if self.details: remote_error += ', ' if self.details: - remote_error += six.text_type(self.details) + remote_error += str(self.details) return "{message}: {remote_error}".format( message=super(HttpException, self).__str__(), @@ -176,7 +175,7 @@ def _extract_message(obj): # Ironic starting with Stein elif obj.get('faultstring'): return obj['faultstring'] - elif isinstance(obj, six.string_types): + elif isinstance(obj, str): # Ironic before Stein has double JSON encoding, nobody remembers why. try: obj = json.loads(obj) diff --git a/openstack/image/_base_proxy.py b/openstack/image/_base_proxy.py index 85055f95c..26f76c898 100644 --- a/openstack/image/_base_proxy.py +++ b/openstack/image/_base_proxy.py @@ -12,13 +12,12 @@ import abc import os -import six from openstack import exceptions from openstack import proxy -class BaseImageProxy(six.with_metaclass(abc.ABCMeta, proxy.Proxy)): +class BaseImageProxy(proxy.Proxy, metaclass=abc.ABCMeta): retriable_status_codes = [503] diff --git a/openstack/image/_download.py b/openstack/image/_download.py index d9176d24b..0c50bffb2 100644 --- a/openstack/image/_download.py +++ b/openstack/image/_download.py @@ -11,7 +11,6 @@ # under the License. import io import hashlib -import six from openstack import exceptions from openstack import utils @@ -49,10 +48,7 @@ def download(self, session, stream=False, output=None, chunk_size=1024): md5 = hashlib.md5() if output: try: - # In python 2 we might get StringIO - delete it as soon as - # py2 support is dropped - if isinstance(output, io.IOBase) \ - or isinstance(output, six.StringIO): + if isinstance(output, io.IOBase): for chunk in resp.iter_content(chunk_size=chunk_size): output.write(chunk) md5.update(chunk) diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index 6a0b00dd3..9e03becfc 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -19,8 +19,6 @@ import time from urllib import parse -import six - from openstack.object_store.v1 import account as _account from openstack.object_store.v1 import container as _container from openstack.object_store.v1 import obj as _obj @@ -637,9 +635,6 @@ def get_object_segment_size(self, segment_size): caps = self.get_info() except exceptions.SDKException as e: if e.response.status_code in (404, 412): - # Clear the exception so that it doesn't linger - # and get reported as an Inner Exception later - _utils._exc_clear() server_max_file_size = DEFAULT_MAX_FILE_SIZE self._connection.log.info( "Swift capabilities not supported. " @@ -730,13 +725,13 @@ def get_temp_url_key(self, container=None): account_meta = self.get_account_metadata() temp_url_key = (account_meta.meta_temp_url_key_2 or account_meta.meta_temp_url_key) - if temp_url_key and not isinstance(temp_url_key, six.binary_type): + if temp_url_key and not isinstance(temp_url_key, bytes): temp_url_key = temp_url_key.encode('utf8') return temp_url_key def _check_temp_url_key(self, container=None, temp_url_key=None): if temp_url_key: - if not isinstance(temp_url_key, six.binary_type): + if not isinstance(temp_url_key, bytes): temp_url_key = temp_url_key.encode('utf8') else: temp_url_key = self.get_temp_url_key(container) @@ -792,8 +787,7 @@ def generate_form_signature( data = '%s\n%s\n%s\n%s\n%s' % (path, redirect_url, max_file_size, max_upload_count, expires) - if six.PY3: - data = data.encode('utf8') + data = data.encode('utf8') sig = hmac.new(temp_url_key, data, sha1).hexdigest() return (expires, sig) @@ -864,7 +858,7 @@ def generate_temp_url( raise ValueError('time must either be a whole number ' 'or in specific ISO 8601 format.') - if isinstance(path, six.binary_type): + if isinstance(path, bytes): try: path_for_body = path.decode('utf-8') except UnicodeDecodeError: @@ -895,7 +889,7 @@ def generate_temp_url( ('prefix:' if prefix else '') + path_for_body] if ip_range: - if isinstance(ip_range, six.binary_type): + if isinstance(ip_range, bytes): try: ip_range = ip_range.decode('utf-8') except UnicodeDecodeError: @@ -924,7 +918,7 @@ def generate_temp_url( if prefix: temp_url += u'&temp_url_prefix={}'.format(parts[4]) # Have return type match path from caller - if isinstance(path, six.binary_type): + if isinstance(path, bytes): return temp_url.encode('utf-8') else: return temp_url diff --git a/openstack/orchestration/util/template_utils.py b/openstack/orchestration/util/template_utils.py index 7b0ee6233..d48a2867c 100644 --- a/openstack/orchestration/util/template_utils.py +++ b/openstack/orchestration/util/template_utils.py @@ -14,7 +14,6 @@ import collections import json -import six from urllib import parse from urllib import request @@ -55,7 +54,7 @@ def get_template_contents(template_file=None, template_url=None, 'Could not fetch template from %s' % template_url) try: - if isinstance(tpl, six.binary_type): + if isinstance(tpl, bytes): tpl = tpl.decode('utf-8') template = template_format.parse(tpl) except ValueError as e: @@ -77,7 +76,7 @@ def resolve_template_get_files(template, files, template_base_url, def ignore_if(key, value): if key != 'get_file' and key != 'type': return True - if not isinstance(value, six.string_types): + if not isinstance(value, str): return True if (key == 'type' and not value.endswith(('.yaml', '.template'))): @@ -93,7 +92,7 @@ def recurse_if(value): def is_template(file_content): try: - if isinstance(file_content, six.binary_type): + if isinstance(file_content, bytes): file_content = file_content.decode('utf-8') template_format.parse(file_content) except (ValueError, TypeError): diff --git a/openstack/resource.py b/openstack/resource.py index f2a9ba598..cf9d9a476 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -34,6 +34,7 @@ class that represent a remote resource. The attributes that import collections import inspect import itertools +import urllib.parse import jsonpatch import operator @@ -41,7 +42,6 @@ class that represent a remote resource. The attributes that from keystoneauth1 import discover import munch from requests import structures -import six from openstack import _log from openstack import exceptions @@ -1094,7 +1094,7 @@ def _prepare_request(self, requires_id=None, prepend_key=False, uri = utils.urljoin(uri, self.id) if params: - query_params = six.moves.urllib.parse.urlencode(params) + query_params = urllib.parse.urlencode(params) uri += '?' + query_params return _Request(uri, body, headers) @@ -1776,11 +1776,10 @@ def _get_next_link(cls, uri, response, data, marker, limit, total_yielded): # This prevents duplication of query parameters that with large # number of pages result in HTTP 414 error eventually. if next_link: - parts = six.moves.urllib.parse.urlparse(next_link) - query_params = six.moves.urllib.parse.parse_qs(parts.query) + parts = urllib.parse.urlparse(next_link) + query_params = urllib.parse.parse_qs(parts.query) params.update(query_params) - next_link = six.moves.urllib.parse.urljoin(next_link, - parts.path) + next_link = urllib.parse.urljoin(next_link, parts.path) # If we still have no link, and limit was given and is non-zero, # and the number of records yielded equals the limit, then the user diff --git a/openstack/tests/functional/cloud/test_compute.py b/openstack/tests/functional/cloud/test_compute.py index eb4cd78e3..e7408fc3f 100644 --- a/openstack/tests/functional/cloud/test_compute.py +++ b/openstack/tests/functional/cloud/test_compute.py @@ -20,7 +20,6 @@ import datetime from fixtures import TimeoutException -import six from openstack.cloud import exc from openstack.tests.functional import base @@ -200,7 +199,7 @@ def test_get_server_console(self): # returning a string tests that the call is correct. Testing that # the cloud returns actual data in the output is out of scope. log = self.user_cloud._get_server_console_output(server_id=server.id) - self.assertTrue(isinstance(log, six.string_types)) + self.assertTrue(isinstance(log, str)) def test_get_server_console_name_or_id(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) @@ -210,7 +209,7 @@ def test_get_server_console_name_or_id(self): flavor=self.flavor, wait=True) log = self.user_cloud.get_server_console(server=self.server_name) - self.assertTrue(isinstance(log, six.string_types)) + self.assertTrue(isinstance(log, str)) def test_list_availability_zone_names(self): self.assertEqual( diff --git a/openstack/tests/functional/cloud/test_floating_ip.py b/openstack/tests/functional/cloud/test_floating_ip.py index 89a0ed4cb..14e1b3a02 100644 --- a/openstack/tests/functional/cloud/test_floating_ip.py +++ b/openstack/tests/functional/cloud/test_floating_ip.py @@ -20,7 +20,6 @@ """ import pprint -import six import sys from testtools import content @@ -102,8 +101,7 @@ def _cleanup_network(self): content.text_content( '\n'.join([str(ex) for ex in exception_list]))) exc = exception_list[0] - tb = tb_list[0] - six.reraise(type(exc), exc, tb) + raise exc def _cleanup_servers(self): exception_list = list() diff --git a/openstack/tests/functional/compute/v2/test_extension.py b/openstack/tests/functional/compute/v2/test_extension.py index c60ead514..e4ff2a408 100644 --- a/openstack/tests/functional/compute/v2/test_extension.py +++ b/openstack/tests/functional/compute/v2/test_extension.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import six from openstack.tests.functional import base @@ -22,6 +21,6 @@ def test_list(self): self.assertGreater(len(extensions), 0) for ext in extensions: - self.assertIsInstance(ext.name, six.string_types) - self.assertIsInstance(ext.namespace, six.string_types) - self.assertIsInstance(ext.alias, six.string_types) + self.assertIsInstance(ext.name, str) + self.assertIsInstance(ext.namespace, str) + self.assertIsInstance(ext.alias, str) diff --git a/openstack/tests/functional/compute/v2/test_flavor.py b/openstack/tests/functional/compute/v2/test_flavor.py index 8785ae1b4..92d636a4b 100644 --- a/openstack/tests/functional/compute/v2/test_flavor.py +++ b/openstack/tests/functional/compute/v2/test_flavor.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import six from openstack import exceptions from openstack.tests.functional import base @@ -28,8 +27,8 @@ def test_flavors(self): self.assertGreater(len(flavors), 0) for flavor in flavors: - self.assertIsInstance(flavor.id, six.string_types) - self.assertIsInstance(flavor.name, six.string_types) + self.assertIsInstance(flavor.id, str) + self.assertIsInstance(flavor.name, str) self.assertIsInstance(flavor.disk, int) self.assertIsInstance(flavor.ram, int) self.assertIsInstance(flavor.vcpus, int) diff --git a/openstack/tests/functional/compute/v2/test_image.py b/openstack/tests/functional/compute/v2/test_image.py index d71f30d79..46b4c9cbf 100644 --- a/openstack/tests/functional/compute/v2/test_image.py +++ b/openstack/tests/functional/compute/v2/test_image.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import six from openstack.tests.functional import base from openstack.tests.functional.image.v2.test_image import TEST_IMAGE_NAME @@ -22,7 +21,7 @@ def test_images(self): images = list(self.conn.compute.images()) self.assertGreater(len(images), 0) for image in images: - self.assertIsInstance(image.id, six.string_types) + self.assertIsInstance(image.id, str) def _get_non_test_image(self): images = self.conn.compute.images() diff --git a/openstack/tests/functional/network/v2/test_availability_zone.py b/openstack/tests/functional/network/v2/test_availability_zone.py index 64fc1910b..4c4cc1438 100644 --- a/openstack/tests/functional/network/v2/test_availability_zone.py +++ b/openstack/tests/functional/network/v2/test_availability_zone.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import six from openstack.tests.functional import base @@ -22,6 +21,6 @@ def test_list(self): self.assertGreater(len(availability_zones), 0) for az in availability_zones: - self.assertIsInstance(az.name, six.string_types) - self.assertIsInstance(az.resource, six.string_types) - self.assertIsInstance(az.state, six.string_types) + self.assertIsInstance(az.name, str) + self.assertIsInstance(az.resource, str) + self.assertIsInstance(az.state, str) diff --git a/openstack/tests/functional/network/v2/test_extension.py b/openstack/tests/functional/network/v2/test_extension.py index 98134cb6e..449f0d4e1 100644 --- a/openstack/tests/functional/network/v2/test_extension.py +++ b/openstack/tests/functional/network/v2/test_extension.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import six from openstack.tests.functional import base @@ -22,8 +21,8 @@ def test_list(self): self.assertGreater(len(extensions), 0) for ext in extensions: - self.assertIsInstance(ext.name, six.string_types) - self.assertIsInstance(ext.alias, six.string_types) + self.assertIsInstance(ext.name, str) + self.assertIsInstance(ext.alias, str) def test_find(self): extension = self.conn.network.find_extension('external-net') diff --git a/openstack/tests/functional/network/v2/test_qos_rule_type.py b/openstack/tests/functional/network/v2/test_qos_rule_type.py index d60907851..b599b2063 100644 --- a/openstack/tests/functional/network/v2/test_qos_rule_type.py +++ b/openstack/tests/functional/network/v2/test_qos_rule_type.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import six from openstack.tests.functional import base @@ -34,4 +33,4 @@ def test_list(self): self.assertGreater(len(rule_types), 0) for rule_type in rule_types: - self.assertIsInstance(rule_type.type, six.string_types) + self.assertIsInstance(rule_type.type, str) diff --git a/openstack/tests/unit/baremetal/test_configdrive.py b/openstack/tests/unit/baremetal/test_configdrive.py index 9b17963a4..a25eaa258 100644 --- a/openstack/tests/unit/baremetal/test_configdrive.py +++ b/openstack/tests/unit/baremetal/test_configdrive.py @@ -17,7 +17,6 @@ import os import mock -import six import testtools from openstack.baremetal import configdrive @@ -58,7 +57,7 @@ def _check(self, metadata, user_data=None, network_data=None, if user_data is None: self.assertFalse(os.path.exists(user_data_file)) else: - if isinstance(user_data, six.text_type): + if isinstance(user_data, str): user_data = user_data.encode() with open(user_data_file, 'rb') as fp: self.assertEqual(user_data, fp.read()) @@ -101,4 +100,4 @@ def test_success(self, mock_popen): mock_popen.return_value.returncode = 0 result = configdrive.pack("/fake") # Make sure the result is string on all python versions - self.assertIsInstance(result, six.string_types) + self.assertIsInstance(result, str) diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index e2c280ac0..e68a6cb57 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -11,12 +11,11 @@ # 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 io import operator import tempfile import uuid -import six - from openstack import exceptions from openstack.cloud import exc from openstack.cloud import meta @@ -75,7 +74,7 @@ def test_download_image_no_output(self): self.cloud.download_image, self.image_name) def test_download_image_two_outputs(self): - fake_fd = six.BytesIO() + fake_fd = io.BytesIO() self.assertRaises(exc.OpenStackCloudException, self.cloud.download_image, self.image_name, output_path='fake_path', output_file=fake_fd) @@ -120,7 +119,7 @@ def _register_image_mocks(self): def test_download_image_with_fd(self): self._register_image_mocks() - output_file = six.BytesIO() + output_file = io.BytesIO() self.cloud.download_image(self.image_name, output_file=output_file) output_file.seek(0) self.assertEqual(output_file.read(), self.output) diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index d9445a89a..6baf3c45c 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -11,7 +11,6 @@ # under the License. import mock -import six from openstack.tests.unit import base from openstack.image.v2 import image @@ -818,7 +817,7 @@ class FakeEndpointData(object): self.sess, host='HOST2', force=False, block_migration=False) self.assertIn( "Live migration on this cloud implies 'force'", - six.text_type(ex)) + str(ex)) def test_live_migrate_no_microversion_force_true(self): sot = server.Server(**EXAMPLE) diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 8d0cafed9..4bc95baf6 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -10,8 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. import hashlib +import io import operator -import six import tempfile from keystoneauth1 import adapter @@ -403,7 +403,7 @@ def test_download_stream(self): self.assertEqual(rv, resp) def test_image_download_output_fd(self): - output_file = six.BytesIO() + output_file = io.BytesIO() sot = image.Image(**EXAMPLE) response = mock.Mock() response.status_code = 200 diff --git a/openstack/tests/unit/object_store/v1/test_proxy.py b/openstack/tests/unit/object_store/v1/test_proxy.py index f3c10724d..ef4786606 100644 --- a/openstack/tests/unit/object_store/v1/test_proxy.py +++ b/openstack/tests/unit/object_store/v1/test_proxy.py @@ -14,7 +14,6 @@ from hashlib import sha1 import mock import random -import six import string import tempfile import time @@ -281,7 +280,7 @@ def test_generate_temp_url(self, time_mock, hmac_mock): url = self.proxy.generate_temp_url( self.url, self.seconds, self.method, temp_url_key=self.key) key = self.key - if not isinstance(key, six.binary_type): + if not isinstance(key, bytes): key = key.encode('utf-8') self.assertEqual(url, self.expected_url) self.assertEqual(hmac_mock.mock_calls, [ @@ -309,10 +308,10 @@ def test_generate_temp_url_ip_range(self, time_mock, hmac_mock): path, self.seconds, self.method, temp_url_key=self.key, ip_range=ip_range) key = self.key - if not isinstance(key, six.binary_type): + if not isinstance(key, bytes): key = key.encode('utf-8') - if isinstance(ip_range, six.binary_type): + if isinstance(ip_range, bytes): ip_range_expected_url = ( expected_url + ip_range.decode('utf-8') ) @@ -357,7 +356,7 @@ def test_generate_temp_url_iso8601_argument(self, hmac_mock): lt = time.localtime() expires = time.strftime(self.expires_iso8601_format[:-1], lt) - if not isinstance(self.expected_url, six.string_types): + if not isinstance(self.expected_url, str): expected_url = self.expected_url.replace( b'1400003600', bytes(str(int(time.mktime(lt))), encoding='ascii')) @@ -372,7 +371,7 @@ def test_generate_temp_url_iso8601_argument(self, hmac_mock): expires = time.strftime(self.short_expires_iso8601_format, lt) lt = time.strptime(expires, self.short_expires_iso8601_format) - if not isinstance(self.expected_url, six.string_types): + if not isinstance(self.expected_url, str): expected_url = self.expected_url.replace( b'1400003600', bytes(str(int(time.mktime(lt))), encoding='ascii')) @@ -393,12 +392,12 @@ def test_generate_temp_url_iso8601_output(self, time_mock, hmac_mock): temp_url_key=self.key, iso8601=True) key = self.key - if not isinstance(key, six.binary_type): + if not isinstance(key, bytes): key = key.encode('utf-8') expires = time.strftime(self.expires_iso8601_format, time.gmtime(1400003600)) - if not isinstance(self.url, six.string_types): + if not isinstance(self.url, str): self.assertTrue(url.endswith(bytes(expires, 'utf-8'))) else: self.assertTrue(url.endswith(expires)) @@ -429,7 +428,7 @@ def test_generate_temp_url_prefix(self, time_mock, hmac_mock): path, self.seconds, self.method, prefix=True, temp_url_key=self.key) key = self.key - if not isinstance(key, six.binary_type): + if not isinstance(key, bytes): key = key.encode('utf-8') self.assertEqual(url, expected_url) self.assertEqual(hmac_mock.mock_calls, [ @@ -448,7 +447,7 @@ def test_generate_temp_url_invalid_path(self): @mock.patch('hmac.HMAC.hexdigest', return_value="temp_url_signature") def test_generate_absolute_expiry_temp_url(self, hmac_mock): - if isinstance(self.expected_url, six.binary_type): + if isinstance(self.expected_url, bytes): expected_url = self.expected_url.replace( b'1400003600', b'2146636800') else: diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index 140baeeb6..b4be71482 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -12,7 +12,6 @@ from testscenarios import load_tests_apply_scenarios as load_tests # noqa import mock -import six from openstack import exceptions from openstack.orchestration.v1 import _proxy @@ -253,7 +252,7 @@ def test_resources_stack_not_found(self, mock_list, mock_find): ex = self.assertRaises(exceptions.ResourceNotFound, self.proxy.resources, stack_name) - self.assertEqual('No stack found for test_stack', six.text_type(ex)) + self.assertEqual('No stack found for test_stack', str(ex)) def test_create_software_config(self): self.verify_create(self.proxy.create_software_config, @@ -320,7 +319,7 @@ def test_validate_template_invalid_request(self): self.proxy.validate_template, None, template_url=None) self.assertEqual("'template_url' must be specified when template is " - "None", six.text_type(err)) + "None", str(err)) class TestExtractName(TestOrchestrationProxy): diff --git a/openstack/tests/unit/orchestration/v1/test_stack.py b/openstack/tests/unit/orchestration/v1/test_stack.py index 38c8a412d..7cbc2fa7b 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack.py +++ b/openstack/tests/unit/orchestration/v1/test_stack.py @@ -11,7 +11,7 @@ # under the License. import mock -import six + from openstack.tests.unit import base from openstack.tests.unit import test_resource @@ -223,10 +223,10 @@ def test_fetch(self): 'stacks/{id}?resolve_outputs=False'.format(id=sot.id), microversion=None) ex = self.assertRaises(exceptions.ResourceNotFound, sot.fetch, sess) - self.assertEqual('oops', six.text_type(ex)) + self.assertEqual('oops', str(ex)) ex = self.assertRaises(exceptions.ResourceNotFound, sot.fetch, sess) self.assertEqual('No stack found for %s' % FAKE_ID, - six.text_type(ex)) + str(ex)) def test_abandon(self): sess = mock.Mock() diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index bc0605c18..6d327a1a8 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -17,7 +17,6 @@ import mock import munch import requests -import six from openstack import exceptions from openstack import format @@ -960,7 +959,7 @@ class Test(resource.Resource): body=False, headers=False, computed=False) self.assertEqual( 'At least one of `body`, `headers` or `computed` must be True', - six.text_type(err)) + str(err)) def test_to_dict_with_mro_no_override(self): diff --git a/openstack/utils.py b/openstack/utils.py index cca475de5..d893d6fc0 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -15,8 +15,6 @@ import threading import time -import six - import keystoneauth1 from keystoneauth1 import discover @@ -31,7 +29,7 @@ def urljoin(*args): like /path this should be joined to http://host/path as it is an anchored link. We generally won't care about that in client. """ - return '/'.join(six.text_type(a or '').strip('/') for a in args) + return '/'.join(str(a or '').strip('/') for a in args) def iterate_timeout(timeout, message, wait=2): @@ -194,7 +192,7 @@ def maximum_supported_microversion(adapter, client_maximum): return discover.version_to_string(result) -class TinyDAG(six.Iterator): +class TinyDAG(object): """Tiny DAG Bases on the Kahn's algorithm, and enables parallel visiting of the nodes diff --git a/requirements.txt b/requirements.txt index 6d60847b4..c443a4544 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,6 @@ PyYAML>=3.12 # MIT appdirs>=1.3.0 # MIT License requestsexceptions>=1.2.0 # Apache-2.0 jsonpatch!=1.20,>=1.16 # BSD -six>=1.10.0 # MIT os-service-types>=1.7.0 # Apache-2.0 keystoneauth1>=3.18.0 # Apache-2.0 From 0c5ae590257d129d0c9d842d7e818d4d1f89e2a9 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 29 Mar 2020 11:06:49 -0500 Subject: [PATCH 2662/3836] Stop subclassing object This wasn't actually even needed in python2.7, but habits die hard. Change-Id: I20f282d285506b1f1dde01be6cde0c0e8c7883c8 --- examples/connect.py | 2 +- openstack/_services_mixin.py | 2 +- openstack/baremetal/v1/_common.py | 2 +- openstack/baremetal/v1/node.py | 2 +- openstack/cloud/_normalize.py | 2 +- openstack/cloud/_utils.py | 2 +- openstack/cloud/inventory.py | 2 +- openstack/cloud/openstackcloud.py | 4 ++-- openstack/compute/v2/metadata.py | 2 +- openstack/config/_util.py | 2 +- openstack/config/cloud_region.py | 2 +- openstack/config/loader.py | 2 +- openstack/format.py | 2 +- openstack/image/_download.py | 2 +- openstack/image/image_signer.py | 2 +- openstack/image/iterable_chunked_file.py | 2 +- openstack/resource.py | 8 +++---- openstack/service_description.py | 4 ++-- openstack/tests/fakes.py | 18 +++++++------- .../tests/functional/image/v2/test_image.py | 2 +- openstack/tests/unit/cloud/test_meta.py | 8 +++---- openstack/tests/unit/cloud/test_zone.py | 2 +- .../tests/unit/compute/v2/test_server.py | 12 +++++----- openstack/tests/unit/image/v2/test_image.py | 2 +- openstack/tests/unit/image/v2/test_proxy.py | 2 +- openstack/tests/unit/test_proxy.py | 2 +- openstack/tests/unit/test_resource.py | 24 +++++++++---------- openstack/utils.py | 4 ++-- tools/print-services.py | 2 +- tox.ini | 3 ++- 30 files changed, 64 insertions(+), 63 deletions(-) diff --git a/examples/connect.py b/examples/connect.py index 1d4b86ad8..09a9cbef1 100644 --- a/examples/connect.py +++ b/examples/connect.py @@ -36,7 +36,7 @@ cloud = openstack.connect(cloud=TEST_CLOUD) -class Opts(object): +class Opts: def __init__(self, cloud_name='devstack-admin', debug=False): self.cloud = cloud_name self.debug = debug diff --git a/openstack/_services_mixin.py b/openstack/_services_mixin.py index eb58e83b8..75c1d19e7 100644 --- a/openstack/_services_mixin.py +++ b/openstack/_services_mixin.py @@ -20,7 +20,7 @@ from openstack.workflow import workflow_service -class ServicesMixin(object): +class ServicesMixin: identity = identity_service.IdentityService(service_type='identity') diff --git a/openstack/baremetal/v1/_common.py b/openstack/baremetal/v1/_common.py index 8fcf7b579..e37c7b73a 100644 --- a/openstack/baremetal/v1/_common.py +++ b/openstack/baremetal/v1/_common.py @@ -68,7 +68,7 @@ """API version in which configdrive can be a dictionary.""" -class ListMixin(object): +class ListMixin: @classmethod def list(cls, session, details=False, **params): diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 5c034e723..8ef4edff4 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -18,7 +18,7 @@ from openstack import utils -class ValidationResult(object): +class ValidationResult: """Result of a single interface validation. :ivar result: Result of a validation, ``True`` for success, ``False`` for diff --git a/openstack/cloud/_normalize.py b/openstack/cloud/_normalize.py index 97a27fa0b..4cbcd9f85 100644 --- a/openstack/cloud/_normalize.py +++ b/openstack/cloud/_normalize.py @@ -139,7 +139,7 @@ def _pop_or_get(resource, key, default, strict): return resource.get(key, default) -class Normalizer(object): +class Normalizer: '''Mix-in class to provide the normalization functions. This is in a separate class just for on-disk source code organization diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index 9a6a8cf0b..bb82a165b 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -667,7 +667,7 @@ def generate_patches_from_kwargs(operation, **kwargs): return sorted(patches) -class FileSegment(object): +class FileSegment: """File-like object to pass to requests.""" def __init__(self, filename, offset, length): diff --git a/openstack/cloud/inventory.py b/openstack/cloud/inventory.py index 8905ce0a7..32a26821b 100644 --- a/openstack/cloud/inventory.py +++ b/openstack/cloud/inventory.py @@ -22,7 +22,7 @@ __all__ = ['OpenStackInventory'] -class OpenStackInventory(object): +class OpenStackInventory: # Put this here so the capability can be detected with hasattr on the class extra_config = None diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 2e821238a..fefdd9ba1 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -48,7 +48,7 @@ OBJECT_CONTAINER_ACLS = _object_store.OBJECT_CONTAINER_ACLS -class _OpenStackCloudMixin(object): +class _OpenStackCloudMixin: """Represent a connection to an OpenStack Cloud. OpenStackCloud is the entry point for all cloud operations, regardless @@ -126,7 +126,7 @@ def __init__(self): def _fake_invalidate(unused): pass - class _FakeCache(object): + class _FakeCache: def invalidate(self): pass diff --git a/openstack/compute/v2/metadata.py b/openstack/compute/v2/metadata.py index 906dc7213..0d8ad2f12 100644 --- a/openstack/compute/v2/metadata.py +++ b/openstack/compute/v2/metadata.py @@ -15,7 +15,7 @@ from openstack import utils -class MetadataMixin(object): +class MetadataMixin: def _metadata(self, method, key=None, clear=False, delete=False, metadata=None): diff --git a/openstack/config/_util.py b/openstack/config/_util.py index cdd6737e8..c77aaf235 100644 --- a/openstack/config/_util.py +++ b/openstack/config/_util.py @@ -45,7 +45,7 @@ def merge_clouds(old_dict, new_dict): return ret -class VersionRequest(object): +class VersionRequest: def __init__( self, version=None, diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 41bcc9d5d..8f03d5386 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -201,7 +201,7 @@ def from_conf(conf, session=None, service_types=None, **kwargs): session=session, config=config_dict, **kwargs) -class CloudRegion(object): +class CloudRegion: # TODO(efried): Doc the rest of the kwargs """The configuration for a Region of an OpenStack Cloud. diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 09e916c37..a86433f31 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -127,7 +127,7 @@ def _fix_argv(argv): options=','.join(overlap))) -class OpenStackConfig(object): +class OpenStackConfig: # These two attribute are to allow os-client-config to plumb in its # local versions for backwards compat. diff --git a/openstack/format.py b/openstack/format.py index 94fd310fe..7e586709d 100644 --- a/openstack/format.py +++ b/openstack/format.py @@ -11,7 +11,7 @@ # under the License. -class Formatter(object): +class Formatter: @classmethod def serialize(cls, value): diff --git a/openstack/image/_download.py b/openstack/image/_download.py index 0c50bffb2..21c6ffe77 100644 --- a/openstack/image/_download.py +++ b/openstack/image/_download.py @@ -24,7 +24,7 @@ def _verify_checksum(md5, checksum): "checksum mismatch: %s != %s" % (checksum, digest)) -class DownloadMixin(object): +class DownloadMixin: def download(self, session, stream=False, output=None, chunk_size=1024): """Download the data contained in an image""" diff --git a/openstack/image/image_signer.py b/openstack/image/image_signer.py index 19c6ec965..23d294811 100644 --- a/openstack/image/image_signer.py +++ b/openstack/image/image_signer.py @@ -28,7 +28,7 @@ } -class ImageSigner(object): +class ImageSigner: """Image file signature generator. Generates signatures for files using a specified private key file. diff --git a/openstack/image/iterable_chunked_file.py b/openstack/image/iterable_chunked_file.py index d887ace5b..3b9d9569d 100644 --- a/openstack/image/iterable_chunked_file.py +++ b/openstack/image/iterable_chunked_file.py @@ -12,7 +12,7 @@ # -class IterableChunkedFile(object): +class IterableChunkedFile: """File object chunk iterator using yield. Represents a local file as an iterable object by splitting the file diff --git a/openstack/resource.py b/openstack/resource.py index cf9d9a476..b102549a4 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -82,7 +82,7 @@ def _convert_type(value, data_type, list_type=None): return data_type(value) -class _BaseComponent(object): +class _BaseComponent: # The name this component is being tracked as in the Resource key = None @@ -257,7 +257,7 @@ def clean(self, only=None): self._dirty = set() -class _Request(object): +class _Request: """Prepared components that go into a KSA request""" def __init__(self, url, body, headers): @@ -266,7 +266,7 @@ def __init__(self, url, body, headers): self.headers = headers -class QueryParameters(object): +class QueryParameters: def __init__(self, *names, **mappings): """Create a dict of accepted query parameters @@ -1862,7 +1862,7 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): "No %s found for %s" % (cls.__name__, name_or_id)) -class TagMixin(object): +class TagMixin: _tag_query_parameters = { 'tags': 'tags', diff --git a/openstack/service_description.py b/openstack/service_description.py index 2aad61a6d..223a166d3 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -27,7 +27,7 @@ _service_type_manager = os_service_types.ServiceTypes() -class _ServiceDisabledProxyShim(object): +class _ServiceDisabledProxyShim: def __init__(self, service_type, reason): self.service_type = service_type self.reason = reason @@ -39,7 +39,7 @@ def __getattr__(self, item): service_type=self.service_type, reason=self.reason or '')) -class ServiceDescription(object): +class ServiceDescription: #: Dictionary of supported versions and proxy classes for that version supported_versions = None diff --git a/openstack/tests/fakes.py b/openstack/tests/fakes.py index 9fae3c7f5..af1bbe235 100644 --- a/openstack/tests/fakes.py +++ b/openstack/tests/fakes.py @@ -282,7 +282,7 @@ def make_fake_port(address, node_id=None, port_id=None): node_id=node_id)) -class FakeFloatingIP(object): +class FakeFloatingIP: def __init__(self, id, pool, ip, fixed_ip, instance_id): self.id = id self.pool = pool @@ -345,7 +345,7 @@ def make_fake_hypervisor(id, name): })) -class FakeVolume(object): +class FakeVolume: def __init__( self, id, status, name, attachments=[], size=75): @@ -363,7 +363,7 @@ def __init__( self.metadata = {} -class FakeVolumeSnapshot(object): +class FakeVolumeSnapshot: def __init__( self, id, status, name, description, size=75): self.id = id @@ -376,7 +376,7 @@ def __init__( self.metadata = {} -class FakeMachine(object): +class FakeMachine: def __init__(self, id, name=None, driver=None, driver_info=None, chassis_uuid=None, instance_info=None, instance_uuid=None, properties=None, reservation=None, last_error=None, @@ -394,7 +394,7 @@ def __init__(self, id, name=None, driver=None, driver_info=None, self.provision_state = provision_state -class FakeMachinePort(object): +class FakeMachinePort: def __init__(self, id, address, node_id): self.uuid = id self.address = address @@ -443,7 +443,7 @@ def make_fake_nova_security_group(id, name, description, rules): })) -class FakeNovaSecgroupRule(object): +class FakeNovaSecgroupRule: def __init__(self, id, from_port=None, to_port=None, ip_protocol=None, cidr=None, parent_group_id=None): self.id = id @@ -455,13 +455,13 @@ def __init__(self, id, from_port=None, to_port=None, ip_protocol=None, self.parent_group_id = parent_group_id -class FakeHypervisor(object): +class FakeHypervisor: def __init__(self, id, hostname): self.id = id self.hypervisor_hostname = hostname -class FakeZone(object): +class FakeZone: def __init__(self, id, name, type_, email, description, ttl, masters): self.id = id @@ -473,7 +473,7 @@ def __init__(self, id, name, type_, email, description, self.masters = masters -class FakeRecordset(object): +class FakeRecordset: def __init__(self, zone, id, name, type_, description, ttl, records): self.zone = zone diff --git a/openstack/tests/functional/image/v2/test_image.py b/openstack/tests/functional/image/v2/test_image.py index badd31fc5..3635b6a72 100644 --- a/openstack/tests/functional/image/v2/test_image.py +++ b/openstack/tests/functional/image/v2/test_image.py @@ -18,7 +18,7 @@ class TestImage(base.BaseFunctionalTest): - class ImageOpts(object): + class ImageOpts: def __init__(self): self.image_api_version = '2' diff --git a/openstack/tests/unit/cloud/test_meta.py b/openstack/tests/unit/cloud/test_meta.py index 0de6620d3..e925c63dd 100644 --- a/openstack/tests/unit/cloud/test_meta.py +++ b/openstack/tests/unit/cloud/test_meta.py @@ -24,13 +24,13 @@ PUBLIC_V6 = '2001:0db8:face:0da0:face::0b00:1c' # rfc3849 -class FakeConfig(object): +class FakeConfig: def get_region_name(self, service_type=None): # TODO(efried): Validate service_type? return 'test-region' -class FakeCloud(object): +class FakeCloud: config = FakeConfig() name = 'test-name' private = False @@ -942,10 +942,10 @@ def test_get_groups_from_server(self): def test_obj_list_to_munch(self): """Test conversion of a list of objects to a list of dictonaries""" - class obj0(object): + class obj0: value = 0 - class obj1(object): + class obj1: value = 1 list = [obj0, obj1] diff --git a/openstack/tests/unit/cloud/test_zone.py b/openstack/tests/unit/cloud/test_zone.py index 3bea3fb71..145e04a26 100644 --- a/openstack/tests/unit/cloud/test_zone.py +++ b/openstack/tests/unit/cloud/test_zone.py @@ -26,7 +26,7 @@ } -class ZoneTestWrapper(object): +class ZoneTestWrapper: def __init__(self, ut, attrs): self.remote_res = attrs diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 6baf3c45c..e48266d8e 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -806,7 +806,7 @@ def test_get_console_output(self): def test_live_migrate_no_force(self): sot = server.Server(**EXAMPLE) - class FakeEndpointData(object): + class FakeEndpointData: min_microversion = None max_microversion = None self.sess.get_endpoint_data.return_value = FakeEndpointData() @@ -822,7 +822,7 @@ class FakeEndpointData(object): def test_live_migrate_no_microversion_force_true(self): sot = server.Server(**EXAMPLE) - class FakeEndpointData(object): + class FakeEndpointData: min_microversion = None max_microversion = None self.sess.get_endpoint_data.return_value = FakeEndpointData() @@ -848,7 +848,7 @@ class FakeEndpointData(object): def test_live_migrate_25(self): sot = server.Server(**EXAMPLE) - class FakeEndpointData(object): + class FakeEndpointData: min_microversion = '2.1' max_microversion = '2.25' self.sess.get_endpoint_data.return_value = FakeEndpointData() @@ -872,7 +872,7 @@ class FakeEndpointData(object): def test_live_migrate_25_default_block(self): sot = server.Server(**EXAMPLE) - class FakeEndpointData(object): + class FakeEndpointData: min_microversion = '2.1' max_microversion = '2.25' self.sess.get_endpoint_data.return_value = FakeEndpointData() @@ -896,7 +896,7 @@ class FakeEndpointData(object): def test_live_migrate_30(self): sot = server.Server(**EXAMPLE) - class FakeEndpointData(object): + class FakeEndpointData: min_microversion = '2.1' max_microversion = '2.30' self.sess.get_endpoint_data.return_value = FakeEndpointData() @@ -920,7 +920,7 @@ class FakeEndpointData(object): def test_live_migrate_30_force(self): sot = server.Server(**EXAMPLE) - class FakeEndpointData(object): + class FakeEndpointData: min_microversion = '2.1' max_microversion = '2.30' self.sess.get_endpoint_data.return_value = FakeEndpointData() diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 4bc95baf6..0086c640f 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -95,7 +95,7 @@ def calculate_md5_checksum(data): return checksum.hexdigest() -class FakeResponse(object): +class FakeResponse: def __init__(self, response, status_code=200, headers=None, reason=None): self.body = response self.content = response diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 20e35d750..5392c7381 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -27,7 +27,7 @@ EXAMPLE = fake_image.EXAMPLE -class FakeResponse(object): +class FakeResponse: def __init__(self, response, status_code=200, headers=None): self.body = response self.status_code = status_code diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index 46c9ff8be..3cd7b0ed2 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -137,7 +137,7 @@ def test__get_resource_from_id(self): # of that same behavior to let us check that `new` gets # called with the expected arguments. - class Fake(object): + class Fake: call = {} @classmethod diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 6d327a1a8..67fd7c863 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -24,7 +24,7 @@ from openstack.tests.unit import base -class FakeResponse(object): +class FakeResponse: def __init__(self, response, status_code=200, headers=None): self.body = response self.status_code = status_code @@ -74,7 +74,7 @@ def test_get_no_instance(self): def test_get_name_None(self): name = "name" - class Parent(object): + class Parent: _example = {name: None} instance = Parent() @@ -87,7 +87,7 @@ class Parent(object): def test_get_default(self): expected_result = 123 - class Parent(object): + class Parent: _example = {} instance = Parent() @@ -104,7 +104,7 @@ def test_get_name_untyped(self): name = "name" expected_result = 123 - class Parent(object): + class Parent: _example = {name: expected_result} instance = Parent() @@ -119,7 +119,7 @@ def test_get_name_typed(self): name = "name" value = "123" - class Parent(object): + class Parent: _example = {name: value} instance = Parent() @@ -134,7 +134,7 @@ def test_get_name_formatter(self): value = "123" expected_result = "one hundred twenty three" - class Parent(object): + class Parent: _example = {name: value} class FakeFormatter(format.Formatter): @@ -154,7 +154,7 @@ def test_set_name_untyped(self): name = "name" expected_value = "123" - class Parent(object): + class Parent: _example = {} instance = Parent() @@ -167,7 +167,7 @@ class Parent(object): def test_set_name_typed(self): expected_value = "123" - class Parent(object): + class Parent: _example = {} instance = Parent() @@ -177,7 +177,7 @@ class Parent(object): # instance that would allow us to call `assert_called_once_with` to # ensure that we're sending the value through the type. # Instead, we use this tiny version of a similar thing. - class FakeType(object): + class FakeType: calls = [] def __init__(self, arg): @@ -192,7 +192,7 @@ def __init__(self, arg): def test_set_name_formatter(self): expected_value = "123" - class Parent(object): + class Parent: _example = {} instance = Parent() @@ -220,7 +220,7 @@ def test_delete_name(self): name = "name" expected_value = "123" - class Parent(object): + class Parent: _example = {name: expected_value} instance = Parent() @@ -235,7 +235,7 @@ def test_delete_name_doesnt_exist(self): name = "name" expected_value = "123" - class Parent(object): + class Parent: _example = {"what": expected_value} instance = Parent() diff --git a/openstack/utils.py b/openstack/utils.py index d893d6fc0..962113fd7 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -75,7 +75,7 @@ def get_string_format_keys(fmt_string, old_style=True): use the old style string formatting. """ if old_style: - class AccessSaver(object): + class AccessSaver: def __init__(self): self.keys = [] @@ -192,7 +192,7 @@ def maximum_supported_microversion(adapter, client_maximum): return discover.version_to_string(result) -class TinyDAG(object): +class TinyDAG: """Tiny DAG Bases on the Kahn's algorithm, and enables parallel visiting of the nodes diff --git a/tools/print-services.py b/tools/print-services.py index b4c664957..73dd3d09f 100644 --- a/tools/print-services.py +++ b/tools/print-services.py @@ -71,7 +71,7 @@ def make_names(): for imp in sorted(imports): print(imp) print('\n') - print("class ServicesMixin(object):\n") + print("class ServicesMixin:\n") for service in services: if service: print(" {service}".format(service=service)) diff --git a/tox.ini b/tox.ini index 3e6f1bfd0..e8809b843 100644 --- a/tox.ini +++ b/tox.ini @@ -107,12 +107,13 @@ commands = # The following are ignored on purpose. It's not super worth it to fix them. # However, if you feel strongly about it, patches will be accepted to fix them # if they fix ALL of the occurances of one and only one of them. +# H238 New Style Classes are the default in Python3 # H306 Is about alphabetical imports - there's a lot to fix. # H4 Are about docstrings and there's just a huge pile of pre-existing issues. # W503 Is supposed to be off by default but in the latest pycodestyle isn't. # Also, both openstacksdk and Donald Knuth disagree with the rule. Line # breaks should occur before the binary operator for readability. -ignore = H306,H4,W503 +ignore = H238,H306,H4,W503 show-source = True exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,openstack/_services_mixin.py From 786aeb7f8a60bea29cfc84b71f44e1f50782418e Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 8 May 2020 14:12:51 -0500 Subject: [PATCH 2663/3836] Set BaseImageProxy.create_image validate_checksum default to False create_image() does not work properly with some vendor deployments, the reported checksum is different than the local checksum. glanceclient did not enforce the checksum validation by default, this will not be a surprise to apps migrating from that library. Change-Id: I4147122e744cc2224ced89f6ac931e81b76e50a7 Signed-off-by: Dean Troyer --- openstack/image/_base_proxy.py | 2 +- openstack/tests/functional/cloud/test_image.py | 2 ++ openstack/tests/unit/cloud/test_image.py | 9 +++++---- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/openstack/image/_base_proxy.py b/openstack/image/_base_proxy.py index 85055f95c..082c98038 100644 --- a/openstack/image/_base_proxy.py +++ b/openstack/image/_base_proxy.py @@ -41,7 +41,7 @@ def create_image( disable_vendor_agent=True, allow_duplicates=False, meta=None, wait=False, timeout=3600, - data=None, validate_checksum=True, + data=None, validate_checksum=False, **kwargs): """Upload an image. diff --git a/openstack/tests/functional/cloud/test_image.py b/openstack/tests/functional/cloud/test_image.py index 5331f5dca..192d1c80c 100644 --- a/openstack/tests/functional/cloud/test_image.py +++ b/openstack/tests/functional/cloud/test_image.py @@ -80,6 +80,7 @@ def test_create_image_skip_duplicate(self): container_format='bare', min_disk=10, min_ram=1024, + validate_checksum=True, wait=True) second_image = self.user_cloud.create_image( name=image_name, @@ -88,6 +89,7 @@ def test_create_image_skip_duplicate(self): container_format='bare', min_disk=10, min_ram=1024, + validate_checksum=True, wait=True) self.assertEqual(first_image.id, second_image.id) finally: diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index e2c280ac0..a87fb9dd7 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -377,7 +377,7 @@ def test_create_image_put_v2(self): self.cloud.create_image( self.image_name, self.imagefile.name, wait=True, timeout=1, tags=['tag1', 'tag2'], - is_public=False) + is_public=False, validate_checksum=True) self.assert_calls() self.assertEqual(self.adapter.request_history[7].text.read(), @@ -553,7 +553,8 @@ def test_create_image_task(self): self.cloud.create_image( self.image_name, self.imagefile.name, wait=True, timeout=1, disk_format='vhd', container_format='ovf', - is_public=False, container=self.container_name) + is_public=False, validate_checksum=True, + container=self.container_name) self.assert_calls() @@ -696,7 +697,7 @@ def _call_create_image(self, name, **kwargs): imagefile.close() self.cloud.create_image( name, imagefile.name, wait=True, timeout=1, - is_public=False, **kwargs) + is_public=False, validate_checksum=True, **kwargs) def test_create_image_put_v1(self): self.cloud.config.config['image_api_version'] = '1' @@ -920,7 +921,7 @@ def test_create_image_put_v2_wrong_checksum_delete(self): self.cloud.create_image, self.image_name, self.imagefile.name, is_public=False, md5='a', sha256='b', - allow_duplicates=True + allow_duplicates=True, validate_checksum=True ) self.assert_calls() From 44136d7f6de11d8b9a3ac31f0982056017940f1b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 12 May 2020 09:15:02 -0500 Subject: [PATCH 2664/3836] Fix issues found by latest flake8 Change-Id: Iacb6ec4ab22594d3b1699523e04bc60c89ab04fe --- openstack/cloud/_block_storage.py | 6 +++--- openstack/cloud/_compute.py | 2 +- openstack/config/loader.py | 4 ++-- openstack/orchestration/util/event_utils.py | 4 ++-- openstack/orchestration/util/utils.py | 4 ++-- openstack/tests/unit/cloud/test_object.py | 3 --- openstack/tests/unit/cloud/test_security_groups.py | 2 +- openstack/tests/unit/cloud/test_stack.py | 4 ++-- 8 files changed, 13 insertions(+), 16 deletions(-) diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index 8759e321c..870f0f059 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -43,9 +43,9 @@ def list_volumes(self, cache=True): def _list(data): volumes.extend(data.get('volumes', [])) endpoint = None - for l in data.get('volumes_links', []): - if 'rel' in l and 'next' == l['rel']: - endpoint = l['href'] + for link in data.get('volumes_links', []): + if 'rel' in link and 'next' == link['rel']: + endpoint = link['href'] break if endpoint: try: diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 5b09d90a1..8afa674a0 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -1813,7 +1813,7 @@ def parse_datetime_for_nova(date): proj = self.get_project(name_or_id) if not proj: raise exc.OpenStackCloudException( - "project does not exist: {}".format(name=proj.id)) + "project does not exist: {name}".format(name=proj.id)) data = proxy._json_response( self.compute.get( diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 7049ddd5b..eebdb3e2b 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -779,8 +779,8 @@ def register_argparse_arguments(self, parser, argv, service_keys=None): # legacy clients have un-prefixed api-version options parser.add_argument( '--{service_key}-api-version'.format( - service_key=service_key.replace('_', '-'), - help=argparse_mod.SUPPRESS)) + service_key=service_key.replace('_', '-')), + help=argparse_mod.SUPPRESS) adapter.register_service_adapter_argparse_arguments( parser, service_type=service_key) diff --git a/openstack/orchestration/util/event_utils.py b/openstack/orchestration/util/event_utils.py index b24a929a3..5d56bf66a 100644 --- a/openstack/orchestration/util/event_utils.py +++ b/openstack/orchestration/util/event_utils.py @@ -64,8 +64,8 @@ def is_stack_event(event): return False phys_id = event.get('physical_resource_id', '') - links = dict((l.get('rel'), - l.get('href')) for l in event.get('links', [])) + links = dict((link.get('rel'), + link.get('href')) for link in event.get('links', [])) stack_id = links.get('stack', phys_id).rsplit('/', 1)[-1] return stack_id == phys_id diff --git a/openstack/orchestration/util/utils.py b/openstack/orchestration/util/utils.py index 89a04b8eb..4e31b2856 100644 --- a/openstack/orchestration/util/utils.py +++ b/openstack/orchestration/util/utils.py @@ -53,8 +53,8 @@ def read_url_content(url): def resource_nested_identifier(rsrc): - nested_link = [l for l in rsrc.links or [] - if l.get('rel') == 'nested'] + nested_link = [link for link in rsrc.links or [] + if link.get('rel') == 'nested'] if nested_link: nested_href = nested_link[0].get('href') nested_identifier = nested_href.split("/")[-2:] diff --git a/openstack/tests/unit/cloud/test_object.py b/openstack/tests/unit/cloud/test_object.py index 0d099b13b..d225af9ff 100644 --- a/openstack/tests/unit/cloud/test_object.py +++ b/openstack/tests/unit/cloud/test_object.py @@ -810,7 +810,6 @@ def test_create_static_large_object(self): 'header mismatch in manifest call') base_object = '/{container}/{object}'.format( - endpoint=self.endpoint, container=self.container, object=self.object) @@ -932,7 +931,6 @@ def test_slo_manifest_retry(self): 'header mismatch in manifest call') base_object = '/{container}/{object}'.format( - endpoint=self.endpoint, container=self.container, object=self.object) @@ -1228,7 +1226,6 @@ def test_object_segment_retries(self): 'header mismatch in manifest call') base_object = '/{container}/{object}'.format( - endpoint=self.endpoint, container=self.container, object=self.object) diff --git a/openstack/tests/unit/cloud/test_security_groups.py b/openstack/tests/unit/cloud/test_security_groups.py index 152455514..4ff5875af 100644 --- a/openstack/tests/unit/cloud/test_security_groups.py +++ b/openstack/tests/unit/cloud/test_security_groups.py @@ -640,7 +640,7 @@ def test_add_security_group_to_server_nova(self): method='GET', uri='{endpoint}/os-security-groups'.format( endpoint=fakes.COMPUTE_ENDPOINT, - id='server_id'), + ), json={'security_groups': [nova_grp_dict]}), dict( method='POST', diff --git a/openstack/tests/unit/cloud/test_stack.py b/openstack/tests/unit/cloud/test_stack.py index c6cce9b2d..7ee2047d3 100644 --- a/openstack/tests/unit/cloud/test_stack.py +++ b/openstack/tests/unit/cloud/test_stack.py @@ -231,7 +231,7 @@ def test_delete_stack_wait(self): dict(method='GET', uri='{endpoint}/stacks/{id}?{resolve}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name, resolve=resolve), + id=self.stack_id, resolve=resolve), status_code=404), ]) @@ -287,7 +287,7 @@ def test_delete_stack_wait_failed(self): dict(method='GET', uri='{endpoint}/stacks/{id}?resolve_outputs=False'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name), + id=self.stack_id), status_code=302, headers=dict( location='{endpoint}/stacks/{name}/{id}?{resolve}'.format( From c5748fb1b29a8321ce8a16b7da6a8dd0d9dd4230 Mon Sep 17 00:00:00 2001 From: Noah Mickus Date: Thu, 30 Apr 2020 21:26:29 -0500 Subject: [PATCH 2665/3836] Add TLS protocol support for Octavia Add a property "tls_versions" for storing a list of TLS protocol versions Change-Id: I6c73a178f01e010535237f839d85bedac5222624 Story: 2006627 Task: 37188 Depends-On:I480b7fb9756d98ba9dbcdfd1d4b193ce6868e291 --- openstack/load_balancer/v2/listener.py | 5 +++-- openstack/load_balancer/v2/pool.py | 5 +++-- openstack/tests/unit/load_balancer/test_listener.py | 6 +++++- openstack/tests/unit/load_balancer/test_pool.py | 6 +++++- ...dd-tls-version-support-for-octavia-7ecb372e6fb58101.yaml | 6 ++++++ 5 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/add-tls-version-support-for-octavia-7ecb372e6fb58101.yaml diff --git a/openstack/load_balancer/v2/listener.py b/openstack/load_balancer/v2/listener.py index 455595a81..d8b50f0aa 100644 --- a/openstack/load_balancer/v2/listener.py +++ b/openstack/load_balancer/v2/listener.py @@ -32,8 +32,7 @@ class Listener(resource.Resource, resource.TagMixin): 'sni_container_refs', 'insert_headers', 'load_balancer_id', 'timeout_client_data', 'timeout_member_connect', 'timeout_member_data', 'timeout_tcp_inspect', 'allowed_cidrs', - 'tls_ciphers', - is_admin_state_up='admin_state_up', + 'tls_ciphers', 'tls_versions', is_admin_state_up='admin_state_up', **resource.TagMixin._tag_query_parameters ) @@ -94,6 +93,8 @@ class Listener(resource.Resource, resource.TagMixin): timeout_tcp_inspect = resource.Body('timeout_tcp_inspect', type=int) #: Stores a cipher string in OpenSSL format. tls_ciphers = resource.Body('tls_ciphers') + #: A lsit of TLS protocols to be used by the listener + tls_versions = resource.Body('tls_versions', type=list) class ListenerStats(resource.Resource): diff --git a/openstack/load_balancer/v2/pool.py b/openstack/load_balancer/v2/pool.py index 2766b381b..8ba1385d7 100644 --- a/openstack/load_balancer/v2/pool.py +++ b/openstack/load_balancer/v2/pool.py @@ -29,8 +29,7 @@ class Pool(resource.Resource, resource.TagMixin): 'health_monitor_id', 'lb_algorithm', 'listener_id', 'loadbalancer_id', 'description', 'name', 'project_id', 'protocol', 'created_at', 'updated_at', 'provisioning_status', 'operating_status', - 'tls_ciphers', - is_admin_state_up='admin_state_up', + 'tls_ciphers', 'tls_versions', is_admin_state_up='admin_state_up', **resource.TagMixin._tag_query_parameters ) @@ -69,5 +68,7 @@ class Pool(resource.Resource, resource.TagMixin): tls_ciphers = resource.Body('tls_ciphers') #: A JSON object specifying the session persistence for the pool. session_persistence = resource.Body('session_persistence', type=dict) + #: A list of TLS protocol versions to be used in by the pool + tls_versions = resource.Body('tls_versions', type=list) #: Timestamp when the pool was updated updated_at = resource.Body('updated_at') diff --git a/openstack/tests/unit/load_balancer/test_listener.py b/openstack/tests/unit/load_balancer/test_listener.py index 6c34d34cc..fe97597a7 100644 --- a/openstack/tests/unit/load_balancer/test_listener.py +++ b/openstack/tests/unit/load_balancer/test_listener.py @@ -41,7 +41,8 @@ 'timeout_member_connect': 5000, 'timeout_member_data': 50000, 'timeout_tcp_inspect': 0, - 'tls_ciphers': 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256' + 'tls_ciphers': 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256', + 'tls_versions': ['TLSv1.1', 'TLSv1.2'] } EXAMPLE_STATS = { @@ -106,6 +107,8 @@ def test_make_it(self): test_listener.timeout_tcp_inspect) self.assertEqual(EXAMPLE['tls_ciphers'], test_listener.tls_ciphers) + self.assertEqual(EXAMPLE['tls_versions'], + test_listener.tls_versions) self.assertDictEqual( {'limit': 'limit', @@ -137,6 +140,7 @@ def test_make_it(self): 'timeout_member_data': 'timeout_member_data', 'timeout_tcp_inspect': 'timeout_tcp_inspect', 'tls_ciphers': 'tls_ciphers', + 'tls_versions': 'tls_versions', }, test_listener._query_mapping._mapping) diff --git a/openstack/tests/unit/load_balancer/test_pool.py b/openstack/tests/unit/load_balancer/test_pool.py index 412449e9d..93773490c 100644 --- a/openstack/tests/unit/load_balancer/test_pool.py +++ b/openstack/tests/unit/load_balancer/test_pool.py @@ -35,7 +35,8 @@ 'health_monitor': 'healthmonitor', 'health_monitor_id': uuid.uuid4(), 'members': [{'id': uuid.uuid4()}], - 'tls_ciphers': 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256' + 'tls_ciphers': 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256', + 'tls_versions': ['TLSv1.1', 'TLSv1.2'], } @@ -84,6 +85,8 @@ def test_make_it(self): self.assertEqual(EXAMPLE['members'], test_pool.members) self.assertEqual(EXAMPLE['tls_ciphers'], test_pool.tls_ciphers) + self.assertEqual(EXAMPLE['tls_versions'], + test_pool.tls_versions) self.assertDictEqual( {'limit': 'limit', @@ -107,5 +110,6 @@ def test_make_it(self): 'loadbalancer_id': 'loadbalancer_id', 'protocol': 'protocol', 'tls_ciphers': 'tls_ciphers', + 'tls_versions': 'tls_versions', }, test_pool._query_mapping._mapping) diff --git a/releasenotes/notes/add-tls-version-support-for-octavia-7ecb372e6fb58101.yaml b/releasenotes/notes/add-tls-version-support-for-octavia-7ecb372e6fb58101.yaml new file mode 100644 index 000000000..d96dd0c9e --- /dev/null +++ b/releasenotes/notes/add-tls-version-support-for-octavia-7ecb372e6fb58101.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Added the ``tls_versions`` properties to listener.py + and pool.py for storing a python list of TLS protocol + versions to be used by the pools and listeners. From acbd3f1058ae8a841308e405dc063e40cf0c92d8 Mon Sep 17 00:00:00 2001 From: Adrien Pennsart Date: Tue, 19 May 2020 16:02:41 -0400 Subject: [PATCH 2666/3836] added new regions for provider OVH Change-Id: I893a2f1c17cbcf2ae73812b5dfe68e25c0977077 --- openstack/config/vendors/ovh.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openstack/config/vendors/ovh.json b/openstack/config/vendors/ovh.json index 8dfca6791..3548358a1 100644 --- a/openstack/config/vendors/ovh.json +++ b/openstack/config/vendors/ovh.json @@ -8,18 +8,22 @@ "BHS", "BHS1", "BHS3", + "BHS5", "DE", "DE1", "GRA", "GRA1", "GRA5", + "GRA7", "SBG", "SBG1", "SBG5", "UK", "UK1", "WAW", - "WAW1" + "WAW1", + "SYD1", + "SGP1" ], "identity_api_version": "3", "floating_ip_source": "None" From d7c12797a97549f64567bf8ab39143dce3f0cb68 Mon Sep 17 00:00:00 2001 From: Adrien Pensart Date: Tue, 19 May 2020 16:32:34 -0400 Subject: [PATCH 2667/3836] new ovh-us provider for OVH US regions Change-Id: Iff08b58316cbc6d359eeb25479a6ac524944147f --- openstack/config/vendors/ovh-us.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 openstack/config/vendors/ovh-us.json diff --git a/openstack/config/vendors/ovh-us.json b/openstack/config/vendors/ovh-us.json new file mode 100644 index 000000000..535a7ded0 --- /dev/null +++ b/openstack/config/vendors/ovh-us.json @@ -0,0 +1,13 @@ +{ + "name": "ovh-us", + "profile": { + "auth": { + "auth_url": "https://auth.cloud.ovh.us/" + }, + "regions": [ + "US-EAST-VA-1" + ], + "identity_api_version": "3", + "floating_ip_source": "None" + } +} From 67f546a93dbf054ecdf838dbd213c64732c024a6 Mon Sep 17 00:00:00 2001 From: Adrien Pensart Date: Wed, 20 May 2020 10:31:13 -0400 Subject: [PATCH 2668/3836] add default user and project domain for OVH provider Change-Id: I6b9867863e590d9a4a70db6b2abbd807f5b9089c --- openstack/config/vendors/ovh-us.json | 4 +++- openstack/config/vendors/ovh.json | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/openstack/config/vendors/ovh-us.json b/openstack/config/vendors/ovh-us.json index 535a7ded0..d388d7d91 100644 --- a/openstack/config/vendors/ovh-us.json +++ b/openstack/config/vendors/ovh-us.json @@ -2,7 +2,9 @@ "name": "ovh-us", "profile": { "auth": { - "auth_url": "https://auth.cloud.ovh.us/" + "auth_url": "https://auth.cloud.ovh.us/", + "user_domain_name": "Default", + "project_domain_name": "Default" }, "regions": [ "US-EAST-VA-1" diff --git a/openstack/config/vendors/ovh.json b/openstack/config/vendors/ovh.json index 3548358a1..09e675836 100644 --- a/openstack/config/vendors/ovh.json +++ b/openstack/config/vendors/ovh.json @@ -2,7 +2,9 @@ "name": "ovh", "profile": { "auth": { - "auth_url": "https://auth.cloud.ovh.net/" + "auth_url": "https://auth.cloud.ovh.net/", + "user_domain_name": "Default", + "project_domain_name": "Default" }, "regions": [ "BHS", From 2caaa989cea93e6fb8c6b16f5c8685f30a6e6cc0 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 12 May 2020 09:59:04 +0200 Subject: [PATCH 2669/3836] Extend statistics reporting In some cases it is desired to extend stat metrics with additional tags based on the selected connection (i.e. "environment"). Statsd does not support tags, so add this only to influxdb for now. Fix establishing connection from parameters when passing influx_config. When exception happens, we need to also generate metric to be able to see errors (i.e. timeout happening from Ansible) I know there are no tests for that area so far at all, this will come later. Change-Id: Ie0862f04eb224345559f9092cd0a9d8ffa43bef3 --- doc/source/user/guides/stats.rst | 12 +++ openstack/config/loader.py | 41 ++++----- openstack/connection.py | 8 ++ openstack/proxy.py | 137 ++++++++++++++++++++----------- 4 files changed, 131 insertions(+), 67 deletions(-) diff --git a/doc/source/user/guides/stats.rst b/doc/source/user/guides/stats.rst index d01ef8b4f..537a12707 100644 --- a/doc/source/user/guides/stats.rst +++ b/doc/source/user/guides/stats.rst @@ -57,3 +57,15 @@ Metrics will be reported only when corresponding client libraries ( `statsd` for 'statsd' reporting, `influxdb` for influxdb reporting correspondingly). When those libraries are not available reporting will be silently ignored. + +InfluxDB reporting allows setting additional tags into the metrics based on the +selected cloud. + +.. code-block:: yaml + + clouds: + my_cloud: + profile: some_profile + ... + additional_metric_tags: + environment: production diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 50bc22adf..6e931d663 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -289,25 +289,28 @@ def __init__(self, config_files=None, vendor_files=None, influxdb_cfg = metrics_config.get('influxdb', {}) # Parse InfluxDB configuration - if influxdb_config: - influxdb_cfg.update(influxdb_config) - if influxdb_cfg: - config = {} - if 'use_udp' in influxdb_cfg: - use_udp = influxdb_cfg['use_udp'] - if isinstance(use_udp, str): - use_udp = use_udp.lower() in ('true', 'yes', '1') - elif not isinstance(use_udp, bool): - use_udp = False - self.log.warning('InfluxDB.use_udp value type is not ' - 'supported. Use one of ' - '[true|false|yes|no|1|0]') - config['use_udp'] = use_udp - for key in ['host', 'port', 'username', 'password', 'database', - 'measurement', 'timeout']: - if key in influxdb_cfg: - config[key] = influxdb_cfg[key] - self._influxdb_config = config + if not influxdb_config: + influxdb_config = influxdb_cfg + else: + influxdb_config.update(influxdb_cfg) + + if influxdb_config: + config = {} + if 'use_udp' in influxdb_config: + use_udp = influxdb_config['use_udp'] + if isinstance(use_udp, str): + use_udp = use_udp.lower() in ('true', 'yes', '1') + elif not isinstance(use_udp, bool): + use_udp = False + self.log.warning('InfluxDB.use_udp value type is not ' + 'supported. Use one of ' + '[true|false|yes|no|1|0]') + config['use_udp'] = use_udp + for key in ['host', 'port', 'username', 'password', 'database', + 'measurement', 'timeout']: + if key in influxdb_config: + config[key] = influxdb_config[key] + self._influxdb_config = config if load_envvars: statsd_host = statsd_host or os.environ.get('STATSD_HOST') diff --git a/openstack/connection.py b/openstack/connection.py index 3d39440d4..eaade0c9c 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -435,6 +435,14 @@ def __init__(self, cloud=None, config=None, session=None, self.log.warning('Configured hook %s cannot be executed: %s', vendor_hook, e) + # Add additional metrics into the configuration according to the + # selected connection. We don't want to deal with overall config in the + # proxy, just pass required part. + if (self.config._influxdb_config + and 'additional_metric_tags' in self.config.config): + self.config._influxdb_config['additional_metric_tags'] = \ + self.config.config['additional_metric_tags'] + @property def session(self): if not self._session: diff --git a/openstack/proxy.py b/openstack/proxy.py index dbb246399..a8812de70 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -91,15 +91,23 @@ def request( if conn: # Per-request setting should take precedence global_request_id = conn._global_request_id - response = super(Proxy, self).request( - url, method, - connect_retries=connect_retries, raise_exc=raise_exc, - global_request_id=global_request_id, - **kwargs) - for h in response.history: - self._report_stats(h) - self._report_stats(response) - return response + try: + response = super(Proxy, self).request( + url, method, + connect_retries=connect_retries, raise_exc=raise_exc, + global_request_id=global_request_id, + **kwargs) + for h in response.history: + self._report_stats(h) + self._report_stats(response) + return response + except Exception as e: + # If we want metrics to be generated we also need to generate some + # in case of exceptions as well, so that timeouts and connection + # problems (especially when called from ansible) are being + # generated as well. + self._report_stats(None, url, method, e) + raise def _extract_name(self, url, service_type=None, project_id=None): '''Produce a key name to use in logging/metrics from the URL path. @@ -185,58 +193,91 @@ def _extract_name_consume_url_parts(self, url_parts): return name_parts - def _report_stats(self, response): + def _report_stats(self, response, url=None, method=None, exc=None): if self._statsd_client: - self._report_stats_statsd(response) + self._report_stats_statsd(response, url, method, exc) if self._prometheus_counter and self._prometheus_histogram: - self._report_stats_prometheus(response) + self._report_stats_prometheus(response, url, method, exc) if self._influxdb_client: - self._report_stats_influxdb(response) - - def _report_stats_statsd(self, response): - name_parts = self._extract_name(response.request.url, + self._report_stats_influxdb(response, url, method, exc) + + def _report_stats_statsd(self, response, url=None, method=None, exc=None): + if response is not None and not url: + url = response.request.url + if response is not None and not method: + method = response.request.method + name_parts = self._extract_name(url, self.service_type, self.session.get_project_id()) key = '.'.join( - [self._statsd_prefix, self.service_type, response.request.method] + [self._statsd_prefix, self.service_type, method] + name_parts) - self._statsd_client.timing(key, int( - response.elapsed.microseconds / 1000)) - self._statsd_client.incr(key) - - def _report_stats_prometheus(self, response): - labels = dict( - method=response.request.method, - endpoint=response.request.url, - service_type=self.service_type, - status_code=response.status_code, - ) - self._prometheus_counter.labels(**labels).inc() - self._prometheus_histogram.labels(**labels).observe( - response.elapsed.microseconds / 1000) + if response is not None: + duration = int(response.elapsed.microseconds / 1000) + self._statsd_client.timing(key, duration) + self._statsd_client.incr(key) + elif exc is not None: + self._statsd_client.incr('%s.failed' % key) + + def _report_stats_prometheus(self, response, url=None, method=None, + exc=None): + if response is not None and not url: + url = response.request.url + if response is not None and not method: + method = response.request.method + if response is not None: + labels = dict( + method=method, + endpoint=url, + service_type=self.service_type, + status_code=response.status_code, + ) + self._prometheus_counter.labels(**labels).inc() + self._prometheus_histogram.labels(**labels).observe( + response.elapsed.microseconds / 1000) - def _report_stats_influxdb(self, response): + def _report_stats_influxdb(self, response, url=None, method=None, + exc=None): # NOTE(gtema): status_code is saved both as tag and field to give # ability showing it as a value and not only as a legend. # However Influx is not ok with having same name in tags and fields, # therefore use different names. + if response is not None and not url: + url = response.request.url + if response is not None and not method: + method = response.request.method + tags = dict( + method=method, + name='_'.join(self._extract_name( + url, self.service_type, + self.session.get_project_id())) + ) + fields = dict( + attempted=1 + ) + if response is not None: + fields['duration'] = int(response.elapsed.microseconds / 1000) + tags['status_code'] = str(response.status_code) + # Note(gtema): emit also status_code as a value (counter) + fields[str(response.status_code)] = 1 + fields['%s.%s' % (method, response.status_code)] = 1 + # Note(gtema): status_code field itself is also very helpful on the + # graphs to show what was the code, instead of counting its + # occurences + fields['status_code_val'] = response.status_code + elif exc: + fields['failed'] = 1 + if 'additional_metric_tags' in self._influxdb_config: + tags.update(self._influxdb_config['additional_metric_tags']) + measurement = self._influxdb_config.get( + 'measurement', 'openstack_api') \ + if self._influxdb_config else 'openstack_api' + # Note(gtema) append service name into the measurement name + measurement = '%s.%s' % (measurement, self.service_type) data = [dict( - measurement=(self._influxdb_config.get('measurement', - 'openstack_api') - if self._influxdb_config else 'openstack_api'), - tags=dict( - method=response.request.method, - service_type=self.service_type, - status_code=response.status_code, - name='_'.join(self._extract_name( - response.request.url, self.service_type, - self.session.get_project_id()) - ) - ), - fields=dict( - duration=int(response.elapsed.microseconds / 1000), - status_code_val=int(response.status_code) - ) + measurement=measurement, + tags=tags, + fields=fields )] try: self._influxdb_client.write_points(data) From 452087a7fe9d5bf9cd8f4bdc1aec077a62b11197 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Thu, 21 May 2020 20:27:54 +0200 Subject: [PATCH 2670/3836] Switch to newer openstackdocstheme and reno versions Switch to openstackdocstheme 2.2.1 and reno 3.1.0 versions. Using these versions will allow especially: * Linking from HTML to PDF document * Allow parallel building of documents * Fix some rendering problems Update Sphinx version as well. Set openstackdocs_pdf_link to link to PDF file. Note that the link to the published document only works on docs.openstack.org where the PDF file is placed in the top-level html directory. The site-preview places the PDF in a pdf directory. Change pygments_style to 'native' since old theme version always used 'native' and the theme now respects the setting and using 'sphinx' can lead to some strange rendering. openstackdocstheme renames some variables, so follow the renames before the next release removes them. A couple of variables are also not needed anymore, remove them. See also http://lists.openstack.org/pipermail/openstack-discuss/2020-May/014971.html Change-Id: I334380cd63f408c8307fec06e8569b599d23e2f9 --- doc/requirements.txt | 6 +++--- doc/source/conf.py | 8 ++++---- releasenotes/source/conf.py | 7 +++---- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index a1d8824e7..33acd8b59 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,9 +1,9 @@ # 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. -sphinx>=1.8.0,!=2.1.0 # BSD +sphinx>=2.0.0,!=2.1.0 # BSD docutils>=0.11 # OSI-Approved Open Source, Public Domain -openstackdocstheme>=1.20.0 # Apache-2.0 +openstackdocstheme>=2.2.1 # Apache-2.0 beautifulsoup4>=4.6.0 # MIT -reno>=2.5.0 # Apache-2.0 +reno>=3.1.0 # Apache-2.0 sphinxcontrib-svg2pdfconverter>=0.1.0 # BSD diff --git a/doc/source/conf.py b/doc/source/conf.py index d29b02ab6..e74947ae5 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -30,9 +30,9 @@ ] # openstackdocstheme options -repository_name = 'openstack/openstacksdk' -bug_project = '972' -bug_tag = '' +openstackdocs_repo_name = 'openstack/openstacksdk' +openstackdocs_pdf_link = True +openstackdocs_use_storyboard = True html_theme = 'openstackdocs' # TODO(shade) Set this to true once the build-openstack-sphinx-docs job is @@ -64,7 +64,7 @@ add_module_names = True # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = 'native' autodoc_member_order = "bysource" diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py index f2f9c186e..e21e447b5 100644 --- a/releasenotes/source/conf.py +++ b/releasenotes/source/conf.py @@ -43,9 +43,8 @@ ] # openstackdocstheme options -repository_name = 'openstack/openstacksdk' -bug_project = '760' -bug_tag = '' +openstackdocs_repo_name = 'openstack/openstacksdk' +openstackdocs_use_storyboard = True # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -98,7 +97,7 @@ # show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = 'native' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] From 632db25246ffc16632491c0efd432b80c5ce9a95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herv=C3=A9=20Beraud?= Date: Tue, 26 May 2020 10:31:57 +0200 Subject: [PATCH 2671/3836] Refresh python versions Introduce support of python 3.8 and move tox and jobs to py38. jsonschema 3.2.0 [1] support python 3.8 [1] https://github.com/Julian/jsonschema/pull/627 Change-Id: Ibcfa044dd0f3b29fd290559795ea1d98e194e886 --- .zuul.yaml | 10 +++++----- lower-constraints.txt | 2 +- setup.cfg | 1 + test-requirements.txt | 2 +- tox.ini | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 69819aed7..2777e2eeb 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -1,8 +1,8 @@ - job: - name: openstacksdk-tox-py36-tips - parent: openstack-tox-py36 + name: openstacksdk-tox-py38-tips + parent: openstack-tox-py38 description: | - Run tox python 36 unittests against master of important libs + Run tox python 38 unittests against master of important libs vars: tox_install_siblings: true zuul_work_dir: src/opendev.org/openstack/openstacksdk @@ -17,10 +17,10 @@ name: openstacksdk-tox-tips check: jobs: - - openstacksdk-tox-py36-tips + - openstacksdk-tox-py38-tips gate: jobs: - - openstacksdk-tox-py36-tips + - openstacksdk-tox-py38-tips - job: name: openstacksdk-functional-devstack-minimum diff --git a/lower-constraints.txt b/lower-constraints.txt index dd0e02353..f643a2712 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -10,7 +10,7 @@ iso8601==0.1.11 jmespath==0.9.0 jsonpatch==1.16 jsonpointer==1.13 -jsonschema==2.6.0 +jsonschema==3.2.0 keystoneauth1==3.18.0 linecache2==1.0.0 mock==3.0.0 diff --git a/setup.cfg b/setup.cfg index 07dd21493..b969c7750 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,6 +17,7 @@ classifier = Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 python-requires = >=3.5 [files] diff --git a/test-requirements.txt b/test-requirements.txt index 9a347b84f..75ea92bc7 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -6,7 +6,7 @@ hacking>=3.0,<3.1.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 ddt>=1.0.1 # MIT fixtures>=3.0.0 # Apache-2.0/BSD -jsonschema>=2.6.0 # MIT +jsonschema>=3.2.0 # MIT mock>=3.0.0 # BSD prometheus-client>=0.4.2 # Apache-2.0 oslo.config>=6.1.0 # Apache-2.0 diff --git a/tox.ini b/tox.ini index e8809b843..8ddc7c2a5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 3.1 -envlist = pep8,py37 +envlist = pep8,py38 skipsdist = True ignore_basepython_conflict = True From 6cab3a619b431da336d3221807c7369fa4e83d1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Vale?= Date: Sat, 4 Apr 2020 02:56:15 +0100 Subject: [PATCH 2672/3836] Fix deleting stacks by id when waiting for result When we want to wait for the result (wait=True), deleting a stack by name works as expected. However, if deleting by id the stack is still deleted but we get an error while polling events because no event is ever found. This happens because the poller always compares the resource_name attribute, which will never match the id. This also required updating the tests as the fake events are always created with the id as the resource_name and they assume that the function is always called with the stack id and never the stack name. Change-Id: Icd3a831faf14a45b1e492a0088160e519b5c6a02 Task: 30189 Story: 2005301 --- openstack/orchestration/util/event_utils.py | 3 +- openstack/tests/unit/cloud/test_stack.py | 61 ++++++++++++++++++++- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/openstack/orchestration/util/event_utils.py b/openstack/orchestration/util/event_utils.py index 5d56bf66a..6ae36a39d 100644 --- a/openstack/orchestration/util/event_utils.py +++ b/openstack/orchestration/util/event_utils.py @@ -60,7 +60,8 @@ def stop_check_no_action(a): msg_template = "\n Stack %(name)s %(status)s \n" def is_stack_event(event): - if event.get('resource_name', '') != stack_name: + if (event.get('resource_name', '') != stack_name + and event.get('physical_resource_id', '') != stack_name): return False phys_id = event.get('physical_resource_id', '') diff --git a/openstack/tests/unit/cloud/test_stack.py b/openstack/tests/unit/cloud/test_stack.py index 7ee2047d3..72335f740 100644 --- a/openstack/tests/unit/cloud/test_stack.py +++ b/openstack/tests/unit/cloud/test_stack.py @@ -183,9 +183,66 @@ def test_delete_stack_exception(self): self.cloud.delete_stack(self.stack_id) self.assert_calls() - def test_delete_stack_wait(self): + def test_delete_stack_by_name_wait(self): marker_event = fakes.make_fake_stack_event( - self.stack_id, self.stack_name, status='CREATE_COMPLETE') + self.stack_id, self.stack_name, status='CREATE_COMPLETE', + resource_name='name') + marker_qs = 'marker={e_id}&sort_dir=asc'.format( + e_id=marker_event['id']) + resolve = 'resolve_outputs=False' + self.register_uris([ + dict(method='GET', + uri='{endpoint}/stacks/{name}?{resolve}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name, + resolve=resolve), + status_code=302, + headers=dict( + location='{endpoint}/stacks/{name}/{id}?{resolve}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, name=self.stack_name, + resolve=resolve))), + dict(method='GET', + uri='{endpoint}/stacks/{name}/{id}?{resolve}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, name=self.stack_name, resolve=resolve), + json={"stack": self.stack}), + dict(method='GET', + uri='{endpoint}/stacks/{name}/events?{qs}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name, + qs='limit=1&sort_dir=desc'), + complete_qs=True, + json={"events": [marker_event]}), + dict(method='DELETE', + uri='{endpoint}/stacks/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id)), + dict(method='GET', + uri='{endpoint}/stacks/{name}/events?{qs}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name, + qs=marker_qs), + complete_qs=True, + json={"events": [ + fakes.make_fake_stack_event( + self.stack_id, self.stack_name, + status='DELETE_COMPLETE', resource_name='name'), + ]}), + dict(method='GET', + uri='{endpoint}/stacks/{name}?{resolve}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, name=self.stack_name, resolve=resolve), + status_code=404), + ]) + + self.assertTrue(self.cloud.delete_stack(self.stack_name, wait=True)) + self.assert_calls() + + def test_delete_stack_by_id_wait(self): + marker_event = fakes.make_fake_stack_event( + self.stack_id, self.stack_name, status='CREATE_COMPLETE', + resource_name='name') marker_qs = 'marker={e_id}&sort_dir=asc'.format( e_id=marker_event['id']) resolve = 'resolve_outputs=False' From b1ec4174e76d5876e17749a6dabb07cc4e919c32 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Wed, 27 May 2020 17:57:16 +0200 Subject: [PATCH 2673/3836] baremetal: use proxy methods in unregister_machine Has a nice side effect of no longer failing when a machine or any port do not exist. Also stop using a deprecated method. The wait argument is deprecated since it never had any effect: node deletion is synchronous. Change-Id: I63ea929540f22c2b73faf4a1f767e30ecc1dd5dd --- openstack/cloud/_baremetal.py | 51 +++++------------ .../tests/unit/cloud/test_baremetal_node.py | 55 +------------------ 2 files changed, 15 insertions(+), 91 deletions(-) diff --git a/openstack/cloud/_baremetal.py b/openstack/cloud/_baremetal.py index b3921c55a..8e8986cce 100644 --- a/openstack/cloud/_baremetal.py +++ b/openstack/cloud/_baremetal.py @@ -325,7 +325,7 @@ def register_machine(self, nics, wait=False, timeout=3600, else: return machine - def unregister_machine(self, nics, uuid, wait=False, timeout=600): + def unregister_machine(self, nics, uuid, wait=None, timeout=600): """Unregister Baremetal from Ironic Removes entries for Network Interfaces and baremetal nodes @@ -335,15 +335,17 @@ def unregister_machine(self, nics, uuid, wait=False, timeout=600): to be removed. :param string uuid: The UUID of the node to be deleted. - :param wait: Boolean value, defaults to false, if to block the method - upon the final step of unregistering the machine. + :param wait: DEPRECATED, do not use. :param timeout: Integer value, representing seconds with a default value of 600, which controls the maximum amount of - time to block the method's completion on. + time to block until a lock is released on machine. :raises: OpenStackCloudException on operation failure. """ + if wait is not None: + warnings.warn("wait argument is deprecated and has no effect", + DeprecationWarning) machine = self.get_machine(uuid) invalid_states = ['active', 'cleaning', 'clean wait', 'clean failed'] @@ -357,47 +359,20 @@ def unregister_machine(self, nics, uuid, wait=False, timeout=600): # previously concealed by exception retry logic that detected the # failure, and resubitted the request in python-ironicclient. try: - self.wait_for_baremetal_node_lock(machine, timeout=timeout) + self.baremetal.wait_for_node_reservation(machine, timeout) except exc.OpenStackCloudException as e: raise exc.OpenStackCloudException( "Error unregistering node '%s': Exception occured while" " waiting to be able to proceed: %s" % (machine['uuid'], e)) for nic in nics: - port_msg = ("Error removing NIC {nic} from baremetal API for " - "node {uuid}").format(nic=nic, uuid=uuid) - port_url = '/ports/detail?address={mac}'.format(mac=nic['mac']) - port = self._baremetal_client.get(port_url, microversion=1.6, - error_message=port_msg) - port_url = '/ports/{uuid}'.format(uuid=port['ports'][0]['uuid']) - _utils._call_client_and_retry(self._baremetal_client.delete, - port_url, retry_on=[409, 503], - error_message=port_msg) - - with _utils.shade_exceptions( - "Error unregistering machine {node_id} from the baremetal " - "API".format(node_id=uuid)): - - # NOTE(TheJulia): While this should not matter microversion wise, - # ironic assumes all calls without an explicit microversion to be - # version 1.0. Ironic expects to deprecate support for older - # microversions in future releases, as such, we explicitly set - # the version to what we have been using with the client library.. - version = "1.6" - msg = "Baremetal machine failed to be deleted" - url = '/nodes/{node_id}'.format( - node_id=uuid) - _utils._call_client_and_retry(self._baremetal_client.delete, - url, retry_on=[409, 503], - error_message=msg, - microversion=version) + try: + port = next(self.baremetal.ports(address=nic['mac'])) + except StopIteration: + continue + self.baremetal.delete_port(port.id) - if wait: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for machine to be deleted"): - if not self.get_machine(uuid): - break + self.baremetal.delete_node(uuid) def patch_machine(self, name_or_id, patch): """Patch Machine Information diff --git a/openstack/tests/unit/cloud/test_baremetal_node.py b/openstack/tests/unit/cloud/test_baremetal_node.py index 6c7905eed..d18c940bf 100644 --- a/openstack/tests/unit/cloud/test_baremetal_node.py +++ b/openstack/tests/unit/cloud/test_baremetal_node.py @@ -1482,7 +1482,6 @@ def test_unregister_machine(self): port_uuid = self.fake_baremetal_port['uuid'] # NOTE(TheJulia): The two values below should be the same. port_node_uuid = self.fake_baremetal_port['node_uuid'] - port_url_address = 'detail?address=%s' % mac_address self.fake_baremetal_node['provision_state'] = 'available' self.register_uris([ dict( @@ -1495,7 +1494,7 @@ def test_unregister_machine(self): method='GET', uri=self.get_mock_url( resource='ports', - append=[port_url_address]), + qs_elements=['address=%s' % mac_address]), json={'ports': [{'address': mac_address, 'node_uuid': port_node_uuid, 'uuid': port_uuid}]}), @@ -1516,55 +1515,6 @@ def test_unregister_machine(self): self.assert_calls() - def test_unregister_machine_timeout(self): - mac_address = self.fake_baremetal_port['address'] - nics = [{'mac': mac_address}] - port_uuid = self.fake_baremetal_port['uuid'] - port_node_uuid = self.fake_baremetal_port['node_uuid'] - port_url_address = 'detail?address=%s' % mac_address - self.fake_baremetal_node['provision_state'] = 'available' - self.register_uris([ - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - dict( - method='GET', - uri=self.get_mock_url( - resource='ports', - append=[port_url_address]), - json={'ports': [{'address': mac_address, - 'node_uuid': port_node_uuid, - 'uuid': port_uuid}]}), - dict( - method='DELETE', - uri=self.get_mock_url( - resource='ports', - append=[self.fake_baremetal_port['uuid']])), - dict( - method='DELETE', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']])), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - ]) - self.assertRaises( - exc.OpenStackCloudException, - self.cloud.unregister_machine, - nics, - self.fake_baremetal_node['uuid'], - wait=True, - timeout=0.001) - - self.assert_calls() - def test_unregister_machine_locked_timeout(self): mac_address = self.fake_baremetal_port['address'] nics = [{'mac': mac_address}] @@ -1598,7 +1548,6 @@ def test_unregister_machine_retries(self): port_uuid = self.fake_baremetal_port['uuid'] # NOTE(TheJulia): The two values below should be the same. port_node_uuid = self.fake_baremetal_port['node_uuid'] - port_url_address = 'detail?address=%s' % mac_address self.fake_baremetal_node['provision_state'] = 'available' self.register_uris([ dict( @@ -1611,7 +1560,7 @@ def test_unregister_machine_retries(self): method='GET', uri=self.get_mock_url( resource='ports', - append=[port_url_address]), + qs_elements=['address=%s' % mac_address]), json={'ports': [{'address': mac_address, 'node_uuid': port_node_uuid, 'uuid': port_uuid}]}), From f39fe6eb916079529ce0538afa36a7ba529b1bee Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 14 May 2020 15:19:01 +0200 Subject: [PATCH 2674/3836] Fix AttributeError exception during authorization When we can't authorize at minimum wrap keystoneauth1 error, instead of generating new problem by looking into not existing property of the exception. This error masks the cause of real failure resulting in huge stack traces on connection problems (epsecially valid for Ansible modules). Change-Id: I8548fa6fc7daa4101e86c839ec3c7b44fed29511 --- openstack/connection.py | 2 +- openstack/tests/unit/test_connection.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openstack/connection.py b/openstack/connection.py index 3d39440d4..1194a5154 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -498,7 +498,7 @@ def authorize(self): try: return self.session.get_token() except keystoneauth1.exceptions.ClientException as e: - raise exceptions.raise_from_response(e.response) + raise exceptions.SDKException(e) @property def _pool_executor(self): diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index 8efc6728e..15ae14de3 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -332,7 +332,7 @@ def test_authorize_works(self): def test_authorize_failure(self): self.use_broken_keystone() - self.assertRaises(openstack.exceptions.HttpException, + self.assertRaises(openstack.exceptions.SDKException, self.cloud.authorize) From 023040ec4286215095378fd8788cb747b6921206 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herv=C3=A9=20Beraud?= Date: Thu, 28 May 2020 19:32:25 +0200 Subject: [PATCH 2675/3836] Add some unit tests for config.loader Add some unit tests that aim to test yaml/json files reading and permissions. Change-Id: I3bdc2f141cba83e43b9349ae0cd0661464ad8e7a --- openstack/tests/unit/config/test_loader.py | 117 +++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 openstack/tests/unit/config/test_loader.py diff --git a/openstack/tests/unit/config/test_loader.py b/openstack/tests/unit/config/test_loader.py new file mode 100644 index 000000000..9772d1a91 --- /dev/null +++ b/openstack/tests/unit/config/test_loader.py @@ -0,0 +1,117 @@ +# Copyright 2020 Red Hat, 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 os +import tempfile +import textwrap + +from openstack.config import loader +from openstack.tests.unit.config import base + +FILES = { + 'yaml': textwrap.dedent(''' + foo: bar + baz: + - 1 + - 2 + - 3 + '''), + 'json': textwrap.dedent(''' + { + "foo": "bar", + "baz": [ + 1, + 2, + 3 + ] + } + '''), + 'txt': textwrap.dedent(''' + foo + bar baz + test + one two + '''), +} + + +class TestLoader(base.TestCase): + + def test_base_load_yaml_json_file(self): + with tempfile.TemporaryDirectory() as tmpdir: + tested_files = [] + for key, value in FILES.items(): + fn = os.path.join(tmpdir, 'file.{ext}'.format(ext=key)) + with open(fn, 'w+') as fp: + fp.write(value) + tested_files.append(fn) + + path, result = loader.OpenStackConfig()._load_yaml_json_file( + tested_files) + # NOTE(hberaud): Prefer to test path rather than file because + # our FILES var is a dict so results are appened + # without keeping the initial order (python 3.5) + self.assertEqual(tmpdir, os.path.dirname(path)) + + def test__load_yaml_json_file_without_json(self): + with tempfile.TemporaryDirectory() as tmpdir: + tested_files = [] + for key, value in FILES.items(): + if key == 'json': + continue + fn = os.path.join(tmpdir, 'file.{ext}'.format(ext=key)) + with open(fn, 'w+') as fp: + fp.write(value) + tested_files.append(fn) + + path, result = loader.OpenStackConfig()._load_yaml_json_file( + tested_files) + # NOTE(hberaud): Prefer to test path rather than file because + # our FILES var is a dict so results are appened + # without keeping the initial order (python 3.5) + self.assertEqual(tmpdir, os.path.dirname(path)) + + def test__load_yaml_json_file_without_json_yaml(self): + with tempfile.TemporaryDirectory() as tmpdir: + tested_files = [] + fn = os.path.join(tmpdir, 'file.txt') + with open(fn, 'w+') as fp: + fp.write(FILES['txt']) + tested_files.append(fn) + + path, result = loader.OpenStackConfig()._load_yaml_json_file( + tested_files) + self.assertEqual(fn, path) + + def test__load_yaml_json_file_without_perm(self): + with tempfile.TemporaryDirectory() as tmpdir: + tested_files = [] + fn = os.path.join(tmpdir, 'file.txt') + with open(fn, 'w+') as fp: + fp.write(FILES['txt']) + os.chmod(fn, 222) + tested_files.append(fn) + + path, result = loader.OpenStackConfig()._load_yaml_json_file( + tested_files) + self.assertEqual(None, path) + + def test__load_yaml_json_file_nonexisting(self): + tested_files = [] + fn = os.path.join('/fake', 'file.txt') + tested_files.append(fn) + + path, result = loader.OpenStackConfig()._load_yaml_json_file( + tested_files) + self.assertEqual(None, path) From 7e0dcaaa4a69b17b97e746ce8de104689c60becc Mon Sep 17 00:00:00 2001 From: Sagi Shnaidman Date: Sun, 7 Jun 2020 21:02:22 +0300 Subject: [PATCH 2676/3836] Make optional name and admin password Nova API doesn't require name and admin password as mandatory arguments for rebuilding server. Make them optional for SDK too. Change-Id: I002101b21827005a7945fcc3669ccca1481204a5 --- openstack/compute/v2/server.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 7064d8be8..14e4ccfd9 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -260,18 +260,20 @@ def force_delete(self, session): body = {'forceDelete': None} self._action(session, body) - def rebuild(self, session, name, admin_password, + def rebuild(self, session, name=None, admin_password=None, preserve_ephemeral=False, image=None, access_ipv4=None, access_ipv6=None, metadata=None, personality=None): """Rebuild the server with the given arguments.""" action = { - 'name': name, - 'adminPass': admin_password, 'preserve_ephemeral': preserve_ephemeral } if image is not None: action['imageRef'] = resource.Resource._get_id(image) + if name is not None: + action['name'] = name + if admin_password is not None: + action['adminPass'] = admin_password if access_ipv4 is not None: action['accessIPv4'] = access_ipv4 if access_ipv6 is not None: From 35dda579f0cc1c14c332bfa5ff853720c612ff79 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 8 Jun 2020 08:45:03 +0200 Subject: [PATCH 2677/3836] Wait for the project cleanup to complete Currently the DAG will wait for the tasks to complete, except the last group (which do not have other depends_on). This is bad from the OSC, since we don't really know when it completes. Therefore let us try to wait here until DAG is done. Change-Id: I4c9af65de404fce62f25a4d453aabfcc9734e2a3 --- openstack/cloud/openstackcloud.py | 7 +++++++ openstack/utils.py | 3 +++ 2 files changed, 10 insertions(+) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index fefdd9ba1..76fb26df4 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -798,6 +798,13 @@ def project_cleanup(self, dry_run=True, else: dep_graph.node_done(service) + for count in utils.iterate_timeout( + timeout=wait_timeout, + message="Timeout waiting for cleanup to finish", + wait=1): + if dep_graph.is_complete(): + return + def cleanup_task(graph, service, fn): fn() diff --git a/openstack/utils.py b/openstack/utils.py index 962113fd7..3090c2cbf 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -297,3 +297,6 @@ def topological_sort(self): def size(self): return len(self._graph.keys()) + + def is_complete(self): + return len(self._done) == self.size() From 8ce2b1c97deabd04143ae39b8f625dd760a17598 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 8 Jun 2020 08:49:55 +0200 Subject: [PATCH 2678/3836] Do not clean keypairs in the project cleanup We can delete only keypairs of the user, which is invoking project cleanup, while others will remain. On the other hand we might also drop keypairs, which are still active in other projects, since KP is not project related, but user related. Change-Id: Iaef2ff204c2e65feda8cf30c3e5497e22dc33059 --- openstack/compute/v2/_proxy.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index a897509eb..6a40c6930 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -1486,9 +1486,3 @@ def _service_cleanup(self, dry_run=True, status_queue=None): obj, dry_run, status_queue) - - for obj in self.keypairs(): - self._service_cleanup_del_res(self.delete_keypair, - obj, - dry_run, - status_queue) From 2cf5ff98900b37aced48bb14ada6b1fdcd55437d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 8 Jun 2020 14:36:37 -0500 Subject: [PATCH 2679/3836] Add user_projects method to docs index Change-Id: I8b5b6199fa4de4c32743ab8ce31e496964416699 --- doc/source/user/proxies/identity_v3.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/source/user/proxies/identity_v3.rst b/doc/source/user/proxies/identity_v3.rst index 8153f6c09..e8f21f22a 100644 --- a/doc/source/user/proxies/identity_v3.rst +++ b/doc/source/user/proxies/identity_v3.rst @@ -105,4 +105,5 @@ User Operations .. autoclass:: openstack.identity.v3._proxy.Proxy :noindex: - :members: create_user, update_user, delete_user, get_user, find_user, users + :members: create_user, update_user, delete_user, get_user, find_user, users, + user_projects From a0a9f4687f5f603c5f6ea414dc1547684e03b9ad Mon Sep 17 00:00:00 2001 From: Vishakha Agarwal Date: Wed, 10 Jun 2020 12:37:46 +0530 Subject: [PATCH 2680/3836] NIT: Fix application credential Change-Id: Ibe2a44a174dfa84019c73d65be1567febbe512c1 --- openstack/identity/v3/_proxy.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 88261adf6..3e23ffe3c 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -1225,7 +1225,7 @@ def application_credentials(self, user, **query): :class:`~openstack.identity.v3.user.User` instance. :param kwargs query: Optional query parameters to be sent to - application credential the resources being returned. + limit the resources being returned. :returns: A generator of application credentials instances. :rtype: :class:`~openstack.identity.v3.application_credential. @@ -1285,7 +1285,7 @@ def find_application_credential(self, user, name_or_id, :param user: Either the ID of a user or a :class:`~openstack.identity.v3.user.User` instance. - :param name_or_id: The name or ID of a application credential. + :param name_or_id: The name or ID of an application credential. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the resource does not exist. @@ -1302,7 +1302,7 @@ def find_application_credential(self, user, name_or_id, def delete_application_credential(self, user, application_credential, ignore_missing=True): - """Delete a application credential + """Delete an application credential :param user: Either the ID of a user or a :class:`~openstack.identity.v3.user.User` instance. From 8b5d4fe7985d3f9319b23121e290ef96dc801863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Jens=C3=A5s?= Date: Mon, 15 Jun 2020 16:07:37 +0200 Subject: [PATCH 2681/3836] Add query parameter 'id' for security_groups Add parity with neutronclient which could list security groups filtering by one or multiple security group ids. Change-Id: Iabafad39fe2b54a0c5fa6fe23f1e8ce3c0eab991 --- openstack/network/v2/_proxy.py | 1 + openstack/network/v2/security_group.py | 4 ++-- .../tests/functional/network/v2/test_security_group.py | 4 ++++ openstack/tests/unit/network/v2/test_security_group.py | 1 + ...curity-group-query-parameter-id-f6dda45b2c09dbaa.yaml | 9 +++++++++ 5 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/network-security-group-query-parameter-id-f6dda45b2c09dbaa.yaml diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 1b4a40534..dcca6fd73 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -3150,6 +3150,7 @@ def security_groups(self, **query): the resources being returned. Valid parameters are: * ``description``: Security group description + * ``ìd``: The id of a security group, or list of security group ids * ``name``: The name of a security group * ``project_id``: The ID of the project this security group is associated with. diff --git a/openstack/network/v2/security_group.py b/openstack/network/v2/security_group.py index 5c9cc2d97..ee35f29c8 100644 --- a/openstack/network/v2/security_group.py +++ b/openstack/network/v2/security_group.py @@ -27,8 +27,8 @@ class SecurityGroup(_base.NetworkResource, resource.TagMixin): allow_list = True _query_mapping = resource.QueryParameters( - 'description', 'fields', 'name', 'stateful', 'project_id', 'tenant_id', - 'revision_number', 'sort_dir', 'sort_key', + 'description', 'fields', 'id', 'name', 'stateful', 'project_id', + 'tenant_id', 'revision_number', 'sort_dir', 'sort_key', **resource.TagMixin._tag_query_parameters ) diff --git a/openstack/tests/functional/network/v2/test_security_group.py b/openstack/tests/functional/network/v2/test_security_group.py index 5bcdb3126..08049dcef 100644 --- a/openstack/tests/functional/network/v2/test_security_group.py +++ b/openstack/tests/functional/network/v2/test_security_group.py @@ -46,6 +46,10 @@ def test_list(self): names = [o.name for o in self.conn.network.security_groups()] self.assertIn(self.NAME, names) + def test_list_query_list_of_ids(self): + ids = [o.id for o in self.conn.network.security_groups(id=[self.ID])] + self.assertIn(self.ID, ids) + def test_set_tags(self): sot = self.conn.network.get_security_group(self.ID) self.assertEqual([], sot.tags) diff --git a/openstack/tests/unit/network/v2/test_security_group.py b/openstack/tests/unit/network/v2/test_security_group.py index 82cda9845..661f8aa49 100644 --- a/openstack/tests/unit/network/v2/test_security_group.py +++ b/openstack/tests/unit/network/v2/test_security_group.py @@ -80,6 +80,7 @@ def test_basic(self): self.assertDictEqual({'any_tags': 'tags-any', 'description': 'description', 'fields': 'fields', + 'id': 'id', 'limit': 'limit', 'marker': 'marker', 'name': 'name', diff --git a/releasenotes/notes/network-security-group-query-parameter-id-f6dda45b2c09dbaa.yaml b/releasenotes/notes/network-security-group-query-parameter-id-f6dda45b2c09dbaa.yaml new file mode 100644 index 000000000..5b0db6808 --- /dev/null +++ b/releasenotes/notes/network-security-group-query-parameter-id-f6dda45b2c09dbaa.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + The ``id`` field was added a query parameter for security_groups. A single + security group id, or a list of security group ids can be passed. For + example:: + + conn.network.security_groups(id=['f959e85a-1a87-4b5c-ae56-dc917ceeb584', + 'a55c0100-7ded-40af-9c61-1d1b9a9c2692']) From 0d691f78e12798f56ecad4588554e4da769b50e4 Mon Sep 17 00:00:00 2001 From: subham rai Date: Thu, 9 Apr 2020 21:50:16 +0530 Subject: [PATCH 2682/3836] baremetal: support for volume connectors API adding volume connectors feature. Change-Id: Id021456e4b52928bd6303a8d400a75de8d1cbf01 Story: #2007416 Task: #39036 Signed-off-by: subham rai --- doc/source/user/proxies/baremetal.rst | 8 + doc/source/user/resources/baremetal/index.rst | 1 + .../baremetal/v1/volume_connector.rst | 13 ++ openstack/baremetal/v1/_proxy.py | 143 ++++++++++++++++ openstack/baremetal/v1/volume_connector.py | 56 ++++++ openstack/tests/functional/baremetal/base.py | 11 ++ .../test_baremetal_volume_connector.py | 161 ++++++++++++++++++ .../tests/unit/baremetal/v1/test_proxy.py | 25 +++ .../baremetal/v1/test_volume_connector.py | 61 +++++++ ...volume_connector-api-f001e6f5fc4d1688.yaml | 4 + 10 files changed, 483 insertions(+) create mode 100644 doc/source/user/resources/baremetal/v1/volume_connector.rst create mode 100644 openstack/baremetal/v1/volume_connector.py create mode 100644 openstack/tests/functional/baremetal/test_baremetal_volume_connector.py create mode 100644 openstack/tests/unit/baremetal/v1/test_volume_connector.py create mode 100644 releasenotes/notes/volume_connector-api-f001e6f5fc4d1688.yaml diff --git a/doc/source/user/proxies/baremetal.rst b/doc/source/user/proxies/baremetal.rst index c38a72045..04538c39e 100644 --- a/doc/source/user/proxies/baremetal.rst +++ b/doc/source/user/proxies/baremetal.rst @@ -61,6 +61,14 @@ Allocation Operations update_allocation, patch_allocation, delete_allocation, wait_for_allocation +Volume Connector Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. autoclass:: openstack.baremetal.v1._proxy.Proxy + :noindex: + :members: volume_connectors, find_volume_connector, get_volume_connector, + create_volume_connector, update_volume_connector, + patch_volume_connector, delete_volume_connector + Utilities --------- diff --git a/doc/source/user/resources/baremetal/index.rst b/doc/source/user/resources/baremetal/index.rst index 3f7c56fa0..eccda6908 100644 --- a/doc/source/user/resources/baremetal/index.rst +++ b/doc/source/user/resources/baremetal/index.rst @@ -10,3 +10,4 @@ Baremetal Resources v1/port v1/port_group v1/allocation + v1/volume_connector diff --git a/doc/source/user/resources/baremetal/v1/volume_connector.rst b/doc/source/user/resources/baremetal/v1/volume_connector.rst new file mode 100644 index 000000000..3ffae2212 --- /dev/null +++ b/doc/source/user/resources/baremetal/v1/volume_connector.rst @@ -0,0 +1,13 @@ +openstack.baremetal.v1.volume_connector +======================================= + +.. automodule:: openstack.baremetal.v1.volume_connector + +The VolumeConnector Class +------------------------- + +The ``VolumeConnector`` class inherits +from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.baremetal.v1.volume_connector.VolumeConnector + :members: diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 0c4e5b4e9..903e584c3 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -17,6 +17,7 @@ from openstack.baremetal.v1 import node as _node from openstack.baremetal.v1 import port as _port from openstack.baremetal.v1 import port_group as _portgroup +from openstack.baremetal.v1 import volume_connector as _volumeconnector from openstack import exceptions from openstack import proxy from openstack import utils @@ -1003,3 +1004,145 @@ def set_node_traits(self, node, traits): """ res = self._get_resource(_node.Node, node) return res.set_traits(self, traits) + + def volume_connectors(self, details=False, **query): + """Retrieve a generator of volume_connector. + + :param details: A boolean indicating whether the detailed information + for every volume_connector should be returned. + :param dict query: Optional query parameters to be sent to restrict + the volume_connectors returned. Available parameters include: + + * ``fields``: A list containing one or more fields to be returned + in the response. This may lead to some performance gain + because other fields of the resource are not refreshed. + * ``limit``: Requests at most the specified number of + volume_connector be returned from the query. + * ``marker``: Specifies the ID of the last-seen volume_connector. + Use the ``limit`` parameter to make an initial limited request + and use the ID of the last-seen volume_connector from the + response as the ``marker`` value in subsequent limited request. + * ``node``:only return the ones associated with this specific node + (name or UUID), or an empty set if not found. + * ``sort_dir``:Sorts the response by the requested sort direction. + A valid value is ``asc`` (ascending) or ``desc`` + (descending). Default is ``asc``. You can specify multiple + pairs of sort key and sort direction query parameters. If + you omit the sort direction in a pair, the API uses the + natural sorting direction of the server attribute that is + provided as the ``sort_key``. + * ``sort_key``: Sorts the response by the this attribute value. + Default is ``id``. You can specify multiple pairs of sort + key and sort direction query parameters. If you omit the + sort direction in a pair, the API uses the natural sorting + direction of the server attribute that is provided as the + ``sort_key``. + + :returns: A generator of volume_connector instances. + """ + if details: + query['detail'] = True + return _volumeconnector.VolumeConnector.list(self, **query) + + def create_volume_connector(self, **attrs): + """Create a new volume_connector from attributes. + + :param dict attrs: Keyword arguments that will be used to create a + :class: + `~openstack.baremetal.v1.volume_connector.VolumeConnector`. + + :returns: The results of volume_connector creation. + :rtype::class: + `~openstack.baremetal.v1.volume_connector.VolumeConnector`. + """ + return self._create(_volumeconnector.VolumeConnector, **attrs) + + def find_volume_connector(self, vc_id, ignore_missing=True): + """Find a single volume connector. + + :param str vc_id: The ID of a volume connector. + + :param bool ignore_missing: When set to ``False``, an exception of + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the volume connector does not exist. When set to `True``, + None will be returned when attempting to find a nonexistent + volume connector. + :returns: One :class: + `~openstack.baremetal.v1.volumeconnector.VolumeConnector` + object or None. + """ + return self._find(_volumeconnector.VolumeConnector, vc_id, + ignore_missing=ignore_missing) + + def get_volume_connector(self, volume_connector, fields=None): + """Get a specific volume_connector. + + :param volume_connector: The value can be the ID of a + volume_connector or a :class: + `~openstack.baremetal.v1.volume_connector.VolumeConnector + instance.` + :param fields: Limit the resource fields to fetch.` + + :returns: One + :class: + `~openstack.baremetal.v1.volume_connector.VolumeConnector` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + volume_connector matching the name or ID could be found.` + """ + return self._get_with_fields(_volumeconnector.VolumeConnector, + volume_connector, + fields=fields) + + def update_volume_connector(self, volume_connector, **attrs): + """Update a volume_connector. + + :param volume_connector:Either the ID of a volume_connector + or an instance of + :class:`~openstack.baremetal.v1.volume_connector.VolumeConnector.` + :param dict attrs: The attributes to update on the + volume_connector represented by the ``volume_connector`` parameter.` + + :returns: The updated volume_connector. + :rtype::class: + `~openstack.baremetal.v1.volume_connector.VolumeConnector.` + """ + return self._update(_volumeconnector.VolumeConnector, + volume_connector, **attrs) + + def patch_volume_connector(self, volume_connector, patch): + """Apply a JSON patch to the volume_connector. + + :param volume_connector: The value can be the ID of a + volume_connector or a :class: + `~openstack.baremetal.v1.volume_connector.VolumeConnector` + instance. + :param patch: JSON patch to apply. + + :returns: The updated volume_connector. + :rtype::class: + `~openstack.baremetal.v1.volume_connector.VolumeConnector.` + """ + return self._get_resource(_volumeconnector.VolumeConnector, + volume_connector).patch(self, patch) + + def delete_volume_connector(self, volume_connector, + ignore_missing=True): + """Delete an volume_connector. + + :param volume_connector: The value can be either the ID of a + volume_connector.VolumeConnector or a + :class: + `~openstack.baremetal.v1.volume_connector.VolumeConnector` + instance. + :param bool ignore_missing: When set to ``False``, an exception + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the volume_connector could not be found. + When set to ``True``, no exception will be raised when + attempting to delete a non-existent volume_connector. + + :returns: The instance of the volume_connector which was deleted. + :rtype::class: + `~openstack.baremetal.v1.volume_connector.VolumeConnector`. + """ + return self._delete(_volumeconnector.VolumeConnector, + volume_connector, ignore_missing=ignore_missing) diff --git a/openstack/baremetal/v1/volume_connector.py b/openstack/baremetal/v1/volume_connector.py new file mode 100644 index 000000000..f7a1a2752 --- /dev/null +++ b/openstack/baremetal/v1/volume_connector.py @@ -0,0 +1,56 @@ +# 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.baremetal.v1 import _common +from openstack import resource + + +class VolumeConnector(_common.ListMixin, resource.Resource): + + resources_key = 'connectors' + base_path = '/volume/connectors' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + allow_patch = True + commit_method = 'PATCH' + commit_jsonpatch = True + + _query_mapping = resource.QueryParameters( + 'node', 'detail', + fields={'type': _common.fields_type}, + ) + + # Volume Connectors is available since 1.32 + _max_microversion = '1.32' + + #: The identifier of Volume connector and this field depends on the "type" + # of the volume_connector + connector_id = resource.Body('connector_id') + #: Timestamp at which the port was created. + created_at = resource.Body('created_at') + #: A set of one or more arbitrary metadata key and value pairs. + extra = resource.Body('extra') + #: A list of relative links, including the self and bookmark links. + links = resource.Body('links', type=list) + #: The UUID of node this port belongs to + node_id = resource.Body('node_uuid') + #: The types of Volume connector + type = resource.Body('type') + #: Timestamp at which the port was last updated. + updated_at = resource.Body('updated_at') + #: The UUID of the port + id = resource.Body('uuid', alternate_id=True) diff --git a/openstack/tests/functional/baremetal/base.py b/openstack/tests/functional/baremetal/base.py index 77796096a..bc0c4cafc 100644 --- a/openstack/tests/functional/baremetal/base.py +++ b/openstack/tests/functional/baremetal/base.py @@ -62,3 +62,14 @@ def create_port_group(self, node_id=None, **kwargs): lambda: self.conn.baremetal.delete_port_group(port_group.id, ignore_missing=True)) return port_group + + def create_volume_connector(self, node_id=None, **kwargs): + node_id = node_id or self.node_id + volume_connector = self.conn.baremetal.create_volume_connector( + node_uuid=node_id, **kwargs) + + self.addCleanup( + lambda: + self.conn.baremetal.delete_volume_connector(volume_connector.id, + ignore_missing=True)) + return volume_connector diff --git a/openstack/tests/functional/baremetal/test_baremetal_volume_connector.py b/openstack/tests/functional/baremetal/test_baremetal_volume_connector.py new file mode 100644 index 000000000..77ac90272 --- /dev/null +++ b/openstack/tests/functional/baremetal/test_baremetal_volume_connector.py @@ -0,0 +1,161 @@ +# 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.tests.functional.baremetal import base + + +class TestBareMetalVolumeconnector(base.BaseBaremetalTest): + + min_microversion = '1.32' + + def setUp(self): + super(TestBareMetalVolumeconnector, self).setUp() + self.node = self.create_node(provision_state='enroll') + + def test_volume_connector_create_get_delete(self): + self.conn.baremetal.set_node_provision_state( + self.node, 'manage', wait=True) + self.conn.baremetal.set_node_power_state(self.node, 'power off') + volume_connector = self.create_volume_connector( + connector_id='iqn.2017-07.org.openstack:01:d9a51732c3f', + type='iqn') + + loaded = self.conn.baremetal.get_volume_connector( + volume_connector.id) + self.assertEqual(loaded.id, volume_connector.id) + self.assertIsNotNone(loaded.node_id) + + with_fields = self.conn.baremetal.get_volume_connector( + volume_connector.id, fields=['uuid', 'extra']) + self.assertEqual(volume_connector.id, with_fields.id) + self.assertIsNone(with_fields.node_id) + + self.conn.baremetal.delete_volume_connector(volume_connector, + ignore_missing=False) + self.assertRaises(exceptions.ResourceNotFound, + self.conn.baremetal.get_volume_connector, + volume_connector.id) + + def test_volume_connector_list(self): + node2 = self.create_node(name='test-node') + self.conn.baremetal.set_node_provision_state( + node2, 'manage', wait=True) + self.conn.baremetal.set_node_power_state(node2, 'power off') + self.conn.baremetal.set_node_provision_state( + self.node, 'manage', wait=True) + self.conn.baremetal.set_node_power_state(self.node, 'power off') + vc1 = self.create_volume_connector( + connector_id='iqn.2018-07.org.openstack:01:d9a514g2c32', + node_id=node2.id, + type='iqn') + vc2 = self.create_volume_connector( + connector_id='iqn.2017-07.org.openstack:01:d9a51732c4g', + node_id=self.node.id, + type='iqn') + + vcs = self.conn.baremetal.volume_connectors( + node=self.node.id) + self.assertEqual([v.id for v in vcs], [vc2.id]) + + vcs = self.conn.baremetal.volume_connectors(node=node2.id) + self.assertEqual([v.id for v in vcs], [vc1.id]) + + vcs = self.conn.baremetal.volume_connectors(node='test-node') + self.assertEqual([v.id for v in vcs], [vc1.id]) + + def test_volume_connector_list_update_delete(self): + self.conn.baremetal.set_node_provision_state( + self.node, 'manage', wait=True) + self.conn.baremetal.set_node_power_state(self.node, 'power off') + self.create_volume_connector( + connector_id='iqn.2020-07.org.openstack:02:d9451472ce2', + node_id=self.node.id, + type='iqn', + extra={'foo': 'bar'}) + volume_connector = next(self.conn.baremetal.volume_connectors( + details=True, + node=self.node.id)) + self.assertEqual(volume_connector.extra, {'foo': 'bar'}) + + # This test checks that resources returned from listing are usable + self.conn.baremetal.update_volume_connector(volume_connector, + extra={'foo': 42}) + self.conn.baremetal.delete_volume_connector(volume_connector, + ignore_missing=False) + + def test_volume_connector_update(self): + self.conn.baremetal.set_node_provision_state( + self.node, 'manage', wait=True) + self.conn.baremetal.set_node_power_state(self.node, 'power off') + volume_connector = self.create_volume_connector( + connector_id='iqn.2019-07.org.openstack:03:de45b472c40', + node_id=self.node.id, + type='iqn') + volume_connector.extra = {'answer': 42} + + volume_connector = self.conn.baremetal.update_volume_connector( + volume_connector) + self.assertEqual({'answer': 42}, volume_connector.extra) + + volume_connector = self.conn.baremetal.get_volume_connector( + volume_connector.id) + self.assertEqual({'answer': 42}, volume_connector.extra) + + def test_volume_connector_patch(self): + vol_conn_id = 'iqn.2020-07.org.openstack:04:de45b472c40' + self.conn.baremetal.set_node_provision_state( + self.node, 'manage', wait=True) + self.conn.baremetal.set_node_power_state(self.node, 'power off') + volume_connector = self.create_volume_connector( + connector_id=vol_conn_id, + node_id=self.node.id, + type='iqn') + + volume_connector = self.conn.baremetal.patch_volume_connector( + volume_connector, dict(path='/extra/answer', op='add', value=42)) + self.assertEqual({'answer': 42}, volume_connector.extra) + self.assertEqual(vol_conn_id, + volume_connector.connector_id) + + volume_connector = self.conn.baremetal.get_volume_connector( + volume_connector.id) + self.assertEqual({'answer': 42}, volume_connector.extra) + + def test_volume_connector_negative_non_existing(self): + uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" + self.assertRaises(exceptions.ResourceNotFound, + self.conn.baremetal.get_volume_connector, uuid) + self.assertRaises(exceptions.ResourceNotFound, + self.conn.baremetal.find_volume_connector, uuid, + ignore_missing=False) + self.assertRaises(exceptions.ResourceNotFound, + self.conn.baremetal.delete_volume_connector, uuid, + ignore_missing=False) + self.assertIsNone(self.conn.baremetal.find_volume_connector(uuid)) + self.assertIsNone(self.conn.baremetal.delete_volume_connector(uuid)) + + def test_volume_connector_fields(self): + self.create_node() + self.conn.baremetal.set_node_provision_state( + self.node, 'manage', wait=True) + self.conn.baremetal.set_node_power_state(self.node, 'power off') + self.create_volume_connector( + connector_id='iqn.2018-08.org.openstack:04:de45f37c48', + node_id=self.node.id, + type='iqn') + result = self.conn.baremetal.volume_connectors( + fields=['uuid', 'node_id']) + for item in result: + self.assertIsNotNone(item.id) + self.assertIsNone(item.connector_id) diff --git a/openstack/tests/unit/baremetal/v1/test_proxy.py b/openstack/tests/unit/baremetal/v1/test_proxy.py index 9d5dfc99c..e83a4a37b 100644 --- a/openstack/tests/unit/baremetal/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal/v1/test_proxy.py @@ -19,6 +19,7 @@ from openstack.baremetal.v1 import node from openstack.baremetal.v1 import port from openstack.baremetal.v1 import port_group +from openstack.baremetal.v1 import volume_connector from openstack import exceptions from openstack.tests.unit import base from openstack.tests.unit import test_proxy_base @@ -180,6 +181,30 @@ def test_delete_allocation_ignore(self): self.verify_delete(self.proxy.delete_allocation, allocation.Allocation, True) + def test_create_volume_connector(self): + self.verify_create(self.proxy.create_volume_connector, + volume_connector.VolumeConnector) + + def test_find_volume_connector(self): + self.verify_find(self.proxy.find_volume_connector, + volume_connector.VolumeConnector) + + def test_get_volume_connector(self): + self.verify_get(self.proxy.get_volume_connector, + volume_connector.VolumeConnector, + mock_method=_MOCK_METHOD, + expected_kwargs={'fields': None}) + + def test_delete_volume_connector(self): + self.verify_delete(self.proxy.delete_volume_connector, + volume_connector.VolumeConnector, + False) + + def test_delete_volume_connector_ignore(self): + self.verify_delete(self.proxy.delete_volume_connector, + volume_connector.VolumeConnector, + True) + @mock.patch.object(node.Node, 'fetch', autospec=True) def test__get_with_fields_none(self, mock_fetch): result = self.proxy._get_with_fields(node.Node, 'value') diff --git a/openstack/tests/unit/baremetal/v1/test_volume_connector.py b/openstack/tests/unit/baremetal/v1/test_volume_connector.py new file mode 100644 index 000000000..9277ef795 --- /dev/null +++ b/openstack/tests/unit/baremetal/v1/test_volume_connector.py @@ -0,0 +1,61 @@ +# 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.tests.unit import base + +from openstack.baremetal.v1 import volume_connector + +FAKE = { + "connector_id": "iqn.2017-07.org.openstack:01:d9a51732c3f", + "created_at": "2016-08-18T22:28:48.643434+11:11", + "extra": {}, + "links": [ + { + "href": "http://127.0.0.1:6385/v1/volume/connector/", + "rel": "self" + }, + { + "href": "http://127.0.0.1:6385/volume/connector/", + "rel": "bookmark" + } + ], + "node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d", + "type": "iqn", + "updated_at": None, + "uuid": "9bf93e01-d728-47a3-ad4b-5e66a835037c" +} + + +class TestVolumeconnector(base.TestCase): + + def test_basic(self): + sot = volume_connector.VolumeConnector() + self.assertIsNone(sot.resource_key) + self.assertEqual('connectors', sot.resources_key) + self.assertEqual('/volume/connectors', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertEqual('PATCH', sot.commit_method) + + def test_instantiate(self): + sot = volume_connector.VolumeConnector(**FAKE) + self.assertEqual(FAKE['connector_id'], sot.connector_id) + self.assertEqual(FAKE['created_at'], sot.created_at) + self.assertEqual(FAKE['extra'], sot.extra) + self.assertEqual(FAKE['links'], sot.links) + self.assertEqual(FAKE['node_uuid'], sot.node_id) + self.assertEqual(FAKE['type'], sot.type) + self.assertEqual(FAKE['updated_at'], sot.updated_at) + self.assertEqual(FAKE['uuid'], sot.id) diff --git a/releasenotes/notes/volume_connector-api-f001e6f5fc4d1688.yaml b/releasenotes/notes/volume_connector-api-f001e6f5fc4d1688.yaml new file mode 100644 index 000000000..776263cf7 --- /dev/null +++ b/releasenotes/notes/volume_connector-api-f001e6f5fc4d1688.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Adds support for the baremetal volume connector API. From b23928b68e1f6e478851fb61f97596e572825337 Mon Sep 17 00:00:00 2001 From: Slawek Kaplonski Date: Tue, 16 Jun 2020 12:31:51 +0200 Subject: [PATCH 2683/3836] Remove neutron-fwaas from the jobs' required project Neutron-fwaas is going to be deprecated in master branch with [1]. [1] https://review.opendev.org/#/c/735828/ Depends-On: https://review.opendev.org/737731 Change-Id: Ifca2676ef89a81d373e3bebf58c3c25880ed1905 --- .zuul.yaml | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 2777e2eeb..b166a7510 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -125,7 +125,6 @@ networking services enabled. required-projects: - openstack/designate - - openstack/neutron-fwaas - openstack/octavia vars: devstack_local_conf: @@ -139,18 +138,11 @@ network_driver: network_noop_driver certificates: cert_manager: local_cert_manager - $NEUTRON_CONF: - fwaas: - agent_version: v2 - driver: iptables_v2 - enabled: true - firewall_l2_driver: ovs devstack_localrc: - Q_SERVICE_PLUGIN_CLASSES: qos,trunk,firewall_v2 + Q_SERVICE_PLUGIN_CLASSES: qos,trunk devstack_plugins: designate: https://opendev.org/openstack/designate octavia: https://opendev.org/openstack/octavia - neutron-fwaas: https://opendev.org/openstack/neutron-fwaas devstack_services: designate: true octavia: true @@ -159,7 +151,6 @@ o-hm: true o-hk: true neutron-dns: true - neutron-fwaas-v2: true s-account: false s-container: false s-object: false From 3ee25df9e7f794cf8d6d15d141ca13c013a22fd6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 16 Jun 2020 16:59:26 -0500 Subject: [PATCH 2684/3836] Drop python3.5 support Now that we released ussuri, we have a stable release that supports 3.5. That means if needed we can backport changes needed for zuul and nodepool, so it should be safe to go ahead and drop 3.5 support. Change-Id: Iaa761cb6f6ab30fa26f6587ac29f11274702e1a3 --- .zuul.yaml | 1 - doc/source/contributor/setup.rst | 8 +++----- doc/source/contributor/testing.rst | 11 +++++------ .../notes/dropped-python-3.5-b154887cce87947c.yaml | 4 ++++ setup.cfg | 3 +-- tox.ini | 4 ---- 6 files changed, 13 insertions(+), 18 deletions(-) create mode 100644 releasenotes/notes/dropped-python-3.5-b154887cce87947c.yaml diff --git a/.zuul.yaml b/.zuul.yaml index b166a7510..53a58f331 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -361,7 +361,6 @@ templates: - check-requirements - openstack-lower-constraints-jobs - - openstack-python35-jobs - openstack-python3-ussuri-jobs - openstacksdk-functional-tips - openstacksdk-tox-tips diff --git a/doc/source/contributor/setup.rst b/doc/source/contributor/setup.rst index 8275da02d..66109ac4b 100644 --- a/doc/source/contributor/setup.rst +++ b/doc/source/contributor/setup.rst @@ -34,15 +34,13 @@ as an administrator in some situations.:: You can create a virtualenv in any location. A common usage is to store all of your virtualenvs in the same place, such as under your home directory. -To create a virtualenv for the default Python, likely a version 2, run -the following:: +To create a virtualenv for the default Python, run the following:: $ virtualenv $HOME/envs/sdk -To create an environment for a different version, such as Python 3, run -the following:: +To create an environment for a different version, run the following:: - $ virtualenv -p python3.5 $HOME/envs/sdk3 + $ virtualenv -p python3.8 $HOME/envs/sdk3 When you want to enable your environment so that you can develop inside of it, you *activate* it. To activate an environment, run the /bin/activate diff --git a/doc/source/contributor/testing.rst b/doc/source/contributor/testing.rst index c45b88049..49ec9c057 100644 --- a/doc/source/contributor/testing.rst +++ b/doc/source/contributor/testing.rst @@ -14,15 +14,14 @@ Run In order to run the entire unit test suite, simply run the ``tox`` command inside of your source checkout. This will attempt to run every test command -listed inside of ``tox.ini``, which includes Python 2.7, 3.5, -and a PEP 8 check. You should run the full test suite on all versions before +listed inside of ``tox.ini``, which includes Python 3.8, and a PEP 8 check. +You should run the full test suite on all versions before submitting changes for review in order to avoid unexpected failures in the continuous integration system.:: (sdk3)$ tox ... - py35: commands succeeded - py27: commands succeeded + py38: commands succeeded pep8: commands succeeded congratulations :) @@ -30,8 +29,8 @@ During development, it may be more convenient to run a subset of the tests to keep test time to a minimum. You can choose to run the tests only on one version. A step further is to run only the tests you are working on.:: - (sdk3)$ tox -e py35 # Run run the tests on Python 3.5 - (sdk3)$ tox -e py35 TestContainer # Run only the TestContainer tests on 3.5 + (sdk3)$ tox -e py38 # Run run the tests on Python 3.8 + (sdk3)$ tox -e py38 TestContainer # Run only the TestContainer tests on 3.8 Functional Tests ---------------- diff --git a/releasenotes/notes/dropped-python-3.5-b154887cce87947c.yaml b/releasenotes/notes/dropped-python-3.5-b154887cce87947c.yaml new file mode 100644 index 000000000..a78c85f45 --- /dev/null +++ b/releasenotes/notes/dropped-python-3.5-b154887cce87947c.yaml @@ -0,0 +1,4 @@ +--- +upgrade: + - | + Python 3.5 is no longer supported. diff --git a/setup.cfg b/setup.cfg index b969c7750..a635769c4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,11 +14,10 @@ classifier = Operating System :: POSIX :: Linux Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 -python-requires = >=3.5 +python-requires = >=3.6 [files] packages = diff --git a/tox.ini b/tox.ini index 8ddc7c2a5..ef1491a33 100644 --- a/tox.ini +++ b/tox.ini @@ -130,7 +130,3 @@ deps = -c{toxinidir}/lower-constraints.txt -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt - -[testenv:py35] -basepython = python3.5 -deps = {[testenv:lower-constraints]deps} From 2aededaa0d60cb2b08f7749f34d6dca60715b664 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 26 Jun 2020 17:03:50 -0500 Subject: [PATCH 2685/3836] Don't use random.SystemRandom in tests We do not need cryptographically secure random content for throwaway test content. Change-Id: Ic65aebbd686fcc1584d7c3dbeedc5eb0f6b1341c --- openstack/tests/unit/object_store/v1/test_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/tests/unit/object_store/v1/test_proxy.py b/openstack/tests/unit/object_store/v1/test_proxy.py index 122589d66..2ed5c187b 100644 --- a/openstack/tests/unit/object_store/v1/test_proxy.py +++ b/openstack/tests/unit/object_store/v1/test_proxy.py @@ -180,7 +180,7 @@ def test_copy_object(self): def test_file_segment(self): file_size = 4200 - content = ''.join(random.SystemRandom().choice( + content = ''.join(random.choice( string.ascii_uppercase + string.digits) for _ in range(file_size)).encode('latin-1') self.imagefile = tempfile.NamedTemporaryFile(delete=False) From 75ae5bf4aadedf47e26264f278c19adb64871735 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 23 Jun 2020 14:22:10 -0500 Subject: [PATCH 2686/3836] Upload image via interop import if needed We have support for interop import already, but it's not stitched in to the image upload call. Add support for using it. We're adding this as a fallback for the case that PUT is turned off due to operator concerns about the need for scratch space. That means we don't want existing users to all of a sudden start uploading via import. However, if they do want to use import, we want that to work, so there is now a flag. Change-Id: I26e7ba5704d58a21f7ae2011e8c21e9b9310751a --- openstack/image/_base_proxy.py | 9 +- openstack/image/v1/_proxy.py | 9 +- openstack/image/v2/_proxy.py | 36 ++- openstack/image/v2/image.py | 10 +- openstack/tests/unit/cloud/test_image.py | 236 +++++++++++++++++- openstack/tests/unit/image/v2/test_proxy.py | 8 +- ...image-import-support-97052cdbc8ce449b.yaml | 6 + 7 files changed, 303 insertions(+), 11 deletions(-) create mode 100644 releasenotes/notes/image-import-support-97052cdbc8ce449b.yaml diff --git a/openstack/image/_base_proxy.py b/openstack/image/_base_proxy.py index a4f36f2aa..e98f4e714 100644 --- a/openstack/image/_base_proxy.py +++ b/openstack/image/_base_proxy.py @@ -41,6 +41,7 @@ def create_image( allow_duplicates=False, meta=None, wait=False, timeout=3600, data=None, validate_checksum=False, + use_import=False, **kwargs): """Upload an image. @@ -78,6 +79,11 @@ def create_image( compares return value with the one calculated or passed into this call. If value does not match - raises exception. Default is 'false' + :param bool use_import: Use the interoperable image import mechanism + to import the image. This defaults to false because it is harder on + the target cloud so should only be used when needed, such as when + the user needs the cloud to transform image format. If the cloud + has disabled direct uploads, this will default to true. Additional kwargs will be passed to the image creation as additional metadata for the image and will have all values converted to string @@ -170,6 +176,7 @@ def create_image( name, filename=filename, data=data, meta=meta, wait=wait, timeout=timeout, validate_checksum=validate_checksum, + use_import=use_import, **image_kwargs) else: image_kwargs['name'] = name @@ -183,7 +190,7 @@ def _create_image(self, name, **image_kwargs): @abc.abstractmethod def _upload_image(self, name, filename, data, meta, wait, timeout, - validate_checksum=True, + validate_checksum=True, use_import=False, **image_kwargs): pass diff --git a/openstack/image/v1/_proxy.py b/openstack/image/v1/_proxy.py index b808b10f5..f95ca5066 100644 --- a/openstack/image/v1/_proxy.py +++ b/openstack/image/v1/_proxy.py @@ -12,6 +12,7 @@ import warnings from openstack.cloud import exc +from openstack import exceptions from openstack.image import _base_proxy from openstack.image.v1 import image as _image @@ -42,7 +43,13 @@ def upload_image(self, **attrs): return self._create(_image.Image, **attrs) def _upload_image( - self, name, filename, data, meta, wait, timeout, **image_kwargs): + self, name, filename, data, meta, wait, timeout, + use_import=False, + **image_kwargs, + ): + if use_import: + raise exceptions.SDKException( + "Glance v1 does not support image import") # NOTE(mordred) wait and timeout parameters are unused, but # are present for ease at calling site. if filename and not data: diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 7961ef31d..eede85798 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -150,6 +150,7 @@ def upload_image(self, container_format=None, disk_format=None, def _upload_image(self, name, filename=None, data=None, meta=None, wait=False, timeout=None, validate_checksum=True, + use_import=False, **kwargs): # We can never have nice things. Glance v1 took "is_public" as a # boolean. Glance v2 takes "visibility". If the user gives us @@ -165,6 +166,12 @@ def _upload_image(self, name, filename=None, data=None, meta=None, try: # This makes me want to die inside if self._connection.image_api_use_tasks: + if use_import: + raise exceptions.SDKException( + "The Glance Task API and Import API are" + " mutually exclusive. Either disable" + " image_api_use_tasks in config, or" + " do not request using import") return self._upload_image_task( name, filename, data=data, meta=meta, wait=wait, timeout=timeout, **kwargs) @@ -172,6 +179,7 @@ def _upload_image(self, name, filename=None, data=None, meta=None, return self._upload_image_put( name, filename, data=data, meta=meta, validate_checksum=validate_checksum, + use_import=use_import, **kwargs) except exceptions.SDKException: self.log.debug("Image creation failed", exc_info=True) @@ -196,8 +204,10 @@ def _make_v2_image_params(self, meta, properties): return ret def _upload_image_put( - self, name, filename, data, meta, - validate_checksum, **image_kwargs): + self, name, filename, data, meta, + validate_checksum, use_import=False, + **image_kwargs, + ): if filename and not data: image_data = open(filename, 'rb') else: @@ -211,10 +221,28 @@ def _upload_image_put( image = self._create(_image.Image, **image_kwargs) image.data = image_data + supports_import = ( + image.image_import_methods + and 'glance-direct' in image.image_import_methods + ) + if use_import and not supports_import: + raise exceptions.SDKException( + "Importing image was requested but the cloud does not" + " support the image import method.") try: - response = image.upload(self) - exceptions.raise_from_response(response) + if not use_import: + try: + response = image.upload(self) + exceptions.raise_from_response(response) + except Exception: + if not supports_import: + raise + use_import = True + if use_import: + image.stage(self) + image.import_image(self) + # image_kwargs are flat here md5 = image_kwargs.get(self._IMAGE_MD5_KEY) sha256 = image_kwargs.get(self._IMAGE_SHA256_KEY) diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index a502e811f..1903b821c 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -276,8 +276,6 @@ def import_image(self, session, method='glance-direct', uri=None, if uri: if method == 'web-download': json['method']['uri'] = uri - elif method == 'glance-direct': - json['method']['uri'] = uri else: raise exceptions.InvalidRequest('URI is only supported with ' 'method: "web-download"') @@ -286,6 +284,14 @@ def import_image(self, session, method='glance-direct', uri=None, headers = {'X-Image-Meta-Store': store.id} session.post(url, json=json, headers=headers) + def _consume_header_attrs(self, attrs): + self.image_import_methods = [] + _image_import_methods = attrs.pop('OpenStack-image-import-methods', '') + if _image_import_methods: + self.image_import_methods = _image_import_methods.split(',') + + return super()._consume_header_attrs(attrs) + def _prepare_request(self, requires_id=None, prepend_key=False, patch=False, base_path=None, **kwargs): request = super(Image, self)._prepare_request(requires_id=requires_id, diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 3f33af578..a45f8ec5d 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -22,6 +22,8 @@ from openstack.tests import fakes from openstack.tests.unit import base +IMPORT_METHODS = 'glance-direct,web-download' + class BaseTestImage(base.TestCase): @@ -314,7 +316,75 @@ def test_list_images_paginated(self): self.cloud.list_images()) self.assert_calls() - def test_create_image_put_v2(self): + def test_create_image_put_v2_no_import(self): + self.cloud.image_api_use_tasks = False + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images', self.image_name], + base_url_append='v2'), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], + base_url_append='v2', + qs_elements=['name=' + self.image_name]), + validate=dict(), + json={'images': []}), + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], + base_url_append='v2', + qs_elements=['os_hidden=True']), + json={'images': []}), + dict(method='POST', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), + json=self.fake_image_dict, + validate=dict( + json={ + u'container_format': u'bare', + u'disk_format': u'qcow2', + u'name': self.image_name, + u'owner_specified.openstack.md5': + self.fake_image_dict[ + 'owner_specified.openstack.md5'], + u'owner_specified.openstack.object': self.object_name, + u'owner_specified.openstack.sha256': + self.fake_image_dict[ + 'owner_specified.openstack.sha256'], + u'visibility': u'private', + u'tags': [u'tag1', u'tag2']}) + ), + dict(method='PUT', + uri=self.get_mock_url( + 'image', append=['images', self.image_id, 'file'], + base_url_append='v2'), + request_headers={'Content-Type': 'application/octet-stream'}), + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images', self.fake_image_dict['id']], + base_url_append='v2' + ), + json=self.fake_image_dict), + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), + complete_qs=True, + json=self.fake_search_return) + ]) + + self.cloud.create_image( + self.image_name, self.imagefile.name, wait=True, timeout=1, + tags=['tag1', 'tag2'], + is_public=False, validate_checksum=True) + + self.assert_calls() + self.assertEqual(self.adapter.request_history[7].text.read(), + self.output) + + def test_create_image_put_v2_import_supported(self): self.cloud.image_api_use_tasks = False self.register_uris([ @@ -340,6 +410,9 @@ def test_create_image_put_v2(self): uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2'), json=self.fake_image_dict, + headers={ + 'OpenStack-image-import-methods': IMPORT_METHODS, + }, validate=dict( json={ u'container_format': u'bare', @@ -382,6 +455,167 @@ def test_create_image_put_v2(self): self.assertEqual(self.adapter.request_history[7].text.read(), self.output) + def test_create_image_use_import(self): + self.cloud.image_api_use_tasks = False + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images', self.image_name], + base_url_append='v2'), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], + base_url_append='v2', + qs_elements=['name=' + self.image_name]), + validate=dict(), + json={'images': []}), + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], + base_url_append='v2', + qs_elements=['os_hidden=True']), + json={'images': []}), + dict(method='POST', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), + json=self.fake_image_dict, + headers={ + 'OpenStack-image-import-methods': IMPORT_METHODS, + }, + validate=dict( + json={ + u'container_format': u'bare', + u'disk_format': u'qcow2', + u'name': self.image_name, + u'owner_specified.openstack.md5': + self.fake_image_dict[ + 'owner_specified.openstack.md5'], + u'owner_specified.openstack.object': self.object_name, + u'owner_specified.openstack.sha256': + self.fake_image_dict[ + 'owner_specified.openstack.sha256'], + u'visibility': u'private', + u'tags': [u'tag1', u'tag2']}) + ), + dict(method='PUT', + uri=self.get_mock_url( + 'image', append=['images', self.image_id, 'stage'], + base_url_append='v2'), + request_headers={'Content-Type': 'application/octet-stream'}), + dict(method='POST', + uri=self.get_mock_url( + 'image', append=['images', self.image_id, 'import'], + base_url_append='v2'), + json={'method': {'name': 'glance-direct'}}), + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images', self.fake_image_dict['id']], + base_url_append='v2' + ), + json=self.fake_image_dict), + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), + complete_qs=True, + json=self.fake_search_return) + ]) + + self.cloud.create_image( + self.image_name, self.imagefile.name, wait=True, timeout=1, + tags=['tag1', 'tag2'], + is_public=False, validate_checksum=True, + use_import=True, + ) + + self.assert_calls() + self.assertEqual(self.adapter.request_history[7].text.read(), + self.output) + + def test_create_image_import_fallback(self): + self.cloud.image_api_use_tasks = False + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images', self.image_name], + base_url_append='v2'), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], + base_url_append='v2', + qs_elements=['name=' + self.image_name]), + validate=dict(), + json={'images': []}), + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], + base_url_append='v2', + qs_elements=['os_hidden=True']), + json={'images': []}), + dict(method='POST', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), + json=self.fake_image_dict, + headers={ + 'OpenStack-image-import-methods': IMPORT_METHODS, + }, + validate=dict( + json={ + u'container_format': u'bare', + u'disk_format': u'qcow2', + u'name': self.image_name, + u'owner_specified.openstack.md5': + self.fake_image_dict[ + 'owner_specified.openstack.md5'], + u'owner_specified.openstack.object': self.object_name, + u'owner_specified.openstack.sha256': + self.fake_image_dict[ + 'owner_specified.openstack.sha256'], + u'visibility': u'private', + u'tags': [u'tag1', u'tag2']}) + ), + dict(method='PUT', + uri=self.get_mock_url( + 'image', append=['images', self.image_id, 'file'], + base_url_append='v2'), + request_headers={'Content-Type': 'application/octet-stream'}, + status_code=403), + dict(method='PUT', + uri=self.get_mock_url( + 'image', append=['images', self.image_id, 'stage'], + base_url_append='v2'), + request_headers={'Content-Type': 'application/octet-stream'}), + dict(method='POST', + uri=self.get_mock_url( + 'image', append=['images', self.image_id, 'import'], + base_url_append='v2'), + json={'method': {'name': 'glance-direct'}}), + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images', self.fake_image_dict['id']], + base_url_append='v2' + ), + json=self.fake_image_dict), + dict(method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2'), + complete_qs=True, + json=self.fake_search_return) + ]) + + self.cloud.create_image( + self.image_name, self.imagefile.name, wait=True, timeout=1, + tags=['tag1', 'tag2'], + is_public=False, validate_checksum=True, + ) + + self.assert_calls() + self.assertEqual(self.adapter.request_history[7].text.read(), + self.output) + def test_create_image_task(self): self.cloud.image_api_use_tasks = True endpoint = self.cloud._object_store_client.get_endpoint() diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 5392c7381..4bde248fd 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -132,7 +132,9 @@ def test_image_create_validate_checksum_data_binary(self): 'd8262cd4f54963f0c93082d8dcf33' '4d4c78', self.proxy._IMAGE_OBJECT_KEY: 'bare/fake'}, - timeout=3600, validate_checksum=True, wait=False) + timeout=3600, validate_checksum=True, + use_import=False, + wait=False) def test_image_create_validate_checksum_data_not_binary(self): self.assertRaises( @@ -161,7 +163,9 @@ def test_image_create_data_binary(self): self.proxy._IMAGE_MD5_KEY: '', self.proxy._IMAGE_SHA256_KEY: '', self.proxy._IMAGE_OBJECT_KEY: 'bare/fake'}, - timeout=3600, validate_checksum=False, wait=False) + timeout=3600, validate_checksum=False, + use_import=False, + wait=False) def test_image_create_without_filename(self): self.proxy._create_image = mock.Mock() diff --git a/releasenotes/notes/image-import-support-97052cdbc8ce449b.yaml b/releasenotes/notes/image-import-support-97052cdbc8ce449b.yaml new file mode 100644 index 000000000..707d92eb2 --- /dev/null +++ b/releasenotes/notes/image-import-support-97052cdbc8ce449b.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Added support for using the image import feature when creating an + image. SDK will now fall back to using image import if there is an + error during PUT. From a66639f806bee1858bee8c929d848b49ae917fb6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 24 Jun 2020 11:40:36 -0500 Subject: [PATCH 2687/3836] Add support for multiple image stores Glance has support for having multiple backend stores. Add support to image creation for specifying them. Change-Id: I3b897abccfecf9353be07abc8f8325d91f3eb9d4 --- openstack/image/_base_proxy.py | 58 +++++++++--- openstack/image/v1/_proxy.py | 8 +- openstack/image/v2/_proxy.py | 93 +++++++++++++++---- openstack/image/v2/image.py | 25 ++++- openstack/tests/unit/image/v2/test_image.py | 54 ++++++++++- openstack/tests/unit/image/v2/test_proxy.py | 23 ++++- .../glance-image-stores-2baa66e6743a2f2d.yaml | 4 + 7 files changed, 225 insertions(+), 40 deletions(-) create mode 100644 releasenotes/notes/glance-image-stores-2baa66e6743a2f2d.yaml diff --git a/openstack/image/_base_proxy.py b/openstack/image/_base_proxy.py index e98f4e714..39a85d2b3 100644 --- a/openstack/image/_base_proxy.py +++ b/openstack/image/_base_proxy.py @@ -33,16 +33,20 @@ class BaseImageProxy(proxy.Proxy, metaclass=abc.ABCMeta): _SHADE_IMAGE_OBJECT_KEY = 'owner_specified.shade.object' def create_image( - self, name, filename=None, - container=None, - md5=None, sha256=None, - disk_format=None, container_format=None, - disable_vendor_agent=True, - allow_duplicates=False, meta=None, - wait=False, timeout=3600, - data=None, validate_checksum=False, - use_import=False, - **kwargs): + self, name, filename=None, + container=None, + md5=None, sha256=None, + disk_format=None, container_format=None, + disable_vendor_agent=True, + allow_duplicates=False, meta=None, + wait=False, timeout=3600, + data=None, validate_checksum=False, + use_import=False, + stores=None, + all_stores=None, + all_stores_must_succeed=None, + **kwargs, + ): """Upload an image. :param str name: Name of the image to create. If it is a pathname @@ -84,6 +88,26 @@ def create_image( the target cloud so should only be used when needed, such as when the user needs the cloud to transform image format. If the cloud has disabled direct uploads, this will default to true. + :param stores: + List of stores to be used when enabled_backends is activated + in glance. List values can be the id of a store or a + :class:`~openstack.image.v2.service_info.Store` instance. + Implies ``use_import`` equals ``True``. + :param all_stores: + Upload to all available stores. Mutually exclusive with + ``store`` and ``stores``. + Implies ``use_import`` equals ``True``. + :param all_stores_must_succeed: + When set to True, if an error occurs during the upload in at + least one store, the worfklow fails, the data is deleted + from stores where copying is done (not staging), and the + state of the image is unchanged. When set to False, the + workflow will fail (data deleted from stores, …) only if the + import fails on all stores specified by the user. In case of + a partial success, the locations added to the image will be + the stores where the data has been correctly uploaded. + Default is True. + Implies ``use_import`` equals ``True``. Additional kwargs will be passed to the image creation as additional metadata for the image and will have all values converted to string @@ -177,6 +201,9 @@ def create_image( wait=wait, timeout=timeout, validate_checksum=validate_checksum, use_import=use_import, + stores=stores, + all_stores=stores, + all_stores_must_succeed=stores, **image_kwargs) else: image_kwargs['name'] = name @@ -189,9 +216,14 @@ def _create_image(self, name, **image_kwargs): pass @abc.abstractmethod - def _upload_image(self, name, filename, data, meta, wait, timeout, - validate_checksum=True, use_import=False, - **image_kwargs): + def _upload_image( + self, name, filename, data, meta, wait, timeout, + validate_checksum=True, use_import=False, + stores=None, + all_stores=None, + all_stores_must_succeed=None, + **image_kwargs + ): pass @abc.abstractmethod diff --git a/openstack/image/v1/_proxy.py b/openstack/image/v1/_proxy.py index f95ca5066..60a487e46 100644 --- a/openstack/image/v1/_proxy.py +++ b/openstack/image/v1/_proxy.py @@ -45,11 +45,17 @@ def upload_image(self, **attrs): def _upload_image( self, name, filename, data, meta, wait, timeout, use_import=False, + stores=None, + all_stores=None, + all_stores_must_succeed=None, **image_kwargs, ): if use_import: - raise exceptions.SDKException( + raise exceptions.InvalidRequest( "Glance v1 does not support image import") + if stores or all_stores or all_stores_must_succeed: + raise exceptions.InvalidRequest( + "Glance v1 does not support stores") # NOTE(mordred) wait and timeout parameters are unused, but # are present for ease at calling site. if filename and not data: diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index eede85798..af3fbf34d 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -36,8 +36,13 @@ def _create_image(self, **kwargs): """ return self._create(_image.Image, **kwargs) - def import_image(self, image, method='glance-direct', uri=None, - store=None): + def import_image( + self, image, method='glance-direct', uri=None, + store=None, + stores=None, + all_stores=None, + all_stores_must_succeed=None, + ): """Import data to an existing image Interoperable image import process are introduced in the Image API @@ -45,24 +50,57 @@ def import_image(self, image, method='glance-direct', uri=None, Image Service download it by itself without sending binary data at image creation. - :param image: The value can be the ID of a image or a - :class:`~openstack.image.v2.image.Image` instance. - :param method: Method to use for importing the image. - A valid value is glance-direct or web-download. - :param uri: Required only if using the web-download import method. - This url is where the data is made available to the Image - service. - :param store: Used when enabled_backends is activated in glance - The value can be the id of a store or a - :class:`~openstack.image.v2.service_info.Store` - instance. + :param image: + The value can be the ID of a image or a + :class:`~openstack.image.v2.image.Image` instance. + :param method: + Method to use for importing the image. + A valid value is glance-direct or web-download. + :param uri: + Required only if using the web-download import method. + This url is where the data is made available to the Image + service. + :param store: + Used when enabled_backends is activated in glance. The value + can be the id of a store or a + :class:`~openstack.image.v2.service_info.Store` instance. + :param stores: + List of stores to be used when enabled_backends is activated + in glance. List values can be the id of a store or a + :class:`~openstack.image.v2.service_info.Store` instance. + :param all_stores: + Upload to all available stores. Mutually exclusive with + ``store`` and ``stores``. + :param all_stores_must_succeed: + When set to True, if an error occurs during the upload in at + least one store, the worfklow fails, the data is deleted + from stores where copying is done (not staging), and the + state of the image is unchanged. When set to False, the + workflow will fail (data deleted from stores, …) only if the + import fails on all stores specified by the user. In case of + a partial success, the locations added to the image will be + the stores where the data has been correctly uploaded. + Default is True. :returns: None """ image = self._get_resource(_image.Image, image) + if all_stores and (store or stores): + raise exceptions.InvalidRequest( + "all_stores is mutually exclusive with" + " store and stores") if store is not None: + if stores: + raise exceptions.InvalidRequest( + "store and stores are mutually exclusive") store = self._get_resource(_si.Store, store) + stores = stores or [] + new_stores = [] + for s in stores: + new_stores.append(self._get_resource(_si.Store, s)) + stores = new_stores + # as for the standard image upload function, container_format and # disk_format are required for using image import process if not all([image.container_format, image.disk_format]): @@ -70,7 +108,13 @@ def import_image(self, image, method='glance-direct', uri=None, "Both container_format and disk_format are required for" " importing an image") - image.import_image(self, method=method, uri=uri, store=store) + image.import_image( + self, method=method, uri=uri, + store=store, + stores=stores, + all_stores=all_stores, + all_stores_must_succeed=all_stores_must_succeed, + ) def stage_image(self, image, filename=None, data=None): """Stage binary image data @@ -148,10 +192,15 @@ def upload_image(self, container_format=None, disk_format=None, return img - def _upload_image(self, name, filename=None, data=None, meta=None, - wait=False, timeout=None, validate_checksum=True, - use_import=False, - **kwargs): + def _upload_image( + self, name, filename=None, data=None, meta=None, + wait=False, timeout=None, validate_checksum=True, + use_import=False, + stores=None, + all_stores=None, + all_stores_must_succeed=None, + **kwargs + ): # We can never have nice things. Glance v1 took "is_public" as a # boolean. Glance v2 takes "visibility". If the user gives us # is_public, we know what they mean. If they give us visibility, they @@ -180,6 +229,9 @@ def _upload_image(self, name, filename=None, data=None, meta=None, name, filename, data=data, meta=meta, validate_checksum=validate_checksum, use_import=use_import, + stores=stores, + all_stores=all_stores, + all_stores_must_succeed=all_stores_must_succeed, **kwargs) except exceptions.SDKException: self.log.debug("Image creation failed", exc_info=True) @@ -206,6 +258,9 @@ def _make_v2_image_params(self, meta, properties): def _upload_image_put( self, name, filename, data, meta, validate_checksum, use_import=False, + stores=None, + all_stores=None, + all_stores_must_succeed=None, **image_kwargs, ): if filename and not data: @@ -225,6 +280,8 @@ def _upload_image_put( image.image_import_methods and 'glance-direct' in image.image_import_methods ) + if stores or all_stores or all_stores_must_succeed: + use_import = True if use_import and not supports_import: raise exceptions.SDKException( "Importing image was requested but the cloud does not" diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index 1903b821c..bbf700a31 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -269,8 +269,22 @@ def stage(self, session): return self def import_image(self, session, method='glance-direct', uri=None, - store=None): + store=None, stores=None, all_stores=None, + all_stores_must_succeed=None): """Import Image via interoperable image import process""" + if all_stores and (store or stores): + raise exceptions.InvalidRequest( + "all_stores is mutually exclusive with" + " store and stores") + if store and stores: + raise exceptions.InvalidRequest( + "store and stores are mutually exclusive." + " Please just use stores.") + if store: + stores = [store] + else: + stores = stores or [] + url = utils.urljoin(self.base_path, self.id, 'import') json = {'method': {'name': method}} if uri: @@ -279,7 +293,16 @@ def import_image(self, session, method='glance-direct', uri=None, else: raise exceptions.InvalidRequest('URI is only supported with ' 'method: "web-download"') + if all_stores is not None: + json['all_stores'] = all_stores + if all_stores_must_succeed is not None: + json['all_stores_must_succeed'] = all_stores_must_succeed + for s in stores: + json.setdefault('stores', []) + json['stores'].append(s.id) + headers = {} + # Backward compat if store is not None: headers = {'X-Image-Meta-Store': store.id} session.post(url, json=json, headers=headers) diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 7ad86c37d..0a3ffee0c 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -277,9 +277,15 @@ def test_import_image_with_uri_not_web_download(self): json={"method": {"name": "glance-direct"}} ) - def test_import_image_with_stores(self): + def test_import_image_with_store(self): sot = image.Image(**EXAMPLE) - json = {"method": {"name": "web-download", "uri": "such-a-good-uri"}} + json = { + "method": { + "name": "web-download", + "uri": "such-a-good-uri", + }, + "stores": ["ceph_1"], + } store = mock.MagicMock() store.id = "ceph_1" sot.import_image(self.sess, "web-download", "such-a-good-uri", store) @@ -289,6 +295,50 @@ def test_import_image_with_stores(self): json=json ) + def test_import_image_with_stores(self): + sot = image.Image(**EXAMPLE) + json = { + "method": { + "name": "web-download", + "uri": "such-a-good-uri", + }, + "stores": ["ceph_1"], + } + store = mock.MagicMock() + store.id = "ceph_1" + sot.import_image( + self.sess, + "web-download", + "such-a-good-uri", + stores=[store], + ) + self.sess.post.assert_called_with( + 'images/IDENTIFIER/import', + headers={}, + json=json, + ) + + def test_import_image_with_all_stores(self): + sot = image.Image(**EXAMPLE) + json = { + "method": { + "name": "web-download", + "uri": "such-a-good-uri", + }, + "all_stores": True, + } + sot.import_image( + self.sess, + "web-download", + "such-a-good-uri", + all_stores=True, + ) + self.sess.post.assert_called_with( + 'images/IDENTIFIER/import', + headers={}, + json=json, + ) + def test_upload(self): sot = image.Image(**EXAMPLE) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 4bde248fd..d1c400dad 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -53,11 +53,18 @@ def test_image_import_no_required_attrs(self): def test_image_import(self): original_image = image.Image(**EXAMPLE) - self._verify("openstack.image.v2.image.Image.import_image", - self.proxy.import_image, - method_args=[original_image, "method", "uri"], - expected_kwargs={"method": "method", "store": None, - "uri": "uri"}) + self._verify( + "openstack.image.v2.image.Image.import_image", + self.proxy.import_image, + method_args=[original_image, "method", "uri"], + expected_kwargs={ + "method": "method", + "store": None, + "uri": "uri", + "stores": [], + "all_stores": None, + "all_stores_must_succeed": None, + }) def test_image_create_conflict(self): self.assertRaises( @@ -134,6 +141,9 @@ def test_image_create_validate_checksum_data_binary(self): self.proxy._IMAGE_OBJECT_KEY: 'bare/fake'}, timeout=3600, validate_checksum=True, use_import=False, + stores=None, + all_stores=None, + all_stores_must_succeed=None, wait=False) def test_image_create_validate_checksum_data_not_binary(self): @@ -165,6 +175,9 @@ def test_image_create_data_binary(self): self.proxy._IMAGE_OBJECT_KEY: 'bare/fake'}, timeout=3600, validate_checksum=False, use_import=False, + stores=None, + all_stores=None, + all_stores_must_succeed=None, wait=False) def test_image_create_without_filename(self): diff --git a/releasenotes/notes/glance-image-stores-2baa66e6743a2f2d.yaml b/releasenotes/notes/glance-image-stores-2baa66e6743a2f2d.yaml new file mode 100644 index 000000000..8721e1f03 --- /dev/null +++ b/releasenotes/notes/glance-image-stores-2baa66e6743a2f2d.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add support for specifying stores when doing glance image uploads. From c92e6c1cdd920491208848251ccb6909d1ac2521 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 27 Jun 2020 09:06:16 -0500 Subject: [PATCH 2688/3836] Remove enforcer It was a great idea, but it doesn't work with modern sphinx. The approach it took is actually untennable - sphinx does not produce output that makes it possible to look for the thing this is looking for. Change-Id: I7e1c6f12c643b7aa7bfc489dfdc188ac256073cf --- doc/requirements.txt | 1 - doc/source/conf.py | 6 -- doc/source/enforcer.py | 134 ----------------------------------------- 3 files changed, 141 deletions(-) delete mode 100644 doc/source/enforcer.py diff --git a/doc/requirements.txt b/doc/requirements.txt index 33acd8b59..070806f54 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -4,6 +4,5 @@ sphinx>=2.0.0,!=2.1.0 # BSD docutils>=0.11 # OSI-Approved Open Source, Public Domain openstackdocstheme>=2.2.1 # Apache-2.0 -beautifulsoup4>=4.6.0 # MIT reno>=3.1.0 # Apache-2.0 sphinxcontrib-svg2pdfconverter>=0.1.0 # BSD diff --git a/doc/source/conf.py b/doc/source/conf.py index e74947ae5..43894ebdd 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -26,7 +26,6 @@ 'sphinx.ext.autodoc', 'openstackdocstheme', 'sphinxcontrib.rsvgconverter', - 'enforcer' ] # openstackdocstheme options @@ -35,11 +34,6 @@ openstackdocs_use_storyboard = True html_theme = 'openstackdocs' -# TODO(shade) Set this to true once the build-openstack-sphinx-docs job is -# updated to use sphinx-build. -# When True, this will raise an exception that kills sphinx-build. -enforcer_warnings_as_errors = False - # autodoc generation is a bit aggressive and a nuisance when doing heavy # text edit cycles. # execute "export SPHINX_DEBUG=1" in your terminal to disable diff --git a/doc/source/enforcer.py b/doc/source/enforcer.py deleted file mode 100644 index 94fd8d3dc..000000000 --- a/doc/source/enforcer.py +++ /dev/null @@ -1,134 +0,0 @@ -# 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 importlib -import os - -from bs4 import BeautifulSoup -from sphinx import errors -from sphinx.util import logging - -LOG = logging.getLogger(__name__) - -# NOTE: We do this because I can't find any way to pass "-v" -# into sphinx-build through pbr... -DEBUG = True if os.getenv("ENFORCER_DEBUG") else False - -WRITTEN_METHODS = set() - - -class EnforcementError(errors.SphinxError): - """A mismatch between what exists and what's documented""" - category = "Enforcer" - - -def get_proxy_methods(): - """Return a set of public names on all proxies""" - names = ["openstack.accelerator.v2._proxy", - "openstack.baremetal.v1._proxy", - "openstack.clustering.v1._proxy", - "openstack.block_storage.v2._proxy", - "openstack.compute.v2._proxy", - "openstack.database.v1._proxy", - "openstack.identity.v2._proxy", - "openstack.identity.v3._proxy", - "openstack.image.v1._proxy", - "openstack.image.v2._proxy", - "openstack.key_manager.v1._proxy", - "openstack.load_balancer.v2._proxy", - "openstack.message.v2._proxy", - "openstack.network.v2._proxy", - "openstack.object_store.v1._proxy", - "openstack.orchestration.v1._proxy", - "openstack.workflow.v2._proxy"] - - modules = (importlib.import_module(name) for name in names) - - methods = set() - for module in modules: - # We're not going to use the Proxy for anything other than a `dir` - # so just pass a dummy value so we can create the instance. - instance = module.Proxy("") - # We only document public names - names = [name for name in dir(instance) if not name.startswith("_")] - - good_names = [module.__name__ + ".Proxy." + name for name in names] - methods.update(good_names) - - return methods - - -def page_context(app, pagename, templatename, context, doctree): - """Handle html-page-context-event - - This event is emitted once the builder has the contents to create - an HTML page, but before the template is rendered. This is the point - where we'll know what documentation is going to be written, so - gather all of the method names that are about to be included - so we can check which ones were or were not processed earlier - by autodoc. - """ - if "users/proxies" in pagename: - soup = BeautifulSoup(context["body"], "html.parser") - dts = soup.find_all("dt") - ids = [dt.get("id") for dt in dts] - - written = 0 - for id in ids: - if id is not None and "_proxy.Proxy" in id: - WRITTEN_METHODS.add(id) - written += 1 - - if DEBUG: - LOG.info("ENFORCER: Wrote %d proxy methods for %s" % ( - written, pagename)) - - -def build_finished(app, exception): - """Handle build-finished event - - This event is emitted once the builder has written all of the output. - At this point we just compare what we know was written to what we know - exists within the modules and share the results. - - When enforcer_warnings_as_errors=True in conf.py, this method - will raise EnforcementError on any failures in order to signal failure. - """ - all_methods = get_proxy_methods() - - LOG.info("ENFORCER: %d proxy methods exist" % len(all_methods)) - LOG.info("ENFORCER: %d proxy methods written" % len(WRITTEN_METHODS)) - missing = all_methods - WRITTEN_METHODS - - missing_count = len(missing) - LOG.info("ENFORCER: Found %d missing proxy methods " - "in the output" % missing_count) - - # TODO(shade) This is spewing a bunch of content for missing thing that - # are not actually missing. Leave it as info rather than warn so that the - # gate doesn't break ... but we should figure out why this is broken and - # fix it. - # We also need to deal with Proxy subclassing keystoneauth.adapter.Adapter - # now - some of the warnings come from Adapter elements. - for name in sorted(missing): - LOG.info("ENFORCER: %s was not included in the output" % name) - - if app.config.enforcer_warnings_as_errors and missing_count > 0: - raise EnforcementError( - "There are %d undocumented proxy methods" % missing_count) - - -def setup(app): - app.add_config_value("enforcer_warnings_as_errors", False, "env") - - app.connect("html-page-context", page_context) - app.connect("build-finished", build_finished) From 661a0eb4b514c079f4388fc32850e05b3de045c7 Mon Sep 17 00:00:00 2001 From: Ian Wienand Date: Fri, 3 Jul 2020 11:30:00 +1000 Subject: [PATCH 2689/3836] Ignore IPv6 addresses if force_ipv4 is set Currently if a cloud config sets force_ipv4, it will still have the public_v6 field populated if the provider API gives an IPv6 address. I can not see why you would want this address populated if you are telling the cloud to force IPv4. As a concrete example; If6e1a0402b9b7f93cc76623c01049764abc68b2a proposes in zuul-jobs adding the IPv6 address to /etc/hosts for multinode jobs. It does this by walking the nodepool interface values, which on some clouds with force_ipv4 set will have invalid/unconfigured IPv6 entries. I've updated the documentation to expand a bit more on what situations this flag might be useful, which AIUI is really mostly about clouds that return you an IPv6 address in the API but don't give you a practical way to auto-configure it. Change-Id: I7aaaf44ab1a1d4d25225843227ef6ab6d8564063 --- doc/source/user/config/configuration.rst | 39 ++++++++++++------- openstack/cloud/meta.py | 8 +++- openstack/tests/unit/cloud/test_meta.py | 1 + ...ipv4_no_ipv6_address-9842168b5d05d262.yaml | 6 +++ 4 files changed, 40 insertions(+), 14 deletions(-) create mode 100644 releasenotes/notes/force_ipv4_no_ipv6_address-9842168b5d05d262.yaml diff --git a/doc/source/user/config/configuration.rst b/doc/source/user/config/configuration.rst index a76ab1f73..36fbf3bb2 100644 --- a/doc/source/user/config/configuration.rst +++ b/doc/source/user/config/configuration.rst @@ -254,18 +254,22 @@ are connecting to OpenStack can share a cache should you desire. IPv6 ---- -IPv6 is the future, and you should always use it if your cloud supports it and -if your local network supports it. Both of those are easily detectable and all -friendly software should do the right thing. However, sometimes you might -exist in a location where you have an IPv6 stack, but something evil has -caused it to not actually function. In that case, there is a config option -you can set to unbreak you `force_ipv4`, or `OS_FORCE_IPV4` boolean -environment variable. +IPv6 is the future, and you should always use it if your cloud +supports it and if your local network supports it. Both of those are +easily detectable and all friendly software should do the right thing. + +However, sometimes a cloud API may return IPv6 information that is not +useful to a production deployment. For example, the API may provide +an IPv6 address for a server, but not provide that to the host +instance via metadata (configdrive) or standard IPv6 autoconfiguration +methods (i.e. the host either needs to make a bespoke API call, or +otherwise statically configure itself). + +For such situations, you can set the ``force_ipv4``, or ``OS_FORCE_IPV4`` +boolean environment variable. For example: .. code-block:: yaml - client: - force_ipv4: true clouds: mtvexx: profile: vexxhost @@ -276,15 +280,24 @@ environment variable. region_name: ca-ymq-1 dns_api_version: 1 monty: - profile: rax + profile: fooprovider + force_ipv4: true auth: username: mordred@inaugust.com password: XXXXXXXXX project_name: mordred@inaugust.com - region_name: DFW + region_name: RegionFoo + +The above snippet will tell client programs to prefer the IPv4 address +and leave the ``public_v6`` field of the `Server` object blank for the +``fooprovider`` cloud . You can also set this with a client flag for +all clouds: + +.. code-block:: yaml + + client: + force_ipv4: true -The above snippet will tell client programs to prefer returning an IPv4 -address. Per-region settings ------------------- diff --git a/openstack/cloud/meta.py b/openstack/cloud/meta.py index 874c69a6e..cdd372436 100644 --- a/openstack/cloud/meta.py +++ b/openstack/cloud/meta.py @@ -280,6 +280,7 @@ def get_server_external_ipv6(server): :param server: the server from which we want to get an IPv6 address :return: a string containing the IPv6 address or None """ + # Don't return ipv6 interfaces if forcing IPv4 if server['accessIPv6']: return server['accessIPv6'] addresses = find_nova_addresses(addresses=server['addresses'], version=6) @@ -448,7 +449,12 @@ def add_server_interfaces(cloud, server): # not exist to remain consistent with the pre-existing missing values server['addresses'] = _get_supplemental_addresses(cloud, server) server['public_v4'] = get_server_external_ipv4(cloud, server) or '' - server['public_v6'] = get_server_external_ipv6(server) or '' + # If we're forcing IPv4, then don't report IPv6 interfaces which + # are likely to be unconfigured. + if cloud.force_ipv4: + server['public_v6'] = '' + else: + server['public_v6'] = get_server_external_ipv6(server) or '' server['private_v4'] = get_server_private_ip(server, cloud) or '' server['interface_ip'] = _get_interface_ip(cloud, server) or '' diff --git a/openstack/tests/unit/cloud/test_meta.py b/openstack/tests/unit/cloud/test_meta.py index e925c63dd..6a6a7349c 100644 --- a/openstack/tests/unit/cloud/test_meta.py +++ b/openstack/tests/unit/cloud/test_meta.py @@ -1018,6 +1018,7 @@ def test_ipv4_hostvars( hostvars = meta.get_hostvars_from_server( fake_cloud, meta.obj_to_munch(standard_fake_server)) self.assertEqual(PUBLIC_V4, hostvars['interface_ip']) + self.assertEqual('', hostvars['public_v6']) @mock.patch.object(meta, 'get_server_external_ipv4') def test_private_interface_ip(self, mock_get_server_external_ipv4): diff --git a/releasenotes/notes/force_ipv4_no_ipv6_address-9842168b5d05d262.yaml b/releasenotes/notes/force_ipv4_no_ipv6_address-9842168b5d05d262.yaml new file mode 100644 index 000000000..8cca5a205 --- /dev/null +++ b/releasenotes/notes/force_ipv4_no_ipv6_address-9842168b5d05d262.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + Cloud with the `force_ipv4` flag will no longer return a + `public_v6` value, even if one is provided by the cloud. This is + to avoid having entries for unconfigured interfaces. From 77e2f67d3856153f875552f84f0a3565f557f2c3 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Sat, 4 Jul 2020 14:26:31 -0400 Subject: [PATCH 2690/3836] switch to importlib.metadata for entrypoint loading Importing pkg_resources scans every installed distribution to find all of the entry points. importlib.metadata can import entry points without scanning all of the installed packages. Change-Id: I9917b10292d191710b37838b3cf099d75139f123 Signed-off-by: Doug Hellmann --- lower-constraints.txt | 1 + openstack/connection.py | 19 ++++++++++++++----- requirements.txt | 2 ++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index f643a2712..b45cc49c5 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -6,6 +6,7 @@ decorator==4.4.1 doc8==0.8.0 dogpile.cache==0.6.5 fixtures==3.0.0 +importlib_metadata==1.7.0 iso8601==0.1.11 jmespath==0.9.0 jsonpatch==1.16 diff --git a/openstack/connection.py b/openstack/connection.py index 3030164cb..7c36b9dbd 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -179,6 +179,13 @@ import warnings import weakref +try: + # For python 3.8 and later + import importlib.metadata as importlib_metadata +except ImportError: + # For everyone else + import importlib_metadata + import concurrent.futures import keystoneauth1.exceptions import requestsexceptions @@ -423,15 +430,17 @@ def __init__(self, cloud=None, config=None, session=None, (package_name, function) = vendor_hook.rsplit(':') if package_name and function: - import pkg_resources - ep = pkg_resources.EntryPoint( - 'vendor_hook', package_name, attrs=(function,)) - hook = ep.resolve() + ep = importlib_metadata.EntryPoint( + name='vendor_hook', + value=vendor_hook, + group='vendor_hook', + ) + hook = ep.load() hook(self) except ValueError: self.log.warning('Hook should be in the entrypoint ' 'module:attribute format') - except (ImportError, TypeError) as e: + except (ImportError, TypeError, AttributeError) as e: self.log.warning('Configured hook %s cannot be executed: %s', vendor_hook, e) diff --git a/requirements.txt b/requirements.txt index c443a4544..52e05ec93 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,3 +17,5 @@ netifaces>=0.10.4 # MIT dogpile.cache>=0.6.5 # BSD cryptography>=2.1 # BSD/Apache-2.0 + +importlib_metadata>=1.7.0;python_version<'3.8' # Apache-2.0 From b23654e53a4577c2a6b3c6b29356c3d9860fe496 Mon Sep 17 00:00:00 2001 From: Pavlo Shchelokovskyy Date: Mon, 6 Jul 2020 20:20:33 +0300 Subject: [PATCH 2691/3836] Tolerate images created with other means when image was created by other means (or older openstacksdk) the `openstack.image.v2.image.Image["properties"]` may be present but be `None`. When such image is present and one tries to create another image with the same name but different content, current openstacksdk fails with AttributeError: 'NoneType' object has no attribute 'get' Change-Id: Ia8a88587d74d3a4081c8f9e23099e3da2077820e Story: 2007898 Task: 40287 --- openstack/image/_base_proxy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openstack/image/_base_proxy.py b/openstack/image/_base_proxy.py index 39a85d2b3..1f4d58b8c 100644 --- a/openstack/image/_base_proxy.py +++ b/openstack/image/_base_proxy.py @@ -156,7 +156,8 @@ def create_image( else: current_image = self.find_image(name) if current_image: - props = current_image.get('properties', {}) + # NOTE(pas-ha) 'properties' may be absent or be None + props = current_image.get('properties') or {} md5_key = props.get( self._IMAGE_MD5_KEY, props.get(self._SHADE_IMAGE_MD5_KEY, '')) From ddac0d578255fe49a40cb32fdf2b2868c3cf2f85 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 9 Jul 2020 08:35:13 -0500 Subject: [PATCH 2692/3836] Allow passing in a logging handler We create formatters on the fly in enable_logging. Allow passing in a custom handler that we wire up. Change-Id: Ic01540fe80be8c91e0121a33a2bfa9c9dc4b2da2 --- openstack/_log.py | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/openstack/_log.py b/openstack/_log.py index ea759ce3d..13529204e 100644 --- a/openstack/_log.py +++ b/openstack/_log.py @@ -44,9 +44,11 @@ def setup_logging(name, handlers=None, level=None): def enable_logging( - debug=False, http_debug=False, path=None, stream=None, - format_stream=False, - format_template='%(asctime)s %(levelname)s: %(name)s %(message)s'): + debug=False, http_debug=False, path=None, stream=None, + format_stream=False, + format_template='%(asctime)s %(levelname)s: %(name)s %(message)s', + handlers=None, +): """Enable logging output. Helper function to enable logging. This function is available for @@ -90,18 +92,23 @@ def enable_logging( formatter = logging.Formatter(format_template) - handlers = [] + if handlers: + for handler in handlers: + handler.setFormatter(formatter) - if stream is not None: - console = logging.StreamHandler(stream) - if format_stream: - console.setFormatter(formatter) - handlers.append(console) - - if path is not None: - file_handler = logging.FileHandler(path) - file_handler.setFormatter(formatter) - handlers.append(file_handler) + else: + handlers = [] + + if stream is not None: + console = logging.StreamHandler(stream) + if format_stream: + console.setFormatter(formatter) + handlers.append(console) + + if path is not None: + file_handler = logging.FileHandler(path) + file_handler.setFormatter(formatter) + handlers.append(file_handler) setup_logging('openstack', handlers=handlers, level=level) setup_logging('keystoneauth', handlers=handlers, level=level) From 3fb43bc134c19df80d3adff820260dcf25daf731 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 9 Jul 2020 16:07:15 +0200 Subject: [PATCH 2693/3836] Add block_storage find functions in order to implement basic ansible block storage modules we also want to have an easy way to search to resources. Change-Id: Ibe800e08cc8ea0e4277e4dc6339fdc9b53194057 --- openstack/block_storage/v3/_proxy.py | 64 +++++++++++++++++++ .../tests/unit/block_storage/v3/test_proxy.py | 15 +++++ 2 files changed, 79 insertions(+) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 1fe43d933..6b0a94306 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -35,6 +35,21 @@ def get_snapshot(self, snapshot): """ return self._get(_snapshot.Snapshot, snapshot) + def find_snapshot(self, name_or_id, ignore_missing=True, **attrs): + """Find a single snapshot + + :param snapshot: The name or ID a snapshot + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the snapshot does not exist. + + :returns: One :class:`~openstack.volume.v3.snapshot.Snapshot` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._find(_snapshot.Snapshot, name_or_id, + ignore_missing=ignore_missing) + def snapshots(self, details=True, **query): """Retrieve a generator of snapshots @@ -98,6 +113,21 @@ def get_type(self, type): """ return self._get(_type.Type, type) + def find_type(self, name_or_id, ignore_missing=True, **attrs): + """Find a single volume type + + :param snapshot: The name or ID a volume type + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the type does not exist. + + :returns: One :class:`~openstack.volume.v3.type.Type` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._find(_type.Type, name_or_id, + ignore_missing=ignore_missing) + def types(self, **query): """Retrieve a generator of volume types @@ -144,6 +174,21 @@ def get_volume(self, volume): """ return self._get(_volume.Volume, volume) + def find_volume(self, name_or_id, ignore_missing=True, **attrs): + """Find a single volume + + :param snapshot: The name or ID a volume + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the volume does not exist. + + :returns: One :class:`~openstack.volume.v3.volume.Volume` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._find(_volume.Volume, name_or_id, + ignore_missing=ignore_missing) + def volumes(self, details=True, **query): """Retrieve a generator of volumes @@ -255,6 +300,25 @@ def get_backup(self, backup): ) return self._get(_backup.Backup, backup) + def find_backup(self, name_or_id, ignore_missing=True, **attrs): + """Find a single backup + + :param snapshot: The name or ID a backup + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the backup does not exist. + + :returns: One :class:`~openstack.volume.v3.backup.Backup` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + if not self._connection.has_service('object-store'): + raise exceptions.SDKException( + 'Object-store service is required for block-store backups' + ) + return self._find(_backup.Backup, name_or_id, + ignore_missing=ignore_missing) + def create_backup(self, **attrs): """Create a new Backup from attributes with native API diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 12a4c77c0..c2a9efe89 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -30,6 +30,9 @@ def setUp(self): def test_snapshot_get(self): self.verify_get(self.proxy.get_snapshot, snapshot.Snapshot) + def test_snapshot_find(self): + self.verify_find(self.proxy.find_snapshot, snapshot.Snapshot) + def test_snapshots_detailed(self): self.verify_list(self.proxy.snapshots, snapshot.SnapshotDetail, method_kwargs={"details": True, "query": 1}, @@ -54,6 +57,9 @@ def test_snapshot_delete_ignore(self): def test_type_get(self): self.verify_get(self.proxy.get_type, type.Type) + def test_type_find(self): + self.verify_find(self.proxy.find_type, type.Type) + def test_types(self): self.verify_list(self.proxy.types, type.Type) @@ -69,6 +75,9 @@ def test_type_delete_ignore(self): def test_volume_get(self): self.verify_get(self.proxy.get_volume, volume.Volume) + def test_volume_find(self): + self.verify_find(self.proxy.find_volume, volume.Volume) + def test_volumes_detailed(self): self.verify_list(self.proxy.volumes, volume.Volume, method_kwargs={"details": True, "query": 1}, @@ -121,6 +130,12 @@ def test_backup_get(self): self.proxy._connection.has_service = mock.Mock(return_value=True) self.verify_get(self.proxy.get_backup, backup.Backup) + def test_backup_find(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self.verify_find(self.proxy.find_backup, backup.Backup) + def test_backup_delete(self): # NOTE: mock has_service self.proxy._connection = mock.Mock() From c9f6b1043dbc4a0ddb8289b8777c8d97f14a6128 Mon Sep 17 00:00:00 2001 From: Lucas Alvares Gomes Date: Mon, 13 Jul 2020 15:33:32 +0100 Subject: [PATCH 2694/3836] Functional tests to explicitly use ML2/OVS The Neutron project is changing DevStack to default to the ML2/OVN mechanism driver by default [0] but, a handful of functinal tests for openstacksdk is not backend agnostic (see the Story linked with this patch) and needs to be adapted or skipped when running with ML2/OVN. As a first step, just to unblock the DevStack gate this patch is explicitly configuring the functional tests to run against ML2/OVS. [0] ML2/OVS is and will be still full supported by the community. Change-Id: I374b03130310e17b01d2b992eebe70658617e297 Story: 2007919 Signed-off-by: Lucas Alvares Gomes --- .zuul.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.zuul.yaml b/.zuul.yaml index 53a58f331..2387c5951 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -44,7 +44,22 @@ vars: devstack_localrc: Q_ML2_PLUGIN_EXT_DRIVERS: qos,port_security + Q_AGENT: openvswitch + Q_ML2_TENANT_NETWORK_TYPE: vxlan + Q_ML2_PLUGIN_MECHANISM_DRIVERS: openvswitch devstack_services: + # OVN services + ovn-controller: false + ovn-northd: false + ovs-vswitchd: false + ovsdb-server: false + q-ovn-metadata-agent: false + # Neutron services + q-agt: true + q-dhcp: true + q-l3: true + q-metering: true + q-svc: true # sdk doesn't need vnc access n-cauth: false n-novnc: false From 1876eba47af2048970719fd51d86e30b6ecf9db8 Mon Sep 17 00:00:00 2001 From: Artom Lifshitz Date: Wed, 15 Jul 2020 14:12:04 -0400 Subject: [PATCH 2695/3836] Add tests for compute microversion 2.2 and 2.10 This patch adds functional testing for compute API versions 2.2 and 2.10, keypair `type` and `user_id` respectively. Story: 2007929 Change-Id: I3e5e6411ad9bc7da97901c3778b8f4e2ec121102 --- openstack/compute/v2/keypair.py | 2 ++ .../functional/compute/v2/test_keypair.py | 31 ++++++++++++++++++- openstack/tests/unit/cloud/test_keypair.py | 4 +++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/openstack/compute/v2/keypair.py b/openstack/compute/v2/keypair.py index f3886b637..e3a1eab2c 100644 --- a/openstack/compute/v2/keypair.py +++ b/openstack/compute/v2/keypair.py @@ -27,6 +27,8 @@ class Keypair(resource.Resource): allow_delete = True allow_list = True + _max_microversion = '2.10' + # Properties #: The date and time when the resource was created. created_at = resource.Body('created_at') diff --git a/openstack/tests/functional/compute/v2/test_keypair.py b/openstack/tests/functional/compute/v2/test_keypair.py index 6fccafa27..f76707174 100644 --- a/openstack/tests/functional/compute/v2/test_keypair.py +++ b/openstack/tests/functional/compute/v2/test_keypair.py @@ -23,7 +23,7 @@ def setUp(self): # Keypairs can't have .'s in the name. Because why? self.NAME = self.getUniqueString().split('.')[-1] - sot = self.conn.compute.create_keypair(name=self.NAME) + sot = self.conn.compute.create_keypair(name=self.NAME, type='ssh') assert isinstance(sot, keypair.Keypair) self.assertEqual(self.NAME, sot.name) self._keypair = sot @@ -42,7 +42,36 @@ def test_get(self): sot = self.conn.compute.get_keypair(self.NAME) self.assertEqual(self.NAME, sot.name) self.assertEqual(self.NAME, sot.id) + self.assertEqual('ssh', sot.type) def test_list(self): names = [o.name for o in self.conn.compute.keypairs()] self.assertIn(self.NAME, names) + + +class TestKeypairAdmin(base.BaseFunctionalTest): + + def setUp(self): + super(TestKeypairAdmin, self).setUp() + self._set_operator_cloud(interface='admin') + + self.NAME = self.getUniqueString().split('.')[-1] + self.USER = self.operator_cloud.list_users()[0] + + sot = self.conn.compute.create_keypair(name=self.NAME, + user_id=self.USER.id) + assert isinstance(sot, keypair.Keypair) + self.assertEqual(self.NAME, sot.name) + self.assertEqual(self.USER.id, sot.user_id) + self._keypair = sot + + def tearDown(self): + sot = self.conn.compute.delete_keypair(self._keypair) + self.assertIsNone(sot) + super(TestKeypairAdmin, self).tearDown() + + def test_get(self): + sot = self.conn.compute.get_keypair(self.NAME) + self.assertEqual(self.NAME, sot.name) + self.assertEqual(self.NAME, sot.id) + self.assertEqual(self.USER.id, sot.user_id) diff --git a/openstack/tests/unit/cloud/test_keypair.py b/openstack/tests/unit/cloud/test_keypair.py index 6dbad0ea7..e29c7306a 100644 --- a/openstack/tests/unit/cloud/test_keypair.py +++ b/openstack/tests/unit/cloud/test_keypair.py @@ -11,6 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import fixtures from openstack.cloud import exc from openstack.tests import fakes @@ -23,6 +24,9 @@ def setUp(self): super(TestKeypair, self).setUp() self.keyname = self.getUniqueString('key') self.key = fakes.make_fake_keypair(self.keyname) + self.useFixture(fixtures.MonkeyPatch( + 'openstack.utils.maximum_supported_microversion', + lambda *args, **kwargs: '2.10')) def test_create_keypair(self): self.register_uris([ From 9ed64cc73f549cccd67401a73f5f18fb8fd2a83a Mon Sep 17 00:00:00 2001 From: Artom Lifshitz Date: Thu, 16 Jul 2020 14:09:29 -0400 Subject: [PATCH 2696/3836] Add func test for compute microversion 2.3 Adds an admin API functional test for the following fields in the show server details API: OS-EXT-SRV-ATTR:reservation_id OS-EXT-SRV-ATTR:launch_index OS-EXT-SRV-ATTR:ramdisk_id OS-EXT-SRV-ATTR:kernel_id OS-EXT-SRV-ATTR:hostname OS-EXT-SRV-ATTR:root_device_name OS-EXT-SRV-ATTR:userdata os-extended-volumes:volumes_attached.delete_on_termination Change-Id: I3eb48139300b75caa7bf7b6464c4c8080d72fa04 Story: 2007929 --- .../functional/compute/v2/test_server.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/openstack/tests/functional/compute/v2/test_server.py b/openstack/tests/functional/compute/v2/test_server.py index 5f8da5533..9c008c89e 100644 --- a/openstack/tests/functional/compute/v2/test_server.py +++ b/openstack/tests/functional/compute/v2/test_server.py @@ -16,6 +16,52 @@ from openstack.tests.functional.network.v2 import test_network +class TestServerAdmin(ft_base.BaseComputeTest): + + def setUp(self): + super(TestServerAdmin, self).setUp() + self._set_operator_cloud(interface='admin') + self.NAME = 'needstobeshortandlowercase' + self.USERDATA = 'SSdtIGFjdHVhbGx5IGEgZ29hdC4=' + flavor = self.conn.compute.find_flavor(base.FLAVOR_NAME, + ignore_missing=False) + image = self.conn.compute.find_image(base.IMAGE_NAME, + ignore_missing=False) + volume = self.conn.create_volume(1) + sot = self.conn.compute.create_server( + name=self.NAME, flavor_id=flavor.id, image_id=image.id, + networks='none', user_data=self.USERDATA, + block_device_mapping=[{ + 'uuid': volume.id, + 'source_type': 'volume', + 'boot_index': 0, + 'destination_type': 'volume', + 'delete_on_termination': True, + 'volume_size': 1}]) + self.conn.compute.wait_for_server(sot, wait=self._wait_for_timeout) + assert isinstance(sot, server.Server) + self.assertEqual(self.NAME, sot.name) + self.server = sot + + def tearDown(self): + sot = self.conn.compute.delete_server(self.server.id) + self.conn.compute.wait_for_delete(self.server, + wait=self._wait_for_timeout) + self.assertIsNone(sot) + super(TestServerAdmin, self).tearDown() + + def test_get(self): + sot = self.conn.compute.get_server(self.server.id) + self.assertIsNotNone(sot.reservation_id) + self.assertIsNotNone(sot.launch_index) + self.assertIsNotNone(sot.ramdisk_id) + self.assertIsNotNone(sot.kernel_id) + self.assertEqual(self.NAME, sot.hostname) + self.assertTrue(sot.root_device_name.startswith('/dev')) + self.assertEqual(self.USERDATA, sot.user_data) + self.assertTrue(sot.attached_volumes[0]['delete_on_termination']) + + class TestServer(ft_base.BaseComputeTest): def setUp(self): From 6f9cd3f7a7b7823195c4b6fa564e094618afbdff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herv=C3=A9=20Beraud?= Date: Tue, 9 Jun 2020 10:52:02 +0200 Subject: [PATCH 2697/3836] Use unittest.mock instead of mock The mock third party library was needed for mock support in py2 runtimes. Since we now only support py36 and later, we can use the standard lib unittest.mock module instead. Change-Id: I90d966a150d128e177f157e292035cfb71d89ad1 --- lower-constraints.txt | 1 - openstack/tests/unit/baremetal/test_configdrive.py | 2 +- openstack/tests/unit/baremetal/v1/test_allocation.py | 3 ++- openstack/tests/unit/baremetal/v1/test_node.py | 11 ++++++----- openstack/tests/unit/baremetal/v1/test_proxy.py | 2 +- .../unit/baremetal_introspection/v1/test_proxy.py | 2 +- openstack/tests/unit/block_storage/v2/test_backup.py | 5 ++--- openstack/tests/unit/block_storage/v2/test_proxy.py | 5 ++--- openstack/tests/unit/block_storage/v2/test_volume.py | 5 ++--- openstack/tests/unit/block_storage/v3/test_backup.py | 5 ++--- openstack/tests/unit/block_storage/v3/test_proxy.py | 5 ++--- openstack/tests/unit/block_storage/v3/test_volume.py | 5 ++--- openstack/tests/unit/cloud/test__utils.py | 3 +-- openstack/tests/unit/cloud/test_create_server.py | 3 +-- openstack/tests/unit/cloud/test_floating_ip_common.py | 2 +- openstack/tests/unit/cloud/test_fwaas.py | 2 +- openstack/tests/unit/cloud/test_inventory.py | 2 +- openstack/tests/unit/cloud/test_meta.py | 2 +- openstack/tests/unit/cloud/test_object.py | 4 ++-- openstack/tests/unit/cloud/test_operator.py | 2 +- openstack/tests/unit/cloud/test_shade.py | 2 +- openstack/tests/unit/clustering/v1/test_cluster.py | 5 ++--- openstack/tests/unit/clustering/v1/test_node.py | 5 ++--- .../tests/unit/clustering/v1/test_profile_type.py | 5 ++--- openstack/tests/unit/clustering/v1/test_proxy.py | 2 +- openstack/tests/unit/clustering/v1/test_service.py | 4 ++-- openstack/tests/unit/compute/v2/test_aggregate.py | 4 ++-- openstack/tests/unit/compute/v2/test_limits.py | 4 ++-- openstack/tests/unit/compute/v2/test_metadata.py | 6 +++--- openstack/tests/unit/compute/v2/test_proxy.py | 2 +- openstack/tests/unit/compute/v2/test_server.py | 6 +++--- openstack/tests/unit/compute/v2/test_server_ip.py | 4 ++-- openstack/tests/unit/compute/v2/test_service.py | 4 ++-- openstack/tests/unit/config/test_cloud_config.py | 5 ++--- openstack/tests/unit/database/v1/test_instance.py | 4 ++-- openstack/tests/unit/dns/v2/test_zone.py | 7 +++---- openstack/tests/unit/dns/v2/test_zone_export.py | 5 +++-- openstack/tests/unit/dns/v2/test_zone_import.py | 6 +++--- openstack/tests/unit/fakes.py | 2 +- openstack/tests/unit/identity/test_version.py | 4 ++-- openstack/tests/unit/identity/v2/test_extension.py | 4 ++-- openstack/tests/unit/image/v2/test_image.py | 4 ++-- openstack/tests/unit/image/v2/test_proxy.py | 3 ++- openstack/tests/unit/key_manager/v1/test_secret.py | 4 ++-- .../tests/unit/load_balancer/test_load_balancer.py | 4 ++-- openstack/tests/unit/load_balancer/test_proxy.py | 2 +- openstack/tests/unit/message/v2/test_claim.py | 5 ++--- openstack/tests/unit/message/v2/test_message.py | 5 ++--- openstack/tests/unit/message/v2/test_proxy.py | 2 +- openstack/tests/unit/message/v2/test_queue.py | 5 ++--- openstack/tests/unit/message/v2/test_subscription.py | 4 ++-- openstack/tests/unit/network/v2/test_agent.py | 4 ++-- openstack/tests/unit/network/v2/test_flavor.py | 4 ++-- openstack/tests/unit/network/v2/test_floating_ip.py | 2 +- openstack/tests/unit/network/v2/test_proxy.py | 2 +- openstack/tests/unit/network/v2/test_router.py | 6 +++--- openstack/tests/unit/network/v2/test_trunk.py | 3 ++- openstack/tests/unit/object_store/v1/test_proxy.py | 2 +- openstack/tests/unit/orchestration/v1/test_proxy.py | 2 +- openstack/tests/unit/orchestration/v1/test_stack.py | 8 +++----- .../tests/unit/orchestration/v1/test_stack_files.py | 4 ++-- .../tests/unit/orchestration/v1/test_template.py | 5 ++--- openstack/tests/unit/test_connection.py | 2 +- openstack/tests/unit/test_exceptions.py | 5 ++--- openstack/tests/unit/test_proxy.py | 5 +++-- openstack/tests/unit/test_proxy_base.py | 2 +- openstack/tests/unit/test_proxy_base2.py | 2 +- openstack/tests/unit/test_resource.py | 2 +- openstack/tests/unit/test_utils.py | 10 ++++------ test-requirements.txt | 1 - 70 files changed, 126 insertions(+), 144 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index b45cc49c5..e83c80063 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -14,7 +14,6 @@ jsonpointer==1.13 jsonschema==3.2.0 keystoneauth1==3.18.0 linecache2==1.0.0 -mock==3.0.0 munch==2.1.0 netifaces==0.10.4 os-service-types==1.7.0 diff --git a/openstack/tests/unit/baremetal/test_configdrive.py b/openstack/tests/unit/baremetal/test_configdrive.py index a25eaa258..4f3b6ce20 100644 --- a/openstack/tests/unit/baremetal/test_configdrive.py +++ b/openstack/tests/unit/baremetal/test_configdrive.py @@ -15,8 +15,8 @@ import json import os +from unittest import mock -import mock import testtools from openstack.baremetal import configdrive diff --git a/openstack/tests/unit/baremetal/v1/test_allocation.py b/openstack/tests/unit/baremetal/v1/test_allocation.py index 5b42bb4ff..8eaaa8bd1 100644 --- a/openstack/tests/unit/baremetal/v1/test_allocation.py +++ b/openstack/tests/unit/baremetal/v1/test_allocation.py @@ -10,8 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from unittest import mock + from keystoneauth1 import adapter -import mock from openstack.baremetal.v1 import allocation from openstack import exceptions diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index c2c1b5ab1..dbc874f4c 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -10,8 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from unittest import mock + from keystoneauth1 import adapter -import mock from openstack.baremetal.v1 import _common from openstack.baremetal.v1 import node @@ -790,7 +791,7 @@ def test_node_patch(self, mock_patch): patch = {'path': 'test'} self.node.patch(self.session, patch=patch) mock_patch.assert_called_once() - kwargs = mock_patch.call_args.kwargs + kwargs = mock_patch.call_args[1] self.assertEqual(kwargs['patch'], {'path': 'test'}) @mock.patch.object(resource.Resource, '_prepare_request', autospec=True) @@ -801,12 +802,12 @@ def test_node_patch_reset_interfaces(self, mock__commit, mock_prepreq, self.node.patch(self.session, patch=patch, retry_on_conflict=True, reset_interfaces=True) mock_prepreq.assert_called_once() - prepreq_kwargs = mock_prepreq.call_args.kwargs + prepreq_kwargs = mock_prepreq.call_args[1] self.assertEqual(prepreq_kwargs['params'], [('reset_interfaces', True)]) mock__commit.assert_called_once() - commit_args = mock__commit.call_args.args - commit_kwargs = mock__commit.call_args.kwargs + commit_args = mock__commit.call_args[0] + commit_kwargs = mock__commit.call_args[1] self.assertIn('1.45', commit_args) self.assertEqual(commit_kwargs['retry_on_conflict'], True) mock_patch.assert_not_called() diff --git a/openstack/tests/unit/baremetal/v1/test_proxy.py b/openstack/tests/unit/baremetal/v1/test_proxy.py index e83a4a37b..3b7597672 100644 --- a/openstack/tests/unit/baremetal/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal/v1/test_proxy.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock from openstack.baremetal.v1 import _proxy from openstack.baremetal.v1 import allocation diff --git a/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py b/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py index 43a6b8311..6d8ff2116 100644 --- a/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock from keystoneauth1 import adapter diff --git a/openstack/tests/unit/block_storage/v2/test_backup.py b/openstack/tests/unit/block_storage/v2/test_backup.py index 50605fc1e..8c54712ae 100644 --- a/openstack/tests/unit/block_storage/v2/test_backup.py +++ b/openstack/tests/unit/block_storage/v2/test_backup.py @@ -10,14 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock from keystoneauth1 import adapter -from openstack.tests.unit import base - from openstack import exceptions from openstack.block_storage.v2 import backup +from openstack.tests.unit import base FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff" diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index 44267ec2c..2c6a1aa6e 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -9,9 +9,7 @@ # 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 openstack import exceptions +from unittest import mock from openstack.block_storage.v2 import _proxy from openstack.block_storage.v2 import backup @@ -19,6 +17,7 @@ from openstack.block_storage.v2 import stats from openstack.block_storage.v2 import type from openstack.block_storage.v2 import volume +from openstack import exceptions from openstack.tests.unit import test_proxy_base diff --git a/openstack/tests/unit/block_storage/v2/test_volume.py b/openstack/tests/unit/block_storage/v2/test_volume.py index d19755981..d866ba640 100644 --- a/openstack/tests/unit/block_storage/v2/test_volume.py +++ b/openstack/tests/unit/block_storage/v2/test_volume.py @@ -10,11 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -import mock - -from openstack.tests.unit import base +from unittest import mock from openstack.block_storage.v2 import volume +from openstack.tests.unit import base FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff" IMAGE_METADATA = { diff --git a/openstack/tests/unit/block_storage/v3/test_backup.py b/openstack/tests/unit/block_storage/v3/test_backup.py index 4522a6464..ec4ca97cf 100644 --- a/openstack/tests/unit/block_storage/v3/test_backup.py +++ b/openstack/tests/unit/block_storage/v3/test_backup.py @@ -10,14 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock from keystoneauth1 import adapter -from openstack.tests.unit import base - from openstack import exceptions from openstack.block_storage.v3 import backup +from openstack.tests.unit import base FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff" diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index c2a9efe89..8af6eaa4d 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -9,9 +9,7 @@ # 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 openstack import exceptions +from unittest import mock from openstack.block_storage.v3 import _proxy from openstack.block_storage.v3 import backup @@ -19,6 +17,7 @@ from openstack.block_storage.v3 import stats from openstack.block_storage.v3 import type from openstack.block_storage.v3 import volume +from openstack import exceptions from openstack.tests.unit import test_proxy_base diff --git a/openstack/tests/unit/block_storage/v3/test_volume.py b/openstack/tests/unit/block_storage/v3/test_volume.py index e16f39aa6..27348a3f6 100644 --- a/openstack/tests/unit/block_storage/v3/test_volume.py +++ b/openstack/tests/unit/block_storage/v3/test_volume.py @@ -10,11 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -import mock - -from openstack.tests.unit import base +from unittest import mock from openstack.block_storage.v3 import volume +from openstack.tests.unit import base FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff" IMAGE_METADATA = { diff --git a/openstack/tests/unit/cloud/test__utils.py b/openstack/tests/unit/cloud/test__utils.py index 2aea81158..20cabae4f 100644 --- a/openstack/tests/unit/cloud/test__utils.py +++ b/openstack/tests/unit/cloud/test__utils.py @@ -12,16 +12,15 @@ # License for the specific language governing permissions and limitations # under the License. +from unittest import mock from uuid import uuid4 -import mock import testtools from openstack.cloud import _utils from openstack.cloud import exc from openstack.tests.unit import base - RANGE_DATA = [ dict(id=1, key1=1, key2=5), dict(id=2, key1=1, key2=20), diff --git a/openstack/tests/unit/cloud/test_create_server.py b/openstack/tests/unit/cloud/test_create_server.py index 5be66d727..c83db7013 100644 --- a/openstack/tests/unit/cloud/test_create_server.py +++ b/openstack/tests/unit/cloud/test_create_server.py @@ -17,10 +17,9 @@ Tests for the `create_server` command. """ import base64 +from unittest import mock import uuid -import mock - from openstack import connection from openstack.cloud import exc from openstack.cloud import meta diff --git a/openstack/tests/unit/cloud/test_floating_ip_common.py b/openstack/tests/unit/cloud/test_floating_ip_common.py index 0103f284a..6845ab0d3 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_common.py +++ b/openstack/tests/unit/cloud/test_floating_ip_common.py @@ -19,7 +19,7 @@ Tests floating IP resource methods for Neutron and Nova-network. """ -from mock import patch +from unittest.mock import patch from openstack import connection from openstack.cloud import meta diff --git a/openstack/tests/unit/cloud/test_fwaas.py b/openstack/tests/unit/cloud/test_fwaas.py index ccf7d78e9..59e381b5c 100644 --- a/openstack/tests/unit/cloud/test_fwaas.py +++ b/openstack/tests/unit/cloud/test_fwaas.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. from copy import deepcopy -import mock +from unittest import mock from openstack import exceptions from openstack.network.v2.firewall_group import FirewallGroup diff --git a/openstack/tests/unit/cloud/test_inventory.py b/openstack/tests/unit/cloud/test_inventory.py index d44864231..e51f271d7 100644 --- a/openstack/tests/unit/cloud/test_inventory.py +++ b/openstack/tests/unit/cloud/test_inventory.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock from openstack.cloud import inventory import openstack.config diff --git a/openstack/tests/unit/cloud/test_meta.py b/openstack/tests/unit/cloud/test_meta.py index 6a6a7349c..35b81a101 100644 --- a/openstack/tests/unit/cloud/test_meta.py +++ b/openstack/tests/unit/cloud/test_meta.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -import mock +from unittest import mock from openstack import connection from openstack.cloud import meta diff --git a/openstack/tests/unit/cloud/test_object.py b/openstack/tests/unit/cloud/test_object.py index d225af9ff..3d74bd445 100644 --- a/openstack/tests/unit/cloud/test_object.py +++ b/openstack/tests/unit/cloud/test_object.py @@ -13,16 +13,16 @@ # under the License. import tempfile +from unittest import mock -import mock import testtools import openstack.cloud import openstack.cloud.openstackcloud as oc_oc from openstack.cloud import exc from openstack import exceptions -from openstack.tests.unit import base from openstack.object_store.v1 import _proxy +from openstack.tests.unit import base class BaseTestObject(base.TestCase): diff --git a/openstack/tests/unit/cloud/test_operator.py b/openstack/tests/unit/cloud/test_operator.py index 15627a476..90c8ed079 100644 --- a/openstack/tests/unit/cloud/test_operator.py +++ b/openstack/tests/unit/cloud/test_operator.py @@ -11,8 +11,8 @@ # under the License. import uuid +from unittest import mock -import mock import testtools from openstack.cloud import exc diff --git a/openstack/tests/unit/cloud/test_shade.py b/openstack/tests/unit/cloud/test_shade.py index 5a9dfbbc9..2b24b1eb8 100644 --- a/openstack/tests/unit/cloud/test_shade.py +++ b/openstack/tests/unit/cloud/test_shade.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock import uuid import testtools diff --git a/openstack/tests/unit/clustering/v1/test_cluster.py b/openstack/tests/unit/clustering/v1/test_cluster.py index ac6eab76f..6bab19394 100644 --- a/openstack/tests/unit/clustering/v1/test_cluster.py +++ b/openstack/tests/unit/clustering/v1/test_cluster.py @@ -10,11 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -import mock -from openstack.tests.unit import base +from unittest import mock from openstack.clustering.v1 import cluster - +from openstack.tests.unit import base FAKE_ID = '092d0955-2645-461a-b8fa-6a44655cdb2c' FAKE_NAME = 'test_cluster' diff --git a/openstack/tests/unit/clustering/v1/test_node.py b/openstack/tests/unit/clustering/v1/test_node.py index 43b0fda51..29a1e5a53 100644 --- a/openstack/tests/unit/clustering/v1/test_node.py +++ b/openstack/tests/unit/clustering/v1/test_node.py @@ -10,11 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -import mock -from openstack.tests.unit import base +from unittest import mock from openstack.clustering.v1 import node - +from openstack.tests.unit import base FAKE_ID = '123d0955-0099-aabb-b8fa-6a44655ceeff' FAKE_NAME = 'test_node' diff --git a/openstack/tests/unit/clustering/v1/test_profile_type.py b/openstack/tests/unit/clustering/v1/test_profile_type.py index f709c3d8c..e9e7d99e1 100644 --- a/openstack/tests/unit/clustering/v1/test_profile_type.py +++ b/openstack/tests/unit/clustering/v1/test_profile_type.py @@ -10,11 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -import mock -from openstack.tests.unit import base +from unittest import mock from openstack.clustering.v1 import profile_type - +from openstack.tests.unit import base FAKE = { 'name': 'FAKE_PROFILE_TYPE', diff --git a/openstack/tests/unit/clustering/v1/test_proxy.py b/openstack/tests/unit/clustering/v1/test_proxy.py index 6f2ff3043..07743a4c2 100644 --- a/openstack/tests/unit/clustering/v1/test_proxy.py +++ b/openstack/tests/unit/clustering/v1/test_proxy.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock from openstack.clustering.v1 import _proxy from openstack.clustering.v1 import action diff --git a/openstack/tests/unit/clustering/v1/test_service.py b/openstack/tests/unit/clustering/v1/test_service.py index f361a0b6f..3824bebdf 100644 --- a/openstack/tests/unit/clustering/v1/test_service.py +++ b/openstack/tests/unit/clustering/v1/test_service.py @@ -10,10 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -import mock -from openstack.tests.unit import base +from unittest import mock from openstack.clustering.v1 import service +from openstack.tests.unit import base IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/compute/v2/test_aggregate.py b/openstack/tests/unit/compute/v2/test_aggregate.py index 7c79f9d0b..092d0dad5 100644 --- a/openstack/tests/unit/compute/v2/test_aggregate.py +++ b/openstack/tests/unit/compute/v2/test_aggregate.py @@ -10,10 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -import mock -from openstack.tests.unit import base +from unittest import mock from openstack.compute.v2 import aggregate +from openstack.tests.unit import base EXAMPLE = { "name": "m-family", diff --git a/openstack/tests/unit/compute/v2/test_limits.py b/openstack/tests/unit/compute/v2/test_limits.py index d77b8ebdf..847f9df35 100644 --- a/openstack/tests/unit/compute/v2/test_limits.py +++ b/openstack/tests/unit/compute/v2/test_limits.py @@ -11,12 +11,12 @@ # under the License. import copy +from unittest import mock from keystoneauth1 import adapter -import mock -from openstack.tests.unit import base from openstack.compute.v2 import limits +from openstack.tests.unit import base ABSOLUTE_LIMITS = { "maxImageMeta": 128, diff --git a/openstack/tests/unit/compute/v2/test_metadata.py b/openstack/tests/unit/compute/v2/test_metadata.py index 0339b78e2..708df44ba 100644 --- a/openstack/tests/unit/compute/v2/test_metadata.py +++ b/openstack/tests/unit/compute/v2/test_metadata.py @@ -10,11 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -import mock -from openstack import exceptions -from openstack.tests.unit import base +from unittest import mock from openstack.compute.v2 import server +from openstack import exceptions +from openstack.tests.unit import base IDENTIFIER = 'IDENTIFIER' diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index c1757c890..3ccf1233b 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -9,7 +9,7 @@ # 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 unittest import mock from openstack.compute.v2 import _proxy from openstack.compute.v2 import availability_zone as az diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index e48266d8e..4f484e394 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -10,11 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -import mock -from openstack.tests.unit import base +from unittest import mock -from openstack.image.v2 import image from openstack.compute.v2 import server +from openstack.image.v2 import image +from openstack.tests.unit import base IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/compute/v2/test_server_ip.py b/openstack/tests/unit/compute/v2/test_server_ip.py index 6cbc0e94a..f20e311f2 100644 --- a/openstack/tests/unit/compute/v2/test_server_ip.py +++ b/openstack/tests/unit/compute/v2/test_server_ip.py @@ -10,10 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -import mock -from openstack.tests.unit import base +from unittest import mock from openstack.compute.v2 import server_ip +from openstack.tests.unit import base IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/compute/v2/test_service.py b/openstack/tests/unit/compute/v2/test_service.py index 5b72b6f52..9f6c20ac7 100644 --- a/openstack/tests/unit/compute/v2/test_service.py +++ b/openstack/tests/unit/compute/v2/test_service.py @@ -10,10 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -import mock -from openstack.tests.unit import base +from unittest import mock from openstack.compute.v2 import service +from openstack.tests.unit import base IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/config/test_cloud_config.py b/openstack/tests/unit/config/test_cloud_config.py index 0e1f8cc1e..fd6d30316 100644 --- a/openstack/tests/unit/config/test_cloud_config.py +++ b/openstack/tests/unit/config/test_cloud_config.py @@ -11,17 +11,16 @@ # under the License. import copy +from unittest import mock from keystoneauth1 import exceptions as ksa_exceptions from keystoneauth1 import session as ksa_session -import mock -from openstack import version as openstack_version from openstack.config import cloud_region from openstack.config import defaults from openstack import exceptions from openstack.tests.unit.config import base - +from openstack import version as openstack_version fake_config_dict = {'a': 1, 'os_b': 2, 'c': 3, 'os_c': 4} fake_services_dict = { diff --git a/openstack/tests/unit/database/v1/test_instance.py b/openstack/tests/unit/database/v1/test_instance.py index 98a8c8a54..ef67afc3f 100644 --- a/openstack/tests/unit/database/v1/test_instance.py +++ b/openstack/tests/unit/database/v1/test_instance.py @@ -10,10 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -import mock -from openstack.tests.unit import base +from unittest import mock from openstack.database.v1 import instance +from openstack.tests.unit import base IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/dns/v2/test_zone.py b/openstack/tests/unit/dns/v2/test_zone.py index 5b2daafdf..190e71a83 100644 --- a/openstack/tests/unit/dns/v2/test_zone.py +++ b/openstack/tests/unit/dns/v2/test_zone.py @@ -10,13 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -from keystoneauth1 import adapter -import mock +from unittest import mock -from openstack.tests.unit import base +from keystoneauth1 import adapter from openstack.dns.v2 import zone - +from openstack.tests.unit import base IDENTIFIER = 'NAME' EXAMPLE = { diff --git a/openstack/tests/unit/dns/v2/test_zone_export.py b/openstack/tests/unit/dns/v2/test_zone_export.py index 5b7876298..a474c88fc 100644 --- a/openstack/tests/unit/dns/v2/test_zone_export.py +++ b/openstack/tests/unit/dns/v2/test_zone_export.py @@ -9,11 +9,12 @@ # 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 unittest import mock + from keystoneauth1 import adapter -from openstack.tests.unit import base from openstack.dns.v2 import zone_export +from openstack.tests.unit import base IDENTIFIER = '074e805e-fe87-4cbb-b10b-21a06e215d41' diff --git a/openstack/tests/unit/dns/v2/test_zone_import.py b/openstack/tests/unit/dns/v2/test_zone_import.py index 808830338..74ec73c99 100644 --- a/openstack/tests/unit/dns/v2/test_zone_import.py +++ b/openstack/tests/unit/dns/v2/test_zone_import.py @@ -9,12 +9,12 @@ # 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 unittest import mock + from keystoneauth1 import adapter -from openstack.tests.unit import base from openstack.dns.v2 import zone_import - +from openstack.tests.unit import base IDENTIFIER = '074e805e-fe87-4cbb-b10b-21a06e215d41' EXAMPLE = { diff --git a/openstack/tests/unit/fakes.py b/openstack/tests/unit/fakes.py index f979ded57..835205035 100644 --- a/openstack/tests/unit/fakes.py +++ b/openstack/tests/unit/fakes.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock class FakeTransport(mock.Mock): diff --git a/openstack/tests/unit/identity/test_version.py b/openstack/tests/unit/identity/test_version.py index c4351ebd9..be9728f0b 100644 --- a/openstack/tests/unit/identity/test_version.py +++ b/openstack/tests/unit/identity/test_version.py @@ -10,10 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -import mock -from openstack.tests.unit import base +from unittest import mock from openstack.identity import version +from openstack.tests.unit import base IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/identity/v2/test_extension.py b/openstack/tests/unit/identity/v2/test_extension.py index d7ff73145..1d0bd9da5 100644 --- a/openstack/tests/unit/identity/v2/test_extension.py +++ b/openstack/tests/unit/identity/v2/test_extension.py @@ -10,10 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -import mock -from openstack.tests.unit import base +from unittest import mock from openstack.identity.v2 import extension +from openstack.tests.unit import base IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 0a3ffee0c..91dc333d9 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -13,15 +13,15 @@ import io import operator import tempfile +from unittest import mock from keystoneauth1 import adapter -import mock import requests -from openstack.tests.unit import base from openstack import _log from openstack import exceptions from openstack.image.v2 import image +from openstack.tests.unit import base IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index d1c400dad..672c15057 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -10,8 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. -import mock import io +from unittest import mock + import requests from openstack import exceptions diff --git a/openstack/tests/unit/key_manager/v1/test_secret.py b/openstack/tests/unit/key_manager/v1/test_secret.py index 96a43e949..a1d5693bd 100644 --- a/openstack/tests/unit/key_manager/v1/test_secret.py +++ b/openstack/tests/unit/key_manager/v1/test_secret.py @@ -10,10 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -import mock -from openstack.tests.unit import base +from unittest import mock from openstack.key_manager.v1 import secret +from openstack.tests.unit import base ID_VAL = "123" IDENTIFIER = 'http://localhost:9311/v1/secrets/%s' % ID_VAL diff --git a/openstack/tests/unit/load_balancer/test_load_balancer.py b/openstack/tests/unit/load_balancer/test_load_balancer.py index c06713ecc..9a0a7b423 100644 --- a/openstack/tests/unit/load_balancer/test_load_balancer.py +++ b/openstack/tests/unit/load_balancer/test_load_balancer.py @@ -10,11 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -import mock -from openstack.tests.unit import base +from unittest import mock import uuid from openstack.load_balancer.v2 import load_balancer +from openstack.tests.unit import base IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/load_balancer/test_proxy.py b/openstack/tests/unit/load_balancer/test_proxy.py index 954d47740..169a02cf1 100644 --- a/openstack/tests/unit/load_balancer/test_proxy.py +++ b/openstack/tests/unit/load_balancer/test_proxy.py @@ -11,7 +11,7 @@ # under the License. import uuid -import mock +from unittest import mock from openstack.load_balancer.v2 import _proxy from openstack.load_balancer.v2 import amphora diff --git a/openstack/tests/unit/message/v2/test_claim.py b/openstack/tests/unit/message/v2/test_claim.py index d2ebc1350..4f3a93f10 100644 --- a/openstack/tests/unit/message/v2/test_claim.py +++ b/openstack/tests/unit/message/v2/test_claim.py @@ -11,12 +11,11 @@ # under the License. import copy -import mock -from openstack.tests.unit import base +from unittest import mock import uuid from openstack.message.v2 import claim - +from openstack.tests.unit import base FAKE1 = { "age": 1632, diff --git a/openstack/tests/unit/message/v2/test_message.py b/openstack/tests/unit/message/v2/test_message.py index d00af7c62..42fa601b5 100644 --- a/openstack/tests/unit/message/v2/test_message.py +++ b/openstack/tests/unit/message/v2/test_message.py @@ -10,12 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -import mock -from openstack.tests.unit import base +from unittest import mock import uuid from openstack.message.v2 import message - +from openstack.tests.unit import base FAKE1 = { 'age': 456, diff --git a/openstack/tests/unit/message/v2/test_proxy.py b/openstack/tests/unit/message/v2/test_proxy.py index fea9f9c84..b4bddd9fe 100644 --- a/openstack/tests/unit/message/v2/test_proxy.py +++ b/openstack/tests/unit/message/v2/test_proxy.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock from openstack.message.v2 import _proxy from openstack.message.v2 import claim diff --git a/openstack/tests/unit/message/v2/test_queue.py b/openstack/tests/unit/message/v2/test_queue.py index 1183076f1..4758a91bd 100644 --- a/openstack/tests/unit/message/v2/test_queue.py +++ b/openstack/tests/unit/message/v2/test_queue.py @@ -10,12 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -import mock -from openstack.tests.unit import base +from unittest import mock import uuid from openstack.message.v2 import queue - +from openstack.tests.unit import base FAKE1 = { 'name': 'test_queue', diff --git a/openstack/tests/unit/message/v2/test_subscription.py b/openstack/tests/unit/message/v2/test_subscription.py index c334a3526..ce3c75fbd 100644 --- a/openstack/tests/unit/message/v2/test_subscription.py +++ b/openstack/tests/unit/message/v2/test_subscription.py @@ -11,11 +11,11 @@ # under the License. import copy -import mock -from openstack.tests.unit import base +from unittest import mock import uuid from openstack.message.v2 import subscription +from openstack.tests.unit import base FAKE1 = { diff --git a/openstack/tests/unit/network/v2/test_agent.py b/openstack/tests/unit/network/v2/test_agent.py index 19316dea0..1cc3829e7 100644 --- a/openstack/tests/unit/network/v2/test_agent.py +++ b/openstack/tests/unit/network/v2/test_agent.py @@ -10,10 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -import mock -from openstack.tests.unit import base +from unittest import mock from openstack.network.v2 import agent +from openstack.tests.unit import base IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/network/v2/test_flavor.py b/openstack/tests/unit/network/v2/test_flavor.py index f516c667f..7ba81829e 100644 --- a/openstack/tests/unit/network/v2/test_flavor.py +++ b/openstack/tests/unit/network/v2/test_flavor.py @@ -10,10 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -import mock -from openstack.tests.unit import base +from unittest import mock from openstack.network.v2 import flavor +from openstack.tests.unit import base IDENTIFIER = 'IDENTIFIER' EXAMPLE_WITH_OPTIONAL = { diff --git a/openstack/tests/unit/network/v2/test_floating_ip.py b/openstack/tests/unit/network/v2/test_floating_ip.py index 544520f69..d443884b5 100644 --- a/openstack/tests/unit/network/v2/test_floating_ip.py +++ b/openstack/tests/unit/network/v2/test_floating_ip.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock from openstack import proxy from openstack.network.v2 import floating_ip diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 7678f4317..488e23b3b 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock import uuid from openstack import exceptions diff --git a/openstack/tests/unit/network/v2/test_router.py b/openstack/tests/unit/network/v2/test_router.py index bd959ea9a..8c0a2fec5 100644 --- a/openstack/tests/unit/network/v2/test_router.py +++ b/openstack/tests/unit/network/v2/test_router.py @@ -10,13 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock + import testtools from openstack import exceptions -from openstack.tests.unit import base - from openstack.network.v2 import router +from openstack.tests.unit import base IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/network/v2/test_trunk.py b/openstack/tests/unit/network/v2/test_trunk.py index 063298b85..1af08fb23 100644 --- a/openstack/tests/unit/network/v2/test_trunk.py +++ b/openstack/tests/unit/network/v2/test_trunk.py @@ -10,7 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock + import testtools from openstack import exceptions diff --git a/openstack/tests/unit/object_store/v1/test_proxy.py b/openstack/tests/unit/object_store/v1/test_proxy.py index 2ed5c187b..a02616420 100644 --- a/openstack/tests/unit/object_store/v1/test_proxy.py +++ b/openstack/tests/unit/object_store/v1/test_proxy.py @@ -12,11 +12,11 @@ from testscenarios import load_tests_apply_scenarios as load_tests # noqa from hashlib import sha1 -import mock import random import string import tempfile import time +from unittest import mock from openstack.object_store.v1 import account from openstack.object_store.v1 import container diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index b4be71482..945d964ce 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -11,7 +11,7 @@ # under the License. from testscenarios import load_tests_apply_scenarios as load_tests # noqa -import mock +from unittest import mock from openstack import exceptions from openstack.orchestration.v1 import _proxy diff --git a/openstack/tests/unit/orchestration/v1/test_stack.py b/openstack/tests/unit/orchestration/v1/test_stack.py index 7cbc2fa7b..080ede820 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack.py +++ b/openstack/tests/unit/orchestration/v1/test_stack.py @@ -10,15 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -import mock - -from openstack.tests.unit import base -from openstack.tests.unit import test_resource +from unittest import mock from openstack import exceptions from openstack.orchestration.v1 import stack from openstack import resource - +from openstack.tests.unit import base +from openstack.tests.unit import test_resource FAKE_ID = 'ce8ae86c-9810-4cb1-8888-7fb53bc523bf' FAKE_NAME = 'test_stack' diff --git a/openstack/tests/unit/orchestration/v1/test_stack_files.py b/openstack/tests/unit/orchestration/v1/test_stack_files.py index c89284749..7f510b7b9 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack_files.py +++ b/openstack/tests/unit/orchestration/v1/test_stack_files.py @@ -10,11 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -import mock -from openstack.tests.unit import base +from unittest import mock from openstack.orchestration.v1 import stack_files as sf from openstack import resource +from openstack.tests.unit import base FAKE = { 'stack_id': 'ID', diff --git a/openstack/tests/unit/orchestration/v1/test_template.py b/openstack/tests/unit/orchestration/v1/test_template.py index 4d7e316bc..0714c3518 100644 --- a/openstack/tests/unit/orchestration/v1/test_template.py +++ b/openstack/tests/unit/orchestration/v1/test_template.py @@ -10,12 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -import mock -from openstack.tests.unit import base +from unittest import mock from openstack.orchestration.v1 import template from openstack import resource - +from openstack.tests.unit import base FAKE = { 'Description': 'Blah blah', diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index 15ae14de3..1fd081c10 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -11,10 +11,10 @@ # under the License. import os +from unittest import mock import fixtures from keystoneauth1 import session -import mock from testtools import matchers from openstack import connection diff --git a/openstack/tests/unit/test_exceptions.py b/openstack/tests/unit/test_exceptions.py index 1a95edecd..a3286719c 100644 --- a/openstack/tests/unit/test_exceptions.py +++ b/openstack/tests/unit/test_exceptions.py @@ -13,12 +13,11 @@ # under the License. import json - -import mock -from openstack.tests.unit import base +from unittest import mock import uuid from openstack import exceptions +from openstack.tests.unit import base class Test_Exception(base.TestCase): diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index 3cd7b0ed2..9b8d97a08 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -11,13 +11,14 @@ # under the License. from testscenarios import load_tests_apply_scenarios as load_tests # noqa -import mock +from unittest import mock + import munch -from openstack.tests.unit import base from openstack import exceptions from openstack import proxy from openstack import resource +from openstack.tests.unit import base class DeleteableResource(resource.Resource): diff --git a/openstack/tests/unit/test_proxy_base.py b/openstack/tests/unit/test_proxy_base.py index f4f520dd2..66652aa1a 100644 --- a/openstack/tests/unit/test_proxy_base.py +++ b/openstack/tests/unit/test_proxy_base.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock from openstack.tests.unit import base diff --git a/openstack/tests/unit/test_proxy_base2.py b/openstack/tests/unit/test_proxy_base2.py index b12b6ebcd..56b8927af 100644 --- a/openstack/tests/unit/test_proxy_base2.py +++ b/openstack/tests/unit/test_proxy_base2.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock from openstack.tests.unit import base diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 67fd7c863..6018434fe 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -12,9 +12,9 @@ import itertools import json +from unittest import mock from keystoneauth1 import adapter -import mock import munch import requests diff --git a/openstack/tests/unit/test_utils.py b/openstack/tests/unit/test_utils.py index 570709222..6f330a75b 100644 --- a/openstack/tests/unit/test_utils.py +++ b/openstack/tests/unit/test_utils.py @@ -12,20 +12,18 @@ # License for the specific language governing permissions and limitations # under the License. +import concurrent.futures import logging -import mock +from unittest import mock import sys -from openstack.tests.unit import base - -import concurrent.futures - -import testtools import fixtures import os_service_types +import testtools import openstack from openstack import exceptions +from openstack.tests.unit import base from openstack import utils diff --git a/test-requirements.txt b/test-requirements.txt index 75ea92bc7..69e9e440c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,7 +7,6 @@ coverage!=4.4,>=4.0 # Apache-2.0 ddt>=1.0.1 # MIT fixtures>=3.0.0 # Apache-2.0/BSD jsonschema>=3.2.0 # MIT -mock>=3.0.0 # BSD prometheus-client>=0.4.2 # Apache-2.0 oslo.config>=6.1.0 # Apache-2.0 oslotest>=3.2.0 # Apache-2.0 From d48d9057a2e230f78e878abe2a15ba8b2aed7470 Mon Sep 17 00:00:00 2001 From: Artom Lifshitz Date: Fri, 17 Jul 2020 12:20:34 -0400 Subject: [PATCH 2698/3836] Add compute microversion 2.6 and 2.8 Compute 2.6 added the ceate console API [1], and 2.8 added the 'mks' and 'webmks' protocol and type [2]. Since we don't do client-side parameter validation, we get 2.8 for free when implementing support for 2.6. [1] https://docs.openstack.org/api-ref/compute/?expanded=create-console-detail#server-consoles [2] https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id7 Change-Id: Iaa7932ab5920c824a7b32bbbcbc4205b8eab6052 Story: 2007929 --- openstack/compute/v2/_proxy.py | 15 ++++++++ openstack/compute/v2/server_remote_console.py | 36 +++++++++++++++++++ .../functional/compute/v2/test_server.py | 7 ++++ 3 files changed, 58 insertions(+) create mode 100644 openstack/compute/v2/server_remote_console.py diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 6a40c6930..bb51c0f99 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -23,6 +23,8 @@ from openstack.compute.v2 import server_diagnostics as _server_diagnostics from openstack.compute.v2 import server_group as _server_group from openstack.compute.v2 import server_interface as _server_interface +from openstack.compute.v2 import ( + server_remote_console as _server_remote_console) from openstack.compute.v2 import server_ip from openstack.compute.v2 import service as _service from openstack.compute.v2 import volume_attachment as _volume_attachment @@ -1473,6 +1475,19 @@ def get_server_diagnostics(self, server): return self._get(_server_diagnostics.ServerDiagnostics, server_id=server_id, requires_id=False) + def create_server_remote_console(self, server, **attrs): + """Create a remote console on the server. + + :param server: Either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance. + :returns: One + :class:`~openstack.compute.v2.server_remote_console. + ServerRemoteConsole` + """ + server_id = resource.Resource._get_id(server) + return self._create(_server_remote_console.ServerRemoteConsole, + server_id=server_id, **attrs) + def _get_cleanup_dependencies(self): return { 'compute': { diff --git a/openstack/compute/v2/server_remote_console.py b/openstack/compute/v2/server_remote_console.py new file mode 100644 index 000000000..a6de29634 --- /dev/null +++ b/openstack/compute/v2/server_remote_console.py @@ -0,0 +1,36 @@ +# 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 resource + + +class ServerRemoteConsole(resource.Resource): + resource_key = 'remote_console' + base_path = '/servers/%(server_id)s/remote-consoles' + + # capabilities + allow_create = True + allow_fetch = False + allow_commit = False + allow_delete = False + allow_list = False + + _max_microversion = '2.8' + + #: Protocol of the remote console. + protocol = resource.Body('protocol') + #: Type of the remote console. + type = resource.Body('type') + #: URL used to connect to the console. + url = resource.Body('url') + #: The ID for the server. + server_id = resource.URI('server_id') diff --git a/openstack/tests/functional/compute/v2/test_server.py b/openstack/tests/functional/compute/v2/test_server.py index 5f8da5533..2bb7d3190 100644 --- a/openstack/tests/functional/compute/v2/test_server.py +++ b/openstack/tests/functional/compute/v2/test_server.py @@ -122,3 +122,10 @@ def test_server_metadata(self): test_server, test_server.metadata.keys()) test_server = self.conn.compute.get_server_metadata(test_server) self.assertFalse(test_server.metadata) + + def test_server_remote_console(self): + console = self.conn.compute.create_server_remote_console( + self.server, protocol='vnc', type='novnc') + self.assertEqual('vnc', console.protocol) + self.assertEqual('novnc', console.type) + self.assertTrue(console.url.startswith('http')) From 707839e29daf72c28b2bbeae5e39a784fae86c66 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Tue, 28 Jul 2020 14:03:19 +0200 Subject: [PATCH 2699/3836] baremetal-introspection: allow fetching unprocessed data Depends-On: https://review.opendev.org/743504 Change-Id: I5b42527a10ca93aff98abb62abf3026e107c549e --- openstack/baremetal_introspection/v1/_proxy.py | 6 ++++-- openstack/baremetal_introspection/v1/introspection.py | 10 ++++++++-- .../unit/baremetal_introspection/v1/test_proxy.py | 9 +++++++++ releasenotes/notes/unprocessed-2d75133911945869.yaml | 5 +++++ 4 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/unprocessed-2d75133911945869.yaml diff --git a/openstack/baremetal_introspection/v1/_proxy.py b/openstack/baremetal_introspection/v1/_proxy.py index f3db88ade..b25eb3378 100644 --- a/openstack/baremetal_introspection/v1/_proxy.py +++ b/openstack/baremetal_introspection/v1/_proxy.py @@ -86,17 +86,19 @@ def get_introspection(self, introspection): """ return self._get(_introspect.Introspection, introspection) - def get_introspection_data(self, introspection): + def get_introspection_data(self, introspection, processed=True): """Get introspection data. :param introspection: The value can be the name or ID of an introspection (matching bare metal node name or ID) or an :class:`~.introspection.Introspection` instance. + :param processed: Whether to fetch the final processed data (the + default) or the raw unprocessed data as received from the ramdisk. :returns: introspection data from the most recent successful run. :rtype: dict """ res = self._get_resource(_introspect.Introspection, introspection) - return res.get_data(self) + return res.get_data(self, processed=processed) def abort_introspection(self, introspection, ignore_missing=True): """Abort an introspection. diff --git a/openstack/baremetal_introspection/v1/introspection.py b/openstack/baremetal_introspection/v1/introspection.py index 88819f9f1..1b01c0fd4 100644 --- a/openstack/baremetal_introspection/v1/introspection.py +++ b/openstack/baremetal_introspection/v1/introspection.py @@ -73,7 +73,7 @@ def abort(self, session): .format(id=self.id)) exceptions.raise_from_response(response, error_message=msg) - def get_data(self, session): + def get_data(self, session, processed=True): """Get introspection data. Note that the introspection data format is not stable and can vary @@ -81,14 +81,20 @@ def get_data(self, session): :param session: The session to use for making this request. :type session: :class:`~keystoneauth1.adapter.Adapter` + :param processed: Whether to fetch the final processed data (the + default) or the raw unprocessed data as received from the ramdisk. + :type processed: bool :returns: introspection data from the most recent successful run. :rtype: dict """ session = self._get_session(session) - version = self._get_microversion_for(session, 'fetch') + version = (self._get_microversion_for(session, 'fetch') + if processed else '1.17') request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'data') + if not processed: + request.url = utils.urljoin(request.url, 'unprocessed') response = session.get( request.url, headers=request.headers, microversion=version) msg = ("Failed to fetch introspection data for node {id}" diff --git a/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py b/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py index 6d8ff2116..c71fd3d66 100644 --- a/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py @@ -163,3 +163,12 @@ def test_get_data(self, mock_request): self.proxy, 'introspection/1234/data', 'GET', headers=mock.ANY, microversion=mock.ANY) self.assertIs(data, mock_request.return_value.json.return_value) + + def test_get_unprocessed_data(self, mock_request): + mock_request.return_value.status_code = 200 + data = self.proxy.get_introspection_data(self.introspection, + processed=False) + mock_request.assert_called_once_with( + self.proxy, 'introspection/1234/data/unprocessed', 'GET', + headers=mock.ANY, microversion='1.17') + self.assertIs(data, mock_request.return_value.json.return_value) diff --git a/releasenotes/notes/unprocessed-2d75133911945869.yaml b/releasenotes/notes/unprocessed-2d75133911945869.yaml new file mode 100644 index 000000000..d8738090b --- /dev/null +++ b/releasenotes/notes/unprocessed-2d75133911945869.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Supports fetching raw (unprocessed) introspection data from the bare metal + introspection service. From 1124c32dd25fd1744502e923244fbb9532bae106 Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Fri, 10 Jul 2020 08:56:06 +0000 Subject: [PATCH 2700/3836] Add "numa_affinity_policy" attribute to "port" Change-Id: I467ac83f04d0724897fb59b789ae9b666942e1ea Related-Bug: #1886798 --- openstack/network/v2/port.py | 2 ++ openstack/tests/unit/network/v2/test_port.py | 3 +++ .../add-port-numa-affinity-policy-b42a85dbe26560d2.yaml | 6 ++++++ 3 files changed, 11 insertions(+) create mode 100644 releasenotes/notes/add-port-numa-affinity-policy-b42a85dbe26560d2.yaml diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index 6efcad0fb..e517607bc 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -104,6 +104,8 @@ class Port(_base.NetworkResource, resource.TagMixin): name = resource.Body('name') #: The ID of the attached network. network_id = resource.Body('network_id') + #: The NUMA affinity policy defined for this port. + numa_affinity_policy = resource.Body('numa_affinity_policy') #: The ID of the project who owns the network. Only administrative #: users can specify a project ID other than their own. project_id = resource.Body('tenant_id') diff --git a/openstack/tests/unit/network/v2/test_port.py b/openstack/tests/unit/network/v2/test_port.py index a40b46294..3312d9af2 100644 --- a/openstack/tests/unit/network/v2/test_port.py +++ b/openstack/tests/unit/network/v2/test_port.py @@ -38,6 +38,7 @@ 'mac_address': '16', 'name': '17', 'network_id': '18', + 'numa_affinity_policy': False, 'port_security_enabled': True, 'qos_network_policy_id': '32', 'qos_policy_id': '21', @@ -130,6 +131,8 @@ def test_make_it(self): self.assertEqual(EXAMPLE['mac_address'], sot.mac_address) self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['network_id'], sot.network_id) + self.assertEqual(EXAMPLE['numa_affinity_policy'], + sot.numa_affinity_policy) self.assertTrue(sot.is_port_security_enabled) self.assertEqual(EXAMPLE['qos_network_policy_id'], sot.qos_network_policy_id) diff --git a/releasenotes/notes/add-port-numa-affinity-policy-b42a85dbe26560d2.yaml b/releasenotes/notes/add-port-numa-affinity-policy-b42a85dbe26560d2.yaml new file mode 100644 index 000000000..2696cb40e --- /dev/null +++ b/releasenotes/notes/add-port-numa-affinity-policy-b42a85dbe26560d2.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add ``numa_affinity_policy`` attribute to ``port`` resource. Users + can set this attribute to ``required``, ``deferred`` or ``legacy``. + This parameter is nullable. From 78e1c613f91bbbcceec9e8f3be86ac90abe52a98 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 31 Jul 2020 16:04:42 -0500 Subject: [PATCH 2701/3836] Stop falling back to image import Import needs to always be explicit, because there are too many cases where it doens't work properly. Change-Id: I89dc48bdbb245d83a0c7d4f112f24728232e391c --- openstack/image/v2/_proxy.py | 9 +- openstack/tests/unit/cloud/test_image.py | 83 ------------------- .../no-import-fallback-a09b5d5a11299933.yaml | 5 ++ 3 files changed, 7 insertions(+), 90 deletions(-) create mode 100644 releasenotes/notes/no-import-fallback-a09b5d5a11299933.yaml diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index af3fbf34d..82c0f4350 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -289,13 +289,8 @@ def _upload_image_put( try: if not use_import: - try: - response = image.upload(self) - exceptions.raise_from_response(response) - except Exception: - if not supports_import: - raise - use_import = True + response = image.upload(self) + exceptions.raise_from_response(response) if use_import: image.stage(self) image.import_image(self) diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index a45f8ec5d..30a991a9b 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -533,89 +533,6 @@ def test_create_image_use_import(self): self.assertEqual(self.adapter.request_history[7].text.read(), self.output) - def test_create_image_import_fallback(self): - self.cloud.image_api_use_tasks = False - - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images', self.image_name], - base_url_append='v2'), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], - base_url_append='v2', - qs_elements=['name=' + self.image_name]), - validate=dict(), - json={'images': []}), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], - base_url_append='v2', - qs_elements=['os_hidden=True']), - json={'images': []}), - dict(method='POST', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - json=self.fake_image_dict, - headers={ - 'OpenStack-image-import-methods': IMPORT_METHODS, - }, - validate=dict( - json={ - u'container_format': u'bare', - u'disk_format': u'qcow2', - u'name': self.image_name, - u'owner_specified.openstack.md5': - self.fake_image_dict[ - 'owner_specified.openstack.md5'], - u'owner_specified.openstack.object': self.object_name, - u'owner_specified.openstack.sha256': - self.fake_image_dict[ - 'owner_specified.openstack.sha256'], - u'visibility': u'private', - u'tags': [u'tag1', u'tag2']}) - ), - dict(method='PUT', - uri=self.get_mock_url( - 'image', append=['images', self.image_id, 'file'], - base_url_append='v2'), - request_headers={'Content-Type': 'application/octet-stream'}, - status_code=403), - dict(method='PUT', - uri=self.get_mock_url( - 'image', append=['images', self.image_id, 'stage'], - base_url_append='v2'), - request_headers={'Content-Type': 'application/octet-stream'}), - dict(method='POST', - uri=self.get_mock_url( - 'image', append=['images', self.image_id, 'import'], - base_url_append='v2'), - json={'method': {'name': 'glance-direct'}}), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images', self.fake_image_dict['id']], - base_url_append='v2' - ), - json=self.fake_image_dict), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - complete_qs=True, - json=self.fake_search_return) - ]) - - self.cloud.create_image( - self.image_name, self.imagefile.name, wait=True, timeout=1, - tags=['tag1', 'tag2'], - is_public=False, validate_checksum=True, - ) - - self.assert_calls() - self.assertEqual(self.adapter.request_history[7].text.read(), - self.output) - def test_create_image_task(self): self.cloud.image_api_use_tasks = True endpoint = self.cloud._object_store_client.get_endpoint() diff --git a/releasenotes/notes/no-import-fallback-a09b5d5a11299933.yaml b/releasenotes/notes/no-import-fallback-a09b5d5a11299933.yaml new file mode 100644 index 000000000..6f897a304 --- /dev/null +++ b/releasenotes/notes/no-import-fallback-a09b5d5a11299933.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + Image upload will no longer fall back to attempting to use + the import workflow if the initial upload does not work. From ef43a9ffb1c946d64f25c081a802117d248a4ae2 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 12 Jun 2020 15:08:33 +0200 Subject: [PATCH 2702/3836] Add support for filters into the project cleanup We might want do delete all resources i.e. older than X days. This is really tricky, especially with regards to cleaning network service in this case. To achieve this let's start looking to the port.device_id and look, whether it will be present/deleted or not. ATM only created_at and updated_at as filters are possible Change-Id: I8b3f53f96f83dc0fbb09957097ed27755f255db1 --- openstack/block_storage/v3/_proxy.py | 54 ++++-- openstack/cloud/openstackcloud.py | 52 +++-- openstack/compute/v2/_proxy.py | 31 ++- openstack/dns/v2/_proxy.py | 4 +- openstack/network/v2/_proxy.py | 169 +++++++++++++--- openstack/orchestration/v1/_proxy.py | 18 +- openstack/proxy.py | 80 +++++++- .../functional/cloud/test_project_cleanup.py | 43 ++++- openstack/tests/unit/test_proxy.py | 180 ++++++++++++++++++ 9 files changed, 553 insertions(+), 78 deletions(-) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 6b0a94306..a025c463d 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -421,23 +421,45 @@ def _get_cleanup_dependencies(self): } } - def _service_cleanup(self, dry_run=True, status_queue=None): + def _service_cleanup(self, dry_run=True, client_status_queue=None, + identified_resources=None, + filters=None, resource_evaluation_fn=None): if self._connection.has_service('object-store'): # Volume backups require object-store to be available, even for # listing + backups = [] for obj in self.backups(details=False): - if status_queue: - status_queue.put(obj) - if not dry_run: - self.delete_backup(obj) + need_delete = self._service_cleanup_del_res( + self.delete_backup, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn) + if not dry_run and need_delete: + backups.append(obj) + + # Before deleting snapshots need to wait for backups to be deleted + for obj in backups: + try: + self.wait_for_delete(obj) + except exceptions.SDKException: + # Well, did our best, still try further + pass snapshots = [] for obj in self.snapshots(details=False): - if status_queue: + need_delete = self._service_cleanup_del_res( + self.delete_snapshot, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn) + if not dry_run and need_delete: snapshots.append(obj) - status_queue.put(obj) - if not dry_run: - self.delete_snapshot(obj) # Before deleting volumes need to wait for snapshots to be deleted for obj in snapshots: @@ -447,8 +469,12 @@ def _service_cleanup(self, dry_run=True, status_queue=None): # Well, did our best, still try further pass - for obj in self.volumes(details=False): - if status_queue: - status_queue.put(obj) - if not dry_run: - self.delete_volume(obj) + for obj in self.volumes(details=True): + self._service_cleanup_del_res( + self.delete_volume, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 76fb26df4..bb520a7d1 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -751,8 +751,14 @@ def has_service(self, service_key): else: return False - def project_cleanup(self, dry_run=True, - wait_timeout=120, status_queue=None): + def project_cleanup( + self, + dry_run=True, + wait_timeout=120, + status_queue=None, + filters=None, + resource_evaluation_fn=None + ): """Cleanup the project resources. Cleanup all resources in all services, which provide cleanup methods. @@ -762,6 +768,12 @@ def project_cleanup(self, dry_run=True, to comlete the cleanup. :param queue status_queue: a threading queue object used to get current process status. The queue contain processed resources. + :param dict filters: Additional filters for the cleanup (only resources + matching all filters will be deleted, if there are no other + dependencies). + :param resource_evaluation_fn: A callback function, which will be + invoked for each resurce and must return True/False depending on + whether resource need to be deleted or not. """ dependencies = {} get_dep_fn_name = '_get_cleanup_dependencies' @@ -771,9 +783,11 @@ def project_cleanup(self, dry_run=True, for service in self.config.get_enabled_services(): if hasattr(self, service): proxy = getattr(self, service) - if (proxy - and hasattr(proxy, get_dep_fn_name) - and hasattr(proxy, cleanup_fn_name)): + if ( + proxy + and hasattr(proxy, get_dep_fn_name) + and hasattr(proxy, cleanup_fn_name) + ): deps = getattr(proxy, get_dep_fn_name)() if deps: dependencies.update(deps) @@ -783,6 +797,10 @@ def project_cleanup(self, dry_run=True, for dep in v['before']: dep_graph.add_node(dep) dep_graph.add_edge(k, dep) + for dep in v.get('after', []): + dep_graph.add_edge(dep, k) + + cleanup_resources = dict() for service in dep_graph.walk(timeout=wait_timeout): fn = None @@ -790,11 +808,18 @@ def project_cleanup(self, dry_run=True, proxy = getattr(self, service) cleanup_fn = getattr(proxy, cleanup_fn_name, None) if cleanup_fn: - fn = functools.partial(cleanup_fn, dry_run=dry_run, - status_queue=status_queue) + fn = functools.partial( + cleanup_fn, + dry_run=dry_run, + client_status_queue=status_queue, + identified_resources=cleanup_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn + ) if fn: - self._pool_executor.submit(cleanup_task, dep_graph, - service, fn) + self._pool_executor.submit( + cleanup_task, dep_graph, service, fn + ) else: dep_graph.node_done(service) @@ -807,5 +832,10 @@ def project_cleanup(self, dry_run=True, def cleanup_task(graph, service, fn): - fn() - graph.node_done(service) + try: + fn() + except Exception: + log = _log.setup_logging('openstack.project_cleanup') + log.exception('Error in the %s cleanup function' % service) + finally: + graph.node_done(service) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index bb51c0f99..fb2aa5464 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -1495,9 +1495,28 @@ def _get_cleanup_dependencies(self): } } - def _service_cleanup(self, dry_run=True, status_queue=None): - for obj in self.servers(details=False): - self._service_cleanup_del_res(self.delete_server, - obj, - dry_run, - status_queue) + def _service_cleanup(self, dry_run=True, client_status_queue=None, + identified_resources=None, + filters=None, resource_evaluation_fn=None): + servers = [] + for obj in self.servers(): + need_delete = self._service_cleanup_del_res( + self.delete_server, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn) + if not dry_run and need_delete: + # In the dry run we identified, that server will go. To propely + # identify consequences we need to tell others, that the port + # will disappear as well + for port in self._connection.network.ports(device_id=obj.id): + identified_resources[port.id] = port + servers.append(obj) + + # We actually need to wait for servers to really disappear, since they + # might be still holding ports on the subnet + for server in servers: + self.wait_for_delete(server) diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py index 610e9e830..980d4c8e4 100644 --- a/openstack/dns/v2/_proxy.py +++ b/openstack/dns/v2/_proxy.py @@ -520,5 +520,7 @@ def _get_cleanup_dependencies(self): } } - def _service_cleanup(self, dry_run=True, status_queue=False): + def _service_cleanup(self, dry_run=True, client_status_queue=False, + identified_resources=None, + filters=None, resource_evaluation_fn=None): pass diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index dcca6fd73..48a2928ec 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -4014,22 +4014,81 @@ def _get_cleanup_dependencies(self): } } - def _service_cleanup(self, dry_run=True, status_queue=None): - for obj in self.ips(): - self._service_cleanup_del_res(self.delete_ip, obj, dry_run, - status_queue) - - for obj in self.security_groups(): + def _service_cleanup(self, dry_run=True, client_status_queue=None, + identified_resources=None, + filters=None, resource_evaluation_fn=None): + project_id = self.get_project_id() + # Delete floating_ips in the project if no filters defined OR all + # filters are matching and port_id is empty + for obj in self.ips(project_id=project_id): + self._service_cleanup_del_res( + self.delete_ip, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=fip_cleanup_evaluation) + + # Delete (try to delete) all security groups in the project + # Let's hope we can't drop SG in use + for obj in self.security_groups(project_id=project_id): if obj.name != 'default': self._service_cleanup_del_res( - self.delete_security_group, obj, - dry_run, status_queue) - - for port in self.ports(): - if port.device_owner in ['network:router_interface', - 'network:router_interface_distributed']: - if status_queue: - status_queue.put(obj) + self.delete_security_group, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn) + + # Networks are crazy, try to delete router+net+subnet + # if there are no "other" ports allocated on the net + for net in self.networks(project_id=project_id): + network_has_ports_allocated = False + router_if = list() + for port in self.ports( + project_id=project_id, + network_id=net.id + ): + self.log.debug('Looking at port %s' % port) + if port.device_owner in [ + 'network:router_interface', + 'network:router_interface_distributed' + ]: + router_if.append(port) + elif port.device_owner == 'network:dhcp': + # we don't treat DHCP as a real port + continue + elif ( + identified_resources + and port.device_id not in identified_resources + ): + # It seems some no other service identified this resource + # to be deleted. We can assume it doesn't count + network_has_ports_allocated = True + if network_has_ports_allocated: + # If some ports are on net - we cannot delete it + continue + self.log.debug('Network %s should be deleted' % net) + # __Check__ if we need to drop network according to filters + network_must_be_deleted = self._service_cleanup_del_res( + self.delete_network, + net, + dry_run=True, + client_status_queue=None, + identified_resources=None, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn) + if not network_must_be_deleted: + # If not - check another net + continue + # otherwise disconnect router, drop net, subnet, router + # Disconnect + for port in router_if: + if client_status_queue: + client_status_queue.put(port) if not dry_run: try: self.remove_interface_from_router( @@ -4037,18 +4096,72 @@ def _service_cleanup(self, dry_run=True, status_queue=None): port_id=port.id) except exceptions.SDKException: self.log.error('Cannot delete object %s' % obj) - - for obj in self.routers(): - self._service_cleanup_del_res( - self.delete_router, obj, - dry_run, status_queue) - - for obj in self.subnets(): - self._service_cleanup_del_res( - self.delete_subnet, obj, - dry_run, status_queue) - - for obj in self.networks(): + # router disconnected, drop it + self._service_cleanup_del_res( + self.delete_router, + self.get_router(port.device_id), + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=None, + resource_evaluation_fn=None) + # Drop all subnets in the net (no further conditions) + for obj in self.subnets( + project_id=project_id, + network_id=net.id + ): + self._service_cleanup_del_res( + self.delete_subnet, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=None, + resource_evaluation_fn=None) + + # And now the network itself (we are here definitely only if we + # need that) self._service_cleanup_del_res( - self.delete_network, obj, - dry_run, status_queue) + self.delete_network, + net, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=None, + resource_evaluation_fn=None) + + # It might happen, that we have routers not attached to anything + for obj in self.routers(): + ports = list(self.ports(device_id=obj.id)) + if len(ports) == 0: + self._service_cleanup_del_res( + self.delete_router, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=None, + resource_evaluation_fn=None) + + +def fip_cleanup_evaluation(obj, identified_resources=None, filters=None): + """Determine whether Floating IP should be deleted + + :param Resource obj: Floating IP object + :param dict identified_resources: Optional dictionary with resources + identified by other services for deletion. + :param dict filters: dictionary with parameters + """ + if ( + filters is not None + and ( + obj.port_id is not None + and identified_resources + and obj.port_id not in identified_resources + ) + ): + # If filters are set, but port is not empty and will not be empty - + # skip + return False + else: + return True diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index 37bbaede2..a94589ff9 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -487,13 +487,21 @@ def _get_cleanup_dependencies(self): } } - def _service_cleanup(self, dry_run=True, status_queue=None): + def _service_cleanup(self, dry_run=True, client_status_queue=None, + identified_resources=None, + filters=None, resource_evaluation_fn=None): stacks = [] for obj in self.stacks(): - stacks.append(obj) - self._project_cleanup_del_res( - self.delete_stack, obj, - dry_run, status_queue) + need_delete = self._service_cleanup_del_res( + self.delete_stack, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn) + if not dry_run and need_delete: + stacks.append(obj) for stack in stacks: self.wait_for_delete(stack) diff --git a/openstack/proxy.py b/openstack/proxy.py index a8812de70..eb2a5fc3d 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -15,6 +15,7 @@ JSONDecodeError = simplejson.scanner.JSONDecodeError except ImportError: JSONDecodeError = ValueError +import iso8601 import urllib from keystoneauth1 import adapter @@ -565,18 +566,79 @@ def _head(self, resource_type, value=None, base_path=None, **attrs): def _get_cleanup_dependencies(self): return None - def _service_cleanup(self, dry_run=True, status_queue=None): + def _service_cleanup(self, dry_run=True, client_status_queue=None, + identified_resources=None, filters=None, + resource_evaluation_fn=None): return None def _service_cleanup_del_res(self, del_fn, obj, dry_run=True, - status_queue=None): - if status_queue: - status_queue.put(obj) - if not dry_run: - try: - del_fn(obj) - except exceptions.SDKException as e: - self.log.error('Cannot delete resource %s: %s', obj, str(e)) + client_status_queue=None, + identified_resources=None, + filters=None, + resource_evaluation_fn=None): + need_delete = False + try: + if ( + resource_evaluation_fn + and callable(resource_evaluation_fn) + ): + # Ask a user-provided evaluation function if we need to delete + # the resource + need_del = resource_evaluation_fn(obj, filters, + identified_resources) + if isinstance(need_del, bool): + # Just double check function returned bool + need_delete = need_del + else: + need_delete = \ + self._service_cleanup_resource_filters_evaluation( + obj, + filters=filters) + + if need_delete: + if client_status_queue: + # Put into queue for client status info + client_status_queue.put(obj) + if identified_resources is not None: + # Put into internal dict shared between threads so that + # other services might know which other resources were + # identified + identified_resources[obj.id] = obj + if not dry_run: + del_fn(obj) + except Exception as e: + self.log.exception('Cannot delete resource %s: %s', obj, str(e)) + return need_delete + + def _service_cleanup_resource_filters_evaluation(self, obj, filters=None): + part_cond = [] + if filters is not None and isinstance(filters, dict): + for k, v in filters.items(): + try: + res_val = None + if k == 'created_at' and hasattr(obj, 'created_at'): + res_val = getattr(obj, 'created_at') + if k == 'updated_at' and hasattr(obj, 'updated_at'): + res_val = getattr(obj, 'updated_at') + if res_val: + res_date = iso8601.parse_date(res_val) + cmp_date = iso8601.parse_date(v) + if res_date and cmp_date and res_date <= cmp_date: + part_cond.append(True) + else: + part_cond.append(False) + else: + # There are filters set, but we can't get required + # attribute, so skip the resource + self.log.debug('Requested cleanup attribute %s is not ' + 'available on the resource' % k) + part_cond.append(False) + except Exception: + self.log.exception('Error during condition evaluation') + if all(part_cond): + return True + else: + return False def _json_response(response, result_key=None, error_message=None): diff --git a/openstack/tests/functional/cloud/test_project_cleanup.py b/openstack/tests/functional/cloud/test_project_cleanup.py index 01a152cda..1295de87e 100644 --- a/openstack/tests/functional/cloud/test_project_cleanup.py +++ b/openstack/tests/functional/cloud/test_project_cleanup.py @@ -48,9 +48,46 @@ def _create_network_resources(self): self.router.id, subnet_id=self.subnet.id) - def _test_cleanup(self): + def test_cleanup(self): self._create_network_resources() status_queue = queue.Queue() + + # First round - check no resources are old enough + self.conn.project_cleanup( + dry_run=True, + wait_timeout=120, + status_queue=status_queue, + filters={'created_at': '2000-01-01'}) + + self.assertTrue(status_queue.empty()) + + # Second round - resource evaluation function return false, ensure + # nothing identified + self.conn.project_cleanup( + dry_run=True, + wait_timeout=120, + status_queue=status_queue, + filters={'created_at': '2200-01-01'}, + resource_evaluation_fn=lambda x, y, z: False) + + self.assertTrue(status_queue.empty()) + + # Third round - filters set too low + self.conn.project_cleanup( + dry_run=True, + wait_timeout=120, + status_queue=status_queue, + filters={'created_at': '2200-01-01'}) + + objects = [] + while not status_queue.empty(): + objects.append(status_queue.get()) + + # At least known networks should be identified + net_names = list(obj.name for obj in objects) + self.assertIn(self.network_name, net_names) + + # Fourth round - dry run with no filters, ensure everything identified self.conn.project_cleanup( dry_run=True, wait_timeout=120, @@ -67,6 +104,7 @@ def _test_cleanup(self): net = self.conn.network.get_network(self.net.id) self.assertEqual(net.name, self.net.name) + # Last round - do a real cleanup self.conn.project_cleanup( dry_run=False, wait_timeout=600, @@ -81,6 +119,3 @@ def _test_cleanup(self): # Since we might not have enough privs to drop all nets - ensure # we do not have our known one self.assertNotIn(self.network_name, net_names) - - def test_cleanup(self): - self._test_cleanup() diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index 9b8d97a08..4263fdfb6 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -14,6 +14,7 @@ from unittest import mock import munch +import queue from openstack import exceptions from openstack import proxy @@ -518,3 +519,182 @@ def test_extract_name(self): results = proxy.Proxy(mock.Mock())._extract_name(self.url) self.assertEqual(self.parts, results) + + +class TestProxyCleanup(base.TestCase): + + def setUp(self): + super(TestProxyCleanup, self).setUp() + + self.session = mock.Mock() + self.session._sdk_connection = self.cloud + + self.fake_id = 1 + self.fake_name = "fake_name" + self.fake_result = "fake_result" + self.res = mock.Mock(spec=resource.Resource) + self.res.id = self.fake_id + self.res.created_at = '2020-01-02T03:04:05' + self.res.updated_at = '2020-01-03T03:04:05' + self.res_no_updated = mock.Mock(spec=resource.Resource) + self.res_no_updated.created_at = '2020-01-02T03:04:05' + + self.sot = proxy.Proxy(self.session) + + self.delete_mock = mock.Mock() + + def test_filters_evaluation_created_at(self): + self.assertTrue( + self.sot._service_cleanup_resource_filters_evaluation( + self.res, + filters={ + 'created_at': '2020-02-03T00:00:00' + } + ) + ) + + def test_filters_evaluation_created_at_not(self): + self.assertFalse( + self.sot._service_cleanup_resource_filters_evaluation( + self.res, + filters={ + 'created_at': '2020-01-01T00:00:00' + } + ) + ) + + def test_filters_evaluation_updated_at(self): + self.assertTrue( + self.sot._service_cleanup_resource_filters_evaluation( + self.res, + filters={ + 'updated_at': '2020-02-03T00:00:00' + } + ) + ) + + def test_filters_evaluation_updated_at_not(self): + self.assertFalse( + self.sot._service_cleanup_resource_filters_evaluation( + self.res, + filters={ + 'updated_at': '2020-01-01T00:00:00' + } + ) + ) + + def test_filters_evaluation_updated_at_missing(self): + self.assertFalse( + self.sot._service_cleanup_resource_filters_evaluation( + self.res_no_updated, + filters={ + 'updated_at': '2020-01-01T00:00:00' + } + ) + ) + + def test_filters_empty(self): + self.assertTrue( + self.sot._service_cleanup_resource_filters_evaluation( + self.res_no_updated + ) + ) + + def test_service_cleanup_dry_run(self): + self.assertTrue( + self.sot._service_cleanup_del_res( + self.delete_mock, + self.res, + dry_run=True + ) + ) + self.delete_mock.assert_not_called() + + def test_service_cleanup_dry_run_default(self): + self.assertTrue( + self.sot._service_cleanup_del_res( + self.delete_mock, + self.res + ) + ) + self.delete_mock.assert_not_called() + + def test_service_cleanup_real_run(self): + self.assertTrue( + self.sot._service_cleanup_del_res( + self.delete_mock, + self.res, + dry_run=False, + ) + ) + self.delete_mock.assert_called_with(self.res) + + def test_service_cleanup_real_run_identified_resources(self): + rd = dict() + self.assertTrue( + self.sot._service_cleanup_del_res( + self.delete_mock, + self.res, + dry_run=False, + identified_resources=rd + ) + ) + self.delete_mock.assert_called_with(self.res) + self.assertEqual(self.res, rd[self.res.id]) + + def test_service_cleanup_resource_evaluation_false(self): + self.assertFalse( + self.sot._service_cleanup_del_res( + self.delete_mock, + self.res, + dry_run=False, + resource_evaluation_fn=lambda x, y, z: False + ) + ) + self.delete_mock.assert_not_called() + + def test_service_cleanup_resource_evaluation_true(self): + self.assertTrue( + self.sot._service_cleanup_del_res( + self.delete_mock, + self.res, + dry_run=False, + resource_evaluation_fn=lambda x, y, z: True + ) + ) + self.delete_mock.assert_called() + + def test_service_cleanup_resource_evaluation_override_filters(self): + self.assertFalse( + self.sot._service_cleanup_del_res( + self.delete_mock, + self.res, + dry_run=False, + resource_evaluation_fn=lambda x, y, z: False, + filters={'created_at': '2200-01-01'} + ) + ) + + def test_service_cleanup_filters(self): + self.assertTrue( + self.sot._service_cleanup_del_res( + self.delete_mock, + self.res, + dry_run=False, + filters={'created_at': '2200-01-01'} + ) + ) + self.delete_mock.assert_called() + + def test_service_cleanup_queue(self): + q = queue.Queue() + self.assertTrue( + self.sot._service_cleanup_del_res( + self.delete_mock, + self.res, + dry_run=False, + client_status_queue=q, + filters={'created_at': '2200-01-01'} + ) + ) + self.assertEqual(self.res, q.get_nowait()) From 5ee8a2b4efa8d9df2ed4f02e0da60e1b1f47c4d6 Mon Sep 17 00:00:00 2001 From: Valery Tschopp Date: Tue, 11 Aug 2020 10:50:57 +0200 Subject: [PATCH 2703/3836] Update config to Keystone v3 Change-Id: I6ef2beaef947a59efbc8674ff71023cd69ce9246 Implements: Update cloud config for SWITCHengines Task: 40644 Story: 2008000 --- doc/source/user/config/vendor-support.rst | 2 +- openstack/config/vendors/switchengines.json | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/doc/source/user/config/vendor-support.rst b/doc/source/user/config/vendor-support.rst index 15d48b338..6d3991d7c 100644 --- a/doc/source/user/config/vendor-support.rst +++ b/doc/source/user/config/vendor-support.rst @@ -274,7 +274,7 @@ SYD Sydney, NSW SWITCHengines ------------- -https://keystone.cloud.switch.ch:5000/v2.0 +https://keystone.cloud.switch.ch:5000/v3 ============== ================ Region Name Location diff --git a/openstack/config/vendors/switchengines.json b/openstack/config/vendors/switchengines.json index 0ec23c2a9..43503018e 100644 --- a/openstack/config/vendors/switchengines.json +++ b/openstack/config/vendors/switchengines.json @@ -2,14 +2,13 @@ "name": "switchengines", "profile": { "auth": { - "auth_url": "https://keystone.cloud.switch.ch:5000/v2.0" + "auth_url": "https://keystone.cloud.switch.ch:5000/v3" }, "regions": [ "LS", "ZH" ], - "block_storage_api_version": "1", - "image_api_use_tasks": true, + "identity_api_version": "3", "image_format": "raw" } } From b7f1911d75ee3eeaba23fdb30d3bc169e5ad160e Mon Sep 17 00:00:00 2001 From: Carlos Goncalves Date: Tue, 18 Aug 2020 18:59:26 +0200 Subject: [PATCH 2704/3836] Add ALPN support to load balancer listener This adds property 'alpn_protocols' to load balancer listeners. Depends-On: https://review.opendev.org/#/c/744520/ Change-Id: I310131afb477d7c33d18963555cc44730f510c1b --- openstack/load_balancer/v2/listener.py | 5 ++++- openstack/tests/unit/load_balancer/test_listener.py | 6 +++++- ...d-balancer-listener-alpn-protocols-ded816c78bf2080c.yaml | 3 +++ 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/add-load-balancer-listener-alpn-protocols-ded816c78bf2080c.yaml diff --git a/openstack/load_balancer/v2/listener.py b/openstack/load_balancer/v2/listener.py index d8b50f0aa..c59e28c92 100644 --- a/openstack/load_balancer/v2/listener.py +++ b/openstack/load_balancer/v2/listener.py @@ -32,13 +32,16 @@ class Listener(resource.Resource, resource.TagMixin): 'sni_container_refs', 'insert_headers', 'load_balancer_id', 'timeout_client_data', 'timeout_member_connect', 'timeout_member_data', 'timeout_tcp_inspect', 'allowed_cidrs', - 'tls_ciphers', 'tls_versions', is_admin_state_up='admin_state_up', + 'tls_ciphers', 'tls_versions', 'alpn_protocols', + is_admin_state_up='admin_state_up', **resource.TagMixin._tag_query_parameters ) # Properties #: List of IPv4 or IPv6 CIDRs. allowed_cidrs = resource.Body('allowed_cidrs', type=list) + #: List of ALPN protocols. + alpn_protocols = resource.Body('alpn_protocols', type=list) #: The maximum number of connections permitted for this load balancer. #: Default is infinite. connection_limit = resource.Body('connection_limit') diff --git a/openstack/tests/unit/load_balancer/test_listener.py b/openstack/tests/unit/load_balancer/test_listener.py index fe97597a7..3d70ac9cd 100644 --- a/openstack/tests/unit/load_balancer/test_listener.py +++ b/openstack/tests/unit/load_balancer/test_listener.py @@ -42,7 +42,8 @@ 'timeout_member_data': 50000, 'timeout_tcp_inspect': 0, 'tls_ciphers': 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256', - 'tls_versions': ['TLSv1.1', 'TLSv1.2'] + 'tls_versions': ['TLSv1.1', 'TLSv1.2'], + 'alpn_protocols': ['h2', 'http/1.1', 'http/1.0'] } EXAMPLE_STATS = { @@ -109,6 +110,8 @@ def test_make_it(self): test_listener.tls_ciphers) self.assertEqual(EXAMPLE['tls_versions'], test_listener.tls_versions) + self.assertEqual(EXAMPLE['alpn_protocols'], + test_listener.alpn_protocols) self.assertDictEqual( {'limit': 'limit', @@ -141,6 +144,7 @@ def test_make_it(self): 'timeout_tcp_inspect': 'timeout_tcp_inspect', 'tls_ciphers': 'tls_ciphers', 'tls_versions': 'tls_versions', + 'alpn_protocols': 'alpn_protocols', }, test_listener._query_mapping._mapping) diff --git a/releasenotes/notes/add-load-balancer-listener-alpn-protocols-ded816c78bf2080c.yaml b/releasenotes/notes/add-load-balancer-listener-alpn-protocols-ded816c78bf2080c.yaml new file mode 100644 index 000000000..685c5695b --- /dev/null +++ b/releasenotes/notes/add-load-balancer-listener-alpn-protocols-ded816c78bf2080c.yaml @@ -0,0 +1,3 @@ +--- +features: + - Adds ALPN protocols support for the Octavia (load_balancer) listeners. From 5db7d5573a1afaed7d7b5805f3832e02354ee6a4 Mon Sep 17 00:00:00 2001 From: Ian Wienand Date: Mon, 31 Aug 2020 10:09:36 +1000 Subject: [PATCH 2705/3836] Switch nodepool test to containers The nodepool-functional-container-openstack-siblings builds nodepool container images (with openstacksdk and other "siblings" of nodepool from source) and runs the build/boot tests with that. The container builds of these tools are what OpenDev uses in production, so this is more practical testing than installing by parts on a bare Bionic system. This allows us to remove the job duplication in nodepool. Change-Id: I91345ebf509238ca1f800cb595818c023cc56b0d --- .zuul.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 2387c5951..771abc1f9 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -385,7 +385,9 @@ - release-notes-jobs-python3 check: jobs: - - nodepool-functional-openstack-src + - opendev-buildset-registry + - nodepool-build-image-siblings + - nodepool-functional-container-openstack-siblings - openstacksdk-functional-devstack - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-senlin @@ -412,7 +414,9 @@ voting: false gate: jobs: - - nodepool-functional-openstack-src + - opendev-buildset-registry + - nodepool-build-image-siblings + - nodepool-functional-container-openstack-siblings - openstacksdk-functional-devstack - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-senlin From 4b7a10839171a625f9ebd87abdef04e611d7ddd6 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Fri, 4 Sep 2020 10:34:39 +0200 Subject: [PATCH 2706/3836] Fix a bogus error in config loader when using several args with dashes Ironic routinely uses arguments like: --driver-info key1=value1 --driver-info key2=value2 This is currently broken because of the duplication detection logic. Fix it to only trigger when there are several *different* froms of the same argument. Change-Id: I326d168d22476b86ae986b471b7c424e153f7076 --- openstack/config/loader.py | 4 ++-- openstack/tests/unit/config/test_loader.py | 24 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 6e931d663..06307ad3a 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -102,7 +102,7 @@ def _fix_argv(argv): # Transform any _ characters in arg names to - so that we don't # have to throw billions of compat argparse arguments around all # over the place. - processed = collections.defaultdict(list) + processed = collections.defaultdict(set) for index in range(0, len(argv)): # If the value starts with '--' and has '-' or '_' in it, then # it's worth looking at it @@ -114,7 +114,7 @@ def _fix_argv(argv): split_args[0] = new argv[index] = "=".join(split_args) # Save both for later so we can throw an error about dupes - processed[new].append(orig) + processed[new].add(orig) overlap = [] for new, old in processed.items(): if len(old) > 1: diff --git a/openstack/tests/unit/config/test_loader.py b/openstack/tests/unit/config/test_loader.py index 9772d1a91..9e54fbcfa 100644 --- a/openstack/tests/unit/config/test_loader.py +++ b/openstack/tests/unit/config/test_loader.py @@ -17,6 +17,7 @@ import textwrap from openstack.config import loader +from openstack import exceptions from openstack.tests.unit.config import base FILES = { @@ -115,3 +116,26 @@ def test__load_yaml_json_file_nonexisting(self): path, result = loader.OpenStackConfig()._load_yaml_json_file( tested_files) self.assertEqual(None, path) + + +class TestFixArgv(base.TestCase): + def test_no_changes(self): + argv = ['-a', '-b', '--long-arg', '--multi-value', 'key1=value1', + '--multi-value', 'key2=value2'] + expected = argv[:] + loader._fix_argv(argv) + self.assertEqual(expected, argv) + + def test_replace(self): + argv = ['-a', '-b', '--long-arg', '--multi_value', 'key1=value1', + '--multi_value', 'key2=value2'] + expected = ['-a', '-b', '--long-arg', '--multi-value', 'key1=value1', + '--multi-value', 'key2=value2'] + loader._fix_argv(argv) + self.assertEqual(expected, argv) + + def test_mix(self): + argv = ['-a', '-b', '--long-arg', '--multi_value', 'key1=value1', + '--multi-value', 'key2=value2'] + self.assertRaises(exceptions.ConfigException, + loader._fix_argv, argv) From 9db62eb6308b4f33bd4a606ce0a2fe8e345816b1 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 4 Sep 2020 16:29:03 +0200 Subject: [PATCH 2707/3836] Squeeze SnapshotDetail class into Snapshot We did same for all other block_storage resources but somehow missed Snapshot. This change gives much better opportunities to operate the resource doesn't matter how it was retrieved (with details or not) Change-Id: I2b1263d124f6b14deacc944995e2072b9407b31b --- openstack/block_storage/v3/_proxy.py | 11 ++++--- openstack/block_storage/v3/snapshot.py | 16 ++-------- .../tests/unit/block_storage/v3/test_proxy.py | 3 +- .../unit/block_storage/v3/test_snapshot.py | 29 ++----------------- 4 files changed, 13 insertions(+), 46 deletions(-) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index a025c463d..247bf3b2e 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -54,10 +54,9 @@ def snapshots(self, details=True, **query): """Retrieve a generator of snapshots :param bool details: When set to ``False`` - :class:`~openstack.block_storage.v3.snapshot.Snapshot` - objects will be returned. The default, ``True``, will cause - :class:`~openstack.block_storage.v3.snapshot.SnapshotDetail` - objects to be returned. + :class:`~openstack.block_storage.v3.snapshot.Snapshot` + objects will be returned. The default, ``True``, will cause + more attributes to be returned. :param kwargs query: Optional query parameters to be sent to limit the snapshots being returned. Available parameters include: @@ -69,8 +68,8 @@ def snapshots(self, details=True, **query): :returns: A generator of snapshot objects. """ - snapshot = _snapshot.SnapshotDetail if details else _snapshot.Snapshot - return self._list(snapshot, **query) + base_path = '/snapshots/detail' if details else None + return self._list(_snapshot.Snapshot, base_path=base_path, **query) def create_snapshot(self, **attrs): """Create a new snapshot from attributes diff --git a/openstack/block_storage/v3/snapshot.py b/openstack/block_storage/v3/snapshot.py index 120b50b50..04b8e7181 100644 --- a/openstack/block_storage/v3/snapshot.py +++ b/openstack/block_storage/v3/snapshot.py @@ -51,20 +51,10 @@ class Snapshot(resource.Resource): #: Indicate whether to create snapshot, even if the volume is attached. #: Default is ``False``. *Type: bool* is_forced = resource.Body("force", type=format.BoolStr) - - -class SnapshotDetail(Snapshot): - - base_path = "/snapshots/detail" - - # capabilities - allow_fetch = False - allow_create = False - allow_delete = False - allow_commit = False - allow_list = True - #: The percentage of completeness the snapshot is currently at. progress = resource.Body("os-extended-snapshot-attributes:progress") #: The project ID this snapshot is associated with. project_id = resource.Body("os-extended-snapshot-attributes:project_id") + + +SnapshotDetail = Snapshot diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 8af6eaa4d..43b6c9107 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -35,7 +35,8 @@ def test_snapshot_find(self): def test_snapshots_detailed(self): self.verify_list(self.proxy.snapshots, snapshot.SnapshotDetail, method_kwargs={"details": True, "query": 1}, - expected_kwargs={"query": 1}) + expected_kwargs={"query": 1, + "base_path": "/snapshots/detail"}) def test_snapshots_not_detailed(self): self.verify_list(self.proxy.snapshots, snapshot.Snapshot, diff --git a/openstack/tests/unit/block_storage/v3/test_snapshot.py b/openstack/tests/unit/block_storage/v3/test_snapshot.py index f55816807..ae980542e 100644 --- a/openstack/tests/unit/block_storage/v3/test_snapshot.py +++ b/openstack/tests/unit/block_storage/v3/test_snapshot.py @@ -26,17 +26,11 @@ "id": FAKE_ID, "name": "snap-001", "force": "true", -} - -DETAILS = { "os-extended-snapshot-attributes:progress": "100%", "os-extended-snapshot-attributes:project_id": "0c2eba2c5af04d3f9e9d0d410b371fde" } -DETAILED_SNAPSHOT = SNAPSHOT.copy() -DETAILED_SNAPSHOT.update(**DETAILS) - class TestSnapshot(base.TestCase): @@ -68,27 +62,10 @@ def test_create_basic(self): self.assertEqual(SNAPSHOT["volume_id"], sot.volume_id) self.assertEqual(SNAPSHOT["size"], sot.size) self.assertEqual(SNAPSHOT["name"], sot.name) - self.assertTrue(sot.is_forced) - - -class TestSnapshotDetail(base.TestCase): - - def test_basic(self): - sot = snapshot.SnapshotDetail(DETAILED_SNAPSHOT) - self.assertIsInstance(sot, snapshot.Snapshot) - self.assertEqual("/snapshots/detail", sot.base_path) - self.assertFalse(sot.allow_fetch) - self.assertFalse(sot.allow_commit) - self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_delete) - self.assertTrue(sot.allow_list) - - def test_create_detailed(self): - sot = snapshot.SnapshotDetail(**DETAILED_SNAPSHOT) - self.assertEqual( - DETAILED_SNAPSHOT["os-extended-snapshot-attributes:progress"], + SNAPSHOT["os-extended-snapshot-attributes:progress"], sot.progress) self.assertEqual( - DETAILED_SNAPSHOT["os-extended-snapshot-attributes:project_id"], + SNAPSHOT["os-extended-snapshot-attributes:project_id"], sot.project_id) + self.assertTrue(sot.is_forced) From 10017ff68fb5f6111f1016c375ec17099f4b6957 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 4 Sep 2020 16:37:44 +0200 Subject: [PATCH 2708/3836] Repair 2 deprecation warnings Using SDK uncovered 2 code deprecation warnings: openstack/resource.py:210: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3, and in 3.9 it will stop working class _ComponentManager(collections.MutableMapping): openstack/resource.py:351: DeprecationWarning: inspect.getargspec() is deprecated since Python 3.0, use inspect.signature() or inspect.getfullargspec() len(inspect.getargspec(type_).args) > 1) # Follow recommendations and replace those usages Change-Id: I0a38dceb1739ba0601d751d5a9837524353475b5 --- openstack/resource.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openstack/resource.py b/openstack/resource.py index 53bdbb75f..34f9c46a9 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -207,7 +207,7 @@ class Computed(_BaseComponent): key = "_computed" -class _ComponentManager(collections.MutableMapping): +class _ComponentManager(collections.abc.MutableMapping): """Storage of a component type""" def __init__(self, attributes=None, synchronized=False): @@ -348,7 +348,7 @@ def _transpose(self, query, resource_type): # single-argument (like int) and double-argument type functions. try: provide_resource_type = ( - len(inspect.getargspec(type_).args) > 1) + len(inspect.getfullargspec(type_).args) > 1) except TypeError: provide_resource_type = False From ce9a3cbe55c5b556fa49ce7051b4f0b5e62efb87 Mon Sep 17 00:00:00 2001 From: Dmitriy Rabotyagov Date: Mon, 7 Sep 2020 19:04:34 +0300 Subject: [PATCH 2709/3836] Add _max_microversion for aggregates Since compute api 2.41[1] uuid filed has been added to the aggregates. We should be able to return it in the aggregate object [1] https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id38 Change-Id: If02a711e02fd72989cde8e2c25e5b4569ffe628e --- openstack/compute/v2/aggregate.py | 4 ++++ openstack/tests/unit/cloud/test_aggregate.py | 1 + openstack/tests/unit/compute/v2/test_aggregate.py | 8 +++++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/openstack/compute/v2/aggregate.py b/openstack/compute/v2/aggregate.py index ab0f05d0f..f0d8d8501 100644 --- a/openstack/compute/v2/aggregate.py +++ b/openstack/compute/v2/aggregate.py @@ -38,6 +38,10 @@ class Aggregate(resource.Resource): hosts = resource.Body('hosts') #: Metadata metadata = resource.Body('metadata') + #: UUID + uuid = resource.Body('uuid') + # uuid introduced in 2.41 + _max_microversion = '2.41' def _action(self, session, body, microversion=None): """Preform aggregate actions given the message body.""" diff --git a/openstack/tests/unit/cloud/test_aggregate.py b/openstack/tests/unit/cloud/test_aggregate.py index 45975bbc1..704f44dab 100644 --- a/openstack/tests/unit/cloud/test_aggregate.py +++ b/openstack/tests/unit/cloud/test_aggregate.py @@ -20,6 +20,7 @@ def setUp(self): super(TestAggregate, self).setUp() self.aggregate_name = self.getUniqueString('aggregate') self.fake_aggregate = fakes.make_fake_aggregate(1, self.aggregate_name) + self.use_compute_discovery() def test_create_aggregate(self): create_aggregate = self.fake_aggregate.copy() diff --git a/openstack/tests/unit/compute/v2/test_aggregate.py b/openstack/tests/unit/compute/v2/test_aggregate.py index 092d0dad5..b43dee5d9 100644 --- a/openstack/tests/unit/compute/v2/test_aggregate.py +++ b/openstack/tests/unit/compute/v2/test_aggregate.py @@ -12,9 +12,13 @@ from unittest import mock +from keystoneauth1 import adapter + from openstack.compute.v2 import aggregate from openstack.tests.unit import base +IDENTIFIER = 'IDENTIFIER' + EXAMPLE = { "name": "m-family", "availability_zone": None, @@ -24,6 +28,7 @@ "hosts": ["oscomp-m001", "oscomp-m002", "oscomp-m003"], "deleted_at": None, "id": 4, + "uuid": IDENTIFIER, "metadata": {"type": "public", "family": "m-family"} } @@ -37,7 +42,7 @@ def setUp(self): self.resp.json = mock.Mock(return_value=self.resp.body) self.resp.status_code = 200 self.resp.headers = {'Accept': ''} - self.sess = mock.Mock() + self.sess = mock.Mock(spec=adapter.Adapter) self.sess.post = mock.Mock(return_value=self.resp) def test_basic(self): @@ -58,6 +63,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['deleted'], sot.deleted) self.assertEqual(EXAMPLE['hosts'], sot.hosts) self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['uuid'], sot.uuid) self.assertDictEqual(EXAMPLE['metadata'], sot.metadata) def test_add_host(self): From b30ff611f1800af83d26b8d69ca59aee741e162d Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Tue, 8 Sep 2020 12:26:21 +0000 Subject: [PATCH 2710/3836] Update master for stable/victoria Add file to the reno documentation build to show release notes for stable/victoria. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/victoria. Change-Id: I3187ecd1642c15ad05cd94cf3be00218f32b7ff8 Sem-Ver: feature --- releasenotes/source/index.rst | 1 + releasenotes/source/victoria.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/victoria.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index f169e76b3..0bd14ba7a 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ :maxdepth: 1 unreleased + victoria ussuri train stein diff --git a/releasenotes/source/victoria.rst b/releasenotes/source/victoria.rst new file mode 100644 index 000000000..4efc7b6f3 --- /dev/null +++ b/releasenotes/source/victoria.rst @@ -0,0 +1,6 @@ +============================= +Victoria Series Release Notes +============================= + +.. release-notes:: + :branch: stable/victoria From 5805b461eb78dce4a807010af95615057e361229 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Sat, 5 Sep 2020 14:13:40 +0200 Subject: [PATCH 2711/3836] Add additional compute flavor operations In order to proceed with switching flavor in OSC from novaclient/self api onto SDK few new methods need to be supported. Those include flavor access and set of extra_specs related operations. While extra_specs in terms of SDK should be a separate resource, it is not easy to implement it this way. On the other hand all those operations are definive subset of flavor related operations. In addition also merge FlavorDetail into Flavor class Change-Id: Ia4f60acce5e0e5665e2ba704fe4cd0ec81437eb5 --- doc/source/user/proxies/compute.rst | 7 +- openstack/compute/v2/_proxy.py | 127 +++++++++++-- openstack/compute/v2/flavor.py | 121 +++++++++++- .../functional/compute/v2/test_flavor.py | 97 +++++++++- .../tests/unit/compute/v2/test_flavor.py | 176 +++++++++++++++++- openstack/tests/unit/compute/v2/test_proxy.py | 157 +++++++++++++++- ...d-compute-flavor-ops-12149e58299c413e.yaml | 7 + 7 files changed, 652 insertions(+), 40 deletions(-) create mode 100644 releasenotes/notes/add-compute-flavor-ops-12149e58299c413e.yaml diff --git a/doc/source/user/proxies/compute.rst b/doc/source/user/proxies/compute.rst index cc59d322c..eaa904271 100644 --- a/doc/source/user/proxies/compute.rst +++ b/doc/source/user/proxies/compute.rst @@ -66,7 +66,12 @@ Flavor Operations .. autoclass:: openstack.compute.v2._proxy.Proxy :noindex: - :members: create_flavor, delete_flavor, get_flavor, find_flavor, flavors + :members: create_flavor, delete_flavor, get_flavor, find_flavor, flavors, + flavor_add_tenant_access, flavor_remove_tenant_access, + get_flavor_access, fetch_flavor_extra_specs, + create_flavor_extra_specs, get_flavor_extra_specs_property, + update_flavor_extra_specs_property, + delete_flavor_extra_specs_property Service Operations ^^^^^^^^^^^^^^^^^^ diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index fb2aa5464..b6d3c7a2e 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -58,7 +58,10 @@ def extensions(self): """ return self._list(extension.Extension) - def find_flavor(self, name_or_id, ignore_missing=True): + # ========== Flavors ========== + + def find_flavor(self, name_or_id, ignore_missing=True, + get_extra_specs=False): """Find a single flavor :param name_or_id: The name or ID of a flavor. @@ -69,8 +72,11 @@ def find_flavor(self, name_or_id, ignore_missing=True): attempting to find a nonexistent resource. :returns: One :class:`~openstack.compute.v2.flavor.Flavor` or None """ - return self._find(_flavor.Flavor, name_or_id, - ignore_missing=ignore_missing) + flavor = self._find(_flavor.Flavor, name_or_id, + ignore_missing=ignore_missing) + if flavor and get_extra_specs and not flavor.extra_specs: + flavor = flavor.fetch_extra_specs(self) + return flavor def create_flavor(self, **attrs): """Create a new flavor from attributes @@ -99,7 +105,7 @@ def delete_flavor(self, flavor, ignore_missing=True): """ self._delete(_flavor.Flavor, flavor, ignore_missing=ignore_missing) - def get_flavor(self, flavor): + def get_flavor(self, flavor, get_extra_specs=False): """Get a single flavor :param flavor: The value can be the ID of a flavor or a @@ -109,22 +115,121 @@ def get_flavor(self, flavor): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get(_flavor.Flavor, flavor) + flavor = self._get(_flavor.Flavor, flavor) + if get_extra_specs and not flavor.extra_specs: + flavor = flavor.fetch_extra_specs(self) + return flavor def flavors(self, details=True, **query): """Return a generator of flavors :param bool details: When ``True``, returns - :class:`~openstack.compute.v2.flavor.FlavorDetail` objects, - otherwise :class:`~openstack.compute.v2.flavor.Flavor`. - *Default: ``True``* + :class:`~openstack.compute.v2.flavor.Flavor` objects, + with additional attributes filled. :param kwargs query: Optional query parameters to be sent to limit - the flavors being returned. + the flavors being returned. :returns: A generator of flavor objects """ - flv = _flavor.FlavorDetail if details else _flavor.Flavor - return self._list(flv, **query) + base_path = '/flavors/detail' if details else '/flavors' + return self._list(_flavor.Flavor, base_path=base_path, **query) + + def flavor_add_tenant_access(self, flavor, tenant): + """Adds tenant/project access to flavor. + + :param flavor: Either the ID of a flavor or a + :class:`~openstack.compute.v2.flavor.Flavor` instance. + :param str tenant: The UUID of the tenant. + + :returns: One :class:`~openstack.compute.v2.flavor.Flavor` + """ + flavor = self._get_resource(_flavor.Flavor, flavor) + return flavor.add_tenant_access(self, tenant) + + def flavor_remove_tenant_access(self, flavor, tenant): + """Removes tenant/project access to flavor. + + :param flavor: Either the ID of a flavor or a + :class:`~openstack.compute.v2.flavor.Flavor` instance. + :param str tenant: The UUID of the tenant. + + :returns: One :class:`~openstack.compute.v2.flavor.Flavor` + """ + flavor = self._get_resource(_flavor.Flavor, flavor) + return flavor.remove_tenant_access(self, tenant) + + def get_flavor_access(self, flavor): + """Lists tenants who have access to private flavor + + :param flavor: Either the ID of a flavor or a + :class:`~openstack.compute.v2.flavor.Flavor` instance. + + :returns: List of dicts with flavor_id and tenant_id attributes. + """ + flavor = self._get_resource(_flavor.Flavor, flavor) + return flavor.get_access(self) + + def fetch_flavor_extra_specs(self, flavor): + """Lists Extra Specs of a flavor + + :param flavor: Either the ID of a flavor or a + :class:`~openstack.compute.v2.flavor.Flavor` instance. + + :returns: One :class:`~openstack.compute.v2.flavor.Flavor` + """ + flavor = self._get_resource(_flavor.Flavor, flavor) + return flavor.fetch_extra_specs(self) + + def create_flavor_extra_specs(self, flavor, extra_specs): + """Lists Extra Specs of a flavor + + :param flavor: Either the ID of a flavor or a + :class:`~openstack.compute.v2.flavor.Flavor` instance. + :param dict extra_specs: dict of extra specs + + :returns: One :class:`~openstack.compute.v2.flavor.Flavor` + """ + flavor = self._get_resource(_flavor.Flavor, flavor) + return flavor.create_extra_specs(self, specs=extra_specs) + + def get_flavor_extra_specs_property(self, flavor, prop): + """Get specific Extra Spec property of a flavor + + :param flavor: Either the ID of a flavor or a + :class:`~openstack.compute.v2.flavor.Flavor` instance. + :param str prop: Property name. + + :returns: String value of the requested property. + """ + flavor = self._get_resource(_flavor.Flavor, flavor) + return flavor.get_extra_specs_property(self, prop) + + def update_flavor_extra_specs_property(self, flavor, prop, val): + """Update specific Extra Spec property of a flavor + + :param flavor: Either the ID of a flavor or a + :class:`~openstack.compute.v2.flavor.Flavor` instance. + :param str prop: Property name. + :param str val: Property value. + + :returns: String value of the requested property. + """ + flavor = self._get_resource(_flavor.Flavor, flavor) + return flavor.update_extra_specs_property(self, prop, val) + + def delete_flavor_extra_specs_property(self, flavor, prop): + """Delete specific Extra Spec property of a flavor + + :param flavor: Either the ID of a flavor or a + :class:`~openstack.compute.v2.flavor.Flavor` instance. + :param str prop: Property name. + + :returns: None + """ + flavor = self._get_resource(_flavor.Flavor, flavor) + return flavor.delete_extra_specs_property(self, prop) + + # ========== Aggregates ========== def aggregates(self, **query): """Return a generator of aggregate diff --git a/openstack/compute/v2/flavor.py b/openstack/compute/v2/flavor.py index 1e6c65eb9..00c9ec54b 100644 --- a/openstack/compute/v2/flavor.py +++ b/openstack/compute/v2/flavor.py @@ -10,7 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import exceptions from openstack import resource +from openstack import utils class Flavor(resource.Resource): @@ -62,12 +64,117 @@ class Flavor(resource.Resource): #: A dictionary of the flavor's extra-specs key-and-value pairs. extra_specs = resource.Body('extra_specs', type=dict) + @classmethod + def list(cls, session, paginated=True, base_path='/flavors/detail', + allow_unknown_params=False, **params): + # Find will invoke list when name was passed. Since we want to return + # flavor with details (same as direct get) we need to swap default here + # and list with "/flavors" if no details explicitely requested + if 'is_public' not in params or params['is_public'] is None: + # is_public is ternary - None means give all flavors. + # Force it to string to avoid requests skipping it. + params['is_public'] = 'None' + return super(Flavor, cls).list( + session, paginated=paginated, + base_path=base_path, + allow_unknown_params=allow_unknown_params, + **params) -class FlavorDetail(Flavor): - base_path = '/flavors/detail' + def _action(self, session, body, microversion=None): + """Preform flavor actions given the message body.""" + url = utils.urljoin(Flavor.base_path, self.id, 'action') + headers = {'Accept': ''} + attrs = {} + if microversion: + # Do not reset microversion if it is set on a session level + attrs['microversion'] = microversion + response = session.post( + url, json=body, headers=headers, **attrs) + exceptions.raise_from_response(response) + return response - allow_create = False - allow_fetch = False - allow_commit = False - allow_delete = False - allow_list = True + def add_tenant_access(self, session, tenant): + """Adds flavor access to a tenant and flavor.""" + body = {'addTenantAccess': {'tenant': tenant}} + self._action(session, body) + + def remove_tenant_access(self, session, tenant): + """Removes flavor access to a tenant and flavor.""" + body = {'removeTenantAccess': {'tenant': tenant}} + self._action(session, body) + + def get_access(self, session): + """Lists tenants who have access to a private flavor and adds private + flavor access to and removes private flavor access from tenants. By + default, only administrators can manage private flavor access. A + private flavor has is_public set to false while a public flavor has + is_public set to true. + + :return: List of dicts with flavor_id and tenant_id attributes + """ + url = utils.urljoin(Flavor.base_path, self.id, 'os-flavor-access') + response = session.get(url) + exceptions.raise_from_response(response) + return response.json().get('flavor_access', []) + + def fetch_extra_specs(self, session): + """Fetch extra_specs of the flavor + Starting with 2.61 extra_specs are returned with the flavor details, + before that a separate call is required + """ + url = utils.urljoin(Flavor.base_path, self.id, 'os-extra_specs') + microversion = self._get_microversion_for(session, 'fetch') + response = session.get(url, microversion=microversion) + exceptions.raise_from_response(response) + specs = response.json().get('extra_specs', {}) + self._update(extra_specs=specs) + return self + + def create_extra_specs(self, session, specs): + """Creates extra specs for a flavor""" + url = utils.urljoin(Flavor.base_path, self.id, 'os-extra_specs') + microversion = self._get_microversion_for(session, 'create') + response = session.post( + url, + json={'extra_specs': specs}, + microversion=microversion) + exceptions.raise_from_response(response) + specs = response.json().get('extra_specs', {}) + self._update(extra_specs=specs) + return self + + def get_extra_specs_property(self, session, prop): + """Get individual extra_spec property""" + url = utils.urljoin(Flavor.base_path, self.id, + 'os-extra_specs', prop) + microversion = self._get_microversion_for(session, 'fetch') + response = session.get(url, microversion=microversion) + exceptions.raise_from_response(response) + val = response.json().get(prop) + return val + + def update_extra_specs_property(self, session, prop, val): + """Update An Extra Spec For A Flavor""" + url = utils.urljoin(Flavor.base_path, self.id, + 'os-extra_specs', prop) + microversion = self._get_microversion_for(session, 'commit') + response = session.put( + url, + json={prop: val}, + microversion=microversion) + exceptions.raise_from_response(response) + val = response.json().get(prop) + return val + + def delete_extra_specs_property(self, session, prop): + """Delete An Extra Spec For A Flavor""" + url = utils.urljoin(Flavor.base_path, self.id, + 'os-extra_specs', prop) + microversion = self._get_microversion_for(session, 'delete') + response = session.delete( + url, + microversion=microversion) + exceptions.raise_from_response(response) + + +FlavorDetail = Flavor diff --git a/openstack/tests/functional/compute/v2/test_flavor.py b/openstack/tests/functional/compute/v2/test_flavor.py index 92d636a4b..13281edcb 100644 --- a/openstack/tests/functional/compute/v2/test_flavor.py +++ b/openstack/tests/functional/compute/v2/test_flavor.py @@ -9,7 +9,7 @@ # 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 uuid from openstack import exceptions from openstack.tests.functional import base @@ -19,7 +19,7 @@ class TestFlavor(base.BaseFunctionalTest): def setUp(self): super(TestFlavor, self).setUp() - + self.new_item_name = self.getUniqueString('flavor') self.one_flavor = list(self.conn.compute.flavors())[0] def test_flavors(self): @@ -50,3 +50,96 @@ def test_find_flavors_no_match_ignore_false(self): self.assertRaises(exceptions.ResourceNotFound, self.conn.compute.find_flavor, "not a flavor", ignore_missing=False) + + def test_list_flavors(self): + pub_flavor_name = self.new_item_name + '_public' + priv_flavor_name = self.new_item_name + '_private' + public_kwargs = dict( + name=pub_flavor_name, ram=1024, vcpus=2, disk=10, is_public=True + ) + private_kwargs = dict( + name=priv_flavor_name, ram=1024, vcpus=2, disk=10, is_public=False + ) + + # Create a public and private flavor. We expect both to be listed + # for an operator. + self.operator_cloud.compute.create_flavor(**public_kwargs) + self.operator_cloud.compute.create_flavor(**private_kwargs) + + flavors = self.operator_cloud.compute.flavors() + + # Flavor list will include the standard devstack flavors. We just want + # to make sure both of the flavors we just created are present. + found = [] + for f in flavors: + # extra_specs should be added within list_flavors() + self.assertIn('extra_specs', f) + if f['name'] in (pub_flavor_name, priv_flavor_name): + found.append(f) + self.assertEqual(2, len(found)) + + def test_flavor_access(self): + flavor_name = uuid.uuid4().hex + flv = self.operator_cloud.compute.create_flavor( + is_public=False, + name=flavor_name, + ram=128, + vcpus=1, + disk=0) + self.addCleanup(self.conn.compute.delete_flavor, flv.id) + # Validate the 'demo' user cannot see the new flavor + flv_cmp = self.user_cloud.compute.find_flavor(flavor_name) + self.assertIsNone(flv_cmp) + + # Validate we can see the new flavor ourselves + flv_cmp = self.operator_cloud.compute.find_flavor(flavor_name) + self.assertIsNotNone(flv_cmp) + self.assertEqual(flavor_name, flv_cmp.name) + + project = self.operator_cloud.get_project('demo') + self.assertIsNotNone(project) + + # Now give 'demo' access + self.operator_cloud.compute.flavor_add_tenant_access( + flv.id, project['id']) + + # Now see if the 'demo' user has access to it + flv_cmp = self.user_cloud.compute.find_flavor( + flavor_name) + self.assertIsNotNone(flv_cmp) + + # Now remove 'demo' access and check we can't find it + self.operator_cloud.compute.flavor_remove_tenant_access( + flv.id, project['id']) + + flv_cmp = self.user_cloud.compute.find_flavor( + flavor_name) + self.assertIsNone(flv_cmp) + + def test_extra_props_calls(self): + flavor_name = uuid.uuid4().hex + flv = self.conn.compute.create_flavor( + is_public=False, + name=flavor_name, + ram=128, + vcpus=1, + disk=0) + self.addCleanup(self.conn.compute.delete_flavor, flv.id) + # Create extra_specs + specs = { + 'a': 'b' + } + self.conn.compute.create_flavor_extra_specs(flv, extra_specs=specs) + # verify specs + flv_cmp = self.conn.compute.fetch_flavor_extra_specs(flv) + self.assertDictEqual(specs, flv_cmp.extra_specs) + # update + self.conn.compute.update_flavor_extra_specs_property(flv, 'c', 'd') + val_cmp = self.conn.compute.get_flavor_extra_specs_property(flv, 'c') + # fetch single prop + self.assertEqual('d', val_cmp) + # drop new prop + self.conn.compute.delete_flavor_extra_specs_property(flv, 'c') + # re-fetch and ensure prev state + flv_cmp = self.conn.compute.fetch_flavor_extra_specs(flv) + self.assertDictEqual(specs, flv_cmp.extra_specs) diff --git a/openstack/tests/unit/compute/v2/test_flavor.py b/openstack/tests/unit/compute/v2/test_flavor.py index c781711d2..bdaff9b79 100644 --- a/openstack/tests/unit/compute/v2/test_flavor.py +++ b/openstack/tests/unit/compute/v2/test_flavor.py @@ -9,6 +9,9 @@ # 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 unittest import mock + +from keystoneauth1 import adapter from openstack.tests.unit import base @@ -33,6 +36,12 @@ class TestFlavor(base.TestCase): + def setUp(self): + super(TestFlavor, self).setUp() + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.default_microversion = 1 + self.sess._get_connection = mock.Mock(return_value=self.cloud) + def test_basic(self): sot = flavor.Flavor() self.assertEqual('flavor', sot.resource_key) @@ -71,13 +80,160 @@ def test_make_basic(self): sot.is_disabled) self.assertEqual(BASIC_EXAMPLE['rxtx_factor'], sot.rxtx_factor) - def test_detail(self): - sot = flavor.FlavorDetail() - self.assertEqual('flavor', sot.resource_key) - self.assertEqual('flavors', sot.resources_key) - self.assertEqual('/flavors/detail', sot.base_path) - self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_fetch) - self.assertFalse(sot.allow_commit) - self.assertFalse(sot.allow_delete) - self.assertTrue(sot.allow_list) + def test_add_tenant_access(self): + sot = flavor.Flavor(**BASIC_EXAMPLE) + resp = mock.Mock() + resp.body = None + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + self.sess.post = mock.Mock(return_value=resp) + + sot.add_tenant_access(self.sess, 'fake_tenant') + + self.sess.post.assert_called_with( + 'flavors/IDENTIFIER/action', + json={ + 'addTenantAccess': { + 'tenant': 'fake_tenant'}}, + headers={'Accept': ''} + ) + + def test_remove_tenant_access(self): + sot = flavor.Flavor(**BASIC_EXAMPLE) + resp = mock.Mock() + resp.body = None + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + self.sess.post = mock.Mock(return_value=resp) + + sot.remove_tenant_access(self.sess, 'fake_tenant') + + self.sess.post.assert_called_with( + 'flavors/IDENTIFIER/action', + json={ + 'removeTenantAccess': { + 'tenant': 'fake_tenant'}}, + headers={'Accept': ''} + ) + + def test_get_flavor_access(self): + sot = flavor.Flavor(**BASIC_EXAMPLE) + resp = mock.Mock() + resp.body = {'flavor_access': [ + {'flavor_id': 'fake_flavor', + 'tenant_id': 'fake_tenant'} + ]} + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + self.sess.get = mock.Mock(return_value=resp) + + rsp = sot.get_access(self.sess) + + self.sess.get.assert_called_with( + 'flavors/IDENTIFIER/os-flavor-access', + ) + + self.assertEqual(resp.body['flavor_access'], rsp) + + def test_fetch_extra_specs(self): + sot = flavor.Flavor(**BASIC_EXAMPLE) + resp = mock.Mock() + resp.body = { + 'extra_specs': + {'a': 'b', + 'c': 'd'} + } + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + self.sess.get = mock.Mock(return_value=resp) + + rsp = sot.fetch_extra_specs(self.sess) + + self.sess.get.assert_called_with( + 'flavors/IDENTIFIER/os-extra_specs', + microversion=self.sess.default_microversion + ) + + self.assertEqual(resp.body['extra_specs'], rsp.extra_specs) + self.assertIsInstance(rsp, flavor.Flavor) + + def test_create_extra_specs(self): + sot = flavor.Flavor(**BASIC_EXAMPLE) + specs = { + 'a': 'b', + 'c': 'd' + } + resp = mock.Mock() + resp.body = { + 'extra_specs': specs + } + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + self.sess.post = mock.Mock(return_value=resp) + + rsp = sot.create_extra_specs(self.sess, specs) + + self.sess.post.assert_called_with( + 'flavors/IDENTIFIER/os-extra_specs', + json={'extra_specs': specs}, + microversion=self.sess.default_microversion + ) + + self.assertEqual(resp.body['extra_specs'], rsp.extra_specs) + self.assertIsInstance(rsp, flavor.Flavor) + + def test_get_extra_specs_property(self): + sot = flavor.Flavor(**BASIC_EXAMPLE) + resp = mock.Mock() + resp.body = { + 'a': 'b' + } + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + self.sess.get = mock.Mock(return_value=resp) + + rsp = sot.get_extra_specs_property(self.sess, 'a') + + self.sess.get.assert_called_with( + 'flavors/IDENTIFIER/os-extra_specs/a', + microversion=self.sess.default_microversion + ) + + self.assertEqual('b', rsp) + + def test_update_extra_specs_property(self): + sot = flavor.Flavor(**BASIC_EXAMPLE) + resp = mock.Mock() + resp.body = { + 'a': 'b' + } + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + self.sess.put = mock.Mock(return_value=resp) + + rsp = sot.update_extra_specs_property(self.sess, 'a', 'b') + + self.sess.put.assert_called_with( + 'flavors/IDENTIFIER/os-extra_specs/a', + json={'a': 'b'}, + microversion=self.sess.default_microversion + ) + + self.assertEqual('b', rsp) + + def test_delete_extra_specs_property(self): + sot = flavor.Flavor(**BASIC_EXAMPLE) + resp = mock.Mock() + resp.body = None + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + self.sess.delete = mock.Mock(return_value=resp) + + rsp = sot.delete_extra_specs_property(self.sess, 'a') + + self.sess.delete.assert_called_with( + 'flavors/IDENTIFIER/os-extra_specs/a', + microversion=self.sess.default_microversion + ) + + self.assertIsNone(rsp) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 3ccf1233b..516f4dfae 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -32,12 +32,8 @@ def setUp(self): super(TestComputeProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) - def test_extension_find(self): - self.verify_find(self.proxy.find_extension, extension.Extension) - - def test_extensions(self): - self.verify_list_no_kwargs(self.proxy.extensions, extension.Extension) +class TestFlavor(TestComputeProxy): def test_flavor_create(self): self.verify_create(self.proxy.create_flavor, flavor.Flavor) @@ -50,18 +46,161 @@ def test_flavor_delete_ignore(self): def test_flavor_find(self): self.verify_find(self.proxy.find_flavor, flavor.Flavor) - def test_flavor_get(self): - self.verify_get(self.proxy.get_flavor, flavor.Flavor) + def test_flavor_find_fetch_extra(self): + """fetch extra_specs is triggered""" + with mock.patch( + 'openstack.compute.v2.flavor.Flavor.fetch_extra_specs' + ) as mocked: + res = flavor.Flavor() + mocked.return_value = res + self._verify2( + 'openstack.proxy.Proxy._find', + self.proxy.find_flavor, + method_args=['res', True, True], + expected_result=res, + expected_args=[flavor.Flavor, 'res'], + expected_kwargs={'ignore_missing': True} + ) + mocked.assert_called_once() + + def test_flavor_find_skip_fetch_extra(self): + """fetch extra_specs not triggered""" + with mock.patch( + 'openstack.compute.v2.flavor.Flavor.fetch_extra_specs' + ) as mocked: + res = flavor.Flavor(extra_specs={'a': 'b'}) + mocked.return_value = res + self._verify2( + 'openstack.proxy.Proxy._find', + self.proxy.find_flavor, + method_args=['res', True], + expected_result=res, + expected_args=[flavor.Flavor, 'res'], + expected_kwargs={'ignore_missing': True} + ) + mocked.assert_not_called() + + def test_flavor_get_no_extra(self): + """fetch extra_specs not triggered""" + with mock.patch( + 'openstack.compute.v2.flavor.Flavor.fetch_extra_specs' + ) as mocked: + res = flavor.Flavor() + mocked.return_value = res + self._verify2( + 'openstack.proxy.Proxy._get', + self.proxy.get_flavor, + method_args=['res'], + expected_result=res, + expected_args=[flavor.Flavor, 'res'] + ) + mocked.assert_not_called() + + def test_flavor_get_fetch_extra(self): + """fetch extra_specs is triggered""" + with mock.patch( + 'openstack.compute.v2.flavor.Flavor.fetch_extra_specs' + ) as mocked: + res = flavor.Flavor() + mocked.return_value = res + self._verify2( + 'openstack.proxy.Proxy._get', + self.proxy.get_flavor, + method_args=['res', True], + expected_result=res, + expected_args=[flavor.Flavor, 'res'] + ) + mocked.assert_called_once() + + def test_flavor_get_skip_fetch_extra(self): + """fetch extra_specs not triggered""" + with mock.patch( + 'openstack.compute.v2.flavor.Flavor.fetch_extra_specs' + ) as mocked: + res = flavor.Flavor(extra_specs={'a': 'b'}) + mocked.return_value = res + self._verify2( + 'openstack.proxy.Proxy._get', + self.proxy.get_flavor, + method_args=['res', True], + expected_result=res, + expected_args=[flavor.Flavor, 'res'] + ) + mocked.assert_not_called() def test_flavors_detailed(self): self.verify_list(self.proxy.flavors, flavor.FlavorDetail, method_kwargs={"details": True, "query": 1}, - expected_kwargs={"query": 1}) + expected_kwargs={"query": 1, + "base_path": "/flavors/detail"}) def test_flavors_not_detailed(self): self.verify_list(self.proxy.flavors, flavor.Flavor, method_kwargs={"details": False, "query": 1}, - expected_kwargs={"query": 1}) + expected_kwargs={"query": 1, + "base_path": "/flavors"}) + + def test_flavor_get_access(self): + self._verify("openstack.compute.v2.flavor.Flavor.get_access", + self.proxy.get_flavor_access, + method_args=["value"], + expected_args=[]) + + def test_flavor_add_tenant_access(self): + self._verify("openstack.compute.v2.flavor.Flavor.add_tenant_access", + self.proxy.flavor_add_tenant_access, + method_args=["value", "fake-tenant"], + expected_args=["fake-tenant"]) + + def test_flavor_remove_tenant_access(self): + self._verify("openstack.compute.v2.flavor.Flavor.remove_tenant_access", + self.proxy.flavor_remove_tenant_access, + method_args=["value", "fake-tenant"], + expected_args=["fake-tenant"]) + + def test_flavor_fetch_extra_specs(self): + self._verify("openstack.compute.v2.flavor.Flavor.fetch_extra_specs", + self.proxy.fetch_flavor_extra_specs, + method_args=["value"], + expected_args=[]) + + def test_create_flavor_extra_specs(self): + specs = { + 'a': 'b' + } + self._verify("openstack.compute.v2.flavor.Flavor.create_extra_specs", + self.proxy.create_flavor_extra_specs, + method_args=["value", specs], + expected_kwargs={"specs": specs}) + + def test_get_flavor_extra_specs_prop(self): + self._verify( + "openstack.compute.v2.flavor.Flavor.get_extra_specs_property", + self.proxy.get_flavor_extra_specs_property, + method_args=["value", "prop"], + expected_args=["prop"]) + + def test_update_flavor_extra_specs_prop(self): + self._verify( + "openstack.compute.v2.flavor.Flavor.update_extra_specs_property", + self.proxy.update_flavor_extra_specs_property, + method_args=["value", "prop", "val"], + expected_args=["prop", "val"]) + + def test_delete_flavor_extra_specs_prop(self): + self._verify( + "openstack.compute.v2.flavor.Flavor.delete_extra_specs_property", + self.proxy.delete_flavor_extra_specs_property, + method_args=["value", "prop"], + expected_args=["prop"]) + + +class TestCompute(TestComputeProxy): + def test_extension_find(self): + self.verify_find(self.proxy.find_extension, extension.Extension) + + def test_extensions(self): + self.verify_list_no_kwargs(self.proxy.extensions, extension.Extension) def test_image_delete(self): self.verify_delete(self.proxy.delete_image, image.Image, False) diff --git a/releasenotes/notes/add-compute-flavor-ops-12149e58299c413e.yaml b/releasenotes/notes/add-compute-flavor-ops-12149e58299c413e.yaml new file mode 100644 index 000000000..fcb4e0345 --- /dev/null +++ b/releasenotes/notes/add-compute-flavor-ops-12149e58299c413e.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Add additional compute flavor operations (flavor_add_tenant_access, flavor_remove_tenant_access, get_flavor_access, extra_specs fetching/updating). +other: + - | + Merge FlavorDetails into Flavor class. From 49029b8792947733ae7d4940ef1ab3a09c1cf06d Mon Sep 17 00:00:00 2001 From: Ghanshyam Mann Date: Wed, 9 Sep 2020 17:28:18 -0500 Subject: [PATCH 2712/3836] Fix l-c testing for ubuntu focal As per victoria cycle testing runtime and community goal[1] we need to migrate upstream CI/CD to Ubuntu Focal(20.04). - Bump the lower constraints for required deps which added python3.8 support in their later version. Story: #2007865 Task: #40206 [1] https://governance.openstack.org/tc/goals/selected/victoria/migrate-ci-cd-jobs-to-ubuntu-focal.html Change-Id: If3ec085184fbba81f455164b803afb6f66e46fd0 --- lower-constraints.txt | 4 ++-- requirements.txt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index b45cc49c5..cd1e02be7 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -1,6 +1,6 @@ appdirs==1.3.0 coverage==4.0 -cryptography==2.1 +cryptography==2.7 ddt==1.0.1 decorator==4.4.1 doc8==0.8.0 @@ -25,7 +25,7 @@ prometheus-client==0.4.2 Pygments==2.2.0 python-mimeparse==1.6.0 python-subunit==1.0.0 -PyYAML==3.12 +PyYAML==3.13 requests==2.18.0 requests-mock==1.2.0 requestsexceptions==1.2.0 diff --git a/requirements.txt b/requirements.txt index 52e05ec93..78e57b411 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # 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 -PyYAML>=3.12 # MIT +PyYAML>=3.13 # MIT appdirs>=1.3.0 # MIT License requestsexceptions>=1.2.0 # Apache-2.0 jsonpatch!=1.20,>=1.16 # BSD @@ -16,6 +16,6 @@ iso8601>=0.1.11 # MIT netifaces>=0.10.4 # MIT dogpile.cache>=0.6.5 # BSD -cryptography>=2.1 # BSD/Apache-2.0 +cryptography>=2.7 # BSD/Apache-2.0 importlib_metadata>=1.7.0;python_version<'3.8' # Apache-2.0 From ec8f810f050b96837a43c92349824617938332ce Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 11 Sep 2020 12:23:58 +0200 Subject: [PATCH 2713/3836] Add some compute console operations In order to switch OSC to SDK for managing consoles add few missing (optimize existing) bits: - add server.get_console_url (deprecated, but still might functional) - add tests for server_remote_console - add compute.create_console, which depending on the supported microversion invokes appropriate underlaying operation. - add some console type validations to fail early Change-Id: I63e752e70fb394fadf88057d71669514532cdddf --- openstack/compute/v2/_proxy.py | 44 ++++++++++++ openstack/compute/v2/server.py | 17 +++++ openstack/compute/v2/server_remote_console.py | 27 +++++++ openstack/tests/unit/compute/v2/test_proxy.py | 51 +++++++++++++ .../tests/unit/compute/v2/test_server.py | 46 ++++++++++++ .../compute/v2/test_server_remote_console.py | 71 +++++++++++++++++++ ...imize-server-console-1d27c107b9a1cdc3.yaml | 4 ++ 7 files changed, 260 insertions(+) create mode 100644 openstack/tests/unit/compute/v2/test_server_remote_console.py create mode 100644 releasenotes/notes/optimize-server-console-1d27c107b9a1cdc3.yaml diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index fb2aa5464..1c979fa44 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -31,6 +31,7 @@ from openstack.network.v2 import security_group as _sg from openstack import proxy from openstack import resource +from openstack import utils class Proxy(proxy.Proxy): @@ -1488,6 +1489,49 @@ def create_server_remote_console(self, server, **attrs): return self._create(_server_remote_console.ServerRemoteConsole, server_id=server_id, **attrs) + def get_server_console_url(self, server, console_type): + """Create a remote console on the server. + + :param server: Either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance. + :param console_type: Type of the console connection. + :returns: Dictionary with console type and url + """ + server = self._get_resource(_server.Server, server) + return server.get_console_url(self, console_type) + + def create_console(self, server, console_type, console_protocol=None): + """Create a remote console on the server. + + When microversion supported is higher then 2.6 remote console is + created, otherwise deprecated call to get server console is issued. + + :param server: Either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance. + :param console_type: Type of the remote console. Supported values as: + * novnc + * spice-html5 + * rdp-html5 + * serial + * webmks (supported after 2.8) + :param console_protocol: Optional console protocol (is respected only + after microversion 2.6). + + :returns: Dictionary with console type, url and optionally protocol. + """ + server = self._get_resource(_server.Server, server) + # NOTE: novaclient supports undocumented type xcpvnc also supported + # historically by OSC. We support it, but do not document either. + if utils.supports_microversion(self, '2.6'): + console = self._create( + _server_remote_console.ServerRemoteConsole, + server_id=server.id, + type=console_type, + protocol=console_protocol) + return console.to_dict() + else: + return server.get_console_url(self, console_type) + def _get_cleanup_dependencies(self): return { 'compute': { diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 14e4ccfd9..4728ddb5b 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -17,6 +17,15 @@ from openstack import utils +CONSOLE_TYPE_ACTION_MAPPING = { + 'novnc': 'os-getVNCConsole', + 'xvpvnc': 'os-getVNCConsole', + 'spice-html5': 'os-getSPICEConsole', + 'rdp-html5': 'os-getRDPConsole', + 'serial': 'os-getSerialConsole' +} + + class Server(resource.Resource, metadata.MetadataMixin, resource.TagMixin): resource_key = 'server' resources_key = 'servers' @@ -475,6 +484,14 @@ def live_migrate(self, session, host, force, block_migration, block_migration=block_migration, disk_over_commit=disk_over_commit) + def get_console_url(self, session, console_type): + action = CONSOLE_TYPE_ACTION_MAPPING.get(console_type) + if not action: + raise ValueError("Unsupported console type") + body = {action: {'type': console_type}} + resp = self._action(session, body) + return resp.json().get('console') + def _live_migrate_30(self, session, host, force, block_migration): microversion = '2.30' body = {'host': None} diff --git a/openstack/compute/v2/server_remote_console.py b/openstack/compute/v2/server_remote_console.py index a6de29634..a60c47678 100644 --- a/openstack/compute/v2/server_remote_console.py +++ b/openstack/compute/v2/server_remote_console.py @@ -12,6 +12,17 @@ from openstack import resource +from openstack import utils + +CONSOLE_TYPE_PROTOCOL_MAPPING = { + 'novnc': 'vnc', + 'xvpvnc': 'vnc', + 'spice-html5': 'spice', + 'rdp-html5': 'rdp', + 'serial': 'serial', + 'webmks': 'mks' +} + class ServerRemoteConsole(resource.Resource): resource_key = 'remote_console' @@ -34,3 +45,19 @@ class ServerRemoteConsole(resource.Resource): url = resource.Body('url') #: The ID for the server. server_id = resource.URI('server_id') + + def create(self, session, prepend_key=True, base_path=None, **params): + if not self.protocol: + self.protocol = \ + CONSOLE_TYPE_PROTOCOL_MAPPING.get(self.type) + if ( + not utils.supports_microversion(session, '2.8') + and self.type == 'webmks' + ): + raise ValueError('Console type webmks is not supported on ' + 'server side') + return super(ServerRemoteConsole, self).create( + session, + prepend_key=prepend_key, + base_path=base_path, + **params) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 3ccf1233b..086702619 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -23,6 +23,7 @@ from openstack.compute.v2 import server_group from openstack.compute.v2 import server_interface from openstack.compute.v2 import server_ip +from openstack.compute.v2 import server_remote_console from openstack.compute.v2 import service from openstack.tests.unit import test_proxy_base @@ -564,3 +565,53 @@ def test_remove_security_groups(self): self.proxy.remove_security_group_from_server, method_args=["value", {'id': 'id', 'name': 'sg'}], expected_args=['sg']) + + def test_create_server_remote_console(self): + self.verify_create( + self.proxy.create_server_remote_console, + server_remote_console.ServerRemoteConsole, + method_kwargs={"server": "test_id", "type": "fake"}, + expected_kwargs={"server_id": "test_id", "type": "fake"}) + + def test_get_console_url(self): + self._verify( + 'openstack.compute.v2.server.Server.get_console_url', + self.proxy.get_server_console_url, + method_args=["value", "console_type"], + expected_args=["console_type"]) + + def test_create_console(self): + with \ + mock.patch('openstack.utils.supports_microversion') as smv, \ + mock.patch('openstack.compute.v2._proxy.Proxy._create') as rcc, \ + mock.patch('openstack.compute.v2.server.Server.get_console_url') \ + as sgc: + console_fake = { + 'url': 'a', + 'type': 'b', + 'protocol': 'c' + } + smv.return_value = False + sgc.return_value = console_fake + ret = self.proxy.create_console('fake_server', 'fake_type') + smv.assert_called_once_with(self.proxy, '2.6') + rcc.assert_not_called() + sgc.assert_called_with(self.proxy, 'fake_type') + self.assertDictEqual(console_fake, ret) + + smv.reset_mock() + sgc.reset_mock() + rcc.reset_mock() + + # Test server_remote_console is triggered when mv>=2.6 + smv.return_value = True + rcc.return_value = server_remote_console.ServerRemoteConsole( + **console_fake) + ret = self.proxy.create_console('fake_server', 'fake_type') + smv.assert_called_once_with(self.proxy, '2.6') + sgc.assert_not_called() + rcc.assert_called_with(server_remote_console.ServerRemoteConsole, + server_id='fake_server', + type='fake_type', + protocol=None) + self.assertEqual(console_fake['url'], ret['url']) diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 4f484e394..74281ba06 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -803,6 +803,52 @@ def test_get_console_output(self): self.sess.post.assert_called_with( url, json=body, headers=headers, microversion=None) + def test_get_console_url(self): + sot = server.Server(**EXAMPLE) + + resp = mock.Mock() + resp.body = {'console': {'a': 'b'}} + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + self.sess.post.return_value = resp + + res = sot.get_console_url(self.sess, 'novnc') + self.sess.post.assert_called_with( + 'servers/IDENTIFIER/action', + json={'os-getVNCConsole': {'type': 'novnc'}}, + headers={'Accept': ''}, microversion=None) + self.assertDictEqual(resp.body['console'], res) + + sot.get_console_url(self.sess, 'xvpvnc') + self.sess.post.assert_called_with( + 'servers/IDENTIFIER/action', + json={'os-getVNCConsole': {'type': 'xvpvnc'}}, + headers={'Accept': ''}, microversion=None) + + sot.get_console_url(self.sess, 'spice-html5') + self.sess.post.assert_called_with( + 'servers/IDENTIFIER/action', + json={'os-getSPICEConsole': {'type': 'spice-html5'}}, + headers={'Accept': ''}, microversion=None) + + sot.get_console_url(self.sess, 'rdp-html5') + self.sess.post.assert_called_with( + 'servers/IDENTIFIER/action', + json={'os-getRDPConsole': {'type': 'rdp-html5'}}, + headers={'Accept': ''}, microversion=None) + + sot.get_console_url(self.sess, 'serial') + self.sess.post.assert_called_with( + 'servers/IDENTIFIER/action', + json={'os-getSerialConsole': {'type': 'serial'}}, + headers={'Accept': ''}, microversion=None) + + self.assertRaises(ValueError, + sot.get_console_url, + self.sess, + 'fake_type' + ) + def test_live_migrate_no_force(self): sot = server.Server(**EXAMPLE) diff --git a/openstack/tests/unit/compute/v2/test_server_remote_console.py b/openstack/tests/unit/compute/v2/test_server_remote_console.py new file mode 100644 index 000000000..dcbb5a5b1 --- /dev/null +++ b/openstack/tests/unit/compute/v2/test_server_remote_console.py @@ -0,0 +1,71 @@ +# 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 unittest import mock + +from keystoneauth1 import adapter + +from openstack.tests.unit import base + +from openstack.compute.v2 import server_remote_console + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'protocol': 'rdp', + 'type': 'rdp', + 'url': 'fake' +} + + +class TestServerRemoteConsole(base.TestCase): + + def setUp(self): + super(TestServerRemoteConsole, self).setUp() + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.default_microversion = '2.9' + self.resp = mock.Mock() + self.resp.body = None + self.resp.json = mock.Mock(return_value=self.resp.body) + self.resp.status_code = 200 + self.sess = mock.Mock() + self.sess.post = mock.Mock(return_value=self.resp) + self.sess._get_connection = mock.Mock(return_value=self.cloud) + + def test_basic(self): + sot = server_remote_console.ServerRemoteConsole() + self.assertEqual('remote_console', sot.resource_key) + self.assertEqual('/servers/%(server_id)s/remote-consoles', + sot.base_path) + self.assertTrue(sot.allow_create) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_delete) + self.assertFalse(sot.allow_list) + + def test_make_it(self): + sot = server_remote_console.ServerRemoteConsole(**EXAMPLE) + self.assertEqual(EXAMPLE['url'], sot.url) + + def test_create_type_mks_old(self): + sot = server_remote_console.ServerRemoteConsole( + server_id='fake_server', type='webmks') + + class FakeEndpointData: + min_microversion = '2' + max_microversion = '2.5' + self.sess.get_endpoint_data.return_value = FakeEndpointData() + + self.assertRaises( + ValueError, + sot.create, + self.sess + ) diff --git a/releasenotes/notes/optimize-server-console-1d27c107b9a1cdc3.yaml b/releasenotes/notes/optimize-server-console-1d27c107b9a1cdc3.yaml new file mode 100644 index 000000000..c943007bc --- /dev/null +++ b/releasenotes/notes/optimize-server-console-1d27c107b9a1cdc3.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Optimizes compute server console creation by addind older get_server_console method to the server and create_console proxy method calling appropriate method depending on the suported microversion. From 1c2a06350d615af6bef2615a94224ec49ac155b4 Mon Sep 17 00:00:00 2001 From: John Petrini Date: Tue, 1 Sep 2020 15:21:59 -0400 Subject: [PATCH 2714/3836] Don't set list_type to dict for server groups. When server_groups is present for retrieved server, this ends up with the following exception [1]. So we should rather have regular list instead of dict. [1] http://paste.openstack.org/show/797758/ Change-Id: I40c4c5241d02aa9d6e61a8064b48a91bde5e6e77 Story: 2007710 Task: 39843 --- openstack/compute/v2/server.py | 2 +- openstack/tests/unit/compute/v2/test_server.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 14e4ccfd9..a6b01fe99 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -170,7 +170,7 @@ class Server(resource.Resource, metadata.MetadataMixin, resource.TagMixin): type=list, list_type=dict) #: The UUIDs of the server groups to which the server belongs. #: Currently this can contain at most one entry. - server_groups = resource.Body('server_groups', type=list, list_type=dict) + server_groups = resource.Body('server_groups', type=list) #: The state this server is in. Valid values include ``ACTIVE``, #: ``BUILDING``, ``DELETED``, ``ERROR``, ``HARD_REBOOT``, ``PASSWORD``, #: ``PAUSED``, ``REBOOT``, ``REBUILD``, ``RESCUED``, ``RESIZED``, diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 4f484e394..ef26d305d 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -104,6 +104,7 @@ 'name': 'default' } ], + 'server_groups': ['3caf4187-8010-491f-b6f5-a4a68a40371e'], 'status': 'ACTIVE', 'tags': [], 'tenant_id': '6f70656e737461636b20342065766572', @@ -206,6 +207,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['progress'], sot.progress) self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertEqual(EXAMPLE['server_groups'], sot.server_groups) self.assertEqual(EXAMPLE['status'], sot.status) self.assertEqual(EXAMPLE['updated'], sot.updated_at) self.assertEqual(EXAMPLE['user_id'], sot.user_id) From 05f79d1d7290f9d1cd0483642ffb9d0a000d7ef7 Mon Sep 17 00:00:00 2001 From: Iury Gregory Melo Ferreira Date: Sun, 13 Sep 2020 01:00:27 +0200 Subject: [PATCH 2715/3836] Increase IRONIC_VM_SPECS_RAM to avoid KP In ironic jobs with tinyipa we should be using 512 not 384. Change-Id: Ifcca4482d4539056bc5112851024812fa92c45a4 --- .zuul.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index 771abc1f9..53f8a8566 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -251,7 +251,7 @@ IRONIC_RAMDISK_TYPE: tinyipa IRONIC_VM_COUNT: 6 IRONIC_VM_LOG_DIR: '{{ devstack_base_dir }}/ironic-bm-logs' - IRONIC_VM_SPECS_RAM: 384 + IRONIC_VM_SPECS_RAM: 512 devstack_plugins: ironic: https://opendev.org/openstack/ironic devstack_services: From 64ca078ef3be126bd5a225deb52939a7360913ed Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 22 Sep 2020 19:04:52 +0200 Subject: [PATCH 2716/3836] Follow-up on 751234 and 750072 Change-Id: I4a9acf9d08791411ccbd0f00dcb1bc053daea6ce --- openstack/compute/v2/_proxy.py | 17 ++-- openstack/compute/v2/flavor.py | 9 ++- openstack/compute/v2/server.py | 2 +- openstack/tests/unit/compute/v2/test_proxy.py | 77 ++++++++++--------- ...d-compute-flavor-ops-12149e58299c413e.yaml | 6 +- ...imize-server-console-1d27c107b9a1cdc3.yaml | 4 +- 6 files changed, 64 insertions(+), 51 deletions(-) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 9046add12..2aec33d1b 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -67,10 +67,12 @@ def find_flavor(self, name_or_id, ignore_missing=True, :param name_or_id: The name or ID of a flavor. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the resource does not exist. When set to ``True``, None will be + returned when attempting to find a nonexistent resource. + :param bool get_extra_specs: When set to ``True`` and extra_specs not + present in the response will invoke additional API call to fetch + extra_specs. :returns: One :class:`~openstack.compute.v2.flavor.Flavor` or None """ flavor = self._find(_flavor.Flavor, name_or_id, @@ -110,11 +112,14 @@ def get_flavor(self, flavor, get_extra_specs=False): """Get a single flavor :param flavor: The value can be the ID of a flavor or a - :class:`~openstack.compute.v2.flavor.Flavor` instance. + :class:`~openstack.compute.v2.flavor.Flavor` instance. + :param bool get_extra_specs: When set to ``True`` and extra_specs not + present in the response will invoke additional API call to fetch + extra_specs. :returns: One :class:`~openstack.compute.v2.flavor.Flavor` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ flavor = self._get(_flavor.Flavor, flavor) if get_extra_specs and not flavor.extra_specs: diff --git a/openstack/compute/v2/flavor.py b/openstack/compute/v2/flavor.py index 00c9ec54b..0b43a441a 100644 --- a/openstack/compute/v2/flavor.py +++ b/openstack/compute/v2/flavor.py @@ -104,9 +104,9 @@ def remove_tenant_access(self, session, tenant): self._action(session, body) def get_access(self, session): - """Lists tenants who have access to a private flavor and adds private - flavor access to and removes private flavor access from tenants. By - default, only administrators can manage private flavor access. A + """Lists tenants who have access to a private flavor + + By default, only administrators can manage private flavor access. A private flavor has is_public set to false while a public flavor has is_public set to true. @@ -119,8 +119,9 @@ def get_access(self, session): def fetch_extra_specs(self, session): """Fetch extra_specs of the flavor + Starting with 2.61 extra_specs are returned with the flavor details, - before that a separate call is required + before that a separate call is required. """ url = utils.urljoin(Flavor.base_path, self.id, 'os-extra_specs') microversion = self._get_microversion_for(session, 'fetch') diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index d5b77fb16..ef027b363 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -487,7 +487,7 @@ def live_migrate(self, session, host, force, block_migration, def get_console_url(self, session, console_type): action = CONSOLE_TYPE_ACTION_MAPPING.get(console_type) if not action: - raise ValueError("Unsupported console type") + raise ValueError("Unsupported console type %s" % console_type) body = {action: {'type': console_type}} resp = self._action(session, body) return resp.json().get('console') diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 33a821715..08c452818 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -719,38 +719,45 @@ def test_get_console_url(self): method_args=["value", "console_type"], expected_args=["console_type"]) - def test_create_console(self): - with \ - mock.patch('openstack.utils.supports_microversion') as smv, \ - mock.patch('openstack.compute.v2._proxy.Proxy._create') as rcc, \ - mock.patch('openstack.compute.v2.server.Server.get_console_url') \ - as sgc: - console_fake = { - 'url': 'a', - 'type': 'b', - 'protocol': 'c' - } - smv.return_value = False - sgc.return_value = console_fake - ret = self.proxy.create_console('fake_server', 'fake_type') - smv.assert_called_once_with(self.proxy, '2.6') - rcc.assert_not_called() - sgc.assert_called_with(self.proxy, 'fake_type') - self.assertDictEqual(console_fake, ret) - - smv.reset_mock() - sgc.reset_mock() - rcc.reset_mock() - - # Test server_remote_console is triggered when mv>=2.6 - smv.return_value = True - rcc.return_value = server_remote_console.ServerRemoteConsole( - **console_fake) - ret = self.proxy.create_console('fake_server', 'fake_type') - smv.assert_called_once_with(self.proxy, '2.6') - sgc.assert_not_called() - rcc.assert_called_with(server_remote_console.ServerRemoteConsole, - server_id='fake_server', - type='fake_type', - protocol=None) - self.assertEqual(console_fake['url'], ret['url']) + @mock.patch('openstack.utils.supports_microversion', autospec=True) + @mock.patch('openstack.compute.v2._proxy.Proxy._create', autospec=True) + @mock.patch('openstack.compute.v2.server.Server.get_console_url', + autospec=True) + def test_create_console_mv_old(self, sgc, rcc, smv): + console_fake = { + 'url': 'a', + 'type': 'b', + 'protocol': 'c' + } + smv.return_value = False + sgc.return_value = console_fake + ret = self.proxy.create_console('fake_server', 'fake_type') + smv.assert_called_once_with(self.proxy, '2.6') + rcc.assert_not_called() + sgc.assert_called_with(mock.ANY, self.proxy, 'fake_type') + self.assertDictEqual(console_fake, ret) + + @mock.patch('openstack.utils.supports_microversion', autospec=True) + @mock.patch('openstack.compute.v2._proxy.Proxy._create', autospec=True) + @mock.patch('openstack.compute.v2.server.Server.get_console_url', + autospec=True) + def test_create_console_mv_2_6(self, sgc, rcc, smv): + console_fake = { + 'url': 'a', + 'type': 'b', + 'protocol': 'c' + } + + # Test server_remote_console is triggered when mv>=2.6 + smv.return_value = True + rcc.return_value = server_remote_console.ServerRemoteConsole( + **console_fake) + ret = self.proxy.create_console('fake_server', 'fake_type') + smv.assert_called_once_with(self.proxy, '2.6') + sgc.assert_not_called() + rcc.assert_called_with(mock.ANY, + server_remote_console.ServerRemoteConsole, + server_id='fake_server', + type='fake_type', + protocol=None) + self.assertEqual(console_fake['url'], ret['url']) diff --git a/releasenotes/notes/add-compute-flavor-ops-12149e58299c413e.yaml b/releasenotes/notes/add-compute-flavor-ops-12149e58299c413e.yaml index fcb4e0345..45c2d6b9d 100644 --- a/releasenotes/notes/add-compute-flavor-ops-12149e58299c413e.yaml +++ b/releasenotes/notes/add-compute-flavor-ops-12149e58299c413e.yaml @@ -1,7 +1,5 @@ --- features: - | - Add additional compute flavor operations (flavor_add_tenant_access, flavor_remove_tenant_access, get_flavor_access, extra_specs fetching/updating). -other: - - | - Merge FlavorDetails into Flavor class. + Add additional compute flavor operations (flavor_add_tenant_access, + flavor_remove_tenant_access, get_flavor_access, extra_specs fetching/updating). diff --git a/releasenotes/notes/optimize-server-console-1d27c107b9a1cdc3.yaml b/releasenotes/notes/optimize-server-console-1d27c107b9a1cdc3.yaml index c943007bc..9cb44b4a8 100644 --- a/releasenotes/notes/optimize-server-console-1d27c107b9a1cdc3.yaml +++ b/releasenotes/notes/optimize-server-console-1d27c107b9a1cdc3.yaml @@ -1,4 +1,6 @@ --- features: - | - Optimizes compute server console creation by addind older get_server_console method to the server and create_console proxy method calling appropriate method depending on the suported microversion. + Optimizes compute server console creation by adding older + get_server_console method to the server and create_console proxy method + calling appropriate method depending on the supported microversion. From 7dfe1db7c32b940bef4282671c1c1d6f68d71a6a Mon Sep 17 00:00:00 2001 From: maaoyu Date: Wed, 23 Sep 2020 14:02:40 +0800 Subject: [PATCH 2717/3836] Remove install unnecessary packages The docs requirements migrated to doc/requirements.txt we need not install things from requirements.txt. Change-Id: Iadb42f085ecd973a401cc38f0666ba43e12e6f32 --- tox.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/tox.ini b/tox.ini index ef1491a33..5334aac5e 100644 --- a/tox.ini +++ b/tox.ini @@ -82,7 +82,6 @@ commands = {toxinidir}/extras/run-ansible-tests.sh -e {envdir} {posargs} [testenv:docs] deps = -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} - -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = sphinx-build -W -d doc/build/doctrees --keep-going -b html doc/source/ doc/build/html @@ -98,7 +97,6 @@ commands = [testenv:releasenotes] deps = -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} - -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = sphinx-build -a -E -W -d releasenotes/build/doctrees --keep-going -b html releasenotes/source releasenotes/build/html From 37d330805a7c30817df5ea7b271c496f479204bd Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Thu, 24 Sep 2020 14:38:29 +0000 Subject: [PATCH 2718/3836] Include "fields" to "Port" query parameters This new query parameter will allow to send a query with "fields" parameter, that contains the needed OVO fields that need to be retrieved from the BD. As commented in the related bug, the OSC "list" command only prints five parameters. Querying for only those ones will speed up the DB operation and the dictionary creation. Related-Bug: #1897100 Change-Id: I136a824896b4209c90ccc812a7e73064941e1b5a --- openstack/network/v2/port.py | 4 ++-- openstack/tests/unit/network/v2/test_port.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index e517607bc..04bf6b794 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -30,8 +30,8 @@ class Port(_base.NetworkResource, resource.TagMixin): _query_mapping = resource.QueryParameters( 'binding:host_id', 'binding:profile', 'binding:vif_details', 'binding:vif_type', 'binding:vnic_type', - 'description', 'device_id', 'device_owner', 'fixed_ips', 'id', - 'ip_address', 'mac_address', 'name', 'network_id', 'status', + 'description', 'device_id', 'device_owner', 'fields', 'fixed_ips', + 'id', 'ip_address', 'mac_address', 'name', 'network_id', 'status', 'subnet_id', is_admin_state_up='admin_state_up', is_port_security_enabled='port_security_enabled', diff --git a/openstack/tests/unit/network/v2/test_port.py b/openstack/tests/unit/network/v2/test_port.py index 3312d9af2..4c5b0bb70 100644 --- a/openstack/tests/unit/network/v2/test_port.py +++ b/openstack/tests/unit/network/v2/test_port.py @@ -85,6 +85,7 @@ def test_basic(self): "description": "description", "device_id": "device_id", "device_owner": "device_owner", + "fields": "fields", "fixed_ips": "fixed_ips", "id": "id", "ip_address": "ip_address", From 121b8ce89279735457422d308f4eded198b1077e Mon Sep 17 00:00:00 2001 From: Bo Tran Date: Fri, 25 Sep 2020 15:58:14 +0700 Subject: [PATCH 2719/3836] add cluster_id to filter by cluster_id when list actions Ref commit: https://review.opendev.org/#/c/669026/ Change-Id: I1576303154117dff77f1fbab61e1676712efeb7e --- openstack/clustering/v1/action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/clustering/v1/action.py b/openstack/clustering/v1/action.py index 280501ffd..e63921ec3 100644 --- a/openstack/clustering/v1/action.py +++ b/openstack/clustering/v1/action.py @@ -28,7 +28,7 @@ class Action(resource.Resource): _query_mapping = resource.QueryParameters( 'name', 'action', 'status', 'sort', 'global_project', - target_id='target') + 'cluster_id', target_id='target') # Properties #: Name of the action. From 778f0f9451bab23224f58e3264f4aa0ee8e47bda Mon Sep 17 00:00:00 2001 From: Hang Yang Date: Mon, 10 Aug 2020 16:48:17 -0500 Subject: [PATCH 2720/3836] Add neutron address group CRUD Add support for neutron address groups CRUD operations. Subsequent patches will be added to use address groups in security group rules. Change-Id: I9252b37d252a46c6708947142705006a56a390c7 Implements: blueprint address-groups-in-sg-rules Depends-On: https://review.opendev.org/738274 --- doc/source/user/resources/network/index.rst | 1 + .../resources/network/v2/address_group.rst | 12 ++ openstack/network/v2/_proxy.py | 113 ++++++++++++++++++ openstack/network/v2/address_group.py | 84 +++++++++++++ .../network/v2/test_address_group.py | 76 ++++++++++++ .../unit/network/v2/test_address_group.py | 55 +++++++++ 6 files changed, 341 insertions(+) create mode 100644 doc/source/user/resources/network/v2/address_group.rst create mode 100644 openstack/network/v2/address_group.py create mode 100644 openstack/tests/functional/network/v2/test_address_group.py create mode 100644 openstack/tests/unit/network/v2/test_address_group.py diff --git a/doc/source/user/resources/network/index.rst b/doc/source/user/resources/network/index.rst index 9011b0c90..3884de78e 100644 --- a/doc/source/user/resources/network/index.rst +++ b/doc/source/user/resources/network/index.rst @@ -4,6 +4,7 @@ Network Resources .. toctree:: :maxdepth: 1 + v2/address_group v2/address_scope v2/agent v2/auto_allocated_topology diff --git a/doc/source/user/resources/network/v2/address_group.rst b/doc/source/user/resources/network/v2/address_group.rst new file mode 100644 index 000000000..34360fc59 --- /dev/null +++ b/doc/source/user/resources/network/v2/address_group.rst @@ -0,0 +1,12 @@ +openstack.network.v2.address_group +================================== + +.. automodule:: openstack.network.v2.address_group + +The AddressGroup Class +---------------------- + +The ``AddressGroup`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.address_group.AddressGroup + :members: diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index dcca6fd73..aceafe9c7 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -11,6 +11,7 @@ # under the License. from openstack import exceptions +from openstack.network.v2 import address_group as _address_group from openstack.network.v2 import address_scope as _address_scope from openstack.network.v2 import agent as _agent from openstack.network.v2 import auto_allocated_topology as \ @@ -80,6 +81,118 @@ def _delete(self, resource_type, value, ignore_missing=True, return rv + def create_address_group(self, **attrs): + """Create a new address group from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.network.v2.address_group.AddressGroup`, + comprised of the properties on the AddressGroup class. + + :returns: The results of address group creation + :rtype: :class:`~openstack.network.v2.address_group.AddressGroup` + """ + return self._create(_address_group.AddressGroup, **attrs) + + def delete_address_group(self, address_group, ignore_missing=True): + """Delete an address group + + :param address_group: The value can be either the ID of an + address group or + a :class:`~openstack.network.v2.address_group.AddressGroup` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will + be raised when the address group does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent address group. + + :returns: ``None`` + """ + self._delete(_address_group.AddressGroup, address_group, + ignore_missing=ignore_missing) + + def find_address_group(self, name_or_id, ignore_missing=True, **args): + """Find a single address group + + :param name_or_id: The name or ID of an address group. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. + :returns: One :class:`~openstack.network.v2.address_group.AddressGroup` + or None + """ + return self._find(_address_group.AddressGroup, name_or_id, + ignore_missing=ignore_missing, **args) + + def get_address_group(self, address_group): + """Get a single address group + + :param address_group: The value can be the ID of an address group or a + :class:`~openstack.network.v2.address_group.AddressGroup` instance. + + :returns: One :class:`~openstack.network.v2.address_group.AddressGroup` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_address_group.AddressGroup, address_group) + + def address_groups(self, **query): + """Return a generator of address groups + + :param dict query: Optional query parameters to be sent to limit + the resources being returned. + + * ``name``: Address group name + * ``description``: Address group description + * ``project_id``: Owner project ID + + :returns: A generator of address group objects + :rtype: :class:`~openstack.network.v2.address_group.AddressGroup` + """ + return self._list(_address_group.AddressGroup, **query) + + def update_address_group(self, address_group, **attrs): + """Update an address group + + :param address_group: Either the ID of an address group or a + :class:`~openstack.network.v2.address_group.AddressGroup` instance. + :param dict attrs: The attributes to update on the address group + represented by ``value``. + + :returns: The updated address group + :rtype: :class:`~openstack.network.v2.address_group.AddressGroup` + """ + return self._update(_address_group.AddressGroup, address_group, + **attrs) + + def add_addresses_to_address_group(self, address_group, addresses): + """Add addresses to a address group + + :param address_group: Either the ID of an address group or a + :class:`~openstack.network.v2.address_group.AddressGroup` instance. + :param list addresses: List of address strings. + :returns: AddressGroup with updated addresses + :rtype: :class: `~openstack.network.v2.address_group.AddressGroup` + """ + ag = self._get_resource(_address_group.AddressGroup, address_group) + return ag.add_addresses(self, addresses) + + def remove_addresses_from_address_group(self, address_group, addresses): + """Remove addresses from a address group + + :param address_group: Either the ID of an address group or a + :class:`~openstack.network.v2.address_group.AddressGroup` instance. + :param list addresses: List of address strings. + :returns: AddressGroup with updated addresses + :rtype: :class: `~openstack.network.v2.address_group.AddressGroup` + """ + ag = self._get_resource(_address_group.AddressGroup, address_group) + return ag.remove_addresses(self, addresses) + def create_address_scope(self, **attrs): """Create a new address scope from attributes diff --git a/openstack/network/v2/address_group.py b/openstack/network/v2/address_group.py new file mode 100644 index 000000000..08a3dcc64 --- /dev/null +++ b/openstack/network/v2/address_group.py @@ -0,0 +1,84 @@ +# 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 import resource +from openstack import utils + + +class AddressGroup(resource.Resource): + """Address group extension.""" + resource_key = 'address_group' + resources_key = 'address_groups' + base_path = '/address-groups' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + _query_mapping = resource.QueryParameters( + "sort_key", "sort_dir", + 'name', 'description', + project_id='tenant_id' + ) + + # Properties + #: The ID of the address group. + id = resource.Body('id') + #: The address group name. + name = resource.Body('name') + #: The address group name. + description = resource.Body('description') + #: The ID of the project that owns the address group. + project_id = resource.Body('tenant_id') + #: The IP addresses of the address group. + addresses = resource.Body('addresses', type=list) + + def _put(self, session, url, body): + resp = session.put(url, json=body) + exceptions.raise_from_response(resp) + return resp + + def add_addresses(self, session, addresses): + """Add addresses into the address group. + + :param session: The session to communicate through. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param list addresses: The list of address strings. + + :returns: The response as a AddressGroup object with updated addresses + + :raises: :class:`~openstack.exceptions.SDKException` on error. + """ + url = utils.urljoin(self.base_path, self.id, 'add_addresses') + resp = self._put(session, url, {'addresses': addresses}) + self._translate_response(resp) + return self + + def remove_addresses(self, session, addresses): + """Remove addresses from the address group. + + :param session: The session to communicate through. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param list addresses: The list of address strings. + + :returns: The response as a AddressGroup object with updated addresses + + :raises: :class:`~openstack.exceptions.SDKException` on error. + """ + url = utils.urljoin(self.base_path, self.id, 'remove_addresses') + resp = self._put(session, url, {'addresses': addresses}) + self._translate_response(resp) + return self diff --git a/openstack/tests/functional/network/v2/test_address_group.py b/openstack/tests/functional/network/v2/test_address_group.py new file mode 100644 index 000000000..cb9b8c95f --- /dev/null +++ b/openstack/tests/functional/network/v2/test_address_group.py @@ -0,0 +1,76 @@ +# 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.network.v2 import address_group as _address_group +from openstack.tests.functional import base + + +class TestAddressGroup(base.BaseFunctionalTest): + + ADDRESS_GROUP_ID = None + ADDRESSES = ['10.0.0.1/32', '2001:db8::/32'] + + def setUp(self): + super(TestAddressGroup, self).setUp() + self.ADDRESS_GROUP_NAME = self.getUniqueString() + self.ADDRESS_GROUP_DESCRIPTION = self.getUniqueString() + self.ADDRESS_GROUP_NAME_UPDATED = self.getUniqueString() + self.ADDRESS_GROUP_DESCRIPTION_UPDATED = self.getUniqueString() + address_group = self.conn.network.create_address_group( + name=self.ADDRESS_GROUP_NAME, + description=self.ADDRESS_GROUP_DESCRIPTION, + addresses=self.ADDRESSES + ) + assert isinstance(address_group, _address_group.AddressGroup) + self.assertEqual(self.ADDRESS_GROUP_NAME, address_group.name) + self.assertEqual(self.ADDRESS_GROUP_DESCRIPTION, + address_group.description) + self.assertItemsEqual(self.ADDRESSES, address_group.addresses) + self.ADDRESS_GROUP_ID = address_group.id + + def tearDown(self): + sot = self.conn.network.delete_address_group(self.ADDRESS_GROUP_ID) + self.assertIsNone(sot) + super(TestAddressGroup, self).tearDown() + + def test_find(self): + sot = self.conn.network.find_address_group(self.ADDRESS_GROUP_NAME) + self.assertEqual(self.ADDRESS_GROUP_ID, sot.id) + + def test_get(self): + sot = self.conn.network.get_address_group(self.ADDRESS_GROUP_ID) + self.assertEqual(self.ADDRESS_GROUP_NAME, sot.name) + + def test_list(self): + names = [ag.name for ag in self.conn.network.address_groups()] + self.assertIn(self.ADDRESS_GROUP_NAME, names) + + def test_update(self): + sot = self.conn.network.update_address_group( + self.ADDRESS_GROUP_ID, + name=self.ADDRESS_GROUP_NAME_UPDATED, + description=self.ADDRESS_GROUP_DESCRIPTION_UPDATED) + self.assertEqual(self.ADDRESS_GROUP_NAME_UPDATED, sot.name) + self.assertEqual(self.ADDRESS_GROUP_DESCRIPTION_UPDATED, + sot.description) + + def test_add_remove_addresses(self): + addrs = ['127.0.0.1/32', 'fe80::/10'] + sot = self.conn.network.add_addresses_to_address_group( + self.ADDRESS_GROUP_ID, addrs) + updated_addrs = self.ADDRESSES.copy() + updated_addrs.extend(addrs) + self.assertItemsEqual(updated_addrs, sot.addresses) + sot = self.conn.network.remove_addresses_from_address_group( + self.ADDRESS_GROUP_ID, addrs) + self.assertItemsEqual(self.ADDRESSES, sot.addresses) diff --git a/openstack/tests/unit/network/v2/test_address_group.py b/openstack/tests/unit/network/v2/test_address_group.py new file mode 100644 index 000000000..0fe6a7cdf --- /dev/null +++ b/openstack/tests/unit/network/v2/test_address_group.py @@ -0,0 +1,55 @@ +# 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.tests.unit import base + +from openstack.network.v2 import address_group + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'id': IDENTIFIER, + 'name': '1', + 'description': '2', + 'tenant_id': '3', + 'addresses': ['10.0.0.1/32'] +} + + +class TestAddressGroup(base.TestCase): + + def test_basic(self): + sot = address_group.AddressGroup() + self.assertEqual('address_group', sot.resource_key) + self.assertEqual('address_groups', sot.resources_key) + self.assertEqual('/address-groups', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + self.assertDictEqual({"name": "name", + "description": "description", + "project_id": "tenant_id", + "sort_key": "sort_key", + "sort_dir": "sort_dir", + "limit": "limit", + "marker": "marker"}, + sot._query_mapping._mapping) + + def test_make_it(self): + sot = address_group.AddressGroup(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertItemsEqual(EXAMPLE['addresses'], sot.addresses) From b429193fbc38bf18c3e5799c948a5bcb83a2f3b0 Mon Sep 17 00:00:00 2001 From: zhangbailin Date: Mon, 28 Sep 2020 17:59:09 +0800 Subject: [PATCH 2721/3836] [Trival]: Add comments in the cloud/accelerator Change-Id: I7cf951bbb50c80ce2839e4ab5cf194f8c63d25f3 --- openstack/cloud/_accelerator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openstack/cloud/_accelerator.py b/openstack/cloud/_accelerator.py index b28ac1f6b..8c2c96c96 100644 --- a/openstack/cloud/_accelerator.py +++ b/openstack/cloud/_accelerator.py @@ -63,6 +63,7 @@ def create_device_profile(self, attrs): def delete_device_profile(self, name_or_id, filters): """Delete a device_profile. :param name_or_id: The Name(or uuid) of device_profile to be deleted. + :param filters: dict of filter conditions to push down :returns: True if delete succeeded, False otherwise. """ @@ -95,6 +96,7 @@ def list_accelerator_requests(self, filters=None): def delete_accelerator_request(self, name_or_id, filters): """Delete a accelerator_request. :param name_or_id: The Name(or uuid) of accelerator_request. + :param filters: dict of filter conditions to push down :returns: True if delete succeeded, False otherwise. """ From 767660b0a0bc87c21d1a66deb0c1b32923e759cd Mon Sep 17 00:00:00 2001 From: "wu.shiming" Date: Wed, 30 Sep 2020 11:20:08 +0800 Subject: [PATCH 2722/3836] Fix hacking min version to 3.0.1 flake8 new release 3.8.0 added new checks and gate pep8 job start failing. hacking 3.0.1 fix the pinning of flake8 to avoid bringing in a new version with new checks. Though it is fixed in latest hacking but 2.0 and 3.0 has cap for flake8 as <4.0.0 which mean flake8 new version 3.9.0 can also break the pep8 job if new check are added. To avoid similar gate break in future, we need to bump the hacking min version. - http://lists.openstack.org/pipermail/openstack-discuss/2020-May/014828.html Change-Id: I4bf93e866b35bd5c7f584539760ff159a94c532b --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 69e9e440c..ac8dbedee 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,7 @@ # 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>=3.0,<3.1.0 # Apache-2.0 +hacking>=3.0.1,<3.1.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 ddt>=1.0.1 # MIT From fa62f7d67f1f3b8567895206dcdbfd88f5db4287 Mon Sep 17 00:00:00 2001 From: Valery Tschopp Date: Thu, 1 Oct 2020 10:21:52 +0200 Subject: [PATCH 2723/3836] Add 'project_id' to Snapshot query parameters This allow to list volume snapshots filtered by project (as admin). Change-Id: I9372dd139868f726c749b4616acb7d0cdac6643c Task: 41002 Story: 2008214 --- openstack/block_storage/v3/_proxy.py | 1 + openstack/block_storage/v3/snapshot.py | 3 ++- openstack/tests/unit/block_storage/v3/test_snapshot.py | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 247bf3b2e..d0f07f063 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -62,6 +62,7 @@ def snapshots(self, details=True, **query): * name: Name of the snapshot as a string. * all_projects: Whether return the snapshots in all projects. + * project_id: Filter the snapshots by project. * volume_id: volume id of a snapshot. * status: Value of the status of the snapshot so that you can filter on "available" for example. diff --git a/openstack/block_storage/v3/snapshot.py b/openstack/block_storage/v3/snapshot.py index 04b8e7181..86e8383a9 100644 --- a/openstack/block_storage/v3/snapshot.py +++ b/openstack/block_storage/v3/snapshot.py @@ -20,7 +20,8 @@ class Snapshot(resource.Resource): base_path = "/snapshots" _query_mapping = resource.QueryParameters( - 'name', 'status', 'volume_id', all_projects='all_tenants') + 'name', 'status', 'volume_id', + 'project_id', all_projects='all_tenants') # capabilities allow_fetch = True diff --git a/openstack/tests/unit/block_storage/v3/test_snapshot.py b/openstack/tests/unit/block_storage/v3/test_snapshot.py index ae980542e..9aa3665eb 100644 --- a/openstack/tests/unit/block_storage/v3/test_snapshot.py +++ b/openstack/tests/unit/block_storage/v3/test_snapshot.py @@ -48,6 +48,7 @@ def test_basic(self): self.assertDictEqual({"name": "name", "status": "status", "all_projects": "all_tenants", + "project_id": "project_id", "volume_id": "volume_id", "limit": "limit", "marker": "marker"}, From 260a5280a0e11b25507a7675588213bc387b7de7 Mon Sep 17 00:00:00 2001 From: Hang Yang Date: Fri, 2 Oct 2020 09:52:31 -0500 Subject: [PATCH 2724/3836] Skip address group functional tests if no extension Address group extension is added to the latest neutron version, so skip its functional tests for older releases. Change-Id: I8723b0902ddd541d28ae36c1146c182e9332b24d Related-change: https://review.opendev.org/745594 Related-change: https://review.opendev.org/741784 Implements: blueprint address-groups-in-sg-rules --- openstack/tests/functional/network/v2/test_address_group.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openstack/tests/functional/network/v2/test_address_group.py b/openstack/tests/functional/network/v2/test_address_group.py index cb9b8c95f..d758e3524 100644 --- a/openstack/tests/functional/network/v2/test_address_group.py +++ b/openstack/tests/functional/network/v2/test_address_group.py @@ -22,6 +22,11 @@ class TestAddressGroup(base.BaseFunctionalTest): def setUp(self): super(TestAddressGroup, self).setUp() + + # Skip the tests if address group extension is not enabled. + if not self.conn.network.find_extension('address-group'): + self.skipTest('Network Address Group extension disabled') + self.ADDRESS_GROUP_NAME = self.getUniqueString() self.ADDRESS_GROUP_DESCRIPTION = self.getUniqueString() self.ADDRESS_GROUP_NAME_UPDATED = self.getUniqueString() From d4022f62a0fdd7da6e56165e498add6a76ead2ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Weing=C3=A4rtner?= Date: Mon, 17 Aug 2020 14:37:58 -0300 Subject: [PATCH 2725/3836] Deprecate 'remote_ip_prefix' parameter in metering label rules As proposed in the RFE and then approved in the spec. The parameter 'remote_ip_prefix' in metering label rules has been deprecated. Its name expresses the opposite of what does when used, and the lack of documentation confuses people. Moreover, an alternative method has been proposed and approved to enable operators to create metering rules using both source and destination IP addresses. The parameter will be removed in future releases. Partially-Implements: https://bugs.launchpad.net/neutron/+bug/1889431 RFE: https://bugs.launchpad.net/neutron/+bug/1889431 Depends-On: https://review.opendev.org/#/c/746203/ Depends-On: https://review.opendev.org/#/c/744702/ Depends-On: https://review.opendev.org/#/c/743828/ Depends-On: https://review.opendev.org/#/c/746142/ Depends-On: https://review.opendev.org/#/c/746347/ Change-Id: Iba2b0d09fdd631f8bd2c3c951fd69b243deed652 --- openstack/network/v2/metering_label_rule.py | 9 +++- openstack/resource.py | 54 +++++++++++++++++-- ...metering-label-rules-843d5a962e4e428c.yaml | 7 +++ 3 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/deprecate-remote_ip_prefix-metering-label-rules-843d5a962e4e428c.yaml diff --git a/openstack/network/v2/metering_label_rule.py b/openstack/network/v2/metering_label_rule.py index ed643ed17..acc81001d 100644 --- a/openstack/network/v2/metering_label_rule.py +++ b/openstack/network/v2/metering_label_rule.py @@ -44,4 +44,11 @@ class MeteringLabelRule(resource.Resource): #: The ID of the project this metering label rule is associated with. project_id = resource.Body('tenant_id') #: The remote IP prefix to be associated with this metering label rule. - remote_ip_prefix = resource.Body('remote_ip_prefix') + remote_ip_prefix = resource.Body( + 'remote_ip_prefix', deprecated=True, + deprecation_reason="The use of 'remote_ip_prefix' in metering label " + "rules is deprecated and will be removed in future " + "releases. One should use instead, the " + "'source_ip_prefix' and/or 'destination_ip_prefix' " + "parameters. For more details, you can check the " + "spec: https://review.opendev.org/#/c/744702/.") diff --git a/openstack/resource.py b/openstack/resource.py index 53bdbb75f..c929f0df1 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -50,6 +50,8 @@ class that represent a remote resource. The attributes that _SEEN_FORMAT = '{name}_seen' +LOG = _log.setup_logging(__name__) + def _convert_type(value, data_type, list_type=None): # This should allow handling list of dicts that have their own @@ -89,8 +91,18 @@ class _BaseComponent: # The class to be used for mappings _map_cls = dict + #: Marks the property as deprecated. + deprecated = False + #: Deprecation reason message used to warn users when deprecated == True + deprecation_reason = None + + #: Control field used to manage the deprecation warning. We want to warn + # only once when the attribute is retrieved in the code. + already_warned_deprecation = False + def __init__(self, name, type=None, default=None, alias=None, aka=None, alternate_id=False, list_type=None, coerce_to_default=False, + deprecated=False, deprecation_reason=None, **kwargs): """A typed descriptor for a component that makes up a Resource @@ -115,6 +127,11 @@ def __init__(self, name, type=None, default=None, alias=None, aka=None, If the Component is None or not present, force the given default to be used. If a default is not given but a type is given, construct an empty version of the type in question. + :param deprecated: + Indicates if the option is deprecated. If it is, we display a + warning message to the user. + :param deprecation_reason: + Custom deprecation message. """ self.name = name self.type = type @@ -128,6 +145,9 @@ def __init__(self, name, type=None, default=None, alias=None, aka=None, self.list_type = list_type self.coerce_to_default = coerce_to_default + self.deprecated = deprecated + self.deprecation_reason = deprecation_reason + def __get__(self, instance, owner): if instance is None: return self @@ -137,6 +157,7 @@ def __get__(self, instance, owner): try: value = attributes[self.name] except KeyError: + value = self.default if self.alias: # Resource attributes can be aliased to each other. If neither # of them exist, then simply doing a @@ -156,15 +177,29 @@ def __get__(self, instance, owner): setattr(instance, seen_flag, True) value = getattr(instance, self.alias) delattr(instance, seen_flag) - return value - return self.default + self.warn_if_deprecated_property(value) + return value # self.type() should not be called on None objects. if value is None: return None + self.warn_if_deprecated_property(value) return _convert_type(value, self.type, self.list_type) + def warn_if_deprecated_property(self, value): + deprecated = object.__getattribute__(self, 'deprecated') + deprecate_reason = object.__getattribute__(self, 'deprecation_reason') + + if value and deprecated and not self.already_warned_deprecation: + self.already_warned_deprecation = True + if not deprecate_reason: + LOG.warning("The option [%s] has been deprecated. " + "Please avoid using it.", self.name) + else: + LOG.warning(deprecate_reason) + return value + def __set__(self, instance, value): if self.coerce_to_default and value is None: value = self.default @@ -562,6 +597,14 @@ def __eq__(self, comparand): self._computed.attributes == comparand._computed.attributes ]) + def warning_if_attribute_deprecated(self, attr, value): + if value and self.deprecated: + if not self.deprecation_reason: + LOG.warning("The option [%s] has been deprecated. " + "Please avoid using it.", attr) + else: + LOG.warning(self.deprecation_reason) + def __getattribute__(self, name): """Return an attribute on this instance @@ -575,7 +618,9 @@ def __getattribute__(self, name): else: try: return self._body[self._alternate_id()] - except KeyError: + except KeyError as e: + LOG.debug("Attribute [%s] not found in [%s]: %s.", + self._alternate_id(), self._body, e) return None else: try: @@ -2014,7 +2059,6 @@ def wait_for_status(session, resource, status, failures, interval=None, :raises: :class:`~AttributeError` if the resource does not have a status attribute """ - log = _log.setup_logging(__name__) current_status = getattr(resource, attribute) if _normalize_status(current_status) == status.lower(): @@ -2048,7 +2092,7 @@ def wait_for_status(session, resource, status, failures, interval=None, "{name} transitioned to failure state {status}".format( name=name, status=new_status)) - log.debug('Still waiting for resource %s to reach state %s, ' + LOG.debug('Still waiting for resource %s to reach state %s, ' 'current state is %s', name, status, new_status) diff --git a/releasenotes/notes/deprecate-remote_ip_prefix-metering-label-rules-843d5a962e4e428c.yaml b/releasenotes/notes/deprecate-remote_ip_prefix-metering-label-rules-843d5a962e4e428c.yaml new file mode 100644 index 000000000..351fb2200 --- /dev/null +++ b/releasenotes/notes/deprecate-remote_ip_prefix-metering-label-rules-843d5a962e4e428c.yaml @@ -0,0 +1,7 @@ +--- +deprecations: + - | + Deprecate the use of 'remote_ip_prefix' in metering label rules, and it + will be removed in future releases. One should use instead the + 'source_ip_prefix' and/or 'destination_ip_prefix' parameters. For more + details, you can check the spec: https://review.opendev.org/#/c/744702/. From 878a8d944d125a650c3e1b45d7e4ebfaa2aa04e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Weing=C3=A4rtner?= Date: Mon, 17 Aug 2020 15:20:51 -0300 Subject: [PATCH 2726/3836] Add source_ip_prefix and destination_ip_prefix to metering label rules As proposed in the RFE and then approved in the spec, we are adding to the neutron metering rules two new parameters. The source IP prefix, and destination IP prefix. Partially-Implements: https://bugs.launchpad.net/neutron/+bug/1889431 RFE: https://bugs.launchpad.net/neutron/+bug/1889431 Depends-On: https://review.opendev.org/#/c/746203/ Depends-On: https://review.opendev.org/#/c/744702/ Depends-On: https://review.opendev.org/#/c/743828/ Depends-On: https://review.opendev.org/#/c/746142/ Depends-On: https://review.opendev.org/#/c/746347/ Change-Id: Ib288e276fbe5337e2dfc92a8f0f11dfcb425322b --- openstack/network/v2/metering_label_rule.py | 7 ++++++- .../network/v2/test_metering_label_rule.py | 20 +++++++++++++++++++ ...metering-label-rules-e04b797adac5d0d0.yaml | 5 +++++ 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/introduce-source-and-destination-ip-prefixes-into-metering-label-rules-e04b797adac5d0d0.yaml diff --git a/openstack/network/v2/metering_label_rule.py b/openstack/network/v2/metering_label_rule.py index acc81001d..3016709d9 100644 --- a/openstack/network/v2/metering_label_rule.py +++ b/openstack/network/v2/metering_label_rule.py @@ -27,7 +27,7 @@ class MeteringLabelRule(resource.Resource): _query_mapping = resource.QueryParameters( 'direction', 'metering_label_id', 'remote_ip_prefix', - project_id='tenant_id', + 'source_ip_prefix', 'destination_ip_prefix', project_id='tenant_id', ) # Properties @@ -52,3 +52,8 @@ class MeteringLabelRule(resource.Resource): "'source_ip_prefix' and/or 'destination_ip_prefix' " "parameters. For more details, you can check the " "spec: https://review.opendev.org/#/c/744702/.") + + #: The source IP prefix to be associated with this metering label rule. + source_ip_prefix = resource.Body('source_ip_prefix') + #: The destination IP prefix to be associated with this metering label rule + destination_ip_prefix = resource.Body('destination_ip_prefix') diff --git a/openstack/tests/unit/network/v2/test_metering_label_rule.py b/openstack/tests/unit/network/v2/test_metering_label_rule.py index ce3057e26..c11f2838b 100644 --- a/openstack/tests/unit/network/v2/test_metering_label_rule.py +++ b/openstack/tests/unit/network/v2/test_metering_label_rule.py @@ -46,3 +46,23 @@ def test_make_it(self): self.assertEqual(EXAMPLE['metering_label_id'], sot.metering_label_id) self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) self.assertEqual(EXAMPLE['remote_ip_prefix'], sot.remote_ip_prefix) + + def test_make_it_source_and_destination(self): + custom_example = EXAMPLE.copy() + custom_example["source_ip_prefix"] = "192.168.0.11/32" + custom_example["destination_ip_prefix"] = "0.0.0.0/0" + + sot = metering_label_rule.MeteringLabelRule(**custom_example) + self.assertEqual(custom_example['direction'], sot.direction) + self.assertFalse(sot.is_excluded) + self.assertEqual(custom_example['id'], sot.id) + self.assertEqual( + custom_example['metering_label_id'], sot.metering_label_id) + self.assertEqual(custom_example['tenant_id'], sot.project_id) + self.assertEqual( + custom_example['remote_ip_prefix'], sot.remote_ip_prefix) + + self.assertEqual( + custom_example['source_ip_prefix'], sot.source_ip_prefix) + self.assertEqual( + custom_example['destination_ip_prefix'], sot.destination_ip_prefix) diff --git a/releasenotes/notes/introduce-source-and-destination-ip-prefixes-into-metering-label-rules-e04b797adac5d0d0.yaml b/releasenotes/notes/introduce-source-and-destination-ip-prefixes-into-metering-label-rules-e04b797adac5d0d0.yaml new file mode 100644 index 000000000..5de538b14 --- /dev/null +++ b/releasenotes/notes/introduce-source-and-destination-ip-prefixes-into-metering-label-rules-e04b797adac5d0d0.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``source_ip_prefix`` and ``destination_ip_prefix`` to Neutron metering + label rules. \ No newline at end of file From c6570d704947e56c391bd218edf0b2a74aa67233 Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Thu, 8 Oct 2020 09:28:19 +0200 Subject: [PATCH 2727/3836] Add "description" to cinder volume types. As per: https://docs.openstack.org/api-ref/block-storage/v3/index.html?expanded=show-volume-type-detail-detail#show-volume-type-detail Volume Types also have a 'description' attribute Change-Id: I0f822aa6291c42161e01bc989363daaade5a9f53 --- openstack/block_storage/v3/type.py | 2 ++ openstack/tests/unit/block_storage/v3/test_type.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/openstack/block_storage/v3/type.py b/openstack/block_storage/v3/type.py index 7e3c81aae..c26f25dc0 100644 --- a/openstack/block_storage/v3/type.py +++ b/openstack/block_storage/v3/type.py @@ -31,6 +31,8 @@ class Type(resource.Resource): id = resource.Body("id") #: Name of the type. name = resource.Body("name") + #: Description of the type. + description = resource.Body("description") #: A dict of extra specifications. "capabilities" is a usual key. extra_specs = resource.Body("extra_specs", type=dict) #: a private volume-type. *Type: bool* diff --git a/openstack/tests/unit/block_storage/v3/test_type.py b/openstack/tests/unit/block_storage/v3/test_type.py index 5bdcff167..547effce9 100644 --- a/openstack/tests/unit/block_storage/v3/test_type.py +++ b/openstack/tests/unit/block_storage/v3/test_type.py @@ -20,7 +20,8 @@ "capabilities": "gpu" }, "id": FAKE_ID, - "name": "SSD" + "name": "SSD", + "description": "Test type", } @@ -46,3 +47,4 @@ def test_create(self): self.assertEqual(TYPE["id"], sot.id) self.assertEqual(TYPE["extra_specs"], sot.extra_specs) self.assertEqual(TYPE["name"], sot.name) + self.assertEqual(TYPE["description"], sot.description) From ac4bdb4bca02104d82553fc9220ec9ee311c7d8b Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Thu, 8 Oct 2020 09:34:56 +0200 Subject: [PATCH 2728/3836] Add support for updating cinder (v3) volume types https://docs.openstack.org/api-ref/block-storage/v3/index.html?expanded=update-a-volume-type-detail#update-a-volume-type v3 volume types permit updates. Change-Id: I777687e8e3e310ad0f845799b595c12fbbf250c5 --- openstack/block_storage/v3/_proxy.py | 13 +++++++++++++ openstack/block_storage/v3/type.py | 1 + openstack/tests/unit/block_storage/v3/test_proxy.py | 3 +++ openstack/tests/unit/block_storage/v3/test_type.py | 2 +- 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index d0f07f063..6314329a7 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -162,6 +162,19 @@ def delete_type(self, type, ignore_missing=True): """ self._delete(_type.Type, type, ignore_missing=ignore_missing) + def update_type(self, type, **attrs): + """Update a type + + :param type: The value can be either the ID of a type or a + :class:`~openstack.volume.v3.type.Type` instance. + :param dict attrs: The attributes to update on the type + represented by ``value``. + + :returns: The updated type + :rtype: :class:`~openstack.volume.v3.type.Type` + """ + return self._update(_type.Type, type, **attrs) + def get_volume(self, volume): """Get a single volume diff --git a/openstack/block_storage/v3/type.py b/openstack/block_storage/v3/type.py index 7e3c81aae..f68c9eafa 100644 --- a/openstack/block_storage/v3/type.py +++ b/openstack/block_storage/v3/type.py @@ -23,6 +23,7 @@ class Type(resource.Resource): allow_create = True allow_delete = True allow_list = True + allow_commit = True _query_mapping = resource.QueryParameters("is_public") diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 43b6c9107..c3e9cbc53 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -72,6 +72,9 @@ def test_type_delete(self): def test_type_delete_ignore(self): self.verify_delete(self.proxy.delete_type, type.Type, True) + def test_type_update(self): + self.verify_update(self.proxy.update_type, type.Type) + def test_volume_get(self): self.verify_get(self.proxy.get_volume, volume.Volume) diff --git a/openstack/tests/unit/block_storage/v3/test_type.py b/openstack/tests/unit/block_storage/v3/test_type.py index 5bdcff167..01320a3fa 100644 --- a/openstack/tests/unit/block_storage/v3/test_type.py +++ b/openstack/tests/unit/block_storage/v3/test_type.py @@ -35,7 +35,7 @@ def test_basic(self): self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertFalse(sot.allow_commit) + self.assertTrue(sot.allow_commit) def test_new(self): sot = type.Type.new(id=FAKE_ID) From 00aea3bc08d1e7d3988b8d05fbed28d7ab08ed9e Mon Sep 17 00:00:00 2001 From: Pavlo Shchelokovskyy Date: Fri, 9 Oct 2020 15:00:48 +0300 Subject: [PATCH 2729/3836] Expand user path when loading SSL-related files with this patch the cacert, cert and key items in the clouds.yaml can have shortened form like "~/.ssl/api.pem" Change-Id: I3bd12fe0c47a5f373bb231babd260b801a6c411a --- openstack/config/cloud_region.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 8f03d5386..cf5fbaa46 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -13,6 +13,7 @@ # under the License. import copy +import os.path import warnings import urllib @@ -338,7 +339,7 @@ def get_requests_verify_args(self): if insecure: verify = False if verify and cacert: - verify = cacert + verify = os.path.expanduser(cacert) else: if cacert: warnings.warn( @@ -349,8 +350,9 @@ def get_requests_verify_args(self): cert = self.config.get('cert') if cert: + cert = os.path.expanduser(cert) if self.config.get('key'): - cert = (cert, self.config.get('key')) + cert = (cert, os.path.expanduser(self.config.get('key'))) return (verify, cert) def get_services(self): From e8f0943bc0fcc3102c6e8e0f1606acff3de11976 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 9 Oct 2020 17:11:01 +0200 Subject: [PATCH 2730/3836] Respect default microversion in the microversion negotiation When the client explicitly chooses the desired microversion (openstack --os-compute-api-version=2.5 console url show test-server) we need to respect this choice. If some feature require particular microversion - ensure it is <= default mv and supported by the server, otherwise do nothing. Change-Id: Ib94f5c9212d00945f378f035563f78fd61d21fa3 --- .../tests/unit/baremetal/v1/test_node.py | 2 + .../tests/unit/compute/v2/test_server.py | 6 ++ openstack/tests/unit/test_utils.py | 61 +++++++++++++++++++ openstack/utils.py | 54 +++++++++++++--- .../fix-microversion-354dc70deb2b2f0b.yaml | 8 +++ 5 files changed, 124 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/fix-microversion-354dc70deb2b2f0b.yaml diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index dbc874f4c..b26bf50e2 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -18,6 +18,7 @@ from openstack.baremetal.v1 import node from openstack import exceptions from openstack import resource +from openstack import utils from openstack.tests.unit import base # NOTE: Sample data from api-ref doc @@ -731,6 +732,7 @@ def test_node_set_boot_device(self): retriable_status_codes=_common.RETRIABLE_STATUS_CODES) +@mock.patch.object(utils, 'pick_microversion', lambda session, v: v) @mock.patch.object(node.Node, 'fetch', lambda self, session: self) @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) class TestNodeTraits(base.TestCase): diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 2e99d4cef..2491b7363 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -447,6 +447,7 @@ def test_create_image_microver(self): min_microversion='2.1', max_microversion='2.56') self.sess.get_endpoint_data.return_value = self.endpoint_data + self.sess.default_microversion = None image_id = sot.create_image(self.sess, name, metadata) @@ -474,6 +475,7 @@ def test_create_image_minimal(self): min_microversion='2.1', max_microversion='2.56') self.sess.get_endpoint_data.return_value = self.endpoint_data + self.sess.default_microversion = None self.assertIsNone(self.resp.body, sot.create_image(self.sess, name)) @@ -900,6 +902,7 @@ class FakeEndpointData: min_microversion = '2.1' max_microversion = '2.25' self.sess.get_endpoint_data.return_value = FakeEndpointData() + self.sess.default_microversion = None res = sot.live_migrate( self.sess, host='HOST2', force=True, block_migration=False) @@ -924,6 +927,7 @@ class FakeEndpointData: min_microversion = '2.1' max_microversion = '2.25' self.sess.get_endpoint_data.return_value = FakeEndpointData() + self.sess.default_microversion = None res = sot.live_migrate( self.sess, host='HOST2', force=True, block_migration=None) @@ -948,6 +952,7 @@ class FakeEndpointData: min_microversion = '2.1' max_microversion = '2.30' self.sess.get_endpoint_data.return_value = FakeEndpointData() + self.sess.default_microversion = None res = sot.live_migrate( self.sess, host='HOST2', force=False, block_migration=False) @@ -972,6 +977,7 @@ class FakeEndpointData: min_microversion = '2.1' max_microversion = '2.30' self.sess.get_endpoint_data.return_value = FakeEndpointData() + self.sess.default_microversion = None res = sot.live_migrate( self.sess, host='HOST2', force=True, block_migration=None) diff --git a/openstack/tests/unit/test_utils.py b/openstack/tests/unit/test_utils.py index 6f330a75b..8aaf15531 100644 --- a/openstack/tests/unit/test_utils.py +++ b/openstack/tests/unit/test_utils.py @@ -133,6 +133,67 @@ def test_unicode_strings(self): self.assertEqual(result, u"http://www.example.com/ascii/extra_chars-™") +class TestSupportsMicroversion(base.TestCase): + def setUp(self): + super(TestSupportsMicroversion, self).setUp() + self.adapter = mock.Mock(spec=['get_endpoint_data']) + self.endpoint_data = mock.Mock(spec=['min_microversion', + 'max_microversion'], + min_microversion='1.1', + max_microversion='1.99') + self.adapter.get_endpoint_data.return_value = self.endpoint_data + + def test_requested_supported_no_default(self): + self.adapter.default_microversion = None + self.assertTrue( + utils.supports_microversion(self.adapter, '1.2')) + + def test_requested_not_supported_no_default(self): + self.adapter.default_microversion = None + self.assertFalse( + utils.supports_microversion(self.adapter, '2.2')) + + def test_requested_not_supported_no_default_exception(self): + self.adapter.default_microversion = None + self.assertRaises( + exceptions.SDKException, + utils.supports_microversion, + self.adapter, + '2.2', + True) + + def test_requested_supported_higher_default(self): + self.adapter.default_microversion = '1.8' + self.assertTrue( + utils.supports_microversion(self.adapter, '1.6')) + + def test_requested_supported_equal_default(self): + self.adapter.default_microversion = '1.8' + self.assertTrue( + utils.supports_microversion(self.adapter, '1.8')) + + def test_requested_supported_lower_default(self): + self.adapter.default_microversion = '1.2' + self.assertFalse( + utils.supports_microversion(self.adapter, '1.8')) + + def test_requested_supported_lower_default_exception(self): + self.adapter.default_microversion = '1.2' + self.assertRaises( + exceptions.SDKException, + utils.supports_microversion, + self.adapter, + '1.8', + True) + + @mock.patch('openstack.utils.supports_microversion') + def test_require_microversion(self, sm_mock): + utils.require_microversion(self.adapter, '1.2') + sm_mock.assert_called_with(self.adapter, + '1.2', + raise_exception=True) + + class TestMaximumSupportedMicroversion(base.TestCase): def setUp(self): super(TestMaximumSupportedMicroversion, self).setUp() diff --git a/openstack/utils.py b/openstack/utils.py index 3090c2cbf..b0358ade9 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -94,18 +94,23 @@ def __getitem__(self, key): return keys -def supports_microversion(adapter, microversion): +def supports_microversion(adapter, microversion, raise_exception=False): """Determine if the given adapter supports the given microversion. - Checks the min and max microversion asserted by the service and checks - to make sure that ``min <= microversion <= max``. + Checks the min and max microversion asserted by the service and checks to + make sure that ``min <= microversion <= max``. Current default microversion + is taken into consideration if set and verifies that ``microversion <= + default``. - :param adapter: - :class:`~keystoneauth1.adapter.Adapter` instance. - :param str microversion: - String containing the desired microversion. + :param adapter: :class:`~keystoneauth1.adapter.Adapter` instance. + :param str microversion: String containing the desired microversion. + :param bool raise_exception: Raise exception when requested microversion + is not supported be the server side or is higher than the current + default microversion. :returns: True if the service supports the microversion. :rtype: bool + :raises: :class:`~openstack.exceptions.SDKException` when requested + microversion is not supported. """ endpoint_data = adapter.get_endpoint_data() @@ -115,10 +120,41 @@ def supports_microversion(adapter, microversion): endpoint_data.min_microversion, endpoint_data.max_microversion, microversion)): + if adapter.default_microversion is not None: + # If default_microversion is set - evaluate + # whether it match the expectation + candidate = discover.normalize_version_number( + adapter.default_microversion) + required = discover.normalize_version_number(microversion) + supports = discover.version_match(required, candidate) + if raise_exception and not supports: + raise exceptions.SDKException( + 'Required microversion {ver} is higher than currently ' + 'selected {curr}'.format( + ver=microversion, + curr=adapter.default_microversion) + ) + return supports return True + if raise_exception: + raise exceptions.SDKException( + 'Required microversion {ver} is not supported ' + 'by the server side'.format(ver=microversion) + ) return False +def require_microversion(adapter, required): + """Require microversion. + + :param adapter: :class:`~keystoneauth1.adapter.Adapter` instance. + :param str microversion: String containing the desired microversion. + :raises: :class:`~openstack.exceptions.SDKException` when requested + microversion is not supported + """ + supports_microversion(adapter, required, raise_exception=True) + + def pick_microversion(session, required): """Get a new microversion if it is higher than session's default. @@ -145,6 +181,10 @@ def pick_microversion(session, required): else required) if required is not None: + if not supports_microversion(session, required): + raise exceptions.SDKException( + 'Requested microversion is not supported by the server side ' + 'or the default microversion is too low') return discover.version_to_string(required) diff --git a/releasenotes/notes/fix-microversion-354dc70deb2b2f0b.yaml b/releasenotes/notes/fix-microversion-354dc70deb2b2f0b.yaml new file mode 100644 index 000000000..3cb5745b3 --- /dev/null +++ b/releasenotes/notes/fix-microversion-354dc70deb2b2f0b.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Modify microversion handling. Microversion chosen by the client/user is + respected in the microversion negotiation. For features, requiring + particular microversion, it would be ensured it is supported by the server + side and required microversion is <= chosen microversion, otherwise call + will be rejected. From 97f7095abd422056d9aa13e9c50c56e0705e0db5 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Mon, 12 Oct 2020 11:49:46 +0200 Subject: [PATCH 2731/3836] Support waiting for bare metal power states Use the new functionality to fix a race in the functional job. Depends-On: https://review.opendev.org/757293 Change-Id: Icc881acebd72ed75e654677d2d5dbff63c969298 --- doc/source/user/proxies/baremetal.rst | 4 +- .../user/resources/baremetal/v1/node.rst | 8 +++ openstack/baremetal/v1/_common.py | 9 +++ openstack/baremetal/v1/_proxy.py | 28 ++++++-- openstack/baremetal/v1/node.py | 70 +++++++++++++++++-- .../baremetal/test_baremetal_node.py | 5 +- .../tests/unit/baremetal/v1/test_node.py | 27 +++++++ .../notes/power-wait-751083852f958cb4.yaml | 4 ++ 8 files changed, 142 insertions(+), 13 deletions(-) create mode 100644 releasenotes/notes/power-wait-751083852f958cb4.yaml diff --git a/doc/source/user/proxies/baremetal.rst b/doc/source/user/proxies/baremetal.rst index 04538c39e..55043c433 100644 --- a/doc/source/user/proxies/baremetal.rst +++ b/doc/source/user/proxies/baremetal.rst @@ -18,8 +18,8 @@ Node Operations :noindex: :members: nodes, find_node, get_node, create_node, update_node, patch_node, delete_node, validate_node, set_node_power_state, set_node_provision_state, - wait_for_nodes_provision_state, wait_for_node_reservation, - set_node_maintenance, unset_node_maintenance + wait_for_nodes_provision_state, wait_for_node_power_state, + wait_for_node_reservation, set_node_maintenance, unset_node_maintenance Port Operations ^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/baremetal/v1/node.rst b/doc/source/user/resources/baremetal/v1/node.rst index ebc5fb327..14e691ed2 100644 --- a/doc/source/user/resources/baremetal/v1/node.rst +++ b/doc/source/user/resources/baremetal/v1/node.rst @@ -11,6 +11,14 @@ The ``Node`` class inherits from :class:`~openstack.resource.Resource`. .. autoclass:: openstack.baremetal.v1.node.Node :members: +The PowerAction Class +^^^^^^^^^^^^^^^^^^^^^ + +The ``PowerAction`` enumeration represents known power actions. + +.. autoclass:: openstack.baremetal.v1.node.PowerAction + :members: + The ValidationResult Class ^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/openstack/baremetal/v1/_common.py b/openstack/baremetal/v1/_common.py index e37c7b73a..7459842ac 100644 --- a/openstack/baremetal/v1/_common.py +++ b/openstack/baremetal/v1/_common.py @@ -49,6 +49,15 @@ } """Mapping of provisioning actions to expected stable states.""" +EXPECTED_POWER_STATES = { + 'power on': 'power on', + 'power off': 'power off', + 'rebooting': 'power on', + 'soft power off': 'power off', + 'soft rebooting': 'power on', +} +"""Mapping of target power states to expected power states.""" + STATE_VERSIONS = { 'enroll': '1.11', 'manageable': '1.4', diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 903e584c3..76d144f2a 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -442,7 +442,7 @@ def wait_for_nodes_provision_state(self, nodes, expected_state, else: return _node.WaitResult(finished, failed, remaining) - def set_node_power_state(self, node, target): + def set_node_power_state(self, node, target, wait=False, timeout=None): """Run an action modifying node's power state. This call is asynchronous, it will return success as soon as the Bare @@ -450,10 +450,30 @@ def set_node_power_state(self, node, target): :param node: The value can be the name or ID of a node or a :class:`~openstack.baremetal.v1.node.Node` instance. - :param target: Target power state, e.g. "rebooting", "power on". - See the Bare Metal service documentation for available actions. + :param target: Target power state, one of + :class:`~openstack.baremetal.v1.node.PowerAction` or a string. + :param wait: Whether to wait for the node to get into the expected + state. + :param timeout: If ``wait`` is set to ``True``, specifies how much (in + seconds) to wait for the expected state to be reached. The value of + ``None`` (the default) means no client-side timeout. """ - self._get_resource(_node.Node, node).set_power_state(self, target) + self._get_resource(_node.Node, node).set_power_state( + self, target, wait=wait, timeout=timeout) + + def wait_for_node_power_state(self, node, expected_state, timeout=None): + """Wait for the node to reach the power state. + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :param timeout: How much (in seconds) to wait for the target state + to be reached. The value of ``None`` (the default) means + no timeout. + + :returns: The updated :class:`~openstack.baremetal.v1.node.Node` + """ + res = self._get_resource(_node.Node, node) + return res.wait_for_power_state(self, expected_state, timeout=timeout) def wait_for_node_reservation(self, node, timeout=None): """Wait for a lock on the node to be released. diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 8ef4edff4..72c50e19a 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -11,6 +11,7 @@ # under the License. import collections +import enum from openstack.baremetal.v1 import _common from openstack import exceptions @@ -32,6 +33,24 @@ def __init__(self, result, reason): self.reason = reason +class PowerAction(enum.Enum): + """Mapping from an action to a target power state.""" + + POWER_ON = 'power on' + """Power on the node.""" + + POWER_OFF = 'power off' + """Power off the node (using hard power off).""" + REBOOT = 'rebooting' + """Reboot the node (using hard power off).""" + + SOFT_POWER_OFF = 'soft power off' + """Power off the node using soft power off.""" + + SOFT_REBOOT = 'soft rebooting' + """Reboot the node using soft power off.""" + + class WaitResult(collections.namedtuple('WaitResult', ['success', 'failure', 'timeout'])): """A named tuple representing a result of waiting for several nodes. @@ -416,6 +435,34 @@ def set_provision_state(self, session, target, config_drive=None, else: return self.fetch(session) + def wait_for_power_state(self, session, expected_state, timeout=None): + """Wait for the node to reach the expected power state. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param expected_state: The expected power state to reach. + :param timeout: If ``wait`` is set to ``True``, specifies how much (in + seconds) to wait for the expected state to be reached. The value of + ``None`` (the default) means no client-side timeout. + + :return: This :class:`Node` instance. + :raises: :class:`~openstack.exceptions.ResourceTimeout` on timeout. + """ + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for node %(node)s to reach " + "power state '%(state)s'" % {'node': self.id, + 'state': expected_state}): + self.fetch(session) + if self.power_state == expected_state: + return self + + session.log.debug( + 'Still waiting for node %(node)s to reach power state ' + '"%(target)s", the current state is "%(state)s"', + {'node': self.id, 'target': expected_state, + 'state': self.power_state}) + def wait_for_provision_state(self, session, expected_state, timeout=None, abort_on_failed_state=True): """Wait for the node to reach the expected state. @@ -532,8 +579,7 @@ def _check_state_reached(self, session, expected_state, "the last error is %(error)s" % {'node': self.id, 'error': self.last_error}) - # TODO(dtantsur): waiting for power state - def set_power_state(self, session, target): + def set_power_state(self, session, target, wait=False, timeout=None): """Run an action modifying this node's power state. This call is asynchronous, it will return success as soon as the Bare @@ -541,9 +587,22 @@ def set_power_state(self, session, target): :param session: The session to use for making this request. :type session: :class:`~keystoneauth1.adapter.Adapter` - :param target: Target power state, e.g. "rebooting", "power on". - See the Bare Metal service documentation for available actions. + :param target: Target power state, as a :class:`PowerAction` or + a string. + :param wait: Whether to wait for the expected power state to be + reached. + :param timeout: Timeout (in seconds) to wait for the target state to be + reached. If ``None``, wait without timeout. """ + if isinstance(target, PowerAction): + target = target.value + if wait: + try: + expected = _common.EXPECTED_POWER_STATES[target] + except KeyError: + raise ValueError("Cannot use target power state %s with wait, " + "the expected state is not known" % target) + session = self._get_session(session) if target.startswith("soft "): @@ -567,6 +626,9 @@ def set_power_state(self, session, target): "to {target}".format(node=self.id, target=target)) exceptions.raise_from_response(response, error_message=msg) + if wait: + self.wait_for_power_state(session, expected, timeout=timeout) + def attach_vif(self, session, vif_id, retry_on_conflict=True): """Attach a VIF to the node. diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index 23f5ed5d8..aa44c2265 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -177,12 +177,11 @@ def test_node_power_state(self): node = self.create_node() self.assertIsNone(node.power_state) - self.conn.baremetal.set_node_power_state(node, 'power on') + self.conn.baremetal.set_node_power_state(node, 'power on', wait=True) node = self.conn.baremetal.get_node(node.id) - # Fake nodes react immediately to power requests. self.assertEqual('power on', node.power_state) - self.conn.baremetal.set_node_power_state(node, 'power off') + self.conn.baremetal.set_node_power_state(node, 'power off', wait=True) node = self.conn.baremetal.get_node(node.id) self.assertEqual('power off', node.power_state) diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index dbc874f4c..4b0802ca9 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -811,3 +811,30 @@ def test_node_patch_reset_interfaces(self, mock__commit, mock_prepreq, self.assertIn('1.45', commit_args) self.assertEqual(commit_kwargs['retry_on_conflict'], True) mock_patch.assert_not_called() + + +@mock.patch('time.sleep', lambda _t: None) +@mock.patch.object(node.Node, 'fetch', autospec=True) +class TestNodeWaitForPowerState(base.TestCase): + def setUp(self): + super(TestNodeWaitForPowerState, self).setUp() + self.node = node.Node(**FAKE) + self.session = mock.Mock() + + def test_success(self, mock_fetch): + self.node.power_state = 'power on' + + def _get_side_effect(_self, session): + self.node.power_state = 'power off' + self.assertIs(session, self.session) + + mock_fetch.side_effect = _get_side_effect + + node = self.node.wait_for_power_state(self.session, 'power off') + self.assertIs(node, self.node) + + def test_timeout(self, mock_fetch): + self.node.power_state = 'power on' + self.assertRaises(exceptions.ResourceTimeout, + self.node.wait_for_power_state, + self.session, 'power off', timeout=0.001) diff --git a/releasenotes/notes/power-wait-751083852f958cb4.yaml b/releasenotes/notes/power-wait-751083852f958cb4.yaml new file mode 100644 index 000000000..359f1d35b --- /dev/null +++ b/releasenotes/notes/power-wait-751083852f958cb4.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Support waiting for bare metal power states. From 5c31eab09c49cfe7df5b3b397dbaf1447a602ace Mon Sep 17 00:00:00 2001 From: "wu.shiming" Date: Tue, 13 Oct 2020 09:38:56 +0800 Subject: [PATCH 2732/3836] Replace assertItemsEqual with assertCountEqual assertItemsEqual was removed from Python's unittest.TestCase in Python 3.3 [1][2]. We have been able to use them since then, because testtools required unittest2, which still included it. With testtools removing Python 2.7 support [3][4], we will lose support for assertItemsEqual, so we should switch to use assertCountEqual. [1] - https://bugs.python.org/issue17866 [2] - https://hg.python.org/cpython/rev/d9921cb6e3cd [3] - testing-cabal/testtools#286 [4] - testing-cabal/testtools#277 Change-Id: I6c165f5600ba281ba41df78de8414404c411d44f --- openstack/tests/functional/network/v2/test_address_group.py | 6 +++--- openstack/tests/unit/network/v2/test_address_group.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openstack/tests/functional/network/v2/test_address_group.py b/openstack/tests/functional/network/v2/test_address_group.py index cb9b8c95f..cf3623e72 100644 --- a/openstack/tests/functional/network/v2/test_address_group.py +++ b/openstack/tests/functional/network/v2/test_address_group.py @@ -35,7 +35,7 @@ def setUp(self): self.assertEqual(self.ADDRESS_GROUP_NAME, address_group.name) self.assertEqual(self.ADDRESS_GROUP_DESCRIPTION, address_group.description) - self.assertItemsEqual(self.ADDRESSES, address_group.addresses) + self.assertCountEqual(self.ADDRESSES, address_group.addresses) self.ADDRESS_GROUP_ID = address_group.id def tearDown(self): @@ -70,7 +70,7 @@ def test_add_remove_addresses(self): self.ADDRESS_GROUP_ID, addrs) updated_addrs = self.ADDRESSES.copy() updated_addrs.extend(addrs) - self.assertItemsEqual(updated_addrs, sot.addresses) + self.assertCountEqual(updated_addrs, sot.addresses) sot = self.conn.network.remove_addresses_from_address_group( self.ADDRESS_GROUP_ID, addrs) - self.assertItemsEqual(self.ADDRESSES, sot.addresses) + self.assertCountEqual(self.ADDRESSES, sot.addresses) diff --git a/openstack/tests/unit/network/v2/test_address_group.py b/openstack/tests/unit/network/v2/test_address_group.py index 0fe6a7cdf..9275575f7 100644 --- a/openstack/tests/unit/network/v2/test_address_group.py +++ b/openstack/tests/unit/network/v2/test_address_group.py @@ -52,4 +52,4 @@ def test_make_it(self): self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['description'], sot.description) self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) - self.assertItemsEqual(EXAMPLE['addresses'], sot.addresses) + self.assertCountEqual(EXAMPLE['addresses'], sot.addresses) From abccf3a6cfd5093f5cd7d4ae43adbdb5bc7d4794 Mon Sep 17 00:00:00 2001 From: Iury Gregory Melo Ferreira Date: Tue, 13 Oct 2020 12:05:59 +0200 Subject: [PATCH 2733/3836] Disable dstat on ironic job Change-Id: I4a7742e5621a3571000b991d49a935d6517cbbe3 --- .zuul.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.zuul.yaml b/.zuul.yaml index 53f8a8566..6e3690d21 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -274,6 +274,7 @@ n-sch: false nova: false placement-api: false + dstat: false tox_environment: OPENSTACKSDK_HAS_IRONIC: 1 # NOTE(dtantsur): this job cannot run many regular tests (e.g. compute From 256e25e321838cc568be56acd9c2d106eb9e4535 Mon Sep 17 00:00:00 2001 From: Riccardo Pittau Date: Wed, 14 Oct 2020 17:57:28 +0200 Subject: [PATCH 2734/3836] Migrate ironic job to focal Depends-On: https://review.opendev.org/758201 Change-Id: Ife258aa7229afed0099036bb2051be73f695b547 --- .zuul.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index 6e3690d21..734f69cd0 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -403,7 +403,7 @@ # Ironic jobs, non-voting to avoid tight coupling - ironic-inspector-tempest-openstacksdk-src: voting: false - - bifrost-integration-tinyipa-ubuntu-bionic: + - bifrost-integration-tinyipa-ubuntu-focal: voting: false - metalsmith-integration-openstacksdk-src: voting: false From 2dead58ceb659af33386e12cc95bf3f136e55aa0 Mon Sep 17 00:00:00 2001 From: "wu.shiming" Date: Tue, 3 Nov 2020 13:57:42 +0800 Subject: [PATCH 2735/3836] Update TOX_CONSTRAINTS_FILE UPPER_CONSTRAINTS_FILE is old name and deprecated -https://zuul-ci.org/docs/zuul-jobs/python-roles.html#rolevar-tox.tox_constraints_file This allows to use lower-constraints file as more readable way instead of UPPER_CONSTRAINTS_FILE=. [1] https://review.opendev.org/#/c/722814/ Change-Id: I6ca7766e35c9c5882203479ffef0437a6925c2ae --- tox.ini | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index 5334aac5e..6804a88ef 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,7 @@ setenv = OS_STDOUT_CAPTURE={env:OS_STDOUT_CAPTURE:true} OS_STDERR_CAPTURE={env:OS_STDERR_CAPTURE:true} deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt commands = stestr run {posargs} @@ -47,7 +47,7 @@ commands = [testenv:venv] deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt @@ -81,7 +81,7 @@ commands = {toxinidir}/extras/run-ansible-tests.sh -e {envdir} {posargs} [testenv:docs] deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/doc/requirements.txt commands = sphinx-build -W -d doc/build/doctrees --keep-going -b html doc/source/ doc/build/html @@ -96,7 +96,7 @@ commands = [testenv:releasenotes] deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/doc/requirements.txt commands = sphinx-build -a -E -W -d releasenotes/build/doctrees --keep-going -b html releasenotes/source releasenotes/build/html From 907634cc0a8350c786135fb8fe92a39832e0818f Mon Sep 17 00:00:00 2001 From: zhufl Date: Mon, 2 Nov 2020 17:33:10 +0800 Subject: [PATCH 2736/3836] Fix invalid assertIsNotNone statement This is to fix the invalid assertIsNotNone statements like "self.assertIsNotNone('maxTotalInstances', sot.absolute)", which will always return True. Change-Id: If3356acb8241198e028adfc2aecceee7dbadcc1e --- openstack/tests/functional/compute/v2/test_limits.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openstack/tests/functional/compute/v2/test_limits.py b/openstack/tests/functional/compute/v2/test_limits.py index 8291b1c84..27c362ae2 100644 --- a/openstack/tests/functional/compute/v2/test_limits.py +++ b/openstack/tests/functional/compute/v2/test_limits.py @@ -17,8 +17,8 @@ class TestLimits(base.BaseFunctionalTest): def test_limits(self): sot = self.conn.compute.get_limits() - self.assertIsNotNone('maxTotalInstances', sot.absolute) - self.assertIsNotNone('maxTotalRAMSize', sot.absolute) - self.assertIsNotNone('maxTotalKeypairs', sot.absolute) - self.assertIsNotNone('maxSecurityGroups', sot.absolute) - self.assertIsNotNone('maxSecurityGroupRules', sot.absolute) + self.assertIsNotNone(sot.absolute['instances']) + self.assertIsNotNone(sot.absolute['total_ram']) + self.assertIsNotNone(sot.absolute['keypairs']) + self.assertIsNotNone(sot.absolute['security_groups']) + self.assertIsNotNone(sot.absolute['security_group_rules']) From 9d0b048fca3cd68bafe6c711b529ba2eacf8014b Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 9 Nov 2020 11:43:36 +0100 Subject: [PATCH 2737/3836] Add user_id as optional param to keypair operations In order to switch OSC to use SDK for keypair operations it is very helpful to add user_id into the corresponding functions. Change-Id: Iea463f3a915d1074f8c1bed1dddaa40348a5bf8f --- openstack/compute/v2/_proxy.py | 43 ++++++------ openstack/tests/unit/compute/v2/test_proxy.py | 67 ++++++++++++++----- 2 files changed, 73 insertions(+), 37 deletions(-) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 2aec33d1b..b3bbe51b3 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -472,48 +472,53 @@ def create_keypair(self, **attrs): """ return self._create(_keypair.Keypair, **attrs) - def delete_keypair(self, keypair, ignore_missing=True): + def delete_keypair(self, keypair, ignore_missing=True, user_id=None): """Delete a keypair :param keypair: The value can be either the ID of a keypair or a - :class:`~openstack.compute.v2.keypair.Keypair` - instance. + :class:`~openstack.compute.v2.keypair.Keypair` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the keypair does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent keypair. + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the keypair does not exist. When set to ``True``, no exception + will be set when attempting to delete a nonexistent keypair. + :param str user_id: Optional user_id owning the keypair :returns: ``None`` """ - self._delete(_keypair.Keypair, keypair, ignore_missing=ignore_missing) + attrs = {'user_id': user_id} if user_id else {} + self._delete(_keypair.Keypair, keypair, ignore_missing=ignore_missing, + **attrs) - def get_keypair(self, keypair): + def get_keypair(self, keypair, user_id=None): """Get a single keypair :param keypair: The value can be the ID of a keypair or a - :class:`~openstack.compute.v2.keypair.Keypair` - instance. + :class:`~openstack.compute.v2.keypair.Keypair` instance. + :param str user_id: Optional user_id owning the keypair :returns: One :class:`~openstack.compute.v2.keypair.Keypair` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ - return self._get(_keypair.Keypair, keypair) + attrs = {'user_id': user_id} if user_id else {} + return self._get(_keypair.Keypair, keypair, **attrs) - def find_keypair(self, name_or_id, ignore_missing=True): + def find_keypair(self, name_or_id, ignore_missing=True, user_id=None): """Find a single keypair :param name_or_id: The name or ID of a keypair. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the resource does not exist. When set to ``True``, None will be + returned when attempting to find a nonexistent resource. + :param str user_id: Optional user_id owning the keypair + :returns: One :class:`~openstack.compute.v2.keypair.Keypair` or None """ + attrs = {'user_id': user_id} if user_id else {} return self._find(_keypair.Keypair, name_or_id, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, + **attrs) def keypairs(self, **query): """Return a generator of keypairs diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 08c452818..77d223d1c 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -196,6 +196,55 @@ def test_delete_flavor_extra_specs_prop(self): expected_args=["prop"]) +class TestKeyPair(TestComputeProxy): + def test_keypair_create(self): + self.verify_create(self.proxy.create_keypair, keypair.Keypair) + + def test_keypair_delete(self): + self.verify_delete(self.proxy.delete_keypair, keypair.Keypair, False) + + def test_keypair_delete_ignore(self): + self.verify_delete(self.proxy.delete_keypair, keypair.Keypair, True) + + def test_keypair_delete_user_id(self): + self.verify_delete( + self.proxy.delete_keypair, keypair.Keypair, + True, + method_kwargs={'user_id': 'fake_user'}, + expected_kwargs={'user_id': 'fake_user'} + ) + + def test_keypair_find(self): + self.verify_find(self.proxy.find_keypair, keypair.Keypair) + + def test_keypair_find_user_id(self): + self.verify_find( + self.proxy.find_keypair, keypair.Keypair, + method_kwargs={'user_id': 'fake_user'}, + expected_kwargs={'user_id': 'fake_user'} + ) + + def test_keypair_get(self): + self.verify_get(self.proxy.get_keypair, keypair.Keypair) + + def test_keypair_get_user_id(self): + self.verify_get( + self.proxy.get_keypair, keypair.Keypair, + method_kwargs={'user_id': 'fake_user'}, + expected_kwargs={'user_id': 'fake_user'} + ) + + def test_keypairs(self): + self.verify_list_no_kwargs(self.proxy.keypairs, keypair.Keypair) + + def test_keypairs_user_id(self): + self.verify_list( + self.proxy.keypairs, keypair.Keypair, + method_kwargs={'user_id': 'fake_user'}, + expected_kwargs={'user_id': 'fake_user'} + ) + + class TestCompute(TestComputeProxy): def test_extension_find(self): self.verify_find(self.proxy.find_extension, extension.Extension) @@ -225,24 +274,6 @@ def test_images_not_detailed(self): method_kwargs={"details": False, "query": 1}, expected_kwargs={"query": 1}) - def test_keypair_create(self): - self.verify_create(self.proxy.create_keypair, keypair.Keypair) - - def test_keypair_delete(self): - self.verify_delete(self.proxy.delete_keypair, keypair.Keypair, False) - - def test_keypair_delete_ignore(self): - self.verify_delete(self.proxy.delete_keypair, keypair.Keypair, True) - - def test_keypair_find(self): - self.verify_find(self.proxy.find_keypair, keypair.Keypair) - - def test_keypair_get(self): - self.verify_get(self.proxy.get_keypair, keypair.Keypair) - - def test_keypairs(self): - self.verify_list_no_kwargs(self.proxy.keypairs, keypair.Keypair) - def test_limits_get(self): self.verify_get(self.proxy.get_limits, limits.Limits, value=[]) From 292c917949295f5e7429a9a2e152a05d05c9e63a Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 3 Nov 2020 13:18:14 +0100 Subject: [PATCH 2738/3836] Switch flavor ops in the cloud layer to proxy Since very long time we want to switch cloud layer functions to use underneath the proxy layer to reduce maintenance complexity. For flavors we even did some tries in the past. This time it is meant even more seriosly. Since flavors listing is a subject of caching, but dogpile/pickle does not support caching of the complex objects returned by funciton we convert and return flavors to Munch objects (we anyway wanted to have it this way). Change-Id: I0353bb8d1be69e18dd31f0abedf25818b42c14ce --- openstack/cloud/_compute.py | 159 +++++------------- openstack/compute/v2/_proxy.py | 20 ++- openstack/compute/v2/flavor.py | 2 +- .../tests/functional/cloud/test_flavor.py | 2 - openstack/tests/unit/cloud/test_caching.py | 16 +- .../tests/unit/cloud/test_create_server.py | 7 +- openstack/tests/unit/cloud/test_flavors.py | 43 ++++- openstack/tests/unit/compute/v2/test_proxy.py | 68 ++++++-- .../flavor-cloud-layer-0b4d130ac1c5e7c4.yaml | 4 + 9 files changed, 174 insertions(+), 147 deletions(-) create mode 100644 releasenotes/notes/flavor-cloud-layer-0b4d130ac1c5e7c4.yaml diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index aad122716..d8f806986 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -159,29 +159,13 @@ def list_flavors(self, get_extra=False): :returns: A list of flavor ``munch.Munch``. """ - data = proxy._json_response( - self.compute.get( - '/flavors/detail', params=dict(is_public='None')), - error_message="Error fetching flavor list") - flavors = self._normalize_flavors( - self._get_and_munchify('flavors', data)) + data = self.compute.flavors(details=True) + flavors = [] - for flavor in flavors: + for flavor in data: if not flavor.extra_specs and get_extra: - endpoint = "/flavors/{id}/os-extra_specs".format( - id=flavor.id) - try: - data = proxy._json_response( - self.compute.get(endpoint), - error_message="Error fetching flavor extra specs") - flavor.extra_specs = self._get_and_munchify( - 'extra_specs', data) - except exc.OpenStackCloudHTTPError as e: - flavor.extra_specs = {} - self.log.debug( - 'Fetching extra specs for flavor failed:' - ' %(msg)s', {'msg': str(e)}) - + flavor.fetch_extra_specs(self.compute) + flavors.append(flavor._to_munch(original_names=False)) return flavors def list_server_security_groups(self, server): @@ -441,9 +425,12 @@ def get_flavor(self, name_or_id, filters=None, get_extra=True): found. """ - search_func = functools.partial( - self.search_flavors, get_extra=get_extra) - return _utils._get_entity(self, search_func, name_or_id, filters) + if not filters: + filters = {} + flavor = self.compute.find_flavor( + name_or_id, get_extra_specs=get_extra, **filters) + if flavor: + return flavor._to_munch(original_names=False) def get_flavor_by_id(self, id, get_extra=False): """ Get a flavor by ID @@ -454,29 +441,8 @@ def get_flavor_by_id(self, id, get_extra=False): specs. :returns: A flavor ``munch.Munch``. """ - data = proxy._json_response( - self.compute.get('/flavors/{id}'.format(id=id)), - error_message="Error getting flavor with ID {id}".format(id=id) - ) - flavor = self._normalize_flavor( - self._get_and_munchify('flavor', data)) - - if not flavor.extra_specs and get_extra: - endpoint = "/flavors/{id}/os-extra_specs".format( - id=flavor.id) - try: - data = proxy._json_response( - self.compute.get(endpoint), - error_message="Error fetching flavor extra specs") - flavor.extra_specs = self._get_and_munchify( - 'extra_specs', data) - except exc.OpenStackCloudHTTPError as e: - flavor.extra_specs = {} - self.log.debug( - 'Fetching extra specs for flavor failed:' - ' %(msg)s', {'msg': str(e)}) - - return flavor + flavor = self.compute.get_flavor(id, get_extra_specs=get_extra) + return flavor._to_munch(original_names=False) def get_server_console(self, server, length=None): """Get the console log for a server. @@ -1412,27 +1378,23 @@ def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", :raises: OpenStackCloudException on operation error. """ - with _utils.shade_exceptions("Failed to create flavor {name}".format( - name=name)): - payload = { - 'disk': disk, - 'OS-FLV-EXT-DATA:ephemeral': ephemeral, - 'id': flavorid, - 'os-flavor-access:is_public': is_public, - 'name': name, - 'ram': ram, - 'rxtx_factor': rxtx_factor, - 'swap': swap, - 'vcpus': vcpus, - } - if flavorid == 'auto': - payload['id'] = None - data = proxy._json_response(self.compute.post( - '/flavors', - json=dict(flavor=payload))) + attrs = { + 'disk': disk, + 'ephemeral': ephemeral, + 'id': flavorid, + 'is_public': is_public, + 'name': name, + 'ram': ram, + 'rxtx_factor': rxtx_factor, + 'swap': swap, + 'vcpus': vcpus, + } + if flavorid == 'auto': + attrs['id'] = None + + flavor = self.compute.create_flavor(**attrs) - return self._normalize_flavor( - self._get_and_munchify('flavor', data)) + return flavor._to_munch(original_names=False) def delete_flavor(self, name_or_id): """Delete a flavor @@ -1443,19 +1405,17 @@ def delete_flavor(self, name_or_id): :raises: OpenStackCloudException on operation error. """ - flavor = self.get_flavor(name_or_id, get_extra=False) - if flavor is None: - self.log.debug( - "Flavor %s not found for deleting", name_or_id) - return False - - proxy._json_response( - self.compute.delete( - '/flavors/{id}'.format(id=flavor['id'])), - error_message="Unable to delete flavor {name}".format( - name=name_or_id)) - - return True + try: + flavor = self.compute.find_flavor(name_or_id) + if not flavor: + self.log.debug( + "Flavor %s not found for deleting", name_or_id) + return False + self.compute.delete_flavor(flavor) + return True + except exceptions.SDKException: + raise exceptions.OpenStackCloudException( + "Unable to delete flavor {name}".format(name=name_or_id)) def set_flavor_specs(self, flavor_id, extra_specs): """Add extra specs to a flavor @@ -1466,11 +1426,7 @@ def set_flavor_specs(self, flavor_id, extra_specs): :raises: OpenStackCloudException on operation error. :raises: OpenStackCloudResourceNotFound if flavor ID is not found. """ - proxy._json_response( - self.compute.post( - "/flavors/{id}/os-extra_specs".format(id=flavor_id), - json=dict(extra_specs=extra_specs)), - error_message="Unable to set flavor specs") + self.compute.create_flavor_extra_specs(flavor_id, extra_specs) def unset_flavor_specs(self, flavor_id, keys): """Delete extra specs from a flavor @@ -1482,24 +1438,7 @@ def unset_flavor_specs(self, flavor_id, keys): :raises: OpenStackCloudResourceNotFound if flavor ID is not found. """ for key in keys: - proxy._json_response( - self.compute.delete( - "/flavors/{id}/os-extra_specs/{key}".format( - id=flavor_id, key=key)), - error_message="Unable to delete flavor spec {0}".format(key)) - - def _mod_flavor_access(self, action, flavor_id, project_id): - """Common method for adding and removing flavor access - """ - with _utils.shade_exceptions("Error trying to {action} access from " - "flavor ID {flavor}".format( - action=action, flavor=flavor_id)): - endpoint = '/flavors/{id}/action'.format(id=flavor_id) - access = {'tenant': project_id} - access_key = '{action}TenantAccess'.format(action=action) - - proxy._json_response( - self.compute.post(endpoint, json={access_key: access})) + self.compute.delete_flavor_extra_specs_property(flavor_id, key) def add_flavor_access(self, flavor_id, project_id): """Grant access to a private flavor for a project/tenant. @@ -1509,7 +1448,7 @@ def add_flavor_access(self, flavor_id, project_id): :raises: OpenStackCloudException on operation error. """ - self._mod_flavor_access('add', flavor_id, project_id) + self.compute.flavor_add_tenant_access(flavor_id, project_id) def remove_flavor_access(self, flavor_id, project_id): """Revoke access from a private flavor for a project/tenant. @@ -1519,7 +1458,7 @@ def remove_flavor_access(self, flavor_id, project_id): :raises: OpenStackCloudException on operation error. """ - self._mod_flavor_access('remove', flavor_id, project_id) + self.compute.flavor_remove_tenant_access(flavor_id, project_id) def list_flavor_access(self, flavor_id): """List access from a private flavor for a project/tenant. @@ -1530,14 +1469,8 @@ def list_flavor_access(self, flavor_id): :raises: OpenStackCloudException on operation error. """ - data = proxy._json_response( - self.compute.get( - '/flavors/{id}/os-flavor-access'.format(id=flavor_id)), - error_message=( - "Error trying to list access from flavorID {flavor}".format( - flavor=flavor_id))) - return _utils.normalize_flavor_accesses( - self._get_and_munchify('flavor_access', data)) + access = self.compute.get_flavor_access(flavor_id) + return _utils.normalize_flavor_accesses(access) def list_hypervisors(self, filters={}): """List all hypervisors diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 2aec33d1b..44ff72b10 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -62,7 +62,7 @@ def extensions(self): # ========== Flavors ========== def find_flavor(self, name_or_id, ignore_missing=True, - get_extra_specs=False): + get_extra_specs=False, **query): """Find a single flavor :param name_or_id: The name or ID of a flavor. @@ -73,10 +73,14 @@ def find_flavor(self, name_or_id, ignore_missing=True, :param bool get_extra_specs: When set to ``True`` and extra_specs not present in the response will invoke additional API call to fetch extra_specs. + + :param kwargs query: Optional query parameters to be sent to limit + the flavors being returned. + :returns: One :class:`~openstack.compute.v2.flavor.Flavor` or None """ - flavor = self._find(_flavor.Flavor, name_or_id, - ignore_missing=ignore_missing) + flavor = self._find( + _flavor.Flavor, name_or_id, ignore_missing=ignore_missing, **query) if flavor and get_extra_specs and not flavor.extra_specs: flavor = flavor.fetch_extra_specs(self) return flavor @@ -126,19 +130,25 @@ def get_flavor(self, flavor, get_extra_specs=False): flavor = flavor.fetch_extra_specs(self) return flavor - def flavors(self, details=True, **query): + def flavors(self, details=True, get_extra_specs=False, **query): """Return a generator of flavors :param bool details: When ``True``, returns :class:`~openstack.compute.v2.flavor.Flavor` objects, with additional attributes filled. + :param bool get_extra_specs: When set to ``True`` and extra_specs not + present in the response will invoke additional API call to fetch + extra_specs. :param kwargs query: Optional query parameters to be sent to limit the flavors being returned. :returns: A generator of flavor objects """ base_path = '/flavors/detail' if details else '/flavors' - return self._list(_flavor.Flavor, base_path=base_path, **query) + for flv in self._list(_flavor.Flavor, base_path=base_path, **query): + if get_extra_specs and not flv.extra_specs: + flv = flv.fetch_extra_specs(self) + yield flv def flavor_add_tenant_access(self, flavor, tenant): """Adds tenant/project access to flavor. diff --git a/openstack/compute/v2/flavor.py b/openstack/compute/v2/flavor.py index 0b43a441a..da94e8507 100644 --- a/openstack/compute/v2/flavor.py +++ b/openstack/compute/v2/flavor.py @@ -62,7 +62,7 @@ class Flavor(resource.Resource): # TODO(mordred) extra_specs can historically also come from # OS-FLV-WITH-EXT-SPECS:extra_specs. Do we care? #: A dictionary of the flavor's extra-specs key-and-value pairs. - extra_specs = resource.Body('extra_specs', type=dict) + extra_specs = resource.Body('extra_specs', type=dict, default={}) @classmethod def list(cls, session, paginated=True, base_path='/flavors/detail', diff --git a/openstack/tests/functional/cloud/test_flavor.py b/openstack/tests/functional/cloud/test_flavor.py index f207d82f9..e7b12b47c 100644 --- a/openstack/tests/functional/cloud/test_flavor.py +++ b/openstack/tests/functional/cloud/test_flavor.py @@ -66,10 +66,8 @@ def test_create_flavor(self): # We should also always have ephemeral and public attributes self.assertIn('ephemeral', flavor) - self.assertIn('OS-FLV-EXT-DATA:ephemeral', flavor) self.assertEqual(5, flavor['ephemeral']) self.assertIn('is_public', flavor) - self.assertIn('os-flavor-access:is_public', flavor) self.assertTrue(flavor['is_public']) for key in flavor_kwargs.keys(): diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py index 66345e27b..bf5dabd44 100644 --- a/openstack/tests/unit/cloud/test_caching.py +++ b/openstack/tests/unit/cloud/test_caching.py @@ -18,6 +18,7 @@ import openstack import openstack.cloud from openstack.cloud import meta +from openstack.compute.v2 import flavor as _flavor from openstack import exceptions from openstack.tests import fakes from openstack.tests.unit import base @@ -436,10 +437,16 @@ def test_list_flavors(self): endpoint=fakes.COMPUTE_ENDPOINT) uris_to_mock = [ - dict(method='GET', uri=mock_uri, json={'flavors': []}), dict(method='GET', uri=mock_uri, + validate=dict( + headers={'OpenStack-API-Version': 'compute 2.53'}), + json={'flavors': []}), + dict(method='GET', uri=mock_uri, + validate=dict( + headers={'OpenStack-API-Version': 'compute 2.53'}), json={'flavors': fakes.FAKE_FLAVOR_LIST}) ] + self.use_compute_discovery() self.register_uris(uris_to_mock) @@ -447,8 +454,11 @@ def test_list_flavors(self): self.assertEqual([], self.cloud.list_flavors()) - fake_flavor_dicts = self.cloud._normalize_flavors( - fakes.FAKE_FLAVOR_LIST) + fake_flavor_dicts = [ + _flavor.Flavor(connection=self.cloud, **f) + for f in fakes.FAKE_FLAVOR_LIST + ] + self.cloud.list_flavors.invalidate(self.cloud) self.assertEqual(fake_flavor_dicts, self.cloud.list_flavors()) diff --git a/openstack/tests/unit/cloud/test_create_server.py b/openstack/tests/unit/cloud/test_create_server.py index c83db7013..f773c9f13 100644 --- a/openstack/tests/unit/cloud/test_create_server.py +++ b/openstack/tests/unit/cloud/test_create_server.py @@ -791,11 +791,12 @@ def test_create_server_get_flavor_image(self): dict(method='GET', uri='https://image.example.com/v2/images', json=fake_image_search_return), + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['flavors', 'detail'], - qs_elements=['is_public=None']), - json={'flavors': fakes.FAKE_FLAVOR_LIST}), + 'compute', 'public', append=['flavors', 'vanilla'], + qs_elements=[]), + json=fakes.FAKE_FLAVOR), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), diff --git a/openstack/tests/unit/cloud/test_flavors.py b/openstack/tests/unit/cloud/test_flavors.py index 72a76bb33..9676ae209 100644 --- a/openstack/tests/unit/cloud/test_flavors.py +++ b/openstack/tests/unit/cloud/test_flavors.py @@ -18,8 +18,13 @@ class TestFlavors(base.TestCase): + def setUp(self): + super(TestFlavors, self).setUp() + # self.use_compute_discovery() + def test_create_flavor(self): + self.use_compute_discovery() self.register_uris([ dict(method='POST', uri='{endpoint}/flavors'.format( @@ -44,11 +49,12 @@ def test_create_flavor(self): self.assert_calls() def test_delete_flavor(self): + self.use_compute_discovery() self.register_uris([ dict(method='GET', - uri='{endpoint}/flavors/detail?is_public=None'.format( + uri='{endpoint}/flavors/vanilla'.format( endpoint=fakes.COMPUTE_ENDPOINT), - json={'flavors': fakes.FAKE_FLAVOR_LIST}), + json=fakes.FAKE_FLAVOR), dict(method='DELETE', uri='{endpoint}/flavors/{id}'.format( endpoint=fakes.COMPUTE_ENDPOINT, id=fakes.FLAVOR_ID))]) @@ -57,7 +63,12 @@ def test_delete_flavor(self): self.assert_calls() def test_delete_flavor_not_found(self): + self.use_compute_discovery() self.register_uris([ + dict(method='GET', + uri='{endpoint}/flavors/invalid'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + status_code=404), dict(method='GET', uri='{endpoint}/flavors/detail?is_public=None'.format( endpoint=fakes.COMPUTE_ENDPOINT), @@ -68,7 +79,12 @@ def test_delete_flavor_not_found(self): self.assert_calls() def test_delete_flavor_exception(self): + self.use_compute_discovery() self.register_uris([ + dict(method='GET', + uri='{endpoint}/flavors/vanilla'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json=fakes.FAKE_FLAVOR), dict(method='GET', uri='{endpoint}/flavors/detail?is_public=None'.format( endpoint=fakes.COMPUTE_ENDPOINT), @@ -82,6 +98,7 @@ def test_delete_flavor_exception(self): self.cloud.delete_flavor, 'vanilla') def test_list_flavors(self): + self.use_compute_discovery() uris_to_mock = [ dict(method='GET', uri='{endpoint}/flavors/detail?is_public=None'.format( @@ -106,6 +123,7 @@ def test_list_flavors(self): self.assert_calls() def test_list_flavors_with_extra(self): + self.use_compute_discovery() uris_to_mock = [ dict(method='GET', uri='{endpoint}/flavors/detail?is_public=None'.format( @@ -136,6 +154,7 @@ def test_list_flavors_with_extra(self): self.assert_calls() def test_get_flavor_by_ram(self): + self.use_compute_discovery() uris_to_mock = [ dict(method='GET', uri='{endpoint}/flavors/detail?is_public=None'.format( @@ -154,6 +173,7 @@ def test_get_flavor_by_ram(self): self.assertEqual(fakes.STRAWBERRY_FLAVOR_ID, flavor['id']) def test_get_flavor_by_ram_and_include(self): + self.use_compute_discovery() uris_to_mock = [ dict(method='GET', uri='{endpoint}/flavors/detail?is_public=None'.format( @@ -171,6 +191,7 @@ def test_get_flavor_by_ram_and_include(self): self.assertEqual(fakes.STRAWBERRY_FLAVOR_ID, flavor['id']) def test_get_flavor_by_ram_not_found(self): + self.use_compute_discovery() self.register_uris([ dict(method='GET', uri='{endpoint}/flavors/detail?is_public=None'.format( @@ -182,19 +203,19 @@ def test_get_flavor_by_ram_not_found(self): ram=100) def test_get_flavor_string_and_int(self): - flavor_list_uri = '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT) + self.use_compute_discovery() flavor_resource_uri = '{endpoint}/flavors/1/os-extra_specs'.format( endpoint=fakes.COMPUTE_ENDPOINT) - flavor_list_json = {'flavors': [fakes.make_fake_flavor( - '1', 'vanilla')]} + flavor = fakes.make_fake_flavor('1', 'vanilla') flavor_json = {'extra_specs': {}} self.register_uris([ - dict(method='GET', uri=flavor_list_uri, json=flavor_list_json), + dict(method='GET', + uri='{endpoint}/flavors/1'.format( + endpoint=fakes.COMPUTE_ENDPOINT), + json=flavor), dict(method='GET', uri=flavor_resource_uri, json=flavor_json), - dict(method='GET', uri=flavor_list_uri, json=flavor_list_json), - dict(method='GET', uri=flavor_resource_uri, json=flavor_json)]) + ]) flavor1 = self.cloud.get_flavor('1') self.assertEqual('1', flavor1['id']) @@ -202,6 +223,7 @@ def test_get_flavor_string_and_int(self): self.assertEqual('1', flavor2['id']) def test_set_flavor_specs(self): + self.use_compute_discovery() extra_specs = dict(key1='value1') self.register_uris([ dict(method='POST', @@ -213,6 +235,7 @@ def test_set_flavor_specs(self): self.assert_calls() def test_unset_flavor_specs(self): + self.use_compute_discovery() keys = ['key1', 'key2'] self.register_uris([ dict(method='DELETE', @@ -262,6 +285,7 @@ def test_list_flavor_access(self): self.assert_calls() def test_get_flavor_by_id(self): + self.use_compute_discovery() flavor_uri = '{endpoint}/flavors/1'.format( endpoint=fakes.COMPUTE_ENDPOINT) flavor_json = {'flavor': fakes.make_fake_flavor('1', 'vanilla')} @@ -278,6 +302,7 @@ def test_get_flavor_by_id(self): self.assertEqual({}, flavor2.extra_specs) def test_get_flavor_with_extra_specs(self): + self.use_compute_discovery() flavor_uri = '{endpoint}/flavors/1'.format( endpoint=fakes.COMPUTE_ENDPOINT) flavor_extra_uri = '{endpoint}/flavors/1/os-extra_specs'.format( diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 08c452818..30a070927 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -47,6 +47,13 @@ def test_flavor_delete_ignore(self): def test_flavor_find(self): self.verify_find(self.proxy.find_flavor, flavor.Flavor) + def test_flavor_find_query(self): + self.verify_find( + self.proxy.find_flavor, flavor.Flavor, + method_kwargs={"a": "b"}, + expected_kwargs={"a": "b", "ignore_missing": False} + ) + def test_flavor_find_fetch_extra(self): """fetch extra_specs is triggered""" with mock.patch( @@ -129,17 +136,56 @@ def test_flavor_get_skip_fetch_extra(self): ) mocked.assert_not_called() - def test_flavors_detailed(self): - self.verify_list(self.proxy.flavors, flavor.FlavorDetail, - method_kwargs={"details": True, "query": 1}, - expected_kwargs={"query": 1, - "base_path": "/flavors/detail"}) - - def test_flavors_not_detailed(self): - self.verify_list(self.proxy.flavors, flavor.Flavor, - method_kwargs={"details": False, "query": 1}, - expected_kwargs={"query": 1, - "base_path": "/flavors"}) + @mock.patch("openstack.proxy.Proxy._list", auto_spec=True) + @mock.patch("openstack.compute.v2.flavor.Flavor.fetch_extra_specs", + auto_spec=True) + def test_flavors_detailed(self, fetch_mock, list_mock): + res = self.proxy.flavors(details=True) + for r in res: + self.assertIsNotNone(r) + fetch_mock.assert_not_called() + list_mock.assert_called_with( + flavor.Flavor, + base_path="/flavors/detail" + ) + + @mock.patch("openstack.proxy.Proxy._list", auto_spec=True) + @mock.patch("openstack.compute.v2.flavor.Flavor.fetch_extra_specs", + auto_spec=True) + def test_flavors_not_detailed(self, fetch_mock, list_mock): + res = self.proxy.flavors(details=False) + for r in res: + self.assertIsNotNone(r) + fetch_mock.assert_not_called() + list_mock.assert_called_with( + flavor.Flavor, + base_path="/flavors" + ) + + @mock.patch("openstack.proxy.Proxy._list", auto_spec=True) + @mock.patch("openstack.compute.v2.flavor.Flavor.fetch_extra_specs", + auto_spec=True) + def test_flavors_query(self, fetch_mock, list_mock): + res = self.proxy.flavors(details=False, get_extra_specs=True, a="b") + for r in res: + fetch_mock.assert_called_with(self.proxy) + list_mock.assert_called_with( + flavor.Flavor, + base_path="/flavors", + a="b" + ) + + @mock.patch("openstack.proxy.Proxy._list", auto_spec=True) + @mock.patch("openstack.compute.v2.flavor.Flavor.fetch_extra_specs", + auto_spec=True) + def test_flavors_get_extra(self, fetch_mock, list_mock): + res = self.proxy.flavors(details=False, get_extra_specs=True) + for r in res: + fetch_mock.assert_called_with(self.proxy) + list_mock.assert_called_with( + flavor.Flavor, + base_path="/flavors" + ) def test_flavor_get_access(self): self._verify("openstack.compute.v2.flavor.Flavor.get_access", diff --git a/releasenotes/notes/flavor-cloud-layer-0b4d130ac1c5e7c4.yaml b/releasenotes/notes/flavor-cloud-layer-0b4d130ac1c5e7c4.yaml new file mode 100644 index 000000000..5c35b42aa --- /dev/null +++ b/releasenotes/notes/flavor-cloud-layer-0b4d130ac1c5e7c4.yaml @@ -0,0 +1,4 @@ +--- +other: + - Flavor operations of the cloud layer are switched to the rely on + the proxy layer From 06db9e37fa51f7f0cd623a9f4d1464cdec915267 Mon Sep 17 00:00:00 2001 From: MartaLais Date: Mon, 21 Sep 2020 13:39:35 -0300 Subject: [PATCH 2739/3836] Volume Target support for Ironic on OpenStack SDK This patch adds support for Ironic Volume Target API. Change-Id: Ic1e080cfc2c6439fddbb41bc6015abcb59291667 Story: #2008169 Task: #40924 --- doc/source/user/proxies/baremetal.rst | 8 + doc/source/user/resources/baremetal/index.rst | 1 + .../resources/baremetal/v1/volume_target.rst | 13 ++ openstack/baremetal/v1/_proxy.py | 145 +++++++++++++- openstack/baremetal/v1/volume_target.py | 60 ++++++ openstack/tests/functional/baremetal/base.py | 11 ++ .../baremetal/test_baremetal_volume_target.py | 179 ++++++++++++++++++ .../tests/unit/baremetal/v1/test_proxy.py | 37 ++++ .../unit/baremetal/v1/test_volume_target.py | 65 +++++++ ...olume_target-support-8130361804366787.yaml | 4 + 10 files changed, 522 insertions(+), 1 deletion(-) create mode 100644 doc/source/user/resources/baremetal/v1/volume_target.rst create mode 100644 openstack/baremetal/v1/volume_target.py create mode 100644 openstack/tests/functional/baremetal/test_baremetal_volume_target.py create mode 100644 openstack/tests/unit/baremetal/v1/test_volume_target.py create mode 100644 releasenotes/notes/ironic-volume_target-support-8130361804366787.yaml diff --git a/doc/source/user/proxies/baremetal.rst b/doc/source/user/proxies/baremetal.rst index 55043c433..350757cd8 100644 --- a/doc/source/user/proxies/baremetal.rst +++ b/doc/source/user/proxies/baremetal.rst @@ -69,6 +69,14 @@ Volume Connector Operations create_volume_connector, update_volume_connector, patch_volume_connector, delete_volume_connector +Volume Target Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. autoclass:: openstack.baremetal.v1._proxy.Proxy + :noindex: + :members: volume_targets, find_volume_target, get_volume_target, + create_volume_target, update_volume_target, + patch_volume_target, delete_volume_target + Utilities --------- diff --git a/doc/source/user/resources/baremetal/index.rst b/doc/source/user/resources/baremetal/index.rst index eccda6908..7ccb9a292 100644 --- a/doc/source/user/resources/baremetal/index.rst +++ b/doc/source/user/resources/baremetal/index.rst @@ -11,3 +11,4 @@ Baremetal Resources v1/port_group v1/allocation v1/volume_connector + v1/volume_target diff --git a/doc/source/user/resources/baremetal/v1/volume_target.rst b/doc/source/user/resources/baremetal/v1/volume_target.rst new file mode 100644 index 000000000..93525a80f --- /dev/null +++ b/doc/source/user/resources/baremetal/v1/volume_target.rst @@ -0,0 +1,13 @@ +openstack.baremetal.v1.volume_target +======================================= + +.. automodule:: openstack.baremetal.v1.volume_target + +The VolumeTarget Class +------------------------- + +The ``VolumeTarget`` class inherits +from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.baremetal.v1.volume_target.VolumeTarget + :members: diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 76d144f2a..356d8ee40 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -18,6 +18,7 @@ from openstack.baremetal.v1 import port as _port from openstack.baremetal.v1 import port_group as _portgroup from openstack.baremetal.v1 import volume_connector as _volumeconnector +from openstack.baremetal.v1 import volume_target as _volumetarget from openstack import exceptions from openstack import proxy from openstack import utils @@ -231,7 +232,7 @@ def nodes(self, details=False, **query): provided as the ``sort_key``. * ``sort_key``: Sorts the response by the this attribute value. Default is ``id``. You can specify multiple pairs of sort - key and sort direction query parameters. If you omit the + key and sort direction query pa rameters. If you omit the sort direction in a pair, the API uses the natural sorting direction of the server attribute that is provided as the ``sort_key``. @@ -1166,3 +1167,145 @@ def delete_volume_connector(self, volume_connector, """ return self._delete(_volumeconnector.VolumeConnector, volume_connector, ignore_missing=ignore_missing) + + def volume_targets(self, details=False, **query): + """Retrieve a generator of volume_target. + + :param details: A boolean indicating whether the detailed information + for every volume_target should be returned. + :param dict query: Optional query parameters to be sent to restrict + the volume_targets returned. Available parameters include: + + * ``fields``: A list containing one or more fields to be returned + in the response. This may lead to some performance gain + because other fields of the resource are not refreshed. + * ``limit``: Requests at most the specified number of + volume_connector be returned from the query. + * ``marker``: Specifies the ID of the last-seen volume_target. + Use the ``limit`` parameter to make an initial limited request + and use the ID of the last-seen volume_target from the + response as the ``marker`` value in subsequent limited request. + * ``node``:only return the ones associated with this specific node + (name or UUID), or an empty set if not found. + * ``sort_dir``:Sorts the response by the requested sort direction. + A valid value is ``asc`` (ascending) or ``desc`` + (descending). Default is ``asc``. You can specify multiple + pairs of sort key and sort direction query parameters. If + you omit the sort direction in a pair, the API uses the + natural sorting direction of the server attribute that is + provided as the ``sort_key``. + * ``sort_key``: Sorts the response by the this attribute value. + Default is ``id``. You can specify multiple pairs of sort + key and sort direction query parameters. If you omit the + sort direction in a pair, the API uses the natural sorting + direction of the server attribute that is provided as the + ``sort_key``. + + :returns: A generator of volume_target instances. + """ + if details: + query['detail'] = True + return _volumetarget.VolumeTarget.list(self, **query) + + def create_volume_target(self, **attrs): + """Create a new volume_target from attributes. + + :param dict attrs: Keyword arguments that will be used to create a + :class: + `~openstack.baremetal.v1.volume_target.VolumeTarget`. + + :returns: The results of volume_target creation. + :rtype::class: + `~openstack.baremetal.v1.volume_target.VolumeTarget`. + """ + return self._create(_volumetarget.VolumeTarget, **attrs) + + def find_volume_target(self, vt_id, ignore_missing=True): + """Find a single volume target. + + :param str vt_id: The ID of a volume target. + + :param bool ignore_missing: When set to ``False``, an exception of + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the volume connector does not exist. When set to `True``, + None will be returned when attempting to find a nonexistent + volume target. + :returns: One :class: + `~openstack.baremetal.v1.volumetarget.VolumeTarget` + object or None. + """ + return self._find(_volumetarget.VolumeTarget, vt_id, + ignore_missing=ignore_missing) + + def get_volume_target(self, volume_target, fields=None): + """Get a specific volume_target. + + :param volume_target: The value can be the ID of a + volume_target or a :class: + `~openstack.baremetal.v1.volume_target.VolumeTarget + instance.` + :param fields: Limit the resource fields to fetch.` + + :returns: One + :class: + `~openstack.baremetal.v1.volume_target.VolumeTarget` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + volume_target matching the name or ID could be found.` + """ + return self._get_with_fields(_volumetarget.VolumeTarget, + volume_target, + fields=fields) + + def update_volume_target(self, volume_target, **attrs): + """Update a volume_target. + + :param volume_target:Either the ID of a volume_target + or an instance of + :class:`~openstack.baremetal.v1.volume_target.VolumeTarget.` + :param dict attrs: The attributes to update on the + volume_target represented by the ``volume_target`` parameter.` + + :returns: The updated volume_target. + :rtype::class: + `~openstack.baremetal.v1.volume_target.VolumeTarget.` + """ + return self._update(_volumetarget.VolumeTarget, + volume_target, **attrs) + + def patch_volume_target(self, volume_target, patch): + """Apply a JSON patch to the volume_target. + + :param volume_target: The value can be the ID of a + volume_target or a :class: + `~openstack.baremetal.v1.volume_target.VolumeTarget` + instance. + :param patch: JSON patch to apply. + + :returns: The updated volume_target. + :rtype::class: + `~openstack.baremetal.v1.volume_target.VolumeTarget.` + """ + return self._get_resource(_volumetarget.VolumeTarget, + volume_target).patch(self, patch) + + def delete_volume_target(self, volume_target, + ignore_missing=True): + """Delete an volume_target. + + :param volume_target: The value can be either the ID of a + volume_target.VolumeTarget or a + :class: + `~openstack.baremetal.v1.volume_target.VolumeTarget` + instance. + :param bool ignore_missing: When set to ``False``, an exception + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the volume_target could not be found. + When set to ``True``, no exception will be raised when + attempting to delete a non-existent volume_target. + + :returns: The instance of the volume_target which was deleted. + :rtype::class: + `~openstack.baremetal.v1.volume_target.VolumeTarget`. + """ + return self._delete(_volumetarget.VolumeTarget, + volume_target, ignore_missing=ignore_missing) diff --git a/openstack/baremetal/v1/volume_target.py b/openstack/baremetal/v1/volume_target.py new file mode 100644 index 000000000..a5762a479 --- /dev/null +++ b/openstack/baremetal/v1/volume_target.py @@ -0,0 +1,60 @@ +# 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.baremetal.v1 import _common +from openstack import resource + + +class VolumeTarget(_common.ListMixin, resource.Resource): + + resources_key = 'targets' + base_path = '/volume/targets' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + allow_patch = True + commit_method = 'PATCH' + commit_jsonpatch = True + + _query_mapping = resource.QueryParameters( + 'node', 'detail', + fields={'type': _common.fields_type}, + ) + + # Volume Targets is available since 1.32 + _max_microversion = '1.32' + + #: The boot index of the Volume target. “0” indicates that this volume is + # used as a boot volume. + boot_index = resource.Body('boot_index') + #: Timestamp at which the port was created. + created_at = resource.Body('created_at') + #: A set of one or more arbitrary metadata key and value pairs. + extra = resource.Body('extra') + #: A list of relative links. Includes the self and bookmark links. + links = resource.Body('links', type=list) + #: The UUID of the Node this resource belongs to. + node_id = resource.Body('node_uuid') + #: A set of physical information of the volume. + properties = resource.Body('properties') + #: Timestamp at which the port was last updated. + updated_at = resource.Body('updated_at') + #: The UUID of the resource. + id = resource.Body('uuid', alternate_id=True) + #: The identifier of the volume. + volume_id = resource.Body('volume_id') + #: The type of Volume target. + volume_type = resource.Body('volume_type') diff --git a/openstack/tests/functional/baremetal/base.py b/openstack/tests/functional/baremetal/base.py index bc0c4cafc..8dc00044d 100644 --- a/openstack/tests/functional/baremetal/base.py +++ b/openstack/tests/functional/baremetal/base.py @@ -73,3 +73,14 @@ def create_volume_connector(self, node_id=None, **kwargs): self.conn.baremetal.delete_volume_connector(volume_connector.id, ignore_missing=True)) return volume_connector + + def create_volume_target(self, node_id=None, **kwargs): + node_id = node_id or self.node_id + volume_target = self.conn.baremetal.create_volume_target( + node_uuid=node_id, **kwargs) + + self.addCleanup( + lambda: + self.conn.baremetal.delete_volume_target(volume_target.id, + ignore_missing=True)) + return volume_target diff --git a/openstack/tests/functional/baremetal/test_baremetal_volume_target.py b/openstack/tests/functional/baremetal/test_baremetal_volume_target.py new file mode 100644 index 000000000..7c65e2619 --- /dev/null +++ b/openstack/tests/functional/baremetal/test_baremetal_volume_target.py @@ -0,0 +1,179 @@ +# 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.tests.functional.baremetal import base + + +class TestBareMetalVolumetarget(base.BaseBaremetalTest): + + min_microversion = '1.32' + + def setUp(self): + super(TestBareMetalVolumetarget, self).setUp() + self.node = self.create_node(provision_state='enroll') + + def test_volume_target_create_get_delete(self): + self.conn.baremetal.set_node_provision_state( + self.node, 'manage', wait=True) + self.conn.baremetal.set_node_power_state(self.node, 'power off') + volume_target = self.create_volume_target( + boot_index=0, + volume_id='04452bed-5367-4202-8bf5-de4335ac56d2', + volume_type='iscsi') + + loaded = self.conn.baremetal.get_volume_target( + volume_target.id) + self.assertEqual(loaded.id, volume_target.id) + self.assertIsNotNone(loaded.node_id) + + with_fields = self.conn.baremetal.get_volume_target( + volume_target.id, fields=['uuid', 'extra']) + self.assertEqual(volume_target.id, with_fields.id) + self.assertIsNone(with_fields.node_id) + + self.conn.baremetal.delete_volume_target(volume_target, + ignore_missing=False) + self.assertRaises(exceptions.ResourceNotFound, + self.conn.baremetal.get_volume_target, + volume_target.id) + + def test_volume_target_list(self): + node2 = self.create_node(name='test-node') + self.conn.baremetal.set_node_provision_state( + node2, 'manage', wait=True) + self.conn.baremetal.set_node_power_state(node2, 'power off') + self.conn.baremetal.set_node_provision_state( + self.node, 'manage', wait=True) + self.conn.baremetal.set_node_power_state(self.node, 'power off') + vt1 = self.create_volume_target( + boot_index=0, + volume_id='bd4d008c-7d31-463d-abf9-6c23d9d55f7f', + node_id=node2.id, + volume_type='iscsi') + vt2 = self.create_volume_target( + boot_index=0, + volume_id='04452bed-5367-4202-8bf5-de4335ac57c2', + node_id=self.node.id, + volume_type='iscsi') + + vts = self.conn.baremetal.volume_targets( + node=self.node.id) + self.assertEqual([v.id for v in vts], [vt2.id]) + + vts = self.conn.baremetal.volume_targets(node=node2.id) + self.assertEqual([v.id for v in vts], [vt1.id]) + + vts = self.conn.baremetal.volume_targets(node='test-node') + self.assertEqual([v.id for v in vts], [vt1.id]) + + vts_with_details = self.conn.baremetal.volume_targets(details=True) + for i in vts_with_details: + self.assertIsNotNone(i.id) + self.assertIsNotNone(i.volume_type) + + vts_with_fields = self.conn.baremetal.volume_targets( + fields=['uuid', 'node_uuid']) + for i in vts_with_fields: + self.assertIsNotNone(i.id) + self.assertIsNone(i.volume_type) + self.assertIsNotNone(i.node_id) + + def test_volume_target_list_update_delete(self): + self.conn.baremetal.set_node_provision_state( + self.node, 'manage', wait=True) + self.conn.baremetal.set_node_power_state(self.node, 'power off') + self.create_volume_target( + boot_index=0, + volume_id='04452bed-5367-4202-8bf5-de4335ac57h3', + node_id=self.node.id, + volume_type='iscsi', + extra={'foo': 'bar'}) + volume_target = next(self.conn.baremetal.volume_targets( + details=True, + node=self.node.id)) + self.assertEqual(volume_target.extra, {'foo': 'bar'}) + + # This test checks that resources returned from listing are usable + self.conn.baremetal.update_volume_target(volume_target, + extra={'foo': 42}) + self.conn.baremetal.delete_volume_target(volume_target, + ignore_missing=False) + + def test_volume_target_update(self): + self.conn.baremetal.set_node_provision_state( + self.node, 'manage', wait=True) + self.conn.baremetal.set_node_power_state(self.node, 'power off') + volume_target = self.create_volume_target( + boot_index=0, + volume_id='04452bed-5367-4202-8bf5-de4335ac53h7', + node_id=self.node.id, + volume_type='isci') + volume_target.extra = {'answer': 42} + + volume_target = self.conn.baremetal.update_volume_target( + volume_target) + self.assertEqual({'answer': 42}, volume_target.extra) + + volume_target = self.conn.baremetal.get_volume_target( + volume_target.id) + self.assertEqual({'answer': 42}, volume_target.extra) + + def test_volume_target_patch(self): + vol_targ_id = '04452bed-5367-4202-9cg6-de4335ac53h7' + self.conn.baremetal.set_node_provision_state( + self.node, 'manage', wait=True) + self.conn.baremetal.set_node_power_state(self.node, 'power off') + volume_target = self.create_volume_target( + boot_index=0, + volume_id=vol_targ_id, + node_id=self.node.id, + volume_type='isci') + + volume_target = self.conn.baremetal.patch_volume_target( + volume_target, dict(path='/extra/answer', op='add', value=42)) + self.assertEqual({'answer': 42}, volume_target.extra) + self.assertEqual(vol_targ_id, + volume_target.volume_id) + + volume_target = self.conn.baremetal.get_volume_target( + volume_target.id) + self.assertEqual({'answer': 42}, volume_target.extra) + + def test_volume_target_negative_non_existing(self): + uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" + self.assertRaises(exceptions.ResourceNotFound, + self.conn.baremetal.get_volume_target, uuid) + self.assertRaises(exceptions.ResourceNotFound, + self.conn.baremetal.find_volume_target, uuid, + ignore_missing=False) + self.assertRaises(exceptions.ResourceNotFound, + self.conn.baremetal.delete_volume_target, uuid, + ignore_missing=False) + self.assertIsNone(self.conn.baremetal.find_volume_target(uuid)) + self.assertIsNone(self.conn.baremetal.delete_volume_target(uuid)) + + def test_volume_target_fields(self): + self.create_node() + self.conn.baremetal.set_node_provision_state( + self.node, 'manage', wait=True) + self.conn.baremetal.set_node_power_state(self.node, 'power off') + self.create_volume_target( + boot_index=0, + volume_id='04452bed-5367-4202-8bf5-99ae634d8971', + node_id=self.node.id, + volume_type='iscsi') + result = self.conn.baremetal.volume_targets( + fields=['uuid', 'node_id']) + for item in result: + self.assertIsNotNone(item.id) diff --git a/openstack/tests/unit/baremetal/v1/test_proxy.py b/openstack/tests/unit/baremetal/v1/test_proxy.py index 3b7597672..f9e8d53a6 100644 --- a/openstack/tests/unit/baremetal/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal/v1/test_proxy.py @@ -20,6 +20,7 @@ from openstack.baremetal.v1 import port from openstack.baremetal.v1 import port_group from openstack.baremetal.v1 import volume_connector +from openstack.baremetal.v1 import volume_target from openstack import exceptions from openstack.tests.unit import base from openstack.tests.unit import test_proxy_base @@ -205,6 +206,42 @@ def test_delete_volume_connector_ignore(self): volume_connector.VolumeConnector, True) + @mock.patch.object(volume_target.VolumeTarget, 'list') + def test_volume_target_detailed(self, mock_list): + result = self.proxy.volume_targets(details=True, query=1) + self.assertIs(result, mock_list.return_value) + mock_list.assert_called_once_with(self.proxy, detail=True, query=1) + + @mock.patch.object(volume_target.VolumeTarget, 'list') + def test_volume_target_not_detailed(self, mock_list): + result = self.proxy.volume_targets(query=1) + self.assertIs(result, mock_list.return_value) + mock_list.assert_called_once_with(self.proxy, query=1) + + def test_create_volume_target(self): + self.verify_create(self.proxy.create_volume_target, + volume_target.VolumeTarget) + + def test_find_volume_target(self): + self.verify_find(self.proxy.find_volume_target, + volume_target.VolumeTarget) + + def test_get_volume_target(self): + self.verify_get(self.proxy.get_volume_target, + volume_target.VolumeTarget, + mock_method=_MOCK_METHOD, + expected_kwargs={'fields': None}) + + def test_delete_volume_target(self): + self.verify_delete(self.proxy.delete_volume_target, + volume_target.VolumeTarget, + False) + + def test_delete_volume_target_ignore(self): + self.verify_delete(self.proxy.delete_volume_target, + volume_target.VolumeTarget, + True) + @mock.patch.object(node.Node, 'fetch', autospec=True) def test__get_with_fields_none(self, mock_fetch): result = self.proxy._get_with_fields(node.Node, 'value') diff --git a/openstack/tests/unit/baremetal/v1/test_volume_target.py b/openstack/tests/unit/baremetal/v1/test_volume_target.py new file mode 100644 index 000000000..4598858a3 --- /dev/null +++ b/openstack/tests/unit/baremetal/v1/test_volume_target.py @@ -0,0 +1,65 @@ +# 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.tests.unit import base + +from openstack.baremetal.v1 import volume_target + +FAKE = { + "boot_index": 0, + "created_at": "2016-08-18T22:28:48.643434+11:11", + "extra": {}, + "links": [ + { + "href": "http://127.0.0.1:6385/v1/volume/targets/", + "rel": "self" + }, + { + "href": "http://127.0.0.1:6385/volume/targets/", + "rel": "bookmark" + } + ], + "node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d", + "properties": {}, + "updated_at": None, + "uuid": "bd4d008c-7d31-463d-abf9-6c23d9d55f7f", + "volume_id": "04452bed-5367-4202-8bf5-de4335ac56d2", + "volume_type": "iscsi" +} + + +class TestVolumeTarget(base.TestCase): + + def test_basic(self): + sot = volume_target.VolumeTarget() + self.assertIsNone(sot.resource_key) + self.assertEqual('targets', sot.resources_key) + self.assertEqual('/volume/targets', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertEqual('PATCH', sot.commit_method) + + def test_instantiate(self): + sot = volume_target.VolumeTarget(**FAKE) + self.assertEqual(FAKE['boot_index'], sot.boot_index) + self.assertEqual(FAKE['created_at'], sot.created_at) + self.assertEqual(FAKE['extra'], sot.extra) + self.assertEqual(FAKE['links'], sot.links) + self.assertEqual(FAKE['node_uuid'], sot.node_id) + self.assertEqual(FAKE['properties'], sot.properties) + self.assertEqual(FAKE['updated_at'], sot.updated_at) + self.assertEqual(FAKE['uuid'], sot.id) + self.assertEqual(FAKE['volume_id'], sot.volume_id) + self.assertEqual(FAKE['volume_type'], sot.volume_type) diff --git a/releasenotes/notes/ironic-volume_target-support-8130361804366787.yaml b/releasenotes/notes/ironic-volume_target-support-8130361804366787.yaml new file mode 100644 index 000000000..eed88c50b --- /dev/null +++ b/releasenotes/notes/ironic-volume_target-support-8130361804366787.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Support for Ironic Volume Target API. From fc36c34724ac389102909fb775d996aa8e1d0245 Mon Sep 17 00:00:00 2001 From: zhufl Date: Tue, 17 Nov 2020 17:11:32 +0800 Subject: [PATCH 2740/3836] Fix the invalid if statement This is to fix the invalid if statement like "if 'interface' == 'internal'" Change-Id: I714d7b5e9dbb2e76c6061419af58fc76817185a9 --- openstack/tests/functional/cloud/test_endpoints.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openstack/tests/functional/cloud/test_endpoints.py b/openstack/tests/functional/cloud/test_endpoints.py index c55581996..67c6f9bf5 100644 --- a/openstack/tests/functional/cloud/test_endpoints.py +++ b/openstack/tests/functional/cloud/test_endpoints.py @@ -155,9 +155,9 @@ def test_list_endpoints(self): found = True self.assertEqual(service['id'], e['service_id']) if 'interface' in e: - if 'interface' == 'internal': + if e['interface'] == 'internal': self.assertEqual('http://internal.test/', e['url']) - elif 'interface' == 'public': + elif e['interface'] == 'public': self.assertEqual('http://public.test/', e['url']) else: self.assertEqual('http://public.test/', From b60915aab3ee0348f3e3cc8aa548f94d2a68b7eb Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 10 Nov 2020 13:44:08 +0100 Subject: [PATCH 2741/3836] Complete compute aggregate functions In order to switch next part of OSC towards SDK we need to add some missing bits to the aggregates. Since this is another example when API returns 400 if we try accessing resource with name and it doesn't support - modify general behavior for also skipping 400 in the find method Change-Id: Ia6711b1c27514d0698fec1efedaefeeb93722b9d --- openstack/compute/v2/_proxy.py | 34 +++++++++++ openstack/compute/v2/aggregate.py | 31 +++++++--- openstack/resource.py | 4 +- .../tests/unit/compute/v2/test_aggregate.py | 24 +++++--- openstack/tests/unit/compute/v2/test_proxy.py | 58 +++++++++++++++++++ ...-aggregate-functions-45d5f2beeeac2b48.yaml | 6 ++ 6 files changed, 141 insertions(+), 16 deletions(-) create mode 100644 releasenotes/notes/complete-aggregate-functions-45d5f2beeeac2b48.yaml diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index b3bbe51b3..32669ee13 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -261,6 +261,20 @@ def get_aggregate(self, aggregate): """ return self._get(_aggregate.Aggregate, aggregate) + def find_aggregate(self, name_or_id, ignore_missing=True): + """Find a single aggregate + + :param name_or_id: The name or ID of an aggregate. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the resource does not exist. When set to ``True``, None will be + returned when attempting to find a nonexistent resource. + :returns: One :class:`~openstack.compute.v2.aggregate.Aggregate` + or None + """ + return self._find(_aggregate.Aggregate, name_or_id, + ignore_missing=ignore_missing) + def create_aggregate(self, **attrs): """Create a new host aggregate from attributes @@ -345,6 +359,26 @@ def set_aggregate_metadata(self, aggregate, metadata): aggregate = self._get_resource(_aggregate.Aggregate, aggregate) return aggregate.set_metadata(self, metadata) + def aggregate_precache_images(self, aggregate, images): + """Requests image precaching on an aggregate + + :param aggregate: Either the ID of a aggregate or a + :class:`~openstack.compute.v2.aggregate.Aggregate` instance. + :param images: Single image id or list of image ids. + + :returns: ``None`` + """ + aggregate = self._get_resource(_aggregate.Aggregate, aggregate) + # We need to ensure we pass list of image IDs + if isinstance(images, str): + images = [images] + image_data = [] + for img in images: + image_data.append({'id': img}) + return aggregate.precache_images(self, image_data) + + # ========== Images ========== + def delete_image(self, image, ignore_missing=True): """Delete an image diff --git a/openstack/compute/v2/aggregate.py b/openstack/compute/v2/aggregate.py index f0d8d8501..b256be3ec 100644 --- a/openstack/compute/v2/aggregate.py +++ b/openstack/compute/v2/aggregate.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. - +from openstack import exceptions from openstack import resource from openstack import utils @@ -30,25 +30,31 @@ class Aggregate(resource.Resource): # Properties #: Availability zone of aggregate availability_zone = resource.Body('availability_zone') + #: The date and time when the resource was created. + created_at = resource.Body('created_at') + #: The date and time when the resource was deleted. + deleted_at = resource.Body('deleted_at') #: Deleted? - deleted = resource.Body('deleted') + is_deleted = resource.Body('deleted', type=bool) #: Name of aggregate name = resource.Body('name') #: Hosts - hosts = resource.Body('hosts') + hosts = resource.Body('hosts', type=list) #: Metadata - metadata = resource.Body('metadata') + metadata = resource.Body('metadata', type=dict) + #: The date and time when the resource was updated + updated_at = resource.Body('updated_at') #: UUID uuid = resource.Body('uuid') - # uuid introduced in 2.41 - _max_microversion = '2.41' + # Image pre-caching introduced in 2.81 + _max_microversion = '2.81' def _action(self, session, body, microversion=None): """Preform aggregate actions given the message body.""" url = utils.urljoin(self.base_path, self.id, 'action') - headers = {'Accept': ''} response = session.post( - url, json=body, headers=headers, microversion=microversion) + url, json=body, microversion=microversion) + exceptions.raise_from_response(response) aggregate = Aggregate() aggregate._translate_response(response=response) return aggregate @@ -67,3 +73,12 @@ def set_metadata(self, session, metadata): """Creates or replaces metadata for an aggregate.""" body = {'set_metadata': {'metadata': metadata}} return self._action(session, body) + + def precache_images(self, session, images): + """Requests image pre-caching""" + body = {'cache': images} + url = utils.urljoin(self.base_path, self.id, 'images') + response = session.post( + url, json=body, microversion=self._max_microversion) + exceptions.raise_from_response(response) + # This API has no result diff --git a/openstack/resource.py b/openstack/resource.py index 618183c67..5010a92fb 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1895,7 +1895,9 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): connection=session._get_connection(), **params) return match.fetch(session, **params) - except exceptions.NotFoundException: + except (exceptions.NotFoundException, exceptions.BadRequestException): + # NOTE(gtema): There are few places around openstack that return + # 400 if we try to GET resource and it doesn't exist. pass if ('name' in cls._query_mapping._mapping.keys() diff --git a/openstack/tests/unit/compute/v2/test_aggregate.py b/openstack/tests/unit/compute/v2/test_aggregate.py index b43dee5d9..220587dbb 100644 --- a/openstack/tests/unit/compute/v2/test_aggregate.py +++ b/openstack/tests/unit/compute/v2/test_aggregate.py @@ -60,7 +60,10 @@ def test_make_it(self): sot = aggregate.Aggregate(**EXAMPLE) self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['availability_zone'], sot.availability_zone) - self.assertEqual(EXAMPLE['deleted'], sot.deleted) + self.assertEqual(EXAMPLE['deleted'], sot.is_deleted) + self.assertEqual(EXAMPLE['deleted_at'], sot.deleted_at) + self.assertEqual(EXAMPLE['created_at'], sot.created_at) + self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) self.assertEqual(EXAMPLE['hosts'], sot.hosts) self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['uuid'], sot.uuid) @@ -73,9 +76,8 @@ def test_add_host(self): url = 'os-aggregates/4/action' body = {"add_host": {"host": "host1"}} - headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, microversion=None) def test_remove_host(self): sot = aggregate.Aggregate(**EXAMPLE) @@ -84,9 +86,8 @@ def test_remove_host(self): url = 'os-aggregates/4/action' body = {"remove_host": {"host": "host1"}} - headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, microversion=None) def test_set_metadata(self): sot = aggregate.Aggregate(**EXAMPLE) @@ -95,6 +96,15 @@ def test_set_metadata(self): url = 'os-aggregates/4/action' body = {"set_metadata": {"metadata": {"key: value"}}} - headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, microversion=None) + + def test_precache_image(self): + sot = aggregate.Aggregate(**EXAMPLE) + + sot.precache_images(self.sess, ['1']) + + url = 'os-aggregates/4/images' + body = {"cache": ['1']} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 77d223d1c..4a36606ee 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -12,6 +12,7 @@ from unittest import mock from openstack.compute.v2 import _proxy +from openstack.compute.v2 import aggregate from openstack.compute.v2 import availability_zone as az from openstack.compute.v2 import extension from openstack.compute.v2 import flavor @@ -245,6 +246,63 @@ def test_keypairs_user_id(self): ) +class TestAggregate(TestComputeProxy): + def test_aggregate_create(self): + self.verify_create(self.proxy.create_aggregate, aggregate.Aggregate) + + def test_aggregate_delete(self): + self.verify_delete( + self.proxy.delete_aggregate, aggregate.Aggregate, False) + + def test_aggregate_delete_ignore(self): + self.verify_delete( + self.proxy.delete_aggregate, aggregate.Aggregate, True) + + def test_aggregate_find(self): + self.verify_find(self.proxy.find_aggregate, aggregate.Aggregate) + + def test_aggregates(self): + self.verify_list_no_kwargs(self.proxy.aggregates, aggregate.Aggregate) + + def test_aggregate_get(self): + self.verify_get(self.proxy.get_aggregate, aggregate.Aggregate) + + def test_aggregate_update(self): + self.verify_update(self.proxy.update_aggregate, aggregate.Aggregate) + + def test_aggregate_add_host(self): + self._verify("openstack.compute.v2.aggregate.Aggregate.add_host", + self.proxy.add_host_to_aggregate, + method_args=["value", "host"], + expected_args=["host"]) + + def test_aggregate_remove_host(self): + self._verify("openstack.compute.v2.aggregate.Aggregate.remove_host", + self.proxy.remove_host_from_aggregate, + method_args=["value", "host"], + expected_args=["host"]) + + def test_aggregate_set_metadata(self): + self._verify("openstack.compute.v2.aggregate.Aggregate.set_metadata", + self.proxy.set_aggregate_metadata, + method_args=["value", {'a': 'b'}], + expected_args=[{'a': 'b'}]) + + def test_aggregate_precache_image(self): + self._verify( + "openstack.compute.v2.aggregate.Aggregate.precache_images", + self.proxy.aggregate_precache_images, + method_args=["value", '1'], + expected_args=[[{'id': '1'}]]) + + def test_aggregate_precache_images(self): + self._verify( + "openstack.compute.v2.aggregate.Aggregate.precache_images", + self.proxy.aggregate_precache_images, + method_args=["value", ['1', '2']], + expected_args=[[{'id': '1'}, {'id': '2'}]]) + + class TestCompute(TestComputeProxy): def test_extension_find(self): self.verify_find(self.proxy.find_extension, extension.Extension) diff --git a/releasenotes/notes/complete-aggregate-functions-45d5f2beeeac2b48.yaml b/releasenotes/notes/complete-aggregate-functions-45d5f2beeeac2b48.yaml new file mode 100644 index 000000000..3fafe6188 --- /dev/null +++ b/releasenotes/notes/complete-aggregate-functions-45d5f2beeeac2b48.yaml @@ -0,0 +1,6 @@ +--- +features: + - Complete compute.aggregate functions to the latest state +fixes: + - aggregate.deleted property is renamed to 'is_deleted' to comply with the + naming convention From 06bcfd8e0c16ec26b240ce255d1835db766e04bd Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Tue, 17 Nov 2020 18:59:19 +0100 Subject: [PATCH 2742/3836] Remove duplicate test_zone_create Looks like a C&P mistake Change-Id: I21f19d4d904e181c172391d481b5c312ec592fc8 --- openstack/tests/unit/dns/v2/test_proxy.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/openstack/tests/unit/dns/v2/test_proxy.py b/openstack/tests/unit/dns/v2/test_proxy.py index 5f6683cee..fe7694c27 100644 --- a/openstack/tests/unit/dns/v2/test_proxy.py +++ b/openstack/tests/unit/dns/v2/test_proxy.py @@ -112,12 +112,6 @@ def test_floating_ip_update(self): self.verify_update(self.proxy.update_floating_ip, floating_ip.FloatingIP) - def test_zone_create(self): - self.verify_create(self.proxy.create_zone, zone.Zone, - method_kwargs={'name': 'id'}, - expected_kwargs={'name': 'id', - 'prepend_key': False}) - class TestDnsZoneImport(TestDnsProxy): def test_zone_import_delete(self): From 8cd14bbe4abcb0f8c869f77e8acb48355f9d71e6 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 17 Nov 2020 11:56:44 +0100 Subject: [PATCH 2743/3836] Add update_flavor method Method for updating flavors was missed in previous implementations. Change-Id: I69587a4a75d1ff2c220b0aee7df09f9c61fe3149 Required-By: https://review.opendev.org/#/c/750151/ --- openstack/compute/v2/_proxy.py | 13 +++++++++++++ openstack/compute/v2/flavor.py | 1 + openstack/tests/unit/compute/v2/test_flavor.py | 2 +- openstack/tests/unit/compute/v2/test_proxy.py | 3 +++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index b3bbe51b3..e066367d4 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -108,6 +108,19 @@ def delete_flavor(self, flavor, ignore_missing=True): """ self._delete(_flavor.Flavor, flavor, ignore_missing=ignore_missing) + def update_flavor(self, flavor, **attrs): + """Update a flavor + + :param server: Either the ID of a flavot or a + :class:`~openstack.compute.v2.flavor.Flavor` instance. + :attrs kwargs: The attributes to update on the flavor represented + by ``flavor``. + + :returns: The updated flavor + :rtype: :class:`~openstack.compute.v2.flavor.Flavor` + """ + return self._update(_flavor.Flavor, flavor, **attrs) + def get_flavor(self, flavor, get_extra_specs=False): """Get a single flavor diff --git a/openstack/compute/v2/flavor.py b/openstack/compute/v2/flavor.py index 0b43a441a..e92b33b75 100644 --- a/openstack/compute/v2/flavor.py +++ b/openstack/compute/v2/flavor.py @@ -25,6 +25,7 @@ class Flavor(resource.Resource): allow_fetch = True allow_delete = True allow_list = True + allow_commit = True _query_mapping = resource.QueryParameters( "sort_key", "sort_dir", "is_public", diff --git a/openstack/tests/unit/compute/v2/test_flavor.py b/openstack/tests/unit/compute/v2/test_flavor.py index bdaff9b79..56cfa2a38 100644 --- a/openstack/tests/unit/compute/v2/test_flavor.py +++ b/openstack/tests/unit/compute/v2/test_flavor.py @@ -51,7 +51,7 @@ def test_basic(self): self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertFalse(sot.allow_commit) + self.assertTrue(sot.allow_commit) self.assertDictEqual({"sort_key": "sort_key", "sort_dir": "sort_dir", diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 77d223d1c..7513a88bd 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -41,6 +41,9 @@ def test_flavor_create(self): def test_flavor_delete(self): self.verify_delete(self.proxy.delete_flavor, flavor.Flavor, False) + def test_flavor_update(self): + self.verify_update(self.proxy.update_flavor, flavor.Flavor, False) + def test_flavor_delete_ignore(self): self.verify_delete(self.proxy.delete_flavor, flavor.Flavor, True) From ed8564ef2ae1a1218fddf130b6c77646d4acdc51 Mon Sep 17 00:00:00 2001 From: Gregory Thiemonge Date: Wed, 18 Nov 2020 15:12:59 +0100 Subject: [PATCH 2744/3836] Add tls_enabled param for Octavia Pools tls_enabled parameter allows the user to enable TLS communication between a load balancer and its members. Story 2008368 Task 41278 Change-Id: If0a2e0c98403baa5fcb2352fc99239106d27ffc6 --- openstack/load_balancer/v2/pool.py | 5 ++++- openstack/tests/unit/load_balancer/test_pool.py | 4 ++++ ...enabled-parameter-for-octavia-pools-f0a23436d826b313.yaml | 5 +++++ 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-tls_enabled-parameter-for-octavia-pools-f0a23436d826b313.yaml diff --git a/openstack/load_balancer/v2/pool.py b/openstack/load_balancer/v2/pool.py index 8ba1385d7..dfc0642d1 100644 --- a/openstack/load_balancer/v2/pool.py +++ b/openstack/load_balancer/v2/pool.py @@ -29,7 +29,8 @@ class Pool(resource.Resource, resource.TagMixin): 'health_monitor_id', 'lb_algorithm', 'listener_id', 'loadbalancer_id', 'description', 'name', 'project_id', 'protocol', 'created_at', 'updated_at', 'provisioning_status', 'operating_status', - 'tls_ciphers', 'tls_versions', is_admin_state_up='admin_state_up', + 'tls_enabled', 'tls_ciphers', 'tls_versions', + is_admin_state_up='admin_state_up', **resource.TagMixin._tag_query_parameters ) @@ -72,3 +73,5 @@ class Pool(resource.Resource, resource.TagMixin): tls_versions = resource.Body('tls_versions', type=list) #: Timestamp when the pool was updated updated_at = resource.Body('updated_at') + #: Use TLS for connections to backend member servers *Type: bool* + tls_enabled = resource.Body('tls_enabled', type=bool) diff --git a/openstack/tests/unit/load_balancer/test_pool.py b/openstack/tests/unit/load_balancer/test_pool.py index 93773490c..34b313f6b 100644 --- a/openstack/tests/unit/load_balancer/test_pool.py +++ b/openstack/tests/unit/load_balancer/test_pool.py @@ -35,6 +35,7 @@ 'health_monitor': 'healthmonitor', 'health_monitor_id': uuid.uuid4(), 'members': [{'id': uuid.uuid4()}], + 'tls_enabled': True, 'tls_ciphers': 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256', 'tls_versions': ['TLSv1.1', 'TLSv1.2'], } @@ -83,6 +84,8 @@ def test_make_it(self): self.assertEqual(EXAMPLE['health_monitor_id'], test_pool.health_monitor_id) self.assertEqual(EXAMPLE['members'], test_pool.members) + self.assertEqual(EXAMPLE['tls_enabled'], + test_pool.tls_enabled) self.assertEqual(EXAMPLE['tls_ciphers'], test_pool.tls_ciphers) self.assertEqual(EXAMPLE['tls_versions'], @@ -109,6 +112,7 @@ def test_make_it(self): 'listener_id': 'listener_id', 'loadbalancer_id': 'loadbalancer_id', 'protocol': 'protocol', + 'tls_enabled': 'tls_enabled', 'tls_ciphers': 'tls_ciphers', 'tls_versions': 'tls_versions', }, diff --git a/releasenotes/notes/add-tls_enabled-parameter-for-octavia-pools-f0a23436d826b313.yaml b/releasenotes/notes/add-tls_enabled-parameter-for-octavia-pools-f0a23436d826b313.yaml new file mode 100644 index 000000000..e3cc8675a --- /dev/null +++ b/releasenotes/notes/add-tls_enabled-parameter-for-octavia-pools-f0a23436d826b313.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``tls_enabled`` parameter for Octavia pools, it can be used to enable + TLS communications between a load balancer and its member servers. From 4913b61713e39e7252c914c5fd476a71a9176106 Mon Sep 17 00:00:00 2001 From: Gregory Thiemonge Date: Mon, 23 Nov 2020 15:46:17 +0100 Subject: [PATCH 2745/3836] Fix exception parsing when using WSME Services that use WSME return errors using a json document that contains 'faultstring' in the root object instead of a dict within a dict. Change-Id: I25d8c64860de7eafb8d543ebcb519fc9664d563e Story: 2008381 Task: 41303 --- openstack/exceptions.py | 3 +++ openstack/tests/unit/test_exceptions.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/openstack/exceptions.py b/openstack/exceptions.py index f449e86dd..e2b1ca309 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -211,6 +211,9 @@ def raise_from_response(response, error_message=None): try: content = response.json() messages = [_extract_message(obj) for obj in content.values()] + if not any(messages): + # Exception dict may be the root dict in projects that use WSME + messages = [_extract_message(content)] # Join all of the messages together nicely and filter out any # objects that don't have a "message" attr. details = '\n'.join(msg for msg in messages if msg) diff --git a/openstack/tests/unit/test_exceptions.py b/openstack/tests/unit/test_exceptions.py index a3286719c..cf2d74736 100644 --- a/openstack/tests/unit/test_exceptions.py +++ b/openstack/tests/unit/test_exceptions.py @@ -213,3 +213,21 @@ def test_raise_baremetal_corrected_format(self): self.assertEqual(response.status_code, exc.status_code) self.assertEqual(self.message, exc.details) self.assertIn(self.message, str(exc)) + + def test_raise_wsme_format(self): + response = mock.Mock() + response.status_code = 404 + response.headers = { + 'content-type': 'application/json', + } + response.json.return_value = { + 'faultstring': self.message, + 'faultcode': 'Client', + 'debuginfo': None, + } + exc = self.assertRaises(exceptions.NotFoundException, + self._do_raise, response, + error_message=self.message) + self.assertEqual(response.status_code, exc.status_code) + self.assertEqual(self.message, exc.details) + self.assertIn(self.message, str(exc)) From cc7369c74317a65adb5334c63eb0cc6ce3796e38 Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Thu, 8 Oct 2020 10:22:10 +0200 Subject: [PATCH 2746/3836] Add support for Block Storage (v3) VolumeType Encyption resources Change-Id: I146bdf8e80d4383f61de4b4305bdaaa2e8c80a23 --- openstack/block_storage/v3/_proxy.py | 93 +++++++++++++++++++ openstack/block_storage/v3/type.py | 35 +++++++ .../tests/unit/block_storage/v3/test_proxy.py | 28 ++++++ .../block_storage/v3/test_type_encryption.py | 61 ++++++++++++ ...rage-type_encryption-121f8a222c822fb5.yaml | 3 + 5 files changed, 220 insertions(+) create mode 100644 openstack/tests/unit/block_storage/v3/test_type_encryption.py create mode 100644 releasenotes/notes/block_storage-type_encryption-121f8a222c822fb5.yaml diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 6314329a7..9b30d8b0c 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -175,6 +175,99 @@ def update_type(self, type, **attrs): """ return self._update(_type.Type, type, **attrs) + def get_type_encryption(self, volume_type_id): + """Get the encryption details of a volume type + + :param volume_type_id: The value can be the ID of a type or a + :class:`~openstack.volume.v3.type.Type` + instance. + + :returns: One :class:`~openstack.volume.v3.type.TypeEncryption` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + volume_type = self._get_resource(_type.Type, volume_type_id) + + return self._get(_type.TypeEncryption, + volume_type_id=volume_type.id, + requires_id=False) + + def create_type_encryption(self, volume_type, **attrs): + """Create new type encryption from attributes + + :param volume_type: The value can be the ID of a type or a + :class:`~openstack.volume.v3.type.Type` + instance. + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.volume.v3.type.TypeEncryption`, + comprised of the properties on the TypeEncryption + class. + + :returns: The results of type encryption creation + :rtype: :class:`~openstack.volume.v3.type.TypeEncryption` + """ + volume_type = self._get_resource(_type.Type, volume_type) + + return self._create(_type.TypeEncryption, + volume_type_id=volume_type.id, **attrs) + + def delete_type_encryption(self, encryption=None, + volume_type=None, ignore_missing=True): + """Delete type encryption attributes + + :param encryption: The value can be None or a + :class:`~openstack.volume.v3.type.TypeEncryption` + instance. If encryption_id is None then + volume_type_id must be specified. + + :param volume_type: The value can be the ID of a type or a + :class:`~openstack.volume.v3.type.Type` + instance. Required if encryption_id is None. + + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the type does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent type. + + :returns: ``None`` + """ + + if volume_type: + volume_type = self._get_resource(_type.Type, volume_type) + encryption = self._get(_type.TypeEncryption, + volume_type=volume_type.id, + requires_id=False) + + self._delete(_type.TypeEncryption, encryption, + ignore_missing=ignore_missing) + + def update_type_encryption(self, encryption=None, + volume_type=None, **attrs): + """Update a type + :param encryption: The value can be None or a + :class:`~openstack.volume.v3.type.TypeEncryption` + instance. If encryption_id is None then + volume_type_id must be specified. + + :param volume_type: The value can be the ID of a type or a + :class:`~openstack.volume.v3.type.Type` + instance. Required if encryption_id is None. + :param dict attrs: The attributes to update on the type encryption. + + :returns: The updated type encryption + :rtype: :class:`~openstack.volume.v3.type.TypeEncryption` + """ + + if volume_type: + volume_type = self._get_resource(_type.Type, volume_type) + encryption = self._get(_type.TypeEncryption, + volume_type=volume_type.id, + requires_id=False) + + return self._update(_type.TypeEncryption, encryption, **attrs) + def get_volume(self, volume): """Get a single volume diff --git a/openstack/block_storage/v3/type.py b/openstack/block_storage/v3/type.py index b8fe44f85..b3b9326c0 100644 --- a/openstack/block_storage/v3/type.py +++ b/openstack/block_storage/v3/type.py @@ -38,3 +38,38 @@ class Type(resource.Resource): extra_specs = resource.Body("extra_specs", type=dict) #: a private volume-type. *Type: bool* is_public = resource.Body('os-volume-type-access:is_public', type=bool) + + +class TypeEncryption(resource.Resource): + resource_key = "encryption" + resources_key = "encryption" + base_path = "/types/%(volume_type_id)s/encryption" + + # capabilities + allow_fetch = True + allow_create = True + allow_delete = True + allow_list = False + allow_commit = True + + # Properties + #: A ID representing this type. + encryption_id = resource.Body("encryption_id", alternate_id=True) + #: The ID of the Volume Type. + volume_type_id = resource.URI("volume_type_id") + #: The Size of encryption key. + key_size = resource.Body("key_size") + #: The class that provides encryption support. + provider = resource.Body("provider") + #: Notional service where encryption is performed. + control_location = resource.Body("control_location") + #: The encryption algorithm or mode. + cipher = resource.Body("cipher") + #: The resource is deleted or not. + deleted = resource.Body("deleted") + #: The date and time when the resource was created. + created_at = resource.Body("created_at") + #: The date and time when the resource was updated. + updated_at = resource.Body("updated_at") + #: The date and time when the resource was deleted. + deleted_at = resource.Body("deleted_at") diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index c3e9cbc53..e9ffdb1aa 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -75,6 +75,34 @@ def test_type_delete_ignore(self): def test_type_update(self): self.verify_update(self.proxy.update_type, type.Type) + def test_type_encryption_get(self): + self.verify_get(self.proxy.get_type_encryption, + type.TypeEncryption, + expected_args=[type.TypeEncryption], + expected_kwargs={ + 'volume_type_id': 'value', + 'requires_id': False + }) + + def test_type_encryption_create(self): + self.verify_create(self.proxy.create_type_encryption, + type.TypeEncryption, + method_kwargs={'volume_type': 'id'}, + expected_kwargs={'volume_type_id': 'id'} + ) + + def test_type_encryption_update(self): + self.verify_update(self.proxy.update_type_encryption, + type.TypeEncryption) + + def test_type_encryption_delete(self): + self.verify_delete(self.proxy.delete_type_encryption, + type.TypeEncryption, False) + + def test_type_encryption_delete_ignore(self): + self.verify_delete(self.proxy.delete_type_encryption, + type.TypeEncryption, True) + def test_volume_get(self): self.verify_get(self.proxy.get_volume, volume.Volume) diff --git a/openstack/tests/unit/block_storage/v3/test_type_encryption.py b/openstack/tests/unit/block_storage/v3/test_type_encryption.py new file mode 100644 index 000000000..98733ddb6 --- /dev/null +++ b/openstack/tests/unit/block_storage/v3/test_type_encryption.py @@ -0,0 +1,61 @@ +# 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.tests.unit import base + +from openstack.block_storage.v3 import type + +FAKE_ID = "479394ab-2f25-416e-8f58-721d8e5e29de" +TYPE_ID = "22373aed-c4a8-4072-b66c-bf0a90dc9a12" +TYPE_ENC = { + "key_size": 256, + "volume_type_id": TYPE_ID, + "encryption_id": FAKE_ID, + "provider": "nova.volume.encryptors.luks.LuksEncryptor", + "control_location": "front-end", + "cipher": "aes-xts-plain64", + "deleted": False, + "created_at": "2020-10-07T07:52:30.000000", + "updated_at": "2020-10-08T07:42:45.000000", + "deleted_at": None, +} + + +class TestTypeEncryption(base.TestCase): + + def test_basic(self): + sot = type.TypeEncryption(**TYPE_ENC) + self.assertEqual("encryption", sot.resource_key) + self.assertEqual("encryption", sot.resources_key) + self.assertEqual("/types/%(volume_type_id)s/encryption", sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_delete) + self.assertFalse(sot.allow_list) + self.assertTrue(sot.allow_commit) + + def test_new(self): + sot = type.TypeEncryption.new(encryption_id=FAKE_ID) + self.assertEqual(FAKE_ID, sot.encryption_id) + + def test_create(self): + sot = type.TypeEncryption(**TYPE_ENC) + self.assertEqual(TYPE_ENC["volume_type_id"], sot.volume_type_id) + self.assertEqual(TYPE_ENC["encryption_id"], sot.encryption_id) + self.assertEqual(TYPE_ENC["key_size"], sot.key_size) + self.assertEqual(TYPE_ENC["provider"], sot.provider) + self.assertEqual(TYPE_ENC["control_location"], sot.control_location) + self.assertEqual(TYPE_ENC["cipher"], sot.cipher) + self.assertEqual(TYPE_ENC["deleted"], sot.deleted) + self.assertEqual(TYPE_ENC["created_at"], sot.created_at) + self.assertEqual(TYPE_ENC["updated_at"], sot.updated_at) + self.assertEqual(TYPE_ENC["deleted_at"], sot.deleted_at) diff --git a/releasenotes/notes/block_storage-type_encryption-121f8a222c822fb5.yaml b/releasenotes/notes/block_storage-type_encryption-121f8a222c822fb5.yaml new file mode 100644 index 000000000..deed2290d --- /dev/null +++ b/releasenotes/notes/block_storage-type_encryption-121f8a222c822fb5.yaml @@ -0,0 +1,3 @@ +--- +features: + - Add support for block storage type encryption parameters. From e21b017770bc2959866327ca5e389ff60eb184b9 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 4 Dec 2020 15:55:52 +0100 Subject: [PATCH 2747/3836] Add support for overriding list base_path in find function In case of some resources we might want to override base_path for list function invoked by find (find resource using /details path). Change-Id: Ie2387c2f262b9a6696358e2874399a095da1ba5e --- openstack/resource.py | 25 ++++++++++++++++--------- openstack/tests/unit/test_resource.py | 18 +++++++++++++++++- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/openstack/resource.py b/openstack/resource.py index 5010a92fb..185b13dde 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1863,22 +1863,26 @@ def _get_one_match(cls, name_or_id, results): return the_result @classmethod - def find(cls, session, name_or_id, ignore_missing=True, **params): + def find( + cls, session, name_or_id, ignore_missing=True, + list_base_path=None, **params + ): """Find a resource by its name or id. :param session: The session to use for making this request. :type session: :class:`~keystoneauth1.adapter.Adapter` :param name_or_id: This resource's identifier, if needed by - the request. The default is ``None``. + the request. The default is ``None``. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the resource does not exist. When set to ``True``, None will be + returned when attempting to find a nonexistent resource. + :param str list_base_path: base_path to be used when need listing + resources. :param dict params: Any additional parameters to be passed into - underlying methods, such as to - :meth:`~openstack.resource.Resource.existing` - in order to pass on URI parameters. + underlying methods, such as to + :meth:`~openstack.resource.Resource.existing` in order to pass on + URI parameters. :return: The :class:`Resource` object matching the given name or id or None if nothing matches. @@ -1900,6 +1904,9 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): # 400 if we try to GET resource and it doesn't exist. pass + if list_base_path: + params['base_path'] = list_base_path + if ('name' in cls._query_mapping._mapping.keys() and 'name' not in params): params['name'] = name_or_id diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 6018434fe..7c9f2a442 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -2668,7 +2668,7 @@ def existing(cls, **kwargs): @classmethod def list(cls, session, **params): - return None + return [] class OneResult(Base): @@ -2789,6 +2789,22 @@ def test_multiple_matches(self): exceptions.DuplicateResource, resource.Resource._get_one_match, the_id, [match, match]) + def test_list_no_base_path(self): + + with mock.patch.object(self.Base, "list") as list_mock: + self.Base.find(self.cloud.compute, "name") + + list_mock.assert_called_with(self.cloud.compute) + + def test_list_base_path(self): + + with mock.patch.object(self.Base, "list") as list_mock: + self.Base.find( + self.cloud.compute, "name", list_base_path='/dummy/list') + + list_mock.assert_called_with( + self.cloud.compute, base_path='/dummy/list') + class TestWaitForStatus(base.TestCase): From 6a3d6e04b3d538fafd2837e144c3692e62e0cb39 Mon Sep 17 00:00:00 2001 From: Hang Yang Date: Thu, 1 Oct 2020 15:15:07 -0500 Subject: [PATCH 2748/3836] Support remote address group in SG rules Add support of using remote_address_group_id in Neutron security group rules. Depends-On: https://review.opendev.org/751110 Change-Id: I50374c339ab7685a6e74f25f9521b8810c532e13 Implements: blueprint address-groups-in-sg-rules --- openstack/cloud/_security_group.py | 5 +++++ openstack/network/v2/security_group_rule.py | 14 ++++++++++---- openstack/tests/unit/cloud/test_security_groups.py | 2 ++ .../unit/network/v2/test_security_group_rule.py | 7 ++++++- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/openstack/cloud/_security_group.py b/openstack/cloud/_security_group.py index 1e31e659b..c5ccb369e 100644 --- a/openstack/cloud/_security_group.py +++ b/openstack/cloud/_security_group.py @@ -238,6 +238,7 @@ def create_security_group_rule(self, protocol=None, remote_ip_prefix=None, remote_group_id=None, + remote_address_group_id=None, direction='ingress', ethertype='IPv4', project_id=None): @@ -269,6 +270,9 @@ def create_security_group_rule(self, :param string remote_group_id: The remote group ID to be associated with this security group rule. + :param string remote_address_group_id: + The remote address group ID to be associated with this security + group rule. :param string direction: Ingress or egress: The direction in which the security group rule is applied. For a compute instance, an ingress security @@ -309,6 +313,7 @@ def create_security_group_rule(self, 'protocol': protocol, 'remote_ip_prefix': remote_ip_prefix, 'remote_group_id': remote_group_id, + 'remote_address_group_id': remote_address_group_id, 'direction': direction, 'ethertype': ethertype } diff --git a/openstack/network/v2/security_group_rule.py b/openstack/network/v2/security_group_rule.py index 398366e5c..06de5b3b3 100644 --- a/openstack/network/v2/security_group_rule.py +++ b/openstack/network/v2/security_group_rule.py @@ -29,6 +29,7 @@ class SecurityGroupRule(_base.NetworkResource, resource.TagMixin): _query_mapping = resource.QueryParameters( 'description', 'direction', 'protocol', 'remote_group_id', 'security_group_id', + 'remote_address_group_id', 'port_range_max', 'port_range_min', 'remote_ip_prefix', 'revision_number', 'project_id', 'tenant_id', @@ -68,12 +69,17 @@ class SecurityGroupRule(_base.NetworkResource, resource.TagMixin): protocol = resource.Body('protocol') #: The remote security group ID to be associated with this security #: group rule. You can specify either ``remote_group_id`` or - #: ``remote_ip_prefix`` in the request body. + #: ``remote_address_group_id`` or ``remote_ip_prefix`` in the request body. remote_group_id = resource.Body('remote_group_id') + #: The remote address group ID to be associated with this security + #: group rule. You can specify either ``remote_group_id`` or + #: ``remote_address_group_id`` or ``remote_ip_prefix`` in the request body. + remote_address_group_id = resource.Body('remote_address_group_id') #: The remote IP prefix to be associated with this security group rule. - #: You can specify either ``remote_group_id`` or ``remote_ip_prefix`` - #: in the request body. This attribute matches the specified IP prefix - #: as the source IP address of the IP packet. + #: You can specify either ``remote_group_id`` or + # ``remote_address_group_id``or ``remote_ip_prefix`` in the request body. + # This attribute matches the specified IP prefix as the source IP address + # of the IP packet. remote_ip_prefix = resource.Body('remote_ip_prefix') #: The security group ID to associate with this security group rule. security_group_id = resource.Body('security_group_id') diff --git a/openstack/tests/unit/cloud/test_security_groups.py b/openstack/tests/unit/cloud/test_security_groups.py index 4ff5875af..70010b3f6 100644 --- a/openstack/tests/unit/cloud/test_security_groups.py +++ b/openstack/tests/unit/cloud/test_security_groups.py @@ -347,6 +347,7 @@ def test_create_security_group_rule_neutron(self): protocol='tcp', remote_ip_prefix='0.0.0.0/0', remote_group_id='456', + remote_address_group_id=None, direction='egress', ethertype='IPv6' ) @@ -398,6 +399,7 @@ def test_create_security_group_rule_neutron_specific_tenant(self): protocol='tcp', remote_ip_prefix='0.0.0.0/0', remote_group_id='456', + remote_address_group_id=None, direction='egress', ethertype='IPv6', project_id='861808a93da0484ea1767967c4df8a23' diff --git a/openstack/tests/unit/network/v2/test_security_group_rule.py b/openstack/tests/unit/network/v2/test_security_group_rule.py index 981c81227..660883e58 100644 --- a/openstack/tests/unit/network/v2/test_security_group_rule.py +++ b/openstack/tests/unit/network/v2/test_security_group_rule.py @@ -30,7 +30,8 @@ 'security_group_id': '10', 'tenant_id': '11', 'project_id': '11', - 'updated_at': '12' + 'updated_at': '12', + 'remote_address_group_id': '13' } @@ -60,6 +61,8 @@ def test_basic(self): 'project_id': 'project_id', 'protocol': 'protocol', 'remote_group_id': 'remote_group_id', + 'remote_address_group_id': + 'remote_address_group_id', 'remote_ip_prefix': 'remote_ip_prefix', 'revision_number': 'revision_number', 'security_group_id': 'security_group_id', @@ -81,6 +84,8 @@ def test_make_it(self): self.assertEqual(EXAMPLE['port_range_min'], sot.port_range_min) self.assertEqual(EXAMPLE['protocol'], sot.protocol) self.assertEqual(EXAMPLE['remote_group_id'], sot.remote_group_id) + self.assertEqual(EXAMPLE['remote_address_group_id'], + sot.remote_address_group_id) self.assertEqual(EXAMPLE['remote_ip_prefix'], sot.remote_ip_prefix) self.assertEqual(EXAMPLE['revision_number'], sot.revision_number) self.assertEqual(EXAMPLE['security_group_id'], sot.security_group_id) From 6c7bdc617b8ea0d4e99d9bacb378b5316f454bb8 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Sat, 5 Dec 2020 16:29:34 +0100 Subject: [PATCH 2749/3836] Add new Open Telekom Cloud region into the profile Open Telekom Cloud soon launches new region. Change-Id: I90a05ea79600523cbaa3243e73e5bba6a0cc20a4 --- openstack/config/vendors/otc.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openstack/config/vendors/otc.json b/openstack/config/vendors/otc.json index c289de8aa..1a860ada4 100644 --- a/openstack/config/vendors/otc.json +++ b/openstack/config/vendors/otc.json @@ -5,7 +5,8 @@ "auth_url": "https://iam.{region_name}.otc.t-systems.com/v3" }, "regions": [ - "eu-de" + "eu-de", + "eu-nl" ], "identity_api_version": "3", "interface": "public", From 213ff35355a9de30aa15cebfa2b724b2f6a67dca Mon Sep 17 00:00:00 2001 From: Anton Sidelnikov Date: Fri, 4 Dec 2020 18:47:00 +0000 Subject: [PATCH 2750/3836] Drop swift check for volume backup Skipped object-store check, because Cinder also supporting other drivers not related to swift Change-Id: I7349b0bf37b3d296f59f4ee3547dd98a142e1ec7 --- openstack/block_storage/v2/_proxy.py | 21 ------ openstack/block_storage/v3/_proxy.py | 67 ++++++------------- .../tests/unit/block_storage/v2/test_proxy.py | 14 ---- .../tests/unit/block_storage/v3/test_proxy.py | 14 ---- 4 files changed, 20 insertions(+), 96 deletions(-) diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 653366d2a..fb2eb0ded 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -16,7 +16,6 @@ from openstack.block_storage.v2 import stats as _stats from openstack.block_storage.v2 import type as _type from openstack.block_storage.v2 import volume as _volume -from openstack import exceptions from openstack import resource @@ -231,10 +230,6 @@ def backups(self, details=True, **query): :returns: A generator of backup objects. """ - if not self._connection.has_service('object-store'): - raise exceptions.SDKException( - 'Object-store service is required for block-store backups' - ) base_path = '/backups/detail' if details else None return self._list(_backup.Backup, base_path=base_path, **query) @@ -248,10 +243,6 @@ def get_backup(self, backup): :returns: Backup instance :rtype: :class:`~openstack.block_storage.v2.backup.Backup` """ - if not self._connection.has_service('object-store'): - raise exceptions.SDKException( - 'Object-store service is required for block-store backups' - ) return self._get(_backup.Backup, backup) def create_backup(self, **attrs): @@ -264,10 +255,6 @@ def create_backup(self, **attrs): :returns: The results of Backup creation :rtype: :class:`~openstack.block_storage.v2.backup.Backup` """ - if not self._connection.has_service('object-store'): - raise exceptions.SDKException( - 'Object-store service is required for block-store backups' - ) return self._create(_backup.Backup, **attrs) def delete_backup(self, backup, ignore_missing=True): @@ -283,10 +270,6 @@ def delete_backup(self, backup, ignore_missing=True): :returns: ``None`` """ - if not self._connection.has_service('object-store'): - raise exceptions.SDKException( - 'Object-store service is required for block-store backups' - ) self._delete(_backup.Backup, backup, ignore_missing=ignore_missing) @@ -301,10 +284,6 @@ def restore_backup(self, backup, volume_id, name): :returns: Updated backup instance :rtype: :class:`~openstack.block_storage.v2.backup.Backup` """ - if not self._connection.has_service('object-store'): - raise exceptions.SDKException( - 'Object-store service is required for block-store backups' - ) backup = self._get_resource(_backup.Backup, backup) return backup.restore(self, volume_id=volume_id, name=name) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 6314329a7..5714bcbdb 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -290,10 +290,6 @@ def backups(self, details=True, **query): :returns: A generator of backup objects. """ - if not self._connection.has_service('object-store'): - raise exceptions.SDKException( - 'Object-store service is required for block-store backups' - ) base_path = '/backups/detail' if details else None return self._list(_backup.Backup, base_path=base_path, **query) @@ -307,10 +303,6 @@ def get_backup(self, backup): :returns: Backup instance :rtype: :class:`~openstack.block_storage.v3.backup.Backup` """ - if not self._connection.has_service('object-store'): - raise exceptions.SDKException( - 'Object-store service is required for block-store backups' - ) return self._get(_backup.Backup, backup) def find_backup(self, name_or_id, ignore_missing=True, **attrs): @@ -325,10 +317,6 @@ def find_backup(self, name_or_id, ignore_missing=True, **attrs): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - if not self._connection.has_service('object-store'): - raise exceptions.SDKException( - 'Object-store service is required for block-store backups' - ) return self._find(_backup.Backup, name_or_id, ignore_missing=ignore_missing) @@ -342,10 +330,6 @@ def create_backup(self, **attrs): :returns: The results of Backup creation :rtype: :class:`~openstack.block_storage.v3.backup.Backup` """ - if not self._connection.has_service('object-store'): - raise exceptions.SDKException( - 'Object-store service is required for block-store backups' - ) return self._create(_backup.Backup, **attrs) def delete_backup(self, backup, ignore_missing=True): @@ -361,10 +345,6 @@ def delete_backup(self, backup, ignore_missing=True): :returns: ``None`` """ - if not self._connection.has_service('object-store'): - raise exceptions.SDKException( - 'Object-store service is required for block-store backups' - ) self._delete(_backup.Backup, backup, ignore_missing=ignore_missing) @@ -379,10 +359,6 @@ def restore_backup(self, backup, volume_id=None, name=None): :returns: Updated backup instance :rtype: :class:`~openstack.block_storage.v3.backup.Backup` """ - if not self._connection.has_service('object-store'): - raise exceptions.SDKException( - 'Object-store service is required for block-store backups' - ) backup = self._get_resource(_backup.Backup, backup) return backup.restore(self, volume_id=volume_id, name=name) @@ -437,29 +413,26 @@ def _get_cleanup_dependencies(self): def _service_cleanup(self, dry_run=True, client_status_queue=None, identified_resources=None, filters=None, resource_evaluation_fn=None): - if self._connection.has_service('object-store'): - # Volume backups require object-store to be available, even for - # listing - backups = [] - for obj in self.backups(details=False): - need_delete = self._service_cleanup_del_res( - self.delete_backup, - obj, - dry_run=dry_run, - client_status_queue=client_status_queue, - identified_resources=identified_resources, - filters=filters, - resource_evaluation_fn=resource_evaluation_fn) - if not dry_run and need_delete: - backups.append(obj) - - # Before deleting snapshots need to wait for backups to be deleted - for obj in backups: - try: - self.wait_for_delete(obj) - except exceptions.SDKException: - # Well, did our best, still try further - pass + backups = [] + for obj in self.backups(details=False): + need_delete = self._service_cleanup_del_res( + self.delete_backup, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn) + if not dry_run and need_delete: + backups.append(obj) + + # Before deleting snapshots need to wait for backups to be deleted + for obj in backups: + try: + self.wait_for_delete(obj) + except exceptions.SDKException: + # Well, did our best, still try further + pass snapshots = [] for obj in self.snapshots(details=False): diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index 2c6a1aa6e..8f89a6d27 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -17,7 +17,6 @@ from openstack.block_storage.v2 import stats from openstack.block_storage.v2 import type from openstack.block_storage.v2 import volume -from openstack import exceptions from openstack.tests.unit import test_proxy_base @@ -150,16 +149,3 @@ def test_backup_restore(self): expected_args=[self.proxy], expected_kwargs={'volume_id': 'vol_id', 'name': 'name'} ) - - def test_backup_no_swift(self): - """Ensure proxy method raises exception if swift is not available - """ - # NOTE: mock has_service - self.proxy._connection = mock.Mock() - self.proxy._connection.has_service = mock.Mock(return_value=False) - self.assertRaises( - exceptions.SDKException, - self.proxy.restore_backup, - 'backup', - 'volume_id', - 'name') diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index c3e9cbc53..b690371b2 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -17,7 +17,6 @@ from openstack.block_storage.v3 import stats from openstack.block_storage.v3 import type from openstack.block_storage.v3 import volume -from openstack import exceptions from openstack.tests.unit import test_proxy_base @@ -169,16 +168,3 @@ def test_backup_restore(self): expected_args=[self.proxy], expected_kwargs={'volume_id': 'vol_id', 'name': 'name'} ) - - def test_backup_no_swift(self): - """Ensure proxy method raises exception if swift is not available - """ - # NOTE: mock has_service - self.proxy._connection = mock.Mock() - self.proxy._connection.has_service = mock.Mock(return_value=False) - self.assertRaises( - exceptions.SDKException, - self.proxy.restore_backup, - 'backup', - 'volume_id', - 'name') From b86edc3683ea73e0ecbe3d7d2c1495a1842d836c Mon Sep 17 00:00:00 2001 From: tischrei Date: Tue, 8 Dec 2020 11:46:50 +0000 Subject: [PATCH 2751/3836] Add id query parameter to sg rules id paramter is needed for filter functionality in Ansible modules. See PR https://review.opendev.org/c/openstack/ansible-collections-openstack/+/765580 Change-Id: If08fa34672ff32dda139b0b50cc7c4af89107846 --- openstack/network/v2/security_group_rule.py | 2 +- openstack/tests/unit/network/v2/test_security_group_rule.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/openstack/network/v2/security_group_rule.py b/openstack/network/v2/security_group_rule.py index 398366e5c..35092ec9f 100644 --- a/openstack/network/v2/security_group_rule.py +++ b/openstack/network/v2/security_group_rule.py @@ -27,7 +27,7 @@ class SecurityGroupRule(_base.NetworkResource, resource.TagMixin): allow_list = True _query_mapping = resource.QueryParameters( - 'description', 'direction', 'protocol', + 'description', 'direction', 'id', 'protocol', 'remote_group_id', 'security_group_id', 'port_range_max', 'port_range_min', 'remote_ip_prefix', 'revision_number', diff --git a/openstack/tests/unit/network/v2/test_security_group_rule.py b/openstack/tests/unit/network/v2/test_security_group_rule.py index 981c81227..bb3450217 100644 --- a/openstack/tests/unit/network/v2/test_security_group_rule.py +++ b/openstack/tests/unit/network/v2/test_security_group_rule.py @@ -50,6 +50,7 @@ def test_basic(self): self.assertDictEqual({'any_tags': 'tags-any', 'description': 'description', 'direction': 'direction', + 'id': 'id', 'ether_type': 'ethertype', 'limit': 'limit', 'marker': 'marker', From b4813fb83cd7ae547dd7bd5f706af54bd8308ee3 Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Mon, 12 Oct 2020 13:50:53 +0200 Subject: [PATCH 2752/3836] Add support for updating Block Storage Volume type extra_spec attributes While the volume_types/ endpoint returns extra_specs and accepts it on creation it doesn't support updating them through volume_types. It has to be updated through the volume_types/extra_specs endpoint. Change-Id: I5f9d5bcb102c5fd9fe60eb03e218c66f9c49592c --- openstack/block_storage/v3/_proxy.py | 30 ++++++++ openstack/block_storage/v3/type.py | 55 ++++++++++++++ .../tests/unit/block_storage/v3/test_proxy.py | 22 ++++++ .../tests/unit/block_storage/v3/test_type.py | 76 +++++++++++++++++++ ...d-volume-type-update-b84f50b7fa3b061d.yaml | 3 + 5 files changed, 186 insertions(+) create mode 100644 releasenotes/notes/add-volume-type-update-b84f50b7fa3b061d.yaml diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 9b30d8b0c..15aaf8ce8 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -175,6 +175,36 @@ def update_type(self, type, **attrs): """ return self._update(_type.Type, type, **attrs) + def update_type_extra_specs(self, type, **attrs): + """Update the extra_specs for a type + + :param type: The value can be either the ID of a type or a + :class:`~openstack.volume.v3.type.Type` instance. + :param dict attrs: The extra_spec attributes to update on the + type represented by ``value``. + + :returns: A dict containing updated extra_specs + + """ + res = self._get_resource(_type.Type, type) + extra_specs = res.set_extra_specs(self, **attrs) + result = _type.Type.existing(id=res.id, extra_specs=extra_specs) + return result + + def delete_type_extra_specs(self, type, keys): + """Delete the extra_specs for a type + + Note: This method will do a HTTP DELETE request for every key in keys. + + :param type: The value can be either the ID of a type or a + :class:`~openstack.volume.v3.type.Type` instance. + :param keys: The keys to delete + + :returns: ``None`` + """ + res = self._get_resource(_type.Type, type) + return res.delete_extra_specs(self, keys) + def get_type_encryption(self, volume_type_id): """Get the encryption details of a volume type diff --git a/openstack/block_storage/v3/type.py b/openstack/block_storage/v3/type.py index b3b9326c0..42fe16afc 100644 --- a/openstack/block_storage/v3/type.py +++ b/openstack/block_storage/v3/type.py @@ -10,7 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import exceptions from openstack import resource +from openstack import utils class Type(resource.Resource): @@ -39,6 +41,59 @@ class Type(resource.Resource): #: a private volume-type. *Type: bool* is_public = resource.Body('os-volume-type-access:is_public', type=bool) + def _extra_specs(self, method, key=None, delete=False, + extra_specs=None): + extra_specs = extra_specs or {} + for k, v in extra_specs.items(): + if not isinstance(v, str): + raise ValueError("The value for %s (%s) must be " + "a text string" % (k, v)) + + if key is not None: + url = utils.urljoin(self.base_path, self.id, "extra_specs", key) + else: + url = utils.urljoin(self.base_path, self.id, "extra_specs") + + kwargs = {} + if extra_specs: + kwargs["json"] = {"extra_specs": extra_specs} + + response = method(url, headers={}, **kwargs) + + # ensure Cinder API has not returned us an error + exceptions.raise_from_response(response) + # DELETE doesn't return a JSON body while everything else does. + return response.json() if not delete else None + + def set_extra_specs(self, session, **extra_specs): + """Update extra_specs + + This call will replace only the extra_specs with the same keys + given here. Other keys will not be modified. + + :param session: The session to use for this request. + :param kwargs extra_specs: key/value extra_specs pairs to be update on + this volume type. All keys and values + """ + if not extra_specs: + return dict() + + result = self._extra_specs(session.post, extra_specs=extra_specs) + return result["extra_specs"] + + def delete_extra_specs(self, session, keys): + """Delete extra_specs + + Note: This method will do a HTTP DELETE request for every key in keys. + + :param session: The session to use for this request. + :param list keys: The keys to delete. + + :rtype: ``None`` + """ + for key in keys: + self._extra_specs(session.delete, key=key, delete=True) + class TypeEncryption(resource.Resource): resource_key = "encryption" diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index e9ffdb1aa..0b72231a6 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -75,6 +75,28 @@ def test_type_delete_ignore(self): def test_type_update(self): self.verify_update(self.proxy.update_type, type.Type) + def test_type_extra_specs_update(self): + kwargs = {"a": "1", "b": "2"} + id = "an_id" + self._verify2( + "openstack.block_storage.v3.type.Type.set_extra_specs", + self.proxy.update_type_extra_specs, + method_args=[id], + method_kwargs=kwargs, + method_result=type.Type.existing(id=id, + extra_specs=kwargs), + expected_args=[self.proxy], + expected_kwargs=kwargs, + expected_result=kwargs) + + def test_type_extra_specs_delete(self): + self._verify2( + "openstack.block_storage.v3.type.Type.delete_extra_specs", + self.proxy.delete_type_extra_specs, + expected_result=None, + method_args=["value", "key"], + expected_args=[self.proxy, "key"]) + def test_type_encryption_get(self): self.verify_get(self.proxy.get_type_encryption, type.TypeEncryption, diff --git a/openstack/tests/unit/block_storage/v3/test_type.py b/openstack/tests/unit/block_storage/v3/test_type.py index 5fcd8380e..e92fb6726 100644 --- a/openstack/tests/unit/block_storage/v3/test_type.py +++ b/openstack/tests/unit/block_storage/v3/test_type.py @@ -10,8 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +from unittest import mock + from openstack.tests.unit import base +from openstack import exceptions from openstack.block_storage.v3 import type FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff" @@ -27,6 +30,10 @@ class TestType(base.TestCase): + def setUp(self): + super(TestType, self).setUp() + self.extra_specs_result = {"extra_specs": {"go": "cubs", "boo": "sox"}} + def test_basic(self): sot = type.Type(**TYPE) self.assertEqual("volume_type", sot.resource_key) @@ -48,3 +55,72 @@ def test_create(self): self.assertEqual(TYPE["extra_specs"], sot.extra_specs) self.assertEqual(TYPE["name"], sot.name) self.assertEqual(TYPE["description"], sot.description) + + def test_set_extra_specs(self): + response = mock.Mock() + response.status_code = 200 + response.json.return_value = self.extra_specs_result + sess = mock.Mock() + sess.post.return_value = response + + sot = type.Type(id=FAKE_ID) + + set_specs = {"lol": "rofl"} + + result = sot.set_extra_specs(sess, **set_specs) + + self.assertEqual(result, self.extra_specs_result["extra_specs"]) + sess.post.assert_called_once_with("types/" + FAKE_ID + "/extra_specs", + headers={}, + json={"extra_specs": set_specs}) + + def test_set_extra_specs_error(self): + sess = mock.Mock() + response = mock.Mock() + response.status_code = 400 + response.content = None + sess.post.return_value = response + + sot = type.Type(id=FAKE_ID) + + set_specs = {"lol": "rofl"} + + self.assertRaises( + exceptions.BadRequestException, + sot.set_extra_specs, + sess, + **set_specs) + + def test_delete_extra_specs(self): + sess = mock.Mock() + response = mock.Mock() + response.status_code = 200 + sess.delete.return_value = response + + sot = type.Type(id=FAKE_ID) + + key = "hey" + + sot.delete_extra_specs(sess, [key]) + + sess.delete.assert_called_once_with( + "types/" + FAKE_ID + "/extra_specs/" + key, + headers={}, + ) + + def test_delete_extra_specs_error(self): + sess = mock.Mock() + response = mock.Mock() + response.status_code = 400 + response.content = None + sess.delete.return_value = response + + sot = type.Type(id=FAKE_ID) + + key = "hey" + + self.assertRaises( + exceptions.BadRequestException, + sot.delete_extra_specs, + sess, + [key]) diff --git a/releasenotes/notes/add-volume-type-update-b84f50b7fa3b061d.yaml b/releasenotes/notes/add-volume-type-update-b84f50b7fa3b061d.yaml new file mode 100644 index 000000000..3e9f3630a --- /dev/null +++ b/releasenotes/notes/add-volume-type-update-b84f50b7fa3b061d.yaml @@ -0,0 +1,3 @@ +--- +features: + - Add support for updating block storage volume type objects. From ffd0f120739dc015a578751c755be8da3f54453e Mon Sep 17 00:00:00 2001 From: zhufl Date: Mon, 7 Dec 2020 14:43:07 +0800 Subject: [PATCH 2753/3836] Fix invalid argument formatting in log messages This is to fix the invalid argument formatting which will display as "Received retryable error {err}, waiting {wait} seconds to retry". Change-Id: Ie99558930f5ce6602dd646f99ad347c4e98e0987 --- openstack/cloud/_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index bb82a165b..e19870986 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -535,8 +535,8 @@ def _call_client_and_retry(client, url, retry_on=None, except exc.OpenStackCloudHTTPError as e: if (retry_on is not None and e.response.status_code in retry_on): - log.debug('Received retryable error {err}, waiting ' - '{wait} seconds to retry', { + log.debug('Received retryable error %(err)s, waiting ' + '%(wait)s seconds to retry', { 'err': e.response.status_code, 'wait': retry_wait }) From 97ccea4683bfd607393b00f9a0cb83620f69b363 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 15 Dec 2020 13:36:39 +0100 Subject: [PATCH 2754/3836] Change nodepool job to build CentOS-8-stream (unblock gate) Due to the recent renaming of the CentOS packages the job is currently failing and requires fix in the diskimage-builder to be merged and released. In the long run CentOS-8 as a standalone release is abandoned, therefore verify building of the stream image instead. Related-Bug: https://review.opendev.org/c/openstack/diskimage-builder/+/765963 Change-Id: I2aca89ce8bd27e920b0f80a66d6ed22d2277d05a --- .zuul.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 734f69cd0..9e772f49e 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -388,7 +388,7 @@ jobs: - opendev-buildset-registry - nodepool-build-image-siblings - - nodepool-functional-container-openstack-siblings + - dib-nodepool-functional-openstack-centos-8-stream-src - openstacksdk-functional-devstack - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-senlin @@ -417,7 +417,7 @@ jobs: - opendev-buildset-registry - nodepool-build-image-siblings - - nodepool-functional-container-openstack-siblings + - dib-nodepool-functional-openstack-centos-8-stream-src - openstacksdk-functional-devstack - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-senlin From 8279de9b631dd3edd1bc63ef09170b4f191c587f Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Thu, 17 Dec 2020 16:59:20 +0000 Subject: [PATCH 2755/3836] Add ``device_profile`` attribute to ``port`` Related-Bug: #1906602 Change-Id: I89e3670c829e42d9a0d19558db12459584df1a37 --- openstack/network/v2/port.py | 4 ++++ openstack/tests/unit/network/v2/test_port.py | 2 ++ releasenotes/notes/port-device-profile-af91e25c45321691.yaml | 5 +++++ 3 files changed, 11 insertions(+) create mode 100644 releasenotes/notes/port-device-profile-af91e25c45321691.yaml diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index 04bf6b794..a5a25ec8c 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -78,6 +78,10 @@ class Port(_base.NetworkResource, resource.TagMixin): device_id = resource.Body('device_id') #: Device owner of this port (e.g. ``network:dhcp``). device_owner = resource.Body('device_owner') + #: Device profile of this port, refers to Cyborg device-profiles: + # https://docs.openstack.org/api-ref/accelerator/v2/index.html# + # device-profiles. + device_profile = resource.Body('device_profile') #: DNS assignment for the port. dns_assignment = resource.Body('dns_assignment') #: DNS domain assigned to the port. diff --git a/openstack/tests/unit/network/v2/test_port.py b/openstack/tests/unit/network/v2/test_port.py index 4c5b0bb70..9c37fe25d 100644 --- a/openstack/tests/unit/network/v2/test_port.py +++ b/openstack/tests/unit/network/v2/test_port.py @@ -28,6 +28,7 @@ 'description': '8', 'device_id': '9', 'device_owner': '10', + 'device_profile': 'cyborg_device_profile_1', 'dns_assignment': [{'11': 11}], 'dns_domain': 'a11', 'dns_name': '12', @@ -122,6 +123,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['description'], sot.description) self.assertEqual(EXAMPLE['device_id'], sot.device_id) self.assertEqual(EXAMPLE['device_owner'], sot.device_owner) + self.assertEqual(EXAMPLE['device_profile'], sot.device_profile) self.assertEqual(EXAMPLE['dns_assignment'], sot.dns_assignment) self.assertEqual(EXAMPLE['dns_domain'], sot.dns_domain) self.assertEqual(EXAMPLE['dns_name'], sot.dns_name) diff --git a/releasenotes/notes/port-device-profile-af91e25c45321691.yaml b/releasenotes/notes/port-device-profile-af91e25c45321691.yaml new file mode 100644 index 000000000..e6abf2488 --- /dev/null +++ b/releasenotes/notes/port-device-profile-af91e25c45321691.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``device_profile`` attribute to ``port`` resource. This parameter + can be define during the port creation. This parameter is nullable string. From 78dd1a73d8ff063612560a9882b26bffa5f9e51a Mon Sep 17 00:00:00 2001 From: Thomas Bechtold Date: Tue, 22 Dec 2020 16:18:45 +0100 Subject: [PATCH 2756/3836] Support SNAP_REAL_HOME when using openstacksdk inside a snap package When the openstacksdk is used inside a snap package (eg. [1]), $HOME is re-written by snapd so that each snap appears to have a dedicated home directory that is a subdirectory of the real home directory [2]. But the clouds.yaml file is still in the real home directory. To be able to use that file, let openstacksdk look for $SNAP_REAL_HOME and if available, use that variable to construct the path for the clouds.yaml file. [1] https://opendev.org/x/snap-openstackclients [2] https://snapcraft.io/docs/environment-variables Change-Id: I570a98328188979f453bed98a2f8d6e35c569f19 --- openstack/config/loader.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 06307ad3a..79bc14741 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -40,8 +40,17 @@ CONFIG_HOME = APPDIRS.user_config_dir CACHE_PATH = APPDIRS.user_cache_dir -UNIX_CONFIG_HOME = os.path.join( - os.path.expanduser(os.path.join('~', '.config')), 'openstack') +# snaps do set $HOME to something like +# /home/$USER/snap/openstackclients/$SNAP_VERSION +# the real home (usually /home/$USERNAME) is stored in $SNAP_REAL_HOME +# see https://snapcraft.io/docs/environment-variables +SNAP_REAL_HOME = os.getenv('SNAP_REAL_HOME') +if SNAP_REAL_HOME: + UNIX_CONFIG_HOME = os.path.join(os.path.join(SNAP_REAL_HOME, '.config'), + 'openstack') +else: + UNIX_CONFIG_HOME = os.path.join( + os.path.expanduser(os.path.join('~', '.config')), 'openstack') UNIX_SITE_CONFIG_HOME = '/etc/openstack' SITE_CONFIG_HOME = APPDIRS.site_config_dir From d4c1cdc689d9368593207fda2b7116afb872f7c2 Mon Sep 17 00:00:00 2001 From: songwenping Date: Sat, 2 Jan 2021 09:09:40 +0800 Subject: [PATCH 2757/3836] Refresh deprecated link Change-Id: I19c7b3b5346c316f7fec0f403a6f071cb30a2b46 --- openstack/accelerator/v2/accelerator_request.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openstack/accelerator/v2/accelerator_request.py b/openstack/accelerator/v2/accelerator_request.py index b45bed453..1453544be 100644 --- a/openstack/accelerator/v2/accelerator_request.py +++ b/openstack/accelerator/v2/accelerator_request.py @@ -50,7 +50,7 @@ def _convert_patch(self, patch): # This overrides the default behavior of _convert_patch because # the PATCH method consumes JSON, its key is the ARQ uuid # and its value is an ordinary JSON patch. spec: - # https://specs.openstack.org/openstack/cyborg-specs/specs/train/approved/cyborg-api + # https://specs.openstack.org/openstack/cyborg-specs/specs/train/implemented/cyborg-api converted = super(AcceleratorRequest, self)._convert_patch(patch) converted = {self.id: converted} @@ -60,7 +60,7 @@ def patch(self, session, patch=None, prepend_key=True, has_body=True, retry_on_conflict=None, base_path=None): # This overrides the default behavior of patch because # the PATCH method consumes a dict rather than a list. spec: - # https://specs.openstack.org/openstack/cyborg-specs/specs/train/approved/cyborg-api + # https://specs.openstack.org/openstack/cyborg-specs/specs/train/implemented/cyborg-api # The id cannot be dirty for an commit self._body._dirty.discard("id") @@ -85,7 +85,7 @@ def patch(self, session, patch=None, prepend_key=True, has_body=True, def _consume_attrs(self, mapping, attrs): # This overrides the default behavior of _consume_attrs because # cyborg api returns an ARQ as list. spec: - # https://specs.openstack.org/openstack/cyborg-specs/specs/train/approved/cyborg-api + # https://specs.openstack.org/openstack/cyborg-specs/specs/train/implemented/cyborg-api if isinstance(self, AcceleratorRequest): if self.resources_key in attrs: attrs = attrs[self.resources_key][0] From 27536928130e3b4d2fe9ec16e860cf908402fb8f Mon Sep 17 00:00:00 2001 From: Ade Lee Date: Wed, 16 Dec 2020 14:59:33 -0500 Subject: [PATCH 2758/3836] encapsulate md5 calls for fips md5 is not an approved algorithm in FIPS mode, and trying to instantiate a hashlib.md5() will fail when the system is running in FIPS mode. md5 is allowed when in a non-security context. There is a plan to add a keyword parameter (usedforsecurity) to hashlib.md5() to annotate whether or not the instance is being used in a security context. In the case where it is not, the instantiation of md5 will be allowed. See https://bugs.python.org/issue9216 for more details. Some downstream python versions already support this parameter. To support these versions, a new encapsulation of md5() is added to openstack/utils.py. This encapsulation is identical to the one being added to oslo.utils, but is recreated here to avoid adding a dependency. This patch is to replace the instances of hashlib.md5() with this new encapsulation, adding an annotation indicating whether the usage is a security context or not. Change-Id: Ibb2cd80fd1f46975b9118c94e0e068d759754048 --- openstack/cloud/_object_store.py | 3 +- openstack/image/_download.py | 6 +- openstack/tests/fakes.py | 3 +- openstack/tests/unit/image/v2/test_image.py | 4 +- openstack/tests/unit/test_utils.py | 86 +++++++++++++++++++++ openstack/utils.py | 17 ++++ 6 files changed, 112 insertions(+), 7 deletions(-) diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index ed3f88c28..e291d0310 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -28,6 +28,7 @@ from openstack.cloud import _utils from openstack import exceptions from openstack import proxy +from openstack import utils DEFAULT_OBJECT_SEGMENT_SIZE = 1073741824 # 1GB @@ -218,7 +219,7 @@ def _get_file_hashes(self, filename): self._file_hash_cache[file_key]['sha256']) def _calculate_data_hashes(self, data): - md5 = hashlib.md5() + md5 = utils.md5(usedforsecurity=False) sha256 = hashlib.sha256() if hasattr(data, 'read'): diff --git a/openstack/image/_download.py b/openstack/image/_download.py index 21c6ffe77..a55cde66c 100644 --- a/openstack/image/_download.py +++ b/openstack/image/_download.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. import io -import hashlib from openstack import exceptions from openstack import utils @@ -45,7 +44,7 @@ def download(self, session, stream=False, output=None, chunk_size=1024): details = self.fetch(session) checksum = details.checksum - md5 = hashlib.md5() + md5 = utils.md5(usedforsecurity=False) if output: try: if isinstance(output, io.IOBase): @@ -73,7 +72,8 @@ def download(self, session, stream=False, output=None, chunk_size=1024): return resp if checksum is not None: - _verify_checksum(hashlib.md5(resp.content), checksum) + _verify_checksum(utils.md5(resp.content, usedforsecurity=False), + checksum) else: session.log.warning( "Unable to verify the integrity of image %s", (self.id)) diff --git a/openstack/tests/fakes.py b/openstack/tests/fakes.py index af1bbe235..e3cbbe621 100644 --- a/openstack/tests/fakes.py +++ b/openstack/tests/fakes.py @@ -24,6 +24,7 @@ from openstack.orchestration.util import template_format from openstack.cloud import meta +from openstack import utils PROJECT_ID = '1c36b64c840a42cd9e9b931a369337f0' FLAVOR_ID = u'0c1d9008-f546-4608-9e8f-f8bdaec8dddd' @@ -225,7 +226,7 @@ def make_fake_image( data=None, checksum=u'ee36e35a297980dee1b514de9803ec6d'): if data: - md5 = hashlib.md5() + md5 = utils.md5(usedforsecurity=False) sha256 = hashlib.sha256() with open(data, 'rb') as file_obj: for chunk in iter(lambda: file_obj.read(8192), b''): diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 91dc333d9..45932cc83 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -9,7 +9,6 @@ # 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 hashlib import io import operator import tempfile @@ -22,6 +21,7 @@ from openstack import exceptions from openstack.image.v2 import image from openstack.tests.unit import base +from openstack import utils IDENTIFIER = 'IDENTIFIER' EXAMPLE = { @@ -89,7 +89,7 @@ def calculate_md5_checksum(data): - checksum = hashlib.md5() + checksum = utils.md5(usedforsecurity=False) for chunk in data: checksum.update(chunk) return checksum.hexdigest() diff --git a/openstack/tests/unit/test_utils.py b/openstack/tests/unit/test_utils.py index 8aaf15531..a28931b7e 100644 --- a/openstack/tests/unit/test_utils.py +++ b/openstack/tests/unit/test_utils.py @@ -13,8 +13,10 @@ # under the License. import concurrent.futures +import hashlib import logging from unittest import mock +from unittest import skipIf import sys import fixtures @@ -302,3 +304,87 @@ def test_add_node_after_edge(self): def test_walker_fn(graph, node, lst): lst.append(node) graph.node_done(node) + + +class Test_md5(base.TestCase): + + def setUp(self): + super(Test_md5, self).setUp() + self.md5_test_data = "Openstack forever".encode('utf-8') + try: + self.md5_digest = hashlib.md5( # nosec + self.md5_test_data).hexdigest() + self.fips_enabled = False + except ValueError: + self.md5_digest = '0d6dc3c588ae71a04ce9a6beebbbba06' + self.fips_enabled = True + + def test_md5_with_data(self): + if not self.fips_enabled: + digest = utils.md5(self.md5_test_data).hexdigest() + self.assertEqual(digest, self.md5_digest) + else: + # on a FIPS enabled system, this throws a ValueError: + # [digital envelope routines: EVP_DigestInit_ex] disabled for FIPS + self.assertRaises(ValueError, utils.md5, self.md5_test_data) + if not self.fips_enabled: + digest = utils.md5(self.md5_test_data, + usedforsecurity=True).hexdigest() + self.assertEqual(digest, self.md5_digest) + else: + self.assertRaises( + ValueError, utils.md5, self.md5_test_data, + usedforsecurity=True) + digest = utils.md5(self.md5_test_data, + usedforsecurity=False).hexdigest() + self.assertEqual(digest, self.md5_digest) + + def test_md5_without_data(self): + if not self.fips_enabled: + test_md5 = utils.md5() + test_md5.update(self.md5_test_data) + digest = test_md5.hexdigest() + self.assertEqual(digest, self.md5_digest) + else: + self.assertRaises(ValueError, utils.md5) + if not self.fips_enabled: + test_md5 = utils.md5(usedforsecurity=True) + test_md5.update(self.md5_test_data) + digest = test_md5.hexdigest() + self.assertEqual(digest, self.md5_digest) + else: + self.assertRaises(ValueError, utils.md5, usedforsecurity=True) + test_md5 = utils.md5(usedforsecurity=False) + test_md5.update(self.md5_test_data) + digest = test_md5.hexdigest() + self.assertEqual(digest, self.md5_digest) + + @skipIf(sys.version_info.major == 2, + "hashlib.md5 does not raise TypeError here in py2") + def test_string_data_raises_type_error(self): + if not self.fips_enabled: + self.assertRaises(TypeError, hashlib.md5, u'foo') + self.assertRaises(TypeError, utils.md5, u'foo') + self.assertRaises( + TypeError, utils.md5, u'foo', usedforsecurity=True) + else: + self.assertRaises(ValueError, hashlib.md5, u'foo') + self.assertRaises(ValueError, utils.md5, u'foo') + self.assertRaises( + ValueError, utils.md5, u'foo', usedforsecurity=True) + self.assertRaises( + TypeError, utils.md5, u'foo', usedforsecurity=False) + + def test_none_data_raises_type_error(self): + if not self.fips_enabled: + self.assertRaises(TypeError, hashlib.md5, None) + self.assertRaises(TypeError, utils.md5, None) + self.assertRaises( + TypeError, utils.md5, None, usedforsecurity=True) + else: + self.assertRaises(ValueError, hashlib.md5, None) + self.assertRaises(ValueError, utils.md5, None) + self.assertRaises( + ValueError, utils.md5, None, usedforsecurity=True) + self.assertRaises( + TypeError, utils.md5, None, usedforsecurity=False) diff --git a/openstack/utils.py b/openstack/utils.py index b0358ade9..8adbc54b6 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import hashlib import queue import string import threading @@ -232,6 +233,22 @@ def maximum_supported_microversion(adapter, client_maximum): return discover.version_to_string(result) +try: + _test_md5 = hashlib.md5(usedforsecurity=False) # nosec + + # Python distributions that support a hashlib.md5 with the usedforsecurity + # keyword can just use that md5 definition as-is + # See https://bugs.python.org/issue9216 + md5 = hashlib.md5 +except TypeError: + def md5(string=b'', usedforsecurity=True): + """Return an md5 hashlib object without usedforsecurity parameter + For python distributions that do not yet support this keyword + parameter, we drop the parameter + """ + return hashlib.md5(string) # nosec + + class TinyDAG: """Tiny DAG From 658b5805be92892581fb0029107f7fc47af271e8 Mon Sep 17 00:00:00 2001 From: Sagi Shnaidman Date: Tue, 5 Jan 2021 16:17:49 +0200 Subject: [PATCH 2759/3836] Support roles 'name' in list_roles call Change-Id: Ia615288da30426c2f689daa3e5f88376aead1d3f --- openstack/cloud/_identity.py | 2 +- .../tests/unit/cloud/test_identity_roles.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index a44119119..4a9a9bfcd 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -1061,7 +1061,7 @@ def delete_group(self, name_or_id, **kwargs): self.list_groups.invalidate(self) return True - @_utils.valid_kwargs('domain_id') + @_utils.valid_kwargs('domain_id', 'name') def list_roles(self, **kwargs): """List Keystone roles. diff --git a/openstack/tests/unit/cloud/test_identity_roles.py b/openstack/tests/unit/cloud/test_identity_roles.py index 89eeddc1a..e8d503a4d 100644 --- a/openstack/tests/unit/cloud/test_identity_roles.py +++ b/openstack/tests/unit/cloud/test_identity_roles.py @@ -54,6 +54,22 @@ def test_list_roles(self): self.cloud.list_roles() self.assert_calls() + def test_list_role_by_name(self): + role_data = self._get_role_data() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + qs_elements=['name={0}'.format(role_data.role_name)]), + status_code=200, + json={'roles': [role_data.json_response['role']]}) + ]) + role = self.cloud.list_roles(name=role_data.role_name)[0] + + self.assertIsNotNone(role) + self.assertThat(role.id, matchers.Equals(role_data.role_id)) + self.assertThat(role.name, matchers.Equals(role_data.role_name)) + self.assert_calls() + def test_get_role_by_name(self): role_data = self._get_role_data() self.register_uris([ From 4b8f5ff008b67f8fbe1254bf946f611a7cdf77f6 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 15 Dec 2020 16:23:08 +0100 Subject: [PATCH 2760/3836] Modify cloud.get_aggregate to use proxy.find Seems that in some cases old cloud layer is not able to find aggregate properly causing it's deletion to fail. Use the proxy.find method instead of own listing (anyway a proper approach in the long run). Change-Id: I04831a3d25db81ac1a37b450faa0da165518a8c2 Required-By: https://review.opendev.org/c/openstack/devstack/+/766622 --- openstack/cloud/_compute.py | 5 +++- openstack/tests/unit/cloud/test_aggregate.py | 29 ++++++++++++-------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index aad122716..ecac1a794 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -1591,7 +1591,10 @@ def get_aggregate(self, name_or_id, filters=None): found. """ - return _utils._get_entity(self, 'aggregate', name_or_id, filters) + aggregate = self.compute.find_aggregate( + name_or_id, ignore_missing=True) + if aggregate: + return aggregate._to_munch() def create_aggregate(self, name, availability_zone=None): """Create a new host aggregate. diff --git a/openstack/tests/unit/cloud/test_aggregate.py b/openstack/tests/unit/cloud/test_aggregate.py index 704f44dab..8de62693c 100644 --- a/openstack/tests/unit/cloud/test_aggregate.py +++ b/openstack/tests/unit/cloud/test_aggregate.py @@ -84,10 +84,15 @@ def test_delete_aggregate_by_name(self): dict( method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['os-aggregates'] + 'compute', 'public', append=['os-aggregates', + self.aggregate_name] ), - json={'aggregates': [self.fake_aggregate]}, + status_code=404, ), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates']), + json={'aggregates': [self.fake_aggregate]}), dict(method='DELETE', uri=self.get_mock_url( 'compute', 'public', append=['os-aggregates', '1'])), @@ -101,8 +106,8 @@ def test_update_aggregate_set_az(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['os-aggregates']), - json={'aggregates': [self.fake_aggregate]}), + 'compute', 'public', append=['os-aggregates', '1']), + json=self.fake_aggregate), dict(method='PUT', uri=self.get_mock_url( 'compute', 'public', append=['os-aggregates', '1']), @@ -122,8 +127,8 @@ def test_update_aggregate_unset_az(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['os-aggregates']), - json={'aggregates': [self.fake_aggregate]}), + 'compute', 'public', append=['os-aggregates', '1']), + json=self.fake_aggregate), dict(method='PUT', uri=self.get_mock_url( 'compute', 'public', append=['os-aggregates', '1']), @@ -144,8 +149,8 @@ def test_set_aggregate_metadata(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['os-aggregates']), - json={'aggregates': [self.fake_aggregate]}), + 'compute', 'public', append=['os-aggregates', '1']), + json=self.fake_aggregate), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', @@ -163,8 +168,8 @@ def test_add_host_to_aggregate(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['os-aggregates']), - json={'aggregates': [self.fake_aggregate]}), + 'compute', 'public', append=['os-aggregates', '1']), + json=self.fake_aggregate), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', @@ -182,8 +187,8 @@ def test_remove_host_from_aggregate(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['os-aggregates']), - json={'aggregates': [self.fake_aggregate]}), + 'compute', 'public', append=['os-aggregates', '1']), + json=self.fake_aggregate), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', From 67397a9d3009735437a25a173561188260cada2f Mon Sep 17 00:00:00 2001 From: songwenping Date: Sat, 2 Jan 2021 10:22:27 +0800 Subject: [PATCH 2761/3836] Trival change: Correct some errors Change-Id: Ib28c6e5b818f6bd76d66af27f11917fd0cac4046 --- openstack/accelerator/v2/_proxy.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/openstack/accelerator/v2/_proxy.py b/openstack/accelerator/v2/_proxy.py index 8be5ac754..480c873b5 100644 --- a/openstack/accelerator/v2/_proxy.py +++ b/openstack/accelerator/v2/_proxy.py @@ -41,7 +41,7 @@ def update_deployable(self, uuid, patch): """Reconfig the FPGA with new bitstream. :param uuid: The value can be the UUID of a deployable - :param patch: The infomation of to reconfig. + :param patch: The information to reconfig. :returns: The results of FPGA reconfig. """ return self._get_resource(_deployable.Deployable, @@ -75,7 +75,7 @@ def get_device(self, uuid, fields=None): :param uuid: The value can be the UUID of a device. :returns: One :class:`~openstack.accelerator.v2.device.Device` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no - deployable matching the criteria could be found. + device matching the criteria could be found. """ return self._get(_device.Device, uuid) @@ -89,7 +89,7 @@ def device_profiles(self, **query): return self._list(_device_profile.DeviceProfile, **query) def create_device_profile(self, **attrs): - """Create a device_profiles. + """Create a device_profile. :param kwargs attrs: a list of device_profiles. :returns: The list of created device profiles @@ -97,10 +97,10 @@ def create_device_profile(self, **attrs): return self._create(_device_profile.DeviceProfile, **attrs) def delete_device_profile(self, name_or_id, ignore_missing=True): - """Delete an device profile + """Delete a device profile - :param name_or_id: The value can be either the ID of - an device profile. + :param name_or_id: The value can be either the ID or name of + a device profile. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the device profile does not exist. @@ -135,12 +135,13 @@ def create_accelerator_request(self, **attrs): """Create an ARQs for a single device profile. :param kwargs attrs: request body. + :returns: The created accelerator request instance. """ return self._create(_arq.AcceleratorRequest, **attrs) def delete_accelerator_request(self, name_or_id, ignore_missing=True): - """Delete an device profile - :param name_or_id: The value can be either the ID of + """Delete a device profile + :param name_or_id: The value can be either the ID or name of an accelerator request. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be @@ -164,7 +165,7 @@ def get_accelerator_request(self, uuid, fields=None): def update_accelerator_request(self, uuid, properties): """Bind/Unbind an accelerator to VM. - :param uuid: The uuid of the accelerator_request to be binded/unbinded. + :param uuid: The uuid of the accelerator_request to be bound/unbound. :param properties: The info of VM that will bind/unbind the accelerator. :returns: True if bind/unbind succeeded, False otherwise. From c3ce787389f916d9b7d0e5b7a453fbb652fd870e Mon Sep 17 00:00:00 2001 From: Irina Pereyaslavskaya Date: Fri, 18 Dec 2020 16:42:01 +0300 Subject: [PATCH 2762/3836] New volume availability zone resource, new functional and unit tests Change-Id: I1863095c2c6d6d3e10ef19a5afe00e1526d52c43 --- openstack/block_storage/v3/_proxy.py | 11 ++++++ .../block_storage/v3/availability_zone.py | 28 +++++++++++++ .../v3/test_availability_zone.py | 25 ++++++++++++ .../v3/test_availability_zone.py | 39 +++++++++++++++++++ 4 files changed, 103 insertions(+) create mode 100644 openstack/block_storage/v3/availability_zone.py create mode 100644 openstack/tests/functional/block_storage/v3/test_availability_zone.py create mode 100644 openstack/tests/unit/block_storage/v3/test_availability_zone.py diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 9b30d8b0c..e9d78b14d 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -11,6 +11,7 @@ # under the License. from openstack.block_storage import _base_proxy +from openstack.block_storage.v3 import availability_zone from openstack.block_storage.v3 import backup as _backup from openstack.block_storage.v3 import snapshot as _snapshot from openstack.block_storage.v3 import stats as _stats @@ -479,6 +480,16 @@ def restore_backup(self, backup, volume_id=None, name=None): backup = self._get_resource(_backup.Backup, backup) return backup.restore(self, volume_id=volume_id, name=name) + def availability_zones(self): + """Return a generator of availability zones + + :returns: A generator of availability zone + :rtype: :class:`~openstack.block_storage.v3.availability_zone.\ + AvailabilityZone` + """ + + return self._list(availability_zone.AvailabilityZone) + def wait_for_status(self, res, status='ACTIVE', failures=None, interval=2, wait=120): """Wait for a resource to be in a particular status. diff --git a/openstack/block_storage/v3/availability_zone.py b/openstack/block_storage/v3/availability_zone.py new file mode 100644 index 000000000..ce842b4f1 --- /dev/null +++ b/openstack/block_storage/v3/availability_zone.py @@ -0,0 +1,28 @@ +# 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 resource + + +class AvailabilityZone(resource.Resource): + resource_key = "" + resources_key = "availabilityZoneInfo" + base_path = "/os-availability-zone" + + # capabilities + allow_list = True + + #: Properties + #: Name of availability zone + name = resource.Body("zoneName", type=str) + #: State of availability zone, "available" is usual key + state = resource.Body("zoneState", type=dict) diff --git a/openstack/tests/functional/block_storage/v3/test_availability_zone.py b/openstack/tests/functional/block_storage/v3/test_availability_zone.py new file mode 100644 index 000000000..e17c52869 --- /dev/null +++ b/openstack/tests/functional/block_storage/v3/test_availability_zone.py @@ -0,0 +1,25 @@ +# 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.tests.functional import base + + +class TestAvailabilityZone(base.BaseFunctionalTest): + + def test_list(self): + availability_zones = list(self.conn.block_storage.availability_zones()) + self.assertGreater(len(availability_zones), 0) + + for az in availability_zones: + self.assertIsInstance(az.name, str) + self.assertIsInstance(az.state, dict) diff --git a/openstack/tests/unit/block_storage/v3/test_availability_zone.py b/openstack/tests/unit/block_storage/v3/test_availability_zone.py new file mode 100644 index 000000000..adc40515b --- /dev/null +++ b/openstack/tests/unit/block_storage/v3/test_availability_zone.py @@ -0,0 +1,39 @@ +# 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.block_storage.v3 import availability_zone as az + +from openstack.tests.unit import base + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + "id": IDENTIFIER, + "zoneState": { + "available": True + }, + "zoneName": "zone1" +} + + +class TestAvailabilityZone(base.TestCase): + + def test_basic(self): + sot = az.AvailabilityZone() + self.assertEqual('availabilityZoneInfo', sot.resources_key) + self.assertEqual('/os-availability-zone', sot.base_path) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = az.AvailabilityZone(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['zoneState'], sot.state) + self.assertEqual(EXAMPLE['zoneName'], sot.name) From 5450c4525313581b2ebc0e1ab6ac6252964c595f Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 4 Dec 2020 18:36:14 +0100 Subject: [PATCH 2763/3836] Complete compute.service operations Complete the operations on the compute.service resource. Change-Id: I0b7d41b407c436dd5583158bc6b5815847cffa31 --- doc/source/user/proxies/compute.rst | 3 +- openstack/compute/v2/_proxy.py | 122 ++++++++++++--- openstack/compute/v2/service.py | 105 ++++++++++--- .../functional/compute/v2/test_service.py | 47 ++++++ openstack/tests/unit/compute/v2/test_proxy.py | 115 +++++++++++--- .../tests/unit/compute/v2/test_service.py | 148 ++++++++++++++++-- ...e-service-force-down-6f462d62959a5315.yaml | 9 ++ 7 files changed, 478 insertions(+), 71 deletions(-) create mode 100644 openstack/tests/functional/compute/v2/test_service.py create mode 100644 releasenotes/notes/rename-service-force-down-6f462d62959a5315.yaml diff --git a/doc/source/user/proxies/compute.rst b/doc/source/user/proxies/compute.rst index eaa904271..65d7c4916 100644 --- a/doc/source/user/proxies/compute.rst +++ b/doc/source/user/proxies/compute.rst @@ -78,7 +78,8 @@ Service Operations .. autoclass:: openstack.compute.v2._proxy.Proxy :noindex: - :members: services, enable_service, disable_service, force_service_down + :members: services, enable_service, disable_service, update_service_forced_down, + delete_service, update_service, find_service Volume Attachment Operations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 33a22d9ca..bf7295df8 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -29,6 +29,7 @@ from openstack.compute.v2 import service as _service from openstack.compute.v2 import volume_attachment as _volume_attachment from openstack.network.v2 import security_group as _sg +from openstack import exceptions from openstack import proxy from openstack import resource from openstack import utils @@ -1402,57 +1403,140 @@ def get_hypervisor(self, hypervisor): """ return self._get(_hypervisor.Hypervisor, hypervisor) - def force_service_down(self, service, host, binary): - """Force a service down + # ========== Services ========== + + def update_service_forced_down( + self, service, host=None, binary=None, forced=True + ): + """Update service forced_down information :param service: Either the ID of a service or a - :class:`~openstack.compute.v2.server.Service` instance. + :class:`~openstack.compute.v2.service.Service` instance. :param str host: The host where service runs. :param str binary: The name of service. + :param bool forced: Whether or not this service was forced down + manually by an administrator after the service was fenced. - :returns: None + :returns: Updated service instance + :rtype: class: `~openstack.compute.v2.service.Service` """ - service = self._get_resource(_service.Service, service) - service.force_down(self, host, binary) + if utils.supports_microversion(self, '2.53'): + return self.update_service( + service, forced_down=forced) - def disable_service(self, service, host, binary, disabled_reason=None): + service = self._get_resource(_service.Service, service) + if ( + (not host or not binary) + and (not service.host or not service.binary) + ): + raise ValueError( + 'Either service instance should have host and binary ' + 'or they should be passed') + service.set_forced_down(self, host, binary, forced) + + force_service_down = update_service_forced_down + + def disable_service( + self, service, host=None, binary=None, disabled_reason=None + ): """Disable a service :param service: Either the ID of a service or a - :class:`~openstack.compute.v2.server.Service` instance. + :class:`~openstack.compute.v2.service.Service` instance. :param str host: The host where service runs. :param str binary: The name of service. :param str disabled_reason: The reason of force down a service. - :returns: None + :returns: Updated service instance + :rtype: class: `~openstack.compute.v2.service.Service` """ + if utils.supports_microversion(self, '2.53'): + attrs = { + 'status': 'disabled' + } + if disabled_reason: + attrs['disabled_reason'] = disabled_reason + return self.update_service( + service, **attrs) + service = self._get_resource(_service.Service, service) - service.disable(self, - host, binary, - disabled_reason) + return service.disable( + self, host, binary, disabled_reason) - def enable_service(self, service, host, binary): + def enable_service(self, service, host=None, binary=None): """Enable a service :param service: Either the ID of a service or a - :class:`~openstack.compute.v2.server.Service` instance. + :class:`~openstack.compute.v2.service.Service` instance. :param str host: The host where service runs. :param str binary: The name of service. - - :returns: None + :returns: Updated service instance + :rtype: class: `~openstack.compute.v2.service.Service` """ + if utils.supports_microversion(self, '2.53'): + return self.update_service( + service, status='enabled') + service = self._get_resource(_service.Service, service) - service.enable(self, host, binary) + return service.enable(self, host, binary) - def services(self): + def services(self, **query): """Return a generator of service + :params dict query: Query parameters :returns: A generator of service :rtype: class: `~openstack.compute.v2.service.Service` """ + return self._list(_service.Service, **query) + + def find_service(self, name_or_id, ignore_missing=True, **attrs): + """Find a service from name or id to get the corresponding info + + :param name_or_id: The name or id of a service + :param dict attrs: Additional attributes like 'host' + + :returns: + One: class:`~openstack.compute.v2.hypervisor.Hypervisor` object + or None + """ + return self._find(_service.Service, name_or_id, + ignore_missing=ignore_missing, **attrs) + + def delete_service(self, service, ignore_missing=True): + """Delete a service + + :param service: + The value can be either the ID of a service or a + :class:`~openstack.compute.v2.service.Service` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the volume attachment does not exist. When set to ``True``, no + exception will be set when attempting to delete a nonexistent + volume attachment. + + :returns: ``None`` + """ + self._delete( + _service.Service, service, ignore_missing=ignore_missing) + + def update_service(self, service, **attrs): + """Update a service + + :param server: Either the ID of a service or a + :class:`~openstack.compute.v2.service.Service` instance. + :attrs kwargs: The attributes to update on the service represented + by ``service``. + + :returns: The updated service + :rtype: :class:`~openstack.compute.v2.service.Service` + """ + if utils.supports_microversion(self, '2.53'): + return self._update(_service.Service, service, **attrs) - return self._list(_service.Service) + raise exceptions.SDKException( + 'Method require at least microversion 2.53' + ) def create_volume_attachment(self, server, **attrs): """Create a new volume attachment from attributes diff --git a/openstack/compute/v2/service.py b/openstack/compute/v2/service.py index d1361ffd8..4694928ed 100644 --- a/openstack/compute/v2/service.py +++ b/openstack/compute/v2/service.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import exceptions from openstack import resource from openstack import utils @@ -22,37 +23,105 @@ class Service(resource.Resource): # capabilities allow_list = True allow_commit = True + allow_delete = True + + _query_mapping = resource.QueryParameters( + 'name', 'binary', 'host', + name='binary', + ) # Properties - #: Status of service - status = resource.Body('status') - #: State of service - state = resource.Body('state') - #: Name of service + #: The availability zone of service + availability_zone = resource.Body("zone") + #: Binary name of service binary = resource.Body('binary') - #: Id of service - id = resource.Body('id') #: Disabled reason of service - disables_reason = resource.Body('disabled_reason') - #: Host where service runs + disabled_reason = resource.Body('disabled_reason') + #: Whether or not this service was forced down manually by an administrator + #: after the service was fenced + is_forced_down = resource.Body('forced_down', type=bool) + #: The name of the host where service runs host = resource.Body('host') - #: The availability zone of service - availability_zone = resource.Body("zone") + #: Service name + name = resource.Body('name', alias='binary') + #: State of service + state = resource.Body('state') + #: Status of service + status = resource.Body('status') + #: The date and time when the resource was updated + updated_at = resource.Body('updated_at') - def _action(self, session, action, body): - url = utils.urljoin(Service.base_path, action) - return session.put(url, json=body) + _max_microversion = '2.69' + + @classmethod + def find(cls, session, name_or_id, ignore_missing=True, **params): + # No direct request possible, thus go directly to list + data = cls.list(session, **params) + + result = None + for maybe_result in data: + # Since ID might be both int and str force cast + id_value = str(cls._get_id(maybe_result)) + name_value = maybe_result.name + + if str(name_or_id) in (id_value, name_value): + if 'host' in params and maybe_result['host'] != params['host']: + continue + # Only allow one resource to be found. If we already + # found a match, raise an exception to show it. + if result is None: + result = maybe_result + else: + msg = "More than one %s exists with the name '%s'." + msg = (msg % (cls.__name__, name_or_id)) + raise exceptions.DuplicateResource(msg) - def force_down(self, session, host, binary): - """Force a service down.""" + if result is not None: + return result + if ignore_missing: + return None + raise exceptions.ResourceNotFound( + "No %s found for %s" % (cls.__name__, name_or_id)) + + def commit(self, session, prepend_key=False, **kwargs): + # we need to set prepend_key to false + return super(Service, self).commit( + session, prepend_key=prepend_key, **kwargs) + + def _action(self, session, action, body, microversion=None): + if not microversion: + microversion = session.default_microversion + url = utils.urljoin(Service.base_path, action) + response = session.put(url, json=body, microversion=microversion) + self._translate_response(response) + return self + + def set_forced_down( + self, session, host=None, binary=None, forced=False + ): + """Update forced_down information of a service.""" + microversion = session.default_microversion + body = {} + if not host: + host = self.host + if not binary: + binary = self.binary body = { 'host': host, 'binary': binary, - 'forced_down': True, } + if utils.supports_microversion(session, '2.11'): + body['forced_down'] = forced + # Using forced_down works only 2.11-2.52, therefore pin it + microversion = '2.11' + + # This will not work with newest microversions + return self._action( + session, 'force-down', body, + microversion=microversion) - return self._action(session, 'force-down', body) + force_down = set_forced_down def enable(self, session, host, binary): """Enable service.""" diff --git a/openstack/tests/functional/compute/v2/test_service.py b/openstack/tests/functional/compute/v2/test_service.py new file mode 100644 index 000000000..31323cc88 --- /dev/null +++ b/openstack/tests/functional/compute/v2/test_service.py @@ -0,0 +1,47 @@ +# 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.tests.functional import base + + +class TestService(base.BaseFunctionalTest): + + def setUp(self): + super(TestService, self).setUp() + self._set_operator_cloud(interface='admin') + + def test_list(self): + sot = list(self.conn.compute.services()) + self.assertIsNotNone(sot) + + def test_disable_enable(self): + for srv in self.conn.compute.services(): + # only nova-compute can be updated + if srv.name == 'nova-compute': + self.conn.compute.disable_service(srv) + self.conn.compute.enable_service(srv) + + def test_update(self): + for srv in self.conn.compute.services(): + if srv.name == 'nova-compute': + self.conn.compute.update_service_forced_down( + srv, None, None, True) + self.conn.compute.update_service_forced_down( + srv, srv.host, srv.binary, False) + self.conn.compute.update_service(srv, status='enabled') + + def test_find(self): + for srv in self.conn.compute.services(): + if srv.name != 'nova-conductor': + # In devstack there are 2 nova-conductor instances on same host + self.conn.compute.find_service( + srv.name, host=srv.host, ignore_missing=False) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 498d864ce..1fc21da07 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -352,6 +352,99 @@ def test_aggregate_precache_images(self): expected_args=[[{'id': '1'}, {'id': '2'}]]) +class TestService(TestComputeProxy): + def test_services(self): + self.verify_list_no_kwargs( + self.proxy.services, service.Service) + + @mock.patch('openstack.utils.supports_microversion', autospec=True, + return_value=False) + def test_enable_service_252(self, mv_mock): + self._verify2( + 'openstack.compute.v2.service.Service.enable', + self.proxy.enable_service, + method_args=["value", "host1", "nova-compute"], + expected_args=[self.proxy, "host1", "nova-compute"] + ) + + @mock.patch('openstack.utils.supports_microversion', autospec=True, + return_value=True) + def test_enable_service_253(self, mv_mock): + self._verify2( + 'openstack.proxy.Proxy._update', + self.proxy.enable_service, + method_args=["value"], + method_kwargs={}, + expected_args=[service.Service, "value"], + expected_kwargs={'status': 'enabled'} + ) + + @mock.patch('openstack.utils.supports_microversion', autospec=True, + return_value=False) + def test_disable_service_252(self, mv_mock): + self._verify2( + 'openstack.compute.v2.service.Service.disable', + self.proxy.disable_service, + method_args=["value", "host1", "nova-compute"], + expected_args=[self.proxy, "host1", "nova-compute", None]) + + @mock.patch('openstack.utils.supports_microversion', autospec=True, + return_value=True) + def test_disable_service_253(self, mv_mock): + self._verify2( + 'openstack.proxy.Proxy._update', + self.proxy.disable_service, + method_args=["value"], + method_kwargs={'disabled_reason': 'some_reason'}, + expected_args=[service.Service, "value"], + expected_kwargs={ + 'status': 'disabled', + 'disabled_reason': 'some_reason' + } + ) + + @mock.patch('openstack.utils.supports_microversion', autospec=True, + return_value=False) + def test_force_service_down_252(self, mv_mock): + self._verify2( + 'openstack.compute.v2.service.Service.set_forced_down', + self.proxy.update_service_forced_down, + method_args=["value", "host1", "nova-compute"], + expected_args=[self.proxy, "host1", "nova-compute", True]) + + @mock.patch('openstack.utils.supports_microversion', autospec=True, + return_value=False) + def test_force_service_down_252_empty_vals(self, mv_mock): + self.assertRaises( + ValueError, + self.proxy.update_service_forced_down, + "value", None, None + ) + + @mock.patch('openstack.utils.supports_microversion', autospec=True, + return_value=False) + def test_force_service_down_252_empty_vals_svc(self, mv_mock): + self._verify2( + 'openstack.compute.v2.service.Service.set_forced_down', + self.proxy.update_service_forced_down, + method_args=[{'host': 'a', 'binary': 'b'}, None, None], + expected_args=[self.proxy, None, None, True]) + + def test_find_service(self): + self.verify_find( + self.proxy.find_service, + service.Service, + ) + + def test_find_service_args(self): + self.verify_find( + self.proxy.find_service, + service.Service, + method_kwargs={'host': 'h1'}, + expected_kwargs={'host': 'h1'} + ) + + class TestCompute(TestComputeProxy): def test_extension_find(self): self.verify_find(self.proxy.find_extension, extension.Extension) @@ -793,28 +886,6 @@ def test_get_hypervisor(self): self.verify_get(self.proxy.get_hypervisor, hypervisor.Hypervisor) - def test_services(self): - self.verify_list_no_kwargs(self.proxy.services, - service.Service) - - def test_enable_service(self): - self._verify('openstack.compute.v2.service.Service.enable', - self.proxy.enable_service, - method_args=["value", "host1", "nova-compute"], - expected_args=["host1", "nova-compute"]) - - def test_disable_service(self): - self._verify('openstack.compute.v2.service.Service.disable', - self.proxy.disable_service, - method_args=["value", "host1", "nova-compute"], - expected_args=["host1", "nova-compute", None]) - - def test_force_service_down(self): - self._verify('openstack.compute.v2.service.Service.force_down', - self.proxy.force_service_down, - method_args=["value", "host1", "nova-compute"], - expected_args=["host1", "nova-compute"]) - def test_live_migrate_server(self): self._verify('openstack.compute.v2.server.Server.live_migrate', self.proxy.live_migrate_server, diff --git a/openstack/tests/unit/compute/v2/test_service.py b/openstack/tests/unit/compute/v2/test_service.py index 9f6c20ac7..0db702852 100644 --- a/openstack/tests/unit/compute/v2/test_service.py +++ b/openstack/tests/unit/compute/v2/test_service.py @@ -12,6 +12,7 @@ from unittest import mock +from openstack import exceptions from openstack.compute.v2 import service from openstack.tests.unit import base @@ -31,10 +32,13 @@ class TestService(base.TestCase): def setUp(self): super(TestService, self).setUp() self.resp = mock.Mock() - self.resp.body = None + self.resp.body = {'service': {}} self.resp.json = mock.Mock(return_value=self.resp.body) + self.resp.status_code = 200 + self.resp.headers = {} self.sess = mock.Mock() self.sess.put = mock.Mock(return_value=self.resp) + self.sess.default_microversion = '2.1' def test_basic(self): sot = service.Service() @@ -45,20 +49,125 @@ def test_basic(self): self.assertTrue(sot.allow_list) self.assertFalse(sot.allow_fetch) + self.assertDictEqual({ + 'binary': 'binary', + 'host': 'host', + 'limit': 'limit', + 'marker': 'marker', + 'name': 'binary', + }, + sot._query_mapping._mapping) + def test_make_it(self): sot = service.Service(**EXAMPLE) self.assertEqual(EXAMPLE['host'], sot.host) self.assertEqual(EXAMPLE['binary'], sot.binary) + self.assertEqual(EXAMPLE['binary'], sot.name) self.assertEqual(EXAMPLE['status'], sot.status) self.assertEqual(EXAMPLE['state'], sot.state) self.assertEqual(EXAMPLE['zone'], sot.availability_zone) self.assertEqual(EXAMPLE['id'], sot.id) - def test_force_down(self): + def test_find_single_match(self): + data = [ + service.Service(name='bin1', host='host', id=1), + service.Service(name='bin2', host='host', id=2), + ] + with mock.patch.object(service.Service, 'list') as list_mock: + list_mock.return_value = data + + sot = service.Service.find( + self.sess, 'bin1', ignore_missing=True, host='host' + ) + + self.assertEqual(data[0], sot) + + def test_find_with_id_single_match(self): + data = [ + service.Service(name='bin1', host='host', id=1), + service.Service(name='bin2', host='host', id='2'), + ] + with mock.patch.object(service.Service, 'list') as list_mock: + list_mock.return_value = data + + sot = service.Service.find( + self.sess, '2', ignore_missing=True, + binary='bin1', host='host' + ) + + self.assertEqual(data[1], sot) + + # Verify find when ID is int + sot = service.Service.find( + self.sess, 1, ignore_missing=True, + binary='bin1', host='host' + ) + + self.assertEqual(data[0], sot) + + def test_find_no_match(self): + data = [ + service.Service(name='bin1', host='host', id=1), + service.Service(name='bin2', host='host', id=2), + ] + with mock.patch.object(service.Service, 'list') as list_mock: + list_mock.return_value = data + + self.assertIsNone(service.Service.find( + self.sess, 'fake', ignore_missing=True, host='host' + )) + + def test_find_no_match_exception(self): + data = [ + service.Service(name='bin1', host='host', id=1), + service.Service(name='bin2', host='host', id=2), + ] + with mock.patch.object(service.Service, 'list') as list_mock: + list_mock.return_value = data + + self.assertRaises( + exceptions.ResourceNotFound, + service.Service.find, + self.sess, 'fake', ignore_missing=False, host='host' + ) + + def test_find_multiple_match(self): + data = [ + service.Service(name='bin1', host='host', id=1), + service.Service(name='bin1', host='host', id=2), + ] + with mock.patch.object(service.Service, 'list') as list_mock: + list_mock.return_value = data + + self.assertRaises( + exceptions.DuplicateResource, + service.Service.find, + self.sess, 'bin1', ignore_missing=False, host='host' + ) + + @mock.patch('openstack.utils.supports_microversion', autospec=True, + return_value=False) + def test_set_forced_down_before_211(self, mv_mock): + sot = service.Service(**EXAMPLE) + + res = sot.set_forced_down(self.sess, 'host1', 'nova-compute', True) + self.assertIsNotNone(res) + + url = 'os-services/force-down' + body = { + 'binary': 'nova-compute', + 'host': 'host1', + } + self.sess.put.assert_called_with( + url, json=body, microversion=self.sess.default_microversion) + + @mock.patch('openstack.utils.supports_microversion', autospec=True, + return_value=True) + def test_set_forced_down_after_211(self, mv_mock): sot = service.Service(**EXAMPLE) - res = sot.force_down(self.sess, 'host1', 'nova-compute') - self.assertIsNone(res.body) + res = sot.set_forced_down(self.sess, 'host1', 'nova-compute', True) + self.assertIsNotNone(res) url = 'os-services/force-down' body = { @@ -67,13 +176,30 @@ def test_force_down(self): 'forced_down': True, } self.sess.put.assert_called_with( - url, json=body) + url, json=body, microversion='2.11') + + @mock.patch('openstack.utils.supports_microversion', autospec=True, + return_value=True) + def test_set_forced_down_after_253(self, mv_mock): + sot = service.Service(**EXAMPLE) + + res = sot.set_forced_down(self.sess, None, None, True) + self.assertIsNotNone(res) + + url = 'os-services/force-down' + body = { + 'binary': sot.binary, + 'host': sot.host, + 'forced_down': True, + } + self.sess.put.assert_called_with( + url, json=body, microversion='2.11') def test_enable(self): sot = service.Service(**EXAMPLE) res = sot.enable(self.sess, 'host1', 'nova-compute') - self.assertIsNone(res.body) + self.assertIsNotNone(res) url = 'os-services/enable' body = { @@ -81,13 +207,13 @@ def test_enable(self): 'host': 'host1', } self.sess.put.assert_called_with( - url, json=body) + url, json=body, microversion=self.sess.default_microversion) def test_disable(self): sot = service.Service(**EXAMPLE) res = sot.disable(self.sess, 'host1', 'nova-compute') - self.assertIsNone(res.body) + self.assertIsNotNone(res) url = 'os-services/disable' body = { @@ -95,7 +221,7 @@ def test_disable(self): 'host': 'host1', } self.sess.put.assert_called_with( - url, json=body) + url, json=body, microversion=self.sess.default_microversion) def test_disable_with_reason(self): sot = service.Service(**EXAMPLE) @@ -103,7 +229,7 @@ def test_disable_with_reason(self): res = sot.disable(self.sess, 'host1', 'nova-compute', reason=reason) - self.assertIsNone(res.body) + self.assertIsNotNone(res) url = 'os-services/disable-log-reason' body = { @@ -112,4 +238,4 @@ def test_disable_with_reason(self): 'disabled_reason': reason } self.sess.put.assert_called_with( - url, json=body) + url, json=body, microversion=self.sess.default_microversion) diff --git a/releasenotes/notes/rename-service-force-down-6f462d62959a5315.yaml b/releasenotes/notes/rename-service-force-down-6f462d62959a5315.yaml new file mode 100644 index 000000000..bad5c790b --- /dev/null +++ b/releasenotes/notes/rename-service-force-down-6f462d62959a5315.yaml @@ -0,0 +1,9 @@ +--- +upgrade: + - | + compute.force_service_down function is renamed to + update_service_forced_down to better fit the operation meaning. + - | + compute.v2.service.force_down is renamed to set_forced_down to fit the operation meaning. + - | + return of compute.service modification operations is changed to be the service itself From e5e7c607c15467d81160b7027388a5052c3ef0d5 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 6 Jan 2021 11:08:29 +0100 Subject: [PATCH 2764/3836] Add query parameters to listing heat stacks Fix query parameters for listing heat stacks. This is also required for corresponding Ansible module. Change-Id: Ie9e27ad67061528cacbef8fa06658b4f61bfba33 --- openstack/cloud/_orchestration.py | 5 ++-- openstack/orchestration/v1/stack.py | 7 ++++-- openstack/tests/unit/cloud/test_stack.py | 24 +++++++++++++++++++ .../tests/unit/orchestration/v1/test_stack.py | 17 +++++++++++++ 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/openstack/cloud/_orchestration.py b/openstack/cloud/_orchestration.py index bab1ed0ce..5bcc5ae11 100644 --- a/openstack/cloud/_orchestration.py +++ b/openstack/cloud/_orchestration.py @@ -209,15 +209,16 @@ def search_stacks(self, name_or_id=None, filters=None): return _utils._filter_list(stacks, name_or_id, filters) @_utils.cache_on_arguments(should_cache_fn=_no_pending_stacks) - def list_stacks(self): + def list_stacks(self, **query): """List all stacks. + :param dict query: Query parameters to limit stacks. :returns: a list of ``munch.Munch`` containing the stack description. :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ - data = self.orchestration.stacks() + data = self.orchestration.stacks(**query) return self._normalize_stacks(data) def get_stack(self, name_or_id, filters=None, resolve_outputs=True): diff --git a/openstack/orchestration/v1/stack.py b/openstack/orchestration/v1/stack.py index adb577b01..1bfc1a7dd 100644 --- a/openstack/orchestration/v1/stack.py +++ b/openstack/orchestration/v1/stack.py @@ -29,7 +29,10 @@ class Stack(resource.Resource): allow_delete = True _query_mapping = resource.QueryParameters( - 'resolve_outputs' + 'action', 'name', 'status', + 'project_id', 'owner_id', 'username', + project_id='tenant_id', + **resource.TagMixin._tag_query_parameters ) # Properties @@ -84,7 +87,7 @@ class Stack(resource.Resource): #: A text explaining how the stack transits to its current status. status_reason = resource.Body('stack_status_reason') #: A list of strings used as tags on the stack - tags = resource.Body('tags') + tags = resource.Body('tags', type=list, default=[]) #: A dict containing the template use for stack creation. template = resource.Body('template', type=dict) #: Stack template description text. Currently contains the same text diff --git a/openstack/tests/unit/cloud/test_stack.py b/openstack/tests/unit/cloud/test_stack.py index 72335f740..1da6381d2 100644 --- a/openstack/tests/unit/cloud/test_stack.py +++ b/openstack/tests/unit/cloud/test_stack.py @@ -51,6 +51,30 @@ def test_list_stacks(self): self.assert_calls() + def test_list_stacks_filters(self): + fake_stacks = [ + self.stack, + fakes.make_fake_stack( + self.getUniqueString('id'), + self.getUniqueString('name')) + ] + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'orchestration', 'public', + append=['stacks'], + qs_elements=['name=a', 'status=b'], + ), + json={"stacks": fake_stacks}), + ]) + stacks = self.cloud.list_stacks(name='a', status='b') + self.assertEqual( + [f.toDict() for f in self.cloud._normalize_stacks( + stack.Stack(**st) for st in fake_stacks)], + [f.toDict() for f in stacks]) + + self.assert_calls() + def test_list_stacks_exception(self): self.register_uris([ dict(method='GET', diff --git a/openstack/tests/unit/orchestration/v1/test_stack.py b/openstack/tests/unit/orchestration/v1/test_stack.py index 080ede820..f3aad5d5f 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack.py +++ b/openstack/tests/unit/orchestration/v1/test_stack.py @@ -135,6 +135,23 @@ def test_basic(self): self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) + self.assertDictEqual( + { + 'action': 'action', + 'any_tags': 'tags-any', + 'limit': 'limit', + 'marker': 'marker', + 'name': 'name', + 'not_any_tags': 'not-tags-any', + 'not_tags': 'not-tags', + 'owner_id': 'owner_id', + 'project_id': 'tenant_id', + 'status': 'status', + 'tags': 'tags', + 'username': 'username', + }, + sot._query_mapping._mapping) + def test_make_it(self): sot = stack.Stack(**FAKE) self.assertEqual(FAKE['capabilities'], sot.capabilities) From 4c870cf2b9221e7c2ce7ca8a9dabab8a3c2707e1 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 15 Jan 2021 12:42:47 +0100 Subject: [PATCH 2765/3836] Apply urllib.parse.quote in unittests to get_mock_url Apply quotations to the mock_url so that it is look exactly like it would be. When using proxy layer and triggering find it is not impossible to have url like: https://neutron.example.com/v2.0/ports/my%20port. This change simplifies size of the followup patch of switching networking cloud layer to rely on proxy. Change-Id: If3cb96d1e4413643061bbb5f13ebf28e8c155c75 --- openstack/tests/unit/base.py | 7 +++-- .../tests/unit/cloud/test_baremetal_node.py | 4 +-- .../tests/unit/cloud/test_baremetal_ports.py | 26 +++++++++++++------ openstack/tests/unit/cloud/test_recordset.py | 3 ++- openstack/tests/unit/cloud/test_zone.py | 8 +++--- 5 files changed, 31 insertions(+), 17 deletions(-) diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index d0eef6ed7..9eb597ea5 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -187,10 +187,13 @@ def get_mock_url(self, service_type, interface='public', resource=None, to_join.append(base_url_append) if resource: to_join.append(resource) - to_join.extend(append or []) + if append: + to_join.extend([urllib.parse.quote(i) for i in append]) if qs_elements is not None: qs = '?%s' % '&'.join(qs_elements) - return '%(uri)s%(qs)s' % {'uri': '/'.join(to_join), 'qs': qs} + return '%(uri)s%(qs)s' % { + 'uri': '/'.join(to_join), + 'qs': qs} def mock_for_keystone_projects(self, project=None, v3=True, list_get=False, id_get=False, diff --git a/openstack/tests/unit/cloud/test_baremetal_node.py b/openstack/tests/unit/cloud/test_baremetal_node.py index d18c940bf..794208a86 100644 --- a/openstack/tests/unit/cloud/test_baremetal_node.py +++ b/openstack/tests/unit/cloud/test_baremetal_node.py @@ -71,13 +71,13 @@ def test_get_machine(self): def test_get_machine_by_mac(self): mac_address = '00:01:02:03:04:05' - url_address = 'detail?address=%s' % mac_address node_uuid = self.fake_baremetal_node['uuid'] self.register_uris([ dict(method='GET', uri=self.get_mock_url( resource='ports', - append=[url_address]), + append=['detail'], + qs_elements=['address=%s' % mac_address]), json={'ports': [{'address': mac_address, 'node_uuid': node_uuid}]}), dict(method='GET', diff --git a/openstack/tests/unit/cloud/test_baremetal_ports.py b/openstack/tests/unit/cloud/test_baremetal_ports.py index aa39b67ed..4ff9b63ef 100644 --- a/openstack/tests/unit/cloud/test_baremetal_ports.py +++ b/openstack/tests/unit/cloud/test_baremetal_ports.py @@ -64,10 +64,13 @@ def test_list_nics_failure(self): self.assert_calls() def test_list_nics_for_machine(self): - query = 'detail?node_uuid=%s' % self.fake_baremetal_node['uuid'] self.register_uris([ dict(method='GET', - uri=self.get_mock_url(resource='ports', append=[query]), + uri=self.get_mock_url( + resource='ports', + append=['detail'], + qs_elements=['node_uuid=%s' % + self.fake_baremetal_node['uuid']]), json={'ports': [self.fake_baremetal_port, self.fake_baremetal_port2]}), ]) @@ -79,10 +82,13 @@ def test_list_nics_for_machine(self): self.assert_calls() def test_list_nics_for_machine_failure(self): - query = 'detail?node_uuid=%s' % self.fake_baremetal_node['uuid'] self.register_uris([ dict(method='GET', - uri=self.get_mock_url(resource='ports', append=[query]), + uri=self.get_mock_url( + resource='ports', + append=['detail'], + qs_elements=['node_uuid=%s' % + self.fake_baremetal_node['uuid']]), status_code=400) ]) @@ -93,10 +99,12 @@ def test_list_nics_for_machine_failure(self): def test_get_nic_by_mac(self): mac = self.fake_baremetal_port['address'] - query = 'detail?address=%s' % mac self.register_uris([ dict(method='GET', - uri=self.get_mock_url(resource='ports', append=[query]), + uri=self.get_mock_url( + resource='ports', + append=['detail'], + qs_elements=['address=%s' % mac]), json={'ports': [self.fake_baremetal_port]}), ]) @@ -107,10 +115,12 @@ def test_get_nic_by_mac(self): def test_get_nic_by_mac_failure(self): mac = self.fake_baremetal_port['address'] - query = 'detail?address=%s' % mac self.register_uris([ dict(method='GET', - uri=self.get_mock_url(resource='ports', append=[query]), + uri=self.get_mock_url( + resource='ports', + append=['detail'], + qs_elements=['address=%s' % mac]), json={'ports': []}), ]) diff --git a/openstack/tests/unit/cloud/test_recordset.py b/openstack/tests/unit/cloud/test_recordset.py index 1d1424310..3c4c5f323 100644 --- a/openstack/tests/unit/cloud/test_recordset.py +++ b/openstack/tests/unit/cloud/test_recordset.py @@ -208,7 +208,8 @@ def test_list_recordsets(self): 'next': self.get_mock_url( 'dns', 'public', append=['v2', 'zones', fake_zone['id'], - 'recordsets?limit=1&marker=asd']), + 'recordsets'], + qs_elements=['limit=1', 'marker=asd']), 'self': self.get_mock_url( 'dns', 'public', append=['v2', 'zones', fake_zone['id'], diff --git a/openstack/tests/unit/cloud/test_zone.py b/openstack/tests/unit/cloud/test_zone.py index 145e04a26..1573ed62f 100644 --- a/openstack/tests/unit/cloud/test_zone.py +++ b/openstack/tests/unit/cloud/test_zone.py @@ -195,12 +195,12 @@ def test_list_zones(self): 'links': { 'next': self.get_mock_url( 'dns', 'public', - append=['v2', 'zones', - '?limit=1&marker=asd']), + append=['v2', 'zones/'], + qs_elements=['limit=1', 'marker=asd']), 'self': self.get_mock_url( 'dns', 'public', - append=['v2', 'zones', - '?limit=1'])}, + append=['v2', 'zones/'], + qs_elements=['limit=1'])}, 'metadata':{'total_count': 2}}), dict(method='GET', uri=self.get_mock_url( From 30116cf3c2f6eeabc662f3579b4f4e8fca9d150e Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 18 Nov 2020 16:11:23 +0100 Subject: [PATCH 2766/3836] Complete compute.hypervisor functions For the OSC ensure we do support everything what nova supports on the hypervisors front Change-Id: I9051123e905f7df44f9cb9be62c90f24140cb9ac --- doc/source/user/proxies/compute.rst | 3 +- doc/source/user/resources/compute/index.rst | 1 + .../user/resources/compute/v2/hypervisor.rst | 12 ++ openstack/compute/v2/_proxy.py | 34 +++++- openstack/compute/v2/hypervisor.py | 87 +++++++++----- .../functional/compute/v2/test_hypervisor.py | 31 +++++ .../tests/unit/compute/v2/test_hypervisor.py | 108 +++++++++++++++--- openstack/tests/unit/compute/v2/test_proxy.py | 84 +++++++++++--- ...k-compute-hypervisor-a62f275a0fd1f074.yaml | 5 + 9 files changed, 300 insertions(+), 65 deletions(-) create mode 100644 doc/source/user/resources/compute/v2/hypervisor.rst create mode 100644 openstack/tests/functional/compute/v2/test_hypervisor.py create mode 100644 releasenotes/notes/rework-compute-hypervisor-a62f275a0fd1f074.yaml diff --git a/doc/source/user/proxies/compute.rst b/doc/source/user/proxies/compute.rst index 65d7c4916..a4b771551 100644 --- a/doc/source/user/proxies/compute.rst +++ b/doc/source/user/proxies/compute.rst @@ -140,7 +140,8 @@ Hypervisor Operations .. autoclass:: openstack.compute.v2._proxy.Proxy :noindex: - :members: get_hypervisor, find_hypervisor, hypervisors + :members: get_hypervisor, find_hypervisor, hypervisors, + get_hypervisor_uptime Extension Operations ^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/compute/index.rst b/doc/source/user/resources/compute/index.rst index f597c7d86..e56d2a058 100644 --- a/doc/source/user/resources/compute/index.rst +++ b/doc/source/user/resources/compute/index.rst @@ -12,3 +12,4 @@ Compute Resources v2/server v2/server_interface v2/server_ip + v2/hypervisor diff --git a/doc/source/user/resources/compute/v2/hypervisor.rst b/doc/source/user/resources/compute/v2/hypervisor.rst new file mode 100644 index 000000000..6959db4ac --- /dev/null +++ b/doc/source/user/resources/compute/v2/hypervisor.rst @@ -0,0 +1,12 @@ +openstack.compute.v2.hypervisor +=============================== + +.. automodule:: openstack.compute.v2.hypervisor + +The Hypervisor Class +-------------------- + +The ``Hypervisor`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.compute.v2.hypervisor.Hypervisor + :members: diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index bf7295df8..5cc6fc187 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -1362,6 +1362,8 @@ def server_groups(self, **query): """ return self._list(_server_group.ServerGroup, **query) + # ========== Hypervisors ========== + def hypervisors(self, details=False, **query): """Return a generator of hypervisor @@ -1374,9 +1376,16 @@ def hypervisors(self, details=False, **query): :rtype: class: `~openstack.compute.v2.hypervisor.Hypervisor` """ base_path = '/os-hypervisors/detail' if details else None + if ( + 'hypervisor_hostname_pattern' in query + and not utils.supports_microversion(self, '2.53') + ): + # Until 2.53 we need to use other API + base_path = '/os-hypervisors/{pattern}/search'.format( + pattern=query.pop('hypervisor_hostname_pattern')) return self._list(_hypervisor.Hypervisor, base_path=base_path, **query) - def find_hypervisor(self, name_or_id, ignore_missing=True): + def find_hypervisor(self, name_or_id, ignore_missing=True, details=True): """Find a hypervisor from name or id to get the corresponding info :param name_or_id: The name or id of a hypervisor @@ -1386,23 +1395,40 @@ def find_hypervisor(self, name_or_id, ignore_missing=True): or None """ + list_base_path = '/os-hypervisors/detail' if details else None return self._find(_hypervisor.Hypervisor, name_or_id, + list_base_path=list_base_path, ignore_missing=ignore_missing) def get_hypervisor(self, hypervisor): """Get a single hypervisor :param hypervisor: The value can be the ID of a hypervisor or a - :class:`~openstack.compute.v2.hypervisor.Hypervisor` - instance. + :class:`~openstack.compute.v2.hypervisor.Hypervisor` + instance. :returns: A :class:`~openstack.compute.v2.hypervisor.Hypervisor` object. :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_hypervisor.Hypervisor, hypervisor) + def get_hypervisor_uptime(self, hypervisor): + """Get uptime information for hypervisor + + :param hypervisor: The value can be the ID of a hypervisor or a + :class:`~openstack.compute.v2.hypervisor.Hypervisor` + instance. + + :returns: + A :class:`~openstack.compute.v2.hypervisor.Hypervisor` object. + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + hypervisor = self._get_resource(_hypervisor.Hypervisor, hypervisor) + return hypervisor.get_uptime(self) + # ========== Services ========== def update_service_forced_down( diff --git a/openstack/compute/v2/hypervisor.py b/openstack/compute/v2/hypervisor.py index 9c18e7502..ae56abc97 100644 --- a/openstack/compute/v2/hypervisor.py +++ b/openstack/compute/v2/hypervisor.py @@ -10,8 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +import warnings +from openstack import exceptions from openstack import resource +from openstack import utils class Hypervisor(resource.Resource): @@ -27,48 +30,72 @@ class Hypervisor(resource.Resource): 'hypervisor_hostname_pattern', 'with_servers' ) - # Hypervisor id is a UUID starting with 2.53 - _max_microversion = '2.53' + # Lot of attributes are dropped in 2.88 + _max_microversion = '2.88' # Properties - #: Status of hypervisor - status = resource.Body('status') - #: State of hypervisor - state = resource.Body('state') - #: Name of hypervisor - name = resource.Body('hypervisor_hostname') - #: Service details - service_details = resource.Body('service') - #: Count of the VCPUs in use - vcpus_used = resource.Body('vcpus_used') - #: Count of all VCPUs - vcpus = resource.Body('vcpus') - #: Count of the running virtual machines - running_vms = resource.Body('running_vms') + #: Information about the hypervisor's CPU. Up to 2.28 it was string. + cpu_info = resource.Body('cpu_info') + #: IP address of the host + host_ip = resource.Body('host_ip') #: The type of hypervisor hypervisor_type = resource.Body('hypervisor_type') #: Version of the hypervisor hypervisor_version = resource.Body('hypervisor_version') + #: Name of hypervisor + name = resource.Body('hypervisor_hostname') + #: Service details + service_details = resource.Body('service', type=dict) + #: List of Servers + servers = resource.Body('servers', type=list, list_type=dict) + #: State of hypervisor + state = resource.Body('state') + #: Status of hypervisor + status = resource.Body('status') + #: The total uptime of the hypervisor and information about average load. + #: This attribute is set only when querying uptime explicitly. + uptime = resource.Body('uptime') + + # Attributes deprecated with 2.88 + #: Measurement of the hypervisor's current workload + current_workload = resource.Body('current_workload', deprecated=True) + #: Disk space available to the scheduler + disk_available = resource.Body("disk_available_least", deprecated=True) #: The amount, in gigabytes, of local storage used - local_disk_used = resource.Body('local_gb_used') + local_disk_used = resource.Body('local_gb_used', deprecated=True) #: The amount, in gigabytes, of the local storage device - local_disk_size = resource.Body('local_gb') + local_disk_size = resource.Body('local_gb', deprecated=True) #: The amount, in gigabytes, of free space on the local storage device - local_disk_free = resource.Body('free_disk_gb') + local_disk_free = resource.Body('free_disk_gb', deprecated=True) #: The amount, in megabytes, of memory - memory_used = resource.Body('memory_mb_used') + memory_used = resource.Body('memory_mb_used', deprecated=True) #: The amount, in megabytes, of total memory - memory_size = resource.Body('memory_mb') + memory_size = resource.Body('memory_mb', deprecated=True) #: The amount, in megabytes, of available memory - memory_free = resource.Body('free_ram_mb') - #: Measurement of the hypervisor's current workload - current_workload = resource.Body('current_workload') - #: Information about the hypervisor's CPU - cpu_info = resource.Body('cpu_info') - #: IP address of the host - host_ip = resource.Body('host_ip') - #: Disk space available to the scheduler - disk_available = resource.Body("disk_available_least") + memory_free = resource.Body('free_ram_mb', deprecated=True) + #: Count of the running virtual machines + running_vms = resource.Body('running_vms', deprecated=True) + #: Count of the VCPUs in use + vcpus_used = resource.Body('vcpus_used', deprecated=True) + #: Count of all VCPUs + vcpus = resource.Body('vcpus', deprecated=True) + + def get_uptime(self, session): + """Get uptime information for the hypervisor + + Updates uptime attribute of the hypervisor object + """ + warnings.warn( + "This call is deprecated and is only available until Nova 2.88") + if utils.supports_microversion(session, '2.88'): + raise exceptions.SDKException( + 'Hypervisor.get_uptime is not supported anymore') + url = utils.urljoin(self.base_path, self.id, 'uptime') + microversion = self._get_microversion_for(session, 'fetch') + response = session.get( + url, microversion=microversion) + self._translate_response(response) + return self HypervisorDetail = Hypervisor diff --git a/openstack/tests/functional/compute/v2/test_hypervisor.py b/openstack/tests/functional/compute/v2/test_hypervisor.py new file mode 100644 index 000000000..383a51dc8 --- /dev/null +++ b/openstack/tests/functional/compute/v2/test_hypervisor.py @@ -0,0 +1,31 @@ +# 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.tests.functional import base + + +class TestHypervisor(base.BaseFunctionalTest): + + def setUp(self): + super(TestHypervisor, self).setUp() + + def test_list_hypervisors(self): + rslt = list(self.conn.compute.hypervisors()) + self.assertIsNotNone(rslt) + + rslt = list(self.conn.compute.hypervisors(details=True)) + self.assertIsNotNone(rslt) + + def test_get_find_hypervisors(self): + for hypervisor in self.conn.compute.hypervisors(): + self.conn.compute.get_hypervisor(hypervisor.id) + self.conn.compute.find_hypervisor(hypervisor.id) diff --git a/openstack/tests/unit/compute/v2/test_hypervisor.py b/openstack/tests/unit/compute/v2/test_hypervisor.py index 2a27c9464..b79b6bd80 100644 --- a/openstack/tests/unit/compute/v2/test_hypervisor.py +++ b/openstack/tests/unit/compute/v2/test_hypervisor.py @@ -9,41 +9,79 @@ # 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 +from unittest import mock +from keystoneauth1 import adapter + +from openstack import exceptions from openstack.tests.unit import base from openstack.compute.v2 import hypervisor EXAMPLE = { + "cpu_info": { + "arch": "x86_64", + "model": "Nehalem", + "vendor": "Intel", + "features": [ + "pge", + "clflush" + ], + "topology": { + "cores": 1, + "threads": 1, + "sockets": 4 + } + }, + "state": "up", "status": "enabled", + "servers": [ + { + "name": "test_server1", + "uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + }, + { + "name": "test_server2", + "uuid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" + } + ], + "host_ip": "1.1.1.1", + "hypervisor_hostname": "fake-mini", + "hypervisor_type": "fake", + "hypervisor_version": 1000, + "id": "b1e43b5f-eec1-44e0-9f10-7b4945c0226d", + "uptime": ( + " 08:32:11 up 93 days, 18:25, 12 users, " + "load average: 0.20, 0.12, 0.14"), "service": { - "host": "fake-mini", - "disabled_reason": None, - "id": 6 + "host": "043b3cacf6f34c90a7245151fc8ebcda", + "id": "5d343e1d-938e-4284-b98b-6a2b5406ba76", + "disabled_reason": None }, + # deprecated attributes "vcpus_used": 0, - "hypervisor_type": "QEMU", "local_gb_used": 0, "vcpus": 8, - "hypervisor_hostname": "fake-mini", "memory_mb_used": 512, "memory_mb": 7980, "current_workload": 0, - "state": "up", - "host_ip": "23.253.248.171", - "cpu_info": "some cpu info", "running_vms": 0, "free_disk_gb": 157, - "hypervisor_version": 2000000, "disk_available_least": 140, "local_gb": 157, "free_ram_mb": 7468, - "id": 1 } class TestHypervisor(base.TestCase): + def setUp(self): + super(TestHypervisor, self).setUp() + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.default_microversion = 1 + self.sess._get_connection = mock.Mock(return_value=self.cloud) + def test_basic(self): sot = hypervisor.Hypervisor() self.assertEqual('hypervisor', sot.resource_key) @@ -62,10 +100,17 @@ def test_basic(self): def test_make_it(self): sot = hypervisor.Hypervisor(**EXAMPLE) self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['cpu_info'], sot.cpu_info) + self.assertEqual(EXAMPLE['host_ip'], sot.host_ip) + self.assertEqual(EXAMPLE['hypervisor_type'], sot.hypervisor_type) + self.assertEqual(EXAMPLE['hypervisor_version'], sot.hypervisor_version) self.assertEqual(EXAMPLE['hypervisor_hostname'], sot.name) + self.assertEqual(EXAMPLE['service'], sot.service_details) + self.assertEqual(EXAMPLE['servers'], sot.servers) self.assertEqual(EXAMPLE['state'], sot.state) self.assertEqual(EXAMPLE['status'], sot.status) - self.assertEqual(EXAMPLE['service'], sot.service_details) + self.assertEqual(EXAMPLE['uptime'], sot.uptime) + # Verify deprecated attributes self.assertEqual(EXAMPLE['vcpus_used'], sot.vcpus_used) self.assertEqual(EXAMPLE['hypervisor_type'], sot.hypervisor_type) self.assertEqual(EXAMPLE['local_gb_used'], sot.local_disk_used) @@ -74,11 +119,46 @@ def test_make_it(self): self.assertEqual(EXAMPLE['memory_mb_used'], sot.memory_used) self.assertEqual(EXAMPLE['memory_mb'], sot.memory_size) self.assertEqual(EXAMPLE['current_workload'], sot.current_workload) - self.assertEqual(EXAMPLE['host_ip'], sot.host_ip) - self.assertEqual(EXAMPLE['cpu_info'], sot.cpu_info) self.assertEqual(EXAMPLE['running_vms'], sot.running_vms) self.assertEqual(EXAMPLE['free_disk_gb'], sot.local_disk_free) - self.assertEqual(EXAMPLE['hypervisor_version'], sot.hypervisor_version) self.assertEqual(EXAMPLE['disk_available_least'], sot.disk_available) self.assertEqual(EXAMPLE['local_gb'], sot.local_disk_size) self.assertEqual(EXAMPLE['free_ram_mb'], sot.memory_free) + + @mock.patch('openstack.utils.supports_microversion', autospec=True, + return_value=False) + def test_get_uptime(self, mv_mock): + sot = hypervisor.Hypervisor(**copy.deepcopy(EXAMPLE)) + rsp = { + "hypervisor": { + "hypervisor_hostname": "fake-mini", + "id": sot.id, + "state": "up", + "status": "enabled", + "uptime": "08:32:11 up 93 days, 18:25, 12 users" + } + } + resp = mock.Mock() + resp.body = copy.deepcopy(rsp) + resp.json = mock.Mock(return_value=resp.body) + resp.headers = {} + resp.status_code = 200 + self.sess.get = mock.Mock(return_value=resp) + + hyp = sot.get_uptime(self.sess) + self.sess.get.assert_called_with( + 'os-hypervisors/{id}/uptime'.format(id=sot.id), + microversion=self.sess.default_microversion + ) + self.assertEqual(rsp['hypervisor']['uptime'], hyp.uptime) + self.assertEqual(rsp['hypervisor']['status'], sot.status) + + @mock.patch('openstack.utils.supports_microversion', autospec=True, + return_value=True) + def test_get_uptime_after_2_88(self, mv_mock): + sot = hypervisor.Hypervisor(**copy.deepcopy(EXAMPLE)) + self.assertRaises( + exceptions.SDKException, + sot.get_uptime, + self.sess + ) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 1fc21da07..fb6e05410 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -445,6 +445,74 @@ def test_find_service_args(self): ) +class TestHypervisor(TestComputeProxy): + + def test_hypervisors_not_detailed(self): + self.verify_list(self.proxy.hypervisors, hypervisor.Hypervisor, + method_kwargs={"details": False}) + + def test_hypervisors_detailed(self): + self.verify_list(self.proxy.hypervisors, hypervisor.HypervisorDetail, + method_kwargs={"details": True}) + + @mock.patch('openstack.utils.supports_microversion', autospec=True, + return_value=False) + def test_hypervisors_search_before_253_no_qp(self, sm): + self.verify_list( + self.proxy.hypervisors, + hypervisor.Hypervisor, + method_kwargs={'details': True}, + base_path='/os-hypervisors/detail' + ) + + @mock.patch('openstack.utils.supports_microversion', autospec=True, + return_value=False) + def test_hypervisors_search_before_253(self, sm): + self.verify_list( + self.proxy.hypervisors, + hypervisor.Hypervisor, + method_kwargs={'hypervisor_hostname_pattern': 'substring'}, + base_path='/os-hypervisors/substring/search' + ) + + @mock.patch('openstack.utils.supports_microversion', autospec=True, + return_value=True) + def test_hypervisors_search_after_253(self, sm): + self.verify_list( + self.proxy.hypervisors, + hypervisor.Hypervisor, + method_kwargs={'hypervisor_hostname_pattern': 'substring'}, + base_path=None, + expected_kwargs={'hypervisor_hostname_pattern': 'substring'} + ) + + def test_find_hypervisor_detail(self): + self.verify_find(self.proxy.find_hypervisor, + hypervisor.Hypervisor, + expected_kwargs={ + 'list_base_path': '/os-hypervisors/detail', + 'ignore_missing': False}) + + def test_find_hypervisor_no_detail(self): + self.verify_find(self.proxy.find_hypervisor, + hypervisor.Hypervisor, + method_kwargs={'details': False}, + expected_kwargs={ + 'list_base_path': None, + 'ignore_missing': False}) + + def test_get_hypervisor(self): + self.verify_get(self.proxy.get_hypervisor, + hypervisor.Hypervisor) + + def test_get_hypervisor_uptime(self): + self._verify( + "openstack.compute.v2.hypervisor.Hypervisor.get_uptime", + self.proxy.get_hypervisor_uptime, + method_args=["value"], + expected_args=[]) + + class TestCompute(TestComputeProxy): def test_extension_find(self): self.verify_find(self.proxy.find_extension, extension.Extension) @@ -870,22 +938,6 @@ def test_server_group_get(self): def test_server_groups(self): self.verify_list(self.proxy.server_groups, server_group.ServerGroup) - def test_hypervisors_not_detailed(self): - self.verify_list(self.proxy.hypervisors, hypervisor.Hypervisor, - method_kwargs={"details": False}) - - def test_hypervisors_detailed(self): - self.verify_list(self.proxy.hypervisors, hypervisor.HypervisorDetail, - method_kwargs={"details": True}) - - def test_find_hypervisor(self): - self.verify_find(self.proxy.find_hypervisor, - hypervisor.Hypervisor) - - def test_get_hypervisor(self): - self.verify_get(self.proxy.get_hypervisor, - hypervisor.Hypervisor) - def test_live_migrate_server(self): self._verify('openstack.compute.v2.server.Server.live_migrate', self.proxy.live_migrate_server, diff --git a/releasenotes/notes/rework-compute-hypervisor-a62f275a0fd1f074.yaml b/releasenotes/notes/rework-compute-hypervisor-a62f275a0fd1f074.yaml new file mode 100644 index 000000000..c82bf6284 --- /dev/null +++ b/releasenotes/notes/rework-compute-hypervisor-a62f275a0fd1f074.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Compute Hypervisor resource and functions are reworked to comply 2.88 + microversion with deprecating misleading attributes. From 39d352977562dc0db66144073caf7f48bba75f85 Mon Sep 17 00:00:00 2001 From: Sebastian Haderecker Date: Thu, 28 Jan 2021 12:30:55 +0100 Subject: [PATCH 2767/3836] Update Open Telekom Cloud vendor docu - Added new eu-nl region - Removed vhd image requirement According to: https://github.com/openstack/openstacksdk/blob/master/openstack/config/vendors/otc.json Change-Id: I2e86b6c33cf0bdaa3e49f2f7ceeb2cb2ca94403d --- doc/source/user/config/vendor-support.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/source/user/config/vendor-support.rst b/doc/source/user/config/vendor-support.rst index 6d3991d7c..41d4a6ca7 100644 --- a/doc/source/user/config/vendor-support.rst +++ b/doc/source/user/config/vendor-support.rst @@ -129,14 +129,14 @@ Open Telekom Cloud https://iam.%(region_name)s.otc.t-systems.com/v3 -============== ================ +============== =================== Region Name Location -============== ================ -eu-de Germany -============== ================ +============== =================== +eu-de Biere/Magdeburg, DE +eu-nl Amsterdam, NL +============== =================== * Identity API Version is 3 -* Images must be in `vhd` format * Public IPv4 is provided via NAT with Neutron Floating IP ELASTX From 4a469fa577eb3f428074e7c4d23334f1e66b623d Mon Sep 17 00:00:00 2001 From: Carlos Goncalves Date: Tue, 15 Sep 2020 21:01:19 +0200 Subject: [PATCH 2768/3836] Add ALPN support to load balancer pools This adds property 'alpn_protocols' to load balancer pools. Change-Id: I89bb1227c7e3820bbe4885e997a8ba6277145f94 --- openstack/load_balancer/v2/pool.py | 4 +++- openstack/tests/unit/load_balancer/test_pool.py | 4 ++++ ...dd-load-balancer-pool-alpn-protocols-77f0c7015f176369.yaml | 3 +++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-load-balancer-pool-alpn-protocols-77f0c7015f176369.yaml diff --git a/openstack/load_balancer/v2/pool.py b/openstack/load_balancer/v2/pool.py index dfc0642d1..2e42b0fca 100644 --- a/openstack/load_balancer/v2/pool.py +++ b/openstack/load_balancer/v2/pool.py @@ -29,12 +29,14 @@ class Pool(resource.Resource, resource.TagMixin): 'health_monitor_id', 'lb_algorithm', 'listener_id', 'loadbalancer_id', 'description', 'name', 'project_id', 'protocol', 'created_at', 'updated_at', 'provisioning_status', 'operating_status', - 'tls_enabled', 'tls_ciphers', 'tls_versions', + 'tls_enabled', 'tls_ciphers', 'tls_versions', 'alpn_protocols', is_admin_state_up='admin_state_up', **resource.TagMixin._tag_query_parameters ) #: Properties + #: List of ALPN protocols. + alpn_protocols = resource.Body('alpn_protocols', type=list) #: Timestamp when the pool was created created_at = resource.Body('created_at') #: Description for the pool. diff --git a/openstack/tests/unit/load_balancer/test_pool.py b/openstack/tests/unit/load_balancer/test_pool.py index 34b313f6b..5bbc0b4e2 100644 --- a/openstack/tests/unit/load_balancer/test_pool.py +++ b/openstack/tests/unit/load_balancer/test_pool.py @@ -38,6 +38,7 @@ 'tls_enabled': True, 'tls_ciphers': 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256', 'tls_versions': ['TLSv1.1', 'TLSv1.2'], + 'alpn_protocols': ['h2', 'http/1.1', 'http/1.0'], } @@ -90,6 +91,8 @@ def test_make_it(self): test_pool.tls_ciphers) self.assertEqual(EXAMPLE['tls_versions'], test_pool.tls_versions) + self.assertEqual(EXAMPLE['alpn_protocols'], + test_pool.alpn_protocols) self.assertDictEqual( {'limit': 'limit', @@ -115,5 +118,6 @@ def test_make_it(self): 'tls_enabled': 'tls_enabled', 'tls_ciphers': 'tls_ciphers', 'tls_versions': 'tls_versions', + 'alpn_protocols': 'alpn_protocols', }, test_pool._query_mapping._mapping) diff --git a/releasenotes/notes/add-load-balancer-pool-alpn-protocols-77f0c7015f176369.yaml b/releasenotes/notes/add-load-balancer-pool-alpn-protocols-77f0c7015f176369.yaml new file mode 100644 index 000000000..fd112b0bd --- /dev/null +++ b/releasenotes/notes/add-load-balancer-pool-alpn-protocols-77f0c7015f176369.yaml @@ -0,0 +1,3 @@ +--- +features: + - Adds ALPN protocols support for the Octavia (load_balancer) pools. From 8442aeab7ed2ac6cc7d496a4908a5f8e1b9e4fc1 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 1 Feb 2021 11:00:14 +0000 Subject: [PATCH 2769/3836] Move 'collections.Mapping' to 'collections.abc' This was moved in Python 3.3 and the alias will be removed in Python 3.10. Change-Id: Id61f186c4b9d0209d45e0a488d7d91b47af6c1b6 Signed-off-by: Stephen Finucane --- openstack/orchestration/util/template_utils.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openstack/orchestration/util/template_utils.py b/openstack/orchestration/util/template_utils.py index d48a2867c..48213bc46 100644 --- a/openstack/orchestration/util/template_utils.py +++ b/openstack/orchestration/util/template_utils.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -import collections +import collections.abc import json from urllib import parse from urllib import request @@ -40,8 +40,7 @@ def get_template_contents(template_file=None, template_url=None, elif template_object: is_object = True template_url = template_object - tpl = object_request and object_request('GET', - template_object) + tpl = object_request and object_request('GET', template_object) elif existing: return {}, None else: @@ -150,7 +149,7 @@ def deep_update(old, new): old = {} for k, v in new.items(): - if isinstance(v, collections.Mapping): + if isinstance(v, collections.abc.Mapping): r = deep_update(old.get(k, {}), v) old[k] = r else: From b25241ccc3f7aa8a81563ba28e49dfc6de72dcf5 Mon Sep 17 00:00:00 2001 From: Ryan Zimmerman Date: Thu, 4 Feb 2021 23:12:11 -0800 Subject: [PATCH 2770/3836] Change microseconds to total_seconds() - Change all instances of response.elapsed.microseconds / 1000 to response.elapsed.total_seconds() * 1000 According to: - http://eavesdrop.openstack.org/irclogs/%23openstack-sdks/%23openstack-sdks.2021-02-03.log.html#t2021-02-03T18:45:14 - https://storyboard.openstack.org/#!/story/2008601 Change-Id: I508335b463b889667c4bdfecad98d91c581fcf45 --- openstack/proxy.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openstack/proxy.py b/openstack/proxy.py index eb2a5fc3d..e135408d2 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -214,7 +214,7 @@ def _report_stats_statsd(self, response, url=None, method=None, exc=None): [self._statsd_prefix, self.service_type, method] + name_parts) if response is not None: - duration = int(response.elapsed.microseconds / 1000) + duration = int(response.elapsed.total_seconds() * 1000) self._statsd_client.timing(key, duration) self._statsd_client.incr(key) elif exc is not None: @@ -235,7 +235,7 @@ def _report_stats_prometheus(self, response, url=None, method=None, ) self._prometheus_counter.labels(**labels).inc() self._prometheus_histogram.labels(**labels).observe( - response.elapsed.microseconds / 1000) + response.elapsed.total_seconds() * 1000) def _report_stats_influxdb(self, response, url=None, method=None, exc=None): @@ -257,7 +257,7 @@ def _report_stats_influxdb(self, response, url=None, method=None, attempted=1 ) if response is not None: - fields['duration'] = int(response.elapsed.microseconds / 1000) + fields['duration'] = int(response.elapsed.total_seconds() * 1000) tags['status_code'] = str(response.status_code) # Note(gtema): emit also status_code as a value (counter) fields[str(response.status_code)] = 1 From 10a1ee452b18b8112a5d66219fab1c97a7d2a9bf Mon Sep 17 00:00:00 2001 From: anuradha1904 Date: Mon, 28 Dec 2020 23:43:47 +0530 Subject: [PATCH 2771/3836] Support Deploy Templates for Ironic API 1. Create Deploy Template 2. List Deploy Templates 3. Show Deploy Template Details 4. Update a Deploy Template 5. Delete Deploy Template Story: 2008193 Task: 40957 Change-Id: I0636d810f3dac03ba18a9d5c09cda415333f80e8 --- doc/source/user/proxies/baremetal.rst | 8 + doc/source/user/resources/baremetal/index.rst | 1 + .../baremetal/v1/deploy_templates.rst | 13 ++ openstack/baremetal/v1/_proxy.py | 106 ++++++++++ openstack/baremetal/v1/deploy_templates.py | 51 +++++ openstack/tests/functional/baremetal/base.py | 13 ++ .../test_baremetal_deploy_templates.py | 190 ++++++++++++++++++ .../baremetal/v1/test_deploy_templates.py | 75 +++++++ ...loy-template-support-fa56005365ed6e4d.yaml | 4 + 9 files changed, 461 insertions(+) create mode 100644 doc/source/user/resources/baremetal/v1/deploy_templates.rst create mode 100644 openstack/baremetal/v1/deploy_templates.py create mode 100644 openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py create mode 100644 openstack/tests/unit/baremetal/v1/test_deploy_templates.py create mode 100644 releasenotes/notes/ironic-deploy-template-support-fa56005365ed6e4d.yaml diff --git a/doc/source/user/proxies/baremetal.rst b/doc/source/user/proxies/baremetal.rst index 350757cd8..d8978c0ff 100644 --- a/doc/source/user/proxies/baremetal.rst +++ b/doc/source/user/proxies/baremetal.rst @@ -77,6 +77,14 @@ Volume Target Operations create_volume_target, update_volume_target, patch_volume_target, delete_volume_target +Deploy Template Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. autoclass:: openstack.baremetal.v1._proxy.Proxy + :noindex: + :members: deploy_templates, get_deploy_template, + create_deploy_template, update_deploy_template, + patch_deploy_template, delete_deploy_template + Utilities --------- diff --git a/doc/source/user/resources/baremetal/index.rst b/doc/source/user/resources/baremetal/index.rst index 7ccb9a292..1b284f304 100644 --- a/doc/source/user/resources/baremetal/index.rst +++ b/doc/source/user/resources/baremetal/index.rst @@ -12,3 +12,4 @@ Baremetal Resources v1/allocation v1/volume_connector v1/volume_target + v1/deploy_templates diff --git a/doc/source/user/resources/baremetal/v1/deploy_templates.rst b/doc/source/user/resources/baremetal/v1/deploy_templates.rst new file mode 100644 index 000000000..e55e63afa --- /dev/null +++ b/doc/source/user/resources/baremetal/v1/deploy_templates.rst @@ -0,0 +1,13 @@ +openstack.baremetal.v1.deploy_templates +======================================= + +.. automodule:: openstack.baremetal.v1.deploy_templates + +The DeployTemplate Class +------------------------- + +The ``DeployTemplate`` class inherits +from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.baremetal.v1.deploy_templates.DeployTemplate + :members: diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 356d8ee40..b395dbc6d 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -19,6 +19,7 @@ from openstack.baremetal.v1 import port_group as _portgroup from openstack.baremetal.v1 import volume_connector as _volumeconnector from openstack.baremetal.v1 import volume_target as _volumetarget +from openstack.baremetal.v1 import deploy_templates as _deploytemplates from openstack import exceptions from openstack import proxy from openstack import utils @@ -1309,3 +1310,108 @@ def delete_volume_target(self, volume_target, """ return self._delete(_volumetarget.VolumeTarget, volume_target, ignore_missing=ignore_missing) + + def deploy_templates(self, details=False, **query): + """Retrieve a generator of deploy_templates. + + :param details: A boolean indicating whether the detailed information + for every deploy_templates should be returned. + :param dict query: Optional query parameters to be sent to + restrict the deploy_templates to be returned. + + :returns: A generator of Deploy templates instances. + """ + if details: + query['detail'] = True + return _deploytemplates.DeployTemplate.list(self, **query) + + def create_deploy_template(self, **attrs): + """Create a new deploy_template from attributes. + + :param dict attrs: Keyword arguments that will be used to create a + :class:`~openstack.baremetal.v1.deploy_templates.DeployTemplate`. + + :returns: The results of deploy_template creation. + :rtype: + :class:`~openstack.baremetal.v1.deploy_templates.DeployTemplate`. + """ + return self._create(_deploytemplates.DeployTemplate, **attrs) + + def update_deploy_template(self, deploy_template, **attrs): + """Update a deploy_template. + + :param deploy_template: Either the ID of a deploy_template, + or an instance of + :class:`~openstack.baremetal.v1.deploy_templates.DeployTemplate`. + :param dict attrs: The attributes to update on + the deploy_template represented + by the ``deploy_template`` parameter. + + :returns: The updated deploy_template. + :rtype::class: + `~openstack.baremetal.v1.deploy_templates.DeployTemplate` + """ + return self._update(_deploytemplates.DeployTemplate, + deploy_template, **attrs) + + def delete_deploy_template(self, deploy_template, + ignore_missing=True): + """Delete a deploy_template. + + :param deploy_template:The value can be + either the ID of a deploy_template or a + :class:`~openstack.baremetal.v1.deploy_templates.DeployTemplate` + instance. + + :param bool ignore_missing: When set to ``False``, + an exception:class:`~openstack.exceptions.ResourceNotFound` + will be raised when the deploy_template + could not be found. + When set to ``True``, no + exception will be raised when attempting + to delete a non-existent + deploy_template. + + :returns: The instance of the deploy_template which was deleted. + :rtype::class: + `~openstack.baremetal.v1.deploy_templates.DeployTemplate`. + """ + + return self._delete(_deploytemplates.DeployTemplate, + deploy_template, ignore_missing=ignore_missing) + + def get_deploy_template(self, deploy_template, fields=None): + """Get a specific deployment template. + + :param deploy_template: The value can be the name or ID + of a deployment template + :class:`~openstack.baremetal.v1.deploy_templates.DeployTemplate` + instance. + + :param fields: Limit the resource fields to fetch. + + :returns: One + :class:`~openstack.baremetal.v1.deploy_templates.DeployTemplate` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no deployment template matching the name or + ID could be found. + """ + return self._get_with_fields(_deploytemplates.DeployTemplate, + deploy_template, fields=fields) + + def patch_deploy_template(self, deploy_template, patch): + """Apply a JSON patch to the deploy_templates. + + :param deploy_templates: The value can be the ID of a + deploy_template or a + :class:`~openstack.baremetal.v1.deploy_templates.DeployTemplate` + instance. + + :param patch: JSON patch to apply. + + :returns: The updated deploy_template. + :rtype::class: + `~openstack.baremetal.v1.deploy_templates.DeployTemplate` + """ + return self._get_resource(_deploytemplates.DeployTemplate, + deploy_template).patch(self, patch) diff --git a/openstack/baremetal/v1/deploy_templates.py b/openstack/baremetal/v1/deploy_templates.py new file mode 100644 index 000000000..3c9e40420 --- /dev/null +++ b/openstack/baremetal/v1/deploy_templates.py @@ -0,0 +1,51 @@ +# 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.baremetal.v1 import _common +from openstack import resource + + +class DeployTemplate(_common.ListMixin, resource.Resource): + + resources_key = 'deploy_templates' + base_path = '/deploy_templates' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + allow_patch = True + commit_method = 'PATCH' + commit_jsonpatch = True + + _query_mapping = resource.QueryParameters( + 'detail', + fields={'type': _common.fields_type}, + ) + + # Deploy Templates is available since 1.55 + _max_microversion = '1.55' + name = resource.Body('name') + #: Timestamp at which the deploy_template was created. + created_at = resource.Body('created_at') + #: A set of one or more arbitrary metadata key and value pairs. + extra = resource.Body('extra') + #: A list of relative links. Includes the self and bookmark links. + links = resource.Body('links', type=list) + #: A set of physical information of the deploy_template. + steps = resource.Body('steps', type=list) + #: Timestamp at which the deploy_template was last updated. + updated_at = resource.Body('updated_at') + #: The UUID of the resource. + id = resource.Body('uuid', alternate_id=True) diff --git a/openstack/tests/functional/baremetal/base.py b/openstack/tests/functional/baremetal/base.py index 8dc00044d..948bc9dcb 100644 --- a/openstack/tests/functional/baremetal/base.py +++ b/openstack/tests/functional/baremetal/base.py @@ -84,3 +84,16 @@ def create_volume_target(self, node_id=None, **kwargs): self.conn.baremetal.delete_volume_target(volume_target.id, ignore_missing=True)) return volume_target + + def create_deploy_template(self, **kwargs): + """Create a new deploy_template from attributes. + """ + + deploy_template = self.conn.baremetal.create_deploy_template( + **kwargs) + + self.addCleanup( + lambda: self.conn.baremetal.delete_deploy_template( + deploy_template.id, + ignore_missing=True)) + return deploy_template diff --git a/openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py b/openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py new file mode 100644 index 000000000..eb59056dc --- /dev/null +++ b/openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py @@ -0,0 +1,190 @@ +# 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.tests.functional.baremetal import base + + +class TestBareMetalDeployTemplate(base.BaseBaremetalTest): + + min_microversion = '1.55' + + def setUp(self): + super(TestBareMetalDeployTemplate, self).setUp() + + def test_baremetal_deploy_create_get_delete(self): + steps = [ + { + "interface": "bios", + "step": "apply_configuration", + "args": { + "settings": [ + { + "name": "LogicalProc", + "value": "Enabled" + } + ] + }, + "priority": 150 + } + ] + deploy_template = self.create_deploy_template( + name='CUSTOM_DEPLOY_TEMPLATE', + steps=steps) + loaded = self.conn.baremetal.get_deploy_template( + deploy_template.id) + self.assertEqual(loaded.id, deploy_template.id) + self.conn.baremetal.delete_deploy_template(deploy_template, + ignore_missing=False) + self.assertRaises(exceptions.ResourceNotFound, + self.conn.baremetal.get_deploy_template, + deploy_template.id) + + def test_baremetal_deploy_template_list(self): + steps = [ + { + "interface": "bios", + "step": "apply_configuration", + "args": { + "settings": [ + { + "name": "LogicalProc", + "value": "Enabled" + } + ] + }, + "priority": 150 + } + ] + + deploy_template1 = self.create_deploy_template( + name='CUSTOM_DEPLOY_TEMPLATE1', + steps=steps) + deploy_template2 = self.create_deploy_template( + name='CUSTOM_DEPLOY_TEMPLATE2', + steps=steps) + deploy_templates = self.conn.baremetal.deploy_templates() + ids = [template.id for template in deploy_templates] + self.assertIn(deploy_template1.id, ids) + self.assertIn(deploy_template2.id, ids) + + deploy_templates_with_details = self.conn.baremetal.deploy_templates( + details=True) + for dp in deploy_templates_with_details: + self.assertIsNotNone(dp.id) + self.assertIsNotNone(dp.name) + + deploy_tempalte_with_fields = self.conn.baremetal.deploy_templates( + fields=['uuid']) + for dp in deploy_tempalte_with_fields: + self.assertIsNotNone(dp.id) + self.assertIsNone(dp.name) + + def test_baremetal_deploy_list_update_delete(self): + steps = [ + { + "interface": "bios", + "step": "apply_configuration", + "args": { + "settings": [ + { + "name": "LogicalProc", + "value": "Enabled" + } + ] + }, + "priority": 150 + } + ] + deploy_template = self.create_deploy_template( + name='CUSTOM_DEPLOY_TEMPLATE4', + steps=steps) + self.assertFalse(deploy_template.extra) + deploy_template.extra = {'answer': 42} + + deploy_template = self.conn.baremetal.update_deploy_template( + deploy_template) + self.assertEqual({'answer': 42}, deploy_template.extra) + + deploy_template = self.conn.baremetal.get_deploy_template( + deploy_template.id) + + self.conn.baremetal.delete_deploy_template(deploy_template.id, + ignore_missing=False) + + def test_baremetal_deploy_update(self): + steps = [ + { + "interface": "bios", + "step": "apply_configuration", + "args": { + "settings": [ + { + "name": "LogicalProc", + "value": "Enabled" + } + ] + }, + "priority": 150 + } + ] + deploy_template = self.create_deploy_template( + name='CUSTOM_DEPLOY_TEMPLATE4', + steps=steps) + deploy_template.extra = {'answer': 42} + + deploy_template = self.conn.baremetal.update_deploy_template( + deploy_template) + self.assertEqual({'answer': 42}, deploy_template.extra) + + deploy_template = self.conn.baremetal.get_deploy_template( + deploy_template.id) + self.assertEqual({'answer': 42}, deploy_template.extra) + + def test_deploy_template_patch(self): + name = "CUSTOM_HYPERTHREADING_ON" + steps = [ + { + "interface": "bios", + "step": "apply_configuration", + "args": { + "settings": [ + { + "name": "LogicalProc", + "value": "Enabled" + } + ] + }, + "priority": 150 + } + ] + deploy_template = self.create_deploy_template( + name=name, + steps=steps) + deploy_template = self.conn.baremetal.patch_deploy_template( + deploy_template, dict(path='/extra/answer', op='add', value=42)) + self.assertEqual({'answer': 42}, deploy_template.extra) + self.assertEqual(name, + deploy_template.name) + + deploy_template = self.conn.baremetal.get_deploy_template( + deploy_template.id) + self.assertEqual({'answer': 42}, deploy_template.extra) + + def test_deploy_template_negative_non_existing(self): + uuid = "bbb45f41-d4bc-4307-8d1d-32f95ce1e920" + self.assertRaises(exceptions.ResourceNotFound, + self.conn.baremetal.get_deploy_template, uuid) + self.assertRaises(exceptions.ResourceNotFound, + self.conn.baremetal.delete_deploy_template, uuid, + ignore_missing=False) + self.assertIsNone(self.conn.baremetal.delete_deploy_template(uuid)) diff --git a/openstack/tests/unit/baremetal/v1/test_deploy_templates.py b/openstack/tests/unit/baremetal/v1/test_deploy_templates.py new file mode 100644 index 000000000..0b826f425 --- /dev/null +++ b/openstack/tests/unit/baremetal/v1/test_deploy_templates.py @@ -0,0 +1,75 @@ +# 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.tests.unit import base + +from openstack.baremetal.v1 import deploy_templates + +FAKE = { + "created_at": "2016-08-18T22:28:48.643434+11:11", + "extra": {}, + "links": [ + { + "href": """http://10.60.253.180:6385/v1/deploy_templates + /bbb45f41-d4bc-4307-8d1d-32f95ce1e920""", + "rel": "self" + }, + { + "href": """http://10.60.253.180:6385/deploy_templates + /bbb45f41-d4bc-4307-8d1d-32f95ce1e920""", + "rel": "bookmark" + } + ], + "name": "CUSTOM_HYPERTHREADING_ON", + "steps": [ + { + "args": { + "settings": [ + { + "name": "LogicalProc", + "value": "Enabled" + } + ] + }, + "interface": "bios", + "priority": 150, + "step": "apply_configuration" + } + ], + "updated_at": None, + "uuid": "bbb45f41-d4bc-4307-8d1d-32f95ce1e920" +} + + +class DeployTemplates(base.TestCase): + + def test_basic(self): + sot = deploy_templates.DeployTemplate() + self.assertIsNone(sot.resource_key) + self.assertEqual('deploy_templates', sot.resources_key) + self.assertEqual('/deploy_templates', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertEqual('PATCH', sot.commit_method) + + def test_instantiate(self): + sot = deploy_templates.DeployTemplate(**FAKE) + self.assertEqual(FAKE['steps'], sot.steps) + self.assertEqual(FAKE['created_at'], sot.created_at) + self.assertEqual(FAKE['extra'], sot.extra) + self.assertEqual(FAKE['links'], sot.links) + self.assertEqual(FAKE['name'], sot.name) + self.assertEqual(FAKE['updated_at'], sot.updated_at) + self.assertEqual(FAKE['uuid'], sot.id) diff --git a/releasenotes/notes/ironic-deploy-template-support-fa56005365ed6e4d.yaml b/releasenotes/notes/ironic-deploy-template-support-fa56005365ed6e4d.yaml new file mode 100644 index 000000000..d8b13ea7a --- /dev/null +++ b/releasenotes/notes/ironic-deploy-template-support-fa56005365ed6e4d.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Support Deploy Templates for Ironic API \ No newline at end of file From 63c5f8a9beeb2c004792597f0545052d3dc2fea3 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 11 Feb 2021 16:57:03 +0100 Subject: [PATCH 2772/3836] Drop .json suffix from networking URLs As a preparation for a major refactoring of rebasing on the proxy layer for the networking functions of the cloud layer drop the obsole .json suffix from the URLs. Change-Id: I3b4506b81cc5e9f04e99cc22bb7e78d0bbf0c48c --- openstack/cloud/_floating_ip.py | 10 +- openstack/cloud/_network.py | 80 +++++------ .../tests/unit/cloud/test_baremetal_node.py | 6 +- openstack/tests/unit/cloud/test_caching.py | 2 +- .../tests/unit/cloud/test_create_server.py | 58 ++++---- .../tests/unit/cloud/test_delete_server.py | 8 +- .../unit/cloud/test_floating_ip_neutron.py | 136 +++++++++--------- openstack/tests/unit/cloud/test_fwaas.py | 4 +- openstack/tests/unit/cloud/test_meta.py | 54 +++---- openstack/tests/unit/cloud/test_network.py | 32 ++--- openstack/tests/unit/cloud/test_port.py | 36 ++--- .../cloud/test_qos_bandwidth_limit_rule.py | 86 +++++------ .../unit/cloud/test_qos_dscp_marking_rule.py | 60 ++++---- .../cloud/test_qos_minimum_bandwidth_rule.py | 60 ++++---- openstack/tests/unit/cloud/test_qos_policy.py | 68 ++++----- .../tests/unit/cloud/test_qos_rule_type.py | 18 +-- openstack/tests/unit/cloud/test_quotas.py | 8 +- .../tests/unit/cloud/test_rebuild_server.py | 8 +- openstack/tests/unit/cloud/test_router.py | 42 +++--- openstack/tests/unit/cloud/test_shade.py | 12 +- openstack/tests/unit/cloud/test_subnet.py | 54 +++---- .../tests/unit/cloud/test_update_server.py | 2 +- 22 files changed, 422 insertions(+), 422 deletions(-) diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index 1f2a2b3a7..bca6cdd5e 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -82,7 +82,7 @@ def search_floating_ips(self, id=None, filters=None): def _neutron_list_floating_ips(self, filters=None): if not filters: filters = {} - data = self.network.get('/floatingips.json', params=filters) + data = self.network.get('/floatingips', params=filters) return self._get_and_munchify('floatingips', data) def _nova_list_floating_ips(self): @@ -462,7 +462,7 @@ def create_floating_ip(self, network=None, server=None, def _submit_create_fip(self, kwargs): # Split into a method to aid in test mocking data = self.network.post( - "/floatingips.json", json={"floatingip": kwargs}) + "/floatingips", json={"floatingip": kwargs}) return self._normalize_floating_ip( self._get_and_munchify('floatingip', data)) @@ -613,7 +613,7 @@ def _delete_floating_ip(self, floating_ip_id): def _neutron_delete_floating_ip(self, floating_ip_id): try: proxy._json_response(self.network.delete( - "/floatingips/{fip_id}.json".format(fip_id=floating_ip_id), + "/floatingips/{fip_id}".format(fip_id=floating_ip_id), error_message="unable to delete floating IP")) except exc.OpenStackCloudResourceNotFound: return False @@ -753,7 +753,7 @@ def _neutron_attach_ip_to_server( return proxy._json_response( self.network.put( - "/floatingips/{fip_id}.json".format(fip_id=floating_ip['id']), + "/floatingips/{fip_id}".format(fip_id=floating_ip['id']), json={'floatingip': floating_ip_args}), error_message=("Error attaching IP {ip} to " "server {server_id}".format( @@ -811,7 +811,7 @@ def _neutron_detach_ip_from_server(self, server_id, floating_ip_id): return False exceptions.raise_from_response( self.network.put( - "/floatingips/{fip_id}.json".format(fip_id=floating_ip_id), + "/floatingips/{fip_id}".format(fip_id=floating_ip_id), json={"floatingip": {"port_id": None}}), error_message=("Error detaching IP {ip} from " "server {server_id}".format( diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index 58c2844c9..06ad076d8 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -34,7 +34,7 @@ def __init__(self): @_utils.cache_on_arguments() def _neutron_extensions(self): extensions = set() - resp = self.network.get('/extensions.json') + resp = self.network.get('/extensions') data = proxy._json_response( resp, error_message="Error fetching extension list for neutron") @@ -128,7 +128,7 @@ def list_networks(self, filters=None): # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} - data = self.network.get("/networks.json", params=filters) + data = self.network.get("/networks", params=filters) return self._get_and_munchify('networks', data) def list_routers(self, filters=None): @@ -144,7 +144,7 @@ def list_routers(self, filters=None): # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} - resp = self.network.get("/routers.json", params=filters) + resp = self.network.get("/routers", params=filters) data = proxy._json_response( resp, error_message="Error fetching router list") @@ -163,7 +163,7 @@ def list_subnets(self, filters=None): # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} - data = self.network.get("/subnets.json", params=filters) + data = self.network.get("/subnets", params=filters) return self._get_and_munchify('subnets', data) def list_ports(self, filters=None): @@ -203,7 +203,7 @@ def _list_ports(self, filters): # If the cloud is running nova-network, just return an empty list. if not self.has_service('network'): return [] - resp = self.network.get("/ports.json", params=filters) + resp = self.network.get("/ports", params=filters) data = proxy._json_response( resp, error_message="Error fetching port list") @@ -264,7 +264,7 @@ def list_qos_rule_types(self, filters=None): # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} - resp = self.network.get("/qos/rule-types.json", params=filters) + resp = self.network.get("/qos/rule-types", params=filters) data = proxy._json_response( resp, error_message="Error fetching QoS rule types list") @@ -289,7 +289,7 @@ def get_qos_rule_type_details(self, rule_type, filters=None): 'on target cloud') resp = self.network.get( - "/qos/rule-types/{rule_type}.json".format(rule_type=rule_type)) + "/qos/rule-types/{rule_type}".format(rule_type=rule_type)) data = proxy._json_response( resp, error_message="Error fetching QoS details of {rule_type} " @@ -309,7 +309,7 @@ def list_qos_policies(self, filters=None): # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} - resp = self.network.get("/qos/policies.json", params=filters) + resp = self.network.get("/qos/policies", params=filters) data = proxy._json_response( resp, error_message="Error fetching QoS policies list") @@ -539,7 +539,7 @@ def create_network(self, name, shared=False, admin_state_up=True, if dns_domain: network['dns_domain'] = dns_domain - data = self.network.post("/networks.json", json={'network': network}) + data = self.network.post("/networks", json={'network': network}) # Reset cache so the new network is picked up self._reset_network_caches() @@ -603,7 +603,7 @@ def update_network(self, name_or_id, **kwargs): "Network %s not found." % name_or_id) data = proxy._json_response(self.network.put( - "/networks/{net_id}.json".format(net_id=network.id), + "/networks/{net_id}".format(net_id=network.id), json={"network": kwargs}), error_message="Error updating network {0}".format(name_or_id)) @@ -626,7 +626,7 @@ def delete_network(self, name_or_id): return False exceptions.raise_from_response(self.network.delete( - "/networks/{network_id}.json".format(network_id=network['id']))) + "/networks/{network_id}".format(network_id=network['id']))) # Reset cache so the deleted network is removed self._reset_network_caches() @@ -649,7 +649,7 @@ def set_network_quotas(self, name_or_id, **kwargs): exceptions.raise_from_response( self.network.put( - '/quotas/{project_id}.json'.format(project_id=proj.id), + '/quotas/{project_id}'.format(project_id=proj.id), json={'quota': kwargs}), error_message=("Error setting Neutron's quota for " "project {0}".format(proj.id))) @@ -670,7 +670,7 @@ def get_network_quotas(self, name_or_id, details=False): url = '/quotas/{project_id}'.format(project_id=proj.id) if details: url = url + "/details" - url = url + ".json" + url = url + "" data = proxy._json_response( self.network.get(url), error_message=("Error fetching Neutron's quota for " @@ -698,7 +698,7 @@ def delete_network_quotas(self, name_or_id): raise exc.OpenStackCloudException("project does not exist") exceptions.raise_from_response( self.network.delete( - '/quotas/{project_id}.json'.format(project_id=proj.id)), + '/quotas/{project_id}'.format(project_id=proj.id)), error_message=("Error deleting Neutron's quota for " "project {0}".format(proj.id))) @@ -1194,7 +1194,7 @@ def create_qos_policy(self, **kwargs): self.log.debug("'qos-default' extension is not available on " "target cloud") - data = self.network.post("/qos/policies.json", json={'policy': kwargs}) + data = self.network.post("/qos/policies", json={'policy': kwargs}) return self._get_and_munchify('policy', data) @_utils.valid_kwargs("name", "description", "shared", "default", @@ -1238,7 +1238,7 @@ def update_qos_policy(self, name_or_id, **kwargs): "QoS policy %s not found." % name_or_id) data = self.network.put( - "/qos/policies/{policy_id}.json".format( + "/qos/policies/{policy_id}".format( policy_id=curr_policy['id']), json={'policy': kwargs}) return self._get_and_munchify('policy', data) @@ -1261,7 +1261,7 @@ def delete_qos_policy(self, name_or_id): return False exceptions.raise_from_response(self.network.delete( - "/qos/policies/{policy_id}.json".format(policy_id=policy['id']))) + "/qos/policies/{policy_id}".format(policy_id=policy['id']))) return True @@ -1310,7 +1310,7 @@ def list_qos_bandwidth_limit_rules(self, policy_name_or_id, filters=None): filters = {} resp = self.network.get( - "/qos/policies/{policy_id}/bandwidth_limit_rules.json".format( + "/qos/policies/{policy_id}/bandwidth_limit_rules".format( policy_id=policy['id']), params=filters) data = proxy._json_response( @@ -1341,7 +1341,7 @@ def get_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): name_or_id=policy_name_or_id)) resp = self.network.get( - "/qos/policies/{policy_id}/bandwidth_limit_rules/{rule_id}.json". + "/qos/policies/{policy_id}/bandwidth_limit_rules/{rule_id}". format(policy_id=policy['id'], rule_id=rule_id)) data = proxy._json_response( resp, @@ -1437,7 +1437,7 @@ def update_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id, policy_id=policy['id'])) data = self.network.put( - "/qos/policies/{policy_id}/bandwidth_limit_rules/{rule_id}.json". + "/qos/policies/{policy_id}/bandwidth_limit_rules/{rule_id}". format(policy_id=policy['id'], rule_id=rule_id), json={'bandwidth_limit_rule': kwargs}) return self._get_and_munchify('bandwidth_limit_rule', data) @@ -1463,7 +1463,7 @@ def delete_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): try: exceptions.raise_from_response(self.network.delete( - "/qos/policies/{policy}/bandwidth_limit_rules/{rule}.json". + "/qos/policies/{policy}/bandwidth_limit_rules/{rule}". format(policy=policy['id'], rule=rule_id))) except exc.OpenStackCloudURINotFound: self.log.debug( @@ -1519,7 +1519,7 @@ def list_qos_dscp_marking_rules(self, policy_name_or_id, filters=None): filters = {} resp = self.network.get( - "/qos/policies/{policy_id}/dscp_marking_rules.json".format( + "/qos/policies/{policy_id}/dscp_marking_rules".format( policy_id=policy['id']), params=filters) data = proxy._json_response( @@ -1550,7 +1550,7 @@ def get_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): name_or_id=policy_name_or_id)) resp = self.network.get( - "/qos/policies/{policy_id}/dscp_marking_rules/{rule_id}.json". + "/qos/policies/{policy_id}/dscp_marking_rules/{rule_id}". format(policy_id=policy['id'], rule_id=rule_id)) data = proxy._json_response( resp, @@ -1624,7 +1624,7 @@ def update_qos_dscp_marking_rule(self, policy_name_or_id, rule_id, policy_id=policy['id'])) data = self.network.put( - "/qos/policies/{policy_id}/dscp_marking_rules/{rule_id}.json". + "/qos/policies/{policy_id}/dscp_marking_rules/{rule_id}". format(policy_id=policy['id'], rule_id=rule_id), json={'dscp_marking_rule': kwargs}) return self._get_and_munchify('dscp_marking_rule', data) @@ -1650,7 +1650,7 @@ def delete_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): try: exceptions.raise_from_response(self.network.delete( - "/qos/policies/{policy}/dscp_marking_rules/{rule}.json". + "/qos/policies/{policy}/dscp_marking_rules/{rule}". format(policy=policy['id'], rule=rule_id))) except exc.OpenStackCloudURINotFound: self.log.debug( @@ -1708,7 +1708,7 @@ def list_qos_minimum_bandwidth_rules(self, policy_name_or_id, filters = {} resp = self.network.get( - "/qos/policies/{policy_id}/minimum_bandwidth_rules.json".format( + "/qos/policies/{policy_id}/minimum_bandwidth_rules".format( policy_id=policy['id']), params=filters) data = proxy._json_response( @@ -1739,7 +1739,7 @@ def get_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): name_or_id=policy_name_or_id)) resp = self.network.get( - "/qos/policies/{policy_id}/minimum_bandwidth_rules/{rule_id}.json". + "/qos/policies/{policy_id}/minimum_bandwidth_rules/{rule_id}". format(policy_id=policy['id'], rule_id=rule_id)) data = proxy._json_response( resp, @@ -1817,7 +1817,7 @@ def update_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id, policy_id=policy['id'])) data = self.network.put( - "/qos/policies/{policy_id}/minimum_bandwidth_rules/{rule_id}.json". + "/qos/policies/{policy_id}/minimum_bandwidth_rules/{rule_id}". format(policy_id=policy['id'], rule_id=rule_id), json={'minimum_bandwidth_rule': kwargs}) return self._get_and_munchify('minimum_bandwidth_rule', data) @@ -1843,7 +1843,7 @@ def delete_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): try: exceptions.raise_from_response(self.network.delete( - "/qos/policies/{policy}/minimum_bandwidth_rules/{rule}.json". + "/qos/policies/{policy}/minimum_bandwidth_rules/{rule}". format(policy=policy['id'], rule=rule_id))) except exc.OpenStackCloudURINotFound: self.log.debug( @@ -1878,7 +1878,7 @@ def add_router_interface(self, router, subnet_id=None, port_id=None): return proxy._json_response( self.network.put( - "/routers/{router_id}/add_router_interface.json".format( + "/routers/{router_id}/add_router_interface".format( router_id=router['id']), json=json_body), error_message="Error attaching interface to router {0}".format( @@ -1913,7 +1913,7 @@ def remove_router_interface(self, router, subnet_id=None, port_id=None): exceptions.raise_from_response( self.network.put( - "/routers/{router_id}/remove_router_interface.json".format( + "/routers/{router_id}/remove_router_interface".format( router_id=router['id']), json=json_body), error_message="Error detaching interface from router {0}".format( @@ -2001,7 +2001,7 @@ def create_router(self, name=None, admin_state_up=True, router['availability_zone_hints'] = availability_zone_hints data = proxy._json_response( - self.network.post("/routers.json", json={"router": router}), + self.network.post("/routers", json={"router": router}), error_message="Error creating router {0}".format(name)) return self._get_and_munchify('router', data) @@ -2069,7 +2069,7 @@ def update_router(self, name_or_id, name=None, admin_state_up=None, "Router %s not found." % name_or_id) resp = self.network.put( - "/routers/{router_id}.json".format(router_id=curr_router['id']), + "/routers/{router_id}".format(router_id=curr_router['id']), json={"router": router}) data = proxy._json_response( resp, @@ -2095,7 +2095,7 @@ def delete_router(self, name_or_id): return False exceptions.raise_from_response(self.network.delete( - "/routers/{router_id}.json".format(router_id=router['id']), + "/routers/{router_id}".format(router_id=router['id']), error_message="Error deleting router {0}".format(name_or_id))) return True @@ -2245,7 +2245,7 @@ def create_subnet(self, network_name_or_id, cidr=None, ip_version=4, if use_default_subnetpool: subnet['use_default_subnetpool'] = True - response = self.network.post("/subnets.json", json={"subnet": subnet}) + response = self.network.post("/subnets", json={"subnet": subnet}) return self._get_and_munchify('subnet', response) @@ -2268,7 +2268,7 @@ def delete_subnet(self, name_or_id): return False exceptions.raise_from_response(self.network.delete( - "/subnets/{subnet_id}.json".format(subnet_id=subnet['id']))) + "/subnets/{subnet_id}".format(subnet_id=subnet['id']))) return True def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, @@ -2354,7 +2354,7 @@ def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, "Subnet %s not found." % name_or_id) response = self.network.put( - "/subnets/{subnet_id}.json".format(subnet_id=curr_subnet['id']), + "/subnets/{subnet_id}".format(subnet_id=curr_subnet['id']), json={"subnet": subnet}) return self._get_and_munchify('subnet', response) @@ -2424,7 +2424,7 @@ def create_port(self, network_id, **kwargs): kwargs['network_id'] = network_id data = proxy._json_response( - self.network.post("/ports.json", json={'port': kwargs}), + self.network.post("/ports", json={'port': kwargs}), error_message="Error creating port for network {0}".format( network_id)) return self._get_and_munchify('port', data) @@ -2496,7 +2496,7 @@ def update_port(self, name_or_id, **kwargs): data = proxy._json_response( self.network.put( - "/ports/{port_id}.json".format(port_id=port['id']), + "/ports/{port_id}".format(port_id=port['id']), json={"port": kwargs}), error_message="Error updating port {0}".format(name_or_id)) return self._get_and_munchify('port', data) @@ -2517,7 +2517,7 @@ def delete_port(self, name_or_id): exceptions.raise_from_response( self.network.delete( - "/ports/{port_id}.json".format(port_id=port['id'])), + "/ports/{port_id}".format(port_id=port['id'])), error_message="Error deleting port {0}".format(name_or_id)) return True diff --git a/openstack/tests/unit/cloud/test_baremetal_node.py b/openstack/tests/unit/cloud/test_baremetal_node.py index 794208a86..09bf80d2e 100644 --- a/openstack/tests/unit/cloud/test_baremetal_node.py +++ b/openstack/tests/unit/cloud/test_baremetal_node.py @@ -1656,7 +1656,7 @@ def test_attach_port_to_machine(self): method='GET', uri=self.get_mock_url( service_type='network', - resource='ports.json', + resource='ports', base_url_append='v2.0'), json={'ports': [{'id': vif_id}]}), dict( @@ -1682,7 +1682,7 @@ def test_detach_port_from_machine(self): method='GET', uri=self.get_mock_url( service_type='network', - resource='ports.json', + resource='ports', base_url_append='v2.0'), json={'ports': [{'id': vif_id}]}), dict( @@ -1716,7 +1716,7 @@ def test_list_ports_attached_to_machine(self): method='GET', uri=self.get_mock_url( service_type='network', - resource='ports.json', + resource='ports', base_url_append='v2.0'), json={'ports': [fake_port]}), ]) diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py index bf5dabd44..97f527fee 100644 --- a/openstack/tests/unit/cloud/test_caching.py +++ b/openstack/tests/unit/cloud/test_caching.py @@ -557,7 +557,7 @@ def test_list_ports_filtered(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json']), + 'network', 'public', append=['v2.0', 'ports']), json={'ports': [ down_port, active_port, diff --git a/openstack/tests/unit/cloud/test_create_server.py b/openstack/tests/unit/cloud/test_create_server.py index f773c9f13..1da26b147 100644 --- a/openstack/tests/unit/cloud/test_create_server.py +++ b/openstack/tests/unit/cloud/test_create_server.py @@ -38,7 +38,7 @@ def test_create_server_with_get_exception(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), dict(method='POST', uri=self.get_mock_url( @@ -71,7 +71,7 @@ def test_create_server_with_server_error(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), dict(method='POST', uri=self.get_mock_url( @@ -104,7 +104,7 @@ def test_create_server_wait_server_error(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), dict(method='POST', uri=self.get_mock_url( @@ -144,7 +144,7 @@ def test_create_server_with_timeout(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), dict(method='POST', uri=self.get_mock_url( @@ -181,7 +181,7 @@ def test_create_server_no_wait(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), dict(method='POST', uri=self.get_mock_url( @@ -218,7 +218,7 @@ def test_create_server_config_drive(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), dict(method='POST', uri=self.get_mock_url( @@ -257,7 +257,7 @@ def test_create_server_config_drive_none(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), dict(method='POST', uri=self.get_mock_url( @@ -298,7 +298,7 @@ def test_create_server_with_admin_pass_no_wait(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), dict(method='POST', uri=self.get_mock_url( @@ -338,7 +338,7 @@ def test_create_server_with_admin_pass_wait(self, mock_wait): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), dict(method='POST', uri=self.get_mock_url( @@ -385,7 +385,7 @@ def test_create_server_user_data_base64(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), dict(method='POST', uri=self.get_mock_url( @@ -458,7 +458,7 @@ def test_create_server_wait(self, mock_wait): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), dict(method='POST', uri=self.get_mock_url( @@ -497,7 +497,7 @@ def test_create_server_no_addresses( self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), dict(method='POST', uri=self.get_mock_url( @@ -521,7 +521,7 @@ def test_create_server_no_addresses( json={'servers': [fake_server]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json'], + 'network', 'public', append=['v2.0', 'ports'], qs_elements=['device_id=1234']), json={'ports': []}), dict(method='DELETE', @@ -555,7 +555,7 @@ def test_create_server_network_with_no_nics(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': [network]}), dict(method='POST', uri=self.get_mock_url( @@ -575,11 +575,11 @@ def test_create_server_network_with_no_nics(self): json={'server': build_server}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': [network]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets.json']), + 'network', 'public', append=['v2.0', 'subnets']), json={'subnets': []}), ]) self.cloud.create_server( @@ -600,7 +600,7 @@ def test_create_server_network_with_empty_nics(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': [network]}), dict(method='POST', uri=self.get_mock_url( @@ -620,11 +620,11 @@ def test_create_server_network_with_empty_nics(self): json={'server': build_server}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': [network]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets.json']), + 'network', 'public', append=['v2.0', 'subnets']), json={'subnets': []}), ]) self.cloud.create_server( @@ -662,11 +662,11 @@ def test_create_server_network_fixed_ip(self): json={'server': build_server}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': [network]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets.json']), + 'network', 'public', append=['v2.0', 'subnets']), json={'subnets': []}), ]) self.cloud.create_server( @@ -704,11 +704,11 @@ def test_create_server_network_v4_fixed_ip(self): json={'server': build_server}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': [network]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets.json']), + 'network', 'public', append=['v2.0', 'subnets']), json={'subnets': []}), ]) self.cloud.create_server( @@ -748,11 +748,11 @@ def test_create_server_network_v6_fixed_ip(self): json={'server': build_server}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': [network]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets.json']), + 'network', 'public', append=['v2.0', 'subnets']), json={'subnets': []}), ]) self.cloud.create_server( @@ -815,7 +815,7 @@ def test_create_server_get_flavor_image(self): json={'server': active_server}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), ]) @@ -851,7 +851,7 @@ def test_create_server_nics_port_id(self): json={'server': active_server}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), ]) @@ -872,7 +872,7 @@ def test_create_boot_attach_volume(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), dict(method='POST', uri=self.get_mock_url( @@ -924,7 +924,7 @@ def test_create_boot_from_volume_image_terminate(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), dict(method='POST', uri=self.get_mock_url( diff --git a/openstack/tests/unit/cloud/test_delete_server.py b/openstack/tests/unit/cloud/test_delete_server.py index b6424bfb5..6d748387d 100644 --- a/openstack/tests/unit/cloud/test_delete_server.py +++ b/openstack/tests/unit/cloud/test_delete_server.py @@ -163,7 +163,7 @@ def test_delete_server_delete_ips(self): json={'servers': [server]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips.json'], + 'network', 'public', append=['v2.0', 'floatingips'], qs_elements=['floating_ip_address=172.24.5.5']), complete_qs=True, json={'floatingips': [{ @@ -179,10 +179,10 @@ def test_delete_server_delete_ips(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'floatingips', - '{fip_id}.json'.format(fip_id=fip_id)])), + '{fip_id}'.format(fip_id=fip_id)])), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips.json']), + 'network', 'public', append=['v2.0', 'floatingips']), complete_qs=True, json={'floatingips': []}), dict(method='DELETE', @@ -212,7 +212,7 @@ def test_delete_server_delete_ips_bad_neutron(self): json={'servers': [server]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips.json'], + 'network', 'public', append=['v2.0', 'floatingips'], qs_elements=['floating_ip_address=172.24.5.5']), complete_qs=True, status_code=404), diff --git a/openstack/tests/unit/cloud/test_floating_ip_neutron.py b/openstack/tests/unit/cloud/test_floating_ip_neutron.py index ba7c90ea1..9b7ae0b54 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_neutron.py +++ b/openstack/tests/unit/cloud/test_floating_ip_neutron.py @@ -164,7 +164,7 @@ def test_float_no_status(self): def test_list_floating_ips(self): self.register_uris([ dict(method='GET', - uri='https://network.example.com/v2.0/floatingips.json', + uri='https://network.example.com/v2.0/floatingips', json=self.mock_floating_ip_list_rep)]) floating_ips = self.cloud.list_floating_ips() @@ -179,7 +179,7 @@ def test_list_floating_ips_with_filters(self): self.register_uris([ dict(method='GET', - uri=('https://network.example.com/v2.0/floatingips.json?' + uri=('https://network.example.com/v2.0/floatingips?' 'Foo=42'), json={'floatingips': []})]) @@ -190,7 +190,7 @@ def test_list_floating_ips_with_filters(self): def test_search_floating_ips(self): self.register_uris([ dict(method='GET', - uri=('https://network.example.com/v2.0/floatingips.json'), + uri=('https://network.example.com/v2.0/floatingips'), json=self.mock_floating_ip_list_rep)]) floating_ips = self.cloud.search_floating_ips( @@ -204,7 +204,7 @@ def test_search_floating_ips(self): def test_get_floating_ip(self): self.register_uris([ dict(method='GET', - uri='https://network.example.com/v2.0/floatingips.json', + uri='https://network.example.com/v2.0/floatingips', json=self.mock_floating_ip_list_rep)]) floating_ip = self.cloud.get_floating_ip( @@ -226,7 +226,7 @@ def test_get_floating_ip(self): def test_get_floating_ip_not_found(self): self.register_uris([ dict(method='GET', - uri='https://network.example.com/v2.0/floatingips.json', + uri='https://network.example.com/v2.0/floatingips', json=self.mock_floating_ip_list_rep)]) floating_ip = self.cloud.get_floating_ip(id='non-existent') @@ -260,10 +260,10 @@ def test_get_floating_ip_by_id(self): def test_create_floating_ip(self): self.register_uris([ dict(method='GET', - uri='https://network.example.com/v2.0/networks.json', + uri='https://network.example.com/v2.0/networks', json={'networks': [self.mock_get_network_rep]}), dict(method='POST', - uri='https://network.example.com/v2.0/floatingips.json', + uri='https://network.example.com/v2.0/floatingips', json=self.mock_floating_ip_new_rep, validate=dict( json={'floatingip': { @@ -279,10 +279,10 @@ def test_create_floating_ip(self): def test_create_floating_ip_port_bad_response(self): self.register_uris([ dict(method='GET', - uri='https://network.example.com/v2.0/networks.json', + uri='https://network.example.com/v2.0/networks', json={'networks': [self.mock_get_network_rep]}), dict(method='POST', - uri='https://network.example.com/v2.0/floatingips.json', + uri='https://network.example.com/v2.0/floatingips', json=self.mock_floating_ip_new_rep, validate=dict( json={'floatingip': { @@ -300,10 +300,10 @@ def test_create_floating_ip_port_bad_response(self): def test_create_floating_ip_port(self): self.register_uris([ dict(method='GET', - uri='https://network.example.com/v2.0/networks.json', + uri='https://network.example.com/v2.0/networks', json={'networks': [self.mock_get_network_rep]}), dict(method='POST', - uri='https://network.example.com/v2.0/floatingips.json', + uri='https://network.example.com/v2.0/floatingips', json=self.mock_floating_ip_port_rep, validate=dict( json={'floatingip': { @@ -323,13 +323,13 @@ def test_neutron_available_floating_ips(self): """ Test without specifying a network name. """ - fips_mock_uri = 'https://network.example.com/v2.0/floatingips.json' + fips_mock_uri = 'https://network.example.com/v2.0/floatingips' self.register_uris([ dict(method='GET', - uri='https://network.example.com/v2.0/networks.json', + uri='https://network.example.com/v2.0/networks', json={'networks': [self.mock_get_network_rep]}), dict(method='GET', - uri='https://network.example.com/v2.0/subnets.json', + uri='https://network.example.com/v2.0/subnets', json={'subnets': []}), dict(method='GET', uri=fips_mock_uri, json={'floatingips': []}), dict(method='POST', uri=fips_mock_uri, @@ -348,13 +348,13 @@ def test_neutron_available_floating_ips_network(self): """ Test with specifying a network name. """ - fips_mock_uri = 'https://network.example.com/v2.0/floatingips.json' + fips_mock_uri = 'https://network.example.com/v2.0/floatingips' self.register_uris([ dict(method='GET', - uri='https://network.example.com/v2.0/networks.json', + uri='https://network.example.com/v2.0/networks', json={'networks': [self.mock_get_network_rep]}), dict(method='GET', - uri='https://network.example.com/v2.0/subnets.json', + uri='https://network.example.com/v2.0/subnets', json={'subnets': []}), dict(method='GET', uri=fips_mock_uri, json={'floatingips': []}), dict(method='POST', uri=fips_mock_uri, @@ -377,10 +377,10 @@ def test_neutron_available_floating_ips_invalid_network(self): """ self.register_uris([ dict(method='GET', - uri='https://network.example.com/v2.0/networks.json', + uri='https://network.example.com/v2.0/networks', json={'networks': [self.mock_get_network_rep]}), dict(method='GET', - uri='https://network.example.com/v2.0/subnets.json', + uri='https://network.example.com/v2.0/subnets', json={'subnets': []}) ]) @@ -395,7 +395,7 @@ def test_auto_ip_pool_no_reuse(self): # payloads taken from citycloud self.register_uris([ dict(method='GET', - uri='https://network.example.com/v2.0/networks.json', + uri='https://network.example.com/v2.0/networks', json={"networks": [{ "status": "ACTIVE", "subnets": [ @@ -436,7 +436,7 @@ def test_auto_ip_pool_no_reuse(self): "description": "" }]}), dict(method='GET', - uri='https://network.example.com/v2.0/ports.json' + uri='https://network.example.com/v2.0/ports' '?device_id=f80e3ad0-e13e-41d4-8e9c-be79bccdb8f7', json={"ports": [{ "status": "ACTIVE", @@ -463,7 +463,7 @@ def test_auto_ip_pool_no_reuse(self): }]}), dict(method='POST', - uri='https://network.example.com/v2.0/floatingips.json', + uri='https://network.example.com/v2.0/floatingips', json={"floatingip": { "router_id": "9de9c787-8f89-4a53-8468-a5533d6d7fd1", "status": "DOWN", @@ -525,7 +525,7 @@ def test_auto_ip_pool_no_reuse(self): "metadata": {} }]}), dict(method='GET', - uri='https://network.example.com/v2.0/networks.json', + uri='https://network.example.com/v2.0/networks', json={"networks": [{ "status": "ACTIVE", "subnets": [ @@ -566,7 +566,7 @@ def test_auto_ip_pool_no_reuse(self): "description": "" }]}), dict(method='GET', - uri='https://network.example.com/v2.0/subnets.json', + uri='https://network.example.com/v2.0/subnets', json={"subnets": [{ "description": "", "enable_dhcp": True, @@ -609,19 +609,19 @@ def test_available_floating_ip_new(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': [self.mock_get_network_rep]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets.json']), + 'network', 'public', append=['v2.0', 'subnets']), json={'subnets': []}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips.json']), + 'network', 'public', append=['v2.0', 'floatingips']), json={'floatingips': []}), dict(method='POST', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips.json']), + 'network', 'public', append=['v2.0', 'floatingips']), validate=dict( json={'floatingip': { 'floating_network_id': 'my-network-id'}}), @@ -646,29 +646,29 @@ def test_delete_floating_ip_existing(self): dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'floatingips/{0}.json'.format(fip_id)]), + append=['v2.0', 'floatingips/{0}'.format(fip_id)]), json={}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips.json']), + 'network', 'public', append=['v2.0', 'floatingips']), json={'floatingips': [fake_fip]}), dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'floatingips/{0}.json'.format(fip_id)]), + append=['v2.0', 'floatingips/{0}'.format(fip_id)]), json={}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips.json']), + 'network', 'public', append=['v2.0', 'floatingips']), json={'floatingips': [fake_fip]}), dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'floatingips/{0}.json'.format(fip_id)]), + append=['v2.0', 'floatingips/{0}'.format(fip_id)]), json={}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips.json']), + 'network', 'public', append=['v2.0', 'floatingips']), json={'floatingips': []}), ]) @@ -692,20 +692,20 @@ def test_delete_floating_ip_existing_down(self): dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'floatingips/{0}.json'.format(fip_id)]), + append=['v2.0', 'floatingips/{0}'.format(fip_id)]), json={}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips.json']), + 'network', 'public', append=['v2.0', 'floatingips']), json={'floatingips': [fake_fip]}), dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'floatingips/{0}.json'.format(fip_id)]), + append=['v2.0', 'floatingips/{0}'.format(fip_id)]), json={}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips.json']), + 'network', 'public', append=['v2.0', 'floatingips']), json={'floatingips': [down_fip]}), ]) @@ -724,29 +724,29 @@ def test_delete_floating_ip_existing_no_delete(self): dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'floatingips/{0}.json'.format(fip_id)]), + append=['v2.0', 'floatingips/{0}'.format(fip_id)]), json={}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips.json']), + 'network', 'public', append=['v2.0', 'floatingips']), json={'floatingips': [fake_fip]}), dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'floatingips/{0}.json'.format(fip_id)]), + append=['v2.0', 'floatingips/{0}'.format(fip_id)]), json={}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips.json']), + 'network', 'public', append=['v2.0', 'floatingips']), json={'floatingips': [fake_fip]}), dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'floatingips/{0}.json'.format(fip_id)]), + append=['v2.0', 'floatingips/{0}'.format(fip_id)]), json={}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips.json']), + 'network', 'public', append=['v2.0', 'floatingips']), json={'floatingips': [fake_fip]}), ]) self.assertRaises( @@ -759,7 +759,7 @@ def test_delete_floating_ip_not_found(self): self.register_uris([ dict(method='DELETE', uri=('https://network.example.com/v2.0/floatingips/' - 'a-wild-id-appears.json'), + 'a-wild-id-appears'), status_code=404)]) ret = self.cloud.delete_floating_ip( @@ -776,13 +776,13 @@ def test_attach_ip_to_server(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json'], + 'network', 'public', append=['v2.0', 'ports'], qs_elements=["device_id={0}".format(device_id)]), json={'ports': self.mock_search_ports_rep}), dict(method='PUT', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'floatingips/{0}.json'.format( + append=['v2.0', 'floatingips/{0}'.format( fip['id'])]), json={'floatingip': self.mock_floating_ip_list_rep['floatingips'][0]}, @@ -805,12 +805,12 @@ def test_detach_ip_from_server(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips.json']), + 'network', 'public', append=['v2.0', 'floatingips']), json={'floatingips': [attached_fip]}), dict(method='PUT', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'floatingips/{0}.json'.format( + append=['v2.0', 'floatingips/{0}'.format( fip['id'])]), json={'floatingip': fip}, validate=dict( @@ -829,33 +829,33 @@ def test_add_ip_from_pool(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': [network]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets.json']), + 'network', 'public', append=['v2.0', 'subnets']), json={'subnets': []}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips.json']), + 'network', 'public', append=['v2.0', 'floatingips']), json={'floatingips': [fip]}), dict(method='POST', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips.json']), + 'network', 'public', append=['v2.0', 'floatingips']), json={'floatingip': fip}, validate=dict( json={'floatingip': { 'floating_network_id': network['id']}})), dict(method="GET", uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json'], + 'network', 'public', append=['v2.0', 'ports'], qs_elements=[ "device_id={0}".format(self.fake_server['id'])]), json={'ports': self.mock_search_ports_rep}), dict(method='PUT', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'floatingips/{0}.json'.format( + append=['v2.0', 'floatingips/{0}'.format( fip['id'])]), json={'floatingip': fip}, validate=dict( @@ -902,28 +902,28 @@ def test_cleanup_floating_ips(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips.json']), + 'network', 'public', append=['v2.0', 'floatingips']), json={'floatingips': floating_ips}), dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'floatingips/{0}.json'.format( + append=['v2.0', 'floatingips/{0}'.format( floating_ips[0]['id'])]), json={}), # First IP has been deleted now, return just the second dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips.json']), + 'network', 'public', append=['v2.0', 'floatingips']), json={'floatingips': floating_ips[1:]}), dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'floatingips/{0}.json'.format( + append=['v2.0', 'floatingips/{0}'.format( floating_ips[1]['id'])]), json={}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips.json']), + 'network', 'public', append=['v2.0', 'floatingips']), json={'floatingips': [floating_ips[2]]}), ]) cleaned_up = self.cloud.delete_unattached_floating_ips() @@ -949,20 +949,20 @@ def test_create_floating_ip_no_port(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': [self.mock_get_network_rep]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets.json']), + 'network', 'public', append=['v2.0', 'subnets']), json={'subnets': []}), dict(method="GET", uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json'], + 'network', 'public', append=['v2.0', 'ports'], qs_elements=['device_id=some-server']), json={'ports': [server_port]}), dict(method='POST', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips.json']), + 'network', 'public', append=['v2.0', 'floatingips']), json={'floatingip': floating_ip}) ]) @@ -976,7 +976,7 @@ def test_find_nat_source_inferred(self): # payloads contrived but based on ones from citycloud self.register_uris([ dict(method='GET', - uri='https://network.example.com/v2.0/networks.json', + uri='https://network.example.com/v2.0/networks', json={"networks": [{ "status": "ACTIVE", "subnets": [ @@ -1037,7 +1037,7 @@ def test_find_nat_source_inferred(self): "description": "" }]}), dict(method='GET', - uri='https://network.example.com/v2.0/subnets.json', + uri='https://network.example.com/v2.0/subnets', json={"subnets": [{ "description": "", "enable_dhcp": True, @@ -1074,7 +1074,7 @@ def test_find_nat_source_config(self): # payloads contrived but based on ones from citycloud self.register_uris([ dict(method='GET', - uri='https://network.example.com/v2.0/networks.json', + uri='https://network.example.com/v2.0/networks', json={"networks": [{ "status": "ACTIVE", "subnets": [ @@ -1135,7 +1135,7 @@ def test_find_nat_source_config(self): "description": "" }]}), dict(method='GET', - uri='https://network.example.com/v2.0/subnets.json', + uri='https://network.example.com/v2.0/subnets', json={"subnets": [{ "description": "", "enable_dhcp": True, diff --git a/openstack/tests/unit/cloud/test_fwaas.py b/openstack/tests/unit/cloud/test_fwaas.py index 59e381b5c..e4870d1a9 100644 --- a/openstack/tests/unit/cloud/test_fwaas.py +++ b/openstack/tests/unit/cloud/test_fwaas.py @@ -882,7 +882,7 @@ def test_create_firewall_group(self): dict(method='GET', uri=self.get_mock_url('network', 'public', - append=['v2.0', 'ports.json']), + append=['v2.0', 'ports']), json={'ports': [self.mock_port]}), dict(method='POST', uri=self._make_mock_url('firewall_groups'), @@ -1079,7 +1079,7 @@ def test_update_firewall_group(self): dict(method='GET', uri=self.get_mock_url('network', 'public', - append=['v2.0', 'ports.json']), + append=['v2.0', 'ports']), json={'ports': [self.mock_port]}), dict(method='PUT', uri=self._make_mock_url('firewall_groups', diff --git a/openstack/tests/unit/cloud/test_meta.py b/openstack/tests/unit/cloud/test_meta.py index 35b81a101..3f4bbbdb4 100644 --- a/openstack/tests/unit/cloud/test_meta.py +++ b/openstack/tests/unit/cloud/test_meta.py @@ -296,13 +296,13 @@ def test_get_server_ip(self): def test_get_server_private_ip(self): self.register_uris([ dict(method='GET', - uri='https://network.example.com/v2.0/networks.json', + uri='https://network.example.com/v2.0/networks', json={'networks': [{ 'id': 'test-net-id', 'name': 'test-net-name'}]} ), dict(method='GET', - uri='https://network.example.com/v2.0/subnets.json', + uri='https://network.example.com/v2.0/subnets', json={'subnets': SUBNETS_WITH_NAT}) ]) @@ -323,13 +323,13 @@ def test_get_server_private_ip(self): def test_get_server_multiple_private_ip(self): self.register_uris([ dict(method='GET', - uri='https://network.example.com/v2.0/networks.json', + uri='https://network.example.com/v2.0/networks', json={'networks': [{ 'id': 'test-net-id', 'name': 'test-net'}]} ), dict(method='GET', - uri='https://network.example.com/v2.0/subnets.json', + uri='https://network.example.com/v2.0/subnets', json={'subnets': SUBNETS_WITH_NAT}) ]) @@ -385,7 +385,7 @@ def test_get_server_private_ip_devstack( self.register_uris([ dict(method='GET', - uri=('https://network.example.com/v2.0/ports.json?' + uri=('https://network.example.com/v2.0/ports?' 'device_id=test-id'), json={'ports': [{ 'id': 'test_port_id', @@ -394,11 +394,11 @@ def test_get_server_private_ip_devstack( ), dict(method='GET', uri=('https://network.example.com/v2.0/' - 'floatingips.json?port_id=test_port_id'), + 'floatingips?port_id=test_port_id'), json={'floatingips': []}), dict(method='GET', - uri='https://network.example.com/v2.0/networks.json', + uri='https://network.example.com/v2.0/networks', json={'networks': [ {'id': 'test_pnztt_net', 'name': 'test_pnztt_net', @@ -408,7 +408,7 @@ def test_get_server_private_ip_devstack( 'name': 'private'}]} ), dict(method='GET', - uri='https://network.example.com/v2.0/subnets.json', + uri='https://network.example.com/v2.0/subnets', json={'subnets': SUBNETS_WITH_NAT}), self.get_nova_discovery_mock_dict(), @@ -457,7 +457,7 @@ def test_get_server_private_ip_no_fip( self.register_uris([ dict(method='GET', - uri='https://network.example.com/v2.0/networks.json', + uri='https://network.example.com/v2.0/networks', json={'networks': [ {'id': 'test_pnztt_net', 'name': 'test_pnztt_net', @@ -467,7 +467,7 @@ def test_get_server_private_ip_no_fip( 'name': 'private'}]} ), dict(method='GET', - uri='https://network.example.com/v2.0/subnets.json', + uri='https://network.example.com/v2.0/subnets', json={'subnets': SUBNETS_WITH_NAT}), self.get_nova_discovery_mock_dict(), dict(method='GET', @@ -512,7 +512,7 @@ def test_get_server_cloud_no_fips( self.register_uris([ dict(method='GET', - uri='https://network.example.com/v2.0/networks.json', + uri='https://network.example.com/v2.0/networks', json={'networks': [ { 'id': 'test_pnztt_net', @@ -524,7 +524,7 @@ def test_get_server_cloud_no_fips( 'name': 'private'}]} ), dict(method='GET', - uri='https://network.example.com/v2.0/subnets.json', + uri='https://network.example.com/v2.0/subnets', json={'subnets': SUBNETS_WITH_NAT}), self.get_nova_discovery_mock_dict(), dict(method='GET', @@ -572,7 +572,7 @@ def test_get_server_cloud_missing_fips( self.register_uris([ # self.get_nova_discovery_mock_dict(), dict(method='GET', - uri=('https://network.example.com/v2.0/ports.json?' + uri=('https://network.example.com/v2.0/ports?' 'device_id=test-id'), json={'ports': [{ 'id': 'test_port_id', @@ -580,7 +580,7 @@ def test_get_server_cloud_missing_fips( 'device_id': 'test-id'}]} ), dict(method='GET', - uri=('https://network.example.com/v2.0/floatingips.json' + uri=('https://network.example.com/v2.0/floatingips' '?port_id=test_port_id'), json={'floatingips': [{ 'id': 'floating-ip-id', @@ -589,7 +589,7 @@ def test_get_server_cloud_missing_fips( 'floating_ip_address': PUBLIC_V4, }]}), dict(method='GET', - uri='https://network.example.com/v2.0/networks.json', + uri='https://network.example.com/v2.0/networks', json={'networks': [ { 'id': 'test_pnztt_net', @@ -602,7 +602,7 @@ def test_get_server_cloud_missing_fips( } ]}), dict(method='GET', - uri='https://network.example.com/v2.0/subnets.json', + uri='https://network.example.com/v2.0/subnets', json={'subnets': SUBNETS_WITH_NAT}), self.get_nova_discovery_mock_dict(), dict(method='GET', @@ -718,10 +718,10 @@ def test_get_server_cloud_osic_split( self.register_uris([ dict(method='GET', - uri='https://network.example.com/v2.0/networks.json', + uri='https://network.example.com/v2.0/networks', json={'networks': OSIC_NETWORKS}), dict(method='GET', - uri='https://network.example.com/v2.0/subnets.json', + uri='https://network.example.com/v2.0/subnets', json={'subnets': OSIC_SUBNETS}), self.get_nova_discovery_mock_dict(), dict(method='GET', @@ -749,14 +749,14 @@ def test_get_server_external_ipv4_neutron(self): # Testing Clouds with Neutron self.register_uris([ dict(method='GET', - uri='https://network.example.com/v2.0/networks.json', + uri='https://network.example.com/v2.0/networks', json={'networks': [{ 'id': 'test-net-id', 'name': 'test-net', 'router:external': True }]}), dict(method='GET', - uri='https://network.example.com/v2.0/subnets.json', + uri='https://network.example.com/v2.0/subnets', json={'subnets': SUBNETS_WITH_NAT}) ]) srv = fakes.make_fake_server( @@ -774,7 +774,7 @@ def test_get_server_external_provider_ipv4_neutron(self): # Testing Clouds with Neutron self.register_uris([ dict(method='GET', - uri='https://network.example.com/v2.0/networks.json', + uri='https://network.example.com/v2.0/networks', json={'networks': [{ 'id': 'test-net-id', 'name': 'test-net', @@ -782,7 +782,7 @@ def test_get_server_external_provider_ipv4_neutron(self): 'provider:physical_network': 'vlan', }]}), dict(method='GET', - uri='https://network.example.com/v2.0/subnets.json', + uri='https://network.example.com/v2.0/subnets', json={'subnets': SUBNETS_WITH_NAT}) ]) @@ -801,7 +801,7 @@ def test_get_server_internal_provider_ipv4_neutron(self): # Testing Clouds with Neutron self.register_uris([ dict(method='GET', - uri='https://network.example.com/v2.0/networks.json', + uri='https://network.example.com/v2.0/networks', json={'networks': [{ 'id': 'test-net-id', 'name': 'test-net', @@ -810,7 +810,7 @@ def test_get_server_internal_provider_ipv4_neutron(self): 'provider:physical_network': None, }]}), dict(method='GET', - uri='https://network.example.com/v2.0/subnets.json', + uri='https://network.example.com/v2.0/subnets', json={'subnets': SUBNETS_WITH_NAT}) ]) srv = fakes.make_fake_server( @@ -830,14 +830,14 @@ def test_get_server_external_none_ipv4_neutron(self): # Testing Clouds with Neutron self.register_uris([ dict(method='GET', - uri='https://network.example.com/v2.0/networks.json', + uri='https://network.example.com/v2.0/networks', json={'networks': [{ 'id': 'test-net-id', 'name': 'test-net', 'router:external': False, }]}), dict(method='GET', - uri='https://network.example.com/v2.0/subnets.json', + uri='https://network.example.com/v2.0/subnets', json={'subnets': SUBNETS_WITH_NAT}) ]) @@ -872,7 +872,7 @@ def test_get_server_external_ipv4_neutron_exception(self): # Testing Clouds with a non working Neutron self.register_uris([ dict(method='GET', - uri='https://network.example.com/v2.0/networks.json', + uri='https://network.example.com/v2.0/networks', status_code=404)]) srv = fakes.make_fake_server( diff --git a/openstack/tests/unit/cloud/test_network.py b/openstack/tests/unit/cloud/test_network.py index 318d3f2e4..35e5a0cd0 100644 --- a/openstack/tests/unit/cloud/test_network.py +++ b/openstack/tests/unit/cloud/test_network.py @@ -65,7 +65,7 @@ def test_list_networks(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': [net1, net2]}) ]) nets = self.cloud.list_networks() @@ -76,7 +76,7 @@ def test_list_networks_filtered(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json'], + 'network', 'public', append=['v2.0', 'networks'], qs_elements=["name=test"]), json={'networks': []}) ]) @@ -87,7 +87,7 @@ def test_create_network(self): self.register_uris([ dict(method='POST', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'network': self.mock_new_network_rep}, validate=dict( json={'network': { @@ -105,7 +105,7 @@ def test_create_network_specific_tenant(self): self.register_uris([ dict(method='POST', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'network': mock_new_network_rep}, validate=dict( json={'network': { @@ -123,7 +123,7 @@ def test_create_network_external(self): self.register_uris([ dict(method='POST', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'network': mock_new_network_rep}, validate=dict( json={'network': { @@ -154,7 +154,7 @@ def test_create_network_provider(self): self.register_uris([ dict(method='POST', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'network': mock_new_network_rep}, validate=dict( json={'network': expected_send_params})) @@ -167,11 +167,11 @@ def test_create_network_with_availability_zone_hints(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='POST', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'network': self.mock_new_network_rep}, validate=dict( json={'network': { @@ -204,7 +204,7 @@ def test_create_network_provider_ignored_value(self): self.register_uris([ dict(method='POST', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'network': mock_new_network_rep}, validate=dict( json={'network': expected_send_params})) @@ -237,7 +237,7 @@ def test_create_network_port_security_disabled(self): self.register_uris([ dict(method='POST', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'network': mock_new_network_rep}, validate=dict( json={'network': { @@ -259,7 +259,7 @@ def test_create_network_with_mtu(self): self.register_uris([ dict(method='POST', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'network': mock_new_network_rep}, validate=dict( json={'network': { @@ -294,12 +294,12 @@ def test_delete_network(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': [network]}), dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'networks', "%s.json" % network_id]), + append=['v2.0', 'networks', "%s" % network_id]), json={}) ]) self.assertTrue(self.cloud.delete_network(network_name)) @@ -309,7 +309,7 @@ def test_delete_network_not_found(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), ]) self.assertFalse(self.cloud.delete_network('test-net')) @@ -322,12 +322,12 @@ def test_delete_network_exception(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': [network]}), dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'networks', "%s.json" % network_id]), + append=['v2.0', 'networks', "%s" % network_id]), status_code=503) ]) self.assertRaises(openstack.cloud.OpenStackCloudException, diff --git a/openstack/tests/unit/cloud/test_port.py b/openstack/tests/unit/cloud/test_port.py index 6067b55fe..fc746964f 100644 --- a/openstack/tests/unit/cloud/test_port.py +++ b/openstack/tests/unit/cloud/test_port.py @@ -143,7 +143,7 @@ def test_create_port(self): self.register_uris([ dict(method="POST", uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json']), + 'network', 'public', append=['v2.0', 'ports']), json=self.mock_neutron_port_create_rep, validate=dict( json={'port': { @@ -168,7 +168,7 @@ def test_create_port_exception(self): self.register_uris([ dict(method="POST", uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json']), + 'network', 'public', append=['v2.0', 'ports']), status_code=500, validate=dict( json={'port': { @@ -187,12 +187,12 @@ def test_update_port(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json']), + 'network', 'public', append=['v2.0', 'ports']), json=self.mock_neutron_port_list_rep), dict(method='PUT', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'ports', '%s.json' % port_id]), + append=['v2.0', 'ports', '%s' % port_id]), json=self.mock_neutron_port_update_rep, validate=dict( json={'port': {'name': 'test-port-name-updated'}})) @@ -214,12 +214,12 @@ def test_update_port_exception(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json']), + 'network', 'public', append=['v2.0', 'ports']), json=self.mock_neutron_port_list_rep), dict(method='PUT', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'ports', '%s.json' % port_id]), + append=['v2.0', 'ports', '%s' % port_id]), status_code=500, validate=dict( json={'port': {'name': 'test-port-name-updated'}})) @@ -234,7 +234,7 @@ def test_list_ports(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json']), + 'network', 'public', append=['v2.0', 'ports']), json=self.mock_neutron_port_list_rep) ]) ports = self.cloud.list_ports() @@ -245,7 +245,7 @@ def test_list_ports_filtered(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json'], + 'network', 'public', append=['v2.0', 'ports'], qs_elements=['status=DOWN']), json=self.mock_neutron_port_list_rep) ]) @@ -257,7 +257,7 @@ def test_list_ports_exception(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json']), + 'network', 'public', append=['v2.0', 'ports']), status_code=500) ]) self.assertRaises(OpenStackCloudException, self.cloud.list_ports) @@ -267,7 +267,7 @@ def test_search_ports_by_id(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json']), + 'network', 'public', append=['v2.0', 'ports']), json=self.mock_neutron_port_list_rep) ]) ports = self.cloud.search_ports(name_or_id=port_id) @@ -281,7 +281,7 @@ def test_search_ports_by_name(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json']), + 'network', 'public', append=['v2.0', 'ports']), json=self.mock_neutron_port_list_rep) ]) ports = self.cloud.search_ports(name_or_id=port_name) @@ -294,7 +294,7 @@ def test_search_ports_not_found(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json']), + 'network', 'public', append=['v2.0', 'ports']), json=self.mock_neutron_port_list_rep) ]) ports = self.cloud.search_ports(name_or_id='non-existent') @@ -306,12 +306,12 @@ def test_delete_port(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json']), + 'network', 'public', append=['v2.0', 'ports']), json=self.mock_neutron_port_list_rep), dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'ports', '%s.json' % port_id]), + append=['v2.0', 'ports', '%s' % port_id]), json={}) ]) @@ -321,7 +321,7 @@ def test_delete_port_not_found(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json']), + 'network', 'public', append=['v2.0', 'ports']), json=self.mock_neutron_port_list_rep) ]) self.assertFalse(self.cloud.delete_port(name_or_id='non-existent')) @@ -334,7 +334,7 @@ def test_delete_subnet_multiple_found(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json']), + 'network', 'public', append=['v2.0', 'ports']), json={'ports': [port1, port2]}) ]) self.assertRaises(OpenStackCloudException, @@ -348,12 +348,12 @@ def test_delete_subnet_multiple_using_id(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json']), + 'network', 'public', append=['v2.0', 'ports']), json={'ports': [port1, port2]}), dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'ports', '%s.json' % port1['id']]), + append=['v2.0', 'ports', '%s' % port1['id']]), json={}) ]) self.assertTrue(self.cloud.delete_port(name_or_id=port1['id'])) diff --git a/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py b/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py index 5e9982c07..e6c4f52e7 100644 --- a/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py @@ -71,23 +71,23 @@ def test_get_qos_bandwidth_limit_rule(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': [self.mock_policy]}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, 'bandwidth_limit_rules', - '%s.json' % self.rule_id]), + '%s' % self.rule_id]), json={'bandwidth_limit_rule': self.mock_rule}) ]) r = self.cloud.get_qos_bandwidth_limit_rule(self.policy_name, @@ -99,16 +99,16 @@ def test_get_qos_bandwidth_limit_rule_no_qos_policy_found(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': []}) ]) self.assertRaises( @@ -121,7 +121,7 @@ def test_get_qos_bandwidth_limit_rule_no_qos_extension(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': []}) ]) self.assertRaises( @@ -134,16 +134,16 @@ def test_create_qos_bandwidth_limit_rule(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': [self.mock_policy]}), dict(method='POST', uri=self.get_mock_url( @@ -161,7 +161,7 @@ def test_create_qos_bandwidth_limit_rule_no_qos_extension(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': []}) ]) self.assertRaises( @@ -174,20 +174,20 @@ def test_create_qos_bandwidth_limit_rule_no_qos_direction_extension(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': [self.qos_extension]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': [self.qos_extension]}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': [self.mock_policy]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': [self.qos_extension]}), dict(method='POST', uri=self.get_mock_url( @@ -207,43 +207,43 @@ def test_update_qos_bandwidth_limit_rule(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': [self.mock_policy]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': [self.mock_policy]}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, 'bandwidth_limit_rules', - '%s.json' % self.rule_id]), + '%s' % self.rule_id]), json={'bandwidth_limit_rule': self.mock_rule}), dict(method='PUT', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, 'bandwidth_limit_rules', - '%s.json' % self.rule_id]), + '%s' % self.rule_id]), json={'bandwidth_limit_rule': expected_rule}, validate=dict( json={'bandwidth_limit_rule': { @@ -258,7 +258,7 @@ def test_update_qos_bandwidth_limit_rule_no_qos_extension(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': []}) ]) self.assertRaises( @@ -273,47 +273,47 @@ def test_update_qos_bandwidth_limit_rule_no_qos_direction_extension(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': [self.qos_extension]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': [self.qos_extension]}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': [self.mock_policy]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': [self.qos_extension]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': [self.qos_extension]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': [self.qos_extension]}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': [self.mock_policy]}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, 'bandwidth_limit_rules', - '%s.json' % self.rule_id]), + '%s' % self.rule_id]), json={'bandwidth_limit_rule': self.mock_rule}), dict(method='PUT', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, 'bandwidth_limit_rules', - '%s.json' % self.rule_id]), + '%s' % self.rule_id]), json={'bandwidth_limit_rule': expected_rule}, validate=dict( json={'bandwidth_limit_rule': { @@ -331,23 +331,23 @@ def test_delete_qos_bandwidth_limit_rule(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': [self.mock_policy]}), dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, 'bandwidth_limit_rules', - '%s.json' % self.rule_id]), + '%s' % self.rule_id]), json={}) ]) self.assertTrue( @@ -359,7 +359,7 @@ def test_delete_qos_bandwidth_limit_rule_no_qos_extension(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': []}) ]) self.assertRaises( @@ -372,23 +372,23 @@ def test_delete_qos_bandwidth_limit_rule_not_found(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': [self.mock_policy]}), dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, 'bandwidth_limit_rules', - '%s.json' % self.rule_id]), + '%s' % self.rule_id]), status_code=404) ]) self.assertFalse( diff --git a/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py b/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py index 5e8d554b8..fe6f08d69 100644 --- a/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py +++ b/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py @@ -58,23 +58,23 @@ def test_get_qos_dscp_marking_rule(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': [self.mock_policy]}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, 'dscp_marking_rules', - '%s.json' % self.rule_id]), + '%s' % self.rule_id]), json={'dscp_marking_rule': self.mock_rule}) ]) r = self.cloud.get_qos_dscp_marking_rule(self.policy_name, @@ -86,16 +86,16 @@ def test_get_qos_dscp_marking_rule_no_qos_policy_found(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': []}) ]) self.assertRaises( @@ -108,7 +108,7 @@ def test_get_qos_dscp_marking_rule_no_qos_extension(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': []}) ]) self.assertRaises( @@ -121,16 +121,16 @@ def test_create_qos_dscp_marking_rule(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': [self.mock_policy]}), dict(method='POST', uri=self.get_mock_url( @@ -148,7 +148,7 @@ def test_create_qos_dscp_marking_rule_no_qos_extension(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': []}) ]) self.assertRaises( @@ -164,43 +164,43 @@ def test_update_qos_dscp_marking_rule(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': [self.mock_policy]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': [self.mock_policy]}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, 'dscp_marking_rules', - '%s.json' % self.rule_id]), + '%s' % self.rule_id]), json={'dscp_marking_rule': self.mock_rule}), dict(method='PUT', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, 'dscp_marking_rules', - '%s.json' % self.rule_id]), + '%s' % self.rule_id]), json={'dscp_marking_rule': expected_rule}, validate=dict( json={'dscp_marking_rule': { @@ -215,7 +215,7 @@ def test_update_qos_dscp_marking_rule_no_qos_extension(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': []}) ]) self.assertRaises( @@ -228,23 +228,23 @@ def test_delete_qos_dscp_marking_rule(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': [self.mock_policy]}), dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, 'dscp_marking_rules', - '%s.json' % self.rule_id]), + '%s' % self.rule_id]), json={}) ]) self.assertTrue( @@ -256,7 +256,7 @@ def test_delete_qos_dscp_marking_rule_no_qos_extension(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': []}) ]) self.assertRaises( @@ -269,23 +269,23 @@ def test_delete_qos_dscp_marking_rule_not_found(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': [self.mock_policy]}), dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, 'dscp_marking_rules', - '%s.json' % self.rule_id]), + '%s' % self.rule_id]), status_code=404) ]) self.assertFalse( diff --git a/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py b/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py index e919b4f5b..3787da4c3 100644 --- a/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py +++ b/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py @@ -59,23 +59,23 @@ def test_get_qos_minimum_bandwidth_rule(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': [self.mock_policy]}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, 'minimum_bandwidth_rules', - '%s.json' % self.rule_id]), + '%s' % self.rule_id]), json={'minimum_bandwidth_rule': self.mock_rule}) ]) r = self.cloud.get_qos_minimum_bandwidth_rule(self.policy_name, @@ -87,16 +87,16 @@ def test_get_qos_minimum_bandwidth_rule_no_qos_policy_found(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': []}) ]) self.assertRaises( @@ -109,7 +109,7 @@ def test_get_qos_minimum_bandwidth_rule_no_qos_extension(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': []}) ]) self.assertRaises( @@ -122,16 +122,16 @@ def test_create_qos_minimum_bandwidth_rule(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': [self.mock_policy]}), dict(method='POST', uri=self.get_mock_url( @@ -149,7 +149,7 @@ def test_create_qos_minimum_bandwidth_rule_no_qos_extension(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': []}) ]) self.assertRaises( @@ -164,43 +164,43 @@ def test_update_qos_minimum_bandwidth_rule(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': [self.mock_policy]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': [self.mock_policy]}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, 'minimum_bandwidth_rules', - '%s.json' % self.rule_id]), + '%s' % self.rule_id]), json={'minimum_bandwidth_rule': self.mock_rule}), dict(method='PUT', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, 'minimum_bandwidth_rules', - '%s.json' % self.rule_id]), + '%s' % self.rule_id]), json={'minimum_bandwidth_rule': expected_rule}, validate=dict( json={'minimum_bandwidth_rule': { @@ -215,7 +215,7 @@ def test_update_qos_minimum_bandwidth_rule_no_qos_extension(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': []}) ]) self.assertRaises( @@ -228,23 +228,23 @@ def test_delete_qos_minimum_bandwidth_rule(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': [self.mock_policy]}), dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, 'minimum_bandwidth_rules', - '%s.json' % self.rule_id]), + '%s' % self.rule_id]), json={}) ]) self.assertTrue( @@ -256,7 +256,7 @@ def test_delete_qos_minimum_bandwidth_rule_no_qos_extension(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': []}) ]) self.assertRaises( @@ -269,23 +269,23 @@ def test_delete_qos_minimum_bandwidth_rule_not_found(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': [self.mock_policy]}), dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, 'minimum_bandwidth_rules', - '%s.json' % self.rule_id]), + '%s' % self.rule_id]), status_code=404) ]) self.assertFalse( diff --git a/openstack/tests/unit/cloud/test_qos_policy.py b/openstack/tests/unit/cloud/test_qos_policy.py index a6d9c43ae..1becef133 100644 --- a/openstack/tests/unit/cloud/test_qos_policy.py +++ b/openstack/tests/unit/cloud/test_qos_policy.py @@ -58,12 +58,12 @@ def test_get_qos_policy(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': [self.mock_policy]}) ]) r = self.cloud.get_qos_policy(self.policy_name) @@ -75,7 +75,7 @@ def test_get_qos_policy_no_qos_extension(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': []}) ]) self.assertRaises( @@ -87,12 +87,12 @@ def test_create_qos_policy(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='POST', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policy': self.mock_policy}) ]) policy = self.cloud.create_qos_policy( @@ -104,7 +104,7 @@ def test_create_qos_policy_no_qos_extension(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': []}) ]) self.assertRaises( @@ -116,16 +116,16 @@ def test_create_qos_policy_no_qos_default_extension(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': [self.qos_extension]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': [self.qos_extension]}), dict(method='POST', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policy': self.mock_policy}, validate=dict( json={'policy': { @@ -141,22 +141,22 @@ def test_delete_qos_policy(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': [self.mock_policy]}), dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', - '%s.json' % self.policy_id]), + '%s' % self.policy_id]), json={}) ]) self.assertTrue(self.cloud.delete_qos_policy(self.policy_name)) @@ -166,7 +166,7 @@ def test_delete_qos_policy_no_qos_extension(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': []}) ]) self.assertRaises( @@ -178,16 +178,16 @@ def test_delete_qos_policy_not_found(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': []}) ]) self.assertFalse(self.cloud.delete_qos_policy('goofy')) @@ -199,16 +199,16 @@ def test_delete_qos_policy_multiple_found(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': [policy1, policy2]}) ]) self.assertRaises(exc.OpenStackCloudException, @@ -222,22 +222,22 @@ def test_delete_qos_policy_multiple_using_id(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': [policy1, policy2]}), dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', - '%s.json' % self.policy_id]), + '%s' % self.policy_id]), json={}) ]) self.assertTrue(self.cloud.delete_qos_policy(policy1['id'])) @@ -249,22 +249,22 @@ def test_update_qos_policy(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': [self.mock_policy]}), dict(method='PUT', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', - '%s.json' % self.policy_id]), + '%s' % self.policy_id]), json={'policy': expected_policy}, validate=dict( json={'policy': {'name': 'goofy'}})) @@ -278,7 +278,7 @@ def test_update_qos_policy_no_qos_extension(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': []}) ]) self.assertRaises( @@ -292,26 +292,26 @@ def test_update_qos_policy_no_qos_default_extension(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': [self.qos_extension]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': [self.qos_extension]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': [self.qos_extension]}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies.json']), + append=['v2.0', 'qos', 'policies']), json={'policies': [self.mock_policy]}), dict(method='PUT', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', - '%s.json' % self.policy_id]), + '%s' % self.policy_id]), json={'policy': expected_policy}, validate=dict( json={'policy': {'name': "goofy"}})) diff --git a/openstack/tests/unit/cloud/test_qos_rule_type.py b/openstack/tests/unit/cloud/test_qos_rule_type.py index e289bba4a..01bb743f7 100644 --- a/openstack/tests/unit/cloud/test_qos_rule_type.py +++ b/openstack/tests/unit/cloud/test_qos_rule_type.py @@ -70,12 +70,12 @@ def test_list_qos_rule_types(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': [self.qos_extension]}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'rule-types.json']), + append=['v2.0', 'qos', 'rule-types']), json={'rule_types': self.mock_rule_types}) ]) rule_types = self.cloud.list_qos_rule_types() @@ -86,7 +86,7 @@ def test_list_qos_rule_types_no_qos_extension(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': []}) ]) self.assertRaises(exc.OpenStackCloudException, @@ -97,13 +97,13 @@ def test_get_qos_rule_type_details(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': [ self.qos_extension, self.qos_rule_type_details_extension]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': [ self.qos_extension, self.qos_rule_type_details_extension]}), @@ -111,7 +111,7 @@ def test_get_qos_rule_type_details(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'rule-types', - '%s.json' % self.rule_type_name]), + '%s' % self.rule_type_name]), json={'rule_type': self.mock_rule_type_details}) ]) self.assertEqual( @@ -124,7 +124,7 @@ def test_get_qos_rule_type_details_no_qos_extension(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': []}) ]) self.assertRaises( @@ -136,11 +136,11 @@ def test_get_qos_rule_type_details_no_qos_details_extension(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': [self.qos_extension]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': [self.qos_extension]}) ]) self.assertRaises( diff --git a/openstack/tests/unit/cloud/test_quotas.py b/openstack/tests/unit/cloud/test_quotas.py index cb42290c1..a35559d89 100644 --- a/openstack/tests/unit/cloud/test_quotas.py +++ b/openstack/tests/unit/cloud/test_quotas.py @@ -155,7 +155,7 @@ def test_neutron_update_quotas(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'quotas', - '%s.json' % project.project_id]), + '%s' % project.project_id]), json={}, validate=dict( json={'quota': {'network': 1}})) @@ -182,7 +182,7 @@ def test_neutron_get_quotas(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'quotas', - '%s.json' % project.project_id]), + '%s' % project.project_id]), json={'quota': quota}) ]) received_quota = self.cloud.get_network_quotas(project.project_id) @@ -235,7 +235,7 @@ def test_neutron_get_quotas_details(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'quotas', - '%s/details.json' % project.project_id]), + '%s/details' % project.project_id]), json={'quota': quota_details}) ]) received_quota_details = self.cloud.get_network_quotas( @@ -251,7 +251,7 @@ def test_neutron_delete_quotas(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'quotas', - '%s.json' % project.project_id]), + '%s' % project.project_id]), json={}) ]) self.cloud.delete_network_quotas(project.project_id) diff --git a/openstack/tests/unit/cloud/test_rebuild_server.py b/openstack/tests/unit/cloud/test_rebuild_server.py index 401fd43ee..24efb63d6 100644 --- a/openstack/tests/unit/cloud/test_rebuild_server.py +++ b/openstack/tests/unit/cloud/test_rebuild_server.py @@ -136,7 +136,7 @@ def test_rebuild_server_no_wait(self): 'imageRef': 'a'}})), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), ]) self.assertEqual( @@ -166,7 +166,7 @@ def test_rebuild_server_with_admin_pass_no_wait(self): 'adminPass': password}})), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), ]) self.assertEqual( @@ -207,7 +207,7 @@ def test_rebuild_server_with_admin_pass_wait(self): json={'servers': [self.fake_server]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), ]) @@ -245,7 +245,7 @@ def test_rebuild_server_wait(self): json={'servers': [self.fake_server]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), ]) self.assertEqual( diff --git a/openstack/tests/unit/cloud/test_router.py b/openstack/tests/unit/cloud/test_router.py index 5a26d4943..70a296077 100644 --- a/openstack/tests/unit/cloud/test_router.py +++ b/openstack/tests/unit/cloud/test_router.py @@ -82,7 +82,7 @@ def test_get_router(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers.json']), + 'network', 'public', append=['v2.0', 'routers']), json={'routers': [self.mock_router_rep]}) ]) r = self.cloud.get_router(self.router_name) @@ -94,7 +94,7 @@ def test_get_router_not_found(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers.json']), + 'network', 'public', append=['v2.0', 'routers']), json={'routers': []}) ]) r = self.cloud.get_router('mickey') @@ -105,7 +105,7 @@ def test_create_router(self): self.register_uris([ dict(method='POST', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers.json']), + 'network', 'public', append=['v2.0', 'routers']), json={'router': self.mock_router_rep}, validate=dict( json={'router': { @@ -125,7 +125,7 @@ def test_create_router_specific_tenant(self): self.register_uris([ dict(method='POST', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers.json']), + 'network', 'public', append=['v2.0', 'routers']), json={'router': mock_router_rep}, validate=dict( json={'router': { @@ -142,11 +142,11 @@ def test_create_router_with_availability_zone_hints(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='POST', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers.json']), + 'network', 'public', append=['v2.0', 'routers']), json={'router': self.mock_router_rep}, validate=dict( json={'router': { @@ -164,7 +164,7 @@ def test_create_router_without_enable_snat(self): self.register_uris([ dict(method='POST', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers.json']), + 'network', 'public', append=['v2.0', 'routers']), json={'router': self.mock_router_rep}, validate=dict( json={'router': { @@ -180,7 +180,7 @@ def test_create_router_with_enable_snat_True(self): self.register_uris([ dict(method='POST', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers.json']), + 'network', 'public', append=['v2.0', 'routers']), json={'router': self.mock_router_rep}, validate=dict( json={'router': { @@ -197,7 +197,7 @@ def test_create_router_with_enable_snat_False(self): self.register_uris([ dict(method='POST', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers.json']), + 'network', 'public', append=['v2.0', 'routers']), json={'router': self.mock_router_rep}, validate=dict( json={'router': { @@ -225,7 +225,7 @@ def test_add_router_interface(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'routers', self.router_id, - 'add_router_interface.json']), + 'add_router_interface']), json={'port': self.mock_router_interface_rep}, validate=dict( json={'subnet_id': self.subnet_id})) @@ -240,7 +240,7 @@ def test_remove_router_interface(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'routers', self.router_id, - 'remove_router_interface.json']), + 'remove_router_interface']), json={'port': self.mock_router_interface_rep}, validate=dict( json={'subnet_id': self.subnet_id})) @@ -265,16 +265,16 @@ def test_update_router(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers.json']), + 'network', 'public', append=['v2.0', 'routers']), json={'routers': [self.mock_router_rep]}), dict(method='PUT', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'routers', '%s.json' % self.router_id]), + append=['v2.0', 'routers', '%s' % self.router_id]), json={'router': expected_router_rep}, validate=dict( json={'router': { @@ -290,12 +290,12 @@ def test_delete_router(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers.json']), + 'network', 'public', append=['v2.0', 'routers']), json={'routers': [self.mock_router_rep]}), dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'routers', '%s.json' % self.router_id]), + append=['v2.0', 'routers', '%s' % self.router_id]), json={}) ]) self.assertTrue(self.cloud.delete_router(self.router_name)) @@ -305,7 +305,7 @@ def test_delete_router_not_found(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers.json']), + 'network', 'public', append=['v2.0', 'routers']), json={'routers': []}), ]) self.assertFalse(self.cloud.delete_router(self.router_name)) @@ -317,7 +317,7 @@ def test_delete_router_multiple_found(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers.json']), + 'network', 'public', append=['v2.0', 'routers']), json={'routers': [router1, router2]}), ]) self.assertRaises(exc.OpenStackCloudException, @@ -331,12 +331,12 @@ def test_delete_router_multiple_using_id(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers.json']), + 'network', 'public', append=['v2.0', 'routers']), json={'routers': [router1, router2]}), dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'routers', '123.json']), + append=['v2.0', 'routers', '123']), json={}) ]) self.assertTrue(self.cloud.delete_router("123")) @@ -345,7 +345,7 @@ def test_delete_router_multiple_using_id(self): def _get_mock_dict(self, owner, json): return dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports.json'], + 'network', 'public', append=['v2.0', 'ports'], qs_elements=["device_id=%s" % self.router_id, "device_owner=network:%s" % owner]), json=json) diff --git a/openstack/tests/unit/cloud/test_shade.py b/openstack/tests/unit/cloud/test_shade.py index 2b24b1eb8..9b8c369b6 100644 --- a/openstack/tests/unit/cloud/test_shade.py +++ b/openstack/tests/unit/cloud/test_shade.py @@ -409,11 +409,11 @@ def test_list_server_private_ip(self): json={'servers': [fake_server]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json=fake_networks), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets.json']), + 'network', 'public', append=['v2.0', 'subnets']), json=fake_subnets) ]) @@ -621,7 +621,7 @@ def test__neutron_extensions(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json=dict(extensions=body)) ]) extensions = self.cloud._neutron_extensions() @@ -633,7 +633,7 @@ def test__neutron_extensions_fails(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), status_code=404) ]) with testtools.ExpectedException( @@ -665,7 +665,7 @@ def test__has_neutron_extension(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json=dict(extensions=body)) ]) self.assertTrue(self.cloud._has_neutron_extension('dvr')) @@ -692,7 +692,7 @@ def test__has_neutron_extension_missing(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions.json']), + 'network', 'public', append=['v2.0', 'extensions']), json=dict(extensions=body)) ]) self.assertFalse(self.cloud._has_neutron_extension('invalid')) diff --git a/openstack/tests/unit/cloud/test_subnet.py b/openstack/tests/unit/cloud/test_subnet.py index b811b28a7..7a71116e2 100644 --- a/openstack/tests/unit/cloud/test_subnet.py +++ b/openstack/tests/unit/cloud/test_subnet.py @@ -70,7 +70,7 @@ def test_get_subnet(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets.json']), + 'network', 'public', append=['v2.0', 'subnets']), json={'subnets': [self.mock_subnet_rep]}) ]) r = self.cloud.get_subnet(self.subnet_name) @@ -103,11 +103,11 @@ def test_create_subnet(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': [self.mock_network_rep]}), dict(method='POST', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets.json']), + 'network', 'public', append=['v2.0', 'subnets']), json={'subnet': mock_subnet_rep}, validate=dict( json={'subnet': { @@ -131,11 +131,11 @@ def test_create_subnet_string_ip_version(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': [self.mock_network_rep]}), dict(method='POST', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets.json']), + 'network', 'public', append=['v2.0', 'subnets']), json={'subnet': self.mock_subnet_rep}, validate=dict( json={'subnet': { @@ -154,7 +154,7 @@ def test_create_subnet_bad_ip_version(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': [self.mock_network_rep]}) ]) with testtools.ExpectedException( @@ -175,11 +175,11 @@ def test_create_subnet_without_gateway_ip(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': [self.mock_network_rep]}), dict(method='POST', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets.json']), + 'network', 'public', append=['v2.0', 'subnets']), json={'subnet': mock_subnet_rep}, validate=dict( json={'subnet': { @@ -209,11 +209,11 @@ def test_create_subnet_with_gateway_ip(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': [self.mock_network_rep]}), dict(method='POST', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets.json']), + 'network', 'public', append=['v2.0', 'subnets']), json={'subnet': mock_subnet_rep}, validate=dict( json={'subnet': { @@ -236,7 +236,7 @@ def test_create_subnet_conflict_gw_ops(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': [self.mock_network_rep]}) ]) gateway = '192.168.200.3' @@ -250,7 +250,7 @@ def test_create_subnet_bad_network(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': [self.mock_network_rep]}) ]) self.assertRaises(exc.OpenStackCloudException, @@ -264,7 +264,7 @@ def test_create_subnet_non_unique_network(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': [net1, net2]}) ]) self.assertRaises(exc.OpenStackCloudException, @@ -289,11 +289,11 @@ def test_create_subnet_from_subnetpool_with_prefixlen(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': [self.mock_network_rep]}), dict(method='POST', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets.json']), + 'network', 'public', append=['v2.0', 'subnets']), json={'subnet': mock_subnet_rep}, validate=dict( json={'subnet': { @@ -319,12 +319,12 @@ def test_delete_subnet(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets.json']), + 'network', 'public', append=['v2.0', 'subnets']), json={'subnets': [self.mock_subnet_rep]}), dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'subnets', '%s.json' % self.subnet_id]), + append=['v2.0', 'subnets', '%s' % self.subnet_id]), json={}) ]) self.assertTrue(self.cloud.delete_subnet(self.subnet_name)) @@ -334,7 +334,7 @@ def test_delete_subnet_not_found(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets.json']), + 'network', 'public', append=['v2.0', 'subnets']), json={'subnets': []}) ]) self.assertFalse(self.cloud.delete_subnet('goofy')) @@ -346,7 +346,7 @@ def test_delete_subnet_multiple_found(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets.json']), + 'network', 'public', append=['v2.0', 'subnets']), json={'subnets': [subnet1, subnet2]}) ]) self.assertRaises(exc.OpenStackCloudException, @@ -360,12 +360,12 @@ def test_delete_subnet_multiple_using_id(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets.json']), + 'network', 'public', append=['v2.0', 'subnets']), json={'subnets': [subnet1, subnet2]}), dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'subnets', '%s.json' % subnet1['id']]), + append=['v2.0', 'subnets', '%s' % subnet1['id']]), json={}) ]) self.assertTrue(self.cloud.delete_subnet(subnet1['id'])) @@ -377,12 +377,12 @@ def test_update_subnet(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets.json']), + 'network', 'public', append=['v2.0', 'subnets']), json={'subnets': [self.mock_subnet_rep]}), dict(method='PUT', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'subnets', '%s.json' % self.subnet_id]), + append=['v2.0', 'subnets', '%s' % self.subnet_id]), json={'subnet': expected_subnet}, validate=dict( json={'subnet': {'name': 'goofy'}})) @@ -398,12 +398,12 @@ def test_update_subnet_gateway_ip(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets.json']), + 'network', 'public', append=['v2.0', 'subnets']), json={'subnets': [self.mock_subnet_rep]}), dict(method='PUT', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'subnets', '%s.json' % self.subnet_id]), + append=['v2.0', 'subnets', '%s' % self.subnet_id]), json={'subnet': expected_subnet}, validate=dict( json={'subnet': {'gateway_ip': gateway}})) @@ -418,12 +418,12 @@ def test_update_subnet_disable_gateway_ip(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets.json']), + 'network', 'public', append=['v2.0', 'subnets']), json={'subnets': [self.mock_subnet_rep]}), dict(method='PUT', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'subnets', '%s.json' % self.subnet_id]), + append=['v2.0', 'subnets', '%s' % self.subnet_id]), json={'subnet': expected_subnet}, validate=dict( json={'subnet': {'gateway_ip': None}})) diff --git a/openstack/tests/unit/cloud/test_update_server.py b/openstack/tests/unit/cloud/test_update_server.py index 584c222d2..4fd5f0097 100644 --- a/openstack/tests/unit/cloud/test_update_server.py +++ b/openstack/tests/unit/cloud/test_update_server.py @@ -79,7 +79,7 @@ def test_update_server_name(self): json={'server': {'name': self.updated_server_name}})), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks.json']), + 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), ]) self.assertEqual( From 7040bf8cc62887f0744bf4b5d244133fa329dea0 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 12 Feb 2021 12:44:18 +0000 Subject: [PATCH 2773/3836] Remove unnecessary string formatting A sequel to change I3b4506b81cc5e9f04e99cc22bb7e78d0bbf0c48c. Change-Id: I654941153e8584a2d7c86798a933edf9a5161f48 Signed-off-by: Stephen Finucane --- openstack/cloud/_network.py | 1 - .../tests/unit/cloud/test_delete_server.py | 3 +-- openstack/tests/unit/cloud/test_network.py | 6 +++--- openstack/tests/unit/cloud/test_port.py | 8 ++++---- .../cloud/test_qos_bandwidth_limit_rule.py | 19 +++++++------------ .../unit/cloud/test_qos_dscp_marking_rule.py | 14 +++++--------- .../cloud/test_qos_minimum_bandwidth_rule.py | 15 +++++---------- openstack/tests/unit/cloud/test_qos_policy.py | 12 ++++-------- .../tests/unit/cloud/test_qos_rule_type.py | 2 +- openstack/tests/unit/cloud/test_quotas.py | 9 +++------ openstack/tests/unit/cloud/test_router.py | 4 ++-- openstack/tests/unit/cloud/test_subnet.py | 10 +++++----- 12 files changed, 40 insertions(+), 63 deletions(-) diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index 06ad076d8..e020feb65 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -670,7 +670,6 @@ def get_network_quotas(self, name_or_id, details=False): url = '/quotas/{project_id}'.format(project_id=proj.id) if details: url = url + "/details" - url = url + "" data = proxy._json_response( self.network.get(url), error_message=("Error fetching Neutron's quota for " diff --git a/openstack/tests/unit/cloud/test_delete_server.py b/openstack/tests/unit/cloud/test_delete_server.py index 6d748387d..10f8be27c 100644 --- a/openstack/tests/unit/cloud/test_delete_server.py +++ b/openstack/tests/unit/cloud/test_delete_server.py @@ -178,8 +178,7 @@ def test_delete_server_delete_ips(self): dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'floatingips', - '{fip_id}'.format(fip_id=fip_id)])), + append=['v2.0', 'floatingips', fip_id])), dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'floatingips']), diff --git a/openstack/tests/unit/cloud/test_network.py b/openstack/tests/unit/cloud/test_network.py index 35e5a0cd0..1ad7aab81 100644 --- a/openstack/tests/unit/cloud/test_network.py +++ b/openstack/tests/unit/cloud/test_network.py @@ -299,7 +299,7 @@ def test_delete_network(self): dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'networks', "%s" % network_id]), + append=['v2.0', 'networks', network_id]), json={}) ]) self.assertTrue(self.cloud.delete_network(network_name)) @@ -327,7 +327,7 @@ def test_delete_network_exception(self): dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'networks', "%s" % network_id]), + append=['v2.0', 'networks', network_id]), status_code=503) ]) self.assertRaises(openstack.cloud.OpenStackCloudException, @@ -342,7 +342,7 @@ def test_get_network_by_id(self): dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'networks', "%s" % network_id]), + append=['v2.0', 'networks', network_id]), json={'network': network}) ]) self.assertTrue(self.cloud.get_network_by_id(network_id)) diff --git a/openstack/tests/unit/cloud/test_port.py b/openstack/tests/unit/cloud/test_port.py index fc746964f..fc200986e 100644 --- a/openstack/tests/unit/cloud/test_port.py +++ b/openstack/tests/unit/cloud/test_port.py @@ -192,7 +192,7 @@ def test_update_port(self): dict(method='PUT', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'ports', '%s' % port_id]), + append=['v2.0', 'ports', port_id]), json=self.mock_neutron_port_update_rep, validate=dict( json={'port': {'name': 'test-port-name-updated'}})) @@ -219,7 +219,7 @@ def test_update_port_exception(self): dict(method='PUT', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'ports', '%s' % port_id]), + append=['v2.0', 'ports', port_id]), status_code=500, validate=dict( json={'port': {'name': 'test-port-name-updated'}})) @@ -311,7 +311,7 @@ def test_delete_port(self): dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'ports', '%s' % port_id]), + append=['v2.0', 'ports', port_id]), json={}) ]) @@ -353,7 +353,7 @@ def test_delete_subnet_multiple_using_id(self): dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'ports', '%s' % port1['id']]), + append=['v2.0', 'ports', port1['id']]), json={}) ]) self.assertTrue(self.cloud.delete_port(name_or_id=port1['id'])) diff --git a/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py b/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py index e6c4f52e7..7cd3b5001 100644 --- a/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py @@ -86,8 +86,7 @@ def test_get_qos_bandwidth_limit_rule(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, - 'bandwidth_limit_rules', - '%s' % self.rule_id]), + 'bandwidth_limit_rules', self.rule_id]), json={'bandwidth_limit_rule': self.mock_rule}) ]) r = self.cloud.get_qos_bandwidth_limit_rule(self.policy_name, @@ -235,15 +234,13 @@ def test_update_qos_bandwidth_limit_rule(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, - 'bandwidth_limit_rules', - '%s' % self.rule_id]), + 'bandwidth_limit_rules', self.rule_id]), json={'bandwidth_limit_rule': self.mock_rule}), dict(method='PUT', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, - 'bandwidth_limit_rules', - '%s' % self.rule_id]), + 'bandwidth_limit_rules', self.rule_id]), json={'bandwidth_limit_rule': expected_rule}, validate=dict( json={'bandwidth_limit_rule': { @@ -306,14 +303,14 @@ def test_update_qos_bandwidth_limit_rule_no_qos_direction_extension(self): 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, 'bandwidth_limit_rules', - '%s' % self.rule_id]), + self.rule_id]), json={'bandwidth_limit_rule': self.mock_rule}), dict(method='PUT', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, 'bandwidth_limit_rules', - '%s' % self.rule_id]), + self.rule_id]), json={'bandwidth_limit_rule': expected_rule}, validate=dict( json={'bandwidth_limit_rule': { @@ -346,8 +343,7 @@ def test_delete_qos_bandwidth_limit_rule(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, - 'bandwidth_limit_rules', - '%s' % self.rule_id]), + 'bandwidth_limit_rules', self.rule_id]), json={}) ]) self.assertTrue( @@ -387,8 +383,7 @@ def test_delete_qos_bandwidth_limit_rule_not_found(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, - 'bandwidth_limit_rules', - '%s' % self.rule_id]), + 'bandwidth_limit_rules', self.rule_id]), status_code=404) ]) self.assertFalse( diff --git a/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py b/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py index fe6f08d69..12de28652 100644 --- a/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py +++ b/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py @@ -73,8 +73,7 @@ def test_get_qos_dscp_marking_rule(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, - 'dscp_marking_rules', - '%s' % self.rule_id]), + 'dscp_marking_rules', self.rule_id]), json={'dscp_marking_rule': self.mock_rule}) ]) r = self.cloud.get_qos_dscp_marking_rule(self.policy_name, @@ -192,15 +191,13 @@ def test_update_qos_dscp_marking_rule(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, - 'dscp_marking_rules', - '%s' % self.rule_id]), + 'dscp_marking_rules', self.rule_id]), json={'dscp_marking_rule': self.mock_rule}), dict(method='PUT', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, - 'dscp_marking_rules', - '%s' % self.rule_id]), + 'dscp_marking_rules', self.rule_id]), json={'dscp_marking_rule': expected_rule}, validate=dict( json={'dscp_marking_rule': { @@ -244,7 +241,7 @@ def test_delete_qos_dscp_marking_rule(self): 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, 'dscp_marking_rules', - '%s' % self.rule_id]), + self.rule_id]), json={}) ]) self.assertTrue( @@ -284,8 +281,7 @@ def test_delete_qos_dscp_marking_rule_not_found(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, - 'dscp_marking_rules', - '%s' % self.rule_id]), + 'dscp_marking_rules', self.rule_id]), status_code=404) ]) self.assertFalse( diff --git a/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py b/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py index 3787da4c3..5ccbb5c38 100644 --- a/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py +++ b/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py @@ -74,8 +74,7 @@ def test_get_qos_minimum_bandwidth_rule(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, - 'minimum_bandwidth_rules', - '%s' % self.rule_id]), + 'minimum_bandwidth_rules', self.rule_id]), json={'minimum_bandwidth_rule': self.mock_rule}) ]) r = self.cloud.get_qos_minimum_bandwidth_rule(self.policy_name, @@ -192,15 +191,13 @@ def test_update_qos_minimum_bandwidth_rule(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, - 'minimum_bandwidth_rules', - '%s' % self.rule_id]), + 'minimum_bandwidth_rules', self.rule_id]), json={'minimum_bandwidth_rule': self.mock_rule}), dict(method='PUT', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, - 'minimum_bandwidth_rules', - '%s' % self.rule_id]), + 'minimum_bandwidth_rules', self.rule_id]), json={'minimum_bandwidth_rule': expected_rule}, validate=dict( json={'minimum_bandwidth_rule': { @@ -243,8 +240,7 @@ def test_delete_qos_minimum_bandwidth_rule(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, - 'minimum_bandwidth_rules', - '%s' % self.rule_id]), + 'minimum_bandwidth_rules', self.rule_id]), json={}) ]) self.assertTrue( @@ -284,8 +280,7 @@ def test_delete_qos_minimum_bandwidth_rule_not_found(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, - 'minimum_bandwidth_rules', - '%s' % self.rule_id]), + 'minimum_bandwidth_rules', self.rule_id]), status_code=404) ]) self.assertFalse( diff --git a/openstack/tests/unit/cloud/test_qos_policy.py b/openstack/tests/unit/cloud/test_qos_policy.py index 1becef133..bedaf59f1 100644 --- a/openstack/tests/unit/cloud/test_qos_policy.py +++ b/openstack/tests/unit/cloud/test_qos_policy.py @@ -155,8 +155,7 @@ def test_delete_qos_policy(self): dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies', - '%s' % self.policy_id]), + append=['v2.0', 'qos', 'policies', self.policy_id]), json={}) ]) self.assertTrue(self.cloud.delete_qos_policy(self.policy_name)) @@ -236,8 +235,7 @@ def test_delete_qos_policy_multiple_using_id(self): dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies', - '%s' % self.policy_id]), + append=['v2.0', 'qos', 'policies', self.policy_id]), json={}) ]) self.assertTrue(self.cloud.delete_qos_policy(policy1['id'])) @@ -263,8 +261,7 @@ def test_update_qos_policy(self): dict(method='PUT', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies', - '%s' % self.policy_id]), + append=['v2.0', 'qos', 'policies', self.policy_id]), json={'policy': expected_policy}, validate=dict( json={'policy': {'name': 'goofy'}})) @@ -310,8 +307,7 @@ def test_update_qos_policy_no_qos_default_extension(self): dict(method='PUT', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies', - '%s' % self.policy_id]), + append=['v2.0', 'qos', 'policies', self.policy_id]), json={'policy': expected_policy}, validate=dict( json={'policy': {'name': "goofy"}})) diff --git a/openstack/tests/unit/cloud/test_qos_rule_type.py b/openstack/tests/unit/cloud/test_qos_rule_type.py index 01bb743f7..e009b3a9d 100644 --- a/openstack/tests/unit/cloud/test_qos_rule_type.py +++ b/openstack/tests/unit/cloud/test_qos_rule_type.py @@ -111,7 +111,7 @@ def test_get_qos_rule_type_details(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'rule-types', - '%s' % self.rule_type_name]), + self.rule_type_name]), json={'rule_type': self.mock_rule_type_details}) ]) self.assertEqual( diff --git a/openstack/tests/unit/cloud/test_quotas.py b/openstack/tests/unit/cloud/test_quotas.py index a35559d89..fc480dff5 100644 --- a/openstack/tests/unit/cloud/test_quotas.py +++ b/openstack/tests/unit/cloud/test_quotas.py @@ -154,8 +154,7 @@ def test_neutron_update_quotas(self): dict(method='PUT', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'quotas', - '%s' % project.project_id]), + append=['v2.0', 'quotas', project.project_id]), json={}, validate=dict( json={'quota': {'network': 1}})) @@ -181,8 +180,7 @@ def test_neutron_get_quotas(self): dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'quotas', - '%s' % project.project_id]), + append=['v2.0', 'quotas', project.project_id]), json={'quota': quota}) ]) received_quota = self.cloud.get_network_quotas(project.project_id) @@ -250,8 +248,7 @@ def test_neutron_delete_quotas(self): dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'quotas', - '%s' % project.project_id]), + append=['v2.0', 'quotas', project.project_id]), json={}) ]) self.cloud.delete_network_quotas(project.project_id) diff --git a/openstack/tests/unit/cloud/test_router.py b/openstack/tests/unit/cloud/test_router.py index 70a296077..e3996b420 100644 --- a/openstack/tests/unit/cloud/test_router.py +++ b/openstack/tests/unit/cloud/test_router.py @@ -274,7 +274,7 @@ def test_update_router(self): dict(method='PUT', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'routers', '%s' % self.router_id]), + append=['v2.0', 'routers', self.router_id]), json={'router': expected_router_rep}, validate=dict( json={'router': { @@ -295,7 +295,7 @@ def test_delete_router(self): dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'routers', '%s' % self.router_id]), + append=['v2.0', 'routers', self.router_id]), json={}) ]) self.assertTrue(self.cloud.delete_router(self.router_name)) diff --git a/openstack/tests/unit/cloud/test_subnet.py b/openstack/tests/unit/cloud/test_subnet.py index 7a71116e2..d8bafeaa2 100644 --- a/openstack/tests/unit/cloud/test_subnet.py +++ b/openstack/tests/unit/cloud/test_subnet.py @@ -324,7 +324,7 @@ def test_delete_subnet(self): dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'subnets', '%s' % self.subnet_id]), + append=['v2.0', 'subnets', self.subnet_id]), json={}) ]) self.assertTrue(self.cloud.delete_subnet(self.subnet_name)) @@ -365,7 +365,7 @@ def test_delete_subnet_multiple_using_id(self): dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'subnets', '%s' % subnet1['id']]), + append=['v2.0', 'subnets', subnet1['id']]), json={}) ]) self.assertTrue(self.cloud.delete_subnet(subnet1['id'])) @@ -382,7 +382,7 @@ def test_update_subnet(self): dict(method='PUT', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'subnets', '%s' % self.subnet_id]), + append=['v2.0', 'subnets', self.subnet_id]), json={'subnet': expected_subnet}, validate=dict( json={'subnet': {'name': 'goofy'}})) @@ -403,7 +403,7 @@ def test_update_subnet_gateway_ip(self): dict(method='PUT', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'subnets', '%s' % self.subnet_id]), + append=['v2.0', 'subnets', self.subnet_id]), json={'subnet': expected_subnet}, validate=dict( json={'subnet': {'gateway_ip': gateway}})) @@ -423,7 +423,7 @@ def test_update_subnet_disable_gateway_ip(self): dict(method='PUT', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'subnets', '%s' % self.subnet_id]), + append=['v2.0', 'subnets', self.subnet_id]), json={'subnet': expected_subnet}, validate=dict( json={'subnet': {'gateway_ip': None}})) From 9af808ffc9bb374eb98f5d574b13201475fe7d76 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 17 Feb 2021 15:09:54 +0100 Subject: [PATCH 2774/3836] Prevent the endless loop in resource listing There is potential (i.e. bug in older nova https://bugs.launchpad.net/nova/+bug/1721791) for resource listing to end up in an endless loop. Try preventing this if next marker is same as marker we have just used. Change-Id: I5fb9ff1675cef7c4cb5e1f50a9efbbd8efc23900 --- openstack/resource.py | 13 ++++++++++++- openstack/tests/unit/test_resource.py | 25 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/openstack/resource.py b/openstack/resource.py index 185b13dde..3f04dde42 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1746,7 +1746,7 @@ def list(cls, session, paginated=True, base_path=None, data = response.json() # Discard any existing pagination keys - query_params.pop('marker', None) + last_marker = query_params.pop('marker', None) query_params.pop('limit', None) if cls.resources_key: @@ -1777,6 +1777,17 @@ def list(cls, session, paginated=True, base_path=None, if resources and paginated: uri, next_params = cls._get_next_link( uri, response, data, marker, limit, total_yielded) + try: + if next_params['marker'] == last_marker: + # If next page marker is same as what we were just + # asked something went terribly wrong. Some ancient + # services had bugs. + raise exceptions.SDKException( + 'Endless pagination loop detected, aborting' + ) + except KeyError: + # do nothing, exception handling is cheaper then "if" + pass query_params.update(next_params) else: return diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 7c9f2a442..53b4bcb9a 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -2086,6 +2086,31 @@ def test_list_multi_page_response_not_paginated(self): self.assertEqual(ids[0], results[0].id) self.assertIsInstance(results[0], self.test_class) + def test_list_paginated_infinite_loop(self): + q_limit = 1 + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.links = {} + mock_response.json.side_effect = [ + { + "resources": [{"id": 1}], + }, { + "resources": [{"id": 1}], + }] + + self.session.get.return_value = mock_response + + class Test(self.test_class): + _query_mapping = resource.QueryParameters("limit") + + res = Test.list(self.session, paginated=True, limit=q_limit) + + self.assertRaises( + exceptions.SDKException, + list, + res + ) + def test_list_query_params(self): id = 1 qp = "query param!" From 1219655ca684e04898e447ab2e57650ecee627c8 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 17 Feb 2021 15:23:19 +0100 Subject: [PATCH 2775/3836] Set resource URI properties in listing method When resource listing is invoked the URI based properties are not saved on the resource, as opposed to all other methods. Fix this by injecting them into each fetched resource. Change-Id: I95cb50a691d095764df30f46c204ea5ee80dacca --- openstack/resource.py | 11 +++++++++++ openstack/tests/unit/test_resource.py | 2 ++ 2 files changed, 13 insertions(+) diff --git a/openstack/resource.py b/openstack/resource.py index 185b13dde..23bb16222 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1729,9 +1729,18 @@ def list(cls, session, paginated=True, base_path=None, allow_unknown_params=allow_unknown_params) query_params = cls._query_mapping._transpose(params, cls) uri = base_path % params + uri_params = {} limit = query_params.get('limit') + for k, v in params.items(): + # We need to gather URI parts to set them on the resource later + if ( + hasattr(cls, k) + and isinstance(getattr(cls, k), URI) + ): + uri_params[k] = v + # Track the total number of resources yielded so we can paginate # swift objects total_yielded = 0 @@ -1765,6 +1774,8 @@ def list(cls, session, paginated=True, base_path=None, # Resource initializer. "self" is already the first # argument and is practically a reserved word. raw_resource.pop("self", None) + # We want that URI props are available on the resource + raw_resource.update(uri_params) value = cls.existing( microversion=microversion, diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 7c9f2a442..c382b3de1 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -2113,6 +2113,8 @@ class Test(self.test_class): query_param=qp, something=uri_param)) self.assertEqual(1, len(results)) + # Verify URI attribute is set on the resource + self.assertEqual(results[0].something, uri_param) # Look at the `params` argument to each of the get calls that # were made. From e9dde2925b9f52b0ef94b2bd76cdd35512ae89d0 Mon Sep 17 00:00:00 2001 From: Ade Lee Date: Wed, 17 Feb 2021 16:37:08 -0500 Subject: [PATCH 2776/3836] Add TODO to remove md5 wrapper Added TODO to make sure we remember to remove the md5 wrapper when the minimum python distribution supports it. Change-Id: I2a489b0f645c77d58c7de070f88c1d79832e92cf --- openstack/utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openstack/utils.py b/openstack/utils.py index 8adbc54b6..5fff2b3e5 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -239,6 +239,10 @@ def maximum_supported_microversion(adapter, client_maximum): # Python distributions that support a hashlib.md5 with the usedforsecurity # keyword can just use that md5 definition as-is # See https://bugs.python.org/issue9216 + # + # TODO(alee) Remove this wrapper when the minimum python version is bumped + # to 3.9 (which is the first upstream version to support this keyword) + # See https://docs.python.org/3.9/library/hashlib.html md5 = hashlib.md5 except TypeError: def md5(string=b'', usedforsecurity=True): From 3371e2b0ed1fc6c5c38f6e99cfa3f30132bc6fcd Mon Sep 17 00:00:00 2001 From: Ryan Zimmerman Date: Wed, 17 Feb 2021 23:21:14 -0800 Subject: [PATCH 2777/3836] Add compute microversion 2.70 Compute 2.70 [1] added the tag parameter to the server interface and volume attachment response bodies. [1] https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id64 Change-Id: Ibde8e269db82d610d142b7785f1b1d8b91f495b0 --- openstack/compute/v2/server_interface.py | 4 ++++ openstack/compute/v2/volume_attachment.py | 4 ++++ openstack/tests/unit/compute/v2/test_server_interface.py | 2 ++ openstack/tests/unit/compute/v2/test_volume_attachment.py | 2 ++ 4 files changed, 12 insertions(+) diff --git a/openstack/compute/v2/server_interface.py b/openstack/compute/v2/server_interface.py index f12213c54..52d3bffa8 100644 --- a/openstack/compute/v2/server_interface.py +++ b/openstack/compute/v2/server_interface.py @@ -37,3 +37,7 @@ class ServerInterface(resource.Resource): port_state = resource.Body('port_state') #: The ID for the server. server_id = resource.URI('server_id') + #: Tags for the virtual interfaces. + tag = resource.Body('tag') + # tag introduced in 2.70 + _max_microversion = '2.70' diff --git a/openstack/compute/v2/volume_attachment.py b/openstack/compute/v2/volume_attachment.py index 750a62a58..770395c7d 100644 --- a/openstack/compute/v2/volume_attachment.py +++ b/openstack/compute/v2/volume_attachment.py @@ -37,3 +37,7 @@ class VolumeAttachment(resource.Resource): volume_id = resource.Body('volumeId') #: The ID of the attachment you want to delete or update. attachment_id = resource.Body('attachment_id', alternate_id=True) + #: Virtual device tags for the attachment. + tag = resource.Body('tag') + # tag introduced in 2.70 + _max_microversion = '2.70' diff --git a/openstack/tests/unit/compute/v2/test_server_interface.py b/openstack/tests/unit/compute/v2/test_server_interface.py index 9b4fa7f2b..5c3ef7e68 100644 --- a/openstack/tests/unit/compute/v2/test_server_interface.py +++ b/openstack/tests/unit/compute/v2/test_server_interface.py @@ -27,6 +27,7 @@ 'port_id': '4', 'port_state': '5', 'server_id': '6', + 'tag': '7', } @@ -51,3 +52,4 @@ def test_make_it(self): self.assertEqual(EXAMPLE['port_id'], sot.port_id) self.assertEqual(EXAMPLE['port_state'], sot.port_state) self.assertEqual(EXAMPLE['server_id'], sot.server_id) + self.assertEqual(EXAMPLE['tag'], sot.tag) diff --git a/openstack/tests/unit/compute/v2/test_volume_attachment.py b/openstack/tests/unit/compute/v2/test_volume_attachment.py index 66068dce9..7a7a55c7d 100644 --- a/openstack/tests/unit/compute/v2/test_volume_attachment.py +++ b/openstack/tests/unit/compute/v2/test_volume_attachment.py @@ -18,6 +18,7 @@ 'device': '1', 'id': '2', 'volume_id': '3', + 'tag': '4', } @@ -44,3 +45,4 @@ def test_make_it(self): self.assertEqual(EXAMPLE['device'], sot.device) self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['volume_id'], sot.volume_id) + self.assertEqual(EXAMPLE['tag'], sot.tag) From 330edbb208abef40da58693d7ebe39d32f5913d8 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 15 Jan 2021 15:38:08 +0100 Subject: [PATCH 2778/3836] Switch cloud.networking.qos* operations to rely on proxy layer This change switches Neutron Qos* operations in the cloud layer to use the proxy layer instead of reimplementing direct requests. Change-Id: Ide4125caa6c45e8d7d985b3e6a227a619ac94c40 --- openstack/cloud/_network.py | 254 ++++++------------ openstack/network/v2/qos_policy.py | 4 +- openstack/network/v2/qos_rule_type.py | 2 +- .../tests/functional/cloud/test_qos_policy.py | 10 +- openstack/tests/unit/cloud/test_caching.py | 3 +- .../cloud/test_qos_bandwidth_limit_rule.py | 112 ++++---- .../unit/cloud/test_qos_dscp_marking_rule.py | 74 ++--- .../cloud/test_qos_minimum_bandwidth_rule.py | 77 +++--- openstack/tests/unit/cloud/test_qos_policy.py | 85 +++--- .../tests/unit/cloud/test_qos_rule_type.py | 12 +- openstack/tests/unit/cloud/test_shade.py | 4 +- .../tests/unit/network/v2/test_qos_policy.py | 2 +- 12 files changed, 284 insertions(+), 355 deletions(-) diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index e020feb65..16e78fb41 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -34,11 +34,7 @@ def __init__(self): @_utils.cache_on_arguments() def _neutron_extensions(self): extensions = set() - resp = self.network.get('/extensions') - data = proxy._json_response( - resp, - error_message="Error fetching extension list for neutron") - for extension in self._get_and_munchify('extensions', data): + for extension in self.network.extensions(): extensions.add(extension['alias']) return extensions @@ -232,8 +228,15 @@ def get_qos_policy(self, name_or_id, filters=None): found. """ - return _utils._get_entity( - self, 'qos_policie', name_or_id, filters) + if not self._has_neutron_extension('qos'): + raise exc.OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + if not filters: + filters = {} + return self.network.find_qos_policy( + name_or_id=name_or_id, + ignore_missing=True, + **filters) def search_qos_policies(self, name_or_id=None, filters=None): """Search QoS policies @@ -247,8 +250,15 @@ def search_qos_policies(self, name_or_id=None, filters=None): :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ - policies = self.list_qos_policies(filters) - return _utils._filter_list(policies, name_or_id, filters) + if not self._has_neutron_extension('qos'): + raise exc.OpenStackCloudUnavailableExtension( + 'QoS extension is not available on target cloud') + query = {} + if name_or_id: + query['name'] = name_or_id + if filters: + query.update(filters) + return list(self.network.qos_policies(**query)) def list_qos_rule_types(self, filters=None): """List all available QoS rule types. @@ -264,11 +274,7 @@ def list_qos_rule_types(self, filters=None): # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} - resp = self.network.get("/qos/rule-types", params=filters) - data = proxy._json_response( - resp, - error_message="Error fetching QoS rule types list") - return self._get_and_munchify('rule_types', data) + return list(self.network.qos_rule_types(**filters)) def get_qos_rule_type_details(self, rule_type, filters=None): """Get a QoS rule type details by rule type name. @@ -288,13 +294,7 @@ def get_qos_rule_type_details(self, rule_type, filters=None): 'qos-rule-type-details extension is not available ' 'on target cloud') - resp = self.network.get( - "/qos/rule-types/{rule_type}".format(rule_type=rule_type)) - data = proxy._json_response( - resp, - error_message="Error fetching QoS details of {rule_type} " - "rule type".format(rule_type=rule_type)) - return self._get_and_munchify('rule_type', data) + return self.network.get_qos_rule_type(rule_type) def list_qos_policies(self, filters=None): """List all available QoS policies. @@ -309,11 +309,7 @@ def list_qos_policies(self, filters=None): # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} - resp = self.network.get("/qos/policies", params=filters) - data = proxy._json_response( - resp, - error_message="Error fetching QoS policies list") - return self._get_and_munchify('policies', data) + return list(self.network.qos_policies(**filters)) def get_network(self, name_or_id, filters=None): """Get a network by name or ID. @@ -1193,8 +1189,7 @@ def create_qos_policy(self, **kwargs): self.log.debug("'qos-default' extension is not available on " "target cloud") - data = self.network.post("/qos/policies", json={'policy': kwargs}) - return self._get_and_munchify('policy', data) + return self.network.create_qos_policy(**kwargs) @_utils.valid_kwargs("name", "description", "shared", "default", "project_id") @@ -1231,16 +1226,11 @@ def update_qos_policy(self, name_or_id, **kwargs): self.log.debug("No QoS policy data to update") return - curr_policy = self.get_qos_policy(name_or_id) + curr_policy = self.network.find_qos_policy(name_or_id) if not curr_policy: raise exc.OpenStackCloudException( "QoS policy %s not found." % name_or_id) - - data = self.network.put( - "/qos/policies/{policy_id}".format( - policy_id=curr_policy['id']), - json={'policy': kwargs}) - return self._get_and_munchify('policy', data) + return self.network.update_qos_policy(curr_policy, **kwargs) def delete_qos_policy(self, name_or_id): """Delete a QoS policy. @@ -1254,18 +1244,17 @@ def delete_qos_policy(self, name_or_id): if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') - policy = self.get_qos_policy(name_or_id) + policy = self.network.find_qos_policy(name_or_id) if not policy: self.log.debug("QoS policy %s not found for deleting", name_or_id) return False - - exceptions.raise_from_response(self.network.delete( - "/qos/policies/{policy_id}".format(policy_id=policy['id']))) + self.network.delete_qos_policy(policy) return True - def search_qos_bandwidth_limit_rules(self, policy_name_or_id, rule_id=None, - filters=None): + def search_qos_bandwidth_limit_rules( + self, policy_name_or_id, rule_id=None, filters=None + ): """Search QoS bandwidth limit rules :param string policy_name_or_id: Name or ID of the QoS policy to which @@ -1298,7 +1287,7 @@ def list_qos_bandwidth_limit_rules(self, policy_name_or_id, filters=None): raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') - policy = self.get_qos_policy(policy_name_or_id) + policy = self.network.find_qos_policy(policy_name_or_id) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( @@ -1308,15 +1297,8 @@ def list_qos_bandwidth_limit_rules(self, policy_name_or_id, filters=None): if not filters: filters = {} - resp = self.network.get( - "/qos/policies/{policy_id}/bandwidth_limit_rules".format( - policy_id=policy['id']), - params=filters) - data = proxy._json_response( - resp, - error_message="Error fetching QoS bandwidth limit rules from " - "{policy}".format(policy=policy['id'])) - return self._get_and_munchify('bandwidth_limit_rules', data) + return list(self.network.qos_bandwidth_limit_rules( + qos_policy=policy, **filters)) def get_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): """Get a QoS bandwidth limit rule by name or ID. @@ -1333,21 +1315,14 @@ def get_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') - policy = self.get_qos_policy(policy_name_or_id) + policy = self.network.find_qos_policy(policy_name_or_id) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) - resp = self.network.get( - "/qos/policies/{policy_id}/bandwidth_limit_rules/{rule_id}". - format(policy_id=policy['id'], rule_id=rule_id)) - data = proxy._json_response( - resp, - error_message="Error fetching QoS bandwidth limit rule {rule_id} " - "from {policy}".format(rule_id=rule_id, - policy=policy['id'])) - return self._get_and_munchify('bandwidth_limit_rule', data) + return self.network.get_qos_bandwidth_limit_rule( + rule_id, policy) @_utils.valid_kwargs("max_burst_kbps", "direction") def create_qos_bandwidth_limit_rule(self, policy_name_or_id, max_kbps, @@ -1369,7 +1344,7 @@ def create_qos_bandwidth_limit_rule(self, policy_name_or_id, max_kbps, raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') - policy = self.get_qos_policy(policy_name_or_id) + policy = self.network.find_qos_policy(policy_name_or_id) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( @@ -1383,11 +1358,7 @@ def create_qos_bandwidth_limit_rule(self, policy_name_or_id, max_kbps, "target cloud") kwargs['max_kbps'] = max_kbps - data = self.network.post( - "/qos/policies/{policy_id}/bandwidth_limit_rules".format( - policy_id=policy['id']), - json={'bandwidth_limit_rule': kwargs}) - return self._get_and_munchify('bandwidth_limit_rule', data) + return self.network.create_qos_bandwidth_limit_rule(policy, **kwargs) @_utils.valid_kwargs("max_kbps", "max_burst_kbps", "direction") def update_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id, @@ -1410,7 +1381,9 @@ def update_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id, raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') - policy = self.get_qos_policy(policy_name_or_id) + policy = self.network.find_qos_policy( + policy_name_or_id, + ignore_missing=True) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( @@ -1427,19 +1400,16 @@ def update_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id, self.log.debug("No QoS bandwidth limit rule data to update") return - curr_rule = self.get_qos_bandwidth_limit_rule( - policy_name_or_id, rule_id) + curr_rule = self.network.get_qos_bandwidth_limit_rule( + qos_rule=rule_id, qos_policy=policy) if not curr_rule: raise exc.OpenStackCloudException( "QoS bandwidth_limit_rule {rule_id} not found in policy " "{policy_id}".format(rule_id=rule_id, policy_id=policy['id'])) - data = self.network.put( - "/qos/policies/{policy_id}/bandwidth_limit_rules/{rule_id}". - format(policy_id=policy['id'], rule_id=rule_id), - json={'bandwidth_limit_rule': kwargs}) - return self._get_and_munchify('bandwidth_limit_rule', data) + return self.network.update_qos_bandwidth_limit_rule( + qos_rule=curr_rule, qos_policy=policy, **kwargs) def delete_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): """Delete a QoS bandwidth limit rule. @@ -1454,17 +1424,16 @@ def delete_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') - policy = self.get_qos_policy(policy_name_or_id) + policy = self.network.find_qos_policy(policy_name_or_id) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) try: - exceptions.raise_from_response(self.network.delete( - "/qos/policies/{policy}/bandwidth_limit_rules/{rule}". - format(policy=policy['id'], rule=rule_id))) - except exc.OpenStackCloudURINotFound: + self.network.delete_qos_bandwidth_limit_rule( + rule_id, policy, ignore_missing=False) + except exceptions.ResourceNotFound: self.log.debug( "QoS bandwidth limit rule {rule_id} not found in policy " "{policy_id}. Ignoring.".format(rule_id=rule_id, @@ -1507,7 +1476,8 @@ def list_qos_dscp_marking_rules(self, policy_name_or_id, filters=None): raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') - policy = self.get_qos_policy(policy_name_or_id) + policy = self.network.find_qos_policy( + policy_name_or_id, ignore_missing=True) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( @@ -1517,15 +1487,7 @@ def list_qos_dscp_marking_rules(self, policy_name_or_id, filters=None): if not filters: filters = {} - resp = self.network.get( - "/qos/policies/{policy_id}/dscp_marking_rules".format( - policy_id=policy['id']), - params=filters) - data = proxy._json_response( - resp, - error_message="Error fetching QoS DSCP marking rules from " - "{policy}".format(policy=policy['id'])) - return self._get_and_munchify('dscp_marking_rules', data) + return list(self.network.qos_dscp_marking_rules(policy, **filters)) def get_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): """Get a QoS DSCP marking rule by name or ID. @@ -1542,21 +1504,13 @@ def get_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') - policy = self.get_qos_policy(policy_name_or_id) + policy = self.network.find_qos_policy(policy_name_or_id) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) - resp = self.network.get( - "/qos/policies/{policy_id}/dscp_marking_rules/{rule_id}". - format(policy_id=policy['id'], rule_id=rule_id)) - data = proxy._json_response( - resp, - error_message="Error fetching QoS DSCP marking rule {rule_id} " - "from {policy}".format(rule_id=rule_id, - policy=policy['id'])) - return self._get_and_munchify('dscp_marking_rule', data) + return self.network.get_qos_dscp_marking_rule(rule_id, policy) def create_qos_dscp_marking_rule(self, policy_name_or_id, dscp_mark): """Create a QoS DSCP marking rule. @@ -1572,20 +1526,14 @@ def create_qos_dscp_marking_rule(self, policy_name_or_id, dscp_mark): raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') - policy = self.get_qos_policy(policy_name_or_id) + policy = self.network.find_qos_policy(policy_name_or_id) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) - body = { - 'dscp_mark': dscp_mark - } - data = self.network.post( - "/qos/policies/{policy_id}/dscp_marking_rules".format( - policy_id=policy['id']), - json={'dscp_marking_rule': body}) - return self._get_and_munchify('dscp_marking_rule', data) + return self.network.create_qos_dscp_marking_rule( + policy, dscp_mark=dscp_mark) @_utils.valid_kwargs("dscp_mark") def update_qos_dscp_marking_rule(self, policy_name_or_id, rule_id, @@ -1604,7 +1552,7 @@ def update_qos_dscp_marking_rule(self, policy_name_or_id, rule_id, raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') - policy = self.get_qos_policy(policy_name_or_id) + policy = self.network.find_qos_policy(policy_name_or_id) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( @@ -1614,19 +1562,16 @@ def update_qos_dscp_marking_rule(self, policy_name_or_id, rule_id, self.log.debug("No QoS DSCP marking rule data to update") return - curr_rule = self.get_qos_dscp_marking_rule( - policy_name_or_id, rule_id) + curr_rule = self.network.get_qos_dscp_marking_rule( + rule_id, policy) if not curr_rule: raise exc.OpenStackCloudException( "QoS dscp_marking_rule {rule_id} not found in policy " "{policy_id}".format(rule_id=rule_id, policy_id=policy['id'])) - data = self.network.put( - "/qos/policies/{policy_id}/dscp_marking_rules/{rule_id}". - format(policy_id=policy['id'], rule_id=rule_id), - json={'dscp_marking_rule': kwargs}) - return self._get_and_munchify('dscp_marking_rule', data) + return self.network.update_qos_dscp_marking_rule( + curr_rule, policy, **kwargs) def delete_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): """Delete a QoS DSCP marking rule. @@ -1641,17 +1586,16 @@ def delete_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') - policy = self.get_qos_policy(policy_name_or_id) + policy = self.network.find_qos_policy(policy_name_or_id) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) try: - exceptions.raise_from_response(self.network.delete( - "/qos/policies/{policy}/dscp_marking_rules/{rule}". - format(policy=policy['id'], rule=rule_id))) - except exc.OpenStackCloudURINotFound: + self.network.delete_qos_dscp_marking_rule( + rule_id, policy, ignore_missing=False) + except exceptions.ResourceNotFound: self.log.debug( "QoS DSCP marking rule {rule_id} not found in policy " "{policy_id}. Ignoring.".format(rule_id=rule_id, @@ -1696,7 +1640,7 @@ def list_qos_minimum_bandwidth_rules(self, policy_name_or_id, raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') - policy = self.get_qos_policy(policy_name_or_id) + policy = self.network.find_qos_policy(policy_name_or_id) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( @@ -1706,15 +1650,9 @@ def list_qos_minimum_bandwidth_rules(self, policy_name_or_id, if not filters: filters = {} - resp = self.network.get( - "/qos/policies/{policy_id}/minimum_bandwidth_rules".format( - policy_id=policy['id']), - params=filters) - data = proxy._json_response( - resp, - error_message="Error fetching QoS minimum bandwidth rules from " - "{policy}".format(policy=policy['id'])) - return self._get_and_munchify('minimum_bandwidth_rules', data) + return list( + self.network.qos_minimum_bandwidth_rules( + policy, **filters)) def get_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): """Get a QoS minimum bandwidth rule by name or ID. @@ -1731,25 +1669,18 @@ def get_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') - policy = self.get_qos_policy(policy_name_or_id) + policy = self.network.find_qos_policy(policy_name_or_id) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) - resp = self.network.get( - "/qos/policies/{policy_id}/minimum_bandwidth_rules/{rule_id}". - format(policy_id=policy['id'], rule_id=rule_id)) - data = proxy._json_response( - resp, - error_message="Error fetching QoS minimum_bandwidth rule {rule_id}" - " from {policy}".format(rule_id=rule_id, - policy=policy['id'])) - return self._get_and_munchify('minimum_bandwidth_rule', data) + return self.network.get_qos_minimum_bandwidth_rule(rule_id, policy) @_utils.valid_kwargs("direction") - def create_qos_minimum_bandwidth_rule(self, policy_name_or_id, min_kbps, - **kwargs): + def create_qos_minimum_bandwidth_rule( + self, policy_name_or_id, min_kbps, **kwargs + ): """Create a QoS minimum bandwidth limit rule. :param string policy_name_or_id: Name or ID of the QoS policy to which @@ -1765,22 +1696,19 @@ def create_qos_minimum_bandwidth_rule(self, policy_name_or_id, min_kbps, raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') - policy = self.get_qos_policy(policy_name_or_id) + policy = self.network.find_qos_policy(policy_name_or_id) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) kwargs['min_kbps'] = min_kbps - data = self.network.post( - "/qos/policies/{policy_id}/minimum_bandwidth_rules".format( - policy_id=policy['id']), - json={'minimum_bandwidth_rule': kwargs}) - return self._get_and_munchify('minimum_bandwidth_rule', data) + return self.network.create_qos_minimum_bandwidth_rule(policy, **kwargs) @_utils.valid_kwargs("min_kbps", "direction") - def update_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id, - **kwargs): + def update_qos_minimum_bandwidth_rule( + self, policy_name_or_id, rule_id, **kwargs + ): """Update a QoS minimum bandwidth rule. :param string policy_name_or_id: Name or ID of the QoS policy to which @@ -1797,7 +1725,7 @@ def update_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id, raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') - policy = self.get_qos_policy(policy_name_or_id) + policy = self.network.find_qos_policy(policy_name_or_id) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( @@ -1807,19 +1735,16 @@ def update_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id, self.log.debug("No QoS minimum bandwidth rule data to update") return - curr_rule = self.get_qos_minimum_bandwidth_rule( - policy_name_or_id, rule_id) + curr_rule = self.network.get_qos_minimum_bandwidth_rule( + rule_id, policy) if not curr_rule: raise exc.OpenStackCloudException( "QoS minimum_bandwidth_rule {rule_id} not found in policy " "{policy_id}".format(rule_id=rule_id, policy_id=policy['id'])) - data = self.network.put( - "/qos/policies/{policy_id}/minimum_bandwidth_rules/{rule_id}". - format(policy_id=policy['id'], rule_id=rule_id), - json={'minimum_bandwidth_rule': kwargs}) - return self._get_and_munchify('minimum_bandwidth_rule', data) + return self.network.update_qos_minimum_bandwidth_rule( + curr_rule, policy, **kwargs) def delete_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): """Delete a QoS minimum bandwidth rule. @@ -1834,17 +1759,16 @@ def delete_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') - policy = self.get_qos_policy(policy_name_or_id) + policy = self.network.find_qos_policy(policy_name_or_id) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id)) try: - exceptions.raise_from_response(self.network.delete( - "/qos/policies/{policy}/minimum_bandwidth_rules/{rule}". - format(policy=policy['id'], rule=rule_id))) - except exc.OpenStackCloudURINotFound: + self.network.delete_qos_minimum_bandwidth_rule( + rule_id, policy, ignore_missing=False) + except exceptions.ResourceNotFound: self.log.debug( "QoS minimum bandwidth rule {rule_id} not found in policy " "{policy_id}. Ignoring.".format(rule_id=rule_id, diff --git a/openstack/network/v2/qos_policy.py b/openstack/network/v2/qos_policy.py index 162de8ed6..4b3584c5d 100644 --- a/openstack/network/v2/qos_policy.py +++ b/openstack/network/v2/qos_policy.py @@ -38,7 +38,9 @@ class QoSPolicy(resource.Resource, resource.TagMixin): name = resource.Body('name') #: The ID of the project who owns the network. Only administrative #: users can specify a project ID other than their own. - project_id = resource.Body('tenant_id') + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) #: The QoS policy description. description = resource.Body('description') #: Indicates whether this QoS policy is the default policy for this diff --git a/openstack/network/v2/qos_rule_type.py b/openstack/network/v2/qos_rule_type.py index cf23a448f..5f771a631 100644 --- a/openstack/network/v2/qos_rule_type.py +++ b/openstack/network/v2/qos_rule_type.py @@ -29,6 +29,6 @@ class QoSRuleType(resource.Resource): # Properties #: QoS rule type name. - type = resource.Body('type') + type = resource.Body('type', alternate_id=True) #: List of QoS backend drivers supporting this QoS rule type drivers = resource.Body('drivers') diff --git a/openstack/tests/functional/cloud/test_qos_policy.py b/openstack/tests/functional/cloud/test_qos_policy.py index 5a6763eab..f78834383 100644 --- a/openstack/tests/functional/cloud/test_qos_policy.py +++ b/openstack/tests/functional/cloud/test_qos_policy.py @@ -49,7 +49,7 @@ def test_create_qos_policy_basic(self): policy = self.operator_cloud.create_qos_policy(name=self.policy_name) self.assertIn('id', policy) self.assertEqual(self.policy_name, policy['name']) - self.assertFalse(policy['shared']) + self.assertFalse(policy['is_shared']) self.assertFalse(policy['is_default']) def test_create_qos_policy_shared(self): @@ -57,7 +57,7 @@ def test_create_qos_policy_shared(self): name=self.policy_name, shared=True) self.assertIn('id', policy) self.assertEqual(self.policy_name, policy['name']) - self.assertTrue(policy['shared']) + self.assertTrue(policy['is_shared']) self.assertFalse(policy['is_default']) def test_create_qos_policy_default(self): @@ -68,19 +68,19 @@ def test_create_qos_policy_default(self): name=self.policy_name, default=True) self.assertIn('id', policy) self.assertEqual(self.policy_name, policy['name']) - self.assertFalse(policy['shared']) + self.assertFalse(policy['is_shared']) self.assertTrue(policy['is_default']) def test_update_qos_policy(self): policy = self.operator_cloud.create_qos_policy(name=self.policy_name) self.assertEqual(self.policy_name, policy['name']) - self.assertFalse(policy['shared']) + self.assertFalse(policy['is_shared']) self.assertFalse(policy['is_default']) updated_policy = self.operator_cloud.update_qos_policy( policy['id'], shared=True, default=True) self.assertEqual(self.policy_name, updated_policy['name']) - self.assertTrue(updated_policy['shared']) + self.assertTrue(updated_policy['is_shared']) self.assertTrue(updated_policy['is_default']) def test_list_qos_policies_filtered(self): diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py index 97f527fee..38dd95668 100644 --- a/openstack/tests/unit/cloud/test_caching.py +++ b/openstack/tests/unit/cloud/test_caching.py @@ -455,7 +455,8 @@ def test_list_flavors(self): self.assertEqual([], self.cloud.list_flavors()) fake_flavor_dicts = [ - _flavor.Flavor(connection=self.cloud, **f) + _flavor.Flavor(connection=self.cloud, + **f)._to_munch(original_names=False) for f in fakes.FAKE_FLAVOR_LIST ] diff --git a/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py b/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py index 7cd3b5001..8febafa3c 100644 --- a/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py @@ -16,6 +16,7 @@ import copy from openstack.cloud import exc +from openstack.network.v2 import qos_bandwidth_limit_rule from openstack.tests.unit import base @@ -67,6 +68,12 @@ class TestQosBandwidthLimitRule(base.TestCase): enabled_neutron_extensions = [qos_extension, qos_bw_limit_direction_extension] + def _compare_rules(self, exp, real): + self.assertDictEqual( + qos_bandwidth_limit_rule.QoSBandwidthLimitRule(**exp).to_dict( + computed=False), + real.to_dict(computed=False)) + def test_get_qos_bandwidth_limit_rule(self): self.register_uris([ dict(method='GET', @@ -75,12 +82,14 @@ def test_get_qos_bandwidth_limit_rule(self): json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_name]), + status_code=404), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies']), + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name]), json={'policies': [self.mock_policy]}), dict(method='GET', uri=self.get_mock_url( @@ -91,7 +100,7 @@ def test_get_qos_bandwidth_limit_rule(self): ]) r = self.cloud.get_qos_bandwidth_limit_rule(self.policy_name, self.rule_id) - self.assertDictEqual(self.mock_rule, r) + self._compare_rules(self.mock_rule, r) self.assert_calls() def test_get_qos_bandwidth_limit_rule_no_qos_policy_found(self): @@ -102,12 +111,14 @@ def test_get_qos_bandwidth_limit_rule_no_qos_policy_found(self): json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_name]), + status_code=404), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies']), + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name]), json={'policies': []}) ]) self.assertRaises( @@ -137,12 +148,14 @@ def test_create_qos_bandwidth_limit_rule(self): json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_name]), + status_code=404), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies']), + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name]), json={'policies': [self.mock_policy]}), dict(method='POST', uri=self.get_mock_url( @@ -153,7 +166,7 @@ def test_create_qos_bandwidth_limit_rule(self): ]) rule = self.cloud.create_qos_bandwidth_limit_rule( self.policy_name, max_kbps=self.rule_max_kbps) - self.assertDictEqual(self.mock_rule, rule) + self._compare_rules(self.mock_rule, rule) self.assert_calls() def test_create_qos_bandwidth_limit_rule_no_qos_extension(self): @@ -177,12 +190,14 @@ def test_create_qos_bandwidth_limit_rule_no_qos_direction_extension(self): json={'extensions': [self.qos_extension]}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': [self.qos_extension]}), + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_name]), + status_code=404), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies']), + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name]), json={'policies': [self.mock_policy]}), dict(method='GET', uri=self.get_mock_url( @@ -197,7 +212,7 @@ def test_create_qos_bandwidth_limit_rule_no_qos_direction_extension(self): ]) rule = self.cloud.create_qos_bandwidth_limit_rule( self.policy_name, max_kbps=self.rule_max_kbps, direction="ingress") - self.assertDictEqual(self.mock_rule, rule) + self._compare_rules(self.mock_rule, rule) self.assert_calls() def test_update_qos_bandwidth_limit_rule(self): @@ -207,29 +222,12 @@ def test_update_qos_bandwidth_limit_rule(self): dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies']), - json={'policies': [self.mock_policy]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), + json={'extensions': [self.qos_extension]}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies']), - json={'policies': [self.mock_policy]}), + append=['v2.0', 'qos', 'policies', self.policy_id]), + json=self.mock_policy), dict(method='GET', uri=self.get_mock_url( 'network', 'public', @@ -248,7 +246,7 @@ def test_update_qos_bandwidth_limit_rule(self): ]) rule = self.cloud.update_qos_bandwidth_limit_rule( self.policy_id, self.rule_id, max_kbps=self.rule_max_kbps + 100) - self.assertDictEqual(expected_rule, rule) + self._compare_rules(expected_rule, rule) self.assert_calls() def test_update_qos_bandwidth_limit_rule_no_qos_extension(self): @@ -272,38 +270,20 @@ def test_update_qos_bandwidth_limit_rule_no_qos_direction_extension(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': [self.qos_extension]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': [self.qos_extension]}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies']), - json={'policies': [self.mock_policy]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': [self.qos_extension]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': [self.qos_extension]}), + append=['v2.0', 'qos', 'policies', self.policy_id]), + json=self.mock_policy), dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': [self.qos_extension]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies']), - json={'policies': [self.mock_policy]}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'qos', 'policies', self.policy_id, - 'bandwidth_limit_rules', - self.rule_id]), + 'bandwidth_limit_rules', self.rule_id]), json={'bandwidth_limit_rule': self.mock_rule}), dict(method='PUT', uri=self.get_mock_url( @@ -321,7 +301,7 @@ def test_update_qos_bandwidth_limit_rule_no_qos_direction_extension(self): direction="ingress") # Even if there was attempt to change direction to 'ingress' it should # be not changed in returned rule - self.assertDictEqual(expected_rule, rule) + self._compare_rules(expected_rule, rule) self.assert_calls() def test_delete_qos_bandwidth_limit_rule(self): @@ -332,12 +312,14 @@ def test_delete_qos_bandwidth_limit_rule(self): json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_name]), + status_code=404), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies']), + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name]), json={'policies': [self.mock_policy]}), dict(method='DELETE', uri=self.get_mock_url( @@ -372,12 +354,14 @@ def test_delete_qos_bandwidth_limit_rule_not_found(self): json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_name]), + status_code=404), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies']), + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name]), json={'policies': [self.mock_policy]}), dict(method='DELETE', uri=self.get_mock_url( diff --git a/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py b/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py index 12de28652..629b4cf7c 100644 --- a/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py +++ b/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py @@ -16,6 +16,7 @@ import copy from openstack.cloud import exc +from openstack.network.v2 import qos_dscp_marking_rule from openstack.tests.unit import base @@ -54,6 +55,12 @@ class TestQosDscpMarkingRule(base.TestCase): enabled_neutron_extensions = [qos_extension] + def _compare_rules(self, exp, real): + self.assertDictEqual( + qos_dscp_marking_rule.QoSDSCPMarkingRule(**exp).to_dict( + computed=False), + real.to_dict(computed=False)) + def test_get_qos_dscp_marking_rule(self): self.register_uris([ dict(method='GET', @@ -62,12 +69,14 @@ def test_get_qos_dscp_marking_rule(self): json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_name]), + status_code=404), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies']), + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name]), json={'policies': [self.mock_policy]}), dict(method='GET', uri=self.get_mock_url( @@ -78,7 +87,7 @@ def test_get_qos_dscp_marking_rule(self): ]) r = self.cloud.get_qos_dscp_marking_rule(self.policy_name, self.rule_id) - self.assertDictEqual(self.mock_rule, r) + self._compare_rules(self.mock_rule, r) self.assert_calls() def test_get_qos_dscp_marking_rule_no_qos_policy_found(self): @@ -89,12 +98,14 @@ def test_get_qos_dscp_marking_rule_no_qos_policy_found(self): json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_name]), + status_code=404), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies']), + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name]), json={'policies': []}) ]) self.assertRaises( @@ -124,12 +135,14 @@ def test_create_qos_dscp_marking_rule(self): json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_name]), + status_code=404), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies']), + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name]), json={'policies': [self.mock_policy]}), dict(method='POST', uri=self.get_mock_url( @@ -140,7 +153,7 @@ def test_create_qos_dscp_marking_rule(self): ]) rule = self.cloud.create_qos_dscp_marking_rule( self.policy_name, dscp_mark=self.rule_dscp_mark) - self.assertDictEqual(self.mock_rule, rule) + self._compare_rules(self.mock_rule, rule) self.assert_calls() def test_create_qos_dscp_marking_rule_no_qos_extension(self): @@ -165,28 +178,11 @@ def test_update_qos_dscp_marking_rule(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies']), - json={'policies': [self.mock_policy]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies']), - json={'policies': [self.mock_policy]}), + append=['v2.0', 'qos', 'policies', self.policy_id]), + json=self.mock_policy), dict(method='GET', uri=self.get_mock_url( 'network', 'public', @@ -205,7 +201,7 @@ def test_update_qos_dscp_marking_rule(self): ]) rule = self.cloud.update_qos_dscp_marking_rule( self.policy_id, self.rule_id, dscp_mark=new_dscp_mark_value) - self.assertDictEqual(expected_rule, rule) + self._compare_rules(expected_rule, rule) self.assert_calls() def test_update_qos_dscp_marking_rule_no_qos_extension(self): @@ -229,12 +225,14 @@ def test_delete_qos_dscp_marking_rule(self): json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_name]), + status_code=404), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies']), + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name]), json={'policies': [self.mock_policy]}), dict(method='DELETE', uri=self.get_mock_url( @@ -270,12 +268,14 @@ def test_delete_qos_dscp_marking_rule_not_found(self): json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_name]), + status_code=404), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies']), + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name]), json={'policies': [self.mock_policy]}), dict(method='DELETE', uri=self.get_mock_url( diff --git a/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py b/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py index 5ccbb5c38..6ee89a8ca 100644 --- a/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py +++ b/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py @@ -16,6 +16,7 @@ import copy from openstack.cloud import exc +from openstack.network.v2 import qos_minimum_bandwidth_rule from openstack.tests.unit import base @@ -55,6 +56,12 @@ class TestQosMinimumBandwidthRule(base.TestCase): enabled_neutron_extensions = [qos_extension] + def _compare_rules(self, exp, real): + self.assertDictEqual( + qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule( + **exp).to_dict(computed=False), + real.to_dict(computed=False)) + def test_get_qos_minimum_bandwidth_rule(self): self.register_uris([ dict(method='GET', @@ -63,13 +70,16 @@ def test_get_qos_minimum_bandwidth_rule(self): json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_name]), + status_code=404), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies']), + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name]), json={'policies': [self.mock_policy]}), + dict(method='GET', uri=self.get_mock_url( 'network', 'public', @@ -79,7 +89,7 @@ def test_get_qos_minimum_bandwidth_rule(self): ]) r = self.cloud.get_qos_minimum_bandwidth_rule(self.policy_name, self.rule_id) - self.assertDictEqual(self.mock_rule, r) + self._compare_rules(self.mock_rule, r) self.assert_calls() def test_get_qos_minimum_bandwidth_rule_no_qos_policy_found(self): @@ -90,13 +100,15 @@ def test_get_qos_minimum_bandwidth_rule_no_qos_policy_found(self): json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_name]), + status_code=404), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies']), - json={'policies': []}) + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name]), + json={'policies': []}), ]) self.assertRaises( exc.OpenStackCloudResourceNotFound, @@ -125,12 +137,14 @@ def test_create_qos_minimum_bandwidth_rule(self): json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_name]), + status_code=404), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies']), + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name]), json={'policies': [self.mock_policy]}), dict(method='POST', uri=self.get_mock_url( @@ -141,7 +155,7 @@ def test_create_qos_minimum_bandwidth_rule(self): ]) rule = self.cloud.create_qos_minimum_bandwidth_rule( self.policy_name, min_kbps=self.rule_min_kbps) - self.assertDictEqual(self.mock_rule, rule) + self._compare_rules(self.mock_rule, rule) self.assert_calls() def test_create_qos_minimum_bandwidth_rule_no_qos_extension(self): @@ -161,23 +175,6 @@ def test_update_qos_minimum_bandwidth_rule(self): expected_rule = copy.copy(self.mock_rule) expected_rule['min_kbps'] = self.rule_min_kbps + 100 self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies']), - json={'policies': [self.mock_policy]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'extensions']), @@ -185,8 +182,8 @@ def test_update_qos_minimum_bandwidth_rule(self): dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies']), - json={'policies': [self.mock_policy]}), + append=['v2.0', 'qos', 'policies', self.policy_id]), + json=self.mock_policy), dict(method='GET', uri=self.get_mock_url( 'network', 'public', @@ -205,7 +202,7 @@ def test_update_qos_minimum_bandwidth_rule(self): ]) rule = self.cloud.update_qos_minimum_bandwidth_rule( self.policy_id, self.rule_id, min_kbps=self.rule_min_kbps + 100) - self.assertDictEqual(expected_rule, rule) + self._compare_rules(expected_rule, rule) self.assert_calls() def test_update_qos_minimum_bandwidth_rule_no_qos_extension(self): @@ -229,12 +226,14 @@ def test_delete_qos_minimum_bandwidth_rule(self): json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_name]), + status_code=404), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies']), + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name]), json={'policies': [self.mock_policy]}), dict(method='DELETE', uri=self.get_mock_url( @@ -269,12 +268,14 @@ def test_delete_qos_minimum_bandwidth_rule_not_found(self): json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), + 'network', 'public', + append=['v2.0', 'qos', 'policies', self.policy_name]), + status_code=404), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies']), + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name]), json={'policies': [self.mock_policy]}), dict(method='DELETE', uri=self.get_mock_url( diff --git a/openstack/tests/unit/cloud/test_qos_policy.py b/openstack/tests/unit/cloud/test_qos_policy.py index bedaf59f1..70da59daa 100644 --- a/openstack/tests/unit/cloud/test_qos_policy.py +++ b/openstack/tests/unit/cloud/test_qos_policy.py @@ -16,6 +16,7 @@ import copy from openstack.cloud import exc +from openstack.network.v2 import qos_policy as _policy from openstack.tests.unit import base @@ -33,7 +34,8 @@ class TestQosPolicy(base.TestCase): 'project_id': project_id, 'tenant_id': project_id, 'shared': False, - 'is_default': False + 'is_default': False, + 'tags': [], } qos_extension = { @@ -54,6 +56,11 @@ class TestQosPolicy(base.TestCase): enabled_neutron_extensions = [qos_extension, qos_default_extension] + def _compare_policies(self, exp, real): + self.assertDictEqual( + _policy.QoSPolicy(**exp).to_dict(computed=False), + real.to_dict(computed=False)) + def test_get_qos_policy(self): self.register_uris([ dict(method='GET', @@ -63,12 +70,19 @@ def test_get_qos_policy(self): dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies']), - json={'policies': [self.mock_policy]}) + append=['v2.0', 'qos', + 'policies', self.policy_name]), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name]), + json={'policies': [self.mock_policy]}), ]) r = self.cloud.get_qos_policy(self.policy_name) self.assertIsNotNone(r) - self.assertDictEqual(self.mock_policy, r) + self._compare_policies(self.mock_policy, r) self.assert_calls() def test_get_qos_policy_no_qos_extension(self): @@ -97,7 +111,7 @@ def test_create_qos_policy(self): ]) policy = self.cloud.create_qos_policy( name=self.policy_name, project_id=self.project_id) - self.assertDictEqual(self.mock_policy, policy) + self._compare_policies(self.mock_policy, policy) self.assert_calls() def test_create_qos_policy_no_qos_extension(self): @@ -134,7 +148,7 @@ def test_create_qos_policy_no_qos_default_extension(self): ]) policy = self.cloud.create_qos_policy( name=self.policy_name, project_id=self.project_id, default=True) - self.assertDictEqual(self.mock_policy, policy) + self._compare_policies(self.mock_policy, policy) self.assert_calls() def test_delete_qos_policy(self): @@ -145,12 +159,15 @@ def test_delete_qos_policy(self): json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), + 'network', 'public', + append=['v2.0', 'qos', 'policies', + self.policy_name]), + status_code=404), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies']), + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name]), json={'policies': [self.mock_policy]}), dict(method='DELETE', uri=self.get_mock_url( @@ -181,12 +198,14 @@ def test_delete_qos_policy_not_found(self): json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), + 'network', 'public', + append=['v2.0', 'qos', 'policies', 'goofy']), + status_code=404), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies']), + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=goofy']), json={'policies': []}) ]) self.assertFalse(self.cloud.delete_qos_policy('goofy')) @@ -202,36 +221,34 @@ def test_delete_qos_policy_multiple_found(self): json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), + 'network', 'public', + append=['v2.0', 'qos', 'policies', + self.policy_name]), + status_code=404), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies']), - json={'policies': [policy1, policy2]}) + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name]), + json={'policies': [policy1, policy2]}), ]) self.assertRaises(exc.OpenStackCloudException, self.cloud.delete_qos_policy, self.policy_name) self.assert_calls() - def test_delete_qos_policy_multiple_using_id(self): + def test_delete_qos_policy_using_id(self): policy1 = self.mock_policy - policy2 = dict(id='456', name=self.policy_name) self.register_uris([ dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies']), - json={'policies': [policy1, policy2]}), + append=['v2.0', 'qos', 'policies', policy1['id']]), + json=policy1), dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', @@ -249,15 +266,11 @@ def test_update_qos_policy(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies']), - json={'policies': [self.mock_policy]}), + append=['v2.0', 'qos', 'policies', self.policy_id]), + json=self.mock_policy), dict(method='PUT', uri=self.get_mock_url( 'network', 'public', @@ -268,7 +281,7 @@ def test_update_qos_policy(self): ]) policy = self.cloud.update_qos_policy( self.policy_id, name='goofy') - self.assertDictEqual(expected_policy, policy) + self._compare_policies(expected_policy, policy) self.assert_calls() def test_update_qos_policy_no_qos_extension(self): @@ -295,15 +308,11 @@ def test_update_qos_policy_no_qos_default_extension(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'extensions']), json={'extensions': [self.qos_extension]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': [self.qos_extension]}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'qos', 'policies']), - json={'policies': [self.mock_policy]}), + append=['v2.0', 'qos', 'policies', self.policy_id]), + json=self.mock_policy), dict(method='PUT', uri=self.get_mock_url( 'network', 'public', @@ -314,5 +323,5 @@ def test_update_qos_policy_no_qos_default_extension(self): ]) policy = self.cloud.update_qos_policy( self.policy_id, name='goofy', default=True) - self.assertDictEqual(expected_policy, policy) + self._compare_policies(expected_policy, policy) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_qos_rule_type.py b/openstack/tests/unit/cloud/test_qos_rule_type.py index e009b3a9d..91244f8d7 100644 --- a/openstack/tests/unit/cloud/test_qos_rule_type.py +++ b/openstack/tests/unit/cloud/test_qos_rule_type.py @@ -14,6 +14,7 @@ # limitations under the License. from openstack.cloud import exc +from openstack.network.v2 import qos_rule_type from openstack.tests.unit import base @@ -66,6 +67,11 @@ class TestQosRuleType(base.TestCase): 'type': rule_type_name } + def _compare_rule_types(self, exp, real): + self.assertDictEqual( + qos_rule_type.QoSRuleType(**exp).to_dict(computed=False), + real.to_dict(computed=False)) + def test_list_qos_rule_types(self): self.register_uris([ dict(method='GET', @@ -79,7 +85,8 @@ def test_list_qos_rule_types(self): json={'rule_types': self.mock_rule_types}) ]) rule_types = self.cloud.list_qos_rule_types() - self.assertEqual(self.mock_rule_types, rule_types) + for a, b in zip(self.mock_rule_types, rule_types): + self._compare_rule_types(a, b) self.assert_calls() def test_list_qos_rule_types_no_qos_extension(self): @@ -114,7 +121,8 @@ def test_get_qos_rule_type_details(self): self.rule_type_name]), json={'rule_type': self.mock_rule_type_details}) ]) - self.assertEqual( + + self._compare_rule_types( self.mock_rule_type_details, self.cloud.get_qos_rule_type_details(self.rule_type_name) ) diff --git a/openstack/tests/unit/cloud/test_shade.py b/openstack/tests/unit/cloud/test_shade.py index 9b8c369b6..fa47345ab 100644 --- a/openstack/tests/unit/cloud/test_shade.py +++ b/openstack/tests/unit/cloud/test_shade.py @@ -17,6 +17,7 @@ from openstack.cloud import exc from openstack import connection +from openstack import exceptions from openstack.tests import fakes from openstack.tests.unit import base from openstack import utils @@ -637,8 +638,7 @@ def test__neutron_extensions_fails(self): status_code=404) ]) with testtools.ExpectedException( - exc.OpenStackCloudURINotFound, - "Error fetching extension list for neutron" + exceptions.ResourceNotFound ): self.cloud._neutron_extensions() diff --git a/openstack/tests/unit/network/v2/test_qos_policy.py b/openstack/tests/unit/network/v2/test_qos_policy.py index eb3ba8a2d..7a786e83d 100644 --- a/openstack/tests/unit/network/v2/test_qos_policy.py +++ b/openstack/tests/unit/network/v2/test_qos_policy.py @@ -45,8 +45,8 @@ def test_make_it(self): self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['description'], sot.description) self.assertEqual(EXAMPLE['name'], sot.name) - self.assertTrue(sot.is_shared) self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertEqual(EXAMPLE['tenant_id'], sot.tenant_id) self.assertEqual(EXAMPLE['rules'], sot.rules) self.assertEqual(EXAMPLE['is_default'], sot.is_default) self.assertEqual(EXAMPLE['tags'], sot.tags) From 5f9522986cec8eb87acff3fc8d49679a4d7b4c37 Mon Sep 17 00:00:00 2001 From: Goutham Pacha Ravi Date: Mon, 1 Feb 2021 17:37:51 -0800 Subject: [PATCH 2779/3836] Add shared file systems support Introduce the shared file systems storage service proxy, and add a basic service resource to list availability zones. [1] https://tree.taiga.io/project/ashrod98-openstacksdk-manila-support/us/11?kanban-status=2360120 Depends-On: Ia2e62d3a11a08adeb6d488b7c9b365f7ff2be3c8 Change-Id: I20f1f713583c53a2df7fd01af11234960c9c8291 Signed-off-by: Goutham Pacha Ravi --- doc/source/user/guides/shared_file_system.rst | 22 +++++++++ doc/source/user/index.rst | 3 ++ .../user/proxies/shared_file_system.rst | 24 ++++++++++ .../resources/shared_file_system/index.rst | 7 +++ .../v2/availability_zone.rst | 13 ++++++ examples/shared_file_system/__init__.py | 0 .../shared_file_system/availability_zones.py | 24 ++++++++++ openstack/_services_mixin.py | 3 +- openstack/cloud/_shared_file_system.py | 23 ++++++++++ openstack/connection.py | 5 +- openstack/shared_file_system/__init__.py | 0 .../shared_file_system_service.py | 21 +++++++++ openstack/shared_file_system/v2/__init__.py | 0 openstack/shared_file_system/v2/_proxy.py | 26 +++++++++++ .../v2/availability_zone.py | 36 +++++++++++++++ .../functional/shared_file_system/__init__.py | 0 .../functional/shared_file_system/base.py | 23 ++++++++++ .../test_availability_zone.py | 26 +++++++++++ openstack/tests/unit/base.py | 16 +++++++ .../unit/cloud/test_shared_file_system.py | 46 +++++++++++++++++++ .../unit/fixtures/shared-file-system.json | 28 +++++++++++ .../tests/unit/shared_file_system/__init__.py | 0 .../unit/shared_file_system/v2/__init__.py | 0 .../v2/test_availability_zone.py | 38 +++++++++++++++ ...-shared-file-systems-83a3767429fd5e8c.yaml | 8 ++++ 25 files changed, 390 insertions(+), 2 deletions(-) create mode 100644 doc/source/user/guides/shared_file_system.rst create mode 100644 doc/source/user/proxies/shared_file_system.rst create mode 100644 doc/source/user/resources/shared_file_system/index.rst create mode 100644 doc/source/user/resources/shared_file_system/v2/availability_zone.rst create mode 100644 examples/shared_file_system/__init__.py create mode 100644 examples/shared_file_system/availability_zones.py create mode 100644 openstack/cloud/_shared_file_system.py create mode 100644 openstack/shared_file_system/__init__.py create mode 100644 openstack/shared_file_system/shared_file_system_service.py create mode 100644 openstack/shared_file_system/v2/__init__.py create mode 100644 openstack/shared_file_system/v2/_proxy.py create mode 100644 openstack/shared_file_system/v2/availability_zone.py create mode 100644 openstack/tests/functional/shared_file_system/__init__.py create mode 100644 openstack/tests/functional/shared_file_system/base.py create mode 100644 openstack/tests/functional/shared_file_system/test_availability_zone.py create mode 100644 openstack/tests/unit/cloud/test_shared_file_system.py create mode 100644 openstack/tests/unit/fixtures/shared-file-system.json create mode 100644 openstack/tests/unit/shared_file_system/__init__.py create mode 100644 openstack/tests/unit/shared_file_system/v2/__init__.py create mode 100644 openstack/tests/unit/shared_file_system/v2/test_availability_zone.py create mode 100644 releasenotes/notes/add-shared-file-systems-83a3767429fd5e8c.yaml diff --git a/doc/source/user/guides/shared_file_system.rst b/doc/source/user/guides/shared_file_system.rst new file mode 100644 index 000000000..40ffc3f2a --- /dev/null +++ b/doc/source/user/guides/shared_file_system.rst @@ -0,0 +1,22 @@ +Using OpenStack Shared File Systems +=================================== + +Before working with the Shared File System service, you'll need to create a +connection to your OpenStack cloud by following the :doc:`connect` user +guide. This will provide you with the ``conn`` variable used in the examples +below. + +.. contents:: Table of Contents + :local: + + +List Availability Zones +----------------------- + +A Shared File System service **availability zone** is a failure domain for +your shared file systems. You may create a shared file system (referred +to simply as **shares**) in a given availability zone, and create replicas +of the share in other availability zones. + +.. literalinclude:: ../examples/shared_file_system/availability_zones.py + :pyobject: list_availability_zones diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index 9559f41de..a08f00416 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -48,6 +48,7 @@ approach, this is where you'll want to begin. Network Object Store Orchestration + Shared File System API Documentation ----------------- @@ -113,6 +114,7 @@ control which services can be used. Network Object Store Orchestration + Shared File System Workflow Resource Interface @@ -145,6 +147,7 @@ The following services have exposed *Resource* classes. Network Orchestration Object Store + Shared File System Workflow Low-Level Classes diff --git a/doc/source/user/proxies/shared_file_system.rst b/doc/source/user/proxies/shared_file_system.rst new file mode 100644 index 000000000..3e8ae7388 --- /dev/null +++ b/doc/source/user/proxies/shared_file_system.rst @@ -0,0 +1,24 @@ +Shared File System API +====================== + +.. automodule:: openstack.shared_file_system.v2._proxy + +The Shared File System Class +---------------------------- + +The high-level interface for accessing the shared file systems service API is +available through the ``shared_file_system`` member of a :class:`~openstack +.connection.Connection` object. The ``shared_file_system`` member will only +be added if the service is detected. ``share`` is an alias of the +``shared_file_system`` member. + + +Shared File System Availability Zones +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Interact with Availability Zones supported by the Shared File Systems +service. + +.. autoclass:: openstack.shared_file_system.v2._proxy.Proxy + :noindex: + :members: availability_zones diff --git a/doc/source/user/resources/shared_file_system/index.rst b/doc/source/user/resources/shared_file_system/index.rst new file mode 100644 index 000000000..1be3611bd --- /dev/null +++ b/doc/source/user/resources/shared_file_system/index.rst @@ -0,0 +1,7 @@ +Shared File System service resources +==================================== + +.. toctree:: + :maxdepth: 1 + + v2/availability_zone diff --git a/doc/source/user/resources/shared_file_system/v2/availability_zone.rst b/doc/source/user/resources/shared_file_system/v2/availability_zone.rst new file mode 100644 index 000000000..9a518908c --- /dev/null +++ b/doc/source/user/resources/shared_file_system/v2/availability_zone.rst @@ -0,0 +1,13 @@ +openstack.shared_file_system.v2.availability_zone +================================================= + +.. automodule:: openstack.shared_file_system.v2.availability_zone + +The AvailabilityZone Class +-------------------------- + +The ``AvailabilityZone`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.shared_file_system.v2.availability_zone.AvailabilityZone + :members: diff --git a/examples/shared_file_system/__init__.py b/examples/shared_file_system/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/shared_file_system/availability_zones.py b/examples/shared_file_system/availability_zones.py new file mode 100644 index 000000000..73c0e3fb5 --- /dev/null +++ b/examples/shared_file_system/availability_zones.py @@ -0,0 +1,24 @@ +# 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. + +""" +List resources from the Shared File System service. + +For a full guide see +https://docs.openstack.org/openstacksdk/latest/user/guides/shared_file_system.html +""" + + +def list_availability_zones(conn): + print("List Shared File System Availability Zones:") + for az in conn.share.availability_zones(): + print(az) diff --git a/openstack/_services_mixin.py b/openstack/_services_mixin.py index 75c1d19e7..48f45f282 100644 --- a/openstack/_services_mixin.py +++ b/openstack/_services_mixin.py @@ -17,6 +17,7 @@ from openstack.network import network_service from openstack.object_store import object_store_service from openstack.orchestration import orchestration_service +from openstack.shared_file_system import shared_file_system_service from openstack.workflow import workflow_service @@ -68,7 +69,7 @@ class ServicesMixin: operator_policy = service_description.ServiceDescription(service_type='operator-policy') policy = operator_policy - shared_file_system = service_description.ServiceDescription(service_type='shared-file-system') + shared_file_system = shared_file_system_service.SharedFilesystemService(service_type='shared-file-system') share = shared_file_system data_protection_orchestration = service_description.ServiceDescription(service_type='data-protection-orchestration') diff --git a/openstack/cloud/_shared_file_system.py b/openstack/cloud/_shared_file_system.py new file mode 100644 index 000000000..9302a8a68 --- /dev/null +++ b/openstack/cloud/_shared_file_system.py @@ -0,0 +1,23 @@ +# 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.cloud import _normalize + + +class SharedFileSystemCloudMixin(_normalize.Normalizer): + + def list_share_availability_zones(self): + """List all availability zones for the Shared File Systems service. + + :returns: A list of Shared File Systems Availability Zones. + """ + return list(self.share.availability_zones()) diff --git a/openstack/connection.py b/openstack/connection.py index 7c36b9dbd..8b8994941 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -208,6 +208,7 @@ from openstack.cloud import _object_store from openstack.cloud import _orchestration from openstack.cloud import _security_group +from openstack.cloud import _shared_file_system from openstack import config as _config from openstack.config import cloud_region from openstack import exceptions @@ -271,7 +272,8 @@ class Connection( _network_common.NetworkCommonCloudMixin, _object_store.ObjectStoreCloudMixin, _orchestration.OrchestrationCloudMixin, - _security_group.SecurityGroupCloudMixin + _security_group.SecurityGroupCloudMixin, + _shared_file_system.SharedFileSystemCloudMixin, ): def __init__(self, cloud=None, config=None, session=None, @@ -415,6 +417,7 @@ def __init__(self, cloud=None, config=None, session=None, _object_store.ObjectStoreCloudMixin.__init__(self) _orchestration.OrchestrationCloudMixin.__init__(self) _security_group.SecurityGroupCloudMixin.__init__(self) + _shared_file_system.SharedFileSystemCloudMixin.__init__(self) # Allow vendors to provide hooks. They will normally only receive a # connection object and a responsible to register additional services diff --git a/openstack/shared_file_system/__init__.py b/openstack/shared_file_system/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/shared_file_system/shared_file_system_service.py b/openstack/shared_file_system/shared_file_system_service.py new file mode 100644 index 000000000..bf6c7541a --- /dev/null +++ b/openstack/shared_file_system/shared_file_system_service.py @@ -0,0 +1,21 @@ +# 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 service_description +from openstack.shared_file_system.v2 import _proxy + + +class SharedFilesystemService(service_description.ServiceDescription): + """The shared file systems service.""" + supported_versions = { + '2': _proxy.Proxy, + } diff --git a/openstack/shared_file_system/v2/__init__.py b/openstack/shared_file_system/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py new file mode 100644 index 000000000..0f6a688c4 --- /dev/null +++ b/openstack/shared_file_system/v2/_proxy.py @@ -0,0 +1,26 @@ +# 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 proxy +from openstack.shared_file_system.v2 import availability_zone + + +class Proxy(proxy.Proxy): + + def availability_zones(self): + """Retrieve shared file system availability zones + + :returns: A generator of availability zone resources + :rtype: :class:`~openstack.shared_file_system.v2. + \availability_zone.AvailabilityZone` + """ + return self._list(availability_zone.AvailabilityZone) diff --git a/openstack/shared_file_system/v2/availability_zone.py b/openstack/shared_file_system/v2/availability_zone.py new file mode 100644 index 000000000..d05e188b3 --- /dev/null +++ b/openstack/shared_file_system/v2/availability_zone.py @@ -0,0 +1,36 @@ +# 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 resource + + +class AvailabilityZone(resource.Resource): + resource_key = "availability_zone" + resources_key = "availability_zones" + base_path = "/availability-zones" + + # capabilities + allow_create = False + allow_fetch = False + allow_commit = False + allow_delete = False + allow_list = True + + #: Properties + #: The ID of the availability zone + id = resource.Body("id", type=str) + #: The name of the availability zone. + name = resource.Body("name", type=str) + #: Date and time the availability zone was created at. + created_at = resource.Body("created_at", type=str) + #: Date and time the availability zone was last updated at. + updated_at = resource.Body("updated_at", type=str) diff --git a/openstack/tests/functional/shared_file_system/__init__.py b/openstack/tests/functional/shared_file_system/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/functional/shared_file_system/base.py b/openstack/tests/functional/shared_file_system/base.py new file mode 100644 index 000000000..3714dcebe --- /dev/null +++ b/openstack/tests/functional/shared_file_system/base.py @@ -0,0 +1,23 @@ +# 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.tests.functional import base + + +class BaseSharedFileSystemTest(base.BaseFunctionalTest): + + min_microversion = None + + def setUp(self): + super(BaseSharedFileSystemTest, self).setUp() + self.require_service('shared-file-system', + min_microversion=self.min_microversion) diff --git a/openstack/tests/functional/shared_file_system/test_availability_zone.py b/openstack/tests/functional/shared_file_system/test_availability_zone.py new file mode 100644 index 000000000..4cb39014d --- /dev/null +++ b/openstack/tests/functional/shared_file_system/test_availability_zone.py @@ -0,0 +1,26 @@ +# 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.tests.functional.shared_file_system import base + + +class AvailabilityZoneTest(base.BaseSharedFileSystemTest): + + min_microversion = '2.7' + + def test_availability_zones(self): + azs = self.conn.shared_file_system.availability_zones() + self.assertGreater(len(list(azs)), 0) + for az in azs: + for attribute in ('id', 'name', 'created_at', 'updated_at'): + self.assertTrue(hasattr(az, attribute)) + self.assertIsInstance(getattr(az, attribute), 'str') diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 9eb597ea5..0847577ad 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -574,6 +574,13 @@ def get_cyborg_discovery_mock_dict(self): return dict(method='GET', uri="https://accelerator.example.com/", text=open(discovery_fixture, 'r').read()) + def get_manila_discovery_mock_dict(self): + discovery_fixture = os.path.join( + self.fixtures_directory, "shared-file-system.json") + return dict(method='GET', + uri="https://shared-file-system.example.com/", + text=open(discovery_fixture, 'r').read()) + def use_glance( self, image_version_json='image-version.json', image_discovery_url='https://image.example.com/'): @@ -630,6 +637,15 @@ def use_cyborg(self): self.__do_register_uris([ self.get_cyborg_discovery_mock_dict()]) + def use_manila(self): + # NOTE(gouthamr): This method is only meant to be used in "setUp" + # where the ordering of the url being registered is tightly controlled + # if the functionality of .use_manila is meant to be used during an + # actual test case, use .get_manila_discovery_mock and apply to the + # right location in the mock_uris when calling .register_uris + self.__do_register_uris([ + self.get_manila_discovery_mock_dict()]) + def register_uris(self, uri_mock_list=None): """Mock a list of URIs and responses via requests mock. diff --git a/openstack/tests/unit/cloud/test_shared_file_system.py b/openstack/tests/unit/cloud/test_shared_file_system.py new file mode 100644 index 000000000..2c87f4678 --- /dev/null +++ b/openstack/tests/unit/cloud/test_shared_file_system.py @@ -0,0 +1,46 @@ +# 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.tests.unit import base +import uuid + +IDENTIFIER = str(uuid.uuid4()) +MANILA_AZ_DICT = { + "id": IDENTIFIER, + "name": "manila-zone-0", + "created_at": "2021-01-21T20:13:55.000000", + "updated_at": None, +} + + +class TestSharedFileSystem(base.TestCase): + + def setUp(self): + super(TestSharedFileSystem, self).setUp() + self.use_manila() + + def test_list_availability_zones(self): + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'shared-file-system', + 'public', + append=['v2', 'availability-zones']), + json={'availability_zones': [MANILA_AZ_DICT]}), + ]) + az_list = self.cloud.list_share_availability_zones() + self.assertEqual(len(az_list), 1) + self.assertEqual(MANILA_AZ_DICT['id'], az_list[0].id) + self.assertEqual(MANILA_AZ_DICT['name'], az_list[0].name) + self.assertEqual(MANILA_AZ_DICT['created_at'], az_list[0].created_at) + self.assertEqual(MANILA_AZ_DICT['updated_at'], az_list[0].updated_at) + self.assert_calls() diff --git a/openstack/tests/unit/fixtures/shared-file-system.json b/openstack/tests/unit/fixtures/shared-file-system.json new file mode 100644 index 000000000..3d22f6e5c --- /dev/null +++ b/openstack/tests/unit/fixtures/shared-file-system.json @@ -0,0 +1,28 @@ +{ + "versions": [ + { + "id": "v2.0", + "status": "CURRENT", + "version": "2.58", + "min_version": "2.0", + "updated": "2015-08-27T11:33:21Z", + "links": [ + { + "rel": "describedby", + "type": "text/html", + "href": "http://docs.openstack.org/" + }, + { + "rel": "self", + "href": "https://shared-file-system.example.com/v2/" + } + ], + "media-types": [ + { + "base": "application/json", + "type": "application/vnd.openstack.share+json;version=1" + } + ] + } + ] +} \ No newline at end of file diff --git a/openstack/tests/unit/shared_file_system/__init__.py b/openstack/tests/unit/shared_file_system/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/unit/shared_file_system/v2/__init__.py b/openstack/tests/unit/shared_file_system/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/unit/shared_file_system/v2/test_availability_zone.py b/openstack/tests/unit/shared_file_system/v2/test_availability_zone.py new file mode 100644 index 000000000..d37a7328f --- /dev/null +++ b/openstack/tests/unit/shared_file_system/v2/test_availability_zone.py @@ -0,0 +1,38 @@ +# 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.shared_file_system.v2 import availability_zone as az +from openstack.tests.unit import base + +IDENTIFIER = '08a87d37-5ca2-4308-86c5-cba06d8d796c' +EXAMPLE = { + "id": IDENTIFIER, + "name": "nova", + "created_at": "2021-01-21T20:13:55.000000", + "updated_at": None, +} + + +class TestAvailabilityZone(base.TestCase): + + def test_basic(self): + az_resource = az.AvailabilityZone() + self.assertEqual('availability_zones', az_resource.resources_key) + self.assertEqual('/availability-zones', az_resource.base_path) + self.assertTrue(az_resource.allow_list) + + def test_make_availability_zone(self): + az_resource = az.AvailabilityZone(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], az_resource.id) + self.assertEqual(EXAMPLE['name'], az_resource.name) + self.assertEqual(EXAMPLE['created_at'], az_resource.created_at) + self.assertEqual(EXAMPLE['updated_at'], az_resource.updated_at) diff --git a/releasenotes/notes/add-shared-file-systems-83a3767429fd5e8c.yaml b/releasenotes/notes/add-shared-file-systems-83a3767429fd5e8c.yaml new file mode 100644 index 000000000..9f770db32 --- /dev/null +++ b/releasenotes/notes/add-shared-file-systems-83a3767429fd5e8c.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Support for the OpenStack Shared File System API (manila) has been + introduced. + - | + Added support to list Shared File System Service API Versions + and Availability Zones. From c81c1f61a8d504d83478d64f959bd6171655a6a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Piliszek?= Date: Wed, 24 Feb 2021 11:42:13 +0100 Subject: [PATCH 2780/3836] Fix a trivial error in one of the error messages Some text got inserted in the error message making it sound odd. Safe to backport. TrivialFix Change-Id: I14f0f1ae5421fd0c53980c79d35f5ea2012ea320 --- openstack/config/cloud_region.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index cf5fbaa46..346d38f4c 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -783,7 +783,6 @@ def get_session_client( raise exceptions.ConfigException( "A default microversion for service {service_type} of" " {default_microversion} was requested, but the cloud" - " only supports a maximum of" " only supports a minimum of {min_microversion} and" " a maximum of {max_microversion}. The default" " microversion was set because a microversion" From e30fd860de6b36bddc27f7215bb2e3cad8eb298d Mon Sep 17 00:00:00 2001 From: anuradha1904 Date: Fri, 11 Dec 2020 00:17:28 +0530 Subject: [PATCH 2781/3836] Improve Ironic API on OpenStack SDK 1. List Conductors 2. Show Conductors Details Story: 20083813 Task: 40956 Change-Id: I48bc5f70cf42e93ee46783cbefdeafed80be56d8 --- doc/source/user/resources/baremetal/index.rst | 1 + .../user/resources/baremetal/v1/conductor.rst | 13 ++++ openstack/baremetal/v1/_proxy.py | 29 +++++++++ openstack/baremetal/v1/conductor.py | 42 +++++++++++++ .../baremetal/test_baremetal_conductor.py | 29 +++++++++ .../tests/unit/baremetal/v1/test_conductor.py | 61 +++++++++++++++++++ ...c-conductors-support-3bf27e8b2f0299ba.yaml | 4 ++ 7 files changed, 179 insertions(+) create mode 100644 doc/source/user/resources/baremetal/v1/conductor.rst create mode 100644 openstack/baremetal/v1/conductor.py create mode 100644 openstack/tests/functional/baremetal/test_baremetal_conductor.py create mode 100644 openstack/tests/unit/baremetal/v1/test_conductor.py create mode 100644 releasenotes/notes/ironic-conductors-support-3bf27e8b2f0299ba.yaml diff --git a/doc/source/user/resources/baremetal/index.rst b/doc/source/user/resources/baremetal/index.rst index 1b284f304..1fabe14b2 100644 --- a/doc/source/user/resources/baremetal/index.rst +++ b/doc/source/user/resources/baremetal/index.rst @@ -13,3 +13,4 @@ Baremetal Resources v1/volume_connector v1/volume_target v1/deploy_templates + v1/conductor diff --git a/doc/source/user/resources/baremetal/v1/conductor.rst b/doc/source/user/resources/baremetal/v1/conductor.rst new file mode 100644 index 000000000..9e1bd1176 --- /dev/null +++ b/doc/source/user/resources/baremetal/v1/conductor.rst @@ -0,0 +1,13 @@ +openstack.baremetal.v1.conductor +================================ + +.. automodule:: openstack.baremetal.v1.conductor + +The Conductor Class +------------------- + +The ``Conductor`` class inherits +from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.baremetal.v1.conductor.Conductor + :members: diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index b395dbc6d..0fb8a4978 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -20,6 +20,8 @@ from openstack.baremetal.v1 import volume_connector as _volumeconnector from openstack.baremetal.v1 import volume_target as _volumetarget from openstack.baremetal.v1 import deploy_templates as _deploytemplates +from openstack.baremetal.v1 import conductor as _conductor + from openstack import exceptions from openstack import proxy from openstack import utils @@ -1415,3 +1417,30 @@ def patch_deploy_template(self, deploy_template, patch): """ return self._get_resource(_deploytemplates.DeployTemplate, deploy_template).patch(self, patch) + + def conductors(self, details=False, **query): + """Retrieve a generator of conductors. + + :param bool details: A boolean indicating whether the detailed + information for every conductor should be returned. + + :returns: A generator of conductor instances. + """ + + if details: + query['details'] = True + return _conductor.Conductor.list(self, **query) + + def get_conductor(self, conductor, fields=None): + """Get a specific conductor. + + :param conductor: The value can be the name of a conductor or a + :class:`~openstack.baremetal.v1.conductor.Conductor` instance. + + :returns: One :class:`~openstack.baremetal.v1.conductor.Conductor` + + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + conductor matching the name could be found. + """ + return self._get_with_fields(_conductor.Conductor, + conductor, fields=fields) diff --git a/openstack/baremetal/v1/conductor.py b/openstack/baremetal/v1/conductor.py new file mode 100644 index 000000000..05ecf043e --- /dev/null +++ b/openstack/baremetal/v1/conductor.py @@ -0,0 +1,42 @@ +# 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.baremetal.v1 import _common +from openstack import resource + + +class Conductor(_common.ListMixin, resource.Resource): + + resources_key = 'conductors' + base_path = '/conductors' + + # capabilities + allow_create = False + allow_fetch = True + allow_commit = False + allow_delete = False + allow_list = True + allow_patch = False + + _query_mapping = resource.QueryParameters( + 'detail', + fields={'type': _common.fields_type}, + ) + + _max_microversion = '1.49' + created_at = resource.Body('created_at') + updated_at = resource.Body('updated_at') + hostname = resource.Body('hostname') + conductor_group = resource.Body('conductor_group') + alive = resource.Body('alive', type=bool) + links = resource.Body('links', type=list) + drivers = resource.Body('drivers', type=list) diff --git a/openstack/tests/functional/baremetal/test_baremetal_conductor.py b/openstack/tests/functional/baremetal/test_baremetal_conductor.py new file mode 100644 index 000000000..310304dee --- /dev/null +++ b/openstack/tests/functional/baremetal/test_baremetal_conductor.py @@ -0,0 +1,29 @@ +# 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.tests.functional.baremetal import base + + +class TestBareMetalConductor(base.BaseBaremetalTest): + + min_microversion = '1.49' + + def test_list_get_conductor(self): + node = self.create_node(name='node-name') + conductors = self.conn.baremetal.conductors() + hostname_list = [conductor.hostname for conductor in conductors] + self.assertIn(node.conductor, hostname_list) + conductor1 = self.conn.baremetal.get_conductor(node.conductor) + self.assertIsNotNone(conductor1.conductor_group) + self.assertIsNotNone(conductor1.links) + self.assertTrue(conductor1.alive) diff --git a/openstack/tests/unit/baremetal/v1/test_conductor.py b/openstack/tests/unit/baremetal/v1/test_conductor.py new file mode 100644 index 000000000..55900f4ed --- /dev/null +++ b/openstack/tests/unit/baremetal/v1/test_conductor.py @@ -0,0 +1,61 @@ +# 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.tests.unit import base + +from openstack.baremetal.v1 import conductor + +FAKE = { + "links": [ + { + "href": "http://127.0.0.1:6385/v1/conductors/compute2.localdomain", + "rel": "self" + }, + { + "href": "http://127.0.0.1:6385/conductors/compute2.localdomain", + "rel": "bookmark" + } + ], + "created_at": "2018-12-05T07:03:19+00:00", + "hostname": "compute2.localdomain", + "conductor_group": "", + "updated_at": "2018-12-05T07:03:21+00:00", + "alive": True, + "drivers": [ + "ipmi" + ] +} + + +class TestContainer(base.TestCase): + + def test_basic(self): + sot = conductor.Conductor() + self.assertIsNone(sot.resource_key) + self.assertEqual('conductors', sot.resources_key) + self.assertEqual('/conductors', sot.base_path) + self.assertFalse(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertFalse(sot.allow_patch) + + def test_instantiate(self): + sot = conductor.Conductor(**FAKE) + self.assertEqual(FAKE['created_at'], sot.created_at) + self.assertEqual(FAKE['updated_at'], sot.updated_at) + self.assertEqual(FAKE['hostname'], sot.hostname) + self.assertEqual(FAKE['conductor_group'], sot.conductor_group) + self.assertEqual(FAKE['alive'], sot.alive) + self.assertEqual(FAKE['links'], sot.links) + self.assertEqual(FAKE['drivers'], sot.drivers) diff --git a/releasenotes/notes/ironic-conductors-support-3bf27e8b2f0299ba.yaml b/releasenotes/notes/ironic-conductors-support-3bf27e8b2f0299ba.yaml new file mode 100644 index 000000000..e2aea13a2 --- /dev/null +++ b/releasenotes/notes/ironic-conductors-support-3bf27e8b2f0299ba.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Support for Ironic Conductor API. From 477445c35a322207475a019d2043194859099e87 Mon Sep 17 00:00:00 2001 From: Thiago Brito Date: Wed, 17 Feb 2021 17:11:43 -0300 Subject: [PATCH 2782/3836] Add set_readonly_volume to BlockStorageCloudMixin We are missing some "/action" operations for volumes so this patch adds the ability to set volumes to readonly through the sdk. Signed-off-by: Thiago Brito Change-Id: I335b8f11b94810f45f0c45b94fc4984f6bf5e122 --- openstack/block_storage/v3/_proxy.py | 28 +++++++++++++++++++ openstack/block_storage/v3/volume.py | 5 ++++ .../tests/unit/block_storage/v3/test_proxy.py | 12 ++++++++ .../unit/block_storage/v3/test_volume.py | 20 +++++++++++++ 4 files changed, 65 insertions(+) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index c926bd13e..c45cf235d 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -384,6 +384,34 @@ def extend_volume(self, volume, size): volume = self._get_resource(_volume.Volume, volume) volume.extend(self, size) + def set_volume_readonly(self, volume, readonly=True): + """Set a volume's read-only flag. + + :param name_or_id: Name, unique ID of the volume or a volume dict. + :param bool readonly: Whether the volume should be a read-only volume + or not + + :raises: OpenStackCloudTimeout if wait time exceeded. + :raises: OpenStackCloudException on operation error. + """ + volume = self._get_resource(_volume.Volume, volume) + volume.set_readonly(self, readonly) + + def retype_volume(self, volume, new_type, migration_policy="never"): + """Retype the volume. + + :param name_or_id: Name, unique ID of the volume or a volume dict. + :param new_type: The new volume type that volume is changed with. + :param migration_policy: Specify if the volume should be migrated when + it is re-typed. Possible values are on-demand + or never. Default: never. + + :raises: OpenStackCloudTimeout if wait time exceeded. + :raises: OpenStackCloudException on operation error. + """ + volume = self._get_resource(_volume.Volume, volume) + volume.retype(self, new_type, migration_policy) + def backend_pools(self): """Returns a generator of cinder Back-end storage pools diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index 849351194..477c03005 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -114,5 +114,10 @@ def extend(self, session, size): body = {'os-extend': {'new_size': size}} self._action(session, body) + def set_readonly(self, session, readonly): + """Set volume readonly flag""" + body = {'os-update_readonly_flag': {'readonly': readonly}} + self._action(session, body) + VolumeDetail = Volume diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 7fd4908ce..ba3fea439 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -156,6 +156,18 @@ def test_volume_extend(self): method_args=["value", "new-size"], expected_args=["new-size"]) + def test_volume_set_readonly_no_argument(self): + self._verify("openstack.block_storage.v3.volume.Volume.set_readonly", + self.proxy.set_volume_readonly, + method_args=["value"], + expected_args=[True]) + + def test_volume_set_readonly_false(self): + self._verify("openstack.block_storage.v3.volume.Volume.set_readonly", + self.proxy.set_volume_readonly, + method_args=["value", False], + expected_args=[False]) + def test_backend_pools(self): self.verify_list(self.proxy.backend_pools, stats.Pools) diff --git a/openstack/tests/unit/block_storage/v3/test_volume.py b/openstack/tests/unit/block_storage/v3/test_volume.py index 27348a3f6..ac1a238f9 100644 --- a/openstack/tests/unit/block_storage/v3/test_volume.py +++ b/openstack/tests/unit/block_storage/v3/test_volume.py @@ -127,3 +127,23 @@ def test_extend(self): body = {"os-extend": {"new_size": "20"}} headers = {'Accept': ''} self.sess.post.assert_called_with(url, json=body, headers=headers) + + def test_set_volume_readonly(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.set_readonly(self.sess, True)) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-update_readonly_flag': {'readonly': True}} + headers = {'Accept': ''} + self.sess.post.assert_called_with(url, json=body, headers=headers) + + def test_set_volume_readonly_false(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.set_readonly(self.sess, False)) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-update_readonly_flag': {'readonly': False}} + headers = {'Accept': ''} + self.sess.post.assert_called_with(url, json=body, headers=headers) From 736f3aa16c7ee95eb5a42f28062305c83cfd05e1 Mon Sep 17 00:00:00 2001 From: sue Date: Wed, 24 Feb 2021 14:10:18 +0800 Subject: [PATCH 2783/3836] add masakari enabled to segment Add 'enabled' to Masakari segment in microversion 1.2. It returns "enabled" flag in segments API. Change-Id: I445774a8af60a75c6b936d25fbab858233c632e9 --- openstack/instance_ha/v1/segment.py | 7 ++++++- openstack/tests/unit/instance_ha/v1/test_segment.py | 5 ++++- .../add-masakari-enabled-to-segment-0e83da869d2ab03f.yaml | 4 ++++ 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/add-masakari-enabled-to-segment-0e83da869d2ab03f.yaml diff --git a/openstack/instance_ha/v1/segment.py b/openstack/instance_ha/v1/segment.py index c13b01e29..e4841202e 100644 --- a/openstack/instance_ha/v1/segment.py +++ b/openstack/instance_ha/v1/segment.py @@ -32,6 +32,9 @@ class Segment(resource.Resource): allow_commit = True allow_delete = True + # add enabled flag to segment in 1.2 + _max_microversion = '1.2' + #: A ID of representing this segment. id = resource.Body("id") #: A Uuid of representing this segment. @@ -48,7 +51,9 @@ class Segment(resource.Resource): recovery_method = resource.Body("recovery_method") #: The service type of this segment. service_type = resource.Body("service_type") + #: The enabled flag of this segment. + is_enabled = resource.Body("enabled", type=bool) _query_mapping = resource.QueryParameters( "sort_key", "sort_dir", recovery_method="recovery_method", - service_type="service_type") + service_type="service_type", is_enabled="enabled") diff --git a/openstack/tests/unit/instance_ha/v1/test_segment.py b/openstack/tests/unit/instance_ha/v1/test_segment.py index e38fefae4..af43349b6 100644 --- a/openstack/tests/unit/instance_ha/v1/test_segment.py +++ b/openstack/tests/unit/instance_ha/v1/test_segment.py @@ -25,7 +25,8 @@ "name": "my_segment", "description": "something", "recovery_method": "auto", - "service_type": "COMPUTE_HOST" + "service_type": "COMPUTE_HOST", + "enabled": True, } @@ -46,6 +47,7 @@ def test_basic(self): "marker": "marker", "recovery_method": "recovery_method", "service_type": "service_type", + "is_enabled": "enabled", "sort_dir": "sort_dir", "sort_key": "sort_key"}, sot._query_mapping._mapping) @@ -60,3 +62,4 @@ def test_create(self): self.assertEqual(SEGMENT["description"], sot.description) self.assertEqual(SEGMENT["recovery_method"], sot.recovery_method) self.assertEqual(SEGMENT["service_type"], sot.service_type) + self.assertEqual(SEGMENT["enabled"], sot.is_enabled) diff --git a/releasenotes/notes/add-masakari-enabled-to-segment-0e83da869d2ab03f.yaml b/releasenotes/notes/add-masakari-enabled-to-segment-0e83da869d2ab03f.yaml new file mode 100644 index 000000000..97d01f59b --- /dev/null +++ b/releasenotes/notes/add-masakari-enabled-to-segment-0e83da869d2ab03f.yaml @@ -0,0 +1,4 @@ +--- +features: + - Add support for the ``enabled`` field of the ``Segment`` resource for + the instance HA service (Masakari). From 9ca72c296b64bbe678dfa9c65039f40f23eac290 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 12 Jun 2020 10:01:20 -0500 Subject: [PATCH 2784/3836] Cache auth token in keyring keystoneauth has support for caching auth tokens. Plumb it through so that we use it in openstacksdk if keyring is installed. Make it an opt-in for now via config option 'cache.auth' which will at the very least help us test it. Change-Id: Ia71d128afd628ed264bcc0d8d61c421861df459f --- doc/source/user/config/configuration.rst | 11 ++++ openstack/config/cloud_region.py | 44 +++++++++++++++- openstack/config/loader.py | 22 ++++++-- openstack/connection.py | 5 ++ openstack/tests/unit/config/base.py | 2 +- openstack/tests/unit/config/test_config.py | 50 +++++++++++++++++++ ...ache-auth-in-keyring-773dd5f682cd1610.yaml | 5 ++ 7 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/cache-auth-in-keyring-773dd5f682cd1610.yaml diff --git a/doc/source/user/config/configuration.rst b/doc/source/user/config/configuration.rst index 36fbf3bb2..221af4705 100644 --- a/doc/source/user/config/configuration.rst +++ b/doc/source/user/config/configuration.rst @@ -250,6 +250,17 @@ are connecting to OpenStack can share a cache should you desire. region_name: ca-ymq-1 dns_api_version: 1 +`openstacksdk` can also cache authorization state (token) in the keyring. +That allow the consequent connections to the same cloud to skip fetching new +token. When the token gets expired or gets invalid `openstacksdk` will +establish new connection. + + +.. code-block:: yaml + + cache: + auth: true + IPv6 ---- diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 346d38f4c..79b5003cf 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -17,6 +17,11 @@ import warnings import urllib +try: + import keyring +except ImportError: + keyring = None + from keystoneauth1 import discover import keystoneauth1.exceptions.catalog from keystoneauth1.loading import adapter as ks_load_adap @@ -239,7 +244,8 @@ def __init__(self, name=None, region_name=None, config=None, cache_arguments=None, password_callback=None, statsd_host=None, statsd_port=None, statsd_prefix=None, influxdb_config=None, - collector_registry=None): + collector_registry=None, + cache_auth=False): self._name = name self.config = _util.normalize_keys(config) # NOTE(efried): For backward compatibility: a) continue to accept the @@ -251,6 +257,8 @@ def __init__(self, name=None, region_name=None, config=None, self.log = _log.setup_logging('openstack.config') self._force_ipv4 = force_ipv4 self._auth = auth_plugin + self._cache_auth = cache_auth + self.load_auth_from_cache() self._openstack_config = openstack_config self._keystone_session = session self._session_constructor = session_constructor or ks_session.Session @@ -557,6 +565,40 @@ def get_auth(self): """Return a keystoneauth plugin from the auth credentials.""" return self._auth + def skip_auth_cache(self): + return not keyring or not self._auth or not self._cache_auth + + def load_auth_from_cache(self): + if self.skip_auth_cache(): + return + + cache_id = self._auth.get_cache_id() + + # skip if the plugin does not support caching + if not cache_id: + return + + try: + state = keyring.get_password('openstacksdk', cache_id) + except RuntimeError: # the fail backend raises this + self.log.debug('Failed to fetch auth from keyring') + return + + self.log.debug('Reusing authentication from keyring') + self._auth.set_auth_state(state) + + def set_auth_cache(self): + if self.skip_auth_cache(): + return + + cache_id = self._auth.get_cache_id() + state = self._auth.get_auth_state() + + try: + keyring.set_password('openstacksdk', cache_id, state) + except RuntimeError: # the fail backend raises this + self.log.debug('Failed to set auth into keyring') + def insert_user_agent(self): """Set sdk information into the user agent of the Session. diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 79bc14741..3267a9967 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -160,9 +160,19 @@ def __init__(self, config_files=None, vendor_files=None, self._load_envvars = load_envvars if load_yaml_config: - self._config_files = config_files or CONFIG_FILES - self._secure_files = secure_files or SECURE_FILES - self._vendor_files = vendor_files or VENDOR_FILES + # "if config_files" is not sufficient to process empty list + if config_files is not None: + self._config_files = config_files + else: + self._config_files = CONFIG_FILES + if secure_files is not None: + self._secure_files = secure_files + else: + self._secure_files = SECURE_FILES + if vendor_files is not None: + self._vendor_files = vendor_files + else: + self._vendor_files = VENDOR_FILES else: self._config_files = [] self._secure_files = [] @@ -259,6 +269,7 @@ def __init__(self, config_files=None, vendor_files=None, clouds=dict(defaults=dict(self.defaults))) self.default_cloud = 'defaults' + self._cache_auth = False self._cache_expiration_time = 0 self._cache_path = CACHE_PATH self._cache_class = 'dogpile.cache.null' @@ -268,6 +279,9 @@ def __init__(self, config_files=None, vendor_files=None, if 'cache' in self.cloud_config: cache_settings = _util.normalize_keys(self.cloud_config['cache']) + self._cache_auth = get_boolean( + cache_settings.get('auth', self._cache_auth)) + # expiration_time used to be 'max_age' but the dogpile setting # is expiration_time. Support max_age for backwards compat. self._cache_expiration_time = cache_settings.get( @@ -1146,6 +1160,7 @@ def get_one( session_constructor=self._session_constructor, app_name=self._app_name, app_version=self._app_version, + cache_auth=self._cache_auth, cache_expiration_time=self._cache_expiration_time, cache_expirations=self._cache_expirations, cache_path=self._cache_path, @@ -1251,6 +1266,7 @@ def get_one_cloud_osc( force_ipv4=force_ipv4, auth_plugin=auth_plugin, openstack_config=self, + cache_auth=self._cache_auth, cache_expiration_time=self._cache_expiration_time, cache_expirations=self._cache_expirations, cache_path=self._cache_path, diff --git a/openstack/connection.py b/openstack/connection.py index 8b8994941..43cce4787 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -455,6 +455,10 @@ def __init__(self, cloud=None, config=None, session=None, self.config._influxdb_config['additional_metric_tags'] = \ self.config.config['additional_metric_tags'] + def __del__(self): + # try to force release of resources and save authorization + self.close() + @property def session(self): if not self._session: @@ -531,6 +535,7 @@ def close(self): """Release any resources held open.""" if self.__pool_executor: self.__pool_executor.shutdown() + self.config.set_auth_cache() def set_global_request_id(self, global_request_id): self._global_request_id = global_request_id diff --git a/openstack/tests/unit/config/base.py b/openstack/tests/unit/config/base.py index 471cb5a6b..81102cea0 100644 --- a/openstack/tests/unit/config/base.py +++ b/openstack/tests/unit/config/base.py @@ -194,7 +194,7 @@ def _write_yaml(obj): # Assume NestedTempfile so we don't have to cleanup - with tempfile.NamedTemporaryFile(delete=False) as obj_yaml: + with tempfile.NamedTemporaryFile(delete=False, suffix='.yaml') as obj_yaml: obj_yaml.write(yaml.safe_dump(obj).encode('utf-8')) return obj_yaml.name diff --git a/openstack/tests/unit/config/test_config.py b/openstack/tests/unit/config/test_config.py index e0fb827cc..f697c03da 100644 --- a/openstack/tests/unit/config/test_config.py +++ b/openstack/tests/unit/config/test_config.py @@ -15,6 +15,7 @@ import argparse import copy import os +from unittest import mock import fixtures import testtools @@ -91,6 +92,7 @@ def test_get_one_default_cloud_from_file(self): } }) c = config.OpenStackConfig(config_files=[single_conf], + secure_files=[], vendor_files=[self.vendor_yaml]) cc = c.get_one() self.assertEqual(cc.name, 'single') @@ -180,6 +182,7 @@ def test_get_one_unscoped_identity(self): } }) c = config.OpenStackConfig(config_files=[single_conf], + secure_files=[], vendor_files=[self.vendor_yaml]) cc = c.get_one() self.assertEqual('http://example.com/v2', cc.get_endpoint('identity')) @@ -463,6 +466,53 @@ def test_get_region_invalid_keys(self): exceptions.ConfigException, c._get_region, cloud='_test_cloud', region_name='region1') + @mock.patch('openstack.config.cloud_region.keyring') + @mock.patch( + 'keystoneauth1.identity.base.BaseIdentityPlugin.set_auth_state') + def test_load_auth_cache_not_found(self, ks_mock, kr_mock): + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], secure_files=[]) + c._cache_auth = True + + kr_mock.get_password = mock.Mock(side_effect=[RuntimeError]) + + region = c.get_one('_test-cloud_') + kr_mock.get_password.assert_called_with( + 'openstacksdk', region._auth.get_cache_id()) + ks_mock.assert_not_called() + + @mock.patch('openstack.config.cloud_region.keyring') + @mock.patch( + 'keystoneauth1.identity.base.BaseIdentityPlugin.set_auth_state') + def test_load_auth_cache_found(self, ks_mock, kr_mock): + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], secure_files=[]) + c._cache_auth = True + fake_auth = {'a': 'b'} + + kr_mock.get_password = mock.Mock(return_value=fake_auth) + + region = c.get_one('_test-cloud_') + kr_mock.get_password.assert_called_with( + 'openstacksdk', region._auth.get_cache_id()) + ks_mock.assert_called_with(fake_auth) + + @mock.patch('openstack.config.cloud_region.keyring') + def test_set_auth_cache(self, kr_mock): + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], secure_files=[]) + c._cache_auth = True + + kr_mock.get_password = mock.Mock(side_effect=[RuntimeError]) + kr_mock.set_password = mock.Mock() + + region = c.get_one('_test-cloud_') + + region.set_auth_cache() + kr_mock.set_password.assert_called_with( + 'openstacksdk', region._auth.get_cache_id(), + region._auth.get_auth_state()) + class TestExcludedFormattedConfigValue(base.TestCase): # verify https://storyboard.openstack.org/#!/story/1635696 diff --git a/releasenotes/notes/cache-auth-in-keyring-773dd5f682cd1610.yaml b/releasenotes/notes/cache-auth-in-keyring-773dd5f682cd1610.yaml new file mode 100644 index 000000000..8e35c048a --- /dev/null +++ b/releasenotes/notes/cache-auth-in-keyring-773dd5f682cd1610.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added support for optionally caching auth information int the local + keyring. Requires the installation of the python ``keyring`` package. From cfb5dd8959f2328a0c3127e61f4efd8fa709410a Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 4 Mar 2021 18:14:18 +0100 Subject: [PATCH 2785/3836] Prepare separate block_storage v3 documentation Clone existing block_storage v2 documentation into v3 to allow futher extending. Change-Id: Ia114286e37e78f773c850a917d7b79713e5cd4df --- doc/source/user/index.rst | 3 +- ...block_storage.rst => block_storage_v2.rst} | 0 doc/source/user/proxies/block_storage_v3.rst | 48 +++++++++++++++++++ .../user/resources/block_storage/index.rst | 5 ++ .../resources/block_storage/v3/backup.rst | 12 +++++ .../resources/block_storage/v3/snapshot.rst | 21 ++++++++ .../user/resources/block_storage/v3/type.rst | 13 +++++ .../resources/block_storage/v3/volume.rst | 12 +++++ 8 files changed, 113 insertions(+), 1 deletion(-) rename doc/source/user/proxies/{block_storage.rst => block_storage_v2.rst} (100%) create mode 100644 doc/source/user/proxies/block_storage_v3.rst create mode 100644 doc/source/user/resources/block_storage/v3/backup.rst create mode 100644 doc/source/user/resources/block_storage/v3/snapshot.rst create mode 100644 doc/source/user/resources/block_storage/v3/type.rst create mode 100644 doc/source/user/resources/block_storage/v3/volume.rst diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index a08f00416..b13a63bf4 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -99,7 +99,8 @@ control which services can be used. Accelerator Baremetal Baremetal Introspection - Block Storage + Block Storage v2 + Block Storage v3 Clustering Compute Database diff --git a/doc/source/user/proxies/block_storage.rst b/doc/source/user/proxies/block_storage_v2.rst similarity index 100% rename from doc/source/user/proxies/block_storage.rst rename to doc/source/user/proxies/block_storage_v2.rst diff --git a/doc/source/user/proxies/block_storage_v3.rst b/doc/source/user/proxies/block_storage_v3.rst new file mode 100644 index 000000000..bfbbed2fb --- /dev/null +++ b/doc/source/user/proxies/block_storage_v3.rst @@ -0,0 +1,48 @@ +Block Storage API +================= + +For details on how to use block_storage, see :doc:`/user/guides/block_storage` + +.. automodule:: openstack.block_storage.v3._proxy + +The BlockStorage Class +---------------------- + +The block_storage high-level interface is available through the +``block_storage`` member of a :class:`~openstack.connection.Connection` object. +The ``block_storage`` member will only be added if the service is detected. + +Volume Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v3._proxy.Proxy + :noindex: + :members: create_volume, delete_volume, get_volume, volumes + +Backup Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v3._proxy.Proxy + :noindex: + :members: create_backup, delete_backup, get_backup, backups, restore_backup + +Type Operations +^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v3._proxy.Proxy + :noindex: + :members: create_type, delete_type, get_type, types + +Snapshot Operations +^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v3._proxy.Proxy + :noindex: + :members: create_snapshot, delete_snapshot, get_snapshot, snapshots + +Stats Operations +^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v3._proxy.Proxy + :noindex: + :members: backend_pools diff --git a/doc/source/user/resources/block_storage/index.rst b/doc/source/user/resources/block_storage/index.rst index 923162185..fdcf654f1 100644 --- a/doc/source/user/resources/block_storage/index.rst +++ b/doc/source/user/resources/block_storage/index.rst @@ -8,3 +8,8 @@ Block Storage Resources v2/snapshot v2/type v2/volume + + v3/backup + v3/snapshot + v3/type + v3/volume diff --git a/doc/source/user/resources/block_storage/v3/backup.rst b/doc/source/user/resources/block_storage/v3/backup.rst new file mode 100644 index 000000000..2382ca978 --- /dev/null +++ b/doc/source/user/resources/block_storage/v3/backup.rst @@ -0,0 +1,12 @@ +openstack.block_storage.v3.backup +================================= + +.. automodule:: openstack.block_storage.v3.backup + +The Backup Class +---------------- + +The ``Backup`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v3.backup.Backup + :members: diff --git a/doc/source/user/resources/block_storage/v3/snapshot.rst b/doc/source/user/resources/block_storage/v3/snapshot.rst new file mode 100644 index 000000000..2185f58ba --- /dev/null +++ b/doc/source/user/resources/block_storage/v3/snapshot.rst @@ -0,0 +1,21 @@ +openstack.block_storage.v3.snapshot +=================================== + +.. automodule:: openstack.block_storage.v3.snapshot + +The Snapshot Class +------------------ + +The ``Snapshot`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v3.snapshot.Snapshot + :members: + +The SnapshotDetail Class +------------------------ + +The ``SnapshotDetail`` class inherits from +:class:`~openstack.block_storage.v3.snapshot.Snapshot`. + +.. autoclass:: openstack.block_storage.v3.snapshot.SnapshotDetail + :members: diff --git a/doc/source/user/resources/block_storage/v3/type.rst b/doc/source/user/resources/block_storage/v3/type.rst new file mode 100644 index 000000000..8cb7650c2 --- /dev/null +++ b/doc/source/user/resources/block_storage/v3/type.rst @@ -0,0 +1,13 @@ +openstack.block_storage.v3.type +=============================== + +.. automodule:: openstack.block_storage.v3.type + +The Type Class +-------------- + +The ``Type`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v3.type.Type + :members: + diff --git a/doc/source/user/resources/block_storage/v3/volume.rst b/doc/source/user/resources/block_storage/v3/volume.rst new file mode 100644 index 000000000..edb734f2a --- /dev/null +++ b/doc/source/user/resources/block_storage/v3/volume.rst @@ -0,0 +1,12 @@ +openstack.block_storage.v3.volume +================================= + +.. automodule:: openstack.block_storage.v3.volume + +The Volume Class +---------------- + +The ``Volume`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v3.volume.Volume + :members: From a714e096f31c56db6afd1db4f5f0f9511ba35cba Mon Sep 17 00:00:00 2001 From: Riccardo Pittau Date: Mon, 8 Mar 2021 12:32:39 +0100 Subject: [PATCH 2786/3836] Apply pep8 import order style Update version of tox to 3.9.0 to support inline comments in tox.ini Import pep8 test requirements directly in tox.ini and do not import all the test-requirements Update version of hacking Fix import orders in various modules Leave filter for imports in tests/ for the time being Change-Id: Ia625036d1f50ae97880ef70335804228320a9c6d --- openstack/accelerator/accelerator_service.py | 2 +- openstack/accelerator/v2/_proxy.py | 5 +++-- openstack/accelerator/v2/accelerator_request.py | 2 +- openstack/baremetal/baremetal_service.py | 2 +- openstack/baremetal/v1/_proxy.py | 5 ++--- .../baremetal_introspection_service.py | 2 +- openstack/cloud/_baremetal.py | 5 +++-- openstack/cloud/_block_storage.py | 2 +- openstack/cloud/_clustering.py | 2 +- openstack/cloud/_coe.py | 2 +- openstack/cloud/_compute.py | 7 ++++--- openstack/cloud/_dns.py | 6 +++--- openstack/cloud/_floating_ip.py | 4 ++-- openstack/cloud/_identity.py | 2 +- openstack/cloud/_image.py | 2 +- openstack/cloud/_network.py | 4 ++-- openstack/cloud/_network_common.py | 2 +- openstack/cloud/_normalize.py | 1 + openstack/cloud/_object_store.py | 2 +- openstack/cloud/_orchestration.py | 4 ++-- openstack/cloud/_security_group.py | 2 +- openstack/cloud/_utils.py | 9 +++++---- openstack/cloud/cmd/inventory.py | 1 + openstack/cloud/inventory.py | 2 +- openstack/cloud/meta.py | 6 +++--- openstack/cloud/openstackcloud.py | 11 +++++------ openstack/clustering/v1/_async_resource.py | 3 +-- openstack/clustering/v1/cluster.py | 3 +-- openstack/clustering/v1/node.py | 3 +-- openstack/compute/compute_service.py | 2 +- openstack/compute/v2/_proxy.py | 10 +++++----- openstack/compute/v2/server.py | 2 +- openstack/compute/v2/server_remote_console.py | 1 - openstack/config/cloud_region.py | 6 ++---- openstack/connection.py | 7 +++---- openstack/dns/v2/_proxy.py | 7 ++++--- openstack/dns/v2/floating_ip.py | 2 +- openstack/dns/v2/recordset.py | 2 +- openstack/dns/v2/zone.py | 4 ++-- openstack/dns/v2/zone_export.py | 4 ++-- openstack/dns/v2/zone_import.py | 4 ++-- openstack/dns/v2/zone_transfer.py | 2 +- openstack/image/image_signer.py | 5 ++--- openstack/image/v1/image.py | 3 ++- openstack/image/v2/_proxy.py | 2 +- openstack/key_manager/v1/_format.py | 4 ++-- openstack/network/v2/firewall_policy.py | 2 +- openstack/object_store/v1/_proxy.py | 8 ++++---- openstack/orchestration/util/template_format.py | 1 + openstack/orchestration/util/template_utils.py | 2 +- openstack/orchestration/v1/_proxy.py | 4 ++-- openstack/proxy.py | 4 ++-- openstack/resource.py | 2 +- openstack/tests/unit/cloud/test_stack.py | 2 +- test-requirements.txt | 4 +--- tox.ini | 17 +++++++++++++---- 56 files changed, 111 insertions(+), 105 deletions(-) diff --git a/openstack/accelerator/accelerator_service.py b/openstack/accelerator/accelerator_service.py index 9d7f1784a..f8210e23e 100644 --- a/openstack/accelerator/accelerator_service.py +++ b/openstack/accelerator/accelerator_service.py @@ -10,8 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import service_description from openstack.accelerator.v2 import _proxy as _proxy_v2 +from openstack import service_description class AcceleratorService(service_description.ServiceDescription): diff --git a/openstack/accelerator/v2/_proxy.py b/openstack/accelerator/v2/_proxy.py index 480c873b5..5f54b631b 100644 --- a/openstack/accelerator/v2/_proxy.py +++ b/openstack/accelerator/v2/_proxy.py @@ -9,11 +9,12 @@ # 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 proxy + +from openstack.accelerator.v2 import accelerator_request as _arq from openstack.accelerator.v2 import deployable as _deployable from openstack.accelerator.v2 import device as _device from openstack.accelerator.v2 import device_profile as _device_profile -from openstack.accelerator.v2 import accelerator_request as _arq +from openstack import proxy class Proxy(proxy.Proxy): diff --git a/openstack/accelerator/v2/accelerator_request.py b/openstack/accelerator/v2/accelerator_request.py index 1453544be..1be505883 100644 --- a/openstack/accelerator/v2/accelerator_request.py +++ b/openstack/accelerator/v2/accelerator_request.py @@ -10,8 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import resource from openstack import exceptions +from openstack import resource class AcceleratorRequest(resource.Resource): diff --git a/openstack/baremetal/baremetal_service.py b/openstack/baremetal/baremetal_service.py index 0b0cb818a..5bbecf6bd 100644 --- a/openstack/baremetal/baremetal_service.py +++ b/openstack/baremetal/baremetal_service.py @@ -10,8 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import service_description from openstack.baremetal.v1 import _proxy +from openstack import service_description class BaremetalService(service_description.ServiceDescription): diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 0fb8a4978..6ac6728e3 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -13,15 +13,14 @@ from openstack.baremetal.v1 import _common from openstack.baremetal.v1 import allocation as _allocation from openstack.baremetal.v1 import chassis as _chassis +from openstack.baremetal.v1 import conductor as _conductor +from openstack.baremetal.v1 import deploy_templates as _deploytemplates from openstack.baremetal.v1 import driver as _driver from openstack.baremetal.v1 import node as _node from openstack.baremetal.v1 import port as _port from openstack.baremetal.v1 import port_group as _portgroup from openstack.baremetal.v1 import volume_connector as _volumeconnector from openstack.baremetal.v1 import volume_target as _volumetarget -from openstack.baremetal.v1 import deploy_templates as _deploytemplates -from openstack.baremetal.v1 import conductor as _conductor - from openstack import exceptions from openstack import proxy from openstack import utils diff --git a/openstack/baremetal_introspection/baremetal_introspection_service.py b/openstack/baremetal_introspection/baremetal_introspection_service.py index aec3adbab..acbdaa894 100644 --- a/openstack/baremetal_introspection/baremetal_introspection_service.py +++ b/openstack/baremetal_introspection/baremetal_introspection_service.py @@ -10,8 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import service_description from openstack.baremetal_introspection.v1 import _proxy +from openstack import service_description class BaremetalIntrospectionService(service_description.ServiceDescription): diff --git a/openstack/cloud/_baremetal.py b/openstack/cloud/_baremetal.py index 8e8986cce..c3c413571 100644 --- a/openstack/cloud/_baremetal.py +++ b/openstack/cloud/_baremetal.py @@ -13,13 +13,14 @@ # import types so that we can reference ListType in sphinx param declarations. # We can't just use list, because sphinx gets confused by # openstack.resource.Resource.list and openstack.resource2.Resource.list -import jsonpatch import types # noqa import warnings -from openstack.cloud import exc +import jsonpatch + from openstack.cloud import _normalize from openstack.cloud import _utils +from openstack.cloud import exc from openstack import utils diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index 870f0f059..dc6fd140f 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -16,9 +16,9 @@ import types # noqa import warnings -from openstack.cloud import exc from openstack.cloud import _normalize from openstack.cloud import _utils +from openstack.cloud import exc from openstack import proxy from openstack import utils diff --git a/openstack/cloud/_clustering.py b/openstack/cloud/_clustering.py index ca45befde..b7aa60b01 100644 --- a/openstack/cloud/_clustering.py +++ b/openstack/cloud/_clustering.py @@ -15,9 +15,9 @@ # openstack.resource.Resource.list and openstack.resource2.Resource.list import types # noqa -from openstack.cloud import exc from openstack.cloud import _normalize from openstack.cloud import _utils +from openstack.cloud import exc from openstack import utils diff --git a/openstack/cloud/_coe.py b/openstack/cloud/_coe.py index 08d6a52a9..3778341c2 100644 --- a/openstack/cloud/_coe.py +++ b/openstack/cloud/_coe.py @@ -15,9 +15,9 @@ # openstack.resource.Resource.list and openstack.resource2.Resource.list import types # noqa -from openstack.cloud import exc from openstack.cloud import _normalize from openstack.cloud import _utils +from openstack.cloud import exc class CoeCloudMixin(_normalize.Normalizer): diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 15b93658b..8bee8f90b 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -16,16 +16,17 @@ import base64 import datetime import functools -import iso8601 import operator import threading import time import types # noqa -from openstack.cloud import exc -from openstack.cloud import meta +import iso8601 + from openstack.cloud import _normalize from openstack.cloud import _utils +from openstack.cloud import exc +from openstack.cloud import meta from openstack import exceptions from openstack import proxy from openstack import utils diff --git a/openstack/cloud/_dns.py b/openstack/cloud/_dns.py index 8c4ce4427..bb534215a 100644 --- a/openstack/cloud/_dns.py +++ b/openstack/cloud/_dns.py @@ -15,11 +15,11 @@ # openstack.resource.Resource.list and openstack.resource2.Resource.list import types # noqa -from openstack import exceptions -from openstack import resource -from openstack.cloud import exc from openstack.cloud import _normalize from openstack.cloud import _utils +from openstack.cloud import exc +from openstack import exceptions +from openstack import resource class DnsCloudMixin(_normalize.Normalizer): diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index bca6cdd5e..5519ace67 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -19,10 +19,10 @@ import time import types # noqa -from openstack.cloud import exc -from openstack.cloud import meta from openstack.cloud import _normalize from openstack.cloud import _utils +from openstack.cloud import exc +from openstack.cloud import meta from openstack import exceptions from openstack import proxy from openstack import utils diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index 4a9a9bfcd..3864ff3c5 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -17,9 +17,9 @@ import munch -from openstack.cloud import exc from openstack.cloud import _normalize from openstack.cloud import _utils +from openstack.cloud import exc from openstack import utils diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py index 53265d9e1..a74ec64dd 100644 --- a/openstack/cloud/_image.py +++ b/openstack/cloud/_image.py @@ -15,9 +15,9 @@ # openstack.resource.Resource.list and openstack.resource2.Resource.list import types # noqa -from openstack.cloud import exc from openstack.cloud import _normalize from openstack.cloud import _utils +from openstack.cloud import exc from openstack import utils diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index 16e78fb41..0200e3d83 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -13,13 +13,13 @@ # import types so that we can reference ListType in sphinx param declarations. # We can't just use list, because sphinx gets confused by # openstack.resource.Resource.list and openstack.resource2.Resource.list -import time import threading +import time import types # noqa -from openstack.cloud import exc from openstack.cloud import _normalize from openstack.cloud import _utils +from openstack.cloud import exc from openstack import exceptions from openstack import proxy diff --git a/openstack/cloud/_network_common.py b/openstack/cloud/_network_common.py index 2c2bdd724..e83309403 100644 --- a/openstack/cloud/_network_common.py +++ b/openstack/cloud/_network_common.py @@ -16,8 +16,8 @@ import threading import types # noqa -from openstack.cloud import exc from openstack.cloud import _normalize +from openstack.cloud import exc class NetworkCommonCloudMixin(_normalize.Normalizer): diff --git a/openstack/cloud/_normalize.py b/openstack/cloud/_normalize.py index 4cbcd9f85..ea00c9f6d 100644 --- a/openstack/cloud/_normalize.py +++ b/openstack/cloud/_normalize.py @@ -17,6 +17,7 @@ # the sdk resource objects. import datetime + import munch from openstack import resource diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index e291d0310..829b11db5 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -23,9 +23,9 @@ import keystoneauth1.exceptions -from openstack.cloud import exc from openstack.cloud import _normalize from openstack.cloud import _utils +from openstack.cloud import exc from openstack import exceptions from openstack import proxy from openstack import utils diff --git a/openstack/cloud/_orchestration.py b/openstack/cloud/_orchestration.py index 5bcc5ae11..eb22825e2 100644 --- a/openstack/cloud/_orchestration.py +++ b/openstack/cloud/_orchestration.py @@ -15,10 +15,10 @@ # openstack.resource.Resource.list and openstack.resource2.Resource.list import types # noqa -from openstack.cloud import exc -from openstack.orchestration.util import event_utils from openstack.cloud import _normalize from openstack.cloud import _utils +from openstack.cloud import exc +from openstack.orchestration.util import event_utils def _no_pending_stacks(stacks): diff --git a/openstack/cloud/_security_group.py b/openstack/cloud/_security_group.py index c5ccb369e..496e588f3 100644 --- a/openstack/cloud/_security_group.py +++ b/openstack/cloud/_security_group.py @@ -16,9 +16,9 @@ # import jsonpatch import types # noqa -from openstack.cloud import exc from openstack.cloud import _normalize from openstack.cloud import _utils +from openstack.cloud import exc from openstack import exceptions from openstack import proxy diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index e19870986..9c7488087 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -16,20 +16,21 @@ import fnmatch import functools import inspect -import jmespath -import munch -import netifaces import re -import sre_constants import time import uuid from decorator import decorator +import jmespath +import munch +import netifaces +import sre_constants from openstack import _log from openstack.cloud import exc from openstack.cloud import meta + _decorated_methods = [] diff --git a/openstack/cloud/cmd/inventory.py b/openstack/cloud/cmd/inventory.py index 0f1186e5e..450669b1f 100755 --- a/openstack/cloud/cmd/inventory.py +++ b/openstack/cloud/cmd/inventory.py @@ -16,6 +16,7 @@ import argparse import json import sys + import yaml import openstack.cloud diff --git a/openstack/cloud/inventory.py b/openstack/cloud/inventory.py index 32a26821b..f8e9040ba 100644 --- a/openstack/cloud/inventory.py +++ b/openstack/cloud/inventory.py @@ -14,10 +14,10 @@ import functools +from openstack.cloud import _utils from openstack.config import loader from openstack import connection from openstack import exceptions -from openstack.cloud import _utils __all__ = ['OpenStackInventory'] diff --git a/openstack/cloud/meta.py b/openstack/cloud/meta.py index cdd372436..a70d9e2d7 100644 --- a/openstack/cloud/meta.py +++ b/openstack/cloud/meta.py @@ -12,14 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. - -import munch import ipaddress import socket +import munch + from openstack import _log -from openstack import utils from openstack.cloud import exc +from openstack import utils NON_CALLABLES = (str, bool, dict, int, float, list, type(None)) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index bb520a7d1..96e7bca44 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -19,29 +19,28 @@ import warnings import dogpile.cache +import keystoneauth1.exceptions +import keystoneauth1.session import munch import requests.models import requestsexceptions -import keystoneauth1.exceptions -import keystoneauth1.session - from openstack import _log -from openstack.cloud import exc from openstack.cloud import _floating_ip from openstack.cloud import _object_store -from openstack.cloud import meta from openstack.cloud import _utils +from openstack.cloud import exc +from openstack.cloud import meta import openstack.config from openstack.config import cloud_region as cloud_region_mod from openstack import proxy from openstack import utils + DEFAULT_SERVER_AGE = 5 DEFAULT_PORT_AGE = 5 DEFAULT_FLOAT_AGE = 5 _CONFIG_DOC_URL = _floating_ip._CONFIG_DOC_URL - DEFAULT_OBJECT_SEGMENT_SIZE = _object_store.DEFAULT_OBJECT_SEGMENT_SIZE # This halves the current default for Swift DEFAULT_MAX_FILE_SIZE = _object_store.DEFAULT_MAX_FILE_SIZE diff --git a/openstack/clustering/v1/_async_resource.py b/openstack/clustering/v1/_async_resource.py index cd01ac3fe..819433902 100644 --- a/openstack/clustering/v1/_async_resource.py +++ b/openstack/clustering/v1/_async_resource.py @@ -10,11 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.clustering.v1 import action as _action from openstack import exceptions from openstack import resource -from openstack.clustering.v1 import action as _action - class AsyncResource(resource.Resource): diff --git a/openstack/clustering/v1/cluster.py b/openstack/clustering/v1/cluster.py index e545080c4..2e8663232 100644 --- a/openstack/clustering/v1/cluster.py +++ b/openstack/clustering/v1/cluster.py @@ -10,11 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.clustering.v1 import _async_resource from openstack import resource from openstack import utils -from openstack.clustering.v1 import _async_resource - class Cluster(_async_resource.AsyncResource): resource_key = 'cluster' diff --git a/openstack/clustering/v1/node.py b/openstack/clustering/v1/node.py index 7dae01522..c914a7210 100644 --- a/openstack/clustering/v1/node.py +++ b/openstack/clustering/v1/node.py @@ -10,11 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.clustering.v1 import _async_resource from openstack import resource from openstack import utils -from openstack.clustering.v1 import _async_resource - class Node(_async_resource.AsyncResource): resource_key = 'node' diff --git a/openstack/compute/compute_service.py b/openstack/compute/compute_service.py index 2204f04d4..03397cbe8 100644 --- a/openstack/compute/compute_service.py +++ b/openstack/compute/compute_service.py @@ -10,8 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import service_description from openstack.compute.v2 import _proxy +from openstack import service_description class ComputeService(service_description.ServiceDescription): diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 5cc6fc187..16160502a 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -9,6 +9,7 @@ # 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 warnings from openstack.compute.v2 import aggregate as _aggregate @@ -23,13 +24,12 @@ from openstack.compute.v2 import server_diagnostics as _server_diagnostics from openstack.compute.v2 import server_group as _server_group from openstack.compute.v2 import server_interface as _server_interface -from openstack.compute.v2 import ( - server_remote_console as _server_remote_console) from openstack.compute.v2 import server_ip +from openstack.compute.v2 import server_remote_console as _src from openstack.compute.v2 import service as _service from openstack.compute.v2 import volume_attachment as _volume_attachment -from openstack.network.v2 import security_group as _sg from openstack import exceptions +from openstack.network.v2 import security_group as _sg from openstack import proxy from openstack import resource from openstack import utils @@ -1768,7 +1768,7 @@ def create_server_remote_console(self, server, **attrs): ServerRemoteConsole` """ server_id = resource.Resource._get_id(server) - return self._create(_server_remote_console.ServerRemoteConsole, + return self._create(_src.ServerRemoteConsole, server_id=server_id, **attrs) def get_server_console_url(self, server, console_type): @@ -1806,7 +1806,7 @@ def create_console(self, server, console_type, console_protocol=None): # historically by OSC. We support it, but do not document either. if utils.supports_microversion(self, '2.6'): console = self._create( - _server_remote_console.ServerRemoteConsole, + _src.ServerRemoteConsole, server_id=server.id, type=console_type, protocol=console_protocol) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index ef027b363..fe492c897 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -11,8 +11,8 @@ # under the License. from openstack.compute.v2 import metadata -from openstack.image.v2 import image from openstack import exceptions +from openstack.image.v2 import image from openstack import resource from openstack import utils diff --git a/openstack/compute/v2/server_remote_console.py b/openstack/compute/v2/server_remote_console.py index a60c47678..2cafdb9f9 100644 --- a/openstack/compute/v2/server_remote_console.py +++ b/openstack/compute/v2/server_remote_console.py @@ -11,7 +11,6 @@ # under the License. from openstack import resource - from openstack import utils CONSOLE_TYPE_PROTOCOL_MAPPING = { diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 346d38f4c..9e29a811d 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -14,8 +14,8 @@ import copy import os.path -import warnings import urllib +import warnings from keystoneauth1 import discover import keystoneauth1.exceptions.catalog @@ -36,13 +36,12 @@ except ImportError: influxdb = None - -from openstack import version as openstack_version from openstack import _log from openstack.config import _util from openstack.config import defaults as config_defaults from openstack import exceptions from openstack import proxy +from openstack import version as openstack_version _logger = _log.setup_logging('openstack') @@ -53,7 +52,6 @@ 'system_scope' } - # Sentinel for nonexistence _ENOENT = object() diff --git a/openstack/connection.py b/openstack/connection.py index 8b8994941..b00d756be 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -176,6 +176,7 @@ Additional information about the services can be found in the :ref:`service-proxies` documentation. """ +import concurrent.futures import warnings import weakref @@ -185,20 +186,17 @@ except ImportError: # For everyone else import importlib_metadata - -import concurrent.futures import keystoneauth1.exceptions import requestsexceptions from openstack import _log from openstack import _services_mixin -from openstack.cloud import openstackcloud as _cloud from openstack.cloud import _accelerator from openstack.cloud import _baremetal from openstack.cloud import _block_storage -from openstack.cloud import _compute from openstack.cloud import _clustering from openstack.cloud import _coe +from openstack.cloud import _compute from openstack.cloud import _dns from openstack.cloud import _floating_ip from openstack.cloud import _identity @@ -209,6 +207,7 @@ from openstack.cloud import _orchestration from openstack.cloud import _security_group from openstack.cloud import _shared_file_system +from openstack.cloud import openstackcloud as _cloud from openstack import config as _config from openstack.config import cloud_region from openstack import exceptions diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py index 980d4c8e4..9fdcb602a 100644 --- a/openstack/dns/v2/_proxy.py +++ b/openstack/dns/v2/_proxy.py @@ -9,13 +9,14 @@ # 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 proxy + +from openstack.dns.v2 import floating_ip as _fip from openstack.dns.v2 import recordset as _rs from openstack.dns.v2 import zone as _zone -from openstack.dns.v2 import zone_import as _zone_import from openstack.dns.v2 import zone_export as _zone_export +from openstack.dns.v2 import zone_import as _zone_import from openstack.dns.v2 import zone_transfer as _zone_transfer -from openstack.dns.v2 import floating_ip as _fip +from openstack import proxy class Proxy(proxy.Proxy): diff --git a/openstack/dns/v2/floating_ip.py b/openstack/dns/v2/floating_ip.py index 703b04570..cc7487b43 100644 --- a/openstack/dns/v2/floating_ip.py +++ b/openstack/dns/v2/floating_ip.py @@ -9,9 +9,9 @@ # 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 resource from openstack.dns.v2 import _base +from openstack import resource class FloatingIP(_base.Resource): diff --git a/openstack/dns/v2/recordset.py b/openstack/dns/v2/recordset.py index 3ad5aace9..9688657f9 100644 --- a/openstack/dns/v2/recordset.py +++ b/openstack/dns/v2/recordset.py @@ -9,9 +9,9 @@ # 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 resource from openstack.dns.v2 import _base +from openstack import resource class Recordset(_base.Resource): diff --git a/openstack/dns/v2/zone.py b/openstack/dns/v2/zone.py index ccee3015a..bc8c4c09b 100644 --- a/openstack/dns/v2/zone.py +++ b/openstack/dns/v2/zone.py @@ -9,12 +9,12 @@ # 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.dns.v2 import _base from openstack import exceptions from openstack import resource from openstack import utils -from openstack.dns.v2 import _base - class Zone(_base.Resource): """DNS ZONE Resource""" diff --git a/openstack/dns/v2/zone_export.py b/openstack/dns/v2/zone_export.py index 1d551235e..d1320e186 100644 --- a/openstack/dns/v2/zone_export.py +++ b/openstack/dns/v2/zone_export.py @@ -9,10 +9,10 @@ # 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 import resource from openstack.dns.v2 import _base +from openstack import exceptions +from openstack import resource class ZoneExport(_base.Resource): diff --git a/openstack/dns/v2/zone_import.py b/openstack/dns/v2/zone_import.py index 1f642e00b..8e0ce8cc0 100644 --- a/openstack/dns/v2/zone_import.py +++ b/openstack/dns/v2/zone_import.py @@ -9,10 +9,10 @@ # 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 import resource from openstack.dns.v2 import _base +from openstack import exceptions +from openstack import resource class ZoneImport(_base.Resource): diff --git a/openstack/dns/v2/zone_transfer.py b/openstack/dns/v2/zone_transfer.py index 989b6f62f..417df265d 100644 --- a/openstack/dns/v2/zone_transfer.py +++ b/openstack/dns/v2/zone_transfer.py @@ -9,9 +9,9 @@ # 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 resource from openstack.dns.v2 import _base +from openstack import resource class ZoneTransferBase(_base.Resource): diff --git a/openstack/image/image_signer.py b/openstack/image/image_signer.py index 23d294811..c9ef993c3 100644 --- a/openstack/image/image_signer.py +++ b/openstack/image/image_signer.py @@ -11,11 +11,10 @@ # under the License. # - -from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives.asymmetric import utils -from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import serialization from openstack.image.iterable_chunked_file import IterableChunkedFile diff --git a/openstack/image/v1/image.py b/openstack/image/v1/image.py index c4905abfd..20d66ad8b 100644 --- a/openstack/image/v1/image.py +++ b/openstack/image/v1/image.py @@ -9,8 +9,9 @@ # 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.image import _download + from openstack import exceptions +from openstack.image import _download from openstack import resource diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 82c0f4350..a11e08f6f 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -18,8 +18,8 @@ from openstack.image.v2 import image as _image from openstack.image.v2 import member as _member from openstack.image.v2 import schema as _schema -from openstack.image.v2 import task as _task from openstack.image.v2 import service_info as _si +from openstack.image.v2 import task as _task from openstack import resource from openstack import utils diff --git a/openstack/key_manager/v1/_format.py b/openstack/key_manager/v1/_format.py index 4ff41efde..56dbca774 100644 --- a/openstack/key_manager/v1/_format.py +++ b/openstack/key_manager/v1/_format.py @@ -10,10 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import format - from urllib import parse +from openstack import format + class HREFToUUID(format.Formatter): diff --git a/openstack/network/v2/firewall_policy.py b/openstack/network/v2/firewall_policy.py index bc4afe484..c8fb70f9d 100644 --- a/openstack/network/v2/firewall_policy.py +++ b/openstack/network/v2/firewall_policy.py @@ -12,8 +12,8 @@ # 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.exceptions import HttpException +from openstack.exceptions import HttpException from openstack import resource from openstack import utils diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index d0874f96e..7fa36a551 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -19,14 +19,14 @@ import time from urllib import parse +from openstack import _log +from openstack.cloud import _utils +from openstack import exceptions from openstack.object_store.v1 import account as _account from openstack.object_store.v1 import container as _container -from openstack.object_store.v1 import obj as _obj from openstack.object_store.v1 import info as _info -from openstack import exceptions -from openstack import _log +from openstack.object_store.v1 import obj as _obj from openstack import proxy -from openstack.cloud import _utils DEFAULT_OBJECT_SEGMENT_SIZE = 1073741824 # 1GB DEFAULT_MAX_FILE_SIZE = (5 * 1024 * 1024 * 1024 + 2) / 2 diff --git a/openstack/orchestration/util/template_format.py b/openstack/orchestration/util/template_format.py index 9b95ef24e..f98e97fbb 100644 --- a/openstack/orchestration/util/template_format.py +++ b/openstack/orchestration/util/template_format.py @@ -11,6 +11,7 @@ # under the License. import json + import yaml if hasattr(yaml, 'CSafeLoader'): diff --git a/openstack/orchestration/util/template_utils.py b/openstack/orchestration/util/template_utils.py index 48213bc46..7280b54d6 100644 --- a/openstack/orchestration/util/template_utils.py +++ b/openstack/orchestration/util/template_utils.py @@ -17,10 +17,10 @@ from urllib import parse from urllib import request +from openstack import exceptions from openstack.orchestration.util import environment_format from openstack.orchestration.util import template_format from openstack.orchestration.util import utils -from openstack import exceptions def get_template_contents(template_file=None, template_url=None, diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index a94589ff9..a39f2671a 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import exceptions +from openstack.orchestration.util import template_utils from openstack.orchestration.v1 import resource as _resource from openstack.orchestration.v1 import software_config as _sc from openstack.orchestration.v1 import software_deployment as _sd @@ -18,8 +20,6 @@ from openstack.orchestration.v1 import stack_files as _stack_files from openstack.orchestration.v1 import stack_template as _stack_template from openstack.orchestration.v1 import template as _template -from openstack.orchestration.util import template_utils -from openstack import exceptions from openstack import proxy from openstack import resource diff --git a/openstack/proxy.py b/openstack/proxy.py index e135408d2..baa92205a 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -10,14 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. +import urllib + try: import simplejson JSONDecodeError = simplejson.scanner.JSONDecodeError except ImportError: JSONDecodeError = ValueError import iso8601 -import urllib - from keystoneauth1 import adapter from openstack import _log diff --git a/openstack/resource.py b/openstack/resource.py index bfc512c8f..035b08be5 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -34,10 +34,10 @@ class that represent a remote resource. The attributes that import collections import inspect import itertools +import operator import urllib.parse import jsonpatch -import operator from keystoneauth1 import adapter from keystoneauth1 import discover import munch diff --git a/openstack/tests/unit/cloud/test_stack.py b/openstack/tests/unit/cloud/test_stack.py index 1da6381d2..3006b185a 100644 --- a/openstack/tests/unit/cloud/test_stack.py +++ b/openstack/tests/unit/cloud/test_stack.py @@ -256,7 +256,7 @@ def test_delete_stack_by_name_wait(self): dict(method='GET', uri='{endpoint}/stacks/{name}?{resolve}'.format( endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name, resolve=resolve), + name=self.stack_name, resolve=resolve), status_code=404), ]) diff --git a/test-requirements.txt b/test-requirements.txt index ac8dbedee..3f22c9d11 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,7 @@ # 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>=3.0.1,<3.1.0 # Apache-2.0 +hacking>=3.1.0,<4.0.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 ddt>=1.0.1 # MIT @@ -15,5 +15,3 @@ statsd>=3.3.0 stestr>=1.0.0 # Apache-2.0 testscenarios>=0.4 # Apache-2.0/BSD testtools>=2.2.0 # MIT -doc8>=0.8.0 # Apache-2.0 -Pygments>=2.2.0 # BSD license diff --git a/tox.ini b/tox.ini index 6804a88ef..aad741513 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -minversion = 3.1 +minversion = 3.9.0 envlist = pep8,py38 skipsdist = True ignore_basepython_conflict = True @@ -41,8 +41,14 @@ commands = stestr --test-path ./openstack/tests/functional/{env:OPENSTACKSDK_TES stestr slowest [testenv:pep8] +deps = + hacking>=3.1.0,<4.0.0 # Apache-2.0 + flake8-import-order>=0.17.1 # LGPLv3 + pycodestyle>=2.0.0,<2.7.0 # MIT + Pygments>=2.2.0 # BSD + doc8>=0.8.0 # Apache 2.0 commands = - flake8 + flake8 {posargs} doc8 doc/source README.rst [testenv:venv] @@ -102,18 +108,21 @@ commands = sphinx-build -a -E -W -d releasenotes/build/doctrees --keep-going -b html releasenotes/source releasenotes/build/html [flake8] +application-import-names = openstack # The following are ignored on purpose. It's not super worth it to fix them. # However, if you feel strongly about it, patches will be accepted to fix them # if they fix ALL of the occurances of one and only one of them. # H238 New Style Classes are the default in Python3 -# H306 Is about alphabetical imports - there's a lot to fix. # H4 Are about docstrings and there's just a huge pile of pre-existing issues. # W503 Is supposed to be off by default but in the latest pycodestyle isn't. # Also, both openstacksdk and Donald Knuth disagree with the rule. Line # breaks should occur before the binary operator for readability. -ignore = H238,H306,H4,W503 +ignore = H238,H4,W503 +import-order-style = pep8 show-source = True exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,openstack/_services_mixin.py +per-file-ignores = + openstack/tests/*:H306,I100,I201,I202 [flake8:local-plugins] extension = From 5d88f882f91d8fb7e625ff8fc979c4bd3c176e45 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 11 Mar 2021 16:28:01 +0000 Subject: [PATCH 2787/3836] Add pre-commit This is helpful to automate code style checks at runtime. Change-Id: I99b0c8fc8ecc7d2a187b99db2ca4530482c6567e Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..c01a5d710 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,28 @@ +--- +default_language_version: + # force all unspecified python hooks to run python3 + python: python3 +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.4.0 + hooks: + - id: trailing-whitespace + - id: mixed-line-ending + args: ['--fix', 'lf'] + exclude: '.*\.(svg)$' + - id: check-byte-order-marker + - id: check-executables-have-shebangs + - id: check-merge-conflict + - id: debug-statements + - id: check-yaml + files: .*\.(yaml|yml)$ + - repo: local + hooks: + - id: flake8 + name: flake8 + additional_dependencies: + - hacking>=3.0.1,<3.1.0 + language: python + entry: flake8 + files: '^.*\.py$' + exclude: '^(doc|releasenotes|tools)/.*$' From 9a91047da6e6ca120b72a16e936aea9acc4c03eb Mon Sep 17 00:00:00 2001 From: Dylan Zapzalka Date: Wed, 17 Feb 2021 22:42:31 -0600 Subject: [PATCH 2788/3836] Added support for the Limits resource Introduce the Limits resource, fill in its resources, and implement API calls to support the Cinder v3 API. Task: 41816 Story: 2008619 Task: 41934 Story: 2008621 Change-Id: Icaf3ef8d33aa9be0f9b19b28d3206756d995e0b8 --- openstack/block_storage/v3/_proxy.py | 11 + openstack/block_storage/v3/limits.py | 79 +++++++ .../block_storage/v3/test_limits.py | 31 +++ .../unit/block_storage/v3/test_limits.py | 200 ++++++++++++++++++ .../tests/unit/block_storage/v3/test_proxy.py | 6 + 5 files changed, 327 insertions(+) create mode 100644 openstack/block_storage/v3/limits.py create mode 100644 openstack/tests/functional/block_storage/v3/test_limits.py create mode 100644 openstack/tests/unit/block_storage/v3/test_limits.py diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index c926bd13e..50bbb841e 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -13,6 +13,7 @@ from openstack.block_storage import _base_proxy from openstack.block_storage.v3 import availability_zone from openstack.block_storage.v3 import backup as _backup +from openstack.block_storage.v3 import limits as _limits from openstack.block_storage.v3 import snapshot as _snapshot from openstack.block_storage.v3 import stats as _stats from openstack.block_storage.v3 import type as _type @@ -486,6 +487,16 @@ def restore_backup(self, backup, volume_id=None, name=None): backup = self._get_resource(_backup.Backup, backup) return backup.restore(self, volume_id=volume_id, name=name) + def get_limits(self): + """Retrieves limits + + :returns: A Limit object, including both + :class:`~openstack.block_storage.v3.limits.AbsoluteLimit` and + :class:`~openstack.block_storage.v3.limits.RateLimit` + :rtype: :class:`~openstack.block_storage.v3.limits.Limit` + """ + return self._get(_limits.Limit, requires_id=False) + def availability_zones(self): """Return a generator of availability zones diff --git a/openstack/block_storage/v3/limits.py b/openstack/block_storage/v3/limits.py new file mode 100644 index 000000000..9448669f3 --- /dev/null +++ b/openstack/block_storage/v3/limits.py @@ -0,0 +1,79 @@ +# 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 resource + + +class AbsoluteLimit(resource.Resource): + #: Properties + #: The maximum total amount of backups, in gibibytes (GiB). + max_total_backup_gigabytes = resource.Body( + "maxTotalBackupGigabytes", type=int) + #: The maximum number of backups. + max_total_backups = resource.Body("maxTotalBackups", type=int) + #: The maximum number of snapshots. + max_total_snapshots = resource.Body("maxTotalSnapshots", type=int) + #: The maximum total amount of volumes, in gibibytes (GiB). + max_total_volume_gigabytes = resource.Body( + "maxTotalVolumeGigabytes", type=int) + #: The maximum number of volumes. + max_total_volumes = resource.Body("maxTotalVolumes", type=int) + #: The total number of backups gibibytes (GiB) used. + total_backup_gigabytes_used = resource.Body( + "totalBackupGigabytesUsed", type=int) + #: The total number of backups used. + total_backups_used = resource.Body("totalBackupsUsed", type=int) + #: The total number of gibibytes (GiB) used. + total_gigabytes_used = resource.Body("totalGigabytesUsed", type=int) + #: The total number of snapshots used. + total_snapshots_used = resource.Body("totalSnapshotsUsed", type=int) + #: The total number of volumes used. + total_volumes_used = resource.Body("totalVolumesUsed", type=int) + + +class RateLimit(resource.Resource): + #: Properties + #: Rate limits next availabe time. + next_available = resource.Body("next-available") + #: Integer for rate limits remaining. + remaining = resource.Body("remaining", type=int) + #: Unit of measurement for the value parameter. + unit = resource.Body("unit") + #: Integer number of requests which can be made. + value = resource.Body("value", type=int) + #: An HTTP verb (POST, PUT, etc.). + verb = resource.Body("verb") + + +class RateLimits(resource.Resource): + #: Properties + #: A list of the specific limits that apply to the ``regex`` and ``uri``. + limits = resource.Body("limit", type=list, list_type=RateLimit) + #: A regex representing which routes this rate limit applies to. + regex = resource.Body("regex") + #: A URI representing which routes this rate limit applies to. + uri = resource.Body("uri") + + +class Limit(resource.Resource): + resource_key = "limits" + base_path = "/limits" + + # capabilities + allow_fetch = True + + #: Properties + #: An absolute limits object. + absolute = resource.Body("absolute", type=AbsoluteLimit) + #: Rate-limit volume copy bandwidth, used to mitigate + #: slow down of data access from the instances. + rate = resource.Body("rate", type=list, list_type=RateLimits) diff --git a/openstack/tests/functional/block_storage/v3/test_limits.py b/openstack/tests/functional/block_storage/v3/test_limits.py new file mode 100644 index 000000000..bd7c9fd66 --- /dev/null +++ b/openstack/tests/functional/block_storage/v3/test_limits.py @@ -0,0 +1,31 @@ +# 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.tests.functional.block_storage.v3 import base + + +class TestLimits(base.BaseBlockStorageTest): + + def test_get(self): + sot = self.conn.block_storage.get_limits() + self.assertIsNotNone(sot.absolute.max_total_backup_gigabytes) + self.assertIsNotNone(sot.absolute.max_total_backups) + self.assertIsNotNone(sot.absolute.max_total_snapshots) + self.assertIsNotNone(sot.absolute.max_total_volume_gigabytes) + self.assertIsNotNone(sot.absolute.max_total_volumes) + self.assertIsNotNone(sot.absolute.total_backup_gigabytes_used) + self.assertIsNotNone(sot.absolute.total_backups_used) + self.assertIsNotNone(sot.absolute.total_gigabytes_used) + self.assertIsNotNone(sot.absolute.total_snapshots_used) + self.assertIsNotNone(sot.absolute.total_volumes_used) + self.assertIsNotNone(sot.rate) diff --git a/openstack/tests/unit/block_storage/v3/test_limits.py b/openstack/tests/unit/block_storage/v3/test_limits.py new file mode 100644 index 000000000..d312b0fd1 --- /dev/null +++ b/openstack/tests/unit/block_storage/v3/test_limits.py @@ -0,0 +1,200 @@ +# 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.block_storage.v3 import limits +from openstack.tests.unit import base + +ABSOLUTE_LIMIT = { + "totalSnapshotsUsed": 1, + "maxTotalBackups": 10, + "maxTotalVolumeGigabytes": 1000, + "maxTotalSnapshots": 10, + "maxTotalBackupGigabytes": 1000, + "totalBackupGigabytesUsed": 1, + "maxTotalVolumes": 10, + "totalVolumesUsed": 2, + "totalBackupsUsed": 3, + "totalGigabytesUsed": 2 +} + +RATE_LIMIT = { + "verb": "POST", + "value": 80, + "remaining": 80, + "unit": "MINUTE", + "next-available": "2021-02-23T22:08:00Z" +} + +RATE_LIMITS = { + "regex": ".*", + "uri": "*", + "limit": [RATE_LIMIT] +} + +LIMIT = { + "rate": [RATE_LIMITS], + "absolute": ABSOLUTE_LIMIT +} + + +class TestAbsoluteLimit(base.TestCase): + + def test_basic(self): + limit_resource = limits.AbsoluteLimit() + self.assertIsNone(limit_resource.resource_key) + self.assertIsNone(limit_resource.resources_key) + self.assertEqual('', limit_resource.base_path) + self.assertFalse(limit_resource.allow_create) + self.assertFalse(limit_resource.allow_fetch) + self.assertFalse(limit_resource.allow_delete) + self.assertFalse(limit_resource.allow_commit) + self.assertFalse(limit_resource.allow_list) + + def test_make_absolute_limit(self): + limit_resource = limits.AbsoluteLimit(**ABSOLUTE_LIMIT) + self.assertEqual( + ABSOLUTE_LIMIT['totalSnapshotsUsed'], + limit_resource.total_snapshots_used) + self.assertEqual( + ABSOLUTE_LIMIT['maxTotalBackups'], + limit_resource.max_total_backups) + self.assertEqual( + ABSOLUTE_LIMIT['maxTotalVolumeGigabytes'], + limit_resource.max_total_volume_gigabytes) + self.assertEqual( + ABSOLUTE_LIMIT['maxTotalSnapshots'], + limit_resource.max_total_snapshots) + self.assertEqual( + ABSOLUTE_LIMIT['maxTotalBackupGigabytes'], + limit_resource.max_total_backup_gigabytes) + self.assertEqual( + ABSOLUTE_LIMIT['totalBackupGigabytesUsed'], + limit_resource.total_backup_gigabytes_used) + self.assertEqual( + ABSOLUTE_LIMIT['maxTotalVolumes'], + limit_resource.max_total_volumes) + self.assertEqual( + ABSOLUTE_LIMIT['totalVolumesUsed'], + limit_resource.total_volumes_used) + self.assertEqual( + ABSOLUTE_LIMIT['totalBackupsUsed'], + limit_resource.total_backups_used) + self.assertEqual( + ABSOLUTE_LIMIT['totalGigabytesUsed'], + limit_resource.total_gigabytes_used) + + +class TestRateLimit(base.TestCase): + + def test_basic(self): + limit_resource = limits.RateLimit() + self.assertIsNone(limit_resource.resource_key) + self.assertIsNone(limit_resource.resources_key) + self.assertEqual('', limit_resource.base_path) + self.assertFalse(limit_resource.allow_create) + self.assertFalse(limit_resource.allow_fetch) + self.assertFalse(limit_resource.allow_delete) + self.assertFalse(limit_resource.allow_commit) + self.assertFalse(limit_resource.allow_list) + + def test_make_rate_limit(self): + limit_resource = limits.RateLimit(**RATE_LIMIT) + self.assertEqual(RATE_LIMIT['verb'], limit_resource.verb) + self.assertEqual(RATE_LIMIT['value'], limit_resource.value) + self.assertEqual(RATE_LIMIT['remaining'], limit_resource.remaining) + self.assertEqual(RATE_LIMIT['unit'], limit_resource.unit) + self.assertEqual( + RATE_LIMIT['next-available'], limit_resource.next_available) + + +class TestRateLimits(base.TestCase): + + def test_basic(self): + limit_resource = limits.RateLimits() + self.assertIsNone(limit_resource.resource_key) + self.assertIsNone(limit_resource.resources_key) + self.assertEqual('', limit_resource.base_path) + self.assertFalse(limit_resource.allow_create) + self.assertFalse(limit_resource.allow_fetch) + self.assertFalse(limit_resource.allow_delete) + self.assertFalse(limit_resource.allow_commit) + self.assertFalse(limit_resource.allow_list) + + def _test_rate_limit(self, expected, actual): + self.assertEqual(expected[0]['verb'], actual[0].verb) + self.assertEqual(expected[0]['value'], actual[0].value) + self.assertEqual(expected[0]['remaining'], actual[0].remaining) + self.assertEqual(expected[0]['unit'], actual[0].unit) + self.assertEqual( + expected[0]['next-available'], actual[0].next_available) + + def test_make_rate_limits(self): + limit_resource = limits.RateLimits(**RATE_LIMITS) + self.assertEqual(RATE_LIMITS['regex'], limit_resource.regex) + self.assertEqual(RATE_LIMITS['uri'], limit_resource.uri) + self._test_rate_limit(RATE_LIMITS['limit'], limit_resource.limits) + + +class TestLimit(base.TestCase): + + def test_basic(self): + limit_resource = limits.Limit() + self.assertEqual('limits', limit_resource.resource_key) + self.assertEqual('/limits', limit_resource.base_path) + self.assertTrue(limit_resource.allow_fetch) + self.assertFalse(limit_resource.allow_create) + self.assertFalse(limit_resource.allow_commit) + self.assertFalse(limit_resource.allow_delete) + self.assertFalse(limit_resource.allow_list) + + def _test_absolute_limit(self, expected, actual): + self.assertEqual( + expected['totalSnapshotsUsed'], actual.total_snapshots_used) + self.assertEqual( + expected['maxTotalBackups'], actual.max_total_backups) + self.assertEqual( + expected['maxTotalVolumeGigabytes'], + actual.max_total_volume_gigabytes) + self.assertEqual( + expected['maxTotalSnapshots'], actual.max_total_snapshots) + self.assertEqual( + expected['maxTotalBackupGigabytes'], + actual.max_total_backup_gigabytes) + self.assertEqual( + expected['totalBackupGigabytesUsed'], + actual.total_backup_gigabytes_used) + self.assertEqual( + expected['maxTotalVolumes'], actual.max_total_volumes) + self.assertEqual( + expected['totalVolumesUsed'], actual.total_volumes_used) + self.assertEqual( + expected['totalBackupsUsed'], actual.total_backups_used) + self.assertEqual( + expected['totalGigabytesUsed'], actual.total_gigabytes_used) + + def _test_rate_limit(self, expected, actual): + self.assertEqual(expected[0]['verb'], actual[0].verb) + self.assertEqual(expected[0]['value'], actual[0].value) + self.assertEqual(expected[0]['remaining'], actual[0].remaining) + self.assertEqual(expected[0]['unit'], actual[0].unit) + self.assertEqual( + expected[0]['next-available'], actual[0].next_available) + + def _test_rate_limits(self, expected, actual): + self.assertEqual(expected[0]['regex'], actual[0].regex) + self.assertEqual(expected[0]['uri'], actual[0].uri) + self._test_rate_limit(expected[0]['limit'], actual[0].limits) + + def test_make_limit(self): + limit_resource = limits.Limit(**LIMIT) + self._test_rate_limits(LIMIT['rate'], limit_resource.rate) + self._test_absolute_limit(LIMIT['absolute'], limit_resource.absolute) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 7fd4908ce..ca63a1ef1 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -13,6 +13,7 @@ from openstack.block_storage.v3 import _proxy from openstack.block_storage.v3 import backup +from openstack.block_storage.v3 import limits from openstack.block_storage.v3 import snapshot from openstack.block_storage.v3 import stats from openstack.block_storage.v3 import type @@ -218,3 +219,8 @@ def test_backup_restore(self): expected_args=[self.proxy], expected_kwargs={'volume_id': 'vol_id', 'name': 'name'} ) + + def test_limits_get(self): + self.verify_get( + self.proxy.get_limits, limits.Limit, ignore_value=True, + expected_kwargs={'requires_id': False}) From f387e250a637835091d654a43afbde4b0d41f209 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 19 Mar 2021 13:13:13 +0000 Subject: [PATCH 2789/3836] Update master for stable/wallaby Add file to the reno documentation build to show release notes for stable/wallaby. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/wallaby. Sem-Ver: feature Change-Id: Id2ca6a7964fc80e61652e42e6505ab0f1214b71b --- releasenotes/source/index.rst | 1 + releasenotes/source/wallaby.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/wallaby.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 0bd14ba7a..7a087bded 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ :maxdepth: 1 unreleased + wallaby victoria ussuri train diff --git a/releasenotes/source/wallaby.rst b/releasenotes/source/wallaby.rst new file mode 100644 index 000000000..d77b56599 --- /dev/null +++ b/releasenotes/source/wallaby.rst @@ -0,0 +1,6 @@ +============================ +Wallaby Series Release Notes +============================ + +.. release-notes:: + :branch: stable/wallaby From 00f39f3fefd8f89325f0be6424c4ee596009975f Mon Sep 17 00:00:00 2001 From: Dylan Zapzalka Date: Sun, 28 Feb 2021 16:47:31 -0600 Subject: [PATCH 2790/3836] Added support for the Capabilities resource Introduce the Capabilities resource, fill in its resources, and implement API calls to support the Cinder v3 API. Task: 41852 Story: 2008619 Task: 41950 Story: 2008621 Change-Id: I695c53fe2551565cbdf0428b518523220935c594 --- openstack/block_storage/v3/_proxy.py | 13 +++ openstack/block_storage/v3/capabilities.py | 45 +++++++++ .../block_storage/v3/test_capabilities.py | 36 ++++++++ .../block_storage/v3/test_capabilities.py | 92 +++++++++++++++++++ .../tests/unit/block_storage/v3/test_proxy.py | 4 + 5 files changed, 190 insertions(+) create mode 100644 openstack/block_storage/v3/capabilities.py create mode 100644 openstack/tests/functional/block_storage/v3/test_capabilities.py create mode 100644 openstack/tests/unit/block_storage/v3/test_capabilities.py diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 6c56f230b..c0276a74c 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -13,6 +13,7 @@ from openstack.block_storage import _base_proxy from openstack.block_storage.v3 import availability_zone from openstack.block_storage.v3 import backup as _backup +from openstack.block_storage.v3 import capabilities as _capabilities from openstack.block_storage.v3 import limits as _limits from openstack.block_storage.v3 import snapshot as _snapshot from openstack.block_storage.v3 import stats as _stats @@ -525,6 +526,18 @@ def get_limits(self): """ return self._get(_limits.Limit, requires_id=False) + def get_capabilities(self, host): + """Get a backend's capabilites + + :param host: Specified backend to obtain volume stats and properties. + + :returns: One :class: + `~openstack.block_storage.v3.capabilites.Capabilities` instance. + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource can be found. + """ + return self._get(_capabilities.Capabilities, host) + def availability_zones(self): """Return a generator of availability zones diff --git a/openstack/block_storage/v3/capabilities.py b/openstack/block_storage/v3/capabilities.py new file mode 100644 index 000000000..03d958d8b --- /dev/null +++ b/openstack/block_storage/v3/capabilities.py @@ -0,0 +1,45 @@ +# 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 resource + + +class Capabilities(resource.Resource): + base_path = "/capabilities" + + # Capabilities + allow_fetch = True + + #: Properties + #: The capabilities description + description = resource.Body("description") + #: The name of volume backend capabilities. + display_name = resource.Body("display_name") + #: The driver version. + driver_version = resource.Body("driver_version") + #: The storage namespace, such as OS::Storage::Capabilities::foo. + namespace = resource.Body("namespace") + #: The name of the storage pool. + pool_name = resource.Body("pool_name") + #: The backend volume capabilites list, which consists of cinder + #: standard capabilities and vendor unique properties. + properties = resource.Body("properties", type=dict) + #: A list of volume backends used to replicate volumes on this backend. + replication_targets = resource.Body("replication_targets", type=list) + #: The storage backend for the backend volume. + storage_protocol = resource.Body("storage_protocol") + #: The name of the vendor. + vendor_name = resource.Body("vendor_name") + #: The volume type access. + visibility = resource.Body("visibility") + #: The name of the back-end volume. + volume_backend_name = resource.Body("volume_backend_name") diff --git a/openstack/tests/functional/block_storage/v3/test_capabilities.py b/openstack/tests/functional/block_storage/v3/test_capabilities.py new file mode 100644 index 000000000..0b650e529 --- /dev/null +++ b/openstack/tests/functional/block_storage/v3/test_capabilities.py @@ -0,0 +1,36 @@ +# 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.tests.functional.block_storage.v3 import base +from openstack import proxy + + +class TestCapabilities(base.BaseBlockStorageTest): + + def test_get(self): + response = ( + proxy._json_response(self.conn.block_storage.get('/os-hosts'))) + host = response['hosts'][0]['host_name'] + + sot = self.conn.block_storage.get_capabilities(host) + self.assertIn('description', sot) + self.assertIn('display_name', sot) + self.assertIn('driver_version', sot) + self.assertIn('namespace', sot) + self.assertIn('pool_name', sot) + self.assertIn('properties', sot) + self.assertIn('replication_targets', sot) + self.assertIn('storage_protocol', sot) + self.assertIn('vendor_name', sot) + self.assertIn('visibility', sot) + self.assertIn('volume_backend_name', sot) diff --git a/openstack/tests/unit/block_storage/v3/test_capabilities.py b/openstack/tests/unit/block_storage/v3/test_capabilities.py new file mode 100644 index 000000000..ad52e0125 --- /dev/null +++ b/openstack/tests/unit/block_storage/v3/test_capabilities.py @@ -0,0 +1,92 @@ +# 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.block_storage.v3 import capabilities +from openstack.tests.unit import base + +CAPABILITIES = { + "namespace": "OS::Storage::Capabilities::fake", + "vendor_name": "OpenStack", + "volume_backend_name": "lvmdriver-1", + "pool_name": "pool", + "driver_version": "2.0.0", + "storage_protocol": "iSCSI", + "display_name": "Capabilities of Cinder LVM driver", + "description": "These are volume type options", + "visibility": "public", + "replication_targets": [], + "properties": { + "compression": { + "title": "Compression", + "description": "Enables compression.", + "type": "boolean" + }, + "qos": { + "title": "QoS", + "description": "Enables QoS.", + "type": "boolean" + }, + "replication": { + "title": "Replication", + "description": "Enables replication.", + "type": "boolean" + }, + "thin_provisioning": { + "title": "Thin Provisioning", + "description": "Sets thin provisioning.", + "type": "boolean" + } + } +} + + +class TestCapabilites(base.TestCase): + + def test_basic(self): + capabilities_resource = capabilities.Capabilities() + self.assertEqual(None, capabilities_resource.resource_key) + self.assertEqual(None, capabilities_resource.resources_key) + self.assertEqual("/capabilities", capabilities_resource.base_path) + self.assertTrue(capabilities_resource.allow_fetch) + self.assertFalse(capabilities_resource.allow_create) + self.assertFalse(capabilities_resource.allow_commit) + self.assertFalse(capabilities_resource.allow_delete) + self.assertFalse(capabilities_resource.allow_list) + + def test_make_capabilities(self): + capabilities_resource = capabilities.Capabilities(**CAPABILITIES) + self.assertEqual( + CAPABILITIES["description"], capabilities_resource.description) + self.assertEqual( + CAPABILITIES["display_name"], capabilities_resource.display_name) + self.assertEqual( + CAPABILITIES["driver_version"], + capabilities_resource.driver_version) + self.assertEqual( + CAPABILITIES["namespace"], capabilities_resource.namespace) + self.assertEqual( + CAPABILITIES["pool_name"], capabilities_resource.pool_name) + self.assertEqual( + CAPABILITIES["properties"], capabilities_resource.properties) + self.assertEqual( + CAPABILITIES["replication_targets"], + capabilities_resource.replication_targets) + self.assertEqual( + CAPABILITIES["storage_protocol"], + capabilities_resource.storage_protocol) + self.assertEqual( + CAPABILITIES["vendor_name"], capabilities_resource.vendor_name) + self.assertEqual( + CAPABILITIES["visibility"], capabilities_resource.visibility) + self.assertEqual( + CAPABILITIES["volume_backend_name"], + capabilities_resource.volume_backend_name) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 72f131be6..b625e6337 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -13,6 +13,7 @@ from openstack.block_storage.v3 import _proxy from openstack.block_storage.v3 import backup +from openstack.block_storage.v3 import capabilities from openstack.block_storage.v3 import limits from openstack.block_storage.v3 import snapshot from openstack.block_storage.v3 import stats @@ -236,3 +237,6 @@ def test_limits_get(self): self.verify_get( self.proxy.get_limits, limits.Limit, ignore_value=True, expected_kwargs={'requires_id': False}) + + def test_capabilites_get(self): + self.verify_get(self.proxy.get_capabilities, capabilities.Capabilities) From 85a2f101598c9c68c8f3f824dbc38b88ab6cdde0 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 23 Mar 2021 18:12:11 +0000 Subject: [PATCH 2791/3836] Improve README to provide example of Resource usage Provide a simple "hello world" example that includes details on using the 'Resource' subclasses. References to 'clouds.yaml' are also moved earlier in the doc since this is a pretty essential part of using openstacksdk. Change-Id: I6e7e6b5d9f54ec15b8e886b78649b50e02d38fc4 Signed-off-by: Stephen Finucane --- README.rst | 111 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 72 insertions(+), 39 deletions(-) diff --git a/README.rst b/README.rst index 3f01b050c..8b4148a72 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,4 @@ +============ openstacksdk ============ @@ -10,17 +11,39 @@ It also contains an abstraction interface layer. Clouds can do many things, but there are probably only about 10 of them that most people care about with any regularity. If you want to do complicated things, the per-service oriented portions of the SDK are for you. However, if what you want is to be able to -write an application that talks to clouds no matter what crazy choices the -deployer has made in an attempt to be more hipster than their self-entitled -narcissist peers, then the Cloud Abstraction layer is for you. +write an application that talks to any OpenStack cloud regardless of +configuration, then the Cloud Abstraction layer is for you. -More information about its history can be found at +More information about the history of openstacksdk can be found at https://docs.openstack.org/openstacksdk/latest/contributor/history.html -openstack -========= +Getting started +--------------- + +openstacksdk aims to talk to any OpenStack cloud. To do this, it requires a +configuration file. openstacksdk favours ``clouds.yaml`` files, but can also +use environment variables. The ``clouds.yaml`` file should be provided by your +cloud provider or deployment tooling. An example: + +.. code-block:: yaml + + clouds: + mordred: + region_name: Dallas + auth: + username: 'mordred' + password: XXXXXXX + project_name: 'demo' + auth_url: 'https://identity.example.com' + +openstacksdk will look for ``clouds.yaml`` files in the following locations: + +* ``.`` (the current directory) +* ``$HOME/.config/openstack`` +* ``/etc/openstack`` -List servers using objects configured with the ``clouds.yaml`` file: +openstacksdk consists of three layers. Most users will make use of the *proxy* +layer. Using the above ``clouds.yaml``, consider listing servers: .. code-block:: python @@ -29,16 +52,15 @@ List servers using objects configured with the ``clouds.yaml`` file: # Initialize and turn on debug logging openstack.enable_logging(debug=True) - # Initialize cloud + # Initialize connection conn = openstack.connect(cloud='mordred') + # List the servers for server in conn.compute.servers(): print(server.to_dict()) -Cloud Layer -=========== - -``openstacksdk`` contains a higher-level layer based on logical operations. +openstacksdk also contains a higher-level *cloud* layer based on logical +operations: .. code-block:: python @@ -47,11 +69,15 @@ Cloud Layer # Initialize and turn on debug logging openstack.enable_logging(debug=True) + # Initialize connection + conn = openstack.connect(cloud='mordred') + + # List the servers for server in conn.list_servers(): print(server.to_dict()) -The benefit is mostly seen in more complicated operations that take multiple -steps and where the steps vary across providers: +The benefit of this layer is mostly seen in more complicated operations that +take multiple steps and where the steps vary across providers. For example: .. code-block:: python @@ -61,7 +87,6 @@ steps and where the steps vary across providers: openstack.enable_logging(debug=True) # Initialize connection - # Cloud configs are read with openstack.config conn = openstack.connect(cloud='mordred') # Upload an image to the cloud @@ -72,14 +97,37 @@ steps and where the steps vary across providers: flavor = conn.get_flavor_by_ram(512) # Boot a server, wait for it to boot, and then do whatever is needed - # to get a public ip for it. + # to get a public IP address for it. conn.create_server( 'my-server', image=image, flavor=flavor, wait=True, auto_ip=True) -openstack.config -================ +Finally, there is the low-level *resource* layer. This provides support for the +basic CRUD operations supported by REST APIs and is the base building block for +the other layers. You typically will not need to use this directly: + +.. code-block:: python + + import openstack + import openstack.config.loader + import openstack.compute.v2.server + + # Initialize and turn on debug logging + openstack.enable_logging(debug=True) + + # Initialize connection + conn = openstack.connect(cloud='mordred') + + # List the servers + for server in openstack.compute.v2.server.Server.list(session=conn.compute): + print(server.to_dict()) + +.. _openstack.config: + +Configuration +------------- -``openstack.config`` will find cloud configuration for as few as 1 clouds and +openstacksdk uses the ``openstack.config`` module to parse configuration. +``openstack.config`` will find cloud configuration for as few as one cloud and as many as you want to put in a config file. It will read environment variables and config files, and it also contains some vendor specific default values so that you don't have to know extra info to use OpenStack @@ -88,32 +136,17 @@ that you don't have to know extra info to use OpenStack * If you have environment variables, you will get a cloud named `envvars` * If you have neither, you will get a cloud named `defaults` with base defaults -Sometimes an example is nice. +You can view the configuration identified by openstacksdk in your current +environment by running ``openstack.config.loader``. For example: -Create a ``clouds.yaml`` file: +.. code-block:: bash -.. code-block:: yaml - - clouds: - mordred: - region_name: Dallas - auth: - username: 'mordred' - password: XXXXXXX - project_name: 'shade' - auth_url: 'https://identity.example.com' - -Please note: ``openstack.config`` will look for a file called ``clouds.yaml`` -in the following locations: - -* Current Directory -* ``~/.config/openstack`` -* ``/etc/openstack`` + $ python -m openstack.config.loader More information at https://docs.openstack.org/openstacksdk/latest/user/config/configuration.html Links -===== +----- * `Issue Tracker `_ * `Code Review `_ From f280f7cdd67dece901f01bfe1a4cc888850f6e44 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 24 Mar 2021 12:15:33 +0000 Subject: [PATCH 2792/3836] docs: Add intro doc to user guide This is essentially a repeat of content from the recently updated README. We simply duplicate it rather than using complicated includes because $effort. Change-Id: If41a6b2d43d00e5bd78f6b2bd5bf33fa4a5d38d5 Signed-off-by: Stephen Finucane --- doc/source/contributor/index.rst | 25 +++--- doc/source/index.rst | 13 ++- doc/source/install/index.rst | 22 +++-- doc/source/user/config/configuration.rst | 6 +- doc/source/user/guides/intro.rst | 102 +++++++++++++++++++++++ doc/source/user/index.rst | 25 ++---- 6 files changed, 148 insertions(+), 45 deletions(-) create mode 100644 doc/source/user/guides/intro.rst diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst index d9573fde1..e55b1d7a7 100644 --- a/doc/source/contributor/index.rst +++ b/doc/source/contributor/index.rst @@ -1,9 +1,10 @@ +================================= Contributing to the OpenStack SDK ================================= This section of documentation pertains to those who wish to contribute to the development of this SDK. If you're looking for documentation on how to use -the SDK to build applications, please see the `user <../user>`_ section. +the SDK to build applications, refer to the `user <../user>`_ section. About the Project ----------------- @@ -30,22 +31,24 @@ Contacting the Developers ------------------------- IRC -*** +~~~ + +The developers of this project are available in the `#openstack-sdks`__ channel +on Freenode. This channel includes conversation on SDKs and tools within the +general OpenStack community, including OpenStackClient as well as occasional +talk about SDKs created for languages outside of Python. -The developers of this project are available in the -`#openstack-sdks `_ -channel on Freenode. This channel includes conversation on SDKs and tools -within the general OpenStack community, including OpenStackClient as well -as occasional talk about SDKs created for languages outside of Python. +.. __: http://webchat.freenode.net?channels=%23openstack-sdks Email -***** +~~~~~ -The `openstack-discuss `_ -mailing list fields questions of all types on OpenStack. Using the -``[sdk]`` filter to begin your email subject will ensure +The `openstack-discuss`__ mailing list fields questions of all types on +OpenStack. Using the ``[sdk]`` filter to begin your email subject will ensure that the message gets to SDK developers. +.. __: mailto:openstack-discuss@openstack.org?subject=[sdk]%20Question%20about%20openstacksdk + Coding Standards ---------------- diff --git a/doc/source/index.rst b/doc/source/index.rst index 19b9bdead..c5a8c5d3a 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -1,12 +1,13 @@ -Welcome to the OpenStack SDK! -============================= +============ +openstacksdk +============ This documentation is split into three sections: -* an :doc:`installation ` guide -* a section for :doc:`users ` looking to build applications +* An :doc:`installation ` guide +* A section for :doc:`users ` looking to build applications which make use of OpenStack -* a section for those looking to :doc:`contribute ` +* A section for those looking to :doc:`contribute ` to this project Installation @@ -33,8 +34,6 @@ For Contributors contributor/index -.. include:: ../../README.rst - General Information ------------------- diff --git a/doc/source/install/index.rst b/doc/source/install/index.rst index fcbfcb74a..49cf87d5d 100644 --- a/doc/source/install/index.rst +++ b/doc/source/install/index.rst @@ -1,12 +1,18 @@ -============ -Installation -============ +================== +Installation guide +================== -At the command line:: +The OpenStack SDK is available on `PyPI`__ under the name **openstacksdk**. To +install it, use ``pip``: - $ pip install openstacksdk +.. code-block:: bash -Or, if you have virtualenv wrapper installed:: + $ pip install openstacksdk - $ mkvirtualenv openstacksdk - $ pip install openstacksdk +To check the installed version you can call the module with: + +.. code-block:: bash + + $ python -m openstack version + +.. __: https://pypi.org/project/openstacksdk diff --git a/doc/source/user/config/configuration.rst b/doc/source/user/config/configuration.rst index 36fbf3bb2..d6fb958b3 100644 --- a/doc/source/user/config/configuration.rst +++ b/doc/source/user/config/configuration.rst @@ -34,9 +34,9 @@ Config Files `openstacksdk` will look for a file called `clouds.yaml` in the following locations: -* Current Directory -* ~/.config/openstack -* /etc/openstack +* ``.`` (the current directory) +* ``$HOME/.config/openstack`` +* ``/etc/openstack`` The first file found wins. diff --git a/doc/source/user/guides/intro.rst b/doc/source/user/guides/intro.rst new file mode 100644 index 000000000..ac53905e4 --- /dev/null +++ b/doc/source/user/guides/intro.rst @@ -0,0 +1,102 @@ +=============== +Getting started +=============== + +openstacksdk aims to talk to any OpenStack cloud. To do this, it requires a +configuration file. openstacksdk favours ``clouds.yaml`` files, but can also +use environment variables. The ``clouds.yaml`` file should be provided by your +cloud provider or deployment tooling. An example: + +.. code-block:: yaml + + clouds: + mordred: + region_name: Dallas + auth: + username: 'mordred' + password: XXXXXXX + project_name: 'demo' + auth_url: 'https://identity.example.com' + +More information on configuring openstacksdk can be found in +:doc:`/user/config/configuration`. + +Given sufficient configuration, you can use openstacksdk to interact with your +cloud. openstacksdk consists of three layers. Most users will make use of the +*proxy* layer. Using the above ``clouds.yaml``, consider listing servers: + +.. code-block:: python + + import openstack + + # Initialize and turn on debug logging + openstack.enable_logging(debug=True) + + # Initialize connection + conn = openstack.connect(cloud='mordred') + + # List the servers + for server in conn.compute.servers(): + print(server.to_dict()) + +openstacksdk also contains a higher-level *cloud* layer based on logical +operations: + +.. code-block:: python + + import openstack + + # Initialize and turn on debug logging + openstack.enable_logging(debug=True) + + # Initialize connection + conn = openstack.connect(cloud='mordred') + + # List the servers + for server in conn.list_servers(): + print(server.to_dict()) + +The benefit of this layer is mostly seen in more complicated operations that +take multiple steps and where the steps vary across providers. For example: + +.. code-block:: python + + import openstack + + # Initialize and turn on debug logging + openstack.enable_logging(debug=True) + + # Initialize connection + conn = openstack.connect(cloud='mordred') + + # Upload an image to the cloud + image = conn.create_image( + 'ubuntu-trusty', filename='ubuntu-trusty.qcow2', wait=True) + + # Find a flavor with at least 512M of RAM + flavor = conn.get_flavor_by_ram(512) + + # Boot a server, wait for it to boot, and then do whatever is needed + # to get a public IP address for it. + conn.create_server( + 'my-server', image=image, flavor=flavor, wait=True, auto_ip=True) + +Finally, there is the low-level *resource* layer. This provides support for the +basic CRUD operations supported by REST APIs and is the base building block for +the other layers. You typically will not need to use this directly: + +.. code-block:: python + + import openstack + import openstack.config.loader + import openstack.compute.v2.server + + # Initialize and turn on debug logging + openstack.enable_logging(debug=True) + + # Initialize connection + conn = openstack.connect(cloud='mordred') + + # List the servers + for server in openstack.compute.v2.server.Server.list(session=conn.compute): + print(server.to_dict()) diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index b13a63bf4..293f027a4 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -1,22 +1,14 @@ -Getting started with the OpenStack SDK -====================================== +======================= +Using the OpenStack SDK +======================= + +This section of documentation pertains to those who wish to use this SDK in +their own application. If you're looking for documentation on how to contribute +to or extend the SDK, refer to the `contributor <../contributor>`_ section. For a listing of terms used throughout the SDK, including the names of projects and services supported by it, see the :doc:`glossary <../glossary>`. -Installation ------------- - -The OpenStack SDK is available on -`PyPI `_ under the name -**openstacksdk**. To install it, use ``pip``:: - - $ pip install openstacksdk - -To check the installed version you can call the module with :: - - $ python -m openstack version - .. _user_guides: User Guides @@ -29,6 +21,7 @@ approach, this is where you'll want to begin. .. toctree:: :maxdepth: 1 + Introduction Configuration Connect to an OpenStack Cloud Connect to an OpenStack Cloud Using a Config File @@ -167,7 +160,7 @@ can be customized. utils Presentations -============= +------------- .. toctree:: :maxdepth: 1 From 5ba1e90273e090d00742af58b826b574734ca8c6 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 24 Mar 2021 12:20:26 +0000 Subject: [PATCH 2793/3836] tox: Enable parallel docs build Add the '-j auto' flag to all 'sphinx-build' invocations. While we're here remove the unnecessary '-d DIR' argument: Sphinx will configure a sane doctree directory for us without any special configuration. This results in a tidy little speed boost. Before: real 1m23.772s user 1m9.665s sys 0m6.299s After: real 0m54.613s user 1m26.129s sys 0m7.810s Change-Id: Id9240456f8bb289620a64792b433dbde4fa09586 Signed-off-by: Stephen Finucane --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index aad741513..acf9cae37 100644 --- a/tox.ini +++ b/tox.ini @@ -90,14 +90,14 @@ deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/doc/requirements.txt commands = - sphinx-build -W -d doc/build/doctrees --keep-going -b html doc/source/ doc/build/html + sphinx-build -W --keep-going -b html -j auto doc/source/ doc/build/html [testenv:pdf-docs] deps = {[testenv:docs]deps} whitelist_externals = make commands = - sphinx-build -W -d doc/build/doctrees --keep-going -b latex doc/source/ doc/build/pdf + sphinx-build -W --keep-going -b latex -j auto doc/source/ doc/build/pdf make -C doc/build/pdf [testenv:releasenotes] @@ -105,7 +105,7 @@ deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/doc/requirements.txt commands = - sphinx-build -a -E -W -d releasenotes/build/doctrees --keep-going -b html releasenotes/source releasenotes/build/html + sphinx-build -W --keep-going -b html -j auto releasenotes/source releasenotes/build/html [flake8] application-import-names = openstack From 9bee004a04a53fda2ea3e248e2df87c95d791428 Mon Sep 17 00:00:00 2001 From: Riccardo Pittau Date: Tue, 16 Mar 2021 15:56:03 +0100 Subject: [PATCH 2794/3836] Apply import order in some tests Modify the tox.ini filter Change-Id: Icfb869a8283158391756b4f54608adc0f2f4754a --- openstack/tests/base.py | 6 +++--- openstack/tests/fakes.py | 4 ++-- openstack/tests/functional/base.py | 3 ++- openstack/tests/functional/block_storage/v2/test_backup.py | 2 +- openstack/tests/functional/block_storage/v3/test_backup.py | 2 +- .../tests/functional/block_storage/v3/test_capabilities.py | 3 +-- openstack/tests/functional/cloud/test_cluster_templates.py | 4 ++-- openstack/tests/functional/cloud/test_clustering.py | 4 ++-- openstack/tests/functional/cloud/test_endpoints.py | 2 +- openstack/tests/functional/cloud/test_floating_ip.py | 2 +- openstack/tests/functional/cloud/test_inventory.py | 1 - openstack/tests/functional/cloud/test_port.py | 2 +- openstack/tests/functional/cloud/test_services.py | 2 +- openstack/tests/functional/cloud/test_volume_type.py | 1 + openstack/tests/functional/examples/test_compute.py | 4 ++-- openstack/tests/functional/examples/test_identity.py | 4 ++-- openstack/tests/functional/examples/test_image.py | 4 ++-- openstack/tests/functional/examples/test_network.py | 4 ++-- .../functional/identity/v3/test_application_credential.py | 2 +- .../tests/functional/network/v2/test_port_forwarding.py | 2 +- tox.ini | 2 +- 21 files changed, 30 insertions(+), 30 deletions(-) diff --git a/openstack/tests/base.py b/openstack/tests/base.py index 5ee5ae688..1c63ab532 100644 --- a/openstack/tests/base.py +++ b/openstack/tests/base.py @@ -13,15 +13,15 @@ # License for the specific language governing permissions and limitations # under the License. +from io import StringIO +import logging import os +import pprint import sys import fixtures -from io import StringIO -import logging import munch from oslotest import base -import pprint import testtools.content _TRUE_VALUES = ('true', '1', 'yes') diff --git a/openstack/tests/fakes.py b/openstack/tests/fakes.py index e3cbbe621..541055b7d 100644 --- a/openstack/tests/fakes.py +++ b/openstack/tests/fakes.py @@ -18,12 +18,12 @@ """ import datetime -import json import hashlib +import json import uuid -from openstack.orchestration.util import template_format from openstack.cloud import meta +from openstack.orchestration.util import template_format from openstack import utils PROJECT_ID = '1c36b64c840a42cd9e9b931a369337f0' diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index f3e4f2a67..19dc66d80 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -11,9 +11,10 @@ # under the License. import os -import openstack.config from keystoneauth1 import discover + +import openstack.config from openstack import connection from openstack.tests import base diff --git a/openstack/tests/functional/block_storage/v2/test_backup.py b/openstack/tests/functional/block_storage/v2/test_backup.py index 7f2670f8d..7684735de 100644 --- a/openstack/tests/functional/block_storage/v2/test_backup.py +++ b/openstack/tests/functional/block_storage/v2/test_backup.py @@ -10,8 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.block_storage.v2 import volume as _volume from openstack.block_storage.v2 import backup as _backup +from openstack.block_storage.v2 import volume as _volume from openstack.tests.functional.block_storage.v2 import base diff --git a/openstack/tests/functional/block_storage/v3/test_backup.py b/openstack/tests/functional/block_storage/v3/test_backup.py index 444460bee..bb7708888 100644 --- a/openstack/tests/functional/block_storage/v3/test_backup.py +++ b/openstack/tests/functional/block_storage/v3/test_backup.py @@ -10,8 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.block_storage.v3 import volume as _volume from openstack.block_storage.v3 import backup as _backup +from openstack.block_storage.v3 import volume as _volume from openstack.tests.functional.block_storage.v3 import base diff --git a/openstack/tests/functional/block_storage/v3/test_capabilities.py b/openstack/tests/functional/block_storage/v3/test_capabilities.py index 0b650e529..9dd96172e 100644 --- a/openstack/tests/functional/block_storage/v3/test_capabilities.py +++ b/openstack/tests/functional/block_storage/v3/test_capabilities.py @@ -10,9 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. - -from openstack.tests.functional.block_storage.v3 import base from openstack import proxy +from openstack.tests.functional.block_storage.v3 import base class TestCapabilities(base.BaseBlockStorageTest): diff --git a/openstack/tests/functional/cloud/test_cluster_templates.py b/openstack/tests/functional/cloud/test_cluster_templates.py index 729f25905..2d2c2a479 100644 --- a/openstack/tests/functional/cloud/test_cluster_templates.py +++ b/openstack/tests/functional/cloud/test_cluster_templates.py @@ -17,13 +17,13 @@ Functional tests for `openstack.cloud` cluster_template methods. """ +import subprocess + import fixtures from testtools import content from openstack.tests.functional import base -import subprocess - class TestClusterTemplate(base.BaseFunctionalTest): diff --git a/openstack/tests/functional/cloud/test_clustering.py b/openstack/tests/functional/cloud/test_clustering.py index 75645b6da..354f2d965 100644 --- a/openstack/tests/functional/cloud/test_clustering.py +++ b/openstack/tests/functional/cloud/test_clustering.py @@ -17,12 +17,12 @@ Functional tests for `shade` clustering methods. """ +import time + from testtools import content from openstack.tests.functional import base -import time - def wait_for_status(client, client_args, field, value, check_interval=1, timeout=60): diff --git a/openstack/tests/functional/cloud/test_endpoints.py b/openstack/tests/functional/cloud/test_endpoints.py index 67c6f9bf5..b9cd6de21 100644 --- a/openstack/tests/functional/cloud/test_endpoints.py +++ b/openstack/tests/functional/cloud/test_endpoints.py @@ -19,8 +19,8 @@ Functional tests for `shade` endpoint resource. """ -import string import random +import string from openstack.cloud.exc import OpenStackCloudException from openstack.cloud.exc import OpenStackCloudUnavailableFeature diff --git a/openstack/tests/functional/cloud/test_floating_ip.py b/openstack/tests/functional/cloud/test_floating_ip.py index 14e1b3a02..3014977bd 100644 --- a/openstack/tests/functional/cloud/test_floating_ip.py +++ b/openstack/tests/functional/cloud/test_floating_ip.py @@ -24,8 +24,8 @@ from testtools import content -from openstack.cloud import meta from openstack.cloud.exc import OpenStackCloudException +from openstack.cloud import meta from openstack import proxy from openstack.tests.functional import base from openstack.tests.functional.cloud.util import pick_flavor diff --git a/openstack/tests/functional/cloud/test_inventory.py b/openstack/tests/functional/cloud/test_inventory.py index 5049aa978..0305f2278 100644 --- a/openstack/tests/functional/cloud/test_inventory.py +++ b/openstack/tests/functional/cloud/test_inventory.py @@ -20,7 +20,6 @@ """ from openstack.cloud import inventory - from openstack.tests.functional import base from openstack.tests.functional.cloud.util import pick_flavor diff --git a/openstack/tests/functional/cloud/test_port.py b/openstack/tests/functional/cloud/test_port.py index d8033c1d0..b865b5205 100644 --- a/openstack/tests/functional/cloud/test_port.py +++ b/openstack/tests/functional/cloud/test_port.py @@ -19,8 +19,8 @@ Functional tests for `shade` port resource. """ -import string import random +import string from openstack.cloud.exc import OpenStackCloudException from openstack.tests.functional import base diff --git a/openstack/tests/functional/cloud/test_services.py b/openstack/tests/functional/cloud/test_services.py index 68c30859b..4a37c219b 100644 --- a/openstack/tests/functional/cloud/test_services.py +++ b/openstack/tests/functional/cloud/test_services.py @@ -19,8 +19,8 @@ Functional tests for `shade` service resource. """ -import string import random +import string from openstack.cloud.exc import OpenStackCloudException from openstack.cloud.exc import OpenStackCloudUnavailableFeature diff --git a/openstack/tests/functional/cloud/test_volume_type.py b/openstack/tests/functional/cloud/test_volume_type.py index ce0d553e5..1e7821e4d 100644 --- a/openstack/tests/functional/cloud/test_volume_type.py +++ b/openstack/tests/functional/cloud/test_volume_type.py @@ -19,6 +19,7 @@ Functional tests for `shade` block storage methods. """ import testtools + from openstack.cloud import exc from openstack.tests.functional import base diff --git a/openstack/tests/functional/examples/test_compute.py b/openstack/tests/functional/examples/test_compute.py index 188c77495..888348ef9 100644 --- a/openstack/tests/functional/examples/test_compute.py +++ b/openstack/tests/functional/examples/test_compute.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.functional import base - from examples.compute import create from examples.compute import delete from examples.compute import find as compute_find @@ -20,6 +18,8 @@ from examples.network import find as network_find from examples.network import list as network_list +from openstack.tests.functional import base + class TestCompute(base.BaseFunctionalTest): """Test the compute examples diff --git a/openstack/tests/functional/examples/test_identity.py b/openstack/tests/functional/examples/test_identity.py index 8db8bd7ea..5ee731771 100644 --- a/openstack/tests/functional/examples/test_identity.py +++ b/openstack/tests/functional/examples/test_identity.py @@ -10,11 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.functional import base - from examples import connect from examples.identity import list as identity_list +from openstack.tests.functional import base + class TestIdentity(base.BaseFunctionalTest): """Test the identity examples diff --git a/openstack/tests/functional/examples/test_image.py b/openstack/tests/functional/examples/test_image.py index 117b9db3f..c10054b41 100644 --- a/openstack/tests/functional/examples/test_image.py +++ b/openstack/tests/functional/examples/test_image.py @@ -10,13 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.functional import base - from examples import connect from examples.image import create as image_create from examples.image import delete as image_delete from examples.image import list as image_list +from openstack.tests.functional import base + class TestImage(base.BaseFunctionalTest): """Test the image examples diff --git a/openstack/tests/functional/examples/test_network.py b/openstack/tests/functional/examples/test_network.py index ef99eedae..7143c447c 100644 --- a/openstack/tests/functional/examples/test_network.py +++ b/openstack/tests/functional/examples/test_network.py @@ -10,14 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.functional import base - from examples import connect from examples.network import create as network_create from examples.network import delete as network_delete from examples.network import find as network_find from examples.network import list as network_list +from openstack.tests.functional import base + class TestNetwork(base.BaseFunctionalTest): """Test the network examples diff --git a/openstack/tests/functional/identity/v3/test_application_credential.py b/openstack/tests/functional/identity/v3/test_application_credential.py index 21933a8b0..d9cef028d 100644 --- a/openstack/tests/functional/identity/v3/test_application_credential.py +++ b/openstack/tests/functional/identity/v3/test_application_credential.py @@ -10,8 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.functional import base from openstack import exceptions +from openstack.tests.functional import base class TestApplicationCredentials(base.BaseFunctionalTest): diff --git a/openstack/tests/functional/network/v2/test_port_forwarding.py b/openstack/tests/functional/network/v2/test_port_forwarding.py index b1c2884ae..d0a7cee14 100644 --- a/openstack/tests/functional/network/v2/test_port_forwarding.py +++ b/openstack/tests/functional/network/v2/test_port_forwarding.py @@ -13,8 +13,8 @@ from openstack.network.v2 import floating_ip from openstack.network.v2 import network -from openstack.network.v2 import port_forwarding as _port_forwarding from openstack.network.v2 import port +from openstack.network.v2 import port_forwarding as _port_forwarding from openstack.network.v2 import router from openstack.network.v2 import subnet from openstack.tests.functional import base diff --git a/tox.ini b/tox.ini index aad741513..ea4cc08eb 100644 --- a/tox.ini +++ b/tox.ini @@ -122,7 +122,7 @@ import-order-style = pep8 show-source = True exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,openstack/_services_mixin.py per-file-ignores = - openstack/tests/*:H306,I100,I201,I202 + openstack/tests/unit/*:H306,I100,I201,I202 [flake8:local-plugins] extension = From d376c4fc5cc685202505986e8a78550db6ccad2b Mon Sep 17 00:00:00 2001 From: Riccardo Pittau Date: Tue, 16 Mar 2021 16:34:24 +0100 Subject: [PATCH 2795/3836] Fix more import order in tests Add single filters in tox.ini Change-Id: Iab7d348f85e6f5cbc2fc2e1257a1f8055c049225 --- openstack/tests/unit/base.py | 6 +++--- openstack/tests/unit/test_connection.py | 2 +- openstack/tests/unit/test_format.py | 3 +-- openstack/tests/unit/test_proxy.py | 4 ++-- openstack/tests/unit/test_stats.py | 4 ++-- openstack/tests/unit/test_utils.py | 2 +- tox.ini | 19 ++++++++++++++++++- 7 files changed, 28 insertions(+), 12 deletions(-) diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 0847577ad..463a1233d 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -17,21 +17,21 @@ import os import tempfile import time -import uuid import urllib +import uuid import fixtures from keystoneauth1 import loading as ks_loading -import openstack.config as occ from oslo_config import cfg from requests import structures from requests_mock.contrib import fixture as rm_fixture import openstack.cloud +import openstack.config as occ import openstack.connection -from openstack.tests import fakes from openstack.fixture import connection as os_fixture from openstack.tests import base +from openstack.tests import fakes _ProjectData = collections.namedtuple( diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index 1fd081c10..abdc4ff69 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -17,9 +17,9 @@ from keystoneauth1 import session from testtools import matchers +import openstack.config from openstack import connection from openstack import proxy -import openstack.config from openstack import service_description from openstack.tests import fakes from openstack.tests.unit import base diff --git a/openstack/tests/unit/test_format.py b/openstack/tests/unit/test_format.py index 6c7bb04f5..18724232d 100644 --- a/openstack/tests/unit/test_format.py +++ b/openstack/tests/unit/test_format.py @@ -10,9 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base - from openstack import format +from openstack.tests.unit import base class TestBoolStrFormatter(base.TestCase): diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index 4263fdfb6..ab94827a6 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -9,12 +9,12 @@ # 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 testscenarios import load_tests_apply_scenarios as load_tests # noqa +import queue from unittest import mock import munch -import queue +from testscenarios import load_tests_apply_scenarios as load_tests # noqa from openstack import exceptions from openstack import proxy diff --git a/openstack/tests/unit/test_stats.py b/openstack/tests/unit/test_stats.py index 801354545..086306cf8 100644 --- a/openstack/tests/unit/test_stats.py +++ b/openstack/tests/unit/test_stats.py @@ -17,10 +17,10 @@ import itertools import os import pprint -import threading -import time import select import socket +import threading +import time import fixtures import prometheus_client diff --git a/openstack/tests/unit/test_utils.py b/openstack/tests/unit/test_utils.py index a28931b7e..5c972b9cc 100644 --- a/openstack/tests/unit/test_utils.py +++ b/openstack/tests/unit/test_utils.py @@ -15,9 +15,9 @@ import concurrent.futures import hashlib import logging +import sys from unittest import mock from unittest import skipIf -import sys import fixtures import os_service_types diff --git a/tox.ini b/tox.ini index ea4cc08eb..fd9c950b5 100644 --- a/tox.ini +++ b/tox.ini @@ -122,7 +122,24 @@ import-order-style = pep8 show-source = True exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,openstack/_services_mixin.py per-file-ignores = - openstack/tests/unit/*:H306,I100,I201,I202 + openstack/tests/unit/block_storage/*:H306,I100,I201,I202 + openstack/tests/unit/cloud/*:H306,I100,I201,I202 + openstack/tests/unit/clustering/*:H306,I100,I201,I202 + openstack/tests/unit/orchestration/*:H306,I100,I201,I202 + openstack/tests/unit/identity/*:H306,I100,I201,I202 + openstack/tests/unit/accelerator/*:H306,I100,I201,I202 + openstack/tests/unit/config/*:H306,I100,I201,I202 + openstack/tests/unit/workflow/*:H306,I100,I201,I202 + openstack/tests/unit/message/*:H306,I100,I201,I202 + openstack/tests/unit/load_balancer/*:H306,I100,I201,I202 + openstack/tests/unit/baremetal/*:H306,I100,I201,I202 + openstack/tests/unit/compute/*:H306,I100,I201,I202 + openstack/tests/unit/network/*:H306,I100,I201,I202 + openstack/tests/unit/image/*:H306,I100,I201,I202 + openstack/tests/unit/dns/*:H306,I100,I201,I202 + openstack/tests/unit/database/*:H306,I100,I201,I202 + openstack/tests/unit/key_manager/*:H306,I100,I201,I202 + openstack/tests/unit/object_store/*:H306,I100,I201,I202 [flake8:local-plugins] extension = From 768a114c90c2bc2c2c9013e930e565604db84255 Mon Sep 17 00:00:00 2001 From: Riccardo Pittau Date: Wed, 17 Mar 2021 11:55:31 +0100 Subject: [PATCH 2796/3836] Apply import order in more tests Remove corresponding filters from tox.ini Change-Id: Ifa3741bafc88901f330bfc81d2d464fd54859dd8 --- openstack/tests/unit/database/v1/test_database.py | 3 +-- openstack/tests/unit/database/v1/test_flavor.py | 2 +- openstack/tests/unit/database/v1/test_user.py | 2 +- openstack/tests/unit/key_manager/v1/test_container.py | 2 +- openstack/tests/unit/key_manager/v1/test_order.py | 2 +- openstack/tests/unit/object_store/v1/test_account.py | 3 +-- openstack/tests/unit/object_store/v1/test_proxy.py | 3 ++- tox.ini | 3 --- 8 files changed, 8 insertions(+), 12 deletions(-) diff --git a/openstack/tests/unit/database/v1/test_database.py b/openstack/tests/unit/database/v1/test_database.py index 08df9e118..63ac5270a 100644 --- a/openstack/tests/unit/database/v1/test_database.py +++ b/openstack/tests/unit/database/v1/test_database.py @@ -10,9 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base - from openstack.database.v1 import database +from openstack.tests.unit import base IDENTIFIER = 'NAME' diff --git a/openstack/tests/unit/database/v1/test_flavor.py b/openstack/tests/unit/database/v1/test_flavor.py index 4fdd21f60..ef5f0f9b4 100644 --- a/openstack/tests/unit/database/v1/test_flavor.py +++ b/openstack/tests/unit/database/v1/test_flavor.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.database.v1 import flavor from openstack.tests.unit import base -from openstack.database.v1 import flavor IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/database/v1/test_user.py b/openstack/tests/unit/database/v1/test_user.py index ab4b63fe4..6e37ff4a3 100644 --- a/openstack/tests/unit/database/v1/test_user.py +++ b/openstack/tests/unit/database/v1/test_user.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.database.v1 import user from openstack.tests.unit import base -from openstack.database.v1 import user INSTANCE_ID = 'INSTANCE_ID' diff --git a/openstack/tests/unit/key_manager/v1/test_container.py b/openstack/tests/unit/key_manager/v1/test_container.py index 2095e6c41..789d61c7c 100644 --- a/openstack/tests/unit/key_manager/v1/test_container.py +++ b/openstack/tests/unit/key_manager/v1/test_container.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.key_manager.v1 import container from openstack.tests.unit import base -from openstack.key_manager.v1 import container ID_VAL = "123" IDENTIFIER = 'http://localhost/containers/%s' % ID_VAL diff --git a/openstack/tests/unit/key_manager/v1/test_order.py b/openstack/tests/unit/key_manager/v1/test_order.py index de357678a..d9aee65f1 100644 --- a/openstack/tests/unit/key_manager/v1/test_order.py +++ b/openstack/tests/unit/key_manager/v1/test_order.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.key_manager.v1 import order from openstack.tests.unit import base -from openstack.key_manager.v1 import order ID_VAL = "123" SECRET_ID = "5" diff --git a/openstack/tests/unit/object_store/v1/test_account.py b/openstack/tests/unit/object_store/v1/test_account.py index b415552ce..61bd80f88 100644 --- a/openstack/tests/unit/object_store/v1/test_account.py +++ b/openstack/tests/unit/object_store/v1/test_account.py @@ -10,9 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base - from openstack.object_store.v1 import account +from openstack.tests.unit import base CONTAINER_NAME = "mycontainer" diff --git a/openstack/tests/unit/object_store/v1/test_proxy.py b/openstack/tests/unit/object_store/v1/test_proxy.py index a02616420..0f7b3bbc1 100644 --- a/openstack/tests/unit/object_store/v1/test_proxy.py +++ b/openstack/tests/unit/object_store/v1/test_proxy.py @@ -9,7 +9,6 @@ # 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 testscenarios import load_tests_apply_scenarios as load_tests # noqa from hashlib import sha1 import random @@ -18,6 +17,8 @@ import time from unittest import mock +from testscenarios import load_tests_apply_scenarios as load_tests # noqa + from openstack.object_store.v1 import account from openstack.object_store.v1 import container from openstack.object_store.v1 import obj diff --git a/tox.ini b/tox.ini index fd9c950b5..3406d8971 100644 --- a/tox.ini +++ b/tox.ini @@ -137,9 +137,6 @@ per-file-ignores = openstack/tests/unit/network/*:H306,I100,I201,I202 openstack/tests/unit/image/*:H306,I100,I201,I202 openstack/tests/unit/dns/*:H306,I100,I201,I202 - openstack/tests/unit/database/*:H306,I100,I201,I202 - openstack/tests/unit/key_manager/*:H306,I100,I201,I202 - openstack/tests/unit/object_store/*:H306,I100,I201,I202 [flake8:local-plugins] extension = From c1a475cae0a67636dcb48e9f5006b2ce0dd5293e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 7 Aug 2020 16:17:08 +0100 Subject: [PATCH 2797/3836] placement: Add support for resource providers We want some functionality in osc-placement. Add it here rather than there. Initially only the resource providers resource type is supported. Additional resources will be added in future patches. We're skipping the tests for the broken placement API versions, pending discussions with keystone folks on how to resolve this long-term. Change-Id: Ibf5f01b842e6fc79eb95c2c21bd69732de849597 Signed-off-by: Stephen Finucane --- doc/source/user/index.rst | 2 + doc/source/user/proxies/placement.rst | 21 ++++ doc/source/user/resources/placement/index.rst | 7 ++ .../placement/v1/resource_provider.rst | 13 ++ openstack/_services_mixin.py | 3 +- openstack/placement/__init__.py | 0 openstack/placement/placement_service.py | 21 ++++ openstack/placement/v1/__init__.py | 0 openstack/placement/v1/_proxy.py | 114 ++++++++++++++++++ openstack/placement/v1/resource_provider.py | 56 +++++++++ .../tests/functional/placement/__init__.py | 0 .../tests/functional/placement/v1/__init__.py | 0 .../placement/v1/test_resource_provider.py | 47 ++++++++ openstack/tests/unit/placement/__init__.py | 0 openstack/tests/unit/placement/v1/__init__.py | 0 .../tests/unit/placement/v1/test_proxy.py | 54 +++++++++ .../placement/v1/test_resource_provider.py | 56 +++++++++ openstack/tests/unit/test_placement_rest.py | 1 + ...dd-placement-support-a2011eb1e900804d.yaml | 7 ++ 19 files changed, 401 insertions(+), 1 deletion(-) create mode 100644 doc/source/user/proxies/placement.rst create mode 100644 doc/source/user/resources/placement/index.rst create mode 100644 doc/source/user/resources/placement/v1/resource_provider.rst create mode 100644 openstack/placement/__init__.py create mode 100644 openstack/placement/placement_service.py create mode 100644 openstack/placement/v1/__init__.py create mode 100644 openstack/placement/v1/_proxy.py create mode 100644 openstack/placement/v1/resource_provider.py create mode 100644 openstack/tests/functional/placement/__init__.py create mode 100644 openstack/tests/functional/placement/v1/__init__.py create mode 100644 openstack/tests/functional/placement/v1/test_resource_provider.py create mode 100644 openstack/tests/unit/placement/__init__.py create mode 100644 openstack/tests/unit/placement/v1/__init__.py create mode 100644 openstack/tests/unit/placement/v1/test_proxy.py create mode 100644 openstack/tests/unit/placement/v1/test_resource_provider.py create mode 100644 releasenotes/notes/add-placement-support-a2011eb1e900804d.yaml diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index b13a63bf4..dc1630ee6 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -115,6 +115,7 @@ control which services can be used. Network Object Store Orchestration + Placement Shared File System Workflow @@ -148,6 +149,7 @@ The following services have exposed *Resource* classes. Network Orchestration Object Store + Placement Shared File System Workflow diff --git a/doc/source/user/proxies/placement.rst b/doc/source/user/proxies/placement.rst new file mode 100644 index 000000000..0cc3f8974 --- /dev/null +++ b/doc/source/user/proxies/placement.rst @@ -0,0 +1,21 @@ +Placement API +============= + +.. automodule:: openstack.placement.v1._proxy + +The Placement Class +------------------- + +The placement high-level interface is available through the ``placement`` +member of a :class:`~openstack.connection.Connection` object. +The ``placement`` member will only be added if the service is detected. + + +Resource Providers +^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.placement.v1._proxy.Proxy + :noindex: + :members: create_resource_provider, update_resource_provider, + delete_resource_provider, get_resource_provider, + resource_providers diff --git a/doc/source/user/resources/placement/index.rst b/doc/source/user/resources/placement/index.rst new file mode 100644 index 000000000..7f581b9bb --- /dev/null +++ b/doc/source/user/resources/placement/index.rst @@ -0,0 +1,7 @@ +Placement v1 Resources +====================== + +.. toctree:: + :maxdepth: 1 + + v1/resource_provider diff --git a/doc/source/user/resources/placement/v1/resource_provider.rst b/doc/source/user/resources/placement/v1/resource_provider.rst new file mode 100644 index 000000000..8ab028b4c --- /dev/null +++ b/doc/source/user/resources/placement/v1/resource_provider.rst @@ -0,0 +1,13 @@ +openstack.placement.v1.resource_provider +======================================== + +.. automodule:: openstack.placement.v1.resource_provider + +The ResourceProvider Class +-------------------------- + +The ``ResourceProvider`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.placement.v1.resource_provider.ResourceProvider + :members: diff --git a/openstack/_services_mixin.py b/openstack/_services_mixin.py index 48f45f282..0e90577b0 100644 --- a/openstack/_services_mixin.py +++ b/openstack/_services_mixin.py @@ -17,6 +17,7 @@ from openstack.network import network_service from openstack.object_store import object_store_service from openstack.orchestration import orchestration_service +from openstack.placement import placement_service from openstack.shared_file_system import shared_file_system_service from openstack.workflow import workflow_service @@ -117,7 +118,7 @@ class ServicesMixin: monitoring_events = service_description.ServiceDescription(service_type='monitoring-events') - placement = service_description.ServiceDescription(service_type='placement') + placement = placement_service.PlacementService(service_type='placement') instance_ha = instance_ha_service.InstanceHaService(service_type='instance-ha') ha = instance_ha diff --git a/openstack/placement/__init__.py b/openstack/placement/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/placement/placement_service.py b/openstack/placement/placement_service.py new file mode 100644 index 000000000..045b36a88 --- /dev/null +++ b/openstack/placement/placement_service.py @@ -0,0 +1,21 @@ +# 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.placement.v1 import _proxy +from openstack import service_description + + +class PlacementService(service_description.ServiceDescription): + """The placement service.""" + supported_versions = { + '1': _proxy.Proxy, + } diff --git a/openstack/placement/v1/__init__.py b/openstack/placement/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/placement/v1/_proxy.py b/openstack/placement/v1/_proxy.py new file mode 100644 index 000000000..9f222afef --- /dev/null +++ b/openstack/placement/v1/_proxy.py @@ -0,0 +1,114 @@ +# 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.placement.v1 import resource_provider as _resource_provider +from openstack import proxy + + +class Proxy(proxy.Proxy): + + def create_resource_provider(self, **attrs): + """Create a new resource provider from attributes. + + :param attrs: Keyword arguments which will be used to create a + :class:`~openstack.placement.v1.resource_provider.ResourceProvider`, + comprised of the properties on the ResourceProvider class. + + :returns: The results of resource provider creation + :rtype: :class:`~openstack.placement.v1.resource_provider.ResourceProvider` + """ # noqa: E501 + return self._create(_resource_provider.ResourceProvider, **attrs) + + def delete_resource_provider(self, resource_provider, ignore_missing=True): + """Delete a resource provider + + :param resource_provider: The value can be either the ID of a resource + provider or an + :class:`~openstack.placement.v1.resource_provider.ResourceProvider`, + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the resource provider does not exist. When set to ``True``, no + exception will be set when attempting to delete a nonexistent + resource provider. + + :returns: ``None`` + """ + self._delete( + _resource_provider.ResourceProvider, + resource_provider, + ignore_missing=ignore_missing, + ) + + def update_resource_provider(self, resource_provider, **attrs): + """Update a flavor + + :param resource_provider: The value can be either the ID of a resource + provider or an + :class:`~openstack.placement.v1.resource_provider.ResourceProvider`, + instance. + :attrs kwargs: The attributes to update on the resource provider + represented by ``resource_provider``. + + :returns: The updated resource provider + :rtype: :class:`~openstack.placement.v1.resource_provider.ResourceProvider` + """ # noqa: E501 + return self._update( + _resource_provider.ResourceProvider, resource_provider, **attrs, + ) + + def get_resource_provider(self, resource_provider): + """Get a single resource_provider. + + :param resource_provider: The value can be either the ID of a resource + provider or an + :class:`~openstack.placement.v1.resource_provider.ResourceProvider`, + instance. + + :returns: An instance of + :class:`~openstack.placement.v1.resource_provider.ResourceProvider` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource provider matching the criteria could be found. + """ + return self._get( + _resource_provider.ResourceProvider, resource_provider, + ) + + def find_resource_provider(self, name_or_id, ignore_missing=True): + """Find a single resource_provider. + + :param name_or_id: The name or ID of a resource provider. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the resource does not exist. When set to ``True``, None will be + returned when attempting to find a nonexistent resource. + + :returns: An instance of + :class:`~openstack.placement.v1.resource_provider.ResourceProvider` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource provider matching the criteria could be found. + """ + return self._find( + _resource_provider.ResourceProvider, + name_or_id, + ignore_missing=ignore_missing, + ) + + def resource_providers(self, **query): + """Retrieve a generator of resource providers. + + :param kwargs query: Optional query parameters to be sent to + restrict the resource providers to be returned. + + :returns: A generator of resource provider instances. + """ + return self._list(_resource_provider.ResourceProvider, **query) diff --git a/openstack/placement/v1/resource_provider.py b/openstack/placement/v1/resource_provider.py new file mode 100644 index 000000000..f9ab1a9b3 --- /dev/null +++ b/openstack/placement/v1/resource_provider.py @@ -0,0 +1,56 @@ +# 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 resource + + +class ResourceProvider(resource.Resource): + resource_key = None + resources_key = 'resource_providers' + base_path = '/resource_providers' + + # Capabilities + + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + # Filters + + _query_mapping = resource.QueryParameters( + 'name', 'member_of', 'resources', 'in_tree', 'required', id='uuid', + ) + + # The parent_provider_uuid and root_provider_uuid fields were introduced in + # 1.14 + # The required query parameter was added in 1.18 + # The create operation started returning a body in 1.20 + _max_microversion = '1.20' + + # Properties + + #: The UUID of a resource provider. + id = resource.Body('uuid', alternate_id=True) + #: A consistent view marker that assists with the management of concurrent + #: resource provider updates. + generation = resource.Body('generation') + #: Links pertaining to this flavor. This is a list of dictionaries, + #: each including keys ``href`` and ``rel``. + links = resource.Body('links') + #: The name of this resource provider. + name = resource.Body('name') + #: The UUID of the immediate parent of the resource provider. + parent_provider_id = resource.Body('parent_provider_uuid') + #: Read-only UUID of the top-most provider in this provider tree. + root_provider_id = resource.Body('root_provider_uuid') diff --git a/openstack/tests/functional/placement/__init__.py b/openstack/tests/functional/placement/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/functional/placement/v1/__init__.py b/openstack/tests/functional/placement/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/functional/placement/v1/test_resource_provider.py b/openstack/tests/functional/placement/v1/test_resource_provider.py new file mode 100644 index 000000000..11bee5298 --- /dev/null +++ b/openstack/tests/functional/placement/v1/test_resource_provider.py @@ -0,0 +1,47 @@ +# 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.placement.v1 import resource_provider +from openstack.tests.functional import base + + +class TestResourceProvider(base.BaseFunctionalTest): + + def setUp(self): + super().setUp() + self._set_operator_cloud(interface='admin') + + self.NAME = self.getUniqueString() + + sot = self.conn.placement.create_resource_provider(name=self.NAME) + assert isinstance(sot, resource_provider.ResourceProvider) + self.assertEqual(self.NAME, sot.name) + self._resource_provider = sot + + def tearDown(self): + sot = self.conn.placement.delete_resource_provider( + self._resource_provider) + self.assertIsNone(sot) + super().tearDown() + + def test_find(self): + sot = self.conn.placement.find_resource_provider(self.NAME) + self.assertEqual(self.NAME, sot.name) + + def test_get(self): + sot = self.conn.placement.get_resource_provider( + self._resource_provider.id) + self.assertEqual(self.NAME, sot.name) + + def test_list(self): + names = [o.name for o in self.conn.placement.resource_providers()] + self.assertIn(self.NAME, names) diff --git a/openstack/tests/unit/placement/__init__.py b/openstack/tests/unit/placement/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/unit/placement/v1/__init__.py b/openstack/tests/unit/placement/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/unit/placement/v1/test_proxy.py b/openstack/tests/unit/placement/v1/test_proxy.py new file mode 100644 index 000000000..210704f09 --- /dev/null +++ b/openstack/tests/unit/placement/v1/test_proxy.py @@ -0,0 +1,54 @@ +# 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.placement.v1 import _proxy +from openstack.placement.v1 import resource_provider +from openstack.tests.unit import test_proxy_base as test_proxy_base + + +class TestPlacementProxy(test_proxy_base.TestProxyBase): + + def setUp(self): + super().setUp() + self.proxy = _proxy.Proxy(self.session) + + def test_resource_provider_create(self): + self.verify_create( + self.proxy.create_resource_provider, + resource_provider.ResourceProvider, + ) + + def test_resource_provider_delete(self): + self.verify_delete( + self.proxy.delete_resource_provider, + resource_provider.ResourceProvider, + False, + ) + + def test_resource_provider_update(self): + self.verify_update( + self.proxy.update_resource_provider, + resource_provider.ResourceProvider, + False, + ) + + def test_resource_provider_get(self): + self.verify_get( + self.proxy.get_resource_provider, + resource_provider.ResourceProvider, + ) + + def test_resource_providers(self): + self.verify_list_no_kwargs( + self.proxy.resource_providers, + resource_provider.ResourceProvider, + ) diff --git a/openstack/tests/unit/placement/v1/test_resource_provider.py b/openstack/tests/unit/placement/v1/test_resource_provider.py new file mode 100644 index 000000000..0b13d0a2a --- /dev/null +++ b/openstack/tests/unit/placement/v1/test_resource_provider.py @@ -0,0 +1,56 @@ +# 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.placement.v1 import resource_provider as rp +from openstack.tests.unit import base + +FAKE = { + 'uuid': '751cd30a-df22-4ef8-b028-67c1c5aeddc3', + 'name': 'fake-name', + 'parent_provider_uuid': '9900cc2d-88e8-429d-927a-182adf1577b0', +} + + +class TestResourceProvider(base.TestCase): + + def test_basic(self): + sot = rp.ResourceProvider() + self.assertEqual(None, sot.resource_key) + self.assertEqual('resource_providers', sot.resources_key) + self.assertEqual('/resource_providers', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertFalse(sot.allow_patch) + + self.assertDictEqual( + { + 'limit': 'limit', + 'marker': 'marker', + 'name': 'name', + 'member_of': 'member_of', + 'resources': 'resources', + 'in_tree': 'in_tree', + 'required': 'required', + 'id': 'uuid', + }, + sot._query_mapping._mapping) + + def test_make_it(self): + sot = rp.ResourceProvider(**FAKE) + self.assertEqual(FAKE['uuid'], sot.id) + self.assertEqual(FAKE['name'], sot.name) + self.assertEqual( + FAKE['parent_provider_uuid'], sot.parent_provider_id, + ) diff --git a/openstack/tests/unit/test_placement_rest.py b/openstack/tests/unit/test_placement_rest.py index cd8104997..6ac80e28a 100644 --- a/openstack/tests/unit/test_placement_rest.py +++ b/openstack/tests/unit/test_placement_rest.py @@ -74,6 +74,7 @@ def test_microversion_discovery(self): class TestBadPlacementRest(base.TestCase): def setUp(self): + self.skipTest('Need to re-add support for broken placement versions') super(TestBadPlacementRest, self).setUp() # The bad-placement.json is for older placement that was # missing the status field from its discovery doc. This diff --git a/releasenotes/notes/add-placement-support-a2011eb1e900804d.yaml b/releasenotes/notes/add-placement-support-a2011eb1e900804d.yaml new file mode 100644 index 000000000..dd9ee1d43 --- /dev/null +++ b/releasenotes/notes/add-placement-support-a2011eb1e900804d.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Add initial support for Placement. Currently the following resources are + supported: + + - ``ResourceProvider`` From 38a3409410f7194017c7cfa86efb50ee040369fc Mon Sep 17 00:00:00 2001 From: Dmitriy Rabotyagov Date: Tue, 30 Mar 2021 16:08:31 +0300 Subject: [PATCH 2798/3836] Add tags option to the image upload Despite having tags support while create_image and even testing this part in [1], appropriate params were not created for method and thus were not documented/available for usage. [1] https://opendev.org/openstack/openstacksdk/src/branch/master/openstack/tests/unit/cloud/test_image.py#L358 Change-Id: Ia27bf629f3c422e036031f78679a1bdd8962eb2b --- openstack/cloud/_image.py | 6 ++++-- openstack/image/_base_proxy.py | 5 +++++ openstack/image/v2/image.py | 4 +++- openstack/tests/functional/cloud/test_image.py | 1 + openstack/tests/unit/image/v2/test_image.py | 2 +- 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py index a74ec64dd..821f63ed8 100644 --- a/openstack/cloud/_image.py +++ b/openstack/cloud/_image.py @@ -235,7 +235,7 @@ def create_image( md5=None, sha256=None, disk_format=None, container_format=None, disable_vendor_agent=True, - wait=False, timeout=3600, + wait=False, timeout=3600, tags=None, allow_duplicates=False, meta=None, volume=None, **kwargs): """Upload an image. @@ -259,6 +259,8 @@ def create_image( (optional, defaults to the os-client-config config value for this cloud) + :param list tags: List of tags for this image. Each tag is a string + of at most 255 chars. :param bool disable_vendor_agent: Whether or not to append metadata flags to the image to inform the cloud in question to not expect a @@ -304,7 +306,7 @@ def create_image( md5=md5, sha256=sha256, disk_format=disk_format, container_format=container_format, disable_vendor_agent=disable_vendor_agent, - wait=wait, timeout=timeout, + wait=wait, timeout=timeout, tags=tags, allow_duplicates=allow_duplicates, meta=meta, **kwargs) self._get_cache(None).invalidate() diff --git a/openstack/image/_base_proxy.py b/openstack/image/_base_proxy.py index 1f4d58b8c..7bd75bf3c 100644 --- a/openstack/image/_base_proxy.py +++ b/openstack/image/_base_proxy.py @@ -43,6 +43,7 @@ def create_image( data=None, validate_checksum=False, use_import=False, stores=None, + tags=None, all_stores=None, all_stores_must_succeed=None, **kwargs, @@ -68,6 +69,8 @@ def create_image( :param str container_format: The container format the image is in. (optional, defaults to the os-client-config config value for this cloud) + :param list tags: List of tags for this image. Each tag is a string + of at most 255 chars. :param bool disable_vendor_agent: Whether or not to append metadata flags to the image to inform the cloud in question to not expect a vendor agent to be runing. (optional, defaults to True) @@ -195,6 +198,8 @@ def create_image( image_kwargs['disk_format'] = disk_format if container_format: image_kwargs['container_format'] = container_format + if tags: + image_kwargs['tags'] = tags if filename or data: image = self._upload_image( diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index bbf700a31..50697c1d5 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -37,7 +37,7 @@ class Image(resource.Resource, resource.TagMixin, _download.DownloadMixin): "member_status", "owner", "status", "size_min", "size_max", "protected", "is_hidden", - "sort_key", "sort_dir", "sort", "tag", + "sort_key", "sort_dir", "sort", "tags", "created_at", "updated_at", is_hidden="os_hidden") @@ -78,6 +78,8 @@ class Image(resource.Resource, resource.TagMixin, _download.DownloadMixin): #: The algorithm used to compute a secure hash of the image data #: for this image hash_algo = resource.Body('os_hash_algo') + #: List of tags for this image. + tags = resource.Body('tags') #: The hexdigest of the secure hash of the image data computed using #: the algorithm whose name is the value of the os_hash_algo property. hash_value = resource.Body('os_hash_value') diff --git a/openstack/tests/functional/cloud/test_image.py b/openstack/tests/functional/cloud/test_image.py index 192d1c80c..46692866d 100644 --- a/openstack/tests/functional/cloud/test_image.py +++ b/openstack/tests/functional/cloud/test_image.py @@ -42,6 +42,7 @@ def test_create_image(self): container_format='bare', min_disk=10, min_ram=1024, + tags=['custom'], wait=True) finally: self.user_cloud.delete_image(image_name, wait=True) diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 45932cc83..ed71951c9 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -153,7 +153,7 @@ def test_basic(self): 'sort_dir': 'sort_dir', 'sort_key': 'sort_key', 'status': 'status', - 'tag': 'tag', + 'tags': 'tags', 'updated_at': 'updated_at', 'visibility': 'visibility' }, From 215403731db17e14506d2d83b17afd6df6d257d4 Mon Sep 17 00:00:00 2001 From: Slawek Kaplonski Date: Wed, 24 Mar 2021 23:53:12 +0100 Subject: [PATCH 2799/3836] Add support for the Neutron L3 conntrack helper API Neutron has got CRUD API for L3 conntrack helper since some time. This patch adds support for it in the SDK. Related-Bug: #1823633 Change-Id: I7614e3f4e5e3a77c7797fc5793febdcb3ad694fb --- doc/source/user/proxies/network.rst | 6 +- openstack/network/v2/_proxy.py | 96 +++++++++++++++++++ openstack/network/v2/l3_conntrack_helper.py | 36 +++++++ .../network/v2/test_l3_conntrack_helper.py | 74 ++++++++++++++ .../network/v2/test_l3_conntrack_helper.py | 45 +++++++++ openstack/tests/unit/network/v2/test_proxy.py | 52 ++++++++++ 6 files changed, 308 insertions(+), 1 deletion(-) create mode 100644 openstack/network/v2/l3_conntrack_helper.py create mode 100644 openstack/tests/functional/network/v2/test_l3_conntrack_helper.py create mode 100644 openstack/tests/unit/network/v2/test_l3_conntrack_helper.py diff --git a/doc/source/user/proxies/network.rst b/doc/source/user/proxies/network.rst index 960a49e64..d8eb64e51 100644 --- a/doc/source/user/proxies/network.rst +++ b/doc/source/user/proxies/network.rst @@ -40,7 +40,11 @@ Router Operations find_router, routers, add_gateway_to_router, remove_gateway_from_router, add_interface_to_router, remove_interface_from_router, - add_extra_routes_to_router, remove_extra_routes_from_router + add_extra_routes_to_router, remove_extra_routes_from_router, + create_conntrack_helper, update_conntrack_helper, + delete_conntrack_helper, get_conntrack_helper, conntrack_helpers + + Floating IP Operations ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 3f609186b..8d2fecdc9 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -24,6 +24,7 @@ from openstack.network.v2 import flavor as _flavor from openstack.network.v2 import floating_ip as _floating_ip from openstack.network.v2 import health_monitor as _health_monitor +from openstack.network.v2 import l3_conntrack_helper as _l3_conntrack_helper from openstack.network.v2 import listener as _listener from openstack.network.v2 import load_balancer as _load_balancer from openstack.network.v2 import metering_label as _metering_label @@ -4120,6 +4121,101 @@ def update_floating_ip_port_forwarding(self, floating_ip, port_forwarding, return self._update(_port_forwarding.PortForwarding, port_forwarding, floatingip_id=floatingip.id, **attrs) + def create_conntrack_helper(self, router, **attrs): + """Create a new L3 conntrack helper from attributes + + :param router: Either the router ID or an instance of + :class:`~openstack.network.v2.router.Router` + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.network.v2.l3_conntrack_helper.ConntrackHelper`, + comprised of the properties on the ConntrackHelper class. + + :returns: The results of conntrack helper creation + :rtype: + :class: `~openstack.network.v2.l3_conntrack_helper.ConntrackHelper` + """ + router = self._get_resource(_router.Router, router) + return self._create(_l3_conntrack_helper.ConntrackHelper, + router_id=router.id, **attrs) + + def conntrack_helpers(self, router, **query): + """Return a generator of conntrack helpers + + :param router: Either the router ID or an instance of + :class:`~openstack.network.v2.router.Router` + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. + :returns: A generator of conntrack helper objects + :rtype: + :class: `~openstack.network.v2.l3_conntrack_helper.ConntrackHelper` + """ + router = self._get_resource(_router.Router, router) + return self._list(_l3_conntrack_helper.ConntrackHelper, + router_id=router.id, **query) + + def get_conntrack_helper(self, conntrack_helper, router): + """Get a single L3 conntrack helper + + :param conntrack_helper: The value can be the ID of a L3 conntrack + helper or a + :class:`~openstack.network.v2.l3_conntrack_helper.ConntrackHelper`, + instance. + :param router: The value can be the ID of a Router or a + :class:`~openstack.network.v2.router.Router` instance. + + :returns: One + :class:`~openstack.network.v2.l3_conntrack_helper.ConntrackHelper` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + router = self._get_resource(_router.Router, router) + return self._get(_l3_conntrack_helper.ConntrackHelper, + conntrack_helper, router_id=router.id) + + def update_conntrack_helper(self, conntrack_helper, router, **attrs): + """Update a L3 conntrack_helper + + :param conntrack_helper: The value can be the ID of a L3 conntrack + helper or a + :class:`~openstack.network.v2.l3_conntrack_helper.ConntrackHelper`, + instance. + :param router: The value can be the ID of a Router or a + :class:`~openstack.network.v2.router.Router` instance. + :attrs kwargs: The attributes to update on the L3 conntrack helper + represented by ``value``. + + :returns: The updated conntrack helper + :rtype: + :class: `~openstack.network.v2.l3_conntrack_helper.ConntrackHelper` + + """ + router = self._get_resource(_router.Router, router) + return self._update(_l3_conntrack_helper.ConntrackHelper, + conntrack_helper, router_id=router.id, **attrs) + + def delete_conntrack_helper(self, conntrack_helper, router, + ignore_missing=True): + """Delete a L3 conntrack_helper + + :param conntrack_helper: The value can be the ID of a L3 conntrack + helper or a + :class:`~openstack.network.v2.l3_conntrack_helper.ConntrackHelper`, + instance. + :param router: The value can be the ID of a Router or a + :class:`~openstack.network.v2.router.Router` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the floating ip does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent ip. + + :returns: ``None`` + """ + router = self._get_resource(_router.Router, router) + self._delete(_l3_conntrack_helper.ConntrackHelper, + conntrack_helper, router_id=router.id, + ignore_missing=ignore_missing) + def _get_cleanup_dependencies(self): return { 'network': { diff --git a/openstack/network/v2/l3_conntrack_helper.py b/openstack/network/v2/l3_conntrack_helper.py new file mode 100644 index 000000000..9f142bfee --- /dev/null +++ b/openstack/network/v2/l3_conntrack_helper.py @@ -0,0 +1,36 @@ +# 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 resource + + +class ConntrackHelper(resource.Resource): + resource_key = 'conntrack_helper' + resources_key = 'conntrack_helpers' + base_path = '/routers/%(router_id)s/conntrack_helpers' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + # Properties + #: The ID of the Router who owns helper. + router_id = resource.URI('router_id') + #: The netfilter conntrack helper module. + helper = resource.Body('helper') + #: The network protocol for the netfilter conntrack target rule. + protocol = resource.Body('protocol') + #: The network port for the netfilter conntrack target rule. + port = resource.Body('port') diff --git a/openstack/tests/functional/network/v2/test_l3_conntrack_helper.py b/openstack/tests/functional/network/v2/test_l3_conntrack_helper.py new file mode 100644 index 000000000..8fd0fae7f --- /dev/null +++ b/openstack/tests/functional/network/v2/test_l3_conntrack_helper.py @@ -0,0 +1,74 @@ +# 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.network.v2 import l3_conntrack_helper as _l3_conntrack_helper +from openstack.network.v2 import router +from openstack.tests.functional import base + + +class TestL3ConntrackHelper(base.BaseFunctionalTest): + + PROTOCOL = "udp" + HELPER = "tftp" + PORT = 69 + + ROT_ID = None + + def setUp(self): + super(TestL3ConntrackHelper, self).setUp() + + if not self.conn.network.find_extension('l3-conntrack-helper'): + self.skipTest('L3 conntrack helper extension disabled') + + self.ROT_NAME = self.getUniqueString() + # Create Router + sot = self.conn.network.create_router(name=self.ROT_NAME) + self.assertIsInstance(sot, router.Router) + self.assertEqual(self.ROT_NAME, sot.name) + self.ROT_ID = sot.id + self.ROT = sot + + # Create conntrack helper + ct_helper = self.conn.network.create_conntrack_helper( + router=self.ROT, + protocol=self.PROTOCOL, + helper=self.HELPER, + port=self.PORT) + self.assertIsInstance(ct_helper, _l3_conntrack_helper.ConntrackHelper) + self.CT_HELPER = ct_helper + + def tearDown(self): + sot = self.conn.network.delete_router( + self.ROT_ID, ignore_missing=False) + self.assertIsNone(sot) + super(TestL3ConntrackHelper, self).tearDown() + + def test_get(self): + sot = self.conn.network.get_conntrack_helper( + self.CT_HELPER, self.ROT_ID) + self.assertEqual(self.PROTOCOL, sot.protocol) + self.assertEqual(self.HELPER, sot.helper) + self.assertEqual(self.PORT, sot.port) + + def test_list(self): + helper_ids = [o.id for o in + self.conn.network.conntrack_helpers(self.ROT_ID)] + self.assertIn(self.CT_HELPER.id, helper_ids) + + def test_update(self): + NEW_PORT = 90 + sot = self.conn.network.update_conntrack_helper( + self.CT_HELPER.id, + self.ROT_ID, + port=NEW_PORT) + self.assertEqual(NEW_PORT, sot.port) diff --git a/openstack/tests/unit/network/v2/test_l3_conntrack_helper.py b/openstack/tests/unit/network/v2/test_l3_conntrack_helper.py new file mode 100644 index 000000000..c1575f228 --- /dev/null +++ b/openstack/tests/unit/network/v2/test_l3_conntrack_helper.py @@ -0,0 +1,45 @@ +# 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.tests.unit import base + +from openstack.network.v2 import l3_conntrack_helper + +EXAMPLE = { + 'id': 'ct_helper_id', + 'protocol': 'udp', + 'port': 69, + 'helper': 'tftp' +} + + +class TestL3ConntrackHelper(base.TestCase): + + def test_basic(self): + sot = l3_conntrack_helper.ConntrackHelper() + self.assertEqual('conntrack_helper', sot.resource_key) + self.assertEqual('conntrack_helpers', sot.resources_key) + self.assertEqual( + '/routers/%(router_id)s/conntrack_helpers', + sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = l3_conntrack_helper.ConntrackHelper(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['protocol'], sot.protocol) + self.assertEqual(EXAMPLE['port'], sot.port) + self.assertEqual(EXAMPLE['helper'], sot.helper) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 488e23b3b..9772d995b 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -26,6 +26,7 @@ from openstack.network.v2 import flavor from openstack.network.v2 import floating_ip from openstack.network.v2 import health_monitor +from openstack.network.v2 import l3_conntrack_helper from openstack.network.v2 import listener from openstack.network.v2 import load_balancer from openstack.network.v2 import metering_label @@ -63,6 +64,7 @@ AGENT_ID = 'agent-id-' + uuid.uuid4().hex ROUTER_ID = 'router-id-' + uuid.uuid4().hex FIP_ID = 'fip-id-' + uuid.uuid4().hex +CT_HELPER_ID = 'ct-helper-id-' + uuid.uuid4().hex class TestNetworkProxy(test_proxy_base.TestProxyBase): @@ -1377,3 +1379,53 @@ def test_update_floating_ip_port_forwarding(self): 'port_forwarding_id'], expected_kwargs={'floatingip_id': FIP_ID, 'foo': 'bar'}) + + def test_create_l3_conntrack_helper(self): + self.verify_create(self.proxy.create_conntrack_helper, + l3_conntrack_helper.ConntrackHelper, + method_kwargs={'router': ROUTER_ID}, + expected_kwargs={'router_id': ROUTER_ID}) + + def test_delete_l3_conntrack_helper(self): + r = router.Router.new(id=ROUTER_ID) + self.verify_delete( + self.proxy.delete_conntrack_helper, + l3_conntrack_helper.ConntrackHelper, + False, input_path_args=['resource_or_id', r], + expected_path_args={'router_id': ROUTER_ID},) + + def test_delete_l3_conntrack_helper_ignore(self): + r = router.Router.new(id=ROUTER_ID) + self.verify_delete( + self.proxy.delete_conntrack_helper, + l3_conntrack_helper.ConntrackHelper, + True, input_path_args=['resource_or_id', r], + expected_path_args={'router_id': ROUTER_ID}, ) + + def test_get_l3_conntrack_helper(self): + r = router.Router.new(id=ROUTER_ID) + self._verify2('openstack.proxy.Proxy._get', + self.proxy.get_conntrack_helper, + method_args=['conntrack_helper_id', r], + expected_args=[ + l3_conntrack_helper.ConntrackHelper, + 'conntrack_helper_id'], + expected_kwargs={'router_id': ROUTER_ID}) + + def test_l3_conntrack_helpers(self): + self.verify_list(self.proxy.conntrack_helpers, + l3_conntrack_helper.ConntrackHelper, + method_args=[ROUTER_ID], + expected_kwargs={'router_id': ROUTER_ID}) + + def test_update_l3_conntrack_helper(self): + r = router.Router.new(id=ROUTER_ID) + self._verify2('openstack.network.v2._proxy.Proxy._update', + self.proxy.update_conntrack_helper, + method_args=['conntrack_helper_id', r], + method_kwargs={'foo': 'bar'}, + expected_args=[ + l3_conntrack_helper.ConntrackHelper, + 'conntrack_helper_id'], + expected_kwargs={'router_id': ROUTER_ID, + 'foo': 'bar'}) From 41377bf78a981fb75fe0208e9d783add27ade078 Mon Sep 17 00:00:00 2001 From: Ryan Zimmerman Date: Wed, 17 Mar 2021 10:06:22 -0700 Subject: [PATCH 2800/3836] Add compute microversion 2.57 Compute 2.57 [1] removed the personality parameter and added the user_data parameter to the server rebuild API. Also removed maxPersonality and maxPersonalitySize from the limits API response. https: //docs.openstack.org/nova/latest/reference/api-microversion-history.html#id53 Change-Id: Id098e7b9eddb2c94e93864211ab1bc6cfc068972 --- openstack/compute/v2/limits.py | 6 ++++-- openstack/compute/v2/server.py | 11 +++-------- openstack/tests/unit/compute/v2/test_limits.py | 9 --------- openstack/tests/unit/compute/v2/test_server.py | 7 ++----- 4 files changed, 9 insertions(+), 24 deletions(-) diff --git a/openstack/compute/v2/limits.py b/openstack/compute/v2/limits.py index 0f019897f..57104eeb8 100644 --- a/openstack/compute/v2/limits.py +++ b/openstack/compute/v2/limits.py @@ -15,12 +15,14 @@ class AbsoluteLimits(resource.Resource): + _max_microversion = '2.57' + #: The number of key-value pairs that can be set as image metadata. image_meta = resource.Body("maxImageMeta") #: The maximum number of personality contents that can be supplied. - personality = resource.Body("maxPersonality") + personality = resource.Body("maxPersonality", deprecated=True) #: The maximum size, in bytes, of a personality. - personality_size = resource.Body("maxPersonalitySize") + personality_size = resource.Body("maxPersonalitySize", deprecated=True) #: The maximum amount of security group rules allowed. security_group_rules = resource.Body("maxSecurityGroupRules") #: The maximum amount of security groups allowed. diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index fe492c897..0342aef5a 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -148,11 +148,6 @@ class Server(resource.Resource, metadata.MetadataMixin, resource.TagMixin): #: networks parameter, the server attaches to the only network #: created for the current tenant. networks = resource.Body('networks') - #: The file path and contents, text only, to inject into the server at - #: launch. The maximum size of the file path data is 255 bytes. - #: The maximum limit is The number of allowed bytes in the decoded, - #: rather than encoded, data. - personality = resource.Body('personality') #: The power state of this server. power_state = resource.Body('OS-EXT-STS:power_state') #: While the server is building, this value represents the percentage @@ -272,7 +267,7 @@ def force_delete(self, session): def rebuild(self, session, name=None, admin_password=None, preserve_ephemeral=False, image=None, access_ipv4=None, access_ipv6=None, - metadata=None, personality=None): + metadata=None, user_data=None): """Rebuild the server with the given arguments.""" action = { 'preserve_ephemeral': preserve_ephemeral @@ -289,8 +284,8 @@ def rebuild(self, session, name=None, admin_password=None, action['accessIPv6'] = access_ipv6 if metadata is not None: action['metadata'] = metadata - if personality is not None: - action['personality'] = personality + if user_data is not None: + action['user_data'] = user_data body = {'rebuild': action} response = self._action(session, body) diff --git a/openstack/tests/unit/compute/v2/test_limits.py b/openstack/tests/unit/compute/v2/test_limits.py index 847f9df35..872e02bfe 100644 --- a/openstack/tests/unit/compute/v2/test_limits.py +++ b/openstack/tests/unit/compute/v2/test_limits.py @@ -20,8 +20,6 @@ ABSOLUTE_LIMITS = { "maxImageMeta": 128, - "maxPersonality": 5, - "maxPersonalitySize": 10240, "maxSecurityGroupRules": 20, "maxSecurityGroups": 10, "maxServerMeta": 128, @@ -78,9 +76,6 @@ def test_basic(self): def test_make_it(self): sot = limits.AbsoluteLimits(**ABSOLUTE_LIMITS) self.assertEqual(ABSOLUTE_LIMITS["maxImageMeta"], sot.image_meta) - self.assertEqual(ABSOLUTE_LIMITS["maxPersonality"], sot.personality) - self.assertEqual(ABSOLUTE_LIMITS["maxPersonalitySize"], - sot.personality_size) self.assertEqual(ABSOLUTE_LIMITS["maxSecurityGroupRules"], sot.security_group_rules) self.assertEqual(ABSOLUTE_LIMITS["maxSecurityGroups"], @@ -157,10 +152,6 @@ def test_get(self): self.assertEqual(ABSOLUTE_LIMITS["maxImageMeta"], sot.absolute.image_meta) - self.assertEqual(ABSOLUTE_LIMITS["maxPersonality"], - sot.absolute.personality) - self.assertEqual(ABSOLUTE_LIMITS["maxPersonalitySize"], - sot.absolute.personality_size) self.assertEqual(ABSOLUTE_LIMITS["maxSecurityGroupRules"], sot.absolute.security_group_rules) self.assertEqual(ABSOLUTE_LIMITS["maxSecurityGroups"], diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 2491b7363..cb9bd9380 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -97,7 +97,6 @@ 'name': 'new-server-test', 'networks': 'auto', 'os-extended-volumes:volumes_attached': [], - 'personality': '28', 'progress': 0, 'security_groups': [ { @@ -225,7 +224,6 @@ def test_make_it(self): sot.terminated_at) self.assertEqual(EXAMPLE['security_groups'], sot.security_groups) self.assertEqual(EXAMPLE['adminPass'], sot.admin_password) - self.assertEqual(EXAMPLE['personality'], sot.personality) self.assertEqual(EXAMPLE['block_device_mapping_v2'], sot.block_device_mapping) self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:host'], @@ -318,8 +316,7 @@ def test_rebuild(self): image='http://image/1', access_ipv4="12.34.56.78", access_ipv6="fe80::100", metadata={"meta var": "meta val"}, - personality=[{"path": "/etc/motd", - "contents": "foo"}]) + user_data="ZWNobyAiaGVsbG8gd29ybGQi") self.assertIsInstance(result, server.Server) @@ -332,7 +329,7 @@ def test_rebuild(self): "accessIPv4": "12.34.56.78", "accessIPv6": "fe80::100", "metadata": {"meta var": "meta val"}, - "personality": [{"path": "/etc/motd", "contents": "foo"}], + "user_data": "ZWNobyAiaGVsbG8gd29ybGQi", "preserve_ephemeral": False } } From 3f2034832bef5084e67afb96816f599b765a6cd5 Mon Sep 17 00:00:00 2001 From: Tobias Henkel Date: Tue, 6 Apr 2021 12:13:04 +0200 Subject: [PATCH 2801/3836] Avoid prometheus metrics explosion Openstacksdk reports metrics for prometheus and adds an endpoint-label containing the full url of every request. In case the endpoints contain query strings this can easily cause metric explosion. To fix this we can parse the url and strip the query string. Change-Id: Ief3c3981a039b3ae38eeaeccd1fb70e974c94082 --- openstack/proxy.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openstack/proxy.py b/openstack/proxy.py index baa92205a..e19ee39e1 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -11,6 +11,7 @@ # under the License. import urllib +from urllib.parse import urlparse try: import simplejson @@ -226,10 +227,13 @@ def _report_stats_prometheus(self, response, url=None, method=None, url = response.request.url if response is not None and not method: method = response.request.method + parsed_url = urlparse(url) + endpoint = "{}://{}{}".format( + parsed_url.scheme, parsed_url.netloc, parsed_url.path) if response is not None: labels = dict( method=method, - endpoint=url, + endpoint=endpoint, service_type=self.service_type, status_code=response.status_code, ) From 19a948047797fd424b9453c098ab1ed6f8a2e7d0 Mon Sep 17 00:00:00 2001 From: cenne Date: Thu, 8 Apr 2021 16:20:32 +0200 Subject: [PATCH 2802/3836] Fix typo - _list lists, doesn't delete Change-Id: I742fc6247bbecf0129026895607b895a420a538e --- openstack/proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/proxy.py b/openstack/proxy.py index e19ee39e1..72527f7e2 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -516,7 +516,7 @@ def _list(self, resource_type, value=None, paginated=True, base_path=None, **attrs): """List a resource - :param resource_type: The type of resource to delete. This should + :param resource_type: The type of resource to list. This should be a :class:`~openstack.resource.Resource` subclass with a ``from_id`` method. :param value: The resource to list. It can be the ID of a resource, or From c206d4b7f2b2c9df7d9b6b75c3620451148f4fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aija=20Jaunt=C4=93va?= Date: Fri, 9 Apr 2021 09:14:54 -0400 Subject: [PATCH 2803/3836] Add deploy_steps to baremetal node provisioning Story: 2008043 Task: 41825 Change-Id: Ic44d3dfeb6d68875951feabbc480461f5db5d22a --- openstack/baremetal/v1/_common.py | 3 ++ openstack/baremetal/v1/_proxy.py | 12 +++++--- openstack/baremetal/v1/node.py | 22 +++++++++++---- .../tests/unit/baremetal/v1/test_node.py | 28 +++++++++++++++++++ .../ironic-deploy-steps-2c0f39d7d2a13289.yaml | 4 +++ 5 files changed, 60 insertions(+), 9 deletions(-) create mode 100644 releasenotes/notes/ironic-deploy-steps-2c0f39d7d2a13289.yaml diff --git a/openstack/baremetal/v1/_common.py b/openstack/baremetal/v1/_common.py index 7459842ac..c2cdaac55 100644 --- a/openstack/baremetal/v1/_common.py +++ b/openstack/baremetal/v1/_common.py @@ -76,6 +76,9 @@ CONFIG_DRIVE_DICT_VERSION = '1.56' """API version in which configdrive can be a dictionary.""" +DEPLOY_STEPS_VERSION = '1.69' +"""API version in which deploy_steps was added to node provisioning.""" + class ListMixin: diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 6ac6728e3..fcf7fc391 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -327,7 +327,7 @@ def patch_node(self, node, patch, reset_interfaces=None, def set_node_provision_state(self, node, target, config_drive=None, clean_steps=None, rescue_password=None, - wait=False, timeout=None): + wait=False, timeout=None, deploy_steps=None): """Run an action modifying node's provision state. This call is asynchronous, it will return success as soon as the Bare @@ -350,16 +350,20 @@ def set_node_provision_state(self, node, target, config_drive=None, :param timeout: If ``wait`` is set to ``True``, specifies how much (in seconds) to wait for the expected state to be reached. The value of ``None`` (the default) means no client-side timeout. + :param deploy_steps: Deploy steps to execute, only valid for ``active`` + and ``rebuild`` target. :returns: The updated :class:`~openstack.baremetal.v1.node.Node` - :raises: ValueError if ``config_drive``, ``clean_steps`` or - ``rescue_password`` are provided with an invalid ``target``. + :raises: ValueError if ``config_drive``, ``clean_steps``, + ``deploy_steps`` or ``rescue_password`` are provided with an + invalid ``target``. """ res = self._get_resource(_node.Node, node) return res.set_provision_state(self, target, config_drive=config_drive, clean_steps=clean_steps, rescue_password=rescue_password, - wait=wait, timeout=timeout) + wait=wait, timeout=timeout, + deploy_steps=deploy_steps) def set_node_boot_device(self, node, boot_device, persistent=False): """Set node boot device diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 72c50e19a..49f3fed80 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -91,8 +91,8 @@ class Node(_common.ListMixin, resource.Resource): is_maintenance='maintenance', ) - # The retired and retired_reason fields introduced in 1.61 (Ussuri). - _max_microversion = '1.61' + # Provision state deploy_steps introduced in 1.69 (Wallaby). + _max_microversion = '1.69' # Properties #: The UUID of the allocation associated with this node. Added in API @@ -346,7 +346,7 @@ def commit(self, session, *args, **kwargs): def set_provision_state(self, session, target, config_drive=None, clean_steps=None, rescue_password=None, - wait=False, timeout=None): + wait=False, timeout=None, deploy_steps=None): """Run an action modifying this node's provision state. This call is asynchronous, it will return success as soon as the Bare @@ -366,10 +366,13 @@ def set_provision_state(self, session, target, config_drive=None, :param wait: Whether to wait for the target state to be reached. :param timeout: Timeout (in seconds) to wait for the target state to be reached. If ``None``, wait without timeout. + :param deploy_steps: Deploy steps to execute, only valid for ``active`` + and ``rebuild`` target. :return: This :class:`Node` instance. - :raises: ValueError if ``config_drive``, ``clean_steps`` or - ``rescue_password`` are provided with an invalid ``target``. + :raises: ValueError if ``config_drive``, ``clean_steps``, + ``deploy_steps`` or ``rescue_password`` are provided with an + invalid ``target``. :raises: :class:`~openstack.exceptions.ResourceFailure` if the node reaches an error state while waiting for the state. :raises: :class:`~openstack.exceptions.ResourceTimeout` if timeout @@ -388,6 +391,9 @@ def set_provision_state(self, session, target, config_drive=None, elif target == 'rebuild': version = _common.CONFIG_DRIVE_REBUILD_VERSION + if deploy_steps: + version = _common.DEPLOY_STEPS_VERSION + version = self._assert_microversion_for(session, 'commit', version) body = {'target': target} @@ -404,6 +410,12 @@ def set_provision_state(self, session, target, config_drive=None, '"clean" target') body['clean_steps'] = clean_steps + if deploy_steps is not None: + if target not in ('active', 'rebuild'): + raise ValueError('Deploy steps can only be provided with ' + '"deploy" and "rebuild" target') + body['deploy_steps'] = deploy_steps + if rescue_password is not None: if target != 'rescue': raise ValueError('Rescue password can only be provided with ' diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index b00ab3309..4f5f1664e 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -296,6 +296,34 @@ def test_configdrive_as_dict(self): headers=mock.ANY, microversion='1.56', retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + def test_deploy_with_deploy_steps(self): + deploy_steps = [{'interface': 'deploy', 'step': 'upgrade_fw'}] + result = self.node.set_provision_state( + self.session, 'active', + deploy_steps=deploy_steps) + + self.assertIs(result, self.node) + self.session.put.assert_called_once_with( + 'nodes/%s/states/provision' % self.node.id, + json={'target': 'active', 'deploy_steps': deploy_steps}, + headers=mock.ANY, microversion='1.69', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES + ) + + def test_rebuild_with_deploy_steps(self): + deploy_steps = [{'interface': 'deploy', 'step': 'upgrade_fw'}] + result = self.node.set_provision_state( + self.session, 'rebuild', + deploy_steps=deploy_steps) + + self.assertIs(result, self.node) + self.session.put.assert_called_once_with( + 'nodes/%s/states/provision' % self.node.id, + json={'target': 'rebuild', 'deploy_steps': deploy_steps}, + headers=mock.ANY, microversion='1.69', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES + ) + @mock.patch.object(node.Node, '_translate_response', mock.Mock()) @mock.patch.object(node.Node, '_get_session', lambda self, x: x) diff --git a/releasenotes/notes/ironic-deploy-steps-2c0f39d7d2a13289.yaml b/releasenotes/notes/ironic-deploy-steps-2c0f39d7d2a13289.yaml new file mode 100644 index 000000000..b6adb8cd1 --- /dev/null +++ b/releasenotes/notes/ironic-deploy-steps-2c0f39d7d2a13289.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Adds ``deploy_steps`` to baremetal node provisioning. From 9081cd54e80de75158cd654676e36daa1b65a92c Mon Sep 17 00:00:00 2001 From: Riccardo Pittau Date: Wed, 24 Mar 2021 16:26:48 +0100 Subject: [PATCH 2804/3836] Fixing more import orders Remove corresponding filters from tox.ini Change-Id: I009de26adf21c7e6a5076a2432e3fce057743475 --- openstack/tests/unit/dns/v2/test_floating_ip.py | 3 +-- openstack/tests/unit/dns/v2/test_proxy.py | 6 +++--- openstack/tests/unit/dns/v2/test_recordset.py | 3 +-- openstack/tests/unit/dns/v2/test_zone_transfer.py | 2 +- openstack/tests/unit/image/v1/test_image.py | 2 +- openstack/tests/unit/image/v2/test_image.py | 1 + openstack/tests/unit/image/v2/test_member.py | 2 +- openstack/tests/unit/image/v2/test_proxy.py | 2 +- openstack/tests/unit/image/v2/test_schema.py | 2 +- openstack/tests/unit/image/v2/test_service_info.py | 2 +- openstack/tests/unit/image/v2/test_task.py | 2 +- tox.ini | 2 -- 12 files changed, 13 insertions(+), 16 deletions(-) diff --git a/openstack/tests/unit/dns/v2/test_floating_ip.py b/openstack/tests/unit/dns/v2/test_floating_ip.py index 412e453c9..f1e6b19cd 100644 --- a/openstack/tests/unit/dns/v2/test_floating_ip.py +++ b/openstack/tests/unit/dns/v2/test_floating_ip.py @@ -10,9 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base - from openstack.dns.v2 import floating_ip as fip +from openstack.tests.unit import base IDENTIFIER = 'RegionOne:id' diff --git a/openstack/tests/unit/dns/v2/test_proxy.py b/openstack/tests/unit/dns/v2/test_proxy.py index fe7694c27..125035e92 100644 --- a/openstack/tests/unit/dns/v2/test_proxy.py +++ b/openstack/tests/unit/dns/v2/test_proxy.py @@ -11,12 +11,12 @@ # under the License. from openstack.dns.v2 import _proxy +from openstack.dns.v2 import floating_ip +from openstack.dns.v2 import recordset from openstack.dns.v2 import zone -from openstack.dns.v2 import zone_import from openstack.dns.v2 import zone_export +from openstack.dns.v2 import zone_import from openstack.dns.v2 import zone_transfer -from openstack.dns.v2 import recordset -from openstack.dns.v2 import floating_ip from openstack.tests.unit import test_proxy_base diff --git a/openstack/tests/unit/dns/v2/test_recordset.py b/openstack/tests/unit/dns/v2/test_recordset.py index e53aa4938..a1eb2706c 100644 --- a/openstack/tests/unit/dns/v2/test_recordset.py +++ b/openstack/tests/unit/dns/v2/test_recordset.py @@ -10,9 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base - from openstack.dns.v2 import recordset +from openstack.tests.unit import base IDENTIFIER = 'NAME' diff --git a/openstack/tests/unit/dns/v2/test_zone_transfer.py b/openstack/tests/unit/dns/v2/test_zone_transfer.py index 7064834fe..bc8ba1a80 100644 --- a/openstack/tests/unit/dns/v2/test_zone_transfer.py +++ b/openstack/tests/unit/dns/v2/test_zone_transfer.py @@ -9,9 +9,9 @@ # 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.tests.unit import base from openstack.dns.v2 import zone_transfer +from openstack.tests.unit import base IDENTIFIER = '074e805e-fe87-4cbb-b10b-21a06e215d41' diff --git a/openstack/tests/unit/image/v1/test_image.py b/openstack/tests/unit/image/v1/test_image.py index d593e727d..9809b91a6 100644 --- a/openstack/tests/unit/image/v1/test_image.py +++ b/openstack/tests/unit/image/v1/test_image.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.image.v1 import image from openstack.tests.unit import base -from openstack.image.v1 import image IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 45932cc83..9ddd95305 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -9,6 +9,7 @@ # 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 io import operator import tempfile diff --git a/openstack/tests/unit/image/v2/test_member.py b/openstack/tests/unit/image/v2/test_member.py index 7fc603862..8168b1254 100644 --- a/openstack/tests/unit/image/v2/test_member.py +++ b/openstack/tests/unit/image/v2/test_member.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.image.v2 import member from openstack.tests.unit import base -from openstack.image.v2 import member IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 672c15057..bba3333a5 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -20,8 +20,8 @@ from openstack.image.v2 import image from openstack.image.v2 import member from openstack.image.v2 import schema -from openstack.image.v2 import task from openstack.image.v2 import service_info as si +from openstack.image.v2 import task from openstack.tests.unit.image.v2 import test_image as fake_image from openstack.tests.unit import test_proxy_base diff --git a/openstack/tests/unit/image/v2/test_schema.py b/openstack/tests/unit/image/v2/test_schema.py index 88e0823cf..ed5cddb15 100644 --- a/openstack/tests/unit/image/v2/test_schema.py +++ b/openstack/tests/unit/image/v2/test_schema.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.image.v2 import schema from openstack.tests.unit import base -from openstack.image.v2 import schema IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/image/v2/test_service_info.py b/openstack/tests/unit/image/v2/test_service_info.py index 6d8c85f92..f00ab88ff 100644 --- a/openstack/tests/unit/image/v2/test_service_info.py +++ b/openstack/tests/unit/image/v2/test_service_info.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.image.v2 import service_info as si from openstack.tests.unit import base -from openstack.image.v2 import service_info as si IDENTIFIER = 'IDENTIFIER' EXAMPLE_IMPORT = { diff --git a/openstack/tests/unit/image/v2/test_task.py b/openstack/tests/unit/image/v2/test_task.py index f137f0611..6b1e3f35b 100644 --- a/openstack/tests/unit/image/v2/test_task.py +++ b/openstack/tests/unit/image/v2/test_task.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.image.v2 import task from openstack.tests.unit import base -from openstack.image.v2 import task IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/tox.ini b/tox.ini index 6786233ae..91a6a7625 100644 --- a/tox.ini +++ b/tox.ini @@ -135,8 +135,6 @@ per-file-ignores = openstack/tests/unit/baremetal/*:H306,I100,I201,I202 openstack/tests/unit/compute/*:H306,I100,I201,I202 openstack/tests/unit/network/*:H306,I100,I201,I202 - openstack/tests/unit/image/*:H306,I100,I201,I202 - openstack/tests/unit/dns/*:H306,I100,I201,I202 [flake8:local-plugins] extension = From abfb719f06a14b9a1351d5193414ce6d4c14876b Mon Sep 17 00:00:00 2001 From: Riccardo Pittau Date: Tue, 13 Apr 2021 17:50:11 +0200 Subject: [PATCH 2805/3836] Fix more import orders Remove corresponding filters from tox.ini Change-Id: Ic30ee6c568ff78effcfc11b0473d6256f22e17c6 --- openstack/tests/unit/baremetal/test_version.py | 2 +- openstack/tests/unit/baremetal/v1/test_chassis.py | 2 +- openstack/tests/unit/baremetal/v1/test_conductor.py | 2 +- openstack/tests/unit/baremetal/v1/test_deploy_templates.py | 2 +- openstack/tests/unit/baremetal/v1/test_driver.py | 2 +- openstack/tests/unit/baremetal/v1/test_node.py | 2 +- openstack/tests/unit/baremetal/v1/test_port.py | 2 +- openstack/tests/unit/baremetal/v1/test_port_group.py | 2 +- openstack/tests/unit/baremetal/v1/test_volume_connector.py | 2 +- openstack/tests/unit/baremetal/v1/test_volume_target.py | 2 +- tox.ini | 1 - 11 files changed, 10 insertions(+), 11 deletions(-) diff --git a/openstack/tests/unit/baremetal/test_version.py b/openstack/tests/unit/baremetal/test_version.py index 06ce752c3..520906143 100644 --- a/openstack/tests/unit/baremetal/test_version.py +++ b/openstack/tests/unit/baremetal/test_version.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.baremetal import version from openstack.tests.unit import base -from openstack.baremetal import version IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/baremetal/v1/test_chassis.py b/openstack/tests/unit/baremetal/v1/test_chassis.py index ca31a07b5..df86ba7f7 100644 --- a/openstack/tests/unit/baremetal/v1/test_chassis.py +++ b/openstack/tests/unit/baremetal/v1/test_chassis.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.baremetal.v1 import chassis from openstack.tests.unit import base -from openstack.baremetal.v1 import chassis FAKE = { "created_at": "2016-08-18T22:28:48.165105+00:00", diff --git a/openstack/tests/unit/baremetal/v1/test_conductor.py b/openstack/tests/unit/baremetal/v1/test_conductor.py index 55900f4ed..58cce1824 100644 --- a/openstack/tests/unit/baremetal/v1/test_conductor.py +++ b/openstack/tests/unit/baremetal/v1/test_conductor.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.baremetal.v1 import conductor from openstack.tests.unit import base -from openstack.baremetal.v1 import conductor FAKE = { "links": [ diff --git a/openstack/tests/unit/baremetal/v1/test_deploy_templates.py b/openstack/tests/unit/baremetal/v1/test_deploy_templates.py index 0b826f425..a7f5af632 100644 --- a/openstack/tests/unit/baremetal/v1/test_deploy_templates.py +++ b/openstack/tests/unit/baremetal/v1/test_deploy_templates.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.baremetal.v1 import deploy_templates from openstack.tests.unit import base -from openstack.baremetal.v1 import deploy_templates FAKE = { "created_at": "2016-08-18T22:28:48.643434+11:11", diff --git a/openstack/tests/unit/baremetal/v1/test_driver.py b/openstack/tests/unit/baremetal/v1/test_driver.py index 8fdcabcd9..364e0c1ae 100644 --- a/openstack/tests/unit/baremetal/v1/test_driver.py +++ b/openstack/tests/unit/baremetal/v1/test_driver.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.baremetal.v1 import driver from openstack.tests.unit import base -from openstack.baremetal.v1 import driver FAKE = { "hosts": [ diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 4f5f1664e..47dbda72e 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -18,8 +18,8 @@ from openstack.baremetal.v1 import node from openstack import exceptions from openstack import resource -from openstack import utils from openstack.tests.unit import base +from openstack import utils # NOTE: Sample data from api-ref doc FAKE = { diff --git a/openstack/tests/unit/baremetal/v1/test_port.py b/openstack/tests/unit/baremetal/v1/test_port.py index c5d80304a..396cd20ba 100644 --- a/openstack/tests/unit/baremetal/v1/test_port.py +++ b/openstack/tests/unit/baremetal/v1/test_port.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.baremetal.v1 import port from openstack.tests.unit import base -from openstack.baremetal.v1 import port FAKE = { "address": "11:11:11:11:11:11", diff --git a/openstack/tests/unit/baremetal/v1/test_port_group.py b/openstack/tests/unit/baremetal/v1/test_port_group.py index 6b57faa25..33af62a3e 100644 --- a/openstack/tests/unit/baremetal/v1/test_port_group.py +++ b/openstack/tests/unit/baremetal/v1/test_port_group.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.baremetal.v1 import port_group from openstack.tests.unit import base -from openstack.baremetal.v1 import port_group FAKE = { "address": "11:11:11:11:11:11", diff --git a/openstack/tests/unit/baremetal/v1/test_volume_connector.py b/openstack/tests/unit/baremetal/v1/test_volume_connector.py index 9277ef795..aa4a0eb9c 100644 --- a/openstack/tests/unit/baremetal/v1/test_volume_connector.py +++ b/openstack/tests/unit/baremetal/v1/test_volume_connector.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.baremetal.v1 import volume_connector from openstack.tests.unit import base -from openstack.baremetal.v1 import volume_connector FAKE = { "connector_id": "iqn.2017-07.org.openstack:01:d9a51732c3f", diff --git a/openstack/tests/unit/baremetal/v1/test_volume_target.py b/openstack/tests/unit/baremetal/v1/test_volume_target.py index 4598858a3..f4b4d0c9a 100644 --- a/openstack/tests/unit/baremetal/v1/test_volume_target.py +++ b/openstack/tests/unit/baremetal/v1/test_volume_target.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.baremetal.v1 import volume_target from openstack.tests.unit import base -from openstack.baremetal.v1 import volume_target FAKE = { "boot_index": 0, diff --git a/tox.ini b/tox.ini index 91a6a7625..6a44739cd 100644 --- a/tox.ini +++ b/tox.ini @@ -132,7 +132,6 @@ per-file-ignores = openstack/tests/unit/workflow/*:H306,I100,I201,I202 openstack/tests/unit/message/*:H306,I100,I201,I202 openstack/tests/unit/load_balancer/*:H306,I100,I201,I202 - openstack/tests/unit/baremetal/*:H306,I100,I201,I202 openstack/tests/unit/compute/*:H306,I100,I201,I202 openstack/tests/unit/network/*:H306,I100,I201,I202 From b562d779e60337926c011b147256ba81cc1e6f88 Mon Sep 17 00:00:00 2001 From: Ian Wienand Date: Wed, 14 Apr 2021 09:40:17 +1000 Subject: [PATCH 2806/3836] Don't send empty remote_address_group_id for security groups We are seeing an error raised by older Neutron's 400: Client Error for url: /v2.0/security-group-rules, Unrecognized attribute(s) 'remote_address_group_id' This field was added unconditionally with I50374c339ab7685a6e74f25f9521b8810c532e13 but, per above, appears to cause problems for older Neutron instances. To work around this, remove the argument from the body when blank by overriding the _prepare_request function of SecurityGroupRule. Two tests where this are used are updated; one checks the body is not sent when None and the other is modified to send a remote_address_group_id value to validate the other path. Story: #2008577 Task: #41729 Change-Id: I25dabfde27b843df1c91c7fc37a1fe8d207b8010 --- openstack/network/v2/security_group_rule.py | 12 ++++++++++++ openstack/tests/unit/cloud/test_security_groups.py | 6 +++++- .../remote-address-group-id-6291816888cb3de7.yaml | 6 ++++++ 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/remote-address-group-id-6291816888cb3de7.yaml diff --git a/openstack/network/v2/security_group_rule.py b/openstack/network/v2/security_group_rule.py index 6653ab923..bd1846021 100644 --- a/openstack/network/v2/security_group_rule.py +++ b/openstack/network/v2/security_group_rule.py @@ -87,3 +87,15 @@ class SecurityGroupRule(_base.NetworkResource, resource.TagMixin): tenant_id = resource.Body('tenant_id') #: Timestamp when the security group rule was last updated. updated_at = resource.Body('updated_at') + + def _prepare_request(self, *args, **kwargs): + _request = super(SecurityGroupRule, self)._prepare_request( + *args, **kwargs) + # Old versions of Neutron do not handle being passed a + # remote_address_group_id and raise and error. Remove it from + # the body if it is blank. + if not self.remote_address_group_id: + if 'security_group_rule' in _request.body: + _rule = _request.body['security_group_rule'] + _rule.pop('remote_address_group_id', None) + return _request diff --git a/openstack/tests/unit/cloud/test_security_groups.py b/openstack/tests/unit/cloud/test_security_groups.py index 70010b3f6..70b3ec29a 100644 --- a/openstack/tests/unit/cloud/test_security_groups.py +++ b/openstack/tests/unit/cloud/test_security_groups.py @@ -347,7 +347,7 @@ def test_create_security_group_rule_neutron(self): protocol='tcp', remote_ip_prefix='0.0.0.0/0', remote_group_id='456', - remote_address_group_id=None, + remote_address_group_id='1234-5678', direction='egress', ethertype='IPv6' ) @@ -415,6 +415,10 @@ def test_create_security_group_rule_neutron_specific_tenant(self): expected_new_rule['id'] = '1234' expected_new_rule['project_id'] = expected_new_rule['tenant_id'] + # This is not sent in body if == None so should not be in the + # JSON; see SecurityGroupRule where it is removed. + expected_args.pop('remote_address_group_id') + self.register_uris([ dict(method='GET', uri=self.get_mock_url( diff --git a/releasenotes/notes/remote-address-group-id-6291816888cb3de7.yaml b/releasenotes/notes/remote-address-group-id-6291816888cb3de7.yaml new file mode 100644 index 000000000..e1ce09505 --- /dev/null +++ b/releasenotes/notes/remote-address-group-id-6291816888cb3de7.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixes a regression sending an unsupported field + ``remote_address_group_id`` when creating security groups with an + older Neutron (introduced 0.53.0). From d783b5889fd7f9a59f948c48cfc5a752d6836ae3 Mon Sep 17 00:00:00 2001 From: "wu.shiming" Date: Wed, 13 Jan 2021 10:18:33 +0800 Subject: [PATCH 2807/3836] Drop lower-constraints job We facing errors related to the new pip resolver, this topic was discussed on the ML and QA team proposed to to test lower-constraints [1]. I propose to drop this job because the complexity and recurring pain needed to maintain that now exceeds the benefits provided by this mechanismes, and We need to drop the lower-constraints.txt file as well as the corresponding tox target [1] http://lists.openstack.org/pipermail/openstack-discuss/2020-December/019390.html Change-Id: Ib0ef1fadf4fe44f1d6af2f7d542b8af919652501 --- .zuul.yaml | 1 - lower-constraints.txt | 35 ----------------------------------- tox.ini | 5 ----- 3 files changed, 41 deletions(-) delete mode 100644 lower-constraints.txt diff --git a/.zuul.yaml b/.zuul.yaml index 53f8a8566..6fe7f08b2 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -375,7 +375,6 @@ - project: templates: - check-requirements - - openstack-lower-constraints-jobs - openstack-python3-ussuri-jobs - openstacksdk-functional-tips - openstacksdk-tox-tips diff --git a/lower-constraints.txt b/lower-constraints.txt deleted file mode 100644 index 0dd43adfc..000000000 --- a/lower-constraints.txt +++ /dev/null @@ -1,35 +0,0 @@ -appdirs==1.3.0 -coverage==4.0 -cryptography==2.7 -ddt==1.0.1 -decorator==4.4.1 -doc8==0.8.0 -dogpile.cache==0.6.5 -fixtures==3.0.0 -importlib_metadata==1.7.0 -iso8601==0.1.11 -jmespath==0.9.0 -jsonpatch==1.16 -jsonpointer==1.13 -jsonschema==3.2.0 -keystoneauth1==3.18.0 -linecache2==1.0.0 -munch==2.1.0 -netifaces==0.10.4 -os-service-types==1.7.0 -oslo.config==6.1.0 -oslotest==3.2.0 -pbr==2.0.0 -prometheus-client==0.4.2 -Pygments==2.2.0 -python-mimeparse==1.6.0 -python-subunit==1.0.0 -PyYAML==3.13 -requests==2.18.0 -requests-mock==1.2.0 -requestsexceptions==1.2.0 -statsd==3.3.0 -stestr==1.0.0 -testscenarios==0.4 -testtools==2.2.0 -traceback2==1.4.0 diff --git a/tox.ini b/tox.ini index 5334aac5e..937893fb9 100644 --- a/tox.ini +++ b/tox.ini @@ -123,8 +123,3 @@ paths = ./openstack [doc8] extensions = .rst, .yaml -[testenv:lower-constraints] -deps = - -c{toxinidir}/lower-constraints.txt - -r{toxinidir}/test-requirements.txt - -r{toxinidir}/requirements.txt From ea1d5c74b49f0baaf754058285b1c68091c38e12 Mon Sep 17 00:00:00 2001 From: James Palmer Date: Wed, 17 Feb 2021 14:39:22 -0600 Subject: [PATCH 2808/3836] Add support for Resource Filters Introduct the resource_filters resource, its attributes, and API Calls for interacting with resource_filters. Task: 41813 Story: 2008619 Change-Id: I03f618648da1f11583fb0dae990e0163ddc0624e Signed-off-by: James Palmer --- openstack/block_storage/v3/_proxy.py | 8 +++ openstack/block_storage/v3/resource_filter.py | 33 ++++++++++++ .../block_storage/v3/test_resource_filters.py | 24 +++++++++ .../tests/unit/block_storage/v3/test_proxy.py | 5 ++ .../block_storage/v3/test_resource_filter.py | 52 +++++++++++++++++++ 5 files changed, 122 insertions(+) create mode 100644 openstack/block_storage/v3/resource_filter.py create mode 100644 openstack/tests/functional/block_storage/v3/test_resource_filters.py create mode 100644 openstack/tests/unit/block_storage/v3/test_resource_filter.py diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index c0276a74c..58d7e6bd7 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -15,6 +15,7 @@ from openstack.block_storage.v3 import backup as _backup from openstack.block_storage.v3 import capabilities as _capabilities from openstack.block_storage.v3 import limits as _limits +from openstack.block_storage.v3 import resource_filter as _resource_filter from openstack.block_storage.v3 import snapshot as _snapshot from openstack.block_storage.v3 import stats as _stats from openstack.block_storage.v3 import type as _type @@ -589,6 +590,13 @@ def wait_for_delete(self, res, interval=2, wait=120): """ return resource.wait_for_delete(self, res, interval, wait) + def resource_filters(self, **query): + """Retrieve a generator of resource filters + + :returns: A generator of resource filters. + """ + return self._list(_resource_filter.ResourceFilter, **query) + def _get_cleanup_dependencies(self): return { 'block_storage': { diff --git a/openstack/block_storage/v3/resource_filter.py b/openstack/block_storage/v3/resource_filter.py new file mode 100644 index 000000000..f26b46c09 --- /dev/null +++ b/openstack/block_storage/v3/resource_filter.py @@ -0,0 +1,33 @@ +# 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 resource + + +class ResourceFilter(resource.Resource): + """Resource Filter""" + resources_key = "resource_filters" + base_path = "/resource_filters" + + _query_mapping = resource.QueryParameters('resource') + + # Capabilities + allow_list = True + + # resource_filters introduced in 3.33 + _max_microversion = '3.33' + + #: Properties + #: The list of filters that are applicable to the specified resource. + filters = resource.Body('filters', type=list) + #: The resource that the filters will be applied to. + resource = resource.Body('resource', type=str) diff --git a/openstack/tests/functional/block_storage/v3/test_resource_filters.py b/openstack/tests/functional/block_storage/v3/test_resource_filters.py new file mode 100644 index 000000000..7380ab1de --- /dev/null +++ b/openstack/tests/functional/block_storage/v3/test_resource_filters.py @@ -0,0 +1,24 @@ +# 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.tests.functional.block_storage.v3 import base + + +class ResourceFilters(base.BaseBlockStorageTest): + + def test_get(self): + resource_filters = list(self.conn.block_storage.resource_filters()) + + for rf in resource_filters: + self.assertIsInstance(rf.filters, list) + self.assertIsInstance(rf.resource, str) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index b625e6337..b117e3f1a 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -15,6 +15,7 @@ from openstack.block_storage.v3 import backup from openstack.block_storage.v3 import capabilities from openstack.block_storage.v3 import limits +from openstack.block_storage.v3 import resource_filter from openstack.block_storage.v3 import snapshot from openstack.block_storage.v3 import stats from openstack.block_storage.v3 import type @@ -240,3 +241,7 @@ def test_limits_get(self): def test_capabilites_get(self): self.verify_get(self.proxy.get_capabilities, capabilities.Capabilities) + + def test_resource_filters(self): + self.verify_list(self.proxy.resource_filters, + resource_filter.ResourceFilter) diff --git a/openstack/tests/unit/block_storage/v3/test_resource_filter.py b/openstack/tests/unit/block_storage/v3/test_resource_filter.py new file mode 100644 index 000000000..21fcc17e4 --- /dev/null +++ b/openstack/tests/unit/block_storage/v3/test_resource_filter.py @@ -0,0 +1,52 @@ +# 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.block_storage.v3 import resource_filter +from openstack.tests.unit import base + +RESOURCE_FILTER = { + 'filters': [ + 'name', + 'status', + 'image_metadata', + 'bootable', + 'migration_status' + ], + 'resource': 'volume' +} + + +class TestResourceFilter(base.TestCase): + + def test_basic(self): + resource = resource_filter.ResourceFilter() + self.assertEqual('resource_filters', + resource.resources_key) + self.assertEqual('/resource_filters', + resource.base_path) + self.assertFalse(resource.allow_create) + self.assertFalse(resource.allow_fetch) + self.assertFalse(resource.allow_commit) + self.assertFalse(resource.allow_delete) + self.assertTrue(resource.allow_list) + + self.assertDictEqual({"resource": "resource", + "limit": "limit", + "marker": "marker"}, + resource._query_mapping._mapping) + + def test_make_resource_filter(self): + resource = resource_filter.ResourceFilter( + **RESOURCE_FILTER) + self.assertEqual( + RESOURCE_FILTER['filters'], resource.filters) + self.assertEqual( + RESOURCE_FILTER['resource'], resource.resource) From d1676c24d317e7ee4115582cdfc28279ea910e2b Mon Sep 17 00:00:00 2001 From: Riccardo Pittau Date: Mon, 19 Apr 2021 15:42:27 +0200 Subject: [PATCH 2809/3836] Fix import order in network unit tests Change-Id: I9bc1d42cfb0cb6574a9f765b21a50e338a98f2b6 --- openstack/tests/unit/network/test_version.py | 2 +- openstack/tests/unit/network/v2/test_address_group.py | 2 +- openstack/tests/unit/network/v2/test_address_scope.py | 2 +- .../tests/unit/network/v2/test_auto_allocated_topology.py | 2 +- openstack/tests/unit/network/v2/test_availability_zone.py | 2 +- openstack/tests/unit/network/v2/test_extension.py | 2 +- openstack/tests/unit/network/v2/test_floating_ip.py | 2 +- openstack/tests/unit/network/v2/test_health_monitor.py | 2 +- openstack/tests/unit/network/v2/test_l3_conntrack_helper.py | 2 +- openstack/tests/unit/network/v2/test_listener.py | 2 +- openstack/tests/unit/network/v2/test_load_balancer.py | 2 +- openstack/tests/unit/network/v2/test_metering_label.py | 2 +- openstack/tests/unit/network/v2/test_metering_label_rule.py | 2 +- openstack/tests/unit/network/v2/test_network.py | 2 +- .../tests/unit/network/v2/test_network_ip_availability.py | 2 +- openstack/tests/unit/network/v2/test_network_segment_range.py | 2 +- openstack/tests/unit/network/v2/test_pool.py | 2 +- openstack/tests/unit/network/v2/test_pool_member.py | 2 +- openstack/tests/unit/network/v2/test_port.py | 2 +- openstack/tests/unit/network/v2/test_port_forwarding.py | 2 +- .../tests/unit/network/v2/test_qos_bandwidth_limit_rule.py | 3 ++- openstack/tests/unit/network/v2/test_qos_dscp_marking_rule.py | 3 ++- .../tests/unit/network/v2/test_qos_minimum_bandwidth_rule.py | 3 ++- openstack/tests/unit/network/v2/test_qos_policy.py | 3 ++- openstack/tests/unit/network/v2/test_qos_rule_type.py | 2 +- openstack/tests/unit/network/v2/test_quota.py | 4 ++-- openstack/tests/unit/network/v2/test_rbac_policy.py | 2 +- openstack/tests/unit/network/v2/test_security_group.py | 2 +- openstack/tests/unit/network/v2/test_security_group_rule.py | 2 +- openstack/tests/unit/network/v2/test_segment.py | 2 +- openstack/tests/unit/network/v2/test_service_profile.py | 2 +- openstack/tests/unit/network/v2/test_service_provider.py | 2 +- openstack/tests/unit/network/v2/test_subnet.py | 2 +- openstack/tests/unit/network/v2/test_subnet_pool.py | 2 +- openstack/tests/unit/network/v2/test_vpn_service.py | 3 +-- tox.ini | 1 - 36 files changed, 40 insertions(+), 38 deletions(-) diff --git a/openstack/tests/unit/network/test_version.py b/openstack/tests/unit/network/test_version.py index 22b6e1028..77df42ba6 100644 --- a/openstack/tests/unit/network/test_version.py +++ b/openstack/tests/unit/network/test_version.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network import version from openstack.tests.unit import base -from openstack.network import version IDENTIFIER = 'v2.0' EXAMPLE = { diff --git a/openstack/tests/unit/network/v2/test_address_group.py b/openstack/tests/unit/network/v2/test_address_group.py index 9275575f7..356857293 100644 --- a/openstack/tests/unit/network/v2/test_address_group.py +++ b/openstack/tests/unit/network/v2/test_address_group.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import address_group from openstack.tests.unit import base -from openstack.network.v2 import address_group IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/network/v2/test_address_scope.py b/openstack/tests/unit/network/v2/test_address_scope.py index 48103fd72..8f7a2059d 100644 --- a/openstack/tests/unit/network/v2/test_address_scope.py +++ b/openstack/tests/unit/network/v2/test_address_scope.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import address_scope from openstack.tests.unit import base -from openstack.network.v2 import address_scope IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/network/v2/test_auto_allocated_topology.py b/openstack/tests/unit/network/v2/test_auto_allocated_topology.py index 500837949..bc836f0bc 100644 --- a/openstack/tests/unit/network/v2/test_auto_allocated_topology.py +++ b/openstack/tests/unit/network/v2/test_auto_allocated_topology.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import auto_allocated_topology from openstack.tests.unit import base -from openstack.network.v2 import auto_allocated_topology EXAMPLE = { 'tenant_id': '1', diff --git a/openstack/tests/unit/network/v2/test_availability_zone.py b/openstack/tests/unit/network/v2/test_availability_zone.py index 8b9fc79c0..ef5f22336 100644 --- a/openstack/tests/unit/network/v2/test_availability_zone.py +++ b/openstack/tests/unit/network/v2/test_availability_zone.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import availability_zone from openstack.tests.unit import base -from openstack.network.v2 import availability_zone IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/network/v2/test_extension.py b/openstack/tests/unit/network/v2/test_extension.py index a2f67e1f2..4ae763445 100644 --- a/openstack/tests/unit/network/v2/test_extension.py +++ b/openstack/tests/unit/network/v2/test_extension.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import extension from openstack.tests.unit import base -from openstack.network.v2 import extension IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/network/v2/test_floating_ip.py b/openstack/tests/unit/network/v2/test_floating_ip.py index d443884b5..1c057b64c 100644 --- a/openstack/tests/unit/network/v2/test_floating_ip.py +++ b/openstack/tests/unit/network/v2/test_floating_ip.py @@ -12,8 +12,8 @@ from unittest import mock -from openstack import proxy from openstack.network.v2 import floating_ip +from openstack import proxy from openstack.tests.unit import base IDENTIFIER = 'IDENTIFIER' diff --git a/openstack/tests/unit/network/v2/test_health_monitor.py b/openstack/tests/unit/network/v2/test_health_monitor.py index 5acfdda5d..143738266 100644 --- a/openstack/tests/unit/network/v2/test_health_monitor.py +++ b/openstack/tests/unit/network/v2/test_health_monitor.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import health_monitor from openstack.tests.unit import base -from openstack.network.v2 import health_monitor IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/network/v2/test_l3_conntrack_helper.py b/openstack/tests/unit/network/v2/test_l3_conntrack_helper.py index c1575f228..38527b50c 100644 --- a/openstack/tests/unit/network/v2/test_l3_conntrack_helper.py +++ b/openstack/tests/unit/network/v2/test_l3_conntrack_helper.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import l3_conntrack_helper from openstack.tests.unit import base -from openstack.network.v2 import l3_conntrack_helper EXAMPLE = { 'id': 'ct_helper_id', diff --git a/openstack/tests/unit/network/v2/test_listener.py b/openstack/tests/unit/network/v2/test_listener.py index 15f598e83..fca356e08 100644 --- a/openstack/tests/unit/network/v2/test_listener.py +++ b/openstack/tests/unit/network/v2/test_listener.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import listener from openstack.tests.unit import base -from openstack.network.v2 import listener IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/network/v2/test_load_balancer.py b/openstack/tests/unit/network/v2/test_load_balancer.py index 4590f513d..b66db3790 100644 --- a/openstack/tests/unit/network/v2/test_load_balancer.py +++ b/openstack/tests/unit/network/v2/test_load_balancer.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import load_balancer from openstack.tests.unit import base -from openstack.network.v2 import load_balancer IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/network/v2/test_metering_label.py b/openstack/tests/unit/network/v2/test_metering_label.py index 8dc4a4a3b..50efac047 100644 --- a/openstack/tests/unit/network/v2/test_metering_label.py +++ b/openstack/tests/unit/network/v2/test_metering_label.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import metering_label from openstack.tests.unit import base -from openstack.network.v2 import metering_label IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/network/v2/test_metering_label_rule.py b/openstack/tests/unit/network/v2/test_metering_label_rule.py index c11f2838b..be005940e 100644 --- a/openstack/tests/unit/network/v2/test_metering_label_rule.py +++ b/openstack/tests/unit/network/v2/test_metering_label_rule.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import metering_label_rule from openstack.tests.unit import base -from openstack.network.v2 import metering_label_rule IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/network/v2/test_network.py b/openstack/tests/unit/network/v2/test_network.py index b6e2d637b..37a51cb82 100644 --- a/openstack/tests/unit/network/v2/test_network.py +++ b/openstack/tests/unit/network/v2/test_network.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import network from openstack.tests.unit import base -from openstack.network.v2 import network IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/network/v2/test_network_ip_availability.py b/openstack/tests/unit/network/v2/test_network_ip_availability.py index 0dc64b788..c40b8b7fb 100644 --- a/openstack/tests/unit/network/v2/test_network_ip_availability.py +++ b/openstack/tests/unit/network/v2/test_network_ip_availability.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import network_ip_availability from openstack.tests.unit import base -from openstack.network.v2 import network_ip_availability IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/network/v2/test_network_segment_range.py b/openstack/tests/unit/network/v2/test_network_segment_range.py index 9a76cc234..2e32f299d 100644 --- a/openstack/tests/unit/network/v2/test_network_segment_range.py +++ b/openstack/tests/unit/network/v2/test_network_segment_range.py @@ -13,9 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import network_segment_range from openstack.tests.unit import base -from openstack.network.v2 import network_segment_range IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/network/v2/test_pool.py b/openstack/tests/unit/network/v2/test_pool.py index acade83da..1dae98a57 100644 --- a/openstack/tests/unit/network/v2/test_pool.py +++ b/openstack/tests/unit/network/v2/test_pool.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import pool from openstack.tests.unit import base -from openstack.network.v2 import pool IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/network/v2/test_pool_member.py b/openstack/tests/unit/network/v2/test_pool_member.py index 156b6beec..9d2df196b 100644 --- a/openstack/tests/unit/network/v2/test_pool_member.py +++ b/openstack/tests/unit/network/v2/test_pool_member.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import pool_member from openstack.tests.unit import base -from openstack.network.v2 import pool_member IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/network/v2/test_port.py b/openstack/tests/unit/network/v2/test_port.py index 9c37fe25d..7fd88b631 100644 --- a/openstack/tests/unit/network/v2/test_port.py +++ b/openstack/tests/unit/network/v2/test_port.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import port from openstack.tests.unit import base -from openstack.network.v2 import port IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/network/v2/test_port_forwarding.py b/openstack/tests/unit/network/v2/test_port_forwarding.py index fd3fbfa7b..8748cae9e 100644 --- a/openstack/tests/unit/network/v2/test_port_forwarding.py +++ b/openstack/tests/unit/network/v2/test_port_forwarding.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import port_forwarding from openstack.tests.unit import base -from openstack.network.v2 import port_forwarding EXAMPLE = { 'id': 'pf_id', diff --git a/openstack/tests/unit/network/v2/test_qos_bandwidth_limit_rule.py b/openstack/tests/unit/network/v2/test_qos_bandwidth_limit_rule.py index cd9f7ef49..581004e10 100644 --- a/openstack/tests/unit/network/v2/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/unit/network/v2/test_qos_bandwidth_limit_rule.py @@ -10,10 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base import uuid from openstack.network.v2 import qos_bandwidth_limit_rule +from openstack.tests.unit import base + EXAMPLE = { 'id': 'IDENTIFIER', diff --git a/openstack/tests/unit/network/v2/test_qos_dscp_marking_rule.py b/openstack/tests/unit/network/v2/test_qos_dscp_marking_rule.py index 9f2bb38b8..02a71a2e4 100644 --- a/openstack/tests/unit/network/v2/test_qos_dscp_marking_rule.py +++ b/openstack/tests/unit/network/v2/test_qos_dscp_marking_rule.py @@ -10,10 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base import uuid from openstack.network.v2 import qos_dscp_marking_rule +from openstack.tests.unit import base + EXAMPLE = { 'id': 'IDENTIFIER', diff --git a/openstack/tests/unit/network/v2/test_qos_minimum_bandwidth_rule.py b/openstack/tests/unit/network/v2/test_qos_minimum_bandwidth_rule.py index e18fa1d85..b42335bd7 100644 --- a/openstack/tests/unit/network/v2/test_qos_minimum_bandwidth_rule.py +++ b/openstack/tests/unit/network/v2/test_qos_minimum_bandwidth_rule.py @@ -10,10 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base import uuid from openstack.network.v2 import qos_minimum_bandwidth_rule +from openstack.tests.unit import base + EXAMPLE = { 'id': 'IDENTIFIER', diff --git a/openstack/tests/unit/network/v2/test_qos_policy.py b/openstack/tests/unit/network/v2/test_qos_policy.py index 7a786e83d..295f56341 100644 --- a/openstack/tests/unit/network/v2/test_qos_policy.py +++ b/openstack/tests/unit/network/v2/test_qos_policy.py @@ -10,10 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base import uuid from openstack.network.v2 import qos_policy +from openstack.tests.unit import base + EXAMPLE = { 'id': 'IDENTIFIER', diff --git a/openstack/tests/unit/network/v2/test_qos_rule_type.py b/openstack/tests/unit/network/v2/test_qos_rule_type.py index bd927c4bb..a10f23aee 100644 --- a/openstack/tests/unit/network/v2/test_qos_rule_type.py +++ b/openstack/tests/unit/network/v2/test_qos_rule_type.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import qos_rule_type from openstack.tests.unit import base -from openstack.network.v2 import qos_rule_type EXAMPLE = { 'type': 'bandwidth_limit', diff --git a/openstack/tests/unit/network/v2/test_quota.py b/openstack/tests/unit/network/v2/test_quota.py index e6b6b7a27..54d7a6949 100644 --- a/openstack/tests/unit/network/v2/test_quota.py +++ b/openstack/tests/unit/network/v2/test_quota.py @@ -10,10 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base - from openstack.network.v2 import quota from openstack import resource +from openstack.tests.unit import base + IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/network/v2/test_rbac_policy.py b/openstack/tests/unit/network/v2/test_rbac_policy.py index d91531956..ea0a9a364 100644 --- a/openstack/tests/unit/network/v2/test_rbac_policy.py +++ b/openstack/tests/unit/network/v2/test_rbac_policy.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import rbac_policy from openstack.tests.unit import base -from openstack.network.v2 import rbac_policy IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/network/v2/test_security_group.py b/openstack/tests/unit/network/v2/test_security_group.py index 661f8aa49..7a4c74163 100644 --- a/openstack/tests/unit/network/v2/test_security_group.py +++ b/openstack/tests/unit/network/v2/test_security_group.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import security_group from openstack.tests.unit import base -from openstack.network.v2 import security_group IDENTIFIER = 'IDENTIFIER' RULES = [ diff --git a/openstack/tests/unit/network/v2/test_security_group_rule.py b/openstack/tests/unit/network/v2/test_security_group_rule.py index c334b654e..cfaf43da0 100644 --- a/openstack/tests/unit/network/v2/test_security_group_rule.py +++ b/openstack/tests/unit/network/v2/test_security_group_rule.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import security_group_rule from openstack.tests.unit import base -from openstack.network.v2 import security_group_rule IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/network/v2/test_segment.py b/openstack/tests/unit/network/v2/test_segment.py index 3dd0c94c0..4130f6650 100644 --- a/openstack/tests/unit/network/v2/test_segment.py +++ b/openstack/tests/unit/network/v2/test_segment.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import segment from openstack.tests.unit import base -from openstack.network.v2 import segment IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/network/v2/test_service_profile.py b/openstack/tests/unit/network/v2/test_service_profile.py index 2e6e26f24..a6be8c079 100644 --- a/openstack/tests/unit/network/v2/test_service_profile.py +++ b/openstack/tests/unit/network/v2/test_service_profile.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import service_profile from openstack.tests.unit import base -from openstack.network.v2 import service_profile IDENTIFIER = 'IDENTIFIER' EXAMPLE_WITH_OPTIONAL = { diff --git a/openstack/tests/unit/network/v2/test_service_provider.py b/openstack/tests/unit/network/v2/test_service_provider.py index 07cdce727..ad578bff9 100644 --- a/openstack/tests/unit/network/v2/test_service_provider.py +++ b/openstack/tests/unit/network/v2/test_service_provider.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import service_provider from openstack.tests.unit import base -from openstack.network.v2 import service_provider IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/network/v2/test_subnet.py b/openstack/tests/unit/network/v2/test_subnet.py index 5f1cb0419..6d87eebba 100644 --- a/openstack/tests/unit/network/v2/test_subnet.py +++ b/openstack/tests/unit/network/v2/test_subnet.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import subnet from openstack.tests.unit import base -from openstack.network.v2 import subnet IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/network/v2/test_subnet_pool.py b/openstack/tests/unit/network/v2/test_subnet_pool.py index 4a13cd34e..a0859b38e 100644 --- a/openstack/tests/unit/network/v2/test_subnet_pool.py +++ b/openstack/tests/unit/network/v2/test_subnet_pool.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import subnet_pool from openstack.tests.unit import base -from openstack.network.v2 import subnet_pool IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/network/v2/test_vpn_service.py b/openstack/tests/unit/network/v2/test_vpn_service.py index 6ae369783..a0bef05f1 100644 --- a/openstack/tests/unit/network/v2/test_vpn_service.py +++ b/openstack/tests/unit/network/v2/test_vpn_service.py @@ -10,9 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base - from openstack.network.v2 import vpn_service +from openstack.tests.unit import base IDENTIFIER = 'IDENTIFIER' diff --git a/tox.ini b/tox.ini index 6a44739cd..64446e384 100644 --- a/tox.ini +++ b/tox.ini @@ -133,7 +133,6 @@ per-file-ignores = openstack/tests/unit/message/*:H306,I100,I201,I202 openstack/tests/unit/load_balancer/*:H306,I100,I201,I202 openstack/tests/unit/compute/*:H306,I100,I201,I202 - openstack/tests/unit/network/*:H306,I100,I201,I202 [flake8:local-plugins] extension = From 239565142975bfa5fa9a1ebd62feead8c9bb27f8 Mon Sep 17 00:00:00 2001 From: Ian Wienand Date: Mon, 19 Apr 2021 14:24:02 +1000 Subject: [PATCH 2810/3836] Allow for override of statsd/influxdb settings per cloud There is currently no way to separate out statsd settings per cloud. While you can set a prefix other than 'openstack.api', this applies to all clouds and groups all the stats together. While this may be what you want, it may also be not what you want :) For example in nodepool, we have multiple cloud providers who each have their own grafana page, so we'd like them all to log themselves to different stats buckets (i.e. set individual prefixes). This allows setting a "metrics" field in each individual cloud entry that overrides the global settings. TBH I don't know if this is important for InfluxDB as well, but I've implemented it for that too for consistency. Test cases are added for the global and merged settings. I've also updated the documentation page a bit. I've separated it into subsections for the three types of stats available. I removed some of the in-depth stuff about logging types which wasn't that clear (I think we can just present what we support and let people decide). Change-Id: I9c3342161a257603f3cfd26bd03f6b71ffbfdd0d --- doc/source/user/guides/stats.rst | 77 +++++++++++++--------- openstack/config/loader.py | 23 +++++-- openstack/tests/unit/config/base.py | 35 ++++++++++ openstack/tests/unit/config/test_config.py | 59 ++++++++++++++++- 4 files changed, 158 insertions(+), 36 deletions(-) diff --git a/doc/source/user/guides/stats.rst b/doc/source/user/guides/stats.rst index 537a12707..19e302f98 100644 --- a/doc/source/user/guides/stats.rst +++ b/doc/source/user/guides/stats.rst @@ -2,28 +2,23 @@ Statistics reporting ==================== -`openstacksdk` offers possibility to report statistics on individual API -requests/responses in different formats. `Statsd` allows reporting of the -response times in the statsd format. `InfluxDB` allows a more event-oriented -reporting of the same data. `Prometheus` reporting is a bit different and -requires the application using SDK to take care of the metrics exporting, while -`openstacksdk` prepares the metrics. - -Due to the nature of the `statsd` protocol lots of tools consuming the metrics -do the data aggregation and processing in the configurable time frame (mean -value calculation for a 1 minute time frame). For the case of periodic tasks -this might not be very useful. A better fit for using `openstacksdk` as a -library is an 'event'-recording, where duration of an individual request is -stored and all required calculations are done if necessary in the monitoring -system based required timeframe, or the data is simply shown as is with no -analytics. A `comparison -`_ article describes -differences in those approaches. - -Simple Usage ------------- - -To receive metrics add a following section to the config file (clouds.yaml): +`openstacksdk` can report statistics on individual API +requests/responses in several different formats. + +Note that metrics will be reported only when corresponding client +libraries (`statsd` for 'statsd' reporting, `influxdb` for influxdb, +etc.). If libraries are not available reporting will be silently +ignored. + +statsd +------ + +`statsd` can be configured via configuration entries or environment +variables. + +A global `metrics` entry defines defaults for all clouds. Each cloud +can specify a `metrics` section to override variables; this may be +useful to separate results reported for each cloud. .. code-block:: yaml @@ -31,12 +26,26 @@ To receive metrics add a following section to the config file (clouds.yaml): statsd: host: __statsd_server_host__ port: __statsd_server_port__ + prefix: __statsd_prefix__ (default 'openstack.api') clouds: - .. + a-cloud: + auth: + ... + metrics: + statsd: + prefix: 'openstack.api.a-cloud' +If the `STATSD_HOST` or `STATSD_PORT` environment variables are set, +they will be taken as the default values (and enable `statsd` +reporting if no other configuration is specified). -In order to enable InfluxDB reporting following configuration need to be done -in the `clouds.yaml` file +InfluxDB +-------- + +`InfluxDB `__ is supported via +configuration in the `metrics` field. Similar to `statsd`, each cloud +can provide it's own `metrics` section to override any global +defaults. .. code-block:: yaml @@ -53,11 +62,6 @@ in the `clouds.yaml` file clouds: .. -Metrics will be reported only when corresponding client libraries ( -`statsd` for 'statsd' reporting, `influxdb` for influxdb reporting -correspondingly). When those libraries are not available reporting will be -silently ignored. - InfluxDB reporting allows setting additional tags into the metrics based on the selected cloud. @@ -69,3 +73,16 @@ selected cloud. ... additional_metric_tags: environment: production + +prometheus +---------- +.. + NOTE(ianw) 2021-04-19 : examples here would be great; this is just terse + description taken from + https://review.opendev.org/c/openstack/openstacksdk/+/614834 + +The prometheus support does not read from config, and does not run an +http service since OpenstackSDK is a library. It is expected that an +application that uses OpenstackSDK and wants request stats be +collected will pass a `prometheus_client.CollectorRegistry` to +`collector_registry`. diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 3267a9967..c2729654c 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -1145,6 +1145,21 @@ def get_one( if not prefer_ipv6: force_ipv4 = True + # Override global metrics config with more specific per-cloud + # details. + metrics_config = config.get('metrics', {}) + statsd_config = metrics_config.get('statsd', {}) + statsd_host = statsd_config.get('host') or self._statsd_host + statsd_port = statsd_config.get('port') or self._statsd_port + statsd_prefix = statsd_config.get('prefix') or self._statsd_prefix + influxdb_config = metrics_config.get('influxdb', {}) + if influxdb_config: + merged_influxdb = copy.deepcopy(self._influxdb_config) + merged_influxdb.update(influxdb_config) + influxdb_config = merged_influxdb + else: + influxdb_config = self._influxdb_config + if cloud is None: cloud_name = '' else: @@ -1167,10 +1182,10 @@ def get_one( cache_class=self._cache_class, cache_arguments=self._cache_arguments, password_callback=self._pw_callback, - statsd_host=self._statsd_host, - statsd_port=self._statsd_port, - statsd_prefix=self._statsd_prefix, - influxdb_config=self._influxdb_config, + statsd_host=statsd_host, + statsd_port=statsd_port, + statsd_prefix=statsd_prefix, + influxdb_config=influxdb_config, ) # TODO(mordred) Backwards compat for OSC transition get_one_cloud = get_one diff --git a/openstack/tests/unit/config/base.py b/openstack/tests/unit/config/base.py index 81102cea0..5d353588a 100644 --- a/openstack/tests/unit/config/base.py +++ b/openstack/tests/unit/config/base.py @@ -47,6 +47,22 @@ 'client': { 'force_ipv4': True, }, + 'metrics': { + 'statsd': { + 'host': '127.0.0.1', + 'port': '1234' + }, + 'influxdb': { + 'host': '127.0.0.1', + 'port': '1234', + 'use_udp': True, + 'username': 'username', + 'password': 'password', + 'database': 'database', + 'measurement': 'measurement.name', + 'timeout': 10, + } + }, 'clouds': { '_test-cloud_': { 'profile': '_test_cloud_in_our_cloud', @@ -172,6 +188,25 @@ 'domain-id': '12345', }, }, + '_test-cloud-override-metrics': { + 'auth': { + 'auth_url': 'http://example.com/v2', + 'username': 'testuser', + 'password': 'testpass', + }, + 'metrics': { + 'statsd': { + 'host': '127.0.0.1', + 'port': 4321, + 'prefix': 'statsd.override.prefix' + }, + 'influxdb': { + 'username': 'override-username', + 'password': 'override-password', + 'database': 'override-database', + } + }, + }, }, 'ansible': { 'expand-hostvars': False, diff --git a/openstack/tests/unit/config/test_config.py b/openstack/tests/unit/config/test_config.py index f697c03da..392d566c5 100644 --- a/openstack/tests/unit/config/test_config.py +++ b/openstack/tests/unit/config/test_config.py @@ -313,7 +313,7 @@ def test_only_secure_yaml(self): def test_get_cloud_names(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], secure_files=[self.no_yaml]) - self.assertEqual( + self.assertCountEqual( ['_test-cloud-domain-id_', '_test-cloud-domain-scoped_', '_test-cloud-int-project_', @@ -323,8 +323,9 @@ def test_get_cloud_names(self): '_test_cloud_hyphenated', '_test_cloud_no_vendor', '_test_cloud_regions', + '_test-cloud-override-metrics', ], - sorted(c.get_cloud_names())) + c.get_cloud_names()) c = config.OpenStackConfig(config_files=[self.no_yaml], vendor_files=[self.no_yaml], secure_files=[self.no_yaml]) @@ -513,6 +514,60 @@ def test_set_auth_cache(self, kr_mock): 'openstacksdk', region._auth.get_cache_id(), region._auth.get_auth_state()) + def test_metrics_global(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + secure_files=[self.secure_yaml]) + self.assertIsInstance(c.cloud_config, dict) + cc = c.get_one('_test-cloud_') + statsd = { + 'host': '127.0.0.1', + 'port': '1234', + } + # NOTE(ianw) we don't test/call get__client() because we + # don't want to instantiate the client, which tries to + # connect / do hostname lookups. + self.assertEqual(statsd['host'], cc._statsd_host) + self.assertEqual(statsd['port'], cc._statsd_port) + self.assertEqual('openstack.api', cc.get_statsd_prefix()) + influxdb = { + 'use_udp': True, + 'host': '127.0.0.1', + 'port': '1234', + 'username': 'username', + 'password': 'password', + 'database': 'database', + 'measurement': 'measurement.name', + 'timeout': 10 + } + self.assertEqual(influxdb, cc._influxdb_config) + + def test_metrics_override(self): + c = config.OpenStackConfig(config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + secure_files=[self.secure_yaml]) + self.assertIsInstance(c.cloud_config, dict) + cc = c.get_one('_test-cloud-override-metrics') + statsd = { + 'host': '127.0.0.1', + 'port': '4321', + 'prefix': 'statsd.override.prefix' + } + self.assertEqual(statsd['host'], cc._statsd_host) + self.assertEqual(statsd['port'], cc._statsd_port) + self.assertEqual(statsd['prefix'], cc.get_statsd_prefix()) + influxdb = { + 'use_udp': True, + 'host': '127.0.0.1', + 'port': '1234', + 'username': 'override-username', + 'password': 'override-password', + 'database': 'override-database', + 'measurement': 'measurement.name', + 'timeout': 10 + } + self.assertEqual(influxdb, cc._influxdb_config) + class TestExcludedFormattedConfigValue(base.TestCase): # verify https://storyboard.openstack.org/#!/story/1635696 From f589f21b3cfcd88452059bcbdf87814582aaa9d8 Mon Sep 17 00:00:00 2001 From: Ian Wienand Date: Tue, 20 Apr 2021 10:04:57 +1000 Subject: [PATCH 2811/3836] statsd: use timedelta and pipeline statsd timing() can take a timedelta directly; avoid the conversion and let the library handle it. We can also put the multiple results into a pipeline and save a network call/packet for each API action by grouping the two stats sent. Change-Id: Ib18df2cd8e06d9d1bf59fac7eb940b5158b8bd96 --- openstack/proxy.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openstack/proxy.py b/openstack/proxy.py index 72527f7e2..202628b65 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -214,12 +214,12 @@ def _report_stats_statsd(self, response, url=None, method=None, exc=None): key = '.'.join( [self._statsd_prefix, self.service_type, method] + name_parts) - if response is not None: - duration = int(response.elapsed.total_seconds() * 1000) - self._statsd_client.timing(key, duration) - self._statsd_client.incr(key) - elif exc is not None: - self._statsd_client.incr('%s.failed' % key) + with self._statsd_client.pipeline() as pipe: + if response is not None: + pipe.timing(key, response.elapsed) + pipe.incr(key) + elif exc is not None: + pipe.incr('%s.failed' % key) def _report_stats_prometheus(self, response, url=None, method=None, exc=None): From e6d4c4c9594b296b3b2b3851f1391db0fff24583 Mon Sep 17 00:00:00 2001 From: songwenping Date: Tue, 20 Apr 2021 01:37:24 +0000 Subject: [PATCH 2812/3836] Use py3 as the default runtime for tox Moving on py3 as the default runtime for tox to avoid to update this at each new cycle. Wallaby support officially the following runtimes [1]: - Python 3.6 - Python 3.8 During Victoria Python 3.7 was used as the default runtime [2] however this version isn't longer officially supported. [1] https://governance.openstack.org/tc/reference/runtimes/wallaby.html#python-runtimes-for-wallaby [2] https://governance.openstack.org/tc/reference/runtimes/victoria.html#python-runtimes-for-victoria Change-Id: Ic55a211381b901f2203988c0fac4e7545666f382 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 6786233ae..90bb630b9 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 3.9.0 -envlist = pep8,py38 +envlist = pep8,py3 skipsdist = True ignore_basepython_conflict = True From bf73fd91ad5896f43e5d645a55b2661ed5bace4e Mon Sep 17 00:00:00 2001 From: Kajal Sah Date: Thu, 8 Apr 2021 22:13:56 +0530 Subject: [PATCH 2813/3836] Adds Node Vendor passthru Implements: Adds node vendor passthru Task: #40959 Story: #2008193 Change-Id: Ie63d8232fa86d93f0ae71d2e1bd808c80c8c93cf --- openstack/baremetal/v1/_proxy.py | 19 +++++++ openstack/baremetal/v1/node.py | 49 +++++++++++++++++++ .../tests/unit/baremetal/v1/test_node.py | 47 ++++++++++++++++++ ...node-vendor_passthru-29b384cadf795b48.yaml | 4 ++ 4 files changed, 119 insertions(+) create mode 100644 releasenotes/notes/add-node-vendor_passthru-29b384cadf795b48.yaml diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 6ac6728e3..2190fcb70 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -1014,6 +1014,25 @@ def remove_node_trait(self, node, trait, ignore_missing=True): res = self._get_resource(_node.Node, node) return res.remove_trait(self, trait, ignore_missing=ignore_missing) + def call_node_vendor_passthru(self, node, verb, method, body=None): + """Calls vendor_passthru for a node. + + :param session: The session to use for making this request. + :param verb: The HTTP verb, one of GET, SET, POST, DELETE. + :param method: The method to call using vendor_passthru. + :param body: The JSON body in the HTTP call. + """ + res = self._get_resource(_node.Node, node) + return res.call_vendor_passthru(self, verb, method, body) + + def list_node_vendor_passthru(self, node): + """Lists vendor_passthru for a node. + + :param session: The session to use for making this request. + """ + res = self._get_resource(_node.Node, node) + return res.list_vendor_passthru(self) + def set_node_traits(self, node, traits): """Set traits for a node. diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 72c50e19a..4aebb5d67 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -920,6 +920,55 @@ def set_traits(self, session, traits): self.traits = traits + def call_vendor_passthru(self, session, verb, method, body=None): + """Call a vendor passthru method. + + :param session: The session to use for making this request. + :param verb: The HTTP verb, one of GET, SET, POST, DELETE. + :param method: The method to call using vendor_passthru. + :param body: The JSON body in the HTTP call. + :returns: The HTTP response. + """ + session = self._get_session(session) + version = self._get_microversion_for(session, 'commit') + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'vendor_passthru?method={}' + .format(method)) + + call = getattr(session, verb.lower()) + response = call( + request.url, json=body, + headers=request.headers, microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + msg = ("Failed to call vendor_passthru for node {node}, verb {verb}" + " and method {method}" + .format(node=self.id, verb=verb, method=method)) + exceptions.raise_from_response(response, error_message=msg) + + return response + + def list_vendor_passthru(self, session): + """List vendor passthru methods. + + :param session: The session to use for making this request. + :returns: The HTTP response. + """ + session = self._get_session(session) + version = self._get_microversion_for(session, 'fetch') + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'vendor_passthru/methods') + + response = session.get( + request.url, headers=request.headers, microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + msg = ("Failed to list vendor_passthru methods for node {node}" + .format(node=self.id)) + exceptions.raise_from_response(response, error_message=msg) + + return response.json() + def patch(self, session, patch=None, prepend_key=True, has_body=True, retry_on_conflict=None, base_path=None, reset_interfaces=None): diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index b00ab3309..c1c90967d 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -840,3 +840,50 @@ def test_timeout(self, mock_fetch): self.assertRaises(exceptions.ResourceTimeout, self.node.wait_for_power_state, self.session, 'power off', timeout=0.001) + + +@mock.patch.object(utils, 'pick_microversion', lambda session, v: v) +@mock.patch.object(node.Node, 'fetch', lambda self, session: self) +@mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) +class TestNodePassthru(object): + def setUp(self): + super(TestNodePassthru, self).setUp() + self.node = node.Node(**FAKE) + self.session = node.Mock(spec=adapter.Adapter, + default_microversion='1.37') + self.session.log = mock.Mock() + + def test_get_passthru(self): + self.node.call_vendor_passthru(self.session, "GET", "test_method") + self.session.get.assert_called_once_with( + 'nodes/%s/vendor_passthru?method=test_method' % self.node.id, + headers=mock.ANY, microversion='1.37', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + def test_post_passthru(self): + self.node.call_vendor_passthru(self.session, "POST", "test_method") + self.session.post.assert_called_once_with( + 'nodes/%s/vendor_passthru?method=test_method' % self.node.id, + headers=mock.ANY, microversion='1.37', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + def test_put_passthru(self): + self.node.call_vendor_passthru(self.session, "PUT", "test_method") + self.session.put.assert_called_once_with( + 'nodes/%s/vendor_passthru?method=test_method' % self.node.id, + headers=mock.ANY, microversion='1.37', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + def test_delete_passthru(self): + self.node.call_vendor_passthru(self.session, "DELETE", "test_method") + self.session.delete.assert_called_once_with( + 'nodes/%s/vendor_passthru?method=test_method' % self.node.id, + headers=mock.ANY, microversion='1.37', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + def test_list_passthru(self): + self.node.list_vendor_passthru(self.session) + self.session.get.assert_called_once_with( + 'nodes/%s/vendor_passthru/methods' % self.node.id, + headers=mock.ANY, microversion='1.37', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) diff --git a/releasenotes/notes/add-node-vendor_passthru-29b384cadf795b48.yaml b/releasenotes/notes/add-node-vendor_passthru-29b384cadf795b48.yaml new file mode 100644 index 000000000..682ee4943 --- /dev/null +++ b/releasenotes/notes/add-node-vendor_passthru-29b384cadf795b48.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add node vendor_passthru interface for Ironic API. From e5381d1296bba744ef77dcd86d3bea32327b5c17 Mon Sep 17 00:00:00 2001 From: Slawek Kaplonski Date: Tue, 22 Dec 2020 15:26:30 +0100 Subject: [PATCH 2814/3836] Allow unknown attributes to be included in request body It is optional for resources to send in request's body attributes which aren't included in mapping. This is needed for Neutron to allow sending extra arguments in POST/PUT requests to Neutron. If that will be possible in SDK and OSC, Neutron team should be finally able to deprecate (and drop) support for neutronclient CLI. Change-Id: Ia3fd1052f438aedc6bc89bcca71834bcbafec9b9 --- openstack/network/v2/_base.py | 2 + openstack/network/v2/address_group.py | 2 + openstack/network/v2/address_scope.py | 2 + openstack/network/v2/agent.py | 2 + .../network/v2/auto_allocated_topology.py | 2 + openstack/network/v2/availability_zone.py | 2 + openstack/network/v2/extension.py | 2 + openstack/network/v2/firewall_group.py | 2 + openstack/network/v2/firewall_policy.py | 2 + openstack/network/v2/firewall_rule.py | 2 + openstack/network/v2/flavor.py | 2 + openstack/network/v2/health_monitor.py | 2 + openstack/network/v2/listener.py | 2 + openstack/network/v2/load_balancer.py | 2 + openstack/network/v2/metering_label.py | 2 + openstack/network/v2/metering_label_rule.py | 2 + .../network/v2/network_ip_availability.py | 2 + openstack/network/v2/network_segment_range.py | 2 + openstack/network/v2/pool.py | 2 + openstack/network/v2/pool_member.py | 2 + openstack/network/v2/port_forwarding.py | 2 + .../network/v2/qos_bandwidth_limit_rule.py | 2 + openstack/network/v2/qos_dscp_marking_rule.py | 2 + .../network/v2/qos_minimum_bandwidth_rule.py | 2 + openstack/network/v2/qos_policy.py | 2 + openstack/network/v2/qos_rule_type.py | 2 + openstack/network/v2/quota.py | 2 + openstack/network/v2/rbac_policy.py | 2 + openstack/network/v2/segment.py | 2 + openstack/network/v2/service_profile.py | 2 + openstack/network/v2/service_provider.py | 2 + openstack/network/v2/subnet_pool.py | 2 + openstack/network/v2/trunk.py | 2 + openstack/network/v2/vpn_service.py | 2 + openstack/resource.py | 31 +++++-- openstack/tests/unit/test_resource.py | 82 +++++++++++++++++++ 36 files changed, 176 insertions(+), 5 deletions(-) diff --git a/openstack/network/v2/_base.py b/openstack/network/v2/_base.py index 147563d26..cee129f0e 100644 --- a/openstack/network/v2/_base.py +++ b/openstack/network/v2/_base.py @@ -16,6 +16,8 @@ class NetworkResource(resource.Resource): #: Revision number of the resource. *Type: int* revision_number = resource.Body('revision_number', type=int) + _allow_unknown_attrs_in_body = True + def _prepare_request(self, requires_id=None, prepend_key=False, patch=False, base_path=None, params=None, if_revision=None, **kwargs): diff --git a/openstack/network/v2/address_group.py b/openstack/network/v2/address_group.py index 08a3dcc64..f9f5d6c94 100644 --- a/openstack/network/v2/address_group.py +++ b/openstack/network/v2/address_group.py @@ -28,6 +28,8 @@ class AddressGroup(resource.Resource): allow_delete = True allow_list = True + _allow_unknown_attrs_in_body = True + _query_mapping = resource.QueryParameters( "sort_key", "sort_dir", 'name', 'description', diff --git a/openstack/network/v2/address_scope.py b/openstack/network/v2/address_scope.py index ff256e035..36b13356a 100644 --- a/openstack/network/v2/address_scope.py +++ b/openstack/network/v2/address_scope.py @@ -19,6 +19,8 @@ class AddressScope(resource.Resource): resources_key = 'address_scopes' base_path = '/address-scopes' + _allow_unknown_attrs_in_body = True + # capabilities allow_create = True allow_fetch = True diff --git a/openstack/network/v2/agent.py b/openstack/network/v2/agent.py index 09054d540..97c471187 100644 --- a/openstack/network/v2/agent.py +++ b/openstack/network/v2/agent.py @@ -20,6 +20,8 @@ class Agent(resource.Resource): resources_key = 'agents' base_path = '/agents' + _allow_unknown_attrs_in_body = True + # capabilities allow_create = False allow_fetch = True diff --git a/openstack/network/v2/auto_allocated_topology.py b/openstack/network/v2/auto_allocated_topology.py index 66ea343e9..ee52de2d6 100644 --- a/openstack/network/v2/auto_allocated_topology.py +++ b/openstack/network/v2/auto_allocated_topology.py @@ -18,6 +18,8 @@ class AutoAllocatedTopology(resource.Resource): resource_key = 'auto_allocated_topology' base_path = '/auto-allocated-topology' + _allow_unknown_attrs_in_body = True + # Capabilities allow_create = False allow_fetch = True diff --git a/openstack/network/v2/availability_zone.py b/openstack/network/v2/availability_zone.py index 9ff48135e..3e5449d45 100644 --- a/openstack/network/v2/availability_zone.py +++ b/openstack/network/v2/availability_zone.py @@ -18,6 +18,8 @@ class AvailabilityZone(_resource.Resource): resources_key = 'availability_zones' base_path = '/availability_zones' + _allow_unknown_attrs_in_body = True + # capabilities allow_create = False allow_fetch = False diff --git a/openstack/network/v2/extension.py b/openstack/network/v2/extension.py index 00c91a7f8..6bb86fa16 100644 --- a/openstack/network/v2/extension.py +++ b/openstack/network/v2/extension.py @@ -18,6 +18,8 @@ class Extension(resource.Resource): resources_key = 'extensions' base_path = '/extensions' + _allow_unknown_attrs_in_body = True + # capabilities allow_fetch = True allow_list = True diff --git a/openstack/network/v2/firewall_group.py b/openstack/network/v2/firewall_group.py index 2ed0ad2d5..93db5b404 100644 --- a/openstack/network/v2/firewall_group.py +++ b/openstack/network/v2/firewall_group.py @@ -21,6 +21,8 @@ class FirewallGroup(resource.Resource): resources_key = 'firewall_groups' base_path = '/fwaas/firewall_groups' + _allow_unknown_attrs_in_body = True + # capabilities allow_create = True allow_fetch = True diff --git a/openstack/network/v2/firewall_policy.py b/openstack/network/v2/firewall_policy.py index c8fb70f9d..d4dc182e8 100644 --- a/openstack/network/v2/firewall_policy.py +++ b/openstack/network/v2/firewall_policy.py @@ -23,6 +23,8 @@ class FirewallPolicy(resource.Resource): resources_key = 'firewall_policies' base_path = '/fwaas/firewall_policies' + _allow_unknown_attrs_in_body = True + # capabilities allow_create = True allow_fetch = True diff --git a/openstack/network/v2/firewall_rule.py b/openstack/network/v2/firewall_rule.py index 786d86e8b..8d5ed5056 100644 --- a/openstack/network/v2/firewall_rule.py +++ b/openstack/network/v2/firewall_rule.py @@ -21,6 +21,8 @@ class FirewallRule(resource.Resource): resources_key = 'firewall_rules' base_path = '/fwaas/firewall_rules' + _allow_unknown_attrs_in_body = True + # capabilities allow_create = True allow_fetch = True diff --git a/openstack/network/v2/flavor.py b/openstack/network/v2/flavor.py index 987a3483b..361465c4d 100644 --- a/openstack/network/v2/flavor.py +++ b/openstack/network/v2/flavor.py @@ -19,6 +19,8 @@ class Flavor(resource.Resource): resources_key = 'flavors' base_path = '/flavors' + _allow_unknown_attrs_in_body = True + # capabilities allow_create = True allow_fetch = True diff --git a/openstack/network/v2/health_monitor.py b/openstack/network/v2/health_monitor.py index c8cfa59f0..6fecb07d5 100644 --- a/openstack/network/v2/health_monitor.py +++ b/openstack/network/v2/health_monitor.py @@ -18,6 +18,8 @@ class HealthMonitor(resource.Resource): resources_key = 'healthmonitors' base_path = '/lbaas/healthmonitors' + _allow_unknown_attrs_in_body = True + # capabilities allow_create = True allow_fetch = True diff --git a/openstack/network/v2/listener.py b/openstack/network/v2/listener.py index 64595b5f4..376650a0e 100644 --- a/openstack/network/v2/listener.py +++ b/openstack/network/v2/listener.py @@ -18,6 +18,8 @@ class Listener(resource.Resource): resources_key = 'listeners' base_path = '/lbaas/listeners' + _allow_unknown_attrs_in_body = True + # capabilities allow_create = True allow_fetch = True diff --git a/openstack/network/v2/load_balancer.py b/openstack/network/v2/load_balancer.py index ab88913c9..2d90a34df 100644 --- a/openstack/network/v2/load_balancer.py +++ b/openstack/network/v2/load_balancer.py @@ -18,6 +18,8 @@ class LoadBalancer(resource.Resource): resources_key = 'loadbalancers' base_path = '/lbaas/loadbalancers' + _allow_unknown_attrs_in_body = True + # capabilities allow_create = True allow_fetch = True diff --git a/openstack/network/v2/metering_label.py b/openstack/network/v2/metering_label.py index 31f2b51c8..fbd6f55f0 100644 --- a/openstack/network/v2/metering_label.py +++ b/openstack/network/v2/metering_label.py @@ -18,6 +18,8 @@ class MeteringLabel(resource.Resource): resources_key = 'metering_labels' base_path = '/metering/metering-labels' + _allow_unknown_attrs_in_body = True + # capabilities allow_create = True allow_fetch = True diff --git a/openstack/network/v2/metering_label_rule.py b/openstack/network/v2/metering_label_rule.py index 3016709d9..445a8df35 100644 --- a/openstack/network/v2/metering_label_rule.py +++ b/openstack/network/v2/metering_label_rule.py @@ -18,6 +18,8 @@ class MeteringLabelRule(resource.Resource): resources_key = 'metering_label_rules' base_path = '/metering/metering-label-rules' + _allow_unknown_attrs_in_body = True + # capabilities allow_create = True allow_fetch = True diff --git a/openstack/network/v2/network_ip_availability.py b/openstack/network/v2/network_ip_availability.py index c2946b6a6..838a6b8f9 100644 --- a/openstack/network/v2/network_ip_availability.py +++ b/openstack/network/v2/network_ip_availability.py @@ -19,6 +19,8 @@ class NetworkIPAvailability(resource.Resource): base_path = '/network-ip-availabilities' name_attribute = 'network_name' + _allow_unknown_attrs_in_body = True + # capabilities allow_create = False allow_fetch = True diff --git a/openstack/network/v2/network_segment_range.py b/openstack/network/v2/network_segment_range.py index 2512f242d..bb7e7ff41 100644 --- a/openstack/network/v2/network_segment_range.py +++ b/openstack/network/v2/network_segment_range.py @@ -21,6 +21,8 @@ class NetworkSegmentRange(resource.Resource): resources_key = 'network_segment_ranges' base_path = '/network_segment_ranges' + _allow_unknown_attrs_in_body = True + # capabilities allow_create = True allow_fetch = True diff --git a/openstack/network/v2/pool.py b/openstack/network/v2/pool.py index 4e463c92f..bc72653ac 100644 --- a/openstack/network/v2/pool.py +++ b/openstack/network/v2/pool.py @@ -18,6 +18,8 @@ class Pool(resource.Resource): resources_key = 'pools' base_path = '/lbaas/pools' + _allow_unknown_attrs_in_body = True + # capabilities allow_create = True allow_fetch = True diff --git a/openstack/network/v2/pool_member.py b/openstack/network/v2/pool_member.py index 909bbe21f..3f872ce28 100644 --- a/openstack/network/v2/pool_member.py +++ b/openstack/network/v2/pool_member.py @@ -18,6 +18,8 @@ class PoolMember(resource.Resource): resources_key = 'members' base_path = '/lbaas/pools/%(pool_id)s/members' + _allow_unknown_attrs_in_body = True + # capabilities allow_create = True allow_fetch = True diff --git a/openstack/network/v2/port_forwarding.py b/openstack/network/v2/port_forwarding.py index 26b6c2e4b..aca7873fc 100644 --- a/openstack/network/v2/port_forwarding.py +++ b/openstack/network/v2/port_forwarding.py @@ -20,6 +20,8 @@ class PortForwarding(resource.Resource): resources_key = 'port_forwardings' base_path = '/floatingips/%(floatingip_id)s/port_forwardings' + _allow_unknown_attrs_in_body = True + # capabilities allow_create = True allow_fetch = True diff --git a/openstack/network/v2/qos_bandwidth_limit_rule.py b/openstack/network/v2/qos_bandwidth_limit_rule.py index d10939d36..33e24b69d 100644 --- a/openstack/network/v2/qos_bandwidth_limit_rule.py +++ b/openstack/network/v2/qos_bandwidth_limit_rule.py @@ -18,6 +18,8 @@ class QoSBandwidthLimitRule(resource.Resource): resources_key = 'bandwidth_limit_rules' base_path = '/qos/policies/%(qos_policy_id)s/bandwidth_limit_rules' + _allow_unknown_attrs_in_body = True + # capabilities allow_create = True allow_fetch = True diff --git a/openstack/network/v2/qos_dscp_marking_rule.py b/openstack/network/v2/qos_dscp_marking_rule.py index fbdf86de1..ac89a1afb 100644 --- a/openstack/network/v2/qos_dscp_marking_rule.py +++ b/openstack/network/v2/qos_dscp_marking_rule.py @@ -18,6 +18,8 @@ class QoSDSCPMarkingRule(resource.Resource): resources_key = 'dscp_marking_rules' base_path = '/qos/policies/%(qos_policy_id)s/dscp_marking_rules' + _allow_unknown_attrs_in_body = True + # capabilities allow_create = True allow_fetch = True diff --git a/openstack/network/v2/qos_minimum_bandwidth_rule.py b/openstack/network/v2/qos_minimum_bandwidth_rule.py index 06d2ce75c..bd22fd01a 100644 --- a/openstack/network/v2/qos_minimum_bandwidth_rule.py +++ b/openstack/network/v2/qos_minimum_bandwidth_rule.py @@ -18,6 +18,8 @@ class QoSMinimumBandwidthRule(resource.Resource): resources_key = 'minimum_bandwidth_rules' base_path = '/qos/policies/%(qos_policy_id)s/minimum_bandwidth_rules' + _allow_unknown_attrs_in_body = True + # capabilities allow_create = True allow_fetch = True diff --git a/openstack/network/v2/qos_policy.py b/openstack/network/v2/qos_policy.py index 4b3584c5d..7ca983a7f 100644 --- a/openstack/network/v2/qos_policy.py +++ b/openstack/network/v2/qos_policy.py @@ -19,6 +19,8 @@ class QoSPolicy(resource.Resource, resource.TagMixin): resources_key = 'policies' base_path = '/qos/policies' + _allow_unknown_attrs_in_body = True + # capabilities allow_create = True allow_fetch = True diff --git a/openstack/network/v2/qos_rule_type.py b/openstack/network/v2/qos_rule_type.py index 5f771a631..99e58f5f6 100644 --- a/openstack/network/v2/qos_rule_type.py +++ b/openstack/network/v2/qos_rule_type.py @@ -18,6 +18,8 @@ class QoSRuleType(resource.Resource): resources_key = 'rule_types' base_path = '/qos/rule-types' + _allow_unknown_attrs_in_body = True + # capabilities allow_create = False allow_fetch = True diff --git a/openstack/network/v2/quota.py b/openstack/network/v2/quota.py index fbda19bc0..ddbd36935 100644 --- a/openstack/network/v2/quota.py +++ b/openstack/network/v2/quota.py @@ -18,6 +18,8 @@ class Quota(resource.Resource): resources_key = 'quotas' base_path = '/quotas' + _allow_unknown_attrs_in_body = True + # capabilities allow_fetch = True allow_commit = True diff --git a/openstack/network/v2/rbac_policy.py b/openstack/network/v2/rbac_policy.py index 3fdb158fa..85a1e1abf 100644 --- a/openstack/network/v2/rbac_policy.py +++ b/openstack/network/v2/rbac_policy.py @@ -18,6 +18,8 @@ class RBACPolicy(resource.Resource): resources_key = 'rbac_policies' base_path = '/rbac-policies' + _allow_unknown_attrs_in_body = True + # capabilities allow_create = True allow_fetch = True diff --git a/openstack/network/v2/segment.py b/openstack/network/v2/segment.py index ce6aa2710..81a18996d 100644 --- a/openstack/network/v2/segment.py +++ b/openstack/network/v2/segment.py @@ -18,6 +18,8 @@ class Segment(resource.Resource): resources_key = 'segments' base_path = '/segments' + _allow_unknown_attrs_in_body = True + # capabilities allow_create = True allow_fetch = True diff --git a/openstack/network/v2/service_profile.py b/openstack/network/v2/service_profile.py index 78178a95e..59532724b 100644 --- a/openstack/network/v2/service_profile.py +++ b/openstack/network/v2/service_profile.py @@ -18,6 +18,8 @@ class ServiceProfile(resource.Resource): resources_key = 'service_profiles' base_path = '/service_profiles' + _allow_unknown_attrs_in_body = True + # capabilities allow_create = True allow_fetch = True diff --git a/openstack/network/v2/service_provider.py b/openstack/network/v2/service_provider.py index 1b19b6002..a5e9260ee 100644 --- a/openstack/network/v2/service_provider.py +++ b/openstack/network/v2/service_provider.py @@ -17,6 +17,8 @@ class ServiceProvider(resource.Resource): resources_key = 'service_providers' base_path = '/service-providers' + _allow_unknown_attrs_in_body = True + # Capabilities allow_create = False allow_fetch = False diff --git a/openstack/network/v2/subnet_pool.py b/openstack/network/v2/subnet_pool.py index 953aa44ca..734197e70 100644 --- a/openstack/network/v2/subnet_pool.py +++ b/openstack/network/v2/subnet_pool.py @@ -18,6 +18,8 @@ class SubnetPool(resource.Resource, resource.TagMixin): resources_key = 'subnetpools' base_path = '/subnetpools' + _allow_unknown_attrs_in_body = True + # capabilities allow_create = True allow_fetch = True diff --git a/openstack/network/v2/trunk.py b/openstack/network/v2/trunk.py index 94fb2fda3..b0e5aeb15 100644 --- a/openstack/network/v2/trunk.py +++ b/openstack/network/v2/trunk.py @@ -20,6 +20,8 @@ class Trunk(resource.Resource, resource.TagMixin): resources_key = 'trunks' base_path = '/trunks' + _allow_unknown_attrs_in_body = True + # capabilities allow_create = True allow_fetch = True diff --git a/openstack/network/v2/vpn_service.py b/openstack/network/v2/vpn_service.py index d9c1483ec..d24f9980a 100644 --- a/openstack/network/v2/vpn_service.py +++ b/openstack/network/v2/vpn_service.py @@ -20,6 +20,8 @@ class VPNService(resource.Resource): resources_key = 'vpnservices' base_path = '/vpn/vpnservices' + _allow_unknown_attrs_in_body = True + # capabilities allow_create = True allow_fetch = True diff --git a/openstack/resource.py b/openstack/resource.py index 035b08be5..5d9c246b5 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -491,6 +491,8 @@ class Resource(dict): _original_body = None _delete_response_class = None _store_unknown_attrs_as_properties = False + _allow_unknown_attrs_in_body = False + _unknown_attrs_in_body = None # Placeholder for aliases as dict of {__alias__:__original} _attr_aliases = {} @@ -510,11 +512,17 @@ def __init__(self, _synchronized=False, connection=None, **attrs): """ self._connection = connection self.microversion = attrs.pop('microversion', None) + + self._unknown_attrs_in_body = {} + # NOTE: _collect_attrs modifies **attrs in place, removing # items as they match up with any of the body, header, # or uri mappings. body, header, uri, computed = self._collect_attrs(attrs) + if self._allow_unknown_attrs_in_body: + self._unknown_attrs_in_body.update(attrs) + self._body = _ComponentManager( attributes=body, synchronized=_synchronized) @@ -630,6 +638,11 @@ def __getattribute__(self, name): # Hmm - not found. But hey, the alias exists... return object.__getattribute__( self, self._attr_aliases[name]) + if self._allow_unknown_attrs_in_body: + # Last chance, maybe it's in body as attribute which isn't + # in the mapping at all... + if name in self._unknown_attrs_in_body: + return self._unknown_attrs_in_body[name] raise e def __getitem__(self, name): @@ -654,6 +667,9 @@ def __setitem__(self, name, value): if isinstance(real_item, _BaseComponent): self.__setattr__(name, value) else: + if self._allow_unknown_attrs_in_body: + self._unknown_attrs_in_body[name] = value + return raise KeyError( "{name} is not found. {module}.{cls} objects do not support" " setting arbitrary keys through the" @@ -740,9 +756,12 @@ def _collect_attrs(self, attrs): header = self._consume_header_attrs(attrs) uri = self._consume_uri_attrs(attrs) - if attrs and self._store_unknown_attrs_as_properties: - # Keep also remaining (unknown) attributes - body = self._pack_attrs_under_properties(body, attrs) + if attrs: + if self._allow_unknown_attrs_in_body: + body.update(attrs) + elif self._store_unknown_attrs_as_properties: + # Keep also remaining (unknown) attributes + body = self._pack_attrs_under_properties(body, attrs) if any([body, header, uri]): attrs = self._compute_attributes(body, header, uri) @@ -1170,8 +1189,10 @@ def _translate_response(self, response, has_body=None, error_message=None): body.pop("self", None) body_attrs = self._consume_body_attrs(body) - - if self._store_unknown_attrs_as_properties: + if self._allow_unknown_attrs_in_body: + body_attrs.update(body) + self._unknown_attrs_in_body.update(body) + elif self._store_unknown_attrs_as_properties: body_attrs = self._pack_attrs_under_properties( body_attrs, body) diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index b616d6b4a..d77b9ad72 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1368,6 +1368,88 @@ class Test(resource.Resource): sot.properties ) + def test_unknown_attrs_in_body_create(self): + class Test(resource.Resource): + known_param = resource.Body("known_param") + _allow_unknown_attrs_in_body = True + + sot = Test.new(**{ + 'known_param': 'v1', + 'unknown_param': 'v2' + }) + self.assertEqual('v1', sot.known_param) + self.assertEqual('v2', sot.unknown_param) + + def test_unknown_attrs_in_body_not_stored(self): + class Test(resource.Resource): + known_param = resource.Body("known_param") + properties = resource.Body("properties") + + sot = Test.new(**{ + 'known_param': 'v1', + 'unknown_param': 'v2' + }) + self.assertEqual('v1', sot.known_param) + self.assertNotIn('unknown_param', sot) + + def test_unknown_attrs_in_body_set(self): + class Test(resource.Resource): + known_param = resource.Body("known_param") + _allow_unknown_attrs_in_body = True + + sot = Test.new(**{ + 'known_param': 'v1', + }) + sot['unknown_param'] = 'v2' + + self.assertEqual('v1', sot.known_param) + self.assertEqual('v2', sot.unknown_param) + + def test_unknown_attrs_in_body_not_allowed_to_set(self): + class Test(resource.Resource): + known_param = resource.Body("known_param") + _allow_unknown_attrs_in_body = False + + sot = Test.new(**{ + 'known_param': 'v1', + }) + try: + sot['unknown_param'] = 'v2' + except KeyError: + self.assertEqual('v1', sot.known_param) + self.assertNotIn('unknown_param', sot) + return + self.fail("Parameter 'unknown_param' unexpectedly set through the " + "dict interface") + + def test_unknown_attrs_in_body_translate_response(self): + class Test(resource.Resource): + known_param = resource.Body("known_param") + _allow_unknown_attrs_in_body = True + + body = {'known_param': 'v1', 'unknown_param': 'v2'} + response = FakeResponse(body) + + sot = Test() + sot._translate_response(response, has_body=True) + + self.assertEqual('v1', sot.known_param) + self.assertEqual('v2', sot.unknown_param) + + def test_unknown_attrs_not_in_body_translate_response(self): + class Test(resource.Resource): + known_param = resource.Body("known_param") + _allow_unknown_attrs_in_body = False + + body = {'known_param': 'v1', 'unknown_param': 'v2'} + response = FakeResponse(body) + + sot = Test() + sot._translate_response(response, has_body=True) + + self.assertEqual('v1', sot.known_param) + self.assertNotIn('unknown_param', sot) + class TestResourceActions(base.TestCase): From 18a5cde8156f9ab9444962ee02a5db6372135f22 Mon Sep 17 00:00:00 2001 From: Ryan Zimmerman Date: Wed, 14 Apr 2021 00:00:21 -0700 Subject: [PATCH 2815/3836] Add compute microversion 2.77 Compute 2.77 [1] added support for specifying an availability zone when unshelving a server. [1] https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id70 Change-Id: I603b39e39ab0fd8c95d7340e0895f38cc7d6daf7 --- openstack/compute/v2/server.py | 4 +++- openstack/tests/unit/compute/v2/test_server.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index fe492c897..765de379a 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -450,8 +450,10 @@ def shelve(self, session): body = {"shelve": None} self._action(session, body) - def unshelve(self, session): + def unshelve(self, session, availability_zone=None): body = {"unshelve": None} + if availability_zone: + body["unshelve"] = {"availability_zone": availability_zone} self._action(session, body) def migrate(self, session): diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 2491b7363..200ef2e77 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -772,6 +772,20 @@ def test_unshelve(self): self.sess.post.assert_called_with( url, json=body, headers=headers, microversion=None) + def test_unshelve_availability_zone(self): + sot = server.Server(**EXAMPLE) + + res = sot.unshelve(self.sess, sot.availability_zone) + + self.assertIsNone(res) + url = 'servers/IDENTIFIER/action' + body = {"unshelve": { + "availability_zone": sot.availability_zone + }} + headers = {'Accept': ''} + self.sess.post.assert_called_with( + url, json=body, headers=headers, microversion=None) + def test_migrate(self): sot = server.Server(**EXAMPLE) From c49bbafb89fa3bbcbdaa8c4c478aa57460bcdf5c Mon Sep 17 00:00:00 2001 From: Riccardo Pittau Date: Thu, 22 Apr 2021 15:09:42 +0200 Subject: [PATCH 2816/3836] Fix import order in compute unit tests Change-Id: I775d7d37d543e306cd870b56a071f8afbbdd5bb6 --- openstack/tests/unit/compute/test_version.py | 2 +- openstack/tests/unit/compute/v2/test_availability_zone.py | 2 +- openstack/tests/unit/compute/v2/test_extension.py | 2 +- openstack/tests/unit/compute/v2/test_flavor.py | 3 ++- openstack/tests/unit/compute/v2/test_hypervisor.py | 3 ++- openstack/tests/unit/compute/v2/test_image.py | 2 +- openstack/tests/unit/compute/v2/test_keypair.py | 2 +- openstack/tests/unit/compute/v2/test_proxy.py | 1 + openstack/tests/unit/compute/v2/test_server_diagnostics.py | 2 +- openstack/tests/unit/compute/v2/test_server_group.py | 2 +- openstack/tests/unit/compute/v2/test_server_interface.py | 2 +- openstack/tests/unit/compute/v2/test_server_remote_console.py | 2 +- openstack/tests/unit/compute/v2/test_service.py | 2 +- openstack/tests/unit/compute/v2/test_volume_attachment.py | 2 +- tox.ini | 1 - 15 files changed, 16 insertions(+), 14 deletions(-) diff --git a/openstack/tests/unit/compute/test_version.py b/openstack/tests/unit/compute/test_version.py index 76fdaf2dd..9bf6bff23 100644 --- a/openstack/tests/unit/compute/test_version.py +++ b/openstack/tests/unit/compute/test_version.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.compute import version from openstack.tests.unit import base -from openstack.compute import version IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/compute/v2/test_availability_zone.py b/openstack/tests/unit/compute/v2/test_availability_zone.py index af646a35e..e454140e0 100644 --- a/openstack/tests/unit/compute/v2/test_availability_zone.py +++ b/openstack/tests/unit/compute/v2/test_availability_zone.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.compute.v2 import availability_zone as az from openstack.tests.unit import base -from openstack.compute.v2 import availability_zone as az IDENTIFIER = 'IDENTIFIER' BASIC_EXAMPLE = { diff --git a/openstack/tests/unit/compute/v2/test_extension.py b/openstack/tests/unit/compute/v2/test_extension.py index 4ee1ad331..08f4930a1 100644 --- a/openstack/tests/unit/compute/v2/test_extension.py +++ b/openstack/tests/unit/compute/v2/test_extension.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.compute.v2 import extension from openstack.tests.unit import base -from openstack.compute.v2 import extension IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/compute/v2/test_flavor.py b/openstack/tests/unit/compute/v2/test_flavor.py index 56cfa2a38..77cf32e96 100644 --- a/openstack/tests/unit/compute/v2/test_flavor.py +++ b/openstack/tests/unit/compute/v2/test_flavor.py @@ -9,13 +9,14 @@ # 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 unittest import mock from keystoneauth1 import adapter +from openstack.compute.v2 import flavor from openstack.tests.unit import base -from openstack.compute.v2 import flavor IDENTIFIER = 'IDENTIFIER' BASIC_EXAMPLE = { diff --git a/openstack/tests/unit/compute/v2/test_hypervisor.py b/openstack/tests/unit/compute/v2/test_hypervisor.py index b79b6bd80..e6e33189b 100644 --- a/openstack/tests/unit/compute/v2/test_hypervisor.py +++ b/openstack/tests/unit/compute/v2/test_hypervisor.py @@ -9,15 +9,16 @@ # 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 from unittest import mock from keystoneauth1 import adapter +from openstack.compute.v2 import hypervisor from openstack import exceptions from openstack.tests.unit import base -from openstack.compute.v2 import hypervisor EXAMPLE = { "cpu_info": { diff --git a/openstack/tests/unit/compute/v2/test_image.py b/openstack/tests/unit/compute/v2/test_image.py index eaa952222..a0b4957d5 100644 --- a/openstack/tests/unit/compute/v2/test_image.py +++ b/openstack/tests/unit/compute/v2/test_image.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.compute.v2 import image from openstack.tests.unit import base -from openstack.compute.v2 import image IDENTIFIER = 'IDENTIFIER' diff --git a/openstack/tests/unit/compute/v2/test_keypair.py b/openstack/tests/unit/compute/v2/test_keypair.py index 6c6e5e8a6..ee5aaecd8 100644 --- a/openstack/tests/unit/compute/v2/test_keypair.py +++ b/openstack/tests/unit/compute/v2/test_keypair.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.compute.v2 import keypair from openstack.tests.unit import base -from openstack.compute.v2 import keypair EXAMPLE = { 'created_at': 'some_time', diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index fb6e05410..4a4c05ddf 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -9,6 +9,7 @@ # 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 unittest import mock from openstack.compute.v2 import _proxy diff --git a/openstack/tests/unit/compute/v2/test_server_diagnostics.py b/openstack/tests/unit/compute/v2/test_server_diagnostics.py index 51309fe7e..d4dc5753c 100644 --- a/openstack/tests/unit/compute/v2/test_server_diagnostics.py +++ b/openstack/tests/unit/compute/v2/test_server_diagnostics.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.compute.v2 import server_diagnostics from openstack.tests.unit import base -from openstack.compute.v2 import server_diagnostics IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/compute/v2/test_server_group.py b/openstack/tests/unit/compute/v2/test_server_group.py index 173761364..20c261f91 100644 --- a/openstack/tests/unit/compute/v2/test_server_group.py +++ b/openstack/tests/unit/compute/v2/test_server_group.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.compute.v2 import server_group from openstack.tests.unit import base -from openstack.compute.v2 import server_group EXAMPLE = { 'id': 'IDENTIFIER', diff --git a/openstack/tests/unit/compute/v2/test_server_interface.py b/openstack/tests/unit/compute/v2/test_server_interface.py index 5c3ef7e68..e506f5637 100644 --- a/openstack/tests/unit/compute/v2/test_server_interface.py +++ b/openstack/tests/unit/compute/v2/test_server_interface.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.compute.v2 import server_interface from openstack.tests.unit import base -from openstack.compute.v2 import server_interface IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/compute/v2/test_server_remote_console.py b/openstack/tests/unit/compute/v2/test_server_remote_console.py index dcbb5a5b1..78708cb15 100644 --- a/openstack/tests/unit/compute/v2/test_server_remote_console.py +++ b/openstack/tests/unit/compute/v2/test_server_remote_console.py @@ -14,9 +14,9 @@ from keystoneauth1 import adapter +from openstack.compute.v2 import server_remote_console from openstack.tests.unit import base -from openstack.compute.v2 import server_remote_console IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/compute/v2/test_service.py b/openstack/tests/unit/compute/v2/test_service.py index 0db702852..3f45a690f 100644 --- a/openstack/tests/unit/compute/v2/test_service.py +++ b/openstack/tests/unit/compute/v2/test_service.py @@ -12,8 +12,8 @@ from unittest import mock -from openstack import exceptions from openstack.compute.v2 import service +from openstack import exceptions from openstack.tests.unit import base IDENTIFIER = 'IDENTIFIER' diff --git a/openstack/tests/unit/compute/v2/test_volume_attachment.py b/openstack/tests/unit/compute/v2/test_volume_attachment.py index 7a7a55c7d..2eb473d4a 100644 --- a/openstack/tests/unit/compute/v2/test_volume_attachment.py +++ b/openstack/tests/unit/compute/v2/test_volume_attachment.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.compute.v2 import volume_attachment from openstack.tests.unit import base -from openstack.compute.v2 import volume_attachment EXAMPLE = { 'device': '1', diff --git a/tox.ini b/tox.ini index 64446e384..9042d7a3a 100644 --- a/tox.ini +++ b/tox.ini @@ -132,7 +132,6 @@ per-file-ignores = openstack/tests/unit/workflow/*:H306,I100,I201,I202 openstack/tests/unit/message/*:H306,I100,I201,I202 openstack/tests/unit/load_balancer/*:H306,I100,I201,I202 - openstack/tests/unit/compute/*:H306,I100,I201,I202 [flake8:local-plugins] extension = From ad82856beaab76e6cce2a446cedef75320233afd Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 23 Apr 2021 16:18:57 +0200 Subject: [PATCH 2817/3836] Drop jobs failing for too long There are 2 jobs failing for a very long time and nobody taking care of fixing them. Disable them for now while still keeping job definitions. Change-Id: I99abe18e3d91d52fc580a3d75146d04ec3de775a --- .zuul.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index f940b73ff..6df3c97f9 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -408,10 +408,6 @@ voting: false - ansible-collections-openstack-functional-devstack: voting: false - - openstacksdk-ansible-stable-2.8-functional-devstack: - voting: false - - openstacksdk-ansible-stable-2.9-functional-devstack: - voting: false gate: jobs: - opendev-buildset-registry From 60ed1fa5e0fdc99634ebdc12933ebd76226645ac Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 23 Apr 2021 16:28:23 +0200 Subject: [PATCH 2818/3836] Switch to openstack-python3-xena-jobs template We somehow missed to update the template jobs. Do this now and switch directly to xena. Change-Id: I2c6a42d481a533b522198127dc6d318d1d2d3752 --- .zuul.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index f940b73ff..ab865b5ae 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -376,7 +376,7 @@ - project: templates: - check-requirements - - openstack-python3-ussuri-jobs + - openstack-python3-xena-jobs - openstacksdk-functional-tips - openstacksdk-tox-tips - os-client-config-tox-tips From 6327052ef0a3e0c4096dc09d673dd1d40dbcc3ed Mon Sep 17 00:00:00 2001 From: Dylan Zapzalka Date: Wed, 31 Mar 2021 17:29:48 -0500 Subject: [PATCH 2819/3836] Add support for the GroupType resource Introduce the GroupType resource, fill in its resources, and implement API calls to support the Cinder v3 API Task: 41856 Story: 2008619 Task: 42191 Story: 2008621 Change-Id: I32b7d6861ed9b15bb0fac1af4acde50c1f3f847a --- openstack/block_storage/v3/_proxy.py | 96 +++++++++++++++++++ openstack/block_storage/v3/group_type.py | 36 +++++++ .../block_storage/v3/test_group_type.py | 39 ++++++++ .../unit/block_storage/v3/test_group_type.py | 46 +++++++++ .../tests/unit/block_storage/v3/test_proxy.py | 24 +++++ 5 files changed, 241 insertions(+) create mode 100644 openstack/block_storage/v3/group_type.py create mode 100644 openstack/tests/functional/block_storage/v3/test_group_type.py create mode 100644 openstack/tests/unit/block_storage/v3/test_group_type.py diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 58d7e6bd7..a62a1a083 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -14,6 +14,7 @@ from openstack.block_storage.v3 import availability_zone from openstack.block_storage.v3 import backup as _backup from openstack.block_storage.v3 import capabilities as _capabilities +from openstack.block_storage.v3 import group_type as _group_type from openstack.block_storage.v3 import limits as _limits from openstack.block_storage.v3 import resource_filter as _resource_filter from openstack.block_storage.v3 import snapshot as _snapshot @@ -549,6 +550,101 @@ def availability_zones(self): return self._list(availability_zone.AvailabilityZone) + def get_group_type(self, group_type): + """Get a specific group type + + :param group_type: The value can be the ID of a group type + or a :class:`~openstack.block_storage.v3.group_type.GroupType` + instance. + + :returns: One :class: + `~openstack.block_storage.v3.group_type.GroupType` instance. + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource can be found. + """ + return self._get(_group_type.GroupType, group_type) + + def find_group_type(self, name_or_id, ignore_missing=True): + """Find a single group type + + :param name_or_id: The name or ID of a group type. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the group type does not exist. + + :returns: One :class:`~openstack.block_storage.v3.group_type + .GroupType' + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._find( + _group_type.GroupType, name_or_id, ignore_missing=ignore_missing) + + def group_types(self, **query): + """Retrive a generator of group types + + :param dict query: Optional query parameters to be sent to limit the + resources being returned: + + * sort: Comma-separated list of sort keys and optional sort + directions in the form of [:]. A valid + direction is asc (ascending) or desc (descending). + * limit: Requests a page size of items. Returns a number of items + up to a limit value. Use the limit parameter to make an + initial limited request and use the ID of the last-seen item + from the response as the marker parameter value in a + subsequent limited request. + * offset: Used in conjunction with limit to return a slice of + items. Is where to start in the list. + * marker: The ID of the last-seen item. + + :returns: A generator of group type objects. + """ + return self._list(_group_type.GroupType, **query) + + def create_group_type(self, **attrs): + """Create a group type + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.block_storage.v3.group_type.GroupType' + comprised of the properties on the GroupType class. + + :returns: The results of group type creation. + :rtype: :class:`~openstack.block_storage.v3.group_type.GroupTye'. + """ + return self._create(_group_type.GroupType, **attrs) + + def delete_group_type(self, group_type, ignore_missing=True): + """Delete a group type + + :param group_type: The value can be the ID of a group type + or a :class:`~openstack.block_storage.v3.group_type.GroupType` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the zone does not exist. + When set to ``True``, no exception will be set when attempting to + delete a nonexistent zone. + + :returns: ''None'' + """ + self._delete( + _group_type.GroupType, group_type, ignore_missing=ignore_missing) + + def update_group_type(self, group_type, **attrs): + """Update a group_type + + :param group_type: The value can be the ID of a group type or a + :class:`~openstack.block_storage.v3.group_type.GroupType` + instance. + :param dict attrs: The attributes to update on the group type. + + :returns: The updated group type. + :rtype: :class:`~openstack.block_storage.v3.group_type.GroupType` + """ + return self._update( + _group_type.GroupType, group_type, **attrs) + def wait_for_status(self, res, status='ACTIVE', failures=None, interval=2, wait=120): """Wait for a resource to be in a particular status. diff --git a/openstack/block_storage/v3/group_type.py b/openstack/block_storage/v3/group_type.py new file mode 100644 index 000000000..89f18e4f9 --- /dev/null +++ b/openstack/block_storage/v3/group_type.py @@ -0,0 +1,36 @@ +# 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 resource + + +class GroupType(resource.Resource): + resource_key = "group_type" + resources_key = "group_types" + base_path = "/group_types" + + # capabilities + allow_fetch = True + allow_create = True + allow_delete = True + allow_commit = True + allow_list = True + + _max_microversion = "3.11" + + #: Properties + #: The group type description. + description = resource.Body("description") + #: Contains the specifications for a group type. + group_specs = resource.Body("group_specs", type=dict) + #: Whether the group type is publicly visible. + is_public = resource.Body("is_public", type=bool) diff --git a/openstack/tests/functional/block_storage/v3/test_group_type.py b/openstack/tests/functional/block_storage/v3/test_group_type.py new file mode 100644 index 000000000..0ca5252a8 --- /dev/null +++ b/openstack/tests/functional/block_storage/v3/test_group_type.py @@ -0,0 +1,39 @@ +# 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.block_storage.v3 import group_type as _group_type +from openstack.tests.functional.block_storage.v3 import base + + +class TestGroupType(base.BaseBlockStorageTest): + + def setUp(self): + super(TestGroupType, self).setUp() + + self.GROUP_TYPE_NAME = self.getUniqueString() + self.GROUP_TYPE_ID = None + + group_type = self.conn.block_storage.create_group_type( + name=self.GROUP_TYPE_NAME) + self.assertIsInstance(group_type, _group_type.GroupType) + self.assertEqual(self.GROUP_TYPE_NAME, group_type.name) + self.GROUP_TYPE_ID = group_type.id + + def tearDown(self): + group_type = self.conn.block_storage.delete_group_type( + self.GROUP_TYPE_ID, ignore_missing=False) + self.assertIsNone(group_type) + super(TestGroupType, self).tearDown() + + def test_get(self): + group_type = self.conn.block_storage.get_group_type(self.GROUP_TYPE_ID) + self.assertEqual(self.GROUP_TYPE_NAME, group_type.name) diff --git a/openstack/tests/unit/block_storage/v3/test_group_type.py b/openstack/tests/unit/block_storage/v3/test_group_type.py new file mode 100644 index 000000000..541339dcb --- /dev/null +++ b/openstack/tests/unit/block_storage/v3/test_group_type.py @@ -0,0 +1,46 @@ +# 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.block_storage.v3 import group_type +from openstack.tests.unit import base + +GROUP_TYPE = { + "id": "6685584b-1eac-4da6-b5c3-555430cf68ff", + "name": "grp-type-001", + "description": "group type 001", + "is_public": True, + "group_specs": { + "consistent_group_snapshot_enabled": " False" + } +} + + +class TestGroupType(base.TestCase): + + def test_basic(self): + resource = group_type.GroupType() + self.assertEqual("group_type", resource.resource_key) + self.assertEqual("group_types", resource.resources_key) + self.assertEqual("/group_types", resource.base_path) + self.assertTrue(resource.allow_create) + self.assertTrue(resource.allow_fetch) + self.assertTrue(resource.allow_delete) + self.assertTrue(resource.allow_commit) + self.assertTrue(resource.allow_list) + + def test_make_resource(self): + resource = group_type.GroupType(**GROUP_TYPE) + self.assertEqual(GROUP_TYPE["id"], resource.id) + self.assertEqual(GROUP_TYPE["name"], resource.name) + self.assertEqual(GROUP_TYPE["description"], resource.description) + self.assertEqual(GROUP_TYPE["is_public"], resource.is_public) + self.assertEqual(GROUP_TYPE["group_specs"], resource.group_specs) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index b117e3f1a..a83e5db4e 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -14,6 +14,7 @@ from openstack.block_storage.v3 import _proxy from openstack.block_storage.v3 import backup from openstack.block_storage.v3 import capabilities +from openstack.block_storage.v3 import group_type from openstack.block_storage.v3 import limits from openstack.block_storage.v3 import resource_filter from openstack.block_storage.v3 import snapshot @@ -245,3 +246,26 @@ def test_capabilites_get(self): def test_resource_filters(self): self.verify_list(self.proxy.resource_filters, resource_filter.ResourceFilter) + + def test_group_type_get(self): + self.verify_get(self.proxy.get_group_type, group_type.GroupType) + + def test_group_type_find(self): + self.verify_find(self.proxy.find_group_type, group_type.GroupType) + + def test_group_types(self): + self.verify_list(self.proxy.group_types, group_type.GroupType) + + def test_group_type_create(self): + self.verify_create(self.proxy.create_group_type, group_type.GroupType) + + def test_group_type_delete(self): + self.verify_delete( + self.proxy.delete_group_type, group_type.GroupType, False) + + def test_group_type_delete_ignore(self): + self.verify_delete( + self.proxy.delete_group_type, group_type.GroupType, True) + + def test_group_type_update(self): + self.verify_update(self.proxy.update_group_type, group_type.GroupType) From 34d23cd12720559ecb95b3783837d06746ea5518 Mon Sep 17 00:00:00 2001 From: Thiago Brito Date: Wed, 17 Feb 2021 18:05:24 -0300 Subject: [PATCH 2820/3836] Adding retype_volume to BlockStorageCloudMixin We are missing some "/action" operations for volumes so this patch adds the ability to retype volumes to the sdk. Signed-off-by: Thiago Brito Change-Id: I6f8d58705db3c71abffd0e939d692bb02b8b1f32 --- openstack/block_storage/v3/volume.py | 10 ++++++++++ .../tests/unit/block_storage/v3/test_proxy.py | 12 ++++++++++++ .../tests/unit/block_storage/v3/test_volume.py | 15 +++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index 477c03005..bef5a2fbd 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -119,5 +119,15 @@ def set_readonly(self, session, readonly): body = {'os-update_readonly_flag': {'readonly': readonly}} self._action(session, body) + def retype(self, session, new_type, migration_policy): + """Retype volume considering the migration policy""" + body = { + 'os-retype': { + 'new_type': new_type, + 'migration_policy': migration_policy + } + } + self._action(session, body) + VolumeDetail = Volume diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index a83e5db4e..93b0b747b 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -172,6 +172,18 @@ def test_volume_set_readonly_false(self): method_args=["value", False], expected_args=[False]) + def test_volume_retype_without_migration_policy(self): + self._verify("openstack.block_storage.v3.volume.Volume.retype", + self.proxy.retype_volume, + method_args=["value", "rbd"], + expected_args=["rbd", "never"]) + + def test_volume_retype_with_migration_policy(self): + self._verify("openstack.block_storage.v3.volume.Volume.retype", + self.proxy.retype_volume, + method_args=["value", "rbd", "on-demand"], + expected_args=["rbd", "on-demand"]) + def test_backend_pools(self): self.verify_list(self.proxy.backend_pools, stats.Pools) diff --git a/openstack/tests/unit/block_storage/v3/test_volume.py b/openstack/tests/unit/block_storage/v3/test_volume.py index ac1a238f9..b6a4fc836 100644 --- a/openstack/tests/unit/block_storage/v3/test_volume.py +++ b/openstack/tests/unit/block_storage/v3/test_volume.py @@ -147,3 +147,18 @@ def test_set_volume_readonly_false(self): body = {'os-update_readonly_flag': {'readonly': False}} headers = {'Accept': ''} self.sess.post.assert_called_with(url, json=body, headers=headers) + + def test_retype(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.retype(self.sess, 'rbd', 'on-demand')) + + url = 'volumes/%s/action' % FAKE_ID + body = { + 'os-retype': { + 'new_type': 'rbd', + 'migration_policy': 'on-demand' + } + } + headers = {'Accept': ''} + self.sess.post.assert_called_with(url, json=body, headers=headers) From f05a6954748d080fa92947b6d1ae94b68f2ef340 Mon Sep 17 00:00:00 2001 From: melanie witt Date: Mon, 26 Apr 2021 23:16:55 +0000 Subject: [PATCH 2821/3836] Add min_count and max_count attributes to Server The min_count and max_count kwargs are listed as valid kwargs on the ComputeCloudMixin.create_server method but when they are specified, they are not passed through to the eventual call to the nova REST API because they are not defined as known/allowed attributes on the Server class. This adds min_count and max_count attributes to the Server class so they will be honored when specified when calling create_server. Story: 2008856 Task: 42377 Change-Id: Iaaedd292dc3dcf6486914d3b1f404ad95b12daeb --- openstack/compute/v2/server.py | 4 ++++ openstack/tests/unit/cloud/test_normalize.py | 6 ++++++ openstack/tests/unit/compute/v2/test_server.py | 5 ++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 5205c430b..e3ae44a2d 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -141,8 +141,12 @@ class Server(resource.Resource, metadata.MetadataMixin, resource.TagMixin): launch_index = resource.Body('OS-EXT-SRV-ATTR:launch_index', type=int) #: The timestamp when the server was launched. launched_at = resource.Body('OS-SRV-USG:launched_at') + #: The maximum number of servers to create. + max_count = resource.Body('max_count') #: Metadata stored for this server. *Type: dict* metadata = resource.Body('metadata', type=dict) + #: The minimum number of servers to create. + min_count = resource.Body('min_count') #: A networks object. Required parameter when there are multiple #: networks defined for the tenant. When you do not specify the #: networks parameter, the server attaches to the only network diff --git a/openstack/tests/unit/cloud/test_normalize.py b/openstack/tests/unit/cloud/test_normalize.py index c0f9fbe5f..ed2b28134 100644 --- a/openstack/tests/unit/cloud/test_normalize.py +++ b/openstack/tests/unit/cloud/test_normalize.py @@ -592,7 +592,9 @@ def test_normalize_servers_normal(self): 'region_name': u'RegionOne', 'zone': u'ca-ymq-2'}, 'locked': True, + 'max_count': None, 'metadata': {u'group': u'irc', u'groups': u'irc,enabled'}, + 'min_count': None, 'name': u'mordred-irc', 'networks': { u'public': [ @@ -625,6 +627,8 @@ def test_normalize_servers_normal(self): 'OS-SRV-USG:terminated_at': None, 'host_status': None, 'locked': True, + 'max_count': None, + 'min_count': None, 'os-extended-volumes:volumes_attached': [], 'trusted_image_certificates': None}, 'public_v4': None, @@ -1175,6 +1179,8 @@ def test_normalize_servers(self): 'properties': { 'host_status': None, 'locked': True, + 'max_count': None, + 'min_count': None, 'trusted_image_certificates': None }, 'public_v4': None, diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 49a0c9363..d3eef970e 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -257,7 +257,7 @@ def test__prepare_server(self): hints = {"hint": 3} sot = server.Server(id=1, availability_zone=zone, user_data=data, - scheduler_hints=hints) + scheduler_hints=hints, min_count=2, max_count=3) request = sot._prepare_request() self.assertNotIn("OS-EXT-AZ:availability_zone", @@ -274,6 +274,9 @@ def test__prepare_server(self): request.body[sot.resource_key]) self.assertEqual(request.body["OS-SCH-HNT:scheduler_hints"], hints) + self.assertEqual(2, request.body[sot.resource_key]['min_count']) + self.assertEqual(3, request.body[sot.resource_key]['max_count']) + def test_change_password(self): sot = server.Server(**EXAMPLE) From d72e60819d5b117797906eaeaebbe463ade1ea27 Mon Sep 17 00:00:00 2001 From: songwenping Date: Thu, 29 Apr 2021 20:21:56 +0800 Subject: [PATCH 2822/3836] setup.cfg: Replace dashes with underscores Setuptools v54.1.0 introduces a warning that the use of dash-separated options in 'setup.cfg' will not be supported in a future version [1]. Get ahead of the issue by replacing the dashes with underscores. Without this, we see 'UserWarning' messages like the following on new enough versions of setuptools: UserWarning: Usage of dash-separated 'description-file' will not be supported in future versions. Please use the underscore name 'description_file' instead [1] https://github.com/pypa/setuptools/commit/a2e9ae4cb Change-Id: I6fc527205aa8bc2c226f5649b894b8a1de7525a2 --- setup.cfg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index a635769c4..0c5f95cde 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,11 +1,11 @@ [metadata] name = openstacksdk summary = An SDK for building applications to work with OpenStack -description-file = +description_file = README.rst author = OpenStack -author-email = openstack-discuss@lists.openstack.org -home-page = https://docs.openstack.org/openstacksdk/ +author_email = openstack-discuss@lists.openstack.org +home_page = https://docs.openstack.org/openstacksdk/ classifier = Environment :: OpenStack Intended Audience :: Information Technology From c7e3081bdbae104e0e5278747a88b8facb80dbc1 Mon Sep 17 00:00:00 2001 From: cenne Date: Sun, 2 May 2021 00:23:04 +0200 Subject: [PATCH 2823/3836] Implement driver vendor passthrough * Define 'list' and 'call' vendor_passthru methods in Driver * Add corresponding proxy methods * Implement unit tests - verify result properly returned for list_vendor_passthru - verify session is being called properly for both list and call Story: 2008193 Task: 40960 Change-Id: Id7e5f6d3f651c371efca29002de63c973e8f33d7 --- openstack/baremetal/v1/_proxy.py | 29 ++++++++ openstack/baremetal/v1/driver.py | 60 +++++++++++++++++ .../tests/unit/baremetal/v1/test_driver.py | 66 +++++++++++++++++++ 3 files changed, 155 insertions(+) diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 4f00e64be..2cbda567b 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -194,6 +194,35 @@ def get_driver(self, driver): """ return self._get(_driver.Driver, driver) + def list_driver_vendor_passthru(self, driver): + """Get driver's vendor_passthru methods. + + :param driver: The value can be the name of a driver or a + :class:`~openstack.baremetal.v1.driver.Driver` instance. + + :returns: One :dict: of vendor methods with corresponding usages + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + driver matching the name could be found. + """ + driver = self.get_driver(driver) + return driver.list_vendor_passthru(self) + + def call_driver_vendor_passthru(self, driver, + verb: str, method: str, body=None): + """Call driver's vendor_passthru method. + + :param driver: The value can be the name of a driver or a + :class:`~openstack.baremetal.v1.driver.Driver` instance. + :param verb: One of GET, POST, PUT, DELETE, + depending on the driver and method. + :param method: Name of vendor method. + :param body: passed to the vendor function as json body. + + :returns: Server response + """ + driver = self.get_driver(driver) + return driver.call_vendor_passthru(self, verb, method, body) + def nodes(self, details=False, **query): """Retrieve a generator of nodes. diff --git a/openstack/baremetal/v1/driver.py b/openstack/baremetal/v1/driver.py index 00a31954d..91311ae72 100644 --- a/openstack/baremetal/v1/driver.py +++ b/openstack/baremetal/v1/driver.py @@ -10,7 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.baremetal.v1 import _common +from openstack import exceptions from openstack import resource +from openstack import utils class Driver(resource.Resource): @@ -117,3 +120,60 @@ class Driver(resource.Resource): #: Enabled vendor interface implementations. #: Introduced in API microversion 1.30. enabled_vendor_interfaces = resource.Body("enabled_vendor_interfaces") + + def list_vendor_passthru(self, session): + """Fetch vendor specific methods exposed by driver + + :param session: The session to use for making this request. + :returns: A dict of the available vendor passthru methods for driver. + Method names keys and corresponding usages in dict form as values + Usage dict properties: + * ``async``: bool # Is passthru function invoked asynchronously + * ``attach``: bool # Is return value attached to response object + * ``description``: str # Description of what the method does + * ``http_methods``: list # List of HTTP methods supported + """ + session = self._get_session(session) + request = self._prepare_request() + request.url = utils.urljoin( + request.url, 'vendor_passthru', 'methods') + response = session.get(request.url, headers=request.headers) + + msg = ("Failed to list list vendor_passthru methods for {driver_name}" + .format(driver_name=self.name)) + exceptions.raise_from_response(response, error_message=msg) + return response.json() + + def call_vendor_passthru(self, session, + verb: str, method: str, body: dict = None): + """Call a vendor specific passthru method + + Contents of body are params passed to the hardware driver + function. Validation happens there. Missing parameters, or + excess parameters will cause the request to be rejected + + :param session: The session to use for making this request. + :param method: Vendor passthru method name. + :param verb: One of GET, POST, PUT, DELETE, + depending on the driver and method. + :param body: passed to the vendor function as json body. + :raises: :exc:`ValueError` if :data:`verb` is not one of + GET, POST, PUT, DELETE + :returns: response of method call. + """ + if verb.upper() not in ['GET', 'PUT', 'POST', 'DELETE']: + raise ValueError('Invalid verb: {}'.format(verb)) + + session = self._get_session(session) + request = self._prepare_request() + request.url = utils.urljoin( + request.url, f'vendor_passthru?method={method}') + call = getattr(session, verb.lower()) + response = call( + request.url, json=body, headers=request.headers, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + msg = ("Failed call to method {method} on driver {driver_name}" + .format(method=method, driver_name=self.name)) + exceptions.raise_from_response(response, error_message=msg) + return response diff --git a/openstack/tests/unit/baremetal/v1/test_driver.py b/openstack/tests/unit/baremetal/v1/test_driver.py index 364e0c1ae..e4d1f6731 100644 --- a/openstack/tests/unit/baremetal/v1/test_driver.py +++ b/openstack/tests/unit/baremetal/v1/test_driver.py @@ -10,7 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. +from unittest import mock + +from keystoneauth1 import adapter + +from openstack.baremetal.v1 import _common from openstack.baremetal.v1 import driver +from openstack import exceptions from openstack.tests.unit import base @@ -63,3 +69,63 @@ def test_instantiate(self): self.assertEqual(FAKE['hosts'], sot.hosts) self.assertEqual(FAKE['links'], sot.links) self.assertEqual(FAKE['properties'], sot.properties) + + @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) + def test_list_vendor_passthru(self): + self.session = mock.Mock(spec=adapter.Adapter) + sot = driver.Driver(**FAKE) + fake_vendor_passthru_info = { + 'fake_vendor_method': { + 'async': True, + 'attach': False, + 'description': "Fake function that does nothing in background", + 'http_methods': ['GET', 'PUT', 'POST', 'DELETE'] + } + } + self.session.get.return_value.json.return_value = ( + fake_vendor_passthru_info) + result = sot.list_vendor_passthru(self.session) + self.session.get.assert_called_once_with( + 'drivers/{driver_name}/vendor_passthru/methods'.format( + driver_name=FAKE["name"]), + headers=mock.ANY) + self.assertEqual(result, fake_vendor_passthru_info) + + @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) + def test_call_vendor_passthru(self): + self.session = mock.Mock(spec=adapter.Adapter) + sot = driver.Driver(**FAKE) + # GET + sot.call_vendor_passthru(self.session, 'GET', 'fake_vendor_method') + self.session.get.assert_called_once_with( + 'drivers/{}/vendor_passthru?method={}'.format( + FAKE["name"], 'fake_vendor_method'), + json=None, + headers=mock.ANY, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + # PUT + sot.call_vendor_passthru(self.session, 'PUT', 'fake_vendor_method', + body={"fake_param_key": "fake_param_value"}) + self.session.put.assert_called_once_with( + 'drivers/{}/vendor_passthru?method={}'.format( + FAKE["name"], 'fake_vendor_method'), + json={"fake_param_key": "fake_param_value"}, + headers=mock.ANY, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + # POST + sot.call_vendor_passthru(self.session, 'POST', 'fake_vendor_method', + body={"fake_param_key": "fake_param_value"}) + self.session.post.assert_called_once_with( + 'drivers/{}/vendor_passthru?method={}'.format( + FAKE["name"], 'fake_vendor_method'), + json={"fake_param_key": "fake_param_value"}, + headers=mock.ANY, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + # DELETE + sot.call_vendor_passthru(self.session, 'DELETE', 'fake_vendor_method') + self.session.delete.assert_called_once_with( + 'drivers/{}/vendor_passthru?method={}'.format( + FAKE["name"], 'fake_vendor_method'), + json=None, + headers=mock.ANY, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) From 6a237bcc1d4118e5a474020e73f4f237882d0dd3 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Mon, 3 May 2021 15:33:04 +0200 Subject: [PATCH 2824/3836] Increase RAM for the Ironic CI jobs And decrease the number of testing VMs since we don't need so many. Change-Id: I4a49116db8e67329fb1f099b5a3806b07d47e5d4 --- .zuul.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 7c96ca17d..6de2c8689 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -249,9 +249,9 @@ IRONIC_CALLBACK_TIMEOUT: 600 IRONIC_DEPLOY_DRIVER: ipmi IRONIC_RAMDISK_TYPE: tinyipa - IRONIC_VM_COUNT: 6 + IRONIC_VM_COUNT: 2 IRONIC_VM_LOG_DIR: '{{ devstack_base_dir }}/ironic-bm-logs' - IRONIC_VM_SPECS_RAM: 512 + IRONIC_VM_SPECS_RAM: 1024 devstack_plugins: ironic: https://opendev.org/openstack/ironic devstack_services: From 722669d05a081e441394461551fed2380e6cf08b Mon Sep 17 00:00:00 2001 From: Dmitriy Rabotyagov Date: Thu, 29 Apr 2021 16:32:23 +0300 Subject: [PATCH 2825/3836] Adjust image _base_proxy condition It makes no sense in running get_image when image is None. Intended behaviour here would be recieving image object, when we were provided with name or id Change-Id: I2fac79a9c5260405f4602583efce7a4d61af5ed2 --- openstack/image/_base_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/image/_base_proxy.py b/openstack/image/_base_proxy.py index 7bd75bf3c..1280181d0 100644 --- a/openstack/image/_base_proxy.py +++ b/openstack/image/_base_proxy.py @@ -251,7 +251,7 @@ def update_image_properties( converted to int. """ - if image is None: + if isinstance(image, str): image = self._connection.get_image(image) if not meta: From 7a19a5ff27cf6517fce3a09d323dbf0b47b2766e Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Tue, 4 May 2021 16:12:29 +0000 Subject: [PATCH 2826/3836] Update .gitreview for feature/r1 Change-Id: I47d4f3830fb7372c8341510f82b8c569cfc5c40f --- .gitreview | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitreview b/.gitreview index 9e465c3ef..38c9ed1db 100644 --- a/.gitreview +++ b/.gitreview @@ -2,3 +2,4 @@ host=review.opendev.org port=29418 project=openstack/openstacksdk.git +defaultbranch=feature/r1 From 2d16931ca9fd3f447f05d7320a8c092ffb720a37 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Tue, 4 May 2021 16:12:33 +0000 Subject: [PATCH 2827/3836] Update TOX_CONSTRAINTS_FILE for feature/r1 Update the URL to the upper-constraints file to point to the redirect rule on releases.openstack.org so that anyone working on this branch will switch to the correct upper-constraints list automatically when the requirements repository branches. Until the requirements repository has as feature/r1 branch, tests will continue to use the upper-constraints list on master. Change-Id: I9c01131c4415ad4d0ed5c6cdc6830728734cc261 --- tox.ini | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index 185cfb5ea..b0069f350 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,7 @@ setenv = OS_STDOUT_CAPTURE={env:OS_STDOUT_CAPTURE:true} OS_STDERR_CAPTURE={env:OS_STDERR_CAPTURE:true} deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/r1} -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt commands = stestr run {posargs} @@ -53,7 +53,7 @@ commands = [testenv:venv] deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/r1} -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt @@ -87,7 +87,7 @@ commands = {toxinidir}/extras/run-ansible-tests.sh -e {envdir} {posargs} [testenv:docs] deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/r1} -r{toxinidir}/doc/requirements.txt commands = sphinx-build -W --keep-going -b html -j auto doc/source/ doc/build/html @@ -102,7 +102,7 @@ commands = [testenv:releasenotes] deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/r1} -r{toxinidir}/doc/requirements.txt commands = sphinx-build -W --keep-going -b html -j auto releasenotes/source releasenotes/build/html From b6c4c50c2b03457ab14952598481f35e4c06979e Mon Sep 17 00:00:00 2001 From: Dmitriy Rabotyagov Date: Wed, 5 May 2021 16:04:52 +0300 Subject: [PATCH 2828/3836] Revert tags query_params back to tag This partially reverts commit 38a3409410f7194017c7cfa86efb50ee040369fc Change-Id: I68a70ba6e87e1a4af80bc303de43db37162cdc6d --- openstack/image/v2/image.py | 2 +- openstack/tests/unit/image/v2/test_image.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index 50697c1d5..4e6881a3f 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -37,7 +37,7 @@ class Image(resource.Resource, resource.TagMixin, _download.DownloadMixin): "member_status", "owner", "status", "size_min", "size_max", "protected", "is_hidden", - "sort_key", "sort_dir", "sort", "tags", + "sort_key", "sort_dir", "sort", "tag", "created_at", "updated_at", is_hidden="os_hidden") diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 13bee6a99..9ddd95305 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -154,7 +154,7 @@ def test_basic(self): 'sort_dir': 'sort_dir', 'sort_key': 'sort_key', 'status': 'status', - 'tags': 'tags', + 'tag': 'tag', 'updated_at': 'updated_at', 'visibility': 'visibility' }, From 2598947d8b6864660d5daf30c389d92d5c991c0a Mon Sep 17 00:00:00 2001 From: Riccardo Pittau Date: Thu, 6 May 2021 11:33:24 +0200 Subject: [PATCH 2829/3836] Fix import order in load_balancer unit tests Remove filter from tox.ini Change-Id: I2e5e8f0910ab1ff9dffaecfd7fef64e29b3df91c --- openstack/tests/unit/load_balancer/test_amphora.py | 3 ++- openstack/tests/unit/load_balancer/test_availability_zone.py | 3 ++- .../tests/unit/load_balancer/test_availability_zone_profile.py | 3 ++- openstack/tests/unit/load_balancer/test_flavor.py | 3 ++- openstack/tests/unit/load_balancer/test_flavor_profile.py | 3 ++- openstack/tests/unit/load_balancer/test_health_monitor.py | 3 ++- openstack/tests/unit/load_balancer/test_l7policy.py | 3 ++- openstack/tests/unit/load_balancer/test_l7rule.py | 3 ++- openstack/tests/unit/load_balancer/test_listener.py | 3 ++- openstack/tests/unit/load_balancer/test_member.py | 3 ++- openstack/tests/unit/load_balancer/test_pool.py | 3 ++- openstack/tests/unit/load_balancer/test_provider.py | 2 +- openstack/tests/unit/load_balancer/test_proxy.py | 2 +- openstack/tests/unit/load_balancer/test_quota.py | 2 +- openstack/tests/unit/load_balancer/test_version.py | 2 +- tox.ini | 1 - 16 files changed, 26 insertions(+), 16 deletions(-) diff --git a/openstack/tests/unit/load_balancer/test_amphora.py b/openstack/tests/unit/load_balancer/test_amphora.py index 03d44f81d..09c087a5a 100644 --- a/openstack/tests/unit/load_balancer/test_amphora.py +++ b/openstack/tests/unit/load_balancer/test_amphora.py @@ -12,10 +12,11 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base import uuid from openstack.load_balancer.v2 import amphora +from openstack.tests.unit import base + IDENTIFIER = uuid.uuid4() LB_ID = uuid.uuid4() diff --git a/openstack/tests/unit/load_balancer/test_availability_zone.py b/openstack/tests/unit/load_balancer/test_availability_zone.py index 3417ee734..63eb9fb52 100644 --- a/openstack/tests/unit/load_balancer/test_availability_zone.py +++ b/openstack/tests/unit/load_balancer/test_availability_zone.py @@ -11,10 +11,11 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base import uuid from openstack.load_balancer.v2 import availability_zone +from openstack.tests.unit import base + AVAILABILITY_ZONE_PROFILE_ID = uuid.uuid4() EXAMPLE = { diff --git a/openstack/tests/unit/load_balancer/test_availability_zone_profile.py b/openstack/tests/unit/load_balancer/test_availability_zone_profile.py index 82d1c0b9d..b6343772f 100644 --- a/openstack/tests/unit/load_balancer/test_availability_zone_profile.py +++ b/openstack/tests/unit/load_balancer/test_availability_zone_profile.py @@ -11,10 +11,11 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base import uuid from openstack.load_balancer.v2 import availability_zone_profile +from openstack.tests.unit import base + IDENTIFIER = uuid.uuid4() EXAMPLE = { diff --git a/openstack/tests/unit/load_balancer/test_flavor.py b/openstack/tests/unit/load_balancer/test_flavor.py index 5d5069a75..5f4bec1ef 100644 --- a/openstack/tests/unit/load_balancer/test_flavor.py +++ b/openstack/tests/unit/load_balancer/test_flavor.py @@ -12,10 +12,11 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base import uuid from openstack.load_balancer.v2 import flavor +from openstack.tests.unit import base + IDENTIFIER = uuid.uuid4() FLAVOR_PROFILE_ID = uuid.uuid4() diff --git a/openstack/tests/unit/load_balancer/test_flavor_profile.py b/openstack/tests/unit/load_balancer/test_flavor_profile.py index 7b7ea43c4..2266f4d1f 100644 --- a/openstack/tests/unit/load_balancer/test_flavor_profile.py +++ b/openstack/tests/unit/load_balancer/test_flavor_profile.py @@ -12,10 +12,11 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base import uuid from openstack.load_balancer.v2 import flavor_profile +from openstack.tests.unit import base + IDENTIFIER = uuid.uuid4() EXAMPLE = { diff --git a/openstack/tests/unit/load_balancer/test_health_monitor.py b/openstack/tests/unit/load_balancer/test_health_monitor.py index 7bfd72c33..85ff52b7c 100644 --- a/openstack/tests/unit/load_balancer/test_health_monitor.py +++ b/openstack/tests/unit/load_balancer/test_health_monitor.py @@ -11,10 +11,11 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base import uuid from openstack.load_balancer.v2 import health_monitor +from openstack.tests.unit import base + EXAMPLE = { 'admin_state_up': True, diff --git a/openstack/tests/unit/load_balancer/test_l7policy.py b/openstack/tests/unit/load_balancer/test_l7policy.py index a3194f93c..cf433fc2f 100644 --- a/openstack/tests/unit/load_balancer/test_l7policy.py +++ b/openstack/tests/unit/load_balancer/test_l7policy.py @@ -10,10 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base import uuid from openstack.load_balancer.v2 import l7_policy +from openstack.tests.unit import base + EXAMPLE = { 'action': 'REJECT', diff --git a/openstack/tests/unit/load_balancer/test_l7rule.py b/openstack/tests/unit/load_balancer/test_l7rule.py index 3d93703f2..0f3788091 100644 --- a/openstack/tests/unit/load_balancer/test_l7rule.py +++ b/openstack/tests/unit/load_balancer/test_l7rule.py @@ -10,10 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base import uuid from openstack.load_balancer.v2 import l7_rule +from openstack.tests.unit import base + EXAMPLE = { 'admin_state_up': True, diff --git a/openstack/tests/unit/load_balancer/test_listener.py b/openstack/tests/unit/load_balancer/test_listener.py index 3d70ac9cd..c8928e77e 100644 --- a/openstack/tests/unit/load_balancer/test_listener.py +++ b/openstack/tests/unit/load_balancer/test_listener.py @@ -10,10 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base import uuid from openstack.load_balancer.v2 import listener +from openstack.tests.unit import base + IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/load_balancer/test_member.py b/openstack/tests/unit/load_balancer/test_member.py index a99a2858d..fe6d8f1ce 100644 --- a/openstack/tests/unit/load_balancer/test_member.py +++ b/openstack/tests/unit/load_balancer/test_member.py @@ -10,10 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base import uuid from openstack.load_balancer.v2 import member +from openstack.tests.unit import base + IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/load_balancer/test_pool.py b/openstack/tests/unit/load_balancer/test_pool.py index 5bbc0b4e2..5ba5e366f 100644 --- a/openstack/tests/unit/load_balancer/test_pool.py +++ b/openstack/tests/unit/load_balancer/test_pool.py @@ -10,10 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base import uuid from openstack.load_balancer.v2 import pool +from openstack.tests.unit import base + IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/load_balancer/test_provider.py b/openstack/tests/unit/load_balancer/test_provider.py index 59b8f4308..3b087419b 100644 --- a/openstack/tests/unit/load_balancer/test_provider.py +++ b/openstack/tests/unit/load_balancer/test_provider.py @@ -12,9 +12,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.load_balancer.v2 import provider from openstack.tests.unit import base -from openstack.load_balancer.v2 import provider EXAMPLE = { 'name': 'best', diff --git a/openstack/tests/unit/load_balancer/test_proxy.py b/openstack/tests/unit/load_balancer/test_proxy.py index 169a02cf1..54bd479b6 100644 --- a/openstack/tests/unit/load_balancer/test_proxy.py +++ b/openstack/tests/unit/load_balancer/test_proxy.py @@ -10,8 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid from unittest import mock +import uuid from openstack.load_balancer.v2 import _proxy from openstack.load_balancer.v2 import amphora diff --git a/openstack/tests/unit/load_balancer/test_quota.py b/openstack/tests/unit/load_balancer/test_quota.py index c9b01aeef..648dfc40a 100644 --- a/openstack/tests/unit/load_balancer/test_quota.py +++ b/openstack/tests/unit/load_balancer/test_quota.py @@ -13,9 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.load_balancer.v2 import quota from openstack.tests.unit import base -from openstack.load_balancer.v2 import quota IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/load_balancer/test_version.py b/openstack/tests/unit/load_balancer/test_version.py index 0e141dfce..db3e53c1c 100644 --- a/openstack/tests/unit/load_balancer/test_version.py +++ b/openstack/tests/unit/load_balancer/test_version.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.load_balancer import version from openstack.tests.unit import base -from openstack.load_balancer import version IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/tox.ini b/tox.ini index 9042d7a3a..a648d7981 100644 --- a/tox.ini +++ b/tox.ini @@ -131,7 +131,6 @@ per-file-ignores = openstack/tests/unit/config/*:H306,I100,I201,I202 openstack/tests/unit/workflow/*:H306,I100,I201,I202 openstack/tests/unit/message/*:H306,I100,I201,I202 - openstack/tests/unit/load_balancer/*:H306,I100,I201,I202 [flake8:local-plugins] extension = From ed566ac2ac6f43a4ee890f51255ddfc256bf38ff Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 6 May 2021 18:10:45 +0200 Subject: [PATCH 2830/3836] Add support for project options Options are currently not supported as an attribute of the identity.project. Change-Id: I1a2d7effbf634c0501c36c2b528eb5e3ceff1958 --- openstack/identity/v3/project.py | 3 +++ openstack/tests/unit/identity/v3/test_project.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/openstack/identity/v3/project.py b/openstack/identity/v3/project.py index 19fa8dc9d..24ae45635 100644 --- a/openstack/identity/v3/project.py +++ b/openstack/identity/v3/project.py @@ -55,6 +55,9 @@ class Project(resource.Resource, resource.TagMixin): is_enabled = resource.Body('enabled', type=bool) #: Unique project name, within the owning domain. *Type: string* name = resource.Body('name') + #: The resource options for the project. Available resource options are + #: immutable. + options = resource.Body('options', type=dict) #: The ID of the parent of the project. #: New in version 3.4 parent_id = resource.Body('parent_id') diff --git a/openstack/tests/unit/identity/v3/test_project.py b/openstack/tests/unit/identity/v3/test_project.py index 95c0ad22d..9406d5abe 100644 --- a/openstack/tests/unit/identity/v3/test_project.py +++ b/openstack/tests/unit/identity/v3/test_project.py @@ -23,6 +23,9 @@ 'is_domain': False, 'name': '5', 'parent_id': '6', + 'options': { + 'foo': 'bar' + } } @@ -65,6 +68,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['parent_id'], sot.parent_id) + self.assertDictEqual(EXAMPLE['options'], sot.options) class TestUserProject(base.TestCase): From bcdeb001d717cb209c3e6a1b19c5ba5b56b7364c Mon Sep 17 00:00:00 2001 From: James Palmer Date: Wed, 28 Apr 2021 18:31:51 +0000 Subject: [PATCH 2831/3836] Add support for API Extensions Introduct the extensions resource, its attributes, and API Calls for interacting with API Extensions. Task: 41810 Story: 2008619 Signed-off-by: James Palmer Change-Id: Ib45a47ef5563d2ca0690420ec859356ea887447a --- openstack/block_storage/v3/_proxy.py | 10 +++++ openstack/block_storage/v3/extension.py | 31 ++++++++++++++ .../block_storage/v3/test_extension.py | 24 +++++++++++ .../unit/block_storage/v3/test_extension.py | 41 +++++++++++++++++++ .../tests/unit/block_storage/v3/test_proxy.py | 4 ++ 5 files changed, 110 insertions(+) create mode 100644 openstack/block_storage/v3/extension.py create mode 100644 openstack/tests/functional/block_storage/v3/test_extension.py create mode 100644 openstack/tests/unit/block_storage/v3/test_extension.py diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index a62a1a083..ffc9e7b4f 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -14,6 +14,7 @@ from openstack.block_storage.v3 import availability_zone from openstack.block_storage.v3 import backup as _backup from openstack.block_storage.v3 import capabilities as _capabilities +from openstack.block_storage.v3 import extension as _extension from openstack.block_storage.v3 import group_type as _group_type from openstack.block_storage.v3 import limits as _limits from openstack.block_storage.v3 import resource_filter as _resource_filter @@ -693,6 +694,15 @@ def resource_filters(self, **query): """ return self._list(_resource_filter.ResourceFilter, **query) + def extensions(self): + """Return a generator of extensions + + :returns: A generator of extension + :rtype: :class:`~openstack.block_storage.v3.extension.\ + Extension` + """ + return self._list(_extension.Extension) + def _get_cleanup_dependencies(self): return { 'block_storage': { diff --git a/openstack/block_storage/v3/extension.py b/openstack/block_storage/v3/extension.py new file mode 100644 index 000000000..e2085e15c --- /dev/null +++ b/openstack/block_storage/v3/extension.py @@ -0,0 +1,31 @@ +# 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 resource + + +class Extension(resource.Resource): + """Extension""" + resources_key = "extensions" + base_path = "/extensions" + + # Capabilities + allow_list = True + + #: Properties + #: The alias for the extension. + alias = resource.Body('alias', type=str) + #: The extension description. + description = resource.Body('description', type=str) + #: The date and time when the resource was updated. + #: The date and time stamp format is ISO 8601. + updated = resource.Body('updated', type=str) diff --git a/openstack/tests/functional/block_storage/v3/test_extension.py b/openstack/tests/functional/block_storage/v3/test_extension.py new file mode 100644 index 000000000..5ad84feda --- /dev/null +++ b/openstack/tests/functional/block_storage/v3/test_extension.py @@ -0,0 +1,24 @@ +# 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.tests.functional.block_storage.v3 import base + + +class Extensions(base.BaseBlockStorageTest): + + def test_get(self): + extensions = list(self.conn.block_storage.extensions()) + + for extension in extensions: + self.assertIsInstance(extension.alias, str) + self.assertIsInstance(extension.description, str) + self.assertIsInstance(extension.updated, str) diff --git a/openstack/tests/unit/block_storage/v3/test_extension.py b/openstack/tests/unit/block_storage/v3/test_extension.py new file mode 100644 index 000000000..4d92f67e9 --- /dev/null +++ b/openstack/tests/unit/block_storage/v3/test_extension.py @@ -0,0 +1,41 @@ +# 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.block_storage.v3 import extension +from openstack.tests.unit import base + +EXTENSION = { + "alias": "os-hosts", + "description": "Admin-only host administration.", + "links": [], + "name": "Hosts", + "updated": "2011-06-29T00:00:00+00:00", +} + + +class TestExtension(base.TestCase): + + def test_basic(self): + extension_resource = extension.Extension() + self.assertEqual('extensions', extension_resource.resources_key) + self.assertEqual('/extensions', extension_resource.base_path) + self.assertFalse(extension_resource.allow_create) + self.assertFalse(extension_resource.allow_fetch) + self.assertFalse(extension_resource.allow_commit) + self.assertFalse(extension_resource.allow_delete) + self.assertTrue(extension_resource.allow_list) + + def test_make_extension(self): + extension_resource = extension.Extension(**EXTENSION) + self.assertEqual(EXTENSION['alias'], extension_resource.alias) + self.assertEqual(EXTENSION['description'], + extension_resource.description) + self.assertEqual(EXTENSION['updated'], extension_resource.updated) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index a83e5db4e..46c9c3b5f 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -14,6 +14,7 @@ from openstack.block_storage.v3 import _proxy from openstack.block_storage.v3 import backup from openstack.block_storage.v3 import capabilities +from openstack.block_storage.v3 import extension from openstack.block_storage.v3 import group_type from openstack.block_storage.v3 import limits from openstack.block_storage.v3 import resource_filter @@ -269,3 +270,6 @@ def test_group_type_delete_ignore(self): def test_group_type_update(self): self.verify_update(self.proxy.update_group_type, group_type.GroupType) + + def test_extensions(self): + self.verify_list(self.proxy.extensions, extension.Extension) From 8b343e38f068945cad4ed904c8e679a6da0b8351 Mon Sep 17 00:00:00 2001 From: Riccardo Pittau Date: Fri, 7 May 2021 12:59:14 +0200 Subject: [PATCH 2832/3836] Fix import order in message and workflow unit tests Remove tox filters Change-Id: Ia7d2fd0b0e824b09d151b4eee6395a2c0debcd6d --- openstack/tests/unit/message/test_version.py | 2 +- openstack/tests/unit/workflow/test_execution.py | 2 +- openstack/tests/unit/workflow/test_version.py | 2 +- openstack/tests/unit/workflow/test_workflow.py | 1 - tox.ini | 2 -- 5 files changed, 3 insertions(+), 6 deletions(-) diff --git a/openstack/tests/unit/message/test_version.py b/openstack/tests/unit/message/test_version.py index 6e3e9bc72..8692bfc32 100644 --- a/openstack/tests/unit/message/test_version.py +++ b/openstack/tests/unit/message/test_version.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.message import version from openstack.tests.unit import base -from openstack.message import version IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/workflow/test_execution.py b/openstack/tests/unit/workflow/test_execution.py index 639382cfc..e31394406 100644 --- a/openstack/tests/unit/workflow/test_execution.py +++ b/openstack/tests/unit/workflow/test_execution.py @@ -11,9 +11,9 @@ # under the License. from openstack.tests.unit import base - from openstack.workflow.v2 import execution + FAKE_INPUT = { 'cluster_id': '8c74607c-5a74-4490-9414-a3475b1926c2', 'node_id': 'fba2cc5d-706f-4631-9577-3956048d13a2', diff --git a/openstack/tests/unit/workflow/test_version.py b/openstack/tests/unit/workflow/test_version.py index 6828ce2b6..89406afbe 100644 --- a/openstack/tests/unit/workflow/test_version.py +++ b/openstack/tests/unit/workflow/test_version.py @@ -11,9 +11,9 @@ # under the License. from openstack.tests.unit import base - from openstack.workflow import version + IDENTIFIER = 'IDENTIFIER' EXAMPLE = { 'id': IDENTIFIER, diff --git a/openstack/tests/unit/workflow/test_workflow.py b/openstack/tests/unit/workflow/test_workflow.py index ace0ae355..acf444e38 100644 --- a/openstack/tests/unit/workflow/test_workflow.py +++ b/openstack/tests/unit/workflow/test_workflow.py @@ -11,7 +11,6 @@ # under the License. from openstack.tests.unit import base - from openstack.workflow.v2 import workflow diff --git a/tox.ini b/tox.ini index a648d7981..160dabf95 100644 --- a/tox.ini +++ b/tox.ini @@ -129,8 +129,6 @@ per-file-ignores = openstack/tests/unit/identity/*:H306,I100,I201,I202 openstack/tests/unit/accelerator/*:H306,I100,I201,I202 openstack/tests/unit/config/*:H306,I100,I201,I202 - openstack/tests/unit/workflow/*:H306,I100,I201,I202 - openstack/tests/unit/message/*:H306,I100,I201,I202 [flake8:local-plugins] extension = From 03b2b3b00e42ae6d3e959e93d6ce301e06adc163 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 3 Mar 2021 10:56:15 +0100 Subject: [PATCH 2833/3836] Add access to the resource attribute by server-side name In order to keep backward compatibility while we are switching more and more things to the proxy layer add possibility to get attribute (in dict style only) by server-side attribute name. While we do this inform user not to use this anymore. Change-Id: I981892aaba8d3f476a5d1ad71d22ab82b8798975 --- openstack/resource.py | 14 ++++++++++++++ openstack/tests/unit/test_resource.py | 10 ++++++++++ 2 files changed, 24 insertions(+) diff --git a/openstack/resource.py b/openstack/resource.py index 5d9c246b5..573931607 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -36,6 +36,7 @@ class that represent a remote resource. The attributes that import itertools import operator import urllib.parse +import warnings import jsonpatch from keystoneauth1 import adapter @@ -657,6 +658,19 @@ def __getitem__(self, name): real_item = getattr(self.__class__, name, None) if isinstance(real_item, _BaseComponent): return getattr(self, name) + if not real_item: + # In order to maintain backwards compatibility where we were + # returning Munch (and server side names) and Resource object with + # normalized attributes we can offer dict access via server side + # names. + for attr, component in self._attributes_iterator(tuple([Body])): + if component.name == name: + warnings.warn( + 'Access to "%s[%s]" is deprecated. ' + 'Please access using "%s.%s" attribute.' % + (self.__class__, name, self.__class__, attr), + DeprecationWarning) + return getattr(self, attr) raise KeyError(name) def __delitem__(self, name): diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index d77b9ad72..8e0eab91e 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -945,6 +945,16 @@ class Test(resource.Resource): self.assertDictEqual(expected, res) self.assertDictEqual(expected, dict(res)) + def test_access_by_resource_name(self): + + class Test(resource.Resource): + blah = resource.Body("blah_resource") + + sot = Test(blah='dummy') + + result = sot["blah_resource"] + self.assertEqual(result, sot.blah) + def test_to_dict_value_error(self): class Test(resource.Resource): From 6db391f674077970b113006e52eeac103b6c1dea Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 15 Jan 2021 15:38:08 +0100 Subject: [PATCH 2834/3836] Switch networking function in cloud layer to proxy We need to complete rework of our cloud layer to rely on proxy layer instead of reimplementing direct API calls. This time networking functions are touched. Change-Id: I23dc30abc8977c8ff14f5e7b9c9940af0d0894c7 --- openstack/cloud/_floating_ip.py | 62 ++--- openstack/cloud/_network.py | 260 ++++++++---------- openstack/cloud/_network_common.py | 15 +- openstack/cloud/_normalize.py | 15 +- .../tests/functional/cloud/test_quotas.py | 21 +- .../tests/functional/cloud/test_router.py | 4 +- .../tests/unit/cloud/test_baremetal_node.py | 20 +- openstack/tests/unit/cloud/test_caching.py | 6 +- .../tests/unit/cloud/test_create_server.py | 18 +- .../unit/cloud/test_floating_ip_neutron.py | 43 ++- openstack/tests/unit/cloud/test_fwaas.py | 22 +- openstack/tests/unit/cloud/test_network.py | 59 +++- openstack/tests/unit/cloud/test_port.py | 50 +++- openstack/tests/unit/cloud/test_quotas.py | 19 +- openstack/tests/unit/cloud/test_router.py | 89 +++--- openstack/tests/unit/cloud/test_subnet.py | 177 +++++++++--- .../use-proxy-layer-dfc3764d52bc1f2a.yaml | 7 + 17 files changed, 540 insertions(+), 347 deletions(-) create mode 100644 releasenotes/notes/use-proxy-layer-dfc3764d52bc1f2a.yaml diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index 5519ace67..562a39ef5 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -82,8 +82,8 @@ def search_floating_ips(self, id=None, filters=None): def _neutron_list_floating_ips(self, filters=None): if not filters: filters = {} - data = self.network.get('/floatingips', params=filters) - return self._get_and_munchify('floatingips', data) + data = list(self.network.ips(**filters)) + return data def _nova_list_floating_ips(self): try: @@ -228,11 +228,8 @@ def get_floating_ip_by_id(self, id): error_message = "Error getting floating ip with ID {id}".format(id=id) if self._use_neutron_floating(): - data = proxy._json_response( - self.network.get('/floatingips/{id}'.format(id=id)), - error_message=error_message) - return self._normalize_floating_ip( - self._get_and_munchify('floatingip', data)) + fip = self.network.get_ip(id) + return self._normalize_floating_ip(fip) else: data = proxy._json_response( self.compute.get('/os-floating-ips/{id}'.format(id=id)), @@ -461,10 +458,8 @@ def create_floating_ip(self, network=None, server=None, def _submit_create_fip(self, kwargs): # Split into a method to aid in test mocking - data = self.network.post( - "/floatingips", json={"floatingip": kwargs}) - return self._normalize_floating_ip( - self._get_and_munchify('floatingip', data)) + data = self.network.create_ip(**kwargs) + return self._normalize_floating_ip(data) def _neutron_create_floating_ip( self, network_name_or_id=None, server=None, @@ -474,8 +469,9 @@ def _neutron_create_floating_ip( if not network_id: if network_name_or_id: - network = self.get_network(network_name_or_id) - if not network: + try: + network = self.network.find_network(network_name_or_id) + except exceptions.ResourceNotFound: raise exc.OpenStackCloudResourceNotFound( "unable to find network for floating ips with ID " "{0}".format(network_name_or_id)) @@ -612,15 +608,11 @@ def _delete_floating_ip(self, floating_ip_id): def _neutron_delete_floating_ip(self, floating_ip_id): try: - proxy._json_response(self.network.delete( - "/floatingips/{fip_id}".format(fip_id=floating_ip_id), - error_message="unable to delete floating IP")) - except exc.OpenStackCloudResourceNotFound: + self.network.delete_ip( + floating_ip_id, ignore_missing=False + ) + except exceptions.ResourceNotFound: return False - except Exception as e: - raise exc.OpenStackCloudException( - "Unable to delete floating IP ID {fip_id}: {msg}".format( - fip_id=floating_ip_id, msg=str(e))) return True def _nova_delete_floating_ip(self, floating_ip_id): @@ -751,14 +743,9 @@ def _neutron_attach_ip_to_server( if fixed_address is not None: floating_ip_args['fixed_ip_address'] = fixed_address - return proxy._json_response( - self.network.put( - "/floatingips/{fip_id}".format(fip_id=floating_ip['id']), - json={'floatingip': floating_ip_args}), - error_message=("Error attaching IP {ip} to " - "server {server_id}".format( - ip=floating_ip['id'], - server_id=server['id']))) + return self.network.update_ip( + floating_ip, + **floating_ip_args) def _nova_attach_ip_to_server(self, server_id, floating_ip_id, fixed_address=None): @@ -809,13 +796,16 @@ def _neutron_detach_ip_from_server(self, server_id, floating_ip_id): f_ip = self.get_floating_ip(id=floating_ip_id) if f_ip is None or not f_ip['attached']: return False - exceptions.raise_from_response( - self.network.put( - "/floatingips/{fip_id}".format(fip_id=floating_ip_id), - json={"floatingip": {"port_id": None}}), - error_message=("Error detaching IP {ip} from " - "server {server_id}".format( - ip=floating_ip_id, server_id=server_id))) + try: + self.network.update_ip( + floating_ip_id, + port_id=None + ) + except exceptions.SDKException: + raise exceptions.SDKException( + ("Error detaching IP {ip} from " + "server {server_id}".format( + ip=floating_ip_id, server_id=server_id))) return True diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index 0200e3d83..caa2630e0 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -21,7 +21,6 @@ from openstack.cloud import _utils from openstack.cloud import exc from openstack import exceptions -from openstack import proxy class NetworkCloudMixin(_normalize.Normalizer): @@ -53,9 +52,12 @@ def search_networks(self, name_or_id=None, filters=None): :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ - networks = self.list_networks( - filters if isinstance(filters, dict) else None) - return _utils._filter_list(networks, name_or_id, filters) + query = {} + if name_or_id: + query['name'] = name_or_id + if filters: + query.update(filters) + return list(self.network.networks(**query)) def search_routers(self, name_or_id=None, filters=None): """Search routers @@ -69,9 +71,12 @@ def search_routers(self, name_or_id=None, filters=None): :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ - routers = self.list_routers( - filters if isinstance(filters, dict) else None) - return _utils._filter_list(routers, name_or_id, filters) + query = {} + if name_or_id: + query['name'] = name_or_id + if filters: + query.update(filters) + return list(self.network.routers(**query)) def search_subnets(self, name_or_id=None, filters=None): """Search subnets @@ -85,9 +90,12 @@ def search_subnets(self, name_or_id=None, filters=None): :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ - subnets = self.list_subnets( - filters if isinstance(filters, dict) else None) - return _utils._filter_list(subnets, name_or_id, filters) + query = {} + if name_or_id: + query['name'] = name_or_id + if filters: + query.update(filters) + return list(self.network.subnets(**query)) def search_ports(self, name_or_id=None, filters=None): """Search ports @@ -121,11 +129,11 @@ def list_networks(self, filters=None): # If the cloud is running nova-network, just return an empty list. if not self.has_service('network'): return [] + # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} - data = self.network.get("/networks", params=filters) - return self._get_and_munchify('networks', data) + return list(self.network.networks(**filters)) def list_routers(self, filters=None): """List all available routers. @@ -137,14 +145,11 @@ def list_routers(self, filters=None): # If the cloud is running nova-network, just return an empty list. if not self.has_service('network'): return [] + # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} - resp = self.network.get("/routers", params=filters) - data = proxy._json_response( - resp, - error_message="Error fetching router list") - return self._get_and_munchify('routers', data) + return list(self.network.routers(**filters)) def list_subnets(self, filters=None): """List all available subnets. @@ -156,11 +161,11 @@ def list_subnets(self, filters=None): # If the cloud is running nova-network, just return an empty list. if not self.has_service('network'): return [] + # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} - data = self.network.get("/subnets", params=filters) - return self._get_and_munchify('subnets', data) + return list(self.network.subnets(**filters)) def list_ports(self, filters=None): """List all available ports. @@ -199,11 +204,10 @@ def _list_ports(self, filters): # If the cloud is running nova-network, just return an empty list. if not self.has_service('network'): return [] - resp = self.network.get("/ports", params=filters) - data = proxy._json_response( - resp, - error_message="Error fetching port list") - return self._get_and_munchify('ports', data) + + if not filters: + filters = {} + return list(self.network.ports(**filters)) def get_qos_policy(self, name_or_id, filters=None): """Get a QoS policy by name or ID. @@ -231,6 +235,7 @@ def get_qos_policy(self, name_or_id, filters=None): if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') + if not filters: filters = {} return self.network.find_qos_policy( @@ -253,6 +258,7 @@ def search_qos_policies(self, name_or_id=None, filters=None): if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud') + query = {} if name_or_id: query['name'] = name_or_id @@ -334,7 +340,12 @@ def get_network(self, name_or_id, filters=None): found. """ - return _utils._get_entity(self, 'network', name_or_id, filters) + if not filters: + filters = {} + return self.network.find_network( + name_or_id=name_or_id, + ignore_missing=True, + **filters) def get_network_by_id(self, id): """ Get a network by ID @@ -342,14 +353,7 @@ def get_network_by_id(self, id): :param id: ID of the network. :returns: A network ``munch.Munch``. """ - resp = self.network.get('/networks/{id}'.format(id=id)) - data = proxy._json_response( - resp, - error_message="Error getting network with ID {id}".format(id=id) - ) - network = self._get_and_munchify('network', data) - - return network + return self.network.get_network(id) def get_router(self, name_or_id, filters=None): """Get a router by name or ID. @@ -374,7 +378,12 @@ def get_router(self, name_or_id, filters=None): found. """ - return _utils._get_entity(self, 'router', name_or_id, filters) + if not filters: + filters = {} + return self.network.find_router( + name_or_id=name_or_id, + ignore_missing=True, + **filters) def get_subnet(self, name_or_id, filters=None): """Get a subnet by name or ID. @@ -395,7 +404,12 @@ def get_subnet(self, name_or_id, filters=None): found. """ - return _utils._get_entity(self, 'subnet', name_or_id, filters) + if not filters: + filters = {} + return self.network.find_subnet( + name_or_id=name_or_id, + ignore_missing=True, + **filters) def get_subnet_by_id(self, id): """ Get a subnet by ID @@ -403,14 +417,7 @@ def get_subnet_by_id(self, id): :param id: ID of the subnet. :returns: A subnet ``munch.Munch``. """ - resp = self.network.get('/subnets/{id}'.format(id=id)) - data = proxy._json_response( - resp, - error_message="Error getting subnet with ID {id}".format(id=id) - ) - subnet = self._get_and_munchify('subnet', data) - - return subnet + return self.network.get_subnet(id) def get_port(self, name_or_id, filters=None): """Get a port by name or ID. @@ -434,7 +441,12 @@ def get_port(self, name_or_id, filters=None): :returns: A port ``munch.Munch`` or None if no matching port is found. """ - return _utils._get_entity(self, 'port', name_or_id, filters) + if not filters: + filters = {} + return self.network.find_port( + name_or_id=name_or_id, + ignore_missing=True, + **filters) def get_port_by_id(self, id): """ Get a port by ID @@ -442,14 +454,7 @@ def get_port_by_id(self, id): :param id: ID of the port. :returns: A port ``munch.Munch``. """ - resp = self.network.get('/ports/{id}'.format(id=id)) - data = proxy._json_response( - resp, - error_message="Error getting port with ID {id}".format(id=id) - ) - port = self._get_and_munchify('port', data) - - return port + return self.network.get_port(id) def create_network(self, name, shared=False, admin_state_up=True, external=False, provider=None, project_id=None, @@ -487,7 +492,7 @@ def create_network(self, name, shared=False, admin_state_up=True, network['shared'] = shared if project_id is not None: - network['tenant_id'] = project_id + network['project_id'] = project_id if availability_zone_hints is not None: if not isinstance(availability_zone_hints, list): @@ -535,11 +540,11 @@ def create_network(self, name, shared=False, admin_state_up=True, if dns_domain: network['dns_domain'] = dns_domain - data = self.network.post("/networks", json={'network': network}) + network = self.network.create_network(**network) # Reset cache so the new network is picked up self._reset_network_caches() - return self._get_and_munchify('network', data) + return network @_utils.valid_kwargs("name", "shared", "admin_state_up", "external", "provider", "mtu_size", "port_security_enabled", @@ -598,14 +603,11 @@ def update_network(self, name_or_id, **kwargs): raise exc.OpenStackCloudException( "Network %s not found." % name_or_id) - data = proxy._json_response(self.network.put( - "/networks/{net_id}".format(net_id=network.id), - json={"network": kwargs}), - error_message="Error updating network {0}".format(name_or_id)) + network = self.network.update_network(network, **kwargs) self._reset_network_caches() - return self._get_and_munchify('network', data) + return network def delete_network(self, name_or_id): """Delete a network. @@ -621,8 +623,7 @@ def delete_network(self, name_or_id): self.log.debug("Network %s not found for deleting", name_or_id) return False - exceptions.raise_from_response(self.network.delete( - "/networks/{network_id}".format(network_id=network['id']))) + self.network.delete_network(network) # Reset cache so the deleted network is removed self._reset_network_caches() @@ -643,12 +644,7 @@ def set_network_quotas(self, name_or_id, **kwargs): if not proj: raise exc.OpenStackCloudException("project does not exist") - exceptions.raise_from_response( - self.network.put( - '/quotas/{project_id}'.format(project_id=proj.id), - json={'quota': kwargs}), - error_message=("Error setting Neutron's quota for " - "project {0}".format(proj.id))) + self.network.update_quota(proj, **kwargs) def get_network_quotas(self, name_or_id, details=False): """ Get network quotas for a project @@ -663,14 +659,7 @@ def get_network_quotas(self, name_or_id, details=False): proj = self.get_project(name_or_id) if not proj: raise exc.OpenStackCloudException("project does not exist") - url = '/quotas/{project_id}'.format(project_id=proj.id) - if details: - url = url + "/details" - data = proxy._json_response( - self.network.get(url), - error_message=("Error fetching Neutron's quota for " - "project {0}".format(proj.id))) - return self._get_and_munchify('quota', data) + return self.network.get_quota(proj, details) def get_network_extensions(self): """Get Cloud provided network extensions @@ -691,11 +680,7 @@ def delete_network_quotas(self, name_or_id): proj = self.get_project(name_or_id) if not proj: raise exc.OpenStackCloudException("project does not exist") - exceptions.raise_from_response( - self.network.delete( - '/quotas/{project_id}'.format(project_id=proj.id)), - error_message=("Error deleting Neutron's quota for " - "project {0}".format(proj.id))) + self.network.delete_quota(proj) @_utils.valid_kwargs( 'action', 'description', 'destination_firewall_group_id', @@ -770,7 +755,10 @@ def get_firewall_rule(self, name_or_id, filters=None): """ if not filters: filters = {} - return self.network.find_firewall_rule(name_or_id, **filters) + return self.network.find_firewall_rule( + name_or_id, + ignore_missing=True, + **filters) def list_firewall_rules(self, filters=None): """ @@ -894,7 +882,10 @@ def get_firewall_policy(self, name_or_id, filters=None): """ if not filters: filters = {} - return self.network.find_firewall_policy(name_or_id, **filters) + return self.network.find_firewall_policy( + name_or_id, + ignore_missing=True, + **filters) def list_firewall_policies(self, filters=None): """ @@ -1093,7 +1084,10 @@ def get_firewall_group(self, name_or_id, filters=None): """ if not filters: filters = {} - return self.network.find_firewall_group(name_or_id, **filters) + return self.network.find_firewall_group( + name_or_id, + ignore_missing=True, + **filters) def list_firewall_groups(self, filters=None): """ @@ -1230,6 +1224,7 @@ def update_qos_policy(self, name_or_id, **kwargs): if not curr_policy: raise exc.OpenStackCloudException( "QoS policy %s not found." % name_or_id) + return self.network.update_qos_policy(curr_policy, **kwargs) def delete_qos_policy(self, name_or_id): @@ -1248,6 +1243,7 @@ def delete_qos_policy(self, name_or_id): if not policy: self.log.debug("QoS policy %s not found for deleting", name_or_id) return False + self.network.delete_qos_policy(policy) return True @@ -1358,6 +1354,7 @@ def create_qos_bandwidth_limit_rule(self, policy_name_or_id, max_kbps, "target cloud") kwargs['max_kbps'] = max_kbps + return self.network.create_qos_bandwidth_limit_rule(policy, **kwargs) @_utils.valid_kwargs("max_kbps", "max_burst_kbps", "direction") @@ -1703,6 +1700,7 @@ def create_qos_minimum_bandwidth_rule( name_or_id=policy_name_or_id)) kwargs['min_kbps'] = min_kbps + return self.network.create_qos_minimum_bandwidth_rule(policy, **kwargs) @_utils.valid_kwargs("min_kbps", "direction") @@ -1793,19 +1791,11 @@ def add_router_interface(self, router, subnet_id=None, port_id=None): :raises: OpenStackCloudException on operation error. """ - json_body = {} - if subnet_id: - json_body['subnet_id'] = subnet_id - if port_id: - json_body['port_id'] = port_id - - return proxy._json_response( - self.network.put( - "/routers/{router_id}/add_router_interface".format( - router_id=router['id']), - json=json_body), - error_message="Error attaching interface to router {0}".format( - router['id'])) + return self.network.add_interface_to_router( + router=router, + subnet_id=subnet_id, + port_id=port_id + ) def remove_router_interface(self, router, subnet_id=None, port_id=None): """Detach a subnet from an internal router interface. @@ -1824,23 +1814,15 @@ def remove_router_interface(self, router, subnet_id=None, port_id=None): :raises: OpenStackCloudException on operation error. """ - json_body = {} - if subnet_id: - json_body['subnet_id'] = subnet_id - if port_id: - json_body['port_id'] = port_id - - if not json_body: + if not subnet_id and not port_id: raise ValueError( "At least one of subnet_id or port_id must be supplied.") - exceptions.raise_from_response( - self.network.put( - "/routers/{router_id}/remove_router_interface".format( - router_id=router['id']), - json=json_body), - error_message="Error detaching interface from router {0}".format( - router['id'])) + self.network.remove_interface_from_router( + router=router, + subnet_id=subnet_id, + port_id=port_id + ) def list_router_interfaces(self, router, interface_type=None): """List all interfaces for a router. @@ -1923,10 +1905,7 @@ def create_router(self, name=None, admin_state_up=True, 'target cloud') router['availability_zone_hints'] = availability_zone_hints - data = proxy._json_response( - self.network.post("/routers", json={"router": router}), - error_message="Error creating router {0}".format(name)) - return self._get_and_munchify('router', data) + return self.network.create_router(**router) def update_router(self, name_or_id, name=None, admin_state_up=None, ext_gateway_net_id=None, enable_snat=None, @@ -1991,13 +1970,7 @@ def update_router(self, name_or_id, name=None, admin_state_up=None, raise exc.OpenStackCloudException( "Router %s not found." % name_or_id) - resp = self.network.put( - "/routers/{router_id}".format(router_id=curr_router['id']), - json={"router": router}) - data = proxy._json_response( - resp, - error_message="Error updating router {0}".format(name_or_id)) - return self._get_and_munchify('router', data) + return self.network.update_router(curr_router, **router) def delete_router(self, name_or_id): """Delete a logical router. @@ -2012,14 +1985,12 @@ def delete_router(self, name_or_id): :raises: OpenStackCloudException on operation error. """ - router = self.get_router(name_or_id) + router = self.network.find_router(name_or_id, ignore_missing=True) if not router: self.log.debug("Router %s not found for deleting", name_or_id) return False - exceptions.raise_from_response(self.network.delete( - "/routers/{router_id}".format(router_id=router['id']), - error_message="Error deleting router {0}".format(name_or_id))) + self.network.delete_router(router) return True @@ -2168,9 +2139,7 @@ def create_subnet(self, network_name_or_id, cidr=None, ip_version=4, if use_default_subnetpool: subnet['use_default_subnetpool'] = True - response = self.network.post("/subnets", json={"subnet": subnet}) - - return self._get_and_munchify('subnet', response) + return self.network.create_subnet(**subnet) def delete_subnet(self, name_or_id): """Delete a subnet. @@ -2185,13 +2154,13 @@ def delete_subnet(self, name_or_id): :raises: OpenStackCloudException on operation error. """ - subnet = self.get_subnet(name_or_id) + subnet = self.network.find_subnet(name_or_id, ignore_missing=True) if not subnet: self.log.debug("Subnet %s not found for deleting", name_or_id) return False - exceptions.raise_from_response(self.network.delete( - "/subnets/{subnet_id}".format(subnet_id=subnet['id']))) + self.network.delete_subnet(subnet) + return True def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, @@ -2276,10 +2245,7 @@ def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, raise exc.OpenStackCloudException( "Subnet %s not found." % name_or_id) - response = self.network.put( - "/subnets/{subnet_id}".format(subnet_id=curr_subnet['id']), - json={"subnet": subnet}) - return self._get_and_munchify('subnet', response) + return self.network.update_subnet(curr_subnet, **subnet) @_utils.valid_kwargs('name', 'admin_state_up', 'mac_address', 'fixed_ips', 'subnet_id', 'ip_address', 'security_groups', @@ -2346,11 +2312,7 @@ def create_port(self, network_id, **kwargs): """ kwargs['network_id'] = network_id - data = proxy._json_response( - self.network.post("/ports", json={'port': kwargs}), - error_message="Error creating port for network {0}".format( - network_id)) - return self._get_and_munchify('port', data) + return self.network.create_port(**kwargs) @_utils.valid_kwargs('name', 'admin_state_up', 'fixed_ips', 'security_groups', 'allowed_address_pairs', @@ -2417,12 +2379,7 @@ def update_port(self, name_or_id, **kwargs): raise exc.OpenStackCloudException( "failed to find port '{port}'".format(port=name_or_id)) - data = proxy._json_response( - self.network.put( - "/ports/{port_id}".format(port_id=port['id']), - json={"port": kwargs}), - error_message="Error updating port {0}".format(name_or_id)) - return self._get_and_munchify('port', data) + return self.network.update_port(port, **kwargs) def delete_port(self, name_or_id): """Delete a port @@ -2433,15 +2390,14 @@ def delete_port(self, name_or_id): :raises: OpenStackCloudException on operation error. """ - port = self.get_port(name_or_id=name_or_id) + port = self.network.find_port(name_or_id) + if port is None: self.log.debug("Port %s not found for deleting", name_or_id) return False - exceptions.raise_from_response( - self.network.delete( - "/ports/{port_id}".format(port_id=port['id'])), - error_message="Error deleting port {0}".format(name_or_id)) + self.network.delete_port(port) + return True def _get_port_ids(self, name_or_id_list, filters=None): diff --git a/openstack/cloud/_network_common.py b/openstack/cloud/_network_common.py index e83309403..ceba41021 100644 --- a/openstack/cloud/_network_common.py +++ b/openstack/cloud/_network_common.py @@ -94,9 +94,8 @@ def _set_interesting_networks(self): if (network['name'] in self._external_ipv4_names or network['id'] in self._external_ipv4_names): external_ipv4_networks.append(network) - elif ((('router:external' in network - and network['router:external']) - or network.get('provider:physical_network')) + elif ((network.is_router_external + or network.provider_physical_network) and network['name'] not in self._internal_ipv4_names and network['id'] not in self._internal_ipv4_names): external_ipv4_networks.append(network) @@ -105,8 +104,8 @@ def _set_interesting_networks(self): if (network['name'] in self._internal_ipv4_names or network['id'] in self._internal_ipv4_names): internal_ipv4_networks.append(network) - elif (not network.get('router:external', False) - and not network.get('provider:physical_network') + elif (not network.is_router_external + and not network.provider_physical_network and network['name'] not in self._external_ipv4_names and network['id'] not in self._external_ipv4_names): internal_ipv4_networks.append(network) @@ -115,7 +114,7 @@ def _set_interesting_networks(self): if (network['name'] in self._external_ipv6_names or network['id'] in self._external_ipv6_names): external_ipv6_networks.append(network) - elif (network.get('router:external') + elif (network.is_router_external and network['name'] not in self._internal_ipv6_names and network['id'] not in self._internal_ipv6_names): external_ipv6_networks.append(network) @@ -124,7 +123,7 @@ def _set_interesting_networks(self): if (network['name'] in self._internal_ipv6_names or network['id'] in self._internal_ipv6_names): internal_ipv6_networks.append(network) - elif (not network.get('router:external', False) + elif (not network.is_router_external and network['name'] not in self._external_ipv6_names and network['id'] not in self._external_ipv6_names): internal_ipv6_networks.append(network) @@ -144,7 +143,7 @@ def _set_interesting_networks(self): external_ipv4_floating_networks.append(network) nat_source = network elif self._nat_source is None: - if network.get('router:external'): + if network.is_router_external: external_ipv4_floating_networks.append(network) nat_source = nat_source or network diff --git a/openstack/cloud/_normalize.py b/openstack/cloud/_normalize.py index ea00c9f6d..92812b91d 100644 --- a/openstack/cloud/_normalize.py +++ b/openstack/cloud/_normalize.py @@ -610,10 +610,19 @@ def _normalize_floating_ips(self, ips): ] def _normalize_floating_ip(self, ip): - ret = munch.Munch() - # Copy incoming floating ip because of shared dicts in unittests - ip = ip.copy() + if isinstance(ip, resource.Resource): + ip = ip.to_dict(ignore_none=True, original_names=True) + location = ip.pop( + 'location', + self._get_current_location(project_id=ip.get('owner'))) + else: + location = self._get_current_location( + project_id=ip.get('owner')) + # This copy is to keep things from getting epically weird in tests + ip = ip.copy() + + ret = munch.Munch(location=location) fixed_ip_address = ip.pop('fixed_ip_address', ip.pop('fixed_ip', None)) floating_ip_address = ip.pop('floating_ip_address', ip.pop('ip', None)) diff --git a/openstack/tests/functional/cloud/test_quotas.py b/openstack/tests/functional/cloud/test_quotas.py index bbf066480..a1ae19745 100644 --- a/openstack/tests/functional/cloud/test_quotas.py +++ b/openstack/tests/functional/cloud/test_quotas.py @@ -66,21 +66,28 @@ def setUp(self): def test_quotas(self): '''Test quotas functionality''' quotas = self.operator_cloud.get_network_quotas('demo') - network = quotas['network'] - self.operator_cloud.set_network_quotas('demo', network=network + 1) + network = quotas['networks'] + self.operator_cloud.set_network_quotas('demo', networks=network + 1) self.assertEqual( network + 1, - self.operator_cloud.get_network_quotas('demo')['network']) + self.operator_cloud.get_network_quotas('demo')['networks']) self.operator_cloud.delete_network_quotas('demo') self.assertEqual( network, - self.operator_cloud.get_network_quotas('demo')['network']) + self.operator_cloud.get_network_quotas('demo')['networks']) def test_get_quotas_details(self): + quotas = [ + 'floating_ips', 'networks', 'ports', + 'rbac_policies', 'routers', 'subnets', + 'subnet_pools', 'security_group_rules', + 'security_groups'] expected_keys = ['limit', 'used', 'reserved'] '''Test getting details about quota usage''' quota_details = self.operator_cloud.get_network_quotas( 'demo', details=True) - for quota_values in quota_details.values(): - for expected_key in expected_keys: - self.assertTrue(expected_key in quota_values.keys()) + for quota in quotas: + quota_val = quota_details[quota] + if quota_val: + for expected_key in expected_keys: + self.assertTrue(expected_key in quota_val) diff --git a/openstack/tests/functional/cloud/test_router.py b/openstack/tests/functional/cloud/test_router.py index 7f4e2e146..8c18392f8 100644 --- a/openstack/tests/functional/cloud/test_router.py +++ b/openstack/tests/functional/cloud/test_router.py @@ -24,8 +24,8 @@ EXPECTED_TOPLEVEL_FIELDS = ( - 'id', 'name', 'admin_state_up', 'external_gateway_info', - 'tenant_id', 'routes', 'status' + 'id', 'name', 'is_admin_state_up', 'external_gateway_info', + 'project_id', 'routes', 'status' ) EXPECTED_GW_INFO_FIELDS = ('network_id', 'enable_snat', 'external_fixed_ips') diff --git a/openstack/tests/unit/cloud/test_baremetal_node.py b/openstack/tests/unit/cloud/test_baremetal_node.py index 09bf80d2e..85e7d7983 100644 --- a/openstack/tests/unit/cloud/test_baremetal_node.py +++ b/openstack/tests/unit/cloud/test_baremetal_node.py @@ -23,6 +23,7 @@ from openstack.cloud import exc from openstack import exceptions +from openstack.network.v2 import port as _port from openstack.tests import fakes from openstack.tests.unit import base @@ -1657,8 +1658,9 @@ def test_attach_port_to_machine(self): uri=self.get_mock_url( service_type='network', resource='ports', - base_url_append='v2.0'), - json={'ports': [{'id': vif_id}]}), + base_url_append='v2.0', + append=[vif_id]), + json={'id': vif_id}), dict( method='POST', uri=self.get_mock_url( @@ -1683,8 +1685,9 @@ def test_detach_port_from_machine(self): uri=self.get_mock_url( service_type='network', resource='ports', - base_url_append='v2.0'), - json={'ports': [{'id': vif_id}]}), + base_url_append='v2.0', + append=[vif_id]), + json={'id': vif_id}), dict( method='DELETE', uri=self.get_mock_url( @@ -1717,13 +1720,16 @@ def test_list_ports_attached_to_machine(self): uri=self.get_mock_url( service_type='network', resource='ports', - base_url_append='v2.0'), - json={'ports': [fake_port]}), + base_url_append='v2.0', + append=[vif_id]), + json=fake_port), ]) res = self.cloud.list_ports_attached_to_machine( self.fake_baremetal_node['uuid']) self.assert_calls() - self.assertEqual([fake_port], res) + self.assertEqual( + [_port.Port(**fake_port).to_dict(computed=False)], + [i.to_dict(computed=False) for i in res]) class TestUpdateMachinePatch(base.IronicTestCase): diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py index 38dd95668..ef072ace0 100644 --- a/openstack/tests/unit/cloud/test_caching.py +++ b/openstack/tests/unit/cloud/test_caching.py @@ -19,6 +19,7 @@ import openstack.cloud from openstack.cloud import meta from openstack.compute.v2 import flavor as _flavor +from openstack.network.v2 import port as _port from openstack import exceptions from openstack.tests import fakes from openstack.tests.unit import base @@ -565,7 +566,10 @@ def test_list_ports_filtered(self): ]}), ]) ports = self.cloud.list_ports(filters={'status': 'DOWN'}) - self.assertCountEqual([down_port], ports) + for a, b in zip([down_port], ports): + self.assertDictEqual( + _port.Port(**a).to_dict(computed=False), + b.to_dict(computed=False)) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_create_server.py b/openstack/tests/unit/cloud/test_create_server.py index 1da26b147..7b293fd62 100644 --- a/openstack/tests/unit/cloud/test_create_server.py +++ b/openstack/tests/unit/cloud/test_create_server.py @@ -555,7 +555,14 @@ def test_create_server_network_with_no_nics(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), + 'network', 'public', + append=['v2.0', 'networks', 'network-name']), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'networks'], + qs_elements=['name=network-name']), json={'networks': [network]}), dict(method='POST', uri=self.get_mock_url( @@ -600,7 +607,14 @@ def test_create_server_network_with_empty_nics(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), + 'network', 'public', + append=['v2.0', 'networks', 'network-name']), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'networks'], + qs_elements=['name=network-name']), json={'networks': [network]}), dict(method='POST', uri=self.get_mock_url( diff --git a/openstack/tests/unit/cloud/test_floating_ip_neutron.py b/openstack/tests/unit/cloud/test_floating_ip_neutron.py index 9b7ae0b54..e2ae862ce 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_neutron.py +++ b/openstack/tests/unit/cloud/test_floating_ip_neutron.py @@ -180,10 +180,10 @@ def test_list_floating_ips_with_filters(self): self.register_uris([ dict(method='GET', uri=('https://network.example.com/v2.0/floatingips?' - 'Foo=42'), + 'description=42'), json={'floatingips': []})]) - self.cloud.list_floating_ips(filters={'Foo': 42}) + self.cloud.list_floating_ips(filters={'description': 42}) self.assert_calls() @@ -260,7 +260,11 @@ def test_get_floating_ip_by_id(self): def test_create_floating_ip(self): self.register_uris([ dict(method='GET', - uri='https://network.example.com/v2.0/networks', + uri='https://network.example.com/v2.0/networks/my-network', + status_code=404), + dict(method='GET', + uri='https://network.example.com/v2.0/networks' + '?name=my-network', json={'networks': [self.mock_get_network_rep]}), dict(method='POST', uri='https://network.example.com/v2.0/floatingips', @@ -279,8 +283,8 @@ def test_create_floating_ip(self): def test_create_floating_ip_port_bad_response(self): self.register_uris([ dict(method='GET', - uri='https://network.example.com/v2.0/networks', - json={'networks': [self.mock_get_network_rep]}), + uri='https://network.example.com/v2.0/networks/my-network', + json=self.mock_get_network_rep), dict(method='POST', uri='https://network.example.com/v2.0/floatingips', json=self.mock_floating_ip_new_rep, @@ -300,7 +304,11 @@ def test_create_floating_ip_port_bad_response(self): def test_create_floating_ip_port(self): self.register_uris([ dict(method='GET', - uri='https://network.example.com/v2.0/networks', + uri='https://network.example.com/v2.0/networks/my-network', + status_code=404), + dict(method='GET', + uri='https://network.example.com/v2.0/networks' + '?name=my-network', json={'networks': [self.mock_get_network_rep]}), dict(method='POST', uri='https://network.example.com/v2.0/floatingips', @@ -395,7 +403,10 @@ def test_auto_ip_pool_no_reuse(self): # payloads taken from citycloud self.register_uris([ dict(method='GET', - uri='https://network.example.com/v2.0/networks', + uri='https://network.example.com/v2.0/networks/ext-net', + status_code=404), + dict(method='GET', + uri='https://network.example.com/v2.0/networks?name=ext-net', json={"networks": [{ "status": "ACTIVE", "subnets": [ @@ -416,24 +427,6 @@ def test_auto_ip_pool_no_reuse(self): "shared": False, "id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", "description": None - }, { - "status": "ACTIVE", - "subnets": ["f0ad1df5-53ee-473f-b86b-3604ea5591e9"], - "availability_zone_hints": [], - "availability_zones": ["nova"], - "name": "private", - "admin_state_up": True, - "tenant_id": "65222a4d09ea4c68934fa1028c77f394", - "created_at": "2016-10-22T13:46:26", - "tags": [], - "updated_at": "2016-10-22T13:46:26", - "ipv6_address_scope": None, - "router:external": False, - "ipv4_address_scope": None, - "shared": False, - "mtu": 1450, - "id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", - "description": "" }]}), dict(method='GET', uri='https://network.example.com/v2.0/ports' diff --git a/openstack/tests/unit/cloud/test_fwaas.py b/openstack/tests/unit/cloud/test_fwaas.py index e4870d1a9..b9121df8a 100644 --- a/openstack/tests/unit/cloud/test_fwaas.py +++ b/openstack/tests/unit/cloud/test_fwaas.py @@ -881,8 +881,15 @@ def test_create_firewall_group(self): json={'firewall_policies': [self.mock_ingress_policy]}), dict(method='GET', - uri=self.get_mock_url('network', 'public', - append=['v2.0', 'ports']), + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'ports', self.mock_port['name']]), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'ports'], + qs_elements=['name=%s' % self.mock_port['name']]), json={'ports': [self.mock_port]}), dict(method='POST', uri=self._make_mock_url('firewall_groups'), @@ -1078,8 +1085,15 @@ def test_update_firewall_group(self): deepcopy(self.mock_ingress_policy)]}), dict(method='GET', - uri=self.get_mock_url('network', 'public', - append=['v2.0', 'ports']), + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'ports', self.mock_port['name']]), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'ports'], + qs_elements=['name=%s' % self.mock_port['name']]), json={'ports': [self.mock_port]}), dict(method='PUT', uri=self._make_mock_url('firewall_groups', diff --git a/openstack/tests/unit/cloud/test_network.py b/openstack/tests/unit/cloud/test_network.py index 1ad7aab81..c8021041d 100644 --- a/openstack/tests/unit/cloud/test_network.py +++ b/openstack/tests/unit/cloud/test_network.py @@ -15,6 +15,7 @@ import openstack import openstack.cloud +from openstack.network.v2 import network as _network from openstack.tests.unit import base @@ -43,10 +44,11 @@ class TestNetwork(base.TestCase): 'qos_policy_id': None, 'name': 'netname', 'admin_state_up': True, - 'tenant_id': '861808a93da0484ea1767967c4df8a23', 'created_at': '2017-04-22T19:22:53Z', 'mtu': 0, - 'dns_domain': 'sample.openstack.org.' + 'dns_domain': 'sample.openstack.org.', + 'vlan_transparent': None, + 'segments': None, } network_availability_zone_extension = { @@ -59,6 +61,11 @@ class TestNetwork(base.TestCase): enabled_neutron_extensions = [network_availability_zone_extension] + def _compare_networks(self, exp, real): + self.assertDictEqual( + _network.Network(**exp).to_dict(computed=False), + real.to_dict(computed=False)) + def test_list_networks(self): net1 = {'id': '1', 'name': 'net1'} net2 = {'id': '2', 'name': 'net2'} @@ -69,7 +76,10 @@ def test_list_networks(self): json={'networks': [net1, net2]}) ]) nets = self.cloud.list_networks() - self.assertEqual([net1, net2], nets) + self.assertEqual( + [_network.Network(**i).to_dict(computed=False) for i in [ + net1, net2]], + [i.to_dict(computed=False) for i in nets]) self.assert_calls() def test_list_networks_filtered(self): @@ -95,7 +105,8 @@ def test_create_network(self): 'name': 'netname'}})) ]) network = self.cloud.create_network("netname") - self.assertEqual(self.mock_new_network_rep, network) + self._compare_networks( + self.mock_new_network_rep, network) self.assert_calls() def test_create_network_specific_tenant(self): @@ -111,10 +122,10 @@ def test_create_network_specific_tenant(self): json={'network': { 'admin_state_up': True, 'name': 'netname', - 'tenant_id': project_id}})) + 'project_id': project_id}})) ]) network = self.cloud.create_network("netname", project_id=project_id) - self.assertEqual(mock_new_network_rep, network) + self._compare_networks(mock_new_network_rep, network) self.assert_calls() def test_create_network_external(self): @@ -132,7 +143,7 @@ def test_create_network_external(self): 'router:external': True}})) ]) network = self.cloud.create_network("netname", external=True) - self.assertEqual(mock_new_network_rep, network) + self._compare_networks(mock_new_network_rep, network) self.assert_calls() def test_create_network_provider(self): @@ -160,7 +171,7 @@ def test_create_network_provider(self): json={'network': expected_send_params})) ]) network = self.cloud.create_network("netname", provider=provider_opts) - self.assertEqual(mock_new_network_rep, network) + self._compare_networks(mock_new_network_rep, network) self.assert_calls() def test_create_network_with_availability_zone_hints(self): @@ -181,7 +192,7 @@ def test_create_network_with_availability_zone_hints(self): ]) network = self.cloud.create_network("netname", availability_zone_hints=['nova']) - self.assertEqual(self.mock_new_network_rep, network) + self._compare_networks(self.mock_new_network_rep, network) self.assert_calls() def test_create_network_provider_ignored_value(self): @@ -210,7 +221,7 @@ def test_create_network_provider_ignored_value(self): json={'network': expected_send_params})) ]) network = self.cloud.create_network("netname", provider=provider_opts) - self.assertEqual(mock_new_network_rep, network) + self._compare_networks(mock_new_network_rep, network) self.assert_calls() def test_create_network_wrong_availability_zone_hints_type(self): @@ -249,7 +260,7 @@ def test_create_network_port_security_disabled(self): "netname", port_security_enabled=port_security_state ) - self.assertEqual(mock_new_network_rep, network) + self._compare_networks(mock_new_network_rep, network) self.assert_calls() def test_create_network_with_mtu(self): @@ -270,7 +281,7 @@ def test_create_network_with_mtu(self): network = self.cloud.create_network("netname", mtu_size=mtu_size ) - self.assertEqual(mock_new_network_rep, network) + self._compare_networks(mock_new_network_rep, network) self.assert_calls() def test_create_network_with_wrong_mtu_size(self): @@ -294,7 +305,13 @@ def test_delete_network(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), + 'network', 'public', + append=['v2.0', 'networks', network_name]), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'], + qs_elements=['name=%s' % network_name]), json={'networks': [network]}), dict(method='DELETE', uri=self.get_mock_url( @@ -309,7 +326,13 @@ def test_delete_network_not_found(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), + 'network', 'public', + append=['v2.0', 'networks', 'test-net']), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'], + qs_elements=['name=test-net']), json={'networks': []}), ]) self.assertFalse(self.cloud.delete_network('test-net')) @@ -322,7 +345,13 @@ def test_delete_network_exception(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), + 'network', 'public', + append=['v2.0', 'networks', network_name]), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'], + qs_elements=['name=%s' % network_name]), json={'networks': [network]}), dict(method='DELETE', uri=self.get_mock_url( diff --git a/openstack/tests/unit/cloud/test_port.py b/openstack/tests/unit/cloud/test_port.py index fc200986e..fee0be6fb 100644 --- a/openstack/tests/unit/cloud/test_port.py +++ b/openstack/tests/unit/cloud/test_port.py @@ -20,6 +20,7 @@ """ from openstack.cloud.exc import OpenStackCloudException +from openstack.network.v2 import port as _port from openstack.tests.unit import base @@ -139,6 +140,11 @@ class TestPort(base.TestCase): ] } + def _compare_ports(self, exp, real): + self.assertDictEqual( + _port.Port(**exp).to_dict(computed=False), + real.to_dict(computed=False)) + def test_create_port(self): self.register_uris([ dict(method="POST", @@ -154,7 +160,7 @@ def test_create_port(self): port = self.cloud.create_port( network_id='test-net-id', name='test-port-name', admin_state_up=True) - self.assertEqual(self.mock_neutron_port_create_rep['port'], port) + self._compare_ports(self.mock_neutron_port_create_rep['port'], port) self.assert_calls() def test_create_port_parameters(self): @@ -187,7 +193,7 @@ def test_update_port(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports']), + 'network', 'public', append=['v2.0', 'ports', port_id]), json=self.mock_neutron_port_list_rep), dict(method='PUT', uri=self.get_mock_url( @@ -200,7 +206,7 @@ def test_update_port(self): port = self.cloud.update_port( name_or_id=port_id, name='test-port-name-updated') - self.assertEqual(self.mock_neutron_port_update_rep['port'], port) + self._compare_ports(self.mock_neutron_port_update_rep['port'], port) self.assert_calls() def test_update_port_parameters(self): @@ -214,7 +220,7 @@ def test_update_port_exception(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports']), + 'network', 'public', append=['v2.0', 'ports', port_id]), json=self.mock_neutron_port_list_rep), dict(method='PUT', uri=self.get_mock_url( @@ -238,7 +244,8 @@ def test_list_ports(self): json=self.mock_neutron_port_list_rep) ]) ports = self.cloud.list_ports() - self.assertCountEqual(self.mock_neutron_port_list_rep['ports'], ports) + for a, b in zip(self.mock_neutron_port_list_rep['ports'], ports): + self._compare_ports(a, b) self.assert_calls() def test_list_ports_filtered(self): @@ -250,7 +257,8 @@ def test_list_ports_filtered(self): json=self.mock_neutron_port_list_rep) ]) ports = self.cloud.list_ports(filters={'status': 'DOWN'}) - self.assertCountEqual(self.mock_neutron_port_list_rep['ports'], ports) + for a, b in zip(self.mock_neutron_port_list_rep['ports'], ports): + self._compare_ports(a, b) self.assert_calls() def test_list_ports_exception(self): @@ -306,7 +314,13 @@ def test_delete_port(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports']), + 'network', 'public', + append=['v2.0', 'ports', 'first-port']), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports'], + qs_elements=['name=first-port']), json=self.mock_neutron_port_list_rep), dict(method='DELETE', uri=self.get_mock_url( @@ -321,8 +335,14 @@ def test_delete_port_not_found(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports']), - json=self.mock_neutron_port_list_rep) + 'network', 'public', append=['v2.0', 'ports', + 'non-existent']), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports'], + qs_elements=['name=non-existent']), + json={'ports': []}) ]) self.assertFalse(self.cloud.delete_port(name_or_id='non-existent')) self.assert_calls() @@ -334,7 +354,12 @@ def test_delete_subnet_multiple_found(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports']), + 'network', 'public', append=['v2.0', 'ports', port_name]), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports'], + qs_elements=['name=%s' % port_name]), json={'ports': [port1, port2]}) ]) self.assertRaises(OpenStackCloudException, @@ -348,7 +373,8 @@ def test_delete_subnet_multiple_using_id(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports']), + 'network', 'public', + append=['v2.0', 'ports', port1['id']]), json={'ports': [port1, port2]}), dict(method='DELETE', uri=self.get_mock_url( @@ -371,5 +397,5 @@ def test_get_port_by_id(self): ]) r = self.cloud.get_port_by_id(fake_port['id']) self.assertIsNotNone(r) - self.assertDictEqual(fake_port, r) + self._compare_ports(fake_port, r) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_quotas.py b/openstack/tests/unit/cloud/test_quotas.py index fc480dff5..ac38f44bd 100644 --- a/openstack/tests/unit/cloud/test_quotas.py +++ b/openstack/tests/unit/cloud/test_quotas.py @@ -11,6 +11,7 @@ # under the License. from openstack.cloud import exc +from openstack.network.v2 import quota as _quota from openstack.tests.unit import base fake_quota_set = { @@ -183,8 +184,16 @@ def test_neutron_get_quotas(self): append=['v2.0', 'quotas', project.project_id]), json={'quota': quota}) ]) - received_quota = self.cloud.get_network_quotas(project.project_id) - self.assertDictEqual(quota, received_quota) + received_quota = self.cloud.get_network_quotas( + project.project_id).to_dict(computed=False) + expected_quota = _quota.Quota(**quota).to_dict(computed=False) + received_quota.pop('id') + received_quota.pop('name') + expected_quota.pop('id') + expected_quota.pop('name') + + self.assertDictEqual(expected_quota, received_quota) + self.assert_calls() def test_neutron_get_quotas_details(self): @@ -233,12 +242,14 @@ def test_neutron_get_quotas_details(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'quotas', - '%s/details' % project.project_id]), + project.project_id, 'details']), json={'quota': quota_details}) ]) received_quota_details = self.cloud.get_network_quotas( project.project_id, details=True) - self.assertDictEqual(quota_details, received_quota_details) + self.assertDictEqual( + _quota.QuotaDetails(**quota_details).to_dict(computed=False), + received_quota_details.to_dict(computed=False)) self.assert_calls() def test_neutron_delete_quotas(self): diff --git a/openstack/tests/unit/cloud/test_router.py b/openstack/tests/unit/cloud/test_router.py index e3996b420..63661ffe7 100644 --- a/openstack/tests/unit/cloud/test_router.py +++ b/openstack/tests/unit/cloud/test_router.py @@ -17,6 +17,8 @@ import testtools from openstack.cloud import exc +from openstack.network.v2 import router as _router +from openstack.network.v2 import port as _port from openstack.tests.unit import base @@ -78,23 +80,40 @@ class TestRouter(base.TestCase): router_availability_zone_extension, router_extraroute_extension] + def _compare_routers(self, exp, real): + self.assertDictEqual( + _router.Router(**exp).to_dict(computed=False), + real.to_dict(computed=False)) + def test_get_router(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers']), + 'network', 'public', + append=['v2.0', 'routers', self.router_name]), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'routers'], + qs_elements=['name=%s' % self.router_name]), json={'routers': [self.mock_router_rep]}) ]) r = self.cloud.get_router(self.router_name) self.assertIsNotNone(r) - self.assertDictEqual(self.mock_router_rep, r) + self._compare_routers(self.mock_router_rep, r) self.assert_calls() def test_get_router_not_found(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers']), + 'network', 'public', + append=['v2.0', 'routers', 'mickey']), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'routers'], + qs_elements=['name=mickey']), json={'routers': []}) ]) r = self.cloud.get_router('mickey') @@ -114,7 +133,8 @@ def test_create_router(self): ]) new_router = self.cloud.create_router(name=self.router_name, admin_state_up=True) - self.assertDictEqual(self.mock_router_rep, new_router) + + self._compare_routers(self.mock_router_rep, new_router) self.assert_calls() def test_create_router_specific_tenant(self): @@ -269,8 +289,9 @@ def test_update_router(self): json={'extensions': self.enabled_neutron_extensions}), dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers']), - json={'routers': [self.mock_router_rep]}), + 'network', 'public', append=['v2.0', 'routers', + self.router_id]), + json=self.mock_router_rep), dict(method='PUT', uri=self.get_mock_url( 'network', 'public', @@ -283,14 +304,21 @@ def test_update_router(self): ]) new_router = self.cloud.update_router( self.router_id, name=new_router_name, routes=new_routes) - self.assertDictEqual(expected_router_rep, new_router) + + self._compare_routers(expected_router_rep, new_router) self.assert_calls() def test_delete_router(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers']), + 'network', 'public', + append=['v2.0', 'routers', self.router_name]), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'routers'], + qs_elements=['name=%s' % self.router_name]), json={'routers': [self.mock_router_rep]}), dict(method='DELETE', uri=self.get_mock_url( @@ -305,8 +333,14 @@ def test_delete_router_not_found(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers']), - json={'routers': []}), + 'network', 'public', + append=['v2.0', 'routers', self.router_name]), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'routers'], + qs_elements=['name=%s' % self.router_name]), + json={'routers': []}) ]) self.assertFalse(self.cloud.delete_router(self.router_name)) self.assert_calls() @@ -317,31 +351,20 @@ def test_delete_router_multiple_found(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers']), - json={'routers': [router1, router2]}), + 'network', 'public', + append=['v2.0', 'routers', 'mickey']), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'routers'], + qs_elements=['name=mickey']), + json={'routers': [router1, router2]}) ]) self.assertRaises(exc.OpenStackCloudException, self.cloud.delete_router, 'mickey') self.assert_calls() - def test_delete_router_multiple_using_id(self): - router1 = dict(id='123', name='mickey') - router2 = dict(id='456', name='mickey') - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers']), - json={'routers': [router1, router2]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'routers', '123']), - json={}) - ]) - self.assertTrue(self.cloud.delete_router("123")) - self.assert_calls() - def _get_mock_dict(self, owner, json): return dict(method='GET', uri=self.get_mock_url( @@ -389,16 +412,20 @@ def _test_list_router_interfaces(self, router, interface_type, for port_type in ['router_interface', 'router_interface_distributed', 'ha_router_replicated_interface']: - ports = {} if port_type == device_owner: ports = {'ports': [internal_port]} + else: + ports = {'ports': []} mock_uris.append(self._get_mock_dict(port_type, ports)) mock_uris.append(self._get_mock_dict('router_gateway', {'ports': [external_port]})) self.register_uris(mock_uris) ret = self.cloud.list_router_interfaces(router, interface_type) - self.assertEqual(expected_result, ret) + self.assertEqual( + [_port.Port(**i).to_dict(computed=False) for i in expected_result], + [i.to_dict(computed=False) for i in ret] + ) self.assert_calls() router = { diff --git a/openstack/tests/unit/cloud/test_subnet.py b/openstack/tests/unit/cloud/test_subnet.py index d8bafeaa2..87e678648 100644 --- a/openstack/tests/unit/cloud/test_subnet.py +++ b/openstack/tests/unit/cloud/test_subnet.py @@ -17,6 +17,7 @@ import testtools from openstack.cloud import exc +from openstack.network.v2 import subnet as _subnet from openstack.tests.unit import base @@ -36,8 +37,8 @@ class TestSubnet(base.TestCase): mock_subnet_rep = { 'allocation_pools': [{ - 'start': u'192.168.199.2', - 'end': u'192.168.199.254' + 'start': '192.168.199.2', + 'end': '192.168.199.254' }], 'cidr': subnet_cidr, 'created_at': '2017-04-24T20:22:23Z', @@ -66,16 +67,27 @@ class TestSubnet(base.TestCase): ] } + def _compare_subnets(self, exp, real): + self.assertDictEqual( + _subnet.Subnet(**exp).to_dict(computed=False), + real.to_dict(computed=False)) + def test_get_subnet(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets']), + 'network', 'public', + append=['v2.0', 'subnets', self.subnet_name]), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets'], + qs_elements=['name=%s' % self.subnet_name]), json={'subnets': [self.mock_subnet_rep]}) ]) r = self.cloud.get_subnet(self.subnet_name) self.assertIsNotNone(r) - self.assertDictEqual(self.mock_subnet_rep, r) + self._compare_subnets(self.mock_subnet_rep, r) self.assert_calls() def test_get_subnet_by_id(self): @@ -89,7 +101,7 @@ def test_get_subnet_by_id(self): ]) r = self.cloud.get_subnet_by_id(self.subnet_id) self.assertIsNotNone(r) - self.assertDictEqual(self.mock_subnet_rep, r) + self._compare_subnets(self.mock_subnet_rep, r) self.assert_calls() def test_create_subnet(self): @@ -103,7 +115,14 @@ def test_create_subnet(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), + 'network', 'public', + append=['v2.0', 'networks', self.network_name]), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'networks'], + qs_elements=['name=%s' % self.network_name]), json={'networks': [self.mock_network_rep]}), dict(method='POST', uri=self.get_mock_url( @@ -123,7 +142,7 @@ def test_create_subnet(self): allocation_pools=pool, dns_nameservers=dns, host_routes=routes) - self.assertDictEqual(mock_subnet_rep, subnet) + self._compare_subnets(mock_subnet_rep, subnet) self.assert_calls() def test_create_subnet_string_ip_version(self): @@ -131,7 +150,14 @@ def test_create_subnet_string_ip_version(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), + 'network', 'public', + append=['v2.0', 'networks', self.network_name]), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'networks'], + qs_elements=['name=%s' % self.network_name]), json={'networks': [self.mock_network_rep]}), dict(method='POST', uri=self.get_mock_url( @@ -146,7 +172,7 @@ def test_create_subnet_string_ip_version(self): ]) subnet = self.cloud.create_subnet( self.network_name, self.subnet_cidr, ip_version='4') - self.assertDictEqual(self.mock_subnet_rep, subnet) + self._compare_subnets(self.mock_subnet_rep, subnet) self.assert_calls() def test_create_subnet_bad_ip_version(self): @@ -154,8 +180,15 @@ def test_create_subnet_bad_ip_version(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': [self.mock_network_rep]}) + 'network', 'public', + append=['v2.0', 'networks', self.network_name]), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'networks'], + qs_elements=['name=%s' % self.network_name]), + json={'networks': [self.mock_network_rep]}), ]) with testtools.ExpectedException( exc.OpenStackCloudException, @@ -175,7 +208,14 @@ def test_create_subnet_without_gateway_ip(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), + 'network', 'public', + append=['v2.0', 'networks', self.network_name]), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'networks'], + qs_elements=['name=%s' % self.network_name]), json={'networks': [self.mock_network_rep]}), dict(method='POST', uri=self.get_mock_url( @@ -195,7 +235,7 @@ def test_create_subnet_without_gateway_ip(self): allocation_pools=pool, dns_nameservers=dns, disable_gateway_ip=True) - self.assertDictEqual(mock_subnet_rep, subnet) + self._compare_subnets(mock_subnet_rep, subnet) self.assert_calls() def test_create_subnet_with_gateway_ip(self): @@ -209,7 +249,14 @@ def test_create_subnet_with_gateway_ip(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), + 'network', 'public', + append=['v2.0', 'networks', self.network_name]), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'networks'], + qs_elements=['name=%s' % self.network_name]), json={'networks': [self.mock_network_rep]}), dict(method='POST', uri=self.get_mock_url( @@ -229,14 +276,21 @@ def test_create_subnet_with_gateway_ip(self): allocation_pools=pool, dns_nameservers=dns, gateway_ip=gateway) - self.assertDictEqual(mock_subnet_rep, subnet) + self._compare_subnets(mock_subnet_rep, subnet) self.assert_calls() def test_create_subnet_conflict_gw_ops(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), + 'network', 'public', + append=['v2.0', 'networks', 'kooky']), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'networks'], + qs_elements=['name=kooky']), json={'networks': [self.mock_network_rep]}) ]) gateway = '192.168.200.3' @@ -250,8 +304,15 @@ def test_create_subnet_bad_network(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': [self.mock_network_rep]}) + 'network', 'public', + append=['v2.0', 'networks', 'duck']), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'networks'], + qs_elements=['name=duck']), + json={'networks': [self.mock_network_rep]}), ]) self.assertRaises(exc.OpenStackCloudException, self.cloud.create_subnet, @@ -264,8 +325,15 @@ def test_create_subnet_non_unique_network(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': [net1, net2]}) + 'network', 'public', + append=['v2.0', 'networks', self.network_name]), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'networks'], + qs_elements=['name=%s' % self.network_name]), + json={'networks': [net1, net2]}), ]) self.assertRaises(exc.OpenStackCloudException, self.cloud.create_subnet, @@ -289,7 +357,14 @@ def test_create_subnet_from_subnetpool_with_prefixlen(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), + 'network', 'public', + append=['v2.0', 'networks', self.network_name]), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'networks'], + qs_elements=['name=%s' % self.network_name]), json={'networks': [self.mock_network_rep]}), dict(method='POST', uri=self.get_mock_url( @@ -312,14 +387,25 @@ def test_create_subnet_from_subnetpool_with_prefixlen(self): use_default_subnetpool=True, prefixlen=self.prefix_length, host_routes=routes) - self.assertDictEqual(mock_subnet_rep, subnet) + mock_subnet_rep.update( + { + 'prefixlen': self.prefix_length, + 'use_default_subnetpool': True + }) + self._compare_subnets(mock_subnet_rep, subnet) self.assert_calls() def test_delete_subnet(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets']), + 'network', 'public', + append=['v2.0', 'subnets', self.subnet_name]), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets'], + qs_elements=['name=%s' % self.subnet_name]), json={'subnets': [self.mock_subnet_rep]}), dict(method='DELETE', uri=self.get_mock_url( @@ -334,7 +420,13 @@ def test_delete_subnet_not_found(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets']), + 'network', 'public', + append=['v2.0', 'subnets', 'goofy']), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets'], + qs_elements=['name=goofy']), json={'subnets': []}) ]) self.assertFalse(self.cloud.delete_subnet('goofy')) @@ -346,7 +438,13 @@ def test_delete_subnet_multiple_found(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets']), + 'network', 'public', + append=['v2.0', 'subnets', self.subnet_name]), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets'], + qs_elements=['name=%s' % self.subnet_name]), json={'subnets': [subnet1, subnet2]}) ]) self.assertRaises(exc.OpenStackCloudException, @@ -354,14 +452,14 @@ def test_delete_subnet_multiple_found(self): self.subnet_name) self.assert_calls() - def test_delete_subnet_multiple_using_id(self): + def test_delete_subnet_using_id(self): subnet1 = dict(id='123', name=self.subnet_name) - subnet2 = dict(id='456', name=self.subnet_name) self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets']), - json={'subnets': [subnet1, subnet2]}), + 'network', 'public', append=['v2.0', 'subnets', + subnet1['id']]), + json=subnet1), dict(method='DELETE', uri=self.get_mock_url( 'network', 'public', @@ -377,8 +475,9 @@ def test_update_subnet(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets']), - json={'subnets': [self.mock_subnet_rep]}), + 'network', 'public', + append=['v2.0', 'subnets', self.subnet_id]), + json=self.mock_subnet_rep), dict(method='PUT', uri=self.get_mock_url( 'network', 'public', @@ -388,7 +487,7 @@ def test_update_subnet(self): json={'subnet': {'name': 'goofy'}})) ]) subnet = self.cloud.update_subnet(self.subnet_id, subnet_name='goofy') - self.assertDictEqual(expected_subnet, subnet) + self._compare_subnets(expected_subnet, subnet) self.assert_calls() def test_update_subnet_gateway_ip(self): @@ -398,8 +497,9 @@ def test_update_subnet_gateway_ip(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets']), - json={'subnets': [self.mock_subnet_rep]}), + 'network', 'public', + append=['v2.0', 'subnets', self.subnet_id]), + json=self.mock_subnet_rep), dict(method='PUT', uri=self.get_mock_url( 'network', 'public', @@ -409,7 +509,7 @@ def test_update_subnet_gateway_ip(self): json={'subnet': {'gateway_ip': gateway}})) ]) subnet = self.cloud.update_subnet(self.subnet_id, gateway_ip=gateway) - self.assertDictEqual(expected_subnet, subnet) + self._compare_subnets(expected_subnet, subnet) self.assert_calls() def test_update_subnet_disable_gateway_ip(self): @@ -418,8 +518,9 @@ def test_update_subnet_disable_gateway_ip(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets']), - json={'subnets': [self.mock_subnet_rep]}), + 'network', 'public', + append=['v2.0', 'subnets', self.subnet_id]), + json=self.mock_subnet_rep), dict(method='PUT', uri=self.get_mock_url( 'network', 'public', @@ -430,7 +531,7 @@ def test_update_subnet_disable_gateway_ip(self): ]) subnet = self.cloud.update_subnet(self.subnet_id, disable_gateway_ip=True) - self.assertDictEqual(expected_subnet, subnet) + self._compare_subnets(expected_subnet, subnet) self.assert_calls() def test_update_subnet_conflict_gw_ops(self): diff --git a/releasenotes/notes/use-proxy-layer-dfc3764d52bc1f2a.yaml b/releasenotes/notes/use-proxy-layer-dfc3764d52bc1f2a.yaml new file mode 100644 index 000000000..5e9882d4f --- /dev/null +++ b/releasenotes/notes/use-proxy-layer-dfc3764d52bc1f2a.yaml @@ -0,0 +1,7 @@ +--- +upgrade: + - | + Networking functions of the cloud layer return now resource objects + `openstack.resource`. While those still implement Munch interface and are + accessible as dictionary modification of an instance might be causing + issues (i.e. forbidden). From 70a06d99908c9a56cb28715c62e8ebe6f076a87d Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 15 Jan 2021 16:35:47 +0100 Subject: [PATCH 2835/3836] Stop sending tenant_id to Neutron For a long time we are supporting both tenant_id and project_id for most of the networking resources. tenant_id is deprecated since long time already and we should stop sending it ourselves to Neutron, while leaving it as attribute where it was present. Change-Id: I19c4b76258eb22897b3ae5d9573b01ea4ce8022b --- openstack/cloud/_network.py | 2 +- openstack/network/v2/address_group.py | 6 +++-- openstack/network/v2/address_scope.py | 6 +++-- .../network/v2/auto_allocated_topology.py | 4 +++- openstack/network/v2/floating_ip.py | 8 +++++-- openstack/network/v2/health_monitor.py | 7 +++--- openstack/network/v2/load_balancer.py | 4 +++- openstack/network/v2/metering_label.py | 7 +++--- openstack/network/v2/metering_label_rule.py | 6 +++-- openstack/network/v2/network.py | 2 +- .../network/v2/network_ip_availability.py | 6 +++-- openstack/network/v2/pool.py | 6 +++-- openstack/network/v2/pool_member.py | 6 +++-- openstack/network/v2/port.py | 7 +++--- openstack/network/v2/qos_policy.py | 2 +- openstack/network/v2/quota.py | 2 +- openstack/network/v2/rbac_policy.py | 4 +++- openstack/network/v2/router.py | 7 +++--- openstack/network/v2/security_group.py | 2 +- openstack/network/v2/security_group_rule.py | 2 +- openstack/network/v2/service_profile.py | 7 +++--- openstack/network/v2/subnet.py | 7 +++--- openstack/network/v2/subnet_pool.py | 7 +++--- openstack/network/v2/trunk.py | 6 +++-- openstack/network/v2/vpn_service.py | 4 +++- openstack/tests/unit/cloud/test_router.py | 2 +- .../unit/network/v2/test_address_group.py | 6 ++--- .../unit/network/v2/test_address_scope.py | 4 ++-- .../v2/test_auto_allocated_topology.py | 4 ++-- .../tests/unit/network/v2/test_floating_ip.py | 24 +++++++++++++++++-- .../unit/network/v2/test_health_monitor.py | 4 ++-- .../unit/network/v2/test_load_balancer.py | 4 ++-- .../unit/network/v2/test_metering_label.py | 4 ++-- .../network/v2/test_metering_label_rule.py | 6 ++--- .../tests/unit/network/v2/test_network.py | 2 +- .../v2/test_network_ip_availability.py | 8 +++---- openstack/tests/unit/network/v2/test_pool.py | 4 ++-- .../tests/unit/network/v2/test_pool_member.py | 4 ++-- openstack/tests/unit/network/v2/test_port.py | 6 ++--- .../tests/unit/network/v2/test_qos_policy.py | 5 ++-- openstack/tests/unit/network/v2/test_quota.py | 12 +++++----- .../tests/unit/network/v2/test_rbac_policy.py | 4 ++-- .../tests/unit/network/v2/test_router.py | 8 +++---- .../unit/network/v2/test_security_group.py | 12 +++++----- .../network/v2/test_security_group_rule.py | 8 +++---- .../unit/network/v2/test_service_profile.py | 4 ++-- .../tests/unit/network/v2/test_subnet.py | 4 ++-- .../tests/unit/network/v2/test_subnet_pool.py | 4 ++-- openstack/tests/unit/network/v2/test_trunk.py | 4 ++-- .../tests/unit/network/v2/test_vpn_service.py | 4 ++-- ...stop-using-tenant-id-42eb35139ba9eeff.yaml | 4 ++++ 51 files changed, 169 insertions(+), 113 deletions(-) create mode 100644 releasenotes/notes/stop-using-tenant-id-42eb35139ba9eeff.yaml diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index caa2630e0..96f261ab3 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -1887,7 +1887,7 @@ def create_router(self, name=None, admin_state_up=True, 'admin_state_up': admin_state_up } if project_id is not None: - router['tenant_id'] = project_id + router['project_id'] = project_id if name: router['name'] = name ext_gw_info = self._build_external_gateway_info( diff --git a/openstack/network/v2/address_group.py b/openstack/network/v2/address_group.py index f9f5d6c94..3fb256f5f 100644 --- a/openstack/network/v2/address_group.py +++ b/openstack/network/v2/address_group.py @@ -33,7 +33,7 @@ class AddressGroup(resource.Resource): _query_mapping = resource.QueryParameters( "sort_key", "sort_dir", 'name', 'description', - project_id='tenant_id' + 'project_id' ) # Properties @@ -44,7 +44,9 @@ class AddressGroup(resource.Resource): #: The address group name. description = resource.Body('description') #: The ID of the project that owns the address group. - project_id = resource.Body('tenant_id') + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) #: The IP addresses of the address group. addresses = resource.Body('addresses', type=list) diff --git a/openstack/network/v2/address_scope.py b/openstack/network/v2/address_scope.py index 36b13356a..1ad1cbd18 100644 --- a/openstack/network/v2/address_scope.py +++ b/openstack/network/v2/address_scope.py @@ -30,7 +30,7 @@ class AddressScope(resource.Resource): _query_mapping = resource.QueryParameters( 'name', 'ip_version', - project_id='tenant_id', + 'project_id', is_shared='shared', ) @@ -38,7 +38,9 @@ class AddressScope(resource.Resource): #: The address scope name. name = resource.Body('name') #: The ID of the project that owns the address scope. - project_id = resource.Body('tenant_id') + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) #: The IP address family of the address scope. #: *Type: int* ip_version = resource.Body('ip_version', type=int) diff --git a/openstack/network/v2/auto_allocated_topology.py b/openstack/network/v2/auto_allocated_topology.py index ee52de2d6..69c52a7aa 100644 --- a/openstack/network/v2/auto_allocated_topology.py +++ b/openstack/network/v2/auto_allocated_topology.py @@ -36,7 +36,9 @@ class AutoAllocatedTopology(resource.Resource): #: Will return in error if resources have not been configured correctly #: To use this feature auto-allocated-topology, subnet_allocation, #: external-net and router extensions must be enabled and set up. - project_id = resource.Body('tenant_id') + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) class ValidateTopology(AutoAllocatedTopology): diff --git a/openstack/network/v2/floating_ip.py b/openstack/network/v2/floating_ip.py index 2f94226f1..fec267033 100644 --- a/openstack/network/v2/floating_ip.py +++ b/openstack/network/v2/floating_ip.py @@ -28,11 +28,13 @@ class FloatingIP(_base.NetworkResource, resource.TagMixin): allow_delete = True allow_list = True + # For backward compatibility include tenant_id as query param _query_mapping = resource.QueryParameters( 'description', 'fixed_ip_address', 'floating_ip_address', 'floating_network_id', 'port_id', 'router_id', 'status', 'subnet_id', - project_id='tenant_id', + 'project_id', 'tenant_id', + tenant_id='project_id', **resource.TagMixin._tag_query_parameters) # Properties @@ -68,7 +70,9 @@ class FloatingIP(_base.NetworkResource, resource.TagMixin): #: The ID of the QoS policy attached to the floating IP. qos_policy_id = resource.Body('qos_policy_id') #: The ID of the project this floating IP is associated with. - project_id = resource.Body('tenant_id') + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) #: The ID of an associated router. router_id = resource.Body('router_id') #: The floating IP status. Value is ``ACTIVE`` or ``DOWN``. diff --git a/openstack/network/v2/health_monitor.py b/openstack/network/v2/health_monitor.py index 6fecb07d5..7859d0b12 100644 --- a/openstack/network/v2/health_monitor.py +++ b/openstack/network/v2/health_monitor.py @@ -29,9 +29,8 @@ class HealthMonitor(resource.Resource): _query_mapping = resource.QueryParameters( 'delay', 'expected_codes', 'http_method', 'max_retries', - 'timeout', 'type', 'url_path', + 'timeout', 'type', 'url_path', 'project_id', is_admin_state_up='adminstate_up', - project_id='tenant_id', ) # Properties @@ -54,7 +53,9 @@ class HealthMonitor(resource.Resource): #: The ID of the pool associated with this health monitor pool_id = resource.Body('pool_id') #: The ID of the project this health monitor is associated with. - project_id = resource.Body('tenant_id') + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) #: The maximum number of seconds for a monitor to wait for a #: connection to be established before it times out. This value must #: be less than the delay value. diff --git a/openstack/network/v2/load_balancer.py b/openstack/network/v2/load_balancer.py index 2d90a34df..7ac082a55 100644 --- a/openstack/network/v2/load_balancer.py +++ b/openstack/network/v2/load_balancer.py @@ -44,7 +44,9 @@ class LoadBalancer(resource.Resource): #: *Type: list of dicts which contain the pool IDs* pool_ids = resource.Body('pools', type=list) #: The ID of the project this load balancer is associated with. - project_id = resource.Body('tenant_id') + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) #: The name of the provider. provider = resource.Body('provider') #: Status of load balancer provisioning, e.g. ACTIVE, INACTIVE. diff --git a/openstack/network/v2/metering_label.py b/openstack/network/v2/metering_label.py index fbd6f55f0..6bedb4ad3 100644 --- a/openstack/network/v2/metering_label.py +++ b/openstack/network/v2/metering_label.py @@ -28,9 +28,8 @@ class MeteringLabel(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'description', 'name', + 'description', 'name', 'project_id', is_shared='shared', - project_id='tenant_id' ) # Properties @@ -39,7 +38,9 @@ class MeteringLabel(resource.Resource): #: Name of the metering label. name = resource.Body('name') #: The ID of the project this metering label is associated with. - project_id = resource.Body('tenant_id') + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) #: Indicates whether this label is shared across all tenants. #: *Type: bool* is_shared = resource.Body('shared', type=bool) diff --git a/openstack/network/v2/metering_label_rule.py b/openstack/network/v2/metering_label_rule.py index 445a8df35..a4c286345 100644 --- a/openstack/network/v2/metering_label_rule.py +++ b/openstack/network/v2/metering_label_rule.py @@ -29,7 +29,7 @@ class MeteringLabelRule(resource.Resource): _query_mapping = resource.QueryParameters( 'direction', 'metering_label_id', 'remote_ip_prefix', - 'source_ip_prefix', 'destination_ip_prefix', project_id='tenant_id', + 'source_ip_prefix', 'destination_ip_prefix', 'project_id', ) # Properties @@ -44,7 +44,9 @@ class MeteringLabelRule(resource.Resource): #: The metering label ID to associate with this metering label rule. metering_label_id = resource.Body('metering_label_id') #: The ID of the project this metering label rule is associated with. - project_id = resource.Body('tenant_id') + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) #: The remote IP prefix to be associated with this metering label rule. remote_ip_prefix = resource.Body( 'remote_ip_prefix', deprecated=True, diff --git a/openstack/network/v2/network.py b/openstack/network/v2/network.py index 884c52a86..730dbdd3a 100644 --- a/openstack/network/v2/network.py +++ b/openstack/network/v2/network.py @@ -29,13 +29,13 @@ class Network(_base.NetworkResource, resource.TagMixin): # NOTE: We don't support query on list or datetime fields yet _query_mapping = resource.QueryParameters( 'description', 'name', 'status', + 'project_id', ipv4_address_scope_id='ipv4_address_scope', ipv6_address_scope_id='ipv6_address_scope', is_admin_state_up='admin_state_up', is_port_security_enabled='port_security_enabled', is_router_external='router:external', is_shared='shared', - project_id='tenant_id', provider_network_type='provider:network_type', provider_physical_network='provider:physical_network', provider_segmentation_id='provider:segmentation_id', diff --git a/openstack/network/v2/network_ip_availability.py b/openstack/network/v2/network_ip_availability.py index 838a6b8f9..a1b1d8d41 100644 --- a/openstack/network/v2/network_ip_availability.py +++ b/openstack/network/v2/network_ip_availability.py @@ -30,7 +30,7 @@ class NetworkIPAvailability(resource.Resource): _query_mapping = resource.QueryParameters( 'ip_version', 'network_id', 'network_name', - project_id='tenant_id' + 'project_id' ) # Properties @@ -42,7 +42,9 @@ class NetworkIPAvailability(resource.Resource): #: *Type: list* subnet_ip_availability = resource.Body('subnet_ip_availability', type=list) #: The ID of the project this network IP availability is associated with. - project_id = resource.Body('tenant_id') + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) #: The total ips of a network. #: *Type: int* total_ips = resource.Body('total_ips', type=int) diff --git a/openstack/network/v2/pool.py b/openstack/network/v2/pool.py index bc72653ac..e5215a712 100644 --- a/openstack/network/v2/pool.py +++ b/openstack/network/v2/pool.py @@ -30,8 +30,8 @@ class Pool(resource.Resource): _query_mapping = resource.QueryParameters( 'description', 'lb_algorithm', 'name', 'protocol', 'provider', 'subnet_id', 'virtual_ip_id', 'listener_id', + 'project_id', is_admin_state_up='admin_state_up', - project_id='tenant_id', load_balancer_id='loadbalancer_id', ) @@ -67,7 +67,9 @@ class Pool(resource.Resource): #: Pool name. Does not have to be unique. name = resource.Body('name') #: The ID of the project this pool is associated with. - project_id = resource.Body('tenant_id') + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) #: The protocol of the pool, which is TCP, HTTP, or HTTPS. protocol = resource.Body('protocol') #: The provider name of the load balancer service. diff --git a/openstack/network/v2/pool_member.py b/openstack/network/v2/pool_member.py index 3f872ce28..aba6fb9cc 100644 --- a/openstack/network/v2/pool_member.py +++ b/openstack/network/v2/pool_member.py @@ -29,8 +29,8 @@ class PoolMember(resource.Resource): _query_mapping = resource.QueryParameters( 'address', 'name', 'protocol_port', 'subnet_id', 'weight', + 'project_id', is_admin_state_up='admin_state_up', - project_id='tenant_id', ) # Properties @@ -44,7 +44,9 @@ class PoolMember(resource.Resource): #: Name of the pool member. name = resource.Body('name') #: The ID of the project this pool member is associated with. - project_id = resource.Body('tenant_id') + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) #: The port on which the application is hosted. protocol_port = resource.Body('protocol_port', type=int) #: Subnet ID in which to access this pool member. diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index a5a25ec8c..4a4ed8b5f 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -32,10 +32,9 @@ class Port(_base.NetworkResource, resource.TagMixin): 'binding:vif_type', 'binding:vnic_type', 'description', 'device_id', 'device_owner', 'fields', 'fixed_ips', 'id', 'ip_address', 'mac_address', 'name', 'network_id', 'status', - 'subnet_id', + 'subnet_id', 'project_id', is_admin_state_up='admin_state_up', is_port_security_enabled='port_security_enabled', - project_id='tenant_id', **resource.TagMixin._tag_query_parameters ) @@ -112,7 +111,9 @@ class Port(_base.NetworkResource, resource.TagMixin): numa_affinity_policy = resource.Body('numa_affinity_policy') #: The ID of the project who owns the network. Only administrative #: users can specify a project ID other than their own. - project_id = resource.Body('tenant_id') + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) #: Whether to propagate uplink status of the port. *Type: bool* propagate_uplink_status = resource.Body('propagate_uplink_status', type=bool) diff --git a/openstack/network/v2/qos_policy.py b/openstack/network/v2/qos_policy.py index 7ca983a7f..00e1414e8 100644 --- a/openstack/network/v2/qos_policy.py +++ b/openstack/network/v2/qos_policy.py @@ -30,7 +30,7 @@ class QoSPolicy(resource.Resource, resource.TagMixin): _query_mapping = resource.QueryParameters( 'name', 'description', 'is_default', - project_id='tenant_id', + 'project_id', is_shared='shared', **resource.TagMixin._tag_query_parameters ) diff --git a/openstack/network/v2/quota.py b/openstack/network/v2/quota.py index ddbd36935..64794d087 100644 --- a/openstack/network/v2/quota.py +++ b/openstack/network/v2/quota.py @@ -114,7 +114,7 @@ class QuotaDetails(Quota): #: The maximum amount of ports you can create. *Type: dict* ports = resource.Body('port', type=dict) #: The ID of the project these quota values are for. - project_id = resource.Body('tenant_id', alternate_id=True) + project_id = resource.Body('project_id', alternate_id=True) #: The maximum amount of RBAC policies you can create. *Type: dict* rbac_policies = resource.Body('rbac_policy', type=dict) #: The maximum amount of routers you can create. *Type: int* diff --git a/openstack/network/v2/rbac_policy.py b/openstack/network/v2/rbac_policy.py index 85a1e1abf..09cb60743 100644 --- a/openstack/network/v2/rbac_policy.py +++ b/openstack/network/v2/rbac_policy.py @@ -38,7 +38,9 @@ class RBACPolicy(resource.Resource): #: The ID of the project this RBAC will be enforced. target_project_id = resource.Body('target_tenant') #: The owner project ID. - project_id = resource.Body('tenant_id') + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) #: Type of the object that this RBAC policy affects. object_type = resource.Body('object_type') #: Action for the RBAC policy. diff --git a/openstack/network/v2/router.py b/openstack/network/v2/router.py index b714b6e48..30de0d6ee 100644 --- a/openstack/network/v2/router.py +++ b/openstack/network/v2/router.py @@ -30,11 +30,10 @@ class Router(_base.NetworkResource, resource.TagMixin): # NOTE: We don't support query on datetime, list or dict fields _query_mapping = resource.QueryParameters( - 'description', 'flavor_id', 'name', 'status', + 'description', 'flavor_id', 'name', 'status', 'project_id', is_admin_state_up='admin_state_up', is_distributed='distributed', is_ha='ha', - project_id='tenant_id', **resource.TagMixin._tag_query_parameters ) @@ -66,7 +65,9 @@ class Router(_base.NetworkResource, resource.TagMixin): #: The router name. name = resource.Body('name') #: The ID of the project this router is associated with. - project_id = resource.Body('tenant_id') + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) #: Revision number of the router. *Type: int* revision_number = resource.Body('revision', type=int) #: The extra routes configuration for the router. diff --git a/openstack/network/v2/security_group.py b/openstack/network/v2/security_group.py index ee35f29c8..7499b28c4 100644 --- a/openstack/network/v2/security_group.py +++ b/openstack/network/v2/security_group.py @@ -48,6 +48,6 @@ class SecurityGroup(_base.NetworkResource, resource.TagMixin): #: objects. *Type: list* security_group_rules = resource.Body('security_group_rules', type=list) #: The ID of the project this security group is associated with. - tenant_id = resource.Body('tenant_id') + tenant_id = resource.Body('tenant_id', deprecated=True) #: Timestamp when the security group was last updated. updated_at = resource.Body('updated_at') diff --git a/openstack/network/v2/security_group_rule.py b/openstack/network/v2/security_group_rule.py index bd1846021..d3fc0f816 100644 --- a/openstack/network/v2/security_group_rule.py +++ b/openstack/network/v2/security_group_rule.py @@ -84,7 +84,7 @@ class SecurityGroupRule(_base.NetworkResource, resource.TagMixin): #: The security group ID to associate with this security group rule. security_group_id = resource.Body('security_group_id') #: The ID of the project this security group rule is associated with. - tenant_id = resource.Body('tenant_id') + tenant_id = resource.Body('tenant_id', deprecated=True) #: Timestamp when the security group rule was last updated. updated_at = resource.Body('updated_at') diff --git a/openstack/network/v2/service_profile.py b/openstack/network/v2/service_profile.py index 59532724b..26cf7bc6b 100644 --- a/openstack/network/v2/service_profile.py +++ b/openstack/network/v2/service_profile.py @@ -28,9 +28,8 @@ class ServiceProfile(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'description', 'driver', + 'description', 'driver', 'project_id', is_enabled='enabled', - project_id='tenant_id' ) # Properties #: Description of the service flavor profile. @@ -42,4 +41,6 @@ class ServiceProfile(resource.Resource): #: Metainformation of the service flavor profile meta_info = resource.Body('metainfo') #: The owner project ID - project_id = resource.Body('tenant_id') + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) diff --git a/openstack/network/v2/subnet.py b/openstack/network/v2/subnet.py index 8ddbdbe4d..65f07f42e 100644 --- a/openstack/network/v2/subnet.py +++ b/openstack/network/v2/subnet.py @@ -30,9 +30,8 @@ class Subnet(_base.NetworkResource, resource.TagMixin): _query_mapping = resource.QueryParameters( 'cidr', 'description', 'gateway_ip', 'ip_version', 'ipv6_address_mode', 'ipv6_ra_mode', 'name', 'network_id', - 'segment_id', 'dns_publish_fixed_ip', + 'segment_id', 'dns_publish_fixed_ip', 'project_id', is_dhcp_enabled='enable_dhcp', - project_id='tenant_id', subnet_pool_id='subnetpool_id', use_default_subnet_pool='use_default_subnetpool', **resource.TagMixin._tag_query_parameters @@ -75,7 +74,9 @@ class Subnet(_base.NetworkResource, resource.TagMixin): #: The prefix length to use for subnet allocation from a subnet pool prefix_length = resource.Body('prefixlen') #: The ID of the project this subnet is associated with. - project_id = resource.Body('tenant_id') + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) #: The ID of the segment this subnet is associated with. segment_id = resource.Body('segment_id') #: Service types for this subnet diff --git a/openstack/network/v2/subnet_pool.py b/openstack/network/v2/subnet_pool.py index 734197e70..4eb05b616 100644 --- a/openstack/network/v2/subnet_pool.py +++ b/openstack/network/v2/subnet_pool.py @@ -29,9 +29,8 @@ class SubnetPool(resource.Resource, resource.TagMixin): _query_mapping = resource.QueryParameters( 'address_scope_id', 'description', 'ip_version', 'is_default', - 'name', + 'name', 'project_id', is_shared='shared', - project_id='tenant_id', **resource.TagMixin._tag_query_parameters ) @@ -69,7 +68,9 @@ class SubnetPool(resource.Resource, resource.TagMixin): #: The subnet pool name. name = resource.Body('name') #: The ID of the project that owns the subnet pool. - project_id = resource.Body('tenant_id') + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) #: A list of subnet prefixes that are assigned to the subnet pool. #: The adjacent prefixes are merged and treated as a single prefix. #: *Type: list* diff --git a/openstack/network/v2/trunk.py b/openstack/network/v2/trunk.py index b0e5aeb15..47d2f5fac 100644 --- a/openstack/network/v2/trunk.py +++ b/openstack/network/v2/trunk.py @@ -31,7 +31,7 @@ class Trunk(resource.Resource, resource.TagMixin): _query_mapping = resource.QueryParameters( 'name', 'description', 'port_id', 'status', 'sub_ports', - project_id='tenant_id', + 'project_id', is_admin_state_up='admin_state_up', **resource.TagMixin._tag_query_parameters ) @@ -41,7 +41,9 @@ class Trunk(resource.Resource, resource.TagMixin): name = resource.Body('name') #: The ID of the project who owns the trunk. Only administrative #: users can specify a project ID other than their own. - project_id = resource.Body('tenant_id') + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) #: The trunk description. description = resource.Body('description') #: The administrative state of the port, which is up ``True`` or diff --git a/openstack/network/v2/vpn_service.py b/openstack/network/v2/vpn_service.py index d24f9980a..0c4ba92fc 100644 --- a/openstack/network/v2/vpn_service.py +++ b/openstack/network/v2/vpn_service.py @@ -44,7 +44,9 @@ class VPNService(resource.Resource): #: ID of the router into which the VPN service is inserted. router_id = resource.Body('router_id') #: The ID of the project this vpnservice is associated with. - project_id = resource.Body('tenant_id') + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) #: The vpnservice status. status = resource.Body('status') #: The ID of the subnet on which the tenant wants the vpnservice. diff --git a/openstack/tests/unit/cloud/test_router.py b/openstack/tests/unit/cloud/test_router.py index 63661ffe7..aac8d791c 100644 --- a/openstack/tests/unit/cloud/test_router.py +++ b/openstack/tests/unit/cloud/test_router.py @@ -151,7 +151,7 @@ def test_create_router_specific_tenant(self): json={'router': { 'name': self.router_name, 'admin_state_up': True, - 'tenant_id': new_router_tenant_id}})) + 'project_id': new_router_tenant_id}})) ]) self.cloud.create_router(self.router_name, diff --git a/openstack/tests/unit/network/v2/test_address_group.py b/openstack/tests/unit/network/v2/test_address_group.py index 356857293..dab387afd 100644 --- a/openstack/tests/unit/network/v2/test_address_group.py +++ b/openstack/tests/unit/network/v2/test_address_group.py @@ -19,7 +19,7 @@ 'id': IDENTIFIER, 'name': '1', 'description': '2', - 'tenant_id': '3', + 'project_id': '3', 'addresses': ['10.0.0.1/32'] } @@ -39,7 +39,7 @@ def test_basic(self): self.assertDictEqual({"name": "name", "description": "description", - "project_id": "tenant_id", + "project_id": "project_id", "sort_key": "sort_key", "sort_dir": "sort_dir", "limit": "limit", @@ -51,5 +51,5 @@ def test_make_it(self): self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['description'], sot.description) - self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertCountEqual(EXAMPLE['addresses'], sot.addresses) diff --git a/openstack/tests/unit/network/v2/test_address_scope.py b/openstack/tests/unit/network/v2/test_address_scope.py index 8f7a2059d..0badd9faf 100644 --- a/openstack/tests/unit/network/v2/test_address_scope.py +++ b/openstack/tests/unit/network/v2/test_address_scope.py @@ -20,7 +20,7 @@ 'ip_version': 4, 'name': '1', 'shared': True, - 'tenant_id': '2', + 'project_id': '2', } @@ -43,4 +43,4 @@ def test_make_it(self): self.assertEqual(EXAMPLE['ip_version'], sot.ip_version) self.assertEqual(EXAMPLE['name'], sot.name) self.assertTrue(sot.is_shared) - self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) diff --git a/openstack/tests/unit/network/v2/test_auto_allocated_topology.py b/openstack/tests/unit/network/v2/test_auto_allocated_topology.py index bc836f0bc..973de536c 100644 --- a/openstack/tests/unit/network/v2/test_auto_allocated_topology.py +++ b/openstack/tests/unit/network/v2/test_auto_allocated_topology.py @@ -15,7 +15,7 @@ EXAMPLE = { - 'tenant_id': '1', + 'project_id': '1', 'dry_run': False, } @@ -34,4 +34,4 @@ def test_basic(self): def test_make_it(self): topo = auto_allocated_topology.AutoAllocatedTopology(**EXAMPLE) - self.assertEqual(EXAMPLE['tenant_id'], topo.project_id) + self.assertEqual(EXAMPLE['project_id'], topo.project_id) diff --git a/openstack/tests/unit/network/v2/test_floating_ip.py b/openstack/tests/unit/network/v2/test_floating_ip.py index 1c057b64c..c00466837 100644 --- a/openstack/tests/unit/network/v2/test_floating_ip.py +++ b/openstack/tests/unit/network/v2/test_floating_ip.py @@ -25,7 +25,7 @@ 'id': IDENTIFIER, 'port_id': '5', 'qos_policy_id': '51', - 'tenant_id': '6', + 'project_id': '6', 'router_id': '7', 'description': '8', 'dns_domain': '9', @@ -61,7 +61,7 @@ def test_make_it(self): sot.floating_network_id) self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['port_id'], sot.port_id) - self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['router_id'], sot.router_id) self.assertEqual(EXAMPLE['description'], sot.description) self.assertEqual(EXAMPLE['dns_domain'], sot.dns_domain) @@ -72,6 +72,26 @@ def test_make_it(self): self.assertEqual(EXAMPLE['subnet_id'], sot.subnet_id) self.assertEqual(EXAMPLE['tags'], sot.tags) + self.assertDictEqual( + {'limit': 'limit', + 'marker': 'marker', + 'description': 'description', + 'project_id': 'project_id', + 'tenant_id': 'project_id', + 'status': 'status', + 'port_id': 'port_id', + 'subnet_id': 'subnet_id', + 'router_id': 'router_id', + 'fixed_ip_address': 'fixed_ip_address', + 'floating_ip_address': 'floating_ip_address', + 'floating_network_id': 'floating_network_id', + 'tags': 'tags', + 'any_tags': 'tags-any', + 'not_tags': 'not-tags', + 'not_any_tags': 'not-tags-any', + }, + sot._query_mapping._mapping) + def test_find_available(self): mock_session = mock.Mock(spec=proxy.Proxy) mock_session.get_filter = mock.Mock(return_value={}) diff --git a/openstack/tests/unit/network/v2/test_health_monitor.py b/openstack/tests/unit/network/v2/test_health_monitor.py index 143738266..a101e361c 100644 --- a/openstack/tests/unit/network/v2/test_health_monitor.py +++ b/openstack/tests/unit/network/v2/test_health_monitor.py @@ -24,7 +24,7 @@ 'max_retries': '6', 'pools': [{'id': '7'}], 'pool_id': '7', - 'tenant_id': '8', + 'project_id': '8', 'timeout': '9', 'type': '10', 'url_path': '11', @@ -55,7 +55,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['max_retries'], sot.max_retries) self.assertEqual(EXAMPLE['pools'], sot.pool_ids) self.assertEqual(EXAMPLE['pool_id'], sot.pool_id) - self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['timeout'], sot.timeout) self.assertEqual(EXAMPLE['type'], sot.type) self.assertEqual(EXAMPLE['url_path'], sot.url_path) diff --git a/openstack/tests/unit/network/v2/test_load_balancer.py b/openstack/tests/unit/network/v2/test_load_balancer.py index b66db3790..f6f1a51dc 100644 --- a/openstack/tests/unit/network/v2/test_load_balancer.py +++ b/openstack/tests/unit/network/v2/test_load_balancer.py @@ -23,7 +23,7 @@ 'name': '5', 'operating_status': '6', 'provisioning_status': '7', - 'tenant_id': '8', + 'project_id': '8', 'vip_address': '9', 'vip_subnet_id': '10', 'vip_port_id': '11', @@ -55,7 +55,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['operating_status'], sot.operating_status) self.assertEqual(EXAMPLE['provisioning_status'], sot.provisioning_status) - self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['vip_address'], sot.vip_address) self.assertEqual(EXAMPLE['vip_subnet_id'], sot.vip_subnet_id) self.assertEqual(EXAMPLE['vip_port_id'], sot.vip_port_id) diff --git a/openstack/tests/unit/network/v2/test_metering_label.py b/openstack/tests/unit/network/v2/test_metering_label.py index 50efac047..2a300346f 100644 --- a/openstack/tests/unit/network/v2/test_metering_label.py +++ b/openstack/tests/unit/network/v2/test_metering_label.py @@ -19,7 +19,7 @@ 'description': '1', 'id': IDENTIFIER, 'name': '3', - 'tenant_id': '4', + 'project_id': '4', 'shared': False, } @@ -42,5 +42,5 @@ def test_make_it(self): self.assertEqual(EXAMPLE['description'], sot.description) self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['name'], sot.name) - self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['shared'], sot.is_shared) diff --git a/openstack/tests/unit/network/v2/test_metering_label_rule.py b/openstack/tests/unit/network/v2/test_metering_label_rule.py index be005940e..95146acaf 100644 --- a/openstack/tests/unit/network/v2/test_metering_label_rule.py +++ b/openstack/tests/unit/network/v2/test_metering_label_rule.py @@ -20,7 +20,7 @@ 'excluded': False, 'id': IDENTIFIER, 'metering_label_id': '4', - 'tenant_id': '5', + 'project_id': '5', 'remote_ip_prefix': '6', } @@ -44,7 +44,7 @@ def test_make_it(self): self.assertFalse(sot.is_excluded) self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['metering_label_id'], sot.metering_label_id) - self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['remote_ip_prefix'], sot.remote_ip_prefix) def test_make_it_source_and_destination(self): @@ -58,7 +58,7 @@ def test_make_it_source_and_destination(self): self.assertEqual(custom_example['id'], sot.id) self.assertEqual( custom_example['metering_label_id'], sot.metering_label_id) - self.assertEqual(custom_example['tenant_id'], sot.project_id) + self.assertEqual(custom_example['project_id'], sot.project_id) self.assertEqual( custom_example['remote_ip_prefix'], sot.remote_ip_prefix) diff --git a/openstack/tests/unit/network/v2/test_network.py b/openstack/tests/unit/network/v2/test_network.py index 37a51cb82..7f9257c65 100644 --- a/openstack/tests/unit/network/v2/test_network.py +++ b/openstack/tests/unit/network/v2/test_network.py @@ -99,7 +99,7 @@ def test_make_it(self): 'marker': 'marker', 'description': 'description', 'name': 'name', - 'project_id': 'tenant_id', + 'project_id': 'project_id', 'status': 'status', 'ipv4_address_scope_id': 'ipv4_address_scope', 'ipv6_address_scope_id': 'ipv6_address_scope', diff --git a/openstack/tests/unit/network/v2/test_network_ip_availability.py b/openstack/tests/unit/network/v2/test_network_ip_availability.py index c40b8b7fb..dcb8ad7b0 100644 --- a/openstack/tests/unit/network/v2/test_network_ip_availability.py +++ b/openstack/tests/unit/network/v2/test_network_ip_availability.py @@ -19,7 +19,7 @@ 'network_id': IDENTIFIER, 'network_name': 'private', 'subnet_ip_availability': [], - 'tenant_id': '5', + 'project_id': '5', 'total_ips': 6, 'used_ips': 10, } @@ -32,7 +32,7 @@ "subnet_name": "private-subnet", "ip_version": 6, "cidr": "fd91:c3ba:e818::/64", "total_ips": 18446744073709551614}], - 'tenant_id': '2', + 'project_id': '2', 'total_ips': 1844, 'used_ips': 6, } @@ -58,7 +58,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['network_name'], sot.network_name) self.assertEqual(EXAMPLE['subnet_ip_availability'], sot.subnet_ip_availability) - self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['total_ips'], sot.total_ips) self.assertEqual(EXAMPLE['used_ips'], sot.used_ips) @@ -70,6 +70,6 @@ def test_make_it_with_optional(self): sot.network_name) self.assertEqual(EXAMPLE_WITH_OPTIONAL['subnet_ip_availability'], sot.subnet_ip_availability) - self.assertEqual(EXAMPLE_WITH_OPTIONAL['tenant_id'], sot.project_id) + self.assertEqual(EXAMPLE_WITH_OPTIONAL['project_id'], sot.project_id) self.assertEqual(EXAMPLE_WITH_OPTIONAL['total_ips'], sot.total_ips) self.assertEqual(EXAMPLE_WITH_OPTIONAL['used_ips'], sot.used_ips) diff --git a/openstack/tests/unit/network/v2/test_pool.py b/openstack/tests/unit/network/v2/test_pool.py index 1dae98a57..36098582b 100644 --- a/openstack/tests/unit/network/v2/test_pool.py +++ b/openstack/tests/unit/network/v2/test_pool.py @@ -27,7 +27,7 @@ 'listener_id': '6', 'members': [{'id': '7'}], 'name': '8', - 'tenant_id': '9', + 'project_id': '9', 'protocol': '10', 'provider': '11', 'session_persistence': '12', @@ -67,7 +67,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['listener_id'], sot.listener_id) self.assertEqual(EXAMPLE['members'], sot.member_ids) self.assertEqual(EXAMPLE['name'], sot.name) - self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['protocol'], sot.protocol) self.assertEqual(EXAMPLE['provider'], sot.provider) self.assertEqual(EXAMPLE['session_persistence'], diff --git a/openstack/tests/unit/network/v2/test_pool_member.py b/openstack/tests/unit/network/v2/test_pool_member.py index 9d2df196b..478b2fefb 100644 --- a/openstack/tests/unit/network/v2/test_pool_member.py +++ b/openstack/tests/unit/network/v2/test_pool_member.py @@ -19,7 +19,7 @@ 'address': '1', 'admin_state_up': True, 'id': IDENTIFIER, - 'tenant_id': '4', + 'project_id': '4', 'protocol_port': 5, 'subnet_id': '6', 'weight': 7, @@ -46,7 +46,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['address'], sot.address) self.assertTrue(sot.is_admin_state_up) self.assertEqual(EXAMPLE['id'], sot.id) - self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['protocol_port'], sot.protocol_port) self.assertEqual(EXAMPLE['subnet_id'], sot.subnet_id) self.assertEqual(EXAMPLE['weight'], sot.weight) diff --git a/openstack/tests/unit/network/v2/test_port.py b/openstack/tests/unit/network/v2/test_port.py index 7fd88b631..861384dca 100644 --- a/openstack/tests/unit/network/v2/test_port.py +++ b/openstack/tests/unit/network/v2/test_port.py @@ -53,7 +53,7 @@ 'revision_number': 22, 'security_groups': ['23'], 'status': '25', - 'tenant_id': '26', + 'project_id': '26', 'trunk_details': { 'trunk_id': '27', 'sub_ports': [{ @@ -98,7 +98,7 @@ def test_basic(self): "is_admin_state_up": "admin_state_up", "is_port_security_enabled": "port_security_enabled", - "project_id": "tenant_id", + "project_id": "project_id", "limit": "limit", "marker": "marker", "any_tags": "tags-any", @@ -146,6 +146,6 @@ def test_make_it(self): self.assertEqual(EXAMPLE['revision_number'], sot.revision_number) self.assertEqual(EXAMPLE['security_groups'], sot.security_group_ids) self.assertEqual(EXAMPLE['status'], sot.status) - self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['trunk_details'], sot.trunk_details) self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) diff --git a/openstack/tests/unit/network/v2/test_qos_policy.py b/openstack/tests/unit/network/v2/test_qos_policy.py index 295f56341..3908ebf4f 100644 --- a/openstack/tests/unit/network/v2/test_qos_policy.py +++ b/openstack/tests/unit/network/v2/test_qos_policy.py @@ -21,7 +21,7 @@ 'description': 'QoS policy description', 'name': 'qos-policy-name', 'shared': True, - 'tenant_id': '2', + 'project_id': '2', 'rules': [uuid.uuid4().hex], 'is_default': False, 'tags': ['3'] @@ -46,8 +46,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['description'], sot.description) self.assertEqual(EXAMPLE['name'], sot.name) - self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) - self.assertEqual(EXAMPLE['tenant_id'], sot.tenant_id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['rules'], sot.rules) self.assertEqual(EXAMPLE['is_default'], sot.is_default) self.assertEqual(EXAMPLE['tags'], sot.tags) diff --git a/openstack/tests/unit/network/v2/test_quota.py b/openstack/tests/unit/network/v2/test_quota.py index 54d7a6949..21feceb50 100644 --- a/openstack/tests/unit/network/v2/test_quota.py +++ b/openstack/tests/unit/network/v2/test_quota.py @@ -20,7 +20,7 @@ 'floatingip': 1, 'network': 2, 'port': 3, - 'tenant_id': '4', + 'project_id': '4', 'router': 5, 'subnet': 6, 'subnetpool': 7, @@ -53,7 +53,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['floatingip'], sot.floating_ips) self.assertEqual(EXAMPLE['network'], sot.networks) self.assertEqual(EXAMPLE['port'], sot.ports) - self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['router'], sot.routers) self.assertEqual(EXAMPLE['subnet'], sot.subnets) self.assertEqual(EXAMPLE['subnetpool'], sot.subnet_pools) @@ -74,10 +74,10 @@ def test_prepare_request(self): self.assertNotIn('id', response) def test_alternate_id(self): - my_tenant_id = 'my-tenant-id' - body = {'tenant_id': my_tenant_id, 'network': 12345} + my_project_id = 'my-tenant-id' + body = {'project_id': my_project_id, 'network': 12345} quota_obj = quota.Quota(**body) - self.assertEqual(my_tenant_id, + self.assertEqual(my_project_id, resource.Resource._get_id(quota_obj)) @@ -99,7 +99,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['floatingip'], sot.floating_ips) self.assertEqual(EXAMPLE['network'], sot.networks) self.assertEqual(EXAMPLE['port'], sot.ports) - self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['router'], sot.routers) self.assertEqual(EXAMPLE['subnet'], sot.subnets) self.assertEqual(EXAMPLE['subnetpool'], sot.subnet_pools) diff --git a/openstack/tests/unit/network/v2/test_rbac_policy.py b/openstack/tests/unit/network/v2/test_rbac_policy.py index ea0a9a364..6b5464b6c 100644 --- a/openstack/tests/unit/network/v2/test_rbac_policy.py +++ b/openstack/tests/unit/network/v2/test_rbac_policy.py @@ -20,7 +20,7 @@ 'object_id': IDENTIFIER, 'object_type': 'network', 'target_tenant': '10', - 'tenant_id': '5', + 'project_id': '5', } @@ -43,4 +43,4 @@ def test_make_it(self): self.assertEqual(EXAMPLE['object_id'], sot.object_id) self.assertEqual(EXAMPLE['object_type'], sot.object_type) self.assertEqual(EXAMPLE['target_tenant'], sot.target_project_id) - self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) diff --git a/openstack/tests/unit/network/v2/test_router.py b/openstack/tests/unit/network/v2/test_router.py index 8c0a2fec5..c109a5fd5 100644 --- a/openstack/tests/unit/network/v2/test_router.py +++ b/openstack/tests/unit/network/v2/test_router.py @@ -34,7 +34,7 @@ 'revision': 7, 'routes': ['8'], 'status': '9', - 'tenant_id': '10', + 'project_id': '10', 'updated_at': 'timestamp2', } @@ -57,7 +57,7 @@ 'destination': '10.0.3.1/24' }], 'status': 'ACTIVE', - 'tenant_id': '2', + 'project_id': '2', } @@ -93,7 +93,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['revision'], sot.revision_number) self.assertEqual(EXAMPLE['routes'], sot.routes) self.assertEqual(EXAMPLE['status'], sot.status) - self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) def test_make_it_with_optional(self): @@ -113,7 +113,7 @@ def test_make_it_with_optional(self): self.assertEqual(EXAMPLE_WITH_OPTIONAL['name'], sot.name) self.assertEqual(EXAMPLE_WITH_OPTIONAL['routes'], sot.routes) self.assertEqual(EXAMPLE_WITH_OPTIONAL['status'], sot.status) - self.assertEqual(EXAMPLE_WITH_OPTIONAL['tenant_id'], sot.project_id) + self.assertEqual(EXAMPLE_WITH_OPTIONAL['project_id'], sot.project_id) def test_add_interface_subnet(self): # Add subnet to a router diff --git a/openstack/tests/unit/network/v2/test_security_group.py b/openstack/tests/unit/network/v2/test_security_group.py index 7a4c74163..13ff1b4ee 100644 --- a/openstack/tests/unit/network/v2/test_security_group.py +++ b/openstack/tests/unit/network/v2/test_security_group.py @@ -23,7 +23,7 @@ "protocol": None, "ethertype": "IPv6", - "tenant_id": "4", + "project_id": "4", "port_range_max": None, "port_range_min": None, "id": "5", @@ -38,7 +38,7 @@ "remote_ip_prefix": None, "protocol": None, "ethertype": "IPv6", - "tenant_id": "4", + "project_id": "4", "port_range_max": None, "port_range_min": None, "id": "6", @@ -57,7 +57,7 @@ 'stateful': True, 'revision_number': 3, 'security_group_rules': RULES, - 'tenant_id': '4', + 'project_id': '4', 'project_id': '4', 'updated_at': '2016-10-14T12:16:57.233772', 'tags': ['5'] @@ -86,12 +86,12 @@ def test_basic(self): 'name': 'name', 'not_any_tags': 'not-tags-any', 'not_tags': 'not-tags', - 'project_id': 'project_id', + 'tenant_id': 'tenant_id', 'revision_number': 'revision_number', 'sort_dir': 'sort_dir', 'sort_key': 'sort_key', 'tags': 'tags', - 'tenant_id': 'tenant_id', + 'project_id': 'project_id', 'stateful': 'stateful', }, sot._query_mapping._mapping) @@ -106,7 +106,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['security_group_rules'], sot.security_group_rules) self.assertEqual(dict, type(sot.security_group_rules[0])) - self.assertEqual(EXAMPLE['tenant_id'], sot.tenant_id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) self.assertEqual(EXAMPLE['tags'], sot.tags) diff --git a/openstack/tests/unit/network/v2/test_security_group_rule.py b/openstack/tests/unit/network/v2/test_security_group_rule.py index cfaf43da0..eca85dfeb 100644 --- a/openstack/tests/unit/network/v2/test_security_group_rule.py +++ b/openstack/tests/unit/network/v2/test_security_group_rule.py @@ -28,7 +28,7 @@ 'remote_ip_prefix': '8', 'revision_number': 9, 'security_group_id': '10', - 'tenant_id': '11', + 'project_id': '11', 'project_id': '11', 'updated_at': '12', 'remote_address_group_id': '13' @@ -59,7 +59,7 @@ def test_basic(self): 'not_tags': 'not-tags', 'port_range_max': 'port_range_max', 'port_range_min': 'port_range_min', - 'project_id': 'project_id', + 'tenant_id': 'tenant_id', 'protocol': 'protocol', 'remote_group_id': 'remote_group_id', 'remote_address_group_id': @@ -70,7 +70,7 @@ def test_basic(self): 'sort_dir': 'sort_dir', 'sort_key': 'sort_key', 'tags': 'tags', - 'tenant_id': 'tenant_id' + 'project_id': 'project_id' }, sot._query_mapping._mapping) @@ -90,6 +90,6 @@ def test_make_it(self): self.assertEqual(EXAMPLE['remote_ip_prefix'], sot.remote_ip_prefix) self.assertEqual(EXAMPLE['revision_number'], sot.revision_number) self.assertEqual(EXAMPLE['security_group_id'], sot.security_group_id) - self.assertEqual(EXAMPLE['tenant_id'], sot.tenant_id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) diff --git a/openstack/tests/unit/network/v2/test_service_profile.py b/openstack/tests/unit/network/v2/test_service_profile.py index a6be8c079..bd877c45b 100644 --- a/openstack/tests/unit/network/v2/test_service_profile.py +++ b/openstack/tests/unit/network/v2/test_service_profile.py @@ -20,7 +20,7 @@ 'driver': 'neutron_lbaas.drivers.octavia.driver.OctaviaDriver', 'enabled': True, 'metainfo': {'foo': 'bar'}, - 'tenant_id': '5', + 'project_id': '5', } EXAMPLE = { @@ -55,5 +55,5 @@ def test_make_it_with_optional(self): service_profiles.is_enabled) self.assertEqual(EXAMPLE_WITH_OPTIONAL['metainfo'], service_profiles.meta_info) - self.assertEqual(EXAMPLE_WITH_OPTIONAL['tenant_id'], + self.assertEqual(EXAMPLE_WITH_OPTIONAL['project_id'], service_profiles.project_id) diff --git a/openstack/tests/unit/network/v2/test_subnet.py b/openstack/tests/unit/network/v2/test_subnet.py index 6d87eebba..1a9b216f2 100644 --- a/openstack/tests/unit/network/v2/test_subnet.py +++ b/openstack/tests/unit/network/v2/test_subnet.py @@ -35,7 +35,7 @@ 'segment_id': '14', 'service_types': ['15'], 'subnetpool_id': '16', - 'tenant_id': '17', + 'project_id': '17', 'updated_at': '18', 'use_default_subnetpool': True, } @@ -75,6 +75,6 @@ def test_make_it(self): self.assertEqual(EXAMPLE['segment_id'], sot.segment_id) self.assertEqual(EXAMPLE['service_types'], sot.service_types) self.assertEqual(EXAMPLE['subnetpool_id'], sot.subnet_pool_id) - self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) self.assertTrue(sot.use_default_subnet_pool) diff --git a/openstack/tests/unit/network/v2/test_subnet_pool.py b/openstack/tests/unit/network/v2/test_subnet_pool.py index a0859b38e..81dea5c1e 100644 --- a/openstack/tests/unit/network/v2/test_subnet_pool.py +++ b/openstack/tests/unit/network/v2/test_subnet_pool.py @@ -30,7 +30,7 @@ 'prefixes': ['10', '11'], 'revision_number': 12, 'shared': True, - 'tenant_id': '13', + 'project_id': '13', 'updated_at': '14', } @@ -65,5 +65,5 @@ def test_make_it(self): self.assertEqual(EXAMPLE['prefixes'], sot.prefixes) self.assertEqual(EXAMPLE['revision_number'], sot.revision_number) self.assertTrue(sot.is_shared) - self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) diff --git a/openstack/tests/unit/network/v2/test_trunk.py b/openstack/tests/unit/network/v2/test_trunk.py index 1af08fb23..5f509c675 100644 --- a/openstack/tests/unit/network/v2/test_trunk.py +++ b/openstack/tests/unit/network/v2/test_trunk.py @@ -22,7 +22,7 @@ 'id': 'IDENTIFIER', 'description': 'Trunk description', 'name': 'trunk-name', - 'tenant_id': '2', + 'project_id': '2', 'admin_state_up': True, 'port_id': 'fake_port_id', 'status': 'ACTIVE', @@ -53,7 +53,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['description'], sot.description) self.assertEqual(EXAMPLE['name'], sot.name) - self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['admin_state_up'], sot.is_admin_state_up) self.assertEqual(EXAMPLE['port_id'], sot.port_id) self.assertEqual(EXAMPLE['status'], sot.status) diff --git a/openstack/tests/unit/network/v2/test_vpn_service.py b/openstack/tests/unit/network/v2/test_vpn_service.py index a0bef05f1..21024db40 100644 --- a/openstack/tests/unit/network/v2/test_vpn_service.py +++ b/openstack/tests/unit/network/v2/test_vpn_service.py @@ -25,7 +25,7 @@ "router_id": "5", "status": "6", "subnet_id": "7", - "tenant_id": "8", + "project_id": "8", } @@ -53,4 +53,4 @@ def test_make_it(self): self.assertEqual(EXAMPLE['router_id'], sot.router_id) self.assertEqual(EXAMPLE['status'], sot.status) self.assertEqual(EXAMPLE['subnet_id'], sot.subnet_id) - self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) diff --git a/releasenotes/notes/stop-using-tenant-id-42eb35139ba9eeff.yaml b/releasenotes/notes/stop-using-tenant-id-42eb35139ba9eeff.yaml new file mode 100644 index 000000000..5e4cbb010 --- /dev/null +++ b/releasenotes/notes/stop-using-tenant-id-42eb35139ba9eeff.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Stop sending tenant_id attribute to Neutron. From b42fbbde661a32279d007f87becf762bf3ba8d4b Mon Sep 17 00:00:00 2001 From: songwenping Date: Fri, 30 Apr 2021 08:35:22 +0000 Subject: [PATCH 2836/3836] Remove references to 'sys.version_info' We support Python 3.6 as a minimum now, making these checks no-ops. Change-Id: I818684c133a17bde8da109bc6ebf4c92293e9e9b --- openstack/tests/unit/test_utils.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openstack/tests/unit/test_utils.py b/openstack/tests/unit/test_utils.py index 5c972b9cc..142478b4f 100644 --- a/openstack/tests/unit/test_utils.py +++ b/openstack/tests/unit/test_utils.py @@ -17,7 +17,6 @@ import logging import sys from unittest import mock -from unittest import skipIf import fixtures import os_service_types @@ -359,8 +358,6 @@ def test_md5_without_data(self): digest = test_md5.hexdigest() self.assertEqual(digest, self.md5_digest) - @skipIf(sys.version_info.major == 2, - "hashlib.md5 does not raise TypeError here in py2") def test_string_data_raises_type_error(self): if not self.fips_enabled: self.assertRaises(TypeError, hashlib.md5, u'foo') From 6269f8cf9fc472da10d5c7700801fb925f41d215 Mon Sep 17 00:00:00 2001 From: ITD27M01 Date: Sat, 8 May 2021 22:40:04 +0300 Subject: [PATCH 2837/3836] Fix get_server_password method This change fixes the get_server_password method where a typo (or mistake) prevents making the actual proxy request. Also, we need to process the Response object, check for any exceptions and return encrypted password string - what the user expects. Story: 2008889 Task: 42449 Change-Id: Idcd6f329dcffd87f70771378ddbb686fd95c6184 --- openstack/compute/v2/_proxy.py | 2 +- openstack/compute/v2/server.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 16160502a..bb5647cb8 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -708,7 +708,7 @@ def get_server_password(self, server): :returns: encrypted password. """ server = self._get_resource(_server.Server, server) - return server.get_password(self._session) + return server.get_password(self) def reset_server_state(self, server, state): """Reset the state of server diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index e3ae44a2d..fd5a35b0c 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -256,7 +256,12 @@ def change_password(self, session, new_password): def get_password(self, session): """Get the encrypted administrator password.""" url = utils.urljoin(Server.base_path, self.id, 'os-server-password') - return session.get(url) + + response = session.get(url) + exceptions.raise_from_response(response) + + data = response.json() + return data.get('password') def reboot(self, session, reboot_type): """Reboot server where reboot_type might be 'SOFT' or 'HARD'.""" From 3958422a869a1342b313a2d201d10911167c526f Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 10 May 2021 15:59:04 +0200 Subject: [PATCH 2838/3836] Fix MFA authorization This is absolutely impractical to force users to code TOTP token in the clouds.yaml. With this change it is possible to pass passcode dynamically, what will especially be helping OSC with --os-passcode to pass the MFA code. Change-Id: I2b3b17f13744c05bdafb2367fb45cc6b39845807 --- doc/source/user/config/configuration.rst | 25 ++++++++++++++++++++++ openstack/config/loader.py | 6 ++++++ openstack/tests/unit/config/test_config.py | 23 ++++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/doc/source/user/config/configuration.rst b/doc/source/user/config/configuration.rst index 829fd5a03..be420f18f 100644 --- a/doc/source/user/config/configuration.rst +++ b/doc/source/user/config/configuration.rst @@ -262,6 +262,31 @@ establish new connection. auth: true +MFA Support +----------- + +MFA support requires a specially prepared configuration file. In this case a +combination of 2 different authorization plugins is used with their individual +requirements to the specified parameteres. + +.. code-block:: yaml + + clouds: + mfa: + auth_type: "v3multifactor" + auth_methods: + - v3password + - v3totp + auth: + auth_url: https://identity.cloud.com + username: user + user_id: uid + password: XXXXXXXXX + project_name: project + user_domain_name: udn + project_domain_name: pdn + + IPv6 ---- diff --git a/openstack/config/loader.py b/openstack/config/loader.py index c2729654c..c3d04c7a7 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -1042,6 +1042,12 @@ def magic_fixes(self, config): or ('token' in config and config['token'])): config.setdefault('token', config.pop('auth_token', None)) + # Infer passcode if it was given separately + # This is generally absolutely impractical to require setting passcode + # in the clouds.yaml + if 'auth' in config and 'passcode' in config: + config['auth']['passcode'] = config.pop('passcode', None) + # These backwards compat values are only set via argparse. If it's # there, it's because it was passed in explicitly, and should win config = self._fix_backwards_api_timeout(config) diff --git a/openstack/tests/unit/config/test_config.py b/openstack/tests/unit/config/test_config.py index 392d566c5..bc31cc2ef 100644 --- a/openstack/tests/unit/config/test_config.py +++ b/openstack/tests/unit/config/test_config.py @@ -205,6 +205,29 @@ def test_get_one_infer_user_domain(self): self.assertNotIn('domain_id', cc.auth) self.assertNotIn('domain_id', cc) + def test_get_one_infer_passcode(self): + single_conf = base._write_yaml({ + 'clouds': { + 'mfa': { + 'auth_type': 'v3multifactor', + 'auth_methods': ['v3password', 'v3totp'], + 'auth': { + 'auth_url': 'fake_url', + 'username': 'testuser', + 'password': 'testpass', + 'project_name': 'testproject', + 'project_domain_name': 'projectdomain', + 'user_domain_name': 'udn' + }, + 'region_name': 'test-region', + } + } + }) + + c = config.OpenStackConfig(config_files=[single_conf]) + cc = c.get_one(cloud='mfa', passcode='123') + self.assertEqual('123', cc.auth['passcode']) + def test_get_one_with_hyphenated_project_id(self): c = config.OpenStackConfig(config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]) From 698ac14a15bd45ab69b762921365c4b744d0bdda Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 10 May 2021 17:23:10 +0200 Subject: [PATCH 2839/3836] Switch object_store cloud functions to proxy layer Get rid of direct requests in the cloud layer of the object_store. Change-Id: I8fabf2352b524ac26116ceb89485fe50c95eca88 --- openstack/cloud/_object_store.py | 73 +++++++++++------------ openstack/tests/unit/cloud/test_object.py | 37 ++++++++---- 2 files changed, 60 insertions(+), 50 deletions(-) diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index 829b11db5..d151a59b8 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -60,9 +60,7 @@ def list_containers(self, full_listing=True, prefix=None): :raises: OpenStackCloudException on operation error. """ - params = dict(format='json', prefix=prefix) - response = self.object_store.get('/', params=params) - return self._get_and_munchify(None, proxy._json_response(response)) + return list(self.object_store.containers(prefix=prefix)) def search_containers(self, name=None, filters=None): """Search containers. @@ -92,13 +90,10 @@ def get_container(self, name, skip_cache=False): """ if skip_cache or name not in self._container_cache: try: - response = self.object_store.head( - self._get_object_endpoint(name) - ) - exceptions.raise_from_response(response) - self._container_cache[name] = response.headers - except exc.OpenStackCloudHTTPError as e: - if e.response.status_code == 404: + container = self.object_store.get_container_metadata(name) + self._container_cache[name] = container + except exceptions.HttpException as ex: + if ex.response.status_code == 404: return None raise return self._container_cache[name] @@ -114,11 +109,12 @@ def create_container(self, name, public=False): container = self.get_container(name) if container: return container - exceptions.raise_from_response(self.object_store.put( - self._get_object_endpoint(name) - )) + attrs = dict( + name=name + ) if public: - self.set_container_access(name, 'public') + attrs['read_ACL'] = OBJECT_CONTAINER_ACLS['public'] + container = self.object_store.create_container(**attrs) return self.get_container(name, skip_cache=True) def delete_container(self, name): @@ -127,21 +123,17 @@ def delete_container(self, name): :param str name: Name of the container to delete. """ try: - exceptions.raise_from_response(self.object_store.delete( - self._get_object_endpoint(name) - )) + self.object_store.delete_container(name, ignore_missing=False) self._container_cache.pop(name, None) return True - except exc.OpenStackCloudHTTPError as e: - if e.response.status_code == 404: - return False - if e.response.status_code == 409: - raise exc.OpenStackCloudException( - 'Attempt to delete container {container} failed. The' - ' container is not empty. Please delete the objects' - ' inside it before deleting the container'.format( - container=name)) - raise + except exceptions.NotFoundException: + return False + except exceptions.ConflictException: + raise exc.OpenStackCloudException( + 'Attempt to delete container {container} failed. The' + ' container is not empty. Please delete the objects' + ' inside it before deleting the container'.format( + container=name)) def update_container(self, name, headers): """Update the metadata in a container. @@ -158,12 +150,14 @@ def update_container(self, name, headers): :param dict headers: Key/Value headers to set on the container. """ + # TODO(gtema): Decide on whether to deprecate this or change i/f to the + # container metadata names exceptions.raise_from_response( self.object_store.post( self._get_object_endpoint(name), headers=headers) ) - def set_container_access(self, name, access): + def set_container_access(self, name, access, refresh=False): """Set the access control list on a container. :param str name: @@ -172,13 +166,17 @@ def set_container_access(self, name, access): ACL string to set on the container. Can also be ``public`` or ``private`` which will be translated into appropriate ACL strings. + :param refresh: Flag to trigger refresh of the container properties """ if access not in OBJECT_CONTAINER_ACLS: raise exc.OpenStackCloudException( "Invalid container access specified: %s. Must be one of %s" % (access, list(OBJECT_CONTAINER_ACLS.keys()))) - header = {'x-container-read': OBJECT_CONTAINER_ACLS[access]} - self.update_container(name, header) + return self.object_store.set_container_metadata( + name, + read_ACL=OBJECT_CONTAINER_ACLS[access], + refresh=refresh + ) def get_container_access(self, name): """Get the control list from a container. @@ -188,7 +186,7 @@ def get_container_access(self, name): container = self.get_container(name, skip_cache=True) if not container: raise exc.OpenStackCloudException("Container not found: %s" % name) - acl = container.get('x-container-read', '') + acl = container.read_ACL for key, value in OBJECT_CONTAINER_ACLS.items(): # Convert to string for the comparison because swiftclient # returns byte values as bytes sometimes and apparently == @@ -636,9 +634,10 @@ def list_objects(self, container, full_listing=True, prefix=None): :raises: OpenStackCloudException on operation error. """ - params = dict(format='json', prefix=prefix) - data = self._object_store_client.get(container, params=params) - return self._get_and_munchify(None, data) + return list(self.object_store.objects( + container=container, + prefix=prefix + )) def search_objects(self, container, name=None, filters=None): """Search objects. @@ -727,10 +726,8 @@ def get_object_metadata(self, container, name): try: return self._object_store_client.head( self._get_object_endpoint(container, name)).headers - except exc.OpenStackCloudException as e: - if e.response.status_code == 404: - return None - raise + except exceptions.NotFoundException: + return None def get_object_raw(self, container, obj, query_string=None, stream=False): """Get a raw response object for an object. diff --git a/openstack/tests/unit/cloud/test_object.py b/openstack/tests/unit/cloud/test_object.py index 3d74bd445..a4114ed70 100644 --- a/openstack/tests/unit/cloud/test_object.py +++ b/openstack/tests/unit/cloud/test_object.py @@ -22,6 +22,8 @@ from openstack.cloud import exc from openstack import exceptions from openstack.object_store.v1 import _proxy +from openstack.object_store.v1 import container +from openstack.object_store.v1 import obj from openstack.tests.unit import base @@ -38,6 +40,18 @@ def setUp(self): self.object_endpoint = '{endpoint}/{object}'.format( endpoint=self.container_endpoint, object=self.object) + def _compare_containers(self, exp, real): + self.assertDictEqual( + container.Container(**exp).to_dict( + computed=False), + real.to_dict(computed=False)) + + def _compare_objects(self, exp, real): + self.assertDictEqual( + obj.Object(**exp).to_dict( + computed=False), + real.to_dict(computed=False)) + class TestObject(BaseTestObject): @@ -79,14 +93,10 @@ def test_create_container_public(self): 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', 'Content-Length': '0', 'Content-Type': 'text/html; charset=UTF-8', - }), - dict(method='POST', uri=self.container_endpoint, - status_code=201, - validate=dict( - headers={ - 'x-container-read': + 'x-container-read': oc_oc.OBJECT_CONTAINER_ACLS[ - 'public']})), + 'public'], + }), dict(method='HEAD', uri=self.container_endpoint, headers={ 'Content-Length': '0', @@ -243,7 +253,7 @@ def test_get_container_access_not_found(self): self.cloud.get_container_access(self.container) def test_list_containers(self): - endpoint = '{endpoint}/?format=json'.format( + endpoint = '{endpoint}/'.format( endpoint=self.endpoint) containers = [ {u'count': 0, u'bytes': 0, u'name': self.container}] @@ -254,10 +264,11 @@ def test_list_containers(self): ret = self.cloud.list_containers() self.assert_calls() - self.assertEqual(containers, ret) + for a, b in zip(containers, ret): + self._compare_containers(a, b) def test_list_containers_exception(self): - endpoint = '{endpoint}/?format=json'.format( + endpoint = '{endpoint}/'.format( endpoint=self.endpoint) self.register_uris([dict(method='GET', uri=endpoint, complete_qs=True, status_code=416)]) @@ -455,7 +466,8 @@ def test_list_objects(self): ret = self.cloud.list_objects(self.container) self.assert_calls() - self.assertEqual(objects, ret) + for a, b in zip(objects, ret): + self._compare_objects(a, b) def test_list_objects_with_prefix(self): endpoint = '{endpoint}?format=json&prefix=test'.format( @@ -474,7 +486,8 @@ def test_list_objects_with_prefix(self): ret = self.cloud.list_objects(self.container, prefix='test') self.assert_calls() - self.assertEqual(objects, ret) + for a, b in zip(objects, ret): + self._compare_objects(a, b) def test_list_objects_exception(self): endpoint = '{endpoint}?format=json'.format( From 38e9ca0090247bee1b999d39634fa8fd6503b7ae Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 25 Mar 2021 12:39:37 +0000 Subject: [PATCH 2840/3836] placement: Add support for resource classes This one is simple. Resource classes are just strings: { "name": "CUSTOM_FPGA", } Change-Id: Ic47a52b329af5f2ca265ac3c307c33fe8ca42ef2 Signed-off-by: Stephen Finucane --- doc/source/user/proxies/placement.rst | 10 ++- doc/source/user/resources/placement/index.rst | 1 + .../resources/placement/v1/resource_class.rst | 13 +++ openstack/placement/v1/_proxy.py | 84 ++++++++++++++++++- openstack/placement/v1/resource_class.py | 32 +++++++ .../tests/unit/placement/v1/test_proxy.py | 37 ++++++++ .../unit/placement/v1/test_resource_class.py | 42 ++++++++++ ...ement-resource-class-e1c644d978b886bc.yaml | 4 + 8 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 doc/source/user/resources/placement/v1/resource_class.rst create mode 100644 openstack/placement/v1/resource_class.py create mode 100644 openstack/tests/unit/placement/v1/test_resource_class.py create mode 100644 releasenotes/notes/add-placement-resource-class-e1c644d978b886bc.yaml diff --git a/doc/source/user/proxies/placement.rst b/doc/source/user/proxies/placement.rst index 0cc3f8974..b9978e7d1 100644 --- a/doc/source/user/proxies/placement.rst +++ b/doc/source/user/proxies/placement.rst @@ -10,6 +10,14 @@ The placement high-level interface is available through the ``placement`` member of a :class:`~openstack.connection.Connection` object. The ``placement`` member will only be added if the service is detected. +Resource Classes +^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.placement.v1._proxy.Proxy + :noindex: + :members: create_resource_class, update_resource_class, + delete_resource_class, get_resource_class, + resource_classes Resource Providers ^^^^^^^^^^^^^^^^^^ @@ -18,4 +26,4 @@ Resource Providers :noindex: :members: create_resource_provider, update_resource_provider, delete_resource_provider, get_resource_provider, - resource_providers + find_resource_provider, resource_providers diff --git a/doc/source/user/resources/placement/index.rst b/doc/source/user/resources/placement/index.rst index 7f581b9bb..5d8180cd9 100644 --- a/doc/source/user/resources/placement/index.rst +++ b/doc/source/user/resources/placement/index.rst @@ -4,4 +4,5 @@ Placement v1 Resources .. toctree:: :maxdepth: 1 + v1/resource_class v1/resource_provider diff --git a/doc/source/user/resources/placement/v1/resource_class.rst b/doc/source/user/resources/placement/v1/resource_class.rst new file mode 100644 index 000000000..2ef5817b9 --- /dev/null +++ b/doc/source/user/resources/placement/v1/resource_class.rst @@ -0,0 +1,13 @@ +openstack.placement.v1.resource_class +===================================== + +.. automodule:: openstack.placement.v1.resource_class + +The ResourceClass Class +----------------------- + +The ``ResourceClass`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.placement.v1.resource_class.ResourceClass + :members: diff --git a/openstack/placement/v1/_proxy.py b/openstack/placement/v1/_proxy.py index 9f222afef..a8ac8f26a 100644 --- a/openstack/placement/v1/_proxy.py +++ b/openstack/placement/v1/_proxy.py @@ -10,12 +10,94 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.placement.v1 import resource_class as _resource_class from openstack.placement.v1 import resource_provider as _resource_provider from openstack import proxy class Proxy(proxy.Proxy): + # resource classes + + def create_resource_class(self, **attrs): + """Create a new resource class from attributes. + + :param attrs: Keyword arguments which will be used to create a + :class:`~openstack.placement.v1.resource_provider.ResourceClass`, + comprised of the properties on the ResourceClass class. + + :returns: The results of resource class creation + :rtype: :class:`~openstack.placement.v1.resource_class.ResourceClass` + """ + return self._create(_resource_class.ResourceClass, **attrs) + + def delete_resource_class(self, resource_class, ignore_missing=True): + """Delete a resource class + + :param resource_class: The value can be either the ID of a resource + class or an + :class:`~openstack.placement.v1.resource_class.ResourceClass`, + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the resource class does not exist. When set to ``True``, no + exception will be set when attempting to delete a nonexistent + resource class. + + :returns: ``None`` + """ + self._delete( + _resource_class.ResourceClass, + resource_class, + ignore_missing=ignore_missing, + ) + + def update_resource_class(self, resource_class, **attrs): + """Update a resource class + + :param resource_class: The value can be either the ID of a resource + class or an + :class:`~openstack.placement.v1.resource_class.ResourceClass`, + instance. + :attrs kwargs: The attributes to update on the resource class + represented by ``resource_class``. + + :returns: The updated resource class + :rtype: :class:`~openstack.placement.v1.resource_class.ResourceClass` + """ + return self._update( + _resource_class.ResourceClass, resource_class, **attrs, + ) + + def get_resource_class(self, resource_class): + """Get a single resource_class. + + :param resource_class: The value can be either the ID of a resource + class or an + :class:`~openstack.placement.v1.resource_class.ResourceClass`, + instance. + + :returns: An instance of + :class:`~openstack.placement.v1.resource_class.ResourceClass` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource class matching the criteria could be found. + """ + return self._get( + _resource_class.ResourceClass, resource_class, + ) + + def resource_classes(self, **query): + """Retrieve a generator of resource classs. + + :param kwargs query: Optional query parameters to be sent to + restrict the resource classs to be returned. + + :returns: A generator of resource class instances. + """ + return self._list(_resource_class.ResourceClass, **query) + + # resource providers + def create_resource_provider(self, **attrs): """Create a new resource provider from attributes. @@ -50,7 +132,7 @@ def delete_resource_provider(self, resource_provider, ignore_missing=True): ) def update_resource_provider(self, resource_provider, **attrs): - """Update a flavor + """Update a resource provider :param resource_provider: The value can be either the ID of a resource provider or an diff --git a/openstack/placement/v1/resource_class.py b/openstack/placement/v1/resource_class.py new file mode 100644 index 000000000..e45f5e642 --- /dev/null +++ b/openstack/placement/v1/resource_class.py @@ -0,0 +1,32 @@ +# 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 resource + + +class ResourceClass(resource.Resource): + resource_key = None + resources_key = 'resource_classes' + base_path = '/resource_classes' + + # Capabilities + + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + # Added in 1.2 + _max_microversion = '1.2' + + name = resource.Body('name', alternate_id=True) diff --git a/openstack/tests/unit/placement/v1/test_proxy.py b/openstack/tests/unit/placement/v1/test_proxy.py index 210704f09..1b915c0e9 100644 --- a/openstack/tests/unit/placement/v1/test_proxy.py +++ b/openstack/tests/unit/placement/v1/test_proxy.py @@ -11,6 +11,7 @@ # under the License. from openstack.placement.v1 import _proxy +from openstack.placement.v1 import resource_class from openstack.placement.v1 import resource_provider from openstack.tests.unit import test_proxy_base as test_proxy_base @@ -21,6 +22,42 @@ def setUp(self): super().setUp() self.proxy = _proxy.Proxy(self.session) + # resource classes + + def test_resource_class_create(self): + self.verify_create( + self.proxy.create_resource_class, + resource_class.ResourceClass, + ) + + def test_resource_class_delete(self): + self.verify_delete( + self.proxy.delete_resource_class, + resource_class.ResourceClass, + False, + ) + + def test_resource_class_update(self): + self.verify_update( + self.proxy.update_resource_class, + resource_class.ResourceClass, + False, + ) + + def test_resource_class_get(self): + self.verify_get( + self.proxy.get_resource_class, + resource_class.ResourceClass, + ) + + def test_resource_classes(self): + self.verify_list_no_kwargs( + self.proxy.resource_classes, + resource_class.ResourceClass, + ) + + # resource providers + def test_resource_provider_create(self): self.verify_create( self.proxy.create_resource_provider, diff --git a/openstack/tests/unit/placement/v1/test_resource_class.py b/openstack/tests/unit/placement/v1/test_resource_class.py new file mode 100644 index 000000000..9114dec34 --- /dev/null +++ b/openstack/tests/unit/placement/v1/test_resource_class.py @@ -0,0 +1,42 @@ +# 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.placement.v1 import resource_class as rc +from openstack.tests.unit import base + +FAKE = { + 'name': 'CUSTOM_FPGA', +} + + +class TestResourceClass(base.TestCase): + + def test_basic(self): + sot = rc.ResourceClass() + self.assertEqual(None, sot.resource_key) + self.assertEqual('resource_classes', sot.resources_key) + self.assertEqual('/resource_classes', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertFalse(sot.allow_patch) + + self.assertDictEqual( + {'limit': 'limit', 'marker': 'marker'}, + sot._query_mapping._mapping) + + def test_make_it(self): + sot = rc.ResourceClass(**FAKE) + self.assertEqual(FAKE['name'], sot.id) + self.assertEqual(FAKE['name'], sot.name) diff --git a/releasenotes/notes/add-placement-resource-class-e1c644d978b886bc.yaml b/releasenotes/notes/add-placement-resource-class-e1c644d978b886bc.yaml new file mode 100644 index 000000000..c3631abde --- /dev/null +++ b/releasenotes/notes/add-placement-resource-class-e1c644d978b886bc.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Added support for the ``ResourceClass`` Placement resource. From 5edbb15e252c53aa9bcd11245e9b554d2cfa4f92 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 23 Apr 2021 14:34:30 +0100 Subject: [PATCH 2841/3836] tests: Remove dead code There are no references to 'openstack.tests.unit.test_proxy_base2' since change I5d288c25797833a850b361af8f067923b00b8706. Change-Id: Iad4122c83d4394dbf1761eeb6d83ccdee9723f52 Signed-off-by: Stephen Finucane --- openstack/tests/unit/test_proxy_base2.py | 234 ----------------------- 1 file changed, 234 deletions(-) delete mode 100644 openstack/tests/unit/test_proxy_base2.py diff --git a/openstack/tests/unit/test_proxy_base2.py b/openstack/tests/unit/test_proxy_base2.py deleted file mode 100644 index 56b8927af..000000000 --- a/openstack/tests/unit/test_proxy_base2.py +++ /dev/null @@ -1,234 +0,0 @@ -# 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 unittest import mock - -from openstack.tests.unit import base - - -class TestProxyBase(base.TestCase): - # object_store makes calls with container= rather than - # path_args=dict(container= because container needs to wind up - # in the uri components. - kwargs_to_path_args = True - - def setUp(self): - super(TestProxyBase, self).setUp() - self.session = mock.Mock() - - def _add_path_args_for_verify(self, path_args, method_args, - expected_kwargs, value=None): - if path_args is not None: - if value is None: - for key in path_args: - method_args.append(path_args[key]) - expected_kwargs['path_args'] = path_args - - def _verify(self, mock_method, test_method, - method_args=None, method_kwargs=None, - expected_args=None, expected_kwargs=None, - expected_result=None): - with mock.patch(mock_method) as mocked: - mocked.return_value = expected_result - if any([method_args, method_kwargs, - expected_args, expected_kwargs]): - method_args = method_args or () - method_kwargs = method_kwargs or {} - expected_args = expected_args or () - expected_kwargs = expected_kwargs or {} - - self.assertEqual(expected_result, test_method(*method_args, - **method_kwargs)) - mocked.assert_called_with(test_method.__self__, - *expected_args, **expected_kwargs) - else: - self.assertEqual(expected_result, test_method()) - mocked.assert_called_with(test_method.__self__) - - # NOTE(briancurtin): This is a duplicate version of _verify that is - # temporarily here while we shift APIs. The difference is that - # calls from the Proxy classes aren't going to be going directly into - # the Resource layer anymore, so they don't pass in the session which - # was tested in assert_called_with. - # This is being done in lieu of adding logic and complicating - # the _verify method. It will be removed once there is one API to - # be verifying. - def _verify2(self, mock_method, test_method, - method_args=None, method_kwargs=None, method_result=None, - expected_args=None, expected_kwargs=None, - expected_result=None): - with mock.patch(mock_method) as mocked: - mocked.return_value = expected_result - if any([method_args, method_kwargs, - expected_args, expected_kwargs]): - method_args = method_args or () - method_kwargs = method_kwargs or {} - expected_args = expected_args or () - expected_kwargs = expected_kwargs or {} - - if method_result: - self.assertEqual(method_result, test_method(*method_args, - **method_kwargs)) - else: - self.assertEqual(expected_result, test_method(*method_args, - **method_kwargs)) - mocked.assert_called_with(*expected_args, **expected_kwargs) - else: - self.assertEqual(expected_result, test_method()) - mocked.assert_called_with(test_method.__self__) - - def verify_create(self, test_method, resource_type, - mock_method="openstack.proxy.Proxy._create", - expected_result="result", **kwargs): - the_kwargs = {"x": 1, "y": 2, "z": 3} - method_kwargs = kwargs.pop("method_kwargs", the_kwargs) - expected_args = [resource_type] - expected_kwargs = kwargs.pop("expected_kwargs", the_kwargs) - - self._verify2(mock_method, test_method, - expected_result=expected_result, - method_kwargs=method_kwargs, - expected_args=expected_args, - expected_kwargs=expected_kwargs, - **kwargs) - - def verify_delete(self, test_method, resource_type, ignore, - input_path_args=None, expected_path_args=None, - method_kwargs=None, expected_args=None, - expected_kwargs=None, - mock_method="openstack.proxy.Proxy._delete"): - method_args = ["resource_or_id"] - method_kwargs = method_kwargs or {} - method_kwargs["ignore_missing"] = ignore - if isinstance(input_path_args, dict): - for key in input_path_args: - method_kwargs[key] = input_path_args[key] - elif isinstance(input_path_args, list): - method_args = input_path_args - expected_kwargs = expected_kwargs or {} - expected_kwargs["ignore_missing"] = ignore - if expected_path_args: - expected_kwargs.update(expected_path_args) - expected_args = expected_args or [resource_type, "resource_or_id"] - self._verify2(mock_method, test_method, - method_args=method_args, - method_kwargs=method_kwargs, - expected_args=expected_args, - expected_kwargs=expected_kwargs) - - def verify_get(self, test_method, resource_type, value=None, args=None, - mock_method="openstack.proxy.Proxy._get", - ignore_value=False, **kwargs): - the_value = value - if value is None: - the_value = [] if ignore_value else ["value"] - expected_args = kwargs.pop("expected_args", []) - expected_kwargs = kwargs.pop("expected_kwargs", {}) - method_kwargs = kwargs.pop("method_kwargs", kwargs) - if args: - expected_kwargs["args"] = args - if kwargs and self.kwargs_to_path_args: - expected_kwargs["path_args"] = kwargs - if not expected_args: - expected_args = [resource_type] + the_value - self._verify2(mock_method, test_method, - method_args=the_value, - method_kwargs=method_kwargs or {}, - expected_args=expected_args, - expected_kwargs=expected_kwargs) - - def verify_head(self, test_method, resource_type, - mock_method="openstack.proxy.Proxy._head", - value=None, **kwargs): - the_value = [value] if value is not None else [] - if self.kwargs_to_path_args: - expected_kwargs = {"path_args": kwargs} if kwargs else {} - else: - expected_kwargs = kwargs or {} - self._verify2(mock_method, test_method, - method_args=the_value, - method_kwargs=kwargs, - expected_args=[resource_type] + the_value, - expected_kwargs=expected_kwargs) - - def verify_find(self, test_method, resource_type, value=None, - mock_method="openstack.proxy.Proxy._find", - path_args=None, **kwargs): - method_args = value or ["name_or_id"] - expected_kwargs = {} - - self._add_path_args_for_verify(path_args, method_args, expected_kwargs, - value=value) - - # TODO(briancurtin): if sub-tests worked in this mess of - # test dependencies, the following would be a lot easier to work with. - expected_kwargs["ignore_missing"] = False - self._verify2(mock_method, test_method, - method_args=method_args + [False], - expected_args=[resource_type, "name_or_id"], - expected_kwargs=expected_kwargs, - expected_result="result", - **kwargs) - - expected_kwargs["ignore_missing"] = True - self._verify2(mock_method, test_method, - method_args=method_args + [True], - expected_args=[resource_type, "name_or_id"], - expected_kwargs=expected_kwargs, - expected_result="result", - **kwargs) - - def verify_list(self, test_method, resource_type, paginated=False, - mock_method="openstack.proxy.Proxy._list", - **kwargs): - expected_kwargs = kwargs.pop("expected_kwargs", {}) - expected_kwargs.update({"paginated": paginated}) - method_kwargs = kwargs.pop("method_kwargs", {}) - self._verify2(mock_method, test_method, - method_kwargs=method_kwargs, - expected_args=[resource_type], - expected_kwargs=expected_kwargs, - expected_result=["result"], - **kwargs) - - def verify_list_no_kwargs(self, test_method, resource_type, - paginated=False, - mock_method="openstack.proxy.Proxy._list"): - self._verify2(mock_method, test_method, - method_kwargs={}, - expected_args=[resource_type], - expected_kwargs={"paginated": paginated}, - expected_result=["result"]) - - def verify_update(self, test_method, resource_type, value=None, - mock_method="openstack.proxy.Proxy._update", - expected_result="result", path_args=None, **kwargs): - method_args = value or ["resource_or_id"] - method_kwargs = {"x": 1, "y": 2, "z": 3} - expected_args = kwargs.pop("expected_args", ["resource_or_id"]) - expected_kwargs = method_kwargs.copy() - - self._add_path_args_for_verify(path_args, method_args, expected_kwargs, - value=value) - - self._verify2(mock_method, test_method, - expected_result=expected_result, - method_args=method_args, - method_kwargs=method_kwargs, - expected_args=[resource_type] + expected_args, - expected_kwargs=expected_kwargs, - **kwargs) - - def verify_wait_for_status( - self, test_method, - mock_method="openstack.resource.wait_for_status", **kwargs): - self._verify(mock_method, test_method, **kwargs) From 2e726ef772fc0dc60a883f63b11078c2b82a68c3 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 23 Apr 2021 14:48:45 +0100 Subject: [PATCH 2842/3836] tests: Sanity check 'test_proxy_base' A lot of things have been tacked on here over the years. Normalize things and make it a little easier to grok. Change-Id: I09da8af13ac7721a9d9b3a107babf3590a3975e4 Signed-off-by: Stephen Finucane --- openstack/baremetal/v1/_proxy.py | 9 +- openstack/block_storage/v2/_proxy.py | 7 +- openstack/block_storage/v3/_proxy.py | 7 +- openstack/clustering/v1/_proxy.py | 4 +- openstack/compute/v2/_proxy.py | 12 +- openstack/image/v2/_proxy.py | 22 +- openstack/proxy.py | 19 +- .../tests/unit/block_storage/v3/test_proxy.py | 7 +- .../tests/unit/clustering/v1/test_proxy.py | 4 +- openstack/tests/unit/compute/v2/test_proxy.py | 51 +-- .../tests/unit/database/v1/test_proxy.py | 27 +- openstack/tests/unit/dns/v2/test_proxy.py | 6 +- openstack/tests/unit/image/v2/test_proxy.py | 2 +- .../tests/unit/instance_ha/v1/test_proxy.py | 4 +- .../tests/unit/load_balancer/v2/__init__.py | 0 .../unit/load_balancer/{ => v2}/test_proxy.py | 23 +- openstack/tests/unit/message/v2/test_proxy.py | 43 ++- openstack/tests/unit/network/v2/test_proxy.py | 141 +++++--- .../tests/unit/object_store/v1/test_proxy.py | 9 +- .../tests/unit/orchestration/v1/test_proxy.py | 10 +- .../tests/unit/placement/v1/test_proxy.py | 4 +- openstack/tests/unit/test_proxy_base.py | 338 ++++++++++-------- openstack/tests/unit/workflow/v2/__init__.py | 0 .../unit/workflow/{ => v2}/test_proxy.py | 0 24 files changed, 438 insertions(+), 311 deletions(-) create mode 100644 openstack/tests/unit/load_balancer/v2/__init__.py rename openstack/tests/unit/load_balancer/{ => v2}/test_proxy.py (96%) create mode 100644 openstack/tests/unit/workflow/v2/__init__.py rename openstack/tests/unit/workflow/{ => v2}/test_proxy.py (100%) diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 2cbda567b..0b9e21413 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -168,19 +168,20 @@ def delete_chassis(self, chassis, ignore_missing=True): return self._delete(_chassis.Chassis, chassis, ignore_missing=ignore_missing) - def drivers(self, details=False): + def drivers(self, details=False, **query): """Retrieve a generator of drivers. :param bool details: A boolean indicating whether the detailed information for every driver should be returned. + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. :returns: A generator of driver instances. """ - kwargs = {} # NOTE(dtantsur): details are available starting with API microversion # 1.30. Thus we do not send any value if not needed. if details: - kwargs['details'] = True - return self._list(_driver.Driver, **kwargs) + query['details'] = True + return self._list(_driver.Driver, **query) def get_driver(self, driver): """Get a specific driver. diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index fb2eb0ded..212e0220c 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -201,12 +201,15 @@ def extend_volume(self, volume, size): volume = self._get_resource(_volume.Volume, volume) volume.extend(self, size) - def backend_pools(self): + def backend_pools(self, **query): """Returns a generator of cinder Back-end storage pools + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. + :returns A generator of cinder Back-end storage pools objects """ - return self._list(_stats.Pools) + return self._list(_stats.Pools, **query) def backups(self, details=True, **query): """Retrieve a generator of backups diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index a62a1a083..ee601385f 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -416,12 +416,15 @@ def retype_volume(self, volume, new_type, migration_policy="never"): volume = self._get_resource(_volume.Volume, volume) volume.retype(self, new_type, migration_policy) - def backend_pools(self): + def backend_pools(self, **query): """Returns a generator of cinder Back-end storage pools + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. + :returns A generator of cinder Back-end storage pools objects """ - return self._list(_stats.Pools) + return self._list(_stats.Pools, **query) def backups(self, details=True, **query): """Retrieve a generator of backups diff --git a/openstack/clustering/v1/_proxy.py b/openstack/clustering/v1/_proxy.py index 89dc7507c..51f877295 100644 --- a/openstack/clustering/v1/_proxy.py +++ b/openstack/clustering/v1/_proxy.py @@ -420,12 +420,14 @@ def update_cluster_policy(self, cluster, policy, **params): obj = self._find(_cluster.Cluster, cluster, ignore_missing=False) return obj.policy_update(self, policy, **params) - def collect_cluster_attrs(self, cluster, path): + def collect_cluster_attrs(self, cluster, path, **query): """Collect attribute values across a cluster. :param cluster: The value can be either the ID of a cluster or a :class:`~openstack.clustering.v1.cluster.Cluster` instance. :param path: A Json path string specifying the attribute to collect. + :param query: Optional query parameters to be sent to limit the + resources being returned. :returns: A dictionary containing the list of attribute values. """ diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 16160502a..a6a7fb321 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -1144,6 +1144,8 @@ def create_server_interface(self, server, **attrs): return self._create(_server_interface.ServerInterface, server_id=server_id, **attrs) + # TODO(stephenfin): Does this work? There's no 'value' parameter for the + # call to '_delete' def delete_server_interface(self, server_interface, server=None, ignore_missing=True): """Delete a server interface @@ -1169,7 +1171,7 @@ def delete_server_interface(self, server_interface, server=None, server_interface = resource.Resource._get_id(server_interface) self._delete(_server_interface.ServerInterface, - port_id=server_interface, + server_interface, server_id=server_id, ignore_missing=ignore_missing) @@ -1197,18 +1199,20 @@ def get_server_interface(self, server_interface, server=None): return self._get(_server_interface.ServerInterface, server_id=server_id, port_id=server_interface) - def server_interfaces(self, server): + def server_interfaces(self, server, **query): """Return a generator of server interfaces :param server: The server can be either the ID of a server or a - :class:`~openstack.compute.v2.server.Server`. + :class:`~openstack.compute.v2.server.Server`. + :param query: Optional query parameters to be sent to limit the + resources being returned. :returns: A generator of ServerInterface objects :rtype: :class:`~openstack.compute.v2.server_interface.ServerInterface` """ server_id = resource.Resource._get_id(server) return self._list(_server_interface.ServerInterface, - server_id=server_id) + server_id=server_id, **query) def server_ips(self, server, network_label=None): """Return a generator of server IPs diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index a11e08f6f..72da40b52 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -579,16 +579,18 @@ def add_member(self, image, **attrs): image_id = resource.Resource._get_id(image) return self._create(_member.Member, image_id=image_id, **attrs) - def remove_member(self, member, image, ignore_missing=True): + def remove_member(self, member, image=None, ignore_missing=True): """Delete a member :param member: The value can be either the ID of a member or a - :class:`~openstack.image.v2.member.Member` instance. + :class:`~openstack.image.v2.member.Member` instance. + :param image: The value can be either the ID of an image or a + :class:`~openstack.image.v2.image.Image` instance that the member + is part of. This is required if ``member`` is an ID. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the member does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent member. + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the member does not exist. When set to ``True``, no exception will + be set when attempting to delete a nonexistent member. :returns: ``None`` """ @@ -632,12 +634,14 @@ def get_member(self, member, image): return self._get(_member.Member, member_id=member_id, image_id=image_id) - def members(self, image): + def members(self, image, **query): """Return a generator of members :param image: This is the image that the member belongs to, - the value can be the ID of a image or a - :class:`~openstack.image.v2.image.Image` instance. + the value can be the ID of a image or a + :class:`~openstack.image.v2.image.Image` instance. + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. :returns: A generator of member objects :rtype: :class:`~openstack.image.v2.member.Member` diff --git a/openstack/proxy.py b/openstack/proxy.py index 202628b65..235b5c132 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -310,14 +310,13 @@ def _get_resource(self, resource_type, value, **attrs): """Get a resource object to work on :param resource_type: The type of resource to operate on. This should - be a subclass of - :class:`~openstack.resource.Resource` with a - ``from_id`` method. + be a subclass of :class:`~openstack.resource.Resource` with a + ``from_id`` method. :param value: The ID of a resource or an object of ``resource_type`` - class if using an existing instance, or ``munch.Munch``, - or None to create a new instance. - :param path_args: A dict containing arguments for forming the request - URL, if needed. + class if using an existing instance, or ``munch.Munch``, + or None to create a new instance. + :param attrs: A dict containing arguments for forming the request + URL, if needed. """ conn = self._get_connection() if value is None: @@ -374,6 +373,7 @@ def _find(self, resource_type, name_or_id, ignore_missing=True, ignore_missing=ignore_missing, **attrs) + # TODO(stephenfin): Update docstring for attrs since it's a lie @_check_resource(strict=False) def _delete(self, resource_type, value, ignore_missing=True, **attrs): """Delete a resource @@ -512,16 +512,13 @@ def _get(self, resource_type, value=None, requires_id=True, error_message="No {resource_type} found for {value}".format( resource_type=resource_type.__name__, value=value)) - def _list(self, resource_type, value=None, + def _list(self, resource_type, paginated=True, base_path=None, **attrs): """List a resource :param resource_type: The type of resource to list. This should be a :class:`~openstack.resource.Resource` subclass with a ``from_id`` method. - :param value: The resource to list. It can be the ID of a resource, or - a :class:`~openstack.resource.Resource` object. When set - to None, a new bare resource is created. :param bool paginated: When set to ``False``, expect all of the data to be returned in one response. When set to ``True``, the resource supports data being diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index a83e5db4e..c6a949f4d 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -103,9 +103,9 @@ def test_type_extra_specs_delete(self): def test_type_encryption_get(self): self.verify_get(self.proxy.get_type_encryption, type.TypeEncryption, - expected_args=[type.TypeEncryption], + expected_args=[], expected_kwargs={ - 'volume_type_id': 'value', + 'volume_type_id': 'resource_id', 'requires_id': False }) @@ -237,7 +237,8 @@ def test_backup_restore(self): def test_limits_get(self): self.verify_get( - self.proxy.get_limits, limits.Limit, ignore_value=True, + self.proxy.get_limits, limits.Limit, + method_args=[], expected_kwargs={'requires_id': False}) def test_capabilites_get(self): diff --git a/openstack/tests/unit/clustering/v1/test_proxy.py b/openstack/tests/unit/clustering/v1/test_proxy.py index 07743a4c2..360b6447a 100644 --- a/openstack/tests/unit/clustering/v1/test_proxy.py +++ b/openstack/tests/unit/clustering/v1/test_proxy.py @@ -37,7 +37,7 @@ def setUp(self): def test_build_info_get(self): self.verify_get(self.proxy.get_build_info, build_info.BuildInfo, - ignore_value=True, + method_args=[], expected_kwargs={'requires_id': False}) def test_profile_types(self): @@ -137,6 +137,7 @@ def test_collect_cluster_attrs(self): self.verify_list(self.proxy.collect_cluster_attrs, cluster_attr.ClusterAttr, method_args=['FAKE_ID', 'path.to.attr'], + expected_args=[], expected_kwargs={'cluster_id': 'FAKE_ID', 'path': 'path.to.attr'}) @@ -265,6 +266,7 @@ def test_cluster_policies(self): self.verify_list(self.proxy.cluster_policies, cluster_policy.ClusterPolicy, method_args=["FAKE_CLUSTER"], + expected_args=[], expected_kwargs={"cluster_id": "FAKE_CLUSTER"}) def test_get_cluster_policy(self): diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 4a4c05ddf..b7d2c1455 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -56,7 +56,7 @@ def test_flavor_find_query(self): self.verify_find( self.proxy.find_flavor, flavor.Flavor, method_kwargs={"a": "b"}, - expected_kwargs={"a": "b", "ignore_missing": False} + expected_kwargs={"a": "b", "ignore_missing": True} ) def test_flavor_find_fetch_extra(self): @@ -286,7 +286,7 @@ def test_keypair_get_user_id(self): ) def test_keypairs(self): - self.verify_list_no_kwargs(self.proxy.keypairs, keypair.Keypair) + self.verify_list(self.proxy.keypairs, keypair.Keypair) def test_keypairs_user_id(self): self.verify_list( @@ -312,7 +312,7 @@ def test_aggregate_find(self): self.verify_find(self.proxy.find_aggregate, aggregate.Aggregate) def test_aggregates(self): - self.verify_list_no_kwargs(self.proxy.aggregates, aggregate.Aggregate) + self.verify_list(self.proxy.aggregates, aggregate.Aggregate) def test_aggregate_get(self): self.verify_get(self.proxy.get_aggregate, aggregate.Aggregate) @@ -355,8 +355,7 @@ def test_aggregate_precache_images(self): class TestService(TestComputeProxy): def test_services(self): - self.verify_list_no_kwargs( - self.proxy.services, service.Service) + self.verify_list(self.proxy.services, service.Service) @mock.patch('openstack.utils.supports_microversion', autospec=True, return_value=False) @@ -450,11 +449,13 @@ class TestHypervisor(TestComputeProxy): def test_hypervisors_not_detailed(self): self.verify_list(self.proxy.hypervisors, hypervisor.Hypervisor, - method_kwargs={"details": False}) + method_kwargs={"details": False}, + expected_kwargs={}) def test_hypervisors_detailed(self): self.verify_list(self.proxy.hypervisors, hypervisor.HypervisorDetail, - method_kwargs={"details": True}) + method_kwargs={"details": True}, + expected_kwargs={}) @mock.patch('openstack.utils.supports_microversion', autospec=True, return_value=False) @@ -462,8 +463,9 @@ def test_hypervisors_search_before_253_no_qp(self, sm): self.verify_list( self.proxy.hypervisors, hypervisor.Hypervisor, + base_path='/os-hypervisors/detail', method_kwargs={'details': True}, - base_path='/os-hypervisors/detail' + expected_kwargs={}, ) @mock.patch('openstack.utils.supports_microversion', autospec=True, @@ -472,8 +474,9 @@ def test_hypervisors_search_before_253(self, sm): self.verify_list( self.proxy.hypervisors, hypervisor.Hypervisor, + base_path='/os-hypervisors/substring/search', method_kwargs={'hypervisor_hostname_pattern': 'substring'}, - base_path='/os-hypervisors/substring/search' + expected_kwargs={}, ) @mock.patch('openstack.utils.supports_microversion', autospec=True, @@ -492,7 +495,7 @@ def test_find_hypervisor_detail(self): hypervisor.Hypervisor, expected_kwargs={ 'list_base_path': '/os-hypervisors/detail', - 'ignore_missing': False}) + 'ignore_missing': True}) def test_find_hypervisor_no_detail(self): self.verify_find(self.proxy.find_hypervisor, @@ -500,7 +503,7 @@ def test_find_hypervisor_no_detail(self): method_kwargs={'details': False}, expected_kwargs={ 'list_base_path': None, - 'ignore_missing': False}) + 'ignore_missing': True}) def test_get_hypervisor(self): self.verify_get(self.proxy.get_hypervisor, @@ -519,7 +522,7 @@ def test_extension_find(self): self.verify_find(self.proxy.find_extension, extension.Extension) def test_extensions(self): - self.verify_list_no_kwargs(self.proxy.extensions, extension.Extension) + self.verify_list(self.proxy.extensions, extension.Extension) def test_image_delete(self): self.verify_delete(self.proxy.delete_image, image.Image, False) @@ -544,7 +547,7 @@ def test_images_not_detailed(self): expected_kwargs={"query": 1}) def test_limits_get(self): - self.verify_get(self.proxy.get_limits, limits.Limits, value=[]) + self.verify_get(self.proxy.get_limits, limits.Limits, method_args=[]) def test_server_interface_create(self): self.verify_create(self.proxy.create_server_interface, @@ -565,9 +568,9 @@ def test_server_interface_delete(self): self.proxy.delete_server_interface, method_args=[test_interface], method_kwargs={"server": server_id}, - expected_args=[server_interface.ServerInterface], + expected_args=[ + server_interface.ServerInterface, interface_id], expected_kwargs={"server_id": server_id, - "port_id": interface_id, "ignore_missing": True}) # Case2: ServerInterface ID is provided as value @@ -575,9 +578,9 @@ def test_server_interface_delete(self): self.proxy.delete_server_interface, method_args=[interface_id], method_kwargs={"server": server_id}, - expected_args=[server_interface.ServerInterface], + expected_args=[ + server_interface.ServerInterface, interface_id], expected_kwargs={"server_id": server_id, - "port_id": interface_id, "ignore_missing": True}) def test_server_interface_delete_ignore(self): @@ -585,9 +588,8 @@ def test_server_interface_delete_ignore(self): self.verify_delete(self.proxy.delete_server_interface, server_interface.ServerInterface, True, method_kwargs={"server": "test_id"}, - expected_args=[server_interface.ServerInterface], - expected_kwargs={"server_id": "test_id", - "port_id": "resource_or_id"}) + expected_args=[], + expected_kwargs={"server_id": "test_id"}) def test_server_interface_get(self): self.proxy._get_uri_attribute = lambda *args: args[1] @@ -619,18 +621,21 @@ def test_server_interfaces(self): self.verify_list(self.proxy.server_interfaces, server_interface.ServerInterface, method_args=["test_id"], + expected_args=[], expected_kwargs={"server_id": "test_id"}) def test_server_ips_with_network_label(self): self.verify_list(self.proxy.server_ips, server_ip.ServerIP, method_args=["test_id"], method_kwargs={"network_label": "test_label"}, + expected_args=[], expected_kwargs={"server_id": "test_id", "network_label": "test_label"}) def test_server_ips_without_network_label(self): self.verify_list(self.proxy.server_ips, server_ip.ServerIP, method_args=["test_id"], + expected_args=[], expected_kwargs={"server_id": "test_id", "network_label": None}) @@ -854,12 +859,14 @@ def test_get_server_output(self): def test_availability_zones_not_detailed(self): self.verify_list(self.proxy.availability_zones, az.AvailabilityZone, - method_kwargs={"details": False}) + method_kwargs={"details": False}, + expected_kwargs={}) def test_availability_zones_detailed(self): self.verify_list(self.proxy.availability_zones, az.AvailabilityZoneDetail, - method_kwargs={"details": True}) + method_kwargs={"details": True}, + expected_kwargs={}) def test_get_all_server_metadata(self): self._verify2("openstack.compute.v2.server.Server.get_metadata", diff --git a/openstack/tests/unit/database/v1/test_proxy.py b/openstack/tests/unit/database/v1/test_proxy.py index 5fa4776fc..017810f06 100644 --- a/openstack/tests/unit/database/v1/test_proxy.py +++ b/openstack/tests/unit/database/v1/test_proxy.py @@ -24,21 +24,24 @@ def setUp(self): self.proxy = _proxy.Proxy(self.session) def test_database_create_attrs(self): - self.verify_create(self.proxy.create_database, database.Database, + self.verify_create(self.proxy.create_database, + database.Database, method_kwargs={"instance": "id"}, expected_kwargs={"instance_id": "id"}) def test_database_delete(self): self.verify_delete(self.proxy.delete_database, - database.Database, False, - input_path_args={"instance": "test_id"}, - expected_path_args={"instance_id": "test_id"}) + database.Database, + ignore_missing=False, + method_kwargs={"instance": "test_id"}, + expected_kwargs={"instance_id": "test_id"}) def test_database_delete_ignore(self): self.verify_delete(self.proxy.delete_database, - database.Database, True, - input_path_args={"instance": "test_id"}, - expected_path_args={"instance_id": "test_id"}) + database.Database, + ignore_missing=True, + method_kwargs={"instance": "test_id"}, + expected_kwargs={"instance_id": "test_id"}) def test_database_find(self): self._verify2('openstack.proxy.Proxy._find', @@ -51,6 +54,7 @@ def test_database_find(self): def test_databases(self): self.verify_list(self.proxy.databases, database.Database, method_args=["id"], + expected_args=[], expected_kwargs={"instance_id": "id"}) def test_database_get(self): @@ -95,13 +99,13 @@ def test_user_create_attrs(self): def test_user_delete(self): self.verify_delete(self.proxy.delete_user, user.User, False, - input_path_args={"instance": "id"}, - expected_path_args={"instance_id": "id"}) + method_kwargs={"instance": "id"}, + expected_kwargs={"instance_id": "id"}) def test_user_delete_ignore(self): self.verify_delete(self.proxy.delete_user, user.User, True, - input_path_args={"instance": "id"}, - expected_path_args={"instance_id": "id"}) + method_kwargs={"instance": "id"}, + expected_kwargs={"instance_id": "id"}) def test_user_find(self): self._verify2('openstack.proxy.Proxy._find', @@ -114,6 +118,7 @@ def test_user_find(self): def test_users(self): self.verify_list(self.proxy.users, user.User, method_args=["test_instance"], + expected_args=[], expected_kwargs={"instance_id": "test_instance"}) def test_user_get(self): diff --git a/openstack/tests/unit/dns/v2/test_proxy.py b/openstack/tests/unit/dns/v2/test_proxy.py index 125035e92..48369b8f3 100644 --- a/openstack/tests/unit/dns/v2/test_proxy.py +++ b/openstack/tests/unit/dns/v2/test_proxy.py @@ -84,7 +84,7 @@ def test_recordset_get(self): def test_recordsets(self): self.verify_list(self.proxy.recordsets, recordset.Recordset, - base_path='/recordsets') + expected_kwargs={'base_path': '/recordsets'}) def test_recordsets_zone(self): self.verify_list(self.proxy.recordsets, recordset.Recordset, @@ -143,7 +143,7 @@ def test_zone_export_get(self): def test_zone_export_get_text(self): self.verify_get(self.proxy.get_zone_export_text, zone_export.ZoneExport, - value=[{'id': 'zone_export_id_value'}], + method_args=[{'id': 'zone_export_id_value'}], expected_kwargs={ 'base_path': '/zones/tasks/export/%(id)s/export' }) @@ -156,6 +156,7 @@ def test_zone_export_create(self): zone_export.ZoneExport, method_args=[{'id': 'zone_id_value'}], method_kwargs={'name': 'id'}, + expected_args=[], expected_kwargs={'name': 'id', 'zone_id': 'zone_id_value', 'prepend_key': False}) @@ -179,6 +180,7 @@ def test_zone_transfer_request_create(self): zone_transfer.ZoneTransferRequest, method_args=[{'id': 'zone_id_value'}], method_kwargs={'name': 'id'}, + expected_args=[], expected_kwargs={'name': 'id', 'zone_id': 'zone_id_value', 'prepend_key': False}) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index bba3333a5..7983a614d 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -364,7 +364,7 @@ def test_member_find(self): def test_members(self): self.verify_list(self.proxy.members, member.Member, - method_args=('image_1',), + method_kwargs={'image': 'image_1'}, expected_kwargs={'image_id': 'image_1'}) def test_images_schema_get(self): diff --git a/openstack/tests/unit/instance_ha/v1/test_proxy.py b/openstack/tests/unit/instance_ha/v1/test_proxy.py index 5e68100f5..8e1049adc 100644 --- a/openstack/tests/unit/instance_ha/v1/test_proxy.py +++ b/openstack/tests/unit/instance_ha/v1/test_proxy.py @@ -31,12 +31,13 @@ def test_hosts(self): self.verify_list(self.proxy.hosts, host.Host, method_args=[SEGMENT_ID], + expected_args=[], expected_kwargs={"segment_id": SEGMENT_ID}) def test_host_get(self): self.verify_get(self.proxy.get_host, host.Host, - value=[HOST_ID], + method_args=[HOST_ID], method_kwargs={"segment_id": SEGMENT_ID}, expected_kwargs={"segment_id": SEGMENT_ID}) @@ -45,6 +46,7 @@ def test_host_create(self): host.Host, method_args=[SEGMENT_ID], method_kwargs={}, + expected_args=[], expected_kwargs={"segment_id": SEGMENT_ID}) def test_host_update(self): diff --git a/openstack/tests/unit/load_balancer/v2/__init__.py b/openstack/tests/unit/load_balancer/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/unit/load_balancer/test_proxy.py b/openstack/tests/unit/load_balancer/v2/test_proxy.py similarity index 96% rename from openstack/tests/unit/load_balancer/test_proxy.py rename to openstack/tests/unit/load_balancer/v2/test_proxy.py index 54bd479b6..0983845aa 100644 --- a/openstack/tests/unit/load_balancer/test_proxy.py +++ b/openstack/tests/unit/load_balancer/v2/test_proxy.py @@ -56,8 +56,8 @@ def test_load_balancer_get(self): def test_load_balancer_stats_get(self): self.verify_get(self.proxy.get_load_balancer_statistics, lb.LoadBalancerStats, - value=[self.LB_ID], - expected_args=[lb.LoadBalancerStats], + method_args=[self.LB_ID], + expected_args=[], expected_kwargs={'lb_id': self.LB_ID, 'requires_id': False}) @@ -108,7 +108,7 @@ def test_load_balancer_update(self): def test_load_balancer_failover(self): self.verify_update(self.proxy.failover_load_balancer, lb.LoadBalancerFailover, - value=[self.LB_ID], + method_args=[self.LB_ID], expected_args=[], expected_kwargs={'lb_id': self.LB_ID}) @@ -123,8 +123,8 @@ def test_listener_get(self): def test_listener_stats_get(self): self.verify_get(self.proxy.get_listener_statistics, listener.ListenerStats, - value=[self.LISTENER_ID], - expected_args=[listener.ListenerStats], + method_args=[self.LISTENER_ID], + expected_args=[], expected_kwargs={'listener_id': self.LISTENER_ID, 'requires_id': False}) @@ -189,9 +189,11 @@ def test_member_create(self): def test_member_delete(self): self.verify_delete(self.proxy.delete_member, member.Member, - True, + ignore_missing=True, method_kwargs={'pool': self.POOL_ID}, - expected_kwargs={'pool_id': self.POOL_ID}) + expected_kwargs={ + 'pool_id': self.POOL_ID, + 'ignore_missing': True}) def test_member_find(self): self._verify2('openstack.proxy.Proxy._find', @@ -277,7 +279,7 @@ def test_l7_rule_create(self): def test_l7_rule_delete(self): self.verify_delete(self.proxy.delete_l7_rule, l7_rule.L7Rule, - True, + ignore_missing=True, method_kwargs={'l7_policy': self.L7_POLICY_ID}, expected_kwargs={'l7policy_id': self.L7_POLICY_ID}) @@ -324,6 +326,7 @@ def test_provider_flavor_capabilities(self): self.verify_list(self.proxy.provider_flavor_capabilities, provider.ProviderFlavorCapabilities, method_args=[self.AMPHORA], + expected_args=[], expected_kwargs={'provider': self.AMPHORA}) def test_flavor_profiles(self): @@ -380,14 +383,14 @@ def test_amphora_find(self): def test_amphora_configure(self): self.verify_update(self.proxy.configure_amphora, amphora.AmphoraConfig, - value=[self.AMPHORA_ID], + method_args=[self.AMPHORA_ID], expected_args=[], expected_kwargs={'amphora_id': self.AMPHORA_ID}) def test_amphora_failover(self): self.verify_update(self.proxy.failover_amphora, amphora.AmphoraFailover, - value=[self.AMPHORA_ID], + method_args=[self.AMPHORA_ID], expected_args=[], expected_kwargs={'amphora_id': self.AMPHORA_ID}) diff --git a/openstack/tests/unit/message/v2/test_proxy.py b/openstack/tests/unit/message/v2/test_proxy.py index b4bddd9fe..c6866de4d 100644 --- a/openstack/tests/unit/message/v2/test_proxy.py +++ b/openstack/tests/unit/message/v2/test_proxy.py @@ -73,7 +73,7 @@ def test_message_get(self, mock_get_resource): def test_messages(self): self.verify_list(self.proxy.messages, message.Message, - method_args=["test_queue"], + method_kwargs={"queue_name": "test_queue"}, expected_kwargs={"queue_name": "test_queue"}) @mock.patch.object(proxy_base.Proxy, '_get_resource') @@ -150,25 +150,29 @@ def test_subscription_get(self, mock_get_resource): def test_subscriptions(self): self.verify_list(self.proxy.subscriptions, subscription.Subscription, - method_args=["test_queue"], + method_kwargs={"queue_name": "test_queue"}, expected_kwargs={"queue_name": "test_queue"}) @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_subscription_delete(self, mock_get_resource): - mock_get_resource.return_value = "resource_or_id" + mock_get_resource.return_value = "test_subscription" self.verify_delete(self.proxy.delete_subscription, - subscription.Subscription, False, - ["test_queue", "resource_or_id"]) + subscription.Subscription, + ignore_missing=False, + method_args=["test_queue", "resource_or_id"], + expected_args=["test_subscription"]) mock_get_resource.assert_called_once_with( subscription.Subscription, "resource_or_id", queue_name="test_queue") @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_subscription_delete_ignore(self, mock_get_resource): - mock_get_resource.return_value = "resource_or_id" + mock_get_resource.return_value = "test_subscription" self.verify_delete(self.proxy.delete_subscription, - subscription.Subscription, True, - ["test_queue", "resource_or_id"]) + subscription.Subscription, + ignore_missing=True, + method_args=["test_queue", "resource_or_id"], + expected_args=["test_subscription"]) mock_get_resource.assert_called_once_with( subscription.Subscription, "resource_or_id", queue_name="test_queue") @@ -202,12 +206,21 @@ def test_claim_update(self): def test_claim_delete(self): self.verify_delete(self.proxy.delete_claim, - claim.Claim, False, - ["test_queue", "resource_or_id"], - expected_kwargs={"queue_name": "test_queue"}) + claim.Claim, + ignore_missing=False, + method_args=["test_queue", "test_claim"], + expected_args=["test_claim"], + expected_kwargs={ + "queue_name": "test_queue", + "ignore_missing": False}) def test_claim_delete_ignore(self): - self.verify_delete(self.proxy.delete_claim, - claim.Claim, True, - ["test_queue", "resource_or_id"], - expected_kwargs={"queue_name": "test_queue"}) + self.verify_delete( + self.proxy.delete_claim, + claim.Claim, + ignore_missing=True, + method_args=["test_queue", "test_claim"], + expected_args=["test_claim"], + expected_kwargs={ + "queue_name": "test_queue", "ignore_missing": True, + }) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 9772d995b..097da6148 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -72,23 +72,37 @@ def setUp(self): super(TestNetworkProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) - def verify_update(self, test_method, resource_type, value=None, - mock_method="openstack.network.v2._proxy.Proxy._update", - expected_result="result", path_args=None, **kwargs): - super(TestNetworkProxy, self).verify_update( - test_method, resource_type, value=value, mock_method=mock_method, - expected_result=expected_result, path_args=path_args, **kwargs) - - def verify_delete(self, test_method, resource_type, ignore, - input_path_args=None, expected_path_args=None, - method_kwargs=None, expected_args=None, - expected_kwargs=None, - mock_method="openstack.network.v2._proxy.Proxy._delete"): - super(TestNetworkProxy, self).verify_delete( - test_method, resource_type, ignore, - input_path_args=input_path_args, - expected_path_args=expected_path_args, method_kwargs=method_kwargs, - expected_args=expected_args, expected_kwargs=expected_kwargs, + def verify_update( + self, test_method, resource_type, base_path=None, *, + method_args=None, method_kwargs=None, + expected_args=None, expected_kwargs=None, expected_result="result", + mock_method="openstack.network.v2._proxy.Proxy._update", + ): + super().verify_update( + test_method, + resource_type, + base_path=base_path, + method_args=method_args, + method_kwargs=method_kwargs, + expected_args=expected_args, + expected_kwargs=expected_kwargs, + expected_result=expected_result, + mock_method=mock_method) + + def verify_delete( + self, test_method, resource_type, ignore_missing=True, *, + method_args=None, method_kwargs=None, + expected_args=None, expected_kwargs=None, + mock_method="openstack.network.v2._proxy.Proxy._delete", + ): + super().verify_delete( + test_method, + resource_type, + ignore_missing=ignore_missing, + method_args=method_args, + method_kwargs=method_kwargs, + expected_args=expected_args, + expected_kwargs=expected_kwargs, mock_method=mock_method) def test_address_scope_create_attrs(self): @@ -134,8 +148,9 @@ def test_agent_update(self): self.verify_update(self.proxy.update_agent, agent.Agent) def test_availability_zones(self): - self.verify_list_no_kwargs(self.proxy.availability_zones, - availability_zone.AvailabilityZone) + self.verify_list( + self.proxy.availability_zones, + availability_zone.AvailabilityZone) def test_dhcp_agent_hosting_networks(self): self.verify_list( @@ -438,14 +453,20 @@ def test_pool_member_create_attrs(self): expected_kwargs={"pool_id": "test_id"}) def test_pool_member_delete(self): - self.verify_delete(self.proxy.delete_pool_member, - pool_member.PoolMember, False, - {"pool": "test_id"}, {"pool_id": "test_id"}) + self.verify_delete( + self.proxy.delete_pool_member, + pool_member.PoolMember, + ignore_missing=False, + method_kwargs={"pool": "test_id"}, + expected_kwargs={"pool_id": "test_id"}) def test_pool_member_delete_ignore(self): - self.verify_delete(self.proxy.delete_pool_member, - pool_member.PoolMember, True, - {"pool": "test_id"}, {"pool_id": "test_id"}) + self.verify_delete( + self.proxy.delete_pool_member, + pool_member.PoolMember, + ignore_missing=True, + method_kwargs={"pool": "test_id"}, + expected_kwargs={"pool_id": "test_id"}) def test_pool_member_find(self): self._verify2('openstack.proxy.Proxy._find', @@ -463,9 +484,11 @@ def test_pool_member_get(self): expected_kwargs={"pool_id": "POOL"}) def test_pool_members(self): - self.verify_list(self.proxy.pool_members, pool_member.PoolMember, - method_args=["test_id"], - expected_kwargs={"pool_id": "test_id"}) + self.verify_list( + self.proxy.pool_members, pool_member.PoolMember, + method_args=["test_id"], + expected_args=[], + expected_kwargs={"pool_id": "test_id"}) def test_pool_member_update(self): self._verify2("openstack.network.v2._proxy.Proxy._update", @@ -551,14 +574,18 @@ def test_qos_bandwidth_limit_rule_delete(self): self.verify_delete( self.proxy.delete_qos_bandwidth_limit_rule, qos_bandwidth_limit_rule.QoSBandwidthLimitRule, - False, input_path_args=["resource_or_id", QOS_POLICY_ID], + ignore_missing=False, + method_args=["resource_or_id", QOS_POLICY_ID], + expected_args=["resource_or_id"], expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) def test_qos_bandwidth_limit_rule_delete_ignore(self): self.verify_delete( self.proxy.delete_qos_bandwidth_limit_rule, qos_bandwidth_limit_rule.QoSBandwidthLimitRule, - True, input_path_args=["resource_or_id", QOS_POLICY_ID], + ignore_missing=True, + method_args=["resource_or_id", QOS_POLICY_ID], + expected_args=["resource_or_id"], expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) def test_qos_bandwidth_limit_rule_find(self): @@ -609,15 +636,19 @@ def test_qos_dscp_marking_rule_delete(self): self.verify_delete( self.proxy.delete_qos_dscp_marking_rule, qos_dscp_marking_rule.QoSDSCPMarkingRule, - False, input_path_args=["resource_or_id", QOS_POLICY_ID], - expected_path_args={'qos_policy_id': QOS_POLICY_ID},) + ignore_missing=False, + method_args=["resource_or_id", QOS_POLICY_ID], + expected_args=["resource_or_id"], + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) def test_qos_dscp_marking_rule_delete_ignore(self): self.verify_delete( self.proxy.delete_qos_dscp_marking_rule, qos_dscp_marking_rule.QoSDSCPMarkingRule, - True, input_path_args=["resource_or_id", QOS_POLICY_ID], - expected_path_args={'qos_policy_id': QOS_POLICY_ID}, ) + ignore_missing=True, + method_args=["resource_or_id", QOS_POLICY_ID], + expected_args=["resource_or_id"], + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}, ) def test_qos_dscp_marking_rule_find(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) @@ -666,15 +697,19 @@ def test_qos_minimum_bandwidth_rule_delete(self): self.verify_delete( self.proxy.delete_qos_minimum_bandwidth_rule, qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, - False, input_path_args=["resource_or_id", QOS_POLICY_ID], - expected_path_args={'qos_policy_id': QOS_POLICY_ID},) + ignore_missing=False, + method_args=["resource_or_id", QOS_POLICY_ID], + expected_args=["resource_or_id"], + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) def test_qos_minimum_bandwidth_rule_delete_ignore(self): self.verify_delete( self.proxy.delete_qos_minimum_bandwidth_rule, qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, - True, input_path_args=["resource_or_id", QOS_POLICY_ID], - expected_path_args={'qos_policy_id': QOS_POLICY_ID}, ) + ignore_missing=True, + method_args=["resource_or_id", QOS_POLICY_ID], + expected_args=["resource_or_id"], + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) def test_qos_minimum_bandwidth_rule_find(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) @@ -1299,9 +1334,8 @@ def test_auto_allocated_topology_delete_ignore(self): def test_validate_topology(self): self.verify_get(self.proxy.validate_auto_allocated_topology, auto_allocated_topology.ValidateTopology, - value=[mock.sentinel.project_id], - expected_args=[ - auto_allocated_topology.ValidateTopology], + method_args=[mock.sentinel.project_id], + expected_args=[], expected_kwargs={"project": mock.sentinel.project_id, "requires_id": False}) @@ -1331,15 +1365,19 @@ def test_delete_floating_ip_port_forwarding(self): self.verify_delete( self.proxy.delete_floating_ip_port_forwarding, port_forwarding.PortForwarding, - False, input_path_args=[FIP_ID, "resource_or_id"], - expected_path_args={'floatingip_id': FIP_ID},) + ignore_missing=False, + method_args=[FIP_ID, "resource_or_id"], + expected_args=["resource_or_id"], + expected_kwargs={'floatingip_id': FIP_ID}) def test_delete_floating_ip_port_forwarding_ignore(self): self.verify_delete( self.proxy.delete_floating_ip_port_forwarding, port_forwarding.PortForwarding, - True, input_path_args=[FIP_ID, "resource_or_id"], - expected_path_args={'floatingip_id': FIP_ID}, ) + ignore_missing=True, + method_args=[FIP_ID, "resource_or_id"], + expected_args=["resource_or_id"], + expected_kwargs={'floatingip_id': FIP_ID}) def test_find_floating_ip_port_forwarding(self): fip = floating_ip.FloatingIP.new(id=FIP_ID) @@ -1391,16 +1429,20 @@ def test_delete_l3_conntrack_helper(self): self.verify_delete( self.proxy.delete_conntrack_helper, l3_conntrack_helper.ConntrackHelper, - False, input_path_args=['resource_or_id', r], - expected_path_args={'router_id': ROUTER_ID},) + ignore_missing=False, + method_args=['resource_or_id', r], + expected_args=['resource_or_id'], + expected_kwargs={'router_id': ROUTER_ID},) def test_delete_l3_conntrack_helper_ignore(self): r = router.Router.new(id=ROUTER_ID) self.verify_delete( self.proxy.delete_conntrack_helper, l3_conntrack_helper.ConntrackHelper, - True, input_path_args=['resource_or_id', r], - expected_path_args={'router_id': ROUTER_ID}, ) + ignore_missing=True, + method_args=['resource_or_id', r], + expected_args=['resource_or_id'], + expected_kwargs={'router_id': ROUTER_ID},) def test_get_l3_conntrack_helper(self): r = router.Router.new(id=ROUTER_ID) @@ -1416,6 +1458,7 @@ def test_l3_conntrack_helpers(self): self.verify_list(self.proxy.conntrack_helpers, l3_conntrack_helper.ConntrackHelper, method_args=[ROUTER_ID], + expected_args=[], expected_kwargs={'router_id': ROUTER_ID}) def test_update_l3_conntrack_helper(self): diff --git a/openstack/tests/unit/object_store/v1/test_proxy.py b/openstack/tests/unit/object_store/v1/test_proxy.py index 0f7b3bbc1..e24c11542 100644 --- a/openstack/tests/unit/object_store/v1/test_proxy.py +++ b/openstack/tests/unit/object_store/v1/test_proxy.py @@ -39,11 +39,13 @@ def setUp(self): endpoint=self.endpoint, container=self.container) def test_account_metadata_get(self): - self.verify_head(self.proxy.get_account_metadata, account.Account) + self.verify_head( + self.proxy.get_account_metadata, account.Account, + method_args=[]) def test_container_metadata_get(self): self.verify_head(self.proxy.get_container_metadata, - container.Container, value="container") + container.Container, method_args=["container"]) def test_container_delete(self): self.verify_delete(self.proxy.delete_container, @@ -58,6 +60,7 @@ def test_container_create_attrs(self): self.proxy.create_container, container.Container, method_args=['container_name'], + expected_args=[], expected_kwargs={'name': 'container_name', "x": 1, "y": 2, "z": 3}) def test_object_metadata_get(self): @@ -103,7 +106,7 @@ def test_object_get(self): kwargs = dict(container="container") self.verify_get( self.proxy.get_object, obj.Object, - value=["object"], + method_args=["object"], method_kwargs=kwargs, expected_kwargs=kwargs) diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index 945d964ce..32239e024 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -35,9 +35,11 @@ def test_create_stack(self): self.verify_create(self.proxy.create_stack, stack.Stack) def test_create_stack_preview(self): - method_kwargs = {"preview": True, "x": 1, "y": 2, "z": 3} - self.verify_create(self.proxy.create_stack, stack.Stack, - method_kwargs=method_kwargs) + self.verify_create( + self.proxy.create_stack, + stack.Stack, + method_kwargs={"preview": True, "x": 1, "y": 2, "z": 3}, + expected_kwargs={"x": 1, "y": 2, "z": 3}) def test_find_stack(self): self.verify_find(self.proxy.find_stack, stack.Stack, @@ -223,6 +225,7 @@ def test_resources_with_stack_object(self, mock_find): self.verify_list(self.proxy.resources, resource.Resource, method_args=[stk], + expected_args=[], expected_kwargs={'stack_name': stack_name, 'stack_id': stack_id}) @@ -237,6 +240,7 @@ def test_resources_with_stack_name(self, mock_find): self.verify_list(self.proxy.resources, resource.Resource, method_args=[stack_id], + expected_args=[], expected_kwargs={'stack_name': stack_name, 'stack_id': stack_id}) diff --git a/openstack/tests/unit/placement/v1/test_proxy.py b/openstack/tests/unit/placement/v1/test_proxy.py index 1b915c0e9..06a2da1b6 100644 --- a/openstack/tests/unit/placement/v1/test_proxy.py +++ b/openstack/tests/unit/placement/v1/test_proxy.py @@ -51,7 +51,7 @@ def test_resource_class_get(self): ) def test_resource_classes(self): - self.verify_list_no_kwargs( + self.verify_list( self.proxy.resource_classes, resource_class.ResourceClass, ) @@ -85,7 +85,7 @@ def test_resource_provider_get(self): ) def test_resource_providers(self): - self.verify_list_no_kwargs( + self.verify_list( self.proxy.resource_providers, resource_provider.ResourceProvider, ) diff --git a/openstack/tests/unit/test_proxy_base.py b/openstack/tests/unit/test_proxy_base.py index 66652aa1a..9e904ae18 100644 --- a/openstack/tests/unit/test_proxy_base.py +++ b/openstack/tests/unit/test_proxy_base.py @@ -20,14 +20,6 @@ def setUp(self): super(TestProxyBase, self).setUp() self.session = mock.Mock() - def _add_path_args_for_verify(self, path_args, method_args, - expected_kwargs, value=None): - if path_args is not None: - if value is None: - for key in path_args: - method_args.append(path_args[key]) - expected_kwargs['path_args'] = path_args - def _verify(self, mock_method, test_method, method_args=None, method_kwargs=None, expected_args=None, expected_kwargs=None, @@ -78,80 +70,99 @@ def _verify2(self, mock_method, test_method, **method_kwargs)) # Check how the mock was called in detail (called_args, called_kwargs) = mocked.call_args - self.assertEqual(list(called_args), expected_args) - base_path = expected_kwargs.get('base_path', None) + self.assertEqual(expected_args, list(called_args)) # NOTE(gtema): if base_path is not in expected_kwargs or empty # exclude it from the comparison, since some methods might # still invoke method with None value - if not base_path: + base_path = expected_kwargs.get('base_path', None) + if base_path is None: expected_kwargs.pop('base_path', None) called_kwargs.pop('base_path', None) - self.assertDictEqual(called_kwargs, expected_kwargs) + # ditto for paginated + paginated = expected_kwargs.get('paginated', None) + if paginated is None: + expected_kwargs.pop('paginated', None) + called_kwargs.pop('paginated', None) + # and ignore_missing + ignore_missing = expected_kwargs.get('ignore_missing', None) + if ignore_missing is None: + expected_kwargs.pop('ignore_missing', None) + called_kwargs.pop('ignore_missing', None) + self.assertDictEqual(expected_kwargs, called_kwargs) else: self.assertEqual(expected_result, test_method()) mocked.assert_called_with(test_method.__self__) - def verify_create(self, test_method, resource_type, - mock_method="openstack.proxy.Proxy._create", - expected_result="result", **kwargs): - the_kwargs = {"x": 1, "y": 2, "z": 3} - method_kwargs = kwargs.pop("method_kwargs", the_kwargs) - expected_args = kwargs.pop('expected_args', [resource_type]) - # Default the_kwargs should be copied, since we might need to extend it - expected_kwargs = kwargs.pop("expected_kwargs", the_kwargs.copy()) - expected_kwargs["base_path"] = kwargs.pop("base_path", None) - - self._verify2(mock_method, test_method, - expected_result=expected_result, - method_kwargs=method_kwargs, - expected_args=expected_args, - expected_kwargs=expected_kwargs, - **kwargs) - - def verify_delete(self, test_method, resource_type, ignore, - input_path_args=None, expected_path_args=None, - method_kwargs=None, expected_args=None, - expected_kwargs=None, - mock_method="openstack.proxy.Proxy._delete"): - method_args = ["resource_or_id"] + def verify_create( + self, test_method, resource_type, base_path=None, *, + method_args=None, method_kwargs=None, + expected_args=None, expected_kwargs=None, expected_result="result", + mock_method="openstack.proxy.Proxy._create", + ): + if method_args is None: + method_args = [] + if method_kwargs is None: + method_kwargs = {"x": 1, "y": 2, "z": 3} + if expected_args is None: + expected_args = method_args.copy() + if expected_kwargs is None: + expected_kwargs = method_kwargs.copy() + expected_kwargs["base_path"] = base_path + + self._verify2( + mock_method, + test_method, + method_args=method_args, + method_kwargs=method_kwargs, + expected_args=[resource_type] + expected_args, + expected_kwargs=expected_kwargs, + expected_result=expected_result, + ) + + def verify_delete( + self, test_method, resource_type, ignore_missing=True, *, + method_args=None, method_kwargs=None, + expected_args=None, expected_kwargs=None, + mock_method="openstack.proxy.Proxy._delete", + ): + method_args = method_args or ['resource_id'] method_kwargs = method_kwargs or {} - method_kwargs["ignore_missing"] = ignore - if isinstance(input_path_args, dict): - for key in input_path_args: - method_kwargs[key] = input_path_args[key] - elif isinstance(input_path_args, list): - method_args = input_path_args - expected_kwargs = expected_kwargs or {} - expected_kwargs["ignore_missing"] = ignore - if expected_path_args: - expected_kwargs.update(expected_path_args) - expected_args = expected_args or [resource_type, "resource_or_id"] - self._verify2(mock_method, test_method, - method_args=method_args, - method_kwargs=method_kwargs, - expected_args=expected_args, - expected_kwargs=expected_kwargs) - - def verify_get(self, test_method, resource_type, value=None, args=None, - mock_method="openstack.proxy.Proxy._get", - ignore_value=False, **kwargs): - the_value = value - if value is None: - the_value = [] if ignore_value else ["value"] - expected_args = kwargs.pop("expected_args", []) - expected_kwargs = kwargs.pop("expected_kwargs", {}) - method_kwargs = kwargs.pop("method_kwargs", kwargs) - if args: - expected_kwargs["args"] = args - if kwargs: - expected_kwargs["path_args"] = kwargs - if not expected_args: - expected_args = [resource_type] + the_value - self._verify2(mock_method, test_method, - method_args=the_value, - method_kwargs=method_kwargs or {}, - expected_args=expected_args, - expected_kwargs=expected_kwargs) + method_kwargs["ignore_missing"] = ignore_missing + expected_args = expected_args or method_args.copy() + expected_kwargs = expected_kwargs or method_kwargs.copy() + + self._verify2( + mock_method, + test_method, + method_args=method_args, + method_kwargs=method_kwargs, + expected_args=[resource_type] + expected_args, + expected_kwargs=expected_kwargs, + ) + + def verify_get( + self, test_method, resource_type, requires_id=False, base_path=None, *, + method_args=None, method_kwargs=None, + expected_args=None, expected_kwargs=None, + mock_method="openstack.proxy.Proxy._get", + ): + if method_args is None: + method_args = ['resource_id'] + if method_kwargs is None: + method_kwargs = {} + if expected_args is None: + expected_args = method_args.copy() + if expected_kwargs is None: + expected_kwargs = method_kwargs.copy() + + self._verify2( + mock_method, + test_method, + method_args=method_args, + method_kwargs=method_kwargs, + expected_args=[resource_type] + expected_args, + expected_kwargs=expected_kwargs, + ) def verify_get_overrided(self, proxy, resource_type, patch_target): with mock.patch(patch_target, autospec=True) as res: @@ -162,89 +173,106 @@ def verify_get_overrided(self, proxy, resource_type, patch_target): base_path=None, error_message=mock.ANY) - def verify_head(self, test_method, resource_type, - mock_method="openstack.proxy.Proxy._head", - value=None, **kwargs): - the_value = [value] if value is not None else [] - expected_kwargs = {"path_args": kwargs} if kwargs else {} - self._verify2(mock_method, test_method, - method_args=the_value, - method_kwargs=kwargs, - expected_args=[resource_type] + the_value, - expected_kwargs=expected_kwargs) - - def verify_find(self, test_method, resource_type, value=None, - mock_method="openstack.proxy.Proxy._find", - path_args=None, **kwargs): - method_args = value or ["name_or_id"] - expected_kwargs = kwargs.pop('expected_kwargs', {}) - - self._add_path_args_for_verify(path_args, method_args, expected_kwargs, - value=value) - - # TODO(briancurtin): if sub-tests worked in this mess of - # test dependencies, the following would be a lot easier to work with. - expected_kwargs["ignore_missing"] = False - self._verify2(mock_method, test_method, - method_args=method_args + [False], - expected_args=[resource_type, "name_or_id"], - expected_kwargs=expected_kwargs, - expected_result="result", - **kwargs) - - expected_kwargs["ignore_missing"] = True - self._verify2(mock_method, test_method, - method_args=method_args + [True], - expected_args=[resource_type, "name_or_id"], - expected_kwargs=expected_kwargs, - expected_result="result", - **kwargs) - - def verify_list(self, test_method, resource_type, - mock_method="openstack.proxy.Proxy._list", - **kwargs): - expected_kwargs = kwargs.pop("expected_kwargs", {}) - if 'paginated' in kwargs: - expected_kwargs.update({"paginated": kwargs.pop('paginated')}) - method_kwargs = kwargs.pop("method_kwargs", {}) - expected_kwargs["base_path"] = kwargs.pop("base_path", None) - self._verify2(mock_method, test_method, - method_kwargs=method_kwargs, - expected_args=[resource_type], - expected_kwargs=expected_kwargs, - expected_result=["result"], - **kwargs) - - def verify_list_no_kwargs(self, test_method, resource_type, - mock_method="openstack.proxy.Proxy._list"): - self._verify2(mock_method, test_method, - method_kwargs={}, - expected_args=[resource_type], - expected_kwargs={}, - expected_result=["result"]) - - def verify_update(self, test_method, resource_type, value=None, - mock_method="openstack.proxy.Proxy._update", - expected_result="result", path_args=None, **kwargs): - method_args = value or ["resource_or_id"] - method_kwargs = kwargs.pop("method_kwargs", {}) - method_kwargs.update({"x": 1, "y": 2, "z": 3}) - expected_args = kwargs.pop("expected_args", ["resource_or_id"]) - expected_kwargs = kwargs.pop("expected_kwargs", method_kwargs.copy()) - expected_kwargs["base_path"] = kwargs.pop("base_path", None) - - self._add_path_args_for_verify(path_args, method_args, expected_kwargs, - value=value) - - self._verify2(mock_method, test_method, - expected_result=expected_result, - method_args=method_args, - method_kwargs=method_kwargs, - expected_args=[resource_type] + expected_args, - expected_kwargs=expected_kwargs, - **kwargs) + def verify_head( + self, test_method, resource_type, base_path=None, *, + method_args=None, method_kwargs=None, + expected_args=None, expected_kwargs=None, + mock_method="openstack.proxy.Proxy._head", + ): + if method_args is None: + method_args = ['resource_id'] + if method_kwargs is None: + method_kwargs = {} + expected_args = expected_args or method_args.copy() + expected_kwargs = expected_kwargs or method_kwargs.copy() + + self._verify2( + mock_method, + test_method, + method_args=method_args, + method_kwargs=method_kwargs, + expected_args=[resource_type] + expected_args, + expected_kwargs=expected_kwargs, + ) + + def verify_find( + self, test_method, resource_type, name_or_id='resource_name', + ignore_missing=True, *, + method_args=None, method_kwargs=None, + expected_args=None, expected_kwargs=None, + mock_method="openstack.proxy.Proxy._find", + ): + method_args = [name_or_id] + (method_args or []) + method_kwargs = method_kwargs or {} + method_kwargs["ignore_missing"] = ignore_missing + expected_args = expected_args or method_args.copy() + expected_kwargs = expected_kwargs or method_kwargs.copy() + + self._verify2( + mock_method, + test_method, + method_args=method_args, + method_kwargs=method_kwargs, + expected_args=[resource_type] + expected_args, + expected_kwargs=expected_kwargs, + ) + + def verify_list( + self, test_method, resource_type, paginated=None, base_path=None, *, + method_args=None, method_kwargs=None, + expected_args=None, expected_kwargs=None, + mock_method="openstack.proxy.Proxy._list", + ): + if method_args is None: + method_args = [] + if method_kwargs is None: + method_kwargs = {} + if paginated is not None: + method_kwargs["paginated"] = paginated + if expected_args is None: + expected_args = method_args.copy() + if expected_kwargs is None: + expected_kwargs = method_kwargs.copy() + if base_path is not None: + expected_kwargs["base_path"] = base_path + + self._verify2( + mock_method, + test_method, + method_args=method_args, + method_kwargs=method_kwargs, + expected_args=[resource_type] + expected_args, + expected_kwargs=expected_kwargs, + ) + + def verify_update( + self, test_method, resource_type, base_path=None, *, + method_args=None, method_kwargs=None, + expected_args=None, expected_kwargs=None, expected_result="result", + mock_method="openstack.proxy.Proxy._update", + ): + if method_args is None: + method_args = ['resource_id'] + if method_kwargs is None: + method_kwargs = {"x": 1, "y": 2, "z": 3} + method_kwargs["base_path"] = base_path + if expected_args is None: + expected_args = method_args.copy() + if expected_kwargs is None: + expected_kwargs = method_kwargs.copy() + + self._verify2( + mock_method, + test_method, + method_args=method_args, + method_kwargs=method_kwargs, + expected_args=[resource_type] + expected_args, + expected_kwargs=expected_kwargs, + ) def verify_wait_for_status( - self, test_method, - mock_method="openstack.resource.wait_for_status", **kwargs): + self, test_method, + mock_method="openstack.resource.wait_for_status", + **kwargs, + ): self._verify(mock_method, test_method, **kwargs) diff --git a/openstack/tests/unit/workflow/v2/__init__.py b/openstack/tests/unit/workflow/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/unit/workflow/test_proxy.py b/openstack/tests/unit/workflow/v2/test_proxy.py similarity index 100% rename from openstack/tests/unit/workflow/test_proxy.py rename to openstack/tests/unit/workflow/v2/test_proxy.py From 6f98539497dd7b0507382fcca3ec584f3a2174b0 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 26 Apr 2021 12:01:41 +0100 Subject: [PATCH 2843/3836] tests: Remove final use of 'TestProxyBase._verify' Everything now uses '_verify2'...at least until we rename that in the next patch. Change-Id: Ia572acde839370fd2aca34f0413d2e5937a912d0 Signed-off-by: Stephen Finucane --- .../tests/unit/block_storage/v2/test_proxy.py | 9 +- .../tests/unit/block_storage/v3/test_proxy.py | 27 +- .../tests/unit/clustering/v1/test_proxy.py | 92 ++-- openstack/tests/unit/compute/v2/test_proxy.py | 403 ++++++++++-------- openstack/tests/unit/image/v2/test_proxy.py | 63 +-- openstack/tests/unit/message/v2/test_proxy.py | 29 +- openstack/tests/unit/network/v2/test_proxy.py | 107 +++-- openstack/tests/unit/test_proxy_base.py | 27 +- 8 files changed, 437 insertions(+), 320 deletions(-) diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index 8f89a6d27..a77e08aab 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -88,10 +88,11 @@ def test_volume_delete_ignore(self): self.verify_delete(self.proxy.delete_volume, volume.Volume, True) def test_volume_extend(self): - self._verify("openstack.block_storage.v2.volume.Volume.extend", - self.proxy.extend_volume, - method_args=["value", "new-size"], - expected_args=["new-size"]) + self._verify2( + "openstack.block_storage.v2.volume.Volume.extend", + self.proxy.extend_volume, + method_args=["value", "new-size"], + expected_args=[self.proxy, "new-size"]) def test_backend_pools(self): self.verify_list(self.proxy.backend_pools, stats.Pools) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index c6a949f4d..c73f6e22a 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -155,22 +155,25 @@ def test_volume_delete_ignore(self): self.verify_delete(self.proxy.delete_volume, volume.Volume, True) def test_volume_extend(self): - self._verify("openstack.block_storage.v3.volume.Volume.extend", - self.proxy.extend_volume, - method_args=["value", "new-size"], - expected_args=["new-size"]) + self._verify2( + "openstack.block_storage.v3.volume.Volume.extend", + self.proxy.extend_volume, + method_args=["value", "new-size"], + expected_args=[self.proxy, "new-size"]) def test_volume_set_readonly_no_argument(self): - self._verify("openstack.block_storage.v3.volume.Volume.set_readonly", - self.proxy.set_volume_readonly, - method_args=["value"], - expected_args=[True]) + self._verify2( + "openstack.block_storage.v3.volume.Volume.set_readonly", + self.proxy.set_volume_readonly, + method_args=["value"], + expected_args=[self.proxy, True]) def test_volume_set_readonly_false(self): - self._verify("openstack.block_storage.v3.volume.Volume.set_readonly", - self.proxy.set_volume_readonly, - method_args=["value", False], - expected_args=[False]) + self._verify2( + "openstack.block_storage.v3.volume.Volume.set_readonly", + self.proxy.set_volume_readonly, + method_args=["value", False], + expected_args=[self.proxy, False]) def test_backend_pools(self): self.verify_list(self.proxy.backend_pools, stats.Pools) diff --git a/openstack/tests/unit/clustering/v1/test_proxy.py b/openstack/tests/unit/clustering/v1/test_proxy.py index 360b6447a..f06be57c5 100644 --- a/openstack/tests/unit/clustering/v1/test_proxy.py +++ b/openstack/tests/unit/clustering/v1/test_proxy.py @@ -91,9 +91,11 @@ def test_cluster_delete_ignore(self): self.verify_delete(self.proxy.delete_cluster, cluster.Cluster, True) def test_cluster_force_delete(self): - self._verify("openstack.clustering.v1.cluster.Cluster.force_delete", - self.proxy.delete_cluster, - method_args=["value", False, True]) + self._verify2( + "openstack.clustering.v1.cluster.Cluster.force_delete", + self.proxy.delete_cluster, + method_args=["value", False, True], + expected_args=[self.proxy]) def test_cluster_find(self): self.verify_find(self.proxy.find_cluster, cluster.Cluster) @@ -117,21 +119,25 @@ def test_services(self): def test_resize_cluster(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_find.return_value = mock_cluster - self._verify("openstack.clustering.v1.cluster.Cluster.resize", - self.proxy.resize_cluster, - method_args=["FAKE_CLUSTER"], - method_kwargs={'k1': 'v1', 'k2': 'v2'}, - expected_kwargs={'k1': 'v1', 'k2': 'v2'}) + self._verify2( + "openstack.clustering.v1.cluster.Cluster.resize", + self.proxy.resize_cluster, + method_args=["FAKE_CLUSTER"], + method_kwargs={'k1': 'v1', 'k2': 'v2'}, + expected_args=[self.proxy], + expected_kwargs={'k1': 'v1', 'k2': 'v2'}) mock_find.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER", ignore_missing=False) def test_resize_cluster_with_obj(self): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') - self._verify("openstack.clustering.v1.cluster.Cluster.resize", - self.proxy.resize_cluster, - method_args=[mock_cluster], - method_kwargs={'k1': 'v1', 'k2': 'v2'}, - expected_kwargs={'k1': 'v1', 'k2': 'v2'}) + self._verify2( + "openstack.clustering.v1.cluster.Cluster.resize", + self.proxy.resize_cluster, + method_args=[mock_cluster], + method_kwargs={'k1': 'v1', 'k2': 'v2'}, + expected_args=[self.proxy], + expected_kwargs={'k1': 'v1', 'k2': 'v2'}) def test_collect_cluster_attrs(self): self.verify_list(self.proxy.collect_cluster_attrs, @@ -145,18 +151,22 @@ def test_collect_cluster_attrs(self): def test_cluster_check(self, mock_get): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_get.return_value = mock_cluster - self._verify("openstack.clustering.v1.cluster.Cluster.check", - self.proxy.check_cluster, - method_args=["FAKE_CLUSTER"]) + self._verify2( + "openstack.clustering.v1.cluster.Cluster.check", + self.proxy.check_cluster, + method_args=["FAKE_CLUSTER"], + expected_args=[self.proxy]) mock_get.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER") @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_cluster_recover(self, mock_get): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_get.return_value = mock_cluster - self._verify("openstack.clustering.v1.cluster.Cluster.recover", - self.proxy.recover_cluster, - method_args=["FAKE_CLUSTER"]) + self._verify2( + "openstack.clustering.v1.cluster.Cluster.recover", + self.proxy.recover_cluster, + method_args=["FAKE_CLUSTER"], + expected_args=[self.proxy]) mock_get.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER") def test_node_create(self): @@ -169,9 +179,11 @@ def test_node_delete_ignore(self): self.verify_delete(self.proxy.delete_node, node.Node, True) def test_node_force_delete(self): - self._verify("openstack.clustering.v1.node.Node.force_delete", - self.proxy.delete_node, - method_args=["value", False, True]) + self._verify2( + "openstack.clustering.v1.node.Node.force_delete", + self.proxy.delete_node, + method_args=["value", False, True], + expected_args=[self.proxy]) def test_node_find(self): self.verify_find(self.proxy.find_node, node.Node) @@ -200,28 +212,34 @@ def test_node_update(self): def test_node_check(self, mock_get): mock_node = node.Node.new(id='FAKE_NODE') mock_get.return_value = mock_node - self._verify("openstack.clustering.v1.node.Node.check", - self.proxy.check_node, - method_args=["FAKE_NODE"]) + self._verify2( + "openstack.clustering.v1.node.Node.check", + self.proxy.check_node, + method_args=["FAKE_NODE"], + expected_args=[self.proxy]) mock_get.assert_called_once_with(node.Node, "FAKE_NODE") @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_node_recover(self, mock_get): mock_node = node.Node.new(id='FAKE_NODE') mock_get.return_value = mock_node - self._verify("openstack.clustering.v1.node.Node.recover", - self.proxy.recover_node, - method_args=["FAKE_NODE"]) + self._verify2( + "openstack.clustering.v1.node.Node.recover", + self.proxy.recover_node, + method_args=["FAKE_NODE"], + expected_args=[self.proxy]) mock_get.assert_called_once_with(node.Node, "FAKE_NODE") @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_node_adopt(self, mock_get): mock_node = node.Node.new() mock_get.return_value = mock_node - self._verify("openstack.clustering.v1.node.Node.adopt", - self.proxy.adopt_node, - method_kwargs={"preview": False, "foo": "bar"}, - expected_kwargs={"preview": False, "foo": "bar"}) + self._verify2( + "openstack.clustering.v1.node.Node.adopt", + self.proxy.adopt_node, + method_kwargs={"preview": False, "foo": "bar"}, + expected_args=[self.proxy], + expected_kwargs={"preview": False, "foo": "bar"}) mock_get.assert_called_once_with(node.Node, None) @@ -229,10 +247,12 @@ def test_node_adopt(self, mock_get): def test_node_adopt_preview(self, mock_get): mock_node = node.Node.new() mock_get.return_value = mock_node - self._verify("openstack.clustering.v1.node.Node.adopt", - self.proxy.adopt_node, - method_kwargs={"preview": True, "foo": "bar"}, - expected_kwargs={"preview": True, "foo": "bar"}) + self._verify2( + "openstack.clustering.v1.node.Node.adopt", + self.proxy.adopt_node, + method_kwargs={"preview": True, "foo": "bar"}, + expected_args=[self.proxy], + expected_kwargs={"preview": True, "foo": "bar"}) mock_get.assert_called_once_with(node.Node, None) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index b7d2c1455..2dcb4adbe 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -193,58 +193,61 @@ def test_flavors_get_extra(self, fetch_mock, list_mock): ) def test_flavor_get_access(self): - self._verify("openstack.compute.v2.flavor.Flavor.get_access", - self.proxy.get_flavor_access, - method_args=["value"], - expected_args=[]) + self._verify2( + "openstack.compute.v2.flavor.Flavor.get_access", + self.proxy.get_flavor_access, + method_args=["value"], + expected_args=[self.proxy]) def test_flavor_add_tenant_access(self): - self._verify("openstack.compute.v2.flavor.Flavor.add_tenant_access", - self.proxy.flavor_add_tenant_access, - method_args=["value", "fake-tenant"], - expected_args=["fake-tenant"]) + self._verify2( + "openstack.compute.v2.flavor.Flavor.add_tenant_access", + self.proxy.flavor_add_tenant_access, + method_args=["value", "fake-tenant"], + expected_args=[self.proxy, "fake-tenant"]) def test_flavor_remove_tenant_access(self): - self._verify("openstack.compute.v2.flavor.Flavor.remove_tenant_access", - self.proxy.flavor_remove_tenant_access, - method_args=["value", "fake-tenant"], - expected_args=["fake-tenant"]) + self._verify2( + "openstack.compute.v2.flavor.Flavor.remove_tenant_access", + self.proxy.flavor_remove_tenant_access, + method_args=["value", "fake-tenant"], + expected_args=[self.proxy, "fake-tenant"]) def test_flavor_fetch_extra_specs(self): - self._verify("openstack.compute.v2.flavor.Flavor.fetch_extra_specs", - self.proxy.fetch_flavor_extra_specs, - method_args=["value"], - expected_args=[]) + self._verify2( + "openstack.compute.v2.flavor.Flavor.fetch_extra_specs", + self.proxy.fetch_flavor_extra_specs, + method_args=["value"], + expected_args=[self.proxy]) def test_create_flavor_extra_specs(self): - specs = { - 'a': 'b' - } - self._verify("openstack.compute.v2.flavor.Flavor.create_extra_specs", - self.proxy.create_flavor_extra_specs, - method_args=["value", specs], - expected_kwargs={"specs": specs}) + self._verify2( + "openstack.compute.v2.flavor.Flavor.create_extra_specs", + self.proxy.create_flavor_extra_specs, + method_args=["value", {'a': 'b'}], + expected_args=[self.proxy], + expected_kwargs={"specs": {'a': 'b'}}) def test_get_flavor_extra_specs_prop(self): - self._verify( + self._verify2( "openstack.compute.v2.flavor.Flavor.get_extra_specs_property", self.proxy.get_flavor_extra_specs_property, method_args=["value", "prop"], - expected_args=["prop"]) + expected_args=[self.proxy, "prop"]) def test_update_flavor_extra_specs_prop(self): - self._verify( + self._verify2( "openstack.compute.v2.flavor.Flavor.update_extra_specs_property", self.proxy.update_flavor_extra_specs_property, method_args=["value", "prop", "val"], - expected_args=["prop", "val"]) + expected_args=[self.proxy, "prop", "val"]) def test_delete_flavor_extra_specs_prop(self): - self._verify( + self._verify2( "openstack.compute.v2.flavor.Flavor.delete_extra_specs_property", self.proxy.delete_flavor_extra_specs_property, method_args=["value", "prop"], - expected_args=["prop"]) + expected_args=[self.proxy, "prop"]) class TestKeyPair(TestComputeProxy): @@ -321,36 +324,39 @@ def test_aggregate_update(self): self.verify_update(self.proxy.update_aggregate, aggregate.Aggregate) def test_aggregate_add_host(self): - self._verify("openstack.compute.v2.aggregate.Aggregate.add_host", - self.proxy.add_host_to_aggregate, - method_args=["value", "host"], - expected_args=["host"]) + self._verify2( + "openstack.compute.v2.aggregate.Aggregate.add_host", + self.proxy.add_host_to_aggregate, + method_args=["value", "host"], + expected_args=[self.proxy, "host"]) def test_aggregate_remove_host(self): - self._verify("openstack.compute.v2.aggregate.Aggregate.remove_host", - self.proxy.remove_host_from_aggregate, - method_args=["value", "host"], - expected_args=["host"]) + self._verify2( + "openstack.compute.v2.aggregate.Aggregate.remove_host", + self.proxy.remove_host_from_aggregate, + method_args=["value", "host"], + expected_args=[self.proxy, "host"]) def test_aggregate_set_metadata(self): - self._verify("openstack.compute.v2.aggregate.Aggregate.set_metadata", - self.proxy.set_aggregate_metadata, - method_args=["value", {'a': 'b'}], - expected_args=[{'a': 'b'}]) + self._verify2( + "openstack.compute.v2.aggregate.Aggregate.set_metadata", + self.proxy.set_aggregate_metadata, + method_args=["value", {'a': 'b'}], + expected_args=[self.proxy, {'a': 'b'}]) def test_aggregate_precache_image(self): - self._verify( + self._verify2( "openstack.compute.v2.aggregate.Aggregate.precache_images", self.proxy.aggregate_precache_images, method_args=["value", '1'], - expected_args=[[{'id': '1'}]]) + expected_args=[self.proxy, [{'id': '1'}]]) def test_aggregate_precache_images(self): - self._verify( + self._verify2( "openstack.compute.v2.aggregate.Aggregate.precache_images", self.proxy.aggregate_precache_images, method_args=["value", ['1', '2']], - expected_args=[[{'id': '1'}, {'id': '2'}]]) + expected_args=[self.proxy, [{'id': '1'}, {'id': '2'}]]) class TestService(TestComputeProxy): @@ -510,11 +516,11 @@ def test_get_hypervisor(self): hypervisor.Hypervisor) def test_get_hypervisor_uptime(self): - self._verify( + self._verify2( "openstack.compute.v2.hypervisor.Hypervisor.get_uptime", self.proxy.get_hypervisor_uptime, method_args=["value"], - expected_args=[]) + expected_args=[self.proxy]) class TestCompute(TestComputeProxy): @@ -649,9 +655,11 @@ def test_server_delete_ignore(self): self.verify_delete(self.proxy.delete_server, server.Server, True) def test_server_force_delete(self): - self._verify("openstack.compute.v2.server.Server.force_delete", - self.proxy.delete_server, - method_args=["value", False, True]) + self._verify2( + "openstack.compute.v2.server.Server.force_delete", + self.proxy.delete_server, + method_args=["value", False, True], + expected_args=[self.proxy]) def test_server_find(self): self.verify_find(self.proxy.find_server, server.Server) @@ -680,23 +688,28 @@ def test_server_wait_for(self): self.verify_wait_for_status( self.proxy.wait_for_server, method_args=[value], - expected_args=[value, 'ACTIVE', ['ERROR'], 2, 120]) + expected_args=[self.proxy, value, 'ACTIVE', ['ERROR'], 2, 120]) def test_server_resize(self): - self._verify("openstack.compute.v2.server.Server.resize", - self.proxy.resize_server, - method_args=["value", "test-flavor"], - expected_args=["test-flavor"]) + self._verify2( + "openstack.compute.v2.server.Server.resize", + self.proxy.resize_server, + method_args=["value", "test-flavor"], + expected_args=[self.proxy, "test-flavor"]) def test_server_confirm_resize(self): - self._verify("openstack.compute.v2.server.Server.confirm_resize", - self.proxy.confirm_server_resize, - method_args=["value"]) + self._verify2( + "openstack.compute.v2.server.Server.confirm_resize", + self.proxy.confirm_server_resize, + method_args=["value"], + expected_args=[self.proxy]) def test_server_revert_resize(self): - self._verify("openstack.compute.v2.server.Server.revert_resize", - self.proxy.revert_server_resize, - method_args=["value"]) + self._verify2( + "openstack.compute.v2.server.Server.revert_resize", + self.proxy.revert_server_resize, + method_args=["value"], + expected_args=[self.proxy]) def test_server_rebuild(self): id = 'test_image_id' @@ -705,156 +718,191 @@ def test_server_rebuild(self): # Case1: image object is provided # NOTE: Inside of Server.rebuild is where image_obj gets converted # to an ID instead of object. - self._verify('openstack.compute.v2.server.Server.rebuild', - self.proxy.rebuild_server, - method_args=["value", "test_server", "test_pass"], - method_kwargs={"metadata": {"k1": "v1"}, - "image": image_obj}, - expected_args=["test_server", "test_pass"], - expected_kwargs={"metadata": {"k1": "v1"}, - "image": image_obj}) + self._verify2( + 'openstack.compute.v2.server.Server.rebuild', + self.proxy.rebuild_server, + method_args=["value", "test_server", "test_pass"], + method_kwargs={"metadata": {"k1": "v1"}, "image": image_obj}, + expected_args=[self.proxy, "test_server", "test_pass"], + expected_kwargs={"metadata": {"k1": "v1"}, "image": image_obj}) # Case2: image name or id is provided - self._verify('openstack.compute.v2.server.Server.rebuild', - self.proxy.rebuild_server, - method_args=["value", "test_server", "test_pass"], - method_kwargs={"metadata": {"k1": "v1"}, - "image": id}, - expected_args=["test_server", "test_pass"], - expected_kwargs={"metadata": {"k1": "v1"}, - "image": id}) + self._verify2( + 'openstack.compute.v2.server.Server.rebuild', + self.proxy.rebuild_server, + method_args=["value", "test_server", "test_pass"], + method_kwargs={"metadata": {"k1": "v1"}, "image": id}, + expected_args=[self.proxy, "test_server", "test_pass"], + expected_kwargs={"metadata": {"k1": "v1"}, "image": id}) def test_add_fixed_ip_to_server(self): - self._verify("openstack.compute.v2.server.Server.add_fixed_ip", - self.proxy.add_fixed_ip_to_server, - method_args=["value", "network-id"], - expected_args=["network-id"]) + self._verify2( + "openstack.compute.v2.server.Server.add_fixed_ip", + self.proxy.add_fixed_ip_to_server, + method_args=["value", "network-id"], + expected_args=[self.proxy, "network-id"]) def test_fixed_ip_from_server(self): - self._verify("openstack.compute.v2.server.Server.remove_fixed_ip", - self.proxy.remove_fixed_ip_from_server, - method_args=["value", "address"], - expected_args=["address"]) + self._verify2( + "openstack.compute.v2.server.Server.remove_fixed_ip", + self.proxy.remove_fixed_ip_from_server, + method_args=["value", "address"], + expected_args=[self.proxy, "address"]) def test_floating_ip_to_server(self): - self._verify("openstack.compute.v2.server.Server.add_floating_ip", - self.proxy.add_floating_ip_to_server, - method_args=["value", "floating-ip"], - expected_args=["floating-ip"], - expected_kwargs={'fixed_address': None}) + self._verify2( + "openstack.compute.v2.server.Server.add_floating_ip", + self.proxy.add_floating_ip_to_server, + method_args=["value", "floating-ip"], + expected_args=[self.proxy, "floating-ip"], + expected_kwargs={'fixed_address': None}) def test_add_floating_ip_to_server_with_fixed_addr(self): - self._verify("openstack.compute.v2.server.Server.add_floating_ip", - self.proxy.add_floating_ip_to_server, - method_args=["value", "floating-ip", 'fixed-addr'], - expected_args=["floating-ip"], - expected_kwargs={'fixed_address': 'fixed-addr'}) + self._verify2( + "openstack.compute.v2.server.Server.add_floating_ip", + self.proxy.add_floating_ip_to_server, + method_args=["value", "floating-ip", 'fixed-addr'], + expected_args=[self.proxy, "floating-ip"], + expected_kwargs={'fixed_address': 'fixed-addr'}) def test_remove_floating_ip_from_server(self): - self._verify("openstack.compute.v2.server.Server.remove_floating_ip", - self.proxy.remove_floating_ip_from_server, - method_args=["value", "address"], - expected_args=["address"]) + self._verify2( + "openstack.compute.v2.server.Server.remove_floating_ip", + self.proxy.remove_floating_ip_from_server, + method_args=["value", "address"], + expected_args=[self.proxy, "address"]) def test_server_backup(self): - self._verify("openstack.compute.v2.server.Server.backup", - self.proxy.backup_server, - method_args=["value", "name", "daily", 1], - expected_args=["name", "daily", 1]) + self._verify2( + "openstack.compute.v2.server.Server.backup", + self.proxy.backup_server, + method_args=["value", "name", "daily", 1], + expected_args=[self.proxy, "name", "daily", 1]) def test_server_pause(self): - self._verify("openstack.compute.v2.server.Server.pause", - self.proxy.pause_server, - method_args=["value"]) + self._verify2( + "openstack.compute.v2.server.Server.pause", + self.proxy.pause_server, + method_args=["value"], + expected_args=[self.proxy]) def test_server_unpause(self): - self._verify("openstack.compute.v2.server.Server.unpause", - self.proxy.unpause_server, - method_args=["value"]) + self._verify2( + "openstack.compute.v2.server.Server.unpause", + self.proxy.unpause_server, + method_args=["value"], + expected_args=[self.proxy]) def test_server_suspend(self): - self._verify("openstack.compute.v2.server.Server.suspend", - self.proxy.suspend_server, - method_args=["value"]) + self._verify2( + "openstack.compute.v2.server.Server.suspend", + self.proxy.suspend_server, + method_args=["value"], + expected_args=[self.proxy]) def test_server_resume(self): - self._verify("openstack.compute.v2.server.Server.resume", - self.proxy.resume_server, - method_args=["value"]) + self._verify2( + "openstack.compute.v2.server.Server.resume", + self.proxy.resume_server, + method_args=["value"], + expected_args=[self.proxy]) def test_server_lock(self): - self._verify("openstack.compute.v2.server.Server.lock", - self.proxy.lock_server, - method_args=["value"]) + self._verify2( + "openstack.compute.v2.server.Server.lock", + self.proxy.lock_server, + method_args=["value"], + expected_args=[self.proxy]) def test_server_unlock(self): - self._verify("openstack.compute.v2.server.Server.unlock", - self.proxy.unlock_server, - method_args=["value"]) + self._verify2( + "openstack.compute.v2.server.Server.unlock", + self.proxy.unlock_server, + method_args=["value"], + expected_args=[self.proxy]) def test_server_rescue(self): - self._verify("openstack.compute.v2.server.Server.rescue", - self.proxy.rescue_server, - method_args=["value"], - expected_kwargs={"admin_pass": None, "image_ref": None}) + self._verify2( + "openstack.compute.v2.server.Server.rescue", + self.proxy.rescue_server, + method_args=["value"], + expected_args=[self.proxy], + expected_kwargs={"admin_pass": None, "image_ref": None}) def test_server_rescue_with_options(self): - self._verify("openstack.compute.v2.server.Server.rescue", - self.proxy.rescue_server, - method_args=["value", 'PASS', 'IMG'], - expected_kwargs={"admin_pass": 'PASS', - "image_ref": 'IMG'}) + self._verify2( + "openstack.compute.v2.server.Server.rescue", + self.proxy.rescue_server, + method_args=["value", 'PASS', 'IMG'], + expected_args=[self.proxy], + expected_kwargs={"admin_pass": 'PASS', "image_ref": 'IMG'}) def test_server_unrescue(self): - self._verify("openstack.compute.v2.server.Server.unrescue", - self.proxy.unrescue_server, - method_args=["value"]) + self._verify2( + "openstack.compute.v2.server.Server.unrescue", + self.proxy.unrescue_server, + method_args=["value"], + expected_args=[self.proxy]) def test_server_evacuate(self): - self._verify("openstack.compute.v2.server.Server.evacuate", - self.proxy.evacuate_server, - method_args=["value"], - expected_kwargs={"host": None, "admin_pass": None, - "force": None}) + self._verify2( + "openstack.compute.v2.server.Server.evacuate", + self.proxy.evacuate_server, + method_args=["value"], + expected_args=[self.proxy], + expected_kwargs={"host": None, "admin_pass": None, "force": None}) def test_server_evacuate_with_options(self): - self._verify("openstack.compute.v2.server.Server.evacuate", - self.proxy.evacuate_server, - method_args=["value", 'HOST2', 'NEW_PASS', True], - expected_kwargs={"host": "HOST2", - "admin_pass": 'NEW_PASS', - "force": True}) + self._verify2( + "openstack.compute.v2.server.Server.evacuate", + self.proxy.evacuate_server, + method_args=["value", 'HOST2', 'NEW_PASS', True], + expected_args=[self.proxy], + expected_kwargs={ + "host": "HOST2", "admin_pass": 'NEW_PASS', "force": True}) def test_server_start(self): - self._verify("openstack.compute.v2.server.Server.start", - self.proxy.start_server, - method_args=["value"]) + self._verify2( + "openstack.compute.v2.server.Server.start", + self.proxy.start_server, + method_args=["value"], + expected_args=[self.proxy]) def test_server_stop(self): - self._verify("openstack.compute.v2.server.Server.stop", - self.proxy.stop_server, - method_args=["value"]) + self._verify2( + "openstack.compute.v2.server.Server.stop", + self.proxy.stop_server, + method_args=["value"], + expected_args=[self.proxy]) def test_server_shelve(self): - self._verify("openstack.compute.v2.server.Server.shelve", - self.proxy.shelve_server, - method_args=["value"]) + self._verify2( + "openstack.compute.v2.server.Server.shelve", + self.proxy.shelve_server, + method_args=["value"], + expected_args=[self.proxy]) def test_server_unshelve(self): - self._verify("openstack.compute.v2.server.Server.unshelve", - self.proxy.unshelve_server, - method_args=["value"]) + self._verify2( + "openstack.compute.v2.server.Server.unshelve", + self.proxy.unshelve_server, + method_args=["value"], + expected_args=[self.proxy]) def test_get_server_output(self): - self._verify("openstack.compute.v2.server.Server.get_console_output", - self.proxy.get_server_console_output, - method_args=["value"], - expected_kwargs={"length": None}) + self._verify2( + "openstack.compute.v2.server.Server.get_console_output", + self.proxy.get_server_console_output, + method_args=["value"], + expected_args=[self.proxy], + expected_kwargs={"length": None}) - self._verify("openstack.compute.v2.server.Server.get_console_output", - self.proxy.get_server_console_output, - method_args=["value", 1], - expected_kwargs={"length": 1}) + self._verify2( + "openstack.compute.v2.server.Server.get_console_output", + self.proxy.get_server_console_output, + method_args=["value", 1], + expected_args=[self.proxy], + expected_kwargs={"length": 1}) def test_availability_zones_not_detailed(self): self.verify_list(self.proxy.availability_zones, @@ -947,32 +995,33 @@ def test_server_groups(self): self.verify_list(self.proxy.server_groups, server_group.ServerGroup) def test_live_migrate_server(self): - self._verify('openstack.compute.v2.server.Server.live_migrate', - self.proxy.live_migrate_server, - method_args=["value", "host1", False], - expected_args=["host1"], - expected_kwargs={'force': False, 'block_migration': None}) + self._verify2( + 'openstack.compute.v2.server.Server.live_migrate', + self.proxy.live_migrate_server, + method_args=["value", "host1", False], + expected_args=[self.proxy, "host1"], + expected_kwargs={'force': False, 'block_migration': None}) def test_fetch_security_groups(self): - self._verify( + self._verify2( 'openstack.compute.v2.server.Server.fetch_security_groups', self.proxy.fetch_server_security_groups, method_args=["value"], - expected_args=[]) + expected_args=[self.proxy]) def test_add_security_groups(self): - self._verify( + self._verify2( 'openstack.compute.v2.server.Server.add_security_group', self.proxy.add_security_group_to_server, method_args=["value", {'id': 'id', 'name': 'sg'}], - expected_args=['sg']) + expected_args=[self.proxy, 'sg']) def test_remove_security_groups(self): - self._verify( + self._verify2( 'openstack.compute.v2.server.Server.remove_security_group', self.proxy.remove_security_group_from_server, method_args=["value", {'id': 'id', 'name': 'sg'}], - expected_args=['sg']) + expected_args=[self.proxy, 'sg']) def test_create_server_remote_console(self): self.verify_create( @@ -982,11 +1031,11 @@ def test_create_server_remote_console(self): expected_kwargs={"server_id": "test_id", "type": "fake"}) def test_get_console_url(self): - self._verify( + self._verify2( 'openstack.compute.v2.server.Server.get_console_url', self.proxy.get_server_console_url, method_args=["value", "console_type"], - expected_args=["console_type"]) + expected_args=[self.proxy, "console_type"]) @mock.patch('openstack.utils.supports_microversion', autospec=True) @mock.patch('openstack.compute.v2._proxy.Proxy._create', autospec=True) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 7983a614d..715d4b2a1 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -54,10 +54,11 @@ def test_image_import_no_required_attrs(self): def test_image_import(self): original_image = image.Image(**EXAMPLE) - self._verify( + self._verify2( "openstack.image.v2.image.Image.import_image", self.proxy.import_image, method_args=[original_image, "method", "uri"], + expected_args=[self.proxy], expected_kwargs={ "method": "method", "store": None, @@ -218,17 +219,21 @@ def test_image_upload(self): def test_image_download(self): original_image = image.Image(**EXAMPLE) - self._verify('openstack.image.v2.image.Image.download', - self.proxy.download_image, - method_args=[original_image], - method_kwargs={ - 'output': 'some_output', - 'chunk_size': 1, - 'stream': True - }, - expected_kwargs={'output': 'some_output', - 'chunk_size': 1, - 'stream': True}) + self._verify2( + 'openstack.image.v2.image.Image.download', + self.proxy.download_image, + method_args=[original_image], + method_kwargs={ + 'output': 'some_output', + 'chunk_size': 1, + 'stream': True + }, + expected_args=[self.proxy], + expected_kwargs={ + 'output': 'some_output', + 'chunk_size': 1, + 'stream': True, + }) @mock.patch("openstack.image.v2.image.Image.fetch") def test_image_stage(self, mock_fetch): @@ -289,26 +294,32 @@ def test_images(self): self.verify_list(self.proxy.images, image.Image) def test_add_tag(self): - self._verify("openstack.image.v2.image.Image.add_tag", - self.proxy.add_tag, - method_args=["image", "tag"], - expected_args=["tag"]) + self._verify2( + "openstack.image.v2.image.Image.add_tag", + self.proxy.add_tag, + method_args=["image", "tag"], + expected_args=[self.proxy, "tag"]) def test_remove_tag(self): - self._verify("openstack.image.v2.image.Image.remove_tag", - self.proxy.remove_tag, - method_args=["image", "tag"], - expected_args=["tag"]) + self._verify2( + "openstack.image.v2.image.Image.remove_tag", + self.proxy.remove_tag, + method_args=["image", "tag"], + expected_args=[self.proxy, "tag"]) def test_deactivate_image(self): - self._verify("openstack.image.v2.image.Image.deactivate", - self.proxy.deactivate_image, - method_args=["image"]) + self._verify2( + "openstack.image.v2.image.Image.deactivate", + self.proxy.deactivate_image, + method_args=["image"], + expected_args=[self.proxy]) def test_reactivate_image(self): - self._verify("openstack.image.v2.image.Image.reactivate", - self.proxy.reactivate_image, - method_args=["image"]) + self._verify2( + "openstack.image.v2.image.Image.reactivate", + self.proxy.reactivate_image, + method_args=["image"], + expected_args=[self.proxy]) def test_member_create(self): self.verify_create(self.proxy.add_member, member.Member, diff --git a/openstack/tests/unit/message/v2/test_proxy.py b/openstack/tests/unit/message/v2/test_proxy.py index c6866de4d..540b91359 100644 --- a/openstack/tests/unit/message/v2/test_proxy.py +++ b/openstack/tests/unit/message/v2/test_proxy.py @@ -50,10 +50,11 @@ def test_queue_delete_ignore(self): def test_message_post(self, mock_get_resource): message_obj = message.Message(queue_name="test_queue") mock_get_resource.return_value = message_obj - self._verify("openstack.message.v2.message.Message.post", - self.proxy.post_message, - method_args=["test_queue", ["msg1", "msg2"]], - expected_args=[["msg1", "msg2"]]) + self._verify2( + "openstack.message.v2.message.Message.post", + self.proxy.post_message, + method_args=["test_queue", ["msg1", "msg2"]], + expected_args=[self.proxy, ["msg1", "msg2"]]) mock_get_resource.assert_called_once_with(message.Message, None, queue_name="test_queue") @@ -128,10 +129,12 @@ def test_message_delete_ignore(self, mock_get_resource): queue_name="test_queue") def test_subscription_create(self): - self._verify("openstack.message.v2.subscription.Subscription.create", - self.proxy.create_subscription, - method_args=["test_queue"], - expected_kwargs={"base_path": None}) + self._verify2( + "openstack.message.v2.subscription.Subscription.create", + self.proxy.create_subscription, + method_args=["test_queue"], + expected_args=[self.proxy], + expected_kwargs={"base_path": None}) @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_subscription_get(self, mock_get_resource): @@ -178,10 +181,12 @@ def test_subscription_delete_ignore(self, mock_get_resource): queue_name="test_queue") def test_claim_create(self): - self._verify("openstack.message.v2.claim.Claim.create", - self.proxy.create_claim, - method_args=["test_queue"], - expected_kwargs={"base_path": None}) + self._verify2( + "openstack.message.v2.claim.Claim.create", + self.proxy.create_claim, + method_args=["test_queue"], + expected_args=[self.proxy], + expected_kwargs={"base_path": None}) def test_claim_get(self): self._verify2("openstack.proxy.Proxy._get", diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 097da6148..00eddda0e 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -891,11 +891,13 @@ def test_add_interface_to_router_with_port(self, mock_add_interface, x_router = router.Router.new(id="ROUTER_ID") mock_get.return_value = x_router - self._verify("openstack.network.v2.router.Router.add_interface", - self.proxy.add_interface_to_router, - method_args=["FAKE_ROUTER"], - method_kwargs={"port_id": "PORT"}, - expected_kwargs={"port_id": "PORT"}) + self._verify2( + "openstack.network.v2.router.Router.add_interface", + self.proxy.add_interface_to_router, + method_args=["FAKE_ROUTER"], + method_kwargs={"port_id": "PORT"}, + expected_args=[self.proxy], + expected_kwargs={"port_id": "PORT"}) mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") @mock.patch.object(proxy_base.Proxy, '_get_resource') @@ -905,11 +907,13 @@ def test_add_interface_to_router_with_subnet(self, mock_add_interface, x_router = router.Router.new(id="ROUTER_ID") mock_get.return_value = x_router - self._verify("openstack.network.v2.router.Router.add_interface", - self.proxy.add_interface_to_router, - method_args=["FAKE_ROUTER"], - method_kwargs={"subnet_id": "SUBNET"}, - expected_kwargs={"subnet_id": "SUBNET"}) + self._verify2( + "openstack.network.v2.router.Router.add_interface", + self.proxy.add_interface_to_router, + method_args=["FAKE_ROUTER"], + method_kwargs={"subnet_id": "SUBNET"}, + expected_args=[self.proxy], + expected_kwargs={"subnet_id": "SUBNET"}) mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") @mock.patch.object(proxy_base.Proxy, '_get_resource') @@ -919,11 +923,13 @@ def test_remove_interface_from_router_with_port(self, mock_remove, x_router = router.Router.new(id="ROUTER_ID") mock_get.return_value = x_router - self._verify("openstack.network.v2.router.Router.remove_interface", - self.proxy.remove_interface_from_router, - method_args=["FAKE_ROUTER"], - method_kwargs={"port_id": "PORT"}, - expected_kwargs={"port_id": "PORT"}) + self._verify2( + "openstack.network.v2.router.Router.remove_interface", + self.proxy.remove_interface_from_router, + method_args=["FAKE_ROUTER"], + method_kwargs={"port_id": "PORT"}, + expected_args=[self.proxy], + expected_kwargs={"port_id": "PORT"}) mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") @mock.patch.object(proxy_base.Proxy, '_get_resource') @@ -933,11 +939,13 @@ def test_remove_interface_from_router_with_subnet(self, mock_remove, x_router = router.Router.new(id="ROUTER_ID") mock_get.return_value = x_router - self._verify("openstack.network.v2.router.Router.remove_interface", - self.proxy.remove_interface_from_router, - method_args=["FAKE_ROUTER"], - method_kwargs={"subnet_id": "SUBNET"}, - expected_kwargs={"subnet_id": "SUBNET"}) + self._verify2( + "openstack.network.v2.router.Router.remove_interface", + self.proxy.remove_interface_from_router, + method_args=["FAKE_ROUTER"], + method_kwargs={"subnet_id": "SUBNET"}, + expected_args=[self.proxy], + expected_kwargs={"subnet_id": "SUBNET"}) mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") @mock.patch.object(proxy_base.Proxy, '_get_resource') @@ -947,11 +955,13 @@ def test_add_extra_routes_to_router( x_router = router.Router.new(id="ROUTER_ID") mock_get.return_value = x_router - self._verify("openstack.network.v2.router.Router.add_extra_routes", - self.proxy.add_extra_routes_to_router, - method_args=["FAKE_ROUTER"], - method_kwargs={"body": {"router": {"routes": []}}}, - expected_kwargs={"body": {"router": {"routes": []}}}) + self._verify2( + "openstack.network.v2.router.Router.add_extra_routes", + self.proxy.add_extra_routes_to_router, + method_args=["FAKE_ROUTER"], + method_kwargs={"body": {"router": {"routes": []}}}, + expected_args=[self.proxy], + expected_kwargs={"body": {"router": {"routes": []}}}) mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") @mock.patch.object(proxy_base.Proxy, '_get_resource') @@ -961,11 +971,13 @@ def test_remove_extra_routes_from_router( x_router = router.Router.new(id="ROUTER_ID") mock_get.return_value = x_router - self._verify("openstack.network.v2.router.Router.remove_extra_routes", - self.proxy.remove_extra_routes_from_router, - method_args=["FAKE_ROUTER"], - method_kwargs={"body": {"router": {"routes": []}}}, - expected_kwargs={"body": {"router": {"routes": []}}}) + self._verify2( + "openstack.network.v2.router.Router.remove_extra_routes", + self.proxy.remove_extra_routes_from_router, + method_args=["FAKE_ROUTER"], + method_kwargs={"body": {"router": {"routes": []}}}, + expected_args=[self.proxy], + expected_kwargs={"body": {"router": {"routes": []}}}) mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") @mock.patch.object(proxy_base.Proxy, '_get_resource') @@ -974,11 +986,13 @@ def test_add_gateway_to_router(self, mock_add, mock_get): x_router = router.Router.new(id="ROUTER_ID") mock_get.return_value = x_router - self._verify("openstack.network.v2.router.Router.add_gateway", - self.proxy.add_gateway_to_router, - method_args=["FAKE_ROUTER"], - method_kwargs={"foo": "bar"}, - expected_kwargs={"foo": "bar"}) + self._verify2( + "openstack.network.v2.router.Router.add_gateway", + self.proxy.add_gateway_to_router, + method_args=["FAKE_ROUTER"], + method_kwargs={"foo": "bar"}, + expected_args=[self.proxy], + expected_kwargs={"foo": "bar"}) mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") @mock.patch.object(proxy_base.Proxy, '_get_resource') @@ -987,11 +1001,13 @@ def test_remove_gateway_from_router(self, mock_remove, mock_get): x_router = router.Router.new(id="ROUTER_ID") mock_get.return_value = x_router - self._verify("openstack.network.v2.router.Router.remove_gateway", - self.proxy.remove_gateway_from_router, - method_args=["FAKE_ROUTER"], - method_kwargs={"foo": "bar"}, - expected_kwargs={"foo": "bar"}) + self._verify2( + "openstack.network.v2.router.Router.remove_gateway", + self.proxy.remove_gateway_from_router, + method_args=["FAKE_ROUTER"], + method_kwargs={"foo": "bar"}, + expected_args=[self.proxy], + expected_kwargs={"foo": "bar"}) mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") def test_router_hosting_l3_agents_list(self): @@ -1341,11 +1357,12 @@ def test_validate_topology(self): def test_set_tags(self): x_network = network.Network.new(id='NETWORK_ID') - self._verify('openstack.network.v2.network.Network.set_tags', - self.proxy.set_tags, - method_args=[x_network, ['TAG1', 'TAG2']], - expected_args=[['TAG1', 'TAG2']], - expected_result=mock.sentinel.result_set_tags) + self._verify2( + 'openstack.network.v2.network.Network.set_tags', + self.proxy.set_tags, + method_args=[x_network, ['TAG1', 'TAG2']], + expected_args=[self.proxy, ['TAG1', 'TAG2']], + expected_result=mock.sentinel.result_set_tags) @mock.patch('openstack.network.v2.network.Network.set_tags') def test_set_tags_resource_without_tag_suport(self, mock_set_tags): diff --git a/openstack/tests/unit/test_proxy_base.py b/openstack/tests/unit/test_proxy_base.py index 9e904ae18..b36fa4770 100644 --- a/openstack/tests/unit/test_proxy_base.py +++ b/openstack/tests/unit/test_proxy_base.py @@ -55,22 +55,32 @@ def _verify2(self, mock_method, test_method, expected_result=None): with mock.patch(mock_method) as mocked: mocked.return_value = expected_result - if any([method_args, method_kwargs, - expected_args, expected_kwargs]): + if any([ + method_args, + method_kwargs, + expected_args, + expected_kwargs, + ]): method_args = method_args or () method_kwargs = method_kwargs or {} expected_args = expected_args or () expected_kwargs = expected_kwargs or {} if method_result: - self.assertEqual(method_result, test_method(*method_args, - **method_kwargs)) + self.assertEqual( + method_result, + test_method(*method_args, **method_kwargs), + ) else: - self.assertEqual(expected_result, test_method(*method_args, - **method_kwargs)) + self.assertEqual( + expected_result, + test_method(*method_args, **method_kwargs), + ) + # Check how the mock was called in detail - (called_args, called_kwargs) = mocked.call_args + called_args, called_kwargs = mocked.call_args self.assertEqual(expected_args, list(called_args)) + # NOTE(gtema): if base_path is not in expected_kwargs or empty # exclude it from the comparison, since some methods might # still invoke method with None value @@ -88,6 +98,7 @@ def _verify2(self, mock_method, test_method, if ignore_missing is None: expected_kwargs.pop('ignore_missing', None) called_kwargs.pop('ignore_missing', None) + self.assertDictEqual(expected_kwargs, called_kwargs) else: self.assertEqual(expected_result, test_method()) @@ -275,4 +286,4 @@ def verify_wait_for_status( mock_method="openstack.resource.wait_for_status", **kwargs, ): - self._verify(mock_method, test_method, **kwargs) + self._verify2(mock_method, test_method, **kwargs) From 0c9742e202a31c45106865e210b812ce7f01c933 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 10 May 2021 12:07:10 +0200 Subject: [PATCH 2844/3836] Extend functional test of image Add few invocations of the tagging funcitonality on the image. Since image resource already include TagMixin there is no need (and actually harm) to include tags attribute explicitly - drop it. Change-Id: Ie6564e03062763c83eeefb1e5b26ea4d873dbc99 --- openstack/image/v2/image.py | 2 -- .../tests/functional/image/v2/test_image.py | 21 +++++++++++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index 4e6881a3f..bbf700a31 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -78,8 +78,6 @@ class Image(resource.Resource, resource.TagMixin, _download.DownloadMixin): #: The algorithm used to compute a secure hash of the image data #: for this image hash_algo = resource.Body('os_hash_algo') - #: List of tags for this image. - tags = resource.Body('tags') #: The hexdigest of the secure hash of the image data computed using #: the algorithm whose name is the value of the os_hash_algo property. hash_value = resource.Body('os_hash_value') diff --git a/openstack/tests/functional/image/v2/test_image.py b/openstack/tests/functional/image/v2/test_image.py index 3635b6a72..530f161a5 100644 --- a/openstack/tests/functional/image/v2/test_image.py +++ b/openstack/tests/functional/image/v2/test_image.py @@ -28,7 +28,7 @@ def setUp(self): self.conn = connection.from_config( cloud_name=base.TEST_CLOUD_NAME, options=opts) - self.img = self.conn.image.upload_image( + self.img = self.conn.image.create_image( name=TEST_IMAGE_NAME, disk_format='raw', container_format='bare', @@ -37,7 +37,9 @@ def setUp(self): # we need to just replace the image upload code with the stuff # from shade. Figuring out mapping the crap-tastic arbitrary # extra key-value pairs into Resource is going to be fun. - properties='{"description": "This is not an image"}', + properties=dict( + description="This is not an image" + ), data=open('CONTRIBUTING.rst', 'r') ) self.addCleanup(self.conn.image.delete_image, self.img) @@ -65,3 +67,18 @@ def test_get_member_schema(self): def test_list_tasks(self): tasks = self.conn.image.tasks() self.assertIsNotNone(tasks) + + def test_tags(self): + img = self.conn.image.get_image(self.img) + self.conn.image.add_tag(img, 't1') + self.conn.image.add_tag(img, 't2') + # Ensure list with array of tags return us our image + list_img = list(self.conn.image.images(tag=['t1', 't2']))[0] + self.assertEqual(img.id, list_img.id) + self.assertIn('t1', list_img.tags) + self.assertIn('t2', list_img.tags) + self.conn.image.remove_tag(img, 't1') + # Refetch img to verify tags + img = self.conn.image.get_image(self.img) + self.assertIn('t2', img.tags) + self.assertNotIn('t1', img.tags) From 8ed2d1001920a9d29f3cbef2a02b3ceb38ba3da0 Mon Sep 17 00:00:00 2001 From: "wu.shiming" Date: Wed, 12 May 2021 09:54:38 +0800 Subject: [PATCH 2845/3836] setup.cfg: Replace dashes with underscores Setuptools v54.1.0 introduces a warning that the use of dash-separated options in 'setup.cfg' will not be supported in a future version [1]. Get ahead of the issue by replacing the dashes with underscores. Without this, we see 'UserWarning' messages like the following on new enough versions of setuptools: UserWarning: Usage of dash-separated 'description-file' will not be supported in future versions. Please use the underscore name 'description_file' instead [1] https://github.com/pypa/setuptools/commit/a2e9ae4cb Change-Id: I6974592dc7cd44a749986ec0373afddc48d43b78 --- setup.cfg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index a635769c4..0c8ae0e52 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,11 +1,11 @@ [metadata] name = openstacksdk summary = An SDK for building applications to work with OpenStack -description-file = +description_file = README.rst author = OpenStack -author-email = openstack-discuss@lists.openstack.org -home-page = https://docs.openstack.org/openstacksdk/ +author_email = openstack-discuss@lists.openstack.org +home_page = https://docs.openstack.org/openstacksdk/ classifier = Environment :: OpenStack Intended Audience :: Information Technology @@ -17,7 +17,7 @@ classifier = Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 -python-requires = >=3.6 +python_requires = >=3.6 [files] packages = From 3fd70ddc0522bea26986694113c7df7fd260c5bf Mon Sep 17 00:00:00 2001 From: Ryan Zimmerman Date: Tue, 11 May 2021 23:05:52 -0700 Subject: [PATCH 2846/3836] Add compute microversion 2.79 Compute 2.79 [1] added support for specifying the delete_on_termination field in the request body, which configures whether a volume is deleted when the server is destroyed. [1] https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#maximum-in-train Change-Id: I52e9f5d888a242ec564b90c8caac86ae02e8f596 --- openstack/compute/v2/volume_attachment.py | 6 ++++-- openstack/tests/unit/compute/v2/test_volume_attachment.py | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/openstack/compute/v2/volume_attachment.py b/openstack/compute/v2/volume_attachment.py index 770395c7d..45fc45bc2 100644 --- a/openstack/compute/v2/volume_attachment.py +++ b/openstack/compute/v2/volume_attachment.py @@ -39,5 +39,7 @@ class VolumeAttachment(resource.Resource): attachment_id = resource.Body('attachment_id', alternate_id=True) #: Virtual device tags for the attachment. tag = resource.Body('tag') - # tag introduced in 2.70 - _max_microversion = '2.70' + #: Indicates whether to delete the volume when server is destroyed + delete_on_termination = resource.Body('delete_on_termination') + # delete_on_termination introduced in 2.79 + _max_microversion = '2.79' diff --git a/openstack/tests/unit/compute/v2/test_volume_attachment.py b/openstack/tests/unit/compute/v2/test_volume_attachment.py index 2eb473d4a..03d031c1f 100644 --- a/openstack/tests/unit/compute/v2/test_volume_attachment.py +++ b/openstack/tests/unit/compute/v2/test_volume_attachment.py @@ -19,6 +19,7 @@ 'id': '2', 'volume_id': '3', 'tag': '4', + 'delete_on_termination': 'true', } @@ -46,3 +47,5 @@ def test_make_it(self): self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['volume_id'], sot.volume_id) self.assertEqual(EXAMPLE['tag'], sot.tag) + self.assertEqual(EXAMPLE['delete_on_termination'], + sot.delete_on_termination) From 288753558e3124d483d9fc686972543b4b7ec3f2 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 14 May 2021 21:33:59 +0000 Subject: [PATCH 2847/3836] Revert "Update TOX_CONSTRAINTS_FILE for feature/r1" This reverts commit 2d16931ca9fd3f447f05d7320a8c092ffb720a37. Reason for revert: r1 in requirements will actually never appear. It was an error merging this. Change-Id: Ifca8f5f0a0cfaed1f6555bacd1256f064efe4f9f --- tox.ini | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index b0069f350..185cfb5ea 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,7 @@ setenv = OS_STDOUT_CAPTURE={env:OS_STDOUT_CAPTURE:true} OS_STDERR_CAPTURE={env:OS_STDERR_CAPTURE:true} deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/r1} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt commands = stestr run {posargs} @@ -53,7 +53,7 @@ commands = [testenv:venv] deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/r1} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt @@ -87,7 +87,7 @@ commands = {toxinidir}/extras/run-ansible-tests.sh -e {envdir} {posargs} [testenv:docs] deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/r1} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/doc/requirements.txt commands = sphinx-build -W --keep-going -b html -j auto doc/source/ doc/build/html @@ -102,7 +102,7 @@ commands = [testenv:releasenotes] deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/r1} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/doc/requirements.txt commands = sphinx-build -W --keep-going -b html -j auto releasenotes/source releasenotes/build/html From 40cc84d53d8ddf2af333845d12bef9fc7a65c447 Mon Sep 17 00:00:00 2001 From: Polina Gubina Date: Thu, 13 May 2021 15:07:32 +0300 Subject: [PATCH 2848/3836] Add IPsecSiteConnection resource and proxy layer functionality for it Change-Id: I04b017f483f07e041cc429706cd92a39900d5eb6 --- doc/source/user/proxies/network.rst | 9 ++ doc/source/user/resources/network/index.rst | 1 + .../network/v2/ipsec_site_connection.rst | 13 +++ openstack/network/v2/_proxy.py | 96 ++++++++++++++++ openstack/network/v2/ipsec_site_connection.py | 105 ++++++++++++++++++ .../network/v2/test_ipsec_site_connection.py | 80 +++++++++++++ openstack/tests/unit/network/v2/test_proxy.py | 29 +++++ 7 files changed, 333 insertions(+) create mode 100644 doc/source/user/resources/network/v2/ipsec_site_connection.rst create mode 100644 openstack/network/v2/ipsec_site_connection.py create mode 100644 openstack/tests/unit/network/v2/test_ipsec_site_connection.py diff --git a/doc/source/user/proxies/network.rst b/doc/source/user/proxies/network.rst index d8eb64e51..3c5f76a76 100644 --- a/doc/source/user/proxies/network.rst +++ b/doc/source/user/proxies/network.rst @@ -234,6 +234,15 @@ VPN Operations :members: create_vpn_service, update_vpn_service, delete_vpn_service, get_vpn_service, find_vpn_service, vpn_services +IPSecSiteConnection Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + :noindex: + :members: create_vpn_ipsec_site_connection, update_vpn_ipsec_site_connection, + delete_vpn_ipsec_site_connection, get_vpn_ipsec_site_connection, + find_vpn_ipsec_site_connection, vpn_ipsec_site_connections + Extension Operations ^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/network/index.rst b/doc/source/user/resources/network/index.rst index 3884de78e..75fb5d928 100644 --- a/doc/source/user/resources/network/index.rst +++ b/doc/source/user/resources/network/index.rst @@ -13,6 +13,7 @@ Network Resources v2/flavor v2/floating_ip v2/health_monitor + v2/ipsec_site_connection v2/listener v2/load_balancer v2/metering_label diff --git a/doc/source/user/resources/network/v2/ipsec_site_connection.rst b/doc/source/user/resources/network/v2/ipsec_site_connection.rst new file mode 100644 index 000000000..3750344e7 --- /dev/null +++ b/doc/source/user/resources/network/v2/ipsec_site_connection.rst @@ -0,0 +1,13 @@ +openstack.network.v2.ipsec_site_connection +========================================== + +.. automodule:: openstack.network.v2.ipsec_site_connection + +The IPSecSiteConnection Class +----------------------------- + +The ``IPSecSiteConnection`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.ipsec_site_connection.IPSecSiteConnection + :members: diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 8d2fecdc9..ac9d735d4 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -24,6 +24,8 @@ from openstack.network.v2 import flavor as _flavor from openstack.network.v2 import floating_ip as _floating_ip from openstack.network.v2 import health_monitor as _health_monitor +from openstack.network.v2 import ipsec_site_connection as \ + _ipsec_site_connection from openstack.network.v2 import l3_conntrack_helper as _l3_conntrack_helper from openstack.network.v2 import listener as _listener from openstack.network.v2 import load_balancer as _load_balancer @@ -951,6 +953,100 @@ def update_health_monitor(self, health_monitor, **attrs): return self._update(_health_monitor.HealthMonitor, health_monitor, **attrs) + def create_vpn_ipsec_site_connection(self, **attrs): + """Create a new ipsec site connection from attributes + + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.network.v2.ipsec_site_connection. + IPSecSiteConnection`, comprised of the properties on the + IPSecSiteConnection class. + + :returns: The results of ipsec site connection creation :rtype: + :class:`~openstack.network.v2.ipsec_site_connection. + IPSecSiteConnection` + """ + return self._create(_ipsec_site_connection.IPSecSiteConnection, + **attrs) + + def find_vpn_ipsec_site_connection(self, name_or_id, + ignore_missing=True, **args): + """Find a single ipsec site connection + + :param name_or_id: The name or ID of an ipsec site connection. + :param bool ignore_missing: When set to ``False`` :class:`~openstack. + exceptions.ResourceNotFound` will be raised when the resource does + not exist.When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods such as query filters. + :returns: One :class:`~openstack.network.v2.ipsec_site_connection. + IPSecSiteConnection` or None + """ + return self._find(_ipsec_site_connection.IPSecSiteConnection, + name_or_id, ignore_missing=ignore_missing, **args) + + def get_vpn_ipsec_site_connection(self, ipsec_site_connection): + """Get a single ipsec site connection + + :param ipsec_site_connection: The value can be the ID of an ipsec site + connection or a :class:`~openstack.network.v2. + ipsec_site_connection.IPSecSiteConnection` instance. + + :returns: One :class:`~openstack.network.v2.ipsec_site_connection. + IPSecSiteConnection` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_ipsec_site_connection.IPSecSiteConnection, + ipsec_site_connection) + + def vpn_ipsec_site_connections(self, **query): + """Return a generator of ipsec site connections + + :param dict query: Optional query parameters to be sent to limit the + resources being returned. + + :returns: A generator of ipsec site connection objects + :rtype: :class:`~openstack.network.v2.ipsec_site_connection. + IPSecSiteConnection` + """ + return self._list(_ipsec_site_connection.IPSecSiteConnection, **query) + + def update_vpn_ipsec_site_connection(self, ipsec_site_connection, **attrs): + """Update a ipsec site connection + + :ipsec_site_connection: Either the id of an ipsec site connection or + a :class:`~openstack.network.v2.ipsec_site_connection. + IPSecSiteConnection` instance. + :param dict attrs: The attributes to update on the ipsec site + connection represented by ``ipsec_site_connection``. + + :returns: The updated ipsec site connection + :rtype: :class:`~openstack.network.v2.ipsec_site_connection. + IPSecSiteConnection` + """ + return self._update(_ipsec_site_connection.IPSecSiteConnection, + ipsec_site_connection, **attrs) + + def delete_vpn_ipsec_site_connection(self, ipsec_site_connection, + ignore_missing=True): + """Delete a ipsec site connection + + :param ipsec_site_connection: The value can be either the ID of an + ipsec site connection, or a :class:`~openstack.network.v2. + ipsec_site_connection.IPSecSiteConnection` instance. + :param bool ignore_missing: + When set to ``False`` :class:`~openstack.exceptions. + ResourceNotFound` will be raised when the ipsec site connection + does not exist. + When set to ``True``, no exception will be set when attempting to + delete a nonexistent ipsec site connection. + + :returns: ``None`` + """ + self._delete(_ipsec_site_connection.IPSecSiteConnection, + ipsec_site_connection, ignore_missing=ignore_missing) + def create_listener(self, **attrs): """Create a new listener from attributes diff --git a/openstack/network/v2/ipsec_site_connection.py b/openstack/network/v2/ipsec_site_connection.py new file mode 100644 index 000000000..1b2aa29e6 --- /dev/null +++ b/openstack/network/v2/ipsec_site_connection.py @@ -0,0 +1,105 @@ +# 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 resource + + +class IPSecSiteConnection(resource.Resource): + resource_key = 'ipsec_site_connection' + resources_key = 'ipsec_site_connections' + base_path = '/vpn/ipsec-site-connections' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + # Properties + #: The dead peer detection (DPD) action. + # A valid value is clear, hold, restart, + # disabled, or restart-by-peer. Default value is hold. + action = resource.Body('action') + #: The authentication mode. A valid value + # is psk, which is the default. + auth_mode = resource.Body('auth_mode') + #: A human-readable description for the resource. + # Default is an empty string. + description = resource.Body('description') + #: A dictionary with dead peer detection (DPD) protocol controls. + dpd = resource.Body('dpd', type=dict) + #: The administrative state of the resource, + # which is up (true) or down (false). + is_admin_state_up = resource.Body('admin_state_up', type=bool) + #: The ID of the IKE policy. + ikepolicy_id = resource.Body('ikepolicy_id') + #: Indicates whether this VPN can only respond + # to connections or both respond + # to and initiate connections. A valid value is + # response- only or bi-directional. Default is bi-directional. + initiator = resource.Body('initiator') + #: The ID of the IPsec policy. + ipsecpolicy_id = resource.Body('ipsecpolicy_id') + #: The dead peer detection (DPD) interval, in seconds. + # A valid value is a positive integer. Default is 30. + interval = resource.Body('interval', type=int) + #: The ID for the endpoint group that contains + # private subnets for the local side of the connection. + # Yo must specify this parameter with the + # peer_ep_group_id parameter unless in backward- compatible + # mode where peer_cidrs is provided with + # a subnet_id for the VPN service. + local_ep_group_id = resource.Body('local_ep_group_id') + #: The peer gateway public IPv4 or IPv6 address or FQDN. + peer_address = resource.Body('peer_address') + #: An ID to be used instead of the external IP address for + # a virtual router used in traffic between + # instances on different networks in east-west traffic. + # Most often, local ID would be domain + # name, email address, etc. If this is not configured + # then the external IP address will be used as the ID. + local_id = resource.Body('local_id') + #: The maximum transmission unit (MTU) + # value to address fragmentation. Minimum value + # is 68 for IPv4, and 1280 for IPv6. + mtu = resource.Body('mtu', type=int) + #: Human-readable name of the resource. Default is an empty string. + name = resource.Body('name') + #: The peer router identity for authentication. + # A valid value is an IPv4 address, IPv6 address, e-mail address, + # key ID, or FQDN. Typically, this value matches + # the peer_address value. + peer_id = resource.Body('peer_id') + #: (Deprecated) Unique list of valid peer private + # CIDRs in the form < net_address > / < prefix > . + peer_cidrs = resource.Body('peer_cidrs', type=list) + #: The ID of the project. + project_id = resource.Body('tenant_id') + #: The pre-shared key. A valid value is any string. + psk = resource.Body('psk') + #: The ID for the endpoint group that contains + # private CIDRs in the form < net_address > / < prefix > + # for the peer side of the connection. You must + # specify this parameter with the local_ep_group_id + # parameter unless in backward-compatible mode + # where peer_cidrs is provided with a subnet_id for the VPN service. + peer_ep_group_id = resource.Body('peer_ep_group_id') + #: The route mode. A valid value is static, which is the default. + route_mode = resource.Body('route_mode') + #: The dead peer detection (DPD) timeout + # in seconds. A valid value is a + # positive integer that is greater + # than the DPD interval value. Default is 120. + timeout = resource.Body('timeout', type=int) + #: The ID of the VPN service. + vpnservice_id = resource.Body('vpnservice_id') diff --git a/openstack/tests/unit/network/v2/test_ipsec_site_connection.py b/openstack/tests/unit/network/v2/test_ipsec_site_connection.py new file mode 100644 index 000000000..bee693bf6 --- /dev/null +++ b/openstack/tests/unit/network/v2/test_ipsec_site_connection.py @@ -0,0 +1,80 @@ +# 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.network.v2 import ipsec_site_connection +from openstack.tests.unit import base + + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + "admin_state_up": True, + "auth_mode": "1", + "ikepolicy_id": "2", + "vpnservice_id": "3", + "local_ep_group_id": "4", + "peer_address": "5", + "route_mode": "6", + "ipsecpolicy_id": "7", + "peer_id": "8", + "psk": "9", + "description": "10", + "initiator": "11", + "peer_cidrs": ['1', '2'], + "name": "12", + "tenant_id": "13", + "interval": 5, + "mtu": 5, + "peer_ep_group_id": "14", + "dpd": {'a': 5}, + "timeout": 16, + "action": "17", + "local_id": "18" +} + + +class TestIPSecSiteConnection(base.TestCase): + + def test_basic(self): + sot = ipsec_site_connection.IPSecSiteConnection() + self.assertEqual('ipsec_site_connection', sot.resource_key) + self.assertEqual('ipsec_site_connections', sot.resources_key) + self.assertEqual('/vpn/ipsec-site-connections', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = ipsec_site_connection.IPSecSiteConnection(**EXAMPLE) + self.assertTrue(sot.is_admin_state_up) + self.assertEqual(EXAMPLE['auth_mode'], sot.auth_mode) + self.assertEqual(EXAMPLE['ikepolicy_id'], sot.ikepolicy_id) + self.assertEqual(EXAMPLE['vpnservice_id'], sot.vpnservice_id) + self.assertEqual(EXAMPLE['local_ep_group_id'], sot.local_ep_group_id) + self.assertEqual(EXAMPLE['peer_address'], sot.peer_address) + self.assertEqual(EXAMPLE['route_mode'], sot.route_mode) + self.assertEqual(EXAMPLE['ipsecpolicy_id'], sot.ipsecpolicy_id) + self.assertEqual(EXAMPLE['peer_id'], sot.peer_id) + self.assertEqual(EXAMPLE['psk'], sot.psk) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['initiator'], sot.initiator) + self.assertEqual(EXAMPLE['peer_cidrs'], sot.peer_cidrs) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertEqual(EXAMPLE['interval'], sot.interval) + self.assertEqual(EXAMPLE['mtu'], sot.mtu) + self.assertEqual(EXAMPLE['peer_ep_group_id'], sot.peer_ep_group_id) + self.assertEqual(EXAMPLE['dpd'], sot.dpd) + self.assertEqual(EXAMPLE['timeout'], sot.timeout) + self.assertEqual(EXAMPLE['action'], sot.action) + self.assertEqual(EXAMPLE['local_id'], sot.local_id) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 9772d995b..369467b81 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -26,6 +26,7 @@ from openstack.network.v2 import flavor from openstack.network.v2 import floating_ip from openstack.network.v2 import health_monitor +from openstack.network.v2 import ipsec_site_connection from openstack.network.v2 import l3_conntrack_helper from openstack.network.v2 import listener from openstack.network.v2 import load_balancer @@ -224,6 +225,34 @@ def test_health_monitor_update(self): self.verify_update(self.proxy.update_health_monitor, health_monitor.HealthMonitor) + def test_ipsec_site_connection_create_attrs(self): + self.verify_create(self.proxy.create_vpn_ipsec_site_connection, + ipsec_site_connection.IPSecSiteConnection) + + def test_ipsec_site_connection_delete(self): + self.verify_delete(self.proxy.delete_vpn_ipsec_site_connection, + ipsec_site_connection.IPSecSiteConnection, False) + + def test_ipsec_site_connection_delete_ignore(self): + self.verify_delete(self.proxy.delete_vpn_ipsec_site_connection, + ipsec_site_connection.IPSecSiteConnection, True) + + def test_ipsec_site_connection_find(self): + self.verify_find(self.proxy.find_vpn_ipsec_site_connection, + ipsec_site_connection.IPSecSiteConnection) + + def test_ipsec_site_connection_get(self): + self.verify_get(self.proxy.get_vpn_ipsec_site_connection, + ipsec_site_connection.IPSecSiteConnection) + + def test_ipsec_site_connections(self): + self.verify_list(self.proxy.vpn_ipsec_site_connections, + ipsec_site_connection.IPSecSiteConnection) + + def test_ipsec_site_connection_update(self): + self.verify_update(self.proxy.update_vpn_ipsec_site_connection, + ipsec_site_connection.IPSecSiteConnection) + def test_listener_create_attrs(self): self.verify_create(self.proxy.create_listener, listener.Listener) From 7cee92d5e35259bc87954df5af1a51005ca67faf Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 19 May 2021 10:58:24 +0200 Subject: [PATCH 2849/3836] Replace deprecated inspect.getargspec call Replace inspect.getargspec with inspect.getfullargspec in the cloud utils. Change-Id: Iac99f7cc69c2e1e6ff6f186c4360d51e92d3a781 --- openstack/cloud/_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index 9c7488087..ab4748260 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -362,7 +362,7 @@ def valid_kwargs(*valid_args): # @decorator def func_wrapper(func, *args, **kwargs): - argspec = inspect.getargspec(func) + argspec = inspect.getfullargspec(func) for k in kwargs: if k not in argspec.args[1:] and k not in valid_args: raise TypeError( From 2e7b48ff4ebb59872d000cde20bacae74b0849d9 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 26 Apr 2021 12:19:12 +0100 Subject: [PATCH 2850/3836] tests: Rename 'TestProxyBase._verify2' to '_verify' Change-Id: I9d055751d631fa8c951f57d680448f9df26061a1 Signed-off-by: Stephen Finucane --- .../tests/unit/block_storage/v2/test_proxy.py | 4 +- .../tests/unit/block_storage/v3/test_proxy.py | 16 +- .../tests/unit/clustering/v1/test_proxy.py | 72 ++--- openstack/tests/unit/compute/v2/test_proxy.py | 218 +++++++------- .../tests/unit/database/v1/test_proxy.py | 26 +- openstack/tests/unit/dns/v2/test_proxy.py | 32 +- openstack/tests/unit/image/v2/test_proxy.py | 168 ++++++----- .../tests/unit/load_balancer/v2/test_proxy.py | 82 ++--- openstack/tests/unit/message/v2/test_proxy.py | 91 +++--- openstack/tests/unit/network/v2/test_proxy.py | 281 +++++++++--------- .../tests/unit/object_store/v1/test_proxy.py | 37 +-- .../tests/unit/orchestration/v1/test_proxy.py | 127 ++++---- openstack/tests/unit/test_proxy_base.py | 54 +--- 13 files changed, 610 insertions(+), 598 deletions(-) diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index a77e08aab..ffb49e030 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -88,7 +88,7 @@ def test_volume_delete_ignore(self): self.verify_delete(self.proxy.delete_volume, volume.Volume, True) def test_volume_extend(self): - self._verify2( + self._verify( "openstack.block_storage.v2.volume.Volume.extend", self.proxy.extend_volume, method_args=["value", "new-size"], @@ -142,7 +142,7 @@ def test_backup_restore(self): # NOTE: mock has_service self.proxy._connection = mock.Mock() self.proxy._connection.has_service = mock.Mock(return_value=True) - self._verify2( + self._verify( 'openstack.block_storage.v2.backup.Backup.restore', self.proxy.restore_backup, method_args=['volume_id'], diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 85a5356a5..044832a7a 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -82,7 +82,7 @@ def test_type_update(self): def test_type_extra_specs_update(self): kwargs = {"a": "1", "b": "2"} id = "an_id" - self._verify2( + self._verify( "openstack.block_storage.v3.type.Type.set_extra_specs", self.proxy.update_type_extra_specs, method_args=[id], @@ -94,7 +94,7 @@ def test_type_extra_specs_update(self): expected_result=kwargs) def test_type_extra_specs_delete(self): - self._verify2( + self._verify( "openstack.block_storage.v3.type.Type.delete_extra_specs", self.proxy.delete_type_extra_specs, expected_result=None, @@ -156,21 +156,21 @@ def test_volume_delete_ignore(self): self.verify_delete(self.proxy.delete_volume, volume.Volume, True) def test_volume_extend(self): - self._verify2( + self._verify( "openstack.block_storage.v3.volume.Volume.extend", self.proxy.extend_volume, method_args=["value", "new-size"], expected_args=[self.proxy, "new-size"]) def test_volume_set_readonly_no_argument(self): - self._verify2( + self._verify( "openstack.block_storage.v3.volume.Volume.set_readonly", self.proxy.set_volume_readonly, method_args=["value"], expected_args=[self.proxy, True]) def test_volume_set_readonly_false(self): - self._verify2( + self._verify( "openstack.block_storage.v3.volume.Volume.set_readonly", self.proxy.set_volume_readonly, method_args=["value", False], @@ -180,13 +180,13 @@ def test_volume_retype_without_migration_policy(self): self._verify("openstack.block_storage.v3.volume.Volume.retype", self.proxy.retype_volume, method_args=["value", "rbd"], - expected_args=["rbd", "never"]) + expected_args=[self.proxy, "rbd", "never"]) def test_volume_retype_with_migration_policy(self): self._verify("openstack.block_storage.v3.volume.Volume.retype", self.proxy.retype_volume, method_args=["value", "rbd", "on-demand"], - expected_args=["rbd", "on-demand"]) + expected_args=[self.proxy, "rbd", "on-demand"]) def test_backend_pools(self): self.verify_list(self.proxy.backend_pools, stats.Pools) @@ -242,7 +242,7 @@ def test_backup_restore(self): # NOTE: mock has_service self.proxy._connection = mock.Mock() self.proxy._connection.has_service = mock.Mock(return_value=True) - self._verify2( + self._verify( 'openstack.block_storage.v3.backup.Backup.restore', self.proxy.restore_backup, method_args=['volume_id'], diff --git a/openstack/tests/unit/clustering/v1/test_proxy.py b/openstack/tests/unit/clustering/v1/test_proxy.py index f06be57c5..7c8f62dc5 100644 --- a/openstack/tests/unit/clustering/v1/test_proxy.py +++ b/openstack/tests/unit/clustering/v1/test_proxy.py @@ -91,7 +91,7 @@ def test_cluster_delete_ignore(self): self.verify_delete(self.proxy.delete_cluster, cluster.Cluster, True) def test_cluster_force_delete(self): - self._verify2( + self._verify( "openstack.clustering.v1.cluster.Cluster.force_delete", self.proxy.delete_cluster, method_args=["value", False, True], @@ -119,7 +119,7 @@ def test_services(self): def test_resize_cluster(self, mock_find): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_find.return_value = mock_cluster - self._verify2( + self._verify( "openstack.clustering.v1.cluster.Cluster.resize", self.proxy.resize_cluster, method_args=["FAKE_CLUSTER"], @@ -131,7 +131,7 @@ def test_resize_cluster(self, mock_find): def test_resize_cluster_with_obj(self): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') - self._verify2( + self._verify( "openstack.clustering.v1.cluster.Cluster.resize", self.proxy.resize_cluster, method_args=[mock_cluster], @@ -151,7 +151,7 @@ def test_collect_cluster_attrs(self): def test_cluster_check(self, mock_get): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_get.return_value = mock_cluster - self._verify2( + self._verify( "openstack.clustering.v1.cluster.Cluster.check", self.proxy.check_cluster, method_args=["FAKE_CLUSTER"], @@ -162,7 +162,7 @@ def test_cluster_check(self, mock_get): def test_cluster_recover(self, mock_get): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') mock_get.return_value = mock_cluster - self._verify2( + self._verify( "openstack.clustering.v1.cluster.Cluster.recover", self.proxy.recover_cluster, method_args=["FAKE_CLUSTER"], @@ -179,7 +179,7 @@ def test_node_delete_ignore(self): self.verify_delete(self.proxy.delete_node, node.Node, True) def test_node_force_delete(self): - self._verify2( + self._verify( "openstack.clustering.v1.node.Node.force_delete", self.proxy.delete_node, method_args=["value", False, True], @@ -192,13 +192,13 @@ def test_node_get(self): self.verify_get(self.proxy.get_node, node.Node) def test_node_get_with_details(self): - self._verify2('openstack.proxy.Proxy._get', - self.proxy.get_node, - method_args=['NODE_ID'], - method_kwargs={'details': True}, - expected_args=[node.NodeDetail], - expected_kwargs={'node_id': 'NODE_ID', - 'requires_id': False}) + self._verify( + 'openstack.proxy.Proxy._get', + self.proxy.get_node, + method_args=['NODE_ID'], + method_kwargs={'details': True}, + expected_args=[node.NodeDetail], + expected_kwargs={'node_id': 'NODE_ID', 'requires_id': False}) def test_nodes(self): self.verify_list(self.proxy.nodes, node.Node, @@ -212,7 +212,7 @@ def test_node_update(self): def test_node_check(self, mock_get): mock_node = node.Node.new(id='FAKE_NODE') mock_get.return_value = mock_node - self._verify2( + self._verify( "openstack.clustering.v1.node.Node.check", self.proxy.check_node, method_args=["FAKE_NODE"], @@ -223,7 +223,7 @@ def test_node_check(self, mock_get): def test_node_recover(self, mock_get): mock_node = node.Node.new(id='FAKE_NODE') mock_get.return_value = mock_node - self._verify2( + self._verify( "openstack.clustering.v1.node.Node.recover", self.proxy.recover_node, method_args=["FAKE_NODE"], @@ -234,7 +234,7 @@ def test_node_recover(self, mock_get): def test_node_adopt(self, mock_get): mock_node = node.Node.new() mock_get.return_value = mock_node - self._verify2( + self._verify( "openstack.clustering.v1.node.Node.adopt", self.proxy.adopt_node, method_kwargs={"preview": False, "foo": "bar"}, @@ -247,7 +247,7 @@ def test_node_adopt(self, mock_get): def test_node_adopt_preview(self, mock_get): mock_node = node.Node.new() mock_get.return_value = mock_node - self._verify2( + self._verify( "openstack.clustering.v1.node.Node.adopt", self.proxy.adopt_node, method_kwargs={"preview": True, "foo": "bar"}, @@ -294,29 +294,29 @@ def test_get_cluster_policy(self): fake_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') # ClusterPolicy object as input - self._verify2('openstack.proxy.Proxy._get', - self.proxy.get_cluster_policy, - method_args=[fake_policy, "FAKE_CLUSTER"], - expected_args=[cluster_policy.ClusterPolicy, - fake_policy], - expected_kwargs={'cluster_id': 'FAKE_CLUSTER'}, - expected_result=fake_policy) + self._verify( + 'openstack.proxy.Proxy._get', + self.proxy.get_cluster_policy, + method_args=[fake_policy, "FAKE_CLUSTER"], + expected_args=[cluster_policy.ClusterPolicy, fake_policy], + expected_kwargs={'cluster_id': 'FAKE_CLUSTER'}, + expected_result=fake_policy) # Policy ID as input - self._verify2('openstack.proxy.Proxy._get', - self.proxy.get_cluster_policy, - method_args=["FAKE_POLICY", "FAKE_CLUSTER"], - expected_args=[cluster_policy.ClusterPolicy, - "FAKE_POLICY"], - expected_kwargs={"cluster_id": "FAKE_CLUSTER"}) + self._verify( + 'openstack.proxy.Proxy._get', + self.proxy.get_cluster_policy, + method_args=["FAKE_POLICY", "FAKE_CLUSTER"], + expected_args=[cluster_policy.ClusterPolicy, "FAKE_POLICY"], + expected_kwargs={"cluster_id": "FAKE_CLUSTER"}) # Cluster object as input - self._verify2('openstack.proxy.Proxy._get', - self.proxy.get_cluster_policy, - method_args=["FAKE_POLICY", fake_cluster], - expected_args=[cluster_policy.ClusterPolicy, - "FAKE_POLICY"], - expected_kwargs={"cluster_id": fake_cluster}) + self._verify( + 'openstack.proxy.Proxy._get', + self.proxy.get_cluster_policy, + method_args=["FAKE_POLICY", fake_cluster], + expected_args=[cluster_policy.ClusterPolicy, "FAKE_POLICY"], + expected_kwargs={"cluster_id": fake_cluster}) def test_receiver_create(self): self.verify_create(self.proxy.create_receiver, receiver.Receiver) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 2dcb4adbe..a0ed8da44 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -66,7 +66,7 @@ def test_flavor_find_fetch_extra(self): ) as mocked: res = flavor.Flavor() mocked.return_value = res - self._verify2( + self._verify( 'openstack.proxy.Proxy._find', self.proxy.find_flavor, method_args=['res', True, True], @@ -83,7 +83,7 @@ def test_flavor_find_skip_fetch_extra(self): ) as mocked: res = flavor.Flavor(extra_specs={'a': 'b'}) mocked.return_value = res - self._verify2( + self._verify( 'openstack.proxy.Proxy._find', self.proxy.find_flavor, method_args=['res', True], @@ -100,7 +100,7 @@ def test_flavor_get_no_extra(self): ) as mocked: res = flavor.Flavor() mocked.return_value = res - self._verify2( + self._verify( 'openstack.proxy.Proxy._get', self.proxy.get_flavor, method_args=['res'], @@ -116,7 +116,7 @@ def test_flavor_get_fetch_extra(self): ) as mocked: res = flavor.Flavor() mocked.return_value = res - self._verify2( + self._verify( 'openstack.proxy.Proxy._get', self.proxy.get_flavor, method_args=['res', True], @@ -132,7 +132,7 @@ def test_flavor_get_skip_fetch_extra(self): ) as mocked: res = flavor.Flavor(extra_specs={'a': 'b'}) mocked.return_value = res - self._verify2( + self._verify( 'openstack.proxy.Proxy._get', self.proxy.get_flavor, method_args=['res', True], @@ -193,35 +193,35 @@ def test_flavors_get_extra(self, fetch_mock, list_mock): ) def test_flavor_get_access(self): - self._verify2( + self._verify( "openstack.compute.v2.flavor.Flavor.get_access", self.proxy.get_flavor_access, method_args=["value"], expected_args=[self.proxy]) def test_flavor_add_tenant_access(self): - self._verify2( + self._verify( "openstack.compute.v2.flavor.Flavor.add_tenant_access", self.proxy.flavor_add_tenant_access, method_args=["value", "fake-tenant"], expected_args=[self.proxy, "fake-tenant"]) def test_flavor_remove_tenant_access(self): - self._verify2( + self._verify( "openstack.compute.v2.flavor.Flavor.remove_tenant_access", self.proxy.flavor_remove_tenant_access, method_args=["value", "fake-tenant"], expected_args=[self.proxy, "fake-tenant"]) def test_flavor_fetch_extra_specs(self): - self._verify2( + self._verify( "openstack.compute.v2.flavor.Flavor.fetch_extra_specs", self.proxy.fetch_flavor_extra_specs, method_args=["value"], expected_args=[self.proxy]) def test_create_flavor_extra_specs(self): - self._verify2( + self._verify( "openstack.compute.v2.flavor.Flavor.create_extra_specs", self.proxy.create_flavor_extra_specs, method_args=["value", {'a': 'b'}], @@ -229,21 +229,21 @@ def test_create_flavor_extra_specs(self): expected_kwargs={"specs": {'a': 'b'}}) def test_get_flavor_extra_specs_prop(self): - self._verify2( + self._verify( "openstack.compute.v2.flavor.Flavor.get_extra_specs_property", self.proxy.get_flavor_extra_specs_property, method_args=["value", "prop"], expected_args=[self.proxy, "prop"]) def test_update_flavor_extra_specs_prop(self): - self._verify2( + self._verify( "openstack.compute.v2.flavor.Flavor.update_extra_specs_property", self.proxy.update_flavor_extra_specs_property, method_args=["value", "prop", "val"], expected_args=[self.proxy, "prop", "val"]) def test_delete_flavor_extra_specs_prop(self): - self._verify2( + self._verify( "openstack.compute.v2.flavor.Flavor.delete_extra_specs_property", self.proxy.delete_flavor_extra_specs_property, method_args=["value", "prop"], @@ -324,35 +324,35 @@ def test_aggregate_update(self): self.verify_update(self.proxy.update_aggregate, aggregate.Aggregate) def test_aggregate_add_host(self): - self._verify2( + self._verify( "openstack.compute.v2.aggregate.Aggregate.add_host", self.proxy.add_host_to_aggregate, method_args=["value", "host"], expected_args=[self.proxy, "host"]) def test_aggregate_remove_host(self): - self._verify2( + self._verify( "openstack.compute.v2.aggregate.Aggregate.remove_host", self.proxy.remove_host_from_aggregate, method_args=["value", "host"], expected_args=[self.proxy, "host"]) def test_aggregate_set_metadata(self): - self._verify2( + self._verify( "openstack.compute.v2.aggregate.Aggregate.set_metadata", self.proxy.set_aggregate_metadata, method_args=["value", {'a': 'b'}], expected_args=[self.proxy, {'a': 'b'}]) def test_aggregate_precache_image(self): - self._verify2( + self._verify( "openstack.compute.v2.aggregate.Aggregate.precache_images", self.proxy.aggregate_precache_images, method_args=["value", '1'], expected_args=[self.proxy, [{'id': '1'}]]) def test_aggregate_precache_images(self): - self._verify2( + self._verify( "openstack.compute.v2.aggregate.Aggregate.precache_images", self.proxy.aggregate_precache_images, method_args=["value", ['1', '2']], @@ -366,7 +366,7 @@ def test_services(self): @mock.patch('openstack.utils.supports_microversion', autospec=True, return_value=False) def test_enable_service_252(self, mv_mock): - self._verify2( + self._verify( 'openstack.compute.v2.service.Service.enable', self.proxy.enable_service, method_args=["value", "host1", "nova-compute"], @@ -376,7 +376,7 @@ def test_enable_service_252(self, mv_mock): @mock.patch('openstack.utils.supports_microversion', autospec=True, return_value=True) def test_enable_service_253(self, mv_mock): - self._verify2( + self._verify( 'openstack.proxy.Proxy._update', self.proxy.enable_service, method_args=["value"], @@ -388,7 +388,7 @@ def test_enable_service_253(self, mv_mock): @mock.patch('openstack.utils.supports_microversion', autospec=True, return_value=False) def test_disable_service_252(self, mv_mock): - self._verify2( + self._verify( 'openstack.compute.v2.service.Service.disable', self.proxy.disable_service, method_args=["value", "host1", "nova-compute"], @@ -397,7 +397,7 @@ def test_disable_service_252(self, mv_mock): @mock.patch('openstack.utils.supports_microversion', autospec=True, return_value=True) def test_disable_service_253(self, mv_mock): - self._verify2( + self._verify( 'openstack.proxy.Proxy._update', self.proxy.disable_service, method_args=["value"], @@ -412,7 +412,7 @@ def test_disable_service_253(self, mv_mock): @mock.patch('openstack.utils.supports_microversion', autospec=True, return_value=False) def test_force_service_down_252(self, mv_mock): - self._verify2( + self._verify( 'openstack.compute.v2.service.Service.set_forced_down', self.proxy.update_service_forced_down, method_args=["value", "host1", "nova-compute"], @@ -430,7 +430,7 @@ def test_force_service_down_252_empty_vals(self, mv_mock): @mock.patch('openstack.utils.supports_microversion', autospec=True, return_value=False) def test_force_service_down_252_empty_vals_svc(self, mv_mock): - self._verify2( + self._verify( 'openstack.compute.v2.service.Service.set_forced_down', self.proxy.update_service_forced_down, method_args=[{'host': 'a', 'binary': 'b'}, None, None], @@ -516,7 +516,7 @@ def test_get_hypervisor(self): hypervisor.Hypervisor) def test_get_hypervisor_uptime(self): - self._verify2( + self._verify( "openstack.compute.v2.hypervisor.Hypervisor.get_uptime", self.proxy.get_hypervisor_uptime, method_args=["value"], @@ -570,24 +570,22 @@ def test_server_interface_delete(self): test_interface.server_id = server_id # Case1: ServerInterface instance is provided as value - self._verify2("openstack.proxy.Proxy._delete", - self.proxy.delete_server_interface, - method_args=[test_interface], - method_kwargs={"server": server_id}, - expected_args=[ - server_interface.ServerInterface, interface_id], - expected_kwargs={"server_id": server_id, - "ignore_missing": True}) + self._verify( + "openstack.proxy.Proxy._delete", + self.proxy.delete_server_interface, + method_args=[test_interface], + method_kwargs={"server": server_id}, + expected_args=[server_interface.ServerInterface, interface_id], + expected_kwargs={"server_id": server_id, "ignore_missing": True}) # Case2: ServerInterface ID is provided as value - self._verify2("openstack.proxy.Proxy._delete", - self.proxy.delete_server_interface, - method_args=[interface_id], - method_kwargs={"server": server_id}, - expected_args=[ - server_interface.ServerInterface, interface_id], - expected_kwargs={"server_id": server_id, - "ignore_missing": True}) + self._verify( + "openstack.proxy.Proxy._delete", + self.proxy.delete_server_interface, + method_args=[interface_id], + method_kwargs={"server": server_id}, + expected_args=[server_interface.ServerInterface, interface_id], + expected_kwargs={"server_id": server_id, "ignore_missing": True}) def test_server_interface_delete_ignore(self): self.proxy._get_uri_attribute = lambda *args: args[1] @@ -606,22 +604,22 @@ def test_server_interface_get(self): test_interface.server_id = server_id # Case1: ServerInterface instance is provided as value - self._verify2('openstack.proxy.Proxy._get', - self.proxy.get_server_interface, - method_args=[test_interface], - method_kwargs={"server": server_id}, - expected_args=[server_interface.ServerInterface], - expected_kwargs={"port_id": interface_id, - "server_id": server_id}) + self._verify( + 'openstack.proxy.Proxy._get', + self.proxy.get_server_interface, + method_args=[test_interface], + method_kwargs={"server": server_id}, + expected_args=[server_interface.ServerInterface], + expected_kwargs={"port_id": interface_id, "server_id": server_id}) # Case2: ServerInterface ID is provided as value - self._verify2('openstack.proxy.Proxy._get', - self.proxy.get_server_interface, - method_args=[interface_id], - method_kwargs={"server": server_id}, - expected_args=[server_interface.ServerInterface], - expected_kwargs={"port_id": interface_id, - "server_id": server_id}) + self._verify( + 'openstack.proxy.Proxy._get', + self.proxy.get_server_interface, + method_args=[interface_id], + method_kwargs={"server": server_id}, + expected_args=[server_interface.ServerInterface], + expected_kwargs={"port_id": interface_id, "server_id": server_id}) def test_server_interfaces(self): self.verify_list(self.proxy.server_interfaces, @@ -655,7 +653,7 @@ def test_server_delete_ignore(self): self.verify_delete(self.proxy.delete_server, server.Server, True) def test_server_force_delete(self): - self._verify2( + self._verify( "openstack.compute.v2.server.Server.force_delete", self.proxy.delete_server, method_args=["value", False, True], @@ -691,21 +689,21 @@ def test_server_wait_for(self): expected_args=[self.proxy, value, 'ACTIVE', ['ERROR'], 2, 120]) def test_server_resize(self): - self._verify2( + self._verify( "openstack.compute.v2.server.Server.resize", self.proxy.resize_server, method_args=["value", "test-flavor"], expected_args=[self.proxy, "test-flavor"]) def test_server_confirm_resize(self): - self._verify2( + self._verify( "openstack.compute.v2.server.Server.confirm_resize", self.proxy.confirm_server_resize, method_args=["value"], expected_args=[self.proxy]) def test_server_revert_resize(self): - self._verify2( + self._verify( "openstack.compute.v2.server.Server.revert_resize", self.proxy.revert_server_resize, method_args=["value"], @@ -718,7 +716,7 @@ def test_server_rebuild(self): # Case1: image object is provided # NOTE: Inside of Server.rebuild is where image_obj gets converted # to an ID instead of object. - self._verify2( + self._verify( 'openstack.compute.v2.server.Server.rebuild', self.proxy.rebuild_server, method_args=["value", "test_server", "test_pass"], @@ -727,7 +725,7 @@ def test_server_rebuild(self): expected_kwargs={"metadata": {"k1": "v1"}, "image": image_obj}) # Case2: image name or id is provided - self._verify2( + self._verify( 'openstack.compute.v2.server.Server.rebuild', self.proxy.rebuild_server, method_args=["value", "test_server", "test_pass"], @@ -736,21 +734,21 @@ def test_server_rebuild(self): expected_kwargs={"metadata": {"k1": "v1"}, "image": id}) def test_add_fixed_ip_to_server(self): - self._verify2( + self._verify( "openstack.compute.v2.server.Server.add_fixed_ip", self.proxy.add_fixed_ip_to_server, method_args=["value", "network-id"], expected_args=[self.proxy, "network-id"]) def test_fixed_ip_from_server(self): - self._verify2( + self._verify( "openstack.compute.v2.server.Server.remove_fixed_ip", self.proxy.remove_fixed_ip_from_server, method_args=["value", "address"], expected_args=[self.proxy, "address"]) def test_floating_ip_to_server(self): - self._verify2( + self._verify( "openstack.compute.v2.server.Server.add_floating_ip", self.proxy.add_floating_ip_to_server, method_args=["value", "floating-ip"], @@ -758,7 +756,7 @@ def test_floating_ip_to_server(self): expected_kwargs={'fixed_address': None}) def test_add_floating_ip_to_server_with_fixed_addr(self): - self._verify2( + self._verify( "openstack.compute.v2.server.Server.add_floating_ip", self.proxy.add_floating_ip_to_server, method_args=["value", "floating-ip", 'fixed-addr'], @@ -766,63 +764,63 @@ def test_add_floating_ip_to_server_with_fixed_addr(self): expected_kwargs={'fixed_address': 'fixed-addr'}) def test_remove_floating_ip_from_server(self): - self._verify2( + self._verify( "openstack.compute.v2.server.Server.remove_floating_ip", self.proxy.remove_floating_ip_from_server, method_args=["value", "address"], expected_args=[self.proxy, "address"]) def test_server_backup(self): - self._verify2( + self._verify( "openstack.compute.v2.server.Server.backup", self.proxy.backup_server, method_args=["value", "name", "daily", 1], expected_args=[self.proxy, "name", "daily", 1]) def test_server_pause(self): - self._verify2( + self._verify( "openstack.compute.v2.server.Server.pause", self.proxy.pause_server, method_args=["value"], expected_args=[self.proxy]) def test_server_unpause(self): - self._verify2( + self._verify( "openstack.compute.v2.server.Server.unpause", self.proxy.unpause_server, method_args=["value"], expected_args=[self.proxy]) def test_server_suspend(self): - self._verify2( + self._verify( "openstack.compute.v2.server.Server.suspend", self.proxy.suspend_server, method_args=["value"], expected_args=[self.proxy]) def test_server_resume(self): - self._verify2( + self._verify( "openstack.compute.v2.server.Server.resume", self.proxy.resume_server, method_args=["value"], expected_args=[self.proxy]) def test_server_lock(self): - self._verify2( + self._verify( "openstack.compute.v2.server.Server.lock", self.proxy.lock_server, method_args=["value"], expected_args=[self.proxy]) def test_server_unlock(self): - self._verify2( + self._verify( "openstack.compute.v2.server.Server.unlock", self.proxy.unlock_server, method_args=["value"], expected_args=[self.proxy]) def test_server_rescue(self): - self._verify2( + self._verify( "openstack.compute.v2.server.Server.rescue", self.proxy.rescue_server, method_args=["value"], @@ -830,7 +828,7 @@ def test_server_rescue(self): expected_kwargs={"admin_pass": None, "image_ref": None}) def test_server_rescue_with_options(self): - self._verify2( + self._verify( "openstack.compute.v2.server.Server.rescue", self.proxy.rescue_server, method_args=["value", 'PASS', 'IMG'], @@ -838,14 +836,14 @@ def test_server_rescue_with_options(self): expected_kwargs={"admin_pass": 'PASS', "image_ref": 'IMG'}) def test_server_unrescue(self): - self._verify2( + self._verify( "openstack.compute.v2.server.Server.unrescue", self.proxy.unrescue_server, method_args=["value"], expected_args=[self.proxy]) def test_server_evacuate(self): - self._verify2( + self._verify( "openstack.compute.v2.server.Server.evacuate", self.proxy.evacuate_server, method_args=["value"], @@ -853,7 +851,7 @@ def test_server_evacuate(self): expected_kwargs={"host": None, "admin_pass": None, "force": None}) def test_server_evacuate_with_options(self): - self._verify2( + self._verify( "openstack.compute.v2.server.Server.evacuate", self.proxy.evacuate_server, method_args=["value", 'HOST2', 'NEW_PASS', True], @@ -862,42 +860,42 @@ def test_server_evacuate_with_options(self): "host": "HOST2", "admin_pass": 'NEW_PASS', "force": True}) def test_server_start(self): - self._verify2( + self._verify( "openstack.compute.v2.server.Server.start", self.proxy.start_server, method_args=["value"], expected_args=[self.proxy]) def test_server_stop(self): - self._verify2( + self._verify( "openstack.compute.v2.server.Server.stop", self.proxy.stop_server, method_args=["value"], expected_args=[self.proxy]) def test_server_shelve(self): - self._verify2( + self._verify( "openstack.compute.v2.server.Server.shelve", self.proxy.shelve_server, method_args=["value"], expected_args=[self.proxy]) def test_server_unshelve(self): - self._verify2( + self._verify( "openstack.compute.v2.server.Server.unshelve", self.proxy.unshelve_server, method_args=["value"], expected_args=[self.proxy]) def test_get_server_output(self): - self._verify2( + self._verify( "openstack.compute.v2.server.Server.get_console_output", self.proxy.get_server_console_output, method_args=["value"], expected_args=[self.proxy], expected_kwargs={"length": None}) - self._verify2( + self._verify( "openstack.compute.v2.server.Server.get_console_output", self.proxy.get_server_console_output, method_args=["value", 1], @@ -917,32 +915,34 @@ def test_availability_zones_detailed(self): expected_kwargs={}) def test_get_all_server_metadata(self): - self._verify2("openstack.compute.v2.server.Server.get_metadata", - self.proxy.get_server_metadata, - method_args=["value"], - method_result=server.Server(id="value", metadata={}), - expected_args=[self.proxy], - expected_result={}) + self._verify( + "openstack.compute.v2.server.Server.get_metadata", + self.proxy.get_server_metadata, + method_args=["value"], + method_result=server.Server(id="value", metadata={}), + expected_args=[self.proxy], + expected_result={}) def test_set_server_metadata(self): kwargs = {"a": "1", "b": "2"} id = "an_id" - self._verify2("openstack.compute.v2.server.Server.set_metadata", - self.proxy.set_server_metadata, - method_args=[id], - method_kwargs=kwargs, - method_result=server.Server.existing(id=id, - metadata=kwargs), - expected_args=[self.proxy], - expected_kwargs=kwargs, - expected_result=kwargs) + self._verify( + "openstack.compute.v2.server.Server.set_metadata", + self.proxy.set_server_metadata, + method_args=[id], + method_kwargs=kwargs, + method_result=server.Server.existing(id=id, metadata=kwargs), + expected_args=[self.proxy], + expected_kwargs=kwargs, + expected_result=kwargs) def test_delete_server_metadata(self): - self._verify2("openstack.compute.v2.server.Server.delete_metadata", - self.proxy.delete_server_metadata, - expected_result=None, - method_args=["value", "key"], - expected_args=[self.proxy, "key"]) + self._verify( + "openstack.compute.v2.server.Server.delete_metadata", + self.proxy.delete_server_metadata, + expected_result=None, + method_args=["value", "key"], + expected_args=[self.proxy, "key"]) def test_create_image(self): metadata = {'k1': 'v1'} @@ -995,7 +995,7 @@ def test_server_groups(self): self.verify_list(self.proxy.server_groups, server_group.ServerGroup) def test_live_migrate_server(self): - self._verify2( + self._verify( 'openstack.compute.v2.server.Server.live_migrate', self.proxy.live_migrate_server, method_args=["value", "host1", False], @@ -1003,21 +1003,21 @@ def test_live_migrate_server(self): expected_kwargs={'force': False, 'block_migration': None}) def test_fetch_security_groups(self): - self._verify2( + self._verify( 'openstack.compute.v2.server.Server.fetch_security_groups', self.proxy.fetch_server_security_groups, method_args=["value"], expected_args=[self.proxy]) def test_add_security_groups(self): - self._verify2( + self._verify( 'openstack.compute.v2.server.Server.add_security_group', self.proxy.add_security_group_to_server, method_args=["value", {'id': 'id', 'name': 'sg'}], expected_args=[self.proxy, 'sg']) def test_remove_security_groups(self): - self._verify2( + self._verify( 'openstack.compute.v2.server.Server.remove_security_group', self.proxy.remove_security_group_from_server, method_args=["value", {'id': 'id', 'name': 'sg'}], @@ -1031,7 +1031,7 @@ def test_create_server_remote_console(self): expected_kwargs={"server_id": "test_id", "type": "fake"}) def test_get_console_url(self): - self._verify2( + self._verify( 'openstack.compute.v2.server.Server.get_console_url', self.proxy.get_server_console_url, method_args=["value", "console_type"], diff --git a/openstack/tests/unit/database/v1/test_proxy.py b/openstack/tests/unit/database/v1/test_proxy.py index 017810f06..81f276dd8 100644 --- a/openstack/tests/unit/database/v1/test_proxy.py +++ b/openstack/tests/unit/database/v1/test_proxy.py @@ -44,12 +44,13 @@ def test_database_delete_ignore(self): expected_kwargs={"instance_id": "test_id"}) def test_database_find(self): - self._verify2('openstack.proxy.Proxy._find', - self.proxy.find_database, - method_args=["db", "instance"], - expected_args=[database.Database, "db"], - expected_kwargs={"instance_id": "instance", - "ignore_missing": True}) + self._verify( + 'openstack.proxy.Proxy._find', + self.proxy.find_database, + method_args=["db", "instance"], + expected_args=[database.Database, "db"], + expected_kwargs={ + "instance_id": "instance", "ignore_missing": True}) def test_databases(self): self.verify_list(self.proxy.databases, database.Database, @@ -108,12 +109,13 @@ def test_user_delete_ignore(self): expected_kwargs={"instance_id": "id"}) def test_user_find(self): - self._verify2('openstack.proxy.Proxy._find', - self.proxy.find_user, - method_args=["user", "instance"], - expected_args=[user.User, "user"], - expected_kwargs={"instance_id": "instance", - "ignore_missing": True}) + self._verify( + 'openstack.proxy.Proxy._find', + self.proxy.find_user, + method_args=["user", "instance"], + expected_args=[user.User, "user"], + expected_kwargs={ + "instance_id": "instance", "ignore_missing": True}) def test_users(self): self.verify_list(self.proxy.users, user.User, diff --git a/openstack/tests/unit/dns/v2/test_proxy.py b/openstack/tests/unit/dns/v2/test_proxy.py index 48369b8f3..d628a724b 100644 --- a/openstack/tests/unit/dns/v2/test_proxy.py +++ b/openstack/tests/unit/dns/v2/test_proxy.py @@ -50,16 +50,18 @@ def test_zone_update(self): self.verify_update(self.proxy.update_zone, zone.Zone) def test_zone_abandon(self): - self._verify2("openstack.dns.v2.zone.Zone.abandon", - self.proxy.abandon_zone, - method_args=[{'zone': 'id'}], - expected_args=[self.proxy]) + self._verify( + "openstack.dns.v2.zone.Zone.abandon", + self.proxy.abandon_zone, + method_args=[{'zone': 'id'}], + expected_args=[self.proxy]) def test_zone_xfr(self): - self._verify2("openstack.dns.v2.zone.Zone.xfr", - self.proxy.xfr_zone, - method_args=[{'zone': 'id'}], - expected_args=[self.proxy]) + self._verify( + "openstack.dns.v2.zone.Zone.xfr", + self.proxy.xfr_zone, + method_args=[{'zone': 'id'}], + expected_args=[self.proxy]) class TestDnsRecordset(TestDnsProxy): @@ -92,13 +94,13 @@ def test_recordsets_zone(self): expected_kwargs={'zone_id': 'zid'}) def test_recordset_find(self): - self._verify2("openstack.proxy.Proxy._find", - self.proxy.find_recordset, - method_args=['zone', 'rs'], - method_kwargs={}, - expected_args=[recordset.Recordset, 'rs'], - expected_kwargs={'ignore_missing': True, - 'zone_id': 'zone'}) + self._verify( + "openstack.proxy.Proxy._find", + self.proxy.find_recordset, + method_args=['zone', 'rs'], + method_kwargs={}, + expected_args=[recordset.Recordset, 'rs'], + expected_kwargs={'ignore_missing': True, 'zone_id': 'zone'}) class TestDnsFloatIP(TestDnsProxy): diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 715d4b2a1..0a2826872 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -54,7 +54,7 @@ def test_image_import_no_required_attrs(self): def test_image_import(self): original_image = image.Image(**EXAMPLE) - self._verify2( + self._verify( "openstack.image.v2.image.Image.import_image", self.proxy.import_image, method_args=[original_image, "method", "uri"], @@ -219,7 +219,7 @@ def test_image_upload(self): def test_image_download(self): original_image = image.Image(**EXAMPLE) - self._verify2( + self._verify( 'openstack.image.v2.image.Image.download', self.proxy.download_image, method_args=[original_image], @@ -294,28 +294,28 @@ def test_images(self): self.verify_list(self.proxy.images, image.Image) def test_add_tag(self): - self._verify2( + self._verify( "openstack.image.v2.image.Image.add_tag", self.proxy.add_tag, method_args=["image", "tag"], expected_args=[self.proxy, "tag"]) def test_remove_tag(self): - self._verify2( + self._verify( "openstack.image.v2.image.Image.remove_tag", self.proxy.remove_tag, method_args=["image", "tag"], expected_args=[self.proxy, "tag"]) def test_deactivate_image(self): - self._verify2( + self._verify( "openstack.image.v2.image.Image.deactivate", self.proxy.deactivate_image, method_args=["image"], expected_args=[self.proxy]) def test_reactivate_image(self): - self._verify2( + self._verify( "openstack.image.v2.image.Image.reactivate", self.proxy.reactivate_image, method_args=["image"], @@ -327,51 +327,54 @@ def test_member_create(self): expected_kwargs={"image_id": "test_id"}) def test_member_delete(self): - self._verify2("openstack.proxy.Proxy._delete", - self.proxy.remove_member, - method_args=["member_id"], - method_kwargs={"image": "image_id", - "ignore_missing": False}, - expected_args=[member.Member], - expected_kwargs={"member_id": "member_id", - "image_id": "image_id", - "ignore_missing": False}) + self._verify( + "openstack.proxy.Proxy._delete", + self.proxy.remove_member, + method_args=["member_id"], + method_kwargs={"image": "image_id", "ignore_missing": False}, + expected_args=[member.Member], + expected_kwargs={ + "member_id": "member_id", + "image_id": "image_id", + "ignore_missing": False}) def test_member_delete_ignore(self): - self._verify2("openstack.proxy.Proxy._delete", - self.proxy.remove_member, - method_args=["member_id"], - method_kwargs={"image": "image_id"}, - expected_args=[member.Member], - expected_kwargs={"member_id": "member_id", - "image_id": "image_id", - "ignore_missing": True}) + self._verify( + "openstack.proxy.Proxy._delete", + self.proxy.remove_member, + method_args=["member_id"], + method_kwargs={"image": "image_id"}, + expected_args=[member.Member], + expected_kwargs={ + "member_id": "member_id", + "image_id": "image_id", + "ignore_missing": True}) def test_member_update(self): - self._verify2("openstack.proxy.Proxy._update", - self.proxy.update_member, - method_args=['member_id', 'image_id'], - expected_args=[member.Member], - expected_kwargs={'member_id': 'member_id', - 'image_id': 'image_id'}) + self._verify( + "openstack.proxy.Proxy._update", + self.proxy.update_member, + method_args=['member_id', 'image_id'], + expected_args=[member.Member], + expected_kwargs={'member_id': 'member_id', 'image_id': 'image_id'}) def test_member_get(self): - self._verify2("openstack.proxy.Proxy._get", - self.proxy.get_member, - method_args=['member_id'], - method_kwargs={"image": "image_id"}, - expected_args=[member.Member], - expected_kwargs={'member_id': 'member_id', - 'image_id': 'image_id'}) + self._verify( + "openstack.proxy.Proxy._get", + self.proxy.get_member, + method_args=['member_id'], + method_kwargs={"image": "image_id"}, + expected_args=[member.Member], + expected_kwargs={'member_id': 'member_id', 'image_id': 'image_id'}) def test_member_find(self): - self._verify2("openstack.proxy.Proxy._find", - self.proxy.find_member, - method_args=['member_id'], - method_kwargs={"image": "image_id"}, - expected_args=[member.Member, "member_id"], - expected_kwargs={'ignore_missing': True, - 'image_id': 'image_id'}) + self._verify( + "openstack.proxy.Proxy._find", + self.proxy.find_member, + method_args=['member_id'], + method_kwargs={"image": "image_id"}, + expected_args=[member.Member, "member_id"], + expected_kwargs={'ignore_missing': True, 'image_id': 'image_id'}) def test_members(self): self.verify_list(self.proxy.members, member.Member, @@ -379,32 +382,36 @@ def test_members(self): expected_kwargs={'image_id': 'image_1'}) def test_images_schema_get(self): - self._verify2("openstack.proxy.Proxy._get", - self.proxy.get_images_schema, - expected_args=[schema.Schema], - expected_kwargs={'base_path': '/schemas/images', - 'requires_id': False}) + self._verify( + "openstack.proxy.Proxy._get", + self.proxy.get_images_schema, + expected_args=[schema.Schema], + expected_kwargs={ + 'base_path': '/schemas/images', 'requires_id': False}) def test_image_schema_get(self): - self._verify2("openstack.proxy.Proxy._get", - self.proxy.get_image_schema, - expected_args=[schema.Schema], - expected_kwargs={'base_path': '/schemas/image', - 'requires_id': False}) + self._verify( + "openstack.proxy.Proxy._get", + self.proxy.get_image_schema, + expected_args=[schema.Schema], + expected_kwargs={ + 'base_path': '/schemas/image', 'requires_id': False}) def test_members_schema_get(self): - self._verify2("openstack.proxy.Proxy._get", - self.proxy.get_members_schema, - expected_args=[schema.Schema], - expected_kwargs={'base_path': '/schemas/members', - 'requires_id': False}) + self._verify( + "openstack.proxy.Proxy._get", + self.proxy.get_members_schema, + expected_args=[schema.Schema], + expected_kwargs={ + 'base_path': '/schemas/members', 'requires_id': False}) def test_member_schema_get(self): - self._verify2("openstack.proxy.Proxy._get", - self.proxy.get_member_schema, - expected_args=[schema.Schema], - expected_kwargs={'base_path': '/schemas/member', - 'requires_id': False}) + self._verify( + "openstack.proxy.Proxy._get", + self.proxy.get_member_schema, + expected_args=[schema.Schema], + expected_kwargs={ + 'base_path': '/schemas/member', 'requires_id': False}) def test_task_get(self): self.verify_get(self.proxy.get_task, task.Task) @@ -488,26 +495,29 @@ def test_wait_for_task_wait(self): self.assertEqual('success', result.status) def test_tasks_schema_get(self): - self._verify2("openstack.proxy.Proxy._get", - self.proxy.get_tasks_schema, - expected_args=[schema.Schema], - expected_kwargs={'base_path': '/schemas/tasks', - 'requires_id': False}) + self._verify( + "openstack.proxy.Proxy._get", + self.proxy.get_tasks_schema, + expected_args=[schema.Schema], + expected_kwargs={ + 'base_path': '/schemas/tasks', 'requires_id': False}) def test_task_schema_get(self): - self._verify2("openstack.proxy.Proxy._get", - self.proxy.get_task_schema, - expected_args=[schema.Schema], - expected_kwargs={'base_path': '/schemas/task', - 'requires_id': False}) + self._verify( + "openstack.proxy.Proxy._get", + self.proxy.get_task_schema, + expected_args=[schema.Schema], + expected_kwargs={ + 'base_path': '/schemas/task', 'requires_id': False}) def test_stores(self): self.verify_list(self.proxy.stores, si.Store) def test_import_info(self): - self._verify2("openstack.proxy.Proxy._get", - self.proxy.get_import_info, - method_args=[], - method_kwargs={}, - expected_args=[si.Import], - expected_kwargs={'require_id': False}) + self._verify( + "openstack.proxy.Proxy._get", + self.proxy.get_import_info, + method_args=[], + method_kwargs={}, + expected_args=[si.Import], + expected_kwargs={'require_id': False}) diff --git a/openstack/tests/unit/load_balancer/v2/test_proxy.py b/openstack/tests/unit/load_balancer/v2/test_proxy.py index 0983845aa..c045826e9 100644 --- a/openstack/tests/unit/load_balancer/v2/test_proxy.py +++ b/openstack/tests/unit/load_balancer/v2/test_proxy.py @@ -70,13 +70,12 @@ def test_load_balancer_delete_non_cascade(self, mock_get_resource): fake_load_balancer = mock.Mock() fake_load_balancer.id = "load_balancer_id" mock_get_resource.return_value = fake_load_balancer - self._verify2("openstack.proxy.Proxy._delete", - self.proxy.delete_load_balancer, - method_args=["resource_or_id", True, - False], - expected_args=[lb.LoadBalancer, - fake_load_balancer], - expected_kwargs={"ignore_missing": True}) + self._verify( + "openstack.proxy.Proxy._delete", + self.proxy.delete_load_balancer, + method_args=["resource_or_id", True, False], + expected_args=[lb.LoadBalancer, fake_load_balancer], + expected_kwargs={"ignore_missing": True}) self.assertFalse(fake_load_balancer.cascade) mock_get_resource.assert_called_once_with(lb.LoadBalancer, "resource_or_id") @@ -86,13 +85,12 @@ def test_load_balancer_delete_cascade(self, mock_get_resource): fake_load_balancer = mock.Mock() fake_load_balancer.id = "load_balancer_id" mock_get_resource.return_value = fake_load_balancer - self._verify2("openstack.proxy.Proxy._delete", - self.proxy.delete_load_balancer, - method_args=["resource_or_id", True, - True], - expected_args=[lb.LoadBalancer, - fake_load_balancer], - expected_kwargs={"ignore_missing": True}) + self._verify( + "openstack.proxy.Proxy._delete", + self.proxy.delete_load_balancer, + method_args=["resource_or_id", True, True], + expected_args=[lb.LoadBalancer, fake_load_balancer], + expected_kwargs={"ignore_missing": True}) self.assertTrue(fake_load_balancer.cascade) mock_get_resource.assert_called_once_with(lb.LoadBalancer, "resource_or_id") @@ -196,19 +194,20 @@ def test_member_delete(self): 'ignore_missing': True}) def test_member_find(self): - self._verify2('openstack.proxy.Proxy._find', - self.proxy.find_member, - method_args=["MEMBER", self.POOL_ID], - expected_args=[member.Member, "MEMBER"], - expected_kwargs={"pool_id": self.POOL_ID, - "ignore_missing": True}) + self._verify( + 'openstack.proxy.Proxy._find', + self.proxy.find_member, + method_args=["MEMBER", self.POOL_ID], + expected_args=[member.Member, "MEMBER"], + expected_kwargs={"pool_id": self.POOL_ID, "ignore_missing": True}) def test_member_update(self): - self._verify2('openstack.proxy.Proxy._update', - self.proxy.update_member, - method_args=["MEMBER", self.POOL_ID], - expected_args=[member.Member, "MEMBER"], - expected_kwargs={"pool_id": self.POOL_ID}) + self._verify( + 'openstack.proxy.Proxy._update', + self.proxy.update_member, + method_args=["MEMBER", self.POOL_ID], + expected_args=[member.Member, "MEMBER"], + expected_kwargs={"pool_id": self.POOL_ID}) def test_health_monitors(self): self.verify_list(self.proxy.health_monitors, @@ -284,19 +283,21 @@ def test_l7_rule_delete(self): expected_kwargs={'l7policy_id': self.L7_POLICY_ID}) def test_l7_rule_find(self): - self._verify2('openstack.proxy.Proxy._find', - self.proxy.find_l7_rule, - method_args=["RULE", self.L7_POLICY_ID], - expected_args=[l7_rule.L7Rule, "RULE"], - expected_kwargs={"l7policy_id": self.L7_POLICY_ID, - "ignore_missing": True}) + self._verify( + 'openstack.proxy.Proxy._find', + self.proxy.find_l7_rule, + method_args=["RULE", self.L7_POLICY_ID], + expected_args=[l7_rule.L7Rule, "RULE"], + expected_kwargs={ + "l7policy_id": self.L7_POLICY_ID, "ignore_missing": True}) def test_l7_rule_update(self): - self._verify2('openstack.proxy.Proxy._update', - self.proxy.update_l7_rule, - method_args=["RULE", self.L7_POLICY_ID], - expected_args=[l7_rule.L7Rule, "RULE"], - expected_kwargs={"l7policy_id": self.L7_POLICY_ID}) + self._verify( + 'openstack.proxy.Proxy._update', + self.proxy.update_l7_rule, + method_args=["RULE", self.L7_POLICY_ID], + expected_args=[l7_rule.L7Rule, "RULE"], + expected_kwargs={"l7policy_id": self.L7_POLICY_ID}) def test_quotas(self): self.verify_list(self.proxy.quotas, quota.Quota) @@ -308,10 +309,11 @@ def test_quota_update(self): self.verify_update(self.proxy.update_quota, quota.Quota) def test_quota_default_get(self): - self._verify2("openstack.proxy.Proxy._get", - self.proxy.get_quota_default, - expected_args=[quota.QuotaDefault], - expected_kwargs={'requires_id': False}) + self._verify( + "openstack.proxy.Proxy._get", + self.proxy.get_quota_default, + expected_args=[quota.QuotaDefault], + expected_kwargs={'requires_id': False}) def test_quota_delete(self): self.verify_delete(self.proxy.delete_quota, quota.Quota, False) diff --git a/openstack/tests/unit/message/v2/test_proxy.py b/openstack/tests/unit/message/v2/test_proxy.py index 540b91359..fa3baf991 100644 --- a/openstack/tests/unit/message/v2/test_proxy.py +++ b/openstack/tests/unit/message/v2/test_proxy.py @@ -50,7 +50,7 @@ def test_queue_delete_ignore(self): def test_message_post(self, mock_get_resource): message_obj = message.Message(queue_name="test_queue") mock_get_resource.return_value = message_obj - self._verify2( + self._verify( "openstack.message.v2.message.Message.post", self.proxy.post_message, method_args=["test_queue", ["msg1", "msg2"]], @@ -61,10 +61,11 @@ def test_message_post(self, mock_get_resource): @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_message_get(self, mock_get_resource): mock_get_resource.return_value = "resource_or_id" - self._verify2("openstack.proxy.Proxy._get", - self.proxy.get_message, - method_args=["test_queue", "resource_or_id"], - expected_args=[message.Message, "resource_or_id"]) + self._verify( + "openstack.proxy.Proxy._get", + self.proxy.get_message, + method_args=["test_queue", "resource_or_id"], + expected_args=[message.Message, "resource_or_id"]) mock_get_resource.assert_called_once_with(message.Message, "resource_or_id", queue_name="test_queue") @@ -82,13 +83,12 @@ def test_message_delete(self, mock_get_resource): fake_message = mock.Mock() fake_message.id = "message_id" mock_get_resource.return_value = fake_message - self._verify2("openstack.proxy.Proxy._delete", - self.proxy.delete_message, - method_args=["test_queue", "resource_or_id", None, - False], - expected_args=[message.Message, - fake_message], - expected_kwargs={"ignore_missing": False}) + self._verify( + "openstack.proxy.Proxy._delete", + self.proxy.delete_message, + method_args=["test_queue", "resource_or_id", None, False], + expected_args=[message.Message, fake_message], + expected_kwargs={"ignore_missing": False}) self.assertIsNone(fake_message.claim_id) mock_get_resource.assert_called_once_with(message.Message, "resource_or_id", @@ -99,13 +99,12 @@ def test_message_delete_claimed(self, mock_get_resource): fake_message = mock.Mock() fake_message.id = "message_id" mock_get_resource.return_value = fake_message - self._verify2("openstack.proxy.Proxy._delete", - self.proxy.delete_message, - method_args=["test_queue", "resource_or_id", "claim_id", - False], - expected_args=[message.Message, - fake_message], - expected_kwargs={"ignore_missing": False}) + self._verify( + "openstack.proxy.Proxy._delete", + self.proxy.delete_message, + method_args=["test_queue", "resource_or_id", "claim_id", False], + expected_args=[message.Message, fake_message], + expected_kwargs={"ignore_missing": False}) self.assertEqual("claim_id", fake_message.claim_id) mock_get_resource.assert_called_once_with(message.Message, "resource_or_id", @@ -116,20 +115,19 @@ def test_message_delete_ignore(self, mock_get_resource): fake_message = mock.Mock() fake_message.id = "message_id" mock_get_resource.return_value = fake_message - self._verify2("openstack.proxy.Proxy._delete", - self.proxy.delete_message, - method_args=["test_queue", "resource_or_id", None, - True], - expected_args=[message.Message, - fake_message], - expected_kwargs={"ignore_missing": True}) + self._verify( + "openstack.proxy.Proxy._delete", + self.proxy.delete_message, + method_args=["test_queue", "resource_or_id", None, True], + expected_args=[message.Message, fake_message], + expected_kwargs={"ignore_missing": True}) self.assertIsNone(fake_message.claim_id) mock_get_resource.assert_called_once_with(message.Message, "resource_or_id", queue_name="test_queue") def test_subscription_create(self): - self._verify2( + self._verify( "openstack.message.v2.subscription.Subscription.create", self.proxy.create_subscription, method_args=["test_queue"], @@ -139,11 +137,11 @@ def test_subscription_create(self): @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_subscription_get(self, mock_get_resource): mock_get_resource.return_value = "resource_or_id" - self._verify2("openstack.proxy.Proxy._get", - self.proxy.get_subscription, - method_args=["test_queue", "resource_or_id"], - expected_args=[subscription.Subscription, - "resource_or_id"]) + self._verify( + "openstack.proxy.Proxy._get", + self.proxy.get_subscription, + method_args=["test_queue", "resource_or_id"], + expected_args=[subscription.Subscription, "resource_or_id"]) mock_get_resource.assert_called_once_with( subscription.Subscription, "resource_or_id", queue_name="test_queue") @@ -181,7 +179,7 @@ def test_subscription_delete_ignore(self, mock_get_resource): queue_name="test_queue") def test_claim_create(self): - self._verify2( + self._verify( "openstack.message.v2.claim.Claim.create", self.proxy.create_claim, method_args=["test_queue"], @@ -189,25 +187,24 @@ def test_claim_create(self): expected_kwargs={"base_path": None}) def test_claim_get(self): - self._verify2("openstack.proxy.Proxy._get", - self.proxy.get_claim, - method_args=["test_queue", "resource_or_id"], - expected_args=[claim.Claim, - "resource_or_id"], - expected_kwargs={"queue_name": "test_queue"}) + self._verify( + "openstack.proxy.Proxy._get", + self.proxy.get_claim, + method_args=["test_queue", "resource_or_id"], + expected_args=[claim.Claim, "resource_or_id"], + expected_kwargs={"queue_name": "test_queue"}) self.verify_get_overrided( self.proxy, claim.Claim, 'openstack.message.v2.claim.Claim') def test_claim_update(self): - self._verify2("openstack.proxy.Proxy._update", - self.proxy.update_claim, - method_args=["test_queue", "resource_or_id"], - method_kwargs={"k1": "v1"}, - expected_args=[claim.Claim, - "resource_or_id"], - expected_kwargs={"queue_name": "test_queue", - "k1": "v1"}) + self._verify( + "openstack.proxy.Proxy._update", + self.proxy.update_claim, + method_args=["test_queue", "resource_or_id"], + method_kwargs={"k1": "v1"}, + expected_args=[claim.Claim, "resource_or_id"], + expected_kwargs={"queue_name": "test_queue", "k1": "v1"}) def test_claim_delete(self): self.verify_delete(self.proxy.delete_claim, diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 00eddda0e..c8e873f63 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -366,13 +366,13 @@ def test_network_find(self): self.verify_find(self.proxy.find_network, network.Network) def test_network_find_with_filter(self): - self._verify2('openstack.proxy.Proxy._find', - self.proxy.find_network, - method_args=["net1"], - method_kwargs={"project_id": "1"}, - expected_args=[network.Network, "net1"], - expected_kwargs={"project_id": "1", - "ignore_missing": True}) + self._verify( + 'openstack.proxy.Proxy._find', + self.proxy.find_network, + method_args=["net1"], + method_kwargs={"project_id": "1"}, + expected_args=[network.Network, "net1"], + expected_kwargs={"project_id": "1", "ignore_missing": True}) def test_network_get(self): self.verify_get(self.proxy.get_network, network.Network) @@ -469,19 +469,20 @@ def test_pool_member_delete_ignore(self): expected_kwargs={"pool_id": "test_id"}) def test_pool_member_find(self): - self._verify2('openstack.proxy.Proxy._find', - self.proxy.find_pool_member, - method_args=["MEMBER", "POOL"], - expected_args=[pool_member.PoolMember, "MEMBER"], - expected_kwargs={"pool_id": "POOL", - "ignore_missing": True}) + self._verify( + 'openstack.proxy.Proxy._find', + self.proxy.find_pool_member, + method_args=["MEMBER", "POOL"], + expected_args=[pool_member.PoolMember, "MEMBER"], + expected_kwargs={"pool_id": "POOL", "ignore_missing": True}) def test_pool_member_get(self): - self._verify2('openstack.proxy.Proxy._get', - self.proxy.get_pool_member, - method_args=["MEMBER", "POOL"], - expected_args=[pool_member.PoolMember, "MEMBER"], - expected_kwargs={"pool_id": "POOL"}) + self._verify( + 'openstack.proxy.Proxy._get', + self.proxy.get_pool_member, + method_args=["MEMBER", "POOL"], + expected_args=[pool_member.PoolMember, "MEMBER"], + expected_kwargs={"pool_id": "POOL"}) def test_pool_members(self): self.verify_list( @@ -491,11 +492,12 @@ def test_pool_members(self): expected_kwargs={"pool_id": "test_id"}) def test_pool_member_update(self): - self._verify2("openstack.network.v2._proxy.Proxy._update", - self.proxy.update_pool_member, - method_args=["MEMBER", "POOL"], - expected_args=[pool_member.PoolMember, "MEMBER"], - expected_kwargs={"pool_id": "POOL"}) + self._verify( + "openstack.network.v2._proxy.Proxy._update", + self.proxy.update_pool_member, + method_args=["MEMBER", "POOL"], + expected_args=[pool_member.PoolMember, "MEMBER"], + expected_kwargs={"pool_id": "POOL"}) def test_pool_create_attrs(self): self.verify_create(self.proxy.create_pool, pool.Pool) @@ -590,14 +592,15 @@ def test_qos_bandwidth_limit_rule_delete_ignore(self): def test_qos_bandwidth_limit_rule_find(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) - self._verify2('openstack.proxy.Proxy._find', - self.proxy.find_qos_bandwidth_limit_rule, - method_args=['rule_id', policy], - expected_args=[ - qos_bandwidth_limit_rule.QoSBandwidthLimitRule, - 'rule_id'], - expected_kwargs={'ignore_missing': True, - 'qos_policy_id': QOS_POLICY_ID}) + self._verify( + 'openstack.proxy.Proxy._find', + self.proxy.find_qos_bandwidth_limit_rule, + method_args=['rule_id', policy], + expected_args=[ + qos_bandwidth_limit_rule.QoSBandwidthLimitRule, + 'rule_id'], + expected_kwargs={ + 'ignore_missing': True, 'qos_policy_id': QOS_POLICY_ID}) def test_qos_bandwidth_limit_rule_get(self): self.verify_get( @@ -615,15 +618,15 @@ def test_qos_bandwidth_limit_rules(self): def test_qos_bandwidth_limit_rule_update(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) - self._verify2('openstack.network.v2._proxy.Proxy._update', - self.proxy.update_qos_bandwidth_limit_rule, - method_args=['rule_id', policy], - method_kwargs={'foo': 'bar'}, - expected_args=[ - qos_bandwidth_limit_rule.QoSBandwidthLimitRule, - 'rule_id'], - expected_kwargs={'qos_policy_id': QOS_POLICY_ID, - 'foo': 'bar'}) + self._verify( + 'openstack.network.v2._proxy.Proxy._update', + self.proxy.update_qos_bandwidth_limit_rule, + method_args=['rule_id', policy], + method_kwargs={'foo': 'bar'}, + expected_args=[ + qos_bandwidth_limit_rule.QoSBandwidthLimitRule, + 'rule_id'], + expected_kwargs={'qos_policy_id': QOS_POLICY_ID, 'foo': 'bar'}) def test_qos_dscp_marking_rule_create_attrs(self): self.verify_create( @@ -652,13 +655,14 @@ def test_qos_dscp_marking_rule_delete_ignore(self): def test_qos_dscp_marking_rule_find(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) - self._verify2('openstack.proxy.Proxy._find', - self.proxy.find_qos_dscp_marking_rule, - method_args=['rule_id', policy], - expected_args=[qos_dscp_marking_rule.QoSDSCPMarkingRule, - 'rule_id'], - expected_kwargs={'ignore_missing': True, - 'qos_policy_id': QOS_POLICY_ID}) + self._verify( + 'openstack.proxy.Proxy._find', + self.proxy.find_qos_dscp_marking_rule, + method_args=['rule_id', policy], + expected_args=[ + qos_dscp_marking_rule.QoSDSCPMarkingRule, 'rule_id'], + expected_kwargs={ + 'ignore_missing': True, 'qos_policy_id': QOS_POLICY_ID}) def test_qos_dscp_marking_rule_get(self): self.verify_get( @@ -676,15 +680,15 @@ def test_qos_dscp_marking_rules(self): def test_qos_dscp_marking_rule_update(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) - self._verify2('openstack.network.v2._proxy.Proxy._update', - self.proxy.update_qos_dscp_marking_rule, - method_args=['rule_id', policy], - method_kwargs={'foo': 'bar'}, - expected_args=[ - qos_dscp_marking_rule.QoSDSCPMarkingRule, - 'rule_id'], - expected_kwargs={'qos_policy_id': QOS_POLICY_ID, - 'foo': 'bar'}) + self._verify( + 'openstack.network.v2._proxy.Proxy._update', + self.proxy.update_qos_dscp_marking_rule, + method_args=['rule_id', policy], + method_kwargs={'foo': 'bar'}, + expected_args=[ + qos_dscp_marking_rule.QoSDSCPMarkingRule, + 'rule_id'], + expected_kwargs={'qos_policy_id': QOS_POLICY_ID, 'foo': 'bar'}) def test_qos_minimum_bandwidth_rule_create_attrs(self): self.verify_create( @@ -713,14 +717,15 @@ def test_qos_minimum_bandwidth_rule_delete_ignore(self): def test_qos_minimum_bandwidth_rule_find(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) - self._verify2('openstack.proxy.Proxy._find', - self.proxy.find_qos_minimum_bandwidth_rule, - method_args=['rule_id', policy], - expected_args=[ - qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, - 'rule_id'], - expected_kwargs={'ignore_missing': True, - 'qos_policy_id': QOS_POLICY_ID}) + self._verify( + 'openstack.proxy.Proxy._find', + self.proxy.find_qos_minimum_bandwidth_rule, + method_args=['rule_id', policy], + expected_args=[ + qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, + 'rule_id'], + expected_kwargs={ + 'ignore_missing': True, 'qos_policy_id': QOS_POLICY_ID}) def test_qos_minimum_bandwidth_rule_get(self): self.verify_get( @@ -738,15 +743,16 @@ def test_qos_minimum_bandwidth_rules(self): def test_qos_minimum_bandwidth_rule_update(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) - self._verify2('openstack.network.v2._proxy.Proxy._update', - self.proxy.update_qos_minimum_bandwidth_rule, - method_args=['rule_id', policy], - method_kwargs={'foo': 'bar'}, - expected_args=[ - qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, - 'rule_id'], - expected_kwargs={'qos_policy_id': QOS_POLICY_ID, - 'foo': 'bar'}) + self._verify( + 'openstack.network.v2._proxy.Proxy._update', + self.proxy.update_qos_minimum_bandwidth_rule, + method_args=['rule_id', policy], + method_kwargs={'foo': 'bar'}, + expected_args=[ + qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, + 'rule_id'], + expected_kwargs={ + 'qos_policy_id': QOS_POLICY_ID, 'foo': 'bar'}) def test_qos_policy_create_attrs(self): self.verify_create(self.proxy.create_qos_policy, qos_policy.QoSPolicy) @@ -795,25 +801,27 @@ def test_quota_get(self): def test_quota_get_details(self, mock_get): fake_quota = mock.Mock(project_id='PROJECT') mock_get.return_value = fake_quota - self._verify2("openstack.proxy.Proxy._get", - self.proxy.get_quota, - method_args=['QUOTA_ID'], - method_kwargs={'details': True}, - expected_args=[quota.QuotaDetails], - expected_kwargs={'project': fake_quota.id, - 'requires_id': False}) + self._verify( + "openstack.proxy.Proxy._get", + self.proxy.get_quota, + method_args=['QUOTA_ID'], + method_kwargs={'details': True}, + expected_args=[quota.QuotaDetails], + expected_kwargs={ + 'project': fake_quota.id, 'requires_id': False}) mock_get.assert_called_once_with(quota.Quota, 'QUOTA_ID') @mock.patch.object(proxy_base.Proxy, "_get_resource") def test_quota_default_get(self, mock_get): fake_quota = mock.Mock(project_id='PROJECT') mock_get.return_value = fake_quota - self._verify2("openstack.proxy.Proxy._get", - self.proxy.get_quota_default, - method_args=['QUOTA_ID'], - expected_args=[quota.QuotaDefault], - expected_kwargs={'project': fake_quota.id, - 'requires_id': False}) + self._verify( + "openstack.proxy.Proxy._get", + self.proxy.get_quota_default, + method_args=['QUOTA_ID'], + expected_args=[quota.QuotaDefault], + expected_kwargs={ + 'project': fake_quota.id, 'requires_id': False}) mock_get.assert_called_once_with(quota.Quota, 'QUOTA_ID') def test_quotas(self): @@ -891,7 +899,7 @@ def test_add_interface_to_router_with_port(self, mock_add_interface, x_router = router.Router.new(id="ROUTER_ID") mock_get.return_value = x_router - self._verify2( + self._verify( "openstack.network.v2.router.Router.add_interface", self.proxy.add_interface_to_router, method_args=["FAKE_ROUTER"], @@ -907,7 +915,7 @@ def test_add_interface_to_router_with_subnet(self, mock_add_interface, x_router = router.Router.new(id="ROUTER_ID") mock_get.return_value = x_router - self._verify2( + self._verify( "openstack.network.v2.router.Router.add_interface", self.proxy.add_interface_to_router, method_args=["FAKE_ROUTER"], @@ -923,7 +931,7 @@ def test_remove_interface_from_router_with_port(self, mock_remove, x_router = router.Router.new(id="ROUTER_ID") mock_get.return_value = x_router - self._verify2( + self._verify( "openstack.network.v2.router.Router.remove_interface", self.proxy.remove_interface_from_router, method_args=["FAKE_ROUTER"], @@ -939,7 +947,7 @@ def test_remove_interface_from_router_with_subnet(self, mock_remove, x_router = router.Router.new(id="ROUTER_ID") mock_get.return_value = x_router - self._verify2( + self._verify( "openstack.network.v2.router.Router.remove_interface", self.proxy.remove_interface_from_router, method_args=["FAKE_ROUTER"], @@ -955,7 +963,7 @@ def test_add_extra_routes_to_router( x_router = router.Router.new(id="ROUTER_ID") mock_get.return_value = x_router - self._verify2( + self._verify( "openstack.network.v2.router.Router.add_extra_routes", self.proxy.add_extra_routes_to_router, method_args=["FAKE_ROUTER"], @@ -971,7 +979,7 @@ def test_remove_extra_routes_from_router( x_router = router.Router.new(id="ROUTER_ID") mock_get.return_value = x_router - self._verify2( + self._verify( "openstack.network.v2.router.Router.remove_extra_routes", self.proxy.remove_extra_routes_from_router, method_args=["FAKE_ROUTER"], @@ -986,7 +994,7 @@ def test_add_gateway_to_router(self, mock_add, mock_get): x_router = router.Router.new(id="ROUTER_ID") mock_get.return_value = x_router - self._verify2( + self._verify( "openstack.network.v2.router.Router.add_gateway", self.proxy.add_gateway_to_router, method_args=["FAKE_ROUTER"], @@ -1001,7 +1009,7 @@ def test_remove_gateway_from_router(self, mock_remove, mock_get): x_router = router.Router.new(id="ROUTER_ID") mock_get.return_value = x_router - self._verify2( + self._verify( "openstack.network.v2.router.Router.remove_gateway", self.proxy.remove_gateway_from_router, method_args=["FAKE_ROUTER"], @@ -1357,7 +1365,7 @@ def test_validate_topology(self): def test_set_tags(self): x_network = network.Network.new(id='NETWORK_ID') - self._verify2( + self._verify( 'openstack.network.v2.network.Network.set_tags', self.proxy.set_tags, method_args=[x_network, ['TAG1', 'TAG2']], @@ -1398,24 +1406,26 @@ def test_delete_floating_ip_port_forwarding_ignore(self): def test_find_floating_ip_port_forwarding(self): fip = floating_ip.FloatingIP.new(id=FIP_ID) - self._verify2('openstack.proxy.Proxy._find', - self.proxy.find_floating_ip_port_forwarding, - method_args=[fip, 'port_forwarding_id'], - expected_args=[ - port_forwarding.PortForwarding, - 'port_forwarding_id'], - expected_kwargs={'ignore_missing': True, - 'floatingip_id': FIP_ID}) + self._verify( + 'openstack.proxy.Proxy._find', + self.proxy.find_floating_ip_port_forwarding, + method_args=[fip, 'port_forwarding_id'], + expected_args=[ + port_forwarding.PortForwarding, + 'port_forwarding_id'], + expected_kwargs={ + 'ignore_missing': True, 'floatingip_id': FIP_ID}) def test_get_floating_ip_port_forwarding(self): fip = floating_ip.FloatingIP.new(id=FIP_ID) - self._verify2('openstack.proxy.Proxy._get', - self.proxy.get_floating_ip_port_forwarding, - method_args=[fip, 'port_forwarding_id'], - expected_args=[ - port_forwarding.PortForwarding, - 'port_forwarding_id'], - expected_kwargs={'floatingip_id': FIP_ID}) + self._verify( + 'openstack.proxy.Proxy._get', + self.proxy.get_floating_ip_port_forwarding, + method_args=[fip, 'port_forwarding_id'], + expected_args=[ + port_forwarding.PortForwarding, + 'port_forwarding_id'], + expected_kwargs={'floatingip_id': FIP_ID}) def test_floating_ip_port_forwardings(self): self.verify_list(self.proxy.floating_ip_port_forwardings, @@ -1425,15 +1435,15 @@ def test_floating_ip_port_forwardings(self): def test_update_floating_ip_port_forwarding(self): fip = floating_ip.FloatingIP.new(id=FIP_ID) - self._verify2('openstack.network.v2._proxy.Proxy._update', - self.proxy.update_floating_ip_port_forwarding, - method_args=[fip, 'port_forwarding_id'], - method_kwargs={'foo': 'bar'}, - expected_args=[ - port_forwarding.PortForwarding, - 'port_forwarding_id'], - expected_kwargs={'floatingip_id': FIP_ID, - 'foo': 'bar'}) + self._verify( + 'openstack.network.v2._proxy.Proxy._update', + self.proxy.update_floating_ip_port_forwarding, + method_args=[fip, 'port_forwarding_id'], + method_kwargs={'foo': 'bar'}, + expected_args=[ + port_forwarding.PortForwarding, + 'port_forwarding_id'], + expected_kwargs={'floatingip_id': FIP_ID, 'foo': 'bar'}) def test_create_l3_conntrack_helper(self): self.verify_create(self.proxy.create_conntrack_helper, @@ -1463,13 +1473,14 @@ def test_delete_l3_conntrack_helper_ignore(self): def test_get_l3_conntrack_helper(self): r = router.Router.new(id=ROUTER_ID) - self._verify2('openstack.proxy.Proxy._get', - self.proxy.get_conntrack_helper, - method_args=['conntrack_helper_id', r], - expected_args=[ - l3_conntrack_helper.ConntrackHelper, - 'conntrack_helper_id'], - expected_kwargs={'router_id': ROUTER_ID}) + self._verify( + 'openstack.proxy.Proxy._get', + self.proxy.get_conntrack_helper, + method_args=['conntrack_helper_id', r], + expected_args=[ + l3_conntrack_helper.ConntrackHelper, + 'conntrack_helper_id'], + expected_kwargs={'router_id': ROUTER_ID}) def test_l3_conntrack_helpers(self): self.verify_list(self.proxy.conntrack_helpers, @@ -1480,12 +1491,12 @@ def test_l3_conntrack_helpers(self): def test_update_l3_conntrack_helper(self): r = router.Router.new(id=ROUTER_ID) - self._verify2('openstack.network.v2._proxy.Proxy._update', - self.proxy.update_conntrack_helper, - method_args=['conntrack_helper_id', r], - method_kwargs={'foo': 'bar'}, - expected_args=[ - l3_conntrack_helper.ConntrackHelper, - 'conntrack_helper_id'], - expected_kwargs={'router_id': ROUTER_ID, - 'foo': 'bar'}) + self._verify( + 'openstack.network.v2._proxy.Proxy._update', + self.proxy.update_conntrack_helper, + method_args=['conntrack_helper_id', r], + method_kwargs={'foo': 'bar'}, + expected_args=[ + l3_conntrack_helper.ConntrackHelper, + 'conntrack_helper_id'], + expected_kwargs={'router_id': ROUTER_ID, 'foo': 'bar'}) diff --git a/openstack/tests/unit/object_store/v1/test_proxy.py b/openstack/tests/unit/object_store/v1/test_proxy.py index e24c11542..81cf1f19c 100644 --- a/openstack/tests/unit/object_store/v1/test_proxy.py +++ b/openstack/tests/unit/object_store/v1/test_proxy.py @@ -64,12 +64,13 @@ def test_container_create_attrs(self): expected_kwargs={'name': 'container_name', "x": 1, "y": 2, "z": 3}) def test_object_metadata_get(self): - self._verify2("openstack.proxy.Proxy._head", - self.proxy.get_object_metadata, - method_args=['object'], - method_kwargs={'container': 'container'}, - expected_args=[obj.Object, 'object'], - expected_kwargs={'container': 'container'}) + self._verify( + "openstack.proxy.Proxy._head", + self.proxy.get_object_metadata, + method_args=['object'], + method_kwargs={'container': 'container'}, + expected_args=[obj.Object, 'object'], + expected_kwargs={'container': 'container'}) def _test_object_delete(self, ignore): expected_kwargs = { @@ -77,12 +78,13 @@ def _test_object_delete(self, ignore): "container": "name", } - self._verify2("openstack.proxy.Proxy._delete", - self.proxy.delete_object, - method_args=["resource"], - method_kwargs=expected_kwargs, - expected_args=[obj.Object, "resource"], - expected_kwargs=expected_kwargs) + self._verify( + "openstack.proxy.Proxy._delete", + self.proxy.delete_object, + method_args=["resource"], + method_kwargs=expected_kwargs, + expected_args=[obj.Object, "resource"], + expected_kwargs=expected_kwargs) def test_object_delete(self): self._test_object_delete(False) @@ -93,11 +95,12 @@ def test_object_delete_ignore(self): def test_object_create_attrs(self): kwargs = {"name": "test", "data": "data", "container": "name"} - self._verify2("openstack.proxy.Proxy._create", - self.proxy.upload_object, - method_kwargs=kwargs, - expected_args=[obj.Object], - expected_kwargs=kwargs) + self._verify( + "openstack.proxy.Proxy._create", + self.proxy.upload_object, + method_kwargs=kwargs, + expected_args=[obj.Object], + expected_kwargs=kwargs) def test_object_create_no_container(self): self.assertRaises(TypeError, self.proxy.upload_object) diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index 32239e024..8c4943e87 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -51,23 +51,25 @@ def test_find_stack(self): # 'ignore_missing': False # } # method_args=["name_or_id"] - # self._verify2(mock_method, test_method, - # method_args=method_args, - # method_kwargs=method_kwargs, - # expected_args=[stack.Stack, "name_or_id"], - # expected_kwargs=method_kwargs, - # expected_result="result") + # self._verify( + # mock_method, test_method, + # method_args=method_args, + # method_kwargs=method_kwargs, + # expected_args=[stack.Stack, "name_or_id"], + # expected_kwargs=method_kwargs, + # expected_result="result") # # method_kwargs = { # 'resolve_outputs': True, # 'ignore_missing': True # } - # self._verify2(mock_method, test_method, - # method_args=method_args, - # method_kwargs=method_kwargs, - # expected_args=[stack.Stack, "name_or_id"], - # expected_kwargs=method_kwargs, - # expected_result="result") + # self._verify( + # mock_method, test_method, + # method_args=method_args, + # method_kwargs=method_kwargs, + # expected_args=[stack.Stack, "name_or_id"], + # expected_kwargs=method_kwargs, + # expected_result="result") def test_stacks(self): self.verify_list(self.proxy.stacks, stack.Stack) @@ -81,27 +83,30 @@ def test_get_stack(self): 'openstack.orchestration.v1.stack.Stack') def test_update_stack(self): - self._verify2('openstack.orchestration.v1.stack.Stack.update', - self.proxy.update_stack, - expected_result='result', - method_args=['stack'], - method_kwargs={'preview': False}, - expected_args=[self.proxy, False]) + self._verify( + 'openstack.orchestration.v1.stack.Stack.update', + self.proxy.update_stack, + expected_result='result', + method_args=['stack'], + method_kwargs={'preview': False}, + expected_args=[self.proxy, False]) def test_update_stack_preview(self): - self._verify2('openstack.orchestration.v1.stack.Stack.update', - self.proxy.update_stack, - expected_result='result', - method_args=['stack'], - method_kwargs={'preview': True}, - expected_args=[self.proxy, True]) + self._verify( + 'openstack.orchestration.v1.stack.Stack.update', + self.proxy.update_stack, + expected_result='result', + method_args=['stack'], + method_kwargs={'preview': True}, + expected_args=[self.proxy, True]) def test_abandon_stack(self): - self._verify2('openstack.orchestration.v1.stack.Stack.abandon', - self.proxy.abandon_stack, - expected_result='result', - method_args=['stack'], - expected_args=[self.proxy]) + self._verify( + 'openstack.orchestration.v1.stack.Stack.abandon', + self.proxy.abandon_stack, + expected_result='result', + method_args=['stack'], + expected_args=[self.proxy]) def test_delete_stack(self): self.verify_delete(self.proxy.delete_stack, stack.Stack, False) @@ -136,13 +141,15 @@ def test_get_stack_environment_with_stack_identity(self, mock_find): stk = stack.Stack(id=stack_id, name=stack_name) mock_find.return_value = stk - self._verify2('openstack.proxy.Proxy._get', - self.proxy.get_stack_environment, - method_args=['IDENTITY'], - expected_args=[stack_environment.StackEnvironment], - expected_kwargs={'requires_id': False, - 'stack_name': stack_name, - 'stack_id': stack_id}) + self._verify( + 'openstack.proxy.Proxy._get', + self.proxy.get_stack_environment, + method_args=['IDENTITY'], + expected_args=[stack_environment.StackEnvironment], + expected_kwargs={ + 'requires_id': False, + 'stack_name': stack_name, + 'stack_id': stack_id}) mock_find.assert_called_once_with(mock.ANY, 'IDENTITY', ignore_missing=False) @@ -151,13 +158,15 @@ def test_get_stack_environment_with_stack_object(self): stack_name = 'test_stack' stk = stack.Stack(id=stack_id, name=stack_name) - self._verify2('openstack.proxy.Proxy._get', - self.proxy.get_stack_environment, - method_args=[stk], - expected_args=[stack_environment.StackEnvironment], - expected_kwargs={'requires_id': False, - 'stack_name': stack_name, - 'stack_id': stack_id}) + self._verify( + 'openstack.proxy.Proxy._get', + self.proxy.get_stack_environment, + method_args=[stk], + expected_args=[stack_environment.StackEnvironment], + expected_kwargs={ + 'requires_id': False, + 'stack_name': stack_name, + 'stack_id': stack_id}) @mock.patch.object(stack_files.StackFiles, 'fetch') @mock.patch.object(stack.Stack, 'find') @@ -194,13 +203,15 @@ def test_get_stack_template_with_stack_identity(self, mock_find): stk = stack.Stack(id=stack_id, name=stack_name) mock_find.return_value = stk - self._verify2('openstack.proxy.Proxy._get', - self.proxy.get_stack_template, - method_args=['IDENTITY'], - expected_args=[stack_template.StackTemplate], - expected_kwargs={'requires_id': False, - 'stack_name': stack_name, - 'stack_id': stack_id}) + self._verify( + 'openstack.proxy.Proxy._get', + self.proxy.get_stack_template, + method_args=['IDENTITY'], + expected_args=[stack_template.StackTemplate], + expected_kwargs={ + 'requires_id': False, + 'stack_name': stack_name, + 'stack_id': stack_id}) mock_find.assert_called_once_with(mock.ANY, 'IDENTITY', ignore_missing=False) @@ -209,13 +220,15 @@ def test_get_stack_template_with_stack_object(self): stack_name = 'test_stack' stk = stack.Stack(id=stack_id, name=stack_name) - self._verify2('openstack.proxy.Proxy._get', - self.proxy.get_stack_template, - method_args=[stk], - expected_args=[stack_template.StackTemplate], - expected_kwargs={'requires_id': False, - 'stack_name': stack_name, - 'stack_id': stack_id}) + self._verify( + 'openstack.proxy.Proxy._get', + self.proxy.get_stack_template, + method_args=[stk], + expected_args=[stack_template.StackTemplate], + expected_kwargs={ + 'requires_id': False, + 'stack_name': stack_name, + 'stack_id': stack_id}) @mock.patch.object(stack.Stack, 'find') def test_resources_with_stack_object(self, mock_find): diff --git a/openstack/tests/unit/test_proxy_base.py b/openstack/tests/unit/test_proxy_base.py index b36fa4770..69a843807 100644 --- a/openstack/tests/unit/test_proxy_base.py +++ b/openstack/tests/unit/test_proxy_base.py @@ -20,39 +20,11 @@ def setUp(self): super(TestProxyBase, self).setUp() self.session = mock.Mock() - def _verify(self, mock_method, test_method, - method_args=None, method_kwargs=None, - expected_args=None, expected_kwargs=None, - expected_result=None): - with mock.patch(mock_method) as mocked: - mocked.return_value = expected_result - if any([method_args, method_kwargs, - expected_args, expected_kwargs]): - method_args = method_args or () - method_kwargs = method_kwargs or {} - expected_args = expected_args or () - expected_kwargs = expected_kwargs or {} - - self.assertEqual(expected_result, test_method(*method_args, - **method_kwargs)) - mocked.assert_called_with(test_method.__self__, - *expected_args, **expected_kwargs) - else: - self.assertEqual(expected_result, test_method()) - mocked.assert_called_with(test_method.__self__) - - # NOTE(briancurtin): This is a duplicate version of _verify that is - # temporarily here while we shift APIs. The difference is that - # calls from the Proxy classes aren't going to be going directly into - # the Resource layer anymore, so they don't pass in the session which - # was tested in assert_called_with. - # This is being done in lieu of adding logic and complicating - # the _verify method. It will be removed once there is one API to - # be verifying. - def _verify2(self, mock_method, test_method, - method_args=None, method_kwargs=None, method_result=None, - expected_args=None, expected_kwargs=None, - expected_result=None): + def _verify( + self, mock_method, test_method, *, + method_args=None, method_kwargs=None, method_result=None, + expected_args=None, expected_kwargs=None, expected_result=None, + ): with mock.patch(mock_method) as mocked: mocked.return_value = expected_result if any([ @@ -120,7 +92,7 @@ def verify_create( expected_kwargs = method_kwargs.copy() expected_kwargs["base_path"] = base_path - self._verify2( + self._verify( mock_method, test_method, method_args=method_args, @@ -142,7 +114,7 @@ def verify_delete( expected_args = expected_args or method_args.copy() expected_kwargs = expected_kwargs or method_kwargs.copy() - self._verify2( + self._verify( mock_method, test_method, method_args=method_args, @@ -166,7 +138,7 @@ def verify_get( if expected_kwargs is None: expected_kwargs = method_kwargs.copy() - self._verify2( + self._verify( mock_method, test_method, method_args=method_args, @@ -197,7 +169,7 @@ def verify_head( expected_args = expected_args or method_args.copy() expected_kwargs = expected_kwargs or method_kwargs.copy() - self._verify2( + self._verify( mock_method, test_method, method_args=method_args, @@ -219,7 +191,7 @@ def verify_find( expected_args = expected_args or method_args.copy() expected_kwargs = expected_kwargs or method_kwargs.copy() - self._verify2( + self._verify( mock_method, test_method, method_args=method_args, @@ -247,7 +219,7 @@ def verify_list( if base_path is not None: expected_kwargs["base_path"] = base_path - self._verify2( + self._verify( mock_method, test_method, method_args=method_args, @@ -272,7 +244,7 @@ def verify_update( if expected_kwargs is None: expected_kwargs = method_kwargs.copy() - self._verify2( + self._verify( mock_method, test_method, method_args=method_args, @@ -286,4 +258,4 @@ def verify_wait_for_status( mock_method="openstack.resource.wait_for_status", **kwargs, ): - self._verify2(mock_method, test_method, **kwargs) + self._verify(mock_method, test_method, **kwargs) From 30600da911cbbc25c00642cfbd731613a96ad890 Mon Sep 17 00:00:00 2001 From: Riccardo Pittau Date: Fri, 21 May 2021 11:47:02 +0200 Subject: [PATCH 2851/3836] Fix import order in accelerator and config unit tests Remove tox filters Change-Id: I6377230915d4c6d85c6015d7c099448f1dbbae41 --- openstack/tests/unit/accelerator/test_version.py | 2 +- .../tests/unit/accelerator/v2/test_accelerator_request.py | 2 +- openstack/tests/unit/accelerator/v2/test_deployable.py | 3 ++- openstack/tests/unit/accelerator/v2/test_device.py | 3 ++- openstack/tests/unit/accelerator/v2/test_device_profile.py | 3 +-- openstack/tests/unit/accelerator/v2/test_proxy.py | 2 +- openstack/tests/unit/config/test_environ.py | 3 +-- openstack/tests/unit/config/test_from_conf.py | 2 +- openstack/tests/unit/config/test_from_session.py | 4 ++-- tox.ini | 2 -- 10 files changed, 12 insertions(+), 14 deletions(-) diff --git a/openstack/tests/unit/accelerator/test_version.py b/openstack/tests/unit/accelerator/test_version.py index 43d6378e7..315ffbbf8 100644 --- a/openstack/tests/unit/accelerator/test_version.py +++ b/openstack/tests/unit/accelerator/test_version.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.accelerator import version from openstack.tests.unit import base -from openstack.accelerator import version IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/accelerator/v2/test_accelerator_request.py b/openstack/tests/unit/accelerator/v2/test_accelerator_request.py index 8b36a717f..ad03131ac 100644 --- a/openstack/tests/unit/accelerator/v2/test_accelerator_request.py +++ b/openstack/tests/unit/accelerator/v2/test_accelerator_request.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.accelerator.v2 import accelerator_request as arq from openstack.tests.unit import base -from openstack.accelerator.v2 import accelerator_request as arq FAKE_ID = '0725b527-e51a-41df-ad22-adad5f4546ad' FAKE_RP_UUID = 'f4b7fe6c-8ab4-4914-a113-547af022935b' diff --git a/openstack/tests/unit/accelerator/v2/test_deployable.py b/openstack/tests/unit/accelerator/v2/test_deployable.py index dcb0af554..0bd8061ab 100644 --- a/openstack/tests/unit/accelerator/v2/test_deployable.py +++ b/openstack/tests/unit/accelerator/v2/test_deployable.py @@ -9,11 +9,12 @@ # 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 uuid +from openstack.accelerator.v2 import deployable from openstack.tests.unit import base -from openstack.accelerator.v2 import deployable EXAMPLE = { 'uuid': uuid.uuid4(), diff --git a/openstack/tests/unit/accelerator/v2/test_device.py b/openstack/tests/unit/accelerator/v2/test_device.py index 22b17b336..54b72041f 100644 --- a/openstack/tests/unit/accelerator/v2/test_device.py +++ b/openstack/tests/unit/accelerator/v2/test_device.py @@ -9,10 +9,11 @@ # 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 uuid -from openstack.tests.unit import base from openstack.accelerator.v2 import device +from openstack.tests.unit import base EXAMPLE = { 'id': '1', diff --git a/openstack/tests/unit/accelerator/v2/test_device_profile.py b/openstack/tests/unit/accelerator/v2/test_device_profile.py index f1708713b..fcaea7e48 100644 --- a/openstack/tests/unit/accelerator/v2/test_device_profile.py +++ b/openstack/tests/unit/accelerator/v2/test_device_profile.py @@ -10,9 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base - from openstack.accelerator.v2 import device_profile +from openstack.tests.unit import base FAKE = { diff --git a/openstack/tests/unit/accelerator/v2/test_proxy.py b/openstack/tests/unit/accelerator/v2/test_proxy.py index b9fd45867..c9c682d14 100644 --- a/openstack/tests/unit/accelerator/v2/test_proxy.py +++ b/openstack/tests/unit/accelerator/v2/test_proxy.py @@ -11,9 +11,9 @@ # under the License. from openstack.accelerator.v2 import _proxy +from openstack.accelerator.v2 import accelerator_request from openstack.accelerator.v2 import deployable from openstack.accelerator.v2 import device_profile -from openstack.accelerator.v2 import accelerator_request from openstack.tests.unit import test_proxy_base as test_proxy_base diff --git a/openstack/tests/unit/config/test_environ.py b/openstack/tests/unit/config/test_environ.py index 95aa3f643..da02bb562 100644 --- a/openstack/tests/unit/config/test_environ.py +++ b/openstack/tests/unit/config/test_environ.py @@ -12,14 +12,13 @@ # License for the specific language governing permissions and limitations # under the License. +import fixtures from openstack import config from openstack.config import cloud_region from openstack import exceptions from openstack.tests.unit.config import base -import fixtures - class TestEnviron(base.TestCase): diff --git a/openstack/tests/unit/config/test_from_conf.py b/openstack/tests/unit/config/test_from_conf.py index 2f1af17b3..31a75b46d 100644 --- a/openstack/tests/unit/config/test_from_conf.py +++ b/openstack/tests/unit/config/test_from_conf.py @@ -10,10 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -import requests.exceptions import uuid from keystoneauth1 import exceptions as ks_exc +import requests.exceptions from openstack.config import cloud_region from openstack import connection diff --git a/openstack/tests/unit/config/test_from_session.py b/openstack/tests/unit/config/test_from_session.py index e5599d915..0873e296c 100644 --- a/openstack/tests/unit/config/test_from_session.py +++ b/openstack/tests/unit/config/test_from_session.py @@ -11,10 +11,10 @@ # License for the specific language governing permissions and limitations # under the License. -from testscenarios import load_tests_apply_scenarios as load_tests # noqa - import uuid +from testscenarios import load_tests_apply_scenarios as load_tests # noqa + from openstack.config import cloud_region from openstack import connection from openstack.tests import fakes diff --git a/tox.ini b/tox.ini index 8f1818fa6..ad5cafe0e 100644 --- a/tox.ini +++ b/tox.ini @@ -127,8 +127,6 @@ per-file-ignores = openstack/tests/unit/clustering/*:H306,I100,I201,I202 openstack/tests/unit/orchestration/*:H306,I100,I201,I202 openstack/tests/unit/identity/*:H306,I100,I201,I202 - openstack/tests/unit/accelerator/*:H306,I100,I201,I202 - openstack/tests/unit/config/*:H306,I100,I201,I202 [flake8:local-plugins] extension = From c6177a472b2b7ce2a63e50634833eceb9b8fd237 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 20 May 2021 10:18:06 +0200 Subject: [PATCH 2852/3836] Drop cloud layer methods for Senlin Rework of the cloud layer showed this was not working properly and tests even verifying wrong things. Due to the big amount of effort required to fix things and most likely nobody using it (this code is not really usable now) drop it (comment-out). python-senlinclient is using direclty proxy layer (correct), Ansible modules for senlin not existing - no direct negative impact is visible. If anybody is interested in getting it back - the code is there and can be uncommented and fixed. Change-Id: I198eed8454fd8ae2ce8fbd70e3562c9f15db7445 --- openstack/cloud/_clustering.py | 1019 +++++++------- openstack/tests/unit/cloud/test_clustering.py | 1238 +++++++++-------- ...p-senlin-cloud-layer-c06d496acc70b014.yaml | 4 + 3 files changed, 1107 insertions(+), 1154 deletions(-) create mode 100644 releasenotes/notes/drop-senlin-cloud-layer-c06d496acc70b014.yaml diff --git a/openstack/cloud/_clustering.py b/openstack/cloud/_clustering.py index b7aa60b01..05cb0a214 100644 --- a/openstack/cloud/_clustering.py +++ b/openstack/cloud/_clustering.py @@ -16,12 +16,10 @@ import types # noqa from openstack.cloud import _normalize -from openstack.cloud import _utils -from openstack.cloud import exc -from openstack import utils class ClusteringCloudMixin(_normalize.Normalizer): + pass @property def _clustering_client(self): @@ -31,537 +29,484 @@ def _clustering_client(self): self._raw_clients['clustering'] = clustering_client return self._raw_clients['clustering'] - def create_cluster(self, name, profile, config=None, desired_capacity=0, - max_size=None, metadata=None, min_size=None, - timeout=None): - profile = self.get_cluster_profile(profile) - profile_id = profile['id'] - body = { - 'desired_capacity': desired_capacity, - 'name': name, - 'profile_id': profile_id - } - - if config is not None: - body['config'] = config - - if max_size is not None: - body['max_size'] = max_size - - if metadata is not None: - body['metadata'] = metadata - - if min_size is not None: - body['min_size'] = min_size - - if timeout is not None: - body['timeout'] = timeout - - data = self._clustering_client.post( - '/clusters', json={'cluster': body}, - error_message="Error creating cluster {name}".format(name=name)) - - return self._get_and_munchify(key=None, data=data) - - def set_cluster_metadata(self, name_or_id, metadata): - cluster = self.get_cluster(name_or_id) - if not cluster: - raise exc.OpenStackCloudException( - 'Invalid Cluster {cluster}'.format(cluster=name_or_id)) - - self._clustering_client.post( - '/clusters/{cluster_id}/metadata'.format(cluster_id=cluster['id']), - json={'metadata': metadata}, - error_message='Error updating cluster metadata') - - def get_cluster_by_id(self, cluster_id): - try: - data = self._clustering_client.get( - "/clusters/{cluster_id}".format(cluster_id=cluster_id), - error_message="Error fetching cluster {name}".format( - name=cluster_id)) - return self._get_and_munchify('cluster', data) - except Exception: - return None - - def get_cluster(self, name_or_id, filters=None): - return _utils._get_entity( - cloud=self, resource='cluster', - name_or_id=name_or_id, filters=filters) - - def update_cluster(self, name_or_id, new_name=None, - profile_name_or_id=None, config=None, metadata=None, - timeout=None, profile_only=False): - old_cluster = self.get_cluster(name_or_id) - if old_cluster is None: - raise exc.OpenStackCloudException( - 'Invalid Cluster {cluster}'.format(cluster=name_or_id)) - cluster = { - 'profile_only': profile_only - } - - if config is not None: - cluster['config'] = config - - if metadata is not None: - cluster['metadata'] = metadata - - if profile_name_or_id is not None: - profile = self.get_cluster_profile(profile_name_or_id) - if profile is None: - raise exc.OpenStackCloudException( - 'Invalid Cluster Profile {profile}'.format( - profile=profile_name_or_id)) - cluster['profile_id'] = profile.id - - if timeout is not None: - cluster['timeout'] = timeout - - if new_name is not None: - cluster['name'] = new_name - - data = self._clustering_client.patch( - "/clusters/{cluster_id}".format(cluster_id=old_cluster['id']), - json={'cluster': cluster}, - error_message="Error updating cluster " - "{name}".format(name=name_or_id)) - - return self._get_and_munchify(key=None, data=data) - - def delete_cluster(self, name_or_id): - cluster = self.get_cluster(name_or_id) - if cluster is None: - self.log.debug("Cluster %s not found for deleting", name_or_id) - return False - - for policy in self.list_policies_on_cluster(name_or_id): - detach_policy = self.get_cluster_policy_by_id( - policy['policy_id']) - self.detach_policy_from_cluster(cluster, detach_policy) - - for receiver in self.list_cluster_receivers(): - if cluster["id"] == receiver["cluster_id"]: - self.delete_cluster_receiver(receiver["id"], wait=True) - - self._clustering_client.delete( - "/clusters/{cluster_id}".format(cluster_id=name_or_id), - error_message="Error deleting cluster {name}".format( - name=name_or_id)) - - return True - - def search_clusters(self, name_or_id=None, filters=None): - clusters = self.list_clusters() - return _utils._filter_list(clusters, name_or_id, filters) - - def list_clusters(self): - try: - data = self._clustering_client.get( - '/clusters', - error_message="Error fetching clusters") - return self._get_and_munchify('clusters', data) - except exc.OpenStackCloudURINotFound as e: - self.log.debug(str(e), exc_info=True) - return [] - - def attach_policy_to_cluster(self, name_or_id, policy_name_or_id, - is_enabled): - cluster = self.get_cluster(name_or_id) - policy = self.get_cluster_policy(policy_name_or_id) - if cluster is None: - raise exc.OpenStackCloudException( - 'Cluster {cluster} not found for attaching'.format( - cluster=name_or_id)) - - if policy is None: - raise exc.OpenStackCloudException( - 'Policy {policy} not found for attaching'.format( - policy=policy_name_or_id)) - - body = { - 'policy_id': policy['id'], - 'enabled': is_enabled - } - - self._clustering_client.post( - "/clusters/{cluster_id}/actions".format(cluster_id=cluster['id']), - error_message="Error attaching policy {policy} to cluster " - "{cluster}".format( - policy=policy['id'], - cluster=cluster['id']), - json={'policy_attach': body}) - - return True - - def detach_policy_from_cluster( - self, name_or_id, policy_name_or_id, wait=False, timeout=3600): - cluster = self.get_cluster(name_or_id) - policy = self.get_cluster_policy(policy_name_or_id) - if cluster is None: - raise exc.OpenStackCloudException( - 'Cluster {cluster} not found for detaching'.format( - cluster=name_or_id)) - - if policy is None: - raise exc.OpenStackCloudException( - 'Policy {policy} not found for detaching'.format( - policy=policy_name_or_id)) - - body = {'policy_id': policy['id']} - self._clustering_client.post( - "/clusters/{cluster_id}/actions".format(cluster_id=cluster['id']), - error_message="Error detaching policy {policy} from cluster " - "{cluster}".format( - policy=policy['id'], - cluster=cluster['id']), - json={'policy_detach': body}) - - if not wait: - return True - - value = [] - - for count in utils.iterate_timeout( - timeout, "Timeout waiting for cluster policy to detach"): - - # TODO(bjjohnson) This logic will wait until there are no policies. - # Since we're detaching a specific policy, checking to make sure - # that policy is not in the list of policies would be better. - policy_status = self.get_cluster_by_id(cluster['id'])['policies'] - - if policy_status == value: - break - return True - - def update_policy_on_cluster(self, name_or_id, policy_name_or_id, - is_enabled): - cluster = self.get_cluster(name_or_id) - policy = self.get_cluster_policy(policy_name_or_id) - if cluster is None: - raise exc.OpenStackCloudException( - 'Cluster {cluster} not found for updating'.format( - cluster=name_or_id)) - - if policy is None: - raise exc.OpenStackCloudException( - 'Policy {policy} not found for updating'.format( - policy=policy_name_or_id)) - - body = { - 'policy_id': policy['id'], - 'enabled': is_enabled - } - self._clustering_client.post( - "/clusters/{cluster_id}/actions".format(cluster_id=cluster['id']), - error_message="Error updating policy {policy} on cluster " - "{cluster}".format( - policy=policy['id'], - cluster=cluster['id']), - json={'policy_update': body}) - - return True - - def get_policy_on_cluster(self, name_or_id, policy_name_or_id): - try: - policy = self._clustering_client.get( - "/clusters/{cluster_id}/policies/{policy_id}".format( - cluster_id=name_or_id, policy_id=policy_name_or_id), - error_message="Error fetching policy " - "{name}".format(name=policy_name_or_id)) - return self._get_and_munchify('cluster_policy', policy) - except Exception: - return False - - def list_policies_on_cluster(self, name_or_id): - endpoint = "/clusters/{cluster_id}/policies".format( - cluster_id=name_or_id) - try: - data = self._clustering_client.get( - endpoint, - error_message="Error fetching cluster policies") - except exc.OpenStackCloudURINotFound as e: - self.log.debug(str(e), exc_info=True) - return [] - return self._get_and_munchify('cluster_policies', data) - - def create_cluster_profile(self, name, spec, metadata=None): - profile = { - 'name': name, - 'spec': spec - } - - if metadata is not None: - profile['metadata'] = metadata - - data = self._clustering_client.post( - '/profiles', json={'profile': profile}, - error_message="Error creating profile {name}".format(name=name)) - - return self._get_and_munchify('profile', data) - - def set_cluster_profile_metadata(self, name_or_id, metadata): - profile = self.get_cluster_profile(name_or_id) - if not profile: - raise exc.OpenStackCloudException( - 'Invalid Profile {profile}'.format(profile=name_or_id)) - - self._clustering_client.post( - '/profiles/{profile_id}/metadata'.format(profile_id=profile['id']), - json={'metadata': metadata}, - error_message='Error updating profile metadata') - - def search_cluster_profiles(self, name_or_id=None, filters=None): - cluster_profiles = self.list_cluster_profiles() - return _utils._filter_list(cluster_profiles, name_or_id, filters) - - def list_cluster_profiles(self): - try: - data = self._clustering_client.get( - '/profiles', - error_message="Error fetching profiles") - except exc.OpenStackCloudURINotFound as e: - self.log.debug(str(e), exc_info=True) - return [] - return self._get_and_munchify('profiles', data) - - def get_cluster_profile_by_id(self, profile_id): - try: - data = self._clustering_client.get( - "/profiles/{profile_id}".format(profile_id=profile_id), - error_message="Error fetching profile {name}".format( - name=profile_id)) - return self._get_and_munchify('profile', data) - except exc.OpenStackCloudURINotFound as e: - self.log.debug(str(e), exc_info=True) - return None - - def get_cluster_profile(self, name_or_id, filters=None): - return _utils._get_entity(self, 'cluster_profile', name_or_id, filters) - - def delete_cluster_profile(self, name_or_id): - profile = self.get_cluster_profile(name_or_id) - if profile is None: - self.log.debug("Profile %s not found for deleting", name_or_id) - return False - - for cluster in self.list_clusters(): - if (name_or_id, profile.id) in cluster.items(): - self.log.debug( - "Profile %s is being used by cluster %s, won't delete", - name_or_id, cluster.name) - return False - - self._clustering_client.delete( - "/profiles/{profile_id}".format(profile_id=profile['id']), - error_message="Error deleting profile " - "{name}".format(name=name_or_id)) - - return True - - def update_cluster_profile(self, name_or_id, metadata=None, new_name=None): - old_profile = self.get_cluster_profile(name_or_id) - if not old_profile: - raise exc.OpenStackCloudException( - 'Invalid Profile {profile}'.format(profile=name_or_id)) - - profile = {} - - if metadata is not None: - profile['metadata'] = metadata - - if new_name is not None: - profile['name'] = new_name - - data = self._clustering_client.patch( - "/profiles/{profile_id}".format(profile_id=old_profile.id), - json={'profile': profile}, - error_message="Error updating profile {name}".format( - name=name_or_id)) - - return self._get_and_munchify(key=None, data=data) - - def create_cluster_policy(self, name, spec): - policy = { - 'name': name, - 'spec': spec - } - - data = self._clustering_client.post( - '/policies', json={'policy': policy}, - error_message="Error creating policy {name}".format( - name=policy['name'])) - return self._get_and_munchify('policy', data) - - def search_cluster_policies(self, name_or_id=None, filters=None): - cluster_policies = self.list_cluster_policies() - return _utils._filter_list(cluster_policies, name_or_id, filters) - - def list_cluster_policies(self): - endpoint = "/policies" - try: - data = self._clustering_client.get( - endpoint, - error_message="Error fetching cluster policies") - except exc.OpenStackCloudURINotFound as e: - self.log.debug(str(e), exc_info=True) - return [] - return self._get_and_munchify('policies', data) - - def get_cluster_policy_by_id(self, policy_id): - try: - data = self._clustering_client.get( - "/policies/{policy_id}".format(policy_id=policy_id), - error_message="Error fetching policy {name}".format( - name=policy_id)) - return self._get_and_munchify('policy', data) - except exc.OpenStackCloudURINotFound as e: - self.log.debug(str(e), exc_info=True) - return None - - def get_cluster_policy(self, name_or_id, filters=None): - return _utils._get_entity( - self, 'cluster_policie', name_or_id, filters) - - def delete_cluster_policy(self, name_or_id): - policy = self.get_cluster_policy_by_id(name_or_id) - if policy is None: - self.log.debug("Policy %s not found for deleting", name_or_id) - return False - - for cluster in self.list_clusters(): - if (name_or_id, policy.id) in cluster.items(): - self.log.debug( - "Policy %s is being used by cluster %s, won't delete", - name_or_id, cluster.name) - return False - - self._clustering_client.delete( - "/policies/{policy_id}".format(policy_id=name_or_id), - error_message="Error deleting policy " - "{name}".format(name=name_or_id)) - - return True - - def update_cluster_policy(self, name_or_id, new_name): - old_policy = self.get_cluster_policy(name_or_id) - if not old_policy: - raise exc.OpenStackCloudException( - 'Invalid Policy {policy}'.format(policy=name_or_id)) - policy = {'name': new_name} - - data = self._clustering_client.patch( - "/policies/{policy_id}".format(policy_id=old_policy.id), - json={'policy': policy}, - error_message="Error updating policy " - "{name}".format(name=name_or_id)) - return self._get_and_munchify(key=None, data=data) - - def create_cluster_receiver(self, name, receiver_type, - cluster_name_or_id=None, action=None, - actor=None, params=None): - cluster = self.get_cluster(cluster_name_or_id) - if cluster is None: - raise exc.OpenStackCloudException( - 'Invalid cluster {cluster}'.format(cluster=cluster_name_or_id)) - - receiver = { - 'name': name, - 'type': receiver_type - } - - if cluster_name_or_id is not None: - receiver['cluster_id'] = cluster.id - - if action is not None: - receiver['action'] = action - - if actor is not None: - receiver['actor'] = actor - - if params is not None: - receiver['params'] = params - - data = self._clustering_client.post( - '/receivers', json={'receiver': receiver}, - error_message="Error creating receiver {name}".format(name=name)) - return self._get_and_munchify('receiver', data) - - def search_cluster_receivers(self, name_or_id=None, filters=None): - cluster_receivers = self.list_cluster_receivers() - return _utils._filter_list(cluster_receivers, name_or_id, filters) - - def list_cluster_receivers(self): - try: - data = self._clustering_client.get( - '/receivers', - error_message="Error fetching receivers") - except exc.OpenStackCloudURINotFound as e: - self.log.debug(str(e), exc_info=True) - return [] - return self._get_and_munchify('receivers', data) - - def get_cluster_receiver_by_id(self, receiver_id): - try: - data = self._clustering_client.get( - "/receivers/{receiver_id}".format(receiver_id=receiver_id), - error_message="Error fetching receiver {name}".format( - name=receiver_id)) - return self._get_and_munchify('receiver', data) - except exc.OpenStackCloudURINotFound as e: - self.log.debug(str(e), exc_info=True) - return None - - def get_cluster_receiver(self, name_or_id, filters=None): - return _utils._get_entity( - self, 'cluster_receiver', name_or_id, filters) - - def delete_cluster_receiver(self, name_or_id, wait=False, timeout=3600): - receiver = self.get_cluster_receiver(name_or_id) - if receiver is None: - self.log.debug("Receiver %s not found for deleting", name_or_id) - return False - - receiver_id = receiver['id'] - - self._clustering_client.delete( - "/receivers/{receiver_id}".format(receiver_id=receiver_id), - error_message="Error deleting receiver {name}".format( - name=name_or_id)) - - if not wait: - return True - - for count in utils.iterate_timeout( - timeout, "Timeout waiting for cluster receiver to delete"): - - receiver = self.get_cluster_receiver_by_id(receiver_id) - - if not receiver: - break - - return True - - def update_cluster_receiver(self, name_or_id, new_name=None, action=None, - params=None): - old_receiver = self.get_cluster_receiver(name_or_id) - if old_receiver is None: - raise exc.OpenStackCloudException( - 'Invalid receiver {receiver}'.format(receiver=name_or_id)) - - receiver = {} - - if new_name is not None: - receiver['name'] = new_name - - if action is not None: - receiver['action'] = action - - if params is not None: - receiver['params'] = params - - data = self._clustering_client.patch( - "/receivers/{receiver_id}".format(receiver_id=old_receiver.id), - json={'receiver': receiver}, - error_message="Error updating receiver {name}".format( - name=name_or_id)) - return self._get_and_munchify(key=None, data=data) +# NOTE(gtema): work on getting rid of direct API calls showed that this +# implementation never worked properly and tests in reality verifying wrong +# things. Unless someone is really interested in this piece of code this will +# be commented out and apparently dropped completely. This has no impact on the +# proxy layer. + +# def create_cluster(self, name, profile, config=None, desired_capacity=0, +# max_size=None, metadata=None, min_size=None, +# timeout=None): +# profile = self.get_cluster_profile(profile) +# profile_id = profile['id'] +# body = { +# 'desired_capacity': desired_capacity, +# 'name': name, +# 'profile_id': profile_id +# } +# +# if config is not None: +# body['config'] = config +# +# if max_size is not None: +# body['max_size'] = max_size +# +# if metadata is not None: +# body['metadata'] = metadata +# +# if min_size is not None: +# body['min_size'] = min_size +# +# if timeout is not None: +# body['timeout'] = timeout +# +# return self.clustering.create_cluster(**body) +# +# def set_cluster_metadata(self, name_or_id, metadata): +# cluster = self.get_cluster(name_or_id) +# if not cluster: +# raise exc.OpenStackCloudException( +# 'Invalid Cluster {cluster}'.format(cluster=name_or_id)) +# self.clustering.set_cluster_metadata(cluster, **metadata) +# +# def get_cluster_by_id(self, cluster_id): +# try: +# return self.get_cluster(cluster_id) +# except Exception: +# return None +# +# def get_cluster(self, name_or_id, filters=None): +# return _utils._get_entity( +# cloud=self, resource='cluster', +# name_or_id=name_or_id, filters=filters) +# +# def update_cluster(self, name_or_id, new_name=None, +# profile_name_or_id=None, config=None, metadata=None, +# timeout=None, profile_only=False): +# old_cluster = self.get_cluster(name_or_id) +# if old_cluster is None: +# raise exc.OpenStackCloudException( +# 'Invalid Cluster {cluster}'.format(cluster=name_or_id)) +# cluster = { +# 'profile_only': profile_only +# } +# +# if config is not None: +# cluster['config'] = config +# +# if metadata is not None: +# cluster['metadata'] = metadata +# +# if profile_name_or_id is not None: +# profile = self.get_cluster_profile(profile_name_or_id) +# if profile is None: +# raise exc.OpenStackCloudException( +# 'Invalid Cluster Profile {profile}'.format( +# profile=profile_name_or_id)) +# cluster['profile_id'] = profile.id +# +# if timeout is not None: +# cluster['timeout'] = timeout +# +# if new_name is not None: +# cluster['name'] = new_name +# +# return self.update_cluster(old_cluster, cluster) +# +# def delete_cluster(self, name_or_id): +# cluster = self.get_cluster(name_or_id) +# if cluster is None: +# self.log.debug("Cluster %s not found for deleting", name_or_id) +# return False +# +# for policy in self.list_policies_on_cluster(name_or_id): +# detach_policy = self.get_cluster_policy_by_id( +# policy['policy_id']) +# self.detach_policy_from_cluster(cluster, detach_policy) +# +# for receiver in self.list_cluster_receivers(): +# if cluster["id"] == receiver["cluster_id"]: +# self.delete_cluster_receiver(receiver["id"], wait=True) +# +# self.clustering.delete_cluster(cluster) +# +# return True +# +# def search_clusters(self, name_or_id=None, filters=None): +# clusters = self.list_clusters() +# return _utils._filter_list(clusters, name_or_id, filters) +# +# def list_clusters(self): +# return list(self.clustering.clusters()) +# +# def attach_policy_to_cluster( +# self, name_or_id, policy_name_or_id, is_enabled +# ): +# cluster = self.get_cluster(name_or_id) +# policy = self.get_cluster_policy(policy_name_or_id) +# if cluster is None: +# raise exc.OpenStackCloudException( +# 'Cluster {cluster} not found for attaching'.format( +# cluster=name_or_id)) +# +# if policy is None: +# raise exc.OpenStackCloudException( +# 'Policy {policy} not found for attaching'.format( +# policy=policy_name_or_id)) +# +# self.clustering.attach_policy_to_cluster(cluster, policy) +# +# return True +# +# def detach_policy_from_cluster( +# self, name_or_id, policy_name_or_id, wait=False, timeout=3600): +# cluster = self.get_cluster(name_or_id) +# policy = self.get_cluster_policy(policy_name_or_id) +# if cluster is None: +# raise exc.OpenStackCloudException( +# 'Cluster {cluster} not found for detaching'.format( +# cluster=name_or_id)) +# +# if policy is None: +# raise exc.OpenStackCloudException( +# 'Policy {policy} not found for detaching'.format( +# policy=policy_name_or_id)) +# +# self.clustering.detach_policy_from_cluster(cluster, policy) +# +# if not wait: +# return True +# +# for count in utils.iterate_timeout( +# timeout, "Timeout waiting for cluster policy to detach"): +# +# policies = self.get_cluster_by_id(cluster['id']).policies +# +# # NOTE(gtema): we iterate over all current policies and "continue" +# # if selected policy is still there +# is_present = False +# for active_policy in policies: +# if active_policy.id == policy.id: +# is_present = True +# if not is_present: +# break +# return True +# +# def update_policy_on_cluster(self, name_or_id, policy_name_or_id, +# is_enabled): +# cluster = self.get_cluster(name_or_id) +# policy = self.get_cluster_policy(policy_name_or_id) +# if cluster is None: +# raise exc.OpenStackCloudException( +# 'Cluster {cluster} not found for updating'.format( +# cluster=name_or_id)) +# +# if policy is None: +# raise exc.OpenStackCloudException( +# 'Policy {policy} not found for updating'.format( +# policy=policy_name_or_id)) +# +# self.clustering.update_cluster_policy( +# cluster, policy, is_enabled=is_enabled) +# +# return True +# +# def get_policy_on_cluster(self, name_or_id, policy_name_or_id): +# cluster = self.get_cluster(name_or_id) +# policy = self.get_policy(policy_name_or_id) +# if policy and cluster: +# return self.clustering.get_cluster_policy( +# policy, cluster) +# else: +# return False +# +# def list_policies_on_cluster(self, name_or_id): +# cluster = self.get_cluster(name_or_id) +# return list(self.clustering.cluster_policies(cluster)) +# +# def create_cluster_profile(self, name, spec, metadata=None): +# profile = { +# 'name': name, +# 'spec': spec +# } +# +# if metadata is not None: +# profile['metadata'] = metadata +# +# data = self._clustering_client.post( +# '/profiles', json={'profile': profile}, +# error_message="Error creating profile {name}".format(name=name)) +# +# return self._get_and_munchify('profile', data) +# +# def set_cluster_profile_metadata(self, name_or_id, metadata): +# profile = self.get_cluster_profile(name_or_id) +# if not profile: +# raise exc.OpenStackCloudException( +# 'Invalid Profile {profile}'.format(profile=name_or_id)) +# +# self._clustering_client.post( +# '/profiles/{profile_id}/metadata'.format(profile_id=profile['id']), +# json={'metadata': metadata}, +# error_message='Error updating profile metadata') +# +# def search_cluster_profiles(self, name_or_id=None, filters=None): +# cluster_profiles = self.list_cluster_profiles() +# return _utils._filter_list(cluster_profiles, name_or_id, filters) +# +# def list_cluster_profiles(self): +# try: +# data = self._clustering_client.get( +# '/profiles', +# error_message="Error fetching profiles") +# except exc.OpenStackCloudURINotFound as e: +# self.log.debug(str(e), exc_info=True) +# return [] +# return self._get_and_munchify('profiles', data) +# +# def get_cluster_profile_by_id(self, profile_id): +# try: +# data = self._clustering_client.get( +# "/profiles/{profile_id}".format(profile_id=profile_id), +# error_message="Error fetching profile {name}".format( +# name=profile_id)) +# return self._get_and_munchify('profile', data) +# except exc.OpenStackCloudURINotFound as e: +# self.log.debug(str(e), exc_info=True) +# return None +# +# def get_cluster_profile(self, name_or_id, filters=None): +# return _utils._get_entity(self, 'cluster_profile', name_or_id, +# filters) +# +# def delete_cluster_profile(self, name_or_id): +# profile = self.get_cluster_profile(name_or_id) +# if profile is None: +# self.log.debug("Profile %s not found for deleting", name_or_id) +# return False +# +# for cluster in self.list_clusters(): +# if (name_or_id, profile.id) in cluster.items(): +# self.log.debug( +# "Profile %s is being used by cluster %s, won't delete", +# name_or_id, cluster.name) +# return False +# +# self._clustering_client.delete( +# "/profiles/{profile_id}".format(profile_id=profile['id']), +# error_message="Error deleting profile " +# "{name}".format(name=name_or_id)) +# +# return True +# +# def update_cluster_profile( +# self, name_or_id, metadata=None, new_name=None +# ): +# old_profile = self.get_cluster_profile(name_or_id) +# if not old_profile: +# raise exc.OpenStackCloudException( +# 'Invalid Profile {profile}'.format(profile=name_or_id)) +# +# profile = {} +# +# if metadata is not None: +# profile['metadata'] = metadata +# +# if new_name is not None: +# profile['name'] = new_name +# +# data = self._clustering_client.patch( +# "/profiles/{profile_id}".format(profile_id=old_profile.id), +# json={'profile': profile}, +# error_message="Error updating profile {name}".format( +# name=name_or_id)) +# +# return self._get_and_munchify(key=None, data=data) +# +# def create_cluster_policy(self, name, spec): +# policy = { +# 'name': name, +# 'spec': spec +# } +# +# data = self._clustering_client.post( +# '/policies', json={'policy': policy}, +# error_message="Error creating policy {name}".format( +# name=policy['name'])) +# return self._get_and_munchify('policy', data) +# +# def search_cluster_policies(self, name_or_id=None, filters=None): +# cluster_policies = self.list_cluster_policies() +# return _utils._filter_list(cluster_policies, name_or_id, filters) +# +# def list_cluster_policies(self): +# endpoint = "/policies" +# try: +# data = self._clustering_client.get( +# endpoint, +# error_message="Error fetching cluster policies") +# except exc.OpenStackCloudURINotFound as e: +# self.log.debug(str(e), exc_info=True) +# return [] +# return self._get_and_munchify('policies', data) +# +# def get_cluster_policy_by_id(self, policy_id): +# try: +# data = self._clustering_client.get( +# "/policies/{policy_id}".format(policy_id=policy_id), +# error_message="Error fetching policy {name}".format( +# name=policy_id)) +# return self._get_and_munchify('policy', data) +# except exc.OpenStackCloudURINotFound as e: +# self.log.debug(str(e), exc_info=True) +# return None +# +# def get_cluster_policy(self, name_or_id, filters=None): +# return _utils._get_entity( +# self, 'cluster_policie', name_or_id, filters) +# +# def delete_cluster_policy(self, name_or_id): +# policy = self.get_cluster_policy_by_id(name_or_id) +# if policy is None: +# self.log.debug("Policy %s not found for deleting", name_or_id) +# return False +# +# for cluster in self.list_clusters(): +# if (name_or_id, policy.id) in cluster.items(): +# self.log.debug( +# "Policy %s is being used by cluster %s, won't delete", +# name_or_id, cluster.name) +# return False +# +# self._clustering_client.delete( +# "/policies/{policy_id}".format(policy_id=name_or_id), +# error_message="Error deleting policy " +# "{name}".format(name=name_or_id)) +# +# return True +# +# def update_cluster_policy(self, name_or_id, new_name): +# old_policy = self.get_cluster_policy(name_or_id) +# if not old_policy: +# raise exc.OpenStackCloudException( +# 'Invalid Policy {policy}'.format(policy=name_or_id)) +# policy = {'name': new_name} +# +# data = self._clustering_client.patch( +# "/policies/{policy_id}".format(policy_id=old_policy.id), +# json={'policy': policy}, +# error_message="Error updating policy " +# "{name}".format(name=name_or_id)) +# return self._get_and_munchify(key=None, data=data) +# +# def create_cluster_receiver(self, name, receiver_type, +# cluster_name_or_id=None, action=None, +# actor=None, params=None): +# cluster = self.get_cluster(cluster_name_or_id) +# if cluster is None: +# raise exc.OpenStackCloudException( +# 'Invalid cluster {cluster}'.format( +# cluster=cluster_name_or_id)) +# +# receiver = { +# 'name': name, +# 'type': receiver_type +# } +# +# if cluster_name_or_id is not None: +# receiver['cluster_id'] = cluster.id +# +# if action is not None: +# receiver['action'] = action +# +# if actor is not None: +# receiver['actor'] = actor +# +# if params is not None: +# receiver['params'] = params +# +# data = self._clustering_client.post( +# '/receivers', json={'receiver': receiver}, +# error_message="Error creating receiver {name}".format(name=name)) +# return self._get_and_munchify('receiver', data) +# +# def search_cluster_receivers(self, name_or_id=None, filters=None): +# cluster_receivers = self.list_cluster_receivers() +# return _utils._filter_list(cluster_receivers, name_or_id, filters) +# +# def list_cluster_receivers(self): +# try: +# data = self._clustering_client.get( +# '/receivers', +# error_message="Error fetching receivers") +# except exc.OpenStackCloudURINotFound as e: +# self.log.debug(str(e), exc_info=True) +# return [] +# return self._get_and_munchify('receivers', data) +# +# def get_cluster_receiver_by_id(self, receiver_id): +# try: +# data = self._clustering_client.get( +# "/receivers/{receiver_id}".format(receiver_id=receiver_id), +# error_message="Error fetching receiver {name}".format( +# name=receiver_id)) +# return self._get_and_munchify('receiver', data) +# except exc.OpenStackCloudURINotFound as e: +# self.log.debug(str(e), exc_info=True) +# return None +# +# def get_cluster_receiver(self, name_or_id, filters=None): +# return _utils._get_entity( +# self, 'cluster_receiver', name_or_id, filters) +# +# def delete_cluster_receiver(self, name_or_id, wait=False, timeout=3600): +# receiver = self.get_cluster_receiver(name_or_id) +# if receiver is None: +# self.log.debug("Receiver %s not found for deleting", name_or_id) +# return False +# +# receiver_id = receiver['id'] +# +# self._clustering_client.delete( +# "/receivers/{receiver_id}".format(receiver_id=receiver_id), +# error_message="Error deleting receiver {name}".format( +# name=name_or_id)) +# +# if not wait: +# return True +# +# for count in utils.iterate_timeout( +# timeout, "Timeout waiting for cluster receiver to delete"): +# +# receiver = self.get_cluster_receiver_by_id(receiver_id) +# +# if not receiver: +# break +# +# return True +# +# def update_cluster_receiver(self, name_or_id, new_name=None, action=None, +# params=None): +# old_receiver = self.get_cluster_receiver(name_or_id) +# if old_receiver is None: +# raise exc.OpenStackCloudException( +# 'Invalid receiver {receiver}'.format(receiver=name_or_id)) +# +# receiver = {} +# +# if new_name is not None: +# receiver['name'] = new_name +# +# if action is not None: +# receiver['action'] = action +# +# if params is not None: +# receiver['params'] = params +# +# data = self._clustering_client.patch( +# "/receivers/{receiver_id}".format(receiver_id=old_receiver.id), +# json={'receiver': receiver}, +# error_message="Error updating receiver {name}".format( +# name=name_or_id)) +# return self._get_and_munchify(key=None, data=data) diff --git a/openstack/tests/unit/cloud/test_clustering.py b/openstack/tests/unit/cloud/test_clustering.py index 89b6c7326..062e62861 100644 --- a/openstack/tests/unit/cloud/test_clustering.py +++ b/openstack/tests/unit/cloud/test_clustering.py @@ -11,9 +11,8 @@ # under the License. import copy -import testtools -from openstack.cloud import exc +from openstack.clustering.v1 import cluster from openstack.tests.unit import base @@ -21,7 +20,7 @@ 'name': 'fake-name', 'profile_id': '1', 'desired_capacity': 1, - 'config': 'fake-config', + 'config': {'a': 'b'}, 'max_size': 1, 'min_size': 1, 'timeout': 100, @@ -63,621 +62,626 @@ def assertAreInstances(self, elements, elem_type): for e in elements: self.assertIsInstance(e, elem_type) + def _compare_clusters(self, exp, real): + self.assertEqual( + cluster.Cluster(**exp).to_dict(computed=False), + real.to_dict(computed=False)) + def setUp(self): super(TestClustering, self).setUp() self.use_senlin() - def test_create_cluster(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'profiles', '1']), - json={ - "profiles": [NEW_PROFILE_DICT]}), - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'profiles']), - json={ - "profiles": [NEW_PROFILE_DICT]}), - dict(method='POST', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'clusters']), - json=NEW_CLUSTERING_DICT) - ]) - profile = self.cloud.get_cluster_profile_by_id(NEW_PROFILE_DICT['id']) - c = self.cloud.create_cluster( - name=CLUSTERING_DICT['name'], - desired_capacity=CLUSTERING_DICT['desired_capacity'], - profile=profile, - config=CLUSTERING_DICT['config'], - max_size=CLUSTERING_DICT['max_size'], - min_size=CLUSTERING_DICT['min_size'], - metadata=CLUSTERING_DICT['metadata'], - timeout=CLUSTERING_DICT['timeout']) - - self.assertEqual(NEW_CLUSTERING_DICT, c) - self.assert_calls() - - def test_create_cluster_exception(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'profiles', '1']), - json={ - "profiles": [NEW_PROFILE_DICT]}), - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'profiles']), - json={ - "profiles": [NEW_PROFILE_DICT]}), - dict(method='POST', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'clusters']), - status_code=500) - ]) - profile = self.cloud.get_cluster_profile_by_id(NEW_PROFILE_DICT['id']) - with testtools.ExpectedException( - exc.OpenStackCloudHTTPError, - "Error creating cluster fake-name.*"): - self.cloud.create_cluster(name='fake-name', profile=profile) - self.assert_calls() - - def test_get_cluster_by_id(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'clusters', '1']), - json={ - "cluster": NEW_CLUSTERING_DICT}) - ]) - cluster = self.cloud.get_cluster_by_id('1') - self.assertEqual(cluster['id'], '1') - self.assert_calls() - - def test_get_cluster_not_found_returns_false(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'clusters', - 'no-cluster']), - status_code=404) - ]) - c = self.cloud.get_cluster_by_id('no-cluster') - self.assertFalse(c) - self.assert_calls() - - def test_update_cluster(self): - new_max_size = 5 - updated_cluster = copy.copy(NEW_CLUSTERING_DICT) - updated_cluster['max_size'] = new_max_size - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'clusters', '1']), - json={ - "cluster": NEW_CLUSTERING_DICT}), - dict(method='PATCH', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'clusters', '1']), - json=updated_cluster, - ) - ]) - cluster = self.cloud.get_cluster_by_id('1') - c = self.cloud.update_cluster(cluster, new_max_size) - self.assertEqual(updated_cluster, c) - self.assert_calls() - - def test_delete_cluster(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'clusters']), - json={ - "clusters": [NEW_CLUSTERING_DICT]}), - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'clusters', '1', - 'policies']), - json={"cluster_policies": []}), - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'receivers']), - json={"receivers": []}), - dict(method='DELETE', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'clusters', '1']), - json=NEW_CLUSTERING_DICT) - ]) - self.assertTrue(self.cloud.delete_cluster('1')) - self.assert_calls() - - def test_list_clusters(self): - clusters = {'clusters': [NEW_CLUSTERING_DICT]} - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'clusters']), - json=clusters) - ]) - c = self.cloud.list_clusters() - - self.assertIsInstance(c, list) - self.assertAreInstances(c, dict) - - self.assert_calls() - - def test_attach_policy_to_cluster(self): - policy = { - 'policy_id': '1', - 'enabled': 'true' - } - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'clusters', '1']), - json={ - "cluster": NEW_CLUSTERING_DICT}), - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'policies', '1']), - json={ - "policy": NEW_POLICY_DICT}), - dict(method='POST', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'clusters', '1', - 'actions']), - json={'policy_attach': policy}) - ]) - cluster = self.cloud.get_cluster_by_id('1') - policy = self.cloud.get_cluster_policy_by_id('1') - p = self.cloud.attach_policy_to_cluster(cluster, policy, 'true') - self.assertTrue(p) - self.assert_calls() - - def test_detach_policy_from_cluster(self): - updated_cluster = copy.copy(NEW_CLUSTERING_DICT) - updated_cluster['policies'] = ['1'] - detached_cluster = copy.copy(NEW_CLUSTERING_DICT) - detached_cluster['policies'] = [] - - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'clusters', '1']), - json={ - "cluster": NEW_CLUSTERING_DICT}), - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'policies', '1']), - json={ - "policy": NEW_POLICY_DICT}), - dict(method='POST', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'clusters', '1', - 'actions']), - json={'policy_detach': {'policy_id': '1'}}), - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'clusters', '1']), - json={ - "cluster": updated_cluster}), - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'clusters', '1']), - json={ - "cluster": detached_cluster}), - ]) - cluster = self.cloud.get_cluster_by_id('1') - policy = self.cloud.get_cluster_policy_by_id('1') - p = self.cloud.detach_policy_from_cluster(cluster, policy, wait=True) - self.assertTrue(p) - self.assert_calls() - - def test_get_policy_on_cluster_by_id(self): - cluster_policy = { - "cluster_id": "1", - "cluster_name": "cluster1", - "enabled": True, - "id": "1", - "policy_id": "1", - "policy_name": "policy1", - "policy_type": "senlin.policy.deletion-1.0" - } - - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'clusters', '1', - 'policies', '1']), - json={ - "cluster_policy": cluster_policy}) - ]) - policy = self.cloud.get_policy_on_cluster('1', '1') - self.assertEqual(policy['cluster_id'], '1') - self.assert_calls() - - def test_get_policy_on_cluster_not_found_returns_false(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'clusters', '1', - 'policies', 'no-policy']), - status_code=404) - ]) - p = self.cloud.get_policy_on_cluster('1', 'no-policy') - self.assertFalse(p) - self.assert_calls() - - def test_update_policy_on_cluster(self): - policy = { - 'policy_id': '1', - 'enabled': 'true' - } - updated_cluster = copy.copy(NEW_CLUSTERING_DICT) - updated_cluster['policies'] = policy - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'clusters', '1']), - json={ - "cluster": NEW_CLUSTERING_DICT}), - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'policies', - '1']), - json={ - "policy": NEW_POLICY_DICT}), - dict(method='POST', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'clusters', '1', - 'actions']), - json={'policies': []}) - ]) - cluster = self.cloud.get_cluster_by_id('1') - policy = self.cloud.get_cluster_policy_by_id('1') - p = self.cloud.update_policy_on_cluster(cluster, policy, True) - self.assertTrue(p) - self.assert_calls() - - def test_get_policy_on_cluster(self): - cluster_policy = { - 'cluster_id': '1', - 'cluster_name': 'cluster1', - 'enabled': 'true', - 'id': '1', - 'policy_id': '1', - 'policy_name': 'policy1', - 'policy_type': 'type' - } - - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'clusters', '1', - 'policies', '1']), - json={ - "cluster_policy": cluster_policy}) - ]) - get_policy = self.cloud.get_policy_on_cluster('1', '1') - self.assertEqual(get_policy, cluster_policy) - self.assert_calls() - - def test_create_cluster_profile(self): - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'profiles']), - json={'profile': NEW_PROFILE_DICT}) - ]) - p = self.cloud.create_cluster_profile('fake-profile-name', {}) - - self.assertEqual(NEW_PROFILE_DICT, p) - self.assert_calls() - - def test_create_cluster_profile_exception(self): - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'profiles']), - status_code=500) - ]) - with testtools.ExpectedException( - exc.OpenStackCloudHTTPError, - "Error creating profile fake-profile-name.*"): - self.cloud.create_cluster_profile('fake-profile-name', {}) - self.assert_calls() - - def test_list_cluster_profiles(self): - profiles = {'profiles': [NEW_PROFILE_DICT]} - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'profiles']), - json=profiles) - ]) - p = self.cloud.list_cluster_profiles() - - self.assertIsInstance(p, list) - self.assertAreInstances(p, dict) - - self.assert_calls() - - def test_get_cluster_profile_by_id(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'profiles', '1']), - json={ - "profile": NEW_PROFILE_DICT}) - ]) - p = self.cloud.get_cluster_profile_by_id('1') - self.assertEqual(p['id'], '1') - self.assert_calls() - - def test_get_cluster_profile_not_found_returns_false(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'profiles', - 'no-profile']), - status_code=404) - ]) - p = self.cloud.get_cluster_profile_by_id('no-profile') - self.assertFalse(p) - self.assert_calls() - - def test_update_cluster_profile(self): - new_name = "new-name" - updated_profile = copy.copy(NEW_PROFILE_DICT) - updated_profile['name'] = new_name - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'profiles']), - json={ - "profiles": [NEW_PROFILE_DICT]}), - dict(method='PATCH', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'profiles', '1']), - json=updated_profile, - ) - ]) - p = self.cloud.update_cluster_profile('1', new_name=new_name) - self.assertEqual(updated_profile, p) - self.assert_calls() - - def test_delete_cluster_profile(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'profiles', '1']), - json={ - "profile": NEW_PROFILE_DICT}), - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'clusters']), - json={}), - dict(method='DELETE', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'profiles', '1']), - json=NEW_PROFILE_DICT) - ]) - profile = self.cloud.get_cluster_profile_by_id('1') - self.assertTrue(self.cloud.delete_cluster_profile(profile)) - self.assert_calls() - - def test_create_cluster_policy(self): - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'policies']), - json={'policy': NEW_POLICY_DICT}) - ]) - p = self.cloud.create_cluster_policy('fake-policy-name', {}) - - self.assertEqual(NEW_POLICY_DICT, p) - self.assert_calls() - - def test_create_cluster_policy_exception(self): - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'policies']), - status_code=500) - ]) - with testtools.ExpectedException( - exc.OpenStackCloudHTTPError, - "Error creating policy fake-policy-name.*"): - self.cloud.create_cluster_policy('fake-policy-name', {}) - self.assert_calls() - - def test_list_cluster_policies(self): - policies = {'policies': [NEW_POLICY_DICT]} - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'policies']), - json=policies) - ]) - p = self.cloud.list_cluster_policies() - - self.assertIsInstance(p, list) - self.assertAreInstances(p, dict) - - self.assert_calls() - - def test_get_cluster_policy_by_id(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'policies', '1']), - json={ - "policy": NEW_POLICY_DICT}) - ]) - p = self.cloud.get_cluster_policy_by_id('1') - self.assertEqual(p['id'], '1') - self.assert_calls() - - def test_get_cluster_policy_not_found_returns_false(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'policies', - 'no-policy']), - status_code=404) - ]) - p = self.cloud.get_cluster_policy_by_id('no-policy') - self.assertFalse(p) - self.assert_calls() - - def test_update_cluster_policy(self): - new_name = "new-name" - updated_policy = copy.copy(NEW_POLICY_DICT) - updated_policy['name'] = new_name - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'policies']), - json={ - "policies": [NEW_POLICY_DICT]}), - dict(method='PATCH', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'policies', '1']), - json=updated_policy, - ) - ]) - p = self.cloud.update_cluster_policy('1', new_name=new_name) - self.assertEqual(updated_policy, p) - self.assert_calls() - - def test_delete_cluster_policy(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'policies', '1']), - json={ - "policy": NEW_POLICY_DICT}), - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'clusters']), - json={}), - dict(method='DELETE', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'policies', '1']), - json=NEW_POLICY_DICT) - ]) - self.assertTrue(self.cloud.delete_cluster_policy('1')) - self.assert_calls() - - def test_create_cluster_receiver(self): - clusters = {'clusters': [NEW_CLUSTERING_DICT]} - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'clusters']), - json=clusters), - dict(method='POST', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'receivers']), - json={'receiver': NEW_RECEIVER_DICT}) - ]) - r = self.cloud.create_cluster_receiver('fake-receiver-name', {}) - - self.assertEqual(NEW_RECEIVER_DICT, r) - self.assert_calls() - - def test_create_cluster_receiver_exception(self): - clusters = {'clusters': [NEW_CLUSTERING_DICT]} - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'clusters']), - json=clusters), - dict(method='POST', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'receivers']), - status_code=500), - ]) - with testtools.ExpectedException( - exc.OpenStackCloudHTTPError, - "Error creating receiver fake-receiver-name.*"): - self.cloud.create_cluster_receiver('fake-receiver-name', {}) - self.assert_calls() - - def test_list_cluster_receivers(self): - receivers = {'receivers': [NEW_RECEIVER_DICT]} - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'receivers']), - json=receivers) - ]) - r = self.cloud.list_cluster_receivers() - - self.assertIsInstance(r, list) - self.assertAreInstances(r, dict) - - self.assert_calls() - - def test_get_cluster_receiver_by_id(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'receivers', '1']), - json={ - "receiver": NEW_RECEIVER_DICT}) - ]) - r = self.cloud.get_cluster_receiver_by_id('1') - self.assertEqual(r['id'], '1') - self.assert_calls() - - def test_get_cluster_receiver_not_found_returns_false(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'receivers', - 'no-receiver']), - json={'receivers': []}) - ]) - p = self.cloud.get_cluster_receiver_by_id('no-receiver') - self.assertFalse(p) - self.assert_calls() - - def test_update_cluster_receiver(self): - new_name = "new-name" - updated_receiver = copy.copy(NEW_RECEIVER_DICT) - updated_receiver['name'] = new_name - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'receivers']), - json={ - "receivers": [NEW_RECEIVER_DICT]}), - dict(method='PATCH', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'receivers', '1']), - json=updated_receiver, - ) - ]) - r = self.cloud.update_cluster_receiver('1', new_name=new_name) - self.assertEqual(updated_receiver, r) - self.assert_calls() - - def test_delete_cluster_receiver(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'receivers']), - json={ - "receivers": [NEW_RECEIVER_DICT]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'receivers', '1']), - json=NEW_RECEIVER_DICT), - dict(method='GET', - uri=self.get_mock_url( - 'clustering', 'public', append=['v1', 'receivers', '1']), - json={}), - ]) - self.assertTrue(self.cloud.delete_cluster_receiver('1', wait=True)) - self.assert_calls() +# def test_create_cluster(self): +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'profiles', '1']), +# json={ +# "profiles": [NEW_PROFILE_DICT]}), +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'profiles']), +# json={ +# "profiles": [NEW_PROFILE_DICT]}), +# dict(method='POST', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'clusters']), +# json=NEW_CLUSTERING_DICT) +# ]) +# profile = self.cloud.get_cluster_profile_by_id(NEW_PROFILE_DICT['id']) +# c = self.cloud.create_cluster( +# name=CLUSTERING_DICT['name'], +# desired_capacity=CLUSTERING_DICT['desired_capacity'], +# profile=profile, +# config=CLUSTERING_DICT['config'], +# max_size=CLUSTERING_DICT['max_size'], +# min_size=CLUSTERING_DICT['min_size'], +# metadata=CLUSTERING_DICT['metadata'], +# timeout=CLUSTERING_DICT['timeout']) +# +# self._compare_clusters(NEW_CLUSTERING_DICT, c) +# self.assert_calls() +# +# def test_create_cluster_exception(self): +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'profiles', '1']), +# json={ +# "profiles": [NEW_PROFILE_DICT]}), +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'profiles']), +# json={ +# "profiles": [NEW_PROFILE_DICT]}), +# dict(method='POST', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'clusters']), +# status_code=500) +# ]) +# profile = self.cloud.get_cluster_profile_by_id(NEW_PROFILE_DICT['id']) +# with testtools.ExpectedException( +# exc.OpenStackCloudHTTPError): +# self.cloud.create_cluster(name='fake-name', profile=profile) +# self.assert_calls() +# +# def test_get_cluster_by_id(self): +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'clusters', '1']), +# json={ +# "cluster": NEW_CLUSTERING_DICT}) +# ]) +# cluster = self.cloud.get_cluster_by_id('1') +# self.assertEqual(cluster['id'], '1') +# self.assert_calls() +# +# def test_get_cluster_not_found_returns_false(self): +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'clusters', +# 'no-cluster']), +# status_code=404) +# ]) +# c = self.cloud.get_cluster_by_id('no-cluster') +# self.assertFalse(c) +# self.assert_calls() +# +# def test_update_cluster(self): +# new_max_size = 5 +# updated_cluster = copy.copy(NEW_CLUSTERING_DICT) +# updated_cluster['max_size'] = new_max_size +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'clusters', '1']), +# json={ +# "cluster": NEW_CLUSTERING_DICT}), +# dict(method='PATCH', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'clusters', '1']), +# json=updated_cluster, +# ) +# ]) +# cluster = self.cloud.get_cluster_by_id('1') +# c = self.cloud.update_cluster(cluster, new_max_size) +# self.assertEqual(updated_cluster, c) +# self.assert_calls() +# +# def test_delete_cluster(self): +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'clusters']), +# json={ +# "clusters": [NEW_CLUSTERING_DICT]}), +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'clusters', '1', +# 'policies']), +# json={"cluster_policies": []}), +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'receivers']), +# json={"receivers": []}), +# dict(method='DELETE', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'clusters', '1']), +# json=NEW_CLUSTERING_DICT) +# ]) +# self.assertTrue(self.cloud.delete_cluster('1')) +# self.assert_calls() +# +# def test_list_clusters(self): +# clusters = {'clusters': [NEW_CLUSTERING_DICT]} +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'clusters']), +# json=clusters) +# ]) +# c = self.cloud.list_clusters() +# +# self.assertIsInstance(c, list) +# self.assertAreInstances(c, dict) +# +# self.assert_calls() +# +# def test_attach_policy_to_cluster(self): +# policy = { +# 'policy_id': '1', +# 'enabled': 'true' +# } +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'clusters', '1']), +# json={ +# "cluster": NEW_CLUSTERING_DICT}), +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'policies', '1']), +# json={ +# "policy": NEW_POLICY_DICT}), +# dict(method='POST', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'clusters', '1', +# 'actions']), +# json={'policy_attach': policy}) +# ]) +# cluster = self.cloud.get_cluster_by_id('1') +# policy = self.cloud.get_cluster_policy_by_id('1') +# p = self.cloud.attach_policy_to_cluster(cluster, policy, 'true') +# self.assertTrue(p) +# self.assert_calls() +# +# def test_detach_policy_from_cluster(self): +# updated_cluster = copy.copy(NEW_CLUSTERING_DICT) +# updated_cluster['policies'] = ['1'] +# detached_cluster = copy.copy(NEW_CLUSTERING_DICT) +# detached_cluster['policies'] = [] +# +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'clusters', '1']), +# json={ +# "cluster": NEW_CLUSTERING_DICT}), +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'policies', '1']), +# json={ +# "policy": NEW_POLICY_DICT}), +# dict(method='POST', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'clusters', '1', +# 'actions']), +# json={'policy_detach': {'policy_id': '1'}}), +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'clusters', '1']), +# json={ +# "cluster": updated_cluster}), +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'clusters', '1']), +# json={ +# "cluster": detached_cluster}), +# ]) +# cluster = self.cloud.get_cluster_by_id('1') +# policy = self.cloud.get_cluster_policy_by_id('1') +# p = self.cloud.detach_policy_from_cluster(cluster, policy, wait=True) +# self.assertTrue(p) +# self.assert_calls() +# +# def test_get_policy_on_cluster_by_id(self): +# cluster_policy = { +# "cluster_id": "1", +# "cluster_name": "cluster1", +# "enabled": True, +# "id": "1", +# "policy_id": "1", +# "policy_name": "policy1", +# "policy_type": "senlin.policy.deletion-1.0" +# } +# +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'clusters', '1', +# 'policies', '1']), +# json={ +# "cluster_policy": cluster_policy}) +# ]) +# policy = self.cloud.get_policy_on_cluster('1', '1') +# self.assertEqual(policy['cluster_id'], '1') +# self.assert_calls() +# +# def test_get_policy_on_cluster_not_found_returns_false(self): +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'clusters', '1', +# 'policies', +# 'no-policy']), +# status_code=404) +# ]) +# p = self.cloud.get_policy_on_cluster('1', 'no-policy') +# self.assertFalse(p) +# self.assert_calls() +# +# def test_update_policy_on_cluster(self): +# policy = { +# 'policy_id': '1', +# 'enabled': 'true' +# } +# updated_cluster = copy.copy(NEW_CLUSTERING_DICT) +# updated_cluster['policies'] = policy +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'clusters', '1']), +# json={ +# "cluster": NEW_CLUSTERING_DICT}), +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'policies', +# '1']), +# json={ +# "policy": NEW_POLICY_DICT}), +# dict(method='POST', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'clusters', '1', +# 'actions']), +# json={'policies': []}) +# ]) +# cluster = self.cloud.get_cluster_by_id('1') +# policy = self.cloud.get_cluster_policy_by_id('1') +# p = self.cloud.update_policy_on_cluster(cluster, policy, True) +# self.assertTrue(p) +# self.assert_calls() +# +# def test_get_policy_on_cluster(self): +# cluster_policy = { +# 'cluster_id': '1', +# 'cluster_name': 'cluster1', +# 'enabled': 'true', +# 'id': '1', +# 'policy_id': '1', +# 'policy_name': 'policy1', +# 'policy_type': 'type' +# } +# +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'clusters', '1', +# 'policies', '1']), +# json={ +# "cluster_policy": cluster_policy}) +# ]) +# get_policy = self.cloud.get_policy_on_cluster('1', '1') +# self.assertEqual(get_policy, cluster_policy) +# self.assert_calls() +# +# def test_create_cluster_profile(self): +# self.register_uris([ +# dict(method='POST', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'profiles']), +# json={'profile': NEW_PROFILE_DICT}) +# ]) +# p = self.cloud.create_cluster_profile('fake-profile-name', {}) +# +# self.assertEqual(NEW_PROFILE_DICT, p) +# self.assert_calls() +# +# def test_create_cluster_profile_exception(self): +# self.register_uris([ +# dict(method='POST', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'profiles']), +# status_code=500) +# ]) +# with testtools.ExpectedException( +# exc.OpenStackCloudHTTPError, +# "Error creating profile fake-profile-name.*"): +# self.cloud.create_cluster_profile('fake-profile-name', {}) +# self.assert_calls() +# +# def test_list_cluster_profiles(self): +# profiles = {'profiles': [NEW_PROFILE_DICT]} +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'profiles']), +# json=profiles) +# ]) +# p = self.cloud.list_cluster_profiles() +# +# self.assertIsInstance(p, list) +# self.assertAreInstances(p, dict) +# +# self.assert_calls() +# +# def test_get_cluster_profile_by_id(self): +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'profiles', '1']), +# json={ +# "profile": NEW_PROFILE_DICT}) +# ]) +# p = self.cloud.get_cluster_profile_by_id('1') +# self.assertEqual(p['id'], '1') +# self.assert_calls() +# +# def test_get_cluster_profile_not_found_returns_false(self): +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'profiles', +# 'no-profile']), +# status_code=404) +# ]) +# p = self.cloud.get_cluster_profile_by_id('no-profile') +# self.assertFalse(p) +# self.assert_calls() +# +# def test_update_cluster_profile(self): +# new_name = "new-name" +# updated_profile = copy.copy(NEW_PROFILE_DICT) +# updated_profile['name'] = new_name +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'profiles']), +# json={ +# "profiles": [NEW_PROFILE_DICT]}), +# dict(method='PATCH', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'profiles', '1']), +# json=updated_profile, +# ) +# ]) +# p = self.cloud.update_cluster_profile('1', new_name=new_name) +# self.assertEqual(updated_profile, p) +# self.assert_calls() +# +# def test_delete_cluster_profile(self): +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'profiles', '1']), +# json={ +# "profile": NEW_PROFILE_DICT}), +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'clusters']), +# json={'clusters': [{'cluster': CLUSTERING_DICT}]}), +# dict(method='DELETE', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'profiles', '1']), +# json=NEW_PROFILE_DICT) +# ]) +# profile = self.cloud.get_cluster_profile_by_id('1') +# self.assertTrue(self.cloud.delete_cluster_profile(profile)) +# self.assert_calls() +# +# def test_create_cluster_policy(self): +# self.register_uris([ +# dict(method='POST', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'policies']), +# json={'policy': NEW_POLICY_DICT}) +# ]) +# p = self.cloud.create_cluster_policy('fake-policy-name', {}) +# +# self.assertEqual(NEW_POLICY_DICT, p) +# self.assert_calls() +# +# def test_create_cluster_policy_exception(self): +# self.register_uris([ +# dict(method='POST', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'policies']), +# status_code=500) +# ]) +# with testtools.ExpectedException( +# exc.OpenStackCloudHTTPError, +# "Error creating policy fake-policy-name.*"): +# self.cloud.create_cluster_policy('fake-policy-name', {}) +# self.assert_calls() +# +# def test_list_cluster_policies(self): +# policies = {'policies': [NEW_POLICY_DICT]} +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'policies']), +# json=policies) +# ]) +# p = self.cloud.list_cluster_policies() +# +# self.assertIsInstance(p, list) +# self.assertAreInstances(p, dict) +# +# self.assert_calls() +# +# def test_get_cluster_policy_by_id(self): +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'policies', '1']), +# json={ +# "policy": NEW_POLICY_DICT}) +# ]) +# p = self.cloud.get_cluster_policy_by_id('1') +# self.assertEqual(p['id'], '1') +# self.assert_calls() +# +# def test_get_cluster_policy_not_found_returns_false(self): +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'policies', +# 'no-policy']), +# status_code=404) +# ]) +# p = self.cloud.get_cluster_policy_by_id('no-policy') +# self.assertFalse(p) +# self.assert_calls() +# +# def test_update_cluster_policy(self): +# new_name = "new-name" +# updated_policy = copy.copy(NEW_POLICY_DICT) +# updated_policy['name'] = new_name +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'policies']), +# json={ +# "policies": [NEW_POLICY_DICT]}), +# dict(method='PATCH', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'policies', '1']), +# json=updated_policy, +# ) +# ]) +# p = self.cloud.update_cluster_policy('1', new_name=new_name) +# self.assertEqual(updated_policy, p) +# self.assert_calls() +# +# def test_delete_cluster_policy(self): +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'policies', '1']), +# json={ +# "policy": NEW_POLICY_DICT}), +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'clusters']), +# json={}), +# dict(method='DELETE', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'policies', '1']), +# json=NEW_POLICY_DICT) +# ]) +# self.assertTrue(self.cloud.delete_cluster_policy('1')) +# self.assert_calls() +# +# def test_create_cluster_receiver(self): +# clusters = {'clusters': [NEW_CLUSTERING_DICT]} +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'clusters']), +# json=clusters), +# dict(method='POST', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'receivers']), +# json={'receiver': NEW_RECEIVER_DICT}) +# ]) +# r = self.cloud.create_cluster_receiver('fake-receiver-name', {}) +# +# self.assertEqual(NEW_RECEIVER_DICT, r) +# self.assert_calls() +# +# def test_create_cluster_receiver_exception(self): +# clusters = {'clusters': [NEW_CLUSTERING_DICT]} +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'clusters']), +# json=clusters), +# dict(method='POST', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'receivers']), +# status_code=500), +# ]) +# with testtools.ExpectedException( +# exc.OpenStackCloudHTTPError, +# "Error creating receiver fake-receiver-name.*"): +# self.cloud.create_cluster_receiver('fake-receiver-name', {}) +# self.assert_calls() +# +# def test_list_cluster_receivers(self): +# receivers = {'receivers': [NEW_RECEIVER_DICT]} +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'receivers']), +# json=receivers) +# ]) +# r = self.cloud.list_cluster_receivers() +# +# self.assertIsInstance(r, list) +# self.assertAreInstances(r, dict) +# +# self.assert_calls() +# +# def test_get_cluster_receiver_by_id(self): +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'receivers', '1']), +# json={ +# "receiver": NEW_RECEIVER_DICT}) +# ]) +# r = self.cloud.get_cluster_receiver_by_id('1') +# self.assertEqual(r['id'], '1') +# self.assert_calls() +# +# def test_get_cluster_receiver_not_found_returns_false(self): +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'receivers', +# 'no-receiver']), +# json={'receivers': []}) +# ]) +# p = self.cloud.get_cluster_receiver_by_id('no-receiver') +# self.assertFalse(p) +# self.assert_calls() +# +# def test_update_cluster_receiver(self): +# new_name = "new-name" +# updated_receiver = copy.copy(NEW_RECEIVER_DICT) +# updated_receiver['name'] = new_name +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'receivers']), +# json={ +# "receivers": [NEW_RECEIVER_DICT]}), +# dict(method='PATCH', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'receivers', '1']), +# json=updated_receiver, +# ) +# ]) +# r = self.cloud.update_cluster_receiver('1', new_name=new_name) +# self.assertEqual(updated_receiver, r) +# self.assert_calls() +# +# def test_delete_cluster_receiver(self): +# self.register_uris([ +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'receivers']), +# json={ +# "receivers": [NEW_RECEIVER_DICT]}), +# dict(method='DELETE', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'receivers', '1']), +# json=NEW_RECEIVER_DICT), +# dict(method='GET', +# uri=self.get_mock_url( +# 'clustering', 'public', append=['v1', 'receivers', '1']), +# json={}), +# ]) +# self.assertTrue(self.cloud.delete_cluster_receiver('1', wait=True)) +# self.assert_calls() diff --git a/releasenotes/notes/drop-senlin-cloud-layer-c06d496acc70b014.yaml b/releasenotes/notes/drop-senlin-cloud-layer-c06d496acc70b014.yaml new file mode 100644 index 000000000..784d97cdc --- /dev/null +++ b/releasenotes/notes/drop-senlin-cloud-layer-c06d496acc70b014.yaml @@ -0,0 +1,4 @@ +--- +upgrade: + - | + Cloud layer operations for Senlin service are dropped due to big amount of bugs there. From 043524ab97c0ecd065c709f851ff419c6bcf982c Mon Sep 17 00:00:00 2001 From: Ryan Zimmerman Date: Wed, 14 Apr 2021 10:38:33 -0700 Subject: [PATCH 2853/3836] Add compute microversion 2.78 Compute 2.78 [1] added the topology sub-resource to show NUMA information. [1] https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id71 Change-Id: Iacaf51a977d79a7411d48b08f05924e8c4bf55c4 --- openstack/compute/v2/server.py | 16 +++++ .../tests/unit/compute/v2/test_server.py | 67 +++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index fe492c897..d0ee30187 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -549,6 +549,22 @@ def _live_migrate(self, session, host, force, block_migration, self._action( session, {'os-migrateLive': body}, microversion=microversion) + def fetch_topology(self, session): + utils.require_microversion(session, 2.78) + + url = utils.urljoin(Server.base_path, self.id, 'topology') + + response = session.get(url) + + exceptions.raise_from_response(response) + + try: + data = response.json() + if 'topology' in data: + return data['topology'] + except ValueError: + pass + def fetch_security_groups(self, session): """Fetch security groups of a server. diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 2491b7363..2062fb1c9 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -996,6 +996,73 @@ class FakeEndpointData: self.sess.post.assert_called_with( url, json=body, headers=headers, microversion='2.30') + def test_get_topology(self): + sot = server.Server(**EXAMPLE) + + class FakeEndpointData: + min_microversion = '2.1' + max_microversion = '2.78' + self.sess.get_endpoint_data.return_value = FakeEndpointData() + self.sess.default_microversion = None + + response = mock.Mock() + + topology = { + "nodes": [ + { + "cpu_pinning": { + "0": 0, + "1": 5 + }, + "host_node": 0, + "memory_mb": 1024, + "siblings": [ + [ + 0, + 1 + ] + ], + "vcpu_set": [ + 0, + 1 + ] + }, + { + "cpu_pinning": { + "2": 1, + "3": 8 + }, + "host_node": 1, + "memory_mb": 2048, + "siblings": [ + [ + 2, + 3 + ] + ], + "vcpu_set": [ + 2, + 3 + ] + } + ], + "pagesize_kb": 4 + } + + response.status_code = 200 + response.json.return_value = { + 'topology': topology + } + + self.sess.get.return_value = response + + fetched_topology = sot.fetch_topology(self.sess) + + url = 'servers/IDENTIFIER/topology' + self.sess.get.assert_called_with(url) + + self.assertEqual(fetched_topology, topology) + def test_get_security_groups(self): sot = server.Server(**EXAMPLE) From 6c2a5ccb9ec1ab1d821821f2de692d7b5620b7cf Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 27 May 2021 15:57:10 +0200 Subject: [PATCH 2854/3836] Move to OFTC Changing this also drop old refenrences to shade (replace them with openstacksdk) Change-Id: I6c0a05efd864f0ae74f2686c2365b8617efae5ad --- doc/source/contributor/index.rst | 4 ++-- doc/source/user/multi-cloud-demo.rst | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst index e55b1d7a7..b785804be 100644 --- a/doc/source/contributor/index.rst +++ b/doc/source/contributor/index.rst @@ -34,11 +34,11 @@ IRC ~~~ The developers of this project are available in the `#openstack-sdks`__ channel -on Freenode. This channel includes conversation on SDKs and tools within the +on OFTC IRC. This channel includes conversation on SDKs and tools within the general OpenStack community, including OpenStackClient as well as occasional talk about SDKs created for languages outside of Python. -.. __: http://webchat.freenode.net?channels=%23openstack-sdks +.. __: http://webchat.oftc.net?channels=%23openstack-sdks Email ~~~~~ diff --git a/doc/source/user/multi-cloud-demo.rst b/doc/source/user/multi-cloud-demo.rst index c013fe7b5..e8fd73d2a 100644 --- a/doc/source/user/multi-cloud-demo.rst +++ b/doc/source/user/multi-cloud-demo.rst @@ -29,7 +29,7 @@ Monty Taylor What are we going to talk about? ================================ -`shade` +`OpenStackSDK` * a task and end-user oriented Python library * abstracts deployment differences @@ -42,12 +42,12 @@ What are we going to talk about? * Initial logic/design extracted from nodepool * Librified to re-use in Ansible -shade is Free Software -====================== +OpenStackSDK is Free Software +============================= -* https://opendev.org/openstack/shade +* https://opendev.org/openstack/openstacksdk * openstack-discuss@lists.openstack.org -* #openstack-shade on freenode +* #openstack-sdks on oftc This talk is Free Software, too =============================== From dc906fbfce7be7dcc3b726181432e5cf4efb1dd9 Mon Sep 17 00:00:00 2001 From: "wu.shiming" Date: Mon, 31 May 2021 15:45:54 +0800 Subject: [PATCH 2855/3836] Changed minversion in tox to 3.18.0 The patch bumps min version of tox to 3.18.0 in order to replace tox's whitelist_externals by allowlist_externals option: https://github.com/tox-dev/tox/blob/master/docs/changelog.rst#v3180-2020-07-23 Change-Id: Ibfb20cc70b4abc64ea08913a2e69c2d0448a84a8 --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 8f1818fa6..36ffd9bbb 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -minversion = 3.9.0 +minversion = 3.18.0 envlist = pep8,py3 skipsdist = True ignore_basepython_conflict = True @@ -62,7 +62,7 @@ commands = {posargs} [testenv:debug] # allow 1 year, or 31536000 seconds, to debug a test before it times out setenv = OS_TEST_TIMEOUT=31536000 -whitelist_externals = find +allowlist_externals = find commands = find . -type f -name "*.pyc" -delete oslo_debug_helper -t openstack/tests {posargs} @@ -94,7 +94,7 @@ commands = [testenv:pdf-docs] deps = {[testenv:docs]deps} -whitelist_externals = +allowlist_externals = make commands = sphinx-build -W --keep-going -b latex -j auto doc/source/ doc/build/pdf From 62781ae228d755017121ead94fc67c5a7acd39ae Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 14 May 2021 10:08:49 +0200 Subject: [PATCH 2856/3836] Sort block storage properties alphabetically Sorting order of the properties plays a big role in maintenance effort for the resource. Before we start moving block storage cloud layer - align things (also drop id/name/links attrs as they are useless and sometimes even harmful being present 2 times on the resource). Change-Id: If6fcf0c658840b3a18672958a9fb02450276dbda --- openstack/block_storage/v2/snapshot.py | 25 +++----- openstack/block_storage/v2/type.py | 4 -- openstack/block_storage/v2/volume.py | 89 ++++++++++++-------------- openstack/block_storage/v3/snapshot.py | 27 ++++---- openstack/block_storage/v3/type.py | 28 ++++---- openstack/block_storage/v3/volume.py | 89 ++++++++++++-------------- 6 files changed, 113 insertions(+), 149 deletions(-) diff --git a/openstack/block_storage/v2/snapshot.py b/openstack/block_storage/v2/snapshot.py index 120b50b50..02d953604 100644 --- a/openstack/block_storage/v2/snapshot.py +++ b/openstack/block_storage/v2/snapshot.py @@ -30,27 +30,22 @@ class Snapshot(resource.Resource): allow_list = True # Properties - #: A ID representing this snapshot. - id = resource.Body("id") - #: Name of the snapshot. Default is None. - name = resource.Body("name") - - #: The current status of this snapshot. Potential values are creating, - #: available, deleting, error, and error_deleting. - status = resource.Body("status") - #: Description of snapshot. Default is None. - description = resource.Body("description") #: The timestamp of this snapshot creation. created_at = resource.Body("created_at") + #: Description of snapshot. Default is None. + description = resource.Body("description") + #: Indicate whether to create snapshot, even if the volume is attached. + #: Default is ``False``. *Type: bool* + is_forced = resource.Body("force", type=format.BoolStr) #: Metadata associated with this snapshot. metadata = resource.Body("metadata", type=dict) - #: The ID of the volume this snapshot was taken of. - volume_id = resource.Body("volume_id") #: The size of the volume, in GBs. size = resource.Body("size", type=int) - #: Indicate whether to create snapshot, even if the volume is attached. - #: Default is ``False``. *Type: bool* - is_forced = resource.Body("force", type=format.BoolStr) + #: The current status of this snapshot. Potential values are creating, + #: available, deleting, error, and error_deleting. + status = resource.Body("status") + #: The ID of the volume this snapshot was taken of. + volume_id = resource.Body("volume_id") class SnapshotDetail(Snapshot): diff --git a/openstack/block_storage/v2/type.py b/openstack/block_storage/v2/type.py index 7e3c81aae..e559b4615 100644 --- a/openstack/block_storage/v2/type.py +++ b/openstack/block_storage/v2/type.py @@ -27,10 +27,6 @@ class Type(resource.Resource): _query_mapping = resource.QueryParameters("is_public") # Properties - #: A ID representing this type. - id = resource.Body("id") - #: Name of the type. - name = resource.Body("name") #: A dict of extra specifications. "capabilities" is a usual key. extra_specs = resource.Body("extra_specs", type=dict) #: a private volume-type. *Type: bool* diff --git a/openstack/block_storage/v2/volume.py b/openstack/block_storage/v2/volume.py index 849351194..696df7d92 100644 --- a/openstack/block_storage/v2/volume.py +++ b/openstack/block_storage/v2/volume.py @@ -31,74 +31,65 @@ class Volume(resource.Resource): allow_list = True # Properties - #: A ID representing this volume. - id = resource.Body("id") - #: The name of this volume. - name = resource.Body("name") - #: A list of links associated with this volume. *Type: list* - links = resource.Body("links", type=list) - + #: TODO(briancurtin): This is currently undocumented in the API. + attachments = resource.Body("attachments") #: The availability zone. availability_zone = resource.Body("availability_zone") - #: To create a volume from an existing volume, specify the ID of - #: the existing volume. If specified, the volume is created with - #: same size of the source volume. - source_volume_id = resource.Body("source_volid") + #: ID of the consistency group. + consistency_group_id = resource.Body("consistencygroup_id") + #: The timestamp of this volume creation. + created_at = resource.Body("created_at") #: The volume description. description = resource.Body("description") - #: To create a volume from an existing snapshot, specify the ID of - #: the existing volume snapshot. If specified, the volume is created - #: in same availability zone and with same size of the snapshot. - snapshot_id = resource.Body("snapshot_id") - #: The size of the volume, in GBs. *Type: int* - size = resource.Body("size", type=int) + #: Extended replication status on this volume. + extended_replication_status = resource.Body( + "os-volume-replication:extended_status") + #: The volume's current back-end. + host = resource.Body("os-vol-host-attr:host") #: The ID of the image from which you want to create the volume. #: Required to create a bootable volume. image_id = resource.Body("imageRef") - #: The name of the associated volume type. - volume_type = resource.Body("volume_type") #: Enables or disables the bootable attribute. You can boot an #: instance from a bootable volume. *Type: bool* is_bootable = resource.Body("bootable", type=format.BoolStr) + #: ``True`` if this volume is encrypted, ``False`` if not. + #: *Type: bool* + is_encrypted = resource.Body("encrypted", type=format.BoolStr) #: One or more metadata key and value pairs to associate with the volume. metadata = resource.Body("metadata") - #: One or more metadata key and value pairs about image - volume_image_metadata = resource.Body("volume_image_metadata") - + #: The volume ID that this volume's name on the back-end is based on. + migration_id = resource.Body("os-vol-mig-status-attr:name_id") + #: The status of this volume's migration (None means that a migration + #: is not currently in progress). + migration_status = resource.Body("os-vol-mig-status-attr:migstat") + #: The project ID associated with current back-end. + project_id = resource.Body("os-vol-tenant-attr:tenant_id") + #: Data set by the replication driver + replication_driver_data = resource.Body( + "os-volume-replication:driver_data") + #: Status of replication on this volume. + replication_status = resource.Body("replication_status") + #: The size of the volume, in GBs. *Type: int* + size = resource.Body("size", type=int) + #: To create a volume from an existing snapshot, specify the ID of + #: the existing volume snapshot. If specified, the volume is created + #: in same availability zone and with same size of the snapshot. + snapshot_id = resource.Body("snapshot_id") + #: To create a volume from an existing volume, specify the ID of + #: the existing volume. If specified, the volume is created with + #: same size of the source volume. + source_volume_id = resource.Body("source_volid") #: One of the following values: creating, available, attaching, in-use #: deleting, error, error_deleting, backing-up, restoring-backup, #: error_restoring. For details on these statuses, see the #: Block Storage API documentation. status = resource.Body("status") - #: TODO(briancurtin): This is currently undocumented in the API. - attachments = resource.Body("attachments") - #: The timestamp of this volume creation. - created_at = resource.Body("created_at") - - #: The volume's current back-end. - host = resource.Body("os-vol-host-attr:host") - #: The project ID associated with current back-end. - project_id = resource.Body("os-vol-tenant-attr:tenant_id") #: The user ID associated with the volume user_id = resource.Body("user_id") - #: The status of this volume's migration (None means that a migration - #: is not currently in progress). - migration_status = resource.Body("os-vol-mig-status-attr:migstat") - #: The volume ID that this volume's name on the back-end is based on. - migration_id = resource.Body("os-vol-mig-status-attr:name_id") - #: Status of replication on this volume. - replication_status = resource.Body("replication_status") - #: Extended replication status on this volume. - extended_replication_status = resource.Body( - "os-volume-replication:extended_status") - #: ID of the consistency group. - consistency_group_id = resource.Body("consistencygroup_id") - #: Data set by the replication driver - replication_driver_data = resource.Body( - "os-volume-replication:driver_data") - #: ``True`` if this volume is encrypted, ``False`` if not. - #: *Type: bool* - is_encrypted = resource.Body("encrypted", type=format.BoolStr) + #: One or more metadata key and value pairs about image + volume_image_metadata = resource.Body("volume_image_metadata") + #: The name of the associated volume type. + volume_type = resource.Body("volume_type") def _action(self, session, body): """Preform volume actions given the message body.""" diff --git a/openstack/block_storage/v3/snapshot.py b/openstack/block_storage/v3/snapshot.py index 86e8383a9..b1361dbb1 100644 --- a/openstack/block_storage/v3/snapshot.py +++ b/openstack/block_storage/v3/snapshot.py @@ -31,31 +31,26 @@ class Snapshot(resource.Resource): allow_list = True # Properties - #: A ID representing this snapshot. - id = resource.Body("id") - #: Name of the snapshot. Default is None. - name = resource.Body("name") - - #: The current status of this snapshot. Potential values are creating, - #: available, deleting, error, and error_deleting. - status = resource.Body("status") - #: Description of snapshot. Default is None. - description = resource.Body("description") #: The timestamp of this snapshot creation. created_at = resource.Body("created_at") - #: Metadata associated with this snapshot. - metadata = resource.Body("metadata", type=dict) - #: The ID of the volume this snapshot was taken of. - volume_id = resource.Body("volume_id") - #: The size of the volume, in GBs. - size = resource.Body("size", type=int) + #: Description of snapshot. Default is None. + description = resource.Body("description") #: Indicate whether to create snapshot, even if the volume is attached. #: Default is ``False``. *Type: bool* is_forced = resource.Body("force", type=format.BoolStr) + #: Metadata associated with this snapshot. + metadata = resource.Body("metadata", type=dict) #: The percentage of completeness the snapshot is currently at. progress = resource.Body("os-extended-snapshot-attributes:progress") #: The project ID this snapshot is associated with. project_id = resource.Body("os-extended-snapshot-attributes:project_id") + #: The size of the volume, in GBs. + size = resource.Body("size", type=int) + #: The current status of this snapshot. Potential values are creating, + #: available, deleting, error, and error_deleting. + status = resource.Body("status") + #: The ID of the volume this snapshot was taken of. + volume_id = resource.Body("volume_id") SnapshotDetail = Snapshot diff --git a/openstack/block_storage/v3/type.py b/openstack/block_storage/v3/type.py index 42fe16afc..e2e02d146 100644 --- a/openstack/block_storage/v3/type.py +++ b/openstack/block_storage/v3/type.py @@ -30,10 +30,6 @@ class Type(resource.Resource): _query_mapping = resource.QueryParameters("is_public") # Properties - #: A ID representing this type. - id = resource.Body("id") - #: Name of the type. - name = resource.Body("name") #: Description of the type. description = resource.Body("description") #: A dict of extra specifications. "capabilities" is a usual key. @@ -108,23 +104,23 @@ class TypeEncryption(resource.Resource): allow_commit = True # Properties + #: The encryption algorithm or mode. + cipher = resource.Body("cipher") + #: Notional service where encryption is performed. + control_location = resource.Body("control_location") + #: The date and time when the resource was created. + created_at = resource.Body("created_at") + #: The resource is deleted or not. + deleted = resource.Body("deleted") + #: The date and time when the resource was deleted. + deleted_at = resource.Body("deleted_at") #: A ID representing this type. encryption_id = resource.Body("encryption_id", alternate_id=True) - #: The ID of the Volume Type. - volume_type_id = resource.URI("volume_type_id") #: The Size of encryption key. key_size = resource.Body("key_size") #: The class that provides encryption support. provider = resource.Body("provider") - #: Notional service where encryption is performed. - control_location = resource.Body("control_location") - #: The encryption algorithm or mode. - cipher = resource.Body("cipher") - #: The resource is deleted or not. - deleted = resource.Body("deleted") - #: The date and time when the resource was created. - created_at = resource.Body("created_at") #: The date and time when the resource was updated. updated_at = resource.Body("updated_at") - #: The date and time when the resource was deleted. - deleted_at = resource.Body("deleted_at") + #: The ID of the Volume Type. + volume_type_id = resource.URI("volume_type_id") diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index bef5a2fbd..02d81d0e1 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -31,74 +31,65 @@ class Volume(resource.Resource): allow_list = True # Properties - #: A ID representing this volume. - id = resource.Body("id") - #: The name of this volume. - name = resource.Body("name") - #: A list of links associated with this volume. *Type: list* - links = resource.Body("links", type=list) - + #: TODO(briancurtin): This is currently undocumented in the API. + attachments = resource.Body("attachments") #: The availability zone. availability_zone = resource.Body("availability_zone") - #: To create a volume from an existing volume, specify the ID of - #: the existing volume. If specified, the volume is created with - #: same size of the source volume. - source_volume_id = resource.Body("source_volid") + #: ID of the consistency group. + consistency_group_id = resource.Body("consistencygroup_id") + #: The timestamp of this volume creation. + created_at = resource.Body("created_at") #: The volume description. description = resource.Body("description") - #: To create a volume from an existing snapshot, specify the ID of - #: the existing volume snapshot. If specified, the volume is created - #: in same availability zone and with same size of the snapshot. - snapshot_id = resource.Body("snapshot_id") - #: The size of the volume, in GBs. *Type: int* - size = resource.Body("size", type=int) + #: Extended replication status on this volume. + extended_replication_status = resource.Body( + "os-volume-replication:extended_status") + #: The volume's current back-end. + host = resource.Body("os-vol-host-attr:host") #: The ID of the image from which you want to create the volume. #: Required to create a bootable volume. image_id = resource.Body("imageRef") - #: The name of the associated volume type. - volume_type = resource.Body("volume_type") #: Enables or disables the bootable attribute. You can boot an #: instance from a bootable volume. *Type: bool* is_bootable = resource.Body("bootable", type=format.BoolStr) + #: ``True`` if this volume is encrypted, ``False`` if not. + #: *Type: bool* + is_encrypted = resource.Body("encrypted", type=format.BoolStr) #: One or more metadata key and value pairs to associate with the volume. metadata = resource.Body("metadata") - #: One or more metadata key and value pairs about image - volume_image_metadata = resource.Body("volume_image_metadata") - + #: The volume ID that this volume's name on the back-end is based on. + migration_id = resource.Body("os-vol-mig-status-attr:name_id") + #: The status of this volume's migration (None means that a migration + #: is not currently in progress). + migration_status = resource.Body("os-vol-mig-status-attr:migstat") + #: The project ID associated with current back-end. + project_id = resource.Body("os-vol-tenant-attr:tenant_id") + #: Data set by the replication driver + replication_driver_data = resource.Body( + "os-volume-replication:driver_data") + #: Status of replication on this volume. + replication_status = resource.Body("replication_status") + #: The size of the volume, in GBs. *Type: int* + size = resource.Body("size", type=int) + #: To create a volume from an existing snapshot, specify the ID of + #: the existing volume snapshot. If specified, the volume is created + #: in same availability zone and with same size of the snapshot. + snapshot_id = resource.Body("snapshot_id") + #: To create a volume from an existing volume, specify the ID of + #: the existing volume. If specified, the volume is created with + #: same size of the source volume. + source_volume_id = resource.Body("source_volid") #: One of the following values: creating, available, attaching, in-use #: deleting, error, error_deleting, backing-up, restoring-backup, #: error_restoring. For details on these statuses, see the #: Block Storage API documentation. status = resource.Body("status") - #: TODO(briancurtin): This is currently undocumented in the API. - attachments = resource.Body("attachments") - #: The timestamp of this volume creation. - created_at = resource.Body("created_at") - - #: The volume's current back-end. - host = resource.Body("os-vol-host-attr:host") - #: The project ID associated with current back-end. - project_id = resource.Body("os-vol-tenant-attr:tenant_id") #: The user ID associated with the volume user_id = resource.Body("user_id") - #: The status of this volume's migration (None means that a migration - #: is not currently in progress). - migration_status = resource.Body("os-vol-mig-status-attr:migstat") - #: The volume ID that this volume's name on the back-end is based on. - migration_id = resource.Body("os-vol-mig-status-attr:name_id") - #: Status of replication on this volume. - replication_status = resource.Body("replication_status") - #: Extended replication status on this volume. - extended_replication_status = resource.Body( - "os-volume-replication:extended_status") - #: ID of the consistency group. - consistency_group_id = resource.Body("consistencygroup_id") - #: Data set by the replication driver - replication_driver_data = resource.Body( - "os-volume-replication:driver_data") - #: ``True`` if this volume is encrypted, ``False`` if not. - #: *Type: bool* - is_encrypted = resource.Body("encrypted", type=format.BoolStr) + #: One or more metadata key and value pairs about image + volume_image_metadata = resource.Body("volume_image_metadata") + #: The name of the associated volume type. + volume_type = resource.Body("volume_type") def _action(self, session, body): """Preform volume actions given the message body.""" From e6fed6d220e5d42d5e2a236a03df2ff15f726c38 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 14 May 2021 10:15:22 +0200 Subject: [PATCH 2857/3836] Add scheduler hints on the block_storage.volume resource Scheduler hints on the volume are available in the cloud layer, but not on the resource. Add it now. Change-Id: I44844dcc556e5cca2ad5cb0eb6f0682ec15a1fd3 --- openstack/block_storage/v2/volume.py | 2 ++ openstack/block_storage/v3/volume.py | 2 ++ openstack/tests/unit/block_storage/v2/test_volume.py | 10 +++++++++- openstack/tests/unit/block_storage/v3/test_volume.py | 10 +++++++++- 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/openstack/block_storage/v2/volume.py b/openstack/block_storage/v2/volume.py index 696df7d92..7ea736071 100644 --- a/openstack/block_storage/v2/volume.py +++ b/openstack/block_storage/v2/volume.py @@ -69,6 +69,8 @@ class Volume(resource.Resource): "os-volume-replication:driver_data") #: Status of replication on this volume. replication_status = resource.Body("replication_status") + #: Scheduler hints for the volume + scheduler_hints = resource.Body('OS-SCH-HNT:scheduler_hints', type=dict) #: The size of the volume, in GBs. *Type: int* size = resource.Body("size", type=int) #: To create a volume from an existing snapshot, specify the ID of diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index 02d81d0e1..9004e4c4e 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -69,6 +69,8 @@ class Volume(resource.Resource): "os-volume-replication:driver_data") #: Status of replication on this volume. replication_status = resource.Body("replication_status") + #: Scheduler hints for the volume + scheduler_hints = resource.Body('OS-SCH-HNT:scheduler_hints', type=dict) #: The size of the volume, in GBs. *Type: int* size = resource.Body("size", type=int) #: To create a volume from an existing snapshot, specify the ID of diff --git a/openstack/tests/unit/block_storage/v2/test_volume.py b/openstack/tests/unit/block_storage/v2/test_volume.py index d866ba640..b95525c19 100644 --- a/openstack/tests/unit/block_storage/v2/test_volume.py +++ b/openstack/tests/unit/block_storage/v2/test_volume.py @@ -50,7 +50,13 @@ "consistencygroup_id": "123asf-asdf123", "os-volume-replication:driver_data": "ahasadfasdfasdfasdfsdf", "snapshot_id": "93c2e2aa-7744-4fd6-a31a-80c4726b08d7", - "encrypted": "false" + "encrypted": "false", + "OS-SCH-HNT:scheduler_hints": { + "same_host": [ + "a0cf03a5-d921-4877-bb5c-86d26cf818e1", + "8c19174f-4220-44f0-824a-cd1eeef10287" + ] + } } @@ -116,6 +122,8 @@ def test_create(self): sot.consistency_group_id) self.assertEqual(VOLUME["os-volume-replication:driver_data"], sot.replication_driver_data) + self.assertDictEqual(VOLUME["OS-SCH-HNT:scheduler_hints"], + sot.scheduler_hints) self.assertFalse(sot.is_encrypted) def test_extend(self): diff --git a/openstack/tests/unit/block_storage/v3/test_volume.py b/openstack/tests/unit/block_storage/v3/test_volume.py index b6a4fc836..002c9467a 100644 --- a/openstack/tests/unit/block_storage/v3/test_volume.py +++ b/openstack/tests/unit/block_storage/v3/test_volume.py @@ -50,7 +50,13 @@ "consistencygroup_id": "123asf-asdf123", "os-volume-replication:driver_data": "ahasadfasdfasdfasdfsdf", "snapshot_id": "93c2e2aa-7744-4fd6-a31a-80c4726b08d7", - "encrypted": "false" + "encrypted": "false", + "OS-SCH-HNT:scheduler_hints": { + "same_host": [ + "a0cf03a5-d921-4877-bb5c-86d26cf818e1", + "8c19174f-4220-44f0-824a-cd1eeef10287" + ] + } } @@ -117,6 +123,8 @@ def test_create(self): self.assertEqual(VOLUME["os-volume-replication:driver_data"], sot.replication_driver_data) self.assertFalse(sot.is_encrypted) + self.assertDictEqual(VOLUME["OS-SCH-HNT:scheduler_hints"], + sot.scheduler_hints) def test_extend(self): sot = volume.Volume(**VOLUME) From a358278660fa6690e3a4f9d95db74f4386e315f1 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 14 May 2021 10:19:47 +0200 Subject: [PATCH 2858/3836] Fix active status for block storage In the wait_for_status for block storage we use ACTIVE, while in the service "available" is the proper value to check. Also add the test for this. Change-Id: Ic4a1c0d91139a8f4d192363e5e6f399dc2304939 --- openstack/block_storage/v2/_proxy.py | 4 ++-- openstack/block_storage/v3/_proxy.py | 4 ++-- openstack/tests/unit/block_storage/v2/test_proxy.py | 7 +++++++ openstack/tests/unit/block_storage/v3/test_proxy.py | 7 +++++++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 212e0220c..e9322b5ef 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -290,7 +290,7 @@ def restore_backup(self, backup, volume_id, name): backup = self._get_resource(_backup.Backup, backup) return backup.restore(self, volume_id=volume_id, name=name) - def wait_for_status(self, res, status='ACTIVE', failures=None, + def wait_for_status(self, res, status='available', failures=None, interval=2, wait=120): """Wait for a resource to be in a particular status. @@ -312,7 +312,7 @@ def wait_for_status(self, res, status='ACTIVE', failures=None, :raises: :class:`~AttributeError` if the resource does not have a ``status`` attribute. """ - failures = ['Error'] if failures is None else failures + failures = ['error'] if failures is None else failures return resource.wait_for_status( self, res, status, failures, interval, wait) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index b040e9815..2ece19054 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -649,7 +649,7 @@ def update_group_type(self, group_type, **attrs): return self._update( _group_type.GroupType, group_type, **attrs) - def wait_for_status(self, res, status='ACTIVE', failures=None, + def wait_for_status(self, res, status='available', failures=None, interval=2, wait=120): """Wait for a resource to be in a particular status. @@ -671,7 +671,7 @@ def wait_for_status(self, res, status='ACTIVE', failures=None, :raises: :class:`~AttributeError` if the resource does not have a ``status`` attribute. """ - failures = ['Error'] if failures is None else failures + failures = ['error'] if failures is None else failures return resource.wait_for_status( self, res, status, failures, interval, wait) diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index ffb49e030..40c7a2233 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -150,3 +150,10 @@ def test_backup_restore(self): expected_args=[self.proxy], expected_kwargs={'volume_id': 'vol_id', 'name': 'name'} ) + + def test_volume_wait_for(self): + value = volume.Volume(id='1234') + self.verify_wait_for_status( + self.proxy.wait_for_status, + method_args=[value], + expected_args=[self.proxy, value, 'available', ['error'], 2, 120]) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 044832a7a..657209d16 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -289,3 +289,10 @@ def test_group_type_update(self): def test_extensions(self): self.verify_list(self.proxy.extensions, extension.Extension) + + def test_volume_wait_for(self): + value = volume.Volume(id='1234') + self.verify_wait_for_status( + self.proxy.wait_for_status, + method_args=[value], + expected_args=[self.proxy, value, 'available', ['error'], 2, 120]) From d1c9a96ef61f87de11c7ce4dcb9e3f44e805d972 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 14 May 2021 10:48:43 +0200 Subject: [PATCH 2859/3836] Copy find_volume to block_storage.v2 proxy Switch of the cloud layer for BS requires this method for v2 also. Change-Id: I3d6fa56ec61c8e4f471f3756193df57c43876eb8 --- openstack/block_storage/v2/_proxy.py | 15 +++++++++++++++ .../tests/unit/block_storage/v2/test_proxy.py | 3 +++ 2 files changed, 18 insertions(+) diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index e9322b5ef..0ff1702b7 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -143,6 +143,21 @@ def get_volume(self, volume): """ return self._get(_volume.Volume, volume) + def find_volume(self, name_or_id, ignore_missing=True, **attrs): + """Find a single volume + + :param snapshot: The name or ID a volume + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the volume does not exist. + + :returns: One :class:`~openstack.volume.v2.volume.Volume` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._find(_volume.Volume, name_or_id, + ignore_missing=ignore_missing) + def volumes(self, details=True, **query): """Retrieve a generator of volumes diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index 40c7a2233..03f33af41 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -67,6 +67,9 @@ def test_type_delete_ignore(self): def test_volume_get(self): self.verify_get(self.proxy.get_volume, volume.Volume) + def test_volume_find(self): + self.verify_find(self.proxy.find_volume, volume.Volume) + def test_volumes_detailed(self): self.verify_list(self.proxy.volumes, volume.Volume, method_kwargs={"details": True, "query": 1}, From f9183cf481981a4ea74af9b4a321b631bf41baa5 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 14 May 2021 16:37:24 +0200 Subject: [PATCH 2860/3836] Add block_storage.volume actions Before we can switch BS cloud layer need to add missing volume actions. Do this for both v2 and v3 (v2 differs slightly). Change-Id: Iee4c137ea703d7adec4f8f1e7a3585a1d01e6d19 --- openstack/block_storage/v2/_proxy.py | 152 +++++++- openstack/block_storage/v2/volume.py | 84 +++- openstack/block_storage/v3/_proxy.py | 260 ++++++++++++- openstack/block_storage/v3/volume.py | 178 ++++++++- .../tests/unit/block_storage/v2/test_proxy.py | 104 ++++- .../unit/block_storage/v2/test_volume.py | 175 ++++++++- .../tests/unit/block_storage/v3/test_proxy.py | 232 ++++++++++-- .../unit/block_storage/v3/test_volume.py | 358 ++++++++++++++++-- 8 files changed, 1439 insertions(+), 104 deletions(-) diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 0ff1702b7..3dafa7550 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -189,26 +189,32 @@ def create_volume(self, **attrs): """ return self._create(_volume.Volume, **attrs) - def delete_volume(self, volume, ignore_missing=True): + def delete_volume(self, volume, ignore_missing=True, force=False): """Delete a volume :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v2.volume.Volume` instance. + :class:`~openstack.volume.v2.volume.Volume` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the volume does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent volume. + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the volume does not exist. When set to ``True``, no + exception will be set when attempting to delete a nonexistent + volume. + :param bool force: Whether to try forcing volume deletion. :returns: ``None`` """ - self._delete(_volume.Volume, volume, ignore_missing=ignore_missing) + if not force: + self._delete(_volume.Volume, volume, ignore_missing=ignore_missing) + else: + volume = self._get_resource(_volume.Volume, volume) + volume.force_delete(self) + # ====== VOLUME ACTIONS ====== def extend_volume(self, volume, size): """Extend a volume :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v2.volume.Volume` instance. + :class:`~openstack.volume.v2.volume.Volume` instance. :param size: New volume size :returns: None @@ -216,6 +222,136 @@ def extend_volume(self, volume, size): volume = self._get_resource(_volume.Volume, volume) volume.extend(self, size) + def retype_volume(self, volume, new_type, migration_policy="never"): + """Retype the volume. + + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.volume.v2.volume.Volume` instance. + :param str new_type: The new volume type that volume is changed with. + :param str migration_policy: Specify if the volume should be migrated + when it is re-typed. Possible values are on-demand or never. + Default: never. + + :returns: None + """ + volume = self._get_resource(_volume.Volume, volume) + volume.retype(self, new_type, migration_policy) + + def set_volume_bootable_status(self, volume, bootable): + """Set bootable status of the volume. + + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.volume.v2.volume.Volume` instance. + :param bool bootable: Specifies whether the volume should be bootable + or not. + + :returns: None + """ + volume = self._get_resource(_volume.Volume, volume) + volume.set_bootable_status(self, bootable) + + def reset_volume_status( + self, volume, status, attach_status, migration_status + ): + """Reset volume statuses. + + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.volume.v2.volume.Volume` instance. + :param str status: The new volume status. + :param str attach_status: The new volume attach status. + :param str migration_status: The new volume migration status (admin + only). + + :returns: None + """ + volume = self._get_resource(_volume.Volume, volume) + volume.reset_status(self, status, attach_status, migration_status) + + def attach_volume( + self, volume, mountpoint, instance=None, host_name=None + ): + """Attaches a volume to a server. + + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.volume.v2.volume.Volume` instance. + :param str mountpoint: The attaching mount point. + :param str instance: The UUID of the attaching instance. + :param str host_name: The name of the attaching host. + + :returns: None + """ + volume = self._get_resource(_volume.Volume, volume) + volume.attach(self, mountpoint, instance, host_name) + + def detach_volume( + self, volume, attachment, force=False, connector=None + ): + """Detaches a volume from a server. + + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.volume.v2.volume.Volume` instance. + :param str attachment: The ID of the attachment. + :param bool force: Whether to force volume detach (Rolls back an + unsuccessful detach operation after you disconnect the volume.) + :param dict connector: The connector object. + + :returns: None + """ + volume = self._get_resource(_volume.Volume, volume) + volume.detach(self, attachment, force, connector) + + def unmanage_volume(self, volume): + """Removes a volume from Block Storage management without removing the + back-end storage object that is associated with it. + + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.volume.v2.volume.Volume` instance. + + :returns: None """ + volume = self._get_resource(_volume.Volume, volume) + volume.unmanage(self) + + def migrate_volume( + self, volume, host=None, force_host_copy=False, + lock_volume=False + ): + """Migrates a volume to the specified host. + + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.volume.v2.volume.Volume` instance. + :param str host: The target host for the volume migration. Host + format is host@backend. + :param bool force_host_copy: If false (the default), rely on the volume + backend driver to perform the migration, which might be optimized. + If true, or the volume driver fails to migrate the volume itself, + a generic host-based migration is performed. + :param bool lock_volume: If true, migrating an available volume will + change its status to maintenance preventing other operations from + being performed on the volume such as attach, detach, retype, etc. + + :returns: None + """ + volume = self._get_resource(_volume.Volume, volume) + volume.migrate(self, host, force_host_copy, lock_volume) + + def complete_volume_migration( + self, volume, new_volume, error=False + ): + """Complete the migration of a volume. + + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.volume.v2.volume.Volume` instance. + :param str new_volume: The UUID of the new volume. + :param bool error: Used to indicate if an error has occured elsewhere + that requires clean up. + + :returns: None + """ + volume = self._get_resource(_volume.Volume, volume) + volume.complete_migration(self, new_volume, error) + + # ====== BACKEND POOLS ====== + def backend_pools(self, **query): """Returns a generator of cinder Back-end storage pools diff --git a/openstack/block_storage/v2/volume.py b/openstack/block_storage/v2/volume.py index 7ea736071..087dd4a8f 100644 --- a/openstack/block_storage/v2/volume.py +++ b/openstack/block_storage/v2/volume.py @@ -99,13 +99,93 @@ def _action(self, session, body): # as both Volume and VolumeDetail instances can be acted on, but # the URL used is sans any additional /detail/ part. url = utils.urljoin(Volume.base_path, self.id, 'action') - headers = {'Accept': ''} - return session.post(url, json=body, headers=headers) + return session.post(url, json=body, microversion=None) def extend(self, session, size): """Extend a volume size.""" body = {'os-extend': {'new_size': size}} self._action(session, body) + def set_bootable_status(self, session, bootable=True): + """Set volume bootable status flag""" + body = {'os-set_bootable': {'bootable': bootable}} + self._action(session, body) + + def reset_status( + self, session, status, attach_status, migration_status + ): + """Reset volume statuses (admin operation)""" + body = {'os-reset_status': { + 'status': status, + 'attach_status': attach_status, + 'migration_status': migration_status + }} + self._action(session, body) + + def attach( + self, session, mountpoint, instance + ): + """Attach volume to server""" + body = {'os-attach': { + 'mountpoint': mountpoint, + 'instance_uuid': instance}} + + self._action(session, body) + + def detach(self, session, attachment, force=False): + """Detach volume from server""" + if not force: + body = {'os-detach': {'attachment_id': attachment}} + if force: + body = {'os-force_detach': { + 'attachment_id': attachment}} + + self._action(session, body) + + def unmanage(self, session): + """Unmanage volume""" + body = {'os-unmanage': {}} + + self._action(session, body) + + def retype(self, session, new_type, migration_policy=None): + """Change volume type""" + body = {'os-retype': { + 'new_type': new_type}} + if migration_policy: + body['os-retype']['migration_policy'] = migration_policy + + self._action(session, body) + + def migrate( + self, session, host=None, force_host_copy=False, + lock_volume=False + ): + """Migrate volume""" + req = dict() + if host is not None: + req['host'] = host + if force_host_copy: + req['force_host_copy'] = force_host_copy + if lock_volume: + req['lock_volume'] = lock_volume + body = {'os-migrate_volume': req} + + self._action(session, body) + + def complete_migration(self, session, new_volume_id, error=False): + """Complete volume migration""" + body = {'os-migrate_volume_completion': { + 'new_volume': new_volume_id, + 'error': error}} + + self._action(session, body) + + def force_delete(self, session): + """Force volume deletion""" + body = {'os-force_delete': {}} + + self._action(session, body) + VolumeDetail = Volume diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 2ece19054..d5014c2d9 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -304,6 +304,7 @@ def update_type_encryption(self, encryption=None, return self._update(_type.TypeEncryption, encryption, **attrs) + # ====== VOLUMES ====== def get_volume(self, volume): """Get a single volume @@ -362,7 +363,7 @@ def create_volume(self, **attrs): """ return self._create(_volume.Volume, **attrs) - def delete_volume(self, volume, ignore_missing=True): + def delete_volume(self, volume, ignore_missing=True, force=False): """Delete a volume :param volume: The value can be either the ID of a volume or a @@ -372,16 +373,22 @@ def delete_volume(self, volume, ignore_missing=True): raised when the volume does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent volume. + :param bool force: Whether to try forcing volume deletion. :returns: ``None`` """ - self._delete(_volume.Volume, volume, ignore_missing=ignore_missing) + if not force: + self._delete(_volume.Volume, volume, ignore_missing=ignore_missing) + else: + volume = self._get_resource(_volume.Volume, volume) + volume.force_delete(self) + # ====== VOLUME ACTIONS ====== def extend_volume(self, volume, size): """Extend a volume :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v3.volume.Volume` instance. + :class:`~openstack.volume.v3.volume.Volume` instance. :param size: New volume size :returns: None @@ -392,12 +399,12 @@ def extend_volume(self, volume, size): def set_volume_readonly(self, volume, readonly=True): """Set a volume's read-only flag. - :param name_or_id: Name, unique ID of the volume or a volume dict. + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.volume.v3.volume.Volume` instance. :param bool readonly: Whether the volume should be a read-only volume - or not + or not. - :raises: OpenStackCloudTimeout if wait time exceeded. - :raises: OpenStackCloudException on operation error. + :returns: None """ volume = self._get_resource(_volume.Volume, volume) volume.set_readonly(self, readonly) @@ -405,18 +412,243 @@ def set_volume_readonly(self, volume, readonly=True): def retype_volume(self, volume, new_type, migration_policy="never"): """Retype the volume. - :param name_or_id: Name, unique ID of the volume or a volume dict. - :param new_type: The new volume type that volume is changed with. - :param migration_policy: Specify if the volume should be migrated when - it is re-typed. Possible values are on-demand - or never. Default: never. + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.volume.v3.volume.Volume` instance. + :param str new_type: The new volume type that volume is changed with. + :param str migration_policy: Specify if the volume should be migrated + when it is re-typed. Possible values are on-demand or never. + Default: never. - :raises: OpenStackCloudTimeout if wait time exceeded. - :raises: OpenStackCloudException on operation error. + :returns: None """ volume = self._get_resource(_volume.Volume, volume) volume.retype(self, new_type, migration_policy) + def set_volume_bootable_status(self, volume, bootable): + """Set bootable status of the volume. + + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.volume.v3.volume.Volume` instance. + :param bool bootable: Specifies whether the volume should be bootable + or not. + + :returns: None + """ + volume = self._get_resource(_volume.Volume, volume) + volume.set_bootable_status(self, bootable) + + def reset_volume_status( + self, volume, status, attach_status, migration_status + ): + """Reset volume statuses. + + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.volume.v3.volume.Volume` instance. + :param str status: The new volume status. + :param str attach_status: The new volume attach status. + :param str migration_status: The new volume migration status (admin + only). + + :returns: None + """ + volume = self._get_resource(_volume.Volume, volume) + volume.reset_status(self, status, attach_status, migration_status) + + def revert_volume_to_snapshot( + self, volume, snapshot + ): + """Revert a volume to its latest snapshot. + + This method only support reverting a detached volume, and the + volume status must be available. + + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.volume.v3.volume.Volume` instance. + :param snapshot: The value can be either the ID of a snapshot or a + :class:`~openstack.volume.v3.snapshot.Snapshot` instance. + + :returns: None + """ + volume = self._get_resource(_volume.Volume, volume) + snapshot = self._get_resource(_snapshot.Snapshot, snapshot) + volume.revert_to_snapshot(self, snapshot.id) + + def attach_volume( + self, volume, mountpoint, instance=None, host_name=None + ): + """Attaches a volume to a server. + + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.volume.v3.volume.Volume` instance. + :param str mountpoint: The attaching mount point. + :param str instance: The UUID of the attaching instance. + :param str host_name: The name of the attaching host. + + :returns: None + """ + volume = self._get_resource(_volume.Volume, volume) + volume.attach(self, mountpoint, instance, host_name) + + def detach_volume( + self, volume, attachment, force=False, connector=None + ): + """Detaches a volume from a server. + + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.volume.v3.volume.Volume` instance. + :param str attachment: The ID of the attachment. + :param bool force: Whether to force volume detach (Rolls back an + unsuccessful detach operation after you disconnect the volume.) + :param dict connector: The connector object. + + :returns: None + """ + volume = self._get_resource(_volume.Volume, volume) + volume.detach(self, attachment, force, connector) + + def unmanage_volume(self, volume): + """Removes a volume from Block Storage management without removing the + back-end storage object that is associated with it. + + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.volume.v3.volume.Volume` instance. + + :returns: None """ + volume = self._get_resource(_volume.Volume, volume) + volume.unmanage(self) + + def migrate_volume( + self, volume, host=None, force_host_copy=False, + lock_volume=False, cluster=None + ): + """Migrates a volume to the specified host. + + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.volume.v3.volume.Volume` instance. + :param str host: The target host for the volume migration. Host + format is host@backend. + :param bool force_host_copy: If false (the default), rely on the volume + backend driver to perform the migration, which might be optimized. + If true, or the volume driver fails to migrate the volume itself, + a generic host-based migration is performed. + :param bool lock_volume: If true, migrating an available volume will + change its status to maintenance preventing other operations from + being performed on the volume such as attach, detach, retype, etc. + :param str cluster: The target cluster for the volume migration. + Cluster format is cluster@backend. Starting with microversion + 3.16, either cluster or host must be specified. If host is + specified and is part of a cluster, the cluster is used as the + target for the migration. + + :returns: None + """ + volume = self._get_resource(_volume.Volume, volume) + volume.migrate(self, host, force_host_copy, lock_volume, cluster) + + def complete_volume_migration( + self, volume, new_volume, error=False + ): + """Complete the migration of a volume. + + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.volume.v3.volume.Volume` instance. + :param str new_volume: The UUID of the new volume. + :param bool error: Used to indicate if an error has occured elsewhere + that requires clean up. + + :returns: None + """ + volume = self._get_resource(_volume.Volume, volume) + volume.complete_migration(self, new_volume, error) + + def upload_volume_to_image( + self, volume, image_name, force=False, disk_format=None, + container_format=None, visibility=None, protected=None + ): + """Uploads the specified volume to image service. + + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.volume.v3.volume.Volume` instance. + :param str image name: The name for the new image. + :param bool force: Enables or disables upload of a volume that is + attached to an instance. + :param str disk_format: Disk format for the new image. + :param str container_format: Container format for the new image. + :param str visibility: The visibility property of the new image. + :param str protected: Whether the new image is protected. + + :returns: dictionary describing the image. + """ + volume = self._get_resource(_volume.Volume, volume) + volume.upload_to_image( + self, image_name, force=force, disk_format=disk_format, + container_format=container_format, visibility=visibility, + protected=protected + ) + + def reserve_volume(self, volume): + """Mark volume as reserved. + + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.volume.v3.volume.Volume` instance. + + :returns: None """ + volume = self._get_resource(_volume.Volume, volume) + volume.reserve(self) + + def unreserve_volume(self, volume): + """Unmark volume as reserved. + + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.volume.v3.volume.Volume` instance. + + :returns: None """ + volume = self._get_resource(_volume.Volume, volume) + volume.unreserve(self) + + def begin_volume_detaching(self, volume): + """Update volume status to 'detaching'. + + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.volume.v3.volume.Volume` instance. + + :returns: None """ + volume = self._get_resource(_volume.Volume, volume) + volume.begin_detaching(self) + + def abort_volume_detaching(self, volume): + """Update volume status to 'in-use'. + + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.volume.v3.volume.Volume` instance. + + :returns: None """ + volume = self._get_resource(_volume.Volume, volume) + volume.abort_detaching(self) + + def init_volume_attachment(self, volume, connector): + """Initialize volume attachment. + + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.volume.v3.volume.Volume` instance. + :param dict connector: The connector object. + + :returns: None """ + volume = self._get_resource(_volume.Volume, volume) + volume.init_attachment(self, connector) + + def terminate_volume_attachment(self, volume, connector): + """Update volume status to 'in-use'. + + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.volume.v3.volume.Volume` instance. + :param dict connector: The connector object. + + :returns: None """ + volume = self._get_resource(_volume.Volume, volume) + volume.terminate_attachment(self, connector) + + # ====== BACKEND POOLS ====== def backend_pools(self, **query): """Returns a generator of cinder Back-end storage pools diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index 9004e4c4e..c86f171a4 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import exceptions from openstack import format from openstack import resource from openstack import utils @@ -21,7 +22,8 @@ class Volume(resource.Resource): base_path = "/volumes" _query_mapping = resource.QueryParameters( - 'name', 'status', 'project_id', all_projects='all_tenants') + 'name', 'status', 'project_id', 'created_at', 'updated_at', + all_projects='all_tenants') # capabilities allow_fetch = True @@ -93,33 +95,185 @@ class Volume(resource.Resource): #: The name of the associated volume type. volume_type = resource.Body("volume_type") - def _action(self, session, body): + _max_microversion = "3.60" + + def _action(self, session, body, microversion=None): """Preform volume actions given the message body.""" # NOTE: This is using Volume.base_path instead of self.base_path # as both Volume and VolumeDetail instances can be acted on, but # the URL used is sans any additional /detail/ part. url = utils.urljoin(Volume.base_path, self.id, 'action') - headers = {'Accept': ''} - return session.post(url, json=body, headers=headers) + resp = session.post(url, json=body, + microversion=self._max_microversion) + exceptions.raise_from_response(resp) + return resp def extend(self, session, size): """Extend a volume size.""" body = {'os-extend': {'new_size': size}} self._action(session, body) + def set_bootable_status(self, session, bootable=True): + """Set volume bootable status flag""" + body = {'os-set_bootable': {'bootable': bootable}} + self._action(session, body) + def set_readonly(self, session, readonly): """Set volume readonly flag""" body = {'os-update_readonly_flag': {'readonly': readonly}} self._action(session, body) - def retype(self, session, new_type, migration_policy): - """Retype volume considering the migration policy""" - body = { - 'os-retype': { - 'new_type': new_type, - 'migration_policy': migration_policy - } - } + def reset_status( + self, session, status, attach_status, migration_status + ): + """Reset volume statuses (admin operation)""" + body = {'os-reset_status': { + 'status': status, + 'attach_status': attach_status, + 'migration_status': migration_status + }} + self._action(session, body) + + def revert_to_snapshot(self, session, snapshot_id): + """Revert volume to its snapshot""" + utils.require_microversion(session, "3.40") + body = {'revert': {'snapshot_id': snapshot_id}} + self._action(session, body) + + def attach( + self, session, mountpoint, instance=None, host_name=None + ): + """Attach volume to server""" + body = {'os-attach': { + 'mountpoint': mountpoint}} + + if instance is not None: + body['os-attach']['instance_uuid'] = instance + elif host_name is not None: + body['os-attach']['host_name'] = host_name + else: + raise ValueError( + 'Either instance_uuid or host_name must be specified') + + self._action(session, body) + + def detach(self, session, attachment, force=False, connector=None): + """Detach volume from server""" + if not force: + body = {'os-detach': {'attachment_id': attachment}} + if force: + body = {'os-force_detach': { + 'attachment_id': attachment}} + if connector: + body['os-force_detach']['connector'] = connector + + self._action(session, body) + + def unmanage(self, session): + """Unmanage volume""" + body = {'os-unmanage': {}} + + self._action(session, body) + + def retype(self, session, new_type, migration_policy=None): + """Change volume type""" + body = {'os-retype': { + 'new_type': new_type}} + if migration_policy: + body['os-retype']['migration_policy'] = migration_policy + + self._action(session, body) + + def migrate( + self, session, host=None, force_host_copy=False, + lock_volume=False, cluster=None + ): + """Migrate volume""" + req = dict() + if host is not None: + req['host'] = host + if force_host_copy: + req['force_host_copy'] = force_host_copy + if lock_volume: + req['lock_volume'] = lock_volume + if cluster is not None: + req['cluster'] = cluster + utils.require_microversion(session, "3.16") + body = {'os-migrate_volume': req} + + self._action(session, body) + + def complete_migration(self, session, new_volume_id, error=False): + """Complete volume migration""" + body = {'os-migrate_volume_completion': { + 'new_volume': new_volume_id, + 'error': error}} + + self._action(session, body) + + def force_delete(self, session): + """Force volume deletion""" + body = {'os-force_delete': {}} + + self._action(session, body) + + def upload_to_image( + self, session, image_name, force=False, disk_format=None, + container_format=None, visibility=None, protected=None + ): + """Upload the volume to image service""" + req = dict(image_name=image_name, force=force) + if disk_format is not None: + req['disk_format'] = disk_format + if container_format is not None: + req['container_format'] = container_format + if visibility is not None: + req['visibility'] = visibility + if protected is not None: + req['protected'] = protected + + if visibility is not None or protected is not None: + utils.require_microversion(session, "3.1") + + body = {'os-volume_upload_image': req} + + resp = self._action(session, body).json() + return resp['os-volume_upload_image'] + + def reserve(self, session): + """Reserve volume""" + body = {'os-reserve': {}} + + self._action(session, body) + + def unreserve(self, session): + """Unreserve volume""" + body = {'os-unreserve': {}} + + self._action(session, body) + + def begin_detaching(self, session): + """Update volume status to 'detaching'""" + body = {'os-begin_detaching': {}} + + self._action(session, body) + + def abort_detaching(self, session): + """Roll back volume status to 'in-use'""" + body = {'os-roll_detaching': {}} + + self._action(session, body) + + def init_attachment(self, session, connector): + """Initialize volume attachment""" + body = {'os-initialize_connection': {'connector': connector}} + + self._action(session, body) + + def terminate_attachment(self, session, connector): + """Terminate volume attachment""" + body = {'os-terminate_connection': {'connector': connector}} + self._action(session, body) diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index 03f33af41..9a6429b17 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -25,6 +25,9 @@ def setUp(self): super(TestVolumeProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) + +class TestVolume(TestVolumeProxy): + def test_snapshot_get(self): self.verify_get(self.proxy.get_snapshot, snapshot.Snapshot) @@ -90,12 +93,14 @@ def test_volume_delete(self): def test_volume_delete_ignore(self): self.verify_delete(self.proxy.delete_volume, volume.Volume, True) - def test_volume_extend(self): + def test_volume_delete_force(self): self._verify( - "openstack.block_storage.v2.volume.Volume.extend", - self.proxy.extend_volume, - method_args=["value", "new-size"], - expected_args=[self.proxy, "new-size"]) + "openstack.block_storage.v2.volume.Volume.force_delete", + self.proxy.delete_volume, + method_args=["value"], + method_kwargs={"force": True}, + expected_args=[self.proxy] + ) def test_backend_pools(self): self.verify_list(self.proxy.backend_pools, stats.Pools) @@ -160,3 +165,92 @@ def test_volume_wait_for(self): self.proxy.wait_for_status, method_args=[value], expected_args=[self.proxy, value, 'available', ['error'], 2, 120]) + + +class TestVolumeActions(TestVolumeProxy): + + def test_volume_extend(self): + self._verify( + "openstack.block_storage.v2.volume.Volume.extend", + self.proxy.extend_volume, + method_args=["value", "new-size"], + expected_args=[self.proxy, "new-size"]) + + def test_volume_set_bootable(self): + self._verify( + "openstack.block_storage.v2.volume.Volume.set_bootable_status", + self.proxy.set_volume_bootable_status, + method_args=["value", True], + expected_args=[self.proxy, True]) + + def test_volume_reset_volume_status(self): + self._verify( + "openstack.block_storage.v2.volume.Volume.reset_status", + self.proxy.reset_volume_status, + method_args=["value", '1', '2', '3'], + expected_args=[self.proxy, '1', '2', '3']) + + def test_attach_instance(self): + self._verify( + "openstack.block_storage.v2.volume.Volume.attach", + self.proxy.attach_volume, + method_args=["value", '1'], + method_kwargs={'instance': '2'}, + expected_args=[self.proxy, '1', '2', None]) + + def test_attach_host(self): + self._verify( + "openstack.block_storage.v2.volume.Volume.attach", + self.proxy.attach_volume, + method_args=["value", '1'], + method_kwargs={'host_name': '3'}, + expected_args=[self.proxy, '1', None, '3']) + + def test_detach_defaults(self): + self._verify( + "openstack.block_storage.v2.volume.Volume.detach", + self.proxy.detach_volume, + method_args=["value", '1'], + expected_args=[self.proxy, '1', False, None]) + + def test_detach_force(self): + self._verify( + "openstack.block_storage.v2.volume.Volume.detach", + self.proxy.detach_volume, + method_args=["value", '1', True, {'a': 'b'}], + expected_args=[self.proxy, '1', True, {'a': 'b'}]) + + def test_unmanage(self): + self._verify( + "openstack.block_storage.v2.volume.Volume.unmanage", + self.proxy.unmanage_volume, + method_args=["value"], + expected_args=[self.proxy]) + + def test_migrate_default(self): + self._verify( + "openstack.block_storage.v2.volume.Volume.migrate", + self.proxy.migrate_volume, + method_args=["value", '1'], + expected_args=[self.proxy, '1', False, False]) + + def test_migrate_nondefault(self): + self._verify( + "openstack.block_storage.v2.volume.Volume.migrate", + self.proxy.migrate_volume, + method_args=["value", '1', True, True], + expected_args=[self.proxy, '1', True, True]) + + def test_complete_migration(self): + self._verify( + "openstack.block_storage.v2.volume.Volume.complete_migration", + self.proxy.complete_volume_migration, + method_args=["value", '1'], + expected_args=[self.proxy, "1", False]) + + def test_complete_migration_error(self): + self._verify( + "openstack.block_storage.v2.volume.Volume.complete_migration", + self.proxy.complete_volume_migration, + method_args=["value", "1", True], + expected_args=[self.proxy, "1", True]) diff --git a/openstack/tests/unit/block_storage/v2/test_volume.py b/openstack/tests/unit/block_storage/v2/test_volume.py index b95525c19..59c61eca8 100644 --- a/openstack/tests/unit/block_storage/v2/test_volume.py +++ b/openstack/tests/unit/block_storage/v2/test_volume.py @@ -12,6 +12,8 @@ from unittest import mock +from keystoneauth1 import adapter + from openstack.block_storage.v2 import volume from openstack.tests.unit import base @@ -62,14 +64,6 @@ class TestVolume(base.TestCase): - def setUp(self): - super(TestVolume, self).setUp() - self.resp = mock.Mock() - self.resp.body = None - self.resp.json = mock.Mock(return_value=self.resp.body) - self.sess = mock.Mock() - self.sess.post = mock.Mock(return_value=self.resp) - def test_basic(self): sot = volume.Volume(VOLUME) self.assertEqual("volume", sot.resource_key) @@ -126,6 +120,20 @@ def test_create(self): sot.scheduler_hints) self.assertFalse(sot.is_encrypted) + +class TestVolumeActions(TestVolume): + + def setUp(self): + super(TestVolumeActions, self).setUp() + self.resp = mock.Mock() + self.resp.body = None + self.resp.status_code = 200 + self.resp.json = mock.Mock(return_value=self.resp.body) + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.default_microversion = '3.0' + self.sess.post = mock.Mock(return_value=self.resp) + self.sess._get_connection = mock.Mock(return_value=self.cloud) + def test_extend(self): sot = volume.Volume(**VOLUME) @@ -133,5 +141,152 @@ def test_extend(self): url = 'volumes/%s/action' % FAKE_ID body = {"os-extend": {"new_size": "20"}} - headers = {'Accept': ''} - self.sess.post.assert_called_with(url, json=body, headers=headers) + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_set_volume_bootable(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.set_bootable_status(self.sess)) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-set_bootable': {'bootable': True}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_set_volume_bootable_false(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.set_bootable_status(self.sess, False)) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-set_bootable': {'bootable': False}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_reset_status(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.reset_status(self.sess, '1', '2', '3')) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-reset_status': {'status': '1', 'attach_status': '2', + 'migration_status': '3'}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_attach_instance(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.attach(self.sess, '1', '2')) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-attach': {'mountpoint': '1', 'instance_uuid': '2'}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_detach(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.detach(self.sess, '1')) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-detach': {'attachment_id': '1'}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_detach_force(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone( + sot.detach(self.sess, '1', force=True)) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-force_detach': {'attachment_id': '1'}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_unmanage(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.unmanage(self.sess)) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-unmanage': {}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_retype(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.retype(self.sess, '1')) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-retype': {'new_type': '1'}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_retype_mp(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.retype(self.sess, '1', migration_policy='2')) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-retype': {'new_type': '1', 'migration_policy': '2'}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_migrate(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.migrate(self.sess, host='1')) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-migrate_volume': {'host': '1'}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_migrate_flags(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.migrate(self.sess, host='1', + force_host_copy=True, lock_volume=True)) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-migrate_volume': {'host': '1', 'force_host_copy': True, + 'lock_volume': True}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_complete_migration(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.complete_migration(self.sess, new_volume_id='1')) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-migrate_volume_completion': {'new_volume': '1', 'error': + False}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_complete_migration_error(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.complete_migration( + self.sess, new_volume_id='1', error=True)) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-migrate_volume_completion': {'new_volume': '1', 'error': + True}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_force_delete(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.force_delete(self.sess)) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-force_delete': {}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 657209d16..259f9b0a9 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -30,6 +30,8 @@ def setUp(self): super(TestVolumeProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) + +class TestVolume(TestVolumeProxy): def test_snapshot_get(self): self.verify_get(self.proxy.get_snapshot, snapshot.Snapshot) @@ -155,38 +157,14 @@ def test_volume_delete(self): def test_volume_delete_ignore(self): self.verify_delete(self.proxy.delete_volume, volume.Volume, True) - def test_volume_extend(self): - self._verify( - "openstack.block_storage.v3.volume.Volume.extend", - self.proxy.extend_volume, - method_args=["value", "new-size"], - expected_args=[self.proxy, "new-size"]) - - def test_volume_set_readonly_no_argument(self): + def test_volume_delete_force(self): self._verify( - "openstack.block_storage.v3.volume.Volume.set_readonly", - self.proxy.set_volume_readonly, + "openstack.block_storage.v3.volume.Volume.force_delete", + self.proxy.delete_volume, method_args=["value"], - expected_args=[self.proxy, True]) - - def test_volume_set_readonly_false(self): - self._verify( - "openstack.block_storage.v3.volume.Volume.set_readonly", - self.proxy.set_volume_readonly, - method_args=["value", False], - expected_args=[self.proxy, False]) - - def test_volume_retype_without_migration_policy(self): - self._verify("openstack.block_storage.v3.volume.Volume.retype", - self.proxy.retype_volume, - method_args=["value", "rbd"], - expected_args=[self.proxy, "rbd", "never"]) - - def test_volume_retype_with_migration_policy(self): - self._verify("openstack.block_storage.v3.volume.Volume.retype", - self.proxy.retype_volume, - method_args=["value", "rbd", "on-demand"], - expected_args=[self.proxy, "rbd", "on-demand"]) + method_kwargs={"force": True}, + expected_args=[self.proxy] + ) def test_backend_pools(self): self.verify_list(self.proxy.backend_pools, stats.Pools) @@ -296,3 +274,197 @@ def test_volume_wait_for(self): self.proxy.wait_for_status, method_args=[value], expected_args=[self.proxy, value, 'available', ['error'], 2, 120]) + + +class TestVolumeActions(TestVolumeProxy): + + def test_volume_extend(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.extend", + self.proxy.extend_volume, + method_args=["value", "new-size"], + expected_args=[self.proxy, "new-size"]) + + def test_volume_set_readonly_no_argument(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.set_readonly", + self.proxy.set_volume_readonly, + method_args=["value"], + expected_args=[self.proxy, True]) + + def test_volume_set_readonly_false(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.set_readonly", + self.proxy.set_volume_readonly, + method_args=["value", False], + expected_args=[self.proxy, False]) + + def test_volume_set_bootable(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.set_bootable_status", + self.proxy.set_volume_bootable_status, + method_args=["value", True], + expected_args=[self.proxy, True]) + + def test_volume_reset_volume_status(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.reset_status", + self.proxy.reset_volume_status, + method_args=["value", '1', '2', '3'], + expected_args=[self.proxy, '1', '2', '3']) + + def test_volume_revert_to_snapshot(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.revert_to_snapshot", + self.proxy.revert_volume_to_snapshot, + method_args=["value", '1'], + expected_args=[self.proxy, '1']) + + def test_attach_instance(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.attach", + self.proxy.attach_volume, + method_args=["value", '1'], + method_kwargs={'instance': '2'}, + expected_args=[self.proxy, '1', '2', None]) + + def test_attach_host(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.attach", + self.proxy.attach_volume, + method_args=["value", '1'], + method_kwargs={'host_name': '3'}, + expected_args=[self.proxy, '1', None, '3']) + + def test_detach_defaults(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.detach", + self.proxy.detach_volume, + method_args=["value", '1'], + expected_args=[self.proxy, '1', False, None]) + + def test_detach_force(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.detach", + self.proxy.detach_volume, + method_args=["value", '1', True, {'a': 'b'}], + expected_args=[self.proxy, '1', True, {'a': 'b'}]) + + def test_unmanage(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.unmanage", + self.proxy.unmanage_volume, + method_args=["value"], + expected_args=[self.proxy]) + + def test_migrate_default(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.migrate", + self.proxy.migrate_volume, + method_args=["value", '1'], + expected_args=[self.proxy, '1', False, False, None]) + + def test_migrate_nondefault(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.migrate", + self.proxy.migrate_volume, + method_args=["value", '1', True, True], + expected_args=[self.proxy, '1', True, True, None]) + + def test_migrate_cluster(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.migrate", + self.proxy.migrate_volume, + method_args=["value"], + method_kwargs={'cluster': '3'}, + expected_args=[self.proxy, None, False, False, '3']) + + def test_complete_migration(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.complete_migration", + self.proxy.complete_volume_migration, + method_args=["value", '1'], + expected_args=[self.proxy, "1", False]) + + def test_complete_migration_error(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.complete_migration", + self.proxy.complete_volume_migration, + method_args=["value", "1", True], + expected_args=[self.proxy, "1", True]) + + def test_upload_to_image(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.upload_to_image", + self.proxy.upload_volume_to_image, + method_args=["value", "1"], + expected_args=[self.proxy, "1"], + expected_kwargs={ + "force": False, + "disk_format": None, + "container_format": None, + "visibility": None, + "protected": None + }) + + def test_upload_to_image_extended(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.upload_to_image", + self.proxy.upload_volume_to_image, + method_args=["value", "1"], + method_kwargs={ + "disk_format": "2", + "container_format": "3", + "visibility": "4", + "protected": "5" + }, + expected_args=[self.proxy, "1"], + expected_kwargs={ + "force": False, + "disk_format": "2", + "container_format": "3", + "visibility": "4", + "protected": "5" + }) + + def test_reserve(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.reserve", + self.proxy.reserve_volume, + method_args=["value"], + expected_args=[self.proxy]) + + def test_unreserve(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.unreserve", + self.proxy.unreserve_volume, + method_args=["value"], + expected_args=[self.proxy]) + + def test_begin_detaching(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.begin_detaching", + self.proxy.begin_volume_detaching, + method_args=["value"], + expected_args=[self.proxy]) + + def test_abort_detaching(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.abort_detaching", + self.proxy.abort_volume_detaching, + method_args=["value"], + expected_args=[self.proxy]) + + def test_init_attachment(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.init_attachment", + self.proxy.init_volume_attachment, + method_args=["value", "1"], + expected_args=[self.proxy, "1"]) + + def test_terminate_attachment(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.terminate_attachment", + self.proxy.terminate_volume_attachment, + method_args=["value", "1"], + expected_args=[self.proxy, "1"]) diff --git a/openstack/tests/unit/block_storage/v3/test_volume.py b/openstack/tests/unit/block_storage/v3/test_volume.py index 002c9467a..2bf262280 100644 --- a/openstack/tests/unit/block_storage/v3/test_volume.py +++ b/openstack/tests/unit/block_storage/v3/test_volume.py @@ -12,6 +12,9 @@ from unittest import mock +from keystoneauth1 import adapter + +from openstack import exceptions from openstack.block_storage.v3 import volume from openstack.tests.unit import base @@ -62,14 +65,6 @@ class TestVolume(base.TestCase): - def setUp(self): - super(TestVolume, self).setUp() - self.resp = mock.Mock() - self.resp.body = None - self.resp.json = mock.Mock(return_value=self.resp.body) - self.sess = mock.Mock() - self.sess.post = mock.Mock(return_value=self.resp) - def test_basic(self): sot = volume.Volume(VOLUME) self.assertEqual("volume", sot.resource_key) @@ -85,6 +80,8 @@ def test_basic(self): "status": "status", "all_projects": "all_tenants", "project_id": "project_id", + "created_at": "created_at", + "updated_at": "updated_at", "limit": "limit", "marker": "marker"}, sot._query_mapping._mapping) @@ -126,6 +123,20 @@ def test_create(self): self.assertDictEqual(VOLUME["OS-SCH-HNT:scheduler_hints"], sot.scheduler_hints) + +class TestVolumeActions(TestVolume): + + def setUp(self): + super(TestVolumeActions, self).setUp() + self.resp = mock.Mock() + self.resp.body = None + self.resp.status_code = 200 + self.resp.json = mock.Mock(return_value=self.resp.body) + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.default_microversion = '3.0' + self.sess.post = mock.Mock(return_value=self.resp) + self.sess._get_connection = mock.Mock(return_value=self.cloud) + def test_extend(self): sot = volume.Volume(**VOLUME) @@ -133,8 +144,8 @@ def test_extend(self): url = 'volumes/%s/action' % FAKE_ID body = {"os-extend": {"new_size": "20"}} - headers = {'Accept': ''} - self.sess.post.assert_called_with(url, json=body, headers=headers) + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) def test_set_volume_readonly(self): sot = volume.Volume(**VOLUME) @@ -143,8 +154,8 @@ def test_set_volume_readonly(self): url = 'volumes/%s/action' % FAKE_ID body = {'os-update_readonly_flag': {'readonly': True}} - headers = {'Accept': ''} - self.sess.post.assert_called_with(url, json=body, headers=headers) + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) def test_set_volume_readonly_false(self): sot = volume.Volume(**VOLUME) @@ -153,20 +164,321 @@ def test_set_volume_readonly_false(self): url = 'volumes/%s/action' % FAKE_ID body = {'os-update_readonly_flag': {'readonly': False}} - headers = {'Accept': ''} - self.sess.post.assert_called_with(url, json=body, headers=headers) + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_set_volume_bootable(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.set_bootable_status(self.sess)) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-set_bootable': {'bootable': True}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_set_volume_bootable_false(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.set_bootable_status(self.sess, False)) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-set_bootable': {'bootable': False}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_reset_status(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.reset_status(self.sess, '1', '2', '3')) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-reset_status': {'status': '1', 'attach_status': '2', + 'migration_status': '3'}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + @mock.patch('openstack.utils.require_microversion', autospec=True, + side_effect=[exceptions.SDKException()]) + def test_revert_to_snapshot_before_340(self, mv_mock): + sot = volume.Volume(**VOLUME) + + self.assertRaises( + exceptions.SDKException, + sot.revert_to_snapshot, + self.sess, + '1' + ) + + @mock.patch('openstack.utils.require_microversion', autospec=True, + side_effect=[None]) + def test_revert_to_snapshot_after_340(self, mv_mock): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.revert_to_snapshot(self.sess, '1')) + + url = 'volumes/%s/action' % FAKE_ID + body = {'revert': {'snapshot_id': '1'}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + mv_mock.assert_called_with(self.sess, '3.40') + + def test_attach_instance(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.attach(self.sess, '1', instance='2')) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-attach': {'mountpoint': '1', 'instance_uuid': '2'}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_attach_host(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.attach(self.sess, '1', host_name='2')) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-attach': {'mountpoint': '1', 'host_name': '2'}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_attach_error(self): + sot = volume.Volume(**VOLUME) + + self.assertRaises( + ValueError, + sot.attach, + self.sess, + '1') + + def test_detach(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.detach(self.sess, '1')) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-detach': {'attachment_id': '1'}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_detach_force(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone( + sot.detach(self.sess, '1', force=True, connector={'a': 'b'})) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-force_detach': {'attachment_id': '1', + 'connector': {'a': 'b'}}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_unmanage(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.unmanage(self.sess)) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-unmanage': {}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) def test_retype(self): sot = volume.Volume(**VOLUME) - self.assertIsNone(sot.retype(self.sess, 'rbd', 'on-demand')) + self.assertIsNone(sot.retype(self.sess, '1')) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-retype': {'new_type': '1'}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_retype_mp(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.retype(self.sess, '1', migration_policy='2')) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-retype': {'new_type': '1', 'migration_policy': '2'}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_migrate(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.migrate(self.sess, host='1')) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-migrate_volume': {'host': '1'}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_migrate_flags(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.migrate(self.sess, host='1', + force_host_copy=True, lock_volume=True)) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-migrate_volume': {'host': '1', 'force_host_copy': True, + 'lock_volume': True}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + @mock.patch('openstack.utils.require_microversion', autospec=True, + side_effect=[None]) + def test_migrate_cluster(self, mv_mock): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.migrate(self.sess, cluster='1', + force_host_copy=True, lock_volume=True)) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-migrate_volume': {'cluster': '1', 'force_host_copy': True, + 'lock_volume': True}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + mv_mock.assert_called_with(self.sess, '3.16') + + def test_complete_migration(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.complete_migration(self.sess, new_volume_id='1')) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-migrate_volume_completion': {'new_volume': '1', 'error': + False}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_complete_migration_error(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.complete_migration( + self.sess, new_volume_id='1', error=True)) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-migrate_volume_completion': {'new_volume': '1', 'error': + True}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_force_delete(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.force_delete(self.sess)) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-force_delete': {}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_upload_image(self): + sot = volume.Volume(**VOLUME) + + self.resp = mock.Mock() + self.resp.body = {'os-volume_upload_image': {'a': 'b'}} + self.resp.status_code = 200 + self.resp.json = mock.Mock(return_value=self.resp.body) + self.sess.post = mock.Mock(return_value=self.resp) + + self.assertDictEqual({'a': 'b'}, sot.upload_to_image(self.sess, '1')) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-volume_upload_image': { + 'image_name': '1', + 'force': False + }} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + @mock.patch('openstack.utils.require_microversion', autospec=True, + side_effect=[None]) + def test_upload_image_args(self, mv_mock): + sot = volume.Volume(**VOLUME) + + self.resp = mock.Mock() + self.resp.body = {'os-volume_upload_image': {'a': 'b'}} + self.resp.status_code = 200 + self.resp.json = mock.Mock(return_value=self.resp.body) + self.sess.post = mock.Mock(return_value=self.resp) + + self.assertDictEqual( + {'a': 'b'}, + sot.upload_to_image(self.sess, '1', disk_format='2', + container_format='3', visibility='4', + protected='5')) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-volume_upload_image': { + 'image_name': '1', + 'force': False, + 'disk_format': '2', + 'container_format': '3', + 'visibility': '4', + 'protected': '5' + }} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + mv_mock.assert_called_with(self.sess, '3.1') + + def test_reserve(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.reserve(self.sess)) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-reserve': {}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_unreserve(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.unreserve(self.sess)) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-unreserve': {}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_begin_detaching(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.begin_detaching(self.sess)) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-begin_detaching': {}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_abort_detaching(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.abort_detaching(self.sess)) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-roll_detaching': {}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_init_attachment(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.init_attachment(self.sess, {'a': 'b'})) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-initialize_connection': {'connector': {'a': 'b'}}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_terminate_attachment(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.terminate_attachment(self.sess, {'a': 'b'})) url = 'volumes/%s/action' % FAKE_ID - body = { - 'os-retype': { - 'new_type': 'rbd', - 'migration_policy': 'on-demand' - } - } - headers = {'Accept': ''} - self.sess.post.assert_called_with(url, json=body, headers=headers) + body = {'os-terminate_connection': {'connector': {'a': 'b'}}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) From d235b1417b99bd6f34276996795f70d47042173d Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 14 May 2021 17:35:18 +0200 Subject: [PATCH 2861/3836] Switch block_storage.volume operations in cloud layer Switch volume operations in the cloud layer to use proxy layer. Change-Id: Ia192071667d9c55c96d23b90fd3c8a4ea322256c --- openstack/cloud/_block_storage.py | 148 +++-------------- openstack/tests/unit/cloud/test_caching.py | 79 +++++---- openstack/tests/unit/cloud/test_volume.py | 184 +++------------------ 3 files changed, 91 insertions(+), 320 deletions(-) diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index dc6fd140f..506fd7df9 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -14,11 +14,11 @@ # We can't just use list, because sphinx gets confused by # openstack.resource.Resource.list and openstack.resource2.Resource.list import types # noqa -import warnings from openstack.cloud import _normalize from openstack.cloud import _utils from openstack.cloud import exc +from openstack import exceptions from openstack import proxy from openstack import utils @@ -40,56 +40,7 @@ def list_volumes(self, cache=True): :returns: A list of volume ``munch.Munch``. """ - def _list(data): - volumes.extend(data.get('volumes', [])) - endpoint = None - for link in data.get('volumes_links', []): - if 'rel' in link and 'next' == link['rel']: - endpoint = link['href'] - break - if endpoint: - try: - _list(proxy._json_response( - self.block_storage.get(endpoint))) - except exc.OpenStackCloudURINotFound: - # Catch and re-raise here because we are making recursive - # calls and we just have context for the log here - self.log.debug( - "While listing volumes, could not find next link" - " {link}.".format(link=data)) - raise - - if not cache: - warnings.warn('cache argument to list_volumes is deprecated. Use ' - 'invalidate instead.') - - # Fetching paginated volumes can fails for several reasons, if - # something goes wrong we'll have to start fetching volumes from - # scratch - attempts = 5 - for _ in range(attempts): - volumes = [] - data = proxy._json_response( - self.block_storage.get('/volumes/detail')) - if 'volumes_links' not in data: - # no pagination needed - volumes.extend(data.get('volumes', [])) - break - - try: - _list(data) - break - except exc.OpenStackCloudURINotFound: - pass - else: - self.log.debug( - "List volumes failed to retrieve all volumes after" - " {attempts} attempts. Returning what we found.".format( - attempts=attempts)) - # list volumes didn't complete succesfully so just return what - # we found - return self._normalize_volumes( - self._get_and_munchify(key=None, data=volumes)) + return list(self.block_storage.volumes()) @_utils.cache_on_arguments() def list_volume_types(self, get_extra=True): @@ -138,15 +89,7 @@ def get_volume_by_id(self, id): :param id: ID of the volume. :returns: A volume ``munch.Munch``. """ - resp = self.block_storage.get('/volumes/{id}'.format(id=id)) - data = proxy._json_response( - resp, - error_message="Error getting volume with ID {id}".format(id=id) - ) - volume = self._normalize_volume( - self._get_and_munchify('volume', data)) - - return volume + return self.block_storage.get_volume(id) def get_volume_type(self, name_or_id, filters=None): """Get a volume type by name or ID. @@ -208,43 +151,20 @@ def create_volume( kwargs['imageRef'] = image_obj['id'] kwargs = self._get_volume_kwargs(kwargs) kwargs['size'] = size - payload = dict(volume=kwargs) - if 'scheduler_hints' in kwargs: - payload['OS-SCH-HNT:scheduler_hints'] = kwargs.pop( - 'scheduler_hints', None) - resp = self.block_storage.post( - '/volumes', - json=dict(payload)) - data = proxy._json_response( - resp, - error_message='Error in creating volume') - volume = self._get_and_munchify('volume', data) + + volume = self.block_storage.create_volume(**kwargs) + self.list_volumes.invalidate(self) if volume['status'] == 'error': raise exc.OpenStackCloudException("Error in creating volume") if wait: - vol_id = volume['id'] - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for the volume to be available."): - volume = self.get_volume(vol_id) - - if not volume: - continue - - if volume['status'] == 'available': - if bootable is not None: - self.set_volume_bootable(volume, bootable=bootable) - # no need to re-fetch to update the flag, just set it. - volume['bootable'] = bootable - return volume - - if volume['status'] == 'error': - raise exc.OpenStackCloudException("Error creating volume") + self.block_storage.wait_for_status(volume, wait=timeout) + if bootable: + self.block_storage.set_volume_bootable_status(volume, True) - return self._normalize_volume(volume) + return volume def update_volume(self, name_or_id, **kwargs): kwargs = self._get_volume_kwargs(kwargs) @@ -254,16 +174,12 @@ def update_volume(self, name_or_id, **kwargs): raise exc.OpenStackCloudException( "Volume %s not found." % name_or_id) - resp = self.block_storage.put( - '/volumes/{volume_id}'.format(volume_id=volume.id), - json=dict({'volume': kwargs})) - data = proxy._json_response( - resp, - error_message='Error updating volume') + volume = self.block_storage.update_volume( + volume, **kwargs) self.list_volumes.invalidate(self) - return self._normalize_volume(self._get_and_munchify('volume', data)) + return volume def set_volume_bootable(self, name_or_id, bootable=True): """Set a volume's bootable flag. @@ -283,14 +199,7 @@ def set_volume_bootable(self, name_or_id, bootable=True): "Volume {name_or_id} does not exist".format( name_or_id=name_or_id)) - resp = self.block_storage.post( - 'volumes/{id}/action'.format(id=volume['id']), - json={'os-set_bootable': {'bootable': bootable}}) - proxy._json_response( - resp, - error_message="Error setting bootable on volume {volume}".format( - volume=volume['id']) - ) + return self.block_storage.set_volume_bootable_status(volume, bootable) def delete_volume(self, name_or_id=None, wait=True, timeout=None, force=False): @@ -307,7 +216,7 @@ def delete_volume(self, name_or_id=None, wait=True, timeout=None, """ self.list_volumes.invalidate(self) - volume = self.get_volume(name_or_id) + volume = self.block_storage.find_volume(name_or_id) if not volume: self.log.debug( @@ -315,30 +224,15 @@ def delete_volume(self, name_or_id=None, wait=True, timeout=None, {'name_or_id': name_or_id}, exc_info=True) return False - - with _utils.shade_exceptions("Error in deleting volume"): - try: - if force: - proxy._json_response(self.block_storage.post( - 'volumes/{id}/action'.format(id=volume['id']), - json={'os-force_delete': None})) - else: - proxy._json_response(self.block_storage.delete( - 'volumes/{id}'.format(id=volume['id']))) - except exc.OpenStackCloudURINotFound: - self.log.debug( - "Volume {id} not found when deleting. Ignoring.".format( - id=volume['id'])) - return False + try: + self.block_storage.delete_volume(volume, force=force) + except exceptions.SDKException: + self.log.exception("error in deleting volume") + raise self.list_volumes.invalidate(self) if wait: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for the volume to be deleted."): - - if not self.get_volume(volume['id']): - break + self.block_storage.wait_for_delete(volume, wait=timeout) return True diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py index 38dd95668..e7792b0d8 100644 --- a/openstack/tests/unit/cloud/test_caching.py +++ b/openstack/tests/unit/cloud/test_caching.py @@ -17,6 +17,7 @@ import openstack import openstack.cloud +from openstack.block_storage.v3 import volume as _volume from openstack.cloud import meta from openstack.compute.v2 import flavor as _flavor from openstack import exceptions @@ -105,6 +106,12 @@ def _image_dict(self, fake_image): def _munch_images(self, fake_image): return self.cloud._normalize_images([fake_image]) + def _compare_volumes(self, exp, real): + self.assertDictEqual( + _volume.Volume(**exp).to_dict(computed=False), + real.to_dict(computed=False) + ) + def test_openstack_cloud(self): self.assertIsInstance(self.cloud, openstack.connection.Connection) @@ -216,18 +223,18 @@ def test_list_volumes(self): uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', 'detail']), json={'volumes': [fake_volume_dict, fake_volume2_dict]})]) - self.assertEqual( - [self.cloud._normalize_volume(fake_volume_dict)], - self.cloud.list_volumes()) + + for a, b in zip([fake_volume_dict], + self.cloud.list_volumes()): + self._compare_volumes(a, b) # this call should hit the cache - self.assertEqual( - [self.cloud._normalize_volume(fake_volume_dict)], - self.cloud.list_volumes()) + for a, b in zip([fake_volume_dict], + self.cloud.list_volumes()): + self._compare_volumes(a, b) self.cloud.list_volumes.invalidate(self.cloud) - self.assertEqual( - [self.cloud._normalize_volume(fake_volume_dict), - self.cloud._normalize_volume(fake_volume2_dict)], - self.cloud.list_volumes()) + for a, b in zip([fake_volume_dict, fake_volume2_dict], + self.cloud.list_volumes()): + self._compare_volumes(a, b) self.assert_calls() def test_list_volumes_creating_invalidates(self): @@ -247,13 +254,12 @@ def test_list_volumes_creating_invalidates(self): uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', 'detail']), json={'volumes': [fake_volume_dict, fake_volume2_dict]})]) - self.assertEqual( - [self.cloud._normalize_volume(fake_volume_dict)], - self.cloud.list_volumes()) - self.assertEqual( - [self.cloud._normalize_volume(fake_volume_dict), - self.cloud._normalize_volume(fake_volume2_dict)], - self.cloud.list_volumes()) + for a, b in zip([fake_volume_dict], + self.cloud.list_volumes()): + self._compare_volumes(a, b) + for a, b in zip([fake_volume_dict, fake_volume2_dict], + self.cloud.list_volumes()): + self._compare_volumes(a, b) self.assert_calls() def test_create_volume_invalidates(self): @@ -280,45 +286,52 @@ def now_deleting(request, context): json={'volume': fake_vol_creating}), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', 'detail']), - json={'volumes': [fake_volb4, fake_vol_creating]}), + 'volumev2', 'public', append=['volumes', _id]), + json={'volume': fake_vol_creating}), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', 'detail']), - json={'volumes': [fake_volb4, fake_vol_avail]}), + 'volumev2', 'public', append=['volumes', _id]), + json={'volume': fake_vol_avail}), dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', 'detail']), json={'volumes': [fake_volb4, fake_vol_avail]}), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['volumes', _id]), + json={'volume': fake_vol_avail}), dict(method='DELETE', uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', _id]), json=now_deleting), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', _id]), + status_code=404), dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', 'detail']), - json={'volumes': [fake_volb4]})]) + json={'volumes': [fake_volb4, fake_vol_avail]}), + ]) - self.assertEqual( - [self.cloud._normalize_volume(fake_volb4)], - self.cloud.list_volumes()) + for a, b in zip([fake_volb4], self.cloud.list_volumes()): + self._compare_volumes(a, b) volume = dict(display_name='junk_vol', size=1, display_description='test junk volume') - self.cloud.create_volume(wait=True, timeout=None, **volume) + self.cloud.create_volume(wait=True, timeout=2, **volume) # If cache was not invalidated, we would not see our own volume here # because the first volume was available and thus would already be # cached. - self.assertEqual( - [self.cloud._normalize_volume(fake_volb4), - self.cloud._normalize_volume(fake_vol_avail)], - self.cloud.list_volumes()) + for a, b in zip([fake_volb4, fake_vol_avail], + self.cloud.list_volumes()): + self._compare_volumes(a, b) self.cloud.delete_volume(_id) # And now delete and check same thing since list is cached as all # available - self.assertEqual( - [self.cloud._normalize_volume(fake_volb4)], - self.cloud.list_volumes()) + for a, b in zip([fake_volb4], self.cloud.list_volumes()): + self._compare_volumes(a, b) self.assert_calls() def test_list_users(self): diff --git a/openstack/tests/unit/cloud/test_volume.py b/openstack/tests/unit/cloud/test_volume.py index 1c5735595..510dcead7 100644 --- a/openstack/tests/unit/cloud/test_volume.py +++ b/openstack/tests/unit/cloud/test_volume.py @@ -14,6 +14,7 @@ import testtools import openstack.cloud +from openstack.block_storage.v3 import volume from openstack.cloud import meta from openstack.tests import fakes from openstack.tests.unit import base @@ -21,6 +22,12 @@ class TestVolume(base.TestCase): + def _compare_volumes(self, exp, real): + self.assertDictEqual( + volume.Volume(**exp).to_dict(computed=False), + real.to_dict(computed=False) + ) + def test_attach_volume(self): server = dict(id='server001') vol = {'id': 'volume001', 'status': 'available', @@ -256,15 +263,15 @@ def test_delete_volume_deletes(self): self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', 'detail']), + 'volumev2', 'public', append=['volumes', volume.id]), json={'volumes': [volume]}), dict(method='DELETE', uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', volume.id])), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', 'detail']), - json={'volumes': []})]) + 'volumev2', 'public', append=['volumes', volume.id]), + status_code=404)]) self.assertTrue(self.cloud.delete_volume(volume['id'])) self.assert_calls() @@ -276,13 +283,18 @@ def test_delete_volume_gone_away(self): self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', 'detail']), - json={'volumes': [volume]}), + 'volumev2', 'public', append=['volumes', volume.id]), + json=volume), dict(method='DELETE', uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', volume.id]), - status_code=404)]) - self.assertFalse(self.cloud.delete_volume(volume['id'])) + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', volume.id]), + status_code=404), + ]) + self.assertTrue(self.cloud.delete_volume(volume['id'])) self.assert_calls() def test_delete_volume_force(self): @@ -293,18 +305,18 @@ def test_delete_volume_force(self): self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', 'detail']), + 'volumev2', 'public', append=['volumes', volume['id']]), json={'volumes': [volume]}), dict(method='POST', uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', volume.id, 'action']), validate=dict( - json={'os-force_delete': None})), + json={'os-force_delete': {}})), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', 'detail']), - json={'volumes': []})]) + 'volumev2', 'public', append=['volumes', volume['id']]), + status_code=404)]) self.assertTrue(self.cloud.delete_volume(volume['id'], force=True)) self.assert_calls() @@ -346,142 +358,6 @@ def test_set_volume_bootable_false(self): self.cloud.set_volume_bootable(volume['id']) self.assert_calls() - def test_list_volumes_with_pagination(self): - vol1 = meta.obj_to_munch(fakes.FakeVolume('01', 'available', 'vol1')) - vol2 = meta.obj_to_munch(fakes.FakeVolume('02', 'available', 'vol2')) - self.register_uris([ - self.get_cinder_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'volumev2', 'public', - append=['volumes', 'detail']), - json={ - 'volumes': [vol1], - 'volumes_links': [ - {'href': self.get_mock_url( - 'volumev2', 'public', - append=['volumes', 'detail'], - qs_elements=['marker=01']), - 'rel': 'next'}]}), - dict(method='GET', - uri=self.get_mock_url( - 'volumev2', 'public', - append=['volumes', 'detail'], - qs_elements=['marker=01']), - json={ - 'volumes': [vol2], - 'volumes_links': [ - {'href': self.get_mock_url( - 'volumev2', 'public', - append=['volumes', 'detail'], - qs_elements=['marker=02']), - 'rel': 'next'}]}), - dict(method='GET', - uri=self.get_mock_url( - 'volumev2', 'public', - append=['volumes', 'detail'], - qs_elements=['marker=02']), - json={'volumes': []})]) - self.assertEqual( - [self.cloud._normalize_volume(vol1), - self.cloud._normalize_volume(vol2)], - self.cloud.list_volumes()) - self.assert_calls() - - def test_list_volumes_with_pagination_next_link_fails_once(self): - vol1 = meta.obj_to_munch(fakes.FakeVolume('01', 'available', 'vol1')) - vol2 = meta.obj_to_munch(fakes.FakeVolume('02', 'available', 'vol2')) - self.register_uris([ - self.get_cinder_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'volumev2', 'public', - append=['volumes', 'detail']), - json={ - 'volumes': [vol1], - 'volumes_links': [ - {'href': self.get_mock_url( - 'volumev2', 'public', - append=['volumes', 'detail'], - qs_elements=['marker=01']), - 'rel': 'next'}]}), - dict(method='GET', - uri=self.get_mock_url( - 'volumev2', 'public', - append=['volumes', 'detail'], - qs_elements=['marker=01']), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'volumev2', 'public', - append=['volumes', 'detail']), - json={ - 'volumes': [vol1], - 'volumes_links': [ - {'href': self.get_mock_url( - 'volumev2', 'public', - append=['volumes', 'detail'], - qs_elements=['marker=01']), - 'rel': 'next'}]}), - dict(method='GET', - uri=self.get_mock_url( - 'volumev2', 'public', - append=['volumes', 'detail'], - qs_elements=['marker=01']), - json={ - 'volumes': [vol2], - 'volumes_links': [ - {'href': self.get_mock_url( - 'volumev2', 'public', - append=['volumes', 'detail'], - qs_elements=['marker=02']), - 'rel': 'next'}]}), - - dict(method='GET', - uri=self.get_mock_url( - 'volumev2', 'public', - append=['volumes', 'detail'], - qs_elements=['marker=02']), - json={'volumes': []})]) - self.assertEqual( - [self.cloud._normalize_volume(vol1), - self.cloud._normalize_volume(vol2)], - self.cloud.list_volumes()) - self.assert_calls() - - def test_list_volumes_with_pagination_next_link_fails_all_attempts(self): - vol1 = meta.obj_to_munch(fakes.FakeVolume('01', 'available', 'vol1')) - uris = [self.get_cinder_discovery_mock_dict()] - attempts = 5 - for i in range(attempts): - uris.extend([ - dict(method='GET', - uri=self.get_mock_url( - 'volumev2', 'public', - append=['volumes', 'detail']), - json={ - 'volumes': [vol1], - 'volumes_links': [ - {'href': self.get_mock_url( - 'volumev2', 'public', - append=['volumes', 'detail'], - qs_elements=['marker=01']), - 'rel': 'next'}]}), - dict(method='GET', - uri=self.get_mock_url( - 'volumev2', 'public', - append=['volumes', 'detail'], - qs_elements=['marker=01']), - status_code=404)]) - self.register_uris(uris) - # Check that found volumes are returned even if pagination didn't - # complete because call to get next link 404'ed for all the allowed - # attempts - self.assertEqual( - [self.cloud._normalize_volume(vol1)], - self.cloud.list_volumes()) - self.assert_calls() - def test_get_volume_by_id(self): vol1 = meta.obj_to_munch(fakes.FakeVolume('01', 'available', 'vol1')) self.register_uris([ @@ -493,9 +369,7 @@ def test_get_volume_by_id(self): json={'volume': vol1} ) ]) - self.assertEqual( - self.cloud._normalize_volume(vol1), - self.cloud.get_volume_by_id('01')) + self._compare_volumes(vol1, self.cloud.get_volume_by_id('01')) self.assert_calls() def test_create_volume(self): @@ -511,11 +385,6 @@ def test_create_volume(self): 'size': 50, 'name': 'vol1', }})), - dict(method='GET', - uri=self.get_mock_url( - 'volumev2', 'public', - append=['volumes', 'detail']), - json={'volumes': [vol1]}), ]) self.cloud.create_volume(50, name='vol1') @@ -534,11 +403,6 @@ def test_create_bootable_volume(self): 'size': 50, 'name': 'vol1', }})), - dict(method='GET', - uri=self.get_mock_url( - 'volumev2', 'public', - append=['volumes', 'detail']), - json={'volumes': [vol1]}), dict(method='POST', uri=self.get_mock_url( 'volumev2', 'public', From 8579b09535148b37121d5ce8388b33e1075be448 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 14 May 2021 19:05:02 +0200 Subject: [PATCH 2862/3836] Add BS type access into proxy/resource layer Add block storage type access methods into the proxy and resource layer of v2 and v3. Change-Id: Id91bcb1ff39b7e5c857269de38c4a756150db72d --- openstack/block_storage/v2/_proxy.py | 40 +++++ openstack/block_storage/v2/type.py | 26 +++ openstack/block_storage/v3/_proxy.py | 39 ++++ openstack/block_storage/v3/type.py | 24 +++ .../tests/unit/block_storage/v2/test_proxy.py | 53 ++++-- .../tests/unit/block_storage/v2/test_type.py | 53 ++++++ .../tests/unit/block_storage/v3/test_proxy.py | 168 ++++++++++-------- .../tests/unit/block_storage/v3/test_type.py | 47 +++++ 8 files changed, 364 insertions(+), 86 deletions(-) diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 3dafa7550..bd6f06d95 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -85,6 +85,7 @@ def delete_snapshot(self, snapshot, ignore_missing=True): self._delete(_snapshot.Snapshot, snapshot, ignore_missing=ignore_missing) + # ====== TYPES ====== def get_type(self, type): """Get a single type @@ -131,6 +132,45 @@ def delete_type(self, type, ignore_missing=True): """ self._delete(_type.Type, type, ignore_missing=ignore_missing) + def get_type_access(self, type): + """Lists project IDs that have access to private volume type. + + :param type: The value can be either the ID of a type or a + :class:`~openstack.volume.v2.type.Type` instance. + + :returns: List of dictionaries describing projects that have access to + the specified type + """ + res = self._get_resource(_type.Type, type) + return res.get_private_access(self) + + def add_type_access(self, type, project_id): + """Adds private volume type access to a project. + + :param type: The value can be either the ID of a type or a + :class:`~openstack.volume.v2.type.Type` instance. + :param str project_id: The ID of the project. Volume Type access to + be added to this project ID. + + :returns: ``None`` + """ + res = self._get_resource(_type.Type, type) + return res.add_private_access(self, project_id) + + def remove_type_access(self, type, project_id): + """Remove private volume type access from a project. + + :param type: The value can be either the ID of a type or a + :class:`~openstack.volume.v2.type.Type` instance. + :param str project_id: The ID of the project. Volume Type access to + be removed to this project ID. + + :returns: ``None`` + """ + res = self._get_resource(_type.Type, type) + return res.remove_private_access(self, project_id) + + # ====== VOLUMES ====== def get_volume(self, volume): """Get a single volume diff --git a/openstack/block_storage/v2/type.py b/openstack/block_storage/v2/type.py index e559b4615..9a4772e08 100644 --- a/openstack/block_storage/v2/type.py +++ b/openstack/block_storage/v2/type.py @@ -10,7 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import exceptions from openstack import resource +from openstack import utils class Type(resource.Resource): @@ -31,3 +33,27 @@ class Type(resource.Resource): extra_specs = resource.Body("extra_specs", type=dict) #: a private volume-type. *Type: bool* is_public = resource.Body('os-volume-type-access:is_public', type=bool) + + def get_private_access(self, session): + url = utils.urljoin(self.base_path, self.id, "os-volume-type-access") + resp = session.get(url) + + exceptions.raise_from_response(resp) + + return resp.json().get("volume_type_access", []) + + def add_private_access(self, session, project_id): + url = utils.urljoin(self.base_path, self.id, "action") + body = {"addProjectAccess": {"project": project_id}} + + resp = session.post(url, json=body) + + exceptions.raise_from_response(resp) + + def remove_private_access(self, session, project_id): + url = utils.urljoin(self.base_path, self.id, "action") + body = {"removeProjectAccess": {"project": project_id}} + + resp = session.post(url, json=body) + + exceptions.raise_from_response(resp) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index d5014c2d9..880d04c87 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -107,6 +107,7 @@ def delete_snapshot(self, snapshot, ignore_missing=True): self._delete(_snapshot.Snapshot, snapshot, ignore_missing=ignore_missing) + # ====== TYPES ====== def get_type(self, type): """Get a single type @@ -211,6 +212,44 @@ def delete_type_extra_specs(self, type, keys): res = self._get_resource(_type.Type, type) return res.delete_extra_specs(self, keys) + def get_type_access(self, type): + """Lists project IDs that have access to private volume type. + + :param type: The value can be either the ID of a type or a + :class:`~openstack.volume.v3.type.Type` instance. + + :returns: List of dictionaries describing projects that have access to + the specified type + """ + res = self._get_resource(_type.Type, type) + return res.get_private_access(self) + + def add_type_access(self, type, project_id): + """Adds private volume type access to a project. + + :param type: The value can be either the ID of a type or a + :class:`~openstack.volume.v3.type.Type` instance. + :param str project_id: The ID of the project. Volume Type access to + be added to this project ID. + + :returns: ``None`` + """ + res = self._get_resource(_type.Type, type) + return res.add_private_access(self, project_id) + + def remove_type_access(self, type, project_id): + """Remove private volume type access from a project. + + :param type: The value can be either the ID of a type or a + :class:`~openstack.volume.v3.type.Type` instance. + :param str project_id: The ID of the project. Volume Type access to + be removed to this project ID. + + :returns: ``None`` + """ + res = self._get_resource(_type.Type, type) + return res.remove_private_access(self, project_id) + def get_type_encryption(self, volume_type_id): """Get the encryption details of a volume type diff --git a/openstack/block_storage/v3/type.py b/openstack/block_storage/v3/type.py index e2e02d146..cb757199c 100644 --- a/openstack/block_storage/v3/type.py +++ b/openstack/block_storage/v3/type.py @@ -90,6 +90,30 @@ def delete_extra_specs(self, session, keys): for key in keys: self._extra_specs(session.delete, key=key, delete=True) + def get_private_access(self, session): + url = utils.urljoin(self.base_path, self.id, "os-volume-type-access") + resp = session.get(url) + + exceptions.raise_from_response(resp) + + return resp.json().get("volume_type_access", []) + + def add_private_access(self, session, project_id): + url = utils.urljoin(self.base_path, self.id, "action") + body = {"addProjectAccess": {"project": project_id}} + + resp = session.post(url, json=body) + + exceptions.raise_from_response(resp) + + def remove_private_access(self, session, project_id): + url = utils.urljoin(self.base_path, self.id, "action") + body = {"removeProjectAccess": {"project": project_id}} + + resp = session.post(url, json=body) + + exceptions.raise_from_response(resp) + class TypeEncryption(resource.Resource): resource_key = "encryption" diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index 9a6429b17..6552c70de 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -52,21 +52,6 @@ def test_snapshot_delete_ignore(self): self.verify_delete(self.proxy.delete_snapshot, snapshot.Snapshot, True) - def test_type_get(self): - self.verify_get(self.proxy.get_type, type.Type) - - def test_types(self): - self.verify_list(self.proxy.types, type.Type) - - def test_type_create_attrs(self): - self.verify_create(self.proxy.create_type, type.Type) - - def test_type_delete(self): - self.verify_delete(self.proxy.delete_type, type.Type, False) - - def test_type_delete_ignore(self): - self.verify_delete(self.proxy.delete_type, type.Type, True) - def test_volume_get(self): self.verify_get(self.proxy.get_volume, volume.Volume) @@ -254,3 +239,41 @@ def test_complete_migration_error(self): self.proxy.complete_volume_migration, method_args=["value", "1", True], expected_args=[self.proxy, "1", True]) + + +class TestType(TestVolumeProxy): + def test_type_get(self): + self.verify_get(self.proxy.get_type, type.Type) + + def test_types(self): + self.verify_list(self.proxy.types, type.Type) + + def test_type_create_attrs(self): + self.verify_create(self.proxy.create_type, type.Type) + + def test_type_delete(self): + self.verify_delete(self.proxy.delete_type, type.Type, False) + + def test_type_delete_ignore(self): + self.verify_delete(self.proxy.delete_type, type.Type, True) + + def test_type_get_private_access(self): + self._verify( + "openstack.block_storage.v2.type.Type.get_private_access", + self.proxy.get_type_access, + method_args=["value"], + expected_args=[self.proxy]) + + def test_type_add_private_access(self): + self._verify( + "openstack.block_storage.v2.type.Type.add_private_access", + self.proxy.add_type_access, + method_args=["value", "a"], + expected_args=[self.proxy, "a"]) + + def test_type_remove_private_access(self): + self._verify( + "openstack.block_storage.v2.type.Type.remove_private_access", + self.proxy.remove_type_access, + method_args=["value", "a"], + expected_args=[self.proxy, "a"]) diff --git a/openstack/tests/unit/block_storage/v2/test_type.py b/openstack/tests/unit/block_storage/v2/test_type.py index d5cf4e7a3..87148fed4 100644 --- a/openstack/tests/unit/block_storage/v2/test_type.py +++ b/openstack/tests/unit/block_storage/v2/test_type.py @@ -9,6 +9,10 @@ # 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 unittest import mock + +from keystoneauth1 import adapter + from openstack.tests.unit import base @@ -26,6 +30,18 @@ class TestType(base.TestCase): + def setUp(self): + super(TestType, self).setUp() + self.extra_specs_result = {"extra_specs": {"go": "cubs", "boo": "sox"}} + self.resp = mock.Mock() + self.resp.body = None + self.resp.status_code = 200 + self.resp.json = mock.Mock(return_value=self.resp.body) + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.default_microversion = '3.0' + self.sess.post = mock.Mock(return_value=self.resp) + self.sess._get_connection = mock.Mock(return_value=self.cloud) + def test_basic(self): sot = type.Type(**TYPE) self.assertEqual("volume_type", sot.resource_key) @@ -46,3 +62,40 @@ def test_create(self): self.assertEqual(TYPE["id"], sot.id) self.assertEqual(TYPE["extra_specs"], sot.extra_specs) self.assertEqual(TYPE["name"], sot.name) + + def test_get_private_access(self): + sot = type.Type(**TYPE) + + response = mock.Mock() + response.status_code = 200 + response.body = {"volume_type_access": [ + {"project_id": "a", "volume_type_id": "b"} + ]} + response.json = mock.Mock(return_value=response.body) + self.sess.get = mock.Mock(return_value=response) + + self.assertEqual(response.body["volume_type_access"], + sot.get_private_access(self.sess)) + + self.sess.get.assert_called_with( + "types/%s/os-volume-type-access" % sot.id) + + def test_add_private_access(self): + sot = type.Type(**TYPE) + + self.assertIsNone(sot.add_private_access(self.sess, "a")) + + url = "types/%s/action" % sot.id + body = {"addProjectAccess": {"project": "a"}} + self.sess.post.assert_called_with( + url, json=body) + + def test_remove_private_access(self): + sot = type.Type(**TYPE) + + self.assertIsNone(sot.remove_private_access(self.sess, "a")) + + url = "types/%s/action" % sot.id + body = {"removeProjectAccess": {"project": "a"}} + self.sess.post.assert_called_with( + url, json=body) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 259f9b0a9..9fffabbeb 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -60,77 +60,6 @@ def test_snapshot_delete_ignore(self): self.verify_delete(self.proxy.delete_snapshot, snapshot.Snapshot, True) - def test_type_get(self): - self.verify_get(self.proxy.get_type, type.Type) - - def test_type_find(self): - self.verify_find(self.proxy.find_type, type.Type) - - def test_types(self): - self.verify_list(self.proxy.types, type.Type) - - def test_type_create_attrs(self): - self.verify_create(self.proxy.create_type, type.Type) - - def test_type_delete(self): - self.verify_delete(self.proxy.delete_type, type.Type, False) - - def test_type_delete_ignore(self): - self.verify_delete(self.proxy.delete_type, type.Type, True) - - def test_type_update(self): - self.verify_update(self.proxy.update_type, type.Type) - - def test_type_extra_specs_update(self): - kwargs = {"a": "1", "b": "2"} - id = "an_id" - self._verify( - "openstack.block_storage.v3.type.Type.set_extra_specs", - self.proxy.update_type_extra_specs, - method_args=[id], - method_kwargs=kwargs, - method_result=type.Type.existing(id=id, - extra_specs=kwargs), - expected_args=[self.proxy], - expected_kwargs=kwargs, - expected_result=kwargs) - - def test_type_extra_specs_delete(self): - self._verify( - "openstack.block_storage.v3.type.Type.delete_extra_specs", - self.proxy.delete_type_extra_specs, - expected_result=None, - method_args=["value", "key"], - expected_args=[self.proxy, "key"]) - - def test_type_encryption_get(self): - self.verify_get(self.proxy.get_type_encryption, - type.TypeEncryption, - expected_args=[], - expected_kwargs={ - 'volume_type_id': 'resource_id', - 'requires_id': False - }) - - def test_type_encryption_create(self): - self.verify_create(self.proxy.create_type_encryption, - type.TypeEncryption, - method_kwargs={'volume_type': 'id'}, - expected_kwargs={'volume_type_id': 'id'} - ) - - def test_type_encryption_update(self): - self.verify_update(self.proxy.update_type_encryption, - type.TypeEncryption) - - def test_type_encryption_delete(self): - self.verify_delete(self.proxy.delete_type_encryption, - type.TypeEncryption, False) - - def test_type_encryption_delete_ignore(self): - self.verify_delete(self.proxy.delete_type_encryption, - type.TypeEncryption, True) - def test_volume_get(self): self.verify_get(self.proxy.get_volume, volume.Volume) @@ -468,3 +397,100 @@ def test_terminate_attachment(self): self.proxy.terminate_volume_attachment, method_args=["value", "1"], expected_args=[self.proxy, "1"]) + + +class TestType(TestVolumeProxy): + def test_type_get(self): + self.verify_get(self.proxy.get_type, type.Type) + + def test_type_find(self): + self.verify_find(self.proxy.find_type, type.Type) + + def test_types(self): + self.verify_list(self.proxy.types, type.Type) + + def test_type_create_attrs(self): + self.verify_create(self.proxy.create_type, type.Type) + + def test_type_delete(self): + self.verify_delete(self.proxy.delete_type, type.Type, False) + + def test_type_delete_ignore(self): + self.verify_delete(self.proxy.delete_type, type.Type, True) + + def test_type_update(self): + self.verify_update(self.proxy.update_type, type.Type) + + def test_type_extra_specs_update(self): + kwargs = {"a": "1", "b": "2"} + id = "an_id" + self._verify( + "openstack.block_storage.v3.type.Type.set_extra_specs", + self.proxy.update_type_extra_specs, + method_args=[id], + method_kwargs=kwargs, + method_result=type.Type.existing(id=id, + extra_specs=kwargs), + expected_args=[self.proxy], + expected_kwargs=kwargs, + expected_result=kwargs) + + def test_type_extra_specs_delete(self): + self._verify( + "openstack.block_storage.v3.type.Type.delete_extra_specs", + self.proxy.delete_type_extra_specs, + expected_result=None, + method_args=["value", "key"], + expected_args=[self.proxy, "key"]) + + def test_type_get_private_access(self): + self._verify( + "openstack.block_storage.v3.type.Type.get_private_access", + self.proxy.get_type_access, + method_args=["value"], + expected_args=[self.proxy]) + + def test_type_add_private_access(self): + self._verify( + "openstack.block_storage.v3.type.Type.add_private_access", + self.proxy.add_type_access, + method_args=["value", "a"], + expected_args=[self.proxy, "a"]) + + def test_type_remove_private_access(self): + self._verify( + "openstack.block_storage.v3.type.Type.remove_private_access", + self.proxy.remove_type_access, + method_args=["value", "a"], + expected_args=[self.proxy, "a"]) + + def test_type_encryption_get(self): + self.verify_get( + self.proxy.get_type_encryption, + type.TypeEncryption, + method_args=['value'], + expected_args=[], + expected_kwargs={ + 'volume_type_id': 'value', + 'requires_id': False + }) + + def test_type_encryption_create(self): + self.verify_create( + self.proxy.create_type_encryption, + type.TypeEncryption, + method_kwargs={'volume_type': 'id'}, + expected_kwargs={'volume_type_id': 'id'} + ) + + def test_type_encryption_update(self): + self.verify_update( + self.proxy.update_type_encryption, type.TypeEncryption) + + def test_type_encryption_delete(self): + self.verify_delete( + self.proxy.delete_type_encryption, type.TypeEncryption, False) + + def test_type_encryption_delete_ignore(self): + self.verify_delete( + self.proxy.delete_type_encryption, type.TypeEncryption, True) diff --git a/openstack/tests/unit/block_storage/v3/test_type.py b/openstack/tests/unit/block_storage/v3/test_type.py index e92fb6726..719bcd577 100644 --- a/openstack/tests/unit/block_storage/v3/test_type.py +++ b/openstack/tests/unit/block_storage/v3/test_type.py @@ -12,6 +12,8 @@ from unittest import mock +from keystoneauth1 import adapter + from openstack.tests.unit import base from openstack import exceptions @@ -33,6 +35,14 @@ class TestType(base.TestCase): def setUp(self): super(TestType, self).setUp() self.extra_specs_result = {"extra_specs": {"go": "cubs", "boo": "sox"}} + self.resp = mock.Mock() + self.resp.body = None + self.resp.status_code = 200 + self.resp.json = mock.Mock(return_value=self.resp.body) + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.default_microversion = '3.0' + self.sess.post = mock.Mock(return_value=self.resp) + self.sess._get_connection = mock.Mock(return_value=self.cloud) def test_basic(self): sot = type.Type(**TYPE) @@ -124,3 +134,40 @@ def test_delete_extra_specs_error(self): sot.delete_extra_specs, sess, [key]) + + def test_get_private_access(self): + sot = type.Type(**TYPE) + + response = mock.Mock() + response.status_code = 200 + response.body = {"volume_type_access": [ + {"project_id": "a", "volume_type_id": "b"} + ]} + response.json = mock.Mock(return_value=response.body) + self.sess.get = mock.Mock(return_value=response) + + self.assertEqual(response.body["volume_type_access"], + sot.get_private_access(self.sess)) + + self.sess.get.assert_called_with( + "types/%s/os-volume-type-access" % sot.id) + + def test_add_private_access(self): + sot = type.Type(**TYPE) + + self.assertIsNone(sot.add_private_access(self.sess, "a")) + + url = "types/%s/action" % sot.id + body = {"addProjectAccess": {"project": "a"}} + self.sess.post.assert_called_with( + url, json=body) + + def test_remove_private_access(self): + sot = type.Type(**TYPE) + + self.assertIsNone(sot.remove_private_access(self.sess, "a")) + + url = "types/%s/action" % sot.id + body = {"removeProjectAccess": {"project": "a"}} + self.sess.post.assert_called_with( + url, json=body) From 118cf73533676c9bc20d4ec42d036a5125e819af Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 14 May 2021 19:06:50 +0200 Subject: [PATCH 2863/3836] Switch BS type access operations in cloud layer Switch type access operations of the cloud layer to the proxy. Change-Id: I403a3fffc7bd5a6050eef586873c2cded89bb164 --- openstack/cloud/_block_storage.py | 38 +++---------------- .../tests/unit/cloud/test_volume_access.py | 26 +++++-------- 2 files changed, 14 insertions(+), 50 deletions(-) diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index 506fd7df9..e8ea09fa2 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -49,14 +49,7 @@ def list_volume_types(self, get_extra=True): :returns: A list of volume ``munch.Munch``. """ - resp = self.block_storage.get( - '/types', - params=dict(is_public='None')) - data = proxy._json_response( - resp, - error_message='Error fetching volume_type list') - return self._normalize_volume_types( - self._get_and_munchify('volume_types', data)) + return list(self.block_storage.types()) def get_volume(self, name_or_id, filters=None): """Get a volume by name or ID. @@ -734,14 +727,8 @@ def get_volume_type_access(self, name_or_id): raise exc.OpenStackCloudException( "VolumeType not found: %s" % name_or_id) - resp = self.block_storage.get( - '/types/{id}/os-volume-type-access'.format(id=volume_type.id)) - data = proxy._json_response( - resp, - error_message="Unable to get volume type access" - " {name}".format(name=name_or_id)) return self._normalize_volume_type_accesses( - self._get_and_munchify('volume_type_access', data)) + self.block_storage.get_type_access(volume_type)) def add_volume_type_access(self, name_or_id, project_id): """Grant access on a volume_type to a project. @@ -757,15 +744,8 @@ def add_volume_type_access(self, name_or_id, project_id): if not volume_type: raise exc.OpenStackCloudException( "VolumeType not found: %s" % name_or_id) - payload = {'project': project_id} - resp = self.block_storage.post( - '/types/{id}/action'.format(id=volume_type.id), - json=dict(addProjectAccess=payload)) - proxy._json_response( - resp, - error_message="Unable to authorize {project} " - "to use volume type {name}".format( - name=name_or_id, project=project_id)) + + self.block_storage.add_type_access(volume_type, project_id) def remove_volume_type_access(self, name_or_id, project_id): """Revoke access on a volume_type to a project. @@ -779,15 +759,7 @@ def remove_volume_type_access(self, name_or_id, project_id): if not volume_type: raise exc.OpenStackCloudException( "VolumeType not found: %s" % name_or_id) - payload = {'project': project_id} - resp = self.block_storage.post( - '/types/{id}/action'.format(id=volume_type.id), - json=dict(removeProjectAccess=payload)) - proxy._json_response( - resp, - error_message="Unable to revoke {project} " - "to use volume type {name}".format( - name=name_or_id, project=project_id)) + self.block_storage.remove_type_access(volume_type, project_id) def set_volume_quotas(self, name_or_id, **kwargs): """ Set a volume quota in a project diff --git a/openstack/tests/unit/cloud/test_volume_access.py b/openstack/tests/unit/cloud/test_volume_access.py index ec83ea4c5..f4f9ffffe 100644 --- a/openstack/tests/unit/cloud/test_volume_access.py +++ b/openstack/tests/unit/cloud/test_volume_access.py @@ -32,8 +32,7 @@ def test_list_volume_types(self): dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', - append=['types'], - qs_elements=['is_public=None']), + append=['types']), json={'volume_types': [volume_type]})]) self.assertTrue(self.cloud.list_volume_types()) self.assert_calls() @@ -46,8 +45,7 @@ def test_get_volume_type(self): dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', - append=['types'], - qs_elements=['is_public=None']), + append=['types']), json={'volume_types': [volume_type]})]) volume_type_got = self.cloud.get_volume_type(volume_type['name']) self.assertEqual(volume_type_got.id, volume_type['id']) @@ -64,8 +62,7 @@ def test_get_volume_type_access(self): dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', - append=['types'], - qs_elements=['is_public=None']), + append=['types']), json={'volume_types': [volume_type]}), dict(method='GET', uri=self.get_mock_url( @@ -90,8 +87,7 @@ def test_remove_volume_type_access(self): dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', - append=['types'], - qs_elements=['is_public=None']), + append=['types']), json={'volume_types': [volume_type]}), dict(method='GET', uri=self.get_mock_url( @@ -102,7 +98,7 @@ def test_remove_volume_type_access(self): dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', - append=['types'], qs_elements=['is_public=None']), + append=['types']), json={'volume_types': [volume_type]}), dict(method='POST', uri=self.get_mock_url( @@ -116,8 +112,7 @@ def test_remove_volume_type_access(self): dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', - append=['types'], - qs_elements=['is_public=None']), + append=['types']), json={'volume_types': [volume_type]}), dict(method='GET', uri=self.get_mock_url( @@ -147,8 +142,7 @@ def test_add_volume_type_access(self): dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', - append=['types'], - qs_elements=['is_public=None']), + append=['types']), json={'volume_types': [volume_type]}), dict(method='POST', uri=self.get_mock_url( @@ -162,8 +156,7 @@ def test_add_volume_type_access(self): dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', - append=['types'], - qs_elements=['is_public=None']), + append=['types']), json={'volume_types': [volume_type]}), dict(method='GET', uri=self.get_mock_url( @@ -187,8 +180,7 @@ def test_add_volume_type_access_missing(self): dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', - append=['types'], - qs_elements=['is_public=None']), + append=['types']), json={'volume_types': [volume_type]})]) with testtools.ExpectedException( openstack.cloud.OpenStackCloudException, From 734ae40f2871e52e16e9e8f59e3170bdd94380f1 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 17 May 2021 13:39:47 +0200 Subject: [PATCH 2864/3836] Add BS snapshot and backup actions Add snapshot and backup acitons required to switch cloud layer to proxy. Doing that fix wrong docstrings in the proxy leading to some other formatting changes. Also drop old style of SnapshotDetails class. Change-Id: I129811b694baef4fde96af8c068dec01f50a4768 --- openstack/block_storage/v2/_proxy.py | 105 +++++--- openstack/block_storage/v2/backup.py | 20 ++ openstack/block_storage/v2/snapshot.py | 28 ++- openstack/block_storage/v3/_proxy.py | 204 ++++++++++------ openstack/block_storage/v3/backup.py | 23 +- openstack/block_storage/v3/snapshot.py | 31 +++ .../unit/block_storage/v2/test_backup.py | 20 ++ .../tests/unit/block_storage/v2/test_proxy.py | 183 ++++++++------ .../unit/block_storage/v2/test_snapshot.py | 42 ++-- .../unit/block_storage/v3/test_backup.py | 20 ++ .../tests/unit/block_storage/v3/test_proxy.py | 225 +++++++++++------- .../unit/block_storage/v3/test_snapshot.py | 49 ++++ 12 files changed, 639 insertions(+), 311 deletions(-) diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index bd6f06d95..e8fe5bdec 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -21,14 +21,15 @@ class Proxy(_base_proxy.BaseBlockStorageProxy): + # ====== SNAPSHOTS ====== def get_snapshot(self, snapshot): """Get a single snapshot :param snapshot: The value can be the ID of a snapshot or a - :class:`~openstack.volume.v2.snapshot.Snapshot` + :class:`~openstack.block_storage.v2.snapshot.Snapshot` instance. - :returns: One :class:`~openstack.volume.v2.snapshot.Snapshot` + :returns: One :class:`~openstack.block_storage.v2.snapshot.Snapshot` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ @@ -53,18 +54,18 @@ def snapshots(self, details=True, **query): :returns: A generator of snapshot objects. """ - snapshot = _snapshot.SnapshotDetail if details else _snapshot.Snapshot - return self._list(snapshot, **query) + base_path = '/snapshots/detail' if details else None + return self._list(_snapshot.Snapshot, base_path=base_path, **query) def create_snapshot(self, **attrs): """Create a new snapshot from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.volume.v2.snapshot.Snapshot`, - comprised of the properties on the Snapshot class. + a :class:`~openstack.block_storage.v2.snapshot.Snapshot`, + comprised of the properties on the Snapshot class. :returns: The results of snapshot creation - :rtype: :class:`~openstack.volume.v2.snapshot.Snapshot` + :rtype: :class:`~openstack.block_storage.v2.snapshot.Snapshot` """ return self._create(_snapshot.Snapshot, **attrs) @@ -72,7 +73,7 @@ def delete_snapshot(self, snapshot, ignore_missing=True): """Delete a snapshot :param snapshot: The value can be either the ID of a snapshot or a - :class:`~openstack.volume.v2.snapshot.Snapshot` + :class:`~openstack.block_storage.v2.snapshot.Snapshot` instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be @@ -85,14 +86,27 @@ def delete_snapshot(self, snapshot, ignore_missing=True): self._delete(_snapshot.Snapshot, snapshot, ignore_missing=ignore_missing) + # ====== SNAPSHOT ACTIONS ====== + def reset_snapshot(self, snapshot, status): + """Reset status of the snapshot + + :param snapshot: The value can be either the ID of a backup or a + :class:`~openstack.block_storage.v2.snapshot.Snapshot` instance. + :param str status: New snapshot status + + :returns: None + """ + snapshot = self._get_resource(_snapshot.Snapshot, snapshot) + snapshot.reset(self, status) + # ====== TYPES ====== def get_type(self, type): """Get a single type :param type: The value can be the ID of a type or a - :class:`~openstack.volume.v2.type.Type` instance. + :class:`~openstack.block_storage.v2.type.Type` instance. - :returns: One :class:`~openstack.volume.v2.type.Type` + :returns: One :class:`~openstack.block_storage.v2.type.Type` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ @@ -109,11 +123,11 @@ def create_type(self, **attrs): """Create a new type from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.volume.v2.type.Type`, + a :class:`~openstack.block_storage.v2.type.Type`, comprised of the properties on the Type class. :returns: The results of type creation - :rtype: :class:`~openstack.volume.v2.type.Type` + :rtype: :class:`~openstack.block_storage.v2.type.Type` """ return self._create(_type.Type, **attrs) @@ -121,7 +135,7 @@ def delete_type(self, type, ignore_missing=True): """Delete a type :param type: The value can be either the ID of a type or a - :class:`~openstack.volume.v2.type.Type` instance. + :class:`~openstack.block_storage.v2.type.Type` instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the type does not exist. @@ -136,7 +150,7 @@ def get_type_access(self, type): """Lists project IDs that have access to private volume type. :param type: The value can be either the ID of a type or a - :class:`~openstack.volume.v2.type.Type` instance. + :class:`~openstack.block_storage.v2.type.Type` instance. :returns: List of dictionaries describing projects that have access to the specified type @@ -148,7 +162,7 @@ def add_type_access(self, type, project_id): """Adds private volume type access to a project. :param type: The value can be either the ID of a type or a - :class:`~openstack.volume.v2.type.Type` instance. + :class:`~openstack.block_storage.v2.type.Type` instance. :param str project_id: The ID of the project. Volume Type access to be added to this project ID. @@ -161,7 +175,7 @@ def remove_type_access(self, type, project_id): """Remove private volume type access from a project. :param type: The value can be either the ID of a type or a - :class:`~openstack.volume.v2.type.Type` instance. + :class:`~openstack.block_storage.v2.type.Type` instance. :param str project_id: The ID of the project. Volume Type access to be removed to this project ID. @@ -175,11 +189,11 @@ def get_volume(self, volume): """Get a single volume :param volume: The value can be the ID of a volume or a - :class:`~openstack.volume.v2.volume.Volume` instance. + :class:`~openstack.block_storage.v2.volume.Volume` instance. - :returns: One :class:`~openstack.volume.v2.volume.Volume` + :returns: One :class:`~openstack.block_storage.v2.volume.Volume` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_volume.Volume, volume) @@ -191,7 +205,7 @@ def find_volume(self, name_or_id, ignore_missing=True, **attrs): :class:`~openstack.exceptions.ResourceNotFound` will be raised when the volume does not exist. - :returns: One :class:`~openstack.volume.v2.volume.Volume` + :returns: One :class:`~openstack.block_storage.v2.volume.Volume` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ @@ -221,11 +235,11 @@ def create_volume(self, **attrs): """Create a new volume from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.volume.v2.volume.Volume`, - comprised of the properties on the Volume class. + a :class:`~openstack.block_storage.v2.volume.Volume`, + comprised of the properties on the Volume class. :returns: The results of volume creation - :rtype: :class:`~openstack.volume.v2.volume.Volume` + :rtype: :class:`~openstack.block_storage.v2.volume.Volume` """ return self._create(_volume.Volume, **attrs) @@ -233,7 +247,7 @@ def delete_volume(self, volume, ignore_missing=True, force=False): """Delete a volume :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v2.volume.Volume` instance. + :class:`~openstack.block_storage.v2.volume.Volume` instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the volume does not exist. When set to ``True``, no @@ -254,7 +268,7 @@ def extend_volume(self, volume, size): """Extend a volume :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v2.volume.Volume` instance. + :class:`~openstack.block_storage.v2.volume.Volume` instance. :param size: New volume size :returns: None @@ -266,7 +280,7 @@ def retype_volume(self, volume, new_type, migration_policy="never"): """Retype the volume. :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v2.volume.Volume` instance. + :class:`~openstack.block_storage.v2.volume.Volume` instance. :param str new_type: The new volume type that volume is changed with. :param str migration_policy: Specify if the volume should be migrated when it is re-typed. Possible values are on-demand or never. @@ -281,7 +295,7 @@ def set_volume_bootable_status(self, volume, bootable): """Set bootable status of the volume. :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v2.volume.Volume` instance. + :class:`~openstack.block_storage.v2.volume.Volume` instance. :param bool bootable: Specifies whether the volume should be bootable or not. @@ -296,7 +310,7 @@ def reset_volume_status( """Reset volume statuses. :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v2.volume.Volume` instance. + :class:`~openstack.block_storage.v2.volume.Volume` instance. :param str status: The new volume status. :param str attach_status: The new volume attach status. :param str migration_status: The new volume migration status (admin @@ -313,7 +327,7 @@ def attach_volume( """Attaches a volume to a server. :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v2.volume.Volume` instance. + :class:`~openstack.block_storage.v2.volume.Volume` instance. :param str mountpoint: The attaching mount point. :param str instance: The UUID of the attaching instance. :param str host_name: The name of the attaching host. @@ -329,7 +343,7 @@ def detach_volume( """Detaches a volume from a server. :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v2.volume.Volume` instance. + :class:`~openstack.block_storage.v2.volume.Volume` instance. :param str attachment: The ID of the attachment. :param bool force: Whether to force volume detach (Rolls back an unsuccessful detach operation after you disconnect the volume.) @@ -345,7 +359,7 @@ def unmanage_volume(self, volume): back-end storage object that is associated with it. :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v2.volume.Volume` instance. + :class:`~openstack.block_storage.v2.volume.Volume` instance. :returns: None """ volume = self._get_resource(_volume.Volume, volume) @@ -358,7 +372,7 @@ def migrate_volume( """Migrates a volume to the specified host. :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v2.volume.Volume` instance. + :class:`~openstack.block_storage.v2.volume.Volume` instance. :param str host: The target host for the volume migration. Host format is host@backend. :param bool force_host_copy: If false (the default), rely on the volume @@ -380,7 +394,7 @@ def complete_volume_migration( """Complete the migration of a volume. :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v2.volume.Volume` instance. + :class:`~openstack.block_storage.v2.volume.Volume` instance. :param str new_volume: The UUID of the new volume. :param bool error: Used to indicate if an error has occured elsewhere that requires clean up. @@ -402,6 +416,7 @@ def backend_pools(self, **query): """ return self._list(_stats.Pools, **query) + # ====== BACKUPS ====== def backups(self, details=True, **query): """Retrieve a generator of backups @@ -451,7 +466,7 @@ def create_backup(self, **attrs): """ return self._create(_backup.Backup, **attrs) - def delete_backup(self, backup, ignore_missing=True): + def delete_backup(self, backup, ignore_missing=True, force=False): """Delete a CloudBackup :param backup: The value can be the ID of a backup or a @@ -461,12 +476,18 @@ def delete_backup(self, backup, ignore_missing=True): the zone does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent zone. + :param bool force: Whether to try forcing backup deletion :returns: ``None`` """ - self._delete(_backup.Backup, backup, - ignore_missing=ignore_missing) + if not force: + self._delete( + _backup.Backup, backup, ignore_missing=ignore_missing) + else: + backup = self._get_resource(_backup.Backup, backup) + backup.force_delete(self) + # ====== BACKUP ACTIONS ====== def restore_backup(self, backup, volume_id, name): """Restore a Backup to volume @@ -481,6 +502,18 @@ def restore_backup(self, backup, volume_id, name): backup = self._get_resource(_backup.Backup, backup) return backup.restore(self, volume_id=volume_id, name=name) + def reset_backup(self, backup, status): + """Reset status of the backup + + :param backup: The value can be either the ID of a backup or a + :class:`~openstack.block_storage.v3.backup.Backup` instance. + :param str status: New backup status + + :returns: None + """ + backup = self._get_resource(_backup.Backup, backup) + backup.reset(self, status) + def wait_for_status(self, res, status='available', failures=None, interval=2, wait=120): """Wait for a resource to be in a particular status. diff --git a/openstack/block_storage/v2/backup.py b/openstack/block_storage/v2/backup.py index 6d87c09aa..0dd7df65e 100644 --- a/openstack/block_storage/v2/backup.py +++ b/openstack/block_storage/v2/backup.py @@ -134,6 +134,14 @@ def create(self, session, prepend_key=True, base_path=None, **params): return self.fetch(session) return self + def _action(self, session, body, microversion=None): + """Preform backup actions given the message body.""" + url = utils.urljoin(self.base_path, self.id, 'action') + resp = session.post(url, json=body, + microversion=self._max_microversion) + exceptions.raise_from_response(resp) + return resp + def restore(self, session, volume_id=None, name=None): """Restore current backup to volume @@ -156,5 +164,17 @@ def restore(self, session, volume_id=None, name=None): self._translate_response(response, has_body=False) return self + def force_delete(self, session): + """Force backup deletion + """ + body = {'os-force_delete': {}} + self._action(session, body) + + def reset(self, session, status): + """Reset the status of the backup + """ + body = {'os-reset_status': {'status': status}} + self._action(session, body) + BackupDetail = Backup diff --git a/openstack/block_storage/v2/snapshot.py b/openstack/block_storage/v2/snapshot.py index 02d953604..7c044c9f6 100644 --- a/openstack/block_storage/v2/snapshot.py +++ b/openstack/block_storage/v2/snapshot.py @@ -10,8 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import exceptions from openstack import format from openstack import resource +from openstack import utils class Snapshot(resource.Resource): @@ -47,19 +49,19 @@ class Snapshot(resource.Resource): #: The ID of the volume this snapshot was taken of. volume_id = resource.Body("volume_id") + def _action(self, session, body, microversion=None): + """Preform backup actions given the message body.""" + url = utils.urljoin(self.base_path, self.id, 'action') + resp = session.post(url, json=body, + microversion=self._max_microversion) + exceptions.raise_from_response(resp) + return resp -class SnapshotDetail(Snapshot): + def reset(self, session, status): + """Reset the status of the snapshot. + """ + body = {'os-reset_status': {'status': status}} + self._action(session, body) - base_path = "/snapshots/detail" - # capabilities - allow_fetch = False - allow_create = False - allow_delete = False - allow_commit = False - allow_list = True - - #: The percentage of completeness the snapshot is currently at. - progress = resource.Body("os-extended-snapshot-attributes:progress") - #: The project ID this snapshot is associated with. - project_id = resource.Body("os-extended-snapshot-attributes:project_id") +SnapshotDetail = Snapshot diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 880d04c87..659b8b787 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -28,14 +28,15 @@ class Proxy(_base_proxy.BaseBlockStorageProxy): + # ====== SNAPSHOTS ====== def get_snapshot(self, snapshot): """Get a single snapshot :param snapshot: The value can be the ID of a snapshot or a - :class:`~openstack.volume.v3.snapshot.Snapshot` + :class:`~openstack.block_storage.v3.snapshot.Snapshot` instance. - :returns: One :class:`~openstack.volume.v3.snapshot.Snapshot` + :returns: One :class:`~openstack.block_storage.v3.snapshot.Snapshot` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ @@ -49,7 +50,7 @@ def find_snapshot(self, name_or_id, ignore_missing=True, **attrs): :class:`~openstack.exceptions.ResourceNotFound` will be raised when the snapshot does not exist. - :returns: One :class:`~openstack.volume.v3.snapshot.Snapshot` + :returns: One :class:`~openstack.block_storage.v3.snapshot.Snapshot` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ @@ -59,8 +60,8 @@ def find_snapshot(self, name_or_id, ignore_missing=True, **attrs): def snapshots(self, details=True, **query): """Retrieve a generator of snapshots - :param bool details: When set to ``False`` - :class:`~openstack.block_storage.v3.snapshot.Snapshot` + :param bool details: When set to ``False`` :class: + `~openstack.block_storage.v3.snapshot.Snapshot` objects will be returned. The default, ``True``, will cause more attributes to be returned. :param kwargs query: Optional query parameters to be sent to limit @@ -82,39 +83,69 @@ def create_snapshot(self, **attrs): """Create a new snapshot from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.volume.v3.snapshot.Snapshot`, - comprised of the properties on the Snapshot class. + a :class:`~openstack.block_storage.v3.snapshot.Snapshot`, + comprised of the properties on the Snapshot class. :returns: The results of snapshot creation - :rtype: :class:`~openstack.volume.v3.snapshot.Snapshot` + :rtype: :class:`~openstack.block_storage.v3.snapshot.Snapshot` """ return self._create(_snapshot.Snapshot, **attrs) - def delete_snapshot(self, snapshot, ignore_missing=True): + def delete_snapshot(self, snapshot, ignore_missing=True, force=False): """Delete a snapshot :param snapshot: The value can be either the ID of a snapshot or a - :class:`~openstack.volume.v3.snapshot.Snapshot` - instance. + :class:`~openstack.block_storage.v3.snapshot.Snapshot` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the snapshot does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent snapshot. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the snapshot does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent snapshot. + :param bool force: Whether to try forcing snapshot deletion. :returns: ``None`` """ - self._delete(_snapshot.Snapshot, snapshot, - ignore_missing=ignore_missing) + if not force: + self._delete( + _snapshot.Snapshot, snapshot, ignore_missing=ignore_missing) + else: + snapshot = self._get_resource(_snapshot.Snapshot, snapshot) + snapshot.force_delete(self) + + # ====== SNAPSHOT ACTIONS ====== + def reset_snapshot(self, snapshot, status): + """Reset status of the snapshot + + :param snapshot: The value can be either the ID of a backup or a + :class:`~openstack.block_storage.v3.snapshot.Snapshot` instance. + :param str status: New snapshot status + + :returns: None + """ + snapshot = self._get_resource(_snapshot.Snapshot, snapshot) + snapshot.reset(self, status) + + def set_snapshot_status(self, snapshot, status, progress=None): + """Update fields related to the status of a snapshot. + + :param snapshot: The value can be either the ID of a backup or a + :class:`~openstack.block_storage.v3.snapshot.Snapshot` instance. + :param str status: New snapshot status + :param str progress: A percentage value for snapshot build progress. + + :returns: None + """ + snapshot = self._get_resource(_snapshot.Snapshot, snapshot) + snapshot.set_status(self, status, progress) # ====== TYPES ====== def get_type(self, type): """Get a single type :param type: The value can be the ID of a type or a - :class:`~openstack.volume.v3.type.Type` instance. + :class:`~openstack.block_storage.v3.type.Type` instance. - :returns: One :class:`~openstack.volume.v3.type.Type` + :returns: One :class:`~openstack.block_storage.v3.type.Type` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ @@ -128,7 +159,7 @@ def find_type(self, name_or_id, ignore_missing=True, **attrs): :class:`~openstack.exceptions.ResourceNotFound` will be raised when the type does not exist. - :returns: One :class:`~openstack.volume.v3.type.Type` + :returns: One :class:`~openstack.block_storage.v3.type.Type` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ @@ -146,11 +177,11 @@ def create_type(self, **attrs): """Create a new type from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.volume.v3.type.Type`, + a :class:`~openstack.block_storage.v3.type.Type`, comprised of the properties on the Type class. :returns: The results of type creation - :rtype: :class:`~openstack.volume.v3.type.Type` + :rtype: :class:`~openstack.block_storage.v3.type.Type` """ return self._create(_type.Type, **attrs) @@ -158,7 +189,7 @@ def delete_type(self, type, ignore_missing=True): """Delete a type :param type: The value can be either the ID of a type or a - :class:`~openstack.volume.v3.type.Type` instance. + :class:`~openstack.block_storage.v3.type.Type` instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the type does not exist. @@ -173,12 +204,12 @@ def update_type(self, type, **attrs): """Update a type :param type: The value can be either the ID of a type or a - :class:`~openstack.volume.v3.type.Type` instance. + :class:`~openstack.block_storage.v3.type.Type` instance. :param dict attrs: The attributes to update on the type represented by ``value``. :returns: The updated type - :rtype: :class:`~openstack.volume.v3.type.Type` + :rtype: :class:`~openstack.block_storage.v3.type.Type` """ return self._update(_type.Type, type, **attrs) @@ -186,7 +217,7 @@ def update_type_extra_specs(self, type, **attrs): """Update the extra_specs for a type :param type: The value can be either the ID of a type or a - :class:`~openstack.volume.v3.type.Type` instance. + :class:`~openstack.block_storage.v3.type.Type` instance. :param dict attrs: The extra_spec attributes to update on the type represented by ``value``. @@ -204,7 +235,7 @@ def delete_type_extra_specs(self, type, keys): Note: This method will do a HTTP DELETE request for every key in keys. :param type: The value can be either the ID of a type or a - :class:`~openstack.volume.v3.type.Type` instance. + :class:`~openstack.block_storage.v3.type.Type` instance. :param keys: The keys to delete :returns: ``None`` @@ -216,7 +247,7 @@ def get_type_access(self, type): """Lists project IDs that have access to private volume type. :param type: The value can be either the ID of a type or a - :class:`~openstack.volume.v3.type.Type` instance. + :class:`~openstack.block_storage.v3.type.Type` instance. :returns: List of dictionaries describing projects that have access to the specified type @@ -228,7 +259,7 @@ def add_type_access(self, type, project_id): """Adds private volume type access to a project. :param type: The value can be either the ID of a type or a - :class:`~openstack.volume.v3.type.Type` instance. + :class:`~openstack.block_storage.v3.type.Type` instance. :param str project_id: The ID of the project. Volume Type access to be added to this project ID. @@ -241,7 +272,7 @@ def remove_type_access(self, type, project_id): """Remove private volume type access from a project. :param type: The value can be either the ID of a type or a - :class:`~openstack.volume.v3.type.Type` instance. + :class:`~openstack.block_storage.v3.type.Type` instance. :param str project_id: The ID of the project. Volume Type access to be removed to this project ID. @@ -254,10 +285,10 @@ def get_type_encryption(self, volume_type_id): """Get the encryption details of a volume type :param volume_type_id: The value can be the ID of a type or a - :class:`~openstack.volume.v3.type.Type` + :class:`~openstack.block_storage.v3.type.Type` instance. - :returns: One :class:`~openstack.volume.v3.type.TypeEncryption` + :returns: One :class:`~openstack.block_storage.v3.type.TypeEncryption` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ @@ -271,16 +302,15 @@ def create_type_encryption(self, volume_type, **attrs): """Create new type encryption from attributes :param volume_type: The value can be the ID of a type or a - :class:`~openstack.volume.v3.type.Type` + :class:`~openstack.block_storage.v3.type.Type` instance. :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.volume.v3.type.TypeEncryption`, - comprised of the properties on the TypeEncryption - class. + a :class:`~openstack.block_storage.v3.type.TypeEncryption`, + comprised of the properties on the TypeEncryption class. :returns: The results of type encryption creation - :rtype: :class:`~openstack.volume.v3.type.TypeEncryption` + :rtype: :class:`~openstack.block_storage.v3.type.TypeEncryption` """ volume_type = self._get_resource(_type.Type, volume_type) @@ -292,12 +322,12 @@ def delete_type_encryption(self, encryption=None, """Delete type encryption attributes :param encryption: The value can be None or a - :class:`~openstack.volume.v3.type.TypeEncryption` + :class:`~openstack.block_storage.v3.type.TypeEncryption` instance. If encryption_id is None then volume_type_id must be specified. :param volume_type: The value can be the ID of a type or a - :class:`~openstack.volume.v3.type.Type` + :class:`~openstack.block_storage.v3.type.Type` instance. Required if encryption_id is None. :param bool ignore_missing: When set to ``False`` @@ -322,17 +352,17 @@ def update_type_encryption(self, encryption=None, volume_type=None, **attrs): """Update a type :param encryption: The value can be None or a - :class:`~openstack.volume.v3.type.TypeEncryption` + :class:`~openstack.block_storage.v3.type.TypeEncryption` instance. If encryption_id is None then volume_type_id must be specified. :param volume_type: The value can be the ID of a type or a - :class:`~openstack.volume.v3.type.Type` + :class:`~openstack.block_storage.v3.type.Type` instance. Required if encryption_id is None. :param dict attrs: The attributes to update on the type encryption. :returns: The updated type encryption - :rtype: :class:`~openstack.volume.v3.type.TypeEncryption` + :rtype: :class:`~openstack.block_storage.v3.type.TypeEncryption` """ if volume_type: @@ -348,11 +378,11 @@ def get_volume(self, volume): """Get a single volume :param volume: The value can be the ID of a volume or a - :class:`~openstack.volume.v3.volume.Volume` instance. + :class:`~openstack.block_storage.v3.volume.Volume` instance. - :returns: One :class:`~openstack.volume.v3.volume.Volume` + :returns: One :class:`~openstack.block_storage.v3.volume.Volume` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_volume.Volume, volume) @@ -364,7 +394,7 @@ def find_volume(self, name_or_id, ignore_missing=True, **attrs): :class:`~openstack.exceptions.ResourceNotFound` will be raised when the volume does not exist. - :returns: One :class:`~openstack.volume.v3.volume.Volume` + :returns: One :class:`~openstack.block_storage.v3.volume.Volume` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ @@ -394,11 +424,11 @@ def create_volume(self, **attrs): """Create a new volume from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.volume.v3.volume.Volume`, - comprised of the properties on the Volume class. + a :class:`~openstack.block_storage.v3.volume.Volume`, + comprised of the properties on the Volume class. :returns: The results of volume creation - :rtype: :class:`~openstack.volume.v3.volume.Volume` + :rtype: :class:`~openstack.block_storage.v3.volume.Volume` """ return self._create(_volume.Volume, **attrs) @@ -406,12 +436,12 @@ def delete_volume(self, volume, ignore_missing=True, force=False): """Delete a volume :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v3.volume.Volume` instance. + :class:`~openstack.block_storage.v3.volume.Volume` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the volume does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent volume. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the volume does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent volume. :param bool force: Whether to try forcing volume deletion. :returns: ``None`` @@ -427,7 +457,7 @@ def extend_volume(self, volume, size): """Extend a volume :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v3.volume.Volume` instance. + :class:`~openstack.block_storage.v3.volume.Volume` instance. :param size: New volume size :returns: None @@ -439,7 +469,7 @@ def set_volume_readonly(self, volume, readonly=True): """Set a volume's read-only flag. :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v3.volume.Volume` instance. + :class:`~openstack.block_storage.v3.volume.Volume` instance. :param bool readonly: Whether the volume should be a read-only volume or not. @@ -452,7 +482,7 @@ def retype_volume(self, volume, new_type, migration_policy="never"): """Retype the volume. :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v3.volume.Volume` instance. + :class:`~openstack.block_storage.v3.volume.Volume` instance. :param str new_type: The new volume type that volume is changed with. :param str migration_policy: Specify if the volume should be migrated when it is re-typed. Possible values are on-demand or never. @@ -467,7 +497,7 @@ def set_volume_bootable_status(self, volume, bootable): """Set bootable status of the volume. :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v3.volume.Volume` instance. + :class:`~openstack.block_storage.v3.volume.Volume` instance. :param bool bootable: Specifies whether the volume should be bootable or not. @@ -482,7 +512,7 @@ def reset_volume_status( """Reset volume statuses. :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v3.volume.Volume` instance. + :class:`~openstack.block_storage.v3.volume.Volume` instance. :param str status: The new volume status. :param str attach_status: The new volume attach status. :param str migration_status: The new volume migration status (admin @@ -502,9 +532,9 @@ def revert_volume_to_snapshot( volume status must be available. :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v3.volume.Volume` instance. + :class:`~openstack.block_storage.v3.volume.Volume` instance. :param snapshot: The value can be either the ID of a snapshot or a - :class:`~openstack.volume.v3.snapshot.Snapshot` instance. + :class:`~openstack.block_storage.v3.snapshot.Snapshot` instance. :returns: None """ @@ -518,7 +548,7 @@ def attach_volume( """Attaches a volume to a server. :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v3.volume.Volume` instance. + :class:`~openstack.block_storage.v3.volume.Volume` instance. :param str mountpoint: The attaching mount point. :param str instance: The UUID of the attaching instance. :param str host_name: The name of the attaching host. @@ -534,7 +564,7 @@ def detach_volume( """Detaches a volume from a server. :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v3.volume.Volume` instance. + :class:`~openstack.block_storage.v3.volume.Volume` instance. :param str attachment: The ID of the attachment. :param bool force: Whether to force volume detach (Rolls back an unsuccessful detach operation after you disconnect the volume.) @@ -550,7 +580,7 @@ def unmanage_volume(self, volume): back-end storage object that is associated with it. :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v3.volume.Volume` instance. + :class:`~openstack.block_storage.v3.volume.Volume` instance. :returns: None """ volume = self._get_resource(_volume.Volume, volume) @@ -563,7 +593,7 @@ def migrate_volume( """Migrates a volume to the specified host. :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v3.volume.Volume` instance. + :class:`~openstack.block_storage.v3.volume.Volume` instance. :param str host: The target host for the volume migration. Host format is host@backend. :param bool force_host_copy: If false (the default), rely on the volume @@ -590,7 +620,7 @@ def complete_volume_migration( """Complete the migration of a volume. :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v3.volume.Volume` instance. + :class:`~openstack.block_storage.v3.volume.Volume` instance. :param str new_volume: The UUID of the new volume. :param bool error: Used to indicate if an error has occured elsewhere that requires clean up. @@ -607,7 +637,7 @@ def upload_volume_to_image( """Uploads the specified volume to image service. :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v3.volume.Volume` instance. + :class:`~openstack.block_storage.v3.volume.Volume` instance. :param str image name: The name for the new image. :param bool force: Enables or disables upload of a volume that is attached to an instance. @@ -629,7 +659,7 @@ def reserve_volume(self, volume): """Mark volume as reserved. :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v3.volume.Volume` instance. + :class:`~openstack.block_storage.v3.volume.Volume` instance. :returns: None """ volume = self._get_resource(_volume.Volume, volume) @@ -639,7 +669,7 @@ def unreserve_volume(self, volume): """Unmark volume as reserved. :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v3.volume.Volume` instance. + :class:`~openstack.block_storage.v3.volume.Volume` instance. :returns: None """ volume = self._get_resource(_volume.Volume, volume) @@ -649,7 +679,7 @@ def begin_volume_detaching(self, volume): """Update volume status to 'detaching'. :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v3.volume.Volume` instance. + :class:`~openstack.block_storage.v3.volume.Volume` instance. :returns: None """ volume = self._get_resource(_volume.Volume, volume) @@ -659,7 +689,7 @@ def abort_volume_detaching(self, volume): """Update volume status to 'in-use'. :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v3.volume.Volume` instance. + :class:`~openstack.block_storage.v3.volume.Volume` instance. :returns: None """ volume = self._get_resource(_volume.Volume, volume) @@ -669,7 +699,7 @@ def init_volume_attachment(self, volume, connector): """Initialize volume attachment. :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v3.volume.Volume` instance. + :class:`~openstack.block_storage.v3.volume.Volume` instance. :param dict connector: The connector object. :returns: None """ @@ -680,7 +710,7 @@ def terminate_volume_attachment(self, volume, connector): """Update volume status to 'in-use'. :param volume: The value can be either the ID of a volume or a - :class:`~openstack.volume.v3.volume.Volume` instance. + :class:`~openstack.block_storage.v3.volume.Volume` instance. :param dict connector: The connector object. :returns: None """ @@ -698,6 +728,7 @@ def backend_pools(self, **query): """ return self._list(_stats.Pools, **query) + # ====== BACKUPS ====== def backups(self, details=True, **query): """Retrieve a generator of backups @@ -744,7 +775,7 @@ def find_backup(self, name_or_id, ignore_missing=True, **attrs): :class:`~openstack.exceptions.ResourceNotFound` will be raised when the backup does not exist. - :returns: One :class:`~openstack.volume.v3.backup.Backup` + :returns: One :class:`~openstack.block_storage.v3.backup.Backup` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ @@ -763,7 +794,7 @@ def create_backup(self, **attrs): """ return self._create(_backup.Backup, **attrs) - def delete_backup(self, backup, ignore_missing=True): + def delete_backup(self, backup, ignore_missing=True, force=False): """Delete a CloudBackup :param backup: The value can be the ID of a backup or a @@ -773,12 +804,18 @@ def delete_backup(self, backup, ignore_missing=True): the zone does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent zone. + :param bool force: Whether to try forcing backup deletion :returns: ``None`` """ - self._delete(_backup.Backup, backup, - ignore_missing=ignore_missing) + if not force: + self._delete( + _backup.Backup, backup, ignore_missing=ignore_missing) + else: + backup = self._get_resource(_backup.Backup, backup) + backup.force_delete(self) + # ====== BACKUP ACTIONS ====== def restore_backup(self, backup, volume_id=None, name=None): """Restore a Backup to volume @@ -793,6 +830,19 @@ def restore_backup(self, backup, volume_id=None, name=None): backup = self._get_resource(_backup.Backup, backup) return backup.restore(self, volume_id=volume_id, name=name) + def reset_backup(self, backup, status): + """Reset status of the backup + + :param backup: The value can be either the ID of a backup or a + :class:`~openstack.block_storage.v3.backup.Backup` instance. + :param str status: New backup status + + :returns: None + """ + backup = self._get_resource(_backup.Backup, backup) + backup.reset(self, status) + + # ====== LIMITS ====== def get_limits(self): """Retrieves limits diff --git a/openstack/block_storage/v3/backup.py b/openstack/block_storage/v3/backup.py index 727ae535c..d5546b7ce 100644 --- a/openstack/block_storage/v3/backup.py +++ b/openstack/block_storage/v3/backup.py @@ -144,6 +144,14 @@ def create(self, session, prepend_key=True, base_path=None, **params): return self.fetch(session) return self + def _action(self, session, body, microversion=None): + """Preform backup actions given the message body.""" + url = utils.urljoin(self.base_path, self.id, 'action') + resp = session.post(url, json=body, + microversion=self._max_microversion) + exceptions.raise_from_response(resp) + return resp + def restore(self, session, volume_id=None, name=None): """Restore current backup to volume @@ -161,10 +169,21 @@ def restore(self, session, volume_id=None, name=None): if not (volume_id or name): raise exceptions.SDKException('Either of `name` or `volume_id`' ' must be specified.') - response = session.post(url, - json=body) + response = session.post(url, json=body) self._translate_response(response, has_body=False) return self + def force_delete(self, session): + """Force backup deletion + """ + body = {'os-force_delete': {}} + self._action(session, body) + + def reset(self, session, status): + """Reset the status of the backup + """ + body = {'os-reset_status': {'status': status}} + self._action(session, body) + BackupDetail = Backup diff --git a/openstack/block_storage/v3/snapshot.py b/openstack/block_storage/v3/snapshot.py index b1361dbb1..3dac8b15f 100644 --- a/openstack/block_storage/v3/snapshot.py +++ b/openstack/block_storage/v3/snapshot.py @@ -10,8 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import exceptions from openstack import format from openstack import resource +from openstack import utils class Snapshot(resource.Resource): @@ -52,5 +54,34 @@ class Snapshot(resource.Resource): #: The ID of the volume this snapshot was taken of. volume_id = resource.Body("volume_id") + def _action(self, session, body, microversion=None): + """Preform backup actions given the message body.""" + url = utils.urljoin(self.base_path, self.id, 'action') + resp = session.post(url, json=body, + microversion=self._max_microversion) + exceptions.raise_from_response(resp) + return resp + + def force_delete(self, session): + """Force snapshot deletion. + """ + body = {'os-force_delete': {}} + self._action(session, body) + + def reset(self, session, status): + """Reset the status of the snapshot. + """ + body = {'os-reset_status': {'status': status}} + self._action(session, body) + + def set_status(self, session, status, progress=None): + """Update fields related to the status of a snapshot. + """ + body = {'os-update_snapshot_status': { + 'status': status}} + if progress is not None: + body['os-update_snapshot_status']['progress'] = progress + self._action(session, body) + SnapshotDetail = Snapshot diff --git a/openstack/tests/unit/block_storage/v2/test_backup.py b/openstack/tests/unit/block_storage/v2/test_backup.py index 8c54712ae..9b3a490fe 100644 --- a/openstack/tests/unit/block_storage/v2/test_backup.py +++ b/openstack/tests/unit/block_storage/v2/test_backup.py @@ -168,3 +168,23 @@ def test_restore_no_params(self): sot.restore, self.sess ) + + def test_force_delete(self): + sot = backup.Backup(**BACKUP) + + self.assertIsNone(sot.force_delete(self.sess)) + + url = 'backups/%s/action' % FAKE_ID + body = {'os-force_delete': {}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_reset(self): + sot = backup.Backup(**BACKUP) + + self.assertIsNone(sot.reset(self.sess, 'new_status')) + + url = 'backups/%s/action' % FAKE_ID + body = {'os-reset_status': {'status': 'new_status'}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index 6552c70de..a3c76c6f5 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -28,30 +28,6 @@ def setUp(self): class TestVolume(TestVolumeProxy): - def test_snapshot_get(self): - self.verify_get(self.proxy.get_snapshot, snapshot.Snapshot) - - def test_snapshots_detailed(self): - self.verify_list(self.proxy.snapshots, snapshot.SnapshotDetail, - method_kwargs={"details": True, "query": 1}, - expected_kwargs={"query": 1}) - - def test_snapshots_not_detailed(self): - self.verify_list(self.proxy.snapshots, snapshot.Snapshot, - method_kwargs={"details": False, "query": 1}, - expected_kwargs={"query": 1}) - - def test_snapshot_create_attrs(self): - self.verify_create(self.proxy.create_snapshot, snapshot.Snapshot) - - def test_snapshot_delete(self): - self.verify_delete(self.proxy.delete_snapshot, - snapshot.Snapshot, False) - - def test_snapshot_delete_ignore(self): - self.verify_delete(self.proxy.delete_snapshot, - snapshot.Snapshot, True) - def test_volume_get(self): self.verify_get(self.proxy.get_volume, volume.Volume) @@ -90,60 +66,6 @@ def test_volume_delete_force(self): def test_backend_pools(self): self.verify_list(self.proxy.backend_pools, stats.Pools) - def test_backups_detailed(self): - # NOTE: mock has_service - self.proxy._connection = mock.Mock() - self.proxy._connection.has_service = mock.Mock(return_value=True) - self.verify_list(self.proxy.backups, backup.Backup, - method_kwargs={"details": True, "query": 1}, - expected_kwargs={"query": 1, - "base_path": "/backups/detail"}) - - def test_backups_not_detailed(self): - # NOTE: mock has_service - self.proxy._connection = mock.Mock() - self.proxy._connection.has_service = mock.Mock(return_value=True) - self.verify_list(self.proxy.backups, backup.Backup, - method_kwargs={"details": False, "query": 1}, - expected_kwargs={"query": 1}) - - def test_backup_get(self): - # NOTE: mock has_service - self.proxy._connection = mock.Mock() - self.proxy._connection.has_service = mock.Mock(return_value=True) - self.verify_get(self.proxy.get_backup, backup.Backup) - - def test_backup_delete(self): - # NOTE: mock has_service - self.proxy._connection = mock.Mock() - self.proxy._connection.has_service = mock.Mock(return_value=True) - self.verify_delete(self.proxy.delete_backup, backup.Backup, False) - - def test_backup_delete_ignore(self): - # NOTE: mock has_service - self.proxy._connection = mock.Mock() - self.proxy._connection.has_service = mock.Mock(return_value=True) - self.verify_delete(self.proxy.delete_backup, backup.Backup, True) - - def test_backup_create_attrs(self): - # NOTE: mock has_service - self.proxy._connection = mock.Mock() - self.proxy._connection.has_service = mock.Mock(return_value=True) - self.verify_create(self.proxy.create_backup, backup.Backup) - - def test_backup_restore(self): - # NOTE: mock has_service - self.proxy._connection = mock.Mock() - self.proxy._connection.has_service = mock.Mock(return_value=True) - self._verify( - 'openstack.block_storage.v2.backup.Backup.restore', - self.proxy.restore_backup, - method_args=['volume_id'], - method_kwargs={'volume_id': 'vol_id', 'name': 'name'}, - expected_args=[self.proxy], - expected_kwargs={'volume_id': 'vol_id', 'name': 'name'} - ) - def test_volume_wait_for(self): value = volume.Volume(id='1234') self.verify_wait_for_status( @@ -241,6 +163,111 @@ def test_complete_migration_error(self): expected_args=[self.proxy, "1", True]) +class TestBackup(TestVolumeProxy): + def test_backups_detailed(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self.verify_list(self.proxy.backups, backup.Backup, + method_kwargs={"details": True, "query": 1}, + expected_kwargs={"query": 1, + "base_path": "/backups/detail"}) + + def test_backups_not_detailed(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self.verify_list(self.proxy.backups, backup.Backup, + method_kwargs={"details": False, "query": 1}, + expected_kwargs={"query": 1}) + + def test_backup_get(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self.verify_get(self.proxy.get_backup, backup.Backup) + + def test_backup_delete(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self.verify_delete(self.proxy.delete_backup, backup.Backup, False) + + def test_backup_delete_ignore(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self.verify_delete(self.proxy.delete_backup, backup.Backup, True) + + def test_backup_delete_force(self): + self._verify( + "openstack.block_storage.v2.backup.Backup.force_delete", + self.proxy.delete_backup, + method_args=["value"], + method_kwargs={"force": True}, + expected_args=[self.proxy] + ) + + def test_backup_create_attrs(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self.verify_create(self.proxy.create_backup, backup.Backup) + + def test_backup_restore(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self._verify( + 'openstack.block_storage.v2.backup.Backup.restore', + self.proxy.restore_backup, + method_args=['volume_id'], + method_kwargs={'volume_id': 'vol_id', 'name': 'name'}, + expected_args=[self.proxy], + expected_kwargs={'volume_id': 'vol_id', 'name': 'name'} + ) + + def test_backup_reset(self): + self._verify( + "openstack.block_storage.v2.backup.Backup.reset", + self.proxy.reset_backup, + method_args=["value", "new_status"], + expected_args=[self.proxy, "new_status"]) + + +class TestSnapshot(TestVolumeProxy): + def test_snapshot_get(self): + self.verify_get(self.proxy.get_snapshot, snapshot.Snapshot) + + def test_snapshots_detailed(self): + self.verify_list(self.proxy.snapshots, snapshot.SnapshotDetail, + method_kwargs={"details": True, "query": 1}, + expected_kwargs={"query": 1}) + + def test_snapshots_not_detailed(self): + self.verify_list(self.proxy.snapshots, snapshot.Snapshot, + method_kwargs={"details": False, "query": 1}, + expected_kwargs={"query": 1}) + + def test_snapshot_create_attrs(self): + self.verify_create(self.proxy.create_snapshot, snapshot.Snapshot) + + def test_snapshot_delete(self): + self.verify_delete(self.proxy.delete_snapshot, + snapshot.Snapshot, False) + + def test_snapshot_delete_ignore(self): + self.verify_delete(self.proxy.delete_snapshot, + snapshot.Snapshot, True) + + def test_reset(self): + self._verify( + "openstack.block_storage.v2.snapshot.Snapshot.reset", + self.proxy.reset_snapshot, + method_args=["value", "new_status"], + expected_args=[self.proxy, "new_status"]) + + class TestType(TestVolumeProxy): def test_type_get(self): self.verify_get(self.proxy.get_type, type.Type) diff --git a/openstack/tests/unit/block_storage/v2/test_snapshot.py b/openstack/tests/unit/block_storage/v2/test_snapshot.py index 4910b6db7..29107eac8 100644 --- a/openstack/tests/unit/block_storage/v2/test_snapshot.py +++ b/openstack/tests/unit/block_storage/v2/test_snapshot.py @@ -9,9 +9,12 @@ # 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 unittest import mock from openstack.tests.unit import base +from keystoneauth1 import adapter + from openstack.block_storage.v2 import snapshot FAKE_ID = "ffa9bc5e-1172-4021-acaf-cdcd78a9584d" @@ -71,24 +74,27 @@ def test_create_basic(self): self.assertTrue(sot.is_forced) -class TestSnapshotDetail(base.TestCase): +class TestSnapshotActions(base.TestCase): - def test_basic(self): - sot = snapshot.SnapshotDetail(DETAILED_SNAPSHOT) - self.assertIsInstance(sot, snapshot.Snapshot) - self.assertEqual("/snapshots/detail", sot.base_path) - self.assertFalse(sot.allow_fetch) - self.assertFalse(sot.allow_commit) - self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_delete) - self.assertTrue(sot.allow_list) + def setUp(self): + super(TestSnapshotActions, self).setUp() + self.resp = mock.Mock() + self.resp.body = None + self.resp.json = mock.Mock(return_value=self.resp.body) + self.resp.headers = {} + self.resp.status_code = 202 + + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.get = mock.Mock() + self.sess.post = mock.Mock(return_value=self.resp) + self.sess.default_microversion = None + + def test_reset(self): + sot = snapshot.Snapshot(**SNAPSHOT) - def test_create_detailed(self): - sot = snapshot.SnapshotDetail(**DETAILED_SNAPSHOT) + self.assertIsNone(sot.reset(self.sess, 'new_status')) - self.assertEqual( - DETAILED_SNAPSHOT["os-extended-snapshot-attributes:progress"], - sot.progress) - self.assertEqual( - DETAILED_SNAPSHOT["os-extended-snapshot-attributes:project_id"], - sot.project_id) + url = 'snapshots/%s/action' % FAKE_ID + body = {'os-reset_status': {'status': 'new_status'}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) diff --git a/openstack/tests/unit/block_storage/v3/test_backup.py b/openstack/tests/unit/block_storage/v3/test_backup.py index ec4ca97cf..73172f00c 100644 --- a/openstack/tests/unit/block_storage/v3/test_backup.py +++ b/openstack/tests/unit/block_storage/v3/test_backup.py @@ -175,3 +175,23 @@ def test_restore_no_params(self): sot.restore, self.sess ) + + def test_force_delete(self): + sot = backup.Backup(**BACKUP) + + self.assertIsNone(sot.force_delete(self.sess)) + + url = 'backups/%s/action' % FAKE_ID + body = {'os-force_delete': {}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_reset(self): + sot = backup.Backup(**BACKUP) + + self.assertIsNone(sot.reset(self.sess, 'new_status')) + + url = 'backups/%s/action' % FAKE_ID + body = {'os-reset_status': {'status': 'new_status'}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 9fffabbeb..b711fc086 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -32,33 +32,6 @@ def setUp(self): class TestVolume(TestVolumeProxy): - def test_snapshot_get(self): - self.verify_get(self.proxy.get_snapshot, snapshot.Snapshot) - - def test_snapshot_find(self): - self.verify_find(self.proxy.find_snapshot, snapshot.Snapshot) - - def test_snapshots_detailed(self): - self.verify_list(self.proxy.snapshots, snapshot.SnapshotDetail, - method_kwargs={"details": True, "query": 1}, - expected_kwargs={"query": 1, - "base_path": "/snapshots/detail"}) - - def test_snapshots_not_detailed(self): - self.verify_list(self.proxy.snapshots, snapshot.Snapshot, - method_kwargs={"details": False, "query": 1}, - expected_kwargs={"query": 1}) - - def test_snapshot_create_attrs(self): - self.verify_create(self.proxy.create_snapshot, snapshot.Snapshot) - - def test_snapshot_delete(self): - self.verify_delete(self.proxy.delete_snapshot, - snapshot.Snapshot, False) - - def test_snapshot_delete_ignore(self): - self.verify_delete(self.proxy.delete_snapshot, - snapshot.Snapshot, True) def test_volume_get(self): self.verify_get(self.proxy.get_volume, volume.Volume) @@ -98,66 +71,6 @@ def test_volume_delete_force(self): def test_backend_pools(self): self.verify_list(self.proxy.backend_pools, stats.Pools) - def test_backups_detailed(self): - # NOTE: mock has_service - self.proxy._connection = mock.Mock() - self.proxy._connection.has_service = mock.Mock(return_value=True) - self.verify_list(self.proxy.backups, backup.Backup, - method_kwargs={"details": True, "query": 1}, - expected_kwargs={"query": 1, - "base_path": "/backups/detail"}) - - def test_backups_not_detailed(self): - # NOTE: mock has_service - self.proxy._connection = mock.Mock() - self.proxy._connection.has_service = mock.Mock(return_value=True) - self.verify_list(self.proxy.backups, backup.Backup, - method_kwargs={"details": False, "query": 1}, - expected_kwargs={"query": 1}) - - def test_backup_get(self): - # NOTE: mock has_service - self.proxy._connection = mock.Mock() - self.proxy._connection.has_service = mock.Mock(return_value=True) - self.verify_get(self.proxy.get_backup, backup.Backup) - - def test_backup_find(self): - # NOTE: mock has_service - self.proxy._connection = mock.Mock() - self.proxy._connection.has_service = mock.Mock(return_value=True) - self.verify_find(self.proxy.find_backup, backup.Backup) - - def test_backup_delete(self): - # NOTE: mock has_service - self.proxy._connection = mock.Mock() - self.proxy._connection.has_service = mock.Mock(return_value=True) - self.verify_delete(self.proxy.delete_backup, backup.Backup, False) - - def test_backup_delete_ignore(self): - # NOTE: mock has_service - self.proxy._connection = mock.Mock() - self.proxy._connection.has_service = mock.Mock(return_value=True) - self.verify_delete(self.proxy.delete_backup, backup.Backup, True) - - def test_backup_create_attrs(self): - # NOTE: mock has_service - self.proxy._connection = mock.Mock() - self.proxy._connection.has_service = mock.Mock(return_value=True) - self.verify_create(self.proxy.create_backup, backup.Backup) - - def test_backup_restore(self): - # NOTE: mock has_service - self.proxy._connection = mock.Mock() - self.proxy._connection.has_service = mock.Mock(return_value=True) - self._verify( - 'openstack.block_storage.v3.backup.Backup.restore', - self.proxy.restore_backup, - method_args=['volume_id'], - method_kwargs={'volume_id': 'vol_id', 'name': 'name'}, - expected_args=[self.proxy], - expected_kwargs={'volume_id': 'vol_id', 'name': 'name'} - ) - def test_limits_get(self): self.verify_get( self.proxy.get_limits, limits.Limit, @@ -399,6 +312,144 @@ def test_terminate_attachment(self): expected_args=[self.proxy, "1"]) +class TestBackup(TestVolumeProxy): + def test_backups_detailed(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self.verify_list(self.proxy.backups, backup.Backup, + method_kwargs={"details": True, "query": 1}, + expected_kwargs={"query": 1, + "base_path": "/backups/detail"}) + + def test_backups_not_detailed(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self.verify_list(self.proxy.backups, backup.Backup, + method_kwargs={"details": False, "query": 1}, + expected_kwargs={"query": 1}) + + def test_backup_get(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self.verify_get(self.proxy.get_backup, backup.Backup) + + def test_backup_find(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self.verify_find(self.proxy.find_backup, backup.Backup) + + def test_backup_delete(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self.verify_delete(self.proxy.delete_backup, backup.Backup, False) + + def test_backup_delete_ignore(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self.verify_delete(self.proxy.delete_backup, backup.Backup, True) + + def test_backup_delete_force(self): + self._verify( + "openstack.block_storage.v3.backup.Backup.force_delete", + self.proxy.delete_backup, + method_args=["value"], + method_kwargs={"force": True}, + expected_args=[self.proxy] + ) + + def test_backup_create_attrs(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self.verify_create(self.proxy.create_backup, backup.Backup) + + def test_backup_restore(self): + # NOTE: mock has_service + self.proxy._connection = mock.Mock() + self.proxy._connection.has_service = mock.Mock(return_value=True) + self._verify( + 'openstack.block_storage.v3.backup.Backup.restore', + self.proxy.restore_backup, + method_args=['volume_id'], + method_kwargs={'volume_id': 'vol_id', 'name': 'name'}, + expected_args=[self.proxy], + expected_kwargs={'volume_id': 'vol_id', 'name': 'name'} + ) + + def test_backup_reset(self): + self._verify( + "openstack.block_storage.v3.backup.Backup.reset", + self.proxy.reset_backup, + method_args=["value", "new_status"], + expected_args=[self.proxy, "new_status"]) + + +class TestSnapshot(TestVolumeProxy): + def test_snapshot_get(self): + self.verify_get(self.proxy.get_snapshot, snapshot.Snapshot) + + def test_snapshot_find(self): + self.verify_find(self.proxy.find_snapshot, snapshot.Snapshot) + + def test_snapshots_detailed(self): + self.verify_list(self.proxy.snapshots, snapshot.SnapshotDetail, + method_kwargs={"details": True, "query": 1}, + expected_kwargs={"query": 1, + "base_path": "/snapshots/detail"}) + + def test_snapshots_not_detailed(self): + self.verify_list(self.proxy.snapshots, snapshot.Snapshot, + method_kwargs={"details": False, "query": 1}, + expected_kwargs={"query": 1}) + + def test_snapshot_create_attrs(self): + self.verify_create(self.proxy.create_snapshot, snapshot.Snapshot) + + def test_snapshot_delete(self): + self.verify_delete(self.proxy.delete_snapshot, + snapshot.Snapshot, False) + + def test_snapshot_delete_ignore(self): + self.verify_delete(self.proxy.delete_snapshot, + snapshot.Snapshot, True) + + def test_snapshot_delete_force(self): + self._verify( + "openstack.block_storage.v3.snapshot.Snapshot.force_delete", + self.proxy.delete_snapshot, + method_args=["value"], + method_kwargs={"force": True}, + expected_args=[self.proxy] + ) + + def test_reset(self): + self._verify( + "openstack.block_storage.v3.snapshot.Snapshot.reset", + self.proxy.reset_snapshot, + method_args=["value", "new_status"], + expected_args=[self.proxy, "new_status"]) + + def test_set_status(self): + self._verify( + "openstack.block_storage.v3.snapshot.Snapshot.set_status", + self.proxy.set_snapshot_status, + method_args=["value", "new_status"], + expected_args=[self.proxy, "new_status", None]) + + def test_set_status_percentage(self): + self._verify( + "openstack.block_storage.v3.snapshot.Snapshot.set_status", + self.proxy.set_snapshot_status, + method_args=["value", "new_status", "per"], + expected_args=[self.proxy, "new_status", "per"]) + + class TestType(TestVolumeProxy): def test_type_get(self): self.verify_get(self.proxy.get_type, type.Type) diff --git a/openstack/tests/unit/block_storage/v3/test_snapshot.py b/openstack/tests/unit/block_storage/v3/test_snapshot.py index 9aa3665eb..a9968b853 100644 --- a/openstack/tests/unit/block_storage/v3/test_snapshot.py +++ b/openstack/tests/unit/block_storage/v3/test_snapshot.py @@ -9,9 +9,12 @@ # 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 unittest import mock from openstack.tests.unit import base +from keystoneauth1 import adapter + from openstack.block_storage.v3 import snapshot FAKE_ID = "ffa9bc5e-1172-4021-acaf-cdcd78a9584d" @@ -70,3 +73,49 @@ def test_create_basic(self): SNAPSHOT["os-extended-snapshot-attributes:project_id"], sot.project_id) self.assertTrue(sot.is_forced) + + +class TestSnapshotActions(base.TestCase): + + def setUp(self): + super(TestSnapshotActions, self).setUp() + self.resp = mock.Mock() + self.resp.body = None + self.resp.json = mock.Mock(return_value=self.resp.body) + self.resp.headers = {} + self.resp.status_code = 202 + + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.get = mock.Mock() + self.sess.post = mock.Mock(return_value=self.resp) + self.sess.default_microversion = None + + def test_force_delete(self): + sot = snapshot.Snapshot(**SNAPSHOT) + + self.assertIsNone(sot.force_delete(self.sess)) + + url = 'snapshots/%s/action' % FAKE_ID + body = {'os-force_delete': {}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_reset(self): + sot = snapshot.Snapshot(**SNAPSHOT) + + self.assertIsNone(sot.reset(self.sess, 'new_status')) + + url = 'snapshots/%s/action' % FAKE_ID + body = {'os-reset_status': {'status': 'new_status'}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_set_status(self): + sot = snapshot.Snapshot(**SNAPSHOT) + + self.assertIsNone(sot.set_status(self.sess, 'new_status')) + + url = 'snapshots/%s/action' % FAKE_ID + body = {'os-update_snapshot_status': {'status': 'new_status'}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) From 485b53bdf632f8fd47efe5cfe5e2495c9c5ea859 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 17 May 2021 14:47:19 +0200 Subject: [PATCH 2865/3836] Switch BS backup and snapshot methods in cloud layer to proxy Now backup and snapshot methods in the cloud layer of the block storage are relying on the proxy layer. Change-Id: Ie1cbf6eda956ddf77039355cc0a3ff3ded0a3fd5 --- openstack/cloud/_block_storage.py | 118 ++++-------------- .../unit/cloud/test_create_volume_snapshot.py | 19 +-- .../unit/cloud/test_delete_volume_snapshot.py | 8 +- .../tests/unit/cloud/test_volume_backups.py | 61 ++++----- 4 files changed, 64 insertions(+), 142 deletions(-) diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index e8ea09fa2..a921572c4 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -443,34 +443,13 @@ def create_volume_snapshot(self, volume_id, force=False, """ kwargs = self._get_volume_kwargs(kwargs) - payload = {'volume_id': volume_id, 'force': force} + payload = {'volume_id': volume_id} payload.update(kwargs) - resp = self.block_storage.post( - '/snapshots', - json=dict(snapshot=payload)) - data = proxy._json_response( - resp, - error_message="Error creating snapshot of volume " - "{volume_id}".format(volume_id=volume_id)) - snapshot = self._get_and_munchify('snapshot', data) + snapshot = self.block_storage.create_snapshot(**payload) if wait: - snapshot_id = snapshot['id'] - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for the volume snapshot to be available." - ): - snapshot = self.get_volume_snapshot_by_id(snapshot_id) - - if snapshot['status'] == 'available': - break - - if snapshot['status'] == 'error': - raise exc.OpenStackCloudException( - "Error in creating volume snapshot") + snapshot = self.block_storage.wait_for_status( + snapshot, wait=timeout) - # TODO(mordred) need to normalize snapshots. We were normalizing them - # as volumes, which is an error. They need to be normalized as - # volume snapshots, which are completely different objects return snapshot def get_volume_snapshot_by_id(self, snapshot_id): @@ -482,14 +461,7 @@ def get_volume_snapshot_by_id(self, snapshot_id): param: snapshot_id: ID of the volume snapshot. """ - resp = self.block_storage.get( - '/snapshots/{snapshot_id}'.format(snapshot_id=snapshot_id)) - data = proxy._json_response( - resp, - error_message="Error getting snapshot " - "{snapshot_id}".format(snapshot_id=snapshot_id)) - return self._normalize_volume( - self._get_and_munchify('snapshot', data)) + return self.block_storage.get_snapshot(snapshot_id) def get_volume_snapshot(self, name_or_id, filters=None): """Get a volume by name or ID. @@ -545,32 +517,14 @@ def create_volume_backup(self, volume_id, name=None, description=None, 'volume_id': volume_id, 'description': description, 'force': force, - 'incremental': incremental, + 'is_incremental': incremental, 'snapshot_id': snapshot_id, } - resp = self.block_storage.post( - '/backups', json=dict(backup=payload)) - data = proxy._json_response( - resp, - error_message="Error creating backup of volume " - "{volume_id}".format(volume_id=volume_id)) - backup = self._get_and_munchify('backup', data) + backup = self.block_storage.create_backup(**payload) if wait: - backup_id = backup['id'] - msg = ("Timeout waiting for the volume backup {} to be " - "available".format(backup_id)) - for _ in utils.iterate_timeout(timeout, msg): - backup = self.get_volume_backup(backup_id) - - if backup['status'] == 'available': - break - - if backup['status'] == 'error': - raise exc.OpenStackCloudException( - "Error in creating volume backup {id}".format( - id=backup_id)) + backup = self.block_storage.wait_for_status(backup, wait=timeout) return backup @@ -589,14 +543,10 @@ def list_volume_snapshots(self, detailed=True, search_opts=None): :returns: A list of volume snapshots ``munch.Munch``. """ - endpoint = '/snapshots/detail' if detailed else '/snapshots' - resp = self.block_storage.get( - endpoint, - params=search_opts) - data = proxy._json_response( - resp, - error_message="Error getting a list of snapshots") - return self._get_and_munchify('snapshots', data) + if not search_opts: + search_opts = {} + return list(self.block_storage.snapshots( + details=detailed, **search_opts)) def list_volume_backups(self, detailed=True, search_opts=None): """ @@ -615,13 +565,11 @@ def list_volume_backups(self, detailed=True, search_opts=None): :returns: A list of volume backups ``munch.Munch``. """ - endpoint = '/backups/detail' if detailed else '/backups' - resp = self.block_storage.get( - endpoint, params=search_opts) - data = proxy._json_response( - resp, - error_message="Error getting a list of backups") - return self._get_and_munchify('backups', data) + if not search_opts: + search_opts = {} + + return list(self.block_storage.backups(details=detailed, + **search_opts)) def delete_volume_backup(self, name_or_id=None, force=False, wait=False, timeout=None): @@ -642,22 +590,10 @@ def delete_volume_backup(self, name_or_id=None, force=False, wait=False, if not volume_backup: return False - msg = "Error in deleting volume backup" - if force: - resp = self.block_storage.post( - '/backups/{backup_id}/action'.format( - backup_id=volume_backup['id']), - json={'os-force_delete': None}) - else: - resp = self.block_storage.delete( - '/backups/{backup_id}'.format( - backup_id=volume_backup['id'])) - proxy._json_response(resp, error_message=msg) + self.block_storage.delete_backup( + volume_backup, ignore_missing=False, force=force) if wait: - msg = "Timeout waiting for the volume backup to be deleted." - for count in utils.iterate_timeout(timeout, msg): - if not self.get_volume_backup(volume_backup['id']): - break + self.block_storage.wait_for_delete(volume_backup, wait=timeout) return True @@ -679,19 +615,11 @@ def delete_volume_snapshot(self, name_or_id=None, wait=False, if not volumesnapshot: return False - resp = self.block_storage.delete( - '/snapshots/{snapshot_id}'.format( - snapshot_id=volumesnapshot['id'])) - proxy._json_response( - resp, - error_message="Error in deleting volume snapshot") + self.block_storage.delete_snapshot( + volumesnapshot, ignore_missing=False) if wait: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for the volume snapshot to be deleted."): - if not self.get_volume_snapshot(volumesnapshot['id']): - break + self.block_storage.wait_for_delete(volumesnapshot, wait=timeout) return True diff --git a/openstack/tests/unit/cloud/test_create_volume_snapshot.py b/openstack/tests/unit/cloud/test_create_volume_snapshot.py index bdca61e96..c205f308a 100644 --- a/openstack/tests/unit/cloud/test_create_volume_snapshot.py +++ b/openstack/tests/unit/cloud/test_create_volume_snapshot.py @@ -17,6 +17,7 @@ Tests for the `create_volume_snapshot` command. """ +from openstack.block_storage.v2 import snapshot from openstack.cloud import exc from openstack.cloud import meta from openstack.tests import fakes @@ -29,6 +30,11 @@ def setUp(self): super(TestCreateVolumeSnapshot, self).setUp() self.use_cinder() + def _compare_snapshots(self, exp, real): + self.assertDictEqual( + snapshot.Snapshot(**exp).to_dict(computed=False), + real.to_dict(computed=False)) + def test_create_volume_snapshot_wait(self): """ Test that create_volume_snapshot with a wait returns the volume @@ -49,7 +55,7 @@ def test_create_volume_snapshot_wait(self): 'volumev2', 'public', append=['snapshots']), json={'snapshot': build_snapshot_dict}, validate=dict(json={ - 'snapshot': {'force': False, 'volume_id': '1234'}})), + 'snapshot': {'volume_id': '1234'}})), dict(method='GET', uri=self.get_mock_url('volumev2', 'public', append=['snapshots', snapshot_id]), @@ -59,10 +65,9 @@ def test_create_volume_snapshot_wait(self): append=['snapshots', snapshot_id]), json={'snapshot': fake_snapshot_dict})]) - self.assertEqual( - self.cloud._normalize_volume(fake_snapshot_dict), - self.cloud.create_volume_snapshot(volume_id=volume_id, wait=True) - ) + self._compare_snapshots( + fake_snapshot_dict, + self.cloud.create_volume_snapshot(volume_id=volume_id, wait=True)) self.assert_calls() def test_create_volume_snapshot_with_timeout(self): @@ -82,7 +87,7 @@ def test_create_volume_snapshot_with_timeout(self): 'volumev2', 'public', append=['snapshots']), json={'snapshot': build_snapshot_dict}, validate=dict(json={ - 'snapshot': {'force': False, 'volume_id': '1234'}})), + 'snapshot': {'volume_id': '1234'}})), dict(method='GET', uri=self.get_mock_url('volumev2', 'public', append=['snapshots', snapshot_id]), @@ -114,7 +119,7 @@ def test_create_volume_snapshot_with_error(self): 'volumev2', 'public', append=['snapshots']), json={'snapshot': build_snapshot_dict}, validate=dict(json={ - 'snapshot': {'force': False, 'volume_id': '1234'}})), + 'snapshot': {'volume_id': '1234'}})), dict(method='GET', uri=self.get_mock_url('volumev2', 'public', append=['snapshots', snapshot_id]), diff --git a/openstack/tests/unit/cloud/test_delete_volume_snapshot.py b/openstack/tests/unit/cloud/test_delete_volume_snapshot.py index 0365b7945..5904e40b9 100644 --- a/openstack/tests/unit/cloud/test_delete_volume_snapshot.py +++ b/openstack/tests/unit/cloud/test_delete_volume_snapshot.py @@ -96,7 +96,13 @@ def test_delete_volume_snapshot_with_timeout(self): dict(method='DELETE', uri=self.get_mock_url( 'volumev2', 'public', - append=['snapshots', fake_snapshot_dict['id']]))]) + append=['snapshots', fake_snapshot_dict['id']])), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['snapshots', '1234']), + json={'snapshot': fake_snapshot_dict}), + ]) self.assertRaises( exc.OpenStackCloudTimeout, diff --git a/openstack/tests/unit/cloud/test_volume_backups.py b/openstack/tests/unit/cloud/test_volume_backups.py index 5e8ff82bc..865d7fb7c 100644 --- a/openstack/tests/unit/cloud/test_volume_backups.py +++ b/openstack/tests/unit/cloud/test_volume_backups.py @@ -9,7 +9,7 @@ # 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.cloud import meta +from openstack.block_storage.v2 import backup from openstack.tests.unit import base @@ -18,6 +18,11 @@ def setUp(self): super(TestVolumeBackups, self).setUp() self.use_cinder() + def _compare_backups(self, exp, real): + self.assertDictEqual( + backup.Backup(**exp).to_dict(computed=False), + real.to_dict(computed=False)) + def test_search_volume_backups(self): name = 'Volume1' vol1 = {'name': name, 'availability_zone': 'az1'} @@ -31,9 +36,8 @@ def test_search_volume_backups(self): result = self.cloud.search_volume_backups( name, {'availability_zone': 'az1'}) self.assertEqual(len(result), 2) - self.assertEqual( - meta.obj_list_to_munch([vol1, vol2]), - result) + for a, b in zip([vol1, vol2], result): + self._compare_backups(a, b) self.assert_calls() def test_get_volume_backup(self): @@ -48,10 +52,7 @@ def test_get_volume_backup(self): json={"backups": [vol1, vol2, vol3]})]) result = self.cloud.get_volume_backup( name, {'availability_zone': 'az1'}) - result = meta.obj_to_munch(result) - self.assertEqual( - meta.obj_to_munch(vol1), - result) + self._compare_backups(vol1, result) self.assert_calls() def test_list_volume_backups(self): @@ -66,14 +67,13 @@ def test_list_volume_backups(self): json={"backups": [backup]})]) result = self.cloud.list_volume_backups(True, search_opts) self.assertEqual(len(result), 1) - self.assertEqual( - meta.obj_list_to_munch([backup]), - result) + + self._compare_backups(backup, result[0]) self.assert_calls() def test_delete_volume_backup_wait(self): backup_id = '6ff16bdf-44d5-4bf9-b0f3-687549c76414' - backup = {'id': backup_id} + backup = {'id': backup_id, 'status': 'available'} self.register_uris([ dict(method='GET', uri=self.get_mock_url( @@ -87,19 +87,19 @@ def test_delete_volume_backup_wait(self): dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', - append=['backups', 'detail']), - json={"backups": [backup]}), + append=['backups', backup_id]), + json={"backup": backup}), dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', - append=['backups', 'detail']), - json={"backups": []})]) + append=['backups', backup_id]), + status_code=404)]) self.cloud.delete_volume_backup(backup_id, False, True, 1) self.assert_calls() def test_delete_volume_backup_force(self): backup_id = '6ff16bdf-44d5-4bf9-b0f3-687549c76414' - backup = {'id': backup_id} + backup = {'id': backup_id, 'status': 'available'} self.register_uris([ dict(method='GET', uri=self.get_mock_url( @@ -111,17 +111,17 @@ def test_delete_volume_backup_force(self): 'volumev2', 'public', append=['backups', backup_id, 'action']), json={'os-force_delete': {}}, - validate=dict(json={u'os-force_delete': None})), + validate=dict(json={u'os-force_delete': {}})), dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', - append=['backups', 'detail']), - json={"backups": [backup]}), + append=['backups', backup_id]), + json={"backup": backup}), dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', - append=['backups', 'detail']), - json={"backups": []}) + append=['backups', backup_id]), + status_code=404) ]) self.cloud.delete_volume_backup(backup_id, True, True, 1) self.assert_calls() @@ -152,12 +152,6 @@ def test_create_volume_backup(self): 'incremental': False } })), - dict(method='GET', - uri=self.get_mock_url( - 'volumev2', 'public', - append=['backups', 'detail']), - json={"backups": [bak1]}), - ]) self.cloud.create_volume_backup(volume_id, name=backup_name) self.assert_calls() @@ -188,12 +182,6 @@ def test_create_incremental_volume_backup(self): 'incremental': True } })), - dict(method='GET', - uri=self.get_mock_url( - 'volumev2', 'public', - append=['backups', 'detail']), - json={"backups": [bak1]}), - ]) self.cloud.create_volume_backup(volume_id, name=backup_name, incremental=True) @@ -226,11 +214,6 @@ def test_create_volume_backup_from_snapshot(self): 'incremental': False } })), - dict(method='GET', - uri=self.get_mock_url( - 'volumev2', 'public', - append=['backups', 'detail']), - json={"backups": [bak1]}), ]) self.cloud.create_volume_backup(volume_id, name=backup_name, From f5c9653a612cb6ec1e6e6675b693bc5a978d3ba3 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 17 May 2021 16:07:41 +0200 Subject: [PATCH 2866/3836] Switch BS volume_attachment operations in cloud layer volume_attachment operations of the block storage cloud layer are now also using proxy layer (compute). Change-Id: Ifaf3a41357e63d482d0f53c87ce3c1293eb6800c --- openstack/cloud/_block_storage.py | 72 +++++------------------ openstack/tests/unit/cloud/test_volume.py | 61 ++++++++++++------- 2 files changed, 55 insertions(+), 78 deletions(-) diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index a921572c4..ce8c3ce4d 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -20,7 +20,6 @@ from openstack.cloud import exc from openstack import exceptions from openstack import proxy -from openstack import utils def _no_pending_volumes(volumes): @@ -300,33 +299,13 @@ def detach_volume(self, server, volume, wait=True, timeout=None): :raises: OpenStackCloudTimeout if wait time exceeded. :raises: OpenStackCloudException on operation error. """ - - proxy._json_response(self.compute.delete( - '/servers/{server_id}/os-volume_attachments/{volume_id}'.format( - server_id=server['id'], volume_id=volume['id'])), - error_message=( - "Error detaching volume {volume} from server {server}".format( - volume=volume['id'], server=server['id']))) + self.compute.delete_volume_attachment( + volume['id'], server['id'], + ignore_missing=False) if wait: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for volume %s to detach." % volume['id']): - try: - vol = self.get_volume(volume['id']) - except Exception: - self.log.debug( - "Error getting volume info %s", volume['id'], - exc_info=True) - continue - - if vol['status'] == 'available': - return - - if vol['status'] == 'error': - raise exc.OpenStackCloudException( - "Error in detaching volume %s" % volume['id'] - ) + vol = self.get_volume(volume['id']) + self.block_storage.wait_for_status(vol) def attach_volume(self, server, volume, device=None, wait=True, timeout=None): @@ -368,40 +347,17 @@ def attach_volume(self, server, volume, device=None, payload = {'volumeId': volume['id']} if device: payload['device'] = device - data = proxy._json_response( - self.compute.post( - '/servers/{server_id}/os-volume_attachments'.format( - server_id=server['id']), - json=dict(volumeAttachment=payload)), - error_message="Error attaching volume {volume_id} to server " - "{server_id}".format(volume_id=volume['id'], - server_id=server['id'])) + attachment = self.compute.create_volume_attachment( + server=server['id'], **payload) if wait: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for volume %s to attach." % volume['id']): - try: - self.list_volumes.invalidate(self) - vol = self.get_volume(volume['id']) - except Exception: - self.log.debug( - "Error getting volume info %s", volume['id'], - exc_info=True) - continue - - if self.get_volume_attach_device(vol, server['id']): - break - - # TODO(Shrews) check to see if a volume can be in error status - # and also attached. If so, we should move this - # above the get_volume_attach_device call - if vol['status'] == 'error': - raise exc.OpenStackCloudException( - "Error in attaching volume %s" % volume['id'] - ) - return self._normalize_volume_attachment( - self._get_and_munchify('volumeAttachment', data)) + if not hasattr(volume, 'fetch'): + # If we got volume as dict we need to re-fetch it to be able to + # use wait_for_status. + volume = self.block_storage.get_volume(volume['id']) + self.block_storage.wait_for_status( + volume, 'in-use', wait=timeout) + return attachment def _get_volume_kwargs(self, kwargs): name = kwargs.pop('name', kwargs.pop('display_name', None)) diff --git a/openstack/tests/unit/cloud/test_volume.py b/openstack/tests/unit/cloud/test_volume.py index 510dcead7..6e24e7912 100644 --- a/openstack/tests/unit/cloud/test_volume.py +++ b/openstack/tests/unit/cloud/test_volume.py @@ -16,6 +16,7 @@ import openstack.cloud from openstack.block_storage.v3 import volume from openstack.cloud import meta +from openstack.compute.v2 import volume_attachment from openstack.tests import fakes from openstack.tests.unit import base @@ -28,6 +29,12 @@ def _compare_volumes(self, exp, real): real.to_dict(computed=False) ) + def _compare_volume_attachments(self, exp, real): + self.assertDictEqual( + volume_attachment.VolumeAttachment(**exp).to_dict(computed=False), + real.to_dict(computed=False) + ) + def test_attach_volume(self): server = dict(id='server001') vol = {'id': 'volume001', 'status': 'available', @@ -36,6 +43,7 @@ def test_attach_volume(self): rattach = {'server_id': server['id'], 'device': 'device001', 'volumeId': volume['id'], 'id': 'attachmentId'} self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', @@ -47,7 +55,7 @@ def test_attach_volume(self): 'volumeId': vol['id']}}) )]) ret = self.cloud.attach_volume(server, volume, wait=False) - self.assertEqual(rattach, ret) + self._compare_volume_attachments(rattach, ret) self.assert_calls() def test_attach_volume_exception(self): @@ -56,6 +64,7 @@ def test_attach_volume_exception(self): 'name': '', 'attachments': []} volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', @@ -67,9 +76,7 @@ def test_attach_volume_exception(self): 'volumeId': vol['id']}}) )]) with testtools.ExpectedException( - openstack.cloud.OpenStackCloudURINotFound, - "Error attaching volume %s to server %s" % ( - volume['id'], server['id']) + openstack.cloud.OpenStackCloudURINotFound ): self.cloud.attach_volume(server, volume, wait=False) self.assert_calls() @@ -81,11 +88,12 @@ def test_attach_volume_wait(self): volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) vol['attachments'] = [{'server_id': server['id'], 'device': 'device001'}] - vol['status'] = 'attached' + vol['status'] = 'in-use' attached_volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) rattach = {'server_id': server['id'], 'device': 'device001', 'volumeId': volume['id'], 'id': 'attachmentId'} self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', @@ -98,15 +106,16 @@ def test_attach_volume_wait(self): self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', 'detail']), - json={'volumes': [volume]}), + 'volumev2', 'public', append=['volumes', vol['id']]), + json={'volume': volume}), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', 'detail']), - json={'volumes': [attached_volume]})]) + 'volumev2', 'public', append=['volumes', vol['id']]), + json={'volume': attached_volume}) + ]) # defaults to wait=True ret = self.cloud.attach_volume(server, volume) - self.assertEqual(rattach, ret) + self._compare_volume_attachments(rattach, ret) self.assert_calls() def test_attach_volume_wait_error(self): @@ -119,6 +128,7 @@ def test_attach_volume_wait_error(self): rattach = {'server_id': server['id'], 'device': 'device001', 'volumeId': volume['id'], 'id': 'attachmentId'} self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', @@ -131,12 +141,16 @@ def test_attach_volume_wait_error(self): self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', 'detail']), - json={'volumes': [errored_volume]})]) + 'volumev2', 'public', append=['volumes', volume['id']]), + json={'volume': errored_volume}), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', append=['volumes', volume['id']]), + json={'volume': errored_volume}) + ]) with testtools.ExpectedException( - openstack.cloud.OpenStackCloudException, - "Error in attaching volume %s" % errored_volume['id'] + openstack.exceptions.ResourceFailure ): self.cloud.attach_volume(server, volume) self.assert_calls() @@ -176,6 +190,7 @@ def test_detach_volume(self): {'server_id': 'server001', 'device': 'device001'} ]) self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='DELETE', uri=self.get_mock_url( 'compute', 'public', @@ -191,6 +206,7 @@ def test_detach_volume_exception(self): {'server_id': 'server001', 'device': 'device001'} ]) self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='DELETE', uri=self.get_mock_url( 'compute', 'public', @@ -198,9 +214,7 @@ def test_detach_volume_exception(self): 'os-volume_attachments', volume['id']]), status_code=404)]) with testtools.ExpectedException( - openstack.cloud.OpenStackCloudURINotFound, - "Error detaching volume %s from server %s" % ( - volume['id'], server['id']) + openstack.cloud.OpenStackCloudURINotFound ): self.cloud.detach_volume(server, volume, wait=False) self.assert_calls() @@ -215,6 +229,7 @@ def test_detach_volume_wait(self): vol['attachments'] = [] avail_volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='DELETE', uri=self.get_mock_url( 'compute', 'public', @@ -238,6 +253,7 @@ def test_detach_volume_wait_error(self): vol['attachments'] = [] errored_volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='DELETE', uri=self.get_mock_url( 'compute', 'public', @@ -247,10 +263,15 @@ def test_detach_volume_wait_error(self): dict(method='GET', uri=self.get_mock_url( 'volumev2', 'public', append=['volumes', 'detail']), - json={'volumes': [errored_volume]})]) + json={'volumes': [errored_volume]}), + dict(method='GET', + uri=self.get_mock_url( + 'volumev2', 'public', + append=['volumes', errored_volume['id']]), + json={'volume': errored_volume}) + ]) with testtools.ExpectedException( - openstack.cloud.OpenStackCloudException, - "Error in detaching volume %s" % errored_volume['id'] + openstack.exceptions.ResourceFailure ): self.cloud.detach_volume(server, volume) self.assert_calls() From d6c51c5335da758c636062dbf552ae916eabb947 Mon Sep 17 00:00:00 2001 From: Kristian Kucerak Date: Tue, 8 Jun 2021 08:36:55 +0000 Subject: [PATCH 2867/3836] Add cleanup function for DNS resource Extended the _service_cleanup method of proxy DNS to cleanup DNS zones and FloatingIPs. Added a separate method unset_floating_ip which calls upate_floating_ip with attrs. Extended the unit test class TestDnsFloatIP() with the new method. Change-Id: I36e73ff183d22fb975ef8ea2f3122bd9d9bde74a --- openstack/dns/v2/_proxy.py | 33 ++++++++++++++++++++++- openstack/tests/unit/dns/v2/test_proxy.py | 9 +++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py index 9fdcb602a..092c688cc 100644 --- a/openstack/dns/v2/_proxy.py +++ b/openstack/dns/v2/_proxy.py @@ -402,6 +402,17 @@ def update_floating_ip(self, floating_ip, **attrs): """ return self._update(_fip.FloatingIP, floating_ip, **attrs) + def unset_floating_ip(self, floating_ip): + """Unset a Floating IP PTR record + :param floating_ip: ID for the floatingip associated with the + project. + :returns: FloatingIP PTR record. + :rtype: :class:`~openstack.dns.v2.fip.FloatipgIP` + """ + # concat `region:floating_ip_id` as id + attrs = {'ptrdname': None} + return self._update(_fip.FloatingIP, floating_ip, **attrs) + # ======== Zone Transfer ======== def zone_transfer_requests(self, **query): """Retrieve a generator of zone transfer requests @@ -524,4 +535,24 @@ def _get_cleanup_dependencies(self): def _service_cleanup(self, dry_run=True, client_status_queue=False, identified_resources=None, filters=None, resource_evaluation_fn=None): - pass + # Delete all zones + for obj in self.zones(): + self._service_cleanup_del_res( + self.delete_zone, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn) + # Unset all floatingIPs + # NOTE: FloatingIPs are not cleaned when filters are set + for obj in self.floating_ips(): + self._service_cleanup_del_res( + self.unset_floating_ip, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn) diff --git a/openstack/tests/unit/dns/v2/test_proxy.py b/openstack/tests/unit/dns/v2/test_proxy.py index d628a724b..ebabc8034 100644 --- a/openstack/tests/unit/dns/v2/test_proxy.py +++ b/openstack/tests/unit/dns/v2/test_proxy.py @@ -114,6 +114,15 @@ def test_floating_ip_update(self): self.verify_update(self.proxy.update_floating_ip, floating_ip.FloatingIP) + def test_floating_ip_unset(self): + self._verify( + 'openstack.proxy.Proxy._update', + self.proxy.unset_floating_ip, + method_args=['value'], + method_kwargs={}, + expected_args=[floating_ip.FloatingIP, 'value'], + expected_kwargs={'ptrdname': None}) + class TestDnsZoneImport(TestDnsProxy): def test_zone_import_delete(self): From ccc6478ccbb9a0f6e835663af2ef358ee4e981b9 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 11 Jun 2021 16:49:49 +0200 Subject: [PATCH 2868/3836] Temporarily disable nodepool job nodepool job is blocking us for quite some time with timeouts. Let us disable it for now and rather fix issues later (on a separate branch anyway) rather then continue waiting. Change-Id: I4802c37276cc5e9b5ec8930d69205a1e11ea87a0 --- .zuul.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 6de2c8689..ddd5183ab 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -387,7 +387,7 @@ jobs: - opendev-buildset-registry - nodepool-build-image-siblings - - dib-nodepool-functional-openstack-centos-8-stream-src + # - dib-nodepool-functional-openstack-centos-8-stream-src - openstacksdk-functional-devstack - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-senlin @@ -412,7 +412,7 @@ jobs: - opendev-buildset-registry - nodepool-build-image-siblings - - dib-nodepool-functional-openstack-centos-8-stream-src + # - dib-nodepool-functional-openstack-centos-8-stream-src - openstacksdk-functional-devstack - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-senlin From b81275b11c9bfda28673bce65eea395445c857f5 Mon Sep 17 00:00:00 2001 From: Joel Capitao Date: Mon, 14 Jun 2021 17:53:50 +0200 Subject: [PATCH 2869/3836] Remove misspelled speccing arguments Some clean up have been done in py3.10 [1] and implemented in [2]. So, we caught this typo while building package on Fedora with python3.10. With 'autospec' instead of 'auto_spec' unit tests fail. As 'autospec' was silently ignored, and in order to keep same behavior, we remove the parameter. [1] https://bugs.python.org/issue41877 [2] https://github.com/python/cpython/pull/23737 Change-Id: I092d48b9806c4cd6f8b2ecbedd7ab5b7c6d20c04 --- openstack/tests/unit/compute/v2/test_proxy.py | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index a0ed8da44..4bcfd8b06 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -141,9 +141,8 @@ def test_flavor_get_skip_fetch_extra(self): ) mocked.assert_not_called() - @mock.patch("openstack.proxy.Proxy._list", auto_spec=True) - @mock.patch("openstack.compute.v2.flavor.Flavor.fetch_extra_specs", - auto_spec=True) + @mock.patch("openstack.proxy.Proxy._list") + @mock.patch("openstack.compute.v2.flavor.Flavor.fetch_extra_specs") def test_flavors_detailed(self, fetch_mock, list_mock): res = self.proxy.flavors(details=True) for r in res: @@ -154,9 +153,8 @@ def test_flavors_detailed(self, fetch_mock, list_mock): base_path="/flavors/detail" ) - @mock.patch("openstack.proxy.Proxy._list", auto_spec=True) - @mock.patch("openstack.compute.v2.flavor.Flavor.fetch_extra_specs", - auto_spec=True) + @mock.patch("openstack.proxy.Proxy._list") + @mock.patch("openstack.compute.v2.flavor.Flavor.fetch_extra_specs") def test_flavors_not_detailed(self, fetch_mock, list_mock): res = self.proxy.flavors(details=False) for r in res: @@ -167,9 +165,8 @@ def test_flavors_not_detailed(self, fetch_mock, list_mock): base_path="/flavors" ) - @mock.patch("openstack.proxy.Proxy._list", auto_spec=True) - @mock.patch("openstack.compute.v2.flavor.Flavor.fetch_extra_specs", - auto_spec=True) + @mock.patch("openstack.proxy.Proxy._list") + @mock.patch("openstack.compute.v2.flavor.Flavor.fetch_extra_specs") def test_flavors_query(self, fetch_mock, list_mock): res = self.proxy.flavors(details=False, get_extra_specs=True, a="b") for r in res: @@ -180,9 +177,8 @@ def test_flavors_query(self, fetch_mock, list_mock): a="b" ) - @mock.patch("openstack.proxy.Proxy._list", auto_spec=True) - @mock.patch("openstack.compute.v2.flavor.Flavor.fetch_extra_specs", - auto_spec=True) + @mock.patch("openstack.proxy.Proxy._list") + @mock.patch("openstack.compute.v2.flavor.Flavor.fetch_extra_specs") def test_flavors_get_extra(self, fetch_mock, list_mock): res = self.proxy.flavors(details=False, get_extra_specs=True) for r in res: From 72aa45311a98a97550665b1b6bc9166a6642db08 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 11 May 2021 14:47:30 +0200 Subject: [PATCH 2870/3836] Switch project management in cloud to proxy Do required switch now for identity.project in the cloud layer. Drop separate identity v2 support from cloud layer. It is relatively expensive to maintain with with most likely nobody using it. Also adding new v3 features into the shim layer is getting increasingly complex. Change-Id: Ied4ccaea3e357c59f422e058e4c9dfe105024f82 --- openstack/cloud/_identity.py | 124 +-- openstack/cloud/_network.py | 6 +- openstack/identity/v3/project.py | 2 - openstack/tests/unit/base.py | 8 +- openstack/tests/unit/cloud/test_caching.py | 61 +- .../tests/unit/cloud/test_domain_params.py | 34 - openstack/tests/unit/cloud/test_project.py | 89 +- .../tests/unit/cloud/test_role_assignment.py | 959 ++---------------- 8 files changed, 172 insertions(+), 1111 deletions(-) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index 3864ff3c5..ed2442f5d 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -20,6 +20,7 @@ from openstack.cloud import _normalize from openstack.cloud import _utils from openstack.cloud import exc +from openstack import exceptions from openstack import utils @@ -50,27 +51,16 @@ def list_projects(self, domain_id=None, name_or_id=None, filters=None): :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ - kwargs = dict( - filters=filters, - domain_id=domain_id) - if self._is_client_version('identity', 3): - kwargs['obj_name'] = 'project' + if not filters: + filters = {} + query = dict( + **filters) + if name_or_id: + query['name'] = name_or_id + if domain_id: + query['domain_id'] = domain_id - pushdown, filters = _normalize._split_filters(**kwargs) - - try: - if self._is_client_version('identity', 3): - key = 'projects' - else: - key = 'tenants' - data = self._identity_client.get( - '/{endpoint}'.format(endpoint=key), params=pushdown) - projects = self._normalize_projects( - self._get_and_munchify(key, data)) - except Exception as e: - self.log.debug("Failed to list projects", exc_info=True) - raise exc.OpenStackCloudException(str(e)) - return _utils._filter_list(projects, name_or_id, filters) + return list(self.identity.projects(**query)) def search_projects(self, name_or_id=None, filters=None, domain_id=None): '''Backwards compatibility method for search_projects @@ -80,8 +70,8 @@ def search_projects(self, name_or_id=None, filters=None, domain_id=None): to allow code written with positional parameter to still work. But really, use keyword arguments. ''' - return self.list_projects( - domain_id=domain_id, name_or_id=name_or_id, filters=filters) + projects = self.list_projects(domain_id=domain_id, filters=filters) + return _utils._filter_list(projects, name_or_id, filters) def get_project(self, name_or_id, filters=None, domain_id=None): """Get exactly one project. @@ -100,48 +90,29 @@ def get_project(self, name_or_id, filters=None, domain_id=None): def update_project(self, name_or_id, enabled=None, domain_id=None, **kwargs): - with _utils.shade_exceptions( - "Error in updating project {project}".format( - project=name_or_id)): - proj = self.get_project(name_or_id, domain_id=domain_id) - if not proj: - raise exc.OpenStackCloudException( - "Project %s not found." % name_or_id) - if enabled is not None: - kwargs.update({'enabled': enabled}) - # NOTE(samueldmq): Current code only allow updates of description - # or enabled fields. - if self._is_client_version('identity', 3): - data = self._identity_client.patch( - '/projects/' + proj['id'], json={'project': kwargs}) - project = self._get_and_munchify('project', data) - else: - data = self._identity_client.post( - '/tenants/' + proj['id'], json={'tenant': kwargs}) - project = self._get_and_munchify('tenant', data) - project = self._normalize_project(project) + + project = self.identity.find_project( + name_or_id=name_or_id, + domain_id=domain_id) + if not project: + raise exceptions.SDKException( + "Project %s not found." % name_or_id) + if enabled is not None: + kwargs.update({'enabled': enabled}) + project = self.identity.update_project(project, **kwargs) self.list_projects.invalidate(self) return project def create_project( - self, name, description=None, domain_id=None, enabled=True): + self, name, domain_id, description=None, enabled=True): """Create a project.""" - with _utils.shade_exceptions( - "Error in creating project {project}".format(project=name)): - project_ref = self._get_domain_id_param_dict(domain_id) - project_ref.update({'name': name, - 'description': description, - 'enabled': enabled}) - endpoint, key = ('tenants', 'tenant') - if self._is_client_version('identity', 3): - endpoint, key = ('projects', 'project') - data = self._identity_client.post( - '/{endpoint}'.format(endpoint=endpoint), - json={key: project_ref}) - project = self._normalize_project( - self._get_and_munchify(key, data)) - self.list_projects.invalidate(self) - return project + attrs = dict( + name=name, + description=description, + domain_id=domain_id, + is_enabled=enabled + ) + return self.identity.create_project(**attrs) def delete_project(self, name_or_id, domain_id=None): """Delete a project. @@ -155,22 +126,22 @@ def delete_project(self, name_or_id, domain_id=None): :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call """ - - with _utils.shade_exceptions( - "Error in deleting project {project}".format( - project=name_or_id)): - project = self.get_project(name_or_id, domain_id=domain_id) - if project is None: + try: + project = self.identity.find_project( + name_or_id=name_or_id, + ignore_missing=True, + domain_id=domain_id + ) + if not project: self.log.debug( "Project %s not found for deleting", name_or_id) return False - - if self._is_client_version('identity', 3): - self._identity_client.delete('/projects/' + project['id']) - else: - self._identity_client.delete('/tenants/' + project['id']) - - return True + self.identity.delete_project(project) + return True + except exceptions.SDKException: + self.log.exception("Error in deleting project {project}".format( + project=name_or_id)) + return False @_utils.valid_kwargs('domain_id', 'name') @_utils.cache_on_arguments() @@ -1159,7 +1130,12 @@ def _keystone_v3_role_assignments(self, **filters): del filters[k] for k in ('project', 'domain'): if k in filters: - filters['scope.' + k + '.id'] = filters[k] + try: + filters['scope.' + k + '.id'] = filters[k].id + except AttributeError: + # NOTE(gtema): will be dropped once domains are switched to + # proxy + filters['scope.' + k + '.id'] = filters[k] del filters[k] if 'os_inherit_extension_inherited_to' in filters: filters['scope.OS-INHERIT:inherited_to'] = ( @@ -1332,7 +1308,7 @@ def _get_grant_revoke_params(self, role, user=None, group=None, if project: # drop domain in favor of project data.pop('domain', None) - data['project'] = self.get_project(project, filters=filters) + data['project'] = self.identity.find_project(project, **filters) if not is_keystone_v2 and group: data['group'] = self.get_group(group, filters=filters) diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index 96f261ab3..500b6e810 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -644,7 +644,7 @@ def set_network_quotas(self, name_or_id, **kwargs): if not proj: raise exc.OpenStackCloudException("project does not exist") - self.network.update_quota(proj, **kwargs) + self.network.update_quota(proj.id, **kwargs) def get_network_quotas(self, name_or_id, details=False): """ Get network quotas for a project @@ -659,7 +659,7 @@ def get_network_quotas(self, name_or_id, details=False): proj = self.get_project(name_or_id) if not proj: raise exc.OpenStackCloudException("project does not exist") - return self.network.get_quota(proj, details) + return self.network.get_quota(proj.id, details) def get_network_extensions(self): """Get Cloud provided network extensions @@ -680,7 +680,7 @@ def delete_network_quotas(self, name_or_id): proj = self.get_project(name_or_id) if not proj: raise exc.OpenStackCloudException("project does not exist") - self.network.delete_quota(proj) + self.network.delete_quota(proj.id) @_utils.valid_kwargs( 'action', 'description', 'destination_firewall_group_id', diff --git a/openstack/identity/v3/project.py b/openstack/identity/v3/project.py index 24ae45635..577bdcd02 100644 --- a/openstack/identity/v3/project.py +++ b/openstack/identity/v3/project.py @@ -53,8 +53,6 @@ class Project(resource.Resource, resource.TagMixin): #: for the project are immediately invalidated. Re-enabling a project #: does not re-enable pre-existing tokens. *Type: bool* is_enabled = resource.Body('enabled', type=bool) - #: Unique project name, within the owning domain. *Type: string* - name = resource.Body('name') #: The resource options for the project. Available resource options are #: immutable. options = resource.Body('options', type=dict) diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 463a1233d..23085b3e0 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -266,13 +266,9 @@ def _get_project_data(self, project_name=None, enabled=None, response['description'] = description request['description'] = description request.setdefault('description', None) - if v3: - project_key = 'project' - else: - project_key = 'tenant' return _ProjectData(project_id, project_name, enabled, domain_id, - description, {project_key: response}, - {project_key: request}) + description, {'project': response}, + {'project': request}) def _get_group_data(self, name=None, domain_id=None, description=None): group_id = uuid.uuid4().hex diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py index ef072ace0..0378d305e 100644 --- a/openstack/tests/unit/cloud/test_caching.py +++ b/openstack/tests/unit/cloud/test_caching.py @@ -20,6 +20,7 @@ from openstack.cloud import meta from openstack.compute.v2 import flavor as _flavor from openstack.network.v2 import port as _port +from openstack.identity.v3 import project as _project from openstack import exceptions from openstack.tests import fakes from openstack.tests.unit import base @@ -109,6 +110,12 @@ def _munch_images(self, fake_image): def test_openstack_cloud(self): self.assertIsInstance(self.cloud, openstack.connection.Connection) + def _compare_projects(self, exp, real): + self.assertDictEqual( + _project.Project(**exp).to_dict(computed=False), + real.to_dict(computed=False) + ) + def test_list_projects_v3(self): project_one = self._get_project_data() project_two = self._get_project_data() @@ -128,57 +135,17 @@ def test_list_projects_v3(self): dict(method='GET', uri=mock_uri, status_code=200, json=second_response)]) - self.assertEqual( - self.cloud._normalize_projects( - meta.obj_list_to_munch(first_response['projects'])), - self.cloud.list_projects()) - self.assertEqual( - self.cloud._normalize_projects( - meta.obj_list_to_munch(first_response['projects'])), - self.cloud.list_projects()) + for a, b in zip(first_response['projects'], + self.cloud.list_projects()): + self._compare_projects(a, b) + # invalidate the list_projects cache self.cloud.list_projects.invalidate(self.cloud) - # ensure the new values are now retrieved - self.assertEqual( - self.cloud._normalize_projects( - meta.obj_list_to_munch(second_response['projects'])), - self.cloud.list_projects()) - self.assert_calls() - - def test_list_projects_v2(self): - self.use_keystone_v2() - project_one = self._get_project_data(v3=False) - project_two = self._get_project_data(v3=False) - project_list = [project_one, project_two] - - first_response = {'tenants': [project_one.json_response['tenant']]} - second_response = {'tenants': [p.json_response['tenant'] - for p in project_list]} - - mock_uri = self.get_mock_url( - service_type='identity', interface='admin', resource='tenants') - self.register_uris([ - dict(method='GET', uri=mock_uri, status_code=200, - json=first_response), - dict(method='GET', uri=mock_uri, status_code=200, - json=second_response)]) + for a, b in zip(second_response['projects'], + self.cloud.list_projects()): + self._compare_projects(a, b) - self.assertEqual( - self.cloud._normalize_projects( - meta.obj_list_to_munch(first_response['tenants'])), - self.cloud.list_projects()) - self.assertEqual( - self.cloud._normalize_projects( - meta.obj_list_to_munch(first_response['tenants'])), - self.cloud.list_projects()) - # invalidate the list_projects cache - self.cloud.list_projects.invalidate(self.cloud) - # ensure the new values are now retrieved - self.assertEqual( - self.cloud._normalize_projects( - meta.obj_list_to_munch(second_response['tenants'])), - self.cloud.list_projects()) self.assert_calls() def test_list_servers_no_herd(self): diff --git a/openstack/tests/unit/cloud/test_domain_params.py b/openstack/tests/unit/cloud/test_domain_params.py index 185a75ac3..4f5e1ab6d 100644 --- a/openstack/tests/unit/cloud/test_domain_params.py +++ b/openstack/tests/unit/cloud/test_domain_params.py @@ -42,37 +42,3 @@ def test_identity_params_v3_no_domain(self): domain_id=None, project=project_data.project_name) self.assert_calls() - - def test_identity_params_v2(self): - self.use_keystone_v2() - project_data = self._get_project_data(v3=False) - self.register_uris([ - dict(method='GET', - uri='https://identity.example.com/v2.0/tenants', - json=dict(tenants=[project_data.json_response['tenant']])) - ]) - - ret = self.cloud._get_identity_params( - domain_id='foo', project=project_data.project_name) - self.assertIn('tenant_id', ret) - self.assertEqual(ret['tenant_id'], project_data.project_id) - self.assertNotIn('domain', ret) - - self.assert_calls() - - def test_identity_params_v2_no_domain(self): - self.use_keystone_v2() - project_data = self._get_project_data(v3=False) - self.register_uris([ - dict(method='GET', - uri='https://identity.example.com/v2.0/tenants', - json=dict(tenants=[project_data.json_response['tenant']])) - ]) - - ret = self.cloud._get_identity_params( - domain_id=None, project=project_data.project_name) - self.assertIn('tenant_id', ret) - self.assertEqual(ret['tenant_id'], project_data.project_id) - self.assertNotIn('domain', ret) - - self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_project.py b/openstack/tests/unit/cloud/test_project.py index 02e534453..a90371909 100644 --- a/openstack/tests/unit/cloud/test_project.py +++ b/openstack/tests/unit/cloud/test_project.py @@ -22,7 +22,7 @@ class TestProject(base.TestCase): def get_mock_url(self, service_type='identity', interface='public', resource=None, append=None, base_url_append=None, - v3=True): + v3=True, qs_elements=None): if v3 and resource is None: resource = 'projects' elif not v3 and resource is None: @@ -31,23 +31,8 @@ def get_mock_url(self, service_type='identity', interface='public', base_url_append = 'v3' return super(TestProject, self).get_mock_url( service_type=service_type, interface=interface, resource=resource, - append=append, base_url_append=base_url_append) - - def test_create_project_v2(self): - self.use_keystone_v2() - project_data = self._get_project_data(v3=False) - self.register_uris([ - dict(method='POST', uri=self.get_mock_url(v3=False), - status_code=200, json=project_data.json_response, - validate=dict(json=project_data.json_request)) - ]) - project = self.cloud.create_project( - name=project_data.project_name, - description=project_data.description) - self.assertThat(project.id, matchers.Equals(project_data.project_id)) - self.assertThat( - project.name, matchers.Equals(project_data.project_name)) - self.assert_calls() + append=append, base_url_append=base_url_append, + qs_elements=qs_elements) def test_create_project_v3(self,): project_data = self._get_project_data( @@ -74,37 +59,13 @@ def test_create_project_v3(self,): project.domain_id, matchers.Equals(project_data.domain_id)) self.assert_calls() - def test_create_project_v3_no_domain(self): - with testtools.ExpectedException( - openstack.cloud.OpenStackCloudException, - "User or project creation requires an explicit" - " domain_id argument." - ): - self.cloud.create_project(name='foo', description='bar') - - def test_delete_project_v2(self): - self.use_keystone_v2() - project_data = self._get_project_data(v3=False) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(v3=False), - status_code=200, - json={'tenants': [project_data.json_response['tenant']]}), - dict(method='DELETE', - uri=self.get_mock_url( - v3=False, append=[project_data.project_id]), - status_code=204) - ]) - self.cloud.delete_project(project_data.project_id) - self.assert_calls() - def test_delete_project_v3(self): project_data = self._get_project_data(v3=False) self.register_uris([ dict(method='GET', - uri=self.get_mock_url(), + uri=self.get_mock_url(append=[project_data.project_id]), status_code=200, - json={'projects': [project_data.json_response['tenant']]}), + json=project_data.json_response), dict(method='DELETE', uri=self.get_mock_url(append=[project_data.project_id]), status_code=204) @@ -116,7 +77,11 @@ def test_update_project_not_found(self): project_data = self._get_project_data() self.register_uris([ dict(method='GET', - uri=self.get_mock_url(), + uri=self.get_mock_url(append=[project_data.project_id]), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + qs_elements=['name=' + project_data.project_id]), status_code=200, json={'projects': []}) ]) @@ -132,36 +97,6 @@ def test_update_project_not_found(self): self.cloud.update_project(project_data.project_id) self.assert_calls() - def test_update_project_v2(self): - self.use_keystone_v2() - project_data = self._get_project_data( - v3=False, - description=self.getUniqueString('projectDesc')) - # remove elements that are not updated in this test. - project_data.json_request['tenant'].pop('name') - project_data.json_request['tenant'].pop('enabled') - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(v3=False), - status_code=200, - json={'tenants': [project_data.json_response['tenant']]}), - dict(method='POST', - uri=self.get_mock_url( - v3=False, append=[project_data.project_id]), - status_code=200, - json=project_data.json_response, - validate=dict(json=project_data.json_request)) - ]) - project = self.cloud.update_project( - project_data.project_id, - description=project_data.description) - self.assertThat(project.id, matchers.Equals(project_data.project_id)) - self.assertThat( - project.name, matchers.Equals(project_data.project_name)) - self.assertThat( - project.description, matchers.Equals(project_data.description)) - self.assert_calls() - def test_update_project_v3(self): project_data = self._get_project_data( description=self.getUniqueString('projectDesc')) @@ -173,8 +108,8 @@ def test_update_project_v3(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - resource=('projects?domain_id=%s' % - project_data.domain_id)), + append=[project_data.project_id], + qs_elements=['domain_id=' + project_data.domain_id]), status_code=200, json={'projects': [project_data.json_response['project']]}), dict(method='PATCH', diff --git a/openstack/tests/unit/cloud/test_role_assignment.py b/openstack/tests/unit/cloud/test_role_assignment.py index d08165470..14da6b3f4 100644 --- a/openstack/tests/unit/cloud/test_role_assignment.py +++ b/openstack/tests/unit/cloud/test_role_assignment.py @@ -91,302 +91,6 @@ def get_mock_url(self, service_type='identity', interface='public', service_type, interface, resource, append, base_url_append, qs_elements) - def test_grant_role_user_v2(self): - self.use_keystone_v2() - self.register_uris([ - # user name - dict(method='GET', - uri=self.get_mock_url(base_url_append='OS-KSADM', - resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='users', - qs_elements=['name=%s' % - self.user_data.name]), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants'), - status_code=200, - json={'tenants': [ - self.project_data_v2.json_response['tenant']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', - self.user_data.user_id, - 'roles']), - status_code=200, - json={'roles': []}), - dict(method='PUT', - status_code=201, - uri=self.get_mock_url( - base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', self.user_data.user_id, 'roles', - 'OS-KSADM', self.role_data.role_id])), - # user id - dict(method='GET', - uri=self.get_mock_url(base_url_append='OS-KSADM', - resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='users'), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants'), - status_code=200, - json={'tenants': [ - self.project_data_v2.json_response['tenant']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', - self.user_data.user_id, - 'roles']), - status_code=200, - json={'roles': []}), - dict(method='PUT', - uri=self.get_mock_url( - base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', self.user_data.user_id, 'roles', - 'OS-KSADM', self.role_data.role_id]), - status_code=201) - ]) - # user name - self.assertTrue( - self.cloud.grant_role( - self.role_data.role_name, - user=self.user_data.name, - project=self.project_data.project_id)) - # user id - self.assertTrue( - self.cloud.grant_role( - self.role_data.role_name, - user=self.user_data.user_id, - project=self.project_data.project_id)) - self.assert_calls() - - def test_grant_role_user_project_v2(self): - self.use_keystone_v2() - self.register_uris([ - # role name and user name - dict(method='GET', - uri=self.get_mock_url(base_url_append='OS-KSADM', - resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='users', - qs_elements=['name=%s' % - self.user_data.name]), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants'), - status_code=200, - json={'tenants': [ - self.project_data_v2.json_response['tenant']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', - self.user_data.user_id, - 'roles']), - status_code=200, - json={'roles': []}), - dict(method='PUT', - uri=self.get_mock_url( - base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', self.user_data.user_id, 'roles', - 'OS-KSADM', self.role_data.role_id]), - status_code=201), - # role name and user id - dict(method='GET', - uri=self.get_mock_url(base_url_append='OS-KSADM', - resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='users'), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants'), - status_code=200, - json={'tenants': [ - self.project_data_v2.json_response['tenant']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', - self.user_data.user_id, - 'roles']), - status_code=200, - json={'roles': []}), - dict(method='PUT', - uri=self.get_mock_url( - base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', self.user_data.user_id, 'roles', - 'OS-KSADM', self.role_data.role_id]), - status_code=201), - # role id and user name - dict(method='GET', - uri=self.get_mock_url(base_url_append='OS-KSADM', - resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='users', - qs_elements=['name=%s' % - self.user_data.name]), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants'), - status_code=200, - json={'tenants': [ - self.project_data_v2.json_response['tenant']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', - self.user_data.user_id, - 'roles']), - status_code=200, - json={'roles': []}), - dict(method='PUT', - uri=self.get_mock_url( - base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', self.user_data.user_id, 'roles', - 'OS-KSADM', self.role_data.role_id]), - status_code=201, - ), - # role id and user id - dict(method='GET', - uri=self.get_mock_url(base_url_append='OS-KSADM', - resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='users'), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants'), - status_code=200, - json={'tenants': [ - self.project_data_v2.json_response['tenant']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', - self.user_data.user_id, - 'roles']), - status_code=200, - json={'roles': []}), - dict(method='PUT', - uri=self.get_mock_url( - base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', self.user_data.user_id, 'roles', - 'OS-KSADM', self.role_data.role_id]), - status_code=201) - ]) - # role name and user name - self.assertTrue( - self.cloud.grant_role( - self.role_data.role_name, - user=self.user_data.name, - project=self.project_data_v2.project_id)) - # role name and user id - self.assertTrue( - self.cloud.grant_role( - self.role_data.role_name, - user=self.user_data.user_id, - project=self.project_data_v2.project_id)) - # role id and user name - self.assertTrue( - self.cloud.grant_role( - self.role_data.role_id, - user=self.user_data.name, - project=self.project_data_v2.project_id)) - # role id and user id - self.assertTrue( - self.cloud.grant_role( - self.role_data.role_id, - user=self.user_data.user_id, - project=self.project_data_v2.project_id)) - self.assert_calls() - - def test_grant_role_user_project_v2_exists(self): - self.use_keystone_v2() - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(base_url_append='OS-KSADM', - resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='users', - qs_elements=['name=%s' % - self.user_data.name]), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants'), - status_code=200, - json={'tenants': [ - self.project_data_v2.json_response['tenant']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', - self.user_data.user_id, - 'roles']), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - ]) - self.assertFalse(self.cloud.grant_role( - self.role_data.role_name, - user=self.user_data.name, - project=self.project_data_v2.project_id)) - self.assert_calls() - def test_grant_role_user_project(self): self.register_uris([ # user name @@ -401,10 +105,12 @@ def test_grant_role_user_project(self): status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', - uri=self.get_mock_url(resource='projects'), + uri=self.get_mock_url( + resource='projects', + append=[self.project_data.project_id]), status_code=200, - json={'projects': [ - self.project_data.json_response['project']]}), + json={'project': + self.project_data.json_response['project']}), dict(method='GET', uri=self.get_mock_url( resource='role_assignments', @@ -432,10 +138,12 @@ def test_grant_role_user_project(self): status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', - uri=self.get_mock_url(resource='projects'), + uri=self.get_mock_url( + resource='projects', + append=[self.project_data.project_id]), status_code=200, - json={'projects': [ - self.project_data.json_response['project']]}), + json={'project': + self.project_data.json_response['project']}), dict(method='GET', uri=self.get_mock_url( resource='role_assignments', @@ -482,10 +190,12 @@ def test_grant_role_user_project_exists(self): status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', - uri=self.get_mock_url(resource='projects'), + uri=self.get_mock_url( + resource='projects', + append=[self.project_data.project_id]), status_code=200, - json={'projects': [ - self.project_data.json_response['project']]}), + json={'project': + self.project_data.json_response['project']}), dict(method='GET', uri=self.get_mock_url( resource='role_assignments', @@ -512,10 +222,12 @@ def test_grant_role_user_project_exists(self): status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', - uri=self.get_mock_url(resource='projects'), + uri=self.get_mock_url( + resource='projects', + append=[self.project_data.project_id]), status_code=200, - json={'projects': [ - self.project_data.json_response['project']]}), + json={'project': + self.project_data.json_response['project']}), dict(method='GET', uri=self.get_mock_url( resource='role_assignments', @@ -552,10 +264,12 @@ def test_grant_role_group_project(self): status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(resource='projects'), + uri=self.get_mock_url( + resource='projects', + append=[self.project_data.project_id]), status_code=200, - json={'projects': [ - self.project_data.json_response['project']]}), + json={'project': + self.project_data.json_response['project']}), dict(method='GET', uri=self.get_mock_url(resource='groups'), status_code=200, @@ -582,10 +296,12 @@ def test_grant_role_group_project(self): status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(resource='projects'), + uri=self.get_mock_url( + resource='projects', + append=[self.project_data.project_id]), status_code=200, - json={'projects': [ - self.project_data.json_response['project']]}), + json={'project': + self.project_data.json_response['project']}), dict(method='GET', uri=self.get_mock_url(resource='groups'), status_code=200, @@ -625,10 +341,12 @@ def test_grant_role_group_project_exists(self): status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(resource='projects'), + uri=self.get_mock_url( + resource='projects', + append=[self.project_data.project_id]), status_code=200, - json={'projects': [ - self.project_data.json_response['project']]}), + json={'project': + self.project_data.json_response['project']}), dict(method='GET', uri=self.get_mock_url(resource='groups'), status_code=200, @@ -654,10 +372,12 @@ def test_grant_role_group_project_exists(self): status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(resource='projects'), + uri=self.get_mock_url( + resource='projects', + append=[self.project_data.project_id]), status_code=200, - json={'projects': [ - self.project_data.json_response['project']]}), + json={'project': + self.project_data.json_response['project']}), dict(method='GET', uri=self.get_mock_url(resource='groups'), status_code=200, @@ -1309,284 +1029,6 @@ def test_grant_role_group_domain_exists(self): domain=self.domain_data.domain_name)) self.assert_calls() - def test_revoke_role_user_v2(self): - self.use_keystone_v2() - self.register_uris([ - # user name - dict(method='GET', - uri=self.get_mock_url( - base_url_append='OS-KSADM', - resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url( - base_url_append=None, - resource='users', qs_elements=['name=%s' % - self.user_data.name]), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url( - base_url_append=None, - resource='tenants'), - status_code=200, - json={ - 'tenants': [ - self.project_data_v2.json_response['tenant']]}), - dict(method='GET', - uri=self.get_mock_url( - base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, 'users', - self.user_data.user_id, 'roles']), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='DELETE', - uri=self.get_mock_url(base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', - self.user_data.user_id, - 'roles', 'OS-KSADM', - self.role_data.role_id]), - status_code=204), - # user id - dict(method='GET', - uri=self.get_mock_url( - base_url_append='OS-KSADM', - resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url( - base_url_append=None, - resource='users'), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url( - base_url_append=None, - resource='tenants'), - status_code=200, - json={ - 'tenants': [ - self.project_data_v2.json_response['tenant']]}), - dict(method='GET', - uri=self.get_mock_url( - base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, 'users', - self.user_data.user_id, 'roles']), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='DELETE', - uri=self.get_mock_url(base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', - self.user_data.user_id, - 'roles', 'OS-KSADM', - self.role_data.role_id]), - status_code=204), - ]) - # user name - self.assertTrue(self.cloud.revoke_role( - self.role_data.role_name, - user=self.user_data.name, - project=self.project_data.project_id)) - # user id - self.assertTrue(self.cloud.revoke_role( - self.role_data.role_name, - user=self.user_data.user_id, - project=self.project_data.project_id)) - self.assert_calls() - - def test_revoke_role_user_project_v2(self): - self.use_keystone_v2() - self.register_uris([ - # role name and user name - dict(method='GET', - uri=self.get_mock_url(base_url_append='OS-KSADM', - resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='users', - qs_elements=['name=%s' % - self.user_data.name]), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants'), - status_code=200, - json={ - 'tenants': [ - self.project_data_v2.json_response['tenant']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', - self.user_data.user_id, - 'roles']), - status_code=200, - json={'roles': []}), - # role name and user id - dict(method='GET', - uri=self.get_mock_url(base_url_append='OS-KSADM', - resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='users'), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants'), - status_code=200, - json={ - 'tenants': [ - self.project_data_v2.json_response['tenant']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', - self.user_data.user_id, - 'roles']), - status_code=200, - json={'roles': []}), - # role id and user name - dict(method='GET', - uri=self.get_mock_url(base_url_append='OS-KSADM', - resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='users', - qs_elements=['name=%s' % - self.user_data.name]), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants'), - status_code=200, - json={ - 'tenants': [ - self.project_data_v2.json_response['tenant']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', - self.user_data.user_id, - 'roles']), - status_code=200, - json={'roles': []}), - # role id and user id - dict(method='GET', - uri=self.get_mock_url(base_url_append='OS-KSADM', - resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='users'), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants'), - status_code=200, - json={ - 'tenants': [ - self.project_data_v2.json_response['tenant']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', - self.user_data.user_id, - 'roles']), - status_code=200, - json={'roles': []}) - ]) - # role name and user name - self.assertFalse(self.cloud.revoke_role( - self.role_data.role_name, - user=self.user_data.name, - project=self.project_data.project_id)) - # role name and user id - self.assertFalse(self.cloud.revoke_role( - self.role_data.role_name, - user=self.user_data.user_id, - project=self.project_data.project_id)) - # role id and user name - self.assertFalse(self.cloud.revoke_role( - self.role_data.role_id, - user=self.user_data.name, - project=self.project_data.project_id)) - # role id and user id - self.assertFalse(self.cloud.revoke_role( - self.role_data.role_id, - user=self.user_data.user_id, - project=self.project_data.project_id)) - self.assert_calls() - - def test_revoke_role_user_project_v2_exists(self): - self.use_keystone_v2() - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(base_url_append='OS-KSADM', - resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='users', - qs_elements=['name=%s' % - self.user_data.name]), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants'), - status_code=200, - json={ - 'tenants': [ - self.project_data_v2.json_response['tenant']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', - self.user_data.user_id, - 'roles']), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='DELETE', - uri=self.get_mock_url(base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', - self.user_data.user_id, - 'roles', 'OS-KSADM', - self.role_data.role_id]), - status_code=204), - ]) - self.assertTrue(self.cloud.revoke_role( - self.role_data.role_name, - user=self.user_data.name, - project=self.project_data.project_id)) - self.assert_calls() - def test_revoke_role_user_project(self): self.register_uris([ # user name @@ -1601,10 +1043,12 @@ def test_revoke_role_user_project(self): status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', - uri=self.get_mock_url(resource='projects'), + uri=self.get_mock_url( + resource='projects', + append=[self.project_data.project_id]), status_code=200, - json={'projects': [ - self.project_data.json_response['project']]}), + json={'project': + self.project_data.json_response['project']}), dict(method='GET', uri=self.get_mock_url( resource='role_assignments', @@ -1625,10 +1069,12 @@ def test_revoke_role_user_project(self): status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', - uri=self.get_mock_url(resource='projects'), + uri=self.get_mock_url( + resource='projects', + append=[self.project_data.project_id]), status_code=200, - json={'projects': [ - self.project_data.json_response['project']]}), + json={'project': + self.project_data.json_response['project']}), dict(method='GET', uri=self.get_mock_url( resource='role_assignments', @@ -1666,10 +1112,12 @@ def test_revoke_role_user_project_exists(self): status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', - uri=self.get_mock_url(resource='projects'), + uri=self.get_mock_url( + resource='projects', + append=[self.project_data.project_id]), status_code=200, - json={'projects': [ - self.project_data.json_response['project']]}), + json={'project': + self.project_data.json_response['project']}), dict(method='GET', uri=self.get_mock_url( resource='role_assignments', @@ -1703,10 +1151,12 @@ def test_revoke_role_user_project_exists(self): status_code=200, json={'users': [self.user_data.json_response['user']]}), dict(method='GET', - uri=self.get_mock_url(resource='projects'), + uri=self.get_mock_url( + resource='projects', + append=[self.project_data.project_id]), status_code=200, - json={'projects': [ - self.project_data.json_response['project']]}), + json={'project': + self.project_data.json_response['project']}), dict(method='GET', uri=self.get_mock_url( resource='role_assignments', @@ -1750,10 +1200,12 @@ def test_revoke_role_group_project(self): status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(resource='projects'), + uri=self.get_mock_url( + resource='projects', + append=[self.project_data.project_id]), status_code=200, - json={'projects': [ - self.project_data.json_response['project']]}), + json={'project': + self.project_data.json_response['project']}), dict(method='GET', uri=self.get_mock_url(resource='groups'), status_code=200, @@ -1773,10 +1225,12 @@ def test_revoke_role_group_project(self): status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(resource='projects'), + uri=self.get_mock_url( + resource='projects', + append=[self.project_data.project_id]), status_code=200, - json={'projects': [ - self.project_data.json_response['project']]}), + json={'project': + self.project_data.json_response['project']}), dict(method='GET', uri=self.get_mock_url(resource='groups'), status_code=200, @@ -1809,10 +1263,12 @@ def test_revoke_role_group_project_exists(self): status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(resource='projects'), + uri=self.get_mock_url( + resource='projects', + append=[self.project_data.project_id]), status_code=200, - json={'projects': [ - self.project_data.json_response['project']]}), + json={'project': + self.project_data.json_response['project']}), dict(method='GET', uri=self.get_mock_url(resource='groups'), status_code=200, @@ -1845,10 +1301,12 @@ def test_revoke_role_group_project_exists(self): status_code=200, json={'roles': [self.role_data.json_response['role']]}), dict(method='GET', - uri=self.get_mock_url(resource='projects'), + uri=self.get_mock_url( + resource='projects', + append=[self.project_data.project_id]), status_code=200, - json={'projects': [ - self.project_data.json_response['project']]}), + json={'project': + self.project_data.json_response['project']}), dict(method='GET', uri=self.get_mock_url(resource='groups'), status_code=200, @@ -2684,11 +2142,13 @@ def test_grant_both_project_and_domain(self): json={'users': [self.user_data.json_response['user']]}), dict(method='GET', uri=self.get_mock_url( - resource=('projects?domain_id=%s' % - self.domain_data.domain_id)), + resource='projects', + append=[self.project_data.project_id], + qs_elements=['domain_id=' + self.domain_data.domain_id] + ), status_code=200, - json={'projects': - [self.project_data.json_response['project']]}), + json={'project': + self.project_data.json_response['project']}), dict(method='GET', uri=self.get_mock_url( resource='role_assignments', @@ -2739,11 +2199,12 @@ def test_revoke_both_project_and_domain(self): json={'users': [self.user_data.json_response['user']]}), dict(method='GET', uri=self.get_mock_url( - resource=('projects?domain_id=%s' % - self.domain_data.domain_id)), + resource='projects', + append=[self.project_data.project_id], + qs_elements=['domain_id=' + self.domain_data.domain_id]), status_code=200, - json={'projects': - [self.project_data.json_response['project']]}), + json={'project': + self.project_data.json_response['project']}), dict(method='GET', uri=self.get_mock_url( resource='role_assignments', @@ -2887,241 +2348,3 @@ def test_revoke_bad_domain_exception(self): user=self.user_data.name, domain='baddomain') self.assert_calls() - - def test_grant_role_user_project_v2_wait(self): - self.use_keystone_v2() - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(base_url_append='OS-KSADM', - resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='users', - qs_elements=['name=%s' % - self.user_data.name]), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants'), - status_code=200, - json={'tenants': [ - self.project_data_v2.json_response['tenant']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', - self.user_data.user_id, - 'roles']), - status_code=200, - json={'roles': []}), - dict(method='PUT', - uri=self.get_mock_url( - base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', self.user_data.user_id, 'roles', - 'OS-KSADM', self.role_data.role_id]), - status_code=201), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', - self.user_data.user_id, - 'roles']), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - ]) - self.assertTrue( - self.cloud.grant_role( - self.role_data.role_name, - user=self.user_data.name, - project=self.project_data.project_id, - wait=True)) - self.assert_calls() - - def test_grant_role_user_project_v2_wait_exception(self): - self.use_keystone_v2() - - with testtools.ExpectedException( - exc.OpenStackCloudTimeout, - 'Timeout waiting for role to be granted' - ): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(base_url_append='OS-KSADM', - resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='users', - qs_elements=['name=%s' % - self.user_data.name]), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants'), - status_code=200, - json={'tenants': [ - self.project_data_v2.json_response['tenant']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants', - append=[ - self.project_data_v2.project_id, - 'users', - self.user_data.user_id, - 'roles']), - status_code=200, - json={'roles': []}), - dict(method='PUT', - uri=self.get_mock_url( - base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', self.user_data.user_id, 'roles', - 'OS-KSADM', self.role_data.role_id]), - status_code=201), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants', - append=[ - self.project_data_v2.project_id, - 'users', - self.user_data.user_id, - 'roles']), - status_code=200, - json={'roles': []}), - ]) - self.assertTrue( - self.cloud.grant_role( - self.role_data.role_name, - user=self.user_data.name, - project=self.project_data.project_id, - wait=True, timeout=0.01)) - self.assert_calls(do_count=False) - - def test_revoke_role_user_project_v2_wait(self): - self.use_keystone_v2() - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(base_url_append='OS-KSADM', - resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='users', - qs_elements=['name=%s' % - self.user_data.name]), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants'), - status_code=200, - json={ - 'tenants': [ - self.project_data_v2.json_response['tenant']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', - self.user_data.user_id, - 'roles']), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='DELETE', - uri=self.get_mock_url(base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', - self.user_data.user_id, - 'roles', 'OS-KSADM', - self.role_data.role_id]), - status_code=204), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', - self.user_data.user_id, - 'roles']), - status_code=200, - json={'roles': []}), - ]) - self.assertTrue( - self.cloud.revoke_role( - self.role_data.role_name, - user=self.user_data.name, - project=self.project_data.project_id, - wait=True)) - self.assert_calls(do_count=False) - - def test_revoke_role_user_project_v2_wait_exception(self): - self.use_keystone_v2() - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(base_url_append='OS-KSADM', - resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='users', - qs_elements=['name=%s' % - self.user_data.name]), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants'), - status_code=200, - json={ - 'tenants': [ - self.project_data_v2.json_response['tenant']]}), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', - self.user_data.user_id, - 'roles']), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='DELETE', - uri=self.get_mock_url(base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', - self.user_data.user_id, - 'roles', 'OS-KSADM', - self.role_data.role_id]), - status_code=204), - dict(method='GET', - uri=self.get_mock_url(base_url_append=None, - resource='tenants', - append=[self.project_data_v2.project_id, - 'users', - self.user_data.user_id, - 'roles']), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - ]) - with testtools.ExpectedException( - exc.OpenStackCloudTimeout, - 'Timeout waiting for role to be revoked' - ): - self.assertTrue(self.cloud.revoke_role( - self.role_data.role_name, - user=self.user_data.name, - project=self.project_data.project_id, - wait=True, timeout=0.01)) - self.assert_calls(do_count=False) From 74fa9871a495d0c5103d37acc1ec3c00a12ece9e Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 11 May 2021 14:48:12 +0200 Subject: [PATCH 2871/3836] Add possibility to create subprojects in cloud layer Add previously missing support for managing sub-projects. Since this is not really working in v2 we depend on dropping v2 support from cloud layer. Change-Id: Ibcfe5d6233c4f1728229f5f1107aada459f7434a --- openstack/cloud/_identity.py | 9 ++++++--- openstack/tests/unit/base.py | 13 +++++++++---- openstack/tests/unit/cloud/test_project.py | 7 +++++-- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index ed2442f5d..5bb04b96f 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -88,8 +88,9 @@ def get_project(self, name_or_id, filters=None, domain_id=None): return _utils._get_entity(self, 'project', name_or_id, filters, domain_id=domain_id) - def update_project(self, name_or_id, enabled=None, domain_id=None, - **kwargs): + def update_project( + self, name_or_id, enabled=None, domain_id=None, **kwargs + ): project = self.identity.find_project( name_or_id=name_or_id, @@ -104,7 +105,7 @@ def update_project(self, name_or_id, enabled=None, domain_id=None, return project def create_project( - self, name, domain_id, description=None, enabled=True): + self, name, domain_id, description=None, enabled=True, **kwargs): """Create a project.""" attrs = dict( name=name, @@ -112,6 +113,8 @@ def create_project( domain_id=domain_id, is_enabled=enabled ) + if kwargs: + attrs.update(kwargs) return self.identity.create_project(**attrs) def delete_project(self, name_or_id, domain_id=None): diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 23085b3e0..2980d085a 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -37,7 +37,7 @@ _ProjectData = collections.namedtuple( 'ProjectData', 'project_id, project_name, enabled, domain_id, description, ' - 'json_response, json_request') + 'parent_id, json_response, json_request') _UserData = collections.namedtuple( @@ -247,9 +247,11 @@ def mock_for_keystone_projects(self, project=None, v3=True, def _get_project_data(self, project_name=None, enabled=None, domain_id=None, description=None, v3=True, - project_id=None): + project_id=None, parent_id=None): project_name = project_name or self.getUniqueString('projectName') project_id = uuid.UUID(project_id or uuid.uuid4().hex).hex + if parent_id: + parent_id = uuid.UUID(parent_id).hex response = {'id': project_id, 'name': project_name} request = {'name': project_name} domain_id = (domain_id or uuid.uuid4().hex) if v3 else None @@ -260,6 +262,9 @@ def _get_project_data(self, project_name=None, enabled=None, enabled = bool(enabled) response['enabled'] = enabled request['enabled'] = enabled + if parent_id: + request['parent_id'] = parent_id + response['parent_id'] = parent_id response.setdefault('enabled', True) request.setdefault('enabled', True) if description: @@ -267,8 +272,8 @@ def _get_project_data(self, project_name=None, enabled=None, request['description'] = description request.setdefault('description', None) return _ProjectData(project_id, project_name, enabled, domain_id, - description, {'project': response}, - {'project': request}) + description, parent_id, + {'project': response}, {'project': request}) def _get_group_data(self, name=None, domain_id=None, description=None): group_id = uuid.uuid4().hex diff --git a/openstack/tests/unit/cloud/test_project.py b/openstack/tests/unit/cloud/test_project.py index a90371909..4070c7a68 100644 --- a/openstack/tests/unit/cloud/test_project.py +++ b/openstack/tests/unit/cloud/test_project.py @@ -12,6 +12,7 @@ import testtools from testtools import matchers +import uuid import openstack.cloud import openstack.cloud._utils @@ -36,7 +37,8 @@ def get_mock_url(self, service_type='identity', interface='public', def test_create_project_v3(self,): project_data = self._get_project_data( - description=self.getUniqueString('projectDesc')) + description=self.getUniqueString('projectDesc'), + parent_id=uuid.uuid4().hex) reference_req = project_data.json_request.copy() reference_req['project']['enabled'] = True self.register_uris([ @@ -49,7 +51,8 @@ def test_create_project_v3(self,): project = self.cloud.create_project( name=project_data.project_name, description=project_data.description, - domain_id=project_data.domain_id) + domain_id=project_data.domain_id, + parent_id=project_data.parent_id) self.assertThat(project.id, matchers.Equals(project_data.project_id)) self.assertThat( project.name, matchers.Equals(project_data.project_name)) From 54f910f2a658a7af5410bd4530b41c59c96f889c Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 11 May 2021 14:48:40 +0200 Subject: [PATCH 2872/3836] Switch identity groups and users in cloud layer Methods for identity groups and users now also use proxy layer. Also here drop some parts of the v2 support (only cloud layer). Change-Id: Iae7c798df8780d8aa70ed54dc4b9eebd9c52b901 --- openstack/cloud/_identity.py | 157 ++++++------------ .../tests/functional/cloud/test_users.py | 6 +- openstack/tests/unit/cloud/test_caching.py | 12 +- openstack/tests/unit/cloud/test_groups.py | 19 ++- .../tests/unit/cloud/test_identity_roles.py | 75 --------- openstack/tests/unit/cloud/test_users.py | 64 +------ 6 files changed, 80 insertions(+), 253 deletions(-) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index 5bb04b96f..09d76ab82 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -158,9 +158,7 @@ def list_users(self, **kwargs): :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ - data = self._identity_client.get('/users', params=kwargs) - return _utils.normalize_users( - self._get_and_munchify('users', data)) + return list(self.identity.users(**kwargs)) @_utils.valid_kwargs('domain_id', 'name') def search_users(self, name_or_id=None, filters=None, **kwargs): @@ -215,17 +213,10 @@ def get_user_by_id(self, user_id, normalize=True): :returns: a single ``munch.Munch`` containing the user description """ - data = self._identity_client.get( - '/users/{user}'.format(user=user_id), - error_message="Error getting user with ID {user_id}".format( - user_id=user_id)) + user = self.identity.get_user(user_id) - user = self._get_and_munchify('user', data) - if user and normalize: - user = _utils.normalize_users(user) return user - # NOTE(Shrews): Keystone v2 supports updating only name, email and enabled. @_utils.valid_kwargs('name', 'email', 'enabled', 'domain_id', 'password', 'description', 'default_project') def update_user(self, name_or_id, **kwargs): @@ -238,39 +229,14 @@ def update_user(self, name_or_id, **kwargs): # TODO(mordred) When this changes to REST, force interface=admin # in the adapter call if it's an admin force call (and figure out how # to make that disctinction) - if self._is_client_version('identity', 2): - # Do not pass v3 args to a v2 keystone. - kwargs.pop('domain_id', None) - kwargs.pop('description', None) - kwargs.pop('default_project', None) - password = kwargs.pop('password', None) - if password is not None: - with _utils.shade_exceptions( - "Error updating password for {user}".format( - user=name_or_id)): - error_msg = "Error updating password for user {}".format( - name_or_id) - data = self._identity_client.put( - '/users/{u}/OS-KSADM/password'.format(u=user['id']), - json={'user': {'password': password}}, - error_message=error_msg) - - # Identity v2.0 implements PUT. v3 PATCH. Both work as PATCH. - data = self._identity_client.put( - '/users/{user}'.format(user=user['id']), json={'user': kwargs}, - error_message="Error in updating user {}".format(name_or_id)) - else: - # NOTE(samueldmq): now this is a REST call and domain_id is dropped - # if None. keystoneclient drops keys with None values. - if 'domain_id' in kwargs and kwargs['domain_id'] is None: - del kwargs['domain_id'] - data = self._identity_client.patch( - '/users/{user}'.format(user=user['id']), json={'user': kwargs}, - error_message="Error in updating user {}".format(name_or_id)) - - user = self._get_and_munchify('user', data) + # NOTE(samueldmq): now this is a REST call and domain_id is dropped + # if None. keystoneclient drops keys with None values. + if 'domain_id' in kwargs and kwargs['domain_id'] is None: + del kwargs['domain_id'] + user = self.identity.update_user(user, **kwargs) + self.list_users.invalidate(self) - return _utils.normalize_users([user])[0] + return user def create_user( self, name, password=None, email=None, default_project=None, @@ -278,41 +244,33 @@ def create_user( """Create a user.""" params = self._get_identity_params(domain_id, default_project) params.update({'name': name, 'password': password, 'email': email, - 'enabled': enabled}) - if self._is_client_version('identity', 3): - params['description'] = description - elif description is not None: - self.log.info( - "description parameter is not supported on Keystone v2") + 'enabled': enabled, 'description': description}) - error_msg = "Error in creating user {user}".format(user=name) - data = self._identity_client.post('/users', json={'user': params}, - error_message=error_msg) - user = self._get_and_munchify('user', data) + user = self.identity.create_user(**params) self.list_users.invalidate(self) - return _utils.normalize_users([user])[0] + return user @_utils.valid_kwargs('domain_id') def delete_user(self, name_or_id, **kwargs): # TODO(mordred) Why are we invalidating at the TOP? self.list_users.invalidate(self) - user = self.get_user(name_or_id, **kwargs) - if not user: - self.log.debug( - "User {0} not found for deleting".format(name_or_id)) - return False + try: + user = self.get_user(name_or_id, **kwargs) + if not user: + self.log.debug( + "User {0} not found for deleting".format(name_or_id)) + return False - # TODO(mordred) Extra GET only needed to support keystoneclient. - # Can be removed as a follow-on. - user = self.get_user_by_id(user['id'], normalize=False) - self._identity_client.delete( - '/users/{user}'.format(user=user['id']), - error_message="Error in deleting user {user}".format( - user=name_or_id)) + self.identity.delete_user(user) + self.list_users.invalidate(self) + return True - self.list_users.invalidate(self) - return True + except exceptions.SDKException: + self.log.exception("Error in deleting user {user}".format( + user=name_or_id + )) + return False def _get_user_and_group(self, user_name_or_id, group_name_or_id): user = self.get_user(user_name_or_id) @@ -909,9 +867,7 @@ def list_groups(self, **kwargs): :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ - data = self._identity_client.get( - '/groups', params=kwargs, error_message="Failed to list groups") - return _utils.normalize_groups(self._get_and_munchify('groups', data)) + return list(self.identity.groups(**kwargs)) @_utils.valid_kwargs('domain_id') def search_groups(self, name_or_id=None, filters=None, **kwargs): @@ -968,10 +924,8 @@ def create_group(self, name, description, domain=None): ) group_ref['domain_id'] = dom['id'] - error_msg = "Error creating group {group}".format(group=name) - data = self._identity_client.post( - '/groups', json={'group': group_ref}, error_message=error_msg) - group = self._get_and_munchify('group', data) + group = self.identity.create_group(**group_ref) + self.list_groups.invalidate(self) return _utils.normalize_groups([group])[0] @@ -990,7 +944,7 @@ def update_group(self, name_or_id, name=None, description=None, the OpenStack API call. """ self.list_groups.invalidate(self) - group = self.get_group(name_or_id, **kwargs) + group = self.identity.find_group(name_or_id, **kwargs) if group is None: raise exc.OpenStackCloudException( "Group {0} not found for updating".format(name_or_id) @@ -1002,11 +956,8 @@ def update_group(self, name_or_id, name=None, description=None, if description: group_ref['description'] = description - error_msg = "Unable to update group {name}".format(name=name_or_id) - data = self._identity_client.patch( - '/groups/{id}'.format(id=group['id']), - json={'group': group_ref}, error_message=error_msg) - group = self._get_and_munchify('group', data) + group = self.identity.update_group(group, **group_ref) + self.list_groups.invalidate(self) return _utils.normalize_groups([group])[0] @@ -1022,18 +973,22 @@ def delete_group(self, name_or_id, **kwargs): :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ - group = self.get_group(name_or_id, **kwargs) - if group is None: - self.log.debug( - "Group %s not found for deleting", name_or_id) - return False + try: + group = self.identity.find_group(name_or_id, **kwargs) + if group is None: + self.log.debug( + "Group %s not found for deleting", name_or_id) + return False - error_msg = "Unable to delete group {name}".format(name=name_or_id) - self._identity_client.delete('/groups/{id}'.format(id=group['id']), - error_message=error_msg) + self.identity.delete_group(group) - self.list_groups.invalidate(self) - return True + self.list_groups.invalidate(self) + return True + + except exceptions.SDKException: + self.log.exception( + "Unable to delete group {name}".format(name=name_or_id)) + return False @_utils.valid_kwargs('domain_id', 'name') def list_roles(self, **kwargs): @@ -1129,7 +1084,11 @@ def _keystone_v3_role_assignments(self, **filters): # 'include_names' and 'include_subtree' whose do not need any renaming. for k in ('group', 'role', 'user'): if k in filters: - filters[k + '.id'] = filters[k] + try: + filters[k + '.id'] = filters[k].id + except AttributeError: + # Also this goes away in next patches + filters[k + '.id'] = filters[k] del filters[k] for k in ('project', 'domain'): if k in filters: @@ -1199,14 +1158,7 @@ def list_role_assignments(self, filters=None): if isinstance(v, munch.Munch): filters[k] = v['id'] - if self._is_client_version('identity', 2): - if filters.get('project') is None or filters.get('user') is None: - raise exc.OpenStackCloudException( - "Must provide project and user for keystone v2" - ) - assignments = self._keystone_v2_role_assignments(**filters) - else: - assignments = self._keystone_v3_role_assignments(**filters) + assignments = self._keystone_v3_role_assignments(**filters) return _utils.normalize_role_assignments(assignments) @@ -1302,9 +1254,8 @@ def _get_grant_revoke_params(self, role, user=None, group=None, if user: if domain: - data['user'] = self.get_user(user, - domain_id=filters['domain_id'], - filters=filters) + data['user'] = self.get_user( + user, domain_id=filters['domain_id'], filters=filters) else: data['user'] = self.get_user(user, filters=filters) diff --git a/openstack/tests/functional/cloud/test_users.py b/openstack/tests/functional/cloud/test_users.py index 1dec4b9f1..f69b4c265 100644 --- a/openstack/tests/functional/cloud/test_users.py +++ b/openstack/tests/functional/cloud/test_users.py @@ -75,7 +75,7 @@ def test_create_user(self): self.assertIsNotNone(user) self.assertEqual(user_name, user['name']) self.assertEqual(user_email, user['email']) - self.assertTrue(user['enabled']) + self.assertTrue(user['is_enabled']) def test_delete_user(self): user_name = self.user_prefix + '_delete' @@ -92,7 +92,7 @@ def test_update_user(self): user_email = 'nobody@nowhere.com' user = self._create_user(name=user_name, email=user_email) self.assertIsNotNone(user) - self.assertTrue(user['enabled']) + self.assertTrue(user['is_enabled']) # Pass some keystone v3 params. This should work no matter which # version of keystone we are testing against. @@ -107,7 +107,7 @@ def test_update_user(self): self.assertEqual(user['id'], new_user['id']) self.assertEqual(user_name + '2', new_user['name']) self.assertEqual('somebody@nowhere.com', new_user['email']) - self.assertFalse(new_user['enabled']) + self.assertFalse(new_user['is_enabled']) def test_update_user_password(self): user_name = self.user_prefix + '_password' diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py index 0378d305e..17c54931c 100644 --- a/openstack/tests/unit/cloud/test_caching.py +++ b/openstack/tests/unit/cloud/test_caching.py @@ -21,6 +21,7 @@ from openstack.compute.v2 import flavor as _flavor from openstack.network.v2 import port as _port from openstack.identity.v3 import project as _project +from openstack.identity.v3 import user as _user from openstack import exceptions from openstack.tests import fakes from openstack.tests.unit import base @@ -116,6 +117,12 @@ def _compare_projects(self, exp, real): real.to_dict(computed=False) ) + def _compare_users(self, exp, real): + self.assertDictEqual( + _user.User(**exp).to_dict(computed=False), + real.to_dict(computed=False) + ) + def test_list_projects_v3(self): project_one = self._get_project_data() project_two = self._get_project_data() @@ -329,7 +336,7 @@ def test_modify_user_invalidates_cache(self): updated_users_list_resp = {'users': [new_resp['user']]} # Password is None in the original create below - user_data.json_request['user']['password'] = None + del user_data.json_request['user']['password'] uris_to_mock = [ # Inital User List is Empty @@ -355,12 +362,9 @@ def test_modify_user_invalidates_cache(self): dict(method='GET', uri=mock_users_url, status_code=200, json=updated_users_list_resp), # List User to get ID for delete - # Get user using user_id from list # delete user dict(method='GET', uri=mock_users_url, status_code=200, json=updated_users_list_resp), - dict(method='GET', uri=mock_user_resource_url, status_code=200, - json=new_resp), dict(method='DELETE', uri=mock_user_resource_url, status_code=204), # List Users Call (empty post delete) dict(method='GET', uri=mock_users_url, status_code=200, diff --git a/openstack/tests/unit/cloud/test_groups.py b/openstack/tests/unit/cloud/test_groups.py index 52eb8ff68..d83e51ce3 100644 --- a/openstack/tests/unit/cloud/test_groups.py +++ b/openstack/tests/unit/cloud/test_groups.py @@ -50,9 +50,10 @@ def test_delete_group(self): group_data = self._get_group_data() self.register_uris([ dict(method='GET', - uri=self.get_mock_url(), + uri=self.get_mock_url( + append=[group_data.group_id]), status_code=200, - json={'groups': [group_data.json_response['group']]}), + json={'group': group_data.json_response['group']}), dict(method='DELETE', uri=self.get_mock_url(append=[group_data.group_id]), status_code=204), @@ -84,14 +85,18 @@ def test_update_group(self): group_data.json_request['group'].pop('domain_id') self.register_uris([ dict(method='GET', - uri=self.get_mock_url(), + uri=self.get_mock_url( + append=[group_data.group_id]), status_code=200, - json={'groups': [group_data.json_response['group']]}), + json={'group': group_data.json_response['group']}), dict(method='PATCH', - uri=self.get_mock_url(append=[group_data.group_id]), + uri=self.get_mock_url( + append=[group_data.group_id]), status_code=200, json=group_data.json_response, - validate=dict(json=group_data.json_request)) + validate=dict(json={ + 'group': {'name': 'new_name', 'description': + 'new_description'}})) ]) self.cloud.update_group( - group_data.group_id, group_data.group_name, group_data.description) + group_data.group_id, 'new_name', 'new_description') diff --git a/openstack/tests/unit/cloud/test_identity_roles.py b/openstack/tests/unit/cloud/test_identity_roles.py index e8d503a4d..bfafc58d7 100644 --- a/openstack/tests/unit/cloud/test_identity_roles.py +++ b/openstack/tests/unit/cloud/test_identity_roles.py @@ -247,78 +247,3 @@ def test_list_role_assignments_exception(self): ): self.cloud.list_role_assignments() self.assert_calls() - - def test_list_role_assignments_keystone_v2(self): - self.use_keystone_v2() - role_data = self._get_role_data() - user_data = self._get_user_data() - project_data = self._get_project_data(v3=False) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - resource='tenants', - append=[project_data.project_id, - 'users', - user_data.user_id, - 'roles'], - base_url_append=None), - status_code=200, - json={'roles': [role_data.json_response['role']]}) - ]) - ret = self.cloud.list_role_assignments( - filters={ - 'user': user_data.user_id, - 'project': project_data.project_id}) - self.assertThat(len(ret), matchers.Equals(1)) - self.assertThat(ret[0].project, - matchers.Equals(project_data.project_id)) - self.assertThat(ret[0].id, matchers.Equals(role_data.role_id)) - self.assertThat(ret[0].user, matchers.Equals(user_data.user_id)) - self.assert_calls() - - def test_list_role_assignments_keystone_v2_with_role(self): - self.use_keystone_v2() - roles_data = [self._get_role_data() for r in range(0, 2)] - user_data = self._get_user_data() - project_data = self._get_project_data(v3=False) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - resource='tenants', - append=[project_data.project_id, - 'users', - user_data.user_id, - 'roles'], - base_url_append=None), - status_code=200, - json={'roles': [r.json_response['role'] for r in roles_data]}) - ]) - ret = self.cloud.list_role_assignments( - filters={ - 'role': roles_data[0].role_id, - 'user': user_data.user_id, - 'project': project_data.project_id}) - self.assertThat(len(ret), matchers.Equals(1)) - self.assertThat(ret[0].project, - matchers.Equals(project_data.project_id)) - self.assertThat(ret[0].id, matchers.Equals(roles_data[0].role_id)) - self.assertThat(ret[0].user, matchers.Equals(user_data.user_id)) - self.assert_calls() - - def test_list_role_assignments_exception_v2(self): - self.use_keystone_v2() - with testtools.ExpectedException( - openstack.cloud.OpenStackCloudException, - "Must provide project and user for keystone v2" - ): - self.cloud.list_role_assignments() - self.assert_calls() - - def test_list_role_assignments_exception_v2_no_project(self): - self.use_keystone_v2() - with testtools.ExpectedException( - openstack.cloud.OpenStackCloudException, - "Must provide project and user for keystone v2" - ): - self.cloud.list_role_assignments(filters={'user': '12345'}) - self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_users.py b/openstack/tests/unit/cloud/test_users.py index 3c054e213..955e93f29 100644 --- a/openstack/tests/unit/cloud/test_users.py +++ b/openstack/tests/unit/cloud/test_users.py @@ -43,28 +43,6 @@ def _get_user_list(self, user_data): } } - def test_create_user_v2(self): - self.use_keystone_v2() - - user_data = self._get_user_data() - - self.register_uris([ - dict(method='POST', - uri=self._get_keystone_mock_url(resource='users', v3=False), - status_code=200, - json=user_data.json_response, - validate=dict(json=user_data.json_request)), - ]) - - user = self.cloud.create_user( - name=user_data.name, email=user_data.email, - password=user_data.password) - - self.assertEqual(user_data.name, user.name) - self.assertEqual(user_data.email, user.email) - self.assertEqual(user_data.user_id, user.id) - self.assert_calls() - def test_create_user_v3(self): user_data = self._get_user_data( domain_id=uuid.uuid4().hex, @@ -89,39 +67,6 @@ def test_create_user_v3(self): self.assertEqual(user_data.user_id, user.id) self.assert_calls() - def test_update_user_password_v2(self): - self.use_keystone_v2() - - user_data = self._get_user_data(email='test@example.com') - mock_user_resource_uri = self._get_keystone_mock_url( - resource='users', append=[user_data.user_id], v3=False) - mock_users_uri = self._get_keystone_mock_url( - resource='users', v3=False) - - self.register_uris([ - # GET list to find user id - # PUT user with password update - # PUT empty update (password change is different than update) - # but is always chained together [keystoneclient oddity] - dict(method='GET', uri=mock_users_uri, status_code=200, - json=self._get_user_list(user_data)), - dict(method='PUT', - uri=self._get_keystone_mock_url( - resource='users', v3=False, - append=[user_data.user_id, 'OS-KSADM', 'password']), - status_code=200, json=user_data.json_response, - validate=dict( - json={'user': {'password': user_data.password}})), - dict(method='PUT', uri=mock_user_resource_uri, status_code=200, - json=user_data.json_response, - validate=dict(json={'user': {}}))]) - - user = self.cloud.update_user( - user_data.user_id, password=user_data.password) - self.assertEqual(user_data.name, user.name) - self.assertEqual(user_data.email, user.email) - self.assert_calls() - def test_create_user_v3_no_domain(self): user_data = self._get_user_data(domain_id=uuid.uuid4().hex, email='test@example.com') @@ -141,14 +86,11 @@ def test_delete_user(self): self.register_uris([ dict(method='GET', - uri=self._get_keystone_mock_url(resource='users', - qs_elements=['name=%s' % - user_data. - name]), + uri=self._get_keystone_mock_url( + resource='users', + qs_elements=['name=%s' % user_data.name]), status_code=200, json=self._get_user_list(user_data)), - dict(method='GET', uri=user_resource_uri, status_code=200, - json=user_data.json_response), dict(method='DELETE', uri=user_resource_uri, status_code=204)]) self.cloud.delete_user(user_data.name) From e192f215fa202cb9023c28c6410c0462541348ff Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 11 May 2021 14:49:09 +0200 Subject: [PATCH 2873/3836] Switch identity.domains in cloud layer identity.domains in the cloud layer go to proxy layer instead of reimplementing requests. Change-Id: I23e0ce374c5f854f9ff797616b02eb31f4593a7a --- openstack/cloud/_identity.py | 74 +++++++------------ openstack/tests/unit/cloud/test_domains.py | 43 +++++++---- .../tests/unit/cloud/test_role_assignment.py | 10 +-- 3 files changed, 57 insertions(+), 70 deletions(-) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index 09d76ab82..b4ccf1099 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -720,11 +720,7 @@ def create_domain(self, name, description=None, enabled=True): domain_ref = {'name': name, 'enabled': enabled} if description is not None: domain_ref['description'] = description - msg = 'Failed to create domain {name}'.format(name=name) - data = self._identity_client.post( - '/domains', json={'domain': domain_ref}, error_message=msg) - domain = self._get_and_munchify('domain', data) - return _utils.normalize_domains([domain])[0] + return self.identity.create_domain(**domain_ref) def update_domain( self, domain_id=None, name=None, description=None, @@ -745,13 +741,7 @@ def update_domain( domain_ref.update({'name': name} if name else {}) domain_ref.update({'description': description} if description else {}) domain_ref.update({'enabled': enabled} if enabled is not None else {}) - - error_msg = "Error in updating domain {id}".format(id=domain_id) - data = self._identity_client.patch( - '/domains/{id}'.format(id=domain_id), - json={'domain': domain_ref}, error_message=error_msg) - domain = self._get_and_munchify('domain', data) - return _utils.normalize_domains([domain])[0] + return self.identity.update_domain(domain_id, **domain_ref) def delete_domain(self, domain_id=None, name_or_id=None): """Delete a domain. @@ -764,25 +754,27 @@ def delete_domain(self, domain_id=None, name_or_id=None): :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ - if domain_id is None: - if name_or_id is None: - raise exc.OpenStackCloudException( - "You must pass either domain_id or name_or_id value" - ) - dom = self.get_domain(name_or_id=name_or_id) - if dom is None: - self.log.debug( - "Domain %s not found for deleting", name_or_id) - return False - domain_id = dom['id'] - - # A domain must be disabled before deleting - self.update_domain(domain_id, enabled=False) - error_msg = "Failed to delete domain {id}".format(id=domain_id) - self._identity_client.delete('/domains/{id}'.format(id=domain_id), - error_message=error_msg) + try: + if domain_id is None: + if name_or_id is None: + raise exc.OpenStackCloudException( + "You must pass either domain_id or name_or_id value" + ) + dom = self.get_domain(name_or_id=name_or_id) + if dom is None: + self.log.debug( + "Domain %s not found for deleting", name_or_id) + return False + domain_id = dom['id'] + + # A domain must be disabled before deleting + self.identity.update_domain(domain_id, is_enabled=False) + self.identity.delete_domain(domain_id, ignore_missing=False) - return True + return True + except exceptions.SDKException: + self.log.exception("Failed to delete domain %s" % domain_id) + raise def list_domains(self, **filters): """List Keystone domains. @@ -792,10 +784,7 @@ def list_domains(self, **filters): :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ - data = self._identity_client.get( - '/domains', params=filters, error_message="Failed to list domains") - domains = self._get_and_munchify('domains', data) - return _utils.normalize_domains(domains) + return list(self.identity.domains(**filters)) def search_domains(self, filters=None, name_or_id=None): """Search Keystone domains. @@ -840,20 +829,11 @@ def get_domain(self, domain_id=None, name_or_id=None, filters=None): the OpenStack API call. """ if domain_id is None: - # NOTE(SamYaple): search_domains() has filters and name_or_id - # in the wrong positional order which prevents _get_entity from - # being able to return quickly if passing a domain object so we - # duplicate that logic here - if hasattr(name_or_id, 'id'): - return name_or_id - return _utils._get_entity(self, 'domain', filters, name_or_id) + if not filters: + filters = {} + return self.identity.find_domain(name_or_id, **filters) else: - error_msg = 'Failed to get domain {id}'.format(id=domain_id) - data = self._identity_client.get( - '/domains/{id}'.format(id=domain_id), - error_message=error_msg) - domain = self._get_and_munchify('domain', data) - return _utils.normalize_domains([domain])[0] + return self.identity.get_domain(domain_id) @_utils.valid_kwargs('domain_id') @_utils.cache_on_arguments() diff --git a/openstack/tests/unit/cloud/test_domains.py b/openstack/tests/unit/cloud/test_domains.py index f57b0514d..29392e492 100644 --- a/openstack/tests/unit/cloud/test_domains.py +++ b/openstack/tests/unit/cloud/test_domains.py @@ -26,10 +26,12 @@ class TestDomains(base.TestCase): def get_mock_url(self, service_type='identity', resource='domains', - append=None, base_url_append='v3'): + append=None, base_url_append='v3', + qs_elements=None): return super(TestDomains, self).get_mock_url( service_type=service_type, resource=resource, - append=append, base_url_append=base_url_append) + append=append, base_url_append=base_url_append, + qs_elements=qs_elements) def test_list_domains(self): domain_data = self._get_domain_data() @@ -60,10 +62,20 @@ def test_get_domain_with_name_or_id(self): domain_data = self._get_domain_data() response = {'domains': [domain_data.json_response['domain']]} self.register_uris([ - dict(method='GET', uri=self.get_mock_url(), status_code=200, + dict(method='GET', + uri=self.get_mock_url(append=[domain_data.domain_id]), + status_code=200, + json=domain_data.json_response), + dict(method='GET', + uri=self.get_mock_url(append=[domain_data.domain_name]), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + qs_elements=['name=' + domain_data.domain_name] + ), + status_code=200, json=response), - dict(method='GET', uri=self.get_mock_url(), status_code=200, - json=response)]) + ]) domain = self.cloud.get_domain(name_or_id=domain_data.domain_id) domain_by_name = self.cloud.get_domain( name_or_id=domain_data.domain_name) @@ -94,8 +106,7 @@ def test_create_domain_exception(self): domain_data = self._get_domain_data(domain_name='domain_name', enabled=True) with testtools.ExpectedException( - openstack.cloud.OpenStackCloudBadRequest, - "Failed to create domain domain_name" + openstack.cloud.OpenStackCloudBadRequest ): self.register_uris([ dict(method='POST', uri=self.get_mock_url(), status_code=400, @@ -124,8 +135,10 @@ def test_delete_domain_name_or_id(self): domain_resource_uri = self.get_mock_url(append=[domain_data.domain_id]) self.register_uris([ - dict(method='GET', uri=self.get_mock_url(), status_code=200, - json={'domains': [domain_data.json_response['domain']]}), + dict(method='GET', + uri=self.get_mock_url(append=[domain_data.domain_id]), + status_code=200, + json={'domain': domain_data.json_response['domain']}), dict(method='PATCH', uri=domain_resource_uri, status_code=200, json=new_resp, validate=dict(json={'domain': {'enabled': False}})), @@ -149,8 +162,7 @@ def test_delete_domain_exception(self): validate=dict(json={'domain': {'enabled': False}})), dict(method='DELETE', uri=domain_resource_uri, status_code=404)]) with testtools.ExpectedException( - openstack.cloud.OpenStackCloudURINotFound, - "Failed to delete domain %s" % domain_data.domain_id + openstack.exceptions.ResourceNotFound ): self.cloud.delete_domain(domain_data.domain_id) self.assert_calls() @@ -178,8 +190,10 @@ def test_update_domain_name_or_id(self): description=self.getUniqueString('domainDesc')) domain_resource_uri = self.get_mock_url(append=[domain_data.domain_id]) self.register_uris([ - dict(method='GET', uri=self.get_mock_url(), status_code=200, - json={'domains': [domain_data.json_response['domain']]}), + dict(method='GET', + uri=self.get_mock_url(append=[domain_data.domain_id]), + status_code=200, + json={'domain': domain_data.json_response['domain']}), dict(method='PATCH', uri=domain_resource_uri, status_code=200, json=domain_data.json_response, validate=dict(json=domain_data.json_request))]) @@ -203,8 +217,7 @@ def test_update_domain_exception(self): json=domain_data.json_response, validate=dict(json={'domain': {'enabled': False}}))]) with testtools.ExpectedException( - openstack.exceptions.ConflictException, - "Error in updating domain %s" % domain_data.domain_id + openstack.exceptions.ConflictException ): self.cloud.delete_domain(domain_data.domain_id) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_role_assignment.py b/openstack/tests/unit/cloud/test_role_assignment.py index 14da6b3f4..a59cb23b2 100644 --- a/openstack/tests/unit/cloud/test_role_assignment.py +++ b/openstack/tests/unit/cloud/test_role_assignment.py @@ -2316,10 +2316,7 @@ def test_grant_bad_domain_exception(self): headers={'Content-Type': 'text/plain'}, text='Could not find domain: baddomain') ]) - with testtools.ExpectedException( - exc.OpenStackCloudURINotFound, - 'Failed to get domain baddomain' - ): + with testtools.ExpectedException(exc.OpenStackCloudURINotFound): self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, @@ -2339,10 +2336,7 @@ def test_revoke_bad_domain_exception(self): headers={'Content-Type': 'text/plain'}, text='Could not find domain: baddomain') ]) - with testtools.ExpectedException( - exc.OpenStackCloudURINotFound, - 'Failed to get domain baddomain' - ): + with testtools.ExpectedException(exc.OpenStackCloudURINotFound): self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name, From 9a038d8ca8e68bc0f65dd389f181454857d1e559 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 11 May 2021 15:04:08 +0200 Subject: [PATCH 2874/3836] Switch identity.service in cloud layer to proxy Switch service methods to rely on proxy instead of reimplementing requests directly. Also here drop v2 support (also for endpoint tests due to dependency). Change-Id: Ic856855f8040f88e618926bf0fdf14b0daa589d9 --- openstack/cloud/_identity.py | 66 +++---------- .../tests/functional/cloud/test_services.py | 2 +- openstack/tests/unit/cloud/test_endpoints.py | 98 ------------------- openstack/tests/unit/cloud/test_services.py | 35 ------- 4 files changed, 13 insertions(+), 188 deletions(-) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index b4ccf1099..bf23babcf 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -368,29 +368,15 @@ def create_service(self, name, enabled=True, **kwargs): # TODO(mordred) When this changes to REST, force interface=admin # in the adapter call - if self._is_client_version('identity', 2): - url, key = '/OS-KSADM/services', 'OS-KSADM:service' - kwargs['type'] = type_ or service_type - else: - url, key = '/services', 'service' - kwargs['type'] = type_ or service_type - kwargs['enabled'] = enabled + kwargs['type'] = type_ or service_type + kwargs['is_enabled'] = enabled kwargs['name'] = name - msg = 'Failed to create service {name}'.format(name=name) - data = self._identity_client.post( - url, json={key: kwargs}, error_message=msg) - service = self._get_and_munchify(key, data) - return _utils.normalize_keystone_services([service])[0] + return self.identity.create_service(**kwargs) @_utils.valid_kwargs('name', 'enabled', 'type', 'service_type', 'description') def update_service(self, name_or_id, **kwargs): - # NOTE(SamYaple): Service updates are only available on v3 api - if self._is_client_version('identity', 2): - raise exc.OpenStackCloudUnavailableFeature( - 'Unavailable Feature: Service update requires Identity v3' - ) # NOTE(SamYaple): Keystone v3 only accepts 'type' but shade accepts # both 'type' and 'service_type' with a preference @@ -400,18 +386,8 @@ def update_service(self, name_or_id, **kwargs): if type_ or service_type: kwargs['type'] = type_ or service_type - if self._is_client_version('identity', 2): - url, key = '/OS-KSADM/services', 'OS-KSADM:service' - else: - url, key = '/services', 'service' - service = self.get_service(name_or_id) - msg = 'Error in updating service {service}'.format(service=name_or_id) - data = self._identity_client.patch( - '{url}/{id}'.format(url=url, id=service['id']), json={key: kwargs}, - error_message=msg) - service = self._get_and_munchify(key, data) - return _utils.normalize_keystone_services([service])[0] + return self.identity.update_service(service, **kwargs) def list_services(self): """List all Keystone services. @@ -421,18 +397,7 @@ def list_services(self): :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ - if self._is_client_version('identity', 2): - url, key = '/OS-KSADM/services', 'OS-KSADM:services' - endpoint_filter = {'interface': 'admin'} - else: - url, key = '/services', 'services' - endpoint_filter = {} - - data = self._identity_client.get( - url, endpoint_filter=endpoint_filter, - error_message="Failed to list services") - services = self._get_and_munchify(key, data) - return _utils.normalize_keystone_services(services) + return list(self.identity.services()) def search_services(self, name_or_id=None, filters=None): """Search Keystone services. @@ -482,20 +447,13 @@ def delete_service(self, name_or_id): if service is None: self.log.debug("Service %s not found for deleting", name_or_id) return False - - if self._is_client_version('identity', 2): - url = '/OS-KSADM/services' - endpoint_filter = {'interface': 'admin'} - else: - url = '/services' - endpoint_filter = {} - - error_msg = 'Failed to delete service {id}'.format(id=service['id']) - self._identity_client.delete( - '{url}/{id}'.format(url=url, id=service['id']), - endpoint_filter=endpoint_filter, error_message=error_msg) - - return True + try: + self.identity.delete_service(service) + return True + except exceptions.SDKException: + self.log.exception( + 'Failed to delete service {id}'.format(id=service['id'])) + return False @_utils.valid_kwargs('public_url', 'internal_url', 'admin_url') def create_endpoint(self, service_name_or_id, url=None, interface=None, diff --git a/openstack/tests/functional/cloud/test_services.py b/openstack/tests/functional/cloud/test_services.py index 4a37c219b..604dd7848 100644 --- a/openstack/tests/functional/cloud/test_services.py +++ b/openstack/tests/functional/cloud/test_services.py @@ -83,7 +83,7 @@ def test_update_service(self): self.new_service_name + '_update') self.assertEqual(new_service.description, 'this is an updated description') - self.assertFalse(new_service.enabled) + self.assertFalse(new_service.is_enabled) self.assertEqual(service.id, new_service.id) def test_list_services(self): diff --git a/openstack/tests/unit/cloud/test_endpoints.py b/openstack/tests/unit/cloud/test_endpoints.py index d05735d5e..bddcb953d 100644 --- a/openstack/tests/unit/cloud/test_endpoints.py +++ b/openstack/tests/unit/cloud/test_endpoints.py @@ -21,8 +21,6 @@ import uuid -from openstack.cloud.exc import OpenStackCloudException -from openstack.cloud.exc import OpenStackCloudUnavailableFeature from openstack.tests.unit import base from testtools import matchers @@ -37,97 +35,6 @@ def get_mock_url(self, service_type='identity', interface='public', def _dummy_url(self): return 'https://%s.example.com/' % uuid.uuid4().hex - def test_create_endpoint_v2(self): - self.use_keystone_v2() - service_data = self._get_service_data() - endpoint_data = self._get_endpoint_v2_data( - service_data.service_id, public_url=self._dummy_url(), - internal_url=self._dummy_url(), admin_url=self._dummy_url()) - other_endpoint_data = self._get_endpoint_v2_data( - service_data.service_id, region=endpoint_data.region, - public_url=endpoint_data.public_url) - # correct the keys - - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - resource='services', base_url_append='OS-KSADM'), - status_code=200, - json={'OS-KSADM:services': [ - service_data.json_response_v2['OS-KSADM:service']]}), - dict(method='POST', - uri=self.get_mock_url(base_url_append=None), - status_code=200, - json=endpoint_data.json_response, - validate=dict(json=endpoint_data.json_request)), - dict(method='GET', - uri=self.get_mock_url( - resource='services', base_url_append='OS-KSADM'), - status_code=200, - json={'OS-KSADM:services': [ - service_data.json_response_v2['OS-KSADM:service']]}), - # NOTE(notmorgan): There is a stupid happening here, we do two - # gets on the services for some insane reason (read: keystoneclient - # is bad and should feel bad). - dict(method='GET', - uri=self.get_mock_url( - resource='services', base_url_append='OS-KSADM'), - status_code=200, - json={'OS-KSADM:services': [ - service_data.json_response_v2['OS-KSADM:service']]}), - dict(method='POST', - uri=self.get_mock_url(base_url_append=None), - status_code=200, - json=other_endpoint_data.json_response, - validate=dict(json=other_endpoint_data.json_request)) - ]) - - endpoints = self.cloud.create_endpoint( - service_name_or_id=service_data.service_id, - region=endpoint_data.region, - public_url=endpoint_data.public_url, - internal_url=endpoint_data.internal_url, - admin_url=endpoint_data.admin_url - ) - - self.assertThat(endpoints[0].id, - matchers.Equals(endpoint_data.endpoint_id)) - self.assertThat(endpoints[0].region, - matchers.Equals(endpoint_data.region)) - self.assertThat(endpoints[0].publicURL, - matchers.Equals(endpoint_data.public_url)) - self.assertThat(endpoints[0].internalURL, - matchers.Equals(endpoint_data.internal_url)) - self.assertThat(endpoints[0].adminURL, - matchers.Equals(endpoint_data.admin_url)) - - # test v3 semantics on v2.0 endpoint - self.assertRaises(OpenStackCloudException, - self.cloud.create_endpoint, - service_name_or_id='service1', - interface='mock_admin_url', - url='admin') - - endpoints_3on2 = self.cloud.create_endpoint( - service_name_or_id=service_data.service_id, - region=endpoint_data.region, - interface='public', - url=endpoint_data.public_url - ) - - # test keys and values are correct - self.assertThat( - endpoints_3on2[0].region, - matchers.Equals(other_endpoint_data.region)) - self.assertThat( - endpoints_3on2[0].publicURL, - matchers.Equals(other_endpoint_data.public_url)) - self.assertThat(endpoints_3on2[0].get('internalURL'), - matchers.Equals(None)) - self.assertThat(endpoints_3on2[0].get('adminURL'), - matchers.Equals(None)) - self.assert_calls() - def test_create_endpoint_v3(self): service_data = self._get_service_data() public_endpoint_data = self._get_endpoint_v3_data( @@ -227,11 +134,6 @@ def test_create_endpoint_v3(self): self.assertThat(result.enabled, matchers.Equals(reference.enabled)) self.assert_calls() - def test_update_endpoint_v2(self): - self.use_keystone_v2() - self.assertRaises(OpenStackCloudUnavailableFeature, - self.cloud.update_endpoint, 'endpoint_id') - def test_update_endpoint_v3(self): service_data = self._get_service_data() dummy_url = self._dummy_url() diff --git a/openstack/tests/unit/cloud/test_services.py b/openstack/tests/unit/cloud/test_services.py index b718d57f5..63e715246 100644 --- a/openstack/tests/unit/cloud/test_services.py +++ b/openstack/tests/unit/cloud/test_services.py @@ -20,7 +20,6 @@ """ from openstack.cloud.exc import OpenStackCloudException -from openstack.cloud.exc import OpenStackCloudUnavailableFeature from openstack.tests.unit import base from testtools import matchers @@ -36,33 +35,6 @@ def get_mock_url(self, service_type='identity', interface='public', return super(CloudServices, self).get_mock_url( service_type, interface, resource, append, base_url_append) - def test_create_service_v2(self): - self.use_keystone_v2() - service_data = self._get_service_data(name='a service', type='network', - description='A test service') - reference_req = service_data.json_request.copy() - reference_req.pop('enabled') - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url(base_url_append='OS-KSADM'), - status_code=200, - json=service_data.json_response_v2, - validate=dict(json={'OS-KSADM:service': reference_req})) - ]) - - service = self.cloud.create_service( - name=service_data.service_name, - service_type=service_data.service_type, - description=service_data.description) - self.assertThat(service.name, - matchers.Equals(service_data.service_name)) - self.assertThat(service.id, matchers.Equals(service_data.service_id)) - self.assertThat(service.description, - matchers.Equals(service_data.description)) - self.assertThat(service.type, - matchers.Equals(service_data.service_type)) - self.assert_calls() - def test_create_service_v3(self): service_data = self._get_service_data(name='a service', type='network', description='A test service') @@ -87,13 +59,6 @@ def test_create_service_v3(self): matchers.Equals(service_data.service_type)) self.assert_calls() - def test_update_service_v2(self): - self.use_keystone_v2() - # NOTE(SamYaple): Update service only works with v3 api - self.assertRaises(OpenStackCloudUnavailableFeature, - self.cloud.update_service, - 'service_id', name='new name') - def test_update_service_v3(self): service_data = self._get_service_data(name='a service', type='network', description='A test service') From df5f987c1e3d77d037fa14d06189f0d343107bcb Mon Sep 17 00:00:00 2001 From: Ashley Rodriguez Date: Mon, 15 Feb 2021 19:32:21 +0000 Subject: [PATCH 2875/3836] Add share resource to shared file system Introduce Shares class with basic methods including list, create, delete, get and update to shared file system storage service. [1] https://tree.taiga.io/project/ashrod98-openstacksdk-manila-support/us/11?kanban-status=2360120 Change-Id: Ia7d7c88eb55307296336e2f04a00e92bdca6f8ae --- .../user/proxies/shared_file_system.rst | 11 ++ .../resources/shared_file_system/index.rst | 1 + .../resources/shared_file_system/v2/share.rst | 13 ++ openstack/shared_file_system/v2/_proxy.py | 126 +++++++++++++++++- openstack/shared_file_system/v2/share.py | 102 ++++++++++++++ .../functional/shared_file_system/base.py | 7 + .../shared_file_system/test_share.py | 60 +++++++++ .../unit/shared_file_system/v2/test_proxy.py | 64 +++++++++ .../unit/shared_file_system/v2/test_share.py | 111 +++++++++++++++ ...d-file-system-shares-e9f356a318045607.yaml | 5 + 10 files changed, 498 insertions(+), 2 deletions(-) create mode 100644 doc/source/user/resources/shared_file_system/v2/share.rst create mode 100644 openstack/shared_file_system/v2/share.py create mode 100644 openstack/tests/functional/shared_file_system/test_share.py create mode 100644 openstack/tests/unit/shared_file_system/v2/test_proxy.py create mode 100644 openstack/tests/unit/shared_file_system/v2/test_share.py create mode 100644 releasenotes/notes/add-shared-file-system-shares-e9f356a318045607.yaml diff --git a/doc/source/user/proxies/shared_file_system.rst b/doc/source/user/proxies/shared_file_system.rst index 3e8ae7388..759d19a39 100644 --- a/doc/source/user/proxies/shared_file_system.rst +++ b/doc/source/user/proxies/shared_file_system.rst @@ -22,3 +22,14 @@ service. .. autoclass:: openstack.shared_file_system.v2._proxy.Proxy :noindex: :members: availability_zones + + +Shared File System Shares +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Interact with Shares supported by the Shared File Systems +service. + +.. autoclass:: openstack.shared_file_system.v2._proxy.Proxy + :noindex: + :members: shares, get_share, delete_share, update_share, create_share diff --git a/doc/source/user/resources/shared_file_system/index.rst b/doc/source/user/resources/shared_file_system/index.rst index 1be3611bd..4d31d2ed7 100644 --- a/doc/source/user/resources/shared_file_system/index.rst +++ b/doc/source/user/resources/shared_file_system/index.rst @@ -5,3 +5,4 @@ Shared File System service resources :maxdepth: 1 v2/availability_zone + v2/share diff --git a/doc/source/user/resources/shared_file_system/v2/share.rst b/doc/source/user/resources/shared_file_system/v2/share.rst new file mode 100644 index 000000000..bac5e9602 --- /dev/null +++ b/doc/source/user/resources/shared_file_system/v2/share.rst @@ -0,0 +1,13 @@ +openstack.shared_file_system.v2.share +===================================== + +.. automodule:: openstack.shared_file_system.v2.share + +The Share Class +--------------- + +The ``Share`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.shared_file_system.v2.share.Share + :members: diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index 0f6a688c4..2f27d0d80 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -11,7 +11,10 @@ # under the License. from openstack import proxy -from openstack.shared_file_system.v2 import availability_zone +from openstack import resource +from openstack.shared_file_system.v2 import ( + availability_zone as _availability_zone) +from openstack.shared_file_system.v2 import share as _share class Proxy(proxy.Proxy): @@ -23,4 +26,123 @@ def availability_zones(self): :rtype: :class:`~openstack.shared_file_system.v2. \availability_zone.AvailabilityZone` """ - return self._list(availability_zone.AvailabilityZone) + return self._list(_availability_zone.AvailabilityZone) + + def shares(self, details=True, **query): + """Lists all shares with details + + :param kwargs query: Optional query parameters to be sent to limit + the shares being returned. Available parameters include: + + * status: Filters by a share status + * share_server_id: The UUID of the share server. + * metadata: One or more metadata key and value pairs as a url + encoded dictionary of strings. + * extra_specs: The extra specifications as a set of one or more + key-value pairs. + * share_type_id: The UUID of a share type to query resources by. + * name: The user defined name of the resource to filter resources + by. + * snapshot_id: The UUID of the share’s base snapshot to filter + the request based on. + * host: The host name of the resource to query with. + * share_network_id: The UUID of the share network to filter + resources by. + * project_id: The ID of the project that owns the resource. + * is_public: A boolean query parameter that, when set to true, + allows retrieving public resources that belong to + all projects. + * share_group_id: The UUID of a share group to filter resource. + * export_location_id: The export location UUID that can be used + to filter shares or share instances. + * export_location_path: The export location path that can be used + to filter shares or share instances. + * name~: The name pattern that can be used to filter shares, share + snapshots, share networks or share groups. + * description~: The description pattern that can be used to filter + shares, share snapshots, share networks or share groups. + * with_count: Whether to show count in API response or not, + default is False. + * limit: The maximum number of shares to return. + * offset: The offset to define start point of share or share group + listing. + * sort_key: The key to sort a list of shares. + * sort_dir: The direction to sort a list of shares. A valid value + is asc, or desc. + + :returns: Details of shares resources + :rtype: :class:`~openstack.shared_file_system.v2. + share.Share` + """ + base_path = '/shares/detail' if details else None + return self._list(_share.Share, base_path=base_path, **query) + + def get_share(self, share_id): + """Lists details of a single share + + :param share: The ID of the share to get + :returns: Details of the identified share + :rtype: :class:`~openstack.shared_file_system.v2. + share.Share` + """ + return self._get(_share.Share, share_id) + + def delete_share(self, share, ignore_missing=True): + """Deletes a single share + + :param share: The ID of the share to delete + :returns: Result of the ``delete`` + :rtype: ``None`` + """ + self._delete(_share.Share, share, + ignore_missing=ignore_missing) + + def update_share(self, share_id, **attrs): + """Updates details of a single share. + + :param share: The ID of the share to update + :pram dict attrs: The attributes to update on the share + :returns: the updated share + :rtype: :class:`~openstack.shared_file_system.v2. + share.Share` + """ + return self._update(_share.Share, share_id, **attrs) + + def create_share(self, **attrs): + """Creates a share from attributes + + :returns: Details of the new share + :param dict attrs: Attributes which will be used to create + a :class:`~openstack.shared_file_system.v2. + shares.Shares`,comprised of the properties + on the Shares class. 'size' and 'share' + are required to create a share. + :rtype: :class:`~openstack.shared_file_system.v2. + share.Share` + """ + return self._create(_share.Share, **attrs) + + def wait_for_status(self, res, status='active', failures=None, + interval=2, wait=120): + """Wait for a resource to be in a particular status. + :param res: The resource to wait on to reach the specified status. + The resource must have a ``status`` attribute. + :type resource: A :class:`~openstack.resource.Resource` object. + :param status: Desired status. + :param failures: Statuses that would be interpreted as failures. + :type failures: :py:class:`list` + :param interval: Number of seconds to wait before to consecutive + checks. Default to 2. + :param wait: Maximum number of seconds to wait before the change. + Default to 120. + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to the desired status failed to occur in specified seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + has transited to one of the failure statuses. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute. + """ + failures = [] if failures is None else failures + return resource.wait_for_status( + self, res, status, failures, interval, wait) diff --git a/openstack/shared_file_system/v2/share.py b/openstack/shared_file_system/v2/share.py new file mode 100644 index 000000000..d2bd95d48 --- /dev/null +++ b/openstack/shared_file_system/v2/share.py @@ -0,0 +1,102 @@ +# 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 resource + + +class Share(resource.Resource): + resource_key = "share" + resources_key = "shares" + base_path = "/shares" + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + allow_head = False + + #: Properties + #: The share instance access rules status. A valid value is active, + #: error, or syncing. + access_rules_status = resource.Body("access_rules_status", type=str) + #: The availability zone. + availability_zone = resource.Body("availability_zone", type=str) + #: The date and time stamp when the resource was created within the + #: service’s database. + created_at = resource.Body("created_at", type=str) + #: The user defined description of the resource. + description = resource.Body("description", type=str) + #: The share host name. + host = resource.Body("host", type=str) + #: The level of visibility for the share. + is_public = resource.Body("is_public", type=bool) + #: Whether or not this share supports snapshots that can be + #: cloned into new shares. + is_creating_new_share_from_snapshot_supported = resource.Body( + "create_share_from_snapshot_support", type=bool) + #: Whether the share's snapshots can be mounted directly and access + #: controlled independently or not. + is_mounting_snapshot_supported = resource.Body( + "mount_snapshot_support", type=bool) + #: Whether the share can be reverted to its latest snapshot or not. + is_reverting_to_snapshot_supported = resource.Body( + "revert_to_snapshot_support", type=bool) + #: An extra specification that filters back ends by whether the share + #: supports snapshots or not. + is_snapshot_supported = resource.Body( + "snapshot_support", type=bool) + #: Indicates whether the share has replicas or not. + is_replicated = resource.Body("has_replicas", type=bool) + #: One or more metadata key and value pairs as a dictionary of strings. + metadata = resource.Body("metadata", type=dict) + #: The progress of the share creation. + progress = resource.Body("progress", type=str) + #: The ID of the project that owns the resource. + project_id = resource.Body("project_id", type=str) + #: The share replication type. Valid values are none, readable, + #: writable and dr. + replication_type = resource.Body("replication_type", type=str) + #: The UUID of the share group that this shares belongs to. + share_group_id = resource.Body("share_group_id", type=str) + #: The share network ID. + share_network_id = resource.Body("share_network_id", type=str) + #: The Shared File Systems protocol. A valid value is NFS, + #: CIFS, GlusterFS, HDFS, CephFS, MAPRFS + share_protocol = resource.Body("share_proto", type=str) + #: The UUID of the share server. + share_server_id = resource.Body("share_server_id", type=str) + #: The UUID of the share type. In minor versions, this parameter is a + #: share type name, as a string. + share_type = resource.Body("share_type", type=str) + #: Name of the share type. + share_type_name = resource.Body("share_type_name", type=str) + #: The share size, in GiBs. + size = resource.Body("size", type=int) + #: The UUID of the snapshot that was used to create the + #: share. + snapshot_id = resource.Body("snapshot_id", type=str) + #: The ID of the group snapshot instance that was used to create + #: this share. + source_share_group_snapshot_member_id = resource.Body( + "source_share_group_snapshot_member_id", type=str) + #: The share status + status = resource.Body("status", type=str) + #: For the share migration, the migration task state. + task_state = resource.Body("task_state", type=str) + #: ID of the user that the share was created by. + user_id = resource.Body("user_id", type=str) + #: Display name for updating name + display_name = resource.Body("display_name", type=str) + #: Display description for updating description + display_description = resource.Body("display_description", type=str) diff --git a/openstack/tests/functional/shared_file_system/base.py b/openstack/tests/functional/shared_file_system/base.py index 3714dcebe..864116ee1 100644 --- a/openstack/tests/functional/shared_file_system/base.py +++ b/openstack/tests/functional/shared_file_system/base.py @@ -21,3 +21,10 @@ def setUp(self): super(BaseSharedFileSystemTest, self).setUp() self.require_service('shared-file-system', min_microversion=self.min_microversion) + + def create_share(self, **kwargs): + share = self.conn.share.create_share(**kwargs) + self.addCleanup(self.conn.share.delete_share, share.id, + ignore_missing=True) + self.assertIsNotNone(share.id) + return share diff --git a/openstack/tests/functional/shared_file_system/test_share.py b/openstack/tests/functional/shared_file_system/test_share.py new file mode 100644 index 000000000..16bae406c --- /dev/null +++ b/openstack/tests/functional/shared_file_system/test_share.py @@ -0,0 +1,60 @@ +# 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.shared_file_system.v2 import share as _share +from openstack.tests.functional.shared_file_system import base + + +class ShareTest(base.BaseSharedFileSystemTest): + + def setUp(self): + super(ShareTest, self).setUp() + + self.SHARE_NAME = self.getUniqueString() + my_share = self.conn.shared_file_system.create_share( + name=self.SHARE_NAME, size=2, share_type="dhss_false", + share_protocol='NFS', description=None) + self.conn.shared_file_system.wait_for_status( + my_share, + status='available', + failures=['error'], + interval=5, + wait=self._wait_for_timeout) + self.assertIsNotNone(my_share) + self.assertIsNotNone(my_share.id) + self.SHARE_ID = my_share.id + + def tearDown(self): + sot = self.conn.shared_file_system.delete_share( + self.SHARE_ID, + ignore_missing=True) + self.assertIsNone(sot) + super(ShareTest, self).tearDown() + + def test_get(self): + sot = self.conn.shared_file_system.get_share(self.SHARE_ID) + assert isinstance(sot, _share.Share) + self.assertEqual(self.SHARE_ID, sot.id) + + def test_list_share(self): + shares = self.conn.shared_file_system.shares(details=False) + self.assertGreater(len(list(shares)), 0) + for share in shares: + for attribute in ('id', 'name', 'created_at', 'updated_at'): + self.assertTrue(hasattr(share, attribute)) + + def test_update(self): + updated_share = self.conn.shared_file_system.update_share( + self.SHARE_ID, display_description='updated share') + get_updated_share = self.conn.shared_file_system.get_share( + updated_share.id) + self.assertEqual('updated share', get_updated_share.description) diff --git a/openstack/tests/unit/shared_file_system/v2/test_proxy.py b/openstack/tests/unit/shared_file_system/v2/test_proxy.py new file mode 100644 index 000000000..1a1e8340a --- /dev/null +++ b/openstack/tests/unit/shared_file_system/v2/test_proxy.py @@ -0,0 +1,64 @@ +# 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 unittest import mock + +from openstack.shared_file_system.v2 import _proxy +from openstack.shared_file_system.v2 import share +from openstack.tests.unit import test_proxy_base + + +class TestSharedFileSystemProxy(test_proxy_base.TestProxyBase): + + def setUp(self): + super(TestSharedFileSystemProxy, self).setUp() + self.proxy = _proxy.Proxy(self.session) + + def test_shares(self): + self.verify_list(self.proxy.shares, share.Share) + + def test_shares_detailed(self): + self.verify_list(self.proxy.shares, share.Share, + method_kwargs={"details": True, "query": 1}, + expected_kwargs={"query": 1}) + + def test_shares_not_detailed(self): + self.verify_list(self.proxy.shares, share.Share, + method_kwargs={"details": False, "query": 1}, + expected_kwargs={"query": 1}) + + def test_share_get(self): + self.verify_get(self.proxy.get_share, share.Share) + + def test_share_delete(self): + self.verify_delete( + self.proxy.delete_share, share.Share, False) + + def test_share_delete_ignore(self): + self.verify_delete( + self.proxy.delete_share, share.Share, True) + + def test_share_create(self): + self.verify_create(self.proxy.create_share, share.Share) + + def test_share_update(self): + self.verify_update(self.proxy.update_share, share.Share) + + @mock.patch("openstack.resource.wait_for_status") + def test_wait_for(self, mock_wait): + mock_resource = mock.Mock() + mock_wait.return_value = mock_resource + + self.proxy.wait_for_status(mock_resource, 'ACTIVE') + + mock_wait.assert_called_once_with(self.proxy, mock_resource, + 'ACTIVE', [], 2, 120) diff --git a/openstack/tests/unit/shared_file_system/v2/test_share.py b/openstack/tests/unit/shared_file_system/v2/test_share.py new file mode 100644 index 000000000..44e8fff3c --- /dev/null +++ b/openstack/tests/unit/shared_file_system/v2/test_share.py @@ -0,0 +1,111 @@ +# 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.shared_file_system.v2 import share +from openstack.tests.unit import base + +IDENTIFIER = '08a87d37-5ca2-4308-86c5-cba06d8d796c' +EXAMPLE = { + "id": IDENTIFIER, + "size": 1, + "availability_zone": "manila-zone-1", + "created_at": "2021-02-11T17:38:00.000000", + "status": "available", + "name": None, + "description": None, + "project_id": "d19444eb73af4b37bc0794532ef6fc50", + "snapshot_id": None, + "share_network_id": None, + "share_protocol": "NFS", + "metadata": {}, + "share_type": "cbb18bb7-cc97-477a-b64b-ed7c7f2a1c67", + "volume_type": "default", + "is_public": False, + "is_snapshot_supported": True, + "task_state": None, + "share_type_name": "default", + "access_rules_status": "active", + "replication_type": None, + "is_replicated": False, + "user_id": "6c262cab98de42c2afc4cfccbefc50c7", + "is_creating_new_share_from_snapshot_supported": True, + "is_reverting_to_snapshot_supported": True, + "share_group_id": None, + "source_share_group_snapshot_member_id": None, + "is_mounting_snapshot_supported": True, + "progress": "100%", + "share_server_id": None, + "host": "new@denver#lvm-single-pool" +} + + +class TestShares(base.TestCase): + + def test_basic(self): + shares_resource = share.Share() + self.assertEqual('shares', shares_resource.resources_key) + self.assertEqual('/shares', shares_resource.base_path) + self.assertTrue(shares_resource.allow_list) + self.assertTrue(shares_resource.allow_create) + self.assertTrue(shares_resource.allow_fetch) + self.assertTrue(shares_resource.allow_commit) + self.assertTrue(shares_resource.allow_delete) + + def test_make_shares(self): + shares_resource = share.Share(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], shares_resource.id) + self.assertEqual(EXAMPLE['size'], shares_resource.size) + self.assertEqual(EXAMPLE['availability_zone'], + shares_resource.availability_zone) + self.assertEqual(EXAMPLE['created_at'], shares_resource.created_at) + self.assertEqual(EXAMPLE['status'], shares_resource.status) + self.assertEqual(EXAMPLE['name'], shares_resource.name) + self.assertEqual(EXAMPLE['description'], + shares_resource.description) + self.assertEqual(EXAMPLE['project_id'], shares_resource.project_id) + self.assertEqual(EXAMPLE['snapshot_id'], shares_resource.snapshot_id) + self.assertEqual(EXAMPLE['share_network_id'], + shares_resource.share_network_id) + self.assertEqual(EXAMPLE['share_protocol'], + shares_resource.share_protocol) + self.assertEqual(EXAMPLE['metadata'], shares_resource.metadata) + self.assertEqual(EXAMPLE['share_type'], shares_resource.share_type) + self.assertEqual(EXAMPLE['is_public'], shares_resource.is_public) + self.assertEqual(EXAMPLE['is_snapshot_supported'], + shares_resource.is_snapshot_supported) + self.assertEqual(EXAMPLE['task_state'], shares_resource.task_state) + self.assertEqual(EXAMPLE['share_type_name'], + shares_resource.share_type_name) + self.assertEqual(EXAMPLE['access_rules_status'], + shares_resource.access_rules_status) + self.assertEqual(EXAMPLE['replication_type'], + shares_resource.replication_type) + self.assertEqual(EXAMPLE['is_replicated'], + shares_resource.is_replicated) + self.assertEqual(EXAMPLE['user_id'], shares_resource.user_id) + self.assertEqual(EXAMPLE[ + 'is_creating_new_share_from_snapshot_supported'], + (shares_resource.is_creating_new_share_from_snapshot_supported)) + self.assertEqual(EXAMPLE['is_reverting_to_snapshot_supported'], + shares_resource.is_reverting_to_snapshot_supported) + self.assertEqual(EXAMPLE['share_group_id'], + shares_resource.share_group_id) + self.assertEqual(EXAMPLE[ + 'source_share_group_snapshot_member_id'], + shares_resource.source_share_group_snapshot_member_id) + self.assertEqual(EXAMPLE['is_mounting_snapshot_supported'], + shares_resource.is_mounting_snapshot_supported) + self.assertEqual(EXAMPLE['progress'], + shares_resource.progress) + self.assertEqual(EXAMPLE['share_server_id'], + shares_resource.share_server_id) + self.assertEqual(EXAMPLE['host'], shares_resource.host) diff --git a/releasenotes/notes/add-shared-file-system-shares-e9f356a318045607.yaml b/releasenotes/notes/add-shared-file-system-shares-e9f356a318045607.yaml new file mode 100644 index 000000000..e7a1bcb01 --- /dev/null +++ b/releasenotes/notes/add-shared-file-system-shares-e9f356a318045607.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added support to create, update, list, get, and delete shares + (from shared file system service). From 533ba93a3042a7d4b4c41b8742fdebf45929be81 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 11 May 2021 15:32:41 +0200 Subject: [PATCH 2876/3836] Switch identiy.endpoints in the cloud layer cloud layer for identity.endpoint is now going to proxy layer without reimplementing requests. Stip out further v2 parts from identity cloud layer. Change-Id: Ifa078a319b383855744b82678b508b85f46b3691 --- openstack/cloud/_identity.py | 137 +++++------------- .../tests/functional/cloud/test_endpoints.py | 28 ++-- openstack/tests/unit/base.py | 44 +----- openstack/tests/unit/cloud/test_endpoints.py | 27 ++-- 4 files changed, 74 insertions(+), 162 deletions(-) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index bf23babcf..e8a5fe996 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -469,10 +469,6 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, :param region: Endpoint region. :param enabled: Whether the endpoint is enabled - NOTE: Both v2 (public_url, internal_url, admin_url) and v3 - (url, interface) calling semantics are supported. But - you can only use one of them at a time. - :returns: a list of ``munch.Munch`` containing the endpoint description :raises: OpenStackCloudException if the service cannot be found or if @@ -493,91 +489,46 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, "service {service} not found".format( service=service_name_or_id)) - if self._is_client_version('identity', 2): - if url: - # v2.0 in use, v3-like arguments, one endpoint created - if interface != 'public': - raise exc.OpenStackCloudException( - "Error adding endpoint for service {service}." - " On a v2 cloud the url/interface API may only be" - " used for public url. Try using the public_url," - " internal_url, admin_url parameters instead of" - " url and interface".format( - service=service_name_or_id)) - endpoint_args = {'publicurl': url} - else: - # v2.0 in use, v2.0-like arguments, one endpoint created - endpoint_args = {} - if public_url: - endpoint_args.update({'publicurl': public_url}) - if internal_url: - endpoint_args.update({'internalurl': internal_url}) - if admin_url: - endpoint_args.update({'adminurl': admin_url}) - - # keystone v2.0 requires 'region' arg even if it is None - endpoint_args.update( - {'service_id': service['id'], 'region': region}) - - data = self._identity_client.post( - '/endpoints', json={'endpoint': endpoint_args}, - endpoint_filter={'interface': 'admin'}, - error_message=("Failed to create endpoint for service" - " {service}".format(service=service['name']))) - return [self._get_and_munchify('endpoint', data)] + endpoints_args = [] + if url: + # v3 in use, v3-like arguments, one endpoint created + endpoints_args.append( + {'url': url, 'interface': interface, + 'service_id': service['id'], 'enabled': enabled, + 'region_id': region}) else: - endpoints_args = [] - if url: - # v3 in use, v3-like arguments, one endpoint created - endpoints_args.append( - {'url': url, 'interface': interface, - 'service_id': service['id'], 'enabled': enabled, - 'region': region}) - else: - # v3 in use, v2.0-like arguments, one endpoint created for each - # interface url provided - endpoint_args = {'region': region, 'enabled': enabled, - 'service_id': service['id']} - if public_url: - endpoint_args.update({'url': public_url, - 'interface': 'public'}) - endpoints_args.append(endpoint_args.copy()) - if internal_url: - endpoint_args.update({'url': internal_url, - 'interface': 'internal'}) - endpoints_args.append(endpoint_args.copy()) - if admin_url: - endpoint_args.update({'url': admin_url, - 'interface': 'admin'}) - endpoints_args.append(endpoint_args.copy()) - - endpoints = [] - error_msg = ("Failed to create endpoint for service" - " {service}".format(service=service['name'])) - for args in endpoints_args: - data = self._identity_client.post( - '/endpoints', json={'endpoint': args}, - error_message=error_msg) - endpoints.append(self._get_and_munchify('endpoint', data)) - return endpoints + # v3 in use, v2.0-like arguments, one endpoint created for each + # interface url provided + endpoint_args = {'region_id': region, 'enabled': enabled, + 'service_id': service['id']} + if public_url: + endpoint_args.update({'url': public_url, + 'interface': 'public'}) + endpoints_args.append(endpoint_args.copy()) + if internal_url: + endpoint_args.update({'url': internal_url, + 'interface': 'internal'}) + endpoints_args.append(endpoint_args.copy()) + if admin_url: + endpoint_args.update({'url': admin_url, + 'interface': 'admin'}) + endpoints_args.append(endpoint_args.copy()) + + endpoints = [] + for args in endpoints_args: + endpoints.append(self.identity.create_endpoint(**args)) + return endpoints @_utils.valid_kwargs('enabled', 'service_name_or_id', 'url', 'interface', 'region') def update_endpoint(self, endpoint_id, **kwargs): - # NOTE(SamYaple): Endpoint updates are only available on v3 api - if self._is_client_version('identity', 2): - raise exc.OpenStackCloudUnavailableFeature( - 'Unavailable Feature: Endpoint update' - ) - service_name_or_id = kwargs.pop('service_name_or_id', None) if service_name_or_id is not None: kwargs['service_id'] = service_name_or_id + if 'region' in kwargs: + kwargs['region_id'] = kwargs.pop('region') - data = self._identity_client.patch( - '/endpoints/{}'.format(endpoint_id), json={'endpoint': kwargs}, - error_message="Failed to update endpoint {}".format(endpoint_id)) - return self._get_and_munchify('endpoint', data) + return self.identity.update_endpoint(endpoint_id, **kwargs) def list_endpoints(self): """List Keystone endpoints. @@ -587,15 +538,7 @@ def list_endpoints(self): :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ - # Force admin interface if v2.0 is in use - v2 = self._is_client_version('identity', 2) - kwargs = {'endpoint_filter': {'interface': 'admin'}} if v2 else {} - - data = self._identity_client.get( - '/endpoints', error_message="Failed to list endpoints", **kwargs) - endpoints = self._get_and_munchify('endpoints', data) - - return endpoints + return list(self.identity.endpoints()) def search_endpoints(self, id=None, filters=None): """List Keystone endpoints. @@ -654,15 +597,13 @@ def delete_endpoint(self, id): self.log.debug("Endpoint %s not found for deleting", id) return False - # Force admin interface if v2.0 is in use - v2 = self._is_client_version('identity', 2) - kwargs = {'endpoint_filter': {'interface': 'admin'}} if v2 else {} - - error_msg = "Failed to delete endpoint {id}".format(id=id) - self._identity_client.delete('/endpoints/{id}'.format(id=id), - error_message=error_msg, **kwargs) - - return True + try: + self.identity.delete_endpoint(id) + return True + except exceptions.SDKException: + self.log.exception( + "Failed to delete endpoint {id}".format(id=id)) + return False def create_domain(self, name, description=None, enabled=True): """Create a domain. diff --git a/openstack/tests/functional/cloud/test_endpoints.py b/openstack/tests/functional/cloud/test_endpoints.py index b9cd6de21..f9d432a9d 100644 --- a/openstack/tests/functional/cloud/test_endpoints.py +++ b/openstack/tests/functional/cloud/test_endpoints.py @@ -77,6 +77,8 @@ def _cleanup_services(self): def test_create_endpoint(self): service_name = self.new_item_name + '_create' + region = list(self.operator_cloud.identity.regions())[0].id + service = self.operator_cloud.create_service( name=service_name, type='test_type', description='this is a test description') @@ -86,7 +88,7 @@ def test_create_endpoint(self): public_url='http://public.test/', internal_url='http://internal.test/', admin_url='http://admin.url/', - region=service_name) + region=region) self.assertNotEqual([], endpoints) self.assertIsNotNone(endpoints[0].get('id')) @@ -95,7 +97,7 @@ def test_create_endpoint(self): endpoints = self.operator_cloud.create_endpoint( service_name_or_id=service['id'], public_url='http://public.test/', - region=service_name) + region=region) self.assertNotEqual([], endpoints) self.assertIsNotNone(endpoints[0].get('id')) @@ -108,13 +110,17 @@ def test_update_endpoint(self): self.operator_cloud.update_endpoint, 'endpoint_id1') else: + # service operations require existing region. Do not test updating + # region for now + region = list(self.operator_cloud.identity.regions())[0].id + service = self.operator_cloud.create_service( name='service1', type='test_type') endpoint = self.operator_cloud.create_endpoint( service_name_or_id=service['id'], url='http://admin.url/', interface='admin', - region='orig_region', + region=region, enabled=False)[0] new_service = self.operator_cloud.create_service( @@ -124,18 +130,20 @@ def test_update_endpoint(self): service_name_or_id=new_service.id, url='http://public.url/', interface='public', - region='update_region', + region=region, enabled=True) self.assertEqual(new_endpoint.url, 'http://public.url/') self.assertEqual(new_endpoint.interface, 'public') - self.assertEqual(new_endpoint.region, 'update_region') + self.assertEqual(new_endpoint.region_id, region) self.assertEqual(new_endpoint.service_id, new_service.id) - self.assertTrue(new_endpoint.enabled) + self.assertTrue(new_endpoint.is_enabled) def test_list_endpoints(self): service_name = self.new_item_name + '_list' + region = list(self.operator_cloud.identity.regions())[0].id + service = self.operator_cloud.create_service( name=service_name, type='test_type', description='this is a test description') @@ -144,7 +152,7 @@ def test_list_endpoints(self): service_name_or_id=service['id'], public_url='http://public.test/', internal_url='http://internal.test/', - region=service_name) + region=region) observed_endpoints = self.operator_cloud.list_endpoints() found = False @@ -164,13 +172,15 @@ def test_list_endpoints(self): e['publicurl']) self.assertEqual('http://internal.test/', e['internalurl']) - self.assertEqual(service_name, e['region']) + self.assertEqual(region, e['region_id']) self.assertTrue(found, msg='new endpoint not found in endpoints list!') def test_delete_endpoint(self): service_name = self.new_item_name + '_delete' + region = list(self.operator_cloud.identity.regions())[0].id + service = self.operator_cloud.create_service( name=service_name, type='test_type', description='this is a test description') @@ -179,7 +189,7 @@ def test_delete_endpoint(self): service_name_or_id=service['id'], public_url='http://public.test/', internal_url='http://internal.test/', - region=service_name) + region=region) self.assertNotEqual([], endpoints) for endpoint in endpoints: diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 2980d085a..804f2ff9c 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -66,17 +66,10 @@ _EndpointDataV3 = collections.namedtuple( 'EndpointData', - 'endpoint_id, service_id, interface, region, url, enabled, ' + 'endpoint_id, service_id, interface, region_id, url, enabled, ' 'json_response, json_request') -_EndpointDataV2 = collections.namedtuple( - 'EndpointData', - 'endpoint_id, service_id, region, public_url, internal_url, ' - 'admin_url, v3_endpoint_list, json_response, ' - 'json_request') - - # NOTE(notmorgan): Shade does not support domain-specific roles # This should eventually be fixed if it becomes a main-stream feature. _RoleData = collections.namedtuple( @@ -361,47 +354,14 @@ def _get_endpoint_v3_data(self, service_id=None, region=None, interface = interface or uuid.uuid4().hex response = {'id': endpoint_id, 'service_id': service_id, - 'region': region, 'interface': interface, + 'region_id': region, 'interface': interface, 'url': url, 'enabled': enabled} request = response.copy() request.pop('id') - response['region_id'] = response['region'] return _EndpointDataV3(endpoint_id, service_id, interface, region, url, enabled, {'endpoint': response}, {'endpoint': request}) - def _get_endpoint_v2_data(self, service_id=None, region=None, - public_url=None, admin_url=None, - internal_url=None): - endpoint_id = uuid.uuid4().hex - service_id = service_id or uuid.uuid4().hex - region = region or uuid.uuid4().hex - response = {'id': endpoint_id, 'service_id': service_id, - 'region': region} - v3_endpoints = {} - request = response.copy() - request.pop('id') - if admin_url: - response['adminURL'] = admin_url - v3_endpoints['admin'] = self._get_endpoint_v3_data( - service_id, region, public_url, interface='admin') - if internal_url: - response['internalURL'] = internal_url - v3_endpoints['internal'] = self._get_endpoint_v3_data( - service_id, region, internal_url, interface='internal') - if public_url: - response['publicURL'] = public_url - v3_endpoints['public'] = self._get_endpoint_v3_data( - service_id, region, public_url, interface='public') - request = response.copy() - request.pop('id') - for u in ('publicURL', 'internalURL', 'adminURL'): - if request.get(u): - request[u.lower()] = request.pop(u) - return _EndpointDataV2(endpoint_id, service_id, region, public_url, - internal_url, admin_url, v3_endpoints, - {'endpoint': response}, {'endpoint': request}) - def _get_role_data(self, role_name=None): role_id = uuid.uuid4().hex role_name = role_name or uuid.uuid4().hex diff --git a/openstack/tests/unit/cloud/test_endpoints.py b/openstack/tests/unit/cloud/test_endpoints.py index bddcb953d..b3ebe9eef 100644 --- a/openstack/tests/unit/cloud/test_endpoints.py +++ b/openstack/tests/unit/cloud/test_endpoints.py @@ -45,10 +45,10 @@ def test_create_endpoint_v3(self): url=self._dummy_url(), enabled=False) admin_endpoint_data = self._get_endpoint_v3_data( service_id=service_data.service_id, interface='admin', - url=self._dummy_url(), region=public_endpoint_data.region) + url=self._dummy_url(), region=public_endpoint_data.region_id) internal_endpoint_data = self._get_endpoint_v3_data( service_id=service_data.service_id, interface='internal', - url=self._dummy_url(), region=public_endpoint_data.region) + url=self._dummy_url(), region=public_endpoint_data.region_id) self.register_uris([ dict(method='GET', @@ -86,7 +86,7 @@ def test_create_endpoint_v3(self): endpoints = self.cloud.create_endpoint( service_name_or_id=service_data.service_id, - region=public_endpoint_data_disabled.region, + region=public_endpoint_data_disabled.region_id, url=public_endpoint_data_disabled.url, interface=public_endpoint_data_disabled.interface, enabled=False) @@ -101,17 +101,17 @@ def test_create_endpoint_v3(self): endpoints[0].interface, matchers.Equals(public_endpoint_data_disabled.interface)) self.assertThat( - endpoints[0].region, - matchers.Equals(public_endpoint_data_disabled.region)) + endpoints[0].region_id, + matchers.Equals(public_endpoint_data_disabled.region_id)) self.assertThat( endpoints[0].region_id, - matchers.Equals(public_endpoint_data_disabled.region)) - self.assertThat(endpoints[0].enabled, + matchers.Equals(public_endpoint_data_disabled.region_id)) + self.assertThat(endpoints[0].is_enabled, matchers.Equals(public_endpoint_data_disabled.enabled)) endpoints_2on3 = self.cloud.create_endpoint( service_name_or_id=service_data.service_id, - region=public_endpoint_data.region, + region=public_endpoint_data.region_id, public_url=public_endpoint_data.url, internal_url=internal_endpoint_data.url, admin_url=admin_endpoint_data.url) @@ -129,9 +129,10 @@ def test_create_endpoint_v3(self): self.assertThat(result.url, matchers.Equals(reference.url)) self.assertThat(result.interface, matchers.Equals(reference.interface)) - self.assertThat(result.region, - matchers.Equals(reference.region)) - self.assertThat(result.enabled, matchers.Equals(reference.enabled)) + self.assertThat(result.region_id, + matchers.Equals(reference.region_id)) + self.assertThat(result.is_enabled, + matchers.Equals(reference.enabled)) self.assert_calls() def test_update_endpoint_v3(self): @@ -152,7 +153,7 @@ def test_update_endpoint_v3(self): endpoint = self.cloud.update_endpoint( endpoint_data.endpoint_id, service_name_or_id=service_data.service_id, - region=endpoint_data.region, + region=endpoint_data.region_id, url=dummy_url, interface=endpoint_data.interface, enabled=False @@ -251,7 +252,7 @@ def test_search_endpoints(self): # test we are getting the correct response for region/region_id compat endpoints = self.cloud.search_endpoints( - filters={'region': 'region1'}) + filters={'region_id': 'region1'}) # # test we are getting exactly 2 elements, this is v3 self.assertEqual(2, len(endpoints)) From 95cec28723a7b1a66b2b6a34aefcb2660140d81c Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 12 May 2021 12:40:49 +0200 Subject: [PATCH 2877/3836] Fix identity role management Domain role management was not exposed in the proxy. There were also no unit tests. - expose domain role mgmt in proxy - add unit tests in proxy for domain and project role mgmt - add unit tests in domain and proxy for role mgmt functions - fix return code for role mgmt to be 204, what is the proper one (according to ref specs, tests of the cloud layer, etc) Change-Id: If5f6ea5ee043601e46cf30d7c126cefaa2ba7d06 --- doc/source/user/proxies/identity_v3.rst | 7 +- openstack/identity/v3/_proxy.py | 100 +++++++++- openstack/identity/v3/domain.py | 4 +- .../tests/unit/identity/v3/test_domain.py | 172 ++++++++++++++++- .../tests/unit/identity/v3/test_project.py | 173 +++++++++++++++++- .../tests/unit/identity/v3/test_proxy.py | 166 ++++++++++++++++- 6 files changed, 612 insertions(+), 10 deletions(-) diff --git a/doc/source/user/proxies/identity_v3.rst b/doc/source/user/proxies/identity_v3.rst index e8f21f22a..10892d29b 100644 --- a/doc/source/user/proxies/identity_v3.rst +++ b/doc/source/user/proxies/identity_v3.rst @@ -82,8 +82,11 @@ Role Assignment Operations :noindex: :members: role_assignments, role_assignments_filter, assign_project_role_to_user, unassign_project_role_from_user, - validate_user_has_role, assign_project_role_to_group, - unassign_project_role_from_group, validate_group_has_role + validate_user_has_project_role, assign_project_role_to_group, + unassign_project_role_from_group, validate_group_has_project_role, + assign_domain_role_to_user, unassign_domain_role_from_user, + validate_user_has_domain_role, assign_domain_role_to_group, + unassign_domain_role_from_group, validate_group_has_domain_role Service Operations ^^^^^^^^^^^^^^^^^^ diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 3e23ffe3c..8bc2b7b1d 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -1116,6 +1116,102 @@ def delete_limit(self, limit, ignore_missing=True): self._delete(limit.Limit, limit, ignore_missing=ignore_missing) + def assign_domain_role_to_user(self, domain, user, role): + """Assign role to user on a domain + + :param domain: Either the ID of a domain or a + :class:`~openstack.identity.v3.domain.Domain` instance. + :param user: Either the ID of a user or a + :class:`~openstack.identity.v3.user.User` instance. + :param role: Either the ID of a role or a + :class:`~openstack.identity.v3.role.Role` instance. + :return: ``None`` + """ + domain = self._get_resource(_domain.Domain, domain) + user = self._get_resource(_user.User, user) + role = self._get_resource(_role.Role, role) + domain.assign_role_to_user(self, user, role) + + def unassign_domain_role_from_user(self, domain, user, role): + """Unassign role from user on a domain + + :param domain: Either the ID of a domain or a + :class:`~openstack.identity.v3.domain.Domain` instance. + :param user: Either the ID of a user or a + :class:`~openstack.identity.v3.user.User` instance. + :param role: Either the ID of a role or a + :class:`~openstack.identity.v3.role.Role` instance. + :return: ``None`` + """ + domain = self._get_resource(_domain.Domain, domain) + user = self._get_resource(_user.User, user) + role = self._get_resource(_role.Role, role) + domain.unassign_role_from_user(self, user, role) + + def validate_user_has_domain_role(self, domain, user, role): + """Validates that a user has a role on a domain + + :param domain: Either the ID of a domain or a + :class:`~openstack.identity.v3.domain.Domain` instance. + :param user: Either the ID of a user or a + :class:`~openstack.identity.v3.user.User` instance. + :param role: Either the ID of a role or a + :class:`~openstack.identity.v3.role.Role` instance. + :returns: True if user has role in domain + """ + domain = self._get_resource(_domain.Domain, domain) + user = self._get_resource(_user.User, user) + role = self._get_resource(_role.Role, role) + return domain.validate_user_has_role(self, user, role) + + def assign_domain_role_to_group(self, domain, group, role): + """Assign role to group on a domain + + :param domain: Either the ID of a domain or a + :class:`~openstack.identity.v3.domain.Domain` instance. + :param group: Either the ID of a group or a + :class:`~openstack.identity.v3.group.Group` instance. + :param role: Either the ID of a role or a + :class:`~openstack.identity.v3.role.Role` instance. + :return: ``None`` + """ + domain = self._get_resource(_domain.Domain, domain) + group = self._get_resource(_group.Group, group) + role = self._get_resource(_role.Role, role) + domain.assign_role_to_group(self, group, role) + + def unassign_domain_role_from_group(self, domain, group, role): + """Unassign role from group on a domain + + :param domain: Either the ID of a domain or a + :class:`~openstack.identity.v3.domain.Domain` instance. + :param group: Either the ID of a group or a + :class:`~openstack.identity.v3.group.Group` instance. + :param role: Either the ID of a role or a + :class:`~openstack.identity.v3.role.Role` instance. + :return: ``None`` + """ + domain = self._get_resource(_domain.Domain, domain) + group = self._get_resource(_group.Group, group) + role = self._get_resource(_role.Role, role) + domain.unassign_role_from_group(self, group, role) + + def validate_group_has_domain_role(self, domain, group, role): + """Validates that a group has a role on a domain + + :param domain: Either the ID of a domain or a + :class:`~openstack.identity.v3.domain.Domain` instance. + :param group: Either the ID of a group or a + :class:`~openstack.identity.v3.group.Group` instance. + :param role: Either the ID of a role or a + :class:`~openstack.identity.v3.role.Role` instance. + :returns: True if group has role on domain + """ + domain = self._get_resource(_domain.Domain, domain) + group = self._get_resource(_group.Group, group) + role = self._get_resource(_role.Role, role) + return domain.validate_group_has_role(self, group, role) + def assign_project_role_to_user(self, project, user, role): """Assign role to user on a project @@ -1150,7 +1246,7 @@ def unassign_project_role_from_user(self, project, user, role): role = self._get_resource(_role.Role, role) project.unassign_role_from_user(self, user, role) - def validate_user_has_role(self, project, user, role): + def validate_user_has_project_role(self, project, user, role): """Validates that a user has a role on a project :param project: Either the ID of a project or a @@ -1201,7 +1297,7 @@ def unassign_project_role_from_group(self, project, group, role): role = self._get_resource(_role.Role, role) project.unassign_role_from_group(self, group, role) - def validate_group_has_role(self, project, group, role): + def validate_group_has_project_role(self, project, group, role): """Validates that a group has a role on a project :param project: Either the ID of a project or a diff --git a/openstack/identity/v3/domain.py b/openstack/identity/v3/domain.py index 8a39d0a8b..bf842a176 100644 --- a/openstack/identity/v3/domain.py +++ b/openstack/identity/v3/domain.py @@ -62,7 +62,7 @@ def validate_user_has_role(self, session, user, role): url = utils.urljoin(self.base_path, self.id, 'users', user.id, 'roles', role.id) resp = session.head(url,) - if resp.status_code == 201: + if resp.status_code == 204: return True return False @@ -89,7 +89,7 @@ def validate_group_has_role(self, session, group, role): url = utils.urljoin(self.base_path, self.id, 'groups', group.id, 'roles', role.id) resp = session.head(url,) - if resp.status_code == 201: + if resp.status_code == 204: return True return False diff --git a/openstack/tests/unit/identity/v3/test_domain.py b/openstack/tests/unit/identity/v3/test_domain.py index 782e3f277..c7c33b387 100644 --- a/openstack/tests/unit/identity/v3/test_domain.py +++ b/openstack/tests/unit/identity/v3/test_domain.py @@ -9,10 +9,15 @@ # 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 unittest import mock -from openstack.tests.unit import base +from keystoneauth1 import adapter from openstack.identity.v3 import domain +from openstack.identity.v3 import group +from openstack.identity.v3 import role +from openstack.identity.v3 import user +from openstack.tests.unit import base IDENTIFIER = 'IDENTIFIER' EXAMPLE = { @@ -26,6 +31,21 @@ class TestDomain(base.TestCase): + def setUp(self): + super(TestDomain, self).setUp() + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.default_microversion = 1 + self.sess._get_connection = mock.Mock(return_value=self.cloud) + self.good_resp = mock.Mock() + self.good_resp.body = None + self.good_resp.json = mock.Mock(return_value=self.good_resp.body) + self.good_resp.status_code = 204 + + self.bad_resp = mock.Mock() + self.bad_resp.body = None + self.bad_resp.json = mock.Mock(return_value=self.bad_resp.body) + self.bad_resp.status_code = 401 + def test_basic(self): sot = domain.Domain() self.assertEqual('domain', sot.resource_key) @@ -54,3 +74,153 @@ def test_make_it(self): self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['links'], sot.links) self.assertEqual(EXAMPLE['name'], sot.name) + + def test_assign_role_to_user_good(self): + sot = domain.Domain(**EXAMPLE) + resp = self.good_resp + self.sess.put = mock.Mock(return_value=resp) + + self.assertTrue( + sot.assign_role_to_user( + self.sess, + user.User(id='1'), + role.Role(id='2'))) + + self.sess.put.assert_called_with( + 'domains/IDENTIFIER/users/1/roles/2') + + def test_assign_role_to_user_bad(self): + sot = domain.Domain(**EXAMPLE) + resp = self.bad_resp + self.sess.put = mock.Mock(return_value=resp) + + self.assertFalse( + sot.assign_role_to_user( + self.sess, + user.User(id='1'), + role.Role(id='2'))) + + def test_validate_user_has_role_good(self): + sot = domain.Domain(**EXAMPLE) + resp = self.good_resp + self.sess.head = mock.Mock(return_value=resp) + + self.assertTrue( + sot.validate_user_has_role( + self.sess, + user.User(id='1'), + role.Role(id='2'))) + + self.sess.head.assert_called_with( + 'domains/IDENTIFIER/users/1/roles/2') + + def test_validate_user_has_role_bad(self): + sot = domain.Domain(**EXAMPLE) + resp = self.bad_resp + self.sess.head = mock.Mock(return_value=resp) + + self.assertFalse( + sot.validate_user_has_role( + self.sess, + user.User(id='1'), + role.Role(id='2'))) + + def test_unassign_role_from_user_good(self): + sot = domain.Domain(**EXAMPLE) + resp = self.good_resp + self.sess.delete = mock.Mock(return_value=resp) + + self.assertTrue( + sot.unassign_role_from_user( + self.sess, + user.User(id='1'), + role.Role(id='2'))) + + self.sess.delete.assert_called_with( + 'domains/IDENTIFIER/users/1/roles/2') + + def test_unassign_role_from_user_bad(self): + sot = domain.Domain(**EXAMPLE) + resp = self.bad_resp + self.sess.delete = mock.Mock(return_value=resp) + + self.assertFalse( + sot.unassign_role_from_user( + self.sess, + user.User(id='1'), + role.Role(id='2'))) + + def test_assign_role_to_group_good(self): + sot = domain.Domain(**EXAMPLE) + resp = self.good_resp + self.sess.put = mock.Mock(return_value=resp) + + self.assertTrue( + sot.assign_role_to_group( + self.sess, + group.Group(id='1'), + role.Role(id='2'))) + + self.sess.put.assert_called_with( + 'domains/IDENTIFIER/groups/1/roles/2') + + def test_assign_role_to_group_bad(self): + sot = domain.Domain(**EXAMPLE) + resp = self.bad_resp + self.sess.put = mock.Mock(return_value=resp) + + self.assertFalse( + sot.assign_role_to_group( + self.sess, + group.Group(id='1'), + role.Role(id='2'))) + + def test_validate_group_has_role_good(self): + sot = domain.Domain(**EXAMPLE) + resp = self.good_resp + self.sess.head = mock.Mock(return_value=resp) + + self.assertTrue( + sot.validate_group_has_role( + self.sess, + group.Group(id='1'), + role.Role(id='2'))) + + self.sess.head.assert_called_with( + 'domains/IDENTIFIER/groups/1/roles/2') + + def test_validate_group_has_role_bad(self): + sot = domain.Domain(**EXAMPLE) + resp = self.bad_resp + self.sess.head = mock.Mock(return_value=resp) + + self.assertFalse( + sot.validate_group_has_role( + self.sess, + group.Group(id='1'), + role.Role(id='2'))) + + def test_unassign_role_from_group_good(self): + sot = domain.Domain(**EXAMPLE) + resp = self.good_resp + self.sess.delete = mock.Mock(return_value=resp) + + self.assertTrue( + sot.unassign_role_from_group( + self.sess, + group.Group(id='1'), + role.Role(id='2'))) + + self.sess.delete.assert_called_with( + 'domains/IDENTIFIER/groups/1/roles/2') + + def test_unassign_role_from_group_bad(self): + sot = domain.Domain(**EXAMPLE) + resp = self.bad_resp + self.sess.delete = mock.Mock(return_value=resp) + + self.assertFalse( + sot.unassign_role_from_group( + self.sess, + group.Group(id='1'), + role.Role(id='2'))) diff --git a/openstack/tests/unit/identity/v3/test_project.py b/openstack/tests/unit/identity/v3/test_project.py index 9406d5abe..e6c45d164 100644 --- a/openstack/tests/unit/identity/v3/test_project.py +++ b/openstack/tests/unit/identity/v3/test_project.py @@ -9,10 +9,16 @@ # 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 unittest import mock -from openstack.tests.unit import base +from keystoneauth1 import adapter +from openstack.identity.v3 import group from openstack.identity.v3 import project +from openstack.identity.v3 import role +from openstack.identity.v3 import user +from openstack.tests.unit import base + IDENTIFIER = 'IDENTIFIER' EXAMPLE = { @@ -31,6 +37,21 @@ class TestProject(base.TestCase): + def setUp(self): + super(TestProject, self).setUp() + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.default_microversion = 1 + self.sess._get_connection = mock.Mock(return_value=self.cloud) + self.good_resp = mock.Mock() + self.good_resp.body = None + self.good_resp.json = mock.Mock(return_value=self.good_resp.body) + self.good_resp.status_code = 204 + + self.bad_resp = mock.Mock() + self.bad_resp.body = None + self.bad_resp.json = mock.Mock(return_value=self.bad_resp.body) + self.bad_resp.status_code = 401 + def test_basic(self): sot = project.Project() self.assertEqual('project', sot.resource_key) @@ -70,6 +91,156 @@ def test_make_it(self): self.assertEqual(EXAMPLE['parent_id'], sot.parent_id) self.assertDictEqual(EXAMPLE['options'], sot.options) + def test_assign_role_to_user_good(self): + sot = project.Project(**EXAMPLE) + resp = self.good_resp + self.sess.put = mock.Mock(return_value=resp) + + self.assertTrue( + sot.assign_role_to_user( + self.sess, + user.User(id='1'), + role.Role(id='2'))) + + self.sess.put.assert_called_with( + 'projects/IDENTIFIER/users/1/roles/2') + + def test_assign_role_to_user_bad(self): + sot = project.Project(**EXAMPLE) + resp = self.bad_resp + self.sess.put = mock.Mock(return_value=resp) + + self.assertFalse( + sot.assign_role_to_user( + self.sess, + user.User(id='1'), + role.Role(id='2'))) + + def test_validate_user_has_role_good(self): + sot = project.Project(**EXAMPLE) + resp = self.good_resp + self.sess.head = mock.Mock(return_value=resp) + + self.assertTrue( + sot.validate_user_has_role( + self.sess, + user.User(id='1'), + role.Role(id='2'))) + + self.sess.head.assert_called_with( + 'projects/IDENTIFIER/users/1/roles/2') + + def test_validate_user_has_role_bad(self): + sot = project.Project(**EXAMPLE) + resp = self.bad_resp + self.sess.head = mock.Mock(return_value=resp) + + self.assertFalse( + sot.validate_user_has_role( + self.sess, + user.User(id='1'), + role.Role(id='2'))) + + def test_unassign_role_from_user_good(self): + sot = project.Project(**EXAMPLE) + resp = self.good_resp + self.sess.delete = mock.Mock(return_value=resp) + + self.assertTrue( + sot.unassign_role_from_user( + self.sess, + user.User(id='1'), + role.Role(id='2'))) + + self.sess.delete.assert_called_with( + 'projects/IDENTIFIER/users/1/roles/2') + + def test_unassign_role_from_user_bad(self): + sot = project.Project(**EXAMPLE) + resp = self.bad_resp + self.sess.delete = mock.Mock(return_value=resp) + + self.assertFalse( + sot.unassign_role_from_user( + self.sess, + user.User(id='1'), + role.Role(id='2'))) + + def test_assign_role_to_group_good(self): + sot = project.Project(**EXAMPLE) + resp = self.good_resp + self.sess.put = mock.Mock(return_value=resp) + + self.assertTrue( + sot.assign_role_to_group( + self.sess, + group.Group(id='1'), + role.Role(id='2'))) + + self.sess.put.assert_called_with( + 'projects/IDENTIFIER/groups/1/roles/2') + + def test_assign_role_to_group_bad(self): + sot = project.Project(**EXAMPLE) + resp = self.bad_resp + self.sess.put = mock.Mock(return_value=resp) + + self.assertFalse( + sot.assign_role_to_group( + self.sess, + group.Group(id='1'), + role.Role(id='2'))) + + def test_validate_group_has_role_good(self): + sot = project.Project(**EXAMPLE) + resp = self.good_resp + self.sess.head = mock.Mock(return_value=resp) + + self.assertTrue( + sot.validate_group_has_role( + self.sess, + group.Group(id='1'), + role.Role(id='2'))) + + self.sess.head.assert_called_with( + 'projects/IDENTIFIER/groups/1/roles/2') + + def test_validate_group_has_role_bad(self): + sot = project.Project(**EXAMPLE) + resp = self.bad_resp + self.sess.head = mock.Mock(return_value=resp) + + self.assertFalse( + sot.validate_group_has_role( + self.sess, + group.Group(id='1'), + role.Role(id='2'))) + + def test_unassign_role_from_group_good(self): + sot = project.Project(**EXAMPLE) + resp = self.good_resp + self.sess.delete = mock.Mock(return_value=resp) + + self.assertTrue( + sot.unassign_role_from_group( + self.sess, + group.Group(id='1'), + role.Role(id='2'))) + + self.sess.delete.assert_called_with( + 'projects/IDENTIFIER/groups/1/roles/2') + + def test_unassign_role_from_group_bad(self): + sot = project.Project(**EXAMPLE) + resp = self.bad_resp + self.sess.delete = mock.Mock(return_value=resp) + + self.assertFalse( + sot.unassign_role_from_group( + self.sess, + group.Group(id='1'), + role.Role(id='2'))) + class TestUserProject(base.TestCase): diff --git a/openstack/tests/unit/identity/v3/test_proxy.py b/openstack/tests/unit/identity/v3/test_proxy.py index 57a49a4aa..3608cc933 100644 --- a/openstack/tests/unit/identity/v3/test_proxy.py +++ b/openstack/tests/unit/identity/v3/test_proxy.py @@ -29,11 +29,14 @@ USER_ID = 'user-id-' + uuid.uuid4().hex -class TestIdentityProxy(test_proxy_base.TestProxyBase): +class TestIdentityProxyBase(test_proxy_base.TestProxyBase): def setUp(self): - super(TestIdentityProxy, self).setUp() + super(TestIdentityProxyBase, self).setUp() self.proxy = _proxy.Proxy(self.session) + +class TestIdentityProxy(TestIdentityProxyBase): + def test_credential_create_attrs(self): self.verify_create(self.proxy.create_credential, credential.Credential) @@ -274,3 +277,162 @@ def test_roles(self): def test_role_update(self): self.verify_update(self.proxy.update_role, role.Role) + + +class TestIdentityProxyRoleAssignments(TestIdentityProxyBase): + + def test_assign_domain_role_to_user(self): + self._verify( + "openstack.identity.v3.domain.Domain.assign_role_to_user", + self.proxy.assign_domain_role_to_user, + method_args=['dom_id'], + method_kwargs={'user': 'uid', 'role': 'rid'}, + expected_args=[ + self.proxy, + self.proxy._get_resource(user.User, 'uid'), + self.proxy._get_resource(role.Role, 'rid') + ] + ) + + def test_unassign_domain_role_from_user(self): + self._verify( + "openstack.identity.v3.domain.Domain.unassign_role_from_user", + self.proxy.unassign_domain_role_from_user, + method_args=['dom_id'], + method_kwargs={'user': 'uid', 'role': 'rid'}, + expected_args=[ + self.proxy, + self.proxy._get_resource(user.User, 'uid'), + self.proxy._get_resource(role.Role, 'rid') + ] + ) + + def test_validate_user_has_domain_role(self): + self._verify( + "openstack.identity.v3.domain.Domain.validate_user_has_role", + self.proxy.validate_user_has_domain_role, + method_args=['dom_id'], + method_kwargs={'user': 'uid', 'role': 'rid'}, + expected_args=[ + self.proxy, + self.proxy._get_resource(user.User, 'uid'), + self.proxy._get_resource(role.Role, 'rid') + ] + ) + + def test_assign_domain_role_to_group(self): + self._verify( + "openstack.identity.v3.domain.Domain.assign_role_to_group", + self.proxy.assign_domain_role_to_group, + method_args=['dom_id'], + method_kwargs={'group': 'uid', 'role': 'rid'}, + expected_args=[ + self.proxy, + self.proxy._get_resource(group.Group, 'uid'), + self.proxy._get_resource(role.Role, 'rid') + ] + ) + + def test_unassign_domain_role_from_group(self): + self._verify( + "openstack.identity.v3.domain.Domain.unassign_role_from_group", + self.proxy.unassign_domain_role_from_group, + method_args=['dom_id'], + method_kwargs={'group': 'uid', 'role': 'rid'}, + expected_args=[ + self.proxy, + self.proxy._get_resource(group.Group, 'uid'), + self.proxy._get_resource(role.Role, 'rid') + ] + ) + + def test_validate_group_has_domain_role(self): + self._verify( + "openstack.identity.v3.domain.Domain.validate_group_has_role", + self.proxy.validate_group_has_domain_role, + method_args=['dom_id'], + method_kwargs={'group': 'uid', 'role': 'rid'}, + expected_args=[ + self.proxy, + self.proxy._get_resource(group.Group, 'uid'), + self.proxy._get_resource(role.Role, 'rid') + ] + ) + + def test_assign_project_role_to_user(self): + self._verify( + "openstack.identity.v3.project.Project.assign_role_to_user", + self.proxy.assign_project_role_to_user, + method_args=['dom_id'], + method_kwargs={'user': 'uid', 'role': 'rid'}, + expected_args=[ + self.proxy, + self.proxy._get_resource(user.User, 'uid'), + self.proxy._get_resource(role.Role, 'rid') + ] + ) + + def test_unassign_project_role_from_user(self): + self._verify( + "openstack.identity.v3.project.Project.unassign_role_from_user", + self.proxy.unassign_project_role_from_user, + method_args=['dom_id'], + method_kwargs={'user': 'uid', 'role': 'rid'}, + expected_args=[ + self.proxy, + self.proxy._get_resource(user.User, 'uid'), + self.proxy._get_resource(role.Role, 'rid') + ] + ) + + def test_validate_user_has_project_role(self): + self._verify( + "openstack.identity.v3.project.Project.validate_user_has_role", + self.proxy.validate_user_has_project_role, + method_args=['dom_id'], + method_kwargs={'user': 'uid', 'role': 'rid'}, + expected_args=[ + self.proxy, + self.proxy._get_resource(user.User, 'uid'), + self.proxy._get_resource(role.Role, 'rid') + ] + ) + + def test_assign_project_role_to_group(self): + self._verify( + "openstack.identity.v3.project.Project.assign_role_to_group", + self.proxy.assign_project_role_to_group, + method_args=['dom_id'], + method_kwargs={'group': 'uid', 'role': 'rid'}, + expected_args=[ + self.proxy, + self.proxy._get_resource(group.Group, 'uid'), + self.proxy._get_resource(role.Role, 'rid') + ] + ) + + def test_unassign_project_role_from_group(self): + self._verify( + "openstack.identity.v3.project.Project.unassign_role_from_group", + self.proxy.unassign_project_role_from_group, + method_args=['dom_id'], + method_kwargs={'group': 'uid', 'role': 'rid'}, + expected_args=[ + self.proxy, + self.proxy._get_resource(group.Group, 'uid'), + self.proxy._get_resource(role.Role, 'rid') + ] + ) + + def test_validate_group_has_project_role(self): + self._verify( + "openstack.identity.v3.project.Project.validate_group_has_role", + self.proxy.validate_group_has_project_role, + method_args=['dom_id'], + method_kwargs={'group': 'uid', 'role': 'rid'}, + expected_args=[ + self.proxy, + self.proxy._get_resource(group.Group, 'uid'), + self.proxy._get_resource(role.Role, 'rid') + ] + ) From 8a1d255ca879e520d1df50b63214721db3c2cc16 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 12 May 2021 15:58:00 +0200 Subject: [PATCH 2878/3836] Switch identity roles in the cloud layer to proxy Now the roles operations including role assignments are switched to be using proxy layer instead of reimplementing REST calls. Change-Id: I74d9202ed452fdbba558b0a4b708bffeea6b266b --- openstack/cloud/_identity.py | 377 +-- openstack/cloud/_utils.py | 2 +- openstack/identity/v3/_proxy.py | 10 +- .../tests/unit/cloud/test_identity_roles.py | 10 +- .../tests/unit/cloud/test_role_assignment.py | 2948 ++++++----------- 5 files changed, 1176 insertions(+), 2171 deletions(-) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index e8a5fe996..6999c2c29 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -21,7 +21,6 @@ from openstack.cloud import _utils from openstack.cloud import exc from openstack import exceptions -from openstack import utils class IdentityCloudMixin(_normalize.Normalizer): @@ -873,18 +872,14 @@ def delete_group(self, name_or_id, **kwargs): def list_roles(self, **kwargs): """List Keystone roles. - :param domain_id: domain id for listing roles (v3) + :param domain_id: domain id for listing roles :returns: a list of ``munch.Munch`` containing the role description. :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ - v2 = self._is_client_version('identity', 2) - url = '/OS-KSADM/roles' if v2 else '/roles' - data = self._identity_client.get( - url, params=kwargs, error_message="Failed to list roles") - return self._normalize_roles(self._get_and_munchify('roles', data)) + return list(self.identity.roles(**kwargs)) @_utils.valid_kwargs('domain_id') def search_roles(self, name_or_id=None, filters=None, **kwargs): @@ -927,34 +922,6 @@ def get_role(self, name_or_id, filters=None, **kwargs): """ return _utils._get_entity(self, 'role', name_or_id, filters, **kwargs) - def _keystone_v2_role_assignments(self, user, project=None, - role=None, **kwargs): - data = self._identity_client.get( - "/tenants/{tenant}/users/{user}/roles".format( - tenant=project, user=user), - error_message="Failed to list role assignments") - - roles = self._get_and_munchify('roles', data) - - ret = [] - for tmprole in roles: - if role is not None and role != tmprole.id: - continue - ret.append({ - 'role': { - 'id': tmprole.id - }, - 'scope': { - 'project': { - 'id': project, - } - }, - 'user': { - 'id': user, - } - }) - return ret - def _keystone_v3_role_assignments(self, **filters): # NOTE(samueldmq): different parameters have different representation # patterns as query parameters in the call to the list role assignments @@ -983,10 +950,7 @@ def _keystone_v3_role_assignments(self, **filters): filters['os_inherit_extension_inherited_to']) del filters['os_inherit_extension_inherited_to'] - data = self._identity_client.get( - '/role_assignments', params=filters, - error_message="Failed to list role assignments") - return self._get_and_munchify('role_assignments', data) + return list(self.identity.role_assignments(**filters)) def list_role_assignments(self, filters=None): """List Keystone role assignments @@ -1006,9 +970,6 @@ def list_role_assignments(self, filters=None): 'user' and 'group' are mutually exclusive, as are 'domain' and 'project'. - NOTE: For keystone v2, only user, project, and role are used. - Project and user are both required in filters. - :returns: a list of ``munch.Munch`` containing the role assignment description. Contains the following attributes:: @@ -1037,7 +998,15 @@ def list_role_assignments(self, filters=None): if isinstance(v, munch.Munch): filters[k] = v['id'] - assignments = self._keystone_v3_role_assignments(**filters) + for k in ['role', 'group', 'user']: + if k in filters: + filters['%s_id' % k] = filters.pop(k) + + for k in ['domain', 'project']: + if k in filters: + filters['scope_%s_id' % k] = filters.pop(k) + + assignments = self.identity.role_assignments(**filters) return _utils.normalize_role_assignments(assignments) @@ -1052,14 +1021,8 @@ def create_role(self, name, **kwargs): :raise OpenStackCloudException: if the role cannot be created """ - v2 = self._is_client_version('identity', 2) - url = '/OS-KSADM/roles' if v2 else '/roles' kwargs['name'] = name - msg = 'Failed to create role {name}'.format(name=name) - data = self._identity_client.post( - url, json={'role': kwargs}, error_message=msg) - role = self._get_and_munchify('role', data) - return self._normalize_role(role) + return self.identity.create_role(**kwargs) @_utils.valid_kwargs('domain_id') def update_role(self, name_or_id, name, **kwargs): @@ -1073,22 +1036,13 @@ def update_role(self, name_or_id, name, **kwargs): :raise OpenStackCloudException: if the role cannot be created """ - if self._is_client_version('identity', 2): - raise exc.OpenStackCloudUnavailableFeature( - 'Unavailable Feature: Role update requires Identity v3' - ) - kwargs['name_or_id'] = name_or_id - role = self.get_role(**kwargs) + role = self.get_role(name_or_id, **kwargs) if role is None: self.log.debug( "Role %s not found for updating", name_or_id) return False - msg = 'Failed to update role {name}'.format(name=name_or_id) - json_kwargs = {'role_id': role.id, 'role': {'name': name}} - data = self._identity_client.patch('/roles', error_message=msg, - json=json_kwargs) - role = self._get_and_munchify('role', data) - return self._normalize_role(role) + + return self.identity.update_role(role, name=name, **kwargs) @_utils.valid_kwargs('domain_id') def delete_role(self, name_or_id, **kwargs): @@ -1108,44 +1062,51 @@ def delete_role(self, name_or_id, **kwargs): "Role %s not found for deleting", name_or_id) return False - v2 = self._is_client_version('identity', 2) - url = '{preffix}/{id}'.format( - preffix='/OS-KSADM/roles' if v2 else '/roles', id=role['id']) - error_msg = "Unable to delete role {name}".format(name=name_or_id) - self._identity_client.delete(url, error_message=error_msg) - - return True + try: + self.identity.delete_role(role) + return True + except exceptions.SDKExceptions: + self.log.exception( + "Unable to delete role {name}".format( + name=name_or_id)) + raise def _get_grant_revoke_params(self, role, user=None, group=None, project=None, domain=None): - role = self.get_role(role) - if role is None: - return {} - data = {'role': role.id} - - # domain and group not available in keystone v2.0 - is_keystone_v2 = self._is_client_version('identity', 2) - - filters = {} - if not is_keystone_v2 and domain: - filters['domain_id'] = data['domain'] = \ - self.get_domain(domain)['id'] + data = {} + search_args = {} + if domain: + data['domain'] = self.identity.find_domain( + domain, ignore_missing=False) + # We have domain. We should use it for further searching user, + # group, role, project + search_args['domain_id'] = data['domain'].id + + data['role'] = self.identity.find_role(name_or_id=role) + if not data['role']: + raise exc.OpenStackCloudException( + 'Role {0} not found.'.format(role)) if user: - if domain: - data['user'] = self.get_user( - user, domain_id=filters['domain_id'], filters=filters) - else: - data['user'] = self.get_user(user, filters=filters) - - if project: - # drop domain in favor of project - data.pop('domain', None) - data['project'] = self.identity.find_project(project, **filters) + # use cloud.get_user to save us from bad searching by name + data['user'] = self.get_user(user, filters=search_args) + if group: + data['group'] = self.identity.find_group( + group, ignore_missing=False, **search_args) - if not is_keystone_v2 and group: - data['group'] = self.get_group(group, filters=filters) + if data.get('user') and data.get('group'): + raise exc.OpenStackCloudException( + 'Specify either a group or a user, not both') + if data.get('user') is None and data.get('group') is None: + raise exc.OpenStackCloudException( + 'Must specify either a user or a group') + if project is None and domain is None: + raise exc.OpenStackCloudException( + 'Must specify either a domain or project') + if project: + data['project'] = self.identity.find_project( + project, ignore_missing=False, **search_args) return data def grant_role(self, name_or_id, user=None, group=None, @@ -1168,66 +1129,56 @@ def grant_role(self, name_or_id, user=None, group=None, NOTE: for wait and timeout, sometimes granting roles is not instantaneous. - NOTE: project is required for keystone v2 - :returns: True if the role is assigned, otherwise False :raise OpenStackCloudException: if the role cannot be granted """ - data = self._get_grant_revoke_params(name_or_id, user, group, - project, domain) - filters = data.copy() - if not data: - raise exc.OpenStackCloudException( - 'Role {0} not found.'.format(name_or_id)) + data = self._get_grant_revoke_params( + name_or_id, user=user, group=group, + project=project, domain=domain) - if data.get('user') is not None and data.get('group') is not None: - raise exc.OpenStackCloudException( - 'Specify either a group or a user, not both') - if data.get('user') is None and data.get('group') is None: - raise exc.OpenStackCloudException( - 'Must specify either a user or a group') - if self._is_client_version('identity', 2) and \ - data.get('project') is None: - raise exc.OpenStackCloudException( - 'Must specify project for keystone v2') - - if self.list_role_assignments(filters=filters): - self.log.debug('Assignment already exists') - return False - - error_msg = "Error granting access to role: {0}".format(data) - if self._is_client_version('identity', 2): - # For v2.0, only tenant/project assignment is supported - url = "/tenants/{t}/users/{u}/roles/OS-KSADM/{r}".format( - t=data['project']['id'], u=data['user']['id'], r=data['role']) - - self._identity_client.put(url, error_message=error_msg, - endpoint_filter={'interface': 'admin'}) - else: - if data.get('project') is None and data.get('domain') is None: - raise exc.OpenStackCloudException( - 'Must specify either a domain or project') + user = data.get('user') + group = data.get('group') + project = data.get('project') + domain = data.get('domain') + role = data.get('role') - # For v3, figure out the assignment type and build the URL - if data.get('domain'): - url = "/domains/{}".format(data['domain']) + if project: + # Proceed with project - precedence over domain + if user: + has_role = self.identity.validate_user_has_project_role( + project, user, role) + if has_role: + self.log.debug('Assignment already exists') + return False + self.identity.assign_project_role_to_user( + project, user, role) else: - url = "/projects/{}".format(data['project']['id']) - if data.get('group'): - url += "/groups/{}".format(data['group']['id']) + has_role = self.identity.validate_group_has_project_role( + project, group, role) + if has_role: + self.log.debug('Assignment already exists') + return False + self.identity.assign_project_role_to_group( + project, group, role) + else: + # Proceed with domain + if user: + has_role = self.identity.validate_user_has_domain_role( + domain, user, role) + if has_role: + self.log.debug('Assignment already exists') + return False + self.identity.assign_domain_role_to_user( + domain, user, role) else: - url += "/users/{}".format(data['user']['id']) - url += "/roles/{}".format(data.get('role')) - - self._identity_client.put(url, error_message=error_msg) - - if wait: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for role to be granted"): - if self.list_role_assignments(filters=filters): - break + has_role = self.identity.validate_group_has_domain_role( + domain, group, role) + if has_role: + self.log.debug('Assignment already exists') + return False + self.identity.assign_domain_role_to_group( + domain, group, role) return True def revoke_role(self, name_or_id, user=None, group=None, @@ -1251,92 +1202,53 @@ def revoke_role(self, name_or_id, user=None, group=None, :raise OpenStackCloudException: if the role cannot be removed """ - data = self._get_grant_revoke_params(name_or_id, user, group, - project, domain) - filters = data.copy() + data = self._get_grant_revoke_params( + name_or_id, user=user, group=group, + project=project, domain=domain) - if not data: - raise exc.OpenStackCloudException( - 'Role {0} not found.'.format(name_or_id)) + user = data.get('user') + group = data.get('group') + project = data.get('project') + domain = data.get('domain') + role = data.get('role') - if data.get('user') is not None and data.get('group') is not None: - raise exc.OpenStackCloudException( - 'Specify either a group or a user, not both') - if data.get('user') is None and data.get('group') is None: - raise exc.OpenStackCloudException( - 'Must specify either a user or a group') - if self._is_client_version('identity', 2) and \ - data.get('project') is None: - raise exc.OpenStackCloudException( - 'Must specify project for keystone v2') - - if not self.list_role_assignments(filters=filters): - self.log.debug('Assignment does not exist') - return False - - error_msg = "Error revoking access to role: {0}".format(data) - if self._is_client_version('identity', 2): - # For v2.0, only tenant/project assignment is supported - url = "/tenants/{t}/users/{u}/roles/OS-KSADM/{r}".format( - t=data['project']['id'], u=data['user']['id'], r=data['role']) - - self._identity_client.delete( - url, error_message=error_msg, - endpoint_filter={'interface': 'admin'}) - else: - if data.get('project') is None and data.get('domain') is None: - raise exc.OpenStackCloudException( - 'Must specify either a domain or project') - - # For v3, figure out the assignment type and build the URL - if data.get('domain'): - url = "/domains/{}".format(data['domain']) - else: - url = "/projects/{}".format(data['project']['id']) - if data.get('group'): - url += "/groups/{}".format(data['group']['id']) - else: - url += "/users/{}".format(data['user']['id']) - url += "/roles/{}".format(data.get('role')) - - self._identity_client.delete(url, error_message=error_msg) - - if wait: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for role to be revoked"): - if not self.list_role_assignments(filters=filters): - break - return True - - def _get_project_id_param_dict(self, name_or_id): - if name_or_id: - project = self.get_project(name_or_id) - if not project: - return {} - if self._is_client_version('identity', 3): - return {'default_project_id': project['id']} + if project: + # Proceed with project - precedence over domain + if user: + has_role = self.identity.validate_user_has_project_role( + project, user, role) + if not has_role: + self.log.debug('Assignment does not exists') + return False + self.identity.unassign_project_role_from_user( + project, user, role) else: - return {'tenant_id': project['id']} + has_role = self.identity.validate_group_has_project_role( + project, group, role) + if not has_role: + self.log.debug('Assignment does not exists') + return False + self.identity.unassign_project_role_from_group( + project, group, role) else: - return {} - - def _get_domain_id_param_dict(self, domain_id): - """Get a useable domain.""" - - # Keystone v3 requires domains for user and project creation. v2 does - # not. However, keystone v2 does not allow user creation by non-admin - # users, so we can throw an error to the user that does not need to - # mention api versions - if self._is_client_version('identity', 3): - if not domain_id: - raise exc.OpenStackCloudException( - "User or project creation requires an explicit" - " domain_id argument.") + # Proceed with domain + if user: + has_role = self.identity.validate_user_has_domain_role( + domain, user, role) + if not has_role: + self.log.debug('Assignment does not exists') + return False + self.identity.unassign_domain_role_from_user( + domain, user, role) else: - return {'domain_id': domain_id} - else: - return {} + has_role = self.identity.validate_group_has_domain_role( + domain, group, role) + if not has_role: + self.log.debug('Assignment does not exists') + return False + self.identity.unassign_domain_role_from_group( + domain, group, role) + return True def _get_identity_params(self, domain_id=None, project=None): """Get the domain and project/tenant parameters if needed. @@ -1345,6 +1257,17 @@ def _get_identity_params(self, domain_id=None, project=None): pass project or tenant_id or domain or nothing in a sane manner. """ ret = {} - ret.update(self._get_domain_id_param_dict(domain_id)) + if not domain_id: + raise exc.OpenStackCloudException( + "User or project creation requires an explicit" + " domain_id argument.") + else: + ret.update({'domain_id': domain_id}) + ret.update(self._get_project_id_param_dict(project)) + if project: + project_obj = self.get_project(project) + if project_obj: + ret.update({'default_project_id': project['id']}) + return ret diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index ab4748260..c86d6b512 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -329,7 +329,7 @@ def normalize_role_assignments(assignments): if scope in assignment['scope']: new_val[scope] = assignment['scope'][scope]['id'] for assignee in ('user', 'group'): - if assignee in assignment: + if assignment[assignee]: new_val[assignee] = assignment[assignee]['id'] new_assignments.append(new_val) return new_assignments diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 8bc2b7b1d..58df68b84 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -309,7 +309,7 @@ def delete_group(self, group, ignore_missing=True): """ self._delete(_group.Group, group, ignore_missing=ignore_missing) - def find_group(self, name_or_id, ignore_missing=True): + def find_group(self, name_or_id, ignore_missing=True, **kwargs): """Find a single group :param name_or_id: The name or ID of a group. @@ -321,7 +321,8 @@ def find_group(self, name_or_id, ignore_missing=True): :returns: One :class:`~openstack.identity.v3.group.Group` or None """ return self._find(_group.Group, name_or_id, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, + **kwargs) def get_group(self, group): """Get a single group @@ -858,7 +859,7 @@ def delete_role(self, role, ignore_missing=True): """ self._delete(_role.Role, role, ignore_missing=ignore_missing) - def find_role(self, name_or_id, ignore_missing=True): + def find_role(self, name_or_id, ignore_missing=True, **kwargs): """Find a single role :param name_or_id: The name or ID of a role. @@ -870,7 +871,8 @@ def find_role(self, name_or_id, ignore_missing=True): :returns: One :class:`~openstack.identity.v3.role.Role` or None """ return self._find(_role.Role, name_or_id, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, + **kwargs) def get_role(self, role): """Get a single role diff --git a/openstack/tests/unit/cloud/test_identity_roles.py b/openstack/tests/unit/cloud/test_identity_roles.py index bfafc58d7..fb32bd088 100644 --- a/openstack/tests/unit/cloud/test_identity_roles.py +++ b/openstack/tests/unit/cloud/test_identity_roles.py @@ -119,22 +119,21 @@ def test_create_role(self): def test_update_role(self): role_data = self._get_role_data() - req = {'role_id': role_data.role_id, - 'role': {'name': role_data.role_name}} + req = {'role': {'name': 'new_name'}} self.register_uris([ dict(method='GET', uri=self.get_mock_url(), status_code=200, json={'roles': [role_data.json_response['role']]}), dict(method='PATCH', - uri=self.get_mock_url(), + uri=self.get_mock_url(append=[role_data.role_id]), status_code=200, json=role_data.json_response, validate=dict(json=req)) ]) role = self.cloud.update_role( - role_data.role_id, role_data.role_name) + role_data.role_id, 'new_name') self.assertIsNotNone(role) self.assertThat(role.name, matchers.Equals(role_data.role_name)) @@ -242,8 +241,7 @@ def test_list_role_assignments_exception(self): status_code=403) ]) with testtools.ExpectedException( - openstack.cloud.exc.OpenStackCloudHTTPError, - "Failed to list role assignments" + openstack.cloud.exc.OpenStackCloudHTTPError ): self.cloud.list_role_assignments() self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_role_assignment.py b/openstack/tests/unit/cloud/test_role_assignment.py index a59cb23b2..4a700c1dc 100644 --- a/openstack/tests/unit/cloud/test_role_assignment.py +++ b/openstack/tests/unit/cloud/test_role_assignment.py @@ -91,1879 +91,1067 @@ def get_mock_url(self, service_type='identity', interface='public', service_type, interface, resource, append, base_url_append, qs_elements) - def test_grant_role_user_project(self): - self.register_uris([ - # user name - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='users', - qs_elements=['name=%s' % - self.user_data.name]), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', + def __get(self, resource, data, attr, qs_elements, use_name=False, + is_found=True): + if not use_name: + if is_found: + return [dict( + method='GET', + uri=self.get_mock_url( + resource=resource + 's', # do roles from role + append=[getattr(data, attr)], + qs_elements=qs_elements), + status_code=200, + json=data.json_response) + ] + else: + return [ + dict( + method='GET', + uri=self.get_mock_url( + resource=resource + 's', # do roles from role + append=[getattr(data, attr)], + qs_elements=qs_elements), + status_code=404), + dict( + method='GET', + uri=self.get_mock_url( + resource=resource + 's', # do roles from role + qs_elements=qs_elements), + status_code=200, + json={(resource + 's'): []}) + ] + else: + return [ + dict( + method='GET', + uri=self.get_mock_url( + resource=resource + 's', + append=[getattr(data, attr)], + qs_elements=qs_elements), + status_code=404), + dict( + method='GET', + uri=self.get_mock_url( + resource=resource + 's', + qs_elements=[ + 'name=' + getattr(data, attr)] + qs_elements), + status_code=200, + json={ + (resource + 's'): [data.json_response[resource]]}) + ] + + def __user_mocks(self, user_data, use_name, is_found=True): + uri_mocks = [] + if not use_name: + uri_mocks.append(dict( + method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={'users': [user_data.json_response['user']] if is_found + else []})) + else: + uri_mocks.append(dict( + method='GET', + uri=self.get_mock_url( + resource='users', + qs_elements=[ + 'name=' + user_data.name + ] + ), + status_code=200, + json={'users': [user_data.json_response['user']] if is_found + else []})) + return uri_mocks + + def _get_mock_role_query_urls( + self, role_data, domain_data=None, project_data=None, + group_data=None, user_data=None, + use_role_name=False, use_domain_name=False, use_project_name=False, + use_group_name=False, use_user_name=False, use_domain_in_query=True + ): + """Build uri mocks for querying role assignments + """ + uri_mocks = [] + + if domain_data: + uri_mocks.extend( + self.__get( + 'domain', domain_data, + 'domain_id' if not use_domain_name else 'domain_name', + [], use_name=use_domain_name) + ) + + qs_elements = [] + if domain_data and use_domain_in_query: + qs_elements = [ + 'domain_id=' + domain_data.domain_id + ] + + uri_mocks.extend( + self.__get( + 'role', role_data, + 'role_id' if not use_role_name else 'role_name', + [], use_name=use_role_name) + ) + + if user_data: + uri_mocks.extend(self.__user_mocks( + user_data, use_user_name, is_found=True)) + + if group_data: + uri_mocks.extend( + self.__get( + 'group', group_data, + 'group_id' if not use_group_name else 'group_name', + qs_elements, use_name=use_group_name) + ) + + if project_data: + uri_mocks.extend( + self.__get( + 'project', project_data, + 'project_id' if not use_project_name else 'project_name', + qs_elements, use_name=use_project_name) + ) + + return uri_mocks + + def test_grant_role_user_id_project(self): + uris = self._get_mock_role_query_urls( + self.role_data, project_data=self.project_data, + user_data=self.user_data, use_role_name=True) + uris.extend([ + dict(method='HEAD', uri=self.get_mock_url( resource='projects', - append=[self.project_data.project_id]), - status_code=200, - json={'project': - self.project_data.json_response['project']}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'user.id=%s' % self.user_data.user_id, - 'scope.project.id=%s' % self.project_data.project_id, - 'role.id=%s' % self.role_data.role_id]), + append=[ + self.project_data.project_id, + 'users', self.user_data.user_id, + 'roles', self.role_data.role_id + ]), complete_qs=True, - status_code=200, - json={'role_assignments': []}), + status_code=404), dict(method='PUT', uri=self.get_mock_url( resource='projects', - append=[self.project_data.project_id, 'users', - self.user_data.user_id, 'roles', - self.role_data.role_id]), - status_code=204), - # user id - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='users'), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', + append=[self.project_data.project_id, + 'users', self.user_data.user_id, + 'roles', self.role_data.role_id]), + status_code=200), + ]) + self.register_uris(uris) + + self.assertTrue( + self.cloud.grant_role( + self.role_data.role_name, + user=self.user_data.user_id, + project=self.project_data.project_id)) + self.assert_calls() + + def test_grant_role_user_name_project(self): + uris = self._get_mock_role_query_urls( + self.role_data, + project_data=self.project_data, user_data=self.user_data, + use_role_name=True, use_user_name=True) + uris.extend([ + dict(method='HEAD', uri=self.get_mock_url( resource='projects', - append=[self.project_data.project_id]), - status_code=200, - json={'project': - self.project_data.json_response['project']}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'user.id=%s' % self.user_data.user_id, - 'scope.project.id=%s' % self.project_data.project_id, - 'role.id=%s' % self.role_data.role_id]), + append=[ + self.project_data.project_id, + 'users', self.user_data.user_id, + 'roles', self.role_data.role_id + ]), complete_qs=True, - status_code=200, - json={'role_assignments': []}), + status_code=404), dict(method='PUT', uri=self.get_mock_url( resource='projects', - append=[self.project_data.project_id, 'users', - self.user_data.user_id, 'roles', - self.role_data.role_id]), - status_code=204), + append=[self.project_data.project_id, + 'users', self.user_data.user_id, + 'roles', self.role_data.role_id]), + status_code=200), ]) - # user name + self.register_uris(uris) + self.assertTrue( self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, project=self.project_data.project_id)) - # user id - self.assertTrue( - self.cloud.grant_role( - self.role_data.role_name, - user=self.user_data.user_id, - project=self.project_data.project_id)) - self.assert_calls() - def test_grant_role_user_project_exists(self): - self.register_uris([ - # user name - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='users', - qs_elements=['name=%s' % - self.user_data.name]), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', + def test_grant_role_user_id_project_exists(self): + uris = self._get_mock_role_query_urls( + self.role_data, + project_data=self.project_data, user_data=self.user_data) + uris.extend([ + dict(method='HEAD', uri=self.get_mock_url( resource='projects', - append=[self.project_data.project_id]), - status_code=200, - json={'project': - self.project_data.json_response['project']}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'user.id=%s' % self.user_data.user_id, - 'scope.project.id=%s' % self.project_data.project_id, - 'role.id=%s' % self.role_data.role_id]), + append=[ + self.project_data.project_id, + 'users', self.user_data.user_id, + 'roles', self.role_data.role_id + ]), complete_qs=True, - status_code=200, - json={ - 'role_assignments': self._build_role_assignment_response( - role_id=self.role_data.role_id, - scope_type='project', - scope_id=self.project_data.project_id, - entity_type='user', - entity_id=self.user_data.user_id)}), - # user id - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='users'), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', + status_code=204), + ]) + self.register_uris(uris) + + self.assertFalse(self.cloud.grant_role( + self.role_data.role_id, + user=self.user_data.user_id, + project=self.project_data.project_id)) + self.assert_calls() + + def test_grant_role_user_name_project_exists(self): + uris = self._get_mock_role_query_urls( + self.role_data, + project_data=self.project_data, user_data=self.user_data, + use_role_name=True, use_user_name=True) + uris.extend([ + dict(method='HEAD', uri=self.get_mock_url( resource='projects', - append=[self.project_data.project_id]), - status_code=200, - json={'project': - self.project_data.json_response['project']}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'user.id=%s' % self.user_data.user_id, - 'scope.project.id=%s' % self.project_data.project_id, - 'role.id=%s' % self.role_data.role_id]), + append=[ + self.project_data.project_id, + 'users', self.user_data.user_id, + 'roles', self.role_data.role_id + ]), complete_qs=True, - status_code=200, - json={ - 'role_assignments': self._build_role_assignment_response( - role_id=self.role_data.role_id, - scope_type='project', - scope_id=self.project_data.project_id, - entity_type='user', - entity_id=self.user_data.user_id)}), + status_code=204), ]) - # user name + self.register_uris(uris) + self.assertFalse(self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, project=self.project_data.project_id)) - # user id - self.assertFalse(self.cloud.grant_role( - self.role_data.role_id, - user=self.user_data.user_id, - project=self.project_data.project_id)) self.assert_calls() - def test_grant_role_group_project(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', + def test_grant_role_group_id_project(self): + uris = self._get_mock_role_query_urls( + self.role_data, + project_data=self.project_data, group_data=self.group_data, + use_role_name=True) + uris.extend([ + dict(method='HEAD', uri=self.get_mock_url( resource='projects', - append=[self.project_data.project_id]), - status_code=200, - json={'project': - self.project_data.json_response['project']}), - dict(method='GET', - uri=self.get_mock_url(resource='groups'), - status_code=200, - json={'groups': [self.group_data.json_response['group']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'group.id=%s' % self.group_data.group_id, - 'scope.project.id=%s' % self.project_data.project_id, - 'role.id=%s' % self.role_data.role_id]), + append=[ + self.project_data.project_id, + 'groups', self.group_data.group_id, + 'roles', self.role_data.role_id + ]), complete_qs=True, - status_code=200, - json={'role_assignments': []}), + status_code=404), dict(method='PUT', uri=self.get_mock_url( resource='projects', - append=[self.project_data.project_id, 'groups', - self.group_data.group_id, 'roles', - self.role_data.role_id]), - status_code=204), - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', + append=[self.project_data.project_id, + 'groups', self.group_data.group_id, + 'roles', self.role_data.role_id]), + status_code=200), + ]) + self.register_uris(uris) + + self.assertTrue( + self.cloud.grant_role( + self.role_data.role_name, + group=self.group_data.group_id, + project=self.project_data.project_id)) + self.assert_calls() + + def test_grant_role_group_name_project(self): + uris = self._get_mock_role_query_urls( + self.role_data, + project_data=self.project_data, group_data=self.group_data, + use_role_name=True, use_group_name=True) + uris.extend([ + dict(method='HEAD', uri=self.get_mock_url( resource='projects', - append=[self.project_data.project_id]), - status_code=200, - json={'project': - self.project_data.json_response['project']}), - dict(method='GET', - uri=self.get_mock_url(resource='groups'), - status_code=200, - json={'groups': [self.group_data.json_response['group']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'group.id=%s' % self.group_data.group_id, - 'scope.project.id=%s' % self.project_data.project_id, - 'role.id=%s' % self.role_data.role_id]), + append=[ + self.project_data.project_id, + 'groups', self.group_data.group_id, + 'roles', self.role_data.role_id + ]), complete_qs=True, - status_code=200, - json={'role_assignments': []}), + status_code=404), dict(method='PUT', uri=self.get_mock_url( resource='projects', - append=[self.project_data.project_id, 'groups', - self.group_data.group_id, 'roles', - self.role_data.role_id]), - status_code=204), + append=[self.project_data.project_id, + 'groups', self.group_data.group_id, + 'roles', self.role_data.role_id]), + status_code=200), ]) - self.assertTrue(self.cloud.grant_role( - self.role_data.role_name, - group=self.group_data.group_name, - project=self.project_data.project_id)) - self.assertTrue(self.cloud.grant_role( - self.role_data.role_name, - group=self.group_data.group_id, - project=self.project_data.project_id)) + self.register_uris(uris) + + self.assertTrue( + self.cloud.grant_role( + self.role_data.role_name, + group=self.group_data.group_name, + project=self.project_data.project_id)) self.assert_calls() - def test_grant_role_group_project_exists(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', + def test_grant_role_group_id_project_exists(self): + uris = self._get_mock_role_query_urls( + self.role_data, + project_data=self.project_data, group_data=self.group_data) + uris.extend([ + dict(method='HEAD', uri=self.get_mock_url( resource='projects', - append=[self.project_data.project_id]), - status_code=200, - json={'project': - self.project_data.json_response['project']}), - dict(method='GET', - uri=self.get_mock_url(resource='groups'), - status_code=200, - json={'groups': [self.group_data.json_response['group']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'group.id=%s' % self.group_data.group_id, - 'scope.project.id=%s' % self.project_data.project_id, - 'role.id=%s' % self.role_data.role_id]), + append=[ + self.project_data.project_id, + 'groups', self.group_data.group_id, + 'roles', self.role_data.role_id + ]), complete_qs=True, - status_code=200, - json={ - 'role_assignments': self._build_role_assignment_response( - role_id=self.role_data.role_id, - scope_type='project', - scope_id=self.project_data.project_id, - entity_type='group', - entity_id=self.group_data.group_id)}), - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', + status_code=204), + ]) + self.register_uris(uris) + + self.assertFalse(self.cloud.grant_role( + self.role_data.role_id, + group=self.group_data.group_id, + project=self.project_data.project_id)) + self.assert_calls() + + def test_grant_role_group_name_project_exists(self): + uris = self._get_mock_role_query_urls( + self.role_data, + project_data=self.project_data, group_data=self.group_data, + use_role_name=True, use_group_name=True) + uris.extend([ + dict(method='HEAD', uri=self.get_mock_url( resource='projects', - append=[self.project_data.project_id]), - status_code=200, - json={'project': - self.project_data.json_response['project']}), - dict(method='GET', - uri=self.get_mock_url(resource='groups'), - status_code=200, - json={'groups': [self.group_data.json_response['group']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'group.id=%s' % self.group_data.group_id, - 'scope.project.id=%s' % self.project_data.project_id, - 'role.id=%s' % self.role_data.role_id]), + append=[ + self.project_data.project_id, + 'groups', self.group_data.group_id, + 'roles', self.role_data.role_id + ]), complete_qs=True, - status_code=200, - json={ - 'role_assignments': self._build_role_assignment_response( - role_id=self.role_data.role_id, - scope_type='project', - scope_id=self.project_data.project_id, - entity_type='group', - entity_id=self.group_data.group_id)}), + status_code=204), ]) + self.register_uris(uris) + self.assertFalse(self.cloud.grant_role( self.role_data.role_name, group=self.group_data.group_name, project=self.project_data.project_id)) - self.assertFalse(self.cloud.grant_role( - self.role_data.role_name, - group=self.group_data.group_id, - project=self.project_data.project_id)) self.assert_calls() - def test_grant_role_user_domain(self): - self.register_uris([ - # user name and domain id - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='users', - qs_elements=['domain_id=%s' % - self.domain_data. - domain_id, - 'name=%s' % - self.user_data.name]), - complete_qs=True, - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'role.id=%s' % self.role_data.role_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'user.id=%s' % self.user_data.user_id]), - complete_qs=True, - status_code=200, - json={'role_assignments': []}), +# ===== Domain + def test_grant_role_user_id_domain(self): + uris = self._get_mock_role_query_urls( + self.role_data, domain_data=self.domain_data, + user_data=self.user_data, use_role_name=True) + uris.extend([ + dict(method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'users', self.user_data.user_id, + 'roles', self.role_data.role_id + ]), + complete_qs=True, + status_code=404), dict(method='PUT', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id, - 'users', - self.user_data.user_id, - 'roles', - self.role_data.role_id]), - status_code=204), - # user id and domain id - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='users', - qs_elements=['domain_id=%s' % - self.domain_data. - domain_id]), - complete_qs=True, - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'role.id=%s' % self.role_data.role_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'user.id=%s' % self.user_data.user_id]), - complete_qs=True, - status_code=200, - json={'role_assignments': []}), + resource='domains', + append=[self.domain_data.domain_id, + 'users', self.user_data.user_id, + 'roles', self.role_data.role_id]), + status_code=200), + ]) + self.register_uris(uris) + + self.assertTrue( + self.cloud.grant_role( + self.role_data.role_name, + user=self.user_data.user_id, + domain=self.domain_data.domain_id)) + self.assert_calls() + + def test_grant_role_user_name_domain(self): + uris = self._get_mock_role_query_urls( + self.role_data, + domain_data=self.domain_data, user_data=self.user_data, + use_role_name=True, use_user_name=True) + uris.extend([ + dict(method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'users', self.user_data.user_id, + 'roles', self.role_data.role_id + ]), + complete_qs=True, + status_code=404), dict(method='PUT', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id, - 'users', - self.user_data.user_id, - 'roles', - self.role_data.role_id]), - status_code=204), - # user name and domain name - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_name]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='users', - qs_elements=['domain_id=%s' % - self.domain_data. - domain_id, - 'name=%s' % - self.user_data.name]), - complete_qs=True, - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'role.id=%s' % self.role_data.role_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'user.id=%s' % self.user_data.user_id]), + resource='domains', + append=[self.domain_data.domain_id, + 'users', self.user_data.user_id, + 'roles', self.role_data.role_id]), + status_code=200), + ]) + self.register_uris(uris) + + self.assertTrue( + self.cloud.grant_role( + self.role_data.role_name, + user=self.user_data.name, + domain=self.domain_data.domain_id)) + + def test_grant_role_user_id_domain_exists(self): + uris = self._get_mock_role_query_urls( + self.role_data, + domain_data=self.domain_data, user_data=self.user_data) + uris.extend([ + dict(method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'users', self.user_data.user_id, + 'roles', self.role_data.role_id + ]), complete_qs=True, - status_code=200, - json={'role_assignments': []}), - dict(method='PUT', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id, - 'users', - self.user_data.user_id, - 'roles', - self.role_data.role_id]), status_code=204), - # user id and domain name - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_name]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='users', - qs_elements=['domain_id=%s' % - self.domain_data. - domain_id]), - complete_qs=True, - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'role.id=%s' % self.role_data.role_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'user.id=%s' % self.user_data.user_id]), + ]) + self.register_uris(uris) + + self.assertFalse(self.cloud.grant_role( + self.role_data.role_id, + user=self.user_data.user_id, + domain=self.domain_data.domain_id)) + self.assert_calls() + + def test_grant_role_user_name_domain_exists(self): + uris = self._get_mock_role_query_urls( + self.role_data, + domain_data=self.domain_data, user_data=self.user_data, + use_role_name=True, use_user_name=True) + uris.extend([ + dict(method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'users', self.user_data.user_id, + 'roles', self.role_data.role_id + ]), complete_qs=True, - status_code=200, - json={'role_assignments': []}), - dict(method='PUT', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id, - 'users', - self.user_data.user_id, - 'roles', - self.role_data.role_id]), status_code=204), ]) - # user name and domain id - self.assertTrue(self.cloud.grant_role( + self.register_uris(uris) + + self.assertFalse(self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, domain=self.domain_data.domain_id)) - # user id and domain id - self.assertTrue(self.cloud.grant_role( - self.role_data.role_name, - user=self.user_data.user_id, - domain=self.domain_data.domain_id)) - # user name and domain name - self.assertTrue(self.cloud.grant_role( - self.role_data.role_name, - user=self.user_data.name, - domain=self.domain_data.domain_name)) - # user id and domain name - self.assertTrue(self.cloud.grant_role( - self.role_data.role_name, - user=self.user_data.user_id, - domain=self.domain_data.domain_name)) self.assert_calls() - def test_grant_role_user_domain_exists(self): - self.register_uris([ - # user name and domain id - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='users', - qs_elements=['domain_id=%s' % - self.domain_data. - domain_id, - 'name=%s' % - self.user_data.name]), - complete_qs=True, - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', + def test_grant_role_group_id_domain(self): + uris = self._get_mock_role_query_urls( + self.role_data, + domain_data=self.domain_data, group_data=self.group_data, + use_role_name=True) + uris.extend([ + dict(method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'groups', self.group_data.group_id, + 'roles', self.role_data.role_id + ]), + complete_qs=True, + status_code=404), + dict(method='PUT', uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'role.id=%s' % self.role_data.role_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'user.id=%s' % self.user_data.user_id]), - complete_qs=True, - status_code=200, - json={ - 'role_assignments': self._build_role_assignment_response( - role_id=self.role_data.role_id, - scope_type='domain', - scope_id=self.domain_data.domain_id, - entity_type='user', - entity_id=self.user_data.user_id)}), - # user id and domain id - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='users', - qs_elements=['domain_id=%s' % - self.domain_data. - domain_id]), - complete_qs=True, - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'role.id=%s' % self.role_data.role_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'user.id=%s' % self.user_data.user_id]), - complete_qs=True, - status_code=200, - json={ - 'role_assignments': self._build_role_assignment_response( - role_id=self.role_data.role_id, - scope_type='domain', - scope_id=self.domain_data.domain_id, - entity_type='user', - entity_id=self.user_data.user_id)}), - # user name and domain name - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_name]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='users', - qs_elements=['domain_id=%s' % - self.domain_data. - domain_id, - 'name=%s' % - self.user_data.name]), - complete_qs=True, - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'role.id=%s' % self.role_data.role_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'user.id=%s' % self.user_data.user_id]), - complete_qs=True, - status_code=200, - json={ - 'role_assignments': self._build_role_assignment_response( - role_id=self.role_data.role_id, - scope_type='domain', - scope_id=self.domain_data.domain_id, - entity_type='user', - entity_id=self.user_data.user_id)}), - # user id and domain name - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_name]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='users', - qs_elements=['domain_id=%s' % - self.domain_data. - domain_id]), - complete_qs=True, - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'role.id=%s' % self.role_data.role_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'user.id=%s' % self.user_data.user_id]), - complete_qs=True, - status_code=200, - json={ - 'role_assignments': self._build_role_assignment_response( - role_id=self.role_data.role_id, - scope_type='domain', - scope_id=self.domain_data.domain_id, - entity_type='user', - entity_id=self.user_data.user_id)}), + resource='domains', + append=[self.domain_data.domain_id, + 'groups', self.group_data.group_id, + 'roles', self.role_data.role_id]), + status_code=200), ]) - # user name and domain id - self.assertFalse(self.cloud.grant_role( - self.role_data.role_name, - user=self.user_data.name, - domain=self.domain_data.domain_id)) - # user id and domain id - self.assertFalse(self.cloud.grant_role( - self.role_data.role_name, - user=self.user_data.user_id, - domain=self.domain_data.domain_id)) - # user name and domain name - self.assertFalse(self.cloud.grant_role( - self.role_data.role_name, - user=self.user_data.name, - domain=self.domain_data.domain_name)) - # user id and domain name - self.assertFalse(self.cloud.grant_role( - self.role_data.role_name, - user=self.user_data.user_id, - domain=self.domain_data.domain_name)) + self.register_uris(uris) + + self.assertTrue( + self.cloud.grant_role( + self.role_data.role_name, + group=self.group_data.group_id, + domain=self.domain_data.domain_id)) self.assert_calls() - def test_grant_role_group_domain(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='groups'), - status_code=200, - json={'groups': [self.group_data.json_response['group']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'role.id=%s' % self.role_data.role_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'group.id=%s' % self.group_data.group_id]), - complete_qs=True, - status_code=200, - json={'role_assignments': []}), - dict(method='PUT', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id, - 'groups', - self.group_data.group_id, - 'roles', - self.role_data.role_id]), - status_code=204), - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='groups'), - status_code=200, - json={'groups': [self.group_data.json_response['group']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'role.id=%s' % self.role_data.role_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'group.id=%s' % self.group_data.group_id]), - complete_qs=True, - status_code=200, - json={'role_assignments': []}), - dict(method='PUT', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id, - 'groups', - self.group_data.group_id, - 'roles', - self.role_data.role_id]), - status_code=204), - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_name]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='groups'), - status_code=200, - json={'groups': [self.group_data.json_response['group']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'role.id=%s' % self.role_data.role_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'group.id=%s' % self.group_data.group_id]), - complete_qs=True, - status_code=200, - json={'role_assignments': []}), + def test_grant_role_group_name_domain(self): + uris = self._get_mock_role_query_urls( + self.role_data, + domain_data=self.domain_data, group_data=self.group_data, + use_role_name=True, use_group_name=True) + uris.extend([ + dict(method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'groups', self.group_data.group_id, + 'roles', self.role_data.role_id + ]), + complete_qs=True, + status_code=404), dict(method='PUT', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id, - 'groups', - self.group_data.group_id, - 'roles', - self.role_data.role_id]), - status_code=204), - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_name]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='groups'), - status_code=200, - json={'groups': [self.group_data.json_response['group']]}), - dict(method='GET', uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'role.id=%s' % self.role_data.role_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'group.id=%s' % self.group_data.group_id]), + resource='domains', + append=[self.domain_data.domain_id, + 'groups', self.group_data.group_id, + 'roles', self.role_data.role_id]), + status_code=200), + ]) + self.register_uris(uris) + + self.assertTrue( + self.cloud.grant_role( + self.role_data.role_name, + group=self.group_data.group_name, + domain=self.domain_data.domain_id)) + self.assert_calls() + + def test_grant_role_group_id_domain_exists(self): + uris = self._get_mock_role_query_urls( + self.role_data, + domain_data=self.domain_data, group_data=self.group_data) + uris.extend([ + dict(method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'groups', self.group_data.group_id, + 'roles', self.role_data.role_id + ]), complete_qs=True, - status_code=200, - json={'role_assignments': []}), - dict(method='PUT', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id, - 'groups', - self.group_data.group_id, - 'roles', - self.role_data.role_id]), status_code=204), ]) - self.assertTrue(self.cloud.grant_role( - self.role_data.role_name, - group=self.group_data.group_name, - domain=self.domain_data.domain_id)) - self.assertTrue(self.cloud.grant_role( - self.role_data.role_name, + self.register_uris(uris) + + self.assertFalse(self.cloud.grant_role( + self.role_data.role_id, group=self.group_data.group_id, domain=self.domain_data.domain_id)) - self.assertTrue(self.cloud.grant_role( - self.role_data.role_name, - group=self.group_data.group_name, - domain=self.domain_data.domain_name)) - self.assertTrue(self.cloud.grant_role( - self.role_data.role_name, - group=self.group_data.group_id, - domain=self.domain_data.domain_name)) self.assert_calls() - def test_grant_role_group_domain_exists(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='groups'), - status_code=200, - json={'groups': [self.group_data.json_response['group']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'role.id=%s' % self.role_data.role_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'group.id=%s' % self.group_data.group_id]), - complete_qs=True, - status_code=200, - json={ - 'role_assignments': self._build_role_assignment_response( - role_id=self.role_data.role_id, - scope_type='domain', - scope_id=self.domain_data.domain_id, - entity_type='group', - entity_id=self.group_data.group_id)}), - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='groups'), - status_code=200, - json={'groups': [self.group_data.json_response['group']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'role.id=%s' % self.role_data.role_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'group.id=%s' % self.group_data.group_id]), - complete_qs=True, - status_code=200, - json={ - 'role_assignments': self._build_role_assignment_response( - role_id=self.role_data.role_id, - scope_type='domain', - scope_id=self.domain_data.domain_id, - entity_type='group', - entity_id=self.group_data.group_id)}), - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_name]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='groups'), - status_code=200, - json={'groups': [self.group_data.json_response['group']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'role.id=%s' % self.role_data.role_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'group.id=%s' % self.group_data.group_id]), - complete_qs=True, - status_code=200, - json={ - 'role_assignments': self._build_role_assignment_response( - role_id=self.role_data.role_id, - scope_type='domain', - scope_id=self.domain_data.domain_id, - entity_type='group', - entity_id=self.group_data.group_id)}), - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_name]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='groups'), - status_code=200, - json={'groups': [self.group_data.json_response['group']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'role.id=%s' % self.role_data.role_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'group.id=%s' % self.group_data.group_id]), + def test_grant_role_group_name_domain_exists(self): + uris = self._get_mock_role_query_urls( + self.role_data, + domain_data=self.domain_data, group_data=self.group_data, + use_role_name=True, use_group_name=True) + uris.extend([ + dict(method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'groups', self.group_data.group_id, + 'roles', self.role_data.role_id + ]), complete_qs=True, - status_code=200, - json={ - 'role_assignments': self._build_role_assignment_response( - role_id=self.role_data.role_id, - scope_type='domain', - scope_id=self.domain_data.domain_id, - entity_type='group', - entity_id=self.group_data.group_id)}), + status_code=204), ]) + self.register_uris(uris) + self.assertFalse(self.cloud.grant_role( self.role_data.role_name, group=self.group_data.group_name, domain=self.domain_data.domain_id)) - self.assertFalse(self.cloud.grant_role( - self.role_data.role_name, - group=self.group_data.group_id, - domain=self.domain_data.domain_id)) - self.assertFalse(self.cloud.grant_role( - self.role_data.role_name, - group=self.group_data.group_name, - domain=self.domain_data.domain_name)) - self.assertFalse(self.cloud.grant_role( - self.role_data.role_name, - group=self.group_data.group_id, - domain=self.domain_data.domain_name)) self.assert_calls() - def test_revoke_role_user_project(self): - self.register_uris([ - # user name - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='users', - qs_elements=['name=%s' % - self.user_data.name]), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', +# ==== Revoke + def test_revoke_role_user_id_project(self): + uris = self._get_mock_role_query_urls( + self.role_data, project_data=self.project_data, + user_data=self.user_data, use_role_name=True) + uris.extend([ + dict(method='HEAD', uri=self.get_mock_url( resource='projects', - append=[self.project_data.project_id]), - status_code=200, - json={'project': - self.project_data.json_response['project']}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'user.id=%s' % self.user_data.user_id, - 'scope.project.id=%s' % self.project_data.project_id, - 'role.id=%s' % self.role_data.role_id]), - status_code=200, + append=[ + self.project_data.project_id, + 'users', self.user_data.user_id, + 'roles', self.role_data.role_id + ]), complete_qs=True, - json={'role_assignments': []}), - # user id - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='users'), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', + status_code=204), + dict(method='DELETE', uri=self.get_mock_url( resource='projects', - append=[self.project_data.project_id]), - status_code=200, - json={'project': - self.project_data.json_response['project']}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'user.id=%s' % self.user_data.user_id, - 'scope.project.id=%s' % self.project_data.project_id, - 'role.id=%s' % self.role_data.role_id]), - status_code=200, - complete_qs=True, - json={'role_assignments': []}), + append=[self.project_data.project_id, + 'users', self.user_data.user_id, + 'roles', self.role_data.role_id]), + status_code=200), ]) - # user name - self.assertFalse(self.cloud.revoke_role( - self.role_data.role_name, - user=self.user_data.name, - project=self.project_data.project_id)) - # user id - self.assertFalse(self.cloud.revoke_role( - self.role_data.role_name, - user=self.user_data.user_id, - project=self.project_data.project_id)) + self.register_uris(uris) + + self.assertTrue( + self.cloud.revoke_role( + self.role_data.role_name, + user=self.user_data.user_id, + project=self.project_data.project_id)) self.assert_calls() - def test_revoke_role_user_project_exists(self): - self.register_uris([ - # role name and user name - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='users', - qs_elements=['name=%s' % - self.user_data.name]), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', + def test_revoke_role_user_name_project(self): + uris = self._get_mock_role_query_urls( + self.role_data, + project_data=self.project_data, user_data=self.user_data, + use_role_name=True, use_user_name=True) + uris.extend([ + dict(method='HEAD', uri=self.get_mock_url( resource='projects', - append=[self.project_data.project_id]), - status_code=200, - json={'project': - self.project_data.json_response['project']}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'user.id=%s' % self.user_data.user_id, - 'scope.project.id=%s' % self.project_data.project_id, - 'role.id=%s' % self.role_data.role_id]), - status_code=200, + append=[ + self.project_data.project_id, + 'users', self.user_data.user_id, + 'roles', self.role_data.role_id + ]), complete_qs=True, - json={'role_assignments': - self._build_role_assignment_response( - role_id=self.role_data.role_id, - scope_type='project', - scope_id=self.project_data.project_id, - entity_type='user', - entity_id=self.user_data.user_id)}), + status_code=204), dict(method='DELETE', - uri=self.get_mock_url(resource='projects', - append=[self.project_data.project_id, - 'users', - self.user_data.user_id, - 'roles', - self.role_data.role_id])), - # role id and user id - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='users'), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', uri=self.get_mock_url( resource='projects', - append=[self.project_data.project_id]), - status_code=200, - json={'project': - self.project_data.json_response['project']}), - dict(method='GET', + append=[self.project_data.project_id, + 'users', self.user_data.user_id, + 'roles', self.role_data.role_id]), + status_code=200), + ]) + self.register_uris(uris) + + self.assertTrue( + self.cloud.revoke_role( + self.role_data.role_name, + user=self.user_data.name, + project=self.project_data.project_id)) + + def test_revoke_role_user_id_project_not_exists(self): + uris = self._get_mock_role_query_urls( + self.role_data, + project_data=self.project_data, user_data=self.user_data) + uris.extend([ + dict(method='HEAD', uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'user.id=%s' % self.user_data.user_id, - 'scope.project.id=%s' % self.project_data.project_id, - 'role.id=%s' % self.role_data.role_id]), - status_code=200, + resource='projects', + append=[ + self.project_data.project_id, + 'users', self.user_data.user_id, + 'roles', self.role_data.role_id + ]), complete_qs=True, - json={'role_assignments': - self._build_role_assignment_response( - role_id=self.role_data.role_id, - scope_type='project', - scope_id=self.project_data.project_id, - entity_type='user', - entity_id=self.user_data.user_id)}), - dict(method='DELETE', - uri=self.get_mock_url(resource='projects', - append=[self.project_data.project_id, - 'users', - self.user_data.user_id, - 'roles', - self.role_data.role_id])), + status_code=404), ]) - # role name and user name - self.assertTrue(self.cloud.revoke_role( - self.role_data.role_name, - user=self.user_data.name, - project=self.project_data.project_id)) - # role id and user id - self.assertTrue(self.cloud.revoke_role( + self.register_uris(uris) + + self.assertFalse(self.cloud.revoke_role( self.role_data.role_id, user=self.user_data.user_id, project=self.project_data.project_id)) self.assert_calls() - def test_revoke_role_group_project(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', + def test_revoke_role_user_name_project_not_exists(self): + uris = self._get_mock_role_query_urls( + self.role_data, + project_data=self.project_data, user_data=self.user_data, + use_role_name=True, use_user_name=True) + uris.extend([ + dict(method='HEAD', uri=self.get_mock_url( resource='projects', - append=[self.project_data.project_id]), - status_code=200, - json={'project': - self.project_data.json_response['project']}), - dict(method='GET', - uri=self.get_mock_url(resource='groups'), - status_code=200, - json={'groups': [self.group_data.json_response['group']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'group.id=%s' % self.group_data.group_id, - 'scope.project.id=%s' % self.project_data.project_id, - 'role.id=%s' % self.role_data.role_id]), - status_code=200, + append=[ + self.project_data.project_id, + 'users', self.user_data.user_id, + 'roles', self.role_data.role_id + ]), complete_qs=True, - json={'role_assignments': []}), - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='projects', - append=[self.project_data.project_id]), - status_code=200, - json={'project': - self.project_data.json_response['project']}), - dict(method='GET', - uri=self.get_mock_url(resource='groups'), - status_code=200, - json={'groups': [self.group_data.json_response['group']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'group.id=%s' % self.group_data.group_id, - 'scope.project.id=%s' % self.project_data.project_id, - 'role.id=%s' % self.role_data.role_id]), - status_code=200, - complete_qs=True, - json={'role_assignments': []}), + status_code=404), ]) + self.register_uris(uris) + self.assertFalse(self.cloud.revoke_role( self.role_data.role_name, - group=self.group_data.group_name, - project=self.project_data.project_id)) - self.assertFalse(self.cloud.revoke_role( - self.role_data.role_name, - group=self.group_data.group_id, + user=self.user_data.name, project=self.project_data.project_id)) self.assert_calls() - def test_revoke_role_group_project_exists(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', + def test_revoke_role_group_id_project(self): + uris = self._get_mock_role_query_urls( + self.role_data, + project_data=self.project_data, group_data=self.group_data, + use_role_name=True) + uris.extend([ + dict(method='HEAD', uri=self.get_mock_url( resource='projects', - append=[self.project_data.project_id]), - status_code=200, - json={'project': - self.project_data.json_response['project']}), - dict(method='GET', - uri=self.get_mock_url(resource='groups'), - status_code=200, - json={'groups': [self.group_data.json_response['group']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'group.id=%s' % self.group_data.group_id, - 'scope.project.id=%s' % self.project_data.project_id, - 'role.id=%s' % self.role_data.role_id]), - status_code=200, + append=[ + self.project_data.project_id, + 'groups', self.group_data.group_id, + 'roles', self.role_data.role_id + ]), complete_qs=True, - json={'role_assignments': - self._build_role_assignment_response( - role_id=self.role_data.role_id, - scope_type='project', - scope_id=self.project_data.project_id, - entity_type='group', - entity_id=self.group_data.group_id)}), + status_code=204), dict(method='DELETE', - uri=self.get_mock_url(resource='projects', - append=[self.project_data.project_id, - 'groups', - self.group_data.group_id, - 'roles', - self.role_data.role_id])), - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', uri=self.get_mock_url( resource='projects', - append=[self.project_data.project_id]), - status_code=200, - json={'project': - self.project_data.json_response['project']}), - dict(method='GET', - uri=self.get_mock_url(resource='groups'), - status_code=200, - json={'groups': [self.group_data.json_response['group']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'group.id=%s' % self.group_data.group_id, - 'scope.project.id=%s' % self.project_data.project_id, - 'role.id=%s' % self.role_data.role_id]), - status_code=200, - complete_qs=True, - json={'role_assignments': - self._build_role_assignment_response( - role_id=self.role_data.role_id, - scope_type='project', - scope_id=self.project_data.project_id, - entity_type='group', - entity_id=self.group_data.group_id)}), - dict(method='DELETE', - uri=self.get_mock_url(resource='projects', - append=[self.project_data.project_id, - 'groups', - self.group_data.group_id, - 'roles', - self.role_data.role_id])), + append=[self.project_data.project_id, + 'groups', self.group_data.group_id, + 'roles', self.role_data.role_id]), + status_code=200), ]) - self.assertTrue(self.cloud.revoke_role( - self.role_data.role_name, - group=self.group_data.group_name, - project=self.project_data.project_id)) - self.assertTrue(self.cloud.revoke_role( - self.role_data.role_name, - group=self.group_data.group_id, - project=self.project_data.project_id)) - self.assert_calls() + self.register_uris(uris) - def test_revoke_role_user_domain(self): - self.register_uris([ - # user name and domain id - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='users', - qs_elements=['domain_id=%s' % - self.domain_data. - domain_id, - 'name=%s' % - self.user_data.name]), - complete_qs=True, - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'user.id=%s' % self.user_data.user_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'role.id=%s' % self.role_data.role_id]), - status_code=200, - complete_qs=True, - json={'role_assignments': []}), - # user id and domain id - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='users', - qs_elements=['domain_id=%s' % - self.domain_data. - domain_id]), - complete_qs=True, - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'user.id=%s' % self.user_data.user_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'role.id=%s' % self.role_data.role_id]), - status_code=200, - complete_qs=True, - json={'role_assignments': []}), - # user name and domain name - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_name]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='users', - qs_elements=['domain_id=%s' % - self.domain_data. - domain_id, - 'name=%s' % - self.user_data.name]), - complete_qs=True, - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'user.id=%s' % self.user_data.user_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'role.id=%s' % self.role_data.role_id]), - status_code=200, - complete_qs=True, - json={'role_assignments': []}), - # user id and domain name - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_name]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='users', - qs_elements=['domain_id=%s' % - self.domain_data. - domain_id]), - complete_qs=True, - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'user.id=%s' % self.user_data.user_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'role.id=%s' % self.role_data.role_id]), - status_code=200, - complete_qs=True, - json={'role_assignments': []}), - ]) - # user name and domain id - self.assertFalse(self.cloud.revoke_role( - self.role_data.role_name, - user=self.user_data.name, - domain=self.domain_data.domain_id)) - # user id and domain id - self.assertFalse(self.cloud.revoke_role( - self.role_data.role_name, - user=self.user_data.user_id, - domain=self.domain_data.domain_id)) - # user name and domain name - self.assertFalse(self.cloud.revoke_role( - self.role_data.role_name, - user=self.user_data.name, - domain=self.domain_data.domain_name)) - # user id and domain name - self.assertFalse(self.cloud.revoke_role( - self.role_data.role_name, - user=self.user_data.user_id, - domain=self.domain_data.domain_name)) + self.assertTrue( + self.cloud.revoke_role( + self.role_data.role_name, + group=self.group_data.group_id, + project=self.project_data.project_id)) self.assert_calls() - def test_revoke_role_user_domain_exists(self): - self.register_uris([ - # user name and domain name - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_name]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='users', - qs_elements=['domain_id=%s' % - self.domain_data. - domain_id, - 'name=%s' % - self.user_data.name]), - complete_qs=True, - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'user.id=%s' % self.user_data.user_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'role.id=%s' % self.role_data.role_id]), - status_code=200, - complete_qs=True, - json={'role_assignments': - self._build_role_assignment_response( - role_id=self.role_data.role_id, - scope_type='domain', - scope_id=self.domain_data.domain_id, - entity_type='user', - entity_id=self.user_data.user_id)}), - dict(method='DELETE', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id, - 'users', - self.user_data.user_id, - 'roles', - self.role_data.role_id])), - # user id and domain name - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_name]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='users', - qs_elements=['domain_id=%s' % - self.domain_data. - domain_id]), - complete_qs=True, - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'user.id=%s' % self.user_data.user_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'role.id=%s' % self.role_data.role_id]), - status_code=200, - complete_qs=True, - json={ - 'role_assignments': self._build_role_assignment_response( - role_id=self.role_data.role_id, - scope_type='domain', - scope_id=self.domain_data.domain_id, - entity_type='user', - entity_id=self.user_data.user_id)}), - dict(method='DELETE', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id, - 'users', - self.user_data.user_id, - 'roles', - self.role_data.role_id])), - # user name and domain id - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='users', - qs_elements=['domain_id=%s' % - self.domain_data. - domain_id, - 'name=%s' % - self.user_data.name]), - complete_qs=True, - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', + def test_revoke_role_group_name_project(self): + uris = self._get_mock_role_query_urls( + self.role_data, + project_data=self.project_data, group_data=self.group_data, + use_role_name=True, use_group_name=True) + uris.extend([ + dict(method='HEAD', uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'user.id=%s' % self.user_data.user_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'role.id=%s' % self.role_data.role_id]), - status_code=200, + resource='projects', + append=[ + self.project_data.project_id, + 'groups', self.group_data.group_id, + 'roles', self.role_data.role_id + ]), complete_qs=True, - json={'role_assignments': - self._build_role_assignment_response( - role_id=self.role_data.role_id, - scope_type='domain', - scope_id=self.domain_data.domain_id, - entity_type='user', - entity_id=self.user_data.user_id)}), + status_code=204), dict(method='DELETE', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id, - 'users', - self.user_data.user_id, - 'roles', - self.role_data.role_id])), - # user id and domain id - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='users', - qs_elements=['domain_id=%s' % - self.domain_data. - domain_id]), - complete_qs=True, - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'user.id=%s' % self.user_data.user_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'role.id=%s' % self.role_data.role_id]), - status_code=200, - complete_qs=True, - json={ - 'role_assignments': self._build_role_assignment_response( - role_id=self.role_data.role_id, - scope_type='domain', - scope_id=self.domain_data.domain_id, - entity_type='user', - entity_id=self.user_data.user_id)}), - dict(method='DELETE', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id, - 'users', - self.user_data.user_id, - 'roles', - self.role_data.role_id])), + resource='projects', + append=[self.project_data.project_id, + 'groups', self.group_data.group_id, + 'roles', self.role_data.role_id]), + status_code=200), ]) - # user name and domain name - self.assertTrue(self.cloud.revoke_role( - self.role_data.role_name, - user=self.user_data.name, - domain=self.domain_data.domain_name)) - # user id and domain name - self.assertTrue(self.cloud.revoke_role( - self.role_data.role_name, - user=self.user_data.user_id, - domain=self.domain_data.domain_name)) - # user name and domain id - self.assertTrue(self.cloud.revoke_role( - self.role_data.role_name, - user=self.user_data.name, - domain=self.domain_data.domain_id)) - # user id and domain id - self.assertTrue(self.cloud.revoke_role( - self.role_data.role_name, - user=self.user_data.user_id, - domain=self.domain_data.domain_id)) + self.register_uris(uris) + + self.assertTrue( + self.cloud.revoke_role( + self.role_data.role_name, + group=self.group_data.group_name, + project=self.project_data.project_id)) self.assert_calls() - def test_revoke_role_group_domain(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_name]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='groups'), - status_code=200, - json={'groups': [self.group_data.json_response['group']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'group.id=%s' % self.group_data.group_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'role.id=%s' % self.role_data.role_id]), - status_code=200, - complete_qs=True, - json={'role_assignments': []}), - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_name]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='groups'), - status_code=200, - json={'groups': [self.group_data.json_response['group']]}), - dict(method='GET', + def test_revoke_role_group_id_project_not_exists(self): + uris = self._get_mock_role_query_urls( + self.role_data, + project_data=self.project_data, group_data=self.group_data) + uris.extend([ + dict(method='HEAD', uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'group.id=%s' % self.group_data.group_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'role.id=%s' % self.role_data.role_id]), - status_code=200, - complete_qs=True, - json={'role_assignments': []}), - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='groups'), - status_code=200, - json={'groups': [self.group_data.json_response['group']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'group.id=%s' % self.group_data.group_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'role.id=%s' % self.role_data.role_id]), - status_code=200, - complete_qs=True, - json={'role_assignments': []}), - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='groups'), - status_code=200, - json={'groups': [self.group_data.json_response['group']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'group.id=%s' % self.group_data.group_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'role.id=%s' % self.role_data.role_id]), - status_code=200, + resource='projects', + append=[ + self.project_data.project_id, + 'groups', self.group_data.group_id, + 'roles', self.role_data.role_id + ]), complete_qs=True, - json={'role_assignments': []}), + status_code=404), ]) + self.register_uris(uris) + self.assertFalse(self.cloud.revoke_role( - self.role_data.role_name, - group=self.group_data.group_name, - domain=self.domain_data.domain_name)) - self.assertFalse(self.cloud.revoke_role( - self.role_data.role_name, + self.role_data.role_id, group=self.group_data.group_id, - domain=self.domain_data.domain_name)) + project=self.project_data.project_id)) + self.assert_calls() + + def test_revoke_role_group_name_project_not_exists(self): + uris = self._get_mock_role_query_urls( + self.role_data, + project_data=self.project_data, group_data=self.group_data, + use_role_name=True, use_group_name=True) + uris.extend([ + dict(method='HEAD', + uri=self.get_mock_url( + resource='projects', + append=[ + self.project_data.project_id, + 'groups', self.group_data.group_id, + 'roles', self.role_data.role_id + ]), + complete_qs=True, + status_code=404), + ]) + self.register_uris(uris) + self.assertFalse(self.cloud.revoke_role( self.role_data.role_name, group=self.group_data.group_name, - domain=self.domain_data.domain_id)) - self.assertFalse(self.cloud.revoke_role( - self.role_data.role_name, - group=self.group_data.group_id, - domain=self.domain_data.domain_id)) + project=self.project_data.project_id)) self.assert_calls() - def test_revoke_role_group_domain_exists(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_name]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='groups'), - status_code=200, - json={'groups': [self.group_data.json_response['group']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'group.id=%s' % self.group_data.group_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'role.id=%s' % self.role_data.role_id]), - status_code=200, +# ==== Domain + def test_revoke_role_user_id_domain(self): + uris = self._get_mock_role_query_urls( + self.role_data, domain_data=self.domain_data, + user_data=self.user_data, use_role_name=True) + uris.extend([ + dict(method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'users', self.user_data.user_id, + 'roles', self.role_data.role_id + ]), complete_qs=True, - json={'role_assignments': - self._build_role_assignment_response( - role_id=self.role_data.role_id, - scope_type='domain', - scope_id=self.domain_data.domain_id, - entity_type='group', - entity_id=self.group_data.group_id)}), + status_code=204), dict(method='DELETE', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id, - 'groups', - self.group_data.group_id, - 'roles', - self.role_data.role_id])), - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_name]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='groups'), - status_code=200, - json={'groups': [self.group_data.json_response['group']]}), - dict(method='GET', uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'group.id=%s' % self.group_data.group_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'role.id=%s' % self.role_data.role_id]), - status_code=200, + resource='domains', + append=[self.domain_data.domain_id, + 'users', self.user_data.user_id, + 'roles', self.role_data.role_id]), + status_code=200), + ]) + self.register_uris(uris) + + self.assertTrue( + self.cloud.revoke_role( + self.role_data.role_name, + user=self.user_data.user_id, + domain=self.domain_data.domain_id)) + self.assert_calls() + + def test_revoke_role_user_name_domain(self): + uris = self._get_mock_role_query_urls( + self.role_data, + domain_data=self.domain_data, user_data=self.user_data, + use_role_name=True, use_user_name=True) + uris.extend([ + dict(method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'users', self.user_data.user_id, + 'roles', self.role_data.role_id + ]), complete_qs=True, - json={ - 'role_assignments': self._build_role_assignment_response( - role_id=self.role_data.role_id, - scope_type='domain', - scope_id=self.domain_data.domain_id, - entity_type='group', - entity_id=self.group_data.group_id)}), + status_code=204), dict(method='DELETE', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id, - 'groups', - self.group_data.group_id, - 'roles', - self.role_data.role_id])), - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='groups'), - status_code=200, - json={'groups': [self.group_data.json_response['group']]}), - dict(method='GET', uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'group.id=%s' % self.group_data.group_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'role.id=%s' % self.role_data.role_id]), - status_code=200, + resource='domains', + append=[self.domain_data.domain_id, + 'users', self.user_data.user_id, + 'roles', self.role_data.role_id]), + status_code=200), + ]) + self.register_uris(uris) + + self.assertTrue( + self.cloud.revoke_role( + self.role_data.role_name, + user=self.user_data.name, + domain=self.domain_data.domain_id)) + + def test_revoke_role_user_id_domain_not_exists(self): + uris = self._get_mock_role_query_urls( + self.role_data, + domain_data=self.domain_data, user_data=self.user_data) + uris.extend([ + dict(method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'users', self.user_data.user_id, + 'roles', self.role_data.role_id + ]), + complete_qs=True, + status_code=404), + ]) + self.register_uris(uris) + + self.assertFalse(self.cloud.revoke_role( + self.role_data.role_id, + user=self.user_data.user_id, + domain=self.domain_data.domain_id)) + self.assert_calls() + + def test_revoke_role_user_name_domain_not_exists(self): + uris = self._get_mock_role_query_urls( + self.role_data, + domain_data=self.domain_data, user_data=self.user_data, + use_role_name=True, use_user_name=True) + uris.extend([ + dict(method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'users', self.user_data.user_id, + 'roles', self.role_data.role_id + ]), + complete_qs=True, + status_code=404), + ]) + self.register_uris(uris) + + self.assertFalse(self.cloud.revoke_role( + self.role_data.role_name, + user=self.user_data.name, + domain=self.domain_data.domain_id)) + self.assert_calls() + + def test_revoke_role_group_id_domain(self): + uris = self._get_mock_role_query_urls( + self.role_data, + domain_data=self.domain_data, group_data=self.group_data, + use_role_name=True) + uris.extend([ + dict(method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'groups', self.group_data.group_id, + 'roles', self.role_data.role_id + ]), complete_qs=True, - json={'role_assignments': - self._build_role_assignment_response( - role_id=self.role_data.role_id, - scope_type='domain', - scope_id=self.domain_data.domain_id, - entity_type='group', - entity_id=self.group_data.group_id)}), + status_code=204), dict(method='DELETE', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id, - 'groups', - self.group_data.group_id, - 'roles', - self.role_data.role_id])), - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='groups'), - status_code=200, - json={'groups': [self.group_data.json_response['group']]}), - dict(method='GET', uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'group.id=%s' % self.group_data.group_id, - 'scope.domain.id=%s' % self.domain_data.domain_id, - 'role.id=%s' % self.role_data.role_id]), - status_code=200, + resource='domains', + append=[self.domain_data.domain_id, + 'groups', self.group_data.group_id, + 'roles', self.role_data.role_id]), + status_code=200), + ]) + self.register_uris(uris) + + self.assertTrue( + self.cloud.revoke_role( + self.role_data.role_name, + group=self.group_data.group_id, + domain=self.domain_data.domain_id)) + self.assert_calls() + + def test_revoke_role_group_name_domain(self): + uris = self._get_mock_role_query_urls( + self.role_data, + domain_data=self.domain_data, group_data=self.group_data, + use_role_name=True, use_group_name=True) + uris.extend([ + dict(method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'groups', self.group_data.group_id, + 'roles', self.role_data.role_id + ]), complete_qs=True, - json={ - 'role_assignments': self._build_role_assignment_response( - role_id=self.role_data.role_id, - scope_type='domain', - scope_id=self.domain_data.domain_id, - entity_type='group', - entity_id=self.group_data.group_id)}), + status_code=204), dict(method='DELETE', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_id, - 'groups', - self.group_data.group_id, - 'roles', - self.role_data.role_id])), + uri=self.get_mock_url( + resource='domains', + append=[self.domain_data.domain_id, + 'groups', self.group_data.group_id, + 'roles', self.role_data.role_id]), + status_code=200), ]) - self.assertTrue(self.cloud.revoke_role( - self.role_data.role_name, - group=self.group_data.group_name, - domain=self.domain_data.domain_name)) - self.assertTrue(self.cloud.revoke_role( - self.role_data.role_name, + self.register_uris(uris) + + self.assertTrue( + self.cloud.revoke_role( + self.role_data.role_name, + group=self.group_data.group_name, + domain=self.domain_data.domain_id)) + self.assert_calls() + + def test_revoke_role_group_id_domain_not_exists(self): + uris = self._get_mock_role_query_urls( + self.role_data, + domain_data=self.domain_data, group_data=self.group_data) + uris.extend([ + dict(method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'groups', self.group_data.group_id, + 'roles', self.role_data.role_id + ]), + complete_qs=True, + status_code=404), + ]) + self.register_uris(uris) + + self.assertFalse(self.cloud.revoke_role( + self.role_data.role_id, group=self.group_data.group_id, - domain=self.domain_data.domain_name)) - self.assertTrue(self.cloud.revoke_role( - self.role_data.role_name, - group=self.group_data.group_name, domain=self.domain_data.domain_id)) - self.assertTrue(self.cloud.revoke_role( + self.assert_calls() + + def test_revoke_role_group_name_domain_not_exists(self): + uris = self._get_mock_role_query_urls( + self.role_data, + domain_data=self.domain_data, group_data=self.group_data, + use_role_name=True, use_group_name=True) + uris.extend([ + dict(method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'groups', self.group_data.group_id, + 'roles', self.role_data.role_id + ]), + complete_qs=True, + status_code=404), + ]) + self.register_uris(uris) + + self.assertFalse(self.cloud.revoke_role( self.role_data.role_name, - group=self.group_data.group_id, + group=self.group_data.group_name, domain=self.domain_data.domain_id)) self.assert_calls() def test_grant_no_role(self): - self.register_uris([ + uris = self.__get( + 'domain', self.domain_data, 'domain_name', [], use_name=True) + uris.extend([ + dict(method='GET', + uri=self.get_mock_url( + resource='roles', + append=[self.role_data.role_name], + ), + status_code=404), dict(method='GET', - uri=self.get_mock_url(resource='roles'), + uri=self.get_mock_url( + resource='roles', + qs_elements=[ + 'name=' + self.role_data.role_name, + ]), status_code=200, json={'roles': []}) ]) + self.register_uris(uris) with testtools.ExpectedException( exc.OpenStackCloudException, @@ -1976,12 +1164,25 @@ def test_grant_no_role(self): self.assert_calls() def test_revoke_no_role(self): - self.register_uris([ + uris = self.__get( + 'domain', self.domain_data, 'domain_name', [], use_name=True) + uris.extend([ + dict(method='GET', + uri=self.get_mock_url( + resource='roles', + append=[self.role_data.role_name], + ), + status_code=404), dict(method='GET', - uri=self.get_mock_url(resource='roles'), + uri=self.get_mock_url( + resource='roles', + qs_elements=[ + 'name=' + self.role_data.role_name, + ]), status_code=200, json={'roles': []}) ]) + self.register_uris(uris) with testtools.ExpectedException( exc.OpenStackCloudException, @@ -1994,46 +1195,34 @@ def test_revoke_no_role(self): self.assert_calls() def test_grant_no_user_or_group_specified(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}) - ]) + uris = self.__get( + 'role', self.role_data, 'role_name', [], use_name=True) + self.register_uris(uris) with testtools.ExpectedException( - exc.OpenStackCloudException, - 'Must specify either a user or a group' + exc.OpenStackCloudException, + 'Must specify either a user or a group' ): self.cloud.grant_role(self.role_data.role_name) self.assert_calls() def test_revoke_no_user_or_group_specified(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}) - ]) + uris = self.__get( + 'role', self.role_data, 'role_name', [], use_name=True) + self.register_uris(uris) with testtools.ExpectedException( - exc.OpenStackCloudException, - 'Must specify either a user or a group' + exc.OpenStackCloudException, + 'Must specify either a user or a group' ): self.cloud.revoke_role(self.role_data.role_name) self.assert_calls() def test_grant_no_user_or_group(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='users', - qs_elements=['name=%s' % - self.user_data.name]), - status_code=200, - json={'users': []}) - ]) + uris = self.__get( + 'role', self.role_data, 'role_name', [], use_name=True) + uris.extend(self.__user_mocks(self.user_data, use_name=True, + is_found=False)) + self.register_uris(uris) + with testtools.ExpectedException( exc.OpenStackCloudException, 'Must specify either a user or a group' @@ -2044,18 +1233,12 @@ def test_grant_no_user_or_group(self): self.assert_calls() def test_revoke_no_user_or_group(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='users', - qs_elements=['name=%s' % - self.user_data.name]), - status_code=200, - json={'users': []}) - ]) + uris = self.__get( + 'role', self.role_data, 'role_name', [], use_name=True) + uris.extend(self.__user_mocks(self.user_data, use_name=True, + is_found=False)) + self.register_uris(uris) + with testtools.ExpectedException( exc.OpenStackCloudException, 'Must specify either a user or a group' @@ -2066,22 +1249,13 @@ def test_revoke_no_user_or_group(self): self.assert_calls() def test_grant_both_user_and_group(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='users', - qs_elements=['name=%s' % - self.user_data.name]), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url(resource='groups'), - status_code=200, - json={'groups': [self.group_data.json_response['group']]}), - ]) + uris = self.__get( + 'role', self.role_data, 'role_name', [], use_name=True) + uris.extend(self.__user_mocks(self.user_data, use_name=True)) + uris.extend(self.__get( + 'group', self.group_data, 'group_name', [], use_name=True)) + self.register_uris(uris) + with testtools.ExpectedException( exc.OpenStackCloudException, 'Specify either a group or a user, not both' @@ -2093,22 +1267,13 @@ def test_grant_both_user_and_group(self): self.assert_calls() def test_revoke_both_user_and_group(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='users', - qs_elements=['name=%s' % - self.user_data.name]), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url(resource='groups'), - status_code=200, - json={'groups': [self.group_data.json_response['group']]}), - ]) + uris = self.__get( + 'role', self.role_data, 'role_name', [], use_name=True) + uris.extend(self.__user_mocks(self.user_data, use_name=True)) + uris.extend(self.__get( + 'group', self.group_data, 'group_name', [], use_name=True)) + self.register_uris(uris) + with testtools.ExpectedException( exc.OpenStackCloudException, 'Specify either a group or a user, not both' @@ -2117,147 +1282,85 @@ def test_revoke_both_user_and_group(self): self.role_data.role_name, user=self.user_data.name, group=self.group_data.group_name) - self.assert_calls() def test_grant_both_project_and_domain(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_name]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='users', - qs_elements=['domain_id=%s' % - self.domain_data. - domain_id, - 'name=%s' % - self.user_data.name]), - complete_qs=True, - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', + uris = self._get_mock_role_query_urls( + self.role_data, + project_data=self.project_data, user_data=self.user_data, + domain_data=self.domain_data, + use_role_name=True, use_user_name=True, use_project_name=True, + use_domain_name=True, use_domain_in_query=True) + uris.extend([ + dict(method='HEAD', uri=self.get_mock_url( resource='projects', - append=[self.project_data.project_id], - qs_elements=['domain_id=' + self.domain_data.domain_id] - ), - status_code=200, - json={'project': - self.project_data.json_response['project']}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'user.id=%s' % self.user_data.user_id, - 'scope.project.id=%s' % self.project_data.project_id, - 'role.id=%s' % self.role_data.role_id]), + append=[ + self.project_data.project_id, + 'users', self.user_data.user_id, + 'roles', self.role_data.role_id + ]), complete_qs=True, - status_code=200, - json={'role_assignments': []}), + status_code=404), dict(method='PUT', - uri=self.get_mock_url(resource='projects', - append=[self.project_data.project_id, - 'users', - self.user_data.user_id, - 'roles', - self.role_data.role_id]), - status_code=204) + uri=self.get_mock_url( + resource='projects', + append=[self.project_data.project_id, + 'users', self.user_data.user_id, + 'roles', self.role_data.role_id]), + status_code=200), ]) + self.register_uris(uris) + self.assertTrue( self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, - project=self.project_data.project_id, + project=self.project_data.project_name, domain=self.domain_data.domain_name)) - self.assert_calls() def test_revoke_both_project_and_domain(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[self.domain_data.domain_name]), - status_code=200, - json=self.domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(resource='users', - qs_elements=['domain_id=%s' % - self.domain_data. - domain_id, - 'name=%s' % - self.user_data.name]), - complete_qs=True, - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', + uris = self._get_mock_role_query_urls( + self.role_data, + project_data=self.project_data, user_data=self.user_data, + domain_data=self.domain_data, + use_role_name=True, use_user_name=True, use_project_name=True, + use_domain_name=True, use_domain_in_query=True) + uris.extend([ + dict(method='HEAD', uri=self.get_mock_url( resource='projects', - append=[self.project_data.project_id], - qs_elements=['domain_id=' + self.domain_data.domain_id]), - status_code=200, - json={'project': - self.project_data.json_response['project']}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=[ - 'user.id=%s' % self.user_data.user_id, - 'scope.project.id=%s' % self.project_data.project_id, - 'role.id=%s' % self.role_data.role_id]), + append=[ + self.project_data.project_id, + 'users', self.user_data.user_id, + 'roles', self.role_data.role_id + ]), complete_qs=True, - status_code=200, - json={ - 'role_assignments': self._build_role_assignment_response( - role_id=self.role_data.role_id, - scope_type='project', - scope_id=self.project_data.project_id, - entity_type='user', - entity_id=self.user_data.user_id)}), + status_code=204), dict(method='DELETE', - uri=self.get_mock_url(resource='projects', - append=[self.project_data.project_id, - 'users', - self.user_data.user_id, - 'roles', - self.role_data.role_id]), - status_code=204) + uri=self.get_mock_url( + resource='projects', + append=[self.project_data.project_id, + 'users', self.user_data.user_id, + 'roles', self.role_data.role_id]), + status_code=200), ]) - self.assertTrue(self.cloud.revoke_role( - self.role_data.role_name, - user=self.user_data.name, - project=self.project_data.project_id, - domain=self.domain_data.domain_name)) - self.assert_calls() + self.register_uris(uris) + + self.assertTrue( + self.cloud.revoke_role( + self.role_data.role_name, + user=self.user_data.name, + project=self.project_data.project_name, + domain=self.domain_data.domain_name)) def test_grant_no_project_or_domain(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='users', - qs_elements=['name=%s' % - self.user_data.name]), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=['user.id=%s' % self.user_data.user_id, - 'role.id=%s' % self.role_data.role_id]), - complete_qs=True, - status_code=200, - json={'role_assignments': []}) - ]) + uris = self._get_mock_role_query_urls( + self.role_data, + user_data=self.user_data, + use_role_name=True, use_user_name=True) + + self.register_uris(uris) + with testtools.ExpectedException( exc.OpenStackCloudException, 'Must specify either a domain or project' @@ -2268,32 +1371,13 @@ def test_grant_no_project_or_domain(self): self.assert_calls() def test_revoke_no_project_or_domain(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), - dict(method='GET', - uri=self.get_mock_url(resource='users', - qs_elements=['name=%s' % - self.user_data.name]), - status_code=200, - json={'users': [self.user_data.json_response['user']]}), - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=['user.id=%s' % self.user_data.user_id, - 'role.id=%s' % self.role_data.role_id]), - complete_qs=True, - status_code=200, - json={ - 'role_assignments': self._build_role_assignment_response( - role_id=self.role_data.role_id, - scope_type='project', - scope_id=self.project_data.project_id, - entity_type='user', - entity_id=self.user_data.user_id)}) - ]) + uris = self._get_mock_role_query_urls( + self.role_data, + user_data=self.user_data, + use_role_name=True, use_user_name=True) + + self.register_uris(uris) + with testtools.ExpectedException( exc.OpenStackCloudException, 'Must specify either a domain or project' @@ -2306,15 +1390,14 @@ def test_revoke_no_project_or_domain(self): def test_grant_bad_domain_exception(self): self.register_uris([ dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), + uri=self.get_mock_url( + resource='domains', append=['baddomain']), + status_code=404), dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=['baddomain']), - status_code=404, - headers={'Content-Type': 'text/plain'}, - text='Could not find domain: baddomain') + uri=self.get_mock_url( + resource='domains', + qs_elements=['name=baddomain']), + status_code=404) ]) with testtools.ExpectedException(exc.OpenStackCloudURINotFound): self.cloud.grant_role( @@ -2326,15 +1409,14 @@ def test_grant_bad_domain_exception(self): def test_revoke_bad_domain_exception(self): self.register_uris([ dict(method='GET', - uri=self.get_mock_url(resource='roles'), - status_code=200, - json={'roles': [self.role_data.json_response['role']]}), + uri=self.get_mock_url( + resource='domains', append=['baddomain']), + status_code=404), dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=['baddomain']), - status_code=404, - headers={'Content-Type': 'text/plain'}, - text='Could not find domain: baddomain') + uri=self.get_mock_url( + resource='domains', + qs_elements=['name=baddomain']), + status_code=404) ]) with testtools.ExpectedException(exc.OpenStackCloudURINotFound): self.cloud.revoke_role( From b86d3a8c3bf4e9854b0d460565acd89dfaa8bf6e Mon Sep 17 00:00:00 2001 From: Riccardo Pittau Date: Mon, 31 May 2021 15:12:22 +0200 Subject: [PATCH 2879/3836] Fix import order in identity tests Remove tox filter. Plus fix some other import orders that were already fixed in master. Change-Id: I810a5c6718df520323568cb74ad7fcdc2992f85e --- openstack/tests/unit/accelerator/test_version.py | 2 +- .../tests/unit/accelerator/v2/test_accelerator_request.py | 2 +- openstack/tests/unit/accelerator/v2/test_deployable.py | 3 ++- openstack/tests/unit/accelerator/v2/test_device.py | 4 +++- openstack/tests/unit/accelerator/v2/test_device_profile.py | 3 +-- openstack/tests/unit/accelerator/v2/test_proxy.py | 2 +- openstack/tests/unit/config/test_environ.py | 3 +-- openstack/tests/unit/config/test_from_conf.py | 2 +- openstack/tests/unit/config/test_from_session.py | 4 ++-- openstack/tests/unit/identity/v2/test_role.py | 2 +- openstack/tests/unit/identity/v2/test_tenant.py | 2 +- openstack/tests/unit/identity/v2/test_user.py | 2 +- .../tests/unit/identity/v3/test_application_credential.py | 3 ++- openstack/tests/unit/identity/v3/test_credential.py | 2 +- openstack/tests/unit/identity/v3/test_domain.py | 2 ++ openstack/tests/unit/identity/v3/test_endpoint.py | 2 +- openstack/tests/unit/identity/v3/test_federation_protocol.py | 2 +- openstack/tests/unit/identity/v3/test_group.py | 2 +- openstack/tests/unit/identity/v3/test_identity_provider.py | 2 +- openstack/tests/unit/identity/v3/test_limit.py | 3 ++- openstack/tests/unit/identity/v3/test_mapping.py | 2 +- openstack/tests/unit/identity/v3/test_policy.py | 2 +- openstack/tests/unit/identity/v3/test_project.py | 1 + openstack/tests/unit/identity/v3/test_region.py | 2 +- openstack/tests/unit/identity/v3/test_registered_limit.py | 3 ++- openstack/tests/unit/identity/v3/test_role.py | 2 +- openstack/tests/unit/identity/v3/test_role_assignment.py | 2 +- .../unit/identity/v3/test_role_domain_group_assignment.py | 2 +- .../unit/identity/v3/test_role_domain_user_assignment.py | 2 +- .../unit/identity/v3/test_role_project_group_assignment.py | 2 +- .../unit/identity/v3/test_role_project_user_assignment.py | 2 +- openstack/tests/unit/identity/v3/test_service.py | 2 +- openstack/tests/unit/identity/v3/test_trust.py | 2 +- openstack/tests/unit/identity/v3/test_user.py | 2 +- tox.ini | 3 --- 35 files changed, 42 insertions(+), 38 deletions(-) diff --git a/openstack/tests/unit/accelerator/test_version.py b/openstack/tests/unit/accelerator/test_version.py index 43d6378e7..315ffbbf8 100644 --- a/openstack/tests/unit/accelerator/test_version.py +++ b/openstack/tests/unit/accelerator/test_version.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.accelerator import version from openstack.tests.unit import base -from openstack.accelerator import version IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/accelerator/v2/test_accelerator_request.py b/openstack/tests/unit/accelerator/v2/test_accelerator_request.py index 8b36a717f..ad03131ac 100644 --- a/openstack/tests/unit/accelerator/v2/test_accelerator_request.py +++ b/openstack/tests/unit/accelerator/v2/test_accelerator_request.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.accelerator.v2 import accelerator_request as arq from openstack.tests.unit import base -from openstack.accelerator.v2 import accelerator_request as arq FAKE_ID = '0725b527-e51a-41df-ad22-adad5f4546ad' FAKE_RP_UUID = 'f4b7fe6c-8ab4-4914-a113-547af022935b' diff --git a/openstack/tests/unit/accelerator/v2/test_deployable.py b/openstack/tests/unit/accelerator/v2/test_deployable.py index dcb0af554..0bd8061ab 100644 --- a/openstack/tests/unit/accelerator/v2/test_deployable.py +++ b/openstack/tests/unit/accelerator/v2/test_deployable.py @@ -9,11 +9,12 @@ # 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 uuid +from openstack.accelerator.v2 import deployable from openstack.tests.unit import base -from openstack.accelerator.v2 import deployable EXAMPLE = { 'uuid': uuid.uuid4(), diff --git a/openstack/tests/unit/accelerator/v2/test_device.py b/openstack/tests/unit/accelerator/v2/test_device.py index 22b17b336..0151ff73f 100644 --- a/openstack/tests/unit/accelerator/v2/test_device.py +++ b/openstack/tests/unit/accelerator/v2/test_device.py @@ -9,10 +9,12 @@ # 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 uuid -from openstack.tests.unit import base from openstack.accelerator.v2 import device +from openstack.tests.unit import base + EXAMPLE = { 'id': '1', diff --git a/openstack/tests/unit/accelerator/v2/test_device_profile.py b/openstack/tests/unit/accelerator/v2/test_device_profile.py index f1708713b..fcaea7e48 100644 --- a/openstack/tests/unit/accelerator/v2/test_device_profile.py +++ b/openstack/tests/unit/accelerator/v2/test_device_profile.py @@ -10,9 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base - from openstack.accelerator.v2 import device_profile +from openstack.tests.unit import base FAKE = { diff --git a/openstack/tests/unit/accelerator/v2/test_proxy.py b/openstack/tests/unit/accelerator/v2/test_proxy.py index b9fd45867..c9c682d14 100644 --- a/openstack/tests/unit/accelerator/v2/test_proxy.py +++ b/openstack/tests/unit/accelerator/v2/test_proxy.py @@ -11,9 +11,9 @@ # under the License. from openstack.accelerator.v2 import _proxy +from openstack.accelerator.v2 import accelerator_request from openstack.accelerator.v2 import deployable from openstack.accelerator.v2 import device_profile -from openstack.accelerator.v2 import accelerator_request from openstack.tests.unit import test_proxy_base as test_proxy_base diff --git a/openstack/tests/unit/config/test_environ.py b/openstack/tests/unit/config/test_environ.py index 95aa3f643..da02bb562 100644 --- a/openstack/tests/unit/config/test_environ.py +++ b/openstack/tests/unit/config/test_environ.py @@ -12,14 +12,13 @@ # License for the specific language governing permissions and limitations # under the License. +import fixtures from openstack import config from openstack.config import cloud_region from openstack import exceptions from openstack.tests.unit.config import base -import fixtures - class TestEnviron(base.TestCase): diff --git a/openstack/tests/unit/config/test_from_conf.py b/openstack/tests/unit/config/test_from_conf.py index 2f1af17b3..31a75b46d 100644 --- a/openstack/tests/unit/config/test_from_conf.py +++ b/openstack/tests/unit/config/test_from_conf.py @@ -10,10 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -import requests.exceptions import uuid from keystoneauth1 import exceptions as ks_exc +import requests.exceptions from openstack.config import cloud_region from openstack import connection diff --git a/openstack/tests/unit/config/test_from_session.py b/openstack/tests/unit/config/test_from_session.py index e5599d915..0873e296c 100644 --- a/openstack/tests/unit/config/test_from_session.py +++ b/openstack/tests/unit/config/test_from_session.py @@ -11,10 +11,10 @@ # License for the specific language governing permissions and limitations # under the License. -from testscenarios import load_tests_apply_scenarios as load_tests # noqa - import uuid +from testscenarios import load_tests_apply_scenarios as load_tests # noqa + from openstack.config import cloud_region from openstack import connection from openstack.tests import fakes diff --git a/openstack/tests/unit/identity/v2/test_role.py b/openstack/tests/unit/identity/v2/test_role.py index bb7b9f79a..c14ceaf35 100644 --- a/openstack/tests/unit/identity/v2/test_role.py +++ b/openstack/tests/unit/identity/v2/test_role.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.identity.v2 import role from openstack.tests.unit import base -from openstack.identity.v2 import role IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/identity/v2/test_tenant.py b/openstack/tests/unit/identity/v2/test_tenant.py index 31847538f..5145cd802 100644 --- a/openstack/tests/unit/identity/v2/test_tenant.py +++ b/openstack/tests/unit/identity/v2/test_tenant.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.identity.v2 import tenant from openstack.tests.unit import base -from openstack.identity.v2 import tenant IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/identity/v2/test_user.py b/openstack/tests/unit/identity/v2/test_user.py index 300b20b0d..c99616b71 100644 --- a/openstack/tests/unit/identity/v2/test_user.py +++ b/openstack/tests/unit/identity/v2/test_user.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.identity.v2 import user from openstack.tests.unit import base -from openstack.identity.v2 import user IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/identity/v3/test_application_credential.py b/openstack/tests/unit/identity/v3/test_application_credential.py index e6cf3101d..f11b4604b 100644 --- a/openstack/tests/unit/identity/v3/test_application_credential.py +++ b/openstack/tests/unit/identity/v3/test_application_credential.py @@ -9,9 +9,10 @@ # 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.tests.unit import base from openstack.identity.v3 import application_credential +from openstack.tests.unit import base + EXAMPLE = { "user": { diff --git a/openstack/tests/unit/identity/v3/test_credential.py b/openstack/tests/unit/identity/v3/test_credential.py index 0e073f97c..ed77e1ca7 100644 --- a/openstack/tests/unit/identity/v3/test_credential.py +++ b/openstack/tests/unit/identity/v3/test_credential.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.identity.v3 import credential from openstack.tests.unit import base -from openstack.identity.v3 import credential IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/identity/v3/test_domain.py b/openstack/tests/unit/identity/v3/test_domain.py index c7c33b387..8a4df7dec 100644 --- a/openstack/tests/unit/identity/v3/test_domain.py +++ b/openstack/tests/unit/identity/v3/test_domain.py @@ -9,6 +9,7 @@ # 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 unittest import mock from keystoneauth1 import adapter @@ -19,6 +20,7 @@ from openstack.identity.v3 import user from openstack.tests.unit import base + IDENTIFIER = 'IDENTIFIER' EXAMPLE = { 'description': '1', diff --git a/openstack/tests/unit/identity/v3/test_endpoint.py b/openstack/tests/unit/identity/v3/test_endpoint.py index da5a4e818..781c4711d 100644 --- a/openstack/tests/unit/identity/v3/test_endpoint.py +++ b/openstack/tests/unit/identity/v3/test_endpoint.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.identity.v3 import endpoint from openstack.tests.unit import base -from openstack.identity.v3 import endpoint IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/identity/v3/test_federation_protocol.py b/openstack/tests/unit/identity/v3/test_federation_protocol.py index 9176ae6c0..b8ec024b1 100644 --- a/openstack/tests/unit/identity/v3/test_federation_protocol.py +++ b/openstack/tests/unit/identity/v3/test_federation_protocol.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.identity.v3 import federation_protocol from openstack.tests.unit import base -from openstack.identity.v3 import federation_protocol IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/identity/v3/test_group.py b/openstack/tests/unit/identity/v3/test_group.py index bc308e6ce..8b53636cc 100644 --- a/openstack/tests/unit/identity/v3/test_group.py +++ b/openstack/tests/unit/identity/v3/test_group.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.identity.v3 import group from openstack.tests.unit import base -from openstack.identity.v3 import group IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/identity/v3/test_identity_provider.py b/openstack/tests/unit/identity/v3/test_identity_provider.py index c3c9c512f..a733bdd46 100644 --- a/openstack/tests/unit/identity/v3/test_identity_provider.py +++ b/openstack/tests/unit/identity/v3/test_identity_provider.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.identity.v3 import identity_provider from openstack.tests.unit import base -from openstack.identity.v3 import identity_provider IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/identity/v3/test_limit.py b/openstack/tests/unit/identity/v3/test_limit.py index 3d1bec8de..9eac13ce2 100644 --- a/openstack/tests/unit/identity/v3/test_limit.py +++ b/openstack/tests/unit/identity/v3/test_limit.py @@ -9,9 +9,10 @@ # 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.tests.unit import base from openstack.identity.v3 import limit +from openstack.tests.unit import base + EXAMPLE = { "service_id": "8ac43bb0926245cead88676a96c750d3", diff --git a/openstack/tests/unit/identity/v3/test_mapping.py b/openstack/tests/unit/identity/v3/test_mapping.py index 41b07c7ba..40a26d2b8 100644 --- a/openstack/tests/unit/identity/v3/test_mapping.py +++ b/openstack/tests/unit/identity/v3/test_mapping.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.identity.v3 import mapping from openstack.tests.unit import base -from openstack.identity.v3 import mapping IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/identity/v3/test_policy.py b/openstack/tests/unit/identity/v3/test_policy.py index 4bd2256f8..4d5ec6317 100644 --- a/openstack/tests/unit/identity/v3/test_policy.py +++ b/openstack/tests/unit/identity/v3/test_policy.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.identity.v3 import policy from openstack.tests.unit import base -from openstack.identity.v3 import policy IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/identity/v3/test_project.py b/openstack/tests/unit/identity/v3/test_project.py index e6c45d164..d5919f24e 100644 --- a/openstack/tests/unit/identity/v3/test_project.py +++ b/openstack/tests/unit/identity/v3/test_project.py @@ -9,6 +9,7 @@ # 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 unittest import mock from keystoneauth1 import adapter diff --git a/openstack/tests/unit/identity/v3/test_region.py b/openstack/tests/unit/identity/v3/test_region.py index 1ac7a745b..a9a4ffc73 100644 --- a/openstack/tests/unit/identity/v3/test_region.py +++ b/openstack/tests/unit/identity/v3/test_region.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.identity.v3 import region from openstack.tests.unit import base -from openstack.identity.v3 import region IDENTIFIER = 'RegionOne' EXAMPLE = { diff --git a/openstack/tests/unit/identity/v3/test_registered_limit.py b/openstack/tests/unit/identity/v3/test_registered_limit.py index dafb5a81a..4764d9648 100644 --- a/openstack/tests/unit/identity/v3/test_registered_limit.py +++ b/openstack/tests/unit/identity/v3/test_registered_limit.py @@ -9,9 +9,10 @@ # 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.tests.unit import base from openstack.identity.v3 import registered_limit +from openstack.tests.unit import base + EXAMPLE = { "service_id": "8ac43bb0926245cead88676a96c750d3", diff --git a/openstack/tests/unit/identity/v3/test_role.py b/openstack/tests/unit/identity/v3/test_role.py index d45f646e1..3f59f02cb 100644 --- a/openstack/tests/unit/identity/v3/test_role.py +++ b/openstack/tests/unit/identity/v3/test_role.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.identity.v3 import role from openstack.tests.unit import base -from openstack.identity.v3 import role IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/identity/v3/test_role_assignment.py b/openstack/tests/unit/identity/v3/test_role_assignment.py index fac7a6dcf..b141baff5 100644 --- a/openstack/tests/unit/identity/v3/test_role_assignment.py +++ b/openstack/tests/unit/identity/v3/test_role_assignment.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.identity.v3 import role_assignment from openstack.tests.unit import base -from openstack.identity.v3 import role_assignment IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/identity/v3/test_role_domain_group_assignment.py b/openstack/tests/unit/identity/v3/test_role_domain_group_assignment.py index 764da5035..b41ad7db8 100644 --- a/openstack/tests/unit/identity/v3/test_role_domain_group_assignment.py +++ b/openstack/tests/unit/identity/v3/test_role_domain_group_assignment.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.identity.v3 import role_domain_group_assignment from openstack.tests.unit import base -from openstack.identity.v3 import role_domain_group_assignment IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/identity/v3/test_role_domain_user_assignment.py b/openstack/tests/unit/identity/v3/test_role_domain_user_assignment.py index 185c89ce8..20fa07ac8 100644 --- a/openstack/tests/unit/identity/v3/test_role_domain_user_assignment.py +++ b/openstack/tests/unit/identity/v3/test_role_domain_user_assignment.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.identity.v3 import role_domain_user_assignment from openstack.tests.unit import base -from openstack.identity.v3 import role_domain_user_assignment IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/identity/v3/test_role_project_group_assignment.py b/openstack/tests/unit/identity/v3/test_role_project_group_assignment.py index 28351652e..ea3bc2772 100644 --- a/openstack/tests/unit/identity/v3/test_role_project_group_assignment.py +++ b/openstack/tests/unit/identity/v3/test_role_project_group_assignment.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.identity.v3 import role_project_group_assignment from openstack.tests.unit import base -from openstack.identity.v3 import role_project_group_assignment IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/identity/v3/test_role_project_user_assignment.py b/openstack/tests/unit/identity/v3/test_role_project_user_assignment.py index 61e1b8963..55448784b 100644 --- a/openstack/tests/unit/identity/v3/test_role_project_user_assignment.py +++ b/openstack/tests/unit/identity/v3/test_role_project_user_assignment.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.identity.v3 import role_project_user_assignment from openstack.tests.unit import base -from openstack.identity.v3 import role_project_user_assignment IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/identity/v3/test_service.py b/openstack/tests/unit/identity/v3/test_service.py index a56924301..e03b2486d 100644 --- a/openstack/tests/unit/identity/v3/test_service.py +++ b/openstack/tests/unit/identity/v3/test_service.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.identity.v3 import service from openstack.tests.unit import base -from openstack.identity.v3 import service IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/identity/v3/test_trust.py b/openstack/tests/unit/identity/v3/test_trust.py index 63c38a70c..6a8dd7bea 100644 --- a/openstack/tests/unit/identity/v3/test_trust.py +++ b/openstack/tests/unit/identity/v3/test_trust.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.identity.v3 import trust from openstack.tests.unit import base -from openstack.identity.v3 import trust IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/identity/v3/test_user.py b/openstack/tests/unit/identity/v3/test_user.py index 9d8040467..0cd7f4cb5 100644 --- a/openstack/tests/unit/identity/v3/test_user.py +++ b/openstack/tests/unit/identity/v3/test_user.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.identity.v3 import user from openstack.tests.unit import base -from openstack.identity.v3 import user IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/tox.ini b/tox.ini index 8f1818fa6..a202d28d9 100644 --- a/tox.ini +++ b/tox.ini @@ -126,9 +126,6 @@ per-file-ignores = openstack/tests/unit/cloud/*:H306,I100,I201,I202 openstack/tests/unit/clustering/*:H306,I100,I201,I202 openstack/tests/unit/orchestration/*:H306,I100,I201,I202 - openstack/tests/unit/identity/*:H306,I100,I201,I202 - openstack/tests/unit/accelerator/*:H306,I100,I201,I202 - openstack/tests/unit/config/*:H306,I100,I201,I202 [flake8:local-plugins] extension = From adcf98c21e91751c08ce4a0aadf12b6071c02944 Mon Sep 17 00:00:00 2001 From: Riccardo Pittau Date: Tue, 1 Jun 2021 11:24:43 +0200 Subject: [PATCH 2880/3836] Fix import order in orchestration tests Remove tox filter Change-Id: I397c715192ac4e47cfe867df22323d30581ba1aa --- openstack/tests/unit/orchestration/test_version.py | 2 +- openstack/tests/unit/orchestration/v1/test_proxy.py | 3 ++- openstack/tests/unit/orchestration/v1/test_resource.py | 3 +-- openstack/tests/unit/orchestration/v1/test_software_config.py | 3 +-- .../tests/unit/orchestration/v1/test_software_deployment.py | 2 +- .../tests/unit/orchestration/v1/test_stack_environment.py | 3 +-- openstack/tests/unit/orchestration/v1/test_stack_template.py | 2 +- tox.ini | 1 - 8 files changed, 8 insertions(+), 11 deletions(-) diff --git a/openstack/tests/unit/orchestration/test_version.py b/openstack/tests/unit/orchestration/test_version.py index 3e59a4994..20fceb315 100644 --- a/openstack/tests/unit/orchestration/test_version.py +++ b/openstack/tests/unit/orchestration/test_version.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.orchestration import version from openstack.tests.unit import base -from openstack.orchestration import version IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index 8c4943e87..94255a6bc 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -9,10 +9,11 @@ # 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 testscenarios import load_tests_apply_scenarios as load_tests # noqa from unittest import mock +from testscenarios import load_tests_apply_scenarios as load_tests # noqa + from openstack import exceptions from openstack.orchestration.v1 import _proxy from openstack.orchestration.v1 import resource diff --git a/openstack/tests/unit/orchestration/v1/test_resource.py b/openstack/tests/unit/orchestration/v1/test_resource.py index ac3e3253c..2e3708a15 100644 --- a/openstack/tests/unit/orchestration/v1/test_resource.py +++ b/openstack/tests/unit/orchestration/v1/test_resource.py @@ -10,9 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base - from openstack.orchestration.v1 import resource +from openstack.tests.unit import base FAKE_ID = '32e39358-2422-4ad0-a1b5-dd60696bf564' diff --git a/openstack/tests/unit/orchestration/v1/test_software_config.py b/openstack/tests/unit/orchestration/v1/test_software_config.py index de47a090a..439b2e100 100644 --- a/openstack/tests/unit/orchestration/v1/test_software_config.py +++ b/openstack/tests/unit/orchestration/v1/test_software_config.py @@ -10,9 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base - from openstack.orchestration.v1 import software_config +from openstack.tests.unit import base FAKE_ID = 'ce8ae86c-9810-4cb1-8888-7fb53bc523bf' diff --git a/openstack/tests/unit/orchestration/v1/test_software_deployment.py b/openstack/tests/unit/orchestration/v1/test_software_deployment.py index 3dad1a61b..7c9640e79 100644 --- a/openstack/tests/unit/orchestration/v1/test_software_deployment.py +++ b/openstack/tests/unit/orchestration/v1/test_software_deployment.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.orchestration.v1 import software_deployment from openstack.tests.unit import base -from openstack.orchestration.v1 import software_deployment FAKE = { 'id': 'ce8ae86c-9810-4cb1-8888-7fb53bc523bf', diff --git a/openstack/tests/unit/orchestration/v1/test_stack_environment.py b/openstack/tests/unit/orchestration/v1/test_stack_environment.py index 08737dc7e..03c39f3fc 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack_environment.py +++ b/openstack/tests/unit/orchestration/v1/test_stack_environment.py @@ -10,9 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base - from openstack.orchestration.v1 import stack_environment as se +from openstack.tests.unit import base FAKE = { diff --git a/openstack/tests/unit/orchestration/v1/test_stack_template.py b/openstack/tests/unit/orchestration/v1/test_stack_template.py index 1de5120af..60ed9d1a5 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack_template.py +++ b/openstack/tests/unit/orchestration/v1/test_stack_template.py @@ -11,9 +11,9 @@ # under the License. import copy -from openstack.tests.unit import base from openstack.orchestration.v1 import stack_template +from openstack.tests.unit import base FAKE = { diff --git a/tox.ini b/tox.ini index a202d28d9..e585a015b 100644 --- a/tox.ini +++ b/tox.ini @@ -125,7 +125,6 @@ per-file-ignores = openstack/tests/unit/block_storage/*:H306,I100,I201,I202 openstack/tests/unit/cloud/*:H306,I100,I201,I202 openstack/tests/unit/clustering/*:H306,I100,I201,I202 - openstack/tests/unit/orchestration/*:H306,I100,I201,I202 [flake8:local-plugins] extension = From b59cda5cfd78b9fb00aa88686f977a0f27db8f31 Mon Sep 17 00:00:00 2001 From: Riccardo Pittau Date: Tue, 1 Jun 2021 11:29:48 +0200 Subject: [PATCH 2881/3836] Fix import order in clustering tests Remove tox filter Change-Id: Ife111995d939f8373637b1668be2f955b58c6189 --- openstack/tests/unit/clustering/test_version.py | 2 +- openstack/tests/unit/clustering/v1/test_action.py | 3 +-- openstack/tests/unit/clustering/v1/test_build_info.py | 3 +-- openstack/tests/unit/clustering/v1/test_cluster_attr.py | 3 +-- openstack/tests/unit/clustering/v1/test_cluster_policy.py | 3 +-- openstack/tests/unit/clustering/v1/test_event.py | 3 +-- openstack/tests/unit/clustering/v1/test_policy.py | 3 +-- openstack/tests/unit/clustering/v1/test_policy_type.py | 3 +-- openstack/tests/unit/clustering/v1/test_profile.py | 3 +-- openstack/tests/unit/clustering/v1/test_receiver.py | 3 +-- tox.ini | 1 - 11 files changed, 10 insertions(+), 20 deletions(-) diff --git a/openstack/tests/unit/clustering/test_version.py b/openstack/tests/unit/clustering/test_version.py index 461a8f4e9..4908e52f4 100644 --- a/openstack/tests/unit/clustering/test_version.py +++ b/openstack/tests/unit/clustering/test_version.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.clustering import version from openstack.tests.unit import base -from openstack.clustering import version IDENTIFIER = 'IDENTIFIER' EXAMPLE = { diff --git a/openstack/tests/unit/clustering/v1/test_action.py b/openstack/tests/unit/clustering/v1/test_action.py index 62e13188d..539967cf7 100644 --- a/openstack/tests/unit/clustering/v1/test_action.py +++ b/openstack/tests/unit/clustering/v1/test_action.py @@ -10,9 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base - from openstack.clustering.v1 import action +from openstack.tests.unit import base FAKE_CLUSTER_ID = 'ffaed25e-46f5-4089-8e20-b3b4722fd597' diff --git a/openstack/tests/unit/clustering/v1/test_build_info.py b/openstack/tests/unit/clustering/v1/test_build_info.py index 0056691d6..e94aa2d48 100644 --- a/openstack/tests/unit/clustering/v1/test_build_info.py +++ b/openstack/tests/unit/clustering/v1/test_build_info.py @@ -10,9 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base - from openstack.clustering.v1 import build_info +from openstack.tests.unit import base FAKE = { diff --git a/openstack/tests/unit/clustering/v1/test_cluster_attr.py b/openstack/tests/unit/clustering/v1/test_cluster_attr.py index 3ef9641ce..9c88946b9 100644 --- a/openstack/tests/unit/clustering/v1/test_cluster_attr.py +++ b/openstack/tests/unit/clustering/v1/test_cluster_attr.py @@ -10,9 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base - from openstack.clustering.v1 import cluster_attr as ca +from openstack.tests.unit import base FAKE = { diff --git a/openstack/tests/unit/clustering/v1/test_cluster_policy.py b/openstack/tests/unit/clustering/v1/test_cluster_policy.py index 463fa642e..21d4bf84b 100644 --- a/openstack/tests/unit/clustering/v1/test_cluster_policy.py +++ b/openstack/tests/unit/clustering/v1/test_cluster_policy.py @@ -10,9 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base - from openstack.clustering.v1 import cluster_policy +from openstack.tests.unit import base FAKE = { diff --git a/openstack/tests/unit/clustering/v1/test_event.py b/openstack/tests/unit/clustering/v1/test_event.py index 9a972580f..01e489668 100644 --- a/openstack/tests/unit/clustering/v1/test_event.py +++ b/openstack/tests/unit/clustering/v1/test_event.py @@ -10,9 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base - from openstack.clustering.v1 import event +from openstack.tests.unit import base FAKE = { diff --git a/openstack/tests/unit/clustering/v1/test_policy.py b/openstack/tests/unit/clustering/v1/test_policy.py index 6af6bd6b6..89b72ac02 100644 --- a/openstack/tests/unit/clustering/v1/test_policy.py +++ b/openstack/tests/unit/clustering/v1/test_policy.py @@ -10,9 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base - from openstack.clustering.v1 import policy +from openstack.tests.unit import base FAKE_ID = 'ac5415bd-f522-4160-8be0-f8853e4bc332' diff --git a/openstack/tests/unit/clustering/v1/test_policy_type.py b/openstack/tests/unit/clustering/v1/test_policy_type.py index 63f88fdec..045366811 100644 --- a/openstack/tests/unit/clustering/v1/test_policy_type.py +++ b/openstack/tests/unit/clustering/v1/test_policy_type.py @@ -10,9 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base - from openstack.clustering.v1 import policy_type +from openstack.tests.unit import base FAKE = { diff --git a/openstack/tests/unit/clustering/v1/test_profile.py b/openstack/tests/unit/clustering/v1/test_profile.py index abcaf098d..d37d17853 100644 --- a/openstack/tests/unit/clustering/v1/test_profile.py +++ b/openstack/tests/unit/clustering/v1/test_profile.py @@ -10,9 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base - from openstack.clustering.v1 import profile +from openstack.tests.unit import base FAKE_ID = '9b127538-a675-4271-ab9b-f24f54cfe173' diff --git a/openstack/tests/unit/clustering/v1/test_receiver.py b/openstack/tests/unit/clustering/v1/test_receiver.py index 197cfb01e..5a46d6b1d 100644 --- a/openstack/tests/unit/clustering/v1/test_receiver.py +++ b/openstack/tests/unit/clustering/v1/test_receiver.py @@ -10,9 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base - from openstack.clustering.v1 import receiver +from openstack.tests.unit import base FAKE_ID = 'ae63a10b-4a90-452c-aef1-113a0b255ee3' diff --git a/tox.ini b/tox.ini index e585a015b..799cc3e56 100644 --- a/tox.ini +++ b/tox.ini @@ -124,7 +124,6 @@ exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,openstack/_services_mix per-file-ignores = openstack/tests/unit/block_storage/*:H306,I100,I201,I202 openstack/tests/unit/cloud/*:H306,I100,I201,I202 - openstack/tests/unit/clustering/*:H306,I100,I201,I202 [flake8:local-plugins] extension = From c6494db0ac46a20bd54e8162b4c6969057f8e5a9 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 17 May 2021 17:36:08 +0200 Subject: [PATCH 2882/3836] Move tag mixin into the common As a continuation of a further generalization of methods add a common module and move tag there. Change-Id: Id23145e427221e7bd80696b9faec2d4c8d76b477 --- openstack/common/__init__.py | 0 openstack/common/tag.py | 127 +++++++++++++ openstack/compute/v2/server.py | 6 +- openstack/identity/v3/project.py | 6 +- openstack/image/v2/image.py | 3 +- openstack/load_balancer/v2/health_monitor.py | 6 +- openstack/load_balancer/v2/l7_policy.py | 6 +- openstack/load_balancer/v2/l7_rule.py | 6 +- openstack/load_balancer/v2/listener.py | 6 +- openstack/load_balancer/v2/load_balancer.py | 6 +- openstack/load_balancer/v2/member.py | 6 +- openstack/load_balancer/v2/pool.py | 6 +- openstack/network/v2/floating_ip.py | 6 +- openstack/network/v2/network.py | 6 +- openstack/network/v2/port.py | 6 +- openstack/network/v2/qos_policy.py | 6 +- openstack/network/v2/router.py | 6 +- openstack/network/v2/security_group.py | 6 +- openstack/network/v2/security_group_rule.py | 7 +- openstack/network/v2/subnet.py | 6 +- openstack/network/v2/subnet_pool.py | 6 +- openstack/network/v2/trunk.py | 6 +- openstack/orchestration/v1/stack.py | 4 +- openstack/resource.py | 113 ------------ openstack/tests/unit/common/__init__.py | 0 openstack/tests/unit/common/test_tag.py | 183 +++++++++++++++++++ openstack/tests/unit/test_resource.py | 163 ----------------- 27 files changed, 371 insertions(+), 337 deletions(-) create mode 100644 openstack/common/__init__.py create mode 100644 openstack/common/tag.py create mode 100644 openstack/tests/unit/common/__init__.py create mode 100644 openstack/tests/unit/common/test_tag.py diff --git a/openstack/common/__init__.py b/openstack/common/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/common/tag.py b/openstack/common/tag.py new file mode 100644 index 000000000..8cd7ea6bb --- /dev/null +++ b/openstack/common/tag.py @@ -0,0 +1,127 @@ +# 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 import resource +from openstack import utils + + +class TagMixin: + + _tag_query_parameters = { + 'tags': 'tags', + 'any_tags': 'tags-any', + 'not_tags': 'not-tags', + 'not_any_tags': 'not-tags-any', + } + + #: A list of associated tags + #: *Type: list of tag strings* + tags = resource.Body('tags', type=list, default=[]) + + def fetch_tags(self, session): + """Lists tags set on the entity. + + :param session: The session to use for making this request. + :return: The list with tags attached to the entity + """ + url = utils.urljoin(self.base_path, self.id, 'tags') + session = self._get_session(session) + response = session.get(url) + exceptions.raise_from_response(response) + # NOTE(gtema): since this is a common method + # we can't rely on the resource_key, because tags are returned + # without resource_key. Do parse response here + json = response.json() + if 'tags' in json: + self._body.attributes.update({'tags': json['tags']}) + return self + + def set_tags(self, session, tags=[]): + """Sets/Replaces all tags on the resource. + + :param session: The session to use for making this request. + :param list tags: List with tags to be set on the resource + """ + url = utils.urljoin(self.base_path, self.id, 'tags') + session = self._get_session(session) + response = session.put(url, json={'tags': tags}) + exceptions.raise_from_response(response) + self._body.attributes.update({'tags': tags}) + return self + + def remove_all_tags(self, session): + """Removes all tags on the entity. + + :param session: The session to use for making this request. + """ + url = utils.urljoin(self.base_path, self.id, 'tags') + session = self._get_session(session) + response = session.delete(url) + exceptions.raise_from_response(response) + self._body.attributes.update({'tags': []}) + return self + + def check_tag(self, session, tag): + """Checks if tag exists on the entity. + + If the tag does not exist a 404 will be returned + + :param session: The session to use for making this request. + :param tag: The tag as a string. + """ + url = utils.urljoin(self.base_path, self.id, 'tags', tag) + session = self._get_session(session) + response = session.get(url) + exceptions.raise_from_response(response, + error_message='Tag does not exist') + return self + + def add_tag(self, session, tag): + """Adds a single tag to the resource. + + :param session: The session to use for making this request. + :param tag: The tag as a string. + """ + url = utils.urljoin(self.base_path, self.id, 'tags', tag) + session = self._get_session(session) + response = session.put(url) + exceptions.raise_from_response(response) + # we do not want to update tags directly + tags = self.tags + tags.append(tag) + self._body.attributes.update({ + 'tags': tags + }) + return self + + def remove_tag(self, session, tag): + """Removes a single tag from the specified server. + + :param session: The session to use for making this request. + :param tag: The tag as a string. + """ + url = utils.urljoin(self.base_path, self.id, 'tags', tag) + session = self._get_session(session) + response = session.delete(url) + exceptions.raise_from_response(response) + # we do not want to update tags directly + tags = self.tags + try: + # NOTE(gtema): if tags were not fetched, but request suceeded + # it is ok. Just ensure tag does not exist locally + tags.remove(tag) + except ValueError: + pass # do nothing! + self._body.attributes.update({ + 'tags': tags + }) + return self diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index fd5a35b0c..c0eab2c59 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -9,7 +9,7 @@ # 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.common import tag from openstack.compute.v2 import metadata from openstack import exceptions from openstack.image.v2 import image @@ -26,7 +26,7 @@ } -class Server(resource.Resource, metadata.MetadataMixin, resource.TagMixin): +class Server(resource.Resource, metadata.MetadataMixin, tag.TagMixin): resource_key = 'server' resources_key = 'servers' base_path = '/servers' @@ -60,7 +60,7 @@ class Server(resource.Resource, metadata.MetadataMixin, resource.TagMixin): changes_before="changes-before", id="uuid", all_projects="all_tenants", - **resource.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters ) _max_microversion = '2.72' diff --git a/openstack/identity/v3/project.py b/openstack/identity/v3/project.py index 577bdcd02..66f9a3ffb 100644 --- a/openstack/identity/v3/project.py +++ b/openstack/identity/v3/project.py @@ -9,12 +9,12 @@ # 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.common import tag from openstack import resource from openstack import utils -class Project(resource.Resource, resource.TagMixin): +class Project(resource.Resource, tag.TagMixin): resource_key = 'project' resources_key = 'projects' base_path = '/projects' @@ -33,7 +33,7 @@ class Project(resource.Resource, resource.TagMixin): 'name', 'parent_id', is_enabled='enabled', - **resource.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters ) # Properties diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index bbf700a31..9509f6f71 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -9,13 +9,14 @@ # 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.common import tag from openstack import exceptions from openstack.image import _download from openstack import resource from openstack import utils -class Image(resource.Resource, resource.TagMixin, _download.DownloadMixin): +class Image(resource.Resource, tag.TagMixin, _download.DownloadMixin): resources_key = 'images' base_path = '/images' diff --git a/openstack/load_balancer/v2/health_monitor.py b/openstack/load_balancer/v2/health_monitor.py index 01f5fbee1..58b5b969f 100644 --- a/openstack/load_balancer/v2/health_monitor.py +++ b/openstack/load_balancer/v2/health_monitor.py @@ -9,11 +9,11 @@ # 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.common import tag from openstack import resource -class HealthMonitor(resource.Resource, resource.TagMixin): +class HealthMonitor(resource.Resource, tag.TagMixin): resource_key = 'healthmonitor' resources_key = 'healthmonitors' base_path = '/lbaas/healthmonitors' @@ -30,7 +30,7 @@ class HealthMonitor(resource.Resource, resource.TagMixin): 'http_method', 'max_retries', 'max_retries_down', 'pool_id', 'provisioning_status', 'operating_status', 'timeout', 'project_id', 'type', 'url_path', is_admin_state_up='admin_state_up', - **resource.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters ) #: Properties diff --git a/openstack/load_balancer/v2/l7_policy.py b/openstack/load_balancer/v2/l7_policy.py index 4b099dc22..3587db9b1 100644 --- a/openstack/load_balancer/v2/l7_policy.py +++ b/openstack/load_balancer/v2/l7_policy.py @@ -9,11 +9,11 @@ # 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.common import tag from openstack import resource -class L7Policy(resource.Resource, resource.TagMixin): +class L7Policy(resource.Resource, tag.TagMixin): resource_key = 'l7policy' resources_key = 'l7policies' base_path = '/lbaas/l7policies' @@ -30,7 +30,7 @@ class L7Policy(resource.Resource, resource.TagMixin): 'redirect_pool_id', 'redirect_url', 'provisioning_status', 'operating_status', 'redirect_prefix', 'project_id', is_admin_state_up='admin_state_up', - **resource.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters ) #: Properties diff --git a/openstack/load_balancer/v2/l7_rule.py b/openstack/load_balancer/v2/l7_rule.py index ee8400e75..c2585b7e6 100644 --- a/openstack/load_balancer/v2/l7_rule.py +++ b/openstack/load_balancer/v2/l7_rule.py @@ -9,11 +9,11 @@ # 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.common import tag from openstack import resource -class L7Rule(resource.Resource, resource.TagMixin): +class L7Rule(resource.Resource, tag.TagMixin): resource_key = 'rule' resources_key = 'rules' base_path = '/lbaas/l7policies/%(l7policy_id)s/rules' @@ -29,7 +29,7 @@ class L7Rule(resource.Resource, resource.TagMixin): 'compare_type', 'created_at', 'invert', 'key', 'project_id', 'provisioning_status', 'type', 'updated_at', 'rule_value', 'operating_status', is_admin_state_up='admin_state_up', - l7_policy_id='l7policy_id', **resource.TagMixin._tag_query_parameters + l7_policy_id='l7policy_id', **tag.TagMixin._tag_query_parameters ) #: Properties diff --git a/openstack/load_balancer/v2/listener.py b/openstack/load_balancer/v2/listener.py index c59e28c92..2dcded468 100644 --- a/openstack/load_balancer/v2/listener.py +++ b/openstack/load_balancer/v2/listener.py @@ -9,11 +9,11 @@ # 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.common import tag from openstack import resource -class Listener(resource.Resource, resource.TagMixin): +class Listener(resource.Resource, tag.TagMixin): resource_key = 'listener' resources_key = 'listeners' base_path = '/lbaas/listeners' @@ -34,7 +34,7 @@ class Listener(resource.Resource, resource.TagMixin): 'timeout_member_data', 'timeout_tcp_inspect', 'allowed_cidrs', 'tls_ciphers', 'tls_versions', 'alpn_protocols', is_admin_state_up='admin_state_up', - **resource.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters ) # Properties diff --git a/openstack/load_balancer/v2/load_balancer.py b/openstack/load_balancer/v2/load_balancer.py index 5614fdc72..422d4ad85 100644 --- a/openstack/load_balancer/v2/load_balancer.py +++ b/openstack/load_balancer/v2/load_balancer.py @@ -9,11 +9,11 @@ # 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.common import tag from openstack import resource -class LoadBalancer(resource.Resource, resource.TagMixin): +class LoadBalancer(resource.Resource, tag.TagMixin): resource_key = 'loadbalancer' resources_key = 'loadbalancers' base_path = '/lbaas/loadbalancers' @@ -30,7 +30,7 @@ class LoadBalancer(resource.Resource, resource.TagMixin): 'vip_address', 'vip_network_id', 'vip_port_id', 'vip_subnet_id', 'vip_qos_policy_id', 'provisioning_status', 'operating_status', 'availability_zone', is_admin_state_up='admin_state_up', - **resource.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters ) # Properties diff --git a/openstack/load_balancer/v2/member.py b/openstack/load_balancer/v2/member.py index 3f4201be7..32ca43bda 100644 --- a/openstack/load_balancer/v2/member.py +++ b/openstack/load_balancer/v2/member.py @@ -9,11 +9,11 @@ # 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.common import tag from openstack import resource -class Member(resource.Resource, resource.TagMixin): +class Member(resource.Resource, tag.TagMixin): resource_key = 'member' resources_key = 'members' base_path = '/lbaas/pools/%(pool_id)s/members' @@ -30,7 +30,7 @@ class Member(resource.Resource, resource.TagMixin): 'created_at', 'updated_at', 'provisioning_status', 'operating_status', 'project_id', 'monitor_address', 'monitor_port', 'backup', is_admin_state_up='admin_state_up', - **resource.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters ) # Properties diff --git a/openstack/load_balancer/v2/pool.py b/openstack/load_balancer/v2/pool.py index 2e42b0fca..9af3d18a9 100644 --- a/openstack/load_balancer/v2/pool.py +++ b/openstack/load_balancer/v2/pool.py @@ -9,11 +9,11 @@ # 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.common import tag from openstack import resource -class Pool(resource.Resource, resource.TagMixin): +class Pool(resource.Resource, tag.TagMixin): resource_key = 'pool' resources_key = 'pools' base_path = '/lbaas/pools' @@ -31,7 +31,7 @@ class Pool(resource.Resource, resource.TagMixin): 'created_at', 'updated_at', 'provisioning_status', 'operating_status', 'tls_enabled', 'tls_ciphers', 'tls_versions', 'alpn_protocols', is_admin_state_up='admin_state_up', - **resource.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters ) #: Properties diff --git a/openstack/network/v2/floating_ip.py b/openstack/network/v2/floating_ip.py index fec267033..76383b2f6 100644 --- a/openstack/network/v2/floating_ip.py +++ b/openstack/network/v2/floating_ip.py @@ -9,12 +9,12 @@ # 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.common import tag from openstack.network.v2 import _base from openstack import resource -class FloatingIP(_base.NetworkResource, resource.TagMixin): +class FloatingIP(_base.NetworkResource, tag.TagMixin): name_attribute = "floating_ip_address" resource_name = "floating ip" resource_key = 'floatingip' @@ -35,7 +35,7 @@ class FloatingIP(_base.NetworkResource, resource.TagMixin): 'port_id', 'router_id', 'status', 'subnet_id', 'project_id', 'tenant_id', tenant_id='project_id', - **resource.TagMixin._tag_query_parameters) + **tag.TagMixin._tag_query_parameters) # Properties #: Timestamp at which the floating IP was created. diff --git a/openstack/network/v2/network.py b/openstack/network/v2/network.py index 730dbdd3a..074bd39c1 100644 --- a/openstack/network/v2/network.py +++ b/openstack/network/v2/network.py @@ -9,12 +9,12 @@ # 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.common import tag from openstack.network.v2 import _base from openstack import resource -class Network(_base.NetworkResource, resource.TagMixin): +class Network(_base.NetworkResource, tag.TagMixin): resource_key = 'network' resources_key = 'networks' base_path = '/networks' @@ -39,7 +39,7 @@ class Network(_base.NetworkResource, resource.TagMixin): provider_network_type='provider:network_type', provider_physical_network='provider:physical_network', provider_segmentation_id='provider:segmentation_id', - **resource.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters ) # Properties diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index 4a4ed8b5f..cdcec8b4b 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -9,12 +9,12 @@ # 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.common import tag from openstack.network.v2 import _base from openstack import resource -class Port(_base.NetworkResource, resource.TagMixin): +class Port(_base.NetworkResource, tag.TagMixin): resource_key = 'port' resources_key = 'ports' base_path = '/ports' @@ -35,7 +35,7 @@ class Port(_base.NetworkResource, resource.TagMixin): 'subnet_id', 'project_id', is_admin_state_up='admin_state_up', is_port_security_enabled='port_security_enabled', - **resource.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters ) # Properties diff --git a/openstack/network/v2/qos_policy.py b/openstack/network/v2/qos_policy.py index 00e1414e8..ec5e98918 100644 --- a/openstack/network/v2/qos_policy.py +++ b/openstack/network/v2/qos_policy.py @@ -9,12 +9,12 @@ # 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.common import tag from openstack import resource from openstack import utils -class QoSPolicy(resource.Resource, resource.TagMixin): +class QoSPolicy(resource.Resource, tag.TagMixin): resource_key = 'policy' resources_key = 'policies' base_path = '/qos/policies' @@ -32,7 +32,7 @@ class QoSPolicy(resource.Resource, resource.TagMixin): 'name', 'description', 'is_default', 'project_id', is_shared='shared', - **resource.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters ) # Properties diff --git a/openstack/network/v2/router.py b/openstack/network/v2/router.py index 30de0d6ee..3d511cba1 100644 --- a/openstack/network/v2/router.py +++ b/openstack/network/v2/router.py @@ -9,14 +9,14 @@ # 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.common import tag from openstack import exceptions from openstack.network.v2 import _base from openstack import resource from openstack import utils -class Router(_base.NetworkResource, resource.TagMixin): +class Router(_base.NetworkResource, tag.TagMixin): resource_key = 'router' resources_key = 'routers' base_path = '/routers' @@ -34,7 +34,7 @@ class Router(_base.NetworkResource, resource.TagMixin): is_admin_state_up='admin_state_up', is_distributed='distributed', is_ha='ha', - **resource.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters ) # Properties diff --git a/openstack/network/v2/security_group.py b/openstack/network/v2/security_group.py index 7499b28c4..cf2d23534 100644 --- a/openstack/network/v2/security_group.py +++ b/openstack/network/v2/security_group.py @@ -9,12 +9,12 @@ # 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.common import tag from openstack.network.v2 import _base from openstack import resource -class SecurityGroup(_base.NetworkResource, resource.TagMixin): +class SecurityGroup(_base.NetworkResource, tag.TagMixin): resource_key = 'security_group' resources_key = 'security_groups' base_path = '/security-groups' @@ -29,7 +29,7 @@ class SecurityGroup(_base.NetworkResource, resource.TagMixin): _query_mapping = resource.QueryParameters( 'description', 'fields', 'id', 'name', 'stateful', 'project_id', 'tenant_id', 'revision_number', 'sort_dir', 'sort_key', - **resource.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters ) # Properties diff --git a/openstack/network/v2/security_group_rule.py b/openstack/network/v2/security_group_rule.py index d3fc0f816..9b3069223 100644 --- a/openstack/network/v2/security_group_rule.py +++ b/openstack/network/v2/security_group_rule.py @@ -9,12 +9,12 @@ # 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.common import tag from openstack.network.v2 import _base from openstack import resource -class SecurityGroupRule(_base.NetworkResource, resource.TagMixin): +class SecurityGroupRule(_base.NetworkResource, tag.TagMixin): resource_key = 'security_group_rule' resources_key = 'security_group_rules' base_path = '/security-group-rules' @@ -35,8 +35,7 @@ class SecurityGroupRule(_base.NetworkResource, resource.TagMixin): 'project_id', 'tenant_id', 'sort_dir', 'sort_key', ether_type='ethertype', - **resource.TagMixin._tag_query_parameters - + **tag.TagMixin._tag_query_parameters ) # Properties diff --git a/openstack/network/v2/subnet.py b/openstack/network/v2/subnet.py index 65f07f42e..d219e1ec8 100644 --- a/openstack/network/v2/subnet.py +++ b/openstack/network/v2/subnet.py @@ -9,12 +9,12 @@ # 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.common import tag from openstack.network.v2 import _base from openstack import resource -class Subnet(_base.NetworkResource, resource.TagMixin): +class Subnet(_base.NetworkResource, tag.TagMixin): resource_key = 'subnet' resources_key = 'subnets' base_path = '/subnets' @@ -34,7 +34,7 @@ class Subnet(_base.NetworkResource, resource.TagMixin): is_dhcp_enabled='enable_dhcp', subnet_pool_id='subnetpool_id', use_default_subnet_pool='use_default_subnetpool', - **resource.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters ) # Properties diff --git a/openstack/network/v2/subnet_pool.py b/openstack/network/v2/subnet_pool.py index 4eb05b616..99a24da32 100644 --- a/openstack/network/v2/subnet_pool.py +++ b/openstack/network/v2/subnet_pool.py @@ -9,11 +9,11 @@ # 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.common import tag from openstack import resource -class SubnetPool(resource.Resource, resource.TagMixin): +class SubnetPool(resource.Resource, tag.TagMixin): resource_key = 'subnetpool' resources_key = 'subnetpools' base_path = '/subnetpools' @@ -31,7 +31,7 @@ class SubnetPool(resource.Resource, resource.TagMixin): 'address_scope_id', 'description', 'ip_version', 'is_default', 'name', 'project_id', is_shared='shared', - **resource.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters ) # Properties diff --git a/openstack/network/v2/trunk.py b/openstack/network/v2/trunk.py index 47d2f5fac..82f1bede8 100644 --- a/openstack/network/v2/trunk.py +++ b/openstack/network/v2/trunk.py @@ -9,13 +9,13 @@ # 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.common import tag from openstack import exceptions from openstack import resource from openstack import utils -class Trunk(resource.Resource, resource.TagMixin): +class Trunk(resource.Resource, tag.TagMixin): resource_key = 'trunk' resources_key = 'trunks' base_path = '/trunks' @@ -33,7 +33,7 @@ class Trunk(resource.Resource, resource.TagMixin): 'name', 'description', 'port_id', 'status', 'sub_ports', 'project_id', is_admin_state_up='admin_state_up', - **resource.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters ) # Properties diff --git a/openstack/orchestration/v1/stack.py b/openstack/orchestration/v1/stack.py index 1bfc1a7dd..ca7c4dc9f 100644 --- a/openstack/orchestration/v1/stack.py +++ b/openstack/orchestration/v1/stack.py @@ -9,7 +9,7 @@ # 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.common import tag from openstack import exceptions from openstack import resource from openstack import utils @@ -32,7 +32,7 @@ class Stack(resource.Resource): 'action', 'name', 'status', 'project_id', 'owner_id', 'username', project_id='tenant_id', - **resource.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters ) # Properties diff --git a/openstack/resource.py b/openstack/resource.py index 573931607..dee4ec577 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1980,119 +1980,6 @@ def find( "No %s found for %s" % (cls.__name__, name_or_id)) -class TagMixin: - - _tag_query_parameters = { - 'tags': 'tags', - 'any_tags': 'tags-any', - 'not_tags': 'not-tags', - 'not_any_tags': 'not-tags-any', - } - - #: A list of associated tags - #: *Type: list of tag strings* - tags = Body('tags', type=list, default=[]) - - def fetch_tags(self, session): - """Lists tags set on the entity. - - :param session: The session to use for making this request. - :return: The list with tags attached to the entity - """ - url = utils.urljoin(self.base_path, self.id, 'tags') - session = self._get_session(session) - response = session.get(url) - exceptions.raise_from_response(response) - # NOTE(gtema): since this is a common method - # we can't rely on the resource_key, because tags are returned - # without resource_key. Do parse response here - json = response.json() - if 'tags' in json: - self._body.attributes.update({'tags': json['tags']}) - return self - - def set_tags(self, session, tags=[]): - """Sets/Replaces all tags on the resource. - - :param session: The session to use for making this request. - :param list tags: List with tags to be set on the resource - """ - url = utils.urljoin(self.base_path, self.id, 'tags') - session = self._get_session(session) - response = session.put(url, json={'tags': tags}) - exceptions.raise_from_response(response) - self._body.attributes.update({'tags': tags}) - return self - - def remove_all_tags(self, session): - """Removes all tags on the entity. - - :param session: The session to use for making this request. - """ - url = utils.urljoin(self.base_path, self.id, 'tags') - session = self._get_session(session) - response = session.delete(url) - exceptions.raise_from_response(response) - self._body.attributes.update({'tags': []}) - return self - - def check_tag(self, session, tag): - """Checks if tag exists on the entity. - - If the tag does not exist a 404 will be returned - - :param session: The session to use for making this request. - :param tag: The tag as a string. - """ - url = utils.urljoin(self.base_path, self.id, 'tags', tag) - session = self._get_session(session) - response = session.get(url) - exceptions.raise_from_response(response, - error_message='Tag does not exist') - return self - - def add_tag(self, session, tag): - """Adds a single tag to the resource. - - :param session: The session to use for making this request. - :param tag: The tag as a string. - """ - url = utils.urljoin(self.base_path, self.id, 'tags', tag) - session = self._get_session(session) - response = session.put(url) - exceptions.raise_from_response(response) - # we do not want to update tags directly - tags = self.tags - tags.append(tag) - self._body.attributes.update({ - 'tags': tags - }) - return self - - def remove_tag(self, session, tag): - """Removes a single tag from the specified server. - - :param session: The session to use for making this request. - :param tag: The tag as a string. - """ - url = utils.urljoin(self.base_path, self.id, 'tags', tag) - session = self._get_session(session) - response = session.delete(url) - exceptions.raise_from_response(response) - # we do not want to update tags directly - tags = self.tags - try: - # NOTE(gtema): if tags were not fetched, but request suceeded - # it is ok. Just ensure tag does not exist locally - tags.remove(tag) - except ValueError: - pass # do nothing! - self._body.attributes.update({ - 'tags': tags - }) - return self - - def _normalize_status(status): if status is not None: status = status.lower() diff --git a/openstack/tests/unit/common/__init__.py b/openstack/tests/unit/common/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/unit/common/test_tag.py b/openstack/tests/unit/common/test_tag.py new file mode 100644 index 000000000..d27ffdca4 --- /dev/null +++ b/openstack/tests/unit/common/test_tag.py @@ -0,0 +1,183 @@ +# 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 unittest import mock + +from keystoneauth1 import adapter + +from openstack.common import tag +from openstack import exceptions +from openstack import resource +from openstack.tests.unit import base +from openstack.tests.unit.test_resource import FakeResponse + + +class TestTagMixin(base.TestCase): + + def setUp(self): + super(TestTagMixin, self).setUp() + + self.service_name = "service" + self.base_path = "base_path" + + class Test(resource.Resource, tag.TagMixin): + service = self.service_name + base_path = self.base_path + resources_key = 'resources' + allow_create = True + allow_fetch = True + allow_head = True + allow_commit = True + allow_delete = True + allow_list = True + + self.test_class = Test + + self.request = mock.Mock(spec=resource._Request) + self.request.url = "uri" + self.request.body = "body" + self.request.headers = "headers" + + self.response = FakeResponse({}) + + self.sot = Test.new(id="id", tags=[]) + self.sot._prepare_request = mock.Mock(return_value=self.request) + self.sot._translate_response = mock.Mock() + + self.session = mock.Mock(spec=adapter.Adapter) + self.session.get = mock.Mock(return_value=self.response) + self.session.put = mock.Mock(return_value=self.response) + self.session.delete = mock.Mock(return_value=self.response) + + def test_tags_attribute(self): + res = self.sot + self.assertTrue(hasattr(res, 'tags')) + self.assertIsInstance(res.tags, list) + + def test_fetch_tags(self): + res = self.sot + sess = self.session + + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.links = {} + mock_response.json.return_value = {'tags': ['blue1', 'green1']} + + sess.get.side_effect = [mock_response] + + result = res.fetch_tags(sess) + # Check tags attribute is updated + self.assertEqual(['blue1', 'green1'], res.tags) + # Check the passed resource is returned + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/tags' + sess.get.assert_called_once_with(url) + + def test_set_tags(self): + res = self.sot + sess = self.session + + # Set some initial value to check rewrite + res.tags.extend(['blue_old', 'green_old']) + + result = res.set_tags(sess, ['blue', 'green']) + # Check tags attribute is updated + self.assertEqual(['blue', 'green'], res.tags) + # Check the passed resource is returned + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/tags' + sess.put.assert_called_once_with( + url, + json={'tags': ['blue', 'green']} + ) + + def test_remove_all_tags(self): + res = self.sot + sess = self.session + + # Set some initial value to check removal + res.tags.extend(['blue_old', 'green_old']) + + result = res.remove_all_tags(sess) + # Check tags attribute is updated + self.assertEqual([], res.tags) + # Check the passed resource is returned + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/tags' + sess.delete.assert_called_once_with(url) + + def test_remove_single_tag(self): + res = self.sot + sess = self.session + + res.tags.extend(['blue', 'dummy']) + + result = res.remove_tag(sess, 'dummy') + # Check tags attribute is updated + self.assertEqual(['blue'], res.tags) + # Check the passed resource is returned + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/tags/dummy' + sess.delete.assert_called_once_with(url) + + def test_check_tag_exists(self): + res = self.sot + sess = self.session + + sess.get.side_effect = [FakeResponse(None, 202)] + + result = res.check_tag(sess, 'blue') + # Check tags attribute is updated + self.assertEqual([], res.tags) + # Check the passed resource is returned + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/tags/blue' + sess.get.assert_called_once_with(url) + + def test_check_tag_not_exists(self): + res = self.sot + sess = self.session + + mock_response = mock.Mock() + mock_response.status_code = 404 + mock_response.links = {} + mock_response.content = None + + sess.get.side_effect = [mock_response] + + # ensure we get 404 + self.assertRaises( + exceptions.NotFoundException, + res.check_tag, + sess, + 'dummy', + ) + + def test_add_tag(self): + res = self.sot + sess = self.session + + # Set some initial value to check add + res.tags.extend(['blue', 'green']) + + result = res.add_tag(sess, 'lila') + # Check tags attribute is updated + self.assertEqual(['blue', 'green', 'lila'], res.tags) + # Check the passed resource is returned + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/tags/lila' + sess.put.assert_called_once_with(url) + + def test_tagged_resource_always_created_with_empty_tag_list(self): + res = self.sot + + self.assertIsNotNone(res.tags) + self.assertEqual(res.tags, list()) diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 8e0eab91e..396cb6adf 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -3121,166 +3121,3 @@ def test_none(self, mock_get_ver): self.res._assert_microversion_for, self.session, 'fetch', '1.6') mock_get_ver.assert_called_once_with(self.res, self.session, 'fetch') - - -class TestTagMixin(base.TestCase): - - def setUp(self): - super(TestTagMixin, self).setUp() - - self.service_name = "service" - self.base_path = "base_path" - - class Test(resource.Resource, resource.TagMixin): - service = self.service_name - base_path = self.base_path - resources_key = 'resources' - allow_create = True - allow_fetch = True - allow_head = True - allow_commit = True - allow_delete = True - allow_list = True - - self.test_class = Test - - self.request = mock.Mock(spec=resource._Request) - self.request.url = "uri" - self.request.body = "body" - self.request.headers = "headers" - - self.response = FakeResponse({}) - - self.sot = Test.new(id="id", tags=[]) - self.sot._prepare_request = mock.Mock(return_value=self.request) - self.sot._translate_response = mock.Mock() - - self.session = mock.Mock(spec=adapter.Adapter) - self.session.get = mock.Mock(return_value=self.response) - self.session.put = mock.Mock(return_value=self.response) - self.session.delete = mock.Mock(return_value=self.response) - - def test_tags_attribute(self): - res = self.sot - self.assertTrue(hasattr(res, 'tags')) - self.assertIsInstance(res.tags, list) - - def test_fetch_tags(self): - res = self.sot - sess = self.session - - mock_response = mock.Mock() - mock_response.status_code = 200 - mock_response.links = {} - mock_response.json.return_value = {'tags': ['blue1', 'green1']} - - sess.get.side_effect = [mock_response] - - result = res.fetch_tags(sess) - # Check tags attribute is updated - self.assertEqual(['blue1', 'green1'], res.tags) - # Check the passed resource is returned - self.assertEqual(res, result) - url = self.base_path + '/' + res.id + '/tags' - sess.get.assert_called_once_with(url) - - def test_set_tags(self): - res = self.sot - sess = self.session - - # Set some initial value to check rewrite - res.tags.extend(['blue_old', 'green_old']) - - result = res.set_tags(sess, ['blue', 'green']) - # Check tags attribute is updated - self.assertEqual(['blue', 'green'], res.tags) - # Check the passed resource is returned - self.assertEqual(res, result) - url = self.base_path + '/' + res.id + '/tags' - sess.put.assert_called_once_with( - url, - json={'tags': ['blue', 'green']} - ) - - def test_remove_all_tags(self): - res = self.sot - sess = self.session - - # Set some initial value to check removal - res.tags.extend(['blue_old', 'green_old']) - - result = res.remove_all_tags(sess) - # Check tags attribute is updated - self.assertEqual([], res.tags) - # Check the passed resource is returned - self.assertEqual(res, result) - url = self.base_path + '/' + res.id + '/tags' - sess.delete.assert_called_once_with(url) - - def test_remove_single_tag(self): - res = self.sot - sess = self.session - - res.tags.extend(['blue', 'dummy']) - - result = res.remove_tag(sess, 'dummy') - # Check tags attribute is updated - self.assertEqual(['blue'], res.tags) - # Check the passed resource is returned - self.assertEqual(res, result) - url = self.base_path + '/' + res.id + '/tags/dummy' - sess.delete.assert_called_once_with(url) - - def test_check_tag_exists(self): - res = self.sot - sess = self.session - - sess.get.side_effect = [FakeResponse(None, 202)] - - result = res.check_tag(sess, 'blue') - # Check tags attribute is updated - self.assertEqual([], res.tags) - # Check the passed resource is returned - self.assertEqual(res, result) - url = self.base_path + '/' + res.id + '/tags/blue' - sess.get.assert_called_once_with(url) - - def test_check_tag_not_exists(self): - res = self.sot - sess = self.session - - mock_response = mock.Mock() - mock_response.status_code = 404 - mock_response.links = {} - mock_response.content = None - - sess.get.side_effect = [mock_response] - - # ensure we get 404 - self.assertRaises( - exceptions.NotFoundException, - res.check_tag, - sess, - 'dummy', - ) - - def test_add_tag(self): - res = self.sot - sess = self.session - - # Set some initial value to check add - res.tags.extend(['blue', 'green']) - - result = res.add_tag(sess, 'lila') - # Check tags attribute is updated - self.assertEqual(['blue', 'green', 'lila'], res.tags) - # Check the passed resource is returned - self.assertEqual(res, result) - url = self.base_path + '/' + res.id + '/tags/lila' - sess.put.assert_called_once_with(url) - - def test_tagged_resource_always_created_with_empty_tag_list(self): - res = self.sot - - self.assertIsNotNone(res.tags) - self.assertEqual(res.tags, list()) From 2d56b681fe07ee653527c98f6bab78763421e102 Mon Sep 17 00:00:00 2001 From: Riccardo Pittau Date: Tue, 1 Jun 2021 11:42:47 +0200 Subject: [PATCH 2883/3836] Fix import order in cloud tests Remove tox filter Change-Id: I298e3f0d5a2c53fbe31c56dfecf6ea54676c564e --- openstack/tests/unit/cloud/test_accelerator.py | 4 +++- openstack/tests/unit/cloud/test_aggregate.py | 2 +- openstack/tests/unit/cloud/test_availability_zones.py | 3 +-- openstack/tests/unit/cloud/test_caching.py | 7 ++++--- openstack/tests/unit/cloud/test_cluster_templates.py | 3 +-- openstack/tests/unit/cloud/test_create_server.py | 3 ++- openstack/tests/unit/cloud/test_endpoints.py | 3 ++- openstack/tests/unit/cloud/test_floating_ip_common.py | 2 +- openstack/tests/unit/cloud/test_floating_ip_neutron.py | 1 + openstack/tests/unit/cloud/test_floating_ip_pool.py | 2 +- openstack/tests/unit/cloud/test_fwaas.py | 3 ++- openstack/tests/unit/cloud/test_identity_roles.py | 2 +- openstack/tests/unit/cloud/test_image.py | 4 +++- openstack/tests/unit/cloud/test_meta.py | 3 ++- openstack/tests/unit/cloud/test_network.py | 1 + openstack/tests/unit/cloud/test_object.py | 2 +- openstack/tests/unit/cloud/test_operator.py | 2 +- openstack/tests/unit/cloud/test_project.py | 3 ++- openstack/tests/unit/cloud/test_recordset.py | 2 +- openstack/tests/unit/cloud/test_role_assignment.py | 5 +++-- openstack/tests/unit/cloud/test_router.py | 3 ++- openstack/tests/unit/cloud/test_security_groups.py | 3 ++- openstack/tests/unit/cloud/test_server_console.py | 3 +-- openstack/tests/unit/cloud/test_server_group.py | 2 +- openstack/tests/unit/cloud/test_services.py | 3 ++- openstack/tests/unit/cloud/test_shared_file_system.py | 4 +++- openstack/tests/unit/cloud/test_stack.py | 5 ++--- openstack/tests/unit/cloud/test_subnet.py | 1 + openstack/tests/unit/cloud/test_volume.py | 2 +- openstack/tests/unit/cloud/test_zone.py | 4 ++-- tox.ini | 1 - 31 files changed, 51 insertions(+), 37 deletions(-) diff --git a/openstack/tests/unit/cloud/test_accelerator.py b/openstack/tests/unit/cloud/test_accelerator.py index 5b09a56e8..39dcf761e 100644 --- a/openstack/tests/unit/cloud/test_accelerator.py +++ b/openstack/tests/unit/cloud/test_accelerator.py @@ -10,10 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base import copy import uuid +from openstack.tests.unit import base + + DEP_UUID = uuid.uuid4().hex DEP_DICT = { 'uuid': DEP_UUID, diff --git a/openstack/tests/unit/cloud/test_aggregate.py b/openstack/tests/unit/cloud/test_aggregate.py index 8de62693c..deec61c12 100644 --- a/openstack/tests/unit/cloud/test_aggregate.py +++ b/openstack/tests/unit/cloud/test_aggregate.py @@ -10,8 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base from openstack.tests import fakes +from openstack.tests.unit import base class TestAggregate(base.TestCase): diff --git a/openstack/tests/unit/cloud/test_availability_zones.py b/openstack/tests/unit/cloud/test_availability_zones.py index 82c3f19e1..05a25bd60 100644 --- a/openstack/tests/unit/cloud/test_availability_zones.py +++ b/openstack/tests/unit/cloud/test_availability_zones.py @@ -11,9 +11,8 @@ # License for the specific language governing permissions and limitations # under the License. - -from openstack.tests.unit import base from openstack.tests import fakes +from openstack.tests.unit import base _fake_zone_list = { diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py index 5f63d9df2..0ea85c0ca 100644 --- a/openstack/tests/unit/cloud/test_caching.py +++ b/openstack/tests/unit/cloud/test_caching.py @@ -9,6 +9,7 @@ # 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 concurrent import time @@ -16,14 +17,14 @@ from testscenarios import load_tests_apply_scenarios as load_tests # noqa import openstack -import openstack.cloud from openstack.block_storage.v3 import volume as _volume +import openstack.cloud from openstack.cloud import meta from openstack.compute.v2 import flavor as _flavor -from openstack.network.v2 import port as _port +from openstack import exceptions from openstack.identity.v3 import project as _project from openstack.identity.v3 import user as _user -from openstack import exceptions +from openstack.network.v2 import port as _port from openstack.tests import fakes from openstack.tests.unit import base from openstack.tests.unit.cloud import test_port diff --git a/openstack/tests/unit/cloud/test_cluster_templates.py b/openstack/tests/unit/cloud/test_cluster_templates.py index dfbea817b..13f02e612 100644 --- a/openstack/tests/unit/cloud/test_cluster_templates.py +++ b/openstack/tests/unit/cloud/test_cluster_templates.py @@ -10,11 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. - import munch +import testtools import openstack.cloud -import testtools from openstack.tests.unit import base diff --git a/openstack/tests/unit/cloud/test_create_server.py b/openstack/tests/unit/cloud/test_create_server.py index 7b293fd62..f2987f865 100644 --- a/openstack/tests/unit/cloud/test_create_server.py +++ b/openstack/tests/unit/cloud/test_create_server.py @@ -16,13 +16,14 @@ Tests for the `create_server` command. """ + import base64 from unittest import mock import uuid -from openstack import connection from openstack.cloud import exc from openstack.cloud import meta +from openstack import connection from openstack.tests import fakes from openstack.tests.unit import base diff --git a/openstack/tests/unit/cloud/test_endpoints.py b/openstack/tests/unit/cloud/test_endpoints.py index b3ebe9eef..476da65a9 100644 --- a/openstack/tests/unit/cloud/test_endpoints.py +++ b/openstack/tests/unit/cloud/test_endpoints.py @@ -21,9 +21,10 @@ import uuid -from openstack.tests.unit import base from testtools import matchers +from openstack.tests.unit import base + class TestCloudEndpoints(base.TestCase): diff --git a/openstack/tests/unit/cloud/test_floating_ip_common.py b/openstack/tests/unit/cloud/test_floating_ip_common.py index 6845ab0d3..66ac42525 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_common.py +++ b/openstack/tests/unit/cloud/test_floating_ip_common.py @@ -21,8 +21,8 @@ from unittest.mock import patch -from openstack import connection from openstack.cloud import meta +from openstack import connection from openstack.tests import fakes from openstack.tests.unit import base diff --git a/openstack/tests/unit/cloud/test_floating_ip_neutron.py b/openstack/tests/unit/cloud/test_floating_ip_neutron.py index e2ae862ce..9cf43fdf9 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_neutron.py +++ b/openstack/tests/unit/cloud/test_floating_ip_neutron.py @@ -21,6 +21,7 @@ import copy import datetime + import munch from openstack.cloud import exc diff --git a/openstack/tests/unit/cloud/test_floating_ip_pool.py b/openstack/tests/unit/cloud/test_floating_ip_pool.py index ddda50010..7e7132b05 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_pool.py +++ b/openstack/tests/unit/cloud/test_floating_ip_pool.py @@ -20,8 +20,8 @@ """ from openstack.cloud.exc import OpenStackCloudException -from openstack.tests.unit import base from openstack.tests import fakes +from openstack.tests.unit import base class TestFloatingIPPool(base.TestCase): diff --git a/openstack/tests/unit/cloud/test_fwaas.py b/openstack/tests/unit/cloud/test_fwaas.py index b9121df8a..529798f64 100644 --- a/openstack/tests/unit/cloud/test_fwaas.py +++ b/openstack/tests/unit/cloud/test_fwaas.py @@ -9,13 +9,14 @@ # 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 copy import deepcopy from unittest import mock from openstack import exceptions from openstack.network.v2.firewall_group import FirewallGroup -from openstack.network.v2.firewall_rule import FirewallRule from openstack.network.v2.firewall_policy import FirewallPolicy +from openstack.network.v2.firewall_rule import FirewallRule from openstack.tests.unit import base diff --git a/openstack/tests/unit/cloud/test_identity_roles.py b/openstack/tests/unit/cloud/test_identity_roles.py index fb32bd088..fd3a56a66 100644 --- a/openstack/tests/unit/cloud/test_identity_roles.py +++ b/openstack/tests/unit/cloud/test_identity_roles.py @@ -12,10 +12,10 @@ # limitations under the License. import testtools +from testtools import matchers import openstack.cloud from openstack.tests.unit import base -from testtools import matchers RAW_ROLE_ASSIGNMENTS = [ diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 30a991a9b..36987ca02 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -11,17 +11,19 @@ # 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 io import operator import tempfile import uuid -from openstack import exceptions from openstack.cloud import exc from openstack.cloud import meta +from openstack import exceptions from openstack.tests import fakes from openstack.tests.unit import base + IMPORT_METHODS = 'glance-direct,web-download' diff --git a/openstack/tests/unit/cloud/test_meta.py b/openstack/tests/unit/cloud/test_meta.py index 3f4bbbdb4..e73d7ce8e 100644 --- a/openstack/tests/unit/cloud/test_meta.py +++ b/openstack/tests/unit/cloud/test_meta.py @@ -14,11 +14,12 @@ from unittest import mock -from openstack import connection from openstack.cloud import meta +from openstack import connection from openstack.tests import fakes from openstack.tests.unit import base + PRIVATE_V4 = '198.51.100.3' PUBLIC_V4 = '192.0.2.99' PUBLIC_V6 = '2001:0db8:face:0da0:face::0b00:1c' # rfc3849 diff --git a/openstack/tests/unit/cloud/test_network.py b/openstack/tests/unit/cloud/test_network.py index c8021041d..aa16247d7 100644 --- a/openstack/tests/unit/cloud/test_network.py +++ b/openstack/tests/unit/cloud/test_network.py @@ -11,6 +11,7 @@ # limitations under the License. import copy + import testtools import openstack diff --git a/openstack/tests/unit/cloud/test_object.py b/openstack/tests/unit/cloud/test_object.py index a4114ed70..b04c4688d 100644 --- a/openstack/tests/unit/cloud/test_object.py +++ b/openstack/tests/unit/cloud/test_object.py @@ -18,8 +18,8 @@ import testtools import openstack.cloud -import openstack.cloud.openstackcloud as oc_oc from openstack.cloud import exc +import openstack.cloud.openstackcloud as oc_oc from openstack import exceptions from openstack.object_store.v1 import _proxy from openstack.object_store.v1 import container diff --git a/openstack/tests/unit/cloud/test_operator.py b/openstack/tests/unit/cloud/test_operator.py index 90c8ed079..95078716c 100644 --- a/openstack/tests/unit/cloud/test_operator.py +++ b/openstack/tests/unit/cloud/test_operator.py @@ -10,8 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid from unittest import mock +import uuid import testtools diff --git a/openstack/tests/unit/cloud/test_project.py b/openstack/tests/unit/cloud/test_project.py index 4070c7a68..b7361b97e 100644 --- a/openstack/tests/unit/cloud/test_project.py +++ b/openstack/tests/unit/cloud/test_project.py @@ -10,9 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. +import uuid + import testtools from testtools import matchers -import uuid import openstack.cloud import openstack.cloud._utils diff --git a/openstack/tests/unit/cloud/test_recordset.py b/openstack/tests/unit/cloud/test_recordset.py index 3c4c5f323..734842331 100644 --- a/openstack/tests/unit/cloud/test_recordset.py +++ b/openstack/tests/unit/cloud/test_recordset.py @@ -9,9 +9,9 @@ # 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.tests.unit import base - from openstack.tests.unit.cloud import test_zone diff --git a/openstack/tests/unit/cloud/test_role_assignment.py b/openstack/tests/unit/cloud/test_role_assignment.py index 4a700c1dc..b13e3b084 100644 --- a/openstack/tests/unit/cloud/test_role_assignment.py +++ b/openstack/tests/unit/cloud/test_role_assignment.py @@ -11,11 +11,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -from openstack.cloud import exc -from openstack.tests.unit import base import testtools from testtools import matchers +from openstack.cloud import exc +from openstack.tests.unit import base + class TestRoleAssignment(base.TestCase): diff --git a/openstack/tests/unit/cloud/test_router.py b/openstack/tests/unit/cloud/test_router.py index aac8d791c..0b0c7fae9 100644 --- a/openstack/tests/unit/cloud/test_router.py +++ b/openstack/tests/unit/cloud/test_router.py @@ -14,11 +14,12 @@ # limitations under the License. import copy + import testtools from openstack.cloud import exc -from openstack.network.v2 import router as _router from openstack.network.v2 import port as _port +from openstack.network.v2 import router as _router from openstack.tests.unit import base diff --git a/openstack/tests/unit/cloud/test_security_groups.py b/openstack/tests/unit/cloud/test_security_groups.py index 70b3ec29a..147887600 100644 --- a/openstack/tests/unit/cloud/test_security_groups.py +++ b/openstack/tests/unit/cloud/test_security_groups.py @@ -14,8 +14,9 @@ import copy import openstack.cloud -from openstack.tests.unit import base from openstack.tests import fakes +from openstack.tests.unit import base + # TODO(mordred): Move id and name to using a getUniqueString() value diff --git a/openstack/tests/unit/cloud/test_server_console.py b/openstack/tests/unit/cloud/test_server_console.py index 9c04174f9..69ddfbeef 100644 --- a/openstack/tests/unit/cloud/test_server_console.py +++ b/openstack/tests/unit/cloud/test_server_console.py @@ -10,11 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. - import uuid -from openstack.tests.unit import base from openstack.tests import fakes +from openstack.tests.unit import base class TestServerConsole(base.TestCase): diff --git a/openstack/tests/unit/cloud/test_server_group.py b/openstack/tests/unit/cloud/test_server_group.py index 8206b7320..ef29523ce 100644 --- a/openstack/tests/unit/cloud/test_server_group.py +++ b/openstack/tests/unit/cloud/test_server_group.py @@ -13,8 +13,8 @@ import uuid -from openstack.tests.unit import base from openstack.tests import fakes +from openstack.tests.unit import base class TestServerGroup(base.TestCase): diff --git a/openstack/tests/unit/cloud/test_services.py b/openstack/tests/unit/cloud/test_services.py index 63e715246..fb4f5a387 100644 --- a/openstack/tests/unit/cloud/test_services.py +++ b/openstack/tests/unit/cloud/test_services.py @@ -19,9 +19,10 @@ Tests Keystone services commands. """ +from testtools import matchers + from openstack.cloud.exc import OpenStackCloudException from openstack.tests.unit import base -from testtools import matchers class CloudServices(base.TestCase): diff --git a/openstack/tests/unit/cloud/test_shared_file_system.py b/openstack/tests/unit/cloud/test_shared_file_system.py index 2c87f4678..51f3a25df 100644 --- a/openstack/tests/unit/cloud/test_shared_file_system.py +++ b/openstack/tests/unit/cloud/test_shared_file_system.py @@ -10,9 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.unit import base import uuid +from openstack.tests.unit import base + + IDENTIFIER = str(uuid.uuid4()) MANILA_AZ_DICT = { "id": IDENTIFIER, diff --git a/openstack/tests/unit/cloud/test_stack.py b/openstack/tests/unit/cloud/test_stack.py index 3006b185a..de00c3775 100644 --- a/openstack/tests/unit/cloud/test_stack.py +++ b/openstack/tests/unit/cloud/test_stack.py @@ -10,16 +10,15 @@ # License for the specific language governing permissions and limitations # under the License. - import tempfile + import testtools import openstack.cloud +from openstack.orchestration.v1 import stack from openstack.tests import fakes from openstack.tests.unit import base -from openstack.orchestration.v1 import stack - class TestStack(base.TestCase): diff --git a/openstack/tests/unit/cloud/test_subnet.py b/openstack/tests/unit/cloud/test_subnet.py index 87e678648..d6db4939b 100644 --- a/openstack/tests/unit/cloud/test_subnet.py +++ b/openstack/tests/unit/cloud/test_subnet.py @@ -14,6 +14,7 @@ # limitations under the License. import copy + import testtools from openstack.cloud import exc diff --git a/openstack/tests/unit/cloud/test_volume.py b/openstack/tests/unit/cloud/test_volume.py index 6e24e7912..b3c61bee1 100644 --- a/openstack/tests/unit/cloud/test_volume.py +++ b/openstack/tests/unit/cloud/test_volume.py @@ -13,8 +13,8 @@ import testtools -import openstack.cloud from openstack.block_storage.v3 import volume +import openstack.cloud from openstack.cloud import meta from openstack.compute.v2 import volume_attachment from openstack.tests import fakes diff --git a/openstack/tests/unit/cloud/test_zone.py b/openstack/tests/unit/cloud/test_zone.py index 1573ed62f..2c3f9b1ba 100644 --- a/openstack/tests/unit/cloud/test_zone.py +++ b/openstack/tests/unit/cloud/test_zone.py @@ -9,11 +9,11 @@ # 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 -from openstack.tests.unit import base +import copy from openstack import exceptions +from openstack.tests.unit import base zone_dict = { diff --git a/tox.ini b/tox.ini index 799cc3e56..7cd8c9dbe 100644 --- a/tox.ini +++ b/tox.ini @@ -123,7 +123,6 @@ show-source = True exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,openstack/_services_mixin.py per-file-ignores = openstack/tests/unit/block_storage/*:H306,I100,I201,I202 - openstack/tests/unit/cloud/*:H306,I100,I201,I202 [flake8:local-plugins] extension = From fae7ab067ebb89994fcc057fad6de4866f6f8c55 Mon Sep 17 00:00:00 2001 From: Riccardo Pittau Date: Tue, 1 Jun 2021 11:47:24 +0200 Subject: [PATCH 2884/3836] Fix import order in block_storage tests Remove tox filter Change-Id: Ib2c343daa8fbec569ae57a1c024b8633262a8b19 --- openstack/tests/unit/block_storage/v2/test_backup.py | 3 ++- openstack/tests/unit/block_storage/v2/test_snapshot.py | 4 ++-- openstack/tests/unit/block_storage/v2/test_stats.py | 2 +- openstack/tests/unit/block_storage/v2/test_type.py | 2 +- .../tests/unit/block_storage/v3/test_availability_zone.py | 2 +- openstack/tests/unit/block_storage/v3/test_backup.py | 3 ++- openstack/tests/unit/block_storage/v3/test_snapshot.py | 4 ++-- openstack/tests/unit/block_storage/v3/test_type.py | 4 ++-- openstack/tests/unit/block_storage/v3/test_type_encryption.py | 2 +- openstack/tests/unit/block_storage/v3/test_volume.py | 2 +- tox.ini | 2 -- 11 files changed, 15 insertions(+), 15 deletions(-) diff --git a/openstack/tests/unit/block_storage/v2/test_backup.py b/openstack/tests/unit/block_storage/v2/test_backup.py index 9b3a490fe..3a937e408 100644 --- a/openstack/tests/unit/block_storage/v2/test_backup.py +++ b/openstack/tests/unit/block_storage/v2/test_backup.py @@ -14,10 +14,11 @@ from keystoneauth1 import adapter -from openstack import exceptions from openstack.block_storage.v2 import backup +from openstack import exceptions from openstack.tests.unit import base + FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff" BACKUP = { diff --git a/openstack/tests/unit/block_storage/v2/test_snapshot.py b/openstack/tests/unit/block_storage/v2/test_snapshot.py index 29107eac8..fc6408d7f 100644 --- a/openstack/tests/unit/block_storage/v2/test_snapshot.py +++ b/openstack/tests/unit/block_storage/v2/test_snapshot.py @@ -11,11 +11,11 @@ # under the License. from unittest import mock -from openstack.tests.unit import base - from keystoneauth1 import adapter from openstack.block_storage.v2 import snapshot +from openstack.tests.unit import base + FAKE_ID = "ffa9bc5e-1172-4021-acaf-cdcd78a9584d" diff --git a/openstack/tests/unit/block_storage/v2/test_stats.py b/openstack/tests/unit/block_storage/v2/test_stats.py index 53817a4a8..10ea9d75d 100644 --- a/openstack/tests/unit/block_storage/v2/test_stats.py +++ b/openstack/tests/unit/block_storage/v2/test_stats.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.block_storage.v2 import stats from openstack.tests.unit import base -from openstack.block_storage.v2 import stats POOLS = {"name": "pool1", "capabilities": { diff --git a/openstack/tests/unit/block_storage/v2/test_type.py b/openstack/tests/unit/block_storage/v2/test_type.py index 87148fed4..337e982b3 100644 --- a/openstack/tests/unit/block_storage/v2/test_type.py +++ b/openstack/tests/unit/block_storage/v2/test_type.py @@ -14,9 +14,9 @@ from keystoneauth1 import adapter +from openstack.block_storage.v2 import type from openstack.tests.unit import base -from openstack.block_storage.v2 import type FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff" TYPE = { diff --git a/openstack/tests/unit/block_storage/v3/test_availability_zone.py b/openstack/tests/unit/block_storage/v3/test_availability_zone.py index adc40515b..26db80615 100644 --- a/openstack/tests/unit/block_storage/v3/test_availability_zone.py +++ b/openstack/tests/unit/block_storage/v3/test_availability_zone.py @@ -11,9 +11,9 @@ # under the License. from openstack.block_storage.v3 import availability_zone as az - from openstack.tests.unit import base + IDENTIFIER = 'IDENTIFIER' EXAMPLE = { "id": IDENTIFIER, diff --git a/openstack/tests/unit/block_storage/v3/test_backup.py b/openstack/tests/unit/block_storage/v3/test_backup.py index 73172f00c..f2da69e5b 100644 --- a/openstack/tests/unit/block_storage/v3/test_backup.py +++ b/openstack/tests/unit/block_storage/v3/test_backup.py @@ -14,10 +14,11 @@ from keystoneauth1 import adapter -from openstack import exceptions from openstack.block_storage.v3 import backup +from openstack import exceptions from openstack.tests.unit import base + FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff" BACKUP = { diff --git a/openstack/tests/unit/block_storage/v3/test_snapshot.py b/openstack/tests/unit/block_storage/v3/test_snapshot.py index a9968b853..05f833051 100644 --- a/openstack/tests/unit/block_storage/v3/test_snapshot.py +++ b/openstack/tests/unit/block_storage/v3/test_snapshot.py @@ -11,11 +11,11 @@ # under the License. from unittest import mock -from openstack.tests.unit import base - from keystoneauth1 import adapter from openstack.block_storage.v3 import snapshot +from openstack.tests.unit import base + FAKE_ID = "ffa9bc5e-1172-4021-acaf-cdcd78a9584d" diff --git a/openstack/tests/unit/block_storage/v3/test_type.py b/openstack/tests/unit/block_storage/v3/test_type.py index 719bcd577..28b285335 100644 --- a/openstack/tests/unit/block_storage/v3/test_type.py +++ b/openstack/tests/unit/block_storage/v3/test_type.py @@ -14,10 +14,10 @@ from keystoneauth1 import adapter +from openstack.block_storage.v3 import type +from openstack import exceptions from openstack.tests.unit import base -from openstack import exceptions -from openstack.block_storage.v3 import type FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff" TYPE = { diff --git a/openstack/tests/unit/block_storage/v3/test_type_encryption.py b/openstack/tests/unit/block_storage/v3/test_type_encryption.py index 98733ddb6..b448125a3 100644 --- a/openstack/tests/unit/block_storage/v3/test_type_encryption.py +++ b/openstack/tests/unit/block_storage/v3/test_type_encryption.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.block_storage.v3 import type from openstack.tests.unit import base -from openstack.block_storage.v3 import type FAKE_ID = "479394ab-2f25-416e-8f58-721d8e5e29de" TYPE_ID = "22373aed-c4a8-4072-b66c-bf0a90dc9a12" diff --git a/openstack/tests/unit/block_storage/v3/test_volume.py b/openstack/tests/unit/block_storage/v3/test_volume.py index 2bf262280..71b6d4916 100644 --- a/openstack/tests/unit/block_storage/v3/test_volume.py +++ b/openstack/tests/unit/block_storage/v3/test_volume.py @@ -14,8 +14,8 @@ from keystoneauth1 import adapter -from openstack import exceptions from openstack.block_storage.v3 import volume +from openstack import exceptions from openstack.tests.unit import base FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff" diff --git a/tox.ini b/tox.ini index 7cd8c9dbe..0e4d00087 100644 --- a/tox.ini +++ b/tox.ini @@ -121,8 +121,6 @@ ignore = H238,H4,W503 import-order-style = pep8 show-source = True exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,openstack/_services_mixin.py -per-file-ignores = - openstack/tests/unit/block_storage/*:H306,I100,I201,I202 [flake8:local-plugins] extension = From 94cb864d869b7a5bb2dd321959900d89bbffc7ff Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 18 May 2021 17:20:57 +0200 Subject: [PATCH 2885/3836] Introduce common class for QuotaSet Multiple services are using QuotaSet resource in the same style. Add a common implementation for it. Change-Id: Ic5523e48a9e284744945b02b0f0b3eaf049133d0 --- openstack/common/quota_set.py | 129 +++++++++++++ openstack/tests/unit/common/test_quota_set.py | 173 ++++++++++++++++++ 2 files changed, 302 insertions(+) create mode 100644 openstack/common/quota_set.py create mode 100644 openstack/tests/unit/common/test_quota_set.py diff --git a/openstack/common/quota_set.py b/openstack/common/quota_set.py new file mode 100644 index 000000000..34730118b --- /dev/null +++ b/openstack/common/quota_set.py @@ -0,0 +1,129 @@ +# 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 import resource + + +# ATTENTION: Please do not inherit this class for anything else then QuotaSet, +# since attribute processing is here very different! +class QuotaSet(resource.Resource): + resource_key = 'quota_set' + # ATTENTION: different services might be using different base_path + base_path = '/os-quota-sets/%(project_id)s' + + # capabilities + allow_create = True + allow_fetch = True + allow_delete = True + allow_commit = True + + _query_mapping = resource.QueryParameters( + "usage") + + # NOTE(gtema) Sadly this attribute is useless in all the methods, but keep + # it here extra as a reminder + requires_id = False + + # Quota-sets are not very well designed. We must keep what is + # there and try to process it on best effort + _allow_unknown_attrs_in_body = True + + #: Properties + #: Current reservations + #: *type:dict* + reservation = resource.Body('reservation', type=dict) + #: Quota usage + #: *type:dict* + usage = resource.Body('usage', type=dict) + + project_id = resource.URI('project_id') + + def fetch(self, session, requires_id=False, + base_path=None, error_message=None, **params): + return super(QuotaSet, self).fetch( + session, + requires_id=False, + base_path=base_path, + error_message=error_message, + **params + ) + + def _translate_response(self, response, has_body=None, error_message=None): + """Given a KSA response, inflate this instance with its data + + DELETE operations don't return a body, so only try to work + with a body when has_body is True. + + This method updates attributes that correspond to headers + and body on this instance and clears the dirty set. + """ + if has_body is None: + has_body = self.has_body + exceptions.raise_from_response(response, error_message=error_message) + if has_body: + try: + body = response.json() + if self.resource_key and self.resource_key in body: + body = body[self.resource_key] + + # Do not allow keys called "self" through. Glance chose + # to name a key "self", so we need to pop it out because + # we can't send it through cls.existing and into the + # Resource initializer. "self" is already the first + # argument and is practically a reserved word. + body.pop("self", None) + + # Process body_attrs to strip usage and reservation out + normalized_attrs = dict( + reservation={}, + usage={}, + ) + + for key, val in body.items(): + if isinstance(val, dict): + if 'in_use' in val: + normalized_attrs['usage'][key] = val['in_use'] + if 'reserved' in val: + normalized_attrs['reservation'][key] = \ + val['reserved'] + if 'limit' in val: + normalized_attrs[key] = val['limit'] + else: + normalized_attrs[key] = val + + self._unknown_attrs_in_body.update(normalized_attrs) + + self._body.attributes.update(normalized_attrs) + self._body.clean() + if self.commit_jsonpatch or self.allow_patch: + # We need the original body to compare against + self._original_body = normalized_attrs.copy() + except ValueError: + # Server returned not parsable response (202, 204, etc) + # Do simply nothing + pass + + headers = self._consume_header_attrs(response.headers) + self._header.attributes.update(headers) + self._header.clean() + self._update_location() + dict.update(self, self.to_dict()) + + def _prepare_request_body(self, patch, prepend_key): + body = self._body.dirty + # Ensure we never try to send meta props reservation and usage + body.pop('reservation', None) + body.pop('usage', None) + + if prepend_key and self.resource_key is not None: + body = {self.resource_key: body} + return body diff --git a/openstack/tests/unit/common/test_quota_set.py b/openstack/tests/unit/common/test_quota_set.py new file mode 100644 index 000000000..cc525010f --- /dev/null +++ b/openstack/tests/unit/common/test_quota_set.py @@ -0,0 +1,173 @@ +# 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 +from unittest import mock + +from keystoneauth1 import adapter + +from openstack.common import quota_set as _qs +from openstack.tests.unit import base + + +BASIC_EXAMPLE = { + "backup_gigabytes": 1000, + "backups": 10, + "gigabytes___DEFAULT__": -1, +} + +USAGE_EXAMPLE = { + "backup_gigabytes": { + "in_use": 0, + "limit": 1000, + "reserved": 0 + }, + "backups": { + "in_use": 0, + "limit": 10, + "reserved": 0 + }, + "gigabytes___DEFAULT__": { + "in_use": 0, + "limit": -1, + "reserved": 0 + } +} + + +class TestQuotaSet(base.TestCase): + + def setUp(self): + super(TestQuotaSet, self).setUp() + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.default_microversion = 1 + self.sess._get_connection = mock.Mock(return_value=self.cloud) + self.sess.retriable_status_codes = set() + + def test_basic(self): + sot = _qs.QuotaSet() + self.assertEqual('quota_set', sot.resource_key) + self.assertIsNone(sot.resources_key) + self.assertEqual('/os-quota-sets/%(project_id)s', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_delete) + self.assertFalse(sot.allow_list) + self.assertTrue(sot.allow_commit) + + self.assertDictEqual( + {"usage": "usage", + "limit": "limit", + "marker": "marker"}, + sot._query_mapping._mapping) + + def test_make_basic(self): + sot = _qs.QuotaSet(**BASIC_EXAMPLE) + + self.assertEqual(BASIC_EXAMPLE['backups'], sot.backups) + + def test_get(self): + sot = _qs.QuotaSet(project_id='proj') + + resp = mock.Mock() + resp.body = {'quota_set': copy.deepcopy(BASIC_EXAMPLE)} + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + resp.headers = {} + self.sess.get = mock.Mock(return_value=resp) + + sot.fetch(self.sess) + + self.sess.get.assert_called_with( + '/os-quota-sets/proj', + microversion=1, + params={}) + + self.assertEqual(BASIC_EXAMPLE['backups'], sot.backups) + self.assertEqual({}, sot.reservation) + self.assertEqual({}, sot.usage) + + def test_get_usage(self): + sot = _qs.QuotaSet(project_id='proj') + + resp = mock.Mock() + resp.body = {'quota_set': copy.deepcopy(USAGE_EXAMPLE)} + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + resp.headers = {} + self.sess.get = mock.Mock(return_value=resp) + + sot.fetch(self.sess, usage=True) + + self.sess.get.assert_called_with( + '/os-quota-sets/proj', + microversion=1, + params={'usage': True}) + + self.assertEqual( + USAGE_EXAMPLE['backups']['limit'], + sot.backups) + + def test_update_quota(self): + # Use QuotaSet as if it was returned by get(usage=True) + sot = _qs.QuotaSet.existing( + project_id='proj', + reservation={'a': 'b'}, + usage={'c': 'd'}, + foo='bar') + + resp = mock.Mock() + resp.body = {'quota_set': copy.deepcopy(BASIC_EXAMPLE)} + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + resp.headers = {} + self.sess.put = mock.Mock(return_value=resp) + + sot._update( + reservation={'b': 'd'}, + backups=15, + something_else=20) + + sot.commit(self.sess) + + self.sess.put.assert_called_with( + '/os-quota-sets/proj', + microversion=1, + headers={}, + json={ + 'quota_set': { + 'backups': 15, + 'something_else': 20 + } + }) + + def test_delete_quota(self): + # Use QuotaSet as if it was returned by get(usage=True) + sot = _qs.QuotaSet.existing( + project_id='proj', + reservation={'a': 'b'}, + usage={'c': 'd'}, + foo='bar') + + resp = mock.Mock() + resp.body = None + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + resp.headers = {} + self.sess.delete = mock.Mock(return_value=resp) + + sot.delete(self.sess) + + self.sess.delete.assert_called_with( + '/os-quota-sets/proj', + microversion=1, + headers={}, + ) From b7ebc731caea4727b4ea346c0ffef624895ea9ed Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 18 May 2021 17:21:55 +0200 Subject: [PATCH 2886/3836] Introduce QuotaSet in the compute service Start using newly introduced QuotaSet base resource in the compute area. Change-Id: I3165dd717035319c713a94326bd964fc63e2acf7 --- doc/source/user/proxies/compute.rst | 7 ++ doc/source/user/resources/compute/index.rst | 1 + .../user/resources/compute/v2/quota_set.rst | 12 +++ openstack/compute/v2/_proxy.py | 70 +++++++++++++++ openstack/compute/v2/quota_set.py | 54 ++++++++++++ .../functional/compute/v2/test_quota_set.py | 48 +++++++++++ openstack/tests/unit/compute/v2/test_proxy.py | 85 +++++++++++++++++++ .../compute-quota-set-e664412d089945d2.yaml | 4 + 8 files changed, 281 insertions(+) create mode 100644 doc/source/user/resources/compute/v2/quota_set.rst create mode 100644 openstack/compute/v2/quota_set.py create mode 100644 openstack/tests/functional/compute/v2/test_quota_set.py create mode 100644 releasenotes/notes/compute-quota-set-e664412d089945d2.yaml diff --git a/doc/source/user/proxies/compute.rst b/doc/source/user/proxies/compute.rst index a4b771551..8c9551212 100644 --- a/doc/source/user/proxies/compute.rst +++ b/doc/source/user/proxies/compute.rst @@ -149,3 +149,10 @@ Extension Operations .. autoclass:: openstack.compute.v2._proxy.Proxy :noindex: :members: find_extension, extensions + +QuotaSet Operations +^^^^^^^^^^^^^^^^^^^ +.. autoclass:: openstack.compute.v2._proxy.Proxy + :noindex: + :members: get_quota_set, get_quota_set_defaults, + revert_quota_set, update_quota_set diff --git a/doc/source/user/resources/compute/index.rst b/doc/source/user/resources/compute/index.rst index e56d2a058..0db8a314b 100644 --- a/doc/source/user/resources/compute/index.rst +++ b/doc/source/user/resources/compute/index.rst @@ -13,3 +13,4 @@ Compute Resources v2/server_interface v2/server_ip v2/hypervisor + v2/quota_set diff --git a/doc/source/user/resources/compute/v2/quota_set.rst b/doc/source/user/resources/compute/v2/quota_set.rst new file mode 100644 index 000000000..8a5d91dfc --- /dev/null +++ b/doc/source/user/resources/compute/v2/quota_set.rst @@ -0,0 +1,12 @@ +openstack.compute.v2.quota_set +============================== + +.. automodule:: openstack.compute.v2.quota_set + +The QuotaSet Class +------------------ + +The ``QuotaSet`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.compute.v2.quota_set.QuotaSet + :members: diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index d520cdac1..85083bea5 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -20,6 +20,7 @@ from openstack.compute.v2 import image as _image from openstack.compute.v2 import keypair as _keypair from openstack.compute.v2 import limits +from openstack.compute.v2 import quota_set as _quota_set from openstack.compute.v2 import server as _server from openstack.compute.v2 import server_diagnostics as _server_diagnostics from openstack.compute.v2 import server_group as _server_group @@ -29,6 +30,7 @@ from openstack.compute.v2 import service as _service from openstack.compute.v2 import volume_attachment as _volume_attachment from openstack import exceptions +from openstack.identity.v3 import project as _project from openstack.network.v2 import security_group as _sg from openstack import proxy from openstack import resource @@ -1818,6 +1820,74 @@ def create_console(self, server, console_type, console_protocol=None): else: return server.get_console_url(self, console_type) + def get_quota_set(self, project, usage=False, **query): + """Show QuotaSet information for the project + + :param project: ID or instance of + :class:`~openstack.identity.project.Project` of the project for + which the quota should be retrieved + :param bool usage: When set to ``True`` quota usage and reservations + would be filled. + :param dict query: Additional query parameters to use. + + :returns: One :class:`~openstack.compute.v2.quota_set.QuotaSet` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + project = self._get_resource(_project.Project, project) + res = self._get_resource( + _quota_set.QuotaSet, None, project_id=project.id) + return res.fetch( + self, usage=usage, **query) + + def get_quota_set_defaults(self, project): + """Show QuotaSet defaults for the project + + :param project: ID or instance of + :class:`~openstack.identity.project.Project` of the project for + which the quota should be retrieved + + :returns: One :class:`~openstack.compute.v2.quota_set.QuotaSet` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + project = self._get_resource(_project.Project, project) + res = self._get_resource( + _quota_set.QuotaSet, None, project_id=project.id) + return res.fetch( + self, base_path='/os-quota-sets/defaults') + + def revert_quota_set(self, project, **query): + """Reset Quota for the project/user. + + :param project: ID or instance of + :class:`~openstack.identity.project.Project` of the project for + which the quota should be resetted. + :param dict query: Additional parameters to be used. + + :returns: ``None`` + """ + project = self._get_resource(_project.Project, project) + res = self._get_resource( + _quota_set.QuotaSet, None, project_id=project.id) + + return res.delete(self, **query) + + def update_quota_set(self, quota_set, query=None, **attrs): + """Update a QuotaSet. + + :param quota_set: Either the ID of a quota_set or a + :class:`~openstack.compute.v2.quota_set.QuotaSet` instance. + :param dict query: Optional parameters to be used with update call. + :attrs kwargs: The attributes to update on the QuotaSet represented + by ``quota_set``. + + :returns: The updated QuotaSet + :rtype: :class:`~openstack.compute.v2.quota_set.QuotaSet` + """ + res = self._get_resource(_quota_set.QuotaSet, quota_set, **attrs) + return res.commit(self, **query) + def _get_cleanup_dependencies(self): return { 'compute': { diff --git a/openstack/compute/v2/quota_set.py b/openstack/compute/v2/quota_set.py new file mode 100644 index 000000000..890a819c1 --- /dev/null +++ b/openstack/compute/v2/quota_set.py @@ -0,0 +1,54 @@ +# 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.common import quota_set +from openstack import resource + + +class QuotaSet(quota_set.QuotaSet): + # We generally only want compute QS support max_microversion. Otherwise be + # explicit and list all the attributes + _max_microversion = '2.56' + + #: Properties + #: The number of allowed server cores for each tenant. + cores = resource.Body('cores', type=int) + #: The number of allowed fixed IP addresses for each tenant. Must be + #: equal to or greater than the number of allowed servers. + fixed_ips = resource.Body('fixed_ips', type=int) + #: The number of allowed floating IP addresses for each tenant. + floating_ips = resource.Body('floating_ips', type=int) + #: The number of allowed bytes of content for each injected file. + injected_file_content_bytes = resource.Body( + 'injected_file_content_bytes', type=int) + #: The number of allowed bytes for each injected file path. + injected_file_path_bytes = resource.Body( + 'injected_file_path_bytes', type=int) + #: The number of allowed injected files for each tenant. + injected_files = resource.Body('injected_files', type=int) + #: The number of allowed servers for each tenant. + instances = resource.Body('instances', type=int) + #: The number of allowed key pairs for each user. + key_pairs = resource.Body('key_pairs', type=int) + #: The number of allowed metadata items for each server. + metadata_items = resource.Body('metadata_items', type=int) + #: The number of private networks that can be created per project. + networks = resource.Body('networks', type=int) + #: The amount of allowed server RAM, in MiB, for each tenant. + ram = resource.Body('ram', type=int) + #: The number of allowed rules for each security group. + security_group_rules = resource.Body('security_group_rules', type=int) + #: The number of allowed security groups for each tenant. + security_groups = resource.Body('security_groups', type=int) + #: The number of allowed server groups for each tenant. + server_groups = resource.Body('server_groups', type=int) + #: The number of allowed members for each server group. + server_group_members = resource.Body('server_group_members', type=int) diff --git a/openstack/tests/functional/compute/v2/test_quota_set.py b/openstack/tests/functional/compute/v2/test_quota_set.py new file mode 100644 index 000000000..5fa04431d --- /dev/null +++ b/openstack/tests/functional/compute/v2/test_quota_set.py @@ -0,0 +1,48 @@ +# 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.tests.functional import base + + +class TestQS(base.BaseFunctionalTest): + + def test_qs(self): + sot = self.conn.compute.get_quota_set( + self.conn.current_project_id + ) + self.assertIsNotNone(sot.key_pairs) + + def test_qs_user(self): + sot = self.conn.compute.get_quota_set( + self.conn.current_project_id, + user_id=self.conn.session.auth.get_user_id(self.conn.compute) + ) + self.assertIsNotNone(sot.key_pairs) + + def test_update(self): + sot = self.conn.compute.get_quota_set( + self.conn.current_project_id + ) + self.conn.compute.update_quota_set( + sot, + query={ + 'user_id': self.conn.session.auth.get_user_id( + self.conn.compute) + }, + key_pairs=100 + ) + + def test_revert(self): + self.conn.compute.revert_quota_set( + self.conn.current_project_id, + user_id=self.conn.session.auth.get_user_id(self.conn.compute) + ) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index a0ed8da44..f9b14e010 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -21,12 +21,14 @@ from openstack.compute.v2 import image from openstack.compute.v2 import keypair from openstack.compute.v2 import limits +from openstack.compute.v2 import quota_set from openstack.compute.v2 import server from openstack.compute.v2 import server_group from openstack.compute.v2 import server_interface from openstack.compute.v2 import server_ip from openstack.compute.v2 import server_remote_console from openstack.compute.v2 import service +from openstack import resource from openstack.tests.unit import test_proxy_base @@ -1079,3 +1081,86 @@ def test_create_console_mv_2_6(self, sgc, rcc, smv): type='fake_type', protocol=None) self.assertEqual(console_fake['url'], ret['url']) + + +class TestQuota(TestComputeProxy): + def test_get(self): + self._verify( + 'openstack.resource.Resource.fetch', + self.proxy.get_quota_set, + method_args=['prj'], + expected_args=[self.proxy], + expected_kwargs={ + 'error_message': None, + 'requires_id': False, + 'usage': False, + }, + method_result=quota_set.QuotaSet(), + expected_result=quota_set.QuotaSet() + ) + + def test_get_query(self): + self._verify( + 'openstack.resource.Resource.fetch', + self.proxy.get_quota_set, + method_args=['prj'], + method_kwargs={ + 'usage': True, + 'user_id': 'uid' + }, + expected_args=[self.proxy], + expected_kwargs={ + 'error_message': None, + 'requires_id': False, + 'usage': True, + 'user_id': 'uid' + } + ) + + def test_get_defaults(self): + self._verify( + 'openstack.resource.Resource.fetch', + self.proxy.get_quota_set_defaults, + method_args=['prj'], + expected_args=[self.proxy], + expected_kwargs={ + 'error_message': None, + 'requires_id': False, + 'base_path': '/os-quota-sets/defaults' + } + ) + + def test_reset(self): + self._verify( + 'openstack.resource.Resource.delete', + self.proxy.revert_quota_set, + method_args=['prj'], + method_kwargs={'user_id': 'uid'}, + expected_args=[self.proxy], + expected_kwargs={ + 'user_id': 'uid' + } + ) + + @mock.patch('openstack.proxy.Proxy._get_resource', autospec=True) + def test_update(self, gr_mock): + gr_mock.return_value = resource.Resource() + gr_mock.commit = mock.Mock() + self._verify( + 'openstack.resource.Resource.commit', + self.proxy.update_quota_set, + method_args=['qs'], + method_kwargs={ + 'query': {'user_id': 'uid'}, + 'a': 'b', + }, + expected_args=[self.proxy], + expected_kwargs={ + 'user_id': 'uid' + } + ) + gr_mock.assert_called_with( + self.proxy, + quota_set.QuotaSet, + 'qs', a='b' + ) diff --git a/releasenotes/notes/compute-quota-set-e664412d089945d2.yaml b/releasenotes/notes/compute-quota-set-e664412d089945d2.yaml new file mode 100644 index 000000000..63f23898e --- /dev/null +++ b/releasenotes/notes/compute-quota-set-e664412d089945d2.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add support for QuotaSet in the compute service. From f3df2095ac5aec23ab4fbd9f9af8f170d9e00220 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 19 May 2021 09:58:56 +0200 Subject: [PATCH 2887/3836] Introduce QuotaSet in block storage service Start using common QuotaSet class in the block storage service. Change-Id: I741c74e596571dae6d28e305b89aafa4b51f4f3c --- doc/source/user/proxies/block_storage_v2.rst | 7 ++ doc/source/user/proxies/block_storage_v3.rst | 7 ++ .../user/resources/block_storage/index.rst | 2 + .../resources/block_storage/v2/quota_set.rst | 12 +++ .../resources/block_storage/v3/quota_set.rst | 12 +++ openstack/block_storage/v2/_proxy.py | 70 +++++++++++++++ openstack/block_storage/v2/quota_set.py | 33 +++++++ openstack/block_storage/v3/_proxy.py | 70 +++++++++++++++ openstack/block_storage/v3/quota_set.py | 33 +++++++ .../tests/unit/block_storage/v2/test_proxy.py | 85 +++++++++++++++++++ .../tests/unit/block_storage/v3/test_proxy.py | 85 +++++++++++++++++++ .../block-storage-qs-0e3b69be2e709b65.yaml | 4 + 12 files changed, 420 insertions(+) create mode 100644 doc/source/user/resources/block_storage/v2/quota_set.rst create mode 100644 doc/source/user/resources/block_storage/v3/quota_set.rst create mode 100644 openstack/block_storage/v2/quota_set.py create mode 100644 openstack/block_storage/v3/quota_set.py create mode 100644 releasenotes/notes/block-storage-qs-0e3b69be2e709b65.yaml diff --git a/doc/source/user/proxies/block_storage_v2.rst b/doc/source/user/proxies/block_storage_v2.rst index d169611ca..e3d650969 100644 --- a/doc/source/user/proxies/block_storage_v2.rst +++ b/doc/source/user/proxies/block_storage_v2.rst @@ -46,3 +46,10 @@ Stats Operations .. autoclass:: openstack.block_storage.v2._proxy.Proxy :noindex: :members: backend_pools + +QuotaSet Operations +^^^^^^^^^^^^^^^^^^^ +.. autoclass:: openstack.block_storage.v2._proxy.Proxy + :noindex: + :members: get_quota_set, get_quota_set_defaults, + revert_quota_set, update_quota_set diff --git a/doc/source/user/proxies/block_storage_v3.rst b/doc/source/user/proxies/block_storage_v3.rst index bfbbed2fb..a7a1e7747 100644 --- a/doc/source/user/proxies/block_storage_v3.rst +++ b/doc/source/user/proxies/block_storage_v3.rst @@ -46,3 +46,10 @@ Stats Operations .. autoclass:: openstack.block_storage.v3._proxy.Proxy :noindex: :members: backend_pools + +QuotaSet Operations +^^^^^^^^^^^^^^^^^^^ +.. autoclass:: openstack.block_storage.v3._proxy.Proxy + :noindex: + :members: get_quota_set, get_quota_set_defaults, + revert_quota_set, update_quota_set diff --git a/doc/source/user/resources/block_storage/index.rst b/doc/source/user/resources/block_storage/index.rst index fdcf654f1..7f9e16d21 100644 --- a/doc/source/user/resources/block_storage/index.rst +++ b/doc/source/user/resources/block_storage/index.rst @@ -5,11 +5,13 @@ Block Storage Resources :maxdepth: 1 v2/backup + v2/quota_set v2/snapshot v2/type v2/volume v3/backup + v3/quota_set v3/snapshot v3/type v3/volume diff --git a/doc/source/user/resources/block_storage/v2/quota_set.rst b/doc/source/user/resources/block_storage/v2/quota_set.rst new file mode 100644 index 000000000..35211aec4 --- /dev/null +++ b/doc/source/user/resources/block_storage/v2/quota_set.rst @@ -0,0 +1,12 @@ +openstack.block_storage.v2.quota_set +==================================== + +.. automodule:: openstack.block_storage.v2.quota_set + +The QuotaSet Class +------------------ + +The ``QuotaSet`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v2.quota_set.QuotaSet + :members: diff --git a/doc/source/user/resources/block_storage/v3/quota_set.rst b/doc/source/user/resources/block_storage/v3/quota_set.rst new file mode 100644 index 000000000..69a287b9c --- /dev/null +++ b/doc/source/user/resources/block_storage/v3/quota_set.rst @@ -0,0 +1,12 @@ +openstack.block_storage.v3.quota_set +==================================== + +.. automodule:: openstack.block_storage.v3.quota_set + +The QuotaSet Class +------------------ + +The ``QuotaSet`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v3.quota_set.QuotaSet + :members: diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index e8fe5bdec..72c6acf1c 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -12,10 +12,12 @@ from openstack.block_storage import _base_proxy from openstack.block_storage.v2 import backup as _backup +from openstack.block_storage.v2 import quota_set as _quota_set from openstack.block_storage.v2 import snapshot as _snapshot from openstack.block_storage.v2 import stats as _stats from openstack.block_storage.v2 import type as _type from openstack.block_storage.v2 import volume as _volume +from openstack.identity.v3 import project as _project from openstack import resource @@ -554,3 +556,71 @@ def wait_for_delete(self, res, interval=2, wait=120): to delete failed to occur in the specified seconds. """ return resource.wait_for_delete(self, res, interval, wait) + + def get_quota_set(self, project, usage=False, **query): + """Show QuotaSet information for the project + + :param project: ID or instance of + :class:`~openstack.identity.project.Project` of the project for + which the quota should be retrieved + :param bool usage: When set to ``True`` quota usage and reservations + would be filled. + :param dict query: Additional query parameters to use. + + :returns: One :class:`~openstack.block_storage.v2.quota_set.QuotaSet` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + project = self._get_resource(_project.Project, project) + res = self._get_resource( + _quota_set.QuotaSet, None, project_id=project.id) + return res.fetch( + self, usage=usage, **query) + + def get_quota_set_defaults(self, project): + """Show QuotaSet defaults for the project + + :param project: ID or instance of + :class:`~openstack.identity.project.Project` of the project for + which the quota should be retrieved + + :returns: One :class:`~openstack.block_storage.v2.quota_set.QuotaSet` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + project = self._get_resource(_project.Project, project) + res = self._get_resource( + _quota_set.QuotaSet, None, project_id=project.id) + return res.fetch( + self, base_path='/os-quota-sets/defaults') + + def revert_quota_set(self, project, **query): + """Reset Quota for the project/user. + + :param project: ID or instance of + :class:`~openstack.identity.project.Project` of the project for + which the quota should be resetted. + :param dict query: Additional parameters to be used. + + :returns: ``None`` + """ + project = self._get_resource(_project.Project, project) + res = self._get_resource( + _quota_set.QuotaSet, None, project_id=project.id) + + return res.delete(self, **query) + + def update_quota_set(self, quota_set, query=None, **attrs): + """Update a QuotaSet. + + :param quota_set: Either the ID of a quota_set or a + :class:`~openstack.block_storage.v2.quota_set.QuotaSet` instance. + :param dict query: Optional parameters to be used with update call. + :attrs kwargs: The attributes to update on the QuotaSet represented + by ``quota_set``. + + :returns: The updated QuotaSet + :rtype: :class:`~openstack.block_storage.v2.quota_set.QuotaSet` + """ + res = self._get_resource(_quota_set.QuotaSet, quota_set, **attrs) + return res.commit(self, **query) diff --git a/openstack/block_storage/v2/quota_set.py b/openstack/block_storage/v2/quota_set.py new file mode 100644 index 000000000..0ef3b51f9 --- /dev/null +++ b/openstack/block_storage/v2/quota_set.py @@ -0,0 +1,33 @@ +# 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.common import quota_set +from openstack import resource + + +class QuotaSet(quota_set.QuotaSet): + + #: Properties + #: The size (GB) of backups that are allowed for each project. + backup_gigabytes = resource.Body('backup_gigabytes', type=int) + #: The number of backups that are allowed for each project. + backups = resource.Body('backups', type=int) + #: The size (GB) of volumes and snapshots that are allowed for each + #: project. + gigabytes = resource.Body('gigabytes', type=int) + #: The number of groups that are allowed for each project. + groups = resource.Body('groups', type=int) + #: The size (GB) of volumes in request that are allowed for each volume. + per_volume_gigabytes = resource.Body('per_volume_gigabytes', type=int) + #: The number of snapshots that are allowed for each project. + snapshots = resource.Body('snapshots', type=int) + #: The number of volumes that are allowed for each project. + volumes = resource.Body('volumes', type=int) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 659b8b787..cc8384ed3 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -17,12 +17,14 @@ from openstack.block_storage.v3 import extension as _extension from openstack.block_storage.v3 import group_type as _group_type from openstack.block_storage.v3 import limits as _limits +from openstack.block_storage.v3 import quota_set as _quota_set from openstack.block_storage.v3 import resource_filter as _resource_filter from openstack.block_storage.v3 import snapshot as _snapshot from openstack.block_storage.v3 import stats as _stats from openstack.block_storage.v3 import type as _type from openstack.block_storage.v3 import volume as _volume from openstack import exceptions +from openstack.identity.v3 import project as _project from openstack import resource @@ -1027,6 +1029,74 @@ def extensions(self): """ return self._list(_extension.Extension) + def get_quota_set(self, project, usage=False, **query): + """Show QuotaSet information for the project + + :param project: ID or instance of + :class:`~openstack.identity.project.Project` of the project for + which the quota should be retrieved + :param bool usage: When set to ``True`` quota usage and reservations + would be filled. + :param dict query: Additional query parameters to use. + + :returns: One :class:`~openstack.block_storage.v3.quota_set.QuotaSet` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + project = self._get_resource(_project.Project, project) + res = self._get_resource( + _quota_set.QuotaSet, None, project_id=project.id) + return res.fetch( + self, usage=usage, **query) + + def get_quota_set_defaults(self, project): + """Show QuotaSet defaults for the project + + :param project: ID or instance of + :class:`~openstack.identity.project.Project` of the project for + which the quota should be retrieved + + :returns: One :class:`~openstack.block_storage.v3.quota_set.QuotaSet` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + project = self._get_resource(_project.Project, project) + res = self._get_resource( + _quota_set.QuotaSet, None, project_id=project.id) + return res.fetch( + self, base_path='/os-quota-sets/defaults') + + def revert_quota_set(self, project, **query): + """Reset Quota for the project/user. + + :param project: ID or instance of + :class:`~openstack.identity.project.Project` of the project for + which the quota should be resetted. + :param dict query: Additional parameters to be used. + + :returns: ``None`` + """ + project = self._get_resource(_project.Project, project) + res = self._get_resource( + _quota_set.QuotaSet, None, project_id=project.id) + + return res.delete(self, **query) + + def update_quota_set(self, quota_set, query=None, **attrs): + """Update a QuotaSet. + + :param quota_set: Either the ID of a quota_set or a + :class:`~openstack.block_storage.v3.quota_set.QuotaSet` instance. + :param dict query: Optional parameters to be used with update call. + :attrs kwargs: The attributes to update on the QuotaSet represented + by ``quota_set``. + + :returns: The updated QuotaSet + :rtype: :class:`~openstack.block_storage.v3.quota_set.QuotaSet` + """ + res = self._get_resource(_quota_set.QuotaSet, quota_set, **attrs) + return res.commit(self, **query) + def _get_cleanup_dependencies(self): return { 'block_storage': { diff --git a/openstack/block_storage/v3/quota_set.py b/openstack/block_storage/v3/quota_set.py new file mode 100644 index 000000000..0ef3b51f9 --- /dev/null +++ b/openstack/block_storage/v3/quota_set.py @@ -0,0 +1,33 @@ +# 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.common import quota_set +from openstack import resource + + +class QuotaSet(quota_set.QuotaSet): + + #: Properties + #: The size (GB) of backups that are allowed for each project. + backup_gigabytes = resource.Body('backup_gigabytes', type=int) + #: The number of backups that are allowed for each project. + backups = resource.Body('backups', type=int) + #: The size (GB) of volumes and snapshots that are allowed for each + #: project. + gigabytes = resource.Body('gigabytes', type=int) + #: The number of groups that are allowed for each project. + groups = resource.Body('groups', type=int) + #: The size (GB) of volumes in request that are allowed for each volume. + per_volume_gigabytes = resource.Body('per_volume_gigabytes', type=int) + #: The number of snapshots that are allowed for each project. + snapshots = resource.Body('snapshots', type=int) + #: The number of volumes that are allowed for each project. + volumes = resource.Body('volumes', type=int) diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index a3c76c6f5..4513da4b9 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -13,10 +13,12 @@ from openstack.block_storage.v2 import _proxy from openstack.block_storage.v2 import backup +from openstack.block_storage.v2 import quota_set from openstack.block_storage.v2 import snapshot from openstack.block_storage.v2 import stats from openstack.block_storage.v2 import type from openstack.block_storage.v2 import volume +from openstack import resource from openstack.tests.unit import test_proxy_base @@ -304,3 +306,86 @@ def test_type_remove_private_access(self): self.proxy.remove_type_access, method_args=["value", "a"], expected_args=[self.proxy, "a"]) + + +class TestQuota(TestVolumeProxy): + def test_get(self): + self._verify( + 'openstack.resource.Resource.fetch', + self.proxy.get_quota_set, + method_args=['prj'], + expected_args=[self.proxy], + expected_kwargs={ + 'error_message': None, + 'requires_id': False, + 'usage': False, + }, + method_result=quota_set.QuotaSet(), + expected_result=quota_set.QuotaSet() + ) + + def test_get_query(self): + self._verify( + 'openstack.resource.Resource.fetch', + self.proxy.get_quota_set, + method_args=['prj'], + method_kwargs={ + 'usage': True, + 'user_id': 'uid' + }, + expected_args=[self.proxy], + expected_kwargs={ + 'error_message': None, + 'requires_id': False, + 'usage': True, + 'user_id': 'uid' + } + ) + + def test_get_defaults(self): + self._verify( + 'openstack.resource.Resource.fetch', + self.proxy.get_quota_set_defaults, + method_args=['prj'], + expected_args=[self.proxy], + expected_kwargs={ + 'error_message': None, + 'requires_id': False, + 'base_path': '/os-quota-sets/defaults' + } + ) + + def test_reset(self): + self._verify( + 'openstack.resource.Resource.delete', + self.proxy.revert_quota_set, + method_args=['prj'], + method_kwargs={'user_id': 'uid'}, + expected_args=[self.proxy], + expected_kwargs={ + 'user_id': 'uid' + } + ) + + @mock.patch('openstack.proxy.Proxy._get_resource', autospec=True) + def test_update(self, gr_mock): + gr_mock.return_value = resource.Resource() + gr_mock.commit = mock.Mock() + self._verify( + 'openstack.resource.Resource.commit', + self.proxy.update_quota_set, + method_args=['qs'], + method_kwargs={ + 'query': {'user_id': 'uid'}, + 'a': 'b', + }, + expected_args=[self.proxy], + expected_kwargs={ + 'user_id': 'uid' + } + ) + gr_mock.assert_called_with( + self.proxy, + quota_set.QuotaSet, + 'qs', a='b' + ) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index b711fc086..46dfe044e 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -17,11 +17,13 @@ from openstack.block_storage.v3 import extension from openstack.block_storage.v3 import group_type from openstack.block_storage.v3 import limits +from openstack.block_storage.v3 import quota_set from openstack.block_storage.v3 import resource_filter from openstack.block_storage.v3 import snapshot from openstack.block_storage.v3 import stats from openstack.block_storage.v3 import type from openstack.block_storage.v3 import volume +from openstack import resource from openstack.tests.unit import test_proxy_base @@ -545,3 +547,86 @@ def test_type_encryption_delete(self): def test_type_encryption_delete_ignore(self): self.verify_delete( self.proxy.delete_type_encryption, type.TypeEncryption, True) + + +class TestQuota(TestVolumeProxy): + def test_get(self): + self._verify( + 'openstack.resource.Resource.fetch', + self.proxy.get_quota_set, + method_args=['prj'], + expected_args=[self.proxy], + expected_kwargs={ + 'error_message': None, + 'requires_id': False, + 'usage': False, + }, + method_result=quota_set.QuotaSet(), + expected_result=quota_set.QuotaSet() + ) + + def test_get_query(self): + self._verify( + 'openstack.resource.Resource.fetch', + self.proxy.get_quota_set, + method_args=['prj'], + method_kwargs={ + 'usage': True, + 'user_id': 'uid' + }, + expected_args=[self.proxy], + expected_kwargs={ + 'error_message': None, + 'requires_id': False, + 'usage': True, + 'user_id': 'uid' + } + ) + + def test_get_defaults(self): + self._verify( + 'openstack.resource.Resource.fetch', + self.proxy.get_quota_set_defaults, + method_args=['prj'], + expected_args=[self.proxy], + expected_kwargs={ + 'error_message': None, + 'requires_id': False, + 'base_path': '/os-quota-sets/defaults' + } + ) + + def test_reset(self): + self._verify( + 'openstack.resource.Resource.delete', + self.proxy.revert_quota_set, + method_args=['prj'], + method_kwargs={'user_id': 'uid'}, + expected_args=[self.proxy], + expected_kwargs={ + 'user_id': 'uid' + } + ) + + @mock.patch('openstack.proxy.Proxy._get_resource', autospec=True) + def test_update(self, gr_mock): + gr_mock.return_value = resource.Resource() + gr_mock.commit = mock.Mock() + self._verify( + 'openstack.resource.Resource.commit', + self.proxy.update_quota_set, + method_args=['qs'], + method_kwargs={ + 'query': {'user_id': 'uid'}, + 'a': 'b', + }, + expected_args=[self.proxy], + expected_kwargs={ + 'user_id': 'uid' + } + ) + gr_mock.assert_called_with( + self.proxy, + quota_set.QuotaSet, + 'qs', a='b' + ) diff --git a/releasenotes/notes/block-storage-qs-0e3b69be2e709b65.yaml b/releasenotes/notes/block-storage-qs-0e3b69be2e709b65.yaml new file mode 100644 index 000000000..01adc564e --- /dev/null +++ b/releasenotes/notes/block-storage-qs-0e3b69be2e709b65.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add block storage QuotaSet resource and proxy methods. From c1c8dfb79f649af9e2fed2d3888781beb72ac9c2 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 17 May 2021 20:11:51 +0200 Subject: [PATCH 2888/3836] Make metadata a common mixin Implement metadata as a common mixin. Use it in the compute service for the beginning. Change-Id: Id29da2f7a21ed3bc6a1b86052d2a8b855df8516a --- openstack/common/metadata.py | 138 ++++++++++++ openstack/common/tag.py | 2 +- openstack/compute/v2/_proxy.py | 44 ++-- openstack/compute/v2/image.py | 5 +- openstack/compute/v2/metadata.py | 101 --------- openstack/compute/v2/server.py | 4 +- openstack/tests/unit/common/test_metadata.py | 202 ++++++++++++++++++ .../tests/unit/compute/v2/test_metadata.py | 124 ----------- openstack/tests/unit/compute/v2/test_proxy.py | 14 +- 9 files changed, 375 insertions(+), 259 deletions(-) create mode 100644 openstack/common/metadata.py delete mode 100644 openstack/compute/v2/metadata.py create mode 100644 openstack/tests/unit/common/test_metadata.py delete mode 100644 openstack/tests/unit/compute/v2/test_metadata.py diff --git a/openstack/common/metadata.py b/openstack/common/metadata.py new file mode 100644 index 000000000..9f129141e --- /dev/null +++ b/openstack/common/metadata.py @@ -0,0 +1,138 @@ +# 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 import resource +from openstack import utils + + +class MetadataMixin: + + #: *Type: list of tag strings* + metadata = resource.Body('metadata', type=dict) + + def fetch_metadata(self, session): + """Lists metadata set on the entity. + + :param session: The session to use for making this request. + :return: The dictionary with metadata attached to the entity + """ + url = utils.urljoin(self.base_path, self.id, 'metadata') + response = session.get(url) + exceptions.raise_from_response(response) + json = response.json() + + if 'metadata' in json: + self._body.attributes.update({'metadata': json['metadata']}) + return self + + def set_metadata(self, session, metadata=None, replace=False): + """Sets/Replaces metadata key value pairs on the resource. + + :param session: The session to use for making this request. + :param dict metadata: Dictionary with key-value pairs + :param bool replace: Replace all resource metadata with the new object + or merge new and existing. + """ + url = utils.urljoin(self.base_path, self.id, 'metadata') + if not metadata: + metadata = {} + if not replace: + response = session.post(url, json={'metadata': metadata}) + else: + response = session.put(url, json={'metadata': metadata}) + exceptions.raise_from_response(response) + self._body.attributes.update({'metadata': metadata}) + return self + + def replace_metadata(self, session, metadata=None): + """Replaces all metadata key value pairs on the resource. + + :param session: The session to use for making this request. + :param dict metadata: Dictionary with key-value pairs + :param bool replace: Replace all resource metadata with the new object + or merge new and existing. + """ + return self.set_metadata(session, metadata, replace=True) + + def delete_metadata(self, session): + """Removes all metadata on the entity. + + :param session: The session to use for making this request. + """ + self.set_metadata(session, None, replace=True) + return self + + def get_metadata_item(self, session, key): + """Get the single metadata item on the entity. + + If the metadata key does not exist a 404 will be returned + + :param session: The session to use for making this request. + :param str key: The key of a metadata item. + """ + url = utils.urljoin(self.base_path, self.id, 'metadata', key) + response = session.get(url) + exceptions.raise_from_response( + response, error_message='Metadata item does not exist') + meta = response.json().get('meta', {}) + # Here we need to potentially init metadata + metadata = self.metadata or {} + metadata[key] = meta.get(key) + self._body.attributes.update({ + 'metadata': metadata + }) + + return self + + def set_metadata_item(self, session, key, value): + """Create or replace single metadata item to the resource. + + :param session: The session to use for making this request. + :param str key: The key for the metadata item. + :param str value: The value. + """ + url = utils.urljoin(self.base_path, self.id, 'metadata', key) + response = session.put( + url, + json={'meta': {key: value}} + ) + exceptions.raise_from_response(response) + # we do not want to update tags directly + metadata = self.metadata + metadata[key] = value + self._body.attributes.update({ + 'metadata': metadata + }) + return self + + def delete_metadata_item(self, session, key): + """Removes a single metadata item from the specified resource. + + :param session: The session to use for making this request. + :param str key: The key as a string. + """ + url = utils.urljoin(self.base_path, self.id, 'metadata', key) + response = session.delete(url) + exceptions.raise_from_response(response) + # we do not want to update tags directly + metadata = self.metadata + try: + if metadata: + metadata.pop(key) + else: + metadata = {} + except ValueError: + pass # do nothing! + self._body.attributes.update({ + 'metadata': metadata + }) + return self diff --git a/openstack/common/tag.py b/openstack/common/tag.py index 8cd7ea6bb..885f1da60 100644 --- a/openstack/common/tag.py +++ b/openstack/common/tag.py @@ -104,7 +104,7 @@ def add_tag(self, session, tag): return self def remove_tag(self, session, tag): - """Removes a single tag from the specified server. + """Removes a single tag from the specified resource. :param session: The session to use for making this request. :param tag: The tag as a string. diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 85083bea5..741321344 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -483,9 +483,7 @@ def get_image_metadata(self, image): :rtype: :class:`~openstack.compute.v2.image.Image` """ res = self._get_base_resource(image, _image.Image) - metadata = res.get_metadata(self) - result = _image.Image.existing(id=res.id, metadata=metadata) - return result + return res.fetch_metadata(self) def set_image_metadata(self, image, **metadata): """Update metadata for an image @@ -502,23 +500,28 @@ def set_image_metadata(self, image, **metadata): :rtype: :class:`~openstack.compute.v2.image.Image` """ res = self._get_base_resource(image, _image.Image) - metadata = res.set_metadata(self, **metadata) - result = _image.Image.existing(id=res.id, metadata=metadata) - return result + return res.set_metadata(self, metadata=metadata) - def delete_image_metadata(self, image, keys): + def delete_image_metadata(self, image, keys=None): """Delete metadata for an image Note: This method will do a HTTP DELETE request for every key in keys. :param image: Either the ID of an image or a :class:`~openstack.compute.v2.image.Image` instance. - :param keys: The keys to delete. + :param list keys: The keys to delete. If left empty complete metadata + will be removed. :rtype: ``None`` """ res = self._get_base_resource(image, _image.Image) - return res.delete_metadata(self, keys) + if keys is not None: + # Create a set as a snapshot of keys to avoid "changed during + # iteration" + for key in set(keys): + res.delete_metadata_item(self, key) + else: + res.delete_metadata(self) def create_keypair(self, **attrs): """Create a new keypair from attributes @@ -1256,14 +1259,12 @@ def get_server_metadata(self, server): :class:`~openstack.compute.v2.server.ServerDetail` instance. - :returns: A :class:`~openstack.compute.v2.server.Server` with only the + :returns: A :class:`~openstack.compute.v2.server.Server` with the server's metadata. All keys and values are Unicode text. :rtype: :class:`~openstack.compute.v2.server.Server` """ res = self._get_base_resource(server, _server.Server) - metadata = res.get_metadata(self) - result = _server.Server.existing(id=res.id, metadata=metadata) - return result + return res.fetch_metadata(self) def set_server_metadata(self, server, **metadata): """Update metadata for a server @@ -1280,23 +1281,28 @@ def set_server_metadata(self, server, **metadata): :rtype: :class:`~openstack.compute.v2.server.Server` """ res = self._get_base_resource(server, _server.Server) - metadata = res.set_metadata(self, **metadata) - result = _server.Server.existing(id=res.id, metadata=metadata) - return result + return res.set_metadata(self, metadata=metadata) - def delete_server_metadata(self, server, keys): + def delete_server_metadata(self, server, keys=None): """Delete metadata for a server Note: This method will do a HTTP DELETE request for every key in keys. :param server: Either the ID of a server or a :class:`~openstack.compute.v2.server.Server` instance. - :param keys: The keys to delete + :param list keys: The keys to delete. If left empty complete + metadata will be removed. :rtype: ``None`` """ res = self._get_base_resource(server, _server.Server) - return res.delete_metadata(self, keys) + if keys is not None: + # Create a set as a snapshot of keys to avoid "changed during + # iteration" + for key in set(keys): + res.delete_metadata_item(self, key) + else: + res.delete_metadata(self) def create_server_group(self, **attrs): """Create a new server group from attributes diff --git a/openstack/compute/v2/image.py b/openstack/compute/v2/image.py index 74e351b05..4ec498f38 100644 --- a/openstack/compute/v2/image.py +++ b/openstack/compute/v2/image.py @@ -9,8 +9,7 @@ # 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.compute.v2 import metadata +from openstack.common import metadata from openstack import resource @@ -38,8 +37,6 @@ class Image(resource.Resource, metadata.MetadataMixin): name = resource.Body('name') #: Timestamp when the image was created. created_at = resource.Body('created') - #: Metadata pertaining to this image. *Type: dict* - metadata = resource.Body('metadata', type=dict) #: The mimimum disk size. *Type: int* min_disk = resource.Body('minDisk', type=int) #: The minimum RAM size. *Type: int* diff --git a/openstack/compute/v2/metadata.py b/openstack/compute/v2/metadata.py deleted file mode 100644 index 0d8ad2f12..000000000 --- a/openstack/compute/v2/metadata.py +++ /dev/null @@ -1,101 +0,0 @@ -# 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 import utils - - -class MetadataMixin: - - def _metadata(self, method, key=None, clear=False, delete=False, - metadata=None): - metadata = metadata or {} - for k, v in metadata.items(): - if not isinstance(v, str): - raise ValueError("The value for %s (%s) must be " - "a text string" % (k, v)) - - # If we're in a ServerDetail, we need to pop the "detail" portion - # of the URL off and then everything else will work the same. - pos = self.base_path.find("detail") - if pos != -1: - base = self.base_path[:pos] - else: - base = self.base_path - - if key is not None: - url = utils.urljoin(base, self.id, "metadata", key) - else: - url = utils.urljoin(base, self.id, "metadata") - - kwargs = {} - if metadata or clear: - # 'meta' is the key for singular modifications. - # 'metadata' is the key for mass modifications. - key = "meta" if key is not None else "metadata" - kwargs["json"] = {key: metadata} - - headers = {"Accept": ""} if delete else {} - - response = method(url, headers=headers, **kwargs) - - # ensure Nova API has not returned us an error - exceptions.raise_from_response(response) - # DELETE doesn't return a JSON body while everything else does. - return response.json() if not delete else None - - def get_metadata(self, session): - """Retrieve metadata - - :param session: The session to use for this request. - - :returns: A dictionary of the requested metadata. All keys and values - are Unicode text. - :rtype: dict - """ - result = self._metadata(session.get) - return result["metadata"] - - def set_metadata(self, session, **metadata): - """Update metadata - - This call will replace only the metadata with the same keys - given here. Metadata with other keys will not be modified. - - :param session: The session to use for this request. - :param kwargs metadata: key/value metadata pairs to be update on - this server instance. All keys and values - are stored as Unicode. - - :returns: A dictionary of the metadata after being updated. - All keys and values are Unicode text. - :rtype: dict - """ - if not metadata: - return dict() - - result = self._metadata(session.post, metadata=metadata) - return result["metadata"] - - def delete_metadata(self, session, keys): - """Delete metadata - - Note: This method will do a HTTP DELETE request for every key in keys. - - :param session: The session to use for this request. - :param list keys: The keys to delete. - - :rtype: ``None`` - """ - for key in keys: - self._metadata(session.delete, key=key, delete=True) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index c0eab2c59..a573b062b 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -9,8 +9,8 @@ # 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.common import metadata from openstack.common import tag -from openstack.compute.v2 import metadata from openstack import exceptions from openstack.image.v2 import image from openstack import resource @@ -143,8 +143,6 @@ class Server(resource.Resource, metadata.MetadataMixin, tag.TagMixin): launched_at = resource.Body('OS-SRV-USG:launched_at') #: The maximum number of servers to create. max_count = resource.Body('max_count') - #: Metadata stored for this server. *Type: dict* - metadata = resource.Body('metadata', type=dict) #: The minimum number of servers to create. min_count = resource.Body('min_count') #: A networks object. Required parameter when there are multiple diff --git a/openstack/tests/unit/common/test_metadata.py b/openstack/tests/unit/common/test_metadata.py new file mode 100644 index 000000000..d1aac4d6d --- /dev/null +++ b/openstack/tests/unit/common/test_metadata.py @@ -0,0 +1,202 @@ +# 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 unittest import mock + +from keystoneauth1 import adapter + +from openstack.common import metadata +from openstack import exceptions +from openstack import resource +from openstack.tests.unit import base +from openstack.tests.unit.test_resource import FakeResponse + +IDENTIFIER = 'IDENTIFIER' + + +class TestMetadata(base.TestCase): + + def setUp(self): + super(TestMetadata, self).setUp() + + self.service_name = "service" + self.base_path = "base_path" + + self.metadata_result = {"metadata": {"go": "cubs", "boo": "sox"}} + self.meta_result = {"meta": {"oh": "yeah"}} + + class Test(resource.Resource, metadata.MetadataMixin): + service = self.service_name + base_path = self.base_path + resources_key = 'resources' + allow_create = True + allow_fetch = True + allow_head = True + allow_commit = True + allow_delete = True + allow_list = True + + self.test_class = Test + + self.request = mock.Mock(spec=resource._Request) + self.request.url = "uri" + self.request.body = "body" + self.request.headers = "headers" + + self.response = FakeResponse({}) + + self.sot = Test.new(id="id") + self.sot._prepare_request = mock.Mock(return_value=self.request) + self.sot._translate_response = mock.Mock() + + self.session = mock.Mock(spec=adapter.Adapter) + self.session.get = mock.Mock(return_value=self.response) + self.session.put = mock.Mock(return_value=self.response) + self.session.post = mock.Mock(return_value=self.response) + self.session.delete = mock.Mock(return_value=self.response) + + def test_metadata_attribute(self): + res = self.sot + self.assertTrue(hasattr(res, 'metadata')) + + def test_get_metadata(self): + res = self.sot + + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.links = {} + mock_response.json.return_value = {'metadata': {'foo': 'bar'}} + + self.session.get.side_effect = [mock_response] + + result = res.fetch_metadata(self.session) + # Check metadata attribute is updated + self.assertDictEqual({'foo': 'bar'}, result.metadata) + # Check passed resource is returned + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/metadata' + self.session.get.assert_called_once_with(url) + + def test_set_metadata(self): + res = self.sot + + result = res.set_metadata(self.session, {'foo': 'bar'}) + # Check metadata attribute is updated + self.assertDictEqual({'foo': 'bar'}, res.metadata) + # Check passed resource is returned + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/metadata' + self.session.post.assert_called_once_with( + url, + json={'metadata': {'foo': 'bar'}} + ) + + def test_replace_metadata(self): + res = self.sot + + result = res.replace_metadata(self.session, {'foo': 'bar'}) + # Check metadata attribute is updated + self.assertDictEqual({'foo': 'bar'}, res.metadata) + # Check passed resource is returned + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/metadata' + self.session.put.assert_called_once_with( + url, + json={'metadata': {'foo': 'bar'}} + ) + + def test_delete_all_metadata(self): + res = self.sot + + # Set some initial value to check removal + res.metadata = {'foo': 'bar'} + + result = res.delete_metadata(self.session) + # Check metadata attribute is updated + self.assertEqual({}, res.metadata) + # Check passed resource is returned + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/metadata' + self.session.put.assert_called_once_with( + url, + json={'metadata': {}}) + + def test_get_metadata_item(self): + res = self.sot + + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {'meta': {'foo': 'bar'}} + self.session.get.side_effect = [mock_response] + + result = res.get_metadata_item(self.session, 'foo') + # Check tags attribute is updated + self.assertEqual({'foo': 'bar'}, res.metadata) + # Check the passed resource is returned + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/metadata/foo' + self.session.get.assert_called_once_with(url) + + def test_delete_single_item(self): + res = self.sot + + res.metadata = {'foo': 'bar', 'foo2': 'bar2'} + + result = res.delete_metadata_item(self.session, 'foo2') + # Check metadata attribute is updated + self.assertEqual({'foo': 'bar'}, res.metadata) + # Check passed resource is returned + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/metadata/foo2' + self.session.delete.assert_called_once_with(url) + + def test_delete_signle_item_empty(self): + res = self.sot + + result = res.delete_metadata_item(self.session, 'foo2') + # Check metadata attribute is updated + self.assertEqual({}, res.metadata) + # Check passed resource is returned + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/metadata/foo2' + self.session.delete.assert_called_once_with(url) + + def test_get_metadata_item_not_exists(self): + res = self.sot + + mock_response = mock.Mock() + mock_response.status_code = 404 + mock_response.content = None + self.session.get.side_effect = [mock_response] + + # ensure we get 404 + self.assertRaises( + exceptions.NotFoundException, + res.get_metadata_item, + self.session, + 'dummy', + ) + + def test_set_metadata_item(self): + res = self.sot + + # Set some initial value to check add + res.metadata = {'foo': 'bar'} + + result = res.set_metadata_item(self.session, 'foo', 'black') + # Check metadata attribute is updated + self.assertEqual({'foo': 'black'}, res.metadata) + # Check passed resource is returned + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/metadata/foo' + self.session.put.assert_called_once_with( + url, + json={'meta': {'foo': 'black'}}) diff --git a/openstack/tests/unit/compute/v2/test_metadata.py b/openstack/tests/unit/compute/v2/test_metadata.py deleted file mode 100644 index 708df44ba..000000000 --- a/openstack/tests/unit/compute/v2/test_metadata.py +++ /dev/null @@ -1,124 +0,0 @@ -# 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 unittest import mock - -from openstack.compute.v2 import server -from openstack import exceptions -from openstack.tests.unit import base - -IDENTIFIER = 'IDENTIFIER' - -# NOTE: The implementation for metadata is done via a mixin class that both -# the server and image resources inherit from. Currently this test class -# uses the Server resource to test it. Ideally it would be parameterized -# to run with both Server and Image when the tooling for subtests starts -# working. - - -class TestMetadata(base.TestCase): - - def setUp(self): - super(TestMetadata, self).setUp() - self.metadata_result = {"metadata": {"go": "cubs", "boo": "sox"}} - self.meta_result = {"meta": {"oh": "yeah"}} - - def test_get_all_metadata_Server(self): - self._test_get_all_metadata(server.Server(id=IDENTIFIER)) - - def test_get_all_metadata_ServerDetail(self): - # This is tested explicitly so we know ServerDetail items are - # properly having /detail stripped out of their base_path. - self._test_get_all_metadata(server.ServerDetail(id=IDENTIFIER)) - - def _test_get_all_metadata(self, sot): - response = mock.Mock() - response.status_code = 200 - response.json.return_value = self.metadata_result - sess = mock.Mock() - sess.get.return_value = response - - result = sot.get_metadata(sess) - - self.assertEqual(result, self.metadata_result["metadata"]) - sess.get.assert_called_once_with( - "servers/IDENTIFIER/metadata", - headers={}) - - def test_set_metadata(self): - response = mock.Mock() - response.status_code = 200 - response.json.return_value = self.metadata_result - sess = mock.Mock() - sess.post.return_value = response - - sot = server.Server(id=IDENTIFIER) - - set_meta = {"lol": "rofl"} - - result = sot.set_metadata(sess, **set_meta) - - self.assertEqual(result, self.metadata_result["metadata"]) - sess.post.assert_called_once_with("servers/IDENTIFIER/metadata", - headers={}, - json={"metadata": set_meta}) - - def test_delete_metadata(self): - sess = mock.Mock() - response = mock.Mock() - response.status_code = 200 - sess.delete.return_value = response - - sot = server.Server(id=IDENTIFIER) - - key = "hey" - - sot.delete_metadata(sess, [key]) - - sess.delete.assert_called_once_with( - "servers/IDENTIFIER/metadata/" + key, - headers={"Accept": ""}, - ) - - def test_delete_metadata_error(self): - sess = mock.Mock() - response = mock.Mock() - response.status_code = 400 - response.content = None - sess.delete.return_value = response - - sot = server.Server(id=IDENTIFIER) - - key = "hey" - - self.assertRaises( - exceptions.BadRequestException, - sot.delete_metadata, - sess, - [key]) - - def test_set_metadata_error(self): - sess = mock.Mock() - response = mock.Mock() - response.status_code = 400 - response.content = None - sess.post.return_value = response - - sot = server.Server(id=IDENTIFIER) - - set_meta = {"lol": "rofl"} - - self.assertRaises( - exceptions.BadRequestException, - sot.set_metadata, - sess, - **set_meta) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index f9b14e010..313535760 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -918,12 +918,11 @@ def test_availability_zones_detailed(self): def test_get_all_server_metadata(self): self._verify( - "openstack.compute.v2.server.Server.get_metadata", + "openstack.compute.v2.server.Server.fetch_metadata", self.proxy.get_server_metadata, method_args=["value"], - method_result=server.Server(id="value", metadata={}), expected_args=[self.proxy], - expected_result={}) + expected_result=server.Server(id="value", metadata={})) def test_set_server_metadata(self): kwargs = {"a": "1", "b": "2"} @@ -935,15 +934,16 @@ def test_set_server_metadata(self): method_kwargs=kwargs, method_result=server.Server.existing(id=id, metadata=kwargs), expected_args=[self.proxy], - expected_kwargs=kwargs, - expected_result=kwargs) + expected_kwargs={'metadata': kwargs}, + expected_result=server.Server.existing(id=id, metadata=kwargs) + ) def test_delete_server_metadata(self): self._verify( - "openstack.compute.v2.server.Server.delete_metadata", + "openstack.compute.v2.server.Server.delete_metadata_item", self.proxy.delete_server_metadata, expected_result=None, - method_args=["value", "key"], + method_args=["value", ["key"]], expected_args=[self.proxy, "key"]) def test_create_image(self): From 3d6aff3464dfcbd757c312ed588f7e4a287da61b Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 18 May 2021 11:23:45 +0200 Subject: [PATCH 2889/3836] Add common metadata mixin to block storage and clustering We have a common metadata mixin. Time to use it in next bunch of services. Change-Id: I71e8ae04a2f98defe8c52d1366d32ac4d95ff3aa --- openstack/block_storage/v2/_proxy.py | 94 +++++++++++++++++++ openstack/block_storage/v2/snapshot.py | 5 +- openstack/block_storage/v2/volume.py | 6 +- openstack/block_storage/v3/_proxy.py | 94 +++++++++++++++++++ openstack/block_storage/v3/snapshot.py | 5 +- openstack/block_storage/v3/volume.py | 5 +- openstack/clustering/v1/_proxy.py | 48 ++++++++++ openstack/clustering/v1/cluster.py | 4 +- .../tests/unit/block_storage/v2/test_proxy.py | 63 +++++++++++++ .../tests/unit/block_storage/v3/test_proxy.py | 63 +++++++++++++ .../tests/unit/clustering/v1/test_proxy.py | 31 ++++++ 11 files changed, 403 insertions(+), 15 deletions(-) diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 72c6acf1c..39b928f3c 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -624,3 +624,97 @@ def update_quota_set(self, quota_set, query=None, **attrs): """ res = self._get_resource(_quota_set.QuotaSet, quota_set, **attrs) return res.commit(self, **query) + + def get_volume_metadata(self, volume): + """Return a dictionary of metadata for a volume + + :param volume: Either the ID of a volume or a + :class:`~openstack.block_storage.v2.volume.Volume`. + + :returns: A :class:`~openstack.block_storage.v2.volume.Volume` with the + volume's metadata. All keys and values are Unicode text. + :rtype: :class:`~openstack.block_storage.v2.volume.Volume` + """ + volume = self._get_resource(_volume.Volume, volume) + return volume.fetch_metadata(self) + + def set_volume_metadata(self, volume, **metadata): + """Update metadata for a volume + + :param volume: Either the ID of a volume or a + :class:`~openstack.block_storage.v2.volume.Volume`. + :param kwargs metadata: Key/value pairs to be updated in the volume's + metadata. No other metadata is modified by this call. All keys + and values are stored as Unicode. + + :returns: A :class:`~openstack.block_storage.v2.volume.Volume` with the + volume's metadata. All keys and values are Unicode text. + :rtype: :class:`~openstack.block_storage.v2.volume.Volume` + """ + volume = self._get_resource(_volume.Volume, volume) + return volume.set_metadata(self, metadata=metadata) + + def delete_volume_metadata(self, volume, keys=None): + """Delete metadata for a volume + + :param volume: Either the ID of a volume or a + :class:`~openstack.block_storage.v2.volume.Volume`. + :param list keys: The keys to delete. If left empty complete + metadata will be removed. + + :rtype: ``None`` + """ + volume = self._get_resource(_volume.Volume, volume) + if keys is not None: + for key in keys: + volume.delete_metadata_item(self, key) + else: + volume.delete_metadata(self) + + def get_snapshot_metadata(self, snapshot): + """Return a dictionary of metadata for a snapshot + + :param snapshot: Either the ID of a snapshot or a + :class:`~openstack.block_storage.v2.snapshot.Snapshot`. + + :returns: A + :class:`~openstack.block_storage.v2.snapshot.Snapshot` with the + snapshot's metadata. All keys and values are Unicode text. + :rtype: :class:`~openstack.block_storage.v2.snapshot.Snapshot` + """ + snapshot = self._get_resource(_snapshot.Snapshot, snapshot) + return snapshot.fetch_metadata(self) + + def set_snapshot_metadata(self, snapshot, **metadata): + """Update metadata for a snapshot + + :param snapshot: Either the ID of a snapshot or a + :class:`~openstack.block_storage.v2.snapshot.Snapshot`. + :param kwargs metadata: Key/value pairs to be updated in the snapshot's + metadata. No other metadata is modified by this call. All keys + and values are stored as Unicode. + + :returns: A + :class:`~openstack.block_storage.v2.snapshot.Snapshot` with the + snapshot's metadata. All keys and values are Unicode text. + :rtype: :class:`~openstack.block_storage.v2.snapshot.Snapshot` + """ + snapshot = self._get_resource(_snapshot.Snapshot, snapshot) + return snapshot.set_metadata(self, metadata=metadata) + + def delete_snapshot_metadata(self, snapshot, keys=None): + """Delete metadata for a snapshot + + :param snapshot: Either the ID of a snapshot or a + :class:`~openstack.block_storage.v2.snapshot.Snapshot`. + :param list keys: The keys to delete. If left empty complete + metadata will be removed. + + :rtype: ``None`` + """ + snapshot = self._get_resource(_snapshot.Snapshot, snapshot) + if keys is not None: + for key in keys: + snapshot.delete_metadata_item(self, key) + else: + snapshot.delete_metadata(self) diff --git a/openstack/block_storage/v2/snapshot.py b/openstack/block_storage/v2/snapshot.py index 7c044c9f6..c23fed107 100644 --- a/openstack/block_storage/v2/snapshot.py +++ b/openstack/block_storage/v2/snapshot.py @@ -10,13 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.common import metadata from openstack import exceptions from openstack import format from openstack import resource from openstack import utils -class Snapshot(resource.Resource): +class Snapshot(resource.Resource, metadata.MetadataMixin): resource_key = "snapshot" resources_key = "snapshots" base_path = "/snapshots" @@ -39,8 +40,6 @@ class Snapshot(resource.Resource): #: Indicate whether to create snapshot, even if the volume is attached. #: Default is ``False``. *Type: bool* is_forced = resource.Body("force", type=format.BoolStr) - #: Metadata associated with this snapshot. - metadata = resource.Body("metadata", type=dict) #: The size of the volume, in GBs. size = resource.Body("size", type=int) #: The current status of this snapshot. Potential values are creating, diff --git a/openstack/block_storage/v2/volume.py b/openstack/block_storage/v2/volume.py index 087dd4a8f..ca66cdcce 100644 --- a/openstack/block_storage/v2/volume.py +++ b/openstack/block_storage/v2/volume.py @@ -9,13 +9,13 @@ # 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.common import metadata from openstack import format from openstack import resource from openstack import utils -class Volume(resource.Resource): +class Volume(resource.Resource, metadata.MetadataMixin): resource_key = "volume" resources_key = "volumes" base_path = "/volumes" @@ -55,8 +55,6 @@ class Volume(resource.Resource): #: ``True`` if this volume is encrypted, ``False`` if not. #: *Type: bool* is_encrypted = resource.Body("encrypted", type=format.BoolStr) - #: One or more metadata key and value pairs to associate with the volume. - metadata = resource.Body("metadata") #: The volume ID that this volume's name on the back-end is based on. migration_id = resource.Body("os-vol-mig-status-attr:name_id") #: The status of this volume's migration (None means that a migration diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index cc8384ed3..fef24f6d4 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -1158,3 +1158,97 @@ def _service_cleanup(self, dry_run=True, client_status_queue=None, identified_resources=identified_resources, filters=filters, resource_evaluation_fn=resource_evaluation_fn) + + def get_volume_metadata(self, volume): + """Return a dictionary of metadata for a volume + + :param volume: Either the ID of a volume or a + :class:`~openstack.block_storage.v3.volume.Volume`. + + :returns: A :class:`~openstack.block_storage.v3.volume.Volume` with the + volume's metadata. All keys and values are Unicode text. + :rtype: :class:`~openstack.block_storage.v3.volume.Volume` + """ + volume = self._get_resource(_volume.Volume, volume) + return volume.fetch_metadata(self) + + def set_volume_metadata(self, volume, **metadata): + """Update metadata for a volume + + :param volume: Either the ID of a volume or a + :class:`~openstack.block_storage.v3.volume.Volume`. + :param kwargs metadata: Key/value pairs to be updated in the volume's + metadata. No other metadata is modified by this call. All keys + and values are stored as Unicode. + + :returns: A :class:`~openstack.block_storage.v3.volume.Volume` with the + volume's metadata. All keys and values are Unicode text. + :rtype: :class:`~openstack.block_storage.v3.volume.Volume` + """ + volume = self._get_resource(_volume.Volume, volume) + return volume.set_metadata(self, metadata=metadata) + + def delete_volume_metadata(self, volume, keys=None): + """Delete metadata for a volume + + :param volume: Either the ID of a volume or a + :class:`~openstack.block_storage.v3.volume.Volume`. + :param list keys: The keys to delete. If left empty complete + metadata will be removed. + + :rtype: ``None`` + """ + volume = self._get_resource(_volume.Volume, volume) + if keys is not None: + for key in keys: + volume.delete_metadata_item(self, key) + else: + volume.delete_metadata(self) + + def get_snapshot_metadata(self, snapshot): + """Return a dictionary of metadata for a snapshot + + :param snapshot: Either the ID of a snapshot or a + :class:`~openstack.block_storage.v3.snapshot.Snapshot`. + + :returns: A + :class:`~openstack.block_storage.v3.snapshot.Snapshot` with the + snapshot's metadata. All keys and values are Unicode text. + :rtype: :class:`~openstack.block_storage.v3.snapshot.Snapshot` + """ + snapshot = self._get_resource(_snapshot.Snapshot, snapshot) + return snapshot.fetch_metadata(self) + + def set_snapshot_metadata(self, snapshot, **metadata): + """Update metadata for a snapshot + + :param snapshot: Either the ID of a snapshot or a + :class:`~openstack.block_storage.v3.snapshot.Snapshot`. + :param kwargs metadata: Key/value pairs to be updated in the snapshot's + metadata. No other metadata is modified by this call. All keys + and values are stored as Unicode. + + :returns: A + :class:`~openstack.block_storage.v3.snapshot.Snapshot` with the + snapshot's metadata. All keys and values are Unicode text. + :rtype: :class:`~openstack.block_storage.v3.snapshot.Snapshot` + """ + snapshot = self._get_resource(_snapshot.Snapshot, snapshot) + return snapshot.set_metadata(self, metadata=metadata) + + def delete_snapshot_metadata(self, snapshot, keys=None): + """Delete metadata for a snapshot + + :param snapshot: Either the ID of a snapshot or a + :class:`~openstack.block_storage.v3.snapshot.Snapshot`. + :param list keys: The keys to delete. If left empty complete + metadata will be removed. + + :rtype: ``None`` + """ + snapshot = self._get_resource(_snapshot.Snapshot, snapshot) + if keys is not None: + for key in keys: + snapshot.delete_metadata_item(self, key) + else: + snapshot.delete_metadata(self) diff --git a/openstack/block_storage/v3/snapshot.py b/openstack/block_storage/v3/snapshot.py index 3dac8b15f..3f025473b 100644 --- a/openstack/block_storage/v3/snapshot.py +++ b/openstack/block_storage/v3/snapshot.py @@ -10,13 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.common import metadata from openstack import exceptions from openstack import format from openstack import resource from openstack import utils -class Snapshot(resource.Resource): +class Snapshot(resource.Resource, metadata.MetadataMixin): resource_key = "snapshot" resources_key = "snapshots" base_path = "/snapshots" @@ -40,8 +41,6 @@ class Snapshot(resource.Resource): #: Indicate whether to create snapshot, even if the volume is attached. #: Default is ``False``. *Type: bool* is_forced = resource.Body("force", type=format.BoolStr) - #: Metadata associated with this snapshot. - metadata = resource.Body("metadata", type=dict) #: The percentage of completeness the snapshot is currently at. progress = resource.Body("os-extended-snapshot-attributes:progress") #: The project ID this snapshot is associated with. diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index c86f171a4..c6814d2be 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -10,13 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.common import metadata from openstack import exceptions from openstack import format from openstack import resource from openstack import utils -class Volume(resource.Resource): +class Volume(resource.Resource, metadata.MetadataMixin): resource_key = "volume" resources_key = "volumes" base_path = "/volumes" @@ -57,8 +58,6 @@ class Volume(resource.Resource): #: ``True`` if this volume is encrypted, ``False`` if not. #: *Type: bool* is_encrypted = resource.Body("encrypted", type=format.BoolStr) - #: One or more metadata key and value pairs to associate with the volume. - metadata = resource.Body("metadata") #: The volume ID that this volume's name on the back-end is based on. migration_id = resource.Body("os-vol-mig-status-attr:name_id") #: The status of this volume's migration (None means that a migration diff --git a/openstack/clustering/v1/_proxy.py b/openstack/clustering/v1/_proxy.py index 51f877295..e7eda23e6 100644 --- a/openstack/clustering/v1/_proxy.py +++ b/openstack/clustering/v1/_proxy.py @@ -183,6 +183,7 @@ def validate_profile(self, **attrs): """ return self._create(_profile.ProfileValidate, **attrs) + # ====== CLUSTERS ====== def create_cluster(self, **attrs): """Create a new cluster from attributes. @@ -282,6 +283,53 @@ def update_cluster(self, cluster, **attrs): """ return self._update(_cluster.Cluster, cluster, **attrs) + def get_cluster_metadata(self, cluster): + """Return a dictionary of metadata for a cluster + + :param cluster: Either the ID of a cluster or a + :class:`~openstack.clustering.v3.cluster.Cluster`. + + :returns: A :class:`~openstack.clustering.v3.cluster.Cluster` with the + cluster's metadata. All keys and values are Unicode text. + :rtype: :class:`~openstack.clustering.v3.cluster.Cluster` + """ + cluster = self._get_resource(_cluster.Cluster, cluster) + return cluster.fetch_metadata(self) + + def set_cluster_metadata(self, cluster, **metadata): + """Update metadata for a cluster + + :param cluster: Either the ID of a cluster or a + :class:`~openstack.clustering.v3.cluster.Cluster`. + :param kwargs metadata: Key/value pairs to be updated in the cluster's + metadata. No other metadata is modified by this call. All keys + and values are stored as Unicode. + + + :returns: A :class:`~openstack.clustering.v3.cluster.Cluster` with the + cluster's metadata. All keys and values are Unicode text. + :rtype: :class:`~openstack.clustering.v3.cluster.Cluster` + """ + cluster = self._get_resource(_cluster.Cluster, cluster) + return cluster.set_metadata(self, metadata=metadata) + + def delete_cluster_metadata(self, cluster, keys=None): + """Delete metadata for a cluster + + :param cluster: Either the ID of a cluster or a + :class:`~openstack.clustering.v3.cluster.Cluster`. + :param list keys: The keys to delete. If left empty complete + metadata will be removed. + + :rtype: ``None`` + """ + cluster = self._get_resource(_cluster.Cluster, cluster) + if keys is not None: + for key in keys: + cluster.delete_metadata_item(self, key) + else: + cluster.delete_metadata(self) + def add_nodes_to_cluster(self, cluster, nodes): """Add nodes to a cluster. diff --git a/openstack/clustering/v1/cluster.py b/openstack/clustering/v1/cluster.py index 2e8663232..09e866272 100644 --- a/openstack/clustering/v1/cluster.py +++ b/openstack/clustering/v1/cluster.py @@ -9,13 +9,13 @@ # 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.clustering.v1 import _async_resource +from openstack.common import metadata from openstack import resource from openstack import utils -class Cluster(_async_resource.AsyncResource): +class Cluster(_async_resource.AsyncResource, metadata.MetadataMixin): resource_key = 'cluster' resources_key = 'clusters' base_path = '/clusters' diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index 4513da4b9..897addcf8 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -65,6 +65,37 @@ def test_volume_delete_force(self): expected_args=[self.proxy] ) + def test_get_volume_metadata(self): + self._verify( + "openstack.block_storage.v2.volume.Volume.fetch_metadata", + self.proxy.get_volume_metadata, + method_args=["value"], + expected_args=[self.proxy], + expected_result=volume.Volume(id="value", metadata={})) + + def test_set_volume_metadata(self): + kwargs = {"a": "1", "b": "2"} + id = "an_id" + self._verify( + "openstack.block_storage.v2.volume.Volume.set_metadata", + self.proxy.set_volume_metadata, + method_args=[id], + method_kwargs=kwargs, + method_result=volume.Volume.existing( + id=id, metadata=kwargs), + expected_args=[self.proxy], + expected_kwargs={'metadata': kwargs}, + expected_result=volume.Volume.existing( + id=id, metadata=kwargs)) + + def test_delete_volume_metadata(self): + self._verify( + "openstack.block_storage.v2.volume.Volume.delete_metadata_item", + self.proxy.delete_volume_metadata, + expected_result=None, + method_args=["value", ["key"]], + expected_args=[self.proxy, "key"]) + def test_backend_pools(self): self.verify_list(self.proxy.backend_pools, stats.Pools) @@ -269,6 +300,38 @@ def test_reset(self): method_args=["value", "new_status"], expected_args=[self.proxy, "new_status"]) + def test_get_snapshot_metadata(self): + self._verify( + "openstack.block_storage.v2.snapshot.Snapshot.fetch_metadata", + self.proxy.get_snapshot_metadata, + method_args=["value"], + expected_args=[self.proxy], + expected_result=snapshot.Snapshot(id="value", metadata={})) + + def test_set_snapshot_metadata(self): + kwargs = {"a": "1", "b": "2"} + id = "an_id" + self._verify( + "openstack.block_storage.v2.snapshot.Snapshot.set_metadata", + self.proxy.set_snapshot_metadata, + method_args=[id], + method_kwargs=kwargs, + method_result=snapshot.Snapshot.existing( + id=id, metadata=kwargs), + expected_args=[self.proxy], + expected_kwargs={'metadata': kwargs}, + expected_result=snapshot.Snapshot.existing( + id=id, metadata=kwargs)) + + def test_delete_snapshot_metadata(self): + self._verify( + "openstack.block_storage.v2.snapshot.Snapshot." + "delete_metadata_item", + self.proxy.delete_snapshot_metadata, + expected_result=None, + method_args=["value", ["key"]], + expected_args=[self.proxy, "key"]) + class TestType(TestVolumeProxy): def test_type_get(self): diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 46dfe044e..ab3eea369 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -112,6 +112,37 @@ def test_group_type_update(self): def test_extensions(self): self.verify_list(self.proxy.extensions, extension.Extension) + def test_get_volume_metadata(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.fetch_metadata", + self.proxy.get_volume_metadata, + method_args=["value"], + expected_args=[self.proxy], + expected_result=volume.Volume(id="value", metadata={})) + + def test_set_volume_metadata(self): + kwargs = {"a": "1", "b": "2"} + id = "an_id" + self._verify( + "openstack.block_storage.v3.volume.Volume.set_metadata", + self.proxy.set_volume_metadata, + method_args=[id], + method_kwargs=kwargs, + method_result=volume.Volume.existing( + id=id, metadata=kwargs), + expected_args=[self.proxy], + expected_kwargs={'metadata': kwargs}, + expected_result=volume.Volume.existing( + id=id, metadata=kwargs)) + + def test_delete_volume_metadata(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.delete_metadata_item", + self.proxy.delete_volume_metadata, + expected_result=None, + method_args=["value", ["key"]], + expected_args=[self.proxy, "key"]) + def test_volume_wait_for(self): value = volume.Volume(id='1234') self.verify_wait_for_status( @@ -451,6 +482,38 @@ def test_set_status_percentage(self): method_args=["value", "new_status", "per"], expected_args=[self.proxy, "new_status", "per"]) + def test_get_snapshot_metadata(self): + self._verify( + "openstack.block_storage.v3.snapshot.Snapshot.fetch_metadata", + self.proxy.get_snapshot_metadata, + method_args=["value"], + expected_args=[self.proxy], + expected_result=snapshot.Snapshot(id="value", metadata={})) + + def test_set_snapshot_metadata(self): + kwargs = {"a": "1", "b": "2"} + id = "an_id" + self._verify( + "openstack.block_storage.v3.snapshot.Snapshot.set_metadata", + self.proxy.set_snapshot_metadata, + method_args=[id], + method_kwargs=kwargs, + method_result=snapshot.Snapshot.existing( + id=id, metadata=kwargs), + expected_args=[self.proxy], + expected_kwargs={'metadata': kwargs}, + expected_result=snapshot.Snapshot.existing( + id=id, metadata=kwargs)) + + def test_delete_snapshot_metadata(self): + self._verify( + "openstack.block_storage.v3.snapshot.Snapshot." + "delete_metadata_item", + self.proxy.delete_snapshot_metadata, + expected_result=None, + method_args=["value", ["key"]], + expected_args=[self.proxy, "key"]) + class TestType(TestVolumeProxy): def test_type_get(self): diff --git a/openstack/tests/unit/clustering/v1/test_proxy.py b/openstack/tests/unit/clustering/v1/test_proxy.py index 7c8f62dc5..2f35248a5 100644 --- a/openstack/tests/unit/clustering/v1/test_proxy.py +++ b/openstack/tests/unit/clustering/v1/test_proxy.py @@ -398,3 +398,34 @@ def test_wait_for_delete_params(self, mock_wait): self.proxy.wait_for_delete(mock_resource, 1, 2) mock_wait.assert_called_once_with(self.proxy, mock_resource, 1, 2) + + def test_get_cluster_metadata(self): + self._verify( + "openstack.clustering.v1.cluster.Cluster.fetch_metadata", + self.proxy.get_cluster_metadata, + method_args=["value"], + expected_args=[self.proxy], + expected_result=cluster.Cluster(id="value", metadata={})) + + def test_set_cluster_metadata(self): + kwargs = {"a": "1", "b": "2"} + id = "an_id" + self._verify( + "openstack.clustering.v1.cluster.Cluster.set_metadata", + self.proxy.set_cluster_metadata, + method_args=[id], + method_kwargs=kwargs, + method_result=cluster.Cluster.existing( + id=id, metadata=kwargs), + expected_args=[self.proxy], + expected_kwargs={'metadata': kwargs}, + expected_result=cluster.Cluster.existing( + id=id, metadata=kwargs)) + + def test_delete_cluster_metadata(self): + self._verify( + "openstack.clustering.v1.cluster.Cluster.delete_metadata_item", + self.proxy.delete_cluster_metadata, + expected_result=None, + method_args=["value", ["key"]], + expected_args=[self.proxy, "key"]) From 1909df1f4219fbf2a1bb0f9ba087ef39758ffded Mon Sep 17 00:00:00 2001 From: Polina Gubina Date: Thu, 8 Jul 2021 15:45:24 +0300 Subject: [PATCH 2890/3836] Vpn ike policy resource Change-Id: I8a69a1d32d4ba822e824864d9d22919a549c02d6 --- doc/source/user/proxies/network.rst | 9 ++ doc/source/user/resources/network/index.rst | 1 + .../user/resources/network/v2/ikepolicy.rst | 13 +++ openstack/network/v2/_proxy.py | 85 +++++++++++++++++++ openstack/network/v2/ikepolicy.py | 63 ++++++++++++++ .../tests/unit/network/v2/test_ikepolicy.py | 59 +++++++++++++ openstack/tests/unit/network/v2/test_proxy.py | 29 +++++++ 7 files changed, 259 insertions(+) create mode 100644 doc/source/user/resources/network/v2/ikepolicy.rst create mode 100644 openstack/network/v2/ikepolicy.py create mode 100644 openstack/tests/unit/network/v2/test_ikepolicy.py diff --git a/doc/source/user/proxies/network.rst b/doc/source/user/proxies/network.rst index 3c5f76a76..6688ee062 100644 --- a/doc/source/user/proxies/network.rst +++ b/doc/source/user/proxies/network.rst @@ -243,6 +243,15 @@ IPSecSiteConnection Operations delete_vpn_ipsec_site_connection, get_vpn_ipsec_site_connection, find_vpn_ipsec_site_connection, vpn_ipsec_site_connections +IkePolicy Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + :noindex: + :members: create_vpn_ikepolicy, update_vpn_ikepolicy, + delete_vpn_ikepolicy, get_vpn_ikepolicy, + find_vpn_ikepolicy, vpn_ikepolicies + Extension Operations ^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/network/index.rst b/doc/source/user/resources/network/index.rst index 75fb5d928..3e54e0ed1 100644 --- a/doc/source/user/resources/network/index.rst +++ b/doc/source/user/resources/network/index.rst @@ -14,6 +14,7 @@ Network Resources v2/floating_ip v2/health_monitor v2/ipsec_site_connection + v2/ikepolicy v2/listener v2/load_balancer v2/metering_label diff --git a/doc/source/user/resources/network/v2/ikepolicy.rst b/doc/source/user/resources/network/v2/ikepolicy.rst new file mode 100644 index 000000000..b6bc623ef --- /dev/null +++ b/doc/source/user/resources/network/v2/ikepolicy.rst @@ -0,0 +1,13 @@ +openstack.network.v2.ikepolicy +============================== + +.. automodule:: openstack.network.v2.ikepolicy + +The IkePolicy Class +------------------- + +The ``IkePolicy`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.ikepolicy.IkePolicy + :members: diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index ac9d735d4..86da25270 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -24,6 +24,7 @@ from openstack.network.v2 import flavor as _flavor from openstack.network.v2 import floating_ip as _floating_ip from openstack.network.v2 import health_monitor as _health_monitor +from openstack.network.v2 import ikepolicy as _ikepolicy from openstack.network.v2 import ipsec_site_connection as \ _ipsec_site_connection from openstack.network.v2 import l3_conntrack_helper as _l3_conntrack_helper @@ -1047,6 +1048,90 @@ def delete_vpn_ipsec_site_connection(self, ipsec_site_connection, self._delete(_ipsec_site_connection.IPSecSiteConnection, ipsec_site_connection, ignore_missing=ignore_missing) + def create_vpn_ikepolicy(self, **attrs): + """Create a new ike policy from attributes + + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.network.v2.ikepolicy.IkePolicy`, comprised of + the properties on the IkePolicy class. + + :returns: The results of ike policy creation :rtype: + :class:`~openstack.network.v2.ikepolicy.IkePolicy` + """ + return self._create(_ikepolicy.IkePolicy, + **attrs) + + def find_vpn_ikepolicy(self, name_or_id, + ignore_missing=True, **args): + """Find a single ike policy + + :param name_or_id: The name or ID of an ike policy. + :param bool ignore_missing: When set to ``False`` :class:`~openstack. + exceptions.ResourceNotFound` will be raised when the resource does + not exist. When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods such as query filters. + :returns: One :class:`~openstack.network.v2.ikepolicy.IkePolicy` or + None. + """ + return self._find(_ikepolicy.IkePolicy, + name_or_id, ignore_missing=ignore_missing, **args) + + def get_vpn_ikepolicy(self, ikepolicy): + """Get a single ike policy + + :param ikepolicy: The value can be the ID of an ikepolicy or a + :class:`~openstack.network.v2.ikepolicy.IkePolicy` instance. + + :returns: One :class:`~openstack.network.v2.ikepolicy.IkePolicy` + :rtype: :class:`~openstack.network.v2.ikepolicy.IkePolicy` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource can be found. + """ + return self._get(_ikepolicy.IkePolicy, ikepolicy) + + def vpn_ikepolicies(self, **query): + """Return a generator of ike policy + + :param dict query: Optional query parameters to be sent to limit the + resources being returned. + + :returns: A generator of ike policy objects + :rtype: :class:`~openstack.network.v2.ikepolicy.IkePolicy` + """ + return self._list(_ikepolicy.IkePolicy, **query) + + def update_vpn_ikepolicy(self, ikepolicy, **attrs): + """Update a ike policy + + :ikepolicy: Either the id of an ike policy or a + :class:`~openstack.network.v2.ikepolicy.IkePolicy` instance. + :param dict attrs: The attributes to update on the ike policy + represented by ``ikepolicy``. + + :returns: The updated ike policy + :rtype: :class:`~openstack.network.v2.ikepolicy.IkePolicy` + """ + return self._update(_ikepolicy.IkePolicy, ikepolicy, **attrs) + + def delete_vpn_ikepolicy(self, ikepolicy, ignore_missing=True): + """Delete a ikepolicy + + :param ikepolicy: The value can be either the ID of an ike policy, or + a :class:`~openstack.network.v2.ikepolicy.IkePolicy` instance. + :param bool ignore_missing: + When set to ``False`` :class:`~openstack.exceptions. + ResourceNotFound` will be raised when the ike policy + does not exist. + When set to ``True``, no exception will be set when attempting to + delete a nonexistent ike policy. + + :returns: ``None`` + """ + self._delete(_ikepolicy.IkePolicy, ikepolicy, + ignore_missing=ignore_missing) + def create_listener(self, **attrs): """Create a new listener from attributes diff --git a/openstack/network/v2/ikepolicy.py b/openstack/network/v2/ikepolicy.py new file mode 100644 index 000000000..b9ca2b529 --- /dev/null +++ b/openstack/network/v2/ikepolicy.py @@ -0,0 +1,63 @@ +# 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 resource + + +class IkePolicy(resource.Resource): + resource_key = 'ikepolicy' + resources_key = 'ikepolicies' + base_path = '/vpn/ikepolicies' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + # Properties + #: The authentication hash algorithm. Valid values are sha1, + # sha256, sha384, sha512. The default is sha1. + auth_algorithm = resource.Body('auth_algorithm') + #: A human-readable description for the resource. + # Default is an empty string. + description = resource.Body('description') + #: The encryption algorithm. A valid value is 3des, aes-128, + # aes-192, aes-256, and so on. Default is aes-128. + encryption_algorithm = resource.Body('encryption_algorithm') + #: The IKE version. A valid value is v1 or v2. Default is v1. + ike_version = resource.Body('ike_version') + #: The lifetime of the security association. The lifetime consists + # of a unit and integer value. You can omit either the unit or value + # portion of the lifetime. Default unit is seconds and + # default value is 3600. + lifetime = resource.Body('lifetime', type=dict) + #: Human-readable name of the resource. Default is an empty string. + name = resource.Body('name') + #: Perfect forward secrecy (PFS). A valid value is Group2, + # Group5, Group14, and so on. Default is Group5. + pfs = resource.Body('pfs') + #: The ID of the project. + project_id = resource.Body('project_id') + #: The IKE mode. A valid value is main, which is the default. + phase1_negotiation_mode = resource.Body('phase1_negotiation_mode') + #: The units for the lifetime of the security association. + # The lifetime consists of a unit and integer value. + # You can omit either the unit or value portion of the lifetime. + # Default unit is seconds and default value is 3600. + units = resource.Body('units') + #: The lifetime value, as a positive integer. The lifetime + # consists of a unit and integer value. + # You can omit either the unit or value portion of the lifetime. + # Default unit is seconds and default value is 3600. + value = resource.Body('value', type=int) diff --git a/openstack/tests/unit/network/v2/test_ikepolicy.py b/openstack/tests/unit/network/v2/test_ikepolicy.py new file mode 100644 index 000000000..f17cfd29a --- /dev/null +++ b/openstack/tests/unit/network/v2/test_ikepolicy.py @@ -0,0 +1,59 @@ +# 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.network.v2 import ikepolicy +from openstack.tests.unit import base + + +EXAMPLE = { + "auth_algorithm": "1", + "description": "2", + "encryption_algorithm": "3", + "ike_version": "4", + "lifetime": {'a': 5}, + "name": "5", + "pfs": "6", + "project_id": "7", + "phase1_negotiation_mode": "8", + "units": "9", + "value": 10 +} + + +class TestIkePolicy(base.TestCase): + + def test_basic(self): + sot = ikepolicy.IkePolicy() + self.assertEqual('ikepolicy', sot.resource_key) + self.assertEqual('ikepolicies', sot.resources_key) + self.assertEqual('/vpn/ikepolicies', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = ikepolicy.IkePolicy(**EXAMPLE) + self.assertEqual(EXAMPLE['auth_algorithm'], sot.auth_algorithm) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['encryption_algorithm'], + sot.encryption_algorithm) + self.assertEqual(EXAMPLE['ike_version'], sot.ike_version) + self.assertEqual(EXAMPLE['lifetime'], sot.lifetime) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['pfs'], sot.pfs) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + self.assertEqual(EXAMPLE['phase1_negotiation_mode'], + sot.phase1_negotiation_mode) + self.assertEqual(EXAMPLE['units'], sot.units) + self.assertEqual(EXAMPLE['value'], sot.value) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 3b040a618..67c866345 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -26,6 +26,7 @@ from openstack.network.v2 import flavor from openstack.network.v2 import floating_ip from openstack.network.v2 import health_monitor +from openstack.network.v2 import ikepolicy from openstack.network.v2 import ipsec_site_connection from openstack.network.v2 import l3_conntrack_helper from openstack.network.v2 import listener @@ -268,6 +269,34 @@ def test_ipsec_site_connection_update(self): self.verify_update(self.proxy.update_vpn_ipsec_site_connection, ipsec_site_connection.IPSecSiteConnection) + def test_ikepolicy_create_attrs(self): + self.verify_create(self.proxy.create_vpn_ikepolicy, + ikepolicy.IkePolicy) + + def test_ikepolicy_delete(self): + self.verify_delete(self.proxy.delete_vpn_ikepolicy, + ikepolicy.IkePolicy, False) + + def test_ikepolicy_delete_ignore(self): + self.verify_delete(self.proxy.delete_vpn_ikepolicy, + ikepolicy.IkePolicy, True) + + def test_ikepolicy_find(self): + self.verify_find(self.proxy.find_vpn_ikepolicy, + ikepolicy.IkePolicy) + + def test_ikepolicy_get(self): + self.verify_get(self.proxy.get_vpn_ikepolicy, + ikepolicy.IkePolicy) + + def test_ikepolicies(self): + self.verify_list(self.proxy.vpn_ikepolicies, + ikepolicy.IkePolicy) + + def test_ikepolicy_update(self): + self.verify_update(self.proxy.update_vpn_ikepolicy, + ikepolicy.IkePolicy) + def test_listener_create_attrs(self): self.verify_create(self.proxy.create_listener, listener.Listener) From 0bc47cdb933e12fd142747371720e8a1f74cb795 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 22 Jul 2021 18:00:25 +0200 Subject: [PATCH 2891/3836] Extend has_version function to accept version parameter We might need not only to check whether the service is supported, but also whether specific version of the service is working. Extend functional tests of the block-storage to actually check specific version. Change-Id: Ie71c4f818d5adc82466e06d994c258af271b5793 --- openstack/cloud/openstackcloud.py | 15 +++++++++++---- .../tests/functional/block_storage/v2/base.py | 2 +- .../tests/functional/block_storage/v3/base.py | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 96e7bca44..eabbbd276 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -712,9 +712,11 @@ def _get_and_munchify(self, key, data): def get_name(self): return self.name - def get_session_endpoint(self, service_key): + def get_session_endpoint(self, service_key, **kwargs): + if not kwargs: + kwargs = {} try: - return self.config.get_session_endpoint(service_key) + return self.config.get_session_endpoint(service_key, **kwargs) except keystoneauth1.exceptions.catalog.EndpointNotFound as e: self.log.debug( "Endpoint not found in %s cloud: %s", self.name, str(e)) @@ -731,7 +733,7 @@ def get_session_endpoint(self, service_key): error=str(e))) return endpoint - def has_service(self, service_key): + def has_service(self, service_key, version=None): if not self.config.has_service(service_key): # TODO(mordred) add a stamp here so that we only report this once if not (service_key in self._disable_warnings @@ -742,7 +744,12 @@ def has_service(self, service_key): self._disable_warnings[service_key] = True return False try: - endpoint = self.get_session_endpoint(service_key) + kwargs = dict() + # If a specific version was requested - try it + if version is not None: + kwargs['min_version'] = version + kwargs['max_version'] = version + endpoint = self.get_session_endpoint(service_key, **kwargs) except exc.OpenStackCloudException: return False if endpoint: diff --git a/openstack/tests/functional/block_storage/v2/base.py b/openstack/tests/functional/block_storage/v2/base.py index 4d7f0c099..6da35eb40 100644 --- a/openstack/tests/functional/block_storage/v2/base.py +++ b/openstack/tests/functional/block_storage/v2/base.py @@ -22,5 +22,5 @@ def setUp(self): self._set_user_cloud(block_storage_api_version='2') self._set_operator_cloud(block_storage_api_version='2') - if not self.user_cloud.has_service('block-storage'): + if not self.user_cloud.has_service('block-storage', '2'): self.skipTest('block-storage service not supported by cloud') diff --git a/openstack/tests/functional/block_storage/v3/base.py b/openstack/tests/functional/block_storage/v3/base.py index adab8dba8..7bb01ce73 100644 --- a/openstack/tests/functional/block_storage/v3/base.py +++ b/openstack/tests/functional/block_storage/v3/base.py @@ -22,5 +22,5 @@ def setUp(self): self._set_user_cloud(block_storage_api_version='3') self._set_operator_cloud(block_storage_api_version='3') - if not self.user_cloud.has_service('block-storage'): + if not self.user_cloud.has_service('block-storage', '3'): self.skipTest('block-storage service not supported by cloud') From e0372b72af8c5f471fc17e53434d7a814ca958bd Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 11 Jun 2021 16:49:49 +0200 Subject: [PATCH 2892/3836] Temporarily disable nodepool job nodepool job is blocking us for quite some time with timeouts. Let us disable it for now and rather fix issues later (on a separate branch anyway) rather then continue waiting. Change-Id: Id76a13b41ffcf6fdaff58ddce6597dc1b1ea347b --- .zuul.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 6de2c8689..ddd5183ab 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -387,7 +387,7 @@ jobs: - opendev-buildset-registry - nodepool-build-image-siblings - - dib-nodepool-functional-openstack-centos-8-stream-src + # - dib-nodepool-functional-openstack-centos-8-stream-src - openstacksdk-functional-devstack - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-senlin @@ -412,7 +412,7 @@ jobs: - opendev-buildset-registry - nodepool-build-image-siblings - - dib-nodepool-functional-openstack-centos-8-stream-src + # - dib-nodepool-functional-openstack-centos-8-stream-src - openstacksdk-functional-devstack - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-senlin From a8c3cb29f29470529e5ebbd0c02ea8e34aca0b69 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 22 Jul 2021 18:00:25 +0200 Subject: [PATCH 2893/3836] Extend has_version function to accept version parameter We might need not only to check whether the service is supported, but also whether specific version of the service is working. Extend functional tests of the block-storage to actually check specific version. Change-Id: Ie71c4f818d5adc82466e06d994c258af271b5793 (cherry picked from commit 0bc47cdb933e12fd142747371720e8a1f74cb795) --- openstack/cloud/openstackcloud.py | 15 +++++++++++---- .../tests/functional/block_storage/v2/base.py | 2 +- .../tests/functional/block_storage/v3/base.py | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 96e7bca44..eabbbd276 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -712,9 +712,11 @@ def _get_and_munchify(self, key, data): def get_name(self): return self.name - def get_session_endpoint(self, service_key): + def get_session_endpoint(self, service_key, **kwargs): + if not kwargs: + kwargs = {} try: - return self.config.get_session_endpoint(service_key) + return self.config.get_session_endpoint(service_key, **kwargs) except keystoneauth1.exceptions.catalog.EndpointNotFound as e: self.log.debug( "Endpoint not found in %s cloud: %s", self.name, str(e)) @@ -731,7 +733,7 @@ def get_session_endpoint(self, service_key): error=str(e))) return endpoint - def has_service(self, service_key): + def has_service(self, service_key, version=None): if not self.config.has_service(service_key): # TODO(mordred) add a stamp here so that we only report this once if not (service_key in self._disable_warnings @@ -742,7 +744,12 @@ def has_service(self, service_key): self._disable_warnings[service_key] = True return False try: - endpoint = self.get_session_endpoint(service_key) + kwargs = dict() + # If a specific version was requested - try it + if version is not None: + kwargs['min_version'] = version + kwargs['max_version'] = version + endpoint = self.get_session_endpoint(service_key, **kwargs) except exc.OpenStackCloudException: return False if endpoint: diff --git a/openstack/tests/functional/block_storage/v2/base.py b/openstack/tests/functional/block_storage/v2/base.py index 4d7f0c099..6da35eb40 100644 --- a/openstack/tests/functional/block_storage/v2/base.py +++ b/openstack/tests/functional/block_storage/v2/base.py @@ -22,5 +22,5 @@ def setUp(self): self._set_user_cloud(block_storage_api_version='2') self._set_operator_cloud(block_storage_api_version='2') - if not self.user_cloud.has_service('block-storage'): + if not self.user_cloud.has_service('block-storage', '2'): self.skipTest('block-storage service not supported by cloud') diff --git a/openstack/tests/functional/block_storage/v3/base.py b/openstack/tests/functional/block_storage/v3/base.py index adab8dba8..7bb01ce73 100644 --- a/openstack/tests/functional/block_storage/v3/base.py +++ b/openstack/tests/functional/block_storage/v3/base.py @@ -22,5 +22,5 @@ def setUp(self): self._set_user_cloud(block_storage_api_version='3') self._set_operator_cloud(block_storage_api_version='3') - if not self.user_cloud.has_service('block-storage'): + if not self.user_cloud.has_service('block-storage', '3'): self.skipTest('block-storage service not supported by cloud') From 88e520f5ad3013a46edec2de52ae1d669d6eb672 Mon Sep 17 00:00:00 2001 From: Kafilat Adeleke Date: Fri, 18 Jun 2021 15:25:58 +0000 Subject: [PATCH 2894/3836] Adds storage pools to shared file system Change-Id: Ia899a7b829302d8a516eb2f3e4ef7f293be26984 --- .../user/proxies/shared_file_system.rst | 11 +++ .../resources/shared_file_system/index.rst | 1 + .../shared_file_system/v2/storage_pool.rst | 13 ++++ openstack/shared_file_system/v2/_proxy.py | 22 ++++++ .../shared_file_system/v2/storage_pool.py | 41 ++++++++++ .../shared_file_system/test_storage_pool.py | 24 ++++++ .../unit/shared_file_system/v2/test_proxy.py | 17 +++++ .../v2/test_storage_pool.py | 74 +++++++++++++++++++ ...-pool-to-shared-file-ad45da1b2510b412.yaml | 5 ++ 9 files changed, 208 insertions(+) create mode 100644 doc/source/user/resources/shared_file_system/v2/storage_pool.rst create mode 100644 openstack/shared_file_system/v2/storage_pool.py create mode 100644 openstack/tests/functional/shared_file_system/test_storage_pool.py create mode 100644 openstack/tests/unit/shared_file_system/v2/test_storage_pool.py create mode 100644 releasenotes/notes/add-storage-pool-to-shared-file-ad45da1b2510b412.yaml diff --git a/doc/source/user/proxies/shared_file_system.rst b/doc/source/user/proxies/shared_file_system.rst index 759d19a39..6d71a2521 100644 --- a/doc/source/user/proxies/shared_file_system.rst +++ b/doc/source/user/proxies/shared_file_system.rst @@ -33,3 +33,14 @@ service. .. autoclass:: openstack.shared_file_system.v2._proxy.Proxy :noindex: :members: shares, get_share, delete_share, update_share, create_share + + +Shared File System Storage Pools +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Interact with the storage pool statistics exposed by the Shared File +Systems Service. + +.. autoclass:: openstack.shared_file_system.v2._proxy.Proxy + :noindex: + :members: storage_pools diff --git a/doc/source/user/resources/shared_file_system/index.rst b/doc/source/user/resources/shared_file_system/index.rst index 4d31d2ed7..8eeb1e7f3 100644 --- a/doc/source/user/resources/shared_file_system/index.rst +++ b/doc/source/user/resources/shared_file_system/index.rst @@ -5,4 +5,5 @@ Shared File System service resources :maxdepth: 1 v2/availability_zone + v2/storage_pool v2/share diff --git a/doc/source/user/resources/shared_file_system/v2/storage_pool.rst b/doc/source/user/resources/shared_file_system/v2/storage_pool.rst new file mode 100644 index 000000000..86649b83a --- /dev/null +++ b/doc/source/user/resources/shared_file_system/v2/storage_pool.rst @@ -0,0 +1,13 @@ +openstack.shared_file_system.v2.storage_pool +============================================ + +.. automodule:: openstack.shared_file_system.v2.storage_pool + +The StoragePool Class +--------------------- + +The ``StoragePool`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.shared_file_system.v2.storage_pool.StoragePool + :members: diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index 2f27d0d80..f64680c85 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -14,6 +14,9 @@ from openstack import resource from openstack.shared_file_system.v2 import ( availability_zone as _availability_zone) +from openstack.shared_file_system.v2 import ( + storage_pool as _storage_pool +) from openstack.shared_file_system.v2 import share as _share @@ -146,3 +149,22 @@ def wait_for_status(self, res, status='active', failures=None, failures = [] if failures is None else failures return resource.wait_for_status( self, res, status, failures, interval, wait) + + def storage_pools(self, details=True, **query): + """Lists all back-end storage pools with details + + :param kwargs query: Optional query parameters to be sent to limit + the storage pools being returned. Available parameters include: + + * pool_name: The pool name for the back end. + * host_name: The host name for the back end. + * backend_name: The name of the back end. + * capabilities: The capabilities for the storage back end. + * share_type: The share type name or UUID. + :returns: A generator of manila storage pool resources + :rtype: :class:`~openstack.shared_file_system.v2. + storage_pool.StoragePool` + """ + base_path = '/scheduler-stats/pools/detail' if details else None + return self._list( + _storage_pool.StoragePool, base_path=base_path, **query) diff --git a/openstack/shared_file_system/v2/storage_pool.py b/openstack/shared_file_system/v2/storage_pool.py new file mode 100644 index 000000000..a15c323c0 --- /dev/null +++ b/openstack/shared_file_system/v2/storage_pool.py @@ -0,0 +1,41 @@ +# 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 resource + + +class StoragePool(resource.Resource): + + resources_key = "pools" + base_path = "/scheduler-stats/pools" + + # capabilities + allow_create = False + allow_fetch = False + allow_commit = False + allow_delete = False + allow_list = True + allow_head = False + + _query_mapping = resource.QueryParameters( + 'pool', 'backend', 'host', 'capabilities', 'share_type', + ) + + #: Properties + #: The name of the back end. + backend = resource.Body("backend", type=str) + #: The host of the back end. + host = resource.Body("host", type=str) + #: The pool for the back end + pool = resource.Body("pool", type=str) + #: The back end capabilities. + capabilities = resource.Body("capabilities", type=dict) diff --git a/openstack/tests/functional/shared_file_system/test_storage_pool.py b/openstack/tests/functional/shared_file_system/test_storage_pool.py new file mode 100644 index 000000000..fea60470a --- /dev/null +++ b/openstack/tests/functional/shared_file_system/test_storage_pool.py @@ -0,0 +1,24 @@ +# 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.tests.functional.shared_file_system import base + + +class StoragePoolTest(base.BaseSharedFileSystemTest): + + def test_storage_pools(self): + pools = self.operator_cloud.shared_file_system.storage_pools() + self.assertGreater(len(list(pools)), 0) + for pool in pools: + for attribute in ('pool', 'name', 'host', 'backend', + 'capabilities'): + self.assertTrue(hasattr(pool, attribute)) diff --git a/openstack/tests/unit/shared_file_system/v2/test_proxy.py b/openstack/tests/unit/shared_file_system/v2/test_proxy.py index 1a1e8340a..3d04ad8c2 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_proxy.py +++ b/openstack/tests/unit/shared_file_system/v2/test_proxy.py @@ -14,6 +14,7 @@ from openstack.shared_file_system.v2 import _proxy from openstack.shared_file_system.v2 import share +from openstack.shared_file_system.v2 import storage_pool from openstack.tests.unit import test_proxy_base @@ -62,3 +63,19 @@ def test_wait_for(self, mock_wait): mock_wait.assert_called_once_with(self.proxy, mock_resource, 'ACTIVE', [], 2, 120) + + def test_storage_pools(self): + self.verify_list( + self.proxy.storage_pools, storage_pool.StoragePool) + + def test_storage_pool_detailed(self): + self.verify_list( + self.proxy.storage_pools, storage_pool.StoragePool, + method_kwargs={"details": True, "backend": "alpha"}, + expected_kwargs={"backend": "alpha"}) + + def test_storage_pool_not_detailed(self): + self.verify_list( + self.proxy.storage_pools, storage_pool.StoragePool, + method_kwargs={"details": False, "backend": "alpha"}, + expected_kwargs={"backend": "alpha"}) diff --git a/openstack/tests/unit/shared_file_system/v2/test_storage_pool.py b/openstack/tests/unit/shared_file_system/v2/test_storage_pool.py new file mode 100644 index 000000000..bdd58e40f --- /dev/null +++ b/openstack/tests/unit/shared_file_system/v2/test_storage_pool.py @@ -0,0 +1,74 @@ +# 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.shared_file_system.v2 import storage_pool +from openstack.tests.unit import base + + +EXAMPLE = { + "name": "opencloud@alpha#ALPHA_pool", + "host": "opencloud", + "backend": "alpha", + "pool": "ALPHA_pool", + "capabilities": { + "pool_name": "ALPHA_pool", + "total_capacity_gb": 1230.0, + "free_capacity_gb": 1210.0, + "reserved_percentage": 0, + "share_backend_name": "ALPHA", + "storage_protocol": "NFS_CIFS", + "vendor_name": "Open Source", + "driver_version": "1.0", + "timestamp": "2021-07-31T00:28:02.935569", + "driver_handles_share_servers": True, + "snapshot_support": True, + "create_share_from_snapshot_support": True, + "revert_to_snapshot_support": True, + "mount_snapshot_support": True, + "dedupe": False, + "compression": False, + "replication_type": None, + "replication_domain": None, + "sg_consistent_snapshot_support": "pool", + "ipv4_support": True, + "ipv6_support": False} +} + + +class TestStoragePool(base.TestCase): + + def test_basic(self): + pool_resource = storage_pool.StoragePool() + self.assertEqual('pools', pool_resource.resources_key) + self.assertEqual( + '/scheduler-stats/pools', pool_resource.base_path) + self.assertTrue(pool_resource.allow_list) + + self.assertDictEqual({ + 'pool': 'pool', + 'backend': 'backend', + 'host': 'host', + 'limit': 'limit', + 'marker': 'marker', + 'capabilities': 'capabilities', + 'share_type': 'share_type', + }, + pool_resource._query_mapping._mapping) + + def test_make_storage_pool(self): + pool_resource = storage_pool.StoragePool(**EXAMPLE) + self.assertEqual(EXAMPLE['pool'], pool_resource.pool) + self.assertEqual(EXAMPLE['host'], pool_resource.host) + self.assertEqual(EXAMPLE['name'], pool_resource.name) + self.assertEqual(EXAMPLE['backend'], pool_resource.backend) + self.assertEqual( + EXAMPLE['capabilities'], pool_resource.capabilities) diff --git a/releasenotes/notes/add-storage-pool-to-shared-file-ad45da1b2510b412.yaml b/releasenotes/notes/add-storage-pool-to-shared-file-ad45da1b2510b412.yaml new file mode 100644 index 000000000..7303d3560 --- /dev/null +++ b/releasenotes/notes/add-storage-pool-to-shared-file-ad45da1b2510b412.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added support for retrieving storage pools information from + the Shared File Systems service. From 4d4eafa761fd6d7000534242579c193162f6eb4e Mon Sep 17 00:00:00 2001 From: Cenne Date: Fri, 23 Jul 2021 21:17:27 +0200 Subject: [PATCH 2895/3836] Add support for reading node's "boot_mode" and "secure_boot" fields boot_mode is saved under node object as `node.boot_mode` secure_boot is saved under node object as `node.is_secure_boot` Story: 2008567 Task: 41709 Change-Id: I15a22970cd4f5ed0bf43c38004449c988cb04925 --- openstack/baremetal/v1/node.py | 9 +++++++-- openstack/tests/unit/baremetal/v1/test_node.py | 4 ++++ .../notes/add-node-boot-mode-5f49882fdd86f35b.yaml | 5 +++++ 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/add-node-boot-mode-5f49882fdd86f35b.yaml diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index a933c2e95..66cf2cd22 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -91,8 +91,8 @@ class Node(_common.ListMixin, resource.Resource): is_maintenance='maintenance', ) - # Provision state deploy_steps introduced in 1.69 (Wallaby). - _max_microversion = '1.69' + # Node states boot_mode and secure_boot introduced in 1.75 (Xena). + _max_microversion = '1.75' # Properties #: The UUID of the allocation associated with this node. Added in API @@ -101,6 +101,8 @@ class Node(_common.ListMixin, resource.Resource): #: A string or UUID of the tenant who owns the baremetal node. Added in API #: microversion 1.50. owner = resource.Body("owner") + #: The current boot mode state (uefi/bios). Added in API microversion 1.75. + boot_mode = resource.Body("boot_mode") #: The UUID of the chassis associated wit this node. Can be empty or None. chassis_id = resource.Body("chassis_uuid") #: The current clean step. @@ -148,6 +150,9 @@ class Node(_common.ListMixin, resource.Resource): #: Whether the node is marked for retirement. Added in API microversion #: 1.61. is_retired = resource.Body("retired", type=bool) + #: Whether the node is currently booted with secure boot turned on. + #: Added in API microversion 1.75. + is_secure_boot = resource.Body("secure_boot", type=bool) #: Any error from the most recent transaction that started but failed to #: finish. last_error = resource.Body("last_error") diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 40c21e329..27a5012dc 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -23,6 +23,7 @@ # NOTE: Sample data from api-ref doc FAKE = { + "boot_mode": "uefi", "chassis_uuid": "1", # NOTE: missed in api-ref sample "clean_step": {}, "console_enabled": False, @@ -81,6 +82,7 @@ "raid_config": {}, "reservation": None, "resource_class": None, + "secure_boot": True, "states": [ { "href": "http://127.0.0.1:6385/v1/nodes//states", @@ -119,6 +121,7 @@ def test_instantiate(self): self.assertEqual(FAKE['uuid'], sot.id) self.assertEqual(FAKE['name'], sot.name) + self.assertEqual(FAKE['boot_mode'], sot.boot_mode) self.assertEqual(FAKE['chassis_uuid'], sot.chassis_id) self.assertEqual(FAKE['clean_step'], sot.clean_step) self.assertEqual(FAKE['created_at'], sot.created_at) @@ -145,6 +148,7 @@ def test_instantiate(self): self.assertEqual(FAKE['raid_config'], sot.raid_config) self.assertEqual(FAKE['reservation'], sot.reservation) self.assertEqual(FAKE['resource_class'], sot.resource_class) + self.assertEqual(FAKE['secure_boot'], sot.is_secure_boot) self.assertEqual(FAKE['states'], sot.states) self.assertEqual(FAKE['target_provision_state'], sot.target_provision_state) diff --git a/releasenotes/notes/add-node-boot-mode-5f49882fdd86f35b.yaml b/releasenotes/notes/add-node-boot-mode-5f49882fdd86f35b.yaml new file mode 100644 index 000000000..b97ad3472 --- /dev/null +++ b/releasenotes/notes/add-node-boot-mode-5f49882fdd86f35b.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add support to display node fields ``boot_mode`` and ``secure_boot`` + which are introduced in API 1.75. From 116d875c04fdf4e922ff428920ce55ace3d52254 Mon Sep 17 00:00:00 2001 From: Cenne Date: Fri, 13 Aug 2021 16:34:58 +0200 Subject: [PATCH 2896/3836] Add support for changing baremetal node's boot_mode and secure_boot states depends-on: https://review.opendev.org/c/openstack/ironic/+/800084 Story: 2008567 Task: 41709 Change-Id: I941a84f36ce79c4cbd0968f4120c66d59091fdd9 --- openstack/baremetal/v1/_common.py | 3 + openstack/baremetal/v1/_proxy.py | 21 ++++++ openstack/baremetal/v1/node.py | 65 ++++++++++++++++++- .../tests/unit/baremetal/v1/test_node.py | 48 ++++++++++++++ ...d-node-boot-mode-set-5718a8d6511b4826.yaml | 5 ++ 5 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/add-node-boot-mode-set-5718a8d6511b4826.yaml diff --git a/openstack/baremetal/v1/_common.py b/openstack/baremetal/v1/_common.py index c2cdaac55..7981c95f5 100644 --- a/openstack/baremetal/v1/_common.py +++ b/openstack/baremetal/v1/_common.py @@ -79,6 +79,9 @@ DEPLOY_STEPS_VERSION = '1.69' """API version in which deploy_steps was added to node provisioning.""" +CHANGE_BOOT_MODE_VERSION = '1.76' +"""API version in which boot_mode and secure_boot states can be changed""" + class ListMixin: diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 0b9e21413..f5a888106 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -408,6 +408,27 @@ def set_node_boot_device(self, node, boot_device, persistent=False): res = self._get_resource(_node.Node, node) return res.set_boot_device(self, boot_device, persistent=persistent) + def set_node_boot_mode(self, node, target): + """Make a request to change node's boot mode + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :param target: Boot mode to set for node, one of either 'uefi'/'bios'. + """ + res = self._get_resource(_node.Node, node) + return res.set_boot_mode(self, target) + + def set_node_secure_boot(self, node, target): + """Make a request to change node's secure boot state + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :param target: Boolean indicating secure boot state to set. + True/False corresponding to 'on'/'off' respectively. + """ + res = self._get_resource(_node.Node, node) + return res.set_secure_boot(self, target) + def wait_for_nodes_provision_state(self, nodes, expected_state, timeout=None, abort_on_failed_state=True, diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 66cf2cd22..75e407f70 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -91,8 +91,8 @@ class Node(_common.ListMixin, resource.Resource): is_maintenance='maintenance', ) - # Node states boot_mode and secure_boot introduced in 1.75 (Xena). - _max_microversion = '1.75' + # Ability to change boot_mode and secure_boot, introduced in 1.76 (Xena). + _max_microversion = '1.76' # Properties #: The UUID of the allocation associated with this node. Added in API @@ -853,6 +853,67 @@ def set_boot_device(self, session, boot_device, persistent=False): .format(node=self.id)) exceptions.raise_from_response(response, error_message=msg) + def set_boot_mode(self, session, target): + """Make a request to change node's boot mode + + This call is asynchronous, it will return success as soon as the Bare + Metal service acknowledges the request. + + :param session: The session to use for making this request. + :param target: Boot mode to set for node, one of either 'uefi'/'bios'. + :raises: ValueError if ``target`` is not one of 'uefi or 'bios'. + """ + session = self._get_session(session) + version = utils.pick_microversion(session, + _common.CHANGE_BOOT_MODE_VERSION) + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'states', 'boot_mode') + if target not in ('uefi', 'bios'): + raise ValueError("Unrecognized boot mode %s." + "Boot mode should be one of 'uefi' or 'bios'." + % target) + body = {'target': target} + + response = session.put( + request.url, json=body, + headers=request.headers, microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + msg = ("Failed to change boot mode for node {node}" + .format(node=self.id)) + exceptions.raise_from_response(response, error_message=msg) + + def set_secure_boot(self, session, target): + """Make a request to change node's secure boot state + + This call is asynchronous, it will return success as soon as the Bare + Metal service acknowledges the request. + + :param session: The session to use for making this request. + :param bool target: Boolean indicating secure boot state to set. + True/False corresponding to 'on'/'off' respectively. + :raises: ValueError if ``target`` is not boolean. + """ + session = self._get_session(session) + version = utils.pick_microversion(session, + _common.CHANGE_BOOT_MODE_VERSION) + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'states', 'secure_boot') + if not isinstance(target, bool): + raise ValueError("Invalid target %s. It should be True or False " + "corresponding to secure boot state 'on' or 'off'" + % target) + body = {'target': target} + + response = session.put( + request.url, json=body, + headers=request.headers, microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + msg = ("Failed to change secure boot state for {node}" + .format(node=self.id)) + exceptions.raise_from_response(response, error_message=msg) + def add_trait(self, session, trait): """Add a trait to a node. diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 27a5012dc..99c8835ad 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -764,6 +764,54 @@ def test_node_set_boot_device(self): retriable_status_codes=_common.RETRIABLE_STATUS_CODES) +@mock.patch.object(utils, 'pick_microversion', lambda session, v: v) +@mock.patch.object(node.Node, 'fetch', lambda self, session: self) +@mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) +class TestNodeSetBootMode(base.TestCase): + + def setUp(self): + super(TestNodeSetBootMode, self).setUp() + self.node = node.Node(**FAKE) + self.session = mock.Mock(spec=adapter.Adapter, + default_microversion='1.1') + + def test_node_set_boot_mode(self): + self.node.set_boot_mode(self.session, 'uefi') + self.session.put.assert_called_once_with( + 'nodes/%s/states/boot_mode' % self.node.id, + json={'target': 'uefi'}, + headers=mock.ANY, microversion=mock.ANY, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + def test_node_set_boot_mode_invalid_mode(self): + self.assertRaises(ValueError, + self.node.set_boot_mode, self.session, 'invalid-efi') + + +@mock.patch.object(utils, 'pick_microversion', lambda session, v: v) +@mock.patch.object(node.Node, 'fetch', lambda self, session: self) +@mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) +class TestNodeSetSecureBoot(base.TestCase): + + def setUp(self): + super(TestNodeSetSecureBoot, self).setUp() + self.node = node.Node(**FAKE) + self.session = mock.Mock(spec=adapter.Adapter, + default_microversion='1.1') + + def test_node_set_secure_boot(self): + self.node.set_secure_boot(self.session, True) + self.session.put.assert_called_once_with( + 'nodes/%s/states/secure_boot' % self.node.id, + json={'target': True}, + headers=mock.ANY, microversion=mock.ANY, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + def test_node_set_secure_boot_invalid_none(self): + self.assertRaises(ValueError, + self.node.set_secure_boot, self.session, None) + + @mock.patch.object(utils, 'pick_microversion', lambda session, v: v) @mock.patch.object(node.Node, 'fetch', lambda self, session: self) @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) diff --git a/releasenotes/notes/add-node-boot-mode-set-5718a8d6511b4826.yaml b/releasenotes/notes/add-node-boot-mode-set-5718a8d6511b4826.yaml new file mode 100644 index 000000000..23d6e439a --- /dev/null +++ b/releasenotes/notes/add-node-boot-mode-set-5718a8d6511b4826.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add support for changing node states ``boot_mode`` and ``secure_boot`` + in sync with functionality introduced in API 1.76. From ebc3419b347051af1d70d843b649fa387aa729db Mon Sep 17 00:00:00 2001 From: Cenne Date: Fri, 13 Aug 2021 15:01:37 +0200 Subject: [PATCH 2897/3836] Fix some docstrings, and a small bug When ignore_missing option was True, The if branch would short circuit code execution to always return `False` even if removing the trait was a success. Change-Id: Ib00e75dcdd65784ca793fe9369785b20921eb371 --- openstack/baremetal/v1/node.py | 11 +++++------ openstack/tests/unit/baremetal/v1/test_node.py | 3 ++- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index a933c2e95..484068dc1 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -830,7 +830,6 @@ def set_boot_device(self, session, boot_device, persistent=False): :param boot_device: Boot device to assign to the node. :param persistent: If the boot device change is maintained after node reboot - :return: The updated :class:`~openstack.baremetal.v1.node.Node` """ session = self._get_session(session) version = self._get_microversion_for(session, 'commit') @@ -853,7 +852,6 @@ def add_trait(self, session, trait): :param session: The session to use for making this request. :param trait: The trait to add to the node. - :returns: The updated :class:`~openstack.baremetal.v1.node.Node` """ session = self._get_session(session) version = utils.pick_microversion(session, '1.37') @@ -879,7 +877,8 @@ def remove_trait(self, session, trait, ignore_missing=True): :class:`~openstack.exceptions.ResourceNotFound` will be raised when the trait does not exist. Otherwise, ``False`` is returned. - :returns: The updated :class:`~openstack.baremetal.v1.node.Node` + :returns bool: True on success removing the trait. + False when the trait does not exist already. """ session = self._get_session(session) version = utils.pick_microversion(session, '1.37') @@ -890,7 +889,7 @@ def remove_trait(self, session, trait, ignore_missing=True): request.url, headers=request.headers, microversion=version, retriable_status_codes=_common.RETRIABLE_STATUS_CODES) - if ignore_missing or response.status_code == 400: + if ignore_missing and response.status_code == 400: session.log.debug( 'Trait %(trait)s was already removed from node %(node)s', {'trait': trait, 'node': self.id}) @@ -900,7 +899,8 @@ def remove_trait(self, session, trait, ignore_missing=True): .format(node=self.id, trait=trait)) exceptions.raise_from_response(response, error_message=msg) - self.traits = list(set(self.traits) - {trait}) + if self.traits: + self.traits = list(set(self.traits) - {trait}) return True @@ -912,7 +912,6 @@ def set_traits(self, session, traits): :param session: The session to use for making this request. :param traits: list of traits to add to the node. - :returns: The updated :class:`~openstack.baremetal.v1.node.Node` """ session = self._get_session(session) version = utils.pick_microversion(session, '1.37') diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 40c21e329..69ad1ad16 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -781,7 +781,8 @@ def test_node_add_trait(self): retriable_status_codes=_common.RETRIABLE_STATUS_CODES) def test_remove_trait(self): - self.node.remove_trait(self.session, 'CUSTOM_FAKE') + self.assertTrue(self.node.remove_trait(self.session, + 'CUSTOM_FAKE')) self.session.delete.assert_called_once_with( 'nodes/%s/traits/%s' % (self.node.id, 'CUSTOM_FAKE'), headers=mock.ANY, microversion='1.37', From d0d4d8bc64da62dc8338e307b4f736cd0b1f5f49 Mon Sep 17 00:00:00 2001 From: Jens Harbott Date: Fri, 13 Aug 2021 16:46:28 +0200 Subject: [PATCH 2898/3836] Fix key generation for caching With a new major version of decorate, the list of args may be non-empty for the first time, exposing some issue in our handling. Ignore them for now in order to unblock gating. Task: 43007 Change-Id: Ia4e71a5ab02516a8ef40a94590b5c9092eab5fa5 --- openstack/cloud/openstackcloud.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index eabbbd276..0ec581ce6 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -331,7 +331,8 @@ def _make_cache_key(self, namespace, fn): name_key = '%s:%s' % (self.name, namespace) def generate_key(*args, **kwargs): - arg_key = ','.join(args) + # TODO(frickler): make handling arg keys actually work + arg_key = '' kw_keys = sorted(kwargs.keys()) kwargs_key = ','.join( ['%s:%s' % (k, kwargs[k]) for k in kw_keys if k != 'cache']) From f7f9446d6e60e6255895ee477a61f2f24f1516ba Mon Sep 17 00:00:00 2001 From: Johannes Kulik Date: Wed, 18 Aug 2021 11:47:26 +0200 Subject: [PATCH 2899/3836] Add "security_group_ids" to Port's query parameters Neutron's port-security-groups-filtering extension allows to filter ports by security-group(s). This feature was added with [1]. We keep the name like in the attribute "security_group_ids" instead of "security_groups" to stay consistent within the Port model. [1] https://launchpad.net/neutron/+bug/1405057 Change-Id: Ic557dc3bf97a193fd28c13792302fb8396e29df1 --- openstack/network/v2/port.py | 1 + openstack/tests/unit/network/v2/test_port.py | 1 + 2 files changed, 2 insertions(+) diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index a5a25ec8c..6e0df60ab 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -36,6 +36,7 @@ class Port(_base.NetworkResource, resource.TagMixin): is_admin_state_up='admin_state_up', is_port_security_enabled='port_security_enabled', project_id='tenant_id', + security_group_ids='security_groups', **resource.TagMixin._tag_query_parameters ) diff --git a/openstack/tests/unit/network/v2/test_port.py b/openstack/tests/unit/network/v2/test_port.py index 7fd88b631..0ea82aae2 100644 --- a/openstack/tests/unit/network/v2/test_port.py +++ b/openstack/tests/unit/network/v2/test_port.py @@ -99,6 +99,7 @@ def test_basic(self): "is_port_security_enabled": "port_security_enabled", "project_id": "tenant_id", + "security_group_ids": "security_groups", "limit": "limit", "marker": "marker", "any_tags": "tags-any", From d62a4e6062f657a63a85a89d044de7dd5b3943d4 Mon Sep 17 00:00:00 2001 From: Jens Harbott Date: Fri, 13 Aug 2021 16:46:28 +0200 Subject: [PATCH 2900/3836] Fix key generation for caching With a new major version of decorate, the list of args may be non-empty for the first time, exposing some issue in our handling. Ignore them for now in order to unblock gating. Task: 43007 Change-Id: Ia4e71a5ab02516a8ef40a94590b5c9092eab5fa5 (cherry picked from commit d0d4d8bc64da62dc8338e307b4f736cd0b1f5f49) --- openstack/cloud/openstackcloud.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index eabbbd276..0ec581ce6 100755 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -331,7 +331,8 @@ def _make_cache_key(self, namespace, fn): name_key = '%s:%s' % (self.name, namespace) def generate_key(*args, **kwargs): - arg_key = ','.join(args) + # TODO(frickler): make handling arg keys actually work + arg_key = '' kw_keys = sorted(kwargs.keys()) kwargs_key = ','.join( ['%s:%s' % (k, kwargs[k]) for k in kw_keys if k != 'cache']) From 21795acff60d4e33f9fcf6339aacc227f86c80e8 Mon Sep 17 00:00:00 2001 From: Goutham Pacha Ravi Date: Mon, 26 Jul 2021 15:49:03 -0700 Subject: [PATCH 2901/3836] Add a manila functional check job This job can be non-voting. Also fix up the functional tests to use a configurable cloud profile, so we can set the desired api microversion to be tested. Change-Id: I952b0590ac571aff220b2285d3dc0b5c5bc99816 Signed-off-by: Goutham Pacha Ravi --- .zuul.yaml | 72 +++++++++++++++++++ .../functional/shared_file_system/base.py | 13 +++- .../test_availability_zone.py | 2 +- .../shared_file_system/test_share.py | 25 ++----- 4 files changed, 89 insertions(+), 23 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index ddd5183ab..8202de424 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -352,6 +352,76 @@ zuul_copy_output: '{{ devstack_base_dir }}/masakari-logs': logs +- job: + name: openstacksdk-functional-devstack-manila + parent: openstacksdk-functional-devstack-minimum + description: | + Run openstacksdk functional tests against a master devstack with manila + required-projects: + - openstack/manila + - name: openstack/openstacksdk + override-branch: feature/r1 + vars: + devstack_localrc: + # Set up manila with a fake driver - makes things super fast and should + # have no impact on the API + MANILA_INSTALL_TEMPEST_PLUGIN_SYSTEMWIDE: false + SHARE_DRIVER: manila.tests.share.drivers.dummy.DummyDriver + MANILA_CONFIGURE_GROUPS: alpha,beta,gamma,membernet + MANILA_CONFIGURE_DEFAULT_TYPES: true + MANILA_SERVICE_IMAGE_ENABLED: false + MANILA_SHARE_MIGRATION_PERIOD_TASK_INTERVAL: 1 + MANILA_SERVER_MIGRATION_PERIOD_TASK_INTERVAL: 10 + MANILA_REPLICA_STATE_UPDATE_INTERVAL: 10 + MANILA_DEFAULT_SHARE_TYPE_EXTRA_SPECS: 'snapshot_support=True create_share_from_snapshot_support=True revert_to_snapshot_support=True mount_snapshot_support=True' + MANILA_ENABLED_BACKENDS: alpha,beta,gamma + MANILA_OPTGROUP_alpha_driver_handles_share_servers: false + MANILA_OPTGROUP_alpha_replication_domain: DUMMY_DOMAIN + MANILA_OPTGROUP_alpha_share_backend_name: ALPHA + MANILA_OPTGROUP_alpha_share_driver: manila.tests.share.drivers.dummy.DummyDriver + MANILA_OPTGROUP_beta_driver_handles_share_servers: false + MANILA_OPTGROUP_beta_replication_domain: DUMMY_DOMAIN + MANILA_OPTGROUP_beta_share_backend_name: BETA + MANILA_OPTGROUP_beta_share_driver: manila.tests.share.drivers.dummy.DummyDriver + MANILA_OPTGROUP_gamma_driver_handles_share_servers: true + MANILA_OPTGROUP_gamma_network_config_group: membernet + MANILA_OPTGROUP_gamma_share_backend_name: GAMMA + MANILA_OPTGROUP_gamma_share_driver: manila.tests.share.drivers.dummy.DummyDriver + MANILA_OPTGROUP_gamma_admin_network_config_group: membernet + MANILA_OPTGROUP_membernet_network_api_class: manila.network.standalone_network_plugin.StandaloneNetworkPlugin + MANILA_OPTGROUP_membernet_network_plugin_ipv4_enabled: true + MANILA_OPTGROUP_membernet_standalone_network_plugin_allowed_ip_ranges: 10.0.0.10-10.0.0.209 + MANILA_OPTGROUP_membernet_standalone_network_plugin_gateway: 10.0.0.1 + MANILA_OPTGROUP_membernet_standalone_network_plugin_mask: 24 + MANILA_OPTGROUP_membernet_standalone_network_plugin_network_type: vlan + MANILA_OPTGROUP_membernet_standalone_network_plugin_segmentation_id: 1010 + devstack_plugins: + manila: https://opendev.org/openstack/manila + devstack_services: + c-api: false + c-bak: false + c-sch: false + c-vol: false + cinder: false + s-account: false + s-container: false + s-object: false + s-proxy: false + n-api: false + n-api-meta: false + n-cauth: false + n-cond: false + n-cpu: false + n-novnc: false + n-obj: false + n-sch: false + nova: false + placement-api: false + dstat: false + tox_environment: + OPENSTACKSDK_HAS_MANILA: 1 + OPENSTACKSDK_TESTS_SUBDIR: shared_file_system + - job: name: metalsmith-integration-openstacksdk-src parent: metalsmith-integration-glance-netboot-cirros-direct @@ -393,6 +463,8 @@ - openstacksdk-functional-devstack-senlin - openstacksdk-functional-devstack-magnum: voting: false + - openstacksdk-functional-devstack-manila: + voting: false - openstacksdk-functional-devstack-masakari: voting: false - openstacksdk-functional-devstack-ironic: diff --git a/openstack/tests/functional/shared_file_system/base.py b/openstack/tests/functional/shared_file_system/base.py index 864116ee1..9a78b2edb 100644 --- a/openstack/tests/functional/shared_file_system/base.py +++ b/openstack/tests/functional/shared_file_system/base.py @@ -21,10 +21,19 @@ def setUp(self): super(BaseSharedFileSystemTest, self).setUp() self.require_service('shared-file-system', min_microversion=self.min_microversion) + self._set_operator_cloud(shared_file_system_api_version='2.63') + self._set_user_cloud(shared_file_system_api_version='2.63') def create_share(self, **kwargs): - share = self.conn.share.create_share(**kwargs) - self.addCleanup(self.conn.share.delete_share, share.id, + share = self.user_cloud.share.create_share(**kwargs) + self.addCleanup(self.user_cloud.share.delete_share, + share.id, ignore_missing=True) + self.user_cloud.share.wait_for_status( + share, + status='available', + failures=['error'], + interval=5, + wait=self._wait_for_timeout) self.assertIsNotNone(share.id) return share diff --git a/openstack/tests/functional/shared_file_system/test_availability_zone.py b/openstack/tests/functional/shared_file_system/test_availability_zone.py index 4cb39014d..045e5bd31 100644 --- a/openstack/tests/functional/shared_file_system/test_availability_zone.py +++ b/openstack/tests/functional/shared_file_system/test_availability_zone.py @@ -18,7 +18,7 @@ class AvailabilityZoneTest(base.BaseSharedFileSystemTest): min_microversion = '2.7' def test_availability_zones(self): - azs = self.conn.shared_file_system.availability_zones() + azs = self.user_cloud.shared_file_system.availability_zones() self.assertGreater(len(list(azs)), 0) for az in azs: for attribute in ('id', 'name', 'created_at', 'updated_at'): diff --git a/openstack/tests/functional/shared_file_system/test_share.py b/openstack/tests/functional/shared_file_system/test_share.py index 16bae406c..4a4951ac7 100644 --- a/openstack/tests/functional/shared_file_system/test_share.py +++ b/openstack/tests/functional/shared_file_system/test_share.py @@ -20,41 +20,26 @@ def setUp(self): super(ShareTest, self).setUp() self.SHARE_NAME = self.getUniqueString() - my_share = self.conn.shared_file_system.create_share( + my_share = self.create_share( name=self.SHARE_NAME, size=2, share_type="dhss_false", share_protocol='NFS', description=None) - self.conn.shared_file_system.wait_for_status( - my_share, - status='available', - failures=['error'], - interval=5, - wait=self._wait_for_timeout) - self.assertIsNotNone(my_share) - self.assertIsNotNone(my_share.id) self.SHARE_ID = my_share.id - def tearDown(self): - sot = self.conn.shared_file_system.delete_share( - self.SHARE_ID, - ignore_missing=True) - self.assertIsNone(sot) - super(ShareTest, self).tearDown() - def test_get(self): - sot = self.conn.shared_file_system.get_share(self.SHARE_ID) + sot = self.user_cloud.share.get_share(self.SHARE_ID) assert isinstance(sot, _share.Share) self.assertEqual(self.SHARE_ID, sot.id) def test_list_share(self): - shares = self.conn.shared_file_system.shares(details=False) + shares = self.user_cloud.share.shares(details=False) self.assertGreater(len(list(shares)), 0) for share in shares: for attribute in ('id', 'name', 'created_at', 'updated_at'): self.assertTrue(hasattr(share, attribute)) def test_update(self): - updated_share = self.conn.shared_file_system.update_share( + updated_share = self.user_cloud.share.update_share( self.SHARE_ID, display_description='updated share') - get_updated_share = self.conn.shared_file_system.get_share( + get_updated_share = self.user_cloud.share.get_share( updated_share.id) self.assertEqual('updated share', get_updated_share.description) From d7d6464c6df1c5649792096e89969c2e79751019 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 26 Aug 2021 18:19:38 +0200 Subject: [PATCH 2902/3836] Add user group assignment support in identity Add possibility to add/remove/check user into/from group. Change-Id: I1e012471badcf6264dced53354472abd8312f62c --- doc/source/user/proxies/identity_v3.rst | 3 +- openstack/cloud/_identity.py | 21 +----- openstack/identity/v3/_proxy.py | 39 +++++++++++ openstack/identity/v3/group.py | 30 +++++++++ .../tests/unit/identity/v3/test_group.py | 50 ++++++++++++++ .../tests/unit/identity/v3/test_proxy.py | 65 ++++++++++++++++++- ...ser-group-assignment-9c419b6c6bfe392c.yaml | 4 ++ 7 files changed, 192 insertions(+), 20 deletions(-) create mode 100644 releasenotes/notes/add-user-group-assignment-9c419b6c6bfe392c.yaml diff --git a/doc/source/user/proxies/identity_v3.rst b/doc/source/user/proxies/identity_v3.rst index 10892d29b..31dccad94 100644 --- a/doc/source/user/proxies/identity_v3.rst +++ b/doc/source/user/proxies/identity_v3.rst @@ -42,7 +42,8 @@ Group Operations .. autoclass:: openstack.identity.v3._proxy.Proxy :noindex: :members: create_group, update_group, delete_group, get_group, find_group, - groups + groups, add_user_to_group, remove_user_from_group, + check_user_in_group Policy Operations ^^^^^^^^^^^^^^^^^ diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index 6999c2c29..ea88711b0 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -295,11 +295,7 @@ def add_user_to_group(self, name_or_id, group_name_or_id): """ user, group = self._get_user_and_group(name_or_id, group_name_or_id) - error_msg = "Error adding user {user} to group {group}".format( - user=name_or_id, group=group_name_or_id) - self._identity_client.put( - '/groups/{g}/users/{u}'.format(g=group['id'], u=user['id']), - error_message=error_msg) + self.identity.add_user_to_group(user, group) def is_user_in_group(self, name_or_id, group_name_or_id): """Check to see if a user is in a group. @@ -314,14 +310,7 @@ def is_user_in_group(self, name_or_id, group_name_or_id): """ user, group = self._get_user_and_group(name_or_id, group_name_or_id) - try: - self._identity_client.head( - '/groups/{g}/users/{u}'.format(g=group['id'], u=user['id'])) - return True - except exc.OpenStackCloudURINotFound: - # NOTE(samueldmq): knowing this URI exists, let's interpret this as - # user not found in group rather than URI not found. - return False + return self.identity.check_user_in_group(user, group) def remove_user_from_group(self, name_or_id, group_name_or_id): """Remove a user from a group. @@ -334,11 +323,7 @@ def remove_user_from_group(self, name_or_id, group_name_or_id): """ user, group = self._get_user_and_group(name_or_id, group_name_or_id) - error_msg = "Error removing user {user} from group {group}".format( - user=name_or_id, group=group_name_or_id) - self._identity_client.delete( - '/groups/{g}/users/{u}'.format(g=group['id'], u=user['id']), - error_message=error_msg) + self.identity.remove_user_from_group(user, group) @_utils.valid_kwargs('type', 'service_type', 'description') def create_service(self, name, enabled=True, **kwargs): diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 58df68b84..282f4b5db 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -362,6 +362,45 @@ def update_group(self, group, **attrs): """ return self._update(_group.Group, group, **attrs) + def add_user_to_group(self, user, group): + """Add user to group + + :param user: Either the ID of a user or a + :class:`~openstack.identity.v3.user.User` instance. + :param group: Either the ID of a group or a + :class:`~openstack.identity.v3.group.Group` instance. + :return: ``None`` + """ + user = self._get_resource(_user.User, user) + group = self._get_resource(_group.Group, group) + group.add_user(self, user) + + def remove_user_from_group(self, user, group): + """Remove user to group + + :param user: Either the ID of a user or a + :class:`~openstack.identity.v3.user.User` instance. + :param group: Either the ID of a group or a + :class:`~openstack.identity.v3.group.Group` instance. + :return: ``None`` + """ + user = self._get_resource(_user.User, user) + group = self._get_resource(_group.Group, group) + group.remove_user(self, user) + + def check_user_in_group(self, user, group): + """Check whether user belongsto group + + :param user: Either the ID of a user or a + :class:`~openstack.identity.v3.user.User` instance. + :param group: Either the ID of a group or a + :class:`~openstack.identity.v3.group.Group` instance. + :return: A boolean representing current relation + """ + user = self._get_resource(_user.User, user) + group = self._get_resource(_group.Group, group) + return group.check_user(self, user) + def create_policy(self, **attrs): """Create a new policy from attributes diff --git a/openstack/identity/v3/group.py b/openstack/identity/v3/group.py index 001f5124c..41d876356 100644 --- a/openstack/identity/v3/group.py +++ b/openstack/identity/v3/group.py @@ -10,7 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import exceptions from openstack import resource +from openstack import utils class Group(resource.Resource): @@ -40,3 +42,31 @@ class Group(resource.Resource): domain_id = resource.Body('domain_id') #: Unique group name, within the owning domain. *Type: string* name = resource.Body('name') + + def add_user(self, session, user): + """Add user to the group""" + url = utils.urljoin( + self.base_path, self.id, 'users', user.id) + resp = session.put(url,) + exceptions.raise_from_response(resp) + + def remove_user(self, session, user): + """Remove user from the group""" + url = utils.urljoin( + self.base_path, self.id, 'users', user.id) + resp = session.delete(url,) + exceptions.raise_from_response(resp) + + def check_user(self, session, user): + """Check whether user belongs to group""" + url = utils.urljoin( + self.base_path, self.id, 'users', user.id) + resp = session.head(url,) + if resp.status_code == 404: + # If we recieve 404 - treat this as False, + # rather then returning exception + return False + exceptions.raise_from_response(resp) + if resp.status_code == 204: + return True + return False diff --git a/openstack/tests/unit/identity/v3/test_group.py b/openstack/tests/unit/identity/v3/test_group.py index 8b53636cc..46b17ae58 100644 --- a/openstack/tests/unit/identity/v3/test_group.py +++ b/openstack/tests/unit/identity/v3/test_group.py @@ -10,7 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. +from unittest import mock + +from keystoneauth1 import adapter + from openstack.identity.v3 import group +from openstack.identity.v3 import user from openstack.tests.unit import base @@ -25,6 +30,16 @@ class TestGroup(base.TestCase): + def setUp(self): + super(TestGroup, self).setUp() + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.default_microversion = 1 + self.sess._get_connection = mock.Mock(return_value=self.cloud) + self.good_resp = mock.Mock() + self.good_resp.body = None + self.good_resp.json = mock.Mock(return_value=self.good_resp.body) + self.good_resp.status_code = 204 + def test_basic(self): sot = group.Group() self.assertEqual('group', sot.resource_key) @@ -52,3 +67,38 @@ def test_make_it(self): self.assertEqual(EXAMPLE['domain_id'], sot.domain_id) self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['name'], sot.name) + + def test_add_user(self): + sot = group.Group(**EXAMPLE) + resp = self.good_resp + self.sess.put = mock.Mock(return_value=resp) + + sot.add_user( + self.sess, user.User(id='1')) + + self.sess.put.assert_called_with( + 'groups/IDENTIFIER/users/1') + + def test_remove_user(self): + sot = group.Group(**EXAMPLE) + resp = self.good_resp + self.sess.delete = mock.Mock(return_value=resp) + + sot.remove_user( + self.sess, user.User(id='1')) + + self.sess.delete.assert_called_with( + 'groups/IDENTIFIER/users/1') + + def test_check_user(self): + sot = group.Group(**EXAMPLE) + resp = self.good_resp + self.sess.head = mock.Mock(return_value=resp) + + self.assertTrue( + sot.check_user( + self.sess, + user.User(id='1'))) + + self.sess.head.assert_called_with( + 'groups/IDENTIFIER/users/1') diff --git a/openstack/tests/unit/identity/v3/test_proxy.py b/openstack/tests/unit/identity/v3/test_proxy.py index 3608cc933..b2af2b570 100644 --- a/openstack/tests/unit/identity/v3/test_proxy.py +++ b/openstack/tests/unit/identity/v3/test_proxy.py @@ -35,7 +35,7 @@ def setUp(self): self.proxy = _proxy.Proxy(self.session) -class TestIdentityProxy(TestIdentityProxyBase): +class TestIdentityProxyCredential(TestIdentityProxyBase): def test_credential_create_attrs(self): self.verify_create(self.proxy.create_credential, @@ -61,6 +61,9 @@ def test_credentials(self): def test_credential_update(self): self.verify_update(self.proxy.update_credential, credential.Credential) + +class TestIdentityProxyDomain(TestIdentityProxyBase): + def test_domain_create_attrs(self): self.verify_create(self.proxy.create_domain, domain.Domain) @@ -82,6 +85,9 @@ def test_domains(self): def test_domain_update(self): self.verify_update(self.proxy.update_domain, domain.Domain) + +class TestIdentityProxyEndpoint(TestIdentityProxyBase): + def test_endpoint_create_attrs(self): self.verify_create(self.proxy.create_endpoint, endpoint.Endpoint) @@ -105,6 +111,9 @@ def test_endpoints(self): def test_endpoint_update(self): self.verify_update(self.proxy.update_endpoint, endpoint.Endpoint) + +class TestIdentityProxyGroup(TestIdentityProxyBase): + def test_group_create_attrs(self): self.verify_create(self.proxy.create_group, group.Group) @@ -126,6 +135,42 @@ def test_groups(self): def test_group_update(self): self.verify_update(self.proxy.update_group, group.Group) + def test_add_user_to_group(self): + self._verify( + "openstack.identity.v3.group.Group.add_user", + self.proxy.add_user_to_group, + method_args=['uid', 'gid'], + expected_args=[ + self.proxy, + self.proxy._get_resource(user.User, 'uid'), + ] + ) + + def test_remove_user_from_group(self): + self._verify( + "openstack.identity.v3.group.Group.remove_user", + self.proxy.remove_user_from_group, + method_args=['uid', 'gid'], + expected_args=[ + self.proxy, + self.proxy._get_resource(user.User, 'uid'), + ] + ) + + def test_check_user_in_group(self): + self._verify( + "openstack.identity.v3.group.Group.check_user", + self.proxy.check_user_in_group, + method_args=['uid', 'gid'], + expected_args=[ + self.proxy, + self.proxy._get_resource(user.User, 'uid'), + ] + ) + + +class TestIdentityProxyPolicy(TestIdentityProxyBase): + def test_policy_create_attrs(self): self.verify_create(self.proxy.create_policy, policy.Policy) @@ -147,6 +192,9 @@ def test_policies(self): def test_policy_update(self): self.verify_update(self.proxy.update_policy, policy.Policy) + +class TestIdentityProxyProject(TestIdentityProxyBase): + def test_project_create_attrs(self): self.verify_create(self.proxy.create_project, project.Project) @@ -176,6 +224,9 @@ def test_user_projects(self): def test_project_update(self): self.verify_update(self.proxy.update_project, project.Project) + +class TestIdentityProxyService(TestIdentityProxyBase): + def test_service_create_attrs(self): self.verify_create(self.proxy.create_service, service.Service) @@ -197,6 +248,9 @@ def test_services(self): def test_service_update(self): self.verify_update(self.proxy.update_service, service.Service) + +class TestIdentityProxyUser(TestIdentityProxyBase): + def test_user_create_attrs(self): self.verify_create(self.proxy.create_user, user.User) @@ -218,6 +272,9 @@ def test_users(self): def test_user_update(self): self.verify_update(self.proxy.update_user, user.User) + +class TestIdentityProxyTrust(TestIdentityProxyBase): + def test_trust_create_attrs(self): self.verify_create(self.proxy.create_trust, trust.Trust) @@ -236,6 +293,9 @@ def test_trust_get(self): def test_trusts(self): self.verify_list(self.proxy.trusts, trust.Trust) + +class TestIdentityProxyRegion(TestIdentityProxyBase): + def test_region_create_attrs(self): self.verify_create(self.proxy.create_region, region.Region) @@ -257,6 +317,9 @@ def test_regions(self): def test_region_update(self): self.verify_update(self.proxy.update_region, region.Region) + +class TestIdentityProxyRole(TestIdentityProxyBase): + def test_role_create_attrs(self): self.verify_create(self.proxy.create_role, role.Role) diff --git a/releasenotes/notes/add-user-group-assignment-9c419b6c6bfe392c.yaml b/releasenotes/notes/add-user-group-assignment-9c419b6c6bfe392c.yaml new file mode 100644 index 000000000..266b5f3c4 --- /dev/null +++ b/releasenotes/notes/add-user-group-assignment-9c419b6c6bfe392c.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add support for user group assignments in identity service. From 9deb08e041bce307b063c21b0c6fb181a5f8dc57 Mon Sep 17 00:00:00 2001 From: songwenping Date: Mon, 9 Aug 2021 17:06:07 +0800 Subject: [PATCH 2903/3836] Add description args for device profile create Change-Id: If80884a827d636fcb6c0365773eac46c1ddfa1c9 --- openstack/accelerator/v2/device_profile.py | 2 ++ openstack/tests/unit/accelerator/v2/test_device_profile.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/openstack/accelerator/v2/device_profile.py b/openstack/accelerator/v2/device_profile.py index a09f48167..a66bc772e 100644 --- a/openstack/accelerator/v2/device_profile.py +++ b/openstack/accelerator/v2/device_profile.py @@ -25,6 +25,8 @@ class DeviceProfile(resource.Resource): #: The timestamp when this device_profile was created. created_at = resource.Body('created_at') + #: The description of the device profile + description = resource.Body('description') #: The groups of the device profile groups = resource.Body('groups') #: The name of the device profile diff --git a/openstack/tests/unit/accelerator/v2/test_device_profile.py b/openstack/tests/unit/accelerator/v2/test_device_profile.py index fcaea7e48..ded8c4c69 100644 --- a/openstack/tests/unit/accelerator/v2/test_device_profile.py +++ b/openstack/tests/unit/accelerator/v2/test_device_profile.py @@ -27,7 +27,8 @@ "resources:CUSTOM_MEMORY": "200", "trait:CUSTOM_TRAIT_ALWAYS": "required", } - ] + ], + 'description': 'description_test' } @@ -51,3 +52,4 @@ def test_make_it(self): self.assertEqual(FAKE['uuid'], sot.uuid) self.assertEqual(FAKE['name'], sot.name) self.assertEqual(FAKE['groups'], sot.groups) + self.assertEqual(FAKE['description'], sot.description) From 67c8c1918993a7a9f1a748f08f8baaa961f25ee1 Mon Sep 17 00:00:00 2001 From: ThomasBucaioni Date: Sat, 4 Sep 2021 11:31:56 +0200 Subject: [PATCH 2904/3836] Replace the 'try except' block The goal is to avoid the error ``` Attribute [] not found in []: ''. ``` in debug messages when the field `_alternate_id` is empty Change-Id: I70dc26ff161227ea1d65d935a743dc6f6d3485ec --- openstack/resource.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/openstack/resource.py b/openstack/resource.py index dee4ec577..2305080d1 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -625,12 +625,9 @@ def __getattribute__(self, name): if name in self._body: return self._body[name] else: - try: - return self._body[self._alternate_id()] - except KeyError as e: - LOG.debug("Attribute [%s] not found in [%s]: %s.", - self._alternate_id(), self._body, e) - return None + key = self._alternate_id() + if key: + return self._body.get(key) else: try: return object.__getattribute__(self, name) From 4914d8d754e312a5ca8cc8a6f47a5a0d7d934498 Mon Sep 17 00:00:00 2001 From: Kafilat Adeleke Date: Thu, 17 Jun 2021 18:01:39 +0000 Subject: [PATCH 2905/3836] Add user message to shared file system Introduce User Message class with basic methods including list, delete, get to shared file system storage service. Change-Id: I0cc3586f6681eb0941294e0ef27edb4d7e1f409a --- .../user/proxies/shared_file_system.rst | 11 +++ .../resources/shared_file_system/index.rst | 1 + .../shared_file_system/v2/user_message.rst | 13 ++++ openstack/shared_file_system/v2/_proxy.py | 56 ++++++++++++++ .../shared_file_system/v2/user_message.py | 54 ++++++++++++++ .../shared_file_system/test_user_message.py | 29 ++++++++ .../unit/shared_file_system/v2/test_proxy.py | 28 +++++++ .../v2/test_user_message.py | 74 +++++++++++++++++++ ...ssage-to-shared-file-85d7bbccf8347c4f.yaml | 5 ++ 9 files changed, 271 insertions(+) create mode 100644 doc/source/user/resources/shared_file_system/v2/user_message.rst create mode 100644 openstack/shared_file_system/v2/user_message.py create mode 100644 openstack/tests/functional/shared_file_system/test_user_message.py create mode 100644 openstack/tests/unit/shared_file_system/v2/test_user_message.py create mode 100644 releasenotes/notes/add-user-message-to-shared-file-85d7bbccf8347c4f.yaml diff --git a/doc/source/user/proxies/shared_file_system.rst b/doc/source/user/proxies/shared_file_system.rst index 6d71a2521..1b28ad124 100644 --- a/doc/source/user/proxies/shared_file_system.rst +++ b/doc/source/user/proxies/shared_file_system.rst @@ -44,3 +44,14 @@ Systems Service. .. autoclass:: openstack.shared_file_system.v2._proxy.Proxy :noindex: :members: storage_pools + + +Shared File System User Messages +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +View and manipulate asynchronous user messages emitted by the Shared +File Systems service. + +.. autoclass:: openstack.shared_file_system.v2._proxy.Proxy + :noindex: + :members: user_messages, get_user_message, delete_user_message diff --git a/doc/source/user/resources/shared_file_system/index.rst b/doc/source/user/resources/shared_file_system/index.rst index 8eeb1e7f3..37f31da0b 100644 --- a/doc/source/user/resources/shared_file_system/index.rst +++ b/doc/source/user/resources/shared_file_system/index.rst @@ -7,3 +7,4 @@ Shared File System service resources v2/availability_zone v2/storage_pool v2/share + v2/user_message diff --git a/doc/source/user/resources/shared_file_system/v2/user_message.rst b/doc/source/user/resources/shared_file_system/v2/user_message.rst new file mode 100644 index 000000000..de3b21f66 --- /dev/null +++ b/doc/source/user/resources/shared_file_system/v2/user_message.rst @@ -0,0 +1,13 @@ +openstack.shared_file_system.v2.user_message +============================================ + +.. automodule:: openstack.shared_file_system.v2.user_message + +The UserMessage Class +--------------------- + +The ``UserMessage`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.shared_file_system.v2.user_message.UserMessage + :members: diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index f64680c85..5b88e7516 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -17,6 +17,9 @@ from openstack.shared_file_system.v2 import ( storage_pool as _storage_pool ) +from openstack.shared_file_system.v2 import ( + user_message as _user_message +) from openstack.shared_file_system.v2 import share as _share @@ -168,3 +171,56 @@ def storage_pools(self, details=True, **query): base_path = '/scheduler-stats/pools/detail' if details else None return self._list( _storage_pool.StoragePool, base_path=base_path, **query) + + def user_messages(self, **query): + """List shared file system user messages + + :param kwargs query: Optional query parameters to be sent to limit + the messages being returned. Available parameters include: + + * action_id: The ID of the action during which the message + was created. + * detail_id: The ID of the message detail. + * limit: The maximum number of shares to return. + * message_level: The message level. + * offset: The offset to define start point of share or share + group listing. + * sort_key: The key to sort a list of messages. + * sort_dir: The direction to sort a list of shares. + * project_id: The ID of the project for which the message + was created. + * request_id: The ID of the request during which the message + was created. + * resource_id: The UUID of the resource for which the message + was created. + * resource_type: The type of the resource for which the message + was created. + + :returns: A generator of user message resources + :rtype: :class:`~openstack.shared_file_system.v2. + user_message.UserMessage` + """ + return self._list( + _user_message.UserMessage, **query) + + def get_user_message(self, message_id): + """List details of a single user message + + :param message_id: The ID of the user message + :returns: Details of the identified user message + :rtype: :class:`~openstack.shared_file_system.v2. + user_message.UserMessage` + """ + return self._get(_user_message.UserMessage, message_id) + + def delete_user_message(self, message_id, ignore_missing=True): + """Deletes a single user message + + :param message_id: The ID of the user message + :returns: Result of the "delete" on the user message + :rtype: :class:`~openstack.shared_file_system.v2. + user_message.UserMessage` + """ + return self._delete( + _user_message.UserMessage, message_id, + ignore_missing=ignore_missing) diff --git a/openstack/shared_file_system/v2/user_message.py b/openstack/shared_file_system/v2/user_message.py new file mode 100644 index 000000000..262fc4d16 --- /dev/null +++ b/openstack/shared_file_system/v2/user_message.py @@ -0,0 +1,54 @@ +# 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 resource + + +class UserMessage(resource.Resource): + resource_key = "message" + resources_key = "messages" + base_path = "/messages" + + # capabilities + allow_fetch = True + allow_commit = False + allow_delete = True + allow_list = True + allow_head = False + + _query_mapping = resource.QueryParameters( + "message_id" + ) + + _max_microversion = '2.37' + + #: Properties + #: The action ID of the user message + action_id = resource.Body("action_id") + #: Indicate when the user message was created + created_at = resource.Body("created_at") + #: The detail ID of the user message + detail_id = resource.Body("detail_id", type=str) + #: Indicate when the share message expires + expires_at = resource.Body("expires_at", type=str) + #: The message level of the user message + message_level = resource.Body("message_level", type=str) + #: The project ID of the user message + project_id = resource.Body("project_id", type=str) + #: The request ID of the user message + request_id = resource.Body("request_id", type=str) + #: The resource ID of the user message + resource_id = resource.Body("resource_id", type=str) + #: The resource type of the user message + resource_type = resource.Body("resource_type", type=str) + #: The message for the user message + user_message = resource.Body("user_message", type=str) diff --git a/openstack/tests/functional/shared_file_system/test_user_message.py b/openstack/tests/functional/shared_file_system/test_user_message.py new file mode 100644 index 000000000..a6b6920b6 --- /dev/null +++ b/openstack/tests/functional/shared_file_system/test_user_message.py @@ -0,0 +1,29 @@ +# 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.tests.functional.shared_file_system import base + + +class UserMessageTest(base.BaseSharedFileSystemTest): + + def test_user_messages(self): + # TODO(kafilat): We must intentionally cause an asynchronous failure to + # ensure that at least one user message exists; + u_messages = self.user_cloud.shared_file_system.user_messages() + # self.assertGreater(len(list(u_messages)), 0) + for u_message in u_messages: + for attribute in ('id', 'created_at', 'name'): + self.assertTrue(hasattr(u_message, attribute)) + self.assertIsInstance(getattr(u_message, attribute), 'str') + + self.conn.shared_file_system.delete_user_message( + u_message) diff --git a/openstack/tests/unit/shared_file_system/v2/test_proxy.py b/openstack/tests/unit/shared_file_system/v2/test_proxy.py index 3d04ad8c2..251251b44 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_proxy.py +++ b/openstack/tests/unit/shared_file_system/v2/test_proxy.py @@ -15,6 +15,7 @@ from openstack.shared_file_system.v2 import _proxy from openstack.shared_file_system.v2 import share from openstack.shared_file_system.v2 import storage_pool +from openstack.shared_file_system.v2 import user_message from openstack.tests.unit import test_proxy_base @@ -79,3 +80,30 @@ def test_storage_pool_not_detailed(self): self.proxy.storage_pools, storage_pool.StoragePool, method_kwargs={"details": False, "backend": "alpha"}, expected_kwargs={"backend": "alpha"}) + + +class TestUserMessageProxy(test_proxy_base.TestProxyBase): + + def setUp(self): + super(TestUserMessageProxy, self).setUp() + self.proxy = _proxy.Proxy(self.session) + + def test_user_messages(self): + self.verify_list(self.proxy.user_messages, user_message.UserMessage) + + def test_user_messages_queried(self): + self.verify_list( + self.proxy.user_messages, user_message.UserMessage, + method_kwargs={"action_id": "1"}, + expected_kwargs={"action_id": "1"}) + + def test_user_message_get(self): + self.verify_get(self.proxy.get_user_message, user_message.UserMessage) + + def test_delete_user_message(self): + self.verify_delete( + self.proxy.delete_user_message, user_message.UserMessage, False) + + def test_delete_user_message_true(self): + self.verify_delete( + self.proxy.delete_user_message, user_message.UserMessage, True) diff --git a/openstack/tests/unit/shared_file_system/v2/test_user_message.py b/openstack/tests/unit/shared_file_system/v2/test_user_message.py new file mode 100644 index 000000000..a718d4062 --- /dev/null +++ b/openstack/tests/unit/shared_file_system/v2/test_user_message.py @@ -0,0 +1,74 @@ +# 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.shared_file_system.v2 import user_message +from openstack.tests.unit import base + + +IDENTIFIER = "2784bc88-b729-4220-a6bb-a8b7a8f53aad" +EXAMPLE = { + "id": IDENTIFIER, + "project_id": "dcc9de3c5fc8471ba3662dbb2b6166d5", + "action_id": "001", + "detail_id": "008", + "message_level": "ERROR", + "created_at": "2021-03-26T05:16:39.000000", + "expires_at": "2021-04-25T05:16:39.000000", + "request_id": "req-e4b3e6de-ce4d-4ef2-b1e7-0087200e4db3", + "resource_type": "SHARE", + "resource_id": "c2e4ca07-8c37-4014-92c9-2171c7813fa0", + "user_message": ( + "allocate host: No storage could be allocated" + "for this share request, Capabilities filter" + "didn't succeed.") +} + + +class TestUserMessage(base.TestCase): + + def test_basic(self): + message = user_message.UserMessage() + self.assertEqual('messages', message.resources_key) + self.assertEqual('/messages', message.base_path) + self.assertTrue(message.allow_list) + self.assertFalse(message.allow_create) + self.assertFalse(message.allow_commit) + self.assertTrue(message.allow_delete) + self.assertTrue(message.allow_fetch) + self.assertFalse(message.allow_head) + + self.assertDictEqual({ + "limit": "limit", + "marker": "marker", + "message_id": "message_id" + }, + message._query_mapping._mapping) + + def test_user_message(self): + messages = user_message.UserMessage(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], messages.id) + self.assertEqual(EXAMPLE['resource_id'], messages.resource_id) + self.assertEqual( + EXAMPLE['message_level'], messages.message_level) + self.assertEqual( + EXAMPLE['user_message'], messages.user_message) + self.assertEqual(EXAMPLE['expires_at'], messages.expires_at) + self.assertEqual(EXAMPLE['detail_id'], messages.detail_id) + self.assertEqual(EXAMPLE['created_at'], messages.created_at) + self.assertEqual( + EXAMPLE['request_id'], messages.request_id) + self.assertEqual( + EXAMPLE['project_id'], messages.project_id) + self.assertEqual( + EXAMPLE['resource_type'], messages.resource_type) + self.assertEqual( + EXAMPLE['action_id'], messages.action_id) diff --git a/releasenotes/notes/add-user-message-to-shared-file-85d7bbccf8347c4f.yaml b/releasenotes/notes/add-user-message-to-shared-file-85d7bbccf8347c4f.yaml new file mode 100644 index 000000000..b4d449230 --- /dev/null +++ b/releasenotes/notes/add-user-message-to-shared-file-85d7bbccf8347c4f.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added support to list, get, and delete user messages + on the shared file system service. From 7f87d2eee45930d274ebf4b851e380ca1352d296 Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Tue, 7 Sep 2021 16:32:45 +0200 Subject: [PATCH 2906/3836] Start splitting the `TestImageProxy` class Five new classes `TestImage`, `TestMember`, `TestSchema`, `TestTask`, `TestMisc` are created. They contain all the tests previously embedded in `TestImageProxy`. In `TestImageProxy`, only the `setUp` method remains Change-Id: I85cf1f7b4738b76936b0216b1bd186bb08f88ef9 --- openstack/tests/unit/image/v2/test_proxy.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 0a2826872..9f86d96c2 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -45,6 +45,8 @@ def setUp(self): self.proxy = _proxy.Proxy(self.session) self.proxy._connection = self.cloud + +class TestImage(TestImageProxy): def test_image_import_no_required_attrs(self): # container_format and disk_format are required attrs of the image existing_image = image.Image(id="id") @@ -321,6 +323,8 @@ def test_reactivate_image(self): method_args=["image"], expected_args=[self.proxy]) + +class TestMember(TestImageProxy): def test_member_create(self): self.verify_create(self.proxy.add_member, member.Member, method_kwargs={"image": "test_id"}, @@ -381,6 +385,8 @@ def test_members(self): method_kwargs={'image': 'image_1'}, expected_kwargs={'image_id': 'image_1'}) + +class TestSchema(TestImageProxy): def test_images_schema_get(self): self._verify( "openstack.proxy.Proxy._get", @@ -413,6 +419,8 @@ def test_member_schema_get(self): expected_kwargs={ 'base_path': '/schemas/member', 'requires_id': False}) + +class TestTask(TestImageProxy): def test_task_get(self): self.verify_get(self.proxy.get_task, task.Task) @@ -510,6 +518,8 @@ def test_task_schema_get(self): expected_kwargs={ 'base_path': '/schemas/task', 'requires_id': False}) + +class TestMisc(TestImageProxy): def test_stores(self): self.verify_list(self.proxy.stores, si.Store) From a2b7c5400245e9c59a52172e22f54730d17c371a Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 1 Sep 2021 10:23:08 +0200 Subject: [PATCH 2907/3836] Optimize code by moving misc functions to utils - There is no need to keep those functions in the connection object. - get rid of file hashes cache, since this is not safe to rely on filename for the cache (file checksums were created exactly to guarantee this). Change-Id: Idd200a1a5a72710b8eb4556bf9fb95b96be6d99b --- openstack/cloud/_object_store.py | 57 ++--------------------- openstack/image/_base_proxy.py | 7 +-- openstack/object_store/v1/_proxy.py | 7 +-- openstack/tests/unit/cloud/test_object.py | 3 +- openstack/utils.py | 40 ++++++++++++++++ 5 files changed, 53 insertions(+), 61 deletions(-) diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index d151a59b8..c8161df15 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -15,7 +15,6 @@ # openstack.resource.Resource.list and openstack.resource2.Resource.list import collections import concurrent.futures -import hashlib import json import os import types # noqa @@ -196,39 +195,6 @@ def get_container_access(self, name): raise exc.OpenStackCloudException( "Could not determine container access for ACL: %s." % acl) - def _get_file_hashes(self, filename): - file_key = "{filename}:{mtime}".format( - filename=filename, - mtime=os.stat(filename).st_mtime) - if file_key not in self._file_hash_cache: - self.log.debug( - 'Calculating hashes for %(filename)s', {'filename': filename}) - (md5, sha256) = (None, None) - with open(filename, 'rb') as file_obj: - (md5, sha256) = self._calculate_data_hashes(file_obj) - self._file_hash_cache[file_key] = dict( - md5=md5, sha256=sha256) - self.log.debug( - "Image file %(filename)s md5:%(md5)s sha256:%(sha256)s", - {'filename': filename, - 'md5': self._file_hash_cache[file_key]['md5'], - 'sha256': self._file_hash_cache[file_key]['sha256']}) - return (self._file_hash_cache[file_key]['md5'], - self._file_hash_cache[file_key]['sha256']) - - def _calculate_data_hashes(self, data): - md5 = utils.md5(usedforsecurity=False) - sha256 = hashlib.sha256() - - if hasattr(data, 'read'): - for chunk in iter(lambda: data.read(8192), b''): - md5.update(chunk) - sha256.update(chunk) - else: - md5.update(data) - sha256.update(data) - return (md5.hexdigest(), sha256.hexdigest()) - @_utils.cache_on_arguments() def get_object_capabilities(self): """Get infomation about the object-storage service @@ -293,13 +259,13 @@ def is_object_stale( return True if not (file_md5 or file_sha256): - (file_md5, file_sha256) = self._get_file_hashes(filename) + (file_md5, file_sha256) = utils._get_file_hashes(filename) md5_key = metadata.get( self._OBJECT_MD5_KEY, metadata.get(self._SHADE_OBJECT_MD5_KEY, '')) sha256_key = metadata.get( self._OBJECT_SHA256_KEY, metadata.get( self._SHADE_OBJECT_SHA256_KEY, '')) - up_to_date = self._hashes_up_to_date( + up_to_date = utils._hashes_up_to_date( md5=file_md5, sha256=file_sha256, md5_key=md5_key, sha256_key=sha256_key) @@ -405,7 +371,7 @@ def create_object( filename = name if generate_checksums and (md5 is None or sha256 is None): - (md5, sha256) = self._get_file_hashes(filename) + (md5, sha256) = utils._get_file_hashes(filename) if md5: headers[self._OBJECT_MD5_KEY] = md5 or '' if sha256: @@ -857,20 +823,3 @@ def _wait_for_futures(self, futures, raise_on_error=True): # can try again retries.append(completed.result()) return results, retries - - def _hashes_up_to_date(self, md5, sha256, md5_key, sha256_key): - '''Compare md5 and sha256 hashes for being up to date - - md5 and sha256 are the current values. - md5_key and sha256_key are the previous values. - ''' - up_to_date = False - if md5 and md5_key == md5: - up_to_date = True - if sha256 and sha256_key == sha256: - up_to_date = True - if md5 and md5_key != md5: - up_to_date = False - if sha256 and sha256_key != sha256: - up_to_date = False - return up_to_date diff --git a/openstack/image/_base_proxy.py b/openstack/image/_base_proxy.py index 1280181d0..56af3043c 100644 --- a/openstack/image/_base_proxy.py +++ b/openstack/image/_base_proxy.py @@ -15,6 +15,7 @@ from openstack import exceptions from openstack import proxy +from openstack import utils class BaseImageProxy(proxy.Proxy, metaclass=abc.ABCMeta): @@ -151,9 +152,9 @@ def create_image( 'direct binary object') if not (md5 or sha256) and validate_checksum: if filename: - (md5, sha256) = self._connection._get_file_hashes(filename) + (md5, sha256) = utils._get_file_hashes(filename) elif data and isinstance(data, bytes): - (md5, sha256) = self._connection._calculate_data_hashes(data) + (md5, sha256) = utils._calculate_data_hashes(data) if allow_duplicates: current_image = None else: @@ -167,7 +168,7 @@ def create_image( sha256_key = props.get( self._IMAGE_SHA256_KEY, props.get(self._SHADE_IMAGE_SHA256_KEY, '')) - up_to_date = self._connection._hashes_up_to_date( + up_to_date = utils._hashes_up_to_date( md5=md5, sha256=sha256, md5_key=md5_key, sha256_key=sha256_key) if up_to_date: diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index 7fa36a551..6722440ff 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -27,6 +27,7 @@ from openstack.object_store.v1 import info as _info from openstack.object_store.v1 import obj as _obj from openstack import proxy +from openstack import utils DEFAULT_OBJECT_SEGMENT_SIZE = 1073741824 # 1GB DEFAULT_MAX_FILE_SIZE = (5 * 1024 * 1024 * 1024 + 2) / 2 @@ -353,7 +354,7 @@ def create_object( filename = name if generate_checksums and (md5 is None or sha256 is None): - (md5, sha256) = self._connection._get_file_hashes(filename) + (md5, sha256) = utils._get_file_hashes(filename) if md5: headers[self._connection._OBJECT_MD5_KEY] = md5 or '' if sha256: @@ -513,14 +514,14 @@ def is_object_stale( if not (file_md5 or file_sha256): (file_md5, file_sha256) = \ - self._connection._get_file_hashes(filename) + utils._get_file_hashes(filename) md5_key = metadata.get( self._connection._OBJECT_MD5_KEY, metadata.get(self._connection._SHADE_OBJECT_MD5_KEY, '')) sha256_key = metadata.get( self._connection._OBJECT_SHA256_KEY, metadata.get( self._connection._SHADE_OBJECT_SHA256_KEY, '')) - up_to_date = self._connection._hashes_up_to_date( + up_to_date = utils._hashes_up_to_date( md5=file_md5, sha256=file_sha256, md5_key=md5_key, sha256_key=sha256_key) diff --git a/openstack/tests/unit/cloud/test_object.py b/openstack/tests/unit/cloud/test_object.py index b04c4688d..3cc861ec5 100644 --- a/openstack/tests/unit/cloud/test_object.py +++ b/openstack/tests/unit/cloud/test_object.py @@ -25,6 +25,7 @@ from openstack.object_store.v1 import container from openstack.object_store.v1 import obj from openstack.tests.unit import base +from openstack import utils class BaseTestObject(base.TestCase): @@ -654,7 +655,7 @@ def setUp(self): self.object_file = tempfile.NamedTemporaryFile(delete=False) self.object_file.write(self.content) self.object_file.close() - (self.md5, self.sha256) = self.cloud._get_file_hashes( + (self.md5, self.sha256) = utils._get_file_hashes( self.object_file.name) self.endpoint = self.cloud._object_store_client.get_endpoint() diff --git a/openstack/utils.py b/openstack/utils.py index 5fff2b3e5..9ef8bdb92 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -233,6 +233,24 @@ def maximum_supported_microversion(adapter, client_maximum): return discover.version_to_string(result) +def _hashes_up_to_date(md5, sha256, md5_key, sha256_key): + '''Compare md5 and sha256 hashes for being up to date + + md5 and sha256 are the current values. + md5_key and sha256_key are the previous values. + ''' + up_to_date = False + if md5 and md5_key == md5: + up_to_date = True + if sha256 and sha256_key == sha256: + up_to_date = True + if md5 and md5_key != md5: + up_to_date = False + if sha256 and sha256_key != sha256: + up_to_date = False + return up_to_date + + try: _test_md5 = hashlib.md5(usedforsecurity=False) # nosec @@ -253,6 +271,28 @@ def md5(string=b'', usedforsecurity=True): return hashlib.md5(string) # nosec +def _calculate_data_hashes(data): + _md5 = md5(usedforsecurity=False) + _sha256 = hashlib.sha256() + + if hasattr(data, 'read'): + for chunk in iter(lambda: data.read(8192), b''): + _md5.update(chunk) + _sha256.update(chunk) + else: + _md5.update(data) + _sha256.update(data) + return (_md5.hexdigest(), _sha256.hexdigest()) + + +def _get_file_hashes(filename): + (_md5, _sha256) = (None, None) + with open(filename, 'rb') as file_obj: + (_md5, _sha256) = _calculate_data_hashes(file_obj) + + return (_md5, _sha256) + + class TinyDAG: """Tiny DAG From 6a2420dbec931167c06c252b26edcb82f8b67209 Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Wed, 8 Sep 2021 16:41:02 +0200 Subject: [PATCH 2908/3836] Splits the `TestAcceleratorProxy` class Three new classes `TestAcceleratorDevice`, `TestAcceleratorDeployable`, and `TestAcceleratorRequest` are created. They contain all the tests previously embedded in `TestAcceleratorProxy`. In `TestAcceleratorProxy`, only the `setUp` method remains Change-Id: I543e1b5064e6e4fa15109ba0ce62364d5efb983c --- openstack/tests/unit/accelerator/v2/test_proxy.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openstack/tests/unit/accelerator/v2/test_proxy.py b/openstack/tests/unit/accelerator/v2/test_proxy.py index c9c682d14..c4ebfe78a 100644 --- a/openstack/tests/unit/accelerator/v2/test_proxy.py +++ b/openstack/tests/unit/accelerator/v2/test_proxy.py @@ -22,9 +22,13 @@ def setUp(self): super(TestAcceleratorProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) + +class TestAcceleratorDeployable(TestAcceleratorProxy): def test_list_deployables(self): self.verify_list(self.proxy.deployables, deployable.Deployable) + +class TestAcceleratorDevice(TestAcceleratorProxy): def test_list_device_profile(self): self.verify_list(self.proxy.device_profiles, device_profile.DeviceProfile) @@ -45,6 +49,8 @@ def test_get_device_profile(self): self.verify_get(self.proxy.get_device_profile, device_profile.DeviceProfile) + +class TestAcceleratorRequest(TestAcceleratorProxy): def test_list_accelerator_request(self): self.verify_list(self.proxy.accelerator_requests, accelerator_request.AcceleratorRequest) From bba9c5c5085173ad633c6b5691a8d29a0b66ce63 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 9 Sep 2021 15:11:39 +0200 Subject: [PATCH 2909/3836] Fix setting initial object/container metadata. As of now we can not easily set object_store resources metadata on the creation when using proxy layer directly. While create_object claimed that it was never really working. As a prerequisite for switching cloud layer to rely on proxy we need to address this. Change-Id: Icacef2044efd6e1096bb88aa830dc64cac43bde5 --- openstack/object_store/v1/_base.py | 17 +++++++++++++++++ openstack/object_store/v1/_proxy.py | 8 ++------ openstack/object_store/v1/obj.py | 5 +++-- .../unit/object_store/v1/test_container.py | 7 +++++-- .../tests/unit/object_store/v1/test_obj.py | 10 +++++++--- .../tests/unit/object_store/v1/test_proxy.py | 7 ++++++- .../swift-set-metadata-c18c60e440f9e4a7.yaml | 5 +++++ 7 files changed, 45 insertions(+), 14 deletions(-) create mode 100644 releasenotes/notes/swift-set-metadata-c18c60e440f9e4a7.yaml diff --git a/openstack/object_store/v1/_base.py b/openstack/object_store/v1/_base.py index 779f1e92d..cb8bbb464 100644 --- a/openstack/object_store/v1/_base.py +++ b/openstack/object_store/v1/_base.py @@ -26,6 +26,23 @@ class BaseResource(resource.Resource): _custom_metadata_prefix = None _system_metadata = dict() + def __init__(self, metadata=None, **attrs): + """Process and save metadata known at creation stage + """ + super().__init__(**attrs) + if metadata is not None: + for k, v in metadata.items(): + if not k.lower().startswith( + self._custom_metadata_prefix.lower()): + self.metadata[self._custom_metadata_prefix + k] = v + else: + self.metadata[k] = v + + def _prepare_request(self, **kwargs): + request = super()._prepare_request(**kwargs) + request.headers.update(self._calculate_headers(self.metadata)) + return request + def _calculate_headers(self, metadata): headers = {} for key in metadata: diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index 6722440ff..9e9130330 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -359,11 +359,6 @@ def create_object( headers[self._connection._OBJECT_MD5_KEY] = md5 or '' if sha256: headers[self._connection._OBJECT_SHA256_KEY] = sha256 or '' - for (k, v) in metadata.items(): - if not k.lower().startswith('x-object-meta-'): - headers['x-object-meta-' + k] = v - else: - headers[k] = v container_name = self._get_container_name(container=container) endpoint = '{container}/{name}'.format(container=container_name, @@ -376,7 +371,8 @@ def create_object( # TODO(gtema): custom headers need to be somehow injected return self._create( _obj.Object, container=container_name, - name=name, data=data, **headers) + name=name, data=data, metadata=metadata, + **headers) # segment_size gets used as a step value in a range call, so needs # to be an int diff --git a/openstack/object_store/v1/obj.py b/openstack/object_store/v1/obj.py index 116b8ccaf..8c7a11aa7 100644 --- a/openstack/object_store/v1/obj.py +++ b/openstack/object_store/v1/obj.py @@ -175,7 +175,7 @@ class Object(_base.BaseResource): has_body = False def __init__(self, data=None, **attrs): - super(_base.BaseResource, self).__init__(**attrs) + super().__init__(**attrs) self.data = data # The Object Store treats the metadata for its resources inconsistently so @@ -290,7 +290,8 @@ def create(self, session, base_path=None): response = session.put( request.url, data=self.data, - headers=request.headers) + headers=request.headers + ) self._translate_response(response, has_body=False) return self diff --git a/openstack/tests/unit/object_store/v1/test_container.py b/openstack/tests/unit/object_store/v1/test_container.py index 7f522aa65..36698e752 100644 --- a/openstack/tests/unit/object_store/v1/test_container.py +++ b/openstack/tests/unit/object_store/v1/test_container.py @@ -137,6 +137,7 @@ def _test_create_update(self, sot, sot_call, sess_method): "x-container-read": "some ACL", "x-container-write": "another ACL", "x-detect-content-type": 'True', + "X-Container-Meta-foo": "bar" } self.register_uris([ dict(method=sess_method, uri=self.container_endpoint, @@ -148,11 +149,13 @@ def _test_create_update(self, sot, sot_call, sess_method): self.assert_calls() def test_create(self): - sot = container.Container.new(name=self.container) + sot = container.Container.new( + name=self.container, metadata={'foo': 'bar'}) self._test_create_update(sot, sot.create, 'PUT') def test_commit(self): - sot = container.Container.new(name=self.container) + sot = container.Container.new( + name=self.container, metadata={'foo': 'bar'}) self._test_create_update(sot, sot.commit, 'POST') def test_to_dict_recursion(self): diff --git a/openstack/tests/unit/object_store/v1/test_obj.py b/openstack/tests/unit/object_store/v1/test_obj.py index 85642251e..448652796 100644 --- a/openstack/tests/unit/object_store/v1/test_obj.py +++ b/openstack/tests/unit/object_store/v1/test_obj.py @@ -128,10 +128,14 @@ def test_download(self): self.assert_calls() def _test_create(self, method, data): - sot = obj.Object.new(container=self.container, name=self.object, - data=data) + sot = obj.Object.new( + container=self.container, name=self.object, + data=data, metadata={'foo': 'bar'}) sot.is_newest = True - sent_headers = {"x-newest": 'True'} + sent_headers = { + "x-newest": 'True', + "X-Object-Meta-foo": "bar" + } self.register_uris([ dict(method=method, uri=self.object_endpoint, headers=self.headers, diff --git a/openstack/tests/unit/object_store/v1/test_proxy.py b/openstack/tests/unit/object_store/v1/test_proxy.py index 81cf1f19c..a88bb5838 100644 --- a/openstack/tests/unit/object_store/v1/test_proxy.py +++ b/openstack/tests/unit/object_store/v1/test_proxy.py @@ -93,7 +93,12 @@ def test_object_delete_ignore(self): self._test_object_delete(True) def test_object_create_attrs(self): - kwargs = {"name": "test", "data": "data", "container": "name"} + kwargs = { + "name": "test", + "data": "data", + "container": "name", + "metadata": {} + } self._verify( "openstack.proxy.Proxy._create", diff --git a/releasenotes/notes/swift-set-metadata-c18c60e440f9e4a7.yaml b/releasenotes/notes/swift-set-metadata-c18c60e440f9e4a7.yaml new file mode 100644 index 000000000..6fe599ef4 --- /dev/null +++ b/releasenotes/notes/swift-set-metadata-c18c60e440f9e4a7.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + It is now possible to pass `metadata` parameter directly into the create_container, + create_object object_store methods and will not be ignored. From b4f93e335e305faf04af1b4828195a9f550628af Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 9 Sep 2021 17:29:56 +0200 Subject: [PATCH 2910/3836] Change image.hw_qemu_guest_agent to be string hw_qemu_guest_agent attribute of the image is a string boolean supporting `yes` and `no` and not a real boolean like we were expecting. Replace it to be string instead. Change-Id: I5b9714e4388df440bf881033283da4e8aa1db497 --- openstack/image/v2/image.py | 5 +++-- openstack/tests/unit/image/v2/test_image.py | 2 +- .../fix-image-hw_qemu_guest_agent-bf1147e52c84b5e8.yaml | 5 +++++ 3 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/fix-image-hw_qemu_guest_agent-bf1147e52c84b5e8.yaml diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index bbf700a31..9509b95c9 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -224,8 +224,9 @@ class Image(resource.Resource, resource.TagMixin, _download.DownloadMixin): os_type = resource.Body('os_type') #: The operating system admin username. os_admin_user = resource.Body('os_admin_user') - #: If true, QEMU guest agent will be exposed to the instance. - hw_qemu_guest_agent = resource.Body('hw_qemu_guest_agent', type=bool) + #: A string boolean, which if "true", QEMU guest agent will be exposed + #: to the instance. + hw_qemu_guest_agent = resource.Body('hw_qemu_guest_agent', type=str) #: If true, require quiesce on snapshot via QEMU guest agent. os_require_quiesce = resource.Body('os_require_quiesce', type=bool) #: The URL for the schema describing a virtual machine image. diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 9ddd95305..0676bd326 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -84,7 +84,7 @@ 'auto_disk_config': True, 'os_type': '49', 'os_admin_user': 'ubuntu', - 'hw_qemu_guest_agent': True, + 'hw_qemu_guest_agent': 'yes', 'os_require_quiesce': True, } diff --git a/releasenotes/notes/fix-image-hw_qemu_guest_agent-bf1147e52c84b5e8.yaml b/releasenotes/notes/fix-image-hw_qemu_guest_agent-bf1147e52c84b5e8.yaml new file mode 100644 index 000000000..3d67a068a --- /dev/null +++ b/releasenotes/notes/fix-image-hw_qemu_guest_agent-bf1147e52c84b5e8.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + hw_qemu_guest_agent attribute of the image is a string boolean with values + `yes` and `no`. From dc0c9d900c73455bad9853c081e7f66d02498e09 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 10 Sep 2021 14:35:12 +0000 Subject: [PATCH 2911/3836] Update master for stable/xena Add file to the reno documentation build to show release notes for stable/xena. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/xena. Sem-Ver: feature Change-Id: I3f6067855847c682baeb1ad57a3ceebb89b19331 --- releasenotes/source/index.rst | 1 + releasenotes/source/xena.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/xena.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 7a087bded..7a8dd3522 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ :maxdepth: 1 unreleased + xena wallaby victoria ussuri diff --git a/releasenotes/source/xena.rst b/releasenotes/source/xena.rst new file mode 100644 index 000000000..1be85be3e --- /dev/null +++ b/releasenotes/source/xena.rst @@ -0,0 +1,6 @@ +========================= +Xena Series Release Notes +========================= + +.. release-notes:: + :branch: stable/xena From 6c42203f18a32632509d756c1ed91a1f6a48ed44 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 10 Sep 2021 14:35:15 +0000 Subject: [PATCH 2912/3836] Add Python3 yoga unit tests This is an automatically generated patch to ensure unit testing is in place for all the of the tested runtimes for yoga. See also the PTI in governance [1]. [1]: https://governance.openstack.org/tc/reference/project-testing-interface.html Change-Id: Id2e70a6b264f528a083e2dbd136c7df348022a7b --- .zuul.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index ddd5183ab..52f288481 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -376,7 +376,7 @@ - project: templates: - check-requirements - - openstack-python3-xena-jobs + - openstack-python3-yoga-jobs - openstacksdk-functional-tips - openstacksdk-tox-tips - os-client-config-tox-tips From 290a6cf56b841c16ce29b6b103f8451d5aaf485d Mon Sep 17 00:00:00 2001 From: Kafilat Adeleke Date: Tue, 27 Jul 2021 10:26:59 +0100 Subject: [PATCH 2913/3836] Add limit resource to shared file system Introduce Limit class to shared file system. Change-Id: Ib1f68e43cf406401038e42879147b1268ba1a2c8 --- .../user/proxies/shared_file_system.rst | 11 +++ .../resources/shared_file_system/index.rst | 1 + .../resources/shared_file_system/v2/limit.rst | 13 ++++ openstack/shared_file_system/v2/_proxy.py | 14 ++++ openstack/shared_file_system/v2/limit.py | 75 ++++++++++++++++++ .../shared_file_system/test_limit.py | 36 +++++++++ .../tests/unit/identity/v3/test_limit.py | 1 - .../unit/shared_file_system/v2/test_limit.py | 76 +++++++++++++++++++ .../unit/shared_file_system/v2/test_proxy.py | 4 + ...limit-to-shared-file-2b443c2a00c75e6e.yaml | 5 ++ 10 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 doc/source/user/resources/shared_file_system/v2/limit.rst create mode 100644 openstack/shared_file_system/v2/limit.py create mode 100644 openstack/tests/functional/shared_file_system/test_limit.py create mode 100644 openstack/tests/unit/shared_file_system/v2/test_limit.py create mode 100644 releasenotes/notes/add-limit-to-shared-file-2b443c2a00c75e6e.yaml diff --git a/doc/source/user/proxies/shared_file_system.rst b/doc/source/user/proxies/shared_file_system.rst index 1b28ad124..df02c4b74 100644 --- a/doc/source/user/proxies/shared_file_system.rst +++ b/doc/source/user/proxies/shared_file_system.rst @@ -55,3 +55,14 @@ File Systems service. .. autoclass:: openstack.shared_file_system.v2._proxy.Proxy :noindex: :members: user_messages, get_user_message, delete_user_message + + +Shared File System Limits +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Get absolute limits of resources supported by the Shared File Systems +service. + +.. autoclass:: openstack.shared_file_system.v2._proxy.Proxy + :noindex: + :members: limits diff --git a/doc/source/user/resources/shared_file_system/index.rst b/doc/source/user/resources/shared_file_system/index.rst index 37f31da0b..459eb403a 100644 --- a/doc/source/user/resources/shared_file_system/index.rst +++ b/doc/source/user/resources/shared_file_system/index.rst @@ -6,5 +6,6 @@ Shared File System service resources v2/availability_zone v2/storage_pool + v2/limit v2/share v2/user_message diff --git a/doc/source/user/resources/shared_file_system/v2/limit.rst b/doc/source/user/resources/shared_file_system/v2/limit.rst new file mode 100644 index 000000000..33342c125 --- /dev/null +++ b/doc/source/user/resources/shared_file_system/v2/limit.rst @@ -0,0 +1,13 @@ +openstack.shared_file_system.v2.limit +===================================== + +.. automodule:: openstack.shared_file_system.v2.limit + +The Limit Class +--------------- + +The ``Limit`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.shared_file_system.v2.limit.Limit + :members: diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index 5b88e7516..2d4c32f60 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -20,6 +20,7 @@ from openstack.shared_file_system.v2 import ( user_message as _user_message ) +from openstack.shared_file_system.v2 import limit as _limit from openstack.shared_file_system.v2 import share as _share @@ -224,3 +225,16 @@ def delete_user_message(self, message_id, ignore_missing=True): return self._delete( _user_message.UserMessage, message_id, ignore_missing=ignore_missing) + + def limits(self, **query): + """Lists all share limits. + + :param kwargs query: Optional query parameters to be sent to limit + the share limits being returned. + + :returns: A generator of manila share limits resources + :rtype: :class:`~openstack.shared_file_system.v2. + limit.Limit` + """ + return self._list( + _limit.Limit, **query) diff --git a/openstack/shared_file_system/v2/limit.py b/openstack/shared_file_system/v2/limit.py new file mode 100644 index 000000000..ef95ad1b2 --- /dev/null +++ b/openstack/shared_file_system/v2/limit.py @@ -0,0 +1,75 @@ +# 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 resource + + +class Limit(resource.Resource): + resources_key = "limits" + base_path = "/limits" + + # capabilities + allow_create = False + allow_fetch = False + allow_commit = False + allow_delete = False + allow_list = True + allow_head = False + + #: Properties + #: The maximum number of replica gigabytes that are allowed + #: in a project. + maxTotalReplicaGigabytes = resource.Body( + "maxTotalReplicaGigabytes", type=int) + #: The total maximum number of shares that are allowed in a project. + maxTotalShares = resource.Body("maxTotalShares", type=int) + #: The total maximum number of share gigabytes that are allowed in a + #: project. + maxTotalShareGigabytes = resource.Body( + "maxTotalShareGigabytes", type=int) + #: The total maximum number of share-networks that are allowed in a + #: project. + maxTotalShareNetworks = resource.Body( + "maxTotalShareNetworks", type=int) + #: The total maximum number of share snapshots that are allowed in a + #: project. + maxTotalShareSnapshots = resource.Body( + "maxTotalShareSnapshots", type=int) + #: The maximum number of share replicas that is allowed. + maxTotalShareReplicas = resource.Body( + "maxTotalShareReplicas", type=int) + #: The total maximum number of snapshot gigabytes that are allowed + #: in a project. + maxTotalSnapshotGigabytes = resource.Body( + "maxTotalSnapshotGigabytes", type=int) + #: The total number of replica gigabytes used in a project by + #: share replicas. + totalReplicaGigabytesUsed = resource.Body( + "totalReplicaGigabytesUsed", type=int) + #: The total number of gigabytes used in a project by shares. + totalShareGigabytesUsed = resource.Body( + "totalShareGigabytesUsed", type=int) + #: The total number of created shares in a project. + totalSharesUsed = resource.Body( + "totalSharesUsed", type=int) + #: The total number of created share-networks in a project. + totalShareNetworksUsed = resource.Body( + "totalShareNetworksUsed", type=int) + #: The total number of created share snapshots in a project. + totalShareSnapshotsUsed = resource.Body( + "totalShareSnapshotsUsed", type=int) + #: The total number of gigabytes used in a project by snapshots. + totalSnapshotGigabytesUsed = resource.Body( + "totalSnapshotGigabytesUsed", type=int) + #: The total number of created share replicas in a project. + totalShareReplicasUsed = resource.Body( + "totalShareReplicasUsed", type=int) diff --git a/openstack/tests/functional/shared_file_system/test_limit.py b/openstack/tests/functional/shared_file_system/test_limit.py new file mode 100644 index 000000000..47cee5257 --- /dev/null +++ b/openstack/tests/functional/shared_file_system/test_limit.py @@ -0,0 +1,36 @@ +# 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.tests.functional.shared_file_system import base + + +class LimitTest(base.BaseSharedFileSystemTest): + + def test_limits(self): + limits = self.user_cloud.shared_file_system.limits() + self.assertGreater(len(list(limits)), 0) + for limit in limits: + for attribute in ("maxTotalReplicaGigabytes", + "maxTotalShares", + "maxTotalShareGigabytes", + "maxTotalShareNetworks", + "maxTotalShareSnapshots", + "maxTotalShareReplicas", + "maxTotalSnapshotGigabytes", + "totalReplicaGigabytesUsed", + "totalShareGigabytesUsed", + "totalSharesUsed", + "totalShareNetworksUsed", + "totalShareSnapshotsUsed", + "totalSnapshotGigabytesUsed", + "totalShareReplicasUsed"): + self.assertTrue(hasattr(limit, attribute)) diff --git a/openstack/tests/unit/identity/v3/test_limit.py b/openstack/tests/unit/identity/v3/test_limit.py index 9eac13ce2..6d2bcf413 100644 --- a/openstack/tests/unit/identity/v3/test_limit.py +++ b/openstack/tests/unit/identity/v3/test_limit.py @@ -29,7 +29,6 @@ class TestLimit(base.TestCase): def test_basic(self): sot = limit.Limit() - self.assertEqual('limit', sot.resource_key) self.assertEqual('limits', sot.resources_key) self.assertEqual('/limits', sot.base_path) self.assertTrue(sot.allow_create) diff --git a/openstack/tests/unit/shared_file_system/v2/test_limit.py b/openstack/tests/unit/shared_file_system/v2/test_limit.py new file mode 100644 index 000000000..e0d2c0b75 --- /dev/null +++ b/openstack/tests/unit/shared_file_system/v2/test_limit.py @@ -0,0 +1,76 @@ +# 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.shared_file_system.v2 import limit +from openstack.tests.unit import base + +EXAMPLE = { + "totalShareNetworksUsed": 0, + "maxTotalShareGigabytes": 1000, + "maxTotalShareNetworks": 10, + "totalSharesUsed": 0, + "totalShareGigabytesUsed": 0, + "totalShareSnapshotsUsed": 0, + "maxTotalShares": 50, + "totalSnapshotGigabytesUsed": 0, + "maxTotalSnapshotGigabytes": 1000, + "maxTotalShareSnapshots": 50, + "maxTotalShareReplicas": 100, + "maxTotalReplicaGigabytes": 1000, + "totalShareReplicasUsed": 0, + "totalReplicaGigabytesUsed": 0 +} + + +class TestLimit(base.TestCase): + + def test_basic(self): + limits = limit.Limit() + self.assertEqual('limits', limits.resources_key) + self.assertEqual('/limits', limits.base_path) + self.assertTrue(limits.allow_list) + self.assertFalse(limits.allow_fetch) + self.assertFalse(limits.allow_create) + self.assertFalse(limits.allow_commit) + self.assertFalse(limits.allow_delete) + self.assertFalse(limits.allow_head) + + def test_make_limits(self): + limits = limit.Limit(**EXAMPLE) + self.assertEqual(EXAMPLE['totalShareNetworksUsed'], + limits.totalShareNetworksUsed) + self.assertEqual(EXAMPLE['maxTotalShareGigabytes'], + limits.maxTotalShareGigabytes) + self.assertEqual(EXAMPLE['maxTotalShareNetworks'], + limits.maxTotalShareNetworks) + self.assertEqual(EXAMPLE['totalSharesUsed'], + limits.totalSharesUsed) + self.assertEqual(EXAMPLE['totalShareGigabytesUsed'], + limits.totalShareGigabytesUsed) + self.assertEqual(EXAMPLE['totalShareSnapshotsUsed'], + limits.totalShareSnapshotsUsed) + self.assertEqual(EXAMPLE['maxTotalShares'], + limits.maxTotalShares) + self.assertEqual(EXAMPLE['totalSnapshotGigabytesUsed'], + limits.totalSnapshotGigabytesUsed) + self.assertEqual(EXAMPLE['maxTotalSnapshotGigabytes'], + limits.maxTotalSnapshotGigabytes) + self.assertEqual(EXAMPLE['maxTotalShareSnapshots'], + limits.maxTotalShareSnapshots) + self.assertEqual(EXAMPLE['maxTotalShareReplicas'], + limits.maxTotalShareReplicas) + self.assertEqual(EXAMPLE['maxTotalReplicaGigabytes'], + limits.maxTotalReplicaGigabytes) + self.assertEqual(EXAMPLE['totalShareReplicasUsed'], + limits.totalShareReplicasUsed) + self.assertEqual(EXAMPLE['totalReplicaGigabytesUsed'], + limits.totalReplicaGigabytesUsed) diff --git a/openstack/tests/unit/shared_file_system/v2/test_proxy.py b/openstack/tests/unit/shared_file_system/v2/test_proxy.py index 251251b44..573a354d2 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_proxy.py +++ b/openstack/tests/unit/shared_file_system/v2/test_proxy.py @@ -13,6 +13,7 @@ from unittest import mock from openstack.shared_file_system.v2 import _proxy +from openstack.shared_file_system.v2 import limit from openstack.shared_file_system.v2 import share from openstack.shared_file_system.v2 import storage_pool from openstack.shared_file_system.v2 import user_message @@ -107,3 +108,6 @@ def test_delete_user_message(self): def test_delete_user_message_true(self): self.verify_delete( self.proxy.delete_user_message, user_message.UserMessage, True) + + def test_limit(self): + self.verify_list(self.proxy.limits, limit.Limit) diff --git a/releasenotes/notes/add-limit-to-shared-file-2b443c2a00c75e6e.yaml b/releasenotes/notes/add-limit-to-shared-file-2b443c2a00c75e6e.yaml new file mode 100644 index 000000000..58ff44f09 --- /dev/null +++ b/releasenotes/notes/add-limit-to-shared-file-2b443c2a00c75e6e.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added support to list absolute resource limits on the shared + file system service. From 50043daf19f0f59244737d3e36ce08c468e1030a Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Fri, 10 Sep 2021 15:07:58 +0200 Subject: [PATCH 2914/3836] Splits the `TestBaremetalProxy` class Nine new classes `TestDrivers`, `TestChassis`, `TestNode`, `TestPort`, `TestPortGroups`, `TestAllocation`, `TestVolumeConnector`, `TestVolumeTarget`, and `TestMisc` are created. They contain all the tests previously embedded in `TestBaremetalProxy`. In `TestBaremetalProxy`, only the `setUp` method remains Change-Id: I055d571ac3b179679c58c1cddc86abc9fe63f5aa --- .../tests/unit/baremetal/v1/test_proxy.py | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/openstack/tests/unit/baremetal/v1/test_proxy.py b/openstack/tests/unit/baremetal/v1/test_proxy.py index f9e8d53a6..19efccc17 100644 --- a/openstack/tests/unit/baremetal/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal/v1/test_proxy.py @@ -30,17 +30,20 @@ class TestBaremetalProxy(test_proxy_base.TestProxyBase): - def setUp(self): super(TestBaremetalProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) + +class TestDrivers(TestBaremetalProxy): def test_drivers(self): self.verify_list(self.proxy.drivers, driver.Driver) def test_get_driver(self): self.verify_get(self.proxy.get_driver, driver.Driver) + +class TestChassis(TestBaremetalProxy): @mock.patch.object(chassis.Chassis, 'list') def test_chassis_detailed(self, mock_list): result = self.proxy.chassis(details=True, query=1) @@ -73,6 +76,8 @@ def test_delete_chassis(self): def test_delete_chassis_ignore(self): self.verify_delete(self.proxy.delete_chassis, chassis.Chassis, True) + +class TestNode(TestBaremetalProxy): @mock.patch.object(node.Node, 'list') def test_nodes_detailed(self, mock_list): result = self.proxy.nodes(details=True, query=1) @@ -117,6 +122,8 @@ def test_delete_node(self): def test_delete_node_ignore(self): self.verify_delete(self.proxy.delete_node, node.Node, True) + +class TestPort(TestBaremetalProxy): @mock.patch.object(port.Port, 'list') def test_ports_detailed(self, mock_list): result = self.proxy.ports(details=True, query=1) @@ -149,6 +156,8 @@ def test_delete_port(self): def test_delete_port_ignore(self): self.verify_delete(self.proxy.delete_port, port.Port, True) + +class TestPortGroups(TestBaremetalProxy): @mock.patch.object(port_group.PortGroup, 'list') def test_port_groups_detailed(self, mock_list): result = self.proxy.port_groups(details=True, query=1) @@ -166,6 +175,8 @@ def test_get_port_group(self): mock_method=_MOCK_METHOD, expected_kwargs={'fields': None}) + +class TestAllocation(TestBaremetalProxy): def test_create_allocation(self): self.verify_create(self.proxy.create_allocation, allocation.Allocation) @@ -182,6 +193,8 @@ def test_delete_allocation_ignore(self): self.verify_delete(self.proxy.delete_allocation, allocation.Allocation, True) + +class TestVolumeConnector(TestBaremetalProxy): def test_create_volume_connector(self): self.verify_create(self.proxy.create_volume_connector, volume_connector.VolumeConnector) @@ -206,6 +219,8 @@ def test_delete_volume_connector_ignore(self): volume_connector.VolumeConnector, True) + +class TestVolumeTarget(TestBaremetalProxy): @mock.patch.object(volume_target.VolumeTarget, 'list') def test_volume_target_detailed(self, mock_list): result = self.proxy.volume_targets(details=True, query=1) @@ -242,6 +257,8 @@ def test_delete_volume_target_ignore(self): volume_target.VolumeTarget, True) + +class TestMisc(TestBaremetalProxy): @mock.patch.object(node.Node, 'fetch', autospec=True) def test__get_with_fields_none(self, mock_fetch): result = self.proxy._get_with_fields(node.Node, 'value') @@ -287,7 +304,7 @@ def test_success(self, mock_get): for i, n in enumerate(nodes): # 1st attempt on 1st node, 2nd attempt on 2nd node n._check_state_reached.return_value = not (i % 2) - mock_get.side_effect = nodes + mock_get.side_effect = nodes result = self.proxy.wait_for_nodes_provision_state( ['abcd', node.Node(id='1234')], 'fake state') @@ -304,7 +321,7 @@ def test_success_no_fail(self, mock_get): for i, n in enumerate(nodes): # 1st attempt on 1st node, 2nd attempt on 2nd node n._check_state_reached.return_value = not (i % 2) - mock_get.side_effect = nodes + mock_get.side_effect = nodes result = self.proxy.wait_for_nodes_provision_state( ['abcd', node.Node(id='1234')], 'fake state', fail=False) From f5f0f9f1aee044ea9fe6e42ceef90971286d0897 Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Mon, 13 Sep 2021 08:02:29 +0200 Subject: [PATCH 2915/3836] Splits class `TestInstanceHaProxy` Three new classes `TestInstanceHaHosts`, `TestInstanceHaNotifications`, and `TestInstanceHaSegments` are created. They contain all the tests previously embedded in `TestInstanceHaProxy`. In `TestInstanceHaProxy`, only the `setUp` method remains Change-Id: Ia2f4ca34bdd1ef489ea3fae6a340df171a07f52e --- openstack/tests/unit/instance_ha/v1/test_proxy.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openstack/tests/unit/instance_ha/v1/test_proxy.py b/openstack/tests/unit/instance_ha/v1/test_proxy.py index 8e1049adc..e2123ea75 100644 --- a/openstack/tests/unit/instance_ha/v1/test_proxy.py +++ b/openstack/tests/unit/instance_ha/v1/test_proxy.py @@ -27,6 +27,8 @@ def setUp(self): super(TestInstanceHaProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) + +class TestInstanceHaHosts(TestInstanceHaProxy): def test_hosts(self): self.verify_list(self.proxy.hosts, host.Host, @@ -61,6 +63,8 @@ def test_host_delete(self): method_kwargs={"segment_id": SEGMENT_ID}, expected_kwargs={"segment_id": SEGMENT_ID}) + +class TestInstanceHaNotifications(TestInstanceHaProxy): def test_notifications(self): self.verify_list(self.proxy.notifications, notification.Notification) @@ -72,6 +76,8 @@ def test_notification_create(self): self.verify_create(self.proxy.create_notification, notification.Notification) + +class TestInstanceHaSegments(TestInstanceHaProxy): def test_segments(self): self.verify_list(self.proxy.segments, segment.Segment) From 45995c88f7765fce6bae4ae58104fdf99908f6a4 Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Mon, 13 Sep 2021 14:52:12 +0200 Subject: [PATCH 2916/3836] Splits class `TestKeyManagerProxy` Three new classes `TestKeyManagerContainer`, `TestKeyManagerOrder`, and `TestKeyManagerSecret` are created. They contain all the tests previously embedded in `TestKeyManagerProxy`. In `TestKeyManagerProxy`, only the `setUp` method remains Change-Id: I188d7ce612148341aa20a4e20bf276ac1a5b967a --- openstack/tests/unit/key_manager/v1/test_proxy.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openstack/tests/unit/key_manager/v1/test_proxy.py b/openstack/tests/unit/key_manager/v1/test_proxy.py index e9ad2b35f..f6d104939 100644 --- a/openstack/tests/unit/key_manager/v1/test_proxy.py +++ b/openstack/tests/unit/key_manager/v1/test_proxy.py @@ -22,6 +22,8 @@ def setUp(self): super(TestKeyManagerProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) + +class TestKeyManagerContainer(TestKeyManagerProxy): def test_server_create_attrs(self): self.verify_create(self.proxy.create_container, container.Container) @@ -45,6 +47,8 @@ def test_containers(self): def test_container_update(self): self.verify_update(self.proxy.update_container, container.Container) + +class TestKeyManagerOrder(TestKeyManagerProxy): def test_order_create_attrs(self): self.verify_create(self.proxy.create_order, order.Order) @@ -66,6 +70,8 @@ def test_orders(self): def test_order_update(self): self.verify_update(self.proxy.update_order, order.Order) + +class TestKeyManagerSecret(TestKeyManagerProxy): def test_secret_create_attrs(self): self.verify_create(self.proxy.create_secret, secret.Secret) From 56068af2a866e6e6f84862b3997490265732027e Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Tue, 14 Sep 2021 09:37:37 +0200 Subject: [PATCH 2917/3836] Splits class `TestMessageProxy` Four new classes `TestMessageQueue`, `TestMessageMessage`, `TestMessageSubscription`, and `TestMessageClaim` are created. They contain all the tests previously embedded in `TestMessageProxy`. In `TestMessageProxy`, only the `setUp` method remains Change-Id: I0f0fafbb9e9ebb196503f251a3a63682252531e4 --- openstack/tests/unit/message/v2/test_proxy.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openstack/tests/unit/message/v2/test_proxy.py b/openstack/tests/unit/message/v2/test_proxy.py index fa3baf991..8538b4dcb 100644 --- a/openstack/tests/unit/message/v2/test_proxy.py +++ b/openstack/tests/unit/message/v2/test_proxy.py @@ -28,6 +28,8 @@ def setUp(self): super(TestMessageProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) + +class TestMessageQueue(TestMessageProxy): def test_queue_create(self): self.verify_create(self.proxy.create_queue, queue.Queue) @@ -46,6 +48,8 @@ def test_queue_delete(self): def test_queue_delete_ignore(self): self.verify_delete(self.proxy.delete_queue, queue.Queue, True) + +class TestMessageMessage(TestMessageProxy): @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_message_post(self, mock_get_resource): message_obj = message.Message(queue_name="test_queue") @@ -126,6 +130,8 @@ def test_message_delete_ignore(self, mock_get_resource): "resource_or_id", queue_name="test_queue") + +class TestMessageSubscription(TestMessageProxy): def test_subscription_create(self): self._verify( "openstack.message.v2.subscription.Subscription.create", @@ -178,6 +184,8 @@ def test_subscription_delete_ignore(self, mock_get_resource): subscription.Subscription, "resource_or_id", queue_name="test_queue") + +class TestMessageClaim(TestMessageProxy): def test_claim_create(self): self._verify( "openstack.message.v2.claim.Claim.create", From 81776e617fbc101b2c33a98b86e4d4c5926e02a5 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 16 Sep 2021 18:26:15 +0200 Subject: [PATCH 2918/3836] Add missing headers in object store there are some missing headers in the Swift support. Those does not look like super important, but better add them to be able to get rid of separate headers management in the cloud layer. Change-Id: Ia885269b57c367878458ef2ebf419394bab71b78 --- openstack/object_store/v1/container.py | 7 +++++++ openstack/object_store/v1/obj.py | 18 +++++++++++++++++- .../unit/object_store/v1/test_container.py | 11 ++++++++++- .../tests/unit/object_store/v1/test_obj.py | 15 +++++++++++++++ 4 files changed, 49 insertions(+), 2 deletions(-) diff --git a/openstack/object_store/v1/container.py b/openstack/object_store/v1/container.py index 670a5db7b..b9c84fd22 100644 --- a/openstack/object_store/v1/container.py +++ b/openstack/object_store/v1/container.py @@ -21,6 +21,7 @@ class Container(_base.BaseResource): "content_type": "content-type", "is_content_type_detected": "x-detect-content-type", "versions_location": "x-versions-location", + "history_location": "x-history-location", "read_ACL": "x-container-read", "write_ACL": "x-container-write", "sync_to": "x-container-sync-to", @@ -86,6 +87,8 @@ class Container(_base.BaseResource): #: the name before you include it in the header. To disable #: versioning, set the header to an empty string. versions_location = resource.Header("x-versions-location") + #: Enables versioning on the container. + history_location = resource.Header("x-history-location") #: The MIME type of the list of names. content_type = resource.Header("content-type") #: If set to true, Object Storage guesses the content type based @@ -93,6 +96,10 @@ class Container(_base.BaseResource): #: Content-Type header, if present. *Type: bool* is_content_type_detected = resource.Header("x-detect-content-type", type=bool) + + #: Storage policy used by the container. + #: It is not possible to change policy of an existing container + storage_policy = resource.Header("x-storage-policy") # TODO(mordred) Shouldn't if-none-match be handled more systemically? #: In combination with Expect: 100-Continue, specify an #: "If-None-Match: \*" header to query whether the server already diff --git a/openstack/object_store/v1/obj.py b/openstack/object_store/v1/obj.py index 8c7a11aa7..0c7836101 100644 --- a/openstack/object_store/v1/obj.py +++ b/openstack/object_store/v1/obj.py @@ -21,12 +21,14 @@ class Object(_base.BaseResource): _custom_metadata_prefix = "X-Object-Meta-" _system_metadata = { + "accept_ranges": "accept-ranges", "content_disposition": "content-disposition", "content_encoding": "content-encoding", "content_type": "content-type", "delete_after": "x-delete-after", "delete_at": "x-delete-at", "is_content_type_detected": "x-detect-content-type", + "manifest": "x-object-manifest" } base_path = "/%(container)s" @@ -40,7 +42,10 @@ class Object(_base.BaseResource): allow_head = True _query_mapping = resource.QueryParameters( - 'prefix', 'format' + 'prefix', 'format', + 'temp_url_sig', 'temp_url_expires', + 'filename', 'multipart_manifest', 'symlink', + multipart_manifest='multipart-manifest' ) # Data to be passed during a POST call to create an object on the server. @@ -93,6 +98,10 @@ class Object(_base.BaseResource): #: signature. For more information about temporary URLs, see #: OpenStack Object Storage API v1 Reference. expires_at = resource.Header("expires") + #: If present, this is a dynamic large object manifest object. + #: The value is the container and object name prefix of the segment + #: objects in the form container/prefix. + manifest = resource.Header("x-object-manifest") #: If you include the multipart-manifest=get query parameter and #: the object is a large object, the object contents are not #: returned. Instead, the manifest is returned in the @@ -171,6 +180,13 @@ class Object(_base.BaseResource): #: Using PUT with X-Copy-From has the same effect as using the #: COPY operation to copy an object. copy_from = resource.Header("x-copy-from") + #: If present, this is a symlink object. The value is the relative path + #: of the target object in the format /. + symlink_target = resource.Header("x-symlink-target") + #: If present, and X-Symlink-Target is present, then this is a + #: cross-account symlink to an object in the account specified in the + #: value. + symlink_target_account = resource.Header("x-symlink-target-account") has_body = False diff --git a/openstack/tests/unit/object_store/v1/test_container.py b/openstack/tests/unit/object_store/v1/test_container.py index 36698e752..3c58bf17d 100644 --- a/openstack/tests/unit/object_store/v1/test_container.py +++ b/openstack/tests/unit/object_store/v1/test_container.py @@ -39,8 +39,10 @@ def setUp(self): 'x-container-sync-key': 'sync-key', 'x-container-bytes-used': '630666', 'x-versions-location': 'versions-location', + 'x-history-location': 'history-location', 'content-type': 'application/json; charset=utf-8', - 'x-timestamp': '1453414055.48672' + 'x-timestamp': '1453414055.48672', + 'x-storage-policy': 'Gold' } self.body_plus_headers = dict(self.body, **self.headers) @@ -98,7 +100,12 @@ def test_create_and_head(self): self.assertEqual( self.body_plus_headers['x-versions-location'], sot.versions_location) + self.assertEqual( + self.body_plus_headers['x-history-location'], + sot.history_location) self.assertEqual(self.body_plus_headers['x-timestamp'], sot.timestamp) + self.assertEqual(self.body_plus_headers['x-storage-policy'], + sot.storage_policy) def test_list(self): containers = [ @@ -191,7 +198,9 @@ def test_to_json(self): 'meta_temp_url_key_2': None, 'timestamp': None, 'versions_location': None, + 'history_location': None, 'write_ACL': None, + 'storage_policy': None }, json.loads(json.dumps(sot))) def _test_no_headers(self, sot, sot_call, sess_method): diff --git a/openstack/tests/unit/object_store/v1/test_obj.py b/openstack/tests/unit/object_store/v1/test_obj.py index 448652796..b4af0c540 100644 --- a/openstack/tests/unit/object_store/v1/test_obj.py +++ b/openstack/tests/unit/object_store/v1/test_obj.py @@ -68,6 +68,21 @@ def test_basic(self): self.assertTrue(sot.allow_list) self.assertTrue(sot.allow_head) + self.assertDictEqual( + { + 'filename': 'filename', + 'format': 'format', + 'limit': 'limit', + 'marker': 'marker', + 'multipart_manifest': 'multipart-manifest', + 'prefix': 'prefix', + 'symlink': 'symlink', + 'temp_url_expires': 'temp_url_expires', + 'temp_url_sig': 'temp_url_sig' + }, + sot._query_mapping._mapping + ) + def test_new(self): sot = obj.Object.new(container=self.container, name=self.object) self.assert_no_calls() From 93f69e42f125fb87c3269881509b3913b9374f34 Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Mon, 20 Sep 2021 08:06:27 +0200 Subject: [PATCH 2919/3836] Splits class `TestNetworkProxy` 36 new classes are created: `TestNetworkTags`, `TestNetworkAddressScope`, `TestNetworkAgent`, `TestNetworkAvailability`, `TestNetworkExtension`, `TestNetworkHealthMonitor`, `TestNetworkSiteConnection`, `TestNetworkIkePolicy`, `TestNetworkListener`, `TestNetworkLoadBalancer`, `TestNetworkMeteringLabel`, `TestNetworkNetwork`, `TestNetworkFlavor`, `TestNetworkServiceProfile`, `TestNetworkIpAvailability`, `TestNetworkPoolMember`, `TestNetworkPool`, `TestNetworkQosBandwidth`, `TestNetworkQosDscpMarking`, `TestNetworkQosMinimumBandwidth`, `TestNetworkQosPolicy`, `TestNetworkQosRuleType`, `TestNetworkQuota`, `TestNetworkRbacPolicy`, `TestNetworkRouter`, `TestNetworkFirewallGroup`, `TestNetworkPolicy`, `TestNetworkRule`, `TestNetworkNetworkSegment`, `TestNetworkSecurityGroup`, `TestNetworkSegment`, `TestNetworkSubnet`, `TestNetworkVpnService`, `TestNetworkServiceProvider`, `TestNetworkAutoAllocatedTopology`, and `TestNetworkFloatingIp`. They contain all the tests previously embedded in `TestNetworkProxy`. In `TestNetworkProxy`, only the `setUp` method remains Change-Id: Ie53751eb5c4ba75d5189f7aa6fb19cd26974ad69 --- openstack/tests/unit/network/v2/test_proxy.py | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 67c866345..6b6d2d98c 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -107,6 +107,8 @@ def verify_delete( expected_kwargs=expected_kwargs, mock_method=mock_method) + +class TestNetworkAddressScope(TestNetworkProxy): def test_address_scope_create_attrs(self): self.verify_create(self.proxy.create_address_scope, address_scope.AddressScope) @@ -137,6 +139,8 @@ def test_address_scope_update(self): self.verify_update(self.proxy.update_address_scope, address_scope.AddressScope) + +class TestNetworkAgent(TestNetworkProxy): def test_agent_delete(self): self.verify_delete(self.proxy.delete_agent, agent.Agent, True) @@ -149,6 +153,8 @@ def test_agents(self): def test_agent_update(self): self.verify_update(self.proxy.update_agent, agent.Agent) + +class TestNetworkAvailability(TestNetworkProxy): def test_availability_zones(self): self.verify_list( self.proxy.availability_zones, @@ -170,6 +176,8 @@ def test_network_hosting_dhcp_agents(self): expected_kwargs={'network_id': NETWORK_ID} ) + +class TestNetworkExtension(TestNetworkProxy): def test_extension_find(self): self.verify_find(self.proxy.find_extension, extension.Extension) @@ -213,6 +221,8 @@ def test_floating_ip_update_if_revision(self): expected_kwargs={'x': 1, 'y': 2, 'z': 3, 'if_revision': 42}) + +class TestNetworkHealthMonitor(TestNetworkProxy): def test_health_monitor_create_attrs(self): self.verify_create(self.proxy.create_health_monitor, health_monitor.HealthMonitor) @@ -241,6 +251,8 @@ def test_health_monitor_update(self): self.verify_update(self.proxy.update_health_monitor, health_monitor.HealthMonitor) + +class TestNetworkSiteConnection(TestNetworkProxy): def test_ipsec_site_connection_create_attrs(self): self.verify_create(self.proxy.create_vpn_ipsec_site_connection, ipsec_site_connection.IPSecSiteConnection) @@ -269,6 +281,8 @@ def test_ipsec_site_connection_update(self): self.verify_update(self.proxy.update_vpn_ipsec_site_connection, ipsec_site_connection.IPSecSiteConnection) + +class TestNetworkIkePolicy(TestNetworkProxy): def test_ikepolicy_create_attrs(self): self.verify_create(self.proxy.create_vpn_ikepolicy, ikepolicy.IkePolicy) @@ -297,6 +311,8 @@ def test_ikepolicy_update(self): self.verify_update(self.proxy.update_vpn_ikepolicy, ikepolicy.IkePolicy) + +class TestNetworkListener(TestNetworkProxy): def test_listener_create_attrs(self): self.verify_create(self.proxy.create_listener, listener.Listener) @@ -320,6 +336,8 @@ def test_listeners(self): def test_listener_update(self): self.verify_update(self.proxy.update_listener, listener.Listener) + +class TestNetworkLoadBalancer(TestNetworkProxy): def test_load_balancer_create_attrs(self): self.verify_create(self.proxy.create_load_balancer, load_balancer.LoadBalancer) @@ -348,6 +366,8 @@ def test_load_balancer_update(self): self.verify_update(self.proxy.update_load_balancer, load_balancer.LoadBalancer) + +class TestNetworkMeteringLabel(TestNetworkProxy): def test_metering_label_create_attrs(self): self.verify_create(self.proxy.create_metering_label, metering_label.MeteringLabel) @@ -404,6 +424,8 @@ def test_metering_label_rule_update(self): self.verify_update(self.proxy.update_metering_label_rule, metering_label_rule.MeteringLabelRule) + +class TestNetworkNetwork(TestNetworkProxy): def test_network_create_attrs(self): self.verify_create(self.proxy.create_network, network.Network) @@ -450,6 +472,8 @@ def test_network_update_if_revision(self): expected_kwargs={'x': 1, 'y': 2, 'z': 3, 'if_revision': 42}) + +class TestNetworkFlavor(TestNetworkProxy): def test_flavor_create_attrs(self): self.verify_create(self.proxy.create_flavor, flavor.Flavor) @@ -468,6 +492,8 @@ def test_flavor_update(self): def test_flavors(self): self.verify_list(self.proxy.flavors, flavor.Flavor) + +class TestNetworkServiceProfile(TestNetworkProxy): def test_service_profile_create_attrs(self): self.verify_create(self.proxy.create_service_profile, service_profile.ServiceProfile) @@ -492,6 +518,8 @@ def test_service_profile_update(self): self.verify_update(self.proxy.update_service_profile, service_profile.ServiceProfile) + +class TestNetworkIpAvailability(TestNetworkProxy): def test_network_ip_availability_find(self): self.verify_find(self.proxy.find_network_ip_availability, network_ip_availability.NetworkIPAvailability) @@ -510,6 +538,8 @@ def test_pool_member_create_attrs(self): method_kwargs={"pool": "test_id"}, expected_kwargs={"pool_id": "test_id"}) + +class TestNetworkPoolMember(TestNetworkProxy): def test_pool_member_delete(self): self.verify_delete( self.proxy.delete_pool_member, @@ -557,6 +587,8 @@ def test_pool_member_update(self): expected_args=[pool_member.PoolMember, "MEMBER"], expected_kwargs={"pool_id": "POOL"}) + +class TestNetworkPool(TestNetworkProxy): def test_pool_create_attrs(self): self.verify_create(self.proxy.create_pool, pool.Pool) @@ -623,6 +655,8 @@ def test_ports_create(self, bc): bc.assert_called_once_with(port.Port, data) + +class TestNetworkQosBandwidth(TestNetworkProxy): def test_qos_bandwidth_limit_rule_create_attrs(self): self.verify_create( self.proxy.create_qos_bandwidth_limit_rule, @@ -686,6 +720,8 @@ def test_qos_bandwidth_limit_rule_update(self): 'rule_id'], expected_kwargs={'qos_policy_id': QOS_POLICY_ID, 'foo': 'bar'}) + +class TestNetworkQosDscpMarking(TestNetworkProxy): def test_qos_dscp_marking_rule_create_attrs(self): self.verify_create( self.proxy.create_qos_dscp_marking_rule, @@ -748,6 +784,8 @@ def test_qos_dscp_marking_rule_update(self): 'rule_id'], expected_kwargs={'qos_policy_id': QOS_POLICY_ID, 'foo': 'bar'}) + +class TestNetworkQosMinimumBandwidth(TestNetworkProxy): def test_qos_minimum_bandwidth_rule_create_attrs(self): self.verify_create( self.proxy.create_qos_minimum_bandwidth_rule, @@ -812,6 +850,8 @@ def test_qos_minimum_bandwidth_rule_update(self): expected_kwargs={ 'qos_policy_id': QOS_POLICY_ID, 'foo': 'bar'}) + +class TestNetworkQosPolicy(TestNetworkProxy): def test_qos_policy_create_attrs(self): self.verify_create(self.proxy.create_qos_policy, qos_policy.QoSPolicy) @@ -835,6 +875,8 @@ def test_qos_policies(self): def test_qos_policy_update(self): self.verify_update(self.proxy.update_qos_policy, qos_policy.QoSPolicy) + +class TestNetworkQosRuleType(TestNetworkProxy): def test_qos_rule_type_find(self): self.verify_find(self.proxy.find_qos_rule_type, qos_rule_type.QoSRuleType) @@ -846,6 +888,8 @@ def test_qos_rule_type_get(self): def test_qos_rule_types(self): self.verify_list(self.proxy.qos_rule_types, qos_rule_type.QoSRuleType) + +class TestNetworkQuota(TestNetworkProxy): def test_quota_delete(self): self.verify_delete(self.proxy.delete_quota, quota.Quota, False) @@ -888,6 +932,8 @@ def test_quotas(self): def test_quota_update(self): self.verify_update(self.proxy.update_quota, quota.Quota) + +class TestNetworkRbacPolicy(TestNetworkProxy): def test_rbac_policy_create_attrs(self): self.verify_create(self.proxy.create_rbac_policy, rbac_policy.RBACPolicy) @@ -913,6 +959,8 @@ def test_rbac_policy_update(self): self.verify_update(self.proxy.update_rbac_policy, rbac_policy.RBACPolicy) + +class TestNetworkRouter(TestNetworkProxy): def test_router_create_attrs(self): self.verify_create(self.proxy.create_router, router.Router) @@ -1092,6 +1140,8 @@ def test_agent_hosted_routers_list(self): expected_kwargs={'agent_id': AGENT_ID}, ) + +class TestNetworkFirewallGroup(TestNetworkProxy): def test_firewall_group_create_attrs(self): self.verify_create(self.proxy.create_firewall_group, firewall_group.FirewallGroup) @@ -1120,6 +1170,8 @@ def test_firewall_group_update(self): self.verify_update(self.proxy.update_firewall_group, firewall_group.FirewallGroup) + +class TestNetworkPolicy(TestNetworkProxy): def test_firewall_policy_create_attrs(self): self.verify_create(self.proxy.create_firewall_policy, firewall_policy.FirewallPolicy) @@ -1148,6 +1200,8 @@ def test_firewall_policy_update(self): self.verify_update(self.proxy.update_firewall_policy, firewall_policy.FirewallPolicy) + +class TestNetworkRule(TestNetworkProxy): def test_firewall_rule_create_attrs(self): self.verify_create(self.proxy.create_firewall_rule, firewall_rule.FirewallRule) @@ -1176,6 +1230,8 @@ def test_firewall_rule_update(self): self.verify_update(self.proxy.update_firewall_rule, firewall_rule.FirewallRule) + +class TestNetworkNetworkSegment(TestNetworkProxy): def test_network_segment_range_create_attrs(self): self.verify_create(self.proxy.create_network_segment_range, network_segment_range.NetworkSegmentRange) @@ -1204,6 +1260,8 @@ def test_network_segment_range_update(self): self.verify_update(self.proxy.update_network_segment_range, network_segment_range.NetworkSegmentRange) + +class TestNetworkSecurityGroup(TestNetworkProxy): def test_security_group_create_attrs(self): self.verify_create(self.proxy.create_security_group, security_group.SecurityGroup) @@ -1290,6 +1348,8 @@ def test_security_group_rules_create(self, bc): bc.assert_called_once_with(security_group_rule.SecurityGroupRule, data) + +class TestNetworkSegment(TestNetworkProxy): def test_segment_create_attrs(self): self.verify_create(self.proxy.create_segment, segment.Segment) @@ -1311,6 +1371,8 @@ def test_segments(self): def test_segment_update(self): self.verify_update(self.proxy.update_segment, segment.Segment) + +class TestNetworkSubnet(TestNetworkProxy): def test_subnet_create_attrs(self): self.verify_create(self.proxy.create_subnet, subnet.Subnet) @@ -1369,6 +1431,8 @@ def test_subnet_pool_update(self): self.verify_update(self.proxy.update_subnet_pool, subnet_pool.SubnetPool) + +class TestNetworkVpnService(TestNetworkProxy): def test_vpn_service_create_attrs(self): self.verify_create(self.proxy.create_vpn_service, vpn_service.VPNService) @@ -1395,10 +1459,14 @@ def test_vpn_service_update(self): self.verify_update(self.proxy.update_vpn_service, vpn_service.VPNService) + +class TestNetworkServiceProvider(TestNetworkProxy): def test_service_provider(self): self.verify_list(self.proxy.service_providers, service_provider.ServiceProvider) + +class TestNetworkAutoAllocatedTopology(TestNetworkProxy): def test_auto_allocated_topology_get(self): self.verify_get(self.proxy.get_auto_allocated_topology, auto_allocated_topology.AutoAllocatedTopology) @@ -1421,6 +1489,8 @@ def test_validate_topology(self): expected_kwargs={"project": mock.sentinel.project_id, "requires_id": False}) + +class TestNetworkTags(TestNetworkProxy): def test_set_tags(self): x_network = network.Network.new(id='NETWORK_ID') self._verify( @@ -1438,6 +1508,8 @@ def test_set_tags_resource_without_tag_suport(self, mock_set_tags): no_tag_resource, ['TAG1', 'TAG2']) self.assertEqual(0, mock_set_tags.call_count) + +class TestNetworkFloatingIp(TestNetworkProxy): def test_create_floating_ip_port_forwarding(self): self.verify_create(self.proxy.create_floating_ip_port_forwarding, port_forwarding.PortForwarding, From 6bdde3d1321ffc0b2a3a7db7f675d3bf6c354202 Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Thu, 23 Sep 2021 09:41:08 +0200 Subject: [PATCH 2920/3836] Splits class `TestOrchestrationProxy` 8 new classes are created: `TestOrchestrationStack`, `TestOrchestrationStackEnvironment`, `TestOrchestrationStackFiles`, `TestOrchestrationStackTemplate`, `TestOrchestrationResource`, `TestOrchestrationSoftwareConfig`, `TestOrchestrationSoftwareDeployment`, and `TestOrchestrationTemplate`. They contain all the tests previously embedded in `TestOrchestrationProxy`. In `TestOrchestrationProxy`, only the `setUp` method remains Change-Id: I3dc4714c2d5d12cf471d977b5237bbfe1dd5645e --- .../tests/unit/orchestration/v1/test_proxy.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index 94255a6bc..76406d5ba 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -32,6 +32,8 @@ def setUp(self): super(TestOrchestrationProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) + +class TestOrchestrationStack(TestOrchestrationProxy): def test_create_stack(self): self.verify_create(self.proxy.create_stack, stack.Stack) @@ -135,6 +137,8 @@ def test_check_stack_with_stack_ID(self, mock_stack): mock_stack.assert_called_once_with(id='FAKE_ID') stk.check.assert_called_once_with(self.proxy) + +class TestOrchestrationStackEnvironment(TestOrchestrationProxy): @mock.patch.object(stack.Stack, 'find') def test_get_stack_environment_with_stack_identity(self, mock_find): stack_id = '1234' @@ -169,6 +173,8 @@ def test_get_stack_environment_with_stack_object(self): 'stack_name': stack_name, 'stack_id': stack_id}) + +class TestOrchestrationStackFiles(TestOrchestrationProxy): @mock.patch.object(stack_files.StackFiles, 'fetch') @mock.patch.object(stack.Stack, 'find') def test_get_stack_files_with_stack_identity(self, mock_find, mock_fetch): @@ -197,6 +203,8 @@ def test_get_stack_files_with_stack_object(self, mock_fetch): self.assertEqual({'file': 'content'}, res) mock_fetch.assert_called_once_with(self.proxy) + +class TestOrchestrationStackTemplate(TestOrchestrationProxy): @mock.patch.object(stack.Stack, 'find') def test_get_stack_template_with_stack_identity(self, mock_find): stack_id = '1234' @@ -231,6 +239,8 @@ def test_get_stack_template_with_stack_object(self): 'stack_name': stack_name, 'stack_id': stack_id}) + +class TestOrchestrationResource(TestOrchestrationProxy): @mock.patch.object(stack.Stack, 'find') def test_resources_with_stack_object(self, mock_find): stack_id = '1234' @@ -272,6 +282,8 @@ def test_resources_stack_not_found(self, mock_list, mock_find): self.proxy.resources, stack_name) self.assertEqual('No stack found for test_stack', str(ex)) + +class TestOrchestrationSoftwareConfig(TestOrchestrationProxy): def test_create_software_config(self): self.verify_create(self.proxy.create_software_config, sc.SoftwareConfig) @@ -288,6 +300,8 @@ def test_delete_software_config(self): self.verify_delete(self.proxy.delete_software_config, sc.SoftwareConfig, False) + +class TestOrchestrationSoftwareDeployment(TestOrchestrationProxy): def test_create_software_deployment(self): self.verify_create(self.proxy.create_software_deployment, sd.SoftwareDeployment) @@ -310,6 +324,8 @@ def test_delete_software_deployment(self): self.verify_delete(self.proxy.delete_software_deployment, sd.SoftwareDeployment, False) + +class TestOrchestrationTemplate(TestOrchestrationProxy): @mock.patch.object(template.Template, 'validate') def test_validate_template(self, mock_validate): tmpl = mock.Mock() From d3b614ad4054c593d2bf16fa9e18f001fc116f7d Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Fri, 24 Sep 2021 17:14:12 +0200 Subject: [PATCH 2921/3836] Splits class `TestPlacementProxy` 2 new classes are created: `TestPlacementResourceClass`, and `TestPlacementResourceProvider`. They contain all the tests previously embedded in `TestPlacementProxy`. In `TestPlacementProxy`, only the `setUp` method remains Change-Id: Ifda2c5bd2ec291973567a862d36f27aa3bd833a0 --- openstack/tests/unit/placement/v1/test_proxy.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openstack/tests/unit/placement/v1/test_proxy.py b/openstack/tests/unit/placement/v1/test_proxy.py index 06a2da1b6..014b7a5f9 100644 --- a/openstack/tests/unit/placement/v1/test_proxy.py +++ b/openstack/tests/unit/placement/v1/test_proxy.py @@ -22,8 +22,9 @@ def setUp(self): super().setUp() self.proxy = _proxy.Proxy(self.session) - # resource classes +# resource classes +class TestPlacementResourceClass: def test_resource_class_create(self): self.verify_create( self.proxy.create_resource_class, @@ -56,8 +57,9 @@ def test_resource_classes(self): resource_class.ResourceClass, ) - # resource providers +# resource providers +class TestPlacementResourceProvider: def test_resource_provider_create(self): self.verify_create( self.proxy.create_resource_provider, From fde9162227c38ada01c4716d8e367de722137d68 Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Tue, 28 Sep 2021 15:31:52 +0200 Subject: [PATCH 2922/3836] Splits class `TestSharedFileSystemProxy` 2 new classes are created: `TestSharedFileSystemShare`, and `TestSharedFileSystemStoragePool`. They contain all the tests previously embedded in `TestSharedFileSystemProxy`. In `TestSharedFileSystemProxy`, only the `setUp` method remains Change-Id: Icee4d7e79dc4e413ac815e0830d7f1fe773faf89 --- openstack/tests/unit/shared_file_system/v2/test_proxy.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openstack/tests/unit/shared_file_system/v2/test_proxy.py b/openstack/tests/unit/shared_file_system/v2/test_proxy.py index 3d04ad8c2..eeaa46934 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_proxy.py +++ b/openstack/tests/unit/shared_file_system/v2/test_proxy.py @@ -24,6 +24,8 @@ def setUp(self): super(TestSharedFileSystemProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) + +class TestSharedFileSystemShare(TestSharedFileSystemProxy): def test_shares(self): self.verify_list(self.proxy.shares, share.Share) @@ -64,6 +66,8 @@ def test_wait_for(self, mock_wait): mock_wait.assert_called_once_with(self.proxy, mock_resource, 'ACTIVE', [], 2, 120) + +class TestSharedFileSystemStoragePool(TestSharedFileSystemProxy): def test_storage_pools(self): self.verify_list( self.proxy.storage_pools, storage_pool.StoragePool) From 16e557f411679ea587366de81774634f2183a4ff Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 2 Sep 2021 14:07:24 +0200 Subject: [PATCH 2923/3836] Switch Swift cloud layer to proxy now it's turn of the object storage service to be relying on the proxy functions in the cloud layer. This includes not only completely relying on proxy implementation of the functionality, but also extending proxy `get_object` method to be able to save data to the file or to store it as `data` attribute of the object instance. Change-Id: Idc34738e42de7229a7c620b272c7385dde39eb1d --- openstack/cloud/_object_store.py | 414 ++---------------- openstack/cloud/openstackcloud.py | 6 +- openstack/object_store/v1/_base.py | 6 + openstack/object_store/v1/_proxy.py | 169 +++++-- .../tests/functional/cloud/test_object.py | 8 +- openstack/tests/unit/cloud/test_image.py | 14 +- openstack/tests/unit/cloud/test_object.py | 3 +- .../tests/unit/object_store/v1/test_proxy.py | 42 +- 8 files changed, 245 insertions(+), 417 deletions(-) mode change 100755 => 100644 openstack/cloud/openstackcloud.py diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index c8161df15..df12b821e 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -13,10 +13,7 @@ # import types so that we can reference ListType in sphinx param declarations. # We can't just use list, because sphinx gets confused by # openstack.resource.Resource.list and openstack.resource2.Resource.list -import collections import concurrent.futures -import json -import os import types # noqa import urllib.parse @@ -26,8 +23,6 @@ from openstack.cloud import _utils from openstack.cloud import exc from openstack import exceptions -from openstack import proxy -from openstack import utils DEFAULT_OBJECT_SEGMENT_SIZE = 1073741824 # 1GB @@ -149,12 +144,8 @@ def update_container(self, name, headers): :param dict headers: Key/Value headers to set on the container. """ - # TODO(gtema): Decide on whether to deprecate this or change i/f to the - # container metadata names - exceptions.raise_from_response( - self.object_store.post( - self._get_object_endpoint(name), headers=headers) - ) + self.object_store.set_container_metadata( + name, refresh=False, **headers) def set_container_access(self, name, access, refresh=False): """Set the access control list on a container. @@ -202,40 +193,11 @@ def get_object_capabilities(self): The object-storage service publishes a set of capabilities that include metadata about maximum values and thresholds. """ - # The endpoint in the catalog has version and project-id in it - # To get capabilities, we have to disassemble and reassemble the URL - # This logic is taken from swiftclient - endpoint = urllib.parse.urlparse(self.object_store.get_endpoint()) - url = "{scheme}://{netloc}/info".format( - scheme=endpoint.scheme, netloc=endpoint.netloc) - - return proxy._json_response(self.object_store.get(url)) + return self.object_store.get_info() def get_object_segment_size(self, segment_size): """Get a segment size that will work given capabilities""" - if segment_size is None: - segment_size = DEFAULT_OBJECT_SEGMENT_SIZE - min_segment_size = 0 - try: - caps = self.get_object_capabilities() - except exc.OpenStackCloudHTTPError as e: - if e.response.status_code in (404, 412): - server_max_file_size = DEFAULT_MAX_FILE_SIZE - self.log.info( - "Swift capabilities not supported. " - "Using default max file size.") - else: - raise - else: - server_max_file_size = caps.get('swift', {}).get('max_file_size', - 0) - min_segment_size = caps.get('slo', {}).get('min_segment_size', 0) - - if segment_size > server_max_file_size: - return server_max_file_size - if segment_size < min_segment_size: - return min_segment_size - return segment_size + return self.object_store.get_object_segment_size(segment_size) def is_object_stale( self, container, name, filename, file_md5=None, file_sha256=None): @@ -251,35 +213,10 @@ def is_object_stale( Pre-calculated sha256 of the file contents. Defaults to None which means calculate locally. """ - metadata = self.get_object_metadata(container, name) - if not metadata: - self.log.debug( - "swift stale check, no object: {container}/{name}".format( - container=container, name=name)) - return True - - if not (file_md5 or file_sha256): - (file_md5, file_sha256) = utils._get_file_hashes(filename) - md5_key = metadata.get( - self._OBJECT_MD5_KEY, metadata.get(self._SHADE_OBJECT_MD5_KEY, '')) - sha256_key = metadata.get( - self._OBJECT_SHA256_KEY, metadata.get( - self._SHADE_OBJECT_SHA256_KEY, '')) - up_to_date = utils._hashes_up_to_date( - md5=file_md5, sha256=file_sha256, - md5_key=md5_key, sha256_key=sha256_key) - - if not up_to_date: - self.log.debug( - "swift checksum mismatch: " - " %(filename)s!=%(container)s/%(name)s", - {'filename': filename, 'container': container, 'name': name}) - return True - - self.log.debug( - "swift object up to date: %(container)s/%(name)s", - {'container': container, 'name': name}) - return False + return self.object_store.is_object_stale( + container, name, filename, + file_md5=file_md5, file_sha256=file_sha256 + ) def create_directory_marker_object(self, container, name, **headers): """Create a zero-byte directory marker object @@ -349,217 +286,14 @@ def create_object( :raises: ``OpenStackCloudException`` on operation error. """ - if data is not None and filename: - raise ValueError( - "Both filename and data given. Please choose one.") - if data is not None and not name: - raise ValueError( - "name is a required parameter when data is given") - if data is not None and generate_checksums: - raise ValueError( - "checksums cannot be generated with data parameter") - if generate_checksums is None: - if data is not None: - generate_checksums = False - else: - generate_checksums = True - - if not metadata: - metadata = {} - - if not filename and data is None: - filename = name - - if generate_checksums and (md5 is None or sha256 is None): - (md5, sha256) = utils._get_file_hashes(filename) - if md5: - headers[self._OBJECT_MD5_KEY] = md5 or '' - if sha256: - headers[self._OBJECT_SHA256_KEY] = sha256 or '' - for (k, v) in metadata.items(): - if not k.lower().startswith('x-object-meta-'): - headers['x-object-meta-' + k] = v - else: - headers[k] = v - - endpoint = self._get_object_endpoint(container, name) - - if data is not None: - self.log.debug( - "swift uploading data to %(endpoint)s", - {'endpoint': endpoint}) - - return self._upload_object_data(endpoint, data, headers) - - # segment_size gets used as a step value in a range call, so needs - # to be an int - if segment_size: - segment_size = int(segment_size) - segment_size = self.get_object_segment_size(segment_size) - file_size = os.path.getsize(filename) - - if self.is_object_stale(container, name, filename, md5, sha256): - - self.log.debug( - "swift uploading %(filename)s to %(endpoint)s", - {'filename': filename, 'endpoint': endpoint}) - - if file_size <= segment_size: - self._upload_object(endpoint, filename, headers) - else: - self._upload_large_object( - endpoint, filename, headers, - file_size, segment_size, use_slo) - - def _upload_object_data(self, endpoint, data, headers): - return proxy._json_response(self.object_store.put( - endpoint, headers=headers, data=data)) - - def _upload_object(self, endpoint, filename, headers): - return proxy._json_response(self.object_store.put( - endpoint, headers=headers, data=open(filename, 'rb'))) - - def _get_file_segments(self, endpoint, filename, file_size, segment_size): - # Use an ordered dict here so that testing can replicate things - segments = collections.OrderedDict() - for (index, offset) in enumerate(range(0, file_size, segment_size)): - remaining = file_size - (index * segment_size) - segment = _utils.FileSegment( - filename, offset, - segment_size if segment_size < remaining else remaining) - name = '{endpoint}/{index:0>6}'.format( - endpoint=endpoint, index=index) - segments[name] = segment - return segments - - def _object_name_from_url(self, url): - '''Get container_name/object_name from the full URL called. - - Remove the Swift endpoint from the front of the URL, and remove - the leaving / that will leave behind.''' - endpoint = self.object_store.get_endpoint() - object_name = url.replace(endpoint, '') - if object_name.startswith('/'): - object_name = object_name[1:] - return object_name - - def _add_etag_to_manifest(self, segment_results, manifest): - for result in segment_results: - if 'Etag' not in result.headers: - continue - name = self._object_name_from_url(result.url) - for entry in manifest: - if entry['path'] == '/{name}'.format(name=name): - entry['etag'] = result.headers['Etag'] - - def _upload_large_object( - self, endpoint, filename, - headers, file_size, segment_size, use_slo): - # If the object is big, we need to break it up into segments that - # are no larger than segment_size, upload each of them individually - # and then upload a manifest object. The segments can be uploaded in - # parallel, so we'll use the async feature of the TaskManager. - - segment_futures = [] - segment_results = [] - retry_results = [] - retry_futures = [] - manifest = [] - - # Get an OrderedDict with keys being the swift location for the - # segment, the value a FileSegment file-like object that is a - # slice of the data for the segment. - segments = self._get_file_segments( - endpoint, filename, file_size, segment_size) - - # Schedule the segments for upload - for name, segment in segments.items(): - # Async call to put - schedules execution and returns a future - segment_future = self._pool_executor.submit( - self.object_store.put, - name, headers=headers, data=segment, - raise_exc=False) - segment_futures.append(segment_future) - # TODO(mordred) Collect etags from results to add to this manifest - # dict. Then sort the list of dicts by path. - manifest.append(dict( - path='/{name}'.format(name=name), - size_bytes=segment.length)) - - # Try once and collect failed results to retry - segment_results, retry_results = self._wait_for_futures( - segment_futures, raise_on_error=False) - - self._add_etag_to_manifest(segment_results, manifest) - - for result in retry_results: - # Grab the FileSegment for the failed upload so we can retry - name = self._object_name_from_url(result.url) - segment = segments[name] - segment.seek(0) - # Async call to put - schedules execution and returns a future - segment_future = self._pool_executor.submit( - self.object_store.put, - name, headers=headers, data=segment) - # TODO(mordred) Collect etags from results to add to this manifest - # dict. Then sort the list of dicts by path. - retry_futures.append(segment_future) - - # If any segments fail the second time, just throw the error - segment_results, retry_results = self._wait_for_futures( - retry_futures, raise_on_error=True) - - self._add_etag_to_manifest(segment_results, manifest) - - # If the final manifest upload fails, remove the segments we've - # already uploaded. - try: - if use_slo: - return self._finish_large_object_slo(endpoint, headers, - manifest) - else: - return self._finish_large_object_dlo(endpoint, headers) - except Exception: - try: - segment_prefix = endpoint.split('/')[-1] - self.log.debug( - "Failed to upload large object manifest for %s. " - "Removing segment uploads.", segment_prefix) - self.delete_autocreated_image_objects( - segment_prefix=segment_prefix) - except Exception: - self.log.exception( - "Failed to cleanup image objects for %s:", - segment_prefix) - raise - - def _finish_large_object_slo(self, endpoint, headers, manifest): - # TODO(mordred) send an etag of the manifest, which is the md5sum - # of the concatenation of the etags of the results - headers = headers.copy() - retries = 3 - while True: - try: - return self._object_store_client.put( - endpoint, - params={'multipart-manifest': 'put'}, - headers=headers, data=json.dumps(manifest)) - except Exception: - retries -= 1 - if retries == 0: - raise - - def _finish_large_object_dlo(self, endpoint, headers): - headers = headers.copy() - headers['X-Object-Manifest'] = endpoint - retries = 3 - while True: - try: - return self._object_store_client.put(endpoint, headers=headers) - except Exception: - retries -= 1 - if retries == 0: - raise + return self.object_store.create_object( + container, name, + filename=filename, data=data, + md5=md5, sha256=sha256, use_slo=use_slo, + generate_checksums=generate_checksums, + metadata=metadata, + **headers + ) def update_object(self, container, name, metadata=None, **headers): """Update the metadata of an object @@ -573,19 +307,10 @@ def update_object(self, container, name, metadata=None, **headers): :raises: ``OpenStackCloudException`` on operation error. """ - if not metadata: - metadata = {} - - metadata_headers = {} - - for (k, v) in metadata.items(): - metadata_headers['x-object-meta-' + k] = v - - headers = dict(headers, **metadata_headers) - - return self._object_store_client.post( - self._get_object_endpoint(container, name), - headers=headers) + meta = metadata.copy() or {} + meta.update(**headers) + self.object_store.set_object_metadata( + name, container, **meta) def list_objects(self, container, full_listing=True, prefix=None): """List objects. @@ -634,27 +359,11 @@ def delete_object(self, container, name, meta=None): :raises: OpenStackCloudException on operation error. """ - # TODO(mordred) DELETE for swift returns status in text/plain format - # like so: - # Number Deleted: 15 - # Number Not Found: 0 - # Response Body: - # Response Status: 200 OK - # Errors: - # We should ultimately do something with that try: - if not meta: - meta = self.get_object_metadata(container, name) - if not meta: - return False - params = {} - if meta.get('X-Static-Large-Object', None) == 'True': - params['multipart-manifest'] = 'delete' - self._object_store_client.delete( - self._get_object_endpoint(container, name), - params=params) + self.object_store.delete_object( + name, ignore_missing=False, container=container) return True - except exc.OpenStackCloudHTTPError: + except exceptions.SDKException: return False def delete_autocreated_image_objects(self, container=None, @@ -672,28 +381,14 @@ def delete_autocreated_image_objects(self, container=None, delete. If not given, all image upload segments present are deleted. """ - if container is None: - container = self._OBJECT_AUTOCREATE_CONTAINER - # This method only makes sense on clouds that use tasks - if not self.image_api_use_tasks: - return False - - deleted = False - for obj in self.list_objects(container, prefix=segment_prefix): - meta = self.get_object_metadata(container, obj['name']) - if meta.get( - self._OBJECT_AUTOCREATE_KEY, meta.get( - self._SHADE_OBJECT_AUTOCREATE_KEY)) == 'true': - if self.delete_object(container, obj['name'], meta): - deleted = True - return deleted + return self.object_store._delete_autocreated_image_objects( + container, segment_prefix=segment_prefix + ) def get_object_metadata(self, container, name): - try: - return self._object_store_client.head( - self._get_object_endpoint(container, name)).headers - except exceptions.NotFoundException: - return None + return self.object_store.get_object_metadata( + name, container + ).metadata def get_object_raw(self, container, obj, query_string=None, stream=False): """Get a raw response object for an object. @@ -739,14 +434,11 @@ def stream_object( :raises: OpenStackCloudException on operation error. """ try: - with self.get_object_raw( - container, obj, query_string=query_string) as response: - for ret in response.iter_content(chunk_size=resp_chunk_size): - yield ret - except exc.OpenStackCloudHTTPError as e: - if e.response.status_code == 404: - return - raise + for ret in self.object_store.stream_object( + obj, container, chunk_size=resp_chunk_size): + yield ret + except exceptions.ResourceNotFound: + return def get_object(self, container, obj, query_string=None, resp_chunk_size=1024, outfile=None, stream=False): @@ -770,33 +462,19 @@ def get_object(self, container, obj, query_string=None, is not found (404). :raises: OpenStackCloudException on operation error. """ - # TODO(mordred) implement resp_chunk_size - endpoint = self._get_object_endpoint(container, obj, query_string) try: - get_stream = (outfile is not None) - with self._object_store_client.get( - endpoint, stream=get_stream) as response: - response_headers = { - k.lower(): v for k, v in response.headers.items()} - if outfile: - if isinstance(outfile, str): - outfile_handle = open(outfile, 'wb') - else: - outfile_handle = outfile - for chunk in response.iter_content( - resp_chunk_size, decode_unicode=False): - outfile_handle.write(chunk) - if isinstance(outfile, str): - outfile_handle.close() - else: - outfile_handle.flush() - return (response_headers, None) - else: - return (response_headers, response.text) - except exc.OpenStackCloudHTTPError as e: - if e.response.status_code == 404: - return None - raise + obj = self.object_store.get_object( + obj, container=container, + resp_chunk_size=resp_chunk_size, + outfile=outfile, + remember_content=(outfile is None) + ) + headers = { + k.lower(): v for k, v in obj._last_headers.items()} + return (headers, obj.data) + + except exceptions.ResourceNotFound: + return None def _wait_for_futures(self, futures, raise_on_error=True): '''Collect results or failures from a list of running future tasks.''' diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py old mode 100755 new mode 100644 index 0ec581ce6..395d09283 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -60,9 +60,9 @@ class _OpenStackCloudMixin: :param bool strict: Only return documented attributes for each resource as per the Data Model contract. (Default False) """ - _OBJECT_MD5_KEY = 'x-object-meta-x-sdk-md5' - _OBJECT_SHA256_KEY = 'x-object-meta-x-sdk-sha256' - _OBJECT_AUTOCREATE_KEY = 'x-object-meta-x-sdk-autocreated' + _OBJECT_MD5_KEY = 'x-sdk-md5' + _OBJECT_SHA256_KEY = 'x-sdk-sha256' + _OBJECT_AUTOCREATE_KEY = 'x-sdk-autocreated' _OBJECT_AUTOCREATE_CONTAINER = 'images' # NOTE(shade) shade keys were x-object-meta-x-shade-md5 - we need to check diff --git a/openstack/object_store/v1/_base.py b/openstack/object_store/v1/_base.py index cb8bbb464..96b620ede 100644 --- a/openstack/object_store/v1/_base.py +++ b/openstack/object_store/v1/_base.py @@ -25,6 +25,7 @@ class BaseResource(resource.Resource): _custom_metadata_prefix = None _system_metadata = dict() + _last_headers = dict() def __init__(self, metadata=None, **attrs): """Process and save metadata known at creation stage @@ -88,6 +89,11 @@ def _set_metadata(self, headers): self.metadata[key] = headers[header] def _translate_response(self, response, has_body=None, error_message=None): + # Save headers of the last operation for potential use (get_object of + # cloud layer). + # This must happen before invoking parent _translate_response, cause it + # pops known headers. + self._last_headers = response.headers.copy() super(BaseResource, self)._translate_response( response, has_body=has_body, error_message=error_message) self._set_metadata(response.headers) diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index 9e9130330..e89bdc262 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -236,24 +236,70 @@ def _get_container_name(self, obj=None, container=None): raise ValueError("container must be specified") - def get_object(self, obj, container=None): + def get_object( + self, obj, container=None, resp_chunk_size=1024, + outfile=None, remember_content=False + ): """Get the data associated with an object :param obj: The value can be the name of an object or a - :class:`~openstack.object_store.v1.obj.Object` instance. + :class:`~openstack.object_store.v1.obj.Object` instance. :param container: The value can be the name of a container or a - :class:`~openstack.object_store.v1.container.Container` - instance. - - :returns: The contents of the object. Use the - :func:`~get_object_metadata` - method if you want an object resource. + :class:`~openstack.object_store.v1.container.Container` + instance. + :param int resp_chunk_size: + chunk size of data to read. Only used if the results are + being written to a file or stream is True. + (optional, defaults to 1k) + :param outfile: + Write the object to a file instead of returning the contents. + If this option is given, body in the return tuple will be None. + outfile can either be a file path given as a string, or a + File like object. + :param bool remember_content: Flag whether object data should be saved + as `data` property of the Object. When left as `false` and + `outfile` is not defined data will not be saved and need to be + fetched separately. + + :returns: Instance of the + :class:`~openstack.object_store.v1.obj.Object` objects. :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ container_name = self._get_container_name( obj=obj, container=container) - return self._get(_obj.Object, obj, container=container_name) + + _object = self._get_resource( + _obj.Object, obj, + container=container_name) + request = _object._prepare_request() + + get_stream = (outfile is not None) + + response = self.get( + request.url, + headers=request.headers, + stream=get_stream + ) + exceptions.raise_from_response(response) + _object._translate_response(response, has_body=False) + + if outfile: + if isinstance(outfile, str): + outfile_handle = open(outfile, 'wb') + else: + outfile_handle = outfile + for chunk in response.iter_content( + resp_chunk_size, decode_unicode=False): + outfile_handle.write(chunk) + if isinstance(outfile, str): + outfile_handle.close() + else: + outfile_handle.flush() + elif remember_content: + _object.data = response.text + + return _object def download_object(self, obj, container=None, **attrs): """Download the data contained inside an object. @@ -288,7 +334,6 @@ def stream_object(self, obj, container=None, chunk_size=1024, **attrs): """ container_name = self._get_container_name( obj=obj, container=container) - container_name = self._get_container_name(container=container) obj = self._get_resource( _obj.Object, obj, container=container_name, **attrs) return obj.stream(self, chunk_size=chunk_size) @@ -356,9 +401,9 @@ def create_object( if generate_checksums and (md5 is None or sha256 is None): (md5, sha256) = utils._get_file_hashes(filename) if md5: - headers[self._connection._OBJECT_MD5_KEY] = md5 or '' + metadata[self._connection._OBJECT_MD5_KEY] = md5 if sha256: - headers[self._connection._OBJECT_SHA256_KEY] = sha256 or '' + metadata[self._connection._OBJECT_SHA256_KEY] = sha256 container_name = self._get_container_name(container=container) endpoint = '{container}/{name}'.format(container=container_name, @@ -368,7 +413,6 @@ def create_object( self.log.debug( "swift uploading data to %(endpoint)s", {'endpoint': endpoint}) - # TODO(gtema): custom headers need to be somehow injected return self._create( _obj.Object, container=container_name, name=name, data=data, metadata=metadata, @@ -387,10 +431,14 @@ def create_object( "swift uploading %(filename)s to %(endpoint)s", {'filename': filename, 'endpoint': endpoint}) + if metadata is not None: + # Rely on the class headers calculation for requested metadata + meta_headers = _obj.Object()._calculate_headers(metadata) + headers.update(meta_headers) + if file_size <= segment_size: - # TODO(gtema): replace with regular resource put, but - # custom headers need to be somehow injected self._upload_object(endpoint, filename, headers) + else: self._upload_large_object( endpoint, filename, headers, @@ -501,8 +549,9 @@ def is_object_stale( Pre-calculated sha256 of the file contents. Defaults to None which means calculate locally. """ - metadata = self._connection.get_object_metadata(container, name) - if not metadata: + try: + metadata = self.get_object_metadata(name, container).metadata + except exceptions.NotFoundException: self._connection.log.debug( "swift stale check, no object: {container}/{name}".format( container=container, name=name)) @@ -592,29 +641,61 @@ def _upload_large_object( self._add_etag_to_manifest(segment_results, manifest) - if use_slo: - return self._finish_large_object_slo(endpoint, headers, manifest) - else: - return self._finish_large_object_dlo(endpoint, headers) + try: + if use_slo: + return self._finish_large_object_slo( + endpoint, headers, manifest) + else: + return self._finish_large_object_dlo( + endpoint, headers) + except Exception: + try: + segment_prefix = endpoint.split('/')[-1] + self.log.debug( + "Failed to upload large object manifest for %s. " + "Removing segment uploads.", segment_prefix) + self._delete_autocreated_image_objects( + segment_prefix=segment_prefix) + except Exception: + self.log.exception( + "Failed to cleanup image objects for %s:", + segment_prefix) + raise def _finish_large_object_slo(self, endpoint, headers, manifest): # TODO(mordred) send an etag of the manifest, which is the md5sum # of the concatenation of the etags of the results headers = headers.copy() - return self.put( - endpoint, - params={'multipart-manifest': 'put'}, - headers=headers, data=json.dumps(manifest)) + retries = 3 + while True: + try: + return exceptions.raise_from_response(self.put( + endpoint, + params={'multipart-manifest': 'put'}, + headers=headers, data=json.dumps(manifest)) + ) + except Exception: + retries -= 1 + if retries == 0: + raise def _finish_large_object_dlo(self, endpoint, headers): headers = headers.copy() headers['X-Object-Manifest'] = endpoint - return self.put(endpoint, headers=headers) + retries = 3 + while True: + try: + return exceptions.raise_from_response( + self.put(endpoint, headers=headers)) + except Exception: + retries -= 1 + if retries == 0: + raise def _upload_object(self, endpoint, filename, headers): with open(filename, 'rb') as dt: - return proxy._json_response(self.put( - endpoint, headers=headers, data=dt)) + return self.put( + endpoint, headers=headers, data=dt) def _get_file_segments(self, endpoint, filename, file_size, segment_size): # Use an ordered dict here so that testing can replicate things @@ -926,3 +1007,33 @@ def generate_temp_url( return temp_url.encode('utf-8') else: return temp_url + + def _delete_autocreated_image_objects( + self, container=None, segment_prefix=None + ): + """Delete all objects autocreated for image uploads. + + This method should generally not be needed, as shade should clean up + the objects it uses for object-based image creation. If something + goes wrong and it is found that there are leaked objects, this method + can be used to delete any objects that shade has created on the user's + behalf in service of image uploads. + + :param str container: Name of the container. Defaults to 'images'. + :param str segment_prefix: Prefix for the image segment names to + delete. If not given, all image upload segments present are + deleted. + """ + if container is None: + container = self._connection._OBJECT_AUTOCREATE_CONTAINER + # This method only makes sense on clouds that use tasks + if not self._connection.image_api_use_tasks: + return False + + deleted = False + for obj in self.objects(container, prefix=segment_prefix): + meta = self.get_object_metadata(obj).metadata + if meta.get(self._connection._OBJECT_AUTOCREATE_KEY) == 'true': + self.delete_object(obj, ignore_missing=True) + deleted = True + return deleted diff --git a/openstack/tests/functional/cloud/test_object.py b/openstack/tests/functional/cloud/test_object.py index fab28d36f..3ee3e3753 100644 --- a/openstack/tests/functional/cloud/test_object.py +++ b/openstack/tests/functional/cloud/test_object.py @@ -71,13 +71,13 @@ def test_create_object(self): )) self.assertEqual( 'bar', self.user_cloud.get_object_metadata( - container_name, name)['x-object-meta-foo'] + container_name, name)['foo'] ) self.user_cloud.update_object(container=container_name, name=name, metadata={'testk': 'testv'}) self.assertEqual( 'testv', self.user_cloud.get_object_metadata( - container_name, name)['x-object-meta-testk'] + container_name, name)['testk'] ) try: self.assertIsNotNone( @@ -139,13 +139,13 @@ def test_download_object_to_file(self): )) self.assertEqual( 'bar', self.user_cloud.get_object_metadata( - container_name, name)['x-object-meta-foo'] + container_name, name)['foo'] ) self.user_cloud.update_object(container=container_name, name=name, metadata={'testk': 'testv'}) self.assertEqual( 'testv', self.user_cloud.get_object_metadata( - container_name, name)['x-object-meta-testk'] + container_name, name)['testk'] ) try: with tempfile.NamedTemporaryFile() as fake_file: diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 36987ca02..9947c1312 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -615,10 +615,10 @@ def test_create_image_task(self): object=self.image_name), status_code=201, validate=dict( - headers={'x-object-meta-x-sdk-md5': + headers={'X-Object-Meta-x-sdk-md5': self.fake_image_dict[ 'owner_specified.openstack.md5'], - 'x-object-meta-x-sdk-sha256': + 'X-Object-Meta-x-sdk-sha256': self.fake_image_dict[ 'owner_specified.openstack.sha256']}) ), @@ -711,12 +711,12 @@ def test_create_image_task(self): self.assert_calls() def test_delete_autocreated_no_tasks(self): - self.use_nothing() + self.use_keystone_v3() self.cloud.image_api_use_tasks = False deleted = self.cloud.delete_autocreated_image_objects( container=self.container_name) self.assertFalse(deleted) - self.assert_calls() + self.assert_calls([]) def test_delete_image_task(self): self.cloud.image_api_use_tasks = True @@ -823,8 +823,10 @@ def test_delete_autocreated_image_objects(self): 'Date': 'Thu, 16 Nov 2017 15:24:30 GMT', 'Accept-Ranges': 'bytes', 'Content-Type': 'application/octet-stream', - self.cloud._OBJECT_AUTOCREATE_KEY: 'true', - 'Etag': fakes.NO_MD5}), + ('X-Object-Meta-' + + self.cloud._OBJECT_AUTOCREATE_KEY): 'true', + 'Etag': fakes.NO_MD5, + 'X-Static-Large-Object': 'false'}), dict(method='DELETE', uri='{endpoint}/{container}/{object}'.format( endpoint=endpoint, container=self.container_name, diff --git a/openstack/tests/unit/cloud/test_object.py b/openstack/tests/unit/cloud/test_object.py index 3cc861ec5..0a74731a6 100644 --- a/openstack/tests/unit/cloud/test_object.py +++ b/openstack/tests/unit/cloud/test_object.py @@ -1079,11 +1079,12 @@ def test_slo_manifest_fail(self): 'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1', 'Content-Length': '1290170880', 'Last-Modified': 'Tue, 14 Apr 2015 18:29:01 GMT', - 'x-object-meta-x-sdk-autocreated': 'true', + 'X-Object-Meta-x-sdk-autocreated': 'true', 'X-Object-Meta-X-Shade-Sha256': 'does not matter', 'X-Object-Meta-X-Shade-Md5': 'does not matter', 'Date': 'Thu, 16 Nov 2017 15:24:30 GMT', 'Accept-Ranges': 'bytes', + 'X-Static-Large-Object': 'false', 'Content-Type': 'application/octet-stream', 'Etag': '249219347276c331b87bf1ac2152d9af', }), diff --git a/openstack/tests/unit/object_store/v1/test_proxy.py b/openstack/tests/unit/object_store/v1/test_proxy.py index a88bb5838..64b74095f 100644 --- a/openstack/tests/unit/object_store/v1/test_proxy.py +++ b/openstack/tests/unit/object_store/v1/test_proxy.py @@ -17,6 +17,7 @@ import time from unittest import mock +import requests_mock from testscenarios import load_tests_apply_scenarios as load_tests # noqa from openstack.object_store.v1 import account @@ -26,6 +27,16 @@ from openstack.tests.unit import test_proxy_base +class FakeResponse: + def __init__(self, response, status_code=200, headers=None): + self.body = response + self.status_code = status_code + self.headers = headers if headers else {} + + def json(self): + return self.body + + class TestObjectStoreProxy(test_proxy_base.TestProxyBase): kwargs_to_path_args = False @@ -111,12 +122,31 @@ def test_object_create_no_container(self): self.assertRaises(TypeError, self.proxy.upload_object) def test_object_get(self): - kwargs = dict(container="container") - self.verify_get( - self.proxy.get_object, obj.Object, - method_args=["object"], - method_kwargs=kwargs, - expected_kwargs=kwargs) + with requests_mock.Mocker() as m: + m.get("%scontainer/object" % self.endpoint, + text="data") + res = self.proxy.get_object("object", container="container") + self.assertIsNone(res.data) + + def test_object_get_write_file(self): + with requests_mock.Mocker() as m: + m.get("%scontainer/object" % self.endpoint, + text="data") + with tempfile.NamedTemporaryFile() as f: + self.proxy.get_object( + "object", container="container", + outfile=f.name) + dt = open(f.name).read() + self.assertEqual(dt, "data") + + def test_object_get_remember_content(self): + with requests_mock.Mocker() as m: + m.get("%scontainer/object" % self.endpoint, + text="data") + res = self.proxy.get_object( + "object", container="container", + remember_content=True) + self.assertEqual(res.data, "data") def test_set_temp_url_key(self): From 15a8fc4c26e8fe7897552ba943b3f502fca7058e Mon Sep 17 00:00:00 2001 From: Jesper Schmitz Mouridsen Date: Sun, 3 Oct 2021 10:16:39 +0200 Subject: [PATCH 2924/3836] Support description in sg-rule creation Story: 2008876 Task: 42420 Signed-off-by: Jesper Schmitz Mouridsen Change-Id: I9e2ab6015155dd2e67788f515fd42231fcf98547 --- openstack/cloud/_security_group.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openstack/cloud/_security_group.py b/openstack/cloud/_security_group.py index 496e588f3..7861e57ed 100644 --- a/openstack/cloud/_security_group.py +++ b/openstack/cloud/_security_group.py @@ -241,7 +241,8 @@ def create_security_group_rule(self, remote_address_group_id=None, direction='ingress', ethertype='IPv4', - project_id=None): + project_id=None, + description=None): """Create a new security group rule :param string secgroup_name_or_id: @@ -285,7 +286,8 @@ def create_security_group_rule(self, :param string project_id: Specify the project ID this security group will be created on (admin-only). - + :param string description: + Description of the rule, max 255 characters. :returns: A ``munch.Munch`` representing the new security group rule. :raises: OpenStackCloudException on operation error. @@ -319,7 +321,8 @@ def create_security_group_rule(self, } if project_id is not None: rule_def['tenant_id'] = project_id - + if description is not None: + rule_def["description"] = description return self.network.create_security_group_rule( **rule_def ) From 07eb09f631dcdf876320fb866bbd48cf123924bc Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 5 Oct 2021 14:37:54 +0200 Subject: [PATCH 2925/3836] Rely on proxy for compute.flavor cloud layer operations - stop applying normalization to flavor operations in the cloud layer - return bare flavor resources - drop logic for fetching extra_specs in cloud layer (already present in proxy) - use conn.compute.extensions to get extensions - drop flavor.links attribute as being absoutely useless on the resource Change-Id: Id6fc34fd1aa7dec37a4886eeef9206cda685f4bb --- openstack/cloud/_compute.py | 30 +++++-------------- openstack/compute/v2/flavor.py | 7 ++--- openstack/tests/unit/base.py | 29 ++++++++++++++++++ openstack/tests/unit/cloud/test_caching.py | 12 ++++---- openstack/tests/unit/cloud/test_shade.py | 8 ++--- .../tests/unit/compute/v2/test_flavor.py | 1 - 6 files changed, 48 insertions(+), 39 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 8bee8f90b..2c46906bd 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -72,13 +72,7 @@ def get_flavor_by_ram(self, ram, include=None, get_extra=True): @_utils.cache_on_arguments() def _nova_extensions(self): - extensions = set() - data = proxy._json_response( - self.compute.get('/extensions'), - error_message="Error fetching extension list for nova") - - for extension in self._get_and_munchify('extensions', data): - extensions.add(extension['alias']) + extensions = set([e.alias for e in self.compute.extensions()]) return extensions def _has_nova_extension(self, extension_name): @@ -160,13 +154,8 @@ def list_flavors(self, get_extra=False): :returns: A list of flavor ``munch.Munch``. """ - data = self.compute.flavors(details=True) - flavors = [] - - for flavor in data: - if not flavor.extra_specs and get_extra: - flavor.fetch_extra_specs(self.compute) - flavors.append(flavor._to_munch(original_names=False)) + flavors = list(self.compute.flavors( + details=True, get_extra_specs=get_extra)) return flavors def list_server_security_groups(self, server): @@ -429,9 +418,9 @@ def get_flavor(self, name_or_id, filters=None, get_extra=True): if not filters: filters = {} flavor = self.compute.find_flavor( - name_or_id, get_extra_specs=get_extra, **filters) - if flavor: - return flavor._to_munch(original_names=False) + name_or_id, get_extra_specs=get_extra, + ignore_missing=True, **filters) + return flavor def get_flavor_by_id(self, id, get_extra=False): """ Get a flavor by ID @@ -442,8 +431,7 @@ def get_flavor_by_id(self, id, get_extra=False): specs. :returns: A flavor ``munch.Munch``. """ - flavor = self.compute.get_flavor(id, get_extra_specs=get_extra) - return flavor._to_munch(original_names=False) + return self.compute.get_flavor(id, get_extra_specs=get_extra) def get_server_console(self, server, length=None): """Get the console log for a server. @@ -1393,9 +1381,7 @@ def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", if flavorid == 'auto': attrs['id'] = None - flavor = self.compute.create_flavor(**attrs) - - return flavor._to_munch(original_names=False) + return self.compute.create_flavor(**attrs) def delete_flavor(self, name_or_id): """Delete a flavor diff --git a/openstack/compute/v2/flavor.py b/openstack/compute/v2/flavor.py index 73b965add..6fb40fb49 100644 --- a/openstack/compute/v2/flavor.py +++ b/openstack/compute/v2/flavor.py @@ -36,11 +36,10 @@ class Flavor(resource.Resource): _max_microversion = '2.61' # Properties - #: Links pertaining to this flavor. This is a list of dictionaries, - #: each including keys ``href`` and ``rel``. - links = resource.Body('links') #: The name of this flavor. - name = resource.Body('name') + name = resource.Body('name', alias='original_name') + #: The name of this flavor when returned by server list/show + original_name = resource.Body('original_name') #: The description of the flavor. description = resource.Body('description') #: Size of the disk this flavor offers. *Type: int* diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 804f2ff9c..e2b6226d9 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -745,6 +745,35 @@ def assert_calls(self, stop_after=None, do_count=True): self.assertEqual( len(self.calls), len(self.adapter.request_history)) + def assertResourceEqual(self, actual, expected, resource_type): + """Helper for the assertEqual which compares Resource object against + dictionary representing expected state. + + :param Resource actual: actual object. + :param dict expected: dictionary representing expected object. + :param class resource_type: class type to be applied for the expected + resource. + """ + return self.assertEqual( + resource_type(**expected).to_dict(computed=False), + actual.to_dict(computed=False) + ) + + def assertResourceListEqual(self, actual, expected, resource_type): + """Helper for the assertEqual which compares Resource lists object against + dictionary representing expected state. + + :param list actual: List of actual objects. + :param listexpected: List of dictionaries representing expected + objects. + :param class resource_type: class type to be applied for the expected + resource. + """ + self.assertEqual( + [resource_type(**f).to_dict(computed=False) for f in expected], + [f.to_dict(computed=False) for f in actual] + ) + class IronicTestCase(TestCase): diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py index 0ea85c0ca..47c165653 100644 --- a/openstack/tests/unit/cloud/test_caching.py +++ b/openstack/tests/unit/cloud/test_caching.py @@ -440,14 +440,12 @@ def test_list_flavors(self): self.assertEqual([], self.cloud.list_flavors()) - fake_flavor_dicts = [ - _flavor.Flavor(connection=self.cloud, - **f)._to_munch(original_names=False) - for f in fakes.FAKE_FLAVOR_LIST - ] - self.cloud.list_flavors.invalidate(self.cloud) - self.assertEqual(fake_flavor_dicts, self.cloud.list_flavors()) + self.assertResourceListEqual( + self.cloud.list_flavors(), + fakes.FAKE_FLAVOR_LIST, + _flavor.Flavor + ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_shade.py b/openstack/tests/unit/cloud/test_shade.py index fa47345ab..91a8377ff 100644 --- a/openstack/tests/unit/cloud/test_shade.py +++ b/openstack/tests/unit/cloud/test_shade.py @@ -535,11 +535,9 @@ def test__nova_extensions_fails(self): endpoint=fakes.COMPUTE_ENDPOINT), status_code=404), ]) - with testtools.ExpectedException( - exc.OpenStackCloudURINotFound, - "Error fetching extension list for nova" - ): - self.cloud._nova_extensions() + self.assertRaises( + exceptions.ResourceNotFound, + self.cloud._nova_extensions) self.assert_calls() diff --git a/openstack/tests/unit/compute/v2/test_flavor.py b/openstack/tests/unit/compute/v2/test_flavor.py index 77cf32e96..c83cc3fce 100644 --- a/openstack/tests/unit/compute/v2/test_flavor.py +++ b/openstack/tests/unit/compute/v2/test_flavor.py @@ -66,7 +66,6 @@ def test_basic(self): def test_make_basic(self): sot = flavor.Flavor(**BASIC_EXAMPLE) self.assertEqual(BASIC_EXAMPLE['id'], sot.id) - self.assertEqual(BASIC_EXAMPLE['links'], sot.links) self.assertEqual(BASIC_EXAMPLE['name'], sot.name) self.assertEqual(BASIC_EXAMPLE['description'], sot.description) self.assertEqual(BASIC_EXAMPLE['disk'], sot.disk) From 93a8258e6668644c7eb93ac72f23187d42f27d92 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 8 Oct 2021 18:23:07 +0100 Subject: [PATCH 2926/3836] resource: Remove deprecated 'allow_get' attribute Change-Id: I7553fa300a4527fd29f7a945a0dc0c269a7e4730 Signed-off-by: Stephen Finucane --- openstack/resource.py | 4 ---- .../drop-Resource-allow_get-attribute-fec75b551fb79465.yaml | 5 +++++ 2 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/drop-Resource-allow_get-attribute-fec75b551fb79465.yaml diff --git a/openstack/resource.py b/openstack/resource.py index 2305080d1..d8dabff7a 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -454,10 +454,6 @@ class Resource(dict): #: Allow patch operation for this resource. allow_patch = False - # TODO(mordred) Unused - here for transition with OSC. Remove once - # OSC no longer checks for allow_get - allow_get = True - #: Commits happen without header or body being dirty. allow_empty_commit = False diff --git a/releasenotes/notes/drop-Resource-allow_get-attribute-fec75b551fb79465.yaml b/releasenotes/notes/drop-Resource-allow_get-attribute-fec75b551fb79465.yaml new file mode 100644 index 000000000..422029a73 --- /dev/null +++ b/releasenotes/notes/drop-Resource-allow_get-attribute-fec75b551fb79465.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + The ``allow_get`` attribute of ``openstack.resource.Resource`` has been + removed. Use ``allow_fetch`` or ``allow_list`` instead. From 45f400dc0d6cb0168c9f517d6d63f29d7753a06f Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 8 Oct 2021 18:30:39 +0100 Subject: [PATCH 2927/3836] resource: Rewrap function signatures We're going to be doing some work on these functions to improve how we handle pagination and nested resources. Before we start this surgery, rewrap the function signatures. Change-Id: I660599355fa6d1501acb5e8584245985dd05440e Signed-off-by: Stephen Finucane --- openstack/resource.py | 250 ++++++++++++++++++++++++++---------------- 1 file changed, 153 insertions(+), 97 deletions(-) diff --git a/openstack/resource.py b/openstack/resource.py index d8dabff7a..f73f5aaac 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -10,7 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -""" +"""Base resource class. + The :class:`~openstack.resource.Resource` class is a base class that represent a remote resource. The attributes that comprise a request or response for this resource are specified @@ -969,7 +970,7 @@ def existing(cls, connection=None, **kwargs): modified on the server. :param dict kwargs: Each of the named arguments will be set as - attributes on the resulting Resource object. + attributes on the resulting Resource object. """ return cls(_synchronized=True, connection=connection, **kwargs) @@ -986,26 +987,33 @@ def _from_munch(cls, obj, synchronized=True, connection=None): """ return cls(_synchronized=synchronized, connection=connection, **obj) - def to_dict(self, body=True, headers=True, computed=True, - ignore_none=False, original_names=False, _to_munch=False): + def to_dict( + self, + body=True, + headers=True, + computed=True, + ignore_none=False, + original_names=False, + _to_munch=False, + ): """Return a dictionary of this resource's contents :param bool body: Include the :class:`~openstack.resource.Body` - attributes in the returned dictionary. + attributes in the returned dictionary. :param bool headers: Include the :class:`~openstack.resource.Header` - attributes in the returned dictionary. + attributes in the returned dictionary. :param bool computed: Include the :class:`~openstack.resource.Computed` - attributes in the returned dictionary. + attributes in the returned dictionary. :param bool ignore_none: When True, exclude key/value pairs where - the value is None. This will exclude - attributes that the server hasn't returned. + the value is None. This will exclude attributes that the server + hasn't returned. :param bool original_names: When True, use attribute names as they - were received from the server. + were received from the server. :param bool _to_munch: For internal use only. Converts to `munch.Munch` - instead of dict. + instead of dict. :return: A dictionary of key/value pairs where keys are named - as they exist as attributes of this class. + as they exist as attributes of this class. """ if _to_munch: mapping = munch.Munch() @@ -1061,6 +1069,7 @@ def to_dict(self, body=True, headers=True, computed=True, mapping[key] = value return mapping + # Compatibility with the munch.Munch.toDict method toDict = to_dict # Make the munch copy method use to_dict @@ -1125,8 +1134,15 @@ def _prepare_request_body(self, patch, prepend_key): body = {self.resource_key: body} return body - def _prepare_request(self, requires_id=None, prepend_key=False, - patch=False, base_path=None, params=None, **kwargs): + def _prepare_request( + self, + requires_id=None, + prepend_key=False, + patch=False, + base_path=None, + params=None, + **kwargs, + ): """Prepare a request to be sent to the server Create operations don't require an ID, but all others do, @@ -1247,6 +1263,7 @@ def _get_microversion_for_list(cls, session): """Get microversion to use when listing resources. The base version uses the following logic: + 1. If the session has a default microversion for the current service, just use it. 2. If ``self._max_microversion`` is not ``None``, use minimum between @@ -1280,8 +1297,9 @@ def _get_microversion_for(self, session, action): return self._get_microversion_for_list(session) - def _assert_microversion_for(self, session, action, expected, - error_message=None): + def _assert_microversion_for( + self, session, action, expected, error_message=None, + ): """Enforce that the microversion for action satisfies the requirement. :param session: :class`keystoneauth1.adapter.Adapter` @@ -1325,15 +1343,14 @@ def create(self, session, prepend_key=True, base_path=None, **params): :param session: The session to use for making this request. :type session: :class:`~keystoneauth1.adapter.Adapter` :param prepend_key: A boolean indicating whether the resource_key - should be prepended in a resource creation - request. Default to True. + should be prepended in a resource creation request. Default to + True. :param str base_path: Base part of the URI for creating resources, if - different from - :data:`~openstack.resource.Resource.base_path`. + different from :data:`~openstack.resource.Resource.base_path`. :param dict params: Additional params to pass. :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if - :data:`Resource.allow_create` is not set to ``True``. + :data:`Resource.allow_create` is not set to ``True``. """ if not self.allow_create: raise exceptions.MethodNotSupported(self, "create") @@ -1376,24 +1393,29 @@ def create(self, session, prepend_key=True, base_path=None, **params): return self @classmethod - def bulk_create(cls, session, data, prepend_key=True, base_path=None, - **params): + def bulk_create( + cls, + session, + data, + prepend_key=True, + base_path=None, + **params, + ): """Create multiple remote resources based on this class and data. :param session: The session to use for making this request. :type session: :class:`~keystoneauth1.adapter.Adapter` :param data: list of dicts, which represent resources to create. :param prepend_key: A boolean indicating whether the resource_key - should be prepended in a resource creation - request. Default to True. + should be prepended in a resource creation request. Default to + True. :param str base_path: Base part of the URI for creating resources, if - different from - :data:`~openstack.resource.Resource.base_path`. + different from :data:`~openstack.resource.Resource.base_path`. :param dict params: Additional params to pass. :return: A generator of :class:`Resource` objects. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if - :data:`Resource.allow_create` is not set to ``True``. + :data:`Resource.allow_create` is not set to ``True``. """ if not cls.allow_create: raise exceptions.MethodNotSupported(cls, "create") @@ -1452,25 +1474,30 @@ def bulk_create(cls, session, data, prepend_key=True, base_path=None, connection=session._get_connection(), **res_dict) for res_dict in data) - def fetch(self, session, requires_id=True, - base_path=None, error_message=None, **params): + def fetch( + self, + session, + requires_id=True, + base_path=None, + error_message=None, + **params, + ): """Get a remote resource based on this instance. :param session: The session to use for making this request. :type session: :class:`~keystoneauth1.adapter.Adapter` :param boolean requires_id: A boolean indicating whether resource ID - should be part of the requested URI. + should be part of the requested URI. :param str base_path: Base part of the URI for fetching resources, if - different from - :data:`~openstack.resource.Resource.base_path`. + different from :data:`~openstack.resource.Resource.base_path`. :param str error_message: An Error message to be returned if - requested object does not exist. + requested object does not exist. :param dict params: Additional parameters that can be consumed. :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if - :data:`Resource.allow_fetch` is not set to ``True``. + :data:`Resource.allow_fetch` is not set to ``True``. :raises: :exc:`~openstack.exceptions.ResourceNotFound` if - the resource was not found. + the resource was not found. """ if not self.allow_fetch: raise exceptions.MethodNotSupported(self, "fetch") @@ -1495,14 +1522,13 @@ def head(self, session, base_path=None): :param session: The session to use for making this request. :type session: :class:`~keystoneauth1.adapter.Adapter` :param str base_path: Base part of the URI for fetching resources, if - different from - :data:`~openstack.resource.Resource.base_path`. + different from :data:`~openstack.resource.Resource.base_path`. :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if - :data:`Resource.allow_head` is not set to ``True``. - :raises: :exc:`~openstack.exceptions.ResourceNotFound` if - the resource was not found. + :data:`Resource.allow_head` is not set to ``True``. + :raises: :exc:`~openstack.exceptions.ResourceNotFound` if the resource + was not found. """ if not self.allow_head: raise exceptions.MethodNotSupported(self, "head") @@ -1524,27 +1550,32 @@ def requires_commit(self): return (self._body.dirty or self._header.dirty or self.allow_empty_commit) - def commit(self, session, prepend_key=True, has_body=True, - retry_on_conflict=None, base_path=None, **kwargs): + def commit( + self, + session, + prepend_key=True, + has_body=True, + retry_on_conflict=None, + base_path=None, + **kwargs, + ): """Commit the state of the instance to the remote resource. :param session: The session to use for making this request. :type session: :class:`~keystoneauth1.adapter.Adapter` :param prepend_key: A boolean indicating whether the resource_key - should be prepended in a resource update request. - Default to True. + should be prepended in a resource update request. + Default to True. :param bool retry_on_conflict: Whether to enable retries on HTTP - CONFLICT (409). Value of ``None`` leaves - the `Adapter` defaults. + CONFLICT (409). Value of ``None`` leaves the `Adapter` defaults. :param str base_path: Base part of the URI for modifying resources, if - different from - :data:`~openstack.resource.Resource.base_path`. + different from :data:`~openstack.resource.Resource.base_path`. :param dict kwargs: Parameters that will be passed to - _prepare_request() + _prepare_request() :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if - :data:`Resource.allow_commit` is not set to ``True``. + :data:`Resource.allow_commit` is not set to ``True``. """ # The id cannot be dirty for an commit self._body._dirty.discard("id") @@ -1570,8 +1601,15 @@ def commit(self, session, prepend_key=True, has_body=True, has_body=has_body, retry_on_conflict=retry_on_conflict) - def _commit(self, session, request, method, microversion, has_body=True, - retry_on_conflict=None): + def _commit( + self, + session, + request, + method, + microversion, + has_body=True, + retry_on_conflict=None, + ): session = self._get_session(session) kwargs = {} @@ -1625,8 +1663,15 @@ def _convert_patch(self, patch): return converted - def patch(self, session, patch=None, prepend_key=True, has_body=True, - retry_on_conflict=None, base_path=None): + def patch( + self, + session, + patch=None, + prepend_key=True, + has_body=True, + retry_on_conflict=None, + base_path=None, + ): """Patch the remote resource. Allows modifying the resource by providing a list of JSON patches to @@ -1636,21 +1681,18 @@ def patch(self, session, patch=None, prepend_key=True, has_body=True, :param session: The session to use for making this request. :type session: :class:`~keystoneauth1.adapter.Adapter` :param patch: Additional JSON patch as a list or one patch item. - If provided, it is applied on top of any changes to the - current resource. + If provided, it is applied on top of any changes to the current + resource. :param prepend_key: A boolean indicating whether the resource_key - should be prepended in a resource update request. - Default to True. + should be prepended in a resource update request. Default to True. :param bool retry_on_conflict: Whether to enable retries on HTTP - CONFLICT (409). Value of ``None`` leaves - the `Adapter` defaults. + CONFLICT (409). Value of ``None`` leaves the `Adapter` defaults. :param str base_path: Base part of the URI for modifying resources, if - different from - :data:`~openstack.resource.Resource.base_path`. + different from :data:`~openstack.resource.Resource.base_path`. :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if - :data:`Resource.allow_patch` is not set to ``True``. + :data:`Resource.allow_patch` is not set to ``True``. """ # The id cannot be dirty for an commit self._body._dirty.discard("id") @@ -1678,13 +1720,13 @@ def delete(self, session, error_message=None, **kwargs): :param session: The session to use for making this request. :type session: :class:`~keystoneauth1.adapter.Adapter` :param dict kwargs: Parameters that will be passed to - _prepare_request() + _prepare_request() :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if - :data:`Resource.allow_commit` is not set to ``True``. + :data:`Resource.allow_commit` is not set to ``True``. :raises: :exc:`~openstack.exceptions.ResourceNotFound` if - the resource was not found. + the resource was not found. """ response = self._raw_delete(session, **kwargs) @@ -1708,8 +1750,14 @@ def _raw_delete(self, session, **kwargs): microversion=microversion) @classmethod - def list(cls, session, paginated=True, base_path=None, - allow_unknown_params=False, **params): + def list( + cls, + session, + paginated=True, + base_path=None, + allow_unknown_params=False, + **params, + ): """This method is a generator which yields resource objects. This resource object list generator handles pagination and takes query @@ -1718,11 +1766,9 @@ def list(cls, session, paginated=True, base_path=None, :param session: The session to use for making this request. :type session: :class:`~keystoneauth1.adapter.Adapter` :param bool paginated: ``True`` if a GET to this resource returns - a paginated series of responses, or ``False`` - if a GET returns only one page of data. - **When paginated is False only one - page of data will be returned regardless - of the API's support of pagination.** + a paginated series of responses, or ``False`` if a GET returns only + one page of data. **When paginated is False only one page of data + will be returned regardless of the API's support of pagination.** :param str base_path: Base part of the URI for listing resources, if different from :data:`~openstack.resource.Resource.base_path`. :param bool allow_unknown_params: ``True`` to accept, but discard @@ -1731,19 +1777,18 @@ def list(cls, session, paginated=True, base_path=None, validation exception when unknown query parameters are passed. :param dict params: These keyword arguments are passed through the :meth:`~openstack.resource.QueryParamter._transpose` method - to find if any of them match expected query parameters to be - sent in the *params* argument to + to find if any of them match expected query parameters to be sent + in the *params* argument to :meth:`~keystoneauth1.adapter.Adapter.get`. They are additionally - checked against the - :data:`~openstack.resource.Resource.base_path` format string - to see if any path fragments need to be filled in by the contents - of this argument. + checked against the :data:`~openstack.resource.Resource.base_path` + format string to see if any path fragments need to be filled in by + the contents of this argument. :return: A generator of :class:`Resource` objects. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if - :data:`Resource.allow_list` is not set to ``True``. + :data:`Resource.allow_list` is not set to ``True``. :raises: :exc:`~openstack.exceptions.InvalidResourceQuery` if query - contains invalid params. + contains invalid params. """ if not cls.allow_list: raise exceptions.MethodNotSupported(cls, "list") @@ -1914,8 +1959,12 @@ def _get_one_match(cls, name_or_id, results): @classmethod def find( - cls, session, name_or_id, ignore_missing=True, - list_base_path=None, **params + cls, + session, + name_or_id, + ignore_missing=True, + list_base_path=None, + **params, ): """Find a resource by its name or id. @@ -1935,11 +1984,11 @@ def find( URI parameters. :return: The :class:`Resource` object matching the given name or id - or None if nothing matches. + or None if nothing matches. :raises: :class:`openstack.exceptions.DuplicateResource` if more - than one resource is found for this request. + than one resource is found for this request. :raises: :class:`openstack.exceptions.ResourceNotFound` if nothing - is found and ignore_missing is ``False``. + is found and ignore_missing is ``False``. """ session = cls._get_session(session) # Try to short-circuit by looking directly for a matching ID. @@ -1979,31 +2028,38 @@ def _normalize_status(status): return status -def wait_for_status(session, resource, status, failures, interval=None, - wait=None, attribute='status'): +def wait_for_status( + session, + resource, + status, + failures, + interval=None, + wait=None, + attribute='status', +): """Wait for the resource to be in a particular status. :param session: The session to use for making this request. :type session: :class:`~keystoneauth1.adapter.Adapter` :param resource: The resource to wait on to reach the status. The resource - must have a status attribute specified via ``attribute``. + must have a status attribute specified via ``attribute``. :type resource: :class:`~openstack.resource.Resource` :param status: Desired status of the resource. :param list failures: Statuses that would indicate the transition - failed such as 'ERROR'. Defaults to ['ERROR']. + failed such as 'ERROR'. Defaults to ['ERROR']. :param interval: Number of seconds to wait between checks. - Set to ``None`` to use the default interval. + Set to ``None`` to use the default interval. :param wait: Maximum number of seconds to wait for transition. - Set to ``None`` to wait forever. + Set to ``None`` to wait forever. :param attribute: Name of the resource attribute that contains the status. :return: The updated resource. :raises: :class:`~openstack.exceptions.ResourceTimeout` transition - to status failed to occur in wait seconds. + to status failed to occur in wait seconds. :raises: :class:`~openstack.exceptions.ResourceFailure` resource - transitioned to one of the failure states. + transitioned to one of the failure states. :raises: :class:`~AttributeError` if the resource does not have a status - attribute + attribute """ current_status = getattr(resource, attribute) @@ -2054,7 +2110,7 @@ def wait_for_delete(session, resource, interval, wait): :return: Method returns self on success. :raises: :class:`~openstack.exceptions.ResourceTimeout` transition - to status failed to occur in wait seconds. + to status failed to occur in wait seconds. """ orig_resource = resource for count in utils.iterate_timeout( From 388a463a47cbc15a7c57b72c1bf1c7d192fad257 Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Mon, 18 Oct 2021 10:42:27 +0200 Subject: [PATCH 2928/3836] Reindentation of the docstrings Change-Id: Ic94fdc59594449583bc699480b19f9697f2ddfe3 --- openstack/instance_ha/v1/_proxy.py | 83 +++++++++++++----------------- 1 file changed, 37 insertions(+), 46 deletions(-) diff --git a/openstack/instance_ha/v1/_proxy.py b/openstack/instance_ha/v1/_proxy.py index 28701e553..518c2217e 100644 --- a/openstack/instance_ha/v1/_proxy.py +++ b/openstack/instance_ha/v1/_proxy.py @@ -30,7 +30,7 @@ def notifications(self, **query): """Return a generator of notifications. :param kwargs query: Optional query parameters to be sent to - limit the notifications being returned. + limit the notifications being returned. :returns: A generator of notifications """ return self._list(_notification.Notification, **query) @@ -39,13 +39,12 @@ def get_notification(self, notification): """Get a single notification. :param notification: The value can be the ID of a notification or a - :class: - `~masakariclient.sdk.ha.v1 - .notification.Notification` instance. - :returns: One :class:`~masakariclient.sdk.ha.v1 - .notification.Notification` + :class: `~masakariclient.sdk.ha.v1.notification.Notification` + instance. + :returns: One + :class:`~masakariclient.sdk.ha.v1.notification.Notification` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_notification.Notification, notification) @@ -53,14 +52,10 @@ def create_notification(self, **attrs): """Create a new notification. :param dict attrs: Keyword arguments which will be used to create - a :class: - `masakariclient.sdk.ha.v1 - .notification.Notification`, - comprised of the propoerties on the Notification - class. + a :class:`masakariclient.sdk.ha.v1.notification.Notification`, + comprised of the propoerties on the Notification class. :returns: The result of notification creation - :rtype: :class: `masakariclient.sdk.ha.v1 - .notification.Notification` + :rtype: :class: `masakariclient.sdk.ha.v1.notification.Notification` """ return self._create(_notification.Notification, **attrs) @@ -68,7 +63,7 @@ def segments(self, **query): """Return a generator of segments. :param kwargs query: Optional query parameters to be sent to - limit the segments being returned. + limit the segments being returned. :returns: A generator of segments """ return self._list(_segment.Segment, **query) @@ -77,11 +72,10 @@ def get_segment(self, segment): """Get a single segment. :param segment: The value can be the ID of a segment or a - :class: - `~masakariclient.sdk.ha.v1.segment.Segment` instance. + :class: `~masakariclient.sdk.ha.v1.segment.Segment` instance. :returns: One :class:`~masakariclient.sdk.ha.v1.segment.Segment` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_segment.Segment, segment) @@ -89,9 +83,8 @@ def create_segment(self, **attrs): """Create a new segment. :param dict attrs: Keyword arguments which will be used to create - a :class: - `masakariclient.sdk.ha.v1.segment.Segment`, - comprised of the propoerties on the Segment class. + a :class: `masakariclient.sdk.ha.v1.segment.Segment`, + comprised of the propoerties on the Segment class. :returns: The result of segment creation :rtype: :class: `masakariclient.sdk.ha.v1.segment.Segment` """ @@ -101,12 +94,10 @@ def update_segment(self, segment, **attrs): """Update a segment. :param segment: The value can be the ID of a segment or a - :class: - `~masakariclient.sdk.ha.v1.segment.Segment` instance. + :class: `~masakariclient.sdk.ha.v1.segment.Segment` instance. :param dict attrs: Keyword arguments which will be used to update - a :class: - `masakariclient.sdk.ha.v1.segment.Segment`, - comprised of the propoerties on the Segment class. + a :class: `masakariclient.sdk.ha.v1.segment.Segment`, + comprised of the propoerties on the Segment class. :returns: The updated segment. :rtype: :class: `masakariclient.sdk.ha.v1.segment.Segment` """ @@ -119,10 +110,10 @@ def delete_segment(self, segment, ignore_missing=True): The value can be either the ID of a segment or a :class:`~masakariclient.sdk.ha.v1.segment.Segment` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the segment does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent segment. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the segment does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent segment. :returns: ``None`` """ return self._delete(_segment.Segment, segment, @@ -133,7 +124,7 @@ def hosts(self, segment_id, **query): :param segment_id: The ID of a failover segment. :param kwargs query: Optional query parameters to be sent to - limit the hosts being returned. + limit the hosts being returned. :returns: A generator of hosts """ @@ -144,8 +135,8 @@ def create_host(self, segment_id, **attrs): :param segment_id: The ID of a failover segment. :param dict attrs: Keyword arguments which will be used to create - a :class: `masakariclient.sdk.ha.v1.host.Host`, - comprised of the propoerties on the Host class. + a :class: `masakariclient.sdk.ha.v1.host.Host`, + comprised of the propoerties on the Host class. :returns: The results of host creation """ @@ -156,13 +147,13 @@ def get_host(self, host, segment_id=None): :param segment_id: The ID of a failover segment. :param host: The value can be the ID of a host or a :class: - `~masakariclient.sdk.ha.v1.host.Host` instance. + `~masakariclient.sdk.ha.v1.host.Host` instance. :returns: One :class:`~masakariclient.sdk.ha.v1.host.Host` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. :raises: :class:`~openstack.exceptions.InvalidRequest` - when segment_id is None. + when segment_id is None. """ if segment_id is None: raise exceptions.InvalidRequest("'segment_id' must be specified.") @@ -175,14 +166,14 @@ def update_host(self, host, segment_id, **attrs): :param segment_id: The ID of a failover segment. :param host: The value can be the ID of a host or a :class: - `~masakariclient.sdk.ha.v1.host.Host` instance. + `~masakariclient.sdk.ha.v1.host.Host` instance. :param dict attrs: The attributes to update on the host represented. :returns: The updated host :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. :raises: :class:`~openstack.exceptions.InvalidRequest` - when segment_id is None. + when segment_id is None. """ host_id = resource.Resource._get_id(host) return self._update(_host.Host, host_id, segment_id=segment_id, @@ -193,18 +184,18 @@ def delete_host(self, host, segment_id=None, ignore_missing=True): :param segment_id: The ID of a failover segment. :param host: The value can be the ID of a host or a :class: - `~masakariclient.sdk.ha.v1.host.Host` instance. + `~masakariclient.sdk.ha.v1.host.Host` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the host does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent host. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the host does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent host. :returns: ``None`` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. :raises: :class:`~openstack.exceptions.InvalidRequest` - when segment_id is None. + when segment_id is None. """ if segment_id is None: From 4ad2d0d2a084ce67d3b24e00da3eeea2bae8ff6a Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Mon, 18 Oct 2021 10:44:46 +0200 Subject: [PATCH 2929/3836] Reindentation of the docstrings Change-Id: If2dfd76bc52ae2810d835e4a530333789f2555ca --- openstack/message/v2/_proxy.py | 56 +++++++++++++++++----------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/openstack/message/v2/_proxy.py b/openstack/message/v2/_proxy.py index 365b73a85..09bbd6907 100644 --- a/openstack/message/v2/_proxy.py +++ b/openstack/message/v2/_proxy.py @@ -24,8 +24,8 @@ def create_queue(self, **attrs): """Create a new queue from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.message.v2.queue.Queue`, - comprised of the properties on the Queue class. + a :class:`~openstack.message.v2.queue.Queue`, + comprised of the properties on the Queue class. :returns: The results of queue creation :rtype: :class:`~openstack.message.v2.queue.Queue` @@ -65,12 +65,12 @@ def delete_queue(self, value, ignore_missing=True): """Delete a queue :param value: The value can be either the name of a queue or a - :class:`~openstack.message.v2.queue.Queue` instance. + :class:`~openstack.message.v2.queue.Queue` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the queue does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent queue. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the queue does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent queue. :returns: ``None`` """ @@ -81,7 +81,7 @@ def post_message(self, queue_name, messages): :param queue_name: The name of target queue to post message to. :param messages: List of messages body and TTL to post. - :type messages: :py:class:`list` + :type messages: :py:class:`list` :returns: A string includes location of messages successfully posted. """ @@ -133,16 +133,16 @@ def delete_message(self, queue_name, value, claim=None, :param queue_name: The name of target queue to delete message from. :param value: The value can be either the name of a message or a - :class:`~openstack.message.v2.message.Message` instance. + :class:`~openstack.message.v2.message.Message` instance. :param claim: The value can be the ID or a - :class:`~openstack.message.v2.claim.Claim` instance of - the claim seizing the message. If None, the message has - not been claimed. + :class:`~openstack.message.v2.claim.Claim` instance of + the claim seizing the message. If None, the message has + not been claimed. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the message does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent message. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the message does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent message. :returns: ``None`` """ @@ -206,15 +206,15 @@ def delete_subscription(self, queue_name, value, ignore_missing=True): """Delete a subscription :param queue_name: The name of target queue to delete subscription - from. + from. :param value: The value can be either the name of a subscription or a - :class:`~openstack.message.v2.subscription.Subscription` - instance. + :class:`~openstack.message.v2.subscription.Subscription` + instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the subscription does not exist. - When set to ``True``, no exception will be thrown when - attempting to delete a nonexistent subscription. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the subscription does not exist. + When set to ``True``, no exception will be thrown when + attempting to delete a nonexistent subscription. :returns: ``None`` """ @@ -270,12 +270,12 @@ def delete_claim(self, queue_name, claim, ignore_missing=True): :param queue_name: The name of target queue to claim messages from. :param claim: The value can be either the ID of a claim or a - :class:`~openstack.message.v2.claim.Claim` instance. + :class:`~openstack.message.v2.claim.Claim` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the claim does not exist. - When set to ``True``, no exception will be thrown when - attempting to delete a nonexistent claim. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the claim does not exist. + When set to ``True``, no exception will be thrown when + attempting to delete a nonexistent claim. :returns: ``None`` """ From 322c72ba9ece6c412123f7ce60f99ce61b48257e Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Mon, 18 Oct 2021 12:17:07 +0200 Subject: [PATCH 2930/3836] Reindentation of the docstrings Change-Id: I43fac0c623422a622d873ea3e26118cb1cf61a75 --- openstack/identity/v2/_proxy.py | 102 ++++++++++++++++---------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/openstack/identity/v2/_proxy.py b/openstack/identity/v2/_proxy.py index f1097f4b3..537c95d9d 100644 --- a/openstack/identity/v2/_proxy.py +++ b/openstack/identity/v2/_proxy.py @@ -31,12 +31,12 @@ def get_extension(self, extension): """Get a single extension :param extension: The value can be the ID of an extension or a - :class:`~openstack.identity.v2.extension.Extension` - instance. + :class:`~openstack.identity.v2.extension.Extension` + instance. :returns: One :class:`~openstack.identity.v2.extension.Extension` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no extension can be found. + when no extension can be found. """ return self._get(_extension.Extension, extension) @@ -44,8 +44,8 @@ def create_role(self, **attrs): """Create a new role from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.identity.v2.role.Role`, - comprised of the properties on the Role class. + a :class:`~openstack.identity.v2.role.Role`, + comprised of the properties on the Role class. :returns: The results of role creation :rtype: :class:`~openstack.identity.v2.role.Role` @@ -56,12 +56,12 @@ def delete_role(self, role, ignore_missing=True): """Delete a role :param role: The value can be either the ID of a role or a - :class:`~openstack.identity.v2.role.Role` instance. + :class:`~openstack.identity.v2.role.Role` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the role does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent role. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the role does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent role. :returns: ``None`` """ @@ -72,10 +72,10 @@ def find_role(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a role. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.identity.v2.role.Role` or None """ return self._find(_role.Role, name_or_id, @@ -85,11 +85,11 @@ def get_role(self, role): """Get a single role :param role: The value can be the ID of a role or a - :class:`~openstack.identity.v2.role.Role` instance. + :class:`~openstack.identity.v2.role.Role` instance. :returns: One :class:`~openstack.identity.v2.role.Role` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_role.Role, role) @@ -97,7 +97,7 @@ def roles(self, **query): """Retrieve a generator of roles :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of role instances. :rtype: :class:`~openstack.identity.v2.role.Role` @@ -108,9 +108,9 @@ def update_role(self, role, **attrs): """Update a role :param role: Either the ID of a role or a - :class:`~openstack.identity.v2.role.Role` instance. + :class:`~openstack.identity.v2.role.Role` instance. :attrs kwargs: The attributes to update on the role represented - by ``value``. + by ``value``. :returns: The updated role :rtype: :class:`~openstack.identity.v2.role.Role` @@ -121,8 +121,8 @@ def create_tenant(self, **attrs): """Create a new tenant from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.identity.v2.tenant.Tenant`, - comprised of the properties on the Tenant class. + a :class:`~openstack.identity.v2.tenant.Tenant`, + comprised of the properties on the Tenant class. :returns: The results of tenant creation :rtype: :class:`~openstack.identity.v2.tenant.Tenant` @@ -133,12 +133,12 @@ def delete_tenant(self, tenant, ignore_missing=True): """Delete a tenant :param tenant: The value can be either the ID of a tenant or a - :class:`~openstack.identity.v2.tenant.Tenant` instance. + :class:`~openstack.identity.v2.tenant.Tenant` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the tenant does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent tenant. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the tenant does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent tenant. :returns: ``None`` """ @@ -149,10 +149,10 @@ def find_tenant(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a tenant. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.identity.v2.tenant.Tenant` or None """ return self._find(_tenant.Tenant, name_or_id, @@ -162,11 +162,11 @@ def get_tenant(self, tenant): """Get a single tenant :param tenant: The value can be the ID of a tenant or a - :class:`~openstack.identity.v2.tenant.Tenant` instance. + :class:`~openstack.identity.v2.tenant.Tenant` instance. :returns: One :class:`~openstack.identity.v2.tenant.Tenant` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_tenant.Tenant, tenant) @@ -174,7 +174,7 @@ def tenants(self, **query): """Retrieve a generator of tenants :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of tenant instances. :rtype: :class:`~openstack.identity.v2.tenant.Tenant` @@ -185,9 +185,9 @@ def update_tenant(self, tenant, **attrs): """Update a tenant :param tenant: Either the ID of a tenant or a - :class:`~openstack.identity.v2.tenant.Tenant` instance. + :class:`~openstack.identity.v2.tenant.Tenant` instance. :attrs kwargs: The attributes to update on the tenant represented - by ``value``. + by ``value``. :returns: The updated tenant :rtype: :class:`~openstack.identity.v2.tenant.Tenant` @@ -198,8 +198,8 @@ def create_user(self, **attrs): """Create a new user from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.identity.v2.user.User`, - comprised of the properties on the User class. + a :class:`~openstack.identity.v2.user.User`, + comprised of the properties on the User class. :returns: The results of user creation :rtype: :class:`~openstack.identity.v2.user.User` @@ -210,12 +210,12 @@ def delete_user(self, user, ignore_missing=True): """Delete a user :param user: The value can be either the ID of a user or a - :class:`~openstack.identity.v2.user.User` instance. + :class:`~openstack.identity.v2.user.User` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the user does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent user. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the user does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent user. :returns: ``None`` """ @@ -226,10 +226,10 @@ def find_user(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a user. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.identity.v2.user.User` or None """ return self._find(_user.User, name_or_id, @@ -239,11 +239,11 @@ def get_user(self, user): """Get a single user :param user: The value can be the ID of a user or a - :class:`~openstack.identity.v2.user.User` instance. + :class:`~openstack.identity.v2.user.User` instance. :returns: One :class:`~openstack.identity.v2.user.User` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_user.User, user) @@ -251,7 +251,7 @@ def users(self, **query): """Retrieve a generator of users :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of user instances. :rtype: :class:`~openstack.identity.v2.user.User` @@ -262,9 +262,9 @@ def update_user(self, user, **attrs): """Update a user :param user: Either the ID of a user or a - :class:`~openstack.identity.v2.user.User` instance. + :class:`~openstack.identity.v2.user.User` instance. :attrs kwargs: The attributes to update on the user represented - by ``value``. + by ``value``. :returns: The updated user :rtype: :class:`~openstack.identity.v2.user.User` From ef1b622b1c526a4ec3a22ca1619db83945bdaa40 Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Mon, 18 Oct 2021 12:19:25 +0200 Subject: [PATCH 2931/3836] Reindentation of the docstrings Change-Id: Ifbb2f76731aae14302bd902ae55930b14a1c876e --- openstack/identity/v3/_proxy.py | 506 ++++++++++++++++---------------- 1 file changed, 253 insertions(+), 253 deletions(-) diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 282f4b5db..0bf02a4b0 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -59,12 +59,12 @@ def delete_credential(self, credential, ignore_missing=True): """Delete a credential :param credential: The value can be either the ID of a credential or a - :class:`~openstack.identity.v3.credential.Credential` instance. + :class:`~openstack.identity.v3.credential.Credential` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the credential does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent credential. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the credential does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent credential. :returns: ``None`` """ @@ -76,12 +76,12 @@ def find_credential(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a credential. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.identity.v3.credential.Credential` - or None + or None """ return self._find(_credential.Credential, name_or_id, ignore_missing=ignore_missing) @@ -94,7 +94,7 @@ def get_credential(self, credential): :returns: One :class:`~openstack.identity.v3.credential.Credential` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_credential.Credential, credential) @@ -102,7 +102,7 @@ def credentials(self, **query): """Retrieve a generator of credentials :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of credentials instances. :rtype: :class:`~openstack.identity.v3.credential.Credential` @@ -116,7 +116,7 @@ def update_credential(self, credential, **attrs): :param credential: Either the ID of a credential or a :class:`~openstack.identity.v3.credential.Credential` instance. :attrs kwargs: The attributes to update on the credential represented - by ``value``. + by ``value``. :returns: The updated credential :rtype: :class:`~openstack.identity.v3.credential.Credential` @@ -127,8 +127,8 @@ def create_domain(self, **attrs): """Create a new domain from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.identity.v3.domain.Domain`, - comprised of the properties on the Domain class. + a :class:`~openstack.identity.v3.domain.Domain`, + comprised of the properties on the Domain class. :returns: The results of domain creation :rtype: :class:`~openstack.identity.v3.domain.Domain` @@ -139,12 +139,12 @@ def delete_domain(self, domain, ignore_missing=True): """Delete a domain :param domain: The value can be either the ID of a domain or a - :class:`~openstack.identity.v3.domain.Domain` instance. + :class:`~openstack.identity.v3.domain.Domain` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the domain does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent domain. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the domain does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent domain. :returns: ``None`` """ @@ -155,10 +155,10 @@ def find_domain(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a domain. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.identity.v3.domain.Domain` or None """ return self._find(_domain.Domain, name_or_id, @@ -168,11 +168,11 @@ def get_domain(self, domain): """Get a single domain :param domain: The value can be the ID of a domain or a - :class:`~openstack.identity.v3.domain.Domain` instance. + :class:`~openstack.identity.v3.domain.Domain` instance. :returns: One :class:`~openstack.identity.v3.domain.Domain` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_domain.Domain, domain) @@ -180,7 +180,7 @@ def domains(self, **query): """Retrieve a generator of domains :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of domain instances. :rtype: :class:`~openstack.identity.v3.domain.Domain` @@ -192,9 +192,9 @@ def update_domain(self, domain, **attrs): """Update a domain :param domain: Either the ID of a domain or a - :class:`~openstack.identity.v3.domain.Domain` instance. + :class:`~openstack.identity.v3.domain.Domain` instance. :attrs kwargs: The attributes to update on the domain represented - by ``value``. + by ``value``. :returns: The updated domain :rtype: :class:`~openstack.identity.v3.domain.Domain` @@ -205,8 +205,8 @@ def create_endpoint(self, **attrs): """Create a new endpoint from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.identity.v3.endpoint.Endpoint`, - comprised of the properties on the Endpoint class. + a :class:`~openstack.identity.v3.endpoint.Endpoint`, + comprised of the properties on the Endpoint class. :returns: The results of endpoint creation :rtype: :class:`~openstack.identity.v3.endpoint.Endpoint` @@ -217,12 +217,12 @@ def delete_endpoint(self, endpoint, ignore_missing=True): """Delete an endpoint :param endpoint: The value can be either the ID of an endpoint or a - :class:`~openstack.identity.v3.endpoint.Endpoint` instance. + :class:`~openstack.identity.v3.endpoint.Endpoint` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the endpoint does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent endpoint. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the endpoint does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent endpoint. :returns: ``None`` """ @@ -234,10 +234,10 @@ def find_endpoint(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a endpoint. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.identity.v3.endpoint.Endpoint` or None """ return self._find(_endpoint.Endpoint, name_or_id, @@ -247,12 +247,12 @@ def get_endpoint(self, endpoint): """Get a single endpoint :param endpoint: The value can be the ID of an endpoint or a - :class:`~openstack.identity.v3.endpoint.Endpoint` - instance. + :class:`~openstack.identity.v3.endpoint.Endpoint` + instance. :returns: One :class:`~openstack.identity.v3.endpoint.Endpoint` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_endpoint.Endpoint, endpoint) @@ -260,7 +260,7 @@ def endpoints(self, **query): """Retrieve a generator of endpoints :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of endpoint instances. :rtype: :class:`~openstack.identity.v3.endpoint.Endpoint` @@ -272,10 +272,10 @@ def update_endpoint(self, endpoint, **attrs): """Update a endpoint :param endpoint: Either the ID of a endpoint or a - :class:`~openstack.identity.v3.endpoint.Endpoint` - instance. + :class:`~openstack.identity.v3.endpoint.Endpoint` + instance. :attrs kwargs: The attributes to update on the endpoint represented - by ``value``. + by ``value``. :returns: The updated endpoint :rtype: :class:`~openstack.identity.v3.endpoint.Endpoint` @@ -286,8 +286,8 @@ def create_group(self, **attrs): """Create a new group from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.identity.v3.group.Group`, - comprised of the properties on the Group class. + a :class:`~openstack.identity.v3.group.Group`, + comprised of the properties on the Group class. :returns: The results of group creation :rtype: :class:`~openstack.identity.v3.group.Group` @@ -298,12 +298,12 @@ def delete_group(self, group, ignore_missing=True): """Delete a group :param group: The value can be either the ID of a group or a - :class:`~openstack.identity.v3.group.Group` instance. + :class:`~openstack.identity.v3.group.Group` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the group does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent group. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the group does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent group. :returns: ``None`` """ @@ -314,10 +314,10 @@ def find_group(self, name_or_id, ignore_missing=True, **kwargs): :param name_or_id: The name or ID of a group. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.identity.v3.group.Group` or None """ return self._find(_group.Group, name_or_id, @@ -328,12 +328,12 @@ def get_group(self, group): """Get a single group :param group: The value can be the ID of a group or a - :class:`~openstack.identity.v3.group.Group` - instance. + :class:`~openstack.identity.v3.group.Group` + instance. :returns: One :class:`~openstack.identity.v3.group.Group` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_group.Group, group) @@ -341,7 +341,7 @@ def groups(self, **query): """Retrieve a generator of groups :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of group instances. :rtype: :class:`~openstack.identity.v3.group.Group` @@ -353,9 +353,9 @@ def update_group(self, group, **attrs): """Update a group :param group: Either the ID of a group or a - :class:`~openstack.identity.v3.group.Group` instance. + :class:`~openstack.identity.v3.group.Group` instance. :attrs kwargs: The attributes to update on the group represented - by ``value``. + by ``value``. :returns: The updated group :rtype: :class:`~openstack.identity.v3.group.Group` @@ -405,8 +405,8 @@ def create_policy(self, **attrs): """Create a new policy from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.identity.v3.policy.Policy`, - comprised of the properties on the Policy class. + a :class:`~openstack.identity.v3.policy.Policy`, + comprised of the properties on the Policy class. :returns: The results of policy creation :rtype: :class:`~openstack.identity.v3.policy.Policy` @@ -417,12 +417,12 @@ def delete_policy(self, policy, ignore_missing=True): """Delete a policy :param policy: The value can be either the ID of a policy or a - :class:`~openstack.identity.v3.policy.Policy` instance. + :class:`~openstack.identity.v3.policy.Policy` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the policy does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent policy. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the policy does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent policy. :returns: ``None`` """ @@ -433,10 +433,10 @@ def find_policy(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a policy. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.identity.v3.policy.Policy` or None """ return self._find(_policy.Policy, name_or_id, @@ -446,11 +446,11 @@ def get_policy(self, policy): """Get a single policy :param policy: The value can be the ID of a policy or a - :class:`~openstack.identity.v3.policy.Policy` instance. + :class:`~openstack.identity.v3.policy.Policy` instance. :returns: One :class:`~openstack.identity.v3.policy.Policy` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_policy.Policy, policy) @@ -458,7 +458,7 @@ def policies(self, **query): """Retrieve a generator of policies :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of policy instances. :rtype: :class:`~openstack.identity.v3.policy.Policy` @@ -470,9 +470,9 @@ def update_policy(self, policy, **attrs): """Update a policy :param policy: Either the ID of a policy or a - :class:`~openstack.identity.v3.policy.Policy` instance. + :class:`~openstack.identity.v3.policy.Policy` instance. :attrs kwargs: The attributes to update on the policy represented - by ``value``. + by ``value``. :returns: The updated policy :rtype: :class:`~openstack.identity.v3.policy.Policy` @@ -483,8 +483,8 @@ def create_project(self, **attrs): """Create a new project from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.identity.v3.project.Project`, - comprised of the properties on the Project class. + a :class:`~openstack.identity.v3.project.Project`, + comprised of the properties on the Project class. :returns: The results of project creation :rtype: :class:`~openstack.identity.v3.project.Project` @@ -497,10 +497,10 @@ def delete_project(self, project, ignore_missing=True): :param project: The value can be either the ID of a project or a :class:`~openstack.identity.v3.project.Project` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the project does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent project. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the project does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent project. :returns: ``None`` """ @@ -511,10 +511,10 @@ def find_project(self, name_or_id, ignore_missing=True, **attrs): :param name_or_id: The name or ID of a project. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.identity.v3.project.Project` or None """ return self._find(_project.Project, name_or_id, @@ -528,7 +528,7 @@ def get_project(self, project): :returns: One :class:`~openstack.identity.v3.project.Project` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_project.Project, project) @@ -536,7 +536,7 @@ def projects(self, **query): """Retrieve a generator of projects :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of project instances. :rtype: :class:`~openstack.identity.v3.project.Project` @@ -549,9 +549,9 @@ def user_projects(self, user, **query): to access. :param user: Either the user id or an instance of - :class:`~openstack.identity.v3.user.User` + :class:`~openstack.identity.v3.user.User` :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of project instances. :rtype: :class:`~openstack.identity.v3.project.UserProject` @@ -565,7 +565,7 @@ def update_project(self, project, **attrs): :param project: Either the ID of a project or a :class:`~openstack.identity.v3.project.Project` instance. :attrs kwargs: The attributes to update on the project represented - by ``value``. + by ``value``. :returns: The updated project :rtype: :class:`~openstack.identity.v3.project.Project` @@ -576,8 +576,8 @@ def create_service(self, **attrs): """Create a new service from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.identity.v3.service.Service`, - comprised of the properties on the Service class. + a :class:`~openstack.identity.v3.service.Service`, + comprised of the properties on the Service class. :returns: The results of service creation :rtype: :class:`~openstack.identity.v3.service.Service` @@ -590,10 +590,10 @@ def delete_service(self, service, ignore_missing=True): :param service: The value can be either the ID of a service or a :class:`~openstack.identity.v3.service.Service` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the service does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent service. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the service does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent service. :returns: ``None`` """ @@ -604,10 +604,10 @@ def find_service(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a service. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.identity.v3.service.Service` or None """ return self._find(_service.Service, name_or_id, @@ -621,7 +621,7 @@ def get_service(self, service): :returns: One :class:`~openstack.identity.v3.service.Service` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_service.Service, service) @@ -629,7 +629,7 @@ def services(self, **query): """Retrieve a generator of services :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of service instances. :rtype: :class:`~openstack.identity.v3.service.Service` @@ -643,7 +643,7 @@ def update_service(self, service, **attrs): :param service: Either the ID of a service or a :class:`~openstack.identity.v3.service.Service` instance. :attrs kwargs: The attributes to update on the service represented - by ``value``. + by ``value``. :returns: The updated service :rtype: :class:`~openstack.identity.v3.service.Service` @@ -654,8 +654,8 @@ def create_user(self, **attrs): """Create a new user from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.identity.v3.user.User`, - comprised of the properties on the User class. + a :class:`~openstack.identity.v3.user.User`, + comprised of the properties on the User class. :returns: The results of user creation :rtype: :class:`~openstack.identity.v3.user.User` @@ -666,12 +666,12 @@ def delete_user(self, user, ignore_missing=True): """Delete a user :param user: The value can be either the ID of a user or a - :class:`~openstack.identity.v3.user.User` instance. + :class:`~openstack.identity.v3.user.User` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the user does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent user. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the user does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent user. :returns: ``None`` """ @@ -682,10 +682,10 @@ def find_user(self, name_or_id, ignore_missing=True, **attrs): :param name_or_id: The name or ID of a user. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.identity.v3.user.User` or None """ return self._find(_user.User, name_or_id, @@ -695,11 +695,11 @@ def get_user(self, user): """Get a single user :param user: The value can be the ID of a user or a - :class:`~openstack.identity.v3.user.User` instance. + :class:`~openstack.identity.v3.user.User` instance. :returns: One :class:`~openstack.identity.v3.user.User` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_user.User, user) @@ -707,7 +707,7 @@ def users(self, **query): """Retrieve a generator of users :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of user instances. :rtype: :class:`~openstack.identity.v3.user.User` @@ -719,9 +719,9 @@ def update_user(self, user, **attrs): """Update a user :param user: Either the ID of a user or a - :class:`~openstack.identity.v3.user.User` instance. + :class:`~openstack.identity.v3.user.User` instance. :attrs kwargs: The attributes to update on the user represented - by ``value``. + by ``value``. :returns: The updated user :rtype: :class:`~openstack.identity.v3.user.User` @@ -732,8 +732,8 @@ def create_trust(self, **attrs): """Create a new trust from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.identity.v3.trust.Trust`, - comprised of the properties on the Trust class. + a :class:`~openstack.identity.v3.trust.Trust`, + comprised of the properties on the Trust class. :returns: The results of trust creation :rtype: :class:`~openstack.identity.v3.trust.Trust` @@ -744,12 +744,12 @@ def delete_trust(self, trust, ignore_missing=True): """Delete a trust :param trust: The value can be either the ID of a trust or a - :class:`~openstack.identity.v3.trust.Trust` instance. + :class:`~openstack.identity.v3.trust.Trust` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the credential does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent credential. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the credential does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent credential. :returns: ``None`` """ @@ -760,10 +760,10 @@ def find_trust(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a trust. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.identity.v3.trust.Trust` or None """ return self._find(_trust.Trust, name_or_id, @@ -773,11 +773,11 @@ def get_trust(self, trust): """Get a single trust :param trust: The value can be the ID of a trust or a - :class:`~openstack.identity.v3.trust.Trust` instance. + :class:`~openstack.identity.v3.trust.Trust` instance. :returns: One :class:`~openstack.identity.v3.trust.Trust` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_trust.Trust, trust) @@ -785,7 +785,7 @@ def trusts(self, **query): """Retrieve a generator of trusts :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of trust instances. :rtype: :class:`~openstack.identity.v3.trust.Trust` @@ -797,8 +797,8 @@ def create_region(self, **attrs): """Create a new region from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.identity.v3.region.Region`, - comprised of the properties on the Region class. + a :class:`~openstack.identity.v3.region.Region`, + comprised of the properties on the Region class. :returns: The results of region creation. :rtype: :class:`~openstack.identity.v3.region.Region` @@ -809,12 +809,12 @@ def delete_region(self, region, ignore_missing=True): """Delete a region :param region: The value can be either the ID of a region or a - :class:`~openstack.identity.v3.region.Region` instance. + :class:`~openstack.identity.v3.region.Region` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the region does not exist. - When set to ``True``, no exception will be thrown when - attempting to delete a nonexistent region. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the region does not exist. + When set to ``True``, no exception will be thrown when + attempting to delete a nonexistent region. :returns: ``None`` """ @@ -825,10 +825,10 @@ def find_region(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a region. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the region does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent region. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the region does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent region. :returns: One :class:`~openstack.identity.v3.region.Region` or None """ return self._find(_region.Region, name_or_id, @@ -838,11 +838,11 @@ def get_region(self, region): """Get a single region :param region: The value can be the ID of a region or a - :class:`~openstack.identity.v3.region.Region` instance. + :class:`~openstack.identity.v3.region.Region` instance. :returns: One :class:`~openstack.identity.v3.region.Region` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no matching region can be found. + when no matching region can be found. """ return self._get(_region.Region, region) @@ -850,7 +850,7 @@ def regions(self, **query): """Retrieve a generator of regions :param kwargs query: Optional query parameters to be sent to limit - the regions being returned. + the regions being returned. :returns: A generator of region instances. :rtype: :class:`~openstack.identity.v3.region.Region` @@ -862,9 +862,9 @@ def update_region(self, region, **attrs): """Update a region :param region: Either the ID of a region or a - :class:`~openstack.identity.v3.region.Region` instance. + :class:`~openstack.identity.v3.region.Region` instance. :attrs kwargs: The attributes to update on the region represented - by ``value``. + by ``value``. :returns: The updated region. :rtype: :class:`~openstack.identity.v3.region.Region` @@ -875,8 +875,8 @@ def create_role(self, **attrs): """Create a new role from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.identity.v3.role.Role`, - comprised of the properties on the Role class. + a :class:`~openstack.identity.v3.role.Role`, + comprised of the properties on the Role class. :returns: The results of role creation. :rtype: :class:`~openstack.identity.v3.role.Role` @@ -887,12 +887,12 @@ def delete_role(self, role, ignore_missing=True): """Delete a role :param role: The value can be either the ID of a role or a - :class:`~openstack.identity.v3.role.Role` instance. + :class:`~openstack.identity.v3.role.Role` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the role does not exist. - When set to ``True``, no exception will be thrown when - attempting to delete a nonexistent role. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the role does not exist. + When set to ``True``, no exception will be thrown when + attempting to delete a nonexistent role. :returns: ``None`` """ @@ -903,10 +903,10 @@ def find_role(self, name_or_id, ignore_missing=True, **kwargs): :param name_or_id: The name or ID of a role. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the role does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent role. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the role does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent role. :returns: One :class:`~openstack.identity.v3.role.Role` or None """ return self._find(_role.Role, name_or_id, @@ -917,11 +917,11 @@ def get_role(self, role): """Get a single role :param role: The value can be the ID of a role or a - :class:`~openstack.identity.v3.role.Role` instance. + :class:`~openstack.identity.v3.role.Role` instance. :returns: One :class:`~openstack.identity.v3.role.Role` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no matching role can be found. + when no matching role can be found. """ return self._get(_role.Role, role) @@ -929,8 +929,8 @@ def roles(self, **query): """Retrieve a generator of roles :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. The options - are: domain_id, name. + the resources being returned. The options + are: domain_id, name. :return: A generator of role instances. :rtype: :class:`~openstack.identity.v3.role.Role` """ @@ -940,9 +940,9 @@ def update_role(self, role, **attrs): """Update a role :param role: Either the ID of a role or a - :class:`~openstack.identity.v3.role.Role` instance. + :class:`~openstack.identity.v3.role.Role` instance. :param dict kwargs: The attributes to update on the role represented - by ``value``. Only name can be updated + by ``value``. Only name can be updated :returns: The updated role. :rtype: :class:`~openstack.identity.v3.role.Role` @@ -954,14 +954,14 @@ def role_assignments_filter(self, domain=None, project=None, group=None, """Retrieve a generator of roles assigned to user/group :param domain: Either the ID of a domain or a - :class:`~openstack.identity.v3.domain.Domain` instance. + :class:`~openstack.identity.v3.domain.Domain` instance. :param project: Either the ID of a project or a - :class:`~openstack.identity.v3.project.Project` - instance. + :class:`~openstack.identity.v3.project.Project` + instance. :param group: Either the ID of a group or a - :class:`~openstack.identity.v3.group.Group` instance. + :class:`~openstack.identity.v3.group.Group` instance. :param user: Either the ID of a user or a - :class:`~openstack.identity.v3.user.User` instance. + :class:`~openstack.identity.v3.user.User` instance. :return: A generator of role instances. :rtype: :class:`~openstack.identity.v3.role.Role` """ @@ -1010,12 +1010,12 @@ def role_assignments(self, **query): """Retrieve a generator of role assignments :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. The options - are: group_id, role_id, scope_domain_id, - scope_project_id, user_id, include_names, - include_subtree. + the resources being returned. The options + are: group_id, role_id, scope_domain_id, + scope_project_id, user_id, include_names, + include_subtree. :return: - :class:`~openstack.identity.v3.role_assignment.RoleAssignment` + :class:`~openstack.identity.v3.role_assignment.RoleAssignment` """ return self._list(_role_assignment.RoleAssignment, **query) @@ -1039,9 +1039,9 @@ def get_registered_limit(self, registered_limit): `~openstack.identity.v3.registered_limit.RegisteredLimit` instance. :returns: One :class: - `~openstack.identity.v3.registered_limit.RegisteredLimit` + `~openstack.identity.v3.registered_limit.RegisteredLimit` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_registered_limit.RegisteredLimit, registered_limit) @@ -1110,7 +1110,7 @@ def get_limit(self, limit): or a :class:`~openstack.identity.v3.limit.Limit` instance. :returns: One :class: - `~openstack.identity.v3.limit.Limit` + `~openstack.identity.v3.limit.Limit` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ @@ -1257,12 +1257,12 @@ def assign_project_role_to_user(self, project, user, role): """Assign role to user on a project :param project: Either the ID of a project or a - :class:`~openstack.identity.v3.project.Project` - instance. + :class:`~openstack.identity.v3.project.Project` + instance. :param user: Either the ID of a user or a - :class:`~openstack.identity.v3.user.User` instance. + :class:`~openstack.identity.v3.user.User` instance. :param role: Either the ID of a role or a - :class:`~openstack.identity.v3.role.Role` instance. + :class:`~openstack.identity.v3.role.Role` instance. :return: ``None`` """ project = self._get_resource(_project.Project, project) @@ -1274,12 +1274,12 @@ def unassign_project_role_from_user(self, project, user, role): """Unassign role from user on a project :param project: Either the ID of a project or a - :class:`~openstack.identity.v3.project.Project` - instance. + :class:`~openstack.identity.v3.project.Project` + instance. :param user: Either the ID of a user or a - :class:`~openstack.identity.v3.user.User` instance. + :class:`~openstack.identity.v3.user.User` instance. :param role: Either the ID of a role or a - :class:`~openstack.identity.v3.role.Role` instance. + :class:`~openstack.identity.v3.role.Role` instance. :return: ``None`` """ project = self._get_resource(_project.Project, project) @@ -1291,12 +1291,12 @@ def validate_user_has_project_role(self, project, user, role): """Validates that a user has a role on a project :param project: Either the ID of a project or a - :class:`~openstack.identity.v3.project.Project` - instance. + :class:`~openstack.identity.v3.project.Project` + instance. :param user: Either the ID of a user or a - :class:`~openstack.identity.v3.user.User` instance. + :class:`~openstack.identity.v3.user.User` instance. :param role: Either the ID of a role or a - :class:`~openstack.identity.v3.role.Role` instance. + :class:`~openstack.identity.v3.role.Role` instance. :returns: True if user has role in project """ project = self._get_resource(_project.Project, project) @@ -1308,12 +1308,12 @@ def assign_project_role_to_group(self, project, group, role): """Assign role to group on a project :param project: Either the ID of a project or a - :class:`~openstack.identity.v3.project.Project` - instance. + :class:`~openstack.identity.v3.project.Project` + instance. :param group: Either the ID of a group or a - :class:`~openstack.identity.v3.group.Group` instance. + :class:`~openstack.identity.v3.group.Group` instance. :param role: Either the ID of a role or a - :class:`~openstack.identity.v3.role.Role` instance. + :class:`~openstack.identity.v3.role.Role` instance. :return: ``None`` """ project = self._get_resource(_project.Project, project) @@ -1325,12 +1325,12 @@ def unassign_project_role_from_group(self, project, group, role): """Unassign role from group on a project :param project: Either the ID of a project or a - :class:`~openstack.identity.v3.project.Project` - instance. + :class:`~openstack.identity.v3.project.Project` + instance. :param group: Either the ID of a group or a - :class:`~openstack.identity.v3.group.Group` instance. + :class:`~openstack.identity.v3.group.Group` instance. :param role: Either the ID of a role or a - :class:`~openstack.identity.v3.role.Role` instance. + :class:`~openstack.identity.v3.role.Role` instance. :return: ``None`` """ project = self._get_resource(_project.Project, project) @@ -1342,12 +1342,12 @@ def validate_group_has_project_role(self, project, group, role): """Validates that a group has a role on a project :param project: Either the ID of a project or a - :class:`~openstack.identity.v3.project.Project` - instance. + :class:`~openstack.identity.v3.project.Project` + instance. :param group: Either the ID of a group or a - :class:`~openstack.identity.v3.group.Group` instance. + :class:`~openstack.identity.v3.group.Group` instance. :param role: Either the ID of a role or a - :class:`~openstack.identity.v3.role.Role` instance. + :class:`~openstack.identity.v3.role.Role` instance. :returns: True if group has role in project """ project = self._get_resource(_project.Project, project) @@ -1359,14 +1359,14 @@ def application_credentials(self, user, **query): """Retrieve a generator of application credentials :param user: Either the ID of a user or a - :class:`~openstack.identity.v3.user.User` instance. + :class:`~openstack.identity.v3.user.User` instance. :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of application credentials instances. :rtype: :class:`~openstack.identity.v3.application_credential. - ApplicationCredential` + ApplicationCredential` """ user = self._get_resource(_user.User, user) return self._list(_application_credential.ApplicationCredential, @@ -1376,17 +1376,17 @@ def get_application_credential(self, user, application_credential): """Get a single application credential :param user: Either the ID of a user or a - :class:`~openstack.identity.v3.user.User` instance. + :class:`~openstack.identity.v3.user.User` instance. :param application_credential: The value can be the ID of a - application credential or a :class: - `~openstack.identity.v3.application_credential. - ApplicationCredential` instance. + application credential or a :class: + `~openstack.identity.v3.application_credential. + ApplicationCredential` instance. :returns: One :class:`~openstack.identity.v3.application_credential. - ApplicationCredential` + ApplicationCredential` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no - resource can be found. + resource can be found. """ user = self._get_resource(_user.User, user) return self._get(_application_credential.ApplicationCredential, @@ -1397,18 +1397,18 @@ def create_application_credential(self, user, name, **attrs): """Create a new application credential from attributes :param user: Either the ID of a user or a - :class:`~openstack.identity.v3.user.User` instance. + :class:`~openstack.identity.v3.user.User` instance. :param name: The name of the application credential which is - unique to the user. + unique to the user. :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.identity.v3.application_credential. - ApplicationCredential`, comprised of the properties on the - ApplicationCredential class. + a :class:`~openstack.identity.v3.application_credential. + ApplicationCredential`, comprised of the properties on the + ApplicationCredential class. :returns: The results of application credential creation. :rtype: :class:`~openstack.identity.v3.application_credential. - ApplicationCredential` + ApplicationCredential` """ user = self._get_resource(_user.User, user) @@ -1421,16 +1421,16 @@ def find_application_credential(self, user, name_or_id, """Find a single application credential :param user: Either the ID of a user or a - :class:`~openstack.identity.v3.user.User` instance. + :class:`~openstack.identity.v3.user.User` instance. :param name_or_id: The name or ID of an application credential. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.identity.v3.application_credential. - ApplicationCredential` or None + ApplicationCredential` or None """ user = self._get_resource(_user.User, user) return self._find(_application_credential.ApplicationCredential, @@ -1442,15 +1442,15 @@ def delete_application_credential(self, user, application_credential, """Delete an application credential :param user: Either the ID of a user or a - :class:`~openstack.identity.v3.user.User` instance. + :class:`~openstack.identity.v3.user.User` instance. :param application credential: The value can be either the ID of a - application credential or a :class: `~openstack.identity.v3. - application_credential.ApplicationCredential` instance. + application credential or a :class: `~openstack.identity.v3. + application_credential.ApplicationCredential` instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised - when the application credential does not exist. When set to - ``True``, no exception will be thrown when attempting to delete - a nonexistent application credential. + when the application credential does not exist. When set to + ``True``, no exception will be thrown when attempting to delete + a nonexistent application credential. :returns: ``None`` """ @@ -1611,8 +1611,8 @@ def create_mapping(self, **attrs): """Create a new mapping from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.identity.v3.mapping.Mapping`, - comprised of the properties on the Mapping class. + a :class:`~openstack.identity.v3.mapping.Mapping`, + comprised of the properties on the Mapping class. :returns: The results of mapping creation :rtype: :class:`~openstack.identity.v3.mapping.Mapping` @@ -1623,13 +1623,13 @@ def delete_mapping(self, mapping, ignore_missing=True): """Delete a mapping :param mapping: The ID of a mapping or a - :class:`~openstack.identity.v3.mapping.Mapping` - instance. + :class:`~openstack.identity.v3.mapping.Mapping` + instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the mapping does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent mapping. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the mapping does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent mapping. :returns: ``None`` """ @@ -1640,10 +1640,10 @@ def find_mapping(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a mapping. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.identity.v3.mapping.Mapping` or None """ return self._find(_mapping.Mapping, name_or_id, @@ -1653,12 +1653,12 @@ def get_mapping(self, mapping): """Get a single mapping :param mapping: The value can be the ID of a mapping or a - :class:`~openstack.identity.v3.mapping.Mapping` - instance. + :class:`~openstack.identity.v3.mapping.Mapping` + instance. :returns: One :class:`~openstack.identity.v3.mapping.Mapping` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_mapping.Mapping, mapping) @@ -1666,7 +1666,7 @@ def mappings(self, **query): """Retrieve a generator of mappings :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of mapping instances. :rtype: :class:`~openstack.identity.v3.mapping.Mapping` @@ -1677,10 +1677,10 @@ def update_mapping(self, mapping, **attrs): """Update a mapping :param mapping: Either the ID of a mapping or a - :class:`~openstack.identity.v3.mapping.Mapping` - instance. + :class:`~openstack.identity.v3.mapping.Mapping` + instance. :attrs kwargs: The attributes to update on the mapping represented - by ``value``. + by ``value``. :returns: The updated mapping :rtype: :class:`~openstack.identity.v3.mapping.Mapping` From c746567a67ee2780d3875f5ecd44ad8031e135d3 Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Mon, 18 Oct 2021 12:20:36 +0200 Subject: [PATCH 2932/3836] Reindentation of the docstrings Change-Id: Id4ed39b3bef56b8bf55dbb9d3d72b1ba747c0898 --- openstack/key_manager/v1/_proxy.py | 118 ++++++++++++++--------------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/openstack/key_manager/v1/_proxy.py b/openstack/key_manager/v1/_proxy.py index 222ad37be..8ca13e854 100644 --- a/openstack/key_manager/v1/_proxy.py +++ b/openstack/key_manager/v1/_proxy.py @@ -22,8 +22,8 @@ def create_container(self, **attrs): """Create a new container from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.key_manager.v1.container.Container`, - comprised of the properties on the Container class. + a :class:`~openstack.key_manager.v1.container.Container`, + comprised of the properties on the Container class. :returns: The results of container creation :rtype: :class:`~openstack.key_manager.v1.container.Container` @@ -34,13 +34,13 @@ def delete_container(self, container, ignore_missing=True): """Delete a container :param container: The value can be either the ID of a container or a - :class:`~openstack.key_manager.v1.container.Container` - instance. + :class:`~openstack.key_manager.v1.container.Container` + instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the container does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent container. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the container does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent container. :returns: ``None`` """ @@ -52,12 +52,12 @@ def find_container(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a container. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.key_manager.v1.container.Container` - or None + or None """ return self._find(_container.Container, name_or_id, ignore_missing=ignore_missing) @@ -66,12 +66,12 @@ def get_container(self, container): """Get a single container :param container: The value can be the ID of a container or a - :class:`~openstack.key_manager.v1.container.Container` - instance. + :class:`~openstack.key_manager.v1.container.Container` + instance. :returns: One :class:`~openstack.key_manager.v1.container.Container` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_container.Container, container) @@ -79,7 +79,7 @@ def containers(self, **query): """Return a generator of containers :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of container objects :rtype: :class:`~openstack.key_manager.v1.container.Container` @@ -90,10 +90,10 @@ def update_container(self, container, **attrs): """Update a container :param container: Either the id of a container or a - :class:`~openstack.key_manager.v1.container.Container` - instance. + :class:`~openstack.key_manager.v1.container.Container` + instance. :attrs kwargs: The attributes to update on the container represented - by ``value``. + by ``value``. :returns: The updated container :rtype: :class:`~openstack.key_manager.v1.container.Container` @@ -104,8 +104,8 @@ def create_order(self, **attrs): """Create a new order from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.key_manager.v1.order.Order`, - comprised of the properties on the Order class. + a :class:`~openstack.key_manager.v1.order.Order`, + comprised of the properties on the Order class. :returns: The results of order creation :rtype: :class:`~openstack.key_manager.v1.order.Order` @@ -116,13 +116,13 @@ def delete_order(self, order, ignore_missing=True): """Delete an order :param order: The value can be either the ID of a order or a - :class:`~openstack.key_manager.v1.order.Order` - instance. + :class:`~openstack.key_manager.v1.order.Order` + instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the order does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent order. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the order does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent order. :returns: ``None`` """ @@ -133,10 +133,10 @@ def find_order(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a order. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.key_manager.v1.order.Order` or None """ return self._find(_order.Order, name_or_id, @@ -146,12 +146,12 @@ def get_order(self, order): """Get a single order :param order: The value can be the ID of an order or a - :class:`~openstack.key_manager.v1.order.Order` - instance. + :class:`~openstack.key_manager.v1.order.Order` + instance. :returns: One :class:`~openstack.key_manager.v1.order.Order` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_order.Order, order) @@ -159,7 +159,7 @@ def orders(self, **query): """Return a generator of orders :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of order objects :rtype: :class:`~openstack.key_manager.v1.order.Order` @@ -170,10 +170,10 @@ def update_order(self, order, **attrs): """Update a order :param order: Either the id of a order or a - :class:`~openstack.key_manager.v1.order.Order` - instance. + :class:`~openstack.key_manager.v1.order.Order` + instance. :attrs kwargs: The attributes to update on the order represented - by ``value``. + by ``value``. :returns: The updated order :rtype: :class:`~openstack.key_manager.v1.order.Order` @@ -184,8 +184,8 @@ def create_secret(self, **attrs): """Create a new secret from attributes :param dict attrs: Keyword arguments which will be used to create a - :class:`~openstack.key_manager.v1.secret.Secret`, - comprised of the properties on the Order class. + :class:`~openstack.key_manager.v1.secret.Secret`, + comprised of the properties on the Order class. :returns: The results of secret creation :rtype: :class:`~openstack.key_manager.v1.secret.Secret` @@ -196,13 +196,13 @@ def delete_secret(self, secret, ignore_missing=True): """Delete a secret :param secret: The value can be either the ID of a secret or a - :class:`~openstack.key_manager.v1.secret.Secret` - instance. + :class:`~openstack.key_manager.v1.secret.Secret` + instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the secret does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent secret. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the secret does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent secret. :returns: ``None`` """ @@ -213,12 +213,12 @@ def find_secret(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a secret. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.key_manager.v1.secret.Secret` or - None + None """ return self._find(_secret.Secret, name_or_id, ignore_missing=ignore_missing) @@ -227,12 +227,12 @@ def get_secret(self, secret): """Get a single secret :param secret: The value can be the ID of a secret or a - :class:`~openstack.key_manager.v1.secret.Secret` - instance. + :class:`~openstack.key_manager.v1.secret.Secret` + instance. :returns: One :class:`~openstack.key_manager.v1.secret.Secret` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_secret.Secret, secret) @@ -240,7 +240,7 @@ def secrets(self, **query): """Return a generator of secrets :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of secret objects :rtype: :class:`~openstack.key_manager.v1.secret.Secret` @@ -251,10 +251,10 @@ def update_secret(self, secret, **attrs): """Update a secret :param secret: Either the id of a secret or a - :class:`~openstack.key_manager.v1.secret.Secret` - instance. + :class:`~openstack.key_manager.v1.secret.Secret` + instance. :attrs kwargs: The attributes to update on the secret represented - by ``value``. + by ``value``. :returns: The updated secret :rtype: :class:`~openstack.key_manager.v1.secret.Secret` From 4581db6c2fdd1d3d386c6ed5dc5fbc66810ae4e9 Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Mon, 18 Oct 2021 10:47:30 +0200 Subject: [PATCH 2933/3836] Reindentation of the docstrings Change-Id: I58b0885639069116298898a29548f1373d4e687c --- openstack/accelerator/v2/_proxy.py | 36 +++++++++++++++++------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/openstack/accelerator/v2/_proxy.py b/openstack/accelerator/v2/_proxy.py index 5f54b631b..dcb44cc84 100644 --- a/openstack/accelerator/v2/_proxy.py +++ b/openstack/accelerator/v2/_proxy.py @@ -53,19 +53,20 @@ def devices(self, **query): :param kwargs query: Optional query parameters to be sent to restrict the devices to be returned. Available parameters include: + * hostname: The hostname of the device. * type: The type of the device. * vendor: The vendor ID of the device. * sort: A list of sorting keys separated by commas. Each sorting - key can optionally be attached with a sorting direction - modifier which can be ``asc`` or ``desc``. + key can optionally be attached with a sorting direction + modifier which can be ``asc`` or ``desc``. * limit: Requests a specified size of returned items from the - query. Returns a number of items up to the specified limit - value. + query. Returns a number of items up to the specified limit + value. * marker: Specifies the ID of the last-seen item. Use the limit - parameter to make an initial limited request and use the ID of - the last-seen item from the response as the marker parameter - value in a subsequent limited request. + parameter to make an initial limited request and use the ID of + the last-seen item from the response as the marker parameter + value in a subsequent limited request. :returns: A generator of device instances. """ return self._list(_device.Device, **query) @@ -141,14 +142,15 @@ def create_accelerator_request(self, **attrs): return self._create(_arq.AcceleratorRequest, **attrs) def delete_accelerator_request(self, name_or_id, ignore_missing=True): - """Delete a device profile + """Delete a device profile. + :param name_or_id: The value can be either the ID or name of - an accelerator request. + an accelerator request. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the device profile does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent accelerator request. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the device profile does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent accelerator request. :returns: ``None`` """ return self._delete(_arq.AcceleratorRequest, name_or_id, @@ -156,19 +158,21 @@ def delete_accelerator_request(self, name_or_id, ignore_missing=True): def get_accelerator_request(self, uuid, fields=None): """Get a single accelerator request. + :param uuid: The value can be the UUID of a accelerator request. :returns: One :class: - `~openstack.accelerator.v2.accelerator_request.AcceleratorRequest` + `~openstack.accelerator.v2.accelerator_request.AcceleratorRequest` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no - accelerator request matching the criteria could be found. + accelerator request matching the criteria could be found. """ return self._get(_arq.AcceleratorRequest, uuid) def update_accelerator_request(self, uuid, properties): """Bind/Unbind an accelerator to VM. + :param uuid: The uuid of the accelerator_request to be bound/unbound. :param properties: The info of VM - that will bind/unbind the accelerator. + that will bind/unbind the accelerator. :returns: True if bind/unbind succeeded, False otherwise. """ return self._get_resource(_arq.AcceleratorRequest, From 9d143ed81811aa2df0aadb748ced953e8a88b378 Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Wed, 29 Sep 2021 10:42:08 +0200 Subject: [PATCH 2934/3836] Reindentation of the docstrings Change-Id: I227d0ea4870abc2cc4f4a9412cb17732d1ba5feb --- openstack/block_storage/v3/_proxy.py | 111 +++++++++++++-------------- 1 file changed, 55 insertions(+), 56 deletions(-) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index fef24f6d4..bec92cf74 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -35,12 +35,12 @@ def get_snapshot(self, snapshot): """Get a single snapshot :param snapshot: The value can be the ID of a snapshot or a - :class:`~openstack.block_storage.v3.snapshot.Snapshot` - instance. + :class:`~openstack.block_storage.v3.snapshot.Snapshot` + instance. :returns: One :class:`~openstack.block_storage.v3.snapshot.Snapshot` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_snapshot.Snapshot, snapshot) @@ -54,7 +54,7 @@ def find_snapshot(self, name_or_id, ignore_missing=True, **attrs): :returns: One :class:`~openstack.block_storage.v3.snapshot.Snapshot` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._find(_snapshot.Snapshot, name_or_id, ignore_missing=ignore_missing) @@ -74,7 +74,7 @@ def snapshots(self, details=True, **query): * project_id: Filter the snapshots by project. * volume_id: volume id of a snapshot. * status: Value of the status of the snapshot so that you can - filter on "available" for example. + filter on "available" for example. :returns: A generator of snapshot objects. """ @@ -145,11 +145,11 @@ def get_type(self, type): """Get a single type :param type: The value can be the ID of a type or a - :class:`~openstack.block_storage.v3.type.Type` instance. + :class:`~openstack.block_storage.v3.type.Type` instance. :returns: One :class:`~openstack.block_storage.v3.type.Type` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_type.Type, type) @@ -163,7 +163,7 @@ def find_type(self, name_or_id, ignore_missing=True, **attrs): :returns: One :class:`~openstack.block_storage.v3.type.Type` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._find(_type.Type, name_or_id, ignore_missing=ignore_missing) @@ -179,8 +179,8 @@ def create_type(self, **attrs): """Create a new type from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.block_storage.v3.type.Type`, - comprised of the properties on the Type class. + a :class:`~openstack.block_storage.v3.type.Type`, + comprised of the properties on the Type class. :returns: The results of type creation :rtype: :class:`~openstack.block_storage.v3.type.Type` @@ -191,12 +191,12 @@ def delete_type(self, type, ignore_missing=True): """Delete a type :param type: The value can be either the ID of a type or a - :class:`~openstack.block_storage.v3.type.Type` instance. + :class:`~openstack.block_storage.v3.type.Type` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the type does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent type. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the type does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent type. :returns: ``None`` """ @@ -206,9 +206,9 @@ def update_type(self, type, **attrs): """Update a type :param type: The value can be either the ID of a type or a - :class:`~openstack.block_storage.v3.type.Type` instance. + :class:`~openstack.block_storage.v3.type.Type` instance. :param dict attrs: The attributes to update on the type - represented by ``value``. + represented by ``value``. :returns: The updated type :rtype: :class:`~openstack.block_storage.v3.type.Type` @@ -219,12 +219,11 @@ def update_type_extra_specs(self, type, **attrs): """Update the extra_specs for a type :param type: The value can be either the ID of a type or a - :class:`~openstack.block_storage.v3.type.Type` instance. + :class:`~openstack.block_storage.v3.type.Type` instance. :param dict attrs: The extra_spec attributes to update on the - type represented by ``value``. + type represented by ``value``. :returns: A dict containing updated extra_specs - """ res = self._get_resource(_type.Type, type) extra_specs = res.set_extra_specs(self, **attrs) @@ -237,7 +236,7 @@ def delete_type_extra_specs(self, type, keys): Note: This method will do a HTTP DELETE request for every key in keys. :param type: The value can be either the ID of a type or a - :class:`~openstack.block_storage.v3.type.Type` instance. + :class:`~openstack.block_storage.v3.type.Type` instance. :param keys: The keys to delete :returns: ``None`` @@ -287,12 +286,12 @@ def get_type_encryption(self, volume_type_id): """Get the encryption details of a volume type :param volume_type_id: The value can be the ID of a type or a - :class:`~openstack.block_storage.v3.type.Type` - instance. + :class:`~openstack.block_storage.v3.type.Type` + instance. :returns: One :class:`~openstack.block_storage.v3.type.TypeEncryption` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ volume_type = self._get_resource(_type.Type, volume_type_id) @@ -304,8 +303,8 @@ def create_type_encryption(self, volume_type, **attrs): """Create new type encryption from attributes :param volume_type: The value can be the ID of a type or a - :class:`~openstack.block_storage.v3.type.Type` - instance. + :class:`~openstack.block_storage.v3.type.Type` + instance. :param dict attrs: Keyword arguments which will be used to create a :class:`~openstack.block_storage.v3.type.TypeEncryption`, @@ -324,19 +323,19 @@ def delete_type_encryption(self, encryption=None, """Delete type encryption attributes :param encryption: The value can be None or a - :class:`~openstack.block_storage.v3.type.TypeEncryption` - instance. If encryption_id is None then - volume_type_id must be specified. + :class:`~openstack.block_storage.v3.type.TypeEncryption` + instance. If encryption_id is None then + volume_type_id must be specified. :param volume_type: The value can be the ID of a type or a - :class:`~openstack.block_storage.v3.type.Type` - instance. Required if encryption_id is None. + :class:`~openstack.block_storage.v3.type.Type` + instance. Required if encryption_id is None. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the type does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent type. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the type does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent type. :returns: ``None`` """ @@ -354,13 +353,13 @@ def update_type_encryption(self, encryption=None, volume_type=None, **attrs): """Update a type :param encryption: The value can be None or a - :class:`~openstack.block_storage.v3.type.TypeEncryption` - instance. If encryption_id is None then - volume_type_id must be specified. + :class:`~openstack.block_storage.v3.type.TypeEncryption` + instance. If encryption_id is None then + volume_type_id must be specified. :param volume_type: The value can be the ID of a type or a - :class:`~openstack.block_storage.v3.type.Type` - instance. Required if encryption_id is None. + :class:`~openstack.block_storage.v3.type.Type` + instance. Required if encryption_id is None. :param dict attrs: The attributes to update on the type encryption. :returns: The updated type encryption @@ -398,7 +397,7 @@ def find_volume(self, name_or_id, ignore_missing=True, **attrs): :returns: One :class:`~openstack.block_storage.v3.volume.Volume` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._find(_volume.Volume, name_or_id, ignore_missing=ignore_missing) @@ -415,7 +414,7 @@ def volumes(self, details=True, **query): * name: Name of the volume as a string. * all_projects: Whether return the volumes in all projects * status: Value of the status of the volume so that you can filter - on "available" for example. + on "available" for example. :returns: A generator of volume objects. """ @@ -715,7 +714,8 @@ def terminate_volume_attachment(self, volume, connector): :class:`~openstack.block_storage.v3.volume.Volume` instance. :param dict connector: The connector object. - :returns: None """ + :returns: None + """ volume = self._get_resource(_volume.Volume, volume) volume.terminate_attachment(self, connector) @@ -779,7 +779,7 @@ def find_backup(self, name_or_id, ignore_missing=True, **attrs): :returns: One :class:`~openstack.block_storage.v3.backup.Backup` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._find(_backup.Backup, name_or_id, ignore_missing=ignore_missing) @@ -872,7 +872,7 @@ def availability_zones(self): :returns: A generator of availability zone :rtype: :class:`~openstack.block_storage.v3.availability_zone.\ - AvailabilityZone` + AvailabilityZone` """ return self._list(availability_zone.AvailabilityZone) @@ -977,22 +977,22 @@ def wait_for_status(self, res, status='available', failures=None, """Wait for a resource to be in a particular status. :param res: The resource to wait on to reach the specified status. - The resource must have a ``status`` attribute. + The resource must have a ``status`` attribute. :type resource: A :class:`~openstack.resource.Resource` object. :param status: Desired status. :param failures: Statuses that would be interpreted as failures. :type failures: :py:class:`list` :param interval: Number of seconds to wait before to consecutive - checks. Default to 2. + checks. Default to 2. :param wait: Maximum number of seconds to wait before the change. - Default to 120. + Default to 120. :returns: The resource is returned on success. :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition - to the desired status failed to occur in specified seconds. + to the desired status failed to occur in specified seconds. :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource - has transited to one of the failure statuses. + has transited to one of the failure statuses. :raises: :class:`~AttributeError` if the resource does not have a - ``status`` attribute. + ``status`` attribute. """ failures = ['error'] if failures is None else failures return resource.wait_for_status( @@ -1004,12 +1004,12 @@ def wait_for_delete(self, res, interval=2, wait=120): :param res: The resource to wait on to be deleted. :type resource: A :class:`~openstack.resource.Resource` object. :param interval: Number of seconds to wait before to consecutive - checks. Default to 2. + checks. Default to 2. :param wait: Maximum number of seconds to wait before the change. - Default to 120. + Default to 120. :returns: The resource is returned on success. :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition - to delete failed to occur in the specified seconds. + to delete failed to occur in the specified seconds. """ return resource.wait_for_delete(self, res, interval, wait) @@ -1024,8 +1024,7 @@ def extensions(self): """Return a generator of extensions :returns: A generator of extension - :rtype: :class:`~openstack.block_storage.v3.extension.\ - Extension` + :rtype: :class:`~openstack.block_storage.v3.extension.Extension` """ return self._list(_extension.Extension) From 894b5450469de600c4a7c7c313c9766de6765fb6 Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Tue, 26 Oct 2021 15:33:17 +0200 Subject: [PATCH 2935/3836] Block storage - reindentation of the docstrings Change-Id: I45f14d355098d100c92eaaf134b68cf7c006473c --- openstack/block_storage/v2/_proxy.py | 83 ++++++++++++++-------------- openstack/block_storage/v3/_proxy.py | 26 ++++----- 2 files changed, 55 insertions(+), 54 deletions(-) diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 39b928f3c..b98601375 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -28,12 +28,12 @@ def get_snapshot(self, snapshot): """Get a single snapshot :param snapshot: The value can be the ID of a snapshot or a - :class:`~openstack.block_storage.v2.snapshot.Snapshot` - instance. + :class:`~openstack.block_storage.v2.snapshot.Snapshot` + instance. :returns: One :class:`~openstack.block_storage.v2.snapshot.Snapshot` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_snapshot.Snapshot, snapshot) @@ -41,10 +41,10 @@ def snapshots(self, details=True, **query): """Retrieve a generator of snapshots :param bool details: When set to ``False`` - :class:`~openstack.block_storage.v2.snapshot.Snapshot` - objects will be returned. The default, ``True``, will cause - :class:`~openstack.block_storage.v2.snapshot.SnapshotDetail` - objects to be returned. + :class:`~openstack.block_storage.v2.snapshot.Snapshot` + objects will be returned. The default, ``True``, will cause + :class:`~openstack.block_storage.v2.snapshot.SnapshotDetail` + objects to be returned. :param kwargs query: Optional query parameters to be sent to limit the snapshots being returned. Available parameters include: @@ -52,7 +52,7 @@ def snapshots(self, details=True, **query): * all_projects: Whether return the snapshots in all projects. * volume_id: volume id of a snapshot. * status: Value of the status of the snapshot so that you can - filter on "available" for example. + filter on "available" for example. :returns: A generator of snapshot objects. """ @@ -75,13 +75,13 @@ def delete_snapshot(self, snapshot, ignore_missing=True): """Delete a snapshot :param snapshot: The value can be either the ID of a snapshot or a - :class:`~openstack.block_storage.v2.snapshot.Snapshot` - instance. + :class:`~openstack.block_storage.v2.snapshot.Snapshot` + instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the snapshot does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent snapshot. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the snapshot does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent snapshot. :returns: ``None`` """ @@ -106,11 +106,11 @@ def get_type(self, type): """Get a single type :param type: The value can be the ID of a type or a - :class:`~openstack.block_storage.v2.type.Type` instance. + :class:`~openstack.block_storage.v2.type.Type` instance. :returns: One :class:`~openstack.block_storage.v2.type.Type` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_type.Type, type) @@ -125,8 +125,8 @@ def create_type(self, **attrs): """Create a new type from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.block_storage.v2.type.Type`, - comprised of the properties on the Type class. + a :class:`~openstack.block_storage.v2.type.Type`, + comprised of the properties on the Type class. :returns: The results of type creation :rtype: :class:`~openstack.block_storage.v2.type.Type` @@ -137,12 +137,12 @@ def delete_type(self, type, ignore_missing=True): """Delete a type :param type: The value can be either the ID of a type or a - :class:`~openstack.block_storage.v2.type.Type` instance. + :class:`~openstack.block_storage.v2.type.Type` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the type does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent type. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the type does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent type. :returns: ``None`` """ @@ -209,7 +209,7 @@ def find_volume(self, name_or_id, ignore_missing=True, **attrs): :returns: One :class:`~openstack.block_storage.v2.volume.Volume` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._find(_volume.Volume, name_or_id, ignore_missing=ignore_missing) @@ -226,7 +226,7 @@ def volumes(self, details=True, **query): * name: Name of the volume as a string. * all_projects: Whether return the volumes in all projects * status: Value of the status of the volume so that you can filter - on "available" for example. + on "available" for example. :returns: A generator of volume objects. """ @@ -363,7 +363,8 @@ def unmanage_volume(self, volume): :param volume: The value can be either the ID of a volume or a :class:`~openstack.block_storage.v2.volume.Volume` instance. - :returns: None """ + :returns: None + """ volume = self._get_resource(_volume.Volume, volume) volume.unmanage(self) @@ -431,13 +432,13 @@ def backups(self, details=True, **query): * offset: pagination marker * limit: pagination limit * sort_key: Sorts by an attribute. A valid value is - name, status, container_format, disk_format, size, id, - created_at, or updated_at. Default is created_at. - The API uses the natural sorting direction of the - sort_key attribute value. + name, status, container_format, disk_format, size, id, + created_at, or updated_at. Default is created_at. + The API uses the natural sorting direction of the + sort_key attribute value. * sort_dir: Sorts by one or more sets of attribute and sort - direction combinations. If you omit the sort direction - in a set, default is desc. + direction combinations. If you omit the sort direction + in a set, default is desc. :returns: A generator of backup objects. """ @@ -521,22 +522,22 @@ def wait_for_status(self, res, status='available', failures=None, """Wait for a resource to be in a particular status. :param res: The resource to wait on to reach the specified status. - The resource must have a ``status`` attribute. + The resource must have a ``status`` attribute. :type resource: A :class:`~openstack.resource.Resource` object. :param status: Desired status. :param failures: Statuses that would be interpreted as failures. :type failures: :py:class:`list` :param interval: Number of seconds to wait before to consecutive - checks. Default to 2. + checks. Default to 2. :param wait: Maximum number of seconds to wait before the change. - Default to 120. + Default to 120. :returns: The resource is returned on success. :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition - to the desired status failed to occur in specified seconds. + to the desired status failed to occur in specified seconds. :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource - has transited to one of the failure statuses. + has transited to one of the failure statuses. :raises: :class:`~AttributeError` if the resource does not have a - ``status`` attribute. + ``status`` attribute. """ failures = ['error'] if failures is None else failures return resource.wait_for_status( @@ -548,12 +549,12 @@ def wait_for_delete(self, res, interval=2, wait=120): :param res: The resource to wait on to be deleted. :type resource: A :class:`~openstack.resource.Resource` object. :param interval: Number of seconds to wait before to consecutive - checks. Default to 2. + checks. Default to 2. :param wait: Maximum number of seconds to wait before the change. - Default to 120. + Default to 120. :returns: The resource is returned on success. :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition - to delete failed to occur in the specified seconds. + to delete failed to occur in the specified seconds. """ return resource.wait_for_delete(self, res, interval, wait) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index bec92cf74..028e23f93 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -743,13 +743,13 @@ def backups(self, details=True, **query): * offset: pagination marker * limit: pagination limit * sort_key: Sorts by an attribute. A valid value is - name, status, container_format, disk_format, size, id, - created_at, or updated_at. Default is created_at. - The API uses the natural sorting direction of the - sort_key attribute value. + name, status, container_format, disk_format, size, id, + created_at, or updated_at. Default is created_at. + The API uses the natural sorting direction of the + sort_key attribute value. * sort_dir: Sorts by one or more sets of attribute and sort - direction combinations. If you omit the sort direction - in a set, default is desc. + direction combinations. If you omit the sort direction + in a set, default is desc. * project_id: Project ID to query backups for. :returns: A generator of backup objects. @@ -914,15 +914,15 @@ def group_types(self, **query): resources being returned: * sort: Comma-separated list of sort keys and optional sort - directions in the form of [:]. A valid - direction is asc (ascending) or desc (descending). + directions in the form of [:]. A valid + direction is asc (ascending) or desc (descending). * limit: Requests a page size of items. Returns a number of items - up to a limit value. Use the limit parameter to make an - initial limited request and use the ID of the last-seen item - from the response as the marker parameter value in a - subsequent limited request. + up to a limit value. Use the limit parameter to make an + initial limited request and use the ID of the last-seen item + from the response as the marker parameter value in a + subsequent limited request. * offset: Used in conjunction with limit to return a slice of - items. Is where to start in the list. + items. Is where to start in the list. * marker: The ID of the last-seen item. :returns: A generator of group type objects. From 9564085691d838cd5c250f2f93db503c46ead8be Mon Sep 17 00:00:00 2001 From: Simon Hensel Date: Fri, 15 Oct 2021 16:44:25 +0200 Subject: [PATCH 2936/3836] Add support for updated_at field for volume objects Change-Id: Ifbdf335a7206d5f27bfc7fe83d6e51f3a41feddb --- openstack/block_storage/v2/volume.py | 2 ++ openstack/block_storage/v3/volume.py | 2 ++ openstack/tests/fakes.py | 1 + openstack/tests/unit/block_storage/v2/test_volume.py | 2 ++ openstack/tests/unit/block_storage/v3/test_volume.py | 2 ++ releasenotes/notes/vol-updated_at-274c3a2bb94c8939.yaml | 3 +++ 6 files changed, 12 insertions(+) create mode 100644 releasenotes/notes/vol-updated_at-274c3a2bb94c8939.yaml diff --git a/openstack/block_storage/v2/volume.py b/openstack/block_storage/v2/volume.py index ca66cdcce..0233bc7f4 100644 --- a/openstack/block_storage/v2/volume.py +++ b/openstack/block_storage/v2/volume.py @@ -39,6 +39,8 @@ class Volume(resource.Resource, metadata.MetadataMixin): consistency_group_id = resource.Body("consistencygroup_id") #: The timestamp of this volume creation. created_at = resource.Body("created_at") + #: The date and time when the resource was updated. + updated_at = resource.Body("updated_at") #: The volume description. description = resource.Body("description") #: Extended replication status on this volume. diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index c6814d2be..034198b64 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -42,6 +42,8 @@ class Volume(resource.Resource, metadata.MetadataMixin): consistency_group_id = resource.Body("consistencygroup_id") #: The timestamp of this volume creation. created_at = resource.Body("created_at") + #: The date and time when the resource was updated. + updated_at = resource.Body("updated_at") #: The volume description. description = resource.Body("description") #: Extended replication status on this volume. diff --git a/openstack/tests/fakes.py b/openstack/tests/fakes.py index 541055b7d..a035895f1 100644 --- a/openstack/tests/fakes.py +++ b/openstack/tests/fakes.py @@ -360,6 +360,7 @@ def __init__( self.volume_type = 'type:volume' self.availability_zone = 'az1' self.created_at = '1900-01-01 12:34:56' + self.updated_at = None self.source_volid = '12345' self.metadata = {} diff --git a/openstack/tests/unit/block_storage/v2/test_volume.py b/openstack/tests/unit/block_storage/v2/test_volume.py index 59c61eca8..a411f98ac 100644 --- a/openstack/tests/unit/block_storage/v2/test_volume.py +++ b/openstack/tests/unit/block_storage/v2/test_volume.py @@ -34,6 +34,7 @@ "availability_zone": "nova", "bootable": "false", "created_at": "2015-03-09T12:14:57.233772", + "updated_at": None, "description": "something", "volume_type": "some_type", "snapshot_id": "93c2e2aa-7744-4fd6-a31a-80c4726b08d7", @@ -91,6 +92,7 @@ def test_create(self): self.assertEqual(VOLUME["availability_zone"], sot.availability_zone) self.assertFalse(sot.is_bootable) self.assertEqual(VOLUME["created_at"], sot.created_at) + self.assertEqual(VOLUME["updated_at"], sot.updated_at) self.assertEqual(VOLUME["description"], sot.description) self.assertEqual(VOLUME["volume_type"], sot.volume_type) self.assertEqual(VOLUME["snapshot_id"], sot.snapshot_id) diff --git a/openstack/tests/unit/block_storage/v3/test_volume.py b/openstack/tests/unit/block_storage/v3/test_volume.py index 71b6d4916..f7acfe55b 100644 --- a/openstack/tests/unit/block_storage/v3/test_volume.py +++ b/openstack/tests/unit/block_storage/v3/test_volume.py @@ -35,6 +35,7 @@ "availability_zone": "nova", "bootable": "false", "created_at": "2015-03-09T12:14:57.233772", + "updated_at": None, "description": "something", "volume_type": "some_type", "snapshot_id": "93c2e2aa-7744-4fd6-a31a-80c4726b08d7", @@ -94,6 +95,7 @@ def test_create(self): self.assertEqual(VOLUME["availability_zone"], sot.availability_zone) self.assertFalse(sot.is_bootable) self.assertEqual(VOLUME["created_at"], sot.created_at) + self.assertEqual(VOLUME["updated_at"], sot.updated_at) self.assertEqual(VOLUME["description"], sot.description) self.assertEqual(VOLUME["volume_type"], sot.volume_type) self.assertEqual(VOLUME["snapshot_id"], sot.snapshot_id) diff --git a/releasenotes/notes/vol-updated_at-274c3a2bb94c8939.yaml b/releasenotes/notes/vol-updated_at-274c3a2bb94c8939.yaml new file mode 100644 index 000000000..e23371458 --- /dev/null +++ b/releasenotes/notes/vol-updated_at-274c3a2bb94c8939.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added support for the updated_at attribute for volume objects. From 58270810eeaafc86d744a8f6964b78cc22208d7d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 25 Mar 2021 17:59:08 +0000 Subject: [PATCH 2937/3836] exception: Correct argument There were multiple calls to subclasses of 'SDKException' that were providing a 'msg' kwarg. This doesn't exist. Instead, the parameter is called 'message'. Correct this by using positional args instead. Change-Id: Id425b9a57033c89b8a8466a32ab202cdf7556cb2 Signed-off-by: Stephen Finucane --- openstack/accelerator/v2/deployable.py | 4 +++- openstack/block_storage/v2/backup.py | 2 +- openstack/block_storage/v3/backup.py | 2 +- openstack/resource.py | 6 +++--- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/openstack/accelerator/v2/deployable.py b/openstack/accelerator/v2/deployable.py index ebfee3197..a499011fc 100644 --- a/openstack/accelerator/v2/deployable.py +++ b/openstack/accelerator/v2/deployable.py @@ -52,11 +52,13 @@ def _commit(self, session, request, method, microversion, has_body=True, # The baremetal proxy defaults to retrying on conflict, allow # overriding it via an explicit retry_on_conflict=False. kwargs['retriable_status_codes'] = retriable_status_codes - {409} + try: call = getattr(session, method.lower()) except AttributeError: raise exceptions.ResourceFailure( - msg="Invalid commit method: %s" % method) + "Invalid commit method: %s" % method) + request.url = request.url + "/program" response = call(request.url, json=request.body, headers=request.headers, microversion=microversion, diff --git a/openstack/block_storage/v2/backup.py b/openstack/block_storage/v2/backup.py index 0dd7df65e..381d77b65 100644 --- a/openstack/block_storage/v2/backup.py +++ b/openstack/block_storage/v2/backup.py @@ -122,7 +122,7 @@ def create(self, session, prepend_key=True, base_path=None, **params): else: # Just for safety of the implementation (since PUT removed) raise exceptions.ResourceFailure( - msg="Invalid create method: %s" % self.create_method) + "Invalid create method: %s" % self.create_method) has_body = (self.has_body if self.create_returns_body is None else self.create_returns_body) diff --git a/openstack/block_storage/v3/backup.py b/openstack/block_storage/v3/backup.py index d5546b7ce..af6560848 100644 --- a/openstack/block_storage/v3/backup.py +++ b/openstack/block_storage/v3/backup.py @@ -132,7 +132,7 @@ def create(self, session, prepend_key=True, base_path=None, **params): else: # Just for safety of the implementation (since PUT removed) raise exceptions.ResourceFailure( - msg="Invalid create method: %s" % self.create_method) + "Invalid create method: %s" % self.create_method) has_body = (self.has_body if self.create_returns_body is None else self.create_returns_body) diff --git a/openstack/resource.py b/openstack/resource.py index f73f5aaac..91a68c25b 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1380,7 +1380,7 @@ def create(self, session, prepend_key=True, base_path=None, **params): microversion=microversion, params=params) else: raise exceptions.ResourceFailure( - msg="Invalid create method: %s" % self.create_method) + "Invalid create method: %s" % self.create_method) has_body = (self.has_body if self.create_returns_body is None else self.create_returns_body) @@ -1435,7 +1435,7 @@ def bulk_create( method = session.post else: raise exceptions.ResourceFailure( - msg="Invalid create method: %s" % cls.create_method) + "Invalid create method: %s" % cls.create_method) body = [] resources = [] @@ -1625,7 +1625,7 @@ def _commit( call = getattr(session, method.lower()) except AttributeError: raise exceptions.ResourceFailure( - msg="Invalid commit method: %s" % method) + "Invalid commit method: %s" % method) response = call(request.url, json=request.body, headers=request.headers, microversion=microversion, From 86c85103bf8770e5aa5ad7abdd31fa73bc9e52f1 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 25 Mar 2021 18:01:15 +0000 Subject: [PATCH 2938/3836] Remove '__unicode__' helper This is a hangover from Python 2. It's no longer needed in Python 3-only land. Change-Id: I8c4ea02ddb522549711c4b31bc9ee8672f55ec60 Signed-off-by: Stephen Finucane --- openstack/exceptions.py | 5 +---- openstack/tests/unit/test_exceptions.py | 8 -------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/openstack/exceptions.py b/openstack/exceptions.py index e2b1ca309..c2370fef5 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -87,7 +87,7 @@ def __init__(self, message='Error', response=None, if self.status_code is not None and (400 <= self.status_code < 500): self.source = "Client" - def __unicode__(self): + def __str__(self): # 'Error' is the default value for self.message. If self.message isn't # 'Error', then someone has set a more informative error message # and we should use it. If it is 'Error', then we should construct a @@ -106,9 +106,6 @@ def __unicode__(self): message=super(HttpException, self).__str__(), remote_error=remote_error) - def __str__(self): - return self.__unicode__() - class BadRequestException(HttpException): """HTTP 400 Bad Request.""" diff --git a/openstack/tests/unit/test_exceptions.py b/openstack/tests/unit/test_exceptions.py index cf2d74736..a4b5f2bf7 100644 --- a/openstack/tests/unit/test_exceptions.py +++ b/openstack/tests/unit/test_exceptions.py @@ -61,14 +61,6 @@ def test_http_status(self): self.assertEqual(self.message, exc.message) self.assertEqual(http_status, exc.status_code) - def test_unicode_message(self): - unicode_message = u"Event: No item found for does_not_exist©" - http_exception = exceptions.HttpException(message=unicode_message) - try: - http_exception.__unicode__() - except Exception: - self.fail("HttpException unicode message error") - class TestRaiseFromResponse(base.TestCase): From b47652cfdcee27a6bc41ad25a10123b9f4b33015 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 26 Mar 2021 10:01:59 +0000 Subject: [PATCH 2939/3836] trivial: Typo fixes Change-Id: Ic60c751ab8842f3029ffa4390a6253c68b47883d Signed-off-by: Stephen Finucane --- openstack/utils.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openstack/utils.py b/openstack/utils.py index 9ef8bdb92..2e9997ccb 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -161,12 +161,14 @@ def pick_microversion(session, required): :param session: The session to use for making this request. :type session: :class:`~keystoneauth1.adapter.Adapter` - :param required: Version that is required for an action. + :param required: Minimum version that is required for an action. :type required: String or tuple or None. :return: ``required`` as a string if the ``session``'s default is too low, - the ``session``'s default otherwise. Returns ``None`` of both + otherwise the ``session``'s default. Returns ``None`` if both are ``None``. :raises: TypeError if ``required`` is invalid. + :raises: :class:`~openstack.exceptions.SDKException` if requested + microversion is not supported. """ if required is not None: required = discover.normalize_version_number(required) @@ -190,7 +192,7 @@ def pick_microversion(session, required): def maximum_supported_microversion(adapter, client_maximum): - """Determinte the maximum microversion supported by both client and server. + """Determine the maximum microversion supported by both client and server. :param adapter: :class:`~keystoneauth1.adapter.Adapter` instance. :param client_maximum: Maximum microversion supported by the client. From 0fc28b39a8f9b2151f3d3fef13b95cbf029c68d3 Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Thu, 28 Oct 2021 16:38:44 +0200 Subject: [PATCH 2940/3836] Network - reindentation of the docstrings Change-Id: I1e4a95c757e1017d1bb2674e5f5f4d3ea10a058b --- openstack/network/v2/_proxy.py | 1670 ++++++++++++++++---------------- 1 file changed, 835 insertions(+), 835 deletions(-) diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 86da25270..0bb246986 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -105,10 +105,10 @@ def delete_address_group(self, address_group, ignore_missing=True): a :class:`~openstack.network.v2.address_group.AddressGroup` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will - be raised when the address group does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent address group. + :class:`~openstack.exceptions.ResourceNotFound` will + be raised when the address group does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent address group. :returns: ``None`` """ @@ -120,14 +120,14 @@ def find_address_group(self, name_or_id, ignore_missing=True, **args): :param name_or_id: The name or ID of an address group. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.address_group.AddressGroup` - or None + or None """ return self._find(_address_group.AddressGroup, name_or_id, ignore_missing=ignore_missing, **args) @@ -140,7 +140,7 @@ def get_address_group(self, address_group): :returns: One :class:`~openstack.network.v2.address_group.AddressGroup` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_address_group.AddressGroup, address_group) @@ -148,7 +148,7 @@ def address_groups(self, **query): """Return a generator of address groups :param dict query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. * ``name``: Address group name * ``description``: Address group description @@ -165,7 +165,7 @@ def update_address_group(self, address_group, **attrs): :param address_group: Either the ID of an address group or a :class:`~openstack.network.v2.address_group.AddressGroup` instance. :param dict attrs: The attributes to update on the address group - represented by ``value``. + represented by ``value``. :returns: The updated address group :rtype: :class:`~openstack.network.v2.address_group.AddressGroup` @@ -217,10 +217,10 @@ def delete_address_scope(self, address_scope, ignore_missing=True): a :class:`~openstack.network.v2.address_scope.AddressScope` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the address scope does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent address scope. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the address scope does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent address scope. :returns: ``None`` """ @@ -232,14 +232,14 @@ def find_address_scope(self, name_or_id, ignore_missing=True, **args): :param name_or_id: The name or ID of an address scope. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.address_scope.AddressScope` - or None + or None """ return self._find(_address_scope.AddressScope, name_or_id, ignore_missing=ignore_missing, **args) @@ -252,7 +252,7 @@ def get_address_scope(self, address_scope): :returns: One :class:`~openstack.network.v2.address_scope.AddressScope` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_address_scope.AddressScope, address_scope) @@ -260,7 +260,7 @@ def address_scopes(self, **query): """Return a generator of address scopes :param dict query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. * ``name``: Address scope name * ``ip_version``: Address scope IP address version @@ -278,7 +278,7 @@ def update_address_scope(self, address_scope, **attrs): :param address_scope: Either the ID of an address scope or a :class:`~openstack.network.v2.address_scope.AddressScope` instance. :param dict attrs: The attributes to update on the address scope - represented by ``value``. + represented by ``value``. :returns: The updated address scope :rtype: :class:`~openstack.network.v2.address_scope.AddressScope` @@ -290,14 +290,14 @@ def agents(self, **query): """Return a generator of network agents :param dict query: Optional query parameters to be sent to limit the - resources being returned. + resources being returned. * ``agent_type``: Agent type. * ``availability_zone``: The availability zone for an agent. * ``binary``: The name of the agent's application binary. * ``description``: The description of the agent. * ``host``: The host (host name or host address) the agent is - running on. + running on. * ``topic``: The message queue topic used. * ``is_admin_state_up``: The administrative state of the agent. * ``is_alive``: Whether the agent is alive. @@ -311,12 +311,12 @@ def delete_agent(self, agent, ignore_missing=True): """Delete a network agent :param agent: The value can be the ID of a agent or a - :class:`~openstack.network.v2.agent.Agent` instance. + :class:`~openstack.network.v2.agent.Agent` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the agent does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent agent. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the agent does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent agent. :returns: ``None`` """ @@ -326,12 +326,12 @@ def get_agent(self, agent): """Get a single network agent :param agent: The value can be the ID of a agent or a - :class:`~openstack.network.v2.agent.Agent` instance. + :class:`~openstack.network.v2.agent.Agent` instance. :returns: One :class:`~openstack.network.v2.agent.Agent` :rtype: :class:`~openstack.network.v2.agent.Agent` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_agent.Agent, agent) @@ -339,9 +339,9 @@ def update_agent(self, agent, **attrs): """Update a network agent :param agent: The value can be the ID of a agent or a - :class:`~openstack.network.v2.agent.Agent` instance. + :class:`~openstack.network.v2.agent.Agent` instance. :param dict attrs: The attributes to update on the agent represented - by ``value``. + by ``value``. :returns: One :class:`~openstack.network.v2.agent.Agent` :rtype: :class:`~openstack.network.v2.agent.Agent` @@ -352,9 +352,9 @@ def dhcp_agent_hosting_networks(self, agent, **query): """A generator of networks hosted by a DHCP agent. :param agent: Either the agent id of an instance of - :class:`~openstack.network.v2.network_agent.Agent` + :class:`~openstack.network.v2.network_agent.Agent` :param query: kwargs query: Optional query parameters to be sent - to limit the resources being returned. + to limit the resources being returned. :return: A generator of networks """ agent_obj = self._get_resource(_agent.Agent, agent) @@ -365,7 +365,7 @@ def add_dhcp_agent_to_network(self, agent, network): """Add a DHCP Agent to a network :param agent: Either the agent id of an instance of - :class:`~openstack.network.v2.network_agent.Agent` + :class:`~openstack.network.v2.network_agent.Agent` :param network: Network instance :return: """ @@ -377,7 +377,7 @@ def remove_dhcp_agent_from_network(self, agent, network): """Remove a DHCP Agent from a network :param agent: Either the agent id of an instance of - :class:`~openstack.network.v2.network_agent.Agent` + :class:`~openstack.network.v2.network_agent.Agent` :param network: Network instance :return: """ @@ -389,9 +389,9 @@ def network_hosting_dhcp_agents(self, network, **query): """A generator of DHCP agents hosted on a network. :param network: The instance of - :class:`~openstack.network.v2.network.Network` + :class:`~openstack.network.v2.network.Network` :param dict query: Optional query parameters to be sent to limit the - resources returned. + resources returned. :return: A generator of hosted DHCP agents """ net = self._get_resource(_network.Network, network) @@ -402,11 +402,11 @@ def get_auto_allocated_topology(self, project=None): """Get the auto-allocated topology of a given tenant :param project: - The value is the ID or name of a project + The value is the ID or name of a project :returns: The auto-allocated topology :rtype: :class:`~openstack.network.v2.\ - auto_allocated_topology.AutoAllocatedTopology` + auto_allocated_topology.AutoAllocatedTopology` """ # If project option is not given, grab project id from session @@ -422,10 +422,10 @@ def delete_auto_allocated_topology(self, project=None, :param project: The value is the ID or name of a project :param ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the topology does not exist. - When set to ``True``, no exception will be raised when - attempting to delete nonexistant topology + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the topology does not exist. + When set to ``True``, no exception will be raised when + attempting to delete nonexistant topology :returns: ``None`` """ @@ -440,11 +440,11 @@ def validate_auto_allocated_topology(self, project=None): """Validate the resources for auto allocation :param project: - The value is the ID or name of a project + The value is the ID or name of a project :returns: Whether all resources are correctly configured or not :rtype: :class:`~openstack.network.v2.\ - auto_allocated_topology.ValidateTopology` + auto_allocated_topology.ValidateTopology` """ # If project option is not given, grab project id from session @@ -457,7 +457,7 @@ def availability_zones(self, **query): """Return a generator of availability zones :param dict query: optional query parameters to be set to limit the - returned resources. Valid parameters include: + returned resources. Valid parameters include: * ``name``: The name of an availability zone. * ``resource``: The type of resource for the availability zone. @@ -473,14 +473,14 @@ def find_extension(self, name_or_id, ignore_missing=True, **args): :param name_or_id: The name or ID of a extension. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.extension.Extension` - or None + or None """ return self._find(extension.Extension, name_or_id, ignore_missing=ignore_missing, **args) @@ -489,8 +489,8 @@ def extensions(self, **query): """Return a generator of extensions :param dict query: Optional query parameters to be sent to limit - the resources being returned. Currently no - parameter is supported. + the resources being returned. Currently no + parameter is supported. :returns: A generator of extension objects :rtype: :class:`~openstack.network.v2.extension.Extension` @@ -501,8 +501,8 @@ def create_flavor(self, **attrs): """Create a new network service flavor from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.network.v2.flavor.Flavor`, - comprised of the properties on the Flavor class. + a :class:`~openstack.network.v2.flavor.Flavor`, + comprised of the properties on the Flavor class. :returns: The results of flavor creation :rtype: :class:`~openstack.network.v2.flavor.Flavor` @@ -516,10 +516,10 @@ def delete_flavor(self, flavor, ignore_missing=True): The value can be either the ID of a flavor or a :class:`~openstack.network.v2.flavor.Flavor` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the flavor does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent flavor. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the flavor does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent flavor. :returns: ``None`` """ @@ -530,12 +530,12 @@ def find_flavor(self, name_or_id, ignore_missing=True, **args): :param name_or_id: The name or ID of a flavor. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.flavor.Flavor` or None """ return self._find(_flavor.Flavor, name_or_id, @@ -550,7 +550,7 @@ def get_flavor(self, flavor): :returns: One :class:`~openstack.network.v2.flavor.Flavor` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_flavor.Flavor, flavor) @@ -561,7 +561,7 @@ def update_flavor(self, flavor, **attrs): Either the id of a flavor or a :class:`~openstack.network.v2.flavor.Flavor` instance. :attrs kwargs: The attributes to update on the flavor represented - by ``value``. + by ``value``. :returns: The updated flavor :rtype: :class:`~openstack.network.v2.flavor.Flavor` @@ -572,8 +572,8 @@ def flavors(self, **query): """Return a generator of network service flavors :param dict query: Optional query parameters to be sent to limit - the resources being returned. Valid parameters - include: + the resources being returned. Valid parameters + include: * ``description``: The description of a flavor. * ``is_enabled``: Whether a flavor is enabled. @@ -638,15 +638,15 @@ def delete_ip(self, floating_ip, ignore_missing=True, if_revision=None): """Delete a floating ip :param floating_ip: The value can be either the ID of a floating ip - or a :class:`~openstack.network.v2.floating_ip.FloatingIP` - instance. + or a :class:`~openstack.network.v2.floating_ip.FloatingIP` + instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the floating ip does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent ip. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the floating ip does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent ip. :param int if_revision: Revision to put in If-Match header of update - request to perform compare-and-swap update. + request to perform compare-and-swap update. :returns: ``None`` """ @@ -657,7 +657,7 @@ def find_available_ip(self): """Find an available IP :returns: One :class:`~openstack.network.v2.floating_ip.FloatingIP` - or None + or None """ return _floating_ip.FloatingIP.find_available(self) @@ -666,14 +666,14 @@ def find_ip(self, name_or_id, ignore_missing=True, **args): :param name_or_id: The name or ID of an IP. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.floating_ip.FloatingIP` - or None + or None """ return self._find(_floating_ip.FloatingIP, name_or_id, ignore_missing=ignore_missing, **args) @@ -682,12 +682,12 @@ def get_ip(self, floating_ip): """Get a single floating ip :param floating_ip: The value can be the ID of a floating ip or a - :class:`~openstack.network.v2.floating_ip.FloatingIP` - instance. + :class:`~openstack.network.v2.floating_ip.FloatingIP` + instance. :returns: One :class:`~openstack.network.v2.floating_ip.FloatingIP` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_floating_ip.FloatingIP, floating_ip) @@ -695,21 +695,21 @@ def ips(self, **query): """Return a generator of ips :param dict query: Optional query parameters to be sent to limit - the resources being returned. Valid parameters are: + the resources being returned. Valid parameters are: * ``description``: The description of a floating IP. * ``fixed_ip_address``: The fixed IP address associated with a - floating IP address. + floating IP address. * ``floating_ip_address``: The IP address of a floating IP. * ``floating_network_id``: The ID of the network associated with - a floating IP. + a floating IP. * ``port_id``: The ID of the port to which a floating IP is - associated. + associated. * ``project_id``: The ID of the project a floating IP is - associated with. + associated with. * ``router_id``: The ID of an associated router. * ``status``: The status of a floating IP, which can be ``ACTIVE`` - or ``DOWN``. + or ``DOWN``. :returns: A generator of floating IP objects :rtype: :class:`~openstack.network.v2.floating_ip.FloatingIP` @@ -720,12 +720,12 @@ def update_ip(self, floating_ip, if_revision=None, **attrs): """Update a ip :param floating_ip: Either the id of a ip or a - :class:`~openstack.network.v2.floating_ip.FloatingIP` - instance. + :class:`~openstack.network.v2.floating_ip.FloatingIP` + instance. :param int if_revision: Revision to put in If-Match header of update - request to perform compare-and-swap update. + request to perform compare-and-swap update. :param dict attrs: The attributes to update on the ip represented - by ``value``. + by ``value``. :returns: The updated ip :rtype: :class:`~openstack.network.v2.floating_ip.FloatingIP` @@ -752,13 +752,13 @@ def get_port_forwarding(self, port_forwarding, floating_ip): or a :class:`~openstack.network.v2.port_forwarding.PortForwarding` instance. :param floating_ip: The value can be the ID of a Floating IP or a - :class:`~openstack.network.v2.floating_ip.FloatingIP` - instance. + :class:`~openstack.network.v2.floating_ip.FloatingIP` + instance. :returns: One :class:`~openstack.network.v2.port_forwarding.PortForwarding` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ floating_ip = self._get_resource(_floating_ip.FloatingIP, floating_ip) return self._get(_port_forwarding.PortForwarding, port_forwarding, @@ -770,15 +770,15 @@ def find_port_forwarding(self, pf_id, floating_ip, ignore_missing=True, :param pf_id: The ID of a port forwarding. :param floating_ip: The value can be the ID of a Floating IP or a - :class:`~openstack.network.v2.floating_ip.FloatingIP` - instance. + :class:`~openstack.network.v2.floating_ip.FloatingIP` + instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.port_forwarding.PortForwarding` or None @@ -796,13 +796,13 @@ def delete_port_forwarding(self, port_forwarding, floating_ip, or a :class:`~openstack.network.v2.port_forwarding.PortForwarding` instance. :param floating_ip: The value can be the ID of a Floating IP or a - :class:`~openstack.network.v2.floating_ip.FloatingIP` - instance. + :class:`~openstack.network.v2.floating_ip.FloatingIP` + instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the floating ip does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent ip. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the floating ip does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent ip. :returns: ``None`` """ @@ -815,10 +815,10 @@ def port_forwardings(self, floating_ip, **query): """Return a generator of port forwardings :param floating_ip: The value can be the ID of a Floating IP or a - :class:`~openstack.network.v2.floating_ip.FloatingIP` - instance. + :class:`~openstack.network.v2.floating_ip.FloatingIP` + instance. :param dict query: Optional query parameters to be sent to limit - the resources being returned. Valid parameters are: + the resources being returned. Valid parameters are: * ``internal_port_id``: The ID of internal port. * ``external_port``: The external TCP/UDP/other port number @@ -838,10 +838,10 @@ def update_port_forwarding(self, port_forwarding, floating_ip, **attrs): or a :class:`~openstack.network.v2.port_forwarding.PortForwarding` instance. :param floating_ip: The value can be the ID of a Floating IP or a - :class:`~openstack.network.v2.floating_ip.FloatingIP` - instance. + :class:`~openstack.network.v2.floating_ip.FloatingIP` + instance. :param dict attrs: The attributes to update on the ip represented - by ``value``. + by ``value``. :returns: The updated port_forwarding :rtype: :class:`~openstack.network.v2.port_forwarding.PortForwarding` @@ -870,10 +870,10 @@ def delete_health_monitor(self, health_monitor, ignore_missing=True): :class:`~openstack.network.v2.health_monitor.HealthMonitor` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the health monitor does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent health monitor. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the health monitor does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent health monitor. :returns: ``None`` """ @@ -885,14 +885,14 @@ def find_health_monitor(self, name_or_id, ignore_missing=True, **args): :param name_or_id: The name or ID of a health monitor. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.health_monitor. - HealthMonitor` or None + HealthMonitor` or None """ return self._find(_health_monitor.HealthMonitor, name_or_id, ignore_missing=ignore_missing, **args) @@ -901,13 +901,13 @@ def get_health_monitor(self, health_monitor): """Get a single health monitor :param health_monitor: The value can be the ID of a health monitor or a - :class:`~openstack.network.v2.health_monitor.HealthMonitor` - instance. + :class:`~openstack.network.v2.health_monitor.HealthMonitor` + instance. :returns: One - :class:`~openstack.network.v2.health_monitor.HealthMonitor` + :class:`~openstack.network.v2.health_monitor.HealthMonitor` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_health_monitor.HealthMonitor, health_monitor) @@ -915,23 +915,23 @@ def health_monitors(self, **query): """Return a generator of health monitors :param dict query: Optional query parameters to be sent to limit - the resources being returned. Valid parameters are: + the resources being returned. Valid parameters are: * ``delay``: the time in milliseconds between sending probes. * ``expected_codes``: The expected HTTP codes for a pssing HTTP(S) - monitor. + monitor. * ``http_method``: The HTTP method a monitor uses for requests. * ``is_admin_state_up``: The administrative state of a health - monitor. + monitor. * ``max_retries``: The maximum consecutive health probe attempts. * ``project_id``: The ID of the project this health monitor is - associated with. + associated with. * ``timeout``: The maximum number of milliseconds for a monitor to - wait for a connection to be established before it - times out. + wait for a connection to be established before it + times out. * ``type``: The type of probe sent by the load balancer for health - check, which can be ``PING``, ``TCP``, ``HTTP`` or - ``HTTPS``. + check, which can be ``PING``, ``TCP``, ``HTTP`` or + ``HTTPS``. * ``url_path``: The path portion of a URI that will be probed. :returns: A generator of health monitor objects @@ -943,10 +943,10 @@ def update_health_monitor(self, health_monitor, **attrs): """Update a health monitor :param health_monitor: Either the id of a health monitor or a - :class:`~openstack.network.v2.health_monitor. - HealthMonitor` instance. + :class:`~openstack.network.v2.health_monitor. + HealthMonitor` instance. :param dict attrs: The attributes to update on the health monitor - represented by ``value``. + represented by ``value``. :returns: The updated health monitor :rtype: :class:`~openstack.network.v2.health_monitor.HealthMonitor` @@ -1073,7 +1073,7 @@ def find_vpn_ikepolicy(self, name_or_id, :param dict args: Any additional parameters to be passed into underlying methods such as query filters. :returns: One :class:`~openstack.network.v2.ikepolicy.IkePolicy` or - None. + None. """ return self._find(_ikepolicy.IkePolicy, name_or_id, ignore_missing=ignore_missing, **args) @@ -1108,7 +1108,7 @@ def update_vpn_ikepolicy(self, ikepolicy, **attrs): :ikepolicy: Either the id of an ike policy or a :class:`~openstack.network.v2.ikepolicy.IkePolicy` instance. :param dict attrs: The attributes to update on the ike policy - represented by ``ikepolicy``. + represented by ``ikepolicy``. :returns: The updated ike policy :rtype: :class:`~openstack.network.v2.ikepolicy.IkePolicy` @@ -1136,8 +1136,8 @@ def create_listener(self, **attrs): """Create a new listener from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.network.v2.listener.Listener`, - comprised of the properties on the Listener class. + a :class:`~openstack.network.v2.listener.Listener`, + comprised of the properties on the Listener class. :returns: The results of listener creation :rtype: :class:`~openstack.network.v2.listener.Listener` @@ -1148,12 +1148,12 @@ def delete_listener(self, listener, ignore_missing=True): """Delete a listener :param listener: The value can be either the ID of a listner or a - :class:`~openstack.network.v2.listener.Listener` instance. + :class:`~openstack.network.v2.listener.Listener` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the listner does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent listener. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the listner does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent listener. :returns: ``None`` """ @@ -1165,12 +1165,12 @@ def find_listener(self, name_or_id, ignore_missing=True, **args): :param name_or_id: The name or ID of a listener. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.listener.Listener` or None """ return self._find(_listener.Listener, name_or_id, @@ -1180,12 +1180,12 @@ def get_listener(self, listener): """Get a single listener :param listener: The value can be the ID of a listener or a - :class:`~openstack.network.v2.listener.Listener` - instance. + :class:`~openstack.network.v2.listener.Listener` + instance. :returns: One :class:`~openstack.network.v2.listener.Listener` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_listener.Listener, listener) @@ -1193,13 +1193,13 @@ def listeners(self, **query): """Return a generator of listeners :param dict query: Optional query parameters to be sent to limit - the resources being returned. Valid parameters are: + the resources being returned. Valid parameters are: * ``connection_limit``: The maximum number of connections - permitted for the load-balancer. + permitted for the load-balancer. * ``default_pool_id``: The ID of the default pool. * ``default_tls_container_ref``: A reference to a container of TLS - secret. + secret. * ``description``: The description of a listener. * ``is_admin_state_up``: The administrative state of the listener. * ``name``: The name of a listener. @@ -1216,10 +1216,10 @@ def update_listener(self, listener, **attrs): """Update a listener :param listener: Either the id of a listener or a - :class:`~openstack.network.v2.listener.Listener` - instance. + :class:`~openstack.network.v2.listener.Listener` + instance. :param dict attrs: The attributes to update on the listener - represented by ``listener``. + represented by ``listener``. :returns: The updated listener :rtype: :class:`~openstack.network.v2.listener.Listener` @@ -1242,13 +1242,13 @@ def delete_load_balancer(self, load_balancer, ignore_missing=True): """Delete a load balancer :param load_balancer: The value can be the ID of a load balancer or a - :class:`~openstack.network.v2.load_balancer.LoadBalancer` - instance. + :class:`~openstack.network.v2.load_balancer.LoadBalancer` + instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the load balancer does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent load balancer. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the load balancer does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent load balancer. :returns: ``None`` """ @@ -1260,14 +1260,14 @@ def find_load_balancer(self, name_or_id, ignore_missing=True, **args): :param name_or_id: The name or ID of a load balancer. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.load_balancer.LoadBalancer` - or None + or None """ return self._find(_load_balancer.LoadBalancer, name_or_id, ignore_missing=ignore_missing, **args) @@ -1276,12 +1276,12 @@ def get_load_balancer(self, load_balancer): """Get a single load balancer :param load_balancer: The value can be the ID of a load balancer or a - :class:`~openstack.network.v2.load_balancer.LoadBalancer` - instance. + :class:`~openstack.network.v2.load_balancer.LoadBalancer` + instance. :returns: One :class:`~openstack.network.v2.load_balancer.LoadBalancer` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_load_balancer.LoadBalancer, load_balancer) @@ -1289,7 +1289,7 @@ def load_balancers(self, **query): """Return a generator of load balancers :param dict query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of load balancer objects :rtype: :class:`~openstack.network.v2.load_balancer.LoadBalancer` @@ -1300,10 +1300,10 @@ def update_load_balancer(self, load_balancer, **attrs): """Update a load balancer :param load_balancer: Either the id of a load balancer or a - :class:`~openstack.network.v2.load_balancer.LoadBalancer` - instance. + :class:`~openstack.network.v2.load_balancer.LoadBalancer` + instance. :param dict attrs: The attributes to update on the load balancer - represented by ``load_balancer``. + represented by ``load_balancer``. :returns: The updated load balancer :rtype: :class:`~openstack.network.v2.load_balancer.LoadBalancer` @@ -1327,14 +1327,14 @@ def delete_metering_label(self, metering_label, ignore_missing=True): """Delete a metering label :param metering_label: - The value can be either the ID of a metering label or a - :class:`~openstack.network.v2.metering_label.MeteringLabel` - instance. + The value can be either the ID of a metering label or a + :class:`~openstack.network.v2.metering_label.MeteringLabel` + instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the metering label does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent metering label. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the metering label does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent metering label. :returns: ``None`` """ @@ -1346,14 +1346,14 @@ def find_metering_label(self, name_or_id, ignore_missing=True, **args): :param name_or_id: The name or ID of a metering label. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.metering_label. - MeteringLabel` or None + MeteringLabel` or None """ return self._find(_metering_label.MeteringLabel, name_or_id, ignore_missing=ignore_missing, **args) @@ -1362,13 +1362,13 @@ def get_metering_label(self, metering_label): """Get a single metering label :param metering_label: The value can be the ID of a metering label or a - :class:`~openstack.network.v2.metering_label.MeteringLabel` - instance. + :class:`~openstack.network.v2.metering_label.MeteringLabel` + instance. :returns: One - :class:`~openstack.network.v2.metering_label.MeteringLabel` + :class:`~openstack.network.v2.metering_label.MeteringLabel` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_metering_label.MeteringLabel, metering_label) @@ -1376,14 +1376,14 @@ def metering_labels(self, **query): """Return a generator of metering labels :param dict query: Optional query parameters to be sent to limit - the resources being returned. Valid parameters are: + the resources being returned. Valid parameters are: * ``description``: Description of a metering label. * ``name``: Name of a metering label. * ``is_shared``: Boolean indicating whether a metering label is - shared. + shared. * ``project_id``: The ID of the project a metering label is - associated with. + associated with. :returns: A generator of metering label objects :rtype: :class:`~openstack.network.v2.metering_label.MeteringLabel` @@ -1394,10 +1394,10 @@ def update_metering_label(self, metering_label, **attrs): """Update a metering label :param metering_label: Either the id of a metering label or a - :class:`~openstack.network.v2.metering_label. - MeteringLabel` instance. + :class:`~openstack.network.v2.metering_label. + MeteringLabel` instance. :param dict attrs: The attributes to update on the metering label - represented by ``metering_label``. + represented by ``metering_label``. :returns: The updated metering label :rtype: :class:`~openstack.network.v2.metering_label.MeteringLabel` @@ -1415,7 +1415,7 @@ def create_metering_label_rule(self, **attrs): :returns: The results of metering label rule creation :rtype: :class:`~openstack.network.v2.metering_label_rule.\ - MeteringLabelRule` + MeteringLabelRule` """ return self._create(_metering_label_rule.MeteringLabelRule, **attrs) @@ -1444,14 +1444,14 @@ def find_metering_label_rule(self, name_or_id, ignore_missing=True, :param name_or_id: The name or ID of a metering label rule. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.metering_label_rule. - MeteringLabelRule` or None + MeteringLabelRule` or None """ return self._find(_metering_label_rule.MeteringLabelRule, name_or_id, ignore_missing=ignore_missing, **args) @@ -1468,7 +1468,7 @@ def get_metering_label_rule(self, metering_label_rule): :class:`~openstack.network.v2.metering_label_rule.\ MeteringLabelRule` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_metering_label_rule.MeteringLabelRule, metering_label_rule) @@ -1477,20 +1477,20 @@ def metering_label_rules(self, **query): """Return a generator of metering label rules :param dict query: Optional query parameters to be sent to limit - the resources being returned. Valid parameters are: + the resources being returned. Valid parameters are: * ``direction``: The direction in which metering label rule is - applied. + applied. * ``metering_label_id``: The ID of a metering label this rule is - associated with. + associated with. * ``project_id``: The ID of the project the metering label rule is - associated with. + associated with. * ``remote_ip_prefix``: The remote IP prefix to be associated with - this metering label rule. + this metering label rule. :returns: A generator of metering label rule objects :rtype: :class:`~openstack.network.v2.metering_label_rule. - MeteringLabelRule` + MeteringLabelRule` """ return self._list(_metering_label_rule.MeteringLabelRule, **query) @@ -1498,15 +1498,15 @@ def update_metering_label_rule(self, metering_label_rule, **attrs): """Update a metering label rule :param metering_label_rule: - Either the id of a metering label rule or a - :class:`~openstack.network.v2.metering_label_rule. - MeteringLabelRule` instance. + Either the id of a metering label rule or a + :class:`~openstack.network.v2.metering_label_rule. + MeteringLabelRule` instance. :param dict attrs: The attributes to update on the metering label rule - represented by ``metering_label_rule``. + represented by ``metering_label_rule``. :returns: The updated metering label rule :rtype: :class:`~openstack.network.v2.metering_label_rule. - MeteringLabelRule` + MeteringLabelRule` """ return self._update(_metering_label_rule.MeteringLabelRule, metering_label_rule, **attrs) @@ -1515,8 +1515,8 @@ def create_network(self, **attrs): """Create a new network from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.network.v2.network.Network`, - comprised of the properties on the Network class. + a :class:`~openstack.network.v2.network.Network`, + comprised of the properties on the Network class. :returns: The results of network creation :rtype: :class:`~openstack.network.v2.network.Network` @@ -1530,12 +1530,12 @@ def delete_network(self, network, ignore_missing=True, if_revision=None): The value can be either the ID of a network or a :class:`~openstack.network.v2.network.Network` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the network does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent network. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the network does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent network. :param int if_revision: Revision to put in If-Match header of update - request to perform compare-and-swap update. + request to perform compare-and-swap update. :returns: ``None`` """ @@ -1547,12 +1547,12 @@ def find_network(self, name_or_id, ignore_missing=True, **args): :param name_or_id: The name or ID of a network. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.network.Network` or None """ return self._find(_network.Network, name_or_id, @@ -1567,7 +1567,7 @@ def get_network(self, network): :returns: One :class:`~openstack.network.v2.network.Network` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_network.Network, network) @@ -1579,9 +1579,9 @@ def networks(self, **query): * ``description``: The network description. * ``ipv4_address_scope_id``: The ID of the IPv4 address scope for - the network. + the network. * ``ipv6_address_scope_id``: The ID of the IPv6 address scope for - the network. + the network. * ``is_admin_state_up``: Network administrative state * ``is_port_security_enabled``: The port security status. * ``is_router_external``: Network is external or not. @@ -1592,7 +1592,7 @@ def networks(self, **query): * ``provider_network_type``: Network physical mechanism * ``provider_physical_network``: Physical network * ``provider_segmentation_id``: VLAN ID for VLAN networks or Tunnel - ID for GENEVE/GRE/VXLAN networks + ID for GENEVE/GRE/VXLAN networks :returns: A generator of network objects :rtype: :class:`~openstack.network.v2.network.Network` @@ -1603,11 +1603,11 @@ def update_network(self, network, if_revision=None, **attrs): """Update a network :param network: Either the id of a network or an instance of type - :class:`~openstack.network.v2.network.Network`. + :class:`~openstack.network.v2.network.Network`. :param int if_revision: Revision to put in If-Match header of update - request to perform compare-and-swap update. + request to perform compare-and-swap update. :param dict attrs: The attributes to update on the network represented - by ``network``. + by ``network``. :returns: The updated network :rtype: :class:`~openstack.network.v2.network.Network` @@ -1621,14 +1621,14 @@ def find_network_ip_availability(self, name_or_id, ignore_missing=True, :param name_or_id: The name or ID of a network. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.network_ip_availability. - NetworkIPAvailability` or None + NetworkIPAvailability` or None """ return self._find(network_ip_availability.NetworkIPAvailability, name_or_id, ignore_missing=ignore_missing, **args) @@ -1641,9 +1641,9 @@ def get_network_ip_availability(self, network): :class:`~openstack.network.v2.network.Network` instance. :returns: One :class:`~openstack.network.v2.network_ip_availability. - NetworkIPAvailability` + NetworkIPAvailability` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(network_ip_availability.NetworkIPAvailability, network) @@ -1656,14 +1656,14 @@ def network_ip_availabilities(self, **query): * ``ip_version``: IP version of the network * ``network_id``: ID of network to use when listening network IP - availability. + availability. * ``network_name``: The name of the network for the particular - network IP availability. + network IP availability. * ``project_id``: Owner tenant ID :returns: A generator of network ip availability objects :rtype: :class:`~openstack.network.v2.network_ip_availability. - NetworkIPAvailability` + NetworkIPAvailability` """ return self._list(network_ip_availability.NetworkIPAvailability, **query) @@ -1672,14 +1672,14 @@ def create_network_segment_range(self, **attrs): """Create a new network segment range from attributes :param dict attrs: Keyword arguments which will be used to create a - :class:`~openstack.network.v2. - network_segment_range.NetworkSegmentRange`, - comprised of the properties on the - NetworkSegmentRange class. + :class:`~openstack.network.v2. + network_segment_range.NetworkSegmentRange`, + comprised of the properties on the + NetworkSegmentRange class. :returns: The results of network segment range creation :rtype: :class:`~openstack.network.v2.network_segment_range - .NetworkSegmentRange` + .NetworkSegmentRange` """ return self._create(_network_segment_range.NetworkSegmentRange, **attrs) @@ -1689,14 +1689,14 @@ def delete_network_segment_range(self, network_segment_range, """Delete a network segment range :param network_segment_range: The value can be either the ID of a - network segment range or a - :class:`~openstack.network.v2.network_segment_range. - NetworkSegmentRange` instance. + network segment range or a + :class:`~openstack.network.v2.network_segment_range. + NetworkSegmentRange` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the network segment range does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent network segment range. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the network segment range does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent network segment range. :returns: ``None`` """ @@ -1709,14 +1709,14 @@ def find_network_segment_range(self, name_or_id, ignore_missing=True, :param name_or_id: The name or ID of a network segment range. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.network_segment_range - .NetworkSegmentRange` or None + .NetworkSegmentRange` or None """ return self._find(_network_segment_range.NetworkSegmentRange, name_or_id, ignore_missing=ignore_missing, **args) @@ -1725,13 +1725,13 @@ def get_network_segment_range(self, network_segment_range): """Get a single network segment range :param network_segment_range: The value can be the ID of a network - segment range or a :class:`~openstack.network.v2. - network_segment_range.NetworkSegmentRange` instance. + segment range or a :class:`~openstack.network.v2. + network_segment_range.NetworkSegmentRange` instance. :returns: One :class:`~openstack.network.v2._network_segment_range. - NetworkSegmentRange` + NetworkSegmentRange` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_network_segment_range.NetworkSegmentRange, network_segment_range) @@ -1744,26 +1744,26 @@ def network_segment_ranges(self, **query): * ``name``: Name of the segments * ``default``: The network segment range is loaded from the host - configuration file. + configuration file. * ``shared``: The network segment range is shared with other - projects + projects * ``project_id``: ID of the project that owns the network - segment range + segment range * ``network_type``: Network type for the network segment ranges * ``physical_network``: Physical network name for the network - segment ranges + segment ranges * ``minimum``: Minimum segmentation ID for the network segment - ranges + ranges * ``maximum``: Maximum Segmentation ID for the network segment - ranges + ranges * ``used``: Mapping of which segmentation ID in the range is - used by which tenant + used by which tenant * ``available``: List of available segmentation IDs in this - network segment range + network segment range :returns: A generator of network segment range objects :rtype: :class:`~openstack.network.v2._network_segment_range. - NetworkSegmentRange` + NetworkSegmentRange` """ return self._list(_network_segment_range.NetworkSegmentRange, **query) @@ -1771,14 +1771,14 @@ def update_network_segment_range(self, network_segment_range, **attrs): """Update a network segment range :param network_segment_range: Either the id of a network segment range - or a :class:`~openstack.network.v2._network_segment_range. - NetworkSegmentRange` instance. + or a :class:`~openstack.network.v2._network_segment_range. + NetworkSegmentRange` instance. :attrs kwargs: The attributes to update on the network segment range - represented by ``value``. + represented by ``value``. :returns: The updated network segment range :rtype: :class:`~openstack.network.v2._network_segment_range. - NetworkSegmentRange` + NetworkSegmentRange` """ return self._update(_network_segment_range.NetworkSegmentRange, network_segment_range, **attrs) @@ -1787,8 +1787,8 @@ def create_pool(self, **attrs): """Create a new pool from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.network.v2.pool.Pool`, - comprised of the properties on the Pool class. + a :class:`~openstack.network.v2.pool.Pool`, + comprised of the properties on the Pool class. :returns: The results of pool creation :rtype: :class:`~openstack.network.v2.pool.Pool` @@ -1799,12 +1799,12 @@ def delete_pool(self, pool, ignore_missing=True): """Delete a pool :param pool: The value can be either the ID of a pool or a - :class:`~openstack.network.v2.pool.Pool` instance. + :class:`~openstack.network.v2.pool.Pool` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the pool does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent pool. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the pool does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent pool. :returns: ``None`` """ @@ -1815,12 +1815,12 @@ def find_pool(self, name_or_id, ignore_missing=True, **args): :param name_or_id: The name or ID of a pool. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.pool.Pool` or None """ return self._find(_pool.Pool, name_or_id, @@ -1830,11 +1830,11 @@ def get_pool(self, pool): """Get a single pool :param pool: The value can be the ID of a pool or a - :class:`~openstack.network.v2.pool.Pool` instance. + :class:`~openstack.network.v2.pool.Pool` instance. :returns: One :class:`~openstack.network.v2.pool.Pool` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_pool.Pool, pool) @@ -1842,21 +1842,21 @@ def pools(self, **query): """Return a generator of pools :param dict query: Optional query parameters to be sent to limit - the resources being returned. Valid parameters are: + the resources being returned. Valid parameters are: * ``description``: The description for the pool. * ``is_admin_state_up``: The administrative state of the pool. * ``lb_algorithm``: The load-balancer algorithm used, which is one - of ``round-robin``, ``least-connections`` and so on. + of ``round-robin``, ``least-connections`` and so on. * ``name``: The name of the node pool. * ``project_id``: The ID of the project the pool is associated - with. + with. * ``protocol``: The protocol used by the pool, which is one of - ``TCP``, ``HTTP`` or ``HTTPS``. + ``TCP``, ``HTTP`` or ``HTTPS``. * ``provider``: The name of the provider of the load balancer - service. + service. * ``subnet_id``: The subnet on which the members of the pool are - located. + located. * ``virtual_ip_id``: The ID of the virtual IP used. :returns: A generator of pool objects @@ -1868,9 +1868,9 @@ def update_pool(self, pool, **attrs): """Update a pool :param pool: Either the id of a pool or a - :class:`~openstack.network.v2.pool.Pool` instance. + :class:`~openstack.network.v2.pool.Pool` instance. :param dict attrs: The attributes to update on the pool represented - by ``pool``. + by ``pool``. :returns: The updated pool :rtype: :class:`~openstack.network.v2.pool.Pool` @@ -1881,8 +1881,8 @@ def create_pool_member(self, pool, **attrs): """Create a new pool member from attributes :param pool: The pool can be either the ID of a pool or a - :class:`~openstack.network.v2.pool.Pool` instance that - the member will be created in. + :class:`~openstack.network.v2.pool.Pool` instance that + the member will be created in. :param dict attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.pool_member.PoolMember`, comprised of the properties on the PoolMember class. @@ -1901,13 +1901,13 @@ def delete_pool_member(self, pool_member, pool, ignore_missing=True): The member can be either the ID of a pool member or a :class:`~openstack.network.v2.pool_member.PoolMember` instance. :param pool: The pool can be either the ID of a pool or a - :class:`~openstack.network.v2.pool.Pool` instance that - the member belongs to. + :class:`~openstack.network.v2.pool.Pool` instance that + the member belongs to. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the pool member does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent pool member. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the pool member does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent pool member. :returns: ``None`` """ @@ -1920,17 +1920,17 @@ def find_pool_member(self, name_or_id, pool, ignore_missing=True, **args): :param str name_or_id: The name or ID of a pool member. :param pool: The pool can be either the ID of a pool or a - :class:`~openstack.network.v2.pool.Pool` instance that - the member belongs to. + :class:`~openstack.network.v2.pool.Pool` instance that + the member belongs to. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.pool_member.PoolMember` - or None + or None """ poolobj = self._get_resource(_pool.Pool, pool) return self._find(_pool_member.PoolMember, name_or_id, @@ -1941,15 +1941,15 @@ def get_pool_member(self, pool_member, pool): """Get a single pool member :param pool_member: The member can be the ID of a pool member or a - :class:`~openstack.network.v2.pool_member.PoolMember` - instance. + :class:`~openstack.network.v2.pool_member.PoolMember` + instance. :param pool: The pool can be either the ID of a pool or a - :class:`~openstack.network.v2.pool.Pool` instance that - the member belongs to. + :class:`~openstack.network.v2.pool.Pool` instance that + the member belongs to. :returns: One :class:`~openstack.network.v2.pool_member.PoolMember` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ poolobj = self._get_resource(_pool.Pool, pool) return self._get(_pool_member.PoolMember, pool_member, @@ -1959,22 +1959,22 @@ def pool_members(self, pool, **query): """Return a generator of pool members :param pool: The pool can be either the ID of a pool or a - :class:`~openstack.network.v2.pool.Pool` instance that - the member belongs to. + :class:`~openstack.network.v2.pool.Pool` instance that + the member belongs to. :param dict query: Optional query parameters to be sent to limit - the resources being returned. Valid parameters are: + the resources being returned. Valid parameters are: * ``address``: The IP address of the pool member. * ``is_admin_state_up``: The administrative state of the pool - member. + member. * ``name``: Name of the pool member. * ``project_id``: The ID of the project this pool member is - associated with. + associated with. * ``protocol_port``: The port on which the application is hosted. * ``subnet_id``: Subnet ID in which to access this pool member. * ``weight``: A positive integer value that indicates the relative - portion of traffic that this member should receive from the - pool. + portion of traffic that this member should receive from the + pool. :returns: A generator of pool member objects :rtype: :class:`~openstack.network.v2.pool_member.PoolMember` @@ -1986,13 +1986,13 @@ def update_pool_member(self, pool_member, pool, **attrs): """Update a pool member :param pool_member: Either the ID of a pool member or a - :class:`~openstack.network.v2.pool_member.PoolMember` - instance. + :class:`~openstack.network.v2.pool_member.PoolMember` + instance. :param pool: The pool can be either the ID of a pool or a - :class:`~openstack.network.v2.pool.Pool` instance that - the member belongs to. + :class:`~openstack.network.v2.pool.Pool` instance that + the member belongs to. :param dict attrs: The attributes to update on the pool member - represented by ``pool_member``. + represented by ``pool_member``. :returns: The updated pool member :rtype: :class:`~openstack.network.v2.pool_member.PoolMember` @@ -2005,8 +2005,8 @@ def create_port(self, **attrs): """Create a new port from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.network.v2.port.Port`, - comprised of the properties on the Port class. + a :class:`~openstack.network.v2.port.Port`, + comprised of the properties on the Port class. :returns: The results of port creation :rtype: :class:`~openstack.network.v2.port.Port` @@ -2017,8 +2017,8 @@ def create_ports(self, data): """Create ports from the list of attributes :param list data: List of dicts of attributes which will be used to - create a :class:`~openstack.network.v2.port.Port`, - comprised of the properties on the Port class. + create a :class:`~openstack.network.v2.port.Port`, + comprised of the properties on the Port class. :returns: A generator of port objects :rtype: :class:`~openstack.network.v2.port.Port` @@ -2029,14 +2029,14 @@ def delete_port(self, port, ignore_missing=True, if_revision=None): """Delete a port :param port: The value can be either the ID of a port or a - :class:`~openstack.network.v2.port.Port` instance. + :class:`~openstack.network.v2.port.Port` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the port does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent port. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the port does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent port. :param int if_revision: Revision to put in If-Match header of update - request to perform compare-and-swap update. + request to perform compare-and-swap update. :returns: ``None`` """ @@ -2048,12 +2048,12 @@ def find_port(self, name_or_id, ignore_missing=True, **args): :param name_or_id: The name or ID of a port. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.port.Port` or None """ return self._find(_port.Port, name_or_id, @@ -2063,11 +2063,11 @@ def get_port(self, port): """Get a single port :param port: The value can be the ID of a port or a - :class:`~openstack.network.v2.port.Port` instance. + :class:`~openstack.network.v2.port.Port` instance. :returns: One :class:`~openstack.network.v2.port.Port` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_port.Port, port) @@ -2099,11 +2099,11 @@ def update_port(self, port, if_revision=None, **attrs): """Update a port :param port: Either the id of a port or a - :class:`~openstack.network.v2.port.Port` instance. + :class:`~openstack.network.v2.port.Port` instance. :param int if_revision: Revision to put in If-Match header of update - request to perform compare-and-swap update. + request to perform compare-and-swap update. :param dict attrs: The attributes to update on the port represented - by ``port``. + by ``port``. :returns: The updated port :rtype: :class:`~openstack.network.v2.port.Port` @@ -2132,17 +2132,17 @@ def create_qos_bandwidth_limit_rule(self, qos_policy, **attrs): """Create a new bandwidth limit rule :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.network.v2. - qos_bandwidth_limit_rule.QoSBandwidthLimitRule`, - comprised of the properties on the - QoSBandwidthLimitRule class. + a :class:`~openstack.network.v2. + qos_bandwidth_limit_rule.QoSBandwidthLimitRule`, + comprised of the properties on the + QoSBandwidthLimitRule class. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a :class:`~openstack.network.v2. + qos_policy.QoSPolicy` instance. :returns: The results of resource creation :rtype: :class:`~openstack.network.v2.qos_bandwidth_limit_rule. - QoSBandwidthLimitRule` + QoSBandwidthLimitRule` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._create(_qos_bandwidth_limit_rule.QoSBandwidthLimitRule, @@ -2153,17 +2153,17 @@ def delete_qos_bandwidth_limit_rule(self, qos_rule, qos_policy, """Delete a bandwidth limit rule :param qos_rule: The value can be either the ID of a bandwidth limit - rule or a :class:`~openstack.network.v2. - qos_bandwidth_limit_rule.QoSBandwidthLimitRule` - instance. + rule or a :class:`~openstack.network.v2. + qos_bandwidth_limit_rule.QoSBandwidthLimitRule` + instance. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a :class:`~openstack.network.v2. + qos_policy.QoSPolicy` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent bandwidth limit rule. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent bandwidth limit rule. :returns: ``None`` """ @@ -2178,17 +2178,17 @@ def find_qos_bandwidth_limit_rule(self, qos_rule_id, qos_policy, :param qos_rule_id: The ID of a bandwidth limit rule. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a :class:`~openstack.network.v2. + qos_policy.QoSPolicy` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.qos_bandwidth_limit_rule. - QoSBandwidthLimitRule` or None + QoSBandwidthLimitRule` or None """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._find(_qos_bandwidth_limit_rule.QoSBandwidthLimitRule, @@ -2199,16 +2199,16 @@ def get_qos_bandwidth_limit_rule(self, qos_rule, qos_policy): """Get a single bandwidth limit rule :param qos_rule: The value can be the ID of a minimum bandwidth rule or - a :class:`~openstack.network.v2. - qos_bandwidth_limit_rule.QoSBandwidthLimitRule` - instance. + a :class:`~openstack.network.v2. + qos_bandwidth_limit_rule.QoSBandwidthLimitRule` + instance. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a :class:`~openstack.network.v2. + qos_policy.QoSPolicy` instance. :returns: One :class:`~openstack.network.v2.qos_bandwidth_limit_rule. - QoSBandwidthLimitRule` + QoSBandwidthLimitRule` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._get(_qos_bandwidth_limit_rule.QoSBandwidthLimitRule, @@ -2218,13 +2218,13 @@ def qos_bandwidth_limit_rules(self, qos_policy, **query): """Return a generator of bandwidth limit rules :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a :class:`~openstack.network.v2. + qos_policy.QoSPolicy` instance. :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of bandwidth limit rule objects :rtype: :class:`~openstack.network.v2.qos_bandwidth_limit_rule. - QoSBandwidthLimitRule` + QoSBandwidthLimitRule` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._list(_qos_bandwidth_limit_rule.QoSBandwidthLimitRule, @@ -2235,18 +2235,18 @@ def update_qos_bandwidth_limit_rule(self, qos_rule, qos_policy, """Update a bandwidth limit rule :param qos_rule: Either the id of a bandwidth limit rule or a - :class:`~openstack.network.v2. - qos_bandwidth_limit_rule.QoSBandwidthLimitRule` - instance. + :class:`~openstack.network.v2. + qos_bandwidth_limit_rule.QoSBandwidthLimitRule` + instance. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a :class:`~openstack.network.v2. + qos_policy.QoSPolicy` instance. :attrs kwargs: The attributes to update on the bandwidth limit rule - represented by ``value``. + represented by ``value``. :returns: The updated minimum bandwidth rule :rtype: :class:`~openstack.network.v2.qos_bandwidth_limit_rule. - QoSBandwidthLimitRule` + QoSBandwidthLimitRule` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._update(_qos_bandwidth_limit_rule.QoSBandwidthLimitRule, @@ -2256,17 +2256,17 @@ def create_qos_dscp_marking_rule(self, qos_policy, **attrs): """Create a new QoS DSCP marking rule :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.network.v2. - qos_dscp_marking_rule.QoSDSCPMarkingRule`, - comprised of the properties on the - QosDscpMarkingRule class. + a :class:`~openstack.network.v2. + qos_dscp_marking_rule.QoSDSCPMarkingRule`, + comprised of the properties on the + QosDscpMarkingRule class. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a :class:`~openstack.network.v2. + qos_policy.QoSPolicy` instance. :returns: The results of router creation :rtype: :class:`~openstack.network.v2.qos_dscp_marking_rule. - QoSDSCPMarkingRule` + QoSDSCPMarkingRule` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._create(_qos_dscp_marking_rule.QoSDSCPMarkingRule, @@ -2277,17 +2277,17 @@ def delete_qos_dscp_marking_rule(self, qos_rule, qos_policy, """Delete a QoS DSCP marking rule :param qos_rule: The value can be either the ID of a minimum bandwidth - rule or a :class:`~openstack.network.v2. - qos_dscp_marking_rule.QoSDSCPMarkingRule` - instance. + rule or a :class:`~openstack.network.v2. + qos_dscp_marking_rule.QoSDSCPMarkingRule` + instance. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a :class:`~openstack.network.v2. + qos_policy.QoSPolicy` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent minimum bandwidth rule. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent minimum bandwidth rule. :returns: ``None`` """ @@ -2302,17 +2302,17 @@ def find_qos_dscp_marking_rule(self, qos_rule_id, qos_policy, :param qos_rule_id: The ID of a QoS DSCP marking rule. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a :class:`~openstack.network.v2. + qos_policy.QoSPolicy` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.qos_dscp_marking_rule. - QoSDSCPMarkingRule` or None + QoSDSCPMarkingRule` or None """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._find(_qos_dscp_marking_rule.QoSDSCPMarkingRule, @@ -2323,15 +2323,15 @@ def get_qos_dscp_marking_rule(self, qos_rule, qos_policy): """Get a single QoS DSCP marking rule :param qos_rule: The value can be the ID of a minimum bandwidth rule or - a :class:`~openstack.network.v2.qos_dscp_marking_rule. - QoSDSCPMarkingRule` instance. + a :class:`~openstack.network.v2.qos_dscp_marking_rule. + QoSDSCPMarkingRule` instance. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a :class:`~openstack.network.v2. + qos_policy.QoSPolicy` instance. :returns: One :class:`~openstack.network.v2.qos_dscp_marking_rule. - QoSDSCPMarkingRule` + QoSDSCPMarkingRule` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._get(_qos_dscp_marking_rule.QoSDSCPMarkingRule, @@ -2341,13 +2341,13 @@ def qos_dscp_marking_rules(self, qos_policy, **query): """Return a generator of QoS DSCP marking rules :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a :class:`~openstack.network.v2. + qos_policy.QoSPolicy` instance. :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of QoS DSCP marking rule objects :rtype: :class:`~openstack.network.v2.qos_dscp_marking_rule. - QoSDSCPMarkingRule` + QoSDSCPMarkingRule` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._list(_qos_dscp_marking_rule.QoSDSCPMarkingRule, @@ -2357,17 +2357,17 @@ def update_qos_dscp_marking_rule(self, qos_rule, qos_policy, **attrs): """Update a QoS DSCP marking rule :param qos_rule: Either the id of a minimum bandwidth rule or a - :class:`~openstack.network.v2.qos_dscp_marking_rule. - QoSDSCPMarkingRule` instance. + :class:`~openstack.network.v2.qos_dscp_marking_rule. + QoSDSCPMarkingRule` instance. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a :class:`~openstack.network.v2. + qos_policy.QoSPolicy` instance. :attrs kwargs: The attributes to update on the QoS DSCP marking rule - represented by ``value``. + represented by ``value``. :returns: The updated QoS DSCP marking rule :rtype: :class:`~openstack.network.v2.qos_dscp_marking_rule. - QoSDSCPMarkingRule` + QoSDSCPMarkingRule` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._update(_qos_dscp_marking_rule.QoSDSCPMarkingRule, @@ -2377,17 +2377,17 @@ def create_qos_minimum_bandwidth_rule(self, qos_policy, **attrs): """Create a new minimum bandwidth rule :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.network.v2. - qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule`, - comprised of the properties on the - QoSMinimumBandwidthRule class. + a :class:`~openstack.network.v2. + qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule`, + comprised of the properties on the + QoSMinimumBandwidthRule class. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a :class:`~openstack.network.v2. + qos_policy.QoSPolicy` instance. :returns: The results of resource creation :rtype: :class:`~openstack.network.v2.qos_minimum_bandwidth_rule. - QoSMinimumBandwidthRule` + QoSMinimumBandwidthRule` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._create( @@ -2399,17 +2399,17 @@ def delete_qos_minimum_bandwidth_rule(self, qos_rule, qos_policy, """Delete a minimum bandwidth rule :param qos_rule: The value can be either the ID of a minimum bandwidth - rule or a :class:`~openstack.network.v2. - qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule` - instance. + rule or a :class:`~openstack.network.v2. + qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule` + instance. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a :class:`~openstack.network.v2. + qos_policy.QoSPolicy` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent minimum bandwidth rule. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent minimum bandwidth rule. :returns: ``None`` """ @@ -2424,17 +2424,17 @@ def find_qos_minimum_bandwidth_rule(self, qos_rule_id, qos_policy, :param qos_rule_id: The ID of a minimum bandwidth rule. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a :class:`~openstack.network.v2. + qos_policy.QoSPolicy` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.qos_minimum_bandwidth_rule. - QoSMinimumBandwidthRule` or None + QoSMinimumBandwidthRule` or None """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._find(_qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, @@ -2445,16 +2445,16 @@ def get_qos_minimum_bandwidth_rule(self, qos_rule, qos_policy): """Get a single minimum bandwidth rule :param qos_rule: The value can be the ID of a minimum bandwidth rule or - a :class:`~openstack.network.v2. - qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule` - instance. + a :class:`~openstack.network.v2. + qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule` + instance. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a :class:`~openstack.network.v2. + qos_policy.QoSPolicy` instance. :returns: One :class:`~openstack.network.v2.qos_minimum_bandwidth_rule. - QoSMinimumBandwidthRule` + QoSMinimumBandwidthRule` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._get(_qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, @@ -2464,13 +2464,13 @@ def qos_minimum_bandwidth_rules(self, qos_policy, **query): """Return a generator of minimum bandwidth rules :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a :class:`~openstack.network.v2. + qos_policy.QoSPolicy` instance. :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of minimum bandwidth rule objects :rtype: :class:`~openstack.network.v2.qos_minimum_bandwidth_rule. - QoSMinimumBandwidthRule` + QoSMinimumBandwidthRule` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._list(_qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, @@ -2481,18 +2481,18 @@ def update_qos_minimum_bandwidth_rule(self, qos_rule, qos_policy, """Update a minimum bandwidth rule :param qos_rule: Either the id of a minimum bandwidth rule or a - :class:`~openstack.network.v2. - qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule` - instance. + :class:`~openstack.network.v2. + qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule` + instance. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a :class:`~openstack.network.v2. + qos_policy.QoSPolicy` instance. :attrs kwargs: The attributes to update on the minimum bandwidth rule - represented by ``value``. + represented by ``value``. :returns: The updated minimum bandwidth rule :rtype: :class:`~openstack.network.v2.qos_minimum_bandwidth_rule. - QoSMinimumBandwidthRule` + QoSMinimumBandwidthRule` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._update(_qos_minimum_bandwidth_rule. @@ -2503,9 +2503,9 @@ def create_qos_policy(self, **attrs): """Create a new QoS policy from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.network.v2.qos_policy. - QoSPolicy`, comprised of the properties on the - QoSPolicy class. + a :class:`~openstack.network.v2.qos_policy. + QoSPolicy`, comprised of the properties on the + QoSPolicy class. :returns: The results of QoS policy creation :rtype: :class:`~openstack.network.v2.qos_policy.QoSPolicy` @@ -2516,13 +2516,13 @@ def delete_qos_policy(self, qos_policy, ignore_missing=True): """Delete a QoS policy :param qos_policy: The value can be either the ID of a QoS policy or a - :class:`~openstack.network.v2.qos_policy.QoSPolicy` - instance. + :class:`~openstack.network.v2.qos_policy.QoSPolicy` + instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the QoS policy does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent QoS policy. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the QoS policy does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent QoS policy. :returns: ``None`` """ @@ -2534,14 +2534,14 @@ def find_qos_policy(self, name_or_id, ignore_missing=True, **args): :param name_or_id: The name or ID of a QoS policy. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.qos_policy.QoSPolicy` or - None + None """ return self._find(_qos_policy.QoSPolicy, name_or_id, ignore_missing=ignore_missing, **args) @@ -2550,12 +2550,12 @@ def get_qos_policy(self, qos_policy): """Get a single QoS policy :param qos_policy: The value can be the ID of a QoS policy or a - :class:`~openstack.network.v2.qos_policy.QoSPolicy` - instance. + :class:`~openstack.network.v2.qos_policy.QoSPolicy` + instance. :returns: One :class:`~openstack.network.v2.qos_policy.QoSPolicy` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_qos_policy.QoSPolicy, qos_policy) @@ -2563,7 +2563,7 @@ def qos_policies(self, **query): """Return a generator of QoS policies :param dict query: Optional query parameters to be sent to limit - the resources being returned. Valid parameters are: + the resources being returned. Valid parameters are: * ``description``: The description of a QoS policy. * ``is_shared``: Whether the policy is shared among projects. @@ -2579,10 +2579,10 @@ def update_qos_policy(self, qos_policy, **attrs): """Update a QoS policy :param qos_policy: Either the id of a QoS policy or a - :class:`~openstack.network.v2.qos_policy.QoSPolicy` - instance. + :class:`~openstack.network.v2.qos_policy.QoSPolicy` + instance. :attrs kwargs: The attributes to update on the QoS policy represented - by ``value``. + by ``value``. :returns: The updated QoS policy :rtype: :class:`~openstack.network.v2.qos_policy.QoSPolicy` @@ -2594,12 +2594,12 @@ def find_qos_rule_type(self, rule_type_name, ignore_missing=True): :param rule_type_name: The name of a QoS rule type. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.network.v2.qos_rule_type.QoSRuleType` - or None + or None """ return self._find(_qos_rule_type.QoSRuleType, rule_type_name, ignore_missing=ignore_missing) @@ -2608,14 +2608,14 @@ def get_qos_rule_type(self, qos_rule_type): """Get details about single QoS rule type :param qos_rule_type: The value can be the name of a QoS policy - rule type or a - :class:`~openstack.network.v2. - qos_rule_type.QoSRuleType` - instance. + rule type or a + :class:`~openstack.network.v2. + qos_rule_type.QoSRuleType` + instance. :returns: One :class:`~openstack.network.v2.qos_rule_type.QoSRuleType` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_qos_rule_type.QoSRuleType, qos_rule_type) @@ -2623,7 +2623,7 @@ def qos_rule_types(self, **query): """Return a generator of QoS rule types :param dict query: Optional query parameters to be sent to limit the - resources returned. Valid parameters include: + resources returned. Valid parameters include: * ``type``: The type of the QoS rule type. @@ -2636,14 +2636,14 @@ def delete_quota(self, quota, ignore_missing=True): """Delete a quota (i.e. reset to the default quota) :param quota: The value can be either the ID of a quota or a - :class:`~openstack.network.v2.quota.Quota` instance. - The ID of a quota is the same as the project ID - for the quota. + :class:`~openstack.network.v2.quota.Quota` instance. + The ID of a quota is the same as the project ID + for the quota. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when quota does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent quota. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when quota does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent quota. :returns: ``None`` """ @@ -2653,15 +2653,15 @@ def get_quota(self, quota, details=False): """Get a quota :param quota: The value can be the ID of a quota or a - :class:`~openstack.network.v2.quota.Quota` instance. - The ID of a quota is the same as the project ID - for the quota. + :class:`~openstack.network.v2.quota.Quota` instance. + The ID of a quota is the same as the project ID + for the quota. :param details: If set to True, details about quota usage will - be returned. + be returned. :returns: One :class:`~openstack.network.v2.quota.Quota` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ if details: quota_obj = self._get_resource(_quota.Quota, quota) @@ -2675,13 +2675,13 @@ def get_quota_default(self, quota): """Get a default quota :param quota: The value can be the ID of a default quota or a - :class:`~openstack.network.v2.quota.QuotaDefault` - instance. The ID of a default quota is the same - as the project ID for the default quota. + :class:`~openstack.network.v2.quota.QuotaDefault` + instance. The ID of a default quota is the same + as the project ID for the default quota. :returns: One :class:`~openstack.network.v2.quota.QuotaDefault` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ quota_obj = self._get_resource(_quota.Quota, quota) return self._get(_quota.QuotaDefault, project=quota_obj.id, @@ -2691,8 +2691,8 @@ def quotas(self, **query): """Return a generator of quotas :param dict query: Optional query parameters to be sent to limit - the resources being returned. Currently no query - parameter is supported. + the resources being returned. Currently no query + parameter is supported. :returns: A generator of quota objects :rtype: :class:`~openstack.network.v2.quota.Quota` @@ -2703,11 +2703,11 @@ def update_quota(self, quota, **attrs): """Update a quota :param quota: Either the ID of a quota or a - :class:`~openstack.network.v2.quota.Quota` instance. - The ID of a quota is the same as the project ID - for the quota. + :class:`~openstack.network.v2.quota.Quota` instance. + The ID of a quota is the same as the project ID + for the quota. :param dict attrs: The attributes to update on the quota represented - by ``quota``. + by ``quota``. :returns: The updated quota :rtype: :class:`~openstack.network.v2.quota.Quota` @@ -2752,7 +2752,7 @@ def find_rbac_policy(self, rbac_policy, ignore_missing=True, **args): When set to ``True``, None will be returned when attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.rbac_policy.RBACPolicy` or None """ @@ -2775,13 +2775,13 @@ def rbac_policies(self, **query): """Return a generator of RBAC policies :param dict query: Optional query parameters to be sent to limit - the resources being returned. Available parameters - include: + the resources being returned. Available parameters + include: * ``action``: RBAC policy action * ``object_type``: Type of the object that the RBAC policy affects * ``target_project_id``: ID of the tenant that the RBAC policy - affects + affects * ``project_id``: Owner tenant ID :returns: A generator of rbac objects @@ -2795,7 +2795,7 @@ def update_rbac_policy(self, rbac_policy, **attrs): :param rbac_policy: Either the id of a RBAC policy or a :class:`~openstack.network.v2.rbac_policy.RBACPolicy` instance. :param dict attrs: The attributes to update on the RBAC policy - represented by ``rbac_policy``. + represented by ``rbac_policy``. :returns: The updated RBAC policy :rtype: :class:`~openstack.network.v2.rbac_policy.RBACPolicy` @@ -2806,8 +2806,8 @@ def create_router(self, **attrs): """Create a new router from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.network.v2.router.Router`, - comprised of the properties on the Router class. + a :class:`~openstack.network.v2.router.Router`, + comprised of the properties on the Router class. :returns: The results of router creation :rtype: :class:`~openstack.network.v2.router.Router` @@ -2818,14 +2818,14 @@ def delete_router(self, router, ignore_missing=True, if_revision=None): """Delete a router :param router: The value can be either the ID of a router or a - :class:`~openstack.network.v2.router.Router` instance. + :class:`~openstack.network.v2.router.Router` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the router does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent router. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the router does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent router. :param int if_revision: Revision to put in If-Match header of update - request to perform compare-and-swap update. + request to perform compare-and-swap update. :returns: ``None`` """ @@ -2837,12 +2837,12 @@ def find_router(self, name_or_id, ignore_missing=True, **args): :param name_or_id: The name or ID of a router. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.router.Router` or None """ return self._find(_router.Router, name_or_id, @@ -2852,11 +2852,11 @@ def get_router(self, router): """Get a single router :param router: The value can be the ID of a router or a - :class:`~openstack.network.v2.router.Router` instance. + :class:`~openstack.network.v2.router.Router` instance. :returns: One :class:`~openstack.network.v2.router.Router` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_router.Router, router) @@ -2864,7 +2864,7 @@ def routers(self, **query): """Return a generator of routers :param dict query: Optional query parameters to be sent to limit - the resources being returned. Valid parameters are: + the resources being returned. Valid parameters are: * ``description``: The description of a router. * ``flavor_id``: The ID of the flavor. @@ -2873,7 +2873,7 @@ def routers(self, **query): * ``is_ha``: The highly-available state of a router * ``name``: Router name * ``project_id``: The ID of the project this router is associated - with. + with. * ``status``: The status of the router. :returns: A generator of router objects @@ -2885,11 +2885,11 @@ def update_router(self, router, if_revision=None, **attrs): """Update a router :param router: Either the id of a router or a - :class:`~openstack.network.v2.router.Router` instance. + :class:`~openstack.network.v2.router.Router` instance. :param int if_revision: Revision to put in If-Match header of update - request to perform compare-and-swap update. + request to perform compare-and-swap update. :param dict attrs: The attributes to update on the router represented - by ``router``. + by ``router``. :returns: The updated router :rtype: :class:`~openstack.network.v2.router.Router` @@ -2901,7 +2901,7 @@ def add_interface_to_router(self, router, subnet_id=None, port_id=None): """Add Interface to a router :param router: Either the router ID or an instance of - :class:`~openstack.network.v2.router.Router` + :class:`~openstack.network.v2.router.Router` :param subnet_id: ID of the subnet :param port_id: ID of the port :returns: Router with updated interface @@ -2920,7 +2920,7 @@ def remove_interface_from_router(self, router, subnet_id=None, """Remove Interface from a router :param router: Either the router ID or an instance of - :class:`~openstack.network.v2.router.Router` + :class:`~openstack.network.v2.router.Router` :param subnet: ID of the subnet :param port: ID of the port :returns: Router with updated interface @@ -2939,7 +2939,7 @@ def add_extra_routes_to_router(self, router, body): """Add extra routes to a router :param router: Either the router ID or an instance of - :class:`~openstack.network.v2.router.Router` + :class:`~openstack.network.v2.router.Router` :param body: The request body as documented in the api-ref. :returns: Router with updated extra routes :rtype: :class: `~openstack.network.v2.router.Router` @@ -2951,7 +2951,7 @@ def remove_extra_routes_from_router(self, router, body): """Remove extra routes from a router :param router: Either the router ID or an instance of - :class:`~openstack.network.v2.router.Router` + :class:`~openstack.network.v2.router.Router` :param body: The request body as documented in the api-ref. :returns: Router with updated extra routes :rtype: :class: `~openstack.network.v2.router.Router` @@ -2963,7 +2963,7 @@ def add_gateway_to_router(self, router, **body): """Add Gateway to a router :param router: Either the router ID or an instance of - :class:`~openstack.network.v2.router.Router` + :class:`~openstack.network.v2.router.Router` :param body: Body with the gateway information :returns: Router with updated interface :rtype: :class: `~openstack.network.v2.router.Router` @@ -2975,7 +2975,7 @@ def remove_gateway_from_router(self, router, **body): """Remove Gateway from a router :param router: Either the router ID or an instance of - :class:`~openstack.network.v2.router.Router` + :class:`~openstack.network.v2.router.Router` :param body: Body with the gateway information :returns: Router with updated interface :rtype: :class: `~openstack.network.v2.router.Router` @@ -2987,9 +2987,9 @@ def routers_hosting_l3_agents(self, router, **query): """Return a generator of L3 agent hosting a router :param router: Either the router id or an instance of - :class:`~openstack.network.v2.router.Router` + :class:`~openstack.network.v2.router.Router` :param kwargs query: Optional query parameters to be sent to limit - the resources returned + the resources returned :returns: A generator of Router L3 Agents :rtype: :class:`~openstack.network.v2.router.RouterL3Agents` @@ -3001,9 +3001,9 @@ def agent_hosted_routers(self, agent, **query): """Return a generator of routers hosted by a L3 agent :param agent: Either the agent id of an instance of - :class:`~openstack.network.v2.network_agent.Agent` + :class:`~openstack.network.v2.network_agent.Agent` :param kwargs query: Optional query parameters to be sent to limit - the resources returned + the resources returned :returns: A generator of routers :rtype: :class:`~openstack.network.v2.agent.L3AgentRouters` @@ -3015,7 +3015,7 @@ def add_router_to_agent(self, agent, router): """Add router to L3 agent :param agent: Either the id of an agent - :class:`~openstack.network.v2.agent.Agent` instance + :class:`~openstack.network.v2.agent.Agent` instance :param router: A router instance :returns: Agent with attached router :rtype: :class:`~openstack.network.v2.agent.Agent` @@ -3028,7 +3028,7 @@ def remove_router_from_agent(self, agent, router): """Remove router from L3 agent :param agent: Either the id of an agent or an - :class:`~openstack.network.v2.agent.Agent` instance + :class:`~openstack.network.v2.agent.Agent` instance :param router: A router instance :returns: Agent with removed router :rtype: :class:`~openstack.network.v2.agent.Agent` @@ -3057,10 +3057,10 @@ def delete_firewall_group(self, firewall_group, ignore_missing=True): :class:`~openstack.network.v2.firewall_group.FirewallGroup` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the firewall group does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent firewall group. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the firewall group does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent firewall group. :returns: ``None`` """ @@ -3072,14 +3072,14 @@ def find_firewall_group(self, name_or_id, ignore_missing=True, **args): :param name_or_id: The name or ID of a firewall group. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.firewall_group. - FirewallGroup` or None + FirewallGroup` or None """ return self._find(_firewall_group.FirewallGroup, name_or_id, ignore_missing=ignore_missing, **args) @@ -3088,13 +3088,13 @@ def get_firewall_group(self, firewall_group): """Get a single firewall group :param firewall_group: The value can be the ID of a firewall group or a - :class:`~openstack.network.v2.firewall_group.FirewallGroup` - instance. + :class:`~openstack.network.v2.firewall_group.FirewallGroup` + instance. :returns: One - :class:`~openstack.network.v2.firewall_group.FirewallGroup` + :class:`~openstack.network.v2.firewall_group.FirewallGroup` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_firewall_group.FirewallGroup, firewall_group) @@ -3102,21 +3102,21 @@ def firewall_groups(self, **query): """Return a generator of firewall_groups :param dict query: Optional query parameters to be sent to limit - the resources being returned. Valid parameters are: + the resources being returned. Valid parameters are: * ``description``: Firewall group description * ``egress_policy_id``: The ID of egress firewall policy * ``ingress_policy_id``: The ID of ingress firewall policy * ``name``: The name of a firewall group * ``shared``: Indicates whether this firewall group is shared - across all projects. + across all projects. * ``status``: The status of the firewall group. Valid values are - ACTIVE, INACTIVE, ERROR, PENDING_UPDATE, or - PENDING_DELETE. + ACTIVE, INACTIVE, ERROR, PENDING_UPDATE, or + PENDING_DELETE. * ``ports``: A list of the IDs of the ports associated with the - firewall group. + firewall group. * ``project_id``: The ID of the project this firewall group is - associated with. + associated with. :returns: A generator of firewall group objects """ @@ -3129,7 +3129,7 @@ def update_firewall_group(self, firewall_group, **attrs): :class:`~openstack.network.v2.firewall_group.FirewallGroup` instance. :param dict attrs: The attributes to update on the firewall group - represented by ``firewall_group``. + represented by ``firewall_group``. :returns: The updated firewall group :rtype: :class:`~openstack.network.v2.firewall_group.FirewallGroup` @@ -3157,10 +3157,10 @@ def delete_firewall_policy(self, firewall_policy, ignore_missing=True): :class:`~openstack.network.v2.firewall_policy.FirewallPolicy` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the firewall policy does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent firewall policy. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the firewall policy does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent firewall policy. :returns: ``None`` """ @@ -3172,14 +3172,14 @@ def find_firewall_policy(self, name_or_id, ignore_missing=True, **args): :param name_or_id: The name or ID of a firewall policy. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.firewall_policy. - FirewallPolicy` or None + FirewallPolicy` or None """ return self._find(_firewall_policy.FirewallPolicy, name_or_id, ignore_missing=ignore_missing, **args) @@ -3188,14 +3188,14 @@ def get_firewall_policy(self, firewall_policy): """Get a single firewall policy :param firewall_policy: The value can be the ID of a firewall policy - or a - :class:`~openstack.network.v2.firewall_policy.FirewallPolicy` - instance. + or a + :class:`~openstack.network.v2.firewall_policy.FirewallPolicy` + instance. :returns: One - :class:`~openstack.network.v2.firewall_policy.FirewallPolicy` + :class:`~openstack.network.v2.firewall_policy.FirewallPolicy` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_firewall_policy.FirewallPolicy, firewall_policy) @@ -3203,14 +3203,14 @@ def firewall_policies(self, **query): """Return a generator of firewall_policies :param dict query: Optional query parameters to be sent to limit - the resources being returned. Valid parameters are: + the resources being returned. Valid parameters are: * ``description``: Firewall policy description * ``firewall_rule``: A list of the IDs of the firewall rules - associated with the firewall policy. + associated with the firewall policy. * ``name``: The name of a firewall policy * ``shared``: Indicates whether this firewall policy is shared - across all projects. + across all projects. * ``project_id``: The ID of the project that owns the resource. :returns: A generator of firewall policy objects @@ -3224,7 +3224,7 @@ def update_firewall_policy(self, firewall_policy, **attrs): :class:`~openstack.network.v2.firewall_policy.FirewallPolicy` instance. :param dict attrs: The attributes to update on the firewall policy - represented by ``firewall_policy``. + represented by ``firewall_policy``. :returns: The updated firewall policy :rtype: :class:`~openstack.network.v2.firewall_policy.FirewallPolicy` @@ -3239,10 +3239,10 @@ def insert_rule_into_policy(self, firewall_policy_id, firewall_rule_id, :param firewall_policy_id: The ID of the firewall policy. :param firewall_rule_id: The ID of the firewall rule. :param insert_after: The ID of the firewall rule to insert the new - rule after. It will be worked only when - insert_before is none. + rule after. It will be worked only when + insert_before is none. :param insert_before: The ID of the firewall rule to insert the new - rule before. + rule before. :returns: The updated firewall policy :rtype: :class:`~openstack.network.v2.firewall_policy.FirewallPolicy` @@ -3288,10 +3288,10 @@ def delete_firewall_rule(self, firewall_rule, ignore_missing=True): :class:`~openstack.network.v2.firewall_rule.FirewallRule` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the firewall rule does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent firewall rule. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the firewall rule does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent firewall rule. :returns: ``None`` """ @@ -3303,14 +3303,14 @@ def find_firewall_rule(self, name_or_id, ignore_missing=True, **args): :param name_or_id: The name or ID of a firewall rule. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.firewall_rule. - FirewallRule` or None + FirewallRule` or None """ return self._find(_firewall_rule.FirewallRule, name_or_id, ignore_missing=ignore_missing, **args) @@ -3319,13 +3319,13 @@ def get_firewall_rule(self, firewall_rule): """Get a single firewall rule :param firewall_rule: The value can be the ID of a firewall rule or a - :class:`~openstack.network.v2.firewall_rule.FirewallRule` - instance. + :class:`~openstack.network.v2.firewall_rule.FirewallRule` + instance. :returns: One - :class:`~openstack.network.v2.firewall_rule.FirewallRule` + :class:`~openstack.network.v2.firewall_rule.FirewallRule` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_firewall_rule.FirewallRule, firewall_rule) @@ -3333,27 +3333,27 @@ def firewall_rules(self, **query): """Return a generator of firewall_rules :param dict query: Optional query parameters to be sent to limit - the resources being returned. Valid parameters are: + the resources being returned. Valid parameters are: * ``action``: The action that the API performs on traffic that - matches the firewall rule. + matches the firewall rule. * ``description``: Firewall rule description * ``name``: The name of a firewall group * ``destination_ip_address``: The destination IPv4 or IPv6 address - or CIDR for the firewall rule. + or CIDR for the firewall rule. * ``destination_port``: The destination port or port range for - the firewall rule. + the firewall rule. * ``enabled``: Facilitates selectively turning off rules. * ``shared``: Indicates whether this firewall group is shared - across all projects. + across all projects. * ``ip_version``: The IP protocol version for the firewall rule. * ``protocol``: The IP protocol for the firewall rule. * ``source_ip_address``: The source IPv4 or IPv6 address or CIDR - for the firewall rule. + for the firewall rule. * ``source_port``: The source port or port range for the firewall - rule. + rule. * ``project_id``: The ID of the project this firewall group is - associated with. + associated with. :returns: A generator of firewall rule objects """ @@ -3366,7 +3366,7 @@ def update_firewall_rule(self, firewall_rule, **attrs): :class:`~openstack.network.v2.firewall_rule.FirewallRule` instance. :param dict attrs: The attributes to update on the firewall rule - represented by ``firewall_rule``. + represented by ``firewall_rule``. :returns: The updated firewall rule :rtype: :class:`~openstack.network.v2.firewall_rule.FirewallRule` @@ -3395,12 +3395,12 @@ def delete_security_group(self, security_group, ignore_missing=True, :class:`~openstack.network.v2.security_group.SecurityGroup` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the security group does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent security group. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the security group does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent security group. :param int if_revision: Revision to put in If-Match header of update - request to perform compare-and-swap update. + request to perform compare-and-swap update. :returns: ``None`` """ @@ -3412,14 +3412,14 @@ def find_security_group(self, name_or_id, ignore_missing=True, **args): :param name_or_id: The name or ID of a security group. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.security_group. - SecurityGroup` or None + SecurityGroup` or None """ return self._find(_security_group.SecurityGroup, name_or_id, ignore_missing=ignore_missing, **args) @@ -3428,13 +3428,13 @@ def get_security_group(self, security_group): """Get a single security group :param security_group: The value can be the ID of a security group or a - :class:`~openstack.network.v2.security_group.SecurityGroup` - instance. + :class:`~openstack.network.v2.security_group.SecurityGroup` + instance. :returns: One - :class:`~openstack.network.v2.security_group.SecurityGroup` + :class:`~openstack.network.v2.security_group.SecurityGroup` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_security_group.SecurityGroup, security_group) @@ -3442,13 +3442,13 @@ def security_groups(self, **query): """Return a generator of security groups :param dict query: Optional query parameters to be sent to limit - the resources being returned. Valid parameters are: + the resources being returned. Valid parameters are: * ``description``: Security group description * ``ìd``: The id of a security group, or list of security group ids * ``name``: The name of a security group * ``project_id``: The ID of the project this security group is - associated with. + associated with. :returns: A generator of security group objects :rtype: :class:`~openstack.network.v2.security_group.SecurityGroup` @@ -3462,9 +3462,9 @@ def update_security_group(self, security_group, if_revision=None, **attrs): :class:`~openstack.network.v2.security_group.SecurityGroup` instance. :param int if_revision: Revision to put in If-Match header of update - request to perform compare-and-swap update. + request to perform compare-and-swap update. :param dict attrs: The attributes to update on the security group - represented by ``security_group``. + represented by ``security_group``. :returns: The updated security group :rtype: :class:`~openstack.network.v2.security_group.SecurityGroup` @@ -3490,10 +3490,10 @@ def create_security_group_rules(self, data): """Create new security group rules from the list of attributes :param list data: List of dicts of attributes which will be used to - create a :class:`~openstack.network.v2.\ - security_group_rule.SecurityGroupRule`, - comprised of the properties on the SecurityGroupRule - class. + create a :class:`~openstack.network.v2.\ + security_group_rule.SecurityGroupRule`, + comprised of the properties on the SecurityGroupRule + class. :returns: A generator of security group rule objects :rtype: :class:`~openstack.network.v2.security_group_rule.\ @@ -3510,12 +3510,12 @@ def delete_security_group_rule(self, security_group_rule, or a :class:`~openstack.network.v2.security_group_rule. SecurityGroupRule` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the security group rule does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent security group rule. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the security group rule does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent security group rule. :param int if_revision: Revision to put in If-Match header of update - request to perform compare-and-swap update. + request to perform compare-and-swap update. :returns: ``None`` """ @@ -3529,14 +3529,14 @@ def find_security_group_rule(self, name_or_id, ignore_missing=True, :param str name_or_id: The ID of a security group rule. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.security_group_rule. - SecurityGroupRule` or None + SecurityGroupRule` or None """ return self._find(_security_group_rule.SecurityGroupRule, name_or_id, ignore_missing=ignore_missing, **args) @@ -3552,7 +3552,7 @@ def get_security_group_rule(self, security_group_rule): :returns: :class:`~openstack.network.v2.security_group_rule.\ SecurityGroupRule` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_security_group_rule.SecurityGroupRule, security_group_rule) @@ -3566,16 +3566,16 @@ def security_group_rules(self, **query): * ``description``: The security group rule description * ``direction``: Security group rule direction * ``ether_type``: Must be IPv4 or IPv6, and addresses represented - in CIDR must match the ingress or egress rule. + in CIDR must match the ingress or egress rule. * ``project_id``: The ID of the project this security group rule - is associated with. + is associated with. * ``protocol``: Security group rule protocol * ``remote_group_id``: ID of a remote security group * ``security_group_id``: ID of security group that owns the rules :returns: A generator of security group rule objects :rtype: :class:`~openstack.network.v2.security_group_rule. - SecurityGroupRule` + SecurityGroupRule` """ return self._list(_security_group_rule.SecurityGroupRule, **query) @@ -3583,8 +3583,8 @@ def create_segment(self, **attrs): """Create a new segment from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.network.v2.segment.Segment`, - comprised of the properties on the Segment class. + a :class:`~openstack.network.v2.segment.Segment`, + comprised of the properties on the Segment class. :returns: The results of segment creation :rtype: :class:`~openstack.network.v2.segment.Segment` @@ -3595,13 +3595,13 @@ def delete_segment(self, segment, ignore_missing=True): """Delete a segment :param segment: The value can be either the ID of a segment or a - :class:`~openstack.network.v2.segment.Segment` - instance. + :class:`~openstack.network.v2.segment.Segment` + instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the segment does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent segment. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the segment does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent segment. :returns: ``None`` """ @@ -3612,12 +3612,12 @@ def find_segment(self, name_or_id, ignore_missing=True, **args): :param name_or_id: The name or ID of a segment. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.segment.Segment` or None """ return self._find(_segment.Segment, name_or_id, @@ -3627,12 +3627,12 @@ def get_segment(self, segment): """Get a single segment :param segment: The value can be the ID of a segment or a - :class:`~openstack.network.v2.segment.Segment` - instance. + :class:`~openstack.network.v2.segment.Segment` + instance. :returns: One :class:`~openstack.network.v2.segment.Segment` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_segment.Segment, segment) @@ -3658,10 +3658,10 @@ def update_segment(self, segment, **attrs): """Update a segment :param segment: Either the id of a segment or a - :class:`~openstack.network.v2.segment.Segment` - instance. + :class:`~openstack.network.v2.segment.Segment` + instance. :attrs kwargs: The attributes to update on the segment represented - by ``value``. + by ``value``. :returns: The update segment :rtype: :class:`~openstack.network.v2.segment.Segment` @@ -3672,7 +3672,7 @@ def service_providers(self, **query): """Return a generator of service providers :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of service provider objects :rtype: :class:`~openstack.network.v2.service_provider.ServiceProvider` @@ -3684,10 +3684,10 @@ def create_service_profile(self, **attrs): """Create a new network service flavor profile from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.network.v2.service_profile - .ServiceProfile`, - comprised of the properties on the ServiceProfile - class. + a :class:`~openstack.network.v2.service_profile + .ServiceProfile`, + comprised of the properties on the ServiceProfile + class. :returns: The results of service profile creation :rtype: :class:`~openstack.network.v2.service_profile.ServiceProfile` @@ -3698,14 +3698,14 @@ def delete_service_profile(self, service_profile, ignore_missing=True): """Delete a network service flavor profile :param service_profile: The value can be either the ID of a service - profile or a - :class:`~openstack.network.v2.service_profile - .ServiceProfile` instance. + profile or a + :class:`~openstack.network.v2.service_profile + .ServiceProfile` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the service profile does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent service profile. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the service profile does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent service profile. :returns: ``None`` """ @@ -3717,14 +3717,14 @@ def find_service_profile(self, name_or_id, ignore_missing=True, **args): :param name_or_id: The name or ID of a service profile. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.service_profile - .ServiceProfile` or None + .ServiceProfile` or None """ return self._find(_service_profile.ServiceProfile, name_or_id, ignore_missing=ignore_missing, **args) @@ -3737,9 +3737,9 @@ def get_service_profile(self, service_profile): instance. :returns: One :class:`~openstack.network.v2.service_profile - .ServiceProfile` + .ServiceProfile` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_service_profile.ServiceProfile, service_profile) @@ -3747,7 +3747,7 @@ def service_profiles(self, **query): """Return a generator of network service flavor profiles :param dict query: Optional query parameters to be sent to limit the - resources returned. Available parameters inclue: + resources returned. Available parameters inclue: * ``description``: The description of the service flavor profile * ``driver``: Provider driver for the service flavor profile @@ -3763,10 +3763,10 @@ def update_service_profile(self, service_profile, **attrs): """Update a network flavor service profile :param service_profile: Either the id of a service profile or a - :class:`~openstack.network.v2.service_profile - .ServiceProfile` instance. + :class:`~openstack.network.v2.service_profile + .ServiceProfile` instance. :attrs kwargs: The attributes to update on the service profile - represented by ``value``. + represented by ``value``. :returns: The updated service profile :rtype: :class:`~openstack.network.v2.service_profile.ServiceProfile` @@ -3778,8 +3778,8 @@ def create_subnet(self, **attrs): """Create a new subnet from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.network.v2.subnet.Subnet`, - comprised of the properties on the Subnet class. + a :class:`~openstack.network.v2.subnet.Subnet`, + comprised of the properties on the Subnet class. :returns: The results of subnet creation :rtype: :class:`~openstack.network.v2.subnet.Subnet` @@ -3790,14 +3790,14 @@ def delete_subnet(self, subnet, ignore_missing=True, if_revision=None): """Delete a subnet :param subnet: The value can be either the ID of a subnet or a - :class:`~openstack.network.v2.subnet.Subnet` instance. + :class:`~openstack.network.v2.subnet.Subnet` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the subnet does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent subnet. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the subnet does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent subnet. :param int if_revision: Revision to put in If-Match header of update - request to perform compare-and-swap update. + request to perform compare-and-swap update. :returns: ``None`` """ @@ -3809,12 +3809,12 @@ def find_subnet(self, name_or_id, ignore_missing=True, **args): :param name_or_id: The name or ID of a subnet. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.subnet.Subnet` or None """ return self._find(_subnet.Subnet, name_or_id, @@ -3824,11 +3824,11 @@ def get_subnet(self, subnet): """Get a single subnet :param subnet: The value can be the ID of a subnet or a - :class:`~openstack.network.v2.subnet.Subnet` instance. + :class:`~openstack.network.v2.subnet.Subnet` instance. :returns: One :class:`~openstack.network.v2.subnet.Subnet` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_subnet.Subnet, subnet) @@ -3849,7 +3849,7 @@ def subnets(self, **query): * ``network_id``: ID of network that owns the subnets * ``project_id``: Owner tenant ID * ``subnet_pool_id``: The subnet pool ID from which to obtain a - CIDR. + CIDR. :returns: A generator of subnet objects :rtype: :class:`~openstack.network.v2.subnet.Subnet` @@ -3860,11 +3860,11 @@ def update_subnet(self, subnet, if_revision=None, **attrs): """Update a subnet :param subnet: Either the id of a subnet or a - :class:`~openstack.network.v2.subnet.Subnet` instance. + :class:`~openstack.network.v2.subnet.Subnet` instance. :param int if_revision: Revision to put in If-Match header of update - request to perform compare-and-swap update. + request to perform compare-and-swap update. :param dict attrs: The attributes to update on the subnet represented - by ``subnet``. + by ``subnet``. :returns: The updated subnet :rtype: :class:`~openstack.network.v2.subnet.Subnet` @@ -3890,10 +3890,10 @@ def delete_subnet_pool(self, subnet_pool, ignore_missing=True): :param subnet_pool: The value can be either the ID of a subnet pool or a :class:`~openstack.network.v2.subnet_pool.SubnetPool` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the subnet pool does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent subnet pool. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the subnet pool does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent subnet pool. :returns: ``None`` """ @@ -3905,14 +3905,14 @@ def find_subnet_pool(self, name_or_id, ignore_missing=True, **args): :param name_or_id: The name or ID of a subnet pool. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.subnet_pool.SubnetPool` - or None + or None """ return self._find(_subnet_pool.SubnetPool, name_or_id, ignore_missing=ignore_missing, **args) @@ -3925,7 +3925,7 @@ def get_subnet_pool(self, subnet_pool): :returns: One :class:`~openstack.network.v2.subnet_pool.SubnetPool` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_subnet_pool.SubnetPool, subnet_pool) @@ -3954,7 +3954,7 @@ def update_subnet_pool(self, subnet_pool, **attrs): :param subnet_pool: Either the ID of a subnet pool or a :class:`~openstack.network.v2.subnet_pool.SubnetPool` instance. :param dict attrs: The attributes to update on the subnet pool - represented by ``subnet_pool``. + represented by ``subnet_pool``. :returns: The updated subnet pool :rtype: :class:`~openstack.network.v2.subnet_pool.SubnetPool` @@ -4012,14 +4012,14 @@ def find_trunk(self, name_or_id, ignore_missing=True, **args): :param name_or_id: The name or ID of a trunk. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.trunk.Trunk` - or None + or None """ return self._find(_trunk.Trunk, name_or_id, ignore_missing=ignore_missing, **args) @@ -4028,12 +4028,12 @@ def get_trunk(self, trunk): """Get a single trunk :param trunk: The value can be the ID of a trunk or a - :class:`~openstack.network.v2.trunk.Trunk` instance. + :class:`~openstack.network.v2.trunk.Trunk` instance. :returns: One - :class:`~openstack.network.v2.trunk.Trunk` + :class:`~openstack.network.v2.trunk.Trunk` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_trunk.Trunk, trunk) @@ -4041,7 +4041,7 @@ def trunks(self, **query): """Return a generator of trunks :param dict query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of trunk objects :rtype: :class:`~openstack.network.v2.trunk.trunk` @@ -4054,7 +4054,7 @@ def update_trunk(self, trunk, **attrs): :param trunk: Either the id of a trunk or a :class:`~openstack.network.v2.trunk.Trunk` instance. :param dict attrs: The attributes to update on the trunk - represented by ``trunk``. + represented by ``trunk``. :returns: The updated trunk :rtype: :class:`~openstack.network.v2.trunk.Trunk` @@ -4065,7 +4065,7 @@ def add_trunk_subports(self, trunk, subports): """Set sub_ports on trunk :param trunk: The value can be the ID of a trunk or a - :class:`~openstack.network.v2.trunk.Trunk` instance. + :class:`~openstack.network.v2.trunk.Trunk` instance. :param subports: New subports to be set. :type subports: "list" @@ -4079,7 +4079,7 @@ def delete_trunk_subports(self, trunk, subports): """Remove sub_ports from trunk :param trunk: The value can be the ID of a trunk or a - :class:`~openstack.network.v2.trunk.Trunk` instance. + :class:`~openstack.network.v2.trunk.Trunk` instance. :param subports: Subports to be removed. :type subports: "list" @@ -4093,7 +4093,7 @@ def get_trunk_subports(self, trunk): """Get sub_ports configured on trunk :param trunk: The value can be the ID of a trunk or a - :class:`~openstack.network.v2.trunk.Trunk` instance. + :class:`~openstack.network.v2.trunk.Trunk` instance. :returns: Trunk sub_ports :rtype: "list" @@ -4120,10 +4120,10 @@ def delete_vpn_service(self, vpn_service, ignore_missing=True): The value can be either the ID of a vpn service or a :class:`~openstack.network.v2.vpn_service.VPNService` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the vpn service does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent vpn service. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the vpn service does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent vpn service. :returns: ``None`` """ @@ -4135,14 +4135,14 @@ def find_vpn_service(self, name_or_id, ignore_missing=True, **args): :param name_or_id: The name or ID of a vpn service. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.vpn_service.VPNService` - or None + or None """ return self._find(_vpn_service.VPNService, name_or_id, ignore_missing=ignore_missing, **args) @@ -4151,13 +4151,13 @@ def get_vpn_service(self, vpn_service): """Get a single vpn service :param vpn_service: The value can be the ID of a vpn service or a - :class:`~openstack.network.v2.vpn_service.VPNService` - instance. + :class:`~openstack.network.v2.vpn_service.VPNService` + instance. :returns: One - :class:`~openstack.network.v2.vpn_service.VPNService` + :class:`~openstack.network.v2.vpn_service.VPNService` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_vpn_service.VPNService, vpn_service) @@ -4165,7 +4165,7 @@ def vpn_services(self, **query): """Return a generator of vpn services :param dict query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of vpn service objects :rtype: :class:`~openstack.network.v2.vpn_service.VPNService` @@ -4178,7 +4178,7 @@ def update_vpn_service(self, vpn_service, **attrs): :param vpn_service: Either the id of a vpn service or a :class:`~openstack.network.v2.vpn_service.VPNService` instance. :param dict attrs: The attributes to update on the VPN service - represented by ``vpn_service``. + represented by ``vpn_service``. :returns: The updated vpnservice :rtype: :class:`~openstack.network.v2.vpn_service.VPNService` @@ -4189,8 +4189,8 @@ def create_floating_ip_port_forwarding(self, floating_ip, **attrs): """Create a new floating ip port forwarding from attributes :param floating_ip: The value can be either the ID of a floating ip - or a :class:`~openstack.network.v2.floating_ip.FloatingIP` - instance. + or a :class:`~openstack.network.v2.floating_ip.FloatingIP` + instance. :param dict attrs:Keyword arguments which will be used to create a:class:`~openstack.network.v2.port_forwarding.PortForwarding`, comprised of the properties on the PortForwarding class. @@ -4207,16 +4207,16 @@ def delete_floating_ip_port_forwarding(self, floating_ip, port_forwarding, """Delete a floating IP port forwarding. :param floating_ip: The value can be either the ID of a floating ip - or a :class:`~openstack.network.v2.floating_ip.FloatingIP` - instance. + or a :class:`~openstack.network.v2.floating_ip.FloatingIP` + instance. :param port_forwarding: The value can be either the ID of a port - forwarding or a :class:`~openstack.network.v2. - port_forwarding.PortForwarding`instance. + forwarding or a :class:`~openstack.network.v2. + port_forwarding.PortForwarding`instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the floating ip does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent ip. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the floating ip does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent ip. :returns: ``None`` """ @@ -4230,18 +4230,18 @@ def find_floating_ip_port_forwarding(self, floating_ip, port_forwarding_id, """Find a floating ip port forwarding :param floating_ip: The value can be the ID of the Floating IP that the - port forwarding belongs or a :class:`~openstack. - network.v2.floating_ip.FloatingIP` instance. + port forwarding belongs or a :class:`~openstack. + network.v2.floating_ip.FloatingIP` instance. :param port_forwarding_id: The ID of a port forwarding. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into - underlying methods. such as query filters. + underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.port_forwarding. - PortForwarding` or None + PortForwarding` or None """ floatingip = self._get_resource(_floating_ip.FloatingIP, floating_ip) return self._find(_port_forwarding.PortForwarding, @@ -4252,15 +4252,15 @@ def get_floating_ip_port_forwarding(self, floating_ip, port_forwarding): """Get a floating ip port forwarding :param floating_ip: The value can be the ID of the Floating IP that the - port forwarding belongs or a :class:`~openstack. - network.v2.floating_ip.FloatingIP` instance. + port forwarding belongs or a :class:`~openstack. + network.v2.floating_ip.FloatingIP` instance. :param port_forwarding: The value can be the ID of a port forwarding - or a :class:`~openstack.network.v2. - port_forwarding.PortForwarding` instance. + or a :class:`~openstack.network.v2. + port_forwarding.PortForwarding` instance. :returns: One :class:`~openstack.network.v2.port_forwarding. - PortForwarding` + PortForwarding` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ floatingip = self._get_resource(_floating_ip.FloatingIP, floating_ip) return self._get(_port_forwarding.PortForwarding, port_forwarding, @@ -4270,13 +4270,13 @@ def floating_ip_port_forwardings(self, floating_ip, **query): """Return a generator of floating ip port forwarding :param floating_ip: The value can be the ID of the Floating IP that the - port forwarding belongs or a :class:`~openstack. - network.v2.floating_ip.FloatingIP` instance. + port forwarding belongs or a :class:`~openstack. + network.v2.floating_ip.FloatingIP` instance. :param kwargs **query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of floating ip port forwarding objects :rtype: :class:`~openstack.network.v2.port_forwarding. - PortForwarding` + PortForwarding` """ floatingip = self._get_resource(_floating_ip.FloatingIP, floating_ip) return self._list(_port_forwarding.PortForwarding, @@ -4287,13 +4287,13 @@ def update_floating_ip_port_forwarding(self, floating_ip, port_forwarding, """Update a floating ip port forwarding :param floating_ip: The value can be the ID of the Floating IP that the - port forwarding belongs or a :class:`~openstack. - network.v2.floating_ip.FloatingIP` instance. + port forwarding belongs or a :class:`~openstack. + network.v2.floating_ip.FloatingIP` instance. :param port_forwarding: Either the id of a floating ip port forwarding - or a :class:`~openstack.network.v2. - port_forwarding.PortForwarding`instance. + or a :class:`~openstack.network.v2. + port_forwarding.PortForwarding`instance. :attrs kwargs: The attributes to update on the floating ip port - forwarding represented by ``value``. + forwarding represented by ``value``. :returns: The updated floating ip port forwarding :rtype: :class:`~openstack.network.v2.port_forwarding.PortForwarding` @@ -4385,10 +4385,10 @@ def delete_conntrack_helper(self, conntrack_helper, router, :param router: The value can be the ID of a Router or a :class:`~openstack.network.v2.router.Router` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the floating ip does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent ip. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the floating ip does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent ip. :returns: ``None`` """ @@ -4539,7 +4539,7 @@ def fip_cleanup_evaluation(obj, identified_resources=None, filters=None): :param Resource obj: Floating IP object :param dict identified_resources: Optional dictionary with resources - identified by other services for deletion. + identified by other services for deletion. :param dict filters: dictionary with parameters """ if ( From e574e4dd3b2a81ec9cb778a48dd3ad3dd98a89d6 Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Fri, 29 Oct 2021 14:30:12 +0200 Subject: [PATCH 2941/3836] Clustering - reindentation of the docstrings Change-Id: I2bdcf4eb573209242aebe5688f73158f6405913a --- openstack/clustering/v1/_proxy.py | 246 +++++++++++++++--------------- 1 file changed, 124 insertions(+), 122 deletions(-) diff --git a/openstack/clustering/v1/_proxy.py b/openstack/clustering/v1/_proxy.py index e7eda23e6..127b521dd 100644 --- a/openstack/clustering/v1/_proxy.py +++ b/openstack/clustering/v1/_proxy.py @@ -40,7 +40,7 @@ def profile_types(self, **query): """Get a generator of profile types. :returns: A generator of objects that are of type - :class:`~openstack.clustering.v1.profile_type.ProfileType` + :class:`~openstack.clustering.v1.profile_type.ProfileType` """ return self._list(_profile_type.ProfileType, **query) @@ -48,10 +48,11 @@ def get_profile_type(self, profile_type): """Get the details about a profile type. :param profile_type: The name of the profile_type to retrieve or an - object of :class:`~openstack.clustering.v1.profile_type.ProfileType`. + object of + :class:`~openstack.clustering.v1.profile_type.ProfileType`. :returns: A :class:`~openstack.clustering.v1.profile_type.ProfileType` - object. + object. :raises: :class:`~openstack.exceptions.ResourceNotFound` when no profile_type matching the name could be found. """ @@ -61,7 +62,7 @@ def policy_types(self, **query): """Get a generator of policy types. :returns: A generator of objects that are of type - :class:`~openstack.clustering.v1.policy_type.PolicyType` + :class:`~openstack.clustering.v1.policy_type.PolicyType` """ return self._list(_policy_type.PolicyType, **query) @@ -69,10 +70,10 @@ def get_policy_type(self, policy_type): """Get the details about a policy type. :param policy_type: The name of a poicy_type or an object of - :class:`~openstack.clustering.v1.policy_type.PolicyType`. + :class:`~openstack.clustering.v1.policy_type.PolicyType`. :returns: A :class:`~openstack.clustering.v1.policy_type.PolicyType` - object. + object. :raises: :class:`~openstack.exceptions.ResourceNotFound` when no policy_type matching the name could be found. """ @@ -82,8 +83,8 @@ def create_profile(self, **attrs): """Create a new profile from attributes. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.clustering.v1.profile.Profile`, it is comprised - of the properties on the Profile class. + :class:`~openstack.clustering.v1.profile.Profile`, it is comprised + of the properties on the Profile class. :returns: The results of profile creation. :rtype: :class:`~openstack.clustering.v1.profile.Profile`. @@ -109,10 +110,10 @@ def find_profile(self, name_or_id, ignore_missing=True): :param str name_or_id: The name or ID of a profile. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.clustering.v1.profile.Profile` object or None """ @@ -140,19 +141,19 @@ def profiles(self, **query): * name: The name of a profile. * type: The type name of a profile. * metadata: A list of key-value pairs that are associated with a - profile. + profile. * sort: A list of sorting keys separated by commas. Each sorting - key can optionally be attached with a sorting direction - modifier which can be ``asc`` or ``desc``. + key can optionally be attached with a sorting direction + modifier which can be ``asc`` or ``desc``. * limit: Requests a specified size of returned items from the - query. Returns a number of items up to the specified limit - value. + query. Returns a number of items up to the specified limit + value. * marker: Specifies the ID of the last-seen item. Use the limit - parameter to make an initial limited request and use the ID of - the last-seen item from the response as the marker parameter - value in a subsequent limited request. + parameter to make an initial limited request and use the ID of + the last-seen item from the response as the marker parameter + value in a subsequent limited request. * global_project: A boolean value indicating whether profiles - from all projects will be returned. + from all projects will be returned. :returns: A generator of profile instances. """ @@ -175,8 +176,8 @@ def validate_profile(self, **attrs): """Validate a profile spec. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.clustering.v1.profile.ProfileValidate`, it is - comprised of the properties on the Profile class. + :class:`~openstack.clustering.v1.profile.ProfileValidate`, it is + comprised of the properties on the Profile class. :returns: The results of profile validation. :rtype: :class:`~openstack.clustering.v1.profile.ProfileValidate`. @@ -188,8 +189,8 @@ def create_cluster(self, **attrs): """Create a new cluster from attributes. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.clustering.v1.cluster.Cluster`, it is comprised - of the properties on the Cluster class. + :class:`~openstack.clustering.v1.cluster.Cluster`, it is comprised + of the properties on the Cluster class. :returns: The results of cluster creation. :rtype: :class:`~openstack.clustering.v1.cluster.Cluster`. @@ -206,7 +207,7 @@ def delete_cluster(self, cluster, ignore_missing=True, force_delete=False): the cluster could not be found. When set to ``True``, no exception will be raised when attempting to delete a non-existent cluster. :param bool force_delete: When set to ``True``, the cluster deletion - will be forced immediately. + will be forced immediately. :returns: The instance of the Cluster which was deleted. :rtype: :class:`~openstack.cluster.v1.cluster.Cluster`. @@ -223,10 +224,10 @@ def find_cluster(self, name_or_id, ignore_missing=True): :param str name_or_id: The name or ID of a cluster. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.clustering.v1.cluster.Cluster` object or None """ @@ -254,17 +255,17 @@ def clusters(self, **query): * name: The name of a cluster. * status: The current status of a cluster. * sort: A list of sorting keys separated by commas. Each sorting - key can optionally be attached with a sorting direction - modifier which can be ``asc`` or ``desc``. + key can optionally be attached with a sorting direction + modifier which can be ``asc`` or ``desc``. * limit: Requests a specified size of returned items from the - query. Returns a number of items up to the specified limit - value. + query. Returns a number of items up to the specified limit + value. * marker: Specifies the ID of the last-seen item. Use the limit - parameter to make an initial limited request and use the ID of - the last-seen item from the response as the marker parameter - value in a subsequent limited request. + parameter to make an initial limited request and use the ID of + the last-seen item from the response as the marker parameter + value in a subsequent limited request. * global_project: A boolean value indicating whether clusters - from all projects will be returned. + from all projects will be returned. :returns: A generator of cluster instances. """ @@ -354,7 +355,7 @@ def remove_nodes_from_cluster(self, cluster, nodes, **params): restrict the nodes to be returned. Available parameters include: * destroy_after_deletion: A boolean value indicating whether the - deleted nodes to be destroyed right away. + deleted nodes to be destroyed right away. :returns: A dict containing the action initiated by this operation. """ if isinstance(cluster, _cluster.Cluster): @@ -501,7 +502,7 @@ def recover_cluster(self, cluster, **params): :param cluster: The value can be either the ID of a cluster or a :class:`~openstack.clustering.v1.cluster.Cluster` instance. :param dict params: A dictionary providing the parameters for the - recover action. + recover action. :returns: A dictionary containing the action ID. """ @@ -515,7 +516,7 @@ def perform_operation_on_cluster(self, cluster, operation, **params): :class:`~openstack.clustering.v1.cluster.Cluster` instance. :param operation: A string specifying the operation to be performed. :param dict params: A dictionary providing the parameters for the - operation. + operation. :returns: A dictionary containing the action ID. """ @@ -526,8 +527,8 @@ def create_node(self, **attrs): """Create a new node from attributes. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.clustering.v1.node.Node`, it is comprised - of the properties on the ``Node`` class. + :class:`~openstack.clustering.v1.node.Node`, it is comprised + of the properties on the ``Node`` class. :returns: The results of node creation. :rtype: :class:`~openstack.clustering.v1.node.Node`. @@ -544,7 +545,7 @@ def delete_node(self, node, ignore_missing=True, force_delete=False): the node could not be found. When set to ``True``, no exception will be raised when attempting to delete a non-existent node. :param bool force_delete: When set to ``True``, the node deletion - will be forced immediately. + will be forced immediately. :returns: The instance of the Node which was deleted. :rtype: :class:`~openstack.cluster.v1.node.Node`. @@ -561,12 +562,12 @@ def find_node(self, name_or_id, ignore_missing=True): :param str name_or_id: The name or ID of a node. :param bool ignore_missing: When set to "False" - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the specified node does not exist. - when set to "True", None will be returned when - attempting to find a nonexistent policy + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the specified node does not exist. + when set to "True", None will be returned when + attempting to find a nonexistent policy :returns: One :class:`~openstack.clustering.v1.node.Node` object - or None. + or None. """ return self._find(_node.Node, name_or_id, ignore_missing=ignore_missing) @@ -598,20 +599,20 @@ def nodes(self, **query): restrict the nodes to be returned. Available parameters include: * cluster_id: A string including the name or ID of a cluster to - which the resulted node(s) is a member. + which the resulted node(s) is a member. * name: The name of a node. * status: The current status of a node. * sort: A list of sorting keys separated by commas. Each sorting - key can optionally be attached with a sorting direction - modifier which can be ``asc`` or ``desc``. + key can optionally be attached with a sorting direction + modifier which can be ``asc`` or ``desc``. * limit: Requests at most the specified number of items be - returned from the query. + returned from the query. * marker: Specifies the ID of the last-seen node. Use the limit - parameter to make an initial limited request and use the ID of - the last-seen node from the response as the marker parameter - value in a subsequent limited request. + parameter to make an initial limited request and use the ID of + the last-seen node from the response as the marker parameter + value in a subsequent limited request. * global_project: A boolean value indicating whether nodes - from all projects will be returned. + from all projects will be returned. :returns: A generator of node instances. """ @@ -636,7 +637,7 @@ def check_node(self, node, **params): :param node: The value can be either the ID of a node or a :class:`~openstack.clustering.v1.node.Node` instance. :param dict params: A dictionary providing the parametes to the check - action. + action. :returns: A dictionary containing the action ID. """ @@ -665,19 +666,19 @@ def adopt_node(self, preview=False, **attrs): parameters include: * type: (Required) A string containing the profile type and - version to be used for node adoption. For example, - ``os.nova.sever-1.0``. + version to be used for node adoption. For example, + ``os.nova.sever-1.0``. * identity: (Required) A string including the name or ID of an - OpenStack resource to be adopted as a Senlin node. + OpenStack resource to be adopted as a Senlin node. * name: (Optional) The name of node to be created. Omitting - this parameter will have the node named automatically. + this parameter will have the node named automatically. * snapshot: (Optional) A boolean indicating whether a snapshot - of the target resource should be created if possible. Default - is False. + of the target resource should be created if possible. Default + is False. * metadata: (Optional) A dictionary of arbitrary key-value pairs - to be associated with the adopted node. + to be associated with the adopted node. * overrides: (Optional) A dictionary of key-value pairs to be used - to override attributes derived from the target resource. + to override attributes derived from the target resource. :returns: The result of node adoption. If `preview` is set to False (default), returns a :class:`~openstack.clustering.v1.node.Node` @@ -694,7 +695,7 @@ def perform_operation_on_node(self, node, operation, **params): :class:`~openstack.clustering.v1.node.Node` instance. :param operation: A string specifying the operation to be performed. :param dict params: A dictionary providing the parameters for the - operation. + operation. :returns: A dictionary containing the action ID. """ @@ -705,8 +706,8 @@ def create_policy(self, **attrs): """Create a new policy from attributes. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.clustering.v1.policy.Policy`, it is comprised - of the properties on the ``Policy`` class. + :class:`~openstack.clustering.v1.policy.Policy`, it is comprised + of the properties on the ``Policy`` class. :returns: The results of policy creation. :rtype: :class:`~openstack.clustering.v1.policy.Policy`. @@ -732,10 +733,10 @@ def find_policy(self, name_or_id, ignore_missing=True): :param str name_or_id: The name or ID of a policy. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the specified policy does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent policy. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the specified policy does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent policy. :returns: A policy object or None. :rtype: :class:`~openstack.clustering.v1.policy.Policy` """ @@ -764,17 +765,17 @@ def policies(self, **query): * name: The name of a policy. * type: The type name of a policy. * sort: A list of sorting keys separated by commas. Each sorting - key can optionally be attached with a sorting direction - modifier which can be ``asc`` or ``desc``. + key can optionally be attached with a sorting direction + modifier which can be ``asc`` or ``desc``. * limit: Requests a specified size of returned items from the - query. Returns a number of items up to the specified limit - value. + query. Returns a number of items up to the specified limit + value. * marker: Specifies the ID of the last-seen item. Use the limit - parameter to make an initial limited request and use the ID of - the last-seen item from the response as the marker parameter - value in a subsequent limited request. + parameter to make an initial limited request and use the ID of + the last-seen item from the response as the marker parameter + value in a subsequent limited request. * global_project: A boolean value indicating whether policies from - all projects will be returned. + all projects will be returned. :returns: A generator of policy instances. """ @@ -797,8 +798,8 @@ def validate_policy(self, **attrs): """Validate a policy spec. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.clustering.v1.policy.PolicyValidate`, it is - comprised of the properties on the Policy class. + :class:`~openstack.clustering.v1.policy.PolicyValidate`, it is + comprised of the properties on the Policy class. :returns: The results of Policy validation. :rtype: :class:`~openstack.clustering.v1.policy.PolicyValidate`. @@ -814,7 +815,7 @@ def cluster_policies(self, cluster, **query): restrict the policies to be returned. Available parameters include: * enabled: A boolean value indicating whether the policy is - enabled on the cluster. + enabled on the cluster. :returns: A generator of cluster-policy binding instances. """ cluster_id = resource.Resource._get_id(cluster) @@ -842,8 +843,8 @@ def create_receiver(self, **attrs): """Create a new receiver from attributes. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.clustering.v1.receiver.Receiver`, it is - comprised of the properties on the Receiver class. + :class:`~openstack.clustering.v1.receiver.Receiver`, it is + comprised of the properties on the Receiver class. :returns: The results of receiver creation. :rtype: :class:`~openstack.clustering.v1.receiver.Receiver`. @@ -882,10 +883,10 @@ def find_receiver(self, name_or_id, ignore_missing=True): :param str name_or_id: The name or ID of a receiver. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the specified receiver does not exist. When - set to ``True``, None will be returned when attempting to - find a nonexistent receiver. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the specified receiver does not exist. When + set to ``True``, None will be returned when attempting to + find a nonexistent receiver. :returns: A receiver object or None. :rtype: :class:`~openstack.clustering.v1.receiver.Receiver` """ @@ -916,8 +917,8 @@ def receivers(self, **query): * cluster_id: The ID of the associated cluster. * action: The name of the associated action. * sort: A list of sorting keys separated by commas. Each sorting - key can optionally be attached with a sorting direction - modifier which can be ``asc`` or ``desc``. + key can optionally be attached with a sorting direction + modifier which can be ``asc`` or ``desc``. * global_project: A boolean value indicating whether receivers * from all projects will be returned. @@ -946,18 +947,18 @@ def actions(self, **query): * name: name of action for query. * target: ID of the target object for which the actions should be - returned. + returned. * action: built-in action types for query. * sort: A list of sorting keys separated by commas. Each sorting - key can optionally be attached with a sorting direction - modifier which can be ``asc`` or ``desc``. + key can optionally be attached with a sorting direction + modifier which can be ``asc`` or ``desc``. * limit: Requests a specified size of returned items from the - query. Returns a number of items up to the specified limit - value. + query. Returns a number of items up to the specified limit + value. * marker: Specifies the ID of the last-seen item. Use the limit - parameter to make an initial limited request and use the ID of - the last-seen item from the response as the marker parameter - value in a subsequent limited request. + parameter to make an initial limited request and use the ID of + the last-seen item from the response as the marker parameter + value in a subsequent limited request. :returns: A generator of action instances. """ @@ -997,23 +998,23 @@ def events(self, **query): * obj_name: name string of the object associated with an event. * obj_type: type string of the object related to an event. The - value can be ``cluster``, ``node``, ``policy`` etc. + value can be ``cluster``, ``node``, ``policy`` etc. * obj_id: ID of the object associated with an event. * cluster_id: ID of the cluster associated with the event, if any. * action: name of the action associated with an event. * sort: A list of sorting keys separated by commas. Each sorting - key can optionally be attached with a sorting direction - modifier which can be ``asc`` or ``desc``. + key can optionally be attached with a sorting direction + modifier which can be ``asc`` or ``desc``. * limit: Requests a specified size of returned items from the - query. Returns a number of items up to the specified limit - value. + query. Returns a number of items up to the specified limit + value. * marker: Specifies the ID of the last-seen item. Use the limit - parameter to make an initial limited request and use the ID of - the last-seen item from the response as the marker parameter - value in a subsequent limited request. + parameter to make an initial limited request and use the ID of + the last-seen item from the response as the marker parameter + value in a subsequent limited request. * global_project: A boolean specifying whether events from all - projects should be returned. This option is subject to access - control checking. + projects should be returned. This option is subject to access + control checking. :returns: A generator of event instances. """ @@ -1024,22 +1025,22 @@ def wait_for_status(self, res, status, failures=None, interval=2, """Wait for a resource to be in a particular status. :param res: The resource to wait on to reach the specified status. - The resource must have a ``status`` attribute. + The resource must have a ``status`` attribute. :type resource: A :class:`~openstack.resource.Resource` object. :param status: Desired status. :param failures: Statuses that would be interpreted as failures. :type failures: :py:class:`list` :param interval: Number of seconds to wait before to consecutive - checks. Default to 2. + checks. Default to 2. :param wait: Maximum number of seconds to wait before the change. - Default to 120. + Default to 120. :returns: The resource is returned on success. :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition - to the desired status failed to occur in specified seconds. + to the desired status failed to occur in specified seconds. :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource - has transited to one of the failure statuses. + has transited to one of the failure statuses. :raises: :class:`~AttributeError` if the resource does not have a - ``status`` attribute. + ``status`` attribute. """ failures = [] if failures is None else failures return resource.wait_for_status( @@ -1051,12 +1052,12 @@ def wait_for_delete(self, res, interval=2, wait=120): :param res: The resource to wait on to be deleted. :type resource: A :class:`~openstack.resource.Resource` object. :param interval: Number of seconds to wait before to consecutive - checks. Default to 2. + checks. Default to 2. :param wait: Maximum number of seconds to wait before the change. - Default to 120. + Default to 120. :returns: The resource is returned on success. :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition - to delete failed to occur in the specified seconds. + to delete failed to occur in the specified seconds. """ return resource.wait_for_delete(self, res, interval, wait) @@ -1064,7 +1065,7 @@ def services(self, **query): """Get a generator of services. :returns: A generator of objects that are of type - :class:`~openstack.clustering.v1.service.Service` + :class:`~openstack.clustering.v1.service.Service` """ return self._list(_service.Service, **query) @@ -1072,10 +1073,11 @@ def list_profile_type_operations(self, profile_type): """Get the operation about a profile type. :param profile_type: The name of the profile_type to retrieve or an - object of :class:`~openstack.clustering.v1.profile_type.ProfileType`. + object of + :class:`~openstack.clustering.v1.profile_type.ProfileType`. :returns: A :class:`~openstack.clustering.v1.profile_type.ProfileType` - object. + object. :raises: :class:`~openstack.exceptions.ResourceNotFound` when no profile_type matching the name could be found. """ From bf267b0e1c5364c369b57f2a372349cfa0cff18b Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Tue, 2 Nov 2021 10:06:56 +0100 Subject: [PATCH 2942/3836] DNS - reindentation of the docstrings Change-Id: Ia91c332b0c89135fe92b8cb620431cfc9ea9b68d --- openstack/dns/v2/_proxy.py | 56 +++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py index 092c688cc..800d465fb 100644 --- a/openstack/dns/v2/_proxy.py +++ b/openstack/dns/v2/_proxy.py @@ -55,7 +55,7 @@ def get_zone(self, zone): """Get a zone :param zone: The value can be the ID of a zone - or a :class:`~openstack.dns.v2.zone.Zone` instance. + or a :class:`~openstack.dns.v2.zone.Zone` instance. :returns: Zone instance. :rtype: :class:`~openstack.dns.v2.zone.Zone` """ @@ -65,7 +65,7 @@ def delete_zone(self, zone, ignore_missing=True): """Delete a zone :param zone: The value can be the ID of a zone - or a :class:`~openstack.dns.v2.zone.Zone` instance. + or a :class:`~openstack.dns.v2.zone.Zone` instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the zone does not exist. @@ -108,7 +108,7 @@ def abandon_zone(self, zone, **attrs): """Abandon Zone :param zone: The value can be the ID of a zone to be abandoned - or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance. + or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance. :returns: None """ @@ -120,7 +120,7 @@ def xfr_zone(self, zone, **attrs): """Trigger update of secondary Zone :param zone: The value can be the ID of a zone to be abandoned - or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance. + or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance. :returns: None """ @@ -132,9 +132,9 @@ def recordsets(self, zone=None, **query): """Retrieve a generator of recordsets :param zone: The optional value can be the ID of a zone - or a :class:`~openstack.dns.v2.zone.Zone` instance. If it is not - given all recordsets for all zones of the tenant would be - retrieved + or a :class:`~openstack.dns.v2.zone.Zone` instance. If it is not + given all recordsets for all zones of the tenant would be + retrieved :param dict query: Optional query parameters to be sent to limit the resources being returned. @@ -185,9 +185,9 @@ def get_recordset(self, recordset, zone): """Get a recordset :param zone: The value can be the ID of a zone - or a :class:`~openstack.dns.v2.zone.Zone` instance. + or a :class:`~openstack.dns.v2.zone.Zone` instance. :param recordset: The value can be the ID of a recordset - or a :class:`~openstack.dns.v2.recordset.Recordset` instance. + or a :class:`~openstack.dns.v2.recordset.Recordset` instance. :returns: Recordset instance :rtype: :class:`~openstack.dns.v2.recordset.Recordset` """ @@ -198,10 +198,10 @@ def delete_recordset(self, recordset, zone=None, ignore_missing=True): """Delete a zone :param recordset: The value can be the ID of a recordset - or a :class:`~openstack.dns.v2.recordset.Recordset` - instance. + or a :class:`~openstack.dns.v2.recordset.Recordset` + instance. :param zone: The value can be the ID of a zone - or a :class:`~openstack.dns.v2.zone.Zone` instance. + or a :class:`~openstack.dns.v2.zone.Zone` instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the zone does not exist. When set to ``True``, no exception will @@ -221,7 +221,7 @@ def find_recordset(self, zone, name_or_id, ignore_missing=True, **attrs): """Find a single recordset :param zone: The value can be the ID of a zone - or a :class:`~openstack.dns.v2.zone.Zone` instance. + or a :class:`~openstack.dns.v2.zone.Zone` instance. :param name_or_id: The name or ID of a zone :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised @@ -268,7 +268,7 @@ def get_zone_import(self, zone_import): """Get a zone import record :param zone: The value can be the ID of a zone import - or a :class:`~openstack.dns.v2.zone_import.ZoneImport` instance. + or a :class:`~openstack.dns.v2.zone_import.ZoneImport` instance. :returns: ZoneImport instance. :rtype: :class:`~openstack.dns.v2.zone_import.ZoneImport` """ @@ -278,7 +278,7 @@ def delete_zone_import(self, zone_import, ignore_missing=True): """Delete a zone import :param zone_import: The value can be the ID of a zone import - or a :class:`~openstack.dns.v2.zone_import.ZoneImport` instance. + or a :class:`~openstack.dns.v2.zone_import.ZoneImport` instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the zone does not exist. @@ -310,7 +310,7 @@ def create_zone_export(self, zone, **attrs): """Create a new zone export from attributes :param zone: The value can be the ID of a zone to be exported - or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance. + or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance. :param dict attrs: Keyword arguments which will be used to create a :class:`~openstack.dns.v2.zone_export.ZoneExport`, comprised of the properties on the ZoneExport class. @@ -328,7 +328,7 @@ def get_zone_export(self, zone_export): """Get a zone export record :param zone: The value can be the ID of a zone import - or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance. + or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance. :returns: ZoneExport instance. :rtype: :class:`~openstack.dns.v2.zone_export.ZoneExport` """ @@ -338,7 +338,7 @@ def get_zone_export_text(self, zone_export): """Get a zone export record as text :param zone: The value can be the ID of a zone import - or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance. + or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance. :returns: ZoneExport instance. :rtype: :class:`~openstack.dns.v2.zone_export.ZoneExport` """ @@ -349,7 +349,7 @@ def delete_zone_export(self, zone_export, ignore_missing=True): """Delete a zone export :param zone_export: The value can be the ID of a zone import - or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance. + or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the zone does not exist. @@ -383,8 +383,8 @@ def get_floating_ip(self, floating_ip): """Get a Floating IP :param floating_ip: The value can be the ID of a floating ip - or a :class:`~openstack.dns.v2.floating_ip.FloatingIP` instance. - The ID is in format "region_name:floatingip_id" + or a :class:`~openstack.dns.v2.floating_ip.FloatingIP` instance. + The ID is in format "region_name:floatingip_id" :returns: FloatingIP instance. :rtype: :class:`~openstack.dns.v2.floating_ip.FloatingIP` """ @@ -432,8 +432,8 @@ def get_zone_transfer_request(self, request): """Get a ZoneTransfer Request info :param request: The value can be the ID of a transfer request - or a :class:`~openstack.dns.v2.zone_transfer.ZoneTransferRequest` - instance. + or a :class:`~openstack.dns.v2.zone_transfer.ZoneTransferRequest` + instance. :returns: Zone transfer request instance. :rtype: :class:`~openstack.dns.v2.zone_transfer.ZoneTransferRequest` """ @@ -443,7 +443,7 @@ def create_zone_transfer_request(self, zone, **attrs): """Create a new ZoneTransfer Request from attributes :param zone: The value can be the ID of a zone to be transferred - or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance. + or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance. :param dict attrs: Keyword arguments which will be used to create a :class:`~openstack.dns.v2.zone_transfer.ZoneTransferRequest`, comprised of the properties on the ZoneTransferRequest class. @@ -475,8 +475,8 @@ def delete_zone_transfer_request(self, request, ignore_missing=True): """Delete a ZoneTransfer Request :param request: The value can be the ID of a zone transfer request - or a :class:`~openstack.dns.v2.zone_transfer.ZoneTransferRequest` - instance. + or a :class:`~openstack.dns.v2.zone_transfer.ZoneTransferRequest` + instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the zone does not exist. @@ -506,8 +506,8 @@ def get_zone_transfer_accept(self, accept): """Get a ZoneTransfer Accept info :param request: The value can be the ID of a transfer accept - or a :class:`~openstack.dns.v2.zone_transfer.ZoneTransferAccept` - instance. + or a :class:`~openstack.dns.v2.zone_transfer.ZoneTransferAccept` + instance. :returns: Zone transfer request instance. :rtype: :class:`~openstack.dns.v2.zone_transfer.ZoneTransferAccept` """ From 6c96faa7d12cedc4383eee0427ed8d2c5d6f67db Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 18 Oct 2021 10:24:35 +0100 Subject: [PATCH 2943/3836] compute: Regroup proxy methods At some point in the past, we grouped proxy methods by their resource type. We seem to have stopped doing this recently. Fix that. Note that there are no functional changes here, just code movement. Change-Id: I6912ece1558d0b726cac0c6eeead9bc9fc85712f Signed-off-by: Stephen Finucane --- openstack/compute/v2/_proxy.py | 367 ++++++++++++++++++--------------- 1 file changed, 199 insertions(+), 168 deletions(-) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 741321344..1a9e87c24 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -39,6 +39,8 @@ class Proxy(proxy.Proxy): + # ========== Extensions ========== + def find_extension(self, name_or_id, ignore_missing=True): """Find a single extension @@ -523,6 +525,8 @@ def delete_image_metadata(self, image, keys=None): else: res.delete_metadata(self) + # ========== Keypairs ========== + def create_keypair(self, **attrs): """Create a new keypair from attributes @@ -593,6 +597,8 @@ def keypairs(self, **query): """ return self._list(_keypair.Keypair, **query) + # ========== Limits ========== + def get_limits(self): """Retrieve limits that are applied to the project's account @@ -603,6 +609,8 @@ def get_limits(self): """ return self._get(limits.Limits) + # ========== Servers ========== + def create_server(self, **attrs): """Create a new server from attributes @@ -826,98 +834,6 @@ def create_server_image(self, server, name, metadata=None, wait=False, return image return self._connection.wait_for_image(image, timeout=timeout) - def fetch_server_security_groups(self, server): - """Fetch security groups with details for a server. - - :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. - - :returns: updated :class:`~openstack.compute.v2.server.Server` instance - """ - server = self._get_resource(_server.Server, server) - return server.fetch_security_groups(self) - - def add_security_group_to_server(self, server, security_group): - """Add a security group to a server - - :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. - :param security_group: Either the ID, Name of a security group or a - :class:`~openstack.network.v2.security_group.SecurityGroup` - instance. - - :returns: None - """ - server = self._get_resource(_server.Server, server) - security_group = self._get_resource(_sg.SecurityGroup, security_group) - server.add_security_group(self, security_group.name) - - def remove_security_group_from_server(self, server, security_group): - """Remove a security group from a server - - :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. - :param security_group: Either the ID of a security group or a - :class:`~openstack.network.v2.security_group.SecurityGroup` - instance. - - :returns: None - """ - server = self._get_resource(_server.Server, server) - security_group = self._get_resource(_sg.SecurityGroup, security_group) - server.remove_security_group(self, security_group.name) - - def add_fixed_ip_to_server(self, server, network_id): - """Adds a fixed IP address to a server instance. - - :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. - :param network_id: The ID of the network from which a fixed IP address - is about to be allocated. - :returns: None - """ - server = self._get_resource(_server.Server, server) - server.add_fixed_ip(self, network_id) - - def remove_fixed_ip_from_server(self, server, address): - """Removes a fixed IP address from a server instance. - - :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. - :param address: The fixed IP address to be disassociated from the - server. - :returns: None - """ - server = self._get_resource(_server.Server, server) - server.remove_fixed_ip(self, address) - - def add_floating_ip_to_server(self, server, address, fixed_address=None): - """Adds a floating IP address to a server instance. - - :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. - :param address: The floating IP address to be added to the server. - :param fixed_address: The fixed IP address to be associated with the - floating IP address. Used when the server is - connected to multiple networks. - :returns: None - """ - server = self._get_resource(_server.Server, server) - server.add_floating_ip(self, address, - fixed_address=fixed_address) - - def remove_floating_ip_from_server(self, server, address): - """Removes a floating IP address from a server instance. - - :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. - :param address: The floating IP address to be disassociated from the - server. - :returns: None - """ - server = self._get_resource(_server.Server, server) - server.remove_floating_ip(self, address) - def backup_server(self, server, name, backup_type, rotation): """Backup a server @@ -1089,49 +1005,6 @@ def unshelve_server(self, server): server = self._get_resource(_server.Server, server) server.unshelve(self) - def get_server_console_output(self, server, length=None): - """Return the console output for a server. - - :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. - :param length: Optional number of line to fetch from the end of console - log. All lines will be returned if this is not specified. - :returns: The console output as a dict. Control characters will be - escaped to create a valid JSON string. - """ - server = self._get_resource(_server.Server, server) - return server.get_console_output(self, length=length) - - def wait_for_server(self, server, status='ACTIVE', failures=None, - interval=2, wait=120): - """Wait for a server to be in a particular status. - - :param server: - The :class:`~openstack.compute.v2.server.Server` to wait on - to reach the specified status. - :type server: :class:`~openstack.compute.v2.server.Server`: - :param status: Desired status. - :param failures: - Statuses that would be interpreted as failures. - :type failures: :py:class:`list` - :param int interval: - Number of seconds to wait before to consecutive checks. - Default to 2. - :param int wait: - Maximum number of seconds to wait before the change. - Default to 120. - :returns: The resource is returned on success. - :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition - to the desired status failed to occur in specified seconds. - :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource - has transited to one of the failure statuses. - :raises: :class:`~AttributeError` if the resource does not have a - ``status`` attribute. - """ - failures = ['ERROR'] if failures is None else failures - return resource.wait_for_status( - self, server, status, failures, interval, wait) - def create_server_interface(self, server, **attrs): """Create a new server interface from attributes @@ -1149,6 +1022,104 @@ def create_server_interface(self, server, **attrs): return self._create(_server_interface.ServerInterface, server_id=server_id, **attrs) + # ========== Server security groups ========== + + def fetch_server_security_groups(self, server): + """Fetch security groups with details for a server. + + :param server: Either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance. + + :returns: updated :class:`~openstack.compute.v2.server.Server` instance + """ + server = self._get_resource(_server.Server, server) + return server.fetch_security_groups(self) + + def add_security_group_to_server(self, server, security_group): + """Add a security group to a server + + :param server: Either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance. + :param security_group: Either the ID, Name of a security group or a + :class:`~openstack.network.v2.security_group.SecurityGroup` + instance. + + :returns: None + """ + server = self._get_resource(_server.Server, server) + security_group = self._get_resource(_sg.SecurityGroup, security_group) + server.add_security_group(self, security_group.name) + + def remove_security_group_from_server(self, server, security_group): + """Remove a security group from a server + + :param server: Either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance. + :param security_group: Either the ID of a security group or a + :class:`~openstack.network.v2.security_group.SecurityGroup` + instance. + + :returns: None + """ + server = self._get_resource(_server.Server, server) + security_group = self._get_resource(_sg.SecurityGroup, security_group) + server.remove_security_group(self, security_group.name) + + # ========== Server IPs ========== + + def add_fixed_ip_to_server(self, server, network_id): + """Adds a fixed IP address to a server instance. + + :param server: Either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance. + :param network_id: The ID of the network from which a fixed IP address + is about to be allocated. + :returns: None + """ + server = self._get_resource(_server.Server, server) + server.add_fixed_ip(self, network_id) + + def remove_fixed_ip_from_server(self, server, address): + """Removes a fixed IP address from a server instance. + + :param server: Either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance. + :param address: The fixed IP address to be disassociated from the + server. + :returns: None + """ + server = self._get_resource(_server.Server, server) + server.remove_fixed_ip(self, address) + + def add_floating_ip_to_server(self, server, address, fixed_address=None): + """Adds a floating IP address to a server instance. + + :param server: Either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance. + :param address: The floating IP address to be added to the server. + :param fixed_address: The fixed IP address to be associated with the + floating IP address. Used when the server is + connected to multiple networks. + :returns: None + """ + server = self._get_resource(_server.Server, server) + server.add_floating_ip(self, address, + fixed_address=fixed_address) + + def remove_floating_ip_from_server(self, server, address): + """Removes a floating IP address from a server instance. + + :param server: Either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance. + :param address: The floating IP address to be disassociated from the + server. + :returns: None + """ + server = self._get_resource(_server.Server, server) + server.remove_floating_ip(self, address) + + # ========== Server Interfaces ========== + # TODO(stephenfin): Does this work? There's no 'value' parameter for the # call to '_delete' def delete_server_interface(self, server_interface, server=None, @@ -1251,6 +1222,8 @@ def availability_zones(self, details=False): availability_zone.AvailabilityZone, base_path=base_path) + # ========== Server Metadata ========== + def get_server_metadata(self, server): """Return a dictionary of metadata for a server @@ -1304,6 +1277,8 @@ def delete_server_metadata(self, server, keys=None): else: res.delete_metadata(self) + # ========== Server Groups ========== + def create_server_group(self, **attrs): """Create a new server group from attributes @@ -1576,6 +1551,8 @@ def update_service(self, service, **attrs): 'Method require at least microversion 2.53' ) + # ========== Volume Attachments ========== + def create_volume_attachment(self, server, **attrs): """Create a new volume attachment from attributes @@ -1699,59 +1676,48 @@ def volume_attachments(self, server): return self._list(_volume_attachment.VolumeAttachment, server_id=server_id) + # ========== Server Migrations ========== + def migrate_server(self, server): """Migrate a server from one host to another :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :returns: None """ server = self._get_resource(_server.Server, server) server.migrate(self) def live_migrate_server( - self, server, host=None, force=False, block_migration=None): + self, server, host=None, force=False, block_migration=None, + ): """Live migrate a server from one host to target host - :param server: - Either the ID of a server or a + :param server: Either the ID of a server or a :class:`~openstack.compute.v2.server.Server` instance. - :param str host: - The host to which to migrate the server. If the Nova service is - too old, the host parameter implies force=True which causes the - Nova scheduler to be bypassed. On such clouds, a ``ValueError`` - will be thrown if ``host`` is given without ``force``. - :param bool force: - Force a live-migration by not verifying the provided destination - host by the scheduler. This is unsafe and not recommended. - :param block_migration: - Perform a block live migration to the destination host by the - scheduler. Can be 'auto', True or False. Some clouds are too old - to support 'auto', in which case a ValueError will be thrown. If - omitted, the value will be 'auto' on clouds that support it, and - False on clouds that do not. + :param str host: The host to which to migrate the server. If the Nova + service is too old, the host parameter implies force=True which + causes the Nova scheduler to be bypassed. On such clouds, a + ``ValueError`` will be thrown if ``host`` is given without + ``force``. + :param bool force: Force a live-migration by not verifying the provided + destination host by the scheduler. This is unsafe and not + recommended. + :param block_migration: Perform a block live migration to the + destination host by the scheduler. Can be 'auto', True or False. + Some clouds are too old to support 'auto', in which case a + ValueError will be thrown. If omitted, the value will be 'auto' on + clouds that support it, and False on clouds that do not. :returns: None """ server = self._get_resource(_server.Server, server) server.live_migrate( self, host, force=force, - block_migration=block_migration) - - def wait_for_delete(self, res, interval=2, wait=120): - """Wait for a resource to be deleted. + block_migration=block_migration, + ) - :param res: The resource to wait on to be deleted. - :type resource: A :class:`~openstack.resource.Resource` object. - :param interval: Number of seconds to wait before to consecutive - checks. Default to 2. - :param wait: Maximum number of seconds to wait before the change. - Default to 120. - :returns: The resource is returned on success. - :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition - to delete failed to occur in the specified seconds. - """ - return resource.wait_for_delete(self, res, interval, wait) + # ========== Server diagnostics ========== def get_server_diagnostics(self, server): """Get a single server diagnostics @@ -1770,6 +1736,8 @@ def get_server_diagnostics(self, server): return self._get(_server_diagnostics.ServerDiagnostics, server_id=server_id, requires_id=False) + # ========== Server consoles ========== + def create_server_remote_console(self, server, **attrs): """Create a remote console on the server. @@ -1794,6 +1762,19 @@ def get_server_console_url(self, server, console_type): server = self._get_resource(_server.Server, server) return server.get_console_url(self, console_type) + def get_server_console_output(self, server, length=None): + """Return the console output for a server. + + :param server: Either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance. + :param length: Optional number of line to fetch from the end of console + log. All lines will be returned if this is not specified. + :returns: The console output as a dict. Control characters will be + escaped to create a valid JSON string. + """ + server = self._get_resource(_server.Server, server) + return server.get_console_output(self, length=length) + def create_console(self, server, console_type, console_protocol=None): """Create a remote console on the server. @@ -1826,6 +1807,8 @@ def create_console(self, server, console_type, console_protocol=None): else: return server.get_console_url(self, console_type) + # ========== Quota sets ========== + def get_quota_set(self, project, usage=False, **query): """Show QuotaSet information for the project @@ -1894,6 +1877,54 @@ def update_quota_set(self, quota_set, query=None, **attrs): res = self._get_resource(_quota_set.QuotaSet, quota_set, **attrs) return res.commit(self, **query) + # ========== Utilities ========== + + def wait_for_server( + self, server, status='ACTIVE', failures=None, interval=2, wait=120, + ): + """Wait for a server to be in a particular status. + + :param server: The :class:`~openstack.compute.v2.server.Server` to wait + on to reach the specified status. + :type server: :class:`~openstack.compute.v2.server.Server`: + :param status: Desired status. + :type status: str + :param failures: Statuses that would be interpreted as failures. + :type failures: :py:class:`list` + :param interval: Number of seconds to wait before to consecutive + checks. Default to 2. + :type interval: int + :param wait: Maximum number of seconds to wait before the change. + Default to 120. + :type wait: int + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to the desired status failed to occur in specified seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + has transited to one of the failure statuses. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute. + """ + failures = ['ERROR'] if failures is None else failures + return resource.wait_for_status( + self, server, status, failures, interval, wait, + ) + + def wait_for_delete(self, res, interval=2, wait=120): + """Wait for a resource to be deleted. + + :param res: The resource to wait on to be deleted. + :type resource: A :class:`~openstack.resource.Resource` object. + :param interval: Number of seconds to wait before to consecutive + checks. Default to 2. + :param wait: Maximum number of seconds to wait before the change. + Default to 120. + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete(self, res, interval, wait) + def _get_cleanup_dependencies(self): return { 'compute': { From 6d1321f5ddad18629d9b02cc77c54d76c465da08 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 15 Oct 2021 11:58:04 +0100 Subject: [PATCH 2944/3836] compute: Add support for server migrations API There are two migrations APIs in nova: the migrations API ('/os-migrations') and the *server* migrations API ('/servers/{id}/migrations'). The former is responsible for listing all migrations in a deployment, while the latter only lists those for a given server. In this change, we're adding support for the latter. Change-Id: Ideeca99a89c920a09cfc3799bbcc7e24046a5c43 Signed-off-by: Stephen Finucane --- doc/source/user/proxies/compute.rst | 9 ++ doc/source/user/resources/compute/index.rst | 1 + .../resources/compute/v2/server_migration.rst | 13 ++ openstack/compute/v2/_proxy.py | 113 ++++++++++++++++++ openstack/compute/v2/server_migration.py | 98 +++++++++++++++ openstack/tests/unit/compute/v2/test_proxy.py | 48 ++++++++ .../unit/compute/v2/test_server_migration.py | 112 +++++++++++++++++ ...dd-server-migrations-6e31183196f14deb.yaml | 6 + 8 files changed, 400 insertions(+) create mode 100644 doc/source/user/resources/compute/v2/server_migration.rst create mode 100644 openstack/compute/v2/server_migration.py create mode 100644 openstack/tests/unit/compute/v2/test_server_migration.py create mode 100644 releasenotes/notes/add-server-migrations-6e31183196f14deb.yaml diff --git a/doc/source/user/proxies/compute.rst b/doc/source/user/proxies/compute.rst index 8c9551212..fb9379802 100644 --- a/doc/source/user/proxies/compute.rst +++ b/doc/source/user/proxies/compute.rst @@ -152,7 +152,16 @@ Extension Operations QuotaSet Operations ^^^^^^^^^^^^^^^^^^^ + .. autoclass:: openstack.compute.v2._proxy.Proxy :noindex: :members: get_quota_set, get_quota_set_defaults, revert_quota_set, update_quota_set + +Server Migration Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.compute.v2._proxy.Proxy + :noindex: + :members: abort_server_migration, force_complete_server_migration, + get_server_migration, server_migrations diff --git a/doc/source/user/resources/compute/index.rst b/doc/source/user/resources/compute/index.rst index 0db8a314b..aee022fdc 100644 --- a/doc/source/user/resources/compute/index.rst +++ b/doc/source/user/resources/compute/index.rst @@ -11,6 +11,7 @@ Compute Resources v2/limits v2/server v2/server_interface + v2/server_migration v2/server_ip v2/hypervisor v2/quota_set diff --git a/doc/source/user/resources/compute/v2/server_migration.rst b/doc/source/user/resources/compute/v2/server_migration.rst new file mode 100644 index 000000000..6587f596f --- /dev/null +++ b/doc/source/user/resources/compute/v2/server_migration.rst @@ -0,0 +1,13 @@ +openstack.compute.v2.server_migration +===================================== + +.. automodule:: openstack.compute.v2.server_migration + +The ServerMigration Class +------------------------- + +The ``ServerMigration`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.compute.v2.server_migration.ServerMigration + :members: diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 1a9e87c24..c0df32926 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -26,6 +26,7 @@ from openstack.compute.v2 import server_group as _server_group from openstack.compute.v2 import server_interface as _server_interface from openstack.compute.v2 import server_ip +from openstack.compute.v2 import server_migration as _server_migration from openstack.compute.v2 import server_remote_console as _src from openstack.compute.v2 import service as _service from openstack.compute.v2 import volume_attachment as _volume_attachment @@ -1717,6 +1718,118 @@ def live_migrate_server( block_migration=block_migration, ) + def abort_server_migration( + self, server_migration, server, ignore_missing=True, + ): + """Abort an in-progress server migration + + :param server_migration: The value can be either the ID of a server + migration or a + :class:`~openstack.compute.v2.server_migration.ServerMigration` + instance. + :param server: This parameter needs to be specified when + ServerMigration ID is given as value. It can be either the ID of a + server or a :class:`~openstack.compute.v2.server.Server` instance + that the migration belongs to. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the volume attachment does not exist. When set to ``True``, no + exception will be set when attempting to delete a nonexistent + volume attachment. + + :returns: ``None`` + """ + server_id = self._get_uri_attribute( + server_migration, server, 'server_id', + ) + server_migration = resource.Resource._get_id(server_migration) + + self._delete( + _server_migration.ServerMigration, + server_migration, + server_id=server_id, + ignore_missing=ignore_missing, + ) + + def force_complete_server_migration(self, server_migration, server=None): + """Force complete an in-progress server migration + + :param server_migration: The value can be either the ID of a server + migration or a + :class:`~openstack.compute.v2.server_migration.ServerMigration` + instance. + :param server: This parameter needs to be specified when + ServerMigration ID is given as value. It can be either the ID of a + server or a :class:`~openstack.compute.v2.server.Server` instance + that the migration belongs to. + + :returns: ``None`` + """ + server_id = self._get_uri_attribute( + server_migration, server, 'server_id', + ) + server_migration = self._get_resource( + _server_migration.ServerMigration, + server_migration, + server_id=server_id, + ) + server_migration.force_complete(self) + + def get_server_migration( + self, + server_migration, + server, + ignore_missing=True, + ): + """Get a single volume attachment + + :param server_migration: The value can be the ID of a server migration + or a + :class:`~openstack.compute.v2.server_migration.ServerMigration` + instance. + :param server: This parameter need to be specified when ServerMigration + ID is given as value. It can be either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance that the + migration belongs to. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the server migration does not exist. When set to ``True``, no + exception will be set when attempting to delete a nonexistent + server migration. + + :returns: One + :class:`~openstack.compute.v2.server_migration.ServerMigration` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + server_id = self._get_uri_attribute( + server_migration, server, 'server_id', + ) + server_migration = resource.Resource._get_id(server_migration) + + return self._get( + _server_migration.ServerMigration, + server_migration, + server_id=server_id, + ignore_missing=ignore_missing, + ) + + def server_migrations(self, server): + """Return a generator of migrations for a server. + + :param server: The server can be either the ID of a server or a + :class:`~openstack.compute.v2.server.Server`. + + :returns: A generator of ServerMigration objects + :rtype: + :class:`~openstack.compute.v2.server_migration.ServerMigration` + """ + server_id = resource.Resource._get_id(server) + return self._list( + _server_migration.ServerMigration, + server_id=server_id, + ) + # ========== Server diagnostics ========== def get_server_diagnostics(self, server): diff --git a/openstack/compute/v2/server_migration.py b/openstack/compute/v2/server_migration.py new file mode 100644 index 000000000..f10f9e163 --- /dev/null +++ b/openstack/compute/v2/server_migration.py @@ -0,0 +1,98 @@ +# 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 import resource +from openstack import utils + + +class ServerMigration(resource.Resource): + resource_key = 'migration' + resources_key = 'migrations' + base_path = '/servers/%(server_uuid)s/migrations' + + # capabilities + allow_fetch = True + allow_list = True + allow_delete = True + + #: The ID for the server. + server_id = resource.URI('server_uuid') + + #: The date and time when the resource was created. + created_at = resource.Body('created_at') + #: The target host of the migration. + dest_host = resource.Body('dest_host') + #: The target compute of the migration. + dest_compute = resource.Body('dest_compute') + #: The target node of the migration. + dest_node = resource.Body('dest_node') + #: The amount of disk, in bytes, that has been processed during the + #: migration. + disk_processed_bytes = resource.Body('disk_processed_bytes') + #: The amount of disk, in bytes, that still needs to be migrated. + disk_remaining_bytes = resource.Body('disk_remaining_bytes') + #: The total amount of disk, in bytes, that needs to be migrated. + disk_total_bytes = resource.Body('disk_total_bytes') + #: The amount of memory, in bytes, that has been processed during the + #: migration. + memory_processed_bytes = resource.Body('memory_processed_bytes') + #: The amount of memory, in bytes, that still needs to be migrated. + memory_remaining_bytes = resource.Body('memory_remaining_bytes') + #: The total amount of memory, in bytes, that needs to be migrated. + memory_total_bytes = resource.Body('memory_total_bytes') + #: The ID of the project that initiated the server migration (since + #: microversion 2.80) + project_id = resource.Body('project_id') + # FIXME(stephenfin): This conflicts since there is a server ID in the URI + # *and* in the body. We need a field that handles both or we need to use + # different names. + # #: The UUID of the server + # server_id = resource.Body('server_uuid') + #: The source compute of the migration. + source_compute = resource.Body('source_compute') + #: The source node of the migration. + source_node = resource.Body('source_node') + #: The current status of the migration. + status = resource.Body('status') + #: The date and time when the resource was last updated. + updated_at = resource.Body('updated_at') + #: The ID of the user that initiated the server migration (since + #: microversion 2.80) + user_id = resource.Body('user_id') + #: The UUID of the migration (since microversion 2.59) + uuid = resource.Body('uuid', alternate_id=True) + + _max_microversion = '2.80' + + @classmethod + def _get_microversion_for_action(cls, session): + return cls._get_microversion_for_list(session) + + def _action(self, session, body): + """Preform server migration actions given the message body.""" + session = self._get_session(session) + microversion = self._get_microversion_for_list(session) + + url = utils.urljoin( + self.base_path % {'server_uuid': self.server_id}, + self.id, + 'action', + ) + response = session.post(url, microversion=microversion, json=body) + exceptions.raise_from_response(response) + return response + + def force_complete(self, session): + """Force on-going live migration to complete.""" + body = {'force_complete': None} + self._action(session, body) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 313535760..5ecfa9cd6 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -26,6 +26,7 @@ from openstack.compute.v2 import server_group from openstack.compute.v2 import server_interface from openstack.compute.v2 import server_ip +from openstack.compute.v2 import server_migration from openstack.compute.v2 import server_remote_console from openstack.compute.v2 import service from openstack import resource @@ -1004,6 +1005,53 @@ def test_live_migrate_server(self): expected_args=[self.proxy, "host1"], expected_kwargs={'force': False, 'block_migration': None}) + def test_abort_server_migration(self): + self._verify( + 'openstack.proxy.Proxy._delete', + self.proxy.abort_server_migration, + method_args=['server_migration', 'server'], + expected_args=[ + server_migration.ServerMigration, + 'server_migration', + ], + expected_kwargs={ + 'server_id': 'server', + 'ignore_missing': True, + }, + ) + + def test_force_complete_server_migration(self): + self._verify( + 'openstack.compute.v2.server_migration.ServerMigration.force_complete', # noqa: E501 + self.proxy.force_complete_server_migration, + method_args=['server_migration', 'server'], + expected_args=[self.proxy], + ) + + def test_get_server_migration(self): + self._verify( + 'openstack.proxy.Proxy._get', + self.proxy.get_server_migration, + method_args=['server_migration', 'server'], + expected_args=[ + server_migration.ServerMigration, + 'server_migration', + ], + expected_kwargs={ + 'server_id': 'server', + 'ignore_missing': True, + }, + ) + + def test_server_migrations(self): + self._verify( + 'openstack.proxy.Proxy._list', + self.proxy.server_migrations, + method_args=['server'], + expected_args=[server_migration.ServerMigration], + expected_kwargs={'server_id': 'server'}, + ) + def test_fetch_security_groups(self): self._verify( 'openstack.compute.v2.server.Server.fetch_security_groups', diff --git a/openstack/tests/unit/compute/v2/test_server_migration.py b/openstack/tests/unit/compute/v2/test_server_migration.py new file mode 100644 index 000000000..b652d38c8 --- /dev/null +++ b/openstack/tests/unit/compute/v2/test_server_migration.py @@ -0,0 +1,112 @@ +# 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 unittest import mock + +from openstack.compute.v2 import server_migration +from openstack.tests.unit import base + +EXAMPLE = { + 'id': 4, + 'server_uuid': '4cfba335-03d8-49b2-8c52-e69043d1e8fe', + 'user_id': '8dbaa0f0-ab95-4ffe-8cb4-9c89d2ac9d24', + 'project_id': '5f705771-3aa9-4f4c-8660-0d9522ffdbea', + 'created_at': '2016-01-29T13:42:02.000000', + 'updated_at': '2016-01-29T13:42:02.000000', + 'status': 'migrating', + 'source_compute': 'compute1', + 'source_node': 'node1', + 'dest_host': '1.2.3.4', + 'dest_compute': 'compute2', + 'dest_node': 'node2', + 'memory_processed_bytes': 12345, + 'memory_remaining_bytes': 111111, + 'memory_total_bytes': 123456, + 'disk_processed_bytes': 23456, + 'disk_remaining_bytes': 211111, + 'disk_total_bytes': 234567, +} + + +class TestServerMigration(base.TestCase): + + def setUp(self): + super().setUp() + self.resp = mock.Mock() + self.resp.body = None + self.resp.json = mock.Mock(return_value=self.resp.body) + self.resp.status_code = 200 + self.sess = mock.Mock() + self.sess.post = mock.Mock(return_value=self.resp) + + def test_basic(self): + sot = server_migration.ServerMigration() + self.assertEqual('migration', sot.resource_key) + self.assertEqual('migrations', sot.resources_key) + self.assertEqual('/servers/%(server_uuid)s/migrations', sot.base_path) + self.assertFalse(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_list) + self.assertFalse(sot.allow_commit) + self.assertTrue(sot.allow_delete) + + def test_make_it(self): + sot = server_migration.ServerMigration(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + # FIXME(stephenfin): This conflicts since there is a server ID in the + # URI *and* in the body. We need a field that handles both or we need + # to use different names. + # self.assertEqual(EXAMPLE['server_uuid'], sot.server_id) + self.assertEqual(EXAMPLE['user_id'], sot.user_id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + self.assertEqual(EXAMPLE['created_at'], sot.created_at) + self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) + self.assertEqual(EXAMPLE['status'], sot.status) + self.assertEqual(EXAMPLE['source_compute'], sot.source_compute) + self.assertEqual(EXAMPLE['source_node'], sot.source_node) + self.assertEqual(EXAMPLE['dest_host'], sot.dest_host) + self.assertEqual(EXAMPLE['dest_compute'], sot.dest_compute) + self.assertEqual(EXAMPLE['dest_node'], sot.dest_node) + self.assertEqual( + EXAMPLE['memory_processed_bytes'], + sot.memory_processed_bytes, + ) + self.assertEqual( + EXAMPLE['memory_remaining_bytes'], + sot.memory_remaining_bytes, + ) + self.assertEqual(EXAMPLE['memory_total_bytes'], sot.memory_total_bytes) + self.assertEqual( + EXAMPLE['disk_processed_bytes'], + sot.disk_processed_bytes, + ) + self.assertEqual( + EXAMPLE['disk_remaining_bytes'], + sot.disk_remaining_bytes, + ) + self.assertEqual(EXAMPLE['disk_total_bytes'], sot.disk_total_bytes) + + @mock.patch.object( + server_migration.ServerMigration, '_get_session', lambda self, x: x, + ) + def test_force_complete(self): + sot = server_migration.ServerMigration(**EXAMPLE) + + self.assertIsNone(sot.force_complete(self.sess)) + + url = 'servers/%s/migrations/%s/action' % ( + EXAMPLE['server_uuid'], EXAMPLE['id'] + ) + body = {'force_complete': None} + self.sess.post.assert_called_with( + url, microversion=mock.ANY, json=body, + ) diff --git a/releasenotes/notes/add-server-migrations-6e31183196f14deb.yaml b/releasenotes/notes/add-server-migrations-6e31183196f14deb.yaml new file mode 100644 index 000000000..8e451ceda --- /dev/null +++ b/releasenotes/notes/add-server-migrations-6e31183196f14deb.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add support for the Compute service's server migrations API, allowing users + to list all migrations for a server as well as force complete or abort + in-progress migrations. From 770a3c3ed8f3a922847842d8cf30779b99218575 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 15 Oct 2021 12:28:04 +0100 Subject: [PATCH 2945/3836] compute: Add support for migrations API Change Ideeca99a89c920a09cfc3799bbcc7e24046a5c43 added support for the server migrations API ('/servers/{id}/migrations'), which allowed interaction with individual server migration records. This change adds support for the migrations API ('/os-migrations'), which allows users to list all migrations. Change-Id: I681a5738c3ed359c2b3b26620c7fd3a51da3e302 Signed-off-by: Stephen Finucane --- doc/source/user/proxies/compute.rst | 7 ++ doc/source/user/resources/compute/index.rst | 1 + .../user/resources/compute/v2/migration.rst | 12 +++ openstack/compute/v2/_proxy.py | 11 +++ openstack/compute/v2/migration.py | 72 +++++++++++++++++ .../tests/unit/compute/v2/test_migration.py | 81 +++++++++++++++++++ openstack/tests/unit/compute/v2/test_proxy.py | 4 + .../add-migrations-946adf16674d4b2a.yaml | 5 ++ 8 files changed, 193 insertions(+) create mode 100644 doc/source/user/resources/compute/v2/migration.rst create mode 100644 openstack/compute/v2/migration.py create mode 100644 openstack/tests/unit/compute/v2/test_migration.py create mode 100644 releasenotes/notes/add-migrations-946adf16674d4b2a.yaml diff --git a/doc/source/user/proxies/compute.rst b/doc/source/user/proxies/compute.rst index fb9379802..ff9853375 100644 --- a/doc/source/user/proxies/compute.rst +++ b/doc/source/user/proxies/compute.rst @@ -165,3 +165,10 @@ Server Migration Operations :noindex: :members: abort_server_migration, force_complete_server_migration, get_server_migration, server_migrations + +Migration Operations +^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.compute.v2._proxy.Proxy + :noindex: + :members: migrations diff --git a/doc/source/user/resources/compute/index.rst b/doc/source/user/resources/compute/index.rst index aee022fdc..993cff52d 100644 --- a/doc/source/user/resources/compute/index.rst +++ b/doc/source/user/resources/compute/index.rst @@ -9,6 +9,7 @@ Compute Resources v2/image v2/keypair v2/limits + v2/migration v2/server v2/server_interface v2/server_migration diff --git a/doc/source/user/resources/compute/v2/migration.rst b/doc/source/user/resources/compute/v2/migration.rst new file mode 100644 index 000000000..ba02f3e17 --- /dev/null +++ b/doc/source/user/resources/compute/v2/migration.rst @@ -0,0 +1,12 @@ +openstack.compute.v2.migration +============================== + +.. automodule:: openstack.compute.v2.migration + +The Migration Class +------------------- + +The ``Migration`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.compute.v2.migration.Migration + :members: diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index c0df32926..d812d8977 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -20,6 +20,7 @@ from openstack.compute.v2 import image as _image from openstack.compute.v2 import keypair as _keypair from openstack.compute.v2 import limits +from openstack.compute.v2 import migration as _migration from openstack.compute.v2 import quota_set as _quota_set from openstack.compute.v2 import server as _server from openstack.compute.v2 import server_diagnostics as _server_diagnostics @@ -1830,6 +1831,16 @@ def server_migrations(self, server): server_id=server_id, ) + # ========== Migrations ========== + + def migrations(self): + """Return a generator of migrations for all servers. + + :returns: A generator of Migration objects + :rtype: :class:`~openstack.compute.v2.migration.Migration` + """ + return self._list(_migration.Migration) + # ========== Server diagnostics ========== def get_server_diagnostics(self, server): diff --git a/openstack/compute/v2/migration.py b/openstack/compute/v2/migration.py new file mode 100644 index 000000000..5fc8c8f5c --- /dev/null +++ b/openstack/compute/v2/migration.py @@ -0,0 +1,72 @@ +# 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 resource + + +class Migration(resource.Resource): + resources_key = 'migrations' + base_path = '/os-migrations' + + # capabilities + allow_list = True + + _query_mapping = resource.QueryParameters( + 'host', + 'status', + 'migration_type', + 'source_compute', + 'user_id', + 'project_id', + changes_since='changes-since', + changes_before='changes-before', + server_id='instance_uuid', + ) + + #: The date and time when the resource was created. + created_at = resource.Body('created_at') + #: The target compute of the migration. + dest_compute = resource.Body('dest_compute') + #: The target host of the migration. + dest_host = resource.Body('dest_host') + #: The target node of the migration. + dest_node = resource.Body('dest_node') + #: The type of the migration. One of 'migration', 'resize', + #: 'live-migration' or 'evacuation' + migration_type = resource.Body('migration_type') + #: The ID of the old flavor. This value corresponds to the ID of the flavor + #: in the database. This will be the same as new_flavor_id except for + #: resize operations. + new_flavor_id = resource.Body('new_instance_type_id') + #: The ID of the old flavor. This value corresponds to the ID of the flavor + #: in the database. + old_flavor_id = resource.Body('old_instance_type_id') + #: The ID of the project that initiated the server migration (since + #: microversion 2.80) + project_id = resource.Body('project_id') + #: The UUID of the server + server_id = resource.Body('instance_uuid') + #: The source compute of the migration. + source_compute = resource.Body('source_compute') + #: The source node of the migration. + source_node = resource.Body('source_node') + #: The current status of the migration. + status = resource.Body('status') + #: The date and time when the resource was last updated. + updated_at = resource.Body('updated_at') + #: The ID of the user that initiated the server migration (since + #: microversion 2.80) + user_id = resource.Body('user_id') + #: The UUID of the migration (since microversion 2.59) + uuid = resource.Body('uuid', alternate_id=True) + + _max_microversion = '2.80' diff --git a/openstack/tests/unit/compute/v2/test_migration.py b/openstack/tests/unit/compute/v2/test_migration.py new file mode 100644 index 000000000..ad4315767 --- /dev/null +++ b/openstack/tests/unit/compute/v2/test_migration.py @@ -0,0 +1,81 @@ +# 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.compute.v2 import migration +from openstack.tests.unit import base + +EXAMPLE = { + 'uuid': '42341d4b-346a-40d0-83c6-5f4f6892b650', + 'instance_uuid': '9128d044-7b61-403e-b766-7547076ff6c1', + 'user_id': '78348f0e-97ee-4d70-ad34-189692673ea2', + 'project_id': '9842f0f7-1229-4355-afe7-15ebdbb8c3d8', + 'created_at': '2016-06-23T14:42:02.000000', + 'updated_at': '2016-06-23T14:42:02.000000', + 'status': 'migrating', + 'source_compute': 'compute10', + 'source_node': 'node10', + 'dest_host': '5.6.7.8', + 'dest_compute': 'compute20', + 'dest_node': 'node20', + 'migration_type': 'resize', + 'old_instance_type_id': 5, + 'new_instance_type_id': 6, +} + + +class TestMigration(base.TestCase): + + def test_basic(self): + sot = migration.Migration() + self.assertIsNone(sot.resource_key) # we don't support fetch + self.assertEqual('migrations', sot.resources_key) + self.assertEqual('/os-migrations', sot.base_path) + self.assertFalse(sot.allow_create) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_delete) + self.assertTrue(sot.allow_list) + + self.assertDictEqual( + { + 'limit': 'limit', + 'marker': 'marker', + 'host': 'host', + 'status': 'status', + 'migration_type': 'migration_type', + 'source_compute': 'source_compute', + 'user_id': 'user_id', + 'project_id': 'project_id', + 'changes_since': 'changes-since', + 'changes_before': 'changes-before', + 'server_id': 'instance_uuid', + }, + sot._query_mapping._mapping, + ) + + def test_make_it(self): + sot = migration.Migration(**EXAMPLE) + self.assertEqual(EXAMPLE['uuid'], sot.id) + self.assertEqual(EXAMPLE['instance_uuid'], sot.server_id) + self.assertEqual(EXAMPLE['user_id'], sot.user_id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + self.assertEqual(EXAMPLE['created_at'], sot.created_at) + self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) + self.assertEqual(EXAMPLE['status'], sot.status) + self.assertEqual(EXAMPLE['source_compute'], sot.source_compute) + self.assertEqual(EXAMPLE['source_node'], sot.source_node) + self.assertEqual(EXAMPLE['dest_host'], sot.dest_host) + self.assertEqual(EXAMPLE['dest_compute'], sot.dest_compute) + self.assertEqual(EXAMPLE['dest_node'], sot.dest_node) + self.assertEqual(EXAMPLE['migration_type'], sot.migration_type) + self.assertEqual(EXAMPLE['old_instance_type_id'], sot.old_flavor_id) + self.assertEqual(EXAMPLE['new_instance_type_id'], sot.new_flavor_id) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 5ecfa9cd6..b8687690e 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -21,6 +21,7 @@ from openstack.compute.v2 import image from openstack.compute.v2 import keypair from openstack.compute.v2 import limits +from openstack.compute.v2 import migration from openstack.compute.v2 import quota_set from openstack.compute.v2 import server from openstack.compute.v2 import server_group @@ -1052,6 +1053,9 @@ def test_server_migrations(self): expected_kwargs={'server_id': 'server'}, ) + def test_migrations(self): + self.verify_list(self.proxy.migrations, migration.Migration) + def test_fetch_security_groups(self): self._verify( 'openstack.compute.v2.server.Server.fetch_security_groups', diff --git a/releasenotes/notes/add-migrations-946adf16674d4b2a.yaml b/releasenotes/notes/add-migrations-946adf16674d4b2a.yaml new file mode 100644 index 000000000..f199f91f2 --- /dev/null +++ b/releasenotes/notes/add-migrations-946adf16674d4b2a.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add support for the Compute service's migrations API, allowing users to + list all in-progress migrations for all servers. From ef587c9a021331f6407320b606a741719c90fafa Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 2 Nov 2021 17:04:31 +0000 Subject: [PATCH 2946/3836] compute: Add support for microversion 2.89 The volume attachments API now returns the attachment ID and internal BDM ID in responses. Change-Id: Ie1482dcffc534893c5d922910afe1c9b40c54fbb Signed-off-by: Stephen Finucane --- openstack/compute/v2/volume_attachment.py | 17 ++++++++---- .../unit/compute/v2/test_volume_attachment.py | 27 ++++++++++++------- ...te-microversion-2-89-8c5187cc3bf6bd02.yaml | 7 +++++ 3 files changed, 37 insertions(+), 14 deletions(-) create mode 100644 releasenotes/notes/compute-microversion-2-89-8c5187cc3bf6bd02.yaml diff --git a/openstack/compute/v2/volume_attachment.py b/openstack/compute/v2/volume_attachment.py index 45fc45bc2..eee123d77 100644 --- a/openstack/compute/v2/volume_attachment.py +++ b/openstack/compute/v2/volume_attachment.py @@ -27,19 +27,26 @@ class VolumeAttachment(resource.Resource): _query_mapping = resource.QueryParameters("limit", "offset") + #: The ID for the server. + server_id = resource.URI('server_id') #: Name of the device such as, /dev/vdb. device = resource.Body('device') #: The ID of the attachment. id = resource.Body('id') - #: The ID for the server. - server_id = resource.URI('server_id') + # FIXME(stephenfin): This conflicts since there is a server ID in the URI + # *and* in the body. We need a field that handles both or we need to use + # different names. + # #: The UUID of the server + # server_id = resource.Body('server_uuid') #: The ID of the attached volume. volume_id = resource.Body('volumeId') - #: The ID of the attachment you want to delete or update. + #: The UUID of the associated volume attachment in Cinder. attachment_id = resource.Body('attachment_id', alternate_id=True) + #: The ID of the block device mapping record for the attachment + bdm_id = resource.Body('bdm_uuid') #: Virtual device tags for the attachment. tag = resource.Body('tag') #: Indicates whether to delete the volume when server is destroyed delete_on_termination = resource.Body('delete_on_termination') - # delete_on_termination introduced in 2.79 - _max_microversion = '2.79' + # attachment_id (in responses) and bdm_id introduced in 2.89 + _max_microversion = '2.89' diff --git a/openstack/tests/unit/compute/v2/test_volume_attachment.py b/openstack/tests/unit/compute/v2/test_volume_attachment.py index 03d031c1f..d623fc523 100644 --- a/openstack/tests/unit/compute/v2/test_volume_attachment.py +++ b/openstack/tests/unit/compute/v2/test_volume_attachment.py @@ -15,11 +15,13 @@ EXAMPLE = { - 'device': '1', - 'id': '2', - 'volume_id': '3', - 'tag': '4', - 'delete_on_termination': 'true', + 'attachment_id': '979ce4f8-033a-409d-85e6-6b5c0f6a6302', + 'delete_on_termination': False, + 'device': '/dev/sdc', + 'serverId': '7696780b-3f53-4688-ab25-019bfcbbd806', + 'tag': 'foo', + 'volumeId': 'a07f71dc-8151-4e7d-a0cc-cd24a3f11113', + 'bdm_uuid': 'c088db45-92b8-49e8-81e2-a1b77a144b3b', } @@ -43,9 +45,16 @@ def test_basic(self): def test_make_it(self): sot = volume_attachment.VolumeAttachment(**EXAMPLE) + self.assertEqual(EXAMPLE['attachment_id'], sot.attachment_id) + self.assertEqual(EXAMPLE['attachment_id'], sot.id) + self.assertEqual( + EXAMPLE['delete_on_termination'], sot.delete_on_termination, + ) self.assertEqual(EXAMPLE['device'], sot.device) - self.assertEqual(EXAMPLE['id'], sot.id) - self.assertEqual(EXAMPLE['volume_id'], sot.volume_id) + # FIXME(stephenfin): This conflicts since there is a server ID in the + # URI *and* in the body. We need a field that handles both or we need + # to use different names. + # self.assertEqual(EXAMPLE['serverId'], sot.server_id) self.assertEqual(EXAMPLE['tag'], sot.tag) - self.assertEqual(EXAMPLE['delete_on_termination'], - sot.delete_on_termination) + self.assertEqual(EXAMPLE['volumeId'], sot.volume_id) + self.assertEqual(EXAMPLE['bdm_uuid'], sot.bdm_id) diff --git a/releasenotes/notes/compute-microversion-2-89-8c5187cc3bf6bd02.yaml b/releasenotes/notes/compute-microversion-2-89-8c5187cc3bf6bd02.yaml new file mode 100644 index 000000000..e22a340dc --- /dev/null +++ b/releasenotes/notes/compute-microversion-2-89-8c5187cc3bf6bd02.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + The 2.89 API microversion is now supported for the compute service. This + adds additional fields to the ``os-volume_attachments`` API, represented + by the ``openstack.compute.v2.volume_attachment.VolumeAttachment`` + resource. From 72c223b52ce9a5e86b41954358c90581015b7ee0 Mon Sep 17 00:00:00 2001 From: Kafilat Adeleke Date: Sun, 20 Jun 2021 09:16:10 +0000 Subject: [PATCH 2947/3836] Add share snapshot to shared file system. Introduce Share Snapshot class with basic methods including list, create, delete, get and update to shared file systems service. Change-Id: Ie3fb8c7b14276d505a415aae857e2c6e5fefa888 --- .../user/proxies/shared_file_system.rst | 12 +++ .../resources/shared_file_system/index.rst | 1 + .../shared_file_system/v2/share_snapshot.rst | 13 +++ openstack/shared_file_system/v2/_proxy.py | 74 ++++++++++++++++ .../shared_file_system/v2/share_snapshot.py | 58 +++++++++++++ .../shared_file_system/test_share_snapshot.py | 84 +++++++++++++++++++ .../unit/shared_file_system/v2/test_proxy.py | 57 +++++++++++++ .../v2/test_share_snapshot.py | 65 ++++++++++++++ ...pshot-to-shared-file-82ecedbdbed2e3c5.yaml | 5 ++ 9 files changed, 369 insertions(+) create mode 100644 doc/source/user/resources/shared_file_system/v2/share_snapshot.rst create mode 100644 openstack/shared_file_system/v2/share_snapshot.py create mode 100644 openstack/tests/functional/shared_file_system/test_share_snapshot.py create mode 100644 openstack/tests/unit/shared_file_system/v2/test_share_snapshot.py create mode 100644 releasenotes/notes/add-share-snapshot-to-shared-file-82ecedbdbed2e3c5.yaml diff --git a/doc/source/user/proxies/shared_file_system.rst b/doc/source/user/proxies/shared_file_system.rst index df02c4b74..0ea5925d5 100644 --- a/doc/source/user/proxies/shared_file_system.rst +++ b/doc/source/user/proxies/shared_file_system.rst @@ -66,3 +66,15 @@ service. .. autoclass:: openstack.shared_file_system.v2._proxy.Proxy :noindex: :members: limits + + +Shared File System Snapshots +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Interact with Share Snapshots supported by the Shared File Systems +service. + +.. autoclass:: openstack.shared_file_system.v2._proxy.Proxy + :noindex: + :members: share_snapshots, get_share_snapshot, delete_share_snapshot, + update_share_snapshot, create_share_snapshot diff --git a/doc/source/user/resources/shared_file_system/index.rst b/doc/source/user/resources/shared_file_system/index.rst index 459eb403a..0a4cc5b4e 100644 --- a/doc/source/user/resources/shared_file_system/index.rst +++ b/doc/source/user/resources/shared_file_system/index.rst @@ -9,3 +9,4 @@ Shared File System service resources v2/limit v2/share v2/user_message + v2/share_snapshot diff --git a/doc/source/user/resources/shared_file_system/v2/share_snapshot.rst b/doc/source/user/resources/shared_file_system/v2/share_snapshot.rst new file mode 100644 index 000000000..4063c4873 --- /dev/null +++ b/doc/source/user/resources/shared_file_system/v2/share_snapshot.rst @@ -0,0 +1,13 @@ +openstack.shared_file_system.v2.share_snapshot +============================================== + +.. automodule:: openstack.shared_file_system.v2.share_snapshot + +The ShareSnapshot Class +----------------------- + +The ``ShareSnapshot`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.shared_file_system.v2.share_snapshot.ShareSnapshot + :members: diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index 2d4c32f60..4d34fd293 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -14,6 +14,9 @@ from openstack import resource from openstack.shared_file_system.v2 import ( availability_zone as _availability_zone) +from openstack.shared_file_system.v2 import ( + share_snapshot as _share_snapshot +) from openstack.shared_file_system.v2 import ( storage_pool as _storage_pool ) @@ -238,3 +241,74 @@ def limits(self, **query): """ return self._list( _limit.Limit, **query) + + def share_snapshots(self, details=True, **query): + """Lists all share snapshots with details. + + :param kwargs query: Optional query parameters to be sent to limit + the snapshots being returned. Available parameters include: + + * project_id: The ID of the user or service making the API request. + + :returns: A generator of manila share snapshot resources + :rtype: :class:`~openstack.shared_file_system.v2. + share_snapshot.ShareSnapshot` + """ + base_path = '/snapshots/detail' if details else None + return self._list( + _share_snapshot.ShareSnapshot, base_path=base_path, **query) + + def get_share_snapshot(self, snapshot_id): + """Lists details of a single share snapshot + + :param snapshot_id: The ID of the snapshot to get + :returns: Details of the identified share snapshot + :rtype: :class:`~openstack.shared_file_system.v2. + share_snapshot.ShareSnapshot` + """ + return self._get(_share_snapshot.ShareSnapshot, snapshot_id) + + def create_share_snapshot(self, **attrs): + """Creates a share snapshot from attributes + + :returns: Details of the new share snapshot + :rtype: :class:`~openstack.shared_file_system.v2. + share_snapshot.ShareSnapshot` + """ + return self._create(_share_snapshot.ShareSnapshot, **attrs) + + def update_share_snapshot(self, snapshot_id, **attrs): + """Updates details of a single share. + + :param snapshot_id: The ID of the snapshot to update + :pram dict attrs: The attributes to update on the snapshot + :returns: the updated share snapshot + :rtype: :class:`~openstack.shared_file_system.v2. + share_snapshot.ShareSnapshot` + """ + return self._update(_share_snapshot.ShareSnapshot, snapshot_id, + **attrs) + + def delete_share_snapshot(self, snapshot_id, ignore_missing=True): + """Deletes a single share snapshot + + :param snapshot_id: The ID of the snapshot to delete + :returns: Result of the ``delete`` + :rtype: ``None`` + """ + self._delete(_share_snapshot.ShareSnapshot, snapshot_id, + ignore_missing=ignore_missing) + + def wait_for_delete(self, res, interval=2, wait=120): + """Wait for a resource to be deleted. + :param res: The resource to wait on to be deleted. + :type resource: A :class:`~openstack.resource.Resource` object. + :param interval: Number of seconds to wait before to consecutive + checks. Default to 2. + :param wait: Maximum number of seconds to wait before the change. + Default to 120. + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete(self, res, interval, wait) diff --git a/openstack/shared_file_system/v2/share_snapshot.py b/openstack/shared_file_system/v2/share_snapshot.py new file mode 100644 index 000000000..c270c23aa --- /dev/null +++ b/openstack/shared_file_system/v2/share_snapshot.py @@ -0,0 +1,58 @@ +# 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 resource + + +class ShareSnapshot(resource.Resource): + resource_key = "snapshot" + resources_key = "snapshots" + base_path = "/snapshots" + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + allow_head = False + + _query_mapping = resource.QueryParameters( + "snapshot_id" + ) + + #: Properties + #: The date and time stamp when the resource was + #: created within the service’s database. + created_at = resource.Body("created_at") + #: The user defined description of the resource. + description = resource.Body("description", type=str) + #: The user defined name of the resource. + display_name = resource.Body("display_name", type=str) + #: The user defined description of the resource + display_description = resource.Body( + "display_description", type=str) + #: ID of the project that the snapshot belongs to. + project_id = resource.Body("project_id", type=str) + #: The UUID of the source share that was used to + #: create the snapshot. + share_id = resource.Body("share_id", type=str) + #: The file system protocol of a share snapshot + share_proto = resource.Body("share_proto", type=str) + #: The snapshot's source share's size, in GiBs. + share_size = resource.Body("share_size", type=int) + #: The snapshot size, in GiBs. + size = resource.Body("size", type=int) + #: The snapshot status + status = resource.Body("status", type=str) + #: ID of the user that the snapshot was created by. + user_id = resource.Body("user_id", type=str) diff --git a/openstack/tests/functional/shared_file_system/test_share_snapshot.py b/openstack/tests/functional/shared_file_system/test_share_snapshot.py new file mode 100644 index 000000000..43bf4229a --- /dev/null +++ b/openstack/tests/functional/shared_file_system/test_share_snapshot.py @@ -0,0 +1,84 @@ +# 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.tests.functional.shared_file_system import base + + +class ShareSnapshotTest(base.BaseSharedFileSystemTest): + + def setUp(self): + super(ShareSnapshotTest, self).setUp() + + self.SHARE_NAME = self.getUniqueString() + self.SNAPSHOT_NAME = self.getUniqueString() + my_share = self.operator_cloud.shared_file_system.create_share( + name=self.SHARE_NAME, size=2, share_type="dhss_false", + share_protocol='NFS', description=None) + self.operator_cloud.shared_file_system.wait_for_status( + my_share, + status='available', + failures=['error'], + interval=5, + wait=self._wait_for_timeout) + self.assertIsNotNone(my_share) + self.assertIsNotNone(my_share.id) + self.SHARE_ID = my_share.id + + msp = self.operator_cloud.shared_file_system.create_share_snapshot( + share_id=self.SHARE_ID, name=self.SNAPSHOT_NAME, force=True) + self.operator_cloud.shared_file_system.wait_for_status( + msp, + status='available', + failures=['error'], + interval=5, + wait=self._wait_for_timeout) + self.assertIsNotNone(msp.id) + self.SNAPSHOT_ID = msp.id + + def tearDown(self): + snpt = self.operator_cloud.shared_file_system.get_share_snapshot( + self.SNAPSHOT_ID) + sot = self.operator_cloud.shared_file_system.delete_share_snapshot( + snpt, ignore_missing=False + ) + self.operator_cloud.shared_file_system.wait_for_delete( + snpt, interval=2, wait=self._wait_for_timeout) + self.assertIsNone(sot) + sot = self.operator_cloud.shared_file_system.delete_share( + self.SHARE_ID, + ignore_missing=False) + self.assertIsNone(sot) + super(ShareSnapshotTest, self).tearDown() + + def test_get(self): + sot = self.operator_cloud.shared_file_system.get_share_snapshot( + self.SNAPSHOT_ID + ) + self.assertEqual(self.SNAPSHOT_NAME, sot.name) + + def test_list(self): + snaps = self.operator_cloud.shared_file_system.share_snapshots( + details=True + ) + self.assertGreater(len(list(snaps)), 0) + for snap in snaps: + for attribute in ('id', 'name', 'created_at', 'updated_at', + 'description', 'share_id', 'share_proto', + 'share_size', 'size', 'status', 'user_id'): + self.assertTrue(hasattr(snap, attribute)) + + def test_update(self): + u_snap = self.operator_cloud.shared_file_system.update_share_snapshot( + self.SNAPSHOT_ID, display_description='updated share snapshot') + get_u_snap = self.operator_cloud.shared_file_system.get_share_snapshot( + u_snap.id) + self.assertEqual('updated share snapshot', get_u_snap.description) diff --git a/openstack/tests/unit/shared_file_system/v2/test_proxy.py b/openstack/tests/unit/shared_file_system/v2/test_proxy.py index 573a354d2..2f6121d4c 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_proxy.py +++ b/openstack/tests/unit/shared_file_system/v2/test_proxy.py @@ -15,6 +15,7 @@ from openstack.shared_file_system.v2 import _proxy from openstack.shared_file_system.v2 import limit from openstack.shared_file_system.v2 import share +from openstack.shared_file_system.v2 import share_snapshot from openstack.shared_file_system.v2 import storage_pool from openstack.shared_file_system.v2 import user_message from openstack.tests.unit import test_proxy_base @@ -111,3 +112,59 @@ def test_delete_user_message_true(self): def test_limit(self): self.verify_list(self.proxy.limits, limit.Limit) + + +class TestShareSnapshotResource(test_proxy_base.TestProxyBase): + + def setUp(self): + super(TestShareSnapshotResource, self).setUp() + self.proxy = _proxy.Proxy(self.session) + + def test_share_snapshots(self): + self.verify_list( + self.proxy.share_snapshots, share_snapshot.ShareSnapshot) + + def test_share_snapshots_detailed(self): + self.verify_list( + self.proxy.share_snapshots, + share_snapshot.ShareSnapshot, + method_kwargs={"details": True, "name": "my_snapshot"}, + expected_kwargs={"name": "my_snapshot"}) + + def test_share_snapshots_not_detailed(self): + self.verify_list( + self.proxy.share_snapshots, + share_snapshot.ShareSnapshot, + method_kwargs={"details": False, "name": "my_snapshot"}, + expected_kwargs={"name": "my_snapshot"}) + + def test_share_snapshot_get(self): + self.verify_get( + self.proxy.get_share_snapshot, share_snapshot.ShareSnapshot) + + def test_share_snapshot_delete(self): + self.verify_delete( + self.proxy.delete_share_snapshot, + share_snapshot.ShareSnapshot, False) + + def test_share_snapshot_delete_ignore(self): + self.verify_delete( + self.proxy.delete_share_snapshot, + share_snapshot.ShareSnapshot, True) + + def test_share_snapshot_create(self): + self.verify_create( + self.proxy.create_share_snapshot, share_snapshot.ShareSnapshot) + + def test_share_snapshot_update(self): + self.verify_update( + self.proxy.update_share_snapshot, share_snapshot.ShareSnapshot) + + @mock.patch("openstack.resource.wait_for_delete") + def test_wait_for_delete(self, mock_wait): + mock_resource = mock.Mock() + mock_wait.return_value = mock_resource + + self.proxy.wait_for_delete(mock_resource) + + mock_wait.assert_called_once_with(self.proxy, mock_resource, 2, 120) diff --git a/openstack/tests/unit/shared_file_system/v2/test_share_snapshot.py b/openstack/tests/unit/shared_file_system/v2/test_share_snapshot.py new file mode 100644 index 000000000..744e7269e --- /dev/null +++ b/openstack/tests/unit/shared_file_system/v2/test_share_snapshot.py @@ -0,0 +1,65 @@ +# 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.shared_file_system.v2 import share_snapshot +from openstack.tests.unit import base + + +EXAMPLE = { + "status": "creating", + "share_id": "406ea93b-32e9-4907-a117-148b3945749f", + "user_id": "5c7bdb6eb0504d54a619acf8375c08ce", + "name": "snapshot_share1", + "created_at": "2021-06-07T11:50:39.756808", + "description": "Here is a snapshot of share Share1", + "share_proto": "NFS", + "share_size": 1, + "id": "6d221c1d-0200-461e-8d20-24b4776b9ddb", + "project_id": "cadd7139bc3148b8973df097c0911016", + "size": 1 +} + + +class TestShareSnapshot(base.TestCase): + + def test_basic(self): + snapshot_resource = share_snapshot.ShareSnapshot() + self.assertEqual( + 'snapshots', snapshot_resource.resources_key) + self.assertEqual('/snapshots', snapshot_resource.base_path) + self.assertTrue(snapshot_resource.allow_list) + + self.assertDictEqual({ + "limit": "limit", + "marker": "marker", + "snapshot_id": "snapshot_id" + }, + snapshot_resource._query_mapping._mapping) + + def test_make_share_snapshot(self): + snapshot_resource = share_snapshot.ShareSnapshot(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], snapshot_resource.id) + self.assertEqual(EXAMPLE['share_id'], snapshot_resource.share_id) + self.assertEqual(EXAMPLE['user_id'], + snapshot_resource.user_id) + self.assertEqual(EXAMPLE['created_at'], snapshot_resource.created_at) + self.assertEqual(EXAMPLE['status'], snapshot_resource.status) + self.assertEqual(EXAMPLE['name'], snapshot_resource.name) + self.assertEqual( + EXAMPLE['description'], snapshot_resource.description) + self.assertEqual( + EXAMPLE['share_proto'], snapshot_resource.share_proto) + self.assertEqual( + EXAMPLE['share_size'], snapshot_resource.share_size) + self.assertEqual( + EXAMPLE['project_id'], snapshot_resource.project_id) + self.assertEqual(EXAMPLE['size'], snapshot_resource.size) diff --git a/releasenotes/notes/add-share-snapshot-to-shared-file-82ecedbdbed2e3c5.yaml b/releasenotes/notes/add-share-snapshot-to-shared-file-82ecedbdbed2e3c5.yaml new file mode 100644 index 000000000..294fddca0 --- /dev/null +++ b/releasenotes/notes/add-share-snapshot-to-shared-file-82ecedbdbed2e3c5.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Adds support to create, update, list, get, and delete share + snapshots to shared file system service. From 31c85038fb4c039014d8f1ba99f66c41b159f121 Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Tue, 2 Nov 2021 09:46:55 +0100 Subject: [PATCH 2948/3836] Shared File System - reindentation of the docstrings Change-Id: I561d90b5fd0605b47fc4ecd92669f17b6c5035d6 --- openstack/shared_file_system/v2/_proxy.py | 78 +++++++++++------------ 1 file changed, 36 insertions(+), 42 deletions(-) diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index 2d4c32f60..7c4c5e783 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -30,8 +30,8 @@ def availability_zones(self): """Retrieve shared file system availability zones :returns: A generator of availability zone resources - :rtype: :class:`~openstack.shared_file_system.v2. - \availability_zone.AvailabilityZone` + :rtype: + :class:`~openstack.shared_file_system.v2.availability_zone.AvailabilityZone` """ return self._list(_availability_zone.AvailabilityZone) @@ -44,42 +44,41 @@ def shares(self, details=True, **query): * status: Filters by a share status * share_server_id: The UUID of the share server. * metadata: One or more metadata key and value pairs as a url - encoded dictionary of strings. + encoded dictionary of strings. * extra_specs: The extra specifications as a set of one or more - key-value pairs. + key-value pairs. * share_type_id: The UUID of a share type to query resources by. * name: The user defined name of the resource to filter resources - by. + by. * snapshot_id: The UUID of the share’s base snapshot to filter - the request based on. + the request based on. * host: The host name of the resource to query with. * share_network_id: The UUID of the share network to filter - resources by. + resources by. * project_id: The ID of the project that owns the resource. * is_public: A boolean query parameter that, when set to true, - allows retrieving public resources that belong to - all projects. + allows retrieving public resources that belong to + all projects. * share_group_id: The UUID of a share group to filter resource. * export_location_id: The export location UUID that can be used - to filter shares or share instances. + to filter shares or share instances. * export_location_path: The export location path that can be used - to filter shares or share instances. + to filter shares or share instances. * name~: The name pattern that can be used to filter shares, share - snapshots, share networks or share groups. + snapshots, share networks or share groups. * description~: The description pattern that can be used to filter - shares, share snapshots, share networks or share groups. + shares, share snapshots, share networks or share groups. * with_count: Whether to show count in API response or not, - default is False. + default is False. * limit: The maximum number of shares to return. * offset: The offset to define start point of share or share group - listing. + listing. * sort_key: The key to sort a list of shares. * sort_dir: The direction to sort a list of shares. A valid value - is asc, or desc. + is asc, or desc. :returns: Details of shares resources - :rtype: :class:`~openstack.shared_file_system.v2. - share.Share` + :rtype: :class:`~openstack.shared_file_system.v2.share.Share` """ base_path = '/shares/detail' if details else None return self._list(_share.Share, base_path=base_path, **query) @@ -89,8 +88,7 @@ def get_share(self, share_id): :param share: The ID of the share to get :returns: Details of the identified share - :rtype: :class:`~openstack.shared_file_system.v2. - share.Share` + :rtype: :class:`~openstack.shared_file_system.v2.share.Share` """ return self._get(_share.Share, share_id) @@ -108,10 +106,9 @@ def update_share(self, share_id, **attrs): """Updates details of a single share. :param share: The ID of the share to update - :pram dict attrs: The attributes to update on the share + :param dict attrs: The attributes to update on the share :returns: the updated share - :rtype: :class:`~openstack.shared_file_system.v2. - share.Share` + :rtype: :class:`~openstack.shared_file_system.v2.share.Share` """ return self._update(_share.Share, share_id, **attrs) @@ -120,12 +117,10 @@ def create_share(self, **attrs): :returns: Details of the new share :param dict attrs: Attributes which will be used to create - a :class:`~openstack.shared_file_system.v2. - shares.Shares`,comprised of the properties - on the Shares class. 'size' and 'share' + a :class:`~openstack.shared_file_system.v2.shares.Shares`, + comprised of the properties on the Shares class. 'size' and 'share' are required to create a share. - :rtype: :class:`~openstack.shared_file_system.v2. - share.Share` + :rtype: :class:`~openstack.shared_file_system.v2.share.Share` """ return self._create(_share.Share, **attrs) @@ -166,8 +161,8 @@ def storage_pools(self, details=True, **query): * capabilities: The capabilities for the storage back end. * share_type: The share type name or UUID. :returns: A generator of manila storage pool resources - :rtype: :class:`~openstack.shared_file_system.v2. - storage_pool.StoragePool` + :rtype: + :class:`~openstack.shared_file_system.v2.storage_pool.StoragePool` """ base_path = '/scheduler-stats/pools/detail' if details else None return self._list( @@ -180,26 +175,26 @@ def user_messages(self, **query): the messages being returned. Available parameters include: * action_id: The ID of the action during which the message - was created. + was created. * detail_id: The ID of the message detail. * limit: The maximum number of shares to return. * message_level: The message level. * offset: The offset to define start point of share or share - group listing. + group listing. * sort_key: The key to sort a list of messages. * sort_dir: The direction to sort a list of shares. * project_id: The ID of the project for which the message - was created. + was created. * request_id: The ID of the request during which the message - was created. + was created. * resource_id: The UUID of the resource for which the message - was created. + was created. * resource_type: The type of the resource for which the message - was created. + was created. :returns: A generator of user message resources - :rtype: :class:`~openstack.shared_file_system.v2. - user_message.UserMessage` + :rtype: + :class:`~openstack.shared_file_system.v2.user_message.UserMessage` """ return self._list( _user_message.UserMessage, **query) @@ -209,8 +204,8 @@ def get_user_message(self, message_id): :param message_id: The ID of the user message :returns: Details of the identified user message - :rtype: :class:`~openstack.shared_file_system.v2. - user_message.UserMessage` + :rtype: + :class:`~openstack.shared_file_system.v2.user_message.UserMessage` """ return self._get(_user_message.UserMessage, message_id) @@ -233,8 +228,7 @@ def limits(self, **query): the share limits being returned. :returns: A generator of manila share limits resources - :rtype: :class:`~openstack.shared_file_system.v2. - limit.Limit` + :rtype: :class:`~openstack.shared_file_system.v2.limit.Limit` """ return self._list( _limit.Limit, **query) From 9ed215d4443c08167117144e0ee938b418bf56c2 Mon Sep 17 00:00:00 2001 From: "Dr. Jens Harbott" Date: Wed, 20 Oct 2021 07:30:30 +0200 Subject: [PATCH 2949/3836] Keep creating keystone admin endpoint for heat Heat still depends on an keystone admin endpoint being present although it has no functional difference anymore from the other endpoints with the v3 API. Make devstack create one. Change-Id: Icd786e89aba5b593feee165226010b25f8627efd --- .zuul.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.zuul.yaml b/.zuul.yaml index 9e772f49e..7724b74f6 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -123,6 +123,8 @@ devstack_localrc: DISABLE_AMP_IMAGE_BUILD: true Q_SERVICE_PLUGIN_CLASSES: qos,trunk + # TODO(frickler): drop this once heat no longer needs it + KEYSTONE_ADMIN_ENDPOINT: true devstack_plugins: heat: https://opendev.org/openstack/heat tox_environment: From ef1bee167879d65bb4e625f62c410a6f4e48407e Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 22 Oct 2021 17:17:50 +0200 Subject: [PATCH 2950/3836] Switch compute limits cloud to proxy - Switch to proxy layer in the cloud.compute.get_limits - Add tenant_id query parameter to compute.limits Change-Id: Ie58c71db2ee6178542b4338f075d77f0c8aadb1f --- openstack/cloud/_compute.py | 8 +------- openstack/compute/v2/_proxy.py | 11 +++++++---- openstack/compute/v2/limits.py | 13 ++++++++++--- openstack/tests/unit/compute/v2/test_limits.py | 8 ++++++++ openstack/tests/unit/compute/v2/test_proxy.py | 10 ++++++++-- 5 files changed, 34 insertions(+), 16 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 2c46906bd..0c5792584 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -349,7 +349,6 @@ def get_compute_limits(self, name_or_id=None): """ params = {} project_id = None - error_msg = "Failed to get limits" if name_or_id: proj = self.get_project(name_or_id) @@ -357,12 +356,7 @@ def get_compute_limits(self, name_or_id=None): raise exc.OpenStackCloudException("project does not exist") project_id = proj.id params['tenant_id'] = project_id - error_msg = "{msg} for the project: {project} ".format( - msg=error_msg, project=name_or_id) - - data = proxy._json_response( - self.compute.get('/limits', params=params)) - limits = self._get_and_munchify('limits', data) + limits = self.compute.get_limits(**params) return self._normalize_compute_limits(limits, project_id=project_id) def get_keypair(self, name_or_id, filters=None): diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index d812d8977..966323090 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -601,15 +601,18 @@ def keypairs(self, **query): # ========== Limits ========== - def get_limits(self): + def get_limits(self, **query): """Retrieve limits that are applied to the project's account :returns: A Limits object, including both - :class:`~openstack.compute.v2.limits.AbsoluteLimits` and - :class:`~openstack.compute.v2.limits.RateLimits` + :class:`~openstack.compute.v2.limits.AbsoluteLimits` and + :class:`~openstack.compute.v2.limits.RateLimits` :rtype: :class:`~openstack.compute.v2.limits.Limits` """ - return self._get(limits.Limits) + res = self._get_resource( + limits.Limits, None) + return res.fetch( + self, **query) # ========== Servers ========== diff --git a/openstack/compute/v2/limits.py b/openstack/compute/v2/limits.py index 57104eeb8..a14da52df 100644 --- a/openstack/compute/v2/limits.py +++ b/openstack/compute/v2/limits.py @@ -75,11 +75,15 @@ class Limits(resource.Resource): allow_fetch = True + _query_mapping = resource.QueryParameters( + 'tenant_id' + ) + absolute = resource.Body("absolute", type=AbsoluteLimits) rate = resource.Body("rate", type=list, list_type=RateLimit) def fetch(self, session, requires_id=False, error_message=None, - base_path=None): + base_path=None, **params): """Get the Limits resource. :param session: The session to use for making this request. @@ -91,5 +95,8 @@ def fetch(self, session, requires_id=False, error_message=None, # TODO(mordred) We shouldn't have to subclass just to declare # requires_id = False. return super(Limits, self).fetch( - session=session, requires_id=False, error_message=error_message, - base_path=base_path) + session=session, requires_id=requires_id, + error_message=error_message, + base_path=base_path, + **params + ) diff --git a/openstack/tests/unit/compute/v2/test_limits.py b/openstack/tests/unit/compute/v2/test_limits.py index 872e02bfe..3c44d90cb 100644 --- a/openstack/tests/unit/compute/v2/test_limits.py +++ b/openstack/tests/unit/compute/v2/test_limits.py @@ -138,6 +138,14 @@ def test_basic(self): self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertFalse(sot.allow_list) + self.assertDictEqual( + { + 'limit': 'limit', + 'marker': 'marker', + 'tenant_id': 'tenant_id' + }, + sot._query_mapping._mapping, + ) def test_get(self): sess = mock.Mock(spec=adapter.Adapter) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index b8687690e..5e3f4bf2c 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -20,7 +20,6 @@ from openstack.compute.v2 import hypervisor from openstack.compute.v2 import image from openstack.compute.v2 import keypair -from openstack.compute.v2 import limits from openstack.compute.v2 import migration from openstack.compute.v2 import quota_set from openstack.compute.v2 import server @@ -557,7 +556,14 @@ def test_images_not_detailed(self): expected_kwargs={"query": 1}) def test_limits_get(self): - self.verify_get(self.proxy.get_limits, limits.Limits, method_args=[]) + self._verify( + "openstack.compute.v2.limits.Limits.fetch", + self.proxy.get_limits, + method_args=[], + method_kwargs={"a": "b"}, + expected_args=[self.proxy], + expected_kwargs={"a": "b"}, + ) def test_server_interface_create(self): self.verify_create(self.proxy.create_server_interface, From 4246811205bbe40523604567dc6b2f1c416e34be Mon Sep 17 00:00:00 2001 From: "Dr. Jens Harbott" Date: Wed, 20 Oct 2021 07:30:30 +0200 Subject: [PATCH 2951/3836] Keep creating keystone admin endpoint for heat Heat still depends on an keystone admin endpoint being present although it has no functional difference anymore from the other endpoints with the v3 API. Make devstack create one. Change-Id: Icd786e89aba5b593feee165226010b25f8627efd (cherry picked from commit 9ed215d4443c08167117144e0ee938b418bf56c2) --- .zuul.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.zuul.yaml b/.zuul.yaml index 8202de424..6a8c943cd 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -123,6 +123,8 @@ devstack_localrc: DISABLE_AMP_IMAGE_BUILD: true Q_SERVICE_PLUGIN_CLASSES: qos,trunk + # TODO(frickler): drop this once heat no longer needs it + KEYSTONE_ADMIN_ENDPOINT: true devstack_plugins: heat: https://opendev.org/openstack/heat tox_environment: From aa58c8843e21daa04ed70b01be7f59443a634c65 Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Fri, 5 Nov 2021 10:39:14 +0100 Subject: [PATCH 2952/3836] Orchestration- reindentation of the docstrings Change-Id: Ied7dd7f8d110c773327c01f8609e6285021945ca --- openstack/orchestration/v1/_proxy.py | 122 +++++++++++++-------------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index a39f2671a..676db93bb 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -101,10 +101,10 @@ def find_stack(self, name_or_id, :param name_or_id: The name or ID of a stack. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.orchestration.v1.stack.Stack` or None """ return self._find(_stack.Stack, name_or_id, @@ -115,7 +115,7 @@ def stacks(self, **query): """Return a generator of stacks :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of stack objects :rtype: :class:`~openstack.orchestration.v1.stack.Stack` @@ -126,12 +126,12 @@ def get_stack(self, stack, resolve_outputs=True): """Get a single stack :param stack: The value can be the ID of a stack or a - :class:`~openstack.orchestration.v1.stack.Stack` instance. + :class:`~openstack.orchestration.v1.stack.Stack` instance. :param resolve_outputs: Whether stack should contain outputs resolved. :returns: One :class:`~openstack.orchestration.v1.stack.Stack` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_stack.Stack, stack, resolve_outputs=resolve_outputs) @@ -139,14 +139,14 @@ def update_stack(self, stack, preview=False, **attrs): """Update a stack :param stack: The value can be the ID of a stack or a - :class:`~openstack.orchestration.v1.stack.Stack` instance. + :class:`~openstack.orchestration.v1.stack.Stack` instance. :param kwargs attrs: The attributes to update on the stack - represented by ``value``. + represented by ``value``. :returns: The updated stack :rtype: :class:`~openstack.orchestration.v1.stack.Stack` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ res = self._get_resource(_stack.Stack, stack, **attrs) return res.update(self, preview) @@ -155,13 +155,13 @@ def delete_stack(self, stack, ignore_missing=True): """Delete a stack :param stack: The value can be either the ID of a stack or a - :class:`~openstack.orchestration.v1.stack.Stack` - instance. + :class:`~openstack.orchestration.v1.stack.Stack` + instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the stack does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent stack. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the stack does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent stack. :returns: ``None`` """ @@ -174,7 +174,7 @@ def check_stack(self, stack): is to track the stack's status. :param stack: The value can be either the ID of a stack or an instance - of :class:`~openstack.orchestration.v1.stack.Stack`. + of :class:`~openstack.orchestration.v1.stack.Stack`. :returns: ``None`` """ if isinstance(stack, _stack.Stack): @@ -188,7 +188,7 @@ def abandon_stack(self, stack): """Abandon a stack's without deleting it's resources :param stack: The value can be either the ID of a stack or an instance - of :class:`~openstack.orchestration.v1.stack.Stack`. + of :class:`~openstack.orchestration.v1.stack.Stack`. :returns: ``None`` """ res = self._get_resource(_stack.Stack, stack) @@ -203,7 +203,7 @@ def get_stack_template(self, stack): :returns: One object of :class:`~openstack.orchestration.v1.stack_template.StackTemplate` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ if isinstance(stack, _stack.Stack): obj = stack @@ -220,10 +220,10 @@ def get_stack_environment(self, stack): :class:`~openstack.orchestration.v1.stack.Stack` :returns: One object of - :class:`~openstack.orchestration.v1.stack_environment.\ - StackEnvironment` + :class:`~openstack.orchestration.v1.stack_environment.\ + StackEnvironment` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no - resource can be found. + resource can be found. """ if isinstance(stack, _stack.Stack): obj = stack @@ -241,9 +241,9 @@ def get_stack_files(self, stack): :class:`~openstack.orchestration.v1.stack.Stack` :returns: A dictionary containing the names and contents of all files - used by the stack. + used by the stack. :raises: :class:`~openstack.exceptions.ResourceNotFound` - when the stack cannot be found. + when the stack cannot be found. """ if isinstance(stack, _stack.Stack): stk = stack @@ -257,17 +257,17 @@ def resources(self, stack, **query): """Return a generator of resources :param stack: This can be a stack object, or the name of a stack - for which the resources are to be listed. + for which the resources are to be listed. :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of resource objects if the stack exists and - there are resources in it. If the stack cannot be found, - an exception is thrown. + there are resources in it. If the stack cannot be found, + an exception is thrown. :rtype: A generator of :class:`~openstack.orchestration.v1.resource.Resource` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when the stack cannot be found. + when the stack cannot be found. """ # first try treat the value as a stack object or an ID if isinstance(stack, _stack.Stack): @@ -295,10 +295,10 @@ def software_configs(self, **query): """Returns a generator of software configs :param dict query: Optional query parameters to be sent to limit the - software configs returned. + software configs returned. :returns: A generator of software config objects. :rtype: :class:`~openstack.orchestration.v1.software_config.\ - SoftwareConfig` + SoftwareConfig` """ return self._list(_sc.SoftwareConfig, **query) @@ -321,10 +321,10 @@ def delete_software_config(self, software_config, ignore_missing=True): config or an instance of :class:`~openstack.orchestration.v1.software_config.SoftwareConfig` :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the software config does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent software config. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the software config does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent software config. :returns: ``None`` """ self._delete(_sc.SoftwareConfig, software_config, @@ -347,10 +347,10 @@ def software_deployments(self, **query): """Returns a generator of software deployments :param dict query: Optional query parameters to be sent to limit the - software deployments returned. + software deployments returned. :returns: A generator of software deployment objects. :rtype: :class:`~openstack.orchestration.v1.software_deployment.\ - SoftwareDeployment` + SoftwareDeployment` """ return self._list(_sd.SoftwareDeployment, **query) @@ -374,10 +374,10 @@ def delete_software_deployment(self, software_deployment, software deployment or an instance of :class:`~openstack.orchestration.v1.software_deployment.SoftwareDeployment` :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the software deployment does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent software deployment. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the software deployment does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent software deployment. :returns: ``None`` """ self._delete(_sd.SoftwareDeployment, software_deployment, @@ -389,11 +389,11 @@ def update_software_deployment(self, software_deployment, **attrs): :param server: Either the ID of a software deployment or an instance of :class:`~openstack.orchestration.v1.software_deployment.SoftwareDeployment` :param dict attrs: The attributes to update on the software deployment - represented by ``software_deployment``. + represented by ``software_deployment``. :returns: The updated software deployment :rtype: :class:`~openstack.orchestration.v1.software_deployment.\ - SoftwareDeployment` + SoftwareDeployment` """ return self._update(_sd.SoftwareDeployment, software_deployment, **attrs) @@ -403,21 +403,21 @@ def validate_template(self, template, environment=None, template_url=None, """Validates a template. :param template: The stack template on which the validation is - performed. + performed. :param environment: A JSON environment for the stack, if provided. :param template_url: A URI to the location containing the stack - template for validation. This parameter is only - required if the ``template`` parameter is None. - This parameter is ignored if ``template`` is - specified. + template for validation. This parameter is only + required if the ``template`` parameter is None. + This parameter is ignored if ``template`` is + specified. :param ignore_errors: A string containing comma separated error codes - to ignore. Currently the only valid error code - is '99001'. + to ignore. Currently the only valid error code + is '99001'. :returns: The result of template validation. :raises: :class:`~openstack.exceptions.InvalidRequest` if neither - `template` not `template_url` is provided. + `template` not `template_url` is provided. :raises: :class:`~openstack.exceptions.HttpException` if the template - fails the validation. + fails the validation. """ if template is None and template_url is None: raise exceptions.InvalidRequest( @@ -433,22 +433,22 @@ def wait_for_status(self, res, status='ACTIVE', failures=None, """Wait for a resource to be in a particular status. :param res: The resource to wait on to reach the specified status. - The resource must have a ``status`` attribute. + The resource must have a ``status`` attribute. :type resource: A :class:`~openstack.resource.Resource` object. :param status: Desired status. :param failures: Statuses that would be interpreted as failures. :type failures: :py:class:`list` :param interval: Number of seconds to wait before to consecutive - checks. Default to 2. + checks. Default to 2. :param wait: Maximum number of seconds to wait before the change. - Default to 120. + Default to 120. :returns: The resource is returned on success. :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition - to the desired status failed to occur in specified seconds. + to the desired status failed to occur in specified seconds. :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource - has transited to one of the failure statuses. + has transited to one of the failure statuses. :raises: :class:`~AttributeError` if the resource does not have a - ``status`` attribute. + ``status`` attribute. """ failures = [] if failures is None else failures return resource.wait_for_status( @@ -460,12 +460,12 @@ def wait_for_delete(self, res, interval=2, wait=120): :param res: The resource to wait on to be deleted. :type resource: A :class:`~openstack.resource.Resource` object. :param interval: Number of seconds to wait before to consecutive - checks. Default to 2. + checks. Default to 2. :param wait: Maximum number of seconds to wait before the change. - Default to 120. + Default to 120. :returns: The resource is returned on success. :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition - to delete failed to occur in the specified seconds. + to delete failed to occur in the specified seconds. """ return resource.wait_for_delete(self, res, interval, wait) From 15da4d22ca6a7f078cbbfd7a9a11d0c239a58912 Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Fri, 5 Nov 2021 10:44:56 +0100 Subject: [PATCH 2953/3836] Image service - reindentation of the docstrings Change-Id: Ifdd59965145d6787a3d512f540d07ce3c224e96d --- openstack/image/v1/_proxy.py | 60 ++++++++-------- openstack/image/v2/_proxy.py | 136 +++++++++++++++++------------------ 2 files changed, 98 insertions(+), 98 deletions(-) diff --git a/openstack/image/v1/_proxy.py b/openstack/image/v1/_proxy.py index 60a487e46..548067f7a 100644 --- a/openstack/image/v1/_proxy.py +++ b/openstack/image/v1/_proxy.py @@ -33,8 +33,8 @@ def upload_image(self, **attrs): `create_image`. :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.image.v1.image.Image`, - comprised of the properties on the Image class. + a :class:`~openstack.image.v1.image.Image`, + comprised of the properties on the Image class. :returns: The results of image creation :rtype: :class:`~openstack.image.v1.image.Image` @@ -119,12 +119,12 @@ def delete_image(self, image, ignore_missing=True): """Delete an image :param image: The value can be either the ID of an image or a - :class:`~openstack.image.v1.image.Image` instance. + :class:`~openstack.image.v1.image.Image` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the image does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent image. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the image does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent image. :returns: ``None`` """ @@ -135,10 +135,10 @@ def find_image(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a image. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.image.v1.image.Image` or None """ return self._find(_image.Image, name_or_id, @@ -148,11 +148,11 @@ def get_image(self, image): """Get a single image :param image: The value can be the ID of an image or a - :class:`~openstack.image.v1.image.Image` instance. + :class:`~openstack.image.v1.image.Image` instance. :returns: One :class:`~openstack.image.v1.image.Image` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_image.Image, image) @@ -160,7 +160,7 @@ def images(self, **query): """Return a generator of images :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of image objects :rtype: :class:`~openstack.image.v1.image.Image` @@ -171,9 +171,9 @@ def update_image(self, image, **attrs): """Update a image :param image: Either the ID of a image or a - :class:`~openstack.image.v1.image.Image` instance. + :class:`~openstack.image.v1.image.Image` instance. :attrs kwargs: The attributes to update on the image represented - by ``value``. + by ``value``. :returns: The updated image :rtype: :class:`~openstack.image.v1.image.Image` @@ -190,22 +190,22 @@ def download_image(self, image, stream=False, output=None, :ref:`download_image-stream-true`. :param image: The value can be either the ID of an image or a - :class:`~openstack.image.v2.image.Image` instance. + :class:`~openstack.image.v2.image.Image` instance. :param bool stream: When ``True``, return a :class:`requests.Response` - instance allowing you to iterate over the - response data stream instead of storing its entire - contents in memory. See - :meth:`requests.Response.iter_content` for more - details. *NOTE*: If you do not consume - the entirety of the response you must explicitly - call :meth:`requests.Response.close` or otherwise - risk inefficiencies with the ``requests`` - library's handling of connections. - - - When ``False``, return the entire - contents of the response. + instance allowing you to iterate over the + response data stream instead of storing its entire + contents in memory. See + :meth:`requests.Response.iter_content` for more + details. *NOTE*: If you do not consume + the entirety of the response you must explicitly + call :meth:`requests.Response.close` or otherwise + risk inefficiencies with the ``requests`` + library's handling of connections. + + + When ``False``, return the entire + contents of the response. :param output: Either a file object or a path to store data into. :param int chunk_size: size in bytes to read from the wire and buffer at one time. Defaults to 1024 diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 72da40b52..b4c726f24 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -419,22 +419,22 @@ def download_image(self, image, stream=False, output=None, :ref:`download_image-stream-true`. :param image: The value can be either the ID of an image or a - :class:`~openstack.image.v2.image.Image` instance. + :class:`~openstack.image.v2.image.Image` instance. :param bool stream: When ``True``, return a :class:`requests.Response` - instance allowing you to iterate over the - response data stream instead of storing its entire - contents in memory. See - :meth:`requests.Response.iter_content` for more - details. *NOTE*: If you do not consume - the entirety of the response you must explicitly - call :meth:`requests.Response.close` or otherwise - risk inefficiencies with the ``requests`` - library's handling of connections. - - - When ``False``, return the entire - contents of the response. + instance allowing you to iterate over the + response data stream instead of storing its entire + contents in memory. See + :meth:`requests.Response.iter_content` for more + details. *NOTE*: If you do not consume + the entirety of the response you must explicitly + call :meth:`requests.Response.close` or otherwise + risk inefficiencies with the ``requests`` + library's handling of connections. + + + When ``False``, return the entire + contents of the response. :param output: Either a file object or a path to store data into. :param int chunk_size: size in bytes to read from the wire and buffer at one time. Defaults to 1024 @@ -454,12 +454,12 @@ def delete_image(self, image, ignore_missing=True): """Delete an image :param image: The value can be either the ID of an image or a - :class:`~openstack.image.v2.image.Image` instance. + :class:`~openstack.image.v2.image.Image` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the image does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent image. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the image does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent image. :returns: ``None`` """ @@ -470,10 +470,10 @@ def find_image(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a image. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.image.v2.image.Image` or None """ return self._find(_image.Image, name_or_id, @@ -483,11 +483,11 @@ def get_image(self, image): """Get a single image :param image: The value can be the ID of a image or a - :class:`~openstack.image.v2.image.Image` instance. + :class:`~openstack.image.v2.image.Image` instance. :returns: One :class:`~openstack.image.v2.image.Image` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_image.Image, image) @@ -495,7 +495,7 @@ def images(self, **query): """Return a generator of images :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of image objects :rtype: :class:`~openstack.image.v2.image.Image` @@ -506,9 +506,9 @@ def update_image(self, image, **attrs): """Update a image :param image: Either the ID of a image or a - :class:`~openstack.image.v2.image.Image` instance. + :class:`~openstack.image.v2.image.Image` instance. :attrs kwargs: The attributes to update on the image represented - by ``value``. + by ``value``. :returns: The updated image :rtype: :class:`~openstack.image.v2.image.Image` @@ -519,7 +519,7 @@ def deactivate_image(self, image): """Deactivate an image :param image: Either the ID of a image or a - :class:`~openstack.image.v2.image.Image` instance. + :class:`~openstack.image.v2.image.Image` instance. :returns: None """ @@ -530,7 +530,7 @@ def reactivate_image(self, image): """Deactivate an image :param image: Either the ID of a image or a - :class:`~openstack.image.v2.image.Image` instance. + :class:`~openstack.image.v2.image.Image` instance. :returns: None """ @@ -541,8 +541,8 @@ def add_tag(self, image, tag): """Add a tag to an image :param image: The value can be the ID of a image or a - :class:`~openstack.image.v2.image.Image` instance - that the member will be created for. + :class:`~openstack.image.v2.image.Image` instance + that the member will be created for. :param str tag: The tag to be added :returns: None @@ -554,8 +554,8 @@ def remove_tag(self, image, tag): """Remove a tag to an image :param image: The value can be the ID of a image or a - :class:`~openstack.image.v2.image.Image` instance - that the member will be created for. + :class:`~openstack.image.v2.image.Image` instance + that the member will be created for. :param str tag: The tag to be removed :returns: None @@ -567,11 +567,11 @@ def add_member(self, image, **attrs): """Create a new member from attributes :param image: The value can be the ID of a image or a - :class:`~openstack.image.v2.image.Image` instance - that the member will be created for. + :class:`~openstack.image.v2.image.Image` instance + that the member will be created for. :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.image.v2.member.Member`, - comprised of the properties on the Member class. + a :class:`~openstack.image.v2.member.Member`, + comprised of the properties on the Member class. :returns: The results of member creation :rtype: :class:`~openstack.image.v2.member.Member` @@ -604,13 +604,13 @@ def find_member(self, name_or_id, image, ignore_missing=True): :param name_or_id: The name or ID of a member. :param image: This is the image that the member belongs to, - the value can be the ID of a image or a - :class:`~openstack.image.v2.image.Image` instance. + the value can be the ID of a image or a + :class:`~openstack.image.v2.image.Image` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.image.v2.member.Member` or None """ image_id = resource.Resource._get_id(image) @@ -621,13 +621,13 @@ def get_member(self, member, image): """Get a single member on an image :param member: The value can be the ID of a member or a - :class:`~openstack.image.v2.member.Member` instance. + :class:`~openstack.image.v2.member.Member` instance. :param image: This is the image that the member belongs to. - The value can be the ID of a image or a - :class:`~openstack.image.v2.image.Image` instance. + The value can be the ID of a image or a + :class:`~openstack.image.v2.image.Image` instance. :returns: One :class:`~openstack.image.v2.member.Member` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ member_id = resource.Resource._get_id(member) image_id = resource.Resource._get_id(image) @@ -653,12 +653,12 @@ def update_member(self, member, image, **attrs): """Update the member of an image :param member: Either the ID of a member or a - :class:`~openstack.image.v2.member.Member` instance. + :class:`~openstack.image.v2.member.Member` instance. :param image: This is the image that the member belongs to. - The value can be the ID of a image or a - :class:`~openstack.image.v2.image.Image` instance. + The value can be the ID of a image or a + :class:`~openstack.image.v2.image.Image` instance. :attrs kwargs: The attributes to update on the member represented - by ``value``. + by ``value``. :returns: The updated member :rtype: :class:`~openstack.image.v2.member.Member` @@ -673,7 +673,7 @@ def get_images_schema(self): :returns: One :class:`~openstack.image.v2.schema.Schema` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_schema.Schema, requires_id=False, base_path='/schemas/images') @@ -683,7 +683,7 @@ def get_image_schema(self): :returns: One :class:`~openstack.image.v2.schema.Schema` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_schema.Schema, requires_id=False, base_path='/schemas/image') @@ -693,7 +693,7 @@ def get_members_schema(self): :returns: One :class:`~openstack.image.v2.schema.Schema` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_schema.Schema, requires_id=False, base_path='/schemas/members') @@ -703,7 +703,7 @@ def get_member_schema(self): :returns: One :class:`~openstack.image.v2.schema.Schema` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_schema.Schema, requires_id=False, base_path='/schemas/member') @@ -712,7 +712,7 @@ def tasks(self, **query): """Return a generator of tasks :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of task objects :rtype: :class:`~openstack.image.v2.task.Task` @@ -723,11 +723,11 @@ def get_task(self, task): """Get task details :param task: The value can be the ID of a task or a - :class:`~openstack.image.v2.task.Task` instance. + :class:`~openstack.image.v2.task.Task` instance. :returns: One :class:`~openstack.image.v2.task.Task` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_task.Task, task) @@ -748,22 +748,22 @@ def wait_for_task(self, task, status='success', failures=None, """Wait for a task to be in a particular status. :param task: The resource to wait on to reach the specified status. - The resource must have a ``status`` attribute. + The resource must have a ``status`` attribute. :type resource: A :class:`~openstack.resource.Resource` object. :param status: Desired status. :param failures: Statuses that would be interpreted as failures. :type failures: :py:class:`list` :param interval: Number of seconds to wait before to consecutive - checks. Default to 2. + checks. Default to 2. :param wait: Maximum number of seconds to wait before the change. - Default to 120. + Default to 120. :returns: The resource is returned on success. :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition - to the desired status failed to occur in specified seconds. + to the desired status failed to occur in specified seconds. :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource - has transited to one of the failure statuses. + has transited to one of the failure statuses. :raises: :class:`~AttributeError` if the resource does not have a - ``status`` attribute. + ``status`` attribute. """ if failures is None: failures = ['failure'] @@ -810,7 +810,7 @@ def get_tasks_schema(self): :returns: One :class:`~openstack.image.v2.schema.Schema` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_schema.Schema, requires_id=False, base_path='/schemas/tasks') @@ -820,7 +820,7 @@ def get_task_schema(self): :returns: One :class:`~openstack.image.v2.schema.Schema` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_schema.Schema, requires_id=False, base_path='/schemas/task') @@ -838,6 +838,6 @@ def get_import_info(self): :returns: One :class:`~openstack.image.v2.service_info.Import` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_si.Import, require_id=False) From b48d0b8c9680c7ff4fc3ee9838eac91d3f823500 Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Fri, 5 Nov 2021 10:58:12 +0100 Subject: [PATCH 2954/3836] Database service - reindentation of the docstrings Change-Id: I1aa073f1a49db5d5a4dd0cff154cd30e2090d8f1 --- openstack/database/v1/_proxy.py | 146 ++++++++++++++++---------------- openstack/workflow/v2/_proxy.py | 40 ++++----- 2 files changed, 93 insertions(+), 93 deletions(-) diff --git a/openstack/database/v1/_proxy.py b/openstack/database/v1/_proxy.py index f5684506a..724b42b6e 100644 --- a/openstack/database/v1/_proxy.py +++ b/openstack/database/v1/_proxy.py @@ -23,10 +23,10 @@ def create_database(self, instance, **attrs): """Create a new database from attributes :param instance: This can be either the ID of an instance - or a :class:`~openstack.database.v1.instance.Instance` + or a :class:`~openstack.database.v1.instance.Instance` :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.database.v1.database.Database`, - comprised of the properties on the Database class. + a :class:`~openstack.database.v1.database.Database`, + comprised of the properties on the Database class. :returns: The results of server creation :rtype: :class:`~openstack.database.v1.database.Database` @@ -39,16 +39,16 @@ def delete_database(self, database, instance=None, ignore_missing=True): """Delete a database :param database: The value can be either the ID of a database or a - :class:`~openstack.database.v1.database.Database` instance. + :class:`~openstack.database.v1.database.Database` instance. :param instance: This parameter needs to be specified when - an ID is given as `database`. - It can be either the ID of an instance - or a :class:`~openstack.database.v1.instance.Instance` + an ID is given as `database`. + It can be either the ID of an instance + or a :class:`~openstack.database.v1.instance.Instance` :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the database does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent database. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the database does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent database. :returns: ``None`` """ @@ -62,12 +62,12 @@ def find_database(self, name_or_id, instance, ignore_missing=True): :param name_or_id: The name or ID of a database. :param instance: This can be either the ID of an instance - or a :class:`~openstack.database.v1.instance.Instance` + or a :class:`~openstack.database.v1.instance.Instance` :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.database.v1.database.Database` or None """ instance = self._get_resource(_instance.Instance, instance) @@ -79,10 +79,10 @@ def databases(self, instance, **query): """Return a generator of databases :param instance: This can be either the ID of an instance - or a :class:`~openstack.database.v1.instance.Instance` - instance that the interface belongs to. + or a :class:`~openstack.database.v1.instance.Instance` + instance that the interface belongs to. :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of database objects :rtype: :class:`~openstack.database.v1.database.Database` @@ -94,16 +94,16 @@ def get_database(self, database, instance=None): """Get a single database :param instance: This parameter needs to be specified when - an ID is given as `database`. - It can be either the ID of an instance - or a :class:`~openstack.database.v1.instance.Instance` + an ID is given as `database`. + It can be either the ID of an instance + or a :class:`~openstack.database.v1.instance.Instance` :param database: The value can be the ID of a database or a - :class:`~openstack.database.v1.database.Database` - instance. + :class:`~openstack.database.v1.database.Database` + instance. :returns: One :class:`~openstack.database.v1.database.Database` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_database.Database, database) @@ -112,10 +112,10 @@ def find_flavor(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a flavor. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.database.v1.flavor.Flavor` or None """ return self._find(_flavor.Flavor, name_or_id, @@ -125,11 +125,11 @@ def get_flavor(self, flavor): """Get a single flavor :param flavor: The value can be the ID of a flavor or a - :class:`~openstack.database.v1.flavor.Flavor` instance. + :class:`~openstack.database.v1.flavor.Flavor` instance. :returns: One :class:`~openstack.database.v1.flavor.Flavor` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_flavor.Flavor, flavor) @@ -137,7 +137,7 @@ def flavors(self, **query): """Return a generator of flavors :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of flavor objects :rtype: :class:`~openstack.database.v1.flavor.Flavor` @@ -148,8 +148,8 @@ def create_instance(self, **attrs): """Create a new instance from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.database.v1.instance.Instance`, - comprised of the properties on the Instance class. + a :class:`~openstack.database.v1.instance.Instance`, + comprised of the properties on the Instance class. :returns: The results of server creation :rtype: :class:`~openstack.database.v1.instance.Instance` @@ -160,12 +160,12 @@ def delete_instance(self, instance, ignore_missing=True): """Delete an instance :param instance: The value can be either the ID of an instance or a - :class:`~openstack.database.v1.instance.Instance` instance. + :class:`~openstack.database.v1.instance.Instance` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the instance does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent instance. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the instance does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent instance. :returns: ``None`` """ @@ -177,10 +177,10 @@ def find_instance(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.database.v1.instance.Instance` or None """ return self._find(_instance.Instance, name_or_id, @@ -190,12 +190,12 @@ def get_instance(self, instance): """Get a single instance :param instance: The value can be the ID of an instance or a - :class:`~openstack.database.v1.instance.Instance` - instance. + :class:`~openstack.database.v1.instance.Instance` + instance. :returns: One :class:`~openstack.database.v1.instance.Instance` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_instance.Instance, instance) @@ -203,7 +203,7 @@ def instances(self, **query): """Return a generator of instances :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of instance objects :rtype: :class:`~openstack.database.v1.instance.Instance` @@ -214,10 +214,10 @@ def update_instance(self, instance, **attrs): """Update a instance :param instance: Either the id of a instance or a - :class:`~openstack.database.v1.instance.Instance` - instance. + :class:`~openstack.database.v1.instance.Instance` + instance. :attrs kwargs: The attributes to update on the instance represented - by ``value``. + by ``value``. :returns: The updated instance :rtype: :class:`~openstack.database.v1.instance.Instance` @@ -228,10 +228,10 @@ def create_user(self, instance, **attrs): """Create a new user from attributes :param instance: This can be either the ID of an instance - or a :class:`~openstack.database.v1.instance.Instance` + or a :class:`~openstack.database.v1.instance.Instance` :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.database.v1.user.User`, - comprised of the properties on the User class. + a :class:`~openstack.database.v1.user.User`, + comprised of the properties on the User class. :returns: The results of server creation :rtype: :class:`~openstack.database.v1.user.User` @@ -243,16 +243,16 @@ def delete_user(self, user, instance=None, ignore_missing=True): """Delete a user :param user: The value can be either the ID of a user or a - :class:`~openstack.database.v1.user.User` instance. + :class:`~openstack.database.v1.user.User` instance. :param instance: This parameter needs to be specified when - an ID is given as `user`. - It can be either the ID of an instance - or a :class:`~openstack.database.v1.instance.Instance` + an ID is given as `user`. + It can be either the ID of an instance + or a :class:`~openstack.database.v1.instance.Instance` :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the user does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent user. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the user does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent user. :returns: ``None`` """ @@ -265,12 +265,12 @@ def find_user(self, name_or_id, instance, ignore_missing=True): :param name_or_id: The name or ID of a user. :param instance: This can be either the ID of an instance - or a :class:`~openstack.database.v1.instance.Instance` + or a :class:`~openstack.database.v1.instance.Instance` :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.database.v1.user.User` or None """ instance = self._get_resource(_instance.Instance, instance) @@ -281,9 +281,9 @@ def users(self, instance, **query): """Return a generator of users :param instance: This can be either the ID of an instance - or a :class:`~openstack.database.v1.instance.Instance` + or a :class:`~openstack.database.v1.instance.Instance` :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of user objects :rtype: :class:`~openstack.database.v1.user.User` @@ -295,15 +295,15 @@ def get_user(self, user, instance=None): """Get a single user :param user: The value can be the ID of a user or a - :class:`~openstack.database.v1.user.User` instance. + :class:`~openstack.database.v1.user.User` instance. :param instance: This parameter needs to be specified when - an ID is given as `database`. - It can be either the ID of an instance - or a :class:`~openstack.database.v1.instance.Instance` + an ID is given as `database`. + It can be either the ID of an instance + or a :class:`~openstack.database.v1.instance.Instance` :returns: One :class:`~openstack.database.v1.user.User` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ instance = self._get_resource(_instance.Instance, instance) return self._get(_user.User, user) diff --git a/openstack/workflow/v2/_proxy.py b/openstack/workflow/v2/_proxy.py index e0e5e5ced..784fd8644 100644 --- a/openstack/workflow/v2/_proxy.py +++ b/openstack/workflow/v2/_proxy.py @@ -21,8 +21,8 @@ def create_workflow(self, **attrs): """Create a new workflow from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.workflow.v2.workflow.Workflow`, - comprised of the properties on the Workflow class. + a :class:`~openstack.workflow.v2.workflow.Workflow`, + comprised of the properties on the Workflow class. :returns: The results of workflow creation :rtype: :class:`~openstack.workflow.v2.workflow.Workflow` @@ -33,11 +33,11 @@ def get_workflow(self, *attrs): """Get a workflow :param workflow: The value can be the name of a workflow or - :class:`~openstack.workflow.v2.workflow.Workflow` instance. + :class:`~openstack.workflow.v2.workflow.Workflow` instance. :returns: One :class:`~openstack.workflow.v2.workflow.Workflow` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no - workflow matching the name could be found. + workflow matching the name could be found. """ return self._get(_workflow.Workflow, *attrs) @@ -49,11 +49,11 @@ def workflows(self, **query): include: * limit: Requests at most the specified number of items be - returned from the query. + returned from the query. * marker: Specifies the ID of the last-seen workflow. Use the - limit parameter to make an initial limited request and use - the ID of the last-seen workflow from the response as the - marker parameter value in a subsequent limited request. + limit parameter to make an initial limited request and use + the ID of the last-seen workflow from the response as the + marker parameter value in a subsequent limited request. :returns: A generator of workflow instances. """ @@ -63,8 +63,8 @@ def delete_workflow(self, value, ignore_missing=True): """Delete a workflow :param value: The value can be either the name of a workflow or a - :class:`~openstack.workflow.v2.workflow.Workflow` - instance. + :class:`~openstack.workflow.v2.workflow.Workflow` + instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the workflow does not exist. @@ -125,11 +125,11 @@ def executions(self, **query): include: * limit: Requests at most the specified number of items be - returned from the query. + returned from the query. * marker: Specifies the ID of the last-seen execution. Use the - limit parameter to make an initial limited request and use - the ID of the last-seen execution from the response as the - marker parameter value in a subsequent limited request. + limit parameter to make an initial limited request and use + the ID of the last-seen execution from the response as the + marker parameter value in a subsequent limited request. :returns: A generator of execution instances. """ @@ -139,13 +139,13 @@ def delete_execution(self, value, ignore_missing=True): """Delete an execution :param value: The value can be either the name of a execution or a - :class:`~openstack.workflow.v2.execute.Execution` - instance. + :class:`~openstack.workflow.v2.execute.Execution` + instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the execution does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent execution. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the execution does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent execution. :returns: ``None`` """ From ee75531ed021f45a80f09384dd5500dd51d86219 Mon Sep 17 00:00:00 2001 From: Arkady Shtempler Date: Fri, 5 Nov 2021 15:47:48 +0200 Subject: [PATCH 2955/3836] Adds "test_update_zone" test case Test scenario: 1) Get setup's Zone TTL 2) Update Zone's TTL value (++1) 3) Validates that the updated TTL value is as expected Change-Id: I9f9fb292b0d463c1c49376c0ba290dcb7798cdf5 --- openstack/tests/functional/dns/v2/test_zone.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openstack/tests/functional/dns/v2/test_zone.py b/openstack/tests/functional/dns/v2/test_zone.py index e3491de2b..8acf77330 100644 --- a/openstack/tests/functional/dns/v2/test_zone.py +++ b/openstack/tests/functional/dns/v2/test_zone.py @@ -46,6 +46,16 @@ def test_list_zones(self): names = [f.name for f in self.conn.dns.zones()] self.assertIn(self.ZONE_NAME, names) + def test_update_zone(self): + current_ttl = self.conn.dns.get_zone(self.zone)['ttl'] + self.conn.dns.update_zone(self.zone, ttl=current_ttl + 1) + updated_zone_ttl = self.conn.dns.get_zone(self.zone)['ttl'] + self.assertEqual( + current_ttl + 1, + updated_zone_ttl, + 'Failed, updated TTL value is:{} instead of expected:{}'.format( + updated_zone_ttl, current_ttl + 1)) + def test_create_rs(self): zone = self.conn.dns.get_zone(self.zone) self.assertIsNotNone(self.conn.dns.create_recordset( From 41893385746e22be1917281fc468fb91aeb64e53 Mon Sep 17 00:00:00 2001 From: Megharth Date: Sun, 7 Nov 2021 16:55:09 -0500 Subject: [PATCH 2956/3836] Fix functional test for user message Change-Id: Iaa3115f10d6af80c97f955e25fd1c5e1486d5cf0 --- openstack/shared_file_system/v2/user_message.py | 4 ++-- .../functional/shared_file_system/test_user_message.py | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/openstack/shared_file_system/v2/user_message.py b/openstack/shared_file_system/v2/user_message.py index 262fc4d16..896e52dec 100644 --- a/openstack/shared_file_system/v2/user_message.py +++ b/openstack/shared_file_system/v2/user_message.py @@ -33,9 +33,9 @@ class UserMessage(resource.Resource): #: Properties #: The action ID of the user message - action_id = resource.Body("action_id") + action_id = resource.Body("action_id", type=str) #: Indicate when the user message was created - created_at = resource.Body("created_at") + created_at = resource.Body("created_at", type=str) #: The detail ID of the user message detail_id = resource.Body("detail_id", type=str) #: Indicate when the share message expires diff --git a/openstack/tests/functional/shared_file_system/test_user_message.py b/openstack/tests/functional/shared_file_system/test_user_message.py index a6b6920b6..7af845496 100644 --- a/openstack/tests/functional/shared_file_system/test_user_message.py +++ b/openstack/tests/functional/shared_file_system/test_user_message.py @@ -21,9 +21,12 @@ def test_user_messages(self): u_messages = self.user_cloud.shared_file_system.user_messages() # self.assertGreater(len(list(u_messages)), 0) for u_message in u_messages: - for attribute in ('id', 'created_at', 'name'): + for attribute in ( + 'id', 'created_at', 'action_id', 'detail_id', + 'expires_at', 'message_level', 'project_id', 'request_id', + 'resource_id', 'resource_type', 'user_message'): self.assertTrue(hasattr(u_message, attribute)) - self.assertIsInstance(getattr(u_message, attribute), 'str') + self.assertIsInstance(getattr(u_message, attribute), str) self.conn.shared_file_system.delete_user_message( u_message) From 3692438ffae91749ce84bb2cc46bb38b5d280bf3 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 2 Nov 2021 17:04:31 +0000 Subject: [PATCH 2957/3836] compute: Add support for microversion 2.89 The volume attachments API now returns the attachment ID and internal BDM ID in responses. Change-Id: Ie1482dcffc534893c5d922910afe1c9b40c54fbb Signed-off-by: Stephen Finucane --- openstack/compute/v2/volume_attachment.py | 17 ++++++++---- .../unit/compute/v2/test_volume_attachment.py | 27 ++++++++++++------- ...te-microversion-2-89-8c5187cc3bf6bd02.yaml | 7 +++++ 3 files changed, 37 insertions(+), 14 deletions(-) create mode 100644 releasenotes/notes/compute-microversion-2-89-8c5187cc3bf6bd02.yaml diff --git a/openstack/compute/v2/volume_attachment.py b/openstack/compute/v2/volume_attachment.py index 45fc45bc2..eee123d77 100644 --- a/openstack/compute/v2/volume_attachment.py +++ b/openstack/compute/v2/volume_attachment.py @@ -27,19 +27,26 @@ class VolumeAttachment(resource.Resource): _query_mapping = resource.QueryParameters("limit", "offset") + #: The ID for the server. + server_id = resource.URI('server_id') #: Name of the device such as, /dev/vdb. device = resource.Body('device') #: The ID of the attachment. id = resource.Body('id') - #: The ID for the server. - server_id = resource.URI('server_id') + # FIXME(stephenfin): This conflicts since there is a server ID in the URI + # *and* in the body. We need a field that handles both or we need to use + # different names. + # #: The UUID of the server + # server_id = resource.Body('server_uuid') #: The ID of the attached volume. volume_id = resource.Body('volumeId') - #: The ID of the attachment you want to delete or update. + #: The UUID of the associated volume attachment in Cinder. attachment_id = resource.Body('attachment_id', alternate_id=True) + #: The ID of the block device mapping record for the attachment + bdm_id = resource.Body('bdm_uuid') #: Virtual device tags for the attachment. tag = resource.Body('tag') #: Indicates whether to delete the volume when server is destroyed delete_on_termination = resource.Body('delete_on_termination') - # delete_on_termination introduced in 2.79 - _max_microversion = '2.79' + # attachment_id (in responses) and bdm_id introduced in 2.89 + _max_microversion = '2.89' diff --git a/openstack/tests/unit/compute/v2/test_volume_attachment.py b/openstack/tests/unit/compute/v2/test_volume_attachment.py index 03d031c1f..d623fc523 100644 --- a/openstack/tests/unit/compute/v2/test_volume_attachment.py +++ b/openstack/tests/unit/compute/v2/test_volume_attachment.py @@ -15,11 +15,13 @@ EXAMPLE = { - 'device': '1', - 'id': '2', - 'volume_id': '3', - 'tag': '4', - 'delete_on_termination': 'true', + 'attachment_id': '979ce4f8-033a-409d-85e6-6b5c0f6a6302', + 'delete_on_termination': False, + 'device': '/dev/sdc', + 'serverId': '7696780b-3f53-4688-ab25-019bfcbbd806', + 'tag': 'foo', + 'volumeId': 'a07f71dc-8151-4e7d-a0cc-cd24a3f11113', + 'bdm_uuid': 'c088db45-92b8-49e8-81e2-a1b77a144b3b', } @@ -43,9 +45,16 @@ def test_basic(self): def test_make_it(self): sot = volume_attachment.VolumeAttachment(**EXAMPLE) + self.assertEqual(EXAMPLE['attachment_id'], sot.attachment_id) + self.assertEqual(EXAMPLE['attachment_id'], sot.id) + self.assertEqual( + EXAMPLE['delete_on_termination'], sot.delete_on_termination, + ) self.assertEqual(EXAMPLE['device'], sot.device) - self.assertEqual(EXAMPLE['id'], sot.id) - self.assertEqual(EXAMPLE['volume_id'], sot.volume_id) + # FIXME(stephenfin): This conflicts since there is a server ID in the + # URI *and* in the body. We need a field that handles both or we need + # to use different names. + # self.assertEqual(EXAMPLE['serverId'], sot.server_id) self.assertEqual(EXAMPLE['tag'], sot.tag) - self.assertEqual(EXAMPLE['delete_on_termination'], - sot.delete_on_termination) + self.assertEqual(EXAMPLE['volumeId'], sot.volume_id) + self.assertEqual(EXAMPLE['bdm_uuid'], sot.bdm_id) diff --git a/releasenotes/notes/compute-microversion-2-89-8c5187cc3bf6bd02.yaml b/releasenotes/notes/compute-microversion-2-89-8c5187cc3bf6bd02.yaml new file mode 100644 index 000000000..e22a340dc --- /dev/null +++ b/releasenotes/notes/compute-microversion-2-89-8c5187cc3bf6bd02.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + The 2.89 API microversion is now supported for the compute service. This + adds additional fields to the ``os-volume_attachments`` API, represented + by the ``openstack.compute.v2.volume_attachment.VolumeAttachment`` + resource. From 353a0ffe53323e556dcb305839b7c4269dff4bf6 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 15 Nov 2021 18:17:57 +0000 Subject: [PATCH 2958/3836] compute: Server group rules are dicts, not lists of dicts We were using an incorrect type. Change-Id: I65b331583447c603d7d319e037607c2d7e94bf2e Signed-off-by: Stephen Finucane Story: 2009684 Task: 43983 --- openstack/compute/v2/server_group.py | 12 +++++++++--- openstack/tests/unit/compute/v2/test_server_group.py | 6 +++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/openstack/compute/v2/server_group.py b/openstack/compute/v2/server_group.py index fe463504e..f67e7c050 100644 --- a/openstack/compute/v2/server_group.py +++ b/openstack/compute/v2/server_group.py @@ -39,12 +39,18 @@ class ServerGroup(resource.Resource): policy = resource.Body('policy') #: The list of members in the server group member_ids = resource.Body('members') - #: The metadata associated with the server group + #: The metadata associated with the server group. This is always empty and + #: only used for preserving compatibility. metadata = resource.Body('metadata') #: The project ID who owns the server group. project_id = resource.Body('project_id') - #: The rules field, which is a dict, can be applied to the policy - rules = resource.Body('rules', type=list, list_type=dict) + #: The rules field, which is a dict, can be applied to the policy. + #: Currently, only the max_server_per_host rule is supported for the + #: anti-affinity policy. The max_server_per_host rule allows specifying how + #: many members of the anti-affinity group can reside on the same compute + #: host. If not specified, only one member from the same anti-affinity + #: group can reside on a given host. + rules = resource.Body('rules', type=dict) #: The user ID who owns the server group user_id = resource.Body('user_id') diff --git a/openstack/tests/unit/compute/v2/test_server_group.py b/openstack/tests/unit/compute/v2/test_server_group.py index 20c261f91..cec61ef2a 100644 --- a/openstack/tests/unit/compute/v2/test_server_group.py +++ b/openstack/tests/unit/compute/v2/test_server_group.py @@ -18,8 +18,11 @@ 'id': 'IDENTIFIER', 'name': 'test', 'members': ['server1', 'server2'], - 'metadata': {'k': 'v'}, + 'metadata': {}, 'policies': ['anti-affinity'], + 'rules': { + 'max_server_per_host': 5, + }, } @@ -47,3 +50,4 @@ def test_make_it(self): self.assertEqual(EXAMPLE['members'], sot.member_ids) self.assertEqual(EXAMPLE['metadata'], sot.metadata) self.assertEqual(EXAMPLE['policies'], sot.policies) + self.assertEqual(EXAMPLE['rules'], sot.rules) From 94c6131cd8b129e6deb06e731ba462a95ed299b7 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 23 Jun 2021 11:32:10 +0100 Subject: [PATCH 2959/3836] trivial: Correct docstrings for 'delete' proxy calls A number of proxy calls used inconsistent docstrings. Correct this. Change-Id: I2843cf0fe95c56edaa55c326540dd963383f76fc Signed-off-by: Stephen Finucane --- openstack/accelerator/v2/_proxy.py | 40 +++++++++++++++++++----------- openstack/cloud/_accelerator.py | 38 +++++++++++++++------------- 2 files changed, 46 insertions(+), 32 deletions(-) diff --git a/openstack/accelerator/v2/_proxy.py b/openstack/accelerator/v2/_proxy.py index dcb44cc84..c443e026e 100644 --- a/openstack/accelerator/v2/_proxy.py +++ b/openstack/accelerator/v2/_proxy.py @@ -98,11 +98,13 @@ def create_device_profile(self, **attrs): """ return self._create(_device_profile.DeviceProfile, **attrs) - def delete_device_profile(self, name_or_id, ignore_missing=True): + def delete_device_profile(self, device_profile, ignore_missing=True): """Delete a device profile - :param name_or_id: The value can be either the ID or name of - a device profile. + :param device_profile: The value can be either the ID of a device + profile or a + :class:`~openstack.accelerator.v2.device_profile.DeviceProfile` + instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the device profile does not exist. @@ -110,8 +112,10 @@ def delete_device_profile(self, name_or_id, ignore_missing=True): attempting to delete a nonexistent device profile. :returns: ``None`` """ - return self._delete(_device_profile.DeviceProfile, - name_or_id, ignore_missing=ignore_missing) + return self._delete( + _device_profile.DeviceProfile, + device_profile, + ignore_missing=ignore_missing) def get_device_profile(self, uuid, fields=None): """Get a single device profile. @@ -141,20 +145,26 @@ def create_accelerator_request(self, **attrs): """ return self._create(_arq.AcceleratorRequest, **attrs) - def delete_accelerator_request(self, name_or_id, ignore_missing=True): - """Delete a device profile. + def delete_accelerator_request( + self, accelerator_request, ignore_missing=True, + ): + """Delete a device profile - :param name_or_id: The value can be either the ID or name of - an accelerator request. + :param device_profile: The value can be either the ID of a device + profile or a + :class:`~openstack.accelerator.v2.device_profile.DeviceProfile` + instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the device profile does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent accelerator request. + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the device profile does not exist. + When set to ``True``, no exception will be set when attempting to + delete a nonexistent accelerator request. :returns: ``None`` """ - return self._delete(_arq.AcceleratorRequest, name_or_id, - ignore_missing=ignore_missing) + return self._delete( + _arq.AcceleratorRequest, + accelerator_request, + ignore_missing=ignore_missing) def get_accelerator_request(self, uuid, fields=None): """Get a single accelerator request. diff --git a/openstack/cloud/_accelerator.py b/openstack/cloud/_accelerator.py index 8c2c96c96..41cf4a15a 100644 --- a/openstack/cloud/_accelerator.py +++ b/openstack/cloud/_accelerator.py @@ -21,10 +21,10 @@ class AcceleratorCloudMixin(_normalize.Normalizer): def list_deployables(self, filters=None): """List all available deployables. + :param filters: (optional) dict of filter conditions to push down :returns: A list of deployable info. """ - # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} @@ -32,10 +32,10 @@ def list_deployables(self, filters=None): def list_devices(self, filters=None): """List all devices. + :param filters: (optional) dict of filter conditions to push down :returns: A list of device info. """ - # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} @@ -43,10 +43,10 @@ def list_devices(self, filters=None): def list_device_profiles(self, filters=None): """List all device_profiles. + :param filters: (optional) dict of filter conditions to push down :returns: A list of device profile info. """ - # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} @@ -54,19 +54,20 @@ def list_device_profiles(self, filters=None): def create_device_profile(self, attrs): """Create a device_profile. + :param attrs: The info of device_profile to be created. :returns: A ``munch.Munch`` of the created device_profile. """ - return self.accelerator.create_device_profile(**attrs) def delete_device_profile(self, name_or_id, filters): """Delete a device_profile. - :param name_or_id: The Name(or uuid) of device_profile to be deleted. + + :param name_or_id: The name or uuid of the device profile to be + deleted. :param filters: dict of filter conditions to push down :returns: True if delete succeeded, False otherwise. """ - device_profile = self.accelerator.get_device_profile( name_or_id, filters @@ -74,20 +75,20 @@ def delete_device_profile(self, name_or_id, filters): if device_profile is None: self.log.debug( "device_profile %s not found for deleting", - name_or_id + name_or_id, ) return False - self.accelerator.delete_device_profile(name_or_id=name_or_id) + self.accelerator.delete_device_profile(device_profile=device_profile) return True def list_accelerator_requests(self, filters=None): """List all accelerator_requests. + :param filters: (optional) dict of filter conditions to push down :returns: A list of accelerator request info. """ - # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} @@ -95,11 +96,12 @@ def list_accelerator_requests(self, filters=None): def delete_accelerator_request(self, name_or_id, filters): """Delete a accelerator_request. - :param name_or_id: The Name(or uuid) of accelerator_request. + + :param name_or_id: The name or UUID of the accelerator request to + be deleted. :param filters: dict of filter conditions to push down :returns: True if delete succeeded, False otherwise. """ - accelerator_request = self.accelerator.get_accelerator_request( name_or_id, filters @@ -107,29 +109,31 @@ def delete_accelerator_request(self, name_or_id, filters): if accelerator_request is None: self.log.debug( "accelerator_request %s not found for deleting", - name_or_id + name_or_id, ) return False - self.accelerator.delete_accelerator_request(name_or_id=name_or_id) + self.accelerator.delete_accelerator_request( + accelerator_request=accelerator_request, + ) return True def create_accelerator_request(self, attrs): """Create an accelerator_request. + :param attrs: The info of accelerator_request to be created. :returns: A ``munch.Munch`` of the created accelerator_request. """ - return self.accelerator.create_accelerator_request(**attrs) def bind_accelerator_request(self, uuid, properties): """Bind an accelerator to VM. + :param uuid: The uuid of the accelerator_request to be binded. :param properties: The info of VM that will bind the accelerator. :returns: True if bind succeeded, False otherwise. """ - accelerator_request = self.accelerator.get_accelerator_request(uuid) if accelerator_request is None: self.log.debug( @@ -141,11 +145,11 @@ def bind_accelerator_request(self, uuid, properties): def unbind_accelerator_request(self, uuid, properties): """Unbind an accelerator from VM. + :param uuid: The uuid of the accelerator_request to be unbinded. :param properties: The info of VM that will unbind the accelerator. - :returns:True if unbind succeeded, False otherwise. + :returns: True if unbind succeeded, False otherwise. """ - accelerator_request = self.accelerator.get_accelerator_request(uuid) if accelerator_request is None: self.log.debug( From f5b60d2b03e611854822f17bd5487d322ca0b71f Mon Sep 17 00:00:00 2001 From: Nurmatov Mamatisa Date: Wed, 18 Aug 2021 14:32:51 +0300 Subject: [PATCH 2960/3836] Add Neutron Local IP CRUD Add support for neutron local ip CRUD operations Change-Id: I3134b181992863280e1a2f7de00a5039d8ebcf4f Partial-Bug: #1930200 Depends-On: https://review.opendev.org/c/openstack/neutron/+/804523 --- doc/source/user/proxies/network.rst | 10 + doc/source/user/resources/network/index.rst | 2 + .../user/resources/network/v2/local_ip.rst | 12 ++ .../network/v2/local_ip_association.rst | 13 ++ openstack/network/v2/_proxy.py | 202 ++++++++++++++++++ openstack/network/v2/local_ip.py | 61 ++++++ openstack/network/v2/local_ip_association.py | 47 ++++ .../functional/network/v2/test_local_ip.py | 68 ++++++ .../network/v2/test_local_ip_association.py | 68 ++++++ .../tests/unit/network/v2/test_local_ip.py | 68 ++++++ .../network/v2/test_local_ip_association.py | 56 +++++ openstack/tests/unit/network/v2/test_proxy.py | 101 +++++++++ 12 files changed, 708 insertions(+) create mode 100644 doc/source/user/resources/network/v2/local_ip.rst create mode 100644 doc/source/user/resources/network/v2/local_ip_association.rst create mode 100644 openstack/network/v2/local_ip.py create mode 100644 openstack/network/v2/local_ip_association.py create mode 100644 openstack/tests/functional/network/v2/test_local_ip.py create mode 100644 openstack/tests/functional/network/v2/test_local_ip_association.py create mode 100644 openstack/tests/unit/network/v2/test_local_ip.py create mode 100644 openstack/tests/unit/network/v2/test_local_ip_association.py diff --git a/doc/source/user/proxies/network.rst b/doc/source/user/proxies/network.rst index 6688ee062..f4f3a42e2 100644 --- a/doc/source/user/proxies/network.rst +++ b/doc/source/user/proxies/network.rst @@ -265,3 +265,13 @@ Service Provider Operations .. autoclass:: openstack.network.v2._proxy.Proxy :noindex: :members: service_providers + +Local IP Operations +^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + :noindex: + :members: create_local_ip, delete_local_ip, find_local_ip, get_local_ip, + local_ips, update_local_ip, create_local_ip_association, + delete_local_ip_association, find_local_ip_association, + get_local_ip_association, local_ip_associations diff --git a/doc/source/user/resources/network/index.rst b/doc/source/user/resources/network/index.rst index 3e54e0ed1..c8e365823 100644 --- a/doc/source/user/resources/network/index.rst +++ b/doc/source/user/resources/network/index.rst @@ -17,6 +17,8 @@ Network Resources v2/ikepolicy v2/listener v2/load_balancer + v2/local_ip + v2/local_ip_association v2/metering_label v2/metering_label_rule v2/network diff --git a/doc/source/user/resources/network/v2/local_ip.rst b/doc/source/user/resources/network/v2/local_ip.rst new file mode 100644 index 000000000..30f846ad7 --- /dev/null +++ b/doc/source/user/resources/network/v2/local_ip.rst @@ -0,0 +1,12 @@ +openstack.network.v2.local_ip +============================= + +.. automodule:: openstack.network.v2.local_ip + +The LocalIP Class +----------------- + +The ``LocalIP`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.local_ip.LocalIP + :members: diff --git a/doc/source/user/resources/network/v2/local_ip_association.rst b/doc/source/user/resources/network/v2/local_ip_association.rst new file mode 100644 index 000000000..12b59a9f2 --- /dev/null +++ b/doc/source/user/resources/network/v2/local_ip_association.rst @@ -0,0 +1,13 @@ +openstack.network.v2.local_ip_association +========================================= + +.. automodule:: openstack.network.v2.local_ip_association + +The LocalIPAssociation Class +---------------------------- + +The ``LocalIPAssociation`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.local_ip_association.LocalIPAssociation + :members: diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 86da25270..7a11d1fe2 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -30,6 +30,8 @@ from openstack.network.v2 import l3_conntrack_helper as _l3_conntrack_helper from openstack.network.v2 import listener as _listener from openstack.network.v2 import load_balancer as _load_balancer +from openstack.network.v2 import local_ip as _local_ip +from openstack.network.v2 import local_ip_association as _local_ip_association from openstack.network.v2 import metering_label as _metering_label from openstack.network.v2 import metering_label_rule as _metering_label_rule from openstack.network.v2 import network as _network @@ -622,6 +624,206 @@ def disassociate_flavor_from_service_profile( return flavor.disassociate_flavor_from_service_profile( self, service_profile.id) + def create_local_ip(self, **attrs): + """Create a new local ip from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.network.v2.local_ip.LocalIP`, + comprised of the properties on the LocalIP class. + + :returns: The results of local ip creation + :rtype: :class:`~openstack.network.v2.local_ip.LocalIP` + """ + return self._create(_local_ip.LocalIP, **attrs) + + def delete_local_ip(self, local_ip, ignore_missing=True, if_revision=None): + """Delete a local ip + + :param local_ip: The value can be either the ID of a local ip or a + :class:`~openstack.network.v2.local_ip.LocalIP` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the local ip does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent ip. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. + + :returns: ``None`` + """ + self._delete(_local_ip.LocalIP, local_ip, + ignore_missing=ignore_missing, if_revision=if_revision) + + def find_local_ip(self, name_or_id, ignore_missing=True, **args): + """Find a local IP + + :param name_or_id: The name or ID of an local IP. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. + :returns: One :class:`~openstack.network.v2.local_ip.LocalIP` + or None + """ + return self._find(_local_ip.LocalIP, name_or_id, + ignore_missing=ignore_missing, **args) + + def get_local_ip(self, local_ip): + """Get a single local ip + + :param local_ip: The value can be the ID of a local ip or a + :class:`~openstack.network.v2.local_ip.LocalIP` + instance. + + :returns: One :class:`~openstack.network.v2.local_ip.LocalIP` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_local_ip.LocalIP, local_ip) + + def local_ips(self, **query): + """Return a generator of local ips + + :param dict query: Optional query parameters to be sent to limit + the resources being returned. + + * ``name``: Local IP name + * ``description``: Local IP description + * ``project_id``: Owner project ID + + :returns: A generator of local ip objects + :rtype: :class:`~openstack.network.v2.local_ip.LocalIP` + """ + return self._list(_local_ip.LocalIP, **query) + + def update_local_ip(self, local_ip, if_revision=None, **attrs): + """Update a local ip + + :param local_ip: Either the id of a local ip or a + :class:`~openstack.network.v2.local_ip.LocalIP` + instance. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. + :param dict attrs: The attributes to update on the ip represented + by ``value``. + + :returns: The updated ip + :rtype: :class:`~openstack.network.v2.local_ip.LocalIP` + """ + return self._update(_local_ip.LocalIP, local_ip, + if_revision=if_revision, **attrs) + + def create_local_ip_association(self, local_ip, **attrs): + """Create a new local ip association from attributes + + :param local_ip: The value can be the ID of a Local IP or a + :class:`~openstack.network.v2.local_ip.LocalIP` + instance. + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.network.v2. + local_ip_association.LocalIPAssociation`, + comprised of the properties on the LocalIP class. + + :returns: The results of local ip association creation + :rtype: :class:`~openstack.network.v2.local_ip_association. + LocalIPAssociation` + """ + local_ip = self._get_resource(_local_ip.LocalIP, local_ip) + return self._create(_local_ip_association.LocalIPAssociation, + local_ip_id=local_ip.id, **attrs) + + def delete_local_ip_association(self, local_ip, fixed_port_id, + ignore_missing=True, if_revision=None): + """Delete a local ip association + + :param local_ip: The value can be the ID of a Local IP or a + :class:`~openstack.network.v2.local_ip.LocalIP` + instance. + :param fixed_port_id: The value can be either the fixed port ID + or a :class: + `~openstack.network.v2.local_ip_association.LocalIPAssociation` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the local ip association does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent ip. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. + + :returns: ``None`` + """ + local_ip = self._get_resource(_local_ip.LocalIP, local_ip) + self._delete(_local_ip_association.LocalIPAssociation, fixed_port_id, + local_ip_id=local_ip.id, + ignore_missing=ignore_missing, if_revision=if_revision) + + def find_local_ip_association(self, name_or_id, local_ip, + ignore_missing=True, **args): + """Find a local ip association + + :param name_or_id: The name or ID of local ip association. + :param local_ip: The value can be the ID of a Local IP or a + :class:`~openstack.network.v2.local_ip.LocalIP` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. + :returns: One :class:`~openstack.network.v2. + local_ip_association.LocalIPAssociation` + or None + """ + local_ip = self._get_resource(_local_ip.LocalIP, local_ip) + return self._find(_local_ip_association.LocalIPAssociation, name_or_id, + local_ip_id=local_ip.id, + ignore_missing=ignore_missing, **args) + + def get_local_ip_association(self, local_ip_association, local_ip): + """Get a single local ip association + + :param local_ip: The value can be the ID of a Local IP or a + :class:`~openstack.network.v2.local_ip.LocalIP` + instance. + :param local_ip_association: The value can be the ID + of a local ip association or a + :class:`~openstack.network.v2. + local_ip_association.LocalIPAssociation` + instance. + + :returns: One :class:`~openstack.network.v2. + local_ip_association.LocalIPAssociation` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + local_ip = self._get_resource(_local_ip.LocalIP, local_ip) + return self._get(_local_ip_association.LocalIPAssociation, + local_ip_association, + local_ip_id=local_ip.id) + + def local_ip_associations(self, local_ip, **query): + """Return a generator of local ip associations + + :param local_ip: The value can be the ID of a Local IP or a + :class:`~openstack.network.v2.local_ip.LocalIP` instance. + :param dict query: Optional query parameters to be sent to limit + the resources being returned. + + :returns: A generator of local ip association objects + :rtype: :class:`~openstack.network.v2. + local_ip_association.LocalIPAssociation` + """ + local_ip = self._get_resource(_local_ip.LocalIP, local_ip) + return self._list(_local_ip_association.LocalIPAssociation, + local_ip_id=local_ip.id, **query) + def create_ip(self, **attrs): """Create a new floating ip from attributes diff --git a/openstack/network/v2/local_ip.py b/openstack/network/v2/local_ip.py new file mode 100644 index 000000000..e791cbca3 --- /dev/null +++ b/openstack/network/v2/local_ip.py @@ -0,0 +1,61 @@ +# Copyright 2021 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 openstack import resource + + +class LocalIP(resource.Resource): + """Local IP extension.""" + resource_name = "local ip" + resource_key = "local_ip" + resources_key = "local_ips" + base_path = "/local_ips" + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + _allow_unknown_attrs_in_body = True + + _query_mapping = resource.QueryParameters( + "sort_key", "sort_dir", + 'name', 'description',) + + # Properties + #: Timestamp at which the floating IP was created. + created_at = resource.Body('created_at') + #: The local ip description. + description = resource.Body('description') + #: The ID of the local ip. + id = resource.Body('id') + #: The local ip ip-mode. + ip_mode = resource.Body('ip_mode') + #: The Local IP address. + local_ip_address = resource.Body('local_ip_address') + #: The ID of the port that owns the local ip. + local_port_id = resource.Body('local_port_id') + #: The local ip name. + name = resource.Body('name') + #: The ID of the network that owns the local ip. + network_id = resource.Body('network_id') + #: The ID of the project that owns the local ip. + project_id = resource.Body('project_id') + #: The local ip revision number. + revision_number = resource.Body('revision_number') + #: Timestamp at which the floating IP was last updated. + updated_at = resource.Body('updated_at') diff --git a/openstack/network/v2/local_ip_association.py b/openstack/network/v2/local_ip_association.py new file mode 100644 index 000000000..d99ef3bed --- /dev/null +++ b/openstack/network/v2/local_ip_association.py @@ -0,0 +1,47 @@ +# Copyright 2021 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 openstack import resource + + +class LocalIPAssociation(resource.Resource): + """Local IP extension.""" + resource_key = "port_association" + resources_key = "port_associations" + base_path = "/local_ips/%(local_ip_id)s/port_associations" + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + _allow_unknown_attrs_in_body = True + + _query_mapping = resource.QueryParameters( + 'local_ip_address', 'fixed_port_id', 'fixed_ip' + ) + # Properties + #: The fixed port ID. + fixed_port_id = resource.Body('fixed_port_id') + #: The fixed IP. + fixed_ip = resource.Body('fixed_ip') + #: Host + host = resource.Body('host') + #: The local ip address + local_ip_address = resource.Body('local_ip_address') + #: The ID of Local IP address + local_ip_id = resource.URI('local_ip_id') diff --git a/openstack/tests/functional/network/v2/test_local_ip.py b/openstack/tests/functional/network/v2/test_local_ip.py new file mode 100644 index 000000000..01e343aeb --- /dev/null +++ b/openstack/tests/functional/network/v2/test_local_ip.py @@ -0,0 +1,68 @@ +# Copyright 2021 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 openstack.network.v2 import local_ip as _local_ip +from openstack.tests.functional import base + + +class TestLocalIP(base.BaseFunctionalTest): + + LOCAL_IP_ID = None + + def setUp(self): + super(TestLocalIP, self).setUp() + + if not self.conn.network.find_extension('local_ip'): + self.skipTest('Local IP extension disabled') + + self.LOCAL_IP_NAME = self.getUniqueString() + self.LOCAL_IP_DESCRIPTION = self.getUniqueString() + self.LOCAL_IP_NAME_UPDATED = self.getUniqueString() + self.LOCAL_IP_DESCRIPTION_UPDATED = self.getUniqueString() + local_ip = self.conn.network.create_local_ip( + name=self.LOCAL_IP_NAME, + description=self.LOCAL_IP_DESCRIPTION, + ) + assert isinstance(local_ip, _local_ip.LocalIP) + self.assertEqual(self.LOCAL_IP_NAME, local_ip.name) + self.assertEqual(self.LOCAL_IP_DESCRIPTION, + local_ip.description) + self.LOCAL_IP_ID = local_ip.id + + def tearDown(self): + sot = self.conn.network.delete_local_ip(self.LOCAL_IP_ID) + self.assertIsNone(sot) + super(TestLocalIP, self).tearDown() + + def test_find(self): + sot = self.conn.network.find_local_ip(self.LOCAL_IP_NAME) + self.assertEqual(self.LOCAL_IP_ID, sot.id) + + def test_get(self): + sot = self.conn.network.get_local_ip(self.LOCAL_IP_ID) + self.assertEqual(self.LOCAL_IP_NAME, sot.name) + + def test_list(self): + names = [local_ip.name for local_ip in self.conn.network.local_ips()] + self.assertIn(self.LOCAL_IP_NAME, names) + + def test_update(self): + sot = self.conn.network.update_local_ip( + self.LOCAL_IP_ID, + name=self.LOCAL_IP_NAME_UPDATED, + description=self.LOCAL_IP_DESCRIPTION_UPDATED) + self.assertEqual(self.LOCAL_IP_NAME_UPDATED, sot.name) + self.assertEqual(self.LOCAL_IP_DESCRIPTION_UPDATED, + sot.description) diff --git a/openstack/tests/functional/network/v2/test_local_ip_association.py b/openstack/tests/functional/network/v2/test_local_ip_association.py new file mode 100644 index 000000000..e4f1dbb48 --- /dev/null +++ b/openstack/tests/functional/network/v2/test_local_ip_association.py @@ -0,0 +1,68 @@ +# Copyright 2021 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 openstack.network.v2 import local_ip_association as _local_ip_association +from openstack.tests.functional import base + + +class TestLocalIPAssociation(base.BaseFunctionalTest): + LOCAL_IP_ID = None + FIXED_PORT_ID = None + FIXED_IP = None + + def setUp(self): + super(TestLocalIPAssociation, self).setUp() + + if not self.conn.network.find_extension('local_ip'): + self.skipTest('Local IP extension disabled') + + self.LOCAL_IP_ID = self.getUniqueString() + self.FIXED_PORT_ID = self.getUniqueString() + self.FIXED_IP = self.getUniqueString() + local_ip_association = self.conn.network.create_local_ip_association( + local_ip=self.LOCAL_IP_ID, + fixed_port_id=self.FIXED_PORT_ID, + fixed_ip=self.FIXED_IP + ) + assert isinstance(local_ip_association, + _local_ip_association.LocalIPAssociation) + self.assertEqual(self.LOCAL_IP_ID, local_ip_association.local_ip_id) + self.assertEqual(self.FIXED_PORT_ID, + local_ip_association.fixed_port_id) + self.assertEqual(self.FIXED_IP, + local_ip_association.fixed_ip) + + def tearDown(self): + sot = self.conn.network.delete_local_ip_association( + self.LOCAL_IP_ID, + self.FIXED_PORT_ID) + self.assertIsNone(sot) + super(TestLocalIPAssociation, self).tearDown() + + def test_find(self): + sot = self.conn.network.find_local_ip_association(self.FIXED_PORT_ID, + self.LOCAL_IP_ID) + self.assertEqual(self.FIXED_PORT_ID, sot.fixed_port_id) + + def test_get(self): + sot = self.conn.network.get_local_ip_association(self.FIXED_PORT_ID, + self.LOCAL_IP_ID) + self.assertEqual(self.FIXED_PORT_ID, sot.fixed_port_id) + + def test_list(self): + fixed_port_id = [obj.fixed_port_id for obj in + self.conn.network.local_ip_associations( + self.LOCAL_IP_ID)] + self.assertIn(self.FIXED_PORT_ID, fixed_port_id) diff --git a/openstack/tests/unit/network/v2/test_local_ip.py b/openstack/tests/unit/network/v2/test_local_ip.py new file mode 100644 index 000000000..2dce8576a --- /dev/null +++ b/openstack/tests/unit/network/v2/test_local_ip.py @@ -0,0 +1,68 @@ +# Copyright 2021 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 openstack.network.v2 import local_ip +from openstack.tests.unit import base + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'created_at': '0', + 'id': IDENTIFIER, + 'name': '1', + 'description': '2', + 'project_id': '3', + 'local_port_id': '4', + 'network_id': '5', + 'local_ip_address': '127.0.0.1', + 'ip_mode': 'translate', + 'revision_number': '6', + 'updated_at': '7', +} + + +class TestLocalIP(base.TestCase): + + def test_basic(self): + sot = local_ip.LocalIP() + self.assertEqual('local_ip', sot.resource_key) + self.assertEqual('local_ips', sot.resources_key) + self.assertEqual('/local_ips', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + self.assertDictEqual({"name": "name", + "description": "description", + "sort_key": "sort_key", + "sort_dir": "sort_dir", + "limit": "limit", + "marker": "marker"}, + sot._query_mapping._mapping) + + def test_make_it(self): + sot = local_ip.LocalIP(**EXAMPLE) + self.assertEqual(EXAMPLE['created_at'], sot.created_at) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + self.assertEqual(EXAMPLE['local_port_id'], sot.local_port_id) + self.assertEqual(EXAMPLE['network_id'], sot.network_id) + self.assertEqual(EXAMPLE['local_ip_address'], sot.local_ip_address) + self.assertEqual(EXAMPLE['ip_mode'], sot.ip_mode) + self.assertEqual(EXAMPLE['revision_number'], sot.revision_number) + self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) diff --git a/openstack/tests/unit/network/v2/test_local_ip_association.py b/openstack/tests/unit/network/v2/test_local_ip_association.py new file mode 100644 index 000000000..5e55f959e --- /dev/null +++ b/openstack/tests/unit/network/v2/test_local_ip_association.py @@ -0,0 +1,56 @@ +# Copyright 2021 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 openstack.network.v2 import local_ip_association +from openstack.tests.unit import base + +EXAMPLE = { + 'local_ip_id': '0', + 'local_ip_address': '127.0.0.1', + 'fixed_port_id': '1', + 'fixed_ip': '127.0.0.2', + 'host': '2', +} + + +class TestLocalIP(base.TestCase): + + def test_basic(self): + sot = local_ip_association.LocalIPAssociation() + self.assertEqual('port_association', sot.resource_key) + self.assertEqual('port_associations', sot.resources_key) + self.assertEqual('/local_ips/%(local_ip_id)s/port_associations', + sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + self.assertDictEqual( + {'local_ip_address': 'local_ip_address', + 'fixed_port_id': 'fixed_port_id', + 'fixed_ip': 'fixed_ip', + 'limit': 'limit', + 'marker': 'marker'}, + sot._query_mapping._mapping) + + def test_make_it(self): + sot = local_ip_association.LocalIPAssociation(**EXAMPLE) + self.assertEqual(EXAMPLE['local_ip_id'], sot.local_ip_id) + self.assertEqual(EXAMPLE['local_ip_address'], sot.local_ip_address) + self.assertEqual(EXAMPLE['fixed_port_id'], sot.fixed_port_id) + self.assertEqual(EXAMPLE['fixed_ip'], sot.fixed_ip) + self.assertEqual(EXAMPLE['host'], sot.host) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 6b6d2d98c..57ffe83fc 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -31,6 +31,8 @@ from openstack.network.v2 import l3_conntrack_helper from openstack.network.v2 import listener from openstack.network.v2 import load_balancer +from openstack.network.v2 import local_ip +from openstack.network.v2 import local_ip_association from openstack.network.v2 import metering_label from openstack.network.v2 import metering_label_rule from openstack.network.v2 import network @@ -67,6 +69,7 @@ ROUTER_ID = 'router-id-' + uuid.uuid4().hex FIP_ID = 'fip-id-' + uuid.uuid4().hex CT_HELPER_ID = 'ct-helper-id-' + uuid.uuid4().hex +LOCAL_IP_ID = 'lip-id-' + uuid.uuid4().hex class TestNetworkProxy(test_proxy_base.TestProxyBase): @@ -493,6 +496,104 @@ def test_flavors(self): self.verify_list(self.proxy.flavors, flavor.Flavor) +class TestNetworkLocalIp(TestNetworkProxy): + def test_local_ip_create_attrs(self): + self.verify_create(self.proxy.create_local_ip, local_ip.LocalIP) + + def test_local_ip_delete(self): + self.verify_delete(self.proxy.delete_local_ip, local_ip.LocalIP, + False, expected_kwargs={'if_revision': None}) + + def test_local_ip_delete_ignore(self): + self.verify_delete(self.proxy.delete_local_ip, local_ip.LocalIP, + True, expected_kwargs={'if_revision': None}) + + def test_local_ip_delete_if_revision(self): + self.verify_delete(self.proxy.delete_local_ip, local_ip.LocalIP, + True, method_kwargs={'if_revision': 42}, + expected_kwargs={'if_revision': 42}) + + def test_local_ip_find(self): + self.verify_find(self.proxy.find_local_ip, local_ip.LocalIP) + + def test_local_ip_get(self): + self.verify_get(self.proxy.get_local_ip, local_ip.LocalIP) + + def test_local_ips(self): + self.verify_list(self.proxy.local_ips, local_ip.LocalIP) + + def test_local_ip_update(self): + self.verify_update(self.proxy.update_local_ip, local_ip.LocalIP, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': None}) + + def test_local_ip_update_if_revision(self): + self.verify_update(self.proxy.update_local_ip, local_ip.LocalIP, + method_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': 42}, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': 42}) + + +class TestNetworkLocalIpAssociation(TestNetworkProxy): + def test_local_ip_association_create_attrs(self): + self.verify_create(self.proxy.create_local_ip_association, + local_ip_association.LocalIPAssociation, + method_kwargs={'local_ip': LOCAL_IP_ID}, + expected_kwargs={'local_ip_id': LOCAL_IP_ID}) + + def test_local_ip_association_delete(self): + self.verify_delete( + self.proxy.delete_local_ip_association, + local_ip_association.LocalIPAssociation, + ignore_missing=False, + method_args=[LOCAL_IP_ID, "resource_or_id"], + expected_args=["resource_or_id"], + expected_kwargs={'if_revision': None, + 'local_ip_id': LOCAL_IP_ID}) + + def test_local_ip_association_delete_ignore(self): + self.verify_delete( + self.proxy.delete_local_ip_association, + local_ip_association.LocalIPAssociation, + ignore_missing=True, + method_args=[LOCAL_IP_ID, "resource_or_id"], + expected_args=["resource_or_id"], + expected_kwargs={'if_revision': None, + 'local_ip_id': LOCAL_IP_ID}) + + def test_local_ip_association_find(self): + lip = local_ip.LocalIP.new(id=LOCAL_IP_ID) + + self._verify( + 'openstack.proxy.Proxy._find', + self.proxy.find_local_ip_association, + method_args=['local_ip_association_id', lip], + expected_args=[ + local_ip_association.LocalIPAssociation, + 'local_ip_association_id'], + expected_kwargs={ + 'ignore_missing': True, 'local_ip_id': LOCAL_IP_ID}) + + def test_local_ip_association_get(self): + lip = local_ip.LocalIP.new(id=LOCAL_IP_ID) + + self._verify( + 'openstack.proxy.Proxy._get', + self.proxy.get_local_ip_association, + method_args=['local_ip_association_id', lip], + expected_args=[ + local_ip_association.LocalIPAssociation, + 'local_ip_association_id'], + expected_kwargs={'local_ip_id': LOCAL_IP_ID}) + + def test_local_ip_associations(self): + self.verify_list(self.proxy.local_ip_associations, + local_ip_association.LocalIPAssociation, + method_kwargs={'local_ip': LOCAL_IP_ID}, + expected_kwargs={'local_ip_id': LOCAL_IP_ID}) + + class TestNetworkServiceProfile(TestNetworkProxy): def test_service_profile_create_attrs(self): self.verify_create(self.proxy.create_service_profile, From 6487cad2507c57d199a868a917b64b1eb3999e3f Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Thu, 26 Aug 2021 17:14:23 +0000 Subject: [PATCH 2961/3836] Add "check_limit" to network Quota class The parameter "check_limit" is a flag used in Neutron Quota engine to check, if enabled, the quota usage before setting the new limit. Depends-On: https://review.opendev.org/c/openstack/neutron/+/801470 Related-Bug: #1936408 Change-Id: Iaac97b2333571c0b11431c6f47ef771829ed1452 --- openstack/network/v2/quota.py | 2 ++ openstack/tests/unit/network/v2/test_quota.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/openstack/network/v2/quota.py b/openstack/network/v2/quota.py index ddbd36935..cffae0184 100644 --- a/openstack/network/v2/quota.py +++ b/openstack/network/v2/quota.py @@ -27,6 +27,8 @@ class Quota(resource.Resource): allow_list = True # Properties + #: Flag to check the quota usage before setting the new limit. *Type: bool* + check_limit = resource.Body('check_limit', type=bool) #: The maximum amount of floating IPs you can have. *Type: int* floating_ips = resource.Body('floatingip', type=int) #: The maximum amount of health monitors you can create. *Type: int* diff --git a/openstack/tests/unit/network/v2/test_quota.py b/openstack/tests/unit/network/v2/test_quota.py index 54d7a6949..97c0b073c 100644 --- a/openstack/tests/unit/network/v2/test_quota.py +++ b/openstack/tests/unit/network/v2/test_quota.py @@ -32,6 +32,7 @@ 'loadbalancer': 13, 'l7policy': 14, 'pool': 15, + 'check_limit': True, } @@ -66,6 +67,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['loadbalancer'], sot.load_balancers) self.assertEqual(EXAMPLE['l7policy'], sot.l7_policies) self.assertEqual(EXAMPLE['pool'], sot.pools) + self.assertEqual(EXAMPLE['check_limit'], sot.check_limit) def test_prepare_request(self): body = {'id': 'ABCDEFGH', 'network': '12345'} @@ -112,4 +114,5 @@ def test_make_it(self): self.assertEqual(EXAMPLE['loadbalancer'], sot.load_balancers) self.assertEqual(EXAMPLE['l7policy'], sot.l7_policies) self.assertEqual(EXAMPLE['pool'], sot.pools) + self.assertEqual(EXAMPLE['check_limit'], sot.check_limit) self.assertEqual('FAKE_PROJECT', sot.project) From 6fdc97b7875a1356602d7b6c9dd4315bc6bcbcb6 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 25 Nov 2021 11:04:45 +0000 Subject: [PATCH 2962/3836] compute: Add support for server lock reason Add support for compute API microversion 2.73, which allowed admins to specify a reason for a server being locked. Change-Id: I676c3b2dd6b4135fb1ab65cc99315f87c13ec3a8 Signed-off-by: Stephen Finucane --- openstack/compute/v2/_proxy.py | 8 +++++--- openstack/compute/v2/server.py | 8 ++++++-- openstack/tests/unit/compute/v2/test_proxy.py | 12 +++++++++++- openstack/tests/unit/compute/v2/test_server.py | 12 ++++++++++++ .../compute-microversion-2-73-abae1d0c3740f76e.yaml | 5 +++++ 5 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/compute-microversion-2-73-abae1d0c3740f76e.yaml diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index d520cdac1..9144c0db3 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -969,15 +969,17 @@ def resume_server(self, server): server = self._get_resource(_server.Server, server) server.resume(self) - def lock_server(self, server): + def lock_server(self, server, locked_reason=None): """Locks a server. :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. + :param locked_reason: The reason behind locking the server. Limited to + 255 characters in length. :returns: None """ server = self._get_resource(_server.Server, server) - server.lock(self) + server.lock(self, locked_reason=locked_reason) def unlock_server(self, server): """Unlocks a locked server. diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 7b78a6915..7b82d45d7 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -63,7 +63,7 @@ class Server(resource.Resource, metadata.MetadataMixin, resource.TagMixin): **resource.TagMixin._tag_query_parameters ) - _max_microversion = '2.72' + _max_microversion = '2.73' #: A list of dictionaries holding links relevant to this server. links = resource.Body('links') @@ -412,8 +412,12 @@ def resume(self, session): body = {"resume": None} self._action(session, body) - def lock(self, session): + def lock(self, session, locked_reason=None): body = {"lock": None} + if locked_reason is not None: + body["lock"] = { + "locked_reason": locked_reason, + } self._action(session, body) def unlock(self, session): diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index a0ed8da44..6bcc059ef 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -810,7 +810,17 @@ def test_server_lock(self): "openstack.compute.v2.server.Server.lock", self.proxy.lock_server, method_args=["value"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + expected_kwargs={"locked_reason": None}) + + def test_server_lock_with_options(self): + self._verify( + "openstack.compute.v2.server.Server.lock", + self.proxy.lock_server, + method_args=["value"], + method_kwargs={"locked_reason": "Because why not"}, + expected_args=[self.proxy], + expected_kwargs={"locked_reason": "Because why not"}) def test_server_unlock(self): self._verify( diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 0f84b0719..084d3118f 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -649,6 +649,18 @@ def test_lock(self): self.sess.post.assert_called_with( url, json=body, headers=headers, microversion=None) + def test_lock_with_options(self): + sot = server.Server(**EXAMPLE) + + res = sot.lock(self.sess, locked_reason='Because why not') + + self.assertIsNone(res) + url = 'servers/IDENTIFIER/action' + body = {'lock': {'locked_reason': 'Because why not'}} + headers = {'Accept': ''} + self.sess.post.assert_called_with( + url, json=body, headers=headers, microversion=None) + def test_unlock(self): sot = server.Server(**EXAMPLE) diff --git a/releasenotes/notes/compute-microversion-2-73-abae1d0c3740f76e.yaml b/releasenotes/notes/compute-microversion-2-73-abae1d0c3740f76e.yaml new file mode 100644 index 000000000..f3d89c2a6 --- /dev/null +++ b/releasenotes/notes/compute-microversion-2-73-abae1d0c3740f76e.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add support for Compute API microversion 2.73, which allows admins to + specify a reason when locking a server. From cc00aafda6abbbb4ff7731a9b5795e64e6dfe5f6 Mon Sep 17 00:00:00 2001 From: Polina Gubina Date: Thu, 8 Jul 2021 15:45:24 +0300 Subject: [PATCH 2963/3836] Vpn ike policy resource Change-Id: I8a69a1d32d4ba822e824864d9d22919a549c02d6 --- doc/source/user/proxies/network.rst | 9 ++ doc/source/user/resources/network/index.rst | 1 + .../user/resources/network/v2/ikepolicy.rst | 13 +++ openstack/network/v2/_proxy.py | 85 +++++++++++++++++++ openstack/network/v2/ikepolicy.py | 63 ++++++++++++++ .../tests/unit/network/v2/test_ikepolicy.py | 59 +++++++++++++ openstack/tests/unit/network/v2/test_proxy.py | 29 +++++++ 7 files changed, 259 insertions(+) create mode 100644 doc/source/user/resources/network/v2/ikepolicy.rst create mode 100644 openstack/network/v2/ikepolicy.py create mode 100644 openstack/tests/unit/network/v2/test_ikepolicy.py diff --git a/doc/source/user/proxies/network.rst b/doc/source/user/proxies/network.rst index 3c5f76a76..6688ee062 100644 --- a/doc/source/user/proxies/network.rst +++ b/doc/source/user/proxies/network.rst @@ -243,6 +243,15 @@ IPSecSiteConnection Operations delete_vpn_ipsec_site_connection, get_vpn_ipsec_site_connection, find_vpn_ipsec_site_connection, vpn_ipsec_site_connections +IkePolicy Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + :noindex: + :members: create_vpn_ikepolicy, update_vpn_ikepolicy, + delete_vpn_ikepolicy, get_vpn_ikepolicy, + find_vpn_ikepolicy, vpn_ikepolicies + Extension Operations ^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/network/index.rst b/doc/source/user/resources/network/index.rst index 75fb5d928..3e54e0ed1 100644 --- a/doc/source/user/resources/network/index.rst +++ b/doc/source/user/resources/network/index.rst @@ -14,6 +14,7 @@ Network Resources v2/floating_ip v2/health_monitor v2/ipsec_site_connection + v2/ikepolicy v2/listener v2/load_balancer v2/metering_label diff --git a/doc/source/user/resources/network/v2/ikepolicy.rst b/doc/source/user/resources/network/v2/ikepolicy.rst new file mode 100644 index 000000000..b6bc623ef --- /dev/null +++ b/doc/source/user/resources/network/v2/ikepolicy.rst @@ -0,0 +1,13 @@ +openstack.network.v2.ikepolicy +============================== + +.. automodule:: openstack.network.v2.ikepolicy + +The IkePolicy Class +------------------- + +The ``IkePolicy`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.ikepolicy.IkePolicy + :members: diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index ac9d735d4..86da25270 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -24,6 +24,7 @@ from openstack.network.v2 import flavor as _flavor from openstack.network.v2 import floating_ip as _floating_ip from openstack.network.v2 import health_monitor as _health_monitor +from openstack.network.v2 import ikepolicy as _ikepolicy from openstack.network.v2 import ipsec_site_connection as \ _ipsec_site_connection from openstack.network.v2 import l3_conntrack_helper as _l3_conntrack_helper @@ -1047,6 +1048,90 @@ def delete_vpn_ipsec_site_connection(self, ipsec_site_connection, self._delete(_ipsec_site_connection.IPSecSiteConnection, ipsec_site_connection, ignore_missing=ignore_missing) + def create_vpn_ikepolicy(self, **attrs): + """Create a new ike policy from attributes + + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.network.v2.ikepolicy.IkePolicy`, comprised of + the properties on the IkePolicy class. + + :returns: The results of ike policy creation :rtype: + :class:`~openstack.network.v2.ikepolicy.IkePolicy` + """ + return self._create(_ikepolicy.IkePolicy, + **attrs) + + def find_vpn_ikepolicy(self, name_or_id, + ignore_missing=True, **args): + """Find a single ike policy + + :param name_or_id: The name or ID of an ike policy. + :param bool ignore_missing: When set to ``False`` :class:`~openstack. + exceptions.ResourceNotFound` will be raised when the resource does + not exist. When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods such as query filters. + :returns: One :class:`~openstack.network.v2.ikepolicy.IkePolicy` or + None. + """ + return self._find(_ikepolicy.IkePolicy, + name_or_id, ignore_missing=ignore_missing, **args) + + def get_vpn_ikepolicy(self, ikepolicy): + """Get a single ike policy + + :param ikepolicy: The value can be the ID of an ikepolicy or a + :class:`~openstack.network.v2.ikepolicy.IkePolicy` instance. + + :returns: One :class:`~openstack.network.v2.ikepolicy.IkePolicy` + :rtype: :class:`~openstack.network.v2.ikepolicy.IkePolicy` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource can be found. + """ + return self._get(_ikepolicy.IkePolicy, ikepolicy) + + def vpn_ikepolicies(self, **query): + """Return a generator of ike policy + + :param dict query: Optional query parameters to be sent to limit the + resources being returned. + + :returns: A generator of ike policy objects + :rtype: :class:`~openstack.network.v2.ikepolicy.IkePolicy` + """ + return self._list(_ikepolicy.IkePolicy, **query) + + def update_vpn_ikepolicy(self, ikepolicy, **attrs): + """Update a ike policy + + :ikepolicy: Either the id of an ike policy or a + :class:`~openstack.network.v2.ikepolicy.IkePolicy` instance. + :param dict attrs: The attributes to update on the ike policy + represented by ``ikepolicy``. + + :returns: The updated ike policy + :rtype: :class:`~openstack.network.v2.ikepolicy.IkePolicy` + """ + return self._update(_ikepolicy.IkePolicy, ikepolicy, **attrs) + + def delete_vpn_ikepolicy(self, ikepolicy, ignore_missing=True): + """Delete a ikepolicy + + :param ikepolicy: The value can be either the ID of an ike policy, or + a :class:`~openstack.network.v2.ikepolicy.IkePolicy` instance. + :param bool ignore_missing: + When set to ``False`` :class:`~openstack.exceptions. + ResourceNotFound` will be raised when the ike policy + does not exist. + When set to ``True``, no exception will be set when attempting to + delete a nonexistent ike policy. + + :returns: ``None`` + """ + self._delete(_ikepolicy.IkePolicy, ikepolicy, + ignore_missing=ignore_missing) + def create_listener(self, **attrs): """Create a new listener from attributes diff --git a/openstack/network/v2/ikepolicy.py b/openstack/network/v2/ikepolicy.py new file mode 100644 index 000000000..b9ca2b529 --- /dev/null +++ b/openstack/network/v2/ikepolicy.py @@ -0,0 +1,63 @@ +# 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 resource + + +class IkePolicy(resource.Resource): + resource_key = 'ikepolicy' + resources_key = 'ikepolicies' + base_path = '/vpn/ikepolicies' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + # Properties + #: The authentication hash algorithm. Valid values are sha1, + # sha256, sha384, sha512. The default is sha1. + auth_algorithm = resource.Body('auth_algorithm') + #: A human-readable description for the resource. + # Default is an empty string. + description = resource.Body('description') + #: The encryption algorithm. A valid value is 3des, aes-128, + # aes-192, aes-256, and so on. Default is aes-128. + encryption_algorithm = resource.Body('encryption_algorithm') + #: The IKE version. A valid value is v1 or v2. Default is v1. + ike_version = resource.Body('ike_version') + #: The lifetime of the security association. The lifetime consists + # of a unit and integer value. You can omit either the unit or value + # portion of the lifetime. Default unit is seconds and + # default value is 3600. + lifetime = resource.Body('lifetime', type=dict) + #: Human-readable name of the resource. Default is an empty string. + name = resource.Body('name') + #: Perfect forward secrecy (PFS). A valid value is Group2, + # Group5, Group14, and so on. Default is Group5. + pfs = resource.Body('pfs') + #: The ID of the project. + project_id = resource.Body('project_id') + #: The IKE mode. A valid value is main, which is the default. + phase1_negotiation_mode = resource.Body('phase1_negotiation_mode') + #: The units for the lifetime of the security association. + # The lifetime consists of a unit and integer value. + # You can omit either the unit or value portion of the lifetime. + # Default unit is seconds and default value is 3600. + units = resource.Body('units') + #: The lifetime value, as a positive integer. The lifetime + # consists of a unit and integer value. + # You can omit either the unit or value portion of the lifetime. + # Default unit is seconds and default value is 3600. + value = resource.Body('value', type=int) diff --git a/openstack/tests/unit/network/v2/test_ikepolicy.py b/openstack/tests/unit/network/v2/test_ikepolicy.py new file mode 100644 index 000000000..f17cfd29a --- /dev/null +++ b/openstack/tests/unit/network/v2/test_ikepolicy.py @@ -0,0 +1,59 @@ +# 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.network.v2 import ikepolicy +from openstack.tests.unit import base + + +EXAMPLE = { + "auth_algorithm": "1", + "description": "2", + "encryption_algorithm": "3", + "ike_version": "4", + "lifetime": {'a': 5}, + "name": "5", + "pfs": "6", + "project_id": "7", + "phase1_negotiation_mode": "8", + "units": "9", + "value": 10 +} + + +class TestIkePolicy(base.TestCase): + + def test_basic(self): + sot = ikepolicy.IkePolicy() + self.assertEqual('ikepolicy', sot.resource_key) + self.assertEqual('ikepolicies', sot.resources_key) + self.assertEqual('/vpn/ikepolicies', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = ikepolicy.IkePolicy(**EXAMPLE) + self.assertEqual(EXAMPLE['auth_algorithm'], sot.auth_algorithm) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['encryption_algorithm'], + sot.encryption_algorithm) + self.assertEqual(EXAMPLE['ike_version'], sot.ike_version) + self.assertEqual(EXAMPLE['lifetime'], sot.lifetime) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['pfs'], sot.pfs) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + self.assertEqual(EXAMPLE['phase1_negotiation_mode'], + sot.phase1_negotiation_mode) + self.assertEqual(EXAMPLE['units'], sot.units) + self.assertEqual(EXAMPLE['value'], sot.value) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 3b040a618..67c866345 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -26,6 +26,7 @@ from openstack.network.v2 import flavor from openstack.network.v2 import floating_ip from openstack.network.v2 import health_monitor +from openstack.network.v2 import ikepolicy from openstack.network.v2 import ipsec_site_connection from openstack.network.v2 import l3_conntrack_helper from openstack.network.v2 import listener @@ -268,6 +269,34 @@ def test_ipsec_site_connection_update(self): self.verify_update(self.proxy.update_vpn_ipsec_site_connection, ipsec_site_connection.IPSecSiteConnection) + def test_ikepolicy_create_attrs(self): + self.verify_create(self.proxy.create_vpn_ikepolicy, + ikepolicy.IkePolicy) + + def test_ikepolicy_delete(self): + self.verify_delete(self.proxy.delete_vpn_ikepolicy, + ikepolicy.IkePolicy, False) + + def test_ikepolicy_delete_ignore(self): + self.verify_delete(self.proxy.delete_vpn_ikepolicy, + ikepolicy.IkePolicy, True) + + def test_ikepolicy_find(self): + self.verify_find(self.proxy.find_vpn_ikepolicy, + ikepolicy.IkePolicy) + + def test_ikepolicy_get(self): + self.verify_get(self.proxy.get_vpn_ikepolicy, + ikepolicy.IkePolicy) + + def test_ikepolicies(self): + self.verify_list(self.proxy.vpn_ikepolicies, + ikepolicy.IkePolicy) + + def test_ikepolicy_update(self): + self.verify_update(self.proxy.update_vpn_ikepolicy, + ikepolicy.IkePolicy) + def test_listener_create_attrs(self): self.verify_create(self.proxy.create_listener, listener.Listener) From 8b09b8f193e34c80de3fc1b7079a969fe97711be Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Mon, 20 Sep 2021 08:06:27 +0200 Subject: [PATCH 2964/3836] Splits class `TestNetworkProxy` 36 new classes are created: `TestNetworkTags`, `TestNetworkAddressScope`, `TestNetworkAgent`, `TestNetworkAvailability`, `TestNetworkExtension`, `TestNetworkHealthMonitor`, `TestNetworkSiteConnection`, `TestNetworkIkePolicy`, `TestNetworkListener`, `TestNetworkLoadBalancer`, `TestNetworkMeteringLabel`, `TestNetworkNetwork`, `TestNetworkFlavor`, `TestNetworkServiceProfile`, `TestNetworkIpAvailability`, `TestNetworkPoolMember`, `TestNetworkPool`, `TestNetworkQosBandwidth`, `TestNetworkQosDscpMarking`, `TestNetworkQosMinimumBandwidth`, `TestNetworkQosPolicy`, `TestNetworkQosRuleType`, `TestNetworkQuota`, `TestNetworkRbacPolicy`, `TestNetworkRouter`, `TestNetworkFirewallGroup`, `TestNetworkPolicy`, `TestNetworkRule`, `TestNetworkNetworkSegment`, `TestNetworkSecurityGroup`, `TestNetworkSegment`, `TestNetworkSubnet`, `TestNetworkVpnService`, `TestNetworkServiceProvider`, `TestNetworkAutoAllocatedTopology`, and `TestNetworkFloatingIp`. They contain all the tests previously embedded in `TestNetworkProxy`. In `TestNetworkProxy`, only the `setUp` method remains Change-Id: Ie53751eb5c4ba75d5189f7aa6fb19cd26974ad69 --- openstack/tests/unit/network/v2/test_proxy.py | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 67c866345..6b6d2d98c 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -107,6 +107,8 @@ def verify_delete( expected_kwargs=expected_kwargs, mock_method=mock_method) + +class TestNetworkAddressScope(TestNetworkProxy): def test_address_scope_create_attrs(self): self.verify_create(self.proxy.create_address_scope, address_scope.AddressScope) @@ -137,6 +139,8 @@ def test_address_scope_update(self): self.verify_update(self.proxy.update_address_scope, address_scope.AddressScope) + +class TestNetworkAgent(TestNetworkProxy): def test_agent_delete(self): self.verify_delete(self.proxy.delete_agent, agent.Agent, True) @@ -149,6 +153,8 @@ def test_agents(self): def test_agent_update(self): self.verify_update(self.proxy.update_agent, agent.Agent) + +class TestNetworkAvailability(TestNetworkProxy): def test_availability_zones(self): self.verify_list( self.proxy.availability_zones, @@ -170,6 +176,8 @@ def test_network_hosting_dhcp_agents(self): expected_kwargs={'network_id': NETWORK_ID} ) + +class TestNetworkExtension(TestNetworkProxy): def test_extension_find(self): self.verify_find(self.proxy.find_extension, extension.Extension) @@ -213,6 +221,8 @@ def test_floating_ip_update_if_revision(self): expected_kwargs={'x': 1, 'y': 2, 'z': 3, 'if_revision': 42}) + +class TestNetworkHealthMonitor(TestNetworkProxy): def test_health_monitor_create_attrs(self): self.verify_create(self.proxy.create_health_monitor, health_monitor.HealthMonitor) @@ -241,6 +251,8 @@ def test_health_monitor_update(self): self.verify_update(self.proxy.update_health_monitor, health_monitor.HealthMonitor) + +class TestNetworkSiteConnection(TestNetworkProxy): def test_ipsec_site_connection_create_attrs(self): self.verify_create(self.proxy.create_vpn_ipsec_site_connection, ipsec_site_connection.IPSecSiteConnection) @@ -269,6 +281,8 @@ def test_ipsec_site_connection_update(self): self.verify_update(self.proxy.update_vpn_ipsec_site_connection, ipsec_site_connection.IPSecSiteConnection) + +class TestNetworkIkePolicy(TestNetworkProxy): def test_ikepolicy_create_attrs(self): self.verify_create(self.proxy.create_vpn_ikepolicy, ikepolicy.IkePolicy) @@ -297,6 +311,8 @@ def test_ikepolicy_update(self): self.verify_update(self.proxy.update_vpn_ikepolicy, ikepolicy.IkePolicy) + +class TestNetworkListener(TestNetworkProxy): def test_listener_create_attrs(self): self.verify_create(self.proxy.create_listener, listener.Listener) @@ -320,6 +336,8 @@ def test_listeners(self): def test_listener_update(self): self.verify_update(self.proxy.update_listener, listener.Listener) + +class TestNetworkLoadBalancer(TestNetworkProxy): def test_load_balancer_create_attrs(self): self.verify_create(self.proxy.create_load_balancer, load_balancer.LoadBalancer) @@ -348,6 +366,8 @@ def test_load_balancer_update(self): self.verify_update(self.proxy.update_load_balancer, load_balancer.LoadBalancer) + +class TestNetworkMeteringLabel(TestNetworkProxy): def test_metering_label_create_attrs(self): self.verify_create(self.proxy.create_metering_label, metering_label.MeteringLabel) @@ -404,6 +424,8 @@ def test_metering_label_rule_update(self): self.verify_update(self.proxy.update_metering_label_rule, metering_label_rule.MeteringLabelRule) + +class TestNetworkNetwork(TestNetworkProxy): def test_network_create_attrs(self): self.verify_create(self.proxy.create_network, network.Network) @@ -450,6 +472,8 @@ def test_network_update_if_revision(self): expected_kwargs={'x': 1, 'y': 2, 'z': 3, 'if_revision': 42}) + +class TestNetworkFlavor(TestNetworkProxy): def test_flavor_create_attrs(self): self.verify_create(self.proxy.create_flavor, flavor.Flavor) @@ -468,6 +492,8 @@ def test_flavor_update(self): def test_flavors(self): self.verify_list(self.proxy.flavors, flavor.Flavor) + +class TestNetworkServiceProfile(TestNetworkProxy): def test_service_profile_create_attrs(self): self.verify_create(self.proxy.create_service_profile, service_profile.ServiceProfile) @@ -492,6 +518,8 @@ def test_service_profile_update(self): self.verify_update(self.proxy.update_service_profile, service_profile.ServiceProfile) + +class TestNetworkIpAvailability(TestNetworkProxy): def test_network_ip_availability_find(self): self.verify_find(self.proxy.find_network_ip_availability, network_ip_availability.NetworkIPAvailability) @@ -510,6 +538,8 @@ def test_pool_member_create_attrs(self): method_kwargs={"pool": "test_id"}, expected_kwargs={"pool_id": "test_id"}) + +class TestNetworkPoolMember(TestNetworkProxy): def test_pool_member_delete(self): self.verify_delete( self.proxy.delete_pool_member, @@ -557,6 +587,8 @@ def test_pool_member_update(self): expected_args=[pool_member.PoolMember, "MEMBER"], expected_kwargs={"pool_id": "POOL"}) + +class TestNetworkPool(TestNetworkProxy): def test_pool_create_attrs(self): self.verify_create(self.proxy.create_pool, pool.Pool) @@ -623,6 +655,8 @@ def test_ports_create(self, bc): bc.assert_called_once_with(port.Port, data) + +class TestNetworkQosBandwidth(TestNetworkProxy): def test_qos_bandwidth_limit_rule_create_attrs(self): self.verify_create( self.proxy.create_qos_bandwidth_limit_rule, @@ -686,6 +720,8 @@ def test_qos_bandwidth_limit_rule_update(self): 'rule_id'], expected_kwargs={'qos_policy_id': QOS_POLICY_ID, 'foo': 'bar'}) + +class TestNetworkQosDscpMarking(TestNetworkProxy): def test_qos_dscp_marking_rule_create_attrs(self): self.verify_create( self.proxy.create_qos_dscp_marking_rule, @@ -748,6 +784,8 @@ def test_qos_dscp_marking_rule_update(self): 'rule_id'], expected_kwargs={'qos_policy_id': QOS_POLICY_ID, 'foo': 'bar'}) + +class TestNetworkQosMinimumBandwidth(TestNetworkProxy): def test_qos_minimum_bandwidth_rule_create_attrs(self): self.verify_create( self.proxy.create_qos_minimum_bandwidth_rule, @@ -812,6 +850,8 @@ def test_qos_minimum_bandwidth_rule_update(self): expected_kwargs={ 'qos_policy_id': QOS_POLICY_ID, 'foo': 'bar'}) + +class TestNetworkQosPolicy(TestNetworkProxy): def test_qos_policy_create_attrs(self): self.verify_create(self.proxy.create_qos_policy, qos_policy.QoSPolicy) @@ -835,6 +875,8 @@ def test_qos_policies(self): def test_qos_policy_update(self): self.verify_update(self.proxy.update_qos_policy, qos_policy.QoSPolicy) + +class TestNetworkQosRuleType(TestNetworkProxy): def test_qos_rule_type_find(self): self.verify_find(self.proxy.find_qos_rule_type, qos_rule_type.QoSRuleType) @@ -846,6 +888,8 @@ def test_qos_rule_type_get(self): def test_qos_rule_types(self): self.verify_list(self.proxy.qos_rule_types, qos_rule_type.QoSRuleType) + +class TestNetworkQuota(TestNetworkProxy): def test_quota_delete(self): self.verify_delete(self.proxy.delete_quota, quota.Quota, False) @@ -888,6 +932,8 @@ def test_quotas(self): def test_quota_update(self): self.verify_update(self.proxy.update_quota, quota.Quota) + +class TestNetworkRbacPolicy(TestNetworkProxy): def test_rbac_policy_create_attrs(self): self.verify_create(self.proxy.create_rbac_policy, rbac_policy.RBACPolicy) @@ -913,6 +959,8 @@ def test_rbac_policy_update(self): self.verify_update(self.proxy.update_rbac_policy, rbac_policy.RBACPolicy) + +class TestNetworkRouter(TestNetworkProxy): def test_router_create_attrs(self): self.verify_create(self.proxy.create_router, router.Router) @@ -1092,6 +1140,8 @@ def test_agent_hosted_routers_list(self): expected_kwargs={'agent_id': AGENT_ID}, ) + +class TestNetworkFirewallGroup(TestNetworkProxy): def test_firewall_group_create_attrs(self): self.verify_create(self.proxy.create_firewall_group, firewall_group.FirewallGroup) @@ -1120,6 +1170,8 @@ def test_firewall_group_update(self): self.verify_update(self.proxy.update_firewall_group, firewall_group.FirewallGroup) + +class TestNetworkPolicy(TestNetworkProxy): def test_firewall_policy_create_attrs(self): self.verify_create(self.proxy.create_firewall_policy, firewall_policy.FirewallPolicy) @@ -1148,6 +1200,8 @@ def test_firewall_policy_update(self): self.verify_update(self.proxy.update_firewall_policy, firewall_policy.FirewallPolicy) + +class TestNetworkRule(TestNetworkProxy): def test_firewall_rule_create_attrs(self): self.verify_create(self.proxy.create_firewall_rule, firewall_rule.FirewallRule) @@ -1176,6 +1230,8 @@ def test_firewall_rule_update(self): self.verify_update(self.proxy.update_firewall_rule, firewall_rule.FirewallRule) + +class TestNetworkNetworkSegment(TestNetworkProxy): def test_network_segment_range_create_attrs(self): self.verify_create(self.proxy.create_network_segment_range, network_segment_range.NetworkSegmentRange) @@ -1204,6 +1260,8 @@ def test_network_segment_range_update(self): self.verify_update(self.proxy.update_network_segment_range, network_segment_range.NetworkSegmentRange) + +class TestNetworkSecurityGroup(TestNetworkProxy): def test_security_group_create_attrs(self): self.verify_create(self.proxy.create_security_group, security_group.SecurityGroup) @@ -1290,6 +1348,8 @@ def test_security_group_rules_create(self, bc): bc.assert_called_once_with(security_group_rule.SecurityGroupRule, data) + +class TestNetworkSegment(TestNetworkProxy): def test_segment_create_attrs(self): self.verify_create(self.proxy.create_segment, segment.Segment) @@ -1311,6 +1371,8 @@ def test_segments(self): def test_segment_update(self): self.verify_update(self.proxy.update_segment, segment.Segment) + +class TestNetworkSubnet(TestNetworkProxy): def test_subnet_create_attrs(self): self.verify_create(self.proxy.create_subnet, subnet.Subnet) @@ -1369,6 +1431,8 @@ def test_subnet_pool_update(self): self.verify_update(self.proxy.update_subnet_pool, subnet_pool.SubnetPool) + +class TestNetworkVpnService(TestNetworkProxy): def test_vpn_service_create_attrs(self): self.verify_create(self.proxy.create_vpn_service, vpn_service.VPNService) @@ -1395,10 +1459,14 @@ def test_vpn_service_update(self): self.verify_update(self.proxy.update_vpn_service, vpn_service.VPNService) + +class TestNetworkServiceProvider(TestNetworkProxy): def test_service_provider(self): self.verify_list(self.proxy.service_providers, service_provider.ServiceProvider) + +class TestNetworkAutoAllocatedTopology(TestNetworkProxy): def test_auto_allocated_topology_get(self): self.verify_get(self.proxy.get_auto_allocated_topology, auto_allocated_topology.AutoAllocatedTopology) @@ -1421,6 +1489,8 @@ def test_validate_topology(self): expected_kwargs={"project": mock.sentinel.project_id, "requires_id": False}) + +class TestNetworkTags(TestNetworkProxy): def test_set_tags(self): x_network = network.Network.new(id='NETWORK_ID') self._verify( @@ -1438,6 +1508,8 @@ def test_set_tags_resource_without_tag_suport(self, mock_set_tags): no_tag_resource, ['TAG1', 'TAG2']) self.assertEqual(0, mock_set_tags.call_count) + +class TestNetworkFloatingIp(TestNetworkProxy): def test_create_floating_ip_port_forwarding(self): self.verify_create(self.proxy.create_floating_ip_port_forwarding, port_forwarding.PortForwarding, From 4f8fae5c7f8f85af3ead70aeab9e3bbdb9886379 Mon Sep 17 00:00:00 2001 From: Nurmatov Mamatisa Date: Wed, 18 Aug 2021 14:32:51 +0300 Subject: [PATCH 2965/3836] Add Neutron Local IP CRUD Add support for neutron local ip CRUD operations Change-Id: I3134b181992863280e1a2f7de00a5039d8ebcf4f Partial-Bug: #1930200 Depends-On: https://review.opendev.org/c/openstack/neutron/+/804523 --- doc/source/user/proxies/network.rst | 10 + doc/source/user/resources/network/index.rst | 2 + .../user/resources/network/v2/local_ip.rst | 12 ++ .../network/v2/local_ip_association.rst | 13 ++ openstack/network/v2/_proxy.py | 202 ++++++++++++++++++ openstack/network/v2/local_ip.py | 61 ++++++ openstack/network/v2/local_ip_association.py | 47 ++++ .../functional/network/v2/test_local_ip.py | 68 ++++++ .../network/v2/test_local_ip_association.py | 68 ++++++ .../tests/unit/network/v2/test_local_ip.py | 68 ++++++ .../network/v2/test_local_ip_association.py | 56 +++++ openstack/tests/unit/network/v2/test_proxy.py | 101 +++++++++ 12 files changed, 708 insertions(+) create mode 100644 doc/source/user/resources/network/v2/local_ip.rst create mode 100644 doc/source/user/resources/network/v2/local_ip_association.rst create mode 100644 openstack/network/v2/local_ip.py create mode 100644 openstack/network/v2/local_ip_association.py create mode 100644 openstack/tests/functional/network/v2/test_local_ip.py create mode 100644 openstack/tests/functional/network/v2/test_local_ip_association.py create mode 100644 openstack/tests/unit/network/v2/test_local_ip.py create mode 100644 openstack/tests/unit/network/v2/test_local_ip_association.py diff --git a/doc/source/user/proxies/network.rst b/doc/source/user/proxies/network.rst index 6688ee062..f4f3a42e2 100644 --- a/doc/source/user/proxies/network.rst +++ b/doc/source/user/proxies/network.rst @@ -265,3 +265,13 @@ Service Provider Operations .. autoclass:: openstack.network.v2._proxy.Proxy :noindex: :members: service_providers + +Local IP Operations +^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + :noindex: + :members: create_local_ip, delete_local_ip, find_local_ip, get_local_ip, + local_ips, update_local_ip, create_local_ip_association, + delete_local_ip_association, find_local_ip_association, + get_local_ip_association, local_ip_associations diff --git a/doc/source/user/resources/network/index.rst b/doc/source/user/resources/network/index.rst index 3e54e0ed1..c8e365823 100644 --- a/doc/source/user/resources/network/index.rst +++ b/doc/source/user/resources/network/index.rst @@ -17,6 +17,8 @@ Network Resources v2/ikepolicy v2/listener v2/load_balancer + v2/local_ip + v2/local_ip_association v2/metering_label v2/metering_label_rule v2/network diff --git a/doc/source/user/resources/network/v2/local_ip.rst b/doc/source/user/resources/network/v2/local_ip.rst new file mode 100644 index 000000000..30f846ad7 --- /dev/null +++ b/doc/source/user/resources/network/v2/local_ip.rst @@ -0,0 +1,12 @@ +openstack.network.v2.local_ip +============================= + +.. automodule:: openstack.network.v2.local_ip + +The LocalIP Class +----------------- + +The ``LocalIP`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.local_ip.LocalIP + :members: diff --git a/doc/source/user/resources/network/v2/local_ip_association.rst b/doc/source/user/resources/network/v2/local_ip_association.rst new file mode 100644 index 000000000..12b59a9f2 --- /dev/null +++ b/doc/source/user/resources/network/v2/local_ip_association.rst @@ -0,0 +1,13 @@ +openstack.network.v2.local_ip_association +========================================= + +.. automodule:: openstack.network.v2.local_ip_association + +The LocalIPAssociation Class +---------------------------- + +The ``LocalIPAssociation`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.local_ip_association.LocalIPAssociation + :members: diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 86da25270..7a11d1fe2 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -30,6 +30,8 @@ from openstack.network.v2 import l3_conntrack_helper as _l3_conntrack_helper from openstack.network.v2 import listener as _listener from openstack.network.v2 import load_balancer as _load_balancer +from openstack.network.v2 import local_ip as _local_ip +from openstack.network.v2 import local_ip_association as _local_ip_association from openstack.network.v2 import metering_label as _metering_label from openstack.network.v2 import metering_label_rule as _metering_label_rule from openstack.network.v2 import network as _network @@ -622,6 +624,206 @@ def disassociate_flavor_from_service_profile( return flavor.disassociate_flavor_from_service_profile( self, service_profile.id) + def create_local_ip(self, **attrs): + """Create a new local ip from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.network.v2.local_ip.LocalIP`, + comprised of the properties on the LocalIP class. + + :returns: The results of local ip creation + :rtype: :class:`~openstack.network.v2.local_ip.LocalIP` + """ + return self._create(_local_ip.LocalIP, **attrs) + + def delete_local_ip(self, local_ip, ignore_missing=True, if_revision=None): + """Delete a local ip + + :param local_ip: The value can be either the ID of a local ip or a + :class:`~openstack.network.v2.local_ip.LocalIP` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the local ip does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent ip. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. + + :returns: ``None`` + """ + self._delete(_local_ip.LocalIP, local_ip, + ignore_missing=ignore_missing, if_revision=if_revision) + + def find_local_ip(self, name_or_id, ignore_missing=True, **args): + """Find a local IP + + :param name_or_id: The name or ID of an local IP. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. + :returns: One :class:`~openstack.network.v2.local_ip.LocalIP` + or None + """ + return self._find(_local_ip.LocalIP, name_or_id, + ignore_missing=ignore_missing, **args) + + def get_local_ip(self, local_ip): + """Get a single local ip + + :param local_ip: The value can be the ID of a local ip or a + :class:`~openstack.network.v2.local_ip.LocalIP` + instance. + + :returns: One :class:`~openstack.network.v2.local_ip.LocalIP` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_local_ip.LocalIP, local_ip) + + def local_ips(self, **query): + """Return a generator of local ips + + :param dict query: Optional query parameters to be sent to limit + the resources being returned. + + * ``name``: Local IP name + * ``description``: Local IP description + * ``project_id``: Owner project ID + + :returns: A generator of local ip objects + :rtype: :class:`~openstack.network.v2.local_ip.LocalIP` + """ + return self._list(_local_ip.LocalIP, **query) + + def update_local_ip(self, local_ip, if_revision=None, **attrs): + """Update a local ip + + :param local_ip: Either the id of a local ip or a + :class:`~openstack.network.v2.local_ip.LocalIP` + instance. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. + :param dict attrs: The attributes to update on the ip represented + by ``value``. + + :returns: The updated ip + :rtype: :class:`~openstack.network.v2.local_ip.LocalIP` + """ + return self._update(_local_ip.LocalIP, local_ip, + if_revision=if_revision, **attrs) + + def create_local_ip_association(self, local_ip, **attrs): + """Create a new local ip association from attributes + + :param local_ip: The value can be the ID of a Local IP or a + :class:`~openstack.network.v2.local_ip.LocalIP` + instance. + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.network.v2. + local_ip_association.LocalIPAssociation`, + comprised of the properties on the LocalIP class. + + :returns: The results of local ip association creation + :rtype: :class:`~openstack.network.v2.local_ip_association. + LocalIPAssociation` + """ + local_ip = self._get_resource(_local_ip.LocalIP, local_ip) + return self._create(_local_ip_association.LocalIPAssociation, + local_ip_id=local_ip.id, **attrs) + + def delete_local_ip_association(self, local_ip, fixed_port_id, + ignore_missing=True, if_revision=None): + """Delete a local ip association + + :param local_ip: The value can be the ID of a Local IP or a + :class:`~openstack.network.v2.local_ip.LocalIP` + instance. + :param fixed_port_id: The value can be either the fixed port ID + or a :class: + `~openstack.network.v2.local_ip_association.LocalIPAssociation` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the local ip association does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent ip. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. + + :returns: ``None`` + """ + local_ip = self._get_resource(_local_ip.LocalIP, local_ip) + self._delete(_local_ip_association.LocalIPAssociation, fixed_port_id, + local_ip_id=local_ip.id, + ignore_missing=ignore_missing, if_revision=if_revision) + + def find_local_ip_association(self, name_or_id, local_ip, + ignore_missing=True, **args): + """Find a local ip association + + :param name_or_id: The name or ID of local ip association. + :param local_ip: The value can be the ID of a Local IP or a + :class:`~openstack.network.v2.local_ip.LocalIP` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. + :returns: One :class:`~openstack.network.v2. + local_ip_association.LocalIPAssociation` + or None + """ + local_ip = self._get_resource(_local_ip.LocalIP, local_ip) + return self._find(_local_ip_association.LocalIPAssociation, name_or_id, + local_ip_id=local_ip.id, + ignore_missing=ignore_missing, **args) + + def get_local_ip_association(self, local_ip_association, local_ip): + """Get a single local ip association + + :param local_ip: The value can be the ID of a Local IP or a + :class:`~openstack.network.v2.local_ip.LocalIP` + instance. + :param local_ip_association: The value can be the ID + of a local ip association or a + :class:`~openstack.network.v2. + local_ip_association.LocalIPAssociation` + instance. + + :returns: One :class:`~openstack.network.v2. + local_ip_association.LocalIPAssociation` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + local_ip = self._get_resource(_local_ip.LocalIP, local_ip) + return self._get(_local_ip_association.LocalIPAssociation, + local_ip_association, + local_ip_id=local_ip.id) + + def local_ip_associations(self, local_ip, **query): + """Return a generator of local ip associations + + :param local_ip: The value can be the ID of a Local IP or a + :class:`~openstack.network.v2.local_ip.LocalIP` instance. + :param dict query: Optional query parameters to be sent to limit + the resources being returned. + + :returns: A generator of local ip association objects + :rtype: :class:`~openstack.network.v2. + local_ip_association.LocalIPAssociation` + """ + local_ip = self._get_resource(_local_ip.LocalIP, local_ip) + return self._list(_local_ip_association.LocalIPAssociation, + local_ip_id=local_ip.id, **query) + def create_ip(self, **attrs): """Create a new floating ip from attributes diff --git a/openstack/network/v2/local_ip.py b/openstack/network/v2/local_ip.py new file mode 100644 index 000000000..e791cbca3 --- /dev/null +++ b/openstack/network/v2/local_ip.py @@ -0,0 +1,61 @@ +# Copyright 2021 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 openstack import resource + + +class LocalIP(resource.Resource): + """Local IP extension.""" + resource_name = "local ip" + resource_key = "local_ip" + resources_key = "local_ips" + base_path = "/local_ips" + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + _allow_unknown_attrs_in_body = True + + _query_mapping = resource.QueryParameters( + "sort_key", "sort_dir", + 'name', 'description',) + + # Properties + #: Timestamp at which the floating IP was created. + created_at = resource.Body('created_at') + #: The local ip description. + description = resource.Body('description') + #: The ID of the local ip. + id = resource.Body('id') + #: The local ip ip-mode. + ip_mode = resource.Body('ip_mode') + #: The Local IP address. + local_ip_address = resource.Body('local_ip_address') + #: The ID of the port that owns the local ip. + local_port_id = resource.Body('local_port_id') + #: The local ip name. + name = resource.Body('name') + #: The ID of the network that owns the local ip. + network_id = resource.Body('network_id') + #: The ID of the project that owns the local ip. + project_id = resource.Body('project_id') + #: The local ip revision number. + revision_number = resource.Body('revision_number') + #: Timestamp at which the floating IP was last updated. + updated_at = resource.Body('updated_at') diff --git a/openstack/network/v2/local_ip_association.py b/openstack/network/v2/local_ip_association.py new file mode 100644 index 000000000..d99ef3bed --- /dev/null +++ b/openstack/network/v2/local_ip_association.py @@ -0,0 +1,47 @@ +# Copyright 2021 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 openstack import resource + + +class LocalIPAssociation(resource.Resource): + """Local IP extension.""" + resource_key = "port_association" + resources_key = "port_associations" + base_path = "/local_ips/%(local_ip_id)s/port_associations" + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + _allow_unknown_attrs_in_body = True + + _query_mapping = resource.QueryParameters( + 'local_ip_address', 'fixed_port_id', 'fixed_ip' + ) + # Properties + #: The fixed port ID. + fixed_port_id = resource.Body('fixed_port_id') + #: The fixed IP. + fixed_ip = resource.Body('fixed_ip') + #: Host + host = resource.Body('host') + #: The local ip address + local_ip_address = resource.Body('local_ip_address') + #: The ID of Local IP address + local_ip_id = resource.URI('local_ip_id') diff --git a/openstack/tests/functional/network/v2/test_local_ip.py b/openstack/tests/functional/network/v2/test_local_ip.py new file mode 100644 index 000000000..01e343aeb --- /dev/null +++ b/openstack/tests/functional/network/v2/test_local_ip.py @@ -0,0 +1,68 @@ +# Copyright 2021 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 openstack.network.v2 import local_ip as _local_ip +from openstack.tests.functional import base + + +class TestLocalIP(base.BaseFunctionalTest): + + LOCAL_IP_ID = None + + def setUp(self): + super(TestLocalIP, self).setUp() + + if not self.conn.network.find_extension('local_ip'): + self.skipTest('Local IP extension disabled') + + self.LOCAL_IP_NAME = self.getUniqueString() + self.LOCAL_IP_DESCRIPTION = self.getUniqueString() + self.LOCAL_IP_NAME_UPDATED = self.getUniqueString() + self.LOCAL_IP_DESCRIPTION_UPDATED = self.getUniqueString() + local_ip = self.conn.network.create_local_ip( + name=self.LOCAL_IP_NAME, + description=self.LOCAL_IP_DESCRIPTION, + ) + assert isinstance(local_ip, _local_ip.LocalIP) + self.assertEqual(self.LOCAL_IP_NAME, local_ip.name) + self.assertEqual(self.LOCAL_IP_DESCRIPTION, + local_ip.description) + self.LOCAL_IP_ID = local_ip.id + + def tearDown(self): + sot = self.conn.network.delete_local_ip(self.LOCAL_IP_ID) + self.assertIsNone(sot) + super(TestLocalIP, self).tearDown() + + def test_find(self): + sot = self.conn.network.find_local_ip(self.LOCAL_IP_NAME) + self.assertEqual(self.LOCAL_IP_ID, sot.id) + + def test_get(self): + sot = self.conn.network.get_local_ip(self.LOCAL_IP_ID) + self.assertEqual(self.LOCAL_IP_NAME, sot.name) + + def test_list(self): + names = [local_ip.name for local_ip in self.conn.network.local_ips()] + self.assertIn(self.LOCAL_IP_NAME, names) + + def test_update(self): + sot = self.conn.network.update_local_ip( + self.LOCAL_IP_ID, + name=self.LOCAL_IP_NAME_UPDATED, + description=self.LOCAL_IP_DESCRIPTION_UPDATED) + self.assertEqual(self.LOCAL_IP_NAME_UPDATED, sot.name) + self.assertEqual(self.LOCAL_IP_DESCRIPTION_UPDATED, + sot.description) diff --git a/openstack/tests/functional/network/v2/test_local_ip_association.py b/openstack/tests/functional/network/v2/test_local_ip_association.py new file mode 100644 index 000000000..e4f1dbb48 --- /dev/null +++ b/openstack/tests/functional/network/v2/test_local_ip_association.py @@ -0,0 +1,68 @@ +# Copyright 2021 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 openstack.network.v2 import local_ip_association as _local_ip_association +from openstack.tests.functional import base + + +class TestLocalIPAssociation(base.BaseFunctionalTest): + LOCAL_IP_ID = None + FIXED_PORT_ID = None + FIXED_IP = None + + def setUp(self): + super(TestLocalIPAssociation, self).setUp() + + if not self.conn.network.find_extension('local_ip'): + self.skipTest('Local IP extension disabled') + + self.LOCAL_IP_ID = self.getUniqueString() + self.FIXED_PORT_ID = self.getUniqueString() + self.FIXED_IP = self.getUniqueString() + local_ip_association = self.conn.network.create_local_ip_association( + local_ip=self.LOCAL_IP_ID, + fixed_port_id=self.FIXED_PORT_ID, + fixed_ip=self.FIXED_IP + ) + assert isinstance(local_ip_association, + _local_ip_association.LocalIPAssociation) + self.assertEqual(self.LOCAL_IP_ID, local_ip_association.local_ip_id) + self.assertEqual(self.FIXED_PORT_ID, + local_ip_association.fixed_port_id) + self.assertEqual(self.FIXED_IP, + local_ip_association.fixed_ip) + + def tearDown(self): + sot = self.conn.network.delete_local_ip_association( + self.LOCAL_IP_ID, + self.FIXED_PORT_ID) + self.assertIsNone(sot) + super(TestLocalIPAssociation, self).tearDown() + + def test_find(self): + sot = self.conn.network.find_local_ip_association(self.FIXED_PORT_ID, + self.LOCAL_IP_ID) + self.assertEqual(self.FIXED_PORT_ID, sot.fixed_port_id) + + def test_get(self): + sot = self.conn.network.get_local_ip_association(self.FIXED_PORT_ID, + self.LOCAL_IP_ID) + self.assertEqual(self.FIXED_PORT_ID, sot.fixed_port_id) + + def test_list(self): + fixed_port_id = [obj.fixed_port_id for obj in + self.conn.network.local_ip_associations( + self.LOCAL_IP_ID)] + self.assertIn(self.FIXED_PORT_ID, fixed_port_id) diff --git a/openstack/tests/unit/network/v2/test_local_ip.py b/openstack/tests/unit/network/v2/test_local_ip.py new file mode 100644 index 000000000..2dce8576a --- /dev/null +++ b/openstack/tests/unit/network/v2/test_local_ip.py @@ -0,0 +1,68 @@ +# Copyright 2021 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 openstack.network.v2 import local_ip +from openstack.tests.unit import base + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'created_at': '0', + 'id': IDENTIFIER, + 'name': '1', + 'description': '2', + 'project_id': '3', + 'local_port_id': '4', + 'network_id': '5', + 'local_ip_address': '127.0.0.1', + 'ip_mode': 'translate', + 'revision_number': '6', + 'updated_at': '7', +} + + +class TestLocalIP(base.TestCase): + + def test_basic(self): + sot = local_ip.LocalIP() + self.assertEqual('local_ip', sot.resource_key) + self.assertEqual('local_ips', sot.resources_key) + self.assertEqual('/local_ips', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + self.assertDictEqual({"name": "name", + "description": "description", + "sort_key": "sort_key", + "sort_dir": "sort_dir", + "limit": "limit", + "marker": "marker"}, + sot._query_mapping._mapping) + + def test_make_it(self): + sot = local_ip.LocalIP(**EXAMPLE) + self.assertEqual(EXAMPLE['created_at'], sot.created_at) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + self.assertEqual(EXAMPLE['local_port_id'], sot.local_port_id) + self.assertEqual(EXAMPLE['network_id'], sot.network_id) + self.assertEqual(EXAMPLE['local_ip_address'], sot.local_ip_address) + self.assertEqual(EXAMPLE['ip_mode'], sot.ip_mode) + self.assertEqual(EXAMPLE['revision_number'], sot.revision_number) + self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) diff --git a/openstack/tests/unit/network/v2/test_local_ip_association.py b/openstack/tests/unit/network/v2/test_local_ip_association.py new file mode 100644 index 000000000..5e55f959e --- /dev/null +++ b/openstack/tests/unit/network/v2/test_local_ip_association.py @@ -0,0 +1,56 @@ +# Copyright 2021 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 openstack.network.v2 import local_ip_association +from openstack.tests.unit import base + +EXAMPLE = { + 'local_ip_id': '0', + 'local_ip_address': '127.0.0.1', + 'fixed_port_id': '1', + 'fixed_ip': '127.0.0.2', + 'host': '2', +} + + +class TestLocalIP(base.TestCase): + + def test_basic(self): + sot = local_ip_association.LocalIPAssociation() + self.assertEqual('port_association', sot.resource_key) + self.assertEqual('port_associations', sot.resources_key) + self.assertEqual('/local_ips/%(local_ip_id)s/port_associations', + sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + self.assertDictEqual( + {'local_ip_address': 'local_ip_address', + 'fixed_port_id': 'fixed_port_id', + 'fixed_ip': 'fixed_ip', + 'limit': 'limit', + 'marker': 'marker'}, + sot._query_mapping._mapping) + + def test_make_it(self): + sot = local_ip_association.LocalIPAssociation(**EXAMPLE) + self.assertEqual(EXAMPLE['local_ip_id'], sot.local_ip_id) + self.assertEqual(EXAMPLE['local_ip_address'], sot.local_ip_address) + self.assertEqual(EXAMPLE['fixed_port_id'], sot.fixed_port_id) + self.assertEqual(EXAMPLE['fixed_ip'], sot.fixed_ip) + self.assertEqual(EXAMPLE['host'], sot.host) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 6b6d2d98c..57ffe83fc 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -31,6 +31,8 @@ from openstack.network.v2 import l3_conntrack_helper from openstack.network.v2 import listener from openstack.network.v2 import load_balancer +from openstack.network.v2 import local_ip +from openstack.network.v2 import local_ip_association from openstack.network.v2 import metering_label from openstack.network.v2 import metering_label_rule from openstack.network.v2 import network @@ -67,6 +69,7 @@ ROUTER_ID = 'router-id-' + uuid.uuid4().hex FIP_ID = 'fip-id-' + uuid.uuid4().hex CT_HELPER_ID = 'ct-helper-id-' + uuid.uuid4().hex +LOCAL_IP_ID = 'lip-id-' + uuid.uuid4().hex class TestNetworkProxy(test_proxy_base.TestProxyBase): @@ -493,6 +496,104 @@ def test_flavors(self): self.verify_list(self.proxy.flavors, flavor.Flavor) +class TestNetworkLocalIp(TestNetworkProxy): + def test_local_ip_create_attrs(self): + self.verify_create(self.proxy.create_local_ip, local_ip.LocalIP) + + def test_local_ip_delete(self): + self.verify_delete(self.proxy.delete_local_ip, local_ip.LocalIP, + False, expected_kwargs={'if_revision': None}) + + def test_local_ip_delete_ignore(self): + self.verify_delete(self.proxy.delete_local_ip, local_ip.LocalIP, + True, expected_kwargs={'if_revision': None}) + + def test_local_ip_delete_if_revision(self): + self.verify_delete(self.proxy.delete_local_ip, local_ip.LocalIP, + True, method_kwargs={'if_revision': 42}, + expected_kwargs={'if_revision': 42}) + + def test_local_ip_find(self): + self.verify_find(self.proxy.find_local_ip, local_ip.LocalIP) + + def test_local_ip_get(self): + self.verify_get(self.proxy.get_local_ip, local_ip.LocalIP) + + def test_local_ips(self): + self.verify_list(self.proxy.local_ips, local_ip.LocalIP) + + def test_local_ip_update(self): + self.verify_update(self.proxy.update_local_ip, local_ip.LocalIP, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': None}) + + def test_local_ip_update_if_revision(self): + self.verify_update(self.proxy.update_local_ip, local_ip.LocalIP, + method_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': 42}, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': 42}) + + +class TestNetworkLocalIpAssociation(TestNetworkProxy): + def test_local_ip_association_create_attrs(self): + self.verify_create(self.proxy.create_local_ip_association, + local_ip_association.LocalIPAssociation, + method_kwargs={'local_ip': LOCAL_IP_ID}, + expected_kwargs={'local_ip_id': LOCAL_IP_ID}) + + def test_local_ip_association_delete(self): + self.verify_delete( + self.proxy.delete_local_ip_association, + local_ip_association.LocalIPAssociation, + ignore_missing=False, + method_args=[LOCAL_IP_ID, "resource_or_id"], + expected_args=["resource_or_id"], + expected_kwargs={'if_revision': None, + 'local_ip_id': LOCAL_IP_ID}) + + def test_local_ip_association_delete_ignore(self): + self.verify_delete( + self.proxy.delete_local_ip_association, + local_ip_association.LocalIPAssociation, + ignore_missing=True, + method_args=[LOCAL_IP_ID, "resource_or_id"], + expected_args=["resource_or_id"], + expected_kwargs={'if_revision': None, + 'local_ip_id': LOCAL_IP_ID}) + + def test_local_ip_association_find(self): + lip = local_ip.LocalIP.new(id=LOCAL_IP_ID) + + self._verify( + 'openstack.proxy.Proxy._find', + self.proxy.find_local_ip_association, + method_args=['local_ip_association_id', lip], + expected_args=[ + local_ip_association.LocalIPAssociation, + 'local_ip_association_id'], + expected_kwargs={ + 'ignore_missing': True, 'local_ip_id': LOCAL_IP_ID}) + + def test_local_ip_association_get(self): + lip = local_ip.LocalIP.new(id=LOCAL_IP_ID) + + self._verify( + 'openstack.proxy.Proxy._get', + self.proxy.get_local_ip_association, + method_args=['local_ip_association_id', lip], + expected_args=[ + local_ip_association.LocalIPAssociation, + 'local_ip_association_id'], + expected_kwargs={'local_ip_id': LOCAL_IP_ID}) + + def test_local_ip_associations(self): + self.verify_list(self.proxy.local_ip_associations, + local_ip_association.LocalIPAssociation, + method_kwargs={'local_ip': LOCAL_IP_ID}, + expected_kwargs={'local_ip_id': LOCAL_IP_ID}) + + class TestNetworkServiceProfile(TestNetworkProxy): def test_service_profile_create_attrs(self): self.verify_create(self.proxy.create_service_profile, From 47ae81d7174d642ebf38d453227edd071e0411f5 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 1 Dec 2021 11:04:09 +0000 Subject: [PATCH 2966/3836] compute: Default to 2.48 for server diagnostics Compute API microversion 2.48 standardized the response for the server diagnostics API [1]. It should be preferred where possible. We also correct the type for a number of fields: these are body fields, not URI fields, and I suspect this was simply a case of fat fingers. [1] https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id44 Change-Id: I55bdd68812b239bfb6ebfaa4339514be0dcff80f Signed-off-by: Stephen Finucane --- openstack/compute/v2/server_diagnostics.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/openstack/compute/v2/server_diagnostics.py b/openstack/compute/v2/server_diagnostics.py index 24a8501ad..faed7e9d7 100644 --- a/openstack/compute/v2/server_diagnostics.py +++ b/openstack/compute/v2/server_diagnostics.py @@ -22,6 +22,8 @@ class ServerDiagnostics(resource.Resource): requires_id = False + _max_microversion = '2.48' + #: Indicates whether or not a config drive was used for this server. has_config_drive = resource.Body('config_drive') #: The current state of the VM. @@ -33,20 +35,20 @@ class ServerDiagnostics(resource.Resource): #: The hypervisor OS. hypervisor_os = resource.Body('hypervisor_os') #: The amount of time in seconds that the VM has been running. - uptime = resource.URI('uptime') + uptime = resource.Body('uptime') #: The number of vCPUs. - num_cpus = resource.URI('num_cpus') + num_cpus = resource.Body('num_cpus') #: The number of disks. - num_disks = resource.URI('num_disks') + num_disks = resource.Body('num_disks') #: The number of vNICs. - num_nics = resource.URI('num_nics') + num_nics = resource.Body('num_nics') #: The dictionary with information about VM memory usage. - memory_details = resource.URI('memory_details') + memory_details = resource.Body('memory_details') #: The list of dictionaries with detailed information about VM CPUs. - cpu_details = resource.URI('cpu_details') + cpu_details = resource.Body('cpu_details') #: The list of dictionaries with detailed information about VM disks. - disk_details = resource.URI('disk_details') + disk_details = resource.Body('disk_details') #: The list of dictionaries with detailed information about VM NICs. - nic_details = resource.URI('nic_details') + nic_details = resource.Body('nic_details') #: The ID for the server. - server_id = resource.URI('server_id') + server_id = resource.Body('server_id') From 02c175db23a330268ee5797b86a30678fe208488 Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Wed, 17 Nov 2021 09:20:58 +0100 Subject: [PATCH 2967/3836] Compute - reindentation of the docstrings Change-Id: Iee76170382faf13c584fa215ef9dbe94de805d32 --- openstack/compute/v2/_proxy.py | 397 ++++++++++++++++----------------- 1 file changed, 198 insertions(+), 199 deletions(-) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 966323090..92b9f96f2 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -48,12 +48,12 @@ def find_extension(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of an extension. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.compute.v2.extension.Extension` or - None + None """ return self._find(extension.Extension, name_or_id, ignore_missing=ignore_missing) @@ -96,8 +96,8 @@ def create_flavor(self, **attrs): """Create a new flavor from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.compute.v2.flavor.Flavor`, - comprised of the properties on the Flavor class. + a :class:`~openstack.compute.v2.flavor.Flavor`, + comprised of the properties on the Flavor class. :returns: The results of flavor creation :rtype: :class:`~openstack.compute.v2.flavor.Flavor` @@ -108,12 +108,12 @@ def delete_flavor(self, flavor, ignore_missing=True): """Delete a flavor :param flavor: The value can be either the ID of a flavor or a - :class:`~openstack.compute.v2.flavor.Flavor` instance. + :class:`~openstack.compute.v2.flavor.Flavor` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the flavor does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent flavor. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the flavor does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent flavor. :returns: ``None`` """ @@ -282,12 +282,12 @@ def get_aggregate(self, aggregate): """Get a single host aggregate :param image: The value can be the ID of an aggregate or a - :class:`~openstack.compute.v2.aggregate.Aggregate` - instance. + :class:`~openstack.compute.v2.aggregate.Aggregate` + instance. :returns: One :class:`~openstack.compute.v2.aggregate.Aggregate` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_aggregate.Aggregate, aggregate) @@ -309,8 +309,8 @@ def create_aggregate(self, **attrs): """Create a new host aggregate from attributes :param dict attrs: Keyword arguments which will be used to create a - :class:`~openstack.compute.v2.aggregate.Aggregate`, - comprised of the properties on the Aggregate class. + :class:`~openstack.compute.v2.aggregate.Aggregate`, + comprised of the properties on the Aggregate class. :returns: The results of aggregate creation :rtype: :class:`~openstack.compute.v2.aggregate.Aggregate` @@ -321,10 +321,10 @@ def update_aggregate(self, aggregate, **attrs): """Update a host aggregate :param server: Either the ID of a host aggregate or a - :class:`~openstack.compute.v2.aggregate.Aggregate` - instance. + :class:`~openstack.compute.v2.aggregate.Aggregate` + instance. :attrs kwargs: The attributes to update on the aggregate represented - by ``aggregate``. + by ``aggregate``. :returns: The updated aggregate :rtype: :class:`~openstack.compute.v2.aggregate.Aggregate` @@ -335,13 +335,13 @@ def delete_aggregate(self, aggregate, ignore_missing=True): """Delete a host aggregate :param keypair: The value can be either the ID of an aggregate or a - :class:`~openstack.compute.v2.aggregate.Aggregate` - instance. + :class:`~openstack.compute.v2.aggregate.Aggregate` + instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the aggregate does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent aggregate. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the aggregate does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent aggregate. :returns: ``None`` """ @@ -352,8 +352,8 @@ def add_host_to_aggregate(self, aggregate, host): """Adds a host to an aggregate :param aggregate: Either the ID of a aggregate or a - :class:`~openstack.compute.v2.aggregate.Aggregate` - instance. + :class:`~openstack.compute.v2.aggregate.Aggregate` + instance. :param str host: The host to add to the aggregate :returns: One :class:`~openstack.compute.v2.aggregate.Aggregate` @@ -365,8 +365,8 @@ def remove_host_from_aggregate(self, aggregate, host): """Removes a host from an aggregate :param aggregate: Either the ID of a aggregate or a - :class:`~openstack.compute.v2.aggregate.Aggregate` - instance. + :class:`~openstack.compute.v2.aggregate.Aggregate` + instance. :param str host: The host to remove from the aggregate :returns: One :class:`~openstack.compute.v2.aggregate.Aggregate` @@ -378,11 +378,11 @@ def set_aggregate_metadata(self, aggregate, metadata): """Creates or replaces metadata for an aggregate :param aggregate: Either the ID of a aggregate or a - :class:`~openstack.compute.v2.aggregate.Aggregate` - instance. + :class:`~openstack.compute.v2.aggregate.Aggregate` + instance. :param dict metadata: Metadata key and value pairs. The maximum - size for each metadata key and value pair - is 255 bytes. + size for each metadata key and value pair + is 255 bytes. :returns: One :class:`~openstack.compute.v2.aggregate.Aggregate` """ @@ -413,12 +413,12 @@ def delete_image(self, image, ignore_missing=True): """Delete an image :param image: The value can be either the ID of an image or a - :class:`~openstack.compute.v2.image.Image` instance. + :class:`~openstack.compute.v2.image.Image` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the image does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent image. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the image does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent image. :returns: ``None`` """ @@ -429,10 +429,10 @@ def find_image(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a image. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.compute.v2.image.Image` or None """ return self._find(_image.Image, name_or_id, @@ -442,11 +442,11 @@ def get_image(self, image): """Get a single image :param image: The value can be the ID of an image or a - :class:`~openstack.compute.v2.image.Image` instance. + :class:`~openstack.compute.v2.image.Image` instance. :returns: One :class:`~openstack.compute.v2.image.Image` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_image.Image, image) @@ -458,7 +458,7 @@ def images(self, details=True, **query): available properties, otherwise only basic properties are returned. *Default: ``True``* :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of image objects """ @@ -483,7 +483,7 @@ def get_image_metadata(self, image): :class:`~openstack.compute.v2.image.Image` instance. :returns: A :class:`~openstack.compute.v2.image.Image` with only the - image's metadata. All keys and values are Unicode text. + image's metadata. All keys and values are Unicode text. :rtype: :class:`~openstack.compute.v2.image.Image` """ res = self._get_base_resource(image, _image.Image) @@ -495,12 +495,12 @@ def set_image_metadata(self, image, **metadata): :param image: Either the ID of an image or a :class:`~openstack.compute.v2.image.Image` instance. :param kwargs metadata: Key/value pairs to be updated in the image's - metadata. No other metadata is modified - by this call. All keys and values are stored - as Unicode. + metadata. No other metadata is modified + by this call. All keys and values are stored + as Unicode. :returns: A :class:`~openstack.compute.v2.image.Image` with only the - image's metadata. All keys and values are Unicode text. + image's metadata. All keys and values are Unicode text. :rtype: :class:`~openstack.compute.v2.image.Image` """ res = self._get_base_resource(image, _image.Image) @@ -533,8 +533,8 @@ def create_keypair(self, **attrs): """Create a new keypair from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.compute.v2.keypair.Keypair`, - comprised of the properties on the Keypair class. + a :class:`~openstack.compute.v2.keypair.Keypair`, + comprised of the properties on the Keypair class. :returns: The results of keypair creation :rtype: :class:`~openstack.compute.v2.keypair.Keypair` @@ -620,8 +620,8 @@ def create_server(self, **attrs): """Create a new server from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.compute.v2.server.Server`, - comprised of the properties on the Server class. + a :class:`~openstack.compute.v2.server.Server`, + comprised of the properties on the Server class. :returns: The results of server creation :rtype: :class:`~openstack.compute.v2.server.Server` @@ -632,14 +632,14 @@ def delete_server(self, server, ignore_missing=True, force=False): """Delete a server :param server: The value can be either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the server does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent server + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the server does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent server :param bool force: When set to ``True``, the server deletion will be - forced immediately. + forced immediately. :returns: ``None`` """ @@ -654,10 +654,10 @@ def find_server(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a server. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.compute.v2.server.Server` or None """ return self._find(_server.Server, name_or_id, @@ -667,11 +667,11 @@ def get_server(self, server): """Get a single server :param server: The value can be the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :returns: One :class:`~openstack.compute.v2.server.Server` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_server.Server, server) @@ -696,9 +696,9 @@ def update_server(self, server, **attrs): """Update a server :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :attrs kwargs: The attributes to update on the server represented - by ``server``. + by ``server``. :returns: The updated server :rtype: :class:`~openstack.compute.v2.server.Server` @@ -709,7 +709,7 @@ def change_server_password(self, server, new_password): """Change the administrator password :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :param str new_password: The new password to be set. :returns: None @@ -721,7 +721,7 @@ def get_server_password(self, server): """Get the administrator password :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :returns: encrypted password. """ @@ -732,9 +732,9 @@ def reset_server_state(self, server, state): """Reset the state of server :param server: The server can be either the ID of a server or a - :class:`~openstack.compute.v2.server.Server`. + :class:`~openstack.compute.v2.server.Server`. :param state: The state of the server to be set, `active` or - `error` are valid. + `error` are valid. :returns: None """ @@ -745,9 +745,9 @@ def reboot_server(self, server, reboot_type): """Reboot a server :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :param str reboot_type: The type of reboot to perform. - "HARD" and "SOFT" are the current options. + "HARD" and "SOFT" are the current options. :returns: None """ @@ -758,7 +758,7 @@ def rebuild_server(self, server, name, admin_password, **attrs): """Rebuild a server :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :param str name: The name of the server :param str admin_password: The administrator password :param bool preserve_ephemeral: Indicates whether the server @@ -766,18 +766,18 @@ def rebuild_server(self, server, name, admin_password, **attrs): *Default: False* :param str image: The id of an image to rebuild with. *Default: None* :param str access_ipv4: The IPv4 address to rebuild with. - *Default: None* + *Default: None* :param str access_ipv6: The IPv6 address to rebuild with. - *Default: None* + *Default: None* :param dict metadata: A dictionary of metadata to rebuild with. - *Default: None* + *Default: None* :param personality: A list of dictionaries, each including a - **path** and **contents** key, to be injected - into the rebuilt server at launch. - *Default: None* + **path** and **contents** key, to be injected + into the rebuilt server at launch. + *Default: None* :returns: The rebuilt :class:`~openstack.compute.v2.server.Server` - instance. + instance. """ server = self._get_resource(_server.Server, server) return server.rebuild(self, name, admin_password, **attrs) @@ -786,9 +786,9 @@ def resize_server(self, server, flavor): """Resize a server :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :param flavor: Either the ID of a flavor or a - :class:`~openstack.compute.v2.flavor.Flavor` instance. + :class:`~openstack.compute.v2.flavor.Flavor` instance. :returns: None """ @@ -800,7 +800,7 @@ def confirm_server_resize(self, server): """Confirm a server resize :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :returns: None """ @@ -811,7 +811,7 @@ def revert_server_resize(self, server): """Revert a server resize :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :returns: None """ @@ -843,12 +843,12 @@ def backup_server(self, server, name, backup_type, rotation): """Backup a server :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :param name: The name of the backup image. :param backup_type: The type of the backup, for example, daily. :param rotation: The rotation of the back up image, the oldest - image will be removed when image count exceed - the rotation count. + image will be removed when image count exceed + the rotation count. :returns: None """ @@ -859,7 +859,7 @@ def pause_server(self, server): """Pauses a server and changes its status to ``PAUSED``. :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :returns: None """ server = self._get_resource(_server.Server, server) @@ -869,7 +869,7 @@ def unpause_server(self, server): """Unpauses a paused server and changes its status to ``ACTIVE``. :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :returns: None """ server = self._get_resource(_server.Server, server) @@ -879,7 +879,7 @@ def suspend_server(self, server): """Suspends a server and changes its status to ``SUSPENDED``. :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :returns: None """ server = self._get_resource(_server.Server, server) @@ -889,7 +889,7 @@ def resume_server(self, server): """Resumes a suspended server and changes its status to ``ACTIVE``. :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :returns: None """ server = self._get_resource(_server.Server, server) @@ -899,7 +899,7 @@ def lock_server(self, server): """Locks a server. :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :returns: None """ server = self._get_resource(_server.Server, server) @@ -909,7 +909,7 @@ def unlock_server(self, server): """Unlocks a locked server. :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :returns: None """ server = self._get_resource(_server.Server, server) @@ -919,14 +919,14 @@ def rescue_server(self, server, admin_pass=None, image_ref=None): """Puts a server in rescue mode and changes it status to ``RESCUE``. :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :param admin_pass: The password for the rescued server. If you omit - this parameter, the operation generates a new - password. + this parameter, the operation generates a new + password. :param image_ref: The image reference to use to rescue your server. - This can be the image ID or its full URL. If you - omit this parameter, the base image reference will - be used. + This can be the image ID or its full URL. If you + omit this parameter, the base image reference will + be used. :returns: None """ server = self._get_resource(_server.Server, server) @@ -937,7 +937,7 @@ def unrescue_server(self, server): """Unrescues a server and changes its status to ``ACTIVE``. :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :returns: None """ server = self._get_resource(_server.Server, server) @@ -947,14 +947,14 @@ def evacuate_server(self, server, host=None, admin_pass=None, force=None): """Evacuates a server from a failed host to a new host. :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :param host: An optional parameter specifying the name or ID of the - host to which the server is evacuated. + host to which the server is evacuated. :param admin_pass: An optional parameter specifying the administrative - password to access the evacuated or rebuilt server. + password to access the evacuated or rebuilt server. :param force: Force an evacuation by not verifying the provided - destination host by the scheduler. (New in API version - 2.29). + destination host by the scheduler. (New in API version + 2.29). :returns: None """ server = self._get_resource(_server.Server, server) @@ -965,7 +965,7 @@ def start_server(self, server): """Starts a stopped server and changes its state to ``ACTIVE``. :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :returns: None """ server = self._get_resource(_server.Server, server) @@ -975,7 +975,7 @@ def stop_server(self, server): """Stops a running server and changes its state to ``SHUTOFF``. :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :returns: None """ server = self._get_resource(_server.Server, server) @@ -990,7 +990,7 @@ def shelve_server(self, server): operation. Cloud provides could change this permission though. :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :returns: None """ server = self._get_resource(_server.Server, server) @@ -1004,7 +1004,7 @@ def unshelve_server(self, server): change this permission though. :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :returns: None """ server = self._get_resource(_server.Server, server) @@ -1014,8 +1014,8 @@ def create_server_interface(self, server, **attrs): """Create a new server interface from attributes :param server: The server can be either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance - that the interface belongs to. + :class:`~openstack.compute.v2.server.Server` instance + that the interface belongs to. :param dict attrs: Keyword arguments which will be used to create a :class:`~openstack.compute.v2.server_interface.ServerInterface`, comprised of the properties on the ServerInterface class. @@ -1076,9 +1076,9 @@ def add_fixed_ip_to_server(self, server, network_id): """Adds a fixed IP address to a server instance. :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :param network_id: The ID of the network from which a fixed IP address - is about to be allocated. + is about to be allocated. :returns: None """ server = self._get_resource(_server.Server, server) @@ -1088,9 +1088,9 @@ def remove_fixed_ip_from_server(self, server, address): """Removes a fixed IP address from a server instance. :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :param address: The fixed IP address to be disassociated from the - server. + server. :returns: None """ server = self._get_resource(_server.Server, server) @@ -1100,11 +1100,11 @@ def add_floating_ip_to_server(self, server, address, fixed_address=None): """Adds a floating IP address to a server instance. :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :param address: The floating IP address to be added to the server. :param fixed_address: The fixed IP address to be associated with the - floating IP address. Used when the server is - connected to multiple networks. + floating IP address. Used when the server is + connected to multiple networks. :returns: None """ server = self._get_resource(_server.Server, server) @@ -1115,9 +1115,9 @@ def remove_floating_ip_from_server(self, server, address): """Removes a floating IP address from a server instance. :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :param address: The floating IP address to be disassociated from the - server. + server. :returns: None """ server = self._get_resource(_server.Server, server) @@ -1136,14 +1136,14 @@ def delete_server_interface(self, server_interface, server=None, :class:`~openstack.compute.v2.server_interface.ServerInterface` instance. :param server: This parameter need to be specified when ServerInterface - ID is given as value. It can be either the ID of a - server or a :class:`~openstack.compute.v2.server.Server` - instance that the interface belongs to. + ID is given as value. It can be either the ID of a + server or a :class:`~openstack.compute.v2.server.Server` + instance that the interface belongs to. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the server interface does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent server interface. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the server interface does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent server interface. :returns: ``None`` """ @@ -1164,14 +1164,14 @@ def get_server_interface(self, server_interface, server=None): :class:`~openstack.compute.v2.server_interface.ServerInterface` instance. :param server: This parameter need to be specified when ServerInterface - ID is given as value. It can be either the ID of a - server or a :class:`~openstack.compute.v2.server.Server` - instance that the interface belongs to. + ID is given as value. It can be either the ID of a + server or a :class:`~openstack.compute.v2.server.Server` + instance that the interface belongs to. :returns: One :class:`~openstack.compute.v2.server_interface.ServerInterface` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ server_id = self._get_uri_attribute(server_interface, server, "server_id") @@ -1199,9 +1199,9 @@ def server_ips(self, server, network_label=None): """Return a generator of server IPs :param server: The server can be either the ID of a server or a - :class:`~openstack.compute.v2.server.Server`. + :class:`~openstack.compute.v2.server.Server`. :param network_label: The name of a particular network to list - IP addresses from. + IP addresses from. :returns: A generator of ServerIP objects :rtype: :class:`~openstack.compute.v2.server_ip.ServerIP` @@ -1214,12 +1214,12 @@ def availability_zones(self, details=False): """Return a generator of availability zones :param bool details: Return extra details about the availability - zones. This defaults to `False` as it generally - requires extra permission. + zones. This defaults to `False` as it generally + requires extra permission. :returns: A generator of availability zone - :rtype: :class:`~openstack.compute.v2.availability_zone.\ - AvailabilityZone` + :rtype: + :class:`~openstack.compute.v2.availability_zone.AvailabilityZone` """ base_path = '/os-availability-zone/detail' if details else None @@ -1233,12 +1233,12 @@ def get_server_metadata(self, server): """Return a dictionary of metadata for a server :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` or - :class:`~openstack.compute.v2.server.ServerDetail` - instance. + :class:`~openstack.compute.v2.server.Server` or + :class:`~openstack.compute.v2.server.ServerDetail` + instance. :returns: A :class:`~openstack.compute.v2.server.Server` with the - server's metadata. All keys and values are Unicode text. + server's metadata. All keys and values are Unicode text. :rtype: :class:`~openstack.compute.v2.server.Server` """ res = self._get_base_resource(server, _server.Server) @@ -1250,12 +1250,12 @@ def set_server_metadata(self, server, **metadata): :param server: Either the ID of a server or a :class:`~openstack.compute.v2.server.Server` instance. :param kwargs metadata: Key/value pairs to be updated in the server's - metadata. No other metadata is modified - by this call. All keys and values are stored - as Unicode. + metadata. No other metadata is modified + by this call. All keys and values are stored + as Unicode. :returns: A :class:`~openstack.compute.v2.server.Server` with only the - server's metadata. All keys and values are Unicode text. + server's metadata. All keys and values are Unicode text. :rtype: :class:`~openstack.compute.v2.server.Server` """ res = self._get_base_resource(server, _server.Server) @@ -1300,13 +1300,13 @@ def delete_server_group(self, server_group, ignore_missing=True): """Delete a server group :param server_group: The value can be either the ID of a server group - or a :class:`~openstack.compute.v2.server_group.ServerGroup` - instance. + or a :class:`~openstack.compute.v2.server_group.ServerGroup` + instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the server group does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent server group. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the server group does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent server group. :returns: ``None`` """ @@ -1318,10 +1318,10 @@ def find_server_group(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a server group. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.compute.v2.server_group.ServerGroup` object or None @@ -1333,13 +1333,13 @@ def get_server_group(self, server_group): """Get a single server group :param server_group: The value can be the ID of a server group or a - :class:`~openstack.compute.v2.server_group.ServerGroup` - instance. + :class:`~openstack.compute.v2.server_group.ServerGroup` + instance. :returns: A :class:`~openstack.compute.v2.server_group.ServerGroup` object. :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_server_group.ServerGroup, server_group) @@ -1347,7 +1347,7 @@ def server_groups(self, **query): """Return a generator of server groups :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :returns: A generator of ServerGroup objects :rtype: :class:`~openstack.compute.v2.server_group.ServerGroup` @@ -1562,7 +1562,7 @@ def create_volume_attachment(self, server, **attrs): """Create a new volume attachment from attributes :param server: The server can be either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :param dict attrs: Keyword arguments which will be used to create a :class:`~openstack.compute.v2.volume_attachment.VolumeAttachment`, comprised of the properties on the VolumeAttachment class. @@ -1584,15 +1584,15 @@ def update_volume_attachment(self, volume_attachment, server, :class:`~openstack.compute.v2.volume_attachment.VolumeAttachment` instance. :param server: This parameter need to be specified when - VolumeAttachment ID is given as value. It can be - either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` - instance that the attachment belongs to. + VolumeAttachment ID is given as value. It can be + either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` + instance that the attachment belongs to. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the volume attachment does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent volume attachment. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the volume attachment does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent volume attachment. :returns: ``None`` """ @@ -1613,15 +1613,15 @@ def delete_volume_attachment(self, volume_attachment, server, :class:`~openstack.compute.v2.volume_attachment.VolumeAttachment` instance. :param server: This parameter need to be specified when - VolumeAttachment ID is given as value. It can be either - the ID of a server or a - :class:`~openstack.compute.v2.server.Server` - instance that the attachment belongs to. + VolumeAttachment ID is given as value. It can be either + the ID of a server or a + :class:`~openstack.compute.v2.server.Server` + instance that the attachment belongs to. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the volume attachment does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent volume attachment. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the volume attachment does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent volume attachment. :returns: ``None`` """ @@ -1643,20 +1643,20 @@ def get_volume_attachment(self, volume_attachment, server, :class:`~openstack.compute.v2.volume_attachment.VolumeAttachment` instance. :param server: This parameter need to be specified when - VolumeAttachment ID is given as value. It can be either - the ID of a server or a - :class:`~openstack.compute.v2.server.Server` - instance that the attachment belongs to. + VolumeAttachment ID is given as value. It can be either + the ID of a server or a + :class:`~openstack.compute.v2.server.Server` + instance that the attachment belongs to. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the volume attachment does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent volume attachment. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the volume attachment does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent volume attachment. :returns: One :class:`~openstack.compute.v2.volume_attachment.VolumeAttachment` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ server_id = self._get_uri_attribute(volume_attachment, server, "server_id") @@ -1671,7 +1671,7 @@ def volume_attachments(self, server): """Return a generator of volume attachments :param server: The server can be either the ID of a server or a - :class:`~openstack.compute.v2.server.Server`. + :class:`~openstack.compute.v2.server.Server`. :returns: A generator of VolumeAttachment objects :rtype: @@ -1850,14 +1850,14 @@ def get_server_diagnostics(self, server): """Get a single server diagnostics :param server: This parameter need to be specified when ServerInterface - ID is given as value. It can be either the ID of a - server or a :class:`~openstack.compute.v2.server.Server` - instance that the interface belongs to. + ID is given as value. It can be either the ID of a + server or a :class:`~openstack.compute.v2.server.Server` + instance that the interface belongs to. :returns: One :class:`~openstack.compute.v2.server_diagnostics.ServerDiagnostics` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ server_id = self._get_resource(_server.Server, server).id return self._get(_server_diagnostics.ServerDiagnostics, @@ -1869,10 +1869,9 @@ def create_server_remote_console(self, server, **attrs): """Create a remote console on the server. :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :returns: One - :class:`~openstack.compute.v2.server_remote_console. - ServerRemoteConsole` + :class:`~openstack.compute.v2.server_remote_console.ServerRemoteConsole` """ server_id = resource.Resource._get_id(server) return self._create(_src.ServerRemoteConsole, @@ -1893,11 +1892,11 @@ def get_server_console_output(self, server, length=None): """Return the console output for a server. :param server: Either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :class:`~openstack.compute.v2.server.Server` instance. :param length: Optional number of line to fetch from the end of console - log. All lines will be returned if this is not specified. + log. All lines will be returned if this is not specified. :returns: The console output as a dict. Control characters will be - escaped to create a valid JSON string. + escaped to create a valid JSON string. """ server = self._get_resource(_server.Server, server) return server.get_console_output(self, length=length) From 9290eeea2c4532c3fccf0902a4503f8f89103b2d Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Fri, 3 Dec 2021 10:34:14 +0100 Subject: [PATCH 2968/3836] Orchestration service - reunite class links (:class:) on one line Change-Id: I29930f630f417bd155bb9235e4d2799bddcacf61 --- openstack/orchestration/v1/_proxy.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index 676db93bb..744961e0a 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -220,8 +220,7 @@ def get_stack_environment(self, stack): :class:`~openstack.orchestration.v1.stack.Stack` :returns: One object of - :class:`~openstack.orchestration.v1.stack_environment.\ - StackEnvironment` + :class:`~openstack.orchestration.v1.stack_environment.StackEnvironment` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ @@ -297,8 +296,8 @@ def software_configs(self, **query): :param dict query: Optional query parameters to be sent to limit the software configs returned. :returns: A generator of software config objects. - :rtype: :class:`~openstack.orchestration.v1.software_config.\ - SoftwareConfig` + :rtype: + :class:`~openstack.orchestration.v1.software_config.SoftwareConfig` """ return self._list(_sc.SoftwareConfig, **query) @@ -349,8 +348,8 @@ def software_deployments(self, **query): :param dict query: Optional query parameters to be sent to limit the software deployments returned. :returns: A generator of software deployment objects. - :rtype: :class:`~openstack.orchestration.v1.software_deployment.\ - SoftwareDeployment` + :rtype: + :class:`~openstack.orchestration.v1.software_deployment.SoftwareDeployment` """ return self._list(_sd.SoftwareDeployment, **query) @@ -392,8 +391,8 @@ def update_software_deployment(self, software_deployment, **attrs): represented by ``software_deployment``. :returns: The updated software deployment - :rtype: :class:`~openstack.orchestration.v1.software_deployment.\ - SoftwareDeployment` + :rtype: + :class:`~openstack.orchestration.v1.software_deployment.SoftwareDeployment` """ return self._update(_sd.SoftwareDeployment, software_deployment, **attrs) From 831f7bd5f68a78de55cf5aa3a7b8fa393d452a8f Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Fri, 10 Dec 2021 09:24:34 +0100 Subject: [PATCH 2969/3836] Network service - reunite :class: links on single lines Change-Id: Iae408761abba5c65f6f57129f16d9cea499b058b --- openstack/network/v2/_proxy.py | 547 +++++++++++++++++---------------- 1 file changed, 285 insertions(+), 262 deletions(-) diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 38f8486a0..634054eb8 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -182,7 +182,7 @@ def add_addresses_to_address_group(self, address_group, addresses): :class:`~openstack.network.v2.address_group.AddressGroup` instance. :param list addresses: List of address strings. :returns: AddressGroup with updated addresses - :rtype: :class: `~openstack.network.v2.address_group.AddressGroup` + :rtype: :class:`~openstack.network.v2.address_group.AddressGroup` """ ag = self._get_resource(_address_group.AddressGroup, address_group) return ag.add_addresses(self, addresses) @@ -194,7 +194,7 @@ def remove_addresses_from_address_group(self, address_group, addresses): :class:`~openstack.network.v2.address_group.AddressGroup` instance. :param list addresses: List of address strings. :returns: AddressGroup with updated addresses - :rtype: :class: `~openstack.network.v2.address_group.AddressGroup` + :rtype: :class:`~openstack.network.v2.address_group.AddressGroup` """ ag = self._get_resource(_address_group.AddressGroup, address_group) return ag.remove_addresses(self, addresses) @@ -407,8 +407,8 @@ def get_auto_allocated_topology(self, project=None): The value is the ID or name of a project :returns: The auto-allocated topology - :rtype: :class:`~openstack.network.v2.\ - auto_allocated_topology.AutoAllocatedTopology` + :rtype: + :class:`~openstack.network.v2.auto_allocated_topology.AutoAllocatedTopology` """ # If project option is not given, grab project id from session @@ -445,8 +445,8 @@ def validate_auto_allocated_topology(self, project=None): The value is the ID or name of a project :returns: Whether all resources are correctly configured or not - :rtype: :class:`~openstack.network.v2.\ - auto_allocated_topology.ValidateTopology` + :rtype: + :class:`~openstack.network.v2.auto_allocated_topology.ValidateTopology` """ # If project option is not given, grab project id from session @@ -724,13 +724,13 @@ def create_local_ip_association(self, local_ip, **attrs): :class:`~openstack.network.v2.local_ip.LocalIP` instance. :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.network.v2. - local_ip_association.LocalIPAssociation`, + a + :class:`~openstack.network.v2.local_ip_association.LocalIPAssociation`, comprised of the properties on the LocalIP class. :returns: The results of local ip association creation - :rtype: :class:`~openstack.network.v2.local_ip_association. - LocalIPAssociation` + :rtype: + :class:`~openstack.network.v2.local_ip_association.LocalIPAssociation` """ local_ip = self._get_resource(_local_ip.LocalIP, local_ip) return self._create(_local_ip_association.LocalIPAssociation, @@ -777,8 +777,8 @@ def find_local_ip_association(self, name_or_id, local_ip, attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into underlying methods. such as query filters. - :returns: One :class:`~openstack.network.v2. - local_ip_association.LocalIPAssociation` + :returns: One + :class:`~openstack.network.v2.local_ip_association.LocalIPAssociation` or None """ local_ip = self._get_resource(_local_ip.LocalIP, local_ip) @@ -794,12 +794,11 @@ def get_local_ip_association(self, local_ip_association, local_ip): instance. :param local_ip_association: The value can be the ID of a local ip association or a - :class:`~openstack.network.v2. - local_ip_association.LocalIPAssociation` + :class:`~openstack.network.v2.local_ip_association.LocalIPAssociation` instance. - :returns: One :class:`~openstack.network.v2. - local_ip_association.LocalIPAssociation` + :returns: One + :class:`~openstack.network.v2.local_ip_association.LocalIPAssociation` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ @@ -817,8 +816,8 @@ def local_ip_associations(self, local_ip, **query): the resources being returned. :returns: A generator of local ip association objects - :rtype: :class:`~openstack.network.v2. - local_ip_association.LocalIPAssociation` + :rtype: + :class:`~openstack.network.v2.local_ip_association.LocalIPAssociation` """ local_ip = self._get_resource(_local_ip.LocalIP, local_ip) return self._list(_local_ip_association.LocalIPAssociation, @@ -1093,8 +1092,9 @@ def find_health_monitor(self, name_or_id, ignore_missing=True, **args): attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into underlying methods. such as query filters. - :returns: One :class:`~openstack.network.v2.health_monitor. - HealthMonitor` or None + :returns: One + :class:`~openstack.network.v2.health_monitor.HealthMonitor` + or None """ return self._find(_health_monitor.HealthMonitor, name_or_id, ignore_missing=ignore_missing, **args) @@ -1145,8 +1145,8 @@ def update_health_monitor(self, health_monitor, **attrs): """Update a health monitor :param health_monitor: Either the id of a health monitor or a - :class:`~openstack.network.v2.health_monitor. - HealthMonitor` instance. + :class:`~openstack.network.v2.health_monitor.HealthMonitor` + instance. :param dict attrs: The attributes to update on the health monitor represented by ``value``. @@ -1160,13 +1160,12 @@ def create_vpn_ipsec_site_connection(self, **attrs): """Create a new ipsec site connection from attributes :param dict attrs: Keyword arguments which will be used to create a - :class:`~openstack.network.v2.ipsec_site_connection. - IPSecSiteConnection`, comprised of the properties on the - IPSecSiteConnection class. + :class:`~openstack.network.v2.ipsec_site_connection.IPSecSiteConnection`, + comprised of the properties on the IPSecSiteConnection class. - :returns: The results of ipsec site connection creation :rtype: - :class:`~openstack.network.v2.ipsec_site_connection. - IPSecSiteConnection` + :returns: The results of ipsec site connection creation + :rtype: + :class:`~openstack.network.v2.ipsec_site_connection.IPSecSiteConnection` """ return self._create(_ipsec_site_connection.IPSecSiteConnection, **attrs) @@ -1176,14 +1175,16 @@ def find_vpn_ipsec_site_connection(self, name_or_id, """Find a single ipsec site connection :param name_or_id: The name or ID of an ipsec site connection. - :param bool ignore_missing: When set to ``False`` :class:`~openstack. - exceptions.ResourceNotFound` will be raised when the resource does - not exist.When set to ``True``, None will be returned when + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` + will be raised when the resource does not exist. + When set to ``True``, None will be returned when attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into underlying methods such as query filters. - :returns: One :class:`~openstack.network.v2.ipsec_site_connection. - IPSecSiteConnection` or None + :returns: One + :class:`~openstack.network.v2.ipsec_site_connection.IPSecSiteConnection` + or None """ return self._find(_ipsec_site_connection.IPSecSiteConnection, name_or_id, ignore_missing=ignore_missing, **args) @@ -1192,11 +1193,12 @@ def get_vpn_ipsec_site_connection(self, ipsec_site_connection): """Get a single ipsec site connection :param ipsec_site_connection: The value can be the ID of an ipsec site - connection or a :class:`~openstack.network.v2. - ipsec_site_connection.IPSecSiteConnection` instance. + connection or a + :class:`~openstack.network.v2.ipsec_site_connection.IPSecSiteConnection` + instance. - :returns: One :class:`~openstack.network.v2.ipsec_site_connection. - IPSecSiteConnection` + :returns: One + :class:`~openstack.network.v2.ipsec_site_connection.IPSecSiteConnection` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ @@ -1210,8 +1212,8 @@ def vpn_ipsec_site_connections(self, **query): resources being returned. :returns: A generator of ipsec site connection objects - :rtype: :class:`~openstack.network.v2.ipsec_site_connection. - IPSecSiteConnection` + :rtype: + :class:`~openstack.network.v2.ipsec_site_connection.IPSecSiteConnection` """ return self._list(_ipsec_site_connection.IPSecSiteConnection, **query) @@ -1219,14 +1221,15 @@ def update_vpn_ipsec_site_connection(self, ipsec_site_connection, **attrs): """Update a ipsec site connection :ipsec_site_connection: Either the id of an ipsec site connection or - a :class:`~openstack.network.v2.ipsec_site_connection. - IPSecSiteConnection` instance. + a + :class:`~openstack.network.v2.ipsec_site_connection.IPSecSiteConnection` + instance. :param dict attrs: The attributes to update on the ipsec site connection represented by ``ipsec_site_connection``. :returns: The updated ipsec site connection - :rtype: :class:`~openstack.network.v2.ipsec_site_connection. - IPSecSiteConnection` + :rtype: + :class:`~openstack.network.v2.ipsec_site_connection.IPSecSiteConnection` """ return self._update(_ipsec_site_connection.IPSecSiteConnection, ipsec_site_connection, **attrs) @@ -1236,12 +1239,13 @@ def delete_vpn_ipsec_site_connection(self, ipsec_site_connection, """Delete a ipsec site connection :param ipsec_site_connection: The value can be either the ID of an - ipsec site connection, or a :class:`~openstack.network.v2. - ipsec_site_connection.IPSecSiteConnection` instance. + ipsec site connection, or a + :class:`~openstack.network.v2.ipsec_site_connection.IPSecSiteConnection` + instance. :param bool ignore_missing: - When set to ``False`` :class:`~openstack.exceptions. - ResourceNotFound` will be raised when the ipsec site connection - does not exist. + When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the ipsec site connection does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent ipsec site connection. @@ -1268,9 +1272,10 @@ def find_vpn_ikepolicy(self, name_or_id, """Find a single ike policy :param name_or_id: The name or ID of an ike policy. - :param bool ignore_missing: When set to ``False`` :class:`~openstack. - exceptions.ResourceNotFound` will be raised when the resource does - not exist. When set to ``True``, None will be returned when + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` + will be raised when the resource does not exist. + When set to ``True``, None will be returned when attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into underlying methods such as query filters. @@ -1323,9 +1328,9 @@ def delete_vpn_ikepolicy(self, ikepolicy, ignore_missing=True): :param ikepolicy: The value can be either the ID of an ike policy, or a :class:`~openstack.network.v2.ikepolicy.IkePolicy` instance. :param bool ignore_missing: - When set to ``False`` :class:`~openstack.exceptions. - ResourceNotFound` will be raised when the ike policy - does not exist. + When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` + will be raised when the ike policy does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent ike policy. @@ -1554,8 +1559,9 @@ def find_metering_label(self, name_or_id, ignore_missing=True, **args): attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into underlying methods. such as query filters. - :returns: One :class:`~openstack.network.v2.metering_label. - MeteringLabel` or None + :returns: One + :class:`~openstack.network.v2.metering_label.MeteringLabel` + or None """ return self._find(_metering_label.MeteringLabel, name_or_id, ignore_missing=ignore_missing, **args) @@ -1596,8 +1602,8 @@ def update_metering_label(self, metering_label, **attrs): """Update a metering label :param metering_label: Either the id of a metering label or a - :class:`~openstack.network.v2.metering_label. - MeteringLabel` instance. + :class:`~openstack.network.v2.metering_label.MeteringLabel` + instance. :param dict attrs: The attributes to update on the metering label represented by ``metering_label``. @@ -1611,13 +1617,12 @@ def create_metering_label_rule(self, **attrs): """Create a new metering label rule from attributes :param dict attrs: Keyword arguments which will be used to create a - :class:`~openstack.network.v2.metering_label_rule.\ - MeteringLabelRule`, comprised of the properties on - the MeteringLabelRule class. + :class:`~openstack.network.v2.metering_label_rule.MeteringLabelRule`, + comprised of the properties on the MeteringLabelRule class. :returns: The results of metering label rule creation - :rtype: :class:`~openstack.network.v2.metering_label_rule.\ - MeteringLabelRule` + :rtype: + :class:`~openstack.network.v2.metering_label_rule.MeteringLabelRule` """ return self._create(_metering_label_rule.MeteringLabelRule, **attrs) @@ -1627,8 +1632,9 @@ def delete_metering_label_rule(self, metering_label_rule, :param metering_label_rule: The value can be either the ID of a metering label rule - or a :class:`~openstack.network.v2.metering_label_rule.\ - MeteringLabelRule` instance. + or a + :class:`~openstack.network.v2.metering_label_rule.MeteringLabelRule` + instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the metering label rule does not exist. When set to ``True``, @@ -1652,8 +1658,9 @@ def find_metering_label_rule(self, name_or_id, ignore_missing=True, attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into underlying methods. such as query filters. - :returns: One :class:`~openstack.network.v2.metering_label_rule. - MeteringLabelRule` or None + :returns: One + :class:`~openstack.network.v2.metering_label_rule.MeteringLabelRule` + or None """ return self._find(_metering_label_rule.MeteringLabelRule, name_or_id, ignore_missing=ignore_missing, **args) @@ -1663,12 +1670,11 @@ def get_metering_label_rule(self, metering_label_rule): :param metering_label_rule: The value can be the ID of a metering label rule or a - :class:`~openstack.network.v2.metering_label_rule.\ - MeteringLabelRule` instance. + :class:`~openstack.network.v2.metering_label_rule.MeteringLabelRule` + instance. :returns: One - :class:`~openstack.network.v2.metering_label_rule.\ - MeteringLabelRule` + :class:`~openstack.network.v2.metering_label_rule.MeteringLabelRule` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ @@ -1691,8 +1697,8 @@ def metering_label_rules(self, **query): this metering label rule. :returns: A generator of metering label rule objects - :rtype: :class:`~openstack.network.v2.metering_label_rule. - MeteringLabelRule` + :rtype: + :class:`~openstack.network.v2.metering_label_rule.MeteringLabelRule` """ return self._list(_metering_label_rule.MeteringLabelRule, **query) @@ -1701,14 +1707,14 @@ def update_metering_label_rule(self, metering_label_rule, **attrs): :param metering_label_rule: Either the id of a metering label rule or a - :class:`~openstack.network.v2.metering_label_rule. - MeteringLabelRule` instance. + :class:`~openstack.network.v2.metering_label_rule.MeteringLabelRule` + instance. :param dict attrs: The attributes to update on the metering label rule represented by ``metering_label_rule``. :returns: The updated metering label rule - :rtype: :class:`~openstack.network.v2.metering_label_rule. - MeteringLabelRule` + :rtype: + :class:`~openstack.network.v2.metering_label_rule.MeteringLabelRule` """ return self._update(_metering_label_rule.MeteringLabelRule, metering_label_rule, **attrs) @@ -1829,8 +1835,9 @@ def find_network_ip_availability(self, name_or_id, ignore_missing=True, attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into underlying methods. such as query filters. - :returns: One :class:`~openstack.network.v2.network_ip_availability. - NetworkIPAvailability` or None + :returns: One + :class:`~openstack.network.v2.network_ip_availability.NetworkIPAvailability` + or None """ return self._find(network_ip_availability.NetworkIPAvailability, name_or_id, ignore_missing=ignore_missing, **args) @@ -1842,8 +1849,8 @@ def get_network_ip_availability(self, network): The value can be the ID of a network or a :class:`~openstack.network.v2.network.Network` instance. - :returns: One :class:`~openstack.network.v2.network_ip_availability. - NetworkIPAvailability` + :returns: One + :class:`~openstack.network.v2.network_ip_availability.NetworkIPAvailability` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ @@ -1864,8 +1871,8 @@ def network_ip_availabilities(self, **query): * ``project_id``: Owner tenant ID :returns: A generator of network ip availability objects - :rtype: :class:`~openstack.network.v2.network_ip_availability. - NetworkIPAvailability` + :rtype: + :class:`~openstack.network.v2.network_ip_availability.NetworkIPAvailability` """ return self._list(network_ip_availability.NetworkIPAvailability, **query) @@ -1874,14 +1881,13 @@ def create_network_segment_range(self, **attrs): """Create a new network segment range from attributes :param dict attrs: Keyword arguments which will be used to create a - :class:`~openstack.network.v2. - network_segment_range.NetworkSegmentRange`, + :class:`~openstack.network.v2.network_segment_range.NetworkSegmentRange`, comprised of the properties on the NetworkSegmentRange class. :returns: The results of network segment range creation - :rtype: :class:`~openstack.network.v2.network_segment_range - .NetworkSegmentRange` + :rtype: + :class:`~openstack.network.v2.network_segment_range.NetworkSegmentRange` """ return self._create(_network_segment_range.NetworkSegmentRange, **attrs) @@ -1892,8 +1898,8 @@ def delete_network_segment_range(self, network_segment_range, :param network_segment_range: The value can be either the ID of a network segment range or a - :class:`~openstack.network.v2.network_segment_range. - NetworkSegmentRange` instance. + :class:`~openstack.network.v2.network_segment_range.NetworkSegmentRange` + instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the network segment range does not exist. @@ -1917,8 +1923,9 @@ def find_network_segment_range(self, name_or_id, ignore_missing=True, attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into underlying methods. such as query filters. - :returns: One :class:`~openstack.network.v2.network_segment_range - .NetworkSegmentRange` or None + :returns: One + :class:`~openstack.network.v2.network_segment_range.NetworkSegmentRange` + or None """ return self._find(_network_segment_range.NetworkSegmentRange, name_or_id, ignore_missing=ignore_missing, **args) @@ -1927,11 +1934,12 @@ def get_network_segment_range(self, network_segment_range): """Get a single network segment range :param network_segment_range: The value can be the ID of a network - segment range or a :class:`~openstack.network.v2. - network_segment_range.NetworkSegmentRange` instance. + segment range or a + :class:`~openstack.network.v2.network_segment_range.NetworkSegmentRange` + instance. - :returns: One :class:`~openstack.network.v2._network_segment_range. - NetworkSegmentRange` + :returns: One + :class:`~openstack.network.v2._network_segment_range.NetworkSegmentRange` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ @@ -1964,8 +1972,8 @@ def network_segment_ranges(self, **query): network segment range :returns: A generator of network segment range objects - :rtype: :class:`~openstack.network.v2._network_segment_range. - NetworkSegmentRange` + :rtype: + :class:`~openstack.network.v2._network_segment_range.NetworkSegmentRange` """ return self._list(_network_segment_range.NetworkSegmentRange, **query) @@ -1973,14 +1981,15 @@ def update_network_segment_range(self, network_segment_range, **attrs): """Update a network segment range :param network_segment_range: Either the id of a network segment range - or a :class:`~openstack.network.v2._network_segment_range. - NetworkSegmentRange` instance. + or a + :class:`~openstack.network.v2._network_segment_range.NetworkSegmentRange` + instance. :attrs kwargs: The attributes to update on the network segment range represented by ``value``. :returns: The updated network segment range - :rtype: :class:`~openstack.network.v2._network_segment_range. - NetworkSegmentRange` + :rtype: + :class:`~openstack.network.v2._network_segment_range.NetworkSegmentRange` """ return self._update(_network_segment_range.NetworkSegmentRange, network_segment_range, **attrs) @@ -2334,17 +2343,17 @@ def create_qos_bandwidth_limit_rule(self, qos_policy, **attrs): """Create a new bandwidth limit rule :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.network.v2. - qos_bandwidth_limit_rule.QoSBandwidthLimitRule`, + a + :class:`~openstack.network.v2.qos_bandwidth_limit_rule.QoSBandwidthLimitRule`, comprised of the properties on the QoSBandwidthLimitRule class. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :returns: The results of resource creation - :rtype: :class:`~openstack.network.v2.qos_bandwidth_limit_rule. - QoSBandwidthLimitRule` + :rtype: + :class:`~openstack.network.v2.qos_bandwidth_limit_rule.QoSBandwidthLimitRule` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._create(_qos_bandwidth_limit_rule.QoSBandwidthLimitRule, @@ -2355,12 +2364,12 @@ def delete_qos_bandwidth_limit_rule(self, qos_rule, qos_policy, """Delete a bandwidth limit rule :param qos_rule: The value can be either the ID of a bandwidth limit - rule or a :class:`~openstack.network.v2. - qos_bandwidth_limit_rule.QoSBandwidthLimitRule` + rule or a + :class:`~openstack.network.v2.qos_bandwidth_limit_rule.QoSBandwidthLimitRule` instance. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the resource does not exist. @@ -2380,8 +2389,8 @@ def find_qos_bandwidth_limit_rule(self, qos_rule_id, qos_policy, :param qos_rule_id: The ID of a bandwidth limit rule. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the resource does not exist. @@ -2389,8 +2398,9 @@ def find_qos_bandwidth_limit_rule(self, qos_rule_id, qos_policy, attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into underlying methods. such as query filters. - :returns: One :class:`~openstack.network.v2.qos_bandwidth_limit_rule. - QoSBandwidthLimitRule` or None + :returns: One + :class:`~openstack.network.v2.qos_bandwidth_limit_rule.QoSBandwidthLimitRule` + or None """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._find(_qos_bandwidth_limit_rule.QoSBandwidthLimitRule, @@ -2401,14 +2411,14 @@ def get_qos_bandwidth_limit_rule(self, qos_rule, qos_policy): """Get a single bandwidth limit rule :param qos_rule: The value can be the ID of a minimum bandwidth rule or - a :class:`~openstack.network.v2. - qos_bandwidth_limit_rule.QoSBandwidthLimitRule` + a + :class:`~openstack.network.v2.qos_bandwidth_limit_rule.QoSBandwidthLimitRule` instance. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. - :returns: One :class:`~openstack.network.v2.qos_bandwidth_limit_rule. - QoSBandwidthLimitRule` + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. + :returns: One + :class:`~openstack.network.v2.qos_bandwidth_limit_rule.QoSBandwidthLimitRule` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ @@ -2420,13 +2430,13 @@ def qos_bandwidth_limit_rules(self, qos_policy, **query): """Return a generator of bandwidth limit rules :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of bandwidth limit rule objects - :rtype: :class:`~openstack.network.v2.qos_bandwidth_limit_rule. - QoSBandwidthLimitRule` + :rtype: + :class:`~openstack.network.v2.qos_bandwidth_limit_rule.QoSBandwidthLimitRule` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._list(_qos_bandwidth_limit_rule.QoSBandwidthLimitRule, @@ -2437,18 +2447,17 @@ def update_qos_bandwidth_limit_rule(self, qos_rule, qos_policy, """Update a bandwidth limit rule :param qos_rule: Either the id of a bandwidth limit rule or a - :class:`~openstack.network.v2. - qos_bandwidth_limit_rule.QoSBandwidthLimitRule` + :class:`~openstack.network.v2.qos_bandwidth_limit_rule.QoSBandwidthLimitRule` instance. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :attrs kwargs: The attributes to update on the bandwidth limit rule represented by ``value``. :returns: The updated minimum bandwidth rule - :rtype: :class:`~openstack.network.v2.qos_bandwidth_limit_rule. - QoSBandwidthLimitRule` + :rtype: + :class:`~openstack.network.v2.qos_bandwidth_limit_rule.QoSBandwidthLimitRule` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._update(_qos_bandwidth_limit_rule.QoSBandwidthLimitRule, @@ -2458,17 +2467,17 @@ def create_qos_dscp_marking_rule(self, qos_policy, **attrs): """Create a new QoS DSCP marking rule :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.network.v2. - qos_dscp_marking_rule.QoSDSCPMarkingRule`, + a + :class:`~openstack.network.v2.qos_dscp_marking_rule.QoSDSCPMarkingRule`, comprised of the properties on the QosDscpMarkingRule class. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :returns: The results of router creation - :rtype: :class:`~openstack.network.v2.qos_dscp_marking_rule. - QoSDSCPMarkingRule` + :rtype: + :class:`~openstack.network.v2.qos_dscp_marking_rule.QoSDSCPMarkingRule` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._create(_qos_dscp_marking_rule.QoSDSCPMarkingRule, @@ -2479,12 +2488,12 @@ def delete_qos_dscp_marking_rule(self, qos_rule, qos_policy, """Delete a QoS DSCP marking rule :param qos_rule: The value can be either the ID of a minimum bandwidth - rule or a :class:`~openstack.network.v2. - qos_dscp_marking_rule.QoSDSCPMarkingRule` + rule or a + :class:`~openstack.network.v2.qos_dscp_marking_rule.QoSDSCPMarkingRule` instance. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the resource does not exist. @@ -2504,8 +2513,8 @@ def find_qos_dscp_marking_rule(self, qos_rule_id, qos_policy, :param qos_rule_id: The ID of a QoS DSCP marking rule. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the resource does not exist. @@ -2513,8 +2522,9 @@ def find_qos_dscp_marking_rule(self, qos_rule_id, qos_policy, attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into underlying methods. such as query filters. - :returns: One :class:`~openstack.network.v2.qos_dscp_marking_rule. - QoSDSCPMarkingRule` or None + :returns: One + :class:`~openstack.network.v2.qos_dscp_marking_rule.QoSDSCPMarkingRule` + or None """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._find(_qos_dscp_marking_rule.QoSDSCPMarkingRule, @@ -2525,13 +2535,14 @@ def get_qos_dscp_marking_rule(self, qos_rule, qos_policy): """Get a single QoS DSCP marking rule :param qos_rule: The value can be the ID of a minimum bandwidth rule or - a :class:`~openstack.network.v2.qos_dscp_marking_rule. - QoSDSCPMarkingRule` instance. + a + :class:`~openstack.network.v2.qos_dscp_marking_rule.QoSDSCPMarkingRule` + instance. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. - :returns: One :class:`~openstack.network.v2.qos_dscp_marking_rule. - QoSDSCPMarkingRule` + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. + :returns: One + :class:`~openstack.network.v2.qos_dscp_marking_rule.QoSDSCPMarkingRule` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ @@ -2543,13 +2554,13 @@ def qos_dscp_marking_rules(self, qos_policy, **query): """Return a generator of QoS DSCP marking rules :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of QoS DSCP marking rule objects - :rtype: :class:`~openstack.network.v2.qos_dscp_marking_rule. - QoSDSCPMarkingRule` + :rtype: + :class:`~openstack.network.v2.qos_dscp_marking_rule.QoSDSCPMarkingRule` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._list(_qos_dscp_marking_rule.QoSDSCPMarkingRule, @@ -2559,17 +2570,17 @@ def update_qos_dscp_marking_rule(self, qos_rule, qos_policy, **attrs): """Update a QoS DSCP marking rule :param qos_rule: Either the id of a minimum bandwidth rule or a - :class:`~openstack.network.v2.qos_dscp_marking_rule. - QoSDSCPMarkingRule` instance. + :class:`~openstack.network.v2.qos_dscp_marking_rule.QoSDSCPMarkingRule` + instance. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :attrs kwargs: The attributes to update on the QoS DSCP marking rule represented by ``value``. :returns: The updated QoS DSCP marking rule - :rtype: :class:`~openstack.network.v2.qos_dscp_marking_rule. - QoSDSCPMarkingRule` + :rtype: + :class:`~openstack.network.v2.qos_dscp_marking_rule.QoSDSCPMarkingRule` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._update(_qos_dscp_marking_rule.QoSDSCPMarkingRule, @@ -2579,17 +2590,17 @@ def create_qos_minimum_bandwidth_rule(self, qos_policy, **attrs): """Create a new minimum bandwidth rule :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.network.v2. - qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule`, + a + :class:`~openstack.network.v2.qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule`, comprised of the properties on the QoSMinimumBandwidthRule class. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :returns: The results of resource creation - :rtype: :class:`~openstack.network.v2.qos_minimum_bandwidth_rule. - QoSMinimumBandwidthRule` + :rtype: + :class:`~openstack.network.v2.qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._create( @@ -2601,12 +2612,12 @@ def delete_qos_minimum_bandwidth_rule(self, qos_rule, qos_policy, """Delete a minimum bandwidth rule :param qos_rule: The value can be either the ID of a minimum bandwidth - rule or a :class:`~openstack.network.v2. - qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule` + rule or a + :class:`~openstack.network.v2.qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule` instance. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the resource does not exist. @@ -2626,8 +2637,8 @@ def find_qos_minimum_bandwidth_rule(self, qos_rule_id, qos_policy, :param qos_rule_id: The ID of a minimum bandwidth rule. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the resource does not exist. @@ -2635,8 +2646,9 @@ def find_qos_minimum_bandwidth_rule(self, qos_rule_id, qos_policy, attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into underlying methods. such as query filters. - :returns: One :class:`~openstack.network.v2.qos_minimum_bandwidth_rule. - QoSMinimumBandwidthRule` or None + :returns: One + :class:`~openstack.network.v2.qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule` + or None """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._find(_qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, @@ -2647,15 +2659,17 @@ def get_qos_minimum_bandwidth_rule(self, qos_rule, qos_policy): """Get a single minimum bandwidth rule :param qos_rule: The value can be the ID of a minimum bandwidth rule or - a :class:`~openstack.network.v2. - qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule` + a + :class:`~openstack.network.v2.qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule` instance. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. - :returns: One :class:`~openstack.network.v2.qos_minimum_bandwidth_rule. - QoSMinimumBandwidthRule` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` + instance. + :returns: One + :class:`~openstack.network.v2.qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule` + :raises: + :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) @@ -2666,13 +2680,13 @@ def qos_minimum_bandwidth_rules(self, qos_policy, **query): """Return a generator of minimum bandwidth rules :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of minimum bandwidth rule objects - :rtype: :class:`~openstack.network.v2.qos_minimum_bandwidth_rule. - QoSMinimumBandwidthRule` + :rtype: + :class:`~openstack.network.v2.qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._list(_qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, @@ -2683,18 +2697,18 @@ def update_qos_minimum_bandwidth_rule(self, qos_rule, qos_policy, """Update a minimum bandwidth rule :param qos_rule: Either the id of a minimum bandwidth rule or a - :class:`~openstack.network.v2. - qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule` + :class:`~openstack.network.v2.qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule` instance. :param qos_policy: The value can be the ID of the QoS policy that the - rule belongs or a :class:`~openstack.network.v2. - qos_policy.QoSPolicy` instance. + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` + instance. :attrs kwargs: The attributes to update on the minimum bandwidth rule represented by ``value``. :returns: The updated minimum bandwidth rule - :rtype: :class:`~openstack.network.v2.qos_minimum_bandwidth_rule. - QoSMinimumBandwidthRule` + :rtype: + :class:`~openstack.network.v2.qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._update(_qos_minimum_bandwidth_rule. @@ -2705,8 +2719,8 @@ def create_qos_policy(self, **attrs): """Create a new QoS policy from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.network.v2.qos_policy. - QoSPolicy`, comprised of the properties on the + a :class:`~openstack.network.v2.qos_policy.QoSPolicy`, + comprised of the properties on the QoSPolicy class. :returns: The results of QoS policy creation @@ -2811,8 +2825,7 @@ def get_qos_rule_type(self, qos_rule_type): :param qos_rule_type: The value can be the name of a QoS policy rule type or a - :class:`~openstack.network.v2. - qos_rule_type.QoSRuleType` + :class:`~openstack.network.v2.qos_rule_type.QoSRuleType` instance. :returns: One :class:`~openstack.network.v2.qos_rule_type.QoSRuleType` @@ -3107,7 +3120,7 @@ def add_interface_to_router(self, router, subnet_id=None, port_id=None): :param subnet_id: ID of the subnet :param port_id: ID of the port :returns: Router with updated interface - :rtype: :class: `~openstack.network.v2.router.Router` + :rtype: :class:`~openstack.network.v2.router.Router` """ body = {} if port_id: @@ -3126,7 +3139,7 @@ def remove_interface_from_router(self, router, subnet_id=None, :param subnet: ID of the subnet :param port: ID of the port :returns: Router with updated interface - :rtype: :class: `~openstack.network.v2.router.Router` + :rtype: :class:`~openstack.network.v2.router.Router` """ body = {} @@ -3144,7 +3157,7 @@ def add_extra_routes_to_router(self, router, body): :class:`~openstack.network.v2.router.Router` :param body: The request body as documented in the api-ref. :returns: Router with updated extra routes - :rtype: :class: `~openstack.network.v2.router.Router` + :rtype: :class:`~openstack.network.v2.router.Router` """ router = self._get_resource(_router.Router, router) return router.add_extra_routes(self, body=body) @@ -3156,7 +3169,7 @@ def remove_extra_routes_from_router(self, router, body): :class:`~openstack.network.v2.router.Router` :param body: The request body as documented in the api-ref. :returns: Router with updated extra routes - :rtype: :class: `~openstack.network.v2.router.Router` + :rtype: :class:`~openstack.network.v2.router.Router` """ router = self._get_resource(_router.Router, router) return router.remove_extra_routes(self, body=body) @@ -3168,7 +3181,7 @@ def add_gateway_to_router(self, router, **body): :class:`~openstack.network.v2.router.Router` :param body: Body with the gateway information :returns: Router with updated interface - :rtype: :class: `~openstack.network.v2.router.Router` + :rtype: :class:`~openstack.network.v2.router.Router` """ router = self._get_resource(_router.Router, router) return router.add_gateway(self, **body) @@ -3180,7 +3193,7 @@ def remove_gateway_from_router(self, router, **body): :class:`~openstack.network.v2.router.Router` :param body: Body with the gateway information :returns: Router with updated interface - :rtype: :class: `~openstack.network.v2.router.Router` + :rtype: :class:`~openstack.network.v2.router.Router` """ router = self._get_resource(_router.Router, router) return router.remove_gateway(self, **body) @@ -3280,8 +3293,8 @@ def find_firewall_group(self, name_or_id, ignore_missing=True, **args): attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into underlying methods. such as query filters. - :returns: One :class:`~openstack.network.v2.firewall_group. - FirewallGroup` or None + :returns: One + :class:`~openstack.network.v2.firewall_group.FirewallGroup` or None """ return self._find(_firewall_group.FirewallGroup, name_or_id, ignore_missing=ignore_missing, **args) @@ -3380,8 +3393,9 @@ def find_firewall_policy(self, name_or_id, ignore_missing=True, **args): attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into underlying methods. such as query filters. - :returns: One :class:`~openstack.network.v2.firewall_policy. - FirewallPolicy` or None + :returns: One + :class:`~openstack.network.v2.firewall_policy.FirewallPolicy` + or None """ return self._find(_firewall_policy.FirewallPolicy, name_or_id, ignore_missing=ignore_missing, **args) @@ -3511,8 +3525,9 @@ def find_firewall_rule(self, name_or_id, ignore_missing=True, **args): attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into underlying methods. such as query filters. - :returns: One :class:`~openstack.network.v2.firewall_rule. - FirewallRule` or None + :returns: One + :class:`~openstack.network.v2.firewall_rule.FirewallRule` + or None """ return self._find(_firewall_rule.FirewallRule, name_or_id, ignore_missing=ignore_missing, **args) @@ -3620,8 +3635,9 @@ def find_security_group(self, name_or_id, ignore_missing=True, **args): attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into underlying methods. such as query filters. - :returns: One :class:`~openstack.network.v2.security_group. - SecurityGroup` or None + :returns: One + :class:`~openstack.network.v2.security_group.SecurityGroup` + or None """ return self._find(_security_group.SecurityGroup, name_or_id, ignore_missing=ignore_missing, **args) @@ -3678,13 +3694,13 @@ def create_security_group_rule(self, **attrs): """Create a new security group rule from attributes :param dict attrs: Keyword arguments which will be used to create a - :class:`~openstack.network.v2.security_group_rule. - SecurityGroupRule`, comprised of the properties on the + :class:`~openstack.network.v2.security_group_rule.SecurityGroupRule`, + comprised of the properties on the SecurityGroupRule class. :returns: The results of security group rule creation - :rtype: :class:`~openstack.network.v2.security_group_rule.\ - SecurityGroupRule` + :rtype: + :class:`~openstack.network.v2.security_group_rule.SecurityGroupRule` """ return self._create(_security_group_rule.SecurityGroupRule, **attrs) @@ -3692,14 +3708,14 @@ def create_security_group_rules(self, data): """Create new security group rules from the list of attributes :param list data: List of dicts of attributes which will be used to - create a :class:`~openstack.network.v2.\ - security_group_rule.SecurityGroupRule`, + create a + :class:`~openstack.network.v2.security_group_rule.SecurityGroupRule`, comprised of the properties on the SecurityGroupRule class. :returns: A generator of security group rule objects - :rtype: :class:`~openstack.network.v2.security_group_rule.\ - SecurityGroupRule` + :rtype: + :class:`~openstack.network.v2.security_group_rule.SecurityGroupRule` """ return self._bulk_create(_security_group_rule.SecurityGroupRule, data) @@ -3709,8 +3725,9 @@ def delete_security_group_rule(self, security_group_rule, :param security_group_rule: The value can be either the ID of a security group rule - or a :class:`~openstack.network.v2.security_group_rule. - SecurityGroupRule` instance. + or a + :class:`~openstack.network.v2.security_group_rule.SecurityGroupRule` + instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the security group rule does not exist. @@ -3737,8 +3754,9 @@ def find_security_group_rule(self, name_or_id, ignore_missing=True, attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into underlying methods. such as query filters. - :returns: One :class:`~openstack.network.v2.security_group_rule. - SecurityGroupRule` or None + :returns: One + :class:`~openstack.network.v2.security_group_rule.SecurityGroupRule` + or None """ return self._find(_security_group_rule.SecurityGroupRule, name_or_id, ignore_missing=ignore_missing, **args) @@ -3748,11 +3766,11 @@ def get_security_group_rule(self, security_group_rule): :param security_group_rule: The value can be the ID of a security group rule or a - :class:`~openstack.network.v2.security_group_rule.\ - SecurityGroupRule` instance. + :class:`~openstack.network.v2.security_group_rule.SecurityGroupRule` + instance. - :returns: :class:`~openstack.network.v2.security_group_rule.\ - SecurityGroupRule` + :returns: + :class:`~openstack.network.v2.security_group_rule.SecurityGroupRule` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ @@ -3776,8 +3794,8 @@ def security_group_rules(self, **query): * ``security_group_id``: ID of security group that owns the rules :returns: A generator of security group rule objects - :rtype: :class:`~openstack.network.v2.security_group_rule. - SecurityGroupRule` + :rtype: + :class:`~openstack.network.v2.security_group_rule.SecurityGroupRule` """ return self._list(_security_group_rule.SecurityGroupRule, **query) @@ -3886,8 +3904,7 @@ def create_service_profile(self, **attrs): """Create a new network service flavor profile from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.network.v2.service_profile - .ServiceProfile`, + a :class:`~openstack.network.v2.service_profile.ServiceProfile`, comprised of the properties on the ServiceProfile class. @@ -3901,8 +3918,8 @@ def delete_service_profile(self, service_profile, ignore_missing=True): :param service_profile: The value can be either the ID of a service profile or a - :class:`~openstack.network.v2.service_profile - .ServiceProfile` instance. + :class:`~openstack.network.v2.service_profile.ServiceProfile` + instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the service profile does not exist. @@ -3925,8 +3942,9 @@ def find_service_profile(self, name_or_id, ignore_missing=True, **args): attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into underlying methods. such as query filters. - :returns: One :class:`~openstack.network.v2.service_profile - .ServiceProfile` or None + :returns: One + :class:`~openstack.network.v2.service_profile.ServiceProfile` + or None """ return self._find(_service_profile.ServiceProfile, name_or_id, ignore_missing=ignore_missing, **args) @@ -3938,8 +3956,8 @@ def get_service_profile(self, service_profile): a :class:`~openstack.network.v2.service_profile.ServiceProfile` instance. - :returns: One :class:`~openstack.network.v2.service_profile - .ServiceProfile` + :returns: One + :class:`~openstack.network.v2.service_profile.ServiceProfile` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ @@ -3965,8 +3983,8 @@ def update_service_profile(self, service_profile, **attrs): """Update a network flavor service profile :param service_profile: Either the id of a service profile or a - :class:`~openstack.network.v2.service_profile - .ServiceProfile` instance. + :class:`~openstack.network.v2.service_profile.ServiceProfile` + instance. :attrs kwargs: The attributes to update on the service profile represented by ``value``. @@ -4191,7 +4209,7 @@ def create_trunk(self, **attrs): """Create a new trunk from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.network.v2.trunk.Trunk, + a :class:`~openstack.network.v2.trunk.Trunk`, comprised of the properties on the Trunk class. :returns: The results of trunk creation @@ -4412,8 +4430,9 @@ def delete_floating_ip_port_forwarding(self, floating_ip, port_forwarding, or a :class:`~openstack.network.v2.floating_ip.FloatingIP` instance. :param port_forwarding: The value can be either the ID of a port - forwarding or a :class:`~openstack.network.v2. - port_forwarding.PortForwarding`instance. + forwarding or a + :class:`~openstack.network.v2.port_forwarding.PortForwarding` + instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the floating ip does not exist. @@ -4432,8 +4451,8 @@ def find_floating_ip_port_forwarding(self, floating_ip, port_forwarding_id, """Find a floating ip port forwarding :param floating_ip: The value can be the ID of the Floating IP that the - port forwarding belongs or a :class:`~openstack. - network.v2.floating_ip.FloatingIP` instance. + port forwarding belongs or a + :class:`~openstack.network.v2.floating_ip.FloatingIP` instance. :param port_forwarding_id: The ID of a port forwarding. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be @@ -4442,8 +4461,9 @@ def find_floating_ip_port_forwarding(self, floating_ip, port_forwarding_id, attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into underlying methods. such as query filters. - :returns: One :class:`~openstack.network.v2.port_forwarding. - PortForwarding` or None + :returns: One + :class:`~openstack.network.v2.port_forwarding.PortForwarding` + or None """ floatingip = self._get_resource(_floating_ip.FloatingIP, floating_ip) return self._find(_port_forwarding.PortForwarding, @@ -4454,13 +4474,14 @@ def get_floating_ip_port_forwarding(self, floating_ip, port_forwarding): """Get a floating ip port forwarding :param floating_ip: The value can be the ID of the Floating IP that the - port forwarding belongs or a :class:`~openstack. - network.v2.floating_ip.FloatingIP` instance. + port forwarding belongs or a + :class:`~openstack.network.v2.floating_ip.FloatingIP` instance. :param port_forwarding: The value can be the ID of a port forwarding - or a :class:`~openstack.network.v2. - port_forwarding.PortForwarding` instance. - :returns: One :class:`~openstack.network.v2.port_forwarding. - PortForwarding` + or a + :class:`~openstack.network.v2.port_forwarding.PortForwarding` + instance. + :returns: One + :class:`~openstack.network.v2.port_forwarding.PortForwarding` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ @@ -4472,13 +4493,14 @@ def floating_ip_port_forwardings(self, floating_ip, **query): """Return a generator of floating ip port forwarding :param floating_ip: The value can be the ID of the Floating IP that the - port forwarding belongs or a :class:`~openstack. - network.v2.floating_ip.FloatingIP` instance. + port forwarding belongs or a + :class:`~openstack.network.v2.floating_ip.FloatingIP` + instance. :param kwargs **query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of floating ip port forwarding objects - :rtype: :class:`~openstack.network.v2.port_forwarding. - PortForwarding` + :rtype: + :class:`~openstack.network.v2.port_forwarding.PortForwarding` """ floatingip = self._get_resource(_floating_ip.FloatingIP, floating_ip) return self._list(_port_forwarding.PortForwarding, @@ -4489,11 +4511,12 @@ def update_floating_ip_port_forwarding(self, floating_ip, port_forwarding, """Update a floating ip port forwarding :param floating_ip: The value can be the ID of the Floating IP that the - port forwarding belongs or a :class:`~openstack. - network.v2.floating_ip.FloatingIP` instance. + port forwarding belongs or a + :class:`~openstack.network.v2.floating_ip.FloatingIP` + instance. :param port_forwarding: Either the id of a floating ip port forwarding - or a :class:`~openstack.network.v2. - port_forwarding.PortForwarding`instance. + or a + :class:`~openstack.network.v2.port_forwarding.PortForwarding`instance. :attrs kwargs: The attributes to update on the floating ip port forwarding represented by ``value``. @@ -4515,7 +4538,7 @@ def create_conntrack_helper(self, router, **attrs): :returns: The results of conntrack helper creation :rtype: - :class: `~openstack.network.v2.l3_conntrack_helper.ConntrackHelper` + :class:`~openstack.network.v2.l3_conntrack_helper.ConntrackHelper` """ router = self._get_resource(_router.Router, router) return self._create(_l3_conntrack_helper.ConntrackHelper, @@ -4530,7 +4553,7 @@ def conntrack_helpers(self, router, **query): the resources being returned. :returns: A generator of conntrack helper objects :rtype: - :class: `~openstack.network.v2.l3_conntrack_helper.ConntrackHelper` + :class:`~openstack.network.v2.l3_conntrack_helper.ConntrackHelper` """ router = self._get_resource(_router.Router, router) return self._list(_l3_conntrack_helper.ConntrackHelper, @@ -4569,7 +4592,7 @@ def update_conntrack_helper(self, conntrack_helper, router, **attrs): :returns: The updated conntrack helper :rtype: - :class: `~openstack.network.v2.l3_conntrack_helper.ConntrackHelper` + :class:`~openstack.network.v2.l3_conntrack_helper.ConntrackHelper` """ router = self._get_resource(_router.Router, router) From a1263076e4d93bb3697b4ed879a51deae10f35ab Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Fri, 3 Dec 2021 10:17:16 +0100 Subject: [PATCH 2970/3836] Load balancer - reindentation of the docstrings Change-Id: Ie98dcd47298389ae52a70e4ab7d623ebcb4a9183 --- openstack/load_balancer/v2/_proxy.py | 391 +++++++++++++-------------- 1 file changed, 190 insertions(+), 201 deletions(-) diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index 1b9ae05dd..be713759d 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -35,10 +35,9 @@ def create_load_balancer(self, **attrs): """Create a new load balancer from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.load_balancer.v2. - load_balancer.LoadBalancer`, - comprised of the properties on the - LoadBalancer class. + a :class:`~openstack.load_balancer.v2.load_balancer.LoadBalancer`, + comprised of the properties on the + LoadBalancer class. :returns: The results of load balancer creation :rtype: :class:`~openstack.load_balancer.v2.load_balancer.LoadBalancer` @@ -49,11 +48,11 @@ def get_load_balancer(self, *attrs): """Get a load balancer :param load_balancer: The value can be the name of a load balancer - or :class:`~openstack.load_balancer.v2.load_balancer.LoadBalancer` - instance. + or :class:`~openstack.load_balancer.v2.load_balancer.LoadBalancer` + instance. :returns: One - :class:`~openstack.load_balancer.v2.load_balancer.LoadBalancer` + :class:`~openstack.load_balancer.v2.load_balancer.LoadBalancer` """ return self._get(_lb.LoadBalancer, *attrs) @@ -62,8 +61,8 @@ def get_load_balancer_statistics(self, name_or_id): :param name_or_id: The name or ID of a load balancer - :returns: One :class:`~openstack.load_balancer.v2.load_balancer. - LoadBalancerStats` + :returns: One + :class:`~openstack.load_balancer.v2.load_balancer.LoadBalancerStats` """ return self._get(_lb.LoadBalancerStats, lb_id=name_or_id, requires_id=False) @@ -119,7 +118,7 @@ def update_load_balancer(self, load_balancer, **attrs): :class:`~openstack.load_balancer.v2.load_balancer.LoadBalancer` instance :param dict attrs: The attributes to update on the load balancer - represented by ``load_balancer``. + represented by ``load_balancer``. :returns: The updated load_balancer :rtype: :class:`~openstack.load_balancer.v2.load_balancer.LoadBalancer` @@ -146,8 +145,8 @@ def create_listener(self, **attrs): """Create a new listener from attributes :param dict attrs: Keyword arguments which will be used to create a - :class:`~openstack.load_balancer.v2.listener.Listener`, - comprised of the properties on the Listener class. + :class:`~openstack.load_balancer.v2.listener.Listener`, + comprised of the properties on the Listener class. :returns: The results of listener creation :rtype: :class:`~openstack.load_balancer.v2.listener.Listener` @@ -158,12 +157,12 @@ def delete_listener(self, listener, ignore_missing=True): """Delete a listener :param listener: The value can be either the ID of a listner or a - :class:`~openstack.load_balancer.v2.listener.Listener` instance. + :class:`~openstack.load_balancer.v2.listener.Listener` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the listner does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent listener. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the listner does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent listener. :returns: ``None`` """ @@ -175,13 +174,13 @@ def find_listener(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a listener. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.load_balancer.v2.listener.Listener` - or None + or None """ return self._find(_listener.Listener, name_or_id, ignore_missing=ignore_missing) @@ -190,12 +189,12 @@ def get_listener(self, listener): """Get a single listener :param listener: The value can be the ID of a listener or a - :class:`~openstack.load_balancer.v2.listener.Listener` - instance. + :class:`~openstack.load_balancer.v2.listener.Listener` + instance. :returns: One :class:`~openstack.load_balancer.v2.listener.Listener` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_listener.Listener, listener) @@ -203,13 +202,13 @@ def get_listener_statistics(self, listener): """Get the listener statistics :param listener: The value can be the ID of a listener or a - :class:`~openstack.load_balancer.v2.listener.Listener` - instance. + :class:`~openstack.load_balancer.v2.listener.Listener` + instance. - :returns: One :class:`~openstack.load_balancer.v2.listener. - ListenerStats` - :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + :returns: One + :class:`~openstack.load_balancer.v2.listener.ListenerStats` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource can be found. """ return self._get(_listener.ListenerStats, listener_id=listener, requires_id=False) @@ -218,7 +217,7 @@ def listeners(self, **query): """Return a generator of listeners :param dict query: Optional query parameters to be sent to limit - the resources being returned. Valid parameters are: + the resources being returned. Valid parameters are: :returns: A generator of listener objects :rtype: :class:`~openstack.load_balancer.v2.listener.Listener` """ @@ -228,10 +227,10 @@ def update_listener(self, listener, **attrs): """Update a listener :param listener: Either the id of a listener or a - :class:`~openstack.load_balancer.v2.listener.Listener` - instance. + :class:`~openstack.load_balancer.v2.listener.Listener` + instance. :param dict attrs: The attributes to update on the listener - represented by ``listener``. + represented by ``listener``. :returns: The updated listener :rtype: :class:`~openstack.load_balancer.v2.listener.Listener` @@ -242,10 +241,8 @@ def create_pool(self, **attrs): """Create a new pool from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.load_balancer.v2. - pool.Pool`, - comprised of the properties on the - Pool class. + a :class:`~openstack.load_balancer.v2.pool.Pool`, comprised of the + properties on the Pool class. :returns: The results of Pool creation :rtype: :class:`~openstack.load_balancer.v2.pool.Pool` @@ -260,7 +257,7 @@ def get_pool(self, *attrs): instance. :returns: One - :class:`~openstack.load_balancer.v2.pool.Pool` + :class:`~openstack.load_balancer.v2.pool.Pool` """ return self._get(_pool.Pool, *attrs) @@ -307,10 +304,10 @@ def update_pool(self, pool, **attrs): """Update a pool :param pool: Either the id of a pool or a - :class:`~openstack.load_balancer.v2.pool.Pool` - instance. + :class:`~openstack.load_balancer.v2.pool.Pool` + instance. :param dict attrs: The attributes to update on the pool - represented by ``pool``. + represented by ``pool``. :returns: The updated pool :rtype: :class:`~openstack.load_balancer.v2.pool.Pool` @@ -321,8 +318,8 @@ def create_member(self, pool, **attrs): """Create a new member from attributes :param pool: The pool can be either the ID of a pool or a - :class:`~openstack.load_balancer.v2.pool.Pool` instance - that the member will be created in. + :class:`~openstack.load_balancer.v2.pool.Pool` instance + that the member will be created in. :param dict attrs: Keyword arguments which will be used to create a :class:`~openstack.load_balancer.v2.member.Member`, comprised of the properties on the Member class. @@ -341,13 +338,13 @@ def delete_member(self, member, pool, ignore_missing=True): The member can be either the ID of a member or a :class:`~openstack.load_balancer.v2.member.Member` instance. :param pool: The pool can be either the ID of a pool or a - :class:`~openstack.load_balancer.v2.pool.Pool` instance - that the member belongs to. + :class:`~openstack.load_balancer.v2.pool.Pool` instance + that the member belongs to. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the member does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent member. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the member does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent member. :returns: ``None`` """ @@ -360,16 +357,16 @@ def find_member(self, name_or_id, pool, ignore_missing=True): :param str name_or_id: The name or ID of a member. :param pool: The pool can be either the ID of a pool or a - :class:`~openstack.load_balancer.v2.pool.Pool` instance - that the member belongs to. + :class:`~openstack.load_balancer.v2.pool.Pool` instance + that the member belongs to. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.load_balancer.v2.member.Member` - or None + or None """ poolobj = self._get_resource(_pool.Pool, pool) return self._find(_member.Member, name_or_id, @@ -379,15 +376,15 @@ def get_member(self, member, pool): """Get a single member :param member: The member can be the ID of a member or a - :class:`~openstack.load_balancer.v2.member.Member` - instance. + :class:`~openstack.load_balancer.v2.member.Member` + instance. :param pool: The pool can be either the ID of a pool or a - :class:`~openstack.load_balancer.v2.pool.Pool` instance - that the member belongs to. + :class:`~openstack.load_balancer.v2.pool.Pool` instance + that the member belongs to. :returns: One :class:`~openstack.load_balancer.v2.member.Member` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ poolobj = self._get_resource(_pool.Pool, pool) return self._get(_member.Member, member, @@ -397,10 +394,10 @@ def members(self, pool, **query): """Return a generator of members :param pool: The pool can be either the ID of a pool or a - :class:`~openstack.load_balancer.v2.pool.Pool` instance - that the member belongs to. + :class:`~openstack.load_balancer.v2.pool.Pool` instance + that the member belongs to. :param dict query: Optional query parameters to be sent to limit - the resources being returned. Valid parameters are: + the resources being returned. Valid parameters are: :returns: A generator of member objects :rtype: :class:`~openstack.load_balancer.v2.member.Member` @@ -412,13 +409,13 @@ def update_member(self, member, pool, **attrs): """Update a member :param member: Either the ID of a member or a - :class:`~openstack.load_balancer.v2.member.Member` - instance. + :class:`~openstack.load_balancer.v2.member.Member` + instance. :param pool: The pool can be either the ID of a pool or a - :class:`~openstack.load_balancer.v2.pool.Pool` instance - that the member belongs to. + :class:`~openstack.load_balancer.v2.pool.Pool` instance + that the member belongs to. :param dict attrs: The attributes to update on the member - represented by ``member``. + represented by ``member``. :returns: The updated member :rtype: :class:`~openstack.load_balancer.v2.member.Member` @@ -442,9 +439,9 @@ def find_health_monitor(self, name_or_id, ignore_missing=True): object matching the given name or id or None if nothing matches. :raises: :class:`openstack.exceptions.DuplicateResource` if more - than one resource is found for this request. + than one resource is found for this request. :raises: :class:`openstack.exceptions.ResourceNotFound` if nothing - is found and ignore_missing is ``False``. + is found and ignore_missing is ``False``. """ return self._find(_hm.HealthMonitor, name_or_id, ignore_missing=ignore_missing) @@ -453,14 +450,12 @@ def create_health_monitor(self, **attrs): """Create a new health monitor from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.load_balancer.v2. - healthmonitor.HealthMonitor`, - comprised of the properties on the - HealthMonitor class. + a :class:`~openstack.load_balancer.v2.healthmonitor.HealthMonitor`, + comprised of the properties on the HealthMonitor class. :returns: The results of HealthMonitor creation - :rtype: :class:`~openstack.load_balancer.v2. - healthmonitor.HealthMonitor` + :rtype: + :class:`~openstack.load_balancer.v2.healthmonitor.HealthMonitor` """ return self._create(_hm.HealthMonitor, **attrs) @@ -473,8 +468,8 @@ def get_health_monitor(self, healthmonitor): instance. :returns: One health monitor - :rtype: :class:`~openstack.load_balancer.v2. - healthmonitor.HealthMonitor` + :rtype: + :class:`~openstack.load_balancer.v2.healthmonitor.HealthMonitor` """ return self._get(_hm.HealthMonitor, healthmonitor) @@ -482,13 +477,13 @@ def health_monitors(self, **query): """Retrieve a generator of health monitors :param dict query: Optional query parameters to be sent to limit - the resources being returned. Valid parameters are: - 'name', 'created_at', 'updated_at', 'delay', - 'expected_codes', 'http_method', 'max_retries', - 'max_retries_down', 'pool_id', - 'provisioning_status', 'operating_status', - 'timeout', 'project_id', 'type', 'url_path', - 'is_admin_state_up'. + the resources being returned. Valid parameters are: + 'name', 'created_at', 'updated_at', 'delay', + 'expected_codes', 'http_method', 'max_retries', + 'max_retries_down', 'pool_id', + 'provisioning_status', 'operating_status', + 'timeout', 'project_id', 'type', 'url_path', + 'is_admin_state_up'. :returns: A generator of health monitor instances """ @@ -520,11 +515,11 @@ def update_health_monitor(self, healthmonitor, **attrs): :class:`~openstack.load_balancer.v2.healthmonitor.HealthMonitor` instance :param dict attrs: The attributes to update on the health monitor - represented by ``healthmonitor``. + represented by ``healthmonitor``. :returns: The updated health monitor - :rtype: :class:`~openstack.load_balancer.v2. - healthmonitor.HealthMonitor` + :rtype: + :class:`~openstack.load_balancer.v2.healthmonitor.HealthMonitor` """ return self._update(_hm.HealthMonitor, healthmonitor, **attrs) @@ -533,8 +528,8 @@ def create_l7_policy(self, **attrs): """Create a new l7policy from attributes :param dict attrs: Keyword arguments which will be used to create a - :class:`~openstack.load_balancer.v2.l7_policy.L7Policy`, - comprised of the properties on the L7Policy class. + :class:`~openstack.load_balancer.v2.l7_policy.L7Policy`, + comprised of the properties on the L7Policy class. :returns: The results of l7policy creation :rtype: :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` @@ -545,12 +540,12 @@ def delete_l7_policy(self, l7_policy, ignore_missing=True): """Delete a l7policy :param l7_policy: The value can be either the ID of a l7policy or a - :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` instance. + :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the l7policy does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent l7policy. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the l7policy does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent l7policy. :returns: ``None`` """ @@ -562,13 +557,13 @@ def find_l7_policy(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a l7policy. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` - or None + or None """ return self._find(_l7policy.L7Policy, name_or_id, ignore_missing=ignore_missing) @@ -577,12 +572,12 @@ def get_l7_policy(self, l7_policy): """Get a single l7policy :param l7_policy: The value can be the ID of a l7policy or a - :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` - instance. + :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` + instance. :returns: One :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_l7policy.L7Policy, l7_policy) @@ -590,7 +585,7 @@ def l7_policies(self, **query): """Return a generator of l7policies :param dict query: Optional query parameters to be sent to limit - the resources being returned. Valid parameters are: + the resources being returned. Valid parameters are: :returns: A generator of l7policy objects :rtype: :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` @@ -601,10 +596,10 @@ def update_l7_policy(self, l7_policy, **attrs): """Update a l7policy :param l7_policy: Either the id of a l7policy or a - :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` - instance. + :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` + instance. :param dict attrs: The attributes to update on the l7policy - represented by ``l7policy``. + represented by ``l7policy``. :returns: The updated l7policy :rtype: :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` @@ -615,8 +610,8 @@ def create_l7_rule(self, l7_policy, **attrs): """Create a new l7rule from attributes :param l7_policy: The l7_policy can be either the ID of a l7policy or - :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` - instance that the l7rule will be created in. + :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` + instance that the l7rule will be created in. :param dict attrs: Keyword arguments which will be used to create a :class:`~openstack.load_balancer.v2.l7_rule.L7Rule`, comprised of the properties on the L7Rule class. @@ -635,13 +630,13 @@ def delete_l7_rule(self, l7rule, l7_policy, ignore_missing=True): The l7rule can be either the ID of a l7rule or a :class:`~openstack.load_balancer.v2.l7_rule.L7Rule` instance. :param l7_policy: The l7_policy can be either the ID of a l7policy or - :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` - instance that the l7rule belongs to. + :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` + instance that the l7rule belongs to. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the l7rule does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent l7rule. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the l7rule does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent l7rule. :returns: ``None`` """ @@ -654,16 +649,16 @@ def find_l7_rule(self, name_or_id, l7_policy, ignore_missing=True): :param str name_or_id: The name or ID of a l7rule. :param l7_policy: The l7_policy can be either the ID of a l7policy or - :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` - instance that the l7rule belongs to. + :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` + instance that the l7rule belongs to. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :returns: One :class:`~openstack.load_balancer.v2.l7_rule.L7Rule` - or None + or None """ l7policyobj = self._get_resource(_l7policy.L7Policy, l7_policy) return self._find(_l7rule.L7Rule, name_or_id, @@ -674,15 +669,15 @@ def get_l7_rule(self, l7rule, l7_policy): """Get a single l7rule :param l7rule: The l7rule can be the ID of a l7rule or a - :class:`~openstack.load_balancer.v2.l7_rule.L7Rule` - instance. + :class:`~openstack.load_balancer.v2.l7_rule.L7Rule` + instance. :param l7_policy: The l7_policy can be either the ID of a l7policy or - :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` - instance that the l7rule belongs to. + :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` + instance that the l7rule belongs to. :returns: One :class:`~openstack.load_balancer.v2.l7_rule.L7Rule` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ l7policyobj = self._get_resource(_l7policy.L7Policy, l7_policy) return self._get(_l7rule.L7Rule, l7rule, @@ -692,10 +687,10 @@ def l7_rules(self, l7_policy, **query): """Return a generator of l7rules :param l7_policy: The l7_policy can be either the ID of a l7_policy or - :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` - instance that the l7rule belongs to. + :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` + instance that the l7rule belongs to. :param dict query: Optional query parameters to be sent to limit - the resources being returned. Valid parameters are: + the resources being returned. Valid parameters are: :returns: A generator of l7rule objects :rtype: :class:`~openstack.load_balancer.v2.l7_rule.L7Rule` @@ -707,13 +702,13 @@ def update_l7_rule(self, l7rule, l7_policy, **attrs): """Update a l7rule :param l7rule: Either the ID of a l7rule or a - :class:`~openstack.load_balancer.v2.l7_rule.L7Rule` - instance. + :class:`~openstack.load_balancer.v2.l7_rule.L7Rule` + instance. :param l7_policy: The l7_policy can be either the ID of a l7policy or - :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` - instance that the l7rule belongs to. + :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` + instance that the l7rule belongs to. :param dict attrs: The attributes to update on the l7rule - represented by ``l7rule``. + represented by ``l7rule``. :returns: The updated l7rule :rtype: :class:`~openstack.load_balancer.v2.l7_rule.L7Rule` @@ -726,8 +721,8 @@ def quotas(self, **query): """Return a generator of quotas :param dict query: Optional query parameters to be sent to limit - the resources being returned. Currently no query - parameter is supported. + the resources being returned. Currently no query + parameter is supported. :returns: A generator of quota objects :rtype: :class:`~openstack.load_balancer.v2.quota.Quota` @@ -738,13 +733,13 @@ def get_quota(self, quota): """Get a quota :param quota: The value can be the ID of a quota or a - :class:`~openstack.load_balancer.v2.quota.Quota` - instance. The ID of a quota is the same as the project - ID for the quota. + :class:`~openstack.load_balancer.v2.quota.Quota` + instance. The ID of a quota is the same as the project + ID for the quota. :returns: One :class:`~openstack.load_balancer.v2.quota.Quota` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._get(_quota.Quota, quota) @@ -752,11 +747,11 @@ def update_quota(self, quota, **attrs): """Update a quota :param quota: Either the ID of a quota or a - :class:`~openstack.load_balancer.v2.quota.Quota` - instance. The ID of a quota is the same as the - project ID for the quota. + :class:`~openstack.load_balancer.v2.quota.Quota` + instance. The ID of a quota is the same as the + project ID for the quota. :param dict attrs: The attributes to update on the quota represented - by ``quota``. + by ``quota``. :returns: The updated quota :rtype: :class:`~openstack.load_balancer.v2.quota.Quota` @@ -774,14 +769,14 @@ def delete_quota(self, quota, ignore_missing=True): """Delete a quota (i.e. reset to the default quota) :param quota: The value can be either the ID of a quota or a - :class:`~openstack.load_balancer.v2.quota.Quota` - instance. The ID of a quota is the same as the - project ID for the quota. + :class:`~openstack.load_balancer.v2.quota.Quota` + instance. The ID of a quota is the same as the + project ID for the quota. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when quota does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent quota. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when quota does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent quota. :returns: ``None`` """ @@ -805,27 +800,25 @@ def provider_flavor_capabilities(self, provider, **query): def create_flavor_profile(self, **attrs): """Create a new flavor profile from attributes - :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.load_balancer.v2. - flavor_profile.FlavorProfile`, - comprised of the properties on the - FlavorProfile class. + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.load_balancer.v2.flavor_profile.FlavorProfile`, + comprised of the properties on the FlavorProfile class. :returns: The results of profile creation creation - :rtype: :class:`~openstack.load_balancer.v2.flavor_profile. - FlavorProfile` + :rtype: + :class:`~openstack.load_balancer.v2.flavor_profile.FlavorProfile` """ return self._create(_flavor_profile.FlavorProfile, **attrs) def get_flavor_profile(self, *attrs): """Get a flavor profile - :param flavor_profile: The value can be the name of a flavor profile - or :class:`~openstack.load_balancer.v2.flavor_profile. - FlavorProfile` instance. + :param flavor_profile: The value can be the name of a flavor profile or + :class:`~openstack.load_balancer.v2.flavor_profile.FlavorProfile` + instance. :returns: One - :class:`~openstack.load_balancer.v2.flavor_profile.FlavorProfile` + :class:`~openstack.load_balancer.v2.flavor_profile.FlavorProfile` """ return self._get(_flavor_profile.FlavorProfile, *attrs) @@ -875,11 +868,11 @@ def update_flavor_profile(self, flavor_profile, **attrs): :class:`~openstack.load_balancer.v2.flavor_profile.FlavorProfile` instance :param dict attrs: The attributes to update on the flavor profile - represented by ``flavor_profile``. + represented by ``flavor_profile``. :returns: The updated flavor profile - :rtype: :class:`~openstack.load_balancer.v2.flavor_profile. - FlavorProfile` + :rtype: + :class:`~openstack.load_balancer.v2.flavor_profile.FlavorProfile` """ return self._update(_flavor_profile.FlavorProfile, flavor_profile, **attrs) @@ -888,9 +881,8 @@ def create_flavor(self, **attrs): """Create a new flavor from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.load_balancer.v2. - flavor.Flavor`, comprised of the properties on the - Flavorclass. + a :class:`~openstack.load_balancer.v2.flavor.Flavor`, + comprised of the properties on the Flavorclass. :returns: The results of flavor creation creation :rtype: :class:`~openstack.load_balancer.v2.flavor.Flavor` @@ -901,10 +893,10 @@ def get_flavor(self, *attrs): """Get a flavor :param flavor: The value can be the name of a flavor - or :class:`~openstack.load_balancer.v2.flavor.Flavor` instance. + or :class:`~openstack.load_balancer.v2.flavor.Flavor` instance. :returns: One - :class:`~openstack.load_balancer.v2.flavor.Flavor` + :class:`~openstack.load_balancer.v2.flavor.Flavor` """ return self._get(_flavor.Flavor, *attrs) @@ -951,7 +943,7 @@ def update_flavor(self, flavor, **attrs): :param flavor: The flavor can be either the name or a :class:`~openstack.load_balancer.v2.flavor.Flavor` instance :param dict attrs: The attributes to update on the flavor - represented by ``flavor``. + represented by ``flavor``. :returns: The updated flavor :rtype: :class:`~openstack.load_balancer.v2.flavor.Flavor` @@ -969,10 +961,10 @@ def get_amphora(self, *attrs): """Get a amphora :param amphora: The value can be the ID of an amphora - or :class:`~openstack.load_balancer.v2.amphora.Amphora` instance. + or :class:`~openstack.load_balancer.v2.amphora.Amphora` instance. :returns: One - :class:`~openstack.load_balancer.v2.amphora.Amphora` + :class:`~openstack.load_balancer.v2.amphora.Amphora` """ return self._get(_amphora.Amphora, *attrs) @@ -1012,15 +1004,14 @@ def failover_amphora(self, amphora_id, **attrs): def create_availability_zone_profile(self, **attrs): """Create a new availability zone profile from attributes - :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.load_balancer.v2. - availability_zone_profile.AvailabilityZoneProfile`, - comprised of the properties on the - AvailabilityZoneProfile class. + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.load_balancer.v2.availability_zone_profile.AvailabilityZoneProfile` + comprised of the properties on the AvailabilityZoneProfile + class. :returns: The results of profile creation creation - :rtype: :class:`~openstack.load_balancer.v2.availability_zone_profile. - AvailabilityZoneProfile` + :rtype: + :class:`~openstack.load_balancer.v2.availability_zone_profile.AvailabilityZoneProfile` """ return self._create(_availability_zone_profile.AvailabilityZoneProfile, **attrs) @@ -1029,12 +1020,12 @@ def get_availability_zone_profile(self, *attrs): """Get an availability zone profile :param availability_zone_profile: The value can be the name of an - availability_zone profile - or :class:`~openstack.load_balancer.v2.availability_zone_profile. - AvailabilityZoneProfile` instance. + availability_zone profile or + :class:`~openstack.load_balancer.v2.availability_zone_profile.AvailabilityZoneProfile` + instance. :returns: One - :class:`~openstack.load_balancer.v2.availability_zone_profile.AvailabilityZoneProfile` + :class:`~openstack.load_balancer.v2.availability_zone_profile.AvailabilityZoneProfile` """ return self._get(_availability_zone_profile.AvailabilityZoneProfile, *attrs) @@ -1090,12 +1081,11 @@ def update_availability_zone_profile(self, availability_zone_profile, :class:`~openstack.load_balancer.v2.availability_zone_profile.AvailabilityZoneProfile` instance :param dict attrs: The attributes to update on the availability_zone - profile represented by - ``availability_zone_profile``. + profile represented by ``availability_zone_profile``. :returns: The updated availability zone profile - :rtype: :class:`~openstack.load_balancer.v2.availability_zone_profile. - AvailabilityZoneProfile` + :rtype: + :class:`~openstack.load_balancer.v2.availability_zone_profile.AvailabilityZoneProfile` """ return self._update(_availability_zone_profile.AvailabilityZoneProfile, availability_zone_profile, **attrs) @@ -1103,10 +1093,9 @@ def update_availability_zone_profile(self, availability_zone_profile, def create_availability_zone(self, **attrs): """Create a new availability zone from attributes - :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.load_balancer.v2. - availability_zone.AvailabilityZone`, comprised of - the properties on the AvailabilityZoneclass. + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.load_balancer.v2.availability_zone.AvailabilityZone` + comprised of the properties on the AvailabilityZoneclass. :returns: The results of availability_zone creation creation :rtype: @@ -1118,12 +1107,12 @@ def get_availability_zone(self, *attrs): """Get an availability zone :param availability_zone: The value can be the name of a - availability_zone or - :class:`~openstack.load_balancer.v2.availability_zone.AvailabilityZone` - instance. + availability_zone or + :class:`~openstack.load_balancer.v2.availability_zone.AvailabilityZone` + instance. :returns: One - :class:`~openstack.load_balancer.v2.availability_zone.AvailabilityZone` + :class:`~openstack.load_balancer.v2.availability_zone.AvailabilityZone` """ return self._get(_availability_zone.AvailabilityZone, *attrs) @@ -1175,7 +1164,7 @@ def update_availability_zone(self, availability_zone, **attrs): :class:`~openstack.load_balancer.v2.availability_zone.AvailabilityZone` instance :param dict attrs: The attributes to update on the availability_zone - represented by ``availability_zone``. + represented by ``availability_zone``. :returns: The updated availability_zone :rtype: From da71735123eb76a831c2d4e5ff76a3df207910ba Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Tue, 14 Dec 2021 09:48:20 +0100 Subject: [PATCH 2971/3836] Instance HA service - reunite :class: links on single lines Change-Id: I3ac71f0eed294e826d46c6358de9e011afb35400 --- openstack/instance_ha/v1/_proxy.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/openstack/instance_ha/v1/_proxy.py b/openstack/instance_ha/v1/_proxy.py index 518c2217e..204b22c5a 100644 --- a/openstack/instance_ha/v1/_proxy.py +++ b/openstack/instance_ha/v1/_proxy.py @@ -39,7 +39,7 @@ def get_notification(self, notification): """Get a single notification. :param notification: The value can be the ID of a notification or a - :class: `~masakariclient.sdk.ha.v1.notification.Notification` + :class:`~masakariclient.sdk.ha.v1.notification.Notification` instance. :returns: One :class:`~masakariclient.sdk.ha.v1.notification.Notification` @@ -55,7 +55,7 @@ def create_notification(self, **attrs): a :class:`masakariclient.sdk.ha.v1.notification.Notification`, comprised of the propoerties on the Notification class. :returns: The result of notification creation - :rtype: :class: `masakariclient.sdk.ha.v1.notification.Notification` + :rtype: :class:`masakariclient.sdk.ha.v1.notification.Notification` """ return self._create(_notification.Notification, **attrs) @@ -72,7 +72,7 @@ def get_segment(self, segment): """Get a single segment. :param segment: The value can be the ID of a segment or a - :class: `~masakariclient.sdk.ha.v1.segment.Segment` instance. + :class:`~masakariclient.sdk.ha.v1.segment.Segment` instance. :returns: One :class:`~masakariclient.sdk.ha.v1.segment.Segment` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. @@ -83,10 +83,10 @@ def create_segment(self, **attrs): """Create a new segment. :param dict attrs: Keyword arguments which will be used to create - a :class: `masakariclient.sdk.ha.v1.segment.Segment`, + a :class:`masakariclient.sdk.ha.v1.segment.Segment`, comprised of the propoerties on the Segment class. :returns: The result of segment creation - :rtype: :class: `masakariclient.sdk.ha.v1.segment.Segment` + :rtype: :class:`masakariclient.sdk.ha.v1.segment.Segment` """ return self._create(_segment.Segment, **attrs) @@ -94,12 +94,12 @@ def update_segment(self, segment, **attrs): """Update a segment. :param segment: The value can be the ID of a segment or a - :class: `~masakariclient.sdk.ha.v1.segment.Segment` instance. + :class:`~masakariclient.sdk.ha.v1.segment.Segment` instance. :param dict attrs: Keyword arguments which will be used to update - a :class: `masakariclient.sdk.ha.v1.segment.Segment`, + a :class:`masakariclient.sdk.ha.v1.segment.Segment`, comprised of the propoerties on the Segment class. :returns: The updated segment. - :rtype: :class: `masakariclient.sdk.ha.v1.segment.Segment` + :rtype: :class:`masakariclient.sdk.ha.v1.segment.Segment` """ return self._update(_segment.Segment, segment, **attrs) @@ -135,7 +135,7 @@ def create_host(self, segment_id, **attrs): :param segment_id: The ID of a failover segment. :param dict attrs: Keyword arguments which will be used to create - a :class: `masakariclient.sdk.ha.v1.host.Host`, + a :class:`masakariclient.sdk.ha.v1.host.Host`, comprised of the propoerties on the Host class. :returns: The results of host creation From 7c003e6a37248f59833b944557f8eef3b210c25b Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Tue, 14 Dec 2021 09:03:20 +0100 Subject: [PATCH 2972/3836] Shared file system - reunite :class: links on single lines Change-Id: I29c879c4a92916fa72d40958e2eacdfcea8533f6 --- openstack/shared_file_system/v2/_proxy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index 7c4c5e783..54c6766cb 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -214,8 +214,8 @@ def delete_user_message(self, message_id, ignore_missing=True): :param message_id: The ID of the user message :returns: Result of the "delete" on the user message - :rtype: :class:`~openstack.shared_file_system.v2. - user_message.UserMessage` + :rtype: + :class:`~openstack.shared_file_system.v2.user_message.UserMessage` """ return self._delete( _user_message.UserMessage, message_id, From 104a361a5f42a1612796df8694fa2232f8fd7c5e Mon Sep 17 00:00:00 2001 From: cheng-jiab Date: Sun, 28 Nov 2021 22:24:09 -0500 Subject: [PATCH 2973/3836] Add revert share to snapshot to shared file system. Introduce revert share to snapshot method for Share class to shared file systems service. Change-Id: Id30cf928d09d5a06524616a9d6969306dc475740 --- .../user/proxies/shared_file_system.rst | 3 ++- openstack/shared_file_system/v2/_proxy.py | 12 +++++++++++ openstack/shared_file_system/v2/share.py | 11 ++++++++++ .../functional/shared_file_system/base.py | 20 +++++++++++++++++++ .../shared_file_system/test_share.py | 17 ++++++++++++++++ ...d-file-system-shares-2e1d44a1bb882d6d.yaml | 5 +++++ 6 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-shared-file-system-shares-2e1d44a1bb882d6d.yaml diff --git a/doc/source/user/proxies/shared_file_system.rst b/doc/source/user/proxies/shared_file_system.rst index 0ea5925d5..6c8b6cb5e 100644 --- a/doc/source/user/proxies/shared_file_system.rst +++ b/doc/source/user/proxies/shared_file_system.rst @@ -32,7 +32,8 @@ service. .. autoclass:: openstack.shared_file_system.v2._proxy.Proxy :noindex: - :members: shares, get_share, delete_share, update_share, create_share + :members: shares, get_share, delete_share, update_share, create_share, + revert_share_to_snapshot Shared File System Storage Pools diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index 4d34fd293..e87274569 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -132,6 +132,18 @@ def create_share(self, **attrs): """ return self._create(_share.Share, **attrs) + def revert_share_to_snapshot(self, share_id, snapshot_id): + """Reverts a share to the specified snapshot, which must be + the most recent one known to manila. + + :param share_id: The ID of the share to revert + :param snapshot_id: The ID of the snapshot to revert to + :returns: Result of the ``revert`` + :rtype: ``None`` + """ + res = self._get(_share.Share, share_id) + res.revert_to_snapshot(self, snapshot_id) + def wait_for_status(self, res, status='active', failures=None, interval=2, wait=120): """Wait for a resource to be in a particular status. diff --git a/openstack/shared_file_system/v2/share.py b/openstack/shared_file_system/v2/share.py index d2bd95d48..75cc636e2 100644 --- a/openstack/shared_file_system/v2/share.py +++ b/openstack/shared_file_system/v2/share.py @@ -11,6 +11,7 @@ # under the License. from openstack import resource +from openstack import utils class Share(resource.Resource): @@ -100,3 +101,13 @@ class Share(resource.Resource): display_name = resource.Body("display_name", type=str) #: Display description for updating description display_description = resource.Body("display_description", type=str) + + def _action(self, session, body): + url = utils.urljoin(self.base_path, self.id, 'action') + headers = {'Accept': ''} + session.post( + url, json=body, headers=headers) + + def revert_to_snapshot(self, session, snapshot_id): + body = {"revert": {"snapshot_id": snapshot_id}} + self._action(session, body) diff --git a/openstack/tests/functional/shared_file_system/base.py b/openstack/tests/functional/shared_file_system/base.py index 9a78b2edb..2dac91937 100644 --- a/openstack/tests/functional/shared_file_system/base.py +++ b/openstack/tests/functional/shared_file_system/base.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import resource from openstack.tests.functional import base @@ -37,3 +38,22 @@ def create_share(self, **kwargs): wait=self._wait_for_timeout) self.assertIsNotNone(share.id) return share + + def create_share_snapshot(self, share_id, **kwargs): + share_snapshot = self.user_cloud.share.create_share_snapshot( + share_id=share_id, force=True) + self.addCleanup(resource.wait_for_delete, + self.user_cloud.share, share_snapshot, + wait=self._wait_for_timeout, + interval=2) + self.addCleanup(self.user_cloud.share.delete_share_snapshot, + share_snapshot.id, + ignore_missing=False) + self.user_cloud.share.wait_for_status( + share_snapshot, + status='available', + failures=['error'], + interval=5, + wait=self._wait_for_timeout) + self.assertIsNotNone(share_snapshot.id) + return share_snapshot diff --git a/openstack/tests/functional/shared_file_system/test_share.py b/openstack/tests/functional/shared_file_system/test_share.py index 4a4951ac7..a134140cd 100644 --- a/openstack/tests/functional/shared_file_system/test_share.py +++ b/openstack/tests/functional/shared_file_system/test_share.py @@ -24,6 +24,10 @@ def setUp(self): name=self.SHARE_NAME, size=2, share_type="dhss_false", share_protocol='NFS', description=None) self.SHARE_ID = my_share.id + my_share_snapshot = self.create_share_snapshot( + share_id=self.SHARE_ID + ) + self.SHARE_SNAPSHOT_ID = my_share_snapshot.id def test_get(self): sot = self.user_cloud.share.get_share(self.SHARE_ID) @@ -43,3 +47,16 @@ def test_update(self): get_updated_share = self.user_cloud.share.get_share( updated_share.id) self.assertEqual('updated share', get_updated_share.description) + + def test_revert_share_to_snapshot(self): + self.user_cloud.share.revert_share_to_snapshot( + self.SHARE_ID, self.SHARE_SNAPSHOT_ID) + get_reverted_share = self.user_cloud.share.get_share( + self.SHARE_ID) + self.user_cloud.share.wait_for_status( + get_reverted_share, + status='available', + failures=['error'], + interval=5, + wait=self._wait_for_timeout) + self.assertIsNotNone(get_reverted_share.id) diff --git a/releasenotes/notes/add-shared-file-system-shares-2e1d44a1bb882d6d.yaml b/releasenotes/notes/add-shared-file-system-shares-2e1d44a1bb882d6d.yaml new file mode 100644 index 000000000..6946d2313 --- /dev/null +++ b/releasenotes/notes/add-shared-file-system-shares-2e1d44a1bb882d6d.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added revert share to snapshot to shared + file system service. From e94fda9e2c3027273bcb579531f0bee32938f9ae Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 22 Oct 2021 17:24:02 +0200 Subject: [PATCH 2974/3836] Switch cloud.compute.get_server_by_id to use proxy Consume proxy layer in cloud.ompute.get_server_by_id Change-Id: Ie6d1776d3bc641e4fbab7c73ca05017cd429d12f --- openstack/cloud/_compute.py | 11 +- openstack/cloud/meta.py | 16 +- openstack/compute/v2/server.py | 24 +- .../tests/functional/cloud/test_compute.py | 12 +- .../tests/unit/cloud/test_create_server.py | 44 ++-- .../unit/cloud/test_floating_ip_common.py | 14 +- openstack/tests/unit/cloud/test_meta.py | 19 +- openstack/tests/unit/cloud/test_normalize.py | 227 ------------------ 8 files changed, 96 insertions(+), 271 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 0c5792584..d153305a9 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -513,14 +513,11 @@ def get_server_by_id(self, id): :param id: ID of the server. - :returns: A server dict or None if no matching server is found. + :returns: A server object or None if no matching server is found. """ try: - data = proxy._json_response( - self.compute.get('/servers/{id}'.format(id=id))) - server = self._get_and_munchify('server', data) - return meta.add_server_interfaces( - self, self._normalize_server(server)) + server = self.compute.get_server(id) + return meta.add_server_interfaces(self, server) except exceptions.ResourceNotFound: return None @@ -905,7 +902,7 @@ def create_server( nat_destination=nat_destination, ) - server.adminPass = admin_pass + server.admin_password = admin_pass return server def _get_boot_from_volume_kwargs( diff --git a/openstack/cloud/meta.py b/openstack/cloud/meta.py index a70d9e2d7..19a9b3bca 100644 --- a/openstack/cloud/meta.py +++ b/openstack/cloud/meta.py @@ -462,11 +462,11 @@ def add_server_interfaces(cloud, server): # server record. Since we know them, go ahead and set them. In the case # where they were set previous, we use the values, so this will not break # clouds that provide the information - if cloud.private and server['private_v4']: - server['accessIPv4'] = server['private_v4'] + if cloud.private and server.private_v4: + server.access_ipv4 = server.private_v4 else: - server['accessIPv4'] = server['public_v4'] - server['accessIPv6'] = server['public_v6'] + server.access_ipv4 = server.public_v4 + server.access_ipv6 = server.public_v6 return server @@ -487,7 +487,8 @@ def get_hostvars_from_server(cloud, server, mounts=None): expand_server_vars if caching is not set up. If caching is set up, the extra cost should be minimal. """ - server_vars = add_server_interfaces(cloud, server) + server_vars = obj_to_munch( + add_server_interfaces(cloud, server)) flavor_id = server['flavor'].get('id') if flavor_id: @@ -514,6 +515,11 @@ def get_hostvars_from_server(cloud, server, mounts=None): if image_name: server_vars['image']['name'] = image_name + # During the switch to returning sdk resource objects we need temporarily + # to force convertion to dict. This will be dropped soon. + if hasattr(server_vars['image'], 'to_dict'): + server_vars['image'] = server_vars['image'].to_dict(computed=False) + volumes = [] if cloud.has_service('volume'): try: diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index a573b062b..4a17accff 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -11,6 +11,7 @@ # under the License. from openstack.common import metadata from openstack.common import tag +from openstack.compute.v2 import volume_attachment from openstack import exceptions from openstack.image.v2 import image from openstack import resource @@ -82,7 +83,11 @@ class Server(resource.Resource, metadata.MetadataMixin, tag.TagMixin): #: A list of an attached volumes. Each item in the list contains at least #: an "id" key to identify the specific volumes. attached_volumes = resource.Body( - 'os-extended-volumes:volumes_attached') + 'os-extended-volumes:volumes_attached', + aka='volumes', + type=list, + list_type=volume_attachment.VolumeAttachment, + default=[]) #: The name of the availability zone this server is a part of. availability_zone = resource.Body('OS-EXT-AZ:availability_zone') #: Enables fine grained control of the block device mapping for an @@ -128,6 +133,12 @@ class Server(resource.Resource, metadata.MetadataMixin, tag.TagMixin): #: instance name template. Appears in the response for administrative users #: only. instance_name = resource.Body('OS-EXT-SRV-ATTR:instance_name') + #: The address to use to connect to this server from the current calling + #: context. This will be set to public_ipv6 if the calling host has + #: routable ipv6 addresses, and to private_ipv4 if the Connection was + #: created with private=True. Otherwise it will be set to public_ipv4. + interface_ip = resource.Computed('interface_ip', default='') + # The locked status of the server is_locked = resource.Body('locked', type=bool) #: The UUID of the kernel image when using an AMI. Will be null if not. @@ -157,6 +168,17 @@ class Server(resource.Resource, metadata.MetadataMixin, tag.TagMixin): progress = resource.Body('progress', type=int) #: The ID of the project this server is associated with. project_id = resource.Body('tenant_id') + + #: The private IPv4 address of this server + private_v4 = resource.Computed('private_v4', default='') + #: The private IPv6 address of this server + private_v6 = resource.Computed('private_v6', default='') + + #: The public IPv4 address of this server + public_v4 = resource.Computed('public_v4', default='') + #: The public IPv6 address of this server + public_v6 = resource.Computed('public_v6', default='') + #: The UUID of the ramdisk image when using an AMI. Will be null if not. #: By default, it appears in the response for administrative users only. ramdisk_id = resource.Body('OS-EXT-SRV-ATTR:ramdisk_id') diff --git a/openstack/tests/functional/cloud/test_compute.py b/openstack/tests/functional/cloud/test_compute.py index e7408fc3f..b41077685 100644 --- a/openstack/tests/functional/cloud/test_compute.py +++ b/openstack/tests/functional/cloud/test_compute.py @@ -76,7 +76,7 @@ def test_create_and_delete_server(self): wait=True) self.assertEqual(self.server_name, server['name']) self.assertEqual(self.image.id, server['image']['id']) - self.assertEqual(self.flavor.id, server['flavor']['id']) + self.assertEqual(self.flavor.name, server['flavor']['original_name']) self.assertIsNotNone(server['adminPass']) self.assertTrue( self.user_cloud.delete_server(self.server_name, wait=True)) @@ -92,7 +92,7 @@ def test_create_and_delete_server_auto_ip_delete_ips(self): wait=True) self.assertEqual(self.server_name, server['name']) self.assertEqual(self.image.id, server['image']['id']) - self.assertEqual(self.flavor.id, server['flavor']['id']) + self.assertEqual(self.flavor.name, server['flavor']['original_name']) self.assertIsNotNone(server['adminPass']) self.assertTrue( self.user_cloud.delete_server( @@ -123,7 +123,7 @@ def test_create_and_delete_server_with_config_drive(self): wait=True) self.assertEqual(self.server_name, server['name']) self.assertEqual(self.image.id, server['image']['id']) - self.assertEqual(self.flavor.id, server['flavor']['id']) + self.assertEqual(self.flavor.name, server['flavor']['original_name']) self.assertTrue(server['has_config_drive']) self.assertIsNotNone(server['adminPass']) self.assertTrue( @@ -143,7 +143,7 @@ def test_create_and_delete_server_with_config_drive_none(self): wait=True) self.assertEqual(self.server_name, server['name']) self.assertEqual(self.image.id, server['image']['id']) - self.assertEqual(self.flavor.id, server['flavor']['id']) + self.assertEqual(self.flavor.name, server['flavor']['original_name']) self.assertFalse(server['has_config_drive']) self.assertIsNotNone(server['adminPass']) self.assertTrue( @@ -182,7 +182,7 @@ def test_create_server_image_flavor_dict(self): wait=True) self.assertEqual(self.server_name, server['name']) self.assertEqual(self.image.id, server['image']['id']) - self.assertEqual(self.flavor.id, server['flavor']['id']) + self.assertEqual(self.flavor.name, server['flavor']['original_name']) self.assertIsNotNone(server['adminPass']) self.assertTrue( self.user_cloud.delete_server(self.server_name, wait=True)) @@ -231,7 +231,7 @@ def test_create_and_delete_server_with_admin_pass(self): wait=True) self.assertEqual(self.server_name, server['name']) self.assertEqual(self.image.id, server['image']['id']) - self.assertEqual(self.flavor.id, server['flavor']['id']) + self.assertEqual(self.flavor.name, server['flavor']['original_name']) self.assertEqual(server['adminPass'], 'sheiqu9loegahSh') self.assertTrue( self.user_cloud.delete_server(self.server_name, wait=True)) diff --git a/openstack/tests/unit/cloud/test_create_server.py b/openstack/tests/unit/cloud/test_create_server.py index f2987f865..d911b0f3d 100644 --- a/openstack/tests/unit/cloud/test_create_server.py +++ b/openstack/tests/unit/cloud/test_create_server.py @@ -23,6 +23,7 @@ from openstack.cloud import exc from openstack.cloud import meta +from openstack.compute.v2 import server from openstack import connection from openstack.tests import fakes from openstack.tests.unit import base @@ -52,6 +53,7 @@ def test_create_server_with_get_exception(self): u'max_count': 1, u'min_count': 1, u'name': u'server-name'}})), + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), @@ -85,6 +87,7 @@ def test_create_server_with_server_error(self): u'max_count': 1, u'min_count': 1, u'name': u'server-name'}})), + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), @@ -195,19 +198,19 @@ def test_create_server_no_wait(self): u'max_count': 1, u'min_count': 1, u'name': u'server-name'}})), + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), json={'server': fake_server}), ]) - normalized = self.cloud._expand_server( - self.cloud._normalize_server(fake_server), False, False) - self.assertEqual( - normalized, + self.assertDictEqual( + server.Server(**fake_server).to_dict(computed=False), self.cloud.create_server( name='server-name', image=dict(id='image-id'), - flavor=dict(id='flavor-id'))) + flavor=dict(id='flavor-id')).to_dict(computed=False) + ) self.assert_calls() @@ -233,20 +236,19 @@ def test_create_server_config_drive(self): u'max_count': 1, u'min_count': 1, u'name': u'server-name'}})), + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), json={'server': fake_server}), ]) - normalized = self.cloud._expand_server( - self.cloud._normalize_server(fake_server), False, False) - self.assertEqual( - normalized, + self.assertDictEqual( + server.Server(**fake_server).to_dict(computed=False), self.cloud.create_server( name='server-name', image=dict(id='image-id'), flavor=dict(id='flavor-id'), - config_drive=True)) + config_drive=True).to_dict(computed=False)) self.assert_calls() @@ -271,20 +273,20 @@ def test_create_server_config_drive_none(self): u'max_count': 1, u'min_count': 1, u'name': u'server-name'}})), + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), json={'server': fake_server}), ]) - normalized = self.cloud._expand_server( - self.cloud._normalize_server(fake_server), False, False) self.assertEqual( - normalized, + server.Server(**fake_server).to_dict(computed=False), self.cloud.create_server( name='server-name', image=dict(id='image-id'), flavor=dict(id='flavor-id'), - config_drive=None)) + config_drive=None).to_dict(computed=False) + ) self.assert_calls() @@ -313,6 +315,7 @@ def test_create_server_with_admin_pass_no_wait(self): u'max_count': 1, u'min_count': 1, u'name': u'server-name'}})), + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), @@ -323,7 +326,7 @@ def test_create_server_with_admin_pass_no_wait(self): self.cloud.create_server( name='server-name', image=dict(id='image-id'), flavor=dict(id='flavor-id'), - admin_pass=admin_pass)['adminPass']) + admin_pass=admin_pass)['admin_password']) self.assert_calls() @@ -368,7 +371,7 @@ def test_create_server_with_admin_pass_wait(self, mock_wait): # Even with the wait, we should still get back a passworded server self.assertEqual( - server['adminPass'], + server['admin_password'], self.cloud._normalize_server(fake_server_with_pass)['adminPass'] ) self.assert_calls() @@ -400,6 +403,7 @@ def test_create_server_user_data_base64(self): u'min_count': 1, u'user_data': user_data_b64, u'name': u'server-name'}})), + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), @@ -577,6 +581,7 @@ def test_create_server_network_with_no_nics(self): u'min_count': 1, u'networks': [{u'uuid': u'network-id'}], u'name': u'server-name'}})), + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), @@ -629,6 +634,7 @@ def test_create_server_network_with_empty_nics(self): u'min_count': 1, u'networks': [{u'uuid': u'network-id'}], u'name': u'server-name'}})), + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), @@ -671,6 +677,7 @@ def test_create_server_network_fixed_ip(self): u'min_count': 1, u'networks': [{'fixed_ip': fixed_ip}], u'name': u'server-name'}})), + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), @@ -713,6 +720,7 @@ def test_create_server_network_v4_fixed_ip(self): u'min_count': 1, u'networks': [{'fixed_ip': fixed_ip}], u'name': u'server-name'}})), + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), @@ -757,6 +765,7 @@ def test_create_server_network_v6_fixed_ip(self): u'min_count': 1, u'networks': [{'fixed_ip': fixed_ip}], u'name': u'server-name'}})), + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), @@ -860,6 +869,7 @@ def test_create_server_nics_port_id(self): u'min_count': 1, u'networks': [{u'port': port_id}], u'name': u'server-name'}})), + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), @@ -916,6 +926,7 @@ def test_create_boot_attach_volume(self): } ], u'name': u'server-name'}})), + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), @@ -959,6 +970,7 @@ def test_create_boot_from_volume_image_terminate(self): u'uuid': u'image-id', u'volume_size': u'1'}], u'name': u'server-name'}})), + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), diff --git a/openstack/tests/unit/cloud/test_floating_ip_common.py b/openstack/tests/unit/cloud/test_floating_ip_common.py index 66ac42525..f79bb5003 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_common.py +++ b/openstack/tests/unit/cloud/test_floating_ip_common.py @@ -22,6 +22,7 @@ from unittest.mock import patch from openstack.cloud import meta +from openstack.compute.v2 import server as _server from openstack import connection from openstack.tests import fakes from openstack.tests.unit import base @@ -96,7 +97,10 @@ def test_add_ips_to_server_ipv6_only( }] } ) - server_dict = meta.add_server_interfaces(self.cloud, server) + server_dict = meta.add_server_interfaces( + self.cloud, + _server.Server(**server) + ) new_server = self.cloud.add_ips_to_server(server=server_dict) mock_get_floating_ip.assert_not_called() @@ -136,7 +140,9 @@ def test_add_ips_to_server_rackspace( }] } ) - server_dict = meta.add_server_interfaces(self.cloud, server) + server_dict = meta.add_server_interfaces( + self.cloud, + _server.Server(**server)) new_server = self.cloud.add_ips_to_server(server=server_dict) mock_get_floating_ip.assert_not_called() @@ -172,7 +178,9 @@ def test_add_ips_to_server_rackspace_local_ipv4( }] } ) - server_dict = meta.add_server_interfaces(self.cloud, server) + server_dict = meta.add_server_interfaces( + self.cloud, + _server.Server(**server)) new_server = self.cloud.add_ips_to_server(server=server_dict) mock_get_floating_ip.assert_not_called() diff --git a/openstack/tests/unit/cloud/test_meta.py b/openstack/tests/unit/cloud/test_meta.py index e73d7ce8e..39f0bb4a9 100644 --- a/openstack/tests/unit/cloud/test_meta.py +++ b/openstack/tests/unit/cloud/test_meta.py @@ -15,6 +15,7 @@ from unittest import mock from openstack.cloud import meta +from openstack.compute.v2 import server as _server from openstack import connection from openstack.tests import fakes from openstack.tests.unit import base @@ -424,7 +425,8 @@ def test_get_server_private_ip_devstack( json={'security_groups': []}) ]) - srv = self.cloud.get_openstack_vars(fake_server) + srv = self.cloud.get_openstack_vars( + _server.Server(**fake_server)) self.assertEqual(PRIVATE_V4, srv['private_v4']) self.assert_calls() @@ -482,7 +484,8 @@ def test_get_server_private_ip_no_fip( json={'security_groups': []}) ]) - srv = self.cloud.get_openstack_vars(fake_server) + srv = self.cloud.get_openstack_vars( + _server.Server(**fake_server)) self.assertEqual(PRIVATE_V4, srv['private_v4']) self.assert_calls() @@ -539,7 +542,8 @@ def test_get_server_cloud_no_fips( json={'security_groups': []}) ]) - srv = self.cloud.get_openstack_vars(fake_server) + srv = self.cloud.get_openstack_vars( + _server.Server(**fake_server)) self.assertEqual(PRIVATE_V4, srv['private_v4']) self.assert_calls() @@ -617,7 +621,8 @@ def test_get_server_cloud_missing_fips( json={'security_groups': []}) ]) - srv = self.cloud.get_openstack_vars(fake_server) + srv = self.cloud.get_openstack_vars( + _server.Server(**fake_server)) self.assertEqual(PUBLIC_V4, srv['public_v4']) self.assert_calls() @@ -669,7 +674,8 @@ def test_get_server_cloud_rackspace_v6( json={'security_groups': []}) ]) - srv = self.cloud.get_openstack_vars(fake_server) + srv = self.cloud.get_openstack_vars( + _server.Server(**fake_server)) self.assertEqual("10.223.160.141", srv['private_v4']) self.assertEqual("104.130.246.91", srv['public_v4']) @@ -736,7 +742,8 @@ def test_get_server_cloud_osic_split( json={'security_groups': []}) ]) - srv = self.cloud.get_openstack_vars(fake_server) + srv = self.cloud.get_openstack_vars( + _server.Server(**fake_server)) self.assertEqual("10.223.160.141", srv['private_v4']) self.assertEqual("104.130.246.91", srv['public_v4']) diff --git a/openstack/tests/unit/cloud/test_normalize.py b/openstack/tests/unit/cloud/test_normalize.py index ed2b28134..75b413509 100644 --- a/openstack/tests/unit/cloud/test_normalize.py +++ b/openstack/tests/unit/cloud/test_normalize.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.compute.v2 import server as server_resource from openstack.image.v2 import image as image_resource from openstack.tests.unit import base @@ -521,141 +520,6 @@ def test_normalize_glance_images(self): retval = self.cloud._normalize_image(image) self.assertDictEqual(expected, retval) - def test_normalize_servers_normal(self): - res = server_resource.Server( - connection=self.cloud, - **RAW_SERVER_DICT) - expected = { - 'OS-DCF:diskConfig': u'MANUAL', - 'OS-EXT-AZ:availability_zone': u'ca-ymq-2', - 'OS-EXT-SRV-ATTR:host': None, - 'OS-EXT-SRV-ATTR:hostname': None, - 'OS-EXT-SRV-ATTR:hypervisor_hostname': None, - 'OS-EXT-SRV-ATTR:instance_name': None, - 'OS-EXT-SRV-ATTR:kernel_id': None, - 'OS-EXT-SRV-ATTR:launch_index': None, - 'OS-EXT-SRV-ATTR:ramdisk_id': None, - 'OS-EXT-SRV-ATTR:reservation_id': None, - 'OS-EXT-SRV-ATTR:root_device_name': None, - 'OS-EXT-SRV-ATTR:user_data': None, - 'OS-EXT-STS:power_state': 1, - 'OS-EXT-STS:task_state': None, - 'OS-EXT-STS:vm_state': u'active', - 'OS-SCH-HNT:scheduler_hints': None, - 'OS-SRV-USG:launched_at': u'2015-08-01T19:52:02.000000', - 'OS-SRV-USG:terminated_at': None, - 'accessIPv4': u'', - 'accessIPv6': u'', - 'addresses': { - u'public': [{ - u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:9f:46:3e', - u'OS-EXT-IPS:type': u'fixed', - u'addr': u'2604:e100:1:0:f816:3eff:fe9f:463e', - u'version': 6 - }, { - u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:9f:46:3e', - u'OS-EXT-IPS:type': u'fixed', - u'addr': u'162.253.54.192', - u'version': 4}]}, - 'adminPass': None, - 'az': u'ca-ymq-2', - 'block_device_mapping': None, - 'cloud': '_test_cloud_', - 'config_drive': u'True', - 'created': u'2015-08-01T19:52:16Z', - 'created_at': u'2015-08-01T19:52:16Z', - 'description': None, - 'disk_config': u'MANUAL', - 'flavor': {u'id': u'bbcb7eb5-5c8d-498f-9d7e-307c575d3566'}, - 'has_config_drive': True, - 'host': None, - 'hostId': u'bd37', - 'host_id': u'bd37', - 'host_status': None, - 'hostname': None, - 'hypervisor_hostname': None, - 'id': u'811c5197-dba7-4d3a-a3f6-68ca5328b9a7', - 'image': {u'id': u'69c99b45-cd53-49de-afdc-f24789eb8f83'}, - 'instance_name': None, - 'interface_ip': '', - 'kernel_id': None, - 'key_name': u'mordred', - 'launch_index': None, - 'launched_at': u'2015-08-01T19:52:02.000000', - 'location': { - 'cloud': '_test_cloud_', - 'project': { - 'domain_id': None, - 'domain_name': None, - 'id': u'db92b20496ae4fbda850a689ea9d563f', - 'name': None}, - 'region_name': u'RegionOne', - 'zone': u'ca-ymq-2'}, - 'locked': True, - 'max_count': None, - 'metadata': {u'group': u'irc', u'groups': u'irc,enabled'}, - 'min_count': None, - 'name': u'mordred-irc', - 'networks': { - u'public': [ - u'2604:e100:1:0:f816:3eff:fe9f:463e', - u'162.253.54.192']}, - 'os-extended-volumes:volumes_attached': [], - 'personality': None, - 'power_state': 1, - 'private_v4': None, - 'progress': 0, - 'project_id': u'db92b20496ae4fbda850a689ea9d563f', - 'properties': { - 'OS-DCF:diskConfig': u'MANUAL', - 'OS-EXT-AZ:availability_zone': u'ca-ymq-2', - 'OS-EXT-SRV-ATTR:host': None, - 'OS-EXT-SRV-ATTR:hostname': None, - 'OS-EXT-SRV-ATTR:hypervisor_hostname': None, - 'OS-EXT-SRV-ATTR:instance_name': None, - 'OS-EXT-SRV-ATTR:kernel_id': None, - 'OS-EXT-SRV-ATTR:launch_index': None, - 'OS-EXT-SRV-ATTR:ramdisk_id': None, - 'OS-EXT-SRV-ATTR:reservation_id': None, - 'OS-EXT-SRV-ATTR:root_device_name': None, - 'OS-EXT-SRV-ATTR:user_data': None, - 'OS-EXT-STS:power_state': 1, - 'OS-EXT-STS:task_state': None, - 'OS-EXT-STS:vm_state': u'active', - 'OS-SCH-HNT:scheduler_hints': None, - 'OS-SRV-USG:launched_at': u'2015-08-01T19:52:02.000000', - 'OS-SRV-USG:terminated_at': None, - 'host_status': None, - 'locked': True, - 'max_count': None, - 'min_count': None, - 'os-extended-volumes:volumes_attached': [], - 'trusted_image_certificates': None}, - 'public_v4': None, - 'public_v6': None, - 'ramdisk_id': None, - 'region': u'RegionOne', - 'reservation_id': None, - 'root_device_name': None, - 'scheduler_hints': None, - 'security_groups': [{u'name': u'default'}], - 'server_groups': None, - 'status': u'ACTIVE', - 'locked': True, - 'tags': [], - 'task_state': None, - 'tenant_id': u'db92b20496ae4fbda850a689ea9d563f', - 'terminated_at': None, - 'trusted_image_certificates': None, - 'updated': u'2016-10-15T15:49:29Z', - 'user_data': None, - 'user_id': u'e9b21dc437d149858faee0898fb08e92', - 'vm_state': u'active', - 'volumes': []} - retval = self.cloud._normalize_server(res._to_munch()) - _assert_server_munch_attributes(self, res, retval) - self.assertEqual(expected, retval) - def test_normalize_secgroups(self): nova_secgroup = dict( id='abc123', @@ -1114,97 +978,6 @@ def test_normalize_glance_images(self): self.assertEqual(sorted(expected.keys()), sorted(retval.keys())) self.assertEqual(expected, retval) - def test_normalize_servers(self): - - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': [RAW_SERVER_DICT]}), - ]) - expected = { - 'accessIPv4': u'', - 'accessIPv6': u'', - 'addresses': { - u'public': [{ - u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:9f:46:3e', - u'OS-EXT-IPS:type': u'fixed', - u'addr': u'2604:e100:1:0:f816:3eff:fe9f:463e', - u'version': 6 - }, { - u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:9f:46:3e', - u'OS-EXT-IPS:type': u'fixed', - u'addr': u'162.253.54.192', - u'version': 4}]}, - 'adminPass': None, - 'block_device_mapping': None, - 'created': u'2015-08-01T19:52:16Z', - 'created_at': u'2015-08-01T19:52:16Z', - 'description': None, - 'disk_config': u'MANUAL', - 'flavor': {u'id': u'bbcb7eb5-5c8d-498f-9d7e-307c575d3566'}, - 'has_config_drive': True, - 'host': None, - 'host_id': u'bd37', - 'hostname': None, - 'hypervisor_hostname': None, - 'id': u'811c5197-dba7-4d3a-a3f6-68ca5328b9a7', - 'image': {u'id': u'69c99b45-cd53-49de-afdc-f24789eb8f83'}, - 'interface_ip': u'', - 'instance_name': None, - 'kernel_id': None, - 'key_name': u'mordred', - 'launch_index': None, - 'launched_at': u'2015-08-01T19:52:02.000000', - 'location': { - 'cloud': '_test_cloud_', - 'project': { - 'domain_id': None, - 'domain_name': None, - 'id': u'db92b20496ae4fbda850a689ea9d563f', - 'name': None}, - 'region_name': u'RegionOne', - 'zone': u'ca-ymq-2'}, - 'metadata': {u'group': u'irc', u'groups': u'irc,enabled'}, - 'name': u'mordred-irc', - 'networks': { - u'public': [ - u'2604:e100:1:0:f816:3eff:fe9f:463e', - u'162.253.54.192']}, - 'personality': None, - 'power_state': 1, - 'private_v4': None, - 'progress': 0, - 'properties': { - 'host_status': None, - 'locked': True, - 'max_count': None, - 'min_count': None, - 'trusted_image_certificates': None - }, - 'public_v4': None, - 'public_v6': None, - 'ramdisk_id': None, - 'reservation_id': None, - 'root_device_name': None, - 'scheduler_hints': None, - 'security_groups': [{u'name': u'default'}], - 'server_groups': None, - 'status': u'ACTIVE', - 'tags': [], - 'task_state': None, - 'terminated_at': None, - 'updated': u'2016-10-15T15:49:29Z', - 'user_data': None, - 'user_id': u'e9b21dc437d149858faee0898fb08e92', - 'vm_state': u'active', - 'volumes': []} - self.cloud.strict_mode = True - retval = self.cloud.list_servers(bare=True)[0] - _assert_server_munch_attributes(self, expected, retval) - self.assertEqual(expected, retval) - def test_normalize_secgroups(self): nova_secgroup = dict( id='abc123', From 6775fb80a79daa61451bdd4fcbb5b985068aab78 Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Tue, 14 Dec 2021 09:11:43 +0100 Subject: [PATCH 2975/3836] Block storage - reunite :class: links on single lines Change-Id: I018285ee56ad4dd449c8d2e04af2d599f0e17967 --- openstack/block_storage/v3/_proxy.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 028e23f93..4bd13def4 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -871,8 +871,8 @@ def availability_zones(self): """Return a generator of availability zones :returns: A generator of availability zone - :rtype: :class:`~openstack.block_storage.v3.availability_zone.\ - AvailabilityZone` + :rtype: + :class:`~openstack.block_storage.v3.availability_zone.AvailabilityZone` """ return self._list(availability_zone.AvailabilityZone) @@ -899,10 +899,10 @@ def find_group_type(self, name_or_id, ignore_missing=True): :class:`~openstack.exceptions.ResourceNotFound` will be raised when the group type does not exist. - :returns: One :class:`~openstack.block_storage.v3.group_type - .GroupType' - :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + :returns: One + :class:`~openstack.block_storage.v3.group_type.GroupType` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource can be found. """ return self._find( _group_type.GroupType, name_or_id, ignore_missing=ignore_missing) @@ -933,11 +933,11 @@ def create_group_type(self, **attrs): """Create a group type :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.block_storage.v3.group_type.GroupType' + a :class:`~openstack.block_storage.v3.group_type.GroupType` comprised of the properties on the GroupType class. :returns: The results of group type creation. - :rtype: :class:`~openstack.block_storage.v3.group_type.GroupTye'. + :rtype: :class:`~openstack.block_storage.v3.group_type.GroupTye`. """ return self._create(_group_type.GroupType, **attrs) From b2bea061b5e5aef70ef50b76db9b7029e3d67a6c Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Tue, 14 Dec 2021 09:38:07 +0100 Subject: [PATCH 2976/3836] Identity service - reunite :class: links on single lines Change-Id: I13696ae5c4f2ca1ecb1f3d8b5a6c3c09f30da881 --- openstack/identity/v3/_proxy.py | 78 ++++++++++++++++----------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 0bf02a4b0..a382e2127 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -1048,8 +1048,8 @@ def get_registered_limit(self, registered_limit): def create_registered_limit(self, **attrs): """Create a new registered_limit from attributes - :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.identity.v3.registered_limit.RegisteredLimit`, + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.identity.v3.registered_limit.RegisteredLimit`, comprised of the properties on the RegisteredLimit class. :returns: The results of registered_limit creation. @@ -1365,8 +1365,8 @@ def application_credentials(self, user, **query): limit the resources being returned. :returns: A generator of application credentials instances. - :rtype: :class:`~openstack.identity.v3.application_credential. - ApplicationCredential` + :rtype: + :class:`~openstack.identity.v3.application_credential.ApplicationCredential` """ user = self._get_resource(_user.User, user) return self._list(_application_credential.ApplicationCredential, @@ -1383,8 +1383,8 @@ def get_application_credential(self, user, application_credential): `~openstack.identity.v3.application_credential. ApplicationCredential` instance. - :returns: One :class:`~openstack.identity.v3.application_credential. - ApplicationCredential` + :returns: One + :class:`~openstack.identity.v3.application_credential.ApplicationCredential` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ @@ -1400,15 +1400,14 @@ def create_application_credential(self, user, name, **attrs): :class:`~openstack.identity.v3.user.User` instance. :param name: The name of the application credential which is unique to the user. - :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.identity.v3.application_credential. - ApplicationCredential`, comprised of the properties on the - ApplicationCredential class. + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.identity.v3.application_credential.ApplicationCredential`, + comprised of the properties on the ApplicationCredential class. :returns: The results of application credential creation. - :rtype: :class:`~openstack.identity.v3.application_credential. - ApplicationCredential` + :rtype: + :class:`~openstack.identity.v3.application_credential.ApplicationCredential` """ user = self._get_resource(_user.User, user) @@ -1429,8 +1428,9 @@ def find_application_credential(self, user, name_or_id, When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :returns: One :class:`~openstack.identity.v3.application_credential. - ApplicationCredential` or None + :returns: One + :class:`~openstack.identity.v3.application_credential.ApplicationCredential` + or None """ user = self._get_resource(_user.User, user) return self._find(_application_credential.ApplicationCredential, @@ -1443,9 +1443,10 @@ def delete_application_credential(self, user, application_credential, :param user: Either the ID of a user or a :class:`~openstack.identity.v3.user.User` instance. - :param application credential: The value can be either the ID of a - application credential or a :class: `~openstack.identity.v3. - application_credential.ApplicationCredential` instance. + :param application credential: The value can be either the ID of an + application credential or a + :class:`~openstack.identity.v3.application_credential.ApplicationCredential` + instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the application credential does not exist. When set to @@ -1468,13 +1469,13 @@ def create_federation_protocol(self, idp_id, **attrs): representing the identity provider the protocol is to be attached to. :param dict attrs: Keyword arguments which will be used to create a - :class:`~openstack.identity.v3.federation_protocol. - FederationProtocol`, comprised of the properties on the + :class:`~openstack.identity.v3.federation_protocol.FederationProtocol`, + comprised of the properties on the FederationProtocol class. :returns: The results of federation protocol creation - :rtype: :class:`~openstack.identity.v3.federation_protocol. - FederationProtocol` + :rtype: + :class:`~openstack.identity.v3.federation_protocol.FederationProtocol` """ idp_cls = _identity_provider.IdentityProvider @@ -1491,11 +1492,11 @@ def delete_federation_protocol(self, idp_id, protocol, :class:`~openstack.identity.v3.identity_provider.IdentityProvider` representing the identity provider the protocol is attached to. Can be None if protocol is a - :class:`~openstack.identity.v3.federation_protocol. - FederationProtocol` instance. + :class:`~openstack.identity.v3.federation_protocol.FederationProtocol` + instance. :param protocol: The ID of a federation protocol or a - :class:`~openstack.identity.v3.federation_protocol. - FederationProtocol` instance. + :class:`~openstack.identity.v3.federation_protocol.FederationProtocol` + instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the federation protocol does not exist. When set to @@ -1526,8 +1527,8 @@ def find_federation_protocol(self, idp_id, protocol, when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. :returns: One federation protocol or None - :rtype: :class:`~openstack.identity.v3.federation_protocol. - FederationProtocol` + :rtype: + :class:`~openstack.identity.v3.federation_protocol.FederationProtocol` """ idp_cls = _identity_provider.IdentityProvider if isinstance(idp_id, idp_cls): @@ -1542,15 +1543,14 @@ def get_federation_protocol(self, idp_id, protocol): :class:`~openstack.identity.v3.identity_provider.IdentityProvider` representing the identity provider the protocol is attached to. Can be None if protocol is a - :class:`~openstack.identity.v3.federation_protocol. + :class:`~openstack.identity.v3.federation_protocol.FederationProtocol` :param protocol: The value can be the ID of a federation protocol or a - :class:`~openstack.identity.v3.federation_protocol. - FederationProtocol` + :class:`~openstack.identity.v3.federation_protocol.FederationProtocol` instance. :returns: One federation protocol - :rtype: :class:`~openstack.identity.v3.federation_protocol. - FederationProtocol` + :rtype: + :class:`~openstack.identity.v3.federation_protocol.FederationProtocol` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ @@ -1572,8 +1572,8 @@ def federation_protocols(self, idp_id, **query): the resources being returned. :returns: A generator of federation protocol instances. - :rtype: :class:`~openstack.identity.v3.federation_protocol. - FederationProtocol` + :rtype: + :class:`~openstack.identity.v3.federation_protocol.FederationProtocol` """ idp_cls = _identity_provider.IdentityProvider if isinstance(idp_id, idp_cls): @@ -1588,16 +1588,16 @@ def update_federation_protocol(self, idp_id, protocol, **attrs): :class:`~openstack.identity.v3.identity_provider.IdentityProvider` representing the identity provider the protocol is attached to. Can be None if protocol is a - :class:`~openstack.identity.v3.federation_protocol. + :class:`~openstack.identity.v3.federation_protocol.FederationProtocol` :param protocol: Either the ID of a federation protocol or a - :class:`~openstack.identity.v3.federation_protocol. - FederationProtocol` instance. + :class:`~openstack.identity.v3.federation_protocol.FederationProtocol` + instance. :attrs kwargs: The attributes to update on the federation protocol represented by ``value``. :returns: The updated federation protocol - :rtype: :class:`~openstack.identity.v3.federation_protocol. - FederationProtocol` + :rtype: + :class:`~openstack.identity.v3.federation_protocol.FederationProtocol` """ cls = _federation_protocol.FederationProtocol if (idp_id is None) and (isinstance(protocol, cls)): From d7098876455a3abbe53609f58d2db380460adc6d Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Sun, 19 Dec 2021 14:56:55 +0900 Subject: [PATCH 2977/3836] Fix misuse of assertTrue Fix misuse of assertTrue by replacing with assertEqual. Signed-off-by: Takashi Natsume Change-Id: If6dfc295e062da660d408af50a06cac69bf03da9 --- openstack/tests/unit/image/v2/test_proxy.py | 4 ++-- openstack/tests/unit/test_resource.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 0a2826872..f07a6badc 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -429,7 +429,7 @@ def test_wait_for_task_immediate_status(self): result = self.proxy.wait_for_task( res, status, "failure", 0.01, 0.1) - self.assertTrue(result, res) + self.assertEqual(res, result) def test_wait_for_task_immediate_status_case(self): status = "SUCcess" @@ -438,7 +438,7 @@ def test_wait_for_task_immediate_status_case(self): result = self.proxy.wait_for_task( res, status, "failure", 0.01, 0.1) - self.assertTrue(result, res) + self.assertEqual(res, result) def test_wait_for_task_error_396(self): # Ensure we create a new task when we get 396 error diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index d77b9ad72..16250c4a3 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -2925,7 +2925,7 @@ def test_immediate_status(self): result = resource.wait_for_status( self.cloud.compute, res, status, "failures", "interval", "wait") - self.assertTrue(result, res) + self.assertEqual(res, result) def test_immediate_status_case(self): status = "LOLing" @@ -2935,7 +2935,7 @@ def test_immediate_status_case(self): result = resource.wait_for_status( self.cloud.compute, res, 'lOling', "failures", "interval", "wait") - self.assertTrue(result, res) + self.assertEqual(res, result) def test_immediate_status_different_attribute(self): status = "loling" @@ -2946,7 +2946,7 @@ def test_immediate_status_different_attribute(self): self.cloud.compute, res, status, "failures", "interval", "wait", attribute='mood') - self.assertTrue(result, res) + self.assertEqual(res, result) def _resources_from_statuses(self, *statuses, **kwargs): attribute = kwargs.pop('attribute', 'status') From a92ee7ae516b7cd797899f84642aa5668d9b23da Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Sun, 19 Dec 2021 12:29:03 +0100 Subject: [PATCH 2978/3836] Switch rebuild_server cloud method to rely on proxy - Switch conn.cloud._compute.rebuild_server to rely on the proxy layer - Adapt proxy parameters of the rebuild_server to correspond to the reality: - name and admin_pass are optional params - image is mandatory param Change-Id: I953476378d4e9fc558c126e6024e4d56031112a1 --- openstack/cloud/_compute.py | 40 +++++-------------- openstack/compute/v2/_proxy.py | 4 +- openstack/compute/v2/server.py | 10 ++--- .../tests/unit/cloud/test_rebuild_server.py | 24 +++++------ openstack/tests/unit/compute/v2/test_proxy.py | 32 +++++++++++---- .../tests/unit/compute/v2/test_server.py | 14 ++++--- 6 files changed, 62 insertions(+), 62 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index d153305a9..21961a910 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -1086,41 +1086,23 @@ def rebuild_server(self, server_id, image_id, admin_pass=None, wait=False, timeout=180): kwargs = {} if image_id: - kwargs['imageRef'] = image_id + kwargs['image'] = image_id if admin_pass: - kwargs['adminPass'] = admin_pass + kwargs['admin_password'] = admin_pass - data = proxy._json_response( - self.compute.post( - '/servers/{server_id}/action'.format(server_id=server_id), - json={'rebuild': kwargs}), - error_message="Error in rebuilding instance") - server = self._get_and_munchify('server', data) + server = self.compute.rebuild_server( + server_id, + **kwargs + ) if not wait: return self._expand_server( - self._normalize_server(server), bare=bare, detailed=detailed) + server, bare=bare, detailed=detailed) admin_pass = server.get('adminPass') or admin_pass - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for server {0} to " - "rebuild.".format(server_id), - wait=self._SERVER_AGE): - try: - server = self.get_server(server_id, bare=True) - except Exception: - continue - if not server: - continue - - if server['status'] == 'ERROR': - raise exc.OpenStackCloudException( - "Error in rebuilding the server", - extra_data=dict(server=server)) - - if server['status'] == 'ACTIVE': - server.adminPass = admin_pass - break + server = self.compute.wait_for_server( + server, wait=timeout) + if server['status'] == 'ACTIVE': + server.adminPass = admin_pass return self._expand_server(server, detailed=detailed, bare=bare) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 92b9f96f2..1760ff14e 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -754,7 +754,7 @@ def reboot_server(self, server, reboot_type): server = self._get_resource(_server.Server, server) server.reboot(self, reboot_type) - def rebuild_server(self, server, name, admin_password, **attrs): + def rebuild_server(self, server, image, **attrs): """Rebuild a server :param server: Either the ID of a server or a @@ -780,7 +780,7 @@ def rebuild_server(self, server, name, admin_password, **attrs): instance. """ server = self._get_resource(_server.Server, server) - return server.rebuild(self, name, admin_password, **attrs) + return server.rebuild(self, image=image, **attrs) def resize_server(self, server, flavor): """Resize a server diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 4a17accff..37241fd86 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -293,16 +293,16 @@ def force_delete(self, session): body = {'forceDelete': None} self._action(session, body) - def rebuild(self, session, name=None, admin_password=None, - preserve_ephemeral=False, image=None, + def rebuild(self, session, image, name=None, admin_password=None, + preserve_ephemeral=None, access_ipv4=None, access_ipv6=None, metadata=None, user_data=None): """Rebuild the server with the given arguments.""" action = { - 'preserve_ephemeral': preserve_ephemeral + 'imageRef': resource.Resource._get_id(image) } - if image is not None: - action['imageRef'] = resource.Resource._get_id(image) + if preserve_ephemeral is not None: + action['preserve_ephemeral'] = preserve_ephemeral if name is not None: action['name'] = name if admin_password is not None: diff --git a/openstack/tests/unit/cloud/test_rebuild_server.py b/openstack/tests/unit/cloud/test_rebuild_server.py index 24efb63d6..8cef0a533 100644 --- a/openstack/tests/unit/cloud/test_rebuild_server.py +++ b/openstack/tests/unit/cloud/test_rebuild_server.py @@ -82,8 +82,8 @@ def test_rebuild_server_server_error(self): self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': [self.error_server]}), + 'compute', 'public', append=['servers', self.server_id]), + json={'server': self.error_server}), ]) self.assertRaises( exc.OpenStackCloudException, @@ -109,8 +109,8 @@ def test_rebuild_server_timeout(self): self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': [self.rebuild_server]}), + 'compute', 'public', append=['servers', self.server_id]), + json={'server': self.rebuild_server}), ]) self.assertRaises( exc.OpenStackCloudTimeout, @@ -199,12 +199,12 @@ def test_rebuild_server_with_admin_pass_wait(self): self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': [self.rebuild_server]}), + 'compute', 'public', append=['servers', self.server_id]), + json={'server': self.rebuild_server}), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': [self.fake_server]}), + 'compute', 'public', append=['servers', self.server_id]), + json={'server': self.fake_server}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks']), @@ -237,12 +237,12 @@ def test_rebuild_server_wait(self): self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': [self.rebuild_server]}), + 'compute', 'public', append=['servers', self.server_id]), + json={'server': self.rebuild_server}), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': [self.fake_server]}), + 'compute', 'public', append=['servers', self.server_id]), + json={'server': self.fake_server}), dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks']), diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 5e3f4bf2c..68f414004 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -729,19 +729,35 @@ def test_server_rebuild(self): self._verify( 'openstack.compute.v2.server.Server.rebuild', self.proxy.rebuild_server, - method_args=["value", "test_server", "test_pass"], - method_kwargs={"metadata": {"k1": "v1"}, "image": image_obj}, - expected_args=[self.proxy, "test_server", "test_pass"], - expected_kwargs={"metadata": {"k1": "v1"}, "image": image_obj}) + method_args=["value"], + method_kwargs={ + "name": "test_server", + "admin_password": "test_pass", + "metadata": {"k1": "v1"}, + "image": image_obj}, + expected_args=[self.proxy], + expected_kwargs={ + "name": "test_server", + "admin_password": "test_pass", + "metadata": {"k1": "v1"}, + "image": image_obj}) # Case2: image name or id is provided self._verify( 'openstack.compute.v2.server.Server.rebuild', self.proxy.rebuild_server, - method_args=["value", "test_server", "test_pass"], - method_kwargs={"metadata": {"k1": "v1"}, "image": id}, - expected_args=[self.proxy, "test_server", "test_pass"], - expected_kwargs={"metadata": {"k1": "v1"}, "image": id}) + method_args=["value"], + method_kwargs={ + "name": "test_server", + "admin_password": "test_pass", + "metadata": {"k1": "v1"}, + "image": id}, + expected_args=[self.proxy], + expected_kwargs={ + "name": "test_server", + "admin_password": "test_pass", + "metadata": {"k1": "v1"}, + "image": id}) def test_add_fixed_ip_to_server(self): self._verify( diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index d3eef970e..2a077063b 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -315,11 +315,14 @@ def test_rebuild(self): # Let the translate pass through, that portion is tested elsewhere sot._translate_response = lambda arg: arg - result = sot.rebuild(self.sess, name='noo', admin_password='seekr3t', - image='http://image/1', access_ipv4="12.34.56.78", - access_ipv6="fe80::100", - metadata={"meta var": "meta val"}, - user_data="ZWNobyAiaGVsbG8gd29ybGQi") + result = sot.rebuild( + self.sess, name='noo', admin_password='seekr3t', + image='http://image/1', access_ipv4="12.34.56.78", + access_ipv6="fe80::100", + metadata={"meta var": "meta val"}, + user_data="ZWNobyAiaGVsbG8gd29ybGQi", + preserve_ephemeral=False + ) self.assertIsInstance(result, server.Server) @@ -357,7 +360,6 @@ def test_rebuild_minimal(self): "name": "nootoo", "imageRef": "http://image/2", "adminPass": "seekr3two", - "preserve_ephemeral": False } } headers = {'Accept': ''} From f4dafb3a5c4e119809756acecfbd7b231a063ddc Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Sun, 19 Dec 2021 12:51:42 +0100 Subject: [PATCH 2979/3836] Switch update_server cloud layer to proxy Rely on proxy in the cloud layer for update_server method Change-Id: I15e387e36a1ea8c39a860aa10767c8a63e5c2390 --- openstack/cloud/_compute.py | 18 +++++++----------- .../tests/unit/cloud/test_update_server.py | 16 ++++++++++++++-- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 21961a910..01eb54cd9 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -1269,18 +1269,14 @@ def update_server(self, name_or_id, detailed=False, bare=False, **kwargs): :raises: OpenStackCloudException on operation error. """ - server = self.get_server(name_or_id=name_or_id, bare=True) - if server is None: - raise exc.OpenStackCloudException( - "failed to find server '{server}'".format(server=name_or_id)) + server = self.compute.find_server( + name_or_id, + ignore_missing=False + ) + + server = self.compute.update_server( + server, **kwargs) - data = proxy._json_response( - self.compute.put( - '/servers/{server_id}'.format(server_id=server['id']), - json={'server': kwargs}), - error_message="Error updating server {0}".format(name_or_id)) - server = self._normalize_server( - self._get_and_munchify('server', data)) return self._expand_server(server, bare=bare, detailed=detailed) def create_server_group(self, name, policies=[], policy=None): diff --git a/openstack/tests/unit/cloud/test_update_server.py b/openstack/tests/unit/cloud/test_update_server.py index 4fd5f0097..e25ec2e36 100644 --- a/openstack/tests/unit/cloud/test_update_server.py +++ b/openstack/tests/unit/cloud/test_update_server.py @@ -43,7 +43,13 @@ def test_update_server_with_update_exception(self): self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), + 'compute', 'public', + append=['servers', self.server_name]), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'], + qs_elements=['name=%s' % self.server_name]), json={'servers': [self.fake_server]}), dict(method='PUT', uri=self.get_mock_url( @@ -69,7 +75,13 @@ def test_update_server_name(self): self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), + 'compute', 'public', + append=['servers', self.server_name]), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'], + qs_elements=['name=%s' % self.server_name]), json={'servers': [self.fake_server]}), dict(method='PUT', uri=self.get_mock_url( From 9d45886028a7470255698b942dfc859fdab16938 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 20 Dec 2021 11:59:48 +0000 Subject: [PATCH 2980/3836] tests: Handle overridden 'verify_delete' (kw)args Sometimes we want to pass an explicit empty list/iterable to indicate that there are no expected (keyword) arguments. You can't do this with the current 'a or b' pattern. Rather, we need an explicit 'is None' check as used in the other 'verify_foo' helpers. Make it so. Change-Id: I348927b14fb91db7328c46672496c0048e2e34f6 Signed-off-by: Stephen Finucane --- openstack/tests/unit/compute/v2/test_proxy.py | 1 - openstack/tests/unit/test_proxy_base.py | 12 ++++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index e81318d50..2d7a9f4bc 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -588,7 +588,6 @@ def test_server_interface_delete_ignore(self): self.verify_delete(self.proxy.delete_server_interface, server_interface.ServerInterface, True, method_kwargs={"server": "test_id"}, - expected_args=[], expected_kwargs={"server_id": "test_id"}) def test_server_interface_get(self): diff --git a/openstack/tests/unit/test_proxy_base.py b/openstack/tests/unit/test_proxy_base.py index 69a843807..e5ee699ac 100644 --- a/openstack/tests/unit/test_proxy_base.py +++ b/openstack/tests/unit/test_proxy_base.py @@ -108,11 +108,15 @@ def verify_delete( expected_args=None, expected_kwargs=None, mock_method="openstack.proxy.Proxy._delete", ): - method_args = method_args or ['resource_id'] - method_kwargs = method_kwargs or {} + if method_args is None: + method_args = ['resource_id'] + if method_kwargs is None: + method_kwargs = {} method_kwargs["ignore_missing"] = ignore_missing - expected_args = expected_args or method_args.copy() - expected_kwargs = expected_kwargs or method_kwargs.copy() + if expected_args is None: + expected_args = method_args.copy() + if expected_kwargs is None: + expected_kwargs = method_kwargs.copy() self._verify( mock_method, From 6a08c2b29366b2a20c21d553a0a8aa0f9eac724a Mon Sep 17 00:00:00 2001 From: songwenping Date: Wed, 22 Dec 2021 08:34:42 +0000 Subject: [PATCH 2981/3836] Update python testing classifier Yoga testing runtime[1] has been updated to add py39 testing as voting. Unit tests update are handled by the job template change in openstack-zuul-job - https://review.opendev.org/c/openstack/openstack-zuul-jobs/+/820286 this commit updates the classifier in setup.cfg file. [1] https://governance.openstack.org/tc/reference/runtimes/yoga.html Change-Id: Ie56b576971d75a6fde7088a74e4e2cea0178284a --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 0c8ae0e52..2fd433c2e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,6 +17,7 @@ classifier = Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 python_requires = >=3.6 [files] From c0a9b64e4961e10711b2cd8324f4001ca222676c Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Sun, 19 Dec 2021 13:09:51 +0100 Subject: [PATCH 2982/3836] Switch delete_server cloud method to rely on proxy Switch delete_server method of the cloud layer to rely on the compute proxy. Change-Id: I4358876ef4c644f24206ea1f188a9fbcdcf5c7ca --- openstack/cloud/_compute.py | 31 ++++--- .../tests/functional/cloud/test_compute.py | 33 +++++--- .../tests/unit/cloud/test_create_server.py | 4 +- .../tests/unit/cloud/test_delete_server.py | 83 ++++++++++++++----- 4 files changed, 102 insertions(+), 49 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 01eb54cd9..3be03a95c 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -27,6 +27,7 @@ from openstack.cloud import _utils from openstack.cloud import exc from openstack.cloud import meta +from openstack.compute.v2 import server as _server from openstack import exceptions from openstack import proxy from openstack import utils @@ -1161,7 +1162,8 @@ def delete_server( :raises: OpenStackCloudException on operation error. """ # If delete_ips is True, we need the server to not be bare. - server = self.get_server(name_or_id, bare=True) + server = self.compute.find_server( + name_or_id, ignore_missing=True) if not server: return False @@ -1205,15 +1207,13 @@ def _delete_server( if not server: return False - if delete_ips and self._has_floating_ips(): + if delete_ips and self._has_floating_ips() and server['addresses']: self._delete_server_floating_ips(server, delete_ip_retry) try: - proxy._json_response( - self.compute.delete( - '/servers/{id}'.format(id=server['id'])), - error_message="Error in deleting server") - except exc.OpenStackCloudURINotFound: + self.compute.delete_server( + server) + except exceptions.ResourceNotFound: return False except Exception: raise @@ -1231,16 +1231,13 @@ def _delete_server( and self.get_volumes(server)): reset_volume_cache = True - for count in utils.iterate_timeout( - timeout, - "Timed out waiting for server to get deleted.", - # if _SERVER_AGE is 0 we still want to wait a bit - # to be friendly with the server. - wait=self._SERVER_AGE or 2): - with _utils.shade_exceptions("Error in deleting server"): - server = self.get_server(server['id'], bare=True) - if not server: - break + if not isinstance(server, _server.Server): + # We might come here with Munch object (at the moment). + # If this is the case - convert it into real server to be able to + # use wait_for_delete + server = _server.Server(id=server['id']) + self.compute.wait_for_delete( + server, wait=timeout) if reset_volume_cache: self.list_volumes.invalidate(self) diff --git a/openstack/tests/functional/cloud/test_compute.py b/openstack/tests/functional/cloud/test_compute.py index b41077685..787f356da 100644 --- a/openstack/tests/functional/cloud/test_compute.py +++ b/openstack/tests/functional/cloud/test_compute.py @@ -80,7 +80,8 @@ def test_create_and_delete_server(self): self.assertIsNotNone(server['adminPass']) self.assertTrue( self.user_cloud.delete_server(self.server_name, wait=True)) - self.assertIsNone(self.user_cloud.get_server(self.server_name)) + srv = self.user_cloud.get_server(self.server_name) + self.assertTrue(srv is None or srv.status.lower() == 'deleted') def test_create_and_delete_server_auto_ip_delete_ips(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) @@ -97,7 +98,8 @@ def test_create_and_delete_server_auto_ip_delete_ips(self): self.assertTrue( self.user_cloud.delete_server( self.server_name, wait=True, delete_ips=True)) - self.assertIsNone(self.user_cloud.get_server(self.server_name)) + srv = self.user_cloud.get_server(self.server_name) + self.assertTrue(srv is None or srv.status.lower() == 'deleted') def test_attach_detach_volume(self): self.skipTest('Volume functional tests temporarily disabled') @@ -128,7 +130,8 @@ def test_create_and_delete_server_with_config_drive(self): self.assertIsNotNone(server['adminPass']) self.assertTrue( self.user_cloud.delete_server(self.server_name, wait=True)) - self.assertIsNone(self.user_cloud.get_server(self.server_name)) + srv = self.user_cloud.get_server(self.server_name) + self.assertTrue(srv is None or srv.status.lower() == 'deleted') def test_create_and_delete_server_with_config_drive_none(self): # check that we're not sending invalid values for config_drive @@ -149,7 +152,8 @@ def test_create_and_delete_server_with_config_drive_none(self): self.assertTrue( self.user_cloud.delete_server( self.server_name, wait=True)) - self.assertIsNone(self.user_cloud.get_server(self.server_name)) + srv = self.user_cloud.get_server(self.server_name) + self.assertTrue(srv is None or srv.status.lower() == 'deleted') def test_list_all_servers(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) @@ -186,7 +190,8 @@ def test_create_server_image_flavor_dict(self): self.assertIsNotNone(server['adminPass']) self.assertTrue( self.user_cloud.delete_server(self.server_name, wait=True)) - self.assertIsNone(self.user_cloud.get_server(self.server_name)) + srv = self.user_cloud.get_server(self.server_name) + self.assertTrue(srv is None or srv.status.lower() == 'deleted') def test_get_server_console(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) @@ -235,7 +240,8 @@ def test_create_and_delete_server_with_admin_pass(self): self.assertEqual(server['adminPass'], 'sheiqu9loegahSh') self.assertTrue( self.user_cloud.delete_server(self.server_name, wait=True)) - self.assertIsNone(self.user_cloud.get_server(self.server_name)) + srv = self.user_cloud.get_server(self.server_name) + self.assertTrue(srv is None or srv.status.lower() == 'deleted') def test_get_image_id(self): self.assertEqual( @@ -285,7 +291,8 @@ def test_create_boot_from_volume_image(self): self.assertTrue(self.user_cloud.delete_server(server.id, wait=True)) self._wait_for_detach(volume.id) self.assertTrue(self.user_cloud.delete_volume(volume.id, wait=True)) - self.assertIsNone(self.user_cloud.get_server(server.id)) + srv = self.user_cloud.get_server(self.server_name) + self.assertTrue(srv is None or srv.status.lower() == 'deleted') self.assertIsNone(self.user_cloud.get_volume(volume.id)) def _wait_for_detach(self, volume_id): @@ -323,7 +330,8 @@ def test_create_terminate_volume_image(self): # that is in the process of being deleted. if volume: self.assertEqual('deleting', volume.status) - self.assertIsNone(self.user_cloud.get_server(self.server_name)) + srv = self.user_cloud.get_server(self.server_name) + self.assertTrue(srv is None or srv.status.lower() == 'deleted') def test_create_boot_from_volume_preexisting(self): self.skipTest('Volume functional tests temporarily disabled') @@ -350,7 +358,8 @@ def test_create_boot_from_volume_preexisting(self): self.assertEqual([], volume['attachments']) self._wait_for_detach(volume.id) self.assertTrue(self.user_cloud.delete_volume(volume_id)) - self.assertIsNone(self.user_cloud.get_server(self.server_name)) + srv = self.user_cloud.get_server(self.server_name) + self.assertTrue(srv is None or srv.status.lower() == 'deleted') self.assertIsNone(self.user_cloud.get_volume(volume_id)) def test_create_boot_attach_volume(self): @@ -378,7 +387,8 @@ def test_create_boot_attach_volume(self): self.assertEqual([], volume['attachments']) self._wait_for_detach(volume.id) self.assertTrue(self.user_cloud.delete_volume(volume_id)) - self.assertIsNone(self.user_cloud.get_server(self.server_name)) + srv = self.user_cloud.get_server(self.server_name) + self.assertTrue(srv is None or srv.status.lower() == 'deleted') self.assertIsNone(self.user_cloud.get_volume(volume_id)) def test_create_boot_from_volume_preexisting_terminate(self): @@ -404,7 +414,8 @@ def test_create_boot_from_volume_preexisting_terminate(self): # that is in the process of being deleted. if volume: self.assertEqual('deleting', volume.status) - self.assertIsNone(self.user_cloud.get_server(self.server_name)) + srv = self.user_cloud.get_server(self.server_name) + self.assertTrue(srv is None or srv.status.lower() == 'deleted') def test_create_image_snapshot_wait_active(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) diff --git a/openstack/tests/unit/cloud/test_create_server.py b/openstack/tests/unit/cloud/test_create_server.py index d911b0f3d..61528882f 100644 --- a/openstack/tests/unit/cloud/test_create_server.py +++ b/openstack/tests/unit/cloud/test_create_server.py @@ -534,8 +534,8 @@ def test_create_server_no_addresses( 'compute', 'public', append=['servers', '1234'])), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': []}), + 'compute', 'public', append=['servers', '1234']), + status_code=404), ]) mock_add_ips_to_server.return_value = fake_server self.cloud._SERVER_AGE = 0 diff --git a/openstack/tests/unit/cloud/test_delete_server.py b/openstack/tests/unit/cloud/test_delete_server.py index 10f8be27c..67c6306e6 100644 --- a/openstack/tests/unit/cloud/test_delete_server.py +++ b/openstack/tests/unit/cloud/test_delete_server.py @@ -34,7 +34,12 @@ def test_delete_server(self): self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), + 'compute', 'public', append=['servers', 'daffy']), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'], + qs_elements=['name=daffy']), json={'servers': [server]}), dict(method='DELETE', uri=self.get_mock_url( @@ -52,7 +57,12 @@ def test_delete_server_already_gone(self): self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), + 'compute', 'public', append=['servers', 'tweety']), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'], + qs_elements=['name=tweety']), json={'servers': []}), ]) self.assertFalse(self.cloud.delete_server('tweety', wait=False)) @@ -64,7 +74,12 @@ def test_delete_server_already_gone_wait(self): self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), + 'compute', 'public', append=['servers', 'speedy']), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'], + qs_elements=['name=speedy']), json={'servers': []}), ]) self.assertFalse(self.cloud.delete_server('speedy', wait=True)) @@ -79,19 +94,24 @@ def test_delete_server_wait_for_deleted(self): self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), + 'compute', 'public', append=['servers', 'wily']), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'], + qs_elements=['name=wily']), json={'servers': [server]}), dict(method='DELETE', uri=self.get_mock_url( 'compute', 'public', append=['servers', '9999'])), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': [server]}), + 'compute', 'public', append=['servers', '9999']), + json={'server': server}), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': []}), + 'compute', 'public', append=['servers', '9999']), + status_code=404), ]) self.assertTrue(self.cloud.delete_server('wily', wait=True)) @@ -106,7 +126,12 @@ def test_delete_server_fails(self): self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), + 'compute', 'public', append=['servers', 'speedy']), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'], + qs_elements=['name=speedy']), json={'servers': [server]}), dict(method='DELETE', uri=self.get_mock_url( @@ -138,7 +163,12 @@ def fake_has_service(service_type): self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), + 'compute', 'public', append=['servers', 'porky']), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'], + qs_elements=['name=porky']), json={'servers': [server]}), dict(method='DELETE', uri=self.get_mock_url( @@ -159,7 +189,12 @@ def test_delete_server_delete_ips(self): self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), + 'compute', 'public', append=['servers', 'porky']), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'], + qs_elements=['name=porky']), json={'servers': [server]}), dict(method='GET', uri=self.get_mock_url( @@ -189,8 +224,8 @@ def test_delete_server_delete_ips(self): 'compute', 'public', append=['servers', '1234'])), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': []}), + 'compute', 'public', append=['servers', '1234']), + status_code=404), ]) self.assertTrue(self.cloud.delete_server( 'porky', wait=True, delete_ips=True)) @@ -207,7 +242,12 @@ def test_delete_server_delete_ips_bad_neutron(self): self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), + 'compute', 'public', append=['servers', 'porky']), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'], + qs_elements=['name=porky']), json={'servers': [server]}), dict(method='GET', uri=self.get_mock_url( @@ -220,8 +260,8 @@ def test_delete_server_delete_ips_bad_neutron(self): 'compute', 'public', append=['servers', '1234'])), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': []}), + 'compute', 'public', append=['servers', '1234']), + status_code=404), ]) self.assertTrue(self.cloud.delete_server( 'porky', wait=True, delete_ips=True)) @@ -239,7 +279,12 @@ def test_delete_server_delete_fips_nova(self): self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), + 'compute', 'public', append=['servers', 'porky']), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'], + qs_elements=['name=porky']), json={'servers': [server]}), dict(method='GET', uri=self.get_mock_url( @@ -264,8 +309,8 @@ def test_delete_server_delete_fips_nova(self): 'compute', 'public', append=['servers', '1234'])), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': []}), + 'compute', 'public', append=['servers', '1234']), + status_code=404), ]) self.assertTrue(self.cloud.delete_server( 'porky', wait=True, delete_ips=True)) From 29c872460b392885882c2fbba35249d188ecdcef Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Sun, 19 Dec 2021 13:41:10 +0100 Subject: [PATCH 2983/3836] Switch create_server cloud method to proxy Switch create_server method of the cloud layer to rely on the compute proxy. Change-Id: I1a6ba311ddedc1b8910051257299d3acd367df46 --- openstack/cloud/_compute.py | 50 +++++----- .../tests/unit/cloud/test_create_server.py | 95 ++++++++++++------- 2 files changed, 84 insertions(+), 61 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 3be03a95c..5742bc06e 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -736,7 +736,6 @@ def create_server( raise TypeError( "create_server() requires either 'image' or 'boot_volume'") - microversion = None server_json = {'server': kwargs} # TODO(mordred) Add support for description starting in 2.19 @@ -836,7 +835,8 @@ def create_server( # A tag supported only in server microversion 2.32-2.36 or >= 2.42 # Bumping the version to 2.42 to support the 'tag' implementation if 'tag' in nic: - microversion = utils.pick_microversion(self.compute, '2.42') + utils.require_microversion( + self.compute, '2.42') net['tag'] = nic.pop('tag') if nic: raise exc.OpenStackCloudException( @@ -845,6 +845,9 @@ def create_server( networks.append(net) if networks: kwargs['networks'] = networks + else: + # If user has not passed networks - let Nova try the best. + kwargs['networks'] = 'auto' if image: if isinstance(image, dict): @@ -870,32 +873,27 @@ def create_server( volumes=volumes, kwargs=kwargs) kwargs['name'] = name - endpoint = '/servers' + + server = self.compute.create_server(**kwargs) # TODO(mordred) We're only testing this in functional tests. We need # to add unit tests for this too. - if 'block_device_mapping_v2' in kwargs: - endpoint = '/os-volumes_boot' - with _utils.shade_exceptions("Error in creating instance"): - data = proxy._json_response( - self.compute.post(endpoint, json=server_json, - microversion=microversion)) - server = self._get_and_munchify('server', data) - admin_pass = server.get('adminPass') or kwargs.get('admin_pass') - if not wait: - # This is a direct get call to skip the list_servers - # cache which has absolutely no chance of containing the - # new server. - # Only do this if we're not going to wait for the server - # to complete booting, because the only reason we do it - # is to get a server record that is the return value from - # get/list rather than the return value of create. If we're - # going to do the wait loop below, this is a waste of a call - server = self.get_server_by_id(server.id) - if server.status == 'ERROR': - raise exc.OpenStackCloudCreateException( - resource='server', resource_id=server.id) - - if wait: + admin_pass = server.admin_password or kwargs.get('admin_pass') + if not wait: + # This is a direct get call to skip the list_servers + # cache which has absolutely no chance of containing the + # new server. + # Only do this if we're not going to wait for the server + # to complete booting, because the only reason we do it + # is to get a server record that is the return value from + # get/list rather than the return value of create. If we're + # going to do the wait loop below, this is a waste of a call + server = self.compute.get_server(server.id) + if server.status == 'ERROR': + raise exc.OpenStackCloudCreateException( + resource='server', resource_id=server.id) + server = meta.add_server_interfaces(self, server) + + else: server = self.wait_for_server( server, auto_ip=auto_ip, ips=ips, ip_pool=ip_pool, diff --git a/openstack/tests/unit/cloud/test_create_server.py b/openstack/tests/unit/cloud/test_create_server.py index 61528882f..48ccd9968 100644 --- a/openstack/tests/unit/cloud/test_create_server.py +++ b/openstack/tests/unit/cloud/test_create_server.py @@ -42,6 +42,7 @@ def test_create_server_with_get_exception(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), @@ -52,8 +53,8 @@ def test_create_server_with_get_exception(self): u'imageRef': u'image-id', u'max_count': 1, u'min_count': 1, - u'name': u'server-name'}})), - self.get_nova_discovery_mock_dict(), + u'name': u'server-name', + 'networks': 'auto'}})), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), @@ -76,6 +77,7 @@ def test_create_server_with_server_error(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), @@ -86,8 +88,8 @@ def test_create_server_with_server_error(self): u'imageRef': u'image-id', u'max_count': 1, u'min_count': 1, - u'name': u'server-name'}})), - self.get_nova_discovery_mock_dict(), + u'name': u'server-name', + 'networks': 'auto'}})), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), @@ -110,6 +112,7 @@ def test_create_server_wait_server_error(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), @@ -120,8 +123,8 @@ def test_create_server_wait_server_error(self): u'imageRef': u'image-id', u'max_count': 1, u'min_count': 1, - u'name': u'server-name'}})), - self.get_nova_discovery_mock_dict(), + u'name': u'server-name', + 'networks': 'auto'}})), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -150,6 +153,7 @@ def test_create_server_with_timeout(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), @@ -160,8 +164,8 @@ def test_create_server_with_timeout(self): u'imageRef': u'image-id', u'max_count': 1, u'min_count': 1, - u'name': u'server-name'}})), - self.get_nova_discovery_mock_dict(), + u'name': u'server-name', + 'networks': 'auto'}})), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -187,6 +191,7 @@ def test_create_server_no_wait(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), @@ -197,8 +202,8 @@ def test_create_server_no_wait(self): u'imageRef': u'image-id', u'max_count': 1, u'min_count': 1, - u'name': u'server-name'}})), - self.get_nova_discovery_mock_dict(), + u'name': u'server-name', + 'networks': 'auto'}})), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), @@ -224,6 +229,7 @@ def test_create_server_config_drive(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), @@ -235,8 +241,8 @@ def test_create_server_config_drive(self): u'config_drive': True, u'max_count': 1, u'min_count': 1, - u'name': u'server-name'}})), - self.get_nova_discovery_mock_dict(), + u'name': u'server-name', + 'networks': 'auto'}})), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), @@ -262,6 +268,7 @@ def test_create_server_config_drive_none(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), @@ -272,8 +279,8 @@ def test_create_server_config_drive_none(self): u'imageRef': u'image-id', u'max_count': 1, u'min_count': 1, - u'name': u'server-name'}})), - self.get_nova_discovery_mock_dict(), + u'name': u'server-name', + 'networks': 'auto'}})), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), @@ -303,6 +310,7 @@ def test_create_server_with_admin_pass_no_wait(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), @@ -314,8 +322,8 @@ def test_create_server_with_admin_pass_no_wait(self): u'imageRef': u'image-id', u'max_count': 1, u'min_count': 1, - u'name': u'server-name'}})), - self.get_nova_discovery_mock_dict(), + u'name': u'server-name', + 'networks': 'auto'}})), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), @@ -344,6 +352,7 @@ def test_create_server_with_admin_pass_wait(self, mock_wait): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), @@ -355,7 +364,8 @@ def test_create_server_with_admin_pass_wait(self, mock_wait): u'max_count': 1, u'min_count': 1, u'adminPass': admin_pass, - u'name': u'server-name'}})), + u'name': u'server-name', + 'networks': 'auto'}})), ]) # The wait returns non-password server @@ -391,6 +401,7 @@ def test_create_server_user_data_base64(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), @@ -402,8 +413,8 @@ def test_create_server_user_data_base64(self): u'max_count': 1, u'min_count': 1, u'user_data': user_data_b64, - u'name': u'server-name'}})), - self.get_nova_discovery_mock_dict(), + u'name': u'server-name', + 'networks': 'auto'}})), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), @@ -465,6 +476,7 @@ def test_create_server_wait(self, mock_wait): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), @@ -475,14 +487,24 @@ def test_create_server_wait(self, mock_wait): u'imageRef': u'image-id', u'max_count': 1, u'min_count': 1, - u'name': u'server-name'}})), + u'name': u'server-name', + 'networks': 'auto'}})), ]) self.cloud.create_server( 'server-name', dict(id='image-id'), dict(id='flavor-id'), wait=True), + # This is a pretty dirty hack to ensure we in principle use object with + # expected properties + srv = server.Server.existing( + connection=self.cloud, + min_count=1, max_count=1, + networks='auto', + imageRef='image-id', + flavorRef='flavor-id', + **fake_server) mock_wait.assert_called_once_with( - fake_server, + srv, auto_ip=True, ips=None, ip_pool=None, reuse=True, timeout=180, nat_destination=None, @@ -504,6 +526,7 @@ def test_create_server_no_addresses( uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), @@ -514,8 +537,8 @@ def test_create_server_no_addresses( u'imageRef': u'image-id', u'max_count': 1, u'min_count': 1, - u'name': u'server-name'}})), - self.get_nova_discovery_mock_dict(), + u'name': u'server-name', + 'networks': 'auto'}})), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', 'detail']), @@ -569,6 +592,7 @@ def test_create_server_network_with_no_nics(self): append=['v2.0', 'networks'], qs_elements=['name=network-name']), json={'networks': [network]}), + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), @@ -581,7 +605,6 @@ def test_create_server_network_with_no_nics(self): u'min_count': 1, u'networks': [{u'uuid': u'network-id'}], u'name': u'server-name'}})), - self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), @@ -622,6 +645,7 @@ def test_create_server_network_with_empty_nics(self): append=['v2.0', 'networks'], qs_elements=['name=network-name']), json={'networks': [network]}), + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), @@ -634,7 +658,6 @@ def test_create_server_network_with_empty_nics(self): u'min_count': 1, u'networks': [{u'uuid': u'network-id'}], u'name': u'server-name'}})), - self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), @@ -665,6 +688,7 @@ def test_create_server_network_fixed_ip(self): fixed_ip = '10.0.0.1' build_server = fakes.make_fake_server('1234', '', 'BUILD') self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), @@ -677,7 +701,6 @@ def test_create_server_network_fixed_ip(self): u'min_count': 1, u'networks': [{'fixed_ip': fixed_ip}], u'name': u'server-name'}})), - self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), @@ -708,6 +731,7 @@ def test_create_server_network_v4_fixed_ip(self): fixed_ip = '10.0.0.1' build_server = fakes.make_fake_server('1234', '', 'BUILD') self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), @@ -720,7 +744,6 @@ def test_create_server_network_v4_fixed_ip(self): u'min_count': 1, u'networks': [{'fixed_ip': fixed_ip}], u'name': u'server-name'}})), - self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), @@ -753,6 +776,7 @@ def test_create_server_network_v6_fixed_ip(self): fixed_ip = 'fe80::28da:5fff:fe57:13ed' build_server = fakes.make_fake_server('1234', '', 'BUILD') self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), @@ -765,7 +789,6 @@ def test_create_server_network_v6_fixed_ip(self): u'min_count': 1, u'networks': [{'fixed_ip': fixed_ip}], u'name': u'server-name'}})), - self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), @@ -857,6 +880,7 @@ def test_create_server_nics_port_id(self): port_id = uuid.uuid4().hex self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', append=['servers']), @@ -869,7 +893,6 @@ def test_create_server_nics_port_id(self): u'min_count': 1, u'networks': [{u'port': port_id}], u'name': u'server-name'}})), - self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), @@ -899,9 +922,10 @@ def test_create_boot_attach_volume(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( - 'compute', 'public', append=['os-volumes_boot']), + 'compute', 'public', append=['servers']), json={'server': build_server}, validate=dict( json={'server': { @@ -925,8 +949,8 @@ def test_create_boot_attach_volume(self): u'uuid': u'volume001' } ], - u'name': u'server-name'}})), - self.get_nova_discovery_mock_dict(), + u'name': u'server-name', + 'networks': 'auto'}})), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), @@ -952,9 +976,10 @@ def test_create_boot_from_volume_image_terminate(self): uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'networks']), json={'networks': []}), + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( - 'compute', 'public', append=['os-volumes_boot']), + 'compute', 'public', append=['servers']), json={'server': build_server}, validate=dict( json={'server': { @@ -969,8 +994,8 @@ def test_create_boot_from_volume_image_terminate(self): u'source_type': u'image', u'uuid': u'image-id', u'volume_size': u'1'}], - u'name': u'server-name'}})), - self.get_nova_discovery_mock_dict(), + u'name': u'server-name', + 'networks': 'auto'}})), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', '1234']), From 4831e22a02219e89a15db04d2bf2583b06dda346 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 22 Dec 2021 12:20:37 +0100 Subject: [PATCH 2984/3836] Switch quota methods of cloud layer to proxy Switch compute quota methods of the cloud layer to the compute proxy. Change-Id: Ied35c73a2876f46f7ffbbb36993b52ef3796b888 --- openstack/cloud/_compute.py | 44 +++++++---------------- openstack/compute/v2/_proxy.py | 6 ++++ openstack/compute/v2/quota_set.py | 3 ++ openstack/tests/unit/cloud/test_quotas.py | 39 +++++++++++++++----- 4 files changed, 52 insertions(+), 40 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 5742bc06e..f59d98e6b 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -27,6 +27,7 @@ from openstack.cloud import _utils from openstack.cloud import exc from openstack.cloud import meta +from openstack.compute.v2 import quota_set as _qs from openstack.compute.v2 import server as _server from openstack import exceptions from openstack import proxy @@ -1593,25 +1594,13 @@ def set_compute_quotas(self, name_or_id, **kwargs): :raises: OpenStackCloudException if the resource to set the quota does not exist. """ - - proj = self.get_project(name_or_id) - if not proj: - raise exc.OpenStackCloudException("project does not exist") - - # compute_quotas = {key: val for key, val in kwargs.items() - # if key in quota.COMPUTE_QUOTAS} - # TODO(ghe): Manage volume and network quotas - # network_quotas = {key: val for key, val in kwargs.items() - # if key in quota.NETWORK_QUOTAS} - # volume_quotas = {key: val for key, val in kwargs.items() - # if key in quota.VOLUME_QUOTAS} - + proj = self.identity.find_project( + name_or_id, ignore_missing=False) kwargs['force'] = True - proxy._json_response( - self.compute.put( - '/os-quota-sets/{project}'.format(project=proj.id), - json={'quota_set': kwargs}), - error_message="No valid quota or resource") + self.compute.update_quota_set( + _qs.QuotaSet(project_id=proj.id), + **kwargs + ) def get_compute_quotas(self, name_or_id): """ Get quota for a project @@ -1621,13 +1610,9 @@ def get_compute_quotas(self, name_or_id): :returns: Munch object with the quotas """ - proj = self.get_project(name_or_id) - if not proj: - raise exc.OpenStackCloudException("project does not exist") - data = proxy._json_response( - self.compute.get( - '/os-quota-sets/{project}'.format(project=proj.id))) - return self._get_and_munchify('quota_set', data) + proj = self.identity.find_project( + name_or_id, ignore_missing=False) + return self.compute.get_quota_set(proj) def delete_compute_quotas(self, name_or_id): """ Delete quota for a project @@ -1638,12 +1623,9 @@ def delete_compute_quotas(self, name_or_id): :returns: dict with the quotas """ - proj = self.get_project(name_or_id) - if not proj: - raise exc.OpenStackCloudException("project does not exist") - return proxy._json_response( - self.compute.delete( - '/os-quota-sets/{project}'.format(project=proj.id))) + proj = self.identity.find_project( + name_or_id, ignore_missing=False) + return self.compute.revert_quota_set(proj) def get_compute_usage(self, name_or_id, start=None, end=None): """ Get usage for a specific project diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 1760ff14e..682175174 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -1952,6 +1952,8 @@ def get_quota_set(self, project, usage=False, **query): project = self._get_resource(_project.Project, project) res = self._get_resource( _quota_set.QuotaSet, None, project_id=project.id) + if not query: + query = {} return res.fetch( self, usage=usage, **query) @@ -1986,6 +1988,8 @@ def revert_quota_set(self, project, **query): res = self._get_resource( _quota_set.QuotaSet, None, project_id=project.id) + if not query: + query = {} return res.delete(self, **query) def update_quota_set(self, quota_set, query=None, **attrs): @@ -2001,6 +2005,8 @@ def update_quota_set(self, quota_set, query=None, **attrs): :rtype: :class:`~openstack.compute.v2.quota_set.QuotaSet` """ res = self._get_resource(_quota_set.QuotaSet, quota_set, **attrs) + if not query: + query = {} return res.commit(self, **query) # ========== Utilities ========== diff --git a/openstack/compute/v2/quota_set.py b/openstack/compute/v2/quota_set.py index 890a819c1..86100de14 100644 --- a/openstack/compute/v2/quota_set.py +++ b/openstack/compute/v2/quota_set.py @@ -26,6 +26,9 @@ class QuotaSet(quota_set.QuotaSet): fixed_ips = resource.Body('fixed_ips', type=int) #: The number of allowed floating IP addresses for each tenant. floating_ips = resource.Body('floating_ips', type=int) + #: You can force the update even if the quota has already been used and + #: the reserved quota exceeds the new quota. + force = resource.Body('force', type=bool) #: The number of allowed bytes of content for each injected file. injected_file_content_bytes = resource.Body( 'injected_file_content_bytes', type=int) diff --git a/openstack/tests/unit/cloud/test_quotas.py b/openstack/tests/unit/cloud/test_quotas.py index ac38f44bd..a54124b6c 100644 --- a/openstack/tests/unit/cloud/test_quotas.py +++ b/openstack/tests/unit/cloud/test_quotas.py @@ -38,10 +38,15 @@ def setUp(self, cloud_config_fixture='clouds.yaml'): cloud_config_fixture=cloud_config_fixture) def test_update_quotas(self): - project = self.mock_for_keystone_projects(project_count=1, - list_get=True)[0] + project = self._get_project_data() self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'identity', 'public', + append=['v3', 'projects', project.project_id]), + json={'project': project.json_response['project']}), + self.get_nova_discovery_mock_dict(), dict(method='PUT', uri=self.get_mock_url( 'compute', 'public', @@ -60,10 +65,15 @@ def test_update_quotas(self): self.assert_calls() def test_update_quotas_bad_request(self): - project = self.mock_for_keystone_projects(project_count=1, - list_get=True)[0] + project = self._get_project_data() self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'identity', 'public', + append=['v3', 'projects', project.project_id]), + json={'project': project.json_response['project']}), + self.get_nova_discovery_mock_dict(), dict(method='PUT', uri=self.get_mock_url( 'compute', 'public', @@ -77,13 +87,19 @@ def test_update_quotas_bad_request(self): self.assert_calls() def test_get_quotas(self): - project = self.mock_for_keystone_projects(project_count=1, - list_get=True)[0] + project = self._get_project_data() self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'identity', 'public', + append=['v3', 'projects', project.project_id]), + json={'project': project.json_response['project']}), + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', - append=['os-quota-sets', project.project_id]), + append=['os-quota-sets', project.project_id], + qs_elements=['usage=False']), json={'quota_set': fake_quota_set}), ]) @@ -92,10 +108,15 @@ def test_get_quotas(self): self.assert_calls() def test_delete_quotas(self): - project = self.mock_for_keystone_projects(project_count=1, - list_get=True)[0] + project = self._get_project_data() self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'identity', 'public', + append=['v3', 'projects', project.project_id]), + json={'project': project.json_response['project']}), + self.get_nova_discovery_mock_dict(), dict(method='DELETE', uri=self.get_mock_url( 'compute', 'public', From 82f09b4eba4e4db58f98612421c3f08f62deb202 Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Mon, 18 Oct 2021 12:15:42 +0200 Subject: [PATCH 2985/3836] Reindentation of the docstrings for baremetal service Change-Id: I015d71a6dc1a424c0bb06e76b049d23a7869c76e --- openstack/baremetal/v1/_proxy.py | 173 +++++++++++++++---------------- 1 file changed, 84 insertions(+), 89 deletions(-) diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 0b9e21413..bdcb14949 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -36,8 +36,8 @@ def _get_with_fields(self, resource_type, value, fields=None): :param resource_type: The type of resource to get. :type resource_type: :class:`~openstack.resource.Resource` :param value: The value to get. Can be either the ID of a - resource or a :class:`~openstack.resource.Resource` - subclass. + resource or a :class:`~openstack.resource.Resource` + subclass. :param fields: Limit the resource fields to fetch. :returns: The result of the ``fetch`` @@ -57,7 +57,7 @@ def chassis(self, details=False, **query): """Retrieve a generator of chassis. :param details: A boolean indicating whether the detailed information - for every chassis should be returned. + for every chassis should be returned. :param dict query: Optional query parameters to be sent to restrict the chassis to be returned. Available parameters include: @@ -92,7 +92,7 @@ def create_chassis(self, **attrs): """Create a new chassis from attributes. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.baremetal.v1.chassis.Chassis`. + :class:`~openstack.baremetal.v1.chassis.Chassis`. :returns: The results of chassis creation. :rtype: :class:`~openstack.baremetal.v1.chassis.Chassis`. @@ -228,7 +228,7 @@ def nodes(self, details=False, **query): """Retrieve a generator of nodes. :param details: A boolean indicating whether the detailed information - for every node should be returned. + for every node should be returned. :param dict query: Optional query parameters to be sent to restrict the nodes returned. Available parameters include: @@ -277,7 +277,7 @@ def create_node(self, **attrs): """Create a new node from attributes. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.baremetal.v1.node.Node`. + :class:`~openstack.baremetal.v1.node.Node`. :returns: The results of node creation. :rtype: :class:`~openstack.baremetal.v1.node.Node`. @@ -344,9 +344,9 @@ def patch_node(self, node, patch, reset_interfaces=None, being locked. However, when setting ``instance_id``, this is a normal code and should not be retried. - See `Update Node - `_ - for details. + See `Update Node + `_ + for details. :returns: The updated node. :rtype: :class:`~openstack.baremetal.v1.node.Node` @@ -596,7 +596,7 @@ def ports(self, details=False, **query): """Retrieve a generator of ports. :param details: A boolean indicating whether the detailed information - for every port should be returned. + for every port should be returned. :param dict query: Optional query parameters to be sent to restrict the ports returned. Available parameters include: @@ -641,7 +641,7 @@ def create_port(self, **attrs): """Create a new port from attributes. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.baremetal.v1.port.Port`. + :class:`~openstack.baremetal.v1.port.Port`. :returns: The results of port creation. :rtype: :class:`~openstack.baremetal.v1.port.Port`. @@ -720,7 +720,7 @@ def port_groups(self, details=False, **query): """Retrieve a generator of port groups. :param details: A boolean indicating whether the detailed information - for every port group should be returned. + for every port group should be returned. :param dict query: Optional query parameters to be sent to restrict the port groups returned. Available parameters include: @@ -759,7 +759,7 @@ def create_port_group(self, **attrs): """Create a new portgroup from attributes. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.baremetal.v1.port_group.PortGroup`. + :class:`~openstack.baremetal.v1.port_group.PortGroup`. :returns: The results of portgroup creation. :rtype: :class:`~openstack.baremetal.v1.port_group.PortGroup`. @@ -873,9 +873,9 @@ def detach_vif_from_node(self, node, vif_id, ignore_missing=True): a :class:`~openstack.baremetal.v1.node.Node` instance. :param string vif_id: Backend-specific VIF ID. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the VIF does not exist. Otherwise, ``False`` - is returned. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the VIF does not exist. Otherwise, ``False`` + is returned. :return: ``True`` if the VIF was detached, otherwise ``False``. :raises: :exc:`~openstack.exceptions.NotSupported` if the server does not support the VIF API. @@ -936,7 +936,7 @@ def create_allocation(self, **attrs): """Create a new allocation from attributes. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.baremetal.v1.allocation.Allocation`. + :class:`~openstack.baremetal.v1.allocation.Allocation`. :returns: The results of allocation creation. :rtype: :class:`~openstack.baremetal.v1.allocation.Allocation`. @@ -1085,7 +1085,7 @@ def volume_connectors(self, details=False, **query): """Retrieve a generator of volume_connector. :param details: A boolean indicating whether the detailed information - for every volume_connector should be returned. + for every volume_connector should be returned. :param dict query: Optional query parameters to be sent to restrict the volume_connectors returned. Available parameters include: @@ -1124,12 +1124,11 @@ def create_volume_connector(self, **attrs): """Create a new volume_connector from attributes. :param dict attrs: Keyword arguments that will be used to create a - :class: - `~openstack.baremetal.v1.volume_connector.VolumeConnector`. + :class:`~openstack.baremetal.v1.volume_connector.VolumeConnector`. :returns: The results of volume_connector creation. - :rtype::class: - `~openstack.baremetal.v1.volume_connector.VolumeConnector`. + :rtype: + :class:`~openstack.baremetal.v1.volume_connector.VolumeConnector`. """ return self._create(_volumeconnector.VolumeConnector, **attrs) @@ -1143,8 +1142,8 @@ def find_volume_connector(self, vc_id, ignore_missing=True): when the volume connector does not exist. When set to `True``, None will be returned when attempting to find a nonexistent volume connector. - :returns: One :class: - `~openstack.baremetal.v1.volumeconnector.VolumeConnector` + :returns: One + :class:`~openstack.baremetal.v1.volumeconnector.VolumeConnector` object or None. """ return self._find(_volumeconnector.VolumeConnector, vc_id, @@ -1154,14 +1153,13 @@ def get_volume_connector(self, volume_connector, fields=None): """Get a specific volume_connector. :param volume_connector: The value can be the ID of a - volume_connector or a :class: - `~openstack.baremetal.v1.volume_connector.VolumeConnector - instance.` + volume_connector or a + :class:`~openstack.baremetal.v1.volume_connector.VolumeConnector` + instance. :param fields: Limit the resource fields to fetch.` :returns: One - :class: - `~openstack.baremetal.v1.volume_connector.VolumeConnector` + :class: `~openstack.baremetal.v1.volume_connector.VolumeConnector` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no volume_connector matching the name or ID could be found.` """ @@ -1172,15 +1170,16 @@ def get_volume_connector(self, volume_connector, fields=None): def update_volume_connector(self, volume_connector, **attrs): """Update a volume_connector. - :param volume_connector:Either the ID of a volume_connector - or an instance of - :class:`~openstack.baremetal.v1.volume_connector.VolumeConnector.` + :param volume_connector: Either the ID of a volume_connector + or an instance of + :class:`~openstack.baremetal.v1.volume_connector.VolumeConnector`. :param dict attrs: The attributes to update on the - volume_connector represented by the ``volume_connector`` parameter.` + volume_connector represented by the ``volume_connector`` + parameter. :returns: The updated volume_connector. - :rtype::class: - `~openstack.baremetal.v1.volume_connector.VolumeConnector.` + :rtype: + :class:`~openstack.baremetal.v1.volume_connector.VolumeConnector` """ return self._update(_volumeconnector.VolumeConnector, volume_connector, **attrs) @@ -1189,14 +1188,14 @@ def patch_volume_connector(self, volume_connector, patch): """Apply a JSON patch to the volume_connector. :param volume_connector: The value can be the ID of a - volume_connector or a :class: - `~openstack.baremetal.v1.volume_connector.VolumeConnector` + volume_connector or a + :class:`~openstack.baremetal.v1.volume_connector.VolumeConnector` instance. :param patch: JSON patch to apply. :returns: The updated volume_connector. - :rtype::class: - `~openstack.baremetal.v1.volume_connector.VolumeConnector.` + :rtype: + :class:`~openstack.baremetal.v1.volume_connector.VolumeConnector.` """ return self._get_resource(_volumeconnector.VolumeConnector, volume_connector).patch(self, patch) @@ -1207,8 +1206,7 @@ def delete_volume_connector(self, volume_connector, :param volume_connector: The value can be either the ID of a volume_connector.VolumeConnector or a - :class: - `~openstack.baremetal.v1.volume_connector.VolumeConnector` + :class:`~openstack.baremetal.v1.volume_connector.VolumeConnector` instance. :param bool ignore_missing: When set to ``False``, an exception :class:`~openstack.exceptions.ResourceNotFound` will be raised @@ -1217,8 +1215,8 @@ def delete_volume_connector(self, volume_connector, attempting to delete a non-existent volume_connector. :returns: The instance of the volume_connector which was deleted. - :rtype::class: - `~openstack.baremetal.v1.volume_connector.VolumeConnector`. + :rtype: + :class:`~openstack.baremetal.v1.volume_connector.VolumeConnector`. """ return self._delete(_volumeconnector.VolumeConnector, volume_connector, ignore_missing=ignore_missing) @@ -1227,7 +1225,7 @@ def volume_targets(self, details=False, **query): """Retrieve a generator of volume_target. :param details: A boolean indicating whether the detailed information - for every volume_target should be returned. + for every volume_target should be returned. :param dict query: Optional query parameters to be sent to restrict the volume_targets returned. Available parameters include: @@ -1266,12 +1264,11 @@ def create_volume_target(self, **attrs): """Create a new volume_target from attributes. :param dict attrs: Keyword arguments that will be used to create a - :class: - `~openstack.baremetal.v1.volume_target.VolumeTarget`. + :class:`~openstack.baremetal.v1.volume_target.VolumeTarget`. :returns: The results of volume_target creation. - :rtype::class: - `~openstack.baremetal.v1.volume_target.VolumeTarget`. + :rtype: + :class:`~openstack.baremetal.v1.volume_target.VolumeTarget`. """ return self._create(_volumetarget.VolumeTarget, **attrs) @@ -1285,8 +1282,8 @@ def find_volume_target(self, vt_id, ignore_missing=True): when the volume connector does not exist. When set to `True``, None will be returned when attempting to find a nonexistent volume target. - :returns: One :class: - `~openstack.baremetal.v1.volumetarget.VolumeTarget` + :returns: One + :class:`~openstack.baremetal.v1.volumetarget.VolumeTarget` object or None. """ return self._find(_volumetarget.VolumeTarget, vt_id, @@ -1296,14 +1293,13 @@ def get_volume_target(self, volume_target, fields=None): """Get a specific volume_target. :param volume_target: The value can be the ID of a - volume_target or a :class: - `~openstack.baremetal.v1.volume_target.VolumeTarget - instance.` + volume_target or a + :class:`~openstack.baremetal.v1.volume_target.VolumeTarget` + instance. :param fields: Limit the resource fields to fetch.` :returns: One - :class: - `~openstack.baremetal.v1.volume_target.VolumeTarget` + :class:`~openstack.baremetal.v1.volume_target.VolumeTarget` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no volume_target matching the name or ID could be found.` """ @@ -1314,15 +1310,15 @@ def get_volume_target(self, volume_target, fields=None): def update_volume_target(self, volume_target, **attrs): """Update a volume_target. - :param volume_target:Either the ID of a volume_target - or an instance of - :class:`~openstack.baremetal.v1.volume_target.VolumeTarget.` + :param volume_target: Either the ID of a volume_target + or an instance of + :class:`~openstack.baremetal.v1.volume_target.VolumeTarget`. :param dict attrs: The attributes to update on the - volume_target represented by the ``volume_target`` parameter.` + volume_target represented by the ``volume_target`` parameter. :returns: The updated volume_target. - :rtype::class: - `~openstack.baremetal.v1.volume_target.VolumeTarget.` + :rtype: + :class:`~openstack.baremetal.v1.volume_target.VolumeTarget` """ return self._update(_volumetarget.VolumeTarget, volume_target, **attrs) @@ -1331,14 +1327,14 @@ def patch_volume_target(self, volume_target, patch): """Apply a JSON patch to the volume_target. :param volume_target: The value can be the ID of a - volume_target or a :class: - `~openstack.baremetal.v1.volume_target.VolumeTarget` + volume_target or a + :class:`~openstack.baremetal.v1.volume_target.VolumeTarget` instance. :param patch: JSON patch to apply. :returns: The updated volume_target. - :rtype::class: - `~openstack.baremetal.v1.volume_target.VolumeTarget.` + :rtype: + :class:`~openstack.baremetal.v1.volume_target.VolumeTarget.` """ return self._get_resource(_volumetarget.VolumeTarget, volume_target).patch(self, patch) @@ -1349,8 +1345,7 @@ def delete_volume_target(self, volume_target, :param volume_target: The value can be either the ID of a volume_target.VolumeTarget or a - :class: - `~openstack.baremetal.v1.volume_target.VolumeTarget` + :class:`~openstack.baremetal.v1.volume_target.VolumeTarget` instance. :param bool ignore_missing: When set to ``False``, an exception :class:`~openstack.exceptions.ResourceNotFound` will be raised @@ -1359,8 +1354,8 @@ def delete_volume_target(self, volume_target, attempting to delete a non-existent volume_target. :returns: The instance of the volume_target which was deleted. - :rtype::class: - `~openstack.baremetal.v1.volume_target.VolumeTarget`. + :rtype: + :class:`~openstack.baremetal.v1.volume_target.VolumeTarget`. """ return self._delete(_volumetarget.VolumeTarget, volume_target, ignore_missing=ignore_missing) @@ -1369,9 +1364,9 @@ def deploy_templates(self, details=False, **query): """Retrieve a generator of deploy_templates. :param details: A boolean indicating whether the detailed information - for every deploy_templates should be returned. + for every deploy_templates should be returned. :param dict query: Optional query parameters to be sent to - restrict the deploy_templates to be returned. + restrict the deploy_templates to be returned. :returns: A generator of Deploy templates instances. """ @@ -1383,11 +1378,11 @@ def create_deploy_template(self, **attrs): """Create a new deploy_template from attributes. :param dict attrs: Keyword arguments that will be used to create a - :class:`~openstack.baremetal.v1.deploy_templates.DeployTemplate`. + :class:`~openstack.baremetal.v1.deploy_templates.DeployTemplate`. :returns: The results of deploy_template creation. :rtype: - :class:`~openstack.baremetal.v1.deploy_templates.DeployTemplate`. + :class:`~openstack.baremetal.v1.deploy_templates.DeployTemplate`. """ return self._create(_deploytemplates.DeployTemplate, **attrs) @@ -1395,15 +1390,15 @@ def update_deploy_template(self, deploy_template, **attrs): """Update a deploy_template. :param deploy_template: Either the ID of a deploy_template, - or an instance of - :class:`~openstack.baremetal.v1.deploy_templates.DeployTemplate`. + or an instance of + :class:`~openstack.baremetal.v1.deploy_templates.DeployTemplate`. :param dict attrs: The attributes to update on - the deploy_template represented - by the ``deploy_template`` parameter. + the deploy_template represented + by the ``deploy_template`` parameter. :returns: The updated deploy_template. - :rtype::class: - `~openstack.baremetal.v1.deploy_templates.DeployTemplate` + :rtype: + :class:`~openstack.baremetal.v1.deploy_templates.DeployTemplate` """ return self._update(_deploytemplates.DeployTemplate, deploy_template, **attrs) @@ -1413,9 +1408,9 @@ def delete_deploy_template(self, deploy_template, """Delete a deploy_template. :param deploy_template:The value can be - either the ID of a deploy_template or a - :class:`~openstack.baremetal.v1.deploy_templates.DeployTemplate` - instance. + either the ID of a deploy_template or a + :class:`~openstack.baremetal.v1.deploy_templates.DeployTemplate` + instance. :param bool ignore_missing: When set to ``False``, an exception:class:`~openstack.exceptions.ResourceNotFound` @@ -1427,8 +1422,8 @@ def delete_deploy_template(self, deploy_template, deploy_template. :returns: The instance of the deploy_template which was deleted. - :rtype::class: - `~openstack.baremetal.v1.deploy_templates.DeployTemplate`. + :rtype: + :class:`~openstack.baremetal.v1.deploy_templates.DeployTemplate`. """ return self._delete(_deploytemplates.DeployTemplate, @@ -1445,10 +1440,10 @@ def get_deploy_template(self, deploy_template, fields=None): :param fields: Limit the resource fields to fetch. :returns: One - :class:`~openstack.baremetal.v1.deploy_templates.DeployTemplate` + :class:`~openstack.baremetal.v1.deploy_templates.DeployTemplate` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no deployment template matching the name or - ID could be found. + when no deployment template matching the name or + ID could be found. """ return self._get_with_fields(_deploytemplates.DeployTemplate, deploy_template, fields=fields) @@ -1464,8 +1459,8 @@ def patch_deploy_template(self, deploy_template, patch): :param patch: JSON patch to apply. :returns: The updated deploy_template. - :rtype::class: - `~openstack.baremetal.v1.deploy_templates.DeployTemplate` + :rtype: + :class:`~openstack.baremetal.v1.deploy_templates.DeployTemplate` """ return self._get_resource(_deploytemplates.DeployTemplate, deploy_template).patch(self, patch) From efdc8c2a2bff482cfd8ec28b22a7512ded09a964 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 23 Dec 2021 13:47:05 +0100 Subject: [PATCH 2986/3836] Switch unittests for BS to use v3 Complete switch to use cinder v3 by default. Change-Id: I76328290525f694a8860a60cc8da707305ce48c7 --- openstack/cloud/_block_storage.py | 10 +--- openstack/tests/unit/cloud/test_caching.py | 26 +++++----- .../unit/cloud/test_create_volume_snapshot.py | 18 +++---- .../unit/cloud/test_delete_volume_snapshot.py | 14 +++--- openstack/tests/unit/cloud/test_image.py | 4 +- openstack/tests/unit/cloud/test_quotas.py | 6 +-- openstack/tests/unit/cloud/test_volume.py | 48 +++++++++---------- .../tests/unit/cloud/test_volume_access.py | 30 ++++++------ .../tests/unit/cloud/test_volume_backups.py | 30 ++++++------ .../unit/fixtures/block-storage-version.json | 10 ++-- .../tests/unit/fixtures/clouds/clouds.yaml | 1 - 11 files changed, 95 insertions(+), 102 deletions(-) diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index ce8c3ce4d..ccd11b322 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -364,15 +364,9 @@ def _get_volume_kwargs(self, kwargs): description = kwargs.pop('description', kwargs.pop('display_description', None)) if name: - if self.block_storage._version_matches(2): - kwargs['name'] = name - else: - kwargs['display_name'] = name + kwargs['name'] = name if description: - if self.block_storage._version_matches(2): - kwargs['description'] = description - else: - kwargs['display_description'] = description + kwargs['description'] = description return kwargs @_utils.valid_kwargs('name', 'display_name', diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py index 47c165653..de0a1453a 100644 --- a/openstack/tests/unit/cloud/test_caching.py +++ b/openstack/tests/unit/cloud/test_caching.py @@ -193,11 +193,11 @@ def test_list_volumes(self): self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', 'detail']), + 'volumev3', 'public', append=['volumes', 'detail']), json={'volumes': [fake_volume_dict]}), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', 'detail']), + 'volumev3', 'public', append=['volumes', 'detail']), json={'volumes': [fake_volume_dict, fake_volume2_dict]})]) for a, b in zip([fake_volume_dict], @@ -224,11 +224,11 @@ def test_list_volumes_creating_invalidates(self): self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', 'detail']), + 'volumev3', 'public', append=['volumes', 'detail']), json={'volumes': [fake_volume_dict]}), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', 'detail']), + 'volumev3', 'public', append=['volumes', 'detail']), json={'volumes': [fake_volume_dict, fake_volume2_dict]})]) for a, b in zip([fake_volume_dict], self.cloud.list_volumes()): @@ -254,40 +254,40 @@ def now_deleting(request, context): self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', 'detail']), + 'volumev3', 'public', append=['volumes', 'detail']), json={'volumes': [fake_volb4]}), dict(method='POST', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes']), + 'volumev3', 'public', append=['volumes']), json={'volume': fake_vol_creating}), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', _id]), + 'volumev3', 'public', append=['volumes', _id]), json={'volume': fake_vol_creating}), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', _id]), + 'volumev3', 'public', append=['volumes', _id]), json={'volume': fake_vol_avail}), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', 'detail']), + 'volumev3', 'public', append=['volumes', 'detail']), json={'volumes': [fake_volb4, fake_vol_avail]}), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['volumes', _id]), json={'volume': fake_vol_avail}), dict(method='DELETE', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', _id]), + 'volumev3', 'public', append=['volumes', _id]), json=now_deleting), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', _id]), + 'volumev3', 'public', append=['volumes', _id]), status_code=404), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', 'detail']), + 'volumev3', 'public', append=['volumes', 'detail']), json={'volumes': [fake_volb4, fake_vol_avail]}), ]) diff --git a/openstack/tests/unit/cloud/test_create_volume_snapshot.py b/openstack/tests/unit/cloud/test_create_volume_snapshot.py index c205f308a..e77f18905 100644 --- a/openstack/tests/unit/cloud/test_create_volume_snapshot.py +++ b/openstack/tests/unit/cloud/test_create_volume_snapshot.py @@ -17,7 +17,7 @@ Tests for the `create_volume_snapshot` command. """ -from openstack.block_storage.v2 import snapshot +from openstack.block_storage.v3 import snapshot from openstack.cloud import exc from openstack.cloud import meta from openstack.tests import fakes @@ -52,16 +52,16 @@ def test_create_volume_snapshot_wait(self): self.register_uris([ dict(method='POST', uri=self.get_mock_url( - 'volumev2', 'public', append=['snapshots']), + 'volumev3', 'public', append=['snapshots']), json={'snapshot': build_snapshot_dict}, validate=dict(json={ 'snapshot': {'volume_id': '1234'}})), dict(method='GET', - uri=self.get_mock_url('volumev2', 'public', + uri=self.get_mock_url('volumev3', 'public', append=['snapshots', snapshot_id]), json={'snapshot': build_snapshot_dict}), dict(method='GET', - uri=self.get_mock_url('volumev2', 'public', + uri=self.get_mock_url('volumev3', 'public', append=['snapshots', snapshot_id]), json={'snapshot': fake_snapshot_dict})]) @@ -84,12 +84,12 @@ def test_create_volume_snapshot_with_timeout(self): self.register_uris([ dict(method='POST', uri=self.get_mock_url( - 'volumev2', 'public', append=['snapshots']), + 'volumev3', 'public', append=['snapshots']), json={'snapshot': build_snapshot_dict}, validate=dict(json={ 'snapshot': {'volume_id': '1234'}})), dict(method='GET', - uri=self.get_mock_url('volumev2', 'public', + uri=self.get_mock_url('volumev3', 'public', append=['snapshots', snapshot_id]), json={'snapshot': build_snapshot_dict})]) @@ -116,16 +116,16 @@ def test_create_volume_snapshot_with_error(self): self.register_uris([ dict(method='POST', uri=self.get_mock_url( - 'volumev2', 'public', append=['snapshots']), + 'volumev3', 'public', append=['snapshots']), json={'snapshot': build_snapshot_dict}, validate=dict(json={ 'snapshot': {'volume_id': '1234'}})), dict(method='GET', - uri=self.get_mock_url('volumev2', 'public', + uri=self.get_mock_url('volumev3', 'public', append=['snapshots', snapshot_id]), json={'snapshot': build_snapshot_dict}), dict(method='GET', - uri=self.get_mock_url('volumev2', 'public', + uri=self.get_mock_url('volumev3', 'public', append=['snapshots', snapshot_id]), json={'snapshot': error_snapshot_dict})]) diff --git a/openstack/tests/unit/cloud/test_delete_volume_snapshot.py b/openstack/tests/unit/cloud/test_delete_volume_snapshot.py index 5904e40b9..1b607d21c 100644 --- a/openstack/tests/unit/cloud/test_delete_volume_snapshot.py +++ b/openstack/tests/unit/cloud/test_delete_volume_snapshot.py @@ -41,12 +41,12 @@ def test_delete_volume_snapshot(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['snapshots', 'detail']), json={'snapshots': [fake_snapshot_dict]}), dict(method='DELETE', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['snapshots', fake_snapshot_dict['id']]))]) self.assertTrue( @@ -64,12 +64,12 @@ def test_delete_volume_snapshot_with_error(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['snapshots', 'detail']), json={'snapshots': [fake_snapshot_dict]}), dict(method='DELETE', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['snapshots', fake_snapshot_dict['id']]), status_code=404)]) @@ -90,16 +90,16 @@ def test_delete_volume_snapshot_with_timeout(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['snapshots', 'detail']), json={'snapshots': [fake_snapshot_dict]}), dict(method='DELETE', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['snapshots', fake_snapshot_dict['id']])), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['snapshots', '1234']), json={'snapshot': fake_snapshot_dict}), ]) diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 9947c1312..6b24b59c8 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -1395,7 +1395,7 @@ def test_create_image_volume(self): self.get_cinder_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( - 'volumev2', append=['volumes', self.volume_id, 'action']), + 'volumev3', append=['volumes', self.volume_id, 'action']), json={'os-volume_upload_image': {'image_id': self.image_id}}, validate=dict(json={ u'os-volume_upload_image': { @@ -1427,7 +1427,7 @@ def test_create_image_volume_duplicate(self): self.get_cinder_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( - 'volumev2', append=['volumes', self.volume_id, 'action']), + 'volumev3', append=['volumes', self.volume_id, 'action']), json={'os-volume_upload_image': {'image_id': self.image_id}}, validate=dict(json={ u'os-volume_upload_image': { diff --git a/openstack/tests/unit/cloud/test_quotas.py b/openstack/tests/unit/cloud/test_quotas.py index ac38f44bd..9671b2e67 100644 --- a/openstack/tests/unit/cloud/test_quotas.py +++ b/openstack/tests/unit/cloud/test_quotas.py @@ -113,7 +113,7 @@ def test_cinder_update_quotas(self): self.get_cinder_discovery_mock_dict(), dict(method='PUT', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['os-quota-sets', project.project_id]), json=dict(quota_set={'volumes': 1}), validate=dict( @@ -130,7 +130,7 @@ def test_cinder_get_quotas(self): self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['os-quota-sets', project.project_id]), json=dict(quota_set={'snapshots': 10, 'volumes': 20}))]) self.cloud.get_volume_quotas(project.project_id) @@ -143,7 +143,7 @@ def test_cinder_delete_quotas(self): self.get_cinder_discovery_mock_dict(), dict(method='DELETE', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['os-quota-sets', project.project_id]))]) self.cloud.delete_volume_quotas(project.project_id) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_volume.py b/openstack/tests/unit/cloud/test_volume.py index b3c61bee1..0d5f1baf6 100644 --- a/openstack/tests/unit/cloud/test_volume.py +++ b/openstack/tests/unit/cloud/test_volume.py @@ -106,11 +106,11 @@ def test_attach_volume_wait(self): self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', vol['id']]), + 'volumev3', 'public', append=['volumes', vol['id']]), json={'volume': volume}), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', vol['id']]), + 'volumev3', 'public', append=['volumes', vol['id']]), json={'volume': attached_volume}) ]) # defaults to wait=True @@ -141,11 +141,11 @@ def test_attach_volume_wait_error(self): self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', volume['id']]), + 'volumev3', 'public', append=['volumes', volume['id']]), json={'volume': errored_volume}), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', volume['id']]), + 'volumev3', 'public', append=['volumes', volume['id']]), json={'volume': errored_volume}) ]) @@ -238,7 +238,7 @@ def test_detach_volume_wait(self): self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', 'detail']), + 'volumev3', 'public', append=['volumes', 'detail']), json={'volumes': [avail_volume]})]) self.cloud.detach_volume(server, volume) self.assert_calls() @@ -262,11 +262,11 @@ def test_detach_volume_wait_error(self): self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', 'detail']), + 'volumev3', 'public', append=['volumes', 'detail']), json={'volumes': [errored_volume]}), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['volumes', errored_volume['id']]), json={'volume': errored_volume}) ]) @@ -284,14 +284,14 @@ def test_delete_volume_deletes(self): self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', volume.id]), + 'volumev3', 'public', append=['volumes', volume.id]), json={'volumes': [volume]}), dict(method='DELETE', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', volume.id])), + 'volumev3', 'public', append=['volumes', volume.id])), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', volume.id]), + 'volumev3', 'public', append=['volumes', volume.id]), status_code=404)]) self.assertTrue(self.cloud.delete_volume(volume['id'])) self.assert_calls() @@ -304,15 +304,15 @@ def test_delete_volume_gone_away(self): self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', volume.id]), + 'volumev3', 'public', append=['volumes', volume.id]), json=volume), dict(method='DELETE', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', volume.id]), + 'volumev3', 'public', append=['volumes', volume.id]), status_code=404), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', volume.id]), + 'volumev3', 'public', append=['volumes', volume.id]), status_code=404), ]) self.assertTrue(self.cloud.delete_volume(volume['id'])) @@ -326,17 +326,17 @@ def test_delete_volume_force(self): self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', volume['id']]), + 'volumev3', 'public', append=['volumes', volume['id']]), json={'volumes': [volume]}), dict(method='POST', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['volumes', volume.id, 'action']), validate=dict( json={'os-force_delete': {}})), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', volume['id']]), + 'volumev3', 'public', append=['volumes', volume['id']]), status_code=404)]) self.assertTrue(self.cloud.delete_volume(volume['id'], force=True)) self.assert_calls() @@ -349,11 +349,11 @@ def test_set_volume_bootable(self): self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', 'detail']), + 'volumev3', 'public', append=['volumes', 'detail']), json={'volumes': [volume]}), dict(method='POST', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['volumes', volume.id, 'action']), json={'os-set_bootable': {'bootable': True}}), ]) @@ -368,11 +368,11 @@ def test_set_volume_bootable_false(self): self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes', 'detail']), + 'volumev3', 'public', append=['volumes', 'detail']), json={'volumes': [volume]}), dict(method='POST', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['volumes', volume.id, 'action']), json={'os-set_bootable': {'bootable': False}}), ]) @@ -385,7 +385,7 @@ def test_get_volume_by_id(self): self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['volumes', '01']), json={'volume': vol1} ) @@ -399,7 +399,7 @@ def test_create_volume(self): self.get_cinder_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes']), + 'volumev3', 'public', append=['volumes']), json={'volume': vol1}, validate=dict(json={ 'volume': { @@ -417,7 +417,7 @@ def test_create_bootable_volume(self): self.get_cinder_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( - 'volumev2', 'public', append=['volumes']), + 'volumev3', 'public', append=['volumes']), json={'volume': vol1}, validate=dict(json={ 'volume': { @@ -426,7 +426,7 @@ def test_create_bootable_volume(self): }})), dict(method='POST', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['volumes', '01', 'action']), validate=dict( json={'os-set_bootable': {'bootable': True}})), diff --git a/openstack/tests/unit/cloud/test_volume_access.py b/openstack/tests/unit/cloud/test_volume_access.py index f4f9ffffe..8768df694 100644 --- a/openstack/tests/unit/cloud/test_volume_access.py +++ b/openstack/tests/unit/cloud/test_volume_access.py @@ -31,7 +31,7 @@ def test_list_volume_types(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['types']), json={'volume_types': [volume_type]})]) self.assertTrue(self.cloud.list_volume_types()) @@ -44,7 +44,7 @@ def test_get_volume_type(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['types']), json={'volume_types': [volume_type]})]) volume_type_got = self.cloud.get_volume_type(volume_type['name']) @@ -61,12 +61,12 @@ def test_get_volume_type_access(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['types']), json={'volume_types': [volume_type]}), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['types', volume_type['id'], 'os-volume-type-access']), json={'volume_type_access': volume_type_access})]) @@ -86,23 +86,23 @@ def test_remove_volume_type_access(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['types']), json={'volume_types': [volume_type]}), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['types', volume_type['id'], 'os-volume-type-access']), json={'volume_type_access': volume_type_access}), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['types']), json={'volume_types': [volume_type]}), dict(method='POST', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['types', volume_type['id'], 'action']), json={'removeProjectAccess': { 'project': project_001['project_id']}}, @@ -111,12 +111,12 @@ def test_remove_volume_type_access(self): 'project': project_001['project_id']}})), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['types']), json={'volume_types': [volume_type]}), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['types', volume_type['id'], 'os-volume-type-access']), json={'volume_type_access': [project_001]})]) @@ -141,12 +141,12 @@ def test_add_volume_type_access(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['types']), json={'volume_types': [volume_type]}), dict(method='POST', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['types', volume_type['id'], 'action']), json={'addProjectAccess': { 'project': project_002['project_id']}}, @@ -155,12 +155,12 @@ def test_add_volume_type_access(self): 'project': project_002['project_id']}})), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['types']), json={'volume_types': [volume_type]}), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['types', volume_type['id'], 'os-volume-type-access']), json={'volume_type_access': volume_type_access})]) @@ -179,7 +179,7 @@ def test_add_volume_type_access_missing(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['types']), json={'volume_types': [volume_type]})]) with testtools.ExpectedException( diff --git a/openstack/tests/unit/cloud/test_volume_backups.py b/openstack/tests/unit/cloud/test_volume_backups.py index 865d7fb7c..5329f3934 100644 --- a/openstack/tests/unit/cloud/test_volume_backups.py +++ b/openstack/tests/unit/cloud/test_volume_backups.py @@ -9,7 +9,7 @@ # 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.block_storage.v2 import backup +from openstack.block_storage.v3 import backup from openstack.tests.unit import base @@ -31,7 +31,7 @@ def test_search_volume_backups(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['backups', 'detail']), + 'volumev3', 'public', append=['backups', 'detail']), json={"backups": [vol1, vol2, vol3]})]) result = self.cloud.search_volume_backups( name, {'availability_zone': 'az1'}) @@ -48,7 +48,7 @@ def test_get_volume_backup(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['backups', 'detail']), + 'volumev3', 'public', append=['backups', 'detail']), json={"backups": [vol1, vol2, vol3]})]) result = self.cloud.get_volume_backup( name, {'availability_zone': 'az1'}) @@ -62,7 +62,7 @@ def test_list_volume_backups(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', append=['backups', 'detail'], + 'volumev3', 'public', append=['backups', 'detail'], qs_elements=['='.join(i) for i in search_opts.items()]), json={"backups": [backup]})]) result = self.cloud.list_volume_backups(True, search_opts) @@ -77,21 +77,21 @@ def test_delete_volume_backup_wait(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['backups', 'detail']), json={"backups": [backup]}), dict(method='DELETE', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['backups', backup_id])), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['backups', backup_id]), json={"backup": backup}), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['backups', backup_id]), status_code=404)]) self.cloud.delete_volume_backup(backup_id, False, True, 1) @@ -103,23 +103,23 @@ def test_delete_volume_backup_force(self): self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['backups', 'detail']), json={"backups": [backup]}), dict(method='POST', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['backups', backup_id, 'action']), json={'os-force_delete': {}}, validate=dict(json={u'os-force_delete': {}})), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['backups', backup_id]), json={"backup": backup}), dict(method='GET', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['backups', backup_id]), status_code=404) ]) @@ -139,7 +139,7 @@ def test_create_volume_backup(self): self.register_uris([ dict(method='POST', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['backups']), json={'backup': bak1}, validate=dict(json={ @@ -169,7 +169,7 @@ def test_create_incremental_volume_backup(self): self.register_uris([ dict(method='POST', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['backups']), json={'backup': bak1}, validate=dict(json={ @@ -201,7 +201,7 @@ def test_create_volume_backup_from_snapshot(self): self.register_uris([ dict(method='POST', uri=self.get_mock_url( - 'volumev2', 'public', + 'volumev3', 'public', append=['backups']), json={'backup': bak1}, validate=dict(json={ diff --git a/openstack/tests/unit/fixtures/block-storage-version.json b/openstack/tests/unit/fixtures/block-storage-version.json index e75ddfd44..f6daa6e85 100644 --- a/openstack/tests/unit/fixtures/block-storage-version.json +++ b/openstack/tests/unit/fixtures/block-storage-version.json @@ -10,19 +10,19 @@ "rel": "describedby" }, { - "href": "https://volume.example.com/v2/", + "href": "https://volume.example.com/v3/", "rel": "self" } ], - "min_version": "", - "version": "", + "min_version": "3.0", + "version": "3.0", "media-types": [ { "base": "application/json", - "type": "application/vnd.openstack.volume+json;version=2" + "type": "application/vnd.openstack.volume+json;version=3" } ], - "id": "v2.0" + "id": "v3.0" } ] } diff --git a/openstack/tests/unit/fixtures/clouds/clouds.yaml b/openstack/tests/unit/fixtures/clouds/clouds.yaml index 99bae938f..ebd4cc0ba 100644 --- a/openstack/tests/unit/fixtures/clouds/clouds.yaml +++ b/openstack/tests/unit/fixtures/clouds/clouds.yaml @@ -7,7 +7,6 @@ clouds: username: admin user_domain_name: default project_domain_name: default - block_storage_api_version: 2 region_name: RegionOne _test_cloud_v2_: auth: From ee24cbffe478984ab5f1286da65d94c5d7c2e50b Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 23 Dec 2021 13:48:22 +0100 Subject: [PATCH 2987/3836] Switch quota_set cloud layer BS methods to proxy - Switch quota_set operations of the cloud layer block_storage to corresponding proxy implementation Change-Id: I58d70fb9ca961adc0e78a781b1be61956680a5b0 --- openstack/block_storage/v2/_proxy.py | 4 +++ openstack/block_storage/v3/_proxy.py | 4 +++ openstack/cloud/_block_storage.py | 43 ++++++++--------------- openstack/tests/unit/cloud/test_quotas.py | 33 ++++++++++++----- 4 files changed, 47 insertions(+), 37 deletions(-) diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index b98601375..358ae1ea7 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -609,6 +609,8 @@ def revert_quota_set(self, project, **query): res = self._get_resource( _quota_set.QuotaSet, None, project_id=project.id) + if not query: + query = {} return res.delete(self, **query) def update_quota_set(self, quota_set, query=None, **attrs): @@ -624,6 +626,8 @@ def update_quota_set(self, quota_set, query=None, **attrs): :rtype: :class:`~openstack.block_storage.v2.quota_set.QuotaSet` """ res = self._get_resource(_quota_set.QuotaSet, quota_set, **attrs) + if not query: + query = {} return res.commit(self, **query) def get_volume_metadata(self, volume): diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 028e23f93..61232b777 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -1079,6 +1079,8 @@ def revert_quota_set(self, project, **query): res = self._get_resource( _quota_set.QuotaSet, None, project_id=project.id) + if not query: + query = {} return res.delete(self, **query) def update_quota_set(self, quota_set, query=None, **attrs): @@ -1094,6 +1096,8 @@ def update_quota_set(self, quota_set, query=None, **attrs): :rtype: :class:`~openstack.block_storage.v3.quota_set.QuotaSet` """ res = self._get_resource(_quota_set.QuotaSet, quota_set, **attrs) + if not query: + query = {} return res.commit(self, **query) def _get_cleanup_dependencies(self): diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index ccd11b322..b58e24017 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -15,6 +15,7 @@ # openstack.resource.Resource.list and openstack.resource2.Resource.list import types # noqa +from openstack.block_storage.v3 import quota_set as _qs from openstack.cloud import _normalize from openstack.cloud import _utils from openstack.cloud import exc @@ -649,17 +650,12 @@ def set_volume_quotas(self, name_or_id, **kwargs): quota does not exist. """ - proj = self.get_project(name_or_id) - if not proj: - raise exc.OpenStackCloudException("project does not exist") + proj = self.identity.find_project( + name_or_id, ignore_missing=False) - kwargs['tenant_id'] = proj.id - resp = self.block_storage.put( - '/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id), - json={'quota_set': kwargs}) - proxy._json_response( - resp, - error_message="No valid quota or resource") + self.block_storage.update_quota_set( + _qs.QuotaSet(project_id=proj.id), + **kwargs) def get_volume_quotas(self, name_or_id): """ Get volume quotas for a project @@ -669,16 +665,11 @@ def get_volume_quotas(self, name_or_id): :returns: Munch object with the quotas """ - proj = self.get_project(name_or_id) - if not proj: - raise exc.OpenStackCloudException("project does not exist") + proj = self.identity.find_project( + name_or_id, ignore_missing=False) - resp = self.block_storage.get( - '/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id)) - data = proxy._json_response( - resp, - error_message="cinder client call failed") - return self._get_and_munchify('quota_set', data) + return self.block_storage.get_quota_set( + proj) def delete_volume_quotas(self, name_or_id): """ Delete volume quotas for a project @@ -689,12 +680,8 @@ def delete_volume_quotas(self, name_or_id): :returns: dict with the quotas """ - proj = self.get_project(name_or_id) - if not proj: - raise exc.OpenStackCloudException("project does not exist") - - resp = self.block_storage.delete( - '/os-quota-sets/{tenant_id}'.format(tenant_id=proj.id)) - return proxy._json_response( - resp, - error_message="cinder client call failed") + proj = self.identity.find_project( + name_or_id, ignore_missing=False) + + return self.block_storage.revert_quota_set( + proj) diff --git a/openstack/tests/unit/cloud/test_quotas.py b/openstack/tests/unit/cloud/test_quotas.py index 9671b2e67..f338e94d7 100644 --- a/openstack/tests/unit/cloud/test_quotas.py +++ b/openstack/tests/unit/cloud/test_quotas.py @@ -107,9 +107,14 @@ def test_delete_quotas(self): self.assert_calls() def test_cinder_update_quotas(self): - project = self.mock_for_keystone_projects(project_count=1, - list_get=True)[0] + project = self._get_project_data() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'identity', 'public', + append=['v3', 'projects', project.project_id]), + json={'project': project.json_response['project']}), self.get_cinder_discovery_mock_dict(), dict(method='PUT', uri=self.get_mock_url( @@ -118,28 +123,38 @@ def test_cinder_update_quotas(self): json=dict(quota_set={'volumes': 1}), validate=dict( json={'quota_set': { - 'volumes': 1, - 'tenant_id': project.project_id}}))]) + 'volumes': 1}}))]) self.cloud.set_volume_quotas(project.project_id, volumes=1) self.assert_calls() def test_cinder_get_quotas(self): - project = self.mock_for_keystone_projects(project_count=1, - list_get=True)[0] + project = self._get_project_data() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'identity', 'public', + append=['v3', 'projects', project.project_id]), + json={'project': project.json_response['project']}), self.get_cinder_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'volumev3', 'public', - append=['os-quota-sets', project.project_id]), + append=['os-quota-sets', project.project_id], + qs_elements=['usage=False']), json=dict(quota_set={'snapshots': 10, 'volumes': 20}))]) self.cloud.get_volume_quotas(project.project_id) self.assert_calls() def test_cinder_delete_quotas(self): - project = self.mock_for_keystone_projects(project_count=1, - list_get=True)[0] + project = self._get_project_data() + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'identity', 'public', + append=['v3', 'projects', project.project_id]), + json={'project': project.json_response['project']}), self.get_cinder_discovery_mock_dict(), dict(method='DELETE', uri=self.get_mock_url( From fc2d56fa16ec4e39bd62ba1c1e3d12598ede8a5f Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Thu, 4 Nov 2021 07:58:55 +0100 Subject: [PATCH 2988/3836] Object Store - reindentation of the docstrings Change-Id: I2d771b25da94353c79aa8c827fcc6f376534c693 --- openstack/object_store/v1/_proxy.py | 242 ++++++++++++---------------- 1 file changed, 105 insertions(+), 137 deletions(-) diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index e89bdc262..917ec3f88 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -95,10 +95,9 @@ def get_account_metadata(self): def set_account_metadata(self, **metadata): """Set metadata for this account. - :param kwargs metadata: Key/value pairs to be set as metadata - on the container. Custom metadata can be set. - Custom metadata are keys and values defined - by the user. + :param kwargs metadata: Key/value pairs to be set as metadata on the + container. Custom metadata can be set. Custom metadata are keys and + values defined by the user. """ account = self._get_resource(_account.Account, None) account.set_metadata(self, metadata) @@ -115,7 +114,7 @@ def containers(self, **query): """Obtain Container objects for this account. :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :rtype: A generator of :class:`~openstack.object_store.v1.container.Container` objects. @@ -127,8 +126,8 @@ def create_container(self, name, **attrs): :param container: Name of the container to create. :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.object_store.v1.container.Container`, - comprised of the properties on the Container class. + a :class:`~openstack.object_store.v1.container.Container`, + comprised of the properties on the Container class. :returns: The results of container creation :rtype: :class:`~openstack.object_store.v1.container.Container` @@ -139,13 +138,11 @@ def delete_container(self, container, ignore_missing=True): """Delete a container :param container: The value can be either the name of a container or a - :class:`~openstack.object_store.v1.container.Container` - instance. + :class:`~openstack.object_store.v1.container.Container` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the container does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent server. + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the container does not exist. When set to ``True``, no exception + will be set when attempting to delete a nonexistent server. :returns: ``None`` """ @@ -156,12 +153,11 @@ def get_container_metadata(self, container): """Get metadata for a container :param container: The value can be the name of a container or a - :class:`~openstack.object_store.v1.container.Container` - instance. + :class:`~openstack.object_store.v1.container.Container` instance. :returns: One :class:`~openstack.object_store.v1.container.Container` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._head(_container.Container, container) @@ -169,24 +165,22 @@ def set_container_metadata(self, container, refresh=True, **metadata): """Set metadata for a container. :param container: The value can be the name of a container or a - :class:`~openstack.object_store.v1.container.Container` - instance. + :class:`~openstack.object_store.v1.container.Container` + instance. :param refresh: Flag to trigger refresh of container object re-fetch. - :param kwargs metadata: Key/value pairs to be set as metadata - on the container. Both custom and system - metadata can be set. Custom metadata are keys - and values defined by the user. System - metadata are keys defined by the Object Store - and values defined by the user. The system - metadata keys are: - - - `content_type` - - `is_content_type_detected` - - `versions_location` - - `read_ACL` - - `write_ACL` - - `sync_to` - - `sync_key` + :param kwargs metadata: Key/value pairs to be set as metadata on the + container. Both custom and system metadata can be set. Custom + metadata are keys and values defined by the user. System metadata + are keys defined by the Object Store and values defined by the + user. The system metadata keys are: + + - `content_type` + - `is_content_type_detected` + - `versions_location` + - `read_ACL` + - `write_ACL` + - `sync_to` + - `sync_key` """ res = self._get_resource(_container.Container, container) res.set_metadata(self, metadata, refresh=refresh) @@ -196,8 +190,7 @@ def delete_container_metadata(self, container, keys): """Delete metadata for a container. :param container: The value can be the ID of a container or a - :class:`~openstack.object_store.v1.container.Container` - instance. + :class:`~openstack.object_store.v1.container.Container` instance. :param keys: The keys of metadata to be deleted. """ res = self._get_resource(_container.Container, container) @@ -212,7 +205,7 @@ def objects(self, container, **query): :type container: :class:`~openstack.object_store.v1.container.Container` :param kwargs query: Optional query parameters to be sent to limit - the resources being returned. + the resources being returned. :rtype: A generator of :class:`~openstack.object_store.v1.obj.Object` objects. @@ -245,16 +238,13 @@ def get_object( :param obj: The value can be the name of an object or a :class:`~openstack.object_store.v1.obj.Object` instance. :param container: The value can be the name of a container or a - :class:`~openstack.object_store.v1.container.Container` - instance. - :param int resp_chunk_size: - chunk size of data to read. Only used if the results are - being written to a file or stream is True. + :class:`~openstack.object_store.v1.container.Container` instance. + :param int resp_chunk_size: chunk size of data to read. Only used if + the results are being written to a file or stream is True. (optional, defaults to 1k) - :param outfile: - Write the object to a file instead of returning the contents. - If this option is given, body in the return tuple will be None. - outfile can either be a file path given as a string, or a + :param outfile: Write the object to a file instead of returning the + contents. If this option is given, body in the return tuple will be + None. outfile can either be a file path given as a string, or a File like object. :param bool remember_content: Flag whether object data should be saved as `data` property of the Object. When left as `false` and @@ -305,13 +295,12 @@ def download_object(self, obj, container=None, **attrs): """Download the data contained inside an object. :param obj: The value can be the name of an object or a - :class:`~openstack.object_store.v1.obj.Object` instance. + :class:`~openstack.object_store.v1.obj.Object` instance. :param container: The value can be the name of a container or a - :class:`~openstack.object_store.v1.container.Container` - instance. + :class:`~openstack.object_store.v1.container.Container` instance. :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ container_name = self._get_container_name( obj=obj, container=container) @@ -323,13 +312,12 @@ def stream_object(self, obj, container=None, chunk_size=1024, **attrs): """Stream the data contained inside an object. :param obj: The value can be the name of an object or a - :class:`~openstack.object_store.v1.obj.Object` instance. + :class:`~openstack.object_store.v1.obj.Object` instance. :param container: The value can be the name of a container or a - :class:`~openstack.object_store.v1.container.Container` - instance. + :class:`~openstack.object_store.v1.container.Container` instance. :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. :returns: An iterator that iterates over chunk_size bytes """ container_name = self._get_container_name( @@ -354,7 +342,7 @@ def create_object( :param filename: The path to the local file whose contents will be uploaded. Mutually exclusive with data. :param data: The content to upload to the object. Mutually exclusive - with filename. + with filename. :param md5: A hexadecimal md5 of the file. (Optional), if it is known and can be passed here, it will save repeating the expensive md5 process. It is assumed to be accurate. @@ -455,16 +443,13 @@ def delete_object(self, obj, ignore_missing=True, container=None): """Delete an object :param obj: The value can be either the name of an object or a - :class:`~openstack.object_store.v1.container.Container` - instance. + :class:`~openstack.object_store.v1.container.Container` instance. :param container: The value can be the ID of a container or a - :class:`~openstack.object_store.v1.container.Container` - instance. + :class:`~openstack.object_store.v1.container.Container` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the object does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent server. + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the object does not exist. When set to ``True``, no exception will + be set when attempting to delete a nonexistent server. :returns: ``None`` """ @@ -477,14 +462,13 @@ def get_object_metadata(self, obj, container=None): """Get metadata for an object. :param obj: The value can be the name of an object or a - :class:`~openstack.object_store.v1.obj.Object` instance. + :class:`~openstack.object_store.v1.obj.Object` instance. :param container: The value can be the ID of a container or a - :class:`~openstack.object_store.v1.container.Container` - instance. + :class:`~openstack.object_store.v1.container.Container` instance. :returns: One :class:`~openstack.object_store.v1.obj.Object` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ container_name = self._get_container_name(obj, container) @@ -496,24 +480,21 @@ def set_object_metadata(self, obj, container=None, **metadata): Note: This method will do an extra HEAD call. :param obj: The value can be the name of an object or a - :class:`~openstack.object_store.v1.obj.Object` instance. + :class:`~openstack.object_store.v1.obj.Object` instance. :param container: The value can be the name of a container or a - :class:`~openstack.object_store.v1.container.Container` - instance. + :class:`~openstack.object_store.v1.container.Container` instance. :param kwargs metadata: Key/value pairs to be set as metadata - on the container. Both custom and system - metadata can be set. Custom metadata are keys - and values defined by the user. System - metadata are keys defined by the Object Store - and values defined by the user. The system - metadata keys are: - - - `content_type` - - `content_encoding` - - `content_disposition` - - `delete_after` - - `delete_at` - - `is_content_type_detected` + on the container. Both custom and system metadata can be set. + Custom metadata are keys and values defined by the user. System + metadata are keys defined by the Object Store and values defined by + the user. The system metadata keys are: + + - `content_type` + - `content_encoding` + - `content_disposition` + - `delete_after` + - `delete_at` + - `is_content_type_detected` """ container_name = self._get_container_name(obj, container) res = self._get_resource(_obj.Object, obj, container=container_name) @@ -524,10 +505,9 @@ def delete_object_metadata(self, obj, container=None, keys=None): """Delete metadata for an object. :param obj: The value can be the name of an object or a - :class:`~openstack.object_store.v1.obj.Object` instance. + :class:`~openstack.object_store.v1.obj.Object` instance. :param container: The value can be the ID of a container or a - :class:`~openstack.object_store.v1.container.Container` - instance. + :class:`~openstack.object_store.v1.container.Container` instance. :param keys: The keys of metadata to be deleted. """ container_name = self._get_container_name(obj, container) @@ -542,12 +522,10 @@ def is_object_stale( :param container: Name of the container. :param name: Name of the object. :param filename: Path to the file. - :param file_md5: - Pre-calculated md5 of the file contents. Defaults to None which - means calculate locally. - :param file_sha256: - Pre-calculated sha256 of the file contents. Defaults to None which - means calculate locally. + :param file_md5: Pre-calculated md5 of the file contents. Defaults to + None which means calculate locally. + :param file_sha256: Pre-calculated sha256 of the file contents. + Defaults to None which means calculate locally. """ try: metadata = self.get_object_metadata(name, container).metadata @@ -767,10 +745,9 @@ def get_info(self): def set_account_temp_url_key(self, key, secondary=False): """Set the temporary URL key for the account. - :param key: - Text of the key to use. - :param bool secondary: - Whether this should set the secondary key. (defaults to False) + :param key: Text of the key to use. + :param bool secondary: Whether this should set the secondary key. + (defaults to False) """ account = self._get_resource(_account.Account, None) account.set_temp_url_key(self, key, secondary) @@ -778,13 +755,11 @@ def set_account_temp_url_key(self, key, secondary=False): def set_container_temp_url_key(self, container, key, secondary=False): """Set the temporary URL key for a container. - :param container: - The value can be the name of a container or a - :class:`~openstack.object_store.v1.container.Container` instance. - :param key: - Text of the key to use. - :param bool secondary: - Whether this should set the secondary key. (defaults to False) + :param container: The value can be the name of a container or a + :class:`~openstack.object_store.v1.container.Container` instance. + :param key: Text of the key to use. + :param bool secondary: Whether this should set the secondary key. + (defaults to False) """ res = self._get_resource(_container.Container, container) res.set_temp_url_key(self, key, secondary) @@ -792,14 +767,12 @@ def set_container_temp_url_key(self, container, key, secondary=False): def get_temp_url_key(self, container=None): """Get the best temporary url key for a given container. - Will first try to return Temp-URL-Key-2 then Temp-URL-Key for - the container, and if neither exist, will attempt to return - Temp-URL-Key-2 then Temp-URL-Key for the account. If neither - exist, will return None. + Will first try to return Temp-URL-Key-2 then Temp-URL-Key for the + container, and if neither exist, will attempt to return Temp-URL-Key-2 + then Temp-URL-Key for the account. If neither exist, will return None. - :param container: - The value can be the name of a container or a - :class:`~openstack.object_store.v1.container.Container` instance. + :param container: The value can be the name of a container or a + :class:`~openstack.object_store.v1.container.Container` instance. """ temp_url_key = None if container: @@ -831,24 +804,19 @@ def generate_form_signature( max_upload_count, timeout, temp_url_key=None): """Generate a signature for a FormPost upload. - :param container: - The value can be the name of a container or a - :class:`~openstack.object_store.v1.container.Container` instance. - :param object_prefix: - Prefix to apply to limit all object names created using this - signature. - :param redirect_url: - The URL to redirect the browser to after the uploads have - completed. - :param max_file_size: - The maximum file size per file uploaded. - :param max_upload_count: - The maximum number of uploaded files allowed. - :param timeout: - The number of seconds from now to allow the form post to begin. - :param temp_url_key: - The X-Account-Meta-Temp-URL-Key for the account. Optional, if - omitted, the key will be fetched from the container or the account. + :param container: The value can be the name of a container or a + :class:`~openstack.object_store.v1.container.Container` instance. + :param object_prefix: Prefix to apply to limit all object names + created using this signature. + :param redirect_url: The URL to redirect the browser to after the + uploads have completed. + :param max_file_size: The maximum file size per file uploaded. + :param max_upload_count: The maximum number of uploaded files allowed. + :param timeout: The number of seconds from now to allow the form + post to begin. + :param temp_url_key: The X-Account-Meta-Temp-URL-Key for the account. + Optional, if omitted, the key will be fetched from the container + or the account. """ max_file_size = int(max_file_size) if max_file_size < 1: @@ -889,9 +857,9 @@ def generate_temp_url( :param seconds: time in seconds or ISO 8601 timestamp. If absolute is False and this is the string representation of an integer, then this specifies the amount of time in seconds for - which the temporary URL will be valid. - If absolute is True then this specifies an absolute time at which - the temporary URL will expire. + which the temporary URL will be valid. If absolute is True then + this specifies an absolute time at which the temporary URL will + expire. :param method: A HTTP method, typically either GET or PUT, to allow for this temporary URL. :param absolute: if True then the seconds parameter is interpreted as a @@ -902,9 +870,9 @@ def generate_temp_url( instead of a UNIX timestamp will be created. :param ip_range: if a valid ip range, restricts the temporary URL to the range of ips. - :param temp_url_key: - The X-Account-Meta-Temp-URL-Key for the account. Optional, if - omitted, the key will be fetched from the container or the account. + :param temp_url_key: The X-Account-Meta-Temp-URL-Key for the account. + Optional, if omitted, the key will be fetched from the container or + the account. :raises ValueError: if timestamp or path is not in valid format. :return: the path portion of a temporary URL """ @@ -1014,10 +982,10 @@ def _delete_autocreated_image_objects( """Delete all objects autocreated for image uploads. This method should generally not be needed, as shade should clean up - the objects it uses for object-based image creation. If something - goes wrong and it is found that there are leaked objects, this method - can be used to delete any objects that shade has created on the user's - behalf in service of image uploads. + the objects it uses for object-based image creation. If something goes + wrong and it is found that there are leaked objects, this method can be + used to delete any objects that shade has created on the user's behalf + in service of image uploads. :param str container: Name of the container. Defaults to 'images'. :param str segment_prefix: Prefix for the image segment names to From 6c905301def9e3842ac07d38effdb0f71dce99f2 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 28 Dec 2021 15:11:49 +0100 Subject: [PATCH 2989/3836] Get rid of normalization for image service Stop using any normalization in the image service. Change-Id: Id64199794214f282165836a76725b606c1a6947b --- openstack/cloud/_image.py | 21 +++--- openstack/tests/unit/cloud/test_caching.py | 85 ++++++++++++---------- openstack/tests/unit/cloud/test_image.py | 81 ++++++++++++--------- 3 files changed, 100 insertions(+), 87 deletions(-) diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py index 821f63ed8..16c8415a3 100644 --- a/openstack/cloud/_image.py +++ b/openstack/cloud/_image.py @@ -15,7 +15,6 @@ # openstack.resource.Resource.list and openstack.resource2.Resource.list import types # noqa -from openstack.cloud import _normalize from openstack.cloud import _utils from openstack.cloud import exc from openstack import utils @@ -29,7 +28,7 @@ def _no_pending_images(images): return True -class ImageCloudMixin(_normalize.Normalizer): +class ImageCloudMixin: def __init__(self): self.image_api_use_tasks = self.config.config['image_api_use_tasks'] @@ -81,7 +80,7 @@ def list_images(self, filter_deleted=True, show_all=False): images.append(image) elif image.status.lower() != 'deleted': images.append(image) - return self._normalize_images(images) + return images def get_image(self, name_or_id, filters=None): """Get an image by name or ID. @@ -112,12 +111,10 @@ def get_image_by_id(self, id): """ Get a image by ID :param id: ID of the image. - :returns: An image ``munch.Munch``. + :returns: An image + :class:`openstack.image.v2.image.Image` object. """ - image = self._normalize_image( - self.image.get_image(image={'id': id})) - - return image + return self.image.get_image(image={'id': id}) def download_image( self, name_or_id, output_path=None, output_file=None, @@ -213,10 +210,10 @@ def delete_image( # Task API means an image was uploaded to swift # TODO(gtema) does it make sense to move this into proxy? if self.image_api_use_tasks and ( - self.image._IMAGE_OBJECT_KEY in image - or self.image._SHADE_IMAGE_OBJECT_KEY in image): - (container, objname) = image.get( - self.image._IMAGE_OBJECT_KEY, image.get( + self.image._IMAGE_OBJECT_KEY in image.properties + or self.image._SHADE_IMAGE_OBJECT_KEY in image.properties): + (container, objname) = image.properties.get( + self.image._IMAGE_OBJECT_KEY, image.properties.get( self.image._SHADE_IMAGE_OBJECT_KEY)).split('/', 1) self.delete_object(container=container, name=objname) diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py index de0a1453a..633b6e3fd 100644 --- a/openstack/tests/unit/cloud/test_caching.py +++ b/openstack/tests/unit/cloud/test_caching.py @@ -24,6 +24,7 @@ from openstack import exceptions from openstack.identity.v3 import project as _project from openstack.identity.v3 import user as _user +from openstack.image.v2 import image as _image from openstack.network.v2 import port as _port from openstack.tests import fakes from openstack.tests.unit import base @@ -104,11 +105,10 @@ def setUp(self): super(TestMemoryCache, self).setUp( cloud_config_fixture='clouds_cache.yaml') - def _image_dict(self, fake_image): - return self.cloud._normalize_image(meta.obj_to_munch(fake_image)) - - def _munch_images(self, fake_image): - return self.cloud._normalize_images([fake_image]) + def _compare_images(self, exp, real): + self.assertDictEqual( + _image.Image(**exp).to_dict(computed=False), + real.to_dict(computed=False)) def _compare_volumes(self, exp, real): self.assertDictEqual( @@ -468,8 +468,9 @@ def test_list_images(self): self.assertEqual([], self.cloud.list_images()) self.assertEqual([], self.cloud.list_images()) self.cloud.list_images.invalidate(self.cloud) - self.assertEqual( - self._munch_images(fake_image), self.cloud.list_images()) + [self._compare_images(a, b) for a, b in zip( + [fake_image], + self.cloud.list_images())] self.assert_calls() @@ -488,13 +489,13 @@ def test_list_images_caches_deleted_status(self): json=list_return), ]) - self.assertEqual( - [self.cloud._normalize_image(active_image)], - self.cloud.list_images()) + [self._compare_images(a, b) for a, b in zip( + [active_image], + self.cloud.list_images())] - self.assertEqual( - [self.cloud._normalize_image(active_image)], - self.cloud.list_images()) + [self._compare_images(a, b) for a, b in zip( + [active_image], + self.cloud.list_images())] # We should only have one call self.assert_calls() @@ -515,23 +516,20 @@ def test_cache_no_cloud_name(self): json={'images': [fi, fi2]}), ]) - self.assertEqual( - self._munch_images(fi), - self.cloud.list_images()) + [self._compare_images(a, b) for a, b in zip( + [fi], + self.cloud.list_images())] # Now test that the list was cached - self.assertEqual( - self._munch_images(fi), - self.cloud.list_images()) + [self._compare_images(a, b) for a, b in zip( + [fi], + self.cloud.list_images())] # Invalidation too self.cloud.list_images.invalidate(self.cloud) - self.assertEqual( - [ - self.cloud._normalize_image(fi), - self.cloud._normalize_image(fi2) - ], - self.cloud.list_images()) + [self._compare_images(a, b) for a, b in zip( + [fi, fi2], + self.cloud.list_images())] def test_list_ports_filtered(self): down_port = test_port.TestPort.mock_neutron_port_create_rep['port'] @@ -577,6 +575,11 @@ def setUp(self): self.steady_list_return = { 'images': [self.active_image, self.steady_image]} + def _compare_images(self, exp, real): + self.assertDictEqual( + _image.Image(**exp).to_dict(computed=False), + real.to_dict(computed=False)) + def test_list_images_ignores_pending_status(self): self.register_uris([ @@ -588,17 +591,14 @@ def test_list_images_ignores_pending_status(self): json=self.steady_list_return), ]) - self.assertEqual( - [self.cloud._normalize_image(self.active_image)], - self.cloud.list_images()) + [self._compare_images(a, b) for a, b in zip( + [self.active_image], + self.cloud.list_images())] # Should expect steady_image to appear if active wasn't cached - self.assertEqual( - [ - self.cloud._normalize_image(self.active_image), - self.cloud._normalize_image(self.steady_image) - ], - self.cloud.list_images()) + [self._compare_images(a, b) for a, b in zip( + [self.active_image, self.steady_image], + self.cloud.list_images())] class TestCacheSteadyStatus(base.TestCase): @@ -617,6 +617,11 @@ def setUp(self): image_id=active_image_id, status=self.status) self.active_list_return = {'images': [self.active_image]} + def _compare_images(self, exp, real): + self.assertDictEqual( + _image.Image(**exp).to_dict(computed=False), + real.to_dict(computed=False)) + def test_list_images_caches_steady_status(self): self.register_uris([ @@ -625,13 +630,13 @@ def test_list_images_caches_steady_status(self): json=self.active_list_return), ]) - self.assertEqual( - [self.cloud._normalize_image(self.active_image)], - self.cloud.list_images()) + [self._compare_images(a, b) for a, b in zip( + [self.active_image], + self.cloud.list_images())] - self.assertEqual( - [self.cloud._normalize_image(self.active_image)], - self.cloud.list_images()) + [self._compare_images(a, b) for a, b in zip( + [self.active_image], + self.cloud.list_images())] # We should only have one call self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 6b24b59c8..2c30d27eb 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -20,6 +20,8 @@ from openstack.cloud import exc from openstack.cloud import meta from openstack import exceptions +from openstack.image.v1 import image as image_v1 +from openstack.image.v2 import image from openstack.tests import fakes from openstack.tests.unit import base @@ -46,6 +48,16 @@ def setUp(self): self.fake_search_return = {'images': [self.fake_image_dict]} self.container_name = self.getUniqueString('container') + def _compare_images(self, exp, real): + self.assertDictEqual( + image.Image(**exp).to_dict(computed=False), + real.to_dict(computed=False)) + + def _compare_images_v1(self, exp, real): + self.assertDictEqual( + image_v1.Image(**exp).to_dict(computed=False), + real.to_dict(computed=False)) + class TestImage(BaseTestImage): @@ -166,9 +178,10 @@ def test_get_image_by_id(self): base_url_append='v2'), json=self.fake_image_dict) ]) - self.assertDictEqual( - self.cloud._normalize_image(self.fake_image_dict), - self.cloud.get_image_by_id(self.image_id)) + self._compare_images( + self.fake_image_dict, + self.cloud.get_image_by_id(self.image_id) + ) self.assert_calls() def test_get_image_id(self, cloud=None): @@ -216,9 +229,9 @@ def test_list_images(self): 'image', append=['images'], base_url_append='v2'), json=self.fake_search_return) ]) - self.assertEqual( - self.cloud._normalize_images([self.fake_image_dict]), - self.cloud.list_images()) + [self._compare_images(a, b) for a, b in zip( + [self.fake_image_dict], + self.cloud.list_images())] self.assert_calls() def test_list_images_show_all(self): @@ -229,9 +242,9 @@ def test_list_images_show_all(self): qs_elements=['member_status=all']), json=self.fake_search_return) ]) - self.assertEqual( - self.cloud._normalize_images([self.fake_image_dict]), - self.cloud.list_images(show_all=True)) + [self._compare_images(a, b) for a, b in zip( + [self.fake_image_dict], + self.cloud.list_images(show_all=True))] self.assert_calls() def test_list_images_show_all_deleted(self): @@ -244,10 +257,9 @@ def test_list_images_show_all_deleted(self): qs_elements=['member_status=all']), json={'images': [self.fake_image_dict, deleted_image]}) ]) - self.assertEqual( - self.cloud._normalize_images([ - self.fake_image_dict, deleted_image]), - self.cloud.list_images(show_all=True)) + [self._compare_images(a, b) for a, b in zip( + [self.fake_image_dict], + self.cloud.list_images(show_all=True))] self.assert_calls() def test_list_images_no_filter_deleted(self): @@ -259,10 +271,9 @@ def test_list_images_no_filter_deleted(self): 'image', append=['images'], base_url_append='v2'), json={'images': [self.fake_image_dict, deleted_image]}) ]) - self.assertEqual( - self.cloud._normalize_images([ - self.fake_image_dict, deleted_image]), - self.cloud.list_images(filter_deleted=False)) + [self._compare_images(a, b) for a, b in zip( + [self.fake_image_dict], + self.cloud.list_images(filter_deleted=False))] self.assert_calls() def test_list_images_filter_deleted(self): @@ -274,9 +285,9 @@ def test_list_images_filter_deleted(self): 'image', append=['images'], base_url_append='v2'), json={'images': [self.fake_image_dict, deleted_image]}) ]) - self.assertEqual( - self.cloud._normalize_images([self.fake_image_dict]), - self.cloud.list_images()) + [self._compare_images(a, b) for a, b in zip( + [self.fake_image_dict], + self.cloud.list_images())] self.assert_calls() def test_list_images_string_properties(self): @@ -289,9 +300,10 @@ def test_list_images_string_properties(self): json={'images': [image_dict]}), ]) images = self.cloud.list_images() - self.assertEqual( - self.cloud._normalize_images([image_dict]), - images) + [self._compare_images(a, b) for a, b in zip( + [image_dict], + images)] + self.assertEqual( images[0]['properties']['properties'], 'list,of,properties') @@ -312,10 +324,9 @@ def test_list_images_paginated(self): qs_elements=['marker={marker}'.format(marker=marker)]), json=self.fake_search_return) ]) - self.assertEqual( - self.cloud._normalize_images([ - self.fake_image_dict, self.fake_image_dict]), - self.cloud.list_images()) + [self._compare_images(a, b) for a, b in zip( + [self.fake_image_dict], + self.cloud.list_images())] self.assert_calls() def test_create_image_put_v2_no_import(self): @@ -894,7 +905,8 @@ def test_create_image_put_v1(self): json={'images': [ret]}), ]) self._call_create_image(self.image_name) - self.assertEqual(self._munch_images(ret), self.cloud.list_images()) + [self._compare_images_v1(b, a) for a, b in zip( + self.cloud.list_images(), [ret])] def test_create_image_put_v1_bad_delete(self): self.cloud.config.config['image_api_version'] = '1' @@ -1306,9 +1318,9 @@ def test_list_images(self): 'image', append=['images'], base_url_append='v2'), json=self.fake_search_return) ]) - self.assertEqual( - self.cloud._normalize_images([self.fake_image_dict]), - self.cloud.list_images()) + [self._compare_images(b, a) for a, b in zip( + self.cloud.list_images(), + [self.fake_image_dict])] self.assert_calls() def test_list_images_paginated(self): @@ -1326,10 +1338,9 @@ def test_list_images_paginated(self): qs_elements=['marker={marker}'.format(marker=marker)]), json=self.fake_search_return) ]) - self.assertEqual( - self.cloud._normalize_images([ - self.fake_image_dict, self.fake_image_dict]), - self.cloud.list_images()) + [self._compare_images(b, a) for a, b in zip( + self.cloud.list_images(), + [self.fake_image_dict, self.fake_image_dict])] self.assert_calls() From f34ea0d13f6e782d2b010473af8127b583d7908f Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 28 Dec 2021 15:35:36 +0100 Subject: [PATCH 2990/3836] Get rid of normalization in orchestration CL Stop using normalization in the orchestration cloud layer methods. Change-Id: I9c674242f6da87923fc8a4adc9a2428030094f00 --- openstack/cloud/_orchestration.py | 13 +++++---- openstack/tests/unit/cloud/test_stack.py | 34 +++++++++--------------- 2 files changed, 18 insertions(+), 29 deletions(-) diff --git a/openstack/cloud/_orchestration.py b/openstack/cloud/_orchestration.py index eb22825e2..6b7bc8407 100644 --- a/openstack/cloud/_orchestration.py +++ b/openstack/cloud/_orchestration.py @@ -15,7 +15,6 @@ # openstack.resource.Resource.list and openstack.resource2.Resource.list import types # noqa -from openstack.cloud import _normalize from openstack.cloud import _utils from openstack.cloud import exc from openstack.orchestration.util import event_utils @@ -30,7 +29,7 @@ def _no_pending_stacks(stacks): return True -class OrchestrationCloudMixin(_normalize.Normalizer): +class OrchestrationCloudMixin: @property def _orchestration_client(self): @@ -213,13 +212,13 @@ def list_stacks(self, **query): """List all stacks. :param dict query: Query parameters to limit stacks. - :returns: a list of ``munch.Munch`` containing the stack description. + :returns: a list of :class:`openstack.orchestration.v1.stack.Stack` + objects containing the stack description. :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ - data = self.orchestration.stacks(**query) - return self._normalize_stacks(data) + return list(self.orchestration.stacks(**query)) def get_stack(self, name_or_id, filters=None, resolve_outputs=True): """Get exactly one stack. @@ -230,7 +229,8 @@ def get_stack(self, name_or_id, filters=None, resolve_outputs=True): :param resolve_outputs: If True, then outputs for this stack will be resolved - :returns: a ``munch.Munch`` containing the stack description + :returns: a :class:`openstack.orchestration.v1.stack.Stack` + containing the stack description :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call or if multiple matches are found. @@ -248,7 +248,6 @@ def _search_one_stack(name_or_id=None, filters=None): return [] except exc.OpenStackCloudURINotFound: return [] - stack = self._normalize_stack(stack) return _utils._filter_list([stack], name_or_id, filters) return _utils._get_entity( diff --git a/openstack/tests/unit/cloud/test_stack.py b/openstack/tests/unit/cloud/test_stack.py index de00c3775..827a465ad 100644 --- a/openstack/tests/unit/cloud/test_stack.py +++ b/openstack/tests/unit/cloud/test_stack.py @@ -29,6 +29,11 @@ def setUp(self): self.stack_tag = self.getUniqueString('tag') self.stack = fakes.make_fake_stack(self.stack_id, self.stack_name) + def _compare_stacks(self, exp, real): + self.assertDictEqual( + stack.Stack(**exp).to_dict(computed=False), + real.to_dict(computed=False)) + def test_list_stacks(self): fake_stacks = [ self.stack, @@ -43,10 +48,7 @@ def test_list_stacks(self): json={"stacks": fake_stacks}), ]) stacks = self.cloud.list_stacks() - self.assertEqual( - [f.toDict() for f in self.cloud._normalize_stacks( - stack.Stack(**st) for st in fake_stacks)], - [f.toDict() for f in stacks]) + [self._compare_stacks(b, a) for a, b in zip(stacks, fake_stacks)] self.assert_calls() @@ -67,10 +69,7 @@ def test_list_stacks_filters(self): json={"stacks": fake_stacks}), ]) stacks = self.cloud.list_stacks(name='a', status='b') - self.assertEqual( - [f.toDict() for f in self.cloud._normalize_stacks( - stack.Stack(**st) for st in fake_stacks)], - [f.toDict() for f in stacks]) + [self._compare_stacks(b, a) for a, b in zip(stacks, fake_stacks)] self.assert_calls() @@ -100,10 +99,7 @@ def test_search_stacks(self): json={"stacks": fake_stacks}), ]) stacks = self.cloud.search_stacks() - self.assertEqual( - self.cloud._normalize_stacks( - stack.Stack(**st) for st in fake_stacks), - stacks) + [self._compare_stacks(b, a) for a, b in zip(stacks, fake_stacks)] self.assert_calls() def test_search_stacks_filters(self): @@ -122,10 +118,7 @@ def test_search_stacks_filters(self): ]) filters = {'status': 'FAILED'} stacks = self.cloud.search_stacks(filters=filters) - self.assertEqual( - self.cloud._normalize_stacks( - stack.Stack(**st) for st in fake_stacks[1:]), - stacks) + [self._compare_stacks(b, a) for a, b in zip(stacks, fake_stacks)] self.assert_calls() def test_search_stacks_exception(self): @@ -618,10 +611,9 @@ def test_get_stack(self): res = self.cloud.get_stack(self.stack_name) self.assertIsNotNone(res) - self.assertEqual(self.stack['stack_name'], res['stack_name']) self.assertEqual(self.stack['stack_name'], res['name']) self.assertEqual(self.stack['stack_status'], res['stack_status']) - self.assertEqual('COMPLETE', res['status']) + self.assertEqual('CREATE_COMPLETE', res['status']) self.assert_calls() @@ -647,10 +639,8 @@ def test_get_stack_in_progress(self): res = self.cloud.get_stack(self.stack_name) self.assertIsNotNone(res) - self.assertEqual(in_progress['stack_name'], res['stack_name']) - self.assertEqual(in_progress['stack_name'], res['name']) + self.assertEqual(in_progress['stack_name'], res.name) self.assertEqual(in_progress['stack_status'], res['stack_status']) - self.assertEqual('CREATE', res['action']) - self.assertEqual('IN_PROGRESS', res['status']) + self.assertEqual('CREATE_IN_PROGRESS', res['status']) self.assert_calls() From e910e6eae236bd051258d134a4701cfeb9b9e7f3 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 28 Dec 2021 15:36:52 +0100 Subject: [PATCH 2991/3836] Get rid of normalization in identity CL Stop using normalization in the identity cloud layer. Change-Id: Ia6c24590d6f12258be6c27fcf45c944a771083fc --- openstack/cloud/_identity.py | 16 +++++------ .../tests/unit/cloud/test_identity_roles.py | 27 ++++++++++++------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index ea88711b0..8244fb798 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -17,13 +17,12 @@ import munch -from openstack.cloud import _normalize from openstack.cloud import _utils from openstack.cloud import exc from openstack import exceptions -class IdentityCloudMixin(_normalize.Normalizer): +class IdentityCloudMixin: @property def _identity_client(self): @@ -790,7 +789,7 @@ def create_group(self, name, description, domain=None): group = self.identity.create_group(**group_ref) self.list_groups.invalidate(self) - return _utils.normalize_groups([group])[0] + return group @_utils.valid_kwargs('domain_id') def update_group(self, name_or_id, name=None, description=None, @@ -822,7 +821,7 @@ def update_group(self, name_or_id, name=None, description=None, group = self.identity.update_group(group, **group_ref) self.list_groups.invalidate(self) - return _utils.normalize_groups([group])[0] + return group @_utils.valid_kwargs('domain_id') def delete_group(self, name_or_id, **kwargs): @@ -955,8 +954,9 @@ def list_role_assignments(self, filters=None): 'user' and 'group' are mutually exclusive, as are 'domain' and 'project'. - :returns: a list of ``munch.Munch`` containing the role assignment - description. Contains the following attributes:: + :returns: a list of + :class:`openstack.identity.v3.role_assignment.RoleAssignment` + objects. Contains the following attributes:: - id: - user|group: @@ -991,9 +991,7 @@ def list_role_assignments(self, filters=None): if k in filters: filters['scope_%s_id' % k] = filters.pop(k) - assignments = self.identity.role_assignments(**filters) - - return _utils.normalize_role_assignments(assignments) + return list(self.identity.role_assignments(**filters)) @_utils.valid_kwargs('domain_id') def create_role(self, name, **kwargs): diff --git a/openstack/tests/unit/cloud/test_identity_roles.py b/openstack/tests/unit/cloud/test_identity_roles.py index fd3a56a66..517c54051 100644 --- a/openstack/tests/unit/cloud/test_identity_roles.py +++ b/openstack/tests/unit/cloud/test_identity_roles.py @@ -196,13 +196,18 @@ def test_list_role_assignments(self): ]) ret = self.cloud.list_role_assignments() self.assertThat(len(ret), matchers.Equals(2)) - self.assertThat(ret[0].user, matchers.Equals(user_data.user_id)) - self.assertThat(ret[0].id, matchers.Equals(role_data.role_id)) - self.assertThat(ret[0].domain, matchers.Equals(domain_data.domain_id)) - self.assertThat(ret[1].group, matchers.Equals(group_data.group_id)) - self.assertThat(ret[1].id, matchers.Equals(role_data.role_id)) - self.assertThat(ret[1].project, - matchers.Equals(project_data.project_id)) + self.assertThat(ret[0].user['id'], matchers.Equals(user_data.user_id)) + self.assertThat(ret[0].role['id'], matchers.Equals(role_data.role_id)) + self.assertThat( + ret[0].scope['domain']['id'], + matchers.Equals(domain_data.domain_id)) + self.assertThat( + ret[1].group['id'], + matchers.Equals(group_data.group_id)) + self.assertThat(ret[1].role['id'], matchers.Equals(role_data.role_id)) + self.assertThat( + ret[1].scope['project']['id'], + matchers.Equals(project_data.project_id)) def test_list_role_assignments_filters(self): domain_data = self._get_domain_data() @@ -230,9 +235,11 @@ def test_list_role_assignments_filters(self): effective=True) ret = self.cloud.list_role_assignments(filters=params) self.assertThat(len(ret), matchers.Equals(1)) - self.assertThat(ret[0].user, matchers.Equals(user_data.user_id)) - self.assertThat(ret[0].id, matchers.Equals(role_data.role_id)) - self.assertThat(ret[0].domain, matchers.Equals(domain_data.domain_id)) + self.assertThat(ret[0].user['id'], matchers.Equals(user_data.user_id)) + self.assertThat(ret[0].role['id'], matchers.Equals(role_data.role_id)) + self.assertThat( + ret[0].scope['domain']['id'], + matchers.Equals(domain_data.domain_id)) def test_list_role_assignments_exception(self): self.register_uris([ From ed86ffbf8382404bf8df98975da33de169597658 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 28 Dec 2021 15:39:06 +0100 Subject: [PATCH 2992/3836] Get rid of normalization in further CL services Stop using normalization in next bunch of services. Change-Id: Ida8b39cdaf984cffbe96ae8105ed3b4d333968e5 --- openstack/cloud/_accelerator.py | 4 +--- openstack/cloud/_baremetal.py | 3 +-- openstack/cloud/_block_storage.py | 6 ++---- openstack/cloud/_clustering.py | 4 +--- openstack/cloud/_dns.py | 3 +-- openstack/cloud/_object_store.py | 3 +-- openstack/cloud/_shared_file_system.py | 4 +--- 7 files changed, 8 insertions(+), 19 deletions(-) diff --git a/openstack/cloud/_accelerator.py b/openstack/cloud/_accelerator.py index 41cf4a15a..abc7d3d9e 100644 --- a/openstack/cloud/_accelerator.py +++ b/openstack/cloud/_accelerator.py @@ -14,10 +14,8 @@ # We can't just use list, because sphinx gets confused by # openstack.resource.Resource.list and openstack.resource2.Resource.list -from openstack.cloud import _normalize - -class AcceleratorCloudMixin(_normalize.Normalizer): +class AcceleratorCloudMixin: def list_deployables(self, filters=None): """List all available deployables. diff --git a/openstack/cloud/_baremetal.py b/openstack/cloud/_baremetal.py index c3c413571..4d852d48d 100644 --- a/openstack/cloud/_baremetal.py +++ b/openstack/cloud/_baremetal.py @@ -18,13 +18,12 @@ import jsonpatch -from openstack.cloud import _normalize from openstack.cloud import _utils from openstack.cloud import exc from openstack import utils -class BaremetalCloudMixin(_normalize.Normalizer): +class BaremetalCloudMixin: @property def _baremetal_client(self): diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index b58e24017..b8f1fb9ce 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -16,7 +16,6 @@ import types # noqa from openstack.block_storage.v3 import quota_set as _qs -from openstack.cloud import _normalize from openstack.cloud import _utils from openstack.cloud import exc from openstack import exceptions @@ -31,7 +30,7 @@ def _no_pending_volumes(volumes): return True -class BlockStorageCloudMixin(_normalize.Normalizer): +class BlockStorageCloudMixin: @_utils.cache_on_arguments(should_cache_fn=_no_pending_volumes) def list_volumes(self, cache=True): @@ -606,8 +605,7 @@ def get_volume_type_access(self, name_or_id): raise exc.OpenStackCloudException( "VolumeType not found: %s" % name_or_id) - return self._normalize_volume_type_accesses( - self.block_storage.get_type_access(volume_type)) + return self.block_storage.get_type_access(volume_type) def add_volume_type_access(self, name_or_id, project_id): """Grant access on a volume_type to a project. diff --git a/openstack/cloud/_clustering.py b/openstack/cloud/_clustering.py index 05cb0a214..c04367693 100644 --- a/openstack/cloud/_clustering.py +++ b/openstack/cloud/_clustering.py @@ -15,10 +15,8 @@ # openstack.resource.Resource.list and openstack.resource2.Resource.list import types # noqa -from openstack.cloud import _normalize - -class ClusteringCloudMixin(_normalize.Normalizer): +class ClusteringCloudMixin: pass @property diff --git a/openstack/cloud/_dns.py b/openstack/cloud/_dns.py index bb534215a..9a5a95fa0 100644 --- a/openstack/cloud/_dns.py +++ b/openstack/cloud/_dns.py @@ -15,14 +15,13 @@ # openstack.resource.Resource.list and openstack.resource2.Resource.list import types # noqa -from openstack.cloud import _normalize from openstack.cloud import _utils from openstack.cloud import exc from openstack import exceptions from openstack import resource -class DnsCloudMixin(_normalize.Normalizer): +class DnsCloudMixin: def list_zones(self, filters=None): """List all available zones. diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index df12b821e..b03b18249 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -19,7 +19,6 @@ import keystoneauth1.exceptions -from openstack.cloud import _normalize from openstack.cloud import _utils from openstack.cloud import exc from openstack import exceptions @@ -36,7 +35,7 @@ } -class ObjectStoreCloudMixin(_normalize.Normalizer): +class ObjectStoreCloudMixin: @property def _object_store_client(self): diff --git a/openstack/cloud/_shared_file_system.py b/openstack/cloud/_shared_file_system.py index 9302a8a68..9063e0794 100644 --- a/openstack/cloud/_shared_file_system.py +++ b/openstack/cloud/_shared_file_system.py @@ -10,10 +10,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from openstack.cloud import _normalize - -class SharedFileSystemCloudMixin(_normalize.Normalizer): +class SharedFileSystemCloudMixin: def list_share_availability_zones(self): """List all availability zones for the Shared File Systems service. From 4f54021ed06efba035089055ecd1654fbd5e0860 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 28 Dec 2021 15:41:12 +0100 Subject: [PATCH 2993/3836] Get rid of normalization in network CL Stop using normalization for the network cloud layer methods. Change-Id: I9fe3e991a77d0ab223ca4a18b9ce4314f87b887d --- openstack/cloud/_network.py | 3 +-- openstack/cloud/_network_common.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index 500b6e810..0cc469b14 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -17,13 +17,12 @@ import time import types # noqa -from openstack.cloud import _normalize from openstack.cloud import _utils from openstack.cloud import exc from openstack import exceptions -class NetworkCloudMixin(_normalize.Normalizer): +class NetworkCloudMixin: def __init__(self): self._ports = None diff --git a/openstack/cloud/_network_common.py b/openstack/cloud/_network_common.py index ceba41021..0d0b3ca9f 100644 --- a/openstack/cloud/_network_common.py +++ b/openstack/cloud/_network_common.py @@ -16,11 +16,10 @@ import threading import types # noqa -from openstack.cloud import _normalize from openstack.cloud import exc -class NetworkCommonCloudMixin(_normalize.Normalizer): +class NetworkCommonCloudMixin: """Shared networking functions used by FloatingIP, Network, Compute classes """ From 3e84351cb8a4904bc1a3eeb1fd2e967bc79d76b1 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 31 Dec 2021 15:37:28 +0100 Subject: [PATCH 2994/3836] Add QoS min pps rule object and CRUD operations Partial-Bug: #1922237 See-Also: https://review.opendev.org/785236 Change-Id: Ibde213f047d8c8cac8f54c418713703c3e6a40ee --- doc/source/user/proxies/network.rst | 6 + doc/source/user/resources/network/index.rst | 1 + .../v2/qos_minimum_packet_rate_rule.rst | 13 ++ openstack/network/v2/_proxy.py | 128 ++++++++++++++++++ .../v2/qos_minimum_packet_rate_rule.py | 37 +++++ .../v2/test_qos_minimum_packet_rate_rule.py | 89 ++++++++++++ openstack/tests/unit/network/v2/test_proxy.py | 76 ++++++++--- .../v2/test_qos_minimum_packet_rate_rule.py | 47 +++++++ .../qos-min-pps-rule-52df1b150b1d3f68.yaml | 5 + 9 files changed, 385 insertions(+), 17 deletions(-) create mode 100644 doc/source/user/resources/network/v2/qos_minimum_packet_rate_rule.rst create mode 100644 openstack/network/v2/qos_minimum_packet_rate_rule.py create mode 100644 openstack/tests/functional/network/v2/test_qos_minimum_packet_rate_rule.py create mode 100644 openstack/tests/unit/network/v2/test_qos_minimum_packet_rate_rule.py create mode 100644 releasenotes/notes/qos-min-pps-rule-52df1b150b1d3f68.yaml diff --git a/doc/source/user/proxies/network.rst b/doc/source/user/proxies/network.rst index f4f3a42e2..3fdbc7306 100644 --- a/doc/source/user/proxies/network.rst +++ b/doc/source/user/proxies/network.rst @@ -119,6 +119,12 @@ QoS Operations get_qos_minimum_bandwidth_rule, find_qos_minimum_bandwidth_rule, qos_minimum_bandwidth_rules, + create_qos_minimum_packet_rate_rule, + update_qos_minimum_packet_rate_rule, + delete_qos_minimum_packet_rate_rule, + get_qos_minimum_packet_rate_rule, + find_qos_minimum_packet_rate_rule, + qos_minimum_packet_rate_rules, create_qos_bandwidth_limit_rule, update_qos_bandwidth_limit_rule, delete_qos_bandwidth_limit_rule, diff --git a/doc/source/user/resources/network/index.rst b/doc/source/user/resources/network/index.rst index c8e365823..4e9cc2ea0 100644 --- a/doc/source/user/resources/network/index.rst +++ b/doc/source/user/resources/network/index.rst @@ -30,6 +30,7 @@ Network Resources v2/qos_bandwidth_limit_rule v2/qos_dscp_marking_rule v2/qos_minimum_bandwidth_rule + v2/qos_minimum_packet_rate_rule v2/qos_policy v2/qos_rule_type v2/quota diff --git a/doc/source/user/resources/network/v2/qos_minimum_packet_rate_rule.rst b/doc/source/user/resources/network/v2/qos_minimum_packet_rate_rule.rst new file mode 100644 index 000000000..19b7b0176 --- /dev/null +++ b/doc/source/user/resources/network/v2/qos_minimum_packet_rate_rule.rst @@ -0,0 +1,13 @@ +openstack.network.v2.qos_minimum_packet_rate_rule +================================================= + +.. automodule:: openstack.network.v2.qos_minimum_packet_rate_rule + +The QoSMinimumPacketRateRule Class +---------------------------------- + +The ``QoSMinimumPacketRateRule`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule + :members: diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 634054eb8..788b2f9e8 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -48,6 +48,8 @@ _qos_dscp_marking_rule from openstack.network.v2 import qos_minimum_bandwidth_rule as \ _qos_minimum_bandwidth_rule +from openstack.network.v2 import qos_minimum_packet_rate_rule as \ + _qos_minimum_packet_rate_rule from openstack.network.v2 import qos_policy as _qos_policy from openstack.network.v2 import qos_rule_type as _qos_rule_type from openstack.network.v2 import quota as _quota @@ -2715,6 +2717,132 @@ def update_qos_minimum_bandwidth_rule(self, qos_rule, qos_policy, QoSMinimumBandwidthRule, qos_rule, qos_policy_id=policy.id, **attrs) + def create_qos_minimum_packet_rate_rule(self, qos_policy, **attrs): + """Create a new minimum packet rate rule + + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.network.v2.qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule`, + comprised of the properties on the QoSMinimumPacketRateRule class. + :param qos_policy: The value can be the ID of the QoS policy that the + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. + + :returns: The results of resource creation + :rtype: + :class:`~openstack.network.v2.qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule` + """ + policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) + return self._create( + _qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule, + qos_policy_id=policy.id, **attrs) + + def delete_qos_minimum_packet_rate_rule(self, qos_rule, qos_policy, + ignore_missing=True): + """Delete a minimum packet rate rule + + :param qos_rule: The value can be either the ID of a minimum packet + rate rule or a + :class:`~openstack.network.v2.qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule` + instance. + :param qos_policy: The value can be the ID of the QoS policy that the + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the resource does not exist. When set to ``True``, no exception + will be set when attempting to delete a nonexistent minimum packet + rate rule. + + :returns: ``None`` + """ + policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) + self._delete(_qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule, + qos_rule, ignore_missing=ignore_missing, + qos_policy_id=policy.id) + + def find_qos_minimum_packet_rate_rule(self, qos_rule_id, qos_policy, + ignore_missing=True, **args): + """Find a minimum packet rate rule + + :param qos_rule_id: The ID of a minimum packet rate rule. + :param qos_policy: The value can be the ID of the QoS policy that the + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the resource does not exist. When set to ``True``, None will be + returned when attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. + :returns: One + :class:`~openstack.network.v2.qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule` + or None + """ + policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) + return self._find( + _qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule, + qos_rule_id, ignore_missing=ignore_missing, + qos_policy_id=policy.id, **args) + + def get_qos_minimum_packet_rate_rule(self, qos_rule, qos_policy): + """Get a single minimum packet rate rule + + :param qos_rule: The value can be the ID of a minimum packet rate rule + or a + :class:`~openstack.network.v2.qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule` + instance. + :param qos_policy: The value can be the ID of the QoS policy that the + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. + :returns: One + :class:`~openstack.network.v2.qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource can be found. + """ + policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) + return self._get( + _qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule, + qos_rule, qos_policy_id=policy.id) + + def qos_minimum_packet_rate_rules(self, qos_policy, **query): + """Return a generator of minimum packet rate rules + + :param qos_policy: The value can be the ID of the QoS policy that the + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. + :param kwargs query: Optional query parameters to be sent to limit the + resources being returned. + :returns: A generator of minimum packet rate rule objects + :rtype: + :class:`~openstack.network.v2.qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule` + """ + policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) + return self._list( + _qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule, + qos_policy_id=policy.id, **query) + + def update_qos_minimum_packet_rate_rule(self, qos_rule, qos_policy, + **attrs): + """Update a minimum packet rate rule + + :param qos_rule: Either the id of a minimum packet rate rule or a + :class:`~openstack.network.v2.qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule` + instance. + :param qos_policy: The value can be the ID of the QoS policy that the + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. + :attrs kwargs: The attributes to update on the minimum packet rate rule + represented by ``value``. + + :returns: The updated minimum packet rate rule + :rtype: + :class:`~openstack.network.v2.qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule` + """ + policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) + return self._update(_qos_minimum_packet_rate_rule. + QoSMinimumPacketRateRule, qos_rule, + qos_policy_id=policy.id, **attrs) + def create_qos_policy(self, **attrs): """Create a new QoS policy from attributes diff --git a/openstack/network/v2/qos_minimum_packet_rate_rule.py b/openstack/network/v2/qos_minimum_packet_rate_rule.py new file mode 100644 index 000000000..4b727ad5d --- /dev/null +++ b/openstack/network/v2/qos_minimum_packet_rate_rule.py @@ -0,0 +1,37 @@ +# 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 resource + + +class QoSMinimumPacketRateRule(resource.Resource): + resource_key = 'minimum_packet_rate_rule' + resources_key = 'minimum_packet_rate_rules' + base_path = '/qos/policies/%(qos_policy_id)s/minimum_packet_rate_rules' + + _allow_unknown_attrs_in_body = True + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + # Properties + #: Traffic direction from the tenant point of view. Valid values: ('any', + #: 'egress', 'ingress') + direction = resource.Body('direction') + #: Minimum packet rate in kpps. + min_kpps = resource.Body('min_kpps') + #: The ID of the QoS policy who owns rule. + qos_policy_id = resource.URI('qos_policy_id') diff --git a/openstack/tests/functional/network/v2/test_qos_minimum_packet_rate_rule.py b/openstack/tests/functional/network/v2/test_qos_minimum_packet_rate_rule.py new file mode 100644 index 000000000..7fbca21a3 --- /dev/null +++ b/openstack/tests/functional/network/v2/test_qos_minimum_packet_rate_rule.py @@ -0,0 +1,89 @@ +# 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.network.v2 import (qos_minimum_packet_rate_rule as + _qos_minimum_packet_rate_rule) +from openstack.tests.functional import base + + +class TestQoSMinimumPacketRateRule(base.BaseFunctionalTest): + + QOS_POLICY_ID = None + QOS_IS_SHARED = False + QOS_POLICY_DESCRIPTION = "QoS policy description" + RULE_ID = None + RULE_MIN_KPPS = 1200 + RULE_MIN_KPPS_NEW = 1800 + RULE_DIRECTION = 'egress' + RULE_DIRECTION_NEW = 'ingress' + + def setUp(self): + super(TestQoSMinimumPacketRateRule, self).setUp() + self.QOS_POLICY_NAME = self.getUniqueString() + qos_policy = self.conn.network.create_qos_policy( + description=self.QOS_POLICY_DESCRIPTION, + name=self.QOS_POLICY_NAME, + shared=self.QOS_IS_SHARED, + ) + self.QOS_POLICY_ID = qos_policy.id + qos_min_pps_rule = ( + self.conn.network.create_qos_minimum_packet_rate_rule( + self.QOS_POLICY_ID, direction=self.RULE_DIRECTION, + min_kpps=self.RULE_MIN_KPPS)) + assert isinstance( + qos_min_pps_rule, + _qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule) + self.assertEqual(self.RULE_MIN_KPPS, qos_min_pps_rule.min_kpps) + self.assertEqual(self.RULE_DIRECTION, qos_min_pps_rule.direction) + self.RULE_ID = qos_min_pps_rule.id + + def tearDown(self): + rule = self.conn.network.delete_qos_minimum_packet_rate_rule( + self.RULE_ID, + self.QOS_POLICY_ID) + qos_policy = self.conn.network.delete_qos_policy(self.QOS_POLICY_ID) + self.assertIsNone(rule) + self.assertIsNone(qos_policy) + super(TestQoSMinimumPacketRateRule, self).tearDown() + + def test_find(self): + sot = self.conn.network.find_qos_minimum_packet_rate_rule( + self.RULE_ID, + self.QOS_POLICY_ID) + self.assertEqual(self.RULE_ID, sot.id) + self.assertEqual(self.RULE_DIRECTION, sot.direction) + self.assertEqual(self.RULE_MIN_KPPS, sot.min_kpps) + + def test_get(self): + sot = self.conn.network.get_qos_minimum_packet_rate_rule( + self.RULE_ID, + self.QOS_POLICY_ID) + self.assertEqual(self.RULE_ID, sot.id) + self.assertEqual(self.QOS_POLICY_ID, sot.qos_policy_id) + self.assertEqual(self.RULE_DIRECTION, sot.direction) + self.assertEqual(self.RULE_MIN_KPPS, sot.min_kpps) + + def test_list(self): + rule_ids = [o.id for o in + self.conn.network.qos_minimum_packet_rate_rules( + self.QOS_POLICY_ID)] + self.assertIn(self.RULE_ID, rule_ids) + + def test_update(self): + sot = self.conn.network.update_qos_minimum_packet_rate_rule( + self.RULE_ID, + self.QOS_POLICY_ID, + min_kpps=self.RULE_MIN_KPPS_NEW, + direction=self.RULE_DIRECTION_NEW) + self.assertEqual(self.RULE_MIN_KPPS_NEW, sot.min_kpps) + self.assertEqual(self.RULE_DIRECTION_NEW, sot.direction) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 57ffe83fc..d3093f445 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -45,6 +45,7 @@ from openstack.network.v2 import qos_bandwidth_limit_rule from openstack.network.v2 import qos_dscp_marking_rule from openstack.network.v2 import qos_minimum_bandwidth_rule +from openstack.network.v2 import qos_minimum_packet_rate_rule from openstack.network.v2 import qos_policy from openstack.network.v2 import qos_rule_type from openstack.network.v2 import quota @@ -952,29 +953,70 @@ def test_qos_minimum_bandwidth_rule_update(self): 'qos_policy_id': QOS_POLICY_ID, 'foo': 'bar'}) -class TestNetworkQosPolicy(TestNetworkProxy): - def test_qos_policy_create_attrs(self): - self.verify_create(self.proxy.create_qos_policy, qos_policy.QoSPolicy) +class TestNetworkQosMinimumPacketRate(TestNetworkProxy): + def test_qos_minimum_packet_rate_rule_create_attrs(self): + self.verify_create( + self.proxy.create_qos_minimum_packet_rate_rule, + qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule, + method_kwargs={'qos_policy': QOS_POLICY_ID}, + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) - def test_qos_policy_delete(self): - self.verify_delete(self.proxy.delete_qos_policy, qos_policy.QoSPolicy, - False) + def test_qos_minimum_packet_rate_rule_delete(self): + self.verify_delete( + self.proxy.delete_qos_minimum_packet_rate_rule, + qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule, + ignore_missing=False, + method_args=["resource_or_id", QOS_POLICY_ID], + expected_args=["resource_or_id"], + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) - def test_qos_policy_delete_ignore(self): - self.verify_delete(self.proxy.delete_qos_policy, qos_policy.QoSPolicy, - True) + def test_qos_minimum_packet_rate_rule_delete_ignore(self): + self.verify_delete( + self.proxy.delete_qos_minimum_packet_rate_rule, + qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule, + ignore_missing=True, + method_args=["resource_or_id", QOS_POLICY_ID], + expected_args=["resource_or_id"], + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) - def test_qos_policy_find(self): - self.verify_find(self.proxy.find_qos_policy, qos_policy.QoSPolicy) + def test_qos_minimum_packet_rate_rule_find(self): + policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) + self._verify( + 'openstack.proxy.Proxy._find', + self.proxy.find_qos_minimum_packet_rate_rule, + method_args=['rule_id', policy], + expected_args=[ + qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule, + 'rule_id'], + expected_kwargs={ + 'ignore_missing': True, 'qos_policy_id': QOS_POLICY_ID}) - def test_qos_policy_get(self): - self.verify_get(self.proxy.get_qos_policy, qos_policy.QoSPolicy) + def test_qos_minimum_packet_rate_rule_get(self): + self.verify_get( + self.proxy.get_qos_minimum_packet_rate_rule, + qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule, + method_kwargs={'qos_policy': QOS_POLICY_ID}, + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) - def test_qos_policies(self): - self.verify_list(self.proxy.qos_policies, qos_policy.QoSPolicy) + def test_qos_minimum_packet_rate_rules(self): + self.verify_list( + self.proxy.qos_minimum_packet_rate_rules, + qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule, + method_kwargs={'qos_policy': QOS_POLICY_ID}, + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) - def test_qos_policy_update(self): - self.verify_update(self.proxy.update_qos_policy, qos_policy.QoSPolicy) + def test_qos_minimum_packet_rate_rule_update(self): + policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) + self._verify( + 'openstack.network.v2._proxy.Proxy._update', + self.proxy.update_qos_minimum_packet_rate_rule, + method_args=['rule_id', policy], + method_kwargs={'foo': 'bar'}, + expected_args=[ + qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule, + 'rule_id'], + expected_kwargs={ + 'qos_policy_id': QOS_POLICY_ID, 'foo': 'bar'}) class TestNetworkQosRuleType(TestNetworkProxy): diff --git a/openstack/tests/unit/network/v2/test_qos_minimum_packet_rate_rule.py b/openstack/tests/unit/network/v2/test_qos_minimum_packet_rate_rule.py new file mode 100644 index 000000000..ebf430957 --- /dev/null +++ b/openstack/tests/unit/network/v2/test_qos_minimum_packet_rate_rule.py @@ -0,0 +1,47 @@ +# 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 uuid + +from openstack.network.v2 import qos_minimum_packet_rate_rule +from openstack.tests.unit import base + + +EXAMPLE = { + 'id': 'IDENTIFIER', + 'qos_policy_id': 'qos-policy-' + uuid.uuid4().hex, + 'min_kpps': 1500, + 'direction': 'any', +} + + +class TestQoSMinimumPacketRateRule(base.TestCase): + + def test_basic(self): + sot = qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule() + self.assertEqual('minimum_packet_rate_rule', sot.resource_key) + self.assertEqual('minimum_packet_rate_rules', sot.resources_key) + self.assertEqual( + '/qos/policies/%(qos_policy_id)s/minimum_packet_rate_rules', + sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['qos_policy_id'], sot.qos_policy_id) + self.assertEqual(EXAMPLE['min_kpps'], sot.min_kpps) + self.assertEqual(EXAMPLE['direction'], sot.direction) diff --git a/releasenotes/notes/qos-min-pps-rule-52df1b150b1d3f68.yaml b/releasenotes/notes/qos-min-pps-rule-52df1b150b1d3f68.yaml new file mode 100644 index 000000000..dfa95c708 --- /dev/null +++ b/releasenotes/notes/qos-min-pps-rule-52df1b150b1d3f68.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added QoS minimum packet rate rule object and introduced support for CRUD + operations. From cadee6fa12653bb27fc929d3653aa57a329fbdd9 Mon Sep 17 00:00:00 2001 From: Polina Gubina Date: Thu, 8 Jul 2021 16:01:45 +0300 Subject: [PATCH 2995/3836] Functional tests for vpn ike policy resource Change-Id: Ic7b494c24d26e0ff51ba26a6fb2b6b7c064d4759 --- .../functional/network/v2/test_ikepolicy.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 openstack/tests/functional/network/v2/test_ikepolicy.py diff --git a/openstack/tests/functional/network/v2/test_ikepolicy.py b/openstack/tests/functional/network/v2/test_ikepolicy.py new file mode 100644 index 000000000..7abfbddb8 --- /dev/null +++ b/openstack/tests/functional/network/v2/test_ikepolicy.py @@ -0,0 +1,55 @@ +# 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.network.v2 import ikepolicy +from openstack.tests.functional import base + + +class TestIkePolicy(base.BaseFunctionalTest): + + ID = None + + def setUp(self): + super(TestIkePolicy, self).setUp() + if not self.conn._has_neutron_extension('vpnaas_v2'): + self.skipTest('vpnaas_v2 service not supported by cloud') + self.IKEPOLICY_NAME = self.getUniqueString('ikepolicy') + self.UPDATE_NAME = self.getUniqueString('ikepolicy-updated') + policy = self.conn.network.create_vpn_ikepolicy( + name=self.IKEPOLICY_NAME) + assert isinstance(policy, ikepolicy.IkePolicy) + self.assertEqual(self.IKEPOLICY_NAME, policy.name) + self.ID = policy.id + + def tearDown(self): + ikepolicy = self.conn.network.\ + delete_vpn_ikepolicy(self.ID, ignore_missing=True) + self.assertIsNone(ikepolicy) + super(TestIkePolicy, self).tearDown() + + def test_list(self): + policies = [f.name for f in self.conn.network.vpn_ikepolicies()] + self.assertIn(self.IKEPOLICY_NAME, policies) + + def test_find(self): + policy = self.conn.network.find_vpn_ikepolicy(self.IKEPOLICY_NAME) + self.assertEqual(self.ID, policy.id) + + def test_get(self): + policy = self.conn.network.get_vpn_ikepolicy(self.ID) + self.assertEqual(self.IKEPOLICY_NAME, policy.name) + self.assertEqual(self.ID, policy.id) + + def test_update(self): + policy = self.conn.network.update_vpn_ikepolicy( + self.ID, name=self.UPDATE_NAME) + self.assertEqual(self.UPDATE_NAME, policy.name) From 3c8826d05cfb794fb89366aa8cc26a56bcbc2c21 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 28 Dec 2021 15:40:38 +0100 Subject: [PATCH 2996/3836] Get rid of normalization in compute CL Stop using normalization in the compute cloud layer. Change-Id: I987161bfdedf837298dfa56173bfdd07d066cbbf --- openstack/cloud/_compute.py | 24 +++++++------------ .../tests/functional/cloud/test_flavor.py | 2 +- .../tests/functional/cloud/test_inventory.py | 3 --- .../tests/functional/cloud/test_limits.py | 15 ++++++------ .../tests/unit/cloud/test_security_groups.py | 3 +-- 5 files changed, 18 insertions(+), 29 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index f59d98e6b..7506e9e37 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -163,7 +163,7 @@ def list_flavors(self, get_extra=False): def list_server_security_groups(self, server): """List all security groups associated with the given server. - :returns: A list of security group ``munch.Munch``. + :returns: A list of security group dictionary objects. """ # Don't even try if we're a cloud that doesn't have them @@ -174,7 +174,7 @@ def list_server_security_groups(self, server): server.fetch_security_groups(self.compute) - return self._normalize_secgroups(server.security_groups) + return server.security_groups def _get_server_security_groups(self, server, security_groups): if not self._has_secgroups(): @@ -319,17 +319,11 @@ def list_servers(self, detailed=False, all_projects=False, bare=False, def _list_servers(self, detailed=False, all_projects=False, bare=False, filters=None): filters = filters or {} - servers = [ - # TODO(mordred) Add original_names=False here and update the - # normalize file for server. Then, just remove the normalize call - # and the to_munch call. - self._normalize_server(server._to_munch()) - for server in self.compute.servers( - all_projects=all_projects, allow_unknown_params=True, - **filters)] return [ self._expand_server(server, detailed, bare) - for server in servers + for server in self.compute.servers( + all_projects=all_projects, allow_unknown_params=True, + **filters) ] def list_server_groups(self): @@ -347,7 +341,7 @@ def get_compute_limits(self, name_or_id=None): if different from the current project :raises: OpenStackCloudException if it's not a valid project - :returns: Munch object with the limits + :returns: :class:`~openstack.compute.v2.limits.Limits` object. """ params = {} project_id = None @@ -358,8 +352,7 @@ def get_compute_limits(self, name_or_id=None): raise exc.OpenStackCloudException("project does not exist") project_id = proj.id params['tenant_id'] = project_id - limits = self.compute.get_limits(**params) - return self._normalize_compute_limits(limits, project_id=project_id) + return self.compute.get_limits(**params) def get_keypair(self, name_or_id, filters=None): """Get a keypair by name or ID. @@ -1421,8 +1414,7 @@ def list_flavor_access(self, flavor_id): :raises: OpenStackCloudException on operation error. """ - access = self.compute.get_flavor_access(flavor_id) - return _utils.normalize_flavor_accesses(access) + return self.compute.get_flavor_access(flavor_id) def list_hypervisors(self, filters={}): """List all hypervisors diff --git a/openstack/tests/functional/cloud/test_flavor.py b/openstack/tests/functional/cloud/test_flavor.py index e7b12b47c..12e24057d 100644 --- a/openstack/tests/functional/cloud/test_flavor.py +++ b/openstack/tests/functional/cloud/test_flavor.py @@ -129,7 +129,7 @@ def test_flavor_access(self): # the demo_cloud access. acls = self.operator_cloud.list_flavor_access(new_flavor['id']) self.assertEqual(1, len(acls)) - self.assertEqual(project['id'], acls[0]['project_id']) + self.assertEqual(project['id'], acls[0]['tenant_id']) # Now revoke the access and make sure we can't find it self.operator_cloud.remove_flavor_access(new_flavor['id'], diff --git a/openstack/tests/functional/cloud/test_inventory.py b/openstack/tests/functional/cloud/test_inventory.py index 0305f2278..9b9d52702 100644 --- a/openstack/tests/functional/cloud/test_inventory.py +++ b/openstack/tests/functional/cloud/test_inventory.py @@ -47,10 +47,7 @@ def _cleanup_server(self): def _test_host_content(self, host): self.assertEqual(host['image']['id'], self.image.id) - self.assertNotIn('links', host['image']) self.assertNotIn('id', host['flavor']) - self.assertNotIn('links', host['flavor']) - self.assertNotIn('links', host) self.assertIsInstance(host['volumes'], list) self.assertIsInstance(host['metadata'], dict) self.assertIn('interface_ip', host) diff --git a/openstack/tests/functional/cloud/test_limits.py b/openstack/tests/functional/cloud/test_limits.py index 5917edf39..c16a0e497 100644 --- a/openstack/tests/functional/cloud/test_limits.py +++ b/openstack/tests/functional/cloud/test_limits.py @@ -16,6 +16,7 @@ Functional tests for `shade` limits method """ +from openstack.compute.v2 import limits as _limits from openstack.tests.functional import base @@ -25,27 +26,27 @@ def test_get_our_compute_limits(self): '''Test quotas functionality''' limits = self.user_cloud.get_compute_limits() self.assertIsNotNone(limits) - self.assertTrue(hasattr(limits, 'max_server_meta')) - # Test normalize limits - self.assertFalse(hasattr(limits, 'maxImageMeta')) + self.assertIsInstance(limits, _limits.Limits) + self.assertIsNotNone(limits.absolute.server_meta) + self.assertIsNotNone(limits.absolute.image_meta) def test_get_other_compute_limits(self): '''Test quotas functionality''' limits = self.operator_cloud.get_compute_limits('demo') self.assertIsNotNone(limits) - self.assertTrue(hasattr(limits, 'max_server_meta')) + self.assertTrue(hasattr(limits.absolute, 'server_meta')) # Test normalize limits - self.assertFalse(hasattr(limits, 'maxImageMeta')) + self.assertFalse(hasattr(limits.absolute, 'maxImageMeta')) def test_get_our_volume_limits(self): '''Test quotas functionality''' limits = self.user_cloud.get_volume_limits() self.assertIsNotNone(limits) - self.assertFalse(hasattr(limits, 'maxTotalVolumes')) + self.assertFalse(hasattr(limits.absolute, 'maxTotalVolumes')) def test_get_other_volume_limits(self): '''Test quotas functionality''' limits = self.operator_cloud.get_volume_limits('demo') - self.assertFalse(hasattr(limits, 'maxTotalVolumes')) + self.assertFalse(hasattr(limits.absolute, 'maxTotalVolumes')) diff --git a/openstack/tests/unit/cloud/test_security_groups.py b/openstack/tests/unit/cloud/test_security_groups.py index 147887600..5bc817aec 100644 --- a/openstack/tests/unit/cloud/test_security_groups.py +++ b/openstack/tests/unit/cloud/test_security_groups.py @@ -623,9 +623,8 @@ def test_list_server_security_groups_nova(self): json={'security_groups': [nova_grp_dict]}), ]) groups = self.cloud.list_server_security_groups(server) - self.assertIn('location', groups[0]) self.assertEqual( - groups[0]['security_group_rules'][0]['remote_ip_prefix'], + groups[0]['rules'][0]['ip_range']['cidr'], nova_grp_dict['rules'][0]['ip_range']['cidr']) self.assert_calls() From 5e969fb49d96fd2071ecc8a379ec6e2c3c5c90bc Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 14 Jan 2022 16:41:23 +0000 Subject: [PATCH 2997/3836] Revert "Add "security_group_ids" to Port's query parameters" This reverts commit f7f9446d6e60e6255895ee477a61f2f24f1516ba. Reason for revert: merge conflict in the r1 merge change. It will be re-reverted once r1 successfully lands Change-Id: I884a13186c55f50ced0af3d43c0dc51cc4281bb6 --- openstack/network/v2/port.py | 1 - openstack/tests/unit/network/v2/test_port.py | 1 - 2 files changed, 2 deletions(-) diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index 6e0df60ab..a5a25ec8c 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -36,7 +36,6 @@ class Port(_base.NetworkResource, resource.TagMixin): is_admin_state_up='admin_state_up', is_port_security_enabled='port_security_enabled', project_id='tenant_id', - security_group_ids='security_groups', **resource.TagMixin._tag_query_parameters ) diff --git a/openstack/tests/unit/network/v2/test_port.py b/openstack/tests/unit/network/v2/test_port.py index 0ea82aae2..7fd88b631 100644 --- a/openstack/tests/unit/network/v2/test_port.py +++ b/openstack/tests/unit/network/v2/test_port.py @@ -99,7 +99,6 @@ def test_basic(self): "is_port_security_enabled": "port_security_enabled", "project_id": "tenant_id", - "security_group_ids": "security_groups", "limit": "limit", "marker": "marker", "any_tags": "tags-any", From afb49692f5bfab259af8c594eedbddcb52cbde81 Mon Sep 17 00:00:00 2001 From: Jan Hartkopf Date: Tue, 7 Dec 2021 18:24:25 +0100 Subject: [PATCH 2998/3836] fix creation of protected image Function _make_v2_image_params interpretes the value of the "protected" key as a string when creating a new image, although it should be a boolean. This causes a server-side schema validation error (400 Bad Request) after sending the request body created in this way. This patch fixes the issue by adding "is_protected" to the raw properties and also adds a unit test to validate correct behavior. Story: 2009729 Task: 44141 Closes-Bug: https://bugs.launchpad.net/python-glanceclient/+bug/1927215 Signed-off-by: Jan Hartkopf Change-Id: I3b318684ae8589acd688d0e724eca5703c25d85f --- openstack/image/v2/_proxy.py | 2 +- openstack/tests/unit/cloud/test_image.py | 2 +- openstack/tests/unit/image/v2/test_proxy.py | 22 +++++++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 72da40b52..f3248931c 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -26,7 +26,7 @@ # Rackspace returns this for intermittent import errors _IMAGE_ERROR_396 = "Image cannot be imported. Error code: '396'" _INT_PROPERTIES = ('min_disk', 'min_ram', 'size', 'virtual_size') -_RAW_PROPERTIES = ('protected', 'tags') +_RAW_PROPERTIES = ('is_protected', 'tags') class Proxy(_base_proxy.BaseImageProxy): diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 30a991a9b..9d61e335a 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -1279,7 +1279,7 @@ def test_create_image_put_protected(self): self._call_create_image( self.image_name, min_disk='0', min_ram=0, - properties={'int_v': 12345}, protected=False) + properties={'int_v': 12345}, is_protected=False) self.assert_calls() diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 0a2826872..9736ee652 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -194,6 +194,28 @@ def test_image_create_without_filename(self): container_format='fake_cformat', disk_format='fake_dformat', name='fake', properties=mock.ANY) + def test_image_create_protected(self): + self.proxy.find_image = mock.Mock() + + created_image = mock.Mock(spec=image.Image(id="id")) + self.proxy._create = mock.Mock() + self.proxy._create.return_value = created_image + self.proxy._create.return_value.image_import_methods = [] + + created_image.upload = mock.Mock() + created_image.upload.return_value = FakeResponse(response="", + status_code=200) + + properties = {"is_protected": True} + + self.proxy.create_image( + name="fake", data="data", container_format="bare", + disk_format="raw", **properties + ) + + args, kwargs = self.proxy._create.call_args + self.assertEqual(kwargs["is_protected"], True) + def test_image_upload_no_args(self): # container_format and disk_format are required args self.assertRaises(exceptions.InvalidRequest, self.proxy.upload_image) From dd9b95c179cea89a2a776a042d822553aa944853 Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Mon, 17 Jan 2022 09:44:09 +0100 Subject: [PATCH 2999/3836] Cloud / Floating service - reindentation of the docstrings Change-Id: Ib77fdab0fb067bb2ccf231d544e128e3c92e806f --- openstack/cloud/_floating_ip.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index 562a39ef5..8e91ac9cf 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -102,10 +102,10 @@ def get_floating_ip(self, id, filters=None): of this dictionary may, themselves, be dictionaries. Example:: { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } } OR @@ -113,7 +113,7 @@ def get_floating_ip(self, id, filters=None): Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A floating IP ``munch.Munch`` or None if no matching floating - IP is found. + IP is found. """ return _utils._get_entity(self, 'floating_ip', id, filters) @@ -250,7 +250,7 @@ def _neutron_available_floating_ips( :returns: a list of floating IP addresses. :raises: ``OpenStackCloudResourceNotFound``, if an external network - that meets the specified criteria cannot be found. + that meets the specified criteria cannot be found. """ if project_id is None: # Make sure we are only listing floatingIPs allocated the current @@ -309,7 +309,7 @@ def _nova_available_floating_ips(self, pool=None): :returns: a list of floating IP addresses. :raises: ``OpenStackCloudResourceNotFound``, if a floating IP pool - is not specified and cannot be found. + is not specified and cannot be found. """ with _utils.shade_exceptions( From 560e1f9178d22bad62c8a92719898be5b2bd7885 Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Mon, 17 Jan 2022 09:47:00 +0100 Subject: [PATCH 3000/3836] Cloud / Normalize service - reindentation of the docstrings Change-Id: Ica41d3250b035d7227af9e18d73f5bdc0ae84472 --- openstack/cloud/_normalize.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/openstack/cloud/_normalize.py b/openstack/cloud/_normalize.py index 92812b91d..f25a43a7c 100644 --- a/openstack/cloud/_normalize.py +++ b/openstack/cloud/_normalize.py @@ -593,16 +593,16 @@ def _normalize_floating_ips(self, ips): :returns: A list of normalized dicts with the following attributes:: - [ - { - "id": "this-is-a-floating-ip-id", - "fixed_ip_address": "192.0.2.10", - "floating_ip_address": "198.51.100.10", - "network": "this-is-a-net-or-pool-id", - "attached": True, - "status": "ACTIVE" - }, ... - ] + [ + { + "id": "this-is-a-floating-ip-id", + "fixed_ip_address": "192.0.2.10", + "floating_ip_address": "198.51.100.10", + "network": "this-is-a-net-or-pool-id", + "attached": True, + "status": "ACTIVE" + }, ... + ] """ return [ From 582e301be1652ad311c2cafd3fb1c653bb15b154 Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Mon, 17 Jan 2022 09:47:47 +0100 Subject: [PATCH 3001/3836] Cloud / Security group service - reindentation of the docstrings Change-Id: Ic5317b8fd91a3abfaa8c954cc685715effa6d590 --- openstack/cloud/_security_group.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openstack/cloud/_security_group.py b/openstack/cloud/_security_group.py index 7861e57ed..46427496d 100644 --- a/openstack/cloud/_security_group.py +++ b/openstack/cloud/_security_group.py @@ -76,10 +76,10 @@ def get_security_group(self, name_or_id, filters=None): of this dictionary may, themselves, be dictionaries. Example:: { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } } OR @@ -87,7 +87,7 @@ def get_security_group(self, name_or_id, filters=None): Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A security group ``munch.Munch`` or None if no matching - security group is found. + security group is found. """ return _utils._get_entity( @@ -130,7 +130,7 @@ def create_security_group(self, name, description, :raises: OpenStackCloudException on operation error. :raises: OpenStackCloudUnavailableFeature if security groups are - not supported on this cloud. + not supported on this cloud. """ # Security groups not supported @@ -166,7 +166,7 @@ def delete_security_group(self, name_or_id): :raises: OpenStackCloudException on operation error. :raises: OpenStackCloudUnavailableFeature if security groups are - not supported on this cloud. + not supported on this cloud. """ # Security groups not supported if not self._has_secgroups(): @@ -384,7 +384,7 @@ def delete_security_group_rule(self, rule_id): :raises: OpenStackCloudException on operation error. :raises: OpenStackCloudUnavailableFeature if security groups are - not supported on this cloud. + not supported on this cloud. """ # Security groups not supported if not self._has_secgroups(): From 0ce0fddc21fd3dfe5bb42c1c3b797bec60a05254 Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Mon, 17 Jan 2022 09:35:25 +0100 Subject: [PATCH 3002/3836] Cloud / Baremetal service - reindentation of the docstrings Change-Id: I111c6c4b25dc20b047484086179fff88eb42d147 --- openstack/cloud/_baremetal.py | 170 ++++++++++++++++------------------ 1 file changed, 80 insertions(+), 90 deletions(-) diff --git a/openstack/cloud/_baremetal.py b/openstack/cloud/_baremetal.py index 4d852d48d..e4dfe79e3 100644 --- a/openstack/cloud/_baremetal.py +++ b/openstack/cloud/_baremetal.py @@ -46,8 +46,8 @@ def list_nics(self): def list_nics_for_machine(self, uuid): """Returns a list of ports present on the machine node. - :param uuid: String representing machine UUID value in - order to identify the machine. + :param uuid: String representing machine UUID value in order to + identify the machine. :returns: A list of ports. """ # TODO(dtantsur): support node names here. @@ -80,7 +80,7 @@ def get_machine(self, name_or_id): :param name_or_id: A node name or UUID that will be looked up. :returns: ``munch.Munch`` representing the node found or None if no - nodes are found. + nodes are found. """ try: return self._normalize_machine(self.baremetal.get_node(name_or_id)) @@ -92,8 +92,8 @@ def get_machine_by_mac(self, mac): :param mac: Port MAC address to query in order to return a node. - :returns: ``munch.Munch`` representing the node found or None - if the node is not found. + :returns: ``munch.Munch`` representing the node found or None if the + node is not found. """ nic = self.get_nic_by_mac(mac) if nic is None: @@ -108,16 +108,16 @@ def inspect_machine(self, name_or_id, wait=False, timeout=3600): metadata about the baremetal machine. :param name_or_id: String representing machine name or UUID value in - order to identify the machine. + order to identify the machine. :param wait: Boolean value controlling if the method is to wait for - the desired state to be reached or a failure to occur. + the desired state to be reached or a failure to occur. - :param timeout: Integer value, defautling to 3600 seconds, for the$ - wait state to reach completion. + :param timeout: Integer value, defautling to 3600 seconds, for the + wait state to reach completion. :returns: ``munch.Munch`` representing the current state of the machine - upon exit of the method. + upon exit of the method. """ return_to_available = False @@ -175,36 +175,34 @@ def register_machine(self, nics, wait=False, timeout=3600, created are deleted, and the node is removed from Ironic. :param nics: - An array of MAC addresses that represent the - network interfaces for the node to be created. + An array of MAC addresses that represent the + network interfaces for the node to be created. - Example:: + Example:: - [ - {'mac': 'aa:bb:cc:dd:ee:01'}, - {'mac': 'aa:bb:cc:dd:ee:02'} - ] + [ + {'mac': 'aa:bb:cc:dd:ee:01'}, + {'mac': 'aa:bb:cc:dd:ee:02'} + ] - :param wait: Boolean value, defaulting to false, to wait for the - node to reach the available state where the node can be - provisioned. It must be noted, when set to false, the - method will still wait for locks to clear before sending - the next required command. + :param wait: Boolean value, defaulting to false, to wait for the node + to reach the available state where the node can be provisioned. It + must be noted, when set to false, the method will still wait for + locks to clear before sending the next required command. - :param timeout: Integer value, defautling to 3600 seconds, for the - wait state to reach completion. + :param timeout: Integer value, defautling to 3600 seconds, for the wait + state to reach completion. :param lock_timeout: Integer value, defaulting to 600 seconds, for - locks to clear. + locks to clear. :param kwargs: Key value pairs to be passed to the Ironic API, - including uuid, name, chassis_uuid, driver_info, - parameters. + including uuid, name, chassis_uuid, driver_info, parameters. :raises: OpenStackCloudException on operation error. :returns: Returns a ``munch.Munch`` representing the new - baremetal node. + baremetal node. """ msg = ("Baremetal machine node failed to be created.") @@ -332,14 +330,14 @@ def unregister_machine(self, nics, uuid, wait=None, timeout=600): from an Ironic API :param nics: An array of strings that consist of MAC addresses - to be removed. + to be removed. :param string uuid: The UUID of the node to be deleted. :param wait: DEPRECATED, do not use. :param timeout: Integer value, representing seconds with a default - value of 600, which controls the maximum amount of - time to block until a lock is released on machine. + value of 600, which controls the maximum amount of time to block + until a lock is released on machine. :raises: OpenStackCloudException on operation failure. """ @@ -382,27 +380,27 @@ def patch_machine(self, name_or_id, patch): :param string name_or_id: A machine name or UUID to be updated. :param patch: - The JSON Patch document is a list of dictonary objects - that comply with RFC 6902 which can be found at - https://tools.ietf.org/html/rfc6902. - - Example patch construction:: - - patch=[] - patch.append({ - 'op': 'remove', - 'path': '/instance_info' - }) - patch.append({ - 'op': 'replace', - 'path': '/name', - 'value': 'newname' - }) - patch.append({ - 'op': 'add', - 'path': '/driver_info/username', - 'value': 'administrator' - }) + The JSON Patch document is a list of dictonary objects that comply + with RFC 6902 which can be found at + https://tools.ietf.org/html/rfc6902. + + Example patch construction:: + + patch=[] + patch.append({ + 'op': 'remove', + 'path': '/instance_info' + }) + patch.append({ + 'op': 'replace', + 'path': '/name', + 'value': 'newname' + }) + patch.append({ + 'op': 'add', + 'path': '/driver_info/username', + 'value': 'administrator' + }) :raises: OpenStackCloudException on operation error. @@ -423,9 +421,9 @@ def update_machine(self, name_or_id, **attrs): :raises: OpenStackCloudException on operation error. :returns: ``munch.Munch`` containing a machine sub-dictonary consisting - of the updated data returned from the API update operation, - and a list named changes which contains all of the API paths - that received updates. + of the updated data returned from the API update operation, and a + list named changes which contains all of the API paths that + received updates. """ machine = self.get_machine(name_or_id) if not machine: @@ -493,10 +491,9 @@ def validate_machine(self, name_or_id, for_deploy=True): """Validate parameters of the machine. :param string name_or_id: The Name or UUID value representing the - baremetal node. + baremetal node. :param bool for_deploy: If ``True``, validate readiness for deployment, - otherwise validate only the power management - properties. + otherwise validate only the power management properties. :raises: :exc:`~openstack.exceptions.ValidationException` """ if for_deploy: @@ -522,27 +519,24 @@ def node_set_provision_state(self, config drive to be utilized. :param string name_or_id: The Name or UUID value representing the - baremetal node. - :param string state: The desired provision state for the - baremetal node. + baremetal node. + :param string state: The desired provision state for the baremetal + node. :param string configdrive: An optional URL or file or path - representing the configdrive. In the - case of a directory, the client API - will create a properly formatted - configuration drive file and post the - file contents to the API for - deployment. + representing the configdrive. In the case of a directory, the + client API will create a properly formatted configuration drive + file and post the file contents to the API for deployment. :param boolean wait: A boolean value, defaulted to false, to control - if the method will wait for the desire end state - to be reached before returning. + if the method will wait for the desire end state to be reached + before returning. :param integer timeout: Integer value, defaulting to 3600 seconds, - representing the amount of time to wait for - the desire end state to be reached. + representing the amount of time to wait for the desire end state to + be reached. :raises: OpenStackCloudException on operation error. :returns: ``munch.Munch`` representing the current state of the machine - upon exit of the method. + upon exit of the method. """ node = self.baremetal.set_node_provision_state( name_or_id, target=state, config_drive=configdrive, @@ -559,14 +553,13 @@ def set_machine_maintenance_state( Sets Baremetal maintenance state and maintenance reason. :param string name_or_id: The Name or UUID value representing the - baremetal node. - :param boolean state: The desired state of the node. True being in - maintenance where as False means the machine - is not in maintenance mode. This value - defaults to True if not explicitly set. + baremetal node. + :param boolean state: The desired state of the node. True being in + maintenance where as False means the machine is not in maintenance + mode. This value defaults to True if not explicitly set. :param string reason: An optional freeform string that is supplied to - the baremetal API to allow for notation as to why - the node is in maintenance state. + the baremetal API to allow for notation as to why the node is in + maintenance state. :raises: OpenStackCloudException on operation error. @@ -580,13 +573,13 @@ def set_machine_maintenance_state( def remove_machine_from_maintenance(self, name_or_id): """Remove Baremetal Machine from Maintenance State - Similarly to set_machine_maintenance_state, this method - removes a machine from maintenance state. It must be noted - that this method simpily calls set_machine_maintenace_state - for the name_or_id requested and sets the state to False. + Similarly to set_machine_maintenance_state, this method removes a + machine from maintenance state. It must be noted that this method + simpily calls set_machine_maintenace_state for the name_or_id requested + and sets the state to False. :param string name_or_id: The Name or UUID value representing the - baremetal node. + baremetal node. :raises: OpenStackCloudException on operation error. @@ -600,8 +593,7 @@ def set_machine_power_on(self, name_or_id): This is a method that sets the node power state to "on". :params string name_or_id: A string representing the baremetal - node to have power turned to an "on" - state. + node to have power turned to an "on" state. :raises: OpenStackCloudException on operation error. @@ -615,8 +607,7 @@ def set_machine_power_off(self, name_or_id): This is a method that sets the node power state to "off". :params string name_or_id: A string representing the baremetal - node to have power turned to an "off" - state. + node to have power turned to an "off" state. :raises: OpenStackCloudException on operation error. @@ -632,8 +623,7 @@ def set_machine_power_reboot(self, name_or_id): to "on". :params string name_or_id: A string representing the baremetal - node to have power turned to an "off" - state. + node to have power turned to an "off" state. :raises: OpenStackCloudException on operation error. From 63aa7c0063725f2bb889268aabf7e34e80db826e Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Mon, 17 Jan 2022 09:37:44 +0100 Subject: [PATCH 3003/3836] Cloud / Coe service - reindentation of the docstrings Change-Id: I95bac59d7aba421005d8bfc5e8cb08064e605127 --- openstack/cloud/_coe.py | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/openstack/cloud/_coe.py b/openstack/cloud/_coe.py index 3778341c2..6fef0e58c 100644 --- a/openstack/cloud/_coe.py +++ b/openstack/cloud/_coe.py @@ -70,10 +70,10 @@ def get_coe_cluster(self, name_or_id, filters=None): of this dictionary may, themselves, be dictionaries. Example:: { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } } OR @@ -91,8 +91,7 @@ def create_coe_cluster( :param string name: Name of the cluster. :param string image_id: ID of the cluster template to use. - - Other arguments will be passed in kwargs. + Other arguments will be passed in kwargs. :returns: a dict containing the cluster description @@ -144,8 +143,7 @@ def update_coe_cluster(self, name_or_id, operation, **kwargs): :param name_or_id: Name or ID of the COE cluster being updated. :param operation: Operation to perform - add, remove, replace. - - Other arguments will be passed with kwargs. + Other arguments will be passed with kwargs. :returns: a dict representing the updated cluster. @@ -195,9 +193,8 @@ def sign_coe_cluster_certificate(self, cluster_id, csr): :param cluster_id: UUID of the cluster. :param csr: Certificate Signing Request (CSR) for authenticating - client key.The CSR will be used by Magnum to generate - a signed certificate that client will use to communicate - with the cluster. + client key.The CSR will be used by Magnum to generate a signed + certificate that client will use to communicate with the cluster. :returns: a dict representing the signed certs. @@ -273,10 +270,10 @@ def get_cluster_template(self, name_or_id, filters=None, detail=False): of this dictionary may, themselves, be dictionaries. Example:: { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } } OR @@ -299,8 +296,7 @@ def create_cluster_template( :param string image_id: Name or ID of the image to use. :param string keypair_id: Name or ID of the keypair to use. :param string coe: Name of the coe for the cluster template. - - Other arguments will be passed in kwargs. + Other arguments will be passed in kwargs. :returns: a dict containing the cluster template description @@ -374,8 +370,7 @@ def update_cluster_template(self, name_or_id, operation, **kwargs): :param name_or_id: Name or ID of the cluster template being updated. :param operation: Operation to perform - add, remove, replace. - - Other arguments will be passed with kwargs. + Other arguments will be passed with kwargs. :returns: a dict representing the updated cluster template. From 441b7d0cc4128059ef3e7880ef64257e8fe76090 Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Mon, 17 Jan 2022 09:43:03 +0100 Subject: [PATCH 3004/3836] Cloud / Dns service - reindentation of the docstrings Change-Id: I2e254a32e127292573a3d55af749eca528f3c7ee --- openstack/cloud/_dns.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openstack/cloud/_dns.py b/openstack/cloud/_dns.py index 9a5a95fa0..e767bc79d 100644 --- a/openstack/cloud/_dns.py +++ b/openstack/cloud/_dns.py @@ -63,11 +63,11 @@ def create_zone(self, name, zone_type=None, email=None, description=None, :param name: Name of the zone being created. :param zone_type: Type of the zone (primary/secondary) :param email: Email of the zone owner (only - applies if zone_type is primary) + applies if zone_type is primary) :param description: Description of the zone :param ttl: TTL (Time to live) value in seconds :param masters: Master nameservers (only applies - if zone_type is secondary) + if zone_type is secondary) :returns: a dict representing the created zone. @@ -110,11 +110,11 @@ def update_zone(self, name_or_id, **kwargs): :param name_or_id: Name or ID of the zone being updated. :param email: Email of the zone owner (only - applies if zone_type is primary) + applies if zone_type is primary) :param description: Description of the zone :param ttl: TTL (Time to live) value in seconds :param masters: Master nameservers (only applies - if zone_type is secondary) + if zone_type is secondary) :returns: a dict representing the updated zone. From 2d69560ed7d90bedfb3b524c87f796e1af9393b3 Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Mon, 17 Jan 2022 09:39:13 +0100 Subject: [PATCH 3005/3836] Cloud / Compute service - reindentation of the docstrings Change-Id: I0874ff07038c95515ea12a8fb22010449c1b4fdb --- openstack/cloud/_compute.py | 217 +++++++++++++++++------------------- 1 file changed, 103 insertions(+), 114 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 7506e9e37..4708d004b 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -127,10 +127,10 @@ def list_availability_zone_names(self, unavailable=False): """List names of availability zones. :param bool unavailable: Whether or not to include unavailable zones - in the output. Defaults to False. + in the output. Defaults to False. :returns: A list of availability zone names, or an empty list if the - list could not be fetched. + list could not be fetched. """ try: zones = self.compute.availability_zones() @@ -150,9 +150,8 @@ def list_flavors(self, get_extra=False): """List all available flavors. :param get_extra: Whether or not to fetch extra specs for each flavor. - Defaults to True. Default behavior value can be - overridden in clouds.yaml by setting - openstack.cloud.get_extra_specs to False. + Defaults to True. Default behavior value can be overridden in + clouds.yaml by setting openstack.cloud.get_extra_specs to False. :returns: A list of flavor ``munch.Munch``. """ @@ -270,13 +269,13 @@ def list_servers(self, detailed=False, all_projects=False, bare=False, """List all available servers. :param detailed: Whether or not to add detailed additional information. - Defaults to False. + Defaults to False. :param all_projects: Whether to list servers from all projects or just - the current auth scoped project. + the current auth scoped project. :param bare: Whether to skip adding any additional information to the - server record. Defaults to False, meaning the addresses - dict will be populated as needed from neutron. Setting - to True implies detailed = False. + server record. Defaults to False, meaning the addresses dict will + be populated as needed from neutron. Setting to True implies + detailed = False. :param filters: Additional query parameters passed to the API server. :returns: A list of server ``munch.Munch``. @@ -338,7 +337,7 @@ def get_compute_limits(self, name_or_id=None): """ Get compute limits for a project :param name_or_id: (optional) project name or ID to get limits for - if different from the current project + if different from the current project :raises: OpenStackCloudException if it's not a valid project :returns: :class:`~openstack.compute.v2.limits.Limits` object. @@ -363,10 +362,10 @@ def get_keypair(self, name_or_id, filters=None): of this dictionary may, themselves, be dictionaries. Example:: { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } } OR @@ -374,7 +373,7 @@ def get_keypair(self, name_or_id, filters=None): Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A keypair ``munch.Munch`` or None if no matching keypair is - found. + found. """ return _utils._get_entity(self, 'keypair', name_or_id, filters) @@ -387,18 +386,17 @@ def get_flavor(self, name_or_id, filters=None, get_extra=True): of this dictionary may, themselves, be dictionaries. Example:: { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } } OR A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - :param get_extra: - Whether or not the list_flavors call should get the extra flavor - specs. + :param get_extra: Whether or not the list_flavors call should get the + extra flavor specs. :returns: A flavor ``munch.Munch`` or None if no matching flavor is found. @@ -416,8 +414,8 @@ def get_flavor_by_id(self, id, get_extra=False): :param id: ID of the flavor. :param get_extra: - Whether or not the list_flavors call should get the extra flavor - specs. + Whether or not the list_flavors call should get the extra flavor + specs. :returns: A flavor ``munch.Munch``. """ return self.compute.get_flavor(id, get_extra_specs=get_extra) @@ -426,14 +424,14 @@ def get_server_console(self, server, length=None): """Get the console log for a server. :param server: The server to fetch the console log for. Can be either - a server dict or the Name or ID of the server. + a server dict or the Name or ID of the server. :param int length: The number of lines you would like to retrieve from - the end of the log. (optional, defaults to all) + the end of the log. (optional, defaults to all) :returns: A string containing the text of the console log or an - empty string if the cloud does not support console logs. + empty string if the cloud does not support console logs. :raises: OpenStackCloudException if an invalid server argument is given - or if something else unforseen happens + or if something else unforseen happens """ if not isinstance(server, dict): @@ -467,26 +465,26 @@ def get_server( of this dictionary may, themselves, be dictionaries. Example:: { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } } OR A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :param detailed: Whether or not to add detailed additional information. - Defaults to False. + Defaults to False. :param bare: Whether to skip adding any additional information to the - server record. Defaults to False, meaning the addresses - dict will be populated as needed from neutron. Setting - to True implies detailed = False. + server record. Defaults to False, meaning the addresses dict will + be populated as needed from neutron. Setting to True implies + detailed = False. :param all_projects: Whether to get server from all projects or just - the current auth scoped project. + the current auth scoped project. :returns: A server ``munch.Munch`` or None if no matching server is - found. + found. """ searchfunc = functools.partial(self.search_servers, @@ -525,7 +523,7 @@ def get_server_group(self, name_or_id=None, filters=None): of this dictionary may, themselves, be dictionaries. Example:: { - 'policy': 'affinity', + 'policy': 'affinity', } OR @@ -533,7 +531,7 @@ def get_server_group(self, name_or_id=None, filters=None): Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A server groups dict or None if no matching server group - is found. + is found. """ return _utils._get_entity(self, 'server_group', name_or_id, @@ -575,14 +573,14 @@ def create_image_snapshot( """Create an image by snapshotting an existing server. ..note:: - On most clouds this is a cold snapshot - meaning that the server - in question will be shutdown before taking the snapshot. It is - possible that it's a live snapshot - but there is no way to know - as a user, so caveat emptor. + On most clouds this is a cold snapshot - meaning that the server in + question will be shutdown before taking the snapshot. It is + possible that it's a live snapshot - but there is no way to know as + a user, so caveat emptor. :param name: Name of the image to be created :param server: Server name or ID or dict representing the server - to be snapshotted + to be snapshotted :param wait: If true, waits for image to be created. :param timeout: Seconds to wait for image creation. None is forever. :param metadata: Metadata to give newly-created image entity @@ -640,84 +638,76 @@ def create_server( :param name: Something to name the server. :param image: Image dict, name or ID to boot with. image is required - unless boot_volume is given. + unless boot_volume is given. :param flavor: Flavor dict, name or ID to boot onto. :param auto_ip: Whether to take actions to find a routable IP for - the server. (defaults to True) + the server. (defaults to True) :param ips: List of IPs to attach to the server (defaults to None) :param ip_pool: Name of the network or floating IP pool to get an - address from. (defaults to None) + address from. (defaults to None) :param root_volume: Name or ID of a volume to boot from - (defaults to None - deprecated, use boot_volume) + (defaults to None - deprecated, use boot_volume) :param boot_volume: Name or ID of a volume to boot from - (defaults to None) + (defaults to None) :param terminate_volume: If booting from a volume, whether it should - be deleted when the server is destroyed. - (defaults to False) + be deleted when the server is destroyed. (defaults to False) :param volumes: (optional) A list of volumes to attach to the server :param meta: (optional) A dict of arbitrary key/value metadata to - store for this server. Both keys and values must be - <=255 characters. + store for this server. Both keys and values must be <=255 + characters. :param files: (optional, deprecated) A dict of files to overwrite - on the server upon boot. Keys are file names (i.e. - ``/etc/passwd``) and values - are the file contents (either as a string or as a - file-like object). A maximum of five entries is allowed, - and each file must be 10k or less. + on the server upon boot. Keys are file names (i.e. + ``/etc/passwd``) and values are the file contents (either as a + string or as a file-like object). A maximum of five entries is + allowed, and each file must be 10k or less. :param reservation_id: a UUID for the set of servers being requested. - :param min_count: (optional extension) The minimum number of - servers to launch. - :param max_count: (optional extension) The maximum number of - servers to launch. + :param min_count: (optional extension) The minimum number of servers to + launch. + :param max_count: (optional extension) The maximum number of servers to + launch. :param security_groups: A list of security group names :param userdata: user data to pass to be exposed by the metadata - server this can be a file type object as well or a - string. + server this can be a file type object as well or a string. :param key_name: (optional extension) name of previously created - keypair to inject into the instance. + keypair to inject into the instance. :param availability_zone: Name of the availability zone for instance - placement. + placement. :param block_device_mapping: (optional) A dict of block - device mappings for this server. + device mappings for this server. :param block_device_mapping_v2: (optional) A dict of block - device mappings for this server. + device mappings for this server. :param nics: (optional extension) an ordered list of nics to be - added to this server, with information about - connected networks, fixed IPs, port etc. + added to this server, with information about connected networks, + fixed IPs, port etc. :param scheduler_hints: (optional extension) arbitrary key-value pairs - specified by the client to help boot an instance + specified by the client to help boot an instance :param config_drive: (optional extension) value for config drive - either boolean, or volume-id + either boolean, or volume-id :param disk_config: (optional extension) control how the disk is - partitioned when the server is created. possible - values are 'AUTO' or 'MANUAL'. + partitioned when the server is created. possible values are 'AUTO' + or 'MANUAL'. :param admin_pass: (optional extension) add a user supplied admin - password. + password. :param wait: (optional) Wait for the address to appear as assigned - to the server. Defaults to False. + to the server. Defaults to False. :param timeout: (optional) Seconds to wait, defaults to 60. - See the ``wait`` parameter. + See the ``wait`` parameter. :param reuse_ips: (optional) Whether to attempt to reuse pre-existing - floating ips should a floating IP be - needed (defaults to True) + floating ips should a floating IP be needed (defaults to True) :param network: (optional) Network dict or name or ID to attach the - server to. Mutually exclusive with the nics parameter. - Can also be a list of network names or IDs or - network dicts. + server to. Mutually exclusive with the nics parameter. Can also + be a list of network names or IDs or network dicts. :param boot_from_volume: Whether to boot from volume. 'boot_volume' - implies True, but boot_from_volume=True with - no boot_volume is valid and will create a - volume from the image and use that. + implies True, but boot_from_volume=True with no boot_volume is + valid and will create a volume from the image and use that. :param volume_size: When booting an image from volume, how big should - the created volume be? Defaults to 50. + the created volume be? Defaults to 50. :param nat_destination: Which network should a created floating IP - be attached to, if it's not possible to - infer from the cloud's configuration. - (Optional, defaults to None) + be attached to, if it's not possible to infer from the cloud's + configuration. (Optional, defaults to None) :param group: ServerGroup dict, name or id to boot the server in. - If a group is provided in both scheduler_hints and in - the group param, the group param will win. - (Optional, defaults to None) + If a group is provided in both scheduler_hints and in the group + param, the group param will win. (Optional, defaults to None) :returns: A ``munch.Munch`` representing the created server. :raises: OpenStackCloudException on operation error. """ @@ -1102,11 +1092,10 @@ def rebuild_server(self, server_id, image_id, admin_pass=None, def set_server_metadata(self, name_or_id, metadata): """Set metadata in a server instance. - :param str name_or_id: The name or ID of the server instance - to update. + :param str name_or_id: The name or ID of the server instance to update. :param dict metadata: A dictionary with the key=value pairs - to set in the server instance. It only updates the key=value - pairs provided. Existing ones will remain untouched. + to set in the server instance. It only updates the key=value pairs + provided. Existing ones will remain untouched. :raises: OpenStackCloudException on operation error. """ @@ -1246,13 +1235,13 @@ def update_server(self, name_or_id, detailed=False, bare=False, **kwargs): :param name_or_id: Name of the server to be updated. :param detailed: Whether or not to add detailed additional information. - Defaults to False. + Defaults to False. :param bare: Whether to skip adding any additional information to the - server record. Defaults to False, meaning the addresses - dict will be populated as needed from neutron. Setting - to True implies detailed = False. - :name: New name for the server - :description: New description for the server + server record. Defaults to False, meaning the addresses dict will + be populated as needed from neutron. Setting to True implies + detailed = False. + :param name: New name for the server + :param description: New description for the server :returns: a dictionary representing the updated server. @@ -1458,14 +1447,14 @@ def get_aggregate(self, name_or_id, filters=None): of this dictionary may, themselves, be dictionaries. Example:: { - 'availability_zone': 'nova', - 'metadata': { - 'cpu_allocation_ratio': '1.0' - } + 'availability_zone': 'nova', + 'metadata': { + 'cpu_allocation_ratio': '1.0' + } } :returns: An aggregate dict or None if no matching aggregate is - found. + found. """ aggregate = self.compute.find_aggregate( @@ -1534,7 +1523,7 @@ def set_aggregate_metadata(self, name_or_id, metadata): :param name_or_id: Name of the host aggregate to update :param metadata: Dict containing metadata to replace (Use - {'key': None} to remove a key) + {'key': None} to remove a key) :returns: a dict representing the new host aggregate. @@ -1611,7 +1600,7 @@ def delete_compute_quotas(self, name_or_id): :param name_or_id: project name or id :raises: OpenStackCloudException if it's not a valid project or the - nova client call failed + nova client call failed :returns: dict with the quotas """ @@ -1624,10 +1613,10 @@ def get_compute_usage(self, name_or_id, start=None, end=None): :param name_or_id: project name or id :param start: :class:`datetime.datetime` or string. Start date in UTC - Defaults to 2010-07-06T12:00:00Z (the date the OpenStack - project was started) + Defaults to 2010-07-06T12:00:00Z (the date the OpenStack project + was started) :param end: :class:`datetime.datetime` or string. End date in UTC. - Defaults to now + Defaults to now :raises: OpenStackCloudException if it's not a valid project :returns: Munch object with the usage From 1f1aa4fae0478a25144aa03a2507ada1d88d796c Mon Sep 17 00:00:00 2001 From: Nurmatov Mamatisa Date: Mon, 20 Dec 2021 10:55:27 +0300 Subject: [PATCH 3006/3836] Add query parameters to local ip Added additional query parameters according local ip API def [1] 1)https://review.opendev.org/c/openstack/neutron-lib/+/803051 Change-Id: Iade9a5d80ad38d7c63315ca1a49666b549561e16 --- openstack/network/v2/_proxy.py | 10 ++++++++++ openstack/network/v2/local_ip.py | 6 +++++- openstack/network/v2/local_ip_association.py | 2 +- openstack/tests/unit/network/v2/test_local_ip.py | 5 +++++ .../tests/unit/network/v2/test_local_ip_association.py | 4 ++-- 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 788b2f9e8..b47da00c5 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -696,6 +696,10 @@ def local_ips(self, **query): * ``name``: Local IP name * ``description``: Local IP description * ``project_id``: Owner project ID + * ``network_id``: Local IP network + * ``local_port_id``: Local port ID + * ``local_ip_address``: The IP address of a Local IP + * ``ip_mode``: The Local IP mode :returns: A generator of local ip objects :rtype: :class:`~openstack.network.v2.local_ip.LocalIP` @@ -817,6 +821,12 @@ def local_ip_associations(self, local_ip, **query): :param dict query: Optional query parameters to be sent to limit the resources being returned. + * ``fixed_port_id``: The ID of the port to which a local IP + is associated + * ``fixed_ip``: The fixed ip address associated with a + a Local IP + * ``host``: Host where local ip is associated + :returns: A generator of local ip association objects :rtype: :class:`~openstack.network.v2.local_ip_association.LocalIPAssociation` diff --git a/openstack/network/v2/local_ip.py b/openstack/network/v2/local_ip.py index e791cbca3..b2393b367 100644 --- a/openstack/network/v2/local_ip.py +++ b/openstack/network/v2/local_ip.py @@ -34,7 +34,11 @@ class LocalIP(resource.Resource): _query_mapping = resource.QueryParameters( "sort_key", "sort_dir", - 'name', 'description',) + 'name', 'description', + 'project_id', 'network_id', + 'local_port_id', 'local_ip_address', + 'ip_mode', + ) # Properties #: Timestamp at which the floating IP was created. diff --git a/openstack/network/v2/local_ip_association.py b/openstack/network/v2/local_ip_association.py index d99ef3bed..310c6d6f2 100644 --- a/openstack/network/v2/local_ip_association.py +++ b/openstack/network/v2/local_ip_association.py @@ -32,7 +32,7 @@ class LocalIPAssociation(resource.Resource): _allow_unknown_attrs_in_body = True _query_mapping = resource.QueryParameters( - 'local_ip_address', 'fixed_port_id', 'fixed_ip' + 'fixed_port_id', 'fixed_ip', 'host', ) # Properties #: The fixed port ID. diff --git a/openstack/tests/unit/network/v2/test_local_ip.py b/openstack/tests/unit/network/v2/test_local_ip.py index 2dce8576a..4752665f9 100644 --- a/openstack/tests/unit/network/v2/test_local_ip.py +++ b/openstack/tests/unit/network/v2/test_local_ip.py @@ -47,6 +47,11 @@ def test_basic(self): self.assertDictEqual({"name": "name", "description": "description", + "project_id": "project_id", + "network_id": "network_id", + "local_port_id": "local_port_id", + "local_ip_address": "local_ip_address", + "ip_mode": "ip_mode", "sort_key": "sort_key", "sort_dir": "sort_dir", "limit": "limit", diff --git a/openstack/tests/unit/network/v2/test_local_ip_association.py b/openstack/tests/unit/network/v2/test_local_ip_association.py index 5e55f959e..602b999da 100644 --- a/openstack/tests/unit/network/v2/test_local_ip_association.py +++ b/openstack/tests/unit/network/v2/test_local_ip_association.py @@ -40,9 +40,9 @@ def test_basic(self): self.assertTrue(sot.allow_list) self.assertDictEqual( - {'local_ip_address': 'local_ip_address', - 'fixed_port_id': 'fixed_port_id', + {'fixed_port_id': 'fixed_port_id', 'fixed_ip': 'fixed_ip', + 'host': 'host', 'limit': 'limit', 'marker': 'marker'}, sot._query_mapping._mapping) From 05a83963f730c6fd4d20eaf5e61609f26624c085 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 21 Dec 2021 17:22:14 +0000 Subject: [PATCH 3007/3836] doc: Remove references to 'examples' test env This was removed a few years ago. Change-Id: I8011d652c1f3527b6c5e75b3c3cb1051cfe9bbbe Signed-off-by: Stephen Finucane --- doc/source/contributor/index.rst | 9 ++++----- doc/source/contributor/testing.rst | 25 ------------------------- tox.ini | 17 +++++++---------- 3 files changed, 11 insertions(+), 40 deletions(-) diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst index b785804be..7d9be8eed 100644 --- a/doc/source/contributor/index.rst +++ b/doc/source/contributor/index.rst @@ -75,11 +75,10 @@ documented in our :doc:`setup ` section. Testing ------- -The project contains three test packages, one for unit tests, one for -functional tests and one for examples tests. The ``openstack.tests.unit`` -package tests the SDK's features in isolation. The -``openstack.tests.functional`` and ``openstack.tests.examples`` packages test -the SDK's features and examples against an OpenStack cloud. +The project contains two test packages, one for unit tests and one for +functional tests. The ``openstack.tests.unit`` package tests the SDK's features +in isolation. The ``openstack.tests.functional`` package tests the SDK's +features and examples against an OpenStack cloud. .. toctree:: diff --git a/doc/source/contributor/testing.rst b/doc/source/contributor/testing.rst index 49ec9c057..991b604ad 100644 --- a/doc/source/contributor/testing.rst +++ b/doc/source/contributor/testing.rst @@ -85,28 +85,3 @@ the continuous integration system.:: ... functional3: commands succeeded congratulations :) - -Examples Tests --------------- - -Similar to the functional tests, the examples tests assume that you have a -public or private OpenStack cloud that you can run the tests against. In -practice, this means that the tests should initially be run against a stable -branch of `DevStack `_. -And like the functional tests, the examples tests connect to an OpenStack cloud -using `os-client-config `_. -See the functional tests instructions for information on setting up DevStack -and os-client-config. - -Run -*** - -In order to run the entire examples test suite, simply run the -``tox -e examples`` command inside of your source checkout. This will -attempt to run every test command under ``/openstack/tests/examples/`` -in the source tree.:: - - (sdk3)$ tox -e examples - ... - examples: commands succeeded - congratulations :) diff --git a/tox.ini b/tox.ini index 3d9726720..e6f59015b 100644 --- a/tox.ini +++ b/tox.ini @@ -21,12 +21,9 @@ deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt -commands = stestr run {posargs} - stestr slowest - -[testenv:examples] -commands = stestr --test-path ./openstack/tests/examples run {posargs} - stestr slowest +commands = + stestr run {posargs} + stestr slowest [testenv:functional] # Some jobs (especially heat) takes longer, therefore increase default timeout @@ -37,8 +34,9 @@ setenv = OPENSTACKSDK_FUNC_TEST_TIMEOUT_LOAD_BALANCER=600 OPENSTACKSDK_EXAMPLE_CONFIG_KEY=functional OPENSTACKSDK_FUNC_TEST_TIMEOUT_PROJECT_CLEANUP=1200 -commands = stestr --test-path ./openstack/tests/functional/{env:OPENSTACKSDK_TESTS_SUBDIR:} run --serial {posargs} - stestr slowest +commands = + stestr --test-path ./openstack/tests/functional/{env:OPENSTACKSDK_TESTS_SUBDIR:} run --serial {posargs} + stestr slowest [testenv:pep8] deps = @@ -120,7 +118,7 @@ application-import-names = openstack ignore = H238,H4,W503 import-order-style = pep8 show-source = True -exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,openstack/_services_mixin.py +exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build,openstack/_services_mixin.py [flake8:local-plugins] extension = @@ -129,4 +127,3 @@ paths = ./openstack [doc8] extensions = .rst, .yaml - From 11d89450c12f47ff7d55f6eed18a5e7f91d7cf0a Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 22 Dec 2021 13:22:02 +0000 Subject: [PATCH 3008/3836] doc: Update testing documentation The information on how to run functional tests was badly out of date. Correct it. Change-Id: I7e77e2a96ab15f7dede01344cb03b0e61c8589bd Signed-off-by: Stephen Finucane --- create_yaml.sh | 31 ---------- doc/source/contributor/clouds.yaml | 44 ++++++++++---- doc/source/contributor/testing.rst | 96 ++++++++++++++++++------------ tox.ini | 2 +- 4 files changed, 90 insertions(+), 83 deletions(-) delete mode 100755 create_yaml.sh diff --git a/create_yaml.sh b/create_yaml.sh deleted file mode 100755 index 4443986ee..000000000 --- a/create_yaml.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash -# -# NOTE(thowe): There are some issues with OCC envvars that force us to do -# this for now. -# -mkdir -p ~/.config/openstack/ -FILE=~/.config/openstack/clouds.yaml -export OS_IDENTITY_API_VERSION=3 # force v3 identity -echo 'clouds:' >$FILE -echo ' test_cloud:' >>$FILE -env | grep OS_ | tr '=' ' ' | while read k v -do - k=$(echo $k | sed -e 's/OS_//') - k=$(echo $k | tr '[A-Z]' '[a-z]') - case "$k" in - region_name|*_api_version) - echo " $k: $v" >>$FILE - esac -done -echo " auth:" >>$FILE -env | grep OS_ | tr '=' ' ' | while read k v -do - k=$(echo $k | sed -e 's/OS_//') - k=$(echo $k | tr '[A-Z]' '[a-z]') - case "$k" in - region_name|*_api_version) - ;; - *) - echo " $k: $v" >>$FILE - esac -done diff --git a/doc/source/contributor/clouds.yaml b/doc/source/contributor/clouds.yaml index b92ba85b7..2c141c5e5 100644 --- a/doc/source/contributor/clouds.yaml +++ b/doc/source/contributor/clouds.yaml @@ -1,19 +1,37 @@ clouds: - test_cloud: - region_name: RegionOne + devstack: auth: - auth_url: http://xxx.xxx.xxx.xxx:5000/v2.0/ - username: demo - password: secrete + auth_url: http://xxx.xxx.xxx.xxx/identity + password: password + project_domain_id: default project_name: demo - rackspace: - cloud: rackspace + user_domain_id: default + username: demo + identity_api_version: '3' + region_name: RegionOne + volume_api_version: '3' + devstack-admin: auth: - username: joe - password: joes-password - project_name: 123123 - region_name: IAD + auth_url: http://xxx.xxx.xxx.xxx/identity + password: password + project_domain_id: default + project_name: admin + user_domain_id: default + username: admin + identity_api_version: '3' + region_name: RegionOne + volume_api_version: '3' + devstack-alt: + auth: + auth_url: http://xxx.xxx.xxx.xxx/identity + password: password + project_domain_id: default + project_name: alt_demo + user_domain_id: default + username: alt_demo + identity_api_version: '3' + region_name: RegionOne + volume_api_version: '3' example: - image_name: fedora-20.x86_64 + image_name: cirros-0.5.2-x86_64-disk flavor_name: m1.small - network_name: private diff --git a/doc/source/contributor/testing.rst b/doc/source/contributor/testing.rst index 991b604ad..c3261df16 100644 --- a/doc/source/contributor/testing.rst +++ b/doc/source/contributor/testing.rst @@ -3,25 +3,27 @@ Testing The tests are run with `tox `_ and configured in ``tox.ini``. The test results are tracked by -`testr `_ and configured -in ``.testr.conf``. +`stestr `_ and configured +in ``.stestr.conf`` and via command line options passed to the ``stestr`` +executable when it's called by ``tox``. + Unit Tests ---------- -Run -*** +Running tests +~~~~~~~~~~~~~ In order to run the entire unit test suite, simply run the ``tox`` command inside of your source checkout. This will attempt to run every test command -listed inside of ``tox.ini``, which includes Python 3.8, and a PEP 8 check. +listed inside of ``tox.ini``, which includes Python 3.x, and a PEP 8 check. You should run the full test suite on all versions before submitting changes for review in order to avoid unexpected failures in the continuous integration system.:: - (sdk3)$ tox + $ tox ... - py38: commands succeeded + py3: commands succeeded pep8: commands succeeded congratulations :) @@ -29,8 +31,13 @@ During development, it may be more convenient to run a subset of the tests to keep test time to a minimum. You can choose to run the tests only on one version. A step further is to run only the tests you are working on.:: - (sdk3)$ tox -e py38 # Run run the tests on Python 3.8 - (sdk3)$ tox -e py38 TestContainer # Run only the TestContainer tests on 3.8 + # Run run the tests on Python 3.9 + $ tox -e py39 + # Run only the compute unit tests on Python 3.9 + $ tox -e py39 openstack.tests.unit.compute + # Run only the tests in a specific file on Python 3.9 + $ tox -e py39 -- -n openstack/tests/unit/compute/test_version.py + Functional Tests ---------------- @@ -41,17 +48,43 @@ public clouds but first and foremost they must be run against OpenStack. In practice, this means that the tests should initially be run against a stable branch of `DevStack `_. -os-client-config -**************** +Configuration +~~~~~~~~~~~~~ + +To connect the functional tests to an OpenStack cloud we require a +``clouds.yaml`` file, as discussed in :doc:`/user/config/configuration`. +You can place this ``clouds.yaml`` file in the root of your source checkout or +in one of the other standard locations, ``$HOME/.config/openstack`` or +``/etc/openstack``. + +There must be at least three clouds configured, or rather three accounts +configured for the one cloud. These accounts are: + +- An admin account, which defaults to ``devstack-admin`` but is configurable + via the ``OPENSTACKSDK_OPERATOR_CLOUD`` environment variable, +- A user account, which defaults to ``devstack`` but is configurable + via the ``OPENSTACKSDK_DEMO_CLOUD`` environment variable, and +- An alternate user account, which defaults to ``devstack-demo`` but is + configurable via the ``OPENSTACKSDK_DEMO_CLOUD_ALT`` environment variable + +In addition, you must indicate the names of the flavor and image that should be +used for tests. These can be configured via ``functional.flavor_name`` and +``functional.image_name`` settings in the ``clouds.yaml`` file. -To connect the functional tests to an OpenStack cloud we use -`os-client-config `_. -To setup os-client-config create a ``clouds.yaml`` file in the root of your -source checkout. +Finally, you can configure the timeout for tests using the +``OPENSTACKSDK_FUNC_TEST_TIMEOUT`` environment variable (defaults to 300 +seconds). Some test modules take specific timeout values. For example, all +tests in ``openstack.tests.functional.compute`` will check for the +``OPENSTACKSDK_FUNC_TEST_TIMEOUT_COMPUTE`` environment variable before checking +for ``OPENSTACKSDK_FUNC_TEST_TIMEOUT``. + +.. note:: + + Recent versions of DevStack will configure a suitable ``clouds.yaml`` file + for you, which will be placed at ``/etc/openstack/clouds.yaml``. This is an example of a minimal configuration for a ``clouds.yaml`` that -connects the functional tests to a DevStack instance. Note that one cloud -under ``clouds`` must be named ``test_cloud``. +connects the functional tests to a DevStack instance. .. literalinclude:: clouds.yaml :language: yaml @@ -59,29 +92,16 @@ under ``clouds`` must be named ``test_cloud``. Replace ``xxx.xxx.xxx.xxx`` with the IP address or FQDN of your DevStack instance. -You can also create a ``~/.config/openstack/clouds.yaml`` file for your -DevStack cloud environment using the following commands. Replace -``DEVSTACK_SOURCE`` with your DevStack source checkout.:: - - (sdk3)$ source DEVSTACK_SOURCE/accrc/admin/admin - (sdk3)$ ./create_yaml.sh +Running tests +~~~~~~~~~~~~~ -Run -*** +Functional tests are also run against multiple Python versions. In order to run +the entire functional test suite against the default Python 3 version in your +environment, run the ``tox -e functional`` command inside of your source +checkout. This will attempt to run every tests in the +``openstack/tests/functional`` directory. For example:: -Functional tests are run against both Python 2 and 3. In order to run the -entire functional test suite, run the ``tox -e functional`` and -``tox -e functional3`` command inside of your source checkout. This will -attempt to run every test command under ``/openstack/tests/functional/`` -in the source tree. You should run the full functional test suite before -submitting changes for review in order to avoid unexpected failures in -the continuous integration system.:: - - (sdk3)$ tox -e functional + $ tox -e functional ... functional: commands succeeded congratulations :) - (sdk3)$ tox -e functional3 - ... - functional3: commands succeeded - congratulations :) diff --git a/tox.ini b/tox.ini index e6f59015b..69e3869ec 100644 --- a/tox.ini +++ b/tox.ini @@ -25,7 +25,7 @@ commands = stestr run {posargs} stestr slowest -[testenv:functional] +[testenv:functional{,-py36,-py37,-py38,-py39}] # Some jobs (especially heat) takes longer, therefore increase default timeout # This timeout should not be smaller, than the longest individual timeout setenv = From 2ee2df6bad58846a333770e10cdf17ce8807d857 Mon Sep 17 00:00:00 2001 From: "Dr. Jens Harbott" Date: Fri, 28 Jan 2022 12:19:01 +0100 Subject: [PATCH 3009/3836] Skip qos-pps-minimum tests if extension is missing This isn't available for pre-Yoga Neutron deployments, make sure our functional tests don't fail because of that. Signed-off-by: Dr. Jens Harbott Change-Id: I1b3db2ab48cb8ddf399ca2e13b846447c0f8b35b --- .../network/v2/test_qos_minimum_packet_rate_rule.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openstack/tests/functional/network/v2/test_qos_minimum_packet_rate_rule.py b/openstack/tests/functional/network/v2/test_qos_minimum_packet_rate_rule.py index 7fbca21a3..34e2debbd 100644 --- a/openstack/tests/functional/network/v2/test_qos_minimum_packet_rate_rule.py +++ b/openstack/tests/functional/network/v2/test_qos_minimum_packet_rate_rule.py @@ -29,6 +29,11 @@ class TestQoSMinimumPacketRateRule(base.BaseFunctionalTest): def setUp(self): super(TestQoSMinimumPacketRateRule, self).setUp() + + # Skip the tests if qos-pps-minimum extension is not enabled. + if not self.conn.network.find_extension('qos-pps-minimum'): + self.skipTest('Network qos-pps-minimum extension disabled') + self.QOS_POLICY_NAME = self.getUniqueString() qos_policy = self.conn.network.create_qos_policy( description=self.QOS_POLICY_DESCRIPTION, From 3244abd02054f2aced3e18668eb752fa052c23d3 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 22 Dec 2021 14:57:19 +0000 Subject: [PATCH 3010/3836] tests: Centralize configuration of default flavor, image Different tests were doing this in different ways. Centralize it all in the base test class for functional tests. For both flavor and image, the order of precedence is: - Environment variables - clouds.yaml configuration - Guesswork (pick a cirros, Ubuntu or CentOS image for images, or the flavor with the lowest RAM for flavors) Change-Id: I90fda8ef48008c7fa634edc295c0e83e5f29387f Signed-off-by: Stephen Finucane --- doc/source/contributor/testing.rst | 5 +- openstack/tests/functional/base.py | 133 +++++++++++++----- .../tests/functional/cloud/test_clustering.py | 76 +++++----- .../tests/functional/cloud/test_compute.py | 6 - .../functional/cloud/test_floating_ip.py | 8 +- .../tests/functional/cloud/test_image.py | 3 - .../tests/functional/cloud/test_inventory.py | 8 +- openstack/tests/functional/cloud/util.py | 43 ------ .../functional/clustering/test_cluster.py | 4 +- .../functional/compute/v2/test_server.py | 41 +++--- 10 files changed, 163 insertions(+), 164 deletions(-) delete mode 100644 openstack/tests/functional/cloud/util.py diff --git a/doc/source/contributor/testing.rst b/doc/source/contributor/testing.rst index c3261df16..275a418b2 100644 --- a/doc/source/contributor/testing.rst +++ b/doc/source/contributor/testing.rst @@ -68,8 +68,9 @@ configured for the one cloud. These accounts are: configurable via the ``OPENSTACKSDK_DEMO_CLOUD_ALT`` environment variable In addition, you must indicate the names of the flavor and image that should be -used for tests. These can be configured via ``functional.flavor_name`` and -``functional.image_name`` settings in the ``clouds.yaml`` file. +used for tests. These can be configured via ``OPENSTACKSDK_FLAVOR`` and +``OPENSTACKSDK_IMAGE`` environment variables or ``functional.flavor_name`` and +``functional.image_name`` settings in the ``clouds.yaml`` file, respectively. Finally, you can configure the timeout for tests using the ``OPENSTACKSDK_FUNC_TEST_TIMEOUT`` environment variable (defaults to 300 diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index 19dc66d80..1e20f3ee4 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import operator import os from keystoneauth1 import discover @@ -28,9 +29,8 @@ TEST_CLOUD_REGION = openstack.config.get_cloud_region(cloud=TEST_CLOUD_NAME) -def _get_resource_value(resource_key, default): - return TEST_CONFIG.get_extra_config( - 'functional').get(resource_key, default) +def _get_resource_value(resource_key): + return TEST_CONFIG.get_extra_config('functional').get(resource_key) def _disable_keep_alive(conn): @@ -38,10 +38,6 @@ def _disable_keep_alive(conn): sess.keep_alive = False -IMAGE_NAME = _get_resource_value('image_name', 'cirros-0.4.0-x86_64-disk') -FLAVOR_NAME = _get_resource_value('flavor_name', 'm1.small') - - class BaseFunctionalTest(base.TestCase): _wait_for_timeout_key = '' @@ -52,10 +48,12 @@ def setUp(self): _disable_keep_alive(self.conn) self._demo_name = os.environ.get('OPENSTACKSDK_DEMO_CLOUD', 'devstack') - self._demo_name_alt = os.environ.get('OPENSTACKSDK_DEMO_CLOUD_ALT', - 'devstack-alt') + self._demo_name_alt = os.environ.get( + 'OPENSTACKSDK_DEMO_CLOUD_ALT', 'devstack-alt', + ) self._op_name = os.environ.get( - 'OPENSTACKSDK_OPERATOR_CLOUD', 'devstack-admin') + 'OPENSTACKSDK_OPERATOR_CLOUD', 'devstack-admin', + ) self.config = openstack.config.OpenStackConfig() self._set_user_cloud() @@ -64,6 +62,9 @@ def setUp(self): self.identity_version = \ self.operator_cloud.config.get_api_version('identity') + self.flavor = self._pick_flavor() + self.image = self._pick_image() + # Defines default timeout for wait_for methods used # in the functional tests self._wait_for_timeout = int( @@ -71,8 +72,7 @@ def setUp(self): 'OPENSTACKSDK_FUNC_TEST_TIMEOUT', 300))) def _set_user_cloud(self, **kwargs): - user_config = self.config.get_one( - cloud=self._demo_name, **kwargs) + user_config = self.config.get_one(cloud=self._demo_name, **kwargs) self.user_cloud = connection.Connection(config=user_config) _disable_keep_alive(self.user_cloud) @@ -84,37 +84,95 @@ def _set_user_cloud(self, **kwargs): _disable_keep_alive(self.user_cloud_alt) def _set_operator_cloud(self, **kwargs): - operator_config = self.config.get_one( - cloud=self._op_name, **kwargs) + operator_config = self.config.get_one(cloud=self._op_name, **kwargs) self.operator_cloud = connection.Connection(config=operator_config) _disable_keep_alive(self.operator_cloud) - def pick_image(self): + def _pick_flavor(self): + """Pick a sensible flavor to run tests with. + + This returns None if the compute service is not present (e.g. + ironic-only deployments). + """ + if not self.user_cloud.has_service('compute'): + return None + + flavors = self.user_cloud.list_flavors(get_extra=False) + self.add_info_on_exception('flavors', flavors) + + flavor_name = os.environ.get('OPENSTACKSDK_FLAVOR') + + if not flavor_name: + flavor_name = _get_resource_value('flavor_name') + + if flavor_name: + for flavor in flavors: + if flavor.name == flavor_name: + return flavor + + raise self.failureException( + "Cloud does not have flavor '%s'", flavor_name, + ) + + # Enable running functional tests against RAX, which requires + # performance flavors be used for boot from volume + + for flavor in sorted(flavors, key=operator.attrgetter('ram')): + if 'performance' in flavor.name: + return flavor + + # Otherwise, pick the smallest flavor with a ephemeral disk configured + + for flavor in sorted(flavors, key=operator.attrgetter('ram')): + if flavor.disk: + return flavor + + raise self.failureException('No sensible flavor found') + + def _pick_image(self): + """Pick a sensible image to run tests with. + + This returns None if the image service is not present. + """ + if not self.user_cloud.has_service('image'): + return None + images = self.user_cloud.list_images() self.add_info_on_exception('images', images) image_name = os.environ.get('OPENSTACKSDK_IMAGE') + + if not image_name: + image_name = _get_resource_value('image_name') + if image_name: for image in images: if image.name == image_name: return image - self.assertFalse( - "Cloud does not have {image}".format(image=image_name)) + + raise self.failureException( + "Cloud does not have image '%s'", image_name, + ) for image in images: if image.name.startswith('cirros') and image.name.endswith('-uec'): return image + for image in images: - if (image.name.startswith('cirros') - and image.disk_format == 'qcow2'): + if ( + image.name.startswith('cirros') + and image.disk_format == 'qcow2' + ): return image + for image in images: if image.name.lower().startswith('ubuntu'): return image for image in images: if image.name.lower().startswith('centos'): return image - self.assertFalse('no sensible image available') + + raise self.failureException('No sensible image found') def addEmptyCleanup(self, func, *args, **kwargs): def cleanup(): @@ -125,12 +183,12 @@ def cleanup(): def require_service(self, service_type, min_microversion=None, **kwargs): """Method to check whether a service exists - Usage: - class TestMeter(base.BaseFunctionalTest): - ... - def setUp(self): - super(TestMeter, self).setUp() - self.require_service('metering') + Usage:: + + class TestMeter(base.BaseFunctionalTest): + def setUp(self): + super(TestMeter, self).setUp() + self.require_service('metering') :returns: True if the service exists, otherwise False. """ @@ -144,16 +202,19 @@ def setUp(self): data = self.conn.session.get_endpoint_data( service_type=service_type, **kwargs) - if not (data.min_microversion - and data.max_microversion - and discover.version_between( - data.min_microversion, - data.max_microversion, - min_microversion)): - self.skipTest('Service {service_type} does not provide ' - 'microversion {ver}'.format( - service_type=service_type, - ver=min_microversion)) + if not ( + data.min_microversion + and data.max_microversion + and discover.version_between( + data.min_microversion, + data.max_microversion, + min_microversion, + ) + ): + self.skipTest( + f'Service {service_type} does not provide microversion ' + f'{min_microversion}' + ) class KeystoneBaseFunctionalTest(BaseFunctionalTest): diff --git a/openstack/tests/functional/cloud/test_clustering.py b/openstack/tests/functional/cloud/test_clustering.py index 354f2d965..80fd8a7a7 100644 --- a/openstack/tests/functional/cloud/test_clustering.py +++ b/openstack/tests/functional/cloud/test_clustering.py @@ -115,8 +115,8 @@ def test_create_profile(self): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -145,8 +145,8 @@ def test_create_cluster(self): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -189,8 +189,8 @@ def test_get_cluster_by_id(self): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -232,8 +232,8 @@ def test_update_cluster(self): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -318,8 +318,8 @@ def test_attach_policy_to_cluster(self): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -392,8 +392,8 @@ def test_detach_policy_from_cluster(self): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -474,8 +474,8 @@ def test_get_policy_on_cluster_by_id(self): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -567,8 +567,8 @@ def test_list_policies_on_cluster(self): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -656,8 +656,8 @@ def test_create_cluster_receiver(self): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -713,8 +713,8 @@ def test_list_cluster_receivers(self): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -775,8 +775,8 @@ def test_delete_cluster(self): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -863,8 +863,8 @@ def test_list_clusters(self): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -914,8 +914,8 @@ def test_update_policy_on_cluster(self): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -1018,8 +1018,8 @@ def test_list_cluster_profiles(self): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -1056,8 +1056,8 @@ def test_get_cluster_profile_by_id(self): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -1094,8 +1094,8 @@ def test_update_cluster_profile(self): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -1130,8 +1130,8 @@ def test_delete_cluster_profile(self): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -1297,8 +1297,8 @@ def test_get_cluster_receiver_by_id(self): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" @@ -1356,8 +1356,8 @@ def test_update_cluster_receiver(self): profile_name = "test_profile" spec = { "properties": { - "flavor": "m1.tiny", - "image": base.IMAGE_NAME, + "flavor": self.flavor.name, + "image": self.image.name, "networks": [ { "network": "private" diff --git a/openstack/tests/functional/cloud/test_compute.py b/openstack/tests/functional/cloud/test_compute.py index 787f356da..c91fe7e08 100644 --- a/openstack/tests/functional/cloud/test_compute.py +++ b/openstack/tests/functional/cloud/test_compute.py @@ -23,7 +23,6 @@ from openstack.cloud import exc from openstack.tests.functional import base -from openstack.tests.functional.cloud.util import pick_flavor from openstack import utils @@ -34,11 +33,6 @@ def setUp(self): self.TIMEOUT_SCALING_FACTOR = 1.5 super(TestCompute, self).setUp() - self.flavor = pick_flavor( - self.user_cloud.list_flavors(get_extra=False)) - if self.flavor is None: - self.assertFalse('no sensible flavor available') - self.image = self.pick_image() self.server_name = self.getUniqueString() def _cleanup_servers_and_volumes(self, server_name): diff --git a/openstack/tests/functional/cloud/test_floating_ip.py b/openstack/tests/functional/cloud/test_floating_ip.py index 3014977bd..24d72ed4b 100644 --- a/openstack/tests/functional/cloud/test_floating_ip.py +++ b/openstack/tests/functional/cloud/test_floating_ip.py @@ -28,7 +28,6 @@ from openstack.cloud import meta from openstack import proxy from openstack.tests.functional import base -from openstack.tests.functional.cloud.util import pick_flavor from openstack import utils @@ -36,12 +35,7 @@ class TestFloatingIP(base.BaseFunctionalTest): timeout = 60 def setUp(self): - super(TestFloatingIP, self).setUp() - self.flavor = pick_flavor( - self.user_cloud.list_flavors(get_extra=False)) - if self.flavor is None: - self.assertFalse('no sensible flavor available') - self.image = self.pick_image() + super().setUp() # Generate a random name for these tests self.new_item_name = self.getUniqueString() diff --git a/openstack/tests/functional/cloud/test_image.py b/openstack/tests/functional/cloud/test_image.py index 46692866d..b6e43fbb0 100644 --- a/openstack/tests/functional/cloud/test_image.py +++ b/openstack/tests/functional/cloud/test_image.py @@ -25,9 +25,6 @@ class TestImage(base.BaseFunctionalTest): - def setUp(self): - super(TestImage, self).setUp() - self.image = self.pick_image() def test_create_image(self): test_image = tempfile.NamedTemporaryFile(delete=False) diff --git a/openstack/tests/functional/cloud/test_inventory.py b/openstack/tests/functional/cloud/test_inventory.py index 9b9d52702..92f0a3e84 100644 --- a/openstack/tests/functional/cloud/test_inventory.py +++ b/openstack/tests/functional/cloud/test_inventory.py @@ -21,21 +21,15 @@ from openstack.cloud import inventory from openstack.tests.functional import base -from openstack.tests.functional.cloud.util import pick_flavor class TestInventory(base.BaseFunctionalTest): def setUp(self): - super(TestInventory, self).setUp() + super().setUp() # This needs to use an admin account, otherwise a public IP # is not allocated from devstack. self.inventory = inventory.OpenStackInventory(cloud='devstack-admin') self.server_name = self.getUniqueString('inventory') - self.flavor = pick_flavor( - self.user_cloud.list_flavors(get_extra=False)) - if self.flavor is None: - self.assertTrue(False, 'no sensible flavor available') - self.image = self.pick_image() self.addCleanup(self._cleanup_server) server = self.operator_cloud.create_server( name=self.server_name, image=self.image, flavor=self.flavor, diff --git a/openstack/tests/functional/cloud/util.py b/openstack/tests/functional/cloud/util.py deleted file mode 100644 index d16bfd8c1..000000000 --- a/openstack/tests/functional/cloud/util.py +++ /dev/null @@ -1,43 +0,0 @@ -# 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. - -""" -util --------------------------------- - -Util methods for functional tests -""" -import operator -import os - - -def pick_flavor(flavors): - """Given a flavor list pick the smallest one.""" - # Enable running functional tests against rax - which requires - # performance flavors be used for boot from volume - flavor_name = os.environ.get('OPENSTACKSDK_FLAVOR') - if flavor_name: - for flavor in flavors: - if flavor.name == flavor_name: - return flavor - return None - - for flavor in sorted( - flavors, - key=operator.attrgetter('ram')): - if 'performance' in flavor.name: - return flavor - for flavor in sorted( - flavors, - key=operator.attrgetter('ram')): - if flavor.disk: - return flavor diff --git a/openstack/tests/functional/clustering/test_cluster.py b/openstack/tests/functional/clustering/test_cluster.py index 4f03e9f21..9db6e18fa 100644 --- a/openstack/tests/functional/clustering/test_cluster.py +++ b/openstack/tests/functional/clustering/test_cluster.py @@ -40,8 +40,8 @@ def setUp(self): 'version': 1.0, 'properties': { 'name': self.getUniqueString(), - 'flavor': base.FLAVOR_NAME, - 'image': base.IMAGE_NAME, + 'flavor': self.flavor.name, + 'image': self.image.name, 'networks': [{'network': self.network.id}] }}} diff --git a/openstack/tests/functional/compute/v2/test_server.py b/openstack/tests/functional/compute/v2/test_server.py index d19b7605b..8803607a4 100644 --- a/openstack/tests/functional/compute/v2/test_server.py +++ b/openstack/tests/functional/compute/v2/test_server.py @@ -11,7 +11,6 @@ # under the License. from openstack.compute.v2 import server -from openstack.tests.functional import base from openstack.tests.functional.compute import base as ft_base from openstack.tests.functional.network.v2 import test_network @@ -23,21 +22,24 @@ def setUp(self): self._set_operator_cloud(interface='admin') self.NAME = 'needstobeshortandlowercase' self.USERDATA = 'SSdtIGFjdHVhbGx5IGEgZ29hdC4=' - flavor = self.conn.compute.find_flavor(base.FLAVOR_NAME, - ignore_missing=False) - image = self.conn.compute.find_image(base.IMAGE_NAME, - ignore_missing=False) volume = self.conn.create_volume(1) sot = self.conn.compute.create_server( - name=self.NAME, flavor_id=flavor.id, image_id=image.id, - networks='none', user_data=self.USERDATA, - block_device_mapping=[{ - 'uuid': volume.id, - 'source_type': 'volume', - 'boot_index': 0, - 'destination_type': 'volume', - 'delete_on_termination': True, - 'volume_size': 1}]) + name=self.NAME, + flavor_id=self.flavor.id, + image_id=self.image.id, + networks='none', + user_data=self.USERDATA, + block_device_mapping=[ + { + 'uuid': volume.id, + 'source_type': 'volume', + 'boot_index': 0, + 'destination_type': 'volume', + 'delete_on_termination': True, + 'volume_size': 1, + }, + ], + ) self.conn.compute.wait_for_server(sot, wait=self._wait_for_timeout) assert isinstance(sot, server.Server) self.assertEqual(self.NAME, sot.name) @@ -72,10 +74,6 @@ def setUp(self): self.subnet = None self.cidr = '10.99.99.0/16' - flavor = self.conn.compute.find_flavor(base.FLAVOR_NAME, - ignore_missing=False) - image = self.conn.compute.find_image(base.IMAGE_NAME, - ignore_missing=False) self.network, self.subnet = test_network.create_network( self.conn, self.NAME, @@ -83,8 +81,11 @@ def setUp(self): self.assertIsNotNone(self.network) sot = self.conn.compute.create_server( - name=self.NAME, flavor_id=flavor.id, image_id=image.id, - networks=[{"uuid": self.network.id}]) + name=self.NAME, + flavor_id=self.flavor.id, + image_id=self.image.id, + networks=[{"uuid": self.network.id}], + ) self.conn.compute.wait_for_server(sot, wait=self._wait_for_timeout) assert isinstance(sot, server.Server) self.assertEqual(self.NAME, sot.name) From 6650374be169ee977de27ce1967ca4d15484b5e8 Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Mon, 17 Jan 2022 09:45:15 +0100 Subject: [PATCH 3011/3836] Cloud / Identity service - reindentation of the docstrings Change-Id: Ia570055dd1d06281185e868b51d1edceada8728e --- openstack/cloud/_identity.py | 52 ++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index 8244fb798..f31bff91a 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -387,7 +387,7 @@ def search_services(self, name_or_id=None, filters=None): :param name_or_id: Name or id of the desired service. :param filters: a dict containing additional filters to use. e.g. - {'type': 'network'}. + {'type': 'network'}. :returns: a list of ``munch.Munch`` containing the services description @@ -402,7 +402,7 @@ def get_service(self, name_or_id, filters=None): :param name_or_id: Name or id of the desired service. :param filters: a dict containing additional filters to use. e.g. - {'type': 'network'} + {'type': 'network'} :returns: a ``munch.Munch`` containing the services description, i.e. the following attributes:: @@ -528,7 +528,7 @@ def search_endpoints(self, id=None, filters=None): :param id: endpoint id. :param filters: a dict containing additional filters to use. e.g. - {'region': 'region-a.geo-1'} + {'region': 'region-a.geo-1'} :returns: a list of ``munch.Munch`` containing the endpoint description. Each dict contains the following attributes:: @@ -553,7 +553,7 @@ def get_endpoint(self, id, filters=None): :param id: endpoint id. :param filters: a dict containing additional filters to use. e.g. - {'region': 'region-a.geo-1'} + {'region': 'region-a.geo-1'} :returns: a ``munch.Munch`` containing the endpoint description. i.e. a ``munch.Munch`` containing the following attributes:: @@ -673,7 +673,7 @@ def search_domains(self, filters=None, name_or_id=None): :param name_or_id: domain name or id :param dict filters: A dict containing additional filters to use. - Keys to search on are id, name, enabled and description. + Keys to search on are id, name, enabled and description. :returns: a list of ``munch.Munch`` containing the domain description. Each ``munch.Munch`` contains the following attributes:: @@ -698,7 +698,7 @@ def get_domain(self, domain_id=None, name_or_id=None, filters=None): :param domain_id: domain id. :param name_or_id: domain name or id. :param dict filters: A dict containing additional filters to use. - Keys to search on are id, name, enabled and description. + Keys to search on are id, name, enabled and description. :returns: a ``munch.Munch`` containing the domain description, or None if not found. Each ``munch.Munch`` contains the following @@ -876,9 +876,9 @@ def search_roles(self, name_or_id=None, filters=None, **kwargs): :returns: a list of ``munch.Munch`` containing the role description. Each ``munch.Munch`` contains the following attributes:: - - id: - - name: - - description: + - id: + - name: + - description: :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. @@ -897,9 +897,9 @@ def get_role(self, name_or_id, filters=None, **kwargs): :returns: a single ``munch.Munch`` containing the role description. Each ``munch.Munch`` contains the following attributes:: - - id: - - name: - - description: + - id: + - name: + - description: :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. @@ -951,16 +951,16 @@ def list_role_assignments(self, filters=None): * 'effective' (boolean) - Return effective role assignments. * 'include_subtree' (boolean) - Include subtree - 'user' and 'group' are mutually exclusive, as are 'domain' and - 'project'. + 'user' and 'group' are mutually exclusive, as are 'domain' and + 'project'. :returns: a list of :class:`openstack.identity.v3.role_assignment.RoleAssignment` objects. Contains the following attributes:: - - id: - - user|group: - - project|domain: + - id: + - user|group: + - project|domain: :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. @@ -1104,13 +1104,13 @@ def grant_role(self, name_or_id, user=None, group=None, :param bool wait: Wait for role to be granted :param int timeout: Timeout to wait for role to be granted - NOTE: domain is a required argument when the grant is on a project, - user or group specified by name. In that situation, they are all - considered to be in that domain. If different domains are in use - in the same role grant, it is required to specify those by ID. + NOTE: domain is a required argument when the grant is on a project, + user or group specified by name. In that situation, they are all + considered to be in that domain. If different domains are in use in + the same role grant, it is required to specify those by ID. - NOTE: for wait and timeout, sometimes granting roles is not - instantaneous. + NOTE: for wait and timeout, sometimes granting roles is not + instantaneous. :returns: True if the role is assigned, otherwise False @@ -1176,10 +1176,10 @@ def revoke_role(self, name_or_id, user=None, group=None, :param bool wait: Wait for role to be revoked :param int timeout: Timeout to wait for role to be revoked - NOTE: for wait and timeout, sometimes revoking roles is not - instantaneous. + NOTE: for wait and timeout, sometimes revoking roles is not + instantaneous. - NOTE: project is required for keystone v2 + NOTE: project is required for keystone v2 :returns: True if the role is revoke, otherwise False From 827736d3c66718c559da8dd74df3c3152ca6370a Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Mon, 17 Jan 2022 09:46:06 +0100 Subject: [PATCH 3012/3836] Cloud / Network service - reindentation of the docstrings Change-Id: I80084bc1fc42e0d7887677e5e82fa61b4cc7aeb2 --- openstack/cloud/_network.py | 412 +++++++++++++++++------------------- 1 file changed, 198 insertions(+), 214 deletions(-) diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index 0cc469b14..1e43d6221 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -44,7 +44,7 @@ def search_networks(self, name_or_id=None, filters=None): :param name_or_id: Name or ID of the desired network. :param filters: a dict containing additional filters to use. e.g. - {'router:external': True} + {'router:external': True} :returns: a list of ``munch.Munch`` containing the network description. @@ -63,7 +63,7 @@ def search_routers(self, name_or_id=None, filters=None): :param name_or_id: Name or ID of the desired router. :param filters: a dict containing additional filters to use. e.g. - {'admin_state_up': True} + {'admin_state_up': True} :returns: a list of ``munch.Munch`` containing the router description. @@ -82,7 +82,7 @@ def search_subnets(self, name_or_id=None, filters=None): :param name_or_id: Name or ID of the desired subnet. :param filters: a dict containing additional filters to use. e.g. - {'enable_dhcp': True} + {'enable_dhcp': True} :returns: a list of ``munch.Munch`` containing the subnet description. @@ -101,7 +101,7 @@ def search_ports(self, name_or_id=None, filters=None): :param name_or_id: Name or ID of the desired port. :param filters: a dict containing additional filters to use. e.g. - {'device_id': '2711c67a-b4a7-43dd-ace7-6187b791c3f0'} + {'device_id': '2711c67a-b4a7-43dd-ace7-6187b791c3f0'} :returns: a list of ``munch.Munch`` containing the port description. @@ -217,10 +217,10 @@ def get_qos_policy(self, name_or_id, filters=None): of this dictionary may, themselves, be dictionaries. Example:: { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } } OR @@ -228,7 +228,7 @@ def get_qos_policy(self, name_or_id, filters=None): Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A policy ``munch.Munch`` or None if no matching network is - found. + found. """ if not self._has_neutron_extension('qos'): @@ -247,7 +247,7 @@ def search_qos_policies(self, name_or_id=None, filters=None): :param name_or_id: Name or ID of the desired policy. :param filters: a dict containing additional filters to use. e.g. - {'shared': True} + {'shared': True} :returns: a list of ``munch.Munch`` containing the network description. @@ -325,10 +325,10 @@ def get_network(self, name_or_id, filters=None): of this dictionary may, themselves, be dictionaries. Example:: { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } } OR @@ -336,7 +336,7 @@ def get_network(self, name_or_id, filters=None): Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A network ``munch.Munch`` or None if no matching network is - found. + found. """ if not filters: @@ -363,10 +363,10 @@ def get_router(self, name_or_id, filters=None): of this dictionary may, themselves, be dictionaries. Example:: { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } } OR @@ -374,7 +374,7 @@ def get_router(self, name_or_id, filters=None): Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A router ``munch.Munch`` or None if no matching router is - found. + found. """ if not filters: @@ -393,14 +393,14 @@ def get_subnet(self, name_or_id, filters=None): of this dictionary may, themselves, be dictionaries. Example:: { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } } :returns: A subnet ``munch.Munch`` or None if no matching subnet is - found. + found. """ if not filters: @@ -427,10 +427,10 @@ def get_port(self, name_or_id, filters=None): of this dictionary may, themselves, be dictionaries. Example:: { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } } OR @@ -468,7 +468,8 @@ def create_network(self, name, shared=False, admin_state_up=True, :param bool external: Whether this network is externally accessible. :param dict provider: A dict of network provider options. Example:: - { 'network_type': 'vlan', 'segmentation_id': 'vlan1' } + { 'network_type': 'vlan', 'segmentation_id': 'vlan1' } + :param string project_id: Specify the project ID this network will be created on (admin-only). :param types.ListType availability_zone_hints: A list of availability @@ -558,7 +559,8 @@ def update_network(self, name_or_id, **kwargs): :param bool external: Whether this network is externally accessible. :param dict provider: A dict of network provider options. Example:: - { 'network_type': 'vlan', 'segmentation_id': 'vlan1' } + { 'network_type': 'vlan', 'segmentation_id': 'vlan1' } + :param int mtu_size: New maximum transmission unit value to address fragmentation. Minimum value is 68 for IPv4, and 1280 for IPv6. :param bool port_security_enabled: Enable or disable port security. @@ -650,7 +652,7 @@ def get_network_quotas(self, name_or_id, details=False): :param name_or_id: project name or id :param details: if set to True it will return details about usage - of quotas by given project + of quotas by given project :raises: OpenStackCloudException if it's not a valid project :returns: Munch object with the quotas @@ -672,7 +674,7 @@ def delete_network_quotas(self, name_or_id): :param name_or_id: project name or id :raises: OpenStackCloudException if it's not a valid project or the - network client call failed + network client call failed :returns: dict with the quotas """ @@ -698,20 +700,17 @@ def create_firewall_rule(self, **kwargs): :param destination_ip_address: IPv4-, IPv6 address or CIDR. :param destination_port: Port or port range (e.g. 80:90) :param bool enabled: Status of firewall rule. You can disable rules - without disassociating them from firewall - policies. Defaults to True. - :param int ip_version: IP Version. - Valid values: 4, 6 - Defaults to 4. + without disassociating them from firewall policies. Defaults to + True. + :param int ip_version: IP Version. Valid values: 4, 6 Defaults to 4. :param name: Human-readable name. :param project_id: Project id. - :param protocol: IP protocol. - Valid values: icmp, tcp, udp, null - :param bool shared: Visibility to other projects. - Defaults to False. + :param protocol: IP protocol. Valid values: icmp, tcp, udp, null + :param bool shared: Visibility to other projects. Defaults to False. :param source_firewall_group_id: ID of source firewall group. :param source_ip_address: IPv4-, IPv6 address or CIDR. :param source_port: Port or port range (e.g. 80:90) + :raises: BadRequestException if parameters are malformed :return: created firewall rule :rtype: FirewallRule @@ -825,15 +824,14 @@ def create_firewall_policy(self, **kwargs): Create firewall policy. :param bool audited: Status of audition of firewall policy. - Set to False each time the firewall policy or the - associated firewall rules are changed. - Has to be explicitly set to True. + Set to False each time the firewall policy or the associated + firewall rules are changed. Has to be explicitly set to True. :param description: Human-readable description. :param list[str] firewall_rules: List of associated firewall rules. :param name: Human-readable name. :param project_id: Project id. :param bool shared: Visibility to other projects. - Defaults to False. + Defaults to False. :raises: BadRequestException if parameters are malformed :raises: ResourceNotFound if a resource from firewall_list not found :return: created firewall policy @@ -942,7 +940,7 @@ def insert_rule_into_policy(self, name_or_id, rule_name_or_id, :param dict filters: optional filters :raises: DuplicateResource on multiple matches :raises: ResourceNotFound if firewall policy or any of the firewall - rules (inserted, after, before) is not found. + rules (inserted, after, before) is not found. :return: updated firewall policy :rtype: FirewallPolicy """ @@ -1025,20 +1023,18 @@ def create_firewall_group(self, **kwargs): request. :param bool admin_state_up: State of firewall group. - Will block all traffic if set to False. - Defaults to True. + Will block all traffic if set to False. Defaults to True. :param description: Human-readable description. :param egress_firewall_policy: Name or id of egress firewall policy. :param ingress_firewall_policy: Name or id of ingress firewall policy. :param name: Human-readable name. :param list[str] ports: List of associated ports (name or id) :param project_id: Project id. - :param shared: Visibility to other projects. - Defaults to False. + :param shared: Visibility to other projects. Defaults to False. :raises: BadRequestException if parameters are malformed :raises: DuplicateResource on multiple matches :raises: ResourceNotFound if (ingress-, egress-) firewall policy or - a port is not found. + a port is not found. :return: created firewall group :rtype: FirewallGroup """ @@ -1118,7 +1114,7 @@ def update_firewall_group(self, name_or_id, filters=None, **kwargs): :raises: BadRequestException if parameters are malformed :raises: DuplicateResource on multiple matches :raises: ResourceNotFound if firewall group, a firewall policy - (egress, ingress) or port is not found + (egress, ingress) or port is not found :return: updated firewall group :rtype: FirewallGroup """ @@ -1189,16 +1185,12 @@ def create_qos_policy(self, **kwargs): def update_qos_policy(self, name_or_id, **kwargs): """Update an existing QoS policy. - :param string name_or_id: - Name or ID of the QoS policy to update. - :param string policy_name: - The new name of the QoS policy. - :param string description: - The new description of the QoS policy. - :param bool shared: - If True, the QoS policy will be set as shared. - :param bool default: - If True, the QoS policy will be set as default for project. + :param string name_or_id: Name or ID of the QoS policy to update. + :param string policy_name: The new name of the QoS policy. + :param string description: The new description of the QoS policy. + :param bool shared: If True, the QoS policy will be set as shared. + :param bool default: If True, the QoS policy will be set as default for + project. :returns: The updated QoS policy object. :raises: OpenStackCloudException on operation error. @@ -1256,7 +1248,7 @@ def search_qos_bandwidth_limit_rules( rules should be associated. :param string rule_id: ID of searched rule. :param filters: a dict containing additional filters to use. e.g. - {'max_kbps': 1000} + {'max_kbps': 1000} :returns: a list of ``munch.Munch`` containing the bandwidth limit rule descriptions. @@ -1446,7 +1438,7 @@ def search_qos_dscp_marking_rules(self, policy_name_or_id, rule_id=None, rules should be associated. :param string rule_id: ID of searched rule. :param filters: a dict containing additional filters to use. e.g. - {'dscp_mark': 32} + {'dscp_mark': 32} :returns: a list of ``munch.Munch`` containing the dscp marking rule descriptions. @@ -1608,7 +1600,7 @@ def search_qos_minimum_bandwidth_rules(self, policy_name_or_id, rules should be associated. :param string rule_id: ID of searched rule. :param filters: a dict containing additional filters to use. e.g. - {'min_kbps': 1000} + {'min_kbps': 1000} :returns: a list of ``munch.Munch`` containing the bandwidth limit rule descriptions. @@ -1785,8 +1777,8 @@ def add_router_interface(self, router, subnet_id=None, port_id=None): :param string port_id: The ID of the port to use for the interface :returns: A ``munch.Munch`` with the router ID (ID), - subnet ID (subnet_id), port ID (port_id) and tenant ID - (tenant_id). + subnet ID (subnet_id), port ID (port_id) and tenant ID + (tenant_id). :raises: OpenStackCloudException on operation error. """ @@ -1870,11 +1862,12 @@ def create_router(self, name=None, admin_state_up=True, external network. Example:: [ - { - "subnet_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b", - "ip_address": "192.168.10.2" - } + { + "subnet_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b", + "ip_address": "192.168.10.2" + } ] + :param string project_id: Project ID for the router. :param types.ListType availability_zone_hints: A list of availability zone hints. @@ -1922,11 +1915,12 @@ def update_router(self, name_or_id, name=None, admin_state_up=None, external network. Example:: [ - { - "subnet_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b", - "ip_address": "192.168.10.2" - } + { + "subnet_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b", + "ip_address": "192.168.10.2" + } ] + :param list routes: A list of dictionaries with destination and nexthop parameters. To clear all routes pass an empty list ([]). @@ -1934,11 +1928,12 @@ def update_router(self, name_or_id, name=None, admin_state_up=None, Example:: [ - { - "destination": "179.24.1.0/24", - "nexthop": "172.24.3.99" - } + { + "destination": "179.24.1.0/24", + "nexthop": "172.24.3.99" + } ] + :returns: The router object. :raises: OpenStackCloudException on operation error. """ @@ -2002,71 +1997,60 @@ def create_subnet(self, network_name_or_id, cidr=None, ip_version=4, prefixlen=None, use_default_subnetpool=False, **kwargs): """Create a subnet on a specified network. - :param string network_name_or_id: - The unique name or ID of the attached network. If a non-unique - name is supplied, an exception is raised. - :param string cidr: - The CIDR. - :param int ip_version: - The IP version, which is 4 or 6. - :param bool enable_dhcp: - Set to ``True`` if DHCP is enabled and ``False`` if disabled. - Default is ``False``. - :param string subnet_name: - The name of the subnet. - :param string tenant_id: - The ID of the tenant who owns the network. Only administrative users - can specify a tenant ID other than their own. - :param allocation_pools: - A list of dictionaries of the start and end addresses for the - allocation pools. For example:: - - [ - { - "start": "192.168.199.2", - "end": "192.168.199.254" - } - ] - - :param string gateway_ip: - The gateway IP address. When you specify both allocation_pools and - gateway_ip, you must ensure that the gateway IP does not overlap - with the specified allocation pools. - :param bool disable_gateway_ip: - Set to ``True`` if gateway IP address is disabled and ``False`` if - enabled. It is not allowed with gateway_ip. - Default is ``False``. - :param dns_nameservers: - A list of DNS name servers for the subnet. For example:: - - [ "8.8.8.7", "8.8.8.8" ] - - :param host_routes: - A list of host route dictionaries for the subnet. For example:: - - [ - { - "destination": "0.0.0.0/0", - "nexthop": "123.456.78.9" - }, - { - "destination": "192.168.0.0/24", - "nexthop": "192.168.0.1" - } - ] - - :param string ipv6_ra_mode: - IPv6 Router Advertisement mode. Valid values are: 'dhcpv6-stateful', - 'dhcpv6-stateless', or 'slaac'. - :param string ipv6_address_mode: - IPv6 address mode. Valid values are: 'dhcpv6-stateful', - 'dhcpv6-stateless', or 'slaac'. - :param string prefixlen: - The prefix length to use for subnet allocation from a subnet pool. - :param bool use_default_subnetpool: - Use the default subnetpool for ``ip_version`` to obtain a CIDR. It - is required to pass ``None`` to the ``cidr`` argument when enabling - this option. + :param string network_name_or_id: The unique name or ID of the attached + network. If a non-unique name is supplied, an exception is raised. + :param string cidr: The CIDR. + :param int ip_version: The IP version, which is 4 or 6. + :param bool enable_dhcp: Set to ``True`` if DHCP is enabled and + ``False`` if disabled. Default is ``False``. + :param string subnet_name: The name of the subnet. + :param string tenant_id: The ID of the tenant who owns the network. + Only administrative users can specify a tenant ID other than their + own. + :param allocation_pools: A list of dictionaries of the start and end + addresses for the allocation pools. For example:: + + [ + { + "start": "192.168.199.2", + "end": "192.168.199.254" + } + ] + + :param string gateway_ip: The gateway IP address. When you specify both + allocation_pools and gateway_ip, you must ensure that the gateway + IP does not overlap with the specified allocation pools. + :param bool disable_gateway_ip: Set to ``True`` if gateway IP address + is disabled and ``False`` if enabled. It is not allowed with + gateway_ip. Default is ``False``. + :param dns_nameservers: A list of DNS name servers for the subnet. For + example:: + + [ "8.8.8.7", "8.8.8.8" ] + + :param host_routes: A list of host route dictionaries for the subnet. + For example:: + + [ + { + "destination": "0.0.0.0/0", + "nexthop": "123.456.78.9" + }, + { + "destination": "192.168.0.0/24", + "nexthop": "192.168.0.1" + } + ] + + :param string ipv6_ra_mode: IPv6 Router Advertisement mode. Valid + values are: 'dhcpv6-stateful', 'dhcpv6-stateless', or 'slaac'. + :param string ipv6_address_mode: IPv6 address mode. Valid values are: + 'dhcpv6-stateful', 'dhcpv6-stateless', or 'slaac'. + :param string prefixlen: The prefix length to use for subnet allocation + from a subnet pool. + :param bool use_default_subnetpool: Use the default subnetpool for + ``ip_version`` to obtain a CIDR. It is required to pass ``None`` to + the ``cidr`` argument when enabling this option. :param kwargs: Key value pairs to be passed to the Neutron API. :returns: The new subnet object. @@ -2168,49 +2152,44 @@ def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, host_routes=None): """Update an existing subnet. - :param string name_or_id: - Name or ID of the subnet to update. - :param string subnet_name: - The new name of the subnet. - :param bool enable_dhcp: - Set to ``True`` if DHCP is enabled and ``False`` if disabled. - :param string gateway_ip: - The gateway IP address. When you specify both allocation_pools and - gateway_ip, you must ensure that the gateway IP does not overlap - with the specified allocation pools. - :param bool disable_gateway_ip: - Set to ``True`` if gateway IP address is disabled and ``False`` if - enabled. It is not allowed with gateway_ip. - Default is ``False``. - :param allocation_pools: - A list of dictionaries of the start and end addresses for the - allocation pools. For example:: - - [ - { - "start": "192.168.199.2", - "end": "192.168.199.254" - } - ] - - :param dns_nameservers: - A list of DNS name servers for the subnet. For example:: - - [ "8.8.8.7", "8.8.8.8" ] - - :param host_routes: - A list of host route dictionaries for the subnet. For example:: - - [ - { - "destination": "0.0.0.0/0", - "nexthop": "123.456.78.9" - }, - { - "destination": "192.168.0.0/24", - "nexthop": "192.168.0.1" - } - ] + :param string name_or_id: Name or ID of the subnet to update. + :param string subnet_name: The new name of the subnet. + :param bool enable_dhcp: Set to ``True`` if DHCP is enabled and + ``False`` if disabled. + :param string gateway_ip: The gateway IP address. When you specify both + allocation_pools and gateway_ip, you must ensure that the gateway + IP does not overlap with the specified allocation pools. + :param bool disable_gateway_ip: Set to ``True`` if gateway IP address + is disabled and ``False`` if enabled. It is not allowed with + gateway_ip. Default is ``False``. + :param allocation_pools: A list of dictionaries of the start and end + addresses for the allocation pools. For example:: + + [ + { + "start": "192.168.199.2", + "end": "192.168.199.254" + } + ] + + :param dns_nameservers: A list of DNS name servers for the subnet. For + example:: + + [ "8.8.8.7", "8.8.8.8" ] + + :param host_routes: A list of host route dictionaries for the subnet. + For example:: + + [ + { + "destination": "0.0.0.0/0", + "nexthop": "123.456.78.9" + }, + { + "destination": "192.168.0.0/24", + "nexthop": "192.168.0.1" + } + ] :returns: The updated subnet object. :raises: OpenStackCloudException on operation error. @@ -2261,15 +2240,15 @@ def create_port(self, network_id, **kwargs): which is up (true, default) or down (false). (Optional) :param mac_address: The MAC address. (Optional) :param fixed_ips: List of ip_addresses and subnet_ids. See subnet_id - and ip_address. (Optional) - For example:: + and ip_address. (Optional) For example:: [ - { - "ip_address": "10.29.29.13", - "subnet_id": "a78484c4-c380-4b47-85aa-21c51a2d8cbd" - }, ... + { + "ip_address": "10.29.29.13", + "subnet_id": "a78484c4-c380-4b47-85aa-21c51a2d8cbd" + }, ... ] + :param subnet_id: If you specify only a subnet ID, OpenStack Networking allocates an available IP from that subnet to the port. (Optional) If you specify both a subnet ID and an IP address, OpenStack @@ -2282,20 +2261,22 @@ def create_port(self, network_id, **kwargs): For example:: [ - { - "ip_address": "23.23.23.1", - "mac_address": "fa:16:3e:c4:cd:3f" - }, ... + { + "ip_address": "23.23.23.1", + "mac_address": "fa:16:3e:c4:cd:3f" + }, ... ] + :param extra_dhcp_opts: Extra DHCP options. (Optional). For example:: [ - { - "opt_name": "opt name1", - "opt_value": "value1" - }, ... + { + "opt_name": "opt name1", + "opt_value": "value1" + }, ... ] + :param device_owner: The ID of the entity that uses this port. For example, a DHCP agent. (Optional) :param device_id: The ID of the device that uses this port. @@ -2337,30 +2318,33 @@ def update_port(self, name_or_id, **kwargs): For example:: [ - { - "ip_address": "10.29.29.13", - "subnet_id": "a78484c4-c380-4b47-85aa-21c51a2d8cbd" - }, ... + { + "ip_address": "10.29.29.13", + "subnet_id": "a78484c4-c380-4b47-85aa-21c51a2d8cbd" + }, ... ] + :param security_groups: List of security group UUIDs. (Optional) :param allowed_address_pairs: Allowed address pairs list (Optional) For example:: [ - { - "ip_address": "23.23.23.1", - "mac_address": "fa:16:3e:c4:cd:3f" - }, ... + { + "ip_address": "23.23.23.1", + "mac_address": "fa:16:3e:c4:cd:3f" + }, ... ] + :param extra_dhcp_opts: Extra DHCP options. (Optional). For example:: [ - { - "opt_name": "opt name1", - "opt_value": "value1" - }, ... + { + "opt_name": "opt name1", + "opt_value": "value1" + }, ... ] + :param device_owner: The ID of the entity that uses this port. For example, a DHCP agent. (Optional) :param device_id: The ID of the resource this port is attached to. From fe3d36f0657bec9f40859cb356d064aec9156f2c Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Mon, 17 Jan 2022 09:48:29 +0100 Subject: [PATCH 3013/3836] Cloud / Utils service - reindentation of the docstrings Change-Id: I8d12a0bc7fd4e7c112955b1173f3fb850514591e --- openstack/cloud/_utils.py | 77 ++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 42 deletions(-) diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index c86d6b512..dc2be40a8 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -37,8 +37,7 @@ def _make_unicode(input): """Turn an input into unicode unconditionally - :param input: - A unicode, string or other object + :param input: A unicode, string or other object """ try: if isinstance(input, unicode): @@ -66,24 +65,23 @@ def _dictify_resource(resource): def _filter_list(data, name_or_id, filters): """Filter a list by name/ID and arbitrary meta data. - :param list data: - The list of dictionary data to filter. It is expected that - each dictionary contains an 'id' and 'name' - key if a value for name_or_id is given. - :param string name_or_id: - The name or ID of the entity being filtered. Can be a glob pattern, - such as 'nb01*'. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: + :param list data: The list of dictionary data to filter. It is expected + that each dictionary contains an 'id' and 'name' key if a value for + name_or_id is given. + :param string name_or_id: The name or ID of the entity being filtered. Can + be a glob pattern, such as 'nb01*'. + :param filters: A dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. Example:: { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } } + OR + A string containing a jmespath expression for further filtering. """ # The logger is openstack.cloud.fmmatch to allow a user/operator to @@ -158,20 +156,16 @@ def _dict_filter(f, d): def _get_entity(cloud, resource, name_or_id, filters, **kwargs): """Return a single entity from the list returned by a given method. - :param object cloud: - The controller class (Example: the main OpenStackCloud object) . - :param string or callable resource: - The string that identifies the resource to use to lookup the - get_<>_by_id or search_s methods(Example: network) - or a callable to invoke. - :param string name_or_id: - The name or ID of the entity being filtered or an object or dict. - If this is an object/dict with an 'id' attr/key, we return it and - bypass resource lookup. - :param filters: - A dictionary of meta data to use for further filtering. - OR - A string containing a jmespath expression for further filtering. + :param object cloud: The controller class (Example: the main OpenStackCloud + object). + :param string or callable resource: The string that identifies the resource + to use to lookup the get_<>_by_id or search_s methods + (Example: network) or a callable to invoke. + :param string name_or_id: The name or ID of the entity being filtered or an + object or dict. If this is an object/dict with an 'id' attr/key, we + return it and bypass resource lookup. + :param filters: A dictionary of meta data to use for further filtering. + OR A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" """ @@ -414,13 +408,13 @@ def shade_exceptions(error_message=None): :param string error_message: String to use for the exception message content on non-OpenStackCloudExceptions. - Useful for avoiding wrapping shade OpenStackCloudException exceptions - within themselves. Code called from within the context may throw such - exceptions without having to catch and reraise them. + Useful for avoiding wrapping shade OpenStackCloudException exceptions + within themselves. Code called from within the context may throw such + exceptions without having to catch and reraise them. - Non-OpenStackCloudException exceptions thrown within the context will - be wrapped and the exception message will be appended to the given error - message. + Non-OpenStackCloudException exceptions thrown within the context will + be wrapped and the exception message will be appended to the given + error message. """ try: yield @@ -506,16 +500,15 @@ def _call_client_and_retry(client, url, retry_on=None, the ability to retry upon known error codes. :param object client: The client method, such as: - ``self.baremetal_client.post`` + ``self.baremetal_client.post`` :param string url: The URL to perform the operation upon. :param integer retry_on: A list of error codes that can be retried on. - The method also supports a single integer to be - defined. + The method also supports a single integer to be + defined. :param integer call_retries: The number of times to retry the call upon - the error code defined by the 'retry_on' - parameter. Default: 3 + the error code defined by the 'retry_on' parameter. Default: 3 :param integer retry_wait: The time in seconds to wait between retry - attempts. Default: 2 + attempts. Default: 2 :returns: The object returned by the client call. """ From 7569cd9a8b90de8452f3b5f485717746f850f76e Mon Sep 17 00:00:00 2001 From: "Dr. Jens Harbott" Date: Fri, 28 Jan 2022 16:15:04 +0100 Subject: [PATCH 3014/3836] Skip tests when needed extensions are disabled Some tests against Neutron depend on specific extensions being available, skip those tests if the extensions are missing. Signed-off-by: Dr. Jens Harbott Change-Id: If0c5262a9199ca5cf4f87d1772221e3a69fead9d --- .../functional/network/v2/test_qos_bandwidth_limit_rule.py | 5 +++++ .../functional/network/v2/test_qos_dscp_marking_rule.py | 5 +++++ .../network/v2/test_qos_minimum_bandwidth_rule.py | 5 +++++ openstack/tests/functional/network/v2/test_qos_policy.py | 5 +++++ openstack/tests/functional/network/v2/test_qos_rule_type.py | 6 ++++++ openstack/tests/functional/network/v2/test_trunk.py | 5 +++++ 6 files changed, 31 insertions(+) diff --git a/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py b/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py index 5302067fa..118ca9ce4 100644 --- a/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py @@ -30,6 +30,11 @@ class TestQoSBandwidthLimitRule(base.BaseFunctionalTest): def setUp(self): super(TestQoSBandwidthLimitRule, self).setUp() + + # Skip the tests if qos-bw-limit-direction extension is not enabled. + if not self.conn.network.find_extension('qos-bw-limit-direction'): + self.skipTest('Network qos-bw-limit-direction extension disabled') + self.QOS_POLICY_NAME = self.getUniqueString() self.RULE_ID = self.getUniqueString() qos_policy = self.conn.network.create_qos_policy( diff --git a/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py b/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py index 61690d31d..63dd7d1fb 100644 --- a/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py +++ b/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py @@ -26,6 +26,11 @@ class TestQoSDSCPMarkingRule(base.BaseFunctionalTest): def setUp(self): super(TestQoSDSCPMarkingRule, self).setUp() + + # Skip the tests if qos extension is not enabled. + if not self.conn.network.find_extension('qos'): + self.skipTest('Network qos extension disabled') + self.QOS_POLICY_NAME = self.getUniqueString() self.RULE_ID = self.getUniqueString() qos_policy = self.conn.network.create_qos_policy( diff --git a/openstack/tests/functional/network/v2/test_qos_minimum_bandwidth_rule.py b/openstack/tests/functional/network/v2/test_qos_minimum_bandwidth_rule.py index 57138c22f..b53f875db 100644 --- a/openstack/tests/functional/network/v2/test_qos_minimum_bandwidth_rule.py +++ b/openstack/tests/functional/network/v2/test_qos_minimum_bandwidth_rule.py @@ -28,6 +28,11 @@ class TestQoSMinimumBandwidthRule(base.BaseFunctionalTest): def setUp(self): super(TestQoSMinimumBandwidthRule, self).setUp() + + # Skip the tests if qos-bw-limit-direction extension is not enabled. + if not self.conn.network.find_extension('qos-bw-limit-direction'): + self.skipTest('Network qos-bw-limit-direction extension disabled') + self.QOS_POLICY_NAME = self.getUniqueString() qos_policy = self.conn.network.create_qos_policy( description=self.QOS_POLICY_DESCRIPTION, diff --git a/openstack/tests/functional/network/v2/test_qos_policy.py b/openstack/tests/functional/network/v2/test_qos_policy.py index df081b8ed..ca9365679 100644 --- a/openstack/tests/functional/network/v2/test_qos_policy.py +++ b/openstack/tests/functional/network/v2/test_qos_policy.py @@ -25,6 +25,11 @@ class TestQoSPolicy(base.BaseFunctionalTest): def setUp(self): super(TestQoSPolicy, self).setUp() + + # Skip the tests if qos extension is not enabled. + if not self.conn.network.find_extension('qos'): + self.skipTest('Network qos extension disabled') + self.QOS_POLICY_NAME = self.getUniqueString() self.QOS_POLICY_NAME_UPDATED = self.getUniqueString() qos = self.conn.network.create_qos_policy( diff --git a/openstack/tests/functional/network/v2/test_qos_rule_type.py b/openstack/tests/functional/network/v2/test_qos_rule_type.py index b599b2063..e71077665 100644 --- a/openstack/tests/functional/network/v2/test_qos_rule_type.py +++ b/openstack/tests/functional/network/v2/test_qos_rule_type.py @@ -18,6 +18,12 @@ class TestQoSRuleType(base.BaseFunctionalTest): QOS_RULE_TYPE = "bandwidth_limit" + def setUp(self): + super(TestQoSRuleType, self).setUp() + # Skip the tests if qos-rule-type-details extension is not enabled. + if not self.conn.network.find_extension('qos-rule-type-details'): + self.skipTest('Network qos-rule-type-details extension disabled') + def test_find(self): sot = self.conn.network.find_qos_rule_type(self.QOS_RULE_TYPE) self.assertEqual(self.QOS_RULE_TYPE, sot.type) diff --git a/openstack/tests/functional/network/v2/test_trunk.py b/openstack/tests/functional/network/v2/test_trunk.py index 0c20a6b20..c3bccc811 100644 --- a/openstack/tests/functional/network/v2/test_trunk.py +++ b/openstack/tests/functional/network/v2/test_trunk.py @@ -23,6 +23,11 @@ class TestTrunk(base.BaseFunctionalTest): def setUp(self): super(TestTrunk, self).setUp() + + # Skip the tests if trunk extension is not enabled. + if not self.conn.network.find_extension('trunk'): + self.skipTest('Network trunk extension disabled') + self.TRUNK_NAME = self.getUniqueString() self.TRUNK_NAME_UPDATED = self.getUniqueString() net = self.conn.network.create_network() From 1b479f0b519382f5237aba5bea941a07872f352f Mon Sep 17 00:00:00 2001 From: "Dr. Jens Harbott" Date: Fri, 28 Jan 2022 12:56:33 +0100 Subject: [PATCH 3015/3836] Revive legacy job The job against newton hasn't been running for five years, switch to run against some oldish, but still supported stable branch. Signed-off-by: Dr. Jens Harbott Change-Id: I89b38cb67deed0a2cdd7ce62c1e3e068de59cec1 --- .zuul.yaml | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index d01995190..24196fc2a 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -101,16 +101,9 @@ parent: openstacksdk-functional-devstack-base description: | Run openstacksdk functional tests against a legacy devstack + nodeset: openstack-single-node-bionic voting: false - vars: - devstack_localrc: - ENABLE_IDENTITY_V2: true - FLAT_INTERFACE: br_flat - PUBLIC_INTERFACE: br_pub - tox_environment: - OPENSTACKSDK_USE_KEYSTONE_V2: 1 - OPENSTACKSDK_HAS_NEUTRON: 0 - override-branch: stable/newton + override-branch: stable/ussuri - job: name: openstacksdk-functional-devstack @@ -471,6 +464,8 @@ voting: false - openstacksdk-functional-devstack-ironic: voting: false + - openstacksdk-functional-devstack-legacy: + voting: false - osc-functional-devstack-tips: voting: false # Ironic jobs, non-voting to avoid tight coupling From 26a5d10dffeebdd534976001c0b8cb102e52ec49 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 28 Jan 2022 17:56:29 +0100 Subject: [PATCH 3016/3836] Restore get_compute_limits backward compatibility get_compute_limits used to return absolute limits. Nodepool is currently relying on this interface. - restore function to return absolute limits - add "aka" access to attributes with old "normalized" names Change-Id: Idc4a563c0f074658452590118e558f389f541358 --- openstack/cloud/_compute.py | 7 ++-- openstack/compute/v2/limits.py | 42 +++++++++++-------- .../tests/functional/cloud/test_limits.py | 14 +++---- 3 files changed, 36 insertions(+), 27 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 7506e9e37..b759c50fa 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -335,13 +335,14 @@ def list_server_groups(self): return list(self.compute.server_groups()) def get_compute_limits(self, name_or_id=None): - """ Get compute limits for a project + """ Get absolute compute limits for a project :param name_or_id: (optional) project name or ID to get limits for if different from the current project :raises: OpenStackCloudException if it's not a valid project - :returns: :class:`~openstack.compute.v2.limits.Limits` object. + :returns: + :class:`~openstack.compute.v2.limits.Limits.AbsoluteLimits` object. """ params = {} project_id = None @@ -352,7 +353,7 @@ def get_compute_limits(self, name_or_id=None): raise exc.OpenStackCloudException("project does not exist") project_id = proj.id params['tenant_id'] = project_id - return self.compute.get_limits(**params) + return self.compute.get_limits(**params).absolute def get_keypair(self, name_or_id, filters=None): """Get a keypair by name or ID. diff --git a/openstack/compute/v2/limits.py b/openstack/compute/v2/limits.py index a14da52df..6c77b320c 100644 --- a/openstack/compute/v2/limits.py +++ b/openstack/compute/v2/limits.py @@ -18,43 +18,51 @@ class AbsoluteLimits(resource.Resource): _max_microversion = '2.57' #: The number of key-value pairs that can be set as image metadata. - image_meta = resource.Body("maxImageMeta") + image_meta = resource.Body("maxImageMeta", aka="max_image_meta") #: The maximum number of personality contents that can be supplied. personality = resource.Body("maxPersonality", deprecated=True) #: The maximum size, in bytes, of a personality. personality_size = resource.Body("maxPersonalitySize", deprecated=True) #: The maximum amount of security group rules allowed. - security_group_rules = resource.Body("maxSecurityGroupRules") + security_group_rules = resource.Body( + "maxSecurityGroupRules", aka="max_security_group_rules") #: The maximum amount of security groups allowed. - security_groups = resource.Body("maxSecurityGroups") + security_groups = resource.Body( + "maxSecurityGroups", aka="max_security_groups") #: The amount of security groups currently in use. - security_groups_used = resource.Body("totalSecurityGroupsUsed") + security_groups_used = resource.Body( + "totalSecurityGroupsUsed", aka="total_security_groups_used") #: The number of key-value pairs that can be set as server metadata. - server_meta = resource.Body("maxServerMeta") + server_meta = resource.Body("maxServerMeta", aka="max_server_meta") #: The maximum amount of cores. - total_cores = resource.Body("maxTotalCores") + total_cores = resource.Body("maxTotalCores", aka="max_total_cores") #: The amount of cores currently in use. - total_cores_used = resource.Body("totalCoresUsed") + total_cores_used = resource.Body("totalCoresUsed", aka="total_cores_used") #: The maximum amount of floating IPs. - floating_ips = resource.Body("maxTotalFloatingIps") + floating_ips = resource.Body( + "maxTotalFloatingIps", aka="max_total_floating_ips") #: The amount of floating IPs currently in use. - floating_ips_used = resource.Body("totalFloatingIpsUsed") + floating_ips_used = resource.Body( + "totalFloatingIpsUsed", aka="total_floating_ips_used") #: The maximum amount of instances. - instances = resource.Body("maxTotalInstances") + instances = resource.Body("maxTotalInstances", aka="max_total_instances") #: The amount of instances currently in use. - instances_used = resource.Body("totalInstancesUsed") + instances_used = resource.Body( + "totalInstancesUsed", aka="total_instances_used") #: The maximum amount of keypairs. - keypairs = resource.Body("maxTotalKeypairs") + keypairs = resource.Body("maxTotalKeypairs", aka="max_total_keypairs") #: The maximum RAM size in megabytes. - total_ram = resource.Body("maxTotalRAMSize") + total_ram = resource.Body("maxTotalRAMSize", aka="max_total_ram_size") #: The RAM size in megabytes currently in use. - total_ram_used = resource.Body("totalRAMUsed") + total_ram_used = resource.Body("totalRAMUsed", aka="total_ram_used") #: The maximum amount of server groups. - server_groups = resource.Body("maxServerGroups") + server_groups = resource.Body("maxServerGroups", aka="max_server_groups") #: The amount of server groups currently in use. - server_groups_used = resource.Body("totalServerGroupsUsed") + server_groups_used = resource.Body( + "totalServerGroupsUsed", aka="total_server_groups_used") #: The maximum number of members in a server group. - server_group_members = resource.Body("maxServerGroupMembers") + server_group_members = resource.Body( + "maxServerGroupMembers", aka="max_server_group_members") class RateLimit(resource.Resource): diff --git a/openstack/tests/functional/cloud/test_limits.py b/openstack/tests/functional/cloud/test_limits.py index c16a0e497..a4ec061c9 100644 --- a/openstack/tests/functional/cloud/test_limits.py +++ b/openstack/tests/functional/cloud/test_limits.py @@ -27,26 +27,26 @@ def test_get_our_compute_limits(self): limits = self.user_cloud.get_compute_limits() self.assertIsNotNone(limits) - self.assertIsInstance(limits, _limits.Limits) - self.assertIsNotNone(limits.absolute.server_meta) - self.assertIsNotNone(limits.absolute.image_meta) + self.assertIsInstance(limits, _limits.AbsoluteLimits) + self.assertIsNotNone(limits.server_meta) + self.assertIsNotNone(limits.image_meta) def test_get_other_compute_limits(self): '''Test quotas functionality''' limits = self.operator_cloud.get_compute_limits('demo') self.assertIsNotNone(limits) - self.assertTrue(hasattr(limits.absolute, 'server_meta')) + self.assertTrue(hasattr(limits, 'server_meta')) # Test normalize limits - self.assertFalse(hasattr(limits.absolute, 'maxImageMeta')) + self.assertFalse(hasattr(limits, 'maxImageMeta')) def test_get_our_volume_limits(self): '''Test quotas functionality''' limits = self.user_cloud.get_volume_limits() self.assertIsNotNone(limits) - self.assertFalse(hasattr(limits.absolute, 'maxTotalVolumes')) + self.assertFalse(hasattr(limits, 'maxTotalVolumes')) def test_get_other_volume_limits(self): '''Test quotas functionality''' limits = self.operator_cloud.get_volume_limits('demo') - self.assertFalse(hasattr(limits.absolute, 'maxTotalVolumes')) + self.assertFalse(hasattr(limits, 'maxTotalVolumes')) From dd892d7b1d471cc90e6f59f66d7adf08b8ca33d1 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 31 Jan 2022 09:12:51 +0100 Subject: [PATCH 3017/3836] Use empty read_acl for swift container in the cloud get_container_access we try to find out which read_acl we currently have. If it is None an exception it thrown - this is wrong. Change-Id: I2719cf41b4a7936895c52f609846995474043a0e --- openstack/cloud/_object_store.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index b03b18249..aecdeb132 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -175,7 +175,7 @@ def get_container_access(self, name): container = self.get_container(name, skip_cache=True) if not container: raise exc.OpenStackCloudException("Container not found: %s" % name) - acl = container.read_ACL + acl = container.read_ACL or '' for key, value in OBJECT_CONTAINER_ACLS.items(): # Convert to string for the comparison because swiftclient # returns byte values as bytes sometimes and apparently == From bcd5916c20fb181962bbd7f69c179e600ee22580 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 28 Jan 2022 18:29:59 +0100 Subject: [PATCH 3018/3836] Improve compute flavor handling nodepool usage uncovered few issues in latest changes. Fix: - ensure server.flavor returns flavor.Flavor instance - set defaults in the flavor according to nova spec. This was previously done in the normalization part. Change-Id: I6161182c95171a942e9242ae08aae096abe00588 --- openstack/compute/v2/flavor.py | 33 +++++++++++++++---- openstack/compute/v2/server.py | 4 +-- .../tests/functional/cloud/test_inventory.py | 4 --- .../tests/unit/compute/v2/test_flavor.py | 27 +++++++++++++++ .../tests/unit/compute/v2/test_server.py | 11 +++++-- 5 files changed, 65 insertions(+), 14 deletions(-) diff --git a/openstack/compute/v2/flavor.py b/openstack/compute/v2/flavor.py index 6fb40fb49..baec64bb4 100644 --- a/openstack/compute/v2/flavor.py +++ b/openstack/compute/v2/flavor.py @@ -43,18 +43,20 @@ class Flavor(resource.Resource): #: The description of the flavor. description = resource.Body('description') #: Size of the disk this flavor offers. *Type: int* - disk = resource.Body('disk', type=int) + disk = resource.Body('disk', type=int, default=0) #: ``True`` if this is a publicly visible flavor. ``False`` if this is #: a private image. *Type: bool* - is_public = resource.Body('os-flavor-access:is_public', type=bool) + is_public = resource.Body( + 'os-flavor-access:is_public', type=bool, default=True) #: The amount of RAM (in MB) this flavor offers. *Type: int* - ram = resource.Body('ram', type=int) + ram = resource.Body('ram', type=int, default=0) #: The number of virtual CPUs this flavor offers. *Type: int* - vcpus = resource.Body('vcpus', type=int) + vcpus = resource.Body('vcpus', type=int, default=0) #: Size of the swap partitions. - swap = resource.Body('swap') + swap = resource.Body('swap', default=0) #: Size of the ephemeral data disk attached to this server. *Type: int* - ephemeral = resource.Body('OS-FLV-EXT-DATA:ephemeral', type=int) + ephemeral = resource.Body( + 'OS-FLV-EXT-DATA:ephemeral', type=int, default=0) #: ``True`` if this flavor is disabled, ``False`` if not. *Type: bool* is_disabled = resource.Body('OS-FLV-DISABLED:disabled', type=bool) #: The bandwidth scaling factor this flavor receives on the network. @@ -64,6 +66,25 @@ class Flavor(resource.Resource): #: A dictionary of the flavor's extra-specs key-and-value pairs. extra_specs = resource.Body('extra_specs', type=dict, default={}) + def __getattribute__(self, name): + """Return an attribute on this instance + + This is mostly a pass-through except for a specialization on + the 'id' name, as this can exist under a different name via the + `alternate_id` argument to resource.Body. + """ + if name == "id": + # ID handling in flavor is very tricky. Sometimes we get ID back, + # sometimes we get only name (but it is same as id), sometimes we + # get original_name back, but it is still id. + # To get this handled try sequentially to access it from various + # places until we find first non-empty value. + for xname in ["id", "name", "original_name"]: + if xname in self._body and self._body[xname]: + return self._body[xname] + else: + return super().__getattribute__(name) + @classmethod def list(cls, session, paginated=True, base_path='/flavors/detail', allow_unknown_params=False, **params): diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 6eee11076..8a16df0c0 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -11,6 +11,7 @@ # under the License. from openstack.common import metadata from openstack.common import tag +from openstack.compute.v2 import flavor from openstack.compute.v2 import volume_attachment from openstack import exceptions from openstack.image.v2 import image @@ -109,8 +110,7 @@ class Server(resource.Resource, metadata.MetadataMixin, tag.TagMixin): #: this server. flavor_id = resource.Body('flavorRef') #: The flavor property as returned from server. - # TODO(gtema): replace with flavor.Flavor addressing flavor.original_name - flavor = resource.Body('flavor', type=dict) + flavor = resource.Body('flavor', type=flavor.Flavor) #: Indicates whether a configuration drive enables metadata injection. #: Not all cloud providers enable this feature. has_config_drive = resource.Body('config_drive') diff --git a/openstack/tests/functional/cloud/test_inventory.py b/openstack/tests/functional/cloud/test_inventory.py index 9b9d52702..92c53ff06 100644 --- a/openstack/tests/functional/cloud/test_inventory.py +++ b/openstack/tests/functional/cloud/test_inventory.py @@ -47,7 +47,6 @@ def _cleanup_server(self): def _test_host_content(self, host): self.assertEqual(host['image']['id'], self.image.id) - self.assertNotIn('id', host['flavor']) self.assertIsInstance(host['volumes'], list) self.assertIsInstance(host['metadata'], dict) self.assertIn('interface_ip', host) @@ -78,9 +77,6 @@ def test_get_host_no_detail(self): self.assertEqual(host['image']['id'], self.image.id) self.assertNotIn('links', host['image']) self.assertNotIn('name', host['name']) - self.assertNotIn('id', host['flavor']) - self.assertNotIn('links', host['flavor']) - self.assertNotIn('name', host['flavor']) self.assertIn('ram', host['flavor']) host_found = False diff --git a/openstack/tests/unit/compute/v2/test_flavor.py b/openstack/tests/unit/compute/v2/test_flavor.py index c83cc3fce..6cc5134c1 100644 --- a/openstack/tests/unit/compute/v2/test_flavor.py +++ b/openstack/tests/unit/compute/v2/test_flavor.py @@ -33,6 +33,11 @@ 'OS-FLV-DISABLED:disabled': False, 'rxtx_factor': 11.0 } +DEFAULTS_EXAMPLE = { + 'links': '2', + 'original_name': IDENTIFIER, + 'description': 'Testing flavor', +} class TestFlavor(base.TestCase): @@ -80,6 +85,28 @@ def test_make_basic(self): sot.is_disabled) self.assertEqual(BASIC_EXAMPLE['rxtx_factor'], sot.rxtx_factor) + def test_make_defaults(self): + sot = flavor.Flavor(**DEFAULTS_EXAMPLE) + self.assertEqual(DEFAULTS_EXAMPLE['original_name'], sot.name) + self.assertEqual(0, sot.disk) + self.assertEqual(True, sot.is_public) + self.assertEqual(0, sot.ram) + self.assertEqual(0, sot.vcpus) + self.assertEqual(0, sot.swap) + self.assertEqual(0, sot.ephemeral) + self.assertEqual(IDENTIFIER, sot.id) + + def test_flavor_id(self): + id = 'fake_id' + sot = flavor.Flavor(id=id) + self.assertEqual(sot.id, id) + sot = flavor.Flavor(name=id) + self.assertEqual(sot.id, id) + self.assertEqual(sot.name, id) + sot = flavor.Flavor(original_name=id) + self.assertEqual(sot.id, id) + self.assertEqual(sot.original_name, id) + def test_add_tenant_access(self): sot = flavor.Flavor(**BASIC_EXAMPLE) resp = mock.Mock() diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 76afc4a10..f677e21d5 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -12,6 +12,7 @@ from unittest import mock +from openstack.compute.v2 import flavor from openstack.compute.v2 import server from openstack.image.v2 import image from openstack.tests.unit import base @@ -64,7 +65,6 @@ 'original_name': 'm1.tiny.specs', 'ram': 512, 'swap': 0, - 'vcpus': 1 }, 'hostId': '2091634baaccdc4c5a1d57069c833e402921df696b7f970791b12ec6', 'host_status': 'UP', @@ -194,7 +194,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['created'], sot.created_at) self.assertEqual(EXAMPLE['config_drive'], sot.has_config_drive) self.assertEqual(EXAMPLE['flavorRef'], sot.flavor_id) - self.assertEqual(EXAMPLE['flavor'], sot.flavor) + self.assertEqual(flavor.Flavor(**EXAMPLE['flavor']), sot.flavor) self.assertEqual(EXAMPLE['hostId'], sot.host_id) self.assertEqual(EXAMPLE['host_status'], sot.host_status) self.assertEqual(EXAMPLE['id'], sot.id) @@ -251,6 +251,13 @@ def test_make_it(self): self.assertEqual(EXAMPLE['trusted_image_certificates'], sot.trusted_image_certificates) + def test_to_dict_flavor(self): + # Ensure to_dict properly resolves flavor and uses defaults for not + # specified flavor proerties. + sot = server.Server(**EXAMPLE) + dct = sot.to_dict() + self.assertEqual(0, dct['flavor']['vcpus']) + def test__prepare_server(self): zone = 1 data = 2 From 55eff088030a95d7a26586aec87e64e9c028d5d5 Mon Sep 17 00:00:00 2001 From: Tom Weininger Date: Thu, 10 Feb 2022 11:38:03 +0100 Subject: [PATCH 3019/3836] Correct documentation about Load Balancer API Many methods claimed to accept a name or ID, while they actually only accept an ID or a resource object. Updated the documentation accordingly. Story: 2009808 Task: 44367 Change-Id: Ida6f9cd3fa1c45394ff8e581a7bd881441d6db69 --- openstack/load_balancer/v2/_proxy.py | 52 +++++++++++++++------------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index be713759d..ca0c010b4 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -47,7 +47,7 @@ def create_load_balancer(self, **attrs): def get_load_balancer(self, *attrs): """Get a load balancer - :param load_balancer: The value can be the name of a load balancer + :param load_balancer: The value can be the ID of a load balancer or :class:`~openstack.load_balancer.v2.load_balancer.LoadBalancer` instance. @@ -56,15 +56,17 @@ def get_load_balancer(self, *attrs): """ return self._get(_lb.LoadBalancer, *attrs) - def get_load_balancer_statistics(self, name_or_id): + def get_load_balancer_statistics(self, load_balancer): """Get the load balancer statistics - :param name_or_id: The name or ID of a load balancer + :param load_balancer: The value can be the ID of a load balancer + or :class:`~openstack.load_balancer.v2.load_balancer.LoadBalancer` + instance. :returns: One :class:`~openstack.load_balancer.v2.load_balancer.LoadBalancerStats` """ - return self._get(_lb.LoadBalancerStats, lb_id=name_or_id, + return self._get(_lb.LoadBalancerStats, lb_id=load_balancer, requires_id=False) def load_balancers(self, **query): @@ -78,7 +80,7 @@ def delete_load_balancer(self, load_balancer, ignore_missing=True, cascade=False): """Delete a load balancer - :param load_balancer: The load_balancer can be either the name or a + :param load_balancer: The load_balancer can be either the ID or a :class:`~openstack.load_balancer.v2.load_balancer.LoadBalancer` instance :param bool ignore_missing: When set to ``False`` @@ -114,7 +116,7 @@ def find_load_balancer(self, name_or_id, ignore_missing=True): def update_load_balancer(self, load_balancer, **attrs): """Update a load balancer - :param load_balancer: The load_balancer can be either the name or a + :param load_balancer: The load_balancer can be either the ID or a :class:`~openstack.load_balancer.v2.load_balancer.LoadBalancer` instance :param dict attrs: The attributes to update on the load balancer @@ -132,14 +134,16 @@ def wait_for_load_balancer(self, name_or_id, status='ACTIVE', return resource.wait_for_status(self, lb, status, failures, interval, wait, attribute='provisioning_status') - def failover_load_balancer(self, name_or_id, **attrs): + def failover_load_balancer(self, load_balancer, **attrs): """Failover a load balancer - :param name_or_id: The name or ID of a load balancer + :param load_balancer: The value can be the ID of a load balancer + or :class:`~openstack.load_balancer.v2.load_balancer.LoadBalancer` + instance. :returns: ``None`` """ - return self._update(_lb.LoadBalancerFailover, lb_id=name_or_id) + return self._update(_lb.LoadBalancerFailover, lb_id=load_balancer) def create_listener(self, **attrs): """Create a new listener from attributes @@ -156,7 +160,7 @@ def create_listener(self, **attrs): def delete_listener(self, listener, ignore_missing=True): """Delete a listener - :param listener: The value can be either the ID of a listner or a + :param listener: The value can be either the ID of a listener or a :class:`~openstack.load_balancer.v2.listener.Listener` instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be @@ -252,7 +256,7 @@ def create_pool(self, **attrs): def get_pool(self, *attrs): """Get a pool - :param pool: Value is + :param pool: Value is either a pool ID or a :class:`~openstack.load_balancer.v2.pool.Pool` instance. @@ -271,7 +275,7 @@ def pools(self, **query): def delete_pool(self, pool, ignore_missing=True): """Delete a pool - :param pool: The pool is a + :param pool: The pool is either a pool ID or a :class:`~openstack.load_balancer.v2.pool.Pool` instance :param bool ignore_missing: When set to ``False`` @@ -832,7 +836,7 @@ def flavor_profiles(self, **query): def delete_flavor_profile(self, flavor_profile, ignore_missing=True): """Delete a flavor profile - :param flavor_profile: The flavor_profile can be either the name or a + :param flavor_profile: The flavor_profile can be either the ID or a :class:`~openstack.load_balancer.v2.flavor_profile.FlavorProfile` instance :param bool ignore_missing: When set to ``False`` @@ -864,7 +868,7 @@ def find_flavor_profile(self, name_or_id, ignore_missing=True): def update_flavor_profile(self, flavor_profile, **attrs): """Update a flavor profile - :param flavor_profile: The flavor_profile can be either the name or a + :param flavor_profile: The flavor_profile can be either the ID or a :class:`~openstack.load_balancer.v2.flavor_profile.FlavorProfile` instance :param dict attrs: The attributes to update on the flavor profile @@ -892,7 +896,7 @@ def create_flavor(self, **attrs): def get_flavor(self, *attrs): """Get a flavor - :param flavor: The value can be the name of a flavor + :param flavor: The value can be the ID of a flavor or :class:`~openstack.load_balancer.v2.flavor.Flavor` instance. :returns: One @@ -910,7 +914,7 @@ def flavors(self, **query): def delete_flavor(self, flavor, ignore_missing=True): """Delete a flavor - :param flavor: The flavorcan be either the name or a + :param flavor: The flavorcan be either the ID or a :class:`~openstack.load_balancer.v2.flavor.Flavor` instance :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when @@ -940,7 +944,7 @@ def find_flavor(self, name_or_id, ignore_missing=True): def update_flavor(self, flavor, **attrs): """Update a flavor - :param flavor: The flavor can be either the name or a + :param flavor: The flavor can be either the ID or a :class:`~openstack.load_balancer.v2.flavor.Flavor` instance :param dict attrs: The attributes to update on the flavor represented by ``flavor``. @@ -1009,7 +1013,7 @@ def create_availability_zone_profile(self, **attrs): comprised of the properties on the AvailabilityZoneProfile class. - :returns: The results of profile creation creation + :returns: The results of profile creation :rtype: :class:`~openstack.load_balancer.v2.availability_zone_profile.AvailabilityZoneProfile` """ @@ -1019,7 +1023,7 @@ def create_availability_zone_profile(self, **attrs): def get_availability_zone_profile(self, *attrs): """Get an availability zone profile - :param availability_zone_profile: The value can be the name of an + :param availability_zone_profile: The value can be the ID of an availability_zone profile or :class:`~openstack.load_balancer.v2.availability_zone_profile.AvailabilityZoneProfile` instance. @@ -1043,7 +1047,7 @@ def delete_availability_zone_profile(self, availability_zone_profile, """Delete an availability zone profile :param availability_zone_profile: The availability_zone_profile can be - either the name or a + either the ID or a :class:`~openstack.load_balancer.v2.availability_zone_profile.AvailabilityZoneProfile` instance :param bool ignore_missing: When set to ``False`` @@ -1077,7 +1081,7 @@ def update_availability_zone_profile(self, availability_zone_profile, """Update an availability zone profile :param availability_zone_profile: The availability_zone_profile can be - either the name or a + either the ID or a :class:`~openstack.load_balancer.v2.availability_zone_profile.AvailabilityZoneProfile` instance :param dict attrs: The attributes to update on the availability_zone @@ -1106,7 +1110,7 @@ def create_availability_zone(self, **attrs): def get_availability_zone(self, *attrs): """Get an availability zone - :param availability_zone: The value can be the name of a + :param availability_zone: The value can be the ID of a availability_zone or :class:`~openstack.load_balancer.v2.availability_zone.AvailabilityZone` instance. @@ -1126,7 +1130,7 @@ def availability_zones(self, **query): def delete_availability_zone(self, availability_zone, ignore_missing=True): """Delete an availability_zone - :param availability_zone: The availability_zone can be either the name + :param availability_zone: The availability_zone can be either the ID or a :class:`~openstack.load_balancer.v2.availability_zone.AvailabilityZone` instance @@ -1159,7 +1163,7 @@ def find_availability_zone(self, name_or_id, ignore_missing=True): def update_availability_zone(self, availability_zone, **attrs): """Update an availability zone - :param availability_zone: The availability_zone can be either the name + :param availability_zone: The availability_zone can be either the ID or a :class:`~openstack.load_balancer.v2.availability_zone.AvailabilityZone` instance From 44c551560242379daa3ae3571aa260f7c6858cb7 Mon Sep 17 00:00:00 2001 From: Ales Musil Date: Mon, 9 Nov 2020 15:10:11 +0100 Subject: [PATCH 3020/3836] network: Fix update of network provider On update the network provider parameters were nested in the "provider". According to the Neutron API [0], update should follow the same semantics as create. [0] https://docs.openstack.org/api-ref/network/v2/?expanded=update-network-detail#update-network Change-Id: Ie2664611873f7528b9a2e6c0fbdbcf12a2c4ba90 --- openstack/cloud/_network.py | 16 ++++----- openstack/tests/unit/cloud/test_network.py | 41 ++++++++++++++++++++++ 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index 0cc469b14..be5da22b8 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -568,17 +568,15 @@ def update_network(self, name_or_id, **kwargs): :returns: The updated network object. :raises: OpenStackCloudException on operation error. """ - if 'provider' in kwargs: - if not isinstance(kwargs['provider'], dict): + provider = kwargs.pop('provider', None) + if provider: + if not isinstance(provider, dict): raise exc.OpenStackCloudException( "Parameter 'provider' must be a dict") - # Only pass what we know - provider = {} - for key in kwargs['provider']: - if key in ('physical_network', 'network_type', - 'segmentation_id'): - provider['provider:' + key] = kwargs['provider'][key] - kwargs['provider'] = provider + for key in ('physical_network', 'network_type', + 'segmentation_id'): + if key in provider: + kwargs['provider:' + key] = provider.pop(key) if 'external' in kwargs: kwargs['router:external'] = kwargs.pop('external') diff --git a/openstack/tests/unit/cloud/test_network.py b/openstack/tests/unit/cloud/test_network.py index aa16247d7..93a3dad24 100644 --- a/openstack/tests/unit/cloud/test_network.py +++ b/openstack/tests/unit/cloud/test_network.py @@ -175,6 +175,47 @@ def test_create_network_provider(self): self._compare_networks(mock_new_network_rep, network) self.assert_calls() + def test_update_network_provider(self): + network_id = "test-net-id" + network_name = "network" + network = {'id': network_id, 'name': network_name} + provider_opts = {'physical_network': 'mynet', + 'network_type': 'vlan', + 'segmentation_id': 'vlan1', + 'should_not_be_passed': 1} + update_network_provider_opts = { + 'provider:physical_network': 'mynet', + 'provider:network_type': 'vlan', + 'provider:segmentation_id': 'vlan1' + } + mock_update_rep = copy.copy(self.mock_new_network_rep) + mock_update_rep.update(update_network_provider_opts) + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'networks', network_name]), + status_code=404), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'], + qs_elements=['name=%s' % network_name]), + json={'networks': [network]}), + dict(method='PUT', + uri=self.get_mock_url( + 'network', 'public', + append=['v2.0', 'networks', network_id]), + json={'network': mock_update_rep}, + validate=dict( + json={'network': update_network_provider_opts})) + ]) + network = self.cloud.update_network( + network_name, + provider=provider_opts + ) + self._compare_networks(mock_update_rep, network) + self.assert_calls() + def test_create_network_with_availability_zone_hints(self): self.register_uris([ dict(method='GET', From 4c1ffee6f55d77efa133ae2e59385a97b0219ada Mon Sep 17 00:00:00 2001 From: Thomas Bucaioni Date: Fri, 18 Feb 2022 10:14:21 +0100 Subject: [PATCH 3021/3836] Indentation of the docstrings Change-Id: I274ef9db600a029229fef2bd8b9c00b5b40917d6 --- openstack/proxy.py | 142 ++++++++++++++++++++++----------------------- 1 file changed, 71 insertions(+), 71 deletions(-) diff --git a/openstack/proxy.py b/openstack/proxy.py index 235b5c132..031abf373 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -359,13 +359,13 @@ def _find(self, resource_type, name_or_id, ignore_missing=True, :param name_or_id: The name or ID of a resource to find. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. :param dict attrs: Attributes to be passed onto the - :meth:`~openstack.resource.Resource.find` - method, such as query parameters. + :meth:`~openstack.resource.Resource.find` + method, such as query parameters. :returns: An instance of ``resource_type`` or None """ @@ -379,27 +379,27 @@ def _delete(self, resource_type, value, ignore_missing=True, **attrs): """Delete a resource :param resource_type: The type of resource to delete. This should - be a :class:`~openstack.resource.Resource` - subclass with a ``from_id`` method. + be a :class:`~openstack.resource.Resource` + subclass with a ``from_id`` method. :param value: The value to delete. Can be either the ID of a - resource or a :class:`~openstack.resource.Resource` - subclass. + resource or a :class:`~openstack.resource.Resource` + subclass. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent resource. :param dict attrs: Attributes to be passed onto the - :meth:`~openstack.resource.Resource.delete` - method, such as the ID of a parent resource. + :meth:`~openstack.resource.Resource.delete` + method, such as the ID of a parent resource. :returns: The result of the ``delete`` :raises: ``ValueError`` if ``value`` is a - :class:`~openstack.resource.Resource` that doesn't match - the ``resource_type``. - :class:`~openstack.exceptions.ResourceNotFound` when - ignore_missing if ``False`` and a nonexistent resource - is attempted to be deleted. + :class:`~openstack.resource.Resource` that doesn't match + the ``resource_type``. + :class:`~openstack.exceptions.ResourceNotFound` when + ignore_missing if ``False`` and a nonexistent resource + is attempted to be deleted. """ res = self._get_resource(resource_type, value, **attrs) @@ -420,17 +420,17 @@ def _update(self, resource_type, value, base_path=None, **attrs): :param resource_type: The type of resource to update. :type resource_type: :class:`~openstack.resource.Resource` :param value: The resource to update. This must either be a - :class:`~openstack.resource.Resource` or an id - that corresponds to a resource. + :class:`~openstack.resource.Resource` or an id + that corresponds to a resource. :param str base_path: Base part of the URI for updating resources, if - different from - :data:`~openstack.resource.Resource.base_path`. + different from + :data:`~openstack.resource.Resource.base_path`. :param dict attrs: Attributes to be passed onto the - :meth:`~openstack.resource.Resource.update` - method to be updated. These should correspond - to either :class:`~openstack.resource.Body` - or :class:`~openstack.resource.Header` - values on this resource. + :meth:`~openstack.resource.Resource.update` + method to be updated. These should correspond + to either :class:`~openstack.resource.Body` + or :class:`~openstack.resource.Header` + values on this resource. :returns: The result of the ``update`` :rtype: :class:`~openstack.resource.Resource` @@ -444,16 +444,16 @@ def _create(self, resource_type, base_path=None, **attrs): :param resource_type: The type of resource to create. :type resource_type: :class:`~openstack.resource.Resource` :param str base_path: Base part of the URI for creating resources, if - different from - :data:`~openstack.resource.Resource.base_path`. + different from + :data:`~openstack.resource.Resource.base_path`. :param path_args: A dict containing arguments for forming the request - URL, if needed. + URL, if needed. :param dict attrs: Attributes to be passed onto the - :meth:`~openstack.resource.Resource.create` - method to be created. These should correspond - to either :class:`~openstack.resource.Body` - or :class:`~openstack.resource.Header` - values on this resource. + :meth:`~openstack.resource.Resource.create` + method to be created. These should correspond + to either :class:`~openstack.resource.Body` + or :class:`~openstack.resource.Header` + values on this resource. :returns: The result of the ``create`` :rtype: :class:`~openstack.resource.Resource` @@ -468,14 +468,14 @@ def _bulk_create(self, resource_type, data, base_path=None): :param resource_type: The type of resource to create. :type resource_type: :class:`~openstack.resource.Resource` :param list data: List of attributes dicts to be passed onto the - :meth:`~openstack.resource.Resource.create` - method to be created. These should correspond - to either :class:`~openstack.resource.Body` - or :class:`~openstack.resource.Header` - values on this resource. + :meth:`~openstack.resource.Resource.create` + method to be created. These should correspond + to either :class:`~openstack.resource.Body` + or :class:`~openstack.resource.Header` + values on this resource. :param str base_path: Base part of the URI for creating resources, if - different from - :data:`~openstack.resource.Resource.base_path`. + different from + :data:`~openstack.resource.Resource.base_path`. :returns: A generator of Resource objects. :rtype: :class:`~openstack.resource.Resource` @@ -490,17 +490,17 @@ def _get(self, resource_type, value=None, requires_id=True, :param resource_type: The type of resource to get. :type resource_type: :class:`~openstack.resource.Resource` :param value: The value to get. Can be either the ID of a - resource or a :class:`~openstack.resource.Resource` - subclass. + resource or a :class:`~openstack.resource.Resource` + subclass. :param str base_path: Base part of the URI for fetching resources, if - different from - :data:`~openstack.resource.Resource.base_path`. + different from + :data:`~openstack.resource.Resource.base_path`. :param dict attrs: Attributes to be passed onto the - :meth:`~openstack.resource.Resource.get` - method. These should correspond - to either :class:`~openstack.resource.Body` - or :class:`~openstack.resource.Header` - values on this resource. + :meth:`~openstack.resource.Resource.get` + method. These should correspond + to either :class:`~openstack.resource.Body` + or :class:`~openstack.resource.Header` + values on this resource. :returns: The result of the ``fetch`` :rtype: :class:`~openstack.resource.Resource` @@ -517,15 +517,15 @@ def _list(self, resource_type, """List a resource :param resource_type: The type of resource to list. This should - be a :class:`~openstack.resource.Resource` - subclass with a ``from_id`` method. + be a :class:`~openstack.resource.Resource` + subclass with a ``from_id`` method. :param bool paginated: When set to ``False``, expect all of the data - to be returned in one response. When set to - ``True``, the resource supports data being - returned across multiple pages. + to be returned in one response. When set to + ``True``, the resource supports data being + returned across multiple pages. :param str base_path: Base part of the URI for listing resources, if - different from - :data:`~openstack.resource.Resource.base_path`. + different from + :data:`~openstack.resource.Resource.base_path`. :param dict attrs: Attributes to be passed onto the :meth:`~openstack.resource.Resource.list` method. These should correspond to either :class:`~openstack.resource.URI` values @@ -533,8 +533,8 @@ def _list(self, resource_type, :returns: A generator of Resource objects. :raises: ``ValueError`` if ``value`` is a - :class:`~openstack.resource.Resource` that doesn't match - the ``resource_type``. + :class:`~openstack.resource.Resource` that doesn't match + the ``resource_type``. """ return resource_type.list( self, paginated=paginated, @@ -547,16 +547,16 @@ def _head(self, resource_type, value=None, base_path=None, **attrs): :param resource_type: The type of resource to retrieve. :type resource_type: :class:`~openstack.resource.Resource` :param value: The value of a specific resource to retreive headers - for. Can be either the ID of a resource, - a :class:`~openstack.resource.Resource` subclass, - or ``None``. + for. Can be either the ID of a resource, + a :class:`~openstack.resource.Resource` subclass, + or ``None``. :param str base_path: Base part of the URI for heading resources, if - different from - :data:`~openstack.resource.Resource.base_path`. + different from + :data:`~openstack.resource.Resource.base_path`. :param dict attrs: Attributes to be passed onto the - :meth:`~openstack.resource.Resource.head` method. - These should correspond to - :class:`~openstack.resource.URI` values. + :meth:`~openstack.resource.Resource.head` method. + These should correspond to + :class:`~openstack.resource.URI` values. :returns: The result of the ``head`` call :rtype: :class:`~openstack.resource.Resource` From bdd87c70423b2c1cabd2820dc28368e1c3f158e1 Mon Sep 17 00:00:00 2001 From: james kirsch Date: Mon, 24 Jan 2022 15:12:29 -0800 Subject: [PATCH 3022/3836] Identity: Add support for system role assignment Allow granting and revoking system role assignment to a user or group. Enable filtering for system role when listing role assignments. https: //docs.openstack.org/api-ref/identity/v3/#system-role-assignments Change-Id: I2870d322fdfd59c91524b4031d8ecf53b60a9a1d --- doc/source/user/proxies/identity_v3.rst | 5 +- openstack/cloud/_identity.py | 79 +++++++++-- openstack/identity/v3/_proxy.py | 124 +++++++++++++++++- .../v3/role_system_group_assignment.py | 28 ++++ .../v3/role_system_user_assignment.py | 28 ++++ openstack/identity/v3/system.py | 75 +++++++++++ .../tests/functional/cloud/test_identity.py | 55 ++++++++ .../tests/unit/cloud/test_role_assignment.py | 6 +- .../tests/unit/identity/v3/test_proxy.py | 72 ++++++++++ .../v3/test_role_system_group_assignment.py | 40 ++++++ .../v3/test_role_system_user_assignment.py | 39 ++++++ ...stem-role-assignment-693dd3e1da33a54d.yaml | 11 ++ 12 files changed, 538 insertions(+), 24 deletions(-) create mode 100644 openstack/identity/v3/role_system_group_assignment.py create mode 100644 openstack/identity/v3/role_system_user_assignment.py create mode 100644 openstack/identity/v3/system.py create mode 100644 openstack/tests/unit/identity/v3/test_role_system_group_assignment.py create mode 100644 openstack/tests/unit/identity/v3/test_role_system_user_assignment.py create mode 100644 releasenotes/notes/add-system-role-assignment-693dd3e1da33a54d.yaml diff --git a/doc/source/user/proxies/identity_v3.rst b/doc/source/user/proxies/identity_v3.rst index 31dccad94..d47e91cb3 100644 --- a/doc/source/user/proxies/identity_v3.rst +++ b/doc/source/user/proxies/identity_v3.rst @@ -87,7 +87,10 @@ Role Assignment Operations unassign_project_role_from_group, validate_group_has_project_role, assign_domain_role_to_user, unassign_domain_role_from_user, validate_user_has_domain_role, assign_domain_role_to_group, - unassign_domain_role_from_group, validate_group_has_domain_role + unassign_domain_role_from_group, validate_group_has_domain_role, + assign_system_role_to_user, unassign_system_role_from_user, + validate_user_has_system_role, assign_system_role_to_group, + unassign_system_role_from_group, validate_group_has_system_role Service Operations ^^^^^^^^^^^^^^^^^^ diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index 8244fb798..f1ab25379 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -945,6 +945,7 @@ def list_role_assignments(self, filters=None): * 'group' (string) - Group ID to be used as query filter. * 'project' (string) - Project ID to be used as query filter. * 'domain' (string) - Domain ID to be used as query filter. + * 'system' (string) - System name to be used as query filter. * 'role' (string) - Role ID to be used as query filter. * 'os_inherit_extension_inherited_to' (string) - Return inherited role assignments for either 'projects' or 'domains' @@ -991,6 +992,10 @@ def list_role_assignments(self, filters=None): if k in filters: filters['scope_%s_id' % k] = filters.pop(k) + if 'system' in filters: + system_scope = filters.pop('system') + filters['scope.system'] = system_scope + return list(self.identity.role_assignments(**filters)) @_utils.valid_kwargs('domain_id') @@ -1055,7 +1060,7 @@ def delete_role(self, name_or_id, **kwargs): raise def _get_grant_revoke_params(self, role, user=None, group=None, - project=None, domain=None): + project=None, domain=None, system=None): data = {} search_args = {} if domain: @@ -1083,9 +1088,9 @@ def _get_grant_revoke_params(self, role, user=None, group=None, if data.get('user') is None and data.get('group') is None: raise exc.OpenStackCloudException( 'Must specify either a user or a group') - if project is None and domain is None: + if project is None and domain is None and system is None: raise exc.OpenStackCloudException( - 'Must specify either a domain or project') + 'Must specify either a domain, project or system') if project: data['project'] = self.identity.find_project( @@ -1093,7 +1098,8 @@ def _get_grant_revoke_params(self, role, user=None, group=None, return data def grant_role(self, name_or_id, user=None, group=None, - project=None, domain=None, wait=False, timeout=60): + project=None, domain=None, system=None, wait=False, + timeout=60): """Grant a role to a user. :param string name_or_id: The name or id of the role. @@ -1101,6 +1107,7 @@ def grant_role(self, name_or_id, user=None, group=None, :param string group: The name or id of the group. (v3) :param string project: The name or id of the project. :param string domain: The id of the domain. (v3) + :param bool system: The name of the system. (v3) :param bool wait: Wait for role to be granted :param int timeout: Timeout to wait for role to be granted @@ -1112,13 +1119,15 @@ def grant_role(self, name_or_id, user=None, group=None, NOTE: for wait and timeout, sometimes granting roles is not instantaneous. + NOTE: precedence is given first to project, then domain, then system + :returns: True if the role is assigned, otherwise False :raise OpenStackCloudException: if the role cannot be granted """ data = self._get_grant_revoke_params( name_or_id, user=user, group=group, - project=project, domain=domain) + project=project, domain=domain, system=system) user = data.get('user') group = data.get('group') @@ -1127,7 +1136,7 @@ def grant_role(self, name_or_id, user=None, group=None, role = data.get('role') if project: - # Proceed with project - precedence over domain + # Proceed with project - precedence over domain and system if user: has_role = self.identity.validate_user_has_project_role( project, user, role) @@ -1144,8 +1153,8 @@ def grant_role(self, name_or_id, user=None, group=None, return False self.identity.assign_project_role_to_group( project, group, role) - else: - # Proceed with domain + elif domain: + # Proceed with domain - precedence over system if user: has_role = self.identity.validate_user_has_domain_role( domain, user, role) @@ -1162,10 +1171,31 @@ def grant_role(self, name_or_id, user=None, group=None, return False self.identity.assign_domain_role_to_group( domain, group, role) + else: + # Proceed with system + # System name must be 'all' due to checks performed in + # _get_grant_revoke_params + if user: + has_role = self.identity.validate_user_has_system_role( + user, role, system) + if has_role: + self.log.debug('Assignment already exists') + return False + self.identity.assign_system_role_to_user( + user, role, system) + else: + has_role = self.identity.validate_group_has_system_role( + group, role, system) + if has_role: + self.log.debug('Assignment already exists') + return False + self.identity.assign_system_role_to_group( + group, role, system) return True def revoke_role(self, name_or_id, user=None, group=None, - project=None, domain=None, wait=False, timeout=60): + project=None, domain=None, system=None, + wait=False, timeout=60): """Revoke a role from a user. :param string name_or_id: The name or id of the role. @@ -1173,6 +1203,7 @@ def revoke_role(self, name_or_id, user=None, group=None, :param string group: The name or id of the group. (v3) :param string project: The name or id of the project. :param string domain: The id of the domain. (v3) + :param bool system: The name of the system. (v3) :param bool wait: Wait for role to be revoked :param int timeout: Timeout to wait for role to be revoked @@ -1181,13 +1212,15 @@ def revoke_role(self, name_or_id, user=None, group=None, NOTE: project is required for keystone v2 + NOTE: precedence is given first to project, then domain, then system + :returns: True if the role is revoke, otherwise False :raise OpenStackCloudException: if the role cannot be removed """ data = self._get_grant_revoke_params( name_or_id, user=user, group=group, - project=project, domain=domain) + project=project, domain=domain, system=system) user = data.get('user') group = data.get('group') @@ -1196,7 +1229,7 @@ def revoke_role(self, name_or_id, user=None, group=None, role = data.get('role') if project: - # Proceed with project - precedence over domain + # Proceed with project - precedence over domain and system if user: has_role = self.identity.validate_user_has_project_role( project, user, role) @@ -1213,8 +1246,8 @@ def revoke_role(self, name_or_id, user=None, group=None, return False self.identity.unassign_project_role_from_group( project, group, role) - else: - # Proceed with domain + elif domain: + # Proceed with domain - precedence over system if user: has_role = self.identity.validate_user_has_domain_role( domain, user, role) @@ -1231,6 +1264,26 @@ def revoke_role(self, name_or_id, user=None, group=None, return False self.identity.unassign_domain_role_from_group( domain, group, role) + else: + # Proceed with system + # System name must be 'all' due to checks performed in + # _get_grant_revoke_params + if user: + has_role = self.identity.validate_user_has_system_role( + user, role, system) + if not has_role: + self.log.debug('Assignment does not exist') + return False + self.identity.unassign_system_role_from_user( + user, role, system) + else: + has_role = self.identity.validate_group_has_system_role( + group, role, system) + if not has_role: + self.log.debug('Assignment does not exist') + return False + self.identity.unassign_system_role_from_group( + group, role, system) return True def _get_identity_params(self, domain_id=None, project=None): diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index a382e2127..141ef6097 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -35,7 +35,12 @@ as _role_project_group_assignment from openstack.identity.v3 import role_project_user_assignment \ as _role_project_user_assignment +from openstack.identity.v3 import role_system_group_assignment \ + as _role_system_group_assignment +from openstack.identity.v3 import role_system_user_assignment \ + as _role_system_user_assignment from openstack.identity.v3 import service as _service +from openstack.identity.v3 import system as _system from openstack.identity.v3 import trust as _trust from openstack.identity.v3 import user as _user from openstack import proxy @@ -949,8 +954,8 @@ def update_role(self, role, **attrs): """ return self._update(_role.Role, role, **attrs) - def role_assignments_filter(self, domain=None, project=None, group=None, - user=None): + def role_assignments_filter(self, domain=None, project=None, system=None, + group=None, user=None): """Retrieve a generator of roles assigned to user/group :param domain: Either the ID of a domain or a @@ -958,6 +963,9 @@ def role_assignments_filter(self, domain=None, project=None, group=None, :param project: Either the ID of a project or a :class:`~openstack.identity.v3.project.Project` instance. + :param system: Either the system name or a + :class:`~openstack.identity.v3.system.System` + instance. :param group: Either the ID of a group or a :class:`~openstack.identity.v3.group.Group` instance. :param user: Either the ID of a user or a @@ -965,13 +973,13 @@ def role_assignments_filter(self, domain=None, project=None, group=None, :return: A generator of role instances. :rtype: :class:`~openstack.identity.v3.role.Role` """ - if domain and project: + if domain and project and system: raise exception.InvalidRequest( - 'Only one of domain or project can be specified') + 'Only one of domain, project, or system can be specified') - if domain is None and project is None: + if domain is None and project is None and system is None: raise exception.InvalidRequest( - 'Either domain or project should be specified') + 'Either domain, project, or system should be specified') if group and user: raise exception.InvalidRequest( @@ -993,7 +1001,7 @@ def role_assignments_filter(self, domain=None, project=None, group=None, return self._list( _role_domain_user_assignment.RoleDomainUserAssignment, domain_id=domain.id, user_id=user.id) - else: + elif project: project = self._get_resource(_project.Project, project) if group: group = self._get_resource(_group.Group, group) @@ -1005,6 +1013,18 @@ def role_assignments_filter(self, domain=None, project=None, group=None, return self._list( _role_project_user_assignment.RoleProjectUserAssignment, project_id=project.id, user_id=user.id) + else: + system = self._get_resource(_project.System, system) + if group: + group = self._get_resource(_group.Group, group) + return self._list( + _role_system_group_assignment.RoleSystemGroupAssignment, + system_id=system.id, group_id=group.id) + else: + user = self._get_resource(_user.User, user) + return self._list( + _role_system_user_assignment.RoleSystemUserAssignment, + system_id=system.id, user_id=user.id) def role_assignments(self, **query): """Retrieve a generator of role assignments @@ -1355,6 +1375,96 @@ def validate_group_has_project_role(self, project, group, role): role = self._get_resource(_role.Role, role) return project.validate_group_has_role(self, group, role) + def assign_system_role_to_user(self, user, role, system): + """Assign a role to user on a system + + :param user: Either the ID of a user or a + :class:`~openstack.identity.v3.user.User` instance. + :param role: Either the ID of a role or a + :class:`~openstack.identity.v3.role.Role` instance. + :param system: The system name + :return: ``None`` + """ + user = self._get_resource(_user.User, user) + role = self._get_resource(_role.Role, role) + system = self._get_resource(_system.System, system) + system.assign_role_to_user(self, user, role) + + def unassign_system_role_from_user(self, user, role, system): + """Unassign a role from user on a system + + :param user: Either the ID of a user or a + :class:`~openstack.identity.v3.user.User` instance. + :param role: Either the ID of a role or a + :class:`~openstack.identity.v3.role.Role` instance. + :param system: The system name + :return: ``None`` + """ + user = self._get_resource(_user.User, user) + role = self._get_resource(_role.Role, role) + system = self._get_resource(_system.System, system) + system.unassign_role_from_user(self, user, role) + + def validate_user_has_system_role(self, user, role, system): + """Validates that a user has a role on a system + + :param user: Either the ID of a user or a + :class:`~openstack.identity.v3.user.User` instance. + :param role: Either the ID of a role or a + :class:`~openstack.identity.v3.role.Role` instance. + :param system: The system name + :returns: True if user has role in system + """ + user = self._get_resource(_user.User, user) + role = self._get_resource(_role.Role, role) + system = self._get_resource(_system.System, system) + return system.validate_user_has_role(self, user, role) + + def assign_system_role_to_group(self, group, role, system): + """Assign a role to group on a system + + :param group: Either the ID of a group or a + :class:`~openstack.identity.v3.group.Group` instance. + :param role: Either the ID of a role or a + :class:`~openstack.identity.v3.role.Role` instance. + :param system: The system name + :return: ``None`` + """ + group = self._get_resource(_group.Group, group) + role = self._get_resource(_role.Role, role) + system = self._get_resource(_system.System, system) + system.assign_role_to_group(self, group, role) + + def unassign_system_role_from_group(self, group, role, system): + """Unassign a role from group on a system + + :param group: Either the ID of a group or a + :class:`~openstack.identity.v3.group.Group` instance. + :param role: Either the ID of a role or a + :class:`~openstack.identity.v3.role.Role` instance. + :param system: The system name + :return: ``None`` + """ + group = self._get_resource(_group.Group, group) + role = self._get_resource(_role.Role, role) + system = self._get_resource(_system.System, system) + system.unassign_role_from_group(self, group, role) + + def validate_group_has_system_role(self, group, role, system): + """Validates that a group has a role on a system + + :param group: Either the ID of a group or a + :class:`~openstack.identity.v3.group.Group` instance. + :param role: Either the ID of a role or a + :class:`~openstack.identity.v3.role.Role` instance. + :param system: The system name + :returns: True if group has role on system + """ + group = self._get_resource(_group.Group, group) + role = self._get_resource(_role.Role, role) + system = self._get_resource(_system.System, system) + return system.validate_group_has_role(self, group, role) + def application_credentials(self, user, **query): """Retrieve a generator of application credentials diff --git a/openstack/identity/v3/role_system_group_assignment.py b/openstack/identity/v3/role_system_group_assignment.py new file mode 100644 index 000000000..a9dd03577 --- /dev/null +++ b/openstack/identity/v3/role_system_group_assignment.py @@ -0,0 +1,28 @@ +# 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 resource + + +class RoleSystemGroupAssignment(resource.Resource): + resource_key = 'role' + resources_key = 'roles' + base_path = '/system/groups/%(group_id)s/roles' + + # capabilities + allow_list = True + + # Properties + #: The ID of the group to list assignment from. *Type: string* + group_id = resource.URI('group_id') + #: The name of the system to list assignment from. *Type: string* + system_id = resource.URI('system_id') diff --git a/openstack/identity/v3/role_system_user_assignment.py b/openstack/identity/v3/role_system_user_assignment.py new file mode 100644 index 000000000..e11781daa --- /dev/null +++ b/openstack/identity/v3/role_system_user_assignment.py @@ -0,0 +1,28 @@ +# 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 resource + + +class RoleSystemUserAssignment(resource.Resource): + resource_key = 'role' + resources_key = 'roles' + base_path = '/system/users/%(user_id)s/roles' + + # capabilities + allow_list = True + + # Properties + #: The name of the system to list assignment from. *Type: string* + system_id = resource.URI('system_id') + #: The ID of the user to list assignment from. *Type: string* + user_id = resource.URI('user_id') diff --git a/openstack/identity/v3/system.py b/openstack/identity/v3/system.py new file mode 100644 index 000000000..9ceea3f76 --- /dev/null +++ b/openstack/identity/v3/system.py @@ -0,0 +1,75 @@ +# 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 resource +from openstack import utils + + +class System(resource.Resource): + resource_key = 'system' + base_path = '/system' + + # capabilities + + def assign_role_to_user(self, session, user, role): + """Assign role to user on system""" + url = utils.urljoin(self.base_path, 'users', user.id, + 'roles', role.id) + resp = session.put(url,) + if resp.status_code == 204: + return True + return False + + def validate_user_has_role(self, session, user, role): + """Validates that a user has a role on a system""" + url = utils.urljoin(self.base_path, 'users', user.id, + 'roles', role.id) + resp = session.head(url,) + if resp.status_code == 204: + return True + return False + + def unassign_role_from_user(self, session, user, role): + """Unassigns a role from a user on a system""" + url = utils.urljoin(self.base_path, 'users', user.id, + 'roles', role.id) + resp = session.delete(url,) + if resp.status_code == 204: + return True + return False + + def assign_role_to_group(self, session, group, role): + """Assign role to group on system""" + url = utils.urljoin(self.base_path, 'groups', group.id, + 'roles', role.id) + resp = session.put(url,) + if resp.status_code == 204: + return True + return False + + def validate_group_has_role(self, session, group, role): + """Validates that a group has a role on a system""" + url = utils.urljoin(self.base_path, 'groups', group.id, + 'roles', role.id) + resp = session.head(url,) + if resp.status_code == 204: + return True + return False + + def unassign_role_from_group(self, session, group, role): + """Unassigns a role from a group on a system""" + url = utils.urljoin(self.base_path, 'groups', group.id, + 'roles', role.id) + resp = session.delete(url,) + if resp.status_code == 204: + return True + return False diff --git a/openstack/tests/functional/cloud/test_identity.py b/openstack/tests/functional/cloud/test_identity.py index f9ecdf663..5c087ba93 100644 --- a/openstack/tests/functional/cloud/test_identity.py +++ b/openstack/tests/functional/cloud/test_identity.py @@ -248,3 +248,58 @@ def test_grant_revoke_role_group_domain(self): }) self.assertIsInstance(assignments, list) self.assertEqual(0, len(assignments)) + + def test_grant_revoke_role_user_system(self): + role_name = self.role_prefix + '_grant_user_system' + role = self.operator_cloud.create_role(role_name) + user_name = self.user_prefix + '_user_system' + user_email = 'nobody@nowhere.com' + user = self._create_user(name=user_name, + email=user_email, + default_project='demo') + self.assertTrue(self.operator_cloud.grant_role( + role_name, user=user['id'], system='all')) + assignments = self.operator_cloud.list_role_assignments({ + 'role': role['id'], + 'user': user['id'], + 'system': 'all' + }) + self.assertIsInstance(assignments, list) + self.assertEqual(1, len(assignments)) + self.assertTrue(self.operator_cloud.revoke_role( + role_name, user=user['id'], system='all')) + assignments = self.operator_cloud.list_role_assignments({ + 'role': role['id'], + 'user': user['id'], + 'system': 'all' + }) + self.assertIsInstance(assignments, list) + self.assertEqual(0, len(assignments)) + + def test_grant_revoke_role_group_system(self): + if self.identity_version in ('2', '2.0'): + self.skipTest("Identity service does not support system or group") + role_name = self.role_prefix + '_grant_group_system' + role = self.operator_cloud.create_role(role_name) + group_name = self.group_prefix + '_group_system' + group = self.operator_cloud.create_group( + name=group_name, + description='test group') + self.assertTrue(self.operator_cloud.grant_role( + role_name, group=group['id'], system='all')) + assignments = self.operator_cloud.list_role_assignments({ + 'role': role['id'], + 'group': group['id'], + 'system': 'all' + }) + self.assertIsInstance(assignments, list) + self.assertEqual(1, len(assignments)) + self.assertTrue(self.operator_cloud.revoke_role( + role_name, group=group['id'], system='all')) + assignments = self.operator_cloud.list_role_assignments({ + 'role': role['id'], + 'group': group['id'], + 'system': 'all' + }) + self.assertIsInstance(assignments, list) + self.assertEqual(0, len(assignments)) diff --git a/openstack/tests/unit/cloud/test_role_assignment.py b/openstack/tests/unit/cloud/test_role_assignment.py index b13e3b084..48b54f71a 100644 --- a/openstack/tests/unit/cloud/test_role_assignment.py +++ b/openstack/tests/unit/cloud/test_role_assignment.py @@ -1364,14 +1364,14 @@ def test_grant_no_project_or_domain(self): with testtools.ExpectedException( exc.OpenStackCloudException, - 'Must specify either a domain or project' + 'Must specify either a domain, project or system' ): self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name) self.assert_calls() - def test_revoke_no_project_or_domain(self): + def test_revoke_no_project_or_domain_or_system(self): uris = self._get_mock_role_query_urls( self.role_data, user_data=self.user_data, @@ -1381,7 +1381,7 @@ def test_revoke_no_project_or_domain(self): with testtools.ExpectedException( exc.OpenStackCloudException, - 'Must specify either a domain or project' + 'Must specify either a domain, project or system' ): self.cloud.revoke_role( self.role_data.role_name, diff --git a/openstack/tests/unit/identity/v3/test_proxy.py b/openstack/tests/unit/identity/v3/test_proxy.py index b2af2b570..62259c735 100644 --- a/openstack/tests/unit/identity/v3/test_proxy.py +++ b/openstack/tests/unit/identity/v3/test_proxy.py @@ -499,3 +499,75 @@ def test_validate_group_has_project_role(self): self.proxy._get_resource(role.Role, 'rid') ] ) + + def test_assign_system_role_to_user(self): + self._verify( + "openstack.identity.v3.system.System.assign_role_to_user", + self.proxy.assign_system_role_to_user, + method_kwargs={'user': 'uid', 'role': 'rid', 'system': 'all'}, + expected_args=[ + self.proxy, + self.proxy._get_resource(user.User, 'uid'), + self.proxy._get_resource(role.Role, 'rid') + ] + ) + + def test_unassign_system_role_from_user(self): + self._verify( + "openstack.identity.v3.system.System.unassign_role_from_user", + self.proxy.unassign_system_role_from_user, + method_kwargs={'user': 'uid', 'role': 'rid', 'system': 'all'}, + expected_args=[ + self.proxy, + self.proxy._get_resource(user.User, 'uid'), + self.proxy._get_resource(role.Role, 'rid') + ] + ) + + def test_validate_user_has_system_role(self): + self._verify( + "openstack.identity.v3.system.System.validate_user_has_role", + self.proxy.validate_user_has_system_role, + method_kwargs={'user': 'uid', 'role': 'rid', 'system': 'all'}, + expected_args=[ + self.proxy, + self.proxy._get_resource(user.User, 'uid'), + self.proxy._get_resource(role.Role, 'rid') + ] + ) + + def test_assign_system_role_to_group(self): + self._verify( + "openstack.identity.v3.system.System.assign_role_to_group", + self.proxy.assign_system_role_to_group, + method_kwargs={'group': 'uid', 'role': 'rid', 'system': 'all'}, + expected_args=[ + self.proxy, + self.proxy._get_resource(group.Group, 'uid'), + self.proxy._get_resource(role.Role, 'rid') + ] + ) + + def test_unassign_system_role_from_group(self): + self._verify( + "openstack.identity.v3.system.System.unassign_role_from_group", + self.proxy.unassign_system_role_from_group, + method_kwargs={'group': 'uid', 'role': 'rid', 'system': 'all'}, + expected_args=[ + self.proxy, + self.proxy._get_resource(group.Group, 'uid'), + self.proxy._get_resource(role.Role, 'rid') + ] + ) + + def test_validate_group_has_system_role(self): + self._verify( + "openstack.identity.v3.system.System.validate_group_has_role", + self.proxy.validate_group_has_system_role, + method_kwargs={'group': 'uid', 'role': 'rid', 'system': 'all'}, + expected_args=[ + self.proxy, + self.proxy._get_resource(group.Group, 'uid'), + self.proxy._get_resource(role.Role, 'rid') + ] + ) diff --git a/openstack/tests/unit/identity/v3/test_role_system_group_assignment.py b/openstack/tests/unit/identity/v3/test_role_system_group_assignment.py new file mode 100644 index 000000000..17b8ec14b --- /dev/null +++ b/openstack/tests/unit/identity/v3/test_role_system_group_assignment.py @@ -0,0 +1,40 @@ +# 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.identity.v3 import role_system_group_assignment +from openstack.tests.unit import base + + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'id': IDENTIFIER, + 'name': '2', + 'group_id': '4' +} + + +class TestRoleSystemGroupAssignment(base.TestCase): + + def test_basic(self): + sot = role_system_group_assignment.RoleSystemGroupAssignment() + self.assertEqual('role', sot.resource_key) + self.assertEqual('roles', sot.resources_key) + self.assertEqual('/system/groups/%(group_id)s/roles', + sot.base_path) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = \ + role_system_group_assignment.RoleSystemGroupAssignment(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['group_id'], sot.group_id) diff --git a/openstack/tests/unit/identity/v3/test_role_system_user_assignment.py b/openstack/tests/unit/identity/v3/test_role_system_user_assignment.py new file mode 100644 index 000000000..98f9cf68a --- /dev/null +++ b/openstack/tests/unit/identity/v3/test_role_system_user_assignment.py @@ -0,0 +1,39 @@ +# 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.identity.v3 import role_system_user_assignment +from openstack.tests.unit import base + + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'id': IDENTIFIER, + 'name': '2', + 'user_id': '4' +} + + +class TestRoleSystemUserAssignment(base.TestCase): + + def test_basic(self): + sot = role_system_user_assignment.RoleSystemUserAssignment() + self.assertEqual('role', sot.resource_key) + self.assertEqual('roles', sot.resources_key) + self.assertEqual('/system/users/%(user_id)s/roles', + sot.base_path) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = \ + role_system_user_assignment.RoleSystemUserAssignment(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['name'], sot.name) diff --git a/releasenotes/notes/add-system-role-assignment-693dd3e1da33a54d.yaml b/releasenotes/notes/add-system-role-assignment-693dd3e1da33a54d.yaml new file mode 100644 index 000000000..cb171b3ef --- /dev/null +++ b/releasenotes/notes/add-system-role-assignment-693dd3e1da33a54d.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + Add support for system role assignment. A system role assignment + ultimately controls access to system-level API calls. + + Good examples of system-level APIs include management of the + service catalog and compute hypervisors. + + `System role assignment API reference + `_. From 194f354583770080494bba2ea33b1c5b81d325a1 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 4 Mar 2022 17:01:23 +0000 Subject: [PATCH 3023/3836] Update master for stable/yoga Add file to the reno documentation build to show release notes for stable/yoga. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/yoga. Sem-Ver: feature Change-Id: I3c46a1eb8d9ff8269dae5b8d2974758d23d45697 --- releasenotes/source/index.rst | 1 + releasenotes/source/yoga.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/yoga.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 7a8dd3522..5360eaa93 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ :maxdepth: 1 unreleased + yoga xena wallaby victoria diff --git a/releasenotes/source/yoga.rst b/releasenotes/source/yoga.rst new file mode 100644 index 000000000..7cd5e908a --- /dev/null +++ b/releasenotes/source/yoga.rst @@ -0,0 +1,6 @@ +========================= +Yoga Series Release Notes +========================= + +.. release-notes:: + :branch: stable/yoga From 53e81c1692ef30357990ec4ee6407401112c2275 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 4 Mar 2022 17:01:27 +0000 Subject: [PATCH 3024/3836] Add Python3 zed unit tests This is an automatically generated patch to ensure unit testing is in place for all the of the tested runtimes for zed. See also the PTI in governance [1]. [1]: https://governance.openstack.org/tc/reference/project-testing-interface.html Change-Id: Ie75ef94457e1e0ccf70de7f0c05e72d38c8d1e8c --- .zuul.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index 24196fc2a..61c1b8350 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -441,7 +441,7 @@ - project: templates: - check-requirements - - openstack-python3-yoga-jobs + - openstack-python3-zed-jobs - openstacksdk-functional-tips - openstacksdk-tox-tips - os-client-config-tox-tips From 358ddcd3944f7d08aea10135ce793b7f1916b9a8 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 28 Jan 2022 18:38:17 +0100 Subject: [PATCH 3025/3836] Run nodepool job The recent changes have broken nodepool's use of openstacksdk. This job brings up a cloud environment with devstack, then a nodepool instance that talks to it. It then builds an image with dib and attempts to have nodepool upload, boot and talk to the instance in the devstack cloud. The place to see the error in this test is from the nodepool-launcher container, that is trying to talk to the devstack cloud. This logfile is https://zuul.opendev.org/t/openstack/build/e60e7cd382834a679b22f419947a4c93/log/docker/nodepool_nodepool-launcher_1.txt#10470 The error is 2022-01-25 21:37:18,590 WARNING openstack.resource: The option [maxPersonalitySize] has been deprecated. Please avoid using it. [e: cf61304adeff45358d97a1b30552125c] [node_request: 900-0000000000] Declining node request due to exception in NodeRequestHandler: Traceback (most recent call last): File "/usr/local/lib/python3.9/site-packages/nodepool/driver/__init__.py", line 660, in run self._runHandler() File "/usr/local/lib/python3.9/site-packages/nodepool/driver/__init__.py", line 566, in _runHandler elif not self.hasProviderQuota(self.request.node_types): File "/usr/local/lib/python3.9/site-packages/nodepool/driver/openstack/handler.py", line 383, in hasProviderQuota cloud_quota = self.manager.estimatedNodepoolQuota() File "/usr/local/lib/python3.9/site-packages/nodepool/driver/utils.py", line 315, in estimatedNodepoolQuota nodepool_quota = self.getProviderLimits() File "/usr/local/lib/python3.9/site-packages/nodepool/driver/openstack/provider.py", line 102, in getProviderLimits return QuotaInformation.construct_from_limits(limits) File "/usr/local/lib/python3.9/site-packages/nodepool/driver/utils.py", line 219, in construct_from_limits instances=bound_value(limits.max_total_instances), File "/usr/local/lib/python3.9/site-packages/openstack/resource.py", line 641, in __getattribute__ raise e File "/usr/local/lib/python3.9/site-packages/openstack/resource.py", line 630, in __getattribute__ return object.__getattribute__(self, name) AttributeError: 'Limits' object has no attribute 'max_total_instances' Since nodepool just keeps trying to talk to the cloud and start an instance that never appears, the job ends up timing out. We have had some discussion over this @ https://meetings.opendev.org/irclogs/%23opendev/%23opendev.2022-01-26.log.html#t2022-01-26T17:06:12 Traditionally we've co-gated these projects as nodepool has had a very close relationship with shade and now openstacksdk. For now nodepool is just testing against released openstacksdk; but the dependent change reverts this and thus should exibit this issue. Depends-On: https://review.opendev.org/c/zuul/nodepool/+/826541 Depends-On: https://review.opendev.org/c/openstack/openstacksdk/+/826924 Depends-On: https://review.opendev.org/c/openstack/openstacksdk/+/826920 Change-Id: I04050988e97b834ce38b3ed1d67555d7df90b60d --- .zuul.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index d01995190..eeecca293 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -459,7 +459,7 @@ jobs: - opendev-buildset-registry - nodepool-build-image-siblings - # - dib-nodepool-functional-openstack-centos-8-stream-src + - dib-nodepool-functional-openstack-centos-8-stream-src - openstacksdk-functional-devstack - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-senlin @@ -486,7 +486,7 @@ jobs: - opendev-buildset-registry - nodepool-build-image-siblings - # - dib-nodepool-functional-openstack-centos-8-stream-src + - dib-nodepool-functional-openstack-centos-8-stream-src - openstacksdk-functional-devstack - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-senlin From bdccc6b798f7e436c88ad771fc19e4bf6cb36bc5 Mon Sep 17 00:00:00 2001 From: Daniel Speichert Date: Mon, 21 Mar 2022 12:46:37 -0400 Subject: [PATCH 3026/3836] fix: improperly encoded object names Static Large Object (SLO) manifests do not allow urlencoded object names. Related change: Ifbcde7f1c59bee16b4c133c3ff4ff69858c774ce Change-Id: I5a7a7c50f67d3fbda7e2df2492ccaa8e3c9f0fee --- openstack/object_store/v1/_proxy.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index 917ec3f88..54a304d85 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -591,7 +591,10 @@ def _upload_large_object( # TODO(mordred) Collect etags from results to add to this manifest # dict. Then sort the list of dicts by path. manifest.append(dict( - path='/{name}'.format(name=name), + # While Object Storage usually expects the name to be + # urlencoded in most requests, the SLO manifest requires + # plain object names instead. + path='/{name}'.format(name=parse.unquote(name)), size_bytes=segment.length)) # Try once and collect failed results to retry @@ -731,7 +734,9 @@ def _add_etag_to_manifest(self, segment_results, manifest): continue name = self._object_name_from_url(result.url) for entry in manifest: - if entry['path'] == '/{name}'.format(name=name): + if entry['path'] == '/{name}'.format( + name=parse.unquote(name) + ): entry['etag'] = result.headers['Etag'] def get_info(self): From 975cabbdd84baa8e05dc5b107b673607e7c0af1a Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Wed, 2 Feb 2022 19:45:11 +0000 Subject: [PATCH 3027/3836] Add QoS rule type filtering keys Added "all_rules" and "all_supported" to ``QoSRuleType`` class query mapping. These parameters are used for filtering the list command. Depends-On: https://review.opendev.org/c/openstack/neutron/+/827683 Related-Bug: #1959749 Change-Id: I72a44b9e9ceaf66cda3e529ac45d7717467491d9 --- openstack/network/v2/qos_rule_type.py | 3 ++- openstack/tests/unit/network/v2/test_qos_rule_type.py | 8 ++++++++ .../network-qos-rule-filter-keys-324e3222510fd362.yaml | 7 +++++++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/network-qos-rule-filter-keys-324e3222510fd362.yaml diff --git a/openstack/network/v2/qos_rule_type.py b/openstack/network/v2/qos_rule_type.py index 99e58f5f6..c115b8d6b 100644 --- a/openstack/network/v2/qos_rule_type.py +++ b/openstack/network/v2/qos_rule_type.py @@ -27,7 +27,8 @@ class QoSRuleType(resource.Resource): allow_delete = False allow_list = True - _query_mapping = resource.QueryParameters('type', 'drivers') + _query_mapping = resource.QueryParameters( + 'type', 'drivers', 'all_rules', 'all_supported') # Properties #: QoS rule type name. diff --git a/openstack/tests/unit/network/v2/test_qos_rule_type.py b/openstack/tests/unit/network/v2/test_qos_rule_type.py index a10f23aee..994eaa939 100644 --- a/openstack/tests/unit/network/v2/test_qos_rule_type.py +++ b/openstack/tests/unit/network/v2/test_qos_rule_type.py @@ -47,6 +47,14 @@ def test_basic(self): self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) + self.assertEqual({'type': 'type', + 'drivers': 'drivers', + 'all_rules': 'all_rules', + 'all_supported': 'all_supported', + 'limit': 'limit', + 'marker': 'marker', + }, + sot._query_mapping._mapping) def test_make_it(self): sot = qos_rule_type.QoSRuleType(**EXAMPLE) diff --git a/releasenotes/notes/network-qos-rule-filter-keys-324e3222510fd362.yaml b/releasenotes/notes/network-qos-rule-filter-keys-324e3222510fd362.yaml new file mode 100644 index 000000000..28b35a116 --- /dev/null +++ b/releasenotes/notes/network-qos-rule-filter-keys-324e3222510fd362.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Added two filtering keys to ``QoSRuleType`` class query mapping, used for + filtering the "list" command: "all_rules", to list all network QoS rule + types implemented in Neutron, and "all_supported", to list all network QoS + rule types supported by at least one networking mechanism driver. From 46db8d3bc135622fc79cdf1c2341b420610d193e Mon Sep 17 00:00:00 2001 From: Dmitriy Rabotyagov Date: Mon, 4 Apr 2022 15:30:28 +0200 Subject: [PATCH 3028/3836] Allow to filter endpoints by region_id Keystone allows to filter endpoints by region id. No reason to prohibit this here. Change-Id: I709df779a33e5e1a01e18e278cf1bfe76595fc86 --- openstack/identity/v3/endpoint.py | 2 +- openstack/tests/unit/identity/v3/test_endpoint.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/openstack/identity/v3/endpoint.py b/openstack/identity/v3/endpoint.py index e18b17a4b..59298aeb0 100644 --- a/openstack/identity/v3/endpoint.py +++ b/openstack/identity/v3/endpoint.py @@ -27,7 +27,7 @@ class Endpoint(resource.Resource): commit_method = 'PATCH' _query_mapping = resource.QueryParameters( - 'interface', 'service_id', + 'interface', 'region_id', 'service_id', ) # Properties diff --git a/openstack/tests/unit/identity/v3/test_endpoint.py b/openstack/tests/unit/identity/v3/test_endpoint.py index 781c4711d..b7c538e66 100644 --- a/openstack/tests/unit/identity/v3/test_endpoint.py +++ b/openstack/tests/unit/identity/v3/test_endpoint.py @@ -43,6 +43,7 @@ def test_basic(self): { 'interface': 'interface', 'service_id': 'service_id', + 'region_id': 'region_id', 'limit': 'limit', 'marker': 'marker', }, From 6b2f555ade95aee14746a570a5b2eebdc56077c8 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 12 Apr 2022 19:11:43 +0100 Subject: [PATCH 3029/3836] image: Add "id" filter for images The Glance v2 API supports a range of operators [1] for the list-images API, allowing us to do things like: GET /v2/images?status=in:saving,queued While there's no good reason to filter images by a single ID, it would be helpful to be able to filter images by *multiple* IDs. Make this possible by adding "id" to the list of possible filters. [1] https://docs.openstack.org/api-ref/image/v2/index.html?expanded=list-images-detail#list-images Change-Id: I735188e20d4e6c3b326e9080e878e717564b057d Signed-off-by: Stephen Finucane --- openstack/image/v2/image.py | 25 +++++++---- openstack/tests/unit/image/v2/test_image.py | 42 ++++++++++--------- .../image-id-filter-key-b9b6b52139a27cbe.yaml | 7 ++++ 3 files changed, 48 insertions(+), 26 deletions(-) create mode 100644 releasenotes/notes/image-id-filter-key-b9b6b52139a27cbe.yaml diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index 8fbcf4ba7..7a941f082 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -34,13 +34,24 @@ class Image(resource.Resource, tag.TagMixin, _download.DownloadMixin): _store_unknown_attrs_as_properties = True _query_mapping = resource.QueryParameters( - "name", "visibility", - "member_status", "owner", - "status", "size_min", "size_max", - "protected", "is_hidden", - "sort_key", "sort_dir", "sort", "tag", - "created_at", "updated_at", - is_hidden="os_hidden") + "id", + "name", + "visibility", + "member_status", + "owner", + "status", + "size_min", + "size_max", + "protected", + "is_hidden", + "sort_key", + "sort_dir", + "sort", + "tag", + "created_at", + "updated_at", + is_hidden="os_hidden", + ) # NOTE: Do not add "self" support here. If you've used Python before, # you know that self, while not being a reserved word, has special diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 0676bd326..ccbf1c9b0 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -140,25 +140,29 @@ def test_basic(self): self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertDictEqual({'created_at': 'created_at', - 'is_hidden': 'os_hidden', - 'limit': 'limit', - 'marker': 'marker', - 'member_status': 'member_status', - 'name': 'name', - 'owner': 'owner', - 'protected': 'protected', - 'size_max': 'size_max', - 'size_min': 'size_min', - 'sort': 'sort', - 'sort_dir': 'sort_dir', - 'sort_key': 'sort_key', - 'status': 'status', - 'tag': 'tag', - 'updated_at': 'updated_at', - 'visibility': 'visibility' - }, - sot._query_mapping._mapping) + self.assertDictEqual( + { + 'created_at': 'created_at', + 'id': 'id', + 'is_hidden': 'os_hidden', + 'limit': 'limit', + 'marker': 'marker', + 'member_status': 'member_status', + 'name': 'name', + 'owner': 'owner', + 'protected': 'protected', + 'size_max': 'size_max', + 'size_min': 'size_min', + 'sort': 'sort', + 'sort_dir': 'sort_dir', + 'sort_key': 'sort_key', + 'status': 'status', + 'tag': 'tag', + 'updated_at': 'updated_at', + 'visibility': 'visibility' + }, + sot._query_mapping._mapping, + ) def test_make_it(self): sot = image.Image(**EXAMPLE) diff --git a/releasenotes/notes/image-id-filter-key-b9b6b52139a27cbe.yaml b/releasenotes/notes/image-id-filter-key-b9b6b52139a27cbe.yaml new file mode 100644 index 000000000..6b501bf4b --- /dev/null +++ b/releasenotes/notes/image-id-filter-key-b9b6b52139a27cbe.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + It is now possible to filter ``openstack.image.v2.Image`` resources by ID + using the ``id`` filter. While this is of little value when used with + single IDs, it can be useful when combined with operators like ``in:`` + to e.g. filter by multiple image IDs. From 46c6ed8a8d99b95d2c549789a69ef4f510f5bb6a Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 24 Aug 2021 17:31:14 +0200 Subject: [PATCH 3030/3836] Rework caching Replace caching solution: - cache on the proxy layer (API communication) - support caching all GET calls - add possibility to bypass cache (important for _wait_for_ operations) Cheery-Picked-From: https://review.opendev.org/c/openstack/openstacksdk/+/805851 Change-Id: I2c8ae2c59d15c750ea8ebd3031ffdd2ced2421ed --- doc/source/user/config/configuration.rst | 34 ++++- openstack/cloud/openstackcloud.py | 38 +++--- openstack/compute/v2/limits.py | 6 +- openstack/key_manager/v1/secret.py | 8 +- openstack/message/v2/claim.py | 6 +- openstack/message/v2/message.py | 5 +- openstack/message/v2/queue.py | 6 +- openstack/message/v2/subscription.py | 6 +- openstack/object_store/v1/info.py | 6 +- openstack/orchestration/v1/stack.py | 6 +- openstack/proxy.py | 85 +++++++++++-- openstack/resource.py | 10 +- openstack/tests/unit/cloud/test_caching.py | 7 +- openstack/tests/unit/common/test_quota_set.py | 6 +- openstack/tests/unit/image/v2/test_image.py | 8 +- .../tests/unit/key_manager/v1/test_secret.py | 6 +- openstack/tests/unit/message/v2/test_claim.py | 8 +- .../tests/unit/message/v2/test_message.py | 8 +- openstack/tests/unit/message/v2/test_queue.py | 8 +- .../unit/message/v2/test_subscription.py | 8 +- .../tests/unit/orchestration/v1/test_stack.py | 6 +- openstack/tests/unit/test_proxy.py | 117 +++++++++++++++++- openstack/tests/unit/test_proxy_base.py | 3 +- openstack/tests/unit/test_resource.py | 15 ++- .../basic-api-cache-4ad8cf2754b004d1.yaml | 4 + 25 files changed, 323 insertions(+), 97 deletions(-) create mode 100644 releasenotes/notes/basic-api-cache-4ad8cf2754b004d1.yaml diff --git a/doc/source/user/config/configuration.rst b/doc/source/user/config/configuration.rst index be420f18f..c3c657d4e 100644 --- a/doc/source/user/config/configuration.rst +++ b/doc/source/user/config/configuration.rst @@ -223,11 +223,32 @@ Different cloud behaviors are also differently expensive to deal with. If you want to get really crazy and tweak stuff, you can specify different expiration times on a per-resource basis by passing values, in seconds to an expiration mapping keyed on the singular name of the resource. A value of `-1` indicates -that the resource should never expire. - -`openstacksdk` does not actually cache anything itself, but it collects -and presents the cache information so that your various applications that -are connecting to OpenStack can share a cache should you desire. +that the resource should never expire. Not specifying a value (same as +specifying `0`) indicates that no caching for this resource should be done. +`openstacksdk` only caches `GET` request responses for the queries which have +non-zero expiration time defined. Caching key contains url and request +parameters, therefore no collisions are expected. + +The expiration time key is constructed (joined with `.`) in the same way as the +metrics are emmited: + +* service type +* meaningful resource url segments (i.e. `/servers` results in `servers`, + `/servers/ID` results in `server`, `/servers/ID/metadata/KEY` results in + `server.metadata` + +Non `GET` requests cause cache invalidation based on the caching key prefix so +that i.e. `PUT` request to `/images/ID` will invalidate all images cache (list +and all individual entries). Moreover it is possible to explicitly pass +`_sdk_skip_cache` parameter to the `proxy._get` function to bypass cache and +invalidate what is already there. This is happening automatically in the +`wait_for_status` methods where it is expected that resource is going to change +some of the attributes over the time. Forcing complete cache invalidation can +be achieved calling `conn._cache.invalidate`. + +`openstacksdk` does not actually cache anything itself, but it collects and +presents the cache information so that your various applications that are +connecting to OpenStack can share a cache should you desire. .. code-block:: yaml @@ -240,6 +261,9 @@ are connecting to OpenStack can share a cache should you desire. expiration: server: 5 flavor: -1 + compute.servers: 5 + compute.flavors: -1 + image.images: 5 clouds: mtvexx: profile: vexxhost diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 395d09283..9805ceaab 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -102,26 +102,15 @@ def __init__(self): cache_class = self.config.get_cache_class() cache_arguments = self.config.get_cache_arguments() - self._resource_caches = {} + self._cache_expirations = dict() if cache_class != 'dogpile.cache.null': self.cache_enabled = True - self._cache = self._make_cache( - cache_class, cache_expiration_time, cache_arguments) - expirations = self.config.get_cache_expirations() - for expire_key in expirations.keys(): - # Only build caches for things we have list operations for - if getattr( - self, 'list_{0}'.format(expire_key), None): - self._resource_caches[expire_key] = self._make_cache( - cache_class, expirations[expire_key], cache_arguments) - - self._SERVER_AGE = DEFAULT_SERVER_AGE - self._PORT_AGE = DEFAULT_PORT_AGE - self._FLOAT_AGE = DEFAULT_FLOAT_AGE else: self.cache_enabled = False + # TODO(gtema): delete it with the standalone cloud layer caching + def _fake_invalidate(unused): pass @@ -148,15 +137,20 @@ def invalidate(self): new_func.invalidate = _fake_invalidate setattr(self, method, new_func) - # If server expiration time is set explicitly, use that. Otherwise - # fall back to whatever it was before - self._SERVER_AGE = self.config.get_cache_resource_expiration( - 'server', self._SERVER_AGE) - self._PORT_AGE = self.config.get_cache_resource_expiration( - 'port', self._PORT_AGE) - self._FLOAT_AGE = self.config.get_cache_resource_expiration( - 'floating_ip', self._FLOAT_AGE) + # Uncoditionally create cache even with a "null" backend + self._cache = self._make_cache( + cache_class, cache_expiration_time, cache_arguments) + expirations = self.config.get_cache_expirations() + for expire_key in expirations.keys(): + self._cache_expirations[expire_key] = \ + expirations[expire_key] + + # TODO(gtema): delete in next change + self._SERVER_AGE = 0 + self._PORT_AGE = 0 + self._FLOAT_AGE = 0 + self._api_cache_keys = set() self._container_cache = dict() self._file_hash_cache = dict() diff --git a/openstack/compute/v2/limits.py b/openstack/compute/v2/limits.py index 6c77b320c..fb5833be1 100644 --- a/openstack/compute/v2/limits.py +++ b/openstack/compute/v2/limits.py @@ -91,7 +91,7 @@ class Limits(resource.Resource): rate = resource.Body("rate", type=list, list_type=RateLimit) def fetch(self, session, requires_id=False, error_message=None, - base_path=None, **params): + base_path=None, skip_cache=False, **params): """Get the Limits resource. :param session: The session to use for making this request. @@ -106,5 +106,5 @@ def fetch(self, session, requires_id=False, error_message=None, session=session, requires_id=requires_id, error_message=error_message, base_path=base_path, - **params - ) + skip_cache=skip_cache, + **params) diff --git a/openstack/key_manager/v1/secret.py b/openstack/key_manager/v1/secret.py index 7cb8c23f9..7be69f997 100644 --- a/openstack/key_manager/v1/secret.py +++ b/openstack/key_manager/v1/secret.py @@ -78,7 +78,7 @@ class Secret(resource.Resource): payload_content_encoding = resource.Body('payload_content_encoding') def fetch(self, session, requires_id=True, - base_path=None, error_message=None): + base_path=None, error_message=None, skip_cache=False): request = self._prepare_request(requires_id=requires_id, base_path=base_path) @@ -93,8 +93,10 @@ def fetch(self, session, requires_id=True, # Only try to get the payload if a content type has been explicitly # specified or if one was found in the metadata response if content_type is not None: - payload = session.get(utils.urljoin(request.url, "payload"), - headers={"Accept": content_type}) + payload = session.get( + utils.urljoin(request.url, "payload"), + headers={"Accept": content_type}, + skip_cache=skip_cache) response["payload"] = payload.text # We already have the JSON here so don't call into _translate_response diff --git a/openstack/message/v2/claim.py b/openstack/message/v2/claim.py index ba9075338..9c1afae47 100644 --- a/openstack/message/v2/claim.py +++ b/openstack/message/v2/claim.py @@ -82,7 +82,7 @@ def create(self, session, prepend_key=False, base_path=None): return self def fetch(self, session, requires_id=True, - base_path=None, error_message=None): + base_path=None, error_message=None, skip_cache=False): request = self._prepare_request(requires_id=requires_id, base_path=base_path) headers = { @@ -91,8 +91,8 @@ def fetch(self, session, requires_id=True, } request.headers.update(headers) - response = session.get(request.url, - headers=request.headers) + response = session.get( + request.url, headers=request.headers, skip_cache=False) self._translate_response(response) return self diff --git a/openstack/message/v2/message.py b/openstack/message/v2/message.py index e30d0f98d..9b12b878e 100644 --- a/openstack/message/v2/message.py +++ b/openstack/message/v2/message.py @@ -112,7 +112,7 @@ def list(cls, session, paginated=True, base_path=None, **params): query_params["marker"] = new_marker def fetch(self, session, requires_id=True, - base_path=None, error_message=None): + base_path=None, error_message=None, skip_cache=False): request = self._prepare_request(requires_id=requires_id, base_path=base_path) headers = { @@ -122,7 +122,8 @@ def fetch(self, session, requires_id=True, request.headers.update(headers) response = session.get(request.url, - headers=headers) + headers=headers, + skip_cache=skip_cache) self._translate_response(response) return self diff --git a/openstack/message/v2/queue.py b/openstack/message/v2/queue.py index d6f60a8b9..560f52e3f 100644 --- a/openstack/message/v2/queue.py +++ b/openstack/message/v2/queue.py @@ -111,7 +111,7 @@ def list(cls, session, paginated=False, base_path=None, **params): query_params["marker"] = new_marker def fetch(self, session, requires_id=True, - base_path=None, error_message=None): + base_path=None, error_message=None, skip_cache=False): request = self._prepare_request(requires_id=requires_id, base_path=base_path) headers = { @@ -119,8 +119,8 @@ def fetch(self, session, requires_id=True, "X-PROJECT-ID": self.project_id or session.get_project_id() } request.headers.update(headers) - response = session.get(request.url, - headers=headers) + response = session.get( + request.url, headers=headers, skip_cache=skip_cache) self._translate_response(response) return self diff --git a/openstack/message/v2/subscription.py b/openstack/message/v2/subscription.py index d9b129c49..f29d10d1a 100644 --- a/openstack/message/v2/subscription.py +++ b/openstack/message/v2/subscription.py @@ -119,7 +119,7 @@ def list(cls, session, paginated=True, base_path=None, **params): query_params["marker"] = new_marker def fetch(self, session, requires_id=True, - base_path=None, error_message=None): + base_path=None, error_message=None, skip_cache=False): request = self._prepare_request(requires_id=requires_id, base_path=base_path) headers = { @@ -128,8 +128,8 @@ def fetch(self, session, requires_id=True, } request.headers.update(headers) - response = session.get(request.url, - headers=request.headers) + response = session.get( + request.url, headers=request.headers, skip_cache=skip_cache) self._translate_response(response) return self diff --git a/openstack/object_store/v1/info.py b/openstack/object_store/v1/info.py index 3948b2140..3ae24e5b2 100644 --- a/openstack/object_store/v1/info.py +++ b/openstack/object_store/v1/info.py @@ -33,8 +33,10 @@ class Info(resource.Resource): staticweb = resource.Body("staticweb", type=dict) tempurl = resource.Body("tempurl", type=dict) - def fetch(self, session, requires_id=False, - base_path=None, error_message=None): + def fetch( + self, session, requires_id=False, + base_path=None, skip_cache=False, error_message=None + ): """Get a remote resource based on this instance. :param session: The session to use for making this request. diff --git a/openstack/orchestration/v1/stack.py b/openstack/orchestration/v1/stack.py index ca7c4dc9f..aac18ce35 100644 --- a/openstack/orchestration/v1/stack.py +++ b/openstack/orchestration/v1/stack.py @@ -168,7 +168,8 @@ def abandon(self, session): return resp.json() def fetch(self, session, requires_id=True, - base_path=None, error_message=None, resolve_outputs=True): + base_path=None, error_message=None, + skip_cache=False, resolve_outputs=True): if not self.allow_fetch: raise exceptions.MethodNotSupported(self, "fetch") @@ -183,7 +184,8 @@ def fetch(self, session, requires_id=True, # apply parameters again, what results in them being set doubled if not resolve_outputs: request.url = request.url + '?resolve_outputs=False' - response = session.get(request.url, microversion=microversion) + response = session.get( + request.url, microversion=microversion, skip_cache=skip_cache) kwargs = {} if error_message: kwargs['error_message'] = error_message diff --git a/openstack/proxy.py b/openstack/proxy.py index 031abf373..5e3f064ed 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import functools import urllib from urllib.parse import urlparse @@ -84,21 +85,79 @@ def __init__( log_name = 'openstack' self.log = _log.setup_logging(log_name) + def _get_cache_key_prefix(self, url): + """Calculate cache prefix for the url""" + name_parts = self._extract_name( + url, self.service_type, + self.session.get_project_id()) + + return '.'.join( + [self.service_type] + + name_parts) + + def _invalidate_cache(self, conn, key_prefix): + """Invalidate all cache entries starting with given prefix""" + for k in set(conn._api_cache_keys): + if k.startswith(key_prefix): + conn._cache.delete(k) + conn._api_cache_keys.remove(k) + def request( self, url, method, error_message=None, raise_exc=False, connect_retries=1, - global_request_id=None, *args, **kwargs): + global_request_id=None, + *args, **kwargs): + conn = self._get_connection() if not global_request_id: - conn = self._get_connection() - if conn: - # Per-request setting should take precedence - global_request_id = conn._global_request_id + # Per-request setting should take precedence + global_request_id = conn._global_request_id + + key = None + key_prefix = self._get_cache_key_prefix(url) + # The caller might want to force cache bypass. + skip_cache = kwargs.pop('skip_cache', False) + if conn.cache_enabled: + # Construct cache key. It consists of: + # service.name_parts.URL.str(kwargs) + key = '.'.join( + [key_prefix, url, str(kwargs)] + ) + + # Track cache key for invalidating possibility + conn._api_cache_keys.add(key) + try: - response = super(Proxy, self).request( - url, method, - connect_retries=connect_retries, raise_exc=raise_exc, - global_request_id=global_request_id, - **kwargs) + if conn.cache_enabled and not skip_cache and method == 'GET': + # Get the object expiration time from config + # default to 0 to disable caching for this resource type + expiration_time = int( + conn._cache_expirations.get(key_prefix, 0)) + # Get from cache or execute and cache + response = conn._cache.get_or_create( + key=key, + creator=super(Proxy, self).request, + creator_args=( + [url, method], + dict( + connect_retries=connect_retries, + raise_exc=raise_exc, + global_request_id=global_request_id, + **kwargs + ) + ), + expiration_time=expiration_time + ) + else: + # invalidate cache if we send modification request or user + # asked for cache bypass + self._invalidate_cache(conn, key_prefix) + # Pass through the API request bypassing cache + response = super(Proxy, self).request( + url, method, + connect_retries=connect_retries, raise_exc=raise_exc, + global_request_id=global_request_id, + **kwargs) + for h in response.history: self._report_stats(h) self._report_stats(response) @@ -111,6 +170,7 @@ def request( self._report_stats(None, url, method, e) raise + @functools.lru_cache(maxsize=256) def _extract_name(self, url, service_type=None, project_id=None): '''Produce a key name to use in logging/metrics from the URL path. @@ -484,7 +544,7 @@ def _bulk_create(self, resource_type, data, base_path=None): @_check_resource(strict=False) def _get(self, resource_type, value=None, requires_id=True, - base_path=None, **attrs): + base_path=None, skip_cache=False, **attrs): """Fetch a resource :param resource_type: The type of resource to get. @@ -495,6 +555,8 @@ def _get(self, resource_type, value=None, requires_id=True, :param str base_path: Base part of the URI for fetching resources, if different from :data:`~openstack.resource.Resource.base_path`. + :param bool skip_cache: A boolean indicating whether optional API + cache should be skipped for this invocation. :param dict attrs: Attributes to be passed onto the :meth:`~openstack.resource.Resource.get` method. These should correspond @@ -509,6 +571,7 @@ def _get(self, resource_type, value=None, requires_id=True, return res.fetch( self, requires_id=requires_id, base_path=base_path, + skip_cache=skip_cache, error_message="No {resource_type} found for {value}".format( resource_type=resource_type.__name__, value=value)) diff --git a/openstack/resource.py b/openstack/resource.py index 91a68c25b..45614f31e 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1480,6 +1480,7 @@ def fetch( requires_id=True, base_path=None, error_message=None, + skip_cache=False, **params, ): """Get a remote resource based on this instance. @@ -1492,6 +1493,8 @@ def fetch( different from :data:`~openstack.resource.Resource.base_path`. :param str error_message: An Error message to be returned if requested object does not exist. + :param bool skip_cache: A boolean indicating whether optional API + cache should be skipped for this invocation. :param dict params: Additional parameters that can be consumed. :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if @@ -1507,7 +1510,8 @@ def fetch( session = self._get_session(session) microversion = self._get_microversion_for(session, 'fetch') response = session.get(request.url, microversion=microversion, - params=params) + params=params, + skip_cache=skip_cache) kwargs = {} if error_message: kwargs['error_message'] = error_message @@ -2078,7 +2082,7 @@ def wait_for_status( timeout=wait, message=msg, wait=interval): - resource = resource.fetch(session) + resource = resource.fetch(session, skip_cache=True) if not resource: raise exceptions.ResourceFailure( @@ -2120,7 +2124,7 @@ def wait_for_delete(session, resource, interval, wait): id=resource.id), wait=interval): try: - resource = resource.fetch(session) + resource = resource.fetch(session, skip_cache=True) if not resource: return orig_resource if resource.status.lower() == 'deleted': diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py index 633b6e3fd..7b700737d 100644 --- a/openstack/tests/unit/cloud/test_caching.py +++ b/openstack/tests/unit/cloud/test_caching.py @@ -535,12 +535,13 @@ def test_list_ports_filtered(self): down_port = test_port.TestPort.mock_neutron_port_create_rep['port'] active_port = down_port.copy() active_port['status'] = 'ACTIVE' - # We're testing to make sure a query string isn't passed when we're - # caching, but that the results are still filtered. + # We're testing to make sure a query string is passed when we're + # caching (cache by url), and that the results are still filtered. self.register_uris([ dict(method='GET', uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports']), + 'network', 'public', append=['v2.0', 'ports'], + qs_elements=['status=DOWN']), json={'ports': [ down_port, active_port, diff --git a/openstack/tests/unit/common/test_quota_set.py b/openstack/tests/unit/common/test_quota_set.py index cc525010f..fe7b49d75 100644 --- a/openstack/tests/unit/common/test_quota_set.py +++ b/openstack/tests/unit/common/test_quota_set.py @@ -89,7 +89,8 @@ def test_get(self): self.sess.get.assert_called_with( '/os-quota-sets/proj', microversion=1, - params={}) + params={}, + skip_cache=False) self.assertEqual(BASIC_EXAMPLE['backups'], sot.backups) self.assertEqual({}, sot.reservation) @@ -110,7 +111,8 @@ def test_get_usage(self): self.sess.get.assert_called_with( '/os-quota-sets/proj', microversion=1, - params={'usage': True}) + params={'usage': True}, + skip_cache=False) self.assertEqual( USAGE_EXAMPLE['backups']['limit'], diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 0676bd326..a546882d2 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -407,7 +407,8 @@ def test_download_no_checksum_header(self): self.sess.get.assert_has_calls( [mock.call('images/IDENTIFIER/file', stream=False), - mock.call('images/IDENTIFIER', microversion=None, params={})]) + mock.call('images/IDENTIFIER', microversion=None, params={}, + skip_cache=False)]) self.assertEqual(rv, resp1) @@ -436,7 +437,8 @@ def test_download_no_checksum_at_all2(self): self.sess.get.assert_has_calls( [mock.call('images/IDENTIFIER/file', stream=False), - mock.call('images/IDENTIFIER', microversion=None, params={})]) + mock.call('images/IDENTIFIER', microversion=None, params={}, + skip_cache=False)]) self.assertEqual(rv, resp1) @@ -536,7 +538,7 @@ def test_image_find(self): self.sess.get.assert_has_calls([ mock.call('images/' + EXAMPLE['name'], microversion=None, - params={}), + params={}, skip_cache=False), mock.call('/images', headers={'Accept': 'application/json'}, microversion=None, params={'name': EXAMPLE['name']}), mock.call('/images', headers={'Accept': 'application/json'}, diff --git a/openstack/tests/unit/key_manager/v1/test_secret.py b/openstack/tests/unit/key_manager/v1/test_secret.py index a1d5693bd..7202c54ae 100644 --- a/openstack/tests/unit/key_manager/v1/test_secret.py +++ b/openstack/tests/unit/key_manager/v1/test_secret.py @@ -113,8 +113,10 @@ def _test_payload(self, sot, metadata, content_type): sess.get.assert_has_calls( [mock.call("secrets/id",), - mock.call("secrets/id/payload", - headers={"Accept": content_type})]) + mock.call( + "secrets/id/payload", + headers={"Accept": content_type}, + skip_cache=False)]) self.assertEqual(rv.payload, payload) self.assertEqual(rv.status, metadata["status"]) diff --git a/openstack/tests/unit/message/v2/test_claim.py b/openstack/tests/unit/message/v2/test_claim.py index 4f3a93f10..673b4547d 100644 --- a/openstack/tests/unit/message/v2/test_claim.py +++ b/openstack/tests/unit/message/v2/test_claim.py @@ -141,8 +141,8 @@ def test_get(self, mock_uuid): "queue": FAKE1["queue_name"], "claim": FAKE1["id"]} headers = {"Client-ID": "NEW_CLIENT_ID", "X-PROJECT-ID": "NEW_PROJECT_ID"} - sess.get.assert_called_with(url, - headers=headers) + sess.get.assert_called_with( + url, headers=headers, skip_cache=False) sess.get_project_id.assert_called_once_with() sot._translate_response.assert_called_once_with(resp) self.assertEqual(sot, res) @@ -160,8 +160,8 @@ def test_get_client_id_project_id_exist(self): "queue": FAKE2["queue_name"], "claim": FAKE2["id"]} headers = {"Client-ID": "OLD_CLIENT_ID", "X-PROJECT-ID": "OLD_PROJECT_ID"} - sess.get.assert_called_with(url, - headers=headers) + sess.get.assert_called_with( + url, headers=headers, skip_cache=False) sot._translate_response.assert_called_once_with(resp) self.assertEqual(sot, res) diff --git a/openstack/tests/unit/message/v2/test_message.py b/openstack/tests/unit/message/v2/test_message.py index 42fa601b5..a6af49ca5 100644 --- a/openstack/tests/unit/message/v2/test_message.py +++ b/openstack/tests/unit/message/v2/test_message.py @@ -151,8 +151,8 @@ def test_get(self, mock_uuid): 'queue': FAKE1['queue_name'], 'message': FAKE1['id']} headers = {'Client-ID': 'NEW_CLIENT_ID', 'X-PROJECT-ID': 'NEW_PROJECT_ID'} - sess.get.assert_called_with(url, - headers=headers) + sess.get.assert_called_with( + url, headers=headers, skip_cache=False) sess.get_project_id.assert_called_once_with() sot._translate_response.assert_called_once_with(resp) self.assertEqual(sot, res) @@ -173,8 +173,8 @@ def test_get_client_id_project_id_exist(self): res = sot.fetch(sess) headers = {'Client-ID': 'OLD_CLIENT_ID', 'X-PROJECT-ID': 'OLD_PROJECT_ID'} - sess.get.assert_called_with(url, - headers=headers) + sess.get.assert_called_with( + url, headers=headers, skip_cache=False) sot._translate_response.assert_called_once_with(resp) self.assertEqual(sot, res) diff --git a/openstack/tests/unit/message/v2/test_queue.py b/openstack/tests/unit/message/v2/test_queue.py index 4758a91bd..95ac8ec13 100644 --- a/openstack/tests/unit/message/v2/test_queue.py +++ b/openstack/tests/unit/message/v2/test_queue.py @@ -109,8 +109,8 @@ def test_get(self, mock_uuid): url = 'queues/%s' % FAKE1['name'] headers = {'Client-ID': 'NEW_CLIENT_ID', 'X-PROJECT-ID': 'NEW_PROJECT_ID'} - sess.get.assert_called_with(url, - headers=headers) + sess.get.assert_called_with( + url, headers=headers, skip_cache=False) sess.get_project_id.assert_called_once_with() sot._translate_response.assert_called_once_with(resp) self.assertEqual(sot, res) @@ -127,8 +127,8 @@ def test_get_client_id_project_id_exist(self): url = 'queues/%s' % FAKE2['name'] headers = {'Client-ID': 'OLD_CLIENT_ID', 'X-PROJECT-ID': 'OLD_PROJECT_ID'} - sess.get.assert_called_with(url, - headers=headers) + sess.get.assert_called_with( + url, headers=headers, skip_cache=False) sot._translate_response.assert_called_once_with(resp) self.assertEqual(sot, res) diff --git a/openstack/tests/unit/message/v2/test_subscription.py b/openstack/tests/unit/message/v2/test_subscription.py index ce3c75fbd..8e50c019d 100644 --- a/openstack/tests/unit/message/v2/test_subscription.py +++ b/openstack/tests/unit/message/v2/test_subscription.py @@ -127,8 +127,8 @@ def test_get(self, mock_uuid): "queue": FAKE1["queue_name"], "subscription": FAKE1["id"]} headers = {"Client-ID": "NEW_CLIENT_ID", "X-PROJECT-ID": "NEW_PROJECT_ID"} - sess.get.assert_called_with(url, - headers=headers) + sess.get.assert_called_with( + url, headers=headers, skip_cache=False) sess.get_project_id.assert_called_once_with() sot._translate_response.assert_called_once_with(resp) self.assertEqual(sot, res) @@ -146,8 +146,8 @@ def test_get_client_id_project_id_exist(self): "queue": FAKE2["queue_name"], "subscription": FAKE2["id"]} headers = {"Client-ID": "OLD_CLIENT_ID", "X-PROJECT-ID": "OLD_PROJECT_ID"} - sess.get.assert_called_with(url, - headers=headers) + sess.get.assert_called_with( + url, headers=headers, skip_cache=False) sot._translate_response.assert_called_once_with(resp) self.assertEqual(sot, res) diff --git a/openstack/tests/unit/orchestration/v1/test_stack.py b/openstack/tests/unit/orchestration/v1/test_stack.py index f3aad5d5f..06912fe09 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack.py +++ b/openstack/tests/unit/orchestration/v1/test_stack.py @@ -232,11 +232,13 @@ def test_fetch(self): self.assertEqual(sot, sot.fetch(sess)) sess.get.assert_called_with( 'stacks/{id}'.format(id=sot.id), - microversion=None) + microversion=None, + skip_cache=False) sot.fetch(sess, resolve_outputs=False) sess.get.assert_called_with( 'stacks/{id}?resolve_outputs=False'.format(id=sot.id), - microversion=None) + microversion=None, + skip_cache=False) ex = self.assertRaises(exceptions.ResourceNotFound, sot.fetch, sess) self.assertEqual('oops', str(ex)) ex = self.assertRaises(exceptions.ResourceNotFound, sot.fetch, sess) diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index ab94827a6..f2f55a54a 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import copy import queue from unittest import mock @@ -35,7 +36,7 @@ class CreateableResource(resource.Resource): class RetrieveableResource(resource.Resource): - allow_retrieve = True + allow_fetch = True class ListableResource(resource.Resource): @@ -380,6 +381,7 @@ def test_get_resource(self): self.res.fetch.assert_called_with( self.sot, requires_id=True, base_path=None, + skip_cache=mock.ANY, error_message=mock.ANY) self.assertEqual(rv, self.fake_result) @@ -390,6 +392,7 @@ def test_get_resource_with_args(self): self.res._update.assert_called_once_with(**args) self.res.fetch.assert_called_with( self.sot, requires_id=True, base_path=None, + skip_cache=mock.ANY, error_message=mock.ANY) self.assertEqual(rv, self.fake_result) @@ -400,6 +403,7 @@ def test_get_id(self): connection=self.cloud, id=self.fake_id) self.res.fetch.assert_called_with( self.sot, requires_id=True, base_path=None, + skip_cache=mock.ANY, error_message=mock.ANY) self.assertEqual(rv, self.fake_result) @@ -412,6 +416,7 @@ def test_get_base_path(self): connection=self.cloud, id=self.fake_id) self.res.fetch.assert_called_with( self.sot, requires_id=True, base_path=base_path, + skip_cache=mock.ANY, error_message=mock.ANY) self.assertEqual(rv, self.fake_result) @@ -521,6 +526,116 @@ def test_extract_name(self): self.assertEqual(self.parts, results) +class TestProxyCache(base.TestCase): + + class Res(resource.Resource): + base_path = 'fake' + + allow_commit = True + allow_fetch = True + + foo = resource.Body('foo') + + def setUp(self): + super(TestProxyCache, self).setUp( + cloud_config_fixture='clouds_cache.yaml') + + self.session = mock.Mock() + self.session._sdk_connection = self.cloud + self.session.get_project_id = mock.Mock(return_value='fake_prj') + + self.response = mock.Mock() + self.response.status_code = 200 + self.response.history = [] + self.response.headers = {} + self.response.body = {} + self.response.json = mock.Mock( + return_value=self.response.body) + self.session.request = mock.Mock( + return_value=self.response) + + self.sot = proxy.Proxy(self.session) + self.sot._connection = self.cloud + self.sot.service_type = 'srv' + + def _get_key(self, id): + return ( + f"srv.fake.fake/{id}." + "{'microversion': None, 'params': {}}") + + def test_get_not_in_cache(self): + self.cloud._cache_expirations['srv.fake'] = 5 + self.sot._get(self.Res, '1') + + self.session.request.assert_called_with( + 'fake/1', + 'GET', + connect_retries=mock.ANY, raise_exc=mock.ANY, + global_request_id=mock.ANY, + endpoint_filter=mock.ANY, + headers=mock.ANY, + microversion=mock.ANY, params=mock.ANY + ) + self.assertIn( + self._get_key(1), + self.cloud._api_cache_keys) + + def test_get_from_cache(self): + key = self._get_key(2) + + self.cloud._cache.set(key, self.response) + # set expiration for the resource to respect cache + self.cloud._cache_expirations['srv.fake'] = 5 + + self.sot._get(self.Res, '2') + self.session.request.assert_not_called() + + def test_modify(self): + key = self._get_key(3) + + self.cloud._cache.set(key, self.response) + self.cloud._api_cache_keys.add(key) + self.cloud._cache_expirations['srv.fake'] = 5 + + # Ensure first call gets value from cache + self.sot._get(self.Res, '3') + self.session.request.assert_not_called() + + # update call invalidates the cache and triggers API + rs = self.Res.existing(id='3') + self.sot._update(self.Res, rs, foo='bar') + + self.session.request.assert_called() + self.assertIsNotNone(self.cloud._cache.get(key)) + self.assertEqual( + 'NoValue', + type(self.cloud._cache.get(key)).__name__) + self.assertNotIn(key, self.cloud._api_cache_keys) + + # next get call again triggers API + self.sot._get(self.Res, '3') + self.session.request.assert_called() + + def test_get_bypass_cache(self): + key = self._get_key(4) + + resp = copy.deepcopy(self.response) + resp.body = {'foo': 'bar'} + self.cloud._api_cache_keys.add(key) + self.cloud._cache.set(key, resp) + # set expiration for the resource to respect cache + self.cloud._cache_expirations['srv.fake'] = 5 + + self.sot._get(self.Res, '4', skip_cache=True) + self.session.request.assert_called() + # validate we got empty body as expected, and not what is in cache + self.assertEqual(dict(), self.response.body) + self.assertNotIn(key, self.cloud._api_cache_keys) + self.assertEqual( + 'NoValue', + type(self.cloud._cache.get(key)).__name__) + + class TestProxyCleanup(base.TestCase): def setUp(self): diff --git a/openstack/tests/unit/test_proxy_base.py b/openstack/tests/unit/test_proxy_base.py index e5ee699ac..a6c62065e 100644 --- a/openstack/tests/unit/test_proxy_base.py +++ b/openstack/tests/unit/test_proxy_base.py @@ -158,7 +158,8 @@ def verify_get_overrided(self, proxy, resource_type, patch_target): res.fetch.assert_called_once_with( proxy, requires_id=True, base_path=None, - error_message=mock.ANY) + error_message=mock.ANY, + skip_cache=False) def verify_head( self, test_method, resource_type, base_path=None, *, diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 66cfff711..1fd84020f 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1620,7 +1620,8 @@ def test_fetch(self): self.sot._prepare_request.assert_called_once_with( requires_id=True, base_path=None) self.session.get.assert_called_once_with( - self.request.url, microversion=None, params={}) + self.request.url, microversion=None, params={}, + skip_cache=False) self.assertIsNone(self.sot.microversion) self.sot._translate_response.assert_called_once_with(self.response) @@ -1632,7 +1633,8 @@ def test_fetch_with_params(self): self.sot._prepare_request.assert_called_once_with( requires_id=True, base_path=None) self.session.get.assert_called_once_with( - self.request.url, microversion=None, params={'fields': 'a,b'}) + self.request.url, microversion=None, params={'fields': 'a,b'}, + skip_cache=False) self.assertIsNone(self.sot.microversion) self.sot._translate_response.assert_called_once_with(self.response) @@ -1654,7 +1656,8 @@ class Test(resource.Resource): sot._prepare_request.assert_called_once_with( requires_id=True, base_path=None) self.session.get.assert_called_once_with( - self.request.url, microversion='1.42', params={}) + self.request.url, microversion='1.42', params={}, + skip_cache=False) self.assertEqual(sot.microversion, '1.42') sot._translate_response.assert_called_once_with(self.response) @@ -1666,7 +1669,8 @@ def test_fetch_not_requires_id(self): self.sot._prepare_request.assert_called_once_with( requires_id=False, base_path=None) self.session.get.assert_called_once_with( - self.request.url, microversion=None, params={}) + self.request.url, microversion=None, params={}, + skip_cache=False) self.sot._translate_response.assert_called_once_with(self.response) self.assertEqual(result, self.sot) @@ -1678,7 +1682,8 @@ def test_fetch_base_path(self): requires_id=False, base_path='dummy') self.session.get.assert_called_once_with( - self.request.url, microversion=None, params={}) + self.request.url, microversion=None, params={}, + skip_cache=False) self.sot._translate_response.assert_called_once_with(self.response) self.assertEqual(result, self.sot) diff --git a/releasenotes/notes/basic-api-cache-4ad8cf2754b004d1.yaml b/releasenotes/notes/basic-api-cache-4ad8cf2754b004d1.yaml new file mode 100644 index 000000000..7ac9f23a2 --- /dev/null +++ b/releasenotes/notes/basic-api-cache-4ad8cf2754b004d1.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add possibility to cache GET requests using dogpile cache. From 1e50ea5023289f81b9b9181309e25c5d3f8fd7a9 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 15 Apr 2022 16:19:42 +0200 Subject: [PATCH 3031/3836] Add R1 summary release note Lot of cleanup and normalization work has been done without release notes. Add now a summary of changes included in the next release. Change-Id: I22a173f7e00cfb0466f1bf6a7f90f75d1db005f3 --- releasenotes/notes/r1-cab94ae7d749a1ec.yaml | 31 +++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 releasenotes/notes/r1-cab94ae7d749a1ec.yaml diff --git a/releasenotes/notes/r1-cab94ae7d749a1ec.yaml b/releasenotes/notes/r1-cab94ae7d749a1ec.yaml new file mode 100644 index 000000000..3f429a3bf --- /dev/null +++ b/releasenotes/notes/r1-cab94ae7d749a1ec.yaml @@ -0,0 +1,31 @@ +--- +prelude: > + This is a first major release of OpenStackSDK. + + From now on interface can be considered stable and will also in future + strictly follow SemVer model. This release includes work in ensuring + methods and attribute naming are consistent across the code basis and first + steps in implementing even more generalizations in the processing logic. + + Microversion support is now considered as stable and session will be + established with the highest version supported by both client and server. +upgrade: + - | + This release includes work in enforcing consistency of the cloud layer + methods. Now they all return SDK resource objects where previously Munch + objects could have been returned. This leads to few important facts: + + - Return object types of various cloud.XXX calls now rely on proxy layer + functions and strictly return SDK resources. + - Some attributes of various resources may be named differently to + follow SDK attribute naming convention. + - Returned objects may forbid setting attributes (read-only attributes). + + Mentioned changes are affecting Ansible modules (which rely on + OpenStackSDK). Historically Ansible modules return to the Ansible engine + whatever SDK returns to it. Under some conditions Ansible may decide to + unset properties (if it decides it contain sensitive information). While + this is correct SDK forbids setting of some attributes what leads to + errors. This release is therefore marking incompatibility with OpenStack + Ansible modules in R1.X.X and the work on fixing it is being done in + R2.X.X of modules repository. From 360d517a1abb0b2f5b5a005b0de1823f170a6c03 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 15 Apr 2022 16:35:52 +0200 Subject: [PATCH 3032/3836] Improve StatsD metric precision Make possible to see timings and counts for invoked APIs also based on the returned status code. This helps to make deeper investigations of the cloud behaviour (it might be very useful to see all variety of particular status code occurence, especially error one). Since latency of 404 and 202 may vary heavily this also helps to reduce such interference. In addition to that start emiting statsd metric for timeouts from API. Change-Id: I8eb0174afc36f9ff10e2bd434f803f63736c160c --- openstack/proxy.py | 60 ++++++++++++------ openstack/tests/unit/test_stats.py | 61 +++++++++++++++++-- .../improve-metrics-5d7ce70ce4021d72.yaml | 5 ++ 3 files changed, 103 insertions(+), 23 deletions(-) create mode 100644 releasenotes/notes/improve-metrics-5d7ce70ce4021d72.yaml diff --git a/openstack/proxy.py b/openstack/proxy.py index 031abf373..156eca646 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -50,6 +50,12 @@ def check(self, expected, actual=None, *args, **kwargs): return wrap +def normalize_metric_name(name): + name = name.replace('.', '_') + name = name.replace(':', '_') + return name + + class Proxy(adapter.Adapter): """Represents a service.""" @@ -204,22 +210,35 @@ def _report_stats(self, response, url=None, method=None, exc=None): self._report_stats_influxdb(response, url, method, exc) def _report_stats_statsd(self, response, url=None, method=None, exc=None): - if response is not None and not url: - url = response.request.url - if response is not None and not method: - method = response.request.method - name_parts = self._extract_name(url, - self.service_type, - self.session.get_project_id()) - key = '.'.join( - [self._statsd_prefix, self.service_type, method] - + name_parts) - with self._statsd_client.pipeline() as pipe: - if response is not None: - pipe.timing(key, response.elapsed) - pipe.incr(key) - elif exc is not None: - pipe.incr('%s.failed' % key) + try: + if response is not None and not url: + url = response.request.url + if response is not None and not method: + method = response.request.method + name_parts = [ + normalize_metric_name(f) for f in + self._extract_name( + url, self.service_type, self.session.get_project_id()) + ] + key = '.'.join( + [self._statsd_prefix, + normalize_metric_name(self.service_type), method, + '_'.join(name_parts) + ]) + with self._statsd_client.pipeline() as pipe: + if response is not None: + duration = int(response.elapsed.total_seconds() * 1000) + metric_name = '%s.%s' % (key, str(response.status_code)) + pipe.timing(metric_name, duration) + pipe.incr(metric_name) + if duration > 1000: + pipe.incr('%s.over_1000' % key) + elif exc is not None: + pipe.incr('%s.failed' % key) + pipe.incr('%s.attempted' % key) + except Exception: + # We do not want errors in metric reporting ever break client + self.log.exception("Exception reporting metrics") def _report_stats_prometheus(self, response, url=None, method=None, exc=None): @@ -253,9 +272,12 @@ def _report_stats_influxdb(self, response, url=None, method=None, method = response.request.method tags = dict( method=method, - name='_'.join(self._extract_name( - url, self.service_type, - self.session.get_project_id())) + name='_'.join([ + normalize_metric_name(f) for f in + self._extract_name( + url, self.service_type, + self.session.get_project_id()) + ]) ) fields = dict( attempted=1 diff --git a/openstack/tests/unit/test_stats.py b/openstack/tests/unit/test_stats.py index 086306cf8..288621b9a 100644 --- a/openstack/tests/unit/test_stats.py +++ b/openstack/tests/unit/test_stats.py @@ -23,7 +23,9 @@ import time import fixtures +from keystoneauth1 import exceptions import prometheus_client +from requests import exceptions as rexceptions import testtools.content from openstack.tests.unit import base @@ -175,7 +177,7 @@ def test_list_projects(self): self.assert_calls() self.assert_reported_stat( - 'openstack.api.identity.GET.projects', value='1', kind='c') + 'openstack.api.identity.GET.projects.200', value='1', kind='c') self.assert_prometheus_stat( 'openstack_http_requests_total', 1, dict( service_type='identity', @@ -196,7 +198,7 @@ def test_projects(self): self.assert_calls() self.assert_reported_stat( - 'openstack.api.identity.GET.projects', value='1', kind='c') + 'openstack.api.identity.GET.projects.200', value='1', kind='c') self.assert_prometheus_stat( 'openstack_http_requests_total', 1, dict( service_type='identity', @@ -217,7 +219,11 @@ def test_servers(self): self.assert_calls() self.assert_reported_stat( - 'openstack.api.compute.GET.servers.detail', value='1', kind='c') + 'openstack.api.compute.GET.servers_detail.200', + value='1', kind='c') + self.assert_reported_stat( + 'openstack.api.compute.GET.servers_detail.200', + value='0', kind='ms') self.assert_prometheus_stat( 'openstack_http_requests_total', 1, dict( service_type='compute', @@ -237,7 +243,11 @@ def test_servers_no_detail(self): self.assert_calls() self.assert_reported_stat( - 'openstack.api.compute.GET.servers', value='1', kind='c') + 'openstack.api.compute.GET.servers.200', value='1', kind='c') + self.assert_reported_stat( + 'openstack.api.compute.GET.servers.200', value='0', kind='ms') + self.assert_reported_stat( + 'openstack.api.compute.GET.servers.attempted', value='1', kind='c') self.assert_prometheus_stat( 'openstack_http_requests_total', 1, dict( service_type='compute', @@ -245,6 +255,49 @@ def test_servers_no_detail(self): method='GET', status_code='200')) + def test_servers_error(self): + + mock_uri = 'https://compute.example.com/v2.1/servers' + + self.register_uris([ + dict(method='GET', uri=mock_uri, status_code=500, + json={})]) + + self.cloud.compute.get('/servers') + self.assert_calls() + + self.assert_reported_stat( + 'openstack.api.compute.GET.servers.500', value='1', kind='c') + self.assert_reported_stat( + 'openstack.api.compute.GET.servers.500', value='0', kind='ms') + self.assert_reported_stat( + 'openstack.api.compute.GET.servers.attempted', value='1', kind='c') + self.assert_prometheus_stat( + 'openstack_http_requests_total', 1, dict( + service_type='compute', + endpoint=mock_uri, + method='GET', + status_code='500')) + + def test_timeout(self): + + mock_uri = 'https://compute.example.com/v2.1/servers' + + self.register_uris([ + dict(method='GET', uri=mock_uri, + exc=rexceptions.ConnectTimeout) + ]) + + try: + self.cloud.compute.get('/servers') + except exceptions.ConnectTimeout: + pass + + self.assert_reported_stat( + 'openstack.api.compute.GET.servers.failed', value='1', kind='c') + self.assert_reported_stat( + 'openstack.api.compute.GET.servers.attempted', value='1', kind='c') + class TestNoStats(base.TestCase): diff --git a/releasenotes/notes/improve-metrics-5d7ce70ce4021d72.yaml b/releasenotes/notes/improve-metrics-5d7ce70ce4021d72.yaml new file mode 100644 index 000000000..86d275947 --- /dev/null +++ b/releasenotes/notes/improve-metrics-5d7ce70ce4021d72.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + API metrics emitted by OpenStackSDK to StatsD now contain status_code + part of the metric name in order to improve information precision. From 9bf73433e4cb8c3b72d553cca48b80c03db427eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20Gagne=CC=81?= Date: Wed, 20 Apr 2022 10:40:13 -0400 Subject: [PATCH 3033/3836] Update Internap auth URL Change-Id: I311a0205999c8b6f810478036b4343dce2855b08 --- doc/source/user/config/vendor-support.rst | 2 +- doc/source/user/multi-cloud-demo.rst | 2 +- openstack/config/vendors/internap.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/user/config/vendor-support.rst b/doc/source/user/config/vendor-support.rst index 41d4a6ca7..5e1ea395c 100644 --- a/doc/source/user/config/vendor-support.rst +++ b/doc/source/user/config/vendor-support.rst @@ -185,7 +185,7 @@ cystack Netherlands Internap -------- -https://identity.api.cloud.iweb.com/v2.0 +https://identity.api.cloud.inap.com/v2.0 ============== ================ Region Name Location diff --git a/doc/source/user/multi-cloud-demo.rst b/doc/source/user/multi-cloud-demo.rst index e8fd73d2a..710055df4 100644 --- a/doc/source/user/multi-cloud-demo.rst +++ b/doc/source/user/multi-cloud-demo.rst @@ -294,7 +294,7 @@ Much more complex clouds.yaml example my-internap: auth: - auth_url: https://identity.api.cloud.iweb.com + auth_url: https://identity.api.cloud.inap.com username: api-55f9a00fb2619 project_name: inap-17037 identity_api_version: 3 diff --git a/openstack/config/vendors/internap.json b/openstack/config/vendors/internap.json index b67fc06d4..03ce2418d 100644 --- a/openstack/config/vendors/internap.json +++ b/openstack/config/vendors/internap.json @@ -2,7 +2,7 @@ "name": "internap", "profile": { "auth": { - "auth_url": "https://identity.api.cloud.iweb.com" + "auth_url": "https://identity.api.cloud.inap.com" }, "regions": [ "ams01", From 136dd034b0892eceac3adc43624b0b3f648f38f8 Mon Sep 17 00:00:00 2001 From: Jakob Meng Date: Thu, 21 Apr 2022 21:10:19 +0200 Subject: [PATCH 3034/3836] Fix creation of protected image for old user code Commit afb49692 [1][2] replaced "protected" with "is_protected" in raw image properties in order to fix an server-side schema validation error (400 Bad Request) when defining image properties. That change broke user code which calls e.g. create_image() with "protected" instead of "is_protected" causing the exact server-side schema validation error which it tried to fix for "is_protected" [3]. This patch readds "protected" to raw image properties and thus allows clients to continue to use the old "protected" attribute. Ref.: [1] https://github.com/openstack/openstacksdk/commit/afb49692f5bfab259af8c594eedbddcb52cbde81 [2] https://review.opendev.org/c/openstack/openstacksdk/+/820926 [3] https://bugs.launchpad.net/python-glanceclient/+bug/1927215 Change-Id: I37b046dd71c29734504cf55f272b6dffbb8b2aad --- openstack/image/v2/_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index de3a8e2e3..161041f5e 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -26,7 +26,7 @@ # Rackspace returns this for intermittent import errors _IMAGE_ERROR_396 = "Image cannot be imported. Error code: '396'" _INT_PROPERTIES = ('min_disk', 'min_ram', 'size', 'virtual_size') -_RAW_PROPERTIES = ('is_protected', 'tags') +_RAW_PROPERTIES = ('is_protected', 'protected', 'tags') class Proxy(_base_proxy.BaseImageProxy): From 0474716c79f0ca651520d647310ebe40c7a75044 Mon Sep 17 00:00:00 2001 From: Jan Hartkopf Date: Mon, 21 Mar 2022 10:25:37 +0100 Subject: [PATCH 3035/3836] network RBAC policy: allow query for target tenant This is required to send the "target_tenant" query parameter in order to be able to filter network RBAC policies by target tenant. Story: 2009937 Task: 44825 Change-Id: I98c599996a316ef2722754bf181d943ed5820978 Signed-off-by: Jan Hartkopf --- openstack/network/v2/rbac_policy.py | 2 +- openstack/tests/unit/network/v2/test_rbac_policy.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/openstack/network/v2/rbac_policy.py b/openstack/network/v2/rbac_policy.py index 09cb60743..f3178dcd9 100644 --- a/openstack/network/v2/rbac_policy.py +++ b/openstack/network/v2/rbac_policy.py @@ -29,7 +29,7 @@ class RBACPolicy(resource.Resource): _query_mapping = resource.QueryParameters( 'action', 'object_id', 'object_type', 'project_id', - 'target_project_id', + 'target_project_id', target_project_id='target_tenant', ) # Properties diff --git a/openstack/tests/unit/network/v2/test_rbac_policy.py b/openstack/tests/unit/network/v2/test_rbac_policy.py index 6b5464b6c..fa8aadae0 100644 --- a/openstack/tests/unit/network/v2/test_rbac_policy.py +++ b/openstack/tests/unit/network/v2/test_rbac_policy.py @@ -37,6 +37,18 @@ def test_basic(self): self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) + self.assertDictEqual( + { + 'action': 'action', + 'object_id': 'object_id', + 'object_type': 'object_type', + 'project_id': 'project_id', + 'target_project_id': 'target_tenant', + 'limit': 'limit', + 'marker': 'marker', + }, + sot._query_mapping._mapping) + def test_make_it(self): sot = rbac_policy.RBACPolicy(**EXAMPLE) self.assertEqual(EXAMPLE['action'], sot.action) From ba96fe850787edff38c053b8d1c2dfde401bb81c Mon Sep 17 00:00:00 2001 From: "Dr. Jens Harbott" Date: Fri, 20 May 2022 15:42:43 +0200 Subject: [PATCH 3036/3836] Change title for "unreleased" renos The title "Unreleased Versions" is confusing when these versions are actually already released and active on pypi. The new title matches what e.g. neutron-lib is using. Change-Id: Ic75dd721bb4e1f3e176a2d5b239f4e519fec694e --- releasenotes/source/unreleased.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/releasenotes/source/unreleased.rst b/releasenotes/source/unreleased.rst index 9826c05b8..875030f9d 100644 --- a/releasenotes/source/unreleased.rst +++ b/releasenotes/source/unreleased.rst @@ -1,5 +1,5 @@ -===================== - Unreleased Versions -===================== +============================ +Current Series Release Notes +============================ .. release-notes:: From f088362797b93951c5b583d14678d9cf077bbc9c Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Mon, 23 May 2022 14:20:03 +0200 Subject: [PATCH 3037/3836] Do not log to stdout by default Stdout is for program output, not for logging. By sending log messages to stdout, openstacksdk breaks CLI output. Change-Id: I3a613c44230d9c6ac39980ce44196acec14fa31d --- openstack/_log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/_log.py b/openstack/_log.py index 13529204e..6c298414a 100644 --- a/openstack/_log.py +++ b/openstack/_log.py @@ -81,7 +81,7 @@ def enable_logging( :rtype: None """ if not stream and not path: - stream = sys.stdout + stream = sys.stderr if http_debug: debug = True From 8d33f1191e34bc4f7eec1643134d7793ac333406 Mon Sep 17 00:00:00 2001 From: "Dr. Jens Harbott" Date: Tue, 24 May 2022 11:58:37 +0200 Subject: [PATCH 3038/3836] Fix python-dev reference in bindep Python2 is a thing of the past, we need python3 now. This should get the python3.10 job running. Change-Id: I0b2deccfbc5cbbffd361106b96f4996172d1ee3c --- bindep.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindep.txt b/bindep.txt index 104a48089..aeb48323a 100644 --- a/bindep.txt +++ b/bindep.txt @@ -2,7 +2,7 @@ # see http://docs.openstack.org/infra/bindep/ for additional information. build-essential [platform:dpkg] -python-dev [platform:dpkg] +python3-dev [platform:dpkg] python-devel [platform:rpm] libffi-dev [platform:dpkg] libffi-devel [platform:rpm] From 041a66a2e775d6a28b0e88ca1345b79f5bdde863 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 18 May 2022 23:07:13 +0100 Subject: [PATCH 3039/3836] Remove unused normalization helpers These should have been removed in the following changes: - Ic856855f8040f88e618926bf0fdf14b0daa589d9 ("Switch identity.service in cloud layer to proxy") - Iae7c798df8780d8aa70ed54dc4b9eebd9c52b901 ("Switch identity groups and users in cloud layer") - I23e0ce374c5f854f9ff797616b02eb31f4593a7a ("Switch identity.domains in cloud layer") - Ia6c24590d6f12258be6c27fcf45c944a771083fc ("Get rid of normalization in identity CL") - I987161bfdedf837298dfa56173bfdd07d066cbbf ("Get rid of normalization in compute CL") Remove them now. Change-Id: I5f2f287e44771e29f3658bb2da141aa45cecf4d6 Signed-off-by: Stephen Finucane --- openstack/cloud/_utils.py | 130 -------------------------------------- 1 file changed, 130 deletions(-) diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index dc2be40a8..259d08bb0 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -22,13 +22,11 @@ from decorator import decorator import jmespath -import munch import netifaces import sre_constants from openstack import _log from openstack.cloud import exc -from openstack.cloud import meta _decorated_methods = [] @@ -198,31 +196,6 @@ def _get_entity(cloud, resource, name_or_id, filters, **kwargs): return None -def normalize_keystone_services(services): - """Normalize the structure of keystone services - - In keystone v2, there is a field called "service_type". In v3, it's - "type". Just make the returned dict have both. - - :param list services: A list of keystone service dicts - - :returns: A list of normalized dicts. - """ - ret = [] - for service in services: - service_type = service.get('type', service.get('service_type')) - new_service = { - 'id': service['id'], - 'name': service['name'], - 'description': service.get('description', None), - 'type': service_type, - 'service_type': service_type, - 'enabled': service['enabled'] - } - ret.append(new_service) - return meta.obj_list_to_munch(ret) - - def localhost_supports_ipv6(): """Determine whether the local host supports IPv6 @@ -237,109 +210,6 @@ def localhost_supports_ipv6(): return False -def normalize_users(users): - ret = [ - dict( - id=user.get('id'), - email=user.get('email'), - name=user.get('name'), - username=user.get('username'), - default_project_id=user.get('default_project_id', - user.get('tenantId')), - domain_id=user.get('domain_id'), - enabled=user.get('enabled'), - description=user.get('description') - ) for user in users - ] - return meta.obj_list_to_munch(ret) - - -def normalize_domains(domains): - ret = [ - dict( - id=domain.get('id'), - name=domain.get('name'), - description=domain.get('description'), - enabled=domain.get('enabled'), - ) for domain in domains - ] - return meta.obj_list_to_munch(ret) - - -def normalize_groups(domains): - """Normalize Identity groups.""" - ret = [ - dict( - id=domain.get('id'), - name=domain.get('name'), - description=domain.get('description'), - domain_id=domain.get('domain_id'), - ) for domain in domains - ] - return meta.obj_list_to_munch(ret) - - -def normalize_role_assignments(assignments): - """Put role_assignments into a form that works with search/get interface. - - Role assignments have the structure:: - - [ - { - "role": { - "id": "--role-id--" - }, - "scope": { - "domain": { - "id": "--domain-id--" - } - }, - "user": { - "id": "--user-id--" - } - }, - ] - - Which is hard to work with in the rest of our interface. Map this to be:: - - [ - { - "id": "--role-id--", - "domain": "--domain-id--", - "user": "--user-id--", - } - ] - - Scope can be "domain" or "project" and "user" can also be "group". - - :param list assignments: A list of dictionaries of role assignments. - - :returns: A list of flattened/normalized role assignment dicts. - """ - new_assignments = [] - for assignment in assignments: - new_val = munch.Munch({'id': assignment['role']['id']}) - for scope in ('project', 'domain'): - if scope in assignment['scope']: - new_val[scope] = assignment['scope'][scope]['id'] - for assignee in ('user', 'group'): - if assignment[assignee]: - new_val[assignee] = assignment[assignee]['id'] - new_assignments.append(new_val) - return new_assignments - - -def normalize_flavor_accesses(flavor_accesses): - """Normalize Flavor access list.""" - return [munch.Munch( - dict( - flavor_id=acl.get('flavor_id'), - project_id=acl.get('project_id') or acl.get('tenant_id'), - ) - ) for acl in flavor_accesses - ] - - def valid_kwargs(*valid_args): # This decorator checks if argument passed as **kwargs to a function are # present in valid_args. From 88b35ef50f8f747d26bfce623ae06dba230e7be2 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 18 May 2022 18:28:19 +0100 Subject: [PATCH 3040/3836] cloud: Update docstrings for network functions We don't use munch objects here any more. While we're here, we also fix a bug in the 'get_internal_networks' and 'get_external_networks' helpers: subclasses of 'Resource' including the 'Network' resource are not hashable so you can't put them in sets. We must use lists and assume there are no duplicates. Change-Id: I8cc7c5d83781698465c44d9a8137b76954de7f1d Signed-off-by: Stephen Finucane --- openstack/cloud/_network.py | 549 ++++++++++++++++++----------- openstack/cloud/_network_common.py | 60 ++-- 2 files changed, 363 insertions(+), 246 deletions(-) diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index a26016a2e..8e7cf9f69 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -39,15 +39,15 @@ def _neutron_extensions(self): def _has_neutron_extension(self, extension_alias): return extension_alias in self._neutron_extensions() + # TODO(stephenfin): Deprecate this in favour of the 'list' function def search_networks(self, name_or_id=None, filters=None): """Search networks :param name_or_id: Name or ID of the desired network. - :param filters: a dict containing additional filters to use. e.g. + :param filters: A dict containing additional filters to use. e.g. {'router:external': True} - - :returns: a list of ``munch.Munch`` containing the network description. - + :returns: A list of network ``Network`` objects matching the search + criteria. :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ @@ -58,15 +58,15 @@ def search_networks(self, name_or_id=None, filters=None): query.update(filters) return list(self.network.networks(**query)) + # TODO(stephenfin): Deprecate this in favour of the 'list' function def search_routers(self, name_or_id=None, filters=None): """Search routers :param name_or_id: Name or ID of the desired router. - :param filters: a dict containing additional filters to use. e.g. + :param filters: A dict containing additional filters to use. e.g. {'admin_state_up': True} - - :returns: a list of ``munch.Munch`` containing the router description. - + :returns: A list of network ``Router`` objects matching the search + criteria. :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ @@ -77,15 +77,15 @@ def search_routers(self, name_or_id=None, filters=None): query.update(filters) return list(self.network.routers(**query)) + # TODO(stephenfin): Deprecate this in favour of the 'list' function def search_subnets(self, name_or_id=None, filters=None): """Search subnets :param name_or_id: Name or ID of the desired subnet. - :param filters: a dict containing additional filters to use. e.g. + :param filters: A dict containing additional filters to use. e.g. {'enable_dhcp': True} - - :returns: a list of ``munch.Munch`` containing the subnet description. - + :returns: A list of network ``Subnet`` objects matching the search + criteria. :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ @@ -96,15 +96,15 @@ def search_subnets(self, name_or_id=None, filters=None): query.update(filters) return list(self.network.subnets(**query)) + # TODO(stephenfin): Deprecate this in favour of the 'list' function def search_ports(self, name_or_id=None, filters=None): """Search ports :param name_or_id: Name or ID of the desired port. - :param filters: a dict containing additional filters to use. e.g. + :param filters: A dict containing additional filters to use. e.g. {'device_id': '2711c67a-b4a7-43dd-ace7-6187b791c3f0'} - - :returns: a list of ``munch.Munch`` containing the port description. - + :returns: A list of network ``Port`` objects matching the search + criteria. :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ @@ -121,9 +121,8 @@ def search_ports(self, name_or_id=None, filters=None): def list_networks(self, filters=None): """List all available networks. - :param filters: (optional) dict of filter conditions to push down - :returns: A list of ``munch.Munch`` containing network info. - + :param filters: (optional) A dict of filter conditions to push down. + :returns: A list of network ``Network`` objects. """ # If the cloud is running nova-network, just return an empty list. if not self.has_service('network'): @@ -137,9 +136,8 @@ def list_networks(self, filters=None): def list_routers(self, filters=None): """List all available routers. - :param filters: (optional) dict of filter conditions to push down - :returns: A list of router ``munch.Munch``. - + :param filters: (optional) A dict of filter conditions to push down + :returns: A list of network ``Router`` objects. """ # If the cloud is running nova-network, just return an empty list. if not self.has_service('network'): @@ -153,9 +151,8 @@ def list_routers(self, filters=None): def list_subnets(self, filters=None): """List all available subnets. - :param filters: (optional) dict of filter conditions to push down - :returns: A list of subnet ``munch.Munch``. - + :param filters: (optional) A dict of filter conditions to push down + :returns: A list of network ``Subnet`` objects. """ # If the cloud is running nova-network, just return an empty list. if not self.has_service('network'): @@ -169,9 +166,8 @@ def list_subnets(self, filters=None): def list_ports(self, filters=None): """List all available ports. - :param filters: (optional) dict of filter conditions to push down - :returns: A list of port ``munch.Munch``. - + :param filters: (optional) A dict of filter conditions to push down + :returns: A list of network ``Port`` objects. """ # If pushdown filters are specified and we do not have batched caching # enabled, bypass local caching and push down the filters. @@ -208,13 +204,14 @@ def _list_ports(self, filters): filters = {} return list(self.network.ports(**filters)) + # TODO(stephenfin): Deprecate 'filters'; users should use 'list' for this def get_qos_policy(self, name_or_id, filters=None): """Get a QoS policy by name or ID. :param name_or_id: Name or ID of the policy. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: + :param filters: A dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. + Example:: { 'last_name': 'Smith', @@ -227,9 +224,7 @@ def get_qos_policy(self, name_or_id, filters=None): A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - :returns: A policy ``munch.Munch`` or None if no matching network is - found. - + :returns: A network ``QoSPolicy`` object if found, else None. """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( @@ -242,15 +237,15 @@ def get_qos_policy(self, name_or_id, filters=None): ignore_missing=True, **filters) + # TODO(stephenfin): Deprecate this in favour of the 'list' function def search_qos_policies(self, name_or_id=None, filters=None): """Search QoS policies :param name_or_id: Name or ID of the desired policy. :param filters: a dict containing additional filters to use. e.g. {'shared': True} - - :returns: a list of ``munch.Munch`` containing the network description. - + :returns: A list of network ``QosPolicy`` objects matching the search + criteria. :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ @@ -268,9 +263,8 @@ def search_qos_policies(self, name_or_id=None, filters=None): def list_qos_rule_types(self, filters=None): """List all available QoS rule types. - :param filters: (optional) dict of filter conditions to push down - :returns: A list of rule types ``munch.Munch``. - + :param filters: (optional) A dict of filter conditions to push down + :returns: A list of network ``QosRuleType`` objects. """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( @@ -281,14 +275,27 @@ def list_qos_rule_types(self, filters=None): filters = {} return list(self.network.qos_rule_types(**filters)) + # TODO(stephenfin): Deprecate 'filters'; users should use 'list' for this def get_qos_rule_type_details(self, rule_type, filters=None): """Get a QoS rule type details by rule type name. - :param string rule_type: Name of the QoS rule type. + :param rule_type: Name of the QoS rule type. + :param filters: A dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. + Example:: - :returns: A rule type details ``munch.Munch`` or None if - no matching rule type is found. + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: A network ``QoSRuleType`` object if found, else None. """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( @@ -304,9 +311,8 @@ def get_qos_rule_type_details(self, rule_type, filters=None): def list_qos_policies(self, filters=None): """List all available QoS policies. - :param filters: (optional) dict of filter conditions to push down - :returns: A list of policies ``munch.Munch``. - + :param filters: (optional) A dict of filter conditions to push down + :returns: A list of network ``QosPolicy`` objects. """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( @@ -316,13 +322,14 @@ def list_qos_policies(self, filters=None): filters = {} return list(self.network.qos_policies(**filters)) + # TODO(stephenfin): Deprecate 'filters'; users should use 'list' for this def get_network(self, name_or_id, filters=None): """Get a network by name or ID. :param name_or_id: Name or ID of the network. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: + :param filters: A dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. + Example:: { 'last_name': 'Smith', @@ -335,9 +342,7 @@ def get_network(self, name_or_id, filters=None): A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - :returns: A network ``munch.Munch`` or None if no matching network is - found. - + :returns: A network ``Network`` object if found, else None. """ if not filters: filters = {} @@ -347,20 +352,21 @@ def get_network(self, name_or_id, filters=None): **filters) def get_network_by_id(self, id): - """ Get a network by ID + """Get a network by ID :param id: ID of the network. - :returns: A network ``munch.Munch``. + :returns: A network ``Network`` object if found, else None. """ return self.network.get_network(id) + # TODO(stephenfin): Deprecate 'filters'; users should use 'list' for this def get_router(self, name_or_id, filters=None): """Get a router by name or ID. :param name_or_id: Name or ID of the router. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: + :param filters: A dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. + Example:: { 'last_name': 'Smith', @@ -373,9 +379,7 @@ def get_router(self, name_or_id, filters=None): A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - :returns: A router ``munch.Munch`` or None if no matching router is - found. - + :returns: A network ``Router`` object if found, else None. """ if not filters: filters = {} @@ -384,13 +388,14 @@ def get_router(self, name_or_id, filters=None): ignore_missing=True, **filters) + # TODO(stephenfin): Deprecate 'filters'; users should use 'list' for this def get_subnet(self, name_or_id, filters=None): """Get a subnet by name or ID. :param name_or_id: Name or ID of the subnet. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: + :param filters: A dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. + Example:: { 'last_name': 'Smith', @@ -399,9 +404,7 @@ def get_subnet(self, name_or_id, filters=None): } } - :returns: A subnet ``munch.Munch`` or None if no matching subnet is - found. - + :returns: A network ``Subnet`` object if found, else None. """ if not filters: filters = {} @@ -411,20 +414,21 @@ def get_subnet(self, name_or_id, filters=None): **filters) def get_subnet_by_id(self, id): - """ Get a subnet by ID + """Get a subnet by ID :param id: ID of the subnet. - :returns: A subnet ``munch.Munch``. + :returns: A network ``Subnet`` object if found, else None. """ return self.network.get_subnet(id) + # TODO(stephenfin): Deprecate 'filters'; users should use 'list' for this def get_port(self, name_or_id, filters=None): """Get a port by name or ID. :param name_or_id: Name or ID of the port. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: + :param filters: A dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. + Example:: { 'last_name': 'Smith', @@ -437,8 +441,7 @@ def get_port(self, name_or_id, filters=None): A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - :returns: A port ``munch.Munch`` or None if no matching port is found. - + :returns: A network ``Port`` object if found, else None. """ if not filters: filters = {} @@ -448,18 +451,26 @@ def get_port(self, name_or_id, filters=None): **filters) def get_port_by_id(self, id): - """ Get a port by ID + """Get a port by ID :param id: ID of the port. - :returns: A port ``munch.Munch``. + :returns: A network ``Port`` object if found, else None. """ return self.network.get_port(id) - def create_network(self, name, shared=False, admin_state_up=True, - external=False, provider=None, project_id=None, - availability_zone_hints=None, - port_security_enabled=None, - mtu_size=None, dns_domain=None): + def create_network( + self, + name, + shared=False, + admin_state_up=True, + external=False, + provider=None, + project_id=None, + availability_zone_hints=None, + port_security_enabled=None, + mtu_size=None, + dns_domain=None, + ): """Create a network. :param string name: Name of the network being created. @@ -479,8 +490,7 @@ def create_network(self, name, shared=False, admin_state_up=True, fragmentation. Minimum value is 68 for IPv4, and 1280 for IPv6. :param string dns_domain: Specify the DNS domain associated with this network. - - :returns: The network object. + :returns: The created network ``Network`` object. :raises: OpenStackCloudException on operation error. """ network = { @@ -566,8 +576,7 @@ def update_network(self, name_or_id, **kwargs): :param bool port_security_enabled: Enable or disable port security. :param string dns_domain: Specify the DNS domain associated with this network. - - :returns: The updated network object. + :returns: The updated network ``Network`` object. :raises: OpenStackCloudException on operation error. """ provider = kwargs.pop('provider', None) @@ -630,7 +639,7 @@ def delete_network(self, name_or_id): return True def set_network_quotas(self, name_or_id, **kwargs): - """ Set a network quota in a project + """Set a network quota in a project :param name_or_id: project name or id :param kwargs: key/value pairs of quota name and quota value @@ -646,14 +655,13 @@ def set_network_quotas(self, name_or_id, **kwargs): self.network.update_quota(proj.id, **kwargs) def get_network_quotas(self, name_or_id, details=False): - """ Get network quotas for a project + """Get network quotas for a project :param name_or_id: project name or id :param details: if set to True it will return details about usage of quotas by given project :raises: OpenStackCloudException if it's not a valid project - - :returns: Munch object with the quotas + :returns: A network ``Quota`` object if found, else None. """ proj = self.get_project(name_or_id) if not proj: @@ -663,12 +671,12 @@ def get_network_quotas(self, name_or_id, details=False): def get_network_extensions(self): """Get Cloud provided network extensions - :returns: set of Neutron extension aliases + :returns: A set of Neutron extension aliases. """ return self._neutron_extensions() def delete_network_quotas(self, name_or_id): - """ Delete network quotas for a project + """Delete network quotas for a project :param name_or_id: project name or id :raises: OpenStackCloudException if it's not a valid project or the @@ -708,22 +716,35 @@ def create_firewall_rule(self, **kwargs): :param source_firewall_group_id: ID of source firewall group. :param source_ip_address: IPv4-, IPv6 address or CIDR. :param source_port: Port or port range (e.g. 80:90) - :raises: BadRequestException if parameters are malformed - :return: created firewall rule - :rtype: FirewallRule + :returns: The created network ``FirewallRule`` object. """ return self.network.create_firewall_rule(**kwargs) def delete_firewall_rule(self, name_or_id, filters=None): """ Deletes firewall rule. + Prints debug message in case to-be-deleted resource was not found. :param name_or_id: firewall rule name or id - :param dict filters: optional filters + :param filters: A dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. + Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :raises: DuplicateResource on multiple matches - :return: True if resource is successfully deleted, False otherwise. + :returns: True if resource is successfully deleted, False otherwise. :rtype: bool """ if not filters: @@ -739,15 +760,29 @@ def delete_firewall_rule(self, name_or_id, filters=None): return False return True + # TODO(stephenfin): Deprecate 'filters'; users should use 'list' for this def get_firewall_rule(self, name_or_id, filters=None): """ Retrieves a single firewall rule. :param name_or_id: firewall rule name or id - :param dict filters: optional filters + :param filters: A dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. + Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :raises: DuplicateResource on multiple matches - :return: firewall rule dict or None if not found - :rtype: FirewallRule + :returns: A network ``FirewallRule`` object if found, else None. """ if not filters: filters = {} @@ -760,8 +795,22 @@ def list_firewall_rules(self, filters=None): """ Lists firewall rules. - :param dict filters: optional filters - :return: list of firewall rules + :param filters: A dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. + Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: A list of network ``FirewallRule`` objects. :rtype: list[FirewallRule] """ if not filters: @@ -778,13 +827,26 @@ def update_firewall_rule(self, name_or_id, filters=None, **kwargs): Updates firewall rule. :param name_or_id: firewall rule name or id - :param dict filters: optional filters + :param filters: A dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. + Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :param kwargs: firewall rule update parameters. See create_firewall_rule docstring for valid parameters. + :returns: The updated network ``FirewallRule`` object. :raises: BadRequestException if parameters are malformed :raises: NotFoundException if resource is not found - :return: updated firewall rule - :rtype: FirewallRule """ if not filters: filters = {} @@ -832,8 +894,7 @@ def create_firewall_policy(self, **kwargs): Defaults to False. :raises: BadRequestException if parameters are malformed :raises: ResourceNotFound if a resource from firewall_list not found - :return: created firewall policy - :rtype: FirewallPolicy + :returns: The created network ``FirewallPolicy`` object. """ if 'firewall_rules' in kwargs: kwargs['firewall_rules'] = self._get_firewall_rule_ids( @@ -847,9 +908,23 @@ def delete_firewall_policy(self, name_or_id, filters=None): Prints debug message in case to-be-deleted resource was not found. :param name_or_id: firewall policy name or id - :param dict filters: optional filters + :param filters: A dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. + Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :raises: DuplicateResource on multiple matches - :return: True if resource is successfully deleted, False otherwise. + :returns: True if resource is successfully deleted, False otherwise. :rtype: bool """ if not filters: @@ -865,15 +940,29 @@ def delete_firewall_policy(self, name_or_id, filters=None): return False return True + # TODO(stephenfin): Deprecate 'filters'; users should use 'list' for this def get_firewall_policy(self, name_or_id, filters=None): """ Retrieves a single firewall policy. :param name_or_id: firewall policy name or id - :param dict filters: optional filters + :param filters: A dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. + Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :raises: DuplicateResource on multiple matches - :return: firewall policy or None if not found - :rtype: FirewallPolicy + :returns: A network ``FirewallPolicy`` object if found, else None. """ if not filters: filters = {} @@ -886,8 +975,22 @@ def list_firewall_policies(self, filters=None): """ Lists firewall policies. - :param dict filters: optional filters - :return: list of firewall policies + :param filters: A dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. + Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: A list of network ``FirewallPolicy`` objects. :rtype: list[FirewallPolicy] """ if not filters: @@ -901,14 +1004,27 @@ def update_firewall_policy(self, name_or_id, filters=None, **kwargs): Updates firewall policy. :param name_or_id: firewall policy name or id - :param dict filters: optional filters + :param filters: A dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. + Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :param kwargs: firewall policy update parameters See create_firewall_policy docstring for valid parameters. + :returns: The updated network ``FirewallPolicy`` object. :raises: BadRequestException if parameters are malformed :raises: DuplicateResource on multiple matches :raises: ResourceNotFound if resource is not found - :return: updated firewall policy - :rtype: FirewallPolicy """ if not filters: filters = {} @@ -924,7 +1040,8 @@ def update_firewall_policy(self, name_or_id, filters=None, **kwargs): def insert_rule_into_policy(self, name_or_id, rule_name_or_id, insert_after=None, insert_before=None, filters=None): - """ + """Add firewall rule to a policy. + Adds firewall rule to the firewall_rules list of a firewall policy. Short-circuits and returns the firewall policy early if the firewall rule id is already present in the firewall_rules list. @@ -1033,8 +1150,7 @@ def create_firewall_group(self, **kwargs): :raises: DuplicateResource on multiple matches :raises: ResourceNotFound if (ingress-, egress-) firewall policy or a port is not found. - :return: created firewall group - :rtype: FirewallGroup + :returns: The created network ``FirewallGroup`` object. """ self._lookup_ingress_egress_firewall_policy_ids(kwargs) if 'ports' in kwargs: @@ -1049,7 +1165,7 @@ def delete_firewall_group(self, name_or_id, filters=None): :param name_or_id: firewall group name or id :param dict filters: optional filters :raises: DuplicateResource on multiple matches - :return: True if resource is successfully deleted, False otherwise. + :returns: True if resource is successfully deleted, False otherwise. :rtype: bool """ if not filters: @@ -1065,6 +1181,7 @@ def delete_firewall_group(self, name_or_id, filters=None): return False return True + # TODO(stephenfin): Deprecate 'filters'; users should use 'list' for this def get_firewall_group(self, name_or_id, filters=None): """ Retrieves firewall group. @@ -1072,8 +1189,7 @@ def get_firewall_group(self, name_or_id, filters=None): :param name_or_id: firewall group name or id :param dict filters: optional filters :raises: DuplicateResource on multiple matches - :return: firewall group or None if not found - :rtype: FirewallGroup + :returns: A network ``FirewallGroup`` object if found, else None. """ if not filters: filters = {} @@ -1086,9 +1202,7 @@ def list_firewall_groups(self, filters=None): """ Lists firewall groups. - :param dict filters: optional filters - :return: list of firewall groups - :rtype: list[FirewallGroup] + :returns: A list of network ``FirewallGroup`` objects. """ if not filters: filters = {} @@ -1109,12 +1223,11 @@ def update_firewall_group(self, name_or_id, filters=None, **kwargs): :param dict filters: optional filters :param kwargs: firewall group update parameters See create_firewall_group docstring for valid parameters. + :returns: The updated network ``FirewallGroup`` object. :raises: BadRequestException if parameters are malformed :raises: DuplicateResource on multiple matches :raises: ResourceNotFound if firewall group, a firewall policy (egress, ingress) or port is not found - :return: updated firewall group - :rtype: FirewallGroup """ if not filters: filters = {} @@ -1160,8 +1273,7 @@ def create_qos_policy(self, **kwargs): :param bool default: Set the QoS policy as default for project. :param string project_id: Specify the project ID this QoS policy will be created on (admin-only). - - :returns: The QoS policy object. + :returns: The created network ``QosPolicy`` object. :raises: OpenStackCloudException on operation error. """ if not self._has_neutron_extension('qos'): @@ -1189,8 +1301,7 @@ def update_qos_policy(self, name_or_id, **kwargs): :param bool shared: If True, the QoS policy will be set as shared. :param bool default: If True, the QoS policy will be set as default for project. - - :returns: The updated QoS policy object. + :returns: The updated network ``QosPolicyRule`` object. :raises: OpenStackCloudException on operation error. """ if not self._has_neutron_extension('qos'): @@ -1237,8 +1348,12 @@ def delete_qos_policy(self, name_or_id): return True + # TODO(stephenfin): Deprecate this in favour of the 'list' function def search_qos_bandwidth_limit_rules( - self, policy_name_or_id, rule_id=None, filters=None + self, + policy_name_or_id, + rule_id=None, + filters=None, ): """Search QoS bandwidth limit rules @@ -1247,10 +1362,8 @@ def search_qos_bandwidth_limit_rules( :param string rule_id: ID of searched rule. :param filters: a dict containing additional filters to use. e.g. {'max_kbps': 1000} - - :returns: a list of ``munch.Munch`` containing the bandwidth limit - rule descriptions. - + :returns: A list of network ``QoSBandwidthLimitRule`` objects matching + the search criteria. :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ @@ -1262,9 +1375,8 @@ def list_qos_bandwidth_limit_rules(self, policy_name_or_id, filters=None): :param string policy_name_or_id: Name or ID of the QoS policy from from rules should be listed. - :param filters: (optional) dict of filter conditions to push down - :returns: A list of ``munch.Munch`` containing rule info. - + :param filters: (optional) A dict of filter conditions to push down + :returns: A list of network ``QoSBandwidthLimitRule`` objects. :raises: ``OpenStackCloudResourceNotFound`` if QoS policy will not be found. """ @@ -1291,10 +1403,8 @@ def get_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): :param string policy_name_or_id: Name or ID of the QoS policy to which rule should be associated. :param rule_id: ID of the rule. - - :returns: A bandwidth limit rule ``munch.Munch`` or None if - no matching rule is found. - + :returns: A network ``QoSBandwidthLimitRule`` object if found, else + None. """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( @@ -1310,8 +1420,12 @@ def get_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): rule_id, policy) @_utils.valid_kwargs("max_burst_kbps", "direction") - def create_qos_bandwidth_limit_rule(self, policy_name_or_id, max_kbps, - **kwargs): + def create_qos_bandwidth_limit_rule( + self, + policy_name_or_id, + max_kbps, + **kwargs, + ): """Create a QoS bandwidth limit rule. :param string policy_name_or_id: Name or ID of the QoS policy to which @@ -1321,8 +1435,7 @@ def create_qos_bandwidth_limit_rule(self, policy_name_or_id, max_kbps, :param int max_burst_kbps: Maximum burst value (in kilobits). :param string direction: Ingress or egress. The direction in which the traffic will be limited. - - :returns: The QoS bandwidth limit rule. + :returns: The created network ``QoSBandwidthLimitRule`` object. :raises: OpenStackCloudException on operation error. """ if not self._has_neutron_extension('qos'): @@ -1359,8 +1472,7 @@ def update_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id, :param int max_burst_kbps: Maximum burst value (in kilobits). :param string direction: Ingress or egress. The direction in which the traffic will be limited. - - :returns: The updated QoS bandwidth limit rule. + :returns: The updated network ``QoSBandwidthLimitRule`` object. :raises: OpenStackCloudException on operation error. """ if not self._has_neutron_extension('qos'): @@ -1428,8 +1540,13 @@ def delete_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): return True - def search_qos_dscp_marking_rules(self, policy_name_or_id, rule_id=None, - filters=None): + # TODO(stephenfin): Deprecate this in favour of the 'list' function + def search_qos_dscp_marking_rules( + self, + policy_name_or_id, + rule_id=None, + filters=None, + ): """Search QoS DSCP marking rules :param string policy_name_or_id: Name or ID of the QoS policy to which @@ -1437,10 +1554,8 @@ def search_qos_dscp_marking_rules(self, policy_name_or_id, rule_id=None, :param string rule_id: ID of searched rule. :param filters: a dict containing additional filters to use. e.g. {'dscp_mark': 32} - - :returns: a list of ``munch.Munch`` containing the dscp marking - rule descriptions. - + :returns: A list of network ``QoSDSCPMarkingRule`` objects matching the + search criteria. :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ @@ -1452,9 +1567,8 @@ def list_qos_dscp_marking_rules(self, policy_name_or_id, filters=None): :param string policy_name_or_id: Name or ID of the QoS policy from from rules should be listed. - :param filters: (optional) dict of filter conditions to push down - :returns: A list of ``munch.Munch`` containing rule info. - + :param filters: (optional) A dict of filter conditions to push down + :returns: A list of network ``QoSDSCPMarkingRule`` objects. :raises: ``OpenStackCloudResourceNotFound`` if QoS policy will not be found. """ @@ -1481,10 +1595,7 @@ def get_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): :param string policy_name_or_id: Name or ID of the QoS policy to which rule should be associated. :param rule_id: ID of the rule. - - :returns: A bandwidth limit rule ``munch.Munch`` or None if - no matching rule is found. - + :returns: A network ``QoSDSCPMarkingRule`` object if found, else None. """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( @@ -1498,14 +1609,17 @@ def get_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): return self.network.get_qos_dscp_marking_rule(rule_id, policy) - def create_qos_dscp_marking_rule(self, policy_name_or_id, dscp_mark): + def create_qos_dscp_marking_rule( + self, + policy_name_or_id, + dscp_mark, + ): """Create a QoS DSCP marking rule. :param string policy_name_or_id: Name or ID of the QoS policy to which rule should be associated. :param int dscp_mark: DSCP mark value - - :returns: The QoS DSCP marking rule. + :returns: The created network ``QoSDSCPMarkingRule`` object. :raises: OpenStackCloudException on operation error. """ if not self._has_neutron_extension('qos'): @@ -1530,8 +1644,7 @@ def update_qos_dscp_marking_rule(self, policy_name_or_id, rule_id, rule is associated. :param string rule_id: ID of rule to update. :param int dscp_mark: DSCP mark value - - :returns: The updated QoS bandwidth limit rule. + :returns: The updated network ``QoSDSCPMarkingRule`` object. :raises: OpenStackCloudException on operation error. """ if not self._has_neutron_extension('qos'): @@ -1590,8 +1703,13 @@ def delete_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): return True - def search_qos_minimum_bandwidth_rules(self, policy_name_or_id, - rule_id=None, filters=None): + # TODO(stephenfin): Deprecate this in favour of the 'list' function + def search_qos_minimum_bandwidth_rules( + self, + policy_name_or_id, + rule_id=None, + filters=None, + ): """Search QoS minimum bandwidth rules :param string policy_name_or_id: Name or ID of the QoS policy to which @@ -1599,10 +1717,8 @@ def search_qos_minimum_bandwidth_rules(self, policy_name_or_id, :param string rule_id: ID of searched rule. :param filters: a dict containing additional filters to use. e.g. {'min_kbps': 1000} - - :returns: a list of ``munch.Munch`` containing the bandwidth limit - rule descriptions. - + :returns: A list of network ``QoSMinimumBandwidthRule`` objects + matching the search criteria. :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ @@ -1616,9 +1732,8 @@ def list_qos_minimum_bandwidth_rules(self, policy_name_or_id, :param string policy_name_or_id: Name or ID of the QoS policy from from rules should be listed. - :param filters: (optional) dict of filter conditions to push down - :returns: A list of ``munch.Munch`` containing rule info. - + :param filters: (optional) A dict of filter conditions to push down + :returns: A list of network ``QoSMinimumBandwidthRule`` objects. :raises: ``OpenStackCloudResourceNotFound`` if QoS policy will not be found. """ @@ -1646,10 +1761,8 @@ def get_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): :param string policy_name_or_id: Name or ID of the QoS policy to which rule should be associated. :param rule_id: ID of the rule. - - :returns: A bandwidth limit rule ``munch.Munch`` or None if - no matching rule is found. - + :returns: A network ``QoSMinimumBandwidthRule`` object if found, else + None. """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( @@ -1665,7 +1778,10 @@ def get_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): @_utils.valid_kwargs("direction") def create_qos_minimum_bandwidth_rule( - self, policy_name_or_id, min_kbps, **kwargs + self, + policy_name_or_id, + min_kbps, + **kwargs, ): """Create a QoS minimum bandwidth limit rule. @@ -1674,8 +1790,7 @@ def create_qos_minimum_bandwidth_rule( :param int min_kbps: Minimum bandwidth value (in kilobits per second). :param string direction: Ingress or egress. The direction in which the traffic will be available. - - :returns: The QoS minimum bandwidth rule. + :returns: The created network ``QoSMinimumBandwidthRule`` object. :raises: OpenStackCloudException on operation error. """ if not self._has_neutron_extension('qos'): @@ -1704,8 +1819,7 @@ def update_qos_minimum_bandwidth_rule( :param int min_kbps: Minimum bandwidth value (in kilobits per second). :param string direction: Ingress or egress. The direction in which the traffic will be available. - - :returns: The updated QoS minimum bandwidth rule. + :returns: The updated network ``QoSMinimumBandwidthRule`` object. :raises: OpenStackCloudException on operation error. """ if not self._has_neutron_extension('qos'): @@ -1773,11 +1887,7 @@ def add_router_interface(self, router, subnet_id=None, port_id=None): :param dict router: The dict object of the router being changed :param string subnet_id: The ID of the subnet to use for the interface :param string port_id: The ID of the port to use for the interface - - :returns: A ``munch.Munch`` with the router ID (ID), - subnet ID (subnet_id), port ID (port_id) and tenant ID - (tenant_id). - + :returns: The raw response body from the request. :raises: OpenStackCloudException on operation error. """ return self.network.add_interface_to_router( @@ -1820,8 +1930,7 @@ def list_router_interfaces(self, router, interface_type=None): :param string interface_type: One of None, "internal", or "external". Controls whether all, internal interfaces or external interfaces are returned. - - :returns: A list of port ``munch.Munch`` objects. + :returns: A list of network ``Port`` objects. """ # Find only router interface and gateway ports, ignore L3 HA ports etc. router_interfaces = self.search_ports(filters={ @@ -1845,10 +1954,16 @@ def list_router_interfaces(self, router, interface_type=None): return router_gateways return ports - def create_router(self, name=None, admin_state_up=True, - ext_gateway_net_id=None, enable_snat=None, - ext_fixed_ips=None, project_id=None, - availability_zone_hints=None): + def create_router( + self, + name=None, + admin_state_up=True, + ext_gateway_net_id=None, + enable_snat=None, + ext_fixed_ips=None, + project_id=None, + availability_zone_hints=None, + ): """Create a logical router. :param string name: The router name. @@ -1869,8 +1984,7 @@ def create_router(self, name=None, admin_state_up=True, :param string project_id: Project ID for the router. :param types.ListType availability_zone_hints: A list of availability zone hints. - - :returns: The router object. + :returns: The created network ``Router`` object. :raises: OpenStackCloudException on operation error. """ router = { @@ -1932,7 +2046,7 @@ def update_router(self, name_or_id, name=None, admin_state_up=None, } ] - :returns: The router object. + :returns: The updated network ``Router`` object. :raises: OpenStackCloudException on operation error. """ router = {} @@ -1986,13 +2100,25 @@ def delete_router(self, name_or_id): return True - def create_subnet(self, network_name_or_id, cidr=None, ip_version=4, - enable_dhcp=False, subnet_name=None, tenant_id=None, - allocation_pools=None, - gateway_ip=None, disable_gateway_ip=False, - dns_nameservers=None, host_routes=None, - ipv6_ra_mode=None, ipv6_address_mode=None, - prefixlen=None, use_default_subnetpool=False, **kwargs): + def create_subnet( + self, + network_name_or_id, + cidr=None, + ip_version=4, + enable_dhcp=False, + subnet_name=None, + tenant_id=None, + allocation_pools=None, + gateway_ip=None, + disable_gateway_ip=False, + dns_nameservers=None, + host_routes=None, + ipv6_ra_mode=None, + ipv6_address_mode=None, + prefixlen=None, + use_default_subnetpool=False, + **kwargs, + ): """Create a subnet on a specified network. :param string network_name_or_id: The unique name or ID of the attached @@ -2050,8 +2176,7 @@ def create_subnet(self, network_name_or_id, cidr=None, ip_version=4, ``ip_version`` to obtain a CIDR. It is required to pass ``None`` to the ``cidr`` argument when enabling this option. :param kwargs: Key value pairs to be passed to the Neutron API. - - :returns: The new subnet object. + :returns: The created network ``Subnet`` object. :raises: OpenStackCloudException on operation error. """ @@ -2189,7 +2314,7 @@ def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, } ] - :returns: The updated subnet object. + :returns: The updated network ``Subnet`` object. :raises: OpenStackCloudException on operation error. """ subnet = {} @@ -2283,9 +2408,7 @@ def create_port(self, network_id, **kwargs): :param port_security_enabled: The security port state created on the network. (Optional) :param qos_policy_id: The ID of the QoS policy to apply for port. - - :returns: a ``munch.Munch`` describing the created port. - + :returns: The created network ``Port`` object. :raises: ``OpenStackCloudException`` on operation error. """ kwargs['network_id'] = network_id @@ -2350,9 +2473,7 @@ def update_port(self, name_or_id, **kwargs): :param port_security_enabled: The security port state created on the network. (Optional) :param qos_policy_id: The ID of the QoS policy to apply for port. - - :returns: a ``munch.Munch`` describing the updated port. - + :returns: The updated network ``Port`` object. :raises: OpenStackCloudException on operation error. """ port = self.get_port(name_or_id=name_or_id) diff --git a/openstack/cloud/_network_common.py b/openstack/cloud/_network_common.py index 0d0b3ca9f..ed6e4b969 100644 --- a/openstack/cloud/_network_common.py +++ b/openstack/cloud/_network_common.py @@ -258,8 +258,10 @@ def _find_interesting_networks(self): try: if self._network_list_stamp: return - if (not self._use_external_network - and not self._use_internal_network): + if ( + not self._use_external_network + and not self._use_internal_network + ): # Both have been flagged as skip - don't do a list return if not self.has_service('network'): @@ -269,18 +271,18 @@ def _find_interesting_networks(self): finally: self._networks_lock.release() - # def get_nat_destination(self): - # """Return the network that is configured to be the NAT destination. - # - # :returns: A network dict if one is found - # """ - # self._find_interesting_networks() - # return self._nat_destination_network + def get_nat_destination(self): + """Return the network that is configured to be the NAT destination. + + :returns: A network ``Network`` object if one is found + """ + self._find_interesting_networks() + return self._nat_destination_network def get_nat_source(self): """Return the network that is configured to be the NAT destination. - :returns: A network dict if one is found + :returns: A network ``Network`` object if one is found """ self._find_interesting_networks() return self._nat_source_network @@ -288,31 +290,24 @@ def get_nat_source(self): def get_default_network(self): """Return the network that is configured to be the default interface. - :returns: A network dict if one is found + :returns: A network ``Network`` object if one is found """ self._find_interesting_networks() return self._default_network_network - def get_nat_destination(self): - """Return the network that is configured to be the NAT destination. - - :returns: A network dict if one is found - """ - self._find_interesting_networks() - return self._nat_destination_network - def get_external_networks(self): """Return the networks that are configured to route northbound. This should be avoided in favor of the specific ipv4/ipv6 method, but is here for backwards compatibility. - :returns: A list of network ``munch.Munch`` if one is found + :returns: A list of network ``Network`` objects if any are found """ self._find_interesting_networks() - return list( - set(self._external_ipv4_networks) - | set(self._external_ipv6_networks)) + return ( + list(self._external_ipv4_networks) + + list(self._external_ipv6_networks) + ) def get_internal_networks(self): """Return the networks that are configured to not route northbound. @@ -320,17 +315,18 @@ def get_internal_networks(self): This should be avoided in favor of the specific ipv4/ipv6 method, but is here for backwards compatibility. - :returns: A list of network ``munch.Munch`` if one is found + :returns: A list of network ``Network`` objects if any are found """ self._find_interesting_networks() - return list( - set(self._internal_ipv4_networks) - | set(self._internal_ipv6_networks)) + return ( + list(self._internal_ipv4_networks) + + list(self._internal_ipv6_networks) + ) def get_external_ipv4_networks(self): """Return the networks that are configured to route northbound. - :returns: A list of network ``munch.Munch`` if one is found + :returns: A list of network ``Network`` objects if any are found """ self._find_interesting_networks() return self._external_ipv4_networks @@ -338,7 +334,7 @@ def get_external_ipv4_networks(self): def get_external_ipv4_floating_networks(self): """Return the networks that are configured to route northbound. - :returns: A list of network ``munch.Munch`` if one is found + :returns: A list of network ``Network`` objects if any are found """ self._find_interesting_networks() return self._external_ipv4_floating_networks @@ -346,7 +342,7 @@ def get_external_ipv4_floating_networks(self): def get_internal_ipv4_networks(self): """Return the networks that are configured to not route northbound. - :returns: A list of network ``munch.Munch`` if one is found + :returns: A list of network ``Network`` objects if any are found """ self._find_interesting_networks() return self._internal_ipv4_networks @@ -354,7 +350,7 @@ def get_internal_ipv4_networks(self): def get_external_ipv6_networks(self): """Return the networks that are configured to route northbound. - :returns: A list of network ``munch.Munch`` if one is found + :returns: A list of network ``Network`` objects if any are found """ self._find_interesting_networks() return self._external_ipv6_networks @@ -362,7 +358,7 @@ def get_external_ipv6_networks(self): def get_internal_ipv6_networks(self): """Return the networks that are configured to not route northbound. - :returns: A list of network ``munch.Munch`` if one is found + :returns: A list of network ``Network`` objects if any are found """ self._find_interesting_networks() return self._internal_ipv6_networks From a1372adc5dc96ef991c6aafd6d889de2e27035ce Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 18 May 2022 23:38:42 +0100 Subject: [PATCH 3041/3836] cloud: Update docstrings for accelerator functions Another example of docstrings lying. These don't return munch.Munch objects anymore. Change-Id: I348f1ee00efc7abc847c5cf9d58b2dceb606046d Signed-off-by: Stephen Finucane --- openstack/cloud/_accelerator.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openstack/cloud/_accelerator.py b/openstack/cloud/_accelerator.py index abc7d3d9e..8eb9769cc 100644 --- a/openstack/cloud/_accelerator.py +++ b/openstack/cloud/_accelerator.py @@ -21,7 +21,7 @@ def list_deployables(self, filters=None): """List all available deployables. :param filters: (optional) dict of filter conditions to push down - :returns: A list of deployable info. + :returns: A list of accelerator ``Deployable`` objects. """ # Translate None from search interface to empty {} for kwargs below if not filters: @@ -32,7 +32,7 @@ def list_devices(self, filters=None): """List all devices. :param filters: (optional) dict of filter conditions to push down - :returns: A list of device info. + :returns: A list of accelerator ``Device`` objects. """ # Translate None from search interface to empty {} for kwargs below if not filters: @@ -43,7 +43,7 @@ def list_device_profiles(self, filters=None): """List all device_profiles. :param filters: (optional) dict of filter conditions to push down - :returns: A list of device profile info. + :returns: A list of accelerator ``DeviceProfile`` objects. """ # Translate None from search interface to empty {} for kwargs below if not filters: @@ -54,7 +54,7 @@ def create_device_profile(self, attrs): """Create a device_profile. :param attrs: The info of device_profile to be created. - :returns: A ``munch.Munch`` of the created device_profile. + :returns: An accelerator ``DeviceProfile`` objects. """ return self.accelerator.create_device_profile(**attrs) @@ -85,7 +85,7 @@ def list_accelerator_requests(self, filters=None): """List all accelerator_requests. :param filters: (optional) dict of filter conditions to push down - :returns: A list of accelerator request info. + :returns: A list of accelerator ``AcceleratorRequest`` objects. """ # Translate None from search interface to empty {} for kwargs below if not filters: @@ -121,7 +121,7 @@ def create_accelerator_request(self, attrs): """Create an accelerator_request. :param attrs: The info of accelerator_request to be created. - :returns: A ``munch.Munch`` of the created accelerator_request. + :returns: An accelerator ``AcceleratorRequest`` object. """ return self.accelerator.create_accelerator_request(**attrs) From 6bae83af0d89186ceeb1315ae5e814bf3c0eda0e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 18 May 2022 23:44:46 +0100 Subject: [PATCH 3042/3836] cloud: Update docstrings for image functions Remove more mentions of munch. Change-Id: Id12765c9e7fdffe62c489c064a3fc07df6ee1d40 Signed-off-by: Stephen Finucane --- openstack/cloud/_image.py | 105 +++++++++++++++++++++----------------- 1 file changed, 59 insertions(+), 46 deletions(-) diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py index 16c8415a3..2ea8d92e7 100644 --- a/openstack/cloud/_image.py +++ b/openstack/cloud/_image.py @@ -86,9 +86,9 @@ def get_image(self, name_or_id, filters=None): """Get an image by name or ID. :param name_or_id: Name or ID of the image. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: + :param filters: A dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. + Example:: { 'last_name': 'Smith', @@ -101,9 +101,7 @@ def get_image(self, name_or_id, filters=None): A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - :returns: An image ``munch.Munch`` or None if no matching image - is found - + :returns: An image :class:`openstack.image.v2.image.Image` object. """ return _utils._get_entity(self, 'image', name_or_id, filters) @@ -111,14 +109,17 @@ def get_image_by_id(self, id): """ Get a image by ID :param id: ID of the image. - :returns: An image - :class:`openstack.image.v2.image.Image` object. + :returns: An image :class:`openstack.image.v2.image.Image` object. """ - return self.image.get_image(image={'id': id}) + return self.image.get_image(id) def download_image( - self, name_or_id, output_path=None, output_file=None, - chunk_size=1024): + self, + name_or_id, + output_path=None, + output_file=None, + chunk_size=1024, + ): """Download an image by name or ID :param str name_or_id: Name or ID of the image. @@ -129,7 +130,11 @@ def download_image( this or output_path must be specified :param int chunk_size: size in bytes to read from the wire and buffer at one time. Defaults to 1024 - + :returns: When output_path and output_file are not given - the bytes + comprising the given Image when stream is False, otherwise a + :class:`requests.Response` instance. When output_path or + output_file are given - an image + :class:`~openstack.image.v2.image.Image` instance. :raises: OpenStackCloudException in the event download_image is called without exactly one of either output_path or output_file :raises: OpenStackCloudResourceNotFound if no images are found matching @@ -189,7 +194,12 @@ def wait_for_image(self, image, timeout=3600): 'Image {image} hit error state'.format(image=image_id)) def delete_image( - self, name_or_id, wait=False, timeout=3600, delete_objects=True): + self, + name_or_id, + wait=False, + timeout=3600, + delete_objects=True, + ): """Delete an existing image. :param name_or_id: Name of the image to be deleted. @@ -227,53 +237,57 @@ def delete_image( return True def create_image( - self, name, filename=None, - container=None, - md5=None, sha256=None, - disk_format=None, container_format=None, - disable_vendor_agent=True, - wait=False, timeout=3600, tags=None, - allow_duplicates=False, meta=None, volume=None, **kwargs): + self, + name, + filename=None, + container=None, + md5=None, + sha256=None, + disk_format=None, + container_format=None, + disable_vendor_agent=True, + wait=False, + timeout=3600, + tags=None, + allow_duplicates=False, + meta=None, + volume=None, + **kwargs, + ): """Upload an image. :param str name: Name of the image to create. If it is a pathname - of an image, the name will be constructed from the - extensionless basename of the path. + of an image, the name will be constructed from the + extensionless basename of the path. :param str filename: The path to the file to upload, if needed. - (optional, defaults to None) + (optional, defaults to None) :param str container: Name of the container in swift where images - should be uploaded for import if the cloud - requires such a thing. (optiona, defaults to - 'images') + should be uploaded for import if the cloud requires such a thing. + (optiona, defaults to 'images') :param str md5: md5 sum of the image file. If not given, an md5 will - be calculated. + be calculated. :param str sha256: sha256 sum of the image file. If not given, an md5 - will be calculated. + will be calculated. :param str disk_format: The disk format the image is in. (optional, - defaults to the os-client-config config value - for this cloud) + defaults to the os-client-config config value for this cloud) :param str container_format: The container format the image is in. - (optional, defaults to the - os-client-config config value for this - cloud) + (optional, defaults to the os-client-config config value for this + cloud) :param list tags: List of tags for this image. Each tag is a string - of at most 255 chars. + of at most 255 chars. :param bool disable_vendor_agent: Whether or not to append metadata - flags to the image to inform the - cloud in question to not expect a - vendor agent to be runing. - (optional, defaults to True) + flags to the image to inform the cloud in question to not expect a + vendor agent to be runing. (optional, defaults to True) :param bool wait: If true, waits for image to be created. Defaults to - true - however, be aware that one of the upload - methods is always synchronous. + true - however, be aware that one of the upload methods is always + synchronous. :param timeout: Seconds to wait for image creation. None is forever. :param allow_duplicates: If true, skips checks that enforce unique - image name. (optional, defaults to False) + image name. (optional, defaults to False) :param meta: A dict of key/value pairs to use for metadata that - bypasses automatic type conversion. + bypasses automatic type conversion. :param volume: Name or ID or volume object of a volume to create an - image from. Mutually exclusive with (optional, defaults - to None) + image from. Mutually exclusive with (optional, defaults to None) Additional kwargs will be passed to the image creation as additional metadata for the image and will have all values converted to string @@ -286,8 +300,7 @@ def create_image( If a value is in meta and kwargs, meta wins. - :returns: A ``munch.Munch`` of the Image object - + :returns: An image :class:`openstack.image.v2.image.Image` object. :raises: OpenStackCloudException if there are problems uploading """ if volume: From 84b2ec36c97a04a2995b1a33764c8b28d763b4c9 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 26 May 2022 11:22:57 +0100 Subject: [PATCH 3043/3836] cloud: Update docstrings for block storage functions Change-Id: I1d011c9339d47675c32f66ec3f58f21693086daa Signed-off-by: Stephen Finucane --- openstack/cloud/_block_storage.py | 442 +++++++++++++++++++++--------- 1 file changed, 312 insertions(+), 130 deletions(-) diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index b8f1fb9ce..96c8ba43e 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -14,6 +14,7 @@ # We can't just use list, because sphinx gets confused by # openstack.resource.Resource.list and openstack.resource2.Resource.list import types # noqa +import warnings from openstack.block_storage.v3 import quota_set as _qs from openstack.cloud import _utils @@ -32,31 +33,46 @@ def _no_pending_volumes(volumes): class BlockStorageCloudMixin: + # TODO(stephenfin): Remove 'cache' in a future major version @_utils.cache_on_arguments(should_cache_fn=_no_pending_volumes) def list_volumes(self, cache=True): """List all available volumes. - :returns: A list of volume ``munch.Munch``. - + :param cache: **DEPRECATED** This parameter no longer does anything. + :returns: A list of volume ``Volume`` objects. """ + warnings.warn( + "the 'cache' argument is deprecated and no longer does anything; " + "consider removing it from calls", + DeprecationWarning, + ) return list(self.block_storage.volumes()) + # TODO(stephenfin): Remove 'get_extra' in a future major version @_utils.cache_on_arguments() - def list_volume_types(self, get_extra=True): + def list_volume_types(self, get_extra=None): """List all available volume types. - :returns: A list of volume ``munch.Munch``. - + :param get_extra: **DEPRECATED** This parameter no longer does + anything. + :returns: A list of volume ``Type`` objects. """ + if get_extra is not None: + warnings.warn( + "the 'get_extra' argument is deprecated and no longer does " + "anything; consider removing it from calls", + DeprecationWarning, + ) return list(self.block_storage.types()) + # TODO(stephenfin): Remove 'filters' in a future major version def get_volume(self, name_or_id, filters=None): """Get a volume by name or ID. - :param name_or_id: Name or ID of the volume. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: + :param name_or_id: Name or unique ID of the volume. + :param filters: **DEPRECATED** A dictionary of meta data to use for + further filtering. Elements of this dictionary may, themselves, be + dictionaries. Example:: { 'last_name': 'Smith', @@ -66,30 +82,32 @@ def get_volume(self, name_or_id, filters=None): } OR + A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + Example:: - :returns: A volume ``munch.Munch`` or None if no matching volume is - found. + "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :returns: A volume ``Volume`` object if found, else None. """ return _utils._get_entity(self, 'volume', name_or_id, filters) def get_volume_by_id(self, id): - """ Get a volume by ID + """Get a volume by ID :param id: ID of the volume. - :returns: A volume ``munch.Munch``. + :returns: A volume ``Volume`` object if found, else None. """ return self.block_storage.get_volume(id) + # TODO(stephenfin): Remove 'filters' in a future major version def get_volume_type(self, name_or_id, filters=None): """Get a volume type by name or ID. - :param name_or_id: Name or ID of the volume. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: + :param name_or_id: Name or unique ID of the volume type. + :param filters: **DEPRECATED** A dictionary of meta data to use for + further filtering. Elements of this dictionary may, themselves, be + dictionaries. Example:: { 'last_name': 'Smith', @@ -99,34 +117,37 @@ def get_volume_type(self, name_or_id, filters=None): } OR + A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + Example:: - :returns: A volume ``munch.Munch`` or None if no matching volume is - found. + "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :returns: A volume ``Type`` object if found, else None. """ return _utils._get_entity( self, 'volume_type', name_or_id, filters) def create_volume( - self, size, - wait=True, timeout=None, image=None, bootable=None, **kwargs): + self, + size, + wait=True, + timeout=None, + image=None, + bootable=None, + **kwargs, + ): """Create a volume. :param size: Size, in GB of the volume to create. - :param name: (optional) Name for the volume. - :param description: (optional) Name for the volume. :param wait: If true, waits for volume to be created. :param timeout: Seconds to wait for volume creation. None is forever. :param image: (optional) Image name, ID or object from which to create - the volume + the volume :param bootable: (optional) Make this volume bootable. If set, wait - will also be set to true. + will also be set to true. :param kwargs: Keyword arguments as expected for cinder client. - - :returns: The created volume object. - + :returns: The created volume ``Volume`` object. :raises: OpenStackCloudTimeout if wait time exceeded. :raises: OpenStackCloudException on operation error. """ @@ -159,6 +180,12 @@ def create_volume( return volume def update_volume(self, name_or_id, **kwargs): + """Update a volume. + + :param name_or_id: Name or unique ID of the volume. + :param kwargs: Volume attributes to be updated. + :returns: The updated volume ``Volume`` object. + """ kwargs = self._get_volume_kwargs(kwargs) volume = self.get_volume(name_or_id) @@ -176,12 +203,13 @@ def update_volume(self, name_or_id, **kwargs): def set_volume_bootable(self, name_or_id, bootable=True): """Set a volume's bootable flag. - :param name_or_id: Name, unique ID of the volume or a volume dict. + :param name_or_id: Name or unique ID of the volume. :param bool bootable: Whether the volume should be bootable. - (Defaults to True) + (Defaults to True) :raises: OpenStackCloudTimeout if wait time exceeded. :raises: OpenStackCloudException on operation error. + :returns: None """ volume = self.get_volume(name_or_id) @@ -191,10 +219,15 @@ def set_volume_bootable(self, name_or_id, bootable=True): "Volume {name_or_id} does not exist".format( name_or_id=name_or_id)) - return self.block_storage.set_volume_bootable_status(volume, bootable) + self.block_storage.set_volume_bootable_status(volume, bootable) - def delete_volume(self, name_or_id=None, wait=True, timeout=None, - force=False): + def delete_volume( + self, + name_or_id=None, + wait=True, + timeout=None, + force=False, + ): """Delete a volume. :param name_or_id: Name or unique ID of the volume. @@ -202,7 +235,7 @@ def delete_volume(self, name_or_id=None, wait=True, timeout=None, :param timeout: Seconds to wait for volume deletion. None is forever. :param force: Force delete volume even if the volume is in deleting or error_deleting state. - + :returns: True if deletion was successful, else False. :raises: OpenStackCloudTimeout if wait time exceeded. :raises: OpenStackCloudException on operation error. """ @@ -228,7 +261,14 @@ def delete_volume(self, name_or_id=None, wait=True, timeout=None, return True + # TODO(stephenfin): Remove 'cache' in a future major version def get_volumes(self, server, cache=True): + """Get volumes for a server. + + :param server: The server to fetch volumes for. + :param cache: **DEPRECATED** This parameter no longer does anything. + :returns: A list of volume ``Volume`` objects. + """ volumes = [] for volume in self.list_volumes(cache=cache): for attach in volume['attachments']: @@ -236,14 +276,13 @@ def get_volumes(self, server, cache=True): volumes.append(volume) return volumes + # TODO(stephenfin): Convert to use proxy def get_volume_limits(self, name_or_id=None): - """ Get volume limits for a project - - :param name_or_id: (optional) project name or ID to get limits for - if different from the current project - :raises: OpenStackCloudException if it's not a valid project + """Get volume limits for the current project - :returns: Munch object with the limits + :param name_or_id: (optional) Project name or ID to get limits for + if different from the current project + :returns: The volume ``Limit`` object if found, else None. """ params = {} project_id = None @@ -264,12 +303,22 @@ def get_volume_limits(self, name_or_id=None): return limits def get_volume_id(self, name_or_id): + """Get ID of a volume. + + :param name_or_id: Name or unique ID of the volume. + :returns: The ID of the volume if found, else None. + """ volume = self.get_volume(name_or_id) if volume: return volume['id'] return None def volume_exists(self, name_or_id): + """Check if a volume exists. + + :param name_or_id: Name or unique ID of the volume. + :returns: True if the volume exists, else False. + """ return self.get_volume(name_or_id) is not None def get_volume_attach_device(self, volume, server_id): @@ -278,9 +327,8 @@ def get_volume_attach_device(self, volume, server_id): This can also be used to verify if a volume is attached to a particular server. - :param volume: Volume dict - :param server_id: ID of server to check - + :param volume: The volume to fetch the device name from. + :param server_id: ID of server to check. :returns: Device name if attached, None if volume is not attached. """ for attach in volume['attachments']: @@ -295,20 +343,26 @@ def detach_volume(self, server, volume, wait=True, timeout=None): :param volume: The volume dict to detach. :param wait: If true, waits for volume to be detached. :param timeout: Seconds to wait for volume detachment. None is forever. - + :returns: None :raises: OpenStackCloudTimeout if wait time exceeded. :raises: OpenStackCloudException on operation error. """ self.compute.delete_volume_attachment( volume['id'], server['id'], - ignore_missing=False) - + ignore_missing=False, + ) if wait: vol = self.get_volume(volume['id']) self.block_storage.wait_for_status(vol) - def attach_volume(self, server, volume, device=None, - wait=True, timeout=None): + def attach_volume( + self, + server, + volume, + device=None, + wait=True, + timeout=None, + ): """Attach a volume to a server. This will attach a volume, described by the passed in volume @@ -325,9 +379,7 @@ def attach_volume(self, server, volume, device=None, :param device: The device name where the volume will attach. :param wait: If true, waits for volume to be attached. :param timeout: Seconds to wait for volume attachment. None is forever. - :returns: a volume attachment object. - :raises: OpenStackCloudTimeout if wait time exceeded. :raises: OpenStackCloudException on operation error. """ @@ -371,27 +423,30 @@ def _get_volume_kwargs(self, kwargs): @_utils.valid_kwargs('name', 'display_name', 'description', 'display_description') - def create_volume_snapshot(self, volume_id, force=False, - wait=True, timeout=None, **kwargs): + def create_volume_snapshot( + self, + volume_id, + force=False, + wait=True, + timeout=None, + **kwargs, + ): """Create a volume. :param volume_id: the ID of the volume to snapshot. :param force: If set to True the snapshot will be created even if the - volume is attached to an instance, if False it will not + volume is attached to an instance, if False it will not :param name: name of the snapshot, one will be generated if one is - not provided + not provided :param description: description of the snapshot, one will be generated - if one is not provided + if one is not provided :param wait: If true, waits for volume snapshot to be created. :param timeout: Seconds to wait for volume snapshot creation. None is - forever. - - :returns: The created volume object. - + forever. + :returns: The created volume ``Snapshot`` object. :raises: OpenStackCloudTimeout if wait time exceeded. :raises: OpenStackCloudException on operation error. """ - kwargs = self._get_volume_kwargs(kwargs) payload = {'volume_id': volume_id} payload.update(kwargs) @@ -409,17 +464,18 @@ def get_volume_snapshot_by_id(self, snapshot_id): Note: This is more efficient than get_volume_snapshot. param: snapshot_id: ID of the volume snapshot. - + :returns: A volume ``Snapshot`` object if found, else None. """ return self.block_storage.get_snapshot(snapshot_id) + # TODO(stephenfin): Remove 'filters' in a future major version def get_volume_snapshot(self, name_or_id, filters=None): """Get a volume by name or ID. - :param name_or_id: Name or ID of the volume snapshot. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: + :param name_or_id: Name or unique ID of the volume snapshot. + :param filters: **DEPRECATED** A dictionary of meta data to use for + further filtering. Elements of this dictionary may, themselves, be + dictionaries. Example:: { 'last_name': 'Smith', @@ -429,36 +485,43 @@ def get_volume_snapshot(self, name_or_id, filters=None): } OR + A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + Example:: - :returns: A volume ``munch.Munch`` or None if no matching volume is - found. + "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: A volume ``Snapshot`` object if found, else None. """ return _utils._get_entity(self, 'volume_snapshot', name_or_id, filters) - def create_volume_backup(self, volume_id, name=None, description=None, - force=False, wait=True, timeout=None, - incremental=False, snapshot_id=None): - + def create_volume_backup( + self, + volume_id, + name=None, + description=None, + force=False, + wait=True, + timeout=None, + incremental=False, + snapshot_id=None, + ): """Create a volume backup. :param volume_id: the ID of the volume to backup. :param name: name of the backup, one will be generated if one is - not provided + not provided :param description: description of the backup, one will be generated - if one is not provided + if one is not provided :param force: If set to True the backup will be created even if the - volume is attached to an instance, if False it will not + volume is attached to an instance, if False it will not :param wait: If true, waits for volume backup to be created. :param timeout: Seconds to wait for volume backup creation. None is - forever. + forever. :param incremental: If set to true, the backup will be incremental. :param snapshot_id: The UUID of the source snapshot to back up. - - :returns: The created volume backup object. - + :returns: The created volume ``Backup`` object. :raises: OpenStackCloudTimeout if wait time exceeded. :raises: OpenStackCloudException on operation error. """ @@ -478,33 +541,59 @@ def create_volume_backup(self, volume_id, name=None, description=None, return backup + # TODO(stephenfin): Remove 'filters' in a future major version def get_volume_backup(self, name_or_id, filters=None): """Get a volume backup by name or ID. - :returns: A backup ``munch.Munch`` or None if no matching backup is - found. + :param name_or_id: Name or unique ID of the volume backup. + :param filters: **DEPRECATED** A dictionary of meta data to use for + further filtering. Elements of this dictionary may, themselves, be + dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + + A string containing a jmespath expression for further filtering. + Example:: + + "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: A volume ``Backup`` object if found, else None. """ return _utils._get_entity(self, 'volume_backup', name_or_id, filters) - def list_volume_snapshots(self, detailed=True, search_opts=None): + def list_volume_snapshots(self, detailed=True, filters=None): """List all volume snapshots. - :returns: A list of volume snapshots ``munch.Munch``. + :param detailed: Whether or not to add detailed additional information. + :param filters: A dictionary of meta data to use for further filtering. + Example:: - """ - if not search_opts: - search_opts = {} - return list(self.block_storage.snapshots( - details=detailed, **search_opts)) + { + 'name': 'my-volume-snapshot', + 'volume_id': 'e126044c-7b4c-43be-a32a-c9cbbc9ddb56', + 'all_tenants': 1 + } - def list_volume_backups(self, detailed=True, search_opts=None): + :returns: A list of volume ``Snapshot`` objects. """ - List all volume backups. + if not filters: + filters = {} + return list(self.block_storage.snapshots(details=detailed, **filters)) - :param bool detailed: Also list details for each entry - :param dict search_opts: Search options - A dictionary of meta data to use for further filtering. Example:: + def list_volume_backups(self, detailed=True, filters=None): + """List all volume backups. + + :param detailed: Whether or not to add detailed additional information. + :param filters: A dictionary of meta data to use for further filtering. + Example:: { 'name': 'my-volume-backup', @@ -513,13 +602,12 @@ def list_volume_backups(self, detailed=True, search_opts=None): 'all_tenants': 1 } - :returns: A list of volume backups ``munch.Munch``. + :returns: A list of volume ``Backup`` objects. """ - if not search_opts: - search_opts = {} + if not filters: + filters = {} - return list(self.block_storage.backups(details=detailed, - **search_opts)) + return list(self.block_storage.backups(details=detailed, **filters)) def delete_volume_backup(self, name_or_id=None, force=False, wait=False, timeout=None): @@ -530,7 +618,7 @@ def delete_volume_backup(self, name_or_id=None, force=False, wait=False, :param wait: If true, waits for volume backup to be deleted. :param timeout: Seconds to wait for volume backup deletion. None is forever. - + :returns: True if deletion was successful, else False. :raises: OpenStackCloudTimeout if wait time exceeded. :raises: OpenStackCloudException on operation error. """ @@ -547,19 +635,22 @@ def delete_volume_backup(self, name_or_id=None, force=False, wait=False, return True - def delete_volume_snapshot(self, name_or_id=None, wait=False, - timeout=None): + def delete_volume_snapshot( + self, + name_or_id=None, + wait=False, + timeout=None, + ): """Delete a volume snapshot. :param name_or_id: Name or unique ID of the volume snapshot. :param wait: If true, waits for volume snapshot to be deleted. :param timeout: Seconds to wait for volume snapshot deletion. None is - forever. - + forever. + :returns: True if deletion was successful, else False. :raises: OpenStackCloudTimeout if wait time exceeded. :raises: OpenStackCloudException on operation error. """ - volumesnapshot = self.get_volume_snapshot(name_or_id) if not volumesnapshot: @@ -574,30 +665,127 @@ def delete_volume_snapshot(self, name_or_id=None, wait=False, return True def search_volumes(self, name_or_id=None, filters=None): + """Search for one or more volumes. + + :param name_or_id: Name or unique ID of volume(s). + :param filters: **DEPRECATED** A dictionary of meta data to use for + further filtering. Elements of this dictionary may, themselves, be + dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + + A string containing a jmespath expression for further filtering. + Example:: + + "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: A list of volume ``Volume`` objects, if any are found. + """ volumes = self.list_volumes() return _utils._filter_list( volumes, name_or_id, filters) def search_volume_snapshots(self, name_or_id=None, filters=None): + """Search for one or more volume snapshots. + + :param name_or_id: Name or unique ID of volume snapshot(s). + :param filters: **DEPRECATED** A dictionary of meta data to use for + further filtering. Elements of this dictionary may, themselves, be + dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + + A string containing a jmespath expression for further filtering. + Example:: + + "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: A list of volume ``Snapshot`` objects, if any are found. + """ volumesnapshots = self.list_volume_snapshots() return _utils._filter_list( volumesnapshots, name_or_id, filters) def search_volume_backups(self, name_or_id=None, filters=None): + """Search for one or more volume backups. + + :param name_or_id: Name or unique ID of volume backup(s). + :param filters: **DEPRECATED** A dictionary of meta data to use for + further filtering. Elements of this dictionary may, themselves, be + dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + + A string containing a jmespath expression for further filtering. + Example:: + + "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: A list of volume ``Backup`` objects, if any are found. + """ volume_backups = self.list_volume_backups() return _utils._filter_list( volume_backups, name_or_id, filters) + # TODO(stephenfin): Remove 'get_extra' in a future major version def search_volume_types( - self, name_or_id=None, filters=None, get_extra=True): + self, + name_or_id=None, + filters=None, + get_extra=None, + ): + """Search for one or more volume types. + + :param name_or_id: Name or unique ID of volume type(s). + :param filters: **DEPRECATED** A dictionary of meta data to use for + further filtering. Elements of this dictionary may, themselves, be + dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + + A string containing a jmespath expression for further filtering. + Example:: + + "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: A list of volume ``Type`` objects, if any are found. + """ volume_types = self.list_volume_types(get_extra=get_extra) return _utils._filter_list(volume_types, name_or_id, filters) def get_volume_type_access(self, name_or_id): """Return a list of volume_type_access. - :param name_or_id: Name or ID of the volume type. - + :param name_or_id: Name or unique ID of the volume type. + :returns: A volume ``Type`` object if found, else None. :raises: OpenStackCloudException on operation error. """ volume_type = self.get_volume_type(name_or_id) @@ -610,11 +798,11 @@ def get_volume_type_access(self, name_or_id): def add_volume_type_access(self, name_or_id, project_id): """Grant access on a volume_type to a project. - :param name_or_id: ID or name of a volume_type - :param project_id: A project id - NOTE: the call works even if the project does not exist. + :param name_or_id: ID or name of a volume_type + :param project_id: A project id + :returns: None :raises: OpenStackCloudException on operation error. """ volume_type = self.get_volume_type(name_or_id) @@ -629,7 +817,7 @@ def remove_volume_type_access(self, name_or_id, project_id): :param name_or_id: ID or name of a volume_type :param project_id: A project id - + :returns: None :raises: OpenStackCloudException on operation error. """ volume_type = self.get_volume_type(name_or_id) @@ -639,11 +827,11 @@ def remove_volume_type_access(self, name_or_id, project_id): self.block_storage.remove_type_access(volume_type, project_id) def set_volume_quotas(self, name_or_id, **kwargs): - """ Set a volume quota in a project + """Set a volume quota in a project :param name_or_id: project name or id :param kwargs: key/value pairs of quota name and quota value - + :returns: None :raises: OpenStackCloudException if the resource to set the quota does not exist. """ @@ -656,30 +844,24 @@ def set_volume_quotas(self, name_or_id, **kwargs): **kwargs) def get_volume_quotas(self, name_or_id): - """ Get volume quotas for a project + """Get volume quotas for a project :param name_or_id: project name or id + :returns: A volume ``QuotaSet`` object with the quotas :raises: OpenStackCloudException if it's not a valid project - - :returns: Munch object with the quotas """ - proj = self.identity.find_project( - name_or_id, ignore_missing=False) + proj = self.identity.find_project(name_or_id, ignore_missing=False) - return self.block_storage.get_quota_set( - proj) + return self.block_storage.get_quota_set(proj) def delete_volume_quotas(self, name_or_id): - """ Delete volume quotas for a project + """Delete volume quotas for a project :param name_or_id: project name or id + :returns: The deleted volume ``QuotaSet`` object. :raises: OpenStackCloudException if it's not a valid project or the - cinder client call failed - - :returns: dict with the quotas + call failed """ - proj = self.identity.find_project( - name_or_id, ignore_missing=False) + proj = self.identity.find_project(name_or_id, ignore_missing=False) - return self.block_storage.revert_quota_set( - proj) + return self.block_storage.revert_quota_set(proj) From 6fd5f99dc6df9403777589bfc8c3414a875f99d2 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 26 May 2022 11:54:34 +0100 Subject: [PATCH 3044/3836] cloud: Update docstrings for identity functions Change-Id: Id453955bf2cf13e3a8dd62ac334fa8772fbd8006 Signed-off-by: Stephen Finucane --- openstack/cloud/_identity.py | 597 ++++++++++++++++++++--------------- 1 file changed, 351 insertions(+), 246 deletions(-) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index 07f9e84ca..159b08d68 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -15,8 +15,6 @@ # openstack.resource.Resource.list and openstack.resource2.Resource.list import types # noqa -import munch - from openstack.cloud import _utils from openstack.cloud import exc from openstack import exceptions @@ -37,22 +35,33 @@ def list_projects(self, domain_id=None, name_or_id=None, filters=None): With no parameters, returns a full listing of all visible projects. - :param domain_id: domain ID to scope the searched projects. - :param name_or_id: project name or ID. - :param filters: a dict containing additional filters to use + :param domain_id: Domain ID to scope the searched projects. + :param name_or_id: Name or ID of the project(s). + :param filters: A dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. + Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + OR + A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + Example:: - :returns: a list of ``munch.Munch`` containing the projects + "[?last_name==`Smith`] | [?other.gender]==`Female`]" - :raises: ``OpenStackCloudException``: if something goes wrong during + :returns: A list of identity ``Project`` objects. + :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ if not filters: filters = {} - query = dict( - **filters) + query = dict(**filters) if name_or_id: query['name'] = name_or_id if domain_id: @@ -61,41 +70,86 @@ def list_projects(self, domain_id=None, name_or_id=None, filters=None): return list(self.identity.projects(**query)) def search_projects(self, name_or_id=None, filters=None, domain_id=None): - '''Backwards compatibility method for search_projects + """Backwards compatibility method for search_projects search_projects originally had a parameter list that was name_or_id, filters and list had domain_id first. This method exists in this form to allow code written with positional parameter to still work. But really, use keyword arguments. - ''' + + :param name_or_id: Name or ID of the project(s). + :param filters: dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. + Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + + A string containing a jmespath expression for further filtering. + Example:: + + "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :param domain_id: Domain ID to scope the searched projects. + :returns: A list of identity ``Project`` objects. + """ projects = self.list_projects(domain_id=domain_id, filters=filters) return _utils._filter_list(projects, name_or_id, filters) def get_project(self, name_or_id, filters=None, domain_id=None): """Get exactly one project. - :param name_or_id: project name or ID. - :param filters: a dict containing additional filters to use. - :param domain_id: domain ID (identity v3 only). + :param name_or_id: Name or unique ID of the project. + :param filters: **DEPRECATED** A dictionary of meta data to use for + further filtering. Elements of this dictionary may, themselves, be + dictionaries. Example:: - :returns: a list of ``munch.Munch`` containing the project description. + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } - :raises: ``OpenStackCloudException``: if something goes wrong during + OR + + A string containing a jmespath expression for further filtering. + Example:: + + "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :param domain_id: Domain ID to scope the retrieved project. + :returns: An identity ``Project`` object. + :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ return _utils._get_entity(self, 'project', name_or_id, filters, domain_id=domain_id) def update_project( - self, name_or_id, enabled=None, domain_id=None, **kwargs + self, + name_or_id, + enabled=None, + domain_id=None, + **kwargs, ): + """Update a project + :param name_or_id: Name or unique ID of the project. + :param enabled: Whether the project is enabled or not. + :param domain_id: Domain ID to scope the retrieved project. + :returns: An identity ``Project`` object. + """ project = self.identity.find_project( name_or_id=name_or_id, - domain_id=domain_id) + domain_id=domain_id, + ) if not project: - raise exceptions.SDKException( - "Project %s not found." % name_or_id) + raise exceptions.SDKException("Project %s not found." % name_or_id) if enabled is not None: kwargs.update({'enabled': enabled}) project = self.identity.update_project(project, **kwargs) @@ -103,8 +157,21 @@ def update_project( return project def create_project( - self, name, domain_id, description=None, enabled=True, **kwargs): - """Create a project.""" + self, + name, + domain_id, + description=None, + enabled=True, + **kwargs, + ): + """Create a project. + + :param name: + :param domain_id: + :param description: + :param enabled: + :returns: An identity ``Project`` object. + """ attrs = dict( name=name, description=description, @@ -118,12 +185,9 @@ def create_project( def delete_project(self, name_or_id, domain_id=None): """Delete a project. - :param string name_or_id: Project name or ID. - :param string domain_id: Domain ID containing the project(identity v3 - only). - + :param name_or_id: Name or unique ID of the project. + :param domain_id: Domain ID to scope the retrieved project. :returns: True if delete succeeded, False if the project was not found. - :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call """ @@ -149,71 +213,89 @@ def delete_project(self, name_or_id, domain_id=None): def list_users(self, **kwargs): """List users. - :param domain_id: Domain ID. (v3) - - :returns: a list of ``munch.Munch`` containing the user description. - + :param name: + :param domain_id: Domain ID to scope the retrieved users. + :returns: A list of identity ``User`` objects. :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ return list(self.identity.users(**kwargs)) - @_utils.valid_kwargs('domain_id', 'name') - def search_users(self, name_or_id=None, filters=None, **kwargs): + def search_users(self, name_or_id=None, filters=None, domain_id=None): """Search users. - :param string name_or_id: user name or ID. - :param domain_id: Domain ID. (v3) - :param filters: a dict containing additional filters to use. + :param name_or_id: Name or ID of the user(s). + :param domain_id: Domain ID to scope the retrieved users. + :param filters: dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. + Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + OR - A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - :returns: a list of ``munch.Munch`` containing the users + A string containing a jmespath expression for further filtering. + Example:: + "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :returns: A list of identity ``User`` objects :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ + kwargs = {} # NOTE(jdwidari) if name_or_id isn't UUID like then make use of server- # side filter for user name https://bit.ly/2qh0Ijk # especially important when using LDAP and using page to limit results if name_or_id and not _utils._is_uuid_like(name_or_id): kwargs['name'] = name_or_id + if domain_id: + kwargs['domain_id'] = domain_id users = self.list_users(**kwargs) return _utils._filter_list(users, name_or_id, filters) + # TODO(stephenfin): Remove 'filters' in a future major version + # TODO(stephenfin): Remove 'kwargs' since it doesn't do anything @_utils.valid_kwargs('domain_id') def get_user(self, name_or_id, filters=None, **kwargs): """Get exactly one user. - :param string name_or_id: user name or ID. - :param domain_id: Domain ID. (v3) - :param filters: a dict containing additional filters to use. + :param name_or_id: Name or unique ID of the user. + :param filters: **DEPRECATED** A dictionary of meta data to use for + further filtering. Elements of this dictionary may, themselves, be + dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + OR - A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - :returns: a single ``munch.Munch`` containing the user description. + A string containing a jmespath expression for further filtering. + Example:: + "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :returns: an identity ``User`` object :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ - if not _utils._is_uuid_like(name_or_id): - kwargs['name'] = name_or_id - return _utils._get_entity(self, 'user', name_or_id, filters, **kwargs) + # TODO(stephenfin): Remove normalize since it doesn't do anything def get_user_by_id(self, user_id, normalize=True): """Get a user by ID. :param string user_id: user ID - :param bool normalize: Flag to control dict normalization - - :returns: a single ``munch.Munch`` containing the user description + :returns: an identity ``User`` object """ - user = self.identity.get_user(user_id) - - return user + return self.identity.get_user(user_id) @_utils.valid_kwargs('name', 'email', 'enabled', 'domain_id', 'password', 'description', 'default_project') @@ -237,8 +319,15 @@ def update_user(self, name_or_id, **kwargs): return user def create_user( - self, name, password=None, email=None, default_project=None, - enabled=True, domain_id=None, description=None): + self, + name, + password=None, + email=None, + default_project=None, + enabled=True, + domain_id=None, + description=None, + ): """Create a user.""" params = self._get_identity_params(domain_id, default_project) params.update({'name': name, 'password': password, 'email': email, @@ -286,9 +375,8 @@ def _get_user_and_group(self, user_name_or_id, group_name_or_id): def add_user_to_group(self, name_or_id, group_name_or_id): """Add a user to a group. - :param string name_or_id: User name or ID - :param string group_name_or_id: Group name or ID - + :param name_or_id: Name or unique ID of the user. + :param group_name_or_id: Group name or ID :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call """ @@ -299,11 +387,9 @@ def add_user_to_group(self, name_or_id, group_name_or_id): def is_user_in_group(self, name_or_id, group_name_or_id): """Check to see if a user is in a group. - :param string name_or_id: User name or ID - :param string group_name_or_id: Group name or ID - + :param name_or_id: Name or unique ID of the user. + :param group_name_or_id: Group name or ID :returns: True if user is in the group, False otherwise - :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call """ @@ -314,9 +400,8 @@ def is_user_in_group(self, name_or_id, group_name_or_id): def remove_user_from_group(self, name_or_id, group_name_or_id): """Remove a user from a group. - :param string name_or_id: User name or ID - :param string group_name_or_id: Group name or ID - + :param name_or_id: Name or unique ID of the user. + :param group_name_or_id: Group name or ID :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call """ @@ -333,18 +418,9 @@ def create_service(self, name, enabled=True, **kwargs): :param service_type: Service type. (type or service_type required.) :param description: Service description (optional). :param enabled: Whether the service is enabled (v3 only) - - :returns: a ``munch.Munch`` containing the services description, - i.e. the following attributes:: - - id: - - name: - - type: - - service_type: - - description: - + :returns: an identity ``Service`` object :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. - """ type_ = kwargs.pop('type', None) service_type = kwargs.pop('service_type', None) @@ -375,8 +451,7 @@ def update_service(self, name_or_id, **kwargs): def list_services(self): """List all Keystone services. - :returns: a list of ``munch.Munch`` containing the services description - + :returns: A list of identity ``Service`` object :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ @@ -385,32 +460,37 @@ def list_services(self): def search_services(self, name_or_id=None, filters=None): """Search Keystone services. - :param name_or_id: Name or id of the desired service. - :param filters: a dict containing additional filters to use. e.g. - {'type': 'network'}. + :param name_or_id: Name or ID of the service(s). + :param filters: dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. + Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } - :returns: a list of ``munch.Munch`` containing the services description + OR + + A string containing a jmespath expression for further filtering. + Example:: + "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :returns: a list of identity ``Service`` objects :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ services = self.list_services() return _utils._filter_list(services, name_or_id, filters) + # TODO(stephenfin): Remove 'filters' since it's a noop def get_service(self, name_or_id, filters=None): """Get exactly one Keystone service. - :param name_or_id: Name or id of the desired service. - :param filters: a dict containing additional filters to use. e.g. - {'type': 'network'} - - :returns: a ``munch.Munch`` containing the services description, - i.e. the following attributes:: - - id: - - name: - - type: - - description: - + :param name_or_id: Name or unique ID of the service. + :returns: an identity ``Service`` object :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call or if multiple matches are found. """ @@ -419,10 +499,8 @@ def get_service(self, name_or_id, filters=None): def delete_service(self, name_or_id): """Delete a Keystone service. - :param name_or_id: Service name or id. - + :param name_or_id: Name or unique ID of the service. :returns: True if delete succeeded, False otherwise. - :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call """ @@ -430,6 +508,7 @@ def delete_service(self, name_or_id): if service is None: self.log.debug("Service %s not found for deleting", name_or_id) return False + try: self.identity.delete_service(service) return True @@ -439,8 +518,15 @@ def delete_service(self, name_or_id): return False @_utils.valid_kwargs('public_url', 'internal_url', 'admin_url') - def create_endpoint(self, service_name_or_id, url=None, interface=None, - region=None, enabled=True, **kwargs): + def create_endpoint( + self, + service_name_or_id, + url=None, + interface=None, + region=None, + enabled=True, + **kwargs, + ): """Create a Keystone endpoint. :param service_name_or_id: Service name or id for this endpoint. @@ -451,9 +537,7 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, :param admin_url: Endpoint admin URL. :param region: Endpoint region. :param enabled: Whether the endpoint is enabled - - :returns: a list of ``munch.Munch`` containing the endpoint description - + :returns: A list of identity ``Endpoint`` objects :raises: OpenStackCloudException if the service cannot be found or if something goes wrong during the OpenStack API call. """ @@ -463,8 +547,9 @@ def create_endpoint(self, service_name_or_id, url=None, interface=None, if (url or interface) and (public_url or internal_url or admin_url): raise exc.OpenStackCloudException( - "create_endpoint takes either url and interface OR" - " public_url, internal_url, admin_url") + "create_endpoint takes either url and interface OR " + "public_url, internal_url, admin_url" + ) service = self.get_service(name_or_id=service_name_or_id) if service is None: @@ -516,8 +601,7 @@ def update_endpoint(self, endpoint_id, **kwargs): def list_endpoints(self): """List Keystone endpoints. - :returns: a list of ``munch.Munch`` containing the endpoint description - + :returns: A list of identity ``Endpoint`` objects :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ @@ -526,18 +610,25 @@ def list_endpoints(self): def search_endpoints(self, id=None, filters=None): """List Keystone endpoints. - :param id: endpoint id. - :param filters: a dict containing additional filters to use. e.g. - {'region': 'region-a.geo-1'} + :param id: ID of endpoint(s). + :param filters: dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. + Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } - :returns: a list of ``munch.Munch`` containing the endpoint - description. Each dict contains the following attributes:: - - id: - - region: - - public_url: - - internal_url: (optional) - - admin_url: (optional) + OR + + A string containing a jmespath expression for further filtering. + Example:: + "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :returns: A list of identity ``Endpoint`` objects :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ @@ -548,30 +639,20 @@ def search_endpoints(self, id=None, filters=None): endpoints = self.list_endpoints() return _utils._filter_list(endpoints, id, filters) + # TODO(stephenfin): Remove 'filters' since it's a noop def get_endpoint(self, id, filters=None): """Get exactly one Keystone endpoint. - :param id: endpoint id. - :param filters: a dict containing additional filters to use. e.g. - {'region': 'region-a.geo-1'} - - :returns: a ``munch.Munch`` containing the endpoint description. - i.e. a ``munch.Munch`` containing the following attributes:: - - id: - - region: - - public_url: - - internal_url: (optional) - - admin_url: (optional) + :param id: ID of endpoint. + :returns: An identity ``Endpoint`` object """ return _utils._get_entity(self, 'endpoint', id, filters) def delete_endpoint(self, id): """Delete a Keystone endpoint. - :param id: Id of the endpoint to delete. - + :param id: ID of the endpoint to delete. :returns: True if delete succeeded, False otherwise. - :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ @@ -594,9 +675,7 @@ def create_domain(self, name, description=None, enabled=True): :param name: The name of the domain. :param description: A description of the domain. :param enabled: Is the domain enabled or not (default True). - - :returns: a ``munch.Munch`` containing the domain representation. - + :returns: The created identity ``Endpoint`` object. :raise OpenStackCloudException: if the domain cannot be created. """ domain_ref = {'name': name, 'enabled': enabled} @@ -604,9 +683,26 @@ def create_domain(self, name, description=None, enabled=True): domain_ref['description'] = description return self.identity.create_domain(**domain_ref) + # TODO(stephenfin): domain_id and name_or_id are the same thing now; + # deprecate one of them def update_domain( - self, domain_id=None, name=None, description=None, - enabled=None, name_or_id=None): + self, + domain_id=None, + name=None, + description=None, + enabled=None, + name_or_id=None, + ): + """Update a Keystone domain + + :param domain_id: + :param name: + :param description: + :param enabled: + :param name_or_id: Name or unique ID of the domain. + :returns: The updated identity ``Domain`` object. + :raise OpenStackCloudException: if the domain cannot be updated + """ if domain_id is None: if name_or_id is None: raise exc.OpenStackCloudException( @@ -625,14 +721,14 @@ def update_domain( domain_ref.update({'enabled': enabled} if enabled is not None else {}) return self.identity.update_domain(domain_id, **domain_ref) + # TODO(stephenfin): domain_id and name_or_id are the same thing now; + # deprecate one of them def delete_domain(self, domain_id=None, name_or_id=None): - """Delete a domain. + """Delete a Keystone domain. :param domain_id: ID of the domain to delete. - :param name_or_id: Name or ID of the domain to delete. - + :param name_or_id: Name or unique ID of the domain. :returns: True if delete succeeded, False otherwise. - :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ @@ -661,26 +757,35 @@ def delete_domain(self, domain_id=None, name_or_id=None): def list_domains(self, **filters): """List Keystone domains. - :returns: a list of ``munch.Munch`` containing the domain description. - + :returns: A list of identity ``Domain`` objects. :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ return list(self.identity.domains(**filters)) + # TODO(stephenfin): These arguments are backwards from everything else. def search_domains(self, filters=None, name_or_id=None): """Search Keystone domains. - :param name_or_id: domain name or id - :param dict filters: A dict containing additional filters to use. - Keys to search on are id, name, enabled and description. + :param name_or_id: Name or ID of the domain(s). + :param filters: dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. + Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } - :returns: a list of ``munch.Munch`` containing the domain description. - Each ``munch.Munch`` contains the following attributes:: - - id: - - name: - - description: + OR + A string containing a jmespath expression for further filtering. + Example:: + + "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :returns: a list of identity ``Domain`` objects :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ @@ -692,40 +797,47 @@ def search_domains(self, filters=None, name_or_id=None): else: return self.list_domains(**filters) + # TODO(stephenfin): domain_id and name_or_id are the same thing now; + # deprecate one of them + # TODO(stephenfin): Remove 'filters' in a future major version def get_domain(self, domain_id=None, name_or_id=None, filters=None): """Get exactly one Keystone domain. - :param domain_id: domain id. - :param name_or_id: domain name or id. - :param dict filters: A dict containing additional filters to use. - Keys to search on are id, name, enabled and description. + :param domain_id: ID of the domain. + :param name_or_id: Name or unique ID of the domain. + :param filters: **DEPRECATED** A dictionary of meta data to use for + further filtering. Elements of this dictionary may, themselves, be + dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR - :returns: a ``munch.Munch`` containing the domain description, or None - if not found. Each ``munch.Munch`` contains the following - attributes:: - - id: - - name: - - description: + A string containing a jmespath expression for further filtering. + Example:: + "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :returns: an identity ``Domain`` object :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ if domain_id is None: - if not filters: - filters = {} - return self.identity.find_domain(name_or_id, **filters) + return self.identity.find_domain(name_or_id) else: return self.identity.get_domain(domain_id) @_utils.valid_kwargs('domain_id') @_utils.cache_on_arguments() def list_groups(self, **kwargs): - """List Keystone Groups. - - :param domain_id: domain id. - - :returns: A list of ``munch.Munch`` containing the group description. + """List Keystone groups. + :param domain_id: Domain ID. + :returns: A list of identity ``Group`` objects :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ @@ -735,28 +847,40 @@ def list_groups(self, **kwargs): def search_groups(self, name_or_id=None, filters=None, **kwargs): """Search Keystone groups. - :param name: Group name or id. - :param filters: A dict containing additional filters to use. - :param domain_id: domain id. + :param name_or_id: Name or ID of the group(s). + :param filters: dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. + Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR - :returns: A list of ``munch.Munch`` containing the group description. + A string containing a jmespath expression for further filtering. + Example:: + "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :param domain_id: domain id. + :returns: A list of identity ``Group`` objects :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ groups = self.list_groups(**kwargs) return _utils._filter_list(groups, name_or_id, filters) + # TODO(stephenfin): Remove filters since it's a noop + # TODO(stephenfin): Remove kwargs since it's a noop @_utils.valid_kwargs('domain_id') def get_group(self, name_or_id, filters=None, **kwargs): """Get exactly one Keystone group. - :param id: Group name or id. - :param filters: A dict containing additional filters to use. - :param domain_id: domain id. - - :returns: A ``munch.Munch`` containing the group description. - + :param name_or_id: Name or unique ID of the group(s). + :returns: An identity ``Group`` object :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ @@ -768,9 +892,7 @@ def create_group(self, name, description, domain=None): :param string name: Group name. :param string description: Group description. :param string domain: Domain name or ID for the group. - - :returns: A ``munch.Munch`` containing the group description. - + :returns: An identity ``Group`` object :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ @@ -791,17 +913,19 @@ def create_group(self, name, description, domain=None): self.list_groups.invalidate(self) return group - @_utils.valid_kwargs('domain_id') - def update_group(self, name_or_id, name=None, description=None, - **kwargs): + def update_group( + self, + name_or_id, + name=None, + description=None, + **kwargs, + ): """Update an existing group - :param string name: New group name. - :param string description: New group description. - :param domain_id: domain id. - - :returns: A ``munch.Munch`` containing the group description. - + :param name_or_id: Name or unique ID of the group. + :param name: New group name. + :param description: New group description. + :returns: The updated identity ``Group`` object. :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ @@ -823,20 +947,16 @@ def update_group(self, name_or_id, name=None, description=None, self.list_groups.invalidate(self) return group - @_utils.valid_kwargs('domain_id') - def delete_group(self, name_or_id, **kwargs): + def delete_group(self, name_or_id): """Delete a group - :param name_or_id: ID or name of the group to delete. - :param domain_id: domain id. - + :param name_or_id: Name or unique ID of the group. :returns: True if delete succeeded, False otherwise. - :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ try: - group = self.identity.find_group(name_or_id, **kwargs) + group = self.identity.find_group(name_or_id) if group is None: self.log.debug( "Group %s not found for deleting", name_or_id) @@ -852,55 +972,51 @@ def delete_group(self, name_or_id, **kwargs): "Unable to delete group {name}".format(name=name_or_id)) return False - @_utils.valid_kwargs('domain_id', 'name') def list_roles(self, **kwargs): """List Keystone roles. - :param domain_id: domain id for listing roles - - :returns: a list of ``munch.Munch`` containing the role description. - + :returns: A list of identity ``Role`` objects :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ return list(self.identity.roles(**kwargs)) - @_utils.valid_kwargs('domain_id') - def search_roles(self, name_or_id=None, filters=None, **kwargs): + def search_roles(self, name_or_id=None, filters=None): """Seach Keystone roles. - :param string name: role name or id. - :param dict filters: a dict containing additional filters to use. - :param domain_id: domain id (v3) + :param name: Name or ID of the role(s). + :param filters: dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. + Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } - :returns: a list of ``munch.Munch`` containing the role description. - Each ``munch.Munch`` contains the following attributes:: + OR - - id: - - name: - - description: + A string containing a jmespath expression for further filtering. + Example:: + "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :returns: a list of identity ``Role`` objects :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ - roles = self.list_roles(**kwargs) + roles = self.list_roles() return _utils._filter_list(roles, name_or_id, filters) + # TODO(stephenfin): Remove filters since it's a noop + # TODO(stephenfin): Remove kwargs since it's a noop @_utils.valid_kwargs('domain_id') def get_role(self, name_or_id, filters=None, **kwargs): - """Get exactly one Keystone role. - - :param id: role name or id. - :param filters: a dict containing additional filters to use. - :param domain_id: domain id (v3) - - :returns: a single ``munch.Munch`` containing the role description. - Each ``munch.Munch`` contains the following attributes:: - - - id: - - name: - - description: + """Get a Keystone role. + :param name_or_id: Name or unique ID of the role. + :returns: An identity ``Role`` object if found, else None. :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ @@ -955,14 +1071,9 @@ def list_role_assignments(self, filters=None): 'user' and 'group' are mutually exclusive, as are 'domain' and 'project'. - :returns: a list of + :returns: A list of indentity :class:`openstack.identity.v3.role_assignment.RoleAssignment` - objects. Contains the following attributes:: - - - id: - - user|group: - - project|domain: - + objects :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ @@ -975,13 +1086,13 @@ def list_role_assignments(self, filters=None): filters = {} # NOTE(samueldmq): the docs above say filters are *IDs*, though if - # munch.Munch objects are passed, this still works for backwards + # dict or Resource objects are passed, this still works for backwards # compatibility as keystoneclient allows either IDs or objects to be # passed in. - # TODO(samueldmq): fix the docs above to advertise munch.Munch objects + # TODO(samueldmq): fix the docs above to advertise Resource objects # can be provided as parameters too for k, v in filters.items(): - if isinstance(v, munch.Munch): + if isinstance(v, dict): filters[k] = v['id'] for k in ['role', 'group', 'user']: @@ -1004,9 +1115,7 @@ def create_role(self, name, **kwargs): :param string name: The name of the role. :param domain_id: domain id (v3) - - :returns: a ``munch.Munch`` containing the role description - + :returns: an identity ``Role`` object :raise OpenStackCloudException: if the role cannot be created """ kwargs['name'] = name @@ -1016,12 +1125,10 @@ def create_role(self, name, **kwargs): def update_role(self, name_or_id, name, **kwargs): """Update a Keystone role. - :param name_or_id: Name or id of the role to update + :param name_or_id: Name or unique ID of the role. :param string name: The new role name :param domain_id: domain id - - :returns: a ``munch.Munch`` containing the role description - + :returns: an identity ``Role`` object :raise OpenStackCloudException: if the role cannot be created """ role = self.get_role(name_or_id, **kwargs) @@ -1036,11 +1143,9 @@ def update_role(self, name_or_id, name, **kwargs): def delete_role(self, name_or_id, **kwargs): """Delete a Keystone role. - :param string id: Name or id of the role to delete. + :param name_or_id: Name or unique ID of the role. :param domain_id: domain id (v3) - :returns: True if delete succeeded, False otherwise. - :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ @@ -1102,7 +1207,7 @@ def grant_role(self, name_or_id, user=None, group=None, timeout=60): """Grant a role to a user. - :param string name_or_id: The name or id of the role. + :param string name_or_id: Name or unique ID of the role. :param string user: The name or id of the user. :param string group: The name or id of the group. (v3) :param string project: The name or id of the project. @@ -1198,7 +1303,7 @@ def revoke_role(self, name_or_id, user=None, group=None, wait=False, timeout=60): """Revoke a role from a user. - :param string name_or_id: The name or id of the role. + :param string name_or_id: Name or unique ID of the role. :param string user: The name or id of the user. :param string group: The name or id of the group. (v3) :param string project: The name or id of the project. From a3b6dcdf4ff8ac71ce3345df83c7ed93de6492e4 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 26 May 2022 12:28:46 +0100 Subject: [PATCH 3045/3836] cloud: Update docstrings for object store functions Change-Id: I4c4193c94852ed790c2a90b1202cc1519d39df21 Signed-off-by: Stephen Finucane --- openstack/cloud/_object_store.py | 196 +++++++++++++++------------- openstack/object_store/v1/_proxy.py | 1 + 2 files changed, 105 insertions(+), 92 deletions(-) diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index aecdeb132..6f3783f6c 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -44,13 +44,14 @@ def _object_store_client(self): self._raw_clients['object-store'] = raw_client return self._raw_clients['object-store'] + # TODO(stephenfin): Remove 'full_listing' as it's a noop def list_containers(self, full_listing=True, prefix=None): """List containers. :param full_listing: Ignored. Present for backwards compat - - :returns: list of Munch of the container objects - + :param prefix: Only objects with this prefix will be returned. + (optional) + :returns: A list of object store ``Container`` objects. :raises: OpenStackCloudException on operation error. """ return list(self.object_store.containers(prefix=prefix)) @@ -58,15 +59,15 @@ def list_containers(self, full_listing=True, prefix=None): def search_containers(self, name=None, filters=None): """Search containers. - :param string name: container name. - :param filters: a dict containing additional filters to use. + :param string name: Container name. + :param filters: A dict containing additional filters to use. OR A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - :returns: a list of ``munch.Munch`` containing the containers. - - :raises: ``OpenStackCloudException``: if something goes wrong during + :returns: A list of object store ``Container`` objects matching the + search criteria. + :raises: ``OpenStackCloudException``: If something goes wrong during the OpenStack API call. """ containers = self.list_containers() @@ -78,8 +79,9 @@ def get_container(self, name, skip_cache=False): :param str name: Name of the container to get metadata for. :param bool skip_cache: - Ignore the cache of container metadata for this container.o + Ignore the cache of container metadata for this container. Defaults to ``False``. + :returns: An object store ``Container`` object if found, else None. """ if skip_cache or name not in self._container_cache: try: @@ -94,10 +96,10 @@ def get_container(self, name, skip_cache=False): def create_container(self, name, public=False): """Create an object-store container. - :param str name: - Name of the container to create. - :param bool public: - Whether to set this container to be public. Defaults to ``False``. + :param str name: Name of the container to create. + :param bool public: Whether to set this container to be public. + Defaults to ``False``. + :returns: The created object store ``Container`` object. """ container = self.get_container(name) if container: @@ -131,17 +133,8 @@ def delete_container(self, name): def update_container(self, name, headers): """Update the metadata in a container. - :param str name: - Name of the container to create. - :param dict headers: - Key/Value headers to set on the container. - """ - """Update the metadata in a container. - - :param str name: - Name of the container to update. - :param dict headers: - Key/Value headers to set on the container. + :param str name: Name of the container to update. + :param dict headers: Key/Value headers to set on the container. """ self.object_store.set_container_metadata( name, refresh=False, **headers) @@ -149,12 +142,10 @@ def update_container(self, name, headers): def set_container_access(self, name, access, refresh=False): """Set the access control list on a container. - :param str name: - Name of the container. - :param str access: - ACL string to set on the container. Can also be ``public`` - or ``private`` which will be translated into appropriate ACL - strings. + :param str name: Name of the container. + :param str access: ACL string to set on the container. Can also be + ``public`` or ``private`` which will be translated into appropriate + ACL strings. :param refresh: Flag to trigger refresh of the container properties """ if access not in OBJECT_CONTAINER_ACLS: @@ -171,6 +162,10 @@ def get_container_access(self, name): """Get the control list from a container. :param str name: Name of the container. + :returns: The contol list for the container. + :raises: :class:`~openstack.exceptions.OpenStackCloudException` if the + container was not found or container access could not be + determined. """ container = self.get_container(name, skip_cache=True) if not container: @@ -191,11 +186,17 @@ def get_object_capabilities(self): The object-storage service publishes a set of capabilities that include metadata about maximum values and thresholds. + + :returns: An object store ``Info`` object. """ return self.object_store.get_info() def get_object_segment_size(self, segment_size): - """Get a segment size that will work given capabilities""" + """Get a segment size that will work given capabilities. + + :param segment_size: + :returns: A segment size. + """ return self.object_store.get_object_segment_size(segment_size) def is_object_stale( @@ -205,12 +206,10 @@ def is_object_stale( :param container: Name of the container. :param name: Name of the object. :param filename: Path to the file. - :param file_md5: - Pre-calculated md5 of the file contents. Defaults to None which - means calculate locally. - :param file_sha256: - Pre-calculated sha256 of the file contents. Defaults to None which - means calculate locally. + :param file_md5: Pre-calculated md5 of the file contents. Defaults to + None which means calculate locally. + :param file_sha256: Pre-calculated sha256 of the file contents. + Defaults to None which means calculate locally. """ return self.object_store.is_object_stale( container, name, filename, @@ -236,6 +235,7 @@ def create_directory_marker_object(self, container, name, **headers): :param name: Name for the directory marker object within the container. :param headers: These will be passed through to the object creation API as HTTP Headers. + :returns: The created object store ``Object`` object. """ headers['content-type'] = 'application/directory' @@ -247,11 +247,19 @@ def create_directory_marker_object(self, container, name, **headers): **headers) def create_object( - self, container, name, filename=None, - md5=None, sha256=None, segment_size=None, - use_slo=True, metadata=None, - generate_checksums=None, data=None, - **headers): + self, + container, + name, + filename=None, + md5=None, + sha256=None, + segment_size=None, + use_slo=True, + metadata=None, + generate_checksums=None, + data=None, + **headers, + ): """Create a file object. Automatically uses large-object segments if needed. @@ -282,7 +290,7 @@ def create_object( uploads of identical data. (optional, defaults to True) :param metadata: This dict will get changed into headers that set metadata of the object - + :returns: The created object store ``Object`` object. :raises: ``OpenStackCloudException`` on operation error. """ return self.object_store.create_object( @@ -303,7 +311,7 @@ def update_object(self, container, name, metadata=None, **headers): metadata of the object :param headers: These will be passed through to the object update API as HTTP Headers. - + :returns: None :raises: ``OpenStackCloudException`` on operation error. """ meta = metadata.copy() or {} @@ -316,12 +324,9 @@ def list_objects(self, container, full_listing=True, prefix=None): :param container: Name of the container to list objects in. :param full_listing: Ignored. Present for backwards compat - :param string prefix: - only objects with this prefix will be returned. + :param prefix: Only objects with this prefix will be returned. (optional) - - :returns: list of Munch of the objects - + :returns: A list of object store ``Object`` objects. :raises: OpenStackCloudException on operation error. """ return list(self.object_store.objects( @@ -332,15 +337,15 @@ def list_objects(self, container, full_listing=True, prefix=None): def search_objects(self, container, name=None, filters=None): """Search objects. - :param string name: object name. - :param filters: a dict containing additional filters to use. + :param string name: Object name. + :param filters: A dict containing additional filters to use. OR A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - :returns: a list of ``munch.Munch`` containing the objects. - - :raises: ``OpenStackCloudException``: if something goes wrong during + :returns: A list of object store ``Object`` objects matching the + search criteria. + :raises: ``OpenStackCloudException``: If something goes wrong during the OpenStack API call. """ objects = self.list_objects(container) @@ -352,21 +357,23 @@ def delete_object(self, container, name, meta=None): :param string container: Name of the container holding the object. :param string name: Name of the object to delete. :param dict meta: Metadata for the object in question. (optional, will - be fetched if not provided) - + be fetched if not provided) :returns: True if delete succeeded, False if the object was not found. - :raises: OpenStackCloudException on operation error. """ try: self.object_store.delete_object( - name, ignore_missing=False, container=container) + name, ignore_missing=False, container=container, + ) return True except exceptions.SDKException: return False - def delete_autocreated_image_objects(self, container=None, - segment_prefix=None): + def delete_autocreated_image_objects( + self, + container=None, + segment_prefix=None, + ): """Delete all objects autocreated for image uploads. This method should generally not be needed, as shade should clean up @@ -379,12 +386,19 @@ def delete_autocreated_image_objects(self, container=None, :param str segment_prefix: Prefix for the image segment names to delete. If not given, all image upload segments present are deleted. + :returns: True if deletion was successful, else False. """ return self.object_store._delete_autocreated_image_objects( container, segment_prefix=segment_prefix ) def get_object_metadata(self, container, name): + """Get object metadata. + + :param container: + :param name: + :returns: The object metadata. + """ return self.object_store.get_object_metadata( name, container ).metadata @@ -392,13 +406,11 @@ def get_object_metadata(self, container, name): def get_object_raw(self, container, obj, query_string=None, stream=False): """Get a raw response object for an object. - :param string container: name of the container. - :param string obj: name of the object. - :param string query_string: - query args for uri. (delimiter, prefix, etc.) - :param bool stream: - Whether to stream the response or not. - + :param string container: Name of the container. + :param string obj: Name of the object. + :param string query_string: Query args for uri. (delimiter, prefix, + etc.) + :param bool stream: Whether to stream the response or not. :returns: A `requests.Response` :raises: OpenStackCloudException on operation error. """ @@ -418,18 +430,22 @@ def _get_object_endpoint(self, container, obj=None, query_string=None): return endpoint def stream_object( - self, container, obj, query_string=None, resp_chunk_size=1024): + self, + container, + obj, + query_string=None, + resp_chunk_size=1024, + ): """Download the content via a streaming iterator. - :param string container: name of the container. - :param string obj: name of the object. - :param string query_string: - query args for uri. (delimiter, prefix, etc.) - :param int resp_chunk_size: - chunk size of data to read. Only used if the results are - - :returns: - An iterator over the content or None if the object is not found. + :param string container: Name of the container. + :param string obj: Name of the object. + :param string query_string: Query args for uri. (delimiter, prefix, + etc.) + :param int resp_chunk_size: Chunk size of data to read. Only used if + the results are + :returns: An iterator over the content or None if the object is not + found. :raises: OpenStackCloudException on operation error. """ try: @@ -443,22 +459,19 @@ def get_object(self, container, obj, query_string=None, resp_chunk_size=1024, outfile=None, stream=False): """Get the headers and body of an object - :param string container: name of the container. - :param string obj: name of the object. - :param string query_string: - query args for uri. (delimiter, prefix, etc.) - :param int resp_chunk_size: - chunk size of data to read. Only used if the results are - being written to a file or stream is True. + :param string container: Name of the container. + :param string obj: Name of the object. + :param string query_string: Query args for uri. (delimiter, prefix, + etc.) + :param int resp_chunk_size: Chunk size of data to read. Only used if + the results are being written to a file or stream is True. (optional, defaults to 1k) - :param outfile: - Write the object to a file instead of returning the contents. - If this option is given, body in the return tuple will be None. - outfile can either be a file path given as a string, or a + :param outfile: Write the object to a file instead of returning the + contents. If this option is given, body in the return tuple will be + None. outfile can either be a file path given as a string, or a File like object. - :returns: Tuple (headers, body) of the object, or None if the object - is not found (404). + is not found (404). :raises: OpenStackCloudException on operation error. """ try: @@ -476,8 +489,7 @@ def get_object(self, container, obj, query_string=None, return None def _wait_for_futures(self, futures, raise_on_error=True): - '''Collect results or failures from a list of running future tasks.''' - + """Collect results or failures from a list of running future tasks.""" results = [] retries = [] diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index 54a304d85..83be650f2 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -996,6 +996,7 @@ def _delete_autocreated_image_objects( :param str segment_prefix: Prefix for the image segment names to delete. If not given, all image upload segments present are deleted. + :returns: True if deletion was succesful, else False. """ if container is None: container = self._connection._OBJECT_AUTOCREATE_CONTAINER From da0165b4b65d8e27d3a0e6db6f89bfc8da214c00 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 26 May 2022 12:54:48 +0100 Subject: [PATCH 3046/3836] cloud: Update docstrings for compute functions Change-Id: Ia033794bab1561d7bf8ba29b88e54bbebdf17cf0 Signed-off-by: Stephen Finucane --- openstack/cloud/_compute.py | 323 ++++++++++++++++++++++-------------- 1 file changed, 202 insertions(+), 121 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index a830c9b32..530b305e1 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -47,6 +47,11 @@ def _compute_region(self): return self.config.get_region_name('compute') def get_flavor_name(self, flavor_id): + """Get the name of a flavor. + + :param flavor_id: ID of the flavor. + :returns: The name of the flavor if a match if found, else None. + """ flavor = self.get_flavor(flavor_id, get_extra=False) if flavor: return flavor['name'] @@ -62,6 +67,10 @@ def get_flavor_by_ram(self, ram, include=None, get_extra=True): :param int ram: Minimum amount of RAM. :param string include: If given, will return a flavor whose name contains this string as a substring. + :param get_extra: + :returns: A compute ``Flavor`` object. + :raises: :class:`~openstack.exceptions.OpenStackCloudException` if no + matching flavour could be found. """ flavors = self.list_flavors(get_extra=get_extra) for flavor in sorted(flavors, key=operator.itemgetter('ram')): @@ -81,30 +90,55 @@ def _has_nova_extension(self, extension_name): return extension_name in self._nova_extensions() def search_keypairs(self, name_or_id=None, filters=None): + """Search keypairs. + + :param name_or_id: + :param filters: + :returns: A list of compute ``Keypair`` objects matching the search + criteria. + """ keypairs = self.list_keypairs( filters=filters if isinstance(filters, dict) else None ) return _utils._filter_list(keypairs, name_or_id, filters) def search_flavors(self, name_or_id=None, filters=None, get_extra=True): + """Search flavors. + + :param name_or_id: + :param flavors: + :param get_extra: + :returns: A list of compute ``Flavor`` objects matching the search + criteria. + """ flavors = self.list_flavors(get_extra=get_extra) return _utils._filter_list(flavors, name_or_id, filters) def search_servers( - self, name_or_id=None, filters=None, detailed=False, - all_projects=False, bare=False): + self, name_or_id=None, filters=None, detailed=False, + all_projects=False, bare=False, + ): + """Search servers. + + :param name_or_id: + :param filters: + :param detailed: + :param all_projects: + :param bare: + :returns: A list of compute ``Server`` objects matching the search + criteria. + """ servers = self.list_servers( detailed=detailed, all_projects=all_projects, bare=bare) return _utils._filter_list(servers, name_or_id, filters) def search_server_groups(self, name_or_id=None, filters=None): - """Seach server groups. - - :param name: server group name or ID. - :param filters: a dict containing additional filters to use. - - :returns: a list of dicts containing the server groups + """Search server groups. + :param name_or_id: Name or unique ID of the server group(s). + :param filters: A dict containing additional filters to use. + :returns: A list of compute ``ServerGroup`` objects matching the search + criteria. :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ @@ -114,8 +148,8 @@ def search_server_groups(self, name_or_id=None, filters=None): def list_keypairs(self, filters=None): """List all available keypairs. - :returns: A list of ``munch.Munch`` containing keypair info. - + :param filters: + :returns: A list of compute ``Keypair`` objects. """ if not filters: filters = {} @@ -128,7 +162,6 @@ def list_availability_zone_names(self, unavailable=False): :param bool unavailable: Whether or not to include unavailable zones in the output. Defaults to False. - :returns: A list of availability zone names, or an empty list if the list could not be fetched. """ @@ -152,12 +185,10 @@ def list_flavors(self, get_extra=False): :param get_extra: Whether or not to fetch extra specs for each flavor. Defaults to True. Default behavior value can be overridden in clouds.yaml by setting openstack.cloud.get_extra_specs to False. - :returns: A list of flavor ``munch.Munch``. - + :returns: A list of compute ``Flavor`` objects. """ - flavors = list(self.compute.flavors( + return list(self.compute.flavors( details=True, get_extra_specs=get_extra)) - return flavors def list_server_security_groups(self, server): """List all security groups associated with the given server. @@ -215,7 +246,6 @@ def add_server_security_groups(self, server, security_groups): :returns: False if server or security groups are undefined, True otherwise. - :raises: ``OpenStackCloudException``, on operation error. """ server, security_groups = self._get_server_security_groups( @@ -238,7 +268,6 @@ def remove_server_security_groups(self, server, security_groups): :returns: False if server or security groups are undefined, True otherwise. - :raises: ``OpenStackCloudException``, on operation error. """ server, security_groups = self._get_server_security_groups( @@ -264,8 +293,13 @@ def remove_server_security_groups(self, server, security_groups): return ret - def list_servers(self, detailed=False, all_projects=False, bare=False, - filters=None): + def list_servers( + self, + detailed=False, + all_projects=False, + bare=False, + filters=None, + ): """List all available servers. :param detailed: Whether or not to add detailed additional information. @@ -277,9 +311,7 @@ def list_servers(self, detailed=False, all_projects=False, bare=False, be populated as needed from neutron. Setting to True implies detailed = False. :param filters: Additional query parameters passed to the API server. - - :returns: A list of server ``munch.Munch``. - + :returns: A list of compute ``Server`` objects. """ # If pushdown filters are specified and we do not have batched caching # enabled, bypass local caching and push down the filters. @@ -328,19 +360,17 @@ def _list_servers(self, detailed=False, all_projects=False, bare=False, def list_server_groups(self): """List all available server groups. - :returns: A list of server group dicts. - + :returns: A list of compute ``ServerGroup`` objects. """ return list(self.compute.server_groups()) def get_compute_limits(self, name_or_id=None): - """ Get absolute compute limits for a project + """Get absolute compute limits for a project :param name_or_id: (optional) project name or ID to get limits for if different from the current project :raises: OpenStackCloudException if it's not a valid project - - :returns: + :returns: A compute :class:`~openstack.compute.v2.limits.Limits.AbsoluteLimits` object. """ params = {} @@ -358,9 +388,9 @@ def get_keypair(self, name_or_id, filters=None): """Get a keypair by name or ID. :param name_or_id: Name or ID of the keypair. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: + :param filters: A dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. + Example:: { 'last_name': 'Smith', @@ -373,8 +403,7 @@ def get_keypair(self, name_or_id, filters=None): A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - :returns: A keypair ``munch.Munch`` or None if no matching keypair is - found. + :returns: A compute ``Keypair`` object if found, else None. """ return _utils._get_entity(self, 'keypair', name_or_id, filters) @@ -382,9 +411,9 @@ def get_flavor(self, name_or_id, filters=None, get_extra=True): """Get a flavor by name or ID. :param name_or_id: Name or ID of the flavor. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: + :param filters: A dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. + Example:: { 'last_name': 'Smith', @@ -396,12 +425,10 @@ def get_flavor(self, name_or_id, filters=None, get_extra=True): OR A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :param get_extra: Whether or not the list_flavors call should get the extra flavor specs. - - :returns: A flavor ``munch.Munch`` or None if no matching flavor is - found. - + :returns: A compute ``Flavor`` object if found, else None. """ if not filters: filters = {} @@ -414,10 +441,9 @@ def get_flavor_by_id(self, id, get_extra=False): """ Get a flavor by ID :param id: ID of the flavor. - :param get_extra: - Whether or not the list_flavors call should get the extra flavor - specs. - :returns: A flavor ``munch.Munch``. + :param get_extra: Whether or not the list_flavors call should get the + extra flavor specs. + :returns: A compute ``Flavor`` object if found, else None. """ return self.compute.get_flavor(id, get_extra_specs=get_extra) @@ -456,8 +482,13 @@ def _get_server_console_output(self, server_id, length=None): return output['output'] def get_server( - self, name_or_id=None, filters=None, detailed=False, bare=False, - all_projects=False): + self, + name_or_id=None, + filters=None, + detailed=False, + bare=False, + all_projects=False, + ): """Get a server by name or ID. :param name_or_id: Name or ID of the server. @@ -483,10 +514,7 @@ def get_server( detailed = False. :param all_projects: Whether to get server from all projects or just the current auth scoped project. - - :returns: A server ``munch.Munch`` or None if no matching server is - found. - + :returns: A compute ``Server`` object if found, else None. """ searchfunc = functools.partial(self.search_servers, detailed=detailed, bare=True, @@ -507,7 +535,7 @@ def get_server_by_id(self, id): :param id: ID of the server. - :returns: A server object or None if no matching server is found. + :returns: A compute ``Server`` object if found, else None. """ try: server = self.compute.get_server(id) @@ -519,9 +547,9 @@ def get_server_group(self, name_or_id=None, filters=None): """Get a server group by name or ID. :param name_or_id: Name or ID of the server group. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: + :param filters: A dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. + Example:: { 'policy': 'affinity', @@ -531,9 +559,7 @@ def get_server_group(self, name_or_id=None, filters=None): A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - :returns: A server groups dict or None if no matching server group - is found. - + :returns: A compute ``ServerGroup`` object if found, else None. """ return _utils._get_entity(self, 'server_group', name_or_id, filters) @@ -543,7 +569,7 @@ def create_keypair(self, name, public_key=None): :param name: Name of the keypair being created. :param public_key: Public key for the new keypair. - + :returns: The created compute ``Keypair`` object. :raises: OpenStackCloudException on operation error. """ keypair = { @@ -570,7 +596,13 @@ def delete_keypair(self, name): return True def create_image_snapshot( - self, name, server, wait=False, timeout=3600, **metadata): + self, + name, + server, + wait=False, + timeout=3600, + **metadata, + ): """Create an image by snapshotting an existing server. ..note:: @@ -585,9 +617,7 @@ def create_image_snapshot( :param wait: If true, waits for image to be created. :param timeout: Seconds to wait for image creation. None is forever. :param metadata: Metadata to give newly-created image entity - - :returns: A ``munch.Munch`` of the Image object - + :returns: The created image ``Image`` object. :raises: OpenStackCloudException if there are problems uploading """ if not isinstance(server, dict): @@ -602,18 +632,38 @@ def create_image_snapshot( return image def get_server_id(self, name_or_id): + """Get the ID of a server. + + :param name_or_id: + :returns: The name of the server if found, else None. + """ server = self.get_server(name_or_id, bare=True) if server: return server['id'] return None def get_server_private_ip(self, server): + """Get the private IP of a server. + + :param server: + :returns: The private IP of the server if set, else None. + """ return meta.get_server_private_ip(server, self) def get_server_public_ip(self, server): + """Get the public IP of a server. + + :param server: + :returns: The public IP of the server if set, else None. + """ return meta.get_server_external_ipv4(self, server) def get_server_meta(self, server): + """Get the metadata for a server. + + :param server: + :returns: The metadata for the server if found, else None. + """ # TODO(mordred) remove once ansible has moved to Inventory interface server_vars = meta.get_hostvars_from_server(self, server) groups = meta.get_groups_from_server(self, server, server_vars) @@ -627,14 +677,27 @@ def get_server_meta(self, server): 'block_device_mapping_v2', 'nics', 'scheduler_hints', 'config_drive', 'admin_pass', 'disk_config') def create_server( - self, name, image=None, flavor=None, - auto_ip=True, ips=None, ip_pool=None, - root_volume=None, terminate_volume=False, - wait=False, timeout=180, reuse_ips=True, - network=None, boot_from_volume=False, volume_size='50', - boot_volume=None, volumes=None, nat_destination=None, - group=None, - **kwargs): + self, + name, + image=None, + flavor=None, + auto_ip=True, + ips=None, + ip_pool=None, + root_volume=None, + terminate_volume=False, + wait=False, + timeout=180, + reuse_ips=True, + network=None, + boot_from_volume=False, + volume_size='50', + boot_volume=None, + volumes=None, + nat_destination=None, + group=None, + **kwargs, + ): """Create a virtual server instance. :param name: Something to name the server. @@ -709,7 +772,7 @@ def create_server( :param group: ServerGroup dict, name or id to boot the server in. If a group is provided in both scheduler_hints and in the group param, the group param will win. (Optional, defaults to None) - :returns: A ``munch.Munch`` representing the created server. + :returns: The created compute ``Server`` object. :raises: OpenStackCloudException on operation error. """ # TODO(shade) Image is optional but flavor is not - yet flavor comes @@ -1023,9 +1086,16 @@ def wait_for_server( return server def get_active_server( - self, server, auto_ip=True, ips=None, ip_pool=None, - reuse=True, wait=False, timeout=180, nat_destination=None): - + self, + server, + auto_ip=True, + ips=None, + ip_pool=None, + reuse=True, + wait=False, + timeout=180, + nat_destination=None, + ): if server['status'] == 'ERROR': if 'fault' in server and 'message' in server['fault']: raise exc.OpenStackCloudException( @@ -1065,9 +1135,27 @@ def get_active_server( extra_data=dict(server=server)) return None - def rebuild_server(self, server_id, image_id, admin_pass=None, - detailed=False, bare=False, - wait=False, timeout=180): + def rebuild_server( + self, + server_id, + image_id, + admin_pass=None, + detailed=False, + bare=False, + wait=False, + timeout=180, + ): + """Rebuild a server. + + :param server_id: + :param image_id: + :param admin_pass: + :param detailed: + :param bare: + :param wait: + :param timeout: + :returns: A compute ``Server`` object. + """ kwargs = {} if image_id: kwargs['image'] = image_id @@ -1097,7 +1185,7 @@ def set_server_metadata(self, name_or_id, metadata): :param dict metadata: A dictionary with the key=value pairs to set in the server instance. It only updates the key=value pairs provided. Existing ones will remain untouched. - + :returns: None :raises: OpenStackCloudException on operation error. """ server = self.get_server(name_or_id, bare=True) @@ -1114,7 +1202,7 @@ def delete_server_metadata(self, name_or_id, metadata_keys): to update. :param metadata_keys: A list with the keys to be deleted from the server instance. - + :returns: None :raises: OpenStackCloudException on operation error. """ server = self.get_server(name_or_id, bare=True) @@ -1126,8 +1214,13 @@ def delete_server_metadata(self, name_or_id, metadata_keys): keys=metadata_keys) def delete_server( - self, name_or_id, wait=False, timeout=180, delete_ips=False, - delete_ip_retry=1): + self, + name_or_id, + wait=False, + timeout=180, + delete_ips=False, + delete_ip_retry=1, + ): """Delete a server instance. :param name_or_id: name or ID of the server to delete @@ -1137,10 +1230,8 @@ def delete_server( associated with the instance. :param int delete_ip_retry: Number of times to retry deleting any floating ips, should the first try be unsuccessful. - :returns: True if delete succeeded, False otherwise if the server does not exist. - :raises: OpenStackCloudException on operation error. """ # If delete_ips is True, we need the server to not be bare. @@ -1243,9 +1334,7 @@ def update_server(self, name_or_id, detailed=False, bare=False, **kwargs): detailed = False. :param name: New name for the server :param description: New description for the server - - :returns: a dictionary representing the updated server. - + :returns: The updated compute ``Server`` object. :raises: OpenStackCloudException on operation error. """ server = self.compute.find_server( @@ -1258,14 +1347,12 @@ def update_server(self, name_or_id, detailed=False, bare=False, **kwargs): return self._expand_server(server, bare=bare, detailed=detailed) - def create_server_group(self, name, policies=[], policy=None): + def create_server_group(self, name, policies=None, policy=None): """Create a new server group. :param name: Name of the server group being created :param policies: List of policies for the server group. - - :returns: a dict representing the new server group. - + :returns: The created compute ``ServerGroup`` object. :raises: OpenStackCloudException on operation error. """ sg_attrs = { @@ -1283,9 +1370,7 @@ def delete_server_group(self, name_or_id): """Delete a server group. :param name_or_id: Name or ID of the server group to delete - :returns: True if delete succeeded, False otherwise - :raises: OpenStackCloudException on operation error. """ server_group = self.get_server_group(name_or_id) @@ -1297,8 +1382,18 @@ def delete_server_group(self, name_or_id): self.compute.delete_server_group(server_group, ignore_missing=False) return True - def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", - ephemeral=0, swap=0, rxtx_factor=1.0, is_public=True): + def create_flavor( + self, + name, + ram, + vcpus, + disk, + flavorid="auto", + ephemeral=0, + swap=0, + rxtx_factor=1.0, + is_public=True, + ): """Create a new flavor. :param name: Descriptive name of the flavor @@ -1310,9 +1405,7 @@ def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", :param swap: Swap space in MB :param rxtx_factor: RX/TX factor :param is_public: Make flavor accessible to the public - - :returns: A ``munch.Munch`` describing the new flavor. - + :returns: The created compute ``Flavor`` object. :raises: OpenStackCloudException on operation error. """ attrs = { @@ -1335,9 +1428,7 @@ def delete_flavor(self, name_or_id): """Delete a flavor :param name_or_id: ID or name of the flavor to delete. - :returns: True if delete succeeded, False otherwise. - :raises: OpenStackCloudException on operation error. """ try: @@ -1399,9 +1490,7 @@ def list_flavor_access(self, flavor_id): """List access from a private flavor for a project/tenant. :param string flavor_id: ID of the private flavor. - - :returns: a list of ``munch.Munch`` containing the access description - + :returns: List of dicts with flavor_id and tenant_id attributes. :raises: OpenStackCloudException on operation error. """ return self.compute.get_flavor_access(flavor_id) @@ -1409,7 +1498,8 @@ def list_flavor_access(self, flavor_id): def list_hypervisors(self, filters={}): """List all hypervisors - :returns: A list of hypervisor ``munch.Munch``. + :param filters: + :returns: A list of compute ``Hypervisor`` objects. """ return list(self.compute.hypervisors( @@ -1422,9 +1512,8 @@ def search_aggregates(self, name_or_id=None, filters=None): :param name: aggregate name or id. :param filters: a dict containing additional filters to use. - - :returns: a list of dicts containing the aggregates - + :returns: A list of compute ``Aggregate`` objects matching the search + criteria. :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ @@ -1434,11 +1523,11 @@ def search_aggregates(self, name_or_id=None, filters=None): def list_aggregates(self, filters={}): """List all available host aggregates. - :returns: A list of aggregate dicts. - + :returns: A list of compute ``Aggregate`` objects. """ return self.compute.aggregates(allow_unknown_params=True, **filters) + # TODO(stephenfin): This shouldn't return a munch def get_aggregate(self, name_or_id, filters=None): """Get an aggregate by name or ID. @@ -1456,7 +1545,6 @@ def get_aggregate(self, name_or_id, filters=None): :returns: An aggregate dict or None if no matching aggregate is found. - """ aggregate = self.compute.find_aggregate( name_or_id, ignore_missing=True) @@ -1468,9 +1556,7 @@ def create_aggregate(self, name, availability_zone=None): :param name: Name of the host aggregate being created :param availability_zone: Availability zone to assign hosts - - :returns: a dict representing the new host aggregate. - + :returns: The created compute ``Aggregate`` object. :raises: OpenStackCloudException on operation error. """ return self.compute.create_aggregate( @@ -1485,9 +1571,7 @@ def update_aggregate(self, name_or_id, **kwargs): :param name_or_id: Name or ID of the aggregate being updated. :param name: New aggregate name :param availability_zone: Availability zone to assign to hosts - - :returns: a dict representing the updated host aggregate. - + :returns: The updated compute ``Aggregate`` object. :raises: OpenStackCloudException on operation error. """ aggregate = self.get_aggregate(name_or_id) @@ -1497,9 +1581,7 @@ def delete_aggregate(self, name_or_id): """Delete a host aggregate. :param name_or_id: Name or ID of the host aggregate to delete. - :returns: True if delete succeeded, False otherwise. - :raises: OpenStackCloudException on operation error. """ if ( @@ -1588,9 +1670,8 @@ def get_compute_quotas(self, name_or_id): """ Get quota for a project :param name_or_id: project name or id + :returns: A compute ``QuotaSet`` object if found, else None. :raises: OpenStackCloudException if it's not a valid project - - :returns: Munch object with the quotas """ proj = self.identity.find_project( name_or_id, ignore_missing=False) @@ -1602,13 +1683,13 @@ def delete_compute_quotas(self, name_or_id): :param name_or_id: project name or id :raises: OpenStackCloudException if it's not a valid project or the nova client call failed - - :returns: dict with the quotas + :returns: None """ proj = self.identity.find_project( name_or_id, ignore_missing=False) - return self.compute.revert_quota_set(proj) + self.compute.revert_quota_set(proj) + # TODO(stephenfin): Convert to proxy methods def get_compute_usage(self, name_or_id, start=None, end=None): """ Get usage for a specific project From 0876a2486abc7def60917e95279655a7eda93cd4 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 25 Oct 2021 19:46:39 +0100 Subject: [PATCH 3047/3836] compute: Correct some docstrings Fix some copy-paste mistakes. Change-Id: Ie97ed490557ce4f2d04fa138111e6a3665790beb Signed-off-by: Stephen Finucane --- openstack/compute/v2/_proxy.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 8dd7e3807..cab0a2619 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -122,9 +122,9 @@ def delete_flavor(self, flavor, ignore_missing=True): def update_flavor(self, flavor, **attrs): """Update a flavor - :param server: Either the ID of a flavot or a + :param flavor: Either the ID of a flavor or a :class:`~openstack.compute.v2.flavor.Flavor` instance. - :attrs kwargs: The attributes to update on the flavor represented + :param attrs: The attributes to update on the flavor represented by ``flavor``. :returns: The updated flavor @@ -321,9 +321,8 @@ def update_aggregate(self, aggregate, **attrs): """Update a host aggregate :param server: Either the ID of a host aggregate or a - :class:`~openstack.compute.v2.aggregate.Aggregate` - instance. - :attrs kwargs: The attributes to update on the aggregate represented + :class:`~openstack.compute.v2.aggregate.Aggregate` instance. + :param attrs: The attributes to update on the aggregate represented by ``aggregate``. :returns: The updated aggregate @@ -697,7 +696,7 @@ def update_server(self, server, **attrs): :param server: Either the ID of a server or a :class:`~openstack.compute.v2.server.Server` instance. - :attrs kwargs: The attributes to update on the server represented + :param attrs: The attributes to update on the server represented by ``server``. :returns: The updated server @@ -1543,9 +1542,9 @@ def delete_service(self, service, ignore_missing=True): def update_service(self, service, **attrs): """Update a service - :param server: Either the ID of a service or a + :param service: Either the ID of a service or a :class:`~openstack.compute.v2.service.Service` instance. - :attrs kwargs: The attributes to update on the service represented + :param attrs: The attributes to update on the service represented by ``service``. :returns: The updated service @@ -2000,7 +1999,7 @@ def update_quota_set(self, quota_set, query=None, **attrs): :param quota_set: Either the ID of a quota_set or a :class:`~openstack.compute.v2.quota_set.QuotaSet` instance. :param dict query: Optional parameters to be used with update call. - :attrs kwargs: The attributes to update on the QuotaSet represented + :params attrs: The attributes to update on the QuotaSet represented by ``quota_set``. :returns: The updated QuotaSet From 6d8be2b2dff1f774408dac9ae02f4e52baeb3c38 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Sat, 23 Oct 2021 15:56:29 +0100 Subject: [PATCH 3048/3836] compute: Add support for instance actions These become server actions, in keeping with our preference for using "server" rather than "instance". This is a read-only API so relatively easy to implement. Change-Id: I97d885cbaf99862cff801816c306e2d95b44e7ce Signed-off-by: Stephen Finucane --- openstack/compute/v2/_proxy.py | 44 +++++++++ openstack/compute/v2/server_action.py | 88 ++++++++++++++++++ openstack/tests/unit/compute/v2/test_proxy.py | 24 +++++ .../unit/compute/v2/test_server_actions.py | 91 +++++++++++++++++++ 4 files changed, 247 insertions(+) create mode 100644 openstack/compute/v2/server_action.py create mode 100644 openstack/tests/unit/compute/v2/test_server_actions.py diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index cab0a2619..f1e6de584 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -23,6 +23,7 @@ from openstack.compute.v2 import migration as _migration from openstack.compute.v2 import quota_set as _quota_set from openstack.compute.v2 import server as _server +from openstack.compute.v2 import server_action as _server_action from openstack.compute.v2 import server_diagnostics as _server_diagnostics from openstack.compute.v2 import server_group as _server_group from openstack.compute.v2 import server_interface as _server_interface @@ -2010,6 +2011,49 @@ def update_quota_set(self, quota_set, query=None, **attrs): query = {} return res.commit(self, **query) + # ========== Server actions ========== + + def get_server_action(self, server_action, server, ignore_missing=True): + """Get a single server action + + :param server_action: The value can be the ID of a server action or a + :class:`~openstack.compute.v2.server_action.ServerAction` instance. + :param server: This parameter need to be specified when ServerAction ID + is given as value. It can be either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance that the + action is associated with. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the server action does not exist. When set to ``True``, no + exception will be set when attempting to retrieve a non-existent + server action. + + :returns: One :class:`~openstack.compute.v2.server_action.ServerAction` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource can be found. + """ + server_id = self._get_uri_attribute(server_action, server, 'server_id') + server_action = resource.Resource._get_id(server_action) + + return self._get( + _server_action.ServerAction, + server_id=server_id, + action_id=server_action, + ignore_missing=ignore_missing, + ) + + def server_actions(self, server): + """Return a generator of server actions + + :param server: The server can be either the ID of a server or a + :class:`~openstack.compute.v2.server.Server`. + + :returns: A generator of ServerAction objects + :rtype: :class:`~openstack.compute.v2.server_action.ServerAction` + """ + server_id = resource.Resource._get_id(server) + return self._list(_server_action.ServerAction, server_id=server_id) + # ========== Utilities ========== def wait_for_server( diff --git a/openstack/compute/v2/server_action.py b/openstack/compute/v2/server_action.py new file mode 100644 index 000000000..fff96ae88 --- /dev/null +++ b/openstack/compute/v2/server_action.py @@ -0,0 +1,88 @@ +# 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 resource + + +class ServerActionEvent(resource.Resource): + + # Added the 'details' field in 2.84 + _max_microversion = '2.84' + + #: The name of the event + event = resource.Body('event') + #: The date and time when the event was started. The date and time stamp + #: format is ISO 8601 + start_time = resource.Body('start_time') + #: The date and time when the event finished. The date and time stamp + #: format is ISO 8601 + finish_time = resource.Body('finish_time') + #: The result of the event + result = resource.Body('result') + #: The traceback stack if an error occurred in this event. + #: This is only visible to cloud admins by default. + traceback = resource.Body('traceback') + #: The name of the host on which the event occurred. + #: This is only visible to cloud admins by default. + host = resource.Body('host') + #: An obfuscated hashed host ID string, or the empty string if there is no + #: host for the event. This is a hashed value so will not actually look + #: like a hostname, and is hashed with data from the project_id, so the + #: same physical host as seen by two different project_ids will be + #: different. This is useful when within the same project you need to + #: determine if two events occurred on the same or different physical + #: hosts. + host_id = resource.Body('hostId') + #: Details of the event. May be unset. + details = resource.Body('details') + + +class ServerAction(resource.Resource): + resource_key = 'instanceAction' + resources_key = 'instanceActions' + base_path = '/servers/{server_id}/os-instance-actions' + + # capabilities + allow_fetch = True + allow_list = True + + # Properties + + #: The ID of the server that this action relates to. + server_id = resource.URI('server_id') + + #: The name of the action. + action = resource.Body('action') + # FIXME(stephenfin): This conflicts since there is a server ID in the URI + # *and* in the body. We need a field that handles both or we need to use + # different names. + # #: The ID of the server that this action relates to. + # server_id = resource.Body('instance_uuid') + #: The ID of the request that this action related to. + request_id = resource.Body('request_id') + #: The ID of the user which initiated the server action. + user_id = resource.Body('user_id') + #: The ID of the project that this server belongs to. + project_id = resource.Body('project_id') + start_time = resource.Body('start_time') + #: The related error message for when an action fails. + message = resource.Body('message') + #: Events + events = resource.Body('events', type=list, list_type=ServerActionEvent) + + # events.details field added in 2.84 + _max_microversion = '2.84' + + _query_mapping = resource.QueryParameters( + changes_since="changes-since", + changes_before="changes-before", + ) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 303831485..bf697cff6 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -23,6 +23,7 @@ from openstack.compute.v2 import migration from openstack.compute.v2 import quota_set from openstack.compute.v2 import server +from openstack.compute.v2 import server_action from openstack.compute.v2 import server_group from openstack.compute.v2 import server_interface from openstack.compute.v2 import server_ip @@ -1243,3 +1244,26 @@ def test_update(self, gr_mock): quota_set.QuotaSet, 'qs', a='b' ) + + +class TestServerAction(TestComputeProxy): + + def test_server_action_get(self): + self._verify( + 'openstack.proxy.Proxy._get', + self.proxy.get_server_action, + method_args=['action_id'], + method_kwargs={'server': 'server_id'}, + expected_args=[server_action.ServerAction], + expected_kwargs={ + 'action_id': 'action_id', 'server_id': 'server_id', + }, + ) + + def test_server_actions(self): + self.verify_list( + self.proxy.server_actions, + server_action.ServerAction, + method_kwargs={'server': 'server_a'}, + expected_kwargs={'server_id': 'server_a'}, + ) diff --git a/openstack/tests/unit/compute/v2/test_server_actions.py b/openstack/tests/unit/compute/v2/test_server_actions.py new file mode 100644 index 000000000..bb34e2661 --- /dev/null +++ b/openstack/tests/unit/compute/v2/test_server_actions.py @@ -0,0 +1,91 @@ +# 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 unittest import mock + +from openstack.compute.v2 import server_action +from openstack.tests.unit import base + +EXAMPLE = { + 'action': 'stop', + 'events': [ + { + 'event': 'compute_stop_instance', + 'finish_time': '2018-04-25T01:26:36.790544', + 'host': 'compute', + 'hostId': '2091634baaccdc4c5a1d57069c833e402921df696b7f970791b12ec6', # noqa: E501 + 'result': 'Success', + 'start_time': '2018-04-25T01:26:36.539271', + 'traceback': None, + 'details': None + } + ], + 'instance_uuid': '4bf3473b-d550-4b65-9409-292d44ab14a2', + 'message': None, + 'project_id': '6f70656e737461636b20342065766572', + 'request_id': 'req-0d819d5c-1527-4669-bdf0-ffad31b5105b', + 'start_time': '2018-04-25T01:26:36.341290', + 'updated_at': '2018-04-25T01:26:36.790544', + 'user_id': 'admin', +} + + +class TestServerAction(base.TestCase): + + def setUp(self): + super().setUp() + self.resp = mock.Mock() + self.resp.body = None + self.resp.json = mock.Mock(return_value=self.resp.body) + self.resp.status_code = 200 + self.sess = mock.Mock() + self.sess.post = mock.Mock(return_value=self.resp) + + def test_basic(self): + sot = server_action.ServerAction() + self.assertEqual('instanceAction', sot.resource_key) + self.assertEqual('instanceActions', sot.resources_key) + self.assertEqual( + '/servers/{server_id}/os-instance-actions', + sot.base_path, + ) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_list) + self.assertFalse(sot.allow_create) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_delete) + + self.assertDictEqual( + { + 'changes_before': 'changes-before', + 'changes_since': 'changes-since', + 'limit': 'limit', + 'marker': 'marker', + }, + sot._query_mapping._mapping, + ) + + def test_make_it(self): + sot = server_action.ServerAction(**EXAMPLE) + self.assertEqual(EXAMPLE['action'], sot.action) + # FIXME: This isn't populated since it conflicts with the server_id URI + # argument + # self.assertEqual(EXAMPLE['instance_uuid'], sot.server_id) + self.assertEqual(EXAMPLE['message'], sot.message) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + self.assertEqual(EXAMPLE['request_id'], sot.request_id) + self.assertEqual(EXAMPLE['start_time'], sot.start_time) + self.assertEqual(EXAMPLE['user_id'], sot.user_id) + self.assertEqual( + [server_action.ServerActionEvent(**e) for e in EXAMPLE['events']], + sot.events, + ) From 1cf2c60aa72dec747b3091df4217f430ca9e531e Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 30 May 2022 15:05:03 +0200 Subject: [PATCH 3049/3836] Warn when no statsd library available When statsd python library is not available and metrics emiting is enabled SDK will silently skip metrics. This can lead to frustration trying to explain absence of metrics. Throw a warning when statsd host is set but lib is not available. Change-Id: I30f48bb43814d13af929976393a3b85d27568123 --- openstack/config/cloud_region.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 2a269376a..55a1bd98a 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -1035,6 +1035,10 @@ def get_concurrency(self, service_type=None): def get_statsd_client(self): if not statsd: + if self._statsd_host: + self.log.warning( + 'StatsD python library is not available. ' + 'Reporting disabled') return None statsd_args = {} if self._statsd_host: From db84b7559cb50d734842c95fcd8f283c897cc780 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 2 Jun 2022 12:15:12 +0200 Subject: [PATCH 3050/3836] Fix object upload for RAX In Rackspace Swift it is required to set access-control-allow-origin header instead of x-object-meta-access-control-allow-origin. Since with switch to the proxy layer we now do more validation it is being simply ignored and that hurts when Zuul uploads logs to rax. Fix this by registering this header with explicit test. Change-Id: I44d61136387acabb9fa8fee5009be637fa8fbaee --- openstack/object_store/v1/obj.py | 10 ++++-- openstack/tests/unit/cloud/test_object.py | 38 +++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/openstack/object_store/v1/obj.py b/openstack/object_store/v1/obj.py index 0c7836101..567e02094 100644 --- a/openstack/object_store/v1/obj.py +++ b/openstack/object_store/v1/obj.py @@ -28,7 +28,9 @@ class Object(_base.BaseResource): "delete_after": "x-delete-after", "delete_at": "x-delete-at", "is_content_type_detected": "x-detect-content-type", - "manifest": "x-object-manifest" + "manifest": "x-object-manifest", + # Rax hack - the need CORS as different header + "access_control_allow_origin": "access-control-allow-origin" } base_path = "/%(container)s" @@ -188,6 +190,10 @@ class Object(_base.BaseResource): #: value. symlink_target_account = resource.Header("x-symlink-target-account") + #: CORS for RAX (deviating from standard) + access_control_allow_origin = resource.Header( + "access-control-allow-origin") + has_body = False def __init__(self, data=None, **attrs): @@ -300,7 +306,7 @@ def stream(self, session, error_message=None, chunk_size=1024): session, error_message=error_message, stream=True) return response.iter_content(chunk_size, decode_unicode=False) - def create(self, session, base_path=None): + def create(self, session, base_path=None, **params): request = self._prepare_request(base_path=base_path) response = session.put( diff --git a/openstack/tests/unit/cloud/test_object.py b/openstack/tests/unit/cloud/test_object.py index 0a74731a6..8635c951b 100644 --- a/openstack/tests/unit/cloud/test_object.py +++ b/openstack/tests/unit/cloud/test_object.py @@ -645,6 +645,18 @@ def test_get_object_segment_size_http_412(self): self.cloud.get_object_segment_size(None)) self.assert_calls() + def test_update_container_cors(self): + headers = { + 'X-Container-Meta-Web-Index': 'index.html', + 'X-Container-Meta-Access-Control-Allow-Origin': '*' + } + self.register_uris([ + dict(method='POST', uri=self.container_endpoint, + status_code=204, + validate=dict(headers=headers))]) + self.cloud.update_container(self.container, headers=headers) + self.assert_calls() + class TestObjectUploads(BaseTestObject): @@ -690,6 +702,32 @@ def test_create_object(self): self.assert_calls() + def test_create_object_index_rax(self): + + self.register_uris([ + dict(method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, object='index.html'), + status_code=201, + validate=dict( + headers={ + 'access-control-allow-origin': '*', + 'content-type': 'text/html' + })) + ]) + + headers = { + 'access-control-allow-origin': '*', + 'content-type': 'text/html' + } + self.cloud.create_object( + self.container, name='index.html', + data='', + **headers) + + self.assert_calls() + def test_create_directory_marker_object(self): self.register_uris([ From 9eaab818a20699e24c8ffe852079ce084392687f Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 2 Jun 2022 15:22:44 +0200 Subject: [PATCH 3051/3836] Make nodepool jobs non voting We ended up in dead loop with Zuul needed to pin older SDK version due to some found issues, nodepool not able to cope with pin on its own and sdk not able to merge any fixes for that. Make nodepool jobs non voting for now unless we get out of problems. Squash increasing of swap for devstack https://review.opendev.org/c/openstack/openstacksdk/+/844010 to get things rolling Change-Id: Ie8af2690f309df01022843a6fa934f0c548f32c3 --- .zuul.yaml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 807502517..1e27a101d 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -137,6 +137,7 @@ - openstack/designate - openstack/octavia vars: + configure_swap_size: 4096 devstack_local_conf: post-config: $OCTAVIA_CONF: @@ -451,8 +452,10 @@ check: jobs: - opendev-buildset-registry - - nodepool-build-image-siblings - - dib-nodepool-functional-openstack-centos-8-stream-src + - nodepool-build-image-siblings: + voting: false + - dib-nodepool-functional-openstack-centos-8-stream-src: + voting: false - openstacksdk-functional-devstack - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-senlin @@ -480,8 +483,10 @@ gate: jobs: - opendev-buildset-registry - - nodepool-build-image-siblings - - dib-nodepool-functional-openstack-centos-8-stream-src + - nodepool-build-image-siblings: + voting: false + - dib-nodepool-functional-openstack-centos-8-stream-src: + voting: false - openstacksdk-functional-devstack - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-senlin From 9efb589eeb2ecb57efccbd0025c5140b548e0029 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 1 Jun 2022 17:02:51 +0200 Subject: [PATCH 3052/3836] Fix Baremetal cloud layer Ensure baremetal cloud layer ops return resource objects consitently. Change-Id: Iec11d7b3a4aeac2e35410dffd24e12cdc400b1c4 --- openstack/cloud/_baremetal.py | 55 +++++++++++++++++------------------ openstack/tests/base.py | 7 ++++- 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/openstack/cloud/_baremetal.py b/openstack/cloud/_baremetal.py index e4dfe79e3..418c1c00c 100644 --- a/openstack/cloud/_baremetal.py +++ b/openstack/cloud/_baremetal.py @@ -41,7 +41,7 @@ def _baremetal_client(self): def list_nics(self): """Return a list of all bare metal ports.""" - return [nic._to_munch() for nic in self.baremetal.ports(details=True)] + return list(self.baremetal.ports(details=True)) def list_nics_for_machine(self, uuid): """Returns a list of ports present on the machine node. @@ -51,13 +51,11 @@ def list_nics_for_machine(self, uuid): :returns: A list of ports. """ # TODO(dtantsur): support node names here. - return [nic._to_munch() - for nic in self.baremetal.ports(details=True, node_id=uuid)] + return list(self.baremetal.ports(details=True, node_id=uuid)) def get_nic_by_mac(self, mac): """Get bare metal NIC by its hardware address (usually MAC).""" - results = [nic._to_munch() - for nic in self.baremetal.ports(address=mac, details=True)] + results = list(self.baremetal.ports(address=mac, details=True)) try: return results[0] except IndexError: @@ -66,10 +64,9 @@ def get_nic_by_mac(self, mac): def list_machines(self): """List Machines. - :returns: list of ``munch.Munch`` representing machines. + :returns: list of :class:`~openstack.baremetal.v1.node.Node`. """ - return [self._normalize_machine(node) - for node in self.baremetal.nodes()] + return list(self.baremetal.nodes()) def get_machine(self, name_or_id): """Get Machine by name or uuid @@ -79,11 +76,11 @@ def get_machine(self, name_or_id): :param name_or_id: A node name or UUID that will be looked up. - :returns: ``munch.Munch`` representing the node found or None if no - nodes are found. + :rtype: :class:`~openstack.baremetal.v1.node.Node`. + :returns: The node found or None if no nodes are found. """ try: - return self._normalize_machine(self.baremetal.get_node(name_or_id)) + return self.baremetal.find_node(name_or_id, ignore_missing=False) except exc.OpenStackCloudResourceNotFound: return None @@ -92,8 +89,8 @@ def get_machine_by_mac(self, mac): :param mac: Port MAC address to query in order to return a node. - :returns: ``munch.Munch`` representing the node found or None if the - node is not found. + :rtype: :class:`~openstack.baremetal.v1.node.Node`. + :returns: The node found or None if no nodes are found. """ nic = self.get_nic_by_mac(mac) if nic is None: @@ -116,8 +113,8 @@ def inspect_machine(self, name_or_id, wait=False, timeout=3600): :param timeout: Integer value, defautling to 3600 seconds, for the wait state to reach completion. - :returns: ``munch.Munch`` representing the current state of the machine - upon exit of the method. + :rtype: :class:`~openstack.baremetal.v1.node.Node`. + :returns: Current state of the node. """ return_to_available = False @@ -158,7 +155,7 @@ def inspect_machine(self, name_or_id, wait=False, timeout=3600): wait=True, timeout=timeout) - return self._normalize_machine(node) + return node def register_machine(self, nics, wait=False, timeout=3600, lock_timeout=600, **kwargs): @@ -201,8 +198,8 @@ def register_machine(self, nics, wait=False, timeout=3600, :raises: OpenStackCloudException on operation error. - :returns: Returns a ``munch.Munch`` representing the new - baremetal node. + :rtype: :class:`~openstack.baremetal.v1.node.Node`. + :returns: Current state of the node. """ msg = ("Baremetal machine node failed to be created.") @@ -404,10 +401,10 @@ def patch_machine(self, name_or_id, patch): :raises: OpenStackCloudException on operation error. - :returns: ``munch.Munch`` representing the newly updated node. + :returns: Current state of the node. + :rtype: :class:`~openstack.baremetal.v1.node.Node`. """ - return self._normalize_machine( - self.baremetal.patch_node(name_or_id, patch)) + return self.baremetal.patch_node(name_or_id, patch) def update_machine(self, name_or_id, **attrs): """Update a machine with new configuration information @@ -420,7 +417,7 @@ def update_machine(self, name_or_id, **attrs): :raises: OpenStackCloudException on operation error. - :returns: ``munch.Munch`` containing a machine sub-dictonary consisting + :returns: Dictionary containing a machine sub-dictonary consisting of the updated data returned from the API update operation, and a list named changes which contains all of the API paths that received updates. @@ -430,10 +427,12 @@ def update_machine(self, name_or_id, **attrs): raise exc.OpenStackCloudException( "Machine update failed to find Machine: %s. " % name_or_id) - new_config = dict(machine, **attrs) + new_config = dict(machine._to_munch(), **attrs) try: - patch = jsonpatch.JsonPatch.from_diff(machine, new_config) + patch = jsonpatch.JsonPatch.from_diff( + machine._to_munch(), + new_config) except Exception as e: raise exc.OpenStackCloudException( "Machine update failed - Error generating JSON patch object " @@ -449,7 +448,7 @@ def update_machine(self, name_or_id, **attrs): change_list = [change['path'] for change in patch] node = self.baremetal.update_node(machine, **attrs) return dict( - node=self._normalize_machine(node), + node=node, changes=change_list ) @@ -535,13 +534,13 @@ def node_set_provision_state(self, :raises: OpenStackCloudException on operation error. - :returns: ``munch.Munch`` representing the current state of the machine - upon exit of the method. + :returns: Current state of the machine upon exit of the method. + :rtype: :class:`~openstack.baremetal.v1.node.Node`. """ node = self.baremetal.set_node_provision_state( name_or_id, target=state, config_drive=configdrive, wait=wait, timeout=timeout) - return self._normalize_machine(node) + return node def set_machine_maintenance_state( self, diff --git a/openstack/tests/base.py b/openstack/tests/base.py index 1c63ab532..2572f3b91 100644 --- a/openstack/tests/base.py +++ b/openstack/tests/base.py @@ -116,7 +116,12 @@ def add_content(unused): self.addOnException(add_content) def assertSubdict(self, part, whole): - missing_keys = set(part) - set(whole) + missing_keys = [] + for key in part: + # In the resource we have virtual access by not existing keys. To + # verify those are there try access it. + if not whole[key] and part[key]: + missing_keys.append(key) if missing_keys: self.fail("Keys %s are in %s but not in %s" % (missing_keys, part, whole)) From ffd3cb1ae584d4ff9681334a756050b3bf1be94e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 19 May 2022 20:19:31 +0100 Subject: [PATCH 3053/3836] cloud: Remove a load of normalize helpers These are no longer needed. Remove them. Change-Id: Ie358fb1e79c7047453807b0b969077996bfc3a23 Signed-off-by: Stephen Finucane --- openstack/cloud/_normalize.py | 531 +------------------ openstack/tests/unit/cloud/test_image.py | 3 - openstack/tests/unit/cloud/test_normalize.py | 282 ---------- 3 files changed, 12 insertions(+), 804 deletions(-) diff --git a/openstack/cloud/_normalize.py b/openstack/cloud/_normalize.py index f25a43a7c..bc4730284 100644 --- a/openstack/cloud/_normalize.py +++ b/openstack/cloud/_normalize.py @@ -16,8 +16,6 @@ # TODO(shade) The normalize functions here should get merged in to # the sdk resource objects. -import datetime - import munch from openstack import resource @@ -55,38 +53,6 @@ 'tags', ) -_KEYPAIR_FIELDS = ( - 'fingerprint', - 'name', - 'private_key', - 'public_key', - 'user_id', -) - -_KEYPAIR_USELESS_FIELDS = ( - 'deleted', - 'deleted_at', - 'id', - 'updated_at', -) - -_COMPUTE_LIMITS_FIELDS = ( - ('maxPersonality', 'max_personality'), - ('maxPersonalitySize', 'max_personality_size'), - ('maxServerGroupMembers', 'max_server_group_members'), - ('maxServerGroups', 'max_server_groups'), - ('maxServerMeta', 'max_server_meta'), - ('maxTotalCores', 'max_total_cores'), - ('maxTotalInstances', 'max_total_instances'), - ('maxTotalKeypairs', 'max_total_keypairs'), - ('maxTotalRAMSize', 'max_total_ram_size'), - ('totalCoresUsed', 'total_cores_used'), - ('totalInstancesUsed', 'total_instances_used'), - ('totalRAMUsed', 'total_ram_used'), - ('totalServerGroupsUsed', 'total_server_groups_used'), -) - - _pushdown_fields = { 'project': [ 'domain_id' @@ -94,28 +60,6 @@ } -def _split_filters(obj_name='', filters=None, **kwargs): - # Handle jmsepath filters - if not filters: - filters = {} - if not isinstance(filters, dict): - return {}, filters - # Filter out None values from extra kwargs, because those are - # defaults. If you want to search for things with None values, - # they're going to need to go into the filters dict - for (key, value) in kwargs.items(): - if value is not None: - filters[key] = value - pushdown = {} - client = {} - for (key, value) in filters.items(): - if key in _pushdown_fields.get(obj_name, {}): - pushdown[key] = value - else: - client[key] = value - return pushdown, client - - def _to_bool(value): if isinstance(value, str): if not value: @@ -129,10 +73,6 @@ def _pop_int(resource, key): return int(resource.pop(key, 0) or 0) -def _pop_float(resource, key): - return float(resource.pop(key, 0) or 0) - - def _pop_or_get(resource, key, default, strict): if strict: return resource.pop(key, default) @@ -147,26 +87,6 @@ class Normalizer: reasons. ''' - def _normalize_compute_limits(self, limits, project_id=None): - """ Normalize a limits object. - - Limits modified in this method and shouldn't be modified afterwards. - """ - - # Copy incoming limits because of shared dicts in unittests - limits = limits['absolute'].copy() - - new_limits = munch.Munch() - new_limits['location'] = self._get_current_location( - project_id=project_id) - - for field in _COMPUTE_LIMITS_FIELDS: - new_limits[field[1]] = limits.pop(field[0], None) - - new_limits['properties'] = limits.copy() - - return new_limits - def _remove_novaclient_artifacts(self, item): # Remove novaclient artifacts item.pop('links', None) @@ -176,98 +96,6 @@ def _remove_novaclient_artifacts(self, item): item.pop('request_ids', None) item.pop('x_openstack_request_ids', None) - def _normalize_flavors(self, flavors): - """ Normalize a list of flavor objects """ - ret = [] - for flavor in flavors: - ret.append(self._normalize_flavor(flavor)) - return ret - - def _normalize_flavor(self, flavor): - """ Normalize a flavor object """ - new_flavor = munch.Munch() - - # Copy incoming group because of shared dicts in unittests - flavor = flavor.copy() - - # Discard noise - self._remove_novaclient_artifacts(flavor) - flavor.pop('links', None) - - ephemeral = int(_pop_or_get( - flavor, 'OS-FLV-EXT-DATA:ephemeral', 0, self.strict_mode)) - ephemeral = flavor.pop('ephemeral', ephemeral) - is_public = _to_bool(_pop_or_get( - flavor, 'os-flavor-access:is_public', True, self.strict_mode)) - is_public = _to_bool(flavor.pop('is_public', is_public)) - is_disabled = _to_bool(_pop_or_get( - flavor, 'OS-FLV-DISABLED:disabled', False, self.strict_mode)) - extra_specs = _pop_or_get( - flavor, 'OS-FLV-WITH-EXT-SPECS:extra_specs', {}, self.strict_mode) - extra_specs = flavor.pop('extra_specs', extra_specs) - extra_specs = munch.Munch(extra_specs) - - new_flavor['location'] = self.current_location - new_flavor['id'] = flavor.pop('id') - new_flavor['name'] = flavor.pop('name') - new_flavor['is_public'] = is_public - new_flavor['is_disabled'] = is_disabled - new_flavor['ram'] = _pop_int(flavor, 'ram') - new_flavor['vcpus'] = _pop_int(flavor, 'vcpus') - new_flavor['disk'] = _pop_int(flavor, 'disk') - new_flavor['ephemeral'] = ephemeral - new_flavor['swap'] = _pop_int(flavor, 'swap') - new_flavor['rxtx_factor'] = _pop_float(flavor, 'rxtx_factor') - - new_flavor['properties'] = flavor.copy() - new_flavor['extra_specs'] = extra_specs - - # Backwards compat with nova - passthrough values - if not self.strict_mode: - for (k, v) in new_flavor['properties'].items(): - new_flavor.setdefault(k, v) - - return new_flavor - - def _normalize_keypairs(self, keypairs): - """Normalize Nova Keypairs""" - ret = [] - for keypair in keypairs: - ret.append(self._normalize_keypair(keypair)) - return ret - - def _normalize_keypair(self, keypair): - """Normalize Ironic Machine""" - - new_keypair = munch.Munch() - keypair = keypair.copy() - - # Discard noise - self._remove_novaclient_artifacts(keypair) - - new_keypair['location'] = self.current_location - for key in _KEYPAIR_FIELDS: - new_keypair[key] = keypair.pop(key, None) - # These are completely meaningless fields - for key in _KEYPAIR_USELESS_FIELDS: - keypair.pop(key, None) - new_keypair['type'] = keypair.pop('type', 'ssh') - # created_at isn't returned from the keypair creation. (what?) - new_keypair['created_at'] = keypair.pop( - 'created_at', datetime.datetime.now().isoformat()) - # Don't even get me started on this - new_keypair['id'] = new_keypair['name'] - - new_keypair['properties'] = keypair.copy() - - return new_keypair - - def _normalize_images(self, images): - ret = [] - for image in images: - ret.append(self._normalize_image(image)) - return ret - def _normalize_image(self, image): if isinstance(image, resource.Resource): image = image.to_dict(ignore_none=True, original_names=True) @@ -351,6 +179,8 @@ def _normalize_image(self, image): new_image['minRam'] = new_image['min_ram'] return new_image + # TODO(stephenfin): Remove this once we get rid of support for nova + # secgroups def _normalize_secgroups(self, groups): """Normalize the structure of security groups @@ -367,6 +197,8 @@ def _normalize_secgroups(self, groups): ret.append(self._normalize_secgroup(group)) return ret + # TODO(stephenfin): Remove this once we get rid of support for nova + # secgroups def _normalize_secgroup(self, group): ret = munch.Munch() @@ -400,6 +232,8 @@ def _normalize_secgroup(self, group): return ret + # TODO(stephenfin): Remove this once we get rid of support for nova + # secgroups def _normalize_secgroup_rules(self, rules): """Normalize the structure of nova security group rules @@ -415,6 +249,8 @@ def _normalize_secgroup_rules(self, rules): ret.append(self._normalize_secgroup_rule(rule)) return ret + # TODO(stephenfin): Remove this once we get rid of support for nova + # secgroups def _normalize_secgroup_rule(self, rule): ret = munch.Munch() # Copy incoming rule because of shared dicts in unittests @@ -456,14 +292,6 @@ def _normalize_secgroup_rule(self, rule): ret.setdefault(key, val) return ret - def _normalize_servers(self, servers): - # Here instead of _utils because we need access to region and cloud - # name from the cloud object - ret = [] - for server in servers: - ret.append(self._normalize_server(server)) - return ret - def _normalize_server(self, server): ret = munch.Munch() # Copy incoming server because of shared dicts in unittests @@ -576,6 +404,8 @@ def _normalize_server(self, server): ret.setdefault(key, val) return ret + # TODO(stephenfin): Remove this once we get rid of support for nova + # floating IPs def _normalize_floating_ips(self, ips): """Normalize the structure of floating IPs @@ -609,6 +439,8 @@ def _normalize_floating_ips(self, ips): self._normalize_floating_ip(ip) for ip in ips ] + # TODO(stephenfin): Remove this once we get rid of support for nova + # floating IPs def _normalize_floating_ip(self, ip): # Copy incoming floating ip because of shared dicts in unittests if isinstance(ip, resource.Resource): @@ -679,230 +511,6 @@ def _normalize_floating_ip(self, ip): return ret - def _normalize_projects(self, projects): - """Normalize the structure of projects - - This makes tenants from keystone v2 look like projects from v3. - - :param list projects: A list of projects to normalize - - :returns: A list of normalized dicts. - """ - ret = [] - for project in projects: - ret.append(self._normalize_project(project)) - return ret - - def _normalize_project(self, project): - - # Copy incoming project because of shared dicts in unittests - project = project.copy() - - # Discard noise - self._remove_novaclient_artifacts(project) - - # In both v2 and v3 - project_id = project.pop('id') - name = project.pop('name', '') - description = project.pop('description', '') - is_enabled = project.pop('enabled', True) - - # v3 additions - domain_id = project.pop('domain_id', 'default') - parent_id = project.pop('parent_id', None) - is_domain = project.pop('is_domain', False) - - # Projects have a special relationship with location - location = self._get_identity_location() - location['project']['domain_id'] = domain_id - location['project']['id'] = parent_id - - ret = munch.Munch( - location=location, - id=project_id, - name=name, - description=description, - is_enabled=is_enabled, - is_domain=is_domain, - domain_id=domain_id, - properties=project.copy() - ) - - # Backwards compat - if not self.strict_mode: - ret['enabled'] = is_enabled - ret['parent_id'] = parent_id - for key, val in ret['properties'].items(): - ret.setdefault(key, val) - - return ret - - def _normalize_volume_type_access(self, volume_type_access): - - volume_type_access = volume_type_access.copy() - - volume_type_id = volume_type_access.pop('volume_type_id') - project_id = volume_type_access.pop('project_id') - ret = munch.Munch( - location=self.current_location, - project_id=project_id, - volume_type_id=volume_type_id, - properties=volume_type_access.copy(), - ) - return ret - - def _normalize_volume_type_accesses(self, volume_type_accesses): - ret = [] - for volume_type_access in volume_type_accesses: - ret.append(self._normalize_volume_type_access(volume_type_access)) - return ret - - def _normalize_volume_type(self, volume_type): - - volume_type = volume_type.copy() - - volume_id = volume_type.pop('id') - description = volume_type.pop('description', None) - name = volume_type.pop('name', None) - old_is_public = volume_type.pop('os-volume-type-access:is_public', - False) - is_public = volume_type.pop('is_public', old_is_public) - qos_specs_id = volume_type.pop('qos_specs_id', None) - extra_specs = volume_type.pop('extra_specs', {}) - ret = munch.Munch( - location=self.current_location, - is_public=is_public, - id=volume_id, - name=name, - description=description, - qos_specs_id=qos_specs_id, - extra_specs=extra_specs, - properties=volume_type.copy(), - ) - return ret - - def _normalize_volume_types(self, volume_types): - ret = [] - for volume in volume_types: - ret.append(self._normalize_volume_type(volume)) - return ret - - def _normalize_volumes(self, volumes): - """Normalize the structure of volumes - - This makes tenants from cinder v1 look like volumes from v2. - - :param list projects: A list of volumes to normalize - - :returns: A list of normalized dicts. - """ - ret = [] - for volume in volumes: - ret.append(self._normalize_volume(volume)) - return ret - - def _normalize_volume(self, volume): - - volume = volume.copy() - - # Discard noise - self._remove_novaclient_artifacts(volume) - - volume_id = volume.pop('id') - - name = volume.pop('display_name', None) - name = volume.pop('name', name) - - description = volume.pop('display_description', None) - description = volume.pop('description', description) - - is_bootable = _to_bool(volume.pop('bootable', True)) - is_encrypted = _to_bool(volume.pop('encrypted', False)) - can_multiattach = _to_bool(volume.pop('multiattach', False)) - - project_id = _pop_or_get( - volume, 'os-vol-tenant-attr:tenant_id', None, self.strict_mode) - az = volume.pop('availability_zone', None) - - location = self._get_current_location(project_id=project_id, zone=az) - - host = _pop_or_get( - volume, 'os-vol-host-attr:host', None, self.strict_mode) - replication_extended_status = _pop_or_get( - volume, 'os-volume-replication:extended_status', - None, self.strict_mode) - - migration_status = _pop_or_get( - volume, 'os-vol-mig-status-attr:migstat', None, self.strict_mode) - migration_status = volume.pop('migration_status', migration_status) - _pop_or_get(volume, 'user_id', None, self.strict_mode) - source_volume_id = _pop_or_get( - volume, 'source_volid', None, self.strict_mode) - replication_driver = _pop_or_get( - volume, 'os-volume-replication:driver_data', - None, self.strict_mode) - - ret = munch.Munch( - location=location, - id=volume_id, - name=name, - description=description, - size=_pop_int(volume, 'size'), - attachments=volume.pop('attachments', []), - status=volume.pop('status'), - migration_status=migration_status, - host=host, - replication_driver=replication_driver, - replication_status=volume.pop('replication_status', None), - replication_extended_status=replication_extended_status, - snapshot_id=volume.pop('snapshot_id', None), - created_at=volume.pop('created_at'), - updated_at=volume.pop('updated_at', None), - source_volume_id=source_volume_id, - consistencygroup_id=volume.pop('consistencygroup_id', None), - volume_type=volume.pop('volume_type', None), - metadata=volume.pop('metadata', {}), - is_bootable=is_bootable, - is_encrypted=is_encrypted, - can_multiattach=can_multiattach, - properties=volume.copy(), - ) - - # Backwards compat - if not self.strict_mode: - ret['display_name'] = name - ret['display_description'] = description - ret['bootable'] = is_bootable - ret['encrypted'] = is_encrypted - ret['multiattach'] = can_multiattach - ret['availability_zone'] = az - for key, val in ret['properties'].items(): - ret.setdefault(key, val) - return ret - - def _normalize_volume_attachment(self, attachment): - """ Normalize a volume attachment object""" - - attachment = attachment.copy() - - # Discard noise - self._remove_novaclient_artifacts(attachment) - return munch.Munch(**attachment) - - def _normalize_volume_backups(self, backups): - ret = [] - for backup in backups: - ret.append(self._normalize_volume_backup(backup)) - return ret - - def _normalize_volume_backup(self, backup): - """ Normalize a volume backup object""" - - backup = backup.copy() - # Discard noise - self._remove_novaclient_artifacts(backup) - return munch.Munch(**backup) - def _normalize_compute_usage(self, usage): """ Normalize a compute usage object """ @@ -1112,69 +720,6 @@ def _normalize_magnum_service(self, magnum_service): ret['properties'] = magnum_service return ret - def _normalize_stacks(self, stacks): - """Normalize Heat Stacks""" - ret = [] - for stack in stacks: - ret.append(self._normalize_stack(stack)) - return ret - - def _normalize_stack(self, stack): - """Normalize Heat Stack""" - if isinstance(stack, resource.Resource): - stack = stack.to_dict(ignore_none=True, original_names=True) - else: - stack = stack.copy() - - # Discard noise - self._remove_novaclient_artifacts(stack) - - # Discard things heatclient adds that aren't in the REST - stack.pop('action', None) - stack.pop('status', None) - stack.pop('identifier', None) - - stack_status = None - - stack_status = stack.pop('stack_status', None) or \ - stack.pop('status', None) - (action, status) = stack_status.split('_', 1) - - ret = munch.Munch( - id=stack.pop('id'), - location=self._get_current_location(), - action=action, - status=status, - ) - if not self.strict_mode: - ret['stack_status'] = stack_status - - for (new_name, old_name) in ( - ('name', 'stack_name'), - ('created_at', 'creation_time'), - ('deleted_at', 'deletion_time'), - ('updated_at', 'updated_time'), - ('description', 'description'), - ('is_rollback_enabled', 'disable_rollback'), - ('parent', 'parent'), - ('notification_topics', 'notification_topics'), - ('parameters', 'parameters'), - ('outputs', 'outputs'), - ('owner', 'stack_owner'), - ('status_reason', 'stack_status_reason'), - ('stack_user_project_id', 'stack_user_project_id'), - ('tempate_description', 'template_description'), - ('timeout_mins', 'timeout_mins'), - ('tags', 'tags')): - value = stack.get(old_name, None) - ret[new_name] = value - if not self.strict_mode: - ret[old_name] = value - ret['identifier'] = '{name}/{id}'.format( - name=ret['name'], id=ret['id']) - # ret['properties'] = stack - return ret - def _normalize_machines(self, machines): """Normalize Ironic Machines""" ret = [] @@ -1193,55 +738,3 @@ def _normalize_machine(self, machine): self._remove_novaclient_artifacts(machine) return machine - - def _normalize_roles(self, roles): - """Normalize Keystone roles""" - ret = [] - for role in roles: - ret.append(self._normalize_role(role)) - return ret - - def _normalize_role(self, role): - """Normalize Identity roles.""" - - return munch.Munch( - id=role.get('id'), - name=role.get('name'), - domain_id=role.get('domain_id'), - location=self._get_identity_location(), - properties={}, - ) - - def _normalize_containers(self, containers): - """Normalize Swift Containers""" - ret = [] - for container in containers: - ret.append(self._normalize_container(container)) - return ret - - def _normalize_container(self, container): - """Normalize Swift Container.""" - - return munch.Munch( - name=container.get('name'), - bytes=container.get('bytes'), - count=container.get('count'), - ) - - def _normalize_objects(self, objects): - """Normalize Swift Objects""" - ret = [] - for object in objects: - ret.append(self._normalize_object(object)) - return ret - - def _normalize_object(self, object): - """Normalize Swift Object.""" - - return munch.Munch( - name=object.get('name'), - bytes=object.get('_bytes'), - content_type=object.get('content_type'), - hash=object.get('_hash'), - last_modified=object.get('_last_modified'), - ) diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index a3b13e278..bf652ecad 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -853,9 +853,6 @@ def test_delete_autocreated_image_objects(self): def _image_dict(self, fake_image): return self.cloud._normalize_image(meta.obj_to_munch(fake_image)) - def _munch_images(self, fake_image): - return self.cloud._normalize_images([fake_image]) - def _call_create_image(self, name, **kwargs): imagefile = tempfile.NamedTemporaryFile(delete=False) imagefile.write(b'\0') diff --git a/openstack/tests/unit/cloud/test_normalize.py b/openstack/tests/unit/cloud/test_normalize.py index 75b413509..be6eab3e4 100644 --- a/openstack/tests/unit/cloud/test_normalize.py +++ b/openstack/tests/unit/cloud/test_normalize.py @@ -276,52 +276,6 @@ def _assert_server_munch_attributes(testcase, raw, server): class TestNormalize(base.TestCase): - def test_normalize_flavors(self): - raw_flavor = RAW_FLAVOR_DICT.copy() - expected = { - 'OS-FLV-EXT-DATA:ephemeral': 80, - 'OS-FLV-WITH-EXT-SPECS:extra_specs': { - u'class': u'performance1', - u'disk_io_index': u'40', - u'number_of_data_disks': u'1', - u'policy_class': u'performance_flavor', - u'resize_policy_class': u'performance_flavor'}, - 'disk': 40, - 'ephemeral': 80, - 'extra_specs': { - u'class': u'performance1', - u'disk_io_index': u'40', - u'number_of_data_disks': u'1', - u'policy_class': u'performance_flavor', - u'resize_policy_class': u'performance_flavor'}, - 'id': u'performance1-8', - 'is_disabled': False, - 'is_public': False, - 'location': { - 'cloud': '_test_cloud_', - 'project': { - 'domain_id': None, - 'domain_name': 'default', - 'id': '1c36b64c840a42cd9e9b931a369337f0', - 'name': 'admin'}, - 'region_name': u'RegionOne', - 'zone': None}, - 'name': u'8 GB Performance', - 'properties': { - 'OS-FLV-EXT-DATA:ephemeral': 80, - 'OS-FLV-WITH-EXT-SPECS:extra_specs': { - u'class': u'performance1', - u'disk_io_index': u'40', - u'number_of_data_disks': u'1', - u'policy_class': u'performance_flavor', - u'resize_policy_class': u'performance_flavor'}}, - 'ram': 8192, - 'rxtx_factor': 1600.0, - 'swap': 0, - 'vcpus': 8} - retval = self.cloud._normalize_flavor(raw_flavor) - self.assertEqual(expected, retval) - def test_normalize_nova_images(self): raw_image = RAW_NOVA_IMAGE_DICT.copy() expected = { @@ -612,116 +566,6 @@ def test_normalize_secgroup_rules(self): retval = self.cloud._normalize_secgroup_rules(nova_rules) self.assertEqual(expected, retval) - def test_normalize_volumes_v1(self): - vol = dict( - id='55db9e89-9cb4-4202-af88-d8c4a174998e', - display_name='test', - display_description='description', - bootable=u'false', # unicode type - multiattach='true', # str type - status='in-use', - created_at='2015-08-27T09:49:58-05:00', - ) - expected = { - 'attachments': [], - 'availability_zone': None, - 'bootable': False, - 'can_multiattach': True, - 'consistencygroup_id': None, - 'created_at': vol['created_at'], - 'description': vol['display_description'], - 'display_description': vol['display_description'], - 'display_name': vol['display_name'], - 'encrypted': False, - 'host': None, - 'id': '55db9e89-9cb4-4202-af88-d8c4a174998e', - 'is_bootable': False, - 'is_encrypted': False, - 'location': { - 'cloud': '_test_cloud_', - 'project': { - 'domain_id': None, - 'domain_name': 'default', - 'id': '1c36b64c840a42cd9e9b931a369337f0', - 'name': 'admin'}, - 'region_name': u'RegionOne', - 'zone': None}, - 'metadata': {}, - 'migration_status': None, - 'multiattach': True, - 'name': vol['display_name'], - 'properties': {}, - 'replication_driver': None, - 'replication_extended_status': None, - 'replication_status': None, - 'size': 0, - 'snapshot_id': None, - 'source_volume_id': None, - 'status': vol['status'], - 'updated_at': None, - 'volume_type': None, - } - retval = self.cloud._normalize_volume(vol) - self.assertEqual(expected, retval) - - def test_normalize_volumes_v2(self): - vol = dict( - id='55db9e89-9cb4-4202-af88-d8c4a174998e', - name='test', - description='description', - bootable=False, - multiattach=True, - status='in-use', - created_at='2015-08-27T09:49:58-05:00', - availability_zone='my-zone', - ) - vol['os-vol-tenant-attr:tenant_id'] = 'my-project' - expected = { - 'attachments': [], - 'availability_zone': vol['availability_zone'], - 'bootable': False, - 'can_multiattach': True, - 'consistencygroup_id': None, - 'created_at': vol['created_at'], - 'description': vol['description'], - 'display_description': vol['description'], - 'display_name': vol['name'], - 'encrypted': False, - 'host': None, - 'id': '55db9e89-9cb4-4202-af88-d8c4a174998e', - 'is_bootable': False, - 'is_encrypted': False, - 'location': { - 'cloud': '_test_cloud_', - 'project': { - 'domain_id': None, - 'domain_name': None, - 'id': vol['os-vol-tenant-attr:tenant_id'], - 'name': None}, - 'region_name': u'RegionOne', - 'zone': vol['availability_zone']}, - 'metadata': {}, - 'migration_status': None, - 'multiattach': True, - 'name': vol['name'], - 'os-vol-tenant-attr:tenant_id': vol[ - 'os-vol-tenant-attr:tenant_id'], - 'properties': { - 'os-vol-tenant-attr:tenant_id': vol[ - 'os-vol-tenant-attr:tenant_id']}, - 'replication_driver': None, - 'replication_extended_status': None, - 'replication_status': None, - 'size': 0, - 'snapshot_id': None, - 'source_volume_id': None, - 'status': vol['status'], - 'updated_at': None, - 'volume_type': None, - } - retval = self.cloud._normalize_volume(vol) - self.assertEqual(expected, retval) - def test_normalize_coe_cluster_template(self): coe_cluster_template = RAW_COE_CLUSTER_TEMPLATE_DICT.copy() expected = { @@ -836,38 +680,6 @@ def setUp(self): super(TestStrictNormalize, self).setUp() self.assertTrue(self.cloud.strict_mode) - def test_normalize_flavors(self): - raw_flavor = RAW_FLAVOR_DICT.copy() - expected = { - 'disk': 40, - 'ephemeral': 80, - 'extra_specs': { - u'class': u'performance1', - u'disk_io_index': u'40', - u'number_of_data_disks': u'1', - u'policy_class': u'performance_flavor', - u'resize_policy_class': u'performance_flavor'}, - 'id': u'performance1-8', - 'is_disabled': False, - 'is_public': False, - 'location': { - 'cloud': '_test_cloud_', - 'project': { - 'domain_id': None, - 'domain_name': 'default', - 'id': u'1c36b64c840a42cd9e9b931a369337f0', - 'name': 'admin'}, - 'region_name': u'RegionOne', - 'zone': None}, - 'name': u'8 GB Performance', - 'properties': {}, - 'ram': 8192, - 'rxtx_factor': 1600.0, - 'swap': 0, - 'vcpus': 8} - retval = self.cloud._normalize_flavor(raw_flavor) - self.assertEqual(expected, retval) - def test_normalize_nova_images(self): raw_image = RAW_NOVA_IMAGE_DICT.copy() expected = { @@ -1028,100 +840,6 @@ def test_normalize_secgroups(self): self.cloud.secgroup_source = 'neutron' self.assertEqual(expected, retval) - def test_normalize_volumes_v1(self): - vol = dict( - id='55db9e89-9cb4-4202-af88-d8c4a174998e', - display_name='test', - display_description='description', - bootable=u'false', # unicode type - multiattach='true', # str type - status='in-use', - created_at='2015-08-27T09:49:58-05:00', - ) - expected = { - 'attachments': [], - 'can_multiattach': True, - 'consistencygroup_id': None, - 'created_at': vol['created_at'], - 'description': vol['display_description'], - 'host': None, - 'id': '55db9e89-9cb4-4202-af88-d8c4a174998e', - 'is_bootable': False, - 'is_encrypted': False, - 'location': { - 'cloud': '_test_cloud_', - 'project': { - 'domain_id': None, - 'domain_name': 'default', - 'id': '1c36b64c840a42cd9e9b931a369337f0', - 'name': 'admin'}, - 'region_name': u'RegionOne', - 'zone': None}, - 'metadata': {}, - 'migration_status': None, - 'name': vol['display_name'], - 'properties': {}, - 'replication_driver': None, - 'replication_extended_status': None, - 'replication_status': None, - 'size': 0, - 'snapshot_id': None, - 'source_volume_id': None, - 'status': vol['status'], - 'updated_at': None, - 'volume_type': None, - } - retval = self.cloud._normalize_volume(vol) - self.assertEqual(expected, retval) - - def test_normalize_volumes_v2(self): - vol = dict( - id='55db9e89-9cb4-4202-af88-d8c4a174998e', - name='test', - description='description', - bootable=False, - multiattach=True, - status='in-use', - created_at='2015-08-27T09:49:58-05:00', - availability_zone='my-zone', - ) - vol['os-vol-tenant-attr:tenant_id'] = 'my-project' - expected = { - 'attachments': [], - 'can_multiattach': True, - 'consistencygroup_id': None, - 'created_at': vol['created_at'], - 'description': vol['description'], - 'host': None, - 'id': '55db9e89-9cb4-4202-af88-d8c4a174998e', - 'is_bootable': False, - 'is_encrypted': False, - 'location': { - 'cloud': '_test_cloud_', - 'project': { - 'domain_id': None, - 'domain_name': None, - 'id': vol['os-vol-tenant-attr:tenant_id'], - 'name': None}, - 'region_name': u'RegionOne', - 'zone': vol['availability_zone']}, - 'metadata': {}, - 'migration_status': None, - 'name': vol['name'], - 'properties': {}, - 'replication_driver': None, - 'replication_extended_status': None, - 'replication_status': None, - 'size': 0, - 'snapshot_id': None, - 'source_volume_id': None, - 'status': vol['status'], - 'updated_at': None, - 'volume_type': None, - } - retval = self.cloud._normalize_volume(vol) - self.assertEqual(expected, retval) - def test_normalize_coe_cluster_template(self): coe_cluster_template = RAW_COE_CLUSTER_TEMPLATE_DICT.copy() expected = { From 9e87611aec8936cc30cd1d0daf8886f3f21a7025 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 8 Jun 2022 14:29:45 +0200 Subject: [PATCH 3054/3836] trivial: Run some files through black Before major surgery. Change-Id: I367bf6df230262858d1d414afed4a43ccbdce72f Signed-off-by: Stephen Finucane --- openstack/proxy.py | 297 ++++++++++++++++------------ openstack/resource.py | 441 ++++++++++++++++++++++++++---------------- tox.ini | 3 +- 3 files changed, 455 insertions(+), 286 deletions(-) diff --git a/openstack/proxy.py b/openstack/proxy.py index 3cb86dcf1..4c4cd9d73 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -16,6 +16,7 @@ try: import simplejson + JSONDecodeError = simplejson.scanner.JSONDecodeError except ImportError: JSONDecodeError = ValueError @@ -38,16 +39,24 @@ def _check_resource(strict=False): def wrap(method): def check(self, expected, actual=None, *args, **kwargs): - if (strict and actual is not None and not - isinstance(actual, resource.Resource)): + if ( + strict + and actual is not None + and not isinstance(actual, resource.Resource) + ): raise ValueError("A %s must be passed" % expected.__name__) - elif (isinstance(actual, resource.Resource) and not - isinstance(actual, expected)): - raise ValueError("Expected %s but received %s" % ( - expected.__name__, actual.__class__.__name__)) + elif isinstance(actual, resource.Resource) and not isinstance( + actual, expected + ): + raise ValueError( + "Expected %s but received %s" + % (expected.__name__, actual.__class__.__name__) + ) return method(self, expected, actual, *args, **kwargs) + return check + return wrap @@ -68,16 +77,22 @@ class Proxy(adapter.Adapter): """ def __init__( - self, - session, - statsd_client=None, statsd_prefix=None, - prometheus_counter=None, prometheus_histogram=None, - influxdb_config=None, influxdb_client=None, - *args, **kwargs): + self, + session, + statsd_client=None, + statsd_prefix=None, + prometheus_counter=None, + prometheus_histogram=None, + influxdb_config=None, + influxdb_client=None, + *args, + **kwargs + ): # NOTE(dtantsur): keystoneauth defaults retriable_status_codes to None, # override it with a class-level value. - kwargs.setdefault('retriable_status_codes', - self.retriable_status_codes) + kwargs.setdefault( + 'retriable_status_codes', self.retriable_status_codes + ) super(Proxy, self).__init__(session=session, *args, **kwargs) self._statsd_client = statsd_client self._statsd_prefix = statsd_prefix @@ -94,12 +109,10 @@ def __init__( def _get_cache_key_prefix(self, url): """Calculate cache prefix for the url""" name_parts = self._extract_name( - url, self.service_type, - self.session.get_project_id()) + url, self.service_type, self.session.get_project_id() + ) - return '.'.join( - [self.service_type] - + name_parts) + return '.'.join([self.service_type] + name_parts) def _invalidate_cache(self, conn, key_prefix): """Invalidate all cache entries starting with given prefix""" @@ -109,10 +122,16 @@ def _invalidate_cache(self, conn, key_prefix): conn._api_cache_keys.remove(k) def request( - self, url, method, error_message=None, - raise_exc=False, connect_retries=1, - global_request_id=None, - *args, **kwargs): + self, + url, + method, + error_message=None, + raise_exc=False, + connect_retries=1, + global_request_id=None, + *args, + **kwargs + ): conn = self._get_connection() if not global_request_id: # Per-request setting should take precedence @@ -125,9 +144,7 @@ def request( if conn.cache_enabled: # Construct cache key. It consists of: # service.name_parts.URL.str(kwargs) - key = '.'.join( - [key_prefix, url, str(kwargs)] - ) + key = '.'.join([key_prefix, url, str(kwargs)]) # Track cache key for invalidating possibility conn._api_cache_keys.add(key) @@ -137,7 +154,8 @@ def request( # Get the object expiration time from config # default to 0 to disable caching for this resource type expiration_time = int( - conn._cache_expirations.get(key_prefix, 0)) + conn._cache_expirations.get(key_prefix, 0) + ) # Get from cache or execute and cache response = conn._cache.get_or_create( key=key, @@ -149,9 +167,9 @@ def request( raise_exc=raise_exc, global_request_id=global_request_id, **kwargs - ) + ), ), - expiration_time=expiration_time + expiration_time=expiration_time, ) else: # invalidate cache if we send modification request or user @@ -159,10 +177,13 @@ def request( self._invalidate_cache(conn, key_prefix) # Pass through the API request bypassing cache response = super(Proxy, self).request( - url, method, - connect_retries=connect_retries, raise_exc=raise_exc, + url, + method, + connect_retries=connect_retries, + raise_exc=raise_exc, global_request_id=global_request_id, - **kwargs) + **kwargs + ) for h in response.history: self._report_stats(h) @@ -178,20 +199,20 @@ def request( @functools.lru_cache(maxsize=256) def _extract_name(self, url, service_type=None, project_id=None): - '''Produce a key name to use in logging/metrics from the URL path. + """Produce a key name to use in logging/metrics from the URL path. We want to be able to logic/metric sane general things, so we pull the url apart to generate names. The function returns a list because there are two different ways in which the elements want to be combined below (one for logging, one for statsd) - Some examples are likely useful: + Some examples are likely useful:: - /servers -> ['servers'] - /servers/{id} -> ['server'] - /servers/{id}/os-security-groups -> ['server', 'os-security-groups'] - /v2.0/networks.json -> ['networks'] - ''' + /servers -> ['servers'] + /servers/{id} -> ['server'] + /servers/{id}/os-security-groups -> ['server', 'os-security-groups'] + /v2.0/networks.json -> ['networks'] + """ url_path = urllib.parse.urlparse(url).path.strip() # Remove / from the beginning to keep the list indexes of interesting @@ -201,16 +222,19 @@ def _extract_name(self, url, service_type=None, project_id=None): # Special case for neutron, which puts .json on the end of urls if url_path.endswith('.json'): - url_path = url_path[:-len('.json')] + url_path = url_path[: -len('.json')] # Split url into parts and exclude potential project_id in some urls url_parts = [ - x for x in url_path.split('/') if ( + x + for x in url_path.split('/') + if ( x != project_id and ( not project_id or (project_id and x != 'AUTH_' + project_id) - )) + ) + ) ] if url_parts[-1] == 'detail': # Special case detail calls @@ -221,9 +245,12 @@ def _extract_name(self, url, service_type=None, project_id=None): # Strip leading version piece so that # GET /v2.0/networks # returns ['networks'] - if (url_parts[0] - and url_parts[0][0] == 'v' - and url_parts[0][1] and url_parts[0][1].isdigit()): + if ( + url_parts[0] + and url_parts[0][0] == 'v' + and url_parts[0][1] + and url_parts[0][1].isdigit() + ): url_parts = url_parts[1:] name_parts = self._extract_name_consume_url_parts(url_parts) @@ -242,19 +269,21 @@ def _extract_name(self, url, service_type=None, project_id=None): return [part for part in name_parts if part] def _extract_name_consume_url_parts(self, url_parts): - """Pull out every other URL portion - so that - GET /servers/{id}/os-security-groups - returns ['server', 'os-security-groups'] + """Pull out every other URL portion. + For example, ``GET /servers/{id}/os-security-groups`` returns + ``['server', 'os-security-groups']``. """ name_parts = [] for idx in range(0, len(url_parts)): if not idx % 2 and url_parts[idx]: # If we are on first segment and it end with 's' stip this 's' # to differentiate LIST and GET_BY_ID - if (len(url_parts) > idx + 1 - and url_parts[idx][-1] == 's' - and url_parts[idx][-2:] != 'is'): + if ( + len(url_parts) > idx + 1 + and url_parts[idx][-1] == 's' + and url_parts[idx][-2:] != 'is' + ): name_parts.append(url_parts[idx][:-1]) else: name_parts.append(url_parts[idx]) @@ -276,15 +305,19 @@ def _report_stats_statsd(self, response, url=None, method=None, exc=None): if response is not None and not method: method = response.request.method name_parts = [ - normalize_metric_name(f) for f in - self._extract_name( - url, self.service_type, self.session.get_project_id()) + normalize_metric_name(f) + for f in self._extract_name( + url, self.service_type, self.session.get_project_id() + ) ] key = '.'.join( - [self._statsd_prefix, - normalize_metric_name(self.service_type), method, - '_'.join(name_parts) - ]) + [ + self._statsd_prefix, + normalize_metric_name(self.service_type), + method, + '_'.join(name_parts), + ] + ) with self._statsd_client.pipeline() as pipe: if response is not None: duration = int(response.elapsed.total_seconds() * 1000) @@ -300,15 +333,17 @@ def _report_stats_statsd(self, response, url=None, method=None, exc=None): # We do not want errors in metric reporting ever break client self.log.exception("Exception reporting metrics") - def _report_stats_prometheus(self, response, url=None, method=None, - exc=None): + def _report_stats_prometheus( + self, response, url=None, method=None, exc=None + ): if response is not None and not url: url = response.request.url if response is not None and not method: method = response.request.method parsed_url = urlparse(url) endpoint = "{}://{}{}".format( - parsed_url.scheme, parsed_url.netloc, parsed_url.path) + parsed_url.scheme, parsed_url.netloc, parsed_url.path + ) if response is not None: labels = dict( method=method, @@ -318,10 +353,12 @@ def _report_stats_prometheus(self, response, url=None, method=None, ) self._prometheus_counter.labels(**labels).inc() self._prometheus_histogram.labels(**labels).observe( - response.elapsed.total_seconds() * 1000) + response.elapsed.total_seconds() * 1000 + ) - def _report_stats_influxdb(self, response, url=None, method=None, - exc=None): + def _report_stats_influxdb( + self, response, url=None, method=None, exc=None + ): # NOTE(gtema): status_code is saved both as tag and field to give # ability showing it as a value and not only as a legend. # However Influx is not ok with having same name in tags and fields, @@ -332,16 +369,16 @@ def _report_stats_influxdb(self, response, url=None, method=None, method = response.request.method tags = dict( method=method, - name='_'.join([ - normalize_metric_name(f) for f in - self._extract_name( - url, self.service_type, - self.session.get_project_id()) - ]) - ) - fields = dict( - attempted=1 + name='_'.join( + [ + normalize_metric_name(f) + for f in self._extract_name( + url, self.service_type, self.session.get_project_id() + ) + ] + ), ) + fields = dict(attempted=1) if response is not None: fields['duration'] = int(response.elapsed.total_seconds() * 1000) tags['status_code'] = str(response.status_code) @@ -356,16 +393,14 @@ def _report_stats_influxdb(self, response, url=None, method=None, fields['failed'] = 1 if 'additional_metric_tags' in self._influxdb_config: tags.update(self._influxdb_config['additional_metric_tags']) - measurement = self._influxdb_config.get( - 'measurement', 'openstack_api') \ - if self._influxdb_config else 'openstack_api' + measurement = ( + self._influxdb_config.get('measurement', 'openstack_api') + if self._influxdb_config + else 'openstack_api' + ) # Note(gtema) append service name into the measurement name measurement = '%s.%s' % (measurement, self.service_type) - data = [dict( - measurement=measurement, - tags=tags, - fields=fields - )] + data = [dict(measurement=measurement, tags=tags, fields=fields)] try: self._influxdb_client.write_points(data) except Exception: @@ -385,8 +420,8 @@ def _get_connection(self): directly on ourselves. Use one of them. """ return getattr( - self, '_connection', getattr( - self.session, '_sdk_connection', None)) + self, '_connection', getattr(self.session, '_sdk_connection', None) + ) def _get_resource(self, resource_type, value, **attrs): """Get a resource object to work on @@ -404,15 +439,14 @@ class if using an existing instance, or ``munch.Munch``, if value is None: # Create a bare resource res = resource_type.new(connection=conn, **attrs) - elif (isinstance(value, dict) - and not isinstance(value, resource.Resource)): - res = resource_type._from_munch( - value, connection=conn) + elif isinstance(value, dict) and not isinstance( + value, resource.Resource + ): + res = resource_type._from_munch(value, connection=conn) res._update(**attrs) elif not isinstance(value, resource_type): # Create from an ID - res = resource_type.new( - id=value, connection=conn, **attrs) + res = resource_type.new(id=value, connection=conn, **attrs) else: # An existing resource instance res = value @@ -435,8 +469,7 @@ def _get_uri_attribute(self, child, parent, name): value = resource.Resource._get_id(parent) return value - def _find(self, resource_type, name_or_id, ignore_missing=True, - **attrs): + def _find(self, resource_type, name_or_id, ignore_missing=True, **attrs): """Find a resource :param name_or_id: The name or ID of a resource to find. @@ -451,9 +484,9 @@ def _find(self, resource_type, name_or_id, ignore_missing=True, :returns: An instance of ``resource_type`` or None """ - return resource_type.find(self, name_or_id, - ignore_missing=ignore_missing, - **attrs) + return resource_type.find( + self, name_or_id, ignore_missing=ignore_missing, **attrs + ) # TODO(stephenfin): Update docstring for attrs since it's a lie @_check_resource(strict=False) @@ -565,8 +598,15 @@ def _bulk_create(self, resource_type, data, base_path=None): return resource_type.bulk_create(self, data, base_path=base_path) @_check_resource(strict=False) - def _get(self, resource_type, value=None, requires_id=True, - base_path=None, skip_cache=False, **attrs): + def _get( + self, + resource_type, + value=None, + requires_id=True, + base_path=None, + skip_cache=False, + **attrs + ): """Fetch a resource :param resource_type: The type of resource to get. @@ -592,13 +632,16 @@ def _get(self, resource_type, value=None, requires_id=True, res = self._get_resource(resource_type, value, **attrs) return res.fetch( - self, requires_id=requires_id, base_path=base_path, + self, + requires_id=requires_id, + base_path=base_path, skip_cache=skip_cache, error_message="No {resource_type} found for {value}".format( - resource_type=resource_type.__name__, value=value)) + resource_type=resource_type.__name__, value=value + ), + ) - def _list(self, resource_type, - paginated=True, base_path=None, **attrs): + def _list(self, resource_type, paginated=True, base_path=None, **attrs): """List a resource :param resource_type: The type of resource to list. This should @@ -622,9 +665,8 @@ def _list(self, resource_type, the ``resource_type``. """ return resource_type.list( - self, paginated=paginated, - base_path=base_path, - **attrs) + self, paginated=paginated, base_path=base_path, **attrs + ) def _head(self, resource_type, value=None, base_path=None, **attrs): """Retrieve a resource's header @@ -652,34 +694,43 @@ def _head(self, resource_type, value=None, base_path=None, **attrs): def _get_cleanup_dependencies(self): return None - def _service_cleanup(self, dry_run=True, client_status_queue=None, - identified_resources=None, filters=None, - resource_evaluation_fn=None): + def _service_cleanup( + self, + dry_run=True, + client_status_queue=None, + identified_resources=None, + filters=None, + resource_evaluation_fn=None, + ): return None - def _service_cleanup_del_res(self, del_fn, obj, dry_run=True, - client_status_queue=None, - identified_resources=None, - filters=None, - resource_evaluation_fn=None): + def _service_cleanup_del_res( + self, + del_fn, + obj, + dry_run=True, + client_status_queue=None, + identified_resources=None, + filters=None, + resource_evaluation_fn=None, + ): need_delete = False try: - if ( - resource_evaluation_fn - and callable(resource_evaluation_fn) - ): + if resource_evaluation_fn and callable(resource_evaluation_fn): # Ask a user-provided evaluation function if we need to delete # the resource - need_del = resource_evaluation_fn(obj, filters, - identified_resources) + need_del = resource_evaluation_fn( + obj, filters, identified_resources + ) if isinstance(need_del, bool): # Just double check function returned bool need_delete = need_del else: - need_delete = \ + need_delete = ( self._service_cleanup_resource_filters_evaluation( - obj, - filters=filters) + obj, filters=filters + ) + ) if need_delete: if client_status_queue: @@ -716,8 +767,10 @@ def _service_cleanup_resource_filters_evaluation(self, obj, filters=None): else: # There are filters set, but we can't get required # attribute, so skip the resource - self.log.debug('Requested cleanup attribute %s is not ' - 'available on the resource' % k) + self.log.debug( + 'Requested cleanup attribute %s is not ' + 'available on the resource' % k + ) part_cond.append(False) except Exception: self.log.exception('Error during condition evaluation') diff --git a/openstack/resource.py b/openstack/resource.py index 45614f31e..27c913e60 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -102,10 +102,20 @@ class _BaseComponent: # only once when the attribute is retrieved in the code. already_warned_deprecation = False - def __init__(self, name, type=None, default=None, alias=None, aka=None, - alternate_id=False, list_type=None, coerce_to_default=False, - deprecated=False, deprecation_reason=None, - **kwargs): + def __init__( + self, + name, + type=None, + default=None, + alias=None, + aka=None, + alternate_id=False, + list_type=None, + coerce_to_default=False, + deprecated=False, + deprecation_reason=None, + **kwargs, + ): """A typed descriptor for a component that makes up a Resource :param name: The name this component exists as on the server @@ -196,8 +206,11 @@ def warn_if_deprecated_property(self, value): if value and deprecated and not self.already_warned_deprecation: self.already_warned_deprecation = True if not deprecate_reason: - LOG.warning("The option [%s] has been deprecated. " - "Please avoid using it.", self.name) + LOG.warning( + "The option [%s] has been deprecated. " + "Please avoid using it.", + self.name, + ) else: LOG.warning(deprecate_reason) return value @@ -279,8 +292,9 @@ def __len__(self): @property def dirty(self): """Return a dict of modified attributes""" - return dict((key, self.attributes.get(key, None)) - for key in self._dirty) + return dict( + (key, self.attributes.get(key, None)) for key in self._dirty + ) def clean(self, only=None): """Signal that the resource no longer has modified attributes. @@ -304,7 +318,6 @@ def __init__(self, url, body, headers): class QueryParameters: - def __init__(self, *names, **mappings): """Create a dict of accepted query parameters @@ -342,7 +355,8 @@ def _validate(self, query, base_path=None, allow_unknown_params=False): expected_params = list(self._mapping.keys()) expected_params.extend( value.get('name', key) if isinstance(value, dict) else value - for key, value in self._mapping.items()) + for key, value in self._mapping.items() + ) if base_path: expected_params += utils.get_string_format_keys(base_path) @@ -353,12 +367,14 @@ def _validate(self, query, base_path=None, allow_unknown_params=False): else: if not allow_unknown_params: raise exceptions.InvalidResourceQuery( - message="Invalid query params: %s" % - ",".join(invalid_keys), - extra_data=invalid_keys) + message="Invalid query params: %s" + % ",".join(invalid_keys), + extra_data=invalid_keys, + ) else: known_keys = set(query.keys()).intersection( - set(expected_params)) + set(expected_params) + ) return {k: query[k] for k in known_keys} def _transpose(self, query, resource_type): @@ -385,7 +401,8 @@ def _transpose(self, query, resource_type): # single-argument (like int) and double-argument type functions. try: provide_resource_type = ( - len(inspect.getfullargspec(type_).args) > 1) + len(inspect.getfullargspec(type_).args) > 1 + ) except TypeError: provide_resource_type = False @@ -522,26 +539,24 @@ def __init__(self, _synchronized=False, connection=None, **attrs): self._unknown_attrs_in_body.update(attrs) self._body = _ComponentManager( - attributes=body, - synchronized=_synchronized) + attributes=body, synchronized=_synchronized + ) self._header = _ComponentManager( - attributes=header, - synchronized=_synchronized) + attributes=header, synchronized=_synchronized + ) self._uri = _ComponentManager( - attributes=uri, - synchronized=_synchronized) + attributes=uri, synchronized=_synchronized + ) self._computed = _ComponentManager( - attributes=computed, - synchronized=_synchronized) + attributes=computed, synchronized=_synchronized + ) if self.commit_jsonpatch or self.allow_patch: # We need the original body to compare against if _synchronized: self._original_body = self._body.attributes.copy() elif self.id: # Never record ID as dirty. - self._original_body = { - self._alternate_id() or 'id': self.id - } + self._original_body = {self._alternate_id() or 'id': self.id} else: self._original_body = {} if self._store_unknown_attrs_as_properties: @@ -568,8 +583,7 @@ def __init__(self, _synchronized=False, connection=None, **attrs): @classmethod def _attributes_iterator(cls, components=tuple([Body, Header])): - """Iterator over all Resource attributes - """ + """Iterator over all Resource attributes""" # isinstance stricly requires this to be a tuple # Since we're looking at class definitions we need to include # subclasses, so check the whole MRO. @@ -581,33 +595,40 @@ def _attributes_iterator(cls, components=tuple([Body, Header])): def __repr__(self): pairs = [ "%s=%s" % (k, v if v is not None else 'None') - for k, v in dict(itertools.chain( - self._body.attributes.items(), - self._header.attributes.items(), - self._uri.attributes.items(), - self._computed.attributes.items())).items() + for k, v in dict( + itertools.chain( + self._body.attributes.items(), + self._header.attributes.items(), + self._uri.attributes.items(), + self._computed.attributes.items(), + ) + ).items() ] args = ", ".join(pairs) - return "%s.%s(%s)" % ( - self.__module__, self.__class__.__name__, args) + return "%s.%s(%s)" % (self.__module__, self.__class__.__name__, args) def __eq__(self, comparand): """Return True if another resource has the same contents""" if not isinstance(comparand, Resource): return False - return all([ - self._body.attributes == comparand._body.attributes, - self._header.attributes == comparand._header.attributes, - self._uri.attributes == comparand._uri.attributes, - self._computed.attributes == comparand._computed.attributes - ]) + return all( + [ + self._body.attributes == comparand._body.attributes, + self._header.attributes == comparand._header.attributes, + self._uri.attributes == comparand._uri.attributes, + self._computed.attributes == comparand._computed.attributes, + ] + ) def warning_if_attribute_deprecated(self, attr, value): if value and self.deprecated: if not self.deprecation_reason: - LOG.warning("The option [%s] has been deprecated. " - "Please avoid using it.", attr) + LOG.warning( + "The option [%s] has been deprecated. " + "Please avoid using it.", + attr, + ) else: LOG.warning(self.deprecation_reason) @@ -632,7 +653,8 @@ def __getattribute__(self, name): if name in self._attr_aliases: # Hmm - not found. But hey, the alias exists... return object.__getattribute__( - self, self._attr_aliases[name]) + self, self._attr_aliases[name] + ) if self._allow_unknown_attrs_in_body: # Last chance, maybe it's in body as attribute which isn't # in the mapping at all... @@ -661,9 +683,10 @@ def __getitem__(self, name): if component.name == name: warnings.warn( 'Access to "%s[%s]" is deprecated. ' - 'Please access using "%s.%s" attribute.' % - (self.__class__, name, self.__class__, attr), - DeprecationWarning) + 'Please access using "%s.%s" attribute.' + % (self.__class__, name, self.__class__, attr), + DeprecationWarning, + ) return getattr(self, attr) raise KeyError(name) @@ -684,12 +707,14 @@ def __setitem__(self, name, value): " dict interface.".format( module=self.__module__, cls=self.__class__.__name__, - name=name)) + name=name, + ) + ) - def _attributes(self, remote_names=False, components=None, - include_aliases=True): - """Generate list of supported attributes - """ + def _attributes( + self, remote_names=False, components=None, include_aliases=True + ): + """Generate list of supported attributes""" attributes = [] if not components: @@ -1029,7 +1054,8 @@ def to_dict( components.append(Computed) if not components: raise ValueError( - "At least one of `body`, `headers` or `computed` must be True") + "At least one of `body`, `headers` or `computed` must be True" + ) # isinstance stricly requires this to be a tuple components = tuple(components) @@ -1059,7 +1085,8 @@ def to_dict( for raw in value: if isinstance(raw, Resource): converted.append( - raw.to_dict(_to_munch=_to_munch)) + raw.to_dict(_to_munch=_to_munch) + ) elif isinstance(raw, dict) and _to_munch: converted.append(munch.Munch(raw)) else: @@ -1078,8 +1105,11 @@ def to_dict( def _to_munch(self, original_names=True): """Convert this resource into a Munch compatible with shade.""" return self.to_dict( - body=True, headers=False, - original_names=original_names, _to_munch=True) + body=True, + headers=False, + original_names=original_names, + _to_munch=True, + ) def _unpack_properties_to_resource_root(self, body): if not body: @@ -1111,16 +1141,16 @@ def _prepare_request_body(self, patch, prepend_key): original_body = self._original_body else: new = self._unpack_properties_to_resource_root( - self._body.attributes) + self._body.attributes + ) original_body = self._unpack_properties_to_resource_root( - self._original_body) + self._original_body + ) # NOTE(gtema) sort result, since we might need validate it in tests body = sorted( - list(jsonpatch.make_patch( - original_body, - new).patch), - key=operator.itemgetter('path') + list(jsonpatch.make_patch(original_body, new).patch), + key=operator.itemgetter('path'), ) else: if not self._store_unknown_attrs_as_properties: @@ -1128,7 +1158,8 @@ def _prepare_request_body(self, patch, prepend_key): body = self._body.dirty else: body = self._unpack_properties_to_resource_root( - self._body.dirty) + self._body.dirty + ) if prepend_key and self.resource_key is not None: body = {self.resource_key: body} @@ -1176,7 +1207,8 @@ def _prepare_request( if requires_id: if self.id is None: raise exceptions.InvalidRequest( - "Request requires an ID but none was found") + "Request requires an ID but none was found" + ) uri = utils.urljoin(uri, self.id) @@ -1217,7 +1249,8 @@ def _translate_response(self, response, has_body=None, error_message=None): self._unknown_attrs_in_body.update(body) elif self._store_unknown_attrs_as_properties: body_attrs = self._pack_attrs_under_properties( - body_attrs, body) + body_attrs, body + ) self._body.attributes.update(body_attrs) self._body.clean() @@ -1256,7 +1289,8 @@ def _get_session(cls, session): raise ValueError( "The session argument to Resource methods requires either an" " instance of an openstack.proxy.Proxy object or at the very least" - " a raw keystoneauth1.adapter.Adapter.") + " a raw keystoneauth1.adapter.Adapter." + ) @classmethod def _get_microversion_for_list(cls, session): @@ -1278,8 +1312,9 @@ def _get_microversion_for_list(cls, session): if session.default_microversion: return session.default_microversion - return utils.maximum_supported_microversion(session, - cls._max_microversion) + return utils.maximum_supported_microversion( + session, cls._max_microversion + ) def _get_microversion_for(self, session, action): """Get microversion to use for the given action. @@ -1298,7 +1333,11 @@ def _get_microversion_for(self, session, action): return self._get_microversion_for_list(session) def _assert_microversion_for( - self, session, action, expected, error_message=None, + self, + session, + action, + expected, + error_message=None, ): """Enforce that the microversion for action satisfies the requirement. @@ -1311,6 +1350,7 @@ def _assert_microversion_for( :raises: :exc:`~openstack.exceptions.NotSupported` if the version used for the action is lower than the expected one. """ + def _raise(message): if error_message: error_message.rstrip('.') @@ -1323,16 +1363,19 @@ def _raise(message): if expected is None: return actual elif actual is None: - message = ("API version %s is required, but the default " - "version will be used.") % expected + message = ( + "API version %s is required, but the default " + "version will be used." + ) % expected _raise(message) actual_n = discover.normalize_version_number(actual) expected_n = discover.normalize_version_number(expected) if actual_n < expected_n: - message = ("API version %(expected)s is required, but %(actual)s " - "will be used.") % {'expected': expected, - 'actual': actual} + message = ( + "API version %(expected)s is required, but %(actual)s " + "will be used." + ) % {'expected': expected, 'actual': actual} _raise(message) return actual @@ -1357,33 +1400,51 @@ def create(self, session, prepend_key=True, base_path=None, **params): session = self._get_session(session) microversion = self._get_microversion_for(session, 'create') - requires_id = (self.create_requires_id - if self.create_requires_id is not None - else self.create_method == 'PUT') + requires_id = ( + self.create_requires_id + if self.create_requires_id is not None + else self.create_method == 'PUT' + ) if self.create_exclude_id_from_body: self._body._dirty.discard("id") if self.create_method == 'PUT': - request = self._prepare_request(requires_id=requires_id, - prepend_key=prepend_key, - base_path=base_path) - response = session.put(request.url, - json=request.body, headers=request.headers, - microversion=microversion, params=params) + request = self._prepare_request( + requires_id=requires_id, + prepend_key=prepend_key, + base_path=base_path, + ) + response = session.put( + request.url, + json=request.body, + headers=request.headers, + microversion=microversion, + params=params, + ) elif self.create_method == 'POST': - request = self._prepare_request(requires_id=requires_id, - prepend_key=prepend_key, - base_path=base_path) - response = session.post(request.url, - json=request.body, headers=request.headers, - microversion=microversion, params=params) + request = self._prepare_request( + requires_id=requires_id, + prepend_key=prepend_key, + base_path=base_path, + ) + response = session.post( + request.url, + json=request.body, + headers=request.headers, + microversion=microversion, + params=params, + ) else: raise exceptions.ResourceFailure( - "Invalid create method: %s" % self.create_method) + "Invalid create method: %s" % self.create_method + ) - has_body = (self.has_body if self.create_returns_body is None - else self.create_returns_body) + has_body = ( + self.has_body + if self.create_returns_body is None + else self.create_returns_body + ) self.microversion = microversion self._translate_response(response, has_body=has_body) # direct comparision to False since we need to rule out None @@ -1420,22 +1481,28 @@ def bulk_create( if not cls.allow_create: raise exceptions.MethodNotSupported(cls, "create") - if not (data and isinstance(data, list) - and all([isinstance(x, dict) for x in data])): + if not ( + data + and isinstance(data, list) + and all([isinstance(x, dict) for x in data]) + ): raise ValueError('Invalid data passed: %s' % data) session = cls._get_session(session) microversion = cls._get_microversion_for(cls, session, 'create') - requires_id = (cls.create_requires_id - if cls.create_requires_id is not None - else cls.create_method == 'PUT') + requires_id = ( + cls.create_requires_id + if cls.create_requires_id is not None + else cls.create_method == 'PUT' + ) if cls.create_method == 'PUT': method = session.put elif cls.create_method == 'POST': method = session.post else: raise exceptions.ResourceFailure( - "Invalid create method: %s" % cls.create_method) + "Invalid create method: %s" % cls.create_method + ) body = [] resources = [] @@ -1447,15 +1514,21 @@ def bulk_create( # to return newly created resource objects. resource = cls.new(connection=session._get_connection(), **attrs) resources.append(resource) - request = resource._prepare_request(requires_id=requires_id, - base_path=base_path) + request = resource._prepare_request( + requires_id=requires_id, base_path=base_path + ) body.append(request.body) if prepend_key: body = {cls.resources_key: body} - response = method(request.url, json=body, headers=request.headers, - microversion=microversion, params=params) + response = method( + request.url, + json=body, + headers=request.headers, + microversion=microversion, + params=params, + ) exceptions.raise_from_response(response) data = response.json() @@ -1465,14 +1538,22 @@ def bulk_create( if not isinstance(data, list): data = [data] - has_body = (cls.has_body if cls.create_returns_body is None - else cls.create_returns_body) + has_body = ( + cls.has_body + if cls.create_returns_body is None + else cls.create_returns_body + ) if has_body and cls.create_returns_body is False: return (r.fetch(session) for r in resources) else: - return (cls.existing(microversion=microversion, - connection=session._get_connection(), - **res_dict) for res_dict in data) + return ( + cls.existing( + microversion=microversion, + connection=session._get_connection(), + **res_dict, + ) + for res_dict in data + ) def fetch( self, @@ -1505,13 +1586,17 @@ def fetch( if not self.allow_fetch: raise exceptions.MethodNotSupported(self, "fetch") - request = self._prepare_request(requires_id=requires_id, - base_path=base_path) + request = self._prepare_request( + requires_id=requires_id, base_path=base_path + ) session = self._get_session(session) microversion = self._get_microversion_for(session, 'fetch') - response = session.get(request.url, microversion=microversion, - params=params, - skip_cache=skip_cache) + response = session.get( + request.url, + microversion=microversion, + params=params, + skip_cache=skip_cache, + ) kwargs = {} if error_message: kwargs['error_message'] = error_message @@ -1541,8 +1626,7 @@ def head(self, session, base_path=None): session = self._get_session(session) microversion = self._get_microversion_for(session, 'fetch') - response = session.head(request.url, - microversion=microversion) + response = session.head(request.url, microversion=microversion) self.microversion = microversion self._translate_response(response, has_body=False) @@ -1551,8 +1635,9 @@ def head(self, session, base_path=None): @property def requires_commit(self): """Whether the next commit() call will do anything.""" - return (self._body.dirty or self._header.dirty - or self.allow_empty_commit) + return ( + self._body.dirty or self._header.dirty or self.allow_empty_commit + ) def commit( self, @@ -1596,14 +1681,19 @@ def commit( if self.commit_jsonpatch: kwargs['patch'] = True - request = self._prepare_request(prepend_key=prepend_key, - base_path=base_path, - **kwargs) + request = self._prepare_request( + prepend_key=prepend_key, base_path=base_path, **kwargs + ) microversion = self._get_microversion_for(session, 'commit') - return self._commit(session, request, self.commit_method, microversion, - has_body=has_body, - retry_on_conflict=retry_on_conflict) + return self._commit( + session, + request, + self.commit_method, + microversion, + has_body=has_body, + retry_on_conflict=retry_on_conflict, + ) def _commit( self, @@ -1629,11 +1719,16 @@ def _commit( call = getattr(session, method.lower()) except AttributeError: raise exceptions.ResourceFailure( - "Invalid commit method: %s" % method) + "Invalid commit method: %s" % method + ) - response = call(request.url, json=request.body, - headers=request.headers, microversion=microversion, - **kwargs) + response = call( + request.url, + json=request.body, + headers=request.headers, + microversion=microversion, + **kwargs, + ) self.microversion = microversion self._translate_response(response, has_body=has_body) @@ -1708,15 +1803,21 @@ def patch( if not self.allow_patch: raise exceptions.MethodNotSupported(self, "patch") - request = self._prepare_request(prepend_key=prepend_key, - base_path=base_path, patch=True) + request = self._prepare_request( + prepend_key=prepend_key, base_path=base_path, patch=True + ) microversion = self._get_microversion_for(session, 'patch') if patch: request.body += self._convert_patch(patch) - return self._commit(session, request, 'PATCH', microversion, - has_body=has_body, - retry_on_conflict=retry_on_conflict) + return self._commit( + session, + request, + 'PATCH', + microversion, + has_body=has_body, + retry_on_conflict=retry_on_conflict, + ) def delete(self, session, error_message=None, **kwargs): """Delete the remote resource based on this instance. @@ -1750,8 +1851,8 @@ def _raw_delete(self, session, **kwargs): microversion = self._get_microversion_for(session, 'delete') return session.delete( - request.url, headers=request.headers, - microversion=microversion) + request.url, headers=request.headers, microversion=microversion + ) @classmethod def list( @@ -1802,8 +1903,10 @@ def list( if base_path is None: base_path = cls.base_path params = cls._query_mapping._validate( - params, base_path=base_path, - allow_unknown_params=allow_unknown_params) + params, + base_path=base_path, + allow_unknown_params=allow_unknown_params, + ) query_params = cls._query_mapping._transpose(params, cls) uri = base_path % params uri_params = {} @@ -1812,10 +1915,7 @@ def list( for k, v in params.items(): # We need to gather URI parts to set them on the resource later - if ( - hasattr(cls, k) - and isinstance(getattr(cls, k), URI) - ): + if hasattr(cls, k) and isinstance(getattr(cls, k), URI): uri_params[k] = v # Track the total number of resources yielded so we can paginate @@ -1827,7 +1927,8 @@ def list( uri, headers={"Accept": "application/json"}, params=query_params.copy(), - microversion=microversion) + microversion=microversion, + ) exceptions.raise_from_response(response) data = response.json() @@ -1857,14 +1958,16 @@ def list( value = cls.existing( microversion=microversion, connection=session._get_connection(), - **raw_resource) + **raw_resource, + ) marker = value.id yield value total_yielded += 1 if resources and paginated: uri, next_params = cls._get_next_link( - uri, response, data, marker, limit, total_yielded) + uri, response, data, marker, limit, total_yielded + ) try: if next_params['marker'] == last_marker: # If next page marker is same as what we were just @@ -1905,7 +2008,7 @@ def _get_next_link(cls, uri, response, data, marker, limit, total_yielded): # Glance has a next field in the main body next_link = next_link or data.get('next') if next_link and next_link.startswith('/v'): - next_link = next_link[next_link.find('/', 1) + 1:] + next_link = next_link[next_link.find('/', 1) + 1 :] if not next_link and 'next' in response.links: # RFC5988 specifies Link headers and requests parses them if they @@ -1956,7 +2059,7 @@ def _get_one_match(cls, name_or_id, results): the_result = maybe_result else: msg = "More than one %s exists with the name '%s'." - msg = (msg % (cls.__name__, name_or_id)) + msg = msg % (cls.__name__, name_or_id) raise exceptions.DuplicateResource(msg) return the_result @@ -1998,9 +2101,8 @@ def find( # Try to short-circuit by looking directly for a matching ID. try: match = cls.existing( - id=name_or_id, - connection=session._get_connection(), - **params) + id=name_or_id, connection=session._get_connection(), **params + ) return match.fetch(session, **params) except (exceptions.NotFoundException, exceptions.BadRequestException): # NOTE(gtema): There are few places around openstack that return @@ -2010,8 +2112,10 @@ def find( if list_base_path: params['base_path'] = list_base_path - if ('name' in cls._query_mapping._mapping.keys() - and 'name' not in params): + if ( + 'name' in cls._query_mapping._mapping.keys() + and 'name' not in params + ): params['name'] = name_or_id data = cls.list(session, **params) @@ -2023,7 +2127,8 @@ def find( if ignore_missing: return None raise exceptions.ResourceNotFound( - "No %s found for %s" % (cls.__name__, name_or_id)) + "No %s found for %s" % (cls.__name__, name_or_id) + ) def _normalize_status(status): @@ -2076,18 +2181,20 @@ def wait_for_status( failures = [f.lower() for f in failures] name = "{res}:{id}".format(res=resource.__class__.__name__, id=resource.id) msg = "Timeout waiting for {name} to transition to {status}".format( - name=name, status=status) + name=name, status=status + ) for count in utils.iterate_timeout( - timeout=wait, - message=msg, - wait=interval): + timeout=wait, message=msg, wait=interval + ): resource = resource.fetch(session, skip_cache=True) if not resource: raise exceptions.ResourceFailure( "{name} went away while waiting for {status}".format( - name=name, status=status)) + name=name, status=status + ) + ) new_status = getattr(resource, attribute) normalized_status = _normalize_status(new_status) @@ -2096,10 +2203,17 @@ def wait_for_status( elif normalized_status in failures: raise exceptions.ResourceFailure( "{name} transitioned to failure state {status}".format( - name=name, status=new_status)) + name=name, status=new_status + ) + ) - LOG.debug('Still waiting for resource %s to reach state %s, ' - 'current state is %s', name, status, new_status) + LOG.debug( + 'Still waiting for resource %s to reach state %s, ' + 'current state is %s', + name, + status, + new_status, + ) def wait_for_delete(session, resource, interval, wait): @@ -2118,11 +2232,12 @@ def wait_for_delete(session, resource, interval, wait): """ orig_resource = resource for count in utils.iterate_timeout( - timeout=wait, - message="Timeout waiting for {res}:{id} to delete".format( - res=resource.__class__.__name__, - id=resource.id), - wait=interval): + timeout=wait, + message="Timeout waiting for {res}:{id} to delete".format( + res=resource.__class__.__name__, id=resource.id + ), + wait=interval, + ): try: resource = resource.fetch(session, skip_cache=True) if not resource: diff --git a/tox.ini b/tox.ini index 69e3869ec..ed565d2b4 100644 --- a/tox.ini +++ b/tox.ini @@ -110,12 +110,13 @@ application-import-names = openstack # The following are ignored on purpose. It's not super worth it to fix them. # However, if you feel strongly about it, patches will be accepted to fix them # if they fix ALL of the occurances of one and only one of them. +# E203 Black will put spaces after colons in list comprehensions # H238 New Style Classes are the default in Python3 # H4 Are about docstrings and there's just a huge pile of pre-existing issues. # W503 Is supposed to be off by default but in the latest pycodestyle isn't. # Also, both openstacksdk and Donald Knuth disagree with the rule. Line # breaks should occur before the binary operator for readability. -ignore = H238,H4,W503 +ignore = E203, H238, H4, W503 import-order-style = pep8 show-source = True exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build,openstack/_services_mixin.py From 299ab27b88d55b64d2eedabc270ef3087d180926 Mon Sep 17 00:00:00 2001 From: Nurmatov Mamatisa Date: Tue, 22 Feb 2022 14:36:57 +0300 Subject: [PATCH 3055/3836] Add network address_group proxy doc and unit tests Change-Id: I94f721a5c1b2174314ef02c683084dabb0593882 --- doc/source/user/proxies/network.rst | 9 +++ openstack/tests/unit/network/v2/test_proxy.py | 56 +++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/doc/source/user/proxies/network.rst b/doc/source/user/proxies/network.rst index 3fdbc7306..441277c67 100644 --- a/doc/source/user/proxies/network.rst +++ b/doc/source/user/proxies/network.rst @@ -83,6 +83,15 @@ Security Group Operations security_groups, create_security_group_rule, create_security_group_rules, delete_security_group_rule +Address Group Operations +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + :noindex: + :members: create_address_group, delete_address_group, find_address_group, + get_address_group, address_groups, update_address_group, + add_addresses_to_address_group, remove_addresses_from_address_group + Availability Zone Operations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index d3093f445..6f9d30b2c 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -15,6 +15,7 @@ from openstack import exceptions from openstack.network.v2 import _proxy +from openstack.network.v2 import address_group from openstack.network.v2 import address_scope from openstack.network.v2 import agent from openstack.network.v2 import auto_allocated_topology @@ -112,6 +113,61 @@ def verify_delete( mock_method=mock_method) +class TestNetworkAddressGroup(TestNetworkProxy): + def test_address_group_create_attrs(self): + self.verify_create(self.proxy.create_address_group, + address_group.AddressGroup) + + def test_address_group_delete(self): + self.verify_delete(self.proxy.delete_address_group, + address_group.AddressGroup, + False) + + def test_address_group_delete_ignore(self): + self.verify_delete(self.proxy.delete_address_group, + address_group.AddressGroup, + True) + + def test_address_group_find(self): + self.verify_find(self.proxy.find_address_group, + address_group.AddressGroup) + + def test_address_group_get(self): + self.verify_get(self.proxy.get_address_group, + address_group.AddressGroup) + + def test_address_groups(self): + self.verify_list(self.proxy.address_groups, + address_group.AddressGroup) + + def test_address_group_update(self): + self.verify_update(self.proxy.update_address_group, + address_group.AddressGroup) + + @mock.patch('openstack.network.v2._proxy.Proxy.' + 'add_addresses_to_address_group') + def test_add_addresses_to_address_group(self, add_addresses): + data = mock.sentinel + + self.proxy.add_addresses_to_address_group(address_group.AddressGroup, + data) + + add_addresses.assert_called_once_with(address_group.AddressGroup, + data) + + @mock.patch('openstack.network.v2._proxy.Proxy.' + 'remove_addresses_from_address_group') + def test_remove_addresses_from_address_group(self, remove_addresses): + data = mock.sentinel + + self.proxy.remove_addresses_from_address_group( + address_group.AddressGroup, + data) + + remove_addresses.assert_called_once_with(address_group.AddressGroup, + data) + + class TestNetworkAddressScope(TestNetworkProxy): def test_address_scope_create_attrs(self): self.verify_create(self.proxy.create_address_scope, From 41f45fb12afaef8efcc2d6a7bda55bb83c6b7178 Mon Sep 17 00:00:00 2001 From: Yang JianFeng Date: Mon, 31 Aug 2020 11:36:21 +0000 Subject: [PATCH 3056/3836] Add CRUD methods for Neutron router ndp proxy Depends-On: https://review.opendev.org/#/c/823000/ Change-Id: Ic280e7f725f86bd32a7e2cc96528a7de91f1fed2 --- doc/source/user/proxies/network.rst | 8 + doc/source/user/resources/network/index.rst | 1 + .../user/resources/network/v2/ndp_proxy.rst | 12 ++ openstack/network/v2/_proxy.py | 91 +++++++++++ openstack/network/v2/ndp_proxy.py | 56 +++++++ openstack/network/v2/router.py | 2 + .../functional/network/v2/test_ndp_proxy.py | 147 ++++++++++++++++++ .../tests/unit/network/v2/test_ndp_proxy.py | 50 ++++++ openstack/tests/unit/network/v2/test_proxy.py | 26 ++++ .../tests/unit/network/v2/test_router.py | 2 + 10 files changed, 395 insertions(+) create mode 100644 doc/source/user/resources/network/v2/ndp_proxy.rst create mode 100644 openstack/network/v2/ndp_proxy.py create mode 100644 openstack/tests/functional/network/v2/test_ndp_proxy.py create mode 100644 openstack/tests/unit/network/v2/test_ndp_proxy.py diff --git a/doc/source/user/proxies/network.rst b/doc/source/user/proxies/network.rst index 3fdbc7306..022d90fc6 100644 --- a/doc/source/user/proxies/network.rst +++ b/doc/source/user/proxies/network.rst @@ -281,3 +281,11 @@ Local IP Operations local_ips, update_local_ip, create_local_ip_association, delete_local_ip_association, find_local_ip_association, get_local_ip_association, local_ip_associations + +Ndp Proxy Operations +^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + :noindex: + :members: create_ndp_proxy, get_ndp_proxy, find_ndp_proxy, delete_ndp_proxy, + ndp_proxies, update_ndp_proxy diff --git a/doc/source/user/resources/network/index.rst b/doc/source/user/resources/network/index.rst index 4e9cc2ea0..d711a4d25 100644 --- a/doc/source/user/resources/network/index.rst +++ b/doc/source/user/resources/network/index.rst @@ -21,6 +21,7 @@ Network Resources v2/local_ip_association v2/metering_label v2/metering_label_rule + v2/ndp_proxy v2/network v2/network_ip_availability v2/network_segment_range diff --git a/doc/source/user/resources/network/v2/ndp_proxy.rst b/doc/source/user/resources/network/v2/ndp_proxy.rst new file mode 100644 index 000000000..c5479e79b --- /dev/null +++ b/doc/source/user/resources/network/v2/ndp_proxy.rst @@ -0,0 +1,12 @@ +openstack.network.v2.ndp_proxy +============================== + +.. automodule:: openstack.network.v2.ndp_proxy + +The NDPProxy Class +------------------ + +The ``NDPProxy`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.ndp_proxy.NDPProxy + :members: diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index b47da00c5..cc8663a69 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -34,6 +34,7 @@ from openstack.network.v2 import local_ip_association as _local_ip_association from openstack.network.v2 import metering_label as _metering_label from openstack.network.v2 import metering_label_rule as _metering_label_rule +from openstack.network.v2 import ndp_proxy as _ndp_proxy from openstack.network.v2 import network as _network from openstack.network.v2 import network_ip_availability from openstack.network.v2 import network_segment_range as \ @@ -3390,6 +3391,96 @@ def remove_router_from_agent(self, agent, router): router = self._get_resource(_router.Router, router) return agent.remove_router_from_agent(self, router.id) + def create_ndp_proxy(self, **attrs): + """Create a new ndp proxy from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.network.v2.ndp_proxy.NDPProxxy`, + comprised of the properties on the NDPProxy class. + + :returns: The results of ndp proxy creation + :rtype: :class:`~openstack.network.v2.ndp_proxy.NDPProxxy` + """ + return self._create(_ndp_proxy.NDPProxy, **attrs) + + def get_ndp_proxy(self, ndp_proxy): + """Get a single ndp proxy + + :param ndp_proxy: The value can be the ID of a ndp proxy + or a :class:`~openstack.network.v2.ndp_proxy.NDPProxy` + instance. + + :returns: One + :class:`~openstack.network.v2.ndp_proxy.NDPProxy` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_ndp_proxy.NDPProxy, ndp_proxy) + + def find_ndp_proxy(self, ndp_proxy_id, + ignore_missing=True, **args): + """Find a single ndp proxy + + :param ndp_proxy_id: The ID of a ndp proxy. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the resource does not exist. When set to ``True``, None will be + returned when attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. + :returns: + One :class:`~openstack.network.v2.ndp_proxy.NDPProxy` or None + """ + return self._find( + _ndp_proxy.NDPProxy, ndp_proxy_id, + ignore_missing=ignore_missing, + **args) + + def delete_ndp_proxy(self, ndp_proxy, ignore_missing=True): + """Delete a ndp proxy + + :param ndp_proxy: The value can be the ID of a ndp proxy + or a :class:`~openstack.network.v2.ndp_proxy.NDPProxy` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the router does not exist. When set to ``True``, no exception will + be set when attempting to delete a nonexistent ndp proxy. + + :returns: ``None`` + """ + self._delete( + _ndp_proxy.NDPProxy, ndp_proxy, + ignore_missing=ignore_missing) + + def ndp_proxies(self, **query): + """Return a generator of ndp proxies + + :param dict query: Optional query parameters to be sent to limit + the resources being returned. Valid parameters are: + + * ``router_id``: The ID fo the router + * ``port_id``: The ID of internal port. + * ``ip_address``: The internal IP address + + :returns: A generator of port forwarding objects + :rtype: :class:`~openstack.network.v2.port_forwarding.PortForwarding` + """ + return self._list(_ndp_proxy.NDPProxy, paginated=False, **query) + + def update_ndp_proxy(self, ndp_proxy, **attrs): + """Update a ndp proxy + + :param ndp_proxy: The value can be the ID of a ndp proxy or a + :class:`~openstack.network.v2.ndp_proxy.NDPProxy` instance. + :param dict attrs: The attributes to update on the ip represented + by ``value``. + + :returns: The updated ndp_proxy + :rtype: :class:`~openstack.network.v2.ndp_proxy.NDPProxy` + """ + return self._update(_ndp_proxy.NDPProxy, ndp_proxy, **attrs) + def create_firewall_group(self, **attrs): """Create a new firewall group from attributes diff --git a/openstack/network/v2/ndp_proxy.py b/openstack/network/v2/ndp_proxy.py new file mode 100644 index 000000000..cff0b8302 --- /dev/null +++ b/openstack/network/v2/ndp_proxy.py @@ -0,0 +1,56 @@ +# 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 resource + + +class NDPProxy(resource.Resource): + resource_name = "ndp proxy" + resource_key = 'ndp_proxy' + resources_key = 'ndp_proxies' + base_path = '/ndp_proxies' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + _allow_unknown_attrs_in_body = True + + _query_mapping = resource.QueryParameters( + "sort_key", "sort_dir", + 'name', 'description', 'project_id', + 'router_id', 'port_id', 'ip_address') + + # Properties + #: Timestamp at which the NDP proxy was created. + created_at = resource.Body('created_at') + #: The description + description = resource.Body('description') + #: The ID of the NDP proxy. + id = resource.Body('id') + #: The internal IP address + ip_address = resource.Body('ip_address') + # The name of ndp proxy + name = resource.Body('name') + #: The ID of internal port + port_id = resource.Body('port_id') + #: The ID of the project that owns the NDP proxy. + project_id = resource.Body('project_id') + #: The NDP proxy revision number. + revision_number = resource.Body('revision_number') + #: The ID of Router + router_id = resource.Body('router_id') + #: Timestamp at which the NDP proxy was last updated. + updated_at = resource.Body('updated_at') diff --git a/openstack/network/v2/router.py b/openstack/network/v2/router.py index 3d511cba1..6a5719e4e 100644 --- a/openstack/network/v2/router.py +++ b/openstack/network/v2/router.py @@ -49,6 +49,8 @@ class Router(_base.NetworkResource, tag.TagMixin): created_at = resource.Body('created_at') #: The router description. description = resource.Body('description') + #: The ndp proxy state of the router + enable_ndp_proxy = resource.Body('enable_ndp_proxy', type=bool) #: The ``network_id``, for the external gateway. *Type: dict* external_gateway_info = resource.Body('external_gateway_info', type=dict) #: The ID of the flavor. diff --git a/openstack/tests/functional/network/v2/test_ndp_proxy.py b/openstack/tests/functional/network/v2/test_ndp_proxy.py new file mode 100644 index 000000000..b81a8ffcc --- /dev/null +++ b/openstack/tests/functional/network/v2/test_ndp_proxy.py @@ -0,0 +1,147 @@ +# 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.network.v2 import ndp_proxy as _ndp_proxy +from openstack.network.v2 import network +from openstack.network.v2 import port +from openstack.network.v2 import router +from openstack.network.v2 import subnet +from openstack.tests.functional import base + + +class TestNDPProxy(base.BaseFunctionalTest): + + IPV6 = 6 + EXT_CIDR = "2002::1:0/112" + INT_CIDR = "2002::2:0/112" + EXT_NET_ID = None + INT_NET_ID = None + EXT_SUB_ID = None + INT_SUB_ID = None + ROT_ID = None + INTERNAL_PORT_ID = None + + def setUp(self): + super(TestNDPProxy, self).setUp() + + if not self.conn.network.find_extension('l3-ndp-proxy'): + self.skipTest('L3 ndp proxy extension disabled') + + self.ROT_NAME = self.getUniqueString() + self.EXT_NET_NAME = self.getUniqueString() + self.EXT_SUB_NAME = self.getUniqueString() + self.INT_NET_NAME = self.getUniqueString() + self.INT_SUB_NAME = self.getUniqueString() + # Create Exeternal Network + args = {'router:external': True} + net = self._create_network(self.EXT_NET_NAME, **args) + self.EXT_NET_ID = net.id + sub = self._create_subnet( + self.EXT_SUB_NAME, self.EXT_NET_ID, self.EXT_CIDR) + self.EXT_SUB_ID = sub.id + # Create Internal Network + net = self._create_network(self.INT_NET_NAME) + self.INT_NET_ID = net.id + sub = self._create_subnet( + self.INT_SUB_NAME, self.INT_NET_ID, self.INT_CIDR) + self.INT_SUB_ID = sub.id + # Create Router + args = {'external_gateway_info': {'network_id': self.EXT_NET_ID}, + 'enable_ndp_proxy': True} + sot = self.conn.network.create_router(name=self.ROT_NAME, **args) + assert isinstance(sot, router.Router) + self.assertEqual(self.ROT_NAME, sot.name) + self.ROT_ID = sot.id + self.ROT = sot + # Add Router's Interface to Internal Network + sot = self.ROT.add_interface( + self.conn.network, subnet_id=self.INT_SUB_ID) + self.assertEqual(sot['subnet_id'], self.INT_SUB_ID) + # Create Port in Internal Network + prt = self.conn.network.create_port(network_id=self.INT_NET_ID) + assert isinstance(prt, port.Port) + self.INTERNAL_PORT_ID = prt.id + self.INTERNAL_IP_ADDRESS = prt.fixed_ips[0]['ip_address'] + # Create ndp proxy + np = self.conn.network.create_ndp_proxy( + router_id=self.ROT_ID, port_id=self.INTERNAL_PORT_ID) + assert isinstance(np, _ndp_proxy.NDPProxy) + self.NP = np + + def tearDown(self): + sot = self.conn.network.delete_ndp_proxy( + self.NP.id, ignore_missing=False) + self.assertIsNone(sot) + sot = self.conn.network.delete_port( + self.INTERNAL_PORT_ID, ignore_missing=False) + self.assertIsNone(sot) + sot = self.ROT.remove_interface( + self.conn.network, subnet_id=self.INT_SUB_ID) + self.assertEqual(sot['subnet_id'], self.INT_SUB_ID) + sot = self.conn.network.delete_router( + self.ROT_ID, ignore_missing=False) + self.assertIsNone(sot) + sot = self.conn.network.delete_subnet( + self.EXT_SUB_ID, ignore_missing=False) + self.assertIsNone(sot) + sot = self.conn.network.delete_network( + self.EXT_NET_ID, ignore_missing=False) + self.assertIsNone(sot) + sot = self.conn.network.delete_subnet( + self.INT_SUB_ID, ignore_missing=False) + self.assertIsNone(sot) + sot = self.conn.network.delete_network( + self.INT_NET_ID, ignore_missing=False) + self.assertIsNone(sot) + super(TestNDPProxy, self).tearDown() + + def _create_network(self, name, **args): + self.name = name + net = self.conn.network.create_network(name=name, **args) + assert isinstance(net, network.Network) + self.assertEqual(self.name, net.name) + return net + + def _create_subnet(self, name, net_id, cidr): + self.name = name + self.net_id = net_id + self.cidr = cidr + sub = self.conn.network.create_subnet( + name=self.name, + ip_version=self.IPV6, + network_id=self.net_id, + cidr=self.cidr) + assert isinstance(sub, subnet.Subnet) + self.assertEqual(self.name, sub.name) + return sub + + def test_find(self): + sot = self.conn.network.find_ndp_proxy(self.NP.id) + self.assertEqual(self.ROT_ID, sot.router_id) + self.assertEqual(self.INTERNAL_PORT_ID, sot.port_id) + self.assertEqual(self.INTERNAL_IP_ADDRESS, sot.ip_address) + + def test_get(self): + sot = self.conn.network.get_ndp_proxy(self.NP.id) + self.assertEqual(self.ROT_ID, sot.router_id) + self.assertEqual(self.INTERNAL_PORT_ID, sot.port_id) + self.assertEqual(self.INTERNAL_IP_ADDRESS, sot.ip_address) + + def test_list(self): + np_ids = [o.id for o in self.conn.network.ndp_proxies()] + self.assertIn(self.NP.id, np_ids) + + def test_update(self): + description = "balabalbala" + sot = self.conn.network.update_ndp_proxy( + self.NP.id, description=description) + self.assertEqual(description, sot.description) diff --git a/openstack/tests/unit/network/v2/test_ndp_proxy.py b/openstack/tests/unit/network/v2/test_ndp_proxy.py new file mode 100644 index 000000000..4d7d7576e --- /dev/null +++ b/openstack/tests/unit/network/v2/test_ndp_proxy.py @@ -0,0 +1,50 @@ +# 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.network.v2 import ndp_proxy +from openstack.tests.unit import base + +EXAMPLE = { + 'id': 'np_id', + 'name': 'np_name', + 'router_id': 'router-uuid', + 'port_id': 'port-uuid', + 'project_id': 'project-uuid', + 'description': 'fake-desc', + 'created_at': '2021-12-21T19:14:57.233772', + 'updated_at': '2021-12-21T19:14:57.233772', +} + + +class TestNDPProxy(base.TestCase): + + def test_basic(self): + sot = ndp_proxy.NDPProxy() + self.assertEqual('ndp_proxy', sot.resource_key) + self.assertEqual('ndp_proxies', sot.resources_key) + self.assertEqual('/ndp_proxies', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = ndp_proxy.NDPProxy(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['router_id'], sot.router_id) + self.assertEqual(EXAMPLE['port_id'], sot.port_id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['created_at'], sot.created_at) + self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index d3093f445..c3add7082 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -35,6 +35,7 @@ from openstack.network.v2 import local_ip_association from openstack.network.v2 import metering_label from openstack.network.v2 import metering_label_rule +from openstack.network.v2 import ndp_proxy from openstack.network.v2 import network from openstack.network.v2 import network_ip_availability from openstack.network.v2 import network_segment_range @@ -1773,3 +1774,28 @@ def test_update_l3_conntrack_helper(self): l3_conntrack_helper.ConntrackHelper, 'conntrack_helper_id'], expected_kwargs={'router_id': ROUTER_ID, 'foo': 'bar'}) + + +class TestNetworkNDPProxy(TestNetworkProxy): + def test_ndp_proxy_create_attrs(self): + self.verify_create(self.proxy.create_ndp_proxy, ndp_proxy.NDPProxy) + + def test_ndp_proxy_delete(self): + self.verify_delete(self.proxy.delete_ndp_proxy, ndp_proxy.NDPProxy, + False) + + def test_ndp_proxy_delete_ignore(self): + self.verify_delete(self.proxy.delete_ndp_proxy, ndp_proxy.NDPProxy, + True) + + def test_ndp_proxy_find(self): + self.verify_find(self.proxy.find_ndp_proxy, ndp_proxy.NDPProxy) + + def test_ndp_proxy_get(self): + self.verify_get(self.proxy.get_ndp_proxy, ndp_proxy.NDPProxy) + + def test_ndp_proxies(self): + self.verify_list(self.proxy.ndp_proxies, ndp_proxy.NDPProxy) + + def test_ndp_proxy_update(self): + self.verify_update(self.proxy.update_ndp_proxy, ndp_proxy.NDPProxy) diff --git a/openstack/tests/unit/network/v2/test_router.py b/openstack/tests/unit/network/v2/test_router.py index c109a5fd5..5f4dc3c53 100644 --- a/openstack/tests/unit/network/v2/test_router.py +++ b/openstack/tests/unit/network/v2/test_router.py @@ -26,6 +26,7 @@ 'created_at': 'timestamp1', 'description': '3', 'distributed': False, + 'enable_ndp_proxy': True, 'external_gateway_info': {'4': 4}, 'flavor_id': '5', 'ha': False, @@ -83,6 +84,7 @@ def test_make_it(self): sot.availability_zones) self.assertEqual(EXAMPLE['created_at'], sot.created_at) self.assertEqual(EXAMPLE['description'], sot.description) + self.assertTrue(sot.enable_ndp_proxy) self.assertFalse(sot.is_distributed) self.assertEqual(EXAMPLE['external_gateway_info'], sot.external_gateway_info) From bcf847bcdb4cf42933d3ec060d871546ecb9987e Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 15 Jun 2022 16:54:40 +0200 Subject: [PATCH 3057/3836] Add VPNaaS Endpoint Group resource Change-Id: Id3239dcada7b71a9664c0ad6b8751d5f13646b40 --- doc/source/user/proxies/network.rst | 9 +- openstack/network/v2/_proxy.py | 106 ++++++++++++++++++ openstack/network/v2/vpn_endpoint_group.py | 42 +++++++ openstack/network/v2/vpn_service.py | 2 - openstack/tests/unit/network/v2/test_proxy.py | 38 +++++++ .../network/v2/test_vpn_endpoint_group.py | 59 ++++++++++ .../tests/unit/network/v2/test_vpn_service.py | 7 ++ 7 files changed, 258 insertions(+), 5 deletions(-) create mode 100644 openstack/network/v2/vpn_endpoint_group.py create mode 100644 openstack/tests/unit/network/v2/test_vpn_endpoint_group.py diff --git a/doc/source/user/proxies/network.rst b/doc/source/user/proxies/network.rst index e82d92743..8aa92b32a 100644 --- a/doc/source/user/proxies/network.rst +++ b/doc/source/user/proxies/network.rst @@ -241,13 +241,16 @@ Tag Operations :noindex: :members: set_tags -VPN Operations -^^^^^^^^^^^^^^ +VPNaaS Operations +^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.network.v2._proxy.Proxy :noindex: :members: create_vpn_service, update_vpn_service, delete_vpn_service, - get_vpn_service, find_vpn_service, vpn_services + get_vpn_service, find_vpn_service, vpn_services, create_vpn_endpoint_group, + update_vpn_endpoint_group, delete_vpn_endpoint_group, + get_vpn_endpoint_group, find_vpn_endpoint_group, vpn_endpoint_groups + IPSecSiteConnection Operations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index cc8663a69..df772eb35 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -64,6 +64,7 @@ from openstack.network.v2 import subnet as _subnet from openstack.network.v2 import subnet_pool as _subnet_pool from openstack.network.v2 import trunk as _trunk +from openstack.network.v2 import vpn_endpoint_group as _vpn_endpoint_group from openstack.network.v2 import vpn_service as _vpn_service from openstack import proxy @@ -4550,6 +4551,111 @@ def get_trunk_subports(self, trunk): trunk = self._get_resource(_trunk.Trunk, trunk) return trunk.get_subports(self) + # ========== VPNaas ========== + # ========== VPN Endpoint group ========== + + def create_vpn_endpoint_group(self, **attrs): + """Create a new vpn endpoint group from attributes + + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.network.v2.vpn_endpoint_group.VPNEndpointGroup`, + comprised of the properties on the VPNEndpointGroup class. + + :returns: The results of vpn endpoint group creation. + :rtype: + :class:`~openstack.network.v2.vpn_endpoint_group.VPNEndpointGroup` + """ + return self._create( + _vpn_endpoint_group.VPNEndpointGroup, **attrs) + + def delete_vpn_endpoint_group( + self, vpn_endpoint_group, ignore_missing=True + ): + """Delete a vpn service + + :param vpn_endpoint_group: + The value can be either the ID of a vpn service or a + :class:`~openstack.network.v2.vpn_endpoint_group.VPNEndpointGroup` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the vpn service does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent vpn service. + + :returns: ``None`` + """ + self._delete( + _vpn_endpoint_group.VPNEndpointGroup, vpn_endpoint_group, + ignore_missing=ignore_missing) + + def find_vpn_endpoint_group( + self, name_or_id, ignore_missing=True, **args + ): + """Find a single vpn service + + :param name_or_id: The name or ID of a vpn service. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods. such as query filters. + :returns: One + :class:`~openstack.network.v2.vpn_endpoint_group.VPNEndpointGroup` + or None + """ + return self._find( + _vpn_endpoint_group.VPNEndpointGroup, name_or_id, + ignore_missing=ignore_missing, **args) + + def get_vpn_endpoint_group(self, vpn_endpoint_group): + """Get a single vpn service + + :param vpn_endpoint_group: The value can be the ID of a vpn service + or a + :class:`~openstack.network.v2.vpn_endpoint_group.VPNEndpointGroup` + instance. + + :returns: One + :class:`~openstack.network.v2.vpn_endpoint_group.VPNEndpointGroup` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get( + _vpn_endpoint_group.VPNEndpointGroup, vpn_endpoint_group) + + def vpn_endpoint_groups(self, **query): + """Return a generator of vpn services + + :param dict query: Optional query parameters to be sent to limit + the resources being returned. + + :returns: A generator of vpn service objects + :rtype: + :class:`~openstack.network.v2.vpn_endpoint_group.VPNEndpointGroup` + """ + return self._list(_vpn_endpoint_group.VPNEndpointGroup, **query) + + def update_vpn_endpoint_group(self, vpn_endpoint_group, **attrs): + """Update a vpn service + + :param vpn_endpoint_group: Either the id of a vpn service or a + :class:`~openstack.network.v2.vpn_endpoint_group.VPNEndpointGroup` + instance. + :param dict attrs: The attributes to update on the VPN service + represented by ``vpn_endpoint_group``. + + :returns: The updated vpnservice + :rtype: + :class:`~openstack.network.v2.vpn_endpoint_group.VPNEndpointGroup` + """ + return self._update( + _vpn_endpoint_group.VPNEndpointGroup, vpn_endpoint_group, **attrs) + + # ========== VPN Service ========== + def create_vpn_service(self, **attrs): """Create a new vpn service from attributes diff --git a/openstack/network/v2/vpn_endpoint_group.py b/openstack/network/v2/vpn_endpoint_group.py new file mode 100644 index 000000000..a9b299bd8 --- /dev/null +++ b/openstack/network/v2/vpn_endpoint_group.py @@ -0,0 +1,42 @@ +# 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 resource + + +class VPNEndpointGroup(resource.Resource): + resource_key = 'endpoint_group' + resources_key = 'endpoint_groups' + base_path = '/vpn/endpoint-groups' + + _allow_unknown_attrs_in_body = True + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + # Properties + #: Human-readable description for the resource. + description = resource.Body('description') + #: List of endpoints of the same type, for the endpoint group. + #: The values will depend on type. + endpoints = resource.Body('endpoints', type=list) + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) + #: The type of the endpoints in the group. A valid value is subnet, cidr, + #: network, router, or vlan. Only subnet and cidr are supported at this + #: moment. + type = resource.Body('type') diff --git a/openstack/network/v2/vpn_service.py b/openstack/network/v2/vpn_service.py index 0c4ba92fc..fccdf9a3b 100644 --- a/openstack/network/v2/vpn_service.py +++ b/openstack/network/v2/vpn_service.py @@ -13,8 +13,6 @@ from openstack import resource -# NOTE: The VPN service is unmaintained, need to consider remove it - class VPNService(resource.Resource): resource_key = 'vpnservice' resources_key = 'vpnservices' diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 772da7c79..43f4a7d06 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -60,6 +60,7 @@ from openstack.network.v2 import service_provider from openstack.network.v2 import subnet from openstack.network.v2 import subnet_pool +from openstack.network.v2 import vpn_endpoint_group from openstack.network.v2 import vpn_service from openstack import proxy as proxy_base from openstack.tests.unit import test_proxy_base @@ -1632,6 +1633,43 @@ def test_subnet_pool_update(self): subnet_pool.SubnetPool) +class TestNetworkVpnEndpointGroup(TestNetworkProxy): + def test_vpn_endpoint_group_create_attrs(self): + self.verify_create( + self.proxy.create_vpn_endpoint_group, + vpn_endpoint_group.VPNEndpointGroup) + + def test_vpn_endpoint_group_delete(self): + self.verify_delete( + self.proxy.delete_vpn_endpoint_group, + vpn_endpoint_group.VPNEndpointGroup, False) + + def test_vpn_endpoint_group_delete_ignore(self): + self.verify_delete( + self.proxy.delete_vpn_endpoint_group, + vpn_endpoint_group.VPNEndpointGroup, True) + + def test_vpn_endpoint_group_find(self): + self.verify_find( + self.proxy.find_vpn_endpoint_group, + vpn_endpoint_group.VPNEndpointGroup) + + def test_vpn_endpoint_group_get(self): + self.verify_get( + self.proxy.get_vpn_endpoint_group, + vpn_endpoint_group.VPNEndpointGroup) + + def test_vpn_endpoint_groups(self): + self.verify_list( + self.proxy.vpn_endpoint_groups, + vpn_endpoint_group.VPNEndpointGroup) + + def test_vpn_endpoint_group_update(self): + self.verify_update( + self.proxy.update_vpn_endpoint_group, + vpn_endpoint_group.VPNEndpointGroup) + + class TestNetworkVpnService(TestNetworkProxy): def test_vpn_service_create_attrs(self): self.verify_create(self.proxy.create_vpn_service, diff --git a/openstack/tests/unit/network/v2/test_vpn_endpoint_group.py b/openstack/tests/unit/network/v2/test_vpn_endpoint_group.py new file mode 100644 index 000000000..9550d1de4 --- /dev/null +++ b/openstack/tests/unit/network/v2/test_vpn_endpoint_group.py @@ -0,0 +1,59 @@ +# 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.network.v2 import vpn_endpoint_group +from openstack.tests.unit import base + + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + "description": "", + "project_id": "4ad57e7ce0b24fca8f12b9834d91079d", + "tenant_id": "4ad57e7ce0b24fca8f12b9834d91079d", + "endpoints": [ + "10.2.0.0/24", + "10.3.0.0/24" + ], + "type": "cidr", + "id": "6ecd9cf3-ca64-46c7-863f-f2eb1b9e838a", + "name": "peers" +} + + +class TestVPNEndpointGroup(base.TestCase): + + def test_basic(self): + sot = vpn_endpoint_group.VPNEndpointGroup() + self.assertEqual('endpoint_group', sot.resource_key) + self.assertEqual('endpoint_groups', sot.resources_key) + self.assertEqual('/vpn/endpoint-groups', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = vpn_endpoint_group.VPNEndpointGroup(**EXAMPLE) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['endpoints'], sot.endpoints) + self.assertEqual(EXAMPLE['type'], sot.type) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + + self.assertDictEqual( + { + "limit": "limit", + "marker": "marker", + }, + sot._query_mapping._mapping) diff --git a/openstack/tests/unit/network/v2/test_vpn_service.py b/openstack/tests/unit/network/v2/test_vpn_service.py index 21024db40..005f37de3 100644 --- a/openstack/tests/unit/network/v2/test_vpn_service.py +++ b/openstack/tests/unit/network/v2/test_vpn_service.py @@ -54,3 +54,10 @@ def test_make_it(self): self.assertEqual(EXAMPLE['status'], sot.status) self.assertEqual(EXAMPLE['subnet_id'], sot.subnet_id) self.assertEqual(EXAMPLE['project_id'], sot.project_id) + + self.assertDictEqual( + { + "limit": "limit", + "marker": "marker", + }, + sot._query_mapping._mapping) From e7d203924aee049804a1b33fbfd3091fa89734f6 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 8 Jun 2022 15:17:13 +0200 Subject: [PATCH 3058/3836] proxy: Resolve a TODO Change-Id: Ie8aa8c095cc70a24bd20bda89aa7f91019ae9976 Signed-off-by: Stephen Finucane --- openstack/load_balancer/v2/_proxy.py | 3 +-- openstack/proxy.py | 8 ++------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index ca0c010b4..8d9059ede 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -630,8 +630,7 @@ def create_l7_rule(self, l7_policy, **attrs): def delete_l7_rule(self, l7rule, l7_policy, ignore_missing=True): """Delete a l7rule - :param l7rule: - The l7rule can be either the ID of a l7rule or a + :param l7rule: The l7rule can be either the ID of a l7rule or a :class:`~openstack.load_balancer.v2.l7_rule.L7Rule` instance. :param l7_policy: The l7_policy can be either the ID of a l7policy or :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` diff --git a/openstack/proxy.py b/openstack/proxy.py index 4c4cd9d73..cd2fc5e73 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -488,7 +488,6 @@ def _find(self, resource_type, name_or_id, ignore_missing=True, **attrs): self, name_or_id, ignore_missing=ignore_missing, **attrs ) - # TODO(stephenfin): Update docstring for attrs since it's a lie @_check_resource(strict=False) def _delete(self, resource_type, value, ignore_missing=True, **attrs): """Delete a resource @@ -504,10 +503,8 @@ def _delete(self, resource_type, value, ignore_missing=True, **attrs): raised when the resource does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent resource. - :param dict attrs: Attributes to be passed onto the - :meth:`~openstack.resource.Resource.delete` - method, such as the ID of a parent resource. - + :param dict attrs: Attributes to be used to form the request URL such + as the ID of a parent resource. :returns: The result of the ``delete`` :raises: ``ValueError`` if ``value`` is a :class:`~openstack.resource.Resource` that doesn't match @@ -515,7 +512,6 @@ def _delete(self, resource_type, value, ignore_missing=True, **attrs): :class:`~openstack.exceptions.ResourceNotFound` when ignore_missing if ``False`` and a nonexistent resource is attempted to be deleted. - """ res = self._get_resource(resource_type, value, **attrs) From 4ce7a3fb619c42b43842d1d033dea9594e1d524f Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 15 Jun 2022 17:45:12 +0200 Subject: [PATCH 3059/3836] Reorg existing vpnaas content VPNaaS consists of few resources. In order to keep things close to each other lets ensure resources are at least similarly prefixed. Change-Id: Ieb0df19dc343ed97418a9b383c18346a5317a0ff --- doc/source/user/proxies/network.rst | 26 +- doc/source/user/resources/network/index.rst | 3 +- .../user/resources/network/v2/ikepolicy.rst | 13 - .../network/v2/ipsec_site_connection.rst | 13 - .../network/v2/vpn/endpoint_group.rst | 13 + .../resources/network/v2/vpn/ikepolicy.rst | 13 + .../user/resources/network/v2/vpn/index.rst | 10 + .../network/v2/vpn/ipsec_site_connection.rst | 13 + .../user/resources/network/v2/vpn/service.rst | 13 + openstack/network/v2/_proxy.py | 454 +++++++++--------- openstack/network/v2/vpn_endpoint_group.py | 2 +- .../v2/{ikepolicy.py => vpn_ikepolicy.py} | 2 +- ...ection.py => vpn_ipsec_site_connection.py} | 210 ++++---- openstack/network/v2/vpn_service.py | 2 +- .../v2/{test_ikepolicy.py => test_vpnaas.py} | 10 +- openstack/tests/unit/network/v2/test_proxy.py | 166 ++++--- .../network/v2/test_vpn_endpoint_group.py | 6 +- ...est_ikepolicy.py => test_vpn_ikepolicy.py} | 8 +- ...n.py => test_vpn_ipsec_site_connection.py} | 160 +++--- .../tests/unit/network/v2/test_vpn_service.py | 6 +- 20 files changed, 598 insertions(+), 545 deletions(-) delete mode 100644 doc/source/user/resources/network/v2/ikepolicy.rst delete mode 100644 doc/source/user/resources/network/v2/ipsec_site_connection.rst create mode 100644 doc/source/user/resources/network/v2/vpn/endpoint_group.rst create mode 100644 doc/source/user/resources/network/v2/vpn/ikepolicy.rst create mode 100644 doc/source/user/resources/network/v2/vpn/index.rst create mode 100644 doc/source/user/resources/network/v2/vpn/ipsec_site_connection.rst create mode 100644 doc/source/user/resources/network/v2/vpn/service.rst rename openstack/network/v2/{ikepolicy.py => vpn_ikepolicy.py} (98%) rename openstack/network/v2/{ipsec_site_connection.py => vpn_ipsec_site_connection.py} (96%) rename openstack/tests/functional/network/v2/{test_ikepolicy.py => test_vpnaas.py} (88%) rename openstack/tests/unit/network/v2/{test_ikepolicy.py => test_vpn_ikepolicy.py} (91%) rename openstack/tests/unit/network/v2/{test_ipsec_site_connection.py => test_vpn_ipsec_site_connection.py} (90%) diff --git a/doc/source/user/proxies/network.rst b/doc/source/user/proxies/network.rst index 8aa92b32a..b7b9ecc4d 100644 --- a/doc/source/user/proxies/network.rst +++ b/doc/source/user/proxies/network.rst @@ -249,26 +249,12 @@ VPNaaS Operations :members: create_vpn_service, update_vpn_service, delete_vpn_service, get_vpn_service, find_vpn_service, vpn_services, create_vpn_endpoint_group, update_vpn_endpoint_group, delete_vpn_endpoint_group, - get_vpn_endpoint_group, find_vpn_endpoint_group, vpn_endpoint_groups - - -IPSecSiteConnection Operations -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.network.v2._proxy.Proxy - :noindex: - :members: create_vpn_ipsec_site_connection, update_vpn_ipsec_site_connection, - delete_vpn_ipsec_site_connection, get_vpn_ipsec_site_connection, - find_vpn_ipsec_site_connection, vpn_ipsec_site_connections - -IkePolicy Operations -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.network.v2._proxy.Proxy - :noindex: - :members: create_vpn_ikepolicy, update_vpn_ikepolicy, - delete_vpn_ikepolicy, get_vpn_ikepolicy, - find_vpn_ikepolicy, vpn_ikepolicies + get_vpn_endpoint_group, find_vpn_endpoint_group, vpn_endpoint_groups, + create_vpn_ipsec_site_connection, update_vpn_ipsec_site_connection, + delete_vpn_ipsec_site_connection, get_vpn_ipsec_site_connection, + find_vpn_ipsec_site_connection, vpn_ipsec_site_connections, + create_vpn_ikepolicy, update_vpn_ikepolicy, delete_vpn_ikepolicy, + get_vpn_ikepolicy, find_vpn_ikepolicy, vpn_ikepolicies Extension Operations ^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/network/index.rst b/doc/source/user/resources/network/index.rst index d711a4d25..3870b5343 100644 --- a/doc/source/user/resources/network/index.rst +++ b/doc/source/user/resources/network/index.rst @@ -13,8 +13,6 @@ Network Resources v2/flavor v2/floating_ip v2/health_monitor - v2/ipsec_site_connection - v2/ikepolicy v2/listener v2/load_balancer v2/local_ip @@ -44,3 +42,4 @@ Network Resources v2/service_provider v2/subnet v2/subnet_pool + v2/vpn/index diff --git a/doc/source/user/resources/network/v2/ikepolicy.rst b/doc/source/user/resources/network/v2/ikepolicy.rst deleted file mode 100644 index b6bc623ef..000000000 --- a/doc/source/user/resources/network/v2/ikepolicy.rst +++ /dev/null @@ -1,13 +0,0 @@ -openstack.network.v2.ikepolicy -============================== - -.. automodule:: openstack.network.v2.ikepolicy - -The IkePolicy Class -------------------- - -The ``IkePolicy`` class inherits from -:class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.network.v2.ikepolicy.IkePolicy - :members: diff --git a/doc/source/user/resources/network/v2/ipsec_site_connection.rst b/doc/source/user/resources/network/v2/ipsec_site_connection.rst deleted file mode 100644 index 3750344e7..000000000 --- a/doc/source/user/resources/network/v2/ipsec_site_connection.rst +++ /dev/null @@ -1,13 +0,0 @@ -openstack.network.v2.ipsec_site_connection -========================================== - -.. automodule:: openstack.network.v2.ipsec_site_connection - -The IPSecSiteConnection Class ------------------------------ - -The ``IPSecSiteConnection`` class inherits from -:class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.network.v2.ipsec_site_connection.IPSecSiteConnection - :members: diff --git a/doc/source/user/resources/network/v2/vpn/endpoint_group.rst b/doc/source/user/resources/network/v2/vpn/endpoint_group.rst new file mode 100644 index 000000000..3e8478cc3 --- /dev/null +++ b/doc/source/user/resources/network/v2/vpn/endpoint_group.rst @@ -0,0 +1,13 @@ +openstack.network.v2.vpn_endpoint_group +======================================= + +.. automodule:: openstack.network.v2.vpn_endpoint_group + +The VpnEndpointGroup Class +-------------------------- + +The ``VpnEndpointGroup`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.vpn_endpoint_group.VpnEndpointGroup + :members: diff --git a/doc/source/user/resources/network/v2/vpn/ikepolicy.rst b/doc/source/user/resources/network/v2/vpn/ikepolicy.rst new file mode 100644 index 000000000..8c43e02cd --- /dev/null +++ b/doc/source/user/resources/network/v2/vpn/ikepolicy.rst @@ -0,0 +1,13 @@ +openstack.network.v2.vpn_ikepolicy +================================== + +.. automodule:: openstack.network.v2.vpn_ikepolicy + +The VpnIkePolicy Class +---------------------- + +The ``VpnIkePolicy`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.vpn_ikepolicy.VpnIkePolicy + :members: diff --git a/doc/source/user/resources/network/v2/vpn/index.rst b/doc/source/user/resources/network/v2/vpn/index.rst new file mode 100644 index 000000000..bb9859323 --- /dev/null +++ b/doc/source/user/resources/network/v2/vpn/index.rst @@ -0,0 +1,10 @@ +VPNaaS Resources +================ + +.. toctree:: + :maxdepth: 1 + + endpoint_group + ipsec_site_connection + ikepolicy + service diff --git a/doc/source/user/resources/network/v2/vpn/ipsec_site_connection.rst b/doc/source/user/resources/network/v2/vpn/ipsec_site_connection.rst new file mode 100644 index 000000000..f90e000b4 --- /dev/null +++ b/doc/source/user/resources/network/v2/vpn/ipsec_site_connection.rst @@ -0,0 +1,13 @@ +openstack.network.v2.vpn_ipsec_site_connection +============================================== + +.. automodule:: openstack.network.v2.vpn_ipsec_site_connection + +The VpnIPSecSiteConnection Class +-------------------------------- + +The ``VpnIPSecSiteConnection`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.vpn_ipsec_site_connection.VpnIPSecSiteConnection + :members: diff --git a/doc/source/user/resources/network/v2/vpn/service.rst b/doc/source/user/resources/network/v2/vpn/service.rst new file mode 100644 index 000000000..0a185ba63 --- /dev/null +++ b/doc/source/user/resources/network/v2/vpn/service.rst @@ -0,0 +1,13 @@ +openstack.network.v2.vpn_service +================================ + +.. automodule:: openstack.network.v2.vpn_service + +The VpnService Class +-------------------- + +The ``VpnService`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.vpn_service.VpnService + :members: diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index df772eb35..f1e8ec300 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -24,9 +24,6 @@ from openstack.network.v2 import flavor as _flavor from openstack.network.v2 import floating_ip as _floating_ip from openstack.network.v2 import health_monitor as _health_monitor -from openstack.network.v2 import ikepolicy as _ikepolicy -from openstack.network.v2 import ipsec_site_connection as \ - _ipsec_site_connection from openstack.network.v2 import l3_conntrack_helper as _l3_conntrack_helper from openstack.network.v2 import listener as _listener from openstack.network.v2 import load_balancer as _load_balancer @@ -65,6 +62,9 @@ from openstack.network.v2 import subnet_pool as _subnet_pool from openstack.network.v2 import trunk as _trunk from openstack.network.v2 import vpn_endpoint_group as _vpn_endpoint_group +from openstack.network.v2 import vpn_ikepolicy as _ikepolicy +from openstack.network.v2 import vpn_ipsec_site_connection as \ + _ipsec_site_connection from openstack.network.v2 import vpn_service as _vpn_service from openstack import proxy @@ -1170,189 +1170,6 @@ def update_health_monitor(self, health_monitor, **attrs): return self._update(_health_monitor.HealthMonitor, health_monitor, **attrs) - def create_vpn_ipsec_site_connection(self, **attrs): - """Create a new ipsec site connection from attributes - - :param dict attrs: Keyword arguments which will be used to create a - :class:`~openstack.network.v2.ipsec_site_connection.IPSecSiteConnection`, - comprised of the properties on the IPSecSiteConnection class. - - :returns: The results of ipsec site connection creation - :rtype: - :class:`~openstack.network.v2.ipsec_site_connection.IPSecSiteConnection` - """ - return self._create(_ipsec_site_connection.IPSecSiteConnection, - **attrs) - - def find_vpn_ipsec_site_connection(self, name_or_id, - ignore_missing=True, **args): - """Find a single ipsec site connection - - :param name_or_id: The name or ID of an ipsec site connection. - :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` - will be raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into - underlying methods such as query filters. - :returns: One - :class:`~openstack.network.v2.ipsec_site_connection.IPSecSiteConnection` - or None - """ - return self._find(_ipsec_site_connection.IPSecSiteConnection, - name_or_id, ignore_missing=ignore_missing, **args) - - def get_vpn_ipsec_site_connection(self, ipsec_site_connection): - """Get a single ipsec site connection - - :param ipsec_site_connection: The value can be the ID of an ipsec site - connection or a - :class:`~openstack.network.v2.ipsec_site_connection.IPSecSiteConnection` - instance. - - :returns: One - :class:`~openstack.network.v2.ipsec_site_connection.IPSecSiteConnection` - :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. - """ - return self._get(_ipsec_site_connection.IPSecSiteConnection, - ipsec_site_connection) - - def vpn_ipsec_site_connections(self, **query): - """Return a generator of ipsec site connections - - :param dict query: Optional query parameters to be sent to limit the - resources being returned. - - :returns: A generator of ipsec site connection objects - :rtype: - :class:`~openstack.network.v2.ipsec_site_connection.IPSecSiteConnection` - """ - return self._list(_ipsec_site_connection.IPSecSiteConnection, **query) - - def update_vpn_ipsec_site_connection(self, ipsec_site_connection, **attrs): - """Update a ipsec site connection - - :ipsec_site_connection: Either the id of an ipsec site connection or - a - :class:`~openstack.network.v2.ipsec_site_connection.IPSecSiteConnection` - instance. - :param dict attrs: The attributes to update on the ipsec site - connection represented by ``ipsec_site_connection``. - - :returns: The updated ipsec site connection - :rtype: - :class:`~openstack.network.v2.ipsec_site_connection.IPSecSiteConnection` - """ - return self._update(_ipsec_site_connection.IPSecSiteConnection, - ipsec_site_connection, **attrs) - - def delete_vpn_ipsec_site_connection(self, ipsec_site_connection, - ignore_missing=True): - """Delete a ipsec site connection - - :param ipsec_site_connection: The value can be either the ID of an - ipsec site connection, or a - :class:`~openstack.network.v2.ipsec_site_connection.IPSecSiteConnection` - instance. - :param bool ignore_missing: - When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when - the ipsec site connection does not exist. - When set to ``True``, no exception will be set when attempting to - delete a nonexistent ipsec site connection. - - :returns: ``None`` - """ - self._delete(_ipsec_site_connection.IPSecSiteConnection, - ipsec_site_connection, ignore_missing=ignore_missing) - - def create_vpn_ikepolicy(self, **attrs): - """Create a new ike policy from attributes - - :param dict attrs: Keyword arguments which will be used to create a - :class:`~openstack.network.v2.ikepolicy.IkePolicy`, comprised of - the properties on the IkePolicy class. - - :returns: The results of ike policy creation :rtype: - :class:`~openstack.network.v2.ikepolicy.IkePolicy` - """ - return self._create(_ikepolicy.IkePolicy, - **attrs) - - def find_vpn_ikepolicy(self, name_or_id, - ignore_missing=True, **args): - """Find a single ike policy - - :param name_or_id: The name or ID of an ike policy. - :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` - will be raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into - underlying methods such as query filters. - :returns: One :class:`~openstack.network.v2.ikepolicy.IkePolicy` or - None. - """ - return self._find(_ikepolicy.IkePolicy, - name_or_id, ignore_missing=ignore_missing, **args) - - def get_vpn_ikepolicy(self, ikepolicy): - """Get a single ike policy - - :param ikepolicy: The value can be the ID of an ikepolicy or a - :class:`~openstack.network.v2.ikepolicy.IkePolicy` instance. - - :returns: One :class:`~openstack.network.v2.ikepolicy.IkePolicy` - :rtype: :class:`~openstack.network.v2.ikepolicy.IkePolicy` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no - resource can be found. - """ - return self._get(_ikepolicy.IkePolicy, ikepolicy) - - def vpn_ikepolicies(self, **query): - """Return a generator of ike policy - - :param dict query: Optional query parameters to be sent to limit the - resources being returned. - - :returns: A generator of ike policy objects - :rtype: :class:`~openstack.network.v2.ikepolicy.IkePolicy` - """ - return self._list(_ikepolicy.IkePolicy, **query) - - def update_vpn_ikepolicy(self, ikepolicy, **attrs): - """Update a ike policy - - :ikepolicy: Either the id of an ike policy or a - :class:`~openstack.network.v2.ikepolicy.IkePolicy` instance. - :param dict attrs: The attributes to update on the ike policy - represented by ``ikepolicy``. - - :returns: The updated ike policy - :rtype: :class:`~openstack.network.v2.ikepolicy.IkePolicy` - """ - return self._update(_ikepolicy.IkePolicy, ikepolicy, **attrs) - - def delete_vpn_ikepolicy(self, ikepolicy, ignore_missing=True): - """Delete a ikepolicy - - :param ikepolicy: The value can be either the ID of an ike policy, or - a :class:`~openstack.network.v2.ikepolicy.IkePolicy` instance. - :param bool ignore_missing: - When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` - will be raised when the ike policy does not exist. - When set to ``True``, no exception will be set when attempting to - delete a nonexistent ike policy. - - :returns: ``None`` - """ - self._delete(_ikepolicy.IkePolicy, ikepolicy, - ignore_missing=ignore_missing) - def create_listener(self, **attrs): """Create a new listener from attributes @@ -4558,15 +4375,15 @@ def create_vpn_endpoint_group(self, **attrs): """Create a new vpn endpoint group from attributes :param dict attrs: Keyword arguments which will be used to create a - :class:`~openstack.network.v2.vpn_endpoint_group.VPNEndpointGroup`, - comprised of the properties on the VPNEndpointGroup class. + :class:`~openstack.network.v2.vpn_endpoint_group.VpnEndpointGroup`, + comprised of the properties on the VpnEndpointGroup class. :returns: The results of vpn endpoint group creation. :rtype: - :class:`~openstack.network.v2.vpn_endpoint_group.VPNEndpointGroup` + :class:`~openstack.network.v2.vpn_endpoint_group.VpnEndpointGroup` """ return self._create( - _vpn_endpoint_group.VPNEndpointGroup, **attrs) + _vpn_endpoint_group.VpnEndpointGroup, **attrs) def delete_vpn_endpoint_group( self, vpn_endpoint_group, ignore_missing=True @@ -4575,7 +4392,7 @@ def delete_vpn_endpoint_group( :param vpn_endpoint_group: The value can be either the ID of a vpn service or a - :class:`~openstack.network.v2.vpn_endpoint_group.VPNEndpointGroup` + :class:`~openstack.network.v2.vpn_endpoint_group.VpnEndpointGroup` instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be @@ -4586,7 +4403,7 @@ def delete_vpn_endpoint_group( :returns: ``None`` """ self._delete( - _vpn_endpoint_group.VPNEndpointGroup, vpn_endpoint_group, + _vpn_endpoint_group.VpnEndpointGroup, vpn_endpoint_group, ignore_missing=ignore_missing) def find_vpn_endpoint_group( @@ -4603,11 +4420,11 @@ def find_vpn_endpoint_group( :param dict args: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One - :class:`~openstack.network.v2.vpn_endpoint_group.VPNEndpointGroup` + :class:`~openstack.network.v2.vpn_endpoint_group.VpnEndpointGroup` or None """ return self._find( - _vpn_endpoint_group.VPNEndpointGroup, name_or_id, + _vpn_endpoint_group.VpnEndpointGroup, name_or_id, ignore_missing=ignore_missing, **args) def get_vpn_endpoint_group(self, vpn_endpoint_group): @@ -4615,16 +4432,16 @@ def get_vpn_endpoint_group(self, vpn_endpoint_group): :param vpn_endpoint_group: The value can be the ID of a vpn service or a - :class:`~openstack.network.v2.vpn_endpoint_group.VPNEndpointGroup` + :class:`~openstack.network.v2.vpn_endpoint_group.VpnEndpointGroup` instance. :returns: One - :class:`~openstack.network.v2.vpn_endpoint_group.VPNEndpointGroup` + :class:`~openstack.network.v2.vpn_endpoint_group.VpnEndpointGroup` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ return self._get( - _vpn_endpoint_group.VPNEndpointGroup, vpn_endpoint_group) + _vpn_endpoint_group.VpnEndpointGroup, vpn_endpoint_group) def vpn_endpoint_groups(self, **query): """Return a generator of vpn services @@ -4634,25 +4451,226 @@ def vpn_endpoint_groups(self, **query): :returns: A generator of vpn service objects :rtype: - :class:`~openstack.network.v2.vpn_endpoint_group.VPNEndpointGroup` + :class:`~openstack.network.v2.vpn_endpoint_group.VpnEndpointGroup` """ - return self._list(_vpn_endpoint_group.VPNEndpointGroup, **query) + return self._list(_vpn_endpoint_group.VpnEndpointGroup, **query) def update_vpn_endpoint_group(self, vpn_endpoint_group, **attrs): """Update a vpn service :param vpn_endpoint_group: Either the id of a vpn service or a - :class:`~openstack.network.v2.vpn_endpoint_group.VPNEndpointGroup` + :class:`~openstack.network.v2.vpn_endpoint_group.VpnEndpointGroup` instance. :param dict attrs: The attributes to update on the VPN service represented by ``vpn_endpoint_group``. :returns: The updated vpnservice :rtype: - :class:`~openstack.network.v2.vpn_endpoint_group.VPNEndpointGroup` + :class:`~openstack.network.v2.vpn_endpoint_group.VpnEndpointGroup` + """ + return self._update( + _vpn_endpoint_group.VpnEndpointGroup, vpn_endpoint_group, **attrs) + + # ========== IPSec Site Connection ========== + def create_vpn_ipsec_site_connection(self, **attrs): + """Create a new ipsec site connection from attributes + + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.network.v2.vpn_ipsec_site_connection.VpnIPSecSiteConnection`, + comprised of the properties on the IPSecSiteConnection class. + + :returns: The results of ipsec site connection creation + :rtype: + :class:`~openstack.network.v2.vpn_ipsec_site_connection.VpnIPSecSiteConnection` + """ + return self._create( + _ipsec_site_connection.VpnIPSecSiteConnection, + **attrs) + + def find_vpn_ipsec_site_connection( + self, name_or_id, ignore_missing=True, **args + ): + """Find a single ipsec site connection + + :param name_or_id: The name or ID of an ipsec site connection. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` + will be raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods such as query filters. + :returns: One + :class:`~openstack.network.v2.vpn_ipsec_site_connection.VpnIPSecSiteConnection` + or None + """ + return self._find( + _ipsec_site_connection.VpnIPSecSiteConnection, + name_or_id, ignore_missing=ignore_missing, **args) + + def get_vpn_ipsec_site_connection(self, ipsec_site_connection): + """Get a single ipsec site connection + + :param ipsec_site_connection: The value can be the ID of an ipsec site + connection or a + :class:`~openstack.network.v2.vpn_ipsec_site_connection.VpnIPSecSiteConnection` + instance. + + :returns: One + :class:`~openstack.network.v2.vpn_ipsec_site_connection.VpnIPSecSiteConnection` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get( + _ipsec_site_connection.VpnIPSecSiteConnection, + ipsec_site_connection) + + def vpn_ipsec_site_connections(self, **query): + """Return a generator of ipsec site connections + + :param dict query: Optional query parameters to be sent to limit the + resources being returned. + + :returns: A generator of ipsec site connection objects + :rtype: + :class:`~openstack.network.v2.vpn_ipsec_site_connection.VpnIPSecSiteConnection` + """ + return self._list( + _ipsec_site_connection.VpnIPSecSiteConnection, **query) + + def update_vpn_ipsec_site_connection(self, ipsec_site_connection, **attrs): + """Update a ipsec site connection + + :ipsec_site_connection: Either the id of an ipsec site connection or + a + :class:`~openstack.network.v2.vpn_ipsec_site_connection.VpnIPSecSiteConnection` + instance. + :param dict attrs: The attributes to update on the ipsec site + connection represented by ``ipsec_site_connection``. + + :returns: The updated ipsec site connection + :rtype: + :class:`~openstack.network.v2.vpn_ipsec_site_connection.VpnIPSecSiteConnection` + """ + return self._update( + _ipsec_site_connection.VpnIPSecSiteConnection, + ipsec_site_connection, **attrs) + + def delete_vpn_ipsec_site_connection( + self, ipsec_site_connection, ignore_missing=True + ): + """Delete a ipsec site connection + + :param ipsec_site_connection: The value can be either the ID of an + ipsec site connection, or a + :class:`~openstack.network.v2.vpn_ipsec_site_connection.VpnIPSecSiteConnection` + instance. + :param bool ignore_missing: + When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the ipsec site connection does not exist. + When set to ``True``, no exception will be set when attempting to + delete a nonexistent ipsec site connection. + + :returns: ``None`` + """ + self._delete( + _ipsec_site_connection.VpnIPSecSiteConnection, + ipsec_site_connection, ignore_missing=ignore_missing) + + # ========== IKEPolicy ========== + def create_vpn_ikepolicy(self, **attrs): + """Create a new ike policy from attributes + + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.network.v2.vpn_ikepolicy.VpnIkePolicy`, + comprised of the properties on the VpnIkePolicy class. + + :returns: The results of ike policy creation :rtype: + :class:`~openstack.network.v2.vpn_ikepolicy.VpnIkePolicy` + """ + return self._create( + _ikepolicy.VpnIkePolicy, **attrs) + + def find_vpn_ikepolicy( + self, name_or_id, ignore_missing=True, **args + ): + """Find a single ike policy + + :param name_or_id: The name or ID of an ike policy. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` + will be raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods such as query filters. + :returns: One :class:`~openstack.network.v2.vpn_ikepolicy.VpnIkePolicy` + or None. + """ + return self._find( + _ikepolicy.VpnIkePolicy, name_or_id, + ignore_missing=ignore_missing, **args) + + def get_vpn_ikepolicy(self, ikepolicy): + """Get a single ike policy + + :param ikepolicy: The value can be the ID of an ikepolicy or a + :class:`~openstack.network.v2.vpn_ikepolicy.VpnIkePolicy` + instance. + + :returns: One :class:`~openstack.network.v2.vpn_ikepolicy.VpnIkePolicy` + :rtype: :class:`~openstack.network.v2.ikepolicy.VpnIkePolicy` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource can be found. + """ + return self._get( + _ikepolicy.VpnIkePolicy, ikepolicy) + + def vpn_ikepolicies(self, **query): + """Return a generator of ike policy + + :param dict query: Optional query parameters to be sent to limit the + resources being returned. + + :returns: A generator of ike policy objects + :rtype: :class:`~openstack.network.v2.vpn_ikepolicy.VpnIkePolicy` + """ + return self._list( + _ikepolicy.VpnIkePolicy, **query) + + def update_vpn_ikepolicy(self, ikepolicy, **attrs): + """Update a ike policy + + :ikepolicy: Either the id of an ike policy or a + :class:`~openstack.network.v2.vpn_ikepolicy.VpnIkePolicy` instance. + :param dict attrs: The attributes to update on the ike policy + represented by ``ikepolicy``. + + :returns: The updated ike policy + :rtype: :class:`~openstack.network.v2.vpn_ikepolicy.VpnIkePolicy` """ return self._update( - _vpn_endpoint_group.VPNEndpointGroup, vpn_endpoint_group, **attrs) + _ikepolicy.VpnIkePolicy, ikepolicy, **attrs) + + def delete_vpn_ikepolicy(self, ikepolicy, ignore_missing=True): + """Delete a ikepolicy + + :param ikepolicy: The value can be either the ID of an ike policy, or + a :class:`~openstack.network.v2.vpn_ikepolicy.VpnIkePolicy` + instance. + :param bool ignore_missing: + When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` + will be raised when the ike policy does not exist. + When set to ``True``, no exception will be set when attempting to + delete a nonexistent ike policy. + + :returns: ``None`` + """ + self._delete( + _ikepolicy.VpnIkePolicy, ikepolicy, + ignore_missing=ignore_missing) # ========== VPN Service ========== @@ -4660,20 +4678,20 @@ def create_vpn_service(self, **attrs): """Create a new vpn service from attributes :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.network.v2.vpn_service.VPNService`, - comprised of the properties on the VPNService class. + a :class:`~openstack.network.v2.vpn_service.VpnService`, + comprised of the properties on the VpnService class. :returns: The results of vpn service creation - :rtype: :class:`~openstack.network.v2.vpn_service.VPNService` + :rtype: :class:`~openstack.network.v2.vpn_service.VpnService` """ - return self._create(_vpn_service.VPNService, **attrs) + return self._create(_vpn_service.VpnService, **attrs) def delete_vpn_service(self, vpn_service, ignore_missing=True): """Delete a vpn service :param vpn_service: The value can be either the ID of a vpn service or a - :class:`~openstack.network.v2.vpn_service.VPNService` instance. + :class:`~openstack.network.v2.vpn_service.VpnService` instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the vpn service does not exist. @@ -4682,7 +4700,7 @@ def delete_vpn_service(self, vpn_service, ignore_missing=True): :returns: ``None`` """ - self._delete(_vpn_service.VPNService, vpn_service, + self._delete(_vpn_service.VpnService, vpn_service, ignore_missing=ignore_missing) def find_vpn_service(self, name_or_id, ignore_missing=True, **args): @@ -4696,25 +4714,25 @@ def find_vpn_service(self, name_or_id, ignore_missing=True, **args): attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into underlying methods. such as query filters. - :returns: One :class:`~openstack.network.v2.vpn_service.VPNService` + :returns: One :class:`~openstack.network.v2.vpn_service.VpnService` or None """ - return self._find(_vpn_service.VPNService, name_or_id, + return self._find(_vpn_service.VpnService, name_or_id, ignore_missing=ignore_missing, **args) def get_vpn_service(self, vpn_service): """Get a single vpn service :param vpn_service: The value can be the ID of a vpn service or a - :class:`~openstack.network.v2.vpn_service.VPNService` + :class:`~openstack.network.v2.vpn_service.VpnService` instance. :returns: One - :class:`~openstack.network.v2.vpn_service.VPNService` + :class:`~openstack.network.v2.vpn_service.VpnService` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get(_vpn_service.VPNService, vpn_service) + return self._get(_vpn_service.VpnService, vpn_service) def vpn_services(self, **query): """Return a generator of vpn services @@ -4723,22 +4741,22 @@ def vpn_services(self, **query): the resources being returned. :returns: A generator of vpn service objects - :rtype: :class:`~openstack.network.v2.vpn_service.VPNService` + :rtype: :class:`~openstack.network.v2.vpn_service.VpnService` """ - return self._list(_vpn_service.VPNService, **query) + return self._list(_vpn_service.VpnService, **query) def update_vpn_service(self, vpn_service, **attrs): """Update a vpn service :param vpn_service: Either the id of a vpn service or a - :class:`~openstack.network.v2.vpn_service.VPNService` instance. + :class:`~openstack.network.v2.vpn_service.VpnService` instance. :param dict attrs: The attributes to update on the VPN service represented by ``vpn_service``. :returns: The updated vpnservice - :rtype: :class:`~openstack.network.v2.vpn_service.VPNService` + :rtype: :class:`~openstack.network.v2.vpn_service.VpnService` """ - return self._update(_vpn_service.VPNService, vpn_service, **attrs) + return self._update(_vpn_service.VpnService, vpn_service, **attrs) def create_floating_ip_port_forwarding(self, floating_ip, **attrs): """Create a new floating ip port forwarding from attributes diff --git a/openstack/network/v2/vpn_endpoint_group.py b/openstack/network/v2/vpn_endpoint_group.py index a9b299bd8..89ffaff84 100644 --- a/openstack/network/v2/vpn_endpoint_group.py +++ b/openstack/network/v2/vpn_endpoint_group.py @@ -13,7 +13,7 @@ from openstack import resource -class VPNEndpointGroup(resource.Resource): +class VpnEndpointGroup(resource.Resource): resource_key = 'endpoint_group' resources_key = 'endpoint_groups' base_path = '/vpn/endpoint-groups' diff --git a/openstack/network/v2/ikepolicy.py b/openstack/network/v2/vpn_ikepolicy.py similarity index 98% rename from openstack/network/v2/ikepolicy.py rename to openstack/network/v2/vpn_ikepolicy.py index b9ca2b529..2edb088ef 100644 --- a/openstack/network/v2/ikepolicy.py +++ b/openstack/network/v2/vpn_ikepolicy.py @@ -13,7 +13,7 @@ from openstack import resource -class IkePolicy(resource.Resource): +class VpnIkePolicy(resource.Resource): resource_key = 'ikepolicy' resources_key = 'ikepolicies' base_path = '/vpn/ikepolicies' diff --git a/openstack/network/v2/ipsec_site_connection.py b/openstack/network/v2/vpn_ipsec_site_connection.py similarity index 96% rename from openstack/network/v2/ipsec_site_connection.py rename to openstack/network/v2/vpn_ipsec_site_connection.py index 1b2aa29e6..37e166f37 100644 --- a/openstack/network/v2/ipsec_site_connection.py +++ b/openstack/network/v2/vpn_ipsec_site_connection.py @@ -1,105 +1,105 @@ -# 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 resource - - -class IPSecSiteConnection(resource.Resource): - resource_key = 'ipsec_site_connection' - resources_key = 'ipsec_site_connections' - base_path = '/vpn/ipsec-site-connections' - - # capabilities - allow_create = True - allow_fetch = True - allow_commit = True - allow_delete = True - allow_list = True - - # Properties - #: The dead peer detection (DPD) action. - # A valid value is clear, hold, restart, - # disabled, or restart-by-peer. Default value is hold. - action = resource.Body('action') - #: The authentication mode. A valid value - # is psk, which is the default. - auth_mode = resource.Body('auth_mode') - #: A human-readable description for the resource. - # Default is an empty string. - description = resource.Body('description') - #: A dictionary with dead peer detection (DPD) protocol controls. - dpd = resource.Body('dpd', type=dict) - #: The administrative state of the resource, - # which is up (true) or down (false). - is_admin_state_up = resource.Body('admin_state_up', type=bool) - #: The ID of the IKE policy. - ikepolicy_id = resource.Body('ikepolicy_id') - #: Indicates whether this VPN can only respond - # to connections or both respond - # to and initiate connections. A valid value is - # response- only or bi-directional. Default is bi-directional. - initiator = resource.Body('initiator') - #: The ID of the IPsec policy. - ipsecpolicy_id = resource.Body('ipsecpolicy_id') - #: The dead peer detection (DPD) interval, in seconds. - # A valid value is a positive integer. Default is 30. - interval = resource.Body('interval', type=int) - #: The ID for the endpoint group that contains - # private subnets for the local side of the connection. - # Yo must specify this parameter with the - # peer_ep_group_id parameter unless in backward- compatible - # mode where peer_cidrs is provided with - # a subnet_id for the VPN service. - local_ep_group_id = resource.Body('local_ep_group_id') - #: The peer gateway public IPv4 or IPv6 address or FQDN. - peer_address = resource.Body('peer_address') - #: An ID to be used instead of the external IP address for - # a virtual router used in traffic between - # instances on different networks in east-west traffic. - # Most often, local ID would be domain - # name, email address, etc. If this is not configured - # then the external IP address will be used as the ID. - local_id = resource.Body('local_id') - #: The maximum transmission unit (MTU) - # value to address fragmentation. Minimum value - # is 68 for IPv4, and 1280 for IPv6. - mtu = resource.Body('mtu', type=int) - #: Human-readable name of the resource. Default is an empty string. - name = resource.Body('name') - #: The peer router identity for authentication. - # A valid value is an IPv4 address, IPv6 address, e-mail address, - # key ID, or FQDN. Typically, this value matches - # the peer_address value. - peer_id = resource.Body('peer_id') - #: (Deprecated) Unique list of valid peer private - # CIDRs in the form < net_address > / < prefix > . - peer_cidrs = resource.Body('peer_cidrs', type=list) - #: The ID of the project. - project_id = resource.Body('tenant_id') - #: The pre-shared key. A valid value is any string. - psk = resource.Body('psk') - #: The ID for the endpoint group that contains - # private CIDRs in the form < net_address > / < prefix > - # for the peer side of the connection. You must - # specify this parameter with the local_ep_group_id - # parameter unless in backward-compatible mode - # where peer_cidrs is provided with a subnet_id for the VPN service. - peer_ep_group_id = resource.Body('peer_ep_group_id') - #: The route mode. A valid value is static, which is the default. - route_mode = resource.Body('route_mode') - #: The dead peer detection (DPD) timeout - # in seconds. A valid value is a - # positive integer that is greater - # than the DPD interval value. Default is 120. - timeout = resource.Body('timeout', type=int) - #: The ID of the VPN service. - vpnservice_id = resource.Body('vpnservice_id') +# 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 resource + + +class VpnIPSecSiteConnection(resource.Resource): + resource_key = 'ipsec_site_connection' + resources_key = 'ipsec_site_connections' + base_path = '/vpn/ipsec-site-connections' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + # Properties + #: The dead peer detection (DPD) action. + # A valid value is clear, hold, restart, + # disabled, or restart-by-peer. Default value is hold. + action = resource.Body('action') + #: The authentication mode. A valid value + # is psk, which is the default. + auth_mode = resource.Body('auth_mode') + #: A human-readable description for the resource. + # Default is an empty string. + description = resource.Body('description') + #: A dictionary with dead peer detection (DPD) protocol controls. + dpd = resource.Body('dpd', type=dict) + #: The administrative state of the resource, + # which is up (true) or down (false). + is_admin_state_up = resource.Body('admin_state_up', type=bool) + #: The ID of the IKE policy. + ikepolicy_id = resource.Body('ikepolicy_id') + #: Indicates whether this VPN can only respond + # to connections or both respond + # to and initiate connections. A valid value is + # response- only or bi-directional. Default is bi-directional. + initiator = resource.Body('initiator') + #: The ID of the IPsec policy. + ipsecpolicy_id = resource.Body('ipsecpolicy_id') + #: The dead peer detection (DPD) interval, in seconds. + # A valid value is a positive integer. Default is 30. + interval = resource.Body('interval', type=int) + #: The ID for the endpoint group that contains + # private subnets for the local side of the connection. + # Yo must specify this parameter with the + # peer_ep_group_id parameter unless in backward- compatible + # mode where peer_cidrs is provided with + # a subnet_id for the VPN service. + local_ep_group_id = resource.Body('local_ep_group_id') + #: The peer gateway public IPv4 or IPv6 address or FQDN. + peer_address = resource.Body('peer_address') + #: An ID to be used instead of the external IP address for + # a virtual router used in traffic between + # instances on different networks in east-west traffic. + # Most often, local ID would be domain + # name, email address, etc. If this is not configured + # then the external IP address will be used as the ID. + local_id = resource.Body('local_id') + #: The maximum transmission unit (MTU) + # value to address fragmentation. Minimum value + # is 68 for IPv4, and 1280 for IPv6. + mtu = resource.Body('mtu', type=int) + #: Human-readable name of the resource. Default is an empty string. + name = resource.Body('name') + #: The peer router identity for authentication. + # A valid value is an IPv4 address, IPv6 address, e-mail address, + # key ID, or FQDN. Typically, this value matches + # the peer_address value. + peer_id = resource.Body('peer_id') + #: (Deprecated) Unique list of valid peer private + # CIDRs in the form < net_address > / < prefix > . + peer_cidrs = resource.Body('peer_cidrs', type=list) + #: The ID of the project. + project_id = resource.Body('tenant_id') + #: The pre-shared key. A valid value is any string. + psk = resource.Body('psk') + #: The ID for the endpoint group that contains + # private CIDRs in the form < net_address > / < prefix > + # for the peer side of the connection. You must + # specify this parameter with the local_ep_group_id + # parameter unless in backward-compatible mode + # where peer_cidrs is provided with a subnet_id for the VPN service. + peer_ep_group_id = resource.Body('peer_ep_group_id') + #: The route mode. A valid value is static, which is the default. + route_mode = resource.Body('route_mode') + #: The dead peer detection (DPD) timeout + # in seconds. A valid value is a + # positive integer that is greater + # than the DPD interval value. Default is 120. + timeout = resource.Body('timeout', type=int) + #: The ID of the VPN service. + vpnservice_id = resource.Body('vpnservice_id') diff --git a/openstack/network/v2/vpn_service.py b/openstack/network/v2/vpn_service.py index fccdf9a3b..f267f1546 100644 --- a/openstack/network/v2/vpn_service.py +++ b/openstack/network/v2/vpn_service.py @@ -13,7 +13,7 @@ from openstack import resource -class VPNService(resource.Resource): +class VpnService(resource.Resource): resource_key = 'vpnservice' resources_key = 'vpnservices' base_path = '/vpn/vpnservices' diff --git a/openstack/tests/functional/network/v2/test_ikepolicy.py b/openstack/tests/functional/network/v2/test_vpnaas.py similarity index 88% rename from openstack/tests/functional/network/v2/test_ikepolicy.py rename to openstack/tests/functional/network/v2/test_vpnaas.py index 7abfbddb8..2617efe47 100644 --- a/openstack/tests/functional/network/v2/test_ikepolicy.py +++ b/openstack/tests/functional/network/v2/test_vpnaas.py @@ -10,23 +10,23 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network.v2 import ikepolicy +from openstack.network.v2 import vpn_ikepolicy from openstack.tests.functional import base -class TestIkePolicy(base.BaseFunctionalTest): +class TestVpnIkePolicy(base.BaseFunctionalTest): ID = None def setUp(self): - super(TestIkePolicy, self).setUp() + super(TestVpnIkePolicy, self).setUp() if not self.conn._has_neutron_extension('vpnaas_v2'): self.skipTest('vpnaas_v2 service not supported by cloud') self.IKEPOLICY_NAME = self.getUniqueString('ikepolicy') self.UPDATE_NAME = self.getUniqueString('ikepolicy-updated') policy = self.conn.network.create_vpn_ikepolicy( name=self.IKEPOLICY_NAME) - assert isinstance(policy, ikepolicy.IkePolicy) + assert isinstance(policy, vpn_ikepolicy.VpnIkePolicy) self.assertEqual(self.IKEPOLICY_NAME, policy.name) self.ID = policy.id @@ -34,7 +34,7 @@ def tearDown(self): ikepolicy = self.conn.network.\ delete_vpn_ikepolicy(self.ID, ignore_missing=True) self.assertIsNone(ikepolicy) - super(TestIkePolicy, self).tearDown() + super(TestVpnIkePolicy, self).tearDown() def test_list(self): policies = [f.name for f in self.conn.network.vpn_ikepolicies()] diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 43f4a7d06..f7794ff29 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -27,8 +27,6 @@ from openstack.network.v2 import flavor from openstack.network.v2 import floating_ip from openstack.network.v2 import health_monitor -from openstack.network.v2 import ikepolicy -from openstack.network.v2 import ipsec_site_connection from openstack.network.v2 import l3_conntrack_helper from openstack.network.v2 import listener from openstack.network.v2 import load_balancer @@ -61,6 +59,8 @@ from openstack.network.v2 import subnet from openstack.network.v2 import subnet_pool from openstack.network.v2 import vpn_endpoint_group +from openstack.network.v2 import vpn_ikepolicy +from openstack.network.v2 import vpn_ipsec_site_connection from openstack.network.v2 import vpn_service from openstack import proxy as proxy_base from openstack.tests.unit import test_proxy_base @@ -314,66 +314,6 @@ def test_health_monitor_update(self): health_monitor.HealthMonitor) -class TestNetworkSiteConnection(TestNetworkProxy): - def test_ipsec_site_connection_create_attrs(self): - self.verify_create(self.proxy.create_vpn_ipsec_site_connection, - ipsec_site_connection.IPSecSiteConnection) - - def test_ipsec_site_connection_delete(self): - self.verify_delete(self.proxy.delete_vpn_ipsec_site_connection, - ipsec_site_connection.IPSecSiteConnection, False) - - def test_ipsec_site_connection_delete_ignore(self): - self.verify_delete(self.proxy.delete_vpn_ipsec_site_connection, - ipsec_site_connection.IPSecSiteConnection, True) - - def test_ipsec_site_connection_find(self): - self.verify_find(self.proxy.find_vpn_ipsec_site_connection, - ipsec_site_connection.IPSecSiteConnection) - - def test_ipsec_site_connection_get(self): - self.verify_get(self.proxy.get_vpn_ipsec_site_connection, - ipsec_site_connection.IPSecSiteConnection) - - def test_ipsec_site_connections(self): - self.verify_list(self.proxy.vpn_ipsec_site_connections, - ipsec_site_connection.IPSecSiteConnection) - - def test_ipsec_site_connection_update(self): - self.verify_update(self.proxy.update_vpn_ipsec_site_connection, - ipsec_site_connection.IPSecSiteConnection) - - -class TestNetworkIkePolicy(TestNetworkProxy): - def test_ikepolicy_create_attrs(self): - self.verify_create(self.proxy.create_vpn_ikepolicy, - ikepolicy.IkePolicy) - - def test_ikepolicy_delete(self): - self.verify_delete(self.proxy.delete_vpn_ikepolicy, - ikepolicy.IkePolicy, False) - - def test_ikepolicy_delete_ignore(self): - self.verify_delete(self.proxy.delete_vpn_ikepolicy, - ikepolicy.IkePolicy, True) - - def test_ikepolicy_find(self): - self.verify_find(self.proxy.find_vpn_ikepolicy, - ikepolicy.IkePolicy) - - def test_ikepolicy_get(self): - self.verify_get(self.proxy.get_vpn_ikepolicy, - ikepolicy.IkePolicy) - - def test_ikepolicies(self): - self.verify_list(self.proxy.vpn_ikepolicies, - ikepolicy.IkePolicy) - - def test_ikepolicy_update(self): - self.verify_update(self.proxy.update_vpn_ikepolicy, - ikepolicy.IkePolicy) - - class TestNetworkListener(TestNetworkProxy): def test_listener_create_attrs(self): self.verify_create(self.proxy.create_listener, listener.Listener) @@ -1637,65 +1577,139 @@ class TestNetworkVpnEndpointGroup(TestNetworkProxy): def test_vpn_endpoint_group_create_attrs(self): self.verify_create( self.proxy.create_vpn_endpoint_group, - vpn_endpoint_group.VPNEndpointGroup) + vpn_endpoint_group.VpnEndpointGroup) def test_vpn_endpoint_group_delete(self): self.verify_delete( self.proxy.delete_vpn_endpoint_group, - vpn_endpoint_group.VPNEndpointGroup, False) + vpn_endpoint_group.VpnEndpointGroup, False) def test_vpn_endpoint_group_delete_ignore(self): self.verify_delete( self.proxy.delete_vpn_endpoint_group, - vpn_endpoint_group.VPNEndpointGroup, True) + vpn_endpoint_group.VpnEndpointGroup, True) def test_vpn_endpoint_group_find(self): self.verify_find( self.proxy.find_vpn_endpoint_group, - vpn_endpoint_group.VPNEndpointGroup) + vpn_endpoint_group.VpnEndpointGroup) def test_vpn_endpoint_group_get(self): self.verify_get( self.proxy.get_vpn_endpoint_group, - vpn_endpoint_group.VPNEndpointGroup) + vpn_endpoint_group.VpnEndpointGroup) def test_vpn_endpoint_groups(self): self.verify_list( self.proxy.vpn_endpoint_groups, - vpn_endpoint_group.VPNEndpointGroup) + vpn_endpoint_group.VpnEndpointGroup) def test_vpn_endpoint_group_update(self): self.verify_update( self.proxy.update_vpn_endpoint_group, - vpn_endpoint_group.VPNEndpointGroup) + vpn_endpoint_group.VpnEndpointGroup) + + +class TestNetworkVpnSiteConnection(TestNetworkProxy): + def test_ipsec_site_connection_create_attrs(self): + self.verify_create( + self.proxy.create_vpn_ipsec_site_connection, + vpn_ipsec_site_connection.VpnIPSecSiteConnection) + + def test_ipsec_site_connection_delete(self): + self.verify_delete( + self.proxy.delete_vpn_ipsec_site_connection, + vpn_ipsec_site_connection.VpnIPSecSiteConnection, False) + + def test_ipsec_site_connection_delete_ignore(self): + self.verify_delete( + self.proxy.delete_vpn_ipsec_site_connection, + vpn_ipsec_site_connection.VpnIPSecSiteConnection, True) + + def test_ipsec_site_connection_find(self): + self.verify_find( + self.proxy.find_vpn_ipsec_site_connection, + vpn_ipsec_site_connection.VpnIPSecSiteConnection) + + def test_ipsec_site_connection_get(self): + self.verify_get( + self.proxy.get_vpn_ipsec_site_connection, + vpn_ipsec_site_connection.VpnIPSecSiteConnection) + + def test_ipsec_site_connections(self): + self.verify_list( + self.proxy.vpn_ipsec_site_connections, + vpn_ipsec_site_connection.VpnIPSecSiteConnection) + + def test_ipsec_site_connection_update(self): + self.verify_update( + self.proxy.update_vpn_ipsec_site_connection, + vpn_ipsec_site_connection.VpnIPSecSiteConnection) + + +class TestNetworkVpnIkePolicy(TestNetworkProxy): + def test_ikepolicy_create_attrs(self): + self.verify_create( + self.proxy.create_vpn_ikepolicy, + vpn_ikepolicy.VpnIkePolicy) + + def test_ikepolicy_delete(self): + self.verify_delete( + self.proxy.delete_vpn_ikepolicy, + vpn_ikepolicy.VpnIkePolicy, False) + + def test_ikepolicy_delete_ignore(self): + self.verify_delete( + self.proxy.delete_vpn_ikepolicy, + vpn_ikepolicy.VpnIkePolicy, True) + + def test_ikepolicy_find(self): + self.verify_find( + self.proxy.find_vpn_ikepolicy, + vpn_ikepolicy.VpnIkePolicy) + + def test_ikepolicy_get(self): + self.verify_get( + self.proxy.get_vpn_ikepolicy, + vpn_ikepolicy.VpnIkePolicy) + + def test_ikepolicies(self): + self.verify_list( + self.proxy.vpn_ikepolicies, + vpn_ikepolicy.VpnIkePolicy) + + def test_ikepolicy_update(self): + self.verify_update( + self.proxy.update_vpn_ikepolicy, + vpn_ikepolicy.VpnIkePolicy) class TestNetworkVpnService(TestNetworkProxy): def test_vpn_service_create_attrs(self): self.verify_create(self.proxy.create_vpn_service, - vpn_service.VPNService) + vpn_service.VpnService) def test_vpn_service_delete(self): self.verify_delete(self.proxy.delete_vpn_service, - vpn_service.VPNService, False) + vpn_service.VpnService, False) def test_vpn_service_delete_ignore(self): self.verify_delete(self.proxy.delete_vpn_service, - vpn_service.VPNService, True) + vpn_service.VpnService, True) def test_vpn_service_find(self): self.verify_find(self.proxy.find_vpn_service, - vpn_service.VPNService) + vpn_service.VpnService) def test_vpn_service_get(self): - self.verify_get(self.proxy.get_vpn_service, vpn_service.VPNService) + self.verify_get(self.proxy.get_vpn_service, vpn_service.VpnService) def test_vpn_services(self): - self.verify_list(self.proxy.vpn_services, vpn_service.VPNService) + self.verify_list(self.proxy.vpn_services, vpn_service.VpnService) def test_vpn_service_update(self): self.verify_update(self.proxy.update_vpn_service, - vpn_service.VPNService) + vpn_service.VpnService) class TestNetworkServiceProvider(TestNetworkProxy): diff --git a/openstack/tests/unit/network/v2/test_vpn_endpoint_group.py b/openstack/tests/unit/network/v2/test_vpn_endpoint_group.py index 9550d1de4..c94643324 100644 --- a/openstack/tests/unit/network/v2/test_vpn_endpoint_group.py +++ b/openstack/tests/unit/network/v2/test_vpn_endpoint_group.py @@ -29,10 +29,10 @@ } -class TestVPNEndpointGroup(base.TestCase): +class TestVpnEndpointGroup(base.TestCase): def test_basic(self): - sot = vpn_endpoint_group.VPNEndpointGroup() + sot = vpn_endpoint_group.VpnEndpointGroup() self.assertEqual('endpoint_group', sot.resource_key) self.assertEqual('endpoint_groups', sot.resources_key) self.assertEqual('/vpn/endpoint-groups', sot.base_path) @@ -43,7 +43,7 @@ def test_basic(self): self.assertTrue(sot.allow_list) def test_make_it(self): - sot = vpn_endpoint_group.VPNEndpointGroup(**EXAMPLE) + sot = vpn_endpoint_group.VpnEndpointGroup(**EXAMPLE) self.assertEqual(EXAMPLE['description'], sot.description) self.assertEqual(EXAMPLE['endpoints'], sot.endpoints) self.assertEqual(EXAMPLE['type'], sot.type) diff --git a/openstack/tests/unit/network/v2/test_ikepolicy.py b/openstack/tests/unit/network/v2/test_vpn_ikepolicy.py similarity index 91% rename from openstack/tests/unit/network/v2/test_ikepolicy.py rename to openstack/tests/unit/network/v2/test_vpn_ikepolicy.py index f17cfd29a..0e9310435 100644 --- a/openstack/tests/unit/network/v2/test_ikepolicy.py +++ b/openstack/tests/unit/network/v2/test_vpn_ikepolicy.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network.v2 import ikepolicy +from openstack.network.v2 import vpn_ikepolicy from openstack.tests.unit import base @@ -29,10 +29,10 @@ } -class TestIkePolicy(base.TestCase): +class TestVpnIkePolicy(base.TestCase): def test_basic(self): - sot = ikepolicy.IkePolicy() + sot = vpn_ikepolicy.VpnIkePolicy() self.assertEqual('ikepolicy', sot.resource_key) self.assertEqual('ikepolicies', sot.resources_key) self.assertEqual('/vpn/ikepolicies', sot.base_path) @@ -43,7 +43,7 @@ def test_basic(self): self.assertTrue(sot.allow_list) def test_make_it(self): - sot = ikepolicy.IkePolicy(**EXAMPLE) + sot = vpn_ikepolicy.VpnIkePolicy(**EXAMPLE) self.assertEqual(EXAMPLE['auth_algorithm'], sot.auth_algorithm) self.assertEqual(EXAMPLE['description'], sot.description) self.assertEqual(EXAMPLE['encryption_algorithm'], diff --git a/openstack/tests/unit/network/v2/test_ipsec_site_connection.py b/openstack/tests/unit/network/v2/test_vpn_ipsec_site_connection.py similarity index 90% rename from openstack/tests/unit/network/v2/test_ipsec_site_connection.py rename to openstack/tests/unit/network/v2/test_vpn_ipsec_site_connection.py index bee693bf6..48e406e55 100644 --- a/openstack/tests/unit/network/v2/test_ipsec_site_connection.py +++ b/openstack/tests/unit/network/v2/test_vpn_ipsec_site_connection.py @@ -1,80 +1,80 @@ -# 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.network.v2 import ipsec_site_connection -from openstack.tests.unit import base - - -IDENTIFIER = 'IDENTIFIER' -EXAMPLE = { - "admin_state_up": True, - "auth_mode": "1", - "ikepolicy_id": "2", - "vpnservice_id": "3", - "local_ep_group_id": "4", - "peer_address": "5", - "route_mode": "6", - "ipsecpolicy_id": "7", - "peer_id": "8", - "psk": "9", - "description": "10", - "initiator": "11", - "peer_cidrs": ['1', '2'], - "name": "12", - "tenant_id": "13", - "interval": 5, - "mtu": 5, - "peer_ep_group_id": "14", - "dpd": {'a': 5}, - "timeout": 16, - "action": "17", - "local_id": "18" -} - - -class TestIPSecSiteConnection(base.TestCase): - - def test_basic(self): - sot = ipsec_site_connection.IPSecSiteConnection() - self.assertEqual('ipsec_site_connection', sot.resource_key) - self.assertEqual('ipsec_site_connections', sot.resources_key) - self.assertEqual('/vpn/ipsec-site-connections', sot.base_path) - self.assertTrue(sot.allow_create) - self.assertTrue(sot.allow_fetch) - self.assertTrue(sot.allow_commit) - self.assertTrue(sot.allow_delete) - self.assertTrue(sot.allow_list) - - def test_make_it(self): - sot = ipsec_site_connection.IPSecSiteConnection(**EXAMPLE) - self.assertTrue(sot.is_admin_state_up) - self.assertEqual(EXAMPLE['auth_mode'], sot.auth_mode) - self.assertEqual(EXAMPLE['ikepolicy_id'], sot.ikepolicy_id) - self.assertEqual(EXAMPLE['vpnservice_id'], sot.vpnservice_id) - self.assertEqual(EXAMPLE['local_ep_group_id'], sot.local_ep_group_id) - self.assertEqual(EXAMPLE['peer_address'], sot.peer_address) - self.assertEqual(EXAMPLE['route_mode'], sot.route_mode) - self.assertEqual(EXAMPLE['ipsecpolicy_id'], sot.ipsecpolicy_id) - self.assertEqual(EXAMPLE['peer_id'], sot.peer_id) - self.assertEqual(EXAMPLE['psk'], sot.psk) - self.assertEqual(EXAMPLE['description'], sot.description) - self.assertEqual(EXAMPLE['initiator'], sot.initiator) - self.assertEqual(EXAMPLE['peer_cidrs'], sot.peer_cidrs) - self.assertEqual(EXAMPLE['name'], sot.name) - self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) - self.assertEqual(EXAMPLE['interval'], sot.interval) - self.assertEqual(EXAMPLE['mtu'], sot.mtu) - self.assertEqual(EXAMPLE['peer_ep_group_id'], sot.peer_ep_group_id) - self.assertEqual(EXAMPLE['dpd'], sot.dpd) - self.assertEqual(EXAMPLE['timeout'], sot.timeout) - self.assertEqual(EXAMPLE['action'], sot.action) - self.assertEqual(EXAMPLE['local_id'], sot.local_id) +# 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.network.v2 import vpn_ipsec_site_connection +from openstack.tests.unit import base + + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + "admin_state_up": True, + "auth_mode": "1", + "ikepolicy_id": "2", + "vpnservice_id": "3", + "local_ep_group_id": "4", + "peer_address": "5", + "route_mode": "6", + "ipsecpolicy_id": "7", + "peer_id": "8", + "psk": "9", + "description": "10", + "initiator": "11", + "peer_cidrs": ['1', '2'], + "name": "12", + "tenant_id": "13", + "interval": 5, + "mtu": 5, + "peer_ep_group_id": "14", + "dpd": {'a': 5}, + "timeout": 16, + "action": "17", + "local_id": "18" +} + + +class TestVpnIPSecSiteConnection(base.TestCase): + + def test_basic(self): + sot = vpn_ipsec_site_connection.VpnIPSecSiteConnection() + self.assertEqual('ipsec_site_connection', sot.resource_key) + self.assertEqual('ipsec_site_connections', sot.resources_key) + self.assertEqual('/vpn/ipsec-site-connections', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = vpn_ipsec_site_connection.VpnIPSecSiteConnection(**EXAMPLE) + self.assertTrue(sot.is_admin_state_up) + self.assertEqual(EXAMPLE['auth_mode'], sot.auth_mode) + self.assertEqual(EXAMPLE['ikepolicy_id'], sot.ikepolicy_id) + self.assertEqual(EXAMPLE['vpnservice_id'], sot.vpnservice_id) + self.assertEqual(EXAMPLE['local_ep_group_id'], sot.local_ep_group_id) + self.assertEqual(EXAMPLE['peer_address'], sot.peer_address) + self.assertEqual(EXAMPLE['route_mode'], sot.route_mode) + self.assertEqual(EXAMPLE['ipsecpolicy_id'], sot.ipsecpolicy_id) + self.assertEqual(EXAMPLE['peer_id'], sot.peer_id) + self.assertEqual(EXAMPLE['psk'], sot.psk) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['initiator'], sot.initiator) + self.assertEqual(EXAMPLE['peer_cidrs'], sot.peer_cidrs) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertEqual(EXAMPLE['interval'], sot.interval) + self.assertEqual(EXAMPLE['mtu'], sot.mtu) + self.assertEqual(EXAMPLE['peer_ep_group_id'], sot.peer_ep_group_id) + self.assertEqual(EXAMPLE['dpd'], sot.dpd) + self.assertEqual(EXAMPLE['timeout'], sot.timeout) + self.assertEqual(EXAMPLE['action'], sot.action) + self.assertEqual(EXAMPLE['local_id'], sot.local_id) diff --git a/openstack/tests/unit/network/v2/test_vpn_service.py b/openstack/tests/unit/network/v2/test_vpn_service.py index 005f37de3..c64a72d67 100644 --- a/openstack/tests/unit/network/v2/test_vpn_service.py +++ b/openstack/tests/unit/network/v2/test_vpn_service.py @@ -29,10 +29,10 @@ } -class TestVPNService(base.TestCase): +class TestVpnService(base.TestCase): def test_basic(self): - sot = vpn_service.VPNService() + sot = vpn_service.VpnService() self.assertEqual('vpnservice', sot.resource_key) self.assertEqual('vpnservices', sot.resources_key) self.assertEqual('/vpn/vpnservices', sot.base_path) @@ -43,7 +43,7 @@ def test_basic(self): self.assertTrue(sot.allow_list) def test_make_it(self): - sot = vpn_service.VPNService(**EXAMPLE) + sot = vpn_service.VpnService(**EXAMPLE) self.assertTrue(sot.is_admin_state_up) self.assertEqual(EXAMPLE['description'], sot.description) self.assertEqual(EXAMPLE['external_v4_ip'], sot.external_v4_ip) From 54e77e6215a9c56f4b4faaecd520736501c19b86 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 20 Jun 2022 12:28:21 +0200 Subject: [PATCH 3060/3836] Add VPNaaS IpsecPolicy resource Change-Id: Id2513fd77fc303b42a52c436e3b0d46f93b7d376 --- doc/source/user/proxies/network.rst | 4 +- .../user/resources/network/v2/vpn/index.rst | 1 + .../resources/network/v2/vpn/ipsecpolicy.rst | 13 +++ openstack/network/v2/_proxy.py | 100 +++++++++++++++++- openstack/network/v2/vpn_ipsec_policy.py | 59 +++++++++++ openstack/network/v2/vpn_ipsecpolicy.py | 57 ++++++++++ openstack/tests/unit/network/v2/test_proxy.py | 38 +++++++ .../unit/network/v2/test_vpn_ipsecpolicy.py | 61 +++++++++++ 8 files changed, 331 insertions(+), 2 deletions(-) create mode 100644 doc/source/user/resources/network/v2/vpn/ipsecpolicy.rst create mode 100644 openstack/network/v2/vpn_ipsec_policy.py create mode 100644 openstack/network/v2/vpn_ipsecpolicy.py create mode 100644 openstack/tests/unit/network/v2/test_vpn_ipsecpolicy.py diff --git a/doc/source/user/proxies/network.rst b/doc/source/user/proxies/network.rst index b7b9ecc4d..e443a12fe 100644 --- a/doc/source/user/proxies/network.rst +++ b/doc/source/user/proxies/network.rst @@ -254,7 +254,9 @@ VPNaaS Operations delete_vpn_ipsec_site_connection, get_vpn_ipsec_site_connection, find_vpn_ipsec_site_connection, vpn_ipsec_site_connections, create_vpn_ikepolicy, update_vpn_ikepolicy, delete_vpn_ikepolicy, - get_vpn_ikepolicy, find_vpn_ikepolicy, vpn_ikepolicies + get_vpn_ikepolicy, find_vpn_ikepolicy, vpn_ikepolicies, + create_vpn_ipsecpolicy, update_vpn_ipsecpolicy, delete_vpn_ipsecpolicy, + get_vpn_ipsecpolicy, find_vpn_ipsecpolicy, vpn_ipsecpolicies Extension Operations ^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/network/v2/vpn/index.rst b/doc/source/user/resources/network/v2/vpn/index.rst index bb9859323..f8bb64b11 100644 --- a/doc/source/user/resources/network/v2/vpn/index.rst +++ b/doc/source/user/resources/network/v2/vpn/index.rst @@ -7,4 +7,5 @@ VPNaaS Resources endpoint_group ipsec_site_connection ikepolicy + ipsecpolicy service diff --git a/doc/source/user/resources/network/v2/vpn/ipsecpolicy.rst b/doc/source/user/resources/network/v2/vpn/ipsecpolicy.rst new file mode 100644 index 000000000..8c43e02cd --- /dev/null +++ b/doc/source/user/resources/network/v2/vpn/ipsecpolicy.rst @@ -0,0 +1,13 @@ +openstack.network.v2.vpn_ikepolicy +================================== + +.. automodule:: openstack.network.v2.vpn_ikepolicy + +The VpnIkePolicy Class +---------------------- + +The ``VpnIkePolicy`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.vpn_ikepolicy.VpnIkePolicy + :members: diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index f1e8ec300..0542ea241 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -65,6 +65,7 @@ from openstack.network.v2 import vpn_ikepolicy as _ikepolicy from openstack.network.v2 import vpn_ipsec_site_connection as \ _ipsec_site_connection +from openstack.network.v2 import vpn_ipsecpolicy as _ipsecpolicy from openstack.network.v2 import vpn_service as _vpn_service from openstack import proxy @@ -4672,8 +4673,105 @@ def delete_vpn_ikepolicy(self, ikepolicy, ignore_missing=True): _ikepolicy.VpnIkePolicy, ikepolicy, ignore_missing=ignore_missing) - # ========== VPN Service ========== + # ========== IPSecPolicy ========== + def create_vpn_ipsecpolicy(self, **attrs): + """Create a new ipsec policy from attributes + + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.network.v2.vpn_ipsecpolicy.VpnIpsecPolicy`, + comprised of the properties on the VpnIpsecPolicy class. + + :returns: The results of ipsec policy creation :rtype: + :class:`~openstack.network.v2.vpn_ipsecpolicy.VpnIpsecPolicy` + """ + return self._create( + _ipsecpolicy.VpnIpsecPolicy, **attrs) + + def find_vpn_ipsecpolicy( + self, name_or_id, ignore_missing=True, **args + ): + """Find a single ipsec policy + + :param name_or_id: The name or ID of an ipsec policy. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` + will be raised when the resource does not exist. When set to + ``True``, None will be returned when attempting to find a + nonexistent resource. + :param dict args: Any additional parameters to be passed into + underlying methods such as query filters. + :returns: One + :class:`~openstack.network.v2.vpn_ipsecpolicy.VpnIpsecPolicy` + or None. + """ + return self._find( + _ipsecpolicy.VpnIpsecPolicy, name_or_id, + ignore_missing=ignore_missing, **args) + + def get_vpn_ipsecpolicy(self, ipsecpolicy): + """Get a single ipsec policy + + :param ipsecpolicy: The value can be the ID of an ipsecpolicy or a + :class:`~openstack.network.v2.vpn_ipsecpolicy.VpnIpsecPolicy` + instance. + + :returns: One + :class:`~openstack.network.v2.vpn_ipsecpolicy.VpnIpsecPolicy` + :rtype: :class:`~openstack.network.v2.ipsecpolicy.VpnIpsecPolicy` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource can be found. + """ + return self._get( + _ipsecpolicy.VpnIpsecPolicy, ipsecpolicy) + + def vpn_ipsecpolicies(self, **query): + """Return a generator of ipsec policy + + :param dict query: Optional query parameters to be sent to limit the + resources being returned. + + :returns: A generator of ipsec policy objects + :rtype: :class:`~openstack.network.v2.vpn_ipsecpolicy.VpnIpsecPolicy` + """ + return self._list( + _ipsecpolicy.VpnIpsecPolicy, **query) + + def update_vpn_ipsecpolicy(self, ipsecpolicy, **attrs): + """Update a ipsec policy + + :ipsecpolicy: Either the id of an ipsec policy or a + :class:`~openstack.network.v2.vpn_ipsecpolicy.VpnIpsecPolicy` + instance. + :param dict attrs: The attributes to update on the ipsec policy + represented by ``ipsecpolicy``. + + :returns: The updated ipsec policy + :rtype: :class:`~openstack.network.v2.vpn_ipsecpolicy.VpnIpsecPolicy` + """ + return self._update( + _ipsecpolicy.VpnIpsecPolicy, ipsecpolicy, **attrs) + def delete_vpn_ipsecpolicy(self, ipsecpolicy, ignore_missing=True): + """Delete a ipsecpolicy + + :param ipsecpolicy: The value can be either the ID of an ipsec policy, + or a + :class:`~openstack.network.v2.vpn_ipsecpolicy.VpnIpsecPolicy` + instance. + :param bool ignore_missing: + When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` + will be raised when the ipsec policy does not exist. When set to + ``True``, no exception will be set when attempting to delete a + nonexistent ipsec policy. + + :returns: ``None`` + """ + self._delete( + _ipsecpolicy.VpnIpsecPolicy, ipsecpolicy, + ignore_missing=ignore_missing) + + # ========== VPN Service ========== def create_vpn_service(self, **attrs): """Create a new vpn service from attributes diff --git a/openstack/network/v2/vpn_ipsec_policy.py b/openstack/network/v2/vpn_ipsec_policy.py new file mode 100644 index 000000000..60df3cbe8 --- /dev/null +++ b/openstack/network/v2/vpn_ipsec_policy.py @@ -0,0 +1,59 @@ +# 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 resource + + +class VpnIpsecPolicy(resource.Resource): + resource_key = 'ipsecpolicy' + resources_key = 'ipsecpolicies' + base_path = '/vpn/ipsecpolicies' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + # Properties + #: The authentication hash algorithm. Valid values are sha1, + # sha256, sha384, sha512. The default is sha1. + auth_algorithm = resource.Body('auth_algorithm') + #: A human-readable description for the resource. + # Default is an empty string. + description = resource.Body('description') + #: The encryption algorithm. A valid value is 3des, aes-128, + # aes-192, aes-256, and so on. Default is aes-128. + encryption_algorithm = resource.Body('encryption_algorithm') + #: The lifetime of the security association. The lifetime consists + # of a unit and integer value. You can omit either the unit or value + # portion of the lifetime. Default unit is seconds and + # default value is 3600. + lifetime = resource.Body('lifetime', type=dict) + #: Perfect forward secrecy (PFS). A valid value is Group2, + # Group5, Group14, and so on. Default is Group5. + pfs = resource.Body('pfs') + #: The ID of the project. + project_id = resource.Body('project_id') + #: The IKE mode. A valid value is main, which is the default. + phase1_negotiation_mode = resource.Body('phase1_negotiation_mode') + #: The units for the lifetime of the security association. + # The lifetime consists of a unit and integer value. + # You can omit either the unit or value portion of the lifetime. + # Default unit is seconds and default value is 3600. + units = resource.Body('units') + #: The lifetime value, as a positive integer. The lifetime + # consists of a unit and integer value. + # You can omit either the unit or value portion of the lifetime. + # Default unit is seconds and default value is 3600. + value = resource.Body('value', type=int) diff --git a/openstack/network/v2/vpn_ipsecpolicy.py b/openstack/network/v2/vpn_ipsecpolicy.py new file mode 100644 index 000000000..bae5609a0 --- /dev/null +++ b/openstack/network/v2/vpn_ipsecpolicy.py @@ -0,0 +1,57 @@ +# 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 resource + + +class VpnIpsecPolicy(resource.Resource): + resource_key = 'ipsecpolicy' + resources_key = 'ipsecpolicies' + base_path = '/vpn/ipsecpolicies' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + # Properties + #: The authentication hash algorithm. Valid values are sha1, + # sha256, sha384, sha512. The default is sha1. + auth_algorithm = resource.Body('auth_algorithm') + #: A human-readable description for the resource. + # Default is an empty string. + description = resource.Body('description') + #: The encryption algorithm. A valid value is 3des, aes-128, + # aes-192, aes-256, and so on. Default is aes-128. + encryption_algorithm = resource.Body('encryption_algorithm') + #: The lifetime of the security association. The lifetime consists + # of a unit and integer value. You can omit either the unit or value + # portion of the lifetime. Default unit is seconds and + # default value is 3600. + lifetime = resource.Body('lifetime', type=dict) + #: Perfect forward secrecy (PFS). A valid value is Group2, + # Group5, Group14, and so on. Default is Group5. + pfs = resource.Body('pfs') + #: The ID of the project. + project_id = resource.Body('project_id') + #: The units for the lifetime of the security association. + # The lifetime consists of a unit and integer value. + # You can omit either the unit or value portion of the lifetime. + # Default unit is seconds and default value is 3600. + units = resource.Body('units') + #: The lifetime value, as a positive integer. The lifetime + # consists of a unit and integer value. + # You can omit either the unit or value portion of the lifetime. + # Default unit is seconds and default value is 3600. + value = resource.Body('value', type=int) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index f7794ff29..d1030692f 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -61,6 +61,7 @@ from openstack.network.v2 import vpn_endpoint_group from openstack.network.v2 import vpn_ikepolicy from openstack.network.v2 import vpn_ipsec_site_connection +from openstack.network.v2 import vpn_ipsecpolicy from openstack.network.v2 import vpn_service from openstack import proxy as proxy_base from openstack.tests.unit import test_proxy_base @@ -1684,6 +1685,43 @@ def test_ikepolicy_update(self): vpn_ikepolicy.VpnIkePolicy) +class TestNetworkVpnIpsecPolicy(TestNetworkProxy): + def test_ipsecpolicy_create_attrs(self): + self.verify_create( + self.proxy.create_vpn_ipsecpolicy, + vpn_ipsecpolicy.VpnIpsecPolicy) + + def test_ipsecpolicy_delete(self): + self.verify_delete( + self.proxy.delete_vpn_ipsecpolicy, + vpn_ipsecpolicy.VpnIpsecPolicy, False) + + def test_ipsecpolicy_delete_ignore(self): + self.verify_delete( + self.proxy.delete_vpn_ipsecpolicy, + vpn_ipsecpolicy.VpnIpsecPolicy, True) + + def test_ipsecpolicy_find(self): + self.verify_find( + self.proxy.find_vpn_ipsecpolicy, + vpn_ipsecpolicy.VpnIpsecPolicy) + + def test_ipsecpolicy_get(self): + self.verify_get( + self.proxy.get_vpn_ipsecpolicy, + vpn_ipsecpolicy.VpnIpsecPolicy) + + def test_ipsecpolicies(self): + self.verify_list( + self.proxy.vpn_ipsecpolicies, + vpn_ipsecpolicy.VpnIpsecPolicy) + + def test_ipsecpolicy_update(self): + self.verify_update( + self.proxy.update_vpn_ipsecpolicy, + vpn_ipsecpolicy.VpnIpsecPolicy) + + class TestNetworkVpnService(TestNetworkProxy): def test_vpn_service_create_attrs(self): self.verify_create(self.proxy.create_vpn_service, diff --git a/openstack/tests/unit/network/v2/test_vpn_ipsecpolicy.py b/openstack/tests/unit/network/v2/test_vpn_ipsecpolicy.py new file mode 100644 index 000000000..bdecd07fc --- /dev/null +++ b/openstack/tests/unit/network/v2/test_vpn_ipsecpolicy.py @@ -0,0 +1,61 @@ +# 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.network.v2 import vpn_ipsecpolicy +from openstack.tests.unit import base + + +EXAMPLE = { + "auth_algorithm": "1", + "description": "2", + "encryption_algorithm": "3", + "lifetime": {'a': 5}, + "name": "5", + "pfs": "6", + "project_id": "7", + "units": "9", + "value": 10 +} + + +class TestVpnIpsecPolicy(base.TestCase): + + def test_basic(self): + sot = vpn_ipsecpolicy.VpnIpsecPolicy() + self.assertEqual('ipsecpolicy', sot.resource_key) + self.assertEqual('ipsecpolicies', sot.resources_key) + self.assertEqual('/vpn/ipsecpolicies', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = vpn_ipsecpolicy.VpnIpsecPolicy(**EXAMPLE) + self.assertEqual(EXAMPLE['auth_algorithm'], sot.auth_algorithm) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['encryption_algorithm'], + sot.encryption_algorithm) + self.assertEqual(EXAMPLE['lifetime'], sot.lifetime) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['pfs'], sot.pfs) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + self.assertEqual(EXAMPLE['units'], sot.units) + self.assertEqual(EXAMPLE['value'], sot.value) + + self.assertDictEqual( + { + "limit": "limit", + "marker": "marker", + }, + sot._query_mapping._mapping) From ca0934c5136d6ee96924d6521ce3dc1c8becff28 Mon Sep 17 00:00:00 2001 From: Thiago Brito Date: Thu, 4 Mar 2021 14:49:11 -0300 Subject: [PATCH 3061/3836] Add update capabilities to Snapshots This patch aims to add the update operation to Snapshots. Signed-off-by: Thiago Brito Change-Id: I6c61ebf9605a7b62653d644dedddb56935ac016d --- openstack/block_storage/v3/_proxy.py | 13 +++++++++++++ .../tests/unit/block_storage/v3/test_proxy.py | 17 +++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 3c7c5406e..67afeb130 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -93,6 +93,19 @@ def create_snapshot(self, **attrs): """ return self._create(_snapshot.Snapshot, **attrs) + def update_snapshot(self, snapshot, **attrs): + """Update a snapshot + + :param snapshot: Either the ID of a snapshot or a + :class:`~openstack.block_storage.v3.snapshot.Snapshot` instance. + :attrs kwargs: The attributes to update on the snapshot represented + by ``snapshot``. + + :returns: The updated snapshot + :rtype: :class:`~openstack.block_storage.v3.snapshot.Snapshot` + """ + return self._update(_snapshot.Snapshot, snapshot, **attrs) + def delete_snapshot(self, snapshot, ignore_missing=True, force=False): """Delete a snapshot diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index ab3eea369..7044db172 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -70,6 +70,23 @@ def test_volume_delete_force(self): expected_args=[self.proxy] ) + def test_snapshot_create_attrs(self): + self.verify_create(self.proxy.create_snapshot, snapshot.Snapshot) + + def test_snapshot_update(self): + self.verify_update(self.proxy.update_snapshot, snapshot.Snapshot) + + def test_snapshot_delete(self): + self.verify_delete(self.proxy.delete_snapshot, + snapshot.Snapshot, False) + + def test_snapshot_delete_ignore(self): + self.verify_delete(self.proxy.delete_snapshot, + snapshot.Snapshot, True) + + def test_type_get(self): + self.verify_get(self.proxy.get_type, type.Type) + def test_backend_pools(self): self.verify_list(self.proxy.backend_pools, stats.Pools) From 48f3a18b8772f83b8986bcaa7373e07d223a0245 Mon Sep 17 00:00:00 2001 From: Rafael Castillo Date: Mon, 30 May 2022 14:55:02 -0700 Subject: [PATCH 3062/3836] Allow Resource.to_dict to allow returning unknown values Resources allow storing arbitrary values in the body with _allow_unknown_attrs_in_body, but these unknown attributes aren't returned when converting to_dict. This patch enhances to_dict with an unknown_attrs parameter, that allows including the unknown attributes stored by the resource in the converted dictionary. Change-Id: I4c7685a4bb4aafb4cceadb6a507980e99c1e1a11 --- openstack/resource.py | 63 ++++++++++++++++-------- openstack/tests/unit/cloud/test_fwaas.py | 4 ++ openstack/tests/unit/cloud/test_port.py | 4 +- openstack/tests/unit/test_resource.py | 16 ++++++ 4 files changed, 66 insertions(+), 21 deletions(-) diff --git a/openstack/resource.py b/openstack/resource.py index 27c913e60..2457b9891 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1012,6 +1012,34 @@ def _from_munch(cls, obj, synchronized=True, connection=None): """ return cls(_synchronized=synchronized, connection=connection, **obj) + def _attr_to_dict(self, attr, to_munch): + """For a given attribute, convert it into a form suitable for a dict value. + + :param bool attr: Attribute name to convert + + :return: A dictionary of key/value pairs where keys are named + as they exist as attributes of this class. + :param bool _to_munch: Converts subresources to munch instead of dict. + """ + value = getattr(self, attr, None) + if isinstance(value, Resource): + return value.to_dict(_to_munch=to_munch) + elif isinstance(value, dict) and to_munch: + return munch.Munch(value) + elif value and isinstance(value, list): + converted = [] + for raw in value: + if isinstance(raw, Resource): + converted.append( + raw.to_dict(_to_munch=to_munch) + ) + elif isinstance(raw, dict) and to_munch: + converted.append(munch.Munch(raw)) + else: + converted.append(raw) + return converted + return value + def to_dict( self, body=True, @@ -1060,6 +1088,15 @@ def to_dict( # isinstance stricly requires this to be a tuple components = tuple(components) + if body and self._allow_unknown_attrs_in_body: + for key in self._unknown_attrs_in_body: + converted = self._attr_to_dict( + key, + to_munch=_to_munch, + ) + if not ignore_none or converted is not None: + mapping[key] = converted + # NOTE: This is similar to the implementation in _get_mapping # but is slightly different in that we're looking at an instance # and we're mapping names on this class to their actual stored @@ -1073,27 +1110,13 @@ def to_dict( # Make sure base classes don't end up overwriting # mappings we've found previously in subclasses. if key not in mapping: - value = getattr(self, attr, None) - if ignore_none and value is None: + converted = self._attr_to_dict( + attr, + to_munch=_to_munch, + ) + if ignore_none and converted is None: continue - if isinstance(value, Resource): - mapping[key] = value.to_dict(_to_munch=_to_munch) - elif isinstance(value, dict) and _to_munch: - mapping[key] = munch.Munch(value) - elif value and isinstance(value, list): - converted = [] - for raw in value: - if isinstance(raw, Resource): - converted.append( - raw.to_dict(_to_munch=_to_munch) - ) - elif isinstance(raw, dict) and _to_munch: - converted.append(munch.Munch(raw)) - else: - converted.append(raw) - mapping[key] = converted - else: - mapping[key] = value + mapping[key] = converted return mapping diff --git a/openstack/tests/unit/cloud/test_fwaas.py b/openstack/tests/unit/cloud/test_fwaas.py index 529798f64..959b3d113 100644 --- a/openstack/tests/unit/cloud/test_fwaas.py +++ b/openstack/tests/unit/cloud/test_fwaas.py @@ -828,7 +828,9 @@ class TestFirewallGroup(FirewallTestCase): _mock_returned_firewall_group_attrs = { 'admin_state_up': True, 'description': 'Providing max security!', + 'egress_firewall_policy': _mock_egress_policy_attrs['name'], 'egress_firewall_policy_id': _mock_egress_policy_attrs['id'], + 'ingress_firewall_policy': _mock_ingress_policy_attrs['name'], 'ingress_firewall_policy_id': _mock_ingress_policy_attrs['id'], 'id': firewall_group_id, 'name': firewall_group_name, @@ -860,6 +862,8 @@ def test_create_firewall_group(self): create_group_attrs = self._mock_firewall_group_attrs.copy() del create_group_attrs['id'] posted_group_attrs = self._mock_returned_firewall_group_attrs.copy() + del posted_group_attrs['egress_firewall_policy'] + del posted_group_attrs['ingress_firewall_policy'] del posted_group_attrs['id'] self.register_uris([ dict(method='GET', # short-circuit diff --git a/openstack/tests/unit/cloud/test_port.py b/openstack/tests/unit/cloud/test_port.py index fee0be6fb..350c1195a 100644 --- a/openstack/tests/unit/cloud/test_port.py +++ b/openstack/tests/unit/cloud/test_port.py @@ -37,6 +37,7 @@ class TestPort(base.TestCase): 'binding:vif_details': {}, 'binding:vnic_type': 'normal', 'binding:vif_type': 'unbound', + 'extra_dhcp_opts': [], 'device_owner': '', 'mac_address': '50:1c:0d:e4:f0:0d', 'binding:profile': {}, @@ -62,6 +63,7 @@ class TestPort(base.TestCase): 'network_id': 'test-net-id', 'tenant_id': 'test-tenant-id', 'binding:vif_details': {}, + 'extra_dhcp_opts': [], 'binding:vnic_type': 'normal', 'binding:vif_type': 'unbound', 'device_owner': '', @@ -194,7 +196,7 @@ def test_update_port(self): dict(method='GET', uri=self.get_mock_url( 'network', 'public', append=['v2.0', 'ports', port_id]), - json=self.mock_neutron_port_list_rep), + json=dict(port=self.mock_neutron_port_list_rep['ports'][0])), dict(method='PUT', uri=self.get_mock_url( 'network', 'public', diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 1fd84020f..2b2f67223 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -883,6 +883,22 @@ class Child(Parent): } self.assertEqual(expected, res.to_dict()) + def test_to_dict_with_unknown_attrs_in_body(self): + class Test(resource.Resource): + foo = resource.Body('foo') + _allow_unknown_attrs_in_body = True + + res = Test(id='FAKE_ID', foo='FOO', bar='BAR') + + expected = { + 'id': 'FAKE_ID', + 'name': None, + 'location': None, + 'foo': 'FOO', + 'bar': 'BAR', + } + self.assertEqual(expected, res.to_dict()) + def test_json_dumps_from_resource(self): class Test(resource.Resource): foo = resource.Body('foo_remote') From 20ffb8ef1b260adc99a5dce5facb842b21803605 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 23 Jun 2022 11:49:37 +0100 Subject: [PATCH 3063/3836] test: Remove duplicated tests We inadvertently introduced some duplicated tests as part of change I6c61ebf9605a7b62653d644dedddb56935ac016d. Remove them, splitting up the test class they were hidden in in the process. Change-Id: Ia9e450785800ab67f268fb3d23c15a5a00751ffc Signed-off-by: Stephen Finucane --- .../tests/unit/block_storage/v3/test_proxy.py | 360 ++++++++++-------- 1 file changed, 201 insertions(+), 159 deletions(-) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 7044db172..f2fa6b324 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -9,6 +9,7 @@ # 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 unittest import mock from openstack.block_storage.v3 import _proxy @@ -34,7 +35,6 @@ def setUp(self): class TestVolume(TestVolumeProxy): - def test_volume_get(self): self.verify_get(self.proxy.get_volume, volume.Volume) @@ -42,15 +42,20 @@ def test_volume_find(self): self.verify_find(self.proxy.find_volume, volume.Volume) def test_volumes_detailed(self): - self.verify_list(self.proxy.volumes, volume.Volume, - method_kwargs={"details": True, "query": 1}, - expected_kwargs={"query": 1, - "base_path": "/volumes/detail"}) + self.verify_list( + self.proxy.volumes, + volume.Volume, + method_kwargs={"details": True, "query": 1}, + expected_kwargs={"query": 1, "base_path": "/volumes/detail"}, + ) def test_volumes_not_detailed(self): - self.verify_list(self.proxy.volumes, volume.Volume, - method_kwargs={"details": False, "query": 1}, - expected_kwargs={"query": 1}) + self.verify_list( + self.proxy.volumes, + volume.Volume, + method_kwargs={"details": False, "query": 1}, + expected_kwargs={"query": 1}, + ) def test_volume_create_attrs(self): self.verify_create(self.proxy.create_volume, volume.Volume) @@ -67,42 +72,78 @@ def test_volume_delete_force(self): self.proxy.delete_volume, method_args=["value"], method_kwargs={"force": True}, - expected_args=[self.proxy] + expected_args=[self.proxy], ) - def test_snapshot_create_attrs(self): - self.verify_create(self.proxy.create_snapshot, snapshot.Snapshot) + def test_get_volume_metadata(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.fetch_metadata", + self.proxy.get_volume_metadata, + method_args=["value"], + expected_args=[self.proxy], + expected_result=volume.Volume(id="value", metadata={}), + ) - def test_snapshot_update(self): - self.verify_update(self.proxy.update_snapshot, snapshot.Snapshot) + def test_set_volume_metadata(self): + kwargs = {"a": "1", "b": "2"} + id = "an_id" + self._verify( + "openstack.block_storage.v3.volume.Volume.set_metadata", + self.proxy.set_volume_metadata, + method_args=[id], + method_kwargs=kwargs, + method_result=volume.Volume.existing(id=id, metadata=kwargs), + expected_args=[self.proxy], + expected_kwargs={'metadata': kwargs}, + expected_result=volume.Volume.existing(id=id, metadata=kwargs), + ) - def test_snapshot_delete(self): - self.verify_delete(self.proxy.delete_snapshot, - snapshot.Snapshot, False) + def test_delete_volume_metadata(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.delete_metadata_item", + self.proxy.delete_volume_metadata, + expected_result=None, + method_args=["value", ["key"]], + expected_args=[self.proxy, "key"], + ) - def test_snapshot_delete_ignore(self): - self.verify_delete(self.proxy.delete_snapshot, - snapshot.Snapshot, True) + def test_volume_wait_for(self): + value = volume.Volume(id='1234') + self.verify_wait_for_status( + self.proxy.wait_for_status, + method_args=[value], + expected_args=[self.proxy, value, 'available', ['error'], 2, 120], + ) - def test_type_get(self): - self.verify_get(self.proxy.get_type, type.Type) +class TestPools(TestVolumeProxy): def test_backend_pools(self): self.verify_list(self.proxy.backend_pools, stats.Pools) + +class TestLimit(TestVolumeProxy): def test_limits_get(self): self.verify_get( - self.proxy.get_limits, limits.Limit, + self.proxy.get_limits, + limits.Limit, method_args=[], - expected_kwargs={'requires_id': False}) + expected_kwargs={'requires_id': False}, + ) + +class TestCapabilities(TestVolumeProxy): def test_capabilites_get(self): self.verify_get(self.proxy.get_capabilities, capabilities.Capabilities) + +class TestResourceFilter(TestVolumeProxy): def test_resource_filters(self): - self.verify_list(self.proxy.resource_filters, - resource_filter.ResourceFilter) + self.verify_list( + self.proxy.resource_filters, resource_filter.ResourceFilter + ) + +class TestGroupType(TestVolumeProxy): def test_group_type_get(self): self.verify_get(self.proxy.get_group_type, group_type.GroupType) @@ -117,100 +158,71 @@ def test_group_type_create(self): def test_group_type_delete(self): self.verify_delete( - self.proxy.delete_group_type, group_type.GroupType, False) + self.proxy.delete_group_type, group_type.GroupType, False + ) def test_group_type_delete_ignore(self): self.verify_delete( - self.proxy.delete_group_type, group_type.GroupType, True) + self.proxy.delete_group_type, group_type.GroupType, True + ) def test_group_type_update(self): self.verify_update(self.proxy.update_group_type, group_type.GroupType) + +class TestExtension(TestVolumeProxy): def test_extensions(self): self.verify_list(self.proxy.extensions, extension.Extension) - def test_get_volume_metadata(self): - self._verify( - "openstack.block_storage.v3.volume.Volume.fetch_metadata", - self.proxy.get_volume_metadata, - method_args=["value"], - expected_args=[self.proxy], - expected_result=volume.Volume(id="value", metadata={})) - - def test_set_volume_metadata(self): - kwargs = {"a": "1", "b": "2"} - id = "an_id" - self._verify( - "openstack.block_storage.v3.volume.Volume.set_metadata", - self.proxy.set_volume_metadata, - method_args=[id], - method_kwargs=kwargs, - method_result=volume.Volume.existing( - id=id, metadata=kwargs), - expected_args=[self.proxy], - expected_kwargs={'metadata': kwargs}, - expected_result=volume.Volume.existing( - id=id, metadata=kwargs)) - - def test_delete_volume_metadata(self): - self._verify( - "openstack.block_storage.v3.volume.Volume.delete_metadata_item", - self.proxy.delete_volume_metadata, - expected_result=None, - method_args=["value", ["key"]], - expected_args=[self.proxy, "key"]) - - def test_volume_wait_for(self): - value = volume.Volume(id='1234') - self.verify_wait_for_status( - self.proxy.wait_for_status, - method_args=[value], - expected_args=[self.proxy, value, 'available', ['error'], 2, 120]) - class TestVolumeActions(TestVolumeProxy): - def test_volume_extend(self): self._verify( "openstack.block_storage.v3.volume.Volume.extend", self.proxy.extend_volume, method_args=["value", "new-size"], - expected_args=[self.proxy, "new-size"]) + expected_args=[self.proxy, "new-size"], + ) def test_volume_set_readonly_no_argument(self): self._verify( "openstack.block_storage.v3.volume.Volume.set_readonly", self.proxy.set_volume_readonly, method_args=["value"], - expected_args=[self.proxy, True]) + expected_args=[self.proxy, True], + ) def test_volume_set_readonly_false(self): self._verify( "openstack.block_storage.v3.volume.Volume.set_readonly", self.proxy.set_volume_readonly, method_args=["value", False], - expected_args=[self.proxy, False]) + expected_args=[self.proxy, False], + ) def test_volume_set_bootable(self): self._verify( "openstack.block_storage.v3.volume.Volume.set_bootable_status", self.proxy.set_volume_bootable_status, method_args=["value", True], - expected_args=[self.proxy, True]) + expected_args=[self.proxy, True], + ) def test_volume_reset_volume_status(self): self._verify( "openstack.block_storage.v3.volume.Volume.reset_status", self.proxy.reset_volume_status, method_args=["value", '1', '2', '3'], - expected_args=[self.proxy, '1', '2', '3']) + expected_args=[self.proxy, '1', '2', '3'], + ) def test_volume_revert_to_snapshot(self): self._verify( "openstack.block_storage.v3.volume.Volume.revert_to_snapshot", self.proxy.revert_volume_to_snapshot, method_args=["value", '1'], - expected_args=[self.proxy, '1']) + expected_args=[self.proxy, '1'], + ) def test_attach_instance(self): self._verify( @@ -218,7 +230,8 @@ def test_attach_instance(self): self.proxy.attach_volume, method_args=["value", '1'], method_kwargs={'instance': '2'}, - expected_args=[self.proxy, '1', '2', None]) + expected_args=[self.proxy, '1', '2', None], + ) def test_attach_host(self): self._verify( @@ -226,42 +239,48 @@ def test_attach_host(self): self.proxy.attach_volume, method_args=["value", '1'], method_kwargs={'host_name': '3'}, - expected_args=[self.proxy, '1', None, '3']) + expected_args=[self.proxy, '1', None, '3'], + ) def test_detach_defaults(self): self._verify( "openstack.block_storage.v3.volume.Volume.detach", self.proxy.detach_volume, method_args=["value", '1'], - expected_args=[self.proxy, '1', False, None]) + expected_args=[self.proxy, '1', False, None], + ) def test_detach_force(self): self._verify( "openstack.block_storage.v3.volume.Volume.detach", self.proxy.detach_volume, method_args=["value", '1', True, {'a': 'b'}], - expected_args=[self.proxy, '1', True, {'a': 'b'}]) + expected_args=[self.proxy, '1', True, {'a': 'b'}], + ) def test_unmanage(self): self._verify( "openstack.block_storage.v3.volume.Volume.unmanage", self.proxy.unmanage_volume, method_args=["value"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_migrate_default(self): self._verify( "openstack.block_storage.v3.volume.Volume.migrate", self.proxy.migrate_volume, method_args=["value", '1'], - expected_args=[self.proxy, '1', False, False, None]) + expected_args=[self.proxy, '1', False, False, None], + ) def test_migrate_nondefault(self): self._verify( "openstack.block_storage.v3.volume.Volume.migrate", self.proxy.migrate_volume, method_args=["value", '1', True, True], - expected_args=[self.proxy, '1', True, True, None]) + expected_args=[self.proxy, '1', True, True, None], + ) def test_migrate_cluster(self): self._verify( @@ -269,21 +288,24 @@ def test_migrate_cluster(self): self.proxy.migrate_volume, method_args=["value"], method_kwargs={'cluster': '3'}, - expected_args=[self.proxy, None, False, False, '3']) + expected_args=[self.proxy, None, False, False, '3'], + ) def test_complete_migration(self): self._verify( "openstack.block_storage.v3.volume.Volume.complete_migration", self.proxy.complete_volume_migration, method_args=["value", '1'], - expected_args=[self.proxy, "1", False]) + expected_args=[self.proxy, "1", False], + ) def test_complete_migration_error(self): self._verify( "openstack.block_storage.v3.volume.Volume.complete_migration", self.proxy.complete_volume_migration, method_args=["value", "1", True], - expected_args=[self.proxy, "1", True]) + expected_args=[self.proxy, "1", True], + ) def test_upload_to_image(self): self._verify( @@ -296,8 +318,9 @@ def test_upload_to_image(self): "disk_format": None, "container_format": None, "visibility": None, - "protected": None - }) + "protected": None, + }, + ) def test_upload_to_image_extended(self): self._verify( @@ -308,7 +331,7 @@ def test_upload_to_image_extended(self): "disk_format": "2", "container_format": "3", "visibility": "4", - "protected": "5" + "protected": "5", }, expected_args=[self.proxy, "1"], expected_kwargs={ @@ -316,50 +339,57 @@ def test_upload_to_image_extended(self): "disk_format": "2", "container_format": "3", "visibility": "4", - "protected": "5" - }) + "protected": "5", + }, + ) def test_reserve(self): self._verify( "openstack.block_storage.v3.volume.Volume.reserve", self.proxy.reserve_volume, method_args=["value"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_unreserve(self): self._verify( "openstack.block_storage.v3.volume.Volume.unreserve", self.proxy.unreserve_volume, method_args=["value"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_begin_detaching(self): self._verify( "openstack.block_storage.v3.volume.Volume.begin_detaching", self.proxy.begin_volume_detaching, method_args=["value"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_abort_detaching(self): self._verify( "openstack.block_storage.v3.volume.Volume.abort_detaching", self.proxy.abort_volume_detaching, method_args=["value"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_init_attachment(self): self._verify( "openstack.block_storage.v3.volume.Volume.init_attachment", self.proxy.init_volume_attachment, method_args=["value", "1"], - expected_args=[self.proxy, "1"]) + expected_args=[self.proxy, "1"], + ) def test_terminate_attachment(self): self._verify( "openstack.block_storage.v3.volume.Volume.terminate_attachment", self.proxy.terminate_volume_attachment, method_args=["value", "1"], - expected_args=[self.proxy, "1"]) + expected_args=[self.proxy, "1"], + ) class TestBackup(TestVolumeProxy): @@ -367,18 +397,23 @@ def test_backups_detailed(self): # NOTE: mock has_service self.proxy._connection = mock.Mock() self.proxy._connection.has_service = mock.Mock(return_value=True) - self.verify_list(self.proxy.backups, backup.Backup, - method_kwargs={"details": True, "query": 1}, - expected_kwargs={"query": 1, - "base_path": "/backups/detail"}) + self.verify_list( + self.proxy.backups, + backup.Backup, + method_kwargs={"details": True, "query": 1}, + expected_kwargs={"query": 1, "base_path": "/backups/detail"}, + ) def test_backups_not_detailed(self): # NOTE: mock has_service self.proxy._connection = mock.Mock() self.proxy._connection.has_service = mock.Mock(return_value=True) - self.verify_list(self.proxy.backups, backup.Backup, - method_kwargs={"details": False, "query": 1}, - expected_kwargs={"query": 1}) + self.verify_list( + self.proxy.backups, + backup.Backup, + method_kwargs={"details": False, "query": 1}, + expected_kwargs={"query": 1}, + ) def test_backup_get(self): # NOTE: mock has_service @@ -410,7 +445,7 @@ def test_backup_delete_force(self): self.proxy.delete_backup, method_args=["value"], method_kwargs={"force": True}, - expected_args=[self.proxy] + expected_args=[self.proxy], ) def test_backup_create_attrs(self): @@ -429,7 +464,7 @@ def test_backup_restore(self): method_args=['volume_id'], method_kwargs={'volume_id': 'vol_id', 'name': 'name'}, expected_args=[self.proxy], - expected_kwargs={'volume_id': 'vol_id', 'name': 'name'} + expected_kwargs={'volume_id': 'vol_id', 'name': 'name'}, ) def test_backup_reset(self): @@ -437,7 +472,8 @@ def test_backup_reset(self): "openstack.block_storage.v3.backup.Backup.reset", self.proxy.reset_backup, method_args=["value", "new_status"], - expected_args=[self.proxy, "new_status"]) + expected_args=[self.proxy, "new_status"], + ) class TestSnapshot(TestVolumeProxy): @@ -448,26 +484,34 @@ def test_snapshot_find(self): self.verify_find(self.proxy.find_snapshot, snapshot.Snapshot) def test_snapshots_detailed(self): - self.verify_list(self.proxy.snapshots, snapshot.SnapshotDetail, - method_kwargs={"details": True, "query": 1}, - expected_kwargs={"query": 1, - "base_path": "/snapshots/detail"}) + self.verify_list( + self.proxy.snapshots, + snapshot.SnapshotDetail, + method_kwargs={"details": True, "query": 1}, + expected_kwargs={"query": 1, "base_path": "/snapshots/detail"}, + ) def test_snapshots_not_detailed(self): - self.verify_list(self.proxy.snapshots, snapshot.Snapshot, - method_kwargs={"details": False, "query": 1}, - expected_kwargs={"query": 1}) + self.verify_list( + self.proxy.snapshots, + snapshot.Snapshot, + method_kwargs={"details": False, "query": 1}, + expected_kwargs={"query": 1}, + ) def test_snapshot_create_attrs(self): self.verify_create(self.proxy.create_snapshot, snapshot.Snapshot) + def test_snapshot_update(self): + self.verify_update(self.proxy.update_snapshot, snapshot.Snapshot) + def test_snapshot_delete(self): - self.verify_delete(self.proxy.delete_snapshot, - snapshot.Snapshot, False) + self.verify_delete( + self.proxy.delete_snapshot, snapshot.Snapshot, False + ) def test_snapshot_delete_ignore(self): - self.verify_delete(self.proxy.delete_snapshot, - snapshot.Snapshot, True) + self.verify_delete(self.proxy.delete_snapshot, snapshot.Snapshot, True) def test_snapshot_delete_force(self): self._verify( @@ -475,7 +519,7 @@ def test_snapshot_delete_force(self): self.proxy.delete_snapshot, method_args=["value"], method_kwargs={"force": True}, - expected_args=[self.proxy] + expected_args=[self.proxy], ) def test_reset(self): @@ -483,21 +527,24 @@ def test_reset(self): "openstack.block_storage.v3.snapshot.Snapshot.reset", self.proxy.reset_snapshot, method_args=["value", "new_status"], - expected_args=[self.proxy, "new_status"]) + expected_args=[self.proxy, "new_status"], + ) def test_set_status(self): self._verify( "openstack.block_storage.v3.snapshot.Snapshot.set_status", self.proxy.set_snapshot_status, method_args=["value", "new_status"], - expected_args=[self.proxy, "new_status", None]) + expected_args=[self.proxy, "new_status", None], + ) def test_set_status_percentage(self): self._verify( "openstack.block_storage.v3.snapshot.Snapshot.set_status", self.proxy.set_snapshot_status, method_args=["value", "new_status", "per"], - expected_args=[self.proxy, "new_status", "per"]) + expected_args=[self.proxy, "new_status", "per"], + ) def test_get_snapshot_metadata(self): self._verify( @@ -505,7 +552,8 @@ def test_get_snapshot_metadata(self): self.proxy.get_snapshot_metadata, method_args=["value"], expected_args=[self.proxy], - expected_result=snapshot.Snapshot(id="value", metadata={})) + expected_result=snapshot.Snapshot(id="value", metadata={}), + ) def test_set_snapshot_metadata(self): kwargs = {"a": "1", "b": "2"} @@ -515,12 +563,11 @@ def test_set_snapshot_metadata(self): self.proxy.set_snapshot_metadata, method_args=[id], method_kwargs=kwargs, - method_result=snapshot.Snapshot.existing( - id=id, metadata=kwargs), + method_result=snapshot.Snapshot.existing(id=id, metadata=kwargs), expected_args=[self.proxy], expected_kwargs={'metadata': kwargs}, - expected_result=snapshot.Snapshot.existing( - id=id, metadata=kwargs)) + expected_result=snapshot.Snapshot.existing(id=id, metadata=kwargs), + ) def test_delete_snapshot_metadata(self): self._verify( @@ -529,7 +576,8 @@ def test_delete_snapshot_metadata(self): self.proxy.delete_snapshot_metadata, expected_result=None, method_args=["value", ["key"]], - expected_args=[self.proxy, "key"]) + expected_args=[self.proxy, "key"], + ) class TestType(TestVolumeProxy): @@ -562,11 +610,11 @@ def test_type_extra_specs_update(self): self.proxy.update_type_extra_specs, method_args=[id], method_kwargs=kwargs, - method_result=type.Type.existing(id=id, - extra_specs=kwargs), + method_result=type.Type.existing(id=id, extra_specs=kwargs), expected_args=[self.proxy], expected_kwargs=kwargs, - expected_result=kwargs) + expected_result=kwargs, + ) def test_type_extra_specs_delete(self): self._verify( @@ -574,28 +622,32 @@ def test_type_extra_specs_delete(self): self.proxy.delete_type_extra_specs, expected_result=None, method_args=["value", "key"], - expected_args=[self.proxy, "key"]) + expected_args=[self.proxy, "key"], + ) def test_type_get_private_access(self): self._verify( "openstack.block_storage.v3.type.Type.get_private_access", self.proxy.get_type_access, method_args=["value"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_type_add_private_access(self): self._verify( "openstack.block_storage.v3.type.Type.add_private_access", self.proxy.add_type_access, method_args=["value", "a"], - expected_args=[self.proxy, "a"]) + expected_args=[self.proxy, "a"], + ) def test_type_remove_private_access(self): self._verify( "openstack.block_storage.v3.type.Type.remove_private_access", self.proxy.remove_type_access, method_args=["value", "a"], - expected_args=[self.proxy, "a"]) + expected_args=[self.proxy, "a"], + ) def test_type_encryption_get(self): self.verify_get( @@ -603,30 +655,31 @@ def test_type_encryption_get(self): type.TypeEncryption, method_args=['value'], expected_args=[], - expected_kwargs={ - 'volume_type_id': 'value', - 'requires_id': False - }) + expected_kwargs={'volume_type_id': 'value', 'requires_id': False}, + ) def test_type_encryption_create(self): self.verify_create( self.proxy.create_type_encryption, type.TypeEncryption, method_kwargs={'volume_type': 'id'}, - expected_kwargs={'volume_type_id': 'id'} + expected_kwargs={'volume_type_id': 'id'}, ) def test_type_encryption_update(self): self.verify_update( - self.proxy.update_type_encryption, type.TypeEncryption) + self.proxy.update_type_encryption, type.TypeEncryption + ) def test_type_encryption_delete(self): self.verify_delete( - self.proxy.delete_type_encryption, type.TypeEncryption, False) + self.proxy.delete_type_encryption, type.TypeEncryption, False + ) def test_type_encryption_delete_ignore(self): self.verify_delete( - self.proxy.delete_type_encryption, type.TypeEncryption, True) + self.proxy.delete_type_encryption, type.TypeEncryption, True + ) class TestQuota(TestVolumeProxy): @@ -642,7 +695,7 @@ def test_get(self): 'usage': False, }, method_result=quota_set.QuotaSet(), - expected_result=quota_set.QuotaSet() + expected_result=quota_set.QuotaSet(), ) def test_get_query(self): @@ -650,17 +703,14 @@ def test_get_query(self): 'openstack.resource.Resource.fetch', self.proxy.get_quota_set, method_args=['prj'], - method_kwargs={ - 'usage': True, - 'user_id': 'uid' - }, + method_kwargs={'usage': True, 'user_id': 'uid'}, expected_args=[self.proxy], expected_kwargs={ 'error_message': None, 'requires_id': False, 'usage': True, - 'user_id': 'uid' - } + 'user_id': 'uid', + }, ) def test_get_defaults(self): @@ -672,8 +722,8 @@ def test_get_defaults(self): expected_kwargs={ 'error_message': None, 'requires_id': False, - 'base_path': '/os-quota-sets/defaults' - } + 'base_path': '/os-quota-sets/defaults', + }, ) def test_reset(self): @@ -683,9 +733,7 @@ def test_reset(self): method_args=['prj'], method_kwargs={'user_id': 'uid'}, expected_args=[self.proxy], - expected_kwargs={ - 'user_id': 'uid' - } + expected_kwargs={'user_id': 'uid'}, ) @mock.patch('openstack.proxy.Proxy._get_resource', autospec=True) @@ -701,12 +749,6 @@ def test_update(self, gr_mock): 'a': 'b', }, expected_args=[self.proxy], - expected_kwargs={ - 'user_id': 'uid' - } - ) - gr_mock.assert_called_with( - self.proxy, - quota_set.QuotaSet, - 'qs', a='b' + expected_kwargs={'user_id': 'uid'}, ) + gr_mock.assert_called_with(self.proxy, quota_set.QuotaSet, 'qs', a='b') From 19c070a4057c0f715e8cb955fbe2707bce4a9bea Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 30 Jun 2022 12:21:54 +0100 Subject: [PATCH 3064/3836] docs: Add missing docs for block storage v3 proxy APIs While we're here, reshuffle some of the APIs to logically group them. Change-Id: Ib8254de9bcc33f76644ed184db8883292ba650fd Signed-off-by: Stephen Finucane --- doc/source/user/proxies/block_storage_v3.rst | 68 +++- openstack/block_storage/v3/_proxy.py | 348 ++++++++++--------- 2 files changed, 241 insertions(+), 175 deletions(-) diff --git a/doc/source/user/proxies/block_storage_v3.rst b/doc/source/user/proxies/block_storage_v3.rst index a7a1e7747..d1e3ecf31 100644 --- a/doc/source/user/proxies/block_storage_v3.rst +++ b/doc/source/user/proxies/block_storage_v3.rst @@ -17,28 +17,80 @@ Volume Operations .. autoclass:: openstack.block_storage.v3._proxy.Proxy :noindex: - :members: create_volume, delete_volume, get_volume, volumes + :members: create_volume, delete_volume, get_volume, find_volume, + volumes, get_volume_metadata, set_volume_metadata, + delete_volume_metadata, extend_volume, set_volume_readonly, + retype_volume, set_volume_bootable_status, reset_volume_status, + revert_volume_to_snapshot, attach_volume, detach_volume, + unmanage_volume, migrate_volume, complete_volume_migration, + upload_volume_to_image, reserve_volume, unreserve_volume, + begin_volume_detaching, abort_volume_detaching, + init_volume_attachment, terminate_volume_attachment, + +Backend Pools Operations +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v3._proxy.Proxy + :noindex: + :members: backend_pools Backup Operations ^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.block_storage.v3._proxy.Proxy :noindex: - :members: create_backup, delete_backup, get_backup, backups, restore_backup + :members: create_backup, delete_backup, get_backup, find_backup, backups, + restore_backup, reset_backup, + +Availability Zone Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v3._proxy.Proxy + :noindex: + :members: availability_zones + +Limits Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v3._proxy.Proxy + :noindex: + :members: get_limits + +Capabilities Operations +^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v3._proxy.Proxy + :noindex: + :members: get_capabilities + +Group Type Operations +^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v3._proxy.Proxy + :noindex: + :members: create_group_type, delete_group_type, update_group_type, + get_group_type, find_group_type, group_types, Type Operations ^^^^^^^^^^^^^^^ .. autoclass:: openstack.block_storage.v3._proxy.Proxy :noindex: - :members: create_type, delete_type, get_type, types + :members: create_type, delete_type, update_type, get_type, find_type, types, + update_type_extra_specs, delete_type_extra_specs, get_type_access, + add_type_access, remove_type_access, get_type_encryption, + create_type_encryption, delete_type_encryption, + update_type_encryption Snapshot Operations ^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.block_storage.v3._proxy.Proxy :noindex: - :members: create_snapshot, delete_snapshot, get_snapshot, snapshots + :members: create_snapshot, delete_snapshot, update_snapshot, get_snapshot, + find_snapshot, snapshots, get_snapshot_metadata, + set_snapshot_metadata, delete_snapshot_metadata, reset_snapshot, + set_snapshot_status Stats Operations ^^^^^^^^^^^^^^^^ @@ -49,7 +101,15 @@ Stats Operations QuotaSet Operations ^^^^^^^^^^^^^^^^^^^ + .. autoclass:: openstack.block_storage.v3._proxy.Proxy :noindex: :members: get_quota_set, get_quota_set_defaults, revert_quota_set, update_quota_set + +Helpers +^^^^^^^ + +.. autoclass:: openstack.block_storage.v3._proxy.Proxy + :noindex: + :members: wait_for_status, wait_for_delete diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 67afeb130..c1df06ce5 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -98,8 +98,7 @@ def update_snapshot(self, snapshot, **attrs): :param snapshot: Either the ID of a snapshot or a :class:`~openstack.block_storage.v3.snapshot.Snapshot` instance. - :attrs kwargs: The attributes to update on the snapshot represented - by ``snapshot``. + :param dict attrs: The attributes to update on the snapshot. :returns: The updated snapshot :rtype: :class:`~openstack.block_storage.v3.snapshot.Snapshot` @@ -127,6 +126,54 @@ def delete_snapshot(self, snapshot, ignore_missing=True, force=False): snapshot = self._get_resource(_snapshot.Snapshot, snapshot) snapshot.force_delete(self) + def get_snapshot_metadata(self, snapshot): + """Return a dictionary of metadata for a snapshot + + :param snapshot: Either the ID of a snapshot or a + :class:`~openstack.block_storage.v3.snapshot.Snapshot`. + + :returns: A + :class:`~openstack.block_storage.v3.snapshot.Snapshot` with the + snapshot's metadata. All keys and values are Unicode text. + :rtype: :class:`~openstack.block_storage.v3.snapshot.Snapshot` + """ + snapshot = self._get_resource(_snapshot.Snapshot, snapshot) + return snapshot.fetch_metadata(self) + + def set_snapshot_metadata(self, snapshot, **metadata): + """Update metadata for a snapshot + + :param snapshot: Either the ID of a snapshot or a + :class:`~openstack.block_storage.v3.snapshot.Snapshot`. + :param kwargs metadata: Key/value pairs to be updated in the snapshot's + metadata. No other metadata is modified by this call. All keys + and values are stored as Unicode. + + :returns: A + :class:`~openstack.block_storage.v3.snapshot.Snapshot` with the + snapshot's metadata. All keys and values are Unicode text. + :rtype: :class:`~openstack.block_storage.v3.snapshot.Snapshot` + """ + snapshot = self._get_resource(_snapshot.Snapshot, snapshot) + return snapshot.set_metadata(self, metadata=metadata) + + def delete_snapshot_metadata(self, snapshot, keys=None): + """Delete metadata for a snapshot + + :param snapshot: Either the ID of a snapshot or a + :class:`~openstack.block_storage.v3.snapshot.Snapshot`. + :param list keys: The keys to delete. If left empty complete + metadata will be removed. + + :rtype: ``None`` + """ + snapshot = self._get_resource(_snapshot.Snapshot, snapshot) + if keys is not None: + for key in keys: + snapshot.delete_metadata_item(self, key) + else: + snapshot.delete_metadata(self) + # ====== SNAPSHOT ACTIONS ====== def reset_snapshot(self, snapshot, status): """Reset status of the snapshot @@ -221,7 +268,6 @@ def update_type(self, type, **attrs): :param type: The value can be either the ID of a type or a :class:`~openstack.block_storage.v3.type.Type` instance. :param dict attrs: The attributes to update on the type - represented by ``value``. :returns: The updated type :rtype: :class:`~openstack.block_storage.v3.type.Type` @@ -233,8 +279,7 @@ def update_type_extra_specs(self, type, **attrs): :param type: The value can be either the ID of a type or a :class:`~openstack.block_storage.v3.type.Type` instance. - :param dict attrs: The extra_spec attributes to update on the - type represented by ``value``. + :param dict attrs: The extra spec attributes to update on the type :returns: A dict containing updated extra_specs """ @@ -362,17 +407,21 @@ def delete_type_encryption(self, encryption=None, self._delete(_type.TypeEncryption, encryption, ignore_missing=ignore_missing) - def update_type_encryption(self, encryption=None, - volume_type=None, **attrs): + def update_type_encryption( + self, + encryption=None, + volume_type=None, + **attrs, + ): """Update a type + :param encryption: The value can be None or a :class:`~openstack.block_storage.v3.type.TypeEncryption` - instance. If encryption_id is None then - volume_type_id must be specified. - + instance. If this is ``None`` then ``volume_type_id`` must be + specified. :param volume_type: The value can be the ID of a type or a - :class:`~openstack.block_storage.v3.type.Type` - instance. Required if encryption_id is None. + :class:`~openstack.block_storage.v3.type.Type` instance. + Required if ``encryption_id`` is None. :param dict attrs: The attributes to update on the type encryption. :returns: The updated type encryption @@ -381,9 +430,11 @@ def update_type_encryption(self, encryption=None, if volume_type: volume_type = self._get_resource(_type.Type, volume_type) - encryption = self._get(_type.TypeEncryption, - volume_type=volume_type.id, - requires_id=False) + encryption = self._get( + _type.TypeEncryption, + volume_type=volume_type.id, + requires_id=False, + ) return self._update(_type.TypeEncryption, encryption, **attrs) @@ -466,6 +517,52 @@ def delete_volume(self, volume, ignore_missing=True, force=False): volume = self._get_resource(_volume.Volume, volume) volume.force_delete(self) + def get_volume_metadata(self, volume): + """Return a dictionary of metadata for a volume + + :param volume: Either the ID of a volume or a + :class:`~openstack.block_storage.v3.volume.Volume`. + + :returns: A :class:`~openstack.block_storage.v3.volume.Volume` with the + volume's metadata. All keys and values are Unicode text. + :rtype: :class:`~openstack.block_storage.v3.volume.Volume` + """ + volume = self._get_resource(_volume.Volume, volume) + return volume.fetch_metadata(self) + + def set_volume_metadata(self, volume, **metadata): + """Update metadata for a volume + + :param volume: Either the ID of a volume or a + :class:`~openstack.block_storage.v3.volume.Volume`. + :param kwargs metadata: Key/value pairs to be updated in the volume's + metadata. No other metadata is modified by this call. All keys + and values are stored as Unicode. + + :returns: A :class:`~openstack.block_storage.v3.volume.Volume` with the + volume's metadata. All keys and values are Unicode text. + :rtype: :class:`~openstack.block_storage.v3.volume.Volume` + """ + volume = self._get_resource(_volume.Volume, volume) + return volume.set_metadata(self, metadata=metadata) + + def delete_volume_metadata(self, volume, keys=None): + """Delete metadata for a volume + + :param volume: Either the ID of a volume or a + :class:`~openstack.block_storage.v3.volume.Volume`. + :param list keys: The keys to delete. If left empty complete + metadata will be removed. + + :rtype: ``None`` + """ + volume = self._get_resource(_volume.Volume, volume) + if keys is not None: + for key in keys: + volume.delete_metadata_item(self, key) + else: + volume.delete_metadata(self) + # ====== VOLUME ACTIONS ====== def extend_volume(self, volume, size): """Extend a volume @@ -985,64 +1082,9 @@ def update_group_type(self, group_type, **attrs): return self._update( _group_type.GroupType, group_type, **attrs) - def wait_for_status(self, res, status='available', failures=None, - interval=2, wait=120): - """Wait for a resource to be in a particular status. - - :param res: The resource to wait on to reach the specified status. - The resource must have a ``status`` attribute. - :type resource: A :class:`~openstack.resource.Resource` object. - :param status: Desired status. - :param failures: Statuses that would be interpreted as failures. - :type failures: :py:class:`list` - :param interval: Number of seconds to wait before to consecutive - checks. Default to 2. - :param wait: Maximum number of seconds to wait before the change. - Default to 120. - :returns: The resource is returned on success. - :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition - to the desired status failed to occur in specified seconds. - :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource - has transited to one of the failure statuses. - :raises: :class:`~AttributeError` if the resource does not have a - ``status`` attribute. - """ - failures = ['error'] if failures is None else failures - return resource.wait_for_status( - self, res, status, failures, interval, wait) - - def wait_for_delete(self, res, interval=2, wait=120): - """Wait for a resource to be deleted. - - :param res: The resource to wait on to be deleted. - :type resource: A :class:`~openstack.resource.Resource` object. - :param interval: Number of seconds to wait before to consecutive - checks. Default to 2. - :param wait: Maximum number of seconds to wait before the change. - Default to 120. - :returns: The resource is returned on success. - :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition - to delete failed to occur in the specified seconds. - """ - return resource.wait_for_delete(self, res, interval, wait) - - def resource_filters(self, **query): - """Retrieve a generator of resource filters - - :returns: A generator of resource filters. - """ - return self._list(_resource_filter.ResourceFilter, **query) - - def extensions(self): - """Return a generator of extensions - - :returns: A generator of extension - :rtype: :class:`~openstack.block_storage.v3.extension.Extension` - """ - return self._list(_extension.Extension) - + # ====== QUOTA SETS ====== def get_quota_set(self, project, usage=False, **query): - """Show QuotaSet information for the project + """Show quota set information for the project :param project: ID or instance of :class:`~openstack.identity.project.Project` of the project for @@ -1062,7 +1104,7 @@ def get_quota_set(self, project, usage=False, **query): self, usage=usage, **query) def get_quota_set_defaults(self, project): - """Show QuotaSet defaults for the project + """Show quota set defaults for the project :param project: ID or instance of :class:`~openstack.identity.project.Project` of the project for @@ -1079,7 +1121,7 @@ def get_quota_set_defaults(self, project): self, base_path='/os-quota-sets/defaults') def revert_quota_set(self, project, **query): - """Reset Quota for the project/user. + """Reset quota for the project/user. :param project: ID or instance of :class:`~openstack.identity.project.Project` of the project for @@ -1097,13 +1139,12 @@ def revert_quota_set(self, project, **query): return res.delete(self, **query) def update_quota_set(self, quota_set, query=None, **attrs): - """Update a QuotaSet. + """Update a quota set. :param quota_set: Either the ID of a quota_set or a :class:`~openstack.block_storage.v3.quota_set.QuotaSet` instance. :param dict query: Optional parameters to be used with update call. - :attrs kwargs: The attributes to update on the QuotaSet represented - by ``quota_set``. + :attrs kwargs: The attributes to update on the quota set :returns: The updated QuotaSet :rtype: :class:`~openstack.block_storage.v3.quota_set.QuotaSet` @@ -1113,6 +1154,65 @@ def update_quota_set(self, quota_set, query=None, **attrs): query = {} return res.commit(self, **query) + # ====== RESOURCE FILTERS ====== + def resource_filters(self, **query): + """Retrieve a generator of resource filters + + :returns: A generator of resource filters. + """ + return self._list(_resource_filter.ResourceFilter, **query) + + # ====== EXTENSIONS ====== + def extensions(self): + """Return a generator of extensions + + :returns: A generator of extension + :rtype: :class:`~openstack.block_storage.v3.extension.Extension` + """ + return self._list(_extension.Extension) + + # ====== UTILS ====== + def wait_for_status(self, res, status='available', failures=None, + interval=2, wait=120): + """Wait for a resource to be in a particular status. + + :param res: The resource to wait on to reach the specified status. + The resource must have a ``status`` attribute. + :type resource: A :class:`~openstack.resource.Resource` object. + :param status: Desired status. + :param failures: Statuses that would be interpreted as failures. + :type failures: :py:class:`list` + :param interval: Number of seconds to wait before to consecutive + checks. Default to 2. + :param wait: Maximum number of seconds to wait before the change. + Default to 120. + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to the desired status failed to occur in specified seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + has transited to one of the failure statuses. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute. + """ + failures = ['error'] if failures is None else failures + return resource.wait_for_status( + self, res, status, failures, interval, wait) + + def wait_for_delete(self, res, interval=2, wait=120): + """Wait for a resource to be deleted. + + :param res: The resource to wait on to be deleted. + :type resource: A :class:`~openstack.resource.Resource` object. + :param interval: Number of seconds to wait before to consecutive + checks. Default to 2. + :param wait: Maximum number of seconds to wait before the change. + Default to 120. + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete(self, res, interval, wait) + def _get_cleanup_dependencies(self): return { 'block_storage': { @@ -1174,97 +1274,3 @@ def _service_cleanup(self, dry_run=True, client_status_queue=None, identified_resources=identified_resources, filters=filters, resource_evaluation_fn=resource_evaluation_fn) - - def get_volume_metadata(self, volume): - """Return a dictionary of metadata for a volume - - :param volume: Either the ID of a volume or a - :class:`~openstack.block_storage.v3.volume.Volume`. - - :returns: A :class:`~openstack.block_storage.v3.volume.Volume` with the - volume's metadata. All keys and values are Unicode text. - :rtype: :class:`~openstack.block_storage.v3.volume.Volume` - """ - volume = self._get_resource(_volume.Volume, volume) - return volume.fetch_metadata(self) - - def set_volume_metadata(self, volume, **metadata): - """Update metadata for a volume - - :param volume: Either the ID of a volume or a - :class:`~openstack.block_storage.v3.volume.Volume`. - :param kwargs metadata: Key/value pairs to be updated in the volume's - metadata. No other metadata is modified by this call. All keys - and values are stored as Unicode. - - :returns: A :class:`~openstack.block_storage.v3.volume.Volume` with the - volume's metadata. All keys and values are Unicode text. - :rtype: :class:`~openstack.block_storage.v3.volume.Volume` - """ - volume = self._get_resource(_volume.Volume, volume) - return volume.set_metadata(self, metadata=metadata) - - def delete_volume_metadata(self, volume, keys=None): - """Delete metadata for a volume - - :param volume: Either the ID of a volume or a - :class:`~openstack.block_storage.v3.volume.Volume`. - :param list keys: The keys to delete. If left empty complete - metadata will be removed. - - :rtype: ``None`` - """ - volume = self._get_resource(_volume.Volume, volume) - if keys is not None: - for key in keys: - volume.delete_metadata_item(self, key) - else: - volume.delete_metadata(self) - - def get_snapshot_metadata(self, snapshot): - """Return a dictionary of metadata for a snapshot - - :param snapshot: Either the ID of a snapshot or a - :class:`~openstack.block_storage.v3.snapshot.Snapshot`. - - :returns: A - :class:`~openstack.block_storage.v3.snapshot.Snapshot` with the - snapshot's metadata. All keys and values are Unicode text. - :rtype: :class:`~openstack.block_storage.v3.snapshot.Snapshot` - """ - snapshot = self._get_resource(_snapshot.Snapshot, snapshot) - return snapshot.fetch_metadata(self) - - def set_snapshot_metadata(self, snapshot, **metadata): - """Update metadata for a snapshot - - :param snapshot: Either the ID of a snapshot or a - :class:`~openstack.block_storage.v3.snapshot.Snapshot`. - :param kwargs metadata: Key/value pairs to be updated in the snapshot's - metadata. No other metadata is modified by this call. All keys - and values are stored as Unicode. - - :returns: A - :class:`~openstack.block_storage.v3.snapshot.Snapshot` with the - snapshot's metadata. All keys and values are Unicode text. - :rtype: :class:`~openstack.block_storage.v3.snapshot.Snapshot` - """ - snapshot = self._get_resource(_snapshot.Snapshot, snapshot) - return snapshot.set_metadata(self, metadata=metadata) - - def delete_snapshot_metadata(self, snapshot, keys=None): - """Delete metadata for a snapshot - - :param snapshot: Either the ID of a snapshot or a - :class:`~openstack.block_storage.v3.snapshot.Snapshot`. - :param list keys: The keys to delete. If left empty complete - metadata will be removed. - - :rtype: ``None`` - """ - snapshot = self._get_resource(_snapshot.Snapshot, snapshot) - if keys is not None: - for key in keys: - snapshot.delete_metadata_item(self, key) - else: - snapshot.delete_metadata(self) From 9e9fc9879583943a08f854980cca5dfb3a5832f7 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 24 Jun 2022 12:23:21 +0100 Subject: [PATCH 3065/3836] block storage: Add support for group type specs Add the ability to manipulate these. Change-Id: I6b0dc26044e0b1e49eb2e16bb72f77bf48a9f603 Signed-off-by: Stephen Finucane --- doc/source/user/proxies/block_storage_v3.rst | 4 + openstack/block_storage/v3/_proxy.py | 60 ++++++++++ openstack/block_storage/v3/group_type.py | 88 ++++++++++++++- .../functional/block_storage/v3/test_group.py | 103 ++++++++++++++++++ .../block_storage/v3/test_group_type.py | 39 ------- .../unit/block_storage/v3/test_group_type.py | 103 +++++++++++++++++- .../tests/unit/block_storage/v3/test_proxy.py | 41 +++++++ ...oup-type-group-specs-d07047167224ec83.yaml | 5 + 8 files changed, 400 insertions(+), 43 deletions(-) create mode 100644 openstack/tests/functional/block_storage/v3/test_group.py delete mode 100644 openstack/tests/functional/block_storage/v3/test_group_type.py create mode 100644 releasenotes/notes/add-block-storage-group-type-group-specs-d07047167224ec83.yaml diff --git a/doc/source/user/proxies/block_storage_v3.rst b/doc/source/user/proxies/block_storage_v3.rst index d1e3ecf31..73ff27cfd 100644 --- a/doc/source/user/proxies/block_storage_v3.rst +++ b/doc/source/user/proxies/block_storage_v3.rst @@ -70,6 +70,10 @@ Group Type Operations :noindex: :members: create_group_type, delete_group_type, update_group_type, get_group_type, find_group_type, group_types, + fetch_group_type_group_specs, create_group_type_group_specs, + get_group_type_group_specs_property, + update_group_type_group_specs_property, + delete_group_type_group_specs_property Type Operations ^^^^^^^^^^^^^^^ diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index c1df06ce5..5570f8188 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -1082,6 +1082,66 @@ def update_group_type(self, group_type, **attrs): return self._update( _group_type.GroupType, group_type, **attrs) + def fetch_group_type_group_specs(self, group_type): + """Lists group specs of a group type. + + :param group_type: Either the ID of a group type or a + :class:`~openstack.block_storage.v3.group_type.GroupType` instance. + + :returns: One :class:`~openstack.block_storage.v3.group_type.GroupType` + """ + group_type = self._get_resource(_group_type.GroupType, group_type) + return group_type.fetch_group_specs(self) + + def create_group_type_group_specs(self, group_type, group_specs): + """Create group specs for a group type. + + :param group_type: Either the ID of a group type or a + :class:`~openstack.block_storage.v3.group_type.GroupType` instance. + :param dict group_specs: dict of extra specs + + :returns: One :class:`~openstack.block_storage.v3.group_type.GroupType` + """ + group_type = self._get_resource(_group_type.GroupType, group_type) + return group_type.create_group_specs(self, specs=group_specs) + + def get_group_type_group_specs_property(self, group_type, prop): + """Retrieve a group spec property for a group type. + + :param group_type: Either the ID of a group type or a + :class:`~openstack.block_storage.v3.group_type.GroupType` instance. + :param str prop: Property name. + + :returns: String value of the requested property. + """ + group_type = self._get_resource(_group_type.GroupType, group_type) + return group_type.get_group_specs_property(self, prop) + + def update_group_type_group_specs_property(self, group_type, prop, val): + """Update a group spec property for a group type. + + :param group_type: Either the ID of a group type or a + :class:`~openstack.block_storage.v3.group_type.GroupType` instance. + :param str prop: Property name. + :param str val: Property value. + + :returns: String value of the requested property. + """ + group_type = self._get_resource(_group_type.GroupType, group_type) + return group_type.update_group_specs_property(self, prop, val) + + def delete_group_type_group_specs_property(self, group_type, prop): + """Delete a group spec property from a group type. + + :param group_type: Either the ID of a group type or a + :class:`~openstack.block_storage.v3.group_type.GroupType` instance. + :param str prop: Property name. + + :returns: None + """ + group_type = self._get_resource(_group_type.GroupType, group_type) + return group_type.delete_group_specs_property(self, prop) + # ====== QUOTA SETS ====== def get_quota_set(self, project, usage=False, **query): """Show quota set information for the project diff --git a/openstack/block_storage/v3/group_type.py b/openstack/block_storage/v3/group_type.py index 89f18e4f9..4f23528d2 100644 --- a/openstack/block_storage/v3/group_type.py +++ b/openstack/block_storage/v3/group_type.py @@ -10,7 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import exceptions from openstack import resource +from openstack import utils class GroupType(resource.Resource): @@ -31,6 +33,90 @@ class GroupType(resource.Resource): #: The group type description. description = resource.Body("description") #: Contains the specifications for a group type. - group_specs = resource.Body("group_specs", type=dict) + group_specs = resource.Body("group_specs", type=dict, default={}) #: Whether the group type is publicly visible. is_public = resource.Body("is_public", type=bool) + + def fetch_group_specs(self, session): + """Fetch group_specs of the group type. + + These are returned by default if the user has suitable permissions + (i.e. you're an admin) but by default you also need the same + permissions to access this API. That means this function is kind of + useless. However, that is how the API was designed and it is + theoretically possible that people will have modified their policy to + allow this but not the other so we provide this anyway. + + :param session: The session to use for making this request. + :returns: An updated version of this object. + """ + url = utils.urljoin(GroupType.base_path, self.id, 'group_specs') + microversion = self._get_microversion_for(session, 'fetch') + response = session.get(url, microversion=microversion) + exceptions.raise_from_response(response) + specs = response.json().get('group_specs', {}) + self._update(group_specs=specs) + return self + + def create_group_specs(self, session, specs): + """Creates group specs for the group type. + + This will override whatever specs are already present on the group + type. + + :param session: The session to use for making this request. + :param specs: A dict of group specs to set on the group type. + :returns: An updated version of this object. + """ + url = utils.urljoin(GroupType.base_path, self.id, 'group_specs') + microversion = self._get_microversion_for(session, 'create') + response = session.post( + url, json={'group_specs': specs}, microversion=microversion, + ) + exceptions.raise_from_response(response) + specs = response.json().get('group_specs', {}) + self._update(group_specs=specs) + return self + + def get_group_specs_property(self, session, prop): + """Retrieve a group spec property of the group type. + + :param session: The session to use for making this request. + :param prop: The name of the group spec property to update. + :returns: The value of the group spec property. + """ + url = utils.urljoin(GroupType.base_path, self.id, 'group_specs', prop) + microversion = self._get_microversion_for(session, 'fetch') + response = session.get(url, microversion=microversion) + exceptions.raise_from_response(response) + val = response.json().get(prop) + return val + + def update_group_specs_property(self, session, prop, val): + """Update a group spec property of the group type. + + :param session: The session to use for making this request. + :param prop: The name of the group spec property to update. + :param val: The value to set for the group spec property. + :returns: The updated value of the group spec property. + """ + url = utils.urljoin(GroupType.base_path, self.id, 'group_specs', prop) + microversion = self._get_microversion_for(session, 'commit') + response = session.put( + url, json={prop: val}, microversion=microversion + ) + exceptions.raise_from_response(response) + val = response.json().get(prop) + return val + + def delete_group_specs_property(self, session, prop): + """Delete a group spec property from the group type. + + :param session: The session to use for making this request. + :param prop: The name of the group spec property to delete. + :returns: None + """ + url = utils.urljoin(GroupType.base_path, self.id, 'group_specs', prop) + microversion = self._get_microversion_for(session, 'delete') + response = session.delete(url, microversion=microversion) + exceptions.raise_from_response(response) diff --git a/openstack/tests/functional/block_storage/v3/test_group.py b/openstack/tests/functional/block_storage/v3/test_group.py new file mode 100644 index 000000000..33e37ffba --- /dev/null +++ b/openstack/tests/functional/block_storage/v3/test_group.py @@ -0,0 +1,103 @@ +# 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.block_storage.v3 import group_type as _group_type +from openstack.tests.functional.block_storage.v3 import base + + +class TestGroup(base.BaseBlockStorageTest): + # TODO(stephenfin): We should use setUpClass here for MOAR SPEED!!! + def setUp(self): + super().setUp() + + if not self.user_cloud.has_service('block-storage'): + self.skipTest('block-storage service not supported by cloud') + + group_type_name = self.getUniqueString() + self.group_type = self.conn.block_storage.create_group_type( + name=group_type_name, + ) + self.addCleanup( + self.conn.block_storage.delete_group_type, + self.group_type, + ) + self.assertIsInstance(self.group_type, _group_type.GroupType) + self.assertEqual(group_type_name, self.group_type.name) + + def test_group_type(self): + # get + group_type = self.conn.block_storage.get_group_type(self.group_type) + self.assertEqual(self.group_type.name, group_type.name) + + # find + group_type = self.conn.block_storage.find_group_type( + self.group_type.name, + ) + self.assertEqual(self.group_type.id, group_type.id) + + # list + group_types = list(self.conn.block_storage.group_types()) + # other tests may have created group types and there can be defaults so + # we don't assert that this is the *only* group type present + self.assertIn(self.group_type.id, {g.id for g in group_types}) + + # update + group_type_name = self.getUniqueString() + group_type_description = self.getUniqueString() + group_type = self.conn.block_storage.update_group_type( + self.group_type, + name=group_type_name, + description=group_type_description, + ) + self.assertIsInstance(group_type, _group_type.GroupType) + group_type = self.conn.block_storage.get_group_type(self.group_type.id) + self.assertEqual(group_type_name, group_type.name) + self.assertEqual(group_type_description, group_type.description) + + def test_group_type_group_specs(self): + # create + group_type = self.conn.block_storage.create_group_type_group_specs( + self.group_type, + {'foo': 'bar', 'acme': 'buzz'}, + ) + self.assertIsInstance(group_type, _group_type.GroupType) + group_type = self.conn.block_storage.get_group_type(self.group_type.id) + self.assertEqual( + {'foo': 'bar', 'acme': 'buzz'}, group_type.group_specs + ) + + # get + spec = self.conn.block_storage.get_group_type_group_specs_property( + self.group_type, + 'foo', + ) + self.assertEqual('bar', spec) + + # update + spec = self.conn.block_storage.update_group_type_group_specs_property( + self.group_type, + 'foo', + 'baz', + ) + self.assertEqual('baz', spec) + group_type = self.conn.block_storage.get_group_type(self.group_type.id) + self.assertEqual( + {'foo': 'baz', 'acme': 'buzz'}, group_type.group_specs + ) + + # delete + self.conn.block_storage.delete_group_type_group_specs_property( + self.group_type, + 'foo', + ) + group_type = self.conn.block_storage.get_group_type(self.group_type.id) + self.assertEqual({'acme': 'buzz'}, group_type.group_specs) diff --git a/openstack/tests/functional/block_storage/v3/test_group_type.py b/openstack/tests/functional/block_storage/v3/test_group_type.py deleted file mode 100644 index 0ca5252a8..000000000 --- a/openstack/tests/functional/block_storage/v3/test_group_type.py +++ /dev/null @@ -1,39 +0,0 @@ -# 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.block_storage.v3 import group_type as _group_type -from openstack.tests.functional.block_storage.v3 import base - - -class TestGroupType(base.BaseBlockStorageTest): - - def setUp(self): - super(TestGroupType, self).setUp() - - self.GROUP_TYPE_NAME = self.getUniqueString() - self.GROUP_TYPE_ID = None - - group_type = self.conn.block_storage.create_group_type( - name=self.GROUP_TYPE_NAME) - self.assertIsInstance(group_type, _group_type.GroupType) - self.assertEqual(self.GROUP_TYPE_NAME, group_type.name) - self.GROUP_TYPE_ID = group_type.id - - def tearDown(self): - group_type = self.conn.block_storage.delete_group_type( - self.GROUP_TYPE_ID, ignore_missing=False) - self.assertIsNone(group_type) - super(TestGroupType, self).tearDown() - - def test_get(self): - group_type = self.conn.block_storage.get_group_type(self.GROUP_TYPE_ID) - self.assertEqual(self.GROUP_TYPE_NAME, group_type.name) diff --git a/openstack/tests/unit/block_storage/v3/test_group_type.py b/openstack/tests/unit/block_storage/v3/test_group_type.py index 541339dcb..842e4de43 100644 --- a/openstack/tests/unit/block_storage/v3/test_group_type.py +++ b/openstack/tests/unit/block_storage/v3/test_group_type.py @@ -10,6 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. +from unittest import mock + +from keystoneauth1 import adapter + from openstack.block_storage.v3 import group_type from openstack.tests.unit import base @@ -18,13 +22,16 @@ "name": "grp-type-001", "description": "group type 001", "is_public": True, - "group_specs": { - "consistent_group_snapshot_enabled": " False" - } + "group_specs": {"consistent_group_snapshot_enabled": " False"}, } class TestGroupType(base.TestCase): + def setUp(self): + super().setUp() + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.default_microversion = 1 + self.sess._get_connection = mock.Mock(return_value=self.cloud) def test_basic(self): resource = group_type.GroupType() @@ -44,3 +51,93 @@ def test_make_resource(self): self.assertEqual(GROUP_TYPE["description"], resource.description) self.assertEqual(GROUP_TYPE["is_public"], resource.is_public) self.assertEqual(GROUP_TYPE["group_specs"], resource.group_specs) + + def test_fetch_group_specs(self): + sot = group_type.GroupType(**GROUP_TYPE) + resp = mock.Mock() + resp.body = {'group_specs': {'a': 'b', 'c': 'd'}} + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + self.sess.get = mock.Mock(return_value=resp) + + rsp = sot.fetch_group_specs(self.sess) + + self.sess.get.assert_called_with( + f"group_types/{GROUP_TYPE['id']}/group_specs", + microversion=self.sess.default_microversion, + ) + + self.assertEqual(resp.body['group_specs'], rsp.group_specs) + self.assertIsInstance(rsp, group_type.GroupType) + + def test_create_group_specs(self): + sot = group_type.GroupType(**GROUP_TYPE) + specs = {'a': 'b', 'c': 'd'} + resp = mock.Mock() + resp.body = {'group_specs': specs} + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + self.sess.post = mock.Mock(return_value=resp) + + rsp = sot.create_group_specs(self.sess, specs) + + self.sess.post.assert_called_with( + f"group_types/{GROUP_TYPE['id']}/group_specs", + json={'group_specs': specs}, + microversion=self.sess.default_microversion, + ) + + self.assertEqual(resp.body['group_specs'], rsp.group_specs) + self.assertIsInstance(rsp, group_type.GroupType) + + def test_get_group_specs_property(self): + sot = group_type.GroupType(**GROUP_TYPE) + resp = mock.Mock() + resp.body = {'a': 'b'} + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + self.sess.get = mock.Mock(return_value=resp) + + rsp = sot.get_group_specs_property(self.sess, 'a') + + self.sess.get.assert_called_with( + f"group_types/{GROUP_TYPE['id']}/group_specs/a", + microversion=self.sess.default_microversion, + ) + + self.assertEqual('b', rsp) + + def test_update_group_specs_property(self): + sot = group_type.GroupType(**GROUP_TYPE) + resp = mock.Mock() + resp.body = {'a': 'b'} + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + self.sess.put = mock.Mock(return_value=resp) + + rsp = sot.update_group_specs_property(self.sess, 'a', 'b') + + self.sess.put.assert_called_with( + f"group_types/{GROUP_TYPE['id']}/group_specs/a", + json={'a': 'b'}, + microversion=self.sess.default_microversion, + ) + + self.assertEqual('b', rsp) + + def test_delete_group_specs_property(self): + sot = group_type.GroupType(**GROUP_TYPE) + resp = mock.Mock() + resp.body = None + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + self.sess.delete = mock.Mock(return_value=resp) + + rsp = sot.delete_group_specs_property(self.sess, 'a') + + self.sess.delete.assert_called_with( + f"group_types/{GROUP_TYPE['id']}/group_specs/a", + microversion=self.sess.default_microversion, + ) + + self.assertIsNone(rsp) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index f2fa6b324..790f6cc34 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -169,6 +169,47 @@ def test_group_type_delete_ignore(self): def test_group_type_update(self): self.verify_update(self.proxy.update_group_type, group_type.GroupType) + def test_group_type_fetch_group_specs(self): + self._verify( + "openstack.block_storage.v3.group_type.GroupType.fetch_group_specs", # noqa: E501 + self.proxy.fetch_group_type_group_specs, + method_args=["value"], + expected_args=[self.proxy], + ) + + def test_group_type_create_group_specs(self): + self._verify( + "openstack.block_storage.v3.group_type.GroupType.create_group_specs", # noqa: E501 + self.proxy.create_group_type_group_specs, + method_args=["value", {'a': 'b'}], + expected_args=[self.proxy], + expected_kwargs={"specs": {'a': 'b'}}, + ) + + def test_group_type_get_group_specs_prop(self): + self._verify( + "openstack.block_storage.v3.group_type.GroupType.get_group_specs_property", # noqa: E501 + self.proxy.get_group_type_group_specs_property, + method_args=["value", "prop"], + expected_args=[self.proxy, "prop"], + ) + + def test_group_type_update_group_specs_prop(self): + self._verify( + "openstack.block_storage.v3.group_type.GroupType.update_group_specs_property", # noqa: E501 + self.proxy.update_group_type_group_specs_property, + method_args=["value", "prop", "val"], + expected_args=[self.proxy, "prop", "val"], + ) + + def test_group_type_delete_group_specs_prop(self): + self._verify( + "openstack.block_storage.v3.group_type.GroupType.delete_group_specs_property", # noqa: E501 + self.proxy.delete_group_type_group_specs_property, + method_args=["value", "prop"], + expected_args=[self.proxy, "prop"], + ) + class TestExtension(TestVolumeProxy): def test_extensions(self): diff --git a/releasenotes/notes/add-block-storage-group-type-group-specs-d07047167224ec83.yaml b/releasenotes/notes/add-block-storage-group-type-group-specs-d07047167224ec83.yaml new file mode 100644 index 000000000..50ae352a0 --- /dev/null +++ b/releasenotes/notes/add-block-storage-group-type-group-specs-d07047167224ec83.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add support for creating, updating and deleting group type group specs for + the block storage service. From 819ccc8bc5ed222518e8b30824fc86da6dc0a06a Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 24 Jun 2022 12:57:53 +0100 Subject: [PATCH 3066/3836] tests: Add pointers to docs on running tests I forgot how to run functional tests despite having recently worked on documentation for this myself. Add a pointer to the docs for the test directories. Change-Id: I5f6ef29f4148b17227f44f4056afc7eb058cdc6b Signed-off-by: Stephen Finucane --- openstack/tests/README.rst | 7 +++++++ openstack/tests/functional/README.rst | 7 +++++++ openstack/tests/unit/README.rst | 7 +++++++ 3 files changed, 21 insertions(+) create mode 100644 openstack/tests/README.rst create mode 100644 openstack/tests/functional/README.rst create mode 100644 openstack/tests/unit/README.rst diff --git a/openstack/tests/README.rst b/openstack/tests/README.rst new file mode 100644 index 000000000..388a40138 --- /dev/null +++ b/openstack/tests/README.rst @@ -0,0 +1,7 @@ +Tests for openstacksdk +====================== + +For information on how to run and extend these tests, refer to the `contributor +guide`__. + +.. __: https://docs.openstack.org/openstacksdk/latest/contributor/testing.html diff --git a/openstack/tests/functional/README.rst b/openstack/tests/functional/README.rst new file mode 100644 index 000000000..a9bbf05c0 --- /dev/null +++ b/openstack/tests/functional/README.rst @@ -0,0 +1,7 @@ +Unit Tests for openstacksdk +=========================== + +For information on how to run and extend these tests, refer to the `contributor +guide`__. + +.. __: https://docs.openstack.org/openstacksdk/latest/contributor/testing.html diff --git a/openstack/tests/unit/README.rst b/openstack/tests/unit/README.rst new file mode 100644 index 000000000..a9bbf05c0 --- /dev/null +++ b/openstack/tests/unit/README.rst @@ -0,0 +1,7 @@ +Unit Tests for openstacksdk +=========================== + +For information on how to run and extend these tests, refer to the `contributor +guide`__. + +.. __: https://docs.openstack.org/openstacksdk/latest/contributor/testing.html From 0efe8cfc9061c37e059922ab3555434540f9360a Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 5 Nov 2021 09:07:49 +0100 Subject: [PATCH 3067/3836] Disable deprecation warning for tenant_id Neutron clients are still refering to tenant_id in very many places and the warning become very intruisive. Disable them untill we ourselves get rid of those usages in OSC and other similar places. Change-Id: Iaf5b467cf42a53b60e14085b0c0cb86d9ede3310 --- openstack/resource.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openstack/resource.py b/openstack/resource.py index 5d9c246b5..05dec43fa 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -184,7 +184,14 @@ def __get__(self, instance, owner): if value is None: return None - self.warn_if_deprecated_property(value) + # This warning are pretty intruisive. Every time attribute is accessed + # a warning is being thrown. In Neutron clients we have way too many + # places that still refer to tenant_id even they may also properly + # support project_id. For now we can silence tenant_id warnings and do + # this here rather then addining support for something similar to + # "suppress_deprecation_warning". + if self.name != "tenant_id": + self.warn_if_deprecated_property(value) return _convert_type(value, self.type, self.list_type) def warn_if_deprecated_property(self, value): From 5f79a1141307b31f702b7f012901c3bee88ce7a3 Mon Sep 17 00:00:00 2001 From: Rafael Castillo Date: Mon, 30 May 2022 15:00:51 -0700 Subject: [PATCH 3068/3836] Allow unknown attributes in project resources Keystone allows setting custom properties in the project resources. This patch enables interacting with this functionality by enabling _allow_unknown_attrs_in_body. Change-Id: I414c68cf0deebd7936bd548a737971dbc44857d3 --- openstack/identity/v3/project.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openstack/identity/v3/project.py b/openstack/identity/v3/project.py index 66f9a3ffb..f90664a9e 100644 --- a/openstack/identity/v3/project.py +++ b/openstack/identity/v3/project.py @@ -27,6 +27,8 @@ class Project(resource.Resource, tag.TagMixin): allow_list = True commit_method = 'PATCH' + _allow_unknown_attrs_in_body = True + _query_mapping = resource.QueryParameters( 'domain_id', 'is_domain', From 70a44339ac400be8a05baa5f2ff2bda238945566 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 19 May 2022 13:29:30 +0100 Subject: [PATCH 3069/3836] compute: Add support for os-simple-tenant-usages API https://docs.openstack.org/api-ref/compute/#list-tenant-usage-statistics-for-all-tenants Change-Id: Id941a224b2b70617e7639148e9e4e25176726f93 Signed-off-by: Stephen Finucane --- openstack/compute/v2/_proxy.py | 61 ++++++++--- openstack/compute/v2/usage.py | 102 ++++++++++++++++++ openstack/tests/unit/compute/v2/test_proxy.py | 39 +++++++ openstack/tests/unit/compute/v2/test_usage.py | 99 +++++++++++++++++ 4 files changed, 289 insertions(+), 12 deletions(-) create mode 100644 openstack/compute/v2/usage.py create mode 100644 openstack/tests/unit/compute/v2/test_usage.py diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index f1e6de584..f9a56cf50 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -31,6 +31,7 @@ from openstack.compute.v2 import server_migration as _server_migration from openstack.compute.v2 import server_remote_console as _src from openstack.compute.v2 import service as _service +from openstack.compute.v2 import usage as _usage from openstack.compute.v2 import volume_attachment as _volume_attachment from openstack import exceptions from openstack.identity.v3 import project as _project @@ -609,10 +610,8 @@ def get_limits(self, **query): :class:`~openstack.compute.v2.limits.RateLimits` :rtype: :class:`~openstack.compute.v2.limits.Limits` """ - res = self._get_resource( - limits.Limits, None) - return res.fetch( - self, **query) + res = self._get_resource(limits.Limits, None) + return res.fetch(self, **query) # ========== Servers ========== @@ -1865,6 +1864,46 @@ def get_server_diagnostics(self, server): return self._get(_server_diagnostics.ServerDiagnostics, server_id=server_id, requires_id=False) + # ========== Project usage ============ + + def usages(self, start=None, end=None, **query): + """Get project usages. + + :param datetime.datetime start: Usage range start date. + :param datetime.datetime end: Usage range end date. + :param dict query: Additional query parameters to use. + :returns: A list of compute ``Usage`` objects. + """ + if start is not None: + query['start'] = start + + if end is not None: + query['end'] = end + + return self._list(_usage.Usage, **query) + + def get_usage(self, project, start=None, end=None, **query): + """Get usage for a single project. + + :param project: ID or instance of + :class:`~openstack.identity.project.Project` of the project for + which the usage should be retrieved. + :param datetime.datetime start: Usage range start date. + :param datetime.datetime end: Usage range end date. + :param dict query: Additional query parameters to use. + :returns: A compute ``Usage`` object. + """ + project = self._get_resource(_project.Project, project) + + if start is not None: + query['start'] = start + + if end is not None: + query['end'] = end + + res = self._get_resource(_usage.Usage, project.id) + return res.fetch(self, **query) + # ========== Server consoles ========== def create_server_remote_console(self, server, **attrs): @@ -1953,11 +1992,9 @@ def get_quota_set(self, project, usage=False, **query): """ project = self._get_resource(_project.Project, project) res = self._get_resource( - _quota_set.QuotaSet, None, project_id=project.id) - if not query: - query = {} - return res.fetch( - self, usage=usage, **query) + _quota_set.QuotaSet, None, project_id=project.id, + ) + return res.fetch(self, usage=usage, **query) def get_quota_set_defaults(self, project): """Show QuotaSet defaults for the project @@ -1972,9 +2009,9 @@ def get_quota_set_defaults(self, project): """ project = self._get_resource(_project.Project, project) res = self._get_resource( - _quota_set.QuotaSet, None, project_id=project.id) - return res.fetch( - self, base_path='/os-quota-sets/defaults') + _quota_set.QuotaSet, None, project_id=project.id, + ) + return res.fetch(self, base_path='/os-quota-sets/defaults') def revert_quota_set(self, project, **query): """Reset Quota for the project/user. diff --git a/openstack/compute/v2/usage.py b/openstack/compute/v2/usage.py new file mode 100644 index 000000000..0d20f6de0 --- /dev/null +++ b/openstack/compute/v2/usage.py @@ -0,0 +1,102 @@ +# 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 resource + + +class ServerUsage(resource.Resource): + resource_key = None + resources_key = None + + # Capabilities + allow_create = False + allow_fetch = False + allow_delete = False + allow_list = False + allow_commit = False + + # Properties + #: The duration that the server exists (in hours). + hours = resource.Body('hours') + #: The display name of a flavor. + flavor = resource.Body('flavor') + #: The UUID of the server. + instance_id = resource.Body('instance_id') + #: The server name. + name = resource.Body('name') + #: The UUID of the project in a multi-tenancy cloud. + project_id = resource.Body('tenant_id') + #: The memory size of the server (in MiB). + memory_mb = resource.Body('memory_mb') + #: The sum of the root disk size of the server and the ephemeral disk size + #: of it (in GiB). + local_gb = resource.Body('local_gb') + #: The number of virtual CPUs that the server uses. + vcpus = resource.Body('vcpus') + #: The date and time when the server was launched. + started_at = resource.Body('started_at') + #: The date and time when the server was deleted. + ended_at = resource.Body('ended_at') + #: The VM state. + state = resource.Body('state') + #: The uptime of the server. + uptime = resource.Body('uptime') + + +class Usage(resource.Resource): + resource_key = 'tenant_usage' + resources_key = 'tenant_usages' + base_path = '/os-simple-tenant-usage' + + # Capabilities + allow_create = False + allow_fetch = True + allow_delete = False + allow_list = True + allow_commit = False + + # TODO(stephenfin): Add 'start', 'end'. These conflict with the body + # responses though. + _query_mapping = resource.QueryParameters( + "detailed", + "limit", + "marker", + "start", + "end", + ) + + # Properties + #: The UUID of the project in a multi-tenancy cloud. + project_id = resource.Body('tenant_id') + #: A list of the server usage objects. + server_usages = resource.Body( + 'server_usages', type=list, list_type=ServerUsage, + ) + #: Multiplying the server disk size (in GiB) by hours the server exists, + #: and then adding that all together for each server. + total_local_gb_usage = resource.Body('total_local_gb_usage') + #: Multiplying the number of virtual CPUs of the server by hours the server + #: exists, and then adding that all together for each server. + total_vcpus_usage = resource.Body('total_vcpus_usage') + #: Multiplying the server memory size (in MiB) by hours the server exists, + #: and then adding that all together for each server. + total_memory_mb_usage = resource.Body('total_memory_mb_usage') + #: The total duration that servers exist (in hours). + total_hours = resource.Body('total_hours') + #: The beginning time to calculate usage statistics on compute and storage + #: resources. + start = resource.Body('start') + #: The ending time to calculate usage statistics on compute and storage + #: resources. + stop = resource.Body('stop') + + _max_microversion = '2.75' diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index bf697cff6..4f6a4f60c 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import datetime from unittest import mock from openstack.compute.v2 import _proxy @@ -30,6 +31,7 @@ from openstack.compute.v2 import server_migration from openstack.compute.v2 import server_remote_console from openstack.compute.v2 import service +from openstack.compute.v2 import usage from openstack import resource from openstack.tests.unit import test_proxy_base @@ -1105,6 +1107,43 @@ def test_remove_security_groups(self): method_args=["value", {'id': 'id', 'name': 'sg'}], expected_args=[self.proxy, 'sg']) + def test_usages(self): + self.verify_list(self.proxy.usages, usage.Usage) + + def test_usages__with_kwargs(self): + now = datetime.datetime.utcnow() + start = now - datetime.timedelta(weeks=4) + end = end = now + datetime.timedelta(days=1) + self.verify_list( + self.proxy.usages, + usage.Usage, + method_kwargs={'start': start, 'end': end}, + expected_kwargs={'start': start, 'end': end}, + ) + + def test_get_usage(self): + self._verify( + "openstack.compute.v2.usage.Usage.fetch", + self.proxy.get_usage, + method_args=['value'], + method_kwargs={}, + expected_args=[self.proxy], + expected_kwargs={}, + ) + + def test_get_usage__with_kwargs(self): + now = datetime.datetime.utcnow() + start = now - datetime.timedelta(weeks=4) + end = end = now + datetime.timedelta(days=1) + self._verify( + "openstack.compute.v2.usage.Usage.fetch", + self.proxy.get_usage, + method_args=['value'], + method_kwargs={'start': start, 'end': end}, + expected_args=[self.proxy], + expected_kwargs={'start': start, 'end': end}, + ) + def test_create_server_remote_console(self): self.verify_create( self.proxy.create_server_remote_console, diff --git a/openstack/tests/unit/compute/v2/test_usage.py b/openstack/tests/unit/compute/v2/test_usage.py new file mode 100644 index 000000000..512bd53f0 --- /dev/null +++ b/openstack/tests/unit/compute/v2/test_usage.py @@ -0,0 +1,99 @@ +# 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.compute.v2 import usage +from openstack.tests.unit import base + + +EXAMPLE = { + "tenant_id": "781c9299e68d4b7c80ef52712889647f", + "server_usages": [ + { + "hours": 79.51840531333333, + "flavor": "m1.tiny", + "instance_id": "76638c30-d199-4c2e-8154-7dea963bfe2f", + "name": "test-server", + "tenant_id": "781c9299e68d4b7c80ef52712889647f", + "memory_mb": 512, + "local_gb": 1, + "vcpus": 1, + "started_at": "2022-05-16T10:35:31.000000", + "ended_at": None, + "state": "active", + "uptime": 286266, + } + ], + "total_local_gb_usage": 79.51840531333333, + "total_vcpus_usage": 79.51840531333333, + "total_memory_mb_usage": 40713.423520426666, + "total_hours": 79.51840531333333, + "start": "2022-04-21T18:06:47.064959", + "stop": "2022-05-19T18:06:37.259128", +} + + +class TestUsage(base.TestCase): + def test_basic(self): + sot = usage.Usage() + self.assertEqual('tenant_usage', sot.resource_key) + self.assertEqual('tenant_usages', sot.resources_key) + self.assertEqual('/os-simple-tenant-usage', sot.base_path) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = usage.Usage(**EXAMPLE) + + self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertEqual( + EXAMPLE['total_local_gb_usage'], + sot.total_local_gb_usage, + ) + self.assertEqual(EXAMPLE['total_vcpus_usage'], sot.total_vcpus_usage) + self.assertEqual( + EXAMPLE['total_memory_mb_usage'], + sot.total_memory_mb_usage, + ) + self.assertEqual(EXAMPLE['total_hours'], sot.total_hours) + self.assertEqual(EXAMPLE['start'], sot.start) + self.assertEqual(EXAMPLE['stop'], sot.stop) + + # now do the embedded objects + self.assertIsInstance(sot.server_usages, list) + self.assertEqual(1, len(sot.server_usages)) + + ssot = sot.server_usages[0] + self.assertIsInstance(ssot, usage.ServerUsage) + self.assertEqual(EXAMPLE['server_usages'][0]['hours'], ssot.hours) + self.assertEqual(EXAMPLE['server_usages'][0]['flavor'], ssot.flavor) + self.assertEqual( + EXAMPLE['server_usages'][0]['instance_id'], ssot.instance_id + ) + self.assertEqual(EXAMPLE['server_usages'][0]['name'], ssot.name) + self.assertEqual( + EXAMPLE['server_usages'][0]['tenant_id'], ssot.project_id + ) + self.assertEqual( + EXAMPLE['server_usages'][0]['memory_mb'], ssot.memory_mb + ) + self.assertEqual( + EXAMPLE['server_usages'][0]['local_gb'], ssot.local_gb + ) + self.assertEqual(EXAMPLE['server_usages'][0]['vcpus'], ssot.vcpus) + self.assertEqual( + EXAMPLE['server_usages'][0]['started_at'], ssot.started_at + ) + self.assertEqual( + EXAMPLE['server_usages'][0]['ended_at'], ssot.ended_at + ) + self.assertEqual(EXAMPLE['server_usages'][0]['state'], ssot.state) + self.assertEqual(EXAMPLE['server_usages'][0]['uptime'], ssot.uptime) From 71a8466f0ff1042367967336665a9d276679bb66 Mon Sep 17 00:00:00 2001 From: Dylan Zapzalka Date: Mon, 22 Mar 2021 00:57:35 -0500 Subject: [PATCH 3070/3836] block storage: Add support for the Group resource Introduce the Group resource, fill in its resources, and implement API calls to support the Cinder v3 API. Change-Id: Ied48b46eb76dfe6cbafa3f08ac8f5bfe78af4058 --- doc/source/user/proxies/block_storage_v3.rst | 8 ++ openstack/block_storage/v3/_proxy.py | 117 +++++++++++++++ openstack/block_storage/v3/group.py | 89 ++++++++++++ openstack/block_storage/v3/volume.py | 2 + .../functional/block_storage/v3/test_group.py | 60 +++++++- .../tests/unit/block_storage/v3/test_group.py | 135 ++++++++++++++++++ .../tests/unit/block_storage/v3/test_proxy.py | 38 +++++ ...block-storage-groups-bf5f1af714c9e505.yaml | 4 + 8 files changed, 448 insertions(+), 5 deletions(-) create mode 100644 openstack/block_storage/v3/group.py create mode 100644 openstack/tests/unit/block_storage/v3/test_group.py create mode 100644 releasenotes/notes/add-block-storage-groups-bf5f1af714c9e505.yaml diff --git a/doc/source/user/proxies/block_storage_v3.rst b/doc/source/user/proxies/block_storage_v3.rst index 73ff27cfd..5fc796549 100644 --- a/doc/source/user/proxies/block_storage_v3.rst +++ b/doc/source/user/proxies/block_storage_v3.rst @@ -63,6 +63,14 @@ Capabilities Operations :noindex: :members: get_capabilities +Group Operations +^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v3._proxy.Proxy + :noindex: + :members: create_group, create_group_from_source, delete_group, update_group, + get_group, find_group, groups, reset_group_state + Group Type Operations ^^^^^^^^^^^^^^^^^^^^^ diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 5570f8188..bbdb1dbb4 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -15,6 +15,7 @@ from openstack.block_storage.v3 import backup as _backup from openstack.block_storage.v3 import capabilities as _capabilities from openstack.block_storage.v3 import extension as _extension +from openstack.block_storage.v3 import group as _group from openstack.block_storage.v3 import group_type as _group_type from openstack.block_storage.v3 import limits as _limits from openstack.block_storage.v3 import quota_set as _quota_set @@ -977,6 +978,121 @@ def get_capabilities(self, host): """ return self._get(_capabilities.Capabilities, host) + # ====== GROUPS ====== + def get_group(self, group_id, **attrs): + """Get a group + + :param group_id: The ID of the group to get. + :param dict attrs: Optional query parameters to be sent to limit the + resources being returned. + + :returns: A Group instance. + :rtype: :class:`~openstack.block_storage.v3.group` + """ + return self._get(_group.Group, group_id, **attrs) + + def find_group(self, name_or_id, ignore_missing=True, **attrs): + """Find a single group + + :param name_or_id: The name or ID of a group. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the group snapshot does not exist. + + :returns: One :class:`~openstack.block_storage.v3.group.Group` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._find( + _group.Group, name_or_id, ignore_missing=ignore_missing) + + def groups(self, details=True, **query): + """Retrieve a generator of groups + + :param bool details: When set to ``False``, no additional details will + be returned. The default, ``True``, will cause additional details + to be returned. + :param dict query: Optional query parameters to be sent to limit the + resources being returned: + + * all_tenants: Shows details for all project. + * sort: Comma-separated list of sort keys and optional sort + directions. + * limit: Returns a number of items up to the limit value. + * offset: Used in conjunction with limit to return a slice of + items. Specifies where to start in the list. + * marker: The ID of the last-seen item. + * list_volume: Show volume ids in this group. + * detailed: If True, will list groups with details. + * search_opts: Search options. + + :returns: A generator of group objects. + """ + base_path = '/groups/detail' if details else '/groups' + return self._list(_group.Group, base_path=base_path, **query) + + def create_group(self, **attrs): + """Create a new group from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.block_storage.v3.group.Group` comprised of + the properties on the Group class. + + :returns: The results of group creation. + :rtype: :class:`~openstack.block_storage.v3.group.Group`. + """ + return self._create(_group.Group, **attrs) + + def create_group_from_source(self, **attrs): + """Creates a new group from source + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.block_storage.v3.group.Group` comprised of + the properties on the Group class. + + :returns: The results of group creation. + :rtype: :class:`~openstack.block_storage.v3.group.Group`. + """ + return _group.Group.create_from_source(self, **attrs) + + def reset_group_state(self, group, status): + """Reset group status + + :param group: The :class:`~openstack.block_storage.v3.group.Group` + to set the state. + :param status: The status for a group. + + :returns: ``None`` + """ + res = self._get_resource(_group.Group, group) + return res.reset_status(self, status) + + def delete_group(self, group, delete_volumes=False): + """Delete a group + + :param group: The :class:`~openstack.block_storage.v3.group.Group` to + delete. + :param bool delete_volumes: When set to ``True``, volumes in group + will be deleted. + + :returns: ``None``. + """ + res = self._get_resource(_group.Group, group) + res.delete(self, delete_volumes=delete_volumes) + + def update_group(self, group, **attrs): + """Update a group + + :param group: The value can be the ID of a group or a + :class:`~openstack.block_storage.v3.group.Group` instance. + :param dict attrs: The attributes to update on the group. + + :returns: The updated group + :rtype: :class:`~openstack.volume.v3.group.Group` + """ + return self._update(_group.Group, group, **attrs) + + # ====== AVAILABILITY ZONES ====== def availability_zones(self): """Return a generator of availability zones @@ -987,6 +1103,7 @@ def availability_zones(self): return self._list(availability_zone.AvailabilityZone) + # ====== GROUP TYPE ====== def get_group_type(self, group_type): """Get a specific group type diff --git a/openstack/block_storage/v3/group.py b/openstack/block_storage/v3/group.py new file mode 100644 index 000000000..b2f54e21c --- /dev/null +++ b/openstack/block_storage/v3/group.py @@ -0,0 +1,89 @@ +# 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 import resource +from openstack import utils + + +class Group(resource.Resource): + resource_key = "group" + resources_key = "groups" + base_path = "/groups" + + # capabilities + allow_fetch = True + allow_create = True + allow_delete = True + allow_commit = True + allow_list = True + + availability_zone = resource.Body("availability_zone") + created_at = resource.Body("created_at") + description = resource.Body("description") + group_snapshot_id = resource.Body("group_snapshot_id") + group_type = resource.Body("group_type") + project_id = resource.Body("project_id") + replication_status = resource.Body("replication_status") + source_group_id = resource.Body("source_group_id") + status = resource.Body("status") + volumes = resource.Body("volumes", type=list) + volume_types = resource.Body("volume_types", type=list) + + _max_microversion = "3.38" + + def _action(self, session, body): + """Preform group actions given the message body.""" + session = self._get_session(session) + microversion = self._get_microversion_for(session, 'create') + url = utils.urljoin(self.base_path, self.id, 'action') + response = session.post(url, json=body, microversion=microversion) + exceptions.raise_from_response(response) + return response + + def delete(self, session, *, delete_volumes=False): + """Delete a group.""" + body = {'delete': {'delete-volumes': delete_volumes}} + self._action(session, body) + + def reset(self, session, status): + """Resets the status for a group.""" + body = {'reset_status': {'status': status}} + self._action(session, body) + + @classmethod + def create_from_source( + cls, + session, + group_snapshot_id, + source_group_id, + name=None, + description=None, + ): + """Creates a new group from source.""" + session = cls._get_session(session) + microversion = cls._get_microversion_for(cls, session, 'create') + url = utils.urljoin(cls.base_path, 'action') + body = { + 'create-from-src': { + 'name': name, + 'description': description, + 'group_snapshot_id': group_snapshot_id, + 'source_group_id': source_group_id, + } + } + response = session.post(url, json=body, microversion=microversion) + exceptions.raise_from_response(response) + + group = Group() + group._translate_response(response=response) + return group diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index 034198b64..e735fa066 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -49,6 +49,8 @@ class Volume(resource.Resource, metadata.MetadataMixin): #: Extended replication status on this volume. extended_replication_status = resource.Body( "os-volume-replication:extended_status") + #: The ID of the group that the volume belongs to. + group_id = resource.Body("group_id") #: The volume's current back-end. host = resource.Body("os-vol-host-attr:host") #: The ID of the image from which you want to create the volume. diff --git a/openstack/tests/functional/block_storage/v3/test_group.py b/openstack/tests/functional/block_storage/v3/test_group.py index 33e37ffba..a3929d610 100644 --- a/openstack/tests/functional/block_storage/v3/test_group.py +++ b/openstack/tests/functional/block_storage/v3/test_group.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.block_storage.v3 import group as _group from openstack.block_storage.v3 import group_type as _group_type from openstack.tests.functional.block_storage.v3 import base @@ -22,20 +23,41 @@ def setUp(self): if not self.user_cloud.has_service('block-storage'): self.skipTest('block-storage service not supported by cloud') + # there will always be at least one volume type, i.e. the default one + volume_types = list(self.conn.block_storage.types()) + self.volume_type = volume_types[0] + group_type_name = self.getUniqueString() self.group_type = self.conn.block_storage.create_group_type( name=group_type_name, ) - self.addCleanup( - self.conn.block_storage.delete_group_type, - self.group_type, - ) self.assertIsInstance(self.group_type, _group_type.GroupType) self.assertEqual(group_type_name, self.group_type.name) + group_name = self.getUniqueString() + self.group = self.conn.block_storage.create_group( + name=group_name, + group_type=self.group_type.id, + volume_types=[self.volume_type.id], + ) + self.assertIsInstance(self.group, _group.Group) + self.assertEqual(group_name, self.group.name) + + def tearDown(self): + # we do this in tearDown rather than via 'addCleanup' since we need to + # wait for the deletion of the group before moving onto the deletion of + # the group type + self.conn.block_storage.delete_group(self.group, delete_volumes=True) + self.conn.block_storage.wait_for_delete(self.group) + + self.conn.block_storage.delete_group_type(self.group_type) + self.conn.block_storage.wait_for_delete(self.group_type) + + super().tearDown() + def test_group_type(self): # get - group_type = self.conn.block_storage.get_group_type(self.group_type) + group_type = self.conn.block_storage.get_group_type(self.group_type.id) self.assertEqual(self.group_type.name, group_type.name) # find @@ -101,3 +123,31 @@ def test_group_type_group_specs(self): ) group_type = self.conn.block_storage.get_group_type(self.group_type.id) self.assertEqual({'acme': 'buzz'}, group_type.group_specs) + + def test_group(self): + # get + group = self.conn.block_storage.get_group(self.group.id) + self.assertEqual(self.group.name, group.name) + + # find + group = self.conn.block_storage.find_group(self.group.name) + self.assertEqual(self.group.id, group.id) + + # list + groups = self.conn.block_storage.groups() + # other tests may have created groups and there can be defaults so we + # don't assert that this is the *only* group present + self.assertIn(self.group.id, {g.id for g in groups}) + + # update + group_name = self.getUniqueString() + group_description = self.getUniqueString() + group = self.conn.block_storage.update_group( + self.group, + name=group_name, + description=group_description, + ) + self.assertIsInstance(group, _group.Group) + group = self.conn.block_storage.get_group(self.group.id) + self.assertEqual(group_name, group.name) + self.assertEqual(group_description, group.description) diff --git a/openstack/tests/unit/block_storage/v3/test_group.py b/openstack/tests/unit/block_storage/v3/test_group.py new file mode 100644 index 000000000..f8cf11a4d --- /dev/null +++ b/openstack/tests/unit/block_storage/v3/test_group.py @@ -0,0 +1,135 @@ +# 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 +from unittest import mock + +from keystoneauth1 import adapter + +from openstack.block_storage.v3 import group +from openstack.tests.unit import base + +GROUP_ID = "6f519a48-3183-46cf-a32f-41815f813986" + +GROUP = { + "id": GROUP_ID, + "status": "available", + "availability_zone": "az1", + "created_at": "2015-09-16T09:28:52.000000", + "name": "first_group", + "description": "my first group", + "group_type": "29514915-5208-46ab-9ece-1cc4688ad0c1", + "volume_types": ["c4daaf47-c530-4901-b28e-f5f0a359c4e6"], + "volumes": ["a2cdf1ad-5497-4e57-bd7d-f573768f3d03"], + "group_snapshot_id": None, + "source_group_id": None, + "project_id": "7ccf4863071f44aeb8f141f65780c51b" +} + + +class TestGroup(base.TestCase): + + def test_basic(self): + resource = group.Group() + self.assertEqual("group", resource.resource_key) + self.assertEqual("groups", resource.resources_key) + self.assertEqual("/groups", resource.base_path) + self.assertTrue(resource.allow_create) + self.assertTrue(resource.allow_fetch) + self.assertTrue(resource.allow_delete) + self.assertTrue(resource.allow_commit) + self.assertTrue(resource.allow_list) + + def test_make_resource(self): + resource = group.Group(**GROUP) + self.assertEqual(GROUP["id"], resource.id) + self.assertEqual(GROUP["status"], resource.status) + self.assertEqual( + GROUP["availability_zone"], resource.availability_zone) + self.assertEqual(GROUP["created_at"], resource.created_at) + self.assertEqual(GROUP["name"], resource.name) + self.assertEqual(GROUP["description"], resource.description) + self.assertEqual(GROUP["group_type"], resource.group_type) + self.assertEqual(GROUP["volume_types"], resource.volume_types) + self.assertEqual(GROUP["volumes"], resource.volumes) + self.assertEqual( + GROUP["group_snapshot_id"], resource.group_snapshot_id) + self.assertEqual(GROUP["source_group_id"], resource.source_group_id) + self.assertEqual(GROUP["project_id"], resource.project_id) + + +class TestGroupAction(base.TestCase): + + def setUp(self): + super().setUp() + self.resp = mock.Mock() + self.resp.body = None + self.resp.json = mock.Mock(return_value=self.resp.body) + self.resp.headers = {} + self.resp.status_code = 202 + + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.get = mock.Mock() + self.sess.post = mock.Mock(return_value=self.resp) + self.sess.default_microversion = '3.38' + + def test_delete(self): + sot = group.Group(**GROUP) + + self.assertIsNone(sot.delete(self.sess)) + + url = 'groups/%s/action' % GROUP_ID + body = {'delete': {'delete-volumes': False}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) + + def test_reset(self): + sot = group.Group(**GROUP) + + self.assertIsNone(sot.reset(self.sess, 'new_status')) + + url = 'groups/%s/action' % GROUP_ID + body = {'reset_status': {'status': 'new_status'}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion, + ) + + def test_create_from_source(self): + resp = mock.Mock() + resp.body = {'group': copy.deepcopy(GROUP)} + resp.json = mock.Mock(return_value=resp.body) + resp.headers = {} + resp.status_code = 202 + + self.sess.post = mock.Mock(return_value=resp) + + sot = group.Group.create_from_source( + self.sess, + group_snapshot_id='9a591346-e595-4bc1-94e7-08f264406b63', + source_group_id='6c5259f6-42ed-4e41-8ffe-e1c667ae9dff', + name='group_from_source', + description='a group from source', + ) + self.assertIsNotNone(sot) + + url = 'groups/action' + body = { + 'create-from-src': { + 'name': 'group_from_source', + 'description': 'a group from source', + 'group_snapshot_id': '9a591346-e595-4bc1-94e7-08f264406b63', + 'source_group_id': '6c5259f6-42ed-4e41-8ffe-e1c667ae9dff', + }, + } + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion, + ) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 790f6cc34..2ff15b8fc 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -16,6 +16,7 @@ from openstack.block_storage.v3 import backup from openstack.block_storage.v3 import capabilities from openstack.block_storage.v3 import extension +from openstack.block_storage.v3 import group from openstack.block_storage.v3 import group_type from openstack.block_storage.v3 import limits from openstack.block_storage.v3 import quota_set @@ -143,6 +144,43 @@ def test_resource_filters(self): ) +class TestGroup(TestVolumeProxy): + def test_group_get(self): + self.verify_get(self.proxy.get_group, group.Group) + + def test_group_find(self): + self.verify_find(self.proxy.find_group, group.Group) + + def test_groups(self): + self.verify_list(self.proxy.groups, group.Group) + + def test_group_create(self): + self.verify_create(self.proxy.create_group, group.Group) + + def test_group_create_from_source(self): + self._verify( + "openstack.block_storage.v3.group.Group.create_from_source", + self.proxy.create_group_from_source, + method_args=[], + expected_args=[self.proxy], + ) + + def test_group_delete(self): + self._verify( + "openstack.block_storage.v3.group.Group.delete", + self.proxy.delete_group, + method_args=['delete_volumes'], + expected_args=[self.proxy], + expected_kwargs={'delete_volumes': False}, + ) + + def test_group_update(self): + self.verify_update(self.proxy.update_group, group.Group) + + def reset_group_state(self): + self._verify(self.proxy.reset_group_state, group.Group) + + class TestGroupType(TestVolumeProxy): def test_group_type_get(self): self.verify_get(self.proxy.get_group_type, group_type.GroupType) diff --git a/releasenotes/notes/add-block-storage-groups-bf5f1af714c9e505.yaml b/releasenotes/notes/add-block-storage-groups-bf5f1af714c9e505.yaml new file mode 100644 index 000000000..2f24f1812 --- /dev/null +++ b/releasenotes/notes/add-block-storage-groups-bf5f1af714c9e505.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add support for groups to the block storage service. From b554e17d5ae934076758342108894b9c802a8571 Mon Sep 17 00:00:00 2001 From: Jakob Meng Date: Fri, 15 Jul 2022 10:56:15 +0200 Subject: [PATCH 3071/3836] Reduce list_router_interfaces() to necessary API calls Change-Id: I162dee557f7c96dfc62ada4ef28f984b7a70b34a --- openstack/cloud/_network.py | 36 +++++------ openstack/tests/unit/cloud/test_router.py | 77 ++++++++--------------- 2 files changed, 43 insertions(+), 70 deletions(-) diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index 8e7cf9f69..7358490a7 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -1933,26 +1933,22 @@ def list_router_interfaces(self, router, interface_type=None): :returns: A list of network ``Port`` objects. """ # Find only router interface and gateway ports, ignore L3 HA ports etc. - router_interfaces = self.search_ports(filters={ - 'device_id': router['id'], - 'device_owner': 'network:router_interface'} - ) + self.search_ports(filters={ - 'device_id': router['id'], - 'device_owner': 'network:router_interface_distributed'} - ) + self.search_ports(filters={ - 'device_id': router['id'], - 'device_owner': 'network:ha_router_replicated_interface'}) - router_gateways = self.search_ports(filters={ - 'device_id': router['id'], - 'device_owner': 'network:router_gateway'}) - ports = router_interfaces + router_gateways - - if interface_type: - if interface_type == 'internal': - return router_interfaces - if interface_type == 'external': - return router_gateways - return ports + ports = list(self.network.ports(device_id=router['id'])) + + router_interfaces = ( + [port for port in ports + if (port['device_owner'] in + ['network:router_interface', + 'network:router_interface_distributed', + 'network:ha_router_replicated_interface']) + ] if not interface_type or interface_type == 'internal' else []) + + router_gateways = ( + [port for port in ports + if port['device_owner'] == 'network:router_gateway' + ] if not interface_type or interface_type == 'external' else []) + + return router_interfaces + router_gateways def create_router( self, diff --git a/openstack/tests/unit/cloud/test_router.py b/openstack/tests/unit/cloud/test_router.py index 0b0c7fae9..6f4fd4c67 100644 --- a/openstack/tests/unit/cloud/test_router.py +++ b/openstack/tests/unit/cloud/test_router.py @@ -366,33 +366,23 @@ def test_delete_router_multiple_found(self): 'mickey') self.assert_calls() - def _get_mock_dict(self, owner, json): - return dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports'], - qs_elements=["device_id=%s" % self.router_id, - "device_owner=network:%s" % owner]), - json=json) - def _test_list_router_interfaces(self, router, interface_type, - router_type="normal", expected_result=None): - if router_type == "normal": - device_owner = 'router_interface' - elif router_type == "ha": - device_owner = 'ha_router_replicated_interface' - elif router_type == "dvr": - device_owner = 'router_interface_distributed' - internal_port = { - 'id': 'internal_port_id', - 'fixed_ips': [{ - 'subnet_id': 'internal_subnet_id', - 'ip_address': "10.0.0.1" - }], - 'device_id': self.router_id, - 'device_owner': 'network:%s' % device_owner - } - external_port = { + internal_ports = [ + { + 'id': 'internal_port_id', + 'fixed_ips': [{ + 'subnet_id': 'internal_subnet_id', + 'ip_address': "10.0.0.1" + }], + 'device_id': self.router_id, + 'device_owner': device_owner + } + for device_owner in ['network:router_interface', + 'network:ha_router_replicated_interface', + 'network:router_interface_distributed']] + + external_ports = [{ 'id': 'external_port_id', 'fixed_ips': [{ 'subnet_id': 'external_subnet_id', @@ -400,28 +390,23 @@ def _test_list_router_interfaces(self, router, interface_type, }], 'device_id': self.router_id, 'device_owner': 'network:router_gateway' - } + }] + if expected_result is None: if interface_type == "internal": - expected_result = [internal_port] + expected_result = internal_ports elif interface_type == "external": - expected_result = [external_port] + expected_result = external_ports else: - expected_result = [internal_port, external_port] - - mock_uris = [] - for port_type in ['router_interface', - 'router_interface_distributed', - 'ha_router_replicated_interface']: - if port_type == device_owner: - ports = {'ports': [internal_port]} - else: - ports = {'ports': []} - mock_uris.append(self._get_mock_dict(port_type, ports)) - mock_uris.append(self._get_mock_dict('router_gateway', - {'ports': [external_port]})) + expected_result = internal_ports + external_ports + + mock_uri = dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports'], + qs_elements=["device_id=%s" % self.router_id]), + json={'ports': (internal_ports + external_ports)}) - self.register_uris(mock_uris) + self.register_uris([mock_uri]) ret = self.cloud.list_router_interfaces(router, interface_type) self.assertEqual( [_port.Port(**i).to_dict(computed=False) for i in expected_result], @@ -449,11 +434,3 @@ def test_list_router_interfaces_internal(self): def test_list_router_interfaces_external(self): self._test_list_router_interfaces(self.router, interface_type="external") - - def test_list_router_interfaces_internal_ha(self): - self._test_list_router_interfaces(self.router, router_type="ha", - interface_type="internal") - - def test_list_router_interfaces_internal_dvr(self): - self._test_list_router_interfaces(self.router, router_type="dvr", - interface_type="internal") From 2b7469cc85c6b1224e57cacd4cc789494b56308d Mon Sep 17 00:00:00 2001 From: Dylan Zapzalka Date: Wed, 17 Mar 2021 15:58:39 -0500 Subject: [PATCH 3072/3836] block storage: Add support for the GroupSnapshot resource Introduce the GroupSnapshot resource, fill in its resources, and implement API calls to support the Cinder v3 API Change-Id: I1189285892d64935912830d22fd2f7e59a797e87 Task: 41855 Story: 2008619 Task: 42074 Story: 2008621 --- doc/source/user/proxies/block_storage_v3.rst | 8 ++ openstack/block_storage/v3/_proxy.py | 88 ++++++++++++++++++- openstack/block_storage/v3/group_snapshot.py | 72 +++++++++++++++ .../functional/block_storage/v3/test_group.py | 64 ++++++++++++++ .../block_storage/v3/test_group_snapshot.py | 51 +++++++++++ .../tests/unit/block_storage/v3/test_proxy.py | 50 +++++++++++ ...rage-group-snapshots-954cc869227317c3.yaml | 4 + 7 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 openstack/block_storage/v3/group_snapshot.py create mode 100644 openstack/tests/unit/block_storage/v3/test_group_snapshot.py create mode 100644 releasenotes/notes/add-block-storage-group-snapshots-954cc869227317c3.yaml diff --git a/doc/source/user/proxies/block_storage_v3.rst b/doc/source/user/proxies/block_storage_v3.rst index 5fc796549..65b5bb919 100644 --- a/doc/source/user/proxies/block_storage_v3.rst +++ b/doc/source/user/proxies/block_storage_v3.rst @@ -71,6 +71,14 @@ Group Operations :members: create_group, create_group_from_source, delete_group, update_group, get_group, find_group, groups, reset_group_state +Group Snapshot Operations +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v3._proxy.Proxy + :noindex: + :members: create_group_snapshot, delete_group_snapshot, get_group_snapshot, + find_group_snapshot, group_snapshots, reset_group_snapshot_state + Group Type Operations ^^^^^^^^^^^^^^^^^^^^^ diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index bbdb1dbb4..0da1d97e9 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -16,6 +16,7 @@ from openstack.block_storage.v3 import capabilities as _capabilities from openstack.block_storage.v3 import extension as _extension from openstack.block_storage.v3 import group as _group +from openstack.block_storage.v3 import group_snapshot as _group_snapshot from openstack.block_storage.v3 import group_type as _group_type from openstack.block_storage.v3 import limits as _limits from openstack.block_storage.v3 import quota_set as _quota_set @@ -1103,6 +1104,91 @@ def availability_zones(self): return self._list(availability_zone.AvailabilityZone) + # ====== GROUP SNAPSHOT ====== + def get_group_snapshot(self, group_snapshot_id): + """Get a group snapshot + + :param group_snapshot_id: The ID of the group snapshot to get. + + :returns: A GroupSnapshot instance. + :rtype: :class:`~openstack.block_storage.v3.group_snapshot` + """ + return self._get(_group_snapshot.GroupSnapshot, group_snapshot_id) + + def find_group_snapshot(self, name_or_id, ignore_missing=True): + """Find a single group snapshot + + :param name_or_id: The name or ID of a group snapshot. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the group snapshot does not exist. + + :returns: One :class:`~openstack.block_storage.v3.group_snapshot` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._find( + _group_snapshot.GroupSnapshot, name_or_id, + ignore_missing=ignore_missing) + + def group_snapshots(self, details=True, **query): + """Retrieve a generator of group snapshots + + :param bool details: When ``True``, returns + :class:`~openstack.block_storage.v3.group_snapshot.GroupSnapshot` + objects with additional attributes filled. + :param kwargs query: Optional query parameters to be sent to limit + the group snapshots being returned. + :returns: A generator of group snapshtos. + """ + base_path = '/group_snapshots' + if details: + base_path = '/group_snapshots/detail' + + return self._list( + _group_snapshot.GroupSnapshot, + base_path=base_path, + **query, + ) + + def create_group_snapshot(self, **attrs): + """Create a group snapshot + + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.block_storage.v3.group_snapshot.GroupSnapshot` + comprised of the properties on the GroupSnapshot class. + + :returns: The results of group snapshot creation. + :rtype: :class:`~openstack.block_storage.v3.group_snapshot`. + """ + return self._create(_group_snapshot.GroupSnapshot, **attrs) + + def reset_group_snapshot_state(self, group_snapshot, state): + """Reset group snapshot status + + :param group_snapshot: The + :class:`~openstack.block_storage.v3.group_snapshot.GroupSnapshot` + to set the state. + :param state: The state of the group snapshot to be set. + + :returns: None + """ + resource = self._get_resource( + _group_snapshot.GroupSnapshot, group_snapshot) + resource.reset_state(self, state) + + def delete_group_snapshot(self, group_snapshot, ignore_missing=True): + """Delete a group snapshot + + :param group_snapshot: The :class:`~openstack.block_storage.v3. + group_snapshot.GroupSnapshot` to delete. + + :returns: None + """ + self._delete( + _group_snapshot.GroupSnapshot, group_snapshot, + ignore_missing=ignore_missing) + # ====== GROUP TYPE ====== def get_group_type(self, group_type): """Get a specific group type @@ -1180,7 +1266,7 @@ def delete_group_type(self, group_type, ignore_missing=True): When set to ``True``, no exception will be set when attempting to delete a nonexistent zone. - :returns: ''None'' + :returns: None """ self._delete( _group_type.GroupType, group_type, ignore_missing=ignore_missing) diff --git a/openstack/block_storage/v3/group_snapshot.py b/openstack/block_storage/v3/group_snapshot.py new file mode 100644 index 000000000..cb72541b5 --- /dev/null +++ b/openstack/block_storage/v3/group_snapshot.py @@ -0,0 +1,72 @@ +# 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 import resource +from openstack import utils + + +class GroupSnapshot(resource.Resource): + resource_key = "group_snapshot" + resources_key = "group_snapshots" + base_path = "/group_snapshots" + + # capabilities + allow_fetch = True + allow_create = True + allow_delete = True + allow_commit = False + allow_list = True + + #: Properties + #: The date and time when the resource was created. + created_at = resource.Body("created_at") + #: The group snapshot description. + description = resource.Body("description") + #: The UUID of the source group. + group_id = resource.Body("group_id") + #: The group type ID. + group_type_id = resource.Body("group_type_id") + #: The ID of the group snapshot. + id = resource.Body("id") + #: The group snapshot name. + name = resource.Body("name") + #: The UUID of the volume group snapshot project. + project_id = resource.Body("project_id") + #: The status of the generic group snapshot. + status = resource.Body("status") + + # Pagination support was added in microversion 3.29 + _max_microversion = '3.29' + + def _action(self, session, body, microversion=None): + """Preform aggregate actions given the message body.""" + url = utils.urljoin(self.base_path, self.id, 'action') + headers = {'Accept': ''} + # TODO(stephenfin): This logic belongs in openstack.resource I suspect + if microversion is None: + if session.default_microversion: + microversion = session.default_microversion + else: + microversion = utils.maximum_supported_microversion( + session, self._max_microversion, + ) + response = session.post( + url, json=body, headers=headers, microversion=microversion, + ) + exceptions.raise_from_response(response) + return response + + def reset_state(self, session, state): + """Resets the status for a group snapshot.""" + body = {'reset_status': {'status': state}} + return self._action(session, body) diff --git a/openstack/tests/functional/block_storage/v3/test_group.py b/openstack/tests/functional/block_storage/v3/test_group.py index a3929d610..7312641bf 100644 --- a/openstack/tests/functional/block_storage/v3/test_group.py +++ b/openstack/tests/functional/block_storage/v3/test_group.py @@ -11,7 +11,9 @@ # under the License. from openstack.block_storage.v3 import group as _group +from openstack.block_storage.v3 import group_snapshot as _group_snapshot from openstack.block_storage.v3 import group_type as _group_type +from openstack.block_storage.v3 import volume as _volume from openstack.tests.functional.block_storage.v3 import base @@ -151,3 +153,65 @@ def test_group(self): group = self.conn.block_storage.get_group(self.group.id) self.assertEqual(group_name, group.name) self.assertEqual(group_description, group.description) + + def test_group_snapshot(self): + # group snapshots require a volume + # no need for a teardown as the deletion of the group (with the + # 'delete_volumes' flag) will handle this but we do need to wait for + # the thing to be created + volume_name = self.getUniqueString() + self.volume = self.conn.block_storage.create_volume( + name=volume_name, + volume_type=self.volume_type.id, + group_id=self.group.id, + size=1, + ) + self.conn.block_storage.wait_for_status( + self.volume, + status='available', + failures=['error'], + interval=2, + wait=self._wait_for_timeout, + ) + self.assertIsInstance(self.volume, _volume.Volume) + + group_snapshot_name = self.getUniqueString() + self.group_snapshot = self.conn.block_storage.create_group_snapshot( + name=group_snapshot_name, + group_id=self.group.id, + ) + self.conn.block_storage.wait_for_status( + self.group_snapshot, + status='available', + failures=['error'], + interval=2, + wait=self._wait_for_timeout, + ) + self.assertIsInstance( + self.group_snapshot, + _group_snapshot.GroupSnapshot, + ) + + # get + group_snapshot = self.conn.block_storage.get_group_snapshot( + self.group_snapshot.id, + ) + self.assertEqual(self.group_snapshot.name, group_snapshot.name) + + # find + group_snapshot = self.conn.block_storage.find_group_snapshot( + self.group_snapshot.name, + ) + self.assertEqual(self.group_snapshot.id, group_snapshot.id) + + # list + group_snapshots = self.conn.block_storage.group_snapshots() + # other tests may have created group snapshot and there can be defaults + # so we don't assert that this is the *only* group snapshot present + self.assertIn(self.group_snapshot.id, {g.id for g in group_snapshots}) + + # update (not supported) + + # delete + self.conn.block_storage.delete_group_snapshot(self.group_snapshot) + self.conn.block_storage.wait_for_delete(self.group_snapshot) diff --git a/openstack/tests/unit/block_storage/v3/test_group_snapshot.py b/openstack/tests/unit/block_storage/v3/test_group_snapshot.py new file mode 100644 index 000000000..8005bd6da --- /dev/null +++ b/openstack/tests/unit/block_storage/v3/test_group_snapshot.py @@ -0,0 +1,51 @@ +# 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.block_storage.v3 import group_snapshot +from openstack.tests.unit import base + +GROUP_SNAPSHOT = { + "id": "6f519a48-3183-46cf-a32f-41815f813986", + "group_id": "6f519a48-3183-46cf-a32f-41815f814444", + "status": "available", + "created_at": "2015-09-16T09:28:52.000000", + "name": "my_group_snapshot1", + "description": "my first group snapshot", + "group_type_id": "7270c56e-6354-4528-8e8b-f54dee2232c8", + "project_id": "7ccf4863071f44aeb8f141f65780c51b", +} + + +class TestGroupSnapshot(base.TestCase): + def test_basic(self): + resource = group_snapshot.GroupSnapshot() + self.assertEqual("group_snapshot", resource.resource_key) + self.assertEqual("group_snapshots", resource.resources_key) + self.assertEqual("/group_snapshots", resource.base_path) + self.assertTrue(resource.allow_create) + self.assertTrue(resource.allow_fetch) + self.assertTrue(resource.allow_delete) + self.assertTrue(resource.allow_list) + self.assertFalse(resource.allow_commit) + + def test_make_resource(self): + resource = group_snapshot.GroupSnapshot(**GROUP_SNAPSHOT) + self.assertEqual(GROUP_SNAPSHOT["created_at"], resource.created_at) + self.assertEqual(GROUP_SNAPSHOT["description"], resource.description) + self.assertEqual(GROUP_SNAPSHOT["group_id"], resource.group_id) + self.assertEqual( + GROUP_SNAPSHOT["group_type_id"], resource.group_type_id + ) + self.assertEqual(GROUP_SNAPSHOT["id"], resource.id) + self.assertEqual(GROUP_SNAPSHOT["name"], resource.name) + self.assertEqual(GROUP_SNAPSHOT["project_id"], resource.project_id) + self.assertEqual(GROUP_SNAPSHOT["status"], resource.status) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 2ff15b8fc..46fa3b0ff 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -17,6 +17,7 @@ from openstack.block_storage.v3 import capabilities from openstack.block_storage.v3 import extension from openstack.block_storage.v3 import group +from openstack.block_storage.v3 import group_snapshot from openstack.block_storage.v3 import group_type from openstack.block_storage.v3 import limits from openstack.block_storage.v3 import quota_set @@ -181,6 +182,55 @@ def reset_group_state(self): self._verify(self.proxy.reset_group_state, group.Group) +class TestGroupSnapshot(TestVolumeProxy): + def test_group_snapshot_get(self): + self.verify_get( + self.proxy.get_group_snapshot, group_snapshot.GroupSnapshot + ) + + def test_group_snapshot_find(self): + self.verify_find( + self.proxy.find_group_snapshot, group_snapshot.GroupSnapshot + ) + + def test_group_snapshots(self): + self.verify_list( + self.proxy.group_snapshots, + group_snapshot.GroupSnapshot, + expected_kwargs={}, + ) + + def test_group_snapshots__detailed(self): + self.verify_list( + self.proxy.group_snapshots, + group_snapshot.GroupSnapshot, + method_kwargs={'details': True, 'query': 1}, + expected_kwargs={ + 'query': 1, + 'base_path': '/group_snapshots/detail', + }, + ) + + def test_group_snapshot_create(self): + self.verify_create( + self.proxy.create_group_snapshot, group_snapshot.GroupSnapshot + ) + + def test_group_snapshot_delete(self): + self.verify_delete( + self.proxy.delete_group_snapshot, + group_snapshot.GroupSnapshot, + False, + ) + + def test_group_snapshot_delete_ignore(self): + self.verify_delete( + self.proxy.delete_group_snapshot, + group_snapshot.GroupSnapshot, + True, + ) + + class TestGroupType(TestVolumeProxy): def test_group_type_get(self): self.verify_get(self.proxy.get_group_type, group_type.GroupType) diff --git a/releasenotes/notes/add-block-storage-group-snapshots-954cc869227317c3.yaml b/releasenotes/notes/add-block-storage-group-snapshots-954cc869227317c3.yaml new file mode 100644 index 000000000..eb4e0f266 --- /dev/null +++ b/releasenotes/notes/add-block-storage-group-snapshots-954cc869227317c3.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add support for group snapshots to the block storage service. From 1ce15c9a8758b4d978eb5239bae100ddc13c8875 Mon Sep 17 00:00:00 2001 From: Jakob Meng Date: Thu, 21 Jul 2022 17:44:23 +0200 Subject: [PATCH 3073/3836] Allow to pass description parameter to cloud.create_server() Change-Id: I6f8018498ae758e47b6e1b17d3a1994179cc85e2 --- openstack/cloud/_compute.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 530b305e1..d9a85f1ac 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -670,7 +670,7 @@ def get_server_meta(self, server): return dict(server_vars=server_vars, groups=groups) @_utils.valid_kwargs( - 'meta', 'files', 'userdata', + 'meta', 'files', 'userdata', 'description', 'reservation_id', 'return_raw', 'min_count', 'max_count', 'security_groups', 'key_name', 'availability_zone', 'block_device_mapping', From 91459d65a2db2c3ed81df5a270386f017d01a4c2 Mon Sep 17 00:00:00 2001 From: Jakob Meng Date: Wed, 27 Jul 2022 14:57:24 +0200 Subject: [PATCH 3074/3836] Restore functionality to attach multiple floating ips with add_ip_list() Seven years ago, OpenStack did not support attaching multiple floating ips to a server, which is why commit 0085effd40 [1] dropped this functionality from add_ip_list(). This patch restores the previous behaviour, because OpenStack Neutron supports attaching multiple floating ips to a server for a while. [1] https://opendev.org/openstack/openstacksdk/commit/0085effd4024fc8f48f7aa4e41853bcbb1675b4a Change-Id: Ia1fd35559a24ce9d2ae79abe54f3a620752466a4 --- openstack/cloud/_floating_ip.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index 8e91ac9cf..026b5ea6a 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -890,15 +890,17 @@ def add_ip_list( :raises: ``OpenStackCloudException``, on operation error. """ - if type(ips) == list: - ip = ips[0] - else: - ip = ips - f_ip = self.get_floating_ip( - id=None, filters={'floating_ip_address': ip}) - return self._attach_ip_to_server( - server=server, floating_ip=f_ip, wait=wait, timeout=timeout, - fixed_address=fixed_address) + + if type(ips) != list: + ips = [ips] + + for ip in ips: + f_ip = self.get_floating_ip( + id=None, filters={'floating_ip_address': ip}) + server = self._attach_ip_to_server( + server=server, floating_ip=f_ip, wait=wait, timeout=timeout, + fixed_address=fixed_address) + return server def add_auto_ip(self, server, wait=False, timeout=60, reuse=True): """Add a floating IP to a server. From 3f81d0001dd994cde990d38f6e2671ee0694d7d5 Mon Sep 17 00:00:00 2001 From: ljhuang Date: Fri, 22 Jul 2022 11:01:07 +0800 Subject: [PATCH 3075/3836] Replace deprecated failUnlessEqual with assertEqual The failUnlessEqual method alias has been deprecated in unittest since version 3.1[1]. [1] https://docs.python.org/3/library/unittest.html#deprecated-aliases Change-Id: I735615b6a9e35d058900571d34838e95302d41ff --- openstack/tests/functional/cloud/test_endpoints.py | 2 +- openstack/tests/functional/cloud/test_services.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openstack/tests/functional/cloud/test_endpoints.py b/openstack/tests/functional/cloud/test_endpoints.py index f9d432a9d..39a539709 100644 --- a/openstack/tests/functional/cloud/test_endpoints.py +++ b/openstack/tests/functional/cloud/test_endpoints.py @@ -202,5 +202,5 @@ def test_delete_endpoint(self): if e['id'] == endpoint['id']: found = True break - self.failUnlessEqual( + self.assertEqual( False, found, message='new endpoint was not deleted!') diff --git a/openstack/tests/functional/cloud/test_services.py b/openstack/tests/functional/cloud/test_services.py index 604dd7848..8e1036e33 100644 --- a/openstack/tests/functional/cloud/test_services.py +++ b/openstack/tests/functional/cloud/test_services.py @@ -113,7 +113,7 @@ def test_delete_service_by_name(self): if s['id'] == service['id']: found = True break - self.failUnlessEqual(False, found, message='service was not deleted!') + self.assertEqual(False, found, message='service was not deleted!') def test_delete_service_by_id(self): # Test delete by id @@ -126,4 +126,4 @@ def test_delete_service_by_id(self): for s in observed_services: if s['id'] == service['id']: found = True - self.failUnlessEqual(False, found, message='service was not deleted!') + self.assertEqual(False, found, message='service was not deleted!') From 0ded7ac398843b6b1ce46668eb3b45ce02628428 Mon Sep 17 00:00:00 2001 From: Jakob Meng Date: Wed, 3 Aug 2022 11:42:15 +0200 Subject: [PATCH 3076/3836] Enable add_ips_to_server() and _needs_floating_ip() for pristine server resources Previously, both functions [1] and [2] would have to be called with server objects which have been enhanced by add_server_interfaces() [3]. The latter adds 'public_v4' and 'private_v4' attributes to the server objects which the default server resource [4] does not have. Now, we check server's 'addresses' attributes for fixed and floating ip addresses [5] in case the 'public_v4' and 'private_v4' attributes are not available. The checks for fixed vs floating ip addresses are equal to how find_nova_addresses() [6] distinguishes between both ip address types. [1] https://opendev.org/openstack/openstacksdk/src/commit/3f81d0001dd994cde990d38f6e2671ee0694d7d5/openstack/cloud/_floating_ip.py#L979 [2] https://opendev.org/openstack/openstacksdk/src/commit/3f81d0001dd994cde990d38f6e2671ee0694d7d5/openstack/cloud/_floating_ip.py#L997 [3] https://opendev.org/openstack/openstacksdk/src/commit/3f81d0001dd994cde990d38f6e2671ee0694d7d5/openstack/cloud/meta.py#L439 [4] https://opendev.org/openstack/openstacksdk/src/branch/master/openstack/compute/v2/server.py [5] https://opendev.org/openstack/openstacksdk/src/commit/3f81d0001dd994cde990d38f6e2671ee0694d7d5/openstack/compute/v2/server.py#L81 [6] https://opendev.org/openstack/openstacksdk/src/commit/3f81d0001dd994cde990d38f6e2671ee0694d7d5/openstack/cloud/meta.py#L69 Change-Id: I064ed922be440d911fbc492dab8465e8f75236df --- openstack/cloud/_floating_ip.py | 37 ++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index 026b5ea6a..532171577 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -997,34 +997,47 @@ def add_ips_to_server( def _needs_floating_ip(self, server, nat_destination): """Figure out if auto_ip should add a floating ip to this server. - If the server has a public_v4 it does not need a floating ip. + If the server has a floating ip it does not need another one. - If the server does not have a private_v4 it does not need a + If the server does not have a fixed ip address it does not need a floating ip. If self.private then the server does not need a floating ip. - If the cloud runs nova, and the server has a private_v4 and not - a public_v4, then the server needs a floating ip. + If the cloud runs nova, and the server has a private address and not a + public address, then the server needs a floating ip. - If the server has a private_v4 and no public_v4 and the cloud has - a network from which floating IPs come that is connected via a - router to the network from which the private_v4 address came, + If the server has a fixed ip address and no floating ip address and the + cloud has a network from which floating IPs come that is connected via + a router to the network from which the fixed ip address came, then the server needs a floating ip. - If the server has a private_v4 and no public_v4 and the cloud - does not have a network from which floating ips come, or it has + If the server has a fixed ip address and no floating ip address and the + cloud does not have a network from which floating ips come, or it has one but that network is not connected to the network from which - the server's private_v4 address came via a router, then the + the server's fixed ip address came via a router, then the server does not need a floating ip. """ if not self._has_floating_ips(): return False - if server['public_v4']: + if server['addresses'] is None: + # fetch missing server details, e.g. because + # meta.add_server_interfaces() was not called + server = self.compute.get_server(server) + + if server['public_v4'] \ + or any([any([address['OS-EXT-IPS:type'] == 'floating' + for address in addresses]) + for addresses + in (server['addresses'] or {}).values()]): return False - if not server['private_v4']: + if not server['private_v4'] \ + and not any([any([address['OS-EXT-IPS:type'] == 'fixed' + for address in addresses]) + for addresses + in (server['addresses'] or {}).values()]): return False if self.private: From 7dd6aa22f59a0b58e520880185193a83830a96db Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 9 Jun 2022 15:54:02 +0200 Subject: [PATCH 3077/3836] resource: Merge unnecessary separation of logic We had '_get_microversion_for_list' and '_get_microversion_for', the latter of which simply called the former. There's no reason for this distinction and I suspect it's simply there because of legacy reasons. Merge them. Change-Id: I5124fa0005e809891a1fc970c6ccdc5f4a766d33 Signed-off-by: Stephen Finucane --- .../accelerator/v2/accelerator_request.py | 2 +- openstack/baremetal/v1/node.py | 10 +- .../v1/introspection.py | 4 +- openstack/block_storage/v2/backup.py | 2 +- openstack/block_storage/v3/backup.py | 2 +- openstack/block_storage/v3/group.py | 4 +- openstack/block_storage/v3/group_type.py | 10 +- openstack/compute/v2/flavor.py | 10 +- openstack/compute/v2/hypervisor.py | 2 +- openstack/compute/v2/server_group.py | 110 +++++++++++++----- openstack/compute/v2/server_migration.py | 6 +- openstack/dns/v2/zone_export.py | 2 +- openstack/dns/v2/zone_import.py | 2 +- openstack/object_store/v1/info.py | 2 +- openstack/object_store/v1/obj.py | 2 +- openstack/orchestration/v1/stack.py | 4 +- openstack/resource.py | 76 ++++++------ openstack/tests/unit/test_resource.py | 10 +- 18 files changed, 151 insertions(+), 109 deletions(-) diff --git a/openstack/accelerator/v2/accelerator_request.py b/openstack/accelerator/v2/accelerator_request.py index 1be505883..f0c492bc8 100644 --- a/openstack/accelerator/v2/accelerator_request.py +++ b/openstack/accelerator/v2/accelerator_request.py @@ -74,7 +74,7 @@ def patch(self, session, patch=None, prepend_key=True, has_body=True, request = self._prepare_request(prepend_key=prepend_key, base_path=base_path, patch=True) - microversion = self._get_microversion_for(session, 'patch') + microversion = self._get_microversion(session, action='patch') if patch: request.body = self._convert_patch(patch) diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index e67b39d4b..5db579a58 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -768,7 +768,7 @@ def validate(self, session, required=('boot', 'deploy', 'power')): fails for a required interface. """ session = self._get_session(session) - version = self._get_microversion_for(session, 'fetch') + version = self._get_microversion(session, action='fetch') request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'validate') @@ -818,7 +818,7 @@ def unset_maintenance(self, session): def _do_maintenance_action(self, session, verb, body=None): session = self._get_session(session) - version = self._get_microversion_for(session, 'commit') + version = self._get_microversion(session, action='commit') request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'maintenance') response = getattr(session, verb)( @@ -837,7 +837,7 @@ def set_boot_device(self, session, boot_device, persistent=False): reboot """ session = self._get_session(session) - version = self._get_microversion_for(session, 'commit') + version = self._get_microversion(session, action='commit') request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'management', 'boot_device') @@ -1007,7 +1007,7 @@ def call_vendor_passthru(self, session, verb, method, body=None): :returns: The HTTP response. """ session = self._get_session(session) - version = self._get_microversion_for(session, 'commit') + version = self._get_microversion(session, action='commit') request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'vendor_passthru?method={}' .format(method)) @@ -1032,7 +1032,7 @@ def list_vendor_passthru(self, session): :returns: The HTTP response. """ session = self._get_session(session) - version = self._get_microversion_for(session, 'fetch') + version = self._get_microversion(session, action='fetch') request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'vendor_passthru/methods') diff --git a/openstack/baremetal_introspection/v1/introspection.py b/openstack/baremetal_introspection/v1/introspection.py index 1b01c0fd4..06d38239d 100644 --- a/openstack/baremetal_introspection/v1/introspection.py +++ b/openstack/baremetal_introspection/v1/introspection.py @@ -63,7 +63,7 @@ def abort(self, session): session = self._get_session(session) - version = self._get_microversion_for(session, 'delete') + version = self._get_microversion(session, action='delete') request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'abort') response = session.post( @@ -89,7 +89,7 @@ def get_data(self, session, processed=True): """ session = self._get_session(session) - version = (self._get_microversion_for(session, 'fetch') + version = (self._get_microversion(session, action='fetch') if processed else '1.17') request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'data') diff --git a/openstack/block_storage/v2/backup.py b/openstack/block_storage/v2/backup.py index 381d77b65..cc4771b4d 100644 --- a/openstack/block_storage/v2/backup.py +++ b/openstack/block_storage/v2/backup.py @@ -96,7 +96,7 @@ def create(self, session, prepend_key=True, base_path=None, **params): raise exceptions.MethodNotSupported(self, "create") session = self._get_session(session) - microversion = self._get_microversion_for(session, 'create') + microversion = self._get_microversion(session, action='create') requires_id = (self.create_requires_id if self.create_requires_id is not None else self.create_method == 'PUT') diff --git a/openstack/block_storage/v3/backup.py b/openstack/block_storage/v3/backup.py index af6560848..64987183a 100644 --- a/openstack/block_storage/v3/backup.py +++ b/openstack/block_storage/v3/backup.py @@ -106,7 +106,7 @@ def create(self, session, prepend_key=True, base_path=None, **params): raise exceptions.MethodNotSupported(self, "create") session = self._get_session(session) - microversion = self._get_microversion_for(session, 'create') + microversion = self._get_microversion(session, action='create') requires_id = (self.create_requires_id if self.create_requires_id is not None else self.create_method == 'PUT') diff --git a/openstack/block_storage/v3/group.py b/openstack/block_storage/v3/group.py index b2f54e21c..ab7c5c536 100644 --- a/openstack/block_storage/v3/group.py +++ b/openstack/block_storage/v3/group.py @@ -44,7 +44,7 @@ class Group(resource.Resource): def _action(self, session, body): """Preform group actions given the message body.""" session = self._get_session(session) - microversion = self._get_microversion_for(session, 'create') + microversion = self._get_microversion(session, action='create') url = utils.urljoin(self.base_path, self.id, 'action') response = session.post(url, json=body, microversion=microversion) exceptions.raise_from_response(response) @@ -71,7 +71,7 @@ def create_from_source( ): """Creates a new group from source.""" session = cls._get_session(session) - microversion = cls._get_microversion_for(cls, session, 'create') + microversion = cls._get_microversion(session, action='create') url = utils.urljoin(cls.base_path, 'action') body = { 'create-from-src': { diff --git a/openstack/block_storage/v3/group_type.py b/openstack/block_storage/v3/group_type.py index 4f23528d2..3e89c8898 100644 --- a/openstack/block_storage/v3/group_type.py +++ b/openstack/block_storage/v3/group_type.py @@ -51,7 +51,7 @@ def fetch_group_specs(self, session): :returns: An updated version of this object. """ url = utils.urljoin(GroupType.base_path, self.id, 'group_specs') - microversion = self._get_microversion_for(session, 'fetch') + microversion = self._get_microversion(session, action='fetch') response = session.get(url, microversion=microversion) exceptions.raise_from_response(response) specs = response.json().get('group_specs', {}) @@ -69,7 +69,7 @@ def create_group_specs(self, session, specs): :returns: An updated version of this object. """ url = utils.urljoin(GroupType.base_path, self.id, 'group_specs') - microversion = self._get_microversion_for(session, 'create') + microversion = self._get_microversion(session, action='create') response = session.post( url, json={'group_specs': specs}, microversion=microversion, ) @@ -86,7 +86,7 @@ def get_group_specs_property(self, session, prop): :returns: The value of the group spec property. """ url = utils.urljoin(GroupType.base_path, self.id, 'group_specs', prop) - microversion = self._get_microversion_for(session, 'fetch') + microversion = self._get_microversion(session, action='fetch') response = session.get(url, microversion=microversion) exceptions.raise_from_response(response) val = response.json().get(prop) @@ -101,7 +101,7 @@ def update_group_specs_property(self, session, prop, val): :returns: The updated value of the group spec property. """ url = utils.urljoin(GroupType.base_path, self.id, 'group_specs', prop) - microversion = self._get_microversion_for(session, 'commit') + microversion = self._get_microversion(session, action='commit') response = session.put( url, json={prop: val}, microversion=microversion ) @@ -117,6 +117,6 @@ def delete_group_specs_property(self, session, prop): :returns: None """ url = utils.urljoin(GroupType.base_path, self.id, 'group_specs', prop) - microversion = self._get_microversion_for(session, 'delete') + microversion = self._get_microversion(session, action='delete') response = session.delete(url, microversion=microversion) exceptions.raise_from_response(response) diff --git a/openstack/compute/v2/flavor.py b/openstack/compute/v2/flavor.py index baec64bb4..f2ad48386 100644 --- a/openstack/compute/v2/flavor.py +++ b/openstack/compute/v2/flavor.py @@ -145,7 +145,7 @@ def fetch_extra_specs(self, session): before that a separate call is required. """ url = utils.urljoin(Flavor.base_path, self.id, 'os-extra_specs') - microversion = self._get_microversion_for(session, 'fetch') + microversion = self._get_microversion(session, action='fetch') response = session.get(url, microversion=microversion) exceptions.raise_from_response(response) specs = response.json().get('extra_specs', {}) @@ -155,7 +155,7 @@ def fetch_extra_specs(self, session): def create_extra_specs(self, session, specs): """Creates extra specs for a flavor""" url = utils.urljoin(Flavor.base_path, self.id, 'os-extra_specs') - microversion = self._get_microversion_for(session, 'create') + microversion = self._get_microversion(session, action='create') response = session.post( url, json={'extra_specs': specs}, @@ -169,7 +169,7 @@ def get_extra_specs_property(self, session, prop): """Get individual extra_spec property""" url = utils.urljoin(Flavor.base_path, self.id, 'os-extra_specs', prop) - microversion = self._get_microversion_for(session, 'fetch') + microversion = self._get_microversion(session, action='fetch') response = session.get(url, microversion=microversion) exceptions.raise_from_response(response) val = response.json().get(prop) @@ -179,7 +179,7 @@ def update_extra_specs_property(self, session, prop, val): """Update An Extra Spec For A Flavor""" url = utils.urljoin(Flavor.base_path, self.id, 'os-extra_specs', prop) - microversion = self._get_microversion_for(session, 'commit') + microversion = self._get_microversion(session, action='commit') response = session.put( url, json={prop: val}, @@ -192,7 +192,7 @@ def delete_extra_specs_property(self, session, prop): """Delete An Extra Spec For A Flavor""" url = utils.urljoin(Flavor.base_path, self.id, 'os-extra_specs', prop) - microversion = self._get_microversion_for(session, 'delete') + microversion = self._get_microversion(session, action='delete') response = session.delete( url, microversion=microversion) diff --git a/openstack/compute/v2/hypervisor.py b/openstack/compute/v2/hypervisor.py index ae56abc97..34bcba2f3 100644 --- a/openstack/compute/v2/hypervisor.py +++ b/openstack/compute/v2/hypervisor.py @@ -91,7 +91,7 @@ def get_uptime(self, session): raise exceptions.SDKException( 'Hypervisor.get_uptime is not supported anymore') url = utils.urljoin(self.base_path, self.id, 'uptime') - microversion = self._get_microversion_for(session, 'fetch') + microversion = self._get_microversion(session, action='fetch') response = session.get( url, microversion=microversion) self._translate_response(response) diff --git a/openstack/compute/v2/server_group.py b/openstack/compute/v2/server_group.py index f67e7c050..fa70a9557 100644 --- a/openstack/compute/v2/server_group.py +++ b/openstack/compute/v2/server_group.py @@ -54,38 +54,86 @@ class ServerGroup(resource.Resource): #: The user ID who owns the server group user_id = resource.Body('user_id') - def _get_microversion_for(self, session, action): - """Get microversion to use for the given action. + # TODO(stephenfin): It would be nice to have a hookpoint to do this + # microversion-based request manipulation, but we don't have anything like + # that right now + def create(self, session, prepend_key=True, base_path=None, **params): + """Create a remote resource based on this instance. - The base version uses :meth:`_get_microversion_for_list`. - Subclasses can override this method if more complex logic is needed. - - :param session: :class`keystoneauth1.adapter.Adapter` - :param action: One of "fetch", "commit", "create", "delete", "patch". - Unused in the base implementation. - :return: microversion as string or ``None`` + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param prepend_key: A boolean indicating whether the resource_key + should be prepended in a resource creation request. Default to + True. + :param str base_path: Base part of the URI for creating resources, if + different from :data:`~openstack.resource.Resource.base_path`. + :param dict params: Additional params to pass. + :return: This :class:`Resource` instance. + :raises: :exc:`~openstack.exceptions.MethodNotSupported` if + :data:`Resource.allow_create` is not set to ``True``. """ - if action not in ('fetch', 'commit', 'create', 'delete', 'patch'): - raise ValueError('Invalid action: %s' % action) + if not self.allow_create: + raise exceptions.MethodNotSupported(self, 'create') + + session = self._get_session(session) + microversion = self._get_microversion(session, action='create') + requires_id = ( + self.create_requires_id + if self.create_requires_id is not None + else self.create_method == 'PUT' + ) + + if self.create_exclude_id_from_body: + self._body._dirty.discard("id") + + # `policy` and `rules` are added with mv=2.64. In it also + # `policies` are removed. + if utils.supports_microversion(session, '2.64'): + if self.policies: + if not self.policy and isinstance(self.policies, list): + self.policy = self.policies[0] + self._body.clean(only={'policies'}) + microversion = self._max_microversion + else: # microversion < 2.64 + if self.rules: + msg = ( + "API version 2.64 is required to set rules, but " + "it is not available." + ) + raise exceptions.NotSupported(msg) + + if self.policy: + if not self.policies: + self.policies = [self.policy] + self._body.clean(only={'policy'}) - microversion = self._get_microversion_for_list(session) - if action == 'create': - # `policy` and `rules` are added with mv=2.64. In it also - # `policies` are removed. - if utils.supports_microversion(session, '2.64'): - if self.policies: - if not self.policy and isinstance(self.policies, list): - self.policy = self.policies[0] - self._body.clean(only={'policies'}) - microversion = self._max_microversion - else: - if self.rules: - message = ("API version %s is required to set rules, but " - "it is not available.") % 2.64 - raise exceptions.NotSupported(message) - if self.policy: - if not self.policies: - self.policies = [self.policy] - self._body.clean(only={'policy'}) + if self.create_method == 'POST': + request = self._prepare_request( + requires_id=requires_id, + prepend_key=prepend_key, + base_path=base_path, + ) + response = session.post( + request.url, + json=request.body, + headers=request.headers, + microversion=microversion, + params=params, + ) + else: + raise exceptions.ResourceFailure( + "Invalid create method: %s" % self.create_method + ) - return microversion + has_body = ( + self.has_body + if self.create_returns_body is None + else self.create_returns_body + ) + self.microversion = microversion + self._translate_response(response, has_body=has_body) + # direct comparision to False since we need to rule out None + if self.has_body and self.create_returns_body is False: + # fetch the body if it's required but not returned by create + return self.fetch(session) + return self diff --git a/openstack/compute/v2/server_migration.py b/openstack/compute/v2/server_migration.py index f10f9e163..eb89de1c8 100644 --- a/openstack/compute/v2/server_migration.py +++ b/openstack/compute/v2/server_migration.py @@ -74,14 +74,10 @@ class ServerMigration(resource.Resource): _max_microversion = '2.80' - @classmethod - def _get_microversion_for_action(cls, session): - return cls._get_microversion_for_list(session) - def _action(self, session, body): """Preform server migration actions given the message body.""" session = self._get_session(session) - microversion = self._get_microversion_for_list(session) + microversion = self._get_microversion(session, action='list') url = utils.urljoin( self.base_path % {'server_uuid': self.server_id}, diff --git a/openstack/dns/v2/zone_export.py b/openstack/dns/v2/zone_export.py index d1320e186..325051200 100644 --- a/openstack/dns/v2/zone_export.py +++ b/openstack/dns/v2/zone_export.py @@ -71,7 +71,7 @@ def create(self, session, prepend_key=True, base_path=None): raise exceptions.MethodNotSupported(self, "create") session = self._get_session(session) - microversion = self._get_microversion_for(session, 'create') + microversion = self._get_microversion(session, action='create') # Create ZoneExport requires empty body # skip _prepare_request completely, since we need just empty body request = resource._Request( diff --git a/openstack/dns/v2/zone_import.py b/openstack/dns/v2/zone_import.py index 8e0ce8cc0..497832ffe 100644 --- a/openstack/dns/v2/zone_import.py +++ b/openstack/dns/v2/zone_import.py @@ -71,7 +71,7 @@ def create(self, session, prepend_key=True, base_path=None): raise exceptions.MethodNotSupported(self, "create") session = self._get_session(session) - microversion = self._get_microversion_for(session, 'create') + microversion = self._get_microversion(session, action='create') # Create ZoneImport requires empty body and 'text/dns' as content-type # skip _prepare_request completely, since we need just empty body request = resource._Request( diff --git a/openstack/object_store/v1/info.py b/openstack/object_store/v1/info.py index 3ae24e5b2..76e740540 100644 --- a/openstack/object_store/v1/info.py +++ b/openstack/object_store/v1/info.py @@ -66,7 +66,7 @@ def fetch( url = "{scheme}://{netloc}/info".format( scheme=endpoint.scheme, netloc=endpoint.netloc) - microversion = self._get_microversion_for(session, 'fetch') + microversion = self._get_microversion(session, action='fetch') response = session.get(url, microversion=microversion) kwargs = {} if error_message: diff --git a/openstack/object_store/v1/obj.py b/openstack/object_store/v1/obj.py index 567e02094..ad0050989 100644 --- a/openstack/object_store/v1/obj.py +++ b/openstack/object_store/v1/obj.py @@ -323,7 +323,7 @@ def _raw_delete(self, session): request = self._prepare_request() session = self._get_session(session) - microversion = self._get_microversion_for(session, 'delete') + microversion = self._get_microversion(session, action='delete') if self.is_static_large_object is None: # Fetch metadata to determine SLO flag diff --git a/openstack/orchestration/v1/stack.py b/openstack/orchestration/v1/stack.py index aac18ce35..511df404d 100644 --- a/openstack/orchestration/v1/stack.py +++ b/openstack/orchestration/v1/stack.py @@ -138,7 +138,7 @@ def update(self, session, preview=False): requires_id=False, base_path=base_path) - microversion = self._get_microversion_for(session, 'commit') + microversion = self._get_microversion(session, action='commit') request_url = request.url if preview: @@ -177,7 +177,7 @@ def fetch(self, session, requires_id=True, request = self._prepare_request(requires_id=requires_id, base_path=base_path) # session = self._get_session(session) - microversion = self._get_microversion_for(session, 'fetch') + microversion = self._get_microversion(session, action='fetch') # NOTE(gtema): would be nice to simply use QueryParameters, however # Heat return 302 with parameters being set into URL and requests diff --git a/openstack/resource.py b/openstack/resource.py index ab2c4458c..12b9532d7 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1300,8 +1300,8 @@ def _get_session(cls, session): ) @classmethod - def _get_microversion_for_list(cls, session): - """Get microversion to use when listing resources. + def _get_microversion(cls, session, *, action): + """Get microversion to use for the given action. The base version uses the following logic: @@ -1313,9 +1313,22 @@ def _get_microversion_for_list(cls, session): Subclasses can override this method if more complex logic is needed. - :param session: :class`keystoneauth1.adapter.Adapter` - :return: microversion as string or ``None`` + :param session: The session to use for making the request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param action: One of "fetch", "commit", "create", "delete", "patch". + :type action: str + :return: Microversion as string or ``None`` """ + if action not in { + 'list', + 'fetch', + 'commit', + 'create', + 'delete', + 'patch', + }: + raise ValueError('Invalid action: %s' % action) + if session.default_microversion: return session.default_microversion @@ -1323,22 +1336,6 @@ def _get_microversion_for_list(cls, session): session, cls._max_microversion ) - def _get_microversion_for(self, session, action): - """Get microversion to use for the given action. - - The base version uses :meth:`_get_microversion_for_list`. - Subclasses can override this method if more complex logic is needed. - - :param session: :class`keystoneauth1.adapter.Adapter` - :param action: One of "fetch", "commit", "create", "delete", "patch". - Unused in the base implementation. - :return: microversion as string or ``None`` - """ - if action not in ('fetch', 'commit', 'create', 'delete', 'patch'): - raise ValueError('Invalid action: %s' % action) - - return self._get_microversion_for_list(session) - def _assert_microversion_for( self, session, @@ -1365,7 +1362,7 @@ def _raise(message): raise exceptions.NotSupported(message) - actual = self._get_microversion_for(session, action) + actual = self._get_microversion(session, action=action) if expected is None: return actual @@ -1403,10 +1400,10 @@ def create(self, session, prepend_key=True, base_path=None, **params): :data:`Resource.allow_create` is not set to ``True``. """ if not self.allow_create: - raise exceptions.MethodNotSupported(self, "create") + raise exceptions.MethodNotSupported(self, 'create') session = self._get_session(session) - microversion = self._get_microversion_for(session, 'create') + microversion = self._get_microversion(session, action='create') requires_id = ( self.create_requires_id if self.create_requires_id is not None @@ -1486,7 +1483,7 @@ def bulk_create( :data:`Resource.allow_create` is not set to ``True``. """ if not cls.allow_create: - raise exceptions.MethodNotSupported(cls, "create") + raise exceptions.MethodNotSupported(cls, 'create') if not ( data @@ -1496,7 +1493,7 @@ def bulk_create( raise ValueError('Invalid data passed: %s' % data) session = cls._get_session(session) - microversion = cls._get_microversion_for(cls, session, 'create') + microversion = cls._get_microversion(session, action='create') requires_id = ( cls.create_requires_id if cls.create_requires_id is not None @@ -1591,13 +1588,13 @@ def fetch( the resource was not found. """ if not self.allow_fetch: - raise exceptions.MethodNotSupported(self, "fetch") + raise exceptions.MethodNotSupported(self, 'fetch') request = self._prepare_request( requires_id=requires_id, base_path=base_path ) session = self._get_session(session) - microversion = self._get_microversion_for(session, 'fetch') + microversion = self._get_microversion(session, action='fetch') response = session.get( request.url, microversion=microversion, @@ -1627,12 +1624,12 @@ def head(self, session, base_path=None): was not found. """ if not self.allow_head: - raise exceptions.MethodNotSupported(self, "head") - - request = self._prepare_request(base_path=base_path) + raise exceptions.MethodNotSupported(self, 'head') session = self._get_session(session) - microversion = self._get_microversion_for(session, 'fetch') + microversion = self._get_microversion(session, action='fetch') + + request = self._prepare_request(base_path=base_path) response = session.head(request.url, microversion=microversion) self.microversion = microversion @@ -1681,7 +1678,7 @@ def commit( return self if not self.allow_commit: - raise exceptions.MethodNotSupported(self, "commit") + raise exceptions.MethodNotSupported(self, 'commit') # Avoid providing patch unconditionally to avoid breaking subclasses # without it. @@ -1691,7 +1688,7 @@ def commit( request = self._prepare_request( prepend_key=prepend_key, base_path=base_path, **kwargs ) - microversion = self._get_microversion_for(session, 'commit') + microversion = self._get_microversion(session, action='commit') return self._commit( session, @@ -1808,12 +1805,12 @@ def patch( return self if not self.allow_patch: - raise exceptions.MethodNotSupported(self, "patch") + raise exceptions.MethodNotSupported(self, 'patch') request = self._prepare_request( prepend_key=prepend_key, base_path=base_path, patch=True ) - microversion = self._get_microversion_for(session, 'patch') + microversion = self._get_microversion(session, action='patch') if patch: request.body += self._convert_patch(patch) @@ -1851,11 +1848,11 @@ def delete(self, session, error_message=None, **kwargs): def _raw_delete(self, session, **kwargs): if not self.allow_delete: - raise exceptions.MethodNotSupported(self, "delete") + raise exceptions.MethodNotSupported(self, 'delete') request = self._prepare_request(**kwargs) session = self._get_session(session) - microversion = self._get_microversion_for(session, 'delete') + microversion = self._get_microversion(session, action='delete') return session.delete( request.url, headers=request.headers, microversion=microversion @@ -1903,9 +1900,10 @@ def list( contains invalid params. """ if not cls.allow_list: - raise exceptions.MethodNotSupported(cls, "list") + raise exceptions.MethodNotSupported(cls, 'list') + session = cls._get_session(session) - microversion = cls._get_microversion_for_list(session) + microversion = cls._get_microversion(session, action='list') if base_path is None: base_path = cls.base_path diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 1fd84020f..59693bb2d 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -3086,7 +3086,7 @@ def test_timeout(self): self.cloud.compute, res, 0.1, 0.3) -@mock.patch.object(resource.Resource, '_get_microversion_for', autospec=True) +@mock.patch.object(resource.Resource, '_get_microversion', autospec=True) class TestAssertMicroversionFor(base.TestCase): session = mock.Mock() res = resource.Resource() @@ -3097,7 +3097,7 @@ def test_compatible(self, mock_get_ver): self.assertEqual( '1.42', self.res._assert_microversion_for(self.session, 'fetch', '1.6')) - mock_get_ver.assert_called_once_with(self.res, self.session, 'fetch') + mock_get_ver.assert_called_once_with(self.session, action='fetch') def test_incompatible(self, mock_get_ver): mock_get_ver.return_value = '1.1' @@ -3106,7 +3106,7 @@ def test_incompatible(self, mock_get_ver): '1.6 is required, but 1.1 will be used', self.res._assert_microversion_for, self.session, 'fetch', '1.6') - mock_get_ver.assert_called_once_with(self.res, self.session, 'fetch') + mock_get_ver.assert_called_once_with(self.session, action='fetch') def test_custom_message(self, mock_get_ver): mock_get_ver.return_value = '1.1' @@ -3116,7 +3116,7 @@ def test_custom_message(self, mock_get_ver): self.res._assert_microversion_for, self.session, 'fetch', '1.6', error_message='boom') - mock_get_ver.assert_called_once_with(self.res, self.session, 'fetch') + mock_get_ver.assert_called_once_with(self.session, action='fetch') def test_none(self, mock_get_ver): mock_get_ver.return_value = None @@ -3125,4 +3125,4 @@ def test_none(self, mock_get_ver): '1.6 is required, but the default version', self.res._assert_microversion_for, self.session, 'fetch', '1.6') - mock_get_ver.assert_called_once_with(self.res, self.session, 'fetch') + mock_get_ver.assert_called_once_with(self.session, action='fetch') From 3ada2fb8b093d489e7eb69a338c848453f721650 Mon Sep 17 00:00:00 2001 From: niuke Date: Mon, 15 Aug 2022 11:16:14 +0800 Subject: [PATCH 3078/3836] remove unicode prefix from code Change-Id: I55f1536dec190d2a62afe6f50b2099cbcb6c24ad --- doc/source/conf.py | 6 +++--- releasenotes/source/conf.py | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 43894ebdd..0669681cf 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -48,7 +48,7 @@ master_doc = 'index' # General information about the project. -copyright = u'2017, Various members of the OpenStack Foundation' +copyright = '2017, Various members of the OpenStack Foundation' # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True @@ -79,8 +79,8 @@ latex_documents = [ ('index', 'doc-openstacksdk.tex', - u'OpenStackSDK Documentation', - u'OpenStack Foundation', 'manual'), + 'OpenStackSDK Documentation', + 'OpenStack Foundation', 'manual'), ] # Allow deeper levels of nesting for \begin...\end stanzas diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py index e21e447b5..ead946fab 100644 --- a/releasenotes/source/conf.py +++ b/releasenotes/source/conf.py @@ -59,7 +59,7 @@ master_doc = 'index' # General information about the project. -copyright = u'2017, Various members of the OpenStack Foundation' +copyright = '2017, Various members of the OpenStack Foundation' # Release notes are version independent. # The short X.Y version. @@ -198,8 +198,8 @@ # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'shadeReleaseNotes.tex', - u'Shade Release Notes Documentation', - u'Shade Developers', 'manual'), + 'Shade Release Notes Documentation', + 'Shade Developers', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -229,8 +229,8 @@ # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'shadereleasenotes', - u'shade Release Notes Documentation', - [u'shade Developers'], 1) + 'shade Release Notes Documentation', + ['shade Developers'], 1) ] # If true, show URL addresses after external links. @@ -244,10 +244,10 @@ # dir menu entry, description, category) texinfo_documents = [ ('index', 'shadeReleaseNotes', - u'shade Release Notes Documentation', - u'shade Developers', 'shadeReleaseNotes', - u'A client library for interacting with OpenStack clouds', - u'Miscellaneous'), + 'shade Release Notes Documentation', + 'shade Developers', 'shadeReleaseNotes', + 'A client library for interacting with OpenStack clouds', + 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. From 6bc56b0eb6eeb21569aae72ee8033ac64db54fb9 Mon Sep 17 00:00:00 2001 From: ljhuang Date: Mon, 22 Aug 2022 16:13:58 +0800 Subject: [PATCH 3079/3836] Replace base64.encodestring with encodebytes Base64.encodestring has been deprecated since 3.1 and removed in python 3.9. Replace it with base64.encodebytes from python3.1[1]. [1]https://docs.python.org/3.9/library/base64.html?highlight=deprecated#base64.encodebytes Change-Id: I2cc7287e9fb9f18f536118cc45e5b99b829af3a2 --- openstack/orchestration/util/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/orchestration/util/utils.py b/openstack/orchestration/util/utils.py index 11d2be066..d5e805195 100644 --- a/openstack/orchestration/util/utils.py +++ b/openstack/orchestration/util/utils.py @@ -47,7 +47,7 @@ def read_url_content(url): try: content = content.decode('utf-8') except ValueError: - content = base64.encodestring(content) + content = base64.encodebytes(content) return content From 915da1e5763496faf78136b0c21576a9770c3c28 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Mon, 11 Jul 2022 15:06:44 +0200 Subject: [PATCH 3080/3836] Allow passing explicit microversions to Resource methods Sometimes users may want a specific behavior of a certain microversion rather than just the most recent supported one. For example, Ironic only supports creating nodes directly in the "available" state before 1.11. Change-Id: I2458650a9ce30440b5e29b940eaf1df60239ff32 --- openstack/object_store/v1/obj.py | 5 +- openstack/resource.py | 67 ++++++++++++++++++----- openstack/tests/unit/test_resource.py | 78 +++++++++++++++++++++++++-- 3 files changed, 130 insertions(+), 20 deletions(-) diff --git a/openstack/object_store/v1/obj.py b/openstack/object_store/v1/obj.py index ad0050989..c78fe00c4 100644 --- a/openstack/object_store/v1/obj.py +++ b/openstack/object_store/v1/obj.py @@ -317,13 +317,14 @@ def create(self, session, base_path=None, **params): self._translate_response(response, has_body=False) return self - def _raw_delete(self, session): + def _raw_delete(self, session, microversion=None): if not self.allow_delete: raise exceptions.MethodNotSupported(self, "delete") request = self._prepare_request() session = self._get_session(session) - microversion = self._get_microversion(session, action='delete') + if microversion is None: + microversion = self._get_microversion(session, action='delete') if self.is_static_large_object is None: # Fetch metadata to determine SLO flag diff --git a/openstack/resource.py b/openstack/resource.py index 12b9532d7..7adebd1b7 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1384,7 +1384,15 @@ def _raise(message): return actual - def create(self, session, prepend_key=True, base_path=None, **params): + def create( + self, + session, + prepend_key=True, + base_path=None, + *, + microversion=None, + **params + ): """Create a remote resource based on this instance. :param session: The session to use for making this request. @@ -1394,6 +1402,7 @@ def create(self, session, prepend_key=True, base_path=None, **params): True. :param str base_path: Base part of the URI for creating resources, if different from :data:`~openstack.resource.Resource.base_path`. + :param str microversion: API version to override the negotiated one. :param dict params: Additional params to pass. :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if @@ -1403,7 +1412,8 @@ def create(self, session, prepend_key=True, base_path=None, **params): raise exceptions.MethodNotSupported(self, 'create') session = self._get_session(session) - microversion = self._get_microversion(session, action='create') + if microversion is None: + microversion = self._get_microversion(session, action='create') requires_id = ( self.create_requires_id if self.create_requires_id is not None @@ -1464,6 +1474,8 @@ def bulk_create( data, prepend_key=True, base_path=None, + *, + microversion=None, **params, ): """Create multiple remote resources based on this class and data. @@ -1476,6 +1488,7 @@ def bulk_create( True. :param str base_path: Base part of the URI for creating resources, if different from :data:`~openstack.resource.Resource.base_path`. + :param str microversion: API version to override the negotiated one. :param dict params: Additional params to pass. :return: A generator of :class:`Resource` objects. @@ -1493,7 +1506,8 @@ def bulk_create( raise ValueError('Invalid data passed: %s' % data) session = cls._get_session(session) - microversion = cls._get_microversion(session, action='create') + if microversion is None: + microversion = cls._get_microversion(session, action='create') requires_id = ( cls.create_requires_id if cls.create_requires_id is not None @@ -1566,6 +1580,8 @@ def fetch( base_path=None, error_message=None, skip_cache=False, + *, + microversion=None, **params, ): """Get a remote resource based on this instance. @@ -1580,6 +1596,7 @@ def fetch( requested object does not exist. :param bool skip_cache: A boolean indicating whether optional API cache should be skipped for this invocation. + :param str microversion: API version to override the negotiated one. :param dict params: Additional parameters that can be consumed. :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if @@ -1594,7 +1611,8 @@ def fetch( requires_id=requires_id, base_path=base_path ) session = self._get_session(session) - microversion = self._get_microversion(session, action='fetch') + if microversion is None: + microversion = self._get_microversion(session, action='fetch') response = session.get( request.url, microversion=microversion, @@ -1609,13 +1627,14 @@ def fetch( self._translate_response(response, **kwargs) return self - def head(self, session, base_path=None): + def head(self, session, base_path=None, *, microversion=None): """Get headers from a remote resource based on this instance. :param session: The session to use for making this request. :type session: :class:`~keystoneauth1.adapter.Adapter` :param str base_path: Base part of the URI for fetching resources, if different from :data:`~openstack.resource.Resource.base_path`. + :param str microversion: API version to override the negotiated one. :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if @@ -1627,7 +1646,8 @@ def head(self, session, base_path=None): raise exceptions.MethodNotSupported(self, 'head') session = self._get_session(session) - microversion = self._get_microversion(session, action='fetch') + if microversion is None: + microversion = self._get_microversion(session, action='fetch') request = self._prepare_request(base_path=base_path) response = session.head(request.url, microversion=microversion) @@ -1650,6 +1670,8 @@ def commit( has_body=True, retry_on_conflict=None, base_path=None, + *, + microversion=None, **kwargs, ): """Commit the state of the instance to the remote resource. @@ -1663,6 +1685,7 @@ def commit( CONFLICT (409). Value of ``None`` leaves the `Adapter` defaults. :param str base_path: Base part of the URI for modifying resources, if different from :data:`~openstack.resource.Resource.base_path`. + :param str microversion: API version to override the negotiated one. :param dict kwargs: Parameters that will be passed to _prepare_request() @@ -1688,7 +1711,8 @@ def commit( request = self._prepare_request( prepend_key=prepend_key, base_path=base_path, **kwargs ) - microversion = self._get_microversion(session, action='commit') + if microversion is None: + microversion = self._get_microversion(session, action='commit') return self._commit( session, @@ -1774,6 +1798,8 @@ def patch( has_body=True, retry_on_conflict=None, base_path=None, + *, + microversion=None, ): """Patch the remote resource. @@ -1792,6 +1818,7 @@ def patch( CONFLICT (409). Value of ``None`` leaves the `Adapter` defaults. :param str base_path: Base part of the URI for modifying resources, if different from :data:`~openstack.resource.Resource.base_path`. + :param str microversion: API version to override the negotiated one. :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if @@ -1810,7 +1837,8 @@ def patch( request = self._prepare_request( prepend_key=prepend_key, base_path=base_path, patch=True ) - microversion = self._get_microversion(session, action='patch') + if microversion is None: + microversion = self._get_microversion(session, action='patch') if patch: request.body += self._convert_patch(patch) @@ -1823,11 +1851,13 @@ def patch( retry_on_conflict=retry_on_conflict, ) - def delete(self, session, error_message=None, **kwargs): + def delete(self, session, error_message=None, *, microversion=None, + **kwargs): """Delete the remote resource based on this instance. :param session: The session to use for making this request. :type session: :class:`~keystoneauth1.adapter.Adapter` + :param str microversion: API version to override the negotiated one. :param dict kwargs: Parameters that will be passed to _prepare_request() @@ -1838,7 +1868,8 @@ def delete(self, session, error_message=None, **kwargs): the resource was not found. """ - response = self._raw_delete(session, **kwargs) + response = self._raw_delete(session, microversion=microversion, + **kwargs) kwargs = {} if error_message: kwargs['error_message'] = error_message @@ -1846,13 +1877,14 @@ def delete(self, session, error_message=None, **kwargs): self._translate_response(response, has_body=False, **kwargs) return self - def _raw_delete(self, session, **kwargs): + def _raw_delete(self, session, microversion=None, **kwargs): if not self.allow_delete: raise exceptions.MethodNotSupported(self, 'delete') request = self._prepare_request(**kwargs) session = self._get_session(session) - microversion = self._get_microversion(session, action='delete') + if microversion is None: + microversion = self._get_microversion(session, action='delete') return session.delete( request.url, headers=request.headers, microversion=microversion @@ -1865,6 +1897,8 @@ def list( paginated=True, base_path=None, allow_unknown_params=False, + *, + microversion=None, **params, ): """This method is a generator which yields resource objects. @@ -1884,6 +1918,7 @@ def list( unknown query parameters. This allows getting list of 'filters' and passing everything known to the server. ``False`` will result in validation exception when unknown query parameters are passed. + :param str microversion: API version to override the negotiated one. :param dict params: These keyword arguments are passed through the :meth:`~openstack.resource.QueryParamter._transpose` method to find if any of them match expected query parameters to be sent @@ -1903,7 +1938,8 @@ def list( raise exceptions.MethodNotSupported(cls, 'list') session = cls._get_session(session) - microversion = cls._get_microversion(session, action='list') + if microversion is None: + microversion = cls._get_microversion(session, action='list') if base_path is None: base_path = cls.base_path @@ -2076,6 +2112,8 @@ def find( name_or_id, ignore_missing=True, list_base_path=None, + *, + microversion=None, **params, ): """Find a resource by its name or id. @@ -2090,6 +2128,7 @@ def find( returned when attempting to find a nonexistent resource. :param str list_base_path: base_path to be used when need listing resources. + :param str microversion: API version to override the negotiated one. :param dict params: Any additional parameters to be passed into underlying methods, such as to :meth:`~openstack.resource.Resource.existing` in order to pass on @@ -2108,7 +2147,7 @@ def find( match = cls.existing( id=name_or_id, connection=session._get_connection(), **params ) - return match.fetch(session, **params) + return match.fetch(session, microversion=microversion, **params) except (exceptions.NotFoundException, exceptions.BadRequestException): # NOTE(gtema): There are few places around openstack that return # 400 if we try to GET resource and it doesn't exist. diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 59693bb2d..ac65ab43b 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1512,15 +1512,19 @@ class Test(resource.Resource): def _test_create(self, cls, requires_id=False, prepend_key=False, microversion=None, base_path=None, params=None, - id_marked_dirty=True): + id_marked_dirty=True, explicit_microversion=None): id = "id" if requires_id else None sot = cls(id=id) sot._prepare_request = mock.Mock(return_value=self.request) sot._translate_response = mock.Mock() params = params or {} + kwargs = params.copy() + if explicit_microversion is not None: + kwargs['microversion'] = explicit_microversion + microversion = explicit_microversion result = sot.create(self.session, prepend_key=prepend_key, - base_path=base_path, **params) + base_path=base_path, **kwargs) id_is_dirty = ('id' in sot._body._dirty) self.assertEqual(id_marked_dirty, id_is_dirty) @@ -1575,6 +1579,17 @@ class Test(resource.Resource): self._test_create(Test, requires_id=True, prepend_key=True, microversion='1.42') + def test_put_create_with_explicit_microversion(self): + class Test(resource.Resource): + service = self.service_name + base_path = self.base_path + allow_create = True + create_method = 'PUT' + _max_microversion = '1.99' + + self._test_create(Test, requires_id=True, prepend_key=True, + explicit_microversion='1.42') + def test_put_create_with_params(self): class Test(resource.Resource): service = self.service_name @@ -1663,6 +1678,29 @@ class Test(resource.Resource): sot._translate_response.assert_called_once_with(self.response) self.assertEqual(result, sot) + def test_fetch_with_explicit_microversion(self): + class Test(resource.Resource): + service = self.service_name + base_path = self.base_path + allow_fetch = True + _max_microversion = '1.99' + + sot = Test(id='id') + sot._prepare_request = mock.Mock(return_value=self.request) + sot._translate_response = mock.Mock() + + result = sot.fetch(self.session, microversion='1.42') + + sot._prepare_request.assert_called_once_with( + requires_id=True, base_path=None) + self.session.get.assert_called_once_with( + self.request.url, microversion='1.42', params={}, + skip_cache=False) + + self.assertEqual(sot.microversion, '1.42') + sot._translate_response.assert_called_once_with(self.response) + self.assertEqual(result, sot) + def test_fetch_not_requires_id(self): result = self.sot.fetch(self.session, False) @@ -1739,16 +1777,21 @@ class Test(resource.Resource): def _test_commit(self, commit_method='PUT', prepend_key=True, has_body=True, microversion=None, - commit_args=None, expected_args=None, base_path=None): + commit_args=None, expected_args=None, base_path=None, + explicit_microversion=None): self.sot.commit_method = commit_method # Need to make sot look dirty so we can attempt an update self.sot._body = mock.Mock() self.sot._body.dirty = mock.Mock(return_value={"x": "y"}) + commit_args = commit_args or {} + if explicit_microversion is not None: + commit_args['microversion'] = explicit_microversion + microversion = explicit_microversion self.sot.commit(self.session, prepend_key=prepend_key, has_body=has_body, base_path=base_path, - **(commit_args or {})) + **commit_args) self.sot._prepare_request.assert_called_once_with( prepend_key=prepend_key, base_path=base_path) @@ -1810,6 +1853,10 @@ def test_commit_put_no_retry_on_conflict(self): commit_args={'retry_on_conflict': False}, expected_args={'retriable_status_codes': {503}}) + def test_commit_put_explicit_microversion(self): + self._test_commit(commit_method='PUT', prepend_key=True, has_body=True, + explicit_microversion='1.42') + def test_commit_not_dirty(self): self.sot._body = mock.Mock() self.sot._body.dirty = dict() @@ -1910,6 +1957,29 @@ class Test(resource.Resource): self.response, has_body=False) self.assertEqual(result, sot) + def test_delete_with_explicit_microversion(self): + class Test(resource.Resource): + service = self.service_name + base_path = self.base_path + allow_delete = True + _max_microversion = '1.99' + + sot = Test(id='id') + sot._prepare_request = mock.Mock(return_value=self.request) + sot._translate_response = mock.Mock() + + result = sot.delete(self.session, microversion='1.42') + + sot._prepare_request.assert_called_once_with() + self.session.delete.assert_called_once_with( + self.request.url, + headers='headers', + microversion='1.42') + + sot._translate_response.assert_called_once_with( + self.response, has_body=False) + self.assertEqual(result, sot) + # NOTE: As list returns a generator, testing it requires consuming # the generator. Wrap calls to self.sot.list in a `list` # and then test the results as a list of responses. From 6e1b5e0ac1fe7b3aeb7dd0dd187fa88e6b2d78c9 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Mon, 11 Jul 2022 15:35:37 +0200 Subject: [PATCH 3081/3836] baremetal: rework node creation to be closer to the backend The proxy layer has been simplified to stay closer to the wire. It no longer runs automated cleaning. The initial state of available is implemented via using an old API version. The default state is enroll (like in Ironic). Change-Id: I2894c82d847f8a3dc6bdae1913cb72a1ca4b764b --- openstack/baremetal/v1/_common.py | 1 + openstack/baremetal/v1/_proxy.py | 3 + openstack/baremetal/v1/node.py | 70 ++++++++++--------- openstack/resource.py | 30 +++++--- .../baremetal/test_baremetal_node.py | 36 ++++++---- .../tests/unit/baremetal/v1/test_node.py | 13 +--- .../notes/node-create-027ea99193f344ef.yaml | 9 +++ 7 files changed, 95 insertions(+), 67 deletions(-) create mode 100644 releasenotes/notes/node-create-027ea99193f344ef.yaml diff --git a/openstack/baremetal/v1/_common.py b/openstack/baremetal/v1/_common.py index 7981c95f5..54f1862e7 100644 --- a/openstack/baremetal/v1/_common.py +++ b/openstack/baremetal/v1/_common.py @@ -59,6 +59,7 @@ """Mapping of target power states to expected power states.""" STATE_VERSIONS = { + 'available': '1.1', 'enroll': '1.11', 'manageable': '1.4', } diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 6b61d2e17..5148b2bf4 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -276,6 +276,9 @@ def nodes(self, details=False, **query): def create_node(self, **attrs): """Create a new node from attributes. + See :meth:`~openstack.baremetal.v1.node.Node.create` for an explanation + of the initial provision state. + :param dict attrs: Keyword arguments that will be used to create a :class:`~openstack.baremetal.v1.node.Node`. diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 5db579a58..0a3a1385d 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -257,12 +257,21 @@ def create(self, session, *args, **kwargs): The overridden version is capable of handling the populated ``provision_state`` field of one of three values: ``enroll``, - ``manageable`` or ``available``. The default is currently - ``available``, since it's the only state supported by all API versions. + ``manageable`` or ``available``. If not provided, the server default + is used (``enroll`` in newer versions of Ironic). + + This call does not cause a node to go through automated cleaning. + If you need it, use ``provision_state=manageable`` followed by + a call to :meth:`set_provision_state`. Note that Bare Metal API 1.4 is required for ``manageable`` and 1.11 is required for ``enroll``. + .. warning:: + Using ``provision_state=available`` is only possible with API + versions 1.1 to 1.10 and thus is incompatible with setting any + fields that appeared after 1.11. + :param session: The session to use for making this request. :type session: :class:`~keystoneauth1.adapter.Adapter` @@ -274,44 +283,41 @@ def create(self, session, *args, **kwargs): supported by the server. """ expected_provision_state = self.provision_state - if expected_provision_state is None: - expected_provision_state = 'available' - - if expected_provision_state not in ('enroll', - 'manageable', - 'available'): - raise ValueError( - "Node's provision_state must be one of 'enroll', " - "'manageable' or 'available' for creation, got %s" % - expected_provision_state) session = self._get_session(session) - # Verify that the requested provision state is reachable with the API - # version we are going to use. - try: - expected_version = _common.STATE_VERSIONS[expected_provision_state] - except KeyError: - pass + if expected_provision_state is not None: + # Verify that the requested provision state is reachable with + # the API version we are going to use. + try: + microversion = _common.STATE_VERSIONS[ + expected_provision_state] + except KeyError: + raise ValueError( + "Node's provision_state must be one of %s for creation, " + "got %s" % (', '.join(_common.STATE_VERSIONS), + expected_provision_state)) + else: + error_message = ("Cannot create a node with initial provision " + "state %s" % expected_provision_state) + # Nodes cannot be created as available using new API versions + maximum = ('1.10' if expected_provision_state == 'available' + else None) + microversion = self._assert_microversion_for( + session, 'create', microversion, maximum=maximum, + error_message=error_message, + ) else: - self._assert_microversion_for( - session, 'create', expected_version, - error_message="Cannot create a node with initial provision " - "state %s" % expected_provision_state) + microversion = None # use the base negotiation # Ironic cannot set provision_state itself, so marking it as unchanged self._clean_body_attrs({'provision_state'}) - super(Node, self).create(session, *args, **kwargs) - - if (self.provision_state == 'enroll' - and expected_provision_state != 'enroll'): - self.set_provision_state(session, 'manage', wait=True) - if (self.provision_state == 'manageable' - and expected_provision_state == 'available'): - self.set_provision_state(session, 'provide', wait=True) + super(Node, self).create(session, *args, microversion=microversion, + **kwargs) - if (self.provision_state == 'available' - and expected_provision_state == 'manageable'): + if (expected_provision_state == 'manageable' + and self.provision_state != 'manageable'): + # Manageable is not reachable directly self.set_provision_state(session, 'manage', wait=True) return self diff --git a/openstack/resource.py b/openstack/resource.py index 7adebd1b7..323383a91 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1342,6 +1342,7 @@ def _assert_microversion_for( action, expected, error_message=None, + maximum=None, ): """Enforce that the microversion for action satisfies the requirement. @@ -1350,6 +1351,7 @@ def _assert_microversion_for( :param expected: Expected microversion. :param error_message: Optional error message with details. Will be prepended to the message generated here. + :param maximum: Maximum microversion. :return: resulting microversion as string. :raises: :exc:`~openstack.exceptions.NotSupported` if the version used for the action is lower than the expected one. @@ -1364,23 +1366,29 @@ def _raise(message): actual = self._get_microversion(session, action=action) - if expected is None: - return actual - elif actual is None: + if actual is None: message = ( "API version %s is required, but the default " "version will be used." ) % expected _raise(message) - actual_n = discover.normalize_version_number(actual) - expected_n = discover.normalize_version_number(expected) - if actual_n < expected_n: - message = ( - "API version %(expected)s is required, but %(actual)s " - "will be used." - ) % {'expected': expected, 'actual': actual} - _raise(message) + + if expected is not None: + expected_n = discover.normalize_version_number(expected) + if actual_n < expected_n: + message = ( + "API version %(expected)s is required, but %(actual)s " + "will be used." + ) % {'expected': expected, 'actual': actual} + _raise(message) + if maximum is not None: + maximum_n = discover.normalize_version_number(maximum) + # Assume that if a service supports higher versions, it also + # supports lower ones. Breaks for services that remove old API + # versions (which is not something they should do). + if actual_n > maximum_n: + return maximum return actual diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index aa44c2265..bdd6cd8b6 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -22,7 +22,7 @@ def test_node_create_get_delete(self): node = self.create_node(name='node-name') self.assertEqual(node.name, 'node-name') self.assertEqual(node.driver, 'fake-hardware') - self.assertEqual(node.provision_state, 'available') + self.assertEqual(node.provision_state, 'enroll') self.assertFalse(node.is_maintenance) # NOTE(dtantsur): get_node and find_node only differ in handing missing @@ -50,6 +50,16 @@ def test_node_create_get_delete(self): self.assertRaises(exceptions.ResourceNotFound, self.conn.baremetal.get_node, self.node_id) + def test_node_create_in_available(self): + node = self.create_node(name='node-name', provision_state='available') + self.assertEqual(node.name, 'node-name') + self.assertEqual(node.driver, 'fake-hardware') + self.assertEqual(node.provision_state, 'available') + + self.conn.baremetal.delete_node(node, ignore_missing=False) + self.assertRaises(exceptions.ResourceNotFound, + self.conn.baremetal.get_node, self.node_id) + def test_node_update(self): node = self.create_node(name='node-name', extra={'foo': 'bar'}) node.name = 'new-name' @@ -128,7 +138,7 @@ def test_node_list_update_delete(self): self.create_node(name='node-name', extra={'foo': 'bar'}) node = next(n for n in self.conn.baremetal.nodes(details=True, - provision_state='available', + provision_state='enroll', is_maintenance=False, associated=False) if n.name == 'node-name') @@ -139,7 +149,7 @@ def test_node_list_update_delete(self): self.conn.baremetal.delete_node(node, ignore_missing=False) def test_node_create_in_enroll_provide(self): - node = self.create_node(provision_state='enroll') + node = self.create_node() self.node_id = node.id self.assertEqual(node.driver, 'fake-hardware') @@ -157,7 +167,7 @@ def test_node_create_in_enroll_provide(self): def test_node_create_in_enroll_provide_by_name(self): name = 'node-%d' % random.randint(0, 1000) - node = self.create_node(provision_state='enroll', name=name) + node = self.create_node(name=name) self.node_id = node.id self.assertEqual(node.driver, 'fake-hardware') @@ -297,16 +307,6 @@ def test_retired(self): node = self.create_node() - # Set retired when node state available should fail! - self.assertRaises( - exceptions.ConflictException, - self.conn.baremetal.update_node, node, is_retired=True) - - # Set node state to manageable - self.conn.baremetal.set_node_provision_state(node, 'manage', - wait=True) - self.assertEqual(node.provision_state, 'manageable') - # Set retired without reason node = self.conn.baremetal.update_node(node, is_retired=True) self.assertTrue(node.is_retired) @@ -348,6 +348,14 @@ def test_retired(self): self.assertTrue(node.is_retired) self.assertEqual(reason, node.retired_reason) + def test_retired_in_available(self): + node = self.create_node(provision_state='available') + + # Set retired when node state available should fail! + self.assertRaises( + exceptions.ConflictException, + self.conn.baremetal.update_node, node, is_retired=True) + class TestBareMetalNodeFields(base.BaseBaremetalTest): diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index f977182d2..3d001497b 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -347,6 +347,7 @@ def _change_state(*args, **kwargs): self.session.post.side_effect = _change_state def test_available_old_version(self, mock_prov): + self.node.provision_state = 'available' result = self.node.create(self.session) self.assertIs(result, self.node) self.session.post.assert_called_once_with( @@ -356,24 +357,16 @@ def test_available_old_version(self, mock_prov): self.assertFalse(mock_prov.called) def test_available_new_version(self, mock_prov): - def _change_state(*args, **kwargs): - self.node.provision_state = 'manageable' - self.session.default_microversion = '1.11' self.node.provision_state = 'available' - self.new_state = 'enroll' - mock_prov.side_effect = _change_state result = self.node.create(self.session) self.assertIs(result, self.node) self.session.post.assert_called_once_with( mock.ANY, json={'driver': FAKE['driver']}, - headers=mock.ANY, microversion=self.session.default_microversion, + headers=mock.ANY, microversion='1.10', params={}) - mock_prov.assert_has_calls([ - mock.call(self.node, self.session, 'manage', wait=True), - mock.call(self.node, self.session, 'provide', wait=True) - ]) + mock_prov.assert_not_called() def test_no_enroll_in_old_version(self, mock_prov): self.node.provision_state = 'enroll' diff --git a/releasenotes/notes/node-create-027ea99193f344ef.yaml b/releasenotes/notes/node-create-027ea99193f344ef.yaml new file mode 100644 index 000000000..3f74ff270 --- /dev/null +++ b/releasenotes/notes/node-create-027ea99193f344ef.yaml @@ -0,0 +1,9 @@ +--- +upgrade: + - | + Changes the baremetal ``create_node`` call to be closer to how Ironic + behaves. If no provision state is requested, the default state of the + current microversion is used (which usually means ``enroll``). + If the ``available`` state is requested, the node does not go through + cleaning (it won't work without creating ports), an old API version is + used to achieve this provision state. From 354903d7ddfa4870d59d95cc04c2e5e2b8b667ef Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Tue, 12 Jul 2022 18:07:48 +0200 Subject: [PATCH 3082/3836] Actually use openstacksdk from source in the Bifrost job Depends-On: https://review.opendev.org/c/openstack/bifrost/+/849563 Change-Id: I5bbbb3ffe715331186eb48ffb0640c0e7c3694ce --- .zuul.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index 1e27a101d..4ffe14fdd 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -424,6 +424,12 @@ required-projects: - openstack/openstacksdk +- job: + name: bifrost-integration-openstacksdk-src + parent: bifrost-integration-tinyipa-ubuntu-focal + required-projects: + - openstack/openstacksdk + - job: name: ironic-inspector-tempest-openstacksdk-src parent: ironic-inspector-tempest @@ -474,7 +480,7 @@ # Ironic jobs, non-voting to avoid tight coupling - ironic-inspector-tempest-openstacksdk-src: voting: false - - bifrost-integration-tinyipa-ubuntu-focal: + - bifrost-integration-openstacksdk-src: voting: false - metalsmith-integration-openstacksdk-src: voting: false From 285e8af1a016b5113d562462760522fcf3d98cfa Mon Sep 17 00:00:00 2001 From: Rafael Castillo Date: Tue, 23 Aug 2022 17:48:18 -0700 Subject: [PATCH 3083/3836] Use /volumes/detail endpoint in find_volume proxy method Currently, the block storage proxy will use /volumes/, which results in many fields being empty when querying a volume by name. The details endpoint returns all the fields we want. Change-Id: I88b2da102c6dd26122b83fad7f6230a73f8ce9c8 --- openstack/block_storage/v3/_proxy.py | 3 ++- openstack/tests/unit/block_storage/v3/test_proxy.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index bbdb1dbb4..d8c196787 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -465,7 +465,8 @@ def find_volume(self, name_or_id, ignore_missing=True, **attrs): when no resource can be found. """ return self._find(_volume.Volume, name_or_id, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, + list_base_path='/volumes/detail') def volumes(self, details=True, **query): """Retrieve a generator of volumes diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 2ff15b8fc..de7c8ae58 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -40,7 +40,9 @@ def test_volume_get(self): self.verify_get(self.proxy.get_volume, volume.Volume) def test_volume_find(self): - self.verify_find(self.proxy.find_volume, volume.Volume) + self.verify_find(self.proxy.find_volume, volume.Volume, + expected_kwargs=dict( + list_base_path='/volumes/detail')) def test_volumes_detailed(self): self.verify_list( From c437698769930252b4e819dba0efb3573216ed36 Mon Sep 17 00:00:00 2001 From: Rafael Castillo Date: Wed, 24 Aug 2022 16:00:28 -0700 Subject: [PATCH 3084/3836] Support unknown attributes in resource.__getitem__ task: 46126 Change-Id: I6a7f9b2d058e89ecde921abc04803af25ff00e76 --- openstack/resource.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openstack/resource.py b/openstack/resource.py index 43f53abfe..5da7bd7ce 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -695,6 +695,9 @@ def __getitem__(self, name): DeprecationWarning, ) return getattr(self, attr) + if self._allow_unknown_attrs_in_body: + if name in self._unknown_attrs_in_body: + return self._unknown_attrs_in_body[name] raise KeyError(name) def __delitem__(self, name): From d9b7beff4373f3d35e1e791819265b8c732782c6 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 12 Aug 2022 15:07:49 +0200 Subject: [PATCH 3085/3836] Fix incremental backups handling in project cleanup Cinder refuse to delete backup if there are dependent (incremental) backups. Introduce second iteration over backups to first drop all incremental ones and then all remainings. Story: 2010217 Change-Id: Id4525bbbe11294b53e981e7654056eeb8969343d --- openstack/block_storage/v3/_proxy.py | 63 +++++++++------ .../functional/cloud/test_project_cleanup.py | 81 +++++++++++++++++++ 2 files changed, 120 insertions(+), 24 deletions(-) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index bbdb1dbb4..54b65d73d 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -1397,36 +1397,51 @@ def _get_cleanup_dependencies(self): } } - def _service_cleanup(self, dry_run=True, client_status_queue=None, - identified_resources=None, - filters=None, resource_evaluation_fn=None): - backups = [] - for obj in self.backups(details=False): - need_delete = self._service_cleanup_del_res( - self.delete_backup, - obj, - dry_run=dry_run, - client_status_queue=client_status_queue, - identified_resources=identified_resources, - filters=filters, - resource_evaluation_fn=resource_evaluation_fn) - if not dry_run and need_delete: - backups.append(obj) - - # Before deleting snapshots need to wait for backups to be deleted - for obj in backups: - try: - self.wait_for_delete(obj) - except exceptions.SDKException: - # Well, did our best, still try further - pass + def _service_cleanup( + self, + dry_run=True, + client_status_queue=None, + identified_resources=None, + filters=None, + resource_evaluation_fn=None + ): + # It is not possible to delete backup if there are dependent backups. + # In order to be able to do cleanup those is required to have at least + # 2 iterations (first cleans up backups with has no dependent backups, + # and in 2nd iteration there should be no backups with dependencies + # remaining. + for i in range(1, 2): + backups = [] + for obj in self.backups(details=False): + if ( + (i == 1 and not obj.has_dependent_backups) + or i != 1 + ): + need_delete = self._service_cleanup_del_res( + self.delete_backup, + obj, + dry_run=True, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn) + if not dry_run and need_delete: + backups.append(obj) + + # Before proceeding need to wait for backups to be deleted + for obj in backups: + try: + self.wait_for_delete(obj) + except exceptions.SDKException: + # Well, did our best, still try further + pass snapshots = [] for obj in self.snapshots(details=False): need_delete = self._service_cleanup_del_res( self.delete_snapshot, obj, - dry_run=dry_run, + dry_run=True, client_status_queue=client_status_queue, identified_resources=identified_resources, filters=filters, diff --git a/openstack/tests/functional/cloud/test_project_cleanup.py b/openstack/tests/functional/cloud/test_project_cleanup.py index 1295de87e..9957e57d3 100644 --- a/openstack/tests/functional/cloud/test_project_cleanup.py +++ b/openstack/tests/functional/cloud/test_project_cleanup.py @@ -119,3 +119,84 @@ def test_cleanup(self): # Since we might not have enough privs to drop all nets - ensure # we do not have our known one self.assertNotIn(self.network_name, net_names) + + def test_block_storage_cleanup(self): + if not self.user_cloud.has_service('object-store'): + self.skipTest('Object service is requred, but not available') + + status_queue = queue.Queue() + + vol = self.conn.block_storage.create_volume(name='vol1', size='1') + self.conn.block_storage.wait_for_status(vol) + s1 = self.conn.block_storage.create_snapshot(volume_id=vol.id) + self.conn.block_storage.wait_for_status(s1) + b1 = self.conn.block_storage.create_backup(volume_id=vol.id) + self.conn.block_storage.wait_for_status(b1) + b2 = self.conn.block_storage.create_backup( + volume_id=vol.id, is_incremental=True, snapshot_id=s1.id) + self.conn.block_storage.wait_for_status(b2) + b3 = self.conn.block_storage.create_backup( + volume_id=vol.id, is_incremental=True, snapshot_id=s1.id) + self.conn.block_storage.wait_for_status(b3) + + # First round - check no resources are old enough + self.conn.project_cleanup( + dry_run=True, + wait_timeout=120, + status_queue=status_queue, + filters={'created_at': '2000-01-01'}) + + self.assertTrue(status_queue.empty()) + + # Second round - resource evaluation function return false, ensure + # nothing identified + self.conn.project_cleanup( + dry_run=True, + wait_timeout=120, + status_queue=status_queue, + filters={'created_at': '2200-01-01'}, + resource_evaluation_fn=lambda x, y, z: False) + + self.assertTrue(status_queue.empty()) + + # Third round - filters set too low + self.conn.project_cleanup( + dry_run=True, + wait_timeout=120, + status_queue=status_queue, + filters={'created_at': '2200-01-01'}) + + objects = [] + while not status_queue.empty(): + objects.append(status_queue.get()) + + # At least known networks should be identified + volumes = list(obj.id for obj in objects) + self.assertIn(vol.id, volumes) + + # Fourth round - dry run with no filters, ensure everything identified + self.conn.project_cleanup( + dry_run=True, + wait_timeout=120, + status_queue=status_queue) + + objects = [] + while not status_queue.empty(): + objects.append(status_queue.get()) + + vol_ids = list(obj.id for obj in objects) + self.assertIn(vol.id, vol_ids) + + # Ensure network still exists + vol_check = self.conn.block_storage.get_volume(vol.id) + self.assertEqual(vol.name, vol_check.name) + + # Last round - do a real cleanup + self.conn.project_cleanup( + dry_run=False, + wait_timeout=600, + status_queue=status_queue) + + objects = [] + while not status_queue.empty(): + objects.append(status_queue.get()) From 497a268acd2a866b4180bac9318bc25823c14827 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 12 Aug 2022 20:28:33 +0200 Subject: [PATCH 3086/3836] Implement project cleanup for object-store Implement cleaning up swift object and containers with project cleanup. When server supports - use bulk-deletion. Change-Id: Ica56afb20bbafb03f43c92fac7e92556198b299d --- openstack/object_store/v1/_proxy.py | 79 +++++++++++++++++++ openstack/object_store/v1/obj.py | 5 +- .../functional/cloud/test_project_cleanup.py | 50 ++++++++++++ ...roject-cleanup-swift-f67615e5c3ab8fd8.yaml | 6 ++ 4 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/project-cleanup-swift-f67615e5c3ab8fd8.yaml diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index 83be650f2..5433a45b2 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -1011,3 +1011,82 @@ def _delete_autocreated_image_objects( self.delete_object(obj, ignore_missing=True) deleted = True return deleted + + # ========== Project Cleanup ========== + def _get_cleanup_dependencies(self): + return { + 'object_store': { + 'before': [] + } + } + + def _service_cleanup( + self, + dry_run=True, + client_status_queue=None, + identified_resources=None, + filters=None, + resource_evaluation_fn=None + ): + is_bulk_delete_supported = False + bulk_delete_max_per_request = None + try: + caps = self.get_info() + except exceptions.SDKException: + pass + else: + bulk_delete = caps.swift.get("bulk_delete", {}) + is_bulk_delete_supported = bulk_delete is not None + bulk_delete_max_per_request = bulk_delete.get( + "max_deletes_per_request", 100) + + elements = [] + for cont in self.containers(): + # Iterate over objects inside container + objects_remaining = False + for obj in self.objects(cont): + need_delete = self._service_cleanup_del_res( + self.delete_object, + obj, + dry_run=True, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn) + if need_delete: + if not is_bulk_delete_supported and not dry_run: + self.delete_object(obj, cont) + else: + elements.append(f"{cont.name}/{obj.name}") + if len(elements) >= bulk_delete_max_per_request: + self._bulk_delete(elements, dry_run=dry_run) + elements.clear() + else: + objects_remaining = True + + if len(elements) > 0: + self._bulk_delete(elements, dry_run=dry_run) + elements.clear() + + # Eventually delete container itself + if not objects_remaining: + self._service_cleanup_del_res( + self.delete_container, + cont, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn) + + def _bulk_delete(self, elements, dry_run=False): + data = "\n".join([parse.quote(x) for x in elements]) + if not dry_run: + self.delete( + "?bulk-delete", + data=data, + headers={ + 'Content-Type': 'text/plain', + 'Accept': 'application/json' + } + ) diff --git a/openstack/object_store/v1/obj.py b/openstack/object_store/v1/obj.py index 567e02094..26024cb67 100644 --- a/openstack/object_store/v1/obj.py +++ b/openstack/object_store/v1/obj.py @@ -163,7 +163,10 @@ class Object(_base.BaseResource): timestamp = resource.Header("x-timestamp") #: The date and time that the object was created or the last #: time that the metadata was changed. - last_modified_at = resource.Header("last-modified", alias='_last_modified') + last_modified_at = resource.Header( + "last-modified", + alias='_last_modified', + aka='updated_at') # Headers for PUT and POST requests #: Set to chunked to enable chunked transfer encoding. If used, diff --git a/openstack/tests/functional/cloud/test_project_cleanup.py b/openstack/tests/functional/cloud/test_project_cleanup.py index 9957e57d3..c25346cba 100644 --- a/openstack/tests/functional/cloud/test_project_cleanup.py +++ b/openstack/tests/functional/cloud/test_project_cleanup.py @@ -196,7 +196,57 @@ def test_block_storage_cleanup(self): dry_run=False, wait_timeout=600, status_queue=status_queue) + objects = [] + while not status_queue.empty(): + objects.append(status_queue.get()) + + def test_cleanup_swift(self): + if not self.user_cloud.has_service('object-store'): + self.skipTest('Object service is requred, but not available') + + status_queue = queue.Queue() + self.conn.object_store.create_container('test_cleanup') + for i in range(1, 10): + self.conn.object_store.create_object( + "test_cleanup", f"test{i}", data="test{i}") + + # First round - check no resources are old enough + self.conn.project_cleanup( + dry_run=True, + wait_timeout=120, + status_queue=status_queue, + filters={'updated_at': '2000-01-01'}) + + self.assertTrue(status_queue.empty()) + # Second round - filters set too low + self.conn.project_cleanup( + dry_run=True, + wait_timeout=120, + status_queue=status_queue, + filters={'updated_at': '2200-01-01'}) objects = [] while not status_queue.empty(): objects.append(status_queue.get()) + + # At least known objects should be identified + obj_names = list(obj.name for obj in objects) + self.assertIn('test1', obj_names) + + # Ensure object still exists + obj = self.conn.object_store.get_object( + "test1", "test_cleanup") + self.assertIsNotNone(obj) + + # Last round - do a real cleanup + self.conn.project_cleanup( + dry_run=False, + wait_timeout=600, + status_queue=status_queue) + + objects.clear() + while not status_queue.empty(): + objects.append(status_queue.get()) + self.assertIsNone( + self.conn.get_container('test_container') + ) diff --git a/releasenotes/notes/project-cleanup-swift-f67615e5c3ab8fd8.yaml b/releasenotes/notes/project-cleanup-swift-f67615e5c3ab8fd8.yaml new file mode 100644 index 000000000..8e62029ed --- /dev/null +++ b/releasenotes/notes/project-cleanup-swift-f67615e5c3ab8fd8.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Project cleanup now supports cleaning Swift (object-store). If supported + by the server bulk deletion is used. Currently only filtering based on + updated_at (last_modified) is supported. From c92f260e2238993e6cdc4aad0290f9d9a3dd695e Mon Sep 17 00:00:00 2001 From: Tom Weininger Date: Mon, 7 Feb 2022 12:25:52 +0100 Subject: [PATCH 3087/3836] Add docstring to wait_for_load_balancer() method The API documentation does not show this method because of the missing docstring. Story: 2009809 Task: 44368 Change-Id: Icbbfdd3fa546c6f61331c805640cc3a9c5da1a0b --- doc/source/user/proxies/load_balancer_v2.rst | 2 +- openstack/load_balancer/v2/_proxy.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/doc/source/user/proxies/load_balancer_v2.rst b/doc/source/user/proxies/load_balancer_v2.rst index ab4d887d1..711d80375 100644 --- a/doc/source/user/proxies/load_balancer_v2.rst +++ b/doc/source/user/proxies/load_balancer_v2.rst @@ -17,7 +17,7 @@ Load Balancer Operations :noindex: :members: create_load_balancer, delete_load_balancer, find_load_balancer, get_load_balancer, get_load_balancer_statistics, load_balancers, - update_load_balancer, failover_load_balancer + update_load_balancer, failover_load_balancer, wait_for_load_balancer Listener Operations ^^^^^^^^^^^^^^^^^^^ diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index be713759d..361fee9c4 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -127,6 +127,26 @@ def update_load_balancer(self, load_balancer, **attrs): def wait_for_load_balancer(self, name_or_id, status='ACTIVE', failures=['ERROR'], interval=2, wait=300): + """Wait for load balancer status + + :param name_or_id: The name or ID of the load balancer. + :param status: Desired status. + :param failures: Statuses that would be interpreted as failures. + Default to ['ERROR']. + :type failures: :py:class:`list` + :param interval: Number of seconds to wait between consecutive + checks. Defaults to 2. + :param wait: Maximum number of seconds to wait before the status + to be reached. Defaults to 300. + :returns: The load balancer is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to the desired status failed to occur within the specified wait + time. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + has transited to one of the failure statuses. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute. + """ lb = self._find(_lb.LoadBalancer, name_or_id, ignore_missing=False) return resource.wait_for_status(self, lb, status, failures, interval, From 7d66c2e0f82f20c720f7333ffa67f7daab721dd1 Mon Sep 17 00:00:00 2001 From: Gregory Thiemonge Date: Thu, 24 Mar 2022 17:12:52 +0100 Subject: [PATCH 3088/3836] Add additional_vips parameter for Octavia load balancers Add a new parameter for the Octavia loadbalancer API: additional_vips. additional_vips support was introduced in Octavia in [0] [0] Id7153dbf33b9616d7af685fcf13ad9a79793c06b Depends-On: https://review.opendev.org/c/openstack/octavia/+/660239/ Change-Id: I05bf9f0fbf030570faaf8a8a12db5e5766a67465 --- openstack/load_balancer/v2/load_balancer.py | 2 ++ .../tests/unit/load_balancer/test_load_balancer.py | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/openstack/load_balancer/v2/load_balancer.py b/openstack/load_balancer/v2/load_balancer.py index 422d4ad85..761555e51 100644 --- a/openstack/load_balancer/v2/load_balancer.py +++ b/openstack/load_balancer/v2/load_balancer.py @@ -70,6 +70,8 @@ class LoadBalancer(resource.Resource, tag.TagMixin): vip_subnet_id = resource.Body('vip_subnet_id') # VIP qos policy id vip_qos_policy_id = resource.Body('vip_qos_policy_id') + #: Additional VIPs + additional_vips = resource.Body('additional_vips', type=list) def delete(self, session, error_message=None): request = self._prepare_request() diff --git a/openstack/tests/unit/load_balancer/test_load_balancer.py b/openstack/tests/unit/load_balancer/test_load_balancer.py index 9a0a7b423..ed52db90d 100644 --- a/openstack/tests/unit/load_balancer/test_load_balancer.py +++ b/openstack/tests/unit/load_balancer/test_load_balancer.py @@ -37,6 +37,15 @@ 'vip_port_id': uuid.uuid4(), 'vip_subnet_id': uuid.uuid4(), 'vip_qos_policy_id': uuid.uuid4(), + 'additional_vips': [ + { + 'subnet_id': uuid.uuid4(), + 'ip_address': '192.0.2.6' + }, { + 'subnet_id': uuid.uuid4(), + 'ip_address': '192.0.2.7' + } + ] } EXAMPLE_STATS = { @@ -92,6 +101,8 @@ def test_make_it(self): test_load_balancer.vip_subnet_id) self.assertEqual(EXAMPLE['vip_qos_policy_id'], test_load_balancer.vip_qos_policy_id) + self.assertEqual(EXAMPLE['additional_vips'], + test_load_balancer.additional_vips) self.assertDictEqual( {'limit': 'limit', From 0536405a445ca906f832d48ca12d5073b5a09cdc Mon Sep 17 00:00:00 2001 From: hai Date: Thu, 18 Aug 2022 15:32:29 -0500 Subject: [PATCH 3089/3836] Add support for fault object per Server API In Openstack CLI, we could do this: openstack server show VM_UUID -f json | jq '.fault.message'. But the same functionality is missing in openstacksdk. This particular patch will address this issue. Change-Id: I05101d5154ee5087d50c5cbf347054fdb1f582fe --- openstack/cloud/_compute.py | 3 ++- openstack/compute/v2/server.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index d9a85f1ac..7cd222923 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -1097,7 +1097,8 @@ def get_active_server( nat_destination=None, ): if server['status'] == 'ERROR': - if 'fault' in server and 'message' in server['fault']: + if ('fault' in server and server['fault'] is not None + and 'message' in server['fault']): raise exc.OpenStackCloudException( "Error in creating the server." " Compute service reports fault: {reason}".format( diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 8a16df0c0..b8fb906f5 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -116,6 +116,9 @@ class Server(resource.Resource, metadata.MetadataMixin, tag.TagMixin): has_config_drive = resource.Body('config_drive') #: An ID representing the host of this server. host_id = resource.Body('hostId') + #: A fault object. Only available when the server status + #: is ERROR or DELETED and a fault occurred. + fault = resource.Body('fault') #: The host status. host_status = resource.Body('host_status') #: The hostname set on the instance when it is booted. From a0292478c1b1682aa02c3911d0ee1a3899b63202 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Sat, 13 Aug 2022 15:34:55 +0200 Subject: [PATCH 3090/3836] Unify resource list filtering Extend resource list method to accept all possible filtering parameters. What is supported by the API are sent to the server. Remaining parameters are applied to the fetched results. With this `allow_unknown_params` parameter of the list call is dropped. Change-Id: Ie9cfb81330d6b98b97b7abad9cf5ae6334ba12e7 --- openstack/cloud/_compute.py | 8 ++--- openstack/cloud/_security_group.py | 3 +- openstack/compute/v2/flavor.py | 10 ++++-- openstack/proxy.py | 24 +++++++++++-- openstack/resource.py | 48 ++++++++++++++++++++++--- openstack/tests/unit/test_proxy.py | 32 +++++++++++++++++ openstack/tests/unit/test_resource.py | 50 ++++++++++++++++----------- 7 files changed, 138 insertions(+), 37 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index d9a85f1ac..a33529c44 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -153,8 +153,7 @@ def list_keypairs(self, filters=None): """ if not filters: filters = {} - return list(self.compute.keypairs(allow_unknown_params=True, - **filters)) + return list(self.compute.keypairs(**filters)) @_utils.cache_on_arguments() def list_availability_zone_names(self, unavailable=False): @@ -353,7 +352,7 @@ def _list_servers(self, detailed=False, all_projects=False, bare=False, return [ self._expand_server(server, detailed, bare) for server in self.compute.servers( - all_projects=all_projects, allow_unknown_params=True, + all_projects=all_projects, **filters) ] @@ -1504,7 +1503,6 @@ def list_hypervisors(self, filters={}): return list(self.compute.hypervisors( details=True, - allow_unknown_params=True, **filters)) def search_aggregates(self, name_or_id=None, filters=None): @@ -1525,7 +1523,7 @@ def list_aggregates(self, filters={}): :returns: A list of compute ``Aggregate`` objects. """ - return self.compute.aggregates(allow_unknown_params=True, **filters) + return self.compute.aggregates(**filters) # TODO(stephenfin): This shouldn't return a munch def get_aggregate(self, name_or_id, filters=None): diff --git a/openstack/cloud/_security_group.py b/openstack/cloud/_security_group.py index 46427496d..b63959701 100644 --- a/openstack/cloud/_security_group.py +++ b/openstack/cloud/_security_group.py @@ -57,8 +57,7 @@ def list_security_groups(self, filters=None): # pass filters dict to the list to filter as much as possible on # the server side return list( - self.network.security_groups(allow_unknown_params=True, - **filters)) + self.network.security_groups(**filters)) # Handle nova security groups else: diff --git a/openstack/compute/v2/flavor.py b/openstack/compute/v2/flavor.py index f2ad48386..55555873c 100644 --- a/openstack/compute/v2/flavor.py +++ b/openstack/compute/v2/flavor.py @@ -86,8 +86,13 @@ def __getattribute__(self, name): return super().__getattribute__(name) @classmethod - def list(cls, session, paginated=True, base_path='/flavors/detail', - allow_unknown_params=False, **params): + def list( + cls, + session, + paginated=True, + base_path='/flavors/detail', + **params + ): # Find will invoke list when name was passed. Since we want to return # flavor with details (same as direct get) we need to swap default here # and list with "/flavors" if no details explicitely requested @@ -98,7 +103,6 @@ def list(cls, session, paginated=True, base_path='/flavors/detail', return super(Flavor, cls).list( session, paginated=paginated, base_path=base_path, - allow_unknown_params=allow_unknown_params, **params) def _action(self, session, body, microversion=None): diff --git a/openstack/proxy.py b/openstack/proxy.py index cd2fc5e73..7790eaac3 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -21,6 +21,7 @@ except ImportError: JSONDecodeError = ValueError import iso8601 +import jmespath from keystoneauth1 import adapter from openstack import _log @@ -637,7 +638,14 @@ def _get( ), ) - def _list(self, resource_type, paginated=True, base_path=None, **attrs): + def _list( + self, + resource_type, + paginated=True, + base_path=None, + jmespath_filters=None, + **attrs + ): """List a resource :param resource_type: The type of resource to list. This should @@ -650,6 +658,9 @@ def _list(self, resource_type, paginated=True, base_path=None, **attrs): :param str base_path: Base part of the URI for listing resources, if different from :data:`~openstack.resource.Resource.base_path`. + :param str jmespath_filters: A string containing a jmespath expression + for further filtering. + :param dict attrs: Attributes to be passed onto the :meth:`~openstack.resource.Resource.list` method. These should correspond to either :class:`~openstack.resource.URI` values @@ -660,10 +671,17 @@ def _list(self, resource_type, paginated=True, base_path=None, **attrs): :class:`~openstack.resource.Resource` that doesn't match the ``resource_type``. """ - return resource_type.list( - self, paginated=paginated, base_path=base_path, **attrs + + data = resource_type.list( + self, paginated=paginated, base_path=base_path, + **attrs ) + if jmespath_filters and isinstance(jmespath_filters, str): + return jmespath.search(jmespath_filters, data) + + return data + def _head(self, resource_type, value=None, base_path=None, **attrs): """Retrieve a resource's header diff --git a/openstack/resource.py b/openstack/resource.py index 1d7724154..d802af09e 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1953,6 +1953,9 @@ def list( checked against the :data:`~openstack.resource.Resource.base_path` format string to see if any path fragments need to be filled in by the contents of this argument. + Parameters supported as filters by the server side are passed in + the API call, remaining parameters are applied as filters to the + retrieved results. :return: A generator of :class:`Resource` objects. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if @@ -1969,12 +1972,24 @@ def list( if base_path is None: base_path = cls.base_path - params = cls._query_mapping._validate( + api_filters = cls._query_mapping._validate( params, base_path=base_path, - allow_unknown_params=allow_unknown_params, + allow_unknown_params=True, ) - query_params = cls._query_mapping._transpose(params, cls) + client_filters = dict() + # Gather query parameters which are not supported by the server + for (k, v) in params.items(): + if ( + # Known attr + hasattr(cls, k) + # Is real attr property + and isinstance(getattr(cls, k), Body) + # not included in the query_params + and k not in cls._query_mapping._mapping.keys() + ): + client_filters[k] = v + query_params = cls._query_mapping._transpose(api_filters, cls) uri = base_path % params uri_params = {} @@ -1985,6 +2000,18 @@ def list( if hasattr(cls, k) and isinstance(getattr(cls, k), URI): uri_params[k] = v + def _dict_filter(f, d): + """Dict param based filtering""" + if not d: + return False + for key in f.keys(): + if isinstance(f[key], dict): + if not _dict_filter(f[key], d.get(key, None)): + return False + elif d.get(key, None) != f[key]: + return False + return True + # Track the total number of resources yielded so we can paginate # swift objects total_yielded = 0 @@ -2028,7 +2055,20 @@ def list( **raw_resource, ) marker = value.id - yield value + filters_matched = True + # Iterate over client filters and return only if matching + for key in client_filters.keys(): + if isinstance(client_filters[key], dict): + if not _dict_filter( + client_filters[key], value.get(key, None)): + filters_matched = False + break + elif value.get(key, None) != client_filters[key]: + filters_matched = False + break + + if filters_matched: + yield value total_yielded += 1 if resources and paginated: diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index f2f55a54a..c4b1c437b 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -43,6 +43,16 @@ class ListableResource(resource.Resource): allow_list = True +class FilterableResource(resource.Resource): + allow_list = True + base_path = '/fakes' + + _query_mapping = resource.QueryParameters('a') + a = resource.Body('a') + b = resource.Body('b') + c = resource.Body('c') + + class HeadableResource(resource.Resource): allow_head = True @@ -461,6 +471,28 @@ def test_list_non_paginated(self): def test_list_override_base_path(self): self._test_list(False, base_path='dummy') + def test_list_filters_jmespath(self): + fake_response = [ + FilterableResource(a='a1', b='b1', c='c'), + FilterableResource(a='a2', b='b2', c='c'), + FilterableResource(a='a3', b='b3', c='c'), + ] + FilterableResource.list = mock.Mock() + FilterableResource.list.return_value = fake_response + + rv = self.sot._list( + FilterableResource, paginated=False, + base_path=None, jmespath_filters="[?c=='c']" + ) + self.assertEqual(3, len(rv)) + + # Test filtering based on unknown attribute + rv = self.sot._list( + FilterableResource, paginated=False, + base_path=None, jmespath_filters="[?d=='c']" + ) + self.assertEqual(0, len(rv)) + class TestProxyHead(base.TestCase): diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 2edb9e380..49ea8439b 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -2333,56 +2333,66 @@ class Test(self.test_class): self.assertEqual(self.session.get.call_args_list[0][0][0], Test.base_path % {"something": uri_param}) - def test_invalid_list_params(self): - id = 1 + def test_allow_invalid_list_params(self): qp = "query param!" qp_name = "query-param" uri_param = "uri param!" - mock_response = mock.Mock() - mock_response.json.side_effect = [[{"id": id}], - []] + mock_empty = mock.Mock() + mock_empty.status_code = 200 + mock_empty.links = {} + mock_empty.json.return_value = {"resources": []} - self.session.get.return_value = mock_response + self.session.get.side_effect = [mock_empty] class Test(self.test_class): _query_mapping = resource.QueryParameters(query_param=qp_name) base_path = "/%(something)s/blah" something = resource.URI("something") - try: - list(Test.list(self.session, paginated=True, query_param=qp, - something=uri_param, something_wrong=True)) - self.assertFail('The above line should fail') - except exceptions.InvalidResourceQuery as err: - self.assertEqual(str(err), 'Invalid query params: something_wrong') + list(Test.list(self.session, paginated=True, query_param=qp, + allow_unknown_params=True, something=uri_param, + something_wrong=True)) + self.session.get.assert_called_once_with( + "/{something}/blah".format(something=uri_param), + headers={'Accept': 'application/json'}, + microversion=None, + params={qp_name: qp} + ) - def test_allow_invalid_list_params(self): + def test_list_client_filters(self): qp = "query param!" - qp_name = "query-param" uri_param = "uri param!" mock_empty = mock.Mock() mock_empty.status_code = 200 mock_empty.links = {} - mock_empty.json.return_value = {"resources": []} + mock_empty.json.return_value = {"resources": [ + {"a": "1", "b": "1"}, + {"a": "1", "b": "2"}, + ]} self.session.get.side_effect = [mock_empty] class Test(self.test_class): - _query_mapping = resource.QueryParameters(query_param=qp_name) + _query_mapping = resource.QueryParameters('a') base_path = "/%(something)s/blah" something = resource.URI("something") + a = resource.Body("a") + b = resource.Body("b") - list(Test.list(self.session, paginated=True, query_param=qp, - allow_unknown_params=True, something=uri_param, - something_wrong=True)) + res = list(Test.list( + self.session, paginated=True, query_param=qp, + allow_unknown_params=True, something=uri_param, + a='1', b='2')) self.session.get.assert_called_once_with( "/{something}/blah".format(something=uri_param), headers={'Accept': 'application/json'}, microversion=None, - params={qp_name: qp} + params={'a': '1'} ) + self.assertEqual(1, len(res)) + self.assertEqual("2", res[0].b) def test_values_as_list_params(self): id = 1 From 0529c30ae2958338aa8c0d56066b762eb412adcc Mon Sep 17 00:00:00 2001 From: Areg Grigoryan Date: Thu, 18 Aug 2022 15:21:24 +0200 Subject: [PATCH 3091/3836] resource: Fix pagination of nested Glance resources This is a simple off-by-one error, but there are lots of things feeding into this that I figured were worth explaining. Apologies in advance for the length of this screed. Hopefully it's useful for someone. There's a small bug in how we paginate when listing resources, or rather when listing Glance objects. Glance doesn't return a 'Links' response header or 'links' response body field like most other OpenStack services. Instead, it returns a simple 'next' body field that is relative. For example, consider the paginated response to the '/images' API. { "images": [ ... ], "first":"/v2/images", "schema":"/v2/schemas/images", "next":"/v2/images?marker=823762fb-3128-43cf-8136-cee6b749f708" } As a result, we're forced to treat Glance as yet another "special" service when it comes to pagination. The first step of this is to look for the 'next' field and grab the value. As you can see, this value also includes the version prefix, which we don't want this since keystoneauth1 already appends the version prefix when generating absolute URLs from information in the service catalog. This means our next step is to strip this version filter, which we have done since change Iad8e69d20783cfe59c3e86bd6980da7dee802869. Unfortunately, the original author of that change was a little too liberal in their stripping (heh) and also removed the leading new slash. This causes issues with the final part of our generation of the next link, namely the deduplication of query parameters. Because most servers enforce an upper limit on the size of URLs, it's important that we don't constantly append our query parameters to the request URL for each page. Instead, we parse the query parameters present in the 'next' URL and munge them with our our parameters. The way we do this is using 'urllib.parse.urljoin' method with this pattern: urljoin(url, urlparse(url).path) For example: >>> url = 'http://example.com/foo/bar?pet=dog&name=baxter' >>> urljoin(url, urlparse(url).path) 'http://example.com/foo/bar' >>> url = '/foo/bar?pet=dog&name=baxter' >>> urljoin(url, urlparse(url).path) '/foo/bar' Because we were stripping the leading '/', the first part of the path was being identified as the server and was stripped. >>> url = 'foo/bar?pet=dog&name=baxter' >>> urljoin(url, urlparse(url).path) 'foo/foo/bar' After all that, the solution is simple. Stop stripping the leading slash and generate a proper relative URL, as expected. Change-Id: Ic9e1891ae7171a0cc45dd9faf1b2a6e37535b777 Co-authored-by: Stephen Finucane Story: 2010273 Task: 46200 --- openstack/resource.py | 9 +++- openstack/tests/unit/test_resource.py | 59 +++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/openstack/resource.py b/openstack/resource.py index 1d7724154..76a1b19ac 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -2054,34 +2054,40 @@ def list( def _get_next_link(cls, uri, response, data, marker, limit, total_yielded): next_link = None params = {} + if isinstance(data, dict): pagination_key = cls.pagination_key if not pagination_key and 'links' in data: # api-wg guidelines are for a links dict in the main body pagination_key = 'links' + if not pagination_key and cls.resources_key: # Nova has a {key}_links dict in the main body pagination_key = '{key}_links'.format(key=cls.resources_key) + if pagination_key: links = data.get(pagination_key, {}) # keystone might return a dict if isinstance(links, dict): links = ({k: v} for k, v in links.items()) + for item in links: if item.get('rel') == 'next' and 'href' in item: next_link = item['href'] break + # Glance has a next field in the main body next_link = next_link or data.get('next') if next_link and next_link.startswith('/v'): - next_link = next_link[next_link.find('/', 1) + 1 :] + next_link = next_link[next_link.find('/', 1):] if not next_link and 'next' in response.links: # RFC5988 specifies Link headers and requests parses them if they # are there. We prefer link dicts in resource body, but if those # aren't there and Link headers are, use them. next_link = response.links['next']['uri'] + # Swift provides a count of resources in a header and a list body if not next_link and cls.pagination_key: total_count = response.headers.get(cls.pagination_key) @@ -2109,6 +2115,7 @@ def _get_next_link(cls, uri, response, data, marker, limit, total_yielded): next_link = uri params['marker'] = marker params['limit'] = limit + return next_link, params @classmethod diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 2edb9e380..d7f9a51c8 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -2205,6 +2205,65 @@ class Test(self.test_class): self.assertEqual(3, len(self.session.get.call_args_list)) self.assertIsInstance(results[0], self.test_class) + def test_list_response_paginated_with_next_field(self): + """Test pagination with a 'next' field in the response. + + Glance doesn't return a 'links' field in the response. Instead, it + returns a 'first' field and, if there are more pages, a 'next' field in + the response body. Ensure we correctly parse these. + """ + class Test(resource.Resource): + service = self.service_name + base_path = '/foos/bars' + resources_key = 'bars' + allow_list = True + _query_mapping = resource.QueryParameters("wow") + + ids = [1, 2] + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.links = {} + mock_response.json.side_effect = [ + { + "bars": [{"id": ids[0]}], + "first": "/v2/foos/bars?wow=cool", + "next": "/v2/foos/bars?marker=baz&wow=cool", + }, + { + "bars": [{"id": ids[1]}], + "first": "/v2/foos/bars?wow=cool", + }, + ] + + self.session.get.return_value = mock_response + + results = list(Test.list(self.session, paginated=True, wow="cool")) + + self.assertEqual(2, len(results)) + self.assertEqual(ids[0], results[0].id) + self.assertEqual(ids[1], results[1].id) + self.assertEqual( + mock.call( + Test.base_path, + headers={'Accept': 'application/json'}, + params={'wow': 'cool'}, + microversion=None, + ), + self.session.get.mock_calls[0] + ) + self.assertEqual( + mock.call( + '/foos/bars', + headers={'Accept': 'application/json'}, + params={'wow': ['cool'], 'marker': ['baz']}, + microversion=None, + ), + self.session.get.mock_calls[2], + ) + + self.assertEqual(2, len(self.session.get.call_args_list)) + self.assertIsInstance(results[0], Test) + def test_list_response_paginated_with_microversions(self): class Test(resource.Resource): service = self.service_name From 703375a8cb2668f4d490a2bf10d248be0ad15a5d Mon Sep 17 00:00:00 2001 From: Areg Grigoryan Date: Thu, 25 Aug 2022 13:53:50 +0200 Subject: [PATCH 3092/3836] added api requirements for new "openstack image metadefs namespace list" command Change-Id: Ibb86d74419faf066edca1a43d2d4da005db780ea --- doc/source/user/proxies/image_v2.rst | 8 +++ doc/source/user/resources/image/index.rst | 1 + .../resources/image/v2/metadef_namespace.rst | 13 ++++ openstack/image/v2/_proxy.py | 10 ++++ openstack/image/v2/metadef_namespace.py | 41 +++++++++++++ .../unit/image/v2/test_metadef_namespace.py | 59 +++++++++++++++++++ openstack/tests/unit/image/v2/test_proxy.py | 7 +++ ...ef-namespace-support-b93557afdcf4272c.yaml | 4 ++ 8 files changed, 143 insertions(+) create mode 100644 doc/source/user/resources/image/v2/metadef_namespace.rst create mode 100644 openstack/image/v2/metadef_namespace.py create mode 100644 openstack/tests/unit/image/v2/test_metadef_namespace.py create mode 100644 releasenotes/notes/add-image-metadef-namespace-support-b93557afdcf4272c.yaml diff --git a/doc/source/user/proxies/image_v2.rst b/doc/source/user/proxies/image_v2.rst index 1496f1b8f..79a5a974b 100644 --- a/doc/source/user/proxies/image_v2.rst +++ b/doc/source/user/proxies/image_v2.rst @@ -51,3 +51,11 @@ Service Info Discovery Operations .. autoclass:: openstack.image.v2._proxy.Proxy :noindex: :members: stores, get_import_info + + +Metadef Namespace Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.image.v2._proxy.Proxy + :noindex: + :members: metadef_namespaces diff --git a/doc/source/user/resources/image/index.rst b/doc/source/user/resources/image/index.rst index 4e09af0e2..a41d3e940 100644 --- a/doc/source/user/resources/image/index.rst +++ b/doc/source/user/resources/image/index.rst @@ -14,5 +14,6 @@ Image v2 Resources v2/image v2/member + v2/metadef_namespace v2/task v2/service_info diff --git a/doc/source/user/resources/image/v2/metadef_namespace.rst b/doc/source/user/resources/image/v2/metadef_namespace.rst new file mode 100644 index 000000000..de2c0f552 --- /dev/null +++ b/doc/source/user/resources/image/v2/metadef_namespace.rst @@ -0,0 +1,13 @@ +openstack.image.v2.metadef_namespace +===================================== + +.. automodule:: openstack.image.v2.metadef_namespace + +The MetadefNamespace Class +---------------------------- + +The ``MetadefNamespace`` class inherits +from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.image.v2.metadef_namespace.MetadefNamespace + :members: diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index de3a8e2e3..816af24a1 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -17,6 +17,7 @@ from openstack.image import _base_proxy from openstack.image.v2 import image as _image from openstack.image.v2 import member as _member +from openstack.image.v2 import metadef_namespace as _metadef_namespace from openstack.image.v2 import schema as _schema from openstack.image.v2 import service_info as _si from openstack.image.v2 import task as _task @@ -841,3 +842,12 @@ def get_import_info(self): when no resource can be found. """ return self._get(_si.Import, require_id=False) + + def metadef_namespaces(self, **query): + """Get a info about image metadef namespaces + + :returns: A generator object of metadef namespaces + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._list(_metadef_namespace.MetadefNamespace, **query) diff --git a/openstack/image/v2/metadef_namespace.py b/openstack/image/v2/metadef_namespace.py new file mode 100644 index 000000000..6a8d4522b --- /dev/null +++ b/openstack/image/v2/metadef_namespace.py @@ -0,0 +1,41 @@ +# 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 resource + + +# TODO(schwicke): create and delete still need to be implemented +class MetadefNamespace(resource.Resource): + resources_key = 'namespaces' + base_path = '/metadefs/namespaces' + + allow_fetch = True + allow_list = True + + _query_mapping = resource.QueryParameters( + "limit", + "marker", + "resource_types", + "sort_dir", + "sort_key", + "visibility" + ) + created_at = resource.Body('created_at') + description = resource.Body('description') + display_name = resource.Body('display_name') + is_protected = resource.Body('protected', type=bool) + namespace = resource.Body('namespace') + owner = resource.Body('owner') + resource_type_associations = resource.Body('resource_type_associations', + type=list, + list_type=dict) + visibility = resource.Body('visibility') diff --git a/openstack/tests/unit/image/v2/test_metadef_namespace.py b/openstack/tests/unit/image/v2/test_metadef_namespace.py new file mode 100644 index 000000000..a4888fe0d --- /dev/null +++ b/openstack/tests/unit/image/v2/test_metadef_namespace.py @@ -0,0 +1,59 @@ +# 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.image.v2 import metadef_namespace +from openstack.tests.unit import base + +EXAMPLE = { + 'display_name': 'Cinder Volume Type', + 'created_at': '2014-08-28T17:13:06Z', + 'is_protected': True, + 'namespace': 'OS::Cinder::Volumetype', + 'owner': 'admin', + 'resource_type_associations': [ + { + 'created_at': '2014-08-28T17:13:06Z', + 'name': 'OS::Glance::Image', + 'updated_at': '2014-08-28T17:13:06Z' + } + ] +} + + +class TestMetadefNamespace(base.TestCase): + def test_basic(self): + sot = metadef_namespace.MetadefNamespace() + self.assertIsNone(sot.resource_key) + self.assertEqual('namespaces', sot.resources_key) + self.assertEqual('/metadefs/namespaces', sot.base_path) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = metadef_namespace.MetadefNamespace(**EXAMPLE) + self.assertEqual(EXAMPLE['namespace'], sot.namespace) + self.assertEqual(EXAMPLE['owner'], sot.owner) + self.assertEqual(EXAMPLE['created_at'], sot.created_at) + self.assertEqual(EXAMPLE['is_protected'], sot.is_protected) + self.assertEqual(EXAMPLE['display_name'], sot.display_name) + self.assertListEqual(EXAMPLE['resource_type_associations'], + sot.resource_type_associations) + + self.assertDictEqual( + { + 'limit': 'limit', + 'marker': 'marker', + 'resource_types': 'resource_types', + 'sort_dir': 'sort_dir', + 'sort_key': 'sort_key', + 'visibility': 'visibility' + }, sot._query_mapping._mapping) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 16ff23c25..589b56d6e 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -19,6 +19,7 @@ from openstack.image.v2 import _proxy from openstack.image.v2 import image from openstack.image.v2 import member +from openstack.image.v2 import metadef_namespace from openstack.image.v2 import schema from openstack.image.v2 import service_info as si from openstack.image.v2 import task @@ -553,3 +554,9 @@ def test_import_info(self): method_kwargs={}, expected_args=[si.Import], expected_kwargs={'require_id': False}) + + +class TestMetadefNamespace(TestImageProxy): + def test_metadef_namespaces(self): + self.verify_list(self.proxy.metadef_namespaces, + metadef_namespace.MetadefNamespace) diff --git a/releasenotes/notes/add-image-metadef-namespace-support-b93557afdcf4272c.yaml b/releasenotes/notes/add-image-metadef-namespace-support-b93557afdcf4272c.yaml new file mode 100644 index 000000000..01514fff0 --- /dev/null +++ b/releasenotes/notes/add-image-metadef-namespace-support-b93557afdcf4272c.yaml @@ -0,0 +1,4 @@ +--- +features: + -| + Adds support to query metadef namespaces from glance. From 60c067cdabe5c0d330a2ee390eaf85ba7bfa3a1d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 7 Sep 2022 12:43:21 +0100 Subject: [PATCH 3093/3836] tests: Remove unnecessary service check The base functional test class already checks that the block-storage service is present and supports the v3 API. No need to duplicate this. Change-Id: I478ed8d08b111aa8ef51cd11978f94d5cc576b14 Signed-off-by: Stephen Finucane --- openstack/tests/functional/block_storage/v3/test_group.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openstack/tests/functional/block_storage/v3/test_group.py b/openstack/tests/functional/block_storage/v3/test_group.py index a3929d610..feafb0996 100644 --- a/openstack/tests/functional/block_storage/v3/test_group.py +++ b/openstack/tests/functional/block_storage/v3/test_group.py @@ -20,9 +20,6 @@ class TestGroup(base.BaseBlockStorageTest): def setUp(self): super().setUp() - if not self.user_cloud.has_service('block-storage'): - self.skipTest('block-storage service not supported by cloud') - # there will always be at least one volume type, i.e. the default one volume_types = list(self.conn.block_storage.types()) self.volume_type = volume_types[0] From abc5977389f4a30a39cb2e7aa5e783031ceb8c10 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 7 Sep 2022 12:04:41 +0100 Subject: [PATCH 3094/3836] image: Trivial grouping of image proxy methods Change-Id: I05557a1ef2cc413c3aa84424f1cab6f53662e0c2 Signed-off-by: Stephen Finucane --- openstack/image/_base_proxy.py | 1 + openstack/image/v2/_proxy.py | 65 ++++++++++++--------- openstack/tests/unit/image/v2/test_proxy.py | 14 +++-- 3 files changed, 45 insertions(+), 35 deletions(-) diff --git a/openstack/image/_base_proxy.py b/openstack/image/_base_proxy.py index 56af3043c..d58de5b57 100644 --- a/openstack/image/_base_proxy.py +++ b/openstack/image/_base_proxy.py @@ -33,6 +33,7 @@ class BaseImageProxy(proxy.Proxy, metaclass=abc.ABCMeta): _SHADE_IMAGE_SHA256_KEY = 'owner_specified.shade.sha256' _SHADE_IMAGE_OBJECT_KEY = 'owner_specified.shade.object' + # ====== IMAGES ====== def create_image( self, name, filename=None, container=None, diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 816af24a1..0578d2552 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -32,6 +32,7 @@ class Proxy(_base_proxy.BaseImageProxy): + # ====== IMAGES ====== def _create_image(self, **kwargs): """Create image resource from attributes """ @@ -564,6 +565,7 @@ def remove_tag(self, image, tag): image = self._get_resource(_image.Image, image) image.remove_tag(self, tag) + # ====== IMAGE MEMBERS ====== def add_member(self, image, **attrs): """Create a new member from attributes @@ -669,6 +671,17 @@ def update_member(self, member, image, **attrs): return self._update(_member.Member, member_id=member_id, image_id=image_id, **attrs) + # ====== METADEF NAMESPACES ====== + def metadef_namespaces(self, **query): + """Get a info about image metadef namespaces + + :returns: A generator object of metadef namespaces + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._list(_metadef_namespace.MetadefNamespace, **query) + + # ====== SCHEMAS ====== def get_images_schema(self): """Get images schema @@ -709,6 +722,27 @@ def get_member_schema(self): return self._get(_schema.Schema, requires_id=False, base_path='/schemas/member') + def get_tasks_schema(self): + """Get image tasks schema + + :returns: One :class:`~openstack.image.v2.schema.Schema` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_schema.Schema, requires_id=False, + base_path='/schemas/tasks') + + def get_task_schema(self): + """Get image task schema + + :returns: One :class:`~openstack.image.v2.schema.Schema` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_schema.Schema, requires_id=False, + base_path='/schemas/task') + + # ====== TASKS ====== def tasks(self, **query): """Return a generator of tasks @@ -806,26 +840,7 @@ def wait_for_task(self, task, status='success', failures=None, self.log.debug('Still waiting for resource %s to reach state %s, ' 'current state is %s', name, status, new_status) - def get_tasks_schema(self): - """Get image tasks schema - - :returns: One :class:`~openstack.image.v2.schema.Schema` - :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. - """ - return self._get(_schema.Schema, requires_id=False, - base_path='/schemas/tasks') - - def get_task_schema(self): - """Get image task schema - - :returns: One :class:`~openstack.image.v2.schema.Schema` - :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. - """ - return self._get(_schema.Schema, requires_id=False, - base_path='/schemas/task') - + # ====== STORES ====== def stores(self, **query): """Return a generator of supported image stores @@ -834,6 +849,7 @@ def stores(self, **query): """ return self._list(_si.Store, **query) + # ====== IMPORTS ====== def get_import_info(self): """Get a info about image constraints @@ -842,12 +858,3 @@ def get_import_info(self): when no resource can be found. """ return self._get(_si.Import, require_id=False) - - def metadef_namespaces(self, **query): - """Get a info about image metadef namespaces - - :returns: A generator object of metadef namespaces - :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. - """ - return self._list(_metadef_namespace.MetadefNamespace, **query) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 589b56d6e..9687b749f 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -409,6 +409,14 @@ def test_members(self): expected_kwargs={'image_id': 'image_1'}) +class TestMetadefNamespace(TestImageProxy): + def test_metadef_namespaces(self): + self.verify_list( + self.proxy.metadef_namespaces, + metadef_namespace.MetadefNamespace, + ) + + class TestSchema(TestImageProxy): def test_images_schema_get(self): self._verify( @@ -554,9 +562,3 @@ def test_import_info(self): method_kwargs={}, expected_args=[si.Import], expected_kwargs={'require_id': False}) - - -class TestMetadefNamespace(TestImageProxy): - def test_metadef_namespaces(self): - self.verify_list(self.proxy.metadef_namespaces, - metadef_namespace.MetadefNamespace) From 581a016cf94abd993fccbdae36315839f2783f4e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 17 Nov 2021 18:57:08 +0000 Subject: [PATCH 3095/3836] resource: Remove unused helper, variable The 'warning_if_attribute_deprecated' helper was added in change Iba2b0d09fdd631f8bd2c3c951fd69b243deed652 but doesn't seem to have been used since. Similarly, the '_delete_response_class' attribute was added in change Ifa44aacc4b4719b73e59d27ed0fcd35130358608 but had not been used since. Remove both. Change-Id: I9f0fb886f1c66842d97f57c2962de1627841aeb6 Signed-off-by: Stephen Finucane --- openstack/resource.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/openstack/resource.py b/openstack/resource.py index 1d7724154..99ae58e1c 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -511,7 +511,6 @@ class Resource(dict): _uri = None _computed = None _original_body = None - _delete_response_class = None _store_unknown_attrs_as_properties = False _allow_unknown_attrs_in_body = False _unknown_attrs_in_body = None @@ -628,17 +627,6 @@ def __eq__(self, comparand): ] ) - def warning_if_attribute_deprecated(self, attr, value): - if value and self.deprecated: - if not self.deprecation_reason: - LOG.warning( - "The option [%s] has been deprecated. " - "Please avoid using it.", - attr, - ) - else: - LOG.warning(self.deprecation_reason) - def __getattribute__(self, name): """Return an attribute on this instance From 1944b4c70b62c0f36cd9c549ff5e478ca9eb01ff Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 24 Jun 2022 12:23:21 +0100 Subject: [PATCH 3096/3836] trivial: Correct some docstrings It seems we were cargo-culting this mistake. Correct it now to stop the rot. Change-Id: Ie7670480f73bfa37411b8244f900750301f6c20b Signed-off-by: Stephen Finucane --- openstack/block_storage/v2/_proxy.py | 2 +- openstack/block_storage/v3/_proxy.py | 13 +++--- openstack/compute/v2/_proxy.py | 2 +- openstack/database/v1/_proxy.py | 7 ++- openstack/identity/v2/_proxy.py | 12 ++--- openstack/identity/v3/_proxy.py | 56 +++++++++++----------- openstack/image/v1/_proxy.py | 4 +- openstack/image/v2/_proxy.py | 8 ++-- openstack/key_manager/v1/_proxy.py | 21 ++++----- openstack/network/v2/_proxy.py | 69 ++++++++++++++-------------- openstack/placement/v1/_proxy.py | 4 +- 11 files changed, 96 insertions(+), 102 deletions(-) diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 358ae1ea7..ceb7777c6 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -619,7 +619,7 @@ def update_quota_set(self, quota_set, query=None, **attrs): :param quota_set: Either the ID of a quota_set or a :class:`~openstack.block_storage.v2.quota_set.QuotaSet` instance. :param dict query: Optional parameters to be used with update call. - :attrs kwargs: The attributes to update on the QuotaSet represented + :param attrs: The attributes to update on the QuotaSet represented by ``quota_set``. :returns: The updated QuotaSet diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index c2436514a..3dc93b5fd 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -1262,7 +1262,7 @@ def delete_group_type_group_specs_property(self, group_type, prop): # ====== QUOTA SETS ====== def get_quota_set(self, project, usage=False, **query): - """Show quota set information for the project + """Show QuotaSet information for the project :param project: ID or instance of :class:`~openstack.identity.project.Project` of the project for @@ -1282,7 +1282,7 @@ def get_quota_set(self, project, usage=False, **query): self, usage=usage, **query) def get_quota_set_defaults(self, project): - """Show quota set defaults for the project + """Show QuotaSet defaults for the project :param project: ID or instance of :class:`~openstack.identity.project.Project` of the project for @@ -1299,7 +1299,7 @@ def get_quota_set_defaults(self, project): self, base_path='/os-quota-sets/defaults') def revert_quota_set(self, project, **query): - """Reset quota for the project/user. + """Reset Quota for the project/user. :param project: ID or instance of :class:`~openstack.identity.project.Project` of the project for @@ -1312,17 +1312,16 @@ def revert_quota_set(self, project, **query): res = self._get_resource( _quota_set.QuotaSet, None, project_id=project.id) - if not query: - query = {} return res.delete(self, **query) def update_quota_set(self, quota_set, query=None, **attrs): - """Update a quota set. + """Update a QuotaSet. :param quota_set: Either the ID of a quota_set or a :class:`~openstack.block_storage.v3.quota_set.QuotaSet` instance. :param dict query: Optional parameters to be used with update call. - :attrs kwargs: The attributes to update on the quota set + :param attrs: The attributes to update on the QuotaSet represented + by ``quota_set``. :returns: The updated QuotaSet :rtype: :class:`~openstack.block_storage.v3.quota_set.QuotaSet` diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index f9a56cf50..3f0373d1f 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -2037,7 +2037,7 @@ def update_quota_set(self, quota_set, query=None, **attrs): :param quota_set: Either the ID of a quota_set or a :class:`~openstack.compute.v2.quota_set.QuotaSet` instance. :param dict query: Optional parameters to be used with update call. - :params attrs: The attributes to update on the QuotaSet represented + :param attrs: The attributes to update on the QuotaSet represented by ``quota_set``. :returns: The updated QuotaSet diff --git a/openstack/database/v1/_proxy.py b/openstack/database/v1/_proxy.py index 724b42b6e..8291344d5 100644 --- a/openstack/database/v1/_proxy.py +++ b/openstack/database/v1/_proxy.py @@ -214,10 +214,9 @@ def update_instance(self, instance, **attrs): """Update a instance :param instance: Either the id of a instance or a - :class:`~openstack.database.v1.instance.Instance` - instance. - :attrs kwargs: The attributes to update on the instance represented - by ``value``. + :class:`~openstack.database.v1.instance.Instance` instance. + :param attrs: The attributes to update on the instance represented + by ``instance``. :returns: The updated instance :rtype: :class:`~openstack.database.v1.instance.Instance` diff --git a/openstack/identity/v2/_proxy.py b/openstack/identity/v2/_proxy.py index 537c95d9d..23772af45 100644 --- a/openstack/identity/v2/_proxy.py +++ b/openstack/identity/v2/_proxy.py @@ -109,8 +109,8 @@ def update_role(self, role, **attrs): :param role: Either the ID of a role or a :class:`~openstack.identity.v2.role.Role` instance. - :attrs kwargs: The attributes to update on the role represented - by ``value``. + :param attrs: The attributes to update on the role represented + by ``role``. :returns: The updated role :rtype: :class:`~openstack.identity.v2.role.Role` @@ -186,8 +186,8 @@ def update_tenant(self, tenant, **attrs): :param tenant: Either the ID of a tenant or a :class:`~openstack.identity.v2.tenant.Tenant` instance. - :attrs kwargs: The attributes to update on the tenant represented - by ``value``. + :param attrs: The attributes to update on the tenant represented + by ``tenant``. :returns: The updated tenant :rtype: :class:`~openstack.identity.v2.tenant.Tenant` @@ -263,8 +263,8 @@ def update_user(self, user, **attrs): :param user: Either the ID of a user or a :class:`~openstack.identity.v2.user.User` instance. - :attrs kwargs: The attributes to update on the user represented - by ``value``. + :param attrs: The attributes to update on the user represented + by ``user``. :returns: The updated user :rtype: :class:`~openstack.identity.v2.user.User` diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 141ef6097..3fbee1d38 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -120,8 +120,8 @@ def update_credential(self, credential, **attrs): :param credential: Either the ID of a credential or a :class:`~openstack.identity.v3.credential.Credential` instance. - :attrs kwargs: The attributes to update on the credential represented - by ``value``. + :param attrs: The attributes to update on the credential represented + by ``credential``. :returns: The updated credential :rtype: :class:`~openstack.identity.v3.credential.Credential` @@ -198,8 +198,8 @@ def update_domain(self, domain, **attrs): :param domain: Either the ID of a domain or a :class:`~openstack.identity.v3.domain.Domain` instance. - :attrs kwargs: The attributes to update on the domain represented - by ``value``. + :param attrs: The attributes to update on the domain represented + by ``domain``. :returns: The updated domain :rtype: :class:`~openstack.identity.v3.domain.Domain` @@ -276,11 +276,10 @@ def endpoints(self, **query): def update_endpoint(self, endpoint, **attrs): """Update a endpoint - :param endpoint: Either the ID of a endpoint or a - :class:`~openstack.identity.v3.endpoint.Endpoint` - instance. - :attrs kwargs: The attributes to update on the endpoint represented - by ``value``. + :param endpoint: Either the ID of an endpoint or a + :class:`~openstack.identity.v3.endpoint.Endpoint` instance. + :param attrs: The attributes to update on the endpoint represented + by ``endpoint``. :returns: The updated endpoint :rtype: :class:`~openstack.identity.v3.endpoint.Endpoint` @@ -359,8 +358,8 @@ def update_group(self, group, **attrs): :param group: Either the ID of a group or a :class:`~openstack.identity.v3.group.Group` instance. - :attrs kwargs: The attributes to update on the group represented - by ``value``. + :param attrs: The attributes to update on the group represented + by ``group``. :returns: The updated group :rtype: :class:`~openstack.identity.v3.group.Group` @@ -476,8 +475,8 @@ def update_policy(self, policy, **attrs): :param policy: Either the ID of a policy or a :class:`~openstack.identity.v3.policy.Policy` instance. - :attrs kwargs: The attributes to update on the policy represented - by ``value``. + :param attrs: The attributes to update on the policy represented + by ``policy``. :returns: The updated policy :rtype: :class:`~openstack.identity.v3.policy.Policy` @@ -569,8 +568,8 @@ def update_project(self, project, **attrs): :param project: Either the ID of a project or a :class:`~openstack.identity.v3.project.Project` instance. - :attrs kwargs: The attributes to update on the project represented - by ``value``. + :param attrs: The attributes to update on the project represented + by ``project``. :returns: The updated project :rtype: :class:`~openstack.identity.v3.project.Project` @@ -647,8 +646,8 @@ def update_service(self, service, **attrs): :param service: Either the ID of a service or a :class:`~openstack.identity.v3.service.Service` instance. - :attrs kwargs: The attributes to update on the service represented - by ``value``. + :param attrs: The attributes to update on the service represented + by ``service``. :returns: The updated service :rtype: :class:`~openstack.identity.v3.service.Service` @@ -725,8 +724,8 @@ def update_user(self, user, **attrs): :param user: Either the ID of a user or a :class:`~openstack.identity.v3.user.User` instance. - :attrs kwargs: The attributes to update on the user represented - by ``value``. + :param attrs: The attributes to update on the user represented + by ``attrs``. :returns: The updated user :rtype: :class:`~openstack.identity.v3.user.User` @@ -868,8 +867,8 @@ def update_region(self, region, **attrs): :param region: Either the ID of a region or a :class:`~openstack.identity.v3.region.Region` instance. - :attrs kwargs: The attributes to update on the region represented - by ``value``. + :param attrs: The attributes to update on the region represented + by ``region``. :returns: The updated region. :rtype: :class:`~openstack.identity.v3.region.Region` @@ -1702,8 +1701,8 @@ def update_federation_protocol(self, idp_id, protocol, **attrs): :param protocol: Either the ID of a federation protocol or a :class:`~openstack.identity.v3.federation_protocol.FederationProtocol` instance. - :attrs kwargs: The attributes to update on the federation protocol - represented by ``value``. + :param attrs: The attributes to update on the federation protocol + represented by ``protocol``. :returns: The updated federation protocol :rtype: @@ -1787,10 +1786,9 @@ def update_mapping(self, mapping, **attrs): """Update a mapping :param mapping: Either the ID of a mapping or a - :class:`~openstack.identity.v3.mapping.Mapping` - instance. - :attrs kwargs: The attributes to update on the mapping represented - by ``value``. + :class:`~openstack.identity.v3.mapping.Mapping` instance. + :param attrs: The attributes to update on the mapping represented + by ``mapping``. :returns: The updated mapping :rtype: :class:`~openstack.identity.v3.mapping.Mapping` @@ -1877,8 +1875,8 @@ def update_identity_provider(self, identity_provider, **attrs): :param mapping: Either the ID of an identity provider or a :class:`~openstack.identity.v3.identity_provider.IdentityProvider` instance. - :attrs kwargs: The attributes to update on the identity_provider - represented by ``value``. + :param attrs: The attributes to update on the identity_provider + represented by ``identity_provider``. :returns: The updated identity provider. :rtype: diff --git a/openstack/image/v1/_proxy.py b/openstack/image/v1/_proxy.py index 548067f7a..4aeffa662 100644 --- a/openstack/image/v1/_proxy.py +++ b/openstack/image/v1/_proxy.py @@ -172,8 +172,8 @@ def update_image(self, image, **attrs): :param image: Either the ID of a image or a :class:`~openstack.image.v1.image.Image` instance. - :attrs kwargs: The attributes to update on the image represented - by ``value``. + :param attrs: The attributes to update on the image represented + by ``image``. :returns: The updated image :rtype: :class:`~openstack.image.v1.image.Image` diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index de3a8e2e3..71a52fec7 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -507,8 +507,8 @@ def update_image(self, image, **attrs): :param image: Either the ID of a image or a :class:`~openstack.image.v2.image.Image` instance. - :attrs kwargs: The attributes to update on the image represented - by ``value``. + :param attrs: The attributes to update on the image represented + by ``image``. :returns: The updated image :rtype: :class:`~openstack.image.v2.image.Image` @@ -657,8 +657,8 @@ def update_member(self, member, image, **attrs): :param image: This is the image that the member belongs to. The value can be the ID of a image or a :class:`~openstack.image.v2.image.Image` instance. - :attrs kwargs: The attributes to update on the member represented - by ``value``. + :param attrs: The attributes to update on the member represented + by ``member``. :returns: The updated member :rtype: :class:`~openstack.image.v2.member.Member` diff --git a/openstack/key_manager/v1/_proxy.py b/openstack/key_manager/v1/_proxy.py index 8ca13e854..f0f42912e 100644 --- a/openstack/key_manager/v1/_proxy.py +++ b/openstack/key_manager/v1/_proxy.py @@ -90,10 +90,9 @@ def update_container(self, container, **attrs): """Update a container :param container: Either the id of a container or a - :class:`~openstack.key_manager.v1.container.Container` - instance. - :attrs kwargs: The attributes to update on the container represented - by ``value``. + :class:`~openstack.key_manager.v1.container.Container` instance. + :param attrs: The attributes to update on the container represented + by ``container``. :returns: The updated container :rtype: :class:`~openstack.key_manager.v1.container.Container` @@ -170,10 +169,9 @@ def update_order(self, order, **attrs): """Update a order :param order: Either the id of a order or a - :class:`~openstack.key_manager.v1.order.Order` - instance. - :attrs kwargs: The attributes to update on the order represented - by ``value``. + :class:`~openstack.key_manager.v1.order.Order` instance. + :param attrs: The attributes to update on the order represented + by ``order``. :returns: The updated order :rtype: :class:`~openstack.key_manager.v1.order.Order` @@ -251,10 +249,9 @@ def update_secret(self, secret, **attrs): """Update a secret :param secret: Either the id of a secret or a - :class:`~openstack.key_manager.v1.secret.Secret` - instance. - :attrs kwargs: The attributes to update on the secret represented - by ``value``. + :class:`~openstack.key_manager.v1.secret.Secret` instance. + :param attrs: The attributes to update on the secret represented + by ``secret``. :returns: The updated secret :rtype: :class:`~openstack.key_manager.v1.secret.Secret` diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 0542ea241..c9a87f602 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -564,11 +564,10 @@ def get_flavor(self, flavor): def update_flavor(self, flavor, **attrs): """Update a network service flavor - :param flavor: - Either the id of a flavor or a + :param flavor: Either the id of a flavor or a :class:`~openstack.network.v2.flavor.Flavor` instance. - :attrs kwargs: The attributes to update on the flavor represented - by ``value``. + :param attrs: The attributes to update on the flavor represented + by ``flavor``. :returns: The updated flavor :rtype: :class:`~openstack.network.v2.flavor.Flavor` @@ -1812,12 +1811,12 @@ def network_segment_ranges(self, **query): def update_network_segment_range(self, network_segment_range, **attrs): """Update a network segment range - :param network_segment_range: Either the id of a network segment range + :param network_segment_range: Either the ID of a network segment range or a :class:`~openstack.network.v2._network_segment_range.NetworkSegmentRange` instance. - :attrs kwargs: The attributes to update on the network segment range - represented by ``value``. + :param attrs: The attributes to update on the network segment range + represented by ``network_segment_range``. :returns: The updated network segment range :rtype: @@ -2274,8 +2273,9 @@ def qos_bandwidth_limit_rules(self, qos_policy, **query): return self._list(_qos_bandwidth_limit_rule.QoSBandwidthLimitRule, qos_policy_id=policy.id, **query) - def update_qos_bandwidth_limit_rule(self, qos_rule, qos_policy, - **attrs): + def update_qos_bandwidth_limit_rule( + self, qos_rule, qos_policy, **attrs, + ): """Update a bandwidth limit rule :param qos_rule: Either the id of a bandwidth limit rule or a @@ -2284,8 +2284,8 @@ def update_qos_bandwidth_limit_rule(self, qos_rule, qos_policy, :param qos_policy: The value can be the ID of the QoS policy that the rule belongs or a :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. - :attrs kwargs: The attributes to update on the bandwidth limit rule - represented by ``value``. + :param attrs: The attributes to update on the bandwidth limit rule + represented by ``qos_rule``. :returns: The updated minimum bandwidth rule :rtype: @@ -2407,8 +2407,8 @@ def update_qos_dscp_marking_rule(self, qos_rule, qos_policy, **attrs): :param qos_policy: The value can be the ID of the QoS policy that the rule belongs or a :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. - :attrs kwargs: The attributes to update on the QoS DSCP marking rule - represented by ``value``. + :param attrs: The attributes to update on the QoS DSCP marking rule + represented by ``qos_rule``. :returns: The updated QoS DSCP marking rule :rtype: @@ -2535,8 +2535,8 @@ def update_qos_minimum_bandwidth_rule(self, qos_rule, qos_policy, rule belongs or a :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. - :attrs kwargs: The attributes to update on the minimum bandwidth rule - represented by ``value``. + :param attrs: The attributes to update on the minimum bandwidth rule + represented by ``qos_rule``. :returns: The updated minimum bandwidth rule :rtype: @@ -2661,17 +2661,20 @@ def update_qos_minimum_packet_rate_rule(self, qos_rule, qos_policy, :param qos_policy: The value can be the ID of the QoS policy that the rule belongs or a :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. - :attrs kwargs: The attributes to update on the minimum packet rate rule - represented by ``value``. + :param attrs: The attributes to update on the minimum packet rate rule + represented by ``qos_rule``. :returns: The updated minimum packet rate rule :rtype: :class:`~openstack.network.v2.qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) - return self._update(_qos_minimum_packet_rate_rule. - QoSMinimumPacketRateRule, qos_rule, - qos_policy_id=policy.id, **attrs) + return self._update( + _qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule, + qos_rule, + qos_policy_id=policy.id, + **attrs, + ) def create_qos_policy(self, **attrs): """Create a new QoS policy from attributes @@ -2753,10 +2756,9 @@ def update_qos_policy(self, qos_policy, **attrs): """Update a QoS policy :param qos_policy: Either the id of a QoS policy or a - :class:`~openstack.network.v2.qos_policy.QoSPolicy` - instance. - :attrs kwargs: The attributes to update on the QoS policy represented - by ``value``. + :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. + :param attrs: The attributes to update on the QoS policy represented + by ``qos_policy``. :returns: The updated QoS policy :rtype: :class:`~openstack.network.v2.qos_policy.QoSPolicy` @@ -3926,10 +3928,9 @@ def update_segment(self, segment, **attrs): """Update a segment :param segment: Either the id of a segment or a - :class:`~openstack.network.v2.segment.Segment` - instance. - :attrs kwargs: The attributes to update on the segment represented - by ``value``. + :class:`~openstack.network.v2.segment.Segment` instance. + :param attrs: The attributes to update on the segment represented + by ``segment``. :returns: The update segment :rtype: :class:`~openstack.network.v2.segment.Segment` @@ -4033,8 +4034,8 @@ def update_service_profile(self, service_profile, **attrs): :param service_profile: Either the id of a service profile or a :class:`~openstack.network.v2.service_profile.ServiceProfile` instance. - :attrs kwargs: The attributes to update on the service profile - represented by ``value``. + :param attrs: The attributes to update on the service profile + represented by ``service_profile``. :returns: The updated service profile :rtype: :class:`~openstack.network.v2.service_profile.ServiceProfile` @@ -4968,8 +4969,8 @@ def update_floating_ip_port_forwarding(self, floating_ip, port_forwarding, :param port_forwarding: Either the id of a floating ip port forwarding or a :class:`~openstack.network.v2.port_forwarding.PortForwarding`instance. - :attrs kwargs: The attributes to update on the floating ip port - forwarding represented by ``value``. + :param attrs: The attributes to update on the floating ip port + forwarding represented by ``floating_ip``. :returns: The updated floating ip port forwarding :rtype: :class:`~openstack.network.v2.port_forwarding.PortForwarding` @@ -5038,8 +5039,8 @@ def update_conntrack_helper(self, conntrack_helper, router, **attrs): instance. :param router: The value can be the ID of a Router or a :class:`~openstack.network.v2.router.Router` instance. - :attrs kwargs: The attributes to update on the L3 conntrack helper - represented by ``value``. + :param attrs: The attributes to update on the L3 conntrack helper + represented by ``conntrack_helper``. :returns: The updated conntrack helper :rtype: diff --git a/openstack/placement/v1/_proxy.py b/openstack/placement/v1/_proxy.py index a8ac8f26a..ff99a7374 100644 --- a/openstack/placement/v1/_proxy.py +++ b/openstack/placement/v1/_proxy.py @@ -59,7 +59,7 @@ def update_resource_class(self, resource_class, **attrs): class or an :class:`~openstack.placement.v1.resource_class.ResourceClass`, instance. - :attrs kwargs: The attributes to update on the resource class + :param attrs: The attributes to update on the resource class represented by ``resource_class``. :returns: The updated resource class @@ -138,7 +138,7 @@ def update_resource_provider(self, resource_provider, **attrs): provider or an :class:`~openstack.placement.v1.resource_provider.ResourceProvider`, instance. - :attrs kwargs: The attributes to update on the resource provider + :param attrs: The attributes to update on the resource provider represented by ``resource_provider``. :returns: The updated resource provider From 29592aeb9caa35ee9026070751e0c3efc9490ab7 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 17 Nov 2021 18:26:18 +0000 Subject: [PATCH 3097/3836] resource: Reformat calls to request This makes things a little easier to grasp as we attempt to decompose the Resource object. Change-Id: Icd73b2ac18d783254d882a3ecd538c086e86f179 Signed-off-by: Stephen Finucane --- openstack/resource.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/openstack/resource.py b/openstack/resource.py index 99ae58e1c..8e8916c8d 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1250,7 +1250,9 @@ def _translate_response(self, response, has_body=None, error_message=None): """ if has_body is None: has_body = self.has_body + exceptions.raise_from_response(response, error_message=error_message) + if has_body: try: body = response.json() @@ -1622,7 +1624,7 @@ def fetch( raise exceptions.MethodNotSupported(self, 'fetch') request = self._prepare_request( - requires_id=requires_id, base_path=base_path + requires_id=requires_id, base_path=base_path, ) session = self._get_session(session) if microversion is None: @@ -1707,6 +1709,9 @@ def commit( :raises: :exc:`~openstack.exceptions.MethodNotSupported` if :data:`Resource.allow_commit` is not set to ``True``. """ + if not self.allow_commit: + raise exceptions.MethodNotSupported(self, 'commit') + # The id cannot be dirty for an commit self._body._dirty.discard("id") @@ -1714,16 +1719,15 @@ def commit( if not self.requires_commit: return self - if not self.allow_commit: - raise exceptions.MethodNotSupported(self, 'commit') - # Avoid providing patch unconditionally to avoid breaking subclasses # without it. if self.commit_jsonpatch: kwargs['patch'] = True request = self._prepare_request( - prepend_key=prepend_key, base_path=base_path, **kwargs + prepend_key=prepend_key, + base_path=base_path, + **kwargs, ) if microversion is None: microversion = self._get_microversion(session, action='commit') @@ -1774,6 +1778,7 @@ def _commit( self.microversion = microversion self._translate_response(response, has_body=has_body) + return self def _convert_patch(self, patch): @@ -1838,6 +1843,9 @@ def patch( :raises: :exc:`~openstack.exceptions.MethodNotSupported` if :data:`Resource.allow_patch` is not set to ``True``. """ + if not self.allow_patch: + raise exceptions.MethodNotSupported(self, 'patch') + # The id cannot be dirty for an commit self._body._dirty.discard("id") @@ -1845,11 +1853,10 @@ def patch( if not patch and not self.requires_commit: return self - if not self.allow_patch: - raise exceptions.MethodNotSupported(self, 'patch') - request = self._prepare_request( - prepend_key=prepend_key, base_path=base_path, patch=True + prepend_key=prepend_key, + base_path=base_path, + patch=True, ) if microversion is None: microversion = self._get_microversion(session, action='patch') @@ -1901,7 +1908,9 @@ def _raw_delete(self, session, microversion=None, **kwargs): microversion = self._get_microversion(session, action='delete') return session.delete( - request.url, headers=request.headers, microversion=microversion + request.url, + headers=request.headers, + microversion=microversion, ) @classmethod From bf6563e97f3e97cc97ac9d680c0ffdb5370df1c3 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 30 Jun 2022 12:23:33 +0100 Subject: [PATCH 3098/3836] block storage: Add update_volume proxy method Noticed during a recent email exchange [1]. [1] http://lists.openstack.org/pipermail/openstack-discuss/2022-June/029325.html Change-Id: If67d0d2a01f691cc837a3f005711638056e735ec Signed-off-by: Stephen Finucane --- doc/source/user/proxies/block_storage_v3.rst | 4 +- openstack/block_storage/v3/_proxy.py | 12 ++++ .../block_storage/v3/test_volume.py | 61 +++++++++++++------ .../notes/volume-update-876e6540c8471440.yaml | 4 ++ 4 files changed, 59 insertions(+), 22 deletions(-) create mode 100644 releasenotes/notes/volume-update-876e6540c8471440.yaml diff --git a/doc/source/user/proxies/block_storage_v3.rst b/doc/source/user/proxies/block_storage_v3.rst index 65b5bb919..d91c5ea5e 100644 --- a/doc/source/user/proxies/block_storage_v3.rst +++ b/doc/source/user/proxies/block_storage_v3.rst @@ -17,8 +17,8 @@ Volume Operations .. autoclass:: openstack.block_storage.v3._proxy.Proxy :noindex: - :members: create_volume, delete_volume, get_volume, find_volume, - volumes, get_volume_metadata, set_volume_metadata, + :members: create_volume, delete_volume, update_volume, get_volume, + find_volume, volumes, get_volume_metadata, set_volume_metadata, delete_volume_metadata, extend_volume, set_volume_readonly, retype_volume, set_volume_bootable_status, reset_volume_status, revert_volume_to_snapshot, attach_volume, detach_volume, diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 0da1d97e9..3643113ea 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -519,6 +519,18 @@ def delete_volume(self, volume, ignore_missing=True, force=False): volume = self._get_resource(_volume.Volume, volume) volume.force_delete(self) + def update_volume(self, volume, **attrs): + """Update a volume + + :param volume: Either the ID of a volume or a + :class:`~openstack.block_storage.v3.volume.Volume` instance. + :param dict attrs: The attributes to update on the volume. + + :returns: The updated volume + :rtype: :class:`~openstack.block_storage.v3.volume.Volume` + """ + return self._update(_volume.Volume, volume, **attrs) + def get_volume_metadata(self, volume): """Return a dictionary of metadata for a volume diff --git a/openstack/tests/functional/block_storage/v3/test_volume.py b/openstack/tests/functional/block_storage/v3/test_volume.py index 155461aca..c38ca1e6a 100644 --- a/openstack/tests/functional/block_storage/v3/test_volume.py +++ b/openstack/tests/functional/block_storage/v3/test_volume.py @@ -17,34 +17,55 @@ class TestVolume(base.BaseBlockStorageTest): def setUp(self): - super(TestVolume, self).setUp() + super().setUp() if not self.user_cloud.has_service('block-storage'): self.skipTest('block-storage service not supported by cloud') - self.VOLUME_NAME = self.getUniqueString() - self.VOLUME_ID = None + volume_name = self.getUniqueString() - volume = self.user_cloud.block_storage.create_volume( - name=self.VOLUME_NAME, - size=1) + self.volume = self.user_cloud.block_storage.create_volume( + name=volume_name, + size=1, + ) self.user_cloud.block_storage.wait_for_status( - volume, + self.volume, status='available', failures=['error'], interval=2, - wait=self._wait_for_timeout) - assert isinstance(volume, _volume.Volume) - self.assertEqual(self.VOLUME_NAME, volume.name) - self.VOLUME_ID = volume.id + wait=self._wait_for_timeout, + ) + self.assertIsInstance(self.volume, _volume.Volume) + self.assertEqual(volume_name, self.volume.name) def tearDown(self): - sot = self.user_cloud.block_storage.delete_volume( - self.VOLUME_ID, - ignore_missing=False) - self.assertIsNone(sot) - super(TestVolume, self).tearDown() - - def test_get(self): - sot = self.user_cloud.block_storage.get_volume(self.VOLUME_ID) - self.assertEqual(self.VOLUME_NAME, sot.name) + self.user_cloud.block_storage.delete_volume(self.volume) + super().tearDown() + + def test_volume(self): + # get + volume = self.user_cloud.block_storage.get_volume(self.volume.id) + self.assertEqual(self.volume.name, volume.name) + + # find + volume = self.user_cloud.block_storage.find_volume(self.volume.name) + self.assertEqual(self.volume.id, volume.id) + + # list + volumes = self.user_cloud.block_storage.volumes() + # other tests may have created volumes so we don't assert that this is + # the *only* volume present + self.assertIn(self.volume.id, {v.id for v in volumes}) + + # update + volume_name = self.getUniqueString() + volume_description = self.getUniqueString() + volume = self.user_cloud.block_storage.update_volume( + self.volume, + name=volume_name, + description=volume_description, + ) + self.assertIsInstance(volume, _volume.Volume) + volume = self.user_cloud.block_storage.get_volume(self.volume.id) + self.assertEqual(volume_name, volume.name) + self.assertEqual(volume_description, volume.description) diff --git a/releasenotes/notes/volume-update-876e6540c8471440.yaml b/releasenotes/notes/volume-update-876e6540c8471440.yaml new file mode 100644 index 000000000..18ac0ed22 --- /dev/null +++ b/releasenotes/notes/volume-update-876e6540c8471440.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Added ``update_volume`` to the block storage proxy. From 68cf49d80677c0b657f3586cc01cfb975b4ae4c1 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 20 Dec 2021 12:59:22 +0000 Subject: [PATCH 3099/3836] volume: Trivial docstring fixes to 'wait_for_status' Fix the docstring for this method so that we include info on expected types. Change-Id: Id5f0e50d7c789cf79b120b7da63aee8446321981 Signed-off-by: Stephen Finucane --- openstack/block_storage/v3/_proxy.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index c2436514a..7210ea58f 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -1350,16 +1350,16 @@ def extensions(self): return self._list(_extension.Extension) # ====== UTILS ====== - def wait_for_status(self, res, status='available', failures=None, - interval=2, wait=120): + def wait_for_status( + self, res, status='available', failures=None, interval=2, wait=120, + ): """Wait for a resource to be in a particular status. :param res: The resource to wait on to reach the specified status. The resource must have a ``status`` attribute. :type resource: A :class:`~openstack.resource.Resource` object. - :param status: Desired status. - :param failures: Statuses that would be interpreted as failures. - :type failures: :py:class:`list` + :param str status: Desired status. + :param list failures: Statuses that would be interpreted as failures. :param interval: Number of seconds to wait before to consecutive checks. Default to 2. :param wait: Maximum number of seconds to wait before the change. @@ -1381,9 +1381,9 @@ def wait_for_delete(self, res, interval=2, wait=120): :param res: The resource to wait on to be deleted. :type resource: A :class:`~openstack.resource.Resource` object. - :param interval: Number of seconds to wait before to consecutive + :param int interval: Number of seconds to wait before two consecutive checks. Default to 2. - :param wait: Maximum number of seconds to wait before the change. + :param int wait: Maximum number of seconds to wait before the change. Default to 120. :returns: The resource is returned on success. :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition From 013795500c3176f5f4130740bbbf75f2010f64e9 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 9 Dec 2021 17:34:41 +0000 Subject: [PATCH 3100/3836] compute: Add support for triggering crash dumps Add support for compute API microversion 2.17, which allowed admins to trigger a crash dump in a running server. Change-Id: Ie0409f11f2a2b03044dfb504748e0177cae0d6b0 Signed-off-by: Stephen Finucane --- openstack/compute/v2/_proxy.py | 17 ++++++++++++++++ openstack/compute/v2/server.py | 20 +++++++++++-------- openstack/tests/unit/compute/v2/test_proxy.py | 7 +++++++ .../tests/unit/compute/v2/test_server.py | 12 +++++++++++ ...te-microversion-2-17-b05cb87580b8d56a.yaml | 6 ++++++ 5 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/compute-microversion-2-17-b05cb87580b8d56a.yaml diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index f9a56cf50..48072b7b5 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -1011,6 +1011,23 @@ def unshelve_server(self, server): server = self._get_resource(_server.Server, server) server.unshelve(self) + def trigger_server_crash_dump(self, server): + """Trigger a crash dump in a server. + + When a server starts behaving oddly at a fundamental level, it maybe be + useful to get a kernel level crash dump to debug further. The crash + dump action forces a crash dump followed by a system reboot of the + server. Once the server comes back online, you can find a Kernel Crash + Dump file in a certain location of the filesystem. For example, for + Ubuntu you can find it in the /var/crash directory. + + :param server: Either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance. + :returns: None + """ + server = self._get_resource(_server.Server, server) + server.trigger_crash_dump(self) + # ========== Server security groups ========== def fetch_server_security_groups(self, server): diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 8a16df0c0..6fb688cc0 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -488,6 +488,10 @@ def migrate(self, session): body = {"migrate": None} self._action(session, body) + def trigger_crash_dump(self, session): + body = {"trigger_crash_dump": None} + self._action(session, body) + def get_console_output(self, session, length=None): body = {"os-getConsoleOutput": {}} if length is not None: @@ -495,6 +499,14 @@ def get_console_output(self, session, length=None): resp = self._action(session, body) return resp.json() + def get_console_url(self, session, console_type): + action = CONSOLE_TYPE_ACTION_MAPPING.get(console_type) + if not action: + raise ValueError("Unsupported console type %s" % console_type) + body = {action: {'type': console_type}} + resp = self._action(session, body) + return resp.json().get('console') + def live_migrate(self, session, host, force, block_migration, disk_over_commit=False): if utils.supports_microversion(session, '2.30'): @@ -514,14 +526,6 @@ def live_migrate(self, session, host, force, block_migration, block_migration=block_migration, disk_over_commit=disk_over_commit) - def get_console_url(self, session, console_type): - action = CONSOLE_TYPE_ACTION_MAPPING.get(console_type) - if not action: - raise ValueError("Unsupported console type %s" % console_type) - body = {action: {'type': console_type}} - resp = self._action(session, body) - return resp.json().get('console') - def _live_migrate_30(self, session, host, force, block_migration): microversion = '2.30' body = {'host': None} diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 4f6a4f60c..172fd198c 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -921,6 +921,13 @@ def test_server_unshelve(self): method_args=["value"], expected_args=[self.proxy]) + def test_server_trigger_dump(self): + self._verify( + "openstack.compute.v2.server.Server.trigger_crash_dump", + self.proxy.trigger_server_crash_dump, + method_args=["value"], + expected_args=[self.proxy]) + def test_get_server_output(self): self._verify( "openstack.compute.v2.server.Server.get_console_output", diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index f677e21d5..85adacc92 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -820,6 +820,18 @@ def test_migrate(self): self.sess.post.assert_called_with( url, json=body, headers=headers, microversion=None) + def test_trigger_crash_dump(self): + sot = server.Server(**EXAMPLE) + + res = sot.trigger_crash_dump(self.sess) + + self.assertIsNone(res) + url = 'servers/IDENTIFIER/action' + body = {'trigger_crash_dump': None} + headers = {'Accept': ''} + self.sess.post.assert_called_with( + url, json=body, headers=headers, microversion=None) + def test_get_console_output(self): sot = server.Server(**EXAMPLE) diff --git a/releasenotes/notes/compute-microversion-2-17-b05cb87580b8d56a.yaml b/releasenotes/notes/compute-microversion-2-17-b05cb87580b8d56a.yaml new file mode 100644 index 000000000..e8f8c5107 --- /dev/null +++ b/releasenotes/notes/compute-microversion-2-17-b05cb87580b8d56a.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add support for Compute API microversion 2.17, which allows admins to + trigger a crash dump for a server. This can be useful for debugging + misbehaving guests. From 41c39a484886770ac9e39e27e74190f60a93411d Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Mon, 11 Jul 2022 15:35:37 +0200 Subject: [PATCH 3101/3836] Migrate register_machine to use the proxy layer The cloud layer now runs cleaning by default, which can be skipped by passing provision_state=enroll or manageable. As an upside, it now supports all fields in newer microversions. Change-Id: I03f01719ddf0e1aef60a9fcc92fe327ce8c8bb96 --- openstack/cloud/_baremetal.py | 199 ++++++------------ .../tests/unit/cloud/test_baremetal_node.py | 172 ++++++++------- .../register-machine-72ac3e65a1ed55b1.yaml | 6 + 3 files changed, 173 insertions(+), 204 deletions(-) create mode 100644 releasenotes/notes/register-machine-72ac3e65a1ed55b1.yaml diff --git a/openstack/cloud/_baremetal.py b/openstack/cloud/_baremetal.py index 418c1c00c..f20e6d6bf 100644 --- a/openstack/cloud/_baremetal.py +++ b/openstack/cloud/_baremetal.py @@ -13,32 +13,19 @@ # import types so that we can reference ListType in sphinx param declarations. # We can't just use list, because sphinx gets confused by # openstack.resource.Resource.list and openstack.resource2.Resource.list + +import contextlib +import sys import types # noqa import warnings import jsonpatch -from openstack.cloud import _utils from openstack.cloud import exc -from openstack import utils class BaremetalCloudMixin: - @property - def _baremetal_client(self): - if 'baremetal' not in self._raw_clients: - client = self._get_raw_client('baremetal') - # Do this to force version discovery. We need to do that, because - # the endpoint-override trick we do for neutron because - # ironicclient just appends a /v1 won't work and will break - # keystoneauth - because ironic's versioned discovery endpoint - # is non-compliant and doesn't return an actual version dict. - client = self._get_versioned_client( - 'baremetal', min_version=1, max_version='1.latest') - self._raw_clients['baremetal'] = client - return self._raw_clients['baremetal'] - def list_nics(self): """Return a list of all bare metal ports.""" return list(self.baremetal.ports(details=True)) @@ -157,8 +144,24 @@ def inspect_machine(self, name_or_id, wait=False, timeout=3600): return node + @contextlib.contextmanager + def _delete_node_on_error(self, node): + try: + yield + except Exception as exc: + self.log.debug("cleaning up node %s because of an error: %s", + node.id, exc) + tb = sys.exc_info()[2] + try: + self.baremetal.delete_node(node) + except Exception: + self.log.debug("could not remove node %s", node.id, + exc_info=True) + raise exc.with_traceback(tb) + def register_machine(self, nics, wait=False, timeout=3600, - lock_timeout=600, **kwargs): + lock_timeout=600, provision_state='available', + **kwargs): """Register Baremetal with Ironic Allows for the registration of Baremetal nodes with Ironic @@ -172,8 +175,9 @@ def register_machine(self, nics, wait=False, timeout=3600, created are deleted, and the node is removed from Ironic. :param nics: - An array of MAC addresses that represent the - network interfaces for the node to be created. + An array of ports that represent the network interfaces for the + node to be created. The ports are created after the node is + enrolled but before it goes through cleaning. Example:: @@ -193,131 +197,62 @@ def register_machine(self, nics, wait=False, timeout=3600, :param lock_timeout: Integer value, defaulting to 600 seconds, for locks to clear. + :param provision_state: The expected provision state, one of "enroll" + "manageable" or "available". Using "available" results in automated + cleaning. + :param kwargs: Key value pairs to be passed to the Ironic API, - including uuid, name, chassis_uuid, driver_info, parameters. + including uuid, name, chassis_uuid, driver_info, properties. :raises: OpenStackCloudException on operation error. :rtype: :class:`~openstack.baremetal.v1.node.Node`. :returns: Current state of the node. """ - - msg = ("Baremetal machine node failed to be created.") - port_msg = ("Baremetal machine port failed to be created.") - - url = '/nodes' - # TODO(TheJulia): At some point we need to figure out how to - # handle data across when the requestor is defining newer items - # with the older api. - machine = self._baremetal_client.post(url, - json=kwargs, - error_message=msg, - microversion="1.6") - - created_nics = [] - try: - for row in nics: - payload = {'address': row['mac'], - 'node_uuid': machine['uuid']} - nic = self._baremetal_client.post('/ports', - json=payload, - error_message=port_msg) - created_nics.append(nic['uuid']) - - except Exception as e: - self.log.debug("ironic NIC registration failed", exc_info=True) - # TODO(mordred) Handle failures here + if provision_state not in ('enroll', 'manageable', 'available'): + raise ValueError('Initial provision state must be enroll, ' + 'manageable or available, got %s' + % provision_state) + + # Available is tricky: it cannot be directly requested on newer API + # versions, we need to go through cleaning. But we cannot go through + # cleaning until we create ports. + if provision_state != 'available': + kwargs['provision_state'] = 'enroll' + machine = self.baremetal.create_node(**kwargs) + + with self._delete_node_on_error(machine): + # Making a node at least manageable + if (machine.provision_state == 'enroll' + and provision_state != 'enroll'): + machine = self.baremetal.set_node_provision_state( + machine, 'manage', wait=True, timeout=timeout) + machine = self.baremetal.wait_for_node_reservation( + machine, timeout=lock_timeout) + + # Create NICs before trying to run cleaning + created_nics = [] try: + for row in nics: + address = row.pop('mac') + nic = self.baremetal.create_port(node_id=machine.id, + address=address, + **row) + created_nics.append(nic.id) + + except Exception: for uuid in created_nics: try: - port_url = '/ports/{uuid}'.format(uuid=uuid) - # NOTE(TheJulia): Added in hope that it is logged. - port_msg = ('Failed to delete port {port} for node ' - '{node}').format(port=uuid, - node=machine['uuid']) - self._baremetal_client.delete(port_url, - error_message=port_msg) + self.baremetal.delete_port(uuid) except Exception: pass - finally: - version = "1.6" - msg = "Baremetal machine failed to be deleted." - url = '/nodes/{node_id}'.format( - node_id=machine['uuid']) - self._baremetal_client.delete(url, - error_message=msg, - microversion=version) - raise exc.OpenStackCloudException( - "Error registering NICs with the baremetal service: %s" - % str(e)) - - with _utils.shade_exceptions( - "Error transitioning node to available state"): - if wait: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for node transition to " - "available state"): - - machine = self.get_machine(machine['uuid']) - - # Note(TheJulia): Per the Ironic state code, a node - # that fails returns to enroll state, which means a failed - # node cannot be determined at this point in time. - if machine['provision_state'] in ['enroll']: - self.node_set_provision_state( - machine['uuid'], 'manage') - elif machine['provision_state'] in ['manageable']: - self.node_set_provision_state( - machine['uuid'], 'provide') - elif machine['last_error'] is not None: - raise exc.OpenStackCloudException( - "Machine encountered a failure: %s" - % machine['last_error']) - - # Note(TheJulia): Earlier versions of Ironic default to - # None and later versions default to available up until - # the introduction of enroll state. - # Note(TheJulia): The node will transition through - # cleaning if it is enabled, and we will wait for - # completion. - elif machine['provision_state'] in ['available', None]: - break - - else: - if machine['provision_state'] in ['enroll']: - self.node_set_provision_state(machine['uuid'], 'manage') - # Note(TheJulia): We need to wait for the lock to clear - # before we attempt to set the machine into provide state - # which allows for the transition to available. - for count in utils.iterate_timeout( - lock_timeout, - "Timeout waiting for reservation to clear " - "before setting provide state"): - machine = self.get_machine(machine['uuid']) - if (machine['reservation'] is None - and machine['provision_state'] != 'enroll'): - # NOTE(TheJulia): In this case, the node has - # has moved on from the previous state and is - # likely not being verified, as no lock is - # present on the node. - self.node_set_provision_state( - machine['uuid'], 'provide') - machine = self.get_machine(machine['uuid']) - break - - elif machine['provision_state'] in [ - 'cleaning', - 'available']: - break - - elif machine['last_error'] is not None: - raise exc.OpenStackCloudException( - "Machine encountered a failure: %s" - % machine['last_error']) - if not isinstance(machine, str): - return self._normalize_machine(machine) - else: + raise + + if (machine.provision_state != 'available' + and provision_state == 'available'): + machine = self.baremetal.set_node_provision_state( + machine, 'provide', wait=wait, timeout=timeout) + return machine def unregister_machine(self, nics, uuid, wait=None, timeout=600): diff --git a/openstack/tests/unit/cloud/test_baremetal_node.py b/openstack/tests/unit/cloud/test_baremetal_node.py index 85e7d7983..c9c2e09e1 100644 --- a/openstack/tests/unit/cloud/test_baremetal_node.py +++ b/openstack/tests/unit/cloud/test_baremetal_node.py @@ -1069,7 +1069,6 @@ def test_register_machine(self): # in testing creation. Surely this hsould be a helper # or something. We should fix this. node_to_post = { - 'chassis_uuid': None, 'driver': None, 'driver_info': None, 'name': self.fake_baremetal_node['name'], @@ -1095,7 +1094,8 @@ def test_register_machine(self): ]) return_value = self.cloud.register_machine(nics, **node_to_post) - self.assertDictEqual(self.fake_baremetal_node, return_value) + self.assertEqual(self.uuid, return_value.id) + self.assertSubdict(self.fake_baremetal_node, return_value) self.assert_calls() # TODO(TheJulia): We need to de-duplicate these tests. @@ -1125,13 +1125,6 @@ def test_register_machine_enroll(self): resource='nodes'), validate=dict(json=node_to_post), json=self.fake_baremetal_node), - dict( - method='POST', - uri=self.get_mock_url( - resource='ports'), - validate=dict(json={'address': mac_address, - 'node_uuid': node_uuid}), - json=self.fake_baremetal_port), dict( method='PUT', uri=self.get_mock_url( @@ -1146,11 +1139,12 @@ def test_register_machine_enroll(self): append=[self.fake_baremetal_node['uuid']]), json=manageable_node), dict( - method='GET', + method='POST', uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=manageable_node), + resource='ports'), + validate=dict(json={'address': mac_address, + 'node_uuid': node_uuid}), + json=self.fake_baremetal_port), dict( method='PUT', uri=self.get_mock_url( @@ -1164,19 +1158,7 @@ def test_register_machine_enroll(self): resource='nodes', append=[self.fake_baremetal_node['uuid']]), json=available_node), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=available_node), ]) - # NOTE(When we migrate to a newer microversion, this test - # may require revision. It was written for microversion - # ?1.13?, which accidently got reverted to 1.6 at one - # point during code being refactored soon after the - # change landed. Presently, with the lock at 1.6, - # this code is never used in the current code path. return_value = self.cloud.register_machine(nics, **node_to_post) self.assertSubdict(available_node, return_value) @@ -1205,19 +1187,6 @@ def test_register_machine_enroll_wait(self): resource='nodes'), validate=dict(json=node_to_post), json=self.fake_baremetal_node), - dict( - method='POST', - uri=self.get_mock_url( - resource='ports'), - validate=dict(json={'address': mac_address, - 'node_uuid': node_uuid}), - json=self.fake_baremetal_port), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), dict( method='PUT', uri=self.get_mock_url( @@ -1237,6 +1206,13 @@ def test_register_machine_enroll_wait(self): resource='nodes', append=[self.fake_baremetal_node['uuid']]), json=manageable_node), + dict( + method='POST', + uri=self.get_mock_url( + resource='ports'), + validate=dict(json={'address': mac_address, + 'node_uuid': node_uuid}), + json=self.fake_baremetal_port), dict( method='PUT', uri=self.get_mock_url( @@ -1249,7 +1225,7 @@ def test_register_machine_enroll_wait(self): uri=self.get_mock_url( resource='nodes', append=[self.fake_baremetal_node['uuid']]), - json=available_node), + json=manageable_node), dict( method='GET', uri=self.get_mock_url( @@ -1277,7 +1253,7 @@ def test_register_machine_enroll_failure(self): self.fake_baremetal_node['provision_state'] = 'enroll' failed_node = self.fake_baremetal_node.copy() failed_node['reservation'] = 'conductor0' - failed_node['provision_state'] = 'verifying' + failed_node['provision_state'] = 'enroll' failed_node['last_error'] = 'kaboom!' self.register_uris([ dict( @@ -1286,13 +1262,6 @@ def test_register_machine_enroll_failure(self): resource='nodes'), json=self.fake_baremetal_node, validate=dict(json=node_to_post)), - dict( - method='POST', - uri=self.get_mock_url( - resource='ports'), - validate=dict(json={'address': mac_address, - 'node_uuid': node_uuid}), - json=self.fake_baremetal_port), dict( method='PUT', uri=self.get_mock_url( @@ -1307,11 +1276,10 @@ def test_register_machine_enroll_failure(self): append=[self.fake_baremetal_node['uuid']]), json=failed_node), dict( - method='GET', + method='DELETE', uri=self.get_mock_url( resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=failed_node), + append=[self.fake_baremetal_node['uuid']])), ]) self.assertRaises( @@ -1343,13 +1311,6 @@ def test_register_machine_enroll_timeout(self): resource='nodes'), json=self.fake_baremetal_node, validate=dict(json=node_to_post)), - dict( - method='POST', - uri=self.get_mock_url( - resource='ports'), - validate=dict(json={'address': mac_address, - 'node_uuid': node_uuid}), - json=self.fake_baremetal_port), dict( method='PUT', uri=self.get_mock_url( @@ -1362,13 +1323,12 @@ def test_register_machine_enroll_timeout(self): uri=self.get_mock_url( resource='nodes', append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), + json=busy_node), dict( - method='GET', + method='DELETE', uri=self.get_mock_url( resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=busy_node), + append=[self.fake_baremetal_node['uuid']])), ]) # NOTE(TheJulia): This test shortcircuits the timeout loop # such that it executes only once. The very last returned @@ -1395,6 +1355,8 @@ def test_register_machine_enroll_timeout_wait(self): 'properties': None, 'uuid': node_uuid} self.fake_baremetal_node['provision_state'] = 'enroll' + manageable_node = self.fake_baremetal_node.copy() + manageable_node['provision_state'] = 'manageable' self.register_uris([ dict( method='POST', @@ -1403,31 +1365,43 @@ def test_register_machine_enroll_timeout_wait(self): json=self.fake_baremetal_node, validate=dict(json=node_to_post)), dict( - method='POST', + method='PUT', uri=self.get_mock_url( - resource='ports'), - validate=dict(json={'address': mac_address, - 'node_uuid': node_uuid}), - json=self.fake_baremetal_port), + resource='nodes', + append=[self.fake_baremetal_node['uuid'], + 'states', 'provision']), + validate=dict(json={'target': 'manage'})), dict( method='GET', uri=self.get_mock_url( resource='nodes', append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), + json=manageable_node), + dict( + method='POST', + uri=self.get_mock_url( + resource='ports'), + validate=dict(json={'address': mac_address, + 'node_uuid': node_uuid}), + json=self.fake_baremetal_port), dict( method='PUT', uri=self.get_mock_url( resource='nodes', append=[self.fake_baremetal_node['uuid'], 'states', 'provision']), - validate=dict(json={'target': 'manage'})), + validate=dict(json={'target': 'provide'})), dict( method='GET', uri=self.get_mock_url( resource='nodes', append=[self.fake_baremetal_node['uuid']]), json=self.fake_baremetal_node), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']])), ]) self.assertRaises( exc.OpenStackCloudException, @@ -1462,7 +1436,7 @@ def test_register_machine_port_create_failed(self): uri=self.get_mock_url( resource='ports'), status_code=400, - json={'error': 'invalid'}, + json={'error': 'no ports for you'}, validate=dict(json={'address': mac_address, 'node_uuid': node_uuid})), dict( @@ -1471,9 +1445,63 @@ def test_register_machine_port_create_failed(self): resource='nodes', append=[self.fake_baremetal_node['uuid']])), ]) - self.assertRaises(exc.OpenStackCloudException, - self.cloud.register_machine, - nics, **node_to_post) + self.assertRaisesRegex(exc.OpenStackCloudException, + 'no ports for you', + self.cloud.register_machine, + nics, **node_to_post) + + self.assert_calls() + + def test_register_machine_several_ports_create_failed(self): + mac_address = '00:01:02:03:04:05' + mac_address2 = mac_address[::-1] + nics = [{'mac': mac_address}, {'mac': mac_address2}] + node_uuid = self.fake_baremetal_node['uuid'] + node_to_post = { + 'chassis_uuid': None, + 'driver': None, + 'driver_info': None, + 'name': self.fake_baremetal_node['name'], + 'properties': None, + 'uuid': node_uuid} + self.fake_baremetal_node['provision_state'] = 'available' + self.register_uris([ + dict( + method='POST', + uri=self.get_mock_url( + resource='nodes'), + json=self.fake_baremetal_node, + validate=dict(json=node_to_post)), + dict( + method='POST', + uri=self.get_mock_url( + resource='ports'), + validate=dict(json={'address': mac_address, + 'node_uuid': node_uuid}), + json=self.fake_baremetal_port), + dict( + method='POST', + uri=self.get_mock_url( + resource='ports'), + status_code=400, + json={'error': 'no ports for you'}, + validate=dict(json={'address': mac_address2, + 'node_uuid': node_uuid})), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='ports', + append=[self.fake_baremetal_port['uuid']])), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']])), + ]) + self.assertRaisesRegex(exc.OpenStackCloudException, + 'no ports for you', + self.cloud.register_machine, + nics, **node_to_post) self.assert_calls() diff --git a/releasenotes/notes/register-machine-72ac3e65a1ed55b1.yaml b/releasenotes/notes/register-machine-72ac3e65a1ed55b1.yaml new file mode 100644 index 000000000..ba1eaaca6 --- /dev/null +++ b/releasenotes/notes/register-machine-72ac3e65a1ed55b1.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + The default behavior of the ``register_machine`` call has been modified to + run cleaning by default, if enabled in Ironic. You can pass + ``provision_state="enroll"/"manageable"`` to avoid it. From 75237720f698c37a7689a20f4f9cb83f9ffc7b2d Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Thu, 8 Sep 2022 18:30:35 +0200 Subject: [PATCH 3102/3836] Update register_machine to use the Ironic format for ports It's confusing to use two different formats across the SDK. Also allow a simple list of MAC addresses, since that's what most people presumably want. Change-Id: I47448850225f04f24fb0dfc6a1700b5e2c26a450 --- openstack/cloud/_baremetal.py | 35 ++++++++++++++----- .../tests/unit/cloud/test_baremetal_node.py | 20 ++++++----- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/openstack/cloud/_baremetal.py b/openstack/cloud/_baremetal.py index f20e6d6bf..0041e0990 100644 --- a/openstack/cloud/_baremetal.py +++ b/openstack/cloud/_baremetal.py @@ -24,6 +24,25 @@ from openstack.cloud import exc +def _normalize_port_list(nics): + ports = [] + for row in nics: + if isinstance(row, str): + address = row + row = {} + elif 'mac' in row: + address = row.pop('mac') + else: + try: + address = row.pop('address') + except KeyError: + raise TypeError( + "Either 'address' or 'mac' must be provided " + "for port %s" % row) + ports.append(dict(row, address=address)) + return ports + + class BaremetalCloudMixin: def list_nics(self): @@ -182,10 +201,12 @@ def register_machine(self, nics, wait=False, timeout=3600, Example:: [ - {'mac': 'aa:bb:cc:dd:ee:01'}, - {'mac': 'aa:bb:cc:dd:ee:02'} + {'address': 'aa:bb:cc:dd:ee:01'}, + {'address': 'aa:bb:cc:dd:ee:02'} ] + Alternatively, you can provide an array of MAC addresses. + :param wait: Boolean value, defaulting to false, to wait for the node to reach the available state where the node can be provisioned. It must be noted, when set to false, the method will still wait for @@ -233,11 +254,9 @@ def register_machine(self, nics, wait=False, timeout=3600, # Create NICs before trying to run cleaning created_nics = [] try: - for row in nics: - address = row.pop('mac') + for port in _normalize_port_list(nics): nic = self.baremetal.create_port(node_id=machine.id, - address=address, - **row) + **port) created_nics.append(nic.id) except Exception: @@ -295,9 +314,9 @@ def unregister_machine(self, nics, uuid, wait=None, timeout=600): "Error unregistering node '%s': Exception occured while" " waiting to be able to proceed: %s" % (machine['uuid'], e)) - for nic in nics: + for nic in _normalize_port_list(nics): try: - port = next(self.baremetal.ports(address=nic['mac'])) + port = next(self.baremetal.ports(address=nic['address'])) except StopIteration: continue self.baremetal.delete_port(port.id) diff --git a/openstack/tests/unit/cloud/test_baremetal_node.py b/openstack/tests/unit/cloud/test_baremetal_node.py index c9c2e09e1..888258bef 100644 --- a/openstack/tests/unit/cloud/test_baremetal_node.py +++ b/openstack/tests/unit/cloud/test_baremetal_node.py @@ -1063,7 +1063,7 @@ def test_deactivate_node(self): def test_register_machine(self): mac_address = '00:01:02:03:04:05' - nics = [{'mac': mac_address}] + nics = [{'address': mac_address}] node_uuid = self.fake_baremetal_node['uuid'] # TODO(TheJulia): There is a lot of duplication # in testing creation. Surely this hsould be a helper @@ -1104,7 +1104,7 @@ def test_register_machine(self): # accounted for newer microversions. def test_register_machine_enroll(self): mac_address = '00:01:02:03:04:05' - nics = [{'mac': mac_address}] + nics = [{'address': mac_address, 'pxe_enabled': False}] node_uuid = self.fake_baremetal_node['uuid'] node_to_post = { 'chassis_uuid': None, @@ -1143,7 +1143,8 @@ def test_register_machine_enroll(self): uri=self.get_mock_url( resource='ports'), validate=dict(json={'address': mac_address, - 'node_uuid': node_uuid}), + 'node_uuid': node_uuid, + 'pxe_enabled': False}), json=self.fake_baremetal_port), dict( method='PUT', @@ -1166,7 +1167,7 @@ def test_register_machine_enroll(self): def test_register_machine_enroll_wait(self): mac_address = self.fake_baremetal_port - nics = [{'mac': mac_address}] + nics = [{'address': mac_address}] node_uuid = self.fake_baremetal_node['uuid'] node_to_post = { 'chassis_uuid': None, @@ -1241,7 +1242,7 @@ def test_register_machine_enroll_wait(self): def test_register_machine_enroll_failure(self): mac_address = '00:01:02:03:04:05' - nics = [{'mac': mac_address}] + nics = [{'address': mac_address}] node_uuid = self.fake_baremetal_node['uuid'] node_to_post = { 'chassis_uuid': None, @@ -1291,7 +1292,7 @@ def test_register_machine_enroll_failure(self): def test_register_machine_enroll_timeout(self): mac_address = '00:01:02:03:04:05' - nics = [{'mac': mac_address}] + nics = [{'address': mac_address}] node_uuid = self.fake_baremetal_node['uuid'] node_to_post = { 'chassis_uuid': None, @@ -1345,7 +1346,7 @@ def test_register_machine_enroll_timeout(self): def test_register_machine_enroll_timeout_wait(self): mac_address = '00:01:02:03:04:05' - nics = [{'mac': mac_address}] + nics = [{'address': mac_address}] node_uuid = self.fake_baremetal_node['uuid'] node_to_post = { 'chassis_uuid': None, @@ -1414,7 +1415,7 @@ def test_register_machine_enroll_timeout_wait(self): def test_register_machine_port_create_failed(self): mac_address = '00:01:02:03:04:05' - nics = [{'mac': mac_address}] + nics = [{'address': mac_address}] node_uuid = self.fake_baremetal_node['uuid'] node_to_post = { 'chassis_uuid': None, @@ -1455,7 +1456,8 @@ def test_register_machine_port_create_failed(self): def test_register_machine_several_ports_create_failed(self): mac_address = '00:01:02:03:04:05' mac_address2 = mac_address[::-1] - nics = [{'mac': mac_address}, {'mac': mac_address2}] + # Verify a couple of ways to provide MACs + nics = [mac_address, {'mac': mac_address2}] node_uuid = self.fake_baremetal_node['uuid'] node_to_post = { 'chassis_uuid': None, From b87431ff4a450fe3107674339c4e302612c41f94 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 9 Sep 2022 11:21:24 +0000 Subject: [PATCH 3103/3836] Update master for stable/zed Add file to the reno documentation build to show release notes for stable/zed. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/zed. Sem-Ver: feature Change-Id: Idfbad58d6c2d3e7895632a9c557cf37baac4cf0d --- releasenotes/source/index.rst | 1 + releasenotes/source/zed.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/zed.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 5360eaa93..e9b2153b9 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ :maxdepth: 1 unreleased + zed yoga xena wallaby diff --git a/releasenotes/source/zed.rst b/releasenotes/source/zed.rst new file mode 100644 index 000000000..9608c05e4 --- /dev/null +++ b/releasenotes/source/zed.rst @@ -0,0 +1,6 @@ +======================== +Zed Series Release Notes +======================== + +.. release-notes:: + :branch: stable/zed From e1c59b63440a8d35a807ed1b1633a0a34f9300cf Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 9 Sep 2022 11:21:25 +0000 Subject: [PATCH 3104/3836] Add Python3 antelope unit tests This is an automatically generated patch to ensure unit testing is in place for all the of the tested runtimes for antelope. See also the PTI in governance [1]. [1]: https://governance.openstack.org/tc/reference/project-testing-interface.html Change-Id: I44ee171d1f5a4995c92175320737d5390d1d6b54 --- .zuul.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index 1e27a101d..645af4bf8 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -442,7 +442,7 @@ - project: templates: - check-requirements - - openstack-python3-zed-jobs + - openstack-python3-antelope-jobs - openstacksdk-functional-tips - openstacksdk-tox-tips - os-client-config-tox-tips From 1e74141cc380c471e7545c264bc27ce5c1cf315b Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 14 Sep 2022 13:11:41 +0100 Subject: [PATCH 3105/3836] docs: Add missing docs for proxy helper methods Change-Id: I3764b74f08294ea1768d09a59d355376996d3ae2 Signed-off-by: Stephen Finucane --- doc/source/user/proxies/block_storage_v2.rst | 7 +++++++ doc/source/user/proxies/compute.rst | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/doc/source/user/proxies/block_storage_v2.rst b/doc/source/user/proxies/block_storage_v2.rst index e3d650969..cd8260535 100644 --- a/doc/source/user/proxies/block_storage_v2.rst +++ b/doc/source/user/proxies/block_storage_v2.rst @@ -53,3 +53,10 @@ QuotaSet Operations :noindex: :members: get_quota_set, get_quota_set_defaults, revert_quota_set, update_quota_set + +Helpers +^^^^^^^ + +.. autoclass:: openstack.block_storage.v2._proxy.Proxy + :noindex: + :members: wait_for_status, wait_for_delete diff --git a/doc/source/user/proxies/compute.rst b/doc/source/user/proxies/compute.rst index ff9853375..698a134c2 100644 --- a/doc/source/user/proxies/compute.rst +++ b/doc/source/user/proxies/compute.rst @@ -172,3 +172,10 @@ Migration Operations .. autoclass:: openstack.compute.v2._proxy.Proxy :noindex: :members: migrations + +Helpers +^^^^^^^ + +.. autoclass:: openstack.compute.v2._proxy.Proxy + :noindex: + :members: wait_for_delete From 0425eb6c33cc69d3bea5a339c759768139cbc077 Mon Sep 17 00:00:00 2001 From: Snow Kim Date: Wed, 14 Sep 2022 22:42:19 +0900 Subject: [PATCH 3106/3836] workflow: Trivial fix doc title * Object Store Resources => Workflow Resources Change-Id: I98cb0162cf27f1cb1dbf8fa3e3c372a43038f77f --- doc/source/user/resources/workflow/index.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/user/resources/workflow/index.rst b/doc/source/user/resources/workflow/index.rst index 30221b15d..ee6e70e49 100644 --- a/doc/source/user/resources/workflow/index.rst +++ b/doc/source/user/resources/workflow/index.rst @@ -1,5 +1,5 @@ -Object Store Resources -====================== +Workflow Resources +================== .. toctree:: :maxdepth: 1 From 9b802d3fd183852cd083d8b7926ffa90f34e8f14 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 7 Sep 2022 11:44:51 +0100 Subject: [PATCH 3107/3836] image: Add support for other metadef namespace operations This is mostly the usual stuff with the exception of the 'update' operation, which is a little weird since the Glance API requires us to supply a 'namespace' argument even when the namespace isn't changing. To be honest, being able to change this argument at all is weird since it's effectively the ID but we won't get into that here :) Change-Id: I458c6e855598451b5203e5c7cde1b1623bd8e1cf Signed-off-by: Stephen Finucane --- doc/source/user/proxies/image_v2.rst | 10 +- openstack/image/v2/_proxy.py | 93 ++++++++++++++++++- openstack/image/v2/metadef_namespace.py | 44 +++++++-- openstack/tests/functional/image/v2/base.py | 26 ++++++ .../image/v2/test_metadef_namespace.py | 86 +++++++++++++++++ .../unit/image/v2/test_metadef_namespace.py | 36 ++++--- openstack/tests/unit/image/v2/test_proxy.py | 36 +++++++ 7 files changed, 312 insertions(+), 19 deletions(-) create mode 100644 openstack/tests/functional/image/v2/base.py create mode 100644 openstack/tests/functional/image/v2/test_metadef_namespace.py diff --git a/doc/source/user/proxies/image_v2.rst b/doc/source/user/proxies/image_v2.rst index 79a5a974b..cbd6feea2 100644 --- a/doc/source/user/proxies/image_v2.rst +++ b/doc/source/user/proxies/image_v2.rst @@ -58,4 +58,12 @@ Metadef Namespace Operations .. autoclass:: openstack.image.v2._proxy.Proxy :noindex: - :members: metadef_namespaces + :members: create_metadef_namespace, delete_metadef_namespace, + get_metadef_namespace, metadef_namespaces, update_metadef_namespace + +Helpers +^^^^^^^ + +.. autoclass:: openstack.image.v2._proxy.Proxy + :noindex: + :members: wait_for_delete diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 40351bc40..c747d1a17 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -672,15 +672,90 @@ def update_member(self, member, image, **attrs): image_id=image_id, **attrs) # ====== METADEF NAMESPACES ====== + def create_metadef_namespace(self, **attrs): + """Create a new metadef namespace from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` + comprised of the properties on the MetadefNamespace class. + + :returns: The results of metadef namespace creation + :rtype: :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` + """ + return self._create(_metadef_namespace.MetadefNamespace, **attrs) + + def delete_metadef_namespace(self, metadef_namespace, ignore_missing=True): + """Delete a metadef namespace + + :param metadef_namespace: The value can be either the name of a metadef + namespace or a + :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` + instance. + :param bool ignore_missing: When set to ``False``, + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the metadef namespace does not exist. + :returns: ``None`` + """ + self._delete( + _metadef_namespace.MetadefNamespace, + metadef_namespace, + ignore_missing=ignore_missing, + ) + + # NOTE(stephenfin): There is no 'find_metadef_namespace' since namespaces + # are identified by the namespace name, not an arbitrary UUID, meaning + # 'find_metadef_namespace' would be identical to 'get_metadef_namespace' + + def get_metadef_namespace(self, metadef_namespace): + """Get a single metadef namespace + + :param metadef_namespace: Either the name of a metadef namespace or an + :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` + instance. + + :returns: One + :class:`~~openstack.image.v2.metadef_namespace.MetadefNamespace` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource can be found. + """ + return self._get( + _metadef_namespace.MetadefNamespace, + metadef_namespace, + ) + def metadef_namespaces(self, **query): - """Get a info about image metadef namespaces + """Return a generator of metadef namespaces :returns: A generator object of metadef namespaces + :rtype: :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ return self._list(_metadef_namespace.MetadefNamespace, **query) + def update_metadef_namespace(self, metadef_namespace, **attrs): + """Update a server + + :param metadef_namespace: Either the name of a metadef namespace or an + :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` + instance. + :param attrs: The attributes to update on the metadef namespace + represented by ``metadef_namespace``. + + :returns: The updated metadef namespace + :rtype: :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` + """ + # rather annoyingly, Glance insists on us providing the 'namespace' + # argument, even if we're not changing it... + if 'namespace' not in attrs: + attrs['namespace'] = resource.Resource._get_id(metadef_namespace) + + return self._update( + _metadef_namespace.MetadefNamespace, + metadef_namespace, + **attrs, + ) + # ====== SCHEMAS ====== def get_images_schema(self): """Get images schema @@ -858,3 +933,19 @@ def get_import_info(self): when no resource can be found. """ return self._get(_si.Import, require_id=False) + + # ====== UTILS ====== + def wait_for_delete(self, res, interval=2, wait=120): + """Wait for a resource to be deleted. + + :param res: The resource to wait on to be deleted. + :type resource: A :class:`~openstack.resource.Resource` object. + :param interval: Number of seconds to wait before to consecutive + checks. Default to 2. + :param wait: Maximum number of seconds to wait before the change. + Default to 120. + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete(self, res, interval, wait) diff --git a/openstack/image/v2/metadef_namespace.py b/openstack/image/v2/metadef_namespace.py index 6a8d4522b..24dd90899 100644 --- a/openstack/image/v2/metadef_namespace.py +++ b/openstack/image/v2/metadef_namespace.py @@ -13,13 +13,15 @@ from openstack import resource -# TODO(schwicke): create and delete still need to be implemented class MetadefNamespace(resource.Resource): resources_key = 'namespaces' base_path = '/metadefs/namespaces' + allow_create = True allow_fetch = True + allow_commit = True allow_list = True + allow_delete = True _query_mapping = resource.QueryParameters( "limit", @@ -27,15 +29,45 @@ class MetadefNamespace(resource.Resource): "resource_types", "sort_dir", "sort_key", - "visibility" + "visibility", ) + created_at = resource.Body('created_at') description = resource.Body('description') display_name = resource.Body('display_name') is_protected = resource.Body('protected', type=bool) - namespace = resource.Body('namespace') + namespace = resource.Body('namespace', alternate_id=True) owner = resource.Body('owner') - resource_type_associations = resource.Body('resource_type_associations', - type=list, - list_type=dict) + resource_type_associations = resource.Body( + 'resource_type_associations', + type=list, + list_type=dict, + ) + updated_at = resource.Body('updated_at') visibility = resource.Body('visibility') + + def _commit( + self, + session, + request, + method, + microversion, + has_body=True, + retry_on_conflict=None, + ): + # Rather annoyingly, Glance insists on us providing the 'namespace' + # argument, even if we're not changing it. We need to add this here + # since it won't be included if Resource.commit thinks its unchanged + # TODO(stephenfin): Eventually we could indicate attributes that are + # required in the body on update, like the 'requires_id' and + # 'create_requires_id' do for the ID in the URL + request.body['namespace'] = self.namespace + + return super()._commit( + session, + request, + method, + microversion, + has_body=True, + retry_on_conflict=None, + ) diff --git a/openstack/tests/functional/image/v2/base.py b/openstack/tests/functional/image/v2/base.py new file mode 100644 index 000000000..f9762d3e4 --- /dev/null +++ b/openstack/tests/functional/image/v2/base.py @@ -0,0 +1,26 @@ +# 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.tests.functional import base + + +class BaseImageTest(base.BaseFunctionalTest): + + _wait_for_timeout_key = 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_IMAGE' + + def setUp(self): + super().setUp() + self._set_user_cloud(image_api_version='2') + self._set_operator_cloud(image_api_version='2') + + if not self.user_cloud.has_service('image', '2'): + self.skipTest('image service not supported by cloud') diff --git a/openstack/tests/functional/image/v2/test_metadef_namespace.py b/openstack/tests/functional/image/v2/test_metadef_namespace.py new file mode 100644 index 000000000..02273cdb8 --- /dev/null +++ b/openstack/tests/functional/image/v2/test_metadef_namespace.py @@ -0,0 +1,86 @@ +# 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.image.v2 import metadef_namespace as _metadef_namespace +from openstack.tests.functional.image.v2 import base + + +class TestMetadefNamespace(base.BaseImageTest): + + # TODO(stephenfin): We should use setUpClass here for MOAR SPEED!!! + def setUp(self): + super().setUp() + + # there's a limit on namespace length + namespace = self.getUniqueString().split('.')[-1] + self.metadef_namespace = self.conn.image.create_metadef_namespace( + namespace=namespace, + ) + self.assertIsInstance( + self.metadef_namespace, + _metadef_namespace.MetadefNamespace, + ) + self.assertEqual(namespace, self.metadef_namespace.namespace) + + def tearDown(self): + # we do this in tearDown rather than via 'addCleanup' since we want to + # wait for the deletion of the resource to ensure it completes + self.conn.image.delete_metadef_namespace(self.metadef_namespace) + self.conn.image.wait_for_delete(self.metadef_namespace) + + super().tearDown() + + def test_metadef_namespace(self): + # get + metadef_namespace = self.conn.image.get_metadef_namespace( + self.metadef_namespace.namespace + ) + self.assertEqual( + self.metadef_namespace.namespace, metadef_namespace.namespace, + ) + + # (no find_metadef_namespace method) + + # list + metadef_namespaces = list(self.conn.image.metadef_namespaces()) + # there are a load of default metadef namespaces so we don't assert + # that this is the *only* metadef namespace present + self.assertIn( + self.metadef_namespace.namespace, + {n.namespace for n in metadef_namespaces}, + ) + + # update + # there's a limit on display name and description lengths and no + # inherent need for randomness so we use fixed strings + metadef_namespace_display_name = 'A display name' + metadef_namespace_description = 'A description' + metadef_namespace = self.conn.image.update_metadef_namespace( + self.metadef_namespace, + display_name=metadef_namespace_display_name, + description=metadef_namespace_description, + ) + self.assertIsInstance( + metadef_namespace, + _metadef_namespace.MetadefNamespace, + ) + metadef_namespace = self.conn.image.get_metadef_namespace( + self.metadef_namespace.namespace + ) + self.assertEqual( + metadef_namespace_display_name, + metadef_namespace.display_name, + ) + self.assertEqual( + metadef_namespace_description, + metadef_namespace.description, + ) diff --git a/openstack/tests/unit/image/v2/test_metadef_namespace.py b/openstack/tests/unit/image/v2/test_metadef_namespace.py index a4888fe0d..8fdd03fd8 100644 --- a/openstack/tests/unit/image/v2/test_metadef_namespace.py +++ b/openstack/tests/unit/image/v2/test_metadef_namespace.py @@ -13,19 +13,27 @@ from openstack.image.v2 import metadef_namespace from openstack.tests.unit import base + EXAMPLE = { 'display_name': 'Cinder Volume Type', - 'created_at': '2014-08-28T17:13:06Z', - 'is_protected': True, + 'created_at': '2022-08-24T17:46:24Z', + 'protected': True, 'namespace': 'OS::Cinder::Volumetype', + 'description': ( + 'The Cinder volume type configuration option. Volume type ' + 'assignment provides a mechanism not only to provide scheduling to a ' + 'specific storage back-end, but also can be used to specify specific ' + 'information for a back-end storage device to act upon.' + ), + 'visibility': 'public', 'owner': 'admin', 'resource_type_associations': [ { - 'created_at': '2014-08-28T17:13:06Z', 'name': 'OS::Glance::Image', - 'updated_at': '2014-08-28T17:13:06Z' - } - ] + 'prefix': 'cinder_', + 'created_at': '2022-08-24T17:46:24Z', + }, + ], } @@ -36,18 +44,22 @@ def test_basic(self): self.assertEqual('namespaces', sot.resources_key) self.assertEqual('/metadefs/namespaces', sot.base_path) self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_delete) def test_make_it(self): sot = metadef_namespace.MetadefNamespace(**EXAMPLE) self.assertEqual(EXAMPLE['namespace'], sot.namespace) + self.assertEqual(EXAMPLE['visibility'], sot.visibility) self.assertEqual(EXAMPLE['owner'], sot.owner) self.assertEqual(EXAMPLE['created_at'], sot.created_at) - self.assertEqual(EXAMPLE['is_protected'], sot.is_protected) + self.assertEqual(EXAMPLE['protected'], sot.is_protected) self.assertEqual(EXAMPLE['display_name'], sot.display_name) - self.assertListEqual(EXAMPLE['resource_type_associations'], - sot.resource_type_associations) - + self.assertEqual( + EXAMPLE['resource_type_associations'], + sot.resource_type_associations, + ) self.assertDictEqual( { 'limit': 'limit', @@ -56,4 +68,6 @@ def test_make_it(self): 'sort_dir': 'sort_dir', 'sort_key': 'sort_key', 'visibility': 'visibility' - }, sot._query_mapping._mapping) + }, + sot._query_mapping._mapping, + ) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 9687b749f..f4e568a59 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -410,12 +410,48 @@ def test_members(self): class TestMetadefNamespace(TestImageProxy): + def test_metadef_namespace_create(self): + self.verify_create( + self.proxy.create_metadef_namespace, + metadef_namespace.MetadefNamespace, + ) + + def test_metadef_namespace_delete(self): + self.verify_delete( + self.proxy.delete_metadef_namespace, + metadef_namespace.MetadefNamespace, + False, + ) + + def test_metadef_namespace_delete__ignore(self): + self.verify_delete( + self.proxy.delete_metadef_namespace, + metadef_namespace.MetadefNamespace, + True, + ) + + def test_metadef_namespace_get(self): + self.verify_get( + self.proxy.get_metadef_namespace, + metadef_namespace.MetadefNamespace, + ) + def test_metadef_namespaces(self): self.verify_list( self.proxy.metadef_namespaces, metadef_namespace.MetadefNamespace, ) + def test_metadef_namespace_update(self): + # we're (intentionally) adding an additional field, 'namespace', to the + # request body + self.verify_update( + self.proxy.update_metadef_namespace, + metadef_namespace.MetadefNamespace, + method_kwargs={'is_protected': True}, + expected_kwargs={'namespace': 'resource_id', 'is_protected': True}, + ) + class TestSchema(TestImageProxy): def test_images_schema_get(self): From 169e27aa27af5d15a4a6f3a5d4ff7c74adc4d2c7 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 7 Sep 2022 13:03:07 +0100 Subject: [PATCH 3108/3836] tests: Improve functional testing for image methods Improve test coverage for a number of APIs and split the large 'test_image' file into multiple smaller files. Change-Id: I1bd3e115282b94a2b327fade7f46f1e83a9c30e1 Signed-off-by: Stephen Finucane --- .../tests/functional/image/v2/test_image.py | 105 ++++++++++-------- .../tests/functional/image/v2/test_schema.py | 37 ++++++ .../tests/functional/image/v2/test_task.py | 26 +++++ 3 files changed, 120 insertions(+), 48 deletions(-) create mode 100644 openstack/tests/functional/image/v2/test_schema.py create mode 100644 openstack/tests/functional/image/v2/test_task.py diff --git a/openstack/tests/functional/image/v2/test_image.py b/openstack/tests/functional/image/v2/test_image.py index 530f161a5..359428f48 100644 --- a/openstack/tests/functional/image/v2/test_image.py +++ b/openstack/tests/functional/image/v2/test_image.py @@ -10,25 +10,21 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import connection -from openstack.tests.functional import base +from openstack.image.v2 import image as _image +from openstack.tests.functional.image.v2 import base +# NOTE(stephenfin): This is referenced in the Compute functional tests to avoid +# attempts to boot from it. TEST_IMAGE_NAME = 'Test Image' -class TestImage(base.BaseFunctionalTest): - - class ImageOpts: - def __init__(self): - self.image_api_version = '2' +class TestImage(base.BaseImageTest): def setUp(self): - super(TestImage, self).setUp() - opts = self.ImageOpts() - self.conn = connection.from_config( - cloud_name=base.TEST_CLOUD_NAME, options=opts) + super().setUp() - self.img = self.conn.image.create_image( + # there's a limit on name length + self.image = self.conn.image.create_image( name=TEST_IMAGE_NAME, disk_format='raw', container_format='bare', @@ -37,48 +33,61 @@ def setUp(self): # we need to just replace the image upload code with the stuff # from shade. Figuring out mapping the crap-tastic arbitrary # extra key-value pairs into Resource is going to be fun. - properties=dict( - description="This is not an image" - ), - data=open('CONTRIBUTING.rst', 'r') + properties={ + 'description': 'This is not an image', + }, + data=open('CONTRIBUTING.rst', 'r'), ) - self.addCleanup(self.conn.image.delete_image, self.img) + self.assertIsInstance(self.image, _image.Image) + self.assertEqual(TEST_IMAGE_NAME, self.image.name) - def test_get_image(self): - img2 = self.conn.image.get_image(self.img) - self.assertEqual(self.img, img2) + def tearDown(self): + # we do this in tearDown rather than via 'addCleanup' since we want to + # wait for the deletion of the resource to ensure it completes + self.conn.image.delete_image(self.image) + self.conn.image.wait_for_delete(self.image) - def test_get_images_schema(self): - schema = self.conn.image.get_images_schema() - self.assertIsNotNone(schema) + super().tearDown() - def test_get_image_schema(self): - schema = self.conn.image.get_image_schema() - self.assertIsNotNone(schema) + def test_images(self): + # get image + image = self.conn.image.get_image(self.image.id) + self.assertEqual(self.image.name, image.name) - def test_get_members_schema(self): - schema = self.conn.image.get_members_schema() - self.assertIsNotNone(schema) + # find image + image = self.conn.image.find_image(self.image.name) + self.assertEqual(self.image.id, image.id) - def test_get_member_schema(self): - schema = self.conn.image.get_member_schema() - self.assertIsNotNone(schema) + # list + images = list(self.conn.image.images()) + # there are many other images so we don't assert that this is the + # *only* image present + self.assertIn(self.image.id, {i.id for i in images}) - def test_list_tasks(self): - tasks = self.conn.image.tasks() - self.assertIsNotNone(tasks) + # update + image_name = self.getUniqueString() + image = self.conn.image.update_image( + self.image, + name=image_name, + ) + self.assertIsInstance(image, _image.Image) + image = self.conn.image.get_image(self.image.id) + self.assertEqual(image_name, image.name) def test_tags(self): - img = self.conn.image.get_image(self.img) - self.conn.image.add_tag(img, 't1') - self.conn.image.add_tag(img, 't2') - # Ensure list with array of tags return us our image - list_img = list(self.conn.image.images(tag=['t1', 't2']))[0] - self.assertEqual(img.id, list_img.id) - self.assertIn('t1', list_img.tags) - self.assertIn('t2', list_img.tags) - self.conn.image.remove_tag(img, 't1') - # Refetch img to verify tags - img = self.conn.image.get_image(self.img) - self.assertIn('t2', img.tags) - self.assertNotIn('t1', img.tags) + # add tag + image = self.conn.image.get_image(self.image) + self.conn.image.add_tag(image, 't1') + self.conn.image.add_tag(image, 't2') + + # filter image by tags + image = list(self.conn.image.images(tag=['t1', 't2']))[0] + self.assertEqual(image.id, image.id) + self.assertIn('t1', image.tags) + self.assertIn('t2', image.tags) + + # remove tag + self.conn.image.remove_tag(image, 't1') + image = self.conn.image.get_image(self.image) + self.assertIn('t2', image.tags) + self.assertNotIn('t1', image.tags) diff --git a/openstack/tests/functional/image/v2/test_schema.py b/openstack/tests/functional/image/v2/test_schema.py new file mode 100644 index 000000000..f9b3186ca --- /dev/null +++ b/openstack/tests/functional/image/v2/test_schema.py @@ -0,0 +1,37 @@ +# 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.image.v2 import schema as _schema +from openstack.tests.functional.image.v2 import base + + +class TestSchema(base.BaseImageTest): + + def test_get_images_schema(self): + schema = self.conn.image.get_images_schema() + self.assertIsNotNone(schema) + self.assertIsInstance(schema, _schema.Schema) + + def test_get_image_schema(self): + schema = self.conn.image.get_image_schema() + self.assertIsNotNone(schema) + self.assertIsInstance(schema, _schema.Schema) + + def test_get_members_schema(self): + schema = self.conn.image.get_members_schema() + self.assertIsNotNone(schema) + self.assertIsInstance(schema, _schema.Schema) + + def test_get_member_schema(self): + schema = self.conn.image.get_member_schema() + self.assertIsNotNone(schema) + self.assertIsInstance(schema, _schema.Schema) diff --git a/openstack/tests/functional/image/v2/test_task.py b/openstack/tests/functional/image/v2/test_task.py new file mode 100644 index 000000000..fb9b2c775 --- /dev/null +++ b/openstack/tests/functional/image/v2/test_task.py @@ -0,0 +1,26 @@ +# 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.tests.functional.image.v2 import base + + +class TestTask(base.BaseImageTest): + + def test_tasks(self): + tasks = list(self.conn.image.tasks()) + # NOTE(stephenfin): Yes, this is a dumb test. Basically all that we're + # checking is that the API endpoint is correct. It would be nice to + # have a proper check here that includes creation of tasks but we don't + # currently have the ability to do this and I'm not even sure if tasks + # are still really a supported thing. A potential future work item, + # perhaps. + self.assertIsInstance(tasks, list) From 0d6ed1ba4d46163238f2431a34c1182275f9483e Mon Sep 17 00:00:00 2001 From: EunYoung Kim Date: Thu, 15 Sep 2022 04:20:29 +0900 Subject: [PATCH 3109/3836] docs: Trivial adjust index structure * Adjust image resources index page * Adjust identity resources index page Change-Id: I6ea77735ae0f22dade0bcad2546bdc072bb544ba --- doc/source/user/resources/identity/index.rst | 8 +++++--- doc/source/user/resources/image/index.rst | 7 +++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/doc/source/user/resources/identity/index.rst b/doc/source/user/resources/identity/index.rst index b536e8b79..b52221494 100644 --- a/doc/source/user/resources/identity/index.rst +++ b/doc/source/user/resources/identity/index.rst @@ -1,6 +1,8 @@ -Identity v2 Resources -===================== +Identity Resources +================== +Identity v2 Resources +--------------------- .. toctree:: :maxdepth: 1 @@ -10,7 +12,7 @@ Identity v2 Resources v2/user Identity v3 Resources -===================== +--------------------- .. toctree:: :maxdepth: 1 diff --git a/doc/source/user/resources/image/index.rst b/doc/source/user/resources/image/index.rst index a41d3e940..986f3018e 100644 --- a/doc/source/user/resources/image/index.rst +++ b/doc/source/user/resources/image/index.rst @@ -1,5 +1,8 @@ +Image Resources +=============== + Image v1 Resources -================== +------------------ .. toctree:: :maxdepth: 1 @@ -7,7 +10,7 @@ Image v1 Resources v1/image Image v2 Resources -================== +------------------ .. toctree:: :maxdepth: 1 From 3bbbf1abdf9fe769f31f5934cea95514619c03e4 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 16 Sep 2022 10:39:58 +0100 Subject: [PATCH 3110/3836] docs: Correct docs for VpnIpsecPolicy We were incorrectly documenting the VpnIkePolicy twice instead. While we're here, add a docstring for the VpnIkePolicy resource so the documentation is actually useful. Change-Id: I5295ba3a48e12a46e3a90a89be3075b3ce2bfdec Signed-off-by: Stephen Finucane --- .../user/resources/network/v2/vpn/ipsecpolicy.rst | 14 +++++++------- openstack/network/v2/vpn_ikepolicy.py | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/doc/source/user/resources/network/v2/vpn/ipsecpolicy.rst b/doc/source/user/resources/network/v2/vpn/ipsecpolicy.rst index 8c43e02cd..5a5d75930 100644 --- a/doc/source/user/resources/network/v2/vpn/ipsecpolicy.rst +++ b/doc/source/user/resources/network/v2/vpn/ipsecpolicy.rst @@ -1,13 +1,13 @@ -openstack.network.v2.vpn_ikepolicy -================================== +openstack.network.v2.vpn_ipsec_policy +===================================== -.. automodule:: openstack.network.v2.vpn_ikepolicy +.. automodule:: openstack.network.v2.vpn_ipsec_policy -The VpnIkePolicy Class ----------------------- +The VpnIpsecPolicy Class +------------------------ -The ``VpnIkePolicy`` class inherits from +The ``VpnIpsecPolicy`` class inherits from :class:`~openstack.resource.Resource`. -.. autoclass:: openstack.network.v2.vpn_ikepolicy.VpnIkePolicy +.. autoclass:: openstack.network.v2.vpn_ipsec_policy.VpnIpsecPolicy :members: diff --git a/openstack/network/v2/vpn_ikepolicy.py b/openstack/network/v2/vpn_ikepolicy.py index 2edb088ef..d86081641 100644 --- a/openstack/network/v2/vpn_ikepolicy.py +++ b/openstack/network/v2/vpn_ikepolicy.py @@ -14,6 +14,7 @@ class VpnIkePolicy(resource.Resource): + """VPN IKE policy extension.""" resource_key = 'ikepolicy' resources_key = 'ikepolicies' base_path = '/vpn/ikepolicies' From 65f67386c22f7f02a451a9e17af7d6e78ffa7e6b Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 19 Sep 2022 11:19:59 +0200 Subject: [PATCH 3111/3836] Improve project cleanup for cinder There are few issues in the cleanup for backups that became visible on older releases - dry_run flag was not properly used - reworked iterations for backups to limitly support longer backup hierarchy. - improved test to verify backups are really deleted Change-Id: Iad2d08f559760bd061d0f92984e4deebe6165f29 --- openstack/block_storage/v3/_proxy.py | 82 ++++++++++++------- .../functional/cloud/test_project_cleanup.py | 9 +- 2 files changed, 59 insertions(+), 32 deletions(-) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 704629fb0..678412688 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -1504,42 +1504,68 @@ def _service_cleanup( resource_evaluation_fn=None ): # It is not possible to delete backup if there are dependent backups. - # In order to be able to do cleanup those is required to have at least - # 2 iterations (first cleans up backups with has no dependent backups, - # and in 2nd iteration there should be no backups with dependencies - # remaining. - for i in range(1, 2): - backups = [] + # In order to be able to do cleanup those is required to have multiple + # iterations (first clean up backups with has no dependent backups, and + # in next iterations there should be no backups with dependencies + # remaining. Logically we can have also failures, therefore it is + # required to limit amount of iterations we do (currently pick 10). In + # dry_run all those iterations are doing not what we want, therefore + # only iterate in a real cleanup mode. + if dry_run: + # Just iterate and evaluate backups in dry_run mode for obj in self.backups(details=False): - if ( - (i == 1 and not obj.has_dependent_backups) - or i != 1 + need_delete = self._service_cleanup_del_res( + self.delete_backup, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn) + else: + # Set initial iterations conditions + need_backup_iteration = True + max_iterations = 10 + while need_backup_iteration and max_iterations > 0: + # Reset iteration controls + need_backup_iteration = False + max_iterations -= 1 + backups = [] + # To increase success chance sort backups by age, dependent + # backups are logically younger. + for obj in self.backups( + details=True, sort_key='created_at', sort_dir='desc' ): - need_delete = self._service_cleanup_del_res( - self.delete_backup, - obj, - dry_run=True, - client_status_queue=client_status_queue, - identified_resources=identified_resources, - filters=filters, - resource_evaluation_fn=resource_evaluation_fn) - if not dry_run and need_delete: - backups.append(obj) - - # Before proceeding need to wait for backups to be deleted - for obj in backups: - try: - self.wait_for_delete(obj) - except exceptions.SDKException: - # Well, did our best, still try further - pass + if not obj.has_dependent_backups: + # If no dependent backups - go with it + need_delete = self._service_cleanup_del_res( + self.delete_backup, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn) + if not dry_run and need_delete: + backups.append(obj) + else: + # Otherwise we need another iteration + need_backup_iteration = True + + # Before proceeding need to wait for backups to be deleted + for obj in backups: + try: + self.wait_for_delete(obj) + except exceptions.SDKException: + # Well, did our best, still try further + pass snapshots = [] for obj in self.snapshots(details=False): need_delete = self._service_cleanup_del_res( self.delete_snapshot, obj, - dry_run=True, + dry_run=dry_run, client_status_queue=client_status_queue, identified_resources=identified_resources, filters=filters, diff --git a/openstack/tests/functional/cloud/test_project_cleanup.py b/openstack/tests/functional/cloud/test_project_cleanup.py index c25346cba..f6bed34c0 100644 --- a/openstack/tests/functional/cloud/test_project_cleanup.py +++ b/openstack/tests/functional/cloud/test_project_cleanup.py @@ -187,7 +187,7 @@ def test_block_storage_cleanup(self): vol_ids = list(obj.id for obj in objects) self.assertIn(vol.id, vol_ids) - # Ensure network still exists + # Ensure volume still exists vol_check = self.conn.block_storage.get_volume(vol.id) self.assertEqual(vol.name, vol_check.name) @@ -196,9 +196,10 @@ def test_block_storage_cleanup(self): dry_run=False, wait_timeout=600, status_queue=status_queue) - objects = [] - while not status_queue.empty(): - objects.append(status_queue.get()) + # Ensure no backups remain + self.assertEqual(0, len(list(self.conn.block_storage.backups()))) + # Ensure no snapshots remain + self.assertEqual(0, len(list(self.conn.block_storage.snapshots()))) def test_cleanup_swift(self): if not self.user_cloud.has_service('object-store'): From 2ec7d7462f24827ae7e90f963babc830ea4040f5 Mon Sep 17 00:00:00 2001 From: Jakob Meng Date: Tue, 20 Sep 2022 14:13:18 +0200 Subject: [PATCH 3112/3836] Drop query parameter 'id' from identity mapping Keystone API does not respect any query parameters such as 'id' when listing mappings [1],[2]. [1] https://opendev.org/openstack/keystone/src/commit/0155ae874181d9c74b0d1fcbc46ddd40ae3835f2/keystone/api/os_federation.py#L267 [2] https://docs.openstack.org/api-ref/identity/v3-ext/index.html?expanded=list-mappings-detail#list-mappings Change-Id: Ie1d0bc69da65861215b13fcb5499d7f982a25282 --- openstack/identity/v3/mapping.py | 4 +--- openstack/tests/unit/identity/v3/test_mapping.py | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/openstack/identity/v3/mapping.py b/openstack/identity/v3/mapping.py index a0327ef8d..646de310b 100644 --- a/openstack/identity/v3/mapping.py +++ b/openstack/identity/v3/mapping.py @@ -27,9 +27,7 @@ class Mapping(resource.Resource): create_method = 'PUT' commit_method = 'PATCH' - _query_mapping = resource.QueryParameters( - 'id', - ) + _query_mapping = resource.QueryParameters() # Properties #: The rules of this mapping. *Type: list* diff --git a/openstack/tests/unit/identity/v3/test_mapping.py b/openstack/tests/unit/identity/v3/test_mapping.py index 40a26d2b8..9b67d618f 100644 --- a/openstack/tests/unit/identity/v3/test_mapping.py +++ b/openstack/tests/unit/identity/v3/test_mapping.py @@ -38,7 +38,6 @@ def test_basic(self): self.assertDictEqual( { - 'id': 'id', 'limit': 'limit', 'marker': 'marker', }, From 8034f84459798feda67d04be07082636c456364b Mon Sep 17 00:00:00 2001 From: Christian Rohmann Date: Fri, 16 Sep 2022 17:29:09 +0200 Subject: [PATCH 3113/3836] Add support for updated_at field for volume snapshots Change-Id: Iba4aaa8a0aeec775d2ecfa17ff0ce065c6ffdaea --- openstack/block_storage/v2/snapshot.py | 2 ++ openstack/block_storage/v3/snapshot.py | 2 ++ openstack/tests/fakes.py | 1 + openstack/tests/unit/block_storage/v2/test_snapshot.py | 2 ++ openstack/tests/unit/block_storage/v3/test_snapshot.py | 2 ++ releasenotes/notes/snap-updated_at-a46711b6160e3a26.yaml | 3 +++ 6 files changed, 12 insertions(+) create mode 100644 releasenotes/notes/snap-updated_at-a46711b6160e3a26.yaml diff --git a/openstack/block_storage/v2/snapshot.py b/openstack/block_storage/v2/snapshot.py index c23fed107..49e5a99d0 100644 --- a/openstack/block_storage/v2/snapshot.py +++ b/openstack/block_storage/v2/snapshot.py @@ -45,6 +45,8 @@ class Snapshot(resource.Resource, metadata.MetadataMixin): #: The current status of this snapshot. Potential values are creating, #: available, deleting, error, and error_deleting. status = resource.Body("status") + #: The date and time when the resource was updated. + updated_at = resource.Body("updated_at") #: The ID of the volume this snapshot was taken of. volume_id = resource.Body("volume_id") diff --git a/openstack/block_storage/v3/snapshot.py b/openstack/block_storage/v3/snapshot.py index 3f025473b..4b81d598e 100644 --- a/openstack/block_storage/v3/snapshot.py +++ b/openstack/block_storage/v3/snapshot.py @@ -50,6 +50,8 @@ class Snapshot(resource.Resource, metadata.MetadataMixin): #: The current status of this snapshot. Potential values are creating, #: available, deleting, error, and error_deleting. status = resource.Body("status") + #: The date and time when the resource was updated. + updated_at = resource.Body("updated_at") #: The ID of the volume this snapshot was taken of. volume_id = resource.Body("volume_id") diff --git a/openstack/tests/fakes.py b/openstack/tests/fakes.py index a035895f1..e686cf2e5 100644 --- a/openstack/tests/fakes.py +++ b/openstack/tests/fakes.py @@ -374,6 +374,7 @@ def __init__( self.description = description self.size = size self.created_at = '1900-01-01 12:34:56' + self.updated_at = None self.volume_id = '12345' self.metadata = {} diff --git a/openstack/tests/unit/block_storage/v2/test_snapshot.py b/openstack/tests/unit/block_storage/v2/test_snapshot.py index fc6408d7f..408264ec5 100644 --- a/openstack/tests/unit/block_storage/v2/test_snapshot.py +++ b/openstack/tests/unit/block_storage/v2/test_snapshot.py @@ -23,6 +23,7 @@ "status": "creating", "description": "Daily backup", "created_at": "2015-03-09T12:14:57.233772", + "updated_at": None, "metadata": {}, "volume_id": "5aa119a8-d25b-45a7-8d1b-88e127885635", "size": 1, @@ -67,6 +68,7 @@ def test_create_basic(self): self.assertEqual(SNAPSHOT["id"], sot.id) self.assertEqual(SNAPSHOT["status"], sot.status) self.assertEqual(SNAPSHOT["created_at"], sot.created_at) + self.assertEqual(SNAPSHOT["updated_at"], sot.updated_at) self.assertEqual(SNAPSHOT["metadata"], sot.metadata) self.assertEqual(SNAPSHOT["volume_id"], sot.volume_id) self.assertEqual(SNAPSHOT["size"], sot.size) diff --git a/openstack/tests/unit/block_storage/v3/test_snapshot.py b/openstack/tests/unit/block_storage/v3/test_snapshot.py index 05f833051..13bef4a85 100644 --- a/openstack/tests/unit/block_storage/v3/test_snapshot.py +++ b/openstack/tests/unit/block_storage/v3/test_snapshot.py @@ -23,6 +23,7 @@ "status": "creating", "description": "Daily backup", "created_at": "2015-03-09T12:14:57.233772", + "updated_at": None, "metadata": {}, "volume_id": "5aa119a8-d25b-45a7-8d1b-88e127885635", "size": 1, @@ -62,6 +63,7 @@ def test_create_basic(self): self.assertEqual(SNAPSHOT["id"], sot.id) self.assertEqual(SNAPSHOT["status"], sot.status) self.assertEqual(SNAPSHOT["created_at"], sot.created_at) + self.assertEqual(SNAPSHOT["updated_at"], sot.updated_at) self.assertEqual(SNAPSHOT["metadata"], sot.metadata) self.assertEqual(SNAPSHOT["volume_id"], sot.volume_id) self.assertEqual(SNAPSHOT["size"], sot.size) diff --git a/releasenotes/notes/snap-updated_at-a46711b6160e3a26.yaml b/releasenotes/notes/snap-updated_at-a46711b6160e3a26.yaml new file mode 100644 index 000000000..927d6d11a --- /dev/null +++ b/releasenotes/notes/snap-updated_at-a46711b6160e3a26.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added support for the updated_at attribute for volume snapshots. From 6f826a137b28fce1a311a227495ac9c69489f49b Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 23 Sep 2022 15:31:39 +0200 Subject: [PATCH 3114/3836] Extend project cleanup Few additional corner cases: - Skip disabled or invalid services - include ports without owners Change-Id: I4500ead4d78c00f2a97b78de6d483aef656d73c9 --- openstack/cloud/openstackcloud.py | 57 +++++++++++++++++++------------ openstack/network/v2/_proxy.py | 21 +++++++++++- 2 files changed, 55 insertions(+), 23 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 9805ceaab..0b5cbb527 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -33,6 +33,7 @@ from openstack.cloud import meta import openstack.config from openstack.config import cloud_region as cloud_region_mod +from openstack import exceptions from openstack import proxy from openstack import utils @@ -782,16 +783,24 @@ def project_cleanup( if not status_queue: status_queue = queue.Queue() for service in self.config.get_enabled_services(): - if hasattr(self, service): - proxy = getattr(self, service) - if ( - proxy - and hasattr(proxy, get_dep_fn_name) - and hasattr(proxy, cleanup_fn_name) - ): - deps = getattr(proxy, get_dep_fn_name)() - if deps: - dependencies.update(deps) + try: + if hasattr(self, service): + proxy = getattr(self, service) + if ( + proxy + and hasattr(proxy, get_dep_fn_name) + and hasattr(proxy, cleanup_fn_name) + ): + deps = getattr(proxy, get_dep_fn_name)() + if deps: + dependencies.update(deps) + except ( + exceptions.NotSupported, + exceptions.ServiceDisabledException + ): + # Cloud may include endpoint in catalog but not + # implement the service or disable it + pass dep_graph = utils.TinyDAG() for k, v in dependencies.items(): dep_graph.add_node(k) @@ -805,18 +814,22 @@ def project_cleanup( for service in dep_graph.walk(timeout=wait_timeout): fn = None - if hasattr(self, service): - proxy = getattr(self, service) - cleanup_fn = getattr(proxy, cleanup_fn_name, None) - if cleanup_fn: - fn = functools.partial( - cleanup_fn, - dry_run=dry_run, - client_status_queue=status_queue, - identified_resources=cleanup_resources, - filters=filters, - resource_evaluation_fn=resource_evaluation_fn - ) + try: + if hasattr(self, service): + proxy = getattr(self, service) + cleanup_fn = getattr(proxy, cleanup_fn_name, None) + if cleanup_fn: + fn = functools.partial( + cleanup_fn, + dry_run=dry_run, + client_status_queue=status_queue, + identified_resources=cleanup_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn + ) + except exceptions.ServiceDisabledException: + # same reason as above + pass if fn: self._pool_executor.submit( cleanup_task, dep_graph, service, fn diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index c9a87f602..fe7309eac 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -5122,12 +5122,16 @@ def _service_cleanup(self, dry_run=True, client_status_queue=None, self.log.debug('Looking at port %s' % port) if port.device_owner in [ 'network:router_interface', - 'network:router_interface_distributed' + 'network:router_interface_distributed', + 'network:ha_router_replicated_interface' ]: router_if.append(port) elif port.device_owner == 'network:dhcp': # we don't treat DHCP as a real port continue + elif port.device_owner is None or port.device_owner == '': + # Nobody owns the port - go with it + continue elif ( identified_resources and port.device_id not in identified_resources @@ -5172,6 +5176,21 @@ def _service_cleanup(self, dry_run=True, client_status_queue=None, identified_resources=identified_resources, filters=None, resource_evaluation_fn=None) + # Drop ports not belonging to anybody + for port in self.ports( + project_id=project_id, + network_id=net.id + ): + if port.device_owner is None or port.device_owner == '': + self._service_cleanup_del_res( + self.delete_port, + port, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=None, + resource_evaluation_fn=None) + # Drop all subnets in the net (no further conditions) for obj in self.subnets( project_id=project_id, From e55a6472cf3eb00820e989b4f58bfa3dddfd4c1b Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 16 Sep 2022 10:56:27 +0100 Subject: [PATCH 3115/3836] network: Remove duplicate module, update references In change Id2513fd77fc303b42a52c436e3b0d46f93b7d376, we added support for IPsec policies. However, in this change we appear to have inadvertently merged two near-identical versions of the resource - one in the 'vpn_ipsecpolicy' module and one in the 'vpn_ipsec_policy' module. The latter is slightly more complete and the naming scheme matches that of other modules so we keep that. We also opt to update the proxy methods so that their naming scheme matches this pattern, along with the docstrings for same. Change-Id: I21799cf4ac6494f76b9c6b1963e214565a0e652d Signed-off-by: Stephen Finucane --- doc/source/user/proxies/network.rst | 4 +- .../user/resources/network/v2/vpn/index.rst | 2 +- .../vpn/{ipsecpolicy.rst => ipsec_policy.rst} | 0 openstack/network/v2/_proxy.py | 114 +++++++++--------- openstack/network/v2/vpn_ipsecpolicy.py | 57 --------- openstack/tests/unit/network/v2/test_proxy.py | 44 +++---- .../unit/network/v2/test_vpn_ipsecpolicy.py | 6 +- 7 files changed, 85 insertions(+), 142 deletions(-) rename doc/source/user/resources/network/v2/vpn/{ipsecpolicy.rst => ipsec_policy.rst} (100%) delete mode 100644 openstack/network/v2/vpn_ipsecpolicy.py diff --git a/doc/source/user/proxies/network.rst b/doc/source/user/proxies/network.rst index e443a12fe..78f13d705 100644 --- a/doc/source/user/proxies/network.rst +++ b/doc/source/user/proxies/network.rst @@ -255,8 +255,8 @@ VPNaaS Operations find_vpn_ipsec_site_connection, vpn_ipsec_site_connections, create_vpn_ikepolicy, update_vpn_ikepolicy, delete_vpn_ikepolicy, get_vpn_ikepolicy, find_vpn_ikepolicy, vpn_ikepolicies, - create_vpn_ipsecpolicy, update_vpn_ipsecpolicy, delete_vpn_ipsecpolicy, - get_vpn_ipsecpolicy, find_vpn_ipsecpolicy, vpn_ipsecpolicies + create_vpn_ipsec_policy, update_vpn_ipsec_policy, delete_vpn_ipsec_policy, + get_vpn_ipsec_policy, find_vpn_ipsec_policy, vpn_ipsec_policies Extension Operations ^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/network/v2/vpn/index.rst b/doc/source/user/resources/network/v2/vpn/index.rst index f8bb64b11..cf3ee2637 100644 --- a/doc/source/user/resources/network/v2/vpn/index.rst +++ b/doc/source/user/resources/network/v2/vpn/index.rst @@ -7,5 +7,5 @@ VPNaaS Resources endpoint_group ipsec_site_connection ikepolicy - ipsecpolicy + ipsec_policy service diff --git a/doc/source/user/resources/network/v2/vpn/ipsecpolicy.rst b/doc/source/user/resources/network/v2/vpn/ipsec_policy.rst similarity index 100% rename from doc/source/user/resources/network/v2/vpn/ipsecpolicy.rst rename to doc/source/user/resources/network/v2/vpn/ipsec_policy.rst diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index fe7309eac..d2c495dd7 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -63,9 +63,9 @@ from openstack.network.v2 import trunk as _trunk from openstack.network.v2 import vpn_endpoint_group as _vpn_endpoint_group from openstack.network.v2 import vpn_ikepolicy as _ikepolicy +from openstack.network.v2 import vpn_ipsec_policy as _ipsec_policy from openstack.network.v2 import vpn_ipsec_site_connection as \ _ipsec_site_connection -from openstack.network.v2 import vpn_ipsecpolicy as _ipsecpolicy from openstack.network.v2 import vpn_service as _vpn_service from openstack import proxy @@ -4473,15 +4473,15 @@ def update_vpn_endpoint_group(self, vpn_endpoint_group, **attrs): return self._update( _vpn_endpoint_group.VpnEndpointGroup, vpn_endpoint_group, **attrs) - # ========== IPSec Site Connection ========== + # ========== IPsec Site Connection ========== def create_vpn_ipsec_site_connection(self, **attrs): - """Create a new ipsec site connection from attributes + """Create a new IPsec site connection from attributes :param dict attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.vpn_ipsec_site_connection.VpnIPSecSiteConnection`, comprised of the properties on the IPSecSiteConnection class. - :returns: The results of ipsec site connection creation + :returns: The results of IPsec site connection creation :rtype: :class:`~openstack.network.v2.vpn_ipsec_site_connection.VpnIPSecSiteConnection` """ @@ -4492,9 +4492,9 @@ def create_vpn_ipsec_site_connection(self, **attrs): def find_vpn_ipsec_site_connection( self, name_or_id, ignore_missing=True, **args ): - """Find a single ipsec site connection + """Find a single IPsec site connection - :param name_or_id: The name or ID of an ipsec site connection. + :param name_or_id: The name or ID of an IPsec site connection. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the resource does not exist. @@ -4511,9 +4511,9 @@ def find_vpn_ipsec_site_connection( name_or_id, ignore_missing=ignore_missing, **args) def get_vpn_ipsec_site_connection(self, ipsec_site_connection): - """Get a single ipsec site connection + """Get a single IPsec site connection - :param ipsec_site_connection: The value can be the ID of an ipsec site + :param ipsec_site_connection: The value can be the ID of an IPsec site connection or a :class:`~openstack.network.v2.vpn_ipsec_site_connection.VpnIPSecSiteConnection` instance. @@ -4528,12 +4528,12 @@ def get_vpn_ipsec_site_connection(self, ipsec_site_connection): ipsec_site_connection) def vpn_ipsec_site_connections(self, **query): - """Return a generator of ipsec site connections + """Return a generator of IPsec site connections :param dict query: Optional query parameters to be sent to limit the resources being returned. - :returns: A generator of ipsec site connection objects + :returns: A generator of IPsec site connection objects :rtype: :class:`~openstack.network.v2.vpn_ipsec_site_connection.VpnIPSecSiteConnection` """ @@ -4541,16 +4541,16 @@ def vpn_ipsec_site_connections(self, **query): _ipsec_site_connection.VpnIPSecSiteConnection, **query) def update_vpn_ipsec_site_connection(self, ipsec_site_connection, **attrs): - """Update a ipsec site connection + """Update a IPsec site connection - :ipsec_site_connection: Either the id of an ipsec site connection or + :ipsec_site_connection: Either the id of an IPsec site connection or a :class:`~openstack.network.v2.vpn_ipsec_site_connection.VpnIPSecSiteConnection` instance. - :param dict attrs: The attributes to update on the ipsec site + :param dict attrs: The attributes to update on the IPsec site connection represented by ``ipsec_site_connection``. - :returns: The updated ipsec site connection + :returns: The updated IPsec site connection :rtype: :class:`~openstack.network.v2.vpn_ipsec_site_connection.VpnIPSecSiteConnection` """ @@ -4561,18 +4561,18 @@ def update_vpn_ipsec_site_connection(self, ipsec_site_connection, **attrs): def delete_vpn_ipsec_site_connection( self, ipsec_site_connection, ignore_missing=True ): - """Delete a ipsec site connection + """Delete a IPsec site connection :param ipsec_site_connection: The value can be either the ID of an - ipsec site connection, or a + IPsec site connection, or a :class:`~openstack.network.v2.vpn_ipsec_site_connection.VpnIPSecSiteConnection` instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when - the ipsec site connection does not exist. + the IPsec site connection does not exist. When set to ``True``, no exception will be set when attempting to - delete a nonexistent ipsec site connection. + delete a nonexistent IPsec site connection. :returns: ``None`` """ @@ -4675,25 +4675,25 @@ def delete_vpn_ikepolicy(self, ikepolicy, ignore_missing=True): ignore_missing=ignore_missing) # ========== IPSecPolicy ========== - def create_vpn_ipsecpolicy(self, **attrs): - """Create a new ipsec policy from attributes + def create_vpn_ipsec_policy(self, **attrs): + """Create a new IPsec policy from attributes :param dict attrs: Keyword arguments which will be used to create a - :class:`~openstack.network.v2.vpn_ipsecpolicy.VpnIpsecPolicy`, + :class:`~openstack.network.v2.vpn_ipsec_policy.VpnIpsecPolicy`, comprised of the properties on the VpnIpsecPolicy class. - :returns: The results of ipsec policy creation :rtype: - :class:`~openstack.network.v2.vpn_ipsecpolicy.VpnIpsecPolicy` + :returns: The results of IPsec policy creation :rtype: + :class:`~openstack.network.v2.vpn_ipsec_policy.VpnIpsecPolicy` """ return self._create( - _ipsecpolicy.VpnIpsecPolicy, **attrs) + _ipsec_policy.VpnIpsecPolicy, **attrs) - def find_vpn_ipsecpolicy( + def find_vpn_ipsec_policy( self, name_or_id, ignore_missing=True, **args ): - """Find a single ipsec policy + """Find a single IPsec policy - :param name_or_id: The name or ID of an ipsec policy. + :param name_or_id: The name or ID of an IPsec policy. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the resource does not exist. When set to @@ -4702,74 +4702,74 @@ def find_vpn_ipsecpolicy( :param dict args: Any additional parameters to be passed into underlying methods such as query filters. :returns: One - :class:`~openstack.network.v2.vpn_ipsecpolicy.VpnIpsecPolicy` + :class:`~openstack.network.v2.vpn_ipsec_policy.VpnIpsecPolicy` or None. """ return self._find( - _ipsecpolicy.VpnIpsecPolicy, name_or_id, + _ipsec_policy.VpnIpsecPolicy, name_or_id, ignore_missing=ignore_missing, **args) - def get_vpn_ipsecpolicy(self, ipsecpolicy): - """Get a single ipsec policy + def get_vpn_ipsec_policy(self, ipsec_policy): + """Get a single IPsec policy - :param ipsecpolicy: The value can be the ID of an ipsecpolicy or a - :class:`~openstack.network.v2.vpn_ipsecpolicy.VpnIpsecPolicy` + :param ipsec_policy: The value can be the ID of an IPcec policy or a + :class:`~openstack.network.v2.vpn_ipsec_policy.VpnIpsecPolicy` instance. :returns: One - :class:`~openstack.network.v2.vpn_ipsecpolicy.VpnIpsecPolicy` - :rtype: :class:`~openstack.network.v2.ipsecpolicy.VpnIpsecPolicy` + :class:`~openstack.network.v2.vpn_ipsec_policy.VpnIpsecPolicy` + :rtype: :class:`~openstack.network.v2.ipsec_policy.VpnIpsecPolicy` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ return self._get( - _ipsecpolicy.VpnIpsecPolicy, ipsecpolicy) + _ipsec_policy.VpnIpsecPolicy, ipsec_policy) - def vpn_ipsecpolicies(self, **query): - """Return a generator of ipsec policy + def vpn_ipsec_policies(self, **query): + """Return a generator of IPsec policies :param dict query: Optional query parameters to be sent to limit the resources being returned. - :returns: A generator of ipsec policy objects - :rtype: :class:`~openstack.network.v2.vpn_ipsecpolicy.VpnIpsecPolicy` + :returns: A generator of IPsec policy objects + :rtype: :class:`~openstack.network.v2.vpn_ipsec_policy.VpnIpsecPolicy` """ return self._list( - _ipsecpolicy.VpnIpsecPolicy, **query) + _ipsec_policy.VpnIpsecPolicy, **query) - def update_vpn_ipsecpolicy(self, ipsecpolicy, **attrs): - """Update a ipsec policy + def update_vpn_ipsec_policy(self, ipsec_policy, **attrs): + """Update an IPsec policy - :ipsecpolicy: Either the id of an ipsec policy or a - :class:`~openstack.network.v2.vpn_ipsecpolicy.VpnIpsecPolicy` + :ipsec_policy: Either the id of an IPsec policy or a + :class:`~openstack.network.v2.vpn_ipsec_policy.VpnIpsecPolicy` instance. - :param dict attrs: The attributes to update on the ipsec policy - represented by ``ipsecpolicy``. + :param dict attrs: The attributes to update on the IPsec policy + represented by ``ipsec_policy``. - :returns: The updated ipsec policy - :rtype: :class:`~openstack.network.v2.vpn_ipsecpolicy.VpnIpsecPolicy` + :returns: The updated IPsec policy + :rtype: :class:`~openstack.network.v2.vpn_ipsec_policy.VpnIpsecPolicy` """ return self._update( - _ipsecpolicy.VpnIpsecPolicy, ipsecpolicy, **attrs) + _ipsec_policy.VpnIpsecPolicy, ipsec_policy, **attrs) - def delete_vpn_ipsecpolicy(self, ipsecpolicy, ignore_missing=True): - """Delete a ipsecpolicy + def delete_vpn_ipsec_policy(self, ipsec_policy, ignore_missing=True): + """Delete an IPsec policy - :param ipsecpolicy: The value can be either the ID of an ipsec policy, + :param ipsec_policy: The value can be either the ID of an IPsec policy, or a - :class:`~openstack.network.v2.vpn_ipsecpolicy.VpnIpsecPolicy` + :class:`~openstack.network.v2.vpn_ipsec_policy.VpnIpsecPolicy` instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` - will be raised when the ipsec policy does not exist. When set to + will be raised when the IPsec policy does not exist. When set to ``True``, no exception will be set when attempting to delete a - nonexistent ipsec policy. + nonexistent IPsec policy. :returns: ``None`` """ self._delete( - _ipsecpolicy.VpnIpsecPolicy, ipsecpolicy, + _ipsec_policy.VpnIpsecPolicy, ipsec_policy, ignore_missing=ignore_missing) # ========== VPN Service ========== diff --git a/openstack/network/v2/vpn_ipsecpolicy.py b/openstack/network/v2/vpn_ipsecpolicy.py deleted file mode 100644 index bae5609a0..000000000 --- a/openstack/network/v2/vpn_ipsecpolicy.py +++ /dev/null @@ -1,57 +0,0 @@ -# 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 resource - - -class VpnIpsecPolicy(resource.Resource): - resource_key = 'ipsecpolicy' - resources_key = 'ipsecpolicies' - base_path = '/vpn/ipsecpolicies' - - # capabilities - allow_create = True - allow_fetch = True - allow_commit = True - allow_delete = True - allow_list = True - - # Properties - #: The authentication hash algorithm. Valid values are sha1, - # sha256, sha384, sha512. The default is sha1. - auth_algorithm = resource.Body('auth_algorithm') - #: A human-readable description for the resource. - # Default is an empty string. - description = resource.Body('description') - #: The encryption algorithm. A valid value is 3des, aes-128, - # aes-192, aes-256, and so on. Default is aes-128. - encryption_algorithm = resource.Body('encryption_algorithm') - #: The lifetime of the security association. The lifetime consists - # of a unit and integer value. You can omit either the unit or value - # portion of the lifetime. Default unit is seconds and - # default value is 3600. - lifetime = resource.Body('lifetime', type=dict) - #: Perfect forward secrecy (PFS). A valid value is Group2, - # Group5, Group14, and so on. Default is Group5. - pfs = resource.Body('pfs') - #: The ID of the project. - project_id = resource.Body('project_id') - #: The units for the lifetime of the security association. - # The lifetime consists of a unit and integer value. - # You can omit either the unit or value portion of the lifetime. - # Default unit is seconds and default value is 3600. - units = resource.Body('units') - #: The lifetime value, as a positive integer. The lifetime - # consists of a unit and integer value. - # You can omit either the unit or value portion of the lifetime. - # Default unit is seconds and default value is 3600. - value = resource.Body('value', type=int) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index d1030692f..e41dceb79 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -60,8 +60,8 @@ from openstack.network.v2 import subnet_pool from openstack.network.v2 import vpn_endpoint_group from openstack.network.v2 import vpn_ikepolicy +from openstack.network.v2 import vpn_ipsec_policy from openstack.network.v2 import vpn_ipsec_site_connection -from openstack.network.v2 import vpn_ipsecpolicy from openstack.network.v2 import vpn_service from openstack import proxy as proxy_base from openstack.tests.unit import test_proxy_base @@ -1686,40 +1686,40 @@ def test_ikepolicy_update(self): class TestNetworkVpnIpsecPolicy(TestNetworkProxy): - def test_ipsecpolicy_create_attrs(self): + def test_ipsec_policy_create_attrs(self): self.verify_create( - self.proxy.create_vpn_ipsecpolicy, - vpn_ipsecpolicy.VpnIpsecPolicy) + self.proxy.create_vpn_ipsec_policy, + vpn_ipsec_policy.VpnIpsecPolicy) - def test_ipsecpolicy_delete(self): + def test_ipsec_policy_delete(self): self.verify_delete( - self.proxy.delete_vpn_ipsecpolicy, - vpn_ipsecpolicy.VpnIpsecPolicy, False) + self.proxy.delete_vpn_ipsec_policy, + vpn_ipsec_policy.VpnIpsecPolicy, False) - def test_ipsecpolicy_delete_ignore(self): + def test_ipsec_policy_delete_ignore(self): self.verify_delete( - self.proxy.delete_vpn_ipsecpolicy, - vpn_ipsecpolicy.VpnIpsecPolicy, True) + self.proxy.delete_vpn_ipsec_policy, + vpn_ipsec_policy.VpnIpsecPolicy, True) - def test_ipsecpolicy_find(self): + def test_ipsec_policy_find(self): self.verify_find( - self.proxy.find_vpn_ipsecpolicy, - vpn_ipsecpolicy.VpnIpsecPolicy) + self.proxy.find_vpn_ipsec_policy, + vpn_ipsec_policy.VpnIpsecPolicy) - def test_ipsecpolicy_get(self): + def test_ipsec_policy_get(self): self.verify_get( - self.proxy.get_vpn_ipsecpolicy, - vpn_ipsecpolicy.VpnIpsecPolicy) + self.proxy.get_vpn_ipsec_policy, + vpn_ipsec_policy.VpnIpsecPolicy) - def test_ipsecpolicies(self): + def test_ipsec_policies(self): self.verify_list( - self.proxy.vpn_ipsecpolicies, - vpn_ipsecpolicy.VpnIpsecPolicy) + self.proxy.vpn_ipsec_policies, + vpn_ipsec_policy.VpnIpsecPolicy) - def test_ipsecpolicy_update(self): + def test_ipsec_policy_update(self): self.verify_update( - self.proxy.update_vpn_ipsecpolicy, - vpn_ipsecpolicy.VpnIpsecPolicy) + self.proxy.update_vpn_ipsec_policy, + vpn_ipsec_policy.VpnIpsecPolicy) class TestNetworkVpnService(TestNetworkProxy): diff --git a/openstack/tests/unit/network/v2/test_vpn_ipsecpolicy.py b/openstack/tests/unit/network/v2/test_vpn_ipsecpolicy.py index bdecd07fc..50136e04d 100644 --- a/openstack/tests/unit/network/v2/test_vpn_ipsecpolicy.py +++ b/openstack/tests/unit/network/v2/test_vpn_ipsecpolicy.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network.v2 import vpn_ipsecpolicy +from openstack.network.v2 import vpn_ipsec_policy from openstack.tests.unit import base @@ -30,7 +30,7 @@ class TestVpnIpsecPolicy(base.TestCase): def test_basic(self): - sot = vpn_ipsecpolicy.VpnIpsecPolicy() + sot = vpn_ipsec_policy.VpnIpsecPolicy() self.assertEqual('ipsecpolicy', sot.resource_key) self.assertEqual('ipsecpolicies', sot.resources_key) self.assertEqual('/vpn/ipsecpolicies', sot.base_path) @@ -41,7 +41,7 @@ def test_basic(self): self.assertTrue(sot.allow_list) def test_make_it(self): - sot = vpn_ipsecpolicy.VpnIpsecPolicy(**EXAMPLE) + sot = vpn_ipsec_policy.VpnIpsecPolicy(**EXAMPLE) self.assertEqual(EXAMPLE['auth_algorithm'], sot.auth_algorithm) self.assertEqual(EXAMPLE['description'], sot.description) self.assertEqual(EXAMPLE['encryption_algorithm'], From 83153457c3b50bd0f447d23a7320a2edc094e90d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 16 Sep 2022 15:36:14 +0100 Subject: [PATCH 3116/3836] network: Rename ikepolicy module Add an underscore, as is typical for our module names. Change-Id: I174b90ec41fdbd3bedb1c96198bc23d4ce595659 Signed-off-by: Stephen Finucane --- doc/source/user/proxies/network.rst | 4 +- .../resources/network/v2/vpn/ike_policy.rst | 13 ++++ .../resources/network/v2/vpn/ikepolicy.rst | 13 ---- .../user/resources/network/v2/vpn/index.rst | 2 +- openstack/network/v2/_proxy.py | 67 ++++++++++--------- .../{vpn_ikepolicy.py => vpn_ike_policy.py} | 0 .../functional/network/v2/test_vpnaas.py | 28 ++++---- openstack/tests/unit/network/v2/test_proxy.py | 44 ++++++------ .../unit/network/v2/test_vpn_ikepolicy.py | 6 +- 9 files changed, 89 insertions(+), 88 deletions(-) create mode 100644 doc/source/user/resources/network/v2/vpn/ike_policy.rst delete mode 100644 doc/source/user/resources/network/v2/vpn/ikepolicy.rst rename openstack/network/v2/{vpn_ikepolicy.py => vpn_ike_policy.py} (100%) diff --git a/doc/source/user/proxies/network.rst b/doc/source/user/proxies/network.rst index 78f13d705..c35633e45 100644 --- a/doc/source/user/proxies/network.rst +++ b/doc/source/user/proxies/network.rst @@ -253,8 +253,8 @@ VPNaaS Operations create_vpn_ipsec_site_connection, update_vpn_ipsec_site_connection, delete_vpn_ipsec_site_connection, get_vpn_ipsec_site_connection, find_vpn_ipsec_site_connection, vpn_ipsec_site_connections, - create_vpn_ikepolicy, update_vpn_ikepolicy, delete_vpn_ikepolicy, - get_vpn_ikepolicy, find_vpn_ikepolicy, vpn_ikepolicies, + create_vpn_ike_policy, update_vpn_ike_policy, delete_vpn_ike_policy, + get_vpn_ike_policy, find_vpn_ike_policy, vpn_ike_policies, create_vpn_ipsec_policy, update_vpn_ipsec_policy, delete_vpn_ipsec_policy, get_vpn_ipsec_policy, find_vpn_ipsec_policy, vpn_ipsec_policies diff --git a/doc/source/user/resources/network/v2/vpn/ike_policy.rst b/doc/source/user/resources/network/v2/vpn/ike_policy.rst new file mode 100644 index 000000000..81bc6b0dd --- /dev/null +++ b/doc/source/user/resources/network/v2/vpn/ike_policy.rst @@ -0,0 +1,13 @@ +openstack.network.v2.vpn_ike_policy +=================================== + +.. automodule:: openstack.network.v2.vpn_ike_policy + +The VpnIkePolicy Class +---------------------- + +The ``VpnIkePolicy`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.vpn_ike_policy.VpnIkePolicy + :members: diff --git a/doc/source/user/resources/network/v2/vpn/ikepolicy.rst b/doc/source/user/resources/network/v2/vpn/ikepolicy.rst deleted file mode 100644 index 8c43e02cd..000000000 --- a/doc/source/user/resources/network/v2/vpn/ikepolicy.rst +++ /dev/null @@ -1,13 +0,0 @@ -openstack.network.v2.vpn_ikepolicy -================================== - -.. automodule:: openstack.network.v2.vpn_ikepolicy - -The VpnIkePolicy Class ----------------------- - -The ``VpnIkePolicy`` class inherits from -:class:`~openstack.resource.Resource`. - -.. autoclass:: openstack.network.v2.vpn_ikepolicy.VpnIkePolicy - :members: diff --git a/doc/source/user/resources/network/v2/vpn/index.rst b/doc/source/user/resources/network/v2/vpn/index.rst index cf3ee2637..5180f8f2a 100644 --- a/doc/source/user/resources/network/v2/vpn/index.rst +++ b/doc/source/user/resources/network/v2/vpn/index.rst @@ -6,6 +6,6 @@ VPNaaS Resources endpoint_group ipsec_site_connection - ikepolicy + ike_policy ipsec_policy service diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index d2c495dd7..bbcf5fd1d 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -62,7 +62,7 @@ from openstack.network.v2 import subnet_pool as _subnet_pool from openstack.network.v2 import trunk as _trunk from openstack.network.v2 import vpn_endpoint_group as _vpn_endpoint_group -from openstack.network.v2 import vpn_ikepolicy as _ikepolicy +from openstack.network.v2 import vpn_ike_policy as _ike_policy from openstack.network.v2 import vpn_ipsec_policy as _ipsec_policy from openstack.network.v2 import vpn_ipsec_site_connection as \ _ipsec_site_connection @@ -4581,25 +4581,25 @@ def delete_vpn_ipsec_site_connection( ipsec_site_connection, ignore_missing=ignore_missing) # ========== IKEPolicy ========== - def create_vpn_ikepolicy(self, **attrs): + def create_vpn_ike_policy(self, **attrs): """Create a new ike policy from attributes :param dict attrs: Keyword arguments which will be used to create a - :class:`~openstack.network.v2.vpn_ikepolicy.VpnIkePolicy`, + :class:`~openstack.network.v2.vpn_ike_policy.VpnIkePolicy`, comprised of the properties on the VpnIkePolicy class. :returns: The results of ike policy creation :rtype: - :class:`~openstack.network.v2.vpn_ikepolicy.VpnIkePolicy` + :class:`~openstack.network.v2.vpn_ike_policy.VpnIkePolicy` """ return self._create( - _ikepolicy.VpnIkePolicy, **attrs) + _ike_policy.VpnIkePolicy, **attrs) - def find_vpn_ikepolicy( + def find_vpn_ike_policy( self, name_or_id, ignore_missing=True, **args ): """Find a single ike policy - :param name_or_id: The name or ID of an ike policy. + :param name_or_id: The name or ID of an IKE policy. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the resource does not exist. @@ -4607,59 +4607,60 @@ def find_vpn_ikepolicy( attempting to find a nonexistent resource. :param dict args: Any additional parameters to be passed into underlying methods such as query filters. - :returns: One :class:`~openstack.network.v2.vpn_ikepolicy.VpnIkePolicy` - or None. + :returns: One + :class:`~openstack.network.v2.vpn_ike_policy.VpnIkePolicy` or None. """ return self._find( - _ikepolicy.VpnIkePolicy, name_or_id, + _ike_policy.VpnIkePolicy, name_or_id, ignore_missing=ignore_missing, **args) - def get_vpn_ikepolicy(self, ikepolicy): + def get_vpn_ike_policy(self, ike_policy): """Get a single ike policy - :param ikepolicy: The value can be the ID of an ikepolicy or a - :class:`~openstack.network.v2.vpn_ikepolicy.VpnIkePolicy` + :param ike_policy: The value can be the ID of an IKE policy or a + :class:`~openstack.network.v2.vpn_ike_policy.VpnIkePolicy` instance. - :returns: One :class:`~openstack.network.v2.vpn_ikepolicy.VpnIkePolicy` - :rtype: :class:`~openstack.network.v2.ikepolicy.VpnIkePolicy` + :returns: One + :class:`~openstack.network.v2.vpn_ike_policy.VpnIkePolicy` + :rtype: :class:`~openstack.network.v2.ike_policy.VpnIkePolicy` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ return self._get( - _ikepolicy.VpnIkePolicy, ikepolicy) + _ike_policy.VpnIkePolicy, ike_policy) - def vpn_ikepolicies(self, **query): - """Return a generator of ike policy + def vpn_ike_policies(self, **query): + """Return a generator of IKE policies :param dict query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of ike policy objects - :rtype: :class:`~openstack.network.v2.vpn_ikepolicy.VpnIkePolicy` + :rtype: :class:`~openstack.network.v2.vpn_ike_policy.VpnIkePolicy` """ - return self._list( - _ikepolicy.VpnIkePolicy, **query) + return self._list(_ike_policy.VpnIkePolicy, **query) - def update_vpn_ikepolicy(self, ikepolicy, **attrs): - """Update a ike policy + def update_vpn_ike_policy(self, ike_policy, **attrs): + """Update an IKE policy - :ikepolicy: Either the id of an ike policy or a - :class:`~openstack.network.v2.vpn_ikepolicy.VpnIkePolicy` instance. + :ike_policy: Either the IK of an IKE policy or a + :class:`~openstack.network.v2.vpn_ike_policy.VpnIkePolicy` + instance. :param dict attrs: The attributes to update on the ike policy - represented by ``ikepolicy``. + represented by ``ike_policy``. :returns: The updated ike policy - :rtype: :class:`~openstack.network.v2.vpn_ikepolicy.VpnIkePolicy` + :rtype: :class:`~openstack.network.v2.vpn_ike_policy.VpnIkePolicy` """ return self._update( - _ikepolicy.VpnIkePolicy, ikepolicy, **attrs) + _ike_policy.VpnIkePolicy, ike_policy, **attrs) - def delete_vpn_ikepolicy(self, ikepolicy, ignore_missing=True): - """Delete a ikepolicy + def delete_vpn_ike_policy(self, ike_policy, ignore_missing=True): + """Delete an IKE policy - :param ikepolicy: The value can be either the ID of an ike policy, or - a :class:`~openstack.network.v2.vpn_ikepolicy.VpnIkePolicy` + :param ike_policy: The value can be either the ID of an ike policy, or + a :class:`~openstack.network.v2.vpn_ike_policy.VpnIkePolicy` instance. :param bool ignore_missing: When set to ``False`` @@ -4671,7 +4672,7 @@ def delete_vpn_ikepolicy(self, ikepolicy, ignore_missing=True): :returns: ``None`` """ self._delete( - _ikepolicy.VpnIkePolicy, ikepolicy, + _ike_policy.VpnIkePolicy, ike_policy, ignore_missing=ignore_missing) # ========== IPSecPolicy ========== diff --git a/openstack/network/v2/vpn_ikepolicy.py b/openstack/network/v2/vpn_ike_policy.py similarity index 100% rename from openstack/network/v2/vpn_ikepolicy.py rename to openstack/network/v2/vpn_ike_policy.py diff --git a/openstack/tests/functional/network/v2/test_vpnaas.py b/openstack/tests/functional/network/v2/test_vpnaas.py index 2617efe47..46c34cadd 100644 --- a/openstack/tests/functional/network/v2/test_vpnaas.py +++ b/openstack/tests/functional/network/v2/test_vpnaas.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network.v2 import vpn_ikepolicy +from openstack.network.v2 import vpn_ike_policy from openstack.tests.functional import base @@ -22,34 +22,34 @@ def setUp(self): super(TestVpnIkePolicy, self).setUp() if not self.conn._has_neutron_extension('vpnaas_v2'): self.skipTest('vpnaas_v2 service not supported by cloud') - self.IKEPOLICY_NAME = self.getUniqueString('ikepolicy') + self.IKE_POLICY_NAME = self.getUniqueString('ikepolicy') self.UPDATE_NAME = self.getUniqueString('ikepolicy-updated') - policy = self.conn.network.create_vpn_ikepolicy( - name=self.IKEPOLICY_NAME) - assert isinstance(policy, vpn_ikepolicy.VpnIkePolicy) - self.assertEqual(self.IKEPOLICY_NAME, policy.name) + policy = self.conn.network.create_vpn_ike_policy( + name=self.IKE_POLICY_NAME) + assert isinstance(policy, vpn_ike_policy.VpnIkePolicy) + self.assertEqual(self.IKE_POLICY_NAME, policy.name) self.ID = policy.id def tearDown(self): - ikepolicy = self.conn.network.\ - delete_vpn_ikepolicy(self.ID, ignore_missing=True) - self.assertIsNone(ikepolicy) + ike_policy = self.conn.network.\ + delete_vpn_ike_policy(self.ID, ignore_missing=True) + self.assertIsNone(ike_policy) super(TestVpnIkePolicy, self).tearDown() def test_list(self): policies = [f.name for f in self.conn.network.vpn_ikepolicies()] - self.assertIn(self.IKEPOLICY_NAME, policies) + self.assertIn(self.IKE_POLICY_NAME, policies) def test_find(self): - policy = self.conn.network.find_vpn_ikepolicy(self.IKEPOLICY_NAME) + policy = self.conn.network.find_vpn_ike_policy(self.IKE_POLICY_NAME) self.assertEqual(self.ID, policy.id) def test_get(self): - policy = self.conn.network.get_vpn_ikepolicy(self.ID) - self.assertEqual(self.IKEPOLICY_NAME, policy.name) + policy = self.conn.network.get_vpn_ike_policy(self.ID) + self.assertEqual(self.IKE_POLICY_NAME, policy.name) self.assertEqual(self.ID, policy.id) def test_update(self): - policy = self.conn.network.update_vpn_ikepolicy( + policy = self.conn.network.update_vpn_ike_policy( self.ID, name=self.UPDATE_NAME) self.assertEqual(self.UPDATE_NAME, policy.name) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index e41dceb79..1a864f60a 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -59,7 +59,7 @@ from openstack.network.v2 import subnet from openstack.network.v2 import subnet_pool from openstack.network.v2 import vpn_endpoint_group -from openstack.network.v2 import vpn_ikepolicy +from openstack.network.v2 import vpn_ike_policy from openstack.network.v2 import vpn_ipsec_policy from openstack.network.v2 import vpn_ipsec_site_connection from openstack.network.v2 import vpn_service @@ -1649,40 +1649,40 @@ def test_ipsec_site_connection_update(self): class TestNetworkVpnIkePolicy(TestNetworkProxy): - def test_ikepolicy_create_attrs(self): + def test_ike_policy_create_attrs(self): self.verify_create( - self.proxy.create_vpn_ikepolicy, - vpn_ikepolicy.VpnIkePolicy) + self.proxy.create_vpn_ike_policy, + vpn_ike_policy.VpnIkePolicy) - def test_ikepolicy_delete(self): + def test_ike_policy_delete(self): self.verify_delete( - self.proxy.delete_vpn_ikepolicy, - vpn_ikepolicy.VpnIkePolicy, False) + self.proxy.delete_vpn_ike_policy, + vpn_ike_policy.VpnIkePolicy, False) - def test_ikepolicy_delete_ignore(self): + def test_ike_policy_delete_ignore(self): self.verify_delete( - self.proxy.delete_vpn_ikepolicy, - vpn_ikepolicy.VpnIkePolicy, True) + self.proxy.delete_vpn_ike_policy, + vpn_ike_policy.VpnIkePolicy, True) - def test_ikepolicy_find(self): + def test_ike_policy_find(self): self.verify_find( - self.proxy.find_vpn_ikepolicy, - vpn_ikepolicy.VpnIkePolicy) + self.proxy.find_vpn_ike_policy, + vpn_ike_policy.VpnIkePolicy) - def test_ikepolicy_get(self): + def test_ike_policy_get(self): self.verify_get( - self.proxy.get_vpn_ikepolicy, - vpn_ikepolicy.VpnIkePolicy) + self.proxy.get_vpn_ike_policy, + vpn_ike_policy.VpnIkePolicy) - def test_ikepolicies(self): + def test_ike_policies(self): self.verify_list( - self.proxy.vpn_ikepolicies, - vpn_ikepolicy.VpnIkePolicy) + self.proxy.vpn_ike_policies, + vpn_ike_policy.VpnIkePolicy) - def test_ikepolicy_update(self): + def test_ike_policy_update(self): self.verify_update( - self.proxy.update_vpn_ikepolicy, - vpn_ikepolicy.VpnIkePolicy) + self.proxy.update_vpn_ike_policy, + vpn_ike_policy.VpnIkePolicy) class TestNetworkVpnIpsecPolicy(TestNetworkProxy): diff --git a/openstack/tests/unit/network/v2/test_vpn_ikepolicy.py b/openstack/tests/unit/network/v2/test_vpn_ikepolicy.py index 0e9310435..b88271ac0 100644 --- a/openstack/tests/unit/network/v2/test_vpn_ikepolicy.py +++ b/openstack/tests/unit/network/v2/test_vpn_ikepolicy.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.network.v2 import vpn_ikepolicy +from openstack.network.v2 import vpn_ike_policy from openstack.tests.unit import base @@ -32,7 +32,7 @@ class TestVpnIkePolicy(base.TestCase): def test_basic(self): - sot = vpn_ikepolicy.VpnIkePolicy() + sot = vpn_ike_policy.VpnIkePolicy() self.assertEqual('ikepolicy', sot.resource_key) self.assertEqual('ikepolicies', sot.resources_key) self.assertEqual('/vpn/ikepolicies', sot.base_path) @@ -43,7 +43,7 @@ def test_basic(self): self.assertTrue(sot.allow_list) def test_make_it(self): - sot = vpn_ikepolicy.VpnIkePolicy(**EXAMPLE) + sot = vpn_ike_policy.VpnIkePolicy(**EXAMPLE) self.assertEqual(EXAMPLE['auth_algorithm'], sot.auth_algorithm) self.assertEqual(EXAMPLE['description'], sot.description) self.assertEqual(EXAMPLE['encryption_algorithm'], From 2535ba7a28ae081f064ae20f9ddb19f48a973625 Mon Sep 17 00:00:00 2001 From: Jakob Meng Date: Wed, 28 Sep 2022 20:13:48 +0200 Subject: [PATCH 3117/3836] Allow to attach a floating ip to a specific fixed address Change-Id: Ic02aa6196e3dd2ee94133d83c6b370f10e3c1626 --- openstack/cloud/_floating_ip.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index 532171577..7429ae4cb 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -856,6 +856,7 @@ def _add_ip_from_pool( f_ip = self.create_floating_ip( server=server, network=network, nat_destination=nat_destination, + fixed_address=fixed_address, wait=wait, timeout=timeout) timeout = timeout - (time.time() - start_time) # Wait for cache invalidation time so that we don't try From 6e9acd5fef13be5bb88b2d090d07e25a16e5c472 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 30 Sep 2022 11:09:28 +0100 Subject: [PATCH 3118/3836] Fix pre-commit issues I should have fixed these when adding pre-commit initially. Change-Id: I082594b6dac985754c307dba6af61a6854d20d35 Signed-off-by: Stephen Finucane --- openstack/cloud/cmd/inventory.py | 4 ---- .../notes/add_image_import_support-6cea2e7d7a781071.yaml | 2 +- releasenotes/notes/volume-quotas-5b674ee8c1f71eb6.yaml | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) mode change 100755 => 100644 openstack/cloud/cmd/inventory.py diff --git a/openstack/cloud/cmd/inventory.py b/openstack/cloud/cmd/inventory.py old mode 100755 new mode 100644 index 450669b1f..548ddf15f --- a/openstack/cloud/cmd/inventory.py +++ b/openstack/cloud/cmd/inventory.py @@ -65,7 +65,3 @@ def main(): sys.stderr.write(e.message + '\n') sys.exit(1) sys.exit(0) - - -if __name__ == '__main__': - main() diff --git a/releasenotes/notes/add_image_import_support-6cea2e7d7a781071.yaml b/releasenotes/notes/add_image_import_support-6cea2e7d7a781071.yaml index 22b35ce6b..da0ffe599 100644 --- a/releasenotes/notes/add_image_import_support-6cea2e7d7a781071.yaml +++ b/releasenotes/notes/add_image_import_support-6cea2e7d7a781071.yaml @@ -1,7 +1,7 @@ --- features: - Add ability to create image without upload data at the same time - - Add support for interoperable image import process as introduced in the + - Add support for interoperable image import process as introduced in the Image API v2.6 at [1] [1]https://developer.openstack.org/api-ref/image/v2/index.html#interoperable-image-import diff --git a/releasenotes/notes/volume-quotas-5b674ee8c1f71eb6.yaml b/releasenotes/notes/volume-quotas-5b674ee8c1f71eb6.yaml index dfb3b1cd6..2507aacf2 100644 --- a/releasenotes/notes/volume-quotas-5b674ee8c1f71eb6.yaml +++ b/releasenotes/notes/volume-quotas-5b674ee8c1f71eb6.yaml @@ -1,3 +1,3 @@ --- -features: +features: - Add new APIs, OperatorCloud.get_volume_quotas(), OperatorCloud.set_volume_quotas() and OperatorCloud.delete_volume_quotas() to manage cinder quotas for projects and users \ No newline at end of file From 6acddc9104b72bd7f6230322d74d503593a6215c Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 30 Sep 2022 19:16:47 +0200 Subject: [PATCH 3119/3836] Improve swift headers handling Test on a cloud with RADOS uncovered that headers come back lowcase. Our devstack tests do not have this issue. To fix this ensure we do lowcase and also add tests for that. Change-Id: I8e3c44e04238dc19d65c670e6b24e8251e44364f --- openstack/object_store/v1/_base.py | 5 ++++- openstack/tests/unit/object_store/v1/test_obj.py | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/openstack/object_store/v1/_base.py b/openstack/object_store/v1/_base.py index 96b620ede..d429b13a1 100644 --- a/openstack/object_store/v1/_base.py +++ b/openstack/object_store/v1/_base.py @@ -84,7 +84,10 @@ def _set_metadata(self, headers): self.metadata = dict() for header in headers: - if header.startswith(self._custom_metadata_prefix): + # RADOS and other stuff in front may actually lowcase headers + if header.lower().startswith( + self._custom_metadata_prefix.lower() + ): key = header[len(self._custom_metadata_prefix):].lower() self.metadata[key] = headers[header] diff --git a/openstack/tests/unit/object_store/v1/test_obj.py b/openstack/tests/unit/object_store/v1/test_obj.py index b4af0c540..30f58a3f4 100644 --- a/openstack/tests/unit/object_store/v1/test_obj.py +++ b/openstack/tests/unit/object_store/v1/test_obj.py @@ -116,6 +116,15 @@ def test_from_headers(self): self.assertEqual(self.headers['Content-Type'], sot.content_type) self.assertEqual(self.headers['X-Delete-At'], sot.delete_at) + # Verify that we also properly process lowcased headers + # All headers are processed in _base._set_metadata therefore invoke it + # here directly + sot._set_metadata(headers={"x-object-meta-foo": "bar"}) + self.assert_no_calls() + + # Attributes from header + self.assertEqual("bar", sot.metadata["foo"]) + def test_download(self): headers = { 'X-Newest': 'True', From 4b8a9ceb2097745a1bd97a4ef52c02ad2acb832b Mon Sep 17 00:00:00 2001 From: Snow Kim Date: Tue, 13 Sep 2022 00:11:03 +0900 Subject: [PATCH 3120/3836] image: Add metadef schema resource to v2 api * Add resource for (Glance) Metadata Definition Schema API (https://docs.openstack.org/api-ref/image/v2/metadefs-index.html?expanded=#metadata-definition-schemas) * Adjust names to avoid abbreviations * Adjust grouping * Add tests * Add docs and release note * Trim underlines in docs * Resolve merge conflict * Seperate functional test to another file * Rename test class name correctly Change-Id: If2dfbca5ffbd425e4183b310af9534411318a395 --- doc/source/user/proxies/image_v2.rst | 9 +- doc/source/user/resources/image/index.rst | 1 + .../resources/image/v2/metadef_schema.rst | 13 ++ openstack/image/v2/_proxy.py | 101 +++++++++++++ openstack/image/v2/metadef_schema.py | 29 ++++ .../image/v2/test_metadef_schema.py | 67 +++++++++ .../unit/image/v2/test_metadef_schema.py | 141 ++++++++++++++++++ openstack/tests/unit/image/v2/test_proxy.py | 93 ++++++++++++ ...image-metadef-schema-b463825481bdf954.yaml | 3 + 9 files changed, 455 insertions(+), 2 deletions(-) create mode 100644 doc/source/user/resources/image/v2/metadef_schema.rst create mode 100644 openstack/image/v2/metadef_schema.py create mode 100644 openstack/tests/functional/image/v2/test_metadef_schema.py create mode 100644 openstack/tests/unit/image/v2/test_metadef_schema.py create mode 100644 releasenotes/notes/add-image-metadef-schema-b463825481bdf954.yaml diff --git a/doc/source/user/proxies/image_v2.rst b/doc/source/user/proxies/image_v2.rst index cbd6feea2..941f803a2 100644 --- a/doc/source/user/proxies/image_v2.rst +++ b/doc/source/user/proxies/image_v2.rst @@ -43,7 +43,12 @@ Schema Operations .. autoclass:: openstack.image.v2._proxy.Proxy :noindex: :members: get_images_schema, get_image_schema, get_members_schema, - get_member_schema, get_tasks_schema, get_task_schema + get_member_schema, get_tasks_schema, get_task_schema, + get_metadef_namespace_schema, get_metadef_namespaces_schema, + get_metadef_resource_type_schema, get_metadef_resource_types_schema, + get_metadef_object_schema, get_metadef_objects_schema, + get_metadef_property_schema, get_metadef_properties_schema, + get_metadef_tag_schema, get_metadef_tags_schema Service Info Discovery Operations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -54,7 +59,7 @@ Service Info Discovery Operations Metadef Namespace Operations -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.image.v2._proxy.Proxy :noindex: diff --git a/doc/source/user/resources/image/index.rst b/doc/source/user/resources/image/index.rst index 986f3018e..660312278 100644 --- a/doc/source/user/resources/image/index.rst +++ b/doc/source/user/resources/image/index.rst @@ -18,5 +18,6 @@ Image v2 Resources v2/image v2/member v2/metadef_namespace + v2/metadef_schema v2/task v2/service_info diff --git a/doc/source/user/resources/image/v2/metadef_schema.rst b/doc/source/user/resources/image/v2/metadef_schema.rst new file mode 100644 index 000000000..8332ee48d --- /dev/null +++ b/doc/source/user/resources/image/v2/metadef_schema.rst @@ -0,0 +1,13 @@ +openstack.image.v2.metadef_schema +================================= + +.. automodule:: openstack.image.v2.metadef_schema + +The MetadefSchema Class +----------------------- + +The ``MetadefSchema`` class inherits +from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.image.v2.metadef_schema.MetadefSchema + :members: diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index c747d1a17..b819c8ae3 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -18,6 +18,7 @@ from openstack.image.v2 import image as _image from openstack.image.v2 import member as _member from openstack.image.v2 import metadef_namespace as _metadef_namespace +from openstack.image.v2 import metadef_schema as _metadef_schema from openstack.image.v2 import schema as _schema from openstack.image.v2 import service_info as _si from openstack.image.v2 import task as _task @@ -817,6 +818,106 @@ def get_task_schema(self): return self._get(_schema.Schema, requires_id=False, base_path='/schemas/task') + def get_metadef_namespace_schema(self): + """Get metadata definition namespace schema + + :returns: One :class:`~openstack.image.v2.metadef_schema.MetadefSchema` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_metadef_schema.MetadefSchema, requires_id=False, + base_path='/schemas/metadefs/namespace') + + def get_metadef_namespaces_schema(self): + """Get metadata definition namespaces schema + + :returns: One :class:`~openstack.image.v2.metadef_schema.MetadefSchema` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_metadef_schema.MetadefSchema, requires_id=False, + base_path='/schemas/metadefs/namespaces') + + def get_metadef_resource_type_schema(self): + """Get metadata definition resource type association schema + + :returns: One :class:`~openstack.image.v2.metadef_schema.MetadefSchema` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_metadef_schema.MetadefSchema, requires_id=False, + base_path='/schemas/metadefs/resource_type') + + def get_metadef_resource_types_schema(self): + """Get metadata definition resource type associations schema + + :returns: One :class:`~openstack.image.v2.metadef_schema.MetadefSchema` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_metadef_schema.MetadefSchema, requires_id=False, + base_path='/schemas/metadefs/resource_types') + + def get_metadef_object_schema(self): + """Get metadata definition object schema + + :returns: One :class:`~openstack.image.v2.metadef_schema.MetadefSchema` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_metadef_schema.MetadefSchema, requires_id=False, + base_path='/schemas/metadefs/object') + + def get_metadef_objects_schema(self): + """Get metadata definition objects schema + + :returns: One :class:`~openstack.image.v2.metadef_schema.MetadefSchema` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_metadef_schema.MetadefSchema, requires_id=False, + base_path='/schemas/metadefs/objects') + + def get_metadef_property_schema(self): + """Get metadata definition property schema + + :returns: One :class:`~openstack.image.v2.metadef_schema.MetadefSchema` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_metadef_schema.MetadefSchema, requires_id=False, + base_path='/schemas/metadefs/property') + + def get_metadef_properties_schema(self): + """Get metadata definition properties schema + + :returns: One :class:`~openstack.image.v2.metadef_schema.MetadefSchema` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_metadef_schema.MetadefSchema, requires_id=False, + base_path='/schemas/metadefs/properties') + + def get_metadef_tag_schema(self): + """Get metadata definition tag schema + + :returns: One :class:`~openstack.image.v2.metadef_schema.MetadefSchema` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_metadef_schema.MetadefSchema, requires_id=False, + base_path='/schemas/metadefs/tag') + + def get_metadef_tags_schema(self): + """Get metadata definition tags schema + + :returns: One :class:`~openstack.image.v2.metadef_schema.MetadefSchema` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_metadef_schema.MetadefSchema, requires_id=False, + base_path='/schemas/metadefs/tags') + # ====== TASKS ====== def tasks(self, **query): """Return a generator of tasks diff --git a/openstack/image/v2/metadef_schema.py b/openstack/image/v2/metadef_schema.py new file mode 100644 index 000000000..b5e4fee68 --- /dev/null +++ b/openstack/image/v2/metadef_schema.py @@ -0,0 +1,29 @@ +# 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 resource + + +class MetadefSchema(resource.Resource): + base_path = '/schemas/metadefs' + + # capabilities + allow_fetch = True + + #: A boolean value that indicates allows users to add custom properties. + additional_properties = resource.Body('additionalProperties', type=bool) + #: A set of definitions. + definitions = resource.Body('definitions', type=dict) + #: A list of required resources. + required = resource.Body('required', type=list) + #: Schema properties. + properties = resource.Body('properties', type=dict) diff --git a/openstack/tests/functional/image/v2/test_metadef_schema.py b/openstack/tests/functional/image/v2/test_metadef_schema.py new file mode 100644 index 000000000..ae5fe3384 --- /dev/null +++ b/openstack/tests/functional/image/v2/test_metadef_schema.py @@ -0,0 +1,67 @@ +# 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.image.v2 import metadef_schema as _metadef_schema +from openstack.tests.functional.image.v2 import base + + +class TestMetadefSchema(base.BaseImageTest): + + def test_get_metadef_namespace_schema(self): + metadef_schema = self.conn.image.get_metadef_namespace_schema() + self.assertIsNotNone(metadef_schema) + self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) + + def test_get_metadef_namespaces_schema(self): + metadef_schema = self.conn.image.get_metadef_namespaces_schema() + self.assertIsNotNone(metadef_schema) + self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) + + def test_get_metadef_resource_type_schema(self): + metadef_schema = self.conn.image.get_metadef_resource_type_schema() + self.assertIsNotNone(metadef_schema) + self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) + + def test_get_metadef_resource_types_schema(self): + metadef_schema = self.conn.image.get_metadef_resource_types_schema() + self.assertIsNotNone(metadef_schema) + self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) + + def test_get_metadef_object_schema(self): + metadef_schema = self.conn.image.get_metadef_object_schema() + self.assertIsNotNone(metadef_schema) + self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) + + def test_get_metadef_objects_schema(self): + metadef_schema = self.conn.image.get_metadef_objects_schema() + self.assertIsNotNone(metadef_schema) + self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) + + def test_get_metadef_property_schema(self): + metadef_schema = self.conn.image.get_metadef_property_schema() + self.assertIsNotNone(metadef_schema) + self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) + + def test_get_metadef_properties_schema(self): + metadef_schema = self.conn.image.get_metadef_properties_schema() + self.assertIsNotNone(metadef_schema) + self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) + + def test_get_metadef_tag_schema(self): + metadef_schema = self.conn.image.get_metadef_tag_schema() + self.assertIsNotNone(metadef_schema) + self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) + + def test_get_metadef_tags_schema(self): + metadef_schema = self.conn.image.get_metadef_tags_schema() + self.assertIsNotNone(metadef_schema) + self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) diff --git a/openstack/tests/unit/image/v2/test_metadef_schema.py b/openstack/tests/unit/image/v2/test_metadef_schema.py new file mode 100644 index 000000000..e26d89823 --- /dev/null +++ b/openstack/tests/unit/image/v2/test_metadef_schema.py @@ -0,0 +1,141 @@ +# 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.image.v2 import metadef_schema +from openstack.tests.unit import base + + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'name': 'namespace', + 'properties': { + 'namespace': { + 'type': 'string', + 'description': 'The unique namespace text.', + 'maxLength': 80 + }, + 'visibility': { + 'type': 'string', + 'description': 'Scope of namespace accessibility.', + 'enum': [ + 'public', + 'private' + ] + }, + 'created_at': { + 'type': 'string', + 'readOnly': True, + 'description': 'Date and time of namespace creation', + 'format': 'date-time' + }, + 'resource_type_associations': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'name': { + 'type': 'string' + }, + 'prefix': { + 'type': 'string' + }, + 'properties_target': { + 'type': 'string' + } + } + } + }, + 'properties': { + '$ref': '#/definitions/property' + }, + 'objects': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'name': { + 'type': 'string' + }, + 'description': { + 'type': 'string' + }, + 'required': { + '$ref': '#/definitions/stringArray' + }, + 'properties': { + '$ref': '#/definitions/property' + } + } + } + }, + 'tags': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'name': { + 'type': 'string' + } + } + } + } + }, + 'additionalProperties': False, + 'definitions': { + 'positiveInteger': { + 'type': 'integer', + 'minimum': 0 + }, + 'positiveIntegerDefault0': { + 'allOf': [ + { + '$ref': '#/definitions/positiveInteger' + }, + { + 'default': 0 + } + ] + }, + 'stringArray': { + 'type': 'array', + 'items': { + 'type': 'string' + }, + 'uniqueItems': True + } + }, + 'required': [ + 'namespace' + ] +} + + +class TestMetadefSchema(base.TestCase): + def test_basic(self): + sot = metadef_schema.MetadefSchema() + self.assertIsNone(sot.resource_key) + self.assertIsNone(sot.resources_key) + self.assertEqual('/schemas/metadefs', sot.base_path) + self.assertFalse(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_delete) + self.assertFalse(sot.allow_list) + + def test_make_it(self): + sot = metadef_schema.MetadefSchema(**EXAMPLE) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['properties'], sot.properties) + self.assertEqual(EXAMPLE['additionalProperties'], + sot.additional_properties) + self.assertEqual(EXAMPLE['definitions'], sot.definitions) + self.assertEqual(EXAMPLE['required'], sot.required) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index f4e568a59..6aeaa1fe7 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -20,6 +20,7 @@ from openstack.image.v2 import image from openstack.image.v2 import member from openstack.image.v2 import metadef_namespace +from openstack.image.v2 import metadef_schema from openstack.image.v2 import schema from openstack.image.v2 import service_info as si from openstack.image.v2 import task @@ -598,3 +599,95 @@ def test_import_info(self): method_kwargs={}, expected_args=[si.Import], expected_kwargs={'require_id': False}) + + +class TestMetadefSchema(TestImageProxy): + def test_metadef_namespace_schema_get(self): + self._verify( + "openstack.proxy.Proxy._get", + self.proxy.get_metadef_namespace_schema, + expected_args=[metadef_schema.MetadefSchema], + expected_kwargs={ + 'base_path': '/schemas/metadefs/namespace', + 'requires_id': False}) + + def test_metadef_namespaces_schema_get(self): + self._verify( + "openstack.proxy.Proxy._get", + self.proxy.get_metadef_namespaces_schema, + expected_args=[metadef_schema.MetadefSchema], + expected_kwargs={ + 'base_path': '/schemas/metadefs/namespaces', + 'requires_id': False}) + + def test_metadef_resource_type_schema_get(self): + self._verify( + "openstack.proxy.Proxy._get", + self.proxy.get_metadef_resource_type_schema, + expected_args=[metadef_schema.MetadefSchema], + expected_kwargs={ + 'base_path': '/schemas/metadefs/resource_type', + 'requires_id': False}) + + def test_metadef_resource_types_schema_get(self): + self._verify( + "openstack.proxy.Proxy._get", + self.proxy.get_metadef_resource_types_schema, + expected_args=[metadef_schema.MetadefSchema], + expected_kwargs={ + 'base_path': '/schemas/metadefs/resource_types', + 'requires_id': False}) + + def test_metadef_object_schema_get(self): + self._verify( + "openstack.proxy.Proxy._get", + self.proxy.get_metadef_object_schema, + expected_args=[metadef_schema.MetadefSchema], + expected_kwargs={ + 'base_path': '/schemas/metadefs/object', + 'requires_id': False}) + + def test_metadef_objects_schema_get(self): + self._verify( + "openstack.proxy.Proxy._get", + self.proxy.get_metadef_objects_schema, + expected_args=[metadef_schema.MetadefSchema], + expected_kwargs={ + 'base_path': '/schemas/metadefs/objects', + 'requires_id': False}) + + def test_metadef_property_schema_get(self): + self._verify( + "openstack.proxy.Proxy._get", + self.proxy.get_metadef_property_schema, + expected_args=[metadef_schema.MetadefSchema], + expected_kwargs={ + 'base_path': '/schemas/metadefs/property', + 'requires_id': False}) + + def test_metadef_properties_schema_get(self): + self._verify( + "openstack.proxy.Proxy._get", + self.proxy.get_metadef_properties_schema, + expected_args=[metadef_schema.MetadefSchema], + expected_kwargs={ + 'base_path': '/schemas/metadefs/properties', + 'requires_id': False}) + + def test_metadef_tag_schema_get(self): + self._verify( + "openstack.proxy.Proxy._get", + self.proxy.get_metadef_tag_schema, + expected_args=[metadef_schema.MetadefSchema], + expected_kwargs={ + 'base_path': '/schemas/metadefs/tag', + 'requires_id': False}) + + def test_metadef_tags_schema_get(self): + self._verify( + "openstack.proxy.Proxy._get", + self.proxy.get_metadef_tags_schema, + expected_args=[metadef_schema.MetadefSchema], + expected_kwargs={ + 'base_path': '/schemas/metadefs/tags', + 'requires_id': False}) diff --git a/releasenotes/notes/add-image-metadef-schema-b463825481bdf954.yaml b/releasenotes/notes/add-image-metadef-schema-b463825481bdf954.yaml new file mode 100644 index 000000000..f02975932 --- /dev/null +++ b/releasenotes/notes/add-image-metadef-schema-b463825481bdf954.yaml @@ -0,0 +1,3 @@ +--- +features: + - Add support for metadata definition schema resource in image service. \ No newline at end of file From 9a1778162961b52201950be9f21f8a6cb9632543 Mon Sep 17 00:00:00 2001 From: Jakob Meng Date: Wed, 5 Oct 2022 10:56:06 +0200 Subject: [PATCH 3121/3836] Added Ansible OpenStack Collection to Bifrost's job.required-projects Job bifrost-integration-openstacksdk-src installs the latest code of openstacksdk's master branch because it has openstacksdk's repo listed in its job.required-projects. It also has to pull the latest code of Ansible OpenStack Collection, because Bifrost jobs pin the former to older releases which is compatible to openstacksdk <0.99.0 only. Change-Id: I6c674ff968088960ee68e2f6dd187f77e97d0755 --- .zuul.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.zuul.yaml b/.zuul.yaml index db8050b5d..9f6a54324 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -428,6 +428,7 @@ name: bifrost-integration-openstacksdk-src parent: bifrost-integration-tinyipa-ubuntu-focal required-projects: + - openstack/ansible-collections-openstack - openstack/openstacksdk - job: From f9d7fc60e85bcedab685b3b2d259e8c1760d3392 Mon Sep 17 00:00:00 2001 From: Yuriy Halytskyy Date: Sun, 17 Jul 2022 15:36:09 +1200 Subject: [PATCH 3122/3836] support nat_destination when attaching existing floating_ip to a server Story: 2010153 Task: 45805 Change-Id: I8a656d4099c763a4b18a29146d057a71bc33ad6e --- openstack/cloud/_floating_ip.py | 9 ++++++--- openstack/tests/unit/cloud/test_floating_ip_common.py | 4 +++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index 532171577..2cee65fcc 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -874,7 +874,7 @@ def _add_ip_from_pool( def add_ip_list( self, server, ips, wait=False, timeout=60, - fixed_address=None): + fixed_address=None, nat_destination=None): """Attach a list of IPs to a server. :param server: a server object @@ -885,6 +885,9 @@ def add_ip_list( See the ``wait`` parameter. :param fixed_address: (optional) Fixed address of the server to attach the IP to + :param nat_destination: (optional) Name or ID of the network that + the fixed IP to attach the + floating IP should be on :returns: The updated server ``munch.Munch`` @@ -899,7 +902,7 @@ def add_ip_list( id=None, filters={'floating_ip_address': ip}) server = self._attach_ip_to_server( server=server, floating_ip=f_ip, wait=wait, timeout=timeout, - fixed_address=fixed_address) + fixed_address=fixed_address, nat_destination=nat_destination) return server def add_auto_ip(self, server, wait=False, timeout=60, reuse=True): @@ -987,7 +990,7 @@ def add_ips_to_server( elif ips: server = self.add_ip_list( server, ips, wait=wait, timeout=timeout, - fixed_address=fixed_address) + fixed_address=fixed_address, nat_destination=nat_destination) elif auto_ip: if self._needs_floating_ip(server, nat_destination): server = self._add_auto_ip( diff --git a/openstack/tests/unit/cloud/test_floating_ip_common.py b/openstack/tests/unit/cloud/test_floating_ip_common.py index f79bb5003..6687e37da 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_common.py +++ b/openstack/tests/unit/cloud/test_floating_ip_common.py @@ -197,7 +197,9 @@ def test_add_ips_to_server_ip_list(self, mock_add_ip_list): self.cloud.add_ips_to_server(server_dict, ips=ips) mock_add_ip_list.assert_called_with( - server_dict, ips, wait=False, timeout=60, fixed_address=None) + server_dict, ips, wait=False, timeout=60, + fixed_address=None, + nat_destination=None) @patch.object(connection.Connection, '_needs_floating_ip') @patch.object(connection.Connection, '_add_auto_ip') From ddf2a2504920702c314bec14733f3816072cc7cc Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 14 Dec 2021 16:31:38 +0000 Subject: [PATCH 3123/3836] compute: Fix '*volume_attachment' proxy methods The majority of these were totally broken and unusable because nova doesn't allow you to retrieve a volume attachment by its ID: rather, you need to use a combination of the server and volume IDs. As such, each method should have been taking a combo of a Server object or ID and a *Volume* object or ID, not a VolumeAttachment object or ID. These fixes require rather extensive changes to the method signature, to the point that we've actually added a release note to call this out. We've included some hacky workarounds to avoid breaking existing users for now, giving them time to migrate to the new, improved patterns. In addition, the 'update_volume_attachment' proxy method never worked since updates were forbidden on the resource (via 'allow_commit=False'). This is fixed by changing this value to True. Change-Id: Iaab40ac4cc71f7626063700f476b53f7ffb5f39f Signed-off-by: Stephen Finucane --- openstack/cloud/_block_storage.py | 10 +- openstack/compute/v2/_proxy.py | 234 ++++++++++++------ openstack/compute/v2/volume_attachment.py | 6 +- openstack/tests/unit/cloud/test_volume.py | 20 ++ openstack/tests/unit/compute/v2/test_proxy.py | 155 ++++++++++++ .../unit/compute/v2/test_volume_attachment.py | 4 +- ...-proxy-method-rework-dc35fe9ca3af1c16.yaml | 13 + 7 files changed, 358 insertions(+), 84 deletions(-) create mode 100644 releasenotes/notes/compute-volume-attachment-proxy-method-rework-dc35fe9ca3af1c16.yaml diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index 96c8ba43e..520ac8ca3 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -348,7 +348,8 @@ def detach_volume(self, server, volume, wait=True, timeout=None): :raises: OpenStackCloudException on operation error. """ self.compute.delete_volume_attachment( - volume['id'], server['id'], + server=server['id'], + volume=volume['id'], ignore_missing=False, ) if wait: @@ -396,11 +397,14 @@ def attach_volume( % (volume['id'], volume['status']) ) - payload = {'volumeId': volume['id']} + payload = {} if device: payload['device'] = device attachment = self.compute.create_volume_attachment( - server=server['id'], **payload) + server=server['id'], + volume=volume['id'], + **payload, + ) if wait: if not hasattr(volume, 'fetch'): diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 3f0373d1f..d7d8e7baa 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -12,6 +12,7 @@ import warnings +from openstack.block_storage.v3 import volume as _volume from openstack.compute.v2 import aggregate as _aggregate from openstack.compute.v2 import availability_zone from openstack.compute.v2 import extension @@ -1559,11 +1560,15 @@ def update_service(self, service, **attrs): # ========== Volume Attachments ========== - def create_volume_attachment(self, server, **attrs): + # TODO(stephenfin): Make the volume argument required in 2.0 + def create_volume_attachment(self, server, volume=None, **attrs): """Create a new volume attachment from attributes - :param server: The server can be either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` instance. + :param server: The value can be either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance that the + volume is attached to. + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.block_storage.v3.volume.Volume` instance. :param dict attrs: Keyword arguments which will be used to create a :class:`~openstack.compute.v2.volume_attachment.VolumeAttachment`, comprised of the properties on the VolumeAttachment class. @@ -1572,115 +1577,192 @@ def create_volume_attachment(self, server, **attrs): :rtype: :class:`~openstack.compute.v2.volume_attachment.VolumeAttachment` """ + # if the user didn't pass the new 'volume' argument, they're probably + # calling things using a legacy parameter + if volume is None: + # there are two ways to pass this legacy parameter: either using + # the openstacksdk alias, 'volume_id', or using the real nova field + # name, 'volumeId' + if 'volume_id' in attrs: + volume_id = attrs.pop('volume_id') + elif 'volumeId' in attrs: + volume_id = attrs.pop('volumeId') + else: + # the user has used neither the new way nor the old way so they + # should start using the new way + # NOTE(stephenfin): we intentionally mimic the behavior of a + # missing positional parameter in stdlib + # https://github.com/python/cpython/blob/v3.10.0/Lib/inspect.py#L1464-L1467 + raise TypeError( + 'create_volume_attachment() missing 1 required positional ' + 'argument: volume' + ) + + # encourage users to the new way so we can eventually remove this + # mess of logic + deprecation_msg = ( + 'This method was called with a volume_id or volumeId ' + 'argument. This is legacy behavior that will be removed in ' + 'a future version. Update callers to use a volume argument.' + ) + warnings.warn(deprecation_msg, DeprecationWarning) + else: + volume_id = resource.Resource._get_id(volume) + server_id = resource.Resource._get_id(server) - return self._create(_volume_attachment.VolumeAttachment, - server_id=server_id, **attrs) + return self._create( + _volume_attachment.VolumeAttachment, + server_id=server_id, + volume_id=volume_id, + **attrs, + ) - def update_volume_attachment(self, volume_attachment, server, - **attrs): - """update a volume attachment + def update_volume_attachment( + self, server, volume, volume_id=None, **attrs, + ): + """Update a volume attachment - :param volume_attachment: - The value can be either the ID of a volume attachment or a - :class:`~openstack.compute.v2.volume_attachment.VolumeAttachment` - instance. - :param server: This parameter need to be specified when - VolumeAttachment ID is given as value. It can be - either the ID of a server or a - :class:`~openstack.compute.v2.server.Server` - instance that the attachment belongs to. - :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the volume attachment does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent volume attachment. + Note that the underlying API expects a volume ID, not a volume + attachment ID. There is currently no way to update volume attachments + by their own ID. + + :param server: The value can be either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance that the + volume is attached to. + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.block_storage.v3.volume.Volume` instance. + :param volume_id: The ID of a volume to swap to. If this is not + specified, we will default to not swapping the volume. + :param attrs: The attributes to update on the volume attachment + represented by ``volume_attachment``. :returns: ``None`` """ - server_id = self._get_uri_attribute(volume_attachment, server, - "server_id") - volume_attachment = resource.Resource._get_id(volume_attachment) + new_volume_id = volume_id + + server_id = resource.Resource._get_id(server) + volume_id = resource.Resource._get_id(volume) + + if new_volume_id is None: + new_volume_id = volume_id + + return self._update( + _volume_attachment.VolumeAttachment, + id=volume_id, + server_id=server_id, + volume_id=new_volume_id, + **attrs, + ) + + # TODO(stephenfin): Remove this hack in openstacksdk 2.0 + def _verify_server_volume_args(self, server, volume): + deprecation_msg = ( + 'The server and volume arguments to this function appear to ' + 'be backwards and have been reversed. This is a breaking ' + 'change introduced in openstacksdk 1.0. This shim will be ' + 'removed in a future version' + ) - return self._update(_volume_attachment.VolumeAttachment, - attachment_id=volume_attachment, - server_id=server_id) + # if we have even partial type information and things look as they + # should, we can assume the user did the right thing + if ( + isinstance(server, _server.Server) + or isinstance(volume, _volume.Volume) + ): + return server, volume + + # conversely, if there's type info and things appear off, tell the user + if ( + isinstance(server, _volume.Volume) + or isinstance(volume, _server.Server) + ): + warnings.warn(deprecation_msg, DeprecationWarning) + return volume, server - def delete_volume_attachment(self, volume_attachment, server, - ignore_missing=True): + # without type info we have to try a find the server corresponding to + # the provided ID and validate it + if self.find_server(server, ignore_missing=True) is not None: + return server, volume + else: + warnings.warn(deprecation_msg, DeprecationWarning) + return volume, server + + def delete_volume_attachment(self, server, volume, ignore_missing=True): """Delete a volume attachment - :param volume_attachment: - The value can be either the ID of a volume attachment or a - :class:`~openstack.compute.v2.volume_attachment.VolumeAttachment` - instance. - :param server: This parameter need to be specified when - VolumeAttachment ID is given as value. It can be either - the ID of a server or a - :class:`~openstack.compute.v2.server.Server` - instance that the attachment belongs to. + Note that the underlying API expects a volume ID, not a volume + attachment ID. There is currently no way to delete volume attachments + by their own ID. + + :param server: The value can be either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance that the + volume is attached to. + :param volume: The value can be the ID of a volume or a + :class:`~openstack.block_storage.v3.volume.Volume` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the volume attachment does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent volume attachment. + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the volume attachment does not exist. When set to ``True``, no + exception will be set when attempting to delete a nonexistent + volume attachment. :returns: ``None`` """ - server_id = self._get_uri_attribute(volume_attachment, server, - "server_id") - volume_attachment = resource.Resource._get_id(volume_attachment) + server, volume = self._verify_server_volume_args(server, volume) - self._delete(_volume_attachment.VolumeAttachment, - attachment_id=volume_attachment, - server_id=server_id, - ignore_missing=ignore_missing) + server_id = resource.Resource._get_id(server) + volume_id = resource.Resource._get_id(volume) + + self._delete( + _volume_attachment.VolumeAttachment, + id=volume_id, + server_id=server_id, + ignore_missing=ignore_missing, + ) - def get_volume_attachment(self, volume_attachment, server, - ignore_missing=True): + def get_volume_attachment(self, server, volume): """Get a single volume attachment - :param volume_attachment: - The value can be the ID of a volume attachment or a - :class:`~openstack.compute.v2.volume_attachment.VolumeAttachment` - instance. - :param server: This parameter need to be specified when - VolumeAttachment ID is given as value. It can be either - the ID of a server or a - :class:`~openstack.compute.v2.server.Server` - instance that the attachment belongs to. - :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the volume attachment does not exist. - When set to ``True``, no exception will be set when - attempting to delete a nonexistent volume attachment. + Note that the underlying API expects a volume ID, not a volume + attachment ID. There is currently no way to retrieve volume attachments + by their own ID. + + :param server: The value can be either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance that the + volume is attached to. + :param volume: The value can be the ID of a volume or a + :class:`~openstack.block_storage.v3.volume.Volume` instance. :returns: One :class:`~openstack.compute.v2.volume_attachment.VolumeAttachment` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - server_id = self._get_uri_attribute(volume_attachment, server, - "server_id") - volume_attachment = resource.Resource._get_id(volume_attachment) + server_id = resource.Resource._get_id(server) + volume_id = resource.Resource._get_id(volume) - return self._get(_volume_attachment.VolumeAttachment, - server_id=server_id, - attachment_id=volume_attachment, - ignore_missing=ignore_missing) + return self._get( + _volume_attachment.VolumeAttachment, + id=volume_id, + server_id=server_id, + ) - def volume_attachments(self, server): + def volume_attachments(self, server, **query): """Return a generator of volume attachments :param server: The server can be either the ID of a server or a :class:`~openstack.compute.v2.server.Server`. + :params dict query: Query parameters :returns: A generator of VolumeAttachment objects :rtype: :class:`~openstack.compute.v2.volume_attachment.VolumeAttachment` """ server_id = resource.Resource._get_id(server) - return self._list(_volume_attachment.VolumeAttachment, - server_id=server_id) + return self._list( + _volume_attachment.VolumeAttachment, + server_id=server_id, + **query, + ) # ========== Server Migrations ========== diff --git a/openstack/compute/v2/volume_attachment.py b/openstack/compute/v2/volume_attachment.py index eee123d77..322c553f7 100644 --- a/openstack/compute/v2/volume_attachment.py +++ b/openstack/compute/v2/volume_attachment.py @@ -21,7 +21,7 @@ class VolumeAttachment(resource.Resource): # capabilities allow_create = True allow_fetch = True - allow_commit = False + allow_commit = True allow_delete = True allow_list = True @@ -39,9 +39,9 @@ class VolumeAttachment(resource.Resource): # #: The UUID of the server # server_id = resource.Body('server_uuid') #: The ID of the attached volume. - volume_id = resource.Body('volumeId') + volume_id = resource.Body('volumeId', alternate_id=True) #: The UUID of the associated volume attachment in Cinder. - attachment_id = resource.Body('attachment_id', alternate_id=True) + attachment_id = resource.Body('attachment_id') #: The ID of the block device mapping record for the attachment bdm_id = resource.Body('bdm_uuid') #: Virtual device tags for the attachment. diff --git a/openstack/tests/unit/cloud/test_volume.py b/openstack/tests/unit/cloud/test_volume.py index 0d5f1baf6..1f6ff2f2e 100644 --- a/openstack/tests/unit/cloud/test_volume.py +++ b/openstack/tests/unit/cloud/test_volume.py @@ -191,6 +191,11 @@ def test_detach_volume(self): ]) self.register_uris([ self.get_nova_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', server['id']]), + json={'server': server}), dict(method='DELETE', uri=self.get_mock_url( 'compute', 'public', @@ -207,6 +212,11 @@ def test_detach_volume_exception(self): ]) self.register_uris([ self.get_nova_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', server['id']]), + json={'server': server}), dict(method='DELETE', uri=self.get_mock_url( 'compute', 'public', @@ -230,6 +240,11 @@ def test_detach_volume_wait(self): avail_volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) self.register_uris([ self.get_nova_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', server['id']]), + json={'server': server}), dict(method='DELETE', uri=self.get_mock_url( 'compute', 'public', @@ -254,6 +269,11 @@ def test_detach_volume_wait_error(self): errored_volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) self.register_uris([ self.get_nova_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', + append=['servers', server['id']]), + json={'server': server}), dict(method='DELETE', uri=self.get_mock_url( 'compute', 'public', diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 4f6a4f60c..37cb6786b 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -12,7 +12,10 @@ import datetime from unittest import mock +import uuid +import warnings +from openstack.block_storage.v3 import volume from openstack.compute.v2 import _proxy from openstack.compute.v2 import aggregate from openstack.compute.v2 import availability_zone as az @@ -32,6 +35,7 @@ from openstack.compute.v2 import server_remote_console from openstack.compute.v2 import service from openstack.compute.v2 import usage +from openstack.compute.v2 import volume_attachment from openstack import resource from openstack.tests.unit import test_proxy_base @@ -453,6 +457,157 @@ def test_find_service_args(self): ) +class TestVolumeAttachment(TestComputeProxy): + + def test_volume_attachment_create(self): + self.verify_create( + self.proxy.create_volume_attachment, + volume_attachment.VolumeAttachment, + method_kwargs={'server': 'server_id', 'volume': 'volume_id'}, + expected_kwargs={ + 'server_id': 'server_id', + 'volume_id': 'volume_id', + }, + ) + + def test_volume_attachment_create__legacy_parameters(self): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + + self.verify_create( + self.proxy.create_volume_attachment, + volume_attachment.VolumeAttachment, + method_kwargs={'server': 'server_id', 'volumeId': 'volume_id'}, + expected_kwargs={ + 'server_id': 'server_id', + 'volume_id': 'volume_id', + }, + ) + + self.assertEqual(1, len(w)) + self.assertEqual(w[-1].category, DeprecationWarning) + self.assertIn( + 'This method was called with a volume_id or volumeId argument', + str(w[-1]), + ) + + def test_volume_attachment_create__missing_parameters(self): + exc = self.assertRaises( + TypeError, + self.proxy.create_volume_attachment, + 'server_id', + ) + self.assertIn( + 'create_volume_attachment() missing 1 required positional argument: volume', # noqa: E501 + str(exc), + ) + + def test_volume_attachment_update(self): + self.verify_update( + self.proxy.update_volume_attachment, + volume_attachment.VolumeAttachment, + method_args=[], + method_kwargs={'server': 'server_id', 'volume': 'volume_id'}, + expected_kwargs={ + 'id': 'volume_id', + 'server_id': 'server_id', + 'volume_id': 'volume_id', + }, + ) + + def test_volume_attachment_delete(self): + # We pass objects to avoid the lookup that's done as part of the + # handling of legacy option order. We test that legacy path separately. + fake_server = server.Server(id=str(uuid.uuid4())) + fake_volume = volume.Volume(id=str(uuid.uuid4())) + + self.verify_delete( + self.proxy.delete_volume_attachment, + volume_attachment.VolumeAttachment, + ignore_missing=False, + method_args=[fake_server, fake_volume], + method_kwargs={}, + expected_args=[], + expected_kwargs={ + 'id': fake_volume.id, + 'server_id': fake_server.id, + }, + ) + + def test_volume_attachment_delete__ignore(self): + # We pass objects to avoid the lookup that's done as part of the + # handling of legacy option order. We test that legacy path separately. + fake_server = server.Server(id=str(uuid.uuid4())) + fake_volume = volume.Volume(id=str(uuid.uuid4())) + + self.verify_delete( + self.proxy.delete_volume_attachment, + volume_attachment.VolumeAttachment, + ignore_missing=True, + method_args=[fake_server, fake_volume], + method_kwargs={}, + expected_args=[], + expected_kwargs={ + 'id': fake_volume.id, + 'server_id': fake_server.id, + }, + ) + + def test_volume_attachment_delete__legacy_parameters(self): + fake_server = server.Server(id=str(uuid.uuid4())) + fake_volume = volume.Volume(id=str(uuid.uuid4())) + + with mock.patch.object( + self.proxy, + 'find_server', + return_value=None, + ) as mock_find_server: + # we are calling the method with volume and server ID arguments as + # strings and in the wrong order, which results in a query as we + # attempt to match the server ID to an actual server before we + # switch the argument order once we realize we can't do this + self.verify_delete( + self.proxy.delete_volume_attachment, + volume_attachment.VolumeAttachment, + ignore_missing=False, + method_args=[fake_volume.id, fake_server.id], + method_kwargs={}, + expected_args=[], + expected_kwargs={ + 'id': fake_volume.id, + 'server_id': fake_server.id, + }, + ) + + # note that we attempted to call the server with the volume ID but + # this was mocked to return None (as would happen in the real + # world) + mock_find_server.assert_called_once_with( + fake_volume.id, + ignore_missing=True, + ) + + def test_volume_attachment_get(self): + self.verify_get( + self.proxy.get_volume_attachment, + volume_attachment.VolumeAttachment, + method_args=[], + method_kwargs={'server': 'server_id', 'volume': 'volume_id'}, + expected_kwargs={ + 'id': 'volume_id', + 'server_id': 'server_id', + }, + ) + + def test_volume_attachments(self): + self.verify_list( + self.proxy.volume_attachments, + volume_attachment.VolumeAttachment, + method_kwargs={'server': 'server_id'}, + expected_kwargs={'server_id': 'server_id'}, + ) + + class TestHypervisor(TestComputeProxy): def test_hypervisors_not_detailed(self): diff --git a/openstack/tests/unit/compute/v2/test_volume_attachment.py b/openstack/tests/unit/compute/v2/test_volume_attachment.py index d623fc523..04218a80b 100644 --- a/openstack/tests/unit/compute/v2/test_volume_attachment.py +++ b/openstack/tests/unit/compute/v2/test_volume_attachment.py @@ -35,7 +35,7 @@ def test_basic(self): sot.base_path) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) - self.assertFalse(sot.allow_commit) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) self.assertDictEqual({"limit": "limit", @@ -45,8 +45,8 @@ def test_basic(self): def test_make_it(self): sot = volume_attachment.VolumeAttachment(**EXAMPLE) + self.assertEqual(EXAMPLE['volumeId'], sot.id) self.assertEqual(EXAMPLE['attachment_id'], sot.attachment_id) - self.assertEqual(EXAMPLE['attachment_id'], sot.id) self.assertEqual( EXAMPLE['delete_on_termination'], sot.delete_on_termination, ) diff --git a/releasenotes/notes/compute-volume-attachment-proxy-method-rework-dc35fe9ca3af1c16.yaml b/releasenotes/notes/compute-volume-attachment-proxy-method-rework-dc35fe9ca3af1c16.yaml new file mode 100644 index 000000000..5962e6e82 --- /dev/null +++ b/releasenotes/notes/compute-volume-attachment-proxy-method-rework-dc35fe9ca3af1c16.yaml @@ -0,0 +1,13 @@ +--- +upgrade: + - | + The signatures of the various volume attachment-related methods in the + compute API proxy layer have changed. These were previously incomplete and + did not function as expected in many scenarios. Some callers may need to be + reworked. The affected proxy methods are: + + - ``create_volume_attachment`` + - ``delete_volume_attachment`` + - ``update_volume_attachment`` + - ``get_volume_attachment`` + - ``volume_attachments`` From a426e6565a021d379cf0cd3820ae57d2e502e0b9 Mon Sep 17 00:00:00 2001 From: Polina-Gubina Date: Mon, 12 Sep 2022 15:24:42 +0300 Subject: [PATCH 3124/3836] Add identity.group_users method Add method to list users of a group. Change-Id: Ic2986b107c921752c9e174fe4b7209eec9bf7704 --- doc/source/user/proxies/identity_v3.rst | 2 +- openstack/identity/v3/_proxy.py | 16 ++++++++++++++++ openstack/tests/unit/identity/v3/test_proxy.py | 6 ++++++ ...roup-users-proxy-method-e37f8983b2406819.yaml | 4 ++++ 4 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-identity-group-users-proxy-method-e37f8983b2406819.yaml diff --git a/doc/source/user/proxies/identity_v3.rst b/doc/source/user/proxies/identity_v3.rst index d47e91cb3..9498f47dc 100644 --- a/doc/source/user/proxies/identity_v3.rst +++ b/doc/source/user/proxies/identity_v3.rst @@ -43,7 +43,7 @@ Group Operations :noindex: :members: create_group, update_group, delete_group, get_group, find_group, groups, add_user_to_group, remove_user_from_group, - check_user_in_group + check_user_in_group, group_users Policy Operations ^^^^^^^^^^^^^^^^^ diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 3fbee1d38..c8a50e56e 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -44,6 +44,7 @@ from openstack.identity.v3 import trust as _trust from openstack.identity.v3 import user as _user from openstack import proxy +from openstack import utils class Proxy(proxy.Proxy): @@ -405,6 +406,21 @@ def check_user_in_group(self, user, group): group = self._get_resource(_group.Group, group) return group.check_user(self, user) + def group_users(self, group, **attrs): + """List users in a group + + :param group: Either the ID of a group or a + :class:`~openstack.identity.v3.group.Group` instance. + :param attrs: Only password_expires_at can be filter for result. + + :return: List of :class:`~openstack.identity.v3.user.User` + """ + group = self._get_resource(_group.Group, group) + base_path = utils.urljoin( + group.base_path, group.id, 'users') + users = self._list(_user.User, base_path=base_path, **attrs) + return users + def create_policy(self, **attrs): """Create a new policy from attributes diff --git a/openstack/tests/unit/identity/v3/test_proxy.py b/openstack/tests/unit/identity/v3/test_proxy.py index 62259c735..eb9eb981a 100644 --- a/openstack/tests/unit/identity/v3/test_proxy.py +++ b/openstack/tests/unit/identity/v3/test_proxy.py @@ -168,6 +168,12 @@ def test_check_user_in_group(self): ] ) + def test_group_users(self): + self.verify_list( + self.proxy.group_users, user.User, + method_kwargs={"group": 'group', "attrs": 1}, + expected_kwargs={"attrs": 1}) + class TestIdentityProxyPolicy(TestIdentityProxyBase): diff --git a/releasenotes/notes/add-identity-group-users-proxy-method-e37f8983b2406819.yaml b/releasenotes/notes/add-identity-group-users-proxy-method-e37f8983b2406819.yaml new file mode 100644 index 000000000..88e67dfa6 --- /dev/null +++ b/releasenotes/notes/add-identity-group-users-proxy-method-e37f8983b2406819.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add possibility to list users in the group. From 70dbcfcd66d85875447a29a5abab110af38b6c5b Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 23 Sep 2022 10:19:04 +0200 Subject: [PATCH 3125/3836] Initialize tests of real clouds We want to start testing sdk and osc on real clouds. They are all different, have different settings, policies, etc. In order to cope with that it would be necessary to rework lot of tests and code to ensure all of those "feautres" are properly detected by sdk and osc. Block-storage test base class should not set operator cloud, only tests that require it explicitly should use it Change-Id: I09d118097bade8f0d8ff497957846a754ebc7c1e --- include-acceptance-regular-user.txt | 7 ++++++ openstack/tests/functional/base.py | 25 ++++++++++++++----- .../tests/functional/block_storage/v3/base.py | 2 -- .../functional/block_storage/v3/test_type.py | 4 ++- tox.ini | 17 +++++++++++++ 5 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 include-acceptance-regular-user.txt diff --git a/include-acceptance-regular-user.txt b/include-acceptance-regular-user.txt new file mode 100644 index 000000000..44563523a --- /dev/null +++ b/include-acceptance-regular-user.txt @@ -0,0 +1,7 @@ +# This file contains list of tests that can work with regular user privileges +# Until all tests are modified to properly identify whether they are able to +# run or must skip the ones that are known to work are listed here. +openstack.tests.functional.block_storage.v3.test_volume +# Do not enable test_backup for now, since it is not capable to determine +# backup capabilities of the cloud +# openstack.tests.functional.block_storage.v3.test_backup diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index 1e20f3ee4..7e965eb71 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -12,6 +12,8 @@ import operator import os +import time +import uuid from keystoneauth1 import discover @@ -57,10 +59,11 @@ def setUp(self): self.config = openstack.config.OpenStackConfig() self._set_user_cloud() - self._set_operator_cloud() + if self._op_name: + self._set_operator_cloud() self.identity_version = \ - self.operator_cloud.config.get_api_version('identity') + self.user_cloud.config.get_api_version('identity') self.flavor = self._pick_flavor() self.image = self._pick_image() @@ -78,10 +81,11 @@ def _set_user_cloud(self, **kwargs): # This cloud is used by the project_cleanup test, so you can't rely on # it - user_config_alt = self.config.get_one( - cloud=self._demo_name_alt, **kwargs) - self.user_cloud_alt = connection.Connection(config=user_config_alt) - _disable_keep_alive(self.user_cloud_alt) + if self._demo_name_alt: + user_config_alt = self.config.get_one( + cloud=self._demo_name_alt, **kwargs) + self.user_cloud_alt = connection.Connection(config=user_config_alt) + _disable_keep_alive(self.user_cloud_alt) def _set_operator_cloud(self, **kwargs): operator_config = self.config.get_one(cloud=self._op_name, **kwargs) @@ -216,6 +220,15 @@ def setUp(self): f'{min_microversion}' ) + def getUniqueString(self, prefix=None): + """Generate unique resource name""" + # Globally unique names can only rely on some form of uuid + # unix_t is also used to easier determine orphans when running real + # functional tests on a real cloud + return (prefix if prefix else '') + "{time}-{uuid}".format( + time=int(time.time()), + uuid=uuid.uuid4().hex) + class KeystoneBaseFunctionalTest(BaseFunctionalTest): diff --git a/openstack/tests/functional/block_storage/v3/base.py b/openstack/tests/functional/block_storage/v3/base.py index 7bb01ce73..d9e60e818 100644 --- a/openstack/tests/functional/block_storage/v3/base.py +++ b/openstack/tests/functional/block_storage/v3/base.py @@ -20,7 +20,5 @@ class BaseBlockStorageTest(base.BaseFunctionalTest): def setUp(self): super(BaseBlockStorageTest, self).setUp() self._set_user_cloud(block_storage_api_version='3') - self._set_operator_cloud(block_storage_api_version='3') - if not self.user_cloud.has_service('block-storage', '3'): self.skipTest('block-storage service not supported by cloud') diff --git a/openstack/tests/functional/block_storage/v3/test_type.py b/openstack/tests/functional/block_storage/v3/test_type.py index c86e15930..db7ae74f0 100644 --- a/openstack/tests/functional/block_storage/v3/test_type.py +++ b/openstack/tests/functional/block_storage/v3/test_type.py @@ -22,7 +22,9 @@ def setUp(self): self.TYPE_NAME = self.getUniqueString() self.TYPE_ID = None - + if not self._op_name: + self.skip("Operator cloud must be set for this test") + self._set_operator_cloud(block_storage_api_version='3') sot = self.operator_cloud.block_storage.create_type( name=self.TYPE_NAME) assert isinstance(sot, _type.Type) diff --git a/tox.ini b/tox.ini index ed565d2b4..a9de9c143 100644 --- a/tox.ini +++ b/tox.ini @@ -38,6 +38,23 @@ commands = stestr --test-path ./openstack/tests/functional/{env:OPENSTACKSDK_TESTS_SUBDIR:} run --serial {posargs} stestr slowest +# Acceptance tests are the ones running on real clouds +[testenv:acceptance-regular-user] +# This env intends to test functions of a regular user without admin privileges +# Some jobs (especially heat) takes longer, therefore increase default timeout +# This timeout should not be smaller, than the longest individual timeout +setenv = + {[testenv]setenv} + OS_TEST_TIMEOUT=600 + OPENSTACKSDK_FUNC_TEST_TIMEOUT_LOAD_BALANCER=600 + # OPENSTACKSDK_DEMO_CLOUD and OS_CLOUD should point to the cloud to test + # Othee clouds are explicitly set empty to let tests detect absense + OPENSTACKSDK_DEMO_CLOUD_ALT= + OPENSTACKSDK_OPERATOR_CLOUD= +commands = + stestr --test-path ./openstack/tests/functional/{env:OPENSTACKSDK_TESTS_SUBDIR:} run --serial {posargs} --include-list include-acceptance-regular-user.txt + stestr slowest + [testenv:pep8] deps = hacking>=3.1.0,<4.0.0 # Apache-2.0 From d9ac526933ed237fa725a72ccea4c872282b3f35 Mon Sep 17 00:00:00 2001 From: Danila Balagansky Date: Wed, 29 Jun 2022 13:30:54 +0300 Subject: [PATCH 3126/3836] Cron Triggers proxy Change-Id: I7dbcaf64f6c90410303c1bcfe9a2c4e734819034 --- doc/source/user/proxies/workflow.rst | 8 ++ doc/source/user/resources/workflow/index.rst | 1 + .../resources/workflow/v2/crontrigger.rst | 12 +++ .../tests/unit/workflow/test_cron_trigger.py | 88 +++++++++++++++++++ .../tests/unit/workflow/v2/test_proxy.py | 27 ++++++ openstack/workflow/v2/_proxy.py | 75 ++++++++++++++++ openstack/workflow/v2/cron_trigger.py | 61 +++++++++++++ .../cron_triggers_proxy-51aa89e91bbb9798.yaml | 4 + 8 files changed, 276 insertions(+) create mode 100644 doc/source/user/resources/workflow/v2/crontrigger.rst create mode 100644 openstack/tests/unit/workflow/test_cron_trigger.py create mode 100644 openstack/workflow/v2/cron_trigger.py create mode 100644 releasenotes/notes/cron_triggers_proxy-51aa89e91bbb9798.yaml diff --git a/doc/source/user/proxies/workflow.rst b/doc/source/user/proxies/workflow.rst index 70fd18b9b..cd866e82f 100644 --- a/doc/source/user/proxies/workflow.rst +++ b/doc/source/user/proxies/workflow.rst @@ -25,3 +25,11 @@ Execution Operations :noindex: :members: create_execution, delete_execution, get_execution, find_execution, executions + +Cron Trigger Operations +^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.workflow.v2._proxy.Proxy + :noindex: + :members: create_cron_trigger, delete_cron_trigger, get_cron_trigger, + find_cron_trigger, cron_triggers diff --git a/doc/source/user/resources/workflow/index.rst b/doc/source/user/resources/workflow/index.rst index ee6e70e49..8d66f71fe 100644 --- a/doc/source/user/resources/workflow/index.rst +++ b/doc/source/user/resources/workflow/index.rst @@ -6,3 +6,4 @@ Workflow Resources v2/execution v2/workflow + v2/crontrigger diff --git a/doc/source/user/resources/workflow/v2/crontrigger.rst b/doc/source/user/resources/workflow/v2/crontrigger.rst new file mode 100644 index 000000000..828af768e --- /dev/null +++ b/doc/source/user/resources/workflow/v2/crontrigger.rst @@ -0,0 +1,12 @@ +openstack.workflow.v2.cron_trigger +================================== + +.. automodule:: openstack.workflow.v2.cron_trigger + +The CronTrigger Class +--------------------- + +The ``CronTrigger`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.workflow.v2.cron_trigger.CronTrigger + :members: diff --git a/openstack/tests/unit/workflow/test_cron_trigger.py b/openstack/tests/unit/workflow/test_cron_trigger.py new file mode 100644 index 000000000..02ce8a676 --- /dev/null +++ b/openstack/tests/unit/workflow/test_cron_trigger.py @@ -0,0 +1,88 @@ +# 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.tests.unit import base +from openstack.workflow.v2 import cron_trigger + + +FAKE_INPUT = { + 'cluster_id': '8c74607c-5a74-4490-9414-a3475b1926c2', + 'node_id': 'fba2cc5d-706f-4631-9577-3956048d13a2', + 'flavor_id': '1' +} + +FAKE_PARAMS = {} + +FAKE = { + 'id': 'ffaed25e-46f5-4089-8e20-b3b4722fd597', + 'pattern': '0 * * * *', + 'remaining_executions': 14, + 'first_execution_time': '1970-01-01T01:00:00.000000', + 'next_execution_time': '1970-01-01T02:00:00.000000', + 'workflow_name': 'cluster-coldmigration', + 'workflow_id': '1995cf40-c22d-4968-b6e8-558942830642', + 'workflow_input': FAKE_INPUT, + 'workflow_params': FAKE_PARAMS, +} + + +class TestCronTrigger(base.TestCase): + + def test_basic(self): + sot = cron_trigger.CronTrigger() + self.assertEqual('cron_trigger', sot.resource_key) + self.assertEqual('cron_triggers', sot.resources_key) + self.assertEqual('/cron_triggers', sot.base_path) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_delete) + + self.assertDictEqual( + { + 'marker': 'marker', + 'limit': 'limit', + 'sort_keys': 'sort_keys', + 'sort_dirs': 'sort_dirs', + 'fields': 'fields', + 'name': 'name', + 'workflow_name': 'workflow_name', + 'workflow_id': 'workflow_id', + 'workflow_input': 'workflow_input', + 'workflow_params': 'workflow_params', + 'scope': 'scope', + 'pattern': 'pattern', + 'remaining_executions': 'remaining_executions', + 'project_id': 'project_id', + 'first_execution_time': 'first_execution_time', + 'next_execution_time': 'next_execution_time', + 'created_at': 'created_at', + 'updated_at': 'updated_at', + 'all_projects': 'all_projects', + }, + sot._query_mapping._mapping + ) + + def test_make_it(self): + sot = cron_trigger.CronTrigger(**FAKE) + self.assertEqual(FAKE['id'], sot.id) + self.assertEqual(FAKE['pattern'], sot.pattern) + self.assertEqual(FAKE['remaining_executions'], + sot.remaining_executions) + self.assertEqual(FAKE['first_execution_time'], + sot.first_execution_time) + self.assertEqual(FAKE['next_execution_time'], + sot.next_execution_time) + self.assertEqual(FAKE['workflow_name'], sot.workflow_name) + self.assertEqual(FAKE['workflow_id'], sot.workflow_id) + self.assertEqual(FAKE['workflow_input'], sot.workflow_input) + self.assertEqual(FAKE['workflow_params'], sot.workflow_params) diff --git a/openstack/tests/unit/workflow/v2/test_proxy.py b/openstack/tests/unit/workflow/v2/test_proxy.py index 2f6ce967e..2ba862d09 100644 --- a/openstack/tests/unit/workflow/v2/test_proxy.py +++ b/openstack/tests/unit/workflow/v2/test_proxy.py @@ -12,6 +12,7 @@ from openstack.tests.unit import test_proxy_base from openstack.workflow.v2 import _proxy +from openstack.workflow.v2 import cron_trigger from openstack.workflow.v2 import execution from openstack.workflow.v2 import workflow @@ -60,3 +61,29 @@ def test_workflow_find(self): def test_execution_find(self): self.verify_find(self.proxy.find_execution, execution.Execution) + + +class TestCronTriggerProxy(test_proxy_base.TestProxyBase): + def setUp(self): + super().setUp() + self.proxy = _proxy.Proxy(self.session) + + def test_cron_triggers(self): + self.verify_list(self.proxy.cron_triggers, + cron_trigger.CronTrigger) + + def test_cron_trigger_get(self): + self.verify_get(self.proxy.get_cron_trigger, + cron_trigger.CronTrigger) + + def test_cron_trigger_create(self): + self.verify_create(self.proxy.create_cron_trigger, + cron_trigger.CronTrigger) + + def test_cron_trigger_delete(self): + self.verify_delete(self.proxy.delete_cron_trigger, + cron_trigger.CronTrigger, True) + + def test_cron_trigger_find(self): + self.verify_find(self.proxy.find_cron_trigger, + cron_trigger.CronTrigger) diff --git a/openstack/workflow/v2/_proxy.py b/openstack/workflow/v2/_proxy.py index 784fd8644..296ba210b 100644 --- a/openstack/workflow/v2/_proxy.py +++ b/openstack/workflow/v2/_proxy.py @@ -11,6 +11,7 @@ # under the License. from openstack import proxy +from openstack.workflow.v2 import cron_trigger as _cron_trigger from openstack.workflow.v2 import execution as _execution from openstack.workflow.v2 import workflow as _workflow @@ -166,3 +167,77 @@ def find_execution(self, name_or_id, ignore_missing=True): """ return self._find(_execution.Execution, name_or_id, ignore_missing=ignore_missing) + + def create_cron_trigger(self, **attrs): + """Create a new cron trigger from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.workflow.v2.cron_trigger.CronTrigger`, + comprised of the properties on the CronTrigger class. + + :returns: The results of cron trigger creation + :rtype: :class:`~openstack.workflow.v2.cron_trigger.CronTrigger` + """ + return self._create(_cron_trigger.CronTrigger, **attrs) + + def get_cron_trigger(self, cron_trigger): + """Get a cron trigger + + :param cron_trigger: The value can be the name of a cron_trigger or + :class:`~openstack.workflow.v2.cron_trigger.CronTrigger` instance. + + :returns: One :class:`~openstack.workflow.v2.cron_trigger.CronTrigger` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + cron triggers matching the criteria could be found. + """ + return self._get(_cron_trigger.CronTrigger, cron_trigger) + + def cron_triggers(self, **query): + """Retrieve a generator of cron triggers + + :param kwargs query: Optional query parameters to be sent to + restrict the cron triggers to be returned. Available parameters + include: + + * limit: Requests at most the specified number of items be + returned from the query. + * marker: Specifies the ID of the last-seen cron trigger. Use the + limit parameter to make an initial limited request and use + the ID of the last-seen cron trigger from the response as the + marker parameter value in a subsequent limited request. + + :returns: A generator of CronTrigger instances. + """ + return self._list(_cron_trigger.CronTrigger, **query) + + def delete_cron_trigger(self, value, ignore_missing=True): + """Delete a cron trigger + + :param value: The value can be either the name of a cron trigger or a + :class:`~openstack.workflow.v2.cron_trigger.CronTrigger` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the cron trigger does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent cron trigger. + + :returns: ``None`` + """ + return self._delete(_cron_trigger.CronTrigger, value, + ignore_missing=ignore_missing) + + def find_cron_trigger(self, name_or_id, ignore_missing=True): + """Find a single cron trigger + + :param name_or_id: The name or ID of a cron trigger. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :returns: One :class:`~openstack.compute.v2.cron_trigger.CronTrigger` + or None + """ + return self._find(_cron_trigger.CronTrigger, name_or_id, + ignore_missing=ignore_missing) diff --git a/openstack/workflow/v2/cron_trigger.py b/openstack/workflow/v2/cron_trigger.py new file mode 100644 index 000000000..f38f58fb7 --- /dev/null +++ b/openstack/workflow/v2/cron_trigger.py @@ -0,0 +1,61 @@ +# 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 resource + + +class CronTrigger(resource.Resource): + resource_key = 'cron_trigger' + resources_key = 'cron_triggers' + base_path = '/cron_triggers' + + # capabilities + allow_create = True + allow_list = True + allow_fetch = True + allow_delete = True + + _query_mapping = resource.QueryParameters( + 'marker', 'limit', 'sort_keys', 'sort_dirs', 'fields', 'name', + 'workflow_name', 'workflow_id', 'workflow_input', 'workflow_params', + 'scope', 'pattern', 'remaining_executions', 'project_id', + 'first_execution_time', 'next_execution_time', 'created_at', + 'updated_at', 'all_projects') + + #: The name of this Cron Trigger + name = resource.Body("name") + #: The pattern for this Cron Trigger + pattern = resource.Body("pattern") + #: Count of remaining exectuions + remaining_executions = resource.Body("remaining_executions") + #: Time of the first execution + first_execution_time = resource.Body("first_execution_time") + #: Time of the next execution + next_execution_time = resource.Body("next_execution_time") + #: Workflow name + workflow_name = resource.Body("workflow_name") + #: Workflow ID + workflow_id = resource.Body("workflow_id") + #: The inputs for Workflow + workflow_input = resource.Body("workflow_input") + #: Workflow params + workflow_params = resource.Body("workflow_params") + #: The ID of the associated project + project_id = resource.Body("project_id") + #: The time at which the cron trigger was created + created_at = resource.Body("created_at") + #: The time at which the cron trigger was created + updated_at = resource.Body("updated_at") + + def create(self, session, base_path=None): + return super(CronTrigger, self).create( + session, prepend_key=False, base_path=base_path) diff --git a/releasenotes/notes/cron_triggers_proxy-51aa89e91bbb9798.yaml b/releasenotes/notes/cron_triggers_proxy-51aa89e91bbb9798.yaml new file mode 100644 index 000000000..c9ec39144 --- /dev/null +++ b/releasenotes/notes/cron_triggers_proxy-51aa89e91bbb9798.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add workflow CronTrigger resource and proxy methods. From 64c83bc894c93f1986acee3449b3d1df6dd49c8d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 20 Oct 2022 18:15:46 +0100 Subject: [PATCH 3127/3836] image: Correct typo with 'get_import_info' proxy method The argument is 'requires_id' (plural), not 'require_id'. Change-Id: I52c5cbb0b1e7db9e7006bea296fa944d8b4ca69e Signed-off-by: Stephen Finucane --- openstack/image/v2/_proxy.py | 2 +- openstack/tests/unit/image/v2/test_proxy.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index b819c8ae3..77f7f1b5a 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -1033,7 +1033,7 @@ def get_import_info(self): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get(_si.Import, require_id=False) + return self._get(_si.Import, requires_id=False) # ====== UTILS ====== def wait_for_delete(self, res, interval=2, wait=120): diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 6aeaa1fe7..3474669a3 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -598,7 +598,7 @@ def test_import_info(self): method_args=[], method_kwargs={}, expected_args=[si.Import], - expected_kwargs={'require_id': False}) + expected_kwargs={'requires_id': False}) class TestMetadefSchema(TestImageProxy): From 89f839daf571c858350dd11fe19a28672260125a Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 19 Oct 2022 21:09:08 +0100 Subject: [PATCH 3128/3836] image: Correct typo Change-Id: I3e84bff0b4fa7d2faefb61ea011b5b86adb8a3c3 Signed-off-by: Stephen Finucane --- openstack/image/v2/_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 77f7f1b5a..5a2c38e5c 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -530,7 +530,7 @@ def deactivate_image(self, image): image.deactivate(self) def reactivate_image(self, image): - """Deactivate an image + """Reactivate an image :param image: Either the ID of a image or a :class:`~openstack.image.v2.image.Image` instance. From d05654e9bc568672a155fc33c44a6154a1aacadc Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 19 Oct 2022 21:38:26 +0100 Subject: [PATCH 3129/3836] image: Allow listing detailed view of stores Change-Id: I7bcddeafdf9f9e7d917c2769c3dab4e9db331cc0 Signed-off-by: Stephen Finucane --- openstack/image/v2/_proxy.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 5a2c38e5c..3b733e0e4 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -1017,12 +1017,14 @@ def wait_for_task(self, task, status='success', failures=None, 'current state is %s', name, status, new_status) # ====== STORES ====== - def stores(self, **query): + def stores(self, details=False, **query): """Return a generator of supported image stores :returns: A generator of store objects :rtype: :class:`~openstack.image.v2.service_info.Store` """ + if details: + query['base_path'] = utils.urljoin(_si.Store, 'details') return self._list(_si.Store, **query) # ====== IMPORTS ====== From caa4e135e275ce6b453557d701a78701690d57b4 Mon Sep 17 00:00:00 2001 From: Sahid Orentino Ferdjaoui Date: Fri, 21 Oct 2022 10:03:58 +0200 Subject: [PATCH 3130/3836] compute/server: add support of target state for evacuate API Related-To: bp/allowing-target-state-for-evacuate Signed-off-by: Sahid Orentino Ferdjaoui Change-Id: Ieb8681bb4fd4834666b7bdb31d0ea6339c396400 --- openstack/compute/v2/_proxy.py | 7 +++++-- openstack/compute/v2/server.py | 5 ++++- openstack/tests/unit/compute/v2/test_proxy.py | 8 +++++--- openstack/tests/unit/compute/v2/test_server.py | 4 ++-- .../add-target-state-for-evacuate-52be85c5c08f4109.yaml | 3 +++ 5 files changed, 19 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/add-target-state-for-evacuate-52be85c5c08f4109.yaml diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 3f0373d1f..cab935ea9 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -944,7 +944,8 @@ def unrescue_server(self, server): server = self._get_resource(_server.Server, server) server.unrescue(self) - def evacuate_server(self, server, host=None, admin_pass=None, force=None): + def evacuate_server(self, server, host=None, admin_pass=None, force=None, + target_state=None): """Evacuates a server from a failed host to a new host. :param server: Either the ID of a server or a @@ -956,11 +957,13 @@ def evacuate_server(self, server, host=None, admin_pass=None, force=None): :param force: Force an evacuation by not verifying the provided destination host by the scheduler. (New in API version 2.29). + :param target_state: Set target state for the evacuated instance (New + in API version 2.94). :returns: None """ server = self._get_resource(_server.Server, server) server.evacuate(self, host=host, admin_pass=admin_pass, - force=force) + force=force, target_state=target_state) def start_server(self, server): """Starts a stopped server and changes its state to ``ACTIVE``. diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index b8fb906f5..c7ca510db 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -459,7 +459,8 @@ def unrescue(self, session): body = {"unrescue": None} self._action(session, body) - def evacuate(self, session, host=None, admin_pass=None, force=None): + def evacuate(self, session, host=None, admin_pass=None, force=None, + target_state=None): body = {"evacuate": {}} if host is not None: body["evacuate"]["host"] = host @@ -467,6 +468,8 @@ def evacuate(self, session, host=None, admin_pass=None, force=None): body["evacuate"]["adminPass"] = admin_pass if force is not None: body["evacuate"]["force"] = force + if target_state is not None: + body["evacuate"]["targetState"] = target_state self._action(session, body) def start(self, session): diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 4f6a4f60c..8255b7108 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -882,16 +882,18 @@ def test_server_evacuate(self): self.proxy.evacuate_server, method_args=["value"], expected_args=[self.proxy], - expected_kwargs={"host": None, "admin_pass": None, "force": None}) + expected_kwargs={"host": None, "admin_pass": None, "force": None, + "target_state": None}) def test_server_evacuate_with_options(self): self._verify( "openstack.compute.v2.server.Server.evacuate", self.proxy.evacuate_server, - method_args=["value", 'HOST2', 'NEW_PASS', True], + method_args=["value", 'HOST2', 'NEW_PASS', True, 'stopped'], expected_args=[self.proxy], expected_kwargs={ - "host": "HOST2", "admin_pass": 'NEW_PASS', "force": True}) + "host": "HOST2", "admin_pass": 'NEW_PASS', "force": True, + "target_state": 'stopped'}) def test_server_start(self): self._verify( diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index f677e21d5..174fbb6e4 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -735,12 +735,12 @@ def test_evacuate_with_options(self): sot = server.Server(**EXAMPLE) res = sot.evacuate(self.sess, host='HOST2', admin_pass='NEW_PASS', - force=True) + force=True, target_state='stopped') self.assertIsNone(res) url = 'servers/IDENTIFIER/action' body = {"evacuate": {'host': 'HOST2', 'adminPass': 'NEW_PASS', - 'force': True}} + 'force': True, 'targetState': 'stopped'}} headers = {'Accept': ''} self.sess.post.assert_called_with( url, json=body, headers=headers, microversion=None) diff --git a/releasenotes/notes/add-target-state-for-evacuate-52be85c5c08f4109.yaml b/releasenotes/notes/add-target-state-for-evacuate-52be85c5c08f4109.yaml new file mode 100644 index 000000000..4580f378f --- /dev/null +++ b/releasenotes/notes/add-target-state-for-evacuate-52be85c5c08f4109.yaml @@ -0,0 +1,3 @@ +--- +features: + - Add support of setting target state for evacuate API. From 72b0956df8811be1de78171b3c4a9401daf26ccb Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 19 Oct 2022 21:38:02 +0100 Subject: [PATCH 3131/3836] image: Add 'store' argument to 'delete_image' proxy method Allow users to delete an image from a specific store. Change-Id: I5e2a5f03f05d25e0286686fa6f838bae6b0737d0 Signed-off-by: Stephen Finucane --- openstack/image/v2/_proxy.py | 12 +++++++-- openstack/image/v2/service_info.py | 27 +++++++++++++++++++ openstack/tests/unit/image/v2/test_proxy.py | 15 ++++++++++- .../tests/unit/image/v2/test_service_info.py | 15 ++++++++++- 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 3b733e0e4..37c8cafd6 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -453,11 +453,15 @@ def download_image(self, image, stream=False, output=None, return image.download( self, stream=stream, output=output, chunk_size=chunk_size) - def delete_image(self, image, ignore_missing=True): + def delete_image(self, image, *, store=None, ignore_missing=True): """Delete an image :param image: The value can be either the ID of an image or a :class:`~openstack.image.v2.image.Image` instance. + :param store: The value can be either the ID of a store or a + :class:`~openstack.image.v2.service_info.Store` instance that the + image is associated with. If specified, the image will only be + deleted from the specified store. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the image does not exist. @@ -466,7 +470,11 @@ def delete_image(self, image, ignore_missing=True): :returns: ``None`` """ - self._delete(_image.Image, image, ignore_missing=ignore_missing) + if store: + store = self._get_resource(_si.Store, store) + store.delete_image(self, image, ignore_missing=ignore_missing) + else: + self._delete(_image.Image, image, ignore_missing=ignore_missing) def find_image(self, name_or_id, ignore_missing=True): """Find a single image diff --git a/openstack/image/v2/service_info.py b/openstack/image/v2/service_info.py index 733e8a34b..15023b028 100644 --- a/openstack/image/v2/service_info.py +++ b/openstack/image/v2/service_info.py @@ -10,7 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import exceptions from openstack import resource +from openstack import utils class Import(resource.Resource): @@ -34,3 +36,28 @@ class Store(resource.Resource): description = resource.Body('description') #: default is_default = resource.Body('default', type=bool) + + def delete_image(self, session, image, *, ignore_missing=False): + """Delete image from store + + :param session: The session to use for making this request. + :param image: The value can be either the ID of an image or a + :class:`~openstack.image.v2.image.Image` instance. + + :returns: The result of the ``delete`` if resource found, else None. + :raises: :class:`~openstack.exceptions.ResourceNotFound` when + ignore_missing if ``False`` and a nonexistent resource + is attempted to be deleted. + """ + image_id = resource.Resource._get_id(image) + url = utils.urljoin('/stores', self.id, image_id) + + try: + response = session.delete(url) + exceptions.raise_from_response(response) + except exceptions.ResourceNotFound: + if ignore_missing: + return None + raise + + return response diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 3474669a3..3aef1e546 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -296,9 +296,22 @@ def test_image_stage_wrong_status(self): def test_image_delete(self): self.verify_delete(self.proxy.delete_image, image.Image, False) - def test_image_delete_ignore(self): + def test_image_delete__ignore(self): self.verify_delete(self.proxy.delete_image, image.Image, True) + def test_delete_image__from_store(self): + store = si.Store(id='fast', is_default=True) + store.delete_image = mock.Mock() + img = image.Image(id="id", status="queued") + + self.proxy.delete_image(img, store=store) + + store.delete_image.assert_called_with( + self.proxy, + img, + ignore_missing=True, + ) + @mock.patch("openstack.resource.Resource._translate_response") @mock.patch("openstack.proxy.Proxy._get") @mock.patch("openstack.image.v2.image.Image.commit") diff --git a/openstack/tests/unit/image/v2/test_service_info.py b/openstack/tests/unit/image/v2/test_service_info.py index f00ab88ff..8e3ad4a51 100644 --- a/openstack/tests/unit/image/v2/test_service_info.py +++ b/openstack/tests/unit/image/v2/test_service_info.py @@ -10,6 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from unittest import mock + +from openstack import exceptions from openstack.image.v2 import service_info as si from openstack.tests.unit import base @@ -26,7 +29,7 @@ } } EXAMPLE_STORE = { - 'id': 'fast', + 'id': IDENTIFIER, 'description': 'Fast access to rbd store', 'default': True } @@ -50,6 +53,16 @@ def test_make_it(self): self.assertEqual(EXAMPLE_STORE['description'], sot.description) self.assertEqual(EXAMPLE_STORE['default'], sot.is_default) + @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) + def test_delete_image(self): + sot = si.Store(**EXAMPLE_STORE) + session = mock.Mock() + session.delete = mock.Mock() + + sot.delete_image(session, image='image_id') + + session.delete.assert_called_with('stores/IDENTIFIER/image_id') + class TestImport(base.TestCase): def test_basic(self): From d9404045ef382bc95006893fe5130b9ed851c896 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 21 Oct 2022 13:18:46 +0100 Subject: [PATCH 3132/3836] tests: Avoid potential aliasing of imports Change-Id: Icefa64f8a93a9267915962637d560e1370257092 Signed-off-by: Stephen Finucane --- openstack/tests/unit/image/v2/test_proxy.py | 486 ++++++++++++-------- 1 file changed, 294 insertions(+), 192 deletions(-) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 3aef1e546..f8b85c683 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -17,13 +17,13 @@ from openstack import exceptions from openstack.image.v2 import _proxy -from openstack.image.v2 import image -from openstack.image.v2 import member -from openstack.image.v2 import metadef_namespace -from openstack.image.v2 import metadef_schema -from openstack.image.v2 import schema -from openstack.image.v2 import service_info as si -from openstack.image.v2 import task +from openstack.image.v2 import image as _image +from openstack.image.v2 import member as _member +from openstack.image.v2 import metadef_namespace as _metadef_namespace +from openstack.image.v2 import metadef_schema as _metadef_schema +from openstack.image.v2 import schema as _schema +from openstack.image.v2 import service_info as _service_info +from openstack.image.v2 import task as _task from openstack.tests.unit.image.v2 import test_image as fake_image from openstack.tests.unit import test_proxy_base @@ -51,13 +51,15 @@ def setUp(self): class TestImage(TestImageProxy): def test_image_import_no_required_attrs(self): # container_format and disk_format are required attrs of the image - existing_image = image.Image(id="id") - self.assertRaises(exceptions.InvalidRequest, - self.proxy.import_image, - existing_image) + existing_image = _image.Image(id="id") + self.assertRaises( + exceptions.InvalidRequest, + self.proxy.import_image, + existing_image, + ) def test_image_import(self): - original_image = image.Image(**EXAMPLE) + original_image = _image.Image(**EXAMPLE) self._verify( "openstack.image.v2.image.Image.import_image", self.proxy.import_image, @@ -70,45 +72,52 @@ def test_image_import(self): "stores": [], "all_stores": None, "all_stores_must_succeed": None, - }) + }, + ) def test_image_create_conflict(self): self.assertRaises( - exceptions.SDKException, self.proxy.create_image, - name='fake', filename='fake', data='fake', - container='bare', disk_format='raw' + exceptions.SDKException, + self.proxy.create_image, + name='fake', + filename='fake', + data='fake', + container='bare', + disk_format='raw', ) def test_image_create_checksum_match(self): - fake_image = image.Image( - id="fake", properties={ + fake_image = _image.Image( + id="fake", + properties={ self.proxy._IMAGE_MD5_KEY: 'fake_md5', - self.proxy._IMAGE_SHA256_KEY: 'fake_sha256' - }) + self.proxy._IMAGE_SHA256_KEY: 'fake_sha256', + }, + ) self.proxy.find_image = mock.Mock(return_value=fake_image) self.proxy._upload_image = mock.Mock() res = self.proxy.create_image( - name='fake', - md5='fake_md5', sha256='fake_sha256' + name='fake', md5='fake_md5', sha256='fake_sha256' ) self.assertEqual(fake_image, res) self.proxy._upload_image.assert_not_called() def test_image_create_checksum_mismatch(self): - fake_image = image.Image( - id="fake", properties={ + fake_image = _image.Image( + id="fake", + properties={ self.proxy._IMAGE_MD5_KEY: 'fake_md5', - self.proxy._IMAGE_SHA256_KEY: 'fake_sha256' - }) + self.proxy._IMAGE_SHA256_KEY: 'fake_sha256', + }, + ) self.proxy.find_image = mock.Mock(return_value=fake_image) self.proxy._upload_image = mock.Mock() self.proxy.create_image( - name='fake', data=b'fake', - md5='fake2_md5', sha256='fake2_sha256' + name='fake', data=b'fake', md5='fake2_md5', sha256='fake2_sha256' ) self.proxy._upload_image.assert_called() @@ -118,45 +127,61 @@ def test_image_create_allow_duplicates_find_not_called(self): self.proxy._upload_image = mock.Mock() self.proxy.create_image( - name='fake', data=b'fake', allow_duplicates=True, + name='fake', + data=b'fake', + allow_duplicates=True, ) self.proxy.find_image.assert_not_called() def test_image_create_validate_checksum_data_binary(self): - """ Pass real data as binary""" + """Pass real data as binary""" self.proxy.find_image = mock.Mock() self.proxy._upload_image = mock.Mock() self.proxy.create_image( - name='fake', data=b'fake', validate_checksum=True, - container='bare', disk_format='raw' + name='fake', + data=b'fake', + validate_checksum=True, + container='bare', + disk_format='raw', ) self.proxy.find_image.assert_called_with('fake') self.proxy._upload_image.assert_called_with( - 'fake', container_format='bare', disk_format='raw', - filename=None, data=b'fake', meta={}, + 'fake', + container_format='bare', + disk_format='raw', + filename=None, + data=b'fake', + meta={}, properties={ self.proxy._IMAGE_MD5_KEY: '144c9defac04969c7bfad8efaa8ea194', self.proxy._IMAGE_SHA256_KEY: 'b5d54c39e66671c9731b9f471e585' - 'd8262cd4f54963f0c93082d8dcf33' - '4d4c78', - self.proxy._IMAGE_OBJECT_KEY: 'bare/fake'}, - timeout=3600, validate_checksum=True, + 'd8262cd4f54963f0c93082d8dcf33' + '4d4c78', + self.proxy._IMAGE_OBJECT_KEY: 'bare/fake', + }, + timeout=3600, + validate_checksum=True, use_import=False, stores=None, all_stores=None, all_stores_must_succeed=None, - wait=False) + wait=False, + ) def test_image_create_validate_checksum_data_not_binary(self): self.assertRaises( - exceptions.SDKException, self.proxy.create_image, - name='fake', data=io.StringIO(), validate_checksum=True, - container='bare', disk_format='raw' + exceptions.SDKException, + self.proxy.create_image, + name='fake', + data=io.StringIO(), + validate_checksum=True, + container='bare', + disk_format='raw', ) def test_image_create_data_binary(self): @@ -168,53 +193,71 @@ def test_image_create_data_binary(self): data = io.BytesIO(b'\0\0') self.proxy.create_image( - name='fake', data=data, validate_checksum=False, - container='bare', disk_format='raw' + name='fake', + data=data, + validate_checksum=False, + container='bare', + disk_format='raw', ) self.proxy._upload_image.assert_called_with( - 'fake', container_format='bare', disk_format='raw', - filename=None, data=data, meta={}, + 'fake', + container_format='bare', + disk_format='raw', + filename=None, + data=data, + meta={}, properties={ self.proxy._IMAGE_MD5_KEY: '', self.proxy._IMAGE_SHA256_KEY: '', - self.proxy._IMAGE_OBJECT_KEY: 'bare/fake'}, - timeout=3600, validate_checksum=False, + self.proxy._IMAGE_OBJECT_KEY: 'bare/fake', + }, + timeout=3600, + validate_checksum=False, use_import=False, stores=None, all_stores=None, all_stores_must_succeed=None, - wait=False) + wait=False, + ) def test_image_create_without_filename(self): self.proxy._create_image = mock.Mock() self.proxy.create_image( allow_duplicates=True, - name='fake', disk_format="fake_dformat", - container_format="fake_cformat" + name='fake', + disk_format="fake_dformat", + container_format="fake_cformat", ) self.proxy._create_image.assert_called_with( - container_format='fake_cformat', disk_format='fake_dformat', - name='fake', properties=mock.ANY) + container_format='fake_cformat', + disk_format='fake_dformat', + name='fake', + properties=mock.ANY, + ) def test_image_create_protected(self): self.proxy.find_image = mock.Mock() - created_image = mock.Mock(spec=image.Image(id="id")) + created_image = mock.Mock(spec=_image.Image(id="id")) self.proxy._create = mock.Mock() self.proxy._create.return_value = created_image self.proxy._create.return_value.image_import_methods = [] created_image.upload = mock.Mock() - created_image.upload.return_value = FakeResponse(response="", - status_code=200) + created_image.upload.return_value = FakeResponse( + response="", status_code=200 + ) properties = {"is_protected": True} self.proxy.create_image( - name="fake", data="data", container_format="bare", - disk_format="raw", **properties + name="fake", + data="data", + container_format="bare", + disk_format="raw", + **properties ) args, kwargs = self.proxy._create.call_args @@ -228,23 +271,26 @@ def test_image_upload(self): # NOTE: This doesn't use any of the base class verify methods # because it ends up making two separate calls to complete the # operation. - created_image = mock.Mock(spec=image.Image(id="id")) + created_image = mock.Mock(spec=_image.Image(id="id")) self.proxy._create = mock.Mock() self.proxy._create.return_value = created_image - rv = self.proxy.upload_image(data="data", container_format="x", - disk_format="y", name="z") + rv = self.proxy.upload_image( + data="data", container_format="x", disk_format="y", name="z" + ) - self.proxy._create.assert_called_with(image.Image, - container_format="x", - disk_format="y", - name="z") + self.proxy._create.assert_called_with( + _image.Image, + container_format="x", + disk_format="y", + name="z", + ) created_image.upload.assert_called_with(self.proxy) self.assertEqual(rv, created_image) def test_image_download(self): - original_image = image.Image(**EXAMPLE) + original_image = _image.Image(**EXAMPLE) self._verify( 'openstack.image.v2.image.Image.download', self.proxy.download_image, @@ -252,120 +298,130 @@ def test_image_download(self): method_kwargs={ 'output': 'some_output', 'chunk_size': 1, - 'stream': True + 'stream': True, }, expected_args=[self.proxy], expected_kwargs={ 'output': 'some_output', 'chunk_size': 1, 'stream': True, - }) + }, + ) @mock.patch("openstack.image.v2.image.Image.fetch") def test_image_stage(self, mock_fetch): - img = image.Image(id="id", status="queued") - img.stage = mock.Mock() + image = _image.Image(id="id", status="queued") + image.stage = mock.Mock() - self.proxy.stage_image(image=img) + self.proxy.stage_image(image=image) mock_fetch.assert_called() - img.stage.assert_called_with(self.proxy) + image.stage.assert_called_with(self.proxy) @mock.patch("openstack.image.v2.image.Image.fetch") def test_image_stage_with_data(self, mock_fetch): - img = image.Image(id="id", status="queued") - img.stage = mock.Mock() - mock_fetch.return_value = img + image = _image.Image(id="id", status="queued") + image.stage = mock.Mock() + mock_fetch.return_value = image - rv = self.proxy.stage_image(image=img, data="data") + rv = self.proxy.stage_image(image=image, data="data") - img.stage.assert_called_with(self.proxy) + image.stage.assert_called_with(self.proxy) mock_fetch.assert_called() self.assertEqual(rv.data, "data") def test_image_stage_wrong_status(self): - img = image.Image(id="id", status="active") - img.stage = mock.Mock() + image = _image.Image(id="id", status="active") + image.stage = mock.Mock() self.assertRaises( exceptions.SDKException, self.proxy.stage_image, - img, - "data" + image, + "data", ) def test_image_delete(self): - self.verify_delete(self.proxy.delete_image, image.Image, False) + self.verify_delete(self.proxy.delete_image, _image.Image, False) def test_image_delete__ignore(self): - self.verify_delete(self.proxy.delete_image, image.Image, True) + self.verify_delete(self.proxy.delete_image, _image.Image, True) def test_delete_image__from_store(self): - store = si.Store(id='fast', is_default=True) + store = _service_info.Store(id='fast', is_default=True) store.delete_image = mock.Mock() - img = image.Image(id="id", status="queued") + image = _image.Image(id="id", status="queued") - self.proxy.delete_image(img, store=store) + self.proxy.delete_image(image, store=store) store.delete_image.assert_called_with( self.proxy, - img, + image, ignore_missing=True, ) @mock.patch("openstack.resource.Resource._translate_response") @mock.patch("openstack.proxy.Proxy._get") @mock.patch("openstack.image.v2.image.Image.commit") - def test_image_update(self, mock_commit_image, mock_get_image, - mock_transpose): - original_image = image.Image(**EXAMPLE) + def test_image_update( + self, mock_commit_image, mock_get_image, mock_transpose + ): + original_image = _image.Image(**EXAMPLE) mock_get_image.return_value = original_image EXAMPLE['name'] = 'fake_name' - updated_image = image.Image(**EXAMPLE) + updated_image = _image.Image(**EXAMPLE) mock_commit_image.return_value = updated_image.to_dict() - result = self.proxy.update_image(original_image, - **updated_image.to_dict()) + result = self.proxy.update_image( + original_image, **updated_image.to_dict() + ) self.assertEqual('fake_name', result.get('name')) def test_image_get(self): - self.verify_get(self.proxy.get_image, image.Image) + self.verify_get(self.proxy.get_image, _image.Image) def test_images(self): - self.verify_list(self.proxy.images, image.Image) + self.verify_list(self.proxy.images, _image.Image) def test_add_tag(self): self._verify( "openstack.image.v2.image.Image.add_tag", self.proxy.add_tag, method_args=["image", "tag"], - expected_args=[self.proxy, "tag"]) + expected_args=[self.proxy, "tag"], + ) def test_remove_tag(self): self._verify( "openstack.image.v2.image.Image.remove_tag", self.proxy.remove_tag, method_args=["image", "tag"], - expected_args=[self.proxy, "tag"]) + expected_args=[self.proxy, "tag"], + ) def test_deactivate_image(self): self._verify( "openstack.image.v2.image.Image.deactivate", self.proxy.deactivate_image, method_args=["image"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_reactivate_image(self): self._verify( "openstack.image.v2.image.Image.reactivate", self.proxy.reactivate_image, method_args=["image"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) class TestMember(TestImageProxy): def test_member_create(self): - self.verify_create(self.proxy.add_member, member.Member, - method_kwargs={"image": "test_id"}, - expected_kwargs={"image_id": "test_id"}) + self.verify_create( + self.proxy.add_member, + _member.Member, + method_kwargs={"image": "test_id"}, + expected_kwargs={"image_id": "test_id"}, + ) def test_member_delete(self): self._verify( @@ -373,11 +429,13 @@ def test_member_delete(self): self.proxy.remove_member, method_args=["member_id"], method_kwargs={"image": "image_id", "ignore_missing": False}, - expected_args=[member.Member], + expected_args=[_member.Member], expected_kwargs={ "member_id": "member_id", "image_id": "image_id", - "ignore_missing": False}) + "ignore_missing": False, + }, + ) def test_member_delete_ignore(self): self._verify( @@ -385,19 +443,22 @@ def test_member_delete_ignore(self): self.proxy.remove_member, method_args=["member_id"], method_kwargs={"image": "image_id"}, - expected_args=[member.Member], + expected_args=[_member.Member], expected_kwargs={ "member_id": "member_id", "image_id": "image_id", - "ignore_missing": True}) + "ignore_missing": True, + }, + ) def test_member_update(self): self._verify( "openstack.proxy.Proxy._update", self.proxy.update_member, method_args=['member_id', 'image_id'], - expected_args=[member.Member], - expected_kwargs={'member_id': 'member_id', 'image_id': 'image_id'}) + expected_args=[_member.Member], + expected_kwargs={'member_id': 'member_id', 'image_id': 'image_id'}, + ) def test_member_get(self): self._verify( @@ -405,8 +466,9 @@ def test_member_get(self): self.proxy.get_member, method_args=['member_id'], method_kwargs={"image": "image_id"}, - expected_args=[member.Member], - expected_kwargs={'member_id': 'member_id', 'image_id': 'image_id'}) + expected_args=[_member.Member], + expected_kwargs={'member_id': 'member_id', 'image_id': 'image_id'}, + ) def test_member_find(self): self._verify( @@ -414,46 +476,50 @@ def test_member_find(self): self.proxy.find_member, method_args=['member_id'], method_kwargs={"image": "image_id"}, - expected_args=[member.Member, "member_id"], - expected_kwargs={'ignore_missing': True, 'image_id': 'image_id'}) + expected_args=[_member.Member, "member_id"], + expected_kwargs={'ignore_missing': True, 'image_id': 'image_id'}, + ) def test_members(self): - self.verify_list(self.proxy.members, member.Member, - method_kwargs={'image': 'image_1'}, - expected_kwargs={'image_id': 'image_1'}) + self.verify_list( + self.proxy.members, + _member.Member, + method_kwargs={'image': 'image_1'}, + expected_kwargs={'image_id': 'image_1'}, + ) class TestMetadefNamespace(TestImageProxy): def test_metadef_namespace_create(self): self.verify_create( self.proxy.create_metadef_namespace, - metadef_namespace.MetadefNamespace, + _metadef_namespace.MetadefNamespace, ) def test_metadef_namespace_delete(self): self.verify_delete( self.proxy.delete_metadef_namespace, - metadef_namespace.MetadefNamespace, + _metadef_namespace.MetadefNamespace, False, ) def test_metadef_namespace_delete__ignore(self): self.verify_delete( self.proxy.delete_metadef_namespace, - metadef_namespace.MetadefNamespace, + _metadef_namespace.MetadefNamespace, True, ) def test_metadef_namespace_get(self): self.verify_get( self.proxy.get_metadef_namespace, - metadef_namespace.MetadefNamespace, + _metadef_namespace.MetadefNamespace, ) def test_metadef_namespaces(self): self.verify_list( self.proxy.metadef_namespaces, - metadef_namespace.MetadefNamespace, + _metadef_namespace.MetadefNamespace, ) def test_metadef_namespace_update(self): @@ -461,7 +527,7 @@ def test_metadef_namespace_update(self): # request body self.verify_update( self.proxy.update_metadef_namespace, - metadef_namespace.MetadefNamespace, + _metadef_namespace.MetadefNamespace, method_kwargs={'is_protected': True}, expected_kwargs={'namespace': 'resource_id', 'is_protected': True}, ) @@ -472,114 +538,123 @@ def test_images_schema_get(self): self._verify( "openstack.proxy.Proxy._get", self.proxy.get_images_schema, - expected_args=[schema.Schema], + expected_args=[_schema.Schema], expected_kwargs={ - 'base_path': '/schemas/images', 'requires_id': False}) + 'base_path': '/schemas/images', + 'requires_id': False, + }, + ) def test_image_schema_get(self): self._verify( "openstack.proxy.Proxy._get", self.proxy.get_image_schema, - expected_args=[schema.Schema], + expected_args=[_schema.Schema], expected_kwargs={ - 'base_path': '/schemas/image', 'requires_id': False}) + 'base_path': '/schemas/image', + 'requires_id': False, + }, + ) def test_members_schema_get(self): self._verify( "openstack.proxy.Proxy._get", self.proxy.get_members_schema, - expected_args=[schema.Schema], + expected_args=[_schema.Schema], expected_kwargs={ - 'base_path': '/schemas/members', 'requires_id': False}) + 'base_path': '/schemas/members', + 'requires_id': False, + }, + ) def test_member_schema_get(self): self._verify( "openstack.proxy.Proxy._get", self.proxy.get_member_schema, - expected_args=[schema.Schema], + expected_args=[_schema.Schema], expected_kwargs={ - 'base_path': '/schemas/member', 'requires_id': False}) + 'base_path': '/schemas/member', + 'requires_id': False, + }, + ) class TestTask(TestImageProxy): def test_task_get(self): - self.verify_get(self.proxy.get_task, task.Task) + self.verify_get(self.proxy.get_task, _task.Task) def test_tasks(self): - self.verify_list(self.proxy.tasks, task.Task) + self.verify_list(self.proxy.tasks, _task.Task) def test_task_create(self): - self.verify_create(self.proxy.create_task, task.Task) + self.verify_create(self.proxy.create_task, _task.Task) def test_wait_for_task_immediate_status(self): status = 'success' - res = task.Task(id='1234', status=status) + res = _task.Task(id='1234', status=status) - result = self.proxy.wait_for_task( - res, status, "failure", 0.01, 0.1) + result = self.proxy.wait_for_task(res, status, "failure", 0.01, 0.1) self.assertEqual(res, result) def test_wait_for_task_immediate_status_case(self): status = "SUCcess" - res = task.Task(id='1234', status=status) + res = _task.Task(id='1234', status=status) - result = self.proxy.wait_for_task( - res, status, "failure", 0.01, 0.1) + result = self.proxy.wait_for_task(res, status, "failure", 0.01, 0.1) self.assertEqual(res, result) def test_wait_for_task_error_396(self): # Ensure we create a new task when we get 396 error - res = task.Task( - id='id', status='waiting', - type='some_type', input='some_input', result='some_result' + res = _task.Task( + id='id', + status='waiting', + type='some_type', + input='some_input', + result='some_result', ) mock_fetch = mock.Mock() mock_fetch.side_effect = [ - task.Task( - id='id', status='failure', - type='some_type', input='some_input', result='some_result', - message=_proxy._IMAGE_ERROR_396 + _task.Task( + id='id', + status='failure', + type='some_type', + input='some_input', + result='some_result', + message=_proxy._IMAGE_ERROR_396, ), - task.Task(id='fake', status='waiting'), - task.Task(id='fake', status='success'), + _task.Task(id='fake', status='waiting'), + _task.Task(id='fake', status='success'), ] self.proxy._create = mock.Mock() self.proxy._create.side_effect = [ - task.Task(id='fake', status='success') + _task.Task(id='fake', status='success') ] - with mock.patch.object(task.Task, - 'fetch', mock_fetch): - - result = self.proxy.wait_for_task( - res, interval=0.01, wait=0.5) + with mock.patch.object(_task.Task, 'fetch', mock_fetch): + result = self.proxy.wait_for_task(res, interval=0.01, wait=0.5) self.assertEqual('success', result.status) self.proxy._create.assert_called_with( - mock.ANY, - input=res.input, - type=res.type) + mock.ANY, input=res.input, type=res.type + ) def test_wait_for_task_wait(self): - res = task.Task(id='id', status='waiting') + res = _task.Task(id='id', status='waiting') mock_fetch = mock.Mock() mock_fetch.side_effect = [ - task.Task(id='id', status='waiting'), - task.Task(id='id', status='waiting'), - task.Task(id='id', status='success'), + _task.Task(id='id', status='waiting'), + _task.Task(id='id', status='waiting'), + _task.Task(id='id', status='success'), ] - with mock.patch.object(task.Task, - 'fetch', mock_fetch): - - result = self.proxy.wait_for_task( - res, interval=0.01, wait=0.5) + with mock.patch.object(_task.Task, 'fetch', mock_fetch): + result = self.proxy.wait_for_task(res, interval=0.01, wait=0.5) self.assertEqual('success', result.status) @@ -587,22 +662,28 @@ def test_tasks_schema_get(self): self._verify( "openstack.proxy.Proxy._get", self.proxy.get_tasks_schema, - expected_args=[schema.Schema], + expected_args=[_schema.Schema], expected_kwargs={ - 'base_path': '/schemas/tasks', 'requires_id': False}) + 'base_path': '/schemas/tasks', + 'requires_id': False, + }, + ) def test_task_schema_get(self): self._verify( "openstack.proxy.Proxy._get", self.proxy.get_task_schema, - expected_args=[schema.Schema], + expected_args=[_schema.Schema], expected_kwargs={ - 'base_path': '/schemas/task', 'requires_id': False}) + 'base_path': '/schemas/task', + 'requires_id': False, + }, + ) class TestMisc(TestImageProxy): def test_stores(self): - self.verify_list(self.proxy.stores, si.Store) + self.verify_list(self.proxy.stores, _service_info.Store) def test_import_info(self): self._verify( @@ -610,8 +691,9 @@ def test_import_info(self): self.proxy.get_import_info, method_args=[], method_kwargs={}, - expected_args=[si.Import], - expected_kwargs={'requires_id': False}) + expected_args=[_service_info.Import], + expected_kwargs={'requires_id': False}, + ) class TestMetadefSchema(TestImageProxy): @@ -619,88 +701,108 @@ def test_metadef_namespace_schema_get(self): self._verify( "openstack.proxy.Proxy._get", self.proxy.get_metadef_namespace_schema, - expected_args=[metadef_schema.MetadefSchema], + expected_args=[_metadef_schema.MetadefSchema], expected_kwargs={ 'base_path': '/schemas/metadefs/namespace', - 'requires_id': False}) + 'requires_id': False, + }, + ) def test_metadef_namespaces_schema_get(self): self._verify( "openstack.proxy.Proxy._get", self.proxy.get_metadef_namespaces_schema, - expected_args=[metadef_schema.MetadefSchema], + expected_args=[_metadef_schema.MetadefSchema], expected_kwargs={ 'base_path': '/schemas/metadefs/namespaces', - 'requires_id': False}) + 'requires_id': False, + }, + ) def test_metadef_resource_type_schema_get(self): self._verify( "openstack.proxy.Proxy._get", self.proxy.get_metadef_resource_type_schema, - expected_args=[metadef_schema.MetadefSchema], + expected_args=[_metadef_schema.MetadefSchema], expected_kwargs={ 'base_path': '/schemas/metadefs/resource_type', - 'requires_id': False}) + 'requires_id': False, + }, + ) def test_metadef_resource_types_schema_get(self): self._verify( "openstack.proxy.Proxy._get", self.proxy.get_metadef_resource_types_schema, - expected_args=[metadef_schema.MetadefSchema], + expected_args=[_metadef_schema.MetadefSchema], expected_kwargs={ 'base_path': '/schemas/metadefs/resource_types', - 'requires_id': False}) + 'requires_id': False, + }, + ) def test_metadef_object_schema_get(self): self._verify( "openstack.proxy.Proxy._get", self.proxy.get_metadef_object_schema, - expected_args=[metadef_schema.MetadefSchema], + expected_args=[_metadef_schema.MetadefSchema], expected_kwargs={ 'base_path': '/schemas/metadefs/object', - 'requires_id': False}) + 'requires_id': False, + }, + ) def test_metadef_objects_schema_get(self): self._verify( "openstack.proxy.Proxy._get", self.proxy.get_metadef_objects_schema, - expected_args=[metadef_schema.MetadefSchema], + expected_args=[_metadef_schema.MetadefSchema], expected_kwargs={ 'base_path': '/schemas/metadefs/objects', - 'requires_id': False}) + 'requires_id': False, + }, + ) def test_metadef_property_schema_get(self): self._verify( "openstack.proxy.Proxy._get", self.proxy.get_metadef_property_schema, - expected_args=[metadef_schema.MetadefSchema], + expected_args=[_metadef_schema.MetadefSchema], expected_kwargs={ 'base_path': '/schemas/metadefs/property', - 'requires_id': False}) + 'requires_id': False, + }, + ) def test_metadef_properties_schema_get(self): self._verify( "openstack.proxy.Proxy._get", self.proxy.get_metadef_properties_schema, - expected_args=[metadef_schema.MetadefSchema], + expected_args=[_metadef_schema.MetadefSchema], expected_kwargs={ 'base_path': '/schemas/metadefs/properties', - 'requires_id': False}) + 'requires_id': False, + }, + ) def test_metadef_tag_schema_get(self): self._verify( "openstack.proxy.Proxy._get", self.proxy.get_metadef_tag_schema, - expected_args=[metadef_schema.MetadefSchema], + expected_args=[_metadef_schema.MetadefSchema], expected_kwargs={ 'base_path': '/schemas/metadefs/tag', - 'requires_id': False}) + 'requires_id': False, + }, + ) def test_metadef_tags_schema_get(self): self._verify( "openstack.proxy.Proxy._get", self.proxy.get_metadef_tags_schema, - expected_args=[metadef_schema.MetadefSchema], + expected_args=[_metadef_schema.MetadefSchema], expected_kwargs={ 'base_path': '/schemas/metadefs/tags', - 'requires_id': False}) + 'requires_id': False, + }, + ) From d3e798920790449731d764147e1d368c8b3d269c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 20 Dec 2021 12:58:28 +0000 Subject: [PATCH 3133/3836] compute: Add functional tests for volume attachments These provide a sort of documentation and prevent us regressing in the future. Change-Id: I759c486d2bd553ed95d97e7b92703ee399821fd2 Signed-off-by: Stephen Finucane --- .../compute/v2/test_volume_attachment.py | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 openstack/tests/functional/compute/v2/test_volume_attachment.py diff --git a/openstack/tests/functional/compute/v2/test_volume_attachment.py b/openstack/tests/functional/compute/v2/test_volume_attachment.py new file mode 100644 index 000000000..c65da7696 --- /dev/null +++ b/openstack/tests/functional/compute/v2/test_volume_attachment.py @@ -0,0 +1,139 @@ +# 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.block_storage.v3 import volume as volume_ +from openstack.compute.v2 import server as server_ +from openstack.compute.v2 import volume_attachment as volume_attachment_ +from openstack.tests.functional.compute import base as ft_base + + +class TestServerVolumeAttachment(ft_base.BaseComputeTest): + def setUp(self): + super().setUp() + + if not self.user_cloud.has_service('block-storage'): + self.skipTest('block-storage service not supported by cloud') + + self.server_name = self.getUniqueString() + self.volume_name = self.getUniqueString() + + # create the server and volume + + server = self.user_cloud.compute.create_server( + name=self.server_name, + flavor_id=self.flavor.id, + image_id=self.image.id, + networks='none', + ) + self.user_cloud.compute.wait_for_server( + server, + wait=self._wait_for_timeout, + ) + self.assertIsInstance(server, server_.Server) + self.assertEqual(self.server_name, server.name) + + volume = self.user_cloud.block_storage.create_volume( + name=self.volume_name, + size=1, + ) + self.user_cloud.block_storage.wait_for_status( + volume, + status='available', + wait=self._wait_for_timeout, + ) + self.assertIsInstance(volume, volume_.Volume) + self.assertEqual(self.volume_name, volume.name) + + self.server = server + self.volume = volume + + def tearDown(self): + self.user_cloud.compute.delete_server(self.server.id) + self.user_cloud.compute.wait_for_delete( + self.server, + wait=self._wait_for_timeout, + ) + + self.user_cloud.block_storage.delete_volume(self.volume.id) + self.user_cloud.block_storage.wait_for_delete( + self.volume, + wait=self._wait_for_timeout, + ) + + super().tearDown() + + def test_volume_attachment(self): + # create the volume attachment + + volume_attachment = self.user_cloud.compute.create_volume_attachment( + self.server, + self.volume, + ) + self.assertIsInstance( + volume_attachment, + volume_attachment_.VolumeAttachment, + ) + self.user_cloud.block_storage.wait_for_status( + self.volume, + status='in-use', + wait=self._wait_for_timeout, + ) + + # list all attached volume attachments (there should only be one) + + volume_attachments = list( + self.user_cloud.compute.volume_attachments(self.server) + ) + self.assertEqual(1, len(volume_attachments)) + self.assertIsInstance( + volume_attachments[0], + volume_attachment_.VolumeAttachment, + ) + + # update the volume attachment + + volume_attachment = self.user_cloud.compute.update_volume_attachment( + self.server, + self.volume, + delete_on_termination=True, + ) + self.assertIsInstance( + volume_attachment, + volume_attachment_.VolumeAttachment, + ) + + # retrieve details of the (updated) volume attachment + + volume_attachment = self.user_cloud.compute.get_volume_attachment( + self.server, + self.volume, + ) + self.assertIsInstance( + volume_attachment, + volume_attachment_.VolumeAttachment, + ) + self.assertTrue(volume_attachment.delete_on_termination) + + # delete the volume attachment + + result = self.user_cloud.compute.delete_volume_attachment( + self.server, + self.volume, + ignore_missing=False, + ) + self.assertIsNone(result) + + self.user_cloud.block_storage.wait_for_status( + self.volume, + status='available', + wait=self._wait_for_timeout, + ) From 19012058a3d78b490e4543cc5c71eb54386d16cf Mon Sep 17 00:00:00 2001 From: Daniel Wilson Date: Tue, 25 Oct 2022 21:58:57 -0400 Subject: [PATCH 3134/3836] Accept queries when listing migrations Change the migration() function in the compute proxy to accept query parameters as kwargs. Change-Id: I87cf7e7378b7fe6276b521be362f3e5d51427c00 --- openstack/compute/v2/_proxy.py | 6 ++++-- openstack/tests/unit/compute/v2/test_proxy.py | 8 ++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index e15629930..aeb8c63c0 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -1936,13 +1936,15 @@ def server_migrations(self, server): # ========== Migrations ========== - def migrations(self): + def migrations(self, **query): """Return a generator of migrations for all servers. + :param kwargs query: Optional query parameters to be sent to limit + the migrations being returned. :returns: A generator of Migration objects :rtype: :class:`~openstack.compute.v2.migration.Migration` """ - return self._list(_migration.Migration) + return self._list(_migration.Migration, **query) # ========== Server diagnostics ========== diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 9b166cc07..0220ddcc7 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -1248,6 +1248,14 @@ def test_server_migrations(self): def test_migrations(self): self.verify_list(self.proxy.migrations, migration.Migration) + def test_migrations_kwargs(self): + self.verify_list( + self.proxy.migrations, + migration.Migration, + method_kwargs={'host': 'h1'}, + expected_kwargs={'host': 'h1'}, + ) + def test_fetch_security_groups(self): self._verify( 'openstack.compute.v2.server.Server.fetch_security_groups', From 25ec686c5b096e7980d60079c86ff5deaa28fd8c Mon Sep 17 00:00:00 2001 From: Rafael Castillo Date: Thu, 15 Sep 2022 17:08:02 -0700 Subject: [PATCH 3135/3836] Use /servers/detail endpoint in find_server proxy method Currently, find_server will return the objects with different fields filled in depending on whether the term provided is an id or a name. This patch, in the same vein of [0], changes the find_server method so it uses the /servers/detail endpoint to find servers in the case of a name being provided. This ensures that the method fills in all fields in every case. [0] https://review.opendev.org/c/openstack/openstacksdk/+/854293 Change-Id: If8306f879361f872a1a87d589c756d9ae2435449 --- openstack/compute/v2/_proxy.py | 3 ++- .../tests/unit/cloud/test_delete_server.py | 18 +++++++++--------- .../tests/unit/cloud/test_update_server.py | 4 ++-- openstack/tests/unit/compute/v2/test_proxy.py | 6 +++++- ...nd_server-use-details-9a22e83ec6540c98.yaml | 4 ++++ 5 files changed, 22 insertions(+), 13 deletions(-) create mode 100644 releasenotes/notes/find_server-use-details-9a22e83ec6540c98.yaml diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 3f0373d1f..9f8b34d03 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -660,7 +660,8 @@ def find_server(self, name_or_id, ignore_missing=True): :returns: One :class:`~openstack.compute.v2.server.Server` or None """ return self._find(_server.Server, name_or_id, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, + list_base_path='/servers/detail') def get_server(self, server): """Get a single server diff --git a/openstack/tests/unit/cloud/test_delete_server.py b/openstack/tests/unit/cloud/test_delete_server.py index 67c6306e6..562e7ffbe 100644 --- a/openstack/tests/unit/cloud/test_delete_server.py +++ b/openstack/tests/unit/cloud/test_delete_server.py @@ -38,7 +38,7 @@ def test_delete_server(self): status_code=404), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers'], + 'compute', 'public', append=['servers', 'detail'], qs_elements=['name=daffy']), json={'servers': [server]}), dict(method='DELETE', @@ -61,7 +61,7 @@ def test_delete_server_already_gone(self): status_code=404), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers'], + 'compute', 'public', append=['servers', 'detail'], qs_elements=['name=tweety']), json={'servers': []}), ]) @@ -78,7 +78,7 @@ def test_delete_server_already_gone_wait(self): status_code=404), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers'], + 'compute', 'public', append=['servers', 'detail'], qs_elements=['name=speedy']), json={'servers': []}), ]) @@ -98,7 +98,7 @@ def test_delete_server_wait_for_deleted(self): status_code=404), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers'], + 'compute', 'public', append=['servers', 'detail'], qs_elements=['name=wily']), json={'servers': [server]}), dict(method='DELETE', @@ -130,7 +130,7 @@ def test_delete_server_fails(self): status_code=404), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers'], + 'compute', 'public', append=['servers', 'detail'], qs_elements=['name=speedy']), json={'servers': [server]}), dict(method='DELETE', @@ -167,7 +167,7 @@ def fake_has_service(service_type): status_code=404), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers'], + 'compute', 'public', append=['servers', 'detail'], qs_elements=['name=porky']), json={'servers': [server]}), dict(method='DELETE', @@ -193,7 +193,7 @@ def test_delete_server_delete_ips(self): status_code=404), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers'], + 'compute', 'public', append=['servers', 'detail'], qs_elements=['name=porky']), json={'servers': [server]}), dict(method='GET', @@ -246,7 +246,7 @@ def test_delete_server_delete_ips_bad_neutron(self): status_code=404), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers'], + 'compute', 'public', append=['servers', 'detail'], qs_elements=['name=porky']), json={'servers': [server]}), dict(method='GET', @@ -283,7 +283,7 @@ def test_delete_server_delete_fips_nova(self): status_code=404), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers'], + 'compute', 'public', append=['servers', 'detail'], qs_elements=['name=porky']), json={'servers': [server]}), dict(method='GET', diff --git a/openstack/tests/unit/cloud/test_update_server.py b/openstack/tests/unit/cloud/test_update_server.py index e25ec2e36..7af7db1d5 100644 --- a/openstack/tests/unit/cloud/test_update_server.py +++ b/openstack/tests/unit/cloud/test_update_server.py @@ -48,7 +48,7 @@ def test_update_server_with_update_exception(self): status_code=404), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers'], + 'compute', 'public', append=['servers', 'detail'], qs_elements=['name=%s' % self.server_name]), json={'servers': [self.fake_server]}), dict(method='PUT', @@ -80,7 +80,7 @@ def test_update_server_name(self): status_code=404), dict(method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers'], + 'compute', 'public', append=['servers', 'detail'], qs_elements=['name=%s' % self.server_name]), json={'servers': [self.fake_server]}), dict(method='PUT', diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 4f6a4f60c..6e797a0c0 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -668,7 +668,11 @@ def test_server_force_delete(self): expected_args=[self.proxy]) def test_server_find(self): - self.verify_find(self.proxy.find_server, server.Server) + self.verify_find( + self.proxy.find_server, + server.Server, + expected_kwargs={'list_base_path': '/servers/detail'}, + ) def test_server_get(self): self.verify_get(self.proxy.get_server, server.Server) diff --git a/releasenotes/notes/find_server-use-details-9a22e83ec6540c98.yaml b/releasenotes/notes/find_server-use-details-9a22e83ec6540c98.yaml new file mode 100644 index 000000000..79cd7c2fa --- /dev/null +++ b/releasenotes/notes/find_server-use-details-9a22e83ec6540c98.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - | + Make sure find_server returns server details when looking up by name. From 117642f1f9c66a0b0a9d742f967f416817f7d87e Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 4 Nov 2022 11:36:31 +0100 Subject: [PATCH 3136/3836] Fix backup metadata management and update max_microversion on the backup resource was missed causing no microversion to be enabled at all for it. This leads to server refusing managing metadata of the backup. Fix this and add corresponding test with attempting to set metadata. Since this change sets microversion we also need to ensure we add all attributes of this version - encryption_key_id is the one added last. Change-Id: I2c161ca30524bf230104958b8afdd8cf0b682a89 --- openstack/block_storage/v3/backup.py | 4 ++++ .../functional/block_storage/v3/test_backup.py | 15 +++++++++++++++ .../tests/unit/block_storage/v3/test_backup.py | 9 ++++++--- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/openstack/block_storage/v3/backup.py b/openstack/block_storage/v3/backup.py index 64987183a..9131e5914 100644 --- a/openstack/block_storage/v3/backup.py +++ b/openstack/block_storage/v3/backup.py @@ -50,6 +50,8 @@ class Backup(resource.Resource): data_timestamp = resource.Body('data_timestamp') #: backup description description = resource.Body("description") + #: The UUID of the encryption key. Only included for encrypted volumes. + encryption_key_id = resource.Body("encryption_key_id") #: Backup fail reason fail_reason = resource.Body("fail_reason") #: Force backup @@ -86,6 +88,8 @@ class Backup(resource.Resource): #: The UUID of the volume. volume_id = resource.Body("volume_id") + _max_microversion = "3.64" + def create(self, session, prepend_key=True, base_path=None, **params): """Create a remote resource based on this instance. diff --git a/openstack/tests/functional/block_storage/v3/test_backup.py b/openstack/tests/functional/block_storage/v3/test_backup.py index bb7708888..2849419e5 100644 --- a/openstack/tests/functional/block_storage/v3/test_backup.py +++ b/openstack/tests/functional/block_storage/v3/test_backup.py @@ -69,6 +69,21 @@ def test_get(self): self.assertEqual(self.BACKUP_NAME, sot.name) self.assertEqual(False, sot.is_incremental) + def test_create_metadata(self): + metadata_backup = self.user_cloud.block_storage.create_backup( + name=self.getUniqueString(), + volume_id=self.VOLUME_ID, + metadata=dict(foo="bar")) + self.user_cloud.block_storage.wait_for_status( + metadata_backup, + status='available', + failures=['error'], + interval=5, + wait=self._wait_for_timeout) + self.user_cloud.block_storage.delete_backup( + metadata_backup.id, + ignore_missing=False) + def test_create_incremental(self): incremental_backup = self.user_cloud.block_storage.create_backup( name=self.getUniqueString(), diff --git a/openstack/tests/unit/block_storage/v3/test_backup.py b/openstack/tests/unit/block_storage/v3/test_backup.py index f2da69e5b..9e7174d8d 100644 --- a/openstack/tests/unit/block_storage/v3/test_backup.py +++ b/openstack/tests/unit/block_storage/v3/test_backup.py @@ -27,6 +27,7 @@ "created_at": "2018-04-02T10:35:27.000000", "updated_at": "2018-04-03T10:35:27.000000", "description": 'description', + "encryption_key_id": "fake_encry_id", "fail_reason": 'fail reason', "id": FAKE_ID, "name": "backup001", @@ -55,7 +56,7 @@ def setUp(self): self.sess = mock.Mock(spec=adapter.Adapter) self.sess.get = mock.Mock() self.sess.post = mock.Mock(return_value=self.resp) - self.sess.default_microversion = None + self.sess.default_microversion = "3.64" def test_basic(self): sot = backup.Backup(BACKUP) @@ -67,6 +68,7 @@ def test_basic(self): self.assertTrue(sot.allow_list) self.assertTrue(sot.allow_get) self.assertTrue(sot.allow_fetch) + self.assertIsNotNone(sot._max_microversion) self.assertDictEqual( { @@ -104,6 +106,7 @@ def test_create(self): sot.project_id) self.assertEqual(BACKUP['metadata'], sot.metadata) self.assertEqual(BACKUP['user_id'], sot.user_id) + self.assertEqual(BACKUP['encryption_key_id'], sot.encryption_key_id) def test_create_incremental(self): sot = backup.Backup(is_incremental=True) @@ -124,7 +127,7 @@ def test_create_incremental(self): 'incremental': True, } }, - microversion=None, + microversion="3.64", params={} ) @@ -137,7 +140,7 @@ def test_create_incremental(self): 'incremental': False, } }, - microversion=None, + microversion="3.64", params={} ) From c52f2decd3b2b2f7b3cde811d9fbf6d1cd7a352c Mon Sep 17 00:00:00 2001 From: EunYoung Kim Date: Sat, 15 Oct 2022 20:34:52 +0900 Subject: [PATCH 3137/3836] image: Add metadef resource type operations - Add resource for Metadata definition resource types Glance v2 API(https://docs.openstack.org/api-ref/image/v2/metadefs-index.html#metadata-definition-resource-types) - Change method names and adjust spacing - Resolve merge conflict Change-Id: I8ac7d15e69cb5cb017fe93e38736b8dbee9ae2f8 --- doc/source/user/proxies/image_v2.rst | 10 +++ doc/source/user/resources/image/index.rst | 1 + .../image/v2/metadef_resource_type.rst | 24 ++++++ openstack/image/v2/_proxy.py | 81 +++++++++++++++++++ openstack/image/v2/metadef_resource_type.py | 56 +++++++++++++ .../image/v2/test_metadef_resource_type.py | 79 ++++++++++++++++++ .../image/v2/test_metadef_resource_type.py | 38 +++++++++ .../test_metadef_resource_type_association.py | 44 ++++++++++ openstack/tests/unit/image/v2/test_proxy.py | 45 +++++++++++ 9 files changed, 378 insertions(+) create mode 100644 doc/source/user/resources/image/v2/metadef_resource_type.rst create mode 100644 openstack/image/v2/metadef_resource_type.py create mode 100644 openstack/tests/functional/image/v2/test_metadef_resource_type.py create mode 100644 openstack/tests/unit/image/v2/test_metadef_resource_type.py create mode 100644 openstack/tests/unit/image/v2/test_metadef_resource_type_association.py diff --git a/doc/source/user/proxies/image_v2.rst b/doc/source/user/proxies/image_v2.rst index 941f803a2..9c283c67a 100644 --- a/doc/source/user/proxies/image_v2.rst +++ b/doc/source/user/proxies/image_v2.rst @@ -66,6 +66,16 @@ Metadef Namespace Operations :members: create_metadef_namespace, delete_metadef_namespace, get_metadef_namespace, metadef_namespaces, update_metadef_namespace + +Metadef Resource Type Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.image.v2._proxy.Proxy + :noindex: + :members: metadef_resource_types, metadef_resource_type_associations, + create_metadef_resource_type_association, + delete_metadef_resource_type_association + Helpers ^^^^^^^ diff --git a/doc/source/user/resources/image/index.rst b/doc/source/user/resources/image/index.rst index 660312278..2f291f2c6 100644 --- a/doc/source/user/resources/image/index.rst +++ b/doc/source/user/resources/image/index.rst @@ -18,6 +18,7 @@ Image v2 Resources v2/image v2/member v2/metadef_namespace + v2/metadef_resource_type v2/metadef_schema v2/task v2/service_info diff --git a/doc/source/user/resources/image/v2/metadef_resource_type.rst b/doc/source/user/resources/image/v2/metadef_resource_type.rst new file mode 100644 index 000000000..c20ba943e --- /dev/null +++ b/doc/source/user/resources/image/v2/metadef_resource_type.rst @@ -0,0 +1,24 @@ +openstack.image.v2.metadef_resource_type +======================================== + +.. automodule:: openstack.image.v2.metadef_resource_type + +The MetadefResourceType Class +----------------------------- + +The ``MetadefResourceType`` class inherits +from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.image.v2.metadef_resource_type.MetadefResourceType + :members: + + +The MetadefResourceTypeAssociation Class +---------------------------------------- + +The ``MetadefResourceTypeAssociation`` class inherits +from :class:`~openstack.resource.Resource`. + +.. autoclass:: + openstack.image.v2.metadef_resource_type.MetadefResourceTypeAssociation + :members: diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 37c8cafd6..cefaeadd5 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -18,6 +18,7 @@ from openstack.image.v2 import image as _image from openstack.image.v2 import member as _member from openstack.image.v2 import metadef_namespace as _metadef_namespace +from openstack.image.v2 import metadef_resource_type as _metadef_resource_type from openstack.image.v2 import metadef_schema as _metadef_schema from openstack.image.v2 import schema as _schema from openstack.image.v2 import service_info as _si @@ -765,6 +766,86 @@ def update_metadef_namespace(self, metadef_namespace, **attrs): **attrs, ) + # ====== METADEF RESOURCE TYPES ====== + def metadef_resource_types(self, **query): + """Return a generator of metadef resource types + + :return: A generator object of metadef resource types + :rtype: + :class:`~openstack.image.v2.metadef_resource_type.MetadefResourceType` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._list(_metadef_resource_type.MetadefResourceType, **query) + + # ====== METADEF RESOURCE TYPES ASSOCIATION====== + def create_metadef_resource_type_association(self, + metadef_namespace, + **attrs): + """Creates a resource type association between a namespace + and the resource type specified in the body of the request. + + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.image.v2.metadef_resource_type.MetadefResourceTypeAssociation` + comprised of the properties on the + MetadefResourceTypeAssociation class. + + :returns: The results of metadef resource type association creation + :rtype: + :class:`~openstack.image.v2.metadef_resource_type.MetadefResourceTypeAssociation` + """ + namespace_name = resource.Resource._get_id(metadef_namespace) + return self._create( + _metadef_resource_type.MetadefResourceTypeAssociation, + namespace_name=namespace_name, + **attrs) + + def delete_metadef_resource_type_association(self, + metadef_resource_type, + metadef_namespace, + ignore_missing=True): + """Removes a resource type association in a namespace. + + :param metadef_resource_type: The value can be either the name of + a metadef resource type association or an + :class:`~openstack.image.v2.metadef_resource_type.MetadefResourceTypeAssociation` + instance. + :param metadef_namespace: The value can be either the name of metadef + namespace or an + :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` + instance + :param bool ignore_missing: When set to ``False``, + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the metadef resource type association does not exist. + :returns: ``None`` + """ + namespace_name = resource.Resource._get_id(metadef_namespace) + self._delete( + _metadef_resource_type.MetadefResourceTypeAssociation, + metadef_resource_type, + namespace_name=namespace_name, + ignore_missing=ignore_missing, + ) + + def metadef_resource_type_associations(self, metadef_namespace, **query): + """Return a generator of metadef resource type associations + + :param metadef_namespace: The value can be either the name of metadef + namespace or an + :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` + instance + :return: A generator object of metadef resource type associations + :rtype: + :class:`~openstack.image.v2.metadef_resource_type.MetadefResourceTypeAssociation` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + namespace_name = resource.Resource._get_id(metadef_namespace) + return self._list( + _metadef_resource_type.MetadefResourceTypeAssociation, + namespace_name=namespace_name, + **query) + # ====== SCHEMAS ====== def get_images_schema(self): """Get images schema diff --git a/openstack/image/v2/metadef_resource_type.py b/openstack/image/v2/metadef_resource_type.py new file mode 100644 index 000000000..4e28b04b5 --- /dev/null +++ b/openstack/image/v2/metadef_resource_type.py @@ -0,0 +1,56 @@ +# 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 resource + + +class MetadefResourceType(resource.Resource): + resources_key = 'resource_types' + base_path = '/metadefs/resource_types' + + # capabilities + allow_list = True + + #: The name of metadata definition resource type + name = resource.Body('name', alternate_id=True) + #: The date and time when the resource type was created. + created_at = resource.Body('created_at') + #: The date and time when the resource type was updated. + updated_at = resource.Body('updated_at') + + +class MetadefResourceTypeAssociation(resource.Resource): + resources_key = 'resource_type_associations' + base_path = '/metadefs/namespaces/%(namespace_name)s/resource_types' + + # capabilities + allow_create = True + allow_delete = True + allow_list = True + + #: The name of the namespace whose details you want to see. + namespace_name = resource.URI('namespace_name') + #: The name of metadata definition resource type + name = resource.Body('name', alternate_id=True) + #: The date and time when the resource type was created. + created_at = resource.Body('created_at') + #: The date and time when the resource type was updated. + updated_at = resource.Body('updated_at') + #: Prefix for any properties in the namespace that you want to apply + #: to the resource type. If you specify a prefix, you must append + #: a prefix separator, such as the colon (:) character. + prefix = resource.Body('prefix') + #: Some resource types allow more than one key and value pair + #: for each instance. For example, the Image service allows + #: both user and image metadata on volumes. The properties_target parameter + #: enables a namespace target to remove the ambiguity + properties_target = resource.Body('properties_target') diff --git a/openstack/tests/functional/image/v2/test_metadef_resource_type.py b/openstack/tests/functional/image/v2/test_metadef_resource_type.py new file mode 100644 index 000000000..520f38911 --- /dev/null +++ b/openstack/tests/functional/image/v2/test_metadef_resource_type.py @@ -0,0 +1,79 @@ +# 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.image.v2 import metadef_namespace as _metadef_namespace +from openstack.image.v2 import metadef_resource_type as _metadef_resource_type +from openstack.tests.functional.image.v2 import base + + +class TestMetadefResourceType(base.BaseImageTest): + + def setUp(self): + super().setUp() + + # there's a limit on namespace length + namespace = self.getUniqueString().split('.')[-1] + self.metadef_namespace = self.conn.image.create_metadef_namespace( + namespace=namespace, + ) + self.assertIsInstance( + self.metadef_namespace, + _metadef_namespace.MetadefNamespace, + ) + self.assertEqual(namespace, self.metadef_namespace.namespace) + + resource_type_name = 'test-resource-type' + resource_type = {'name': resource_type_name} + self.metadef_resource_type = \ + self.conn.image.create_metadef_resource_type_association( + metadef_namespace=namespace, + **resource_type + ) + self.assertIsInstance( + self.metadef_resource_type, + _metadef_resource_type.MetadefResourceTypeAssociation + ) + self.assertEqual(resource_type_name, self.metadef_resource_type.name) + + def tearDown(self): + # we do this in tearDown rather than via 'addCleanup' since we want to + # wait for the deletion of the resource to ensure it completes + self.conn.image.delete_metadef_namespace(self.metadef_namespace) + self.conn.image.wait_for_delete(self.metadef_namespace) + + super().tearDown() + + def test_metadef_resource_types(self): + # list resource type associations + associations = list(self.conn.image.metadef_resource_type_associations( + metadef_namespace=self.metadef_namespace)) + + self.assertIn( + self.metadef_resource_type.name, + {a.name for a in associations} + ) + + # (no find_metadef_resource_type_association method) + + # list resource types + resource_types = list(self.conn.image.metadef_resource_types()) + + self.assertIn( + self.metadef_resource_type.name, + {t.name for t in resource_types} + ) + + # delete + self.conn.image.delete_metadef_resource_type_association( + self.metadef_resource_type, + metadef_namespace=self.metadef_namespace + ) diff --git a/openstack/tests/unit/image/v2/test_metadef_resource_type.py b/openstack/tests/unit/image/v2/test_metadef_resource_type.py new file mode 100644 index 000000000..571828458 --- /dev/null +++ b/openstack/tests/unit/image/v2/test_metadef_resource_type.py @@ -0,0 +1,38 @@ +# 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.image.v2 import metadef_resource_type +from openstack.tests.unit import base + + +EXAMPLE = { + "name": "OS::Nova::Aggregate", + "created_at": "2022-07-09T04:10:37Z" +} + + +class TestMetadefResourceType(base.TestCase): + def test_basic(self): + sot = metadef_resource_type.MetadefResourceType() + self.assertIsNone(sot.resource_key) + self.assertEqual('resource_types', sot.resources_key) + self.assertEqual('/metadefs/resource_types', sot.base_path) + self.assertFalse(sot.allow_create) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = metadef_resource_type.MetadefResourceType(**EXAMPLE) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['created_at'], sot.created_at) diff --git a/openstack/tests/unit/image/v2/test_metadef_resource_type_association.py b/openstack/tests/unit/image/v2/test_metadef_resource_type_association.py new file mode 100644 index 000000000..fcd430a51 --- /dev/null +++ b/openstack/tests/unit/image/v2/test_metadef_resource_type_association.py @@ -0,0 +1,44 @@ +# 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.image.v2 import metadef_resource_type +from openstack.tests.unit import base + + +EXAMPLE = { + "name": "OS::Cinder::Volume", + "prefix": "CIM_PASD_", + "properties_target": "image", + "created_at": "2022-07-09T04:10:38Z" +} + + +class TestMetadefResourceTypeAssociation(base.TestCase): + def test_basic(self): + sot = metadef_resource_type.MetadefResourceTypeAssociation() + self.assertIsNone(sot.resource_key) + self.assertEqual('resource_type_associations', sot.resources_key) + self.assertEqual( + '/metadefs/namespaces/%(namespace_name)s/resource_types', + sot.base_path) + self.assertTrue(sot.allow_create) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = metadef_resource_type.MetadefResourceTypeAssociation(**EXAMPLE) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['created_at'], sot.created_at) + self.assertEqual(EXAMPLE['prefix'], sot.prefix) + self.assertEqual(EXAMPLE['properties_target'], sot.properties_target) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index f8b85c683..f663c89ee 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -20,6 +20,7 @@ from openstack.image.v2 import image as _image from openstack.image.v2 import member as _member from openstack.image.v2 import metadef_namespace as _metadef_namespace +from openstack.image.v2 import metadef_resource_type as _metadef_resource_type from openstack.image.v2 import metadef_schema as _metadef_schema from openstack.image.v2 import schema as _schema from openstack.image.v2 import service_info as _service_info @@ -533,6 +534,50 @@ def test_metadef_namespace_update(self): ) +class TestMetadefResourceType(TestImageProxy): + def test_metadef_resource_types(self): + self.verify_list( + self.proxy.metadef_resource_types, + _metadef_resource_type.MetadefResourceType + ) + + +class TestMetadefResourceTypeAssociation(TestImageProxy): + def test_create_metadef_resource_type_association(self): + self.verify_create( + self.proxy.create_metadef_resource_type_association, + _metadef_resource_type.MetadefResourceTypeAssociation, + method_kwargs={'metadef_namespace': 'namespace_name'}, + expected_kwargs={'namespace_name': 'namespace_name'} + ) + + def test_delete_metadef_resource_type_association(self): + self.verify_delete( + self.proxy.delete_metadef_resource_type_association, + _metadef_resource_type.MetadefResourceTypeAssociation, + False, + method_kwargs={'metadef_namespace': 'namespace_name'}, + expected_kwargs={'namespace_name': 'namespace_name'} + ) + + def test_delete_metadef_resource_type_association_ignore(self): + self.verify_delete( + self.proxy.delete_metadef_resource_type_association, + _metadef_resource_type.MetadefResourceTypeAssociation, + True, + method_kwargs={'metadef_namespace': 'namespace_name'}, + expected_kwargs={'namespace_name': 'namespace_name'} + ) + + def test_metadef_resource_type_associations(self): + self.verify_list( + self.proxy.metadef_resource_type_associations, + _metadef_resource_type.MetadefResourceTypeAssociation, + method_kwargs={'metadef_namespace': 'namespace_name'}, + expected_kwargs={'namespace_name': 'namespace_name'} + ) + + class TestSchema(TestImageProxy): def test_images_schema_get(self): self._verify( From 4421b8750883b8f4059259b5865062a647376d04 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 4 Nov 2022 15:32:48 +0100 Subject: [PATCH 3138/3836] Introduce resource_registry in the proxies Instead of hardcoding resource types on various places (what is especially problematic once different api versions support different resources) it would be possible to rely on a regisitry describing which resources are supported by the certain proxy pointing to the exact object classes. This is going to be used in a series of changes implementing search_XXX method accepting string "service.resource" and the method figuring out which exact class should be used as well as it a first step into auto-generation of ansible modules. Change-Id: If4abc89e88b0843877517cf9eaf5648778b325b0 --- openstack/baremetal/v1/_proxy.py | 13 +++++ .../baremetal_introspection/v1/_proxy.py | 3 + openstack/block_storage/v3/_proxy.py | 16 ++++++ openstack/clustering/v1/_proxy.py | 15 +++++ openstack/compute/v2/_proxy.py | 23 ++++++++ openstack/database/v1/_proxy.py | 6 ++ openstack/dns/v2/_proxy.py | 8 +++ openstack/identity/v3/_proxy.py | 34 +++++++++++ openstack/image/v2/_proxy.py | 9 +++ openstack/instance_ha/v1/_proxy.py | 6 ++ openstack/key_manager/v1/_proxy.py | 5 ++ openstack/load_balancer/v2/_proxy.py | 16 ++++++ openstack/message/v2/_proxy.py | 6 ++ openstack/network/v2/_proxy.py | 57 +++++++++++++++++++ openstack/object_store/v1/_proxy.py | 6 ++ openstack/orchestration/v1/_proxy.py | 9 +++ openstack/placement/v1/_proxy.py | 4 ++ openstack/proxy.py | 6 ++ openstack/shared_file_system/v2/_proxy.py | 8 +++ openstack/workflow/v2/_proxy.py | 4 ++ 20 files changed, 254 insertions(+) diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 5148b2bf4..1722f5ac8 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -30,6 +30,19 @@ class Proxy(proxy.Proxy): retriable_status_codes = _common.RETRIABLE_STATUS_CODES + _resource_registry = { + "allocation": _allocation.Allocation, + "chassis": _chassis.Chassis, + "conductor": _conductor.Conductor, + "deploy_template": _deploytemplates.DeployTemplate, + "driver": _driver.Driver, + "node": _node.Node, + "port": _port.Port, + "port_group": _portgroup.PortGroup, + "volume_connector": _volumeconnector.VolumeConnector, + "volume_target": _volumetarget.VolumeTarget, + } + def _get_with_fields(self, resource_type, value, fields=None): """Fetch a bare metal resource. diff --git a/openstack/baremetal_introspection/v1/_proxy.py b/openstack/baremetal_introspection/v1/_proxy.py index b25eb3378..cec1bdfda 100644 --- a/openstack/baremetal_introspection/v1/_proxy.py +++ b/openstack/baremetal_introspection/v1/_proxy.py @@ -21,6 +21,9 @@ class Proxy(proxy.Proxy): + _resource_registry = { + "introspection": _introspect.Introspection, + } def introspections(self, **query): """Retrieve a generator of introspection records. diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 678412688..e5fad7b07 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -31,6 +31,22 @@ class Proxy(_base_proxy.BaseBlockStorageProxy): + _resource_registry = { + "availability_zone": availability_zone.AvailabilityZone, + "backup": _backup.Backup, + "capabilities": _capabilities.Capabilities, + "extension": _extension.Extension, + "group": _group.Group, + "group_snapshot": _group_snapshot.GroupSnapshot, + "group_type": _group_type, + "limits": _limits.Limit, + "quota_set": _quota_set.QuotaSet, + "resource_filter": _resource_filter.ResourceFilter, + "snapshot": _snapshot.Snapshot, + "stats_pools": _stats.Pools, + "type": _type.Type, + "volume": _volume.Volume + } # ====== SNAPSHOTS ====== def get_snapshot(self, snapshot): diff --git a/openstack/clustering/v1/_proxy.py b/openstack/clustering/v1/_proxy.py index 127b521dd..c22cd6461 100644 --- a/openstack/clustering/v1/_proxy.py +++ b/openstack/clustering/v1/_proxy.py @@ -28,6 +28,21 @@ class Proxy(proxy.Proxy): + _resource_registry = { + "action": _action.Action, + "build_info": build_info.BuildInfo, + "cluster": _cluster.Cluster, + "cluster_attr": _cluster_attr.ClusterAttr, + "cluster_policy": _cluster_policy.ClusterPolicy, + "event": _event.Event, + "node": _node.Node, + "policy": _policy.Policy, + "policy_type": _policy_type.PolicyType, + "profile": _profile.Profile, + "profile_type": _profile_type.ProfileType, + "receiver": _receiver.Receiver, + "service": _service.Service, + } def get_build_info(self): """Get build info for service engine and API diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index e15629930..425ee16cf 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -43,6 +43,29 @@ class Proxy(proxy.Proxy): + _resource_registry = { + "aggregate": _aggregate.Aggregate, + "availability_zone": availability_zone.AvailabilityZone, + "extension": extension.Extension, + "flavor": _flavor.Flavor, + "hypervisor": _hypervisor.Hypervisor, + "image": _image.Image, + "keypair": _keypair.Keypair, + "limits": limits.Limits, + "migration": _migration.Migration, + "quota_set": _quota_set.QuotaSet, + "server": _server.Server, + "server_action": _server_action.ServerAction, + "server_diagnostics": _server_diagnostics.ServerDiagnostics, + "server_group": _server_group.ServerGroup, + "server_interface": _server_interface.ServerInterface, + "server_ip": server_ip.ServerIP, + "server_migration": _server_migration.ServerMigration, + "server_remote_console": _src.ServerRemoteConsole, + "service": _service.Service, + "usage": _usage.Usage, + "volume_attachment": _volume_attachment.VolumeAttachment + } # ========== Extensions ========== diff --git a/openstack/database/v1/_proxy.py b/openstack/database/v1/_proxy.py index 8291344d5..154debc31 100644 --- a/openstack/database/v1/_proxy.py +++ b/openstack/database/v1/_proxy.py @@ -18,6 +18,12 @@ class Proxy(proxy.Proxy): + _resource_registry = { + "database": _database.Database, + "flavor": _flavor.Flavor, + "instance": _instance.Instance, + "user": _user.User, + } def create_database(self, instance, **attrs): """Create a new database from attributes diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py index 800d465fb..1b097e1a7 100644 --- a/openstack/dns/v2/_proxy.py +++ b/openstack/dns/v2/_proxy.py @@ -20,6 +20,14 @@ class Proxy(proxy.Proxy): + _resource_registry = { + "floating_ip": _fip.FloatingIP, + "recordset": _rs.Recordset, + "zone": _zone.Zone, + "zone_export": _zone_export.ZoneExport, + "zone_import": _zone_import.ZoneImport, + "zone_transfer_request": _zone_transfer.ZoneTransferRequest, + } # ======== Zones ======== def zones(self, **query): diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index c8a50e56e..6f40b8b2f 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -48,6 +48,40 @@ class Proxy(proxy.Proxy): + _resource_registry = { + "application_credential": + _application_credential.ApplicationCredential, + "credential": _credential.Credential, + "domain": _domain.Domain, + "endpoint": _endpoint.Endpoint, + "federation_protocol": _federation_protocol.FederationProtocol, + "group": _group.Group, + "identity_provider": _identity_provider.IdentityProvider, + "limit": _limit.Limit, + "mapping": _mapping.Mapping, + "policy": _policy.Policy, + "project": _project.Project, + "region": _region.Region, + "registered_limit": _registered_limit.RegisteredLimit, + "role": _role.Role, + "role_assignment": _role_assignment.RoleAssignment, + "role_domain_group_assignment": + _role_domain_group_assignment.RoleDomainGroupAssignment, + "role_domain_user_assignment": + _role_domain_user_assignment.RoleDomainUserAssignment, + "role_project_group_assignment": + _role_project_group_assignment.RoleProjectGroupAssignment, + "role_project_user_assignment": + _role_project_user_assignment.RoleProjectUserAssignment, + "role_system_group_assignment": + _role_system_group_assignment.RoleSystemGroupAssignment, + "role_system_user_assignment": + _role_system_user_assignment.RoleSystemUserAssignment, + "service": _service.Service, + "system": _system.System, + "trust": _trust.Trust, + "user": _user.User, + } def create_credential(self, **attrs): """Create a new credential from attributes diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index b819c8ae3..4ae007e52 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -32,6 +32,15 @@ class Proxy(_base_proxy.BaseImageProxy): + _resource_registry = { + "image": _image.Image, + "image_member": _member.Member, + "metadef_namespace": _metadef_namespace.MetadefNamespace, + "schema": _schema.Schema, + "info_import": _si.Import, + "info_store": _si.Store, + "task": _task.Task + } # ====== IMAGES ====== def _create_image(self, **kwargs): diff --git a/openstack/instance_ha/v1/_proxy.py b/openstack/instance_ha/v1/_proxy.py index 204b22c5a..ef1773759 100644 --- a/openstack/instance_ha/v1/_proxy.py +++ b/openstack/instance_ha/v1/_proxy.py @@ -26,6 +26,12 @@ class Proxy(proxy.Proxy): Create method for each action of each API. """ + _resource_registry = { + "host": _host.Host, + "notification": _notification.Notification, + "segment": _segment.Segment, + } + def notifications(self, **query): """Return a generator of notifications. diff --git a/openstack/key_manager/v1/_proxy.py b/openstack/key_manager/v1/_proxy.py index f0f42912e..c02b537e9 100644 --- a/openstack/key_manager/v1/_proxy.py +++ b/openstack/key_manager/v1/_proxy.py @@ -17,6 +17,11 @@ class Proxy(proxy.Proxy): + _resource_registry = { + "container": _container.Container, + "order": _order.Order, + "secret": _secret.Secret, + } def create_container(self, **attrs): """Create a new container from attributes diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index c7b347fd7..b5b4d7f50 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -30,6 +30,22 @@ class Proxy(proxy.Proxy): + _resource_registry = { + "amphora": _amphora.Amphora, + "availability_zone": _availability_zone.AvailabilityZone, + "availability_zone_profile": + _availability_zone_profile.AvailabilityZoneProfile, + "flavor": _flavor.Flavor, + "flavor_profile": _flavor_profile.FlavorProfile, + "health_monitor": _hm.HealthMonitor, + "l7_policy": _l7policy.L7Policy, + "l7_rule": _l7rule.L7Rule, + "load_balancer": _lb.LoadBalancer, + "member": _member.Member, + "pool": _pool.Pool, + "provider": _provider.Provider, + "quota": _quota.Quota + } def create_load_balancer(self, **attrs): """Create a new load balancer from attributes diff --git a/openstack/message/v2/_proxy.py b/openstack/message/v2/_proxy.py index 09bbd6907..31741890c 100644 --- a/openstack/message/v2/_proxy.py +++ b/openstack/message/v2/_proxy.py @@ -19,6 +19,12 @@ class Proxy(proxy.Proxy): + _resource_registry = { + "claim": _claim.Claim, + "message": _message.Message, + "queue": _queue.Queue, + "subscription": _subscription.Subscription, + } def create_queue(self, **attrs): """Create a new queue from attributes diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index bbcf5fd1d..fffe5073b 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -71,6 +71,63 @@ class Proxy(proxy.Proxy): + _resource_registry = { + "address_group": _address_group.AddressGroup, + "address_scope": _address_scope.AddressScope, + "agent": _agent.Agent, + "auto_allocated_topology": + _auto_allocated_topology.AutoAllocatedTopology, + "availability_zone": availability_zone.AvailabilityZone, + "extension": extension.Extension, + "firewall_group": _firewall_group.FirewallGroup, + "firewall_policy": _firewall_policy.FirewallPolicy, + "firewall_rule": _firewall_rule.FirewallRule, + "flavor": _flavor.Flavor, + "floating_ip": _floating_ip.FloatingIP, + "health_monitor": _health_monitor.HealthMonitor, + "l3_conntrack_helper": _l3_conntrack_helper.ConntrackHelper, + "listener": _listener.Listener, + "load_balancer": _load_balancer.LoadBalancer, + "local_ip": _local_ip.LocalIP, + "local_ip_association": _local_ip_association.LocalIPAssociation, + "metering_label": _metering_label.MeteringLabel, + "metering_label_rule": _metering_label_rule.MeteringLabelRule, + "ndp_proxy": _ndp_proxy.NDPProxy, + "network": _network.Network, + "network_ip_availability": + network_ip_availability.NetworkIPAvailability, + "network_segment_range": _network_segment_range.NetworkSegmentRange, + "pool": _pool.Pool, + "pool_member": _pool_member.PoolMember, + "port": _port.Port, + "port_forwarding": _port_forwarding.PortForwarding, + "qos_bandwidth_limit_rule": + _qos_bandwidth_limit_rule.QoSBandwidthLimitRule, + "qos_dscp_marking_rule": _qos_dscp_marking_rule.QoSDSCPMarkingRule, + "qos_minimum_bandwidth_rule": + _qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, + "qos_minimum_packet_rate_rule": + _qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule, + "qos_policy": _qos_policy.QoSPolicy, + "qos_rule_type": _qos_rule_type.QoSRuleType, + "quota": _quota.Quota, + "rbac_policy": _rbac_policy.RBACPolicy, + "router": _router.Router, + "security_group": _security_group.SecurityGroup, + "security_group_rule": _security_group_rule.SecurityGroupRule, + "segment": _segment.Segment, + "service_profile": _service_profile.ServiceProfile, + "service_provider": _service_provider.ServiceProvider, + "subnet": _subnet.Subnet, + "subnet_pool": _subnet_pool.SubnetPool, + "trunk": _trunk.Trunk, + "vpn_endpoint_group": _vpn_endpoint_group.VpnEndpointGroup, + "vpn_ike_policy": _ike_policy.VpnIkePolicy, + "vpn_ipsec_policy": _ipsec_policy.VpnIpsecPolicy, + "vpn_ipsec_site_connection": + _ipsec_site_connection.VpnIPSecSiteConnection, + "vpn_service": _vpn_service.VpnService, + } @proxy._check_resource(strict=False) def _update(self, resource_type, value, base_path=None, diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index 5433a45b2..f6dc9328b 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -36,6 +36,12 @@ class Proxy(proxy.Proxy): + _resource_registry = { + "account": _account.Account, + "container": _container.Container, + "info": _info.Info, + "object": _obj.Object + } skip_discovery = True diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index 744961e0a..d2c6367f0 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -25,6 +25,15 @@ class Proxy(proxy.Proxy): + _resource_registry = { + "resource": _resource.Resource, + "software_config": _sc.SoftwareConfig, + "software_deployment": _sd.SoftwareDeployment, + "stack": _stack.Stack, + "stack_environment": _stack_environment.StackEnvironment, + "stack_files": _stack_files.StackFiles, + "stack_template": _stack_template.StackTemplate, + } def _extract_name_consume_url_parts(self, url_parts): if (len(url_parts) == 3 and url_parts[0] == 'software_deployments' diff --git a/openstack/placement/v1/_proxy.py b/openstack/placement/v1/_proxy.py index ff99a7374..67dee393c 100644 --- a/openstack/placement/v1/_proxy.py +++ b/openstack/placement/v1/_proxy.py @@ -16,6 +16,10 @@ class Proxy(proxy.Proxy): + _resource_registry = { + "resource_class": _resource_class.ResourceClass, + "resource_provider": _resource_provider.ResourceProvider, + } # resource classes diff --git a/openstack/proxy.py b/openstack/proxy.py index 7790eaac3..26918d3f2 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -77,6 +77,12 @@ class Proxy(adapter.Adapter): ``_status_code_retries``. """ + _resource_registry = dict() + """Registry of the supported resourses. + + Dictionary of resource names (key) types (value). + """ + def __init__( self, session, diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index d77b63cc4..c4ffb1a9b 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -28,6 +28,14 @@ class Proxy(proxy.Proxy): + _resource_registry = { + "availability_zone": _availability_zone.AvailabilityZone, + "share_snapshot": _share_snapshot.ShareSnapshot, + "storage_pool": _storage_pool.StoragePool, + "user_message": _user_message.UserMessage, + "limit": _limit.Limit, + "share": _share.Share, + } def availability_zones(self): """Retrieve shared file system availability zones diff --git a/openstack/workflow/v2/_proxy.py b/openstack/workflow/v2/_proxy.py index 784fd8644..158ad7f20 100644 --- a/openstack/workflow/v2/_proxy.py +++ b/openstack/workflow/v2/_proxy.py @@ -16,6 +16,10 @@ class Proxy(proxy.Proxy): + _resource_registry = { + "execution": _execution.Execution, + "workflow": _workflow.Workflow, + } def create_workflow(self, **attrs): """Create a new workflow from attributes From e7e28fe548a8b30a857227a9a6942cbab644d2b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herv=C3=A9=20Beraud?= Date: Mon, 7 Nov 2022 11:02:00 +0100 Subject: [PATCH 3139/3836] Remove python-dev from bindep It is no longer supported by jammy and lead us to the following errors with the announce-release job. ``` No package matching 'python-dev' is available ``` Change-Id: Ie84a6e9f641f2bd586fdf6e56eab6406a7fbf48b --- bindep.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/bindep.txt b/bindep.txt index aeb48323a..a74f386ae 100644 --- a/bindep.txt +++ b/bindep.txt @@ -3,7 +3,6 @@ build-essential [platform:dpkg] python3-dev [platform:dpkg] -python-devel [platform:rpm] libffi-dev [platform:dpkg] libffi-devel [platform:rpm] openssl-devel [platform:rpm] From 666976ab05dfefaefb41a6fa41024084c1045028 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 20 Oct 2022 12:18:36 +0100 Subject: [PATCH 3140/3836] image: Allow providing 'data' argument to image upload, stage This feels a little more natural that setting an attribute on the object first. Change-Id: I11fd4847152b4df6229e82d9953bc7567611628c Signed-off-by: Stephen Finucane --- openstack/image/v2/image.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index 7a941f082..5a79c9c1c 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -264,15 +264,32 @@ def reactivate(self, session): """ self._action(session, "reactivate") - def upload(self, session): - """Upload data into an existing image""" + def upload(self, session, *, data=None): + """Upload data into an existing image + + :param session: The session to use for making this request + :param data: Optional data to be uploaded. If not provided, the + `~Image.data` attribute will be used + :returns: The server response + """ + if data: + self.data = data url = utils.urljoin(self.base_path, self.id, 'file') return session.put(url, data=self.data, headers={"Content-Type": "application/octet-stream", "Accept": ""}) - def stage(self, session): - """Stage binary image data into an existing image""" + def stage(self, session, *, data=None): + """Stage binary image data into an existing image + + :param session: The session to use for making this request + :param data: Optional data to be uploaded. If not provided, the + `~Image.data` attribute will be used + :returns: The server response + """ + if data: + self.data = data + url = utils.urljoin(self.base_path, self.id, 'stage') response = session.put( url, data=self.data, From 0bf4d86e5a6509095c9f01def41f9b8dd928d21a Mon Sep 17 00:00:00 2001 From: Daniel Wilson Date: Wed, 9 Nov 2022 23:16:50 -0500 Subject: [PATCH 3141/3836] Fix server action request generation Server action request paths were not interpolating server_id. This change marks server_id for interpolation in the ServerAction base_path, and uses request_id as the identifier for server actions. Change-Id: I0da8ec51ab94a094dc0e7d89aedf1830a08fd150 --- openstack/compute/v2/_proxy.py | 2 +- openstack/compute/v2/server_action.py | 4 ++-- openstack/tests/unit/compute/v2/test_proxy.py | 4 ++-- openstack/tests/unit/compute/v2/test_server_actions.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index e15629930..61bf6a71e 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -2174,7 +2174,7 @@ def get_server_action(self, server_action, server, ignore_missing=True): return self._get( _server_action.ServerAction, server_id=server_id, - action_id=server_action, + request_id=server_action, ignore_missing=ignore_missing, ) diff --git a/openstack/compute/v2/server_action.py b/openstack/compute/v2/server_action.py index fff96ae88..06e4a12e3 100644 --- a/openstack/compute/v2/server_action.py +++ b/openstack/compute/v2/server_action.py @@ -49,7 +49,7 @@ class ServerActionEvent(resource.Resource): class ServerAction(resource.Resource): resource_key = 'instanceAction' resources_key = 'instanceActions' - base_path = '/servers/{server_id}/os-instance-actions' + base_path = '/servers/%(server_id)s/os-instance-actions' # capabilities allow_fetch = True @@ -68,7 +68,7 @@ class ServerAction(resource.Resource): # #: The ID of the server that this action relates to. # server_id = resource.Body('instance_uuid') #: The ID of the request that this action related to. - request_id = resource.Body('request_id') + request_id = resource.Body('request_id', alternate_id=True) #: The ID of the user which initiated the server action. user_id = resource.Body('user_id') #: The ID of the project that this server belongs to. diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 9b166cc07..8c71fadcd 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -1453,11 +1453,11 @@ def test_server_action_get(self): self._verify( 'openstack.proxy.Proxy._get', self.proxy.get_server_action, - method_args=['action_id'], + method_args=['request_id'], method_kwargs={'server': 'server_id'}, expected_args=[server_action.ServerAction], expected_kwargs={ - 'action_id': 'action_id', 'server_id': 'server_id', + 'request_id': 'request_id', 'server_id': 'server_id', }, ) diff --git a/openstack/tests/unit/compute/v2/test_server_actions.py b/openstack/tests/unit/compute/v2/test_server_actions.py index bb34e2661..881634c35 100644 --- a/openstack/tests/unit/compute/v2/test_server_actions.py +++ b/openstack/tests/unit/compute/v2/test_server_actions.py @@ -55,7 +55,7 @@ def test_basic(self): self.assertEqual('instanceAction', sot.resource_key) self.assertEqual('instanceActions', sot.resources_key) self.assertEqual( - '/servers/{server_id}/os-instance-actions', + '/servers/%(server_id)s/os-instance-actions', sot.base_path, ) self.assertTrue(sot.allow_fetch) From 842133f88fd640ff72cd7854f799392f0991793a Mon Sep 17 00:00:00 2001 From: Samuel Kunkel Date: Tue, 15 Nov 2022 15:45:53 +0100 Subject: [PATCH 3142/3836] add flavor description to flavor_create To properly describe a flavor via openstacksdk we add the parameter to flavor create. If the value is not set we default to None. Change-Id: I9607230edfbd70fd085e694c4ee3fe08088051db --- openstack/cloud/_compute.py | 3 +++ openstack/tests/unit/cloud/test_flavors.py | 1 + 2 files changed, 4 insertions(+) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index ed95362d3..02661c692 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -1388,6 +1388,7 @@ def create_flavor( ram, vcpus, disk, + description=None, flavorid="auto", ephemeral=0, swap=0, @@ -1400,6 +1401,7 @@ def create_flavor( :param ram: Memory in MB for the flavor :param vcpus: Number of VCPUs for the flavor :param disk: Size of local disk in GB + :param description: Description of the flavor :param flavorid: ID for the flavor (optional) :param ephemeral: Ephemeral space size in GB :param swap: Swap space in MB @@ -1418,6 +1420,7 @@ def create_flavor( 'rxtx_factor': rxtx_factor, 'swap': swap, 'vcpus': vcpus, + 'description': description, } if flavorid == 'auto': attrs['id'] = None diff --git a/openstack/tests/unit/cloud/test_flavors.py b/openstack/tests/unit/cloud/test_flavors.py index 9676ae209..9ca45f4ad 100644 --- a/openstack/tests/unit/cloud/test_flavors.py +++ b/openstack/tests/unit/cloud/test_flavors.py @@ -34,6 +34,7 @@ def test_create_flavor(self): json={ 'flavor': { "name": "vanilla", + "description": None, "ram": 65536, "vcpus": 24, "swap": 0, From 45336fa45c6d934fa321407dd1a3dbfa959a28cc Mon Sep 17 00:00:00 2001 From: Felix Huettner Date: Tue, 15 Nov 2022 11:33:39 +0100 Subject: [PATCH 3143/3836] Prevent sending None password on create_user previously `create_user` would send a "password" and "description" to keystone even though they where `None`. However keystone will fail decoding the "password" as tries to parse `None` as a string which fails. Therefor we now skip sending the password if its None. Same applies to the description. Story: 2010429 Task: 46830 Change-Id: I111bad46e4c8045985965d9f5780368f39eba4b7 --- openstack/cloud/_identity.py | 7 +- .../tests/unit/cloud/test_identity_users.py | 68 +++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 openstack/tests/unit/cloud/test_identity_users.py diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index 159b08d68..bd4ead502 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -330,8 +330,11 @@ def create_user( ): """Create a user.""" params = self._get_identity_params(domain_id, default_project) - params.update({'name': name, 'password': password, 'email': email, - 'enabled': enabled, 'description': description}) + params.update({'name': name, 'email': email, 'enabled': enabled}) + if password is not None: + params['password'] = password + if description is not None: + params['description'] = description user = self.identity.create_user(**params) diff --git a/openstack/tests/unit/cloud/test_identity_users.py b/openstack/tests/unit/cloud/test_identity_users.py new file mode 100644 index 000000000..943029d67 --- /dev/null +++ b/openstack/tests/unit/cloud/test_identity_users.py @@ -0,0 +1,68 @@ +# 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 testtools import matchers + +from openstack.tests.unit import base + + +class TestIdentityUsers(base.TestCase): + + def get_mock_url(self, service_type='identity', interface='public', + resource='users', append=None, base_url_append='v3', + qs_elements=None): + return super(TestIdentityUsers, self).get_mock_url( + service_type, interface, resource, append, base_url_append, + qs_elements) + + def test_create_user(self): + domain_data = self._get_domain_data() + user_data = self._get_user_data("myusername", "mypassword", + domain_id=domain_data.domain_id) + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url(), + status_code=200, + json=user_data.json_response, + validate=dict(json=user_data.json_request)) + ]) + + user = self.cloud.create_user(user_data.name, + password=user_data.password, + domain_id=domain_data.domain_id) + + self.assertIsNotNone(user) + self.assertThat(user.name, matchers.Equals(user_data.name)) + self.assert_calls() + + def test_create_user_without_password(self): + domain_data = self._get_domain_data() + user_data = self._get_user_data("myusername", + domain_id=domain_data.domain_id) + user_data._replace( + password=None, + json_request=user_data.json_request["user"].pop("password")) + + self.register_uris([ + dict(method='POST', + uri=self.get_mock_url(), + status_code=200, + json=user_data.json_response, + validate=dict(json=user_data.json_request)) + ]) + + user = self.cloud.create_user(user_data.name, + domain_id=domain_data.domain_id) + + self.assertIsNotNone(user) + self.assertThat(user.name, matchers.Equals(user_data.name)) + self.assert_calls() From 03611413724c5def8a68cb71bf5af959e459ee0d Mon Sep 17 00:00:00 2001 From: Polina-Gubina Date: Thu, 10 Nov 2022 23:12:06 +0100 Subject: [PATCH 3144/3836] block storage volume resource - add 'is_multiattach' parameter (when true, disk will be shareble) Change-Id: Ic35bbb252e6da4084093dbae7dbc9106e4d1f539 --- openstack/block_storage/v3/volume.py | 2 ++ openstack/tests/unit/block_storage/v3/test_volume.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index e735fa066..15128f818 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -62,6 +62,8 @@ class Volume(resource.Resource, metadata.MetadataMixin): #: ``True`` if this volume is encrypted, ``False`` if not. #: *Type: bool* is_encrypted = resource.Body("encrypted", type=format.BoolStr) + #: Whether volume will be sharable or not. + is_multiattach = resource.Body("multiattach", type=bool) #: The volume ID that this volume's name on the back-end is based on. migration_id = resource.Body("os-vol-mig-status-attr:name_id") #: The status of this volume's migration (None means that a migration diff --git a/openstack/tests/unit/block_storage/v3/test_volume.py b/openstack/tests/unit/block_storage/v3/test_volume.py index f7acfe55b..9b2a76892 100644 --- a/openstack/tests/unit/block_storage/v3/test_volume.py +++ b/openstack/tests/unit/block_storage/v3/test_volume.py @@ -42,6 +42,7 @@ "source_volid": None, "imageRef": "some_image", "metadata": {}, + "multiattach": False, "volume_image_metadata": IMAGE_METADATA, "id": FAKE_ID, "size": 10, @@ -101,6 +102,7 @@ def test_create(self): self.assertEqual(VOLUME["snapshot_id"], sot.snapshot_id) self.assertEqual(VOLUME["source_volid"], sot.source_volume_id) self.assertEqual(VOLUME["metadata"], sot.metadata) + self.assertEqual(VOLUME["multiattach"], sot.is_multiattach) self.assertEqual(VOLUME["volume_image_metadata"], sot.volume_image_metadata) self.assertEqual(VOLUME["size"], sot.size) From dc6be6e906585cc2b86fe956f58fe80c6c17bfad Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 18 Nov 2022 13:25:35 +0100 Subject: [PATCH 3145/3836] Move get_compute_usage to use proxy Address todo in compute cloud layer and move get_compute_usage to rely to the proxy layer. With this change method returns Usage object instead of munch. This also allows us to stop importing and inheriting from normalize class here. "attached" filter of the search_floating_ips is dopped since it is being pretty awkward. Change-Id: I3de1654fc35b2985e4a0658f7117bbe9db554dd3 --- openstack/cloud/_compute.py | 42 +++---------------- openstack/compute/v2/_proxy.py | 8 ++-- .../tests/functional/cloud/test_compute.py | 4 +- .../functional/cloud/test_floating_ip.py | 8 +--- openstack/tests/unit/cloud/test_usage.py | 1 + openstack/tests/unit/compute/v2/test_proxy.py | 10 ++++- .../get_compute_usage-01811dccd60dc92a.yaml | 5 +++ 7 files changed, 26 insertions(+), 52 deletions(-) create mode 100644 releasenotes/notes/get_compute_usage-01811dccd60dc92a.yaml diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index ed95362d3..069595053 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -14,27 +14,23 @@ # We can't just use list, because sphinx gets confused by # openstack.resource.Resource.list and openstack.resource2.Resource.list import base64 -import datetime import functools import operator import threading import time -import types # noqa import iso8601 -from openstack.cloud import _normalize from openstack.cloud import _utils from openstack.cloud import exc from openstack.cloud import meta from openstack.compute.v2 import quota_set as _qs from openstack.compute.v2 import server as _server from openstack import exceptions -from openstack import proxy from openstack import utils -class ComputeCloudMixin(_normalize.Normalizer): +class ComputeCloudMixin: def __init__(self): self._servers = None @@ -1688,7 +1684,6 @@ def delete_compute_quotas(self, name_or_id): name_or_id, ignore_missing=False) self.compute.revert_quota_set(proj) - # TODO(stephenfin): Convert to proxy methods def get_compute_usage(self, name_or_id, start=None, end=None): """ Get usage for a specific project @@ -1700,9 +1695,8 @@ def get_compute_usage(self, name_or_id, start=None, end=None): Defaults to now :raises: OpenStackCloudException if it's not a valid project - :returns: Munch object with the usage + :returns: A :class:`~openstack.compute.v2.usage.Usage` object """ - def parse_date(date): try: return iso8601.parse_date(date) @@ -1716,43 +1710,17 @@ def parse_date(date): " YYYY-MM-DDTHH:MM:SS".format( date=date)) - def parse_datetime_for_nova(date): - # Must strip tzinfo from the date- it breaks Nova. Also, - # Nova is expecting this in UTC. If someone passes in an - # ISO8601 date string or a datetime with timzeone data attached, - # strip the timezone data but apply offset math first so that - # the user's well formed perfectly valid date will be used - # correctly. - offset = date.utcoffset() - if offset: - date = date - datetime.timedelta(hours=offset) - return date.replace(tzinfo=None) - - if not start: - start = parse_date('2010-07-06') - elif not isinstance(start, datetime.datetime): + if isinstance(start, str): start = parse_date(start) - if not end: - end = datetime.datetime.utcnow() - elif not isinstance(start, datetime.datetime): + if isinstance(end, str): end = parse_date(end) - start = parse_datetime_for_nova(start) - end = parse_datetime_for_nova(end) - proj = self.get_project(name_or_id) if not proj: raise exc.OpenStackCloudException( "project does not exist: {name}".format(name=proj.id)) - data = proxy._json_response( - self.compute.get( - '/os-simple-tenant-usage/{project}'.format(project=proj.id), - params=dict(start=start.isoformat(), end=end.isoformat())), - error_message="Unable to get usage for project: {name}".format( - name=proj.id)) - return self._normalize_compute_usage( - self._get_and_munchify('tenant_usage', data)) + return self.compute.get_usage(proj, start, end) def _encode_server_userdata(self, userdata): if hasattr(userdata, 'read'): diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index d7d8bae05..30cefcb42 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -1977,10 +1977,10 @@ def usages(self, start=None, end=None, **query): :returns: A list of compute ``Usage`` objects. """ if start is not None: - query['start'] = start + query['start'] = start.isoformat() if end is not None: - query['end'] = end + query['end'] = end.isoformat() return self._list(_usage.Usage, **query) @@ -1998,10 +1998,10 @@ def get_usage(self, project, start=None, end=None, **query): project = self._get_resource(_project.Project, project) if start is not None: - query['start'] = start + query['start'] = start.isoformat() if end is not None: - query['end'] = end + query['end'] = end.isoformat() res = self._get_resource(_usage.Usage, project.id) return res.fetch(self, **query) diff --git a/openstack/tests/functional/cloud/test_compute.py b/openstack/tests/functional/cloud/test_compute.py index c91fe7e08..f25421abd 100644 --- a/openstack/tests/functional/cloud/test_compute.py +++ b/openstack/tests/functional/cloud/test_compute.py @@ -485,6 +485,6 @@ def test_get_compute_usage(self): self.add_info_on_exception('usage', usage) self.assertIsNotNone(usage) self.assertIn('total_hours', usage) - self.assertIn('started_at', usage) - self.assertEqual(start.isoformat(), usage['started_at']) + self.assertIn('start', usage) + self.assertEqual(start.isoformat(), usage['start']) self.assertIn('location', usage) diff --git a/openstack/tests/functional/cloud/test_floating_ip.py b/openstack/tests/functional/cloud/test_floating_ip.py index 24d72ed4b..8987decd0 100644 --- a/openstack/tests/functional/cloud/test_floating_ip.py +++ b/openstack/tests/functional/cloud/test_floating_ip.py @@ -274,13 +274,7 @@ def test_search_floating_ips(self): self.assertIn( fip_user['id'], - [fip.id for fip in self.user_cloud.search_floating_ips( - filters={"attached": False})] - ) - self.assertNotIn( - fip_user['id'], - [fip.id for fip in self.user_cloud.search_floating_ips( - filters={"attached": True})] + [fip.id for fip in self.user_cloud.search_floating_ips()] ) def test_get_floating_ip_by_id(self): diff --git a/openstack/tests/unit/cloud/test_usage.py b/openstack/tests/unit/cloud/test_usage.py index 0aedc4c0e..29cea236c 100644 --- a/openstack/tests/unit/cloud/test_usage.py +++ b/openstack/tests/unit/cloud/test_usage.py @@ -25,6 +25,7 @@ def test_get_usage(self): start = end = datetime.datetime.now() self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 0625ad8e7..e7f850b92 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -1292,7 +1292,10 @@ def test_usages__with_kwargs(self): self.proxy.usages, usage.Usage, method_kwargs={'start': start, 'end': end}, - expected_kwargs={'start': start, 'end': end}, + expected_kwargs={ + 'start': start.isoformat(), + 'end': end.isoformat() + }, ) def test_get_usage(self): @@ -1315,7 +1318,10 @@ def test_get_usage__with_kwargs(self): method_args=['value'], method_kwargs={'start': start, 'end': end}, expected_args=[self.proxy], - expected_kwargs={'start': start, 'end': end}, + expected_kwargs={ + 'start': start.isoformat(), + 'end': end.isoformat() + }, ) def test_create_server_remote_console(self): diff --git a/releasenotes/notes/get_compute_usage-01811dccd60dc92a.yaml b/releasenotes/notes/get_compute_usage-01811dccd60dc92a.yaml new file mode 100644 index 000000000..7eaf86982 --- /dev/null +++ b/releasenotes/notes/get_compute_usage-01811dccd60dc92a.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + cloud.get_compute_usage method return instance of compute.usage.Usage class + instead of munch. From 6b62d2815131f327f093e4b95489a31efa7d9d3c Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 4 Nov 2022 17:23:00 +0100 Subject: [PATCH 3146/3836] Implement unified search_resources method We have a lot of search_XXX calls in the cloud layer, but not for everything. They are also doing nearly same to what it is possible to do with plain proxy methods and therfore we want to simplify and unify this so that Ansible modules can easily rely on a single function in different modules instead or needing a dedicated search method for every resource doing the same. New method accepts resource_type, resource identifier (which may be empty), filters and also possibility to pass additional args into the _get and _list calls (for unpredicted special cases). With this all search_XXX functions can be finally deprecated. Change-Id: I375c2b625698c4920211eb6e089a1b820755be84 --- openstack/cloud/openstackcloud.py | 71 +++++++++ .../tests/unit/cloud/test_openstackcloud.py | 135 ++++++++++++++++++ .../search_resource-b9c2f772e01d3b2c.yaml | 7 + 3 files changed, 213 insertions(+) create mode 100644 openstack/tests/unit/cloud/test_openstackcloud.py create mode 100644 releasenotes/notes/search_resource-b9c2f772e01d3b2c.yaml diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 0b5cbb527..9e715cb9d 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -753,6 +753,77 @@ def has_service(self, service_key, version=None): else: return False + def search_resources( + self, + resource_type, + name_or_id, + get_args=None, + get_kwargs=None, + list_args=None, + list_kwargs=None, + **filters + ): + """Search resources + + Search resources matching certain conditions + + :param str resource_type: String representation of the expected + resource as `service.resource` (i.e. "network.security_group"). + :param str name_or_id: Name or ID of the resource + :param list get_args: Optional args to be passed to the _get call. + :param dict get_kwargs: Optional kwargs to be passed to the _get call. + :param list list_args: Optional args to be passed to the _list call. + :param dict list_kwargs: Optional kwargs to be passed to the _list call + :param dict filters: Additional filters to be used for querying + resources. + """ + get_args = get_args or () + get_kwargs = get_kwargs or {} + list_args = list_args or () + list_kwargs = list_kwargs or {} + + # User used string notation. Try to find proper + # resource + (service_name, resource_name) = resource_type.split('.') + if not hasattr(self, service_name): + raise exceptions.SDKException( + "service %s is not existing/enabled" % + service_name + ) + service_proxy = getattr(self, service_name) + try: + resource_type = service_proxy._resource_registry[resource_name] + except KeyError: + raise exceptions.SDKException( + "Resource %s is not known in service %s" % + (resource_name, service_name) + ) + + if name_or_id: + # name_or_id is definitely not None + try: + resource_by_id = service_proxy._get( + resource_type, + name_or_id, + *get_args, + **get_kwargs) + return [resource_by_id] + except exceptions.ResourceNotFound: + pass + + if not filters: + filters = {} + + if name_or_id: + filters["name"] = name_or_id + list_kwargs.update(filters) + + return list(service_proxy._list( + resource_type, + *list_args, + **list_kwargs + )) + def project_cleanup( self, dry_run=True, diff --git a/openstack/tests/unit/cloud/test_openstackcloud.py b/openstack/tests/unit/cloud/test_openstackcloud.py new file mode 100644 index 000000000..07d227995 --- /dev/null +++ b/openstack/tests/unit/cloud/test_openstackcloud.py @@ -0,0 +1,135 @@ +# 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 unittest import mock + +from openstack import exceptions +from openstack import proxy +from openstack import resource +from openstack.tests.unit import base + + +class TestSearch(base.TestCase): + + class FakeResource(resource.Resource): + allow_fetch = True + allow_list = True + + foo = resource.Body("foo") + + def setUp(self): + super(TestSearch, self).setUp() + + self.session = proxy.Proxy(self.cloud) + self.session._sdk_connection = self.cloud + self.session._get = mock.Mock() + self.session._list = mock.Mock() + self.session._resource_registry = dict( + fake=self.FakeResource + ) + # Set the mock into the cloud connection + setattr(self.cloud, "mock_session", self.session) + + def test_raises_unknown_service(self): + self.assertRaises( + exceptions.SDKException, + self.cloud.search_resources, + "wrong_service.wrong_resource", + "name" + ) + + def test_raises_unknown_resource(self): + self.assertRaises( + exceptions.SDKException, + self.cloud.search_resources, + "mock_session.wrong_resource", + "name" + ) + + def test_search_resources_get_finds(self): + self.session._get.return_value = self.FakeResource(foo="bar") + + ret = self.cloud.search_resources( + "mock_session.fake", + "fake_name" + ) + self.session._get.assert_called_with( + self.FakeResource, "fake_name") + + self.assertEqual(1, len(ret)) + self.assertEqual( + self.FakeResource(foo="bar").to_dict(), + ret[0].to_dict() + ) + + def test_search_resources_list(self): + self.session._get.side_effect = exceptions.ResourceNotFound + self.session._list.return_value = [ + self.FakeResource(foo="bar") + ] + + ret = self.cloud.search_resources( + "mock_session.fake", + "fake_name" + ) + self.session._get.assert_called_with( + self.FakeResource, "fake_name") + self.session._list.assert_called_with( + self.FakeResource, name="fake_name") + + self.assertEqual(1, len(ret)) + self.assertEqual( + self.FakeResource(foo="bar").to_dict(), + ret[0].to_dict() + ) + + def test_search_resources_args(self): + self.session._get.side_effect = exceptions.ResourceNotFound + self.session._list.return_value = [] + + self.cloud.search_resources( + "mock_session.fake", + "fake_name", + get_args=["getarg1"], + get_kwargs={"getkwarg1": "1"}, + list_args=["listarg1"], + list_kwargs={"listkwarg1": "1"}, + filter1="foo" + ) + self.session._get.assert_called_with( + self.FakeResource, "fake_name", + "getarg1", getkwarg1="1") + self.session._list.assert_called_with( + self.FakeResource, + "listarg1", listkwarg1="1", + name="fake_name", filter1="foo" + ) + + def test_search_resources_name_empty(self): + self.session._list.return_value = [ + self.FakeResource(foo="bar") + ] + + ret = self.cloud.search_resources( + "mock_session.fake", + None, + foo="bar" + ) + self.session._get.assert_not_called() + self.session._list.assert_called_with( + self.FakeResource, foo="bar") + + self.assertEqual(1, len(ret)) + self.assertEqual( + self.FakeResource(foo="bar").to_dict(), + ret[0].to_dict() + ) diff --git a/releasenotes/notes/search_resource-b9c2f772e01d3b2c.yaml b/releasenotes/notes/search_resource-b9c2f772e01d3b2c.yaml new file mode 100644 index 000000000..70efca4a2 --- /dev/null +++ b/releasenotes/notes/search_resource-b9c2f772e01d3b2c.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Add search_resources method implementing generic search interface accepting + resource name (as "service.resource"), name_or_id and list of additional + filters and returning 0 or many resources matching those. This interface is + primarily designed to be used by Ansible modules. From 1ea95cb04ef6dddba35383d204c1768b31be237e Mon Sep 17 00:00:00 2001 From: Daniel Wilson Date: Sat, 12 Nov 2022 13:38:36 -0500 Subject: [PATCH 3147/3836] Fix server topology and diagnostics Server topology assumed that the response from a topology request included a top-level topology attribute, which actually does not exist. Server diagnostics expected that server_id was a body field, but it is actually a URI field. Change-Id: I18baf1a8c39c5f150b64ce9c0d8944214c9e8024 --- openstack/compute/v2/server.py | 4 +--- openstack/compute/v2/server_diagnostics.py | 2 +- openstack/tests/unit/compute/v2/test_server.py | 4 +--- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 4a864dc84..5f95bccb4 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -596,9 +596,7 @@ def fetch_topology(self, session): exceptions.raise_from_response(response) try: - data = response.json() - if 'topology' in data: - return data['topology'] + return response.json() except ValueError: pass diff --git a/openstack/compute/v2/server_diagnostics.py b/openstack/compute/v2/server_diagnostics.py index faed7e9d7..1208639ed 100644 --- a/openstack/compute/v2/server_diagnostics.py +++ b/openstack/compute/v2/server_diagnostics.py @@ -51,4 +51,4 @@ class ServerDiagnostics(resource.Resource): #: The list of dictionaries with detailed information about VM NICs. nic_details = resource.Body('nic_details') #: The ID for the server. - server_id = resource.Body('server_id') + server_id = resource.URI('server_id') diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 85adacc92..9f2ffd254 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -1097,9 +1097,7 @@ class FakeEndpointData: } response.status_code = 200 - response.json.return_value = { - 'topology': topology - } + response.json.return_value = topology self.sess.get.return_value = response From 2267d458bb007073ade2496998877a02dda2f904 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 18 Nov 2022 15:04:41 +0100 Subject: [PATCH 3148/3836] Stop normalizing floating ips Next steps of a cleanup of the cloud layer are: - stop doing normalization of floating ips in case of neutron - stop inheriting from _normalize in the floating_ip class (move 2 required functions locally). Soon this is going to be reworked in favor of making better object support for those still using nova networking Change-Id: If67c2f8b78240c03f86daefcf0f472502821365a --- openstack/cloud/_floating_ip.py | 143 +++++++++++++++--- openstack/cloud/_normalize.py | 107 ------------- .../unit/cloud/test_floating_ip_neutron.py | 22 +-- ...ing_ip_normalization-41e0edcdb0c98aee.yaml | 10 ++ 4 files changed, 131 insertions(+), 151 deletions(-) create mode 100644 releasenotes/notes/floating_ip_normalization-41e0edcdb0c98aee.yaml diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index e8eccc725..7123a261f 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -18,8 +18,8 @@ import threading import time import types # noqa +import warnings -from openstack.cloud import _normalize from openstack.cloud import _utils from openstack.cloud import exc from openstack.cloud import meta @@ -32,7 +32,7 @@ "user/config/configuration.html") -class FloatingIPCloudMixin(_normalize.Normalizer): +class FloatingIPCloudMixin: def __init__(self): self.private = self.config.config.get('private', False) @@ -64,20 +64,14 @@ def search_floating_ip_pools(self, name=None, filters=None): def search_floating_ips(self, id=None, filters=None): # `filters` could be a jmespath expression which Neutron server doesn't # understand, obviously. + warnings.warn( + "search_floating_ips is deprecated. " + "Use search_resource instead.") if self._use_neutron_floating() and isinstance(filters, dict): - filter_keys = ['router_id', 'status', 'tenant_id', 'project_id', - 'revision_number', 'description', - 'floating_network_id', 'fixed_ip_address', - 'floating_ip_address', 'port_id', 'sort_dir', - 'sort_key', 'tags', 'tags-any', 'not-tags', - 'not-tags-any', 'fields'] - neutron_filters = {k: v for k, v in filters.items() - if k in filter_keys} - kwargs = {'filters': neutron_filters} + return list(self.network.ips(**filters)) else: - kwargs = {} - floating_ips = self.list_floating_ips(**kwargs) - return _utils._filter_list(floating_ips, id, filters) + floating_ips = self.list_floating_ips() + return _utils._filter_list(floating_ips, id, filters) def _neutron_list_floating_ips(self, filters=None): if not filters: @@ -121,8 +115,7 @@ def get_floating_ip(self, id, filters=None): def _list_floating_ips(self, filters=None): if self._use_neutron_floating(): try: - return self._normalize_floating_ips( - self._neutron_list_floating_ips(filters)) + return self._neutron_list_floating_ips(filters) except exc.OpenStackCloudURINotFound as e: # Nova-network don't support server-side floating ips # filtering, so it's safer to return and empty list than @@ -223,13 +216,15 @@ def get_floating_ip_by_id(self, id): """ Get a floating ip by ID :param id: ID of the floating ip. - :returns: A floating ip ``munch.Munch``. + :returns: A floating ip + `:class:`~openstack.network.v2.floating_ip.FloatingIP` or + ``munch.Munch``. """ error_message = "Error getting floating ip with ID {id}".format(id=id) if self._use_neutron_floating(): fip = self.network.get_ip(id) - return self._normalize_floating_ip(fip) + return fip else: data = proxy._json_response( self.compute.get('/os-floating-ips/{id}'.format(id=id)), @@ -372,9 +367,8 @@ def available_floating_ip(self, network=None, server=None): """ if self._use_neutron_floating(): try: - f_ips = self._normalize_floating_ips( - self._neutron_available_floating_ips( - network=network, server=server)) + f_ips = self._neutron_available_floating_ips( + network=network, server=server) return f_ips[0] except exc.OpenStackCloudURINotFound as e: self.log.debug( @@ -458,8 +452,7 @@ def create_floating_ip(self, network=None, server=None, def _submit_create_fip(self, kwargs): # Split into a method to aid in test mocking - data = self.network.create_ip(**kwargs) - return self._normalize_floating_ip(data) + return self.network.create_ip(**kwargs) def _neutron_create_floating_ip( self, network_name_or_id=None, server=None, @@ -651,7 +644,7 @@ def delete_unattached_floating_ips(self, retry=1): processed = [] if self._use_neutron_floating(): for ip in self.list_floating_ips(): - if not ip['attached']: + if not bool(ip.port_id): processed.append(self.delete_floating_ip( floating_ip_id=ip['id'], retry=retry)) return len(processed) if all(processed) else False @@ -794,7 +787,7 @@ def detach_ip_from_server(self, server_id, floating_ip_id): def _neutron_detach_ip_from_server(self, server_id, floating_ip_id): f_ip = self.get_floating_ip(id=floating_ip_id) - if f_ip is None or not f_ip['attached']: + if f_ip is None or not bool(f_ip.port_id): return False try: self.network.update_ip( @@ -1184,3 +1177,103 @@ def _has_floating_ips(self): def _use_neutron_floating(self): return (self.has_service('network') and self._floating_ip_source == 'neutron') + + def _normalize_floating_ips(self, ips): + """Normalize the structure of floating IPs + + Unfortunately, not all the Neutron floating_ip attributes are available + with Nova and not all Nova floating_ip attributes are available with + Neutron. + This function extract attributes that are common to Nova and Neutron + floating IP resource. + If the whole structure is needed inside shade, shade provides private + methods that returns "original" objects (e.g. + _neutron_allocate_floating_ip) + + :param list ips: A list of Neutron floating IPs. + + :returns: + A list of normalized dicts with the following attributes:: + + [ + { + "id": "this-is-a-floating-ip-id", + "fixed_ip_address": "192.0.2.10", + "floating_ip_address": "198.51.100.10", + "network": "this-is-a-net-or-pool-id", + "attached": True, + "status": "ACTIVE" + }, ... + ] + + """ + return [ + self._normalize_floating_ip(ip) for ip in ips + ] + + def _normalize_floating_ip(self, ip): + # Copy incoming floating ip because of shared dicts in unittests + # Only import munch when we really need it + import munch + + location = self._get_current_location( + project_id=ip.get('owner')) + # This copy is to keep things from getting epically weird in tests + ip = ip.copy() + + ret = munch.Munch(location=location) + + fixed_ip_address = ip.pop('fixed_ip_address', ip.pop('fixed_ip', None)) + floating_ip_address = ip.pop('floating_ip_address', ip.pop('ip', None)) + network_id = ip.pop( + 'floating_network_id', ip.pop('network', ip.pop('pool', None))) + project_id = ip.pop('tenant_id', '') + project_id = ip.pop('project_id', project_id) + + instance_id = ip.pop('instance_id', None) + router_id = ip.pop('router_id', None) + id = ip.pop('id') + port_id = ip.pop('port_id', None) + created_at = ip.pop('created_at', None) + updated_at = ip.pop('updated_at', None) + # Note - description may not always be on the underlying cloud. + # Normalizing it here is easy - what do we do when people want to + # set a description? + description = ip.pop('description', '') + revision_number = ip.pop('revision_number', None) + + if self._use_neutron_floating(): + attached = bool(port_id) + status = ip.pop('status', 'UNKNOWN') + else: + attached = bool(instance_id) + # In neutron's terms, Nova floating IPs are always ACTIVE + status = 'ACTIVE' + + ret = munch.Munch( + attached=attached, + fixed_ip_address=fixed_ip_address, + floating_ip_address=floating_ip_address, + id=id, + location=self._get_current_location(project_id=project_id), + network=network_id, + port=port_id, + router=router_id, + status=status, + created_at=created_at, + updated_at=updated_at, + description=description, + revision_number=revision_number, + properties=ip.copy(), + ) + # Backwards compat + if not self.strict_mode: + ret['port_id'] = port_id + ret['router_id'] = router_id + ret['project_id'] = project_id + ret['tenant_id'] = project_id + ret['floating_network_id'] = network_id + for key, val in ret['properties'].items(): + ret.setdefault(key, val) + + return ret diff --git a/openstack/cloud/_normalize.py b/openstack/cloud/_normalize.py index bc4730284..3d3a84dbc 100644 --- a/openstack/cloud/_normalize.py +++ b/openstack/cloud/_normalize.py @@ -404,113 +404,6 @@ def _normalize_server(self, server): ret.setdefault(key, val) return ret - # TODO(stephenfin): Remove this once we get rid of support for nova - # floating IPs - def _normalize_floating_ips(self, ips): - """Normalize the structure of floating IPs - - Unfortunately, not all the Neutron floating_ip attributes are available - with Nova and not all Nova floating_ip attributes are available with - Neutron. - This function extract attributes that are common to Nova and Neutron - floating IP resource. - If the whole structure is needed inside shade, shade provides private - methods that returns "original" objects (e.g. - _neutron_allocate_floating_ip) - - :param list ips: A list of Neutron floating IPs. - - :returns: - A list of normalized dicts with the following attributes:: - - [ - { - "id": "this-is-a-floating-ip-id", - "fixed_ip_address": "192.0.2.10", - "floating_ip_address": "198.51.100.10", - "network": "this-is-a-net-or-pool-id", - "attached": True, - "status": "ACTIVE" - }, ... - ] - - """ - return [ - self._normalize_floating_ip(ip) for ip in ips - ] - - # TODO(stephenfin): Remove this once we get rid of support for nova - # floating IPs - def _normalize_floating_ip(self, ip): - # Copy incoming floating ip because of shared dicts in unittests - if isinstance(ip, resource.Resource): - ip = ip.to_dict(ignore_none=True, original_names=True) - location = ip.pop( - 'location', - self._get_current_location(project_id=ip.get('owner'))) - else: - location = self._get_current_location( - project_id=ip.get('owner')) - # This copy is to keep things from getting epically weird in tests - ip = ip.copy() - - ret = munch.Munch(location=location) - - fixed_ip_address = ip.pop('fixed_ip_address', ip.pop('fixed_ip', None)) - floating_ip_address = ip.pop('floating_ip_address', ip.pop('ip', None)) - network_id = ip.pop( - 'floating_network_id', ip.pop('network', ip.pop('pool', None))) - project_id = ip.pop('tenant_id', '') - project_id = ip.pop('project_id', project_id) - - instance_id = ip.pop('instance_id', None) - router_id = ip.pop('router_id', None) - id = ip.pop('id') - port_id = ip.pop('port_id', None) - created_at = ip.pop('created_at', None) - updated_at = ip.pop('updated_at', None) - # Note - description may not always be on the underlying cloud. - # Normalizing it here is easy - what do we do when people want to - # set a description? - description = ip.pop('description', '') - revision_number = ip.pop('revision_number', None) - - if self._use_neutron_floating(): - attached = bool(port_id) - status = ip.pop('status', 'UNKNOWN') - else: - attached = bool(instance_id) - # In neutron's terms, Nova floating IPs are always ACTIVE - status = 'ACTIVE' - - ret = munch.Munch( - attached=attached, - fixed_ip_address=fixed_ip_address, - floating_ip_address=floating_ip_address, - id=id, - location=self._get_current_location(project_id=project_id), - network=network_id, - port=port_id, - router=router_id, - status=status, - created_at=created_at, - updated_at=updated_at, - description=description, - revision_number=revision_number, - properties=ip.copy(), - ) - # Backwards compat - if not self.strict_mode: - ret['port_id'] = port_id - ret['router_id'] = router_id - ret['project_id'] = project_id - ret['tenant_id'] = project_id - ret['floating_network_id'] = network_id - for key, val in ret['properties'].items(): - ret.setdefault(key, val) - - return ret - def _normalize_compute_usage(self, usage): """ Normalize a compute usage object """ diff --git a/openstack/tests/unit/cloud/test_floating_ip_neutron.py b/openstack/tests/unit/cloud/test_floating_ip_neutron.py index 9cf43fdf9..a3ae14229 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_neutron.py +++ b/openstack/tests/unit/cloud/test_floating_ip_neutron.py @@ -144,23 +144,7 @@ def setUp(self): u'version': 4, u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:ae:7d:42'}]}) - self.floating_ip = self.cloud._normalize_floating_ips( - self.mock_floating_ip_list_rep['floatingips'])[0] - - def test_float_no_status(self): - floating_ips = [ - { - 'fixed_ip_address': '10.0.0.4', - 'floating_ip_address': '172.24.4.229', - 'floating_network_id': 'my-network-id', - 'id': '2f245a7b-796b-4f26-9cf9-9e82d248fda8', - 'port_id': None, - 'router_id': None, - 'tenant_id': '4969c491a3c74ee4af974e6d800c62df' - } - ] - normalized = self.cloud._normalize_floating_ips(floating_ips) - self.assertEqual('UNKNOWN', normalized[0]['status']) + self.floating_ip = self.mock_floating_ip_list_rep['floatingips'][0] def test_list_floating_ips(self): self.register_uris([ @@ -195,11 +179,11 @@ def test_search_floating_ips(self): json=self.mock_floating_ip_list_rep)]) floating_ips = self.cloud.search_floating_ips( - filters={'attached': False}) + filters={'updated_at': 'never'}) self.assertIsInstance(floating_ips, list) self.assertAreInstances(floating_ips, dict) - self.assertEqual(1, len(floating_ips)) + self.assertEqual(0, len(floating_ips)) self.assert_calls() def test_get_floating_ip(self): diff --git a/releasenotes/notes/floating_ip_normalization-41e0edcdb0c98aee.yaml b/releasenotes/notes/floating_ip_normalization-41e0edcdb0c98aee.yaml new file mode 100644 index 000000000..82de33d14 --- /dev/null +++ b/releasenotes/notes/floating_ip_normalization-41e0edcdb0c98aee.yaml @@ -0,0 +1,10 @@ +--- +upgrade: + - | + No Munch conversion and normalization of the floating ips is happening + anymore. For Neutron network a pure FloatingIP object is being returned, + for Nova still munch object. +deprecations: + - | + search_floating_ips method is deprecated and should not be used anymore. It + is going to be dropped approximately after one major cycle. From 62f64754f100d4ed03c377197d5f81358729acf1 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 21 Nov 2022 18:07:54 +0000 Subject: [PATCH 3149/3836] docs: Add docstring to 'openstack' module Change-Id: I8e16961c8812ee9c15a8771ea7babf81c9ec534e Signed-off-by: Stephen Finucane --- openstack/__init__.py | 72 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 58 insertions(+), 14 deletions(-) diff --git a/openstack/__init__.py b/openstack/__init__.py index e7827c5d6..f90b4ec92 100644 --- a/openstack/__init__.py +++ b/openstack/__init__.py @@ -11,9 +11,49 @@ # 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 typing -from openstack._log import enable_logging # noqa +"""The openstack SDK. + +:py:mod:`openstacksdk` is a client library for building applications to work +with OpenStack clouds. The project aims to provide a consistent and complete +set of interactions with OpenStack's many services, along with complete +documentation, examples, and tools. + +There are three ways to interact with :py:mod:`openstacksdk`. The *clouds +layer*, the *proxy layer*, and the *resource layer*. Most users will make use +of either the *cloud layer* or *proxy layer*. + +Listing flavours using the *cloud layer*:: + + >>> import openstack + >>> conn = openstack.connect(cloud='mordred') + >>> for server in conn.list_servers(): + ... print(server.to_dict()) + +Listing servers using the *proxy layer*:: + + >>> import openstack + >>> conn = openstack.connect(cloud='mordred') + >>> for server in conn.compute.servers(): + ... print(server.to_dict()) + +Listing servers using the *resource layer*:: + + >>> import openstack + >>> import openstack.compute.v2.server + >>> conn = openstack.connect(cloud='mordred') + >>> for server in openstack.compute.v2.server.Server.list( + ... session=conn.compute, + ... ): + ... print(server.to_dict()) + +For more information, refer to the documentation found in each submodule. +""" + +import argparse +import typing as ty + +from openstack._log import enable_logging import openstack.config import openstack.connection @@ -24,14 +64,14 @@ def connect( - cloud=None, - app_name=None, # type: typing.Optional[str] - app_version=None, # type: typing.Optional[str] - options=None, - load_yaml_config=True, # type: bool - load_envvars=True, # type: bool - **kwargs): - # type: (...) -> openstack.connection.Connection + cloud: ty.Optional[str] = None, + app_name: ty.Optional[str] = None, + app_version: ty.Optional[str] = None, + options: ty.Optional[argparse.Namespace] = None, + load_yaml_config: bool = True, + load_envvars: bool = True, + **kwargs, +) -> openstack.connection.Connection: """Create a :class:`~openstack.connection.Connection` :param string cloud: @@ -39,7 +79,7 @@ def connect( to 'envvars' which will load configuration settings from environment variables that start with ``OS_``. :param argparse.Namespace options: - An argparse Namespace object. allows direct passing in of + An argparse Namespace object. Allows direct passing in of argparse options to be added to the cloud config. Values of None and '' will be removed. :param bool load_yaml_config: @@ -57,10 +97,14 @@ def connect( """ cloud_region = openstack.config.get_cloud_region( cloud=cloud, - app_name=app_name, app_version=app_version, + app_name=app_name, + app_version=app_version, load_yaml_config=load_yaml_config, load_envvars=load_envvars, - options=options, **kwargs) + options=options, + **kwargs, + ) return openstack.connection.Connection( config=cloud_region, - vendor_hook=kwargs.get('vendor_hook')) + vendor_hook=kwargs.get('vendor_hook'), + ) From 400fbfbe01bbaec885b8c55e643662ca89fb5e46 Mon Sep 17 00:00:00 2001 From: Jerry Zhao Date: Thu, 17 Mar 2022 10:40:29 -0700 Subject: [PATCH 3150/3836] support None as expected status in wait_for_status if the status attribute has a None value, such as task_state, wait_for_status will not work as it only compares the string form, so if expected status is None, no need to convert to lower case Change-Id: I9010ef6c0eb67c971b71881bd370c77a04346ab6 --- openstack/resource.py | 4 ++-- openstack/tests/unit/test_resource.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/openstack/resource.py b/openstack/resource.py index 6044de88e..d3db1d08f 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -2294,7 +2294,7 @@ def wait_for_status( """ current_status = getattr(resource, attribute) - if _normalize_status(current_status) == status.lower(): + if _normalize_status(current_status) == _normalize_status(status): return resource if failures is None: @@ -2320,7 +2320,7 @@ def wait_for_status( new_status = getattr(resource, attribute) normalized_status = _normalize_status(new_status) - if normalized_status == status.lower(): + if normalized_status == _normalize_status(status): return resource elif normalized_status in failures: raise exceptions.ResourceFailure( diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 8611b473b..5a97d42dd 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -3155,6 +3155,18 @@ def test_status_match_with_none(self): self.assertEqual(result, resources[-1]) + def test_status_match_none(self): + status = None + + # apparently, None can be expected status in some cases + resources = self._resources_from_statuses( + "first", "other", "another", "another", status) + + result = resource.wait_for_status( + mock.Mock(), resources[0], status, None, 1, 5) + + self.assertEqual(result, resources[-1]) + def test_status_match_different_attribute(self): status = "loling" From 601e5199dc188f48b4693c791b1bd228b127ac8f Mon Sep 17 00:00:00 2001 From: sean mooney Date: Tue, 22 Nov 2022 11:09:58 +0000 Subject: [PATCH 3151/3836] Revert "compute/server: add support of target state for evacuate API" This reverts commit caa4e135e275ce6b453557d701a78701690d57b4. Reason for revert: The spec is still under review and at the PTG we agrered not to add a target state paramater so the sdk change is not in line with the spec direction Change-Id: I34873491d928ebd791150d1eda66f697f56e134f --- openstack/compute/v2/_proxy.py | 7 ++----- openstack/compute/v2/server.py | 5 +---- openstack/tests/unit/compute/v2/test_proxy.py | 8 +++----- openstack/tests/unit/compute/v2/test_server.py | 4 ++-- .../add-target-state-for-evacuate-52be85c5c08f4109.yaml | 3 --- 5 files changed, 8 insertions(+), 19 deletions(-) delete mode 100644 releasenotes/notes/add-target-state-for-evacuate-52be85c5c08f4109.yaml diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index cab935ea9..3f0373d1f 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -944,8 +944,7 @@ def unrescue_server(self, server): server = self._get_resource(_server.Server, server) server.unrescue(self) - def evacuate_server(self, server, host=None, admin_pass=None, force=None, - target_state=None): + def evacuate_server(self, server, host=None, admin_pass=None, force=None): """Evacuates a server from a failed host to a new host. :param server: Either the ID of a server or a @@ -957,13 +956,11 @@ def evacuate_server(self, server, host=None, admin_pass=None, force=None, :param force: Force an evacuation by not verifying the provided destination host by the scheduler. (New in API version 2.29). - :param target_state: Set target state for the evacuated instance (New - in API version 2.94). :returns: None """ server = self._get_resource(_server.Server, server) server.evacuate(self, host=host, admin_pass=admin_pass, - force=force, target_state=target_state) + force=force) def start_server(self, server): """Starts a stopped server and changes its state to ``ACTIVE``. diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index c7ca510db..b8fb906f5 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -459,8 +459,7 @@ def unrescue(self, session): body = {"unrescue": None} self._action(session, body) - def evacuate(self, session, host=None, admin_pass=None, force=None, - target_state=None): + def evacuate(self, session, host=None, admin_pass=None, force=None): body = {"evacuate": {}} if host is not None: body["evacuate"]["host"] = host @@ -468,8 +467,6 @@ def evacuate(self, session, host=None, admin_pass=None, force=None, body["evacuate"]["adminPass"] = admin_pass if force is not None: body["evacuate"]["force"] = force - if target_state is not None: - body["evacuate"]["targetState"] = target_state self._action(session, body) def start(self, session): diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 8255b7108..4f6a4f60c 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -882,18 +882,16 @@ def test_server_evacuate(self): self.proxy.evacuate_server, method_args=["value"], expected_args=[self.proxy], - expected_kwargs={"host": None, "admin_pass": None, "force": None, - "target_state": None}) + expected_kwargs={"host": None, "admin_pass": None, "force": None}) def test_server_evacuate_with_options(self): self._verify( "openstack.compute.v2.server.Server.evacuate", self.proxy.evacuate_server, - method_args=["value", 'HOST2', 'NEW_PASS', True, 'stopped'], + method_args=["value", 'HOST2', 'NEW_PASS', True], expected_args=[self.proxy], expected_kwargs={ - "host": "HOST2", "admin_pass": 'NEW_PASS', "force": True, - "target_state": 'stopped'}) + "host": "HOST2", "admin_pass": 'NEW_PASS', "force": True}) def test_server_start(self): self._verify( diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 174fbb6e4..f677e21d5 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -735,12 +735,12 @@ def test_evacuate_with_options(self): sot = server.Server(**EXAMPLE) res = sot.evacuate(self.sess, host='HOST2', admin_pass='NEW_PASS', - force=True, target_state='stopped') + force=True) self.assertIsNone(res) url = 'servers/IDENTIFIER/action' body = {"evacuate": {'host': 'HOST2', 'adminPass': 'NEW_PASS', - 'force': True, 'targetState': 'stopped'}} + 'force': True}} headers = {'Accept': ''} self.sess.post.assert_called_with( url, json=body, headers=headers, microversion=None) diff --git a/releasenotes/notes/add-target-state-for-evacuate-52be85c5c08f4109.yaml b/releasenotes/notes/add-target-state-for-evacuate-52be85c5c08f4109.yaml deleted file mode 100644 index 4580f378f..000000000 --- a/releasenotes/notes/add-target-state-for-evacuate-52be85c5c08f4109.yaml +++ /dev/null @@ -1,3 +0,0 @@ ---- -features: - - Add support of setting target state for evacuate API. From b1f15919e13cd30ca16063ccc7671820d7d7669b Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 21 Nov 2022 19:00:14 +0000 Subject: [PATCH 3152/3836] docs: Add overview of supported services to README Change-Id: I401f58a65555aa397c14e2795e11d3e76047ba63 Signed-off-by: Stephen Finucane --- README.rst | 157 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/README.rst b/README.rst index 8b4148a72..f46537d06 100644 --- a/README.rst +++ b/README.rst @@ -145,6 +145,163 @@ environment by running ``openstack.config.loader``. For example: More information at https://docs.openstack.org/openstacksdk/latest/user/config/configuration.html +Supported services +------------------ + +The following services are currently supported. A full list of all available +OpenStack service can be found in the `Project Navigator`__. + +.. __: https://www.openstack.org/software/project-navigator/openstack-components#openstack-services + +.. note:: + + Support here does not guarantee full-support for all APIs. It simply means + some aspect of the project is supported. + +.. list-table:: Supported services + :widths: 15 25 10 40 + :header-rows: 1 + + * - Service + - Description + - Cloud Layer + - Proxy & Resource Layer + + * - **Compute** + - + - + - + + * - Nova + - Compute + - ✔ + - ✔ (``openstack.compute``) + + * - **Hardware Lifecycle** + - + - + - + + * - Ironic + - Bare metal provisioning + - ✔ + - ✔ (``openstack.baremetal``, ``openstack.baremetal_introspection``) + + * - Cyborg + - Lifecycle management of accelerators + - ✔ + - ✔ (``openstack.accelerator``) + + * - **Storage** + - + - + - + + * - Cinder + - Block storage + - ✔ + - ✔ (``openstack.block_storage``) + + * - Swift + - Object store + - ✔ + - ✔ (``openstack.object_store``) + + * - Cinder + - Shared filesystems + - ✔ + - ✔ (``openstack.share_file_system``) + + * - **Networking** + - + - + - + + * - Neutron + - Networking + - ✔ + - ✔ (``openstack.network``) + + * - Octavia + - Load balancing + - ✔ + - ✔ (``openstack.load_balancer``) + + * - Designate + - DNS + - ✔ + - ✔ (``openstack.dns``) + + * - **Shared services** + - + - + - + + * - Keystone + - Identity + - ✔ + - ✔ (``openstack.identity``) + + * - Placement + - Placement + - ✔ + - ✔ (``openstack.placement``) + + * - Glance + - Image storage + - ✔ + - ✔ (``openstack.image``) + + * - Barbican + - Key management + - ✔ + - ✔ (``openstack.key_manager``) + + * - **Workload provisioning** + - + - + - + + * - Magnum + - Container orchestration engine provisioning + - ✔ + - ✘ + + * - **Orchestration** + - + - + - + + * - Heat + - Orchestration + - ✔ + - ✔ (``openstack.orchestration``) + + * - Senlin + - Clustering + - ✔ + - ✔ (``openstack.clustering``) + + * - Mistral + - Workflow + - ✔ + - ✔ (``openstack.workflow``) + + * - Zaqar + - Messaging + - ✔ + - ✔ (``openstack.message``) + + * - **Application lifecycle** + - + - + - + + * - Masakari + - Instances high availability service + - ✔ + - ✔ (``openstack.instance_ha``) + Links ----- From 2baee35a5b054fe4ed505f2d8e9f276bbcae472f Mon Sep 17 00:00:00 2001 From: Ian Wienand Date: Wed, 23 Nov 2022 11:43:06 +1100 Subject: [PATCH 3153/3836] compute: don't pass networks: auto for older microversions Change I1a6ba311ddedc1b8910051257299d3acd367df46 made this unconditionally specify "networks: auto" when no network info was specified. From the nova release notes "Starting with microversion 2.37, this field is required and the special string values auto and none can be specified for networks. auto tells the Compute service to use a network that is available to the project, if one exists." So we should restrict where this is specified. This was noticed when trying to use openstacksdk >=0.99 against the Rackspace API. I have run this change against that API and verified it is not passing the argument. Change-Id: Ifc5bd0ddc92bbbc7b250df3f3931518f62cc339f --- openstack/cloud/_compute.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 4501e3abe..58da2feec 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -889,8 +889,10 @@ def create_server( if networks: kwargs['networks'] = networks else: - # If user has not passed networks - let Nova try the best. - kwargs['networks'] = 'auto' + # If user has not passed networks - let Nova try the best; + # note earlier microversions expect this to be blank. + if utils.supports_microversion(self.compute, '2.37'): + kwargs['networks'] = 'auto' if image: if isinstance(image, dict): From 88d8d53d46ad158a732eeccd3ce46bd15a4f882d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 22 Nov 2022 10:52:23 +0000 Subject: [PATCH 3154/3836] coe: Add support for clusters This allows us to start migrating the coe cloud layer to this newly introduced proxy layer. Change-Id: Ia9c622dc284234bb618c2caf3d035097bba58ec5 Signed-off-by: Stephen Finucane --- openstack/_services_mixin.py | 5 +- .../__init__.py | 0 ...ainer_infrastructure_management_service.py | 24 +++ .../v1/__init__.py | 0 .../v1/_proxy.py | 109 ++++++++++++ .../v1/cluster.py | 167 ++++++++++++++++++ .../__init__.py | 0 .../v1/__init__.py | 0 .../v1/test_cluster.py | 56 ++++++ tools/print-services.py | 19 +- 10 files changed, 372 insertions(+), 8 deletions(-) create mode 100644 openstack/container_infrastructure_management/__init__.py create mode 100644 openstack/container_infrastructure_management/container_infrastructure_management_service.py create mode 100644 openstack/container_infrastructure_management/v1/__init__.py create mode 100644 openstack/container_infrastructure_management/v1/_proxy.py create mode 100644 openstack/container_infrastructure_management/v1/cluster.py create mode 100644 openstack/tests/unit/container_infrastructure_management/__init__.py create mode 100644 openstack/tests/unit/container_infrastructure_management/v1/__init__.py create mode 100644 openstack/tests/unit/container_infrastructure_management/v1/test_cluster.py diff --git a/openstack/_services_mixin.py b/openstack/_services_mixin.py index 0e90577b0..13c5358c7 100644 --- a/openstack/_services_mixin.py +++ b/openstack/_services_mixin.py @@ -6,6 +6,7 @@ from openstack.block_storage import block_storage_service from openstack.clustering import clustering_service from openstack.compute import compute_service +from openstack.container_infrastructure_management import container_infrastructure_management_service from openstack.database import database_service from openstack.dns import dns_service from openstack.identity import identity_service @@ -55,9 +56,9 @@ class ServicesMixin: application_catalog = service_description.ServiceDescription(service_type='application-catalog') - container_infrastructure_management = service_description.ServiceDescription(service_type='container-infrastructure-management') - container_infrastructure = container_infrastructure_management + container_infrastructure_management = container_infrastructure_management_service.ContainerInfrastructureManagementService(service_type='container-infrastructure-management') container_infra = container_infrastructure_management + container_infrastructure = container_infrastructure_management search = service_description.ServiceDescription(service_type='search') diff --git a/openstack/container_infrastructure_management/__init__.py b/openstack/container_infrastructure_management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/container_infrastructure_management/container_infrastructure_management_service.py b/openstack/container_infrastructure_management/container_infrastructure_management_service.py new file mode 100644 index 000000000..e71676d08 --- /dev/null +++ b/openstack/container_infrastructure_management/container_infrastructure_management_service.py @@ -0,0 +1,24 @@ +# 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.container_infrastructure_management.v1 import _proxy +from openstack import service_description + + +class ContainerInfrastructureManagementService( + service_description.ServiceDescription, +): + """The container infrastructure management service.""" + + supported_versions = { + '1': _proxy.Proxy, + } diff --git a/openstack/container_infrastructure_management/v1/__init__.py b/openstack/container_infrastructure_management/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/container_infrastructure_management/v1/_proxy.py b/openstack/container_infrastructure_management/v1/_proxy.py new file mode 100644 index 000000000..7d3d2783b --- /dev/null +++ b/openstack/container_infrastructure_management/v1/_proxy.py @@ -0,0 +1,109 @@ +# 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.container_infrastructure_management.v1 import ( + cluster as _cluster +) +from openstack import proxy + + +class Proxy(proxy.Proxy): + + _resource_registry = { + "cluster": _cluster.Cluster, + } + + def create_cluster(self, **attrs): + """Create a new cluster from attributes + + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.container_infrastructure_management.v1.cluster.Cluster`, + comprised of the properties on the Cluster class. + :returns: The results of cluster creation + :rtype: + :class:`~openstack.container_infrastructure_management.v1.cluster.Cluster` + """ + return self._create(_cluster.Cluster, **attrs) + + def delete_cluster(self, cluster, ignore_missing=True): + """Delete a cluster + + :param cluster: The value can be either the ID of a cluster or a + :class:`~openstack.container_infrastructure_management.v1.cluster.Cluster` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the cluster does not exist. When set to ``True``, no exception will + be set when attempting to delete a nonexistent cluster. + :returns: ``None`` + """ + self._delete(_cluster.Cluster, cluster, ignore_missing=ignore_missing) + + def find_cluster(self, name_or_id, ignore_missing=True): + """Find a single cluster + + :param name_or_id: The name or ID of a cluster. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :returns: One + :class:`~openstack.container_infrastructure_management.v1.cluster.Cluster` + or None + """ + return self._find( + _cluster.Cluster, + name_or_id, + ignore_missing=ignore_missing, + ) + + def get_cluster(self, cluster): + """Get a single cluster + + :param cluster: The value can be the ID of a cluster or a + :class:`~openstack.container_infrastructure_management.v1.cluster.Cluster` + instance. + + :returns: One + :class:`~openstack.container_infrastructure_management.v1.cluster.Cluster` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_cluster.Cluster, cluster) + + def clusters(self, **query): + """Return a generator of clusters + + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. + + :returns: A generator of cluster objects + :rtype: + :class:`~openstack.container_infrastructure_management.v1.cluster.Cluster` + """ + return self._list(_cluster.Cluster, **query) + + def update_cluster(self, cluster, **attrs): + """Update a cluster + + :param cluster: Either the id of a cluster or a + :class:`~openstack.container_infrastructure_management.v1.cluster.Cluster` + instance. + :param attrs: The attributes to update on the cluster represented + by ``cluster``. + + :returns: The updated cluster + :rtype: + :class:`~openstack.container_infrastructure_management.v1.cluster.Cluster` + """ + return self._update(_cluster.Cluster, cluster, **attrs) diff --git a/openstack/container_infrastructure_management/v1/cluster.py b/openstack/container_infrastructure_management/v1/cluster.py new file mode 100644 index 000000000..71c880437 --- /dev/null +++ b/openstack/container_infrastructure_management/v1/cluster.py @@ -0,0 +1,167 @@ +# 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 import resource +from openstack import utils + + +class Cluster(resource.Resource): + + resources_key = 'clusters' + base_path = '/clusters' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + allow_patch = True + + #: The endpoint URL of COE API exposed to end-users. + api_address = resource.Body('api_address') + #: The UUID of the cluster template. + cluster_template_id = resource.Body('cluster_template_id') + #: Version info of chosen COE in bay/cluster for helping client in picking + #: the right version of client. + coe_version = resource.Body('coe_version') + #: The timeout for cluster creation in minutes. The value expected is a + #: positive integer. If the timeout is reached during cluster creation + #: process, the operation will be aborted and the cluster status will be + #: set to CREATE_FAILED. Defaults to 60. + create_timeout = resource.Body('create_timeout', type=int) + #: The date and time when the resource was created. The date and time stamp + #: format is ISO 8601:: + #: + #: CCYY-MM-DDThh:mm:ss±hh:mm + #: + #: For example, `2015-08-27T09:49:58-05:00`. The ±hh:mm value, if included, + #: is the time zone as an offset from UTC. + created_at = resource.Body('created_at') + #: The custom discovery url for node discovery. This is used by the COE to + #: discover the servers that have been created to host the containers. The + #: actual discovery mechanism varies with the COE. In some cases, the + #: service fills in the server info in the discovery service. In other + #: cases,if the discovery_url is not specified, the service will use the + #: public discovery service at https://discovery.etcd.io. In this case, the + #: service will generate a unique url here for each bay and store the info + #: for the servers. + discovery_url = resource.Body('discovery_url') + #: The name or ID of the network to provide connectivity to the internal + #: network for the bay/cluster. + fixed_network = resource.Body('fixed_network') + #: The fixed subnet to use when allocating network addresses for nodes in + #: bay/cluster. + fixed_subnet = resource.Body('fixed_subnet') + #: The flavor name or ID to use when booting the node servers. Defaults to + #: m1.small. + flavor_id = resource.Body('flavor_id') + #: Whether to enable using the floating IP of cloud provider. Some cloud + #: providers use floating IPs while some use public IPs. When set to true, + #: floating IPs will be used. If this value is not provided, the value of + #: ``floating_ip_enabled`` provided in the template will be used. + is_floating_ip_enabled = resource.Body('floating_ip_enabled', type=bool) + #: Whether to enable the master load balancer. Since multiple masters may + #: exist in a bay/cluster, a Neutron load balancer is created to provide + #: the API endpoint for the bay/cluster and to direct requests to the + #: masters. In some cases, such as when the LBaaS service is not available, + #: this option can be set to false to create a bay/cluster without the load + #: balancer. In this case, one of the masters will serve as the API + #: endpoint. The default is true, i.e. to create the load balancer for the + #: bay. + is_master_lb_enabled = resource.Body('master_lb_enabled', type=bool) + #: The name of the SSH keypair to configure in the bay/cluster servers for + #: SSH access. Users will need the key to be able to ssh to the servers in + #: the bay/cluster. The login name is specific to the bay/cluster driver. + #: For example, with fedora-atomic image the default login name is fedora. + keypair = resource.Body('keypair') + #: Arbitrary labels in the form of key=value pairs. The accepted keys and + #: valid values are defined in the bay/cluster drivers. They are used as a + #: way to pass additional parameters that are specific to a bay/cluster + #: driver. + labels = resource.Body('labels', type=list) + #: A list of floating IPs of all master nodes. + master_addresses = resource.Body('master_addresses', type=list) + #: The number of servers that will serve as master for the bay/cluster. Set + #: to more than 1 master to enable High Availability. If the option + #: master-lb-enabled is specified in the baymodel/cluster template, the + #: master servers will be placed in a load balancer pool. Defaults to 1. + master_count = resource.Body('master_count', type=int) + #: The flavor of the master node for this baymodel/cluster template. + master_flavor_id = resource.Body('master_flavor_id') + #: Name of the resource. + name = resource.Body('name') + #: The number of servers that will serve as node in the bay/cluster. + #: Defaults to 1. + node_count = resource.Body('node_count', type=int) + #: A list of floating IPs of all servers that serve as nodes. + node_addresses = resource.Body('node_addresses', type=list) + #: The reference UUID of orchestration stack from Heat orchestration + #: service. + stack_id = resource.Body('stack_id') + #: The current state of the bay/cluster. + status = resource.Body('status') + #: The reason of bay/cluster current status. + status_reason = resource.Body('reason') + #: The date and time when the resource was updated. The date and time stamp + #: format is ISO 8601:: + #: + #: CCYY-MM-DDThh:mm:ss±hh:mm + #: + #: For example, `2015-08-27T09:49:58-05:00`. The ±hh:mm value, if included, + #: is the time zone as an offset from UTC. If the updated_at date and time + #: stamp is not set, its value is null. + updated_at = resource.Body('updated_at') + #: The UUID of the cluster. + uuid = resource.Body('uuid', alternate_id=True) + + def resize(self, session, *, node_count, nodes_to_remove=None): + """Resize the cluster. + + :param node_count: The number of servers that will serve as node in the + bay/cluster. The default is 1. + :param nodes_to_remove: The server ID list will be removed if + downsizing the cluster. + :returns: The UUID of the resized cluster. + :raises: :exc:`~openstack.exceptions.ResourceNotFound` if + the resource was not found. + """ + url = utils.urljoin(Cluster.base_path, self.id, 'actions', 'resize') + headers = {'Accept': ''} + body = { + 'node_count': node_count, + 'nodes_to_remove': nodes_to_remove, + } + response = session.post(url, json=body, headers=headers) + exceptions.raise_from_response(response) + return response['uuid'] + + def upgrade(self, session, *, cluster_template, max_batch_size=None): + """Upgrade the cluster. + + :param cluster_template: The UUID of the cluster template. + :param max_batch_size: The max batch size each time when doing upgrade. + The default is 1 + :returns: The UUID of the updated cluster. + :raises: :exc:`~openstack.exceptions.ResourceNotFound` if + the resource was not found. + """ + url = utils.urljoin(Cluster.base_path, self.id, 'actions', 'upgrade') + headers = {'Accept': ''} + body = { + 'cluster_template': cluster_template, + 'max_batch_size': max_batch_size, + } + response = session.post(url, json=body, headers=headers) + exceptions.raise_from_response(response) + return response['uuid'] diff --git a/openstack/tests/unit/container_infrastructure_management/__init__.py b/openstack/tests/unit/container_infrastructure_management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/unit/container_infrastructure_management/v1/__init__.py b/openstack/tests/unit/container_infrastructure_management/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/unit/container_infrastructure_management/v1/test_cluster.py b/openstack/tests/unit/container_infrastructure_management/v1/test_cluster.py new file mode 100644 index 000000000..8e370877b --- /dev/null +++ b/openstack/tests/unit/container_infrastructure_management/v1/test_cluster.py @@ -0,0 +1,56 @@ +# 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.container_infrastructure_management.v1 import cluster +from openstack.tests.unit import base + +EXAMPLE = { + "cluster_template_id": "0562d357-8641-4759-8fed-8173f02c9633", + "create_timeout": 60, + "discovery_url": None, + "flavor_id": None, + "keypair": "my_keypair", + "labels": [], + "master_count": 2, + "master_flavor_id": None, + "name": "k8s", + "node_count": 2, +} + + +class TestCluster(base.TestCase): + def test_basic(self): + sot = cluster.Cluster() + self.assertIsNone(sot.resource_key) + self.assertEqual('clusters', sot.resources_key) + self.assertEqual('/clusters', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = cluster.Cluster(**EXAMPLE) + self.assertEqual( + EXAMPLE['cluster_template_id'], + sot.cluster_template_id, + ) + self.assertEqual(EXAMPLE['create_timeout'], sot.create_timeout) + self.assertEqual(EXAMPLE['discovery_url'], sot.discovery_url) + self.assertEqual(EXAMPLE['flavor_id'], sot.flavor_id) + self.assertEqual(EXAMPLE['keypair'], sot.keypair) + self.assertEqual(EXAMPLE['labels'], sot.labels) + self.assertEqual(EXAMPLE['master_count'], sot.master_count) + self.assertEqual(EXAMPLE['master_flavor_id'], sot.master_flavor_id) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['node_count'], sot.node_count) diff --git a/tools/print-services.py b/tools/print-services.py index 73dd3d09f..f30d29106 100644 --- a/tools/print-services.py +++ b/tools/print-services.py @@ -103,11 +103,17 @@ def _get_aliases(service_type, aliases=None): def _find_service_description_class(service_type): - package_name = 'openstack.{service_type}'.format( - service_type=service_type).replace('-', '_') + package_name = f'openstack.{service_type}'.replace('-', '_') module_name = service_type.replace('-', '_') + '_service' class_name = ''.join( - [part.capitalize() for part in module_name.split('_')]) + [part.capitalize() for part in module_name.split('_')] + ) + + # We have some exceptions :( + # This should have been called 'shared-filesystem' + if service_type == 'shared-file-system': + class_name = 'SharedFilesystemService' + try: import_name = '.'.join([package_name, module_name]) service_description_module = importlib.import_module(import_name) @@ -116,10 +122,11 @@ def _find_service_description_class(service_type): # as an opt-in for people trying to figure out why something # didn't work. warnings.warn( - "Could not import {service_type} service description: {e}".format( - service_type=service_type, e=str(e)), - ImportWarning) + f"Could not import {service_type} service description: {str(e)}", + ImportWarning, + ) return service_description.ServiceDescription + # There are no cases in which we should have a module but not the class # inside it. service_description_class = getattr(service_description_module, class_name) From 4ad5c174c87c9f93f7bbe55f6f04b5b0c7289913 Mon Sep 17 00:00:00 2001 From: Patrik Lundin Date: Fri, 25 Nov 2022 10:32:16 +0100 Subject: [PATCH 3155/3836] cloud: create_server: fix scheduler_hints/group Change I1a6ba311ddedc1b8910051257299d3acd367df46 stopped sending server_json data and instead called self.compute.create_server(**kwargs). Before this change the code would pop() scheduler_hints from kwargs, so it was not present when creating the server. Additionally the "group" code would also only operate on the no longer used server_json. Remove server_json and override/set the scheduler_hints group key directly in kwargs if "group" is supplied. Also add tests for this functionality. Co-Authored-by: stephenfin Change-Id: I7ac2fd85970bf4c6c6e73af8aad11348a290a90a --- openstack/cloud/_compute.py | 10 +- .../tests/unit/cloud/test_create_server.py | 172 ++++++++++++++++++ 2 files changed, 176 insertions(+), 6 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 58da2feec..64eb80949 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -779,8 +779,6 @@ def create_server( raise TypeError( "create_server() requires either 'image' or 'boot_volume'") - server_json = {'server': kwargs} - # TODO(mordred) Add support for description starting in 2.19 security_groups = kwargs.get('security_groups', []) if security_groups and not isinstance(kwargs['security_groups'], list): @@ -803,16 +801,16 @@ def create_server( if value: kwargs[desired] = value - hints = kwargs.pop('scheduler_hints', {}) if group: group_obj = self.get_server_group(group) if not group_obj: raise exc.OpenStackCloudException( "Server Group {group} was requested but was not found" " on the cloud".format(group=group)) - hints['group'] = group_obj['id'] - if hints: - server_json['os:scheduler_hints'] = hints + if 'scheduler_hints' not in kwargs: + kwargs['scheduler_hints'] = {} + kwargs['scheduler_hints']['group'] = group_obj['id'] + kwargs.setdefault('max_count', kwargs.get('max_count', 1)) kwargs.setdefault('min_count', kwargs.get('min_count', 1)) diff --git a/openstack/tests/unit/cloud/test_create_server.py b/openstack/tests/unit/cloud/test_create_server.py index 48ccd9968..d955bea34 100644 --- a/openstack/tests/unit/cloud/test_create_server.py +++ b/openstack/tests/unit/cloud/test_create_server.py @@ -1012,3 +1012,175 @@ def test_create_boot_from_volume_image_terminate(self): wait=False) self.assert_calls() + + def test_create_server_scheduler_hints(self): + """ + Test that setting scheduler_hints will include them in POST request + """ + scheduler_hints = { + 'group': self.getUniqueString('group'), + } + fake_server = fakes.make_fake_server('1234', '', 'BUILD') + fake_server['scheduler_hints'] = scheduler_hints + + self.register_uris([ + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks']), + json={'networks': []}), + self.get_nova_discovery_mock_dict(), + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers']), + json={'server': fake_server}, + validate=dict( + json={ + 'server': { + u'flavorRef': u'flavor-id', + u'imageRef': u'image-id', + u'max_count': 1, + u'min_count': 1, + u'name': u'server-name', + 'networks': 'auto'}, + u'OS-SCH-HNT:scheduler_hints': scheduler_hints, })), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234']), + json={'server': fake_server}), + ]) + + self.cloud.create_server( + name='server-name', image=dict(id='image-id'), + flavor=dict(id='flavor-id'), + scheduler_hints=scheduler_hints, wait=False) + + self.assert_calls() + + def test_create_server_scheduler_hints_group_merge(self): + """ + Test that setting both scheduler_hints and group results in merged + hints in POST request + """ + group_id = uuid.uuid4().hex + group_name = self.getUniqueString('server-group') + policies = ['affinity'] + fake_group = fakes.make_fake_server_group( + group_id, group_name, policies) + + # The scheduler hints we pass in + scheduler_hints = { + 'different_host': [], + } + + # The scheduler hints we expect to be in POST request + scheduler_hints_merged = { + 'different_host': [], + 'group': group_id, + } + + fake_server = fakes.make_fake_server('1234', '', 'BUILD') + fake_server['scheduler_hints'] = scheduler_hints_merged + + self.register_uris([ + self.get_nova_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-server-groups']), + json={'server_groups': [fake_group]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks']), + json={'networks': []}), + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers']), + json={'server': fake_server}, + validate=dict( + json={ + 'server': { + u'flavorRef': u'flavor-id', + u'imageRef': u'image-id', + u'max_count': 1, + u'min_count': 1, + u'name': u'server-name', + 'networks': 'auto'}, + u'OS-SCH-HNT:scheduler_hints': scheduler_hints_merged, + })), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234']), + json={'server': fake_server}), + ]) + + self.cloud.create_server( + name='server-name', image=dict(id='image-id'), + flavor=dict(id='flavor-id'), + scheduler_hints=dict(scheduler_hints), group=group_name, + wait=False) + + self.assert_calls() + + def test_create_server_scheduler_hints_group_override(self): + """ + Test that setting group in both scheduler_hints and group param prefers + param + """ + group_id_scheduler_hints = uuid.uuid4().hex + group_id_param = uuid.uuid4().hex + group_name = self.getUniqueString('server-group') + policies = ['affinity'] + fake_group = fakes.make_fake_server_group( + group_id_param, group_name, policies) + + # The scheduler hints we pass in that are expected to be ignored in + # POST call + scheduler_hints = { + 'group': group_id_scheduler_hints, + } + + # The scheduler hints we expect to be in POST request + group_scheduler_hints = { + 'group': group_id_param, + } + + fake_server = fakes.make_fake_server('1234', '', 'BUILD') + fake_server['scheduler_hints'] = group_scheduler_hints + + self.register_uris([ + self.get_nova_discovery_mock_dict(), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-server-groups']), + json={'server_groups': [fake_group]}), + dict(method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks']), + json={'networks': []}), + dict(method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers']), + json={'server': fake_server}, + validate=dict( + json={ + 'server': { + u'flavorRef': u'flavor-id', + u'imageRef': u'image-id', + u'max_count': 1, + u'min_count': 1, + u'name': u'server-name', + 'networks': 'auto'}, + u'OS-SCH-HNT:scheduler_hints': group_scheduler_hints, + })), + dict(method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234']), + json={'server': fake_server}), + ]) + + self.cloud.create_server( + name='server-name', image=dict(id='image-id'), + flavor=dict(id='flavor-id'), + scheduler_hints=dict(scheduler_hints), group=group_name, + wait=False) + + self.assert_calls() From 163b6944105804c6c757b2ccf66a999e74ef98bd Mon Sep 17 00:00:00 2001 From: Johannes Kulik Date: Wed, 30 Nov 2022 14:21:31 +0000 Subject: [PATCH 3156/3836] Revert "Revert "Add "security_group_ids" to Port's query parameters"" This reverts commit 5e969fb49d96fd2071ecc8a379ec6e2c3c5c90bc. Reason for revert: The revert happened to resolve a merge conflict and was supposed to be re-reverted afterwards. This is the re-revert. Original Commit message for original change Ic557dc3bf97a193fd28c13792302fb8396e29df1: Neutron's port-security-groups-filtering extension allows to filter ports by security-group(s). This feature was added with [1]. We keep the name like in the attribute "security_group_ids" instead of "security_groups" to stay consistent within the Port model. [1] https://launchpad.net/neutron/+bug/1405057 Change-Id: I060edb9c0d2eeb03d514707b69978f3d5f0ae3f4 --- openstack/network/v2/port.py | 1 + openstack/tests/unit/network/v2/test_port.py | 1 + 2 files changed, 2 insertions(+) diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index cdcec8b4b..a977275c2 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -35,6 +35,7 @@ class Port(_base.NetworkResource, tag.TagMixin): 'subnet_id', 'project_id', is_admin_state_up='admin_state_up', is_port_security_enabled='port_security_enabled', + security_group_ids='security_groups', **tag.TagMixin._tag_query_parameters ) diff --git a/openstack/tests/unit/network/v2/test_port.py b/openstack/tests/unit/network/v2/test_port.py index 861384dca..7e418e839 100644 --- a/openstack/tests/unit/network/v2/test_port.py +++ b/openstack/tests/unit/network/v2/test_port.py @@ -99,6 +99,7 @@ def test_basic(self): "is_port_security_enabled": "port_security_enabled", "project_id": "project_id", + "security_group_ids": "security_groups", "limit": "limit", "marker": "marker", "any_tags": "tags-any", From 3159933778c55e41a8e0bca76781d79057294e40 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 23 Sep 2022 17:52:16 +0200 Subject: [PATCH 3157/3836] Rework network functional tests Fix skip conditions of the functional tests in the network area and switch to use of the proper cloud. This is required to allow executing functional tests on real OpenStack clouds differentiating regular user vs administrator accounts. Change-Id: I792dd52136c31fd95cd98be903f12a9273fa6c76 --- include-acceptance-regular-user.txt | 1 + openstack/tests/functional/base.py | 2 + .../network/v2/test_address_group.py | 47 +++--- .../network/v2/test_address_scope.py | 18 +- .../tests/functional/network/v2/test_agent.py | 18 +- .../v2/test_agent_add_remove_network.py | 34 ++-- .../v2/test_agent_add_remove_router.py | 28 ++-- .../v2/test_auto_allocated_topology.py | 44 +++-- .../network/v2/test_availability_zone.py | 13 +- .../functional/network/v2/test_dvr_router.py | 24 ++- .../functional/network/v2/test_extension.py | 7 +- .../network/v2/test_firewall_group.py | 17 +- .../network/v2/test_firewall_policy.py | 17 +- .../network/v2/test_firewall_rule.py | 42 +++-- ...test_firewall_rule_insert_remove_policy.py | 75 +++++---- .../functional/network/v2/test_flavor.py | 90 ++++++---- .../functional/network/v2/test_floating_ip.py | 154 +++++++++++------- .../network/v2/test_l3_conntrack_helper.py | 34 ++-- .../functional/network/v2/test_local_ip.py | 27 +-- .../network/v2/test_local_ip_association.py | 51 +++--- .../functional/network/v2/test_ndp_proxy.py | 113 +++++++------ .../functional/network/v2/test_network.py | 51 +++--- .../v2/test_network_ip_availability.py | 43 +++-- .../network/v2/test_network_segment_range.py | 85 ++++++---- .../tests/functional/network/v2/test_port.py | 51 +++--- .../network/v2/test_port_forwarding.py | 136 +++++++++------- .../v2/test_qos_bandwidth_limit_rule.py | 66 +++++--- .../network/v2/test_qos_dscp_marking_rule.py | 41 +++-- .../v2/test_qos_minimum_bandwidth_rule.py | 66 +++++--- .../v2/test_qos_minimum_packet_rate_rule.py | 66 +++++--- .../functional/network/v2/test_qos_policy.py | 35 ++-- .../network/v2/test_qos_rule_type.py | 15 +- .../tests/functional/network/v2/test_quota.py | 25 ++- .../functional/network/v2/test_rbac_policy.py | 76 ++++++--- .../functional/network/v2/test_router.py | 36 ++-- .../v2/test_router_add_remove_interface.py | 38 +++-- .../network/v2/test_security_group.py | 29 ++-- .../network/v2/test_security_group_rule.py | 36 ++-- .../functional/network/v2/test_segment.py | 48 +++--- .../network/v2/test_service_profile.py | 79 ++++++--- .../network/v2/test_service_provider.py | 7 +- .../functional/network/v2/test_subnet.py | 37 +++-- .../v2/test_subnet_from_subnet_pool.py | 25 +-- .../functional/network/v2/test_subnet_pool.py | 42 +++-- .../tests/functional/network/v2/test_trunk.py | 77 +++++---- .../functional/network/v2/test_vpnaas.py | 41 +++-- 46 files changed, 1257 insertions(+), 850 deletions(-) diff --git a/include-acceptance-regular-user.txt b/include-acceptance-regular-user.txt index 44563523a..5c5a76019 100644 --- a/include-acceptance-regular-user.txt +++ b/include-acceptance-regular-user.txt @@ -1,6 +1,7 @@ # This file contains list of tests that can work with regular user privileges # Until all tests are modified to properly identify whether they are able to # run or must skip the ones that are known to work are listed here. +openstack.tests.functional.network openstack.tests.functional.block_storage.v3.test_volume # Do not enable test_backup for now, since it is not capable to determine # backup capabilities of the cloud diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index 7e965eb71..ff5d6b10c 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -61,6 +61,8 @@ def setUp(self): self._set_user_cloud() if self._op_name: self._set_operator_cloud() + else: + self.operator_cloud = None self.identity_version = \ self.user_cloud.config.get_api_version('identity') diff --git a/openstack/tests/functional/network/v2/test_address_group.py b/openstack/tests/functional/network/v2/test_address_group.py index aa9b5e969..66ac5885a 100644 --- a/openstack/tests/functional/network/v2/test_address_group.py +++ b/openstack/tests/functional/network/v2/test_address_group.py @@ -18,64 +18,71 @@ class TestAddressGroup(base.BaseFunctionalTest): ADDRESS_GROUP_ID = None - ADDRESSES = ['10.0.0.1/32', '2001:db8::/32'] + ADDRESSES = ["10.0.0.1/32", "2001:db8::/32"] def setUp(self): super(TestAddressGroup, self).setUp() # Skip the tests if address group extension is not enabled. - if not self.conn.network.find_extension('address-group'): - self.skipTest('Network Address Group extension disabled') + if not self.user_cloud.network.find_extension("address-group"): + self.skipTest("Network Address Group extension disabled") self.ADDRESS_GROUP_NAME = self.getUniqueString() self.ADDRESS_GROUP_DESCRIPTION = self.getUniqueString() self.ADDRESS_GROUP_NAME_UPDATED = self.getUniqueString() self.ADDRESS_GROUP_DESCRIPTION_UPDATED = self.getUniqueString() - address_group = self.conn.network.create_address_group( + address_group = self.user_cloud.network.create_address_group( name=self.ADDRESS_GROUP_NAME, description=self.ADDRESS_GROUP_DESCRIPTION, - addresses=self.ADDRESSES + addresses=self.ADDRESSES, ) assert isinstance(address_group, _address_group.AddressGroup) self.assertEqual(self.ADDRESS_GROUP_NAME, address_group.name) - self.assertEqual(self.ADDRESS_GROUP_DESCRIPTION, - address_group.description) + self.assertEqual( + self.ADDRESS_GROUP_DESCRIPTION, address_group.description + ) self.assertCountEqual(self.ADDRESSES, address_group.addresses) self.ADDRESS_GROUP_ID = address_group.id def tearDown(self): - sot = self.conn.network.delete_address_group(self.ADDRESS_GROUP_ID) + sot = self.user_cloud.network.delete_address_group( + self.ADDRESS_GROUP_ID) self.assertIsNone(sot) super(TestAddressGroup, self).tearDown() def test_find(self): - sot = self.conn.network.find_address_group(self.ADDRESS_GROUP_NAME) + sot = self.user_cloud.network.find_address_group( + self.ADDRESS_GROUP_NAME) self.assertEqual(self.ADDRESS_GROUP_ID, sot.id) def test_get(self): - sot = self.conn.network.get_address_group(self.ADDRESS_GROUP_ID) + sot = self.user_cloud.network.get_address_group(self.ADDRESS_GROUP_ID) self.assertEqual(self.ADDRESS_GROUP_NAME, sot.name) def test_list(self): - names = [ag.name for ag in self.conn.network.address_groups()] + names = [ag.name for ag in self.user_cloud.network.address_groups()] self.assertIn(self.ADDRESS_GROUP_NAME, names) def test_update(self): - sot = self.conn.network.update_address_group( + sot = self.user_cloud.network.update_address_group( self.ADDRESS_GROUP_ID, name=self.ADDRESS_GROUP_NAME_UPDATED, - description=self.ADDRESS_GROUP_DESCRIPTION_UPDATED) + description=self.ADDRESS_GROUP_DESCRIPTION_UPDATED, + ) self.assertEqual(self.ADDRESS_GROUP_NAME_UPDATED, sot.name) - self.assertEqual(self.ADDRESS_GROUP_DESCRIPTION_UPDATED, - sot.description) + self.assertEqual( + self.ADDRESS_GROUP_DESCRIPTION_UPDATED, sot.description + ) def test_add_remove_addresses(self): - addrs = ['127.0.0.1/32', 'fe80::/10'] - sot = self.conn.network.add_addresses_to_address_group( - self.ADDRESS_GROUP_ID, addrs) + addrs = ["127.0.0.1/32", "fe80::/10"] + sot = self.user_cloud.network.add_addresses_to_address_group( + self.ADDRESS_GROUP_ID, addrs + ) updated_addrs = self.ADDRESSES.copy() updated_addrs.extend(addrs) self.assertCountEqual(updated_addrs, sot.addresses) - sot = self.conn.network.remove_addresses_from_address_group( - self.ADDRESS_GROUP_ID, addrs) + sot = self.user_cloud.network.remove_addresses_from_address_group( + self.ADDRESS_GROUP_ID, addrs + ) self.assertCountEqual(self.ADDRESSES, sot.addresses) diff --git a/openstack/tests/functional/network/v2/test_address_scope.py b/openstack/tests/functional/network/v2/test_address_scope.py index c67a5caec..9b5193be7 100644 --- a/openstack/tests/functional/network/v2/test_address_scope.py +++ b/openstack/tests/functional/network/v2/test_address_scope.py @@ -25,7 +25,7 @@ def setUp(self): super(TestAddressScope, self).setUp() self.ADDRESS_SCOPE_NAME = self.getUniqueString() self.ADDRESS_SCOPE_NAME_UPDATED = self.getUniqueString() - address_scope = self.conn.network.create_address_scope( + address_scope = self.user_cloud.network.create_address_scope( ip_version=self.IP_VERSION, name=self.ADDRESS_SCOPE_NAME, shared=self.IS_SHARED, @@ -35,26 +35,28 @@ def setUp(self): self.ADDRESS_SCOPE_ID = address_scope.id def tearDown(self): - sot = self.conn.network.delete_address_scope(self.ADDRESS_SCOPE_ID) + sot = self.user_cloud.network.delete_address_scope( + self.ADDRESS_SCOPE_ID) self.assertIsNone(sot) super(TestAddressScope, self).tearDown() def test_find(self): - sot = self.conn.network.find_address_scope(self.ADDRESS_SCOPE_NAME) + sot = self.user_cloud.network.find_address_scope( + self.ADDRESS_SCOPE_NAME) self.assertEqual(self.ADDRESS_SCOPE_ID, sot.id) def test_get(self): - sot = self.conn.network.get_address_scope(self.ADDRESS_SCOPE_ID) + sot = self.user_cloud.network.get_address_scope(self.ADDRESS_SCOPE_ID) self.assertEqual(self.ADDRESS_SCOPE_NAME, sot.name) self.assertEqual(self.IS_SHARED, sot.is_shared) self.assertEqual(self.IP_VERSION, sot.ip_version) def test_list(self): - names = [o.name for o in self.conn.network.address_scopes()] + names = [o.name for o in self.user_cloud.network.address_scopes()] self.assertIn(self.ADDRESS_SCOPE_NAME, names) def test_update(self): - sot = self.conn.network.update_address_scope( - self.ADDRESS_SCOPE_ID, - name=self.ADDRESS_SCOPE_NAME_UPDATED) + sot = self.user_cloud.network.update_address_scope( + self.ADDRESS_SCOPE_ID, name=self.ADDRESS_SCOPE_NAME_UPDATED + ) self.assertEqual(self.ADDRESS_SCOPE_NAME_UPDATED, sot.name) diff --git a/openstack/tests/functional/network/v2/test_agent.py b/openstack/tests/functional/network/v2/test_agent.py index 7e5c35bfa..3ef3c89ad 100644 --- a/openstack/tests/functional/network/v2/test_agent.py +++ b/openstack/tests/functional/network/v2/test_agent.py @@ -19,7 +19,7 @@ class TestAgent(base.BaseFunctionalTest): AGENT = None - DESC = 'test description' + DESC = "test description" def validate_uuid(self, s): try: @@ -30,21 +30,27 @@ def validate_uuid(self, s): def setUp(self): super(TestAgent, self).setUp() - agent_list = list(self.conn.network.agents()) + if not self.user_cloud._has_neutron_extension("agent"): + self.skipTest("Neutron agent extension is required for this test") + + agent_list = list(self.user_cloud.network.agents()) + if len(agent_list) == 0: + self.skipTest("No agents available") self.AGENT = agent_list[0] assert isinstance(self.AGENT, agent.Agent) def test_list(self): - agent_list = list(self.conn.network.agents()) + agent_list = list(self.user_cloud.network.agents()) self.AGENT = agent_list[0] assert isinstance(self.AGENT, agent.Agent) self.assertTrue(self.validate_uuid(self.AGENT.id)) def test_get(self): - sot = self.conn.network.get_agent(self.AGENT.id) + sot = self.user_cloud.network.get_agent(self.AGENT.id) self.assertEqual(self.AGENT.id, sot.id) def test_update(self): - sot = self.conn.network.update_agent(self.AGENT.id, - description=self.DESC) + sot = self.user_cloud.network.update_agent( + self.AGENT.id, description=self.DESC + ) self.assertEqual(self.DESC, sot.description) diff --git a/openstack/tests/functional/network/v2/test_agent_add_remove_network.py b/openstack/tests/functional/network/v2/test_agent_add_remove_network.py index 6cd7837fa..10cdbcd84 100644 --- a/openstack/tests/functional/network/v2/test_agent_add_remove_network.py +++ b/openstack/tests/functional/network/v2/test_agent_add_remove_network.py @@ -23,33 +23,43 @@ class TestAgentNetworks(base.BaseFunctionalTest): def setUp(self): super(TestAgentNetworks, self).setUp() + if not self.user_cloud._has_neutron_extension("agent"): + self.skipTest("Neutron agent extension is required for this test") - self.NETWORK_NAME = self.getUniqueString('network') - net = self.conn.network.create_network(name=self.NETWORK_NAME) - self.addCleanup(self.conn.network.delete_network, net.id) + self.NETWORK_NAME = self.getUniqueString("network") + net = self.user_cloud.network.create_network(name=self.NETWORK_NAME) + self.addCleanup(self.user_cloud.network.delete_network, net.id) assert isinstance(net, network.Network) self.NETWORK_ID = net.id - agent_list = list(self.conn.network.agents()) - agents = [agent for agent in agent_list - if agent.agent_type == 'DHCP agent'] + agent_list = list(self.user_cloud.network.agents()) + agents = [ + agent for agent in agent_list if agent.agent_type == "DHCP agent" + ] + if len(agent_list) == 0: + self.skipTest("No agents available") + self.AGENT = agents[0] self.AGENT_ID = self.AGENT.id def test_add_remove_agent(self): - net = self.AGENT.add_agent_to_network(self.conn.network, - network_id=self.NETWORK_ID) + net = self.AGENT.add_agent_to_network( + self.user_cloud.network, network_id=self.NETWORK_ID + ) self._verify_add(net) - net = self.AGENT.remove_agent_from_network(self.conn.network, - network_id=self.NETWORK_ID) + net = self.AGENT.remove_agent_from_network( + self.user_cloud.network, network_id=self.NETWORK_ID + ) self._verify_remove(net) def _verify_add(self, network): - net = self.conn.network.dhcp_agent_hosting_networks(self.AGENT_ID) + net = self.user_cloud.network.dhcp_agent_hosting_networks( + self.AGENT_ID) net_ids = [n.id for n in net] self.assertIn(self.NETWORK_ID, net_ids) def _verify_remove(self, network): - net = self.conn.network.dhcp_agent_hosting_networks(self.AGENT_ID) + net = self.user_cloud.network.dhcp_agent_hosting_networks( + self.AGENT_ID) net_ids = [n.id for n in net] self.assertNotIn(self.NETWORK_ID, net_ids) diff --git a/openstack/tests/functional/network/v2/test_agent_add_remove_router.py b/openstack/tests/functional/network/v2/test_agent_add_remove_router.py index f22eab354..0b1ddca37 100644 --- a/openstack/tests/functional/network/v2/test_agent_add_remove_router.py +++ b/openstack/tests/functional/network/v2/test_agent_add_remove_router.py @@ -22,24 +22,32 @@ class TestAgentRouters(base.BaseFunctionalTest): def setUp(self): super(TestAgentRouters, self).setUp() + if not self.user_cloud._has_neutron_extension("agent"): + self.skipTest("Neutron agent extension is required for this test") - self.ROUTER_NAME = 'router-name-' + self.getUniqueString('router-name') - self.ROUTER = self.conn.network.create_router(name=self.ROUTER_NAME) - self.addCleanup(self.conn.network.delete_router, self.ROUTER) + self.ROUTER_NAME = "router-name-" + self.getUniqueString("router-name") + self.ROUTER = self.user_cloud.network.create_router( + name=self.ROUTER_NAME) + self.addCleanup(self.user_cloud.network.delete_router, self.ROUTER) assert isinstance(self.ROUTER, router.Router) - agent_list = list(self.conn.network.agents()) - agents = [agent for agent in agent_list - if agent.agent_type == 'L3 agent'] + agent_list = list(self.user_cloud.network.agents()) + agents = [ + agent for agent in agent_list if agent.agent_type == "L3 agent" + ] + if len(agent_list) == 0: + self.skipTest("No agents available") + self.AGENT = agents[0] def test_add_router_to_agent(self): - self.conn.network.add_router_to_agent(self.AGENT, self.ROUTER) - rots = self.conn.network.agent_hosted_routers(self.AGENT) + self.user_cloud.network.add_router_to_agent(self.AGENT, self.ROUTER) + rots = self.user_cloud.network.agent_hosted_routers(self.AGENT) routers = [router.id for router in rots] self.assertIn(self.ROUTER.id, routers) def test_remove_router_from_agent(self): - self.conn.network.remove_router_from_agent(self.AGENT, self.ROUTER) - rots = self.conn.network.agent_hosted_routers(self.AGENT) + self.user_cloud.network.remove_router_from_agent( + self.AGENT, self.ROUTER) + rots = self.user_cloud.network.agent_hosted_routers(self.AGENT) routers = [router.id for router in rots] self.assertNotIn(self.ROUTER.id, routers) diff --git a/openstack/tests/functional/network/v2/test_auto_allocated_topology.py b/openstack/tests/functional/network/v2/test_auto_allocated_topology.py index a2850e2c4..ab961e8d6 100644 --- a/openstack/tests/functional/network/v2/test_auto_allocated_topology.py +++ b/openstack/tests/functional/network/v2/test_auto_allocated_topology.py @@ -15,46 +15,62 @@ class TestAutoAllocatedTopology(base.BaseFunctionalTest): - NETWORK_NAME = 'auto_allocated_network' + NETWORK_NAME = "auto_allocated_network" NETWORK_ID = None PROJECT_ID = None def setUp(self): super(TestAutoAllocatedTopology, self).setUp() - projects = [o.project_id for o in self.conn.network.networks()] + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") + if not self.operator_cloud._has_neutron_extension( + "auto-allocated-topology" + ): + self.skipTest( + "Neutron auto-allocated-topology extension is " + "required for this test" + ) + + projects = [ + o.project_id + for o in self.operator_cloud.network.networks()] self.PROJECT_ID = projects[0] def tearDown(self): - res = self.conn.network.delete_auto_allocated_topology(self.PROJECT_ID) + res = self.operator_cloud.network.delete_auto_allocated_topology( + self.PROJECT_ID) self.assertIsNone(res) super(TestAutoAllocatedTopology, self).tearDown() def test_dry_run_option_pass(self): # Dry run will only pass if there is a public network - networks = self.conn.network.networks() + networks = self.operator_cloud.network.networks() self._set_network_external(networks) # Dry run option will return "dry-run=pass" in the 'id' resource - top = self.conn.network.validate_auto_allocated_topology( - self.PROJECT_ID) + top = self.operator_cloud.network.validate_auto_allocated_topology( + self.PROJECT_ID + ) self.assertEqual(self.PROJECT_ID, top.project) - self.assertEqual('dry-run=pass', top.id) + self.assertEqual("dry-run=pass", top.id) def test_show_no_project_option(self): - top = self.conn.network.get_auto_allocated_topology() + top = self.operator_cloud.network.get_auto_allocated_topology() project = self.conn.session.get_project_id() - network = self.conn.network.get_network(top.id) + network = self.operator_cloud.network.get_network(top.id) self.assertEqual(top.project_id, project) self.assertEqual(top.id, network.id) def test_show_project_option(self): - top = self.conn.network.get_auto_allocated_topology(self.PROJECT_ID) - network = self.conn.network.get_network(top.id) + top = self.operator_cloud.network.get_auto_allocated_topology( + self.PROJECT_ID) + network = self.operator_cloud.network.get_network(top.id) self.assertEqual(top.project_id, network.project_id) self.assertEqual(top.id, network.id) - self.assertEqual(network.name, 'auto_allocated_network') + self.assertEqual(network.name, "auto_allocated_network") def _set_network_external(self, networks): for network in networks: - if network.name == 'public': - self.conn.network.update_network(network, is_default=True) + if network.name == "public": + self.operator_cloud.network.update_network( + network, is_default=True) diff --git a/openstack/tests/functional/network/v2/test_availability_zone.py b/openstack/tests/functional/network/v2/test_availability_zone.py index 4c4cc1438..255b38990 100644 --- a/openstack/tests/functional/network/v2/test_availability_zone.py +++ b/openstack/tests/functional/network/v2/test_availability_zone.py @@ -15,12 +15,11 @@ class TestAvailabilityZone(base.BaseFunctionalTest): - def test_list(self): - availability_zones = list(self.conn.network.availability_zones()) - self.assertGreater(len(availability_zones), 0) + availability_zones = list(self.user_cloud.network.availability_zones()) + if len(availability_zones) > 0: - for az in availability_zones: - self.assertIsInstance(az.name, str) - self.assertIsInstance(az.resource, str) - self.assertIsInstance(az.state, str) + for az in availability_zones: + self.assertIsInstance(az.name, str) + self.assertIsInstance(az.resource, str) + self.assertIsInstance(az.state, str) diff --git a/openstack/tests/functional/network/v2/test_dvr_router.py b/openstack/tests/functional/network/v2/test_dvr_router.py index 20b479c50..1c4bf1b37 100644 --- a/openstack/tests/functional/network/v2/test_dvr_router.py +++ b/openstack/tests/functional/network/v2/test_dvr_router.py @@ -21,34 +21,44 @@ class TestDVRRouter(base.BaseFunctionalTest): def setUp(self): super(TestDVRRouter, self).setUp() + if not self.operator_cloud: + # Current policies forbid regular user use it + self.skipTest("Operator cloud is required for this test") + + if not self.operator_cloud._has_neutron_extension("dvr"): + self.skipTest("dvr service not supported by cloud") + self.NAME = self.getUniqueString() self.UPDATE_NAME = self.getUniqueString() - sot = self.conn.network.create_router(name=self.NAME, distributed=True) + sot = self.operator_cloud.network.create_router( + name=self.NAME, distributed=True) assert isinstance(sot, router.Router) self.assertEqual(self.NAME, sot.name) self.ID = sot.id def tearDown(self): - sot = self.conn.network.delete_router(self.ID, ignore_missing=False) + sot = self.operator_cloud.network.delete_router( + self.ID, ignore_missing=False) self.assertIsNone(sot) super(TestDVRRouter, self).tearDown() def test_find(self): - sot = self.conn.network.find_router(self.NAME) + sot = self.operator_cloud.network.find_router(self.NAME) self.assertEqual(self.ID, sot.id) def test_get(self): - sot = self.conn.network.get_router(self.ID) + sot = self.operator_cloud.network.get_router(self.ID) self.assertEqual(self.NAME, sot.name) self.assertEqual(self.ID, sot.id) self.assertTrue(sot.is_distributed) def test_list(self): - names = [o.name for o in self.conn.network.routers()] + names = [o.name for o in self.operator_cloud.network.routers()] self.assertIn(self.NAME, names) - dvr = [o.is_distributed for o in self.conn.network.routers()] + dvr = [o.is_distributed for o in self.operator_cloud.network.routers()] self.assertTrue(dvr) def test_update(self): - sot = self.conn.network.update_router(self.ID, name=self.UPDATE_NAME) + sot = self.operator_cloud.network.update_router( + self.ID, name=self.UPDATE_NAME) self.assertEqual(self.UPDATE_NAME, sot.name) diff --git a/openstack/tests/functional/network/v2/test_extension.py b/openstack/tests/functional/network/v2/test_extension.py index 449f0d4e1..16bbcf43d 100644 --- a/openstack/tests/functional/network/v2/test_extension.py +++ b/openstack/tests/functional/network/v2/test_extension.py @@ -15,9 +15,8 @@ class TestExtension(base.BaseFunctionalTest): - def test_list(self): - extensions = list(self.conn.network.extensions()) + extensions = list(self.user_cloud.network.extensions()) self.assertGreater(len(extensions), 0) for ext in extensions: @@ -25,5 +24,5 @@ def test_list(self): self.assertIsInstance(ext.alias, str) def test_find(self): - extension = self.conn.network.find_extension('external-net') - self.assertEqual('Neutron external network', extension.name) + extension = self.user_cloud.network.find_extension("external-net") + self.assertEqual("Neutron external network", extension.name) diff --git a/openstack/tests/functional/network/v2/test_firewall_group.py b/openstack/tests/functional/network/v2/test_firewall_group.py index 7e19f82ab..17daa0e71 100644 --- a/openstack/tests/functional/network/v2/test_firewall_group.py +++ b/openstack/tests/functional/network/v2/test_firewall_group.py @@ -24,29 +24,30 @@ class TestFirewallGroup(base.BaseFunctionalTest): def setUp(self): super(TestFirewallGroup, self).setUp() - if not self.conn._has_neutron_extension('fwaas_v2'): - self.skipTest('fwaas_v2 service not supported by cloud') + if not self.user_cloud._has_neutron_extension("fwaas_v2"): + self.skipTest("fwaas_v2 service not supported by cloud") self.NAME = self.getUniqueString() - sot = self.conn.network.create_firewall_group(name=self.NAME) + sot = self.user_cloud.network.create_firewall_group(name=self.NAME) assert isinstance(sot, firewall_group.FirewallGroup) self.assertEqual(self.NAME, sot.name) self.ID = sot.id def tearDown(self): - sot = self.conn.network.delete_firewall_group(self.ID, - ignore_missing=False) + sot = self.user_cloud.network.delete_firewall_group( + self.ID, ignore_missing=False + ) self.assertIs(None, sot) super(TestFirewallGroup, self).tearDown() def test_find(self): - sot = self.conn.network.find_firewall_group(self.NAME) + sot = self.user_cloud.network.find_firewall_group(self.NAME) self.assertEqual(self.ID, sot.id) def test_get(self): - sot = self.conn.network.get_firewall_group(self.ID) + sot = self.user_cloud.network.get_firewall_group(self.ID) self.assertEqual(self.NAME, sot.name) self.assertEqual(self.ID, sot.id) def test_list(self): - names = [o.name for o in self.conn.network.firewall_groups()] + names = [o.name for o in self.user_cloud.network.firewall_groups()] self.assertIn(self.NAME, names) diff --git a/openstack/tests/functional/network/v2/test_firewall_policy.py b/openstack/tests/functional/network/v2/test_firewall_policy.py index e0409cd8e..6bb134db2 100644 --- a/openstack/tests/functional/network/v2/test_firewall_policy.py +++ b/openstack/tests/functional/network/v2/test_firewall_policy.py @@ -24,29 +24,30 @@ class TestFirewallPolicy(base.BaseFunctionalTest): def setUp(self): super(TestFirewallPolicy, self).setUp() - if not self.conn._has_neutron_extension('fwaas_v2'): - self.skipTest('fwaas_v2 service not supported by cloud') + if not self.user_cloud._has_neutron_extension("fwaas_v2"): + self.skipTest("fwaas_v2 service not supported by cloud") self.NAME = self.getUniqueString() - sot = self.conn.network.create_firewall_policy(name=self.NAME) + sot = self.user_cloud.network.create_firewall_policy(name=self.NAME) assert isinstance(sot, firewall_policy.FirewallPolicy) self.assertEqual(self.NAME, sot.name) self.ID = sot.id def tearDown(self): - sot = self.conn.network.delete_firewall_policy(self.ID, - ignore_missing=False) + sot = self.user_cloud.network.delete_firewall_policy( + self.ID, ignore_missing=False + ) self.assertIs(None, sot) super(TestFirewallPolicy, self).tearDown() def test_find(self): - sot = self.conn.network.find_firewall_policy(self.NAME) + sot = self.user_cloud.network.find_firewall_policy(self.NAME) self.assertEqual(self.ID, sot.id) def test_get(self): - sot = self.conn.network.get_firewall_policy(self.ID) + sot = self.user_cloud.network.get_firewall_policy(self.ID) self.assertEqual(self.NAME, sot.name) self.assertEqual(self.ID, sot.id) def test_list(self): - names = [o.name for o in self.conn.network.firewall_policies()] + names = [o.name for o in self.user_cloud.network.firewall_policies()] self.assertIn(self.NAME, names) diff --git a/openstack/tests/functional/network/v2/test_firewall_rule.py b/openstack/tests/functional/network/v2/test_firewall_rule.py index b8c1cbca2..185fc6cdf 100644 --- a/openstack/tests/functional/network/v2/test_firewall_rule.py +++ b/openstack/tests/functional/network/v2/test_firewall_rule.py @@ -20,41 +20,47 @@ class TestFirewallRule(base.BaseFunctionalTest): - ACTION = 'allow' - DEST_IP = '10.0.0.0/24' - DEST_PORT = '80' + ACTION = "allow" + DEST_IP = "10.0.0.0/24" + DEST_PORT = "80" IP_VERSION = 4 - PROTOCOL = 'tcp' - SOUR_IP = '10.0.1.0/24' - SOUR_PORT = '8000' + PROTOCOL = "tcp" + SOUR_IP = "10.0.1.0/24" + SOUR_PORT = "8000" ID = None def setUp(self): super(TestFirewallRule, self).setUp() - if not self.conn._has_neutron_extension('fwaas_v2'): - self.skipTest('fwaas_v2 service not supported by cloud') + if not self.user_cloud._has_neutron_extension("fwaas_v2"): + self.skipTest("fwaas_v2 service not supported by cloud") self.NAME = self.getUniqueString() - sot = self.conn.network.create_firewall_rule( - name=self.NAME, action=self.ACTION, source_port=self.SOUR_PORT, - destination_port=self.DEST_PORT, source_ip_address=self.SOUR_IP, - destination_ip_address=self.DEST_IP, ip_version=self.IP_VERSION, - protocol=self.PROTOCOL) + sot = self.user_cloud.network.create_firewall_rule( + name=self.NAME, + action=self.ACTION, + source_port=self.SOUR_PORT, + destination_port=self.DEST_PORT, + source_ip_address=self.SOUR_IP, + destination_ip_address=self.DEST_IP, + ip_version=self.IP_VERSION, + protocol=self.PROTOCOL, + ) assert isinstance(sot, firewall_rule.FirewallRule) self.assertEqual(self.NAME, sot.name) self.ID = sot.id def tearDown(self): - sot = self.conn.network.delete_firewall_rule(self.ID, - ignore_missing=False) + sot = self.user_cloud.network.delete_firewall_rule( + self.ID, ignore_missing=False + ) self.assertIs(None, sot) super(TestFirewallRule, self).tearDown() def test_find(self): - sot = self.conn.network.find_firewall_rule(self.NAME) + sot = self.user_cloud.network.find_firewall_rule(self.NAME) self.assertEqual(self.ID, sot.id) def test_get(self): - sot = self.conn.network.get_firewall_rule(self.ID) + sot = self.user_cloud.network.get_firewall_rule(self.ID) self.assertEqual(self.ID, sot.id) self.assertEqual(self.NAME, sot.name) self.assertEqual(self.ACTION, sot.action) @@ -65,5 +71,5 @@ def test_get(self): self.assertEqual(self.SOUR_PORT, sot.source_port) def test_list(self): - ids = [o.id for o in self.conn.network.firewall_rules()] + ids = [o.id for o in self.user_cloud.network.firewall_rules()] self.assertIn(self.ID, ids) diff --git a/openstack/tests/functional/network/v2/test_firewall_rule_insert_remove_policy.py b/openstack/tests/functional/network/v2/test_firewall_rule_insert_remove_policy.py index 1dc7f9788..6eba7d65d 100644 --- a/openstack/tests/functional/network/v2/test_firewall_rule_insert_remove_policy.py +++ b/openstack/tests/functional/network/v2/test_firewall_rule_insert_remove_policy.py @@ -31,15 +31,18 @@ class TestFirewallPolicyRuleAssociations(base.BaseFunctionalTest): def setUp(self): super(TestFirewallPolicyRuleAssociations, self).setUp() - if not self.conn._has_neutron_extension('fwaas_v2'): - self.skipTest('fwaas_v2 service not supported by cloud') - rul1 = self.conn.network.create_firewall_rule(name=self.RULE1_NAME) + if not self.user_cloud._has_neutron_extension("fwaas_v2"): + self.skipTest("fwaas_v2 service not supported by cloud") + rul1 = self.user_cloud.network.create_firewall_rule( + name=self.RULE1_NAME) assert isinstance(rul1, firewall_rule.FirewallRule) self.assertEqual(self.RULE1_NAME, rul1.name) - rul2 = self.conn.network.create_firewall_rule(name=self.RULE2_NAME) + rul2 = self.user_cloud.network.create_firewall_rule( + name=self.RULE2_NAME) assert isinstance(rul2, firewall_rule.FirewallRule) self.assertEqual(self.RULE2_NAME, rul2.name) - pol = self.conn.network.create_firewall_policy(name=self.POLICY_NAME) + pol = self.user_cloud.network.create_firewall_policy( + name=self.POLICY_NAME) assert isinstance(pol, firewall_policy.FirewallPolicy) self.assertEqual(self.POLICY_NAME, pol.name) self.RULE1_ID = rul1.id @@ -47,45 +50,51 @@ def setUp(self): self.POLICY_ID = pol.id def tearDown(self): - sot = self.conn.network.delete_firewall_policy(self.POLICY_ID, - ignore_missing=False) + sot = self.user_cloud.network.delete_firewall_policy( + self.POLICY_ID, ignore_missing=False + ) self.assertIs(None, sot) - sot = self.conn.network.delete_firewall_rule(self.RULE1_ID, - ignore_missing=False) + sot = self.user_cloud.network.delete_firewall_rule( + self.RULE1_ID, ignore_missing=False + ) self.assertIs(None, sot) - sot = self.conn.network.delete_firewall_rule(self.RULE2_ID, - ignore_missing=False) + sot = self.user_cloud.network.delete_firewall_rule( + self.RULE2_ID, ignore_missing=False + ) self.assertIs(None, sot) super(TestFirewallPolicyRuleAssociations, self).tearDown() def test_insert_rule_into_policy(self): - policy = self.conn.network.insert_rule_into_policy( - self.POLICY_ID, - firewall_rule_id=self.RULE1_ID) - self.assertIn(self.RULE1_ID, policy['firewall_rules']) - policy = self.conn.network.insert_rule_into_policy( + policy = self.user_cloud.network.insert_rule_into_policy( + self.POLICY_ID, firewall_rule_id=self.RULE1_ID + ) + self.assertIn(self.RULE1_ID, policy["firewall_rules"]) + policy = self.user_cloud.network.insert_rule_into_policy( self.POLICY_ID, firewall_rule_id=self.RULE2_ID, - insert_before=self.RULE1_ID) - self.assertEqual(self.RULE1_ID, policy['firewall_rules'][1]) - self.assertEqual(self.RULE2_ID, policy['firewall_rules'][0]) + insert_before=self.RULE1_ID, + ) + self.assertEqual(self.RULE1_ID, policy["firewall_rules"][1]) + self.assertEqual(self.RULE2_ID, policy["firewall_rules"][0]) def test_remove_rule_from_policy(self): # insert rules into policy before we remove it again - policy = self.conn.network.insert_rule_into_policy( - self.POLICY_ID, firewall_rule_id=self.RULE1_ID) - self.assertIn(self.RULE1_ID, policy['firewall_rules']) + policy = self.user_cloud.network.insert_rule_into_policy( + self.POLICY_ID, firewall_rule_id=self.RULE1_ID + ) + self.assertIn(self.RULE1_ID, policy["firewall_rules"]) - policy = self.conn.network.insert_rule_into_policy( - self.POLICY_ID, firewall_rule_id=self.RULE2_ID) - self.assertIn(self.RULE2_ID, policy['firewall_rules']) + policy = self.user_cloud.network.insert_rule_into_policy( + self.POLICY_ID, firewall_rule_id=self.RULE2_ID + ) + self.assertIn(self.RULE2_ID, policy["firewall_rules"]) - policy = self.conn.network.remove_rule_from_policy( - self.POLICY_ID, - firewall_rule_id=self.RULE1_ID) - self.assertNotIn(self.RULE1_ID, policy['firewall_rules']) + policy = self.user_cloud.network.remove_rule_from_policy( + self.POLICY_ID, firewall_rule_id=self.RULE1_ID + ) + self.assertNotIn(self.RULE1_ID, policy["firewall_rules"]) - policy = self.conn.network.remove_rule_from_policy( - self.POLICY_ID, - firewall_rule_id=self.RULE2_ID) - self.assertNotIn(self.RULE2_ID, policy['firewall_rules']) + policy = self.user_cloud.network.remove_rule_from_policy( + self.POLICY_ID, firewall_rule_id=self.RULE2_ID + ) + self.assertNotIn(self.RULE2_ID, policy["firewall_rules"]) diff --git a/openstack/tests/functional/network/v2/test_flavor.py b/openstack/tests/functional/network/v2/test_flavor.py index 24d0f126f..1f8a8befb 100644 --- a/openstack/tests/functional/network/v2/test_flavor.py +++ b/openstack/tests/functional/network/v2/test_flavor.py @@ -25,54 +25,80 @@ class TestFlavor(base.BaseFunctionalTest): def setUp(self): super(TestFlavor, self).setUp() - self.FLAVOR_NAME = self.getUniqueString('flavor') - flavors = self.conn.network.create_flavor( - name=self.FLAVOR_NAME, - service_type=self.SERVICE_TYPE) - assert isinstance(flavors, flavor.Flavor) - self.assertEqual(self.FLAVOR_NAME, flavors.name) - self.assertEqual(self.SERVICE_TYPE, flavors.service_type) - - self.ID = flavors.id - - self.service_profiles = self.conn.network.create_service_profile( - description=self.SERVICE_PROFILE_DESCRIPTION, - metainfo=self.METAINFO,) + if not self.user_cloud._has_neutron_extension("flavors"): + self.skipTest("Neutron flavor extension is required for this test") + + self.FLAVOR_NAME = self.getUniqueString("flavor") + if self.operator_cloud: + flavors = self.operator_cloud.network.create_flavor( + name=self.FLAVOR_NAME, service_type=self.SERVICE_TYPE + ) + assert isinstance(flavors, flavor.Flavor) + self.assertEqual(self.FLAVOR_NAME, flavors.name) + self.assertEqual(self.SERVICE_TYPE, flavors.service_type) + + self.ID = flavors.id + + self.service_profiles = ( + self.operator_cloud.network.create_service_profile( + description=self.SERVICE_PROFILE_DESCRIPTION, + metainfo=self.METAINFO, + ) + ) def tearDown(self): - flavors = self.conn.network.delete_flavor(self.ID, ignore_missing=True) - self.assertIsNone(flavors) - - service_profiles = self.conn.network.delete_service_profile( - self.ID, ignore_missing=True) - self.assertIsNone(service_profiles) + if self.operator_cloud and self.ID: + flavors = self.operator_cloud.network.delete_flavor( + self.ID, ignore_missing=True + ) + self.assertIsNone(flavors) + + service_profiles = self.user_cloud.network.delete_service_profile( + self.ID, ignore_missing=True + ) + self.assertIsNone(service_profiles) super(TestFlavor, self).tearDown() def test_find(self): - flavors = self.conn.network.find_flavor(self.FLAVOR_NAME) - self.assertEqual(self.ID, flavors.id) + if self.ID: + flavors = self.user_cloud.network.find_flavor(self.FLAVOR_NAME) + self.assertEqual(self.ID, flavors.id) + else: + self.user_cloud.network.find_flavor("definitely_missing") def test_get(self): - flavors = self.conn.network.get_flavor(self.ID) + if not self.ID: + self.skipTest("Operator cloud required for this test") + + flavors = self.user_cloud.network.get_flavor(self.ID) self.assertEqual(self.FLAVOR_NAME, flavors.name) self.assertEqual(self.ID, flavors.id) def test_list(self): - names = [f.name for f in self.conn.network.flavors()] - self.assertIn(self.FLAVOR_NAME, names) + names = [f.name for f in self.user_cloud.network.flavors()] + if self.ID: + self.assertIn(self.FLAVOR_NAME, names) def test_update(self): - flavor = self.conn.network.update_flavor(self.ID, - name=self.UPDATE_NAME) + if not self.operator_cloud: + self.skipTest("Operator cloud required for this test") + flavor = self.operator_cloud.network.update_flavor( + self.ID, name=self.UPDATE_NAME + ) self.assertEqual(self.UPDATE_NAME, flavor.name) def test_associate_disassociate_flavor_with_service_profile(self): - response = \ - self.conn.network.associate_flavor_with_service_profile( - self.ID, self.service_profiles.id) + if not self.operator_cloud: + self.skipTest("Operator cloud required for this test") + response = ( + self.operator_cloud.network.associate_flavor_with_service_profile( + self.ID, self.service_profiles.id + ) + ) self.assertIsNotNone(response) - response = \ - self.conn.network.disassociate_flavor_from_service_profile( - self.ID, self.service_profiles.id) + response = self.operator_cloud.network \ + .disassociate_flavor_from_service_profile( + self.ID, self.service_profiles.id + ) self.assertIsNone(response) diff --git a/openstack/tests/functional/network/v2/test_floating_ip.py b/openstack/tests/functional/network/v2/test_floating_ip.py index 69e6e9ed1..1a0bda1a0 100644 --- a/openstack/tests/functional/network/v2/test_floating_ip.py +++ b/openstack/tests/functional/network/v2/test_floating_ip.py @@ -36,78 +36,107 @@ class TestFloatingIP(base.BaseFunctionalTest): def setUp(self): super(TestFloatingIP, self).setUp() - if not self.conn.has_service('dns'): - self.skipTest('dns service not supported by cloud') + if not self.user_cloud._has_neutron_extension("external-net"): + self.skipTest( + "Neutron external-net extension is required for this test" + ) self.TIMEOUT_SCALING_FACTOR = 1.5 self.ROT_NAME = self.getUniqueString() - self.EXT_NET_NAME = self.getUniqueString() - self.EXT_SUB_NAME = self.getUniqueString() self.INT_NET_NAME = self.getUniqueString() self.INT_SUB_NAME = self.getUniqueString() - # Create Exeternal Network - args = {'router:external': True} - net = self._create_network(self.EXT_NET_NAME, **args) - self.EXT_NET_ID = net.id - sub = self._create_subnet( - self.EXT_SUB_NAME, self.EXT_NET_ID, self.EXT_CIDR) - self.EXT_SUB_ID = sub.id + self.EXT_NET_ID = None + self.EXT_SUB_ID = None + self.is_dns_supported = False + + # Find External Network + for net in self.user_cloud.network.networks(is_router_external=True): + self.EXT_NET_ID = net.id + # Find subnet of the chosen external net + for sub in self.user_cloud.network.subnets(network_id=self.EXT_NET_ID): + self.EXT_SUB_ID = sub.id + if not self.EXT_NET_ID and self.operator_cloud: + # There is no existing external net, but operator + # credentials available + # WARNING: this external net is not dropped + # Create External Network + args = {"router:external": True} + net = self._create_network(self.EXT_NET_NAME, **args) + self.EXT_NET_ID = net.id + sub = self._create_subnet( + self.EXT_SUB_NAME, self.EXT_NET_ID, self.EXT_CIDR + ) + self.EXT_SUB_ID = sub.id + # Create Internal Network net = self._create_network(self.INT_NET_NAME) self.INT_NET_ID = net.id sub = self._create_subnet( - self.INT_SUB_NAME, self.INT_NET_ID, self.INT_CIDR) + self.INT_SUB_NAME, self.INT_NET_ID, self.INT_CIDR + ) self.INT_SUB_ID = sub.id # Create Router - args = {'external_gateway_info': {'network_id': self.EXT_NET_ID}} - sot = self.conn.network.create_router(name=self.ROT_NAME, **args) + args = {"external_gateway_info": {"network_id": self.EXT_NET_ID}} + sot = self.user_cloud.network.create_router(name=self.ROT_NAME, **args) assert isinstance(sot, router.Router) self.assertEqual(self.ROT_NAME, sot.name) self.ROT_ID = sot.id self.ROT = sot # Add Router's Interface to Internal Network sot = self.ROT.add_interface( - self.conn.network, subnet_id=self.INT_SUB_ID) - self.assertEqual(sot['subnet_id'], self.INT_SUB_ID) + self.user_cloud.network, subnet_id=self.INT_SUB_ID + ) + self.assertEqual(sot["subnet_id"], self.INT_SUB_ID) # Create Port in Internal Network - prt = self.conn.network.create_port(network_id=self.INT_NET_ID) + prt = self.user_cloud.network.create_port(network_id=self.INT_NET_ID) assert isinstance(prt, port.Port) self.PORT_ID = prt.id self.PORT = prt # Create Floating IP. - fip = self.conn.network.create_ip( + fip_args = dict( floating_network_id=self.EXT_NET_ID, - dns_domain=self.DNS_DOMAIN, dns_name=self.DNS_NAME) + ) + if ( + self.user_cloud._has_neutron_extension("dns-integration") + and self.user_cloud.has_service("dns") + ): + self.is_dns_supported = True + fip_args.update( + dict(dns_domain=self.DNS_DOMAIN, dns_name=self.DNS_NAME) + ) + fip = self.user_cloud.network.create_ip(**fip_args) assert isinstance(fip, floating_ip.FloatingIP) self.FIP = fip def tearDown(self): - sot = self.conn.network.delete_ip(self.FIP.id, ignore_missing=False) + sot = self.user_cloud.network.delete_ip( + self.FIP.id, ignore_missing=False + ) self.assertIsNone(sot) - sot = self.conn.network.delete_port(self.PORT_ID, ignore_missing=False) + sot = self.user_cloud.network.delete_port( + self.PORT_ID, ignore_missing=False + ) self.assertIsNone(sot) sot = self.ROT.remove_interface( - self.conn.network, subnet_id=self.INT_SUB_ID) - self.assertEqual(sot['subnet_id'], self.INT_SUB_ID) - sot = self.conn.network.delete_router( - self.ROT_ID, ignore_missing=False) - self.assertIsNone(sot) - sot = self.conn.network.delete_subnet( - self.EXT_SUB_ID, ignore_missing=False) - self.assertIsNone(sot) - sot = self.conn.network.delete_network( - self.EXT_NET_ID, ignore_missing=False) + self.user_cloud.network, subnet_id=self.INT_SUB_ID + ) + self.assertEqual(sot["subnet_id"], self.INT_SUB_ID) + sot = self.user_cloud.network.delete_router( + self.ROT_ID, ignore_missing=False + ) self.assertIsNone(sot) - sot = self.conn.network.delete_subnet( - self.INT_SUB_ID, ignore_missing=False) + sot = self.user_cloud.network.delete_subnet( + self.INT_SUB_ID, ignore_missing=False + ) self.assertIsNone(sot) - sot = self.conn.network.delete_network( - self.INT_NET_ID, ignore_missing=False) + sot = self.user_cloud.network.delete_network( + self.INT_NET_ID, ignore_missing=False + ) self.assertIsNone(sot) super(TestFloatingIP, self).tearDown() def _create_network(self, name, **args): self.name = name - net = self.conn.network.create_network(name=name, **args) + net = self.user_cloud.network.create_network(name=name, **args) assert isinstance(net, network.Network) self.assertEqual(self.name, net.name) return net @@ -116,32 +145,33 @@ def _create_subnet(self, name, net_id, cidr): self.name = name self.net_id = net_id self.cidr = cidr - sub = self.conn.network.create_subnet( + sub = self.user_cloud.network.create_subnet( name=self.name, ip_version=self.IPV4, network_id=self.net_id, - cidr=self.cidr) + cidr=self.cidr, + ) assert isinstance(sub, subnet.Subnet) self.assertEqual(self.name, sub.name) return sub def test_find_by_id(self): - sot = self.conn.network.find_ip(self.FIP.id) + sot = self.user_cloud.network.find_ip(self.FIP.id) self.assertEqual(self.FIP.id, sot.id) def test_find_by_ip_address(self): - sot = self.conn.network.find_ip(self.FIP.floating_ip_address) + sot = self.user_cloud.network.find_ip(self.FIP.floating_ip_address) self.assertEqual(self.FIP.floating_ip_address, sot.floating_ip_address) self.assertEqual(self.FIP.floating_ip_address, sot.name) def test_find_available_ip(self): - sot = self.conn.network.find_available_ip() + sot = self.user_cloud.network.find_available_ip() self.assertIsNotNone(sot.id) self.assertIsNone(sot.port_id) self.assertIsNone(sot.port_details) def test_get(self): - sot = self.conn.network.get_ip(self.FIP.id) + sot = self.user_cloud.network.get_ip(self.FIP.id) self.assertEqual(self.EXT_NET_ID, sot.floating_network_id) self.assertEqual(self.FIP.id, sot.id) self.assertEqual(self.FIP.floating_ip_address, sot.floating_ip_address) @@ -149,37 +179,41 @@ def test_get(self): self.assertEqual(self.FIP.port_id, sot.port_id) self.assertEqual(self.FIP.port_details, sot.port_details) self.assertEqual(self.FIP.router_id, sot.router_id) - self.assertEqual(self.DNS_DOMAIN, sot.dns_domain) - self.assertEqual(self.DNS_NAME, sot.dns_name) + if self.is_dns_supported: + self.assertEqual(self.DNS_DOMAIN, sot.dns_domain) + self.assertEqual(self.DNS_NAME, sot.dns_name) def test_list(self): - ids = [o.id for o in self.conn.network.ips()] + ids = [o.id for o in self.user_cloud.network.ips()] self.assertIn(self.FIP.id, ids) def test_update(self): - sot = self.conn.network.update_ip(self.FIP.id, port_id=self.PORT_ID) + sot = self.user_cloud.network.update_ip( + self.FIP.id, port_id=self.PORT_ID + ) self.assertEqual(self.PORT_ID, sot.port_id) self._assert_port_details(self.PORT, sot.port_details) self.assertEqual(self.FIP.id, sot.id) def test_set_tags(self): - sot = self.conn.network.get_ip(self.FIP.id) + sot = self.user_cloud.network.get_ip(self.FIP.id) self.assertEqual([], sot.tags) - self.conn.network.set_tags(sot, ['blue']) - sot = self.conn.network.get_ip(self.FIP.id) - self.assertEqual(['blue'], sot.tags) + self.user_cloud.network.set_tags(sot, ["blue"]) + sot = self.user_cloud.network.get_ip(self.FIP.id) + self.assertEqual(["blue"], sot.tags) - self.conn.network.set_tags(sot, []) - sot = self.conn.network.get_ip(self.FIP.id) + self.user_cloud.network.set_tags(sot, []) + sot = self.user_cloud.network.get_ip(self.FIP.id) self.assertEqual([], sot.tags) def _assert_port_details(self, port, port_details): - self.assertEqual(port.name, port_details['name']) - self.assertEqual(port.network_id, port_details['network_id']) - self.assertEqual(port.mac_address, port_details['mac_address']) - self.assertEqual(port.is_admin_state_up, - port_details['admin_state_up']) - self.assertEqual(port.status, port_details['status']) - self.assertEqual(port.device_id, port_details['device_id']) - self.assertEqual(port.device_owner, port_details['device_owner']) + self.assertEqual(port.name, port_details["name"]) + self.assertEqual(port.network_id, port_details["network_id"]) + self.assertEqual(port.mac_address, port_details["mac_address"]) + self.assertEqual( + port.is_admin_state_up, port_details["admin_state_up"] + ) + self.assertEqual(port.status, port_details["status"]) + self.assertEqual(port.device_id, port_details["device_id"]) + self.assertEqual(port.device_owner, port_details["device_owner"]) diff --git a/openstack/tests/functional/network/v2/test_l3_conntrack_helper.py b/openstack/tests/functional/network/v2/test_l3_conntrack_helper.py index 8fd0fae7f..a9e0315d3 100644 --- a/openstack/tests/functional/network/v2/test_l3_conntrack_helper.py +++ b/openstack/tests/functional/network/v2/test_l3_conntrack_helper.py @@ -27,48 +27,52 @@ class TestL3ConntrackHelper(base.BaseFunctionalTest): def setUp(self): super(TestL3ConntrackHelper, self).setUp() - if not self.conn.network.find_extension('l3-conntrack-helper'): - self.skipTest('L3 conntrack helper extension disabled') + if not self.user_cloud.network.find_extension("l3-conntrack-helper"): + self.skipTest("L3 conntrack helper extension disabled") self.ROT_NAME = self.getUniqueString() # Create Router - sot = self.conn.network.create_router(name=self.ROT_NAME) + sot = self.user_cloud.network.create_router(name=self.ROT_NAME) self.assertIsInstance(sot, router.Router) self.assertEqual(self.ROT_NAME, sot.name) self.ROT_ID = sot.id self.ROT = sot # Create conntrack helper - ct_helper = self.conn.network.create_conntrack_helper( + ct_helper = self.user_cloud.network.create_conntrack_helper( router=self.ROT, protocol=self.PROTOCOL, helper=self.HELPER, - port=self.PORT) + port=self.PORT, + ) self.assertIsInstance(ct_helper, _l3_conntrack_helper.ConntrackHelper) self.CT_HELPER = ct_helper def tearDown(self): - sot = self.conn.network.delete_router( - self.ROT_ID, ignore_missing=False) + sot = self.user_cloud.network.delete_router( + self.ROT_ID, ignore_missing=False + ) self.assertIsNone(sot) super(TestL3ConntrackHelper, self).tearDown() def test_get(self): - sot = self.conn.network.get_conntrack_helper( - self.CT_HELPER, self.ROT_ID) + sot = self.user_cloud.network.get_conntrack_helper( + self.CT_HELPER, self.ROT_ID + ) self.assertEqual(self.PROTOCOL, sot.protocol) self.assertEqual(self.HELPER, sot.helper) self.assertEqual(self.PORT, sot.port) def test_list(self): - helper_ids = [o.id for o in - self.conn.network.conntrack_helpers(self.ROT_ID)] + helper_ids = [ + o.id + for o in self.user_cloud.network.conntrack_helpers(self.ROT_ID) + ] self.assertIn(self.CT_HELPER.id, helper_ids) def test_update(self): NEW_PORT = 90 - sot = self.conn.network.update_conntrack_helper( - self.CT_HELPER.id, - self.ROT_ID, - port=NEW_PORT) + sot = self.user_cloud.network.update_conntrack_helper( + self.CT_HELPER.id, self.ROT_ID, port=NEW_PORT + ) self.assertEqual(NEW_PORT, sot.port) diff --git a/openstack/tests/functional/network/v2/test_local_ip.py b/openstack/tests/functional/network/v2/test_local_ip.py index 01e343aeb..9489a2df7 100644 --- a/openstack/tests/functional/network/v2/test_local_ip.py +++ b/openstack/tests/functional/network/v2/test_local_ip.py @@ -24,45 +24,46 @@ class TestLocalIP(base.BaseFunctionalTest): def setUp(self): super(TestLocalIP, self).setUp() - if not self.conn.network.find_extension('local_ip'): - self.skipTest('Local IP extension disabled') + if not self.user_cloud.network.find_extension("local_ip"): + self.skipTest("Local IP extension disabled") self.LOCAL_IP_NAME = self.getUniqueString() self.LOCAL_IP_DESCRIPTION = self.getUniqueString() self.LOCAL_IP_NAME_UPDATED = self.getUniqueString() self.LOCAL_IP_DESCRIPTION_UPDATED = self.getUniqueString() - local_ip = self.conn.network.create_local_ip( + local_ip = self.user_cloud.network.create_local_ip( name=self.LOCAL_IP_NAME, description=self.LOCAL_IP_DESCRIPTION, ) assert isinstance(local_ip, _local_ip.LocalIP) self.assertEqual(self.LOCAL_IP_NAME, local_ip.name) - self.assertEqual(self.LOCAL_IP_DESCRIPTION, - local_ip.description) + self.assertEqual(self.LOCAL_IP_DESCRIPTION, local_ip.description) self.LOCAL_IP_ID = local_ip.id def tearDown(self): - sot = self.conn.network.delete_local_ip(self.LOCAL_IP_ID) + sot = self.user_cloud.network.delete_local_ip(self.LOCAL_IP_ID) self.assertIsNone(sot) super(TestLocalIP, self).tearDown() def test_find(self): - sot = self.conn.network.find_local_ip(self.LOCAL_IP_NAME) + sot = self.user_cloud.network.find_local_ip(self.LOCAL_IP_NAME) self.assertEqual(self.LOCAL_IP_ID, sot.id) def test_get(self): - sot = self.conn.network.get_local_ip(self.LOCAL_IP_ID) + sot = self.user_cloud.network.get_local_ip(self.LOCAL_IP_ID) self.assertEqual(self.LOCAL_IP_NAME, sot.name) def test_list(self): - names = [local_ip.name for local_ip in self.conn.network.local_ips()] + names = [ + local_ip.name + for local_ip in self.user_cloud.network.local_ips()] self.assertIn(self.LOCAL_IP_NAME, names) def test_update(self): - sot = self.conn.network.update_local_ip( + sot = self.user_cloud.network.update_local_ip( self.LOCAL_IP_ID, name=self.LOCAL_IP_NAME_UPDATED, - description=self.LOCAL_IP_DESCRIPTION_UPDATED) + description=self.LOCAL_IP_DESCRIPTION_UPDATED, + ) self.assertEqual(self.LOCAL_IP_NAME_UPDATED, sot.name) - self.assertEqual(self.LOCAL_IP_DESCRIPTION_UPDATED, - sot.description) + self.assertEqual(self.LOCAL_IP_DESCRIPTION_UPDATED, sot.description) diff --git a/openstack/tests/functional/network/v2/test_local_ip_association.py b/openstack/tests/functional/network/v2/test_local_ip_association.py index e4f1dbb48..a3f2c019d 100644 --- a/openstack/tests/functional/network/v2/test_local_ip_association.py +++ b/openstack/tests/functional/network/v2/test_local_ip_association.py @@ -25,44 +25,51 @@ class TestLocalIPAssociation(base.BaseFunctionalTest): def setUp(self): super(TestLocalIPAssociation, self).setUp() - if not self.conn.network.find_extension('local_ip'): - self.skipTest('Local IP extension disabled') + if not self.user_cloud.network.find_extension("local_ip"): + self.skipTest("Local IP extension disabled") self.LOCAL_IP_ID = self.getUniqueString() self.FIXED_PORT_ID = self.getUniqueString() self.FIXED_IP = self.getUniqueString() - local_ip_association = self.conn.network.create_local_ip_association( - local_ip=self.LOCAL_IP_ID, - fixed_port_id=self.FIXED_PORT_ID, - fixed_ip=self.FIXED_IP + local_ip_association = self.user_cloud.network \ + .create_local_ip_association( + local_ip=self.LOCAL_IP_ID, + fixed_port_id=self.FIXED_PORT_ID, + fixed_ip=self.FIXED_IP, + ) + assert isinstance( + local_ip_association, _local_ip_association.LocalIPAssociation ) - assert isinstance(local_ip_association, - _local_ip_association.LocalIPAssociation) self.assertEqual(self.LOCAL_IP_ID, local_ip_association.local_ip_id) - self.assertEqual(self.FIXED_PORT_ID, - local_ip_association.fixed_port_id) - self.assertEqual(self.FIXED_IP, - local_ip_association.fixed_ip) + self.assertEqual( + self.FIXED_PORT_ID, local_ip_association.fixed_port_id + ) + self.assertEqual(self.FIXED_IP, local_ip_association.fixed_ip) def tearDown(self): - sot = self.conn.network.delete_local_ip_association( - self.LOCAL_IP_ID, - self.FIXED_PORT_ID) + sot = self.user_cloud.network.delete_local_ip_association( + self.LOCAL_IP_ID, self.FIXED_PORT_ID + ) self.assertIsNone(sot) super(TestLocalIPAssociation, self).tearDown() def test_find(self): - sot = self.conn.network.find_local_ip_association(self.FIXED_PORT_ID, - self.LOCAL_IP_ID) + sot = self.user_cloud.network.find_local_ip_association( + self.FIXED_PORT_ID, self.LOCAL_IP_ID + ) self.assertEqual(self.FIXED_PORT_ID, sot.fixed_port_id) def test_get(self): - sot = self.conn.network.get_local_ip_association(self.FIXED_PORT_ID, - self.LOCAL_IP_ID) + sot = self.user_cloud.network.get_local_ip_association( + self.FIXED_PORT_ID, self.LOCAL_IP_ID + ) self.assertEqual(self.FIXED_PORT_ID, sot.fixed_port_id) def test_list(self): - fixed_port_id = [obj.fixed_port_id for obj in - self.conn.network.local_ip_associations( - self.LOCAL_IP_ID)] + fixed_port_id = [ + obj.fixed_port_id + for obj in self.user_cloud.network.local_ip_associations( + self.LOCAL_IP_ID + ) + ] self.assertIn(self.FIXED_PORT_ID, fixed_port_id) diff --git a/openstack/tests/functional/network/v2/test_ndp_proxy.py b/openstack/tests/functional/network/v2/test_ndp_proxy.py index b81a8ffcc..300e31c58 100644 --- a/openstack/tests/functional/network/v2/test_ndp_proxy.py +++ b/openstack/tests/functional/network/v2/test_ndp_proxy.py @@ -33,80 +33,91 @@ class TestNDPProxy(base.BaseFunctionalTest): def setUp(self): super(TestNDPProxy, self).setUp() - if not self.conn.network.find_extension('l3-ndp-proxy'): - self.skipTest('L3 ndp proxy extension disabled') + if not self.user_cloud.network.find_extension("l3-ndp-proxy"): + self.skipTest("L3 ndp proxy extension disabled") self.ROT_NAME = self.getUniqueString() self.EXT_NET_NAME = self.getUniqueString() self.EXT_SUB_NAME = self.getUniqueString() self.INT_NET_NAME = self.getUniqueString() self.INT_SUB_NAME = self.getUniqueString() - # Create Exeternal Network - args = {'router:external': True} - net = self._create_network(self.EXT_NET_NAME, **args) - self.EXT_NET_ID = net.id - sub = self._create_subnet( - self.EXT_SUB_NAME, self.EXT_NET_ID, self.EXT_CIDR) - self.EXT_SUB_ID = sub.id - # Create Internal Network - net = self._create_network(self.INT_NET_NAME) - self.INT_NET_ID = net.id - sub = self._create_subnet( - self.INT_SUB_NAME, self.INT_NET_ID, self.INT_CIDR) - self.INT_SUB_ID = sub.id + + # Find External Network + for net in self.user_cloud.network.networks(is_router_external=True): + self.EXT_NET_ID = net.id + # Find subnet of the chosen external net + for sub in self.user_cloud.network.subnets(network_id=self.EXT_NET_ID): + self.EXT_SUB_ID = sub.id + if not self.EXT_NET_ID and self.operator_cloud: + # There is no existing external net, but operator + # credentials available + # WARNING: this external net is not dropped + # Create External Network + args = {"router:external": True} + net = self._create_network(self.EXT_NET_NAME, **args) + self.EXT_NET_ID = net.id + sub = self._create_subnet( + self.EXT_SUB_NAME, self.EXT_NET_ID, self.EXT_CIDR + ) + self.EXT_SUB_ID = sub.id + # Create Router - args = {'external_gateway_info': {'network_id': self.EXT_NET_ID}, - 'enable_ndp_proxy': True} - sot = self.conn.network.create_router(name=self.ROT_NAME, **args) + args = { + "external_gateway_info": {"network_id": self.EXT_NET_ID}, + "enable_ndp_proxy": True, + } + sot = self.user_cloud.network.create_router(name=self.ROT_NAME, **args) assert isinstance(sot, router.Router) self.assertEqual(self.ROT_NAME, sot.name) self.ROT_ID = sot.id self.ROT = sot # Add Router's Interface to Internal Network sot = self.ROT.add_interface( - self.conn.network, subnet_id=self.INT_SUB_ID) - self.assertEqual(sot['subnet_id'], self.INT_SUB_ID) + self.user_cloud.network, subnet_id=self.INT_SUB_ID + ) + self.assertEqual(sot["subnet_id"], self.INT_SUB_ID) # Create Port in Internal Network - prt = self.conn.network.create_port(network_id=self.INT_NET_ID) + prt = self.user_cloud.network.create_port(network_id=self.INT_NET_ID) assert isinstance(prt, port.Port) self.INTERNAL_PORT_ID = prt.id - self.INTERNAL_IP_ADDRESS = prt.fixed_ips[0]['ip_address'] + self.INTERNAL_IP_ADDRESS = prt.fixed_ips[0]["ip_address"] # Create ndp proxy - np = self.conn.network.create_ndp_proxy( - router_id=self.ROT_ID, port_id=self.INTERNAL_PORT_ID) + np = self.user_cloud.network.create_ndp_proxy( + router_id=self.ROT_ID, port_id=self.INTERNAL_PORT_ID + ) assert isinstance(np, _ndp_proxy.NDPProxy) self.NP = np def tearDown(self): - sot = self.conn.network.delete_ndp_proxy( - self.NP.id, ignore_missing=False) + sot = self.user_cloud.network.delete_ndp_proxy( + self.NP.id, ignore_missing=False + ) self.assertIsNone(sot) - sot = self.conn.network.delete_port( - self.INTERNAL_PORT_ID, ignore_missing=False) + sot = self.user_cloud.network.delete_port( + self.INTERNAL_PORT_ID, ignore_missing=False + ) self.assertIsNone(sot) sot = self.ROT.remove_interface( - self.conn.network, subnet_id=self.INT_SUB_ID) - self.assertEqual(sot['subnet_id'], self.INT_SUB_ID) - sot = self.conn.network.delete_router( - self.ROT_ID, ignore_missing=False) - self.assertIsNone(sot) - sot = self.conn.network.delete_subnet( - self.EXT_SUB_ID, ignore_missing=False) - self.assertIsNone(sot) - sot = self.conn.network.delete_network( - self.EXT_NET_ID, ignore_missing=False) + self.user_cloud.network, subnet_id=self.INT_SUB_ID + ) + self.assertEqual(sot["subnet_id"], self.INT_SUB_ID) + sot = self.user_cloud.network.delete_router( + self.ROT_ID, ignore_missing=False + ) self.assertIsNone(sot) - sot = self.conn.network.delete_subnet( - self.INT_SUB_ID, ignore_missing=False) + sot = self.user_cloud.network.delete_subnet( + self.INT_SUB_ID, ignore_missing=False + ) self.assertIsNone(sot) - sot = self.conn.network.delete_network( - self.INT_NET_ID, ignore_missing=False) + sot = self.user_cloud.network.delete_network( + self.INT_NET_ID, ignore_missing=False + ) self.assertIsNone(sot) super(TestNDPProxy, self).tearDown() def _create_network(self, name, **args): self.name = name - net = self.conn.network.create_network(name=name, **args) + net = self.user_cloud.network.create_network(name=name, **args) assert isinstance(net, network.Network) self.assertEqual(self.name, net.name) return net @@ -115,33 +126,35 @@ def _create_subnet(self, name, net_id, cidr): self.name = name self.net_id = net_id self.cidr = cidr - sub = self.conn.network.create_subnet( + sub = self.user_cloud.network.create_subnet( name=self.name, ip_version=self.IPV6, network_id=self.net_id, - cidr=self.cidr) + cidr=self.cidr, + ) assert isinstance(sub, subnet.Subnet) self.assertEqual(self.name, sub.name) return sub def test_find(self): - sot = self.conn.network.find_ndp_proxy(self.NP.id) + sot = self.user_cloud.network.find_ndp_proxy(self.NP.id) self.assertEqual(self.ROT_ID, sot.router_id) self.assertEqual(self.INTERNAL_PORT_ID, sot.port_id) self.assertEqual(self.INTERNAL_IP_ADDRESS, sot.ip_address) def test_get(self): - sot = self.conn.network.get_ndp_proxy(self.NP.id) + sot = self.user_cloud.network.get_ndp_proxy(self.NP.id) self.assertEqual(self.ROT_ID, sot.router_id) self.assertEqual(self.INTERNAL_PORT_ID, sot.port_id) self.assertEqual(self.INTERNAL_IP_ADDRESS, sot.ip_address) def test_list(self): - np_ids = [o.id for o in self.conn.network.ndp_proxies()] + np_ids = [o.id for o in self.user_cloud.network.ndp_proxies()] self.assertIn(self.NP.id, np_ids) def test_update(self): description = "balabalbala" - sot = self.conn.network.update_ndp_proxy( - self.NP.id, description=description) + sot = self.user_cloud.network.update_ndp_proxy( + self.NP.id, description=description + ) self.assertEqual(description, sot.description) diff --git a/openstack/tests/functional/network/v2/test_network.py b/openstack/tests/functional/network/v2/test_network.py index 18234f6d0..520eb761c 100644 --- a/openstack/tests/functional/network/v2/test_network.py +++ b/openstack/tests/functional/network/v2/test_network.py @@ -19,10 +19,8 @@ def create_network(conn, name, cidr): try: network = conn.network.create_network(name=name) subnet = conn.network.create_subnet( - name=name, - ip_version=4, - network_id=network.id, - cidr=cidr) + name=name, ip_version=4, network_id=network.id, cidr=cidr + ) return (network, subnet) except Exception as e: print(str(e)) @@ -44,50 +42,57 @@ class TestNetwork(base.BaseFunctionalTest): def setUp(self): super(TestNetwork, self).setUp() self.NAME = self.getUniqueString() - sot = self.conn.network.create_network(name=self.NAME) + sot = self.user_cloud.network.create_network(name=self.NAME) assert isinstance(sot, network.Network) self.assertEqual(self.NAME, sot.name) self.ID = sot.id def tearDown(self): - sot = self.conn.network.delete_network(self.ID, ignore_missing=False) + sot = self.user_cloud.network.delete_network( + self.ID, ignore_missing=False + ) self.assertIsNone(sot) super(TestNetwork, self).tearDown() def test_find(self): - sot = self.conn.network.find_network(self.NAME) + sot = self.user_cloud.network.find_network(self.NAME) self.assertEqual(self.ID, sot.id) def test_find_with_filter(self): + if not self.operator_cloud: + self.skipTest("Operator cloud required for this test") project_id_1 = "1" project_id_2 = "2" - sot1 = self.conn.network.create_network(name=self.NAME, - project_id=project_id_1) - sot2 = self.conn.network.create_network(name=self.NAME, - project_id=project_id_2) - sot = self.conn.network.find_network(self.NAME, - project_id=project_id_1) + sot1 = self.operator_cloud.network.create_network( + name=self.NAME, project_id=project_id_1 + ) + sot2 = self.operator_cloud.network.create_network( + name=self.NAME, project_id=project_id_2 + ) + sot = self.operator_cloud.network.find_network( + self.NAME, project_id=project_id_1 + ) self.assertEqual(project_id_1, sot.project_id) - self.conn.network.delete_network(sot1.id) - self.conn.network.delete_network(sot2.id) + self.operator_cloud.network.delete_network(sot1.id) + self.operator_cloud.network.delete_network(sot2.id) def test_get(self): - sot = self.conn.network.get_network(self.ID) + sot = self.user_cloud.network.get_network(self.ID) self.assertEqual(self.NAME, sot.name) self.assertEqual(self.ID, sot.id) def test_list(self): - names = [o.name for o in self.conn.network.networks()] + names = [o.name for o in self.user_cloud.network.networks()] self.assertIn(self.NAME, names) def test_set_tags(self): - sot = self.conn.network.get_network(self.ID) + sot = self.user_cloud.network.get_network(self.ID) self.assertEqual([], sot.tags) - self.conn.network.set_tags(sot, ['blue']) - sot = self.conn.network.get_network(self.ID) - self.assertEqual(['blue'], sot.tags) + self.user_cloud.network.set_tags(sot, ["blue"]) + sot = self.user_cloud.network.get_network(self.ID) + self.assertEqual(["blue"], sot.tags) - self.conn.network.set_tags(sot, []) - sot = self.conn.network.get_network(self.ID) + self.user_cloud.network.set_tags(sot, []) + sot = self.user_cloud.network.get_network(self.ID) self.assertEqual([], sot.tags) diff --git a/openstack/tests/functional/network/v2/test_network_ip_availability.py b/openstack/tests/functional/network/v2/test_network_ip_availability.py index 6f7eda4bc..e066be774 100644 --- a/openstack/tests/functional/network/v2/test_network_ip_availability.py +++ b/openstack/tests/functional/network/v2/test_network_ip_availability.py @@ -27,48 +27,65 @@ class TestNetworkIPAvailability(base.BaseFunctionalTest): def setUp(self): super(TestNetworkIPAvailability, self).setUp() + if not self.operator_cloud: + self.skipTest("Operator cloud required for this test") + if not self.operator_cloud._has_neutron_extension( + "network-ip-availability" + ): + self.skipTest( + "Neutron network-ip-availability extension is required " + "for this test" + ) + self.NET_NAME = self.getUniqueString() self.SUB_NAME = self.getUniqueString() self.PORT_NAME = self.getUniqueString() self.UPDATE_NAME = self.getUniqueString() - net = self.conn.network.create_network(name=self.NET_NAME) + net = self.operator_cloud.network.create_network(name=self.NET_NAME) assert isinstance(net, network.Network) self.assertEqual(self.NET_NAME, net.name) self.NET_ID = net.id - sub = self.conn.network.create_subnet( + sub = self.operator_cloud.network.create_subnet( name=self.SUB_NAME, ip_version=self.IPV4, network_id=self.NET_ID, - cidr=self.CIDR) + cidr=self.CIDR, + ) assert isinstance(sub, subnet.Subnet) self.assertEqual(self.SUB_NAME, sub.name) self.SUB_ID = sub.id - prt = self.conn.network.create_port( - name=self.PORT_NAME, - network_id=self.NET_ID) + prt = self.operator_cloud.network.create_port( + name=self.PORT_NAME, network_id=self.NET_ID + ) assert isinstance(prt, port.Port) self.assertEqual(self.PORT_NAME, prt.name) self.PORT_ID = prt.id def tearDown(self): - sot = self.conn.network.delete_port(self.PORT_ID) + sot = self.operator_cloud.network.delete_port(self.PORT_ID) self.assertIsNone(sot) - sot = self.conn.network.delete_subnet(self.SUB_ID) + sot = self.operator_cloud.network.delete_subnet(self.SUB_ID) self.assertIsNone(sot) - sot = self.conn.network.delete_network(self.NET_ID) + sot = self.operator_cloud.network.delete_network(self.NET_ID) self.assertIsNone(sot) super(TestNetworkIPAvailability, self).tearDown() def test_find(self): - sot = self.conn.network.find_network_ip_availability(self.NET_ID) + sot = self.operator_cloud.network.find_network_ip_availability( + self.NET_ID + ) self.assertEqual(self.NET_ID, sot.network_id) def test_get(self): - sot = self.conn.network.get_network_ip_availability(self.NET_ID) + sot = self.operator_cloud.network.get_network_ip_availability( + self.NET_ID + ) self.assertEqual(self.NET_ID, sot.network_id) self.assertEqual(self.NET_NAME, sot.network_name) def test_list(self): - ids = [o.network_id for o in - self.conn.network.network_ip_availabilities()] + ids = [ + o.network_id + for o in self.operator_cloud.network.network_ip_availabilities() + ] self.assertIn(self.NET_ID, ids) diff --git a/openstack/tests/functional/network/v2/test_network_segment_range.py b/openstack/tests/functional/network/v2/test_network_segment_range.py index d626d72f0..314aa6f0f 100644 --- a/openstack/tests/functional/network/v2/test_network_segment_range.py +++ b/openstack/tests/functional/network/v2/test_network_segment_range.py @@ -20,44 +20,52 @@ class TestNetworkSegmentRange(base.BaseFunctionalTest): NETWORK_SEGMENT_RANGE_ID = None - NAME = 'test_name' + NAME = "test_name" DEFAULT = False SHARED = False - PROJECT_ID = '2018' - NETWORK_TYPE = 'vlan' - PHYSICAL_NETWORK = 'phys_net' + PROJECT_ID = "2018" + NETWORK_TYPE = "vlan" + PHYSICAL_NETWORK = "phys_net" MINIMUM = 100 MAXIMUM = 200 def setUp(self): super(TestNetworkSegmentRange, self).setUp() + if not self.operator_cloud: + self.skipTest("Operator cloud required for this test") # NOTE(kailun): The network segment range extension is not yet enabled # by default. # Skip the tests if not enabled. - if not self.conn.network.find_extension('network-segment-range'): - self.skipTest('Network Segment Range extension disabled') + if not self.operator_cloud.network.find_extension( + "network-segment-range" + ): + self.skipTest("Network Segment Range extension disabled") - test_seg_range = self.conn.network.create_network_segment_range( - name=self.NAME, - default=self.DEFAULT, - shared=self.SHARED, - project_id=self.PROJECT_ID, - network_type=self.NETWORK_TYPE, - physical_network=self.PHYSICAL_NETWORK, - minimum=self.MINIMUM, - maximum=self.MAXIMUM, + test_seg_range = ( + self.operator_cloud.network.create_network_segment_range( + name=self.NAME, + default=self.DEFAULT, + shared=self.SHARED, + project_id=self.PROJECT_ID, + network_type=self.NETWORK_TYPE, + physical_network=self.PHYSICAL_NETWORK, + minimum=self.MINIMUM, + maximum=self.MAXIMUM, + ) + ) + self.assertIsInstance( + test_seg_range, network_segment_range.NetworkSegmentRange ) - self.assertIsInstance(test_seg_range, - network_segment_range.NetworkSegmentRange) self.NETWORK_SEGMENT_RANGE_ID = test_seg_range.id self.assertEqual(self.NAME, test_seg_range.name) self.assertEqual(self.DEFAULT, test_seg_range.default) self.assertEqual(self.SHARED, test_seg_range.shared) self.assertEqual(self.PROJECT_ID, test_seg_range.project_id) self.assertEqual(self.NETWORK_TYPE, test_seg_range.network_type) - self.assertEqual(self.PHYSICAL_NETWORK, - test_seg_range.physical_network) + self.assertEqual( + self.PHYSICAL_NETWORK, test_seg_range.physical_network + ) self.assertEqual(self.MINIMUM, test_seg_range.minimum) self.assertEqual(self.MAXIMUM, test_seg_range.maximum) @@ -65,35 +73,48 @@ def tearDown(self): super(TestNetworkSegmentRange, self).tearDown() def test_create_delete(self): - del_test_seg_range = self.conn.network.delete_network_segment_range( - self.NETWORK_SEGMENT_RANGE_ID) + del_test_seg_range = ( + self.operator_cloud.network.delete_network_segment_range( + self.NETWORK_SEGMENT_RANGE_ID + ) + ) self.assertIsNone(del_test_seg_range) def test_find(self): - test_seg_range = self.conn.network.find_network_segment_range( - self.NETWORK_SEGMENT_RANGE_ID) + test_seg_range = ( + self.operator_cloud.network.find_network_segment_range( + self.NETWORK_SEGMENT_RANGE_ID + ) + ) self.assertEqual(self.NETWORK_SEGMENT_RANGE_ID, test_seg_range.id) def test_get(self): - test_seg_range = self.conn.network.get_network_segment_range( - self.NETWORK_SEGMENT_RANGE_ID) + test_seg_range = self.operator_cloud.network.get_network_segment_range( + self.NETWORK_SEGMENT_RANGE_ID + ) self.assertEqual(self.NETWORK_SEGMENT_RANGE_ID, test_seg_range.id) self.assertEqual(self.NAME, test_seg_range.name) self.assertEqual(self.DEFAULT, test_seg_range.default) self.assertEqual(self.SHARED, test_seg_range.shared) self.assertEqual(self.PROJECT_ID, test_seg_range.project_id) self.assertEqual(self.NETWORK_TYPE, test_seg_range.network_type) - self.assertEqual(self.PHYSICAL_NETWORK, - test_seg_range.physical_network) + self.assertEqual( + self.PHYSICAL_NETWORK, test_seg_range.physical_network + ) self.assertEqual(self.MINIMUM, test_seg_range.minimum) self.assertEqual(self.MAXIMUM, test_seg_range.maximum) def test_list(self): - ids = [o.id for o in self.conn.network.network_segment_ranges( - name=None)] + ids = [ + o.id + for o in self.operator_cloud.network.network_segment_ranges( + name=None + ) + ] self.assertIn(self.NETWORK_SEGMENT_RANGE_ID, ids) def test_update(self): - update_seg_range = self.conn.network.update_segment( - self.NETWORK_SEGMENT_RANGE_ID, name='update_test_name') - self.assertEqual('update_test_name', update_seg_range.name) + update_seg_range = self.operator_cloud.network.update_segment( + self.NETWORK_SEGMENT_RANGE_ID, name="update_test_name" + ) + self.assertEqual("update_test_name", update_seg_range.name) diff --git a/openstack/tests/functional/network/v2/test_port.py b/openstack/tests/functional/network/v2/test_port.py index 346e919f7..12a281c26 100644 --- a/openstack/tests/functional/network/v2/test_port.py +++ b/openstack/tests/functional/network/v2/test_port.py @@ -31,64 +31,69 @@ def setUp(self): self.SUB_NAME = self.getUniqueString() self.PORT_NAME = self.getUniqueString() self.UPDATE_NAME = self.getUniqueString() - net = self.conn.network.create_network(name=self.NET_NAME) + net = self.user_cloud.network.create_network(name=self.NET_NAME) assert isinstance(net, network.Network) self.assertEqual(self.NET_NAME, net.name) self.NET_ID = net.id - sub = self.conn.network.create_subnet( + sub = self.user_cloud.network.create_subnet( name=self.SUB_NAME, ip_version=self.IPV4, network_id=self.NET_ID, - cidr=self.CIDR) + cidr=self.CIDR, + ) assert isinstance(sub, subnet.Subnet) self.assertEqual(self.SUB_NAME, sub.name) self.SUB_ID = sub.id - prt = self.conn.network.create_port( - name=self.PORT_NAME, - network_id=self.NET_ID) + prt = self.user_cloud.network.create_port( + name=self.PORT_NAME, network_id=self.NET_ID + ) assert isinstance(prt, port.Port) self.assertEqual(self.PORT_NAME, prt.name) self.PORT_ID = prt.id def tearDown(self): - sot = self.conn.network.delete_port( - self.PORT_ID, ignore_missing=False) + sot = self.user_cloud.network.delete_port( + self.PORT_ID, ignore_missing=False + ) self.assertIsNone(sot) - sot = self.conn.network.delete_subnet( - self.SUB_ID, ignore_missing=False) + sot = self.user_cloud.network.delete_subnet( + self.SUB_ID, ignore_missing=False + ) self.assertIsNone(sot) - sot = self.conn.network.delete_network( - self.NET_ID, ignore_missing=False) + sot = self.user_cloud.network.delete_network( + self.NET_ID, ignore_missing=False + ) self.assertIsNone(sot) super(TestPort, self).tearDown() def test_find(self): - sot = self.conn.network.find_port(self.PORT_NAME) + sot = self.user_cloud.network.find_port(self.PORT_NAME) self.assertEqual(self.PORT_ID, sot.id) def test_get(self): - sot = self.conn.network.get_port(self.PORT_ID) + sot = self.user_cloud.network.get_port(self.PORT_ID) self.assertEqual(self.PORT_ID, sot.id) self.assertEqual(self.PORT_NAME, sot.name) self.assertEqual(self.NET_ID, sot.network_id) def test_list(self): - ids = [o.id for o in self.conn.network.ports()] + ids = [o.id for o in self.user_cloud.network.ports()] self.assertIn(self.PORT_ID, ids) def test_update(self): - sot = self.conn.network.update_port(self.PORT_ID, - name=self.UPDATE_NAME) + sot = self.user_cloud.network.update_port( + self.PORT_ID, name=self.UPDATE_NAME + ) self.assertEqual(self.UPDATE_NAME, sot.name) def test_set_tags(self): - sot = self.conn.network.get_port(self.PORT_ID) + sot = self.user_cloud.network.get_port(self.PORT_ID) self.assertEqual([], sot.tags) - self.conn.network.set_tags(sot, ['blue']) - sot = self.conn.network.get_port(self.PORT_ID) - self.assertEqual(['blue'], sot.tags) + self.user_cloud.network.set_tags(sot, ["blue"]) + sot = self.user_cloud.network.get_port(self.PORT_ID) + self.assertEqual(["blue"], sot.tags) - self.conn.network.set_tags(sot, []) - sot = self.conn.network.get_port(self.PORT_ID) + self.user_cloud.network.set_tags(sot, []) + sot = self.user_cloud.network.get_port(self.PORT_ID) self.assertEqual([], sot.tags) diff --git a/openstack/tests/functional/network/v2/test_port_forwarding.py b/openstack/tests/functional/network/v2/test_port_forwarding.py index d0a7cee14..3afe3f9b5 100644 --- a/openstack/tests/functional/network/v2/test_port_forwarding.py +++ b/openstack/tests/functional/network/v2/test_port_forwarding.py @@ -37,97 +37,122 @@ class TestPortForwarding(base.BaseFunctionalTest): INTERNAL_PORT = 8080 EXTERNAL_PORT = 80 PROTOCOL = "tcp" - DESCRIPTION = 'description' + DESCRIPTION = "description" def setUp(self): super(TestPortForwarding, self).setUp() - if not self.conn.network.find_extension('floating-ip-port-forwarding'): - self.skipTest('Floating IP Port Forwarding extension disabled') + if not self.user_cloud._has_neutron_extension("external-net"): + self.skipTest( + "Neutron external-net extension is required for this test" + ) + if not self.user_cloud.network.find_extension( + "floating-ip-port-forwarding" + ): + self.skipTest("Floating IP Port Forwarding extension disabled") self.ROT_NAME = self.getUniqueString() - self.EXT_NET_NAME = self.getUniqueString() - self.EXT_SUB_NAME = self.getUniqueString() self.INT_NET_NAME = self.getUniqueString() self.INT_SUB_NAME = self.getUniqueString() - # Create Exeternal Network - args = {'router:external': True} - net = self._create_network(self.EXT_NET_NAME, **args) - self.EXT_NET_ID = net.id - sub = self._create_subnet( - self.EXT_SUB_NAME, self.EXT_NET_ID, self.EXT_CIDR) - self.EXT_SUB_ID = sub.id + self.EXT_NET_ID = None + self.EXT_SUB_ID = None + + # Find External Network + for net in self.user_cloud.network.networks(is_router_external=True): + self.EXT_NET_ID = net.id + # Find subnet of the chosen external net + for sub in self.user_cloud.network.subnets(network_id=self.EXT_NET_ID): + self.EXT_SUB_ID = sub.id + if not self.EXT_NET_ID and self.operator_cloud: + # There is no existing external net, but operator + # credentials available + # WARNING: this external net is not dropped + # Create External Network + args = {"router:external": True} + net = self._create_network(self.EXT_NET_NAME, **args) + self.EXT_NET_ID = net.id + sub = self._create_subnet( + self.EXT_SUB_NAME, self.EXT_NET_ID, self.EXT_CIDR + ) + self.EXT_SUB_ID = sub.id + # Create Internal Network net = self._create_network(self.INT_NET_NAME) self.INT_NET_ID = net.id sub = self._create_subnet( - self.INT_SUB_NAME, self.INT_NET_ID, self.INT_CIDR) + self.INT_SUB_NAME, self.INT_NET_ID, self.INT_CIDR + ) self.INT_SUB_ID = sub.id # Create Router - args = {'external_gateway_info': {'network_id': self.EXT_NET_ID}} - sot = self.conn.network.create_router(name=self.ROT_NAME, **args) + args = {"external_gateway_info": {"network_id": self.EXT_NET_ID}} + sot = self.user_cloud.network.create_router(name=self.ROT_NAME, **args) assert isinstance(sot, router.Router) self.assertEqual(self.ROT_NAME, sot.name) self.ROT_ID = sot.id self.ROT = sot # Add Router's Interface to Internal Network sot = self.ROT.add_interface( - self.conn.network, subnet_id=self.INT_SUB_ID) - self.assertEqual(sot['subnet_id'], self.INT_SUB_ID) + self.user_cloud.network, subnet_id=self.INT_SUB_ID + ) + self.assertEqual(sot["subnet_id"], self.INT_SUB_ID) # Create Port in Internal Network - prt = self.conn.network.create_port(network_id=self.INT_NET_ID) + prt = self.user_cloud.network.create_port(network_id=self.INT_NET_ID) assert isinstance(prt, port.Port) self.INTERNAL_PORT_ID = prt.id - self.INTERNAL_IP_ADDRESS = prt.fixed_ips[0]['ip_address'] + self.INTERNAL_IP_ADDRESS = prt.fixed_ips[0]["ip_address"] # Create Floating IP. - fip = self.conn.network.create_ip( - floating_network_id=self.EXT_NET_ID) + fip = self.user_cloud.network.create_ip( + floating_network_id=self.EXT_NET_ID + ) assert isinstance(fip, floating_ip.FloatingIP) self.FIP_ID = fip.id # Create Port Forwarding - pf = self.conn.network.create_port_forwarding( + pf = self.user_cloud.network.create_port_forwarding( floatingip_id=self.FIP_ID, internal_port_id=self.INTERNAL_PORT_ID, internal_ip_address=self.INTERNAL_IP_ADDRESS, internal_port=self.INTERNAL_PORT, external_port=self.EXTERNAL_PORT, protocol=self.PROTOCOL, - description=self.DESCRIPTION) + description=self.DESCRIPTION, + ) assert isinstance(pf, _port_forwarding.PortForwarding) self.PF = pf def tearDown(self): - sot = self.conn.network.delete_port_forwarding( - self.PF, self.FIP_ID, ignore_missing=False) + sot = self.user_cloud.network.delete_port_forwarding( + self.PF, self.FIP_ID, ignore_missing=False + ) self.assertIsNone(sot) - sot = self.conn.network.delete_ip(self.FIP_ID, ignore_missing=False) + sot = self.user_cloud.network.delete_ip( + self.FIP_ID, ignore_missing=False + ) self.assertIsNone(sot) - sot = self.conn.network.delete_port( - self.INTERNAL_PORT_ID, ignore_missing=False) + sot = self.user_cloud.network.delete_port( + self.INTERNAL_PORT_ID, ignore_missing=False + ) self.assertIsNone(sot) sot = self.ROT.remove_interface( - self.conn.network, subnet_id=self.INT_SUB_ID) - self.assertEqual(sot['subnet_id'], self.INT_SUB_ID) - sot = self.conn.network.delete_router( - self.ROT_ID, ignore_missing=False) - self.assertIsNone(sot) - sot = self.conn.network.delete_subnet( - self.EXT_SUB_ID, ignore_missing=False) - self.assertIsNone(sot) - sot = self.conn.network.delete_network( - self.EXT_NET_ID, ignore_missing=False) + self.user_cloud.network, subnet_id=self.INT_SUB_ID + ) + self.assertEqual(sot["subnet_id"], self.INT_SUB_ID) + sot = self.user_cloud.network.delete_router( + self.ROT_ID, ignore_missing=False + ) self.assertIsNone(sot) - sot = self.conn.network.delete_subnet( - self.INT_SUB_ID, ignore_missing=False) + sot = self.user_cloud.network.delete_subnet( + self.INT_SUB_ID, ignore_missing=False + ) self.assertIsNone(sot) - sot = self.conn.network.delete_network( - self.INT_NET_ID, ignore_missing=False) + sot = self.user_cloud.network.delete_network( + self.INT_NET_ID, ignore_missing=False + ) self.assertIsNone(sot) super(TestPortForwarding, self).tearDown() def _create_network(self, name, **args): self.name = name - net = self.conn.network.create_network(name=name, **args) + net = self.user_cloud.network.create_network(name=name, **args) assert isinstance(net, network.Network) self.assertEqual(self.name, net.name) return net @@ -136,18 +161,20 @@ def _create_subnet(self, name, net_id, cidr): self.name = name self.net_id = net_id self.cidr = cidr - sub = self.conn.network.create_subnet( + sub = self.user_cloud.network.create_subnet( name=self.name, ip_version=self.IPV4, network_id=self.net_id, - cidr=self.cidr) + cidr=self.cidr, + ) assert isinstance(sub, subnet.Subnet) self.assertEqual(self.name, sub.name) return sub def test_find(self): - sot = self.conn.network.find_port_forwarding( - self.PF.id, self.FIP_ID) + sot = self.user_cloud.network.find_port_forwarding( + self.PF.id, self.FIP_ID + ) self.assertEqual(self.INTERNAL_PORT_ID, sot.internal_port_id) self.assertEqual(self.INTERNAL_IP_ADDRESS, sot.internal_ip_address) self.assertEqual(self.INTERNAL_PORT, sot.internal_port) @@ -156,8 +183,7 @@ def test_find(self): self.assertEqual(self.DESCRIPTION, sot.description) def test_get(self): - sot = self.conn.network.get_port_forwarding( - self.PF, self.FIP_ID) + sot = self.user_cloud.network.get_port_forwarding(self.PF, self.FIP_ID) self.assertEqual(self.INTERNAL_PORT_ID, sot.internal_port_id) self.assertEqual(self.INTERNAL_IP_ADDRESS, sot.internal_ip_address) self.assertEqual(self.INTERNAL_PORT, sot.internal_port) @@ -166,14 +192,14 @@ def test_get(self): self.assertEqual(self.DESCRIPTION, sot.description) def test_list(self): - pf_ids = [o.id for o in - self.conn.network.port_forwardings(self.FIP_ID)] + pf_ids = [ + o.id for o in self.user_cloud.network.port_forwardings(self.FIP_ID) + ] self.assertIn(self.PF.id, pf_ids) def test_update(self): NEW_EXTERNAL_PORT = 90 - sot = self.conn.network.update_port_forwarding( - self.PF.id, - self.FIP_ID, - external_port=NEW_EXTERNAL_PORT) + sot = self.user_cloud.network.update_port_forwarding( + self.PF.id, self.FIP_ID, external_port=NEW_EXTERNAL_PORT + ) self.assertEqual(NEW_EXTERNAL_PORT, sot.external_port) diff --git a/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py b/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py index 118ca9ce4..edd5fb1d1 100644 --- a/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py @@ -11,8 +11,9 @@ # under the License. -from openstack.network.v2 import (qos_bandwidth_limit_rule as - _qos_bandwidth_limit_rule) +from openstack.network.v2 import ( + qos_bandwidth_limit_rule as _qos_bandwidth_limit_rule +) from openstack.tests.functional import base @@ -25,58 +26,67 @@ class TestQoSBandwidthLimitRule(base.BaseFunctionalTest): RULE_MAX_KBPS_NEW = 1800 RULE_MAX_BURST_KBPS = 1100 RULE_MAX_BURST_KBPS_NEW = 1300 - RULE_DIRECTION = 'egress' - RULE_DIRECTION_NEW = 'ingress' + RULE_DIRECTION = "egress" + RULE_DIRECTION_NEW = "ingress" def setUp(self): super(TestQoSBandwidthLimitRule, self).setUp() + if not self.operator_cloud: + self.skipTest("Operator cloud required for this test") + # Skip the tests if qos-bw-limit-direction extension is not enabled. - if not self.conn.network.find_extension('qos-bw-limit-direction'): - self.skipTest('Network qos-bw-limit-direction extension disabled') + if not self.operator_cloud.network.find_extension( + "qos-bw-limit-direction" + ): + self.skipTest("Network qos-bw-limit-direction extension disabled") self.QOS_POLICY_NAME = self.getUniqueString() self.RULE_ID = self.getUniqueString() - qos_policy = self.conn.network.create_qos_policy( + qos_policy = self.operator_cloud.network.create_qos_policy( description=self.QOS_POLICY_DESCRIPTION, name=self.QOS_POLICY_NAME, shared=self.QOS_IS_SHARED, ) self.QOS_POLICY_ID = qos_policy.id - qos_rule = self.conn.network.create_qos_bandwidth_limit_rule( - self.QOS_POLICY_ID, max_kbps=self.RULE_MAX_KBPS, + qos_rule = self.operator_cloud.network.create_qos_bandwidth_limit_rule( + self.QOS_POLICY_ID, + max_kbps=self.RULE_MAX_KBPS, max_burst_kbps=self.RULE_MAX_BURST_KBPS, direction=self.RULE_DIRECTION, ) - assert isinstance(qos_rule, - _qos_bandwidth_limit_rule.QoSBandwidthLimitRule) + assert isinstance( + qos_rule, _qos_bandwidth_limit_rule.QoSBandwidthLimitRule + ) self.assertEqual(self.RULE_MAX_KBPS, qos_rule.max_kbps) self.assertEqual(self.RULE_MAX_BURST_KBPS, qos_rule.max_burst_kbps) self.assertEqual(self.RULE_DIRECTION, qos_rule.direction) self.RULE_ID = qos_rule.id def tearDown(self): - rule = self.conn.network.delete_qos_minimum_bandwidth_rule( - self.RULE_ID, - self.QOS_POLICY_ID) - qos_policy = self.conn.network.delete_qos_policy(self.QOS_POLICY_ID) + rule = self.operator_cloud.network.delete_qos_minimum_bandwidth_rule( + self.RULE_ID, self.QOS_POLICY_ID + ) + qos_policy = self.operator_cloud.network.delete_qos_policy( + self.QOS_POLICY_ID + ) self.assertIsNone(rule) self.assertIsNone(qos_policy) super(TestQoSBandwidthLimitRule, self).tearDown() def test_find(self): - sot = self.conn.network.find_qos_bandwidth_limit_rule( - self.RULE_ID, - self.QOS_POLICY_ID) + sot = self.operator_cloud.network.find_qos_bandwidth_limit_rule( + self.RULE_ID, self.QOS_POLICY_ID + ) self.assertEqual(self.RULE_ID, sot.id) self.assertEqual(self.RULE_MAX_KBPS, sot.max_kbps) self.assertEqual(self.RULE_MAX_BURST_KBPS, sot.max_burst_kbps) self.assertEqual(self.RULE_DIRECTION, sot.direction) def test_get(self): - sot = self.conn.network.get_qos_bandwidth_limit_rule( - self.RULE_ID, - self.QOS_POLICY_ID) + sot = self.operator_cloud.network.get_qos_bandwidth_limit_rule( + self.RULE_ID, self.QOS_POLICY_ID + ) self.assertEqual(self.RULE_ID, sot.id) self.assertEqual(self.QOS_POLICY_ID, sot.qos_policy_id) self.assertEqual(self.RULE_MAX_KBPS, sot.max_kbps) @@ -84,18 +94,22 @@ def test_get(self): self.assertEqual(self.RULE_DIRECTION, sot.direction) def test_list(self): - rule_ids = [o.id for o in - self.conn.network.qos_bandwidth_limit_rules( - self.QOS_POLICY_ID)] + rule_ids = [ + o.id + for o in self.operator_cloud.network.qos_bandwidth_limit_rules( + self.QOS_POLICY_ID + ) + ] self.assertIn(self.RULE_ID, rule_ids) def test_update(self): - sot = self.conn.network.update_qos_bandwidth_limit_rule( + sot = self.operator_cloud.network.update_qos_bandwidth_limit_rule( self.RULE_ID, self.QOS_POLICY_ID, max_kbps=self.RULE_MAX_KBPS_NEW, max_burst_kbps=self.RULE_MAX_BURST_KBPS_NEW, - direction=self.RULE_DIRECTION_NEW) + direction=self.RULE_DIRECTION_NEW, + ) self.assertEqual(self.RULE_MAX_KBPS_NEW, sot.max_kbps) self.assertEqual(self.RULE_MAX_BURST_KBPS_NEW, sot.max_burst_kbps) self.assertEqual(self.RULE_DIRECTION_NEW, sot.direction) diff --git a/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py b/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py index 63dd7d1fb..6eca0cc38 100644 --- a/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py +++ b/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py @@ -11,8 +11,9 @@ # under the License. -from openstack.network.v2 import (qos_dscp_marking_rule as - _qos_dscp_marking_rule) +from openstack.network.v2 import ( + qos_dscp_marking_rule as _qos_dscp_marking_rule +) from openstack.tests.functional import base @@ -27,9 +28,12 @@ class TestQoSDSCPMarkingRule(base.BaseFunctionalTest): def setUp(self): super(TestQoSDSCPMarkingRule, self).setUp() + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") + # Skip the tests if qos extension is not enabled. - if not self.conn.network.find_extension('qos'): - self.skipTest('Network qos extension disabled') + if not self.conn.network.find_extension("qos"): + self.skipTest("Network qos extension disabled") self.QOS_POLICY_NAME = self.getUniqueString() self.RULE_ID = self.getUniqueString() @@ -40,7 +44,8 @@ def setUp(self): ) self.QOS_POLICY_ID = qos_policy.id qos_rule = self.conn.network.create_qos_dscp_marking_rule( - self.QOS_POLICY_ID, dscp_mark=self.RULE_DSCP_MARK, + self.QOS_POLICY_ID, + dscp_mark=self.RULE_DSCP_MARK, ) assert isinstance(qos_rule, _qos_dscp_marking_rule.QoSDSCPMarkingRule) self.assertEqual(self.RULE_DSCP_MARK, qos_rule.dscp_mark) @@ -48,8 +53,8 @@ def setUp(self): def tearDown(self): rule = self.conn.network.delete_qos_minimum_bandwidth_rule( - self.RULE_ID, - self.QOS_POLICY_ID) + self.RULE_ID, self.QOS_POLICY_ID + ) qos_policy = self.conn.network.delete_qos_policy(self.QOS_POLICY_ID) self.assertIsNone(rule) self.assertIsNone(qos_policy) @@ -57,28 +62,30 @@ def tearDown(self): def test_find(self): sot = self.conn.network.find_qos_dscp_marking_rule( - self.RULE_ID, - self.QOS_POLICY_ID) + self.RULE_ID, self.QOS_POLICY_ID + ) self.assertEqual(self.RULE_ID, sot.id) self.assertEqual(self.RULE_DSCP_MARK, sot.dscp_mark) def test_get(self): sot = self.conn.network.get_qos_dscp_marking_rule( - self.RULE_ID, - self.QOS_POLICY_ID) + self.RULE_ID, self.QOS_POLICY_ID + ) self.assertEqual(self.RULE_ID, sot.id) self.assertEqual(self.QOS_POLICY_ID, sot.qos_policy_id) self.assertEqual(self.RULE_DSCP_MARK, sot.dscp_mark) def test_list(self): - rule_ids = [o.id for o in - self.conn.network.qos_dscp_marking_rules( - self.QOS_POLICY_ID)] + rule_ids = [ + o.id + for o in self.conn.network.qos_dscp_marking_rules( + self.QOS_POLICY_ID + ) + ] self.assertIn(self.RULE_ID, rule_ids) def test_update(self): sot = self.conn.network.update_qos_dscp_marking_rule( - self.RULE_ID, - self.QOS_POLICY_ID, - dscp_mark=self.RULE_DSCP_MARK_NEW) + self.RULE_ID, self.QOS_POLICY_ID, dscp_mark=self.RULE_DSCP_MARK_NEW + ) self.assertEqual(self.RULE_DSCP_MARK_NEW, sot.dscp_mark) diff --git a/openstack/tests/functional/network/v2/test_qos_minimum_bandwidth_rule.py b/openstack/tests/functional/network/v2/test_qos_minimum_bandwidth_rule.py index b53f875db..5efbc8c2c 100644 --- a/openstack/tests/functional/network/v2/test_qos_minimum_bandwidth_rule.py +++ b/openstack/tests/functional/network/v2/test_qos_minimum_bandwidth_rule.py @@ -11,8 +11,9 @@ # under the License. -from openstack.network.v2 import (qos_minimum_bandwidth_rule as - _qos_minimum_bandwidth_rule) +from openstack.network.v2 import ( + qos_minimum_bandwidth_rule as _qos_minimum_bandwidth_rule +) from openstack.tests.functional import base @@ -24,67 +25,78 @@ class TestQoSMinimumBandwidthRule(base.BaseFunctionalTest): RULE_ID = None RULE_MIN_KBPS = 1200 RULE_MIN_KBPS_NEW = 1800 - RULE_DIRECTION = 'egress' + RULE_DIRECTION = "egress" def setUp(self): super(TestQoSMinimumBandwidthRule, self).setUp() + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") + # Skip the tests if qos-bw-limit-direction extension is not enabled. - if not self.conn.network.find_extension('qos-bw-limit-direction'): - self.skipTest('Network qos-bw-limit-direction extension disabled') + if not self.operator_cloud.network.find_extension( + "qos-bw-limit-direction"): + self.skipTest("Network qos-bw-limit-direction extension disabled") self.QOS_POLICY_NAME = self.getUniqueString() - qos_policy = self.conn.network.create_qos_policy( + qos_policy = self.operator_cloud.network.create_qos_policy( description=self.QOS_POLICY_DESCRIPTION, name=self.QOS_POLICY_NAME, shared=self.QOS_IS_SHARED, ) self.QOS_POLICY_ID = qos_policy.id - qos_min_bw_rule = self.conn.network.create_qos_minimum_bandwidth_rule( - self.QOS_POLICY_ID, direction=self.RULE_DIRECTION, - min_kbps=self.RULE_MIN_KBPS, + qos_min_bw_rule = self.operator_cloud.network \ + .create_qos_minimum_bandwidth_rule( + self.QOS_POLICY_ID, + direction=self.RULE_DIRECTION, + min_kbps=self.RULE_MIN_KBPS, + ) + assert isinstance( + qos_min_bw_rule, + _qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, ) - assert isinstance(qos_min_bw_rule, - _qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule) self.assertEqual(self.RULE_MIN_KBPS, qos_min_bw_rule.min_kbps) self.assertEqual(self.RULE_DIRECTION, qos_min_bw_rule.direction) self.RULE_ID = qos_min_bw_rule.id def tearDown(self): - rule = self.conn.network.delete_qos_minimum_bandwidth_rule( - self.RULE_ID, + rule = self.operator_cloud.network.delete_qos_minimum_bandwidth_rule( + self.RULE_ID, self.QOS_POLICY_ID + ) + qos_policy = self.operator_cloud.network.delete_qos_policy( self.QOS_POLICY_ID) - qos_policy = self.conn.network.delete_qos_policy(self.QOS_POLICY_ID) self.assertIsNone(rule) self.assertIsNone(qos_policy) super(TestQoSMinimumBandwidthRule, self).tearDown() def test_find(self): - sot = self.conn.network.find_qos_minimum_bandwidth_rule( - self.RULE_ID, - self.QOS_POLICY_ID) + sot = self.operator_cloud.network.find_qos_minimum_bandwidth_rule( + self.RULE_ID, self.QOS_POLICY_ID + ) self.assertEqual(self.RULE_ID, sot.id) self.assertEqual(self.RULE_DIRECTION, sot.direction) self.assertEqual(self.RULE_MIN_KBPS, sot.min_kbps) def test_get(self): - sot = self.conn.network.get_qos_minimum_bandwidth_rule( - self.RULE_ID, - self.QOS_POLICY_ID) + sot = self.operator_cloud.network.get_qos_minimum_bandwidth_rule( + self.RULE_ID, self.QOS_POLICY_ID + ) self.assertEqual(self.RULE_ID, sot.id) self.assertEqual(self.QOS_POLICY_ID, sot.qos_policy_id) self.assertEqual(self.RULE_DIRECTION, sot.direction) self.assertEqual(self.RULE_MIN_KBPS, sot.min_kbps) def test_list(self): - rule_ids = [o.id for o in - self.conn.network.qos_minimum_bandwidth_rules( - self.QOS_POLICY_ID)] + rule_ids = [ + o.id + for o in self.operator_cloud.network.qos_minimum_bandwidth_rules( + self.QOS_POLICY_ID + ) + ] self.assertIn(self.RULE_ID, rule_ids) def test_update(self): - sot = self.conn.network.update_qos_minimum_bandwidth_rule( - self.RULE_ID, - self.QOS_POLICY_ID, - min_kbps=self.RULE_MIN_KBPS_NEW) + sot = self.operator_cloud.network.update_qos_minimum_bandwidth_rule( + self.RULE_ID, self.QOS_POLICY_ID, min_kbps=self.RULE_MIN_KBPS_NEW + ) self.assertEqual(self.RULE_MIN_KBPS_NEW, sot.min_kbps) diff --git a/openstack/tests/functional/network/v2/test_qos_minimum_packet_rate_rule.py b/openstack/tests/functional/network/v2/test_qos_minimum_packet_rate_rule.py index 34e2debbd..c3aa77070 100644 --- a/openstack/tests/functional/network/v2/test_qos_minimum_packet_rate_rule.py +++ b/openstack/tests/functional/network/v2/test_qos_minimum_packet_rate_rule.py @@ -11,8 +11,9 @@ # under the License. -from openstack.network.v2 import (qos_minimum_packet_rate_rule as - _qos_minimum_packet_rate_rule) +from openstack.network.v2 import ( + qos_minimum_packet_rate_rule as _qos_minimum_packet_rate_rule +) from openstack.tests.functional import base @@ -24,71 +25,84 @@ class TestQoSMinimumPacketRateRule(base.BaseFunctionalTest): RULE_ID = None RULE_MIN_KPPS = 1200 RULE_MIN_KPPS_NEW = 1800 - RULE_DIRECTION = 'egress' - RULE_DIRECTION_NEW = 'ingress' + RULE_DIRECTION = "egress" + RULE_DIRECTION_NEW = "ingress" def setUp(self): super(TestQoSMinimumPacketRateRule, self).setUp() + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") + # Skip the tests if qos-pps-minimum extension is not enabled. - if not self.conn.network.find_extension('qos-pps-minimum'): - self.skipTest('Network qos-pps-minimum extension disabled') + if not self.operator_cloud.network.find_extension("qos-pps-minimum"): + self.skipTest("Network qos-pps-minimum extension disabled") self.QOS_POLICY_NAME = self.getUniqueString() - qos_policy = self.conn.network.create_qos_policy( + qos_policy = self.operator_cloud.network.create_qos_policy( description=self.QOS_POLICY_DESCRIPTION, name=self.QOS_POLICY_NAME, shared=self.QOS_IS_SHARED, ) self.QOS_POLICY_ID = qos_policy.id qos_min_pps_rule = ( - self.conn.network.create_qos_minimum_packet_rate_rule( - self.QOS_POLICY_ID, direction=self.RULE_DIRECTION, - min_kpps=self.RULE_MIN_KPPS)) + self.operator_cloud.network.create_qos_minimum_packet_rate_rule( + self.QOS_POLICY_ID, + direction=self.RULE_DIRECTION, + min_kpps=self.RULE_MIN_KPPS, + ) + ) assert isinstance( qos_min_pps_rule, - _qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule) + _qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule, + ) self.assertEqual(self.RULE_MIN_KPPS, qos_min_pps_rule.min_kpps) self.assertEqual(self.RULE_DIRECTION, qos_min_pps_rule.direction) self.RULE_ID = qos_min_pps_rule.id def tearDown(self): - rule = self.conn.network.delete_qos_minimum_packet_rate_rule( - self.RULE_ID, - self.QOS_POLICY_ID) - qos_policy = self.conn.network.delete_qos_policy(self.QOS_POLICY_ID) + rule = self.operator_cloud.network.delete_qos_minimum_packet_rate_rule( + self.RULE_ID, self.QOS_POLICY_ID + ) + qos_policy = self.operator_cloud.network.delete_qos_policy( + self.QOS_POLICY_ID + ) self.assertIsNone(rule) self.assertIsNone(qos_policy) super(TestQoSMinimumPacketRateRule, self).tearDown() def test_find(self): - sot = self.conn.network.find_qos_minimum_packet_rate_rule( - self.RULE_ID, - self.QOS_POLICY_ID) + sot = self.operator_cloud.network.find_qos_minimum_packet_rate_rule( + self.RULE_ID, self.QOS_POLICY_ID + ) self.assertEqual(self.RULE_ID, sot.id) self.assertEqual(self.RULE_DIRECTION, sot.direction) self.assertEqual(self.RULE_MIN_KPPS, sot.min_kpps) def test_get(self): - sot = self.conn.network.get_qos_minimum_packet_rate_rule( - self.RULE_ID, - self.QOS_POLICY_ID) + sot = self.operator_cloud.network.get_qos_minimum_packet_rate_rule( + self.RULE_ID, self.QOS_POLICY_ID + ) self.assertEqual(self.RULE_ID, sot.id) self.assertEqual(self.QOS_POLICY_ID, sot.qos_policy_id) self.assertEqual(self.RULE_DIRECTION, sot.direction) self.assertEqual(self.RULE_MIN_KPPS, sot.min_kpps) def test_list(self): - rule_ids = [o.id for o in - self.conn.network.qos_minimum_packet_rate_rules( - self.QOS_POLICY_ID)] + rule_ids = [ + o.id + for o in self.operator_cloud.network.qos_minimum_packet_rate_rules( + self.QOS_POLICY_ID + ) + ] self.assertIn(self.RULE_ID, rule_ids) def test_update(self): - sot = self.conn.network.update_qos_minimum_packet_rate_rule( + sot = self.operator_cloud.network.update_qos_minimum_packet_rate_rule( self.RULE_ID, self.QOS_POLICY_ID, min_kpps=self.RULE_MIN_KPPS_NEW, - direction=self.RULE_DIRECTION_NEW) + direction=self.RULE_DIRECTION_NEW, + ) self.assertEqual(self.RULE_MIN_KPPS_NEW, sot.min_kpps) self.assertEqual(self.RULE_DIRECTION_NEW, sot.direction) diff --git a/openstack/tests/functional/network/v2/test_qos_policy.py b/openstack/tests/functional/network/v2/test_qos_policy.py index ca9365679..734130cbf 100644 --- a/openstack/tests/functional/network/v2/test_qos_policy.py +++ b/openstack/tests/functional/network/v2/test_qos_policy.py @@ -26,13 +26,16 @@ class TestQoSPolicy(base.BaseFunctionalTest): def setUp(self): super(TestQoSPolicy, self).setUp() + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") + # Skip the tests if qos extension is not enabled. - if not self.conn.network.find_extension('qos'): - self.skipTest('Network qos extension disabled') + if not self.operator_cloud.network.find_extension("qos"): + self.skipTest("Network qos extension disabled") self.QOS_POLICY_NAME = self.getUniqueString() self.QOS_POLICY_NAME_UPDATED = self.getUniqueString() - qos = self.conn.network.create_qos_policy( + qos = self.operator_cloud.network.create_qos_policy( description=self.QOS_POLICY_DESCRIPTION, name=self.QOS_POLICY_NAME, shared=self.IS_SHARED, @@ -43,16 +46,16 @@ def setUp(self): self.QOS_POLICY_ID = qos.id def tearDown(self): - sot = self.conn.network.delete_qos_policy(self.QOS_POLICY_ID) + sot = self.operator_cloud.network.delete_qos_policy(self.QOS_POLICY_ID) self.assertIsNone(sot) super(TestQoSPolicy, self).tearDown() def test_find(self): - sot = self.conn.network.find_qos_policy(self.QOS_POLICY_NAME) + sot = self.operator_cloud.network.find_qos_policy(self.QOS_POLICY_NAME) self.assertEqual(self.QOS_POLICY_ID, sot.id) def test_get(self): - sot = self.conn.network.get_qos_policy(self.QOS_POLICY_ID) + sot = self.operator_cloud.network.get_qos_policy(self.QOS_POLICY_ID) self.assertEqual(self.QOS_POLICY_NAME, sot.name) self.assertEqual(self.IS_SHARED, sot.is_shared) self.assertEqual(self.RULES, sot.rules) @@ -60,23 +63,23 @@ def test_get(self): self.assertEqual(self.IS_DEFAULT, sot.is_default) def test_list(self): - names = [o.name for o in self.conn.network.qos_policies()] + names = [o.name for o in self.operator_cloud.network.qos_policies()] self.assertIn(self.QOS_POLICY_NAME, names) def test_update(self): - sot = self.conn.network.update_qos_policy( - self.QOS_POLICY_ID, - name=self.QOS_POLICY_NAME_UPDATED) + sot = self.operator_cloud.network.update_qos_policy( + self.QOS_POLICY_ID, name=self.QOS_POLICY_NAME_UPDATED + ) self.assertEqual(self.QOS_POLICY_NAME_UPDATED, sot.name) def test_set_tags(self): - sot = self.conn.network.get_qos_policy(self.QOS_POLICY_ID) + sot = self.operator_cloud.network.get_qos_policy(self.QOS_POLICY_ID) self.assertEqual([], sot.tags) - self.conn.network.set_tags(sot, ['blue']) - sot = self.conn.network.get_qos_policy(self.QOS_POLICY_ID) - self.assertEqual(['blue'], sot.tags) + self.operator_cloud.network.set_tags(sot, ["blue"]) + sot = self.operator_cloud.network.get_qos_policy(self.QOS_POLICY_ID) + self.assertEqual(["blue"], sot.tags) - self.conn.network.set_tags(sot, []) - sot = self.conn.network.get_qos_policy(self.QOS_POLICY_ID) + self.operator_cloud.network.set_tags(sot, []) + sot = self.operator_cloud.network.get_qos_policy(self.QOS_POLICY_ID) self.assertEqual([], sot.tags) diff --git a/openstack/tests/functional/network/v2/test_qos_rule_type.py b/openstack/tests/functional/network/v2/test_qos_rule_type.py index e71077665..a0d662371 100644 --- a/openstack/tests/functional/network/v2/test_qos_rule_type.py +++ b/openstack/tests/functional/network/v2/test_qos_rule_type.py @@ -20,22 +20,27 @@ class TestQoSRuleType(base.BaseFunctionalTest): def setUp(self): super(TestQoSRuleType, self).setUp() + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") + # Skip the tests if qos-rule-type-details extension is not enabled. - if not self.conn.network.find_extension('qos-rule-type-details'): - self.skipTest('Network qos-rule-type-details extension disabled') + if not self.operator_cloud.network.find_extension( + "qos-rule-type-details"): + self.skipTest("Network qos-rule-type-details extension disabled") def test_find(self): - sot = self.conn.network.find_qos_rule_type(self.QOS_RULE_TYPE) + sot = self.operator_cloud.network.find_qos_rule_type( + self.QOS_RULE_TYPE) self.assertEqual(self.QOS_RULE_TYPE, sot.type) self.assertIsInstance(sot.drivers, list) def test_get(self): - sot = self.conn.network.get_qos_rule_type(self.QOS_RULE_TYPE) + sot = self.operator_cloud.network.get_qos_rule_type(self.QOS_RULE_TYPE) self.assertEqual(self.QOS_RULE_TYPE, sot.type) self.assertIsInstance(sot.drivers, list) def test_list(self): - rule_types = list(self.conn.network.qos_rule_types()) + rule_types = list(self.operator_cloud.network.qos_rule_types()) self.assertGreater(len(rule_types), 0) for rule_type in rule_types: diff --git a/openstack/tests/functional/network/v2/test_quota.py b/openstack/tests/functional/network/v2/test_quota.py index 23dfa9774..ae582488b 100644 --- a/openstack/tests/functional/network/v2/test_quota.py +++ b/openstack/tests/functional/network/v2/test_quota.py @@ -14,23 +14,32 @@ class TestQuota(base.BaseFunctionalTest): + def setUp(self): + super(TestQuota, self).setUp() + + if not self.operator_cloud: + self.skipTest("Operator cloud required for this test") def test_list(self): - for qot in self.conn.network.quotas(): + for qot in self.operator_cloud.network.quotas(): self.assertIsNotNone(qot.project_id) self.assertIsNotNone(qot.networks) def test_list_details(self): - expected_keys = ['limit', 'used', 'reserved'] - project_id = self.conn.session.get_project_id() - quota_details = self.conn.network.get_quota(project_id, details=True) + expected_keys = ["limit", "used", "reserved"] + project_id = self.operator_cloud.session.get_project_id() + quota_details = self.operator_cloud.network.get_quota( + project_id, details=True + ) for details in quota_details._body.attributes.values(): for expected_key in expected_keys: self.assertTrue(expected_key in details.keys()) def test_set(self): - attrs = {'networks': 123456789} - for project_quota in self.conn.network.quotas(): - self.conn.network.update_quota(project_quota, **attrs) - new_quota = self.conn.network.get_quota(project_quota.project_id) + attrs = {"networks": 123456789} + for project_quota in self.operator_cloud.network.quotas(): + self.operator_cloud.network.update_quota(project_quota, **attrs) + new_quota = self.operator_cloud.network.get_quota( + project_quota.project_id + ) self.assertEqual(123456789, new_quota.networks) diff --git a/openstack/tests/functional/network/v2/test_rbac_policy.py b/openstack/tests/functional/network/v2/test_rbac_policy.py index b99e73c14..70db50ae9 100644 --- a/openstack/tests/functional/network/v2/test_rbac_policy.py +++ b/openstack/tests/functional/network/v2/test_rbac_policy.py @@ -18,47 +18,77 @@ class TestRBACPolicy(base.BaseFunctionalTest): - ACTION = 'access_as_shared' - OBJ_TYPE = 'network' - TARGET_TENANT_ID = '*' + ACTION = "access_as_shared" + OBJ_TYPE = "network" + TARGET_TENANT_ID = "*" NET_ID = None ID = None def setUp(self): super(TestRBACPolicy, self).setUp() - self.NET_NAME = self.getUniqueString('net') + if not self.user_cloud._has_neutron_extension("rbac-policies"): + self.skipTest( + "Neutron rbac-policies extension is required for this test" + ) + + self.NET_NAME = self.getUniqueString("net") self.UPDATE_NAME = self.getUniqueString() - net = self.conn.network.create_network(name=self.NET_NAME) + net = self.user_cloud.network.create_network(name=self.NET_NAME) assert isinstance(net, network.Network) self.NET_ID = net.id - - sot = self.conn.network.create_rbac_policy( - action=self.ACTION, - object_type=self.OBJ_TYPE, - target_tenant=self.TARGET_TENANT_ID, - object_id=self.NET_ID) - assert isinstance(sot, rbac_policy.RBACPolicy) - self.ID = sot.id + if self.operator_cloud: + sot = self.operator_cloud.network.create_rbac_policy( + action=self.ACTION, + object_type=self.OBJ_TYPE, + target_tenant=self.TARGET_TENANT_ID, + object_id=self.NET_ID, + ) + assert isinstance(sot, rbac_policy.RBACPolicy) + self.ID = sot.id + else: + sot = self.user_cloud.network.create_rbac_policy( + action=self.ACTION, + object_type=self.OBJ_TYPE, + target_tenant=self.user_cloud.current_project_id, + object_id=self.NET_ID, + ) + assert isinstance(sot, rbac_policy.RBACPolicy) + self.ID = sot.id def tearDown(self): - sot = self.conn.network.delete_rbac_policy( - self.ID, - ignore_missing=False) + if self.operator_cloud: + sot = self.operator_cloud.network.delete_rbac_policy( + self.ID, ignore_missing=False + ) + else: + sot = self.user_cloud.network.delete_rbac_policy( + self.ID, ignore_missing=False + ) self.assertIsNone(sot) - sot = self.conn.network.delete_network( - self.NET_ID, - ignore_missing=False) + sot = self.user_cloud.network.delete_network( + self.NET_ID, ignore_missing=False + ) self.assertIsNone(sot) super(TestRBACPolicy, self).tearDown() def test_find(self): - sot = self.conn.network.find_rbac_policy(self.ID) + if self.operator_cloud: + sot = self.operator_cloud.network.find_rbac_policy(self.ID) + else: + sot = self.user_cloud.network.find_rbac_policy(self.ID) self.assertEqual(self.ID, sot.id) def test_get(self): - sot = self.conn.network.get_rbac_policy(self.ID) + if self.operator_cloud: + sot = self.operator_cloud.network.get_rbac_policy(self.ID) + else: + sot = self.user_cloud.network.get_rbac_policy(self.ID) self.assertEqual(self.ID, sot.id) def test_list(self): - ids = [o.id for o in self.conn.network.rbac_policies()] - self.assertIn(self.ID, ids) + if self.operator_cloud: + ids = [o.id for o in self.operator_cloud.network.rbac_policies()] + else: + ids = [o.id for o in self.user_cloud.network.rbac_policies()] + if self.ID: + self.assertIn(self.ID, ids) diff --git a/openstack/tests/functional/network/v2/test_router.py b/openstack/tests/functional/network/v2/test_router.py index 285bd6feb..bb5e71496 100644 --- a/openstack/tests/functional/network/v2/test_router.py +++ b/openstack/tests/functional/network/v2/test_router.py @@ -23,44 +23,50 @@ def setUp(self): super(TestRouter, self).setUp() self.NAME = self.getUniqueString() self.UPDATE_NAME = self.getUniqueString() - sot = self.conn.network.create_router(name=self.NAME) + sot = self.user_cloud.network.create_router(name=self.NAME) assert isinstance(sot, router.Router) self.assertEqual(self.NAME, sot.name) self.ID = sot.id def tearDown(self): - sot = self.conn.network.delete_router(self.ID, ignore_missing=False) + sot = self.user_cloud.network.delete_router( + self.ID, ignore_missing=False + ) self.assertIsNone(sot) super(TestRouter, self).tearDown() def test_find(self): - sot = self.conn.network.find_router(self.NAME) + sot = self.user_cloud.network.find_router(self.NAME) self.assertEqual(self.ID, sot.id) def test_get(self): - sot = self.conn.network.get_router(self.ID) + sot = self.user_cloud.network.get_router(self.ID) self.assertEqual(self.NAME, sot.name) self.assertEqual(self.ID, sot.id) - self.assertFalse(sot.is_ha) + if not self.user_cloud._has_neutron_extension("l3-ha"): + self.assertFalse(sot.is_ha) def test_list(self): - names = [o.name for o in self.conn.network.routers()] + names = [o.name for o in self.user_cloud.network.routers()] self.assertIn(self.NAME, names) - ha = [o.is_ha for o in self.conn.network.routers()] - self.assertIn(False, ha) + if not self.user_cloud._has_neutron_extension("l3-ha"): + ha = [o.is_ha for o in self.user_cloud.network.routers()] + self.assertIn(False, ha) def test_update(self): - sot = self.conn.network.update_router(self.ID, name=self.UPDATE_NAME) + sot = self.user_cloud.network.update_router( + self.ID, name=self.UPDATE_NAME + ) self.assertEqual(self.UPDATE_NAME, sot.name) def test_set_tags(self): - sot = self.conn.network.get_router(self.ID) + sot = self.user_cloud.network.get_router(self.ID) self.assertEqual([], sot.tags) - self.conn.network.set_tags(sot, ['blue']) - sot = self.conn.network.get_router(self.ID) - self.assertEqual(['blue'], sot.tags) + self.user_cloud.network.set_tags(sot, ["blue"]) + sot = self.user_cloud.network.get_router(self.ID) + self.assertEqual(["blue"], sot.tags) - self.conn.network.set_tags(sot, []) - sot = self.conn.network.get_router(self.ID) + self.user_cloud.network.set_tags(sot, []) + sot = self.user_cloud.network.get_router(self.ID) self.assertEqual([], sot.tags) diff --git a/openstack/tests/functional/network/v2/test_router_add_remove_interface.py b/openstack/tests/functional/network/v2/test_router_add_remove_interface.py index a300368a0..9cd10553f 100644 --- a/openstack/tests/functional/network/v2/test_router_add_remove_interface.py +++ b/openstack/tests/functional/network/v2/test_router_add_remove_interface.py @@ -31,17 +31,18 @@ def setUp(self): self.ROUTER_NAME = self.getUniqueString() self.NET_NAME = self.getUniqueString() self.SUB_NAME = self.getUniqueString() - sot = self.conn.network.create_router(name=self.ROUTER_NAME) + sot = self.user_cloud.network.create_router(name=self.ROUTER_NAME) assert isinstance(sot, router.Router) self.assertEqual(self.ROUTER_NAME, sot.name) - net = self.conn.network.create_network(name=self.NET_NAME) + net = self.user_cloud.network.create_network(name=self.NET_NAME) assert isinstance(net, network.Network) self.assertEqual(self.NET_NAME, net.name) - sub = self.conn.network.create_subnet( + sub = self.user_cloud.network.create_subnet( name=self.SUB_NAME, ip_version=self.IPV4, network_id=net.id, - cidr=self.CIDR) + cidr=self.CIDR, + ) assert isinstance(sub, subnet.Subnet) self.assertEqual(self.SUB_NAME, sub.name) self.ROUTER_ID = sot.id @@ -50,25 +51,30 @@ def setUp(self): self.SUB_ID = sub.id def tearDown(self): - sot = self.conn.network.delete_router( - self.ROUTER_ID, ignore_missing=False) + sot = self.user_cloud.network.delete_router( + self.ROUTER_ID, ignore_missing=False + ) self.assertIsNone(sot) - sot = self.conn.network.delete_subnet( - self.SUB_ID, ignore_missing=False) + sot = self.user_cloud.network.delete_subnet( + self.SUB_ID, ignore_missing=False + ) self.assertIsNone(sot) - sot = self.conn.network.delete_network( - self.NET_ID, ignore_missing=False) + sot = self.user_cloud.network.delete_network( + self.NET_ID, ignore_missing=False + ) self.assertIsNone(sot) super(TestRouterInterface, self).tearDown() def test_router_add_remove_interface(self): - iface = self.ROT.add_interface(self.conn.network, - subnet_id=self.SUB_ID) + iface = self.ROT.add_interface( + self.user_cloud.network, subnet_id=self.SUB_ID + ) self._verification(iface) - iface = self.ROT.remove_interface(self.conn.network, - subnet_id=self.SUB_ID) + iface = self.ROT.remove_interface( + self.user_cloud.network, subnet_id=self.SUB_ID + ) self._verification(iface) def _verification(self, interface): - self.assertEqual(interface['subnet_id'], self.SUB_ID) - self.assertIn('port_id', interface) + self.assertEqual(interface["subnet_id"], self.SUB_ID) + self.assertIn("port_id", interface) diff --git a/openstack/tests/functional/network/v2/test_security_group.py b/openstack/tests/functional/network/v2/test_security_group.py index 08049dcef..93f997e31 100644 --- a/openstack/tests/functional/network/v2/test_security_group.py +++ b/openstack/tests/functional/network/v2/test_security_group.py @@ -22,42 +22,45 @@ class TestSecurityGroup(base.BaseFunctionalTest): def setUp(self): super(TestSecurityGroup, self).setUp() self.NAME = self.getUniqueString() - sot = self.conn.network.create_security_group(name=self.NAME) + sot = self.user_cloud.network.create_security_group(name=self.NAME) assert isinstance(sot, security_group.SecurityGroup) self.assertEqual(self.NAME, sot.name) self.ID = sot.id def tearDown(self): - sot = self.conn.network.delete_security_group( - self.ID, ignore_missing=False) + sot = self.user_cloud.network.delete_security_group( + self.ID, ignore_missing=False + ) self.assertIsNone(sot) super(TestSecurityGroup, self).tearDown() def test_find(self): - sot = self.conn.network.find_security_group(self.NAME) + sot = self.user_cloud.network.find_security_group(self.NAME) self.assertEqual(self.ID, sot.id) def test_get(self): - sot = self.conn.network.get_security_group(self.ID) + sot = self.user_cloud.network.get_security_group(self.ID) self.assertEqual(self.NAME, sot.name) self.assertEqual(self.ID, sot.id) def test_list(self): - names = [o.name for o in self.conn.network.security_groups()] + names = [o.name for o in self.user_cloud.network.security_groups()] self.assertIn(self.NAME, names) def test_list_query_list_of_ids(self): - ids = [o.id for o in self.conn.network.security_groups(id=[self.ID])] + ids = [ + o.id for o in self.user_cloud.network.security_groups(id=[self.ID]) + ] self.assertIn(self.ID, ids) def test_set_tags(self): - sot = self.conn.network.get_security_group(self.ID) + sot = self.user_cloud.network.get_security_group(self.ID) self.assertEqual([], sot.tags) - self.conn.network.set_tags(sot, ['blue']) - sot = self.conn.network.get_security_group(self.ID) - self.assertEqual(['blue'], sot.tags) + self.user_cloud.network.set_tags(sot, ["blue"]) + sot = self.user_cloud.network.get_security_group(self.ID) + self.assertEqual(["blue"], sot.tags) - self.conn.network.set_tags(sot, []) - sot = self.conn.network.get_security_group(self.ID) + self.user_cloud.network.set_tags(sot, []) + sot = self.user_cloud.network.get_security_group(self.ID) self.assertEqual([], sot.tags) diff --git a/openstack/tests/functional/network/v2/test_security_group_rule.py b/openstack/tests/functional/network/v2/test_security_group_rule.py index 544587183..e2a7229a1 100644 --- a/openstack/tests/functional/network/v2/test_security_group_rule.py +++ b/openstack/tests/functional/network/v2/test_security_group_rule.py @@ -18,43 +18,49 @@ class TestSecurityGroupRule(base.BaseFunctionalTest): - IPV4 = 'IPv4' - PROTO = 'tcp' + IPV4 = "IPv4" + PROTO = "tcp" PORT = 22 - DIR = 'ingress' + DIR = "ingress" ID = None RULE_ID = None def setUp(self): super(TestSecurityGroupRule, self).setUp() self.NAME = self.getUniqueString() - sot = self.conn.network.create_security_group(name=self.NAME) + sot = self.user_cloud.network.create_security_group(name=self.NAME) assert isinstance(sot, security_group.SecurityGroup) self.assertEqual(self.NAME, sot.name) self.ID = sot.id - rul = self.conn.network.create_security_group_rule( - direction=self.DIR, ethertype=self.IPV4, - port_range_max=self.PORT, port_range_min=self.PORT, - protocol=self.PROTO, security_group_id=self.ID) + rul = self.user_cloud.network.create_security_group_rule( + direction=self.DIR, + ethertype=self.IPV4, + port_range_max=self.PORT, + port_range_min=self.PORT, + protocol=self.PROTO, + security_group_id=self.ID, + ) assert isinstance(rul, security_group_rule.SecurityGroupRule) self.assertEqual(self.ID, rul.security_group_id) self.RULE_ID = rul.id def tearDown(self): - sot = self.conn.network.delete_security_group_rule( - self.RULE_ID, ignore_missing=False) + sot = self.user_cloud.network.delete_security_group_rule( + self.RULE_ID, ignore_missing=False + ) self.assertIsNone(sot) - sot = self.conn.network.delete_security_group( - self.ID, ignore_missing=False) + sot = self.user_cloud.network.delete_security_group( + self.ID, ignore_missing=False + ) self.assertIsNone(sot) super(TestSecurityGroupRule, self).tearDown() def test_find(self): - sot = self.conn.network.find_security_group_rule(self.RULE_ID) + sot = self.user_cloud.network.find_security_group_rule(self.RULE_ID) self.assertEqual(self.RULE_ID, sot.id) def test_get(self): - sot = self.conn.network.get_security_group_rule(self.RULE_ID) + sot = self.user_cloud.network.get_security_group_rule(self.RULE_ID) self.assertEqual(self.RULE_ID, sot.id) self.assertEqual(self.DIR, sot.direction) self.assertEqual(self.PROTO, sot.protocol) @@ -63,5 +69,5 @@ def test_get(self): self.assertEqual(self.ID, sot.security_group_id) def test_list(self): - ids = [o.id for o in self.conn.network.security_group_rules()] + ids = [o.id for o in self.user_cloud.network.security_group_rules()] self.assertIn(self.RULE_ID, ids) diff --git a/openstack/tests/functional/network/v2/test_segment.py b/openstack/tests/functional/network/v2/test_segment.py index 88499ed9e..723a9c90b 100644 --- a/openstack/tests/functional/network/v2/test_segment.py +++ b/openstack/tests/functional/network/v2/test_segment.py @@ -29,20 +29,25 @@ def setUp(self): super(TestSegment, self).setUp() self.NETWORK_NAME = self.getUniqueString() + if not self.operator_cloud: + self.skipTest("Operator cloud required for this test") + # NOTE(rtheis): The segment extension is not yet enabled by default. # Skip the tests if not enabled. - if not self.conn.network.find_extension('segment'): - self.skipTest('Segment extension disabled') + if not self.operator_cloud.network.find_extension("segment"): + self.skipTest("Segment extension disabled") # Create a network to hold the segment. - net = self.conn.network.create_network(name=self.NETWORK_NAME) + net = self.operator_cloud.network.create_network( + name=self.NETWORK_NAME + ) assert isinstance(net, network.Network) self.assertEqual(self.NETWORK_NAME, net.name) self.NETWORK_ID = net.id if self.SEGMENT_EXTENSION: # Get the segment for the network. - for seg in self.conn.network.segments(): + for seg in self.operator_cloud.network.segments(): assert isinstance(seg, segment.Segment) if self.NETWORK_ID == seg.network_id: self.NETWORK_TYPE = seg.network_type @@ -52,36 +57,36 @@ def setUp(self): break def tearDown(self): - sot = self.conn.network.delete_network( - self.NETWORK_ID, - ignore_missing=False) + sot = self.operator_cloud.network.delete_network( + self.NETWORK_ID, ignore_missing=False + ) self.assertIsNone(sot) super(TestSegment, self).tearDown() def test_create_delete(self): - sot = self.conn.network.create_segment( - description='test description', - name='test name', + sot = self.operator_cloud.network.create_segment( + description="test description", + name="test name", network_id=self.NETWORK_ID, - network_type='geneve', + network_type="geneve", segmentation_id=2055, ) self.assertIsInstance(sot, segment.Segment) - del_sot = self.conn.network.delete_segment(sot.id) - self.assertEqual('test description', sot.description) - self.assertEqual('test name', sot.name) + del_sot = self.operator_cloud.network.delete_segment(sot.id) + self.assertEqual("test description", sot.description) + self.assertEqual("test name", sot.name) self.assertEqual(self.NETWORK_ID, sot.network_id) - self.assertEqual('geneve', sot.network_type) + self.assertEqual("geneve", sot.network_type) self.assertIsNone(sot.physical_network) self.assertEqual(2055, sot.segmentation_id) self.assertIsNone(del_sot) def test_find(self): - sot = self.conn.network.find_segment(self.SEGMENT_ID) + sot = self.operator_cloud.network.find_segment(self.SEGMENT_ID) self.assertEqual(self.SEGMENT_ID, sot.id) def test_get(self): - sot = self.conn.network.get_segment(self.SEGMENT_ID) + sot = self.operator_cloud.network.get_segment(self.SEGMENT_ID) self.assertEqual(self.SEGMENT_ID, sot.id) self.assertIsNone(sot.name) self.assertEqual(self.NETWORK_ID, sot.network_id) @@ -90,10 +95,11 @@ def test_get(self): self.assertEqual(self.SEGMENTATION_ID, sot.segmentation_id) def test_list(self): - ids = [o.id for o in self.conn.network.segments(name=None)] + ids = [o.id for o in self.operator_cloud.network.segments(name=None)] self.assertIn(self.SEGMENT_ID, ids) def test_update(self): - sot = self.conn.network.update_segment(self.SEGMENT_ID, - description='update') - self.assertEqual('update', sot.description) + sot = self.operator_cloud.network.update_segment( + self.SEGMENT_ID, description="update" + ) + self.assertEqual("update", sot.description) diff --git a/openstack/tests/functional/network/v2/test_service_profile.py b/openstack/tests/functional/network/v2/test_service_profile.py index 221d9b6bd..7bce681fa 100644 --- a/openstack/tests/functional/network/v2/test_service_profile.py +++ b/openstack/tests/functional/network/v2/test_service_profile.py @@ -23,42 +23,71 @@ class TestServiceProfile(base.BaseFunctionalTest): def setUp(self): super(TestServiceProfile, self).setUp() - service_profiles = self.conn.network.create_service_profile( - description=self.SERVICE_PROFILE_DESCRIPTION, - metainfo=self.METAINFO,) - assert isinstance(service_profiles, _service_profile.ServiceProfile) - self.assertEqual( - self.SERVICE_PROFILE_DESCRIPTION, - service_profiles.description) - self.assertEqual(self.METAINFO, service_profiles.meta_info) + if not self.user_cloud._has_neutron_extension("flavors"): + self.skipTest("Neutron flavor extension is required for this test") - self.ID = service_profiles.id + if self.operator_cloud: + service_profiles = ( + self.operator_cloud.network.create_service_profile( + description=self.SERVICE_PROFILE_DESCRIPTION, + metainfo=self.METAINFO, + ) + ) + assert isinstance( + service_profiles, _service_profile.ServiceProfile + ) + self.assertEqual( + self.SERVICE_PROFILE_DESCRIPTION, service_profiles.description + ) + self.assertEqual(self.METAINFO, service_profiles.meta_info) + + self.ID = service_profiles.id def tearDown(self): - service_profiles = self.conn.network.delete_service_profile( - self.ID, - ignore_missing=True) - self.assertIsNone(service_profiles) + if self.ID: + service_profiles = ( + self.operator_cloud.network.delete_service_profile( + self.ID, ignore_missing=True + ) + ) + self.assertIsNone(service_profiles) super(TestServiceProfile, self).tearDown() def test_find(self): - service_profiles = self.conn.network.find_service_profile( - self.ID) - self.assertEqual(self.METAINFO, - service_profiles.meta_info) + self.user_cloud.network.find_service_profile( + name_or_id="not_existing", + ignore_missing=True) + if self.operator_cloud and self.ID: + service_profiles = self.operator_cloud.network \ + .find_service_profile(self.ID) + self.assertEqual(self.METAINFO, service_profiles.meta_info) def test_get(self): - service_profiles = self.conn.network.get_service_profile(self.ID) + if not self.ID: + self.skipTest("ServiceProfile was not created") + service_profiles = self.operator_cloud.network.get_service_profile( + self.ID) self.assertEqual(self.METAINFO, service_profiles.meta_info) - self.assertEqual(self.SERVICE_PROFILE_DESCRIPTION, - service_profiles.description) + self.assertEqual( + self.SERVICE_PROFILE_DESCRIPTION, service_profiles.description + ) def test_update(self): - service_profiles = self.conn.network.update_service_profile( - self.ID, - description=self.UPDATE_DESCRIPTION) + if not self.ID: + self.skipTest("ServiceProfile was not created") + service_profiles = self.operator_cloud.network.update_service_profile( + self.ID, description=self.UPDATE_DESCRIPTION + ) self.assertEqual(self.UPDATE_DESCRIPTION, service_profiles.description) def test_list(self): - metainfos = [f.meta_info for f in self.conn.network.service_profiles()] - self.assertIn(self.METAINFO, metainfos) + # Test in user scope + self.user_cloud.network.service_profiles() + # Test as operator + if self.operator_cloud: + metainfos = [ + f.meta_info for f in + self.operator_cloud.network.service_profiles() + ] + if self.ID: + self.assertIn(self.METAINFO, metainfos) diff --git a/openstack/tests/functional/network/v2/test_service_provider.py b/openstack/tests/functional/network/v2/test_service_provider.py index fca659c05..4dcd21037 100644 --- a/openstack/tests/functional/network/v2/test_service_provider.py +++ b/openstack/tests/functional/network/v2/test_service_provider.py @@ -15,8 +15,9 @@ class TestServiceProvider(base.BaseFunctionalTest): def test_list(self): - providers = list(self.conn.network.service_providers()) + providers = list(self.user_cloud.network.service_providers()) names = [o.name for o in providers] service_types = [o.service_type for o in providers] - self.assertIn('ha', names) - self.assertIn('L3_ROUTER_NAT', service_types) + if self.user_cloud._has_neutron_extension("l3-ha"): + self.assertIn("ha", names) + self.assertIn("L3_ROUTER_NAT", service_types) diff --git a/openstack/tests/functional/network/v2/test_subnet.py b/openstack/tests/functional/network/v2/test_subnet.py index abf8ccb12..2bafd40d2 100644 --- a/openstack/tests/functional/network/v2/test_subnet.py +++ b/openstack/tests/functional/network/v2/test_subnet.py @@ -31,36 +31,38 @@ def setUp(self): self.NET_NAME = self.getUniqueString() self.SUB_NAME = self.getUniqueString() self.UPDATE_NAME = self.getUniqueString() - net = self.conn.network.create_network(name=self.NET_NAME) + net = self.user_cloud.network.create_network(name=self.NET_NAME) assert isinstance(net, network.Network) self.assertEqual(self.NET_NAME, net.name) self.NET_ID = net.id - sub = self.conn.network.create_subnet( + sub = self.user_cloud.network.create_subnet( name=self.SUB_NAME, ip_version=self.IPV4, network_id=self.NET_ID, cidr=self.CIDR, dns_nameservers=self.DNS_SERVERS, allocation_pools=self.POOL, - host_routes=self.ROUTES) + host_routes=self.ROUTES, + ) assert isinstance(sub, subnet.Subnet) self.assertEqual(self.SUB_NAME, sub.name) self.SUB_ID = sub.id def tearDown(self): - sot = self.conn.network.delete_subnet(self.SUB_ID) + sot = self.user_cloud.network.delete_subnet(self.SUB_ID) self.assertIsNone(sot) - sot = self.conn.network.delete_network( - self.NET_ID, ignore_missing=False) + sot = self.user_cloud.network.delete_network( + self.NET_ID, ignore_missing=False + ) self.assertIsNone(sot) super(TestSubnet, self).tearDown() def test_find(self): - sot = self.conn.network.find_subnet(self.SUB_NAME) + sot = self.user_cloud.network.find_subnet(self.SUB_NAME) self.assertEqual(self.SUB_ID, sot.id) def test_get(self): - sot = self.conn.network.get_subnet(self.SUB_ID) + sot = self.user_cloud.network.get_subnet(self.SUB_ID) self.assertEqual(self.SUB_NAME, sot.name) self.assertEqual(self.SUB_ID, sot.id) self.assertEqual(self.DNS_SERVERS, sot.dns_nameservers) @@ -72,22 +74,23 @@ def test_get(self): self.assertTrue(sot.is_dhcp_enabled) def test_list(self): - names = [o.name for o in self.conn.network.subnets()] + names = [o.name for o in self.user_cloud.network.subnets()] self.assertIn(self.SUB_NAME, names) def test_update(self): - sot = self.conn.network.update_subnet(self.SUB_ID, - name=self.UPDATE_NAME) + sot = self.user_cloud.network.update_subnet( + self.SUB_ID, name=self.UPDATE_NAME + ) self.assertEqual(self.UPDATE_NAME, sot.name) def test_set_tags(self): - sot = self.conn.network.get_subnet(self.SUB_ID) + sot = self.user_cloud.network.get_subnet(self.SUB_ID) self.assertEqual([], sot.tags) - self.conn.network.set_tags(sot, ['blue']) - sot = self.conn.network.get_subnet(self.SUB_ID) - self.assertEqual(['blue'], sot.tags) + self.user_cloud.network.set_tags(sot, ["blue"]) + sot = self.user_cloud.network.get_subnet(self.SUB_ID) + self.assertEqual(["blue"], sot.tags) - self.conn.network.set_tags(sot, []) - sot = self.conn.network.get_subnet(self.SUB_ID) + self.user_cloud.network.set_tags(sot, []) + sot = self.user_cloud.network.get_subnet(self.SUB_ID) self.assertEqual([], sot.tags) diff --git a/openstack/tests/functional/network/v2/test_subnet_from_subnet_pool.py b/openstack/tests/functional/network/v2/test_subnet_from_subnet_pool.py index c03dce637..00f23597b 100644 --- a/openstack/tests/functional/network/v2/test_subnet_from_subnet_pool.py +++ b/openstack/tests/functional/network/v2/test_subnet_from_subnet_pool.py @@ -26,7 +26,7 @@ class TestSubnetFromSubnetPool(base.BaseFunctionalTest): MAXIMUM_PREFIX_LENGTH = 32 SUBNET_PREFIX_LENGTH = 28 IP_VERSION = 4 - PREFIXES = ['10.100.0.0/24'] + PREFIXES = ["10.100.0.0/24"] NET_ID = None SUB_ID = None SUB_POOL_ID = None @@ -37,41 +37,44 @@ def setUp(self): self.SUB_NAME = self.getUniqueString() self.SUB_POOL_NAME = self.getUniqueString() - sub_pool = self.conn.network.create_subnet_pool( + sub_pool = self.user_cloud.network.create_subnet_pool( name=self.SUB_POOL_NAME, min_prefixlen=self.MINIMUM_PREFIX_LENGTH, default_prefixlen=self.DEFAULT_PREFIX_LENGTH, max_prefixlen=self.MAXIMUM_PREFIX_LENGTH, - prefixes=self.PREFIXES) + prefixes=self.PREFIXES, + ) self.assertIsInstance(sub_pool, subnet_pool.SubnetPool) self.assertEqual(self.SUB_POOL_NAME, sub_pool.name) self.SUB_POOL_ID = sub_pool.id - net = self.conn.network.create_network(name=self.NET_NAME) + net = self.user_cloud.network.create_network(name=self.NET_NAME) self.assertIsInstance(net, network.Network) self.assertEqual(self.NET_NAME, net.name) self.NET_ID = net.id - sub = self.conn.network.create_subnet( + sub = self.user_cloud.network.create_subnet( name=self.SUB_NAME, ip_version=self.IPV4, network_id=self.NET_ID, prefixlen=self.SUBNET_PREFIX_LENGTH, - subnetpool_id=self.SUB_POOL_ID) + subnetpool_id=self.SUB_POOL_ID, + ) self.assertIsInstance(sub, subnet.Subnet) self.assertEqual(self.SUB_NAME, sub.name) self.SUB_ID = sub.id def tearDown(self): - sot = self.conn.network.delete_subnet(self.SUB_ID) + sot = self.user_cloud.network.delete_subnet(self.SUB_ID) self.assertIsNone(sot) - sot = self.conn.network.delete_network( - self.NET_ID, ignore_missing=False) + sot = self.user_cloud.network.delete_network( + self.NET_ID, ignore_missing=False + ) self.assertIsNone(sot) - sot = self.conn.network.delete_subnet_pool(self.SUB_POOL_ID) + sot = self.user_cloud.network.delete_subnet_pool(self.SUB_POOL_ID) self.assertIsNone(sot) super(TestSubnetFromSubnetPool, self).tearDown() def test_get(self): - sot = self.conn.network.get_subnet(self.SUB_ID) + sot = self.user_cloud.network.get_subnet(self.SUB_ID) self.assertEqual(self.SUB_NAME, sot.name) self.assertEqual(self.SUB_ID, sot.id) self.assertEqual(self.CIDR, sot.cidr) diff --git a/openstack/tests/functional/network/v2/test_subnet_pool.py b/openstack/tests/functional/network/v2/test_subnet_pool.py index d238d0e9d..15848eaa9 100644 --- a/openstack/tests/functional/network/v2/test_subnet_pool.py +++ b/openstack/tests/functional/network/v2/test_subnet_pool.py @@ -24,65 +24,63 @@ class TestSubnetPool(base.BaseFunctionalTest): DEFAULT_QUOTA = 24 IS_SHARED = False IP_VERSION = 4 - PREFIXES = ['10.100.0.0/24', '10.101.0.0/24'] + PREFIXES = ["10.100.0.0/24", "10.101.0.0/24"] def setUp(self): super(TestSubnetPool, self).setUp() self.SUBNET_POOL_NAME = self.getUniqueString() self.SUBNET_POOL_NAME_UPDATED = self.getUniqueString() - subnet_pool = self.conn.network.create_subnet_pool( + subnet_pool = self.user_cloud.network.create_subnet_pool( name=self.SUBNET_POOL_NAME, min_prefixlen=self.MINIMUM_PREFIX_LENGTH, default_prefixlen=self.DEFAULT_PREFIX_LENGTH, max_prefixlen=self.MAXIMUM_PREFIX_LENGTH, default_quota=self.DEFAULT_QUOTA, shared=self.IS_SHARED, - prefixes=self.PREFIXES) + prefixes=self.PREFIXES, + ) assert isinstance(subnet_pool, _subnet_pool.SubnetPool) self.assertEqual(self.SUBNET_POOL_NAME, subnet_pool.name) self.SUBNET_POOL_ID = subnet_pool.id def tearDown(self): - sot = self.conn.network.delete_subnet_pool(self.SUBNET_POOL_ID) + sot = self.user_cloud.network.delete_subnet_pool(self.SUBNET_POOL_ID) self.assertIsNone(sot) super(TestSubnetPool, self).tearDown() def test_find(self): - sot = self.conn.network.find_subnet_pool(self.SUBNET_POOL_NAME) + sot = self.user_cloud.network.find_subnet_pool(self.SUBNET_POOL_NAME) self.assertEqual(self.SUBNET_POOL_ID, sot.id) def test_get(self): - sot = self.conn.network.get_subnet_pool(self.SUBNET_POOL_ID) + sot = self.user_cloud.network.get_subnet_pool(self.SUBNET_POOL_ID) self.assertEqual(self.SUBNET_POOL_NAME, sot.name) - self.assertEqual(self.MINIMUM_PREFIX_LENGTH, - sot.minimum_prefix_length) - self.assertEqual(self.DEFAULT_PREFIX_LENGTH, - sot.default_prefix_length) - self.assertEqual(self.MAXIMUM_PREFIX_LENGTH, - sot.maximum_prefix_length) + self.assertEqual(self.MINIMUM_PREFIX_LENGTH, sot.minimum_prefix_length) + self.assertEqual(self.DEFAULT_PREFIX_LENGTH, sot.default_prefix_length) + self.assertEqual(self.MAXIMUM_PREFIX_LENGTH, sot.maximum_prefix_length) self.assertEqual(self.DEFAULT_QUOTA, sot.default_quota) self.assertEqual(self.IS_SHARED, sot.is_shared) self.assertEqual(self.IP_VERSION, sot.ip_version) self.assertEqual(self.PREFIXES, sot.prefixes) def test_list(self): - names = [o.name for o in self.conn.network.subnet_pools()] + names = [o.name for o in self.user_cloud.network.subnet_pools()] self.assertIn(self.SUBNET_POOL_NAME, names) def test_update(self): - sot = self.conn.network.update_subnet_pool( - self.SUBNET_POOL_ID, - name=self.SUBNET_POOL_NAME_UPDATED) + sot = self.user_cloud.network.update_subnet_pool( + self.SUBNET_POOL_ID, name=self.SUBNET_POOL_NAME_UPDATED + ) self.assertEqual(self.SUBNET_POOL_NAME_UPDATED, sot.name) def test_set_tags(self): - sot = self.conn.network.get_subnet_pool(self.SUBNET_POOL_ID) + sot = self.user_cloud.network.get_subnet_pool(self.SUBNET_POOL_ID) self.assertEqual([], sot.tags) - self.conn.network.set_tags(sot, ['blue']) - sot = self.conn.network.get_subnet_pool(self.SUBNET_POOL_ID) - self.assertEqual(['blue'], sot.tags) + self.user_cloud.network.set_tags(sot, ["blue"]) + sot = self.user_cloud.network.get_subnet_pool(self.SUBNET_POOL_ID) + self.assertEqual(["blue"], sot.tags) - self.conn.network.set_tags(sot, []) - sot = self.conn.network.get_subnet_pool(self.SUBNET_POOL_ID) + self.user_cloud.network.set_tags(sot, []) + sot = self.user_cloud.network.get_subnet_pool(self.SUBNET_POOL_ID) self.assertEqual([], sot.tags) diff --git a/openstack/tests/functional/network/v2/test_trunk.py b/openstack/tests/functional/network/v2/test_trunk.py index c3bccc811..b0d29348b 100644 --- a/openstack/tests/functional/network/v2/test_trunk.py +++ b/openstack/tests/functional/network/v2/test_trunk.py @@ -25,67 +25,76 @@ def setUp(self): super(TestTrunk, self).setUp() # Skip the tests if trunk extension is not enabled. - if not self.conn.network.find_extension('trunk'): - self.skipTest('Network trunk extension disabled') + if not self.user_cloud.network.find_extension("trunk"): + self.skipTest("Network trunk extension disabled") self.TRUNK_NAME = self.getUniqueString() self.TRUNK_NAME_UPDATED = self.getUniqueString() - net = self.conn.network.create_network() + net = self.user_cloud.network.create_network() assert isinstance(net, network.Network) self.NET_ID = net.id - prt = self.conn.network.create_port(network_id=self.NET_ID) + prt = self.user_cloud.network.create_port(network_id=self.NET_ID) assert isinstance(prt, port.Port) self.PORT_ID = prt.id self.ports_to_clean = [self.PORT_ID] - trunk = self.conn.network.create_trunk( - name=self.TRUNK_NAME, - port_id=self.PORT_ID) + trunk = self.user_cloud.network.create_trunk( + name=self.TRUNK_NAME, port_id=self.PORT_ID + ) assert isinstance(trunk, _trunk.Trunk) self.TRUNK_ID = trunk.id def tearDown(self): - self.conn.network.delete_trunk(self.TRUNK_ID, ignore_missing=False) + self.user_cloud.network.delete_trunk( + self.TRUNK_ID, ignore_missing=False + ) for port_id in self.ports_to_clean: - self.conn.network.delete_port(port_id, ignore_missing=False) - self.conn.network.delete_network(self.NET_ID, ignore_missing=False) + self.user_cloud.network.delete_port(port_id, ignore_missing=False) + self.user_cloud.network.delete_network( + self.NET_ID, ignore_missing=False + ) super(TestTrunk, self).tearDown() def test_find(self): - sot = self.conn.network.find_trunk(self.TRUNK_NAME) + sot = self.user_cloud.network.find_trunk(self.TRUNK_NAME) self.assertEqual(self.TRUNK_ID, sot.id) def test_get(self): - sot = self.conn.network.get_trunk(self.TRUNK_ID) + sot = self.user_cloud.network.get_trunk(self.TRUNK_ID) self.assertEqual(self.TRUNK_ID, sot.id) self.assertEqual(self.TRUNK_NAME, sot.name) def test_list(self): - ids = [o.id for o in self.conn.network.trunks()] + ids = [o.id for o in self.user_cloud.network.trunks()] self.assertIn(self.TRUNK_ID, ids) def test_update(self): - sot = self.conn.network.update_trunk(self.TRUNK_ID, - name=self.TRUNK_NAME_UPDATED) + sot = self.user_cloud.network.update_trunk( + self.TRUNK_ID, name=self.TRUNK_NAME_UPDATED + ) self.assertEqual(self.TRUNK_NAME_UPDATED, sot.name) def test_subports(self): - port_for_subport = self.conn.network.create_port( - network_id=self.NET_ID) + port_for_subport = self.user_cloud.network.create_port( + network_id=self.NET_ID + ) self.ports_to_clean.append(port_for_subport.id) - subports = [{ - 'port_id': port_for_subport.id, - 'segmentation_type': 'vlan', - 'segmentation_id': 111 - }] - - sot = self.conn.network.get_trunk_subports(self.TRUNK_ID) - self.assertEqual({'sub_ports': []}, sot) - - self.conn.network.add_trunk_subports(self.TRUNK_ID, subports) - sot = self.conn.network.get_trunk_subports(self.TRUNK_ID) - self.assertEqual({'sub_ports': subports}, sot) - - self.conn.network.delete_trunk_subports( - self.TRUNK_ID, [{'port_id': port_for_subport.id}]) - sot = self.conn.network.get_trunk_subports(self.TRUNK_ID) - self.assertEqual({'sub_ports': []}, sot) + subports = [ + { + "port_id": port_for_subport.id, + "segmentation_type": "vlan", + "segmentation_id": 111, + } + ] + + sot = self.user_cloud.network.get_trunk_subports(self.TRUNK_ID) + self.assertEqual({"sub_ports": []}, sot) + + self.user_cloud.network.add_trunk_subports(self.TRUNK_ID, subports) + sot = self.user_cloud.network.get_trunk_subports(self.TRUNK_ID) + self.assertEqual({"sub_ports": subports}, sot) + + self.user_cloud.network.delete_trunk_subports( + self.TRUNK_ID, [{"port_id": port_for_subport.id}] + ) + sot = self.user_cloud.network.get_trunk_subports(self.TRUNK_ID) + self.assertEqual({"sub_ports": []}, sot) diff --git a/openstack/tests/functional/network/v2/test_vpnaas.py b/openstack/tests/functional/network/v2/test_vpnaas.py index 46c34cadd..55ed989da 100644 --- a/openstack/tests/functional/network/v2/test_vpnaas.py +++ b/openstack/tests/functional/network/v2/test_vpnaas.py @@ -20,36 +20,43 @@ class TestVpnIkePolicy(base.BaseFunctionalTest): def setUp(self): super(TestVpnIkePolicy, self).setUp() - if not self.conn._has_neutron_extension('vpnaas_v2'): - self.skipTest('vpnaas_v2 service not supported by cloud') - self.IKE_POLICY_NAME = self.getUniqueString('ikepolicy') - self.UPDATE_NAME = self.getUniqueString('ikepolicy-updated') - policy = self.conn.network.create_vpn_ike_policy( - name=self.IKE_POLICY_NAME) + if not self.user_cloud._has_neutron_extension("vpnaas"): + self.skipTest("vpnaas service not supported by cloud") + self.IKEPOLICY_NAME = self.getUniqueString("ikepolicy") + self.UPDATE_NAME = self.getUniqueString("ikepolicy-updated") + policy = self.user_cloud.network.create_vpn_ike_policy( + name=self.IKEPOLICY_NAME + ) assert isinstance(policy, vpn_ike_policy.VpnIkePolicy) - self.assertEqual(self.IKE_POLICY_NAME, policy.name) + self.assertEqual(self.IKEPOLICY_NAME, policy.name) self.ID = policy.id def tearDown(self): - ike_policy = self.conn.network.\ - delete_vpn_ike_policy(self.ID, ignore_missing=True) - self.assertIsNone(ike_policy) + ikepolicy = self.user_cloud.network.delete_vpn_ike_policy( + self.ID, ignore_missing=True + ) + self.assertIsNone(ikepolicy) super(TestVpnIkePolicy, self).tearDown() def test_list(self): - policies = [f.name for f in self.conn.network.vpn_ikepolicies()] - self.assertIn(self.IKE_POLICY_NAME, policies) + policies = [ + f.name for f in + self.user_cloud.network.vpn_ike_policies()] + self.assertIn(self.IKEPOLICY_NAME, policies) def test_find(self): - policy = self.conn.network.find_vpn_ike_policy(self.IKE_POLICY_NAME) + policy = self.user_cloud.network.find_vpn_ike_policy( + self.IKEPOLICY_NAME + ) self.assertEqual(self.ID, policy.id) def test_get(self): - policy = self.conn.network.get_vpn_ike_policy(self.ID) - self.assertEqual(self.IKE_POLICY_NAME, policy.name) + policy = self.user_cloud.network.get_vpn_ike_policy(self.ID) + self.assertEqual(self.IKEPOLICY_NAME, policy.name) self.assertEqual(self.ID, policy.id) def test_update(self): - policy = self.conn.network.update_vpn_ike_policy( - self.ID, name=self.UPDATE_NAME) + policy = self.user_cloud.network.update_vpn_ike_policy( + self.ID, name=self.UPDATE_NAME + ) self.assertEqual(self.UPDATE_NAME, policy.name) From ae22f1532d8454e84fdc6f541f2458c877c1c194 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 30 Sep 2022 19:49:00 +0200 Subject: [PATCH 3158/3836] Whitelist cloud functional tests in acceptance Next bunch of functional tests adapted to be included in the acceptance tests. Inclusion of tests uncovered issue in getting quota related to the fact that not every user is capable to find/list projects. This is fixed by introduction of ForbiddenException, skipping it in the _find call and using current_project_id. Change-Id: I53e718de239de7fb6f0347ff995282da079c68f3 --- include-acceptance-regular-user.txt | 6 +- openstack/cloud/_network.py | 6 +- openstack/exceptions.py | 15 +++-- openstack/resource.py | 3 +- openstack/tests/functional/base.py | 2 + .../tests/functional/cloud/test_aggregate.py | 2 + .../tests/functional/cloud/test_compute.py | 5 ++ .../tests/functional/cloud/test_devstack.py | 10 --- .../tests/functional/cloud/test_domain.py | 2 + .../tests/functional/cloud/test_endpoints.py | 2 + .../tests/functional/cloud/test_flavor.py | 61 +++++++++++------- .../functional/cloud/test_floating_ip.py | 60 ++++++++++------- .../tests/functional/cloud/test_groups.py | 3 + .../tests/functional/cloud/test_identity.py | 2 + .../tests/functional/cloud/test_inventory.py | 3 + .../tests/functional/cloud/test_limits.py | 6 ++ .../functional/cloud/test_magnum_services.py | 4 +- .../tests/functional/cloud/test_network.py | 3 + .../tests/functional/cloud/test_object.py | 15 +++-- openstack/tests/functional/cloud/test_port.py | 64 +++++++------------ .../tests/functional/cloud/test_project.py | 3 + .../functional/cloud/test_project_cleanup.py | 3 + .../cloud/test_qos_bandwidth_limit_rule.py | 2 + .../cloud/test_qos_dscp_marking_rule.py | 2 + .../cloud/test_qos_minimum_bandwidth_rule.py | 2 + .../tests/functional/cloud/test_qos_policy.py | 2 + .../tests/functional/cloud/test_quotas.py | 43 ++++++++++--- .../tests/functional/cloud/test_router.py | 2 + .../functional/cloud/test_security_groups.py | 16 +++++ .../tests/functional/cloud/test_services.py | 2 + .../tests/functional/cloud/test_users.py | 3 + .../functional/cloud/test_volume_type.py | 2 + .../unit/cloud/test_cluster_templates.py | 4 +- .../tests/unit/cloud/test_identity_roles.py | 4 +- openstack/tests/unit/cloud/test_quotas.py | 4 +- 35 files changed, 240 insertions(+), 128 deletions(-) diff --git a/include-acceptance-regular-user.txt b/include-acceptance-regular-user.txt index 5c5a76019..62773e767 100644 --- a/include-acceptance-regular-user.txt +++ b/include-acceptance-regular-user.txt @@ -1,8 +1,12 @@ # This file contains list of tests that can work with regular user privileges # Until all tests are modified to properly identify whether they are able to # run or must skip the ones that are known to work are listed here. -openstack.tests.functional.network +### Block Storage openstack.tests.functional.block_storage.v3.test_volume # Do not enable test_backup for now, since it is not capable to determine # backup capabilities of the cloud # openstack.tests.functional.block_storage.v3.test_backup +### Cloud +openstack.tests.functional.cloud +### Network +openstack.tests.functional.network diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index 7358490a7..22e4b7dfe 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -1,3 +1,4 @@ + # 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 @@ -663,9 +664,8 @@ def get_network_quotas(self, name_or_id, details=False): :raises: OpenStackCloudException if it's not a valid project :returns: A network ``Quota`` object if found, else None. """ - proj = self.get_project(name_or_id) - if not proj: - raise exc.OpenStackCloudException("project does not exist") + proj = self.identity.find_project( + name_or_id, ignore_missing=False) return self.network.get_quota(proj.id, details) def get_network_extensions(self): diff --git a/openstack/exceptions.py b/openstack/exceptions.py index c2370fef5..ec13dde09 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -112,6 +112,11 @@ class BadRequestException(HttpException): pass +class ForbiddenException(HttpException): + """HTTP 403 Forbidden Request.""" + pass + + class ConflictException(HttpException): """HTTP 409 Conflict.""" pass @@ -187,12 +192,14 @@ def raise_from_response(response, error_message=None): if response.status_code < 400: return - if response.status_code == 409: - cls = ConflictException + if response.status_code == 400: + cls = BadRequestException + elif response.status_code == 403: + cls = ForbiddenException elif response.status_code == 404: cls = NotFoundException - elif response.status_code == 400: - cls = BadRequestException + elif response.status_code == 409: + cls = ConflictException elif response.status_code == 412: cls = PreconditionFailedException else: diff --git a/openstack/resource.py b/openstack/resource.py index 6044de88e..405507339 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -2226,7 +2226,8 @@ def find( id=name_or_id, connection=session._get_connection(), **params ) return match.fetch(session, microversion=microversion, **params) - except (exceptions.NotFoundException, exceptions.BadRequestException): + except (exceptions.NotFoundException, exceptions.BadRequestException, + exceptions.ForbiddenException): # NOTE(gtema): There are few places around openstack that return # 400 if we try to GET resource and it doesn't exist. pass diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index ff5d6b10c..381aece28 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -88,6 +88,8 @@ def _set_user_cloud(self, **kwargs): cloud=self._demo_name_alt, **kwargs) self.user_cloud_alt = connection.Connection(config=user_config_alt) _disable_keep_alive(self.user_cloud_alt) + else: + self.user_cloud_alt = None def _set_operator_cloud(self, **kwargs): operator_config = self.config.get_one(cloud=self._op_name, **kwargs) diff --git a/openstack/tests/functional/cloud/test_aggregate.py b/openstack/tests/functional/cloud/test_aggregate.py index d7bcc0fd4..e96b64f3d 100644 --- a/openstack/tests/functional/cloud/test_aggregate.py +++ b/openstack/tests/functional/cloud/test_aggregate.py @@ -23,6 +23,8 @@ class TestAggregate(base.BaseFunctionalTest): def test_aggregates(self): + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") aggregate_name = self.getUniqueString() availability_zone = self.getUniqueString() self.addCleanup(self.cleanup, aggregate_name) diff --git a/openstack/tests/functional/cloud/test_compute.py b/openstack/tests/functional/cloud/test_compute.py index f25421abd..d0fefb20f 100644 --- a/openstack/tests/functional/cloud/test_compute.py +++ b/openstack/tests/functional/cloud/test_compute.py @@ -150,6 +150,8 @@ def test_create_and_delete_server_with_config_drive_none(self): self.assertTrue(srv is None or srv.status.lower() == 'deleted') def test_list_all_servers(self): + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) server = self.user_cloud.create_server( name=self.server_name, @@ -474,6 +476,9 @@ def test_update_server(self): def test_get_compute_usage(self): '''Test usage functionality''' # Add a server so that we can know we have usage + if not self.operator_cloud: + # TODO(gtema) rework method not to require getting project + self.skipTest("Operator cloud is required for this test") self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) self.user_cloud.create_server( name=self.server_name, diff --git a/openstack/tests/functional/cloud/test_devstack.py b/openstack/tests/functional/cloud/test_devstack.py index 1c72c6aa4..52cced22a 100644 --- a/openstack/tests/functional/cloud/test_devstack.py +++ b/openstack/tests/functional/cloud/test_devstack.py @@ -43,13 +43,3 @@ def test_has_service(self): if os.environ.get( 'OPENSTACKSDK_HAS_{env}'.format(env=self.env), '0') == '1': self.assertTrue(self.user_cloud.has_service(self.service)) - - -class TestKeystoneVersion(base.BaseFunctionalTest): - - def test_keystone_version(self): - use_keystone_v2 = os.environ.get('OPENSTACKSDK_USE_KEYSTONE_V2', False) - if use_keystone_v2 and use_keystone_v2 != '0': - self.assertEqual('2.0', self.identity_version) - else: - self.assertEqual('3', self.identity_version) diff --git a/openstack/tests/functional/cloud/test_domain.py b/openstack/tests/functional/cloud/test_domain.py index 18a070f91..447864356 100644 --- a/openstack/tests/functional/cloud/test_domain.py +++ b/openstack/tests/functional/cloud/test_domain.py @@ -25,6 +25,8 @@ class TestDomain(base.BaseFunctionalTest): def setUp(self): super(TestDomain, self).setUp() + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") i_ver = self.operator_cloud.config.get_api_version('identity') if i_ver in ('2', '2.0'): self.skipTest('Identity service does not support domains') diff --git a/openstack/tests/functional/cloud/test_endpoints.py b/openstack/tests/functional/cloud/test_endpoints.py index 39a539709..2381f9775 100644 --- a/openstack/tests/functional/cloud/test_endpoints.py +++ b/openstack/tests/functional/cloud/test_endpoints.py @@ -34,6 +34,8 @@ class TestEndpoints(base.KeystoneBaseFunctionalTest): def setUp(self): super(TestEndpoints, self).setUp() + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") # Generate a random name for services and regions in this test self.new_item_name = 'test_' + ''.join( diff --git a/openstack/tests/functional/cloud/test_flavor.py b/openstack/tests/functional/cloud/test_flavor.py index 12e24057d..f1a76b7ff 100644 --- a/openstack/tests/functional/cloud/test_flavor.py +++ b/openstack/tests/functional/cloud/test_flavor.py @@ -35,13 +35,15 @@ def setUp(self): def _cleanup_flavors(self): exception_list = list() - for f in self.operator_cloud.list_flavors(get_extra=False): - if f['name'].startswith(self.new_item_name): - try: - self.operator_cloud.delete_flavor(f['id']) - except Exception as e: - # We were unable to delete a flavor, let's try with next - exception_list.append(str(e)) + if self.operator_cloud: + for f in self.operator_cloud.list_flavors(get_extra=False): + if f['name'].startswith(self.new_item_name): + try: + self.operator_cloud.delete_flavor(f['id']) + except Exception as e: + # We were unable to delete a flavor, let's try with + # next + exception_list.append(str(e)) continue if exception_list: # Raise an error: we must make users aware that something went @@ -49,6 +51,9 @@ def _cleanup_flavors(self): raise OpenStackCloudException('\n'.join(exception_list)) def test_create_flavor(self): + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") + flavor_name = self.new_item_name + '_create' flavor_kwargs = dict( name=flavor_name, ram=1024, vcpus=2, disk=10, ephemeral=5, @@ -85,24 +90,31 @@ def test_list_flavors(self): name=priv_flavor_name, ram=1024, vcpus=2, disk=10, is_public=False ) - # Create a public and private flavor. We expect both to be listed - # for an operator. - self.operator_cloud.create_flavor(**public_kwargs) - self.operator_cloud.create_flavor(**private_kwargs) - - flavors = self.operator_cloud.list_flavors(get_extra=False) - - # Flavor list will include the standard devstack flavors. We just want - # to make sure both of the flavors we just created are present. - found = [] - for f in flavors: - # extra_specs should be added within list_flavors() - self.assertIn('extra_specs', f) - if f['name'] in (pub_flavor_name, priv_flavor_name): - found.append(f) - self.assertEqual(2, len(found)) + if self.operator_cloud: + # Create a public and private flavor. We expect both to be listed + # for an operator. + self.operator_cloud.create_flavor(**public_kwargs) + self.operator_cloud.create_flavor(**private_kwargs) + + flavors = self.operator_cloud.list_flavors(get_extra=False) + + # Flavor list will include the standard devstack flavors. We just + # want to make sure both of the flavors we just created are + # present. + found = [] + for f in flavors: + # extra_specs should be added within list_flavors() + self.assertIn('extra_specs', f) + if f['name'] in (pub_flavor_name, priv_flavor_name): + found.append(f) + self.assertEqual(2, len(found)) + else: + self.user_cloud.list_flavors() def test_flavor_access(self): + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") + priv_flavor_name = self.new_item_name + '_private' private_kwargs = dict( name=priv_flavor_name, ram=1024, vcpus=2, disk=10, is_public=False @@ -141,6 +153,9 @@ def test_set_unset_flavor_specs(self): """ Test setting and unsetting flavor extra specs """ + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") + flavor_name = self.new_item_name + '_spec_test' kwargs = dict( name=flavor_name, ram=1024, vcpus=2, disk=10 diff --git a/openstack/tests/functional/cloud/test_floating_ip.py b/openstack/tests/functional/cloud/test_floating_ip.py index 8987decd0..23610c4a3 100644 --- a/openstack/tests/functional/cloud/test_floating_ip.py +++ b/openstack/tests/functional/cloud/test_floating_ip.py @@ -62,7 +62,7 @@ def _cleanup_network(self): r, subnet_id=s['id']) except Exception: pass - self.user_cloud.delete_router(r) + self.user_cloud.delete_router(r.id) except Exception as e: exception_list.append(e) tb_list.append(sys.exc_info()[2]) @@ -71,7 +71,7 @@ def _cleanup_network(self): for s in self.user_cloud.list_subnets(): if s['name'].startswith(self.new_item_name): try: - self.user_cloud.delete_subnet(s) + self.user_cloud.delete_subnet(s.id) except Exception as e: exception_list.append(e) tb_list.append(sys.exc_info()[2]) @@ -80,7 +80,7 @@ def _cleanup_network(self): for n in self.user_cloud.list_networks(): if n['name'].startswith(self.new_item_name): try: - self.user_cloud.delete_network(n) + self.user_cloud.delete_network(n.id) except Exception as e: exception_list.append(e) tb_list.append(sys.exc_info()[2]) @@ -104,7 +104,7 @@ def _cleanup_servers(self): for i in self.user_cloud.list_servers(bare=True): if i.name.startswith(self.new_item_name): try: - self.user_cloud.delete_server(i, wait=True) + self.user_cloud.delete_server(i.id, wait=True) except Exception as e: exception_list.append(str(e)) continue @@ -124,7 +124,7 @@ def _cleanup_ips(self, server): if (ip.get('fixed_ip', None) == fixed_ip or ip.get('fixed_ip_address', None) == fixed_ip): try: - self.user_cloud.delete_floating_ip(ip) + self.user_cloud.delete_floating_ip(ip.id) except Exception as e: exception_list.append(str(e)) continue @@ -235,38 +235,50 @@ def test_detach_ip_from_server(self): server_id=new_server.id, floating_ip_id=f_ip['id']) def test_list_floating_ips(self): - fip_admin = self.operator_cloud.create_floating_ip() - self.addCleanup(self.operator_cloud.delete_floating_ip, fip_admin.id) + if self.operator_cloud: + fip_admin = self.operator_cloud.create_floating_ip() + self.addCleanup( + self.operator_cloud.delete_floating_ip, fip_admin.id) fip_user = self.user_cloud.create_floating_ip() self.addCleanup(self.user_cloud.delete_floating_ip, fip_user.id) # Get all the floating ips. - fip_id_list = [ - fip.id for fip in self.operator_cloud.list_floating_ips() + if self.operator_cloud: + fip_op_id_list = [ + fip.id for fip in self.operator_cloud.list_floating_ips() + ] + fip_user_id_list = [ + fip.id for fip in self.user_cloud.list_floating_ips() ] + if self.user_cloud.has_service('network'): + self.assertIn(fip_user.id, fip_user_id_list) # Neutron returns all FIP for all projects by default - self.assertIn(fip_admin.id, fip_id_list) - self.assertIn(fip_user.id, fip_id_list) + if self.operator_cloud and fip_admin: + self.assertIn(fip_user.id, fip_op_id_list) # Ask Neutron for only a subset of all the FIPs. - filtered_fip_id_list = [ - fip.id for fip in self.operator_cloud.list_floating_ips( - {'tenant_id': self.user_cloud.current_project_id} - ) - ] - self.assertNotIn(fip_admin.id, filtered_fip_id_list) - self.assertIn(fip_user.id, filtered_fip_id_list) + if self.operator_cloud: + filtered_fip_id_list = [ + fip.id for fip in self.operator_cloud.list_floating_ips( + {'tenant_id': self.user_cloud.current_project_id} + ) + ] + self.assertNotIn(fip_admin.id, filtered_fip_id_list) + self.assertIn(fip_user.id, filtered_fip_id_list) else: - self.assertIn(fip_admin.id, fip_id_list) + if fip_admin: + self.assertIn(fip_admin.id, fip_op_id_list) # By default, Nova returns only the FIPs that belong to the # project which made the listing request. - self.assertNotIn(fip_user.id, fip_id_list) - self.assertRaisesRegex( - ValueError, "Nova-network don't support server-side.*", - self.operator_cloud.list_floating_ips, filters={'foo': 'bar'} - ) + if self.operator_cloud: + self.assertNotIn(fip_user.id, fip_op_id_list) + self.assertRaisesRegex( + ValueError, "Nova-network don't support server-side.*", + self.operator_cloud.list_floating_ips, + filters={'foo': 'bar'} + ) def test_search_floating_ips(self): fip_user = self.user_cloud.create_floating_ip() diff --git a/openstack/tests/functional/cloud/test_groups.py b/openstack/tests/functional/cloud/test_groups.py index e9ae28a84..3ef2a5626 100644 --- a/openstack/tests/functional/cloud/test_groups.py +++ b/openstack/tests/functional/cloud/test_groups.py @@ -25,6 +25,9 @@ class TestGroup(base.BaseFunctionalTest): def setUp(self): super(TestGroup, self).setUp() + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") + i_ver = self.operator_cloud.config.get_api_version('identity') if i_ver in ('2', '2.0'): self.skipTest('Identity service does not support groups') diff --git a/openstack/tests/functional/cloud/test_identity.py b/openstack/tests/functional/cloud/test_identity.py index 5c087ba93..118cf33f9 100644 --- a/openstack/tests/functional/cloud/test_identity.py +++ b/openstack/tests/functional/cloud/test_identity.py @@ -27,6 +27,8 @@ class TestIdentity(base.KeystoneBaseFunctionalTest): def setUp(self): super(TestIdentity, self).setUp() + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") self.role_prefix = 'test_role' + ''.join( random.choice(string.ascii_lowercase) for _ in range(5)) self.user_prefix = self.getUniqueString('user') diff --git a/openstack/tests/functional/cloud/test_inventory.py b/openstack/tests/functional/cloud/test_inventory.py index 1401e6c2d..79de47a83 100644 --- a/openstack/tests/functional/cloud/test_inventory.py +++ b/openstack/tests/functional/cloud/test_inventory.py @@ -26,6 +26,9 @@ class TestInventory(base.BaseFunctionalTest): def setUp(self): super().setUp() + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") + # This needs to use an admin account, otherwise a public IP # is not allocated from devstack. self.inventory = inventory.OpenStackInventory(cloud='devstack-admin') diff --git a/openstack/tests/functional/cloud/test_limits.py b/openstack/tests/functional/cloud/test_limits.py index a4ec061c9..160adaca6 100644 --- a/openstack/tests/functional/cloud/test_limits.py +++ b/openstack/tests/functional/cloud/test_limits.py @@ -33,6 +33,9 @@ def test_get_our_compute_limits(self): def test_get_other_compute_limits(self): '''Test quotas functionality''' + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") + limits = self.operator_cloud.get_compute_limits('demo') self.assertIsNotNone(limits) self.assertTrue(hasattr(limits, 'server_meta')) @@ -48,5 +51,8 @@ def test_get_our_volume_limits(self): def test_get_other_volume_limits(self): '''Test quotas functionality''' + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") + limits = self.operator_cloud.get_volume_limits('demo') self.assertFalse(hasattr(limits, 'maxTotalVolumes')) diff --git a/openstack/tests/functional/cloud/test_magnum_services.py b/openstack/tests/functional/cloud/test_magnum_services.py index 991b8aac2..80ef5ccf7 100644 --- a/openstack/tests/functional/cloud/test_magnum_services.py +++ b/openstack/tests/functional/cloud/test_magnum_services.py @@ -24,7 +24,7 @@ class TestMagnumServices(base.BaseFunctionalTest): def setUp(self): super(TestMagnumServices, self).setUp() - if not self.operator_cloud.has_service( + if not self.user_cloud.has_service( 'container-infrastructure-management' ): self.skipTest('Container service not supported by cloud') @@ -33,7 +33,7 @@ def test_magnum_services(self): '''Test magnum services functionality''' # Test that we can list services - services = self.operator_cloud.list_magnum_services() + services = self.user_cloud.list_magnum_services() self.assertEqual(1, len(services)) self.assertEqual(services[0]['id'], 1) diff --git a/openstack/tests/functional/cloud/test_network.py b/openstack/tests/functional/cloud/test_network.py index d9d9cada7..d4cfe4267 100644 --- a/openstack/tests/functional/cloud/test_network.py +++ b/openstack/tests/functional/cloud/test_network.py @@ -24,6 +24,9 @@ class TestNetwork(base.BaseFunctionalTest): def setUp(self): super(TestNetwork, self).setUp() + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") + if not self.operator_cloud.has_service('network'): self.skipTest('Network service not supported by cloud') self.network_name = self.getUniqueString('network') diff --git a/openstack/tests/functional/cloud/test_object.py b/openstack/tests/functional/cloud/test_object.py index 3ee3e3753..cc707f53d 100644 --- a/openstack/tests/functional/cloud/test_object.py +++ b/openstack/tests/functional/cloud/test_object.py @@ -40,10 +40,12 @@ def test_create_object(self): self.addDetail('container', content.text_content(container_name)) self.addCleanup(self.user_cloud.delete_container, container_name) self.user_cloud.create_container(container_name) - self.assertEqual(container_name, - self.user_cloud.list_containers()[0]['name']) - self.assertEqual([], - self.user_cloud.list_containers(prefix='somethin')) + container = self.user_cloud.get_container(container_name) + self.assertEqual( + container_name, container.name) + self.assertEqual( + [], + self.user_cloud.list_containers(prefix='somethin')) sizes = ( (64 * 1024, 1), # 64K, one segment (64 * 1024, 5) # 64MB, 5 segments @@ -99,8 +101,9 @@ def test_create_object(self): self.assertTrue( self.user_cloud.delete_object(container_name, name)) self.assertEqual([], self.user_cloud.list_objects(container_name)) - self.assertEqual(container_name, - self.user_cloud.list_containers()[0]['name']) + self.assertEqual( + container_name, + self.user_cloud.get_container(container_name).name) self.user_cloud.delete_container(container_name) def test_download_object_to_file(self): diff --git a/openstack/tests/functional/cloud/test_port.py b/openstack/tests/functional/cloud/test_port.py index b865b5205..a798a20f9 100644 --- a/openstack/tests/functional/cloud/test_port.py +++ b/openstack/tests/functional/cloud/test_port.py @@ -31,9 +31,13 @@ class TestPort(base.BaseFunctionalTest): def setUp(self): super(TestPort, self).setUp() # Skip Neutron tests if neutron is not present - if not self.operator_cloud.has_service('network'): + if not self.user_cloud.has_service('network'): self.skipTest('Network service not supported by cloud') + net_name = self.getUniqueString('CloudPortName') + self.net = self.user_cloud.network.create_network(name=net_name) + self.addCleanup(self.user_cloud.network.delete_network, self.net.id) + # Generate a unique port name to allow concurrent tests self.new_port_name = 'test_' + ''.join( random.choice(string.ascii_lowercase) for _ in range(5)) @@ -43,10 +47,10 @@ def setUp(self): def _cleanup_ports(self): exception_list = list() - for p in self.operator_cloud.list_ports(): + for p in self.user_cloud.list_ports(): if p['name'].startswith(self.new_port_name): try: - self.operator_cloud.delete_port(name_or_id=p['id']) + self.user_cloud.delete_port(name_or_id=p['id']) except Exception as e: # We were unable to delete this port, let's try with next exception_list.append(str(e)) @@ -60,12 +64,8 @@ def _cleanup_ports(self): def test_create_port(self): port_name = self.new_port_name + '_create' - networks = self.operator_cloud.list_networks() - if not networks: - self.assertFalse('no sensible network available') - - port = self.operator_cloud.create_port( - network_id=networks[0]['id'], name=port_name) + port = self.user_cloud.create_port( + network_id=self.net.id, name=port_name) self.assertIsInstance(port, dict) self.assertIn('id', port) self.assertEqual(port.get('name'), port_name) @@ -73,17 +73,13 @@ def test_create_port(self): def test_get_port(self): port_name = self.new_port_name + '_get' - networks = self.operator_cloud.list_networks() - if not networks: - self.assertFalse('no sensible network available') - - port = self.operator_cloud.create_port( - network_id=networks[0]['id'], name=port_name) + port = self.user_cloud.create_port( + network_id=self.net.id, name=port_name) self.assertIsInstance(port, dict) self.assertIn('id', port) self.assertEqual(port.get('name'), port_name) - updated_port = self.operator_cloud.get_port(name_or_id=port['id']) + updated_port = self.user_cloud.get_port(name_or_id=port['id']) # extra_dhcp_opts is added later by Neutron... if 'extra_dhcp_opts' in updated_port and 'extra_dhcp_opts' not in port: del updated_port['extra_dhcp_opts'] @@ -92,17 +88,13 @@ def test_get_port(self): def test_get_port_by_id(self): port_name = self.new_port_name + '_get_by_id' - networks = self.operator_cloud.list_networks() - if not networks: - self.assertFalse('no sensible network available') - - port = self.operator_cloud.create_port( - network_id=networks[0]['id'], name=port_name) + port = self.user_cloud.create_port( + network_id=self.net.id, name=port_name) self.assertIsInstance(port, dict) self.assertIn('id', port) self.assertEqual(port.get('name'), port_name) - updated_port = self.operator_cloud.get_port_by_id(port['id']) + updated_port = self.user_cloud.get_port_by_id(port['id']) # extra_dhcp_opts is added later by Neutron... if 'extra_dhcp_opts' in updated_port and 'extra_dhcp_opts' not in port: del updated_port['extra_dhcp_opts'] @@ -112,19 +104,15 @@ def test_update_port(self): port_name = self.new_port_name + '_update' new_port_name = port_name + '_new' - networks = self.operator_cloud.list_networks() - if not networks: - self.assertFalse('no sensible network available') - - self.operator_cloud.create_port( - network_id=networks[0]['id'], name=port_name) + self.user_cloud.create_port( + network_id=self.net.id, name=port_name) - port = self.operator_cloud.update_port( + port = self.user_cloud.update_port( name_or_id=port_name, name=new_port_name) self.assertIsInstance(port, dict) self.assertEqual(port.get('name'), new_port_name) - updated_port = self.operator_cloud.get_port(name_or_id=port['id']) + updated_port = self.user_cloud.get_port(name_or_id=port['id']) self.assertEqual(port.get('name'), new_port_name) port.pop('revision_number', None) port.pop(u'revision_number', None) @@ -140,20 +128,16 @@ def test_update_port(self): def test_delete_port(self): port_name = self.new_port_name + '_delete' - networks = self.operator_cloud.list_networks() - if not networks: - self.assertFalse('no sensible network available') - - port = self.operator_cloud.create_port( - network_id=networks[0]['id'], name=port_name) + port = self.user_cloud.create_port( + network_id=self.net.id, name=port_name) self.assertIsInstance(port, dict) self.assertIn('id', port) self.assertEqual(port.get('name'), port_name) - updated_port = self.operator_cloud.get_port(name_or_id=port['id']) + updated_port = self.user_cloud.get_port(name_or_id=port['id']) self.assertIsNotNone(updated_port) - self.operator_cloud.delete_port(name_or_id=port_name) + self.user_cloud.delete_port(name_or_id=port_name) - updated_port = self.operator_cloud.get_port(name_or_id=port['id']) + updated_port = self.user_cloud.get_port(name_or_id=port['id']) self.assertIsNone(updated_port) diff --git a/openstack/tests/functional/cloud/test_project.py b/openstack/tests/functional/cloud/test_project.py index b51e1d4d7..26390f7db 100644 --- a/openstack/tests/functional/cloud/test_project.py +++ b/openstack/tests/functional/cloud/test_project.py @@ -28,6 +28,9 @@ class TestProject(base.KeystoneBaseFunctionalTest): def setUp(self): super(TestProject, self).setUp() + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") + self.new_project_name = self.getUniqueString('project') self.addCleanup(self._cleanup_projects) diff --git a/openstack/tests/functional/cloud/test_project_cleanup.py b/openstack/tests/functional/cloud/test_project_cleanup.py index f6bed34c0..9016358cc 100644 --- a/openstack/tests/functional/cloud/test_project_cleanup.py +++ b/openstack/tests/functional/cloud/test_project_cleanup.py @@ -27,6 +27,9 @@ class TestProjectCleanup(base.BaseFunctionalTest): def setUp(self): super(TestProjectCleanup, self).setUp() + if not self.user_cloud_alt: + self.skipTest("Alternate demo cloud is required for this test") + self.conn = self.user_cloud_alt self.network_name = self.getUniqueString('network') diff --git a/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py b/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py index 8b731ec5e..e91098df2 100644 --- a/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py @@ -25,6 +25,8 @@ class TestQosBandwidthLimitRule(base.BaseFunctionalTest): def setUp(self): super(TestQosBandwidthLimitRule, self).setUp() + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") if not self.operator_cloud.has_service('network'): self.skipTest('Network service not supported by cloud') if not self.operator_cloud._has_neutron_extension('qos'): diff --git a/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py b/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py index a08f128a6..aff8813b3 100644 --- a/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py +++ b/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py @@ -25,6 +25,8 @@ class TestQosDscpMarkingRule(base.BaseFunctionalTest): def setUp(self): super(TestQosDscpMarkingRule, self).setUp() + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") if not self.operator_cloud.has_service('network'): self.skipTest('Network service not supported by cloud') if not self.operator_cloud._has_neutron_extension('qos'): diff --git a/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py b/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py index 36e3d7588..8cc16a893 100644 --- a/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py +++ b/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py @@ -25,6 +25,8 @@ class TestQosMinimumBandwidthRule(base.BaseFunctionalTest): def setUp(self): super(TestQosMinimumBandwidthRule, self).setUp() + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") if not self.operator_cloud.has_service('network'): self.skipTest('Network service not supported by cloud') if not self.operator_cloud._has_neutron_extension('qos'): diff --git a/openstack/tests/functional/cloud/test_qos_policy.py b/openstack/tests/functional/cloud/test_qos_policy.py index f78834383..6bd3150ce 100644 --- a/openstack/tests/functional/cloud/test_qos_policy.py +++ b/openstack/tests/functional/cloud/test_qos_policy.py @@ -25,6 +25,8 @@ class TestQosPolicy(base.BaseFunctionalTest): def setUp(self): super(TestQosPolicy, self).setUp() + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") if not self.operator_cloud.has_service('network'): self.skipTest('Network service not supported by cloud') if not self.operator_cloud._has_neutron_extension('qos'): diff --git a/openstack/tests/functional/cloud/test_quotas.py b/openstack/tests/functional/cloud/test_quotas.py index a1ae19745..2d06167a4 100644 --- a/openstack/tests/functional/cloud/test_quotas.py +++ b/openstack/tests/functional/cloud/test_quotas.py @@ -22,8 +22,16 @@ class TestComputeQuotas(base.BaseFunctionalTest): - def test_quotas(self): + def test_get_quotas(self): + '''Test quotas functionality''' + self.user_cloud.get_compute_quotas( + self.user_cloud.current_project_id) + + def test_set_quotas(self): '''Test quotas functionality''' + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") + quotas = self.operator_cloud.get_compute_quotas('demo') cores = quotas['cores'] self.operator_cloud.set_compute_quotas('demo', cores=cores + 1) @@ -39,11 +47,20 @@ class TestVolumeQuotas(base.BaseFunctionalTest): def setUp(self): super(TestVolumeQuotas, self).setUp() - if not self.operator_cloud.has_service('volume'): + if not self.user_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') - def test_quotas(self): - '''Test quotas functionality''' + def test_get_quotas(self): + '''Test get quotas functionality''' + self.user_cloud.get_volume_quotas( + self.user_cloud.current_project_id + ) + + def test_set_quotas(self): + '''Test set quotas functionality''' + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") + quotas = self.operator_cloud.get_volume_quotas('demo') volumes = quotas['volumes'] self.operator_cloud.set_volume_quotas('demo', volumes=volumes + 1) @@ -58,13 +75,18 @@ def test_quotas(self): class TestNetworkQuotas(base.BaseFunctionalTest): - def setUp(self): - super(TestNetworkQuotas, self).setUp() - if not self.operator_cloud.has_service('network'): - self.skipTest('network service not supported by cloud') + def test_get_quotas(self): + '''Test get quotas functionality''' + self.user_cloud.get_network_quotas( + self.user_cloud.current_project_id) def test_quotas(self): '''Test quotas functionality''' + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") + if not self.operator_cloud.has_service('network'): + self.skipTest('network service not supported by cloud') + quotas = self.operator_cloud.get_network_quotas('demo') network = quotas['networks'] self.operator_cloud.set_network_quotas('demo', networks=network + 1) @@ -77,6 +99,11 @@ def test_quotas(self): self.operator_cloud.get_network_quotas('demo')['networks']) def test_get_quotas_details(self): + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") + if not self.operator_cloud.has_service('network'): + self.skipTest('network service not supported by cloud') + quotas = [ 'floating_ips', 'networks', 'ports', 'rbac_policies', 'routers', 'subnets', diff --git a/openstack/tests/functional/cloud/test_router.py b/openstack/tests/functional/cloud/test_router.py index 8c18392f8..ef1dbc2d9 100644 --- a/openstack/tests/functional/cloud/test_router.py +++ b/openstack/tests/functional/cloud/test_router.py @@ -34,6 +34,8 @@ class TestRouter(base.BaseFunctionalTest): def setUp(self): super(TestRouter, self).setUp() + if not self.operator_cloud: + self.skipTest("Operator cloud required for this test") if not self.operator_cloud.has_service('network'): self.skipTest('Network service not supported by cloud') diff --git a/openstack/tests/functional/cloud/test_security_groups.py b/openstack/tests/functional/cloud/test_security_groups.py index 1a249da77..54ceda6fc 100644 --- a/openstack/tests/functional/cloud/test_security_groups.py +++ b/openstack/tests/functional/cloud/test_security_groups.py @@ -22,6 +22,22 @@ class TestSecurityGroups(base.BaseFunctionalTest): def test_create_list_security_groups(self): + sg1 = self.user_cloud.create_security_group( + name="sg1", description="sg1") + self.addCleanup(self.user_cloud.delete_security_group, sg1['id']) + if self.user_cloud.has_service('network'): + # Neutron defaults to all_tenants=1 when admin + sg_list = self.user_cloud.list_security_groups() + self.assertIn(sg1['id'], [sg['id'] for sg in sg_list]) + + else: + # Nova does not list all tenants by default + sg_list = self.operator_cloud.list_security_groups() + + def test_create_list_security_groups_operator(self): + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") + sg1 = self.user_cloud.create_security_group( name="sg1", description="sg1") self.addCleanup(self.user_cloud.delete_security_group, sg1['id']) diff --git a/openstack/tests/functional/cloud/test_services.py b/openstack/tests/functional/cloud/test_services.py index 8e1036e33..70c85d58d 100644 --- a/openstack/tests/functional/cloud/test_services.py +++ b/openstack/tests/functional/cloud/test_services.py @@ -33,6 +33,8 @@ class TestServices(base.KeystoneBaseFunctionalTest): def setUp(self): super(TestServices, self).setUp() + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") # Generate a random name for services in this test self.new_service_name = 'test_' + ''.join( diff --git a/openstack/tests/functional/cloud/test_users.py b/openstack/tests/functional/cloud/test_users.py index f69b4c265..3dc94a220 100644 --- a/openstack/tests/functional/cloud/test_users.py +++ b/openstack/tests/functional/cloud/test_users.py @@ -24,6 +24,9 @@ class TestUsers(base.KeystoneBaseFunctionalTest): def setUp(self): super(TestUsers, self).setUp() + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") + self.user_prefix = self.getUniqueString('user') self.addCleanup(self._cleanup_users) diff --git a/openstack/tests/functional/cloud/test_volume_type.py b/openstack/tests/functional/cloud/test_volume_type.py index 1e7821e4d..d71ee014c 100644 --- a/openstack/tests/functional/cloud/test_volume_type.py +++ b/openstack/tests/functional/cloud/test_volume_type.py @@ -33,6 +33,8 @@ def _assert_project(self, volume_name_or_id, project_id, allowed=True): def setUp(self): super(TestVolumeType, self).setUp() + if not self.operator_cloud: + self.skipTest("Operator cloud is required for this test") if not self.user_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') volume_type = { diff --git a/openstack/tests/unit/cloud/test_cluster_templates.py b/openstack/tests/unit/cloud/test_cluster_templates.py index 13f02e612..15e41d836 100644 --- a/openstack/tests/unit/cloud/test_cluster_templates.py +++ b/openstack/tests/unit/cloud/test_cluster_templates.py @@ -13,7 +13,7 @@ import munch import testtools -import openstack.cloud +from openstack import exceptions from openstack.tests.unit import base @@ -198,7 +198,7 @@ def test_create_cluster_template_exception(self): # match the more specific HTTPError, even though it's a subclass # of OpenStackCloudException. with testtools.ExpectedException( - openstack.cloud.OpenStackCloudHTTPError): + exceptions.ForbiddenException): self.cloud.create_cluster_template('fake-cluster-template') self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_identity_roles.py b/openstack/tests/unit/cloud/test_identity_roles.py index 517c54051..045552325 100644 --- a/openstack/tests/unit/cloud/test_identity_roles.py +++ b/openstack/tests/unit/cloud/test_identity_roles.py @@ -14,7 +14,7 @@ import testtools from testtools import matchers -import openstack.cloud +from openstack import exceptions from openstack.tests.unit import base @@ -248,7 +248,7 @@ def test_list_role_assignments_exception(self): status_code=403) ]) with testtools.ExpectedException( - openstack.cloud.exc.OpenStackCloudHTTPError + exceptions.ForbiddenException ): self.cloud.list_role_assignments() self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_quotas.py b/openstack/tests/unit/cloud/test_quotas.py index 8eec55af0..00da3985d 100644 --- a/openstack/tests/unit/cloud/test_quotas.py +++ b/openstack/tests/unit/cloud/test_quotas.py @@ -212,7 +212,7 @@ def test_neutron_get_quotas(self): 'port': 500 } project = self.mock_for_keystone_projects(project_count=1, - list_get=True)[0] + id_get=True)[0] self.register_uris([ dict(method='GET', uri=self.get_mock_url( @@ -272,7 +272,7 @@ def test_neutron_get_quotas_details(self): 'reserved': 0} } project = self.mock_for_keystone_projects(project_count=1, - list_get=True)[0] + id_get=True)[0] self.register_uris([ dict(method='GET', uri=self.get_mock_url( From d0e89705033807ae77490038dabaf1a00f9996e1 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 5 Dec 2022 10:17:26 +0000 Subject: [PATCH 3159/3836] baremetal: Add Node.inject_nmi method Noted while attempting to migrate nova from ironicclient to openstacksdk. Change-Id: I3fc92219f55bb723d7675c1c0c078b9c9b8da304 Signed-off-by: Stephen Finucane --- doc/source/user/proxies/baremetal.rst | 16 +++++++--- openstack/baremetal/v1/_common.py | 3 ++ openstack/baremetal/v1/_proxy.py | 15 ++++++++++ openstack/baremetal/v1/node.py | 28 ++++++++++++++++++ .../tests/unit/baremetal/v1/test_node.py | 29 +++++++++++++++++++ .../node-inject-nmi-53d12681026e0b6c.yaml | 6 ++++ 6 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/node-inject-nmi-53d12681026e0b6c.yaml diff --git a/doc/source/user/proxies/baremetal.rst b/doc/source/user/proxies/baremetal.rst index d8978c0ff..da35cdcab 100644 --- a/doc/source/user/proxies/baremetal.rst +++ b/doc/source/user/proxies/baremetal.rst @@ -16,10 +16,18 @@ Node Operations ^^^^^^^^^^^^^^^ .. autoclass:: openstack.baremetal.v1._proxy.Proxy :noindex: - :members: nodes, find_node, get_node, create_node, update_node, patch_node, delete_node, - validate_node, set_node_power_state, set_node_provision_state, - wait_for_nodes_provision_state, wait_for_node_power_state, - wait_for_node_reservation, set_node_maintenance, unset_node_maintenance + :members: nodes, create_node, find_node, get_node, update_node, patch_node, delete_node, + set_node_provision_state, set_node_boot_device, set_node_boot_mode, + set_node_secure_boot, inject_nmi_to_node, wait_for_nodes_provision_state, + set_node_power_state, wait_for_node_power_state, + wait_for_node_reservation, validate_node, set_node_maintenance, + unset_node_maintenance, delete_node, list_node_vendor_passthru + +Node Trait Operations +^^^^^^^^^^^^^^^^^^^^^ +.. autoclass:: openstack.baremetal.v1._proxy.Proxy + :noindex: + :members: add_node_trait, remove_node_trait, set_node_traits Port Operations ^^^^^^^^^^^^^^^ diff --git a/openstack/baremetal/v1/_common.py b/openstack/baremetal/v1/_common.py index 54f1862e7..3cbb68ba8 100644 --- a/openstack/baremetal/v1/_common.py +++ b/openstack/baremetal/v1/_common.py @@ -68,6 +68,9 @@ VIF_VERSION = '1.28' """API version in which the VIF operations were introduced.""" +INJECT_NMI_VERSION = '1.29' +"""API vresion in which support for injecting NMI was introduced.""" + CONFIG_DRIVE_REBUILD_VERSION = '1.35' """API version in which rebuild accepts a configdrive.""" diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 1722f5ac8..0285fc773 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -445,6 +445,21 @@ def set_node_secure_boot(self, node, target): res = self._get_resource(_node.Node, node) return res.set_secure_boot(self, target) + def inject_nmi_to_node(self, node): + """Inject NMI to node. + + Injects a non-maskable interrupt (NMI) message to the node. This is + used when response time is critical, such as during non-recoverable + hardware errors. In addition, virsh inject-nmi is useful for triggering + a crashdump in Windows guests. + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :return: None + """ + res = self._get_resource(_node.Node, node) + res.inject_nmi(self) + def wait_for_nodes_provision_state(self, nodes, expected_state, timeout=None, abort_on_failed_state=True, diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 0a3a1385d..276540abc 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -602,6 +602,34 @@ def _check_state_reached(self, session, expected_state, "the last error is %(error)s" % {'node': self.id, 'error': self.last_error}) + def inject_nmi(self, session): + """Inject NMI. + + :param session: The session to use for making this request. + :return: None + """ + session = self._get_session(session) + version = self._assert_microversion_for( + session, + 'commit', + _common.INJECT_NMI_VERSION, + ) + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'management', 'inject_nmi') + + body = {} + + response = session.put( + request.url, + json=body, + headers=request.headers, + microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) + + msg = ("Failed to inject NMI to node {node}".format(node=self.id)) + exceptions.raise_from_response(response, error_message=msg) + def set_power_state(self, session, target, wait=False, timeout=None): """Run an action modifying this node's power state. diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 3d001497b..73386fd3e 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -590,6 +590,35 @@ def test_timeout(self, mock_fetch): mock_fetch.assert_called_with(self.node, self.session) +@mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) +class TestNodeInjectNMI(base.TestCase): + + def setUp(self): + super().setUp() + self.node = node.Node(**FAKE) + self.session = mock.Mock(spec=adapter.Adapter) + self.session.default_microversion = '1.29' + self.node = node.Node(**FAKE) + + def test_inject_nmi(self): + self.node.inject_nmi(self.session) + self.session.put.assert_called_once_with( + 'nodes/%s/management/inject_nmi' % FAKE['uuid'], + json={}, + headers=mock.ANY, + microversion='1.29', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) + + def test_incompatible_microversion(self): + self.session.default_microversion = '1.28' + self.assertRaises( + exceptions.NotSupported, + self.node.inject_nmi, + self.session, + ) + + @mock.patch.object(node.Node, '_assert_microversion_for', _fake_assert) @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) class TestNodeSetPowerState(base.TestCase): diff --git a/releasenotes/notes/node-inject-nmi-53d12681026e0b6c.yaml b/releasenotes/notes/node-inject-nmi-53d12681026e0b6c.yaml new file mode 100644 index 000000000..41f065451 --- /dev/null +++ b/releasenotes/notes/node-inject-nmi-53d12681026e0b6c.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Adds ``inject_nmi`` ``openstack.baremetal.v1.Node``. + - | + Adds ``inject_nmi_to_node`` to the baremetal Proxy. From 397562bc42196c03ba63894ba46761ecdc25fecd Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 5 Dec 2022 11:31:29 +0000 Subject: [PATCH 3160/3836] baremetal: Add Node console methods Add Node.get_console and Node.set_console_mode methods. Noted while attempting to migrate nova from ironicclient to openstacksdk Change-Id: I6eb255de711b55d56056f8a29828f30e45a105c8 Signed-off-by: Stephen Finucane --- doc/source/user/proxies/baremetal.rst | 3 +- openstack/baremetal/v1/_proxy.py | 38 ++++++++++++++- openstack/baremetal/v1/node.py | 46 +++++++++++++++++++ .../tests/unit/baremetal/v1/test_node.py | 40 ++++++++++++++++ .../notes/node-consoles-63589f22da98a689.yaml | 8 ++++ 5 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/node-consoles-63589f22da98a689.yaml diff --git a/doc/source/user/proxies/baremetal.rst b/doc/source/user/proxies/baremetal.rst index da35cdcab..0b760a18f 100644 --- a/doc/source/user/proxies/baremetal.rst +++ b/doc/source/user/proxies/baremetal.rst @@ -21,7 +21,8 @@ Node Operations set_node_secure_boot, inject_nmi_to_node, wait_for_nodes_provision_state, set_node_power_state, wait_for_node_power_state, wait_for_node_reservation, validate_node, set_node_maintenance, - unset_node_maintenance, delete_node, list_node_vendor_passthru + unset_node_maintenance, delete_node, list_node_vendor_passthru, + get_node_console, enable_node_console, disable_node_console Node Trait Operations ^^^^^^^^^^^^^^^^^^^^^ diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 0285fc773..8108939f0 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -1103,10 +1103,12 @@ def remove_node_trait(self, node, trait, ignore_missing=True): def call_node_vendor_passthru(self, node, verb, method, body=None): """Calls vendor_passthru for a node. - :param session: The session to use for making this request. + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. :param verb: The HTTP verb, one of GET, SET, POST, DELETE. :param method: The method to call using vendor_passthru. :param body: The JSON body in the HTTP call. + :returns: The raw response from the method. """ res = self._get_resource(_node.Node, node) return res.call_vendor_passthru(self, verb, method, body) @@ -1114,11 +1116,43 @@ def call_node_vendor_passthru(self, node, verb, method, body=None): def list_node_vendor_passthru(self, node): """Lists vendor_passthru for a node. - :param session: The session to use for making this request. + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :returns: A list of vendor_passthru methods for the node. """ res = self._get_resource(_node.Node, node) return res.list_vendor_passthru(self) + def get_node_console(self, node): + """Get the console for a node. + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :returns: Connection information for the console. + """ + res = self._get_resource(_node.Node, node) + return res.get_node_console(self) + + def enable_node_console(self, node): + """Enable the console for a node. + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :returns: None + """ + res = self._get_resource(_node.Node, node) + return res.set_console_mode(self, True) + + def disable_node_console(self, node): + """Disable the console for a node. + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :returns: None + """ + res = self._get_resource(_node.Node, node) + return res.set_console_mode(self, False) + def set_node_traits(self, node, traits): """Set traits for a node. diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 276540abc..8d117b069 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -1080,6 +1080,52 @@ def list_vendor_passthru(self, session): return response.json() + def get_console(self, session): + session = self._get_session(session) + version = self._get_microversion(session, action='fetch') + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'states', 'console') + + response = session.get( + request.url, + headers=request.headers, + microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) + + msg = "Failed to get console for node {node}".format( + node=self.id, + ) + exceptions.raise_from_response(response, error_message=msg) + + return response.json() + + def set_console_mode(self, session, enabled): + session = self._get_session(session) + version = self._get_microversion(session, action='commit') + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'states', 'console') + if not isinstance(enabled, bool): + raise ValueError( + "Invalid enabled %s. It should be True or False " + "corresponding to console enabled or disabled" + % enabled + ) + body = {'enabled': enabled} + + response = session.put( + request.url, + json=body, + headers=request.headers, + microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) + + msg = "Failed to change console mode for {node}".format( + node=self.id, + ) + exceptions.raise_from_response(response, error_message=msg) + def patch(self, session, patch=None, prepend_key=True, has_body=True, retry_on_conflict=None, base_path=None, reset_interfaces=None): diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 73386fd3e..a943153d4 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -990,3 +990,43 @@ def test_list_passthru(self): 'nodes/%s/vendor_passthru/methods' % self.node.id, headers=mock.ANY, microversion='1.37', retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + +@mock.patch.object(node.Node, 'fetch', lambda self, session: self) +@mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) +class TestNodeConsole(base.TestCase): + + def setUp(self): + super().setUp() + self.node = node.Node(**FAKE) + self.session = mock.Mock( + spec=adapter.Adapter, + default_microversion='1.1', + ) + + def test_get_console(self): + self.node.get_console(self.session) + self.session.get.assert_called_once_with( + 'nodes/%s/states/console' % self.node.id, + headers=mock.ANY, + microversion=mock.ANY, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) + + def test_set_console_mode(self): + self.node.set_console_mode(self.session, True) + self.session.put.assert_called_once_with( + 'nodes/%s/states/console' % self.node.id, + json={'enabled': True}, + headers=mock.ANY, + microversion=mock.ANY, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) + + def test_set_console_mode_invalid_enabled(self): + self.assertRaises( + ValueError, + self.node.set_console_mode, + self.session, + 'true', # not a bool + ) diff --git a/releasenotes/notes/node-consoles-63589f22da98a689.yaml b/releasenotes/notes/node-consoles-63589f22da98a689.yaml new file mode 100644 index 000000000..95a2451d8 --- /dev/null +++ b/releasenotes/notes/node-consoles-63589f22da98a689.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Adds ``get_console`` and ``set_console_state`` to + ``openstack.baremetal.v1.Node``. + - | + Adds ``get_node_console``, ``enable_node_console`` and + ``disable_node_console`` to the baremetal Proxy. From d1384a98f7590b857e16dfd13d6c742b2e136517 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 5 Dec 2022 11:30:10 +0000 Subject: [PATCH 3161/3836] baremetal: Add Node boot_device methods Add Node.get_node_boot_device and Node. get_node_supported_boot_devices. These were both missing previously. Change-Id: Ie13ea2e50691298888fac37b6261d0a1d69fb8f1 Signed-off-by: Stephen Finucane --- doc/source/user/proxies/baremetal.rst | 4 +- openstack/baremetal/v1/_proxy.py | 20 +++++++++ openstack/baremetal/v1/node.py | 45 +++++++++++++++++++ .../tests/unit/baremetal/v1/test_node.py | 24 ++++++++-- .../node-boot-devices-2ab4991d75a2ab52.yaml | 8 ++++ 5 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/node-boot-devices-2ab4991d75a2ab52.yaml diff --git a/doc/source/user/proxies/baremetal.rst b/doc/source/user/proxies/baremetal.rst index 0b760a18f..557c8d4eb 100644 --- a/doc/source/user/proxies/baremetal.rst +++ b/doc/source/user/proxies/baremetal.rst @@ -17,7 +17,9 @@ Node Operations .. autoclass:: openstack.baremetal.v1._proxy.Proxy :noindex: :members: nodes, create_node, find_node, get_node, update_node, patch_node, delete_node, - set_node_provision_state, set_node_boot_device, set_node_boot_mode, + set_node_provision_state, get_node_boot_device, + set_node_boot_device, get_node_supported_boot_devices, + set_node_boot_mode, set_node_secure_boot, inject_nmi_to_node, wait_for_nodes_provision_state, set_node_power_state, wait_for_node_power_state, wait_for_node_reservation, validate_node, set_node_maintenance, diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 8108939f0..cdc62ce32 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -411,6 +411,16 @@ def set_node_provision_state(self, node, target, config_drive=None, wait=wait, timeout=timeout, deploy_steps=deploy_steps) + def get_node_boot_device(self, node): + """Get node boot device + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :return: The node boot device + """ + res = self._get_resource(_node.Node, node) + return res.get_boot_device() + def set_node_boot_device(self, node, boot_device, persistent=False): """Set node boot device @@ -424,6 +434,16 @@ def set_node_boot_device(self, node, boot_device, persistent=False): res = self._get_resource(_node.Node, node) return res.set_boot_device(self, boot_device, persistent=persistent) + def get_node_supported_boot_devices(self, node): + """Get supported boot devices for node + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :return: The node boot device + """ + res = self._get_resource(_node.Node, node) + return res.get_supported_boot_devices() + def set_node_boot_mode(self, node, target): """Make a request to change node's boot mode diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 8d117b069..df58c0326 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -862,6 +862,26 @@ def _do_maintenance_action(self, session, verb, body=None): .format(node=self.id)) exceptions.raise_from_response(response, error_message=msg) + def get_boot_device(self, session): + session = self._get_session(session) + version = self._get_microversion(session, action='fetch') + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'management', 'boot_device') + + response = session.get( + request.url, + headers=request.headers, + microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) + + msg = "Failed to get boot device for node {node}".format( + node=self.id, + ) + exceptions.raise_from_response(response, error_message=msg) + + return response.json() + def set_boot_device(self, session, boot_device, persistent=False): """Set node boot device @@ -886,6 +906,31 @@ def set_boot_device(self, session, boot_device, persistent=False): .format(node=self.id)) exceptions.raise_from_response(response, error_message=msg) + def get_supported_boot_devices(self, session): + session = self._get_session(session) + version = self._get_microversion(session, action='fetch') + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin( + request.url, + 'management', + 'boot_device', + 'supported', + ) + + response = session.get( + request.url, + headers=request.headers, + microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) + + msg = "Failed to get supported boot devices for node {node}".format( + node=self.id, + ) + exceptions.raise_from_response(response, error_message=msg) + + return response.json() + def set_boot_mode(self, session, target): """Make a request to change node's boot mode diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index a943153d4..84987cef4 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -769,15 +769,24 @@ def test_set_unset_maintenance(self): @mock.patch.object(node.Node, 'fetch', lambda self, session: self) @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) -class TestNodeSetBootDevice(base.TestCase): +class TestNodeBootDevice(base.TestCase): def setUp(self): - super(TestNodeSetBootDevice, self).setUp() + super().setUp() self.node = node.Node(**FAKE) self.session = mock.Mock(spec=adapter.Adapter, default_microversion='1.1') - def test_node_set_boot_device(self): + def test_get_boot_device(self): + self.node.get_boot_device(self.session) + self.session.get.assert_called_once_with( + 'nodes/%s/management/boot_device' % self.node.id, + headers=mock.ANY, + microversion=mock.ANY, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) + + def test_set_boot_device(self): self.node.set_boot_device(self.session, 'pxe', persistent=False) self.session.put.assert_called_once_with( 'nodes/%s/management/boot_device' % self.node.id, @@ -785,6 +794,15 @@ def test_node_set_boot_device(self): headers=mock.ANY, microversion=mock.ANY, retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + def test_get_supported_boot_devices(self): + self.node.get_supported_boot_devices(self.session) + self.session.get.assert_called_once_with( + 'nodes/%s/management/boot_device/supported' % self.node.id, + headers=mock.ANY, + microversion=mock.ANY, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) + @mock.patch.object(utils, 'pick_microversion', lambda session, v: v) @mock.patch.object(node.Node, 'fetch', lambda self, session: self) diff --git a/releasenotes/notes/node-boot-devices-2ab4991d75a2ab52.yaml b/releasenotes/notes/node-boot-devices-2ab4991d75a2ab52.yaml new file mode 100644 index 000000000..c568d9e99 --- /dev/null +++ b/releasenotes/notes/node-boot-devices-2ab4991d75a2ab52.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Adds ``get_boot_device`` and ``get_supported_boot_devices`` to + ``openstack.baremetal.v1.Node``. + - | + Adds ``get_node_boot_device`` and ``get_node_supported_boot_devices`` + to the baremetal Proxy. From 0018c5df184145f10f25da72e37b9550efcd67b7 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 16 Dec 2022 16:44:48 +0000 Subject: [PATCH 3162/3836] Normalise query strings passed to 'find_*' methods There were a number of methods that weren't using these, so stop gathering and silently dropping them. We also take the opportunity to standardise on our naming, opting for 'query'. Change-Id: Ic7bddd44f6c3363c75cad21a724a4ffff04d8557 Signed-off-by: Stephen Finucane --- openstack/block_storage/v2/_proxy.py | 2 +- openstack/block_storage/v3/_proxy.py | 10 +- openstack/compute/v2/_proxy.py | 22 ++- openstack/dns/v2/_proxy.py | 14 +- openstack/identity/v3/_proxy.py | 64 ++++--- openstack/network/v2/_proxy.py | 258 +++++++++++++-------------- 6 files changed, 202 insertions(+), 168 deletions(-) diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index ceb7777c6..80bfea696 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -199,7 +199,7 @@ def get_volume(self, volume): """ return self._get(_volume.Volume, volume) - def find_volume(self, name_or_id, ignore_missing=True, **attrs): + def find_volume(self, name_or_id, ignore_missing=True): """Find a single volume :param snapshot: The name or ID a volume diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index e5fad7b07..d3bb65623 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -62,7 +62,7 @@ def get_snapshot(self, snapshot): """ return self._get(_snapshot.Snapshot, snapshot) - def find_snapshot(self, name_or_id, ignore_missing=True, **attrs): + def find_snapshot(self, name_or_id, ignore_missing=True): """Find a single snapshot :param snapshot: The name or ID a snapshot @@ -231,7 +231,7 @@ def get_type(self, type): """ return self._get(_type.Type, type) - def find_type(self, name_or_id, ignore_missing=True, **attrs): + def find_type(self, name_or_id, ignore_missing=True): """Find a single volume type :param snapshot: The name or ID a volume type @@ -469,7 +469,7 @@ def get_volume(self, volume): """ return self._get(_volume.Volume, volume) - def find_volume(self, name_or_id, ignore_missing=True, **attrs): + def find_volume(self, name_or_id, ignore_missing=True): """Find a single volume :param snapshot: The name or ID a volume @@ -910,7 +910,7 @@ def get_backup(self, backup): """ return self._get(_backup.Backup, backup) - def find_backup(self, name_or_id, ignore_missing=True, **attrs): + def find_backup(self, name_or_id, ignore_missing=True): """Find a single backup :param snapshot: The name or ID a backup @@ -1021,7 +1021,7 @@ def get_group(self, group_id, **attrs): """ return self._get(_group.Group, group_id, **attrs) - def find_group(self, name_or_id, ignore_missing=True, **attrs): + def find_group(self, name_or_id, ignore_missing=True): """Find a single group :param name_or_id: The name or ID of a group. diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index f23719f9a..7aa7af93b 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -1551,18 +1551,27 @@ def services(self, **query): """ return self._list(_service.Service, **query) - def find_service(self, name_or_id, ignore_missing=True, **attrs): + def find_service(self, name_or_id, ignore_missing=True, **query): """Find a service from name or id to get the corresponding info :param name_or_id: The name or id of a service - :param dict attrs: Additional attributes like 'host' + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict query: Additional attributes like 'host' :returns: One: class:`~openstack.compute.v2.hypervisor.Hypervisor` object or None """ - return self._find(_service.Service, name_or_id, - ignore_missing=ignore_missing, **attrs) + return self._find( + _service.Service, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def delete_service(self, service, ignore_missing=True): """Delete a service @@ -1572,9 +1581,8 @@ def delete_service(self, service, ignore_missing=True): :class:`~openstack.compute.v2.service.Service` instance. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when - the volume attachment does not exist. When set to ``True``, no - exception will be set when attempting to delete a nonexistent - volume attachment. + the service does not exist. When set to ``True``, no exception + will be set when attempting to delete a nonexistent service. :returns: ``None`` """ diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py index 1b097e1a7..1f147b7e3 100644 --- a/openstack/dns/v2/_proxy.py +++ b/openstack/dns/v2/_proxy.py @@ -97,7 +97,7 @@ def update_zone(self, zone, **attrs): """ return self._update(_zone.Zone, zone, **attrs) - def find_zone(self, name_or_id, ignore_missing=True, **attrs): + def find_zone(self, name_or_id, ignore_missing=True): """Find a single zone :param name_or_id: The name or ID of a zone @@ -225,7 +225,7 @@ def delete_recordset(self, recordset, zone=None, ignore_missing=True): return self._delete(_rs.Recordset, recordset, ignore_missing=ignore_missing) - def find_recordset(self, zone, name_or_id, ignore_missing=True, **attrs): + def find_recordset(self, zone, name_or_id, ignore_missing=True, **query): """Find a single recordset :param zone: The value can be the ID of a zone @@ -240,9 +240,13 @@ def find_recordset(self, zone, name_or_id, ignore_missing=True, **attrs): :returns: :class:`~openstack.dns.v2.recordset.Recordset` """ zone = self._get_resource(_zone.Zone, zone) - return self._find(_rs.Recordset, name_or_id, - ignore_missing=ignore_missing, zone_id=zone.id, - **attrs) + return self._find( + _rs.Recordset, + name_or_id, + ignore_missing=ignore_missing, + zone_id=zone.id, + **query, + ) # ======== Zone Imports ======== def zone_imports(self, **query): diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 6f40b8b2f..7228bf209 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -348,7 +348,7 @@ def delete_group(self, group, ignore_missing=True): """ self._delete(_group.Group, group, ignore_missing=ignore_missing) - def find_group(self, name_or_id, ignore_missing=True, **kwargs): + def find_group(self, name_or_id, ignore_missing=True, **query): """Find a single group :param name_or_id: The name or ID of a group. @@ -359,9 +359,12 @@ def find_group(self, name_or_id, ignore_missing=True, **kwargs): attempting to find a nonexistent resource. :returns: One :class:`~openstack.identity.v3.group.Group` or None """ - return self._find(_group.Group, name_or_id, - ignore_missing=ignore_missing, - **kwargs) + return self._find( + _group.Group, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_group(self, group): """Get a single group @@ -560,7 +563,7 @@ def delete_project(self, project, ignore_missing=True): """ self._delete(_project.Project, project, ignore_missing=ignore_missing) - def find_project(self, name_or_id, ignore_missing=True, **attrs): + def find_project(self, name_or_id, ignore_missing=True, **query): """Find a single project :param name_or_id: The name or ID of a project. @@ -571,8 +574,12 @@ def find_project(self, name_or_id, ignore_missing=True, **attrs): attempting to find a nonexistent resource. :returns: One :class:`~openstack.identity.v3.project.Project` or None """ - return self._find(_project.Project, name_or_id, - ignore_missing=ignore_missing, **attrs) + return self._find( + _project.Project, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_project(self, project): """Get a single project @@ -731,7 +738,7 @@ def delete_user(self, user, ignore_missing=True): """ self._delete(_user.User, user, ignore_missing=ignore_missing) - def find_user(self, name_or_id, ignore_missing=True, **attrs): + def find_user(self, name_or_id, ignore_missing=True, **query): """Find a single user :param name_or_id: The name or ID of a user. @@ -742,8 +749,12 @@ def find_user(self, name_or_id, ignore_missing=True, **attrs): attempting to find a nonexistent resource. :returns: One :class:`~openstack.identity.v3.user.User` or None """ - return self._find(_user.User, name_or_id, - ignore_missing=ignore_missing, **attrs) + return self._find( + _user.User, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_user(self, user): """Get a single user @@ -952,7 +963,7 @@ def delete_role(self, role, ignore_missing=True): """ self._delete(_role.Role, role, ignore_missing=ignore_missing) - def find_role(self, name_or_id, ignore_missing=True, **kwargs): + def find_role(self, name_or_id, ignore_missing=True, **query): """Find a single role :param name_or_id: The name or ID of a role. @@ -963,9 +974,12 @@ def find_role(self, name_or_id, ignore_missing=True, **kwargs): attempting to find a nonexistent role. :returns: One :class:`~openstack.identity.v3.role.Role` or None """ - return self._find(_role.Role, name_or_id, - ignore_missing=ignore_missing, - **kwargs) + return self._find( + _role.Role, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_role(self, role): """Get a single role @@ -1574,8 +1588,13 @@ def create_application_credential(self, user, name, **attrs): name=name, user_id=user.id, **attrs) - def find_application_credential(self, user, name_or_id, - ignore_missing=True, **args): + def find_application_credential( + self, + user, + name_or_id, + ignore_missing=True, + **query, + ): """Find a single application credential :param user: Either the ID of a user or a @@ -1592,9 +1611,13 @@ def find_application_credential(self, user, name_or_id, or None """ user = self._get_resource(_user.User, user) - return self._find(_application_credential.ApplicationCredential, - user_id=user.id, name_or_id=name_or_id, - ignore_missing=ignore_missing, **args) + return self._find( + _application_credential.ApplicationCredential, + user_id=user.id, + name_or_id=name_or_id, + ignore_missing=ignore_missing, + **query, + ) def delete_application_credential(self, user, application_credential, ignore_missing=True): @@ -1673,8 +1696,7 @@ def delete_federation_protocol(self, idp_id, protocol, self._delete(cls, protocol, ignore_missing=ignore_missing, idp_id=idp_id) - def find_federation_protocol(self, idp_id, protocol, - ignore_missing=True): + def find_federation_protocol(self, idp_id, protocol, ignore_missing=True): """Find a single federation protocol :param idp_id: The ID of the identity provider or a diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index fffe5073b..3e4f328ee 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -179,7 +179,7 @@ def delete_address_group(self, address_group, ignore_missing=True): self._delete(_address_group.AddressGroup, address_group, ignore_missing=ignore_missing) - def find_address_group(self, name_or_id, ignore_missing=True, **args): + def find_address_group(self, name_or_id, ignore_missing=True, **query): """Find a single address group :param name_or_id: The name or ID of an address group. @@ -188,13 +188,13 @@ def find_address_group(self, name_or_id, ignore_missing=True, **args): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.address_group.AddressGroup` or None """ return self._find(_address_group.AddressGroup, name_or_id, - ignore_missing=ignore_missing, **args) + ignore_missing=ignore_missing, **query) def get_address_group(self, address_group): """Get a single address group @@ -291,7 +291,7 @@ def delete_address_scope(self, address_scope, ignore_missing=True): self._delete(_address_scope.AddressScope, address_scope, ignore_missing=ignore_missing) - def find_address_scope(self, name_or_id, ignore_missing=True, **args): + def find_address_scope(self, name_or_id, ignore_missing=True, **query): """Find a single address scope :param name_or_id: The name or ID of an address scope. @@ -300,13 +300,13 @@ def find_address_scope(self, name_or_id, ignore_missing=True, **args): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.address_scope.AddressScope` or None """ return self._find(_address_scope.AddressScope, name_or_id, - ignore_missing=ignore_missing, **args) + ignore_missing=ignore_missing, **query) def get_address_scope(self, address_scope): """Get a single address scope @@ -532,7 +532,7 @@ def availability_zones(self, **query): """ return self._list(availability_zone.AvailabilityZone) - def find_extension(self, name_or_id, ignore_missing=True, **args): + def find_extension(self, name_or_id, ignore_missing=True, **query): """Find a single extension :param name_or_id: The name or ID of a extension. @@ -541,13 +541,13 @@ def find_extension(self, name_or_id, ignore_missing=True, **args): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.extension.Extension` or None """ return self._find(extension.Extension, name_or_id, - ignore_missing=ignore_missing, **args) + ignore_missing=ignore_missing, **query) def extensions(self, **query): """Return a generator of extensions @@ -589,7 +589,7 @@ def delete_flavor(self, flavor, ignore_missing=True): """ self._delete(_flavor.Flavor, flavor, ignore_missing=ignore_missing) - def find_flavor(self, name_or_id, ignore_missing=True, **args): + def find_flavor(self, name_or_id, ignore_missing=True, **query): """Find a single network service flavor :param name_or_id: The name or ID of a flavor. @@ -598,12 +598,12 @@ def find_flavor(self, name_or_id, ignore_missing=True, **args): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.flavor.Flavor` or None """ return self._find(_flavor.Flavor, name_or_id, - ignore_missing=ignore_missing, **args) + ignore_missing=ignore_missing, **query) def get_flavor(self, flavor): """Get a single network service flavor @@ -716,7 +716,7 @@ def delete_local_ip(self, local_ip, ignore_missing=True, if_revision=None): self._delete(_local_ip.LocalIP, local_ip, ignore_missing=ignore_missing, if_revision=if_revision) - def find_local_ip(self, name_or_id, ignore_missing=True, **args): + def find_local_ip(self, name_or_id, ignore_missing=True, **query): """Find a local IP :param name_or_id: The name or ID of an local IP. @@ -725,13 +725,13 @@ def find_local_ip(self, name_or_id, ignore_missing=True, **args): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.local_ip.LocalIP` or None """ return self._find(_local_ip.LocalIP, name_or_id, - ignore_missing=ignore_missing, **args) + ignore_missing=ignore_missing, **query) def get_local_ip(self, local_ip): """Get a single local ip @@ -828,7 +828,7 @@ def delete_local_ip_association(self, local_ip, fixed_port_id, ignore_missing=ignore_missing, if_revision=if_revision) def find_local_ip_association(self, name_or_id, local_ip, - ignore_missing=True, **args): + ignore_missing=True, **query): """Find a local ip association :param name_or_id: The name or ID of local ip association. @@ -840,7 +840,7 @@ def find_local_ip_association(self, name_or_id, local_ip, raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.local_ip_association.LocalIPAssociation` @@ -849,7 +849,7 @@ def find_local_ip_association(self, name_or_id, local_ip, local_ip = self._get_resource(_local_ip.LocalIP, local_ip) return self._find(_local_ip_association.LocalIPAssociation, name_or_id, local_ip_id=local_ip.id, - ignore_missing=ignore_missing, **args) + ignore_missing=ignore_missing, **query) def get_local_ip_association(self, local_ip_association, local_ip): """Get a single local ip association @@ -933,7 +933,7 @@ def find_available_ip(self): """ return _floating_ip.FloatingIP.find_available(self) - def find_ip(self, name_or_id, ignore_missing=True, **args): + def find_ip(self, name_or_id, ignore_missing=True, **query): """Find a single IP :param name_or_id: The name or ID of an IP. @@ -942,13 +942,13 @@ def find_ip(self, name_or_id, ignore_missing=True, **args): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.floating_ip.FloatingIP` or None """ return self._find(_floating_ip.FloatingIP, name_or_id, - ignore_missing=ignore_missing, **args) + ignore_missing=ignore_missing, **query) def get_ip(self, floating_ip): """Get a single floating ip @@ -1037,7 +1037,7 @@ def get_port_forwarding(self, port_forwarding, floating_ip): floatingip_id=floating_ip.id) def find_port_forwarding(self, pf_id, floating_ip, ignore_missing=True, - **args): + **query): """Find a single port forwarding :param pf_id: The ID of a port forwarding. @@ -1049,7 +1049,7 @@ def find_port_forwarding(self, pf_id, floating_ip, ignore_missing=True, raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.port_forwarding.PortForwarding` @@ -1058,7 +1058,7 @@ def find_port_forwarding(self, pf_id, floating_ip, ignore_missing=True, floating_ip = self._get_resource(_floating_ip.FloatingIP, floating_ip) return self._find(_port_forwarding.PortForwarding, pf_id, floatingip_id=floating_ip.id, - ignore_missing=ignore_missing, **args) + ignore_missing=ignore_missing, **query) def delete_port_forwarding(self, port_forwarding, floating_ip, ignore_missing=True): @@ -1152,7 +1152,7 @@ def delete_health_monitor(self, health_monitor, ignore_missing=True): self._delete(_health_monitor.HealthMonitor, health_monitor, ignore_missing=ignore_missing) - def find_health_monitor(self, name_or_id, ignore_missing=True, **args): + def find_health_monitor(self, name_or_id, ignore_missing=True, **query): """Find a single health monitor :param name_or_id: The name or ID of a health monitor. @@ -1161,14 +1161,14 @@ def find_health_monitor(self, name_or_id, ignore_missing=True, **args): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.health_monitor.HealthMonitor` or None """ return self._find(_health_monitor.HealthMonitor, - name_or_id, ignore_missing=ignore_missing, **args) + name_or_id, ignore_missing=ignore_missing, **query) def get_health_monitor(self, health_monitor): """Get a single health monitor @@ -1255,7 +1255,7 @@ def delete_listener(self, listener, ignore_missing=True): self._delete(_listener.Listener, listener, ignore_missing=ignore_missing) - def find_listener(self, name_or_id, ignore_missing=True, **args): + def find_listener(self, name_or_id, ignore_missing=True, **query): """Find a single listener :param name_or_id: The name or ID of a listener. @@ -1264,12 +1264,12 @@ def find_listener(self, name_or_id, ignore_missing=True, **args): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.listener.Listener` or None """ return self._find(_listener.Listener, name_or_id, - ignore_missing=ignore_missing, **args) + ignore_missing=ignore_missing, **query) def get_listener(self, listener): """Get a single listener @@ -1350,7 +1350,7 @@ def delete_load_balancer(self, load_balancer, ignore_missing=True): self._delete(_load_balancer.LoadBalancer, load_balancer, ignore_missing=ignore_missing) - def find_load_balancer(self, name_or_id, ignore_missing=True, **args): + def find_load_balancer(self, name_or_id, ignore_missing=True, **query): """Find a single load balancer :param name_or_id: The name or ID of a load balancer. @@ -1359,13 +1359,13 @@ def find_load_balancer(self, name_or_id, ignore_missing=True, **args): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.load_balancer.LoadBalancer` or None """ return self._find(_load_balancer.LoadBalancer, name_or_id, - ignore_missing=ignore_missing, **args) + ignore_missing=ignore_missing, **query) def get_load_balancer(self, load_balancer): """Get a single load balancer @@ -1436,7 +1436,7 @@ def delete_metering_label(self, metering_label, ignore_missing=True): self._delete(_metering_label.MeteringLabel, metering_label, ignore_missing=ignore_missing) - def find_metering_label(self, name_or_id, ignore_missing=True, **args): + def find_metering_label(self, name_or_id, ignore_missing=True, **query): """Find a single metering label :param name_or_id: The name or ID of a metering label. @@ -1445,14 +1445,14 @@ def find_metering_label(self, name_or_id, ignore_missing=True, **args): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.metering_label.MeteringLabel` or None """ return self._find(_metering_label.MeteringLabel, name_or_id, - ignore_missing=ignore_missing, **args) + ignore_missing=ignore_missing, **query) def get_metering_label(self, metering_label): """Get a single metering label @@ -1535,7 +1535,7 @@ def delete_metering_label_rule(self, metering_label_rule, metering_label_rule, ignore_missing=ignore_missing) def find_metering_label_rule(self, name_or_id, ignore_missing=True, - **args): + **query): """Find a single metering label rule :param name_or_id: The name or ID of a metering label rule. @@ -1544,14 +1544,14 @@ def find_metering_label_rule(self, name_or_id, ignore_missing=True, raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.metering_label_rule.MeteringLabelRule` or None """ return self._find(_metering_label_rule.MeteringLabelRule, name_or_id, - ignore_missing=ignore_missing, **args) + ignore_missing=ignore_missing, **query) def get_metering_label_rule(self, metering_label_rule): """Get a single metering label rule @@ -1638,7 +1638,7 @@ def delete_network(self, network, ignore_missing=True, if_revision=None): self._delete(_network.Network, network, ignore_missing=ignore_missing, if_revision=if_revision) - def find_network(self, name_or_id, ignore_missing=True, **args): + def find_network(self, name_or_id, ignore_missing=True, **query): """Find a single network :param name_or_id: The name or ID of a network. @@ -1647,12 +1647,12 @@ def find_network(self, name_or_id, ignore_missing=True, **args): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.network.Network` or None """ return self._find(_network.Network, name_or_id, - ignore_missing=ignore_missing, **args) + ignore_missing=ignore_missing, **query) def get_network(self, network): """Get a single network @@ -1712,7 +1712,7 @@ def update_network(self, network, if_revision=None, **attrs): **attrs) def find_network_ip_availability(self, name_or_id, ignore_missing=True, - **args): + **query): """Find IP availability of a network :param name_or_id: The name or ID of a network. @@ -1721,14 +1721,14 @@ def find_network_ip_availability(self, name_or_id, ignore_missing=True, raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.network_ip_availability.NetworkIPAvailability` or None """ return self._find(network_ip_availability.NetworkIPAvailability, - name_or_id, ignore_missing=ignore_missing, **args) + name_or_id, ignore_missing=ignore_missing, **query) def get_network_ip_availability(self, network): """Get IP availability of a network @@ -1800,7 +1800,7 @@ def delete_network_segment_range(self, network_segment_range, network_segment_range, ignore_missing=ignore_missing) def find_network_segment_range(self, name_or_id, ignore_missing=True, - **args): + **query): """Find a single network segment range :param name_or_id: The name or ID of a network segment range. @@ -1809,14 +1809,14 @@ def find_network_segment_range(self, name_or_id, ignore_missing=True, raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.network_segment_range.NetworkSegmentRange` or None """ return self._find(_network_segment_range.NetworkSegmentRange, - name_or_id, ignore_missing=ignore_missing, **args) + name_or_id, ignore_missing=ignore_missing, **query) def get_network_segment_range(self, network_segment_range): """Get a single network segment range @@ -1909,7 +1909,7 @@ def delete_pool(self, pool, ignore_missing=True): """ self._delete(_pool.Pool, pool, ignore_missing=ignore_missing) - def find_pool(self, name_or_id, ignore_missing=True, **args): + def find_pool(self, name_or_id, ignore_missing=True, **query): """Find a single pool :param name_or_id: The name or ID of a pool. @@ -1918,12 +1918,12 @@ def find_pool(self, name_or_id, ignore_missing=True, **args): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.pool.Pool` or None """ return self._find(_pool.Pool, name_or_id, - ignore_missing=ignore_missing, **args) + ignore_missing=ignore_missing, **query) def get_pool(self, pool): """Get a single pool @@ -2014,7 +2014,7 @@ def delete_pool_member(self, pool_member, pool, ignore_missing=True): self._delete(_pool_member.PoolMember, pool_member, ignore_missing=ignore_missing, pool_id=poolobj.id) - def find_pool_member(self, name_or_id, pool, ignore_missing=True, **args): + def find_pool_member(self, name_or_id, pool, ignore_missing=True, **query): """Find a single pool member :param str name_or_id: The name or ID of a pool member. @@ -2026,7 +2026,7 @@ def find_pool_member(self, name_or_id, pool, ignore_missing=True, **args): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.pool_member.PoolMember` or None @@ -2034,7 +2034,7 @@ def find_pool_member(self, name_or_id, pool, ignore_missing=True, **args): poolobj = self._get_resource(_pool.Pool, pool) return self._find(_pool_member.PoolMember, name_or_id, ignore_missing=ignore_missing, pool_id=poolobj.id, - **args) + **query) def get_pool_member(self, pool_member, pool): """Get a single pool member @@ -2142,7 +2142,7 @@ def delete_port(self, port, ignore_missing=True, if_revision=None): self._delete(_port.Port, port, ignore_missing=ignore_missing, if_revision=if_revision) - def find_port(self, name_or_id, ignore_missing=True, **args): + def find_port(self, name_or_id, ignore_missing=True, **query): """Find a single port :param name_or_id: The name or ID of a port. @@ -2151,12 +2151,12 @@ def find_port(self, name_or_id, ignore_missing=True, **args): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.port.Port` or None """ return self._find(_port.Port, name_or_id, - ignore_missing=ignore_missing, **args) + ignore_missing=ignore_missing, **query) def get_port(self, port): """Get a single port @@ -2272,7 +2272,7 @@ def delete_qos_bandwidth_limit_rule(self, qos_rule, qos_policy, qos_policy_id=policy.id) def find_qos_bandwidth_limit_rule(self, qos_rule_id, qos_policy, - ignore_missing=True, **args): + ignore_missing=True, **query): """Find a bandwidth limit rule :param qos_rule_id: The ID of a bandwidth limit rule. @@ -2284,7 +2284,7 @@ def find_qos_bandwidth_limit_rule(self, qos_rule_id, qos_policy, raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.qos_bandwidth_limit_rule.QoSBandwidthLimitRule` @@ -2293,7 +2293,7 @@ def find_qos_bandwidth_limit_rule(self, qos_rule_id, qos_policy, policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._find(_qos_bandwidth_limit_rule.QoSBandwidthLimitRule, qos_rule_id, ignore_missing=ignore_missing, - qos_policy_id=policy.id, **args) + qos_policy_id=policy.id, **query) def get_qos_bandwidth_limit_rule(self, qos_rule, qos_policy): """Get a single bandwidth limit rule @@ -2397,7 +2397,7 @@ def delete_qos_dscp_marking_rule(self, qos_rule, qos_policy, qos_policy_id=policy.id) def find_qos_dscp_marking_rule(self, qos_rule_id, qos_policy, - ignore_missing=True, **args): + ignore_missing=True, **query): """Find a QoS DSCP marking rule :param qos_rule_id: The ID of a QoS DSCP marking rule. @@ -2409,7 +2409,7 @@ def find_qos_dscp_marking_rule(self, qos_rule_id, qos_policy, raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.qos_dscp_marking_rule.QoSDSCPMarkingRule` @@ -2418,7 +2418,7 @@ def find_qos_dscp_marking_rule(self, qos_rule_id, qos_policy, policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._find(_qos_dscp_marking_rule.QoSDSCPMarkingRule, qos_rule_id, ignore_missing=ignore_missing, - qos_policy_id=policy.id, **args) + qos_policy_id=policy.id, **query) def get_qos_dscp_marking_rule(self, qos_rule, qos_policy): """Get a single QoS DSCP marking rule @@ -2521,7 +2521,7 @@ def delete_qos_minimum_bandwidth_rule(self, qos_rule, qos_policy, qos_policy_id=policy.id) def find_qos_minimum_bandwidth_rule(self, qos_rule_id, qos_policy, - ignore_missing=True, **args): + ignore_missing=True, **query): """Find a minimum bandwidth rule :param qos_rule_id: The ID of a minimum bandwidth rule. @@ -2533,7 +2533,7 @@ def find_qos_minimum_bandwidth_rule(self, qos_rule_id, qos_policy, raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule` @@ -2542,7 +2542,7 @@ def find_qos_minimum_bandwidth_rule(self, qos_rule_id, qos_policy, policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._find(_qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, qos_rule_id, ignore_missing=ignore_missing, - qos_policy_id=policy.id, **args) + qos_policy_id=policy.id, **query) def get_qos_minimum_bandwidth_rule(self, qos_rule, qos_policy): """Get a single minimum bandwidth rule @@ -2648,7 +2648,7 @@ def delete_qos_minimum_packet_rate_rule(self, qos_rule, qos_policy, qos_policy_id=policy.id) def find_qos_minimum_packet_rate_rule(self, qos_rule_id, qos_policy, - ignore_missing=True, **args): + ignore_missing=True, **query): """Find a minimum packet rate rule :param qos_rule_id: The ID of a minimum packet rate rule. @@ -2659,7 +2659,7 @@ def find_qos_minimum_packet_rate_rule(self, qos_rule_id, qos_policy, :class:`~openstack.exceptions.ResourceNotFound` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule` @@ -2669,7 +2669,7 @@ def find_qos_minimum_packet_rate_rule(self, qos_rule_id, qos_policy, return self._find( _qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule, qos_rule_id, ignore_missing=ignore_missing, - qos_policy_id=policy.id, **args) + qos_policy_id=policy.id, **query) def get_qos_minimum_packet_rate_rule(self, qos_rule, qos_policy): """Get a single minimum packet rate rule @@ -2763,7 +2763,7 @@ def delete_qos_policy(self, qos_policy, ignore_missing=True): self._delete(_qos_policy.QoSPolicy, qos_policy, ignore_missing=ignore_missing) - def find_qos_policy(self, name_or_id, ignore_missing=True, **args): + def find_qos_policy(self, name_or_id, ignore_missing=True, **query): """Find a single QoS policy :param name_or_id: The name or ID of a QoS policy. @@ -2772,13 +2772,13 @@ def find_qos_policy(self, name_or_id, ignore_missing=True, **args): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.qos_policy.QoSPolicy` or None """ return self._find(_qos_policy.QoSPolicy, name_or_id, - ignore_missing=ignore_missing, **args) + ignore_missing=ignore_missing, **query) def get_qos_policy(self, qos_policy): """Get a single QoS policy @@ -2974,7 +2974,7 @@ def delete_rbac_policy(self, rbac_policy, ignore_missing=True): self._delete(_rbac_policy.RBACPolicy, rbac_policy, ignore_missing=ignore_missing) - def find_rbac_policy(self, rbac_policy, ignore_missing=True, **args): + def find_rbac_policy(self, rbac_policy, ignore_missing=True, **query): """Find a single RBAC policy :param rbac_policy: The ID of a RBAC policy. @@ -2983,13 +2983,13 @@ def find_rbac_policy(self, rbac_policy, ignore_missing=True, **args): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.rbac_policy.RBACPolicy` or None """ return self._find(_rbac_policy.RBACPolicy, rbac_policy, - ignore_missing=ignore_missing, **args) + ignore_missing=ignore_missing, **query) def get_rbac_policy(self, rbac_policy): """Get a single RBAC policy @@ -3064,7 +3064,7 @@ def delete_router(self, router, ignore_missing=True, if_revision=None): self._delete(_router.Router, router, ignore_missing=ignore_missing, if_revision=if_revision) - def find_router(self, name_or_id, ignore_missing=True, **args): + def find_router(self, name_or_id, ignore_missing=True, **query): """Find a single router :param name_or_id: The name or ID of a router. @@ -3073,12 +3073,12 @@ def find_router(self, name_or_id, ignore_missing=True, **args): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.router.Router` or None """ return self._find(_router.Router, name_or_id, - ignore_missing=ignore_missing, **args) + ignore_missing=ignore_missing, **query) def get_router(self, router): """Get a single router @@ -3296,7 +3296,7 @@ def get_ndp_proxy(self, ndp_proxy): return self._get(_ndp_proxy.NDPProxy, ndp_proxy) def find_ndp_proxy(self, ndp_proxy_id, - ignore_missing=True, **args): + ignore_missing=True, **query): """Find a single ndp proxy :param ndp_proxy_id: The ID of a ndp proxy. @@ -3304,7 +3304,7 @@ def find_ndp_proxy(self, ndp_proxy_id, :class:`~openstack.exceptions.ResourceNotFound` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.ndp_proxy.NDPProxy` or None @@ -3312,7 +3312,7 @@ def find_ndp_proxy(self, ndp_proxy_id, return self._find( _ndp_proxy.NDPProxy, ndp_proxy_id, ignore_missing=ignore_missing, - **args) + **query) def delete_ndp_proxy(self, ndp_proxy, ignore_missing=True): """Delete a ndp proxy @@ -3389,7 +3389,7 @@ def delete_firewall_group(self, firewall_group, ignore_missing=True): self._delete(_firewall_group.FirewallGroup, firewall_group, ignore_missing=ignore_missing) - def find_firewall_group(self, name_or_id, ignore_missing=True, **args): + def find_firewall_group(self, name_or_id, ignore_missing=True, **query): """Find a single firewall group :param name_or_id: The name or ID of a firewall group. @@ -3398,13 +3398,13 @@ def find_firewall_group(self, name_or_id, ignore_missing=True, **args): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.firewall_group.FirewallGroup` or None """ return self._find(_firewall_group.FirewallGroup, - name_or_id, ignore_missing=ignore_missing, **args) + name_or_id, ignore_missing=ignore_missing, **query) def get_firewall_group(self, firewall_group): """Get a single firewall group @@ -3489,7 +3489,7 @@ def delete_firewall_policy(self, firewall_policy, ignore_missing=True): self._delete(_firewall_policy.FirewallPolicy, firewall_policy, ignore_missing=ignore_missing) - def find_firewall_policy(self, name_or_id, ignore_missing=True, **args): + def find_firewall_policy(self, name_or_id, ignore_missing=True, **query): """Find a single firewall policy :param name_or_id: The name or ID of a firewall policy. @@ -3498,14 +3498,14 @@ def find_firewall_policy(self, name_or_id, ignore_missing=True, **args): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.firewall_policy.FirewallPolicy` or None """ return self._find(_firewall_policy.FirewallPolicy, - name_or_id, ignore_missing=ignore_missing, **args) + name_or_id, ignore_missing=ignore_missing, **query) def get_firewall_policy(self, firewall_policy): """Get a single firewall policy @@ -3621,7 +3621,7 @@ def delete_firewall_rule(self, firewall_rule, ignore_missing=True): self._delete(_firewall_rule.FirewallRule, firewall_rule, ignore_missing=ignore_missing) - def find_firewall_rule(self, name_or_id, ignore_missing=True, **args): + def find_firewall_rule(self, name_or_id, ignore_missing=True, **query): """Find a single firewall rule :param name_or_id: The name or ID of a firewall rule. @@ -3630,14 +3630,14 @@ def find_firewall_rule(self, name_or_id, ignore_missing=True, **args): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.firewall_rule.FirewallRule` or None """ return self._find(_firewall_rule.FirewallRule, - name_or_id, ignore_missing=ignore_missing, **args) + name_or_id, ignore_missing=ignore_missing, **query) def get_firewall_rule(self, firewall_rule): """Get a single firewall rule @@ -3731,7 +3731,7 @@ def delete_security_group(self, security_group, ignore_missing=True, self._delete(_security_group.SecurityGroup, security_group, ignore_missing=ignore_missing, if_revision=if_revision) - def find_security_group(self, name_or_id, ignore_missing=True, **args): + def find_security_group(self, name_or_id, ignore_missing=True, **query): """Find a single security group :param name_or_id: The name or ID of a security group. @@ -3740,14 +3740,14 @@ def find_security_group(self, name_or_id, ignore_missing=True, **args): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.security_group.SecurityGroup` or None """ return self._find(_security_group.SecurityGroup, name_or_id, - ignore_missing=ignore_missing, **args) + ignore_missing=ignore_missing, **query) def get_security_group(self, security_group): """Get a single security group @@ -3850,7 +3850,7 @@ def delete_security_group_rule(self, security_group_rule, if_revision=if_revision) def find_security_group_rule(self, name_or_id, ignore_missing=True, - **args): + **query): """Find a single security group rule :param str name_or_id: The ID of a security group rule. @@ -3859,14 +3859,14 @@ def find_security_group_rule(self, name_or_id, ignore_missing=True, raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.security_group_rule.SecurityGroupRule` or None """ return self._find(_security_group_rule.SecurityGroupRule, - name_or_id, ignore_missing=ignore_missing, **args) + name_or_id, ignore_missing=ignore_missing, **query) def get_security_group_rule(self, security_group_rule): """Get a single security group rule @@ -3934,7 +3934,7 @@ def delete_segment(self, segment, ignore_missing=True): """ self._delete(_segment.Segment, segment, ignore_missing=ignore_missing) - def find_segment(self, name_or_id, ignore_missing=True, **args): + def find_segment(self, name_or_id, ignore_missing=True, **query): """Find a single segment :param name_or_id: The name or ID of a segment. @@ -3943,12 +3943,12 @@ def find_segment(self, name_or_id, ignore_missing=True, **args): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.segment.Segment` or None """ return self._find(_segment.Segment, name_or_id, - ignore_missing=ignore_missing, **args) + ignore_missing=ignore_missing, **query) def get_segment(self, segment): """Get a single segment @@ -4037,7 +4037,7 @@ def delete_service_profile(self, service_profile, ignore_missing=True): self._delete(_service_profile.ServiceProfile, service_profile, ignore_missing=ignore_missing) - def find_service_profile(self, name_or_id, ignore_missing=True, **args): + def find_service_profile(self, name_or_id, ignore_missing=True, **query): """Find a single network service flavor profile :param name_or_id: The name or ID of a service profile. @@ -4046,14 +4046,14 @@ def find_service_profile(self, name_or_id, ignore_missing=True, **args): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.service_profile.ServiceProfile` or None """ return self._find(_service_profile.ServiceProfile, name_or_id, - ignore_missing=ignore_missing, **args) + ignore_missing=ignore_missing, **query) def get_service_profile(self, service_profile): """Get a single network service flavor profile @@ -4130,7 +4130,7 @@ def delete_subnet(self, subnet, ignore_missing=True, if_revision=None): self._delete(_subnet.Subnet, subnet, ignore_missing=ignore_missing, if_revision=if_revision) - def find_subnet(self, name_or_id, ignore_missing=True, **args): + def find_subnet(self, name_or_id, ignore_missing=True, **query): """Find a single subnet :param name_or_id: The name or ID of a subnet. @@ -4139,12 +4139,12 @@ def find_subnet(self, name_or_id, ignore_missing=True, **args): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.subnet.Subnet` or None """ return self._find(_subnet.Subnet, name_or_id, - ignore_missing=ignore_missing, **args) + ignore_missing=ignore_missing, **query) def get_subnet(self, subnet): """Get a single subnet @@ -4226,7 +4226,7 @@ def delete_subnet_pool(self, subnet_pool, ignore_missing=True): self._delete(_subnet_pool.SubnetPool, subnet_pool, ignore_missing=ignore_missing) - def find_subnet_pool(self, name_or_id, ignore_missing=True, **args): + def find_subnet_pool(self, name_or_id, ignore_missing=True, **query): """Find a single subnet pool :param name_or_id: The name or ID of a subnet pool. @@ -4235,13 +4235,13 @@ def find_subnet_pool(self, name_or_id, ignore_missing=True, **args): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.subnet_pool.SubnetPool` or None """ return self._find(_subnet_pool.SubnetPool, name_or_id, - ignore_missing=ignore_missing, **args) + ignore_missing=ignore_missing, **query) def get_subnet_pool(self, subnet_pool): """Get a single subnet pool @@ -4333,7 +4333,7 @@ def delete_trunk(self, trunk, ignore_missing=True): """ self._delete(_trunk.Trunk, trunk, ignore_missing=ignore_missing) - def find_trunk(self, name_or_id, ignore_missing=True, **args): + def find_trunk(self, name_or_id, ignore_missing=True, **query): """Find a single trunk :param name_or_id: The name or ID of a trunk. @@ -4342,13 +4342,13 @@ def find_trunk(self, name_or_id, ignore_missing=True, **args): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.trunk.Trunk` or None """ return self._find(_trunk.Trunk, name_or_id, - ignore_missing=ignore_missing, **args) + ignore_missing=ignore_missing, **query) def get_trunk(self, trunk): """Get a single trunk @@ -4466,7 +4466,7 @@ def delete_vpn_endpoint_group( ignore_missing=ignore_missing) def find_vpn_endpoint_group( - self, name_or_id, ignore_missing=True, **args + self, name_or_id, ignore_missing=True, **query ): """Find a single vpn service @@ -4476,7 +4476,7 @@ def find_vpn_endpoint_group( raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.vpn_endpoint_group.VpnEndpointGroup` @@ -4484,7 +4484,7 @@ def find_vpn_endpoint_group( """ return self._find( _vpn_endpoint_group.VpnEndpointGroup, name_or_id, - ignore_missing=ignore_missing, **args) + ignore_missing=ignore_missing, **query) def get_vpn_endpoint_group(self, vpn_endpoint_group): """Get a single vpn service @@ -4547,7 +4547,7 @@ def create_vpn_ipsec_site_connection(self, **attrs): **attrs) def find_vpn_ipsec_site_connection( - self, name_or_id, ignore_missing=True, **args + self, name_or_id, ignore_missing=True, **query ): """Find a single IPsec site connection @@ -4557,7 +4557,7 @@ def find_vpn_ipsec_site_connection( will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods such as query filters. :returns: One :class:`~openstack.network.v2.vpn_ipsec_site_connection.VpnIPSecSiteConnection` @@ -4565,7 +4565,7 @@ def find_vpn_ipsec_site_connection( """ return self._find( _ipsec_site_connection.VpnIPSecSiteConnection, - name_or_id, ignore_missing=ignore_missing, **args) + name_or_id, ignore_missing=ignore_missing, **query) def get_vpn_ipsec_site_connection(self, ipsec_site_connection): """Get a single IPsec site connection @@ -4652,7 +4652,7 @@ def create_vpn_ike_policy(self, **attrs): _ike_policy.VpnIkePolicy, **attrs) def find_vpn_ike_policy( - self, name_or_id, ignore_missing=True, **args + self, name_or_id, ignore_missing=True, **query ): """Find a single ike policy @@ -4662,14 +4662,14 @@ def find_vpn_ike_policy( will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods such as query filters. :returns: One :class:`~openstack.network.v2.vpn_ike_policy.VpnIkePolicy` or None. """ return self._find( _ike_policy.VpnIkePolicy, name_or_id, - ignore_missing=ignore_missing, **args) + ignore_missing=ignore_missing, **query) def get_vpn_ike_policy(self, ike_policy): """Get a single ike policy @@ -4747,7 +4747,7 @@ def create_vpn_ipsec_policy(self, **attrs): _ipsec_policy.VpnIpsecPolicy, **attrs) def find_vpn_ipsec_policy( - self, name_or_id, ignore_missing=True, **args + self, name_or_id, ignore_missing=True, **query ): """Find a single IPsec policy @@ -4757,7 +4757,7 @@ def find_vpn_ipsec_policy( will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods such as query filters. :returns: One :class:`~openstack.network.v2.vpn_ipsec_policy.VpnIpsecPolicy` @@ -4765,7 +4765,7 @@ def find_vpn_ipsec_policy( """ return self._find( _ipsec_policy.VpnIpsecPolicy, name_or_id, - ignore_missing=ignore_missing, **args) + ignore_missing=ignore_missing, **query) def get_vpn_ipsec_policy(self, ipsec_policy): """Get a single IPsec policy @@ -4860,7 +4860,7 @@ def delete_vpn_service(self, vpn_service, ignore_missing=True): self._delete(_vpn_service.VpnService, vpn_service, ignore_missing=ignore_missing) - def find_vpn_service(self, name_or_id, ignore_missing=True, **args): + def find_vpn_service(self, name_or_id, ignore_missing=True, **query): """Find a single vpn service :param name_or_id: The name or ID of a vpn service. @@ -4869,13 +4869,13 @@ def find_vpn_service(self, name_or_id, ignore_missing=True, **args): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.vpn_service.VpnService` or None """ return self._find(_vpn_service.VpnService, name_or_id, - ignore_missing=ignore_missing, **args) + ignore_missing=ignore_missing, **query) def get_vpn_service(self, vpn_service): """Get a single vpn service @@ -4957,7 +4957,7 @@ def delete_floating_ip_port_forwarding(self, floating_ip, port_forwarding, floatingip_id=floatingip.id) def find_floating_ip_port_forwarding(self, floating_ip, port_forwarding_id, - ignore_missing=True, **args): + ignore_missing=True, **query): """Find a floating ip port forwarding :param floating_ip: The value can be the ID of the Floating IP that the @@ -4969,7 +4969,7 @@ def find_floating_ip_port_forwarding(self, floating_ip, port_forwarding_id, raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. - :param dict args: Any additional parameters to be passed into + :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.port_forwarding.PortForwarding` @@ -4978,7 +4978,7 @@ def find_floating_ip_port_forwarding(self, floating_ip, port_forwarding_id, floatingip = self._get_resource(_floating_ip.FloatingIP, floating_ip) return self._find(_port_forwarding.PortForwarding, port_forwarding_id, ignore_missing=ignore_missing, - floatingip_id=floatingip.id, **args) + floatingip_id=floatingip.id, **query) def get_floating_ip_port_forwarding(self, floating_ip, port_forwarding): """Get a floating ip port forwarding From 6b937b2c6c3ce0625f5faa557856914a0aca4ecc Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 16 Dec 2022 16:59:34 +0000 Subject: [PATCH 3163/3836] Deprecate all of the compute image proxy APIs This is the only proxy API we support in SDK. We should stop doing that. For now, just deprecate it. Some examples are updated to use the image API. Change-Id: Id4905782e64998c7293625f22298bbce0baed82a Signed-off-by: Stephen Finucane --- examples/compute/create.py | 2 +- examples/compute/find.py | 2 +- openstack/compute/v2/_proxy.py | 22 ++++++++- openstack/tests/unit/compute/v2/test_proxy.py | 45 ++++++++++++++----- ...ute-image-proxy-apis-986263f6aa1b1b25.yaml | 12 +++++ 5 files changed, 69 insertions(+), 14 deletions(-) create mode 100644 releasenotes/notes/deprecated-compute-image-proxy-apis-986263f6aa1b1b25.yaml diff --git a/examples/compute/create.py b/examples/compute/create.py index 342421f07..7a3aa0850 100644 --- a/examples/compute/create.py +++ b/examples/compute/create.py @@ -56,7 +56,7 @@ def create_keypair(conn): def create_server(conn): print("Create Server:") - image = conn.compute.find_image(IMAGE_NAME) + image = conn.image.find_image(IMAGE_NAME) flavor = conn.compute.find_flavor(FLAVOR_NAME) network = conn.network.find_network(NETWORK_NAME) keypair = create_keypair(conn) diff --git a/examples/compute/find.py b/examples/compute/find.py index 64ca9e355..988b4d970 100644 --- a/examples/compute/find.py +++ b/examples/compute/find.py @@ -23,7 +23,7 @@ def find_image(conn): print("Find Image:") - image = conn.compute.find_image(examples.connect.IMAGE_NAME) + image = conn.image.find_image(examples.connect.IMAGE_NAME) print(image) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 7aa7af93b..4f701ef3a 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -447,6 +447,11 @@ def delete_image(self, image, ignore_missing=True): :returns: ``None`` """ + warnings.warn( + 'This API is a proxy to the image service and has been ' + 'deprecated; use the image service proxy API instead', + DeprecationWarning, + ) self._delete(_image.Image, image, ignore_missing=ignore_missing) def find_image(self, name_or_id, ignore_missing=True): @@ -460,6 +465,11 @@ def find_image(self, name_or_id, ignore_missing=True): attempting to find a nonexistent resource. :returns: One :class:`~openstack.compute.v2.image.Image` or None """ + warnings.warn( + 'This API is a proxy to the image service and has been ' + 'deprecated; use the image service proxy API instead', + DeprecationWarning, + ) return self._find(_image.Image, name_or_id, ignore_missing=ignore_missing) @@ -473,6 +483,11 @@ def get_image(self, image): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ + warnings.warn( + 'This API is a proxy to the image service and has been ' + 'deprecated; use the image service proxy API instead', + DeprecationWarning, + ) return self._get(_image.Image, image) def images(self, details=True, **query): @@ -487,8 +502,11 @@ def images(self, details=True, **query): :returns: A generator of image objects """ - warnings.warn('This API is deprecated and may disappear shortly', - DeprecationWarning) + warnings.warn( + 'This API is a proxy to the image service and has been ' + 'deprecated; use the image service proxy API instead', + DeprecationWarning, + ) base_path = '/images/detail' if details else None return self._list(_image.Image, base_path=base_path, **query) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index e7f850b92..f9010c10b 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import contextlib import datetime from unittest import mock import uuid @@ -687,27 +688,51 @@ def test_extension_find(self): def test_extensions(self): self.verify_list(self.proxy.extensions, extension.Extension) + @contextlib.contextmanager + def _check_image_proxy_deprecation_warning(self): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + yield + self.assertEqual(1, len(w)) + self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) + self.assertIn( + "This API is a proxy to the image service ", + str(w[-1].message), + ) + def test_image_delete(self): - self.verify_delete(self.proxy.delete_image, image.Image, False) + with self._check_image_proxy_deprecation_warning(): + self.verify_delete(self.proxy.delete_image, image.Image, False) def test_image_delete_ignore(self): - self.verify_delete(self.proxy.delete_image, image.Image, True) + with self._check_image_proxy_deprecation_warning(): + self.verify_delete(self.proxy.delete_image, image.Image, True) def test_image_find(self): - self.verify_find(self.proxy.find_image, image.Image) + with self._check_image_proxy_deprecation_warning(): + self.verify_find(self.proxy.find_image, image.Image) def test_image_get(self): - self.verify_get(self.proxy.get_image, image.Image) + with self._check_image_proxy_deprecation_warning(): + self.verify_get(self.proxy.get_image, image.Image) def test_images_detailed(self): - self.verify_list(self.proxy.images, image.ImageDetail, - method_kwargs={"details": True, "query": 1}, - expected_kwargs={"query": 1}) + with self._check_image_proxy_deprecation_warning(): + self.verify_list( + self.proxy.images, + image.ImageDetail, + method_kwargs={"details": True, "query": 1}, + expected_kwargs={"query": 1}, + ) def test_images_not_detailed(self): - self.verify_list(self.proxy.images, image.Image, - method_kwargs={"details": False, "query": 1}, - expected_kwargs={"query": 1}) + with self._check_image_proxy_deprecation_warning(): + self.verify_list( + self.proxy.images, + image.Image, + method_kwargs={"details": False, "query": 1}, + expected_kwargs={"query": 1}, + ) def test_limits_get(self): self._verify( diff --git a/releasenotes/notes/deprecated-compute-image-proxy-apis-986263f6aa1b1b25.yaml b/releasenotes/notes/deprecated-compute-image-proxy-apis-986263f6aa1b1b25.yaml new file mode 100644 index 000000000..c63ff8205 --- /dev/null +++ b/releasenotes/notes/deprecated-compute-image-proxy-apis-986263f6aa1b1b25.yaml @@ -0,0 +1,12 @@ +--- +deprecations: + - | + The following Compute service proxy methods are now deprecated: + + * ``find_image`` + * ``get_image`` + * ``delete_image`` + * ``images`` + + These are proxy APIs for the Image service. You should use the Image + service instead via the Image service proxy methods. From e350c80acac1bf745f1cec47e6c43d4a2f40e9d9 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 16 Dec 2022 16:25:46 +0000 Subject: [PATCH 3164/3836] Add missing block storage v2 'find_*' methods These were present in v3 but not v2. Increasingly few people should be using v2 but it's nice to be complete. Change-Id: Ic2635b3c3eae5a735d79121fd17cb1fbdd07f9f0 Signed-off-by: Stephen Finucane --- openstack/block_storage/v2/_proxy.py | 42 +++++++++++++++++++ .../tests/unit/block_storage/v2/test_proxy.py | 6 +++ ...kup-find-snapshot-v2-756a05ccd150db82.yaml | 6 +++ 3 files changed, 54 insertions(+) create mode 100644 releasenotes/notes/add-find-backup-find-snapshot-v2-756a05ccd150db82.yaml diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 80bfea696..50f065a79 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -37,6 +37,28 @@ def get_snapshot(self, snapshot): """ return self._get(_snapshot.Snapshot, snapshot) + def find_snapshot(self, name_or_id, ignore_missing=True): + """Find a single snapshot + + :param snapshot: The name or ID a snapshot + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the snapshot does not exist. When set to ``True``, None will + be returned when attempting to find a nonexistent resource. + + :returns: One :class:`~openstack.block_storage.v2.snapshot.Snapshot` or + None. + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple + resources are found. + """ + return self._find( + _snapshot.Snapshot, + name_or_id, + ignore_missing=ignore_missing, + ) + def snapshots(self, details=True, **query): """Retrieve a generator of snapshots @@ -457,6 +479,26 @@ def get_backup(self, backup): """ return self._get(_backup.Backup, backup) + def find_backup(self, name_or_id, ignore_missing=True): + """Find a single backup + + :param snapshot: The name or ID a backup + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the backup does not exist. + + :returns: One :class:`~openstack.block_storage.v2.backup.Backup` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple + resources are found. + """ + return self._find( + _backup.Backup, + name_or_id, + ignore_missing=ignore_missing, + ) + def create_backup(self, **attrs): """Create a new Backup from attributes with native API diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index 897addcf8..4bd5e84c2 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -220,6 +220,9 @@ def test_backup_get(self): self.proxy._connection.has_service = mock.Mock(return_value=True) self.verify_get(self.proxy.get_backup, backup.Backup) + def test_backup_find(self): + self.verify_find(self.proxy.find_backup, backup.Backup) + def test_backup_delete(self): # NOTE: mock has_service self.proxy._connection = mock.Mock() @@ -272,6 +275,9 @@ class TestSnapshot(TestVolumeProxy): def test_snapshot_get(self): self.verify_get(self.proxy.get_snapshot, snapshot.Snapshot) + def test_snapshot_find(self): + self.verify_find(self.proxy.find_snapshot, snapshot.Snapshot) + def test_snapshots_detailed(self): self.verify_list(self.proxy.snapshots, snapshot.SnapshotDetail, method_kwargs={"details": True, "query": 1}, diff --git a/releasenotes/notes/add-find-backup-find-snapshot-v2-756a05ccd150db82.yaml b/releasenotes/notes/add-find-backup-find-snapshot-v2-756a05ccd150db82.yaml new file mode 100644 index 000000000..68789d3b8 --- /dev/null +++ b/releasenotes/notes/add-find-backup-find-snapshot-v2-756a05ccd150db82.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + The ``find_snapshot`` and ``find_backup`` methods have been added to the + v2 block storage proxy API. These were previously only available for the v3 + proxy API. From 0c54b0993ecc4167f09da9c0dc28363301699ed6 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 19 Dec 2022 10:47:47 +0000 Subject: [PATCH 3165/3836] Remove unnecessary mocks We haven't called 'has_service' since we removed some unnecessary checks for Swift in change I7349b0bf37b3d296f59f4ee3547dd98a142e1ec7. Change-Id: I2ce72027ebd96dcb165cd3aa0249ebb87ed28f84 Signed-off-by: Stephen Finucane --- .../tests/unit/block_storage/v2/test_proxy.py | 43 ++++++------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index 4bd5e84c2..87db84477 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -198,41 +198,31 @@ def test_complete_migration_error(self): class TestBackup(TestVolumeProxy): def test_backups_detailed(self): - # NOTE: mock has_service - self.proxy._connection = mock.Mock() - self.proxy._connection.has_service = mock.Mock(return_value=True) - self.verify_list(self.proxy.backups, backup.Backup, - method_kwargs={"details": True, "query": 1}, - expected_kwargs={"query": 1, - "base_path": "/backups/detail"}) + self.verify_list( + self.proxy.backups, + backup.Backup, + method_kwargs={"details": True, "query": 1}, + expected_kwargs={"query": 1, "base_path": "/backups/detail"}, + ) def test_backups_not_detailed(self): - # NOTE: mock has_service - self.proxy._connection = mock.Mock() - self.proxy._connection.has_service = mock.Mock(return_value=True) - self.verify_list(self.proxy.backups, backup.Backup, - method_kwargs={"details": False, "query": 1}, - expected_kwargs={"query": 1}) + self.verify_list( + self.proxy.backups, + backup.Backup, + method_kwargs={"details": False, "query": 1}, + expected_kwargs={"query": 1}, + ) def test_backup_get(self): - # NOTE: mock has_service - self.proxy._connection = mock.Mock() - self.proxy._connection.has_service = mock.Mock(return_value=True) self.verify_get(self.proxy.get_backup, backup.Backup) def test_backup_find(self): self.verify_find(self.proxy.find_backup, backup.Backup) def test_backup_delete(self): - # NOTE: mock has_service - self.proxy._connection = mock.Mock() - self.proxy._connection.has_service = mock.Mock(return_value=True) self.verify_delete(self.proxy.delete_backup, backup.Backup, False) def test_backup_delete_ignore(self): - # NOTE: mock has_service - self.proxy._connection = mock.Mock() - self.proxy._connection.has_service = mock.Mock(return_value=True) self.verify_delete(self.proxy.delete_backup, backup.Backup, True) def test_backup_delete_force(self): @@ -245,15 +235,9 @@ def test_backup_delete_force(self): ) def test_backup_create_attrs(self): - # NOTE: mock has_service - self.proxy._connection = mock.Mock() - self.proxy._connection.has_service = mock.Mock(return_value=True) self.verify_create(self.proxy.create_backup, backup.Backup) def test_backup_restore(self): - # NOTE: mock has_service - self.proxy._connection = mock.Mock() - self.proxy._connection.has_service = mock.Mock(return_value=True) self._verify( 'openstack.block_storage.v2.backup.Backup.restore', self.proxy.restore_backup, @@ -268,7 +252,8 @@ def test_backup_reset(self): "openstack.block_storage.v2.backup.Backup.reset", self.proxy.reset_backup, method_args=["value", "new_status"], - expected_args=[self.proxy, "new_status"]) + expected_args=[self.proxy, "new_status"], + ) class TestSnapshot(TestVolumeProxy): From 6fee18fcd1c1fc4715d0520abd9ab18e259c43e0 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 16 Dec 2022 12:07:25 +0000 Subject: [PATCH 3166/3836] Add 'all_projects' support to proxy layers A number of compute and block storage APIs support this parameter. Plumb them through. This turns out to be a bigger change than expected as it highlights a number of gaps in our documentation. Change-Id: Id4049193ab2e6c173208692ed46fc5f9491da3dc Signed-off-by: Stephen Finucane --- openstack/block_storage/v2/_proxy.py | 58 +++++- openstack/block_storage/v3/_proxy.py | 115 +++++++--- openstack/compute/v2/_proxy.py | 196 ++++++++++++++---- openstack/resource.py | 13 +- .../tests/unit/block_storage/v2/test_proxy.py | 61 ++++-- .../tests/unit/block_storage/v3/test_proxy.py | 19 +- openstack/tests/unit/compute/v2/test_proxy.py | 17 +- .../tests/unit/workflow/v2/test_proxy.py | 7 +- openstack/workflow/v2/_proxy.py | 42 +++- ...-all_projects-filter-27f1d471a7848507.yaml | 33 +++ 10 files changed, 448 insertions(+), 113 deletions(-) create mode 100644 releasenotes/notes/list-all_projects-filter-27f1d471a7848507.yaml diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 50f065a79..c1808e4a7 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -37,7 +37,13 @@ def get_snapshot(self, snapshot): """ return self._get(_snapshot.Snapshot, snapshot) - def find_snapshot(self, name_or_id, ignore_missing=True): + def find_snapshot( + self, + name_or_id, + ignore_missing=True, + *, + all_projects=False, + ): """Find a single snapshot :param snapshot: The name or ID a snapshot @@ -45,6 +51,9 @@ def find_snapshot(self, name_or_id, ignore_missing=True): :class:`~openstack.exceptions.ResourceNotFound` will be raised when the snapshot does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param bool all_projects: When set to ``True``, search for snapshot by + name across all projects. Note that this will likely result in + a higher chance of duplicates. Admin-only by default. :returns: One :class:`~openstack.block_storage.v2.snapshot.Snapshot` or None. @@ -53,13 +62,17 @@ def find_snapshot(self, name_or_id, ignore_missing=True): :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple resources are found. """ + query = {} + if all_projects: + query['all_projects'] = True return self._find( _snapshot.Snapshot, name_or_id, ignore_missing=ignore_missing, + **query, ) - def snapshots(self, details=True, **query): + def snapshots(self, *, details=True, all_projects=False, **query): """Retrieve a generator of snapshots :param bool details: When set to ``False`` @@ -67,17 +80,20 @@ def snapshots(self, details=True, **query): objects will be returned. The default, ``True``, will cause :class:`~openstack.block_storage.v2.snapshot.SnapshotDetail` objects to be returned. + :param bool all_projects: When set to ``True``, list snapshots from all + projects. Admin-only by default. :param kwargs query: Optional query parameters to be sent to limit the snapshots being returned. Available parameters include: * name: Name of the snapshot as a string. - * all_projects: Whether return the snapshots in all projects. * volume_id: volume id of a snapshot. * status: Value of the status of the snapshot so that you can filter on "available" for example. :returns: A generator of snapshot objects. """ + if all_projects: + query['all_projects'] = True base_path = '/snapshots/detail' if details else None return self._list(_snapshot.Snapshot, base_path=base_path, **query) @@ -221,37 +237,59 @@ def get_volume(self, volume): """ return self._get(_volume.Volume, volume) - def find_volume(self, name_or_id, ignore_missing=True): + def find_volume( + self, + name_or_id, + ignore_missing=True, + *, + all_projects=False, + ): """Find a single volume - :param snapshot: The name or ID a volume + :param volume: The name or ID a volume :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the volume does not exist. + :param bool all_projects: When set to ``True``, search for volume by + name across all projects. Note that this will likely result in + a higher chance of duplicates. Admin-only by default. - :returns: One :class:`~openstack.block_storage.v2.volume.Volume` + :returns: One :class:`~openstack.block_storage.v2.volume.Volume` or + None. :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. + :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple + resources are found. """ - return self._find(_volume.Volume, name_or_id, - ignore_missing=ignore_missing) + query = {} + if all_projects: + query['all_projects'] = True + return self._find( + _volume.Volume, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) - def volumes(self, details=True, **query): + def volumes(self, *, details=True, all_projects=False, **query): """Retrieve a generator of volumes :param bool details: When set to ``False`` no extended attributes will be returned. The default, ``True``, will cause objects with additional attributes to be returned. + :param bool all_projects: When set to ``True``, list volumes from all + projects. Admin-only by default. :param kwargs query: Optional query parameters to be sent to limit the volumes being returned. Available parameters include: * name: Name of the volume as a string. - * all_projects: Whether return the volumes in all projects * status: Value of the status of the volume so that you can filter on "available" for example. :returns: A generator of volume objects. """ + if all_projects: + query['all_projects'] = True base_path = '/volumes/detail' if details else None return self._list(_volume.Volume, base_path=base_path, **query) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index d3bb65623..7dc7518ea 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -62,33 +62,53 @@ def get_snapshot(self, snapshot): """ return self._get(_snapshot.Snapshot, snapshot) - def find_snapshot(self, name_or_id, ignore_missing=True): + def find_snapshot( + self, + name_or_id, + ignore_missing=True, + *, + all_projects=False, + ): """Find a single snapshot :param snapshot: The name or ID a snapshot :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised - when the snapshot does not exist. + when the snapshot does not exist. When set to ``True``, None will + be returned when attempting to find a nonexistent resource. + :param bool all_projects: When set to ``True``, search for snapshot by + name across all projects. Note that this will likely result in + a higher chance of duplicates. Admin-only by default. :returns: One :class:`~openstack.block_storage.v3.snapshot.Snapshot` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. + :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple + resources are found. """ - return self._find(_snapshot.Snapshot, name_or_id, - ignore_missing=ignore_missing) + query = {} + if all_projects: + query['all_projects'] = True + return self._find( + _snapshot.Snapshot, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) - def snapshots(self, details=True, **query): + def snapshots(self, *, details=True, all_projects=False, **query): """Retrieve a generator of snapshots :param bool details: When set to ``False`` :class: `~openstack.block_storage.v3.snapshot.Snapshot` objects will be returned. The default, ``True``, will cause more attributes to be returned. + :param bool all_projects: When set to ``True``, list snapshots from all + projects. Admin-only by default. :param kwargs query: Optional query parameters to be sent to limit the snapshots being returned. Available parameters include: * name: Name of the snapshot as a string. - * all_projects: Whether return the snapshots in all projects. * project_id: Filter the snapshots by project. * volume_id: volume id of a snapshot. * status: Value of the status of the snapshot so that you can @@ -96,6 +116,8 @@ def snapshots(self, details=True, **query): :returns: A generator of snapshot objects. """ + if all_projects: + query['all_projects'] = True base_path = '/snapshots/detail' if details else None return self._list(_snapshot.Snapshot, base_path=base_path, **query) @@ -237,14 +259,19 @@ def find_type(self, name_or_id, ignore_missing=True): :param snapshot: The name or ID a volume type :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised - when the type does not exist. + when the volume type does not exist. :returns: One :class:`~openstack.block_storage.v3.type.Type` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. + :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple + resources are found. """ - return self._find(_type.Type, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _type.Type, + name_or_id, + ignore_missing=ignore_missing, + ) def types(self, **query): """Retrieve a generator of volume types @@ -469,38 +496,59 @@ def get_volume(self, volume): """ return self._get(_volume.Volume, volume) - def find_volume(self, name_or_id, ignore_missing=True): + def find_volume( + self, + name_or_id, + ignore_missing=True, + *, + all_projects=False, + ): """Find a single volume :param snapshot: The name or ID a volume :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the volume does not exist. + :param bool all_projects: When set to ``True``, search for volume by + name across all projects. Note that this will likely result in + a higher chance of duplicates. Admin-only by default. :returns: One :class:`~openstack.block_storage.v3.volume.Volume` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. + :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple + resources are found. """ - return self._find(_volume.Volume, name_or_id, - ignore_missing=ignore_missing, - list_base_path='/volumes/detail') + query = {} + if all_projects: + query['all_projects'] = True + return self._find( + _volume.Volume, + name_or_id, + ignore_missing=ignore_missing, + list_base_path='/volumes/detail', + **query, + ) - def volumes(self, details=True, **query): + def volumes(self, *, details=True, all_projects=False, **query): """Retrieve a generator of volumes :param bool details: When set to ``False`` no extended attributes will be returned. The default, ``True``, will cause objects with additional attributes to be returned. + :param bool all_projects: When set to ``True``, list volumes from all + projects. Admin-only by default. :param kwargs query: Optional query parameters to be sent to limit the volumes being returned. Available parameters include: * name: Name of the volume as a string. - * all_projects: Whether return the volumes in all projects * status: Value of the status of the volume so that you can filter on "available" for example. :returns: A generator of volume objects. """ + if all_projects: + query['all_projects'] = True base_path = '/volumes/detail' if details else None return self._list(_volume.Volume, base_path=base_path, **query) @@ -872,7 +920,7 @@ def backend_pools(self, **query): return self._list(_stats.Pools, **query) # ====== BACKUPS ====== - def backups(self, details=True, **query): + def backups(self, *, details=True, **query): """Retrieve a generator of backups :param bool details: When set to ``False`` @@ -921,9 +969,14 @@ def find_backup(self, name_or_id, ignore_missing=True): :returns: One :class:`~openstack.block_storage.v3.backup.Backup` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. + :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple + resources are found. """ - return self._find(_backup.Backup, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _backup.Backup, + name_or_id, + ignore_missing=ignore_missing, + ) def create_backup(self, **attrs): """Create a new Backup from attributes with native API @@ -1032,11 +1085,16 @@ def find_group(self, name_or_id, ignore_missing=True): :returns: One :class:`~openstack.block_storage.v3.group.Group` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. + :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple + resources are found. """ return self._find( - _group.Group, name_or_id, ignore_missing=ignore_missing) + _group.Group, + name_or_id, + ignore_missing=ignore_missing, + ) - def groups(self, details=True, **query): + def groups(self, *, details=True, **query): """Retrieve a generator of groups :param bool details: When set to ``False``, no additional details will @@ -1155,12 +1213,16 @@ def find_group_snapshot(self, name_or_id, ignore_missing=True): :returns: One :class:`~openstack.block_storage.v3.group_snapshot` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. + :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple + resources are found. """ return self._find( - _group_snapshot.GroupSnapshot, name_or_id, - ignore_missing=ignore_missing) + _group_snapshot.GroupSnapshot, + name_or_id, + ignore_missing=ignore_missing, + ) - def group_snapshots(self, details=True, **query): + def group_snapshots(self, *, details=True, **query): """Retrieve a generator of group snapshots :param bool details: When ``True``, returns @@ -1245,9 +1307,14 @@ def find_group_type(self, name_or_id, ignore_missing=True): :class:`~openstack.block_storage.v3.group_type.GroupType` :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. + :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple + resources are found. """ return self._find( - _group_type.GroupType, name_or_id, ignore_missing=ignore_missing) + _group_type.GroupType, + name_or_id, + ignore_missing=ignore_missing, + ) def group_types(self, **query): """Retrive a generator of group types diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 4f701ef3a..9dc86f333 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -78,11 +78,19 @@ def find_extension(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :returns: One :class:`~openstack.compute.v2.extension.Extension` or None + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple + resources are found. """ - return self._find(extension.Extension, name_or_id, - ignore_missing=ignore_missing) + return self._find( + extension.Extension, + name_or_id, + ignore_missing=ignore_missing, + ) def extensions(self): """Retrieve a generator of extensions @@ -94,8 +102,15 @@ def extensions(self): # ========== Flavors ========== - def find_flavor(self, name_or_id, ignore_missing=True, - get_extra_specs=False, **query): + # TODO(stephenfin): Drop 'query' parameter or apply it consistently + def find_flavor( + self, + name_or_id, + ignore_missing=True, + *, + get_extra_specs=False, + **query, + ): """Find a single flavor :param name_or_id: The name or ID of a flavor. @@ -106,14 +121,21 @@ def find_flavor(self, name_or_id, ignore_missing=True, :param bool get_extra_specs: When set to ``True`` and extra_specs not present in the response will invoke additional API call to fetch extra_specs. - :param kwargs query: Optional query parameters to be sent to limit the flavors being returned. :returns: One :class:`~openstack.compute.v2.flavor.Flavor` or None + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple + resources are found. """ flavor = self._find( - _flavor.Flavor, name_or_id, ignore_missing=ignore_missing, **query) + _flavor.Flavor, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) if flavor and get_extra_specs and not flavor.extra_specs: flavor = flavor.fetch_extra_specs(self) return flavor @@ -325,11 +347,19 @@ def find_aggregate(self, name_or_id, ignore_missing=True): :class:`~openstack.exceptions.ResourceNotFound` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :returns: One :class:`~openstack.compute.v2.aggregate.Aggregate` or None + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple + resources are found. """ - return self._find(_aggregate.Aggregate, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _aggregate.Aggregate, + name_or_id, + ignore_missing=ignore_missing, + ) def create_aggregate(self, **attrs): """Create a new host aggregate from attributes @@ -463,15 +493,23 @@ def find_image(self, name_or_id, ignore_missing=True): raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :returns: One :class:`~openstack.compute.v2.image.Image` or None + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple + resources are found. """ warnings.warn( 'This API is a proxy to the image service and has been ' 'deprecated; use the image service proxy API instead', DeprecationWarning, ) - return self._find(_image.Image, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _image.Image, + name_or_id, + ignore_missing=ignore_missing, + ) def get_image(self, image): """Get a single image @@ -615,7 +653,7 @@ def get_keypair(self, keypair, user_id=None): attrs = {'user_id': user_id} if user_id else {} return self._get(_keypair.Keypair, keypair, **attrs) - def find_keypair(self, name_or_id, ignore_missing=True, user_id=None): + def find_keypair(self, name_or_id, ignore_missing=True, *, user_id=None): """Find a single keypair :param name_or_id: The name or ID of a keypair. @@ -626,11 +664,18 @@ def find_keypair(self, name_or_id, ignore_missing=True, user_id=None): :param str user_id: Optional user_id owning the keypair :returns: One :class:`~openstack.compute.v2.keypair.Keypair` or None + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple + resources are found. """ attrs = {'user_id': user_id} if user_id else {} - return self._find(_keypair.Keypair, name_or_id, - ignore_missing=ignore_missing, - **attrs) + return self._find( + _keypair.Keypair, + name_or_id, + ignore_missing=ignore_missing, + **attrs, + ) def keypairs(self, **query): """Return a generator of keypairs @@ -690,20 +735,40 @@ def delete_server(self, server, ignore_missing=True, force=False): else: self._delete(_server.Server, server, ignore_missing=ignore_missing) - def find_server(self, name_or_id, ignore_missing=True): + def find_server( + self, + name_or_id, + ignore_missing=True, + *, + all_projects=False, + ): """Find a single server :param name_or_id: The name or ID of a server. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the resource does not exist. When set to ``True``, None will be + returned when attempting to find a nonexistent resource. + :param bool all_projects: When set to ``True``, search for server + by name across all projects. Note that this will likely result in a + higher chance of duplicates. Admin-only by default. + :returns: One :class:`~openstack.compute.v2.server.Server` or None + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple + resources are found. """ - return self._find(_server.Server, name_or_id, - ignore_missing=ignore_missing, - list_base_path='/servers/detail') + query = {} + if all_projects: + query['all_projects'] = True + return self._find( + _server.Server, + name_or_id, + ignore_missing=ignore_missing, + list_base_path='/servers/detail', + **query, + ) def get_server(self, server): """Get a single server @@ -723,6 +788,8 @@ def servers(self, details=True, all_projects=False, **query): :param bool details: When set to ``False`` instances with only basic data will be returned. The default, ``True``, will cause instances with full data to be returned. + :param bool all_projects: When set to ``True``, lists servers from all + projects. Admin-only by default. :param kwargs query: Optional query parameters to be sent to limit the servers being returned. Available parameters can be seen under https://docs.openstack.org/api-ref/compute/#list-servers @@ -1374,21 +1441,40 @@ def delete_server_group(self, server_group, ignore_missing=True): self._delete(_server_group.ServerGroup, server_group, ignore_missing=ignore_missing) - def find_server_group(self, name_or_id, ignore_missing=True): + def find_server_group( + self, + name_or_id, + ignore_missing=True, + *, + all_projects=False, + ): """Find a single server group :param name_or_id: The name or ID of a server group. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. - :returns: - One :class:`~openstack.compute.v2.server_group.ServerGroup` object + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the resource does not exist. When set to ``True``, None will be + returned when attempting to find a nonexistent resource. + :param bool all_projects: When set to ``True``, search for server + groups by name across all projects. Note that this will likely + result in a higher chance of duplicates. Admin-only by default. + + :returns: One :class:`~openstack.compute.v2.server_group.ServerGroup` or None + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple + resources are found. """ - return self._find(_server_group.ServerGroup, name_or_id, - ignore_missing=ignore_missing) + query = {} + if all_projects: + query['all_projects'] = True + return self._find( + _server_group.ServerGroup, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_server_group(self, server_group): """Get a single server group @@ -1404,21 +1490,25 @@ def get_server_group(self, server_group): """ return self._get(_server_group.ServerGroup, server_group) - def server_groups(self, **query): + def server_groups(self, *, all_projects=False, **query): """Return a generator of server groups + :param bool all_projects: When set to ``True``, lists servers groups + from all projects. Admin-only by default. :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of ServerGroup objects :rtype: :class:`~openstack.compute.v2.server_group.ServerGroup` """ + if all_projects: + query['all_projects'] = True return self._list(_server_group.ServerGroup, **query) # ========== Hypervisors ========== def hypervisors(self, details=False, **query): - """Return a generator of hypervisor + """Return a generator of hypervisors :param bool details: When set to the default, ``False``, :class:`~openstack.compute.v2.hypervisor.Hypervisor` @@ -1438,20 +1528,36 @@ def hypervisors(self, details=False, **query): pattern=query.pop('hypervisor_hostname_pattern')) return self._list(_hypervisor.Hypervisor, base_path=base_path, **query) - def find_hypervisor(self, name_or_id, ignore_missing=True, details=True): - """Find a hypervisor from name or id to get the corresponding info + def find_hypervisor( + self, + name_or_id, + ignore_missing=True, + *, + details=True, + ): + """Find a single hypervisor - :param name_or_id: The name or id of a hypervisor + :param name_or_id: The name or ID of a hypervisor + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the resource does not exist. When set to ``True``, None will be + returned when attempting to find a nonexistent resource. - :returns: - One: class:`~openstack.compute.v2.hypervisor.Hypervisor` object + :returns: One: class:`~openstack.compute.v2.hypervisor.Hypervisor` or None + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple + resources are found. """ list_base_path = '/os-hypervisors/detail' if details else None - return self._find(_hypervisor.Hypervisor, name_or_id, - list_base_path=list_base_path, - ignore_missing=ignore_missing) + return self._find( + _hypervisor.Hypervisor, + name_or_id, + list_base_path=list_base_path, + ignore_missing=ignore_missing, + ) def get_hypervisor(self, hypervisor): """Get a single hypervisor @@ -1580,9 +1686,11 @@ def find_service(self, name_or_id, ignore_missing=True, **query): attempting to find a nonexistent resource. :param dict query: Additional attributes like 'host' - :returns: - One: class:`~openstack.compute.v2.hypervisor.Hypervisor` object - or None + :returns: One: class:`~openstack.compute.v2.service.Service` or None + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple + resources are found. """ return self._find( _service.Service, diff --git a/openstack/resource.py b/openstack/resource.py index 6044de88e..7cb275820 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -2192,6 +2192,7 @@ def find( list_base_path=None, *, microversion=None, + all_projects=None, **params, ): """Find a resource by its name or id. @@ -2220,10 +2221,13 @@ def find( is found and ignore_missing is ``False``. """ session = cls._get_session(session) + # Try to short-circuit by looking directly for a matching ID. try: match = cls.existing( - id=name_or_id, connection=session._get_connection(), **params + id=name_or_id, + connection=session._get_connection(), + **params, ) return match.fetch(session, microversion=microversion, **params) except (exceptions.NotFoundException, exceptions.BadRequestException): @@ -2234,6 +2238,12 @@ def find( if list_base_path: params['base_path'] = list_base_path + # all_projects is a special case that is used by multiple services. We + # handle it here since it doesn't make sense to pass it to the .fetch + # call above + if all_projects is not None: + params['all_projects'] = all_projects + if ( 'name' in cls._query_mapping._mapping.keys() and 'name' not in params @@ -2248,6 +2258,7 @@ def find( if ignore_missing: return None + raise exceptions.ResourceNotFound( "No %s found for %s" % (cls.__name__, name_or_id) ) diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index 87db84477..cc9194b86 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -23,8 +23,9 @@ class TestVolumeProxy(test_proxy_base.TestProxyBase): + def setUp(self): - super(TestVolumeProxy, self).setUp() + super().setUp() self.proxy = _proxy.Proxy(self.session) @@ -34,18 +35,31 @@ def test_volume_get(self): self.verify_get(self.proxy.get_volume, volume.Volume) def test_volume_find(self): - self.verify_find(self.proxy.find_volume, volume.Volume) + self.verify_find( + self.proxy.find_volume, + volume.Volume, + method_kwargs={'all_projects': True}, + expected_kwargs={'all_projects': True}, + ) def test_volumes_detailed(self): - self.verify_list(self.proxy.volumes, volume.Volume, - method_kwargs={"details": True, "query": 1}, - expected_kwargs={"query": 1, - "base_path": "/volumes/detail"}) + self.verify_list( + self.proxy.volumes, + volume.Volume, + method_kwargs={"details": True, "all_projects": True}, + expected_kwargs={ + "base_path": "/volumes/detail", + "all_projects": True, + } + ) def test_volumes_not_detailed(self): - self.verify_list(self.proxy.volumes, volume.Volume, - method_kwargs={"details": False, "query": 1}, - expected_kwargs={"query": 1}) + self.verify_list( + self.proxy.volumes, + volume.Volume, + method_kwargs={"details": False, "all_projects": True}, + expected_kwargs={"all_projects": True}, + ) def test_volume_create_attrs(self): self.verify_create(self.proxy.create_volume, volume.Volume) @@ -197,6 +211,7 @@ def test_complete_migration_error(self): class TestBackup(TestVolumeProxy): + def test_backups_detailed(self): self.verify_list( self.proxy.backups, @@ -257,21 +272,33 @@ def test_backup_reset(self): class TestSnapshot(TestVolumeProxy): + def test_snapshot_get(self): self.verify_get(self.proxy.get_snapshot, snapshot.Snapshot) def test_snapshot_find(self): - self.verify_find(self.proxy.find_snapshot, snapshot.Snapshot) + self.verify_find( + self.proxy.find_snapshot, + snapshot.Snapshot, + method_kwargs={'all_projects': True}, + expected_kwargs={'all_projects': True}, + ) def test_snapshots_detailed(self): - self.verify_list(self.proxy.snapshots, snapshot.SnapshotDetail, - method_kwargs={"details": True, "query": 1}, - expected_kwargs={"query": 1}) + self.verify_list( + self.proxy.snapshots, + snapshot.SnapshotDetail, + method_kwargs={"details": True, "all_projects": True}, + expected_kwargs={"all_projects": True}, + ) def test_snapshots_not_detailed(self): - self.verify_list(self.proxy.snapshots, snapshot.Snapshot, - method_kwargs={"details": False, "query": 1}, - expected_kwargs={"query": 1}) + self.verify_list( + self.proxy.snapshots, + snapshot.Snapshot, + method_kwargs={"details": False, "all_projects": True}, + expected_kwargs={"all_projects": 1}, + ) def test_snapshot_create_attrs(self): self.verify_create(self.proxy.create_snapshot, snapshot.Snapshot) @@ -325,6 +352,7 @@ def test_delete_snapshot_metadata(self): class TestType(TestVolumeProxy): + def test_type_get(self): self.verify_get(self.proxy.get_type, type.Type) @@ -363,6 +391,7 @@ def test_type_remove_private_access(self): class TestQuota(TestVolumeProxy): + def test_get(self): self._verify( 'openstack.resource.Resource.fetch', diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 539b82b45..39677628f 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -41,9 +41,15 @@ def test_volume_get(self): self.verify_get(self.proxy.get_volume, volume.Volume) def test_volume_find(self): - self.verify_find(self.proxy.find_volume, volume.Volume, - expected_kwargs=dict( - list_base_path='/volumes/detail')) + self.verify_find( + self.proxy.find_volume, + volume.Volume, + method_kwargs={'all_projects': True}, + expected_kwargs={ + "list_base_path": "/volumes/detail", + "all_projects": True, + }, + ) def test_volumes_detailed(self): self.verify_list( @@ -612,7 +618,12 @@ def test_snapshot_get(self): self.verify_get(self.proxy.get_snapshot, snapshot.Snapshot) def test_snapshot_find(self): - self.verify_find(self.proxy.find_snapshot, snapshot.Snapshot) + self.verify_find( + self.proxy.find_snapshot, + snapshot.Snapshot, + method_kwargs={'all_projects': True}, + expected_kwargs={'all_projects': True}, + ) def test_snapshots_detailed(self): self.verify_list( diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index f9010c10b..12a0c41e6 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -80,7 +80,8 @@ def test_flavor_find_fetch_extra(self): self._verify( 'openstack.proxy.Proxy._find', self.proxy.find_flavor, - method_args=['res', True, True], + method_args=['res', True], + method_kwargs={'get_extra_specs': True}, expected_result=res, expected_args=[flavor.Flavor, 'res'], expected_kwargs={'ignore_missing': True} @@ -851,7 +852,11 @@ def test_server_find(self): self.verify_find( self.proxy.find_server, server.Server, - expected_kwargs={'list_base_path': '/servers/detail'}, + method_kwargs={'all_projects': True}, + expected_kwargs={ + 'list_base_path': '/servers/detail', + 'all_projects': True, + }, ) def test_server_get(self): @@ -1209,8 +1214,12 @@ def test_server_group_delete_ignore(self): server_group.ServerGroup, True) def test_server_group_find(self): - self.verify_find(self.proxy.find_server_group, - server_group.ServerGroup) + self.verify_find( + self.proxy.find_server_group, + server_group.ServerGroup, + method_kwargs={'all_projects': True}, + expected_kwargs={'all_projects': True}, + ) def test_server_group_get(self): self.verify_get(self.proxy.get_server_group, diff --git a/openstack/tests/unit/workflow/v2/test_proxy.py b/openstack/tests/unit/workflow/v2/test_proxy.py index 2ba862d09..428b59fd6 100644 --- a/openstack/tests/unit/workflow/v2/test_proxy.py +++ b/openstack/tests/unit/workflow/v2/test_proxy.py @@ -85,5 +85,8 @@ def test_cron_trigger_delete(self): cron_trigger.CronTrigger, True) def test_cron_trigger_find(self): - self.verify_find(self.proxy.find_cron_trigger, - cron_trigger.CronTrigger) + self.verify_find( + self.proxy.find_cron_trigger, + cron_trigger.CronTrigger, + expected_kwargs={'all_projects': False}, + ) diff --git a/openstack/workflow/v2/_proxy.py b/openstack/workflow/v2/_proxy.py index c29c1ae17..915a76aa9 100644 --- a/openstack/workflow/v2/_proxy.py +++ b/openstack/workflow/v2/_proxy.py @@ -196,9 +196,11 @@ def get_cron_trigger(self, cron_trigger): """ return self._get(_cron_trigger.CronTrigger, cron_trigger) - def cron_triggers(self, **query): + def cron_triggers(self, *, all_projects=False, **query): """Retrieve a generator of cron triggers + :param bool all_projects: When set to ``True``, list cron triggers from + all projects. Admin-only by default. :param kwargs query: Optional query parameters to be sent to restrict the cron triggers to be returned. Available parameters include: @@ -212,6 +214,8 @@ def cron_triggers(self, **query): :returns: A generator of CronTrigger instances. """ + if all_projects: + query['all_projects'] = True return self._list(_cron_trigger.CronTrigger, **query) def delete_cron_trigger(self, value, ignore_missing=True): @@ -231,17 +235,39 @@ def delete_cron_trigger(self, value, ignore_missing=True): return self._delete(_cron_trigger.CronTrigger, value, ignore_missing=ignore_missing) - def find_cron_trigger(self, name_or_id, ignore_missing=True): + # TODO(stephenfin): Drop 'query' parameter or apply it consistently + def find_cron_trigger( + self, + name_or_id, + ignore_missing=True, + *, + all_projects=False, + **query, + ): """Find a single cron trigger :param name_or_id: The name or ID of a cron trigger. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be - raised when the resource does not exist. - When set to ``True``, None will be returned when - attempting to find a nonexistent resource. + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the resource does not exist. When set to ``True``, None will be + returned when attempting to find a nonexistent resource. + :param bool all_projects: When set to ``True``, search for cron + triggers by name across all projects. Note that this will likely + result in a higher chance of duplicates. + :param kwargs query: Optional query parameters to be sent to limit + the cron triggers being returned. + :returns: One :class:`~openstack.compute.v2.cron_trigger.CronTrigger` or None + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource can be found. + :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple + resources are found. """ - return self._find(_cron_trigger.CronTrigger, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _cron_trigger.CronTrigger, + name_or_id, + ignore_missing=ignore_missing, + all_projects=all_projects, + **query, + ) diff --git a/releasenotes/notes/list-all_projects-filter-27f1d471a7848507.yaml b/releasenotes/notes/list-all_projects-filter-27f1d471a7848507.yaml new file mode 100644 index 000000000..cb90309e1 --- /dev/null +++ b/releasenotes/notes/list-all_projects-filter-27f1d471a7848507.yaml @@ -0,0 +1,33 @@ +--- +features: + - | + A number of APIs support passing an admin-only ``all_projects`` filter when + listing certain resources, allowing you to retrieve resources from all + projects rather than just the current projects. This filter is now + explicitly supported at the proxy layer for services and resources that + support it. These are: + + * Block storage (v2) + + * ``find_snapshot`` + * ``snapshots`` + * ``find_volume`` + * ``volumes`` + + * Block storage (v3) + + * ``find_snapshot`` + * ``snapshots`` + * ``find_volume`` + * ``volumes`` + + * Compute (v2) + + * ``find_server`` + * ``find_server_group`` + * ``server_groups`` + + * Workflow (v2) + + * ``find_cron_triggers`` + * ``cron_triggers`` From f9a3cc2f135e5c28fa1e046bb1ca034be36be9aa Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 19 Dec 2022 10:49:04 +0000 Subject: [PATCH 3167/3836] Add 'details' parameter to various 'find' proxy methods Allow retrieving "detailed" results where the API supports this. Change-Id: I13f32e6ca9be9ed4eb42398aace47e3c5205a81f Signed-off-by: Stephen Finucane --- openstack/baremetal/v1/_proxy.py | 77 +++++++++++++++---- openstack/block_storage/v2/_proxy.py | 26 ++++++- openstack/block_storage/v3/_proxy.py | 46 +++++++++-- openstack/compute/v2/_proxy.py | 14 +++- .../tests/unit/baremetal/v1/test_proxy.py | 18 ++++- .../tests/unit/block_storage/v2/test_proxy.py | 16 +++- .../tests/unit/block_storage/v3/test_proxy.py | 23 +++++- ...r-find-proxy-methods-947a3280732c448a.yaml | 32 ++++++++ 8 files changed, 213 insertions(+), 39 deletions(-) create mode 100644 releasenotes/notes/retrieve-detailed-view-for-find-proxy-methods-947a3280732c448a.yaml diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 1722f5ac8..f0fcd1530 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -112,7 +112,7 @@ def create_chassis(self, **attrs): """ return self._create(_chassis.Chassis, **attrs) - def find_chassis(self, name_or_id, ignore_missing=True): + def find_chassis(self, name_or_id, ignore_missing=True, *, details=True): """Find a single chassis. :param str name_or_id: The ID of a chassis. @@ -120,11 +120,19 @@ def find_chassis(self, name_or_id, ignore_missing=True): :class:`~openstack.exceptions.ResourceNotFound` will be raised when the chassis does not exist. When set to `True``, None will be returned when attempting to find a nonexistent chassis. + :param details: A boolean indicating whether the detailed information + for the chassis should be returned. + :returns: One :class:`~openstack.baremetal.v1.chassis.Chassis` object or None. """ - return self._find(_chassis.Chassis, name_or_id, - ignore_missing=ignore_missing) + list_base_path = '/chassis/detail' if details else None + return self._find( + _chassis.Chassis, + name_or_id, + ignore_missing=ignore_missing, + list_base_path=list_base_path, + ) def get_chassis(self, chassis, fields=None): """Get a specific chassis. @@ -300,7 +308,7 @@ def create_node(self, **attrs): """ return self._create(_node.Node, **attrs) - def find_node(self, name_or_id, ignore_missing=True): + def find_node(self, name_or_id, ignore_missing=True, *, details=True): """Find a single node. :param str name_or_id: The name or ID of a node. @@ -308,11 +316,18 @@ def find_node(self, name_or_id, ignore_missing=True): :class:`~openstack.exceptions.ResourceNotFound` will be raised when the node does not exist. When set to `True``, None will be returned when attempting to find a nonexistent node. + :param details: A boolean indicating whether the detailed information + for the node should be returned. :returns: One :class:`~openstack.baremetal.v1.node.Node` object or None. """ - return self._find(_node.Node, name_or_id, - ignore_missing=ignore_missing) + list_base_path = '/nodes/detail' if details else None + return self._find( + _node.Node, + name_or_id, + ignore_missing=ignore_missing, + list_base_path=list_base_path, + ) def get_node(self, node, fields=None): """Get a specific node. @@ -685,7 +700,7 @@ def create_port(self, **attrs): """ return self._create(_port.Port, **attrs) - def find_port(self, name_or_id, ignore_missing=True): + def find_port(self, name_or_id, ignore_missing=True, *, details=True): """Find a single port. :param str name_or_id: The ID of a port. @@ -693,11 +708,18 @@ def find_port(self, name_or_id, ignore_missing=True): :class:`~openstack.exceptions.ResourceNotFound` will be raised when the port does not exist. When set to `True``, None will be returned when attempting to find a nonexistent port. + :param details: A boolean indicating whether the detailed information + for every port should be returned. :returns: One :class:`~openstack.baremetal.v1.port.Port` object or None. """ - return self._find(_port.Port, name_or_id, - ignore_missing=ignore_missing) + list_base_path = '/ports/detail' if details else None + return self._find( + _port.Port, + name_or_id, + ignore_missing=ignore_missing, + list_base_path=list_base_path, + ) def get_port(self, port, fields=None): """Get a specific port. @@ -803,7 +825,13 @@ def create_port_group(self, **attrs): """ return self._create(_portgroup.PortGroup, **attrs) - def find_port_group(self, name_or_id, ignore_missing=True): + def find_port_group( + self, + name_or_id, + ignore_missing=True, + *, + details=True, + ): """Find a single port group. :param str name_or_id: The name or ID of a portgroup. @@ -811,11 +839,18 @@ def find_port_group(self, name_or_id, ignore_missing=True): :class:`~openstack.exceptions.ResourceNotFound` will be raised when the port group does not exist. When set to `True``, None will be returned when attempting to find a nonexistent port group. + :param details: A boolean indicating whether the detailed information + for the port group should be returned. :returns: One :class:`~openstack.baremetal.v1.port_group.PortGroup` object or None. """ - return self._find(_portgroup.PortGroup, name_or_id, - ignore_missing=ignore_missing) + list_base_path = '/portgroups/detail' if details else None + return self._find( + _portgroup.PortGroup, + name_or_id, + ignore_missing=ignore_missing, + list_base_path=list_base_path, + ) def get_port_group(self, port_group, fields=None): """Get a specific port group. @@ -1169,6 +1204,8 @@ def create_volume_connector(self, **attrs): """ return self._create(_volumeconnector.VolumeConnector, **attrs) + # TODO(stephenfin): Delete this. You can't lookup a volume connector by + # name so this is identical to get_volume_connector def find_volume_connector(self, vc_id, ignore_missing=True): """Find a single volume connector. @@ -1183,8 +1220,11 @@ def find_volume_connector(self, vc_id, ignore_missing=True): :class:`~openstack.baremetal.v1.volumeconnector.VolumeConnector` object or None. """ - return self._find(_volumeconnector.VolumeConnector, vc_id, - ignore_missing=ignore_missing) + return self._find( + _volumeconnector.VolumeConnector, + vc_id, + ignore_missing=ignore_missing, + ) def get_volume_connector(self, volume_connector, fields=None): """Get a specific volume_connector. @@ -1309,6 +1349,8 @@ def create_volume_target(self, **attrs): """ return self._create(_volumetarget.VolumeTarget, **attrs) + # TODO(stephenfin): Delete this. You can't lookup a volume target by + # name so this is identical to get_volume_connector def find_volume_target(self, vt_id, ignore_missing=True): """Find a single volume target. @@ -1323,8 +1365,11 @@ def find_volume_target(self, vt_id, ignore_missing=True): :class:`~openstack.baremetal.v1.volumetarget.VolumeTarget` object or None. """ - return self._find(_volumetarget.VolumeTarget, vt_id, - ignore_missing=ignore_missing) + return self._find( + _volumetarget.VolumeTarget, + vt_id, + ignore_missing=ignore_missing, + ) def get_volume_target(self, volume_target, fields=None): """Get a specific volume_target. diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index c1808e4a7..f2ceacaa6 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -42,6 +42,7 @@ def find_snapshot( name_or_id, ignore_missing=True, *, + details=True, all_projects=False, ): """Find a single snapshot @@ -51,12 +52,18 @@ def find_snapshot( :class:`~openstack.exceptions.ResourceNotFound` will be raised when the snapshot does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param bool details: When set to ``False``, an + :class:`~openstack.block_storage.v2.snapshot.Snapshot` object will + be returned. The default, ``True``, will cause an + :class:`~openstack.block_storage.v2.snapshot.SnapshotDetail` object + to be returned. :param bool all_projects: When set to ``True``, search for snapshot by name across all projects. Note that this will likely result in a higher chance of duplicates. Admin-only by default. - :returns: One :class:`~openstack.block_storage.v2.snapshot.Snapshot` or - None. + :returns: One :class:`~openstack.block_storage.v2.snapshot.Snapshot`, + one :class:`~openstack.block_storage.v2.snapshot.SnapshotDetail` + object, or None. :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple @@ -65,10 +72,12 @@ def find_snapshot( query = {} if all_projects: query['all_projects'] = True + list_base_path = '/snapshots/detail' if details else None return self._find( _snapshot.Snapshot, name_or_id, ignore_missing=ignore_missing, + list_base_path=list_base_path, **query, ) @@ -242,6 +251,7 @@ def find_volume( name_or_id, ignore_missing=True, *, + details=True, all_projects=False, ): """Find a single volume @@ -250,6 +260,9 @@ def find_volume( :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the volume does not exist. + :param bool details: When set to ``False`` no extended attributes + will be returned. The default, ``True``, will cause an object with + additional attributes to be returned. :param bool all_projects: When set to ``True``, search for volume by name across all projects. Note that this will likely result in a higher chance of duplicates. Admin-only by default. @@ -264,10 +277,12 @@ def find_volume( query = {} if all_projects: query['all_projects'] = True + list_base_path = '/volumes/detail' if details else None return self._find( _volume.Volume, name_or_id, ignore_missing=ignore_missing, + list_base_path=list_base_path, **query, ) @@ -517,13 +532,16 @@ def get_backup(self, backup): """ return self._get(_backup.Backup, backup) - def find_backup(self, name_or_id, ignore_missing=True): + def find_backup(self, name_or_id, ignore_missing=True, *, details=True): """Find a single backup :param snapshot: The name or ID a backup :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the backup does not exist. + :param bool details: When set to ``False`` no additional details will + be returned. The default, ``True``, will cause objects with + additional attributes to be returned. :returns: One :class:`~openstack.block_storage.v2.backup.Backup` :raises: :class:`~openstack.exceptions.ResourceNotFound` @@ -531,10 +549,12 @@ def find_backup(self, name_or_id, ignore_missing=True): :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple resources are found. """ + list_base_path = '/backups/detail' if details else None return self._find( _backup.Backup, name_or_id, ignore_missing=ignore_missing, + list_base_path=list_base_path, ) def create_backup(self, **attrs): diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 7dc7518ea..106b09531 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -67,6 +67,7 @@ def find_snapshot( name_or_id, ignore_missing=True, *, + details=True, all_projects=False, ): """Find a single snapshot @@ -76,6 +77,10 @@ def find_snapshot( :class:`~openstack.exceptions.ResourceNotFound` will be raised when the snapshot does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param bool details: When set to ``False`` :class: + `~openstack.block_storage.v3.snapshot.Snapshot` objects will be + returned. The default, ``True``, will cause more attributes to be + returned. :param bool all_projects: When set to ``True``, search for snapshot by name across all projects. Note that this will likely result in a higher chance of duplicates. Admin-only by default. @@ -89,10 +94,12 @@ def find_snapshot( query = {} if all_projects: query['all_projects'] = True + list_base_path = '/snapshots/detail' if details else None return self._find( _snapshot.Snapshot, name_or_id, ignore_missing=ignore_missing, + list_base_path=list_base_path, **query, ) @@ -501,6 +508,7 @@ def find_volume( name_or_id, ignore_missing=True, *, + details=True, all_projects=False, ): """Find a single volume @@ -509,6 +517,9 @@ def find_volume( :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the volume does not exist. + :param bool details: When set to ``False`` no extended attributes + will be returned. The default, ``True``, will cause objects with + additional attributes to be returned. :param bool all_projects: When set to ``True``, search for volume by name across all projects. Note that this will likely result in a higher chance of duplicates. Admin-only by default. @@ -522,11 +533,12 @@ def find_volume( query = {} if all_projects: query['all_projects'] = True + list_base_path = '/volumes/detail' if details else None return self._find( _volume.Volume, name_or_id, ignore_missing=ignore_missing, - list_base_path='/volumes/detail', + list_base_path=list_base_path, **query, ) @@ -958,13 +970,16 @@ def get_backup(self, backup): """ return self._get(_backup.Backup, backup) - def find_backup(self, name_or_id, ignore_missing=True): + def find_backup(self, name_or_id, ignore_missing=True, *, details=True): """Find a single backup :param snapshot: The name or ID a backup :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the backup does not exist. + :param bool details: When set to ``False`` no additional details will + be returned. The default, ``True``, will cause objects with + additional attributes to be returned. :returns: One :class:`~openstack.block_storage.v3.backup.Backup` :raises: :class:`~openstack.exceptions.ResourceNotFound` @@ -972,10 +987,12 @@ def find_backup(self, name_or_id, ignore_missing=True): :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple resources are found. """ + list_base_path = '/backups/detail' if details else None return self._find( _backup.Backup, name_or_id, ignore_missing=ignore_missing, + list_base_path=list_base_path, ) def create_backup(self, **attrs): @@ -1074,13 +1091,16 @@ def get_group(self, group_id, **attrs): """ return self._get(_group.Group, group_id, **attrs) - def find_group(self, name_or_id, ignore_missing=True): + def find_group(self, name_or_id, ignore_missing=True, *, details=True): """Find a single group :param name_or_id: The name or ID of a group. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the group snapshot does not exist. + :param bool details: When set to ``False``, no additional details will + be returned. The default, ``True``, will cause additional details + to be returned. :returns: One :class:`~openstack.block_storage.v3.group.Group` :raises: :class:`~openstack.exceptions.ResourceNotFound` @@ -1088,10 +1108,12 @@ def find_group(self, name_or_id, ignore_missing=True): :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple resources are found. """ + list_base_path = '/groups/detail' if details else None return self._find( _group.Group, name_or_id, ignore_missing=ignore_missing, + list_base_path=list_base_path, ) def groups(self, *, details=True, **query): @@ -1202,13 +1224,22 @@ def get_group_snapshot(self, group_snapshot_id): """ return self._get(_group_snapshot.GroupSnapshot, group_snapshot_id) - def find_group_snapshot(self, name_or_id, ignore_missing=True): + def find_group_snapshot( + self, + name_or_id, + ignore_missing=True, + *, + details=True, + ): """Find a single group snapshot :param name_or_id: The name or ID of a group snapshot. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the group snapshot does not exist. + :param bool details: When set to ``False``, no additional details will + be returned. The default, ``True``, will cause additional details + to be returned. :returns: One :class:`~openstack.block_storage.v3.group_snapshot` :raises: :class:`~openstack.exceptions.ResourceNotFound` @@ -1216,10 +1247,12 @@ def find_group_snapshot(self, name_or_id, ignore_missing=True): :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple resources are found. """ + list_base_path = '/group_snapshots/detail' if details else None return self._find( _group_snapshot.GroupSnapshot, name_or_id, ignore_missing=ignore_missing, + list_base_path=list_base_path, ) def group_snapshots(self, *, details=True, **query): @@ -1232,10 +1265,7 @@ def group_snapshots(self, *, details=True, **query): the group snapshots being returned. :returns: A generator of group snapshtos. """ - base_path = '/group_snapshots' - if details: - base_path = '/group_snapshots/detail' - + base_path = '/group_snapshots/detail' if details else None return self._list( _group_snapshot.GroupSnapshot, base_path=base_path, diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 9dc86f333..b26a02487 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -484,6 +484,8 @@ def delete_image(self, image, ignore_missing=True): ) self._delete(_image.Image, image, ignore_missing=ignore_missing) + # NOTE(stephenfin): We haven't added 'details' support here since this + # method is deprecated def find_image(self, name_or_id, ignore_missing=True): """Find a single image @@ -740,6 +742,7 @@ def find_server( name_or_id, ignore_missing=True, *, + details=True, all_projects=False, ): """Find a single server @@ -749,6 +752,9 @@ def find_server( :class:`~openstack.exceptions.ResourceNotFound` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param bool details: When set to ``False`` + instances with only basic data will be returned. The default, + ``True``, will cause instances with full data to be returned. :param bool all_projects: When set to ``True``, search for server by name across all projects. Note that this will likely result in a higher chance of duplicates. Admin-only by default. @@ -762,11 +768,12 @@ def find_server( query = {} if all_projects: query['all_projects'] = True + list_base_path = '/servers/detail' if details else None return self._find( _server.Server, name_or_id, ignore_missing=ignore_missing, - list_base_path='/servers/detail', + list_base_path=list_base_path, **query, ) @@ -1515,6 +1522,7 @@ def hypervisors(self, details=False, **query): instances will be returned with only basic information populated. :param kwargs query: Optional query parameters to be sent to limit the resources being returned. + :returns: A generator of hypervisor :rtype: class: `~openstack.compute.v2.hypervisor.Hypervisor` """ @@ -1542,6 +1550,9 @@ def find_hypervisor( :class:`~openstack.exceptions.ResourceNotFound` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. + :param bool details: When set to ``False`` + instances with only basic data will be returned. The default, + ``True``, will cause instances with full data to be returned. :returns: One: class:`~openstack.compute.v2.hypervisor.Hypervisor` or None @@ -1550,7 +1561,6 @@ def find_hypervisor( :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple resources are found. """ - list_base_path = '/os-hypervisors/detail' if details else None return self._find( _hypervisor.Hypervisor, diff --git a/openstack/tests/unit/baremetal/v1/test_proxy.py b/openstack/tests/unit/baremetal/v1/test_proxy.py index 19efccc17..d3bf06a76 100644 --- a/openstack/tests/unit/baremetal/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal/v1/test_proxy.py @@ -60,7 +60,11 @@ def test_create_chassis(self): self.verify_create(self.proxy.create_chassis, chassis.Chassis) def test_find_chassis(self): - self.verify_find(self.proxy.find_chassis, chassis.Chassis) + self.verify_find( + self.proxy.find_chassis, + chassis.Chassis, + expected_kwargs={'list_base_path': '/chassis/detail'}, + ) def test_get_chassis(self): self.verify_get(self.proxy.get_chassis, chassis.Chassis, @@ -94,7 +98,11 @@ def test_create_node(self): self.verify_create(self.proxy.create_node, node.Node) def test_find_node(self): - self.verify_find(self.proxy.find_node, node.Node) + self.verify_find( + self.proxy.find_node, + node.Node, + expected_kwargs={'list_base_path': '/nodes/detail'}, + ) def test_get_node(self): self.verify_get(self.proxy.get_node, node.Node, @@ -140,7 +148,11 @@ def test_create_port(self): self.verify_create(self.proxy.create_port, port.Port) def test_find_port(self): - self.verify_find(self.proxy.find_port, port.Port) + self.verify_find( + self.proxy.find_port, + port.Port, + expected_kwargs={'list_base_path': '/ports/detail'}, + ) def test_get_port(self): self.verify_get(self.proxy.get_port, port.Port, diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index cc9194b86..c65675e9a 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -39,7 +39,10 @@ def test_volume_find(self): self.proxy.find_volume, volume.Volume, method_kwargs={'all_projects': True}, - expected_kwargs={'all_projects': True}, + expected_kwargs={ + 'list_base_path': '/volumes/detail', + 'all_projects': True, + }, ) def test_volumes_detailed(self): @@ -232,7 +235,11 @@ def test_backup_get(self): self.verify_get(self.proxy.get_backup, backup.Backup) def test_backup_find(self): - self.verify_find(self.proxy.find_backup, backup.Backup) + self.verify_find( + self.proxy.find_backup, + backup.Backup, + expected_kwargs={'list_base_path': '/backups/detail'}, + ) def test_backup_delete(self): self.verify_delete(self.proxy.delete_backup, backup.Backup, False) @@ -281,7 +288,10 @@ def test_snapshot_find(self): self.proxy.find_snapshot, snapshot.Snapshot, method_kwargs={'all_projects': True}, - expected_kwargs={'all_projects': True}, + expected_kwargs={ + 'list_base_path': '/snapshots/detail', + 'all_projects': True, + }, ) def test_snapshots_detailed(self): diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 39677628f..38638205b 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -158,7 +158,11 @@ def test_group_get(self): self.verify_get(self.proxy.get_group, group.Group) def test_group_find(self): - self.verify_find(self.proxy.find_group, group.Group) + self.verify_find( + self.proxy.find_group, + group.Group, + expected_kwargs={'list_base_path': '/groups/detail'}, + ) def test_groups(self): self.verify_list(self.proxy.groups, group.Group) @@ -198,7 +202,11 @@ def test_group_snapshot_get(self): def test_group_snapshot_find(self): self.verify_find( - self.proxy.find_group_snapshot, group_snapshot.GroupSnapshot + self.proxy.find_group_snapshot, + group_snapshot.GroupSnapshot, + expected_kwargs={ + 'list_base_path': '/group_snapshots/detail', + }, ) def test_group_snapshots(self): @@ -562,7 +570,11 @@ def test_backup_find(self): # NOTE: mock has_service self.proxy._connection = mock.Mock() self.proxy._connection.has_service = mock.Mock(return_value=True) - self.verify_find(self.proxy.find_backup, backup.Backup) + self.verify_find( + self.proxy.find_backup, + backup.Backup, + expected_kwargs={'list_base_path': '/backups/detail'}, + ) def test_backup_delete(self): # NOTE: mock has_service @@ -622,7 +634,10 @@ def test_snapshot_find(self): self.proxy.find_snapshot, snapshot.Snapshot, method_kwargs={'all_projects': True}, - expected_kwargs={'all_projects': True}, + expected_kwargs={ + 'list_base_path': '/snapshots/detail', + 'all_projects': True, + }, ) def test_snapshots_detailed(self): diff --git a/releasenotes/notes/retrieve-detailed-view-for-find-proxy-methods-947a3280732c448a.yaml b/releasenotes/notes/retrieve-detailed-view-for-find-proxy-methods-947a3280732c448a.yaml new file mode 100644 index 000000000..b15ba5fb2 --- /dev/null +++ b/releasenotes/notes/retrieve-detailed-view-for-find-proxy-methods-947a3280732c448a.yaml @@ -0,0 +1,32 @@ +--- +features: + - | + The following proxy ``find_*`` operations will now retrieve a detailed + resource by default when retrieving by name: + + * Bare metal (v1) + + * ``find_chassis`` + * ``find_node`` + * ``find_port`` + * ``find_port_group`` + + * Block storage (v2) + + * ``find_volume`` + * ``find_snapshot`` + * ``find_backup`` + + * Block storage (v3) + + * ``find_volume`` + * ``find_snapshot`` + * ``find_backup`` + * ``find_group`` + * ``find_group_snapshot`` + + * Compute (v2) + + * ``find_image`` + * ``find_server`` + * ``find_hypervisor`` From f4b9cc850afb7228a2561f116787b6a65eda9b5d Mon Sep 17 00:00:00 2001 From: "Dr. Jens Harbott" Date: Tue, 20 Dec 2022 10:36:14 +0100 Subject: [PATCH 3168/3836] Fix docs for class SecurityGroupRule The rendering was broken from [0], also amend the wording a bit. [0] I50374c339ab7685a6e74f25f9521b8810c532e13 Change-Id: I10a91cb97d680acace4230273e1bceb80769637d --- openstack/network/v2/security_group_rule.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openstack/network/v2/security_group_rule.py b/openstack/network/v2/security_group_rule.py index 9b3069223..dd5b4c8e1 100644 --- a/openstack/network/v2/security_group_rule.py +++ b/openstack/network/v2/security_group_rule.py @@ -68,17 +68,17 @@ class SecurityGroupRule(_base.NetworkResource, tag.TagMixin): protocol = resource.Body('protocol') #: The remote security group ID to be associated with this security #: group rule. You can specify either ``remote_group_id`` or - #: ``remote_address_group_id`` or ``remote_ip_prefix`` in the request body. + #: ``remote_address_group_id`` or ``remote_ip_prefix``. remote_group_id = resource.Body('remote_group_id') #: The remote address group ID to be associated with this security #: group rule. You can specify either ``remote_group_id`` or - #: ``remote_address_group_id`` or ``remote_ip_prefix`` in the request body. + #: ``remote_address_group_id`` or ``remote_ip_prefix``. remote_address_group_id = resource.Body('remote_address_group_id') #: The remote IP prefix to be associated with this security group rule. #: You can specify either ``remote_group_id`` or - # ``remote_address_group_id``or ``remote_ip_prefix`` in the request body. - # This attribute matches the specified IP prefix as the source IP address - # of the IP packet. + #: ``remote_address_group_id`` or ``remote_ip_prefix``. + #: This attribute matches the specified IP prefix as the source or + #: destination IP address of the IP packet depending on direction. remote_ip_prefix = resource.Body('remote_ip_prefix') #: The security group ID to associate with this security group rule. security_group_id = resource.Body('security_group_id') From f075e908345289342aade3a836288edd0d36222f Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 15 Dec 2022 18:54:21 +0000 Subject: [PATCH 3169/3836] compute: Pass microversion for actions This wasn't happening which meant we were using the default 2.1 API version. Oops. Change-Id: I26f29a47608b66fbafada0b99d71ea13adf4b52e Signed-off-by: Stephen Finucane --- openstack/compute/v2/server.py | 12 +- .../tests/unit/cloud/test_rebuild_server.py | 11 +- .../tests/unit/cloud/test_security_groups.py | 2 + .../tests/unit/cloud/test_server_console.py | 2 + .../tests/unit/compute/v2/test_server.py | 182 +++++++++++++----- ...microversion-support-f14b293d9c3d3d5e.yaml | 5 + 6 files changed, 164 insertions(+), 50 deletions(-) create mode 100644 releasenotes/notes/server-actions-microversion-support-f14b293d9c3d3d5e.yaml diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 5f95bccb4..56a7fe6d7 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -266,8 +266,18 @@ def _action(self, session, body, microversion=None): # the URL used is sans any additional /detail/ part. url = utils.urljoin(Server.base_path, self.id, 'action') headers = {'Accept': ''} + + # these aren't all necessary "commit" actions (i.e. updates) but it's + # good enough... + if microversion is None: + microversion = self._get_microversion(session, action='commit') + response = session.post( - url, json=body, headers=headers, microversion=microversion) + url, + json=body, + headers=headers, + microversion=microversion, + ) exceptions.raise_from_response(response) return response diff --git a/openstack/tests/unit/cloud/test_rebuild_server.py b/openstack/tests/unit/cloud/test_rebuild_server.py index 8cef0a533..460d79aa9 100644 --- a/openstack/tests/unit/cloud/test_rebuild_server.py +++ b/openstack/tests/unit/cloud/test_rebuild_server.py @@ -45,6 +45,7 @@ def test_rebuild_server_rebuild_exception(self): rebuild_server. """ self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', @@ -70,6 +71,7 @@ def test_rebuild_server_server_error(self): raises an exception in rebuild_server. """ self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', @@ -79,7 +81,6 @@ def test_rebuild_server_server_error(self): json={ 'rebuild': { 'imageRef': 'a'}})), - self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', self.server_id]), @@ -97,6 +98,7 @@ def test_rebuild_server_timeout(self): exception in rebuild_server. """ self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', @@ -106,7 +108,6 @@ def test_rebuild_server_timeout(self): json={ 'rebuild': { 'imageRef': 'a'}})), - self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', self.server_id]), @@ -125,6 +126,7 @@ def test_rebuild_server_no_wait(self): rebuild call returns the server instance. """ self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', @@ -154,6 +156,7 @@ def test_rebuild_server_with_admin_pass_no_wait(self): rebuild_server['adminPass'] = password self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', @@ -186,6 +189,7 @@ def test_rebuild_server_with_admin_pass_wait(self): rebuild_server['adminPass'] = password self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', @@ -196,7 +200,6 @@ def test_rebuild_server_with_admin_pass_wait(self): 'rebuild': { 'imageRef': 'a', 'adminPass': password}})), - self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', self.server_id]), @@ -225,6 +228,7 @@ def test_rebuild_server_wait(self): its status changes to "ACTIVE". """ self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='POST', uri=self.get_mock_url( 'compute', 'public', @@ -234,7 +238,6 @@ def test_rebuild_server_wait(self): json={ 'rebuild': { 'imageRef': 'a'}})), - self.get_nova_discovery_mock_dict(), dict(method='GET', uri=self.get_mock_url( 'compute', 'public', append=['servers', self.server_id]), diff --git a/openstack/tests/unit/cloud/test_security_groups.py b/openstack/tests/unit/cloud/test_security_groups.py index 5bc817aec..8a72efdbf 100644 --- a/openstack/tests/unit/cloud/test_security_groups.py +++ b/openstack/tests/unit/cloud/test_security_groups.py @@ -648,6 +648,7 @@ def test_add_security_group_to_server_nova(self): endpoint=fakes.COMPUTE_ENDPOINT, ), json={'security_groups': [nova_grp_dict]}), + self.get_nova_discovery_mock_dict(), dict( method='POST', uri='%s/servers/%s/action' % (fakes.COMPUTE_ENDPOINT, '1234'), @@ -705,6 +706,7 @@ def test_remove_security_group_from_server_nova(self): uri='{endpoint}/os-security-groups'.format( endpoint=fakes.COMPUTE_ENDPOINT), json={'security_groups': [nova_grp_dict]}), + self.get_nova_discovery_mock_dict(), dict( method='POST', uri='%s/servers/%s/action' % (fakes.COMPUTE_ENDPOINT, '1234'), diff --git a/openstack/tests/unit/cloud/test_server_console.py b/openstack/tests/unit/cloud/test_server_console.py index 69ddfbeef..9b885a654 100644 --- a/openstack/tests/unit/cloud/test_server_console.py +++ b/openstack/tests/unit/cloud/test_server_console.py @@ -29,6 +29,7 @@ def setUp(self): def test_get_server_console_dict(self): self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='POST', uri='{endpoint}/servers/{id}/action'.format( endpoint=fakes.COMPUTE_ENDPOINT, @@ -67,6 +68,7 @@ def test_get_server_console_name_or_id(self): def test_get_server_console_no_console(self): self.register_uris([ + self.get_nova_discovery_mock_dict(), dict(method='POST', uri='{endpoint}/servers/{id}/action'.format( endpoint=fakes.COMPUTE_ENDPOINT, diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 9f2ffd254..74aeeecc1 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -126,6 +126,8 @@ def setUp(self): self.resp.status_code = 200 self.sess = mock.Mock() self.sess.post = mock.Mock(return_value=self.resp) + # totally arbitrary + self.sess.default_microversion = '2.88' def test_basic(self): sot = server.Server() @@ -293,7 +295,9 @@ def test_change_password(self): body = {"changePassword": {"adminPass": "a"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_reboot(self): sot = server.Server(**EXAMPLE) @@ -304,7 +308,9 @@ def test_reboot(self): body = {"reboot": {"type": "HARD"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_force_delete(self): sot = server.Server(**EXAMPLE) @@ -315,7 +321,9 @@ def test_force_delete(self): body = {'forceDelete': None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_rebuild(self): sot = server.Server(**EXAMPLE) @@ -348,7 +356,9 @@ def test_rebuild(self): } headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_rebuild_minimal(self): sot = server.Server(**EXAMPLE) @@ -371,7 +381,9 @@ def test_rebuild_minimal(self): } headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_resize(self): sot = server.Server(**EXAMPLE) @@ -382,7 +394,9 @@ def test_resize(self): body = {"resize": {"flavorRef": "2"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_confirm_resize(self): sot = server.Server(**EXAMPLE) @@ -393,7 +407,9 @@ def test_confirm_resize(self): body = {"confirmResize": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_revert_resize(self): sot = server.Server(**EXAMPLE) @@ -404,7 +420,9 @@ def test_revert_resize(self): body = {"revertResize": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_create_image_header(self): sot = server.Server(**EXAMPLE) @@ -431,7 +449,9 @@ def test_create_image_header(self): image_id = sot.create_image(self.sess, name, metadata) self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) self.assertEqual('dummy2', image_id) @@ -500,7 +520,9 @@ def test_add_security_group(self): body = {"addSecurityGroup": {"name": "group"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_remove_security_group(self): sot = server.Server(**EXAMPLE) @@ -511,7 +533,9 @@ def test_remove_security_group(self): body = {"removeSecurityGroup": {"name": "group"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_reset_state(self): sot = server.Server(**EXAMPLE) @@ -522,7 +546,9 @@ def test_reset_state(self): body = {"os-resetState": {"state": 'active'}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_add_fixed_ip(self): sot = server.Server(**EXAMPLE) @@ -534,7 +560,9 @@ def test_add_fixed_ip(self): body = {"addFixedIp": {"networkId": "NETWORK-ID"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_remove_fixed_ip(self): sot = server.Server(**EXAMPLE) @@ -546,7 +574,9 @@ def test_remove_fixed_ip(self): body = {"removeFixedIp": {"address": "ADDRESS"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_add_floating_ip(self): sot = server.Server(**EXAMPLE) @@ -558,7 +588,9 @@ def test_add_floating_ip(self): body = {"addFloatingIp": {"address": "FLOATING-IP"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_add_floating_ip_with_fixed_addr(self): sot = server.Server(**EXAMPLE) @@ -571,7 +603,9 @@ def test_add_floating_ip_with_fixed_addr(self): "fixed_address": "FIXED-ADDR"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_remove_floating_ip(self): sot = server.Server(**EXAMPLE) @@ -583,7 +617,9 @@ def test_remove_floating_ip(self): body = {"removeFloatingIp": {"address": "I-AM-FLOATING"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_backup(self): sot = server.Server(**EXAMPLE) @@ -596,7 +632,9 @@ def test_backup(self): "rotation": 1}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_pause(self): sot = server.Server(**EXAMPLE) @@ -608,7 +646,9 @@ def test_pause(self): body = {"pause": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_unpause(self): sot = server.Server(**EXAMPLE) @@ -620,7 +660,9 @@ def test_unpause(self): body = {"unpause": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_suspend(self): sot = server.Server(**EXAMPLE) @@ -632,7 +674,9 @@ def test_suspend(self): body = {"suspend": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_resume(self): sot = server.Server(**EXAMPLE) @@ -644,7 +688,9 @@ def test_resume(self): body = {"resume": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_lock(self): sot = server.Server(**EXAMPLE) @@ -656,7 +702,9 @@ def test_lock(self): body = {"lock": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_lock_with_options(self): sot = server.Server(**EXAMPLE) @@ -668,7 +716,9 @@ def test_lock_with_options(self): body = {'lock': {'locked_reason': 'Because why not'}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_unlock(self): sot = server.Server(**EXAMPLE) @@ -680,7 +730,9 @@ def test_unlock(self): body = {"unlock": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_rescue(self): sot = server.Server(**EXAMPLE) @@ -692,7 +744,9 @@ def test_rescue(self): body = {"rescue": {}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_rescue_with_options(self): sot = server.Server(**EXAMPLE) @@ -705,7 +759,9 @@ def test_rescue_with_options(self): 'rescue_image_ref': 'IMG-ID'}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_unrescue(self): sot = server.Server(**EXAMPLE) @@ -717,7 +773,9 @@ def test_unrescue(self): body = {"unrescue": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_evacuate(self): sot = server.Server(**EXAMPLE) @@ -729,7 +787,9 @@ def test_evacuate(self): body = {"evacuate": {}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_evacuate_with_options(self): sot = server.Server(**EXAMPLE) @@ -743,7 +803,9 @@ def test_evacuate_with_options(self): 'force': True}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_start(self): sot = server.Server(**EXAMPLE) @@ -755,7 +817,9 @@ def test_start(self): body = {"os-start": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_stop(self): sot = server.Server(**EXAMPLE) @@ -767,7 +831,9 @@ def test_stop(self): body = {"os-stop": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_shelve(self): sot = server.Server(**EXAMPLE) @@ -779,7 +845,9 @@ def test_shelve(self): body = {"shelve": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_unshelve(self): sot = server.Server(**EXAMPLE) @@ -791,7 +859,9 @@ def test_unshelve(self): body = {"unshelve": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_unshelve_availability_zone(self): sot = server.Server(**EXAMPLE) @@ -805,7 +875,9 @@ def test_unshelve_availability_zone(self): }} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_migrate(self): sot = server.Server(**EXAMPLE) @@ -818,7 +890,9 @@ def test_migrate(self): headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_trigger_crash_dump(self): sot = server.Server(**EXAMPLE) @@ -830,7 +904,9 @@ def test_trigger_crash_dump(self): body = {'trigger_crash_dump': None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_get_console_output(self): sot = server.Server(**EXAMPLE) @@ -842,7 +918,9 @@ def test_get_console_output(self): body = {'os-getConsoleOutput': {}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) res = sot.get_console_output(self.sess, length=1) @@ -852,7 +930,9 @@ def test_get_console_output(self): headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_get_console_url(self): sot = server.Server(**EXAMPLE) @@ -867,32 +947,42 @@ def test_get_console_url(self): self.sess.post.assert_called_with( 'servers/IDENTIFIER/action', json={'os-getVNCConsole': {'type': 'novnc'}}, - headers={'Accept': ''}, microversion=None) + headers={'Accept': ''}, + microversion=self.sess.default_microversion, + ) self.assertDictEqual(resp.body['console'], res) sot.get_console_url(self.sess, 'xvpvnc') self.sess.post.assert_called_with( 'servers/IDENTIFIER/action', json={'os-getVNCConsole': {'type': 'xvpvnc'}}, - headers={'Accept': ''}, microversion=None) + headers={'Accept': ''}, + microversion=self.sess.default_microversion, + ) sot.get_console_url(self.sess, 'spice-html5') self.sess.post.assert_called_with( 'servers/IDENTIFIER/action', json={'os-getSPICEConsole': {'type': 'spice-html5'}}, - headers={'Accept': ''}, microversion=None) + headers={'Accept': ''}, + microversion=self.sess.default_microversion, + ) sot.get_console_url(self.sess, 'rdp-html5') self.sess.post.assert_called_with( 'servers/IDENTIFIER/action', json={'os-getRDPConsole': {'type': 'rdp-html5'}}, - headers={'Accept': ''}, microversion=None) + headers={'Accept': ''}, + microversion=self.sess.default_microversion, + ) sot.get_console_url(self.sess, 'serial') self.sess.post.assert_called_with( 'servers/IDENTIFIER/action', json={'os-getSerialConsole': {'type': 'serial'}}, - headers={'Accept': ''}, microversion=None) + headers={'Accept': ''}, + microversion=self.sess.default_microversion, + ) self.assertRaises(ValueError, sot.get_console_url, @@ -940,7 +1030,9 @@ class FakeEndpointData: headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) def test_live_migrate_25(self): sot = server.Server(**EXAMPLE) diff --git a/releasenotes/notes/server-actions-microversion-support-f14b293d9c3d3d5e.yaml b/releasenotes/notes/server-actions-microversion-support-f14b293d9c3d3d5e.yaml new file mode 100644 index 000000000..1421a703d --- /dev/null +++ b/releasenotes/notes/server-actions-microversion-support-f14b293d9c3d3d5e.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Server actions such as reboot and resize will now default to the + latest microversion instead of 2.1 as before. From 3356e7ceca8f2f8fd910126123e4d6390461d7c8 Mon Sep 17 00:00:00 2001 From: Felix Huettner Date: Mon, 19 Dec 2022 17:34:15 +0100 Subject: [PATCH 3170/3836] Allow passing more arguments to create_port create_port missed a large amount of arguments that are valid from an api perspective. We add them now here. Change-Id: Ie311a6b90e4a855fbb2a76c7595d8703fa8caa0b --- openstack/cloud/_network.py | 18 ++++++++++++++++-- openstack/tests/unit/cloud/test_port.py | 23 +++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index 7358490a7..99af1456c 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -2349,7 +2349,10 @@ def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, 'allowed_address_pairs', 'extra_dhcp_opts', 'device_owner', 'device_id', 'binding:vnic_type', 'binding:profile', 'port_security_enabled', - 'qos_policy_id', 'binding:host_id') + 'qos_policy_id', 'binding:host_id', 'project_id', + 'description', 'dns_domain', 'dns_name', + 'numa_affinity_policy', 'propagate_uplink_status', + 'mac_learning_enabled') def create_port(self, network_id, **kwargs): """Create a port @@ -2403,7 +2406,18 @@ def create_port(self, network_id, **kwargs): :param binding vnic_type: The type of the created port. (Optional) :param port_security_enabled: The security port state created on the network. (Optional) - :param qos_policy_id: The ID of the QoS policy to apply for port. + :param qos_policy_id: The ID of the QoS policy to apply for + port. (Optional) + :param project_id: The project in which to create the port. (Optional) + :param description: Description of the port. (Optional) + :param dns_domain: DNS domain relevant for the port. (Optional) + :param dns_name: DNS name of the port. (Optional) + :param numa_affinity_policy: the numa affinitiy policy. May be + "None", "required", "preferred" or "legacy". (Optional) + :param propagate_uplink_status: If the uplink status of the port should + be propagated. (Optional) + :param mac_learning_enabled: If mac learning should be enabled on the + port. (Optional) :returns: The created network ``Port`` object. :raises: ``OpenStackCloudException`` on operation error. """ diff --git a/openstack/tests/unit/cloud/test_port.py b/openstack/tests/unit/cloud/test_port.py index 350c1195a..04e5726de 100644 --- a/openstack/tests/unit/cloud/test_port.py +++ b/openstack/tests/unit/cloud/test_port.py @@ -190,6 +190,29 @@ def test_create_port_exception(self): admin_state_up=True) self.assert_calls() + def test_create_port_with_project(self): + self.mock_neutron_port_create_rep["port"].update( + { + 'project_id': 'test-project-id', + }) + self.register_uris([ + dict(method="POST", + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports']), + json=self.mock_neutron_port_create_rep, + validate=dict( + json={'port': { + 'network_id': 'test-net-id', + 'project_id': 'test-project-id', + 'name': 'test-port-name', + 'admin_state_up': True}})) + ]) + port = self.cloud.create_port( + network_id='test-net-id', name='test-port-name', + admin_state_up=True, project_id='test-project-id') + self._compare_ports(self.mock_neutron_port_create_rep['port'], port) + self.assert_calls() + def test_update_port(self): port_id = 'd80b1a3b-4fc1-49f3-952e-1e2ab7081d8b' self.register_uris([ From a0f969cf92691c4c8beeeedd69d9884923a92c0e Mon Sep 17 00:00:00 2001 From: Clark Boylan Date: Thu, 15 Dec 2022 08:53:07 -0800 Subject: [PATCH 3171/3836] Update tox.ini for tox v4 compatibility Tox v4 made a number of changes that break compatibility with old tox v3 tox.ini files. In particular openstacksdk lists env vars for passenv on a single line separating them with spaces. This is no longer allowed and produces this error: failed with pass_env values cannot contain whitespace, use comma to have multiple values in a single line, invalid values found 'OS_* OPENSTACKSDK_*' We can fix this by listing each value on a single line which we do here. Depends-On: https://review.opendev.org/c/openstack/python-openstackclient/+/868430 Change-Id: Ibf4a16e544921ec914f6e23112e0fc37a12096a5 --- tox.ini | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index a9de9c143..59b96e9d5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,15 @@ [tox] minversion = 3.18.0 envlist = pep8,py3 -skipsdist = True +# skipsdist = True ignore_basepython_conflict = True [testenv] usedevelop = True install_command = pip install {opts} {packages} -passenv = OS_* OPENSTACKSDK_* +passenv = + OS_* + OPENSTACKSDK_* basepython = python3 setenv = VIRTUAL_ENV={envdir} @@ -76,7 +78,8 @@ commands = {posargs} [testenv:debug] # allow 1 year, or 31536000 seconds, to debug a test before it times out -setenv = OS_TEST_TIMEOUT=31536000 +setenv = + OS_TEST_TIMEOUT=31536000 allowlist_externals = find commands = find . -type f -name "*.pyc" -delete @@ -94,7 +97,10 @@ commands = [testenv:ansible] # Need to pass some env vars for the Ansible playbooks -passenv = HOME USER ANSIBLE_VAR_* +passenv = + HOME + USER + ANSIBLE_VAR_* deps = {[testenv]deps} ansible From 0ea056f3a308aa227a102fa35e532bc561e2158d Mon Sep 17 00:00:00 2001 From: Bodo Petermann Date: Thu, 29 Dec 2022 13:35:18 +0100 Subject: [PATCH 3172/3836] Add query mappings for vpnaas resources Network VPNaaS resources like VPN service, IPSEC site connections etc didn't have explicit query mappings. Adding the query mappings allows to filter for e.g. project_id already on the server instead of transferring the list of all resources and then filter at the client side. Affected resource types: VpnService, VpnIPSecSiteConnection, VpnIpsecPolicy, VpnIkePolicy, VpnEndpointGroup, LoadBalancer Change-Id: I2c708de7dfc9f3be780e721248883e8a165b6294 --- openstack/network/v2/load_balancer.py | 6 ++++++ openstack/network/v2/vpn_endpoint_group.py | 7 +++++++ openstack/network/v2/vpn_ike_policy.py | 5 +++++ openstack/network/v2/vpn_ipsec_policy.py | 7 +++++++ openstack/network/v2/vpn_ipsec_site_connection.py | 10 ++++++++++ openstack/network/v2/vpn_service.py | 6 ++++++ .../tests/unit/network/v2/test_vpn_endpoint_group.py | 5 +++++ .../tests/unit/network/v2/test_vpn_ipsecpolicy.py | 7 +++++++ openstack/tests/unit/network/v2/test_vpn_service.py | 9 +++++++++ 9 files changed, 62 insertions(+) diff --git a/openstack/network/v2/load_balancer.py b/openstack/network/v2/load_balancer.py index 7ac082a55..6d4e07b3c 100644 --- a/openstack/network/v2/load_balancer.py +++ b/openstack/network/v2/load_balancer.py @@ -27,6 +27,12 @@ class LoadBalancer(resource.Resource): allow_delete = True allow_list = True + _query_mapping = resource.QueryParameters( + 'description', 'name', 'project_id', 'provider', 'provisioning_status', + 'tenant_id', 'vip_address', 'vip_subnet_id', + is_admin_state_up='admin_state_up' + ) + # Properties #: Description for the load balancer. description = resource.Body('description') diff --git a/openstack/network/v2/vpn_endpoint_group.py b/openstack/network/v2/vpn_endpoint_group.py index 89ffaff84..12a0a4e59 100644 --- a/openstack/network/v2/vpn_endpoint_group.py +++ b/openstack/network/v2/vpn_endpoint_group.py @@ -27,12 +27,19 @@ class VpnEndpointGroup(resource.Resource): allow_delete = True allow_list = True + _query_mapping = resource.QueryParameters( + 'description', 'name', 'project_id', 'tenant_id', + type='endpoint_type' + ) + # Properties #: Human-readable description for the resource. description = resource.Body('description') #: List of endpoints of the same type, for the endpoint group. #: The values will depend on type. endpoints = resource.Body('endpoints', type=list) + #: Human-readable name of the resource. Default is an empty string. + name = resource.Body('name') project_id = resource.Body('project_id', alias='tenant_id') #: Tenant_id (deprecated attribute). tenant_id = resource.Body('tenant_id', deprecated=True) diff --git a/openstack/network/v2/vpn_ike_policy.py b/openstack/network/v2/vpn_ike_policy.py index d86081641..9cc585ae0 100644 --- a/openstack/network/v2/vpn_ike_policy.py +++ b/openstack/network/v2/vpn_ike_policy.py @@ -26,6 +26,11 @@ class VpnIkePolicy(resource.Resource): allow_delete = True allow_list = True + _query_mapping = resource.QueryParameters( + 'auth_algorithm', 'description', 'encryption_algorithm', 'ike_version', + 'name', 'pfs', 'project_id', 'phase1_negotiation_mode', + ) + # Properties #: The authentication hash algorithm. Valid values are sha1, # sha256, sha384, sha512. The default is sha1. diff --git a/openstack/network/v2/vpn_ipsec_policy.py b/openstack/network/v2/vpn_ipsec_policy.py index 60df3cbe8..5b685b33e 100644 --- a/openstack/network/v2/vpn_ipsec_policy.py +++ b/openstack/network/v2/vpn_ipsec_policy.py @@ -25,6 +25,11 @@ class VpnIpsecPolicy(resource.Resource): allow_delete = True allow_list = True + _query_mapping = resource.QueryParameters( + 'auth_algorithm', 'description', 'encryption_algorithm', 'name', 'pfs', + 'project_id', 'phase1_negotiation_mode', + ) + # Properties #: The authentication hash algorithm. Valid values are sha1, # sha256, sha384, sha512. The default is sha1. @@ -40,6 +45,8 @@ class VpnIpsecPolicy(resource.Resource): # portion of the lifetime. Default unit is seconds and # default value is 3600. lifetime = resource.Body('lifetime', type=dict) + #: Human-readable name of the resource. Default is an empty string. + name = resource.Body('name') #: Perfect forward secrecy (PFS). A valid value is Group2, # Group5, Group14, and so on. Default is Group5. pfs = resource.Body('pfs') diff --git a/openstack/network/v2/vpn_ipsec_site_connection.py b/openstack/network/v2/vpn_ipsec_site_connection.py index 37e166f37..f74c2c5ee 100644 --- a/openstack/network/v2/vpn_ipsec_site_connection.py +++ b/openstack/network/v2/vpn_ipsec_site_connection.py @@ -25,6 +25,14 @@ class VpnIPSecSiteConnection(resource.Resource): allow_delete = True allow_list = True + _query_mapping = resource.QueryParameters( + 'auth_mode', 'description', 'ikepolicy_id', 'ipsecpolicy_id', + 'initiator', 'local_ep_group_id', 'peer_address', 'local_id', + 'mtu', 'name', 'peer_id', 'project_id', 'psk', 'peer_ep_group_id', + 'route_mode', 'vpnservice_id', 'status', + is_admin_state_up='admin_state_up' + ) + # Properties #: The dead peer detection (DPD) action. # A valid value is clear, hold, restart, @@ -96,6 +104,8 @@ class VpnIPSecSiteConnection(resource.Resource): peer_ep_group_id = resource.Body('peer_ep_group_id') #: The route mode. A valid value is static, which is the default. route_mode = resource.Body('route_mode') + #: The site connection status + status = resource.Body('status') #: The dead peer detection (DPD) timeout # in seconds. A valid value is a # positive integer that is greater diff --git a/openstack/network/v2/vpn_service.py b/openstack/network/v2/vpn_service.py index f267f1546..ef887b5a2 100644 --- a/openstack/network/v2/vpn_service.py +++ b/openstack/network/v2/vpn_service.py @@ -27,6 +27,12 @@ class VpnService(resource.Resource): allow_delete = True allow_list = True + _query_mapping = resource.QueryParameters( + 'description', 'external_v4_ip', 'external_v6_ip', 'name', 'router_id', + 'project_id', 'tenant_id', 'subnet_id', + is_admin_state_up='admin_state_up' + ) + # Properties #: Human-readable description for the vpnservice. description = resource.Body('description') diff --git a/openstack/tests/unit/network/v2/test_vpn_endpoint_group.py b/openstack/tests/unit/network/v2/test_vpn_endpoint_group.py index c94643324..4c98666c9 100644 --- a/openstack/tests/unit/network/v2/test_vpn_endpoint_group.py +++ b/openstack/tests/unit/network/v2/test_vpn_endpoint_group.py @@ -55,5 +55,10 @@ def test_make_it(self): { "limit": "limit", "marker": "marker", + 'description': 'description', + 'name': 'name', + 'project_id': 'project_id', + 'tenant_id': 'tenant_id', + 'type': 'endpoint_type', }, sot._query_mapping._mapping) diff --git a/openstack/tests/unit/network/v2/test_vpn_ipsecpolicy.py b/openstack/tests/unit/network/v2/test_vpn_ipsecpolicy.py index 50136e04d..fd6f7c88e 100644 --- a/openstack/tests/unit/network/v2/test_vpn_ipsecpolicy.py +++ b/openstack/tests/unit/network/v2/test_vpn_ipsecpolicy.py @@ -57,5 +57,12 @@ def test_make_it(self): { "limit": "limit", "marker": "marker", + 'auth_algorithm': 'auth_algorithm', + 'description': 'description', + 'encryption_algorithm': 'encryption_algorithm', + 'name': 'name', + 'pfs': 'pfs', + 'project_id': 'project_id', + 'phase1_negotiation_mode': 'phase1_negotiation_mode', }, sot._query_mapping._mapping) diff --git a/openstack/tests/unit/network/v2/test_vpn_service.py b/openstack/tests/unit/network/v2/test_vpn_service.py index c64a72d67..8b4c29236 100644 --- a/openstack/tests/unit/network/v2/test_vpn_service.py +++ b/openstack/tests/unit/network/v2/test_vpn_service.py @@ -59,5 +59,14 @@ def test_make_it(self): { "limit": "limit", "marker": "marker", + 'description': 'description', + 'external_v4_ip': 'external_v4_ip', + 'external_v6_ip': 'external_v6_ip', + 'name': 'name', + 'router_id': 'router_id', + 'project_id': 'project_id', + 'tenant_id': 'tenant_id', + 'subnet_id': 'subnet_id', + 'is_admin_state_up': 'admin_state_up', }, sot._query_mapping._mapping) From ea6ed716a20eae2f86acc9f19cd637958cd55ae4 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 18 Nov 2022 15:15:11 +0100 Subject: [PATCH 3173/3836] Move normalize security group to sg class Move normalize security group methods directly to _security_group mixin to stop inheriting from normalize Change-Id: If866bce4ae40b17ad9f9c8823281d4093791320c --- openstack/cloud/_normalize.py | 113 -------------- openstack/cloud/_security_group.py | 128 +++++++++++++++- openstack/tests/unit/cloud/test_normalize.py | 142 ------------------ .../tests/unit/cloud/test_security_groups.py | 88 +++++++++++ 4 files changed, 214 insertions(+), 257 deletions(-) diff --git a/openstack/cloud/_normalize.py b/openstack/cloud/_normalize.py index 3d3a84dbc..a6020f61a 100644 --- a/openstack/cloud/_normalize.py +++ b/openstack/cloud/_normalize.py @@ -179,119 +179,6 @@ def _normalize_image(self, image): new_image['minRam'] = new_image['min_ram'] return new_image - # TODO(stephenfin): Remove this once we get rid of support for nova - # secgroups - def _normalize_secgroups(self, groups): - """Normalize the structure of security groups - - This makes security group dicts, as returned from nova, look like the - security group dicts as returned from neutron. This does not make them - look exactly the same, but it's pretty close. - - :param list groups: A list of security group dicts. - - :returns: A list of normalized dicts. - """ - ret = [] - for group in groups: - ret.append(self._normalize_secgroup(group)) - return ret - - # TODO(stephenfin): Remove this once we get rid of support for nova - # secgroups - def _normalize_secgroup(self, group): - - ret = munch.Munch() - # Copy incoming group because of shared dicts in unittests - group = group.copy() - - # Discard noise - self._remove_novaclient_artifacts(group) - - rules = self._normalize_secgroup_rules( - group.pop('security_group_rules', group.pop('rules', []))) - project_id = group.pop('tenant_id', '') - project_id = group.pop('project_id', project_id) - - ret['location'] = self._get_current_location(project_id=project_id) - ret['id'] = group.pop('id') - ret['name'] = group.pop('name') - ret['security_group_rules'] = rules - ret['description'] = group.pop('description') - ret['properties'] = group - - if self._use_neutron_secgroups(): - ret['stateful'] = group.pop('stateful', True) - - # Backwards compat with Neutron - if not self.strict_mode: - ret['tenant_id'] = project_id - ret['project_id'] = project_id - for key, val in ret['properties'].items(): - ret.setdefault(key, val) - - return ret - - # TODO(stephenfin): Remove this once we get rid of support for nova - # secgroups - def _normalize_secgroup_rules(self, rules): - """Normalize the structure of nova security group rules - - Note that nova uses -1 for non-specific port values, but neutron - represents these with None. - - :param list rules: A list of security group rule dicts. - - :returns: A list of normalized dicts. - """ - ret = [] - for rule in rules: - ret.append(self._normalize_secgroup_rule(rule)) - return ret - - # TODO(stephenfin): Remove this once we get rid of support for nova - # secgroups - def _normalize_secgroup_rule(self, rule): - ret = munch.Munch() - # Copy incoming rule because of shared dicts in unittests - rule = rule.copy() - - ret['id'] = rule.pop('id') - ret['direction'] = rule.pop('direction', 'ingress') - ret['ethertype'] = rule.pop('ethertype', 'IPv4') - port_range_min = rule.get( - 'port_range_min', rule.pop('from_port', None)) - if port_range_min == -1: - port_range_min = None - if port_range_min is not None: - port_range_min = int(port_range_min) - ret['port_range_min'] = port_range_min - port_range_max = rule.pop( - 'port_range_max', rule.pop('to_port', None)) - if port_range_max == -1: - port_range_max = None - if port_range_min is not None: - port_range_min = int(port_range_min) - ret['port_range_max'] = port_range_max - ret['protocol'] = rule.pop('protocol', rule.pop('ip_protocol', None)) - ret['remote_ip_prefix'] = rule.pop( - 'remote_ip_prefix', rule.pop('ip_range', {}).get('cidr', None)) - ret['security_group_id'] = rule.pop( - 'security_group_id', rule.pop('parent_group_id', None)) - ret['remote_group_id'] = rule.pop('remote_group_id', None) - project_id = rule.pop('tenant_id', '') - project_id = rule.pop('project_id', project_id) - ret['location'] = self._get_current_location(project_id=project_id) - ret['properties'] = rule - - # Backwards compat with Neutron - if not self.strict_mode: - ret['tenant_id'] = project_id - ret['project_id'] = project_id - for key, val in ret['properties'].items(): - ret.setdefault(key, val) - return ret - def _normalize_server(self, server): ret = munch.Munch() # Copy incoming server because of shared dicts in unittests diff --git a/openstack/cloud/_security_group.py b/openstack/cloud/_security_group.py index b63959701..4637773c7 100644 --- a/openstack/cloud/_security_group.py +++ b/openstack/cloud/_security_group.py @@ -16,14 +16,13 @@ # import jsonpatch import types # noqa -from openstack.cloud import _normalize from openstack.cloud import _utils from openstack.cloud import exc from openstack import exceptions from openstack import proxy -class SecurityGroupCloudMixin(_normalize.Normalizer): +class SecurityGroupCloudMixin: def __init__(self): self.secgroup_source = self.config.config['secgroup_source'] @@ -417,3 +416,128 @@ def _has_secgroups(self): def _use_neutron_secgroups(self): return (self.has_service('network') and self.secgroup_source == 'neutron') + + def _normalize_secgroups(self, groups): + """Normalize the structure of security groups + + This makes security group dicts, as returned from nova, look like the + security group dicts as returned from neutron. This does not make them + look exactly the same, but it's pretty close. + + :param list groups: A list of security group dicts. + + :returns: A list of normalized dicts. + """ + ret = [] + for group in groups: + ret.append(self._normalize_secgroup(group)) + return ret + + # TODO(stephenfin): Remove this once we get rid of support for nova + # secgroups + def _normalize_secgroup(self, group): + + import munch + + ret = munch.Munch() + # Copy incoming group because of shared dicts in unittests + group = group.copy() + + # Discard noise + self._remove_novaclient_artifacts(group) + + rules = self._normalize_secgroup_rules( + group.pop('security_group_rules', group.pop('rules', []))) + project_id = group.pop('tenant_id', '') + project_id = group.pop('project_id', project_id) + + ret['location'] = self._get_current_location(project_id=project_id) + ret['id'] = group.pop('id') + ret['name'] = group.pop('name') + ret['security_group_rules'] = rules + ret['description'] = group.pop('description') + ret['properties'] = group + + if self._use_neutron_secgroups(): + ret['stateful'] = group.pop('stateful', True) + + # Backwards compat with Neutron + if not self.strict_mode: + ret['tenant_id'] = project_id + ret['project_id'] = project_id + for key, val in ret['properties'].items(): + ret.setdefault(key, val) + + return ret + + # TODO(stephenfin): Remove this once we get rid of support for nova + # secgroups + def _normalize_secgroup_rules(self, rules): + """Normalize the structure of nova security group rules + + Note that nova uses -1 for non-specific port values, but neutron + represents these with None. + + :param list rules: A list of security group rule dicts. + + :returns: A list of normalized dicts. + """ + ret = [] + for rule in rules: + ret.append(self._normalize_secgroup_rule(rule)) + return ret + + # TODO(stephenfin): Remove this once we get rid of support for nova + # secgroups + def _normalize_secgroup_rule(self, rule): + + import munch + + ret = munch.Munch() + # Copy incoming rule because of shared dicts in unittests + rule = rule.copy() + + ret['id'] = rule.pop('id') + ret['direction'] = rule.pop('direction', 'ingress') + ret['ethertype'] = rule.pop('ethertype', 'IPv4') + port_range_min = rule.get( + 'port_range_min', rule.pop('from_port', None)) + if port_range_min == -1: + port_range_min = None + if port_range_min is not None: + port_range_min = int(port_range_min) + ret['port_range_min'] = port_range_min + port_range_max = rule.pop( + 'port_range_max', rule.pop('to_port', None)) + if port_range_max == -1: + port_range_max = None + if port_range_min is not None: + port_range_min = int(port_range_min) + ret['port_range_max'] = port_range_max + ret['protocol'] = rule.pop('protocol', rule.pop('ip_protocol', None)) + ret['remote_ip_prefix'] = rule.pop( + 'remote_ip_prefix', rule.pop('ip_range', {}).get('cidr', None)) + ret['security_group_id'] = rule.pop( + 'security_group_id', rule.pop('parent_group_id', None)) + ret['remote_group_id'] = rule.pop('remote_group_id', None) + project_id = rule.pop('tenant_id', '') + project_id = rule.pop('project_id', project_id) + ret['location'] = self._get_current_location(project_id=project_id) + ret['properties'] = rule + + # Backwards compat with Neutron + if not self.strict_mode: + ret['tenant_id'] = project_id + ret['project_id'] = project_id + for key, val in ret['properties'].items(): + ret.setdefault(key, val) + return ret + + def _remove_novaclient_artifacts(self, item): + # Remove novaclient artifacts + item.pop('links', None) + item.pop('NAME_ATTR', None) + item.pop('HUMAN_ID', None) + item.pop('human_id', None) + item.pop('request_ids', None) + item.pop('x_openstack_request_ids', None) diff --git a/openstack/tests/unit/cloud/test_normalize.py b/openstack/tests/unit/cloud/test_normalize.py index be6eab3e4..fe5c9beeb 100644 --- a/openstack/tests/unit/cloud/test_normalize.py +++ b/openstack/tests/unit/cloud/test_normalize.py @@ -474,98 +474,6 @@ def test_normalize_glance_images(self): retval = self.cloud._normalize_image(image) self.assertDictEqual(expected, retval) - def test_normalize_secgroups(self): - nova_secgroup = dict( - id='abc123', - name='nova_secgroup', - description='A Nova security group', - rules=[ - dict(id='123', from_port=80, to_port=81, ip_protocol='tcp', - ip_range={'cidr': '0.0.0.0/0'}, parent_group_id='xyz123') - ] - ) - - expected = dict( - id='abc123', - name='nova_secgroup', - description='A Nova security group', - tenant_id='', - project_id='', - properties={}, - location=dict( - region_name='RegionOne', - zone=None, - project=dict( - domain_name='default', - id='1c36b64c840a42cd9e9b931a369337f0', - domain_id=None, - name='admin'), - cloud='_test_cloud_'), - security_group_rules=[ - dict(id='123', direction='ingress', ethertype='IPv4', - port_range_min=80, port_range_max=81, protocol='tcp', - remote_ip_prefix='0.0.0.0/0', security_group_id='xyz123', - properties={}, - tenant_id='', - project_id='', - remote_group_id=None, - location=dict( - region_name='RegionOne', - zone=None, - project=dict( - domain_name='default', - id='1c36b64c840a42cd9e9b931a369337f0', - domain_id=None, - name='admin'), - cloud='_test_cloud_')) - ] - ) - # Set secgroup source to nova for this test as stateful parameter - # is only valid for neutron security groups. - self.cloud.secgroup_source = 'nova' - retval = self.cloud._normalize_secgroup(nova_secgroup) - self.cloud.secgroup_source = 'neutron' - self.assertEqual(expected, retval) - - def test_normalize_secgroups_negone_port(self): - nova_secgroup = dict( - id='abc123', - name='nova_secgroup', - description='A Nova security group with -1 ports', - rules=[ - dict(id='123', from_port=-1, to_port=-1, ip_protocol='icmp', - ip_range={'cidr': '0.0.0.0/0'}, parent_group_id='xyz123') - ] - ) - - retval = self.cloud._normalize_secgroup(nova_secgroup) - self.assertIsNone(retval['security_group_rules'][0]['port_range_min']) - self.assertIsNone(retval['security_group_rules'][0]['port_range_max']) - - def test_normalize_secgroup_rules(self): - nova_rules = [ - dict(id='123', from_port=80, to_port=81, ip_protocol='tcp', - ip_range={'cidr': '0.0.0.0/0'}, parent_group_id='xyz123') - ] - expected = [ - dict(id='123', direction='ingress', ethertype='IPv4', - port_range_min=80, port_range_max=81, protocol='tcp', - remote_ip_prefix='0.0.0.0/0', security_group_id='xyz123', - tenant_id='', project_id='', remote_group_id=None, - properties={}, - location=dict( - region_name='RegionOne', - zone=None, - project=dict( - domain_name='default', - id='1c36b64c840a42cd9e9b931a369337f0', - domain_id=None, - name='admin'), - cloud='_test_cloud_')) - ] - retval = self.cloud._normalize_secgroup_rules(nova_rules) - self.assertEqual(expected, retval) - def test_normalize_coe_cluster_template(self): coe_cluster_template = RAW_COE_CLUSTER_TEMPLATE_DICT.copy() expected = { @@ -790,56 +698,6 @@ def test_normalize_glance_images(self): self.assertEqual(sorted(expected.keys()), sorted(retval.keys())) self.assertEqual(expected, retval) - def test_normalize_secgroups(self): - nova_secgroup = dict( - id='abc123', - name='nova_secgroup', - description='A Nova security group', - rules=[ - dict(id='123', from_port=80, to_port=81, ip_protocol='tcp', - ip_range={'cidr': '0.0.0.0/0'}, parent_group_id='xyz123') - ] - ) - - expected = dict( - id='abc123', - name='nova_secgroup', - description='A Nova security group', - properties={}, - location=dict( - region_name='RegionOne', - zone=None, - project=dict( - domain_name='default', - id='1c36b64c840a42cd9e9b931a369337f0', - domain_id=None, - name='admin'), - cloud='_test_cloud_'), - security_group_rules=[ - dict(id='123', direction='ingress', ethertype='IPv4', - port_range_min=80, port_range_max=81, protocol='tcp', - remote_ip_prefix='0.0.0.0/0', security_group_id='xyz123', - properties={}, - remote_group_id=None, - location=dict( - region_name='RegionOne', - zone=None, - project=dict( - domain_name='default', - id='1c36b64c840a42cd9e9b931a369337f0', - domain_id=None, - name='admin'), - cloud='_test_cloud_')) - ] - ) - - # Set secgroup source to nova for this test as stateful parameter - # is only valid for neutron security groups. - self.cloud.secgroup_source = 'nova' - retval = self.cloud._normalize_secgroup(nova_secgroup) - self.cloud.secgroup_source = 'neutron' - self.assertEqual(expected, retval) - def test_normalize_coe_cluster_template(self): coe_cluster_template = RAW_COE_CLUSTER_TEMPLATE_DICT.copy() expected = { diff --git a/openstack/tests/unit/cloud/test_security_groups.py b/openstack/tests/unit/cloud/test_security_groups.py index 5bc817aec..ed5c83707 100644 --- a/openstack/tests/unit/cloud/test_security_groups.py +++ b/openstack/tests/unit/cloud/test_security_groups.py @@ -851,3 +851,91 @@ def test_get_security_group_by_id_nova(self): self.assertEqual(nova_grp_dict['id'], ret_sg['id']) self.assertEqual(nova_grp_dict['name'], ret_sg['name']) self.assert_calls() + + def test_normalize_secgroups(self): + nova_secgroup = dict( + id='abc123', + name='nova_secgroup', + description='A Nova security group', + rules=[ + dict(id='123', from_port=80, to_port=81, ip_protocol='tcp', + ip_range={'cidr': '0.0.0.0/0'}, parent_group_id='xyz123') + ] + ) + expected = dict( + id='abc123', + name='nova_secgroup', + description='A Nova security group', + project_id='', + tenant_id='', + properties={}, + location=dict( + region_name='RegionOne', + zone=None, + project=dict( + domain_name='default', + id='1c36b64c840a42cd9e9b931a369337f0', + domain_id=None, + name='admin'), + cloud='_test_cloud_'), + security_group_rules=[ + dict(id='123', direction='ingress', ethertype='IPv4', + port_range_min=80, port_range_max=81, protocol='tcp', + remote_ip_prefix='0.0.0.0/0', security_group_id='xyz123', + project_id='', tenant_id='', properties={}, + remote_group_id=None, + location=dict( + region_name='RegionOne', + zone=None, + project=dict( + domain_name='default', + id='1c36b64c840a42cd9e9b931a369337f0', + domain_id=None, + name='admin'), + cloud='_test_cloud_')) + ] + ) + # Set secgroup source to nova for this test as stateful parameter + # is only valid for neutron security groups. + self.cloud.secgroup_source = 'nova' + retval = self.cloud._normalize_secgroup(nova_secgroup) + self.cloud.secgroup_source = 'neutron' + self.assertEqual(expected, retval) + + def test_normalize_secgroups_negone_port(self): + nova_secgroup = dict( + id='abc123', + name='nova_secgroup', + description='A Nova security group with -1 ports', + rules=[ + dict(id='123', from_port=-1, to_port=-1, ip_protocol='icmp', + ip_range={'cidr': '0.0.0.0/0'}, parent_group_id='xyz123') + ] + ) + retval = self.cloud._normalize_secgroup(nova_secgroup) + self.assertIsNone(retval['security_group_rules'][0]['port_range_min']) + self.assertIsNone(retval['security_group_rules'][0]['port_range_max']) + + def test_normalize_secgroup_rules(self): + nova_rules = [ + dict(id='123', from_port=80, to_port=81, ip_protocol='tcp', + ip_range={'cidr': '0.0.0.0/0'}, parent_group_id='xyz123') + ] + expected = [ + dict(id='123', direction='ingress', ethertype='IPv4', + port_range_min=80, port_range_max=81, protocol='tcp', + remote_ip_prefix='0.0.0.0/0', security_group_id='xyz123', + tenant_id='', project_id='', remote_group_id=None, + properties={}, + location=dict( + region_name='RegionOne', + zone=None, + project=dict( + domain_name='default', + id='1c36b64c840a42cd9e9b931a369337f0', + domain_id=None, + name='admin'), + cloud='_test_cloud_')) + ] + retval = self.cloud._normalize_secgroup_rules(nova_rules) + self.assertEqual(expected, retval) From bfe2c5981e25cfcff0380bc3a8947d9b63be6332 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 5 Dec 2022 11:48:45 +0100 Subject: [PATCH 3174/3836] Move normalize_server to compute mixin Move normalize_server method to where it is really invoked. This helps us clean up normalize class. Change-Id: I5d8daf53b86a9d81f365a4dcaefccd364c1e0a3e --- openstack/cloud/_compute.py | 164 ++++++++++++++++++++++++++++++++++ openstack/cloud/_normalize.py | 112 ----------------------- 2 files changed, 164 insertions(+), 112 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 58da2feec..d621fd29c 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -30,6 +30,48 @@ from openstack import utils +_SERVER_FIELDS = ( + 'accessIPv4', + 'accessIPv6', + 'addresses', + 'adminPass', + 'created', + 'description', + 'key_name', + 'metadata', + 'networks', + 'personality', + 'private_v4', + 'public_v4', + 'public_v6', + 'server_groups', + 'status', + 'updated', + 'user_id', + 'tags', +) + + +def _to_bool(value): + if isinstance(value, str): + if not value: + return False + prospective = value.lower().capitalize() + return prospective == 'True' + return bool(value) + + +def _pop_int(resource, key): + return int(resource.pop(key, 0) or 0) + + +def _pop_or_get(resource, key, default, strict): + if strict: + return resource.pop(key, default) + else: + return resource.get(key, default) + + class ComputeCloudMixin: def __init__(self): @@ -1749,3 +1791,125 @@ def _expand_server_vars(self, server): # TODO(mordred) remove after these make it into what we # actually want the API to be. return meta.expand_server_vars(self, server) + + def _remove_novaclient_artifacts(self, item): + # Remove novaclient artifacts + item.pop('links', None) + item.pop('NAME_ATTR', None) + item.pop('HUMAN_ID', None) + item.pop('human_id', None) + item.pop('request_ids', None) + item.pop('x_openstack_request_ids', None) + + def _normalize_server(self, server): + import munch + ret = munch.Munch() + # Copy incoming server because of shared dicts in unittests + # Wrap the copy in munch so that sub-dicts are properly munched + server = munch.Munch(server) + + self._remove_novaclient_artifacts(server) + + ret['id'] = server.pop('id') + ret['name'] = server.pop('name') + + server['flavor'].pop('links', None) + ret['flavor'] = server.pop('flavor') + # From original_names from sdk + server.pop('flavorRef', None) + + # OpenStack can return image as a string when you've booted + # from volume + image = server.pop('image', None) + if str(image) != image: + image = munch.Munch(id=image['id']) + + ret['image'] = image + # From original_names from sdk + server.pop('imageRef', None) + # From original_names from sdk + ret['block_device_mapping'] = server.pop('block_device_mapping_v2', {}) + + project_id = server.pop('tenant_id', '') + project_id = server.pop('project_id', project_id) + + az = _pop_or_get( + server, 'OS-EXT-AZ:availability_zone', None, self.strict_mode) + # the server resource has this already, but it's missing az info + # from the resource. + # TODO(mordred) create_server is still normalizing servers that aren't + # from the resource layer. + ret['location'] = server.pop( + 'location', self._get_current_location( + project_id=project_id, zone=az)) + + # Ensure volumes is always in the server dict, even if empty + ret['volumes'] = _pop_or_get( + server, 'os-extended-volumes:volumes_attached', + [], self.strict_mode) + + config_drive = server.pop( + 'has_config_drive', server.pop('config_drive', False)) + ret['has_config_drive'] = _to_bool(config_drive) + + host_id = server.pop('hostId', server.pop('host_id', None)) + ret['host_id'] = host_id + + ret['progress'] = _pop_int(server, 'progress') + + # Leave these in so that the general properties handling works + ret['disk_config'] = _pop_or_get( + server, 'OS-DCF:diskConfig', None, self.strict_mode) + for key in ( + 'OS-EXT-STS:power_state', + 'OS-EXT-STS:task_state', + 'OS-EXT-STS:vm_state', + 'OS-SRV-USG:launched_at', + 'OS-SRV-USG:terminated_at', + 'OS-EXT-SRV-ATTR:hypervisor_hostname', + 'OS-EXT-SRV-ATTR:instance_name', + 'OS-EXT-SRV-ATTR:user_data', + 'OS-EXT-SRV-ATTR:host', + 'OS-EXT-SRV-ATTR:hostname', + 'OS-EXT-SRV-ATTR:kernel_id', + 'OS-EXT-SRV-ATTR:launch_index', + 'OS-EXT-SRV-ATTR:ramdisk_id', + 'OS-EXT-SRV-ATTR:reservation_id', + 'OS-EXT-SRV-ATTR:root_device_name', + 'OS-SCH-HNT:scheduler_hints', + ): + short_key = key.split(':')[1] + ret[short_key] = _pop_or_get(server, key, None, self.strict_mode) + + # Protect against security_groups being None + ret['security_groups'] = server.pop('security_groups', None) or [] + + # NOTE(mnaser): The Nova API returns the creation date in `created` + # however the Shade contract returns `created_at` for + # all resources. + ret['created_at'] = server.get('created') + + for field in _SERVER_FIELDS: + ret[field] = server.pop(field, None) + if not ret['networks']: + ret['networks'] = {} + + ret['interface_ip'] = '' + + ret['properties'] = server.copy() + + # Backwards compat + if not self.strict_mode: + ret['hostId'] = host_id + ret['config_drive'] = config_drive + ret['project_id'] = project_id + ret['tenant_id'] = project_id + # TODO(efried): This is hardcoded to 'compute' because this method + # should only ever be used by the compute proxy. (That said, it + # doesn't appear to be used at all, so can we get rid of it?) + ret['region'] = self.config.get_region_name('compute') + ret['cloud'] = self.config.name + ret['az'] = az + for key, val in ret['properties'].items(): + ret.setdefault(key, val) + return ret diff --git a/openstack/cloud/_normalize.py b/openstack/cloud/_normalize.py index a6020f61a..df7b97703 100644 --- a/openstack/cloud/_normalize.py +++ b/openstack/cloud/_normalize.py @@ -179,118 +179,6 @@ def _normalize_image(self, image): new_image['minRam'] = new_image['min_ram'] return new_image - def _normalize_server(self, server): - ret = munch.Munch() - # Copy incoming server because of shared dicts in unittests - # Wrap the copy in munch so that sub-dicts are properly munched - server = munch.Munch(server) - - self._remove_novaclient_artifacts(server) - - ret['id'] = server.pop('id') - ret['name'] = server.pop('name') - - server['flavor'].pop('links', None) - ret['flavor'] = server.pop('flavor') - # From original_names from sdk - server.pop('flavorRef', None) - - # OpenStack can return image as a string when you've booted - # from volume - image = server.pop('image', None) - if str(image) != image: - image = munch.Munch(id=image['id']) - - ret['image'] = image - # From original_names from sdk - server.pop('imageRef', None) - # From original_names from sdk - ret['block_device_mapping'] = server.pop('block_device_mapping_v2', {}) - - project_id = server.pop('tenant_id', '') - project_id = server.pop('project_id', project_id) - - az = _pop_or_get( - server, 'OS-EXT-AZ:availability_zone', None, self.strict_mode) - # the server resource has this already, but it's missing az info - # from the resource. - # TODO(mordred) create_server is still normalizing servers that aren't - # from the resource layer. - ret['location'] = server.pop( - 'location', self._get_current_location( - project_id=project_id, zone=az)) - - # Ensure volumes is always in the server dict, even if empty - ret['volumes'] = _pop_or_get( - server, 'os-extended-volumes:volumes_attached', - [], self.strict_mode) - - config_drive = server.pop( - 'has_config_drive', server.pop('config_drive', False)) - ret['has_config_drive'] = _to_bool(config_drive) - - host_id = server.pop('hostId', server.pop('host_id', None)) - ret['host_id'] = host_id - - ret['progress'] = _pop_int(server, 'progress') - - # Leave these in so that the general properties handling works - ret['disk_config'] = _pop_or_get( - server, 'OS-DCF:diskConfig', None, self.strict_mode) - for key in ( - 'OS-EXT-STS:power_state', - 'OS-EXT-STS:task_state', - 'OS-EXT-STS:vm_state', - 'OS-SRV-USG:launched_at', - 'OS-SRV-USG:terminated_at', - 'OS-EXT-SRV-ATTR:hypervisor_hostname', - 'OS-EXT-SRV-ATTR:instance_name', - 'OS-EXT-SRV-ATTR:user_data', - 'OS-EXT-SRV-ATTR:host', - 'OS-EXT-SRV-ATTR:hostname', - 'OS-EXT-SRV-ATTR:kernel_id', - 'OS-EXT-SRV-ATTR:launch_index', - 'OS-EXT-SRV-ATTR:ramdisk_id', - 'OS-EXT-SRV-ATTR:reservation_id', - 'OS-EXT-SRV-ATTR:root_device_name', - 'OS-SCH-HNT:scheduler_hints', - ): - short_key = key.split(':')[1] - ret[short_key] = _pop_or_get(server, key, None, self.strict_mode) - - # Protect against security_groups being None - ret['security_groups'] = server.pop('security_groups', None) or [] - - # NOTE(mnaser): The Nova API returns the creation date in `created` - # however the Shade contract returns `created_at` for - # all resources. - ret['created_at'] = server.get('created') - - for field in _SERVER_FIELDS: - ret[field] = server.pop(field, None) - if not ret['networks']: - ret['networks'] = {} - - ret['interface_ip'] = '' - - ret['properties'] = server.copy() - - # Backwards compat - if not self.strict_mode: - ret['hostId'] = host_id - ret['config_drive'] = config_drive - ret['project_id'] = project_id - ret['tenant_id'] = project_id - # TODO(efried): This is hardcoded to 'compute' because this method - # should only ever be used by the compute proxy. (That said, it - # doesn't appear to be used at all, so can we get rid of it?) - ret['region'] = self.config.get_region_name('compute') - ret['cloud'] = self.config.name - ret['az'] = az - for key, val in ret['properties'].items(): - ret.setdefault(key, val) - return ret - def _normalize_compute_usage(self, usage): """ Normalize a compute usage object """ From 1046b2e367878409b581e076dd2f0bcca8b3c8bf Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 4 Jan 2023 12:15:31 +0000 Subject: [PATCH 3175/3836] tox: Trivial fixes Remove errant whitespace and a commented out line. Change-Id: Ice5ef168891fe35c4447468dd0d7956ef04db090 Signed-off-by: Stephen Finucane --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 59b96e9d5..6602a2901 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,6 @@ [tox] minversion = 3.18.0 envlist = pep8,py3 -# skipsdist = True ignore_basepython_conflict = True [testenv] @@ -78,7 +77,7 @@ commands = {posargs} [testenv:debug] # allow 1 year, or 31536000 seconds, to debug a test before it times out -setenv = +setenv = OS_TEST_TIMEOUT=31536000 allowlist_externals = find commands = From a79ee9ae3664c138208674e7ae71919becd67909 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 18 Nov 2022 16:56:56 +0100 Subject: [PATCH 3176/3836] Move _normalize_coe_* into _coe class stop inheriting _coe class from _normalize and move 2 required method into the class itself. Since this is the last class pulling _normalize it is seen that image.v1 method is failing on trying to do normalize. Stop doing that and return plain SDK resource. Change-Id: I58061e4df18ab1bb92bb52b8d2ea110b5233c980 --- openstack/cloud/_coe.py | 149 ++++++++- openstack/image/v1/_proxy.py | 2 +- openstack/tests/unit/cloud/test_image.py | 2 +- openstack/tests/unit/cloud/test_normalize.py | 309 ------------------- 4 files changed, 149 insertions(+), 313 deletions(-) diff --git a/openstack/cloud/_coe.py b/openstack/cloud/_coe.py index 6fef0e58c..67b39c078 100644 --- a/openstack/cloud/_coe.py +++ b/openstack/cloud/_coe.py @@ -15,12 +15,11 @@ # openstack.resource.Resource.list and openstack.resource2.Resource.list import types # noqa -from openstack.cloud import _normalize from openstack.cloud import _utils from openstack.cloud import exc -class CoeCloudMixin(_normalize.Normalizer): +class CoeCloudMixin: @property def _container_infra_client(self): @@ -418,3 +417,149 @@ def list_magnum_services(self): data = self._container_infra_client.get('/mservices') return self._normalize_magnum_services( self._get_and_munchify('mservices', data)) + + def _normalize_coe_clusters(self, coe_clusters): + ret = [] + for coe_cluster in coe_clusters: + ret.append(self._normalize_coe_cluster(coe_cluster)) + return ret + + def _normalize_coe_cluster(self, coe_cluster): + """Normalize Magnum COE cluster.""" + + # Only import munch when really necessary + import munch + + coe_cluster = coe_cluster.copy() + + # Discard noise + coe_cluster.pop('links', None) + + c_id = coe_cluster.pop('uuid') + + ret = munch.Munch( + id=c_id, + location=self._get_current_location(), + ) + + if not self.strict_mode: + ret['uuid'] = c_id + + for key in ( + 'status', + 'cluster_template_id', + 'stack_id', + 'keypair', + 'master_count', + 'create_timeout', + 'node_count', + 'name'): + if key in coe_cluster: + ret[key] = coe_cluster.pop(key) + + ret['properties'] = coe_cluster + return ret + + def _normalize_cluster_templates(self, cluster_templates): + ret = [] + for cluster_template in cluster_templates: + ret.append(self._normalize_cluster_template(cluster_template)) + return ret + + def _normalize_cluster_template(self, cluster_template): + """Normalize Magnum cluster_templates.""" + + import munch + + cluster_template = cluster_template.copy() + + # Discard noise + cluster_template.pop('links', None) + cluster_template.pop('human_id', None) + # model_name is a magnumclient-ism + cluster_template.pop('model_name', None) + + ct_id = cluster_template.pop('uuid') + + ret = munch.Munch( + id=ct_id, + location=self._get_current_location(), + ) + ret['is_public'] = cluster_template.pop('public') + ret['is_registry_enabled'] = cluster_template.pop('registry_enabled') + ret['is_tls_disabled'] = cluster_template.pop('tls_disabled') + # pop floating_ip_enabled since we want to hide it in a future patch + fip_enabled = cluster_template.pop('floating_ip_enabled', None) + if not self.strict_mode: + ret['uuid'] = ct_id + if fip_enabled is not None: + ret['floating_ip_enabled'] = fip_enabled + ret['public'] = ret['is_public'] + ret['registry_enabled'] = ret['is_registry_enabled'] + ret['tls_disabled'] = ret['is_tls_disabled'] + + # Optional keys + for (key, default) in ( + ('fixed_network', None), + ('fixed_subnet', None), + ('http_proxy', None), + ('https_proxy', None), + ('labels', {}), + ('master_flavor_id', None), + ('no_proxy', None)): + if key in cluster_template: + ret[key] = cluster_template.pop(key, default) + + for key in ( + 'apiserver_port', + 'cluster_distro', + 'coe', + 'created_at', + 'dns_nameserver', + 'docker_volume_size', + 'external_network_id', + 'flavor_id', + 'image_id', + 'insecure_registry', + 'keypair_id', + 'name', + 'network_driver', + 'server_type', + 'updated_at', + 'volume_driver'): + ret[key] = cluster_template.pop(key) + + ret['properties'] = cluster_template + return ret + + def _normalize_magnum_services(self, magnum_services): + ret = [] + for magnum_service in magnum_services: + ret.append(self._normalize_magnum_service(magnum_service)) + return ret + + def _normalize_magnum_service(self, magnum_service): + """Normalize Magnum magnum_services.""" + import munch + magnum_service = magnum_service.copy() + + # Discard noise + magnum_service.pop('links', None) + magnum_service.pop('human_id', None) + # model_name is a magnumclient-ism + magnum_service.pop('model_name', None) + + ret = munch.Munch(location=self._get_current_location()) + + for key in ( + 'binary', + 'created_at', + 'disabled_reason', + 'host', + 'id', + 'report_count', + 'state', + 'updated_at'): + ret[key] = magnum_service.pop(key) + ret['properties'] = magnum_service + return ret diff --git a/openstack/image/v1/_proxy.py b/openstack/image/v1/_proxy.py index 4aeffa662..b59744ded 100644 --- a/openstack/image/v1/_proxy.py +++ b/openstack/image/v1/_proxy.py @@ -97,7 +97,7 @@ def _upload_image( "Failed deleting image after we failed uploading it.", exc_info=True) raise - return self._connection._normalize_image(image) + return image def _update_image_properties(self, image, meta, properties): properties.update(meta) diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index bf652ecad..96ca6445d 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -971,7 +971,7 @@ def test_update_image_no_patch(self): ret['status'] = 'success' self.cloud.update_image_properties( - image=self._image_dict(ret), + image=image.Image.existing(**ret), **{'owner_specified.openstack.object': 'images/{name}'.format( name=self.image_name)}) diff --git a/openstack/tests/unit/cloud/test_normalize.py b/openstack/tests/unit/cloud/test_normalize.py index fe5c9beeb..a1316384a 100644 --- a/openstack/tests/unit/cloud/test_normalize.py +++ b/openstack/tests/unit/cloud/test_normalize.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.image.v2 import image as image_resource from openstack.tests.unit import base RAW_SERVER_DICT = { @@ -276,204 +275,6 @@ def _assert_server_munch_attributes(testcase, raw, server): class TestNormalize(base.TestCase): - def test_normalize_nova_images(self): - raw_image = RAW_NOVA_IMAGE_DICT.copy() - expected = { - u'auto_disk_config': u'False', - u'com.rackspace__1__build_core': u'1', - u'com.rackspace__1__build_managed': u'1', - u'com.rackspace__1__build_rackconnect': u'1', - u'com.rackspace__1__options': u'0', - u'com.rackspace__1__source': u'import', - u'com.rackspace__1__visible_core': u'1', - u'com.rackspace__1__visible_managed': u'1', - u'com.rackspace__1__visible_rackconnect': u'1', - u'image_type': u'import', - u'org.openstack__1__architecture': u'x64', - u'os_type': u'linux', - u'user_id': u'156284', - u'vm_mode': u'hvm', - u'xenapi_use_agent': u'False', - 'OS-DCF:diskConfig': u'MANUAL', - 'checksum': None, - 'container_format': None, - 'created': u'2015-02-15T22:58:45Z', - 'created_at': '2015-02-15T22:58:45Z', - 'direct_url': None, - 'disk_format': None, - 'file': None, - 'id': u'f2868d7c-63e1-4974-a64d-8670a86df21e', - 'is_protected': False, - 'is_public': False, - 'location': { - 'cloud': '_test_cloud_', - 'project': { - 'domain_id': None, - 'domain_name': 'default', - 'id': '1c36b64c840a42cd9e9b931a369337f0', - 'name': 'admin'}, - 'region_name': u'RegionOne', - 'zone': None}, - 'locations': [], - 'metadata': { - u'auto_disk_config': u'False', - u'com.rackspace__1__build_core': u'1', - u'com.rackspace__1__build_managed': u'1', - u'com.rackspace__1__build_rackconnect': u'1', - u'com.rackspace__1__options': u'0', - u'com.rackspace__1__source': u'import', - u'com.rackspace__1__visible_core': u'1', - u'com.rackspace__1__visible_managed': u'1', - u'com.rackspace__1__visible_rackconnect': u'1', - u'image_type': u'import', - u'org.openstack__1__architecture': u'x64', - u'os_type': u'linux', - u'user_id': u'156284', - u'vm_mode': u'hvm', - u'xenapi_use_agent': u'False', - 'OS-DCF:diskConfig': u'MANUAL', - 'progress': 100}, - 'minDisk': 20, - 'minRam': 0, - 'min_disk': 20, - 'min_ram': 0, - 'name': u'Test Monty Ubuntu', - 'owner': None, - 'progress': 100, - 'properties': { - u'auto_disk_config': u'False', - u'com.rackspace__1__build_core': u'1', - u'com.rackspace__1__build_managed': u'1', - u'com.rackspace__1__build_rackconnect': u'1', - u'com.rackspace__1__options': u'0', - u'com.rackspace__1__source': u'import', - u'com.rackspace__1__visible_core': u'1', - u'com.rackspace__1__visible_managed': u'1', - u'com.rackspace__1__visible_rackconnect': u'1', - u'image_type': u'import', - u'org.openstack__1__architecture': u'x64', - u'os_type': u'linux', - u'user_id': u'156284', - u'vm_mode': u'hvm', - u'xenapi_use_agent': u'False', - 'OS-DCF:diskConfig': u'MANUAL', - 'progress': 100}, - 'protected': False, - 'size': 323004185, - 'status': u'active', - 'tags': [], - 'updated': u'2015-02-15T23:04:34Z', - 'updated_at': u'2015-02-15T23:04:34Z', - 'virtual_size': 0, - 'visibility': 'private'} - retval = self.cloud._normalize_image(raw_image) - self.assertEqual(expected, retval) - - def test_normalize_glance_images(self): - raw_image = RAW_GLANCE_IMAGE_DICT.copy() - expected = { - u'auto_disk_config': u'False', - 'checksum': u'774f48af604ab1ec319093234c5c0019', - u'com.rackspace__1__build_core': u'1', - u'com.rackspace__1__build_managed': u'1', - u'com.rackspace__1__build_rackconnect': u'1', - u'com.rackspace__1__options': u'0', - u'com.rackspace__1__source': u'import', - u'com.rackspace__1__visible_core': u'1', - u'com.rackspace__1__visible_managed': u'1', - u'com.rackspace__1__visible_rackconnect': u'1', - 'container_format': u'ovf', - 'created': u'2015-02-15T22:58:45Z', - 'created_at': u'2015-02-15T22:58:45Z', - 'direct_url': None, - 'disk_format': u'vhd', - 'file': u'/v2/images/f2868d7c-63e1-4974-a64d-8670a86df21e/file', - 'id': u'f2868d7c-63e1-4974-a64d-8670a86df21e', - u'image_type': u'import', - 'is_protected': False, - 'is_public': False, - 'location': { - 'cloud': '_test_cloud_', - 'project': { - 'domain_id': None, - 'domain_name': None, - 'id': u'610275', - 'name': None}, - 'region_name': u'RegionOne', - 'zone': None}, - 'locations': [], - 'metadata': { - u'auto_disk_config': u'False', - u'com.rackspace__1__build_core': u'1', - u'com.rackspace__1__build_managed': u'1', - u'com.rackspace__1__build_rackconnect': u'1', - u'com.rackspace__1__options': u'0', - u'com.rackspace__1__source': u'import', - u'com.rackspace__1__visible_core': u'1', - u'com.rackspace__1__visible_managed': u'1', - u'com.rackspace__1__visible_rackconnect': u'1', - u'image_type': u'import', - u'org.openstack__1__architecture': u'x64', - u'os_hash_algo': u'sha512', - u'os_hash_value': u'fake_hash', - u'os_hidden': False, - u'os_type': u'linux', - u'schema': u'/v2/schemas/image', - u'user_id': u'156284', - u'vm_mode': u'hvm', - u'xenapi_use_agent': u'False'}, - 'minDisk': 20, - 'min_disk': 20, - 'minRam': 0, - 'min_ram': 0, - 'name': u'Test Monty Ubuntu', - u'org.openstack__1__architecture': u'x64', - u'os_hash_algo': u'sha512', - u'os_hash_value': u'fake_hash', - u'os_hidden': False, - u'os_type': u'linux', - 'owner': u'610275', - 'properties': { - u'auto_disk_config': u'False', - u'com.rackspace__1__build_core': u'1', - u'com.rackspace__1__build_managed': u'1', - u'com.rackspace__1__build_rackconnect': u'1', - u'com.rackspace__1__options': u'0', - u'com.rackspace__1__source': u'import', - u'com.rackspace__1__visible_core': u'1', - u'com.rackspace__1__visible_managed': u'1', - u'com.rackspace__1__visible_rackconnect': u'1', - u'image_type': u'import', - u'org.openstack__1__architecture': u'x64', - u'os_hash_algo': u'sha512', - u'os_hash_value': u'fake_hash', - u'os_hidden': False, - u'os_type': u'linux', - u'schema': u'/v2/schemas/image', - u'user_id': u'156284', - u'vm_mode': u'hvm', - u'xenapi_use_agent': u'False'}, - 'protected': False, - u'schema': u'/v2/schemas/image', - 'size': 323004185, - 'status': u'active', - 'tags': [], - 'updated': u'2015-02-15T23:04:34Z', - 'updated_at': u'2015-02-15T23:04:34Z', - u'user_id': u'156284', - 'virtual_size': 0, - 'visibility': u'private', - u'vm_mode': u'hvm', - u'xenapi_use_agent': u'False'} - retval = self.cloud._normalize_image(raw_image) - self.assertEqual(expected, retval) - - # Check normalization from Image resource - image = image_resource.Image.existing(**RAW_GLANCE_IMAGE_DICT) - - retval = self.cloud._normalize_image(image) - self.assertDictEqual(expected, retval) - def test_normalize_coe_cluster_template(self): coe_cluster_template = RAW_COE_CLUSTER_TEMPLATE_DICT.copy() expected = { @@ -588,116 +389,6 @@ def setUp(self): super(TestStrictNormalize, self).setUp() self.assertTrue(self.cloud.strict_mode) - def test_normalize_nova_images(self): - raw_image = RAW_NOVA_IMAGE_DICT.copy() - expected = { - 'checksum': None, - 'container_format': None, - 'created_at': '2015-02-15T22:58:45Z', - 'direct_url': None, - 'disk_format': None, - 'file': None, - 'id': u'f2868d7c-63e1-4974-a64d-8670a86df21e', - 'is_protected': False, - 'is_public': False, - 'location': { - 'cloud': '_test_cloud_', - 'project': { - 'domain_id': None, - 'domain_name': 'default', - 'id': u'1c36b64c840a42cd9e9b931a369337f0', - 'name': 'admin'}, - 'region_name': u'RegionOne', - 'zone': None}, - 'locations': [], - 'min_disk': 20, - 'min_ram': 0, - 'name': u'Test Monty Ubuntu', - 'owner': None, - 'properties': { - u'auto_disk_config': u'False', - u'com.rackspace__1__build_core': u'1', - u'com.rackspace__1__build_managed': u'1', - u'com.rackspace__1__build_rackconnect': u'1', - u'com.rackspace__1__options': u'0', - u'com.rackspace__1__source': u'import', - u'com.rackspace__1__visible_core': u'1', - u'com.rackspace__1__visible_managed': u'1', - u'com.rackspace__1__visible_rackconnect': u'1', - u'image_type': u'import', - u'org.openstack__1__architecture': u'x64', - u'os_type': u'linux', - u'user_id': u'156284', - u'vm_mode': u'hvm', - u'xenapi_use_agent': u'False', - 'OS-DCF:diskConfig': u'MANUAL', - 'progress': 100}, - 'size': 323004185, - 'status': u'active', - 'tags': [], - 'updated_at': u'2015-02-15T23:04:34Z', - 'virtual_size': 0, - 'visibility': 'private'} - retval = self.cloud._normalize_image(raw_image) - self.assertEqual(sorted(expected.keys()), sorted(retval.keys())) - self.assertEqual(expected, retval) - - def test_normalize_glance_images(self): - raw_image = RAW_GLANCE_IMAGE_DICT.copy() - expected = { - 'checksum': u'774f48af604ab1ec319093234c5c0019', - 'container_format': u'ovf', - 'created_at': u'2015-02-15T22:58:45Z', - 'direct_url': None, - 'disk_format': u'vhd', - 'file': u'/v2/images/f2868d7c-63e1-4974-a64d-8670a86df21e/file', - 'id': u'f2868d7c-63e1-4974-a64d-8670a86df21e', - 'is_protected': False, - 'is_public': False, - 'location': { - 'cloud': '_test_cloud_', - 'project': { - 'domain_id': None, - 'domain_name': None, - 'id': u'610275', - 'name': None}, - 'region_name': u'RegionOne', - 'zone': None}, - 'locations': [], - 'min_disk': 20, - 'min_ram': 0, - 'name': u'Test Monty Ubuntu', - 'owner': u'610275', - 'properties': { - u'auto_disk_config': u'False', - u'com.rackspace__1__build_core': u'1', - u'com.rackspace__1__build_managed': u'1', - u'com.rackspace__1__build_rackconnect': u'1', - u'com.rackspace__1__options': u'0', - u'com.rackspace__1__source': u'import', - u'com.rackspace__1__visible_core': u'1', - u'com.rackspace__1__visible_managed': u'1', - u'com.rackspace__1__visible_rackconnect': u'1', - u'image_type': u'import', - u'org.openstack__1__architecture': u'x64', - u'os_type': u'linux', - u'os_hash_algo': u'sha512', - u'os_hash_value': u'fake_hash', - u'os_hidden': False, - u'schema': u'/v2/schemas/image', - u'user_id': u'156284', - u'vm_mode': u'hvm', - u'xenapi_use_agent': u'False'}, - 'size': 323004185, - 'status': u'active', - 'tags': [], - 'updated_at': u'2015-02-15T23:04:34Z', - 'virtual_size': 0, - 'visibility': 'private'} - retval = self.cloud._normalize_image(raw_image) - self.assertEqual(sorted(expected.keys()), sorted(retval.keys())) - self.assertEqual(expected, retval) - def test_normalize_coe_cluster_template(self): coe_cluster_template = RAW_COE_CLUSTER_TEMPLATE_DICT.copy() expected = { From 6a0fd31a0324d79ae0eaff011c3eb2a99a691812 Mon Sep 17 00:00:00 2001 From: "Dr. Jens Harbott" Date: Thu, 5 Jan 2023 12:46:56 +0000 Subject: [PATCH 3177/3836] Revert "Add 'details' parameter to various 'find' proxy methods" This partially reverts commit f9a3cc2f135e5c28fa1e046bb1ca034be36be9aa. Reason for revert: The change to the baremetal functions is not working, this part of the original change is reverted here. Change-Id: Icc70bbdc06b5f32722a93775aee2da4d7b7ca4ae --- openstack/baremetal/v1/_proxy.py | 77 ++++--------------- .../tests/unit/baremetal/v1/test_proxy.py | 18 +---- ...r-find-proxy-methods-947a3280732c448a.yaml | 7 -- 3 files changed, 19 insertions(+), 83 deletions(-) diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index f0fcd1530..1722f5ac8 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -112,7 +112,7 @@ def create_chassis(self, **attrs): """ return self._create(_chassis.Chassis, **attrs) - def find_chassis(self, name_or_id, ignore_missing=True, *, details=True): + def find_chassis(self, name_or_id, ignore_missing=True): """Find a single chassis. :param str name_or_id: The ID of a chassis. @@ -120,19 +120,11 @@ def find_chassis(self, name_or_id, ignore_missing=True, *, details=True): :class:`~openstack.exceptions.ResourceNotFound` will be raised when the chassis does not exist. When set to `True``, None will be returned when attempting to find a nonexistent chassis. - :param details: A boolean indicating whether the detailed information - for the chassis should be returned. - :returns: One :class:`~openstack.baremetal.v1.chassis.Chassis` object or None. """ - list_base_path = '/chassis/detail' if details else None - return self._find( - _chassis.Chassis, - name_or_id, - ignore_missing=ignore_missing, - list_base_path=list_base_path, - ) + return self._find(_chassis.Chassis, name_or_id, + ignore_missing=ignore_missing) def get_chassis(self, chassis, fields=None): """Get a specific chassis. @@ -308,7 +300,7 @@ def create_node(self, **attrs): """ return self._create(_node.Node, **attrs) - def find_node(self, name_or_id, ignore_missing=True, *, details=True): + def find_node(self, name_or_id, ignore_missing=True): """Find a single node. :param str name_or_id: The name or ID of a node. @@ -316,18 +308,11 @@ def find_node(self, name_or_id, ignore_missing=True, *, details=True): :class:`~openstack.exceptions.ResourceNotFound` will be raised when the node does not exist. When set to `True``, None will be returned when attempting to find a nonexistent node. - :param details: A boolean indicating whether the detailed information - for the node should be returned. :returns: One :class:`~openstack.baremetal.v1.node.Node` object or None. """ - list_base_path = '/nodes/detail' if details else None - return self._find( - _node.Node, - name_or_id, - ignore_missing=ignore_missing, - list_base_path=list_base_path, - ) + return self._find(_node.Node, name_or_id, + ignore_missing=ignore_missing) def get_node(self, node, fields=None): """Get a specific node. @@ -700,7 +685,7 @@ def create_port(self, **attrs): """ return self._create(_port.Port, **attrs) - def find_port(self, name_or_id, ignore_missing=True, *, details=True): + def find_port(self, name_or_id, ignore_missing=True): """Find a single port. :param str name_or_id: The ID of a port. @@ -708,18 +693,11 @@ def find_port(self, name_or_id, ignore_missing=True, *, details=True): :class:`~openstack.exceptions.ResourceNotFound` will be raised when the port does not exist. When set to `True``, None will be returned when attempting to find a nonexistent port. - :param details: A boolean indicating whether the detailed information - for every port should be returned. :returns: One :class:`~openstack.baremetal.v1.port.Port` object or None. """ - list_base_path = '/ports/detail' if details else None - return self._find( - _port.Port, - name_or_id, - ignore_missing=ignore_missing, - list_base_path=list_base_path, - ) + return self._find(_port.Port, name_or_id, + ignore_missing=ignore_missing) def get_port(self, port, fields=None): """Get a specific port. @@ -825,13 +803,7 @@ def create_port_group(self, **attrs): """ return self._create(_portgroup.PortGroup, **attrs) - def find_port_group( - self, - name_or_id, - ignore_missing=True, - *, - details=True, - ): + def find_port_group(self, name_or_id, ignore_missing=True): """Find a single port group. :param str name_or_id: The name or ID of a portgroup. @@ -839,18 +811,11 @@ def find_port_group( :class:`~openstack.exceptions.ResourceNotFound` will be raised when the port group does not exist. When set to `True``, None will be returned when attempting to find a nonexistent port group. - :param details: A boolean indicating whether the detailed information - for the port group should be returned. :returns: One :class:`~openstack.baremetal.v1.port_group.PortGroup` object or None. """ - list_base_path = '/portgroups/detail' if details else None - return self._find( - _portgroup.PortGroup, - name_or_id, - ignore_missing=ignore_missing, - list_base_path=list_base_path, - ) + return self._find(_portgroup.PortGroup, name_or_id, + ignore_missing=ignore_missing) def get_port_group(self, port_group, fields=None): """Get a specific port group. @@ -1204,8 +1169,6 @@ def create_volume_connector(self, **attrs): """ return self._create(_volumeconnector.VolumeConnector, **attrs) - # TODO(stephenfin): Delete this. You can't lookup a volume connector by - # name so this is identical to get_volume_connector def find_volume_connector(self, vc_id, ignore_missing=True): """Find a single volume connector. @@ -1220,11 +1183,8 @@ def find_volume_connector(self, vc_id, ignore_missing=True): :class:`~openstack.baremetal.v1.volumeconnector.VolumeConnector` object or None. """ - return self._find( - _volumeconnector.VolumeConnector, - vc_id, - ignore_missing=ignore_missing, - ) + return self._find(_volumeconnector.VolumeConnector, vc_id, + ignore_missing=ignore_missing) def get_volume_connector(self, volume_connector, fields=None): """Get a specific volume_connector. @@ -1349,8 +1309,6 @@ def create_volume_target(self, **attrs): """ return self._create(_volumetarget.VolumeTarget, **attrs) - # TODO(stephenfin): Delete this. You can't lookup a volume target by - # name so this is identical to get_volume_connector def find_volume_target(self, vt_id, ignore_missing=True): """Find a single volume target. @@ -1365,11 +1323,8 @@ def find_volume_target(self, vt_id, ignore_missing=True): :class:`~openstack.baremetal.v1.volumetarget.VolumeTarget` object or None. """ - return self._find( - _volumetarget.VolumeTarget, - vt_id, - ignore_missing=ignore_missing, - ) + return self._find(_volumetarget.VolumeTarget, vt_id, + ignore_missing=ignore_missing) def get_volume_target(self, volume_target, fields=None): """Get a specific volume_target. diff --git a/openstack/tests/unit/baremetal/v1/test_proxy.py b/openstack/tests/unit/baremetal/v1/test_proxy.py index d3bf06a76..19efccc17 100644 --- a/openstack/tests/unit/baremetal/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal/v1/test_proxy.py @@ -60,11 +60,7 @@ def test_create_chassis(self): self.verify_create(self.proxy.create_chassis, chassis.Chassis) def test_find_chassis(self): - self.verify_find( - self.proxy.find_chassis, - chassis.Chassis, - expected_kwargs={'list_base_path': '/chassis/detail'}, - ) + self.verify_find(self.proxy.find_chassis, chassis.Chassis) def test_get_chassis(self): self.verify_get(self.proxy.get_chassis, chassis.Chassis, @@ -98,11 +94,7 @@ def test_create_node(self): self.verify_create(self.proxy.create_node, node.Node) def test_find_node(self): - self.verify_find( - self.proxy.find_node, - node.Node, - expected_kwargs={'list_base_path': '/nodes/detail'}, - ) + self.verify_find(self.proxy.find_node, node.Node) def test_get_node(self): self.verify_get(self.proxy.get_node, node.Node, @@ -148,11 +140,7 @@ def test_create_port(self): self.verify_create(self.proxy.create_port, port.Port) def test_find_port(self): - self.verify_find( - self.proxy.find_port, - port.Port, - expected_kwargs={'list_base_path': '/ports/detail'}, - ) + self.verify_find(self.proxy.find_port, port.Port) def test_get_port(self): self.verify_get(self.proxy.get_port, port.Port, diff --git a/releasenotes/notes/retrieve-detailed-view-for-find-proxy-methods-947a3280732c448a.yaml b/releasenotes/notes/retrieve-detailed-view-for-find-proxy-methods-947a3280732c448a.yaml index b15ba5fb2..347ac2aca 100644 --- a/releasenotes/notes/retrieve-detailed-view-for-find-proxy-methods-947a3280732c448a.yaml +++ b/releasenotes/notes/retrieve-detailed-view-for-find-proxy-methods-947a3280732c448a.yaml @@ -4,13 +4,6 @@ features: The following proxy ``find_*`` operations will now retrieve a detailed resource by default when retrieving by name: - * Bare metal (v1) - - * ``find_chassis`` - * ``find_node`` - * ``find_port`` - * ``find_port_group`` - * Block storage (v2) * ``find_volume`` From d6ccf29f6efd8927b663495291bf6554b2b6f52e Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 18 Nov 2022 17:00:11 +0100 Subject: [PATCH 3178/3836] Drop _normalize class Finally nothing in the SDK is needing _normalize class anymore and we can get rid of it. Change-Id: Ia0d94cd3bd6742cbf90f9e6f5d3c05648819c228 --- openstack/cloud/_normalize.py | 408 --------------- openstack/tests/unit/cloud/test_normalize.py | 490 ------------------- 2 files changed, 898 deletions(-) delete mode 100644 openstack/cloud/_normalize.py delete mode 100644 openstack/tests/unit/cloud/test_normalize.py diff --git a/openstack/cloud/_normalize.py b/openstack/cloud/_normalize.py deleted file mode 100644 index df7b97703..000000000 --- a/openstack/cloud/_normalize.py +++ /dev/null @@ -1,408 +0,0 @@ -# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. -# Copyright (c) 2016 Red Hat, 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. - -# TODO(shade) The normalize functions here should get merged in to -# the sdk resource objects. - -import munch - -from openstack import resource - -_IMAGE_FIELDS = ( - 'checksum', - 'container_format', - 'direct_url', - 'disk_format', - 'file', - 'id', - 'name', - 'owner', - 'virtual_size', -) - -_SERVER_FIELDS = ( - 'accessIPv4', - 'accessIPv6', - 'addresses', - 'adminPass', - 'created', - 'description', - 'key_name', - 'metadata', - 'networks', - 'personality', - 'private_v4', - 'public_v4', - 'public_v6', - 'server_groups', - 'status', - 'updated', - 'user_id', - 'tags', -) - -_pushdown_fields = { - 'project': [ - 'domain_id' - ] -} - - -def _to_bool(value): - if isinstance(value, str): - if not value: - return False - prospective = value.lower().capitalize() - return prospective == 'True' - return bool(value) - - -def _pop_int(resource, key): - return int(resource.pop(key, 0) or 0) - - -def _pop_or_get(resource, key, default, strict): - if strict: - return resource.pop(key, default) - else: - return resource.get(key, default) - - -class Normalizer: - '''Mix-in class to provide the normalization functions. - - This is in a separate class just for on-disk source code organization - reasons. - ''' - - def _remove_novaclient_artifacts(self, item): - # Remove novaclient artifacts - item.pop('links', None) - item.pop('NAME_ATTR', None) - item.pop('HUMAN_ID', None) - item.pop('human_id', None) - item.pop('request_ids', None) - item.pop('x_openstack_request_ids', None) - - def _normalize_image(self, image): - if isinstance(image, resource.Resource): - image = image.to_dict(ignore_none=True, original_names=True) - location = image.pop( - 'location', - self._get_current_location(project_id=image.get('owner'))) - else: - location = self._get_current_location( - project_id=image.get('owner')) - # This copy is to keep things from getting epically weird in tests - image = image.copy() - - new_image = munch.Munch(location=location) - - # Discard noise - self._remove_novaclient_artifacts(image) - - # If someone made a property called "properties" that contains a - # string (this has happened at least one time in the wild), the - # the rest of the normalization here goes belly up. - properties = image.pop('properties', {}) - if not isinstance(properties, dict): - properties = {'properties': properties} - - visibility = image.pop('visibility', None) - protected = _to_bool(image.pop('protected', False)) - - if visibility: - is_public = (visibility == 'public') - else: - is_public = image.pop('is_public', False) - visibility = 'public' if is_public else 'private' - - new_image['size'] = image.pop('OS-EXT-IMG-SIZE:size', 0) - new_image['size'] = image.pop('size', new_image['size']) - - new_image['min_ram'] = image.pop('minRam', 0) - new_image['min_ram'] = image.pop('min_ram', new_image['min_ram']) - - new_image['min_disk'] = image.pop('minDisk', 0) - new_image['min_disk'] = image.pop('min_disk', new_image['min_disk']) - - new_image['created_at'] = image.pop('created', '') - new_image['created_at'] = image.pop( - 'created_at', new_image['created_at']) - - new_image['updated_at'] = image.pop('updated', '') - new_image['updated_at'] = image.pop( - 'updated_at', new_image['updated_at']) - - for field in _IMAGE_FIELDS: - new_image[field] = image.pop(field, None) - - new_image['tags'] = image.pop('tags', []) - new_image['status'] = image.pop('status').lower() - for field in ('min_ram', 'min_disk', 'size', 'virtual_size'): - new_image[field] = _pop_int(new_image, field) - new_image['is_protected'] = protected - new_image['locations'] = image.pop('locations', []) - - metadata = image.pop('metadata', {}) or {} - for key, val in metadata.items(): - properties.setdefault(key, val) - - for key, val in image.items(): - properties.setdefault(key, val) - new_image['properties'] = properties - new_image['is_public'] = is_public - new_image['visibility'] = visibility - - # Backwards compat with glance - if not self.strict_mode: - for key, val in properties.items(): - if key != 'properties': - new_image[key] = val - new_image['protected'] = protected - new_image['metadata'] = properties - new_image['created'] = new_image['created_at'] - new_image['updated'] = new_image['updated_at'] - new_image['minDisk'] = new_image['min_disk'] - new_image['minRam'] = new_image['min_ram'] - return new_image - - def _normalize_compute_usage(self, usage): - """ Normalize a compute usage object """ - - usage = usage.copy() - - # Discard noise - self._remove_novaclient_artifacts(usage) - project_id = usage.pop('tenant_id', None) - - ret = munch.Munch( - location=self._get_current_location(project_id=project_id), - ) - for key in ( - 'max_personality', - 'max_personality_size', - 'max_server_group_members', - 'max_server_groups', - 'max_server_meta', - 'max_total_cores', - 'max_total_instances', - 'max_total_keypairs', - 'max_total_ram_size', - 'total_cores_used', - 'total_hours', - 'total_instances_used', - 'total_local_gb_usage', - 'total_memory_mb_usage', - 'total_ram_used', - 'total_server_groups_used', - 'total_vcpus_usage'): - ret[key] = usage.pop(key, 0) - ret['started_at'] = usage.pop('start', None) - ret['stopped_at'] = usage.pop('stop', None) - ret['server_usages'] = self._normalize_server_usages( - usage.pop('server_usages', [])) - ret['properties'] = usage - return ret - - def _normalize_server_usage(self, server_usage): - """ Normalize a server usage object """ - - server_usage = server_usage.copy() - # TODO(mordred) Right now there is already a location on the usage - # object. Including one here seems verbose. - server_usage.pop('tenant_id') - ret = munch.Munch() - - ret['ended_at'] = server_usage.pop('ended_at', None) - ret['started_at'] = server_usage.pop('started_at', None) - for key in ( - 'flavor', - 'instance_id', - 'name', - 'state'): - ret[key] = server_usage.pop(key, '') - for key in ( - 'hours', - 'local_gb', - 'memory_mb', - 'uptime', - 'vcpus'): - ret[key] = server_usage.pop(key, 0) - ret['properties'] = server_usage - return ret - - def _normalize_server_usages(self, server_usages): - ret = [] - for server_usage in server_usages: - ret.append(self._normalize_server_usage(server_usage)) - return ret - - def _normalize_coe_clusters(self, coe_clusters): - ret = [] - for coe_cluster in coe_clusters: - ret.append(self._normalize_coe_cluster(coe_cluster)) - return ret - - def _normalize_coe_cluster(self, coe_cluster): - """Normalize Magnum COE cluster.""" - coe_cluster = coe_cluster.copy() - - # Discard noise - coe_cluster.pop('links', None) - - c_id = coe_cluster.pop('uuid') - - ret = munch.Munch( - id=c_id, - location=self._get_current_location(), - ) - - if not self.strict_mode: - ret['uuid'] = c_id - - for key in ( - 'status', - 'cluster_template_id', - 'stack_id', - 'keypair', - 'master_count', - 'create_timeout', - 'node_count', - 'name'): - if key in coe_cluster: - ret[key] = coe_cluster.pop(key) - - ret['properties'] = coe_cluster - return ret - - def _normalize_cluster_templates(self, cluster_templates): - ret = [] - for cluster_template in cluster_templates: - ret.append(self._normalize_cluster_template(cluster_template)) - return ret - - def _normalize_cluster_template(self, cluster_template): - """Normalize Magnum cluster_templates.""" - cluster_template = cluster_template.copy() - - # Discard noise - cluster_template.pop('links', None) - cluster_template.pop('human_id', None) - # model_name is a magnumclient-ism - cluster_template.pop('model_name', None) - - ct_id = cluster_template.pop('uuid') - - ret = munch.Munch( - id=ct_id, - location=self._get_current_location(), - ) - ret['is_public'] = cluster_template.pop('public') - ret['is_registry_enabled'] = cluster_template.pop('registry_enabled') - ret['is_tls_disabled'] = cluster_template.pop('tls_disabled') - # pop floating_ip_enabled since we want to hide it in a future patch - fip_enabled = cluster_template.pop('floating_ip_enabled', None) - if not self.strict_mode: - ret['uuid'] = ct_id - if fip_enabled is not None: - ret['floating_ip_enabled'] = fip_enabled - ret['public'] = ret['is_public'] - ret['registry_enabled'] = ret['is_registry_enabled'] - ret['tls_disabled'] = ret['is_tls_disabled'] - - # Optional keys - for (key, default) in ( - ('fixed_network', None), - ('fixed_subnet', None), - ('http_proxy', None), - ('https_proxy', None), - ('labels', {}), - ('master_flavor_id', None), - ('no_proxy', None)): - if key in cluster_template: - ret[key] = cluster_template.pop(key, default) - - for key in ( - 'apiserver_port', - 'cluster_distro', - 'coe', - 'created_at', - 'dns_nameserver', - 'docker_volume_size', - 'external_network_id', - 'flavor_id', - 'image_id', - 'insecure_registry', - 'keypair_id', - 'name', - 'network_driver', - 'server_type', - 'updated_at', - 'volume_driver'): - ret[key] = cluster_template.pop(key) - - ret['properties'] = cluster_template - return ret - - def _normalize_magnum_services(self, magnum_services): - ret = [] - for magnum_service in magnum_services: - ret.append(self._normalize_magnum_service(magnum_service)) - return ret - - def _normalize_magnum_service(self, magnum_service): - """Normalize Magnum magnum_services.""" - magnum_service = magnum_service.copy() - - # Discard noise - magnum_service.pop('links', None) - magnum_service.pop('human_id', None) - # model_name is a magnumclient-ism - magnum_service.pop('model_name', None) - - ret = munch.Munch(location=self._get_current_location()) - - for key in ( - 'binary', - 'created_at', - 'disabled_reason', - 'host', - 'id', - 'report_count', - 'state', - 'updated_at'): - ret[key] = magnum_service.pop(key) - ret['properties'] = magnum_service - return ret - - def _normalize_machines(self, machines): - """Normalize Ironic Machines""" - ret = [] - for machine in machines: - ret.append(self._normalize_machine(machine)) - return ret - - def _normalize_machine(self, machine): - """Normalize Ironic Machine""" - if isinstance(machine, resource.Resource): - machine = machine._to_munch() - else: - machine = machine.copy() - - # Discard noise - self._remove_novaclient_artifacts(machine) - - return machine diff --git a/openstack/tests/unit/cloud/test_normalize.py b/openstack/tests/unit/cloud/test_normalize.py deleted file mode 100644 index a1316384a..000000000 --- a/openstack/tests/unit/cloud/test_normalize.py +++ /dev/null @@ -1,490 +0,0 @@ -# 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.tests.unit import base - -RAW_SERVER_DICT = { - 'HUMAN_ID': True, - 'NAME_ATTR': 'name', - 'OS-DCF:diskConfig': u'MANUAL', - 'OS-EXT-AZ:availability_zone': u'ca-ymq-2', - 'OS-EXT-STS:power_state': 1, - 'OS-EXT-STS:task_state': None, - 'OS-EXT-STS:vm_state': u'active', - 'OS-SRV-USG:launched_at': u'2015-08-01T19:52:02.000000', - 'OS-SRV-USG:terminated_at': None, - 'accessIPv4': u'', - 'accessIPv6': u'', - 'addresses': { - u'public': [{ - u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:9f:46:3e', - u'OS-EXT-IPS:type': u'fixed', - u'addr': u'2604:e100:1:0:f816:3eff:fe9f:463e', - u'version': 6 - }, { - u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:9f:46:3e', - u'OS-EXT-IPS:type': u'fixed', - u'addr': u'162.253.54.192', - u'version': 4}]}, - 'config_drive': u'True', - 'created': u'2015-08-01T19:52:16Z', - 'flavor': { - u'id': u'bbcb7eb5-5c8d-498f-9d7e-307c575d3566', - u'links': [{ - u'href': u'https://compute-ca-ymq-1.vexxhost.net/db9/flavors/bbc', - u'rel': u'bookmark'}]}, - 'hostId': u'bd37', - 'human_id': u'mordred-irc', - 'id': u'811c5197-dba7-4d3a-a3f6-68ca5328b9a7', - 'image': { - u'id': u'69c99b45-cd53-49de-afdc-f24789eb8f83', - u'links': [{ - u'href': u'https://compute-ca-ymq-1.vexxhost.net/db9/images/69c', - u'rel': u'bookmark'}]}, - 'key_name': u'mordred', - 'links': [{ - u'href': u'https://compute-ca-ymq-1.vexxhost.net/v2/db9/servers/811', - u'rel': u'self' - }, { - u'href': u'https://compute-ca-ymq-1.vexxhost.net/db9/servers/811', - u'rel': u'bookmark'}], - 'metadata': {u'group': u'irc', u'groups': u'irc,enabled'}, - 'name': u'mordred-irc', - 'networks': {u'public': [u'2604:e100:1:0:f816:3eff:fe9f:463e', - u'162.253.54.192']}, - 'os-extended-volumes:volumes_attached': [], - 'progress': 0, - 'request_ids': [], - 'security_groups': [{u'name': u'default'}], - 'locked': True, - 'status': u'ACTIVE', - 'tenant_id': u'db92b20496ae4fbda850a689ea9d563f', - 'updated': u'2016-10-15T15:49:29Z', - 'user_id': u'e9b21dc437d149858faee0898fb08e92'} - -RAW_GLANCE_IMAGE_DICT = { - u'auto_disk_config': u'False', - u'checksum': u'774f48af604ab1ec319093234c5c0019', - u'com.rackspace__1__build_core': u'1', - u'com.rackspace__1__build_managed': u'1', - u'com.rackspace__1__build_rackconnect': u'1', - u'com.rackspace__1__options': u'0', - u'com.rackspace__1__source': u'import', - u'com.rackspace__1__visible_core': u'1', - u'com.rackspace__1__visible_managed': u'1', - u'com.rackspace__1__visible_rackconnect': u'1', - u'container_format': u'ovf', - u'created_at': u'2015-02-15T22:58:45Z', - u'disk_format': u'vhd', - u'file': u'/v2/images/f2868d7c-63e1-4974-a64d-8670a86df21e/file', - u'id': u'f2868d7c-63e1-4974-a64d-8670a86df21e', - u'image_type': u'import', - u'min_disk': 20, - u'min_ram': 0, - u'name': u'Test Monty Ubuntu', - u'org.openstack__1__architecture': u'x64', - u'os_type': u'linux', - u'os_hash_algo': u'sha512', - u'os_hash_value': u'fake_hash', - u'os_hidden': False, - u'owner': u'610275', - u'protected': False, - u'schema': u'/v2/schemas/image', - u'size': 323004185, - u'status': u'active', - u'tags': [], - u'updated_at': u'2015-02-15T23:04:34Z', - u'user_id': u'156284', - u'virtual_size': None, - u'visibility': u'private', - u'vm_mode': u'hvm', - u'xenapi_use_agent': u'False'} - -RAW_NOVA_IMAGE_DICT = { - 'HUMAN_ID': True, - 'NAME_ATTR': 'name', - 'OS-DCF:diskConfig': u'MANUAL', - 'OS-EXT-IMG-SIZE:size': 323004185, - 'created': u'2015-02-15T22:58:45Z', - 'human_id': u'test-monty-ubuntu', - 'id': u'f2868d7c-63e1-4974-a64d-8670a86df21e', - 'links': [{ - u'href': u'https://example.com/v2/610275/images/f2868d7c', - u'rel': u'self' - }, { - u'href': u'https://example.com/610275/images/f2868d7c', - u'rel': u'bookmark' - }, { - u'href': u'https://example.com/images/f2868d7c', - u'rel': u'alternate', - u'type': u'application/vnd.openstack.image'}], - 'metadata': { - u'auto_disk_config': u'False', - u'com.rackspace__1__build_core': u'1', - u'com.rackspace__1__build_managed': u'1', - u'com.rackspace__1__build_rackconnect': u'1', - u'com.rackspace__1__options': u'0', - u'com.rackspace__1__source': u'import', - u'com.rackspace__1__visible_core': u'1', - u'com.rackspace__1__visible_managed': u'1', - u'com.rackspace__1__visible_rackconnect': u'1', - u'image_type': u'import', - u'org.openstack__1__architecture': u'x64', - u'os_type': u'linux', - u'user_id': u'156284', - u'vm_mode': u'hvm', - u'xenapi_use_agent': u'False'}, - 'minDisk': 20, - 'minRam': 0, - 'name': u'Test Monty Ubuntu', - 'progress': 100, - 'request_ids': [], - 'status': u'ACTIVE', - 'updated': u'2015-02-15T23:04:34Z'} - -RAW_FLAVOR_DICT = { - 'HUMAN_ID': True, - 'NAME_ATTR': 'name', - 'OS-FLV-EXT-DATA:ephemeral': 80, - 'OS-FLV-WITH-EXT-SPECS:extra_specs': { - u'class': u'performance1', - u'disk_io_index': u'40', - u'number_of_data_disks': u'1', - u'policy_class': u'performance_flavor', - u'resize_policy_class': u'performance_flavor'}, - 'disk': 40, - 'ephemeral': 80, - 'human_id': u'8-gb-performance', - 'id': u'performance1-8', - 'is_public': 'N/A', - 'links': [{ - u'href': u'https://example.com/v2/610275/flavors/performance1-8', - u'rel': u'self' - }, { - u'href': u'https://example.com/610275/flavors/performance1-8', - u'rel': u'bookmark'}], - 'name': u'8 GB Performance', - 'ram': 8192, - 'request_ids': [], - 'rxtx_factor': 1600.0, - 'swap': u'', - 'vcpus': 8} - -RAW_COE_CLUSTER_TEMPLATE_DICT = { - "insecure_registry": "", - "labels": {}, - "updated_at": "", - "floating_ip_enabled": True, - "fixed_subnet": "", - "master_flavor_id": "ds2G", - "uuid": "7d4935d3-2bdc-4fb0-9e6d-ee4ac201d7f6", - "no_proxy": "", - "https_proxy": "", - "tls_disabled": False, - "keypair_id": "", - "public": False, - "http_proxy": "", - "docker_volume_size": "", - "server_type": "vm", - "external_network_id": "67ecffec-ba11-4698-b7a7-9b3cfd81054f", - "cluster_distro": "fedora-atomic", - "image_id": "Fedora-AtomicHost-29-20191126.0.x86_64", - "volume_driver": "cinder", - "registry_enabled": False, - "docker_storage_driver": "overlay2", - "apiserver_port": "", - "name": "k8s-fedora-atomic-flannel", - "created_at": "2020-02-27T17:16:55+00:00", - "network_driver": "flannel", - "fixed_network": "", - "coe": "kubernetes", - "flavor_id": "ds4G", - "master_lb_enabled": True, - "dns_nameserver": "", - "hidden": False -} - -RAW_COE_CLUSTER_DICT = { - "status": "CREATE_COMPLETE", - "health_status": "HEALTHY", - "cluster_template_id": "697e4b1a-33de-47cf-9181-d93bdfbe6aff", - "node_addresses": [ - "172.24.4.58" - ], - "uuid": "028f8287-5c12-4dae-bbf0-7b76b4d3612d", - "stack_id": "ce2e5b48-dfc9-4981-9fc5-36959ff08d12", - "status_reason": None, - "created_at": "2020-03-02T15:29:28+00:00", - "updated_at": "2020-03-02T15:34:58+00:00", - "coe_version": "v1.17.3", - "labels": { - "auto_healing_enabled": "true", - "auto_scaling_enabled": "true", - "autoscaler_tag": "v1.15.2", - "cloud_provider_tag": "v1.17.0", - "etcd_tag": "3.4.3", - "heat_container_agent_tag": "ussuri-dev", - "ingress_controller": "nginx", - "kube_tag": "v1.17.3", - "master_lb_floating_ip_enabled": "true", - "monitoring_enabled": "true", - "tiller_enabled": "true", - "tiller_tag": "v2.16.3", - "use_podman": "true" - }, - "faults": "", - "keypair": "default", - "api_address": "https://172.24.4.164:6443", - "master_addresses": [ - "172.24.4.70" - ], - "create_timeout": None, - "node_count": 1, - "discovery_url": "https://discovery.etcd.io/abc", - "master_count": 1, - "container_version": "1.12.6", - "name": "k8s", - "master_flavor_id": "ds2G", - "flavor_id": "ds4G", - "health_status_reason": { - "api": "ok", - "k8s-l36u5jjz5kvk-master-0.Ready": "True", - "k8s-l36u5jjz5kvk-node-0.Ready": "True", - }, - "project_id": "4e016477e7394decaf2cc158a7d9c75f" -} - - -def _assert_server_munch_attributes(testcase, raw, server): - testcase.assertEqual(server.flavor.id, raw['flavor']['id']) - testcase.assertEqual(server.image.id, raw['image']['id']) - testcase.assertEqual(server.metadata.group, raw['metadata']['group']) - testcase.assertEqual( - server.security_groups[0].name, - raw['security_groups'][0]['name']) - - -class TestNormalize(base.TestCase): - - def test_normalize_coe_cluster_template(self): - coe_cluster_template = RAW_COE_CLUSTER_TEMPLATE_DICT.copy() - expected = { - 'apiserver_port': '', - 'cluster_distro': 'fedora-atomic', - 'coe': 'kubernetes', - 'created_at': '2020-02-27T17:16:55+00:00', - 'dns_nameserver': '', - 'docker_volume_size': '', - 'external_network_id': '67ecffec-ba11-4698-b7a7-9b3cfd81054f', - 'fixed_network': '', - 'fixed_subnet': '', - 'flavor_id': 'ds4G', - 'floating_ip_enabled': True, - 'http_proxy': '', - 'https_proxy': '', - 'id': '7d4935d3-2bdc-4fb0-9e6d-ee4ac201d7f6', - 'image_id': 'Fedora-AtomicHost-29-20191126.0.x86_64', - 'insecure_registry': '', - 'is_public': False, - 'is_registry_enabled': False, - 'is_tls_disabled': False, - 'keypair_id': '', - 'labels': {}, - 'location': {'cloud': '_test_cloud_', - 'project': {'domain_id': None, - 'domain_name': 'default', - 'id': '1c36b64c840a42cd9e9b931a369337f0', - 'name': 'admin'}, - 'region_name': 'RegionOne', - 'zone': None}, - 'master_flavor_id': 'ds2G', - 'name': 'k8s-fedora-atomic-flannel', - 'network_driver': 'flannel', - 'no_proxy': '', - 'properties': {'docker_storage_driver': 'overlay2', - 'hidden': False, - 'master_lb_enabled': True}, - 'public': False, - 'registry_enabled': False, - 'server_type': 'vm', - 'tls_disabled': False, - 'updated_at': '', - 'uuid': '7d4935d3-2bdc-4fb0-9e6d-ee4ac201d7f6', - 'volume_driver': 'cinder', - } - retval = self.cloud._normalize_cluster_template(coe_cluster_template) - self.assertEqual(expected, retval) - - def test_normalize_coe_cluster(self): - coe_cluster = RAW_COE_CLUSTER_DICT.copy() - expected = { - 'cluster_template_id': '697e4b1a-33de-47cf-9181-d93bdfbe6aff', - 'create_timeout': None, - 'id': '028f8287-5c12-4dae-bbf0-7b76b4d3612d', - 'keypair': 'default', - 'location': {'cloud': '_test_cloud_', - 'project': {'domain_id': None, - 'domain_name': 'default', - 'id': '1c36b64c840a42cd9e9b931a369337f0', - 'name': 'admin'}, - 'region_name': 'RegionOne', - 'zone': None}, - 'master_count': 1, - 'name': 'k8s', - 'node_count': 1, - 'properties': {'api_address': 'https://172.24.4.164:6443', - 'coe_version': 'v1.17.3', - 'container_version': '1.12.6', - 'created_at': '2020-03-02T15:29:28+00:00', - 'discovery_url': 'https://discovery.etcd.io/abc', - 'faults': '', - 'flavor_id': 'ds4G', - 'health_status': 'HEALTHY', - 'health_status_reason': { - 'api': 'ok', - 'k8s-l36u5jjz5kvk-master-0.Ready': 'True', - 'k8s-l36u5jjz5kvk-node-0.Ready': 'True'}, - 'labels': { - 'auto_healing_enabled': 'true', - 'auto_scaling_enabled': 'true', - 'autoscaler_tag': 'v1.15.2', - 'cloud_provider_tag': 'v1.17.0', - 'etcd_tag': '3.4.3', - 'heat_container_agent_tag': 'ussuri-dev', - 'ingress_controller': 'nginx', - 'kube_tag': 'v1.17.3', - 'master_lb_floating_ip_enabled': 'true', - 'monitoring_enabled': 'true', - 'tiller_enabled': 'true', - 'tiller_tag': 'v2.16.3', - 'use_podman': 'true'}, - 'master_addresses': ['172.24.4.70'], - 'master_flavor_id': 'ds2G', - 'node_addresses': ['172.24.4.58'], - 'project_id': '4e016477e7394decaf2cc158a7d9c75f', - 'status_reason': None, - 'updated_at': '2020-03-02T15:34:58+00:00'}, - 'stack_id': 'ce2e5b48-dfc9-4981-9fc5-36959ff08d12', - 'status': 'CREATE_COMPLETE', - 'uuid': '028f8287-5c12-4dae-bbf0-7b76b4d3612d', - } - retval = self.cloud._normalize_coe_cluster(coe_cluster) - self.assertEqual(expected, retval) - - -class TestStrictNormalize(base.TestCase): - - strict_cloud = True - - def setUp(self): - super(TestStrictNormalize, self).setUp() - self.assertTrue(self.cloud.strict_mode) - - def test_normalize_coe_cluster_template(self): - coe_cluster_template = RAW_COE_CLUSTER_TEMPLATE_DICT.copy() - expected = { - 'apiserver_port': '', - 'cluster_distro': 'fedora-atomic', - 'coe': 'kubernetes', - 'created_at': '2020-02-27T17:16:55+00:00', - 'dns_nameserver': '', - 'docker_volume_size': '', - 'external_network_id': '67ecffec-ba11-4698-b7a7-9b3cfd81054f', - 'fixed_network': '', - 'fixed_subnet': '', - 'flavor_id': 'ds4G', - 'http_proxy': '', - 'https_proxy': '', - 'id': '7d4935d3-2bdc-4fb0-9e6d-ee4ac201d7f6', - 'image_id': 'Fedora-AtomicHost-29-20191126.0.x86_64', - 'insecure_registry': '', - 'is_public': False, - 'is_registry_enabled': False, - 'is_tls_disabled': False, - 'keypair_id': '', - 'labels': {}, - 'location': {'cloud': '_test_cloud_', - 'project': {'domain_id': None, - 'domain_name': 'default', - 'id': '1c36b64c840a42cd9e9b931a369337f0', - 'name': 'admin'}, - 'region_name': 'RegionOne', - 'zone': None}, - 'master_flavor_id': 'ds2G', - 'name': 'k8s-fedora-atomic-flannel', - 'network_driver': 'flannel', - 'no_proxy': '', - 'properties': {'docker_storage_driver': 'overlay2', - 'hidden': False, - 'master_lb_enabled': True}, - 'server_type': 'vm', - 'updated_at': '', - 'volume_driver': 'cinder', - } - - retval = self.cloud._normalize_cluster_template(coe_cluster_template) - self.assertEqual(expected, retval) - - def test_normalize_coe_cluster(self): - coe_cluster = RAW_COE_CLUSTER_DICT.copy() - expected = { - 'cluster_template_id': '697e4b1a-33de-47cf-9181-d93bdfbe6aff', - 'create_timeout': None, - 'id': '028f8287-5c12-4dae-bbf0-7b76b4d3612d', - 'keypair': 'default', - 'location': {'cloud': '_test_cloud_', - 'project': {'domain_id': None, - 'domain_name': 'default', - 'id': '1c36b64c840a42cd9e9b931a369337f0', - 'name': 'admin'}, - 'region_name': 'RegionOne', - 'zone': None}, - 'master_count': 1, - 'name': 'k8s', - 'node_count': 1, - 'properties': {'api_address': 'https://172.24.4.164:6443', - 'coe_version': 'v1.17.3', - 'container_version': '1.12.6', - 'created_at': '2020-03-02T15:29:28+00:00', - 'discovery_url': 'https://discovery.etcd.io/abc', - 'faults': '', - 'flavor_id': 'ds4G', - 'health_status': 'HEALTHY', - 'health_status_reason': { - 'api': 'ok', - 'k8s-l36u5jjz5kvk-master-0.Ready': 'True', - 'k8s-l36u5jjz5kvk-node-0.Ready': 'True'}, - 'labels': { - 'auto_healing_enabled': 'true', - 'auto_scaling_enabled': 'true', - 'autoscaler_tag': 'v1.15.2', - 'cloud_provider_tag': 'v1.17.0', - 'etcd_tag': '3.4.3', - 'heat_container_agent_tag': 'ussuri-dev', - 'ingress_controller': 'nginx', - 'kube_tag': 'v1.17.3', - 'master_lb_floating_ip_enabled': 'true', - 'monitoring_enabled': 'true', - 'tiller_enabled': 'true', - 'tiller_tag': 'v2.16.3', - 'use_podman': 'true'}, - 'master_addresses': ['172.24.4.70'], - 'master_flavor_id': 'ds2G', - 'node_addresses': ['172.24.4.58'], - 'project_id': '4e016477e7394decaf2cc158a7d9c75f', - 'status_reason': None, - 'updated_at': '2020-03-02T15:34:58+00:00'}, - 'stack_id': 'ce2e5b48-dfc9-4981-9fc5-36959ff08d12', - 'status': 'CREATE_COMPLETE', - } - retval = self.cloud._normalize_coe_cluster(coe_cluster) - self.assertEqual(expected, retval) From e8d351ddd8686e359261f3c05e72caeaa6976cdd Mon Sep 17 00:00:00 2001 From: Tom Weininger Date: Fri, 16 Dec 2022 10:59:59 +0100 Subject: [PATCH 3179/3836] Add typing information and documentation Tools like mypy and IDEs like PyCharm use this kind of information in order to do type checking and to provide better auto-completion, which can be an enormous help for developers. Type hints are supported since Python 3.5. [1] Variable annotations are supported since Python 3.6. [2] [1]: https://peps.python.org/pep-0484/ [2]: https://peps.python.org/pep-0526/ Change-Id: I00834257335baa070e81ee4418d9314065db289c --- openstack/cloud/_accelerator.py | 2 + openstack/cloud/_baremetal.py | 2 + openstack/cloud/_block_storage.py | 2 + openstack/cloud/_compute.py | 2 + openstack/cloud/_dns.py | 2 + openstack/cloud/_floating_ip.py | 2 + openstack/cloud/_identity.py | 2 + openstack/cloud/_image.py | 2 + openstack/cloud/_network.py | 2 + openstack/cloud/_object_store.py | 3 +- openstack/cloud/_orchestration.py | 2 + openstack/cloud/_security_group.py | 2 + openstack/cloud/_shared_file_system.py | 2 + openstack/network/v2/_proxy.py | 167 +++++++++++++------------ openstack/network/v2/port.py | 8 +- openstack/network/v2/router.py | 4 +- openstack/proxy.py | 57 +++++---- 17 files changed, 156 insertions(+), 107 deletions(-) diff --git a/openstack/cloud/_accelerator.py b/openstack/cloud/_accelerator.py index 8eb9769cc..b4705815e 100644 --- a/openstack/cloud/_accelerator.py +++ b/openstack/cloud/_accelerator.py @@ -13,9 +13,11 @@ # import types so that we can reference ListType in sphinx param declarations. # We can't just use list, because sphinx gets confused by # openstack.resource.Resource.list and openstack.resource2.Resource.list +from openstack.accelerator.v2._proxy import Proxy class AcceleratorCloudMixin: + accelerator: Proxy def list_deployables(self, filters=None): """List all available deployables. diff --git a/openstack/cloud/_baremetal.py b/openstack/cloud/_baremetal.py index 0041e0990..3226ff519 100644 --- a/openstack/cloud/_baremetal.py +++ b/openstack/cloud/_baremetal.py @@ -21,6 +21,7 @@ import jsonpatch +from openstack.baremetal.v1._proxy import Proxy from openstack.cloud import exc @@ -44,6 +45,7 @@ def _normalize_port_list(nics): class BaremetalCloudMixin: + baremetal: Proxy def list_nics(self): """Return a list of all bare metal ports.""" diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index 520ac8ca3..1ab4d3fac 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -16,6 +16,7 @@ import types # noqa import warnings +from openstack.block_storage.v3._proxy import Proxy from openstack.block_storage.v3 import quota_set as _qs from openstack.cloud import _utils from openstack.cloud import exc @@ -32,6 +33,7 @@ def _no_pending_volumes(volumes): class BlockStorageCloudMixin: + block_storage: Proxy # TODO(stephenfin): Remove 'cache' in a future major version @_utils.cache_on_arguments(should_cache_fn=_no_pending_volumes) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index d621fd29c..179153561 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -24,6 +24,7 @@ from openstack.cloud import _utils from openstack.cloud import exc from openstack.cloud import meta +from openstack.compute.v2._proxy import Proxy from openstack.compute.v2 import quota_set as _qs from openstack.compute.v2 import server as _server from openstack import exceptions @@ -73,6 +74,7 @@ def _pop_or_get(resource, key, default, strict): class ComputeCloudMixin: + compute: Proxy def __init__(self): self._servers = None diff --git a/openstack/cloud/_dns.py b/openstack/cloud/_dns.py index e767bc79d..8efd07d6c 100644 --- a/openstack/cloud/_dns.py +++ b/openstack/cloud/_dns.py @@ -17,11 +17,13 @@ from openstack.cloud import _utils from openstack.cloud import exc +from openstack.dns.v2._proxy import Proxy from openstack import exceptions from openstack import resource class DnsCloudMixin: + dns: Proxy def list_zones(self, filters=None): """List all available zones. diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index 7123a261f..b8ec688a8 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -24,6 +24,7 @@ from openstack.cloud import exc from openstack.cloud import meta from openstack import exceptions +from openstack.network.v2._proxy import Proxy from openstack import proxy from openstack import utils @@ -33,6 +34,7 @@ class FloatingIPCloudMixin: + network: Proxy def __init__(self): self.private = self.config.config.get('private', False) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index bd4ead502..5c8952797 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -18,9 +18,11 @@ from openstack.cloud import _utils from openstack.cloud import exc from openstack import exceptions +from openstack.identity.v3._proxy import Proxy class IdentityCloudMixin: + identity: Proxy @property def _identity_client(self): diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py index 2ea8d92e7..68174c995 100644 --- a/openstack/cloud/_image.py +++ b/openstack/cloud/_image.py @@ -17,6 +17,7 @@ from openstack.cloud import _utils from openstack.cloud import exc +from openstack.image.v2._proxy import Proxy from openstack import utils @@ -29,6 +30,7 @@ def _no_pending_images(images): class ImageCloudMixin: + image: Proxy def __init__(self): self.image_api_use_tasks = self.config.config['image_api_use_tasks'] diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index 22e4b7dfe..c122462fb 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -21,9 +21,11 @@ from openstack.cloud import _utils from openstack.cloud import exc from openstack import exceptions +from openstack.network.v2._proxy import Proxy class NetworkCloudMixin: + network: Proxy def __init__(self): self._ports = None diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index 6f3783f6c..d4a392af1 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -22,7 +22,7 @@ from openstack.cloud import _utils from openstack.cloud import exc from openstack import exceptions - +from openstack.object_store.v1._proxy import Proxy DEFAULT_OBJECT_SEGMENT_SIZE = 1073741824 # 1GB # This halves the current default for Swift @@ -36,6 +36,7 @@ class ObjectStoreCloudMixin: + object_store: Proxy @property def _object_store_client(self): diff --git a/openstack/cloud/_orchestration.py b/openstack/cloud/_orchestration.py index 6b7bc8407..984c9a3cd 100644 --- a/openstack/cloud/_orchestration.py +++ b/openstack/cloud/_orchestration.py @@ -18,6 +18,7 @@ from openstack.cloud import _utils from openstack.cloud import exc from openstack.orchestration.util import event_utils +from openstack.orchestration.v1._proxy import Proxy def _no_pending_stacks(stacks): @@ -30,6 +31,7 @@ def _no_pending_stacks(stacks): class OrchestrationCloudMixin: + orchestration: Proxy @property def _orchestration_client(self): diff --git a/openstack/cloud/_security_group.py b/openstack/cloud/_security_group.py index 4637773c7..258335dcd 100644 --- a/openstack/cloud/_security_group.py +++ b/openstack/cloud/_security_group.py @@ -19,10 +19,12 @@ from openstack.cloud import _utils from openstack.cloud import exc from openstack import exceptions +from openstack.network.v2._proxy import Proxy from openstack import proxy class SecurityGroupCloudMixin: + network: Proxy def __init__(self): self.secgroup_source = self.config.config['secgroup_source'] diff --git a/openstack/cloud/_shared_file_system.py b/openstack/cloud/_shared_file_system.py index 9063e0794..87e907371 100644 --- a/openstack/cloud/_shared_file_system.py +++ b/openstack/cloud/_shared_file_system.py @@ -9,9 +9,11 @@ # 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.shared_file_system.v2._proxy import Proxy class SharedFileSystemCloudMixin: + share: Proxy def list_share_availability_zones(self): """List all availability zones for the Shared File Systems service. diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 3e4f328ee..b5012d39f 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -9,6 +9,10 @@ # 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 typing import Generic +from typing import Optional +from typing import Type +from typing import TypeVar from openstack import exceptions from openstack.network.v2 import address_group as _address_group @@ -69,8 +73,10 @@ from openstack.network.v2 import vpn_service as _vpn_service from openstack import proxy +T = TypeVar('T') -class Proxy(proxy.Proxy): + +class Proxy(proxy.Proxy, Generic[T]): _resource_registry = { "address_group": _address_group.AddressGroup, "address_scope": _address_scope.AddressScope, @@ -130,14 +136,14 @@ class Proxy(proxy.Proxy): } @proxy._check_resource(strict=False) - def _update(self, resource_type, value, base_path=None, - if_revision=None, **attrs): + def _update(self, resource_type: Type[T], value, base_path=None, + if_revision=None, **attrs) -> T: res = self._get_resource(resource_type, value, **attrs) return res.commit(self, base_path=base_path, if_revision=if_revision) @proxy._check_resource(strict=False) - def _delete(self, resource_type, value, ignore_missing=True, - if_revision=None, **attrs): + def _delete(self, resource_type: Type[T], value, ignore_missing=True, + if_revision=None, **attrs) -> Optional[T]: res = self._get_resource(resource_type, value, **attrs) try: @@ -152,7 +158,7 @@ def _delete(self, resource_type, value, ignore_missing=True, def create_address_group(self, **attrs): """Create a new address group from attributes - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.address_group.AddressGroup`, comprised of the properties on the AddressGroup class. @@ -223,12 +229,13 @@ def address_groups(self, **query): """ return self._list(_address_group.AddressGroup, **query) - def update_address_group(self, address_group, **attrs): + def update_address_group(self, address_group, + **attrs) -> _address_group.AddressGroup: """Update an address group :param address_group: Either the ID of an address group or a :class:`~openstack.network.v2.address_group.AddressGroup` instance. - :param dict attrs: The attributes to update on the address group + :param attrs: The attributes to update on the address group represented by ``value``. :returns: The updated address group @@ -264,7 +271,7 @@ def remove_addresses_from_address_group(self, address_group, addresses): def create_address_scope(self, **attrs): """Create a new address scope from attributes - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.address_scope.AddressScope`, comprised of the properties on the AddressScope class. @@ -341,7 +348,7 @@ def update_address_scope(self, address_scope, **attrs): :param address_scope: Either the ID of an address scope or a :class:`~openstack.network.v2.address_scope.AddressScope` instance. - :param dict attrs: The attributes to update on the address scope + :param attrs: The attributes to update on the address scope represented by ``value``. :returns: The updated address scope @@ -404,7 +411,7 @@ def update_agent(self, agent, **attrs): :param agent: The value can be the ID of a agent or a :class:`~openstack.network.v2.agent.Agent` instance. - :param dict attrs: The attributes to update on the agent represented + :param attrs: The attributes to update on the agent represented by ``value``. :returns: One :class:`~openstack.network.v2.agent.Agent` @@ -564,7 +571,7 @@ def extensions(self, **query): def create_flavor(self, **attrs): """Create a new network service flavor from attributes - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.flavor.Flavor`, comprised of the properties on the Flavor class. @@ -688,7 +695,7 @@ def disassociate_flavor_from_service_profile( def create_local_ip(self, **attrs): """Create a new local ip from attributes - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.local_ip.LocalIP`, comprised of the properties on the LocalIP class. @@ -773,7 +780,7 @@ def update_local_ip(self, local_ip, if_revision=None, **attrs): instance. :param int if_revision: Revision to put in If-Match header of update request to perform compare-and-swap update. - :param dict attrs: The attributes to update on the ip represented + :param attrs: The attributes to update on the ip represented by ``value``. :returns: The updated ip @@ -788,7 +795,7 @@ def create_local_ip_association(self, local_ip, **attrs): :param local_ip: The value can be the ID of a Local IP or a :class:`~openstack.network.v2.local_ip.LocalIP` instance. - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.local_ip_association.LocalIPAssociation`, comprised of the properties on the LocalIP class. @@ -897,7 +904,7 @@ def local_ip_associations(self, local_ip, **query): def create_ip(self, **attrs): """Create a new floating ip from attributes - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.floating_ip.FloatingIP`, comprised of the properties on the FloatingIP class. @@ -996,7 +1003,7 @@ def update_ip(self, floating_ip, if_revision=None, **attrs): instance. :param int if_revision: Revision to put in If-Match header of update request to perform compare-and-swap update. - :param dict attrs: The attributes to update on the ip represented + :param attrs: The attributes to update on the ip represented by ``value``. :returns: The updated ip @@ -1008,7 +1015,7 @@ def update_ip(self, floating_ip, if_revision=None, **attrs): def create_port_forwarding(self, **attrs): """Create a new floating ip port forwarding from attributes - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.port_forwarding.PortForwarding`, comprised of the properties on the PortForwarding class. @@ -1112,7 +1119,7 @@ def update_port_forwarding(self, port_forwarding, floating_ip, **attrs): :param floating_ip: The value can be the ID of a Floating IP or a :class:`~openstack.network.v2.floating_ip.FloatingIP` instance. - :param dict attrs: The attributes to update on the ip represented + :param attrs: The attributes to update on the ip represented by ``value``. :returns: The updated port_forwarding @@ -1125,7 +1132,7 @@ def update_port_forwarding(self, port_forwarding, floating_ip, **attrs): def create_health_monitor(self, **attrs): """Create a new health monitor from attributes - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.health_monitor.HealthMonitor`, comprised of the properties on the HealthMonitor class. @@ -1218,7 +1225,7 @@ def update_health_monitor(self, health_monitor, **attrs): :param health_monitor: Either the id of a health monitor or a :class:`~openstack.network.v2.health_monitor.HealthMonitor` instance. - :param dict attrs: The attributes to update on the health monitor + :param attrs: The attributes to update on the health monitor represented by ``value``. :returns: The updated health monitor @@ -1230,7 +1237,7 @@ def update_health_monitor(self, health_monitor, **attrs): def create_listener(self, **attrs): """Create a new listener from attributes - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.listener.Listener`, comprised of the properties on the Listener class. @@ -1313,7 +1320,7 @@ def update_listener(self, listener, **attrs): :param listener: Either the id of a listener or a :class:`~openstack.network.v2.listener.Listener` instance. - :param dict attrs: The attributes to update on the listener + :param attrs: The attributes to update on the listener represented by ``listener``. :returns: The updated listener @@ -1324,7 +1331,7 @@ def update_listener(self, listener, **attrs): def create_load_balancer(self, **attrs): """Create a new load balancer from attributes - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.load_balancer.LoadBalancer`, comprised of the properties on the LoadBalancer class. @@ -1397,7 +1404,7 @@ def update_load_balancer(self, load_balancer, **attrs): :param load_balancer: Either the id of a load balancer or a :class:`~openstack.network.v2.load_balancer.LoadBalancer` instance. - :param dict attrs: The attributes to update on the load balancer + :param attrs: The attributes to update on the load balancer represented by ``load_balancer``. :returns: The updated load balancer @@ -1409,7 +1416,7 @@ def update_load_balancer(self, load_balancer, **attrs): def create_metering_label(self, **attrs): """Create a new metering label from attributes - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.metering_label.MeteringLabel`, comprised of the properties on the MeteringLabel class. @@ -1492,7 +1499,7 @@ def update_metering_label(self, metering_label, **attrs): :param metering_label: Either the id of a metering label or a :class:`~openstack.network.v2.metering_label.MeteringLabel` instance. - :param dict attrs: The attributes to update on the metering label + :param attrs: The attributes to update on the metering label represented by ``metering_label``. :returns: The updated metering label @@ -1504,7 +1511,7 @@ def update_metering_label(self, metering_label, **attrs): def create_metering_label_rule(self, **attrs): """Create a new metering label rule from attributes - :param dict attrs: Keyword arguments which will be used to create a + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.metering_label_rule.MeteringLabelRule`, comprised of the properties on the MeteringLabelRule class. @@ -1597,7 +1604,7 @@ def update_metering_label_rule(self, metering_label_rule, **attrs): Either the id of a metering label rule or a :class:`~openstack.network.v2.metering_label_rule.MeteringLabelRule` instance. - :param dict attrs: The attributes to update on the metering label rule + :param attrs: The attributes to update on the metering label rule represented by ``metering_label_rule``. :returns: The updated metering label rule @@ -1610,7 +1617,7 @@ def update_metering_label_rule(self, metering_label_rule, **attrs): def create_network(self, **attrs): """Create a new network from attributes - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.network.Network`, comprised of the properties on the Network class. @@ -1702,7 +1709,7 @@ def update_network(self, network, if_revision=None, **attrs): :class:`~openstack.network.v2.network.Network`. :param int if_revision: Revision to put in If-Match header of update request to perform compare-and-swap update. - :param dict attrs: The attributes to update on the network represented + :param attrs: The attributes to update on the network represented by ``network``. :returns: The updated network @@ -1768,7 +1775,7 @@ def network_ip_availabilities(self, **query): def create_network_segment_range(self, **attrs): """Create a new network segment range from attributes - :param dict attrs: Keyword arguments which will be used to create a + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.network_segment_range.NetworkSegmentRange`, comprised of the properties on the NetworkSegmentRange class. @@ -1885,7 +1892,7 @@ def update_network_segment_range(self, network_segment_range, **attrs): def create_pool(self, **attrs): """Create a new pool from attributes - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.pool.Pool`, comprised of the properties on the Pool class. @@ -1968,7 +1975,7 @@ def update_pool(self, pool, **attrs): :param pool: Either the id of a pool or a :class:`~openstack.network.v2.pool.Pool` instance. - :param dict attrs: The attributes to update on the pool represented + :param attrs: The attributes to update on the pool represented by ``pool``. :returns: The updated pool @@ -1982,7 +1989,7 @@ def create_pool_member(self, pool, **attrs): :param pool: The pool can be either the ID of a pool or a :class:`~openstack.network.v2.pool.Pool` instance that the member will be created in. - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.pool_member.PoolMember`, comprised of the properties on the PoolMember class. @@ -2090,7 +2097,7 @@ def update_pool_member(self, pool_member, pool, **attrs): :param pool: The pool can be either the ID of a pool or a :class:`~openstack.network.v2.pool.Pool` instance that the member belongs to. - :param dict attrs: The attributes to update on the pool member + :param attrs: The attributes to update on the pool member represented by ``pool_member``. :returns: The updated pool member @@ -2103,7 +2110,7 @@ def update_pool_member(self, pool_member, pool, **attrs): def create_port(self, **attrs): """Create a new port from attributes - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.port.Port`, comprised of the properties on the Port class. @@ -2194,14 +2201,14 @@ def ports(self, **query): """ return self._list(_port.Port, **query) - def update_port(self, port, if_revision=None, **attrs): + def update_port(self, port, if_revision=None, **attrs) -> _port.Port: """Update a port :param port: Either the id of a port or a :class:`~openstack.network.v2.port.Port` instance. :param int if_revision: Revision to put in If-Match header of update request to perform compare-and-swap update. - :param dict attrs: The attributes to update on the port represented + :param attrs: The attributes to update on the port represented by ``port``. :returns: The updated port @@ -2230,7 +2237,7 @@ def get_subnet_ports(self, subnet_id): def create_qos_bandwidth_limit_rule(self, qos_policy, **attrs): """Create a new bandwidth limit rule - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.qos_bandwidth_limit_rule.QoSBandwidthLimitRule`, comprised of the properties on the @@ -2355,7 +2362,7 @@ def update_qos_bandwidth_limit_rule( def create_qos_dscp_marking_rule(self, qos_policy, **attrs): """Create a new QoS DSCP marking rule - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.qos_dscp_marking_rule.QoSDSCPMarkingRule`, comprised of the properties on the @@ -2478,7 +2485,7 @@ def update_qos_dscp_marking_rule(self, qos_rule, qos_policy, **attrs): def create_qos_minimum_bandwidth_rule(self, qos_policy, **attrs): """Create a new minimum bandwidth rule - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule`, comprised of the properties on the @@ -2607,7 +2614,7 @@ def update_qos_minimum_bandwidth_rule(self, qos_rule, qos_policy, def create_qos_minimum_packet_rate_rule(self, qos_policy, **attrs): """Create a new minimum packet rate rule - :param dict attrs: Keyword arguments which will be used to create a + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule`, comprised of the properties on the QoSMinimumPacketRateRule class. :param qos_policy: The value can be the ID of the QoS policy that the @@ -2736,7 +2743,7 @@ def update_qos_minimum_packet_rate_rule(self, qos_rule, qos_policy, def create_qos_policy(self, **attrs): """Create a new QoS policy from attributes - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.qos_policy.QoSPolicy`, comprised of the properties on the QoSPolicy class. @@ -2938,7 +2945,7 @@ def update_quota(self, quota, **attrs): :class:`~openstack.network.v2.quota.Quota` instance. The ID of a quota is the same as the project ID for the quota. - :param dict attrs: The attributes to update on the quota represented + :param attrs: The attributes to update on the quota represented by ``quota``. :returns: The updated quota @@ -2949,7 +2956,7 @@ def update_quota(self, quota, **attrs): def create_rbac_policy(self, **attrs): """Create a new RBAC policy from attributes - :param dict attrs: Keyword arguments which will be used to create a + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.rbac_policy.RBACPolicy`, comprised of the properties on the RBACPolicy class. @@ -3026,7 +3033,7 @@ def update_rbac_policy(self, rbac_policy, **attrs): :param rbac_policy: Either the id of a RBAC policy or a :class:`~openstack.network.v2.rbac_policy.RBACPolicy` instance. - :param dict attrs: The attributes to update on the RBAC policy + :param attrs: The attributes to update on the RBAC policy represented by ``rbac_policy``. :returns: The updated RBAC policy @@ -3037,7 +3044,7 @@ def update_rbac_policy(self, rbac_policy, **attrs): def create_router(self, **attrs): """Create a new router from attributes - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.router.Router`, comprised of the properties on the Router class. @@ -3120,7 +3127,7 @@ def update_router(self, router, if_revision=None, **attrs): :class:`~openstack.network.v2.router.Router` instance. :param int if_revision: Revision to put in If-Match header of update request to perform compare-and-swap update. - :param dict attrs: The attributes to update on the router represented + :param attrs: The attributes to update on the router represented by ``router``. :returns: The updated router @@ -3272,7 +3279,7 @@ def remove_router_from_agent(self, agent, router): def create_ndp_proxy(self, **attrs): """Create a new ndp proxy from attributes - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.ndp_proxy.NDPProxxy`, comprised of the properties on the NDPProxy class. @@ -3351,7 +3358,7 @@ def update_ndp_proxy(self, ndp_proxy, **attrs): :param ndp_proxy: The value can be the ID of a ndp proxy or a :class:`~openstack.network.v2.ndp_proxy.NDPProxy` instance. - :param dict attrs: The attributes to update on the ip represented + :param attrs: The attributes to update on the ip represented by ``value``. :returns: The updated ndp_proxy @@ -3362,7 +3369,7 @@ def update_ndp_proxy(self, ndp_proxy, **attrs): def create_firewall_group(self, **attrs): """Create a new firewall group from attributes - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.firewall_group.FirewallGroup`, comprised of the properties on the FirewallGroup class. @@ -3450,7 +3457,7 @@ def update_firewall_group(self, firewall_group, **attrs): :param firewall_group: Either the id of a firewall group or a :class:`~openstack.network.v2.firewall_group.FirewallGroup` instance. - :param dict attrs: The attributes to update on the firewall group + :param attrs: The attributes to update on the firewall group represented by ``firewall_group``. :returns: The updated firewall group @@ -3462,7 +3469,7 @@ def update_firewall_group(self, firewall_group, **attrs): def create_firewall_policy(self, **attrs): """Create a new firewall policy from attributes - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.firewall_policy.FirewallPolicy`, comprised of the properties on the FirewallPolicy class. @@ -3546,7 +3553,7 @@ def update_firewall_policy(self, firewall_policy, **attrs): :param firewall_policy: Either the id of a firewall policy or a :class:`~openstack.network.v2.firewall_policy.FirewallPolicy` instance. - :param dict attrs: The attributes to update on the firewall policy + :param attrs: The attributes to update on the firewall policy represented by ``firewall_policy``. :returns: The updated firewall policy @@ -3594,7 +3601,7 @@ def remove_rule_from_policy(self, firewall_policy_id, firewall_rule_id): def create_firewall_rule(self, **attrs): """Create a new firewall rule from attributes - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.firewall_rule.FirewallRule`, comprised of the properties on the FirewallRule class. @@ -3689,7 +3696,7 @@ def update_firewall_rule(self, firewall_rule, **attrs): :param firewall_rule: Either the id of a firewall rule or a :class:`~openstack.network.v2.firewall_rule.FirewallRule` instance. - :param dict attrs: The attributes to update on the firewall rule + :param attrs: The attributes to update on the firewall rule represented by ``firewall_rule``. :returns: The updated firewall rule @@ -3701,7 +3708,7 @@ def update_firewall_rule(self, firewall_rule, **attrs): def create_security_group(self, **attrs): """Create a new security group from attributes - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.security_group.SecurityGroup`, comprised of the properties on the SecurityGroup class. @@ -3788,7 +3795,7 @@ def update_security_group(self, security_group, if_revision=None, **attrs): instance. :param int if_revision: Revision to put in If-Match header of update request to perform compare-and-swap update. - :param dict attrs: The attributes to update on the security group + :param attrs: The attributes to update on the security group represented by ``security_group``. :returns: The updated security group @@ -3800,7 +3807,7 @@ def update_security_group(self, security_group, if_revision=None, **attrs): def create_security_group_rule(self, **attrs): """Create a new security group rule from attributes - :param dict attrs: Keyword arguments which will be used to create a + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.security_group_rule.SecurityGroupRule`, comprised of the properties on the SecurityGroupRule class. @@ -3909,7 +3916,7 @@ def security_group_rules(self, **query): def create_segment(self, **attrs): """Create a new segment from attributes - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.segment.Segment`, comprised of the properties on the Segment class. @@ -4009,7 +4016,7 @@ def service_providers(self, **query): def create_service_profile(self, **attrs): """Create a new network service flavor profile from attributes - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.service_profile.ServiceProfile`, comprised of the properties on the ServiceProfile class. @@ -4103,7 +4110,7 @@ def update_service_profile(self, service_profile, **attrs): def create_subnet(self, **attrs): """Create a new subnet from attributes - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.subnet.Subnet`, comprised of the properties on the Subnet class. @@ -4189,7 +4196,7 @@ def update_subnet(self, subnet, if_revision=None, **attrs): :class:`~openstack.network.v2.subnet.Subnet` instance. :param int if_revision: Revision to put in If-Match header of update request to perform compare-and-swap update. - :param dict attrs: The attributes to update on the subnet represented + :param attrs: The attributes to update on the subnet represented by ``subnet``. :returns: The updated subnet @@ -4201,7 +4208,7 @@ def update_subnet(self, subnet, if_revision=None, **attrs): def create_subnet_pool(self, **attrs): """Create a new subnet pool from attributes - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.subnet_pool.SubnetPool`, comprised of the properties on the SubnetPool class. @@ -4279,7 +4286,7 @@ def update_subnet_pool(self, subnet_pool, **attrs): :param subnet_pool: Either the ID of a subnet pool or a :class:`~openstack.network.v2.subnet_pool.SubnetPool` instance. - :param dict attrs: The attributes to update on the subnet pool + :param attrs: The attributes to update on the subnet pool represented by ``subnet_pool``. :returns: The updated subnet pool @@ -4314,7 +4321,7 @@ def set_tags(self, resource, tags): def create_trunk(self, **attrs): """Create a new trunk from attributes - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.trunk.Trunk`, comprised of the properties on the Trunk class. @@ -4379,7 +4386,7 @@ def update_trunk(self, trunk, **attrs): :param trunk: Either the id of a trunk or a :class:`~openstack.network.v2.trunk.Trunk` instance. - :param dict attrs: The attributes to update on the trunk + :param attrs: The attributes to update on the trunk represented by ``trunk``. :returns: The updated trunk @@ -4433,7 +4440,7 @@ def get_trunk_subports(self, trunk): def create_vpn_endpoint_group(self, **attrs): """Create a new vpn endpoint group from attributes - :param dict attrs: Keyword arguments which will be used to create a + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.vpn_endpoint_group.VpnEndpointGroup`, comprised of the properties on the VpnEndpointGroup class. @@ -4520,7 +4527,7 @@ def update_vpn_endpoint_group(self, vpn_endpoint_group, **attrs): :param vpn_endpoint_group: Either the id of a vpn service or a :class:`~openstack.network.v2.vpn_endpoint_group.VpnEndpointGroup` instance. - :param dict attrs: The attributes to update on the VPN service + :param attrs: The attributes to update on the VPN service represented by ``vpn_endpoint_group``. :returns: The updated vpnservice @@ -4534,7 +4541,7 @@ def update_vpn_endpoint_group(self, vpn_endpoint_group, **attrs): def create_vpn_ipsec_site_connection(self, **attrs): """Create a new IPsec site connection from attributes - :param dict attrs: Keyword arguments which will be used to create a + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.vpn_ipsec_site_connection.VpnIPSecSiteConnection`, comprised of the properties on the IPSecSiteConnection class. @@ -4604,7 +4611,7 @@ def update_vpn_ipsec_site_connection(self, ipsec_site_connection, **attrs): a :class:`~openstack.network.v2.vpn_ipsec_site_connection.VpnIPSecSiteConnection` instance. - :param dict attrs: The attributes to update on the IPsec site + :param attrs: The attributes to update on the IPsec site connection represented by ``ipsec_site_connection``. :returns: The updated IPsec site connection @@ -4641,7 +4648,7 @@ def delete_vpn_ipsec_site_connection( def create_vpn_ike_policy(self, **attrs): """Create a new ike policy from attributes - :param dict attrs: Keyword arguments which will be used to create a + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.vpn_ike_policy.VpnIkePolicy`, comprised of the properties on the VpnIkePolicy class. @@ -4704,7 +4711,7 @@ def update_vpn_ike_policy(self, ike_policy, **attrs): :ike_policy: Either the IK of an IKE policy or a :class:`~openstack.network.v2.vpn_ike_policy.VpnIkePolicy` instance. - :param dict attrs: The attributes to update on the ike policy + :param attrs: The attributes to update on the ike policy represented by ``ike_policy``. :returns: The updated ike policy @@ -4736,7 +4743,7 @@ def delete_vpn_ike_policy(self, ike_policy, ignore_missing=True): def create_vpn_ipsec_policy(self, **attrs): """Create a new IPsec policy from attributes - :param dict attrs: Keyword arguments which will be used to create a + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.vpn_ipsec_policy.VpnIpsecPolicy`, comprised of the properties on the VpnIpsecPolicy class. @@ -4801,7 +4808,7 @@ def update_vpn_ipsec_policy(self, ipsec_policy, **attrs): :ipsec_policy: Either the id of an IPsec policy or a :class:`~openstack.network.v2.vpn_ipsec_policy.VpnIpsecPolicy` instance. - :param dict attrs: The attributes to update on the IPsec policy + :param attrs: The attributes to update on the IPsec policy represented by ``ipsec_policy``. :returns: The updated IPsec policy @@ -4834,7 +4841,7 @@ def delete_vpn_ipsec_policy(self, ipsec_policy, ignore_missing=True): def create_vpn_service(self, **attrs): """Create a new vpn service from attributes - :param dict attrs: Keyword arguments which will be used to create + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.vpn_service.VpnService`, comprised of the properties on the VpnService class. @@ -4907,7 +4914,7 @@ def update_vpn_service(self, vpn_service, **attrs): :param vpn_service: Either the id of a vpn service or a :class:`~openstack.network.v2.vpn_service.VpnService` instance. - :param dict attrs: The attributes to update on the VPN service + :param attrs: The attributes to update on the VPN service represented by ``vpn_service``. :returns: The updated vpnservice @@ -4921,7 +4928,7 @@ def create_floating_ip_port_forwarding(self, floating_ip, **attrs): :param floating_ip: The value can be either the ID of a floating ip or a :class:`~openstack.network.v2.floating_ip.FloatingIP` instance. - :param dict attrs:Keyword arguments which will be used to create + :param attrs:Keyword arguments which will be used to create a:class:`~openstack.network.v2.port_forwarding.PortForwarding`, comprised of the properties on the PortForwarding class. @@ -5042,7 +5049,7 @@ def create_conntrack_helper(self, router, **attrs): :param router: Either the router ID or an instance of :class:`~openstack.network.v2.router.Router` - :param dict attrs: Keyword arguments which will be used to create a + :param attrs: Keyword arguments which will be used to create a :class:`~openstack.network.v2.l3_conntrack_helper.ConntrackHelper`, comprised of the properties on the ConntrackHelper class. diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index cdcec8b4b..5502e48fd 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -9,6 +9,8 @@ # 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 typing import List + from openstack.common import tag from openstack.network.v2 import _base from openstack import resource @@ -39,8 +41,10 @@ class Port(_base.NetworkResource, tag.TagMixin): ) # Properties - #: Allowed address pairs. - allowed_address_pairs = resource.Body('allowed_address_pairs', type=list) + #: Allowed address pairs list. Dictionary key ``ip_address`` is required + #: and key ``mac_address`` is optional. + allowed_address_pairs: List[dict] = resource.Body('allowed_address_pairs', + type=list) #: The ID of the host where the port is allocated. In some cases, #: different implementations can run on different hosts. binding_host_id = resource.Body('binding:host_id') diff --git a/openstack/network/v2/router.py b/openstack/network/v2/router.py index 6a5719e4e..9b5ef557c 100644 --- a/openstack/network/v2/router.py +++ b/openstack/network/v2/router.py @@ -114,7 +114,7 @@ def remove_interface(self, session, **body): resp = self._put(session, url, body) return resp.json() - def add_extra_routes(self, session, body): + def add_extra_routes(self, session, body) -> 'Router': """Add extra routes to a logical router. :param session: The session to communicate through. @@ -130,7 +130,7 @@ def add_extra_routes(self, session, body): self._translate_response(resp) return self - def remove_extra_routes(self, session, body): + def remove_extra_routes(self, session, body) -> 'Router': """Remove extra routes from a logical router. :param session: The session to communicate through. diff --git a/openstack/proxy.py b/openstack/proxy.py index 26918d3f2..6a2774e41 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -11,6 +11,11 @@ # under the License. import functools +from typing import Generator +from typing import Generic +from typing import Optional +from typing import Type +from typing import TypeVar import urllib from urllib.parse import urlparse @@ -28,6 +33,8 @@ from openstack import exceptions from openstack import resource +T = TypeVar('T') + # The _check_resource decorator is used on Proxy methods to ensure that # the `actual` argument is in fact the type of the `expected` argument. @@ -67,7 +74,7 @@ def normalize_metric_name(name): return name -class Proxy(adapter.Adapter): +class Proxy(adapter.Adapter, Generic[T]): """Represents a service.""" retriable_status_codes = None @@ -430,7 +437,8 @@ def _get_connection(self): self, '_connection', getattr(self.session, '_sdk_connection', None) ) - def _get_resource(self, resource_type, value, **attrs): + def _get_resource(self, resource_type: Type[T], value, + **attrs) -> T: """Get a resource object to work on :param resource_type: The type of resource to operate on. This should @@ -476,7 +484,8 @@ def _get_uri_attribute(self, child, parent, name): value = resource.Resource._get_id(parent) return value - def _find(self, resource_type, name_or_id, ignore_missing=True, **attrs): + def _find(self, resource_type: Type[T], name_or_id, ignore_missing=True, + **attrs) -> Optional[T]: """Find a resource :param name_or_id: The name or ID of a resource to find. @@ -496,7 +505,8 @@ def _find(self, resource_type, name_or_id, ignore_missing=True, **attrs): ) @_check_resource(strict=False) - def _delete(self, resource_type, value, ignore_missing=True, **attrs): + def _delete(self, resource_type: Type[T], value, ignore_missing=True, + **attrs): """Delete a resource :param resource_type: The type of resource to delete. This should @@ -532,7 +542,8 @@ def _delete(self, resource_type, value, ignore_missing=True, **attrs): return rv @_check_resource(strict=False) - def _update(self, resource_type, value, base_path=None, **attrs): + def _update(self, resource_type: Type[T], value, base_path=None, + **attrs) -> T: """Update a resource :param resource_type: The type of resource to update. @@ -556,7 +567,7 @@ def _update(self, resource_type, value, base_path=None, **attrs): res = self._get_resource(resource_type, value, **attrs) return res.commit(self, base_path=base_path) - def _create(self, resource_type, base_path=None, **attrs): + def _create(self, resource_type: Type[T], base_path=None, **attrs): """Create a resource from attributes :param resource_type: The type of resource to create. @@ -580,7 +591,8 @@ def _create(self, resource_type, base_path=None, **attrs): res = resource_type.new(connection=conn, **attrs) return res.create(self, base_path=base_path) - def _bulk_create(self, resource_type, data, base_path=None): + def _bulk_create(self, resource_type: Type[T], data, base_path=None + ) -> Generator[T, None, None]: """Create a resource from attributes :param resource_type: The type of resource to create. @@ -602,13 +614,13 @@ def _bulk_create(self, resource_type, data, base_path=None): @_check_resource(strict=False) def _get( - self, - resource_type, - value=None, - requires_id=True, - base_path=None, - skip_cache=False, - **attrs + self, + resource_type: Type[T], + value=None, + requires_id=True, + base_path=None, + skip_cache=False, + **attrs ): """Fetch a resource @@ -645,13 +657,13 @@ def _get( ) def _list( - self, - resource_type, - paginated=True, - base_path=None, - jmespath_filters=None, - **attrs - ): + self, + resource_type: Type[T], + paginated=True, + base_path=None, + jmespath_filters=None, + **attrs + ) -> Generator[T, None, None]: """List a resource :param resource_type: The type of resource to list. This should @@ -688,7 +700,8 @@ def _list( return data - def _head(self, resource_type, value=None, base_path=None, **attrs): + def _head(self, resource_type: Type[T], value=None, base_path=None, + **attrs): """Retrieve a resource's header :param resource_type: The type of resource to retrieve. From 433d97c40cb65e0986609b852d76040d50d6e032 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 8 Nov 2022 17:05:14 +0000 Subject: [PATCH 3180/3836] image: Add missing image import options Add support for the following options: - remote_region - remote_image_id - remote_service_interface In addition, we now return the response to the user. Change-Id: I7ebb75896002ea8e0eca6617eb407e94050bce65 Signed-off-by: Stephen Finucane --- openstack/image/v2/_proxy.py | 50 ++++++++++++---- openstack/image/v2/image.py | 58 +++++++++++++------ openstack/tests/unit/image/v2/test_image.py | 13 +++-- openstack/tests/unit/image/v2/test_proxy.py | 8 ++- ...-import-proxy-params-f19d8b6166104ebe.yaml | 12 ++++ ...kwarg-only-arguments-94c9b2033d386160.yaml | 5 ++ 6 files changed, 111 insertions(+), 35 deletions(-) create mode 100644 releasenotes/notes/image-import-proxy-params-f19d8b6166104ebe.yaml create mode 100644 releasenotes/notes/image-proxy-layer-kwarg-only-arguments-94c9b2033d386160.yaml diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index f9e2fb314..5d7ca00fb 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -50,7 +50,14 @@ def _create_image(self, **kwargs): return self._create(_image.Image, **kwargs) def import_image( - self, image, method='glance-direct', uri=None, + self, + image, + method='glance-direct', + *, + uri=None, + remote_region=None, + remote_image_id=None, + remote_service_interface=None, store=None, stores=None, all_stores=None, @@ -67,12 +74,23 @@ def import_image( The value can be the ID of a image or a :class:`~openstack.image.v2.image.Image` instance. :param method: - Method to use for importing the image. - A valid value is glance-direct or web-download. + Method to use for importing the image. Not all deployments support + all methods. One of: ``glance-direct`` (default), ``web-download``, + ``glance-download``, or ``copy-image``. Use of ``glance-direct`` + requires the image be first staged. :param uri: Required only if using the web-download import method. This url is where the data is made available to the Image service. + :param remote_region: + The remote glance region to download the image from when using + glance-download. + :param remote_image_id: + The ID of the image to import from the remote glance when using + glance-download. + :param remote_service_interface: + The remote glance service interface to use when using + glance-download :param store: Used when enabled_backends is activated in glance. The value can be the id of a store or a @@ -95,17 +113,19 @@ def import_image( the stores where the data has been correctly uploaded. Default is True. - :returns: None + :returns: The raw response from the request. """ image = self._get_resource(_image.Image, image) if all_stores and (store or stores): raise exceptions.InvalidRequest( - "all_stores is mutually exclusive with" - " store and stores") + "all_stores is mutually exclusive with " + "store and stores" + ) if store is not None: if stores: raise exceptions.InvalidRequest( - "store and stores are mutually exclusive") + "store and stores are mutually exclusive" + ) store = self._get_resource(_si.Store, store) stores = stores or [] @@ -118,11 +138,17 @@ def import_image( # disk_format are required for using image import process if not all([image.container_format, image.disk_format]): raise exceptions.InvalidRequest( - "Both container_format and disk_format are required for" - " importing an image") - - image.import_image( - self, method=method, uri=uri, + "Both container_format and disk_format are required for " + "importing an image" + ) + + return image.import_image( + self, + method=method, + uri=uri, + remote_region=remote_region, + remote_image_id=remote_image_id, + remote_service_interface=remote_service_interface, store=store, stores=stores, all_stores=all_stores, diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index 5a79c9c1c..6b71897bb 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -298,44 +298,66 @@ def stage(self, session, *, data=None): self._translate_response(response, has_body=False) return self - def import_image(self, session, method='glance-direct', uri=None, - store=None, stores=None, all_stores=None, - all_stores_must_succeed=None): + def import_image( + self, + session, + method='glance-direct', + *, + uri=None, + remote_region=None, + remote_image_id=None, + remote_service_interface=None, + store=None, + stores=None, + all_stores=None, + all_stores_must_succeed=None, + ): """Import Image via interoperable image import process""" if all_stores and (store or stores): raise exceptions.InvalidRequest( - "all_stores is mutually exclusive with" - " store and stores") + 'all_stores is mutually exclusive with store and stores' + ) if store and stores: raise exceptions.InvalidRequest( - "store and stores are mutually exclusive." - " Please just use stores.") + 'store and stores are mutually exclusive. stores should be ' + 'preferred.' + ) if store: stores = [store] else: stores = stores or [] url = utils.urljoin(self.base_path, self.id, 'import') - json = {'method': {'name': method}} + data = {'method': {'name': method}} + if uri: - if method == 'web-download': - json['method']['uri'] = uri - else: - raise exceptions.InvalidRequest('URI is only supported with ' - 'method: "web-download"') + if method != 'web-download': + raise exceptions.InvalidRequest( + 'URI is only supported with method: "web-download"' + ) + data['method']['uri'] = uri + + if remote_region and remote_image_id: + if remote_service_interface: + data['method']['glance_service_interface'] = \ + remote_service_interface + data['method']['glance_region'] = remote_region + data['method']['glance_image_id'] = remote_image_id + if all_stores is not None: - json['all_stores'] = all_stores + data['all_stores'] = all_stores if all_stores_must_succeed is not None: - json['all_stores_must_succeed'] = all_stores_must_succeed + data['all_stores_must_succeed'] = all_stores_must_succeed for s in stores: - json.setdefault('stores', []) - json['stores'].append(s.id) + data.setdefault('stores', []) + data['stores'].append(s.id) headers = {} # Backward compat if store is not None: headers = {'X-Image-Meta-Store': store.id} - session.post(url, json=json, headers=headers) + + return session.post(url, json=data, headers=headers) def _consume_header_attrs(self, attrs): self.image_import_methods = [] diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index e8bbf45cf..4596a4821 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -265,7 +265,7 @@ def test_remove_tag(self): def test_import_image(self): sot = image.Image(**EXAMPLE) json = {"method": {"name": "web-download", "uri": "such-a-good-uri"}} - sot.import_image(self.sess, "web-download", "such-a-good-uri") + sot.import_image(self.sess, "web-download", uri="such-a-good-uri") self.sess.post.assert_called_with( 'images/IDENTIFIER/import', headers={}, @@ -293,7 +293,12 @@ def test_import_image_with_store(self): } store = mock.MagicMock() store.id = "ceph_1" - sot.import_image(self.sess, "web-download", "such-a-good-uri", store) + sot.import_image( + self.sess, + "web-download", + uri="such-a-good-uri", + store=store, + ) self.sess.post.assert_called_with( 'images/IDENTIFIER/import', headers={'X-Image-Meta-Store': 'ceph_1'}, @@ -314,7 +319,7 @@ def test_import_image_with_stores(self): sot.import_image( self.sess, "web-download", - "such-a-good-uri", + uri="such-a-good-uri", stores=[store], ) self.sess.post.assert_called_with( @@ -335,7 +340,7 @@ def test_import_image_with_all_stores(self): sot.import_image( self.sess, "web-download", - "such-a-good-uri", + uri="such-a-good-uri", all_stores=True, ) self.sess.post.assert_called_with( diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index f663c89ee..0775ff787 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -64,12 +64,18 @@ def test_image_import(self): self._verify( "openstack.image.v2.image.Image.import_image", self.proxy.import_image, - method_args=[original_image, "method", "uri"], + method_args=[original_image, "method"], + method_kwargs={ + "uri": "uri", + }, expected_args=[self.proxy], expected_kwargs={ "method": "method", "store": None, "uri": "uri", + "remote_region": None, + "remote_image_id": None, + "remote_service_interface": None, "stores": [], "all_stores": None, "all_stores_must_succeed": None, diff --git a/releasenotes/notes/image-import-proxy-params-f19d8b6166104ebe.yaml b/releasenotes/notes/image-import-proxy-params-f19d8b6166104ebe.yaml new file mode 100644 index 000000000..56b59f4f1 --- /dev/null +++ b/releasenotes/notes/image-import-proxy-params-f19d8b6166104ebe.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + The ``openstack.image.Image.import_image`` method and ``import_image`` + image proxy method now accept the following additional paramters: + + - ``remote_region`` + - ``remote_image_id`` + - ``remote_service_interface`` + + These are required to support the ``glance-download`` image import + method. diff --git a/releasenotes/notes/image-proxy-layer-kwarg-only-arguments-94c9b2033d386160.yaml b/releasenotes/notes/image-proxy-layer-kwarg-only-arguments-94c9b2033d386160.yaml new file mode 100644 index 000000000..882bd8653 --- /dev/null +++ b/releasenotes/notes/image-proxy-layer-kwarg-only-arguments-94c9b2033d386160.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + The signatures of the ``openstack.image.v2.import_image`` has changed. All + arguments except ``image`` and ``method`` are now kwarg-only. From 121911feecbbc92920f67235454c692faff43c2e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 9 Nov 2022 17:01:40 +0000 Subject: [PATCH 3181/3836] image: Reformat proxy modules We're going to be conducting surgery on these shortly. Clean them up before that happens. Change-Id: Iec490c844efe735f01f6a9f6cc12876f2913b98c Signed-off-by: Stephen Finucane --- openstack/image/_base_proxy.py | 161 +++++++----- openstack/image/v1/_proxy.py | 62 +++-- openstack/image/v2/_proxy.py | 460 +++++++++++++++++++++------------ 3 files changed, 429 insertions(+), 254 deletions(-) diff --git a/openstack/image/_base_proxy.py b/openstack/image/_base_proxy.py index d58de5b57..71267f4de 100644 --- a/openstack/image/_base_proxy.py +++ b/openstack/image/_base_proxy.py @@ -9,15 +9,29 @@ # 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 os - from openstack import exceptions from openstack import proxy from openstack import utils +def _get_name_and_filename(name, image_format): + # See if name points to an existing file + if os.path.exists(name): + # Neat. Easy enough + return os.path.splitext(os.path.basename(name))[0], name + + # Try appending the disk format + name_with_ext = '.'.join((name, image_format)) + if os.path.exists(name_with_ext): + return os.path.basename(name), name_with_ext + + return name, None + + class BaseImageProxy(proxy.Proxy, metaclass=abc.ABCMeta): retriable_status_codes = [503] @@ -35,14 +49,21 @@ class BaseImageProxy(proxy.Proxy, metaclass=abc.ABCMeta): # ====== IMAGES ====== def create_image( - self, name, filename=None, + self, + name, + filename=None, container=None, - md5=None, sha256=None, - disk_format=None, container_format=None, + md5=None, + sha256=None, + disk_format=None, + container_format=None, disable_vendor_agent=True, - allow_duplicates=False, meta=None, - wait=False, timeout=3600, - data=None, validate_checksum=False, + allow_duplicates=False, + meta=None, + wait=False, + timeout=3600, + data=None, + validate_checksum=False, use_import=False, stores=None, tags=None, @@ -72,7 +93,7 @@ def create_image( (optional, defaults to the os-client-config config value for this cloud) :param list tags: List of tags for this image. Each tag is a string - of at most 255 chars. + of at most 255 chars. :param bool disable_vendor_agent: Whether or not to append metadata flags to the image to inform the cloud in question to not expect a vendor agent to be runing. (optional, defaults to True) @@ -93,24 +114,21 @@ def create_image( the target cloud so should only be used when needed, such as when the user needs the cloud to transform image format. If the cloud has disabled direct uploads, this will default to true. - :param stores: - List of stores to be used when enabled_backends is activated - in glance. List values can be the id of a store or a + :param stores: List of stores to be used when enabled_backends is + activated in glance. List values can be the id of a store or a :class:`~openstack.image.v2.service_info.Store` instance. Implies ``use_import`` equals ``True``. - :param all_stores: - Upload to all available stores. Mutually exclusive with - ``store`` and ``stores``. + :param all_stores: Upload to all available stores. Mutually exclusive + with ``store`` and ``stores``. Implies ``use_import`` equals ``True``. - :param all_stores_must_succeed: - When set to True, if an error occurs during the upload in at - least one store, the worfklow fails, the data is deleted - from stores where copying is done (not staging), and the - state of the image is unchanged. When set to False, the - workflow will fail (data deleted from stores, …) only if the - import fails on all stores specified by the user. In case of - a partial success, the locations added to the image will be - the stores where the data has been correctly uploaded. + :param all_stores_must_succeed: When set to True, if an error occurs + during the upload in at least one store, the worfklow fails, the + data is deleted from stores where copying is done (not staging), + and the state of the image is unchanged. When set to False, the + workflow will fail (data deleted from stores, …) only if the import + fails on all stores specified by the user. In case of a partial + success, the locations added to the image will be the stores where + the data has been correctly uploaded. Default is True. Implies ``use_import`` equals ``True``. @@ -126,36 +144,45 @@ def create_image( If a value is in meta and kwargs, meta wins. :returns: A ``munch.Munch`` of the Image object - :raises: SDKException if there are problems uploading """ if container is None: container = self._connection._OBJECT_AUTOCREATE_CONTAINER + if not meta: meta = {} if not disk_format: disk_format = self._connection.config.config['image_format'] + if not container_format: # https://docs.openstack.org/image-guide/image-formats.html container_format = 'bare' if data and filename: raise exceptions.SDKException( - 'Passing filename and data simultaneously is not supported') + 'Passing filename and data simultaneously is not supported' + ) + # If there is no filename, see if name is actually the filename if not filename and not data: - name, filename = self._get_name_and_filename( - name, self._connection.config.config['image_format']) + name, filename = _get_name_and_filename( + name, + self._connection.config.config['image_format'], + ) + if validate_checksum and data and not isinstance(data, bytes): raise exceptions.SDKException( 'Validating checksum is not possible when data is not a ' - 'direct binary object') + 'direct binary object' + ) + if not (md5 or sha256) and validate_checksum: if filename: - (md5, sha256) = utils._get_file_hashes(filename) + md5, sha256 = utils._get_file_hashes(filename) elif data and isinstance(data, bytes): - (md5, sha256) = utils._calculate_data_hashes(data) + md5, sha256 = utils._calculate_data_hashes(data) + if allow_duplicates: current_image = None else: @@ -165,37 +192,44 @@ def create_image( props = current_image.get('properties') or {} md5_key = props.get( self._IMAGE_MD5_KEY, - props.get(self._SHADE_IMAGE_MD5_KEY, '')) + props.get(self._SHADE_IMAGE_MD5_KEY, ''), + ) sha256_key = props.get( self._IMAGE_SHA256_KEY, - props.get(self._SHADE_IMAGE_SHA256_KEY, '')) + props.get(self._SHADE_IMAGE_SHA256_KEY, ''), + ) up_to_date = utils._hashes_up_to_date( - md5=md5, sha256=sha256, - md5_key=md5_key, sha256_key=sha256_key) + md5=md5, + sha256=sha256, + md5_key=md5_key, + sha256_key=sha256_key, + ) if up_to_date: self.log.debug( "image %(name)s exists and is up to date", - {'name': name}) + {'name': name}, + ) return current_image else: self.log.debug( "image %(name)s exists, but contains different " "checksums. Updating.", - {'name': name}) + {'name': name}, + ) if disable_vendor_agent: kwargs.update( - self._connection.config.config['disable_vendor_agent']) + self._connection.config.config['disable_vendor_agent'] + ) # If a user used the v1 calling format, they will have # passed a dict called properties along properties = kwargs.pop('properties', {}) properties[self._IMAGE_MD5_KEY] = md5 or '' properties[self._IMAGE_SHA256_KEY] = sha256 or '' - properties[self._IMAGE_OBJECT_KEY] = '/'.join( - [container, name]) + properties[self._IMAGE_OBJECT_KEY] = '/'.join([container, name]) kwargs.update(properties) - image_kwargs = dict(properties=kwargs) + image_kwargs = {'properties': kwargs} if disk_format: image_kwargs['disk_format'] = disk_format if container_format: @@ -205,18 +239,25 @@ def create_image( if filename or data: image = self._upload_image( - name, filename=filename, data=data, meta=meta, - wait=wait, timeout=timeout, + name, + filename=filename, + data=data, + meta=meta, + wait=wait, + timeout=timeout, validate_checksum=validate_checksum, use_import=use_import, stores=stores, all_stores=stores, all_stores_must_succeed=stores, - **image_kwargs) + **image_kwargs, + ) else: image_kwargs['name'] = name image = self._create_image(**image_kwargs) + self._connection._get_cache(None).invalidate() + return image @abc.abstractmethod @@ -225,12 +266,19 @@ def _create_image(self, name, **image_kwargs): @abc.abstractmethod def _upload_image( - self, name, filename, data, meta, wait, timeout, - validate_checksum=True, use_import=False, + self, + name, + filename, + data, + meta, + wait, + timeout, + validate_checksum=True, + use_import=False, stores=None, all_stores=None, all_stores_must_succeed=None, - **image_kwargs + **image_kwargs, ): pass @@ -239,13 +287,17 @@ def _update_image_properties(self, image, meta, properties): pass def update_image_properties( - self, image=None, meta=None, **kwargs): + self, + image=None, + meta=None, + **kwargs, + ): """ Update the properties of an existing image. :param image: Name or id of an image or an Image object. :param meta: A dict of key/value pairs to use for metadata that - bypasses automatic type conversion. + bypasses automatic type conversion. Additional kwargs will be passed to the image creation as additional metadata for the image and will have all values converted to string @@ -267,16 +319,3 @@ def update_image_properties( img_props[k] = v return self._update_image_properties(image, meta, img_props) - - def _get_name_and_filename(self, name, image_format): - # See if name points to an existing file - if os.path.exists(name): - # Neat. Easy enough - return (os.path.splitext(os.path.basename(name))[0], name) - - # Try appending the disk format - name_with_ext = '.'.join((name, image_format)) - if os.path.exists(name_with_ext): - return (os.path.basename(name), name_with_ext) - - return (name, None) diff --git a/openstack/image/v1/_proxy.py b/openstack/image/v1/_proxy.py index b59744ded..bdbdded47 100644 --- a/openstack/image/v1/_proxy.py +++ b/openstack/image/v1/_proxy.py @@ -18,10 +18,8 @@ class Proxy(_base_proxy.BaseImageProxy): - def _create_image(self, **kwargs): - """Create image resource from attributes - """ + """Create image resource from attributes""" return self._create(_image.Image, **kwargs) def upload_image(self, **attrs): @@ -43,7 +41,13 @@ def upload_image(self, **attrs): return self._create(_image.Image, **attrs) def _upload_image( - self, name, filename, data, meta, wait, timeout, + self, + name, + filename, + data, + meta, + wait, + timeout, use_import=False, stores=None, all_stores=None, @@ -52,10 +56,12 @@ def _upload_image( ): if use_import: raise exceptions.InvalidRequest( - "Glance v1 does not support image import") + "Glance v1 does not support image import" + ) if stores or all_stores or all_stores_must_succeed: raise exceptions.InvalidRequest( - "Glance v1 does not support stores") + "Glance v1 does not support stores" + ) # NOTE(mordred) wait and timeout parameters are unused, but # are present for ease at calling site. if filename and not data: @@ -67,8 +73,8 @@ def _upload_image( # TODO(mordred) Convert this to use image Resource image = self._connection._get_and_munchify( - 'image', - self.post('/images', json=image_kwargs)) + 'image', self.post('/images', json=image_kwargs) + ) checksum = image_kwargs['properties'].get(self._IMAGE_MD5_KEY, '') try: @@ -84,18 +90,20 @@ def _upload_image( 'image', self.put( '/images/{id}'.format(id=image.id), - headers=headers, data=image_data)) - + headers=headers, + data=image_data, + ), + ) except exc.OpenStackCloudHTTPError: - self.log.debug( - "Deleting failed upload of image %s", name) + self.log.debug("Deleting failed upload of image %s", name) try: self.delete('/images/{id}'.format(id=image.id)) except exc.OpenStackCloudHTTPError: # We're just trying to clean up - if it doesn't work - shrug self.log.warning( "Failed deleting image after we failed uploading it.", - exc_info=True) + exc_info=True, + ) raise return image @@ -107,8 +115,7 @@ def _update_image_properties(self, image, meta, properties): img_props['x-image-meta-{key}'.format(key=k)] = v if not img_props: return False - self.put( - '/images/{id}'.format(id=image.id), headers=img_props) + self.put('/images/{id}'.format(id=image.id), headers=img_props) self._connection.list_images.invalidate(self._connection) return True @@ -141,8 +148,9 @@ def find_image(self, name_or_id, ignore_missing=True): attempting to find a nonexistent resource. :returns: One :class:`~openstack.image.v1.image.Image` or None """ - return self._find(_image.Image, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _image.Image, name_or_id, ignore_missing=ignore_missing + ) def get_image(self, image): """Get a single image @@ -180,8 +188,13 @@ def update_image(self, image, **attrs): """ return self._update(_image.Image, image, **attrs) - def download_image(self, image, stream=False, output=None, - chunk_size=1024): + def download_image( + self, + image, + stream=False, + output=None, + chunk_size=1024, + ): """Download an image This will download an image to memory when ``stream=False``, or allow @@ -191,7 +204,6 @@ def download_image(self, image, stream=False, output=None, :param image: The value can be either the ID of an image or a :class:`~openstack.image.v2.image.Image` instance. - :param bool stream: When ``True``, return a :class:`requests.Response` instance allowing you to iterate over the response data stream instead of storing its entire @@ -203,9 +215,7 @@ def download_image(self, image, stream=False, output=None, risk inefficiencies with the ``requests`` library's handling of connections. - - When ``False``, return the entire - contents of the response. + When ``False``, return the entire contents of the response. :param output: Either a file object or a path to store data into. :param int chunk_size: size in bytes to read from the wire and buffer at one time. Defaults to 1024 @@ -219,4 +229,8 @@ def download_image(self, image, stream=False, output=None, image = self._get_resource(_image.Image, image) return image.download( - self, stream=stream, output=output, chunk_size=chunk_size) + self, + stream=stream, + output=output, + chunk_size=chunk_size, + ) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 5d7ca00fb..3a7640e8d 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -40,13 +40,12 @@ class Proxy(_base_proxy.BaseImageProxy): "schema": _schema.Schema, "info_import": _si.Import, "info_store": _si.Store, - "task": _task.Task + "task": _task.Task, } # ====== IMAGES ====== def _create_image(self, **kwargs): - """Create image resource from attributes - """ + """Create image resource from attributes""" return self._create(_image.Image, **kwargs) def import_image( @@ -70,43 +69,34 @@ def import_image( Image Service download it by itself without sending binary data at image creation. - :param image: - The value can be the ID of a image or a + :param image: The value can be the ID of a image or a :class:`~openstack.image.v2.image.Image` instance. - :param method: - Method to use for importing the image. Not all deployments support - all methods. One of: ``glance-direct`` (default), ``web-download``, - ``glance-download``, or ``copy-image``. Use of ``glance-direct`` - requires the image be first staged. - :param uri: - Required only if using the web-download import method. + :param method: Method to use for importing the image. Not all + deployments support all methods. One of: ``glance-direct`` + (default), ``web-download``, ``glance-download``, or + ``copy-image``. Use of ``glance-direct`` requires the image be + first staged. + :param uri: Required only if using the ``web-download`` import method. This url is where the data is made available to the Image service. - :param remote_region: - The remote glance region to download the image from when using - glance-download. - :param remote_image_id: - The ID of the image to import from the remote glance when using - glance-download. - :param remote_service_interface: - The remote glance service interface to use when using - glance-download - :param store: - Used when enabled_backends is activated in glance. The value - can be the id of a store or a + :param remote_region: The remote glance region to download the image + from when using glance-download. + :param remote_image_id: The ID of the image to import from the + remote glance when using glance-download. + :param remote_service_interface: The remote glance service interface to + use when using glance-download. + :param store: Used when enabled_backends is activated in glance. The + value can be the id of a store or a. :class:`~openstack.image.v2.service_info.Store` instance. - :param stores: - List of stores to be used when enabled_backends is activated - in glance. List values can be the id of a store or a + :param stores: List of stores to be used when enabled_backends is + activated in glance. List values can be the id of a store or a :class:`~openstack.image.v2.service_info.Store` instance. - :param all_stores: - Upload to all available stores. Mutually exclusive with - ``store`` and ``stores``. - :param all_stores_must_succeed: - When set to True, if an error occurs during the upload in at - least one store, the worfklow fails, the data is deleted - from stores where copying is done (not staging), and the - state of the image is unchanged. When set to False, the + :param all_stores: Upload to all available stores. Mutually exclusive + with ``store`` and ``stores``. + :param all_stores_must_succeed: When set to True, if an error occurs + during the upload in at least one store, the worfklow fails, the + data is deleted from stores where copying is done (not staging), + and the state of the image is unchanged. When set to False, the workflow will fail (data deleted from stores, …) only if the import fails on all stores specified by the user. In case of a partial success, the locations added to the image will be @@ -118,8 +108,7 @@ def import_image( image = self._get_resource(_image.Image, image) if all_stores and (store or stores): raise exceptions.InvalidRequest( - "all_stores is mutually exclusive with " - "store and stores" + "all_stores is mutually exclusive with store and stores" ) if store is not None: if stores: @@ -169,10 +158,10 @@ def stage_image(self, image, filename=None, data=None): image = self._get_resource(_image.Image, image) if 'queued' != image.status: - raise exceptions.SDKException('Image stage is only possible for ' - 'images in the queued state.' - ' Current state is {status}' - .format(status=image.status)) + raise exceptions.SDKException( + 'Image stage is only possible for images in the queued state. ' + 'Current state is {status}'.format(status=image.status) + ) if filename: image.data = open(filename, 'rb') @@ -185,14 +174,15 @@ def stage_image(self, image, filename=None, data=None): return image - def upload_image(self, container_format=None, disk_format=None, - data=None, **attrs): + def upload_image( + self, container_format=None, disk_format=None, data=None, **attrs + ): """Create and upload a new image from attributes .. warning: - This method is deprecated - and also doesn't work very well. - Please stop using it immediately and switch to - `create_image`. + + This method is deprecated - and also doesn't work very well. + Please stop using it immediately and switch to `create_image`. :param container_format: Format of the container. A valid value is ami, ari, aki, bare, ovf, ova, or docker. @@ -215,11 +205,15 @@ def upload_image(self, container_format=None, disk_format=None, # not being set. if not all([container_format, disk_format]): raise exceptions.InvalidRequest( - "Both container_format and disk_format are required") + "Both container_format and disk_format are required" + ) - img = self._create(_image.Image, disk_format=disk_format, - container_format=container_format, - **attrs) + img = self._create( + _image.Image, + disk_format=disk_format, + container_format=container_format, + **attrs, + ) # TODO(briancurtin): Perhaps we should run img.upload_image # in a background thread and just return what is called by @@ -232,13 +226,19 @@ def upload_image(self, container_format=None, disk_format=None, return img def _upload_image( - self, name, filename=None, data=None, meta=None, - wait=False, timeout=None, validate_checksum=True, + self, + name, + filename=None, + data=None, + meta=None, + wait=False, + timeout=None, + validate_checksum=True, use_import=False, stores=None, all_stores=None, all_stores_must_succeed=None, - **kwargs + **kwargs, ): # We can never have nice things. Glance v1 took "is_public" as a # boolean. Glance v2 takes "visibility". If the user gives us @@ -256,28 +256,39 @@ def _upload_image( if self._connection.image_api_use_tasks: if use_import: raise exceptions.SDKException( - "The Glance Task API and Import API are" - " mutually exclusive. Either disable" - " image_api_use_tasks in config, or" - " do not request using import") + "The Glance Task API and Import API are mutually " + "exclusive. Either disable image_api_use_tasks in " + "config, or do not request using import" + ) return self._upload_image_task( - name, filename, data=data, meta=meta, - wait=wait, timeout=timeout, **kwargs) + name, + filename, + data=data, + meta=meta, + wait=wait, + timeout=timeout, + **kwargs, + ) else: return self._upload_image_put( - name, filename, data=data, meta=meta, + name, + filename, + data=data, + meta=meta, validate_checksum=validate_checksum, use_import=use_import, stores=stores, all_stores=all_stores, all_stores_must_succeed=all_stores_must_succeed, - **kwargs) + **kwargs, + ) except exceptions.SDKException: self.log.debug("Image creation failed", exc_info=True) raise except Exception as e: raise exceptions.SDKException( - "Image creation failed: {message}".format(message=str(e))) + "Image creation failed: {message}".format(message=str(e)) + ) def _make_v2_image_params(self, meta, properties): ret = {} @@ -295,8 +306,13 @@ def _make_v2_image_params(self, meta, properties): return ret def _upload_image_put( - self, name, filename, data, meta, - validate_checksum, use_import=False, + self, + name, + filename, + data, + meta, + validate_checksum, + use_import=False, stores=None, all_stores=None, all_stores_must_succeed=None, @@ -323,8 +339,9 @@ def _upload_image_put( use_import = True if use_import and not supports_import: raise exceptions.SDKException( - "Importing image was requested but the cloud does not" - " support the image import method.") + "Importing image was requested but the cloud does not " + "support the image import method." + ) try: if not use_import: @@ -343,26 +360,33 @@ def _upload_image_put( data = image.fetch(self) checksum = data.get('checksum') if checksum: - valid = (checksum == md5 or checksum == sha256) + valid = checksum == md5 or checksum == sha256 if not valid: raise Exception('Image checksum verification failed') except Exception: - self.log.debug( - "Deleting failed upload of image %s", name) + self.log.debug("Deleting failed upload of image %s", name) self.delete_image(image.id) raise return image def _upload_image_task( - self, name, filename, data, - wait, timeout, meta, **image_kwargs): - + self, + name, + filename, + data, + wait, + timeout, + meta, + **image_kwargs, + ): if not self._connection.has_service('object-store'): raise exceptions.SDKException( - "The cloud {cloud} is configured to use tasks for image" - " upload, but no object-store service is available." - " Aborting.".format(cloud=self._connection.config.name)) + "The cloud {cloud} is configured to use tasks for image " + "upload, but no object-store service is available. " + "Aborting.".format(cloud=self._connection.config.name) + ) + properties = image_kwargs.get('properties', {}) md5 = properties[self._IMAGE_MD5_KEY] sha256 = properties[self._IMAGE_SHA256_KEY] @@ -372,20 +396,28 @@ def _upload_image_task( self._connection.create_container(container) self._connection.create_object( - container, name, filename, - md5=md5, sha256=sha256, + container, + name, + filename, + md5=md5, + sha256=sha256, data=data, metadata={self._connection._OBJECT_AUTOCREATE_KEY: 'true'}, - **{'content-type': 'application/octet-stream', - 'x-delete-after': str(24 * 60 * 60)}) + **{ + 'content-type': 'application/octet-stream', + 'x-delete-after': str(24 * 60 * 60), + }, + ) # TODO(mordred): Can we do something similar to what nodepool does # using glance properties to not delete then upload but instead make a # new "good" image and then mark the old one as "bad" - task_args = dict( - type='import', input=dict( - import_from='{container}/{name}'.format( - container=container, name=name), - image_properties=dict(name=name))) + task_args = { + 'type': 'import', + 'input': { + 'import_from': f'{container}/{name}', + 'image_properties': {'name': name}, + }, + } glance_task = self.create_task(**task_args) self._connection.list_images.invalidate(self) @@ -394,9 +426,8 @@ def _upload_image_task( try: glance_task = self.wait_for_task( - task=glance_task, - status='success', - wait=timeout) + task=glance_task, status='success', wait=timeout + ) image_id = glance_task.result['image_id'] image = self.get_image(image_id) @@ -410,13 +441,18 @@ def _upload_image_task( image = self.update_image(image, **image_kwargs) self.log.debug( "Image Task %s imported %s in %s", - glance_task.id, image_id, (time.time() - start)) + glance_task.id, + image_id, + (time.time() - start), + ) except exceptions.ResourceFailure as e: glance_task = self.get_task(glance_task) raise exceptions.SDKException( "Image creation failed: {message}".format( - message=e.message), - extra_data=glance_task) + message=e.message + ), + extra_data=glance_task, + ) finally: # Clean up after ourselves. The object we created is not # needed after the import is done. @@ -448,8 +484,9 @@ def _update_image_properties(self, image, meta, properties): def _existing_image(self, **kwargs): return _image.Image.existing(connection=self._connection, **kwargs) - def download_image(self, image, stream=False, output=None, - chunk_size=1024): + def download_image( + self, image, stream=False, output=None, chunk_size=1024 + ): """Download an image This will download an image to memory when ``stream=False``, or allow @@ -459,21 +496,17 @@ def download_image(self, image, stream=False, output=None, :param image: The value can be either the ID of an image or a :class:`~openstack.image.v2.image.Image` instance. - :param bool stream: When ``True``, return a :class:`requests.Response` - instance allowing you to iterate over the - response data stream instead of storing its entire - contents in memory. See - :meth:`requests.Response.iter_content` for more - details. *NOTE*: If you do not consume - the entirety of the response you must explicitly - call :meth:`requests.Response.close` or otherwise - risk inefficiencies with the ``requests`` - library's handling of connections. - - - When ``False``, return the entire - contents of the response. + instance allowing you to iterate over the response data stream + instead of storing its entire contents in memory. See + :meth:`requests.Response.iter_content` for more details. + + *NOTE*: If you do not consume the entirety of the response you must + explicitly call :meth:`requests.Response.close` or otherwise risk + inefficiencies with the ``requests`` library's handling of + connections. + + When ``False``, return the entire contents of the response. :param output: Either a file object or a path to store data into. :param int chunk_size: size in bytes to read from the wire and buffer at one time. Defaults to 1024 @@ -487,7 +520,11 @@ def download_image(self, image, stream=False, output=None, image = self._get_resource(_image.Image, image) return image.download( - self, stream=stream, output=output, chunk_size=chunk_size) + self, + stream=stream, + output=output, + chunk_size=chunk_size, + ) def delete_image(self, image, *, store=None, ignore_missing=True): """Delete an image @@ -523,8 +560,11 @@ def find_image(self, name_or_id, ignore_missing=True): attempting to find a nonexistent resource. :returns: One :class:`~openstack.image.v2.image.Image` or None """ - return self._find(_image.Image, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _image.Image, + name_or_id, + ignore_missing=ignore_missing, + ) def get_image(self, image): """Get a single image @@ -644,8 +684,12 @@ def remove_member(self, member, image=None, ignore_missing=True): """ image_id = resource.Resource._get_id(image) member_id = resource.Resource._get_id(member) - self._delete(_member.Member, member_id=member_id, image_id=image_id, - ignore_missing=ignore_missing) + self._delete( + _member.Member, + member_id=member_id, + image_id=image_id, + ignore_missing=ignore_missing, + ) def find_member(self, name_or_id, image, ignore_missing=True): """Find a single member @@ -662,8 +706,12 @@ def find_member(self, name_or_id, image, ignore_missing=True): :returns: One :class:`~openstack.image.v2.member.Member` or None """ image_id = resource.Resource._get_id(image) - return self._find(_member.Member, name_or_id, image_id=image_id, - ignore_missing=ignore_missing) + return self._find( + _member.Member, + name_or_id, + image_id=image_id, + ignore_missing=ignore_missing, + ) def get_member(self, member, image): """Get a single member on an image @@ -679,8 +727,9 @@ def get_member(self, member, image): """ member_id = resource.Resource._get_id(member) image_id = resource.Resource._get_id(image) - return self._get(_member.Member, member_id=member_id, - image_id=image_id) + return self._get( + _member.Member, member_id=member_id, image_id=image_id + ) def members(self, image, **query): """Return a generator of members @@ -713,8 +762,12 @@ def update_member(self, member, image, **attrs): """ member_id = resource.Resource._get_id(member) image_id = resource.Resource._get_id(image) - return self._update(_member.Member, member_id=member_id, - image_id=image_id, **attrs) + return self._update( + _member.Member, + member_id=member_id, + image_id=image_id, + **attrs, + ) # ====== METADEF NAMESPACES ====== def create_metadef_namespace(self, **attrs): @@ -814,9 +867,11 @@ def metadef_resource_types(self, **query): return self._list(_metadef_resource_type.MetadefResourceType, **query) # ====== METADEF RESOURCE TYPES ASSOCIATION====== - def create_metadef_resource_type_association(self, - metadef_namespace, - **attrs): + def create_metadef_resource_type_association( + self, + metadef_namespace, + **attrs, + ): """Creates a resource type association between a namespace and the resource type specified in the body of the request. @@ -833,12 +888,15 @@ def create_metadef_resource_type_association(self, return self._create( _metadef_resource_type.MetadefResourceTypeAssociation, namespace_name=namespace_name, - **attrs) + **attrs, + ) - def delete_metadef_resource_type_association(self, - metadef_resource_type, - metadef_namespace, - ignore_missing=True): + def delete_metadef_resource_type_association( + self, + metadef_resource_type, + metadef_namespace, + ignore_missing=True, + ): """Removes a resource type association in a namespace. :param metadef_resource_type: The value can be either the name of @@ -879,7 +937,8 @@ def metadef_resource_type_associations(self, metadef_namespace, **query): return self._list( _metadef_resource_type.MetadefResourceTypeAssociation, namespace_name=namespace_name, - **query) + **query, + ) # ====== SCHEMAS ====== def get_images_schema(self): @@ -889,8 +948,11 @@ def get_images_schema(self): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get(_schema.Schema, requires_id=False, - base_path='/schemas/images') + return self._get( + _schema.Schema, + requires_id=False, + base_path='/schemas/images', + ) def get_image_schema(self): """Get single image schema @@ -899,8 +961,11 @@ def get_image_schema(self): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get(_schema.Schema, requires_id=False, - base_path='/schemas/image') + return self._get( + _schema.Schema, + requires_id=False, + base_path='/schemas/image', + ) def get_members_schema(self): """Get image members schema @@ -909,8 +974,11 @@ def get_members_schema(self): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get(_schema.Schema, requires_id=False, - base_path='/schemas/members') + return self._get( + _schema.Schema, + requires_id=False, + base_path='/schemas/members', + ) def get_member_schema(self): """Get image member schema @@ -919,8 +987,11 @@ def get_member_schema(self): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get(_schema.Schema, requires_id=False, - base_path='/schemas/member') + return self._get( + _schema.Schema, + requires_id=False, + base_path='/schemas/member', + ) def get_tasks_schema(self): """Get image tasks schema @@ -929,8 +1000,11 @@ def get_tasks_schema(self): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get(_schema.Schema, requires_id=False, - base_path='/schemas/tasks') + return self._get( + _schema.Schema, + requires_id=False, + base_path='/schemas/tasks', + ) def get_task_schema(self): """Get image task schema @@ -939,8 +1013,11 @@ def get_task_schema(self): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get(_schema.Schema, requires_id=False, - base_path='/schemas/task') + return self._get( + _schema.Schema, + requires_id=False, + base_path='/schemas/task', + ) def get_metadef_namespace_schema(self): """Get metadata definition namespace schema @@ -949,8 +1026,11 @@ def get_metadef_namespace_schema(self): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get(_metadef_schema.MetadefSchema, requires_id=False, - base_path='/schemas/metadefs/namespace') + return self._get( + _metadef_schema.MetadefSchema, + requires_id=False, + base_path='/schemas/metadefs/namespace', + ) def get_metadef_namespaces_schema(self): """Get metadata definition namespaces schema @@ -959,8 +1039,11 @@ def get_metadef_namespaces_schema(self): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get(_metadef_schema.MetadefSchema, requires_id=False, - base_path='/schemas/metadefs/namespaces') + return self._get( + _metadef_schema.MetadefSchema, + requires_id=False, + base_path='/schemas/metadefs/namespaces', + ) def get_metadef_resource_type_schema(self): """Get metadata definition resource type association schema @@ -969,8 +1052,11 @@ def get_metadef_resource_type_schema(self): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get(_metadef_schema.MetadefSchema, requires_id=False, - base_path='/schemas/metadefs/resource_type') + return self._get( + _metadef_schema.MetadefSchema, + requires_id=False, + base_path='/schemas/metadefs/resource_type', + ) def get_metadef_resource_types_schema(self): """Get metadata definition resource type associations schema @@ -979,8 +1065,11 @@ def get_metadef_resource_types_schema(self): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get(_metadef_schema.MetadefSchema, requires_id=False, - base_path='/schemas/metadefs/resource_types') + return self._get( + _metadef_schema.MetadefSchema, + requires_id=False, + base_path='/schemas/metadefs/resource_types', + ) def get_metadef_object_schema(self): """Get metadata definition object schema @@ -989,8 +1078,11 @@ def get_metadef_object_schema(self): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get(_metadef_schema.MetadefSchema, requires_id=False, - base_path='/schemas/metadefs/object') + return self._get( + _metadef_schema.MetadefSchema, + requires_id=False, + base_path='/schemas/metadefs/object', + ) def get_metadef_objects_schema(self): """Get metadata definition objects schema @@ -999,8 +1091,11 @@ def get_metadef_objects_schema(self): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get(_metadef_schema.MetadefSchema, requires_id=False, - base_path='/schemas/metadefs/objects') + return self._get( + _metadef_schema.MetadefSchema, + requires_id=False, + base_path='/schemas/metadefs/objects', + ) def get_metadef_property_schema(self): """Get metadata definition property schema @@ -1009,8 +1104,11 @@ def get_metadef_property_schema(self): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get(_metadef_schema.MetadefSchema, requires_id=False, - base_path='/schemas/metadefs/property') + return self._get( + _metadef_schema.MetadefSchema, + requires_id=False, + base_path='/schemas/metadefs/property', + ) def get_metadef_properties_schema(self): """Get metadata definition properties schema @@ -1019,8 +1117,11 @@ def get_metadef_properties_schema(self): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get(_metadef_schema.MetadefSchema, requires_id=False, - base_path='/schemas/metadefs/properties') + return self._get( + _metadef_schema.MetadefSchema, + requires_id=False, + base_path='/schemas/metadefs/properties', + ) def get_metadef_tag_schema(self): """Get metadata definition tag schema @@ -1029,8 +1130,11 @@ def get_metadef_tag_schema(self): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get(_metadef_schema.MetadefSchema, requires_id=False, - base_path='/schemas/metadefs/tag') + return self._get( + _metadef_schema.MetadefSchema, + requires_id=False, + base_path='/schemas/metadefs/tag', + ) def get_metadef_tags_schema(self): """Get metadata definition tags schema @@ -1039,8 +1143,11 @@ def get_metadef_tags_schema(self): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get(_metadef_schema.MetadefSchema, requires_id=False, - base_path='/schemas/metadefs/tags') + return self._get( + _metadef_schema.MetadefSchema, + requires_id=False, + base_path='/schemas/metadefs/tags', + ) # ====== TASKS ====== def tasks(self, **query): @@ -1078,8 +1185,14 @@ def create_task(self, **attrs): """ return self._create(_task.Task, **attrs) - def wait_for_task(self, task, status='success', failures=None, - interval=2, wait=120): + def wait_for_task( + self, + task, + status='success', + failures=None, + interval=2, + wait=120, + ): """Wait for a task to be in a particular status. :param task: The resource to wait on to reach the specified status. @@ -1110,18 +1223,20 @@ def wait_for_task(self, task, status='success', failures=None, name = "{res}:{id}".format(res=task.__class__.__name__, id=task.id) msg = "Timeout waiting for {name} to transition to {status}".format( - name=name, status=status) + name=name, status=status + ) for count in utils.iterate_timeout( - timeout=wait, - message=msg, - wait=interval): + timeout=wait, message=msg, wait=interval + ): task = task.fetch(self) if not task: raise exceptions.ResourceFailure( "{name} went away while waiting for {status}".format( - name=name, status=status)) + name=name, status=status + ) + ) new_status = task.status normalized_status = new_status.lower() @@ -1129,16 +1244,23 @@ def wait_for_task(self, task, status='success', failures=None, return task elif normalized_status in failures: if task.message == _IMAGE_ERROR_396: - task_args = dict(input=task.input, type=task.type) + task_args = {'input': task.input, 'type': task.type} task = self.create_task(**task_args) self.log.debug('Got error 396. Recreating task %s' % task) else: raise exceptions.ResourceFailure( "{name} transitioned to failure state {status}".format( - name=name, status=new_status)) + name=name, status=new_status + ) + ) - self.log.debug('Still waiting for resource %s to reach state %s, ' - 'current state is %s', name, status, new_status) + self.log.debug( + 'Still waiting for resource %s to reach state %s, ' + 'current state is %s', + name, + status, + new_status, + ) # ====== STORES ====== def stores(self, details=False, **query): From 6462005c7b9678ec3e960a34ec44cbe5ab91394c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 8 Dec 2022 18:03:14 +0000 Subject: [PATCH 3182/3836] image: Remove _base_proxy module These have diverged so significantly that there isn't really any benefit in keeping them together any longer. This change is purely code motion: a future change will remove the now unnecessary abstractions. Change-Id: Ic20dc90be983c03a3166debeabe4af4587341723 Signed-off-by: Stephen Finucane --- openstack/image/_base_proxy.py | 321 --------------------------------- openstack/image/v1/_proxy.py | 308 +++++++++++++++++++++++++++++-- openstack/image/v2/_proxy.py | 317 +++++++++++++++++++++++++++++--- 3 files changed, 590 insertions(+), 356 deletions(-) delete mode 100644 openstack/image/_base_proxy.py diff --git a/openstack/image/_base_proxy.py b/openstack/image/_base_proxy.py deleted file mode 100644 index 71267f4de..000000000 --- a/openstack/image/_base_proxy.py +++ /dev/null @@ -1,321 +0,0 @@ -# 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 os - -from openstack import exceptions -from openstack import proxy -from openstack import utils - - -def _get_name_and_filename(name, image_format): - # See if name points to an existing file - if os.path.exists(name): - # Neat. Easy enough - return os.path.splitext(os.path.basename(name))[0], name - - # Try appending the disk format - name_with_ext = '.'.join((name, image_format)) - if os.path.exists(name_with_ext): - return os.path.basename(name), name_with_ext - - return name, None - - -class BaseImageProxy(proxy.Proxy, metaclass=abc.ABCMeta): - - retriable_status_codes = [503] - - _IMAGE_MD5_KEY = 'owner_specified.openstack.md5' - _IMAGE_SHA256_KEY = 'owner_specified.openstack.sha256' - _IMAGE_OBJECT_KEY = 'owner_specified.openstack.object' - - # NOTE(shade) shade keys were owner_specified.shade.md5 - we need to add - # those to freshness checks so that a shade->sdk transition - # doesn't result in a re-upload - _SHADE_IMAGE_MD5_KEY = 'owner_specified.shade.md5' - _SHADE_IMAGE_SHA256_KEY = 'owner_specified.shade.sha256' - _SHADE_IMAGE_OBJECT_KEY = 'owner_specified.shade.object' - - # ====== IMAGES ====== - def create_image( - self, - name, - filename=None, - container=None, - md5=None, - sha256=None, - disk_format=None, - container_format=None, - disable_vendor_agent=True, - allow_duplicates=False, - meta=None, - wait=False, - timeout=3600, - data=None, - validate_checksum=False, - use_import=False, - stores=None, - tags=None, - all_stores=None, - all_stores_must_succeed=None, - **kwargs, - ): - """Upload an image. - - :param str name: Name of the image to create. If it is a pathname - of an image, the name will be constructed from the extensionless - basename of the path. - :param str filename: The path to the file to upload, if needed. - (optional, defaults to None) - :param data: Image data (string or file-like object). It is mutually - exclusive with filename - :param str container: Name of the container in swift where images - should be uploaded for import if the cloud requires such a thing. - (optional, defaults to 'images') - :param str md5: md5 sum of the image file. If not given, an md5 will - be calculated. - :param str sha256: sha256 sum of the image file. If not given, an md5 - will be calculated. - :param str disk_format: The disk format the image is in. (optional, - defaults to the os-client-config config value for this cloud) - :param str container_format: The container format the image is in. - (optional, defaults to the os-client-config config value for this - cloud) - :param list tags: List of tags for this image. Each tag is a string - of at most 255 chars. - :param bool disable_vendor_agent: Whether or not to append metadata - flags to the image to inform the cloud in question to not expect a - vendor agent to be runing. (optional, defaults to True) - :param allow_duplicates: If true, skips checks that enforce unique - image name. (optional, defaults to False) - :param meta: A dict of key/value pairs to use for metadata that - bypasses automatic type conversion. - :param bool wait: If true, waits for image to be created. Defaults to - true - however, be aware that one of the upload methods is always - synchronous. - :param timeout: Seconds to wait for image creation. None is forever. - :param bool validate_checksum: If true and cloud returns checksum, - compares return value with the one calculated or passed into this - call. If value does not match - raises exception. Default is - 'false' - :param bool use_import: Use the interoperable image import mechanism - to import the image. This defaults to false because it is harder on - the target cloud so should only be used when needed, such as when - the user needs the cloud to transform image format. If the cloud - has disabled direct uploads, this will default to true. - :param stores: List of stores to be used when enabled_backends is - activated in glance. List values can be the id of a store or a - :class:`~openstack.image.v2.service_info.Store` instance. - Implies ``use_import`` equals ``True``. - :param all_stores: Upload to all available stores. Mutually exclusive - with ``store`` and ``stores``. - Implies ``use_import`` equals ``True``. - :param all_stores_must_succeed: When set to True, if an error occurs - during the upload in at least one store, the worfklow fails, the - data is deleted from stores where copying is done (not staging), - and the state of the image is unchanged. When set to False, the - workflow will fail (data deleted from stores, …) only if the import - fails on all stores specified by the user. In case of a partial - success, the locations added to the image will be the stores where - the data has been correctly uploaded. - Default is True. - Implies ``use_import`` equals ``True``. - - Additional kwargs will be passed to the image creation as additional - metadata for the image and will have all values converted to string - except for min_disk, min_ram, size and virtual_size which will be - converted to int. - - If you are sure you have all of your data types correct or have an - advanced need to be explicit, use meta. If you are just a normal - consumer, using kwargs is likely the right choice. - - If a value is in meta and kwargs, meta wins. - - :returns: A ``munch.Munch`` of the Image object - :raises: SDKException if there are problems uploading - """ - if container is None: - container = self._connection._OBJECT_AUTOCREATE_CONTAINER - - if not meta: - meta = {} - - if not disk_format: - disk_format = self._connection.config.config['image_format'] - - if not container_format: - # https://docs.openstack.org/image-guide/image-formats.html - container_format = 'bare' - - if data and filename: - raise exceptions.SDKException( - 'Passing filename and data simultaneously is not supported' - ) - - # If there is no filename, see if name is actually the filename - if not filename and not data: - name, filename = _get_name_and_filename( - name, - self._connection.config.config['image_format'], - ) - - if validate_checksum and data and not isinstance(data, bytes): - raise exceptions.SDKException( - 'Validating checksum is not possible when data is not a ' - 'direct binary object' - ) - - if not (md5 or sha256) and validate_checksum: - if filename: - md5, sha256 = utils._get_file_hashes(filename) - elif data and isinstance(data, bytes): - md5, sha256 = utils._calculate_data_hashes(data) - - if allow_duplicates: - current_image = None - else: - current_image = self.find_image(name) - if current_image: - # NOTE(pas-ha) 'properties' may be absent or be None - props = current_image.get('properties') or {} - md5_key = props.get( - self._IMAGE_MD5_KEY, - props.get(self._SHADE_IMAGE_MD5_KEY, ''), - ) - sha256_key = props.get( - self._IMAGE_SHA256_KEY, - props.get(self._SHADE_IMAGE_SHA256_KEY, ''), - ) - up_to_date = utils._hashes_up_to_date( - md5=md5, - sha256=sha256, - md5_key=md5_key, - sha256_key=sha256_key, - ) - if up_to_date: - self.log.debug( - "image %(name)s exists and is up to date", - {'name': name}, - ) - return current_image - else: - self.log.debug( - "image %(name)s exists, but contains different " - "checksums. Updating.", - {'name': name}, - ) - - if disable_vendor_agent: - kwargs.update( - self._connection.config.config['disable_vendor_agent'] - ) - - # If a user used the v1 calling format, they will have - # passed a dict called properties along - properties = kwargs.pop('properties', {}) - properties[self._IMAGE_MD5_KEY] = md5 or '' - properties[self._IMAGE_SHA256_KEY] = sha256 or '' - properties[self._IMAGE_OBJECT_KEY] = '/'.join([container, name]) - kwargs.update(properties) - image_kwargs = {'properties': kwargs} - if disk_format: - image_kwargs['disk_format'] = disk_format - if container_format: - image_kwargs['container_format'] = container_format - if tags: - image_kwargs['tags'] = tags - - if filename or data: - image = self._upload_image( - name, - filename=filename, - data=data, - meta=meta, - wait=wait, - timeout=timeout, - validate_checksum=validate_checksum, - use_import=use_import, - stores=stores, - all_stores=stores, - all_stores_must_succeed=stores, - **image_kwargs, - ) - else: - image_kwargs['name'] = name - image = self._create_image(**image_kwargs) - - self._connection._get_cache(None).invalidate() - - return image - - @abc.abstractmethod - def _create_image(self, name, **image_kwargs): - pass - - @abc.abstractmethod - def _upload_image( - self, - name, - filename, - data, - meta, - wait, - timeout, - validate_checksum=True, - use_import=False, - stores=None, - all_stores=None, - all_stores_must_succeed=None, - **image_kwargs, - ): - pass - - @abc.abstractmethod - def _update_image_properties(self, image, meta, properties): - pass - - def update_image_properties( - self, - image=None, - meta=None, - **kwargs, - ): - """ - Update the properties of an existing image. - - :param image: Name or id of an image or an Image object. - :param meta: A dict of key/value pairs to use for metadata that - bypasses automatic type conversion. - - Additional kwargs will be passed to the image creation as additional - metadata for the image and will have all values converted to string - except for min_disk, min_ram, size and virtual_size which will be - converted to int. - """ - - if isinstance(image, str): - image = self._connection.get_image(image) - - if not meta: - meta = {} - - img_props = {} - for k, v in iter(kwargs.items()): - if v and k in ['ramdisk', 'kernel']: - v = self._connection.get_image_id(v) - k = '{0}_id'.format(k) - img_props[k] = v - - return self._update_image_properties(image, meta, img_props) diff --git a/openstack/image/v1/_proxy.py b/openstack/image/v1/_proxy.py index bdbdded47..fd8f7dde8 100644 --- a/openstack/image/v1/_proxy.py +++ b/openstack/image/v1/_proxy.py @@ -9,15 +9,261 @@ # 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 import warnings from openstack.cloud import exc from openstack import exceptions -from openstack.image import _base_proxy from openstack.image.v1 import image as _image +from openstack import proxy +from openstack import utils + + +def _get_name_and_filename(name, image_format): + # See if name points to an existing file + if os.path.exists(name): + # Neat. Easy enough + return os.path.splitext(os.path.basename(name))[0], name + + # Try appending the disk format + name_with_ext = '.'.join((name, image_format)) + if os.path.exists(name_with_ext): + return os.path.basename(name), name_with_ext + + return name, None + + +class Proxy(proxy.Proxy): + retriable_status_codes = [503] + + _IMAGE_MD5_KEY = 'owner_specified.openstack.md5' + _IMAGE_SHA256_KEY = 'owner_specified.openstack.sha256' + _IMAGE_OBJECT_KEY = 'owner_specified.openstack.object' + + # NOTE(shade) shade keys were owner_specified.shade.md5 - we need to add + # those to freshness checks so that a shade->sdk transition + # doesn't result in a re-upload + _SHADE_IMAGE_MD5_KEY = 'owner_specified.shade.md5' + _SHADE_IMAGE_SHA256_KEY = 'owner_specified.shade.sha256' + _SHADE_IMAGE_OBJECT_KEY = 'owner_specified.shade.object' + + # ====== IMAGES ====== + def create_image( + self, + name, + filename=None, + container=None, + md5=None, + sha256=None, + disk_format=None, + container_format=None, + disable_vendor_agent=True, + allow_duplicates=False, + meta=None, + wait=False, + timeout=3600, + data=None, + validate_checksum=False, + use_import=False, + stores=None, + tags=None, + all_stores=None, + all_stores_must_succeed=None, + **kwargs, + ): + """Upload an image. + + :param str name: Name of the image to create. If it is a pathname + of an image, the name will be constructed from the extensionless + basename of the path. + :param str filename: The path to the file to upload, if needed. + (optional, defaults to None) + :param data: Image data (string or file-like object). It is mutually + exclusive with filename + :param str container: Name of the container in swift where images + should be uploaded for import if the cloud requires such a thing. + (optional, defaults to 'images') + :param str md5: md5 sum of the image file. If not given, an md5 will + be calculated. + :param str sha256: sha256 sum of the image file. If not given, an md5 + will be calculated. + :param str disk_format: The disk format the image is in. (optional, + defaults to the os-client-config config value for this cloud) + :param str container_format: The container format the image is in. + (optional, defaults to the os-client-config config value for this + cloud) + :param list tags: List of tags for this image. Each tag is a string + of at most 255 chars. + :param bool disable_vendor_agent: Whether or not to append metadata + flags to the image to inform the cloud in question to not expect a + vendor agent to be runing. (optional, defaults to True) + :param allow_duplicates: If true, skips checks that enforce unique + image name. (optional, defaults to False) + :param meta: A dict of key/value pairs to use for metadata that + bypasses automatic type conversion. + :param bool wait: If true, waits for image to be created. Defaults to + true - however, be aware that one of the upload methods is always + synchronous. + :param timeout: Seconds to wait for image creation. None is forever. + :param bool validate_checksum: If true and cloud returns checksum, + compares return value with the one calculated or passed into this + call. If value does not match - raises exception. Default is + 'false' + :param bool use_import: Use the interoperable image import mechanism + to import the image. This defaults to false because it is harder on + the target cloud so should only be used when needed, such as when + the user needs the cloud to transform image format. If the cloud + has disabled direct uploads, this will default to true. + :param stores: + List of stores to be used when enabled_backends is activated + in glance. List values can be the id of a store or a + :class:`~openstack.image.v2.service_info.Store` instance. + Implies ``use_import`` equals ``True``. + :param all_stores: + Upload to all available stores. Mutually exclusive with + ``store`` and ``stores``. + Implies ``use_import`` equals ``True``. + :param all_stores_must_succeed: + When set to True, if an error occurs during the upload in at + least one store, the worfklow fails, the data is deleted + from stores where copying is done (not staging), and the + state of the image is unchanged. When set to False, the + workflow will fail (data deleted from stores, …) only if the + import fails on all stores specified by the user. In case of + a partial success, the locations added to the image will be + the stores where the data has been correctly uploaded. + Default is True. + Implies ``use_import`` equals ``True``. + + Additional kwargs will be passed to the image creation as additional + metadata for the image and will have all values converted to string + except for min_disk, min_ram, size and virtual_size which will be + converted to int. + + If you are sure you have all of your data types correct or have an + advanced need to be explicit, use meta. If you are just a normal + consumer, using kwargs is likely the right choice. + + If a value is in meta and kwargs, meta wins. + + :returns: A ``munch.Munch`` of the Image object + :raises: SDKException if there are problems uploading + """ + if container is None: + container = self._connection._OBJECT_AUTOCREATE_CONTAINER + + if not meta: + meta = {} + if not disk_format: + disk_format = self._connection.config.config['image_format'] + + if not container_format: + # https://docs.openstack.org/image-guide/image-formats.html + container_format = 'bare' + + if data and filename: + raise exceptions.SDKException( + 'Passing filename and data simultaneously is not supported' + ) + + # If there is no filename, see if name is actually the filename + if not filename and not data: + name, filename = _get_name_and_filename( + name, + self._connection.config.config['image_format'], + ) + + if validate_checksum and data and not isinstance(data, bytes): + raise exceptions.SDKException( + 'Validating checksum is not possible when data is not a ' + 'direct binary object' + ) + + if not (md5 or sha256) and validate_checksum: + if filename: + md5, sha256 = utils._get_file_hashes(filename) + elif data and isinstance(data, bytes): + md5, sha256 = utils._calculate_data_hashes(data) + + if allow_duplicates: + current_image = None + else: + current_image = self.find_image(name) + if current_image: + # NOTE(pas-ha) 'properties' may be absent or be None + props = current_image.get('properties') or {} + md5_key = props.get( + self._IMAGE_MD5_KEY, + props.get(self._SHADE_IMAGE_MD5_KEY, ''), + ) + sha256_key = props.get( + self._IMAGE_SHA256_KEY, + props.get(self._SHADE_IMAGE_SHA256_KEY, ''), + ) + up_to_date = utils._hashes_up_to_date( + md5=md5, + sha256=sha256, + md5_key=md5_key, + sha256_key=sha256_key, + ) + if up_to_date: + self.log.debug( + "image %(name)s exists and is up to date", + {'name': name}, + ) + return current_image + else: + self.log.debug( + "image %(name)s exists, but contains different " + "checksums. Updating.", + {'name': name}, + ) + + if disable_vendor_agent: + kwargs.update( + self._connection.config.config['disable_vendor_agent'] + ) + + # If a user used the v1 calling format, they will have + # passed a dict called properties along + properties = kwargs.pop('properties', {}) + properties[self._IMAGE_MD5_KEY] = md5 or '' + properties[self._IMAGE_SHA256_KEY] = sha256 or '' + properties[self._IMAGE_OBJECT_KEY] = '/'.join([container, name]) + kwargs.update(properties) + image_kwargs = {'properties': kwargs} + if disk_format: + image_kwargs['disk_format'] = disk_format + if container_format: + image_kwargs['container_format'] = container_format + if tags: + image_kwargs['tags'] = tags + + if filename or data: + image = self._upload_image( + name, + filename=filename, + data=data, + meta=meta, + wait=wait, + timeout=timeout, + validate_checksum=validate_checksum, + use_import=use_import, + stores=stores, + all_stores=stores, + all_stores_must_succeed=stores, + **image_kwargs, + ) + else: + image_kwargs['name'] = name + image = self._create_image(**image_kwargs) + + self._connection._get_cache(None).invalidate() + + return image -class Proxy(_base_proxy.BaseImageProxy): def _create_image(self, **kwargs): """Create image resource from attributes""" return self._create(_image.Image, **kwargs) @@ -107,18 +353,6 @@ def _upload_image( raise return image - def _update_image_properties(self, image, meta, properties): - properties.update(meta) - img_props = {} - for k, v in iter(properties.items()): - if image.properties.get(k, None) != v: - img_props['x-image-meta-{key}'.format(key=k)] = v - if not img_props: - return False - self.put('/images/{id}'.format(id=image.id), headers=img_props) - self._connection.list_images.invalidate(self._connection) - return True - def _existing_image(self, **kwargs): return _image.Image.existing(connection=self._connection, **kwargs) @@ -234,3 +468,49 @@ def download_image( output=output, chunk_size=chunk_size, ) + + def _update_image_properties(self, image, meta, properties): + properties.update(meta) + img_props = {} + for k, v in iter(properties.items()): + if image.properties.get(k, None) != v: + img_props['x-image-meta-{key}'.format(key=k)] = v + if not img_props: + return False + self.put('/images/{id}'.format(id=image.id), headers=img_props) + self._connection.list_images.invalidate(self._connection) + return True + + def update_image_properties( + self, + image=None, + meta=None, + **kwargs, + ): + """ + Update the properties of an existing image. + + :param image: Name or id of an image or an Image object. + :param meta: A dict of key/value pairs to use for metadata that + bypasses automatic type conversion. + + Additional kwargs will be passed to the image creation as additional + metadata for the image and will have all values converted to string + except for min_disk, min_ram, size and virtual_size which will be + converted to int. + """ + + if isinstance(image, str): + image = self._connection.get_image(image) + + if not meta: + meta = {} + + img_props = {} + for k, v in iter(kwargs.items()): + if v and k in ['ramdisk', 'kernel']: + v = self._connection.get_image_id(v) + k = '{0}_id'.format(k) + img_props[k] = v + + return self._update_image_properties(image, meta, img_props) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 3a7640e8d..d62d2be82 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -10,11 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +import os import time import warnings from openstack import exceptions -from openstack.image import _base_proxy from openstack.image.v2 import image as _image from openstack.image.v2 import member as _member from openstack.image.v2 import metadef_namespace as _metadef_namespace @@ -23,6 +23,7 @@ from openstack.image.v2 import schema as _schema from openstack.image.v2 import service_info as _si from openstack.image.v2 import task as _task +from openstack import proxy from openstack import resource from openstack import utils @@ -32,7 +33,22 @@ _RAW_PROPERTIES = ('is_protected', 'protected', 'tags') -class Proxy(_base_proxy.BaseImageProxy): +def _get_name_and_filename(name, image_format): + # See if name points to an existing file + if os.path.exists(name): + # Neat. Easy enough + return os.path.splitext(os.path.basename(name))[0], name + + # Try appending the disk format + name_with_ext = '.'.join((name, image_format)) + if os.path.exists(name_with_ext): + return os.path.basename(name), name_with_ext + + return name, None + + +class Proxy(proxy.Proxy): + _resource_registry = { "image": _image.Image, "image_member": _member.Member, @@ -43,7 +59,232 @@ class Proxy(_base_proxy.BaseImageProxy): "task": _task.Task, } + retriable_status_codes = [503] + + _IMAGE_MD5_KEY = 'owner_specified.openstack.md5' + _IMAGE_SHA256_KEY = 'owner_specified.openstack.sha256' + _IMAGE_OBJECT_KEY = 'owner_specified.openstack.object' + + # NOTE(shade) shade keys were owner_specified.shade.md5 - we need to add + # those to freshness checks so that a shade->sdk transition + # doesn't result in a re-upload + _SHADE_IMAGE_MD5_KEY = 'owner_specified.shade.md5' + _SHADE_IMAGE_SHA256_KEY = 'owner_specified.shade.sha256' + _SHADE_IMAGE_OBJECT_KEY = 'owner_specified.shade.object' + # ====== IMAGES ====== + def create_image( + self, + name, + filename=None, + container=None, + md5=None, + sha256=None, + disk_format=None, + container_format=None, + disable_vendor_agent=True, + allow_duplicates=False, + meta=None, + wait=False, + timeout=3600, + data=None, + validate_checksum=False, + use_import=False, + stores=None, + tags=None, + all_stores=None, + all_stores_must_succeed=None, + **kwargs, + ): + """Upload an image. + + :param str name: Name of the image to create. If it is a pathname + of an image, the name will be constructed from the extensionless + basename of the path. + :param str filename: The path to the file to upload, if needed. + (optional, defaults to None) + :param data: Image data (string or file-like object). It is mutually + exclusive with filename + :param str container: Name of the container in swift where images + should be uploaded for import if the cloud requires such a thing. + (optional, defaults to 'images') + :param str md5: md5 sum of the image file. If not given, an md5 will + be calculated. + :param str sha256: sha256 sum of the image file. If not given, an md5 + will be calculated. + :param str disk_format: The disk format the image is in. (optional, + defaults to the os-client-config config value for this cloud) + :param str container_format: The container format the image is in. + (optional, defaults to the os-client-config config value for this + cloud) + :param list tags: List of tags for this image. Each tag is a string + of at most 255 chars. + :param bool disable_vendor_agent: Whether or not to append metadata + flags to the image to inform the cloud in question to not expect a + vendor agent to be runing. (optional, defaults to True) + :param allow_duplicates: If true, skips checks that enforce unique + image name. (optional, defaults to False) + :param meta: A dict of key/value pairs to use for metadata that + bypasses automatic type conversion. + :param bool wait: If true, waits for image to be created. Defaults to + true - however, be aware that one of the upload methods is always + synchronous. + :param timeout: Seconds to wait for image creation. None is forever. + :param bool validate_checksum: If true and cloud returns checksum, + compares return value with the one calculated or passed into this + call. If value does not match - raises exception. Default is + 'false' + :param bool use_import: Use the interoperable image import mechanism + to import the image. This defaults to false because it is harder on + the target cloud so should only be used when needed, such as when + the user needs the cloud to transform image format. If the cloud + has disabled direct uploads, this will default to true. + :param stores: List of stores to be used when enabled_backends is + activated in glance. List values can be the id of a store or a + :class:`~openstack.image.v2.service_info.Store` instance. + Implies ``use_import`` equals ``True``. + :param all_stores: Upload to all available stores. Mutually exclusive + with ``store`` and ``stores``. + Implies ``use_import`` equals ``True``. + :param all_stores_must_succeed: When set to True, if an error occurs + during the upload in at least one store, the worfklow fails, the + data is deleted from stores where copying is done (not staging), + and the state of the image is unchanged. When set to False, the + workflow will fail (data deleted from stores, …) only if the import + fails on all stores specified by the user. In case of a partial + success, the locations added to the image will be the stores where + the data has been correctly uploaded. + Default is True. + Implies ``use_import`` equals ``True``. + + Additional kwargs will be passed to the image creation as additional + metadata for the image and will have all values converted to string + except for min_disk, min_ram, size and virtual_size which will be + converted to int. + + If you are sure you have all of your data types correct or have an + advanced need to be explicit, use meta. If you are just a normal + consumer, using kwargs is likely the right choice. + + If a value is in meta and kwargs, meta wins. + + :returns: A ``munch.Munch`` of the Image object + :raises: SDKException if there are problems uploading + """ + if container is None: + container = self._connection._OBJECT_AUTOCREATE_CONTAINER + + if not meta: + meta = {} + + if not disk_format: + disk_format = self._connection.config.config['image_format'] + + if not container_format: + # https://docs.openstack.org/image-guide/image-formats.html + container_format = 'bare' + + if data and filename: + raise exceptions.SDKException( + 'Passing filename and data simultaneously is not supported' + ) + + # If there is no filename, see if name is actually the filename + if not filename and not data: + name, filename = _get_name_and_filename( + name, + self._connection.config.config['image_format'], + ) + + if validate_checksum and data and not isinstance(data, bytes): + raise exceptions.SDKException( + 'Validating checksum is not possible when data is not a ' + 'direct binary object' + ) + + if not (md5 or sha256) and validate_checksum: + if filename: + md5, sha256 = utils._get_file_hashes(filename) + elif data and isinstance(data, bytes): + md5, sha256 = utils._calculate_data_hashes(data) + + if allow_duplicates: + current_image = None + else: + current_image = self.find_image(name) + if current_image: + # NOTE(pas-ha) 'properties' may be absent or be None + props = current_image.get('properties') or {} + md5_key = props.get( + self._IMAGE_MD5_KEY, + props.get(self._SHADE_IMAGE_MD5_KEY, ''), + ) + sha256_key = props.get( + self._IMAGE_SHA256_KEY, + props.get(self._SHADE_IMAGE_SHA256_KEY, ''), + ) + up_to_date = utils._hashes_up_to_date( + md5=md5, + sha256=sha256, + md5_key=md5_key, + sha256_key=sha256_key, + ) + if up_to_date: + self.log.debug( + "image %(name)s exists and is up to date", + {'name': name}, + ) + return current_image + else: + self.log.debug( + "image %(name)s exists, but contains different " + "checksums. Updating.", + {'name': name}, + ) + + if disable_vendor_agent: + kwargs.update( + self._connection.config.config['disable_vendor_agent'] + ) + + # If a user used the v1 calling format, they will have + # passed a dict called properties along + properties = kwargs.pop('properties', {}) + properties[self._IMAGE_MD5_KEY] = md5 or '' + properties[self._IMAGE_SHA256_KEY] = sha256 or '' + properties[self._IMAGE_OBJECT_KEY] = '/'.join([container, name]) + kwargs.update(properties) + image_kwargs = {'properties': kwargs} + if disk_format: + image_kwargs['disk_format'] = disk_format + if container_format: + image_kwargs['container_format'] = container_format + if tags: + image_kwargs['tags'] = tags + + if filename or data: + image = self._upload_image( + name, + filename=filename, + data=data, + meta=meta, + wait=wait, + timeout=timeout, + validate_checksum=validate_checksum, + use_import=use_import, + stores=stores, + all_stores=stores, + all_stores_must_succeed=stores, + **image_kwargs, + ) + else: + image_kwargs['name'] = name + image = self._create_image(**image_kwargs) + + self._connection._get_cache(None).invalidate() + + return image + def _create_image(self, **kwargs): """Create image resource from attributes""" return self._create(_image.Image, **kwargs) @@ -462,25 +703,6 @@ def _upload_image_task( else: return glance_task - def _update_image_properties(self, image, meta, properties): - if not isinstance(image, _image.Image): - # If we come here with a dict (cloud) - convert dict to real object - # to properly consume all properties (to calculate the diff). - # This currently happens from unittests. - image = _image.Image.existing(**image) - img_props = image.properties.copy() - - for k, v in iter(self._make_v2_image_params(meta, properties).items()): - if image.get(k, None) != v: - img_props[k] = v - if not img_props: - return False - - self.update_image(image, **img_props) - - self._connection.list_images.invalidate(self._connection) - return True - def _existing_image(self, **kwargs): return _image.Image.existing(connection=self._connection, **kwargs) @@ -624,6 +846,59 @@ def reactivate_image(self, image): image = self._get_resource(_image.Image, image) image.reactivate(self) + def _update_image_properties(self, image, meta, properties): + if not isinstance(image, _image.Image): + # If we come here with a dict (cloud) - convert dict to real object + # to properly consume all properties (to calculate the diff). + # This currently happens from unittests. + image = _image.Image.existing(**image) + img_props = image.properties.copy() + + for k, v in iter(self._make_v2_image_params(meta, properties).items()): + if image.get(k, None) != v: + img_props[k] = v + if not img_props: + return False + + self.update_image(image, **img_props) + + self._connection.list_images.invalidate(self._connection) + return True + + def update_image_properties( + self, + image=None, + meta=None, + **kwargs, + ): + """ + Update the properties of an existing image. + + :param image: Name or id of an image or an Image object. + :param meta: A dict of key/value pairs to use for metadata that + bypasses automatic type conversion. + + Additional kwargs will be passed to the image creation as additional + metadata for the image and will have all values converted to string + except for min_disk, min_ram, size and virtual_size which will be + converted to int. + """ + + if isinstance(image, str): + image = self._connection.get_image(image) + + if not meta: + meta = {} + + img_props = {} + for k, v in iter(kwargs.items()): + if v and k in ['ramdisk', 'kernel']: + v = self._connection.get_image_id(v) + k = '{0}_id'.format(k) + img_props[k] = v + + return self._update_image_properties(image, meta, img_props) + def add_tag(self, image, tag): """Add a tag to an image From d99fc11214f732bafdc07a9800fabfd5d1624ff3 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 8 Dec 2022 12:01:08 +0000 Subject: [PATCH 3183/3836] image: Remove unnecessary abstractions Change-Id: I3b09deddbc3914d3bd43888465fe6948f88a36ff --- openstack/image/v1/_proxy.py | 6 +- openstack/image/v2/_proxy.py | 67 ++++++++++----------- openstack/tests/unit/image/v2/test_proxy.py | 39 ++++++------ 3 files changed, 54 insertions(+), 58 deletions(-) diff --git a/openstack/image/v1/_proxy.py b/openstack/image/v1/_proxy.py index fd8f7dde8..aa503ca5b 100644 --- a/openstack/image/v1/_proxy.py +++ b/openstack/image/v1/_proxy.py @@ -258,16 +258,12 @@ def create_image( ) else: image_kwargs['name'] = name - image = self._create_image(**image_kwargs) + image = self._create(_image.Image, **kwargs) self._connection._get_cache(None).invalidate() return image - def _create_image(self, **kwargs): - """Create image resource from attributes""" - return self._create(_image.Image, **kwargs) - def upload_image(self, **attrs): """Upload a new image from attributes diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index d62d2be82..54cafd096 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -279,16 +279,12 @@ def create_image( ) else: image_kwargs['name'] = name - image = self._create_image(**image_kwargs) + image = self._create(_image.Image, **image_kwargs) self._connection._get_cache(None).invalidate() return image - def _create_image(self, **kwargs): - """Create image resource from attributes""" - return self._create(_image.Image, **kwargs) - def import_image( self, image, @@ -347,10 +343,12 @@ def import_image( :returns: The raw response from the request. """ image = self._get_resource(_image.Image, image) + if all_stores and (store or stores): raise exceptions.InvalidRequest( "all_stores is mutually exclusive with store and stores" ) + if store is not None: if stores: raise exceptions.InvalidRequest( @@ -416,7 +414,11 @@ def stage_image(self, image, filename=None, data=None): return image def upload_image( - self, container_format=None, disk_format=None, data=None, **attrs + self, + container_format=None, + disk_format=None, + data=None, + **attrs, ): """Create and upload a new image from attributes @@ -707,7 +709,11 @@ def _existing_image(self, **kwargs): return _image.Image.existing(connection=self._connection, **kwargs) def download_image( - self, image, stream=False, output=None, chunk_size=1024 + self, + image, + stream=False, + output=None, + chunk_size=1024, ): """Download an image @@ -846,35 +852,16 @@ def reactivate_image(self, image): image = self._get_resource(_image.Image, image) image.reactivate(self) - def _update_image_properties(self, image, meta, properties): - if not isinstance(image, _image.Image): - # If we come here with a dict (cloud) - convert dict to real object - # to properly consume all properties (to calculate the diff). - # This currently happens from unittests. - image = _image.Image.existing(**image) - img_props = image.properties.copy() - - for k, v in iter(self._make_v2_image_params(meta, properties).items()): - if image.get(k, None) != v: - img_props[k] = v - if not img_props: - return False - - self.update_image(image, **img_props) - - self._connection.list_images.invalidate(self._connection) - return True - def update_image_properties( self, image=None, meta=None, **kwargs, ): - """ - Update the properties of an existing image. + """Update the properties of an existing image - :param image: Name or id of an image or an Image object. + :param image: The value can be the ID of a image or a + :class:`~openstack.image.v2.image.Image` instance. :param meta: A dict of key/value pairs to use for metadata that bypasses automatic type conversion. @@ -883,21 +870,31 @@ def update_image_properties( except for min_disk, min_ram, size and virtual_size which will be converted to int. """ - - if isinstance(image, str): - image = self._connection.get_image(image) + image = self._get_resource(_image.Image, image) if not meta: meta = {} - img_props = {} + properties = {} for k, v in iter(kwargs.items()): if v and k in ['ramdisk', 'kernel']: v = self._connection.get_image_id(v) k = '{0}_id'.format(k) - img_props[k] = v + properties[k] = v + + img_props = image.properties.copy() + + for k, v in iter(self._make_v2_image_params(meta, properties).items()): + if image.get(k, None) != v: + img_props[k] = v + if not img_props: + return False - return self._update_image_properties(image, meta, img_props) + self.update_image(image, **img_props) + + self._connection.list_images.invalidate(self._connection) + + return True def add_tag(self, image, tag): """Add a tag to an image diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 0775ff787..926288150 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -82,8 +82,8 @@ def test_image_import(self): }, ) - def test_image_create_conflict(self): - self.assertRaises( + def test_image_create_conflicting_options(self): + exc = self.assertRaises( exceptions.SDKException, self.proxy.create_image, name='fake', @@ -92,6 +92,25 @@ def test_image_create_conflict(self): container='bare', disk_format='raw', ) + self.assertIn('Passing filename and data simultaneously', str(exc)) + + def test_image_create_minimal(self): + self.verify_create( + self.proxy.create_image, + _image.Image, + method_kwargs={ + 'name': 'fake', + 'disk_format': 'fake_dformat', + 'container_format': 'fake_cformat', + 'allow_duplicates': True, + }, + expected_kwargs={ + 'name': 'fake', + 'disk_format': 'fake_dformat', + 'container_format': 'fake_cformat', + 'properties': mock.ANY, + }, + ) def test_image_create_checksum_match(self): fake_image = _image.Image( @@ -228,22 +247,6 @@ def test_image_create_data_binary(self): wait=False, ) - def test_image_create_without_filename(self): - self.proxy._create_image = mock.Mock() - - self.proxy.create_image( - allow_duplicates=True, - name='fake', - disk_format="fake_dformat", - container_format="fake_cformat", - ) - self.proxy._create_image.assert_called_with( - container_format='fake_cformat', - disk_format='fake_dformat', - name='fake', - properties=mock.ANY, - ) - def test_image_create_protected(self): self.proxy.find_image = mock.Mock() From e2e5042d383b0a95dbea3d7072c8f935958f2fa4 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 8 Dec 2022 16:47:25 +0000 Subject: [PATCH 3184/3836] image: Modify signatures of various image methods We also update the docstrings to clarify some methods. Change-Id: If38f900a8386833559474270154b913bbc9a5e87 Signed-off-by: Stephen Finucane --- openstack/image/v2/_proxy.py | 43 ++++++++++++++++----- openstack/tests/unit/image/v2/test_proxy.py | 2 +- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 54cafd096..8fd70b08b 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -76,27 +76,45 @@ class Proxy(proxy.Proxy): def create_image( self, name, + *, filename=None, + data=None, container=None, md5=None, sha256=None, disk_format=None, container_format=None, + tags=None, disable_vendor_agent=True, allow_duplicates=False, meta=None, wait=False, timeout=3600, - data=None, validate_checksum=False, use_import=False, stores=None, - tags=None, all_stores=None, all_stores_must_succeed=None, **kwargs, ): - """Upload an image. + """Create an image and optionally upload data + + Create a new image. If ``filename`` or ``data`` are provided, it will + also upload data to this image. + + Note that uploading image data is actually quite a complicated + procedure. There are three ways to upload an image: + + * Image upload + * Image import + * Image tasks + + If the image tasks API is enabled, this must be used. However, this API + is deprecated since the Image service's Mitaka (12.0.0) release and is + now admin-only. Assuming this API is not enabled, you may choose + between image upload or image import. Image import is more powerful and + allows you to upload data from multiple sources including other glance + instances. It should be preferred on all services that support it. :param str name: Name of the image to create. If it is a pathname of an image, the name will be constructed from the extensionless @@ -134,11 +152,13 @@ def create_image( compares return value with the one calculated or passed into this call. If value does not match - raises exception. Default is 'false' - :param bool use_import: Use the interoperable image import mechanism - to import the image. This defaults to false because it is harder on - the target cloud so should only be used when needed, such as when - the user needs the cloud to transform image format. If the cloud - has disabled direct uploads, this will default to true. + :param bool use_import: Use the 'glance-direct' method of the + interoperable image import mechanism to import the image. This + defaults to false because it is harder on the target cloud so + should only be used when needed, such as when the user needs the + cloud to transform image format. If the cloud has disabled direct + uploads, this will default to true. If you wish to use other import + methods, use the ``import_image`` method instead. :param stores: List of stores to be used when enabled_backends is activated in glance. List values can be the id of a store or a :class:`~openstack.image.v2.service_info.Store` instance. @@ -168,7 +188,8 @@ def create_image( If a value is in meta and kwargs, meta wins. - :returns: A ``munch.Munch`` of the Image object + :returns: The results of image creation + :rtype: :class:`~openstack.image.v2.image.Image` :raises: SDKException if there are problems uploading """ if container is None: @@ -383,7 +404,7 @@ def import_image( all_stores_must_succeed=all_stores_must_succeed, ) - def stage_image(self, image, filename=None, data=None): + def stage_image(self, image, *, filename=None, data=None): """Stage binary image data :param image: The value can be the ID of a image or a @@ -471,6 +492,7 @@ def upload_image( def _upload_image( self, name, + *, filename=None, data=None, meta=None, @@ -711,6 +733,7 @@ def _existing_image(self, **kwargs): def download_image( self, image, + *, stream=False, output=None, chunk_size=1024, diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 926288150..9a5f020f3 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -347,7 +347,7 @@ def test_image_stage_wrong_status(self): exceptions.SDKException, self.proxy.stage_image, image, - "data", + data="data", ) def test_image_delete(self): From 6baa435a0409d4d6b5b00303f2308cd4412f46ad Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 8 Dec 2022 12:17:03 +0000 Subject: [PATCH 3185/3836] image: Remove unsupported parameters from v1 proxy There was an idea to provide the exact same method signature for 'create_image' in the v1 and v2 Image proxy APIs. This was a bad idea as it limits how the v2 version of this method can evolve. Stop doing it. Or rather, try. In reality, nothing really changes and all we're doing is removing the documentation for these unsupported options and reworking the exception logic. The docstring for the 'create_image' function is reworked. Change-Id: I65da2e6795d7fd417dd2ab46c272b36e4ae83b69 Signed-off-by: Stephen Finucane --- openstack/image/v1/_proxy.py | 87 +++++++++++------------------------- 1 file changed, 25 insertions(+), 62 deletions(-) diff --git a/openstack/image/v1/_proxy.py b/openstack/image/v1/_proxy.py index aa503ca5b..522d99a37 100644 --- a/openstack/image/v1/_proxy.py +++ b/openstack/image/v1/_proxy.py @@ -61,20 +61,17 @@ def create_image( disable_vendor_agent=True, allow_duplicates=False, meta=None, - wait=False, - timeout=3600, data=None, validate_checksum=False, - use_import=False, - stores=None, tags=None, - all_stores=None, - all_stores_must_succeed=None, **kwargs, ): - """Upload an image. + """Create an image and optionally upload data. - :param str name: Name of the image to create. If it is a pathname + Create a new image. If ``filename`` or ``data`` are provided, it will + also upload data to this image. + + :param str name: Name of the image to create. If it is a path name of an image, the name will be constructed from the extensionless basename of the path. :param str filename: The path to the file to upload, if needed. @@ -102,39 +99,10 @@ def create_image( image name. (optional, defaults to False) :param meta: A dict of key/value pairs to use for metadata that bypasses automatic type conversion. - :param bool wait: If true, waits for image to be created. Defaults to - true - however, be aware that one of the upload methods is always - synchronous. - :param timeout: Seconds to wait for image creation. None is forever. :param bool validate_checksum: If true and cloud returns checksum, compares return value with the one calculated or passed into this call. If value does not match - raises exception. Default is 'false' - :param bool use_import: Use the interoperable image import mechanism - to import the image. This defaults to false because it is harder on - the target cloud so should only be used when needed, such as when - the user needs the cloud to transform image format. If the cloud - has disabled direct uploads, this will default to true. - :param stores: - List of stores to be used when enabled_backends is activated - in glance. List values can be the id of a store or a - :class:`~openstack.image.v2.service_info.Store` instance. - Implies ``use_import`` equals ``True``. - :param all_stores: - Upload to all available stores. Mutually exclusive with - ``store`` and ``stores``. - Implies ``use_import`` equals ``True``. - :param all_stores_must_succeed: - When set to True, if an error occurs during the upload in at - least one store, the worfklow fails, the data is deleted - from stores where copying is done (not staging), and the - state of the image is unchanged. When set to False, the - workflow will fail (data deleted from stores, …) only if the - import fails on all stores specified by the user. In case of - a partial success, the locations added to the image will be - the stores where the data has been correctly uploaded. - Default is True. - Implies ``use_import`` equals ``True``. Additional kwargs will be passed to the image creation as additional metadata for the image and will have all values converted to string @@ -147,9 +115,27 @@ def create_image( If a value is in meta and kwargs, meta wins. - :returns: A ``munch.Munch`` of the Image object + :returns: The results of image creation + :rtype: :class:`~openstack.image.v1.image.Image` :raises: SDKException if there are problems uploading """ + # these were previously provided for API (method) compatibility; that + # was a bad idea + if ( + 'use_import' in kwargs + or 'stores' in kwargs + or 'all_stores' in kwargs + or 'all_stores_must_succeed' in kwargs + ): + raise exceptions.InvalidRequest( + "Glance v1 does not support stores or image import" + ) + + # silently ignore these; they were never supported and were only given + # for API (method) compatibility + kwargs.pop('wait') + kwargs.pop('timeout') + if container is None: container = self._connection._OBJECT_AUTOCREATE_CONTAINER @@ -247,18 +233,11 @@ def create_image( filename=filename, data=data, meta=meta, - wait=wait, - timeout=timeout, validate_checksum=validate_checksum, - use_import=use_import, - stores=stores, - all_stores=stores, - all_stores_must_succeed=stores, **image_kwargs, ) else: - image_kwargs['name'] = name - image = self._create(_image.Image, **kwargs) + image = self._create(_image.Image, name=name, **kwargs) self._connection._get_cache(None).invalidate() @@ -288,24 +267,8 @@ def _upload_image( filename, data, meta, - wait, - timeout, - use_import=False, - stores=None, - all_stores=None, - all_stores_must_succeed=None, **image_kwargs, ): - if use_import: - raise exceptions.InvalidRequest( - "Glance v1 does not support image import" - ) - if stores or all_stores or all_stores_must_succeed: - raise exceptions.InvalidRequest( - "Glance v1 does not support stores" - ) - # NOTE(mordred) wait and timeout parameters are unused, but - # are present for ease at calling site. if filename and not data: image_data = open(filename, 'rb') else: From 8a8189ad947940b3f8e691781af0467692f7a3c8 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 8 Dec 2022 18:12:26 +0000 Subject: [PATCH 3186/3836] image: Prevent passing conflicts args to stage_image Change-Id: Id6927a7f1e213634a2845b6ac0708c13759a19dc Signed-off-by: Stephen Finucane --- openstack/image/v2/_proxy.py | 23 ++++++++++------ openstack/tests/unit/image/v2/test_proxy.py | 30 ++++++++++++++++++--- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 8fd70b08b..a43abe999 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -192,6 +192,11 @@ def create_image( :rtype: :class:`~openstack.image.v2.image.Image` :raises: SDKException if there are problems uploading """ + if filename and data: + raise exceptions.SDKException( + 'filename and data are mutually exclusive' + ) + if container is None: container = self._connection._OBJECT_AUTOCREATE_CONTAINER @@ -205,11 +210,6 @@ def create_image( # https://docs.openstack.org/image-guide/image-formats.html container_format = 'bare' - if data and filename: - raise exceptions.SDKException( - 'Passing filename and data simultaneously is not supported' - ) - # If there is no filename, see if name is actually the filename if not filename and not data: name, filename = _get_name_and_filename( @@ -415,6 +415,11 @@ def stage_image(self, image, *, filename=None, data=None): :returns: The results of image creation :rtype: :class:`~openstack.image.v2.image.Image` """ + if filename and data: + raise exceptions.SDKException( + 'filename and data are mutually exclusive' + ) + image = self._get_resource(_image.Image, image) if 'queued' != image.status: @@ -583,6 +588,10 @@ def _upload_image_put( all_stores_must_succeed=None, **image_kwargs, ): + # use of any of these imply use_import=True + if stores or all_stores or all_stores_must_succeed: + use_import = True + if filename and not data: image_data = open(filename, 'rb') else: @@ -594,14 +603,12 @@ def _upload_image_put( image_kwargs['name'] = name image = self._create(_image.Image, **image_kwargs) - image.data = image_data + supports_import = ( image.image_import_methods and 'glance-direct' in image.image_import_methods ) - if stores or all_stores or all_stores_must_succeed: - use_import = True if use_import and not supports_import: raise exceptions.SDKException( "Importing image was requested but the cloud does not " diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 9a5f020f3..047eba04c 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -92,7 +92,7 @@ def test_image_create_conflicting_options(self): container='bare', disk_format='raw', ) - self.assertIn('Passing filename and data simultaneously', str(exc)) + self.assertIn('filename and data are mutually exclusive', str(exc)) def test_image_create_minimal(self): self.verify_create( @@ -323,7 +323,7 @@ def test_image_stage(self, mock_fetch): image = _image.Image(id="id", status="queued") image.stage = mock.Mock() - self.proxy.stage_image(image=image) + self.proxy.stage_image(image) mock_fetch.assert_called() image.stage.assert_called_with(self.proxy) @@ -333,22 +333,44 @@ def test_image_stage_with_data(self, mock_fetch): image.stage = mock.Mock() mock_fetch.return_value = image - rv = self.proxy.stage_image(image=image, data="data") + rv = self.proxy.stage_image(image, data="data") image.stage.assert_called_with(self.proxy) mock_fetch.assert_called() self.assertEqual(rv.data, "data") + def test_image_stage_conflicting_options(self): + image = _image.Image(id="id", status="queued") + image.stage = mock.Mock() + + exc = self.assertRaises( + exceptions.SDKException, + self.proxy.stage_image, + image, + filename='foo', + data='data', + ) + self.assertIn( + 'filename and data are mutually exclusive', + str(exc), + ) + image.stage.assert_not_called() + def test_image_stage_wrong_status(self): image = _image.Image(id="id", status="active") image.stage = mock.Mock() - self.assertRaises( + exc = self.assertRaises( exceptions.SDKException, self.proxy.stage_image, image, data="data", ) + self.assertIn( + 'Image stage is only possible for images in the queued state.', + str(exc), + ) + image.stage.assert_not_called() def test_image_delete(self): self.verify_delete(self.proxy.delete_image, _image.Image, False) From b8038e6535d9c2f483829ea3a3213fe3559b6f97 Mon Sep 17 00:00:00 2001 From: elajkat Date: Fri, 6 Jan 2023 17:26:35 +0100 Subject: [PATCH 3187/3836] Add BGP Speakers and Peers to SDK Change-Id: If03254bf43652690fe3bbb106baa1da396247050 Related-Bug: #1999774 --- doc/source/user/proxies/network.rst | 16 ++ doc/source/user/resources/network/index.rst | 2 + .../user/resources/network/v2/bgp_peer.rst | 12 ++ .../user/resources/network/v2/bgp_speaker.rst | 12 ++ openstack/network/v2/_proxy.py | 103 ++++++++++ openstack/network/v2/agent.py | 13 ++ openstack/network/v2/bgp_peer.py | 45 +++++ openstack/network/v2/bgp_speaker.py | 169 ++++++++++++++++ .../tests/functional/network/v2/test_bgp.py | 123 ++++++++++++ openstack/tests/unit/network/v2/test_agent.py | 17 ++ .../tests/unit/network/v2/test_bgp_peer.py | 54 ++++++ .../tests/unit/network/v2/test_bgp_speaker.py | 183 ++++++++++++++++++ openstack/tests/unit/network/v2/test_proxy.py | 52 +++++ ...rk_add_bgp_resources-c182dc2873d6db18.yaml | 10 + 14 files changed, 811 insertions(+) create mode 100644 doc/source/user/resources/network/v2/bgp_peer.rst create mode 100644 doc/source/user/resources/network/v2/bgp_speaker.rst create mode 100644 openstack/network/v2/bgp_peer.py create mode 100644 openstack/network/v2/bgp_speaker.py create mode 100644 openstack/tests/functional/network/v2/test_bgp.py create mode 100644 openstack/tests/unit/network/v2/test_bgp_peer.py create mode 100644 openstack/tests/unit/network/v2/test_bgp_speaker.py create mode 100644 releasenotes/notes/network_add_bgp_resources-c182dc2873d6db18.yaml diff --git a/doc/source/user/proxies/network.rst b/doc/source/user/proxies/network.rst index c35633e45..29d30ce37 100644 --- a/doc/source/user/proxies/network.rst +++ b/doc/source/user/proxies/network.rst @@ -289,3 +289,19 @@ Ndp Proxy Operations :noindex: :members: create_ndp_proxy, get_ndp_proxy, find_ndp_proxy, delete_ndp_proxy, ndp_proxies, update_ndp_proxy + +BGP Operations +^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + :noindex: + :members: create_bgp_peer, delete_bgp_peer, find_bgp_peer, get_bgp_peer, + update_bgp_peer, bgp_peers, create_bgp_speaker, + delete_bgp_speaker, find_bgp_speaker, get_bgp_speaker, + update_bgp_speaker, bgp_speakers, add_bgp_peer_to_speaker, + remove_bgp_peer_from_speaker, add_gateway_network_to_speaker, + remove_gateway_network_from_speaker, + get_advertised_routes_of_speaker, + get_bgp_dragents_hosting_speaker, add_bgp_speaker_to_dragent, + get_bgp_speakers_hosted_by_dragent, + remove_bgp_speaker_from_dragent diff --git a/doc/source/user/resources/network/index.rst b/doc/source/user/resources/network/index.rst index 3870b5343..ce8a6c21e 100644 --- a/doc/source/user/resources/network/index.rst +++ b/doc/source/user/resources/network/index.rst @@ -9,6 +9,8 @@ Network Resources v2/agent v2/auto_allocated_topology v2/availability_zone + v2/bgp_peer + v2/bgp_speaker v2/extension v2/flavor v2/floating_ip diff --git a/doc/source/user/resources/network/v2/bgp_peer.rst b/doc/source/user/resources/network/v2/bgp_peer.rst new file mode 100644 index 000000000..35fd5a61a --- /dev/null +++ b/doc/source/user/resources/network/v2/bgp_peer.rst @@ -0,0 +1,12 @@ +openstack.network.v2.bgp_peer +============================= + +.. automodule:: openstack.network.v2.bgp_peer + +The BgpPeer Class +----------------- + +The ``BgpPeer`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.bgp_peer.BgpPeer + :members: diff --git a/doc/source/user/resources/network/v2/bgp_speaker.rst b/doc/source/user/resources/network/v2/bgp_speaker.rst new file mode 100644 index 000000000..147d6b54f --- /dev/null +++ b/doc/source/user/resources/network/v2/bgp_speaker.rst @@ -0,0 +1,12 @@ +openstack.network.v2.bgp_speaker +================================ + +.. automodule:: openstack.network.v2.bgp_speaker + +The BgpSpeaker Class +-------------------- + +The ``BgpSpeaker`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.bgp_speaker.BgpSpeaker + :members: diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 3e4f328ee..296a3e956 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -17,6 +17,8 @@ from openstack.network.v2 import auto_allocated_topology as \ _auto_allocated_topology from openstack.network.v2 import availability_zone +from openstack.network.v2 import bgp_peer as _bgp_peer +from openstack.network.v2 import bgp_speaker as _bgp_speaker from openstack.network.v2 import extension from openstack.network.v2 import firewall_group as _firewall_group from openstack.network.v2 import firewall_policy as _firewall_policy @@ -78,6 +80,8 @@ class Proxy(proxy.Proxy): "auto_allocated_topology": _auto_allocated_topology.AutoAllocatedTopology, "availability_zone": availability_zone.AvailabilityZone, + "bgp_peer": _bgp_peer.BgpPeer, + "bgp_speaker": _bgp_speaker.BgpSpeaker, "extension": extension.Extension, "firewall_group": _firewall_group.FirewallGroup, "firewall_policy": _firewall_policy.FirewallPolicy, @@ -532,6 +536,105 @@ def availability_zones(self, **query): """ return self._list(availability_zone.AvailabilityZone) + def create_bgp_peer(self, **attrs): + """Create a new BGP Peer from attributes""" + return self._create(_bgp_peer.BgpPeer, **attrs) + + def delete_bgp_peer(self, peer, ignore_missing=True): + """Delete a BGP Peer""" + self._delete(_bgp_peer.BgpPeer, peer, ignore_missing=ignore_missing) + + def find_bgp_peer(self, name_or_id, ignore_missing=True, **query): + """"Find a single BGP Peer""" + return self._find(_bgp_peer.BgpPeer, name_or_id, + ignore_missing=ignore_missing, **query) + + def get_bgp_peer(self, peer): + """Get a signle BGP Peer""" + return self._get(_bgp_peer.BgpPeer, peer) + + def update_bgp_peer(self, peer, **attrs): + """Update a BGP Peer""" + return self._update(_bgp_peer.BgpPeer, peer, **attrs) + + def bgp_peers(self, **query): + """Return a generator of BGP Peers""" + return self._list(_bgp_peer.BgpPeer, **query) + + def create_bgp_speaker(self, **attrs): + """Create a new BGP Speaker""" + return self._create(_bgp_speaker.BgpSpeaker, **attrs) + + def delete_bgp_speaker(self, speaker, ignore_missing=True): + """Delete a BGP Speaker""" + self._delete(_bgp_speaker.BgpSpeaker, speaker, + ignore_missing=ignore_missing) + + def find_bgp_speaker(self, name_or_id, ignore_missing=True, **query): + """"Find a single BGP Peer""" + return self._find(_bgp_speaker.BgpSpeaker, name_or_id, + ignore_missing=ignore_missing, **query) + + def get_bgp_speaker(self, speaker): + """Get a signle BGP Speaker""" + return self._get(_bgp_speaker.BgpSpeaker, speaker) + + def update_bgp_speaker(self, speaker, **attrs): + """Update a BGP Speaker""" + return self._update(_bgp_speaker.BgpSpeaker, speaker, **attrs) + + def bgp_speakers(self, **query): + """Return a generator of BGP Peers""" + return self._list(_bgp_speaker.BgpSpeaker, **query) + + def add_bgp_peer_to_speaker(self, speaker, peer_id): + """Bind the BGP peer to the specified BGP Speaker.""" + speaker = self._get_resource(_bgp_speaker.BgpSpeaker, speaker) + return speaker.add_bgp_peer(self, peer_id) + + def remove_bgp_peer_from_speaker(self, speaker, peer_id): + """Unbind the BGP peer from a BGP Speaker.""" + speaker = self._get_resource(_bgp_speaker.BgpSpeaker, speaker) + return speaker.remove_bgp_peer(self, peer_id) + + def add_gateway_network_to_speaker(self, speaker, network_id): + """Add a network to the specified BGP speaker.""" + speaker = self._get_resource(_bgp_speaker.BgpSpeaker, speaker) + return speaker.add_gateway_network(self, network_id) + + def remove_gateway_network_from_speaker(self, speaker, network_id): + """Remove a network from the specified BGP speaker.""" + speaker = self._get_resource(_bgp_speaker.BgpSpeaker, speaker) + return speaker.remove_gateway_network(self, network_id) + + def get_advertised_routes_of_speaker(self, speaker): + """List all routes advertised by the specified BGP Speaker.""" + speaker = self._get_resource(_bgp_speaker.BgpSpeaker, speaker) + return speaker.get_advertised_routes(self) + + def get_bgp_dragents_hosting_speaker(self, speaker): + """List all BGP dynamic agents which are hosting the + specified BGP Speaker.""" + speaker = self._get_resource(_bgp_speaker.BgpSpeaker, speaker) + return speaker.get_bgp_dragents(self) + + def add_bgp_speaker_to_dragent(self, bgp_agent, bgp_speaker_id): + """Add a BGP Speaker to the specified dynamic routing agent.""" + speaker = self._get_resource(_bgp_speaker.BgpSpeaker, bgp_speaker_id) + speaker.add_bgp_speaker_to_dragent(self, bgp_agent) + + def get_bgp_speakers_hosted_by_dragent(self, bgp_agent): + """List all BGP Seakers hosted on the specified dynamic routing + agent.""" + agent = self._get_resource(_agent.Agent, bgp_agent) + return agent.get_bgp_speakers_hosted_by_dragent(self) + + def remove_bgp_speaker_from_dragent(self, bgp_agent, bgp_speaker_id): + """Delete the BGP Speaker hosted by the specified dynamic + routing agent.""" + speaker = self._get_resource(_bgp_speaker.BgpSpeaker, bgp_speaker_id) + speaker.remove_bgp_speaker_from_dragent(self, bgp_agent) + def find_extension(self, name_or_id, ignore_missing=True, **query): """Find a single extension diff --git a/openstack/network/v2/agent.py b/openstack/network/v2/agent.py index 97c471187..3c39ddb2b 100644 --- a/openstack/network/v2/agent.py +++ b/openstack/network/v2/agent.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import exceptions from openstack import resource from openstack import utils @@ -99,6 +100,18 @@ def remove_router_from_agent(self, session, router): url = utils.urljoin(self.base_path, self.id, 'l3-routers', router) session.delete(url, json=body) + def get_bgp_speakers_hosted_by_dragent(self, session): + """List BGP speakers hosted by a Dynamic Routing Agent + + :param session: The session to communicate through. + :type session: :class:`~keystoneauth1.adapter.Adapter` + """ + url = utils.urljoin(self.base_path, self.id, 'bgp-drinstances') + resp = session.get(url) + exceptions.raise_from_response(resp) + self._body.attributes.update(resp.json()) + return resp.json() + class NetworkHostingDHCPAgent(Agent): resource_key = 'agent' diff --git a/openstack/network/v2/bgp_peer.py b/openstack/network/v2/bgp_peer.py new file mode 100644 index 000000000..330424732 --- /dev/null +++ b/openstack/network/v2/bgp_peer.py @@ -0,0 +1,45 @@ +# 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 resource + + +class BgpPeer(resource.Resource): + resource_key = 'bgp_peer' + resources_key = 'bgp_peers' + base_path = '/bgp-peers' + + _allow_unknown_attrs_in_body = True + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + # Properties + #: The Id of the BGP Peer + id = resource.Body('id') + #: The BGP Peer's name. + name = resource.Body('name') + #: The ID of the project that owns the BGP Peer + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) + #: The authentication type for the BGP Peer, can be none or md5. + #: none by default. + auth_type = resource.Body('auth_type') + #: The remote Autonomous System number of the BGP Peer. + remote_as = resource.Body('remote_as') + #: The ip address of the Peer. + peer_ip = resource.Body('peer_ip') diff --git a/openstack/network/v2/bgp_speaker.py b/openstack/network/v2/bgp_speaker.py new file mode 100644 index 000000000..ad553e4af --- /dev/null +++ b/openstack/network/v2/bgp_speaker.py @@ -0,0 +1,169 @@ +# 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 import resource +from openstack import utils + + +class BgpSpeaker(resource.Resource): + resource_key = 'bgp_speaker' + resources_key = 'bgp_speakers' + base_path = '/bgp-speakers' + + _allow_unknown_attrs_in_body = True + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + # Properties + #: The Id of the BGP Speaker + id = resource.Body('id') + #: The BGP speaker's name. + name = resource.Body('name') + #: The ID of the project that owns the BGP Speaker. + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) + #: The IP version (4 or 6) of the BGP Speaker. + ip_version = resource.Body('ip_version') + #: Whether to enable or disable the advertisement of floating ip host + #: routes by the BGP Speaker. True by default. + advertise_floating_ip_host_routes = resource.Body( + 'advertise_floating_ip_host_routes') + #: Whether to enable or disable the advertisement of tenant network + #: routes by the BGP Speaker. True by default. + advertise_tenant_networks = resource.Body('advertise_tenant_networks') + #: The local Autonomous System number of the BGP Speaker. + local_as = resource.Body('local_as') + #: The ID of the network to which the BGP Speaker is associated. + networks = resource.Body('networks') + + def _put(self, session, url, body): + resp = session.put(url, json=body) + exceptions.raise_from_response(resp) + return resp + + def add_bgp_peer(self, session, peer_id): + """Add BGP Peer to a BGP Speaker + + :param session: The session to communicate through. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param peer_id: id of the peer to associate with the speaker. + + :returns: A dictionary as the API Reference describes it. + + :raises: :class:`~openstack.exceptions.SDKException` on error. + """ + url = utils.urljoin(self.base_path, self.id, 'add_bgp_peer') + body = {'bgp_peer_id': peer_id} + resp = self._put(session, url, body) + return resp.json() + + def remove_bgp_peer(self, session, peer_id): + """Remove BGP Peer from a BGP Speaker + + :param session: The session to communicate through. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param peer_id: The ID of the peer to disassociate from the speaker. + + :raises: :class:`~openstack.exceptions.SDKException` on error. + """ + url = utils.urljoin(self.base_path, self.id, 'remove_bgp_peer') + body = {'bgp_peer_id': peer_id} + self._put(session, url, body) + + def add_gateway_network(self, session, network_id): + """Add Network to a BGP Speaker + + :param: session: The session to communicate through. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param network_id: The ID of the network to associate with the speaker + + :returns: A dictionary as the API Reference describes it. + """ + body = {'network_id': network_id} + url = utils.urljoin(self.base_path, self.id, 'add_gateway_network') + resp = session.put(url, json=body) + return resp.json() + + def remove_gateway_network(self, session, network_id): + """Delete Network from a BGP Speaker + + :param session: The session to communicate through. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param network_id: The ID of the network to disassociate + from the speaker + """ + body = {'network_id': network_id} + url = utils.urljoin(self.base_path, self.id, 'remove_gateway_network') + session.put(url, json=body) + + def get_advertised_routes(self, session): + """List routes advertised by a BGP Speaker + + :param session: The session to communicate through. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :returns: The response as a list of routes (cidr/nexthop pair + advertised by the BGP Speaker. + + :raises: :class:`~openstack.exceptions.SDKException` on error. + """ + url = utils.urljoin(self.base_path, self.id, 'get_advertised_routes') + resp = session.get(url) + exceptions.raise_from_response(resp) + self._body.attributes.update(resp.json()) + return resp.json() + + def get_bgp_dragents(self, session): + """List Dynamic Routing Agents hosting a specific BGP Speaker + + :param session: The session to communicate through. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :returns: The response as a list of dragents hosting a specific + BGP Speaker. + + :raises: :class:`~openstack.exceptions.SDKException` on error. + """ + url = utils.urljoin(self.base_path, self.id, 'bgp-dragents') + resp = session.get(url) + exceptions.raise_from_response(resp) + self._body.attributes.update(resp.json()) + return resp.json() + + def add_bgp_speaker_to_dragent(self, session, bgp_agent_id): + """Add BGP Speaker to a Dynamic Routing Agent + + :param session: The session to communicate through. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param bgp_agent_id: The id of the dynamic routing agent to which + add the speaker. + """ + body = {'bgp_speaker_id': self.id} + url = utils.urljoin('agents', bgp_agent_id, 'bgp-drinstances') + session.post(url, json=body) + + def remove_bgp_speaker_from_dragent(self, session, bgp_agent_id): + """Delete BGP Speaker from a Dynamic Routing Agent + + :param session: The session to communicate through. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param bgp_agent_id: The id of the dynamic routing agent from which + remove the speaker. + """ + url = utils.urljoin('agents', bgp_agent_id, + 'bgp-drinstances', self.id) + session.delete(url) diff --git a/openstack/tests/functional/network/v2/test_bgp.py b/openstack/tests/functional/network/v2/test_bgp.py new file mode 100644 index 000000000..a495f4107 --- /dev/null +++ b/openstack/tests/functional/network/v2/test_bgp.py @@ -0,0 +1,123 @@ +# 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.network.v2 import bgp_peer as _bgp_peer +from openstack.network.v2 import bgp_speaker as _bgp_speaker +from openstack.tests.functional import base + + +class TestBGPSpeaker(base.BaseFunctionalTest): + + def setUp(self): + super().setUp() + self.LOCAL_AS = 101 + self.IP_VERSION = 4 + self.REMOTE_AS = 42 + self.PEER_IP = '172.200.12.3' + self.SPEAKER_NAME = 'my_speaker' + self.getUniqueString() + self.PEER_NAME = 'my_peer' + self.getUniqueString() + + if not self.user_cloud.network.find_extension("bgp"): + self.skipTest("Neutron BGP Dynamic Routing Extension disabled") + + bgp_speaker = self.operator_cloud.network.create_bgp_speaker( + ip_version=self.IP_VERSION, local_as=self.LOCAL_AS, + name=self.SPEAKER_NAME) + assert isinstance(bgp_speaker, _bgp_speaker.BgpSpeaker) + self.SPEAKER = bgp_speaker + + bgp_peer = self.operator_cloud.network.create_bgp_peer( + name=self.PEER_NAME, auth_type='none', + remote_as=self.REMOTE_AS, peer_ip=self.PEER_IP) + assert isinstance(bgp_peer, _bgp_peer.BgpPeer) + self.PEER = bgp_peer + + def tearDown(self): + sot = self.operator_cloud.network.delete_bgp_peer(self.PEER.id) + self.assertIsNone(sot) + sot = self.operator_cloud.network.delete_bgp_speaker(self.SPEAKER.id) + self.assertIsNone(sot) + super().tearDown() + + def test_find_bgp_speaker(self): + sot = self.operator_cloud.network.find_bgp_speaker(self.SPEAKER.name) + self.assertEqual(self.IP_VERSION, sot.ip_version) + self.assertEqual(self.LOCAL_AS, sot.local_as) + # Check defaults + self.assertTrue(sot.advertise_floating_ip_host_routes) + self.assertTrue(sot.advertise_tenant_networks) + + def test_get_bgp_speaker(self): + sot = self.operator_cloud.network.get_bgp_speaker(self.SPEAKER.id) + self.assertEqual(self.IP_VERSION, sot.ip_version) + self.assertEqual(self.LOCAL_AS, sot.local_as) + + def test_list_bgp_speakers(self): + speaker_ids = [sp.id for sp in + self.operator_cloud.network.bgp_speakers()] + self.assertIn(self.SPEAKER.id, speaker_ids) + + def test_update_bgp_speaker(self): + sot = self.operator_cloud.network.update_bgp_speaker( + self.SPEAKER.id, advertise_floating_ip_host_routes=False) + self.assertFalse(sot.advertise_floating_ip_host_routes) + + def test_find_bgp_peer(self): + sot = self.operator_cloud.network.find_bgp_peer(self.PEER.name) + self.assertEqual(self.PEER_IP, sot.peer_ip) + self.assertEqual(self.REMOTE_AS, sot.remote_as) + + def test_get_bgp_peer(self): + sot = self.operator_cloud.network.get_bgp_peer(self.PEER.id) + self.assertEqual(self.PEER_IP, sot.peer_ip) + self.assertEqual(self.REMOTE_AS, sot.remote_as) + + def test_list_bgp_peers(self): + peer_ids = [pe.id for pe in self.operator_cloud.network.bgp_peers()] + self.assertIn(self.PEER.id, peer_ids) + + def test_update_bgp_peer(self): + name = 'new_peer_name' + self.getUniqueString() + sot = self.operator_cloud.network.update_bgp_peer( + self.PEER.id, name=name) + self.assertEqual(name, sot.name) + + def test_add_remove_peer_to_speaker(self): + self.operator_cloud.network.add_bgp_peer_to_speaker( + self.SPEAKER.id, self.PEER.id) + sot = self.operator_cloud.network.get_bgp_speaker(self.SPEAKER.id) + self.assertEqual([self.PEER.id], sot.peers) + + # Remove the peer + self.operator_cloud.network.remove_bgp_peer_from_speaker( + self.SPEAKER.id, self.PEER.id) + sot = self.operator_cloud.network.get_bgp_speaker(self.SPEAKER.id) + self.assertEqual([], sot.peers) + + def test_add_remove_gw_network_to_speaker(self): + net_name = 'my_network' + self.getUniqueString() + net = self.user_cloud.create_network(name=net_name) + self.operator_cloud.network.add_gateway_network_to_speaker( + self.SPEAKER.id, net.id) + sot = self.operator_cloud.network.get_bgp_speaker(self.SPEAKER.id) + self.assertEqual([net.id], sot.networks) + + # Remove the network + self.operator_cloud.network.remove_gateway_network_from_speaker( + self.SPEAKER.id, net.id) + sot = self.operator_cloud.network.get_bgp_speaker(self.SPEAKER.id) + self.assertEqual([], sot.networks) + + def test_get_advertised_routes_of_speaker(self): + sot = self.operator_cloud.network.get_advertised_routes_of_speaker( + self.SPEAKER.id) + self.assertEqual({'advertised_routes': []}, sot) diff --git a/openstack/tests/unit/network/v2/test_agent.py b/openstack/tests/unit/network/v2/test_agent.py index 1cc3829e7..ae2bf4858 100644 --- a/openstack/tests/unit/network/v2/test_agent.py +++ b/openstack/tests/unit/network/v2/test_agent.py @@ -120,6 +120,23 @@ def test_remove_router_from_agent(self): sess.delete.assert_called_with('agents/IDENTIFIER/l3-routers/', json=body) + def test_get_bgp_speakers_hosted_by_dragent(self): + sot = agent.Agent(**EXAMPLE) + sess = mock.Mock() + response = mock.Mock() + response.body = { + 'bgp_speakers': [ + {'name': 'bgp_speaker_1', 'ip_version': 4} + ] + } + response.json = mock.Mock(return_value=response.body) + response.status_code = 200 + sess.get = mock.Mock(return_value=response) + resp = sot.get_bgp_speakers_hosted_by_dragent(sess) + + self.assertEqual(resp, response.body) + sess.get.assert_called_with('agents/IDENTIFIER/bgp-drinstances') + class TestNetworkHostingDHCPAgent(base.TestCase): diff --git a/openstack/tests/unit/network/v2/test_bgp_peer.py b/openstack/tests/unit/network/v2/test_bgp_peer.py new file mode 100644 index 000000000..06aac5fa1 --- /dev/null +++ b/openstack/tests/unit/network/v2/test_bgp_peer.py @@ -0,0 +1,54 @@ +# 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.network.v2 import bgp_peer +from openstack.tests.unit import base + + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'auth_type': 'none', + 'remote_as': '1001', + 'name': 'bgp-peer', + 'peer_ip': '10.0.0.3', + 'id': IDENTIFIER, + 'project_id': '42' +} + + +class TestBgpPeer(base.TestCase): + + def test_basic(self): + sot = bgp_peer.BgpPeer() + self.assertEqual('bgp_peer', sot.resource_key) + self.assertEqual('bgp_peers', sot.resources_key) + self.assertEqual('/bgp-peers', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = bgp_peer.BgpPeer(**EXAMPLE) + self.assertEqual(EXAMPLE['auth_type'], sot.auth_type) + self.assertEqual(EXAMPLE['remote_as'], sot.remote_as) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['peer_ip'], sot.peer_ip) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + + self.assertDictEqual( + {'limit': 'limit', + 'marker': 'marker', + }, + sot._query_mapping._mapping) diff --git a/openstack/tests/unit/network/v2/test_bgp_speaker.py b/openstack/tests/unit/network/v2/test_bgp_speaker.py new file mode 100644 index 000000000..8f78d3ac3 --- /dev/null +++ b/openstack/tests/unit/network/v2/test_bgp_speaker.py @@ -0,0 +1,183 @@ +# 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 unittest import mock + +from openstack.network.v2 import bgp_speaker +from openstack.tests.unit import base + + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'id': IDENTIFIER, + 'name': 'bgp-speaker', + 'peers': [], + 'ip_version': 4, + 'advertise_floating_ip_host_routes': 'true', + 'advertise_tenant_networks': 'true', + 'local_as': 1000, + 'networks': [], + 'project_id': '42' +} + + +class TestBgpSpeaker(base.TestCase): + def test_basic(self): + sot = bgp_speaker.BgpSpeaker() + self.assertEqual('bgp_speaker', sot.resource_key) + self.assertEqual('bgp_speakers', sot.resources_key) + self.assertEqual('/bgp-speakers', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = bgp_speaker.BgpSpeaker(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['ip_version'], sot.ip_version) + self.assertEqual(EXAMPLE['advertise_floating_ip_host_routes'], + sot.advertise_floating_ip_host_routes) + self.assertEqual(EXAMPLE['local_as'], sot.local_as) + self.assertEqual(EXAMPLE['networks'], sot.networks) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + + self.assertDictEqual( + {'limit': 'limit', + 'marker': 'marker', + }, + sot._query_mapping._mapping) + + def test_add_bgp_peer(self): + sot = bgp_speaker.BgpSpeaker(**EXAMPLE) + response = mock.Mock() + response.body = {'bgp_peer_id': '101'} + response.json = mock.Mock(return_value=response.body) + response.status_code = 200 + sess = mock.Mock() + sess.put = mock.Mock(return_value=response) + ret = sot.add_bgp_peer(sess, '101') + self.assertIsInstance(ret, dict) + self.assertEqual(ret, {'bgp_peer_id': '101'}) + + body = {'bgp_peer_id': '101'} + url = 'bgp-speakers/IDENTIFIER/add_bgp_peer' + sess.put.assert_called_with(url, json=body) + + def test_remove_bgp_peer(self): + sot = bgp_speaker.BgpSpeaker(**EXAMPLE) + response = mock.Mock() + response.body = {'bgp_peer_id': '102'} + response.json = mock.Mock(return_value=response.body) + response.status_code = 200 + sess = mock.Mock() + sess.put = mock.Mock(return_value=response) + ret = sot.remove_bgp_peer(sess, '102') + self.assertIsNone(ret) + + body = {'bgp_peer_id': '102'} + url = 'bgp-speakers/IDENTIFIER/remove_bgp_peer' + sess.put.assert_called_with(url, json=body) + + def test_add_gateway_network(self): + sot = bgp_speaker.BgpSpeaker(**EXAMPLE) + response = mock.Mock() + response.body = {'network_id': 'net_id'} + response.json = mock.Mock(return_value=response.body) + response.status_code = 200 + sess = mock.Mock() + sess.put = mock.Mock(return_value=response) + ret = sot.add_gateway_network(sess, 'net_id') + self.assertIsInstance(ret, dict) + self.assertEqual(ret, {'network_id': 'net_id'}) + + body = {'network_id': 'net_id'} + url = 'bgp-speakers/IDENTIFIER/add_gateway_network' + sess.put.assert_called_with(url, json=body) + + def test_remove_gateway_network(self): + sot = bgp_speaker.BgpSpeaker(**EXAMPLE) + response = mock.Mock() + response.body = {'network_id': 'net_id42'} + response.json = mock.Mock(return_value=response.body) + response.status_code = 200 + sess = mock.Mock() + sess.put = mock.Mock(return_value=response) + ret = sot.remove_gateway_network(sess, 'net_id42') + self.assertIsNone(ret) + + body = {'network_id': 'net_id42'} + url = 'bgp-speakers/IDENTIFIER/remove_gateway_network' + sess.put.assert_called_with(url, json=body) + + def test_get_advertised_routes(self): + sot = bgp_speaker.BgpSpeaker(**EXAMPLE) + response = mock.Mock() + response.body = { + 'advertised_routes': [ + {'cidr': '192.168.10.0/24', 'nexthop': '10.0.0.1'} + ] + } + response.json = mock.Mock(return_value=response.body) + response.status_code = 200 + sess = mock.Mock() + sess.get = mock.Mock(return_value=response) + ret = sot.get_advertised_routes(sess) + + url = 'bgp-speakers/IDENTIFIER/get_advertised_routes' + sess.get.assert_called_with(url) + self.assertEqual(ret, response.body) + + def test_get_bgp_dragents(self): + sot = bgp_speaker.BgpSpeaker(**EXAMPLE) + response = mock.Mock() + response.body = { + 'agents': [ + {'binary': 'neutron-bgp-dragent', 'alive': True} + ] + } + response.json = mock.Mock(return_value=response.body) + response.status_code = 200 + sess = mock.Mock() + sess.get = mock.Mock(return_value=response) + ret = sot.get_bgp_dragents(sess) + + url = 'bgp-speakers/IDENTIFIER/bgp-dragents' + sess.get.assert_called_with(url) + self.assertEqual(ret, response.body) + + def test_add_bgp_speaker_to_dragent(self): + sot = bgp_speaker.BgpSpeaker(**EXAMPLE) + agent_id = '123-42' + response = mock.Mock() + response.status_code = 201 + sess = mock.Mock() + sess.post = mock.Mock(return_value=response) + self.assertIsNone(sot.add_bgp_speaker_to_dragent(sess, agent_id)) + + body = {'bgp_speaker_id': sot.id} + url = 'agents/%s/bgp-drinstances' % agent_id + sess.post.assert_called_with(url, json=body) + + def test_remove_bgp_speaker_from_dragent(self): + sot = bgp_speaker.BgpSpeaker(**EXAMPLE) + agent_id = '123-42' + response = mock.Mock() + response.status_code = 204 + sess = mock.Mock() + sess.delete = mock.Mock(return_value=response) + self.assertIsNone(sot.remove_bgp_speaker_from_dragent(sess, agent_id)) + + url = 'agents/%s/bgp-drinstances/%s' % (agent_id, IDENTIFIER) + sess.delete.assert_called_with(url) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 1a864f60a..66a0fd14f 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -20,6 +20,8 @@ from openstack.network.v2 import agent from openstack.network.v2 import auto_allocated_topology from openstack.network.v2 import availability_zone +from openstack.network.v2 import bgp_peer +from openstack.network.v2 import bgp_speaker from openstack.network.v2 import extension from openstack.network.v2 import firewall_group from openstack.network.v2 import firewall_policy @@ -1945,3 +1947,53 @@ def test_ndp_proxies(self): def test_ndp_proxy_update(self): self.verify_update(self.proxy.update_ndp_proxy, ndp_proxy.NDPProxy) + + +class TestNetworkBGP(TestNetworkProxy): + + def test_bgp_speaker_create(self): + self.verify_create(self.proxy.create_bgp_speaker, + bgp_speaker.BgpSpeaker) + + def test_bgp_speaker_delete(self): + self.verify_delete(self.proxy.delete_bgp_speaker, + bgp_speaker.BgpSpeaker, False) + + def test_bgp_speaker_delete_ignore(self): + self.verify_delete(self.proxy.delete_bgp_speaker, + bgp_speaker.BgpSpeaker, True) + + def test_bgp_speaker_find(self): + self.verify_find(self.proxy.find_bgp_speaker, bgp_speaker.BgpSpeaker) + + def test_bgp_speaker_get(self): + self.verify_get(self.proxy.get_bgp_speaker, bgp_speaker.BgpSpeaker) + + def test_bgp_speakers(self): + self.verify_list(self.proxy.bgp_speakers, bgp_speaker.BgpSpeaker) + + def test_bgp_speaker_update(self): + self.verify_update(self.proxy.update_bgp_speaker, + bgp_speaker.BgpSpeaker) + + def test_bgp_peer_create(self): + self.verify_create(self.proxy.create_bgp_peer, bgp_peer.BgpPeer) + + def test_bgp_peer_delete(self): + self.verify_delete(self.proxy.delete_bgp_peer, + bgp_peer.BgpPeer, False) + + def test_bgp_peer_delete_ignore(self): + self.verify_delete(self.proxy.delete_bgp_peer, bgp_peer.BgpPeer, True) + + def test_bgp_peer_find(self): + self.verify_find(self.proxy.find_bgp_peer, bgp_peer.BgpPeer) + + def test_bgp_peer_get(self): + self.verify_get(self.proxy.get_bgp_peer, bgp_peer.BgpPeer) + + def test_bgp_peers(self): + self.verify_list(self.proxy.bgp_peers, bgp_peer.BgpPeer) + + def test_bgp_peer_update(self): + self.verify_update(self.proxy.update_bgp_peer, bgp_peer.BgpPeer) diff --git a/releasenotes/notes/network_add_bgp_resources-c182dc2873d6db18.yaml b/releasenotes/notes/network_add_bgp_resources-c182dc2873d6db18.yaml new file mode 100644 index 000000000..b87c8d556 --- /dev/null +++ b/releasenotes/notes/network_add_bgp_resources-c182dc2873d6db18.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Add BGP Speaker and BGP Peer resources, and introduce support for CRUD + operations for these. Additional REST operations introduced for speakers: + add_bgp_peer, remove_bgp_peer, add_gateway_network, remove_gateway_network, + get_advertised_routes, get_bgp_dragents, add_bgp_speaker_to_draget, + remove_bgp_speaker_from_dragent. + One new REST method is added to agents to cover the features + of Dynamic Routing Agents schedulers: get_bgp_speakers_hosted_by_dragent From 289e5c2d3cba0eb1c008988ae5dccab5be05d9b6 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 22 Nov 2022 11:14:48 +0000 Subject: [PATCH 3188/3836] Convert cloud layer to use COE proxy layer We only have the Cluster resource modelled thus far, so that is all that's converted. Change-Id: I7ea63b2cdda881c621cbb0e212479328a96e73bd Signed-off-by: Stephen Finucane --- openstack/cloud/_coe.py | 92 +++---- .../v1/cluster.py | 3 + .../functional/cloud/test_magnum_services.py | 2 +- .../tests/unit/cloud/test_coe_clusters.py | 233 +++++++++++------- .../v1/test_proxy.py | 51 ++++ 5 files changed, 232 insertions(+), 149 deletions(-) create mode 100644 openstack/tests/unit/container_infrastructure_management/v1/test_proxy.py diff --git a/openstack/cloud/_coe.py b/openstack/cloud/_coe.py index 67b39c078..cbd547cd5 100644 --- a/openstack/cloud/_coe.py +++ b/openstack/cloud/_coe.py @@ -30,20 +30,16 @@ def _container_infra_client(self): @_utils.cache_on_arguments() def list_coe_clusters(self): - """List COE(Ccontainer Orchestration Engine) cluster. - - :returns: a list of dicts containing the cluster. + """List COE (Container Orchestration Engine) cluster. + :returns: A list of container infrastructure management ``Cluster`` + objects. :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ - with _utils.shade_exceptions("Error fetching cluster list"): - data = self._container_infra_client.get('/clusters') - return self._normalize_coe_clusters( - self._get_and_munchify('clusters', data)) + return list(self.container_infrastructure_management.clusters()) - def search_coe_clusters( - self, name_or_id=None, filters=None): + def search_coe_clusters(self, name_or_id=None, filters=None): """Search COE cluster. :param name_or_id: cluster name or ID. @@ -51,14 +47,13 @@ def search_coe_clusters( :param detail: a boolean to control if we need summarized or detailed output. - :returns: a list of dict containing the cluster - + :returns: A list of container infrastructure management ``Cluster`` + objects. :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ coe_clusters = self.list_coe_clusters() - return _utils._filter_list( - coe_clusters, name_or_id, filters) + return _utils._filter_list(coe_clusters, name_or_id, filters) def get_coe_cluster(self, name_or_id, filters=None): """Get a COE cluster by name or ID. @@ -79,36 +74,34 @@ def get_coe_cluster(self, name_or_id, filters=None): A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - :returns: A cluster dict or None if no matching cluster is found. + :returns: A container infrastructure management ``Cluster`` object if + found, else None. """ - return _utils._get_entity(self, 'coe_cluster', name_or_id, - filters=filters) + return _utils._get_entity(self, 'coe_cluster', name_or_id, filters) def create_coe_cluster( - self, name, cluster_template_id, **kwargs): + self, name, cluster_template_id, **kwargs, + ): """Create a COE cluster based on given cluster template. :param string name: Name of the cluster. - :param string image_id: ID of the cluster template to use. - Other arguments will be passed in kwargs. + :param string cluster_template_id: ID of the cluster template to use. + :param dict kwargs: Any other arguments to pass in. :returns: a dict containing the cluster description - + :returns: The created container infrastructure management ``Cluster`` + object. :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call """ - error_message = ("Error creating cluster of name" - " {cluster_name}".format(cluster_name=name)) - with _utils.shade_exceptions(error_message): - body = kwargs.copy() - body['name'] = name - body['cluster_template_id'] = cluster_template_id - - cluster = self._container_infra_client.post( - '/clusters', json=body) + cluster = self.container_infrastructure_management.create_cluster( + name=name, + cluster_template_id=cluster_template_id, + **kwargs, + ) self.list_coe_clusters.invalidate(self) - return self._normalize_coe_cluster(cluster) + return cluster def delete_coe_cluster(self, name_or_id): """Delete a COE cluster. @@ -126,25 +119,21 @@ def delete_coe_cluster(self, name_or_id): self.log.debug( "COE Cluster %(name_or_id)s does not exist", {'name_or_id': name_or_id}, - exc_info=True) + exc_info=True, + ) return False - with _utils.shade_exceptions("Error in deleting COE cluster"): - self._container_infra_client.delete( - '/clusters/{id}'.format(id=cluster['id'])) - self.list_coe_clusters.invalidate(self) - + self.container_infrastructure_management.delete_cluster(cluster) + self.list_coe_clusters.invalidate(self) return True - @_utils.valid_kwargs('node_count') - def update_coe_cluster(self, name_or_id, operation, **kwargs): + def update_coe_cluster(self, name_or_id, **kwargs): """Update a COE cluster. :param name_or_id: Name or ID of the COE cluster being updated. - :param operation: Operation to perform - add, remove, replace. - Other arguments will be passed with kwargs. + :param kwargs: Cluster attributes to be updated. - :returns: a dict representing the updated cluster. + :returns: The updated cluster ``Cluster`` object. :raises: OpenStackCloudException on operation error. """ @@ -154,23 +143,12 @@ def update_coe_cluster(self, name_or_id, operation, **kwargs): raise exc.OpenStackCloudException( "COE cluster %s not found." % name_or_id) - if operation not in ['add', 'replace', 'remove']: - raise TypeError( - "%s operation not in 'add', 'replace', 'remove'" % operation) - - patches = _utils.generate_patches_from_kwargs(operation, **kwargs) - # No need to fire an API call if there is an empty patch - if not patches: - return cluster - - with _utils.shade_exceptions( - "Error updating COE cluster {0}".format(name_or_id)): - self._container_infra_client.patch( - '/clusters/{id}'.format(id=cluster['id']), - json=patches) + cluster = self.container_infrastructure_management.update_cluster( + cluster, + **kwargs + ) - new_cluster = self.get_coe_cluster(name_or_id) - return new_cluster + return cluster def get_coe_cluster_certificate(self, cluster_id): """Get details about the CA certificate for a cluster by name or ID. diff --git a/openstack/container_infrastructure_management/v1/cluster.py b/openstack/container_infrastructure_management/v1/cluster.py index 71c880437..684f096d0 100644 --- a/openstack/container_infrastructure_management/v1/cluster.py +++ b/openstack/container_infrastructure_management/v1/cluster.py @@ -28,6 +28,9 @@ class Cluster(resource.Resource): allow_list = True allow_patch = True + commit_method = 'PATCH' + commit_jsonpatch = True + #: The endpoint URL of COE API exposed to end-users. api_address = resource.Body('api_address') #: The UUID of the cluster template. diff --git a/openstack/tests/functional/cloud/test_magnum_services.py b/openstack/tests/functional/cloud/test_magnum_services.py index 80ef5ccf7..b971f1383 100644 --- a/openstack/tests/functional/cloud/test_magnum_services.py +++ b/openstack/tests/functional/cloud/test_magnum_services.py @@ -33,7 +33,7 @@ def test_magnum_services(self): '''Test magnum services functionality''' # Test that we can list services - services = self.user_cloud.list_magnum_services() + services = self.operator_cloud.list_magnum_services() self.assertEqual(1, len(services)) self.assertEqual(services[0]['id'], 1) diff --git a/openstack/tests/unit/cloud/test_coe_clusters.py b/openstack/tests/unit/cloud/test_coe_clusters.py index dd8402ebd..e05ba774c 100644 --- a/openstack/tests/unit/cloud/test_coe_clusters.py +++ b/openstack/tests/unit/cloud/test_coe_clusters.py @@ -11,11 +11,11 @@ # under the License. -import munch - +from openstack.container_infrastructure_management.v1 import cluster from openstack.tests.unit import base -coe_cluster_obj = munch.Munch( + +coe_cluster_obj = dict( status="CREATE_IN_PROGRESS", cluster_template_id="0562d357-8641-4759-8fed-8173f02c9633", uuid="731387cf-a92b-4c36-981e-3271d63e5597", @@ -38,130 +38,181 @@ class TestCOEClusters(base.TestCase): + def _compare_clusters(self, exp, real): + self.assertDictEqual( + cluster.Cluster(**exp).to_dict(computed=False), + real.to_dict(computed=False), + ) def get_mock_url( - self, - service_type='container-infrastructure-management', - base_url_append=None, append=None, resource=None): + self, + service_type="container-infrastructure-management", + base_url_append=None, + append=None, + resource=None, + ): return super(TestCOEClusters, self).get_mock_url( - service_type=service_type, resource=resource, - append=append, base_url_append=base_url_append) + service_type=service_type, + resource=resource, + append=append, + base_url_append=base_url_append, + ) def test_list_coe_clusters(self): - self.register_uris([dict( - method='GET', - uri=self.get_mock_url(resource='clusters'), - json=dict(clusters=[coe_cluster_obj.toDict()]))]) + self.register_uris( + [ + dict( + method="GET", + uri=self.get_mock_url(resource="clusters"), + json=dict(clusters=[coe_cluster_obj]), + ) + ] + ) cluster_list = self.cloud.list_coe_clusters() - self.assertEqual( + self._compare_clusters( + coe_cluster_obj, cluster_list[0], - self.cloud._normalize_coe_cluster(coe_cluster_obj)) + ) self.assert_calls() def test_create_coe_cluster(self): - json_response = dict(uuid=coe_cluster_obj.get('uuid')) - kwargs = dict(name=coe_cluster_obj.name, - cluster_template_id=coe_cluster_obj.cluster_template_id, - master_count=coe_cluster_obj.master_count, - node_count=coe_cluster_obj.node_count) - self.register_uris([dict( - method='POST', - uri=self.get_mock_url(resource='clusters'), - json=json_response, - validate=dict(json=kwargs)), - ]) - expected = self.cloud._normalize_coe_cluster(json_response) + json_response = dict(uuid=coe_cluster_obj.get("uuid")) + kwargs = dict( + name=coe_cluster_obj["name"], + cluster_template_id=coe_cluster_obj["cluster_template_id"], + master_count=coe_cluster_obj["master_count"], + node_count=coe_cluster_obj["node_count"], + ) + self.register_uris( + [ + dict( + method="POST", + uri=self.get_mock_url(resource="clusters"), + json=json_response, + validate=dict(json=kwargs), + ), + ] + ) response = self.cloud.create_coe_cluster(**kwargs) - self.assertEqual(response, expected) + expected = kwargs.copy() + expected.update(**json_response) + self._compare_clusters(expected, response) self.assert_calls() def test_search_coe_cluster_by_name(self): - self.register_uris([dict( - method='GET', - uri=self.get_mock_url(resource='clusters'), - json=dict(clusters=[coe_cluster_obj.toDict()]))]) - - coe_clusters = self.cloud.search_coe_clusters( - name_or_id='k8s') + self.register_uris( + [ + dict( + method="GET", + uri=self.get_mock_url(resource="clusters"), + json=dict(clusters=[coe_cluster_obj]), + ) + ] + ) + + coe_clusters = self.cloud.search_coe_clusters(name_or_id="k8s") self.assertEqual(1, len(coe_clusters)) - self.assertEqual(coe_cluster_obj.uuid, coe_clusters[0]['id']) + self.assertEqual(coe_cluster_obj["uuid"], coe_clusters[0]["id"]) self.assert_calls() def test_search_coe_cluster_not_found(self): - self.register_uris([dict( - method='GET', - uri=self.get_mock_url(resource='clusters'), - json=dict(clusters=[coe_cluster_obj.toDict()]))]) + self.register_uris( + [ + dict( + method="GET", + uri=self.get_mock_url(resource="clusters"), + json=dict(clusters=[coe_cluster_obj]), + ) + ] + ) coe_clusters = self.cloud.search_coe_clusters( - name_or_id='non-existent') + name_or_id="non-existent" + ) self.assertEqual(0, len(coe_clusters)) self.assert_calls() def test_get_coe_cluster(self): - self.register_uris([dict( - method='GET', - uri=self.get_mock_url(resource='clusters'), - json=dict(clusters=[coe_cluster_obj.toDict()]))]) - - r = self.cloud.get_coe_cluster(coe_cluster_obj.name) + self.register_uris( + [ + dict( + method="GET", + uri=self.get_mock_url(resource="clusters"), + json=dict(clusters=[coe_cluster_obj]), + ) + ] + ) + + r = self.cloud.get_coe_cluster(coe_cluster_obj["name"]) self.assertIsNotNone(r) - self.assertDictEqual( - r, self.cloud._normalize_coe_cluster(coe_cluster_obj)) + self._compare_clusters( + coe_cluster_obj, + r, + ) self.assert_calls() def test_get_coe_cluster_not_found(self): - self.register_uris([dict( - method='GET', - uri=self.get_mock_url(resource='clusters'), - json=dict(clusters=[]))]) - r = self.cloud.get_coe_cluster('doesNotExist') + self.register_uris( + [ + dict( + method="GET", + uri=self.get_mock_url(resource="clusters"), + json=dict(clusters=[]), + ) + ] + ) + r = self.cloud.get_coe_cluster("doesNotExist") self.assertIsNone(r) self.assert_calls() def test_delete_coe_cluster(self): - self.register_uris([ - dict( - method='GET', - uri=self.get_mock_url(resource='clusters'), - json=dict(clusters=[coe_cluster_obj.toDict()])), - dict( - method='DELETE', - uri=self.get_mock_url( - resource='clusters', - append=[coe_cluster_obj.uuid])), - ]) - self.cloud.delete_coe_cluster(coe_cluster_obj.uuid) + self.register_uris( + [ + dict( + method="GET", + uri=self.get_mock_url(resource="clusters"), + json=dict(clusters=[coe_cluster_obj]), + ), + dict( + method="DELETE", + uri=self.get_mock_url( + resource="clusters", append=[coe_cluster_obj['uuid']] + ), + ), + ] + ) + self.cloud.delete_coe_cluster(coe_cluster_obj["uuid"]) self.assert_calls() def test_update_coe_cluster(self): - self.register_uris([ - dict( - method='GET', - uri=self.get_mock_url(resource='clusters'), - json=dict(clusters=[coe_cluster_obj.toDict()])), - dict( - method='PATCH', - uri=self.get_mock_url( - resource='clusters', - append=[coe_cluster_obj.uuid]), - status_code=200, - validate=dict( - json=[{ - u'op': u'replace', - u'path': u'/node_count', - u'value': 3 - }] - )), - dict( - method='GET', - uri=self.get_mock_url(resource='clusters'), - # This json value is not meaningful to the test - it just has - # to be valid. - json=dict(clusters=[coe_cluster_obj.toDict()])), - ]) + self.register_uris( + [ + dict( + method="GET", + uri=self.get_mock_url(resource="clusters"), + json=dict(clusters=[coe_cluster_obj]), + ), + dict( + method="PATCH", + uri=self.get_mock_url( + resource="clusters", append=[coe_cluster_obj["uuid"]] + ), + status_code=200, + validate=dict( + json=[ + { + "op": "replace", + "path": "/node_count", + "value": 3, + } + ] + ), + ), + ] + ) self.cloud.update_coe_cluster( - coe_cluster_obj.uuid, 'replace', node_count=3) + coe_cluster_obj["uuid"], node_count=3 + ) self.assert_calls() diff --git a/openstack/tests/unit/container_infrastructure_management/v1/test_proxy.py b/openstack/tests/unit/container_infrastructure_management/v1/test_proxy.py new file mode 100644 index 000000000..3f2555693 --- /dev/null +++ b/openstack/tests/unit/container_infrastructure_management/v1/test_proxy.py @@ -0,0 +1,51 @@ +# 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.container_infrastructure_management.v1 import _proxy +from openstack.container_infrastructure_management.v1 import cluster +from openstack.tests.unit import test_proxy_base + + +class TestClusterProxy(test_proxy_base.TestProxyBase): + def setUp(self): + super().setUp() + self.proxy = _proxy.Proxy(self.session) + + +class TestCluster(TestClusterProxy): + def test_cluster_get(self): + self.verify_get(self.proxy.get_cluster, cluster.Cluster) + + def test_cluster_find(self): + self.verify_find( + self.proxy.find_cluster, + cluster.Cluster, + method_kwargs={}, + expected_kwargs={}, + ) + + def test_clusters(self): + self.verify_list( + self.proxy.clusters, + cluster.Cluster, + method_kwargs={"query": 1}, + expected_kwargs={"query": 1}, + ) + + def test_cluster_create_attrs(self): + self.verify_create(self.proxy.create_cluster, cluster.Cluster) + + def test_cluster_delete(self): + self.verify_delete(self.proxy.delete_cluster, cluster.Cluster, False) + + def test_cluster_delete_ignore(self): + self.verify_delete(self.proxy.delete_cluster, cluster.Cluster, True) From b66c6cc847bc0bfd9ffdbc25bfb74a0bf568d6e3 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 24 Jan 2023 18:34:46 +0100 Subject: [PATCH 3189/3836] Add magnum cluster templates resource Implement support for magnum clustertemplate. - drop support for bays. Those were never tested properly in SDK and are deprecated since Newton. Change-Id: I8a7198231fd60abf5ac2dd44985961c8c47db657 --- openstack/cloud/_coe.py | 120 ++++------------ .../v1/_proxy.py | 100 +++++++++++++ .../v1/cluster.py | 9 +- .../v1/cluster_template.py | 116 +++++++++++++++ .../cloud/test_cluster_templates.py | 2 +- .../unit/cloud/test_cluster_templates.py | 135 +++++++----------- .../v1/test_cluster.py | 2 +- .../v1/test_cluster_template.py | 98 +++++++++++++ .../v1/test_proxy.py | 48 ++++++- 9 files changed, 445 insertions(+), 185 deletions(-) create mode 100644 openstack/container_infrastructure_management/v1/cluster_template.py create mode 100644 openstack/tests/unit/container_infrastructure_management/v1/test_cluster_template.py diff --git a/openstack/cloud/_coe.py b/openstack/cloud/_coe.py index cbd547cd5..401564ecc 100644 --- a/openstack/cloud/_coe.py +++ b/openstack/cloud/_coe.py @@ -201,22 +201,8 @@ def list_cluster_templates(self, detail=False): :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ - with _utils.shade_exceptions("Error fetching cluster template list"): - try: - data = self._container_infra_client.get('/clustertemplates') - # NOTE(flwang): Magnum adds /clustertemplates and /cluster - # to deprecate /baymodels and /bay since Newton release. So - # we're using a small tag to indicate if current - # cloud has those two new API endpoints. - self._container_infra_client._has_magnum_after_newton = True - return self._normalize_cluster_templates( - self._get_and_munchify('clustertemplates', data)) - except exc.OpenStackCloudURINotFound: - data = self._container_infra_client.get('/baymodels/detail') - return self._normalize_cluster_templates( - self._get_and_munchify('baymodels', data)) - list_baymodels = list_cluster_templates - list_coe_cluster_templates = list_cluster_templates + return list( + self.container_infrastructure_management.cluster_templates()) def search_cluster_templates( self, name_or_id=None, filters=None, detail=False): @@ -235,8 +221,6 @@ def search_cluster_templates( cluster_templates = self.list_cluster_templates(detail=detail) return _utils._filter_list( cluster_templates, name_or_id, filters) - search_baymodels = search_cluster_templates - search_coe_cluster_templates = search_cluster_templates def get_cluster_template(self, name_or_id, filters=None, detail=False): """Get a cluster template by name or ID. @@ -260,10 +244,9 @@ def get_cluster_template(self, name_or_id, filters=None, detail=False): :returns: A cluster template dict or None if no matching cluster template is found. """ - return _utils._get_entity(self, 'cluster_template', name_or_id, - filters=filters, detail=detail) - get_baymodel = get_cluster_template - get_coe_cluster_template = get_cluster_template + return _utils._get_entity( + self, 'cluster_template', name_or_id, + filters=filters, detail=detail) def create_cluster_template( self, name, image_id=None, keypair_id=None, coe=None, **kwargs): @@ -280,28 +263,16 @@ def create_cluster_template( :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call """ - error_message = ("Error creating cluster template of name" - " {cluster_template_name}".format( - cluster_template_name=name)) - with _utils.shade_exceptions(error_message): - body = kwargs.copy() - body['name'] = name - body['image_id'] = image_id - body['keypair_id'] = keypair_id - body['coe'] = coe - - try: - cluster_template = self._container_infra_client.post( - '/clustertemplates', json=body) - self._container_infra_client._has_magnum_after_newton = True - except exc.OpenStackCloudURINotFound: - cluster_template = self._container_infra_client.post( - '/baymodels', json=body) - - self.list_cluster_templates.invalidate(self) - return self._normalize_cluster_template(cluster_template) - create_baymodel = create_cluster_template - create_coe_cluster_template = create_cluster_template + cluster_template = self.container_infrastructure_management \ + .create_cluster_template( + name=name, + image_id=image_id, + keypair_id=keypair_id, + coe=coe, + **kwargs, + ) + + return cluster_template def delete_cluster_template(self, name_or_id): """Delete a cluster template. @@ -322,68 +293,31 @@ def delete_cluster_template(self, name_or_id): exc_info=True) return False - with _utils.shade_exceptions("Error in deleting cluster template"): - if getattr(self._container_infra_client, - '_has_magnum_after_newton', False): - self._container_infra_client.delete( - '/clustertemplates/{id}'.format(id=cluster_template['id'])) - else: - self._container_infra_client.delete( - '/baymodels/{id}'.format(id=cluster_template['id'])) - self.list_cluster_templates.invalidate(self) - + self.container_infrastructure_management.delete_cluster_template( + cluster_template) return True - delete_baymodel = delete_cluster_template - delete_coe_cluster_template = delete_cluster_template - - @_utils.valid_kwargs('name', 'image_id', 'flavor_id', 'master_flavor_id', - 'keypair_id', 'external_network_id', 'fixed_network', - 'dns_nameserver', 'docker_volume_size', 'labels', - 'coe', 'http_proxy', 'https_proxy', 'no_proxy', - 'network_driver', 'tls_disabled', 'public', - 'registry_enabled', 'volume_driver') - def update_cluster_template(self, name_or_id, operation, **kwargs): + + def update_cluster_template(self, name_or_id, **kwargs): """Update a cluster template. :param name_or_id: Name or ID of the cluster template being updated. - :param operation: Operation to perform - add, remove, replace. - Other arguments will be passed with kwargs. - :returns: a dict representing the updated cluster template. + :returns: an update cluster template. :raises: OpenStackCloudException on operation error. """ - self.list_cluster_templates.invalidate(self) cluster_template = self.get_cluster_template(name_or_id) if not cluster_template: raise exc.OpenStackCloudException( "Cluster template %s not found." % name_or_id) - if operation not in ['add', 'replace', 'remove']: - raise TypeError( - "%s operation not in 'add', 'replace', 'remove'" % operation) - - patches = _utils.generate_patches_from_kwargs(operation, **kwargs) - # No need to fire an API call if there is an empty patch - if not patches: - return cluster_template - - with _utils.shade_exceptions( - "Error updating cluster template {0}".format(name_or_id)): - if getattr(self._container_infra_client, - '_has_magnum_after_newton', False): - self._container_infra_client.patch( - '/clustertemplates/{id}'.format(id=cluster_template['id']), - json=patches) - else: - self._container_infra_client.patch( - '/baymodels/{id}'.format(id=cluster_template['id']), - json=patches) - - new_cluster_template = self.get_cluster_template(name_or_id) - return new_cluster_template - update_baymodel = update_cluster_template - update_coe_cluster_template = update_cluster_template + cluster_template = self.container_infrastructure_management \ + .update_cluster_template( + cluster_template, + **kwargs + ) + + return cluster_template def list_magnum_services(self): """List all Magnum services. diff --git a/openstack/container_infrastructure_management/v1/_proxy.py b/openstack/container_infrastructure_management/v1/_proxy.py index 7d3d2783b..ca40a8227 100644 --- a/openstack/container_infrastructure_management/v1/_proxy.py +++ b/openstack/container_infrastructure_management/v1/_proxy.py @@ -13,6 +13,9 @@ from openstack.container_infrastructure_management.v1 import ( cluster as _cluster ) +from openstack.container_infrastructure_management.v1 import ( + cluster_template as _cluster_template +) from openstack import proxy @@ -20,6 +23,7 @@ class Proxy(proxy.Proxy): _resource_registry = { "cluster": _cluster.Cluster, + "cluster_template": _cluster_template.ClusterTemplate, } def create_cluster(self, **attrs): @@ -107,3 +111,99 @@ def update_cluster(self, cluster, **attrs): :class:`~openstack.container_infrastructure_management.v1.cluster.Cluster` """ return self._update(_cluster.Cluster, cluster, **attrs) + + # ============== Cluster Templates ============== + def create_cluster_template(self, **attrs): + """Create a new cluster_template from attributes + + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.container_infrastructure_management.v1.cluster_template.ClusterTemplate`, + comprised of the properties on the ClusterTemplate class. + :returns: The results of cluster_template creation + :rtype: + :class:`~openstack.container_infrastructure_management.v1.cluster_template.ClusterTemplate` + """ + return self._create(_cluster_template.ClusterTemplate, **attrs) + + def delete_cluster_template(self, cluster_template, ignore_missing=True): + """Delete a cluster_template + + :param cluster_template: The value can be either the ID of a + cluster_template or a + :class:`~openstack.container_infrastructure_management.v1.cluster_template.ClusterTemplate` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the cluster_template does not exist. When set to ``True``, no + exception will be set when attempting to delete a nonexistent + cluster_template. + :returns: ``None`` + """ + self._delete( + _cluster_template.ClusterTemplate, + cluster_template, + ignore_missing=ignore_missing, + ) + + def find_cluster_template(self, name_or_id, ignore_missing=True): + """Find a single cluster_template + + :param name_or_id: The name or ID of a cluster_template. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :returns: One + :class:`~openstack.container_infrastructure_management.v1.cluster_template.ClusterTemplate` + or None + """ + return self._find( + _cluster_template.ClusterTemplate, + name_or_id, + ignore_missing=ignore_missing, + ) + + def get_cluster_template(self, cluster_template): + """Get a single cluster_template + + :param cluster_template: The value can be the ID of a cluster_template + or a + :class:`~openstack.container_infrastructure_management.v1.cluster_template.ClusterTemplate` + instance. + + :returns: One + :class:`~openstack.container_infrastructure_management.v1.cluster_template.ClusterTemplate` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_cluster_template.ClusterTemplate, cluster_template) + + def cluster_templates(self, **query): + """Return a generator of cluster_templates + + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. + + :returns: A generator of cluster_template objects + :rtype: + :class:`~openstack.container_infrastructure_management.v1.cluster_template.ClusterTemplate` + """ + return self._list(_cluster_template.ClusterTemplate, **query) + + def update_cluster_template(self, cluster_template, **attrs): + """Update a cluster_template + + :param cluster_template: Either the id of a cluster_template or a + :class:`~openstack.container_infrastructure_management.v1.cluster_template.ClusterTemplate` + instance. + :param attrs: The attributes to update on the cluster_template + represented by ``cluster_template``. + + :returns: The updated cluster_template + :rtype: + :class:`~openstack.container_infrastructure_management.v1.cluster_template.ClusterTemplate` + """ + return self._update( + _cluster_template.ClusterTemplate, cluster_template, **attrs + ) diff --git a/openstack/container_infrastructure_management/v1/cluster.py b/openstack/container_infrastructure_management/v1/cluster.py index 684f096d0..bd7618555 100644 --- a/openstack/container_infrastructure_management/v1/cluster.py +++ b/openstack/container_infrastructure_management/v1/cluster.py @@ -88,11 +88,10 @@ class Cluster(resource.Resource): #: the bay/cluster. The login name is specific to the bay/cluster driver. #: For example, with fedora-atomic image the default login name is fedora. keypair = resource.Body('keypair') - #: Arbitrary labels in the form of key=value pairs. The accepted keys and - #: valid values are defined in the bay/cluster drivers. They are used as a - #: way to pass additional parameters that are specific to a bay/cluster - #: driver. - labels = resource.Body('labels', type=list) + #: Arbitrary labels. The accepted keys and valid values are defined in the + #: bay/cluster drivers. They are used as a way to pass additional + #: parameters that are specific to a bay/cluster driver. + labels = resource.Body('labels', type=dict) #: A list of floating IPs of all master nodes. master_addresses = resource.Body('master_addresses', type=list) #: The number of servers that will serve as master for the bay/cluster. Set diff --git a/openstack/container_infrastructure_management/v1/cluster_template.py b/openstack/container_infrastructure_management/v1/cluster_template.py new file mode 100644 index 000000000..31acf376f --- /dev/null +++ b/openstack/container_infrastructure_management/v1/cluster_template.py @@ -0,0 +1,116 @@ +# 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 resource + + +class ClusterTemplate(resource.Resource): + + resources_key = 'clustertemplates' + base_path = '/clustertemplates' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + allow_patch = True + + commit_method = 'PATCH' + commit_jsonpatch = True + + #: The exposed port of COE API server. + apiserver_port = resource.Body('apiserver_port', type=int) + #: Display the attribute os_distro defined as appropriate metadata in image + #: for the bay/cluster driver. + cluster_distro = resource.Body('cluster_distro') + #: Specify the Container Orchestration Engine to use. Supported COEs + #: include kubernetes, swarm, mesos. + coe = resource.Body('coe') + #: The date and time when the resource was created. + created_at = resource.Body('created_at') + #: The name of a driver to manage the storage for the images and the + #: container’s writable layer. + docker_storage_driver = resource.Body('docker_storage_driver') + #: The size in GB for the local storage on each server for the Docker + #: daemon to cache the images and host the containers. + docker_volume_size = resource.Body('docker_volume_size', type=int) + #: The DNS nameserver for the servers and containers in the bay/cluster to + #: use. + dns_nameserver = resource.Body('dns_nameserver') + #: The name or network ID of a Neutron network to provide connectivity to + #: the external internet for the bay/cluster. + external_network_id = resource.Body('external_network_id') + #: The name or network ID of a Neutron network to provide connectivity to + #: the internal network for the bay/cluster. + fixed_network = resource.Body('fixed_network') + #: Fixed subnet that are using to allocate network address for nodes in + #: bay/cluster. + fixed_subnet = resource.Body('fixed_subnet') + #: The nova flavor ID or name for booting the node servers. + flavor_id = resource.Body('flavor_id') + #: The IP address for a proxy to use when direct http access + #: from the servers to sites on the external internet is blocked. + #: This may happen in certain countries or enterprises, and the + #: proxy allows the servers and containers to access these sites. + #: The format is a URL including a port number. The default is + #: None. + http_proxy = resource.Body('http_proxy') + #: The IP address for a proxy to use when direct https access from the + #: servers to sites on the external internet is blocked. + https_proxy = resource.Body('https_proxy') + #: The name or UUID of the base image in Glance to boot the servers for the + #: bay/cluster. + image_id = resource.Body('image_id') + #: The URL pointing to users’s own private insecure docker + #: registry to deploy and run docker containers. + insecure_registry = resource.Body('insecure_registry') + #: Whether enable or not using the floating IP of cloud provider. + is_floating_ip_enabled = resource.Body('floating_ip_enabled') + #: Indicates whether the ClusterTemplate is hidden or not. + is_hidden = resource.Body('hidden', type=bool) + #: this option can be set to false to create a bay/cluster without the load + #: balancer. + is_master_lb_enabled = resource.Body('master_lb_enabled', type=bool) + #: Specifying this parameter will disable TLS so that users can access the + #: COE endpoints without a certificate. + is_tls_disabled = resource.Body('tls_disabled', type=bool) + #: Setting this flag makes the baymodel/cluster template public and + #: accessible by other users. + is_public = resource.Body('public', type=bool) + #: This option provides an alternative registry based on the Registry V2 + is_registry_enabled = resource.Body('registry_enabled', type=bool) + #: The name of the SSH keypair to configure in the bay/cluster servers for + #: ssh access. + keypair_id = resource.Body('keypair_id') + #: Arbitrary labels. The accepted keys and valid values are defined in the + #: bay/cluster drivers. They are used as a way to pass additional + #: parameters that are specific to a bay/cluster driver. + labels = resource.Body('labels', type=dict) + #: The flavor of the master node for this baymodel/cluster template. + master_flavor_id = resource.Body('master_flavor_id') + #: The name of a network driver for providing the networks for the + #: containers. + network_driver = resource.Body('network_driver') + #: When a proxy server is used, some sites should not go through the proxy + #: and should be accessed normally. + no_proxy = resource.Body('no_proxy') + #: The servers in the bay/cluster can be vm or baremetal. + server_type = resource.Body('server_type') + #: The date and time when the resource was updated. + updated_at = resource.Body('updated_at') + #: The UUID of the cluster template. + uuid = resource.Body('uuid', alternate_id=True) + #: The name of a volume driver for managing the persistent storage for the + #: containers. + volume_driver = resource.Body('volume_driver') diff --git a/openstack/tests/functional/cloud/test_cluster_templates.py b/openstack/tests/functional/cloud/test_cluster_templates.py index 2d2c2a479..afc304bba 100644 --- a/openstack/tests/functional/cloud/test_cluster_templates.py +++ b/openstack/tests/functional/cloud/test_cluster_templates.py @@ -90,7 +90,7 @@ def test_cluster_templates(self): # Test we can update a field on the cluster_template and only that # field is updated cluster_template_update = self.user_cloud.update_cluster_template( - self.ct['uuid'], 'replace', tls_disabled=True) + self.ct, tls_disabled=True) self.assertEqual( cluster_template_update['uuid'], self.ct['uuid']) self.assertTrue(cluster_template_update['tls_disabled']) diff --git a/openstack/tests/unit/cloud/test_cluster_templates.py b/openstack/tests/unit/cloud/test_cluster_templates.py index 15e41d836..c6f5ad70f 100644 --- a/openstack/tests/unit/cloud/test_cluster_templates.py +++ b/openstack/tests/unit/cloud/test_cluster_templates.py @@ -10,14 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. -import munch import testtools +from openstack.container_infrastructure_management.v1 import cluster_template from openstack import exceptions from openstack.tests.unit import base -cluster_template_obj = munch.Munch( +cluster_template_obj = dict( apiserver_port=12345, cluster_distro='fake-distro', coe='fake-coe', @@ -50,6 +50,12 @@ class TestClusterTemplates(base.TestCase): + def _compare_clustertemplates(self, exp, real): + self.assertDictEqual( + cluster_template.ClusterTemplate(**exp).to_dict(computed=False), + real.to_dict(computed=False), + ) + def get_mock_url( self, service_type='container-infrastructure-management', @@ -64,15 +70,12 @@ def test_list_cluster_templates_without_detail(self): dict( method='GET', uri=self.get_mock_url(resource='clustertemplates'), - status_code=404), - dict( - method='GET', - uri=self.get_mock_url(resource='baymodels/detail'), - json=dict(baymodels=[cluster_template_obj.toDict()]))]) + json=dict(clustertemplates=[cluster_template_obj]))]) cluster_templates_list = self.cloud.list_cluster_templates() - self.assertEqual( + self._compare_clustertemplates( + cluster_template_obj, cluster_templates_list[0], - self.cloud._normalize_cluster_template(cluster_template_obj)) + ) self.assert_calls() def test_list_cluster_templates_with_detail(self): @@ -80,15 +83,12 @@ def test_list_cluster_templates_with_detail(self): dict( method='GET', uri=self.get_mock_url(resource='clustertemplates'), - status_code=404), - dict( - method='GET', - uri=self.get_mock_url(resource='baymodels/detail'), - json=dict(baymodels=[cluster_template_obj.toDict()]))]) + json=dict(clustertemplates=[cluster_template_obj]))]) cluster_templates_list = self.cloud.list_cluster_templates(detail=True) - self.assertEqual( + self._compare_clustertemplates( + cluster_template_obj, cluster_templates_list[0], - self.cloud._normalize_cluster_template(cluster_template_obj)) + ) self.assert_calls() def test_search_cluster_templates_by_name(self): @@ -96,11 +96,7 @@ def test_search_cluster_templates_by_name(self): dict( method='GET', uri=self.get_mock_url(resource='clustertemplates'), - status_code=404), - dict( - method='GET', - uri=self.get_mock_url(resource='baymodels/detail'), - json=dict(baymodels=[cluster_template_obj.toDict()]))]) + json=dict(clustertemplates=[cluster_template_obj]))]) cluster_templates = self.cloud.search_cluster_templates( name_or_id='fake-cluster-template') @@ -115,11 +111,7 @@ def test_search_cluster_templates_not_found(self): dict( method='GET', uri=self.get_mock_url(resource='clustertemplates'), - status_code=404), - dict( - method='GET', - uri=self.get_mock_url(resource='baymodels/detail'), - json=dict(baymodels=[cluster_template_obj.toDict()]))]) + json=dict(clustertemplates=[cluster_template_obj]))]) cluster_templates = self.cloud.search_cluster_templates( name_or_id='non-existent') @@ -132,16 +124,14 @@ def test_get_cluster_template(self): dict( method='GET', uri=self.get_mock_url(resource='clustertemplates'), - status_code=404), - dict( - method='GET', - uri=self.get_mock_url(resource='baymodels/detail'), - json=dict(baymodels=[cluster_template_obj.toDict()]))]) + json=dict(clustertemplates=[cluster_template_obj]))]) r = self.cloud.get_cluster_template('fake-cluster-template') self.assertIsNotNone(r) - self.assertDictEqual( - r, self.cloud._normalize_cluster_template(cluster_template_obj)) + self._compare_clustertemplates( + cluster_template_obj, + r, + ) self.assert_calls() def test_get_cluster_template_not_found(self): @@ -149,35 +139,29 @@ def test_get_cluster_template_not_found(self): dict( method='GET', uri=self.get_mock_url(resource='clustertemplates'), - status_code=404), - dict( - method='GET', - uri=self.get_mock_url(resource='baymodels/detail'), - json=dict(baymodels=[]))]) + json=dict(clustertemplates=[]))]) r = self.cloud.get_cluster_template('doesNotExist') self.assertIsNone(r) self.assert_calls() def test_create_cluster_template(self): - json_response = cluster_template_obj.toDict() - kwargs = dict(name=cluster_template_obj.name, - image_id=cluster_template_obj.image_id, - keypair_id=cluster_template_obj.keypair_id, - coe=cluster_template_obj.coe) + json_response = cluster_template_obj.copy() + kwargs = dict(name=cluster_template_obj['name'], + image_id=cluster_template_obj['image_id'], + keypair_id=cluster_template_obj['keypair_id'], + coe=cluster_template_obj['coe']) self.register_uris([ dict( method='POST', uri=self.get_mock_url(resource='clustertemplates'), - status_code=404), - dict( - method='POST', - uri=self.get_mock_url(resource='baymodels'), json=json_response, - validate=dict(json=kwargs)), - ]) - expected = self.cloud._normalize_cluster_template(json_response) + validate=dict(json=kwargs))]) response = self.cloud.create_cluster_template(**kwargs) - self.assertEqual(response, expected) + self._compare_clustertemplates( + json_response, + response + ) + self.assert_calls() def test_create_cluster_template_exception(self): @@ -185,10 +169,6 @@ def test_create_cluster_template_exception(self): dict( method='POST', uri=self.get_mock_url(resource='clustertemplates'), - status_code=404), - dict( - method='POST', - uri=self.get_mock_url(resource='baymodels'), status_code=403)]) # TODO(mordred) requests here doens't give us a great story # for matching the old error message text. Investigate plumbing @@ -207,14 +187,10 @@ def test_delete_cluster_template(self): dict( method='GET', uri=self.get_mock_url(resource='clustertemplates'), - status_code=404), - dict( - method='GET', - uri=self.get_mock_url(resource='baymodels/detail'), - json=dict(baymodels=[cluster_template_obj.toDict()])), + json=dict(clustertemplates=[cluster_template_obj])), dict( method='DELETE', - uri=self.get_mock_url(resource='baymodels/fake-uuid')), + uri=self.get_mock_url(resource='clustertemplates/fake-uuid')), ]) self.cloud.delete_cluster_template('fake-uuid') self.assert_calls() @@ -224,43 +200,36 @@ def test_update_cluster_template(self): dict( method='GET', uri=self.get_mock_url(resource='clustertemplates'), - status_code=404), - dict( - method='GET', - uri=self.get_mock_url(resource='baymodels/detail'), - json=dict(baymodels=[cluster_template_obj.toDict()])), + json=dict(clustertemplates=[cluster_template_obj])), dict( method='PATCH', - uri=self.get_mock_url(resource='baymodels/fake-uuid'), + uri=self.get_mock_url(resource='clustertemplates/fake-uuid'), status_code=200, validate=dict( json=[{ - u'op': u'replace', - u'path': u'/name', - u'value': u'new-cluster-template' + 'op': 'replace', + 'path': '/name', + 'value': 'new-cluster-template' }] )), - dict( - method='GET', - uri=self.get_mock_url(resource='clustertemplates'), - # This json value is not meaningful to the test - it just has - # to be valid. - json=dict(baymodels=[cluster_template_obj.toDict()])), ]) new_name = 'new-cluster-template' - self.cloud.update_cluster_template( - 'fake-uuid', 'replace', name=new_name) + updated = self.cloud.update_cluster_template( + 'fake-uuid', name=new_name) + self.assertEqual(new_name, updated.name) self.assert_calls() - def test_get_coe_cluster_template(self): + def test_coe_get_cluster_template(self): self.register_uris([ dict( method='GET', uri=self.get_mock_url(resource='clustertemplates'), - json=dict(clustertemplates=[cluster_template_obj.toDict()]))]) + json=dict(clustertemplates=[cluster_template_obj]))]) - r = self.cloud.get_coe_cluster_template('fake-cluster-template') + r = self.cloud.get_cluster_template('fake-cluster-template') self.assertIsNotNone(r) - self.assertDictEqual( - r, self.cloud._normalize_cluster_template(cluster_template_obj)) + self._compare_clustertemplates( + cluster_template_obj, + r, + ) self.assert_calls() diff --git a/openstack/tests/unit/container_infrastructure_management/v1/test_cluster.py b/openstack/tests/unit/container_infrastructure_management/v1/test_cluster.py index 8e370877b..809eeb099 100644 --- a/openstack/tests/unit/container_infrastructure_management/v1/test_cluster.py +++ b/openstack/tests/unit/container_infrastructure_management/v1/test_cluster.py @@ -19,7 +19,7 @@ "discovery_url": None, "flavor_id": None, "keypair": "my_keypair", - "labels": [], + "labels": {}, "master_count": 2, "master_flavor_id": None, "name": "k8s", diff --git a/openstack/tests/unit/container_infrastructure_management/v1/test_cluster_template.py b/openstack/tests/unit/container_infrastructure_management/v1/test_cluster_template.py new file mode 100644 index 000000000..a3a8b5272 --- /dev/null +++ b/openstack/tests/unit/container_infrastructure_management/v1/test_cluster_template.py @@ -0,0 +1,98 @@ +# 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.container_infrastructure_management.v1 import cluster_template +from openstack.tests.unit import base + +EXAMPLE = { + "insecure_registry": None, + "http_proxy": "http://10.164.177.169:8080", + "updated_at": None, + "floating_ip_enabled": True, + "fixed_subnet": None, + "master_flavor_id": None, + "uuid": "085e1c4d-4f68-4bfd-8462-74b9e14e4f39", + "no_proxy": "10.0.0.0/8,172.0.0.0/8,192.0.0.0/8,localhost", + "https_proxy": "http://10.164.177.169:8080", + "tls_disabled": False, + "keypair_id": "kp", + "public": False, + "labels": {}, + "docker_volume_size": 3, + "server_type": "vm", + "external_network_id": "public", + "cluster_distro": "fedora-atomic", + "image_id": "fedora-atomic-latest", + "volume_driver": "cinder", + "registry_enabled": False, + "docker_storage_driver": "devicemapper", + "apiserver_port": None, + "name": "k8s-bm2", + "created_at": "2016-08-29T02:08:08+00:00", + "network_driver": "flannel", + "fixed_network": None, + "coe": "kubernetes", + "flavor_id": "m1.small", + "master_lb_enabled": True, + "dns_nameserver": "8.8.8.8", + "hidden": True, +} + + +class TestClusterTemplate(base.TestCase): + def test_basic(self): + sot = cluster_template.ClusterTemplate() + self.assertIsNone(sot.resource_key) + self.assertEqual('clustertemplates', sot.resources_key) + self.assertEqual('/clustertemplates', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = cluster_template.ClusterTemplate(**EXAMPLE) + + self.assertEqual(EXAMPLE['apiserver_port'], sot.apiserver_port) + self.assertEqual(EXAMPLE['cluster_distro'], sot.cluster_distro) + self.assertEqual(EXAMPLE['coe'], sot.coe) + self.assertEqual(EXAMPLE['created_at'], sot.created_at) + self.assertEqual(EXAMPLE['docker_storage_driver'], + sot.docker_storage_driver) + self.assertEqual(EXAMPLE['docker_volume_size'], sot.docker_volume_size) + self.assertEqual(EXAMPLE['dns_nameserver'], sot.dns_nameserver) + self.assertEqual(EXAMPLE['external_network_id'], + sot.external_network_id) + self.assertEqual(EXAMPLE['fixed_network'], sot.fixed_network) + self.assertEqual(EXAMPLE['fixed_subnet'], sot.fixed_subnet) + self.assertEqual(EXAMPLE['flavor_id'], sot.flavor_id) + self.assertEqual(EXAMPLE['http_proxy'], sot.http_proxy) + self.assertEqual(EXAMPLE['https_proxy'], sot.https_proxy) + self.assertEqual(EXAMPLE['image_id'], sot.image_id) + self.assertEqual(EXAMPLE['insecure_registry'], sot.insecure_registry) + self.assertEqual(EXAMPLE['floating_ip_enabled'], + sot.is_floating_ip_enabled) + self.assertEqual(EXAMPLE['hidden'], sot.is_hidden) + self.assertEqual(EXAMPLE['master_lb_enabled'], + sot.is_master_lb_enabled) + self.assertEqual(EXAMPLE['tls_disabled'], sot.is_tls_disabled) + self.assertEqual(EXAMPLE['public'], sot.is_public) + self.assertEqual(EXAMPLE['registry_enabled'], sot.is_registry_enabled) + self.assertEqual(EXAMPLE['keypair_id'], sot.keypair_id) + self.assertEqual(EXAMPLE['master_flavor_id'], sot.master_flavor_id) + self.assertEqual(EXAMPLE['network_driver'], sot.network_driver) + self.assertEqual(EXAMPLE['no_proxy'], sot.no_proxy) + self.assertEqual(EXAMPLE['server_type'], sot.server_type) + self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) + self.assertEqual(EXAMPLE['uuid'], sot.uuid) + self.assertEqual(EXAMPLE['volume_driver'], sot.volume_driver) diff --git a/openstack/tests/unit/container_infrastructure_management/v1/test_proxy.py b/openstack/tests/unit/container_infrastructure_management/v1/test_proxy.py index 3f2555693..337bdee72 100644 --- a/openstack/tests/unit/container_infrastructure_management/v1/test_proxy.py +++ b/openstack/tests/unit/container_infrastructure_management/v1/test_proxy.py @@ -12,16 +12,17 @@ from openstack.container_infrastructure_management.v1 import _proxy from openstack.container_infrastructure_management.v1 import cluster +from openstack.container_infrastructure_management.v1 import cluster_template from openstack.tests.unit import test_proxy_base -class TestClusterProxy(test_proxy_base.TestProxyBase): +class TestMagnumProxy(test_proxy_base.TestProxyBase): def setUp(self): super().setUp() self.proxy = _proxy.Proxy(self.session) -class TestCluster(TestClusterProxy): +class TestCluster(TestMagnumProxy): def test_cluster_get(self): self.verify_get(self.proxy.get_cluster, cluster.Cluster) @@ -49,3 +50,46 @@ def test_cluster_delete(self): def test_cluster_delete_ignore(self): self.verify_delete(self.proxy.delete_cluster, cluster.Cluster, True) + + +class TestClusterTemplate(TestMagnumProxy): + def test_cluster_template_get(self): + self.verify_get( + self.proxy.get_cluster_template, cluster_template.ClusterTemplate + ) + + def test_cluster_template_find(self): + self.verify_find( + self.proxy.find_cluster_template, + cluster_template.ClusterTemplate, + method_kwargs={}, + expected_kwargs={}, + ) + + def test_cluster_templates(self): + self.verify_list( + self.proxy.cluster_templates, + cluster_template.ClusterTemplate, + method_kwargs={"query": 1}, + expected_kwargs={"query": 1}, + ) + + def test_cluster_template_create_attrs(self): + self.verify_create( + self.proxy.create_cluster_template, + cluster_template.ClusterTemplate, + ) + + def test_cluster_template_delete(self): + self.verify_delete( + self.proxy.delete_cluster_template, + cluster_template.ClusterTemplate, + False, + ) + + def test_cluster_template_delete_ignore(self): + self.verify_delete( + self.proxy.delete_cluster_template, + cluster_template.ClusterTemplate, + True, + ) From a27619cbf430fc99fb16e4adae0bb2cbda1e4507 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 27 Jan 2023 12:34:08 +0100 Subject: [PATCH 3190/3836] Finish Magnum rework - add coe service resource and proxy methods - add coe cluster certificates resource and methods - switch remaining cloud methods to use proxy - add coe docs Change-Id: I7532d03ad26785dccdcc37b19165c19246ebd6e1 --- doc/source/user/index.rst | 2 + .../container_infrastructure_management.rst | 35 ++++ .../cluster.rst | 12 ++ .../cluster_certificate.rst | 13 ++ .../cluster_template.rst | 13 ++ .../index.rst | 10 + .../service.rst | 12 ++ openstack/cloud/_coe.py | 182 +----------------- .../v1/_proxy.py | 45 +++++ .../v1/cluster_certificate.py | 32 +++ .../v1/service.py | 38 ++++ .../cloud/test_coe_clusters_certificate.py | 29 +-- .../tests/unit/cloud/test_magnum_services.py | 7 +- .../v1/test_cluster_certificate.py | 43 +++++ .../v1/test_proxy.py | 29 +++ .../v1/test_service.py | 49 +++++ .../switch-coe-to-proxy-c18789ed27cc1d95.yaml | 5 + 17 files changed, 367 insertions(+), 189 deletions(-) create mode 100644 doc/source/user/proxies/container_infrastructure_management.rst create mode 100644 doc/source/user/resources/container_infrastructure_management/cluster.rst create mode 100644 doc/source/user/resources/container_infrastructure_management/cluster_certificate.rst create mode 100644 doc/source/user/resources/container_infrastructure_management/cluster_template.rst create mode 100644 doc/source/user/resources/container_infrastructure_management/index.rst create mode 100644 doc/source/user/resources/container_infrastructure_management/service.rst create mode 100644 openstack/container_infrastructure_management/v1/cluster_certificate.py create mode 100644 openstack/container_infrastructure_management/v1/service.py create mode 100644 openstack/tests/unit/container_infrastructure_management/v1/test_cluster_certificate.py create mode 100644 openstack/tests/unit/container_infrastructure_management/v1/test_service.py create mode 100644 releasenotes/notes/switch-coe-to-proxy-c18789ed27cc1d95.yaml diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index b3e968d43..51a242047 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -96,6 +96,7 @@ control which services can be used. Block Storage v3 Clustering Compute + Container Infrastructure Management Database DNS Identity v2 @@ -133,6 +134,7 @@ The following services have exposed *Resource* classes. Block Storage Clustering Compute + Container Infrastructure Management Database DNS Identity diff --git a/doc/source/user/proxies/container_infrastructure_management.rst b/doc/source/user/proxies/container_infrastructure_management.rst new file mode 100644 index 000000000..99e33063a --- /dev/null +++ b/doc/source/user/proxies/container_infrastructure_management.rst @@ -0,0 +1,35 @@ +Container Infrastructure Management +=================================== + +.. automodule:: openstack.container_infrastructure_management.v1._proxy + +Cluster Operations +^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.container_infrastructure_management.v1._proxy.Proxy + :noindex: + :members: create_cluster, delete_cluster, update_cluster, get_cluster, + find_cluster, clusters + +Cluster Certificates Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.container_infrastructure_management.v1._proxy.Proxy + :noindex: + :members: create_cluster_certificate, get_cluster_certificate + +Cluster Templates Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.container_infrastructure_management.v1._proxy.Proxy + :noindex: + :members: create_cluster_template, delete_cluster_template, + find_cluster_template, + get_cluster_template, cluster_templates, update_cluster_template + +Service Operations +^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.container_infrastructure_management.v1._proxy.Proxy + :noindex: + :members: services diff --git a/doc/source/user/resources/container_infrastructure_management/cluster.rst b/doc/source/user/resources/container_infrastructure_management/cluster.rst new file mode 100644 index 000000000..df44b6fad --- /dev/null +++ b/doc/source/user/resources/container_infrastructure_management/cluster.rst @@ -0,0 +1,12 @@ +openstack.container_infrastructure_management.v1.cluster +======================================================== + +.. automodule:: openstack.container_infrastructure_management.v1.cluster + +The Cluster Class +------------------ + +The ``Cluster`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.container_infrastructure_management.v1.cluster.Cluster + :members: diff --git a/doc/source/user/resources/container_infrastructure_management/cluster_certificate.rst b/doc/source/user/resources/container_infrastructure_management/cluster_certificate.rst new file mode 100644 index 000000000..6dbb3b102 --- /dev/null +++ b/doc/source/user/resources/container_infrastructure_management/cluster_certificate.rst @@ -0,0 +1,13 @@ +openstack.container_infrastructure_management.v1.cluster_certificate +==================================================================== + +.. automodule:: openstack.container_infrastructure_management.v1.cluster_certificate + +The Cluster Certificate Class +----------------------------- + +The ``ClusterCertificate`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.container_infrastructure_management.v1.cluster_certificate.ClusterCertificate + :members: diff --git a/doc/source/user/resources/container_infrastructure_management/cluster_template.rst b/doc/source/user/resources/container_infrastructure_management/cluster_template.rst new file mode 100644 index 000000000..fbf17725a --- /dev/null +++ b/doc/source/user/resources/container_infrastructure_management/cluster_template.rst @@ -0,0 +1,13 @@ +openstack.container_infrastructure_management.v1.cluster_template +================================================================= + +.. automodule:: openstack.container_infrastructure_management.v1.cluster_template + +The Cluster Template Class +-------------------------- + +The ``ClusterTemplate`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.container_infrastructure_management.v1.cluster_template.ClusterTemplate + :members: diff --git a/doc/source/user/resources/container_infrastructure_management/index.rst b/doc/source/user/resources/container_infrastructure_management/index.rst new file mode 100644 index 000000000..90fdb8558 --- /dev/null +++ b/doc/source/user/resources/container_infrastructure_management/index.rst @@ -0,0 +1,10 @@ +Container Infrastructure Management Resources +============================================= + +.. toctree:: + :maxdepth: 1 + + cluster + cluster_certificate + cluster_template + service diff --git a/doc/source/user/resources/container_infrastructure_management/service.rst b/doc/source/user/resources/container_infrastructure_management/service.rst new file mode 100644 index 000000000..2be9147d4 --- /dev/null +++ b/doc/source/user/resources/container_infrastructure_management/service.rst @@ -0,0 +1,12 @@ +openstack.container_infrastructure_management.v1.service +======================================================== + +.. automodule:: openstack.container_infrastructure_management.v1.service + +The Service Class +----------------- + +The ``Service`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.container_infrastructure_management.v1.service.Service + :members: diff --git a/openstack/cloud/_coe.py b/openstack/cloud/_coe.py index 401564ecc..6a5996f94 100644 --- a/openstack/cloud/_coe.py +++ b/openstack/cloud/_coe.py @@ -21,13 +21,6 @@ class CoeCloudMixin: - @property - def _container_infra_client(self): - if 'container-infra' not in self._raw_clients: - self._raw_clients['container-infra'] = self._get_raw_client( - 'container-infra') - return self._raw_clients['container-infra'] - @_utils.cache_on_arguments() def list_coe_clusters(self): """List COE (Container Orchestration Engine) cluster. @@ -157,13 +150,8 @@ def get_coe_cluster_certificate(self, cluster_id): :returns: Details about the CA certificate for the given cluster. """ - msg = ("Error fetching CA cert for the cluster {cluster_id}".format( - cluster_id=cluster_id)) - url = "/certificates/{cluster_id}".format(cluster_id=cluster_id) - data = self._container_infra_client.get(url, - error_message=msg) - - return self._get_and_munchify(key=None, data=data) + return self.container_infrastructure_management\ + .get_cluster_certificate(cluster_id) def sign_coe_cluster_certificate(self, cluster_id, csr): """Sign client key and generate the CA certificate for a cluster @@ -177,17 +165,10 @@ def sign_coe_cluster_certificate(self, cluster_id, csr): :raises: OpenStackCloudException on operation error. """ - error_message = ("Error signing certs for cluster" - " {cluster_id}".format(cluster_id=cluster_id)) - with _utils.shade_exceptions(error_message): - body = {} - body['cluster_uuid'] = cluster_id - body['csr'] = csr - - certs = self._container_infra_client.post( - '/certificates', json=body) - - return self._get_and_munchify(key=None, data=certs) + return self.container_infrastructure_management\ + .create_cluster_certificate( + cluster_uuid=cluster_id, + csr=csr) @_utils.cache_on_arguments() def list_cluster_templates(self, detail=False): @@ -325,153 +306,4 @@ def list_magnum_services(self): :raises: OpenStackCloudException on operation error. """ - with _utils.shade_exceptions("Error fetching Magnum services list"): - data = self._container_infra_client.get('/mservices') - return self._normalize_magnum_services( - self._get_and_munchify('mservices', data)) - - def _normalize_coe_clusters(self, coe_clusters): - ret = [] - for coe_cluster in coe_clusters: - ret.append(self._normalize_coe_cluster(coe_cluster)) - return ret - - def _normalize_coe_cluster(self, coe_cluster): - """Normalize Magnum COE cluster.""" - - # Only import munch when really necessary - import munch - - coe_cluster = coe_cluster.copy() - - # Discard noise - coe_cluster.pop('links', None) - - c_id = coe_cluster.pop('uuid') - - ret = munch.Munch( - id=c_id, - location=self._get_current_location(), - ) - - if not self.strict_mode: - ret['uuid'] = c_id - - for key in ( - 'status', - 'cluster_template_id', - 'stack_id', - 'keypair', - 'master_count', - 'create_timeout', - 'node_count', - 'name'): - if key in coe_cluster: - ret[key] = coe_cluster.pop(key) - - ret['properties'] = coe_cluster - return ret - - def _normalize_cluster_templates(self, cluster_templates): - ret = [] - for cluster_template in cluster_templates: - ret.append(self._normalize_cluster_template(cluster_template)) - return ret - - def _normalize_cluster_template(self, cluster_template): - """Normalize Magnum cluster_templates.""" - - import munch - - cluster_template = cluster_template.copy() - - # Discard noise - cluster_template.pop('links', None) - cluster_template.pop('human_id', None) - # model_name is a magnumclient-ism - cluster_template.pop('model_name', None) - - ct_id = cluster_template.pop('uuid') - - ret = munch.Munch( - id=ct_id, - location=self._get_current_location(), - ) - ret['is_public'] = cluster_template.pop('public') - ret['is_registry_enabled'] = cluster_template.pop('registry_enabled') - ret['is_tls_disabled'] = cluster_template.pop('tls_disabled') - # pop floating_ip_enabled since we want to hide it in a future patch - fip_enabled = cluster_template.pop('floating_ip_enabled', None) - if not self.strict_mode: - ret['uuid'] = ct_id - if fip_enabled is not None: - ret['floating_ip_enabled'] = fip_enabled - ret['public'] = ret['is_public'] - ret['registry_enabled'] = ret['is_registry_enabled'] - ret['tls_disabled'] = ret['is_tls_disabled'] - - # Optional keys - for (key, default) in ( - ('fixed_network', None), - ('fixed_subnet', None), - ('http_proxy', None), - ('https_proxy', None), - ('labels', {}), - ('master_flavor_id', None), - ('no_proxy', None)): - if key in cluster_template: - ret[key] = cluster_template.pop(key, default) - - for key in ( - 'apiserver_port', - 'cluster_distro', - 'coe', - 'created_at', - 'dns_nameserver', - 'docker_volume_size', - 'external_network_id', - 'flavor_id', - 'image_id', - 'insecure_registry', - 'keypair_id', - 'name', - 'network_driver', - 'server_type', - 'updated_at', - 'volume_driver'): - ret[key] = cluster_template.pop(key) - - ret['properties'] = cluster_template - return ret - - def _normalize_magnum_services(self, magnum_services): - ret = [] - for magnum_service in magnum_services: - ret.append(self._normalize_magnum_service(magnum_service)) - return ret - - def _normalize_magnum_service(self, magnum_service): - """Normalize Magnum magnum_services.""" - import munch - magnum_service = magnum_service.copy() - - # Discard noise - magnum_service.pop('links', None) - magnum_service.pop('human_id', None) - # model_name is a magnumclient-ism - magnum_service.pop('model_name', None) - - ret = munch.Munch(location=self._get_current_location()) - - for key in ( - 'binary', - 'created_at', - 'disabled_reason', - 'host', - 'id', - 'report_count', - 'state', - 'updated_at'): - ret[key] = magnum_service.pop(key) - ret['properties'] = magnum_service - return ret + return list(self.container_infrastructure_management.services()) diff --git a/openstack/container_infrastructure_management/v1/_proxy.py b/openstack/container_infrastructure_management/v1/_proxy.py index ca40a8227..27adc7965 100644 --- a/openstack/container_infrastructure_management/v1/_proxy.py +++ b/openstack/container_infrastructure_management/v1/_proxy.py @@ -13,9 +13,15 @@ from openstack.container_infrastructure_management.v1 import ( cluster as _cluster ) +from openstack.container_infrastructure_management.v1 import ( + cluster_certificate as _cluster_cert +) from openstack.container_infrastructure_management.v1 import ( cluster_template as _cluster_template ) +from openstack.container_infrastructure_management.v1 import ( + service as _service +) from openstack import proxy @@ -24,6 +30,7 @@ class Proxy(proxy.Proxy): _resource_registry = { "cluster": _cluster.Cluster, "cluster_template": _cluster_template.ClusterTemplate, + "service": _service.Service } def create_cluster(self, **attrs): @@ -207,3 +214,41 @@ def update_cluster_template(self, cluster_template, **attrs): return self._update( _cluster_template.ClusterTemplate, cluster_template, **attrs ) + + # ============== Cluster Certificates ============== + def create_cluster_certificate(self, **attrs): + """Create a new cluster_certificate from CSR + + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.container_infrastructure_management.v1.cluster_certificate.ClusterCertificate`, + comprised of the properties on the ClusterCertificate class. + :returns: The results of cluster_certificate creation + :rtype: + :class:`~openstack.container_infrastructure_management.v1.cluster_certificate.ClusterCertificate` + """ + return self._create(_cluster_cert.ClusterCertificate, **attrs) + + def get_cluster_certificate(self, cluster_certificate): + """Get a single cluster_certificate + + :param cluster_certificate: The value can be the ID of a + cluster_certificate or a + :class:`~openstack.container_infrastructure_management.v1.cluster_certificate.ClusterCertificate` + instance. + + :returns: One + :class:`~openstack.container_infrastructure_management.v1.cluster_certificate.ClusterCertificate` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_cluster_cert.ClusterCertificate, cluster_certificate) + + # ============== Services ============== + def services(self): + """Return a generator of services + + :returns: A generator of service objects + :rtype: + :class:`~openstack.container_infrastructure_management.v1.service.Service` + """ + return self._list(_service.Service) diff --git a/openstack/container_infrastructure_management/v1/cluster_certificate.py b/openstack/container_infrastructure_management/v1/cluster_certificate.py new file mode 100644 index 000000000..5bf9a3656 --- /dev/null +++ b/openstack/container_infrastructure_management/v1/cluster_certificate.py @@ -0,0 +1,32 @@ +# 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 resource + + +class ClusterCertificate(resource.Resource): + + base_path = '/certificates' + + # capabilities + allow_create = True + allow_list = False + allow_fetch = True + + #: The UUID of the bay. + bay_uuid = resource.Body('bay_uuid') + #: The UUID of the cluster. + cluster_uuid = resource.Body('cluster_uuid', alternate_id=True) + #: Certificate Signing Request (CSR) for authenticating client key. + csr = resource.Body('csr') + #: CA certificate for the bay/cluster. + pem = resource.Body('pem') diff --git a/openstack/container_infrastructure_management/v1/service.py b/openstack/container_infrastructure_management/v1/service.py new file mode 100644 index 000000000..277fa54b7 --- /dev/null +++ b/openstack/container_infrastructure_management/v1/service.py @@ -0,0 +1,38 @@ +# 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 resource + + +class Service(resource.Resource): + + resources_key = 'mservices' + base_path = '/mservices' + + # capabilities + allow_list = True + + #: The name of the binary form of the Magnum service. + binary = resource.Body('binary') + #: The date and time when the resource was created. + created_at = resource.Body('created_at') + #: The disable reason of the service, null if the service is enabled or + #: disabled without reason provided. + disabled_reason = resource.Body('disabled_reason') + #: The host for the service. + host = resource.Body('host') + #: The total number of report. + report_count = resource.Body('report_count') + #: The current state of Magnum services. + state = resource.Body('state') + #: The date and time when the resource was updated. + updated_at = resource.Body('updated_at') diff --git a/openstack/tests/unit/cloud/test_coe_clusters_certificate.py b/openstack/tests/unit/cloud/test_coe_clusters_certificate.py index 8e448d549..56c42a846 100644 --- a/openstack/tests/unit/cloud/test_coe_clusters_certificate.py +++ b/openstack/tests/unit/cloud/test_coe_clusters_certificate.py @@ -11,18 +11,19 @@ # under the License. -import munch - +from openstack.container_infrastructure_management.v1 import ( + cluster_certificate +) from openstack.tests.unit import base -coe_cluster_ca_obj = munch.Munch( +coe_cluster_ca_obj = dict( cluster_uuid="43e305ce-3a5f-412a-8a14-087834c34c8c", pem="-----BEGIN CERTIFICATE-----\nMIIDAO\n-----END CERTIFICATE-----\n", bay_uuid="43e305ce-3a5f-412a-8a14-087834c34c8c", links=[] ) -coe_cluster_signed_cert_obj = munch.Munch( +coe_cluster_signed_cert_obj = dict( cluster_uuid='43e305ce-3a5f-412a-8a14-087834c34c8c', pem='-----BEGIN CERTIFICATE-----\nMIIDAO\n-----END CERTIFICATE-----', bay_uuid='43e305ce-3a5f-412a-8a14-087834c34c8c', @@ -33,6 +34,12 @@ class TestCOEClusters(base.TestCase): + def _compare_cluster_certs(self, exp, real): + self.assertDictEqual( + cluster_certificate.ClusterCertificate( + **exp).to_dict(computed=False), + real.to_dict(computed=False), + ) def get_mock_url( self, @@ -47,12 +54,12 @@ def test_get_coe_cluster_certificate(self): method='GET', uri=self.get_mock_url( resource='certificates', - append=[coe_cluster_ca_obj.cluster_uuid]), + append=[coe_cluster_ca_obj['cluster_uuid']]), json=coe_cluster_ca_obj) ]) ca_cert = self.cloud.get_coe_cluster_certificate( - coe_cluster_ca_obj.cluster_uuid) - self.assertEqual( + coe_cluster_ca_obj['cluster_uuid']) + self._compare_cluster_certs( coe_cluster_ca_obj, ca_cert) self.assert_calls() @@ -61,10 +68,10 @@ def test_sign_coe_cluster_certificate(self): self.register_uris([dict( method='POST', uri=self.get_mock_url(resource='certificates'), - json={"cluster_uuid": coe_cluster_signed_cert_obj.cluster_uuid, - "csr": coe_cluster_signed_cert_obj.csr} + json={"cluster_uuid": coe_cluster_signed_cert_obj['cluster_uuid'], + "csr": coe_cluster_signed_cert_obj['csr']} )]) self.cloud.sign_coe_cluster_certificate( - coe_cluster_signed_cert_obj.cluster_uuid, - coe_cluster_signed_cert_obj.csr) + coe_cluster_signed_cert_obj['cluster_uuid'], + coe_cluster_signed_cert_obj['csr']) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_magnum_services.py b/openstack/tests/unit/cloud/test_magnum_services.py index f5dee60e5..94ff8da85 100644 --- a/openstack/tests/unit/cloud/test_magnum_services.py +++ b/openstack/tests/unit/cloud/test_magnum_services.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.container_infrastructure_management.v1 import service from openstack.tests.unit import base @@ -18,7 +19,6 @@ created_at='2015-08-27T09:49:58-05:00', disabled_reason=None, host='fake-host', - human_id=None, id=1, report_count=1, state='up', @@ -37,6 +37,7 @@ def test_list_magnum_services(self): json=dict(mservices=[magnum_service_obj]))]) mservices_list = self.cloud.list_magnum_services() self.assertEqual( - mservices_list[0], - self.cloud._normalize_magnum_service(magnum_service_obj)) + mservices_list[0].to_dict(computed=False), + service.Service(**magnum_service_obj).to_dict(computed=False), + ) self.assert_calls() diff --git a/openstack/tests/unit/container_infrastructure_management/v1/test_cluster_certificate.py b/openstack/tests/unit/container_infrastructure_management/v1/test_cluster_certificate.py new file mode 100644 index 000000000..390bdd489 --- /dev/null +++ b/openstack/tests/unit/container_infrastructure_management/v1/test_cluster_certificate.py @@ -0,0 +1,43 @@ +# 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.container_infrastructure_management.v1 import ( + cluster_certificate +) +from openstack.tests.unit import base + +EXAMPLE = { + "cluster_uuid": "0b4b766f-1500-44b3-9804-5a6e12fe6df4", + "pem": "-----BEGIN CERTIFICATE-----\nMIICzDCCAbSgAwIBAgIQOOkVcEN7TNa9E80G", + "bay_uuid": "0b4b766f-1500-44b3-9804-5a6e12fe6df4", + "csr": "-----BEGIN CERTIFICATE REQUEST-----\nMIIEfzCCAmcCAQAwFDESMBAGA1UE" +} + + +class TestClusterCertificate(base.TestCase): + def test_basic(self): + sot = cluster_certificate.ClusterCertificate() + self.assertIsNone(sot.resource_key) + self.assertEqual('/certificates', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_delete) + self.assertFalse(sot.allow_list) + + def test_make_it(self): + sot = cluster_certificate.ClusterCertificate(**EXAMPLE) + + self.assertEqual(EXAMPLE['cluster_uuid'], sot.cluster_uuid) + self.assertEqual(EXAMPLE['bay_uuid'], sot.bay_uuid) + self.assertEqual(EXAMPLE['csr'], sot.csr) + self.assertEqual(EXAMPLE['pem'], sot.pem) diff --git a/openstack/tests/unit/container_infrastructure_management/v1/test_proxy.py b/openstack/tests/unit/container_infrastructure_management/v1/test_proxy.py index 337bdee72..e352dd083 100644 --- a/openstack/tests/unit/container_infrastructure_management/v1/test_proxy.py +++ b/openstack/tests/unit/container_infrastructure_management/v1/test_proxy.py @@ -10,9 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.container_infrastructure_management.v1 import ( + cluster_certificate +) from openstack.container_infrastructure_management.v1 import _proxy from openstack.container_infrastructure_management.v1 import cluster from openstack.container_infrastructure_management.v1 import cluster_template +from openstack.container_infrastructure_management.v1 import service from openstack.tests.unit import test_proxy_base @@ -52,6 +56,20 @@ def test_cluster_delete_ignore(self): self.verify_delete(self.proxy.delete_cluster, cluster.Cluster, True) +class TestClusterCertificate(TestMagnumProxy): + def test_cluster_certificate_get(self): + self.verify_get( + self.proxy.get_cluster_certificate, + cluster_certificate.ClusterCertificate + ) + + def test_cluster_certificate_create_attrs(self): + self.verify_create( + self.proxy.create_cluster_certificate, + cluster_certificate.ClusterCertificate, + ) + + class TestClusterTemplate(TestMagnumProxy): def test_cluster_template_get(self): self.verify_get( @@ -93,3 +111,14 @@ def test_cluster_template_delete_ignore(self): cluster_template.ClusterTemplate, True, ) + + +class TestService(TestMagnumProxy): + + def test_services(self): + self.verify_list( + self.proxy.services, + service.Service, + method_kwargs={}, + expected_kwargs={}, + ) diff --git a/openstack/tests/unit/container_infrastructure_management/v1/test_service.py b/openstack/tests/unit/container_infrastructure_management/v1/test_service.py new file mode 100644 index 000000000..908c72c9b --- /dev/null +++ b/openstack/tests/unit/container_infrastructure_management/v1/test_service.py @@ -0,0 +1,49 @@ +# 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.container_infrastructure_management.v1 import service +from openstack.tests.unit import base + +EXAMPLE = { + "binary": "magnum-conductor", + "created_at": "2016-08-23T10:52:13+00:00", + "state": "up", + "report_count": 2179, + "updated_at": "2016-08-25T01:13:16+00:00", + "host": "magnum-manager", + "disabled_reason": None, + "id": 1 +} + + +class TestService(base.TestCase): + def test_basic(self): + sot = service.Service() + self.assertIsNone(sot.resource_key) + self.assertEqual('mservices', sot.resources_key) + self.assertEqual('/mservices', sot.base_path) + self.assertFalse(sot.allow_create) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = service.Service(**EXAMPLE) + + self.assertEqual(EXAMPLE['binary'], sot.binary) + self.assertEqual(EXAMPLE['created_at'], sot.created_at) + self.assertEqual(EXAMPLE['disabled_reason'], sot.disabled_reason) + self.assertEqual(EXAMPLE['host'], sot.host) + self.assertEqual(EXAMPLE['report_count'], sot.report_count) + self.assertEqual(EXAMPLE['state'], sot.state) + self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) diff --git a/releasenotes/notes/switch-coe-to-proxy-c18789ed27cc1d95.yaml b/releasenotes/notes/switch-coe-to-proxy-c18789ed27cc1d95.yaml new file mode 100644 index 000000000..ad4e0e3e0 --- /dev/null +++ b/releasenotes/notes/switch-coe-to-proxy-c18789ed27cc1d95.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Convert container_infrastructure_management cloud operations to rely fully + on service proxy with all resource classes created. From 71dcfba0cadcc371b95b3fb2b97743018b14fce4 Mon Sep 17 00:00:00 2001 From: elajkat Date: Fri, 20 Jan 2023 15:52:28 +0100 Subject: [PATCH 3191/3836] Add Tap Services and Flows to SDK Change-Id: I631379e4711148a5a470a91b069d8b58019c0eef Related-Bug: #1999774 --- doc/source/user/proxies/network.rst | 9 ++ doc/source/user/resources/network/index.rst | 2 + .../user/resources/network/v2/tap_flow.rst | 12 ++ .../user/resources/network/v2/tap_service.rst | 12 ++ openstack/network/v2/_proxy.py | 56 +++++++++ openstack/network/v2/tap_flow.py | 55 ++++++++ openstack/network/v2/tap_service.py | 51 ++++++++ .../tests/functional/network/v2/test_taas.py | 119 ++++++++++++++++++ .../tests/unit/network/v2/test_tap_flow.py | 56 +++++++++ .../tests/unit/network/v2/test_tap_service.py | 54 ++++++++ ...k_add_taas_resources-86a947265e11ce84.yaml | 5 + 11 files changed, 431 insertions(+) create mode 100644 doc/source/user/resources/network/v2/tap_flow.rst create mode 100644 doc/source/user/resources/network/v2/tap_service.rst create mode 100644 openstack/network/v2/tap_flow.py create mode 100644 openstack/network/v2/tap_service.py create mode 100644 openstack/tests/functional/network/v2/test_taas.py create mode 100644 openstack/tests/unit/network/v2/test_tap_flow.py create mode 100644 openstack/tests/unit/network/v2/test_tap_service.py create mode 100644 releasenotes/notes/network_add_taas_resources-86a947265e11ce84.yaml diff --git a/doc/source/user/proxies/network.rst b/doc/source/user/proxies/network.rst index 29d30ce37..748d78d98 100644 --- a/doc/source/user/proxies/network.rst +++ b/doc/source/user/proxies/network.rst @@ -305,3 +305,12 @@ BGP Operations get_bgp_dragents_hosting_speaker, add_bgp_speaker_to_dragent, get_bgp_speakers_hosted_by_dragent, remove_bgp_speaker_from_dragent + +Tap As A Service Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + :noindex: + :members: create_tap_flow, delete_tap_flow, find_tap_flow, get_tap_flow, + update_tap_flow, tap_flows, create_tap_service, delete_tap_service, + find_tap_service, update_tap_service, tap_services diff --git a/doc/source/user/resources/network/index.rst b/doc/source/user/resources/network/index.rst index ce8a6c21e..aa13d628a 100644 --- a/doc/source/user/resources/network/index.rst +++ b/doc/source/user/resources/network/index.rst @@ -44,4 +44,6 @@ Network Resources v2/service_provider v2/subnet v2/subnet_pool + v2/tap_flow + v2/tap_service v2/vpn/index diff --git a/doc/source/user/resources/network/v2/tap_flow.rst b/doc/source/user/resources/network/v2/tap_flow.rst new file mode 100644 index 000000000..21cbce3aa --- /dev/null +++ b/doc/source/user/resources/network/v2/tap_flow.rst @@ -0,0 +1,12 @@ +openstack.network.v2.tap_flow +============================= + +.. automodule:: openstack.network.v2.tap_flow + +The TapFlow Class +----------------- + +The ``TapFlow`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.tap_flow.TapFlow + :members: diff --git a/doc/source/user/resources/network/v2/tap_service.rst b/doc/source/user/resources/network/v2/tap_service.rst new file mode 100644 index 000000000..74b51c802 --- /dev/null +++ b/doc/source/user/resources/network/v2/tap_service.rst @@ -0,0 +1,12 @@ +openstack.network.v2.tap_service +================================ + +.. automodule:: openstack.network.v2.tap_service + +The TapService Class +-------------------- + +The ``TapService`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.tap_service.TapService + :members: diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index c343f75b4..51272e72c 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -66,6 +66,8 @@ from openstack.network.v2 import service_provider as _service_provider from openstack.network.v2 import subnet as _subnet from openstack.network.v2 import subnet_pool as _subnet_pool +from openstack.network.v2 import tap_flow as _tap_flow +from openstack.network.v2 import tap_service as _tap_service from openstack.network.v2 import trunk as _trunk from openstack.network.v2 import vpn_endpoint_group as _vpn_endpoint_group from openstack.network.v2 import vpn_ike_policy as _ike_policy @@ -130,6 +132,8 @@ class Proxy(proxy.Proxy, Generic[T]): "service_provider": _service_provider.ServiceProvider, "subnet": _subnet.Subnet, "subnet_pool": _subnet_pool.SubnetPool, + "tap_flow": _tap_flow.TapFlow, + "tap_service": _tap_service.TapService, "trunk": _trunk.Trunk, "vpn_endpoint_group": _vpn_endpoint_group.VpnEndpointGroup, "vpn_ike_policy": _ike_policy.VpnIkePolicy, @@ -5242,6 +5246,58 @@ def delete_conntrack_helper(self, conntrack_helper, router, conntrack_helper, router_id=router.id, ignore_missing=ignore_missing) + def create_tap_flow(self, **attrs): + """Create a new Tap Flow from attributes""" + return self._create(_tap_flow.TapFlow, **attrs) + + def delete_tap_flow(self, tap_flow, ignore_missing=True): + """Delete a Tap Flow""" + self._delete(_tap_flow.TapFlow, tap_flow, + ignore_missing=ignore_missing) + + def find_tap_flow(self, name_or_id, ignore_missing=True, **query): + """"Find a single Tap Service""" + return self._find(_tap_flow.TapFlow, name_or_id, + ignore_missing=ignore_missing, **query) + + def get_tap_flow(self, tap_flow): + """Get a signle Tap Flow""" + return self._get(_tap_flow.TapFlow, tap_flow) + + def update_tap_flow(self, tap_flow, **attrs): + """Update a Tap Flow""" + return self._update(_tap_flow.TapFlow, tap_flow, **attrs) + + def tap_flows(self, **query): + """Return a generator of Tap Flows""" + return self._list(_tap_flow.TapFlow, **query) + + def create_tap_service(self, **attrs): + """Create a new Tap Service from attributes""" + return self._create(_tap_service.TapService, **attrs) + + def delete_tap_service(self, tap_service, ignore_missing=True): + """Delete a Tap Service""" + self._delete(_tap_service.TapService, tap_service, + ignore_missing=ignore_missing) + + def find_tap_service(self, name_or_id, ignore_missing=True, **query): + """"Find a single Tap Service""" + return self._find(_tap_service.TapService, name_or_id, + ignore_missing=ignore_missing, **query) + + def get_tap_service(self, tap_service): + """Get a signle Tap Service""" + return self._get(_tap_service.TapService, tap_service) + + def update_tap_service(self, tap_service, **attrs): + """Update a Tap Service""" + return self._update(_tap_service.TapService, tap_service, **attrs) + + def tap_services(self, **query): + """Return a generator of Tap Services""" + return self._list(_tap_service.TapService, **query) + def _get_cleanup_dependencies(self): return { 'network': { diff --git a/openstack/network/v2/tap_flow.py b/openstack/network/v2/tap_flow.py new file mode 100644 index 000000000..9707170db --- /dev/null +++ b/openstack/network/v2/tap_flow.py @@ -0,0 +1,55 @@ +# 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 resource + + +class TapFlow(resource.Resource): + """Tap Flow""" + + resource_key = 'tap_flow' + resources_key = 'tap_flows' + base_path = '/taas/tap_flows' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + _allow_unknown_attrs_in_body = True + + _query_mapping = resource.QueryParameters( + "sort_key", "sort_dir", + 'name', 'project_id' + ) + + # Properties + #: The ID of the tap flow. + id = resource.Body('id') + #: The tap flow's name. + name = resource.Body('name') + #: The tap flow's description. + description = resource.Body('description') + #: The ID of the project that owns the tap flow. + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) + #: The id of the tap_service with which the tap flow is associated + tap_service_id = resource.Body('tap_service_id') + #: The direction of the tap flow. + direction = resource.Body('direction') + #: The status for the tap flow. + status = resource.Body('status') + #: The id of the port the tap flow is associated with + source_port = resource.Body('source_port') diff --git a/openstack/network/v2/tap_service.py b/openstack/network/v2/tap_service.py new file mode 100644 index 000000000..527b1e0a6 --- /dev/null +++ b/openstack/network/v2/tap_service.py @@ -0,0 +1,51 @@ +# 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 resource + + +class TapService(resource.Resource): + """Tap Service""" + + resource_key = 'tap_service' + resources_key = 'tap_services' + base_path = '/taas/tap_services' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + _allow_unknown_attrs_in_body = True + + _query_mapping = resource.QueryParameters( + "sort_key", "sort_dir", + 'name', 'project_id' + ) + + # Properties + #: The ID of the tap service. + id = resource.Body('id') + #: The tap service name. + name = resource.Body('name') + #: The tap service description. + description = resource.Body('description') + #: The ID of the project that owns the tap service. + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) + #: The id of the port the tap service is associated with + port_id = resource.Body('port_id') + #: The status for the tap service. + status = resource.Body('status') diff --git a/openstack/tests/functional/network/v2/test_taas.py b/openstack/tests/functional/network/v2/test_taas.py new file mode 100644 index 000000000..20eacafa5 --- /dev/null +++ b/openstack/tests/functional/network/v2/test_taas.py @@ -0,0 +1,119 @@ +# 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.network.v2 import network as _network +from openstack.network.v2 import port as _port +from openstack.network.v2 import tap_flow as _tap_flow +from openstack.network.v2 import tap_service as _tap_service +from openstack.tests.functional import base + + +class TestTapService(base.BaseFunctionalTest): + + def setUp(self): + super().setUp() + if not self.user_cloud.network.find_extension("taas"): + self.skipTest("Neutron Tap-as-a-service Extension disabled") + + self.TAP_S_NAME = 'my_service' + self.getUniqueString() + self.TAP_F_NAME = 'my_flow' + self.getUniqueString() + net = self.user_cloud.network.create_network() + assert isinstance(net, _network.Network) + self.SERVICE_NET_ID = net.id + + net = self.user_cloud.network.create_network() + assert isinstance(net, _network.Network) + self.FLOW_NET_ID = net.id + + port = self.user_cloud.network.create_port( + network_id=self.SERVICE_NET_ID) + assert isinstance(port, _port.Port) + self.SERVICE_PORT_ID = port.id + + port = self.user_cloud.network.create_port( + network_id=self.FLOW_NET_ID) + assert isinstance(port, _port.Port) + self.FLOW_PORT_ID = port.id + + tap_service = self.user_cloud.network.create_tap_service( + name=self.TAP_S_NAME, port_id=self.SERVICE_PORT_ID) + assert isinstance(tap_service, _tap_service.TapService) + self.TAP_SERVICE = tap_service + + tap_flow = self.user_cloud.network.create_tap_flow( + name=self.TAP_F_NAME, tap_service_id=self.TAP_SERVICE.id, + source_port=self.FLOW_PORT_ID, direction='BOTH') + assert isinstance(tap_flow, _tap_flow.TapFlow) + self.TAP_FLOW = tap_flow + + def tearDown(self): + sot = self.user_cloud.network.delete_tap_flow(self.TAP_FLOW.id, + ignore_missing=False) + self.assertIsNone(sot) + sot = self.user_cloud.network.delete_tap_service(self.TAP_SERVICE.id, + ignore_missing=False) + self.assertIsNone(sot) + sot = self.user_cloud.network.delete_port(self.SERVICE_PORT_ID) + self.assertIsNone(sot) + sot = self.user_cloud.network.delete_port(self.FLOW_PORT_ID) + self.assertIsNone(sot) + sot = self.user_cloud.network.delete_network(self.SERVICE_NET_ID) + self.assertIsNone(sot) + sot = self.user_cloud.network.delete_network(self.FLOW_NET_ID) + self.assertIsNone(sot) + super().tearDown() + + def test_find_tap_service(self): + sot = self.user_cloud.network.find_tap_service(self.TAP_SERVICE.name) + self.assertEqual(self.SERVICE_PORT_ID, sot.port_id) + self.assertEqual(self.TAP_S_NAME, sot.name) + + def test_get_tap_service(self): + sot = self.user_cloud.network.get_tap_service(self.TAP_SERVICE.id) + self.assertEqual(self.SERVICE_PORT_ID, sot.port_id) + self.assertEqual(self.TAP_S_NAME, sot.name) + + def test_list_tap_services(self): + tap_service_ids = [ts.id for ts in + self.user_cloud.network.tap_services()] + self.assertIn(self.TAP_SERVICE.id, tap_service_ids) + + def test_update_tap_service(self): + description = 'My tap service' + sot = self.user_cloud.network.update_tap_service( + self.TAP_SERVICE.id, description=description) + self.assertEqual(description, sot.description) + + def test_find_tap_flow(self): + sot = self.user_cloud.network.find_tap_flow(self.TAP_FLOW.name) + self.assertEqual(self.FLOW_PORT_ID, sot.source_port) + self.assertEqual(self.TAP_SERVICE.id, sot.tap_service_id) + self.assertEqual('BOTH', sot.direction) + self.assertEqual(self.TAP_F_NAME, sot.name) + + def test_get_tap_flow(self): + sot = self.user_cloud.network.get_tap_flow(self.TAP_FLOW.id) + self.assertEqual(self.FLOW_PORT_ID, sot.source_port) + self.assertEqual(self.TAP_F_NAME, sot.name) + self.assertEqual(self.TAP_SERVICE.id, sot.tap_service_id) + self.assertEqual('BOTH', sot.direction) + + def test_list_tap_flows(self): + tap_flow_ids = [tf.id for tf in + self.user_cloud.network.tap_flows()] + self.assertIn(self.TAP_FLOW.id, tap_flow_ids) + + def test_update_tap_flow(self): + description = 'My tap flow' + sot = self.user_cloud.network.update_tap_flow( + self.TAP_FLOW.id, description=description) + self.assertEqual(description, sot.description) diff --git a/openstack/tests/unit/network/v2/test_tap_flow.py b/openstack/tests/unit/network/v2/test_tap_flow.py new file mode 100644 index 000000000..fdeaeaffb --- /dev/null +++ b/openstack/tests/unit/network/v2/test_tap_flow.py @@ -0,0 +1,56 @@ +# 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.network.v2 import tap_flow +from openstack.tests.unit import base + + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'name': 'my_tap_flow', + 'source_port': '1234', + 'tap_service_id': '4321', + 'id': IDENTIFIER, + 'project_id': '42' +} + + +class TestTapFlow(base.TestCase): + + def test_basic(self): + sot = tap_flow.TapFlow() + self.assertEqual('tap_flow', sot.resource_key) + self.assertEqual('tap_flows', sot.resources_key) + self.assertEqual('/taas/tap_flows', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = tap_flow.TapFlow(**EXAMPLE) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['source_port'], sot.source_port) + self.assertEqual(EXAMPLE['tap_service_id'], sot.tap_service_id) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + + self.assertDictEqual( + {'limit': 'limit', + 'marker': 'marker', + 'name': 'name', + 'project_id': 'project_id', + 'sort_key': 'sort_key', + 'sort_dir': 'sort_dir', + }, + sot._query_mapping._mapping) diff --git a/openstack/tests/unit/network/v2/test_tap_service.py b/openstack/tests/unit/network/v2/test_tap_service.py new file mode 100644 index 000000000..1892ffa50 --- /dev/null +++ b/openstack/tests/unit/network/v2/test_tap_service.py @@ -0,0 +1,54 @@ +# 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.network.v2 import tap_service +from openstack.tests.unit import base + + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'name': 'my_tap_service', + 'port_id': '1234', + 'id': IDENTIFIER, + 'project_id': '42' +} + + +class TestTapService(base.TestCase): + + def test_basic(self): + sot = tap_service.TapService() + self.assertEqual('tap_service', sot.resource_key) + self.assertEqual('tap_services', sot.resources_key) + self.assertEqual('/taas/tap_services', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = tap_service.TapService(**EXAMPLE) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['port_id'], sot.port_id) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + + self.assertDictEqual( + {'limit': 'limit', + 'marker': 'marker', + 'name': 'name', + 'project_id': 'project_id', + 'sort_key': 'sort_key', + 'sort_dir': 'sort_dir', + }, + sot._query_mapping._mapping) diff --git a/releasenotes/notes/network_add_taas_resources-86a947265e11ce84.yaml b/releasenotes/notes/network_add_taas_resources-86a947265e11ce84.yaml new file mode 100644 index 000000000..54fb3730b --- /dev/null +++ b/releasenotes/notes/network_add_taas_resources-86a947265e11ce84.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``Tap Service`` and ``Tap Flow`` resources, and introduce support for + CRUD operations for these. From 6e5f34dba55365805033694b7a01ea39e0072c99 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 18 Nov 2022 17:42:19 +0100 Subject: [PATCH 3192/3836] Drop munch dependency Importing munch inside of SDK is taking around 0.3 second. Itself it is not a big problem, but it hurts on the openstackclient front. In addition to that munch project does not seem to be actively maintained and had no releases since 2 years. Dropping this dependency at once is requiring quite a big rework so instead copy a heavily stripped version of what we really require from it. This helps us to gain performance improvement while giving time to rework our code to come up with a decicion on how to deal with it. Change-Id: I6612278ae798d48b296239e3359026584efb8a70 --- openstack/cloud/_baremetal.py | 3 +- openstack/cloud/_coe.py | 1 - openstack/cloud/_compute.py | 12 +- openstack/cloud/_floating_ip.py | 24 +-- openstack/cloud/_orchestration.py | 3 +- openstack/cloud/_security_group.py | 29 +-- openstack/cloud/meta.py | 10 +- openstack/cloud/openstackcloud.py | 13 +- openstack/proxy.py | 2 +- openstack/resource.py | 15 +- openstack/tests/base.py | 7 +- .../tests/unit/cloud/test_create_server.py | 15 +- .../unit/cloud/test_floating_ip_neutron.py | 5 +- openstack/tests/unit/test_proxy.py | 4 +- openstack/tests/unit/test_resource.py | 10 +- openstack/utils.py | 195 +++++++++++++++++- requirements.txt | 1 - 17 files changed, 271 insertions(+), 78 deletions(-) diff --git a/openstack/cloud/_baremetal.py b/openstack/cloud/_baremetal.py index 3226ff519..35895c35f 100644 --- a/openstack/cloud/_baremetal.py +++ b/openstack/cloud/_baremetal.py @@ -436,7 +436,8 @@ def list_ports_attached_to_machine(self, name_or_id): """List virtual ports attached to the bare metal machine. :param string name_or_id: A machine name or UUID. - :returns: List of ``munch.Munch`` representing the ports. + :returns: List of ``openstack.Resource`` objects representing + the ports. """ machine = self.get_machine(name_or_id) vif_ids = self.baremetal.list_node_vifs(machine) diff --git a/openstack/cloud/_coe.py b/openstack/cloud/_coe.py index 6a5996f94..6533f3f36 100644 --- a/openstack/cloud/_coe.py +++ b/openstack/cloud/_coe.py @@ -13,7 +13,6 @@ # import types so that we can reference ListType in sphinx param declarations. # We can't just use list, because sphinx gets confused by # openstack.resource.Resource.list and openstack.resource2.Resource.list -import types # noqa from openstack.cloud import _utils from openstack.cloud import exc diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 179153561..62d5b46f5 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -1571,7 +1571,6 @@ def list_aggregates(self, filters={}): """ return self.compute.aggregates(**filters) - # TODO(stephenfin): This shouldn't return a munch def get_aggregate(self, name_or_id, filters=None): """Get an aggregate by name or ID. @@ -1590,10 +1589,8 @@ def get_aggregate(self, name_or_id, filters=None): :returns: An aggregate dict or None if no matching aggregate is found. """ - aggregate = self.compute.find_aggregate( + return self.compute.find_aggregate( name_or_id, ignore_missing=True) - if aggregate: - return aggregate._to_munch() def create_aggregate(self, name, availability_zone=None): """Create a new host aggregate. @@ -1804,11 +1801,10 @@ def _remove_novaclient_artifacts(self, item): item.pop('x_openstack_request_ids', None) def _normalize_server(self, server): - import munch - ret = munch.Munch() + ret = utils.Munch() # Copy incoming server because of shared dicts in unittests # Wrap the copy in munch so that sub-dicts are properly munched - server = munch.Munch(server) + server = utils.Munch(server) self._remove_novaclient_artifacts(server) @@ -1824,7 +1820,7 @@ def _normalize_server(self, server): # from volume image = server.pop('image', None) if str(image) != image: - image = munch.Munch(id=image['id']) + image = utils.Munch(id=image['id']) ret['image'] = image # From original_names from sdk diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index b8ec688a8..ee6ac0c3d 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -108,8 +108,8 @@ def get_floating_ip(self, id, filters=None): A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - :returns: A floating IP ``munch.Munch`` or None if no matching floating - IP is found. + :returns: A floating IP ``openstack.network.v2.floating_ip.FloatingIP`` + or None if no matching floating IP is found. """ return _utils._get_entity(self, 'floating_ip', id, filters) @@ -168,7 +168,8 @@ def list_floating_ip_pools(self): neutron. `get_external_ipv4_floating_networks` is what you should almost certainly be using. - :returns: A list of floating IP pool ``munch.Munch``. + :returns: A list of floating IP pool + ``openstack.network.v2.floating_ip.FloatingIP``. """ if not self._has_nova_extension('os-floating-ip-pools'): @@ -185,7 +186,8 @@ def list_floating_ips(self, filters=None): """List all available floating IPs. :param filters: (optional) dict of filter conditions to push down - :returns: A list of floating IP ``munch.Munch``. + :returns: A list of floating IP + ``openstack.network.v2.floating_ip.FloatingIP``. """ # If pushdown filters are specified and we do not have batched caching @@ -219,8 +221,7 @@ def get_floating_ip_by_id(self, id): :param id: ID of the floating ip. :returns: A floating ip - `:class:`~openstack.network.v2.floating_ip.FloatingIP` or - ``munch.Munch``. + `:class:`~openstack.network.v2.floating_ip.FloatingIP`. """ error_message = "Error getting floating ip with ID {id}".format(id=id) @@ -670,7 +671,7 @@ def _attach_ip_to_server( :param nat_destination: The fixed network the server's port for the FIP to attach to will come from. - :returns: The server ``munch.Munch`` + :returns: The server ``openstack.compute.v2.server.Server`` :raises: OpenStackCloudException, on operation error. """ @@ -842,7 +843,7 @@ def _add_ip_from_pool( :param nat_destination: (optional) the name of the network of the port to associate with the floating ip. - :returns: the updated server ``munch.Munch`` + :returns: the updated server ``openstack.compute.v2.server.Server`` """ if reuse: f_ip = self.available_floating_ip(network=network) @@ -885,7 +886,7 @@ def add_ip_list( the fixed IP to attach the floating IP should be on - :returns: The updated server ``munch.Munch`` + :returns: The updated server ``openstack.compute.v2.server.Server`` :raises: ``OpenStackCloudException``, on operation error. """ @@ -1216,14 +1217,13 @@ def _normalize_floating_ips(self, ips): def _normalize_floating_ip(self, ip): # Copy incoming floating ip because of shared dicts in unittests # Only import munch when we really need it - import munch location = self._get_current_location( project_id=ip.get('owner')) # This copy is to keep things from getting epically weird in tests ip = ip.copy() - ret = munch.Munch(location=location) + ret = utils.Munch(location=location) fixed_ip_address = ip.pop('fixed_ip_address', ip.pop('fixed_ip', None)) floating_ip_address = ip.pop('floating_ip_address', ip.pop('ip', None)) @@ -1252,7 +1252,7 @@ def _normalize_floating_ip(self, ip): # In neutron's terms, Nova floating IPs are always ACTIVE status = 'ACTIVE' - ret = munch.Munch( + ret = utils.Munch( attached=attached, fixed_ip_address=fixed_ip_address, floating_ip_address=floating_ip_address, diff --git a/openstack/cloud/_orchestration.py b/openstack/cloud/_orchestration.py index 984c9a3cd..0aaa27cdd 100644 --- a/openstack/cloud/_orchestration.py +++ b/openstack/cloud/_orchestration.py @@ -201,7 +201,8 @@ def search_stacks(self, name_or_id=None, filters=None): :param filters: a dict containing additional filters to use. e.g. {'stack_status': 'CREATE_COMPLETE'} - :returns: a list of ``munch.Munch`` containing the stack description. + :returns: a list of ``openstack.orchestration.v1.stack.Stack`` + containing the stack description. :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. diff --git a/openstack/cloud/_security_group.py b/openstack/cloud/_security_group.py index 258335dcd..b0bf33185 100644 --- a/openstack/cloud/_security_group.py +++ b/openstack/cloud/_security_group.py @@ -21,6 +21,7 @@ from openstack import exceptions from openstack.network.v2._proxy import Proxy from openstack import proxy +from openstack import utils class SecurityGroupCloudMixin: @@ -40,7 +41,8 @@ def list_security_groups(self, filters=None): """List all available security groups. :param filters: (optional) dict of filter conditions to push down - :returns: A list of security group ``munch.Munch``. + :returns: A list of security group + ``openstack.network.v2.security_group.SecurityGroup``. """ # Security groups not supported @@ -86,8 +88,9 @@ def get_security_group(self, name_or_id, filters=None): A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - :returns: A security group ``munch.Munch`` or None if no matching - security group is found. + :returns: A security group + ``openstack.network.v2.security_group.SecurityGroup`` + or None if no matching security group is found. """ return _utils._get_entity( @@ -97,7 +100,8 @@ def get_security_group_by_id(self, id): """ Get a security group by ID :param id: ID of the security group. - :returns: A security group ``munch.Munch``. + :returns: A security group + ``openstack.network.v2.security_group.SecurityGroup``. """ if not self._has_secgroups(): raise exc.OpenStackCloudUnavailableFeature( @@ -126,7 +130,8 @@ def create_security_group(self, name, description, on (admin-only). :param string stateful: Whether the security group is stateful or not. - :returns: A ``munch.Munch`` representing the new security group. + :returns: A ``openstack.network.v2.security_group.SecurityGroup`` + representing the new security group. :raises: OpenStackCloudException on operation error. :raises: OpenStackCloudUnavailableFeature if security groups are @@ -200,7 +205,8 @@ def update_security_group(self, name_or_id, **kwargs): :param string name: New name for the security group. :param string description: New description for the security group. - :returns: A ``munch.Munch`` describing the updated security group. + :returns: A ``openstack.network.v2.security_group.SecurityGroup`` + describing the updated security group. :raises: OpenStackCloudException on operation error. """ @@ -288,7 +294,8 @@ def create_security_group_rule(self, on (admin-only). :param string description: Description of the rule, max 255 characters. - :returns: A ``munch.Munch`` representing the new security group rule. + :returns: A ``openstack.network.v2.security_group.SecurityGroup`` + representing the new security group rule. :raises: OpenStackCloudException on operation error. """ @@ -439,9 +446,7 @@ def _normalize_secgroups(self, groups): # secgroups def _normalize_secgroup(self, group): - import munch - - ret = munch.Munch() + ret = utils.Munch() # Copy incoming group because of shared dicts in unittests group = group.copy() @@ -493,9 +498,7 @@ def _normalize_secgroup_rules(self, rules): # secgroups def _normalize_secgroup_rule(self, rule): - import munch - - ret = munch.Munch() + ret = utils.Munch() # Copy incoming rule because of shared dicts in unittests rule = rule.copy() diff --git a/openstack/cloud/meta.py b/openstack/cloud/meta.py index 19a9b3bca..5622aeaf0 100644 --- a/openstack/cloud/meta.py +++ b/openstack/cloud/meta.py @@ -15,8 +15,6 @@ import ipaddress import socket -import munch - from openstack import _log from openstack.cloud import exc from openstack import utils @@ -551,7 +549,7 @@ def obj_to_munch(obj): """ if obj is None: return None - elif isinstance(obj, munch.Munch) or hasattr(obj, 'mock_add_spec'): + elif isinstance(obj, utils.Munch) or hasattr(obj, 'mock_add_spec'): # If we obj_to_munch twice, don't fail, just return the munch # Also, don't try to modify Mock objects - that way lies madness return obj @@ -563,14 +561,14 @@ def obj_to_munch(obj): # the dict we get, but we also want it to fall through to object # attribute processing so that we can also get the request_ids # data into our resulting object. - instance = munch.Munch(obj) + instance = utils.Munch(obj) else: - instance = munch.Munch() + instance = utils.Munch() for key in dir(obj): try: value = getattr(obj, key) - # some attributes can be defined as a @propierty, so we can't assure + # some attributes can be defined as a @property, so we can't assure # to have a valid value # e.g. id in python-novaclient/tree/novaclient/v2/quotas.py except AttributeError: diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 9e715cb9d..9b96e58db 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -21,7 +21,6 @@ import dogpile.cache import keystoneauth1.exceptions import keystoneauth1.session -import munch import requests.models import requestsexceptions @@ -551,11 +550,11 @@ def current_project_id(self): @property def current_project(self): - """Return a ``munch.Munch`` describing the current project""" + """Return a ``utils.Munch`` describing the current project""" return self._get_project_info() def _get_project_info(self, project_id=None): - project_info = munch.Munch( + project_info = utils.Munch( id=project_id, name=None, domain_id=None, @@ -581,11 +580,11 @@ def _get_project_info(self, project_id=None): @property def current_location(self): - """Return a ``munch.Munch`` explaining the current cloud location.""" + """Return a ``utils.Munch`` explaining the current cloud location.""" return self._get_current_location() def _get_current_location(self, project_id=None, zone=None): - return munch.Munch( + return utils.Munch( cloud=self.name, # TODO(efried): This is wrong, but it only seems to be used in a # repr; can we get rid of it? @@ -596,11 +595,11 @@ def _get_current_location(self, project_id=None, zone=None): def _get_identity_location(self): '''Identity resources do not exist inside of projects.''' - return munch.Munch( + return utils.Munch( cloud=self.name, region_name=None, zone=None, - project=munch.Munch( + project=utils.Munch( id=None, name=None, domain_id=None, diff --git a/openstack/proxy.py b/openstack/proxy.py index 6a2774e41..5519b9d9a 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -445,7 +445,7 @@ def _get_resource(self, resource_type: Type[T], value, be a subclass of :class:`~openstack.resource.Resource` with a ``from_id`` method. :param value: The ID of a resource or an object of ``resource_type`` - class if using an existing instance, or ``munch.Munch``, + class if using an existing instance, or ``utils.Munch``, or None to create a new instance. :param attrs: A dict containing arguments for forming the request URL, if needed. diff --git a/openstack/resource.py b/openstack/resource.py index 1a68fab39..3f02faddf 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -42,7 +42,6 @@ class that represent a remote resource. The attributes that import jsonpatch from keystoneauth1 import adapter from keystoneauth1 import discover -import munch from requests import structures from openstack import _log @@ -999,12 +998,12 @@ def existing(cls, connection=None, **kwargs): @classmethod def _from_munch(cls, obj, synchronized=True, connection=None): - """Create an instance from a ``munch.Munch`` object. + """Create an instance from a ``utils.Munch`` object. This is intended as a temporary measure to convert between shade-style Munch objects and original openstacksdk resources. - :param obj: a ``munch.Munch`` object to convert from. + :param obj: a ``utils.Munch`` object to convert from. :param bool synchronized: whether this object already exists on server Must be set to ``False`` for newly created objects. """ @@ -1023,7 +1022,7 @@ def _attr_to_dict(self, attr, to_munch): if isinstance(value, Resource): return value.to_dict(_to_munch=to_munch) elif isinstance(value, dict) and to_munch: - return munch.Munch(value) + return utils.Munch(value) elif value and isinstance(value, list): converted = [] for raw in value: @@ -1032,7 +1031,7 @@ def _attr_to_dict(self, attr, to_munch): raw.to_dict(_to_munch=to_munch) ) elif isinstance(raw, dict) and to_munch: - converted.append(munch.Munch(raw)) + converted.append(utils.Munch(raw)) else: converted.append(raw) return converted @@ -1060,14 +1059,14 @@ def to_dict( hasn't returned. :param bool original_names: When True, use attribute names as they were received from the server. - :param bool _to_munch: For internal use only. Converts to `munch.Munch` + :param bool _to_munch: For internal use only. Converts to `utils.Munch` instead of dict. :return: A dictionary of key/value pairs where keys are named as they exist as attributes of this class. """ if _to_munch: - mapping = munch.Munch() + mapping = utils.Munch() else: mapping = {} @@ -1118,7 +1117,7 @@ def to_dict( return mapping - # Compatibility with the munch.Munch.toDict method + # Compatibility with the utils.Munch.toDict method toDict = to_dict # Make the munch copy method use to_dict copy = to_dict diff --git a/openstack/tests/base.py b/openstack/tests/base.py index 2572f3b91..e74851dfd 100644 --- a/openstack/tests/base.py +++ b/openstack/tests/base.py @@ -20,10 +20,11 @@ import sys import fixtures -import munch from oslotest import base import testtools.content +from openstack import utils + _TRUE_VALUES = ('true', '1', 'yes') @@ -84,9 +85,9 @@ def _fake_logs(self): def assertEqual(self, first, second, *args, **kwargs): '''Munch aware wrapper''' - if isinstance(first, munch.Munch): + if isinstance(first, utils.Munch): first = first.toDict() - if isinstance(second, munch.Munch): + if isinstance(second, utils.Munch): second = second.toDict() return super(TestCase, self).assertEqual( first, second, *args, **kwargs) diff --git a/openstack/tests/unit/cloud/test_create_server.py b/openstack/tests/unit/cloud/test_create_server.py index 48ccd9968..d255e60e6 100644 --- a/openstack/tests/unit/cloud/test_create_server.py +++ b/openstack/tests/unit/cloud/test_create_server.py @@ -30,6 +30,11 @@ class TestCreateServer(base.TestCase): + def _compare_servers(self, exp, real): + self.assertDictEqual( + server.Server(**exp).to_dict(computed=False), + real.to_dict(computed=False), + ) def test_create_server_with_get_exception(self): """ @@ -330,7 +335,7 @@ def test_create_server_with_admin_pass_no_wait(self): json={'server': fake_server}), ]) self.assertEqual( - self.cloud._normalize_server(fake_create_server)['adminPass'], + admin_pass, self.cloud.create_server( name='server-name', image=dict(id='image-id'), flavor=dict(id='flavor-id'), @@ -369,9 +374,9 @@ def test_create_server_with_admin_pass_wait(self, mock_wait): ]) # The wait returns non-password server - mock_wait.return_value = self.cloud._normalize_server(fake_server) + mock_wait.return_value = server.Server(**fake_server) - server = self.cloud.create_server( + new_server = self.cloud.create_server( name='server-name', image=dict(id='image-id'), flavor=dict(id='flavor-id'), admin_pass=admin_pass, wait=True) @@ -381,8 +386,8 @@ def test_create_server_with_admin_pass_wait(self, mock_wait): # Even with the wait, we should still get back a passworded server self.assertEqual( - server['admin_password'], - self.cloud._normalize_server(fake_server_with_pass)['adminPass'] + new_server['admin_password'], + fake_server_with_pass['adminPass'] ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_floating_ip_neutron.py b/openstack/tests/unit/cloud/test_floating_ip_neutron.py index a3ae14229..d2fb8710b 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_neutron.py +++ b/openstack/tests/unit/cloud/test_floating_ip_neutron.py @@ -22,11 +22,10 @@ import copy import datetime -import munch - from openstack.cloud import exc from openstack.tests import fakes from openstack.tests.unit import base +from openstack import utils class TestFloatingIP(base.TestCase): @@ -570,7 +569,7 @@ def test_auto_ip_pool_no_reuse(self): }]})]) self.cloud.add_ips_to_server( - munch.Munch( + utils.Munch( id='f80e3ad0-e13e-41d4-8e9c-be79bccdb8f7', addresses={ "private": [{ diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index c4b1c437b..cd1b09b58 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -14,13 +14,13 @@ import queue from unittest import mock -import munch from testscenarios import load_tests_apply_scenarios as load_tests # noqa from openstack import exceptions from openstack import proxy from openstack import resource from openstack.tests.unit import base +from openstack import utils class DeleteableResource(resource.Resource): @@ -182,7 +182,7 @@ def test__get_resource_from_munch(self): res._update = mock.Mock() cls._from_munch.return_value = res - m = munch.Munch(answer=42) + m = utils.Munch(answer=42) attrs = {"first": "Brian", "last": "Curtin"} result = self.fake_proxy._get_resource(cls, m, **attrs) diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 8611b473b..c2b46fa3e 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -15,13 +15,13 @@ from unittest import mock from keystoneauth1 import adapter -import munch import requests from openstack import exceptions from openstack import format from openstack import resource from openstack.tests.unit import base +from openstack import utils class FakeResponse: @@ -947,14 +947,14 @@ class Test(resource.Resource): self.assertEqual('bar', res.foo_alias) self.assertTrue('foo' in res.keys()) self.assertTrue('foo_alias' in res.keys()) - expected = munch.Munch({ + expected = utils.Munch({ 'id': None, 'name': 'test', 'location': None, 'foo': 'bar', 'foo_alias': 'bar' }) - actual = munch.Munch(res) + actual = utils.Munch(res) self.assertEqual(expected, actual) self.assertEqual(expected, res.toDict()) self.assertEqual(expected, res.to_dict()) @@ -1035,7 +1035,7 @@ class Test(resource.Resource): attr = resource.Body("body_attr") value = "value" - orig = munch.Munch(body_attr=value) + orig = utils.Munch(body_attr=value) sot = Test._from_munch(orig, synchronized=False) self.assertIn("body_attr", sot._body.dirty) @@ -1046,7 +1046,7 @@ class Test(resource.Resource): attr = resource.Body("body_attr") value = "value" - orig = munch.Munch(body_attr=value) + orig = utils.Munch(body_attr=value) sot = Test._from_munch(orig) self.assertNotIn("body_attr", sot._body.dirty) diff --git a/openstack/utils.py b/openstack/utils.py index 2e9997ccb..7c23e9cf8 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -9,7 +9,7 @@ # 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 collections.abc import Mapping import hashlib import queue import string @@ -403,3 +403,196 @@ def size(self): def is_complete(self): return len(self._done) == self.size() + + +# Importing Munch is a relatively expensive operation (0.3s) while we do not +# really even need much of it. Before we can rework all places where we rely on +# it we can have a reduced version. +class Munch(dict): + """A slightly stripped version of munch.Munch class""" + def __init__(self, *args, **kwargs): + self.update(*args, **kwargs) + + # only called if k not found in normal places + def __getattr__(self, k): + """Gets key if it exists, otherwise throws AttributeError. + """ + try: + return object.__getattribute__(self, k) + except AttributeError: + try: + return self[k] + except KeyError: + raise AttributeError(k) + + def __setattr__(self, k, v): + """Sets attribute k if it exists, otherwise sets key k. A KeyError + raised by set-item (only likely if you subclass Munch) will + propagate as an AttributeError instead. + """ + try: + # Throws exception if not in prototype chain + object.__getattribute__(self, k) + except AttributeError: + try: + self[k] = v + except Exception: + raise AttributeError(k) + else: + object.__setattr__(self, k, v) + + def __delattr__(self, k): + """Deletes attribute k if it exists, otherwise deletes key k. A KeyError + raised by deleting the key--such as when the key is missing--will + propagate as an AttributeError instead. + """ + try: + # Throws exception if not in prototype chain + object.__getattribute__(self, k) + except AttributeError: + try: + del self[k] + except KeyError: + raise AttributeError(k) + else: + object.__delattr__(self, k) + + def toDict(self): + """Recursively converts a munch back into a dictionary. + """ + return unmunchify(self) + + @property + def __dict__(self): + return self.toDict() + + def __repr__(self): + """Invertible* string-form of a Munch. """ + return f'{self.__class__.__name__}({dict.__repr__(self)})' + + def __dir__(self): + return list(self.keys()) + + def __getstate__(self): + """Implement a serializable interface used for pickling. + See https://docs.python.org/3.6/library/pickle.html. + """ + return {k: v for k, v in self.items()} + + def __setstate__(self, state): + """Implement a serializable interface used for pickling. + See https://docs.python.org/3.6/library/pickle.html. + """ + self.clear() + self.update(state) + + @classmethod + def fromDict(cls, d): + """Recursively transforms a dictionary into a Munch via copy.""" + return munchify(d, cls) + + def copy(self): + return type(self).fromDict(self) + + def update(self, *args, **kwargs): + """ + Override built-in method to call custom __setitem__ method that may + be defined in subclasses. + """ + for k, v in dict(*args, **kwargs).items(): + self[k] = v + + def get(self, k, d=None): + """ + D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None. + """ + if k not in self: + return d + return self[k] + + def setdefault(self, k, d=None): + """ + D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D + """ + if k not in self: + self[k] = d + return self[k] + + +def munchify(x, factory=Munch): + """Recursively transforms a dictionary into a Munch via copy.""" + # Munchify x, using `seen` to track object cycles + seen = dict() + + def munchify_cycles(obj): + try: + return seen[id(obj)] + except KeyError: + pass + + seen[id(obj)] = partial = pre_munchify(obj) + return post_munchify(partial, obj) + + def pre_munchify(obj): + if isinstance(obj, Mapping): + return factory({}) + elif isinstance(obj, list): + return type(obj)() + elif isinstance(obj, tuple): + type_factory = getattr(obj, "_make", type(obj)) + return type_factory(munchify_cycles(item) for item in obj) + else: + return obj + + def post_munchify(partial, obj): + if isinstance(obj, Mapping): + partial.update((k, munchify_cycles(obj[k])) for k in obj.keys()) + elif isinstance(obj, list): + partial.extend(munchify_cycles(item) for item in obj) + elif isinstance(obj, tuple): + for (item_partial, item) in zip(partial, obj): + post_munchify(item_partial, item) + + return partial + + return munchify_cycles(x) + + +def unmunchify(x): + """Recursively converts a Munch into a dictionary.""" + + # Munchify x, using `seen` to track object cycles + seen = dict() + + def unmunchify_cycles(obj): + try: + return seen[id(obj)] + except KeyError: + pass + + seen[id(obj)] = partial = pre_unmunchify(obj) + return post_unmunchify(partial, obj) + + def pre_unmunchify(obj): + if isinstance(obj, Mapping): + return dict() + elif isinstance(obj, list): + return type(obj)() + elif isinstance(obj, tuple): + type_factory = getattr(obj, "_make", type(obj)) + return type_factory(unmunchify_cycles(item) for item in obj) + else: + return obj + + def post_unmunchify(partial, obj): + if isinstance(obj, Mapping): + partial.update((k, unmunchify_cycles(obj[k])) for k in obj.keys()) + elif isinstance(obj, list): + partial.extend(unmunchify_cycles(v) for v in obj) + elif isinstance(obj, tuple): + for (value_partial, value) in zip(partial, obj): + post_unmunchify(value_partial, value) + + return partial + + return unmunchify_cycles(x) diff --git a/requirements.txt b/requirements.txt index 78e57b411..6dc4d905c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,6 @@ jsonpatch!=1.20,>=1.16 # BSD os-service-types>=1.7.0 # Apache-2.0 keystoneauth1>=3.18.0 # Apache-2.0 -munch>=2.1.0 # MIT decorator>=4.4.1 # BSD jmespath>=0.9.0 # MIT iso8601>=0.1.11 # MIT From 9ea832d660fe2ae52b8756ae499b23171cb61048 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 27 Jan 2023 18:09:56 +0100 Subject: [PATCH 3193/3836] Prepare release note for R1.0 We made a technical problem releasing lot of breaking changes in 0.99, but that was necessary due to inability to build RC. In order to log things properly create an explanational releasenote describing issues and repeating major stuff. Change-Id: I34d68d2d22d0a5a221d976713ec7a0db4745b299 --- releasenotes/notes/r1-d4efe289ebf0cbcd.yaml | 40 +++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 releasenotes/notes/r1-d4efe289ebf0cbcd.yaml diff --git a/releasenotes/notes/r1-d4efe289ebf0cbcd.yaml b/releasenotes/notes/r1-d4efe289ebf0cbcd.yaml new file mode 100644 index 000000000..c2e79ad64 --- /dev/null +++ b/releasenotes/notes/r1-d4efe289ebf0cbcd.yaml @@ -0,0 +1,40 @@ +--- +prelude: > + This is a final R1.0 release of the OpenStackSDK. A few technical issues + caused us not to reach this milestone cleanly, therefore we decided to one + more time explicitly log everything what should be considered as R1.0. For + detailed list of changes please see individual release notes from 0.99.0 to + 0.103.0. Most important changes are explicitly repeated here. There were + issues with maintainability of multiple available access interfaces, which + forced us to consider what we are able to maintain in the long run and what + we can not. That means that certain things were dropped, which is why we + are releasing this as a major release. R1.0 is considered as a first major + release with corresponding promise regarding backwards-compatibility. +features: + - | + Cloud layer is now consistently returning ``Resource`` class objects. + Previously this was not always the case. + - | + API response caching is implemented deep inside the code which will + minimize roundtrips for repeated requests. + - | + The majority of services were verified and adapted to the latest state of + the API. + - | + Certain code reorganization to further help in code reduction has been made + (metadata, tag and quota support moved to standalone common classes). +upgrade: + - | + Cloud layer methods are returning ``Resource`` class objects instead of + ``Munch`` objects. In some cases this cause renaming of the attributes. + ``Resource`` class is ``Munch`` compatible and allows both dictionary and + attribute base access. + - | + Some historical methods, which were never properly tested were dropped. +deprecations: + - | + ``Munch`` is dropped as a dependency. The project has no releases since + multiple years and was causing huge performance impact already during + import. This has directly no negative imapct to SDK users (it now starts + faster), but in the code we copied used ``Munch`` pieces. They are going to + be consistently eliminated in next releases. From 3876f3f6d9d45f22de7429e14744e66439d90fdf Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 31 Jan 2023 14:48:01 +0000 Subject: [PATCH 3194/3836] Update README to indicate COE resource/proxy support We now have first class support for Magnum. A unrelated typo is also corrected. Change-Id: I5065d3a780bc3d530e22e1eb2619fe107741abec Signed-off-by: Stephen Finucane --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index f46537d06..c3d8a3a67 100644 --- a/README.rst +++ b/README.rst @@ -210,7 +210,7 @@ OpenStack service can be found in the `Project Navigator`__. * - Cinder - Shared filesystems - ✔ - - ✔ (``openstack.share_file_system``) + - ✔ (``openstack.shared_file_system``) * - **Networking** - @@ -265,7 +265,7 @@ OpenStack service can be found in the `Project Navigator`__. * - Magnum - Container orchestration engine provisioning - ✔ - - ✘ + - ✔ (``openstack.container_infrastructure_management``) * - **Orchestration** - From 4ce2e960510b78986d6983eef478afee93eea0e7 Mon Sep 17 00:00:00 2001 From: Johannes Beisiegel Date: Thu, 9 Feb 2023 13:00:14 +0100 Subject: [PATCH 3195/3836] fix docstrings refering to volume attachments instead of server migrations Change-Id: Ie6d3f4e749d3abb598dd8d2f9c1686f1aef8a810 --- openstack/compute/v2/_proxy.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index b26a02487..445dbe11d 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -2005,9 +2005,9 @@ def abort_server_migration( that the migration belongs to. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when - the volume attachment does not exist. When set to ``True``, no + the server migration does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent - volume attachment. + server migration. :returns: ``None`` """ @@ -2053,7 +2053,7 @@ def get_server_migration( server, ignore_missing=True, ): - """Get a single volume attachment + """Get a single server migration :param server_migration: The value can be the ID of a server migration or a From 8588a4c7afb7969fc62a03e058236149b54ddedc Mon Sep 17 00:00:00 2001 From: Marvin Vogt Date: Mon, 13 Feb 2023 10:28:05 +0100 Subject: [PATCH 3196/3836] Remove usage of deprecated `sre_constants` module Remove usage of undocumented `sre_constants` module deprecated in `python3.11` and use `re` module instead. As dicsussed in https://github.com/python/cpython/issues/91308, `sre_constants` is undocumented and it's usage was deprecated starting with `python3.11`, where it causes a deprecation warning. https://docs.python.org/3/whatsnew/3.11.html#modules Importing `sre_constants` for exception handling of invalid regular expressions in not necessary, as the same exception class is exposed through the `re` module. Change-Id: Ifd9cccf504a5493683152178ebef9183f30b7f4c --- openstack/cloud/_utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index 259d08bb0..50bbb6d3e 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -23,7 +23,6 @@ from decorator import decorator import jmespath import netifaces -import sre_constants from openstack import _log from openstack.cloud import exc @@ -93,7 +92,7 @@ def _filter_list(data, name_or_id, filters): bad_pattern = False try: fn_reg = re.compile(fnmatch.translate(name_or_id)) - except sre_constants.error: + except re.error: # If the fnmatch re doesn't compile, then we don't care, # but log it in case the user DID pass a pattern but did # it poorly and wants to know what went wrong with their From c0e8027e8374732037bba827631001c126ed0582 Mon Sep 17 00:00:00 2001 From: Mridula Joshi Date: Tue, 10 Jan 2023 12:21:26 +0000 Subject: [PATCH 3197/3836] Add support for glance cache Change-Id: I539807d329b9529580b298cc96172c3a120950e1 --- openstack/image/v2/_proxy.py | 6 +++ openstack/image/v2/cache.py | 33 ++++++++++++++ openstack/tests/unit/image/v2/test_cache.py | 45 +++++++++++++++++++ openstack/tests/unit/image/v2/test_proxy.py | 13 ++++++ ...-image-cache-support-3f8c13550a84d749.yaml | 4 ++ 5 files changed, 101 insertions(+) create mode 100644 openstack/image/v2/cache.py create mode 100644 openstack/tests/unit/image/v2/test_cache.py create mode 100644 releasenotes/notes/add-image-cache-support-3f8c13550a84d749.yaml diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index a43abe999..0ea8fefd8 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -15,6 +15,7 @@ import warnings from openstack import exceptions +from openstack.image.v2 import cache as _cache from openstack.image.v2 import image as _image from openstack.image.v2 import member as _member from openstack.image.v2 import metadef_namespace as _metadef_namespace @@ -50,6 +51,7 @@ def _get_name_and_filename(name, image_format): class Proxy(proxy.Proxy): _resource_registry = { + "cache": _cache.Cache, "image": _image.Image, "image_member": _member.Member, "metadef_namespace": _metadef_namespace.MetadefNamespace, @@ -72,6 +74,10 @@ class Proxy(proxy.Proxy): _SHADE_IMAGE_SHA256_KEY = 'owner_specified.shade.sha256' _SHADE_IMAGE_OBJECT_KEY = 'owner_specified.shade.object' + # ====== CACHE MANAGEMENT====== + def get_image_cache(self): + return self._get(_cache.Cache, requires_id=False) + # ====== IMAGES ====== def create_image( self, diff --git a/openstack/image/v2/cache.py b/openstack/image/v2/cache.py new file mode 100644 index 000000000..fdc8630e1 --- /dev/null +++ b/openstack/image/v2/cache.py @@ -0,0 +1,33 @@ +# 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 resource + + +class CachedImage(resource.Resource): + image_id = resource.Body('image_id') + hits = resource.Body('hits') + last_accessed = resource.Body('last_accessed') + last_modified = resource.Body('last_modified') + size = resource.Body('size') + + +class Cache(resource.Resource): + base_path = '/cache' + + allow_fetch = True + + _max_microversion = '2.14' + + cached_images = resource.Body('cached_images', type=list, + list_type=CachedImage) + queued_images = resource.Body('queued_images', type=list) diff --git a/openstack/tests/unit/image/v2/test_cache.py b/openstack/tests/unit/image/v2/test_cache.py new file mode 100644 index 000000000..70c41294f --- /dev/null +++ b/openstack/tests/unit/image/v2/test_cache.py @@ -0,0 +1,45 @@ +# 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.image.v2 import cache +from openstack.tests.unit import base + + +EXAMPLE = { + 'cached_images': [ + {'hits': 0, + 'image_id': '1a56983c-f71f-490b-a7ac-6b321a18935a', + 'last_accessed': 1671699579.444378, + 'last_modified': 1671699579.444378, + 'size': 0}, + ], + 'queued_images': [ + '3a4560a1-e585-443e-9b39-553b46ec92d1', + '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810' + ] +} + + +class TestCache(base.TestCase): + def test_basic(self): + sot = cache.Cache() + self.assertIsNone(sot.resource_key) + self.assertEqual('/cache', sot.base_path) + self.assertTrue(sot.allow_fetch) + + def test_make_it(self): + sot = cache.Cache(**EXAMPLE) + self.assertEqual( + [cache.CachedImage(**e) for e in EXAMPLE['cached_images']], + sot.cached_images, + ) + self.assertEqual(EXAMPLE['queued_images'], sot.queued_images) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 047eba04c..491e702fb 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -17,6 +17,7 @@ from openstack import exceptions from openstack.image.v2 import _proxy +from openstack.image.v2 import cache as _cache from openstack.image.v2 import image as _image from openstack.image.v2 import member as _member from openstack.image.v2 import metadef_namespace as _metadef_namespace @@ -882,3 +883,15 @@ def test_metadef_tags_schema_get(self): 'requires_id': False, }, ) + + +class TestCache(TestImageProxy): + def test_image_cache_get(self): + self._verify( + "openstack.proxy.Proxy._get", + self.proxy.get_image_cache, + expected_args=[_cache.Cache], + expected_kwargs={ + 'requires_id': False + }, + ) diff --git a/releasenotes/notes/add-image-cache-support-3f8c13550a84d749.yaml b/releasenotes/notes/add-image-cache-support-3f8c13550a84d749.yaml new file mode 100644 index 000000000..36dc0fb83 --- /dev/null +++ b/releasenotes/notes/add-image-cache-support-3f8c13550a84d749.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add support for glance Cache API. From 8086da0d11121006f164f15371f6ae96021ebbe8 Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Tue, 14 Feb 2023 13:52:33 +0100 Subject: [PATCH 3198/3836] Include "security_groups" to "Port" query parameters This new query parameter will allow to send a list query filtering by the port security groups. Story: #2010585 Task: #47378 Change-Id: Ifa9d55b258800879f053098915802aabef48e951 --- openstack/network/v2/port.py | 2 +- openstack/tests/unit/network/v2/test_port.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index 5502e48fd..e7cecd9e2 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -34,7 +34,7 @@ class Port(_base.NetworkResource, tag.TagMixin): 'binding:vif_type', 'binding:vnic_type', 'description', 'device_id', 'device_owner', 'fields', 'fixed_ips', 'id', 'ip_address', 'mac_address', 'name', 'network_id', 'status', - 'subnet_id', 'project_id', + 'subnet_id', 'project_id', 'security_groups', is_admin_state_up='admin_state_up', is_port_security_enabled='port_security_enabled', **tag.TagMixin._tag_query_parameters diff --git a/openstack/tests/unit/network/v2/test_port.py b/openstack/tests/unit/network/v2/test_port.py index 861384dca..cad5f9577 100644 --- a/openstack/tests/unit/network/v2/test_port.py +++ b/openstack/tests/unit/network/v2/test_port.py @@ -93,6 +93,7 @@ def test_basic(self): "mac_address": "mac_address", "name": "name", "network_id": "network_id", + "security_groups": "security_groups", "status": "status", "subnet_id": "subnet_id", "is_admin_state_up": "admin_state_up", From 74f8869fd9ca436071d2227a8a668463e1f414e6 Mon Sep 17 00:00:00 2001 From: Goutham Pacha Ravi Date: Wed, 15 Feb 2023 16:58:46 -0800 Subject: [PATCH 3199/3836] Remove "feature/r1" override from manila job This was cruft left over from the merge of "feature/r1" to the "master" branch. Currently manila jobs are failing since a bindep fix [1] that's present in the "master" branch aren't present in the "feature/r1" branch. [1] https://review.opendev.org/c/openstack/openstacksdk/+/863839 Change-Id: Iecd6434a8b3eb92180e463f984e7982ed97ad61f --- .zuul.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 9f6a54324..446be8113 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -355,8 +355,7 @@ Run openstacksdk functional tests against a master devstack with manila required-projects: - openstack/manila - - name: openstack/openstacksdk - override-branch: feature/r1 + - openstack/openstacksdk vars: devstack_localrc: # Set up manila with a fake driver - makes things super fast and should From 7c7092e72e3589ee0af3119f9e77def4cd59600c Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Thu, 2 Mar 2023 14:10:35 +0000 Subject: [PATCH 3200/3836] Update master for stable/2023.1 Add file to the reno documentation build to show release notes for stable/2023.1. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/2023.1. Sem-Ver: feature Change-Id: If0ead68bf815af40bfe690c7d1a3517efc8fd56e --- releasenotes/source/2023.1.rst | 6 ++++++ releasenotes/source/index.rst | 1 + 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/2023.1.rst diff --git a/releasenotes/source/2023.1.rst b/releasenotes/source/2023.1.rst new file mode 100644 index 000000000..d1238479b --- /dev/null +++ b/releasenotes/source/2023.1.rst @@ -0,0 +1,6 @@ +=========================== +2023.1 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2023.1 diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index e9b2153b9..317361138 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ :maxdepth: 1 unreleased + 2023.1 zed yoga xena From c2b8612af170c44d2c978ffcf5545849ff186258 Mon Sep 17 00:00:00 2001 From: Kafilat Adeleke Date: Sat, 19 Jun 2021 07:49:37 +0000 Subject: [PATCH 3201/3836] Add share network resource to shared file system Introduce Share network class with basic methods including list, create, delete, get and update to shared file systems. Change-Id: Id91a6d3745897533c3d280a7b751146d9d30f898 Co-Authored-By: Samuel Loegering --- .../user/proxies/shared_file_system.rst | 11 +++ .../resources/shared_file_system/index.rst | 1 + .../shared_file_system/v2/share_network.rst | 13 ++++ openstack/shared_file_system/v2/_proxy.py | 71 +++++++++++++++++++ .../shared_file_system/v2/share_network.py | 48 +++++++++++++ .../shared_file_system/test_share_network.py | 61 ++++++++++++++++ .../unit/shared_file_system/v2/test_proxy.py | 41 +++++++++++ .../v2/test_share_network.py | 65 +++++++++++++++++ ...twork-to-shared-file-c5c9a6b8ccf1d958.yaml | 5 ++ 9 files changed, 316 insertions(+) create mode 100644 doc/source/user/resources/shared_file_system/v2/share_network.rst create mode 100644 openstack/shared_file_system/v2/share_network.py create mode 100644 openstack/tests/functional/shared_file_system/test_share_network.py create mode 100644 openstack/tests/unit/shared_file_system/v2/test_share_network.py create mode 100644 releasenotes/notes/add-share-network-to-shared-file-c5c9a6b8ccf1d958.yaml diff --git a/doc/source/user/proxies/shared_file_system.rst b/doc/source/user/proxies/shared_file_system.rst index 6c8b6cb5e..07581978f 100644 --- a/doc/source/user/proxies/shared_file_system.rst +++ b/doc/source/user/proxies/shared_file_system.rst @@ -79,3 +79,14 @@ service. :noindex: :members: share_snapshots, get_share_snapshot, delete_share_snapshot, update_share_snapshot, create_share_snapshot + + +Shared File System Share Networks +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Create and manipulate Share Networks with the Shared File Systems service. + +.. autoclass:: openstack.shared_file_system.v2._proxy.Proxy + :noindex: + :members: share_networks, get_share_network, delete_share_network, + update_share_network, create_share_network diff --git a/doc/source/user/resources/shared_file_system/index.rst b/doc/source/user/resources/shared_file_system/index.rst index 0a4cc5b4e..d769a0a49 100644 --- a/doc/source/user/resources/shared_file_system/index.rst +++ b/doc/source/user/resources/shared_file_system/index.rst @@ -10,3 +10,4 @@ Shared File System service resources v2/share v2/user_message v2/share_snapshot + v2/share_network diff --git a/doc/source/user/resources/shared_file_system/v2/share_network.rst b/doc/source/user/resources/shared_file_system/v2/share_network.rst new file mode 100644 index 000000000..793265d87 --- /dev/null +++ b/doc/source/user/resources/shared_file_system/v2/share_network.rst @@ -0,0 +1,13 @@ +openstack.shared_file_system.v2.share_network +============================================= + +.. automodule:: openstack.shared_file_system.v2.share_network + +The ShareNetwork Class +---------------------- + +The ``ShareNetwork`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.shared_file_system.v2.share_network.ShareNetwork + :members: diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index c4ffb1a9b..9325d9834 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -14,6 +14,9 @@ from openstack import resource from openstack.shared_file_system.v2 import ( availability_zone as _availability_zone) +from openstack.shared_file_system.v2 import ( + share_network as _share_network +) from openstack.shared_file_system.v2 import ( share_snapshot as _share_snapshot ) @@ -35,6 +38,7 @@ class Proxy(proxy.Proxy): "user_message": _user_message.UserMessage, "limit": _limit.Limit, "share": _share.Share, + "share_network": _share_network.ShareNetwork } def availability_zones(self): @@ -326,3 +330,70 @@ def wait_for_delete(self, res, interval=2, wait=120): to delete failed to occur in the specified seconds. """ return resource.wait_for_delete(self, res, interval, wait) + + def share_networks(self, details=True, **query): + """Lists all share networks with details. + + :param dict query: Optional query parameters to be sent to limit the + resources being returned. Available parameters include: + + * name~: The user defined name of the resource to filter resources + by. + * project_id: The ID of the user or service making the request. + * description~: The description pattern that can be used to filter + shares, share snapshots, share networks or share groups. + * all_projects: (Admin only). Defines whether to list the requested + resources for all projects. + + :returns: Details of shares networks + :rtype: :class:`~openstack.shared_file_system.v2. + share_network.ShareNetwork` + """ + base_path = '/share-networks/detail' if details else None + return self._list( + _share_network.ShareNetwork, base_path=base_path, **query) + + def get_share_network(self, share_network_id): + """Lists details of a single share network + + :param share_network: The ID of the share network to get + :returns: Details of the identified share network + :rtype: :class:`~openstack.shared_file_system.v2. + share_network.ShareNetwork` + """ + return self._get(_share_network.ShareNetwork, share_network_id) + + def delete_share_network(self, share_network_id, ignore_missing=True): + """Deletes a single share network + + :param share_network_id: The ID of the share network to delete + :rtype: ``None`` + """ + self._delete( + _share_network.ShareNetwork, share_network_id, + ignore_missing=ignore_missing) + + def update_share_network(self, share_network_id, **attrs): + """Updates details of a single share network. + + :param share_network_id: The ID of the share network to update + :pram dict attrs: The attributes to update on the share network + :returns: the updated share network + :rtype: :class:`~openstack.shared_file_system.v2. + share_network.ShareNetwork` + """ + return self._update( + _share_network.ShareNetwork, share_network_id, **attrs) + + def create_share_network(self, **attrs): + """Creates a share network from attributes + + :returns: Details of the new share network + :param dict attrs: Attributes which will be used to create + a :class:`~openstack.shared_file_system.v2. + share_network.ShareNetwork`,comprised of the properties + on the ShareNetwork class. + :rtype: :class:`~openstack.shared_file_system.v2. + share_network.ShareNetwork` + """ + return self._create(_share_network.ShareNetwork, **attrs) diff --git a/openstack/shared_file_system/v2/share_network.py b/openstack/shared_file_system/v2/share_network.py new file mode 100644 index 000000000..1cdc1431f --- /dev/null +++ b/openstack/shared_file_system/v2/share_network.py @@ -0,0 +1,48 @@ +# 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 resource + + +class ShareNetwork(resource.Resource): + resource_key = "share_network" + resources_key = "share_networks" + base_path = "/share-networks" + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + allow_head = False + + _query_mapping = resource.QueryParameters( + "project_id", "name", "description", + "created_since", "created_before", "security_service_id", + "limit", "offset", all_projects="all_tenants", + ) + + #: Properties + #: The date and time stamp when the resource was created within the + #: service’s database. + created_at = resource.Body("created_at") + #: The user defined description of the resource. + description = resource.Body("description", type=str) + #: The ID of the project that owns the resource. + project_id = resource.Body("project_id", type=str) + #: A list of share network subnets that pertain to the related share + #: network. + # share_network_subnets = resource.Body("share_network_subnets", type=list) + #: The date and time stamp when the resource was last updated within + #: the service’s database. + updated_at = resource.Body("updated_at", type=str) diff --git a/openstack/tests/functional/shared_file_system/test_share_network.py b/openstack/tests/functional/shared_file_system/test_share_network.py new file mode 100644 index 000000000..72880df96 --- /dev/null +++ b/openstack/tests/functional/shared_file_system/test_share_network.py @@ -0,0 +1,61 @@ +# 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.shared_file_system.v2 import share_network as _share_network +from openstack.tests.functional.shared_file_system import base + + +class ShareNetworkTest(base.BaseSharedFileSystemTest): + + def setUp(self): + super(ShareNetworkTest, self).setUp() + + self.SHARE_NETWORK_NAME = self.getUniqueString() + snt = self.user_cloud.shared_file_system.create_share_network( + name=self.SHARE_NETWORK_NAME) + self.assertIsNotNone(snt) + self.assertIsNotNone(snt.id) + self.SHARE_NETWORK_ID = snt.id + + def tearDown(self): + sot = self.user_cloud.shared_file_system.delete_share_network( + self.SHARE_NETWORK_ID, + ignore_missing=True) + self.assertIsNone(sot) + super(ShareNetworkTest, self).tearDown() + + def test_get(self): + sot = self.user_cloud.shared_file_system.get_share_network( + self.SHARE_NETWORK_ID) + assert isinstance(sot, _share_network.ShareNetwork) + self.assertEqual(self.SHARE_NETWORK_ID, sot.id) + + def test_list_share_network(self): + share_nets = self.user_cloud.shared_file_system.share_networks( + details=False + ) + self.assertGreater(len(list(share_nets)), 0) + for share_net in share_nets: + for attribute in ('id', 'name', 'created_at', 'updated_at'): + self.assertTrue(hasattr(share_net, attribute)) + + def test_delete_share_network(self): + sot = self.user_cloud.shared_file_system.delete_share_network( + self.SHARE_NETWORK_ID) + self.assertIsNone(sot) + + def test_update(self): + unt = self.user_cloud.shared_file_system.update_share_network( + self.SHARE_NETWORK_ID, description='updated share network') + get_unt = self.user_cloud.shared_file_system.get_share_network( + unt.id) + self.assertEqual('updated share network', get_unt.description) diff --git a/openstack/tests/unit/shared_file_system/v2/test_proxy.py b/openstack/tests/unit/shared_file_system/v2/test_proxy.py index be1cdc7fd..9c5c879f3 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_proxy.py +++ b/openstack/tests/unit/shared_file_system/v2/test_proxy.py @@ -15,6 +15,7 @@ from openstack.shared_file_system.v2 import _proxy from openstack.shared_file_system.v2 import limit from openstack.shared_file_system.v2 import share +from openstack.shared_file_system.v2 import share_network from openstack.shared_file_system.v2 import share_snapshot from openstack.shared_file_system.v2 import storage_pool from openstack.shared_file_system.v2 import user_message @@ -172,3 +173,43 @@ def test_wait_for_delete(self, mock_wait): self.proxy.wait_for_delete(mock_resource) mock_wait.assert_called_once_with(self.proxy, mock_resource, 2, 120) + + +class TestShareNetworkResource(test_proxy_base.TestProxyBase): + + def setUp(self): + super(TestShareNetworkResource, self).setUp() + self.proxy = _proxy.Proxy(self.session) + + def test_share_networks(self): + self.verify_list(self.proxy.share_networks, share_network.ShareNetwork) + + def test_share_networks_detailed(self): + self.verify_list(self.proxy.share_networks, share_network.ShareNetwork, + method_kwargs={"details": True, "name": "my_net"}, + expected_kwargs={"name": "my_net"}) + + def test_share_networks_not_detailed(self): + self.verify_list(self.proxy.share_networks, share_network.ShareNetwork, + method_kwargs={"details": False, "name": "my_net"}, + expected_kwargs={"name": "my_net"}) + + def test_share_network_get(self): + self.verify_get( + self.proxy.get_share_network, share_network.ShareNetwork) + + def test_share_network_delete(self): + self.verify_delete( + self.proxy.delete_share_network, share_network.ShareNetwork, False) + + def test_share_network_delete_ignore(self): + self.verify_delete( + self.proxy.delete_share_network, share_network.ShareNetwork, True) + + def test_share_network_create(self): + self.verify_create( + self.proxy.create_share_network, share_network.ShareNetwork) + + def test_share_network_update(self): + self.verify_update( + self.proxy.update_share_network, share_network.ShareNetwork) diff --git a/openstack/tests/unit/shared_file_system/v2/test_share_network.py b/openstack/tests/unit/shared_file_system/v2/test_share_network.py new file mode 100644 index 000000000..505740ab4 --- /dev/null +++ b/openstack/tests/unit/shared_file_system/v2/test_share_network.py @@ -0,0 +1,65 @@ +# 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.shared_file_system.v2 import share_network +from openstack.tests.unit import base + +IDENTIFIER = '6e1821be-c494-4f62-8301-5dcd19f4d615' +EXAMPLE = { + "id": IDENTIFIER, + "project_id": "4b8184eddd6b429a93231c056ae9cd12", + "name": "my_share_net", + "description": "My share network", + "created_at": "2021-06-10T10:11:17.291981", + "updated_at": None, + "share_network_subnets": [] +} + + +class TestShareNetwork(base.TestCase): + + def test_basic(self): + networks = share_network.ShareNetwork() + self.assertEqual('share_networks', networks.resources_key) + self.assertEqual('/share-networks', networks.base_path) + self.assertTrue(networks.allow_list) + self.assertTrue(networks.allow_create) + self.assertTrue(networks.allow_fetch) + self.assertTrue(networks.allow_commit) + self.assertTrue(networks.allow_delete) + self.assertFalse(networks.allow_head) + + self.assertDictEqual({ + "limit": "limit", + "marker": "marker", + "project_id": "project_id", + "created_since": "created_since", + "created_before": "created_before", + "offset": "offset", + "security_service_id": "security_service_id", + "project_id": "project_id", + "all_projects": "all_tenants", + "name": "name", + "description": "description" + }, + networks._query_mapping._mapping) + + def test_share_network(self): + networks = share_network.ShareNetwork(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], networks.id) + self.assertEqual(EXAMPLE['name'], networks.name) + self.assertEqual(EXAMPLE['project_id'], networks.project_id) + self.assertEqual( + EXAMPLE['description'], networks.description) + self.assertEqual( + EXAMPLE['created_at'], networks.created_at) + self.assertEqual(EXAMPLE['updated_at'], networks.updated_at) diff --git a/releasenotes/notes/add-share-network-to-shared-file-c5c9a6b8ccf1d958.yaml b/releasenotes/notes/add-share-network-to-shared-file-c5c9a6b8ccf1d958.yaml new file mode 100644 index 000000000..5177b9977 --- /dev/null +++ b/releasenotes/notes/add-share-network-to-shared-file-c5c9a6b8ccf1d958.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added support to create, update, list, get, and delete share + networks on the shared file system service. From 9f0737527e681602d6942027e5d5d07b1d3a2c1e Mon Sep 17 00:00:00 2001 From: Gregory Thiemonge Date: Tue, 7 Mar 2023 08:39:51 +0100 Subject: [PATCH 3202/3836] Fix Accept header for deleting Octavia load balancers Octavia now handles properly the Accept headers of incomining requests, sending an Accept header with an empty string doesn't make much sense as it implies that the client accepts nothing. Change-Id: Ibdd389342f1960ea0f5bd28521e438553f3b1935 --- openstack/load_balancer/v2/load_balancer.py | 6 ------ openstack/tests/unit/load_balancer/test_load_balancer.py | 4 ---- 2 files changed, 10 deletions(-) diff --git a/openstack/load_balancer/v2/load_balancer.py b/openstack/load_balancer/v2/load_balancer.py index 761555e51..d54e560f5 100644 --- a/openstack/load_balancer/v2/load_balancer.py +++ b/openstack/load_balancer/v2/load_balancer.py @@ -75,17 +75,11 @@ class LoadBalancer(resource.Resource, tag.TagMixin): def delete(self, session, error_message=None): request = self._prepare_request() - headers = { - "Accept": "" - } - - request.headers.update(headers) params = {} if (hasattr(self, 'cascade') and isinstance(self.cascade, bool) and self.cascade): params['cascade'] = True response = session.delete(request.url, - headers=headers, params=params) self._translate_response(response, has_body=False, diff --git a/openstack/tests/unit/load_balancer/test_load_balancer.py b/openstack/tests/unit/load_balancer/test_load_balancer.py index ed52db90d..fb4f19cb1 100644 --- a/openstack/tests/unit/load_balancer/test_load_balancer.py +++ b/openstack/tests/unit/load_balancer/test_load_balancer.py @@ -141,10 +141,8 @@ def test_delete_non_cascade(self): url = 'lbaas/loadbalancers/%(lb)s' % { 'lb': EXAMPLE['id'] } - headers = {'Accept': ''} params = {} sess.delete.assert_called_with(url, - headers=headers, params=params) sot._translate_response.assert_called_once_with( resp, @@ -165,10 +163,8 @@ def test_delete_cascade(self): url = 'lbaas/loadbalancers/%(lb)s' % { 'lb': EXAMPLE['id'] } - headers = {'Accept': ''} params = {'cascade': True} sess.delete.assert_called_with(url, - headers=headers, params=params) sot._translate_response.assert_called_once_with( resp, From 43ab59d8b38152e255ebd0501a4d763eb815ad03 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 5 Oct 2022 12:23:57 +0200 Subject: [PATCH 3203/3836] Implement acceptance test job Implement acceptance tests. Those jobs will run in the post-review pipeline requiring access to secrets containing credentials of friendly public clouds to test sdk with them. Base job is generating a token from the given credentials and writes clouds.yaml file with the token inside instead of password. As a post step the token is physically revoked. This is done to prevent potential leakage of real credentials from the test jobs/logs. Since devstack is not a real cloud we do not use zuul secrets. Change-Id: I95af9b81e6abd51af2a7dd91cae14b56926a869c --- .zuul.yaml | 38 +++++++++ playbooks/acceptance/library | 1 + playbooks/acceptance/post.yaml | 18 ++++ playbooks/acceptance/pre.yaml | 60 ++++++++++++++ playbooks/acceptance/run-with-devstack.yaml | 83 +++++++++++++++++++ playbooks/library/os_auth.py | 45 ++++++++++ roles/deploy-clouds-config/README.rst | 0 roles/deploy-clouds-config/defaults/main.yaml | 1 + roles/deploy-clouds-config/tasks/main.yaml | 11 +++ .../templates/clouds.yaml.j2 | 2 + roles/revoke_token/README.rst | 0 roles/revoke_token/library/os_auth_revoke.py | 72 ++++++++++++++++ roles/revoke_token/tasks/main.yaml | 7 ++ 13 files changed, 338 insertions(+) create mode 120000 playbooks/acceptance/library create mode 100644 playbooks/acceptance/post.yaml create mode 100644 playbooks/acceptance/pre.yaml create mode 100644 playbooks/acceptance/run-with-devstack.yaml create mode 100644 playbooks/library/os_auth.py create mode 100644 roles/deploy-clouds-config/README.rst create mode 100644 roles/deploy-clouds-config/defaults/main.yaml create mode 100644 roles/deploy-clouds-config/tasks/main.yaml create mode 100644 roles/deploy-clouds-config/templates/clouds.yaml.j2 create mode 100644 roles/revoke_token/README.rst create mode 100644 roles/revoke_token/library/os_auth_revoke.py create mode 100644 roles/revoke_token/tasks/main.yaml diff --git a/.zuul.yaml b/.zuul.yaml index 446be8113..5244a4df7 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -436,6 +436,41 @@ required-projects: - openstack/openstacksdk +- job: + name: openstacksdk-acceptance-base + parent: openstack-tox + description: Acceptance test of the OpenStackSDK on real clouds + pre-run: + - playbooks/acceptance/pre.yaml + post-run: + - playbooks/acceptance/post.yaml + +- job: + name: openstacksdk-acceptance-devstack + parent: openstacksdk-functional-devstack + description: Acceptance test of the OpenStackSDK on real clouds + run: + - playbooks/acceptance/run-with-devstack.yaml + post-run: + - playbooks/acceptance/post.yaml + vars: + tox_envlist: acceptance-regular-user + tox_environment: + OPENSTACKSDK_DEMO_CLOUD: acceptance + OS_CLOUD: acceptance + OS_TEST_CLOUD: acceptance + openstack_credentials: + auth: + auth_url: "https://{{ hostvars['controller']['nodepool']['private_ipv4'] }}/identity" + username: demo + password: secretadmin + project_domain_id: default + project_name: demo + user_domain_id: default + identity_api_version: '3' + region_name: RegionOne + volume_api_version: '3' + - project-template: name: openstacksdk-functional-tips check: @@ -486,6 +521,9 @@ voting: false - ansible-collections-openstack-functional-devstack: voting: false + post-review: + jobs: + - openstacksdk-acceptance-devstack gate: jobs: - opendev-buildset-registry diff --git a/playbooks/acceptance/library b/playbooks/acceptance/library new file mode 120000 index 000000000..53bed9684 --- /dev/null +++ b/playbooks/acceptance/library @@ -0,0 +1 @@ +../library \ No newline at end of file diff --git a/playbooks/acceptance/post.yaml b/playbooks/acceptance/post.yaml new file mode 100644 index 000000000..4e3e00e82 --- /dev/null +++ b/playbooks/acceptance/post.yaml @@ -0,0 +1,18 @@ +- hosts: localhost + tasks: + # TODO: + # - clean the resources, which might have been created + # - revoke the temp token explicitly + - name: read token + command: "cat {{ zuul.executor.work_root }}/.{{ zuul.build }}" + register: token_data + no_log: true + + - name: delete data file + command: "shred {{ zuul.executor.work_root }}/.{{ zuul.build }}" + + - include_role: + name: revoke_token + vars: + cloud: "{{ openstack_credentials }}" + token: "{{ token_data.stdout }}" diff --git a/playbooks/acceptance/pre.yaml b/playbooks/acceptance/pre.yaml new file mode 100644 index 000000000..078fd2040 --- /dev/null +++ b/playbooks/acceptance/pre.yaml @@ -0,0 +1,60 @@ +- hosts: all + tasks: + - name: Get temporary token for the cloud + # nolog is important to keep job-output.json clean + no_log: true + os_auth: + cloud: + profile: "{{ openstack_credentials.profile | default(omit) }}" + auth: + auth_url: "{{ openstack_credentials.auth.auth_url }}" + username: "{{ openstack_credentials.auth.username }}" + password: "{{ openstack_credentials.auth.password }}" + user_domain_name: "{{ openstack_credentials.auth.user_domain_name | default(omit) }}" + user_domain_id: "{{ openstack_credentials.auth.user_domain_id | default(omit) }}" + domain_name: "{{ openstack_credentials.auth.domain_name | default(omit) }}" + domain_id: "{{ openstack_credentials.auth.domain_id | default(omit) }}" + project_name: "{{ openstack_credentials.auth.project_name | default(omit) }}" + project_id: "{{ openstack_credentials.auth.project_id | default(omit) }}" + project_domain_name: "{{ openstack_credentials.auth.project_domain_name | default(omit) }}" + project_domain_id: "{{ openstack_credentials.auth.project_domain_id | default(omit) }}" + register: os_auth + delegate_to: localhost + + - name: Verify token + no_log: true + os_auth: + cloud: + profile: "{{ openstack_credentials.profile | default(omit) }}" + auth_type: token + auth: + auth_url: "{{ openstack_credentials.auth.auth_url }}" + token: "{{ os_auth.auth_token }}" + project_name: "{{ openstack_credentials.auth.project_name | default(omit) }}" + project_id: "{{ openstack_credentials.auth.project_id | default(omit) }}" + project_domain_id: "{{ openstack_credentials.auth.project_domain_id | default(omit) }}" + project_domain_name: "{{ openstack_credentials.auth.project_domain_name | default(omit) }}" + delegate_to: localhost + + - name: Include deploy-clouds-config role + include_role: + name: deploy-clouds-config + vars: + cloud_config: + clouds: + acceptance: + profile: "{{ openstack_credentials.profile | default(omit) }}" + auth_type: "token" + auth: + auth_url: "{{ openstack_credentials.auth.auth_url | default(omit) }}" + project_name: "{{ openstack_credentials.auth.project_name | default(omit) }}" + token: "{{ os_auth.auth_token }}" + + # Intruders might want to corrupt clouds.yaml to avoid revoking token in the post phase + # To prevent this we save token on the executor for later use. + - name: Save token + delegate_to: localhost + copy: + dest: "{{ zuul.executor.work_root }}/.{{ zuul.build }}" + content: "{{ os_auth.auth_token }}" + mode: "0440" diff --git a/playbooks/acceptance/run-with-devstack.yaml b/playbooks/acceptance/run-with-devstack.yaml new file mode 100644 index 000000000..a8e8b3522 --- /dev/null +++ b/playbooks/acceptance/run-with-devstack.yaml @@ -0,0 +1,83 @@ +# Need to actually start devstack first +- hosts: all + roles: + - run-devstack + +# Prepare local clouds.yaml +# We can't rely on pre.yaml, since it is specifically delegates to +# localhost, while on devstack it will not work unless APIs are available +# over the net. +- hosts: all + tasks: + - name: Get temporary token for the cloud + # nolog is important to keep job-output.json clean + no_log: true + os_auth: + cloud: + profile: "{{ openstack_credentials.profile | default(omit) }}" + auth: + auth_url: "{{ openstack_credentials.auth.auth_url }}" + username: "{{ openstack_credentials.auth.username }}" + password: "{{ openstack_credentials.auth.password }}" + user_domain_name: "{{ openstack_credentials.auth.user_domain_name | default(omit) }}" + user_domain_id: "{{ openstack_credentials.auth.user_domain_id | default(omit) }}" + domain_name: "{{ openstack_credentials.auth.domain_name | default(omit) }}" + domain_id: "{{ openstack_credentials.auth.domain_id | default(omit) }}" + project_name: "{{ openstack_credentials.auth.project_name | default(omit) }}" + project_id: "{{ openstack_credentials.auth.project_id | default(omit) }}" + project_domain_name: "{{ openstack_credentials.auth.project_domain_name | default(omit) }}" + project_domain_id: "{{ openstack_credentials.auth.project_domain_id | default(omit) }}" + register: os_auth + + - name: Verify token + # nolog is important to keep job-output.json clean + no_log: true + os_auth: + cloud: + profile: "{{ openstack_credentials.profile | default(omit) }}" + auth_type: token + auth: + auth_url: "{{ openstack_credentials.auth.auth_url }}" + token: "{{ os_auth.auth_token }}" + project_name: "{{ openstack_credentials.auth.project_name | default(omit) }}" + project_id: "{{ openstack_credentials.auth.project_id | default(omit) }}" + project_domain_name: "{{ openstack_credentials.auth.project_domain_name | default(omit) }}" + project_domain_id: "{{ openstack_credentials.auth.project_domain_id | default(omit) }}" + + - name: Include deploy-clouds-config role + include_role: + name: deploy-clouds-config + vars: + cloud_config: + clouds: + acceptance: + profile: "{{ openstack_credentials.profile | default(omit) }}" + auth_type: "token" + auth: + + auth_url: "{{ openstack_credentials.auth.auth_url }}" + project_name: "{{ openstack_credentials.auth.project_name | default(omit) }}" + project_domain_id: "{{ openstack_credentials.auth.project_domain_id | default(omit) }}" + token: "{{ os_auth.auth_token }}" + verify: false + + # Intruders might want to corrupt clouds.yaml to avoid revoking token in + # the post phase. To prevent this we save token on the executor for later + # use. + - name: Save token + delegate_to: localhost + copy: + dest: "{{ zuul.executor.work_root }}/.{{ zuul.build }}" + content: "{{ os_auth.auth_token }}" + mode: "0640" + +# Run the rest +- hosts: all + roles: + - role: bindep + bindep_profile: test + bindep_dir: "{{ zuul_work_dir }}" + - test-setup + - ensure-tox + - get-devstack-os-environment + - tox diff --git a/playbooks/library/os_auth.py b/playbooks/library/os_auth.py new file mode 100644 index 000000000..48903a085 --- /dev/null +++ b/playbooks/library/os_auth.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +# +# 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. + +""" +Utility to get Keystone token +""" +from ansible.module_utils.basic import AnsibleModule + +import openstack + + +def get_cloud(cloud): + if isinstance(cloud, dict): + config = openstack.config.loader.OpenStackConfig().get_one(**cloud) + return openstack.connection.Connection(config=config) + else: + return openstack.connect(cloud=cloud) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + cloud=dict(required=True, type='raw', no_log=True), + ) + ) + cloud = get_cloud(module.params.get('cloud')) + module.exit_json( + changed=True, + auth_token=cloud.auth_token + ) + + +if __name__ == '__main__': + main() diff --git a/roles/deploy-clouds-config/README.rst b/roles/deploy-clouds-config/README.rst new file mode 100644 index 000000000..e69de29bb diff --git a/roles/deploy-clouds-config/defaults/main.yaml b/roles/deploy-clouds-config/defaults/main.yaml new file mode 100644 index 000000000..9739eb171 --- /dev/null +++ b/roles/deploy-clouds-config/defaults/main.yaml @@ -0,0 +1 @@ +zuul_work_dir: "{{ zuul.project.src_dir }}" diff --git a/roles/deploy-clouds-config/tasks/main.yaml b/roles/deploy-clouds-config/tasks/main.yaml new file mode 100644 index 000000000..f10533bda --- /dev/null +++ b/roles/deploy-clouds-config/tasks/main.yaml @@ -0,0 +1,11 @@ +- name: Create OpenStack config dir + ansible.builtin.file: + dest: ~/.config/openstack + state: directory + recurse: true + +- name: Deploy clouds.yaml + ansible.builtin.template: + src: clouds.yaml.j2 + dest: ~/.config/openstack/clouds.yaml + mode: 0440 diff --git a/roles/deploy-clouds-config/templates/clouds.yaml.j2 b/roles/deploy-clouds-config/templates/clouds.yaml.j2 new file mode 100644 index 000000000..267d90065 --- /dev/null +++ b/roles/deploy-clouds-config/templates/clouds.yaml.j2 @@ -0,0 +1,2 @@ +--- +{{ cloud_config | to_nice_yaml }} diff --git a/roles/revoke_token/README.rst b/roles/revoke_token/README.rst new file mode 100644 index 000000000..e69de29bb diff --git a/roles/revoke_token/library/os_auth_revoke.py b/roles/revoke_token/library/os_auth_revoke.py new file mode 100644 index 000000000..85a16a462 --- /dev/null +++ b/roles/revoke_token/library/os_auth_revoke.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# +# Copyright 2014 Rackspace Australia +# Copyright 2018 Red Hat, 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. + +""" +Utility to revoke Keystone token +""" + +import logging +import traceback + +from ansible.module_utils.basic import AnsibleModule +import keystoneauth1.exceptions +import requests +import requests.exceptions + +import openstack + + +def get_cloud(cloud): + if isinstance(cloud, dict): + config = openstack.config.loader.OpenStackConfig().get_one(**cloud) + return openstack.connection.Connection(config=config) + else: + return openstack.connect(cloud=cloud) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + cloud=dict(required=True, type='raw', no_log=True), + revoke_token=dict(required=True, type='str', no_log=True) + ) + ) + + p = module.params + cloud = get_cloud(p.get('cloud')) + try: + cloud.identity.delete( + '/auth/tokens', + headers={ + 'X-Subject-Token': p.get('revoke_token') + } + ) + except (keystoneauth1.exceptions.http.HttpError, + requests.exceptions.RequestException): + s = "Error performing token revoke" + logging.exception(s) + s += "\n" + traceback.format_exc() + module.fail_json( + changed=False, + msg=s, + cloud=cloud.name, + region_name=cloud.config.region_name) + module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/revoke_token/tasks/main.yaml b/roles/revoke_token/tasks/main.yaml new file mode 100644 index 000000000..d7730eb08 --- /dev/null +++ b/roles/revoke_token/tasks/main.yaml @@ -0,0 +1,7 @@ +- name: Revoke token + delegate_to: localhost + no_log: true + os_auth_revoke: + cloud: "{{ cloud }}" + revoke_token: "{{ token }}" + failed_when: false From 1b241d74c021d2efd80cd4f4bb4b72d51e9cdd01 Mon Sep 17 00:00:00 2001 From: elajkat Date: Mon, 27 Feb 2023 10:00:02 +0100 Subject: [PATCH 3204/3836] Add BGPVPN to SDK Included resources are: * BgpVpn * BgpVpnNetworkAssociation * BgpVpnPortAssociation * BgpVpnRouterAssociation Change-Id: I77ffa307d2797368591693cb296606045e949144 Related-Bug: #1999774 --- doc/source/user/proxies/network.rst | 15 + doc/source/user/resources/network/index.rst | 4 + .../user/resources/network/v2/bgpvpn.rst | 12 + .../network/v2/bgpvpn_network_association.rst | 13 + .../network/v2/bgpvpn_port_association.rst | 13 + .../network/v2/bgpvpn_router_association.rst | 13 + openstack/network/v2/_proxy.py | 393 ++++++++++++++++++ openstack/network/v2/bgpvpn.py | 54 +++ .../network/v2/bgpvpn_network_association.py | 40 ++ .../network/v2/bgpvpn_port_association.py | 49 +++ .../network/v2/bgpvpn_router_association.py | 44 ++ .../functional/network/v2/test_bgpvpn.py | 181 ++++++++ .../tests/unit/network/v2/test_bgpvpn.py | 99 +++++ openstack/tests/unit/network/v2/test_proxy.py | 209 ++++++++++ ...add_bgpvpn_resources-b3bd0b568c3c99db.yaml | 7 + 15 files changed, 1146 insertions(+) create mode 100644 doc/source/user/resources/network/v2/bgpvpn.rst create mode 100644 doc/source/user/resources/network/v2/bgpvpn_network_association.rst create mode 100644 doc/source/user/resources/network/v2/bgpvpn_port_association.rst create mode 100644 doc/source/user/resources/network/v2/bgpvpn_router_association.rst create mode 100644 openstack/network/v2/bgpvpn.py create mode 100644 openstack/network/v2/bgpvpn_network_association.py create mode 100644 openstack/network/v2/bgpvpn_port_association.py create mode 100644 openstack/network/v2/bgpvpn_router_association.py create mode 100644 openstack/tests/functional/network/v2/test_bgpvpn.py create mode 100644 openstack/tests/unit/network/v2/test_bgpvpn.py create mode 100644 releasenotes/notes/network_add_bgpvpn_resources-b3bd0b568c3c99db.yaml diff --git a/doc/source/user/proxies/network.rst b/doc/source/user/proxies/network.rst index 748d78d98..4ce20b92b 100644 --- a/doc/source/user/proxies/network.rst +++ b/doc/source/user/proxies/network.rst @@ -314,3 +314,18 @@ Tap As A Service Operations :members: create_tap_flow, delete_tap_flow, find_tap_flow, get_tap_flow, update_tap_flow, tap_flows, create_tap_service, delete_tap_service, find_tap_service, update_tap_service, tap_services + +BGPVPN operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + :noindex: + :members: create_bgpvpn, delete_bgpvpn, find_bgpvpn, get_bgpvpn, + update_bgpvpn, bgpvpns, create_bgpvpn_network_association, + delete_bgpvpn_network_association, get_bgpvpn_network_association, + bgpvpn_network_associations, create_bgpvpn_port_association, + delete_bgpvpn_port_association, find_bgpvpn_port_association, + get_bgpvpn_port_association, update_bgpvpn_port_association, + bgpvpn_port_associations, create_bgpvpn_router_association, + delete_bgpvpn_router_association, get_bgpvpn_router_association, + update_bgpvpn_router_association, bgpvpn_router_associations diff --git a/doc/source/user/resources/network/index.rst b/doc/source/user/resources/network/index.rst index aa13d628a..167c47746 100644 --- a/doc/source/user/resources/network/index.rst +++ b/doc/source/user/resources/network/index.rst @@ -11,6 +11,10 @@ Network Resources v2/availability_zone v2/bgp_peer v2/bgp_speaker + v2/bgpvpn + v2/bgpvpn_network_association + v2/bgpvpn_port_association + v2/bgpvpn_router_association v2/extension v2/flavor v2/floating_ip diff --git a/doc/source/user/resources/network/v2/bgpvpn.rst b/doc/source/user/resources/network/v2/bgpvpn.rst new file mode 100644 index 000000000..4c5d8af03 --- /dev/null +++ b/doc/source/user/resources/network/v2/bgpvpn.rst @@ -0,0 +1,12 @@ +openstack.network.v2.bgpvpn +============================= + +.. automodule:: openstack.network.v2.bgpvpn + +The BgpVpn Class +----------------- + +The ``BgpVpn`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.bgpvpn.BgpVpn + :members: diff --git a/doc/source/user/resources/network/v2/bgpvpn_network_association.rst b/doc/source/user/resources/network/v2/bgpvpn_network_association.rst new file mode 100644 index 000000000..9d78df436 --- /dev/null +++ b/doc/source/user/resources/network/v2/bgpvpn_network_association.rst @@ -0,0 +1,13 @@ +openstack.network.v2.bgpvpn_network_association +=============================================== + +.. automodule:: openstack.network.v2.bgpvpn_network_association + +The BgpVpnNetworkAssociation Class +---------------------------------- + +The ``BgpVpnNetworkAssociation`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.bgpvpn_network_association.BgpVpnNetworkAssociation + :members: diff --git a/doc/source/user/resources/network/v2/bgpvpn_port_association.rst b/doc/source/user/resources/network/v2/bgpvpn_port_association.rst new file mode 100644 index 000000000..07584c1aa --- /dev/null +++ b/doc/source/user/resources/network/v2/bgpvpn_port_association.rst @@ -0,0 +1,13 @@ +openstack.network.v2.bgpvpn_port_association +============================================ + +.. automodule:: openstack.network.v2.bgpvpn_port_association + +The BgpVpnPortAssociation Class +------------------------------- + +The ``BgpVpnPortAssociation`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.bgpvpn_port_association.BgpVpnPortAssociation + :members: diff --git a/doc/source/user/resources/network/v2/bgpvpn_router_association.rst b/doc/source/user/resources/network/v2/bgpvpn_router_association.rst new file mode 100644 index 000000000..4f046da7e --- /dev/null +++ b/doc/source/user/resources/network/v2/bgpvpn_router_association.rst @@ -0,0 +1,13 @@ +openstack.network.v2.bgpvpn_router_association +============================================== + +.. automodule:: openstack.network.v2.bgpvpn_router_association + +The BgpVpnRouterAssociation Class +--------------------------------- + +The ``BgpVpnRouterAssociation`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.bgpvpn_router_association.BgpVpnRouterAssociation + :members: diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 51272e72c..dd7140abc 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -23,6 +23,13 @@ from openstack.network.v2 import availability_zone from openstack.network.v2 import bgp_peer as _bgp_peer from openstack.network.v2 import bgp_speaker as _bgp_speaker +from openstack.network.v2 import bgpvpn as _bgpvpn +from openstack.network.v2 import bgpvpn_network_association as \ + _bgpvpn_network_association +from openstack.network.v2 import bgpvpn_port_association as \ + _bgpvpn_port_association +from openstack.network.v2 import bgpvpn_router_association as \ + _bgpvpn_router_association from openstack.network.v2 import extension from openstack.network.v2 import firewall_group as _firewall_group from openstack.network.v2 import firewall_policy as _firewall_policy @@ -90,6 +97,13 @@ class Proxy(proxy.Proxy, Generic[T]): "availability_zone": availability_zone.AvailabilityZone, "bgp_peer": _bgp_peer.BgpPeer, "bgp_speaker": _bgp_speaker.BgpSpeaker, + "bgpvpn": _bgpvpn.BgpVpn, + "bgpvpn_network_association": + _bgpvpn_network_association.BgpVpnNetworkAssociation, + "bgpvpn_port_association": + _bgpvpn_port_association.BgpVpnPortAssociation, + "bgpvpn_router_association": + _bgpvpn_router_association.BgpVpnRouterAssociation, "extension": extension.Extension, "firewall_group": _firewall_group.FirewallGroup, "firewall_policy": _firewall_policy.FirewallPolicy, @@ -646,6 +660,385 @@ def remove_bgp_speaker_from_dragent(self, bgp_agent, bgp_speaker_id): speaker = self._get_resource(_bgp_speaker.BgpSpeaker, bgp_speaker_id) speaker.remove_bgp_speaker_from_dragent(self, bgp_agent) + def create_bgpvpn(self, **attrs): + """Create a new BGPVPN + + :param attrs: Keyword arguments which will be used to create a + :class:`~openstack.network.v2.bgpvpn.BgpVpn`, comprised of the + properties on the BGPVPN class, for details see the Neutron + api-ref. + + :returns: The result of BGPVPN creation + :rtype: :class:`~openstack.network.v2.bgpvpn.BgpVpn` + """ + return self._create(_bgpvpn.BgpVpn, **attrs) + + def delete_bgpvpn(self, bgpvpn, ignore_missing=True): + """Delete a BGPVPN + + :param bgpvpn: The value can be either the ID of a bgpvpn or + a :class:`~openstack.network.v2.bgpvpn.BgpVpn` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the BGPVPN does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent BGPVPN. + + :returns: ``None`` + """ + self._delete(_bgpvpn.BgpVpn, bgpvpn, ignore_missing=ignore_missing) + + def find_bgpvpn(self, name_or_id, ignore_missing=True, **query): + """"Find a single BGPVPN + + :param name_or_id: The name or ID of a BGPVPN. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict query: Any additional parameters to be passed into + underlying methods. such as query filters. + :returns: One :class:`~openstack.network.v2.bgpvpn.BGPVPN` + or None + """ + return self._find(_bgpvpn.BgpVpn, name_or_id, + ignore_missing=ignore_missing, **query) + + def get_bgpvpn(self, bgpvpn): + """Get a signle BGPVPN + + :param bgpvpn: The value can be the ID of a BGPVPN or a + :class:`~openstack.network.v2.bgpvpn.BgpVpn` instance. + + :returns: One :class:`~openstack.network.v2.bgpvpn.BgpVpn` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_bgpvpn.BgpVpn, bgpvpn) + + def update_bgpvpn(self, bgppvpn, **attrs): + """Update a BGPVPN + + :param bgpvpn: Either the ID of a BGPVPN or a + :class:`~openstack.network.v2.bgpvpn.BgpVpn` instance. + :param attrs: The attributes to update on the BGPVPN represented + by ``value``. + + :returns: The updated BGPVPN + :rtype: :class:`~openstack.network.v2.bgpvpn.BgpVpn` + """ + return self._update(_bgpvpn.BgpVpn, bgppvpn, **attrs) + + def bgpvpns(self, **query): + """Return a generator of BGP VPNs + + :param dict query: Optional query parameters to be sent to limit + the resources being returned. + + :returns: A generator of BgpVPN objects + :rtype: :class:`~openstack.network.v2.bgpvpn.BgpVpn` + """ + return self._list(_bgpvpn.BgpVpn, **query) + + def create_bgpvpn_network_association(self, bgpvpn, **attrs): + """Create a new BGPVPN Network Association + + :param bgpvpn: The value can be either the ID of a bgpvpn or + a :class:`~openstack.network.v2.bgpvpn.BgpVpn` instance. + :param attrs: Keyword arguments which will be used to create + a :class:`~openstack.network.v2.bgpvpn_network_association. + BgpVpnNetworkAssociation`, + comprised of the properties on the BgpVpnNetworkAssociation class. + + :returns: The results of BgpVpnNetworkAssociation creation + :rtype: :class:`~openstack.network.v2.bgpvpn_network_association. + BgpVpnNetworkAssociation` + """ + bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) + return self._create( + _bgpvpn_network_association.BgpVpnNetworkAssociation, + bgpvpn_id=bgpvpn_res.id, **attrs) + + def delete_bgpvpn_network_association(self, bgpvpn, net_association, + ignore_missing=True): + """Delete a BGPVPN Network Association + + :param bgpvpn: The value can be either the ID of a bgpvpn or + a :class:`~openstack.network.v2.bgpvpn.BgpVpn` instance. + :param net_association: The value can be either the ID of a + bgpvpn_network_association or + a :class:`~openstack.network.v2.bgpvpn_network_association. + BgpVpnNetworkAssociation` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the BgpVpnNetworkAssociation does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent BgpVpnNetworkAssociation. + + :returns: ``None`` + """ + bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) + self._delete( + _bgpvpn_network_association.BgpVpnNetworkAssociation, + net_association, ignore_missing=ignore_missing, + bgpvpn_id=bgpvpn_res.id) + + def get_bgpvpn_network_association(self, bgpvpn, net_association): + """Get a signle BGPVPN Network Association + + :param bgpvpn: The value can be the ID of a BGPVPN or a + :class:`~openstack.network.v2.bgpvpn.BgpVpn` instance. + :param net_association: The value can be the ID of a + BgpVpnNetworkAssociation or a + :class:`~openstack.network.v2.bgpvpn_network_association. + BgpVpnNetworkAssociation` instance. + + :returns: One :class:`~openstack.network.v2. + bgpvpn_network_associaition.BgpVpnNetworkAssociation` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) + return self._get( + _bgpvpn_network_association.BgpVpnNetworkAssociation, + net_association, bgpvpn_id=bgpvpn_res.id) + + def bgpvpn_network_associations(self, bgpvpn, **query): + """Return a generator of BGP VPN Network Associations + + :param: bgpvpn: The value can be the ID of a BGPVPN or a + :class:`~openstack.network.v2.bgpvpn.BgpVpn` instance. + :param dict query: Optional query parameters to be sent to limit + the resources being returned. + + :returns: A generator of BgpVpnNetworkAssociation objects + :rtype: :class:`~openstack.network.v2.bgpvpn_network_association. + BgpVpnNetworkAssociation` + """ + bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) + return self._list( + _bgpvpn_network_association.BgpVpnNetworkAssociation, + bgpvpn_id=bgpvpn_res.id, **query) + + def create_bgpvpn_port_association(self, bgpvpn, **attrs): + """Create a new BGPVPN Port Association + + :param bgpvpn: The value can be either the ID of a bgpvpn or + a :class:`~openstack.network.v2.bgpvpn.BgpVpn` instance. + :param attrs: Keyword arguments which will be used to create + a :class:`~openstack.network.v2.bgpvpn_port_association. + BgpVpnPortAssociation`, + comprised of the properties on the BgpVpnPortAssociation class. + + :returns: The results of BgpVpnPortAssociation creation + :rtype: :class:`~openstack.network.v2.bgpvpn_port_association. + BgpVpnPortAssociation` + """ + bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) + return self._create( + _bgpvpn_port_association.BgpVpnPortAssociation, + bgpvpn_id=bgpvpn_res.id, **attrs) + + def delete_bgpvpn_port_association(self, bgpvpn, port_association, + ignore_missing=True): + """Delete a BGPVPN Port Association + + :param bgpvpn: The value can be either the ID of a bgpvpn or + a :class:`~openstack.network.v2.bgpvpn.BgpVpn` instance. + :param port_association: The value can be either the ID of a + bgpvpn_port_association or + a :class:`~openstack.network.v2.bgpvpn_port_association. + BgpVpnPortAssociation` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the BgpVpnPortAssociation does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent BgpVpnPortAssociation. + + :returns: ``None`` + """ + bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) + self._delete( + _bgpvpn_port_association.BgpVpnPortAssociation, + port_association, ignore_missing=ignore_missing, + bgpvpn_id=bgpvpn_res.id) + + def find_bgpvpn_port_association(self, name_or_id, bgpvpn_id, + ignore_missing=True, **query): + """"Find a single BGPVPN Port Association + + :param name_or_id: The name or ID of a BgpVpnNetworkAssociation. + :param bgpvpn_id: The value can be the ID of a BGPVPN. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict query: Any additional parameters to be passed into + underlying methods. such as query filters. + :returns: One :class:`~openstack.network.v2.bgpvpn.BGPVPN` + or None + """ + return self._find( + _bgpvpn_port_association.BgpVpnPortAssociation, + name_or_id, + ignore_missing=ignore_missing, bgpvpn_id=bgpvpn_id, **query) + + def get_bgpvpn_port_association(self, bgpvpn, port_association): + """Get a signle BGPVPN Port Association + + :param bgpvpn: The value can be the ID of a BGPVPN or a + :class:`~openstack.network.v2.bgpvpn.BgpVpn` instance. + :param port_association: The value can be the ID of a + BgpVpnPortAssociation or a + :class:`~openstack.network.v2.bgpvpn_port_association. + BgpVpnPortAssociation` instance. + + :returns: One :class:`~openstack.network.v2. + bgpvpn_port_associaition.BgpVpnPortAssociation` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) + return self._get( + _bgpvpn_port_association.BgpVpnPortAssociation, + port_association, bgpvpn_id=bgpvpn_res.id) + + def update_bgpvpn_port_association(self, bgpvpn, port_association, + **attrs): + """Update a BPGPN Port Association + + :param bgpvpn: Either the ID of a BGPVPN or a + :class:`~openstack.network.v2.bgpvpn.BgpVpn` instance. + :param port_association: The value can be the ID of a + BgpVpnPortAssociation or a + :class:`~openstack.network.v2.bgpvpn_port_association. + BgpVpnPortAssociation` instance. + :param attrs: The attributes to update on the BGPVPN represented + by ``value``. + + :returns: The updated BgpVpnPortAssociation. + :rtype: :class:`~openstack.network.v2.bgpvpn.BgpVpn` + """ + bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) + return self._update( + _bgpvpn_port_association.BgpVpnPortAssociation, + port_association, bgpvpn_id=bgpvpn_res.id, **attrs) + + def bgpvpn_port_associations(self, bgpvpn, **query): + """Return a generator of BGP VPN Port Associations + + :param: bgpvpn: The value can be the ID of a BGPVPN or a + :class:`~openstack.network.v2.bgpvpn.BgpVpn` instance. + :param dict query: Optional query parameters to be sent to limit + the resources being returned. + + :returns: A generator of BgpVpnNetworkAssociation objects + :rtype: :class:`~openstack.network.v2.bgpvpn_network_association. + BgpVpnNetworkAssociation` + """ + bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) + return self._list( + _bgpvpn_port_association.BgpVpnPortAssociation, + bgpvpn_id=bgpvpn_res.id, **query) + + def create_bgpvpn_router_association(self, bgpvpn, **attrs): + """Create a new BGPVPN Router Association + + :param bgpvpn: The value can be either the ID of a bgpvpn or + a :class:`~openstack.network.v2.bgpvpn.BgpVpn` instance. + :param attrs: Keyword arguments which will be used to create + a :class:`~openstack.network.v2.bgpvpn_router_association. + BgpVpnRouterAssociation`, + comprised of the properties on the BgpVpnRouterAssociation class. + + :returns: The results of BgpVpnRouterAssociation creation + :rtype: :class:`~openstack.network.v2.bgpvpn_router_association. + BgpVpnRouterAssociation` + """ + bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) + return self._create( + _bgpvpn_router_association.BgpVpnRouterAssociation, + bgpvpn_id=bgpvpn_res.id, **attrs) + + def delete_bgpvpn_router_association(self, bgpvpn, router_association, + ignore_missing=True): + """Delete a BGPVPN Router Association + + :param bgpvpn: The value can be either the ID of a bgpvpn or + a :class:`~openstack.network.v2.bgpvpn.BgpVpn` instance. + :param port_association: The value can be either the ID of a + bgpvpn_router_association or + a :class:`~openstack.network.v2.bgpvpn_router_association. + BgpVpnRouterAssociation` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the BgpVpnRouterAssociation does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent BgpVpnRouterAsociation. + + :returns: ``None`` + """ + bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) + self._delete( + _bgpvpn_router_association.BgpVpnRouterAssociation, + router_association, ignore_missing=ignore_missing, + bgpvpn_id=bgpvpn_res.id) + + def get_bgpvpn_router_association(self, bgpvpn, router_association): + """Get a signle BGPVPN Router Association + + :param bgpvpn: The value can be the ID of a BGPVPN or a + :class:`~openstack.network.v2.bgpvpn.BgpVpn` instance. + :param router_association: The value can be the ID of a + BgpVpnRouterAssociation or a + :class:`~openstack.network.v2.bgpvpn_router_association. + BgpVpnRouterAssociation` instance. + + :returns: One :class:`~openstack.network.v2. + bgpvpn_router_associaition.BgpVpnRouterAssociation` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) + return self._get( + _bgpvpn_router_association.BgpVpnRouterAssociation, + router_association, bgpvpn_id=bgpvpn_res.id) + + def update_bgpvpn_router_association(self, bgpvpn, + router_association, **attrs): + """Update a BPGPN Router Association + + :param dict query: Optional query parameters to be sent to limit + the resources being returned. + + :returns: A generator of BgpVpnNetworkAssociation objects + :rtype: :class:`~openstack.network.v2.bgpvpn_network_association. + BgpVpnNetworkAssociation` + """ + bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) + return self._update( + _bgpvpn_router_association.BgpVpnRouterAssociation, + router_association, bgpvpn_id=bgpvpn_res.id, **attrs) + + def bgpvpn_router_associations(self, bgpvpn, **query): + """Return a generator of BGP VPN router Associations + + :param: bgpvpn: The value can be the ID of a BGPVPN or a + :class:`~openstack.network.v2.bgpvpn.BgpVpn` instance. + :param dict query: Optional query parameters to be sent to limit + the resources being returned. + + :returns: A generator of BgpVpnRouterAssociation objects + :rtype: :class:`~openstack.network.v2.bgpvpn_router_association. + BgpVpnRouterAssociation` + """ + bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) + return self._list( + _bgpvpn_router_association.BgpVpnRouterAssociation, + bgpvpn_id=bgpvpn_res.id, **query) + def find_extension(self, name_or_id, ignore_missing=True, **query): """Find a single extension diff --git a/openstack/network/v2/bgpvpn.py b/openstack/network/v2/bgpvpn.py new file mode 100644 index 000000000..98de33208 --- /dev/null +++ b/openstack/network/v2/bgpvpn.py @@ -0,0 +1,54 @@ +# 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 resource + + +class BgpVpn(resource.Resource): + resource_key = 'bgpvpn' + resources_key = 'bgpvpns' + base_path = '/bgpvpn/bgpvpns' + + _allow_unknown_attrs_in_body = True + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + # Properties + #: The Id of the BGPVPN + id = resource.Body('id') + #: The BGPVPN's name. + name = resource.Body('name') + #: The ID of the project that owns the BGPVPN + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) + #: List of route distinguisher strings. + route_distinguishers = resource.Body('route_distinguishers') + #: Route Targets that will be both imported and used for export. + route_targets = resource.Body('route_targets') + #: Additional Route Targets that will be imported. + import_targets = resource.Body('import_targets') + #: Additional Route Targets that will be used for export. + export_targets = resource.Body('export_targets') + #: The default BGP LOCAL_PREF of routes that will be advertised to + #: the BGPVPN. + local_pref = resource.Body('local_pref') + #: The globally-assigned VXLAN vni for the BGP VPN. + vni = resource.Body('vni') + #: Selection of the type of VPN and the technology behind it. + #: Allowed values are l2 or l3. + type = resource.Body('type') diff --git a/openstack/network/v2/bgpvpn_network_association.py b/openstack/network/v2/bgpvpn_network_association.py new file mode 100644 index 000000000..2a041f578 --- /dev/null +++ b/openstack/network/v2/bgpvpn_network_association.py @@ -0,0 +1,40 @@ +# 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 resource + + +class BgpVpnNetworkAssociation(resource.Resource): + resource_key = 'network_association' + resources_key = 'network_associations' + base_path = '/bgpvpn/bgpvpns/%(bgpvpn_id)s/network_associations' + + _allow_unknown_attrs_in_body = True + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = False + allow_delete = True + allow_list = True + + # Properties + #: The Id of the BGPVPN + id = resource.Body('id') + #: The ID of the BGPVPN who owns Network Association. + bgpvpn_id = resource.URI('bgpvpn_id') + #: The ID of the project that owns the BGPVPN + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) + #: The ID of a Neutron network with which to associate the BGP VPN. + network_id = resource.Body('network_id') diff --git a/openstack/network/v2/bgpvpn_port_association.py b/openstack/network/v2/bgpvpn_port_association.py new file mode 100644 index 000000000..9b37b3e5c --- /dev/null +++ b/openstack/network/v2/bgpvpn_port_association.py @@ -0,0 +1,49 @@ +# 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 resource + + +class BgpVpnPortAssociation(resource.Resource): + resource_key = 'port_association' + resources_key = 'port_associations' + base_path = '/bgpvpn/bgpvpns/%(bgpvpn_id)s/port_associations' + _allow_unknown_attrs_in_body = True + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + # Properties + #: The Id of the BGPVPN + id = resource.Body('id') + #: The ID of the BGPVPN who owns Network Association. + bgpvpn_id = resource.URI('bgpvpn_id') + #: The ID of the project that owns the BGPVPN + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) + #: The ID of a Neutron Port with which to associate the BGP VPN. + port_id = resource.Body('port_id') + #: Boolean flag controlling whether or not the fixed IPs of a port will be + #: advertised to the BGPVPN (default: true). + advertise_fixed_ips = resource.Body('advertise_fixed_ips') + #: List of routes, each route being a dict with at least a type key, + #: which can be prefix or bgpvpn. + #: For the prefix type, the IP prefix (v4 or v6) to advertise is specified + #: in the prefix key. + #: For the bgpvpn type, the bgpvpn_id key specifies the BGPVPN from which + #: routes will be readvertised + routes = resource.Body('routes') diff --git a/openstack/network/v2/bgpvpn_router_association.py b/openstack/network/v2/bgpvpn_router_association.py new file mode 100644 index 000000000..bbc11a7c7 --- /dev/null +++ b/openstack/network/v2/bgpvpn_router_association.py @@ -0,0 +1,44 @@ +# 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 resource + + +class BgpVpnRouterAssociation(resource.Resource): + resource_key = 'router_association' + resources_key = 'router_associations' + base_path = '/bgpvpn/bgpvpns/%(bgpvpn_id)s/router_associations' + + _allow_unknown_attrs_in_body = True + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + # Properties + #: The Id of the BGPVPN + id = resource.Body('id') + #: The ID of the BGPVPN who owns Network Association. + bgpvpn_id = resource.URI('bgpvpn_id') + #: The ID of the project that owns the BGPVPN + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) + #: The ID of a Neutron router with which to associate the BGP VPN. + router_id = resource.Body('router_id') + #: Boolean flag controlling whether or not the routes specified in the + #: routes attribute of the router will be advertised to the BGPVPN + #: (default: true). + advertise_extra_routes = resource.Body('advertise_extra_routes') diff --git a/openstack/tests/functional/network/v2/test_bgpvpn.py b/openstack/tests/functional/network/v2/test_bgpvpn.py new file mode 100644 index 000000000..f706b4a29 --- /dev/null +++ b/openstack/tests/functional/network/v2/test_bgpvpn.py @@ -0,0 +1,181 @@ +# 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.network.v2 import bgpvpn as _bgpvpn +from openstack.network.v2 import bgpvpn_network_association as \ + _bgpvpn_net_assoc +from openstack.network.v2 import bgpvpn_port_association as _bgpvpn_port_assoc +from openstack.network.v2 import bgpvpn_router_association as \ + _bgpvpn_router_assoc +from openstack.network.v2 import network as _network +from openstack.network.v2 import port as _port +from openstack.network.v2 import router as _router +from openstack.network.v2 import subnet as _subnet +from openstack.tests.functional import base + + +class TestBGPVPN(base.BaseFunctionalTest): + + def setUp(self): + super().setUp() + + self.BGPVPN_NAME = 'my_bgpvpn' + self.getUniqueString() + self.NET_NAME = 'my_net' + self.getUniqueString() + self.SUBNET_NAME = 'my_subnet' + self.getUniqueString() + self.PORT_NAME = 'my_port' + self.getUniqueString() + self.ROUTER_NAME = 'my_router' + self.getUniqueString() + self.CIDR = "10.101.0.0/24" + self.ROUTE_DISTINGUISHERS = ['64512:1777', '64512:1888', '64512:1999'] + self.VNI = 1000 + self.ROUTE_TARGETS = '64512:1444', + self.IMPORT_TARGETS = '64512:1555', + self.EXPORT_TARGETS = '64512:1666' + self.TYPE = 'l3' + + if not self.user_cloud.network.find_extension("bgpvpn"): + self.skipTest("Neutron BGPVPN Extension disabled") + bgpvpn = self.operator_cloud.network.create_bgpvpn( + name=self.BGPVPN_NAME, + route_distinguishers=self.ROUTE_DISTINGUISHERS, + route_targets=self.ROUTE_TARGETS, + import_targets=self.IMPORT_TARGETS, + export_targets=self.EXPORT_TARGETS, + ) + assert isinstance(bgpvpn, _bgpvpn.BgpVpn) + self.BGPVPN = bgpvpn + + net = self.operator_cloud.network.create_network(name=self.NET_NAME) + assert isinstance(net, _network.Network) + self.NETWORK = net + subnet = self.operator_cloud.network.create_subnet( + name=self.SUBNET_NAME, + ip_version=4, + network_id=self.NETWORK.id, + cidr=self.CIDR, + ) + assert isinstance(subnet, _subnet.Subnet) + self.SUBNET = subnet + + port = self.operator_cloud.network.create_port( + name=self.PORT_NAME, network_id=self.NETWORK.id) + assert isinstance(port, _port.Port) + self.PORT = port + + router = self.operator_cloud.network.create_router( + name=self.ROUTER_NAME) + assert isinstance(router, _router.Router) + self.ROUTER = router + + net_assoc = ( + self.operator_cloud.network.create_bgpvpn_network_association( + self.BGPVPN, network_id=self.NETWORK.id)) + assert isinstance(net_assoc, + _bgpvpn_net_assoc.BgpVpnNetworkAssociation) + self.NET_ASSOC = net_assoc + + port_assoc = ( + self.operator_cloud.network.create_bgpvpn_port_association( + self.BGPVPN, port_id=self.PORT.id)) + assert isinstance(port_assoc, + _bgpvpn_port_assoc.BgpVpnPortAssociation) + self.PORT_ASSOC = port_assoc + + router_assoc = ( + self.operator_cloud.network.create_bgpvpn_router_association( + self.BGPVPN, router_id=self.ROUTER.id)) + assert isinstance(router_assoc, + _bgpvpn_router_assoc.BgpVpnRouterAssociation) + self.ROUTER_ASSOC = router_assoc + + def tearDown(self): + sot = self.operator_cloud.network.delete_bgpvpn(self.BGPVPN.id) + self.assertIsNone(sot) + sot = self.operator_cloud.network.delete_bgpvpn_network_association( + self.BGPVPN.id, self.NET_ASSOC.id) + self.assertIsNone(sot) + + sot = self.operator_cloud.network.delete_bgpvpn_port_association( + self.BGPVPN.id, self.PORT_ASSOC.id) + self.assertIsNone(sot) + sot = self.operator_cloud.network.delete_bgpvpn_router_association( + self.BGPVPN.id, self.ROUTER_ASSOC.id) + self.assertIsNone(sot) + + sot = self.operator_cloud.network.delete_router(self.ROUTER) + self.assertIsNone(sot) + sot = self.operator_cloud.network.delete_port(self.PORT) + self.assertIsNone(sot) + sot = self.operator_cloud.network.delete_subnet(self.SUBNET) + self.assertIsNone(sot) + sot = self.operator_cloud.network.delete_network(self.NETWORK) + self.assertIsNone(sot) + + super().tearDown() + + def test_find_bgpvpn(self): + sot = self.operator_cloud.network.find_bgpvpn(self.BGPVPN.name) + self.assertEqual(list(self.ROUTE_TARGETS), sot.route_targets) + self.assertEqual(list(self.IMPORT_TARGETS), sot.import_targets) + # Check defaults + self.assertEqual(self.TYPE, sot.type) + + def test_get_bgpvpn(self): + sot = self.operator_cloud.network.get_bgpvpn(self.BGPVPN.id) + self.assertEqual(list(self.ROUTE_TARGETS), sot.route_targets) + self.assertEqual([self.EXPORT_TARGETS], sot.export_targets) + self.assertEqual(list(self.IMPORT_TARGETS), sot.import_targets) + + def test_list_bgpvpns(self): + bgpvpn_ids = [bgpvpn.id for bgpvpn in + self.operator_cloud.network.bgpvpns()] + self.assertIn(self.BGPVPN.id, bgpvpn_ids) + + def test_update_bgpvpn(self): + sot = self.operator_cloud.network.update_bgpvpn( + self.BGPVPN.id, import_targets='64512:1333') + self.assertEqual(['64512:1333'], sot.import_targets) + + def test_get_bgpvpnnetwork_association(self): + sot = self.operator_cloud.network.get_bgpvpn_network_association( + self.BGPVPN.id, self.NET_ASSOC.id) + self.assertEqual(self.NETWORK.id, sot.network_id) + + def test_list_bgpvpn_network_associations(self): + net_assoc_ids = [ + net_assoc.id for net_assoc in + self.operator_cloud.network.bgpvpn_network_associations( + self.BGPVPN.id)] + self.assertIn(self.NET_ASSOC.id, net_assoc_ids) + + def test_get_bgpvpn_port_association(self): + sot = self.operator_cloud.network.get_bgpvpn_port_association( + self.BGPVPN.id, self.PORT_ASSOC.id) + self.assertEqual(self.PORT.id, sot.port_id) + + def test_list_bgpvpn_port_associations(self): + port_assoc_ids = [ + port_assoc.id for port_assoc in + self.operator_cloud.network.bgpvpn_port_associations( + self.BGPVPN.id)] + self.assertIn(self.PORT_ASSOC.id, port_assoc_ids) + + def test_get_bgpvpn_router_association(self): + sot = self.operator_cloud.network.get_bgpvpn_router_association( + self.BGPVPN.id, self.ROUTER_ASSOC.id) + self.assertEqual(self.ROUTER.id, sot.router_id) + + def test_list_bgpvpn_router_associations(self): + router_assoc_ids = [ + router_assoc.id for router_assoc in + self.operator_cloud.network.bgpvpn_router_associations( + self.BGPVPN.id)] + self.assertIn(self.ROUTER_ASSOC.id, router_assoc_ids) diff --git a/openstack/tests/unit/network/v2/test_bgpvpn.py b/openstack/tests/unit/network/v2/test_bgpvpn.py new file mode 100644 index 000000000..af2a661be --- /dev/null +++ b/openstack/tests/unit/network/v2/test_bgpvpn.py @@ -0,0 +1,99 @@ +# 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.network.v2 import bgpvpn +from openstack.network.v2 import bgpvpn_network_association +from openstack.network.v2 import bgpvpn_port_association +from openstack.network.v2 import bgpvpn_router_association +from openstack.network.v2 import network +from openstack.network.v2 import port +from openstack.network.v2 import router +from openstack.tests.unit import base + +IDENTIFIER = 'IDENTIFIER' +NET_ID = 'NET_ID' +PORT_ID = 'PORT_ID' +ROUTER_ID = 'ROUTER_ID' +EXAMPLE = { + 'id': IDENTIFIER, + 'name': 'bgpvpn', + 'project_id': '42', + 'route_distinguishers': ['64512:1777', '64512:1888', '64512:1999'], + 'route_targets': '64512:1444', + 'import_targets': '64512:1555', + 'export_targets': '64512:1666', +} + + +class TestBgpVpn(base.TestCase): + + def test_basic(self): + sot = bgpvpn.BgpVpn() + self.assertEqual('bgpvpn', sot.resource_key) + self.assertEqual('bgpvpns', sot.resources_key) + self.assertEqual('/bgpvpn/bgpvpns', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = bgpvpn.BgpVpn(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + self.assertEqual(EXAMPLE['route_distinguishers'], + sot.route_distinguishers) + self.assertEqual(EXAMPLE['route_targets'], sot.route_targets) + self.assertEqual(EXAMPLE['import_targets'], sot.import_targets) + self.assertEqual(EXAMPLE['export_targets'], sot.export_targets) + + self.assertDictEqual( + {'limit': 'limit', + 'marker': 'marker', + }, + sot._query_mapping._mapping) + + def test_create_bgpvpn_network_association(self): + test_bpgvpn = bgpvpn.BgpVpn(**EXAMPLE) + test_net = network.Network(**{'name': 'foo_net', 'id': NET_ID}) + sot = bgpvpn_network_association.BgpVpnNetworkAssociation( + bgpvn_id=test_bpgvpn.id, + network_id=test_net.id + ) + self.assertEqual(test_net.id, sot.network_id) + self.assertEqual(test_bpgvpn.id, sot.bgpvn_id) + + def test_create_bgpvpn_port_association(self): + test_bpgvpn = bgpvpn.BgpVpn(**EXAMPLE) + test_port = port.Port(**{ + 'name': 'foo_port', + 'id': PORT_ID, + 'network_id': NET_ID + }) + sot = bgpvpn_port_association.BgpVpnPortAssociation( + bgpvn_id=test_bpgvpn.id, + port_id=test_port.id + ) + self.assertEqual(test_port.id, sot.port_id) + self.assertEqual(test_bpgvpn.id, sot.bgpvn_id) + + def test_create_bgpvpn_router_association(self): + test_bpgvpn = bgpvpn.BgpVpn(**EXAMPLE) + test_router = router.Router(**{'name': 'foo_port'}) + sot = bgpvpn_router_association.BgpVpnRouterAssociation( + bgpvn_id=test_bpgvpn.id, + router_id=test_router.id + ) + self.assertEqual(test_router.id, sot.router_id) + self.assertEqual(test_bpgvpn.id, sot.bgpvn_id) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 66a0fd14f..57403f83f 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -22,6 +22,10 @@ from openstack.network.v2 import availability_zone from openstack.network.v2 import bgp_peer from openstack.network.v2 import bgp_speaker +from openstack.network.v2 import bgpvpn +from openstack.network.v2 import bgpvpn_network_association +from openstack.network.v2 import bgpvpn_port_association +from openstack.network.v2 import bgpvpn_router_association from openstack.network.v2 import extension from openstack.network.v2 import firewall_group from openstack.network.v2 import firewall_policy @@ -77,6 +81,7 @@ FIP_ID = 'fip-id-' + uuid.uuid4().hex CT_HELPER_ID = 'ct-helper-id-' + uuid.uuid4().hex LOCAL_IP_ID = 'lip-id-' + uuid.uuid4().hex +BGPVPN_ID = 'bgpvpn-id-' + uuid.uuid4().hex class TestNetworkProxy(test_proxy_base.TestProxyBase): @@ -1997,3 +2002,207 @@ def test_bgp_peers(self): def test_bgp_peer_update(self): self.verify_update(self.proxy.update_bgp_peer, bgp_peer.BgpPeer) + + +class TestNetworkBGPVPN(TestNetworkProxy): + NETWORK_ASSOCIATION = 'net-assoc-id' + uuid.uuid4().hex + PORT_ASSOCIATION = 'port-assoc-id' + uuid.uuid4().hex + ROUTER_ASSOCIATION = 'router-assoc-id' + uuid.uuid4().hex + + def test_bgpvpn_create(self): + self.verify_create(self.proxy.create_bgpvpn, bgpvpn.BgpVpn) + + def test_bgpvpn_delete(self): + self.verify_delete(self.proxy.delete_bgpvpn, + bgpvpn.BgpVpn, False) + + def test_bgpvpn_delete_ignore(self): + self.verify_delete(self.proxy.delete_bgpvpn, + bgpvpn.BgpVpn, True) + + def test_bgpvpn_find(self): + self.verify_find(self.proxy.find_bgpvpn, bgpvpn.BgpVpn) + + def test_bgpvpn_get(self): + self.verify_get(self.proxy.get_bgpvpn, bgpvpn.BgpVpn) + + def test_bgpvpns(self): + self.verify_list(self.proxy.bgpvpns, bgpvpn.BgpVpn) + + def test_bgpvpn_update(self): + self.verify_update(self.proxy.update_bgpvpn, bgpvpn.BgpVpn) + + def test_bgpvpn_network_association_create(self): + self.verify_create( + self.proxy.create_bgpvpn_network_association, + bgpvpn_network_association.BgpVpnNetworkAssociation, + method_kwargs={'bgpvpn': BGPVPN_ID}, + expected_kwargs={'bgpvpn_id': BGPVPN_ID} + ) + + def test_bgpvpn_network_association_delete(self): + self.verify_delete( + self.proxy.delete_bgpvpn_network_association, + bgpvpn_network_association.BgpVpnNetworkAssociation, + False, + method_args=[BGPVPN_ID, self.NETWORK_ASSOCIATION], + expected_args=[self.NETWORK_ASSOCIATION], + expected_kwargs={'ignore_missing': False, + 'bgpvpn_id': BGPVPN_ID} + ) + + def test_bgpvpn_network_association_delete_ignore(self): + self.verify_delete( + self.proxy.delete_bgpvpn_network_association, + bgpvpn_network_association.BgpVpnNetworkAssociation, + True, + method_args=[BGPVPN_ID, self.NETWORK_ASSOCIATION], + expected_args=[self.NETWORK_ASSOCIATION], + expected_kwargs={'ignore_missing': True, + 'bgpvpn_id': BGPVPN_ID} + ) + + def test_bgpvpn_network_association_get(self): + self.verify_get( + self.proxy.get_bgpvpn_network_association, + bgpvpn_network_association.BgpVpnNetworkAssociation, + method_args=[BGPVPN_ID, self.NETWORK_ASSOCIATION], + expected_args=[self.NETWORK_ASSOCIATION], + expected_kwargs={'bgpvpn_id': BGPVPN_ID} + ) + + def test_bgpvpn_network_associations(self): + self.verify_list( + self.proxy.bgpvpn_network_associations, + bgpvpn_network_association.BgpVpnNetworkAssociation, + method_args=[BGPVPN_ID, ], + expected_args=[], + expected_kwargs={'bgpvpn_id': BGPVPN_ID} + ) + + def test_bgpvpn_port_association_create(self): + self.verify_create( + self.proxy.create_bgpvpn_port_association, + bgpvpn_port_association.BgpVpnPortAssociation, + method_kwargs={'bgpvpn': BGPVPN_ID}, + expected_kwargs={'bgpvpn_id': BGPVPN_ID} + ) + + def test_bgpvpn_port_association_delete(self): + self.verify_delete( + self.proxy.delete_bgpvpn_port_association, + bgpvpn_port_association.BgpVpnPortAssociation, + False, + method_args=[BGPVPN_ID, self.PORT_ASSOCIATION], + expected_args=[self.PORT_ASSOCIATION], + expected_kwargs={'ignore_missing': False, + 'bgpvpn_id': BGPVPN_ID} + ) + + def test_bgpvpn_port_association_delete_ignore(self): + self.verify_delete( + self.proxy.delete_bgpvpn_port_association, + bgpvpn_port_association.BgpVpnPortAssociation, + True, + method_args=[BGPVPN_ID, self.PORT_ASSOCIATION], + expected_args=[self.PORT_ASSOCIATION], + expected_kwargs={'ignore_missing': True, + 'bgpvpn_id': BGPVPN_ID} + ) + + def test_bgpvpn_port_association_find(self): + self.verify_find( + self.proxy.find_bgpvpn_port_association, + bgpvpn_port_association.BgpVpnPortAssociation, + method_args=[BGPVPN_ID], + expected_args=['resource_name'], + method_kwargs={'ignore_missing': True}, + expected_kwargs={'ignore_missing': True, + 'bgpvpn_id': BGPVPN_ID}, + ) + + def test_bgpvpn_port_association_get(self): + self.verify_get( + self.proxy.get_bgpvpn_port_association, + bgpvpn_port_association.BgpVpnPortAssociation, + method_args=[BGPVPN_ID, self.PORT_ASSOCIATION], + expected_args=[self.PORT_ASSOCIATION], + expected_kwargs={'bgpvpn_id': BGPVPN_ID} + ) + + def test_bgpvpn_port_associations(self): + self.verify_list( + self.proxy.bgpvpn_port_associations, + bgpvpn_port_association.BgpVpnPortAssociation, + method_args=[BGPVPN_ID, ], + expected_args=[], + expected_kwargs={'bgpvpn_id': BGPVPN_ID} + ) + + def test_bgpvpn_port_association_update(self): + self.verify_update( + self.proxy.update_bgpvpn_port_association, + bgpvpn_port_association.BgpVpnPortAssociation, + method_args=[BGPVPN_ID, self.PORT_ASSOCIATION], + method_kwargs={}, + expected_args=[self.PORT_ASSOCIATION], + expected_kwargs={'bgpvpn_id': BGPVPN_ID} + ) + + def test_bgpvpn_router_association_create(self): + self.verify_create( + self.proxy.create_bgpvpn_router_association, + bgpvpn_router_association.BgpVpnRouterAssociation, + method_kwargs={'bgpvpn': BGPVPN_ID}, + expected_kwargs={'bgpvpn_id': BGPVPN_ID} + ) + + def test_bgpvpn_router_association_delete(self): + self.verify_delete( + self.proxy.delete_bgpvpn_router_association, + bgpvpn_router_association.BgpVpnRouterAssociation, + False, + method_args=[BGPVPN_ID, self.ROUTER_ASSOCIATION], + expected_args=[self.ROUTER_ASSOCIATION], + expected_kwargs={'ignore_missing': False, + 'bgpvpn_id': BGPVPN_ID} + ) + + def test_bgpvpn_router_association_delete_ignore(self): + self.verify_delete( + self.proxy.delete_bgpvpn_router_association, + bgpvpn_router_association.BgpVpnRouterAssociation, + True, + method_args=[BGPVPN_ID, self.ROUTER_ASSOCIATION], + expected_args=[self.ROUTER_ASSOCIATION], + expected_kwargs={'ignore_missing': True, + 'bgpvpn_id': BGPVPN_ID} + ) + + def test_bgpvpn_router_association_get(self): + self.verify_get( + self.proxy.get_bgpvpn_router_association, + bgpvpn_router_association.BgpVpnRouterAssociation, + method_args=[BGPVPN_ID, self.ROUTER_ASSOCIATION], + expected_args=[self.ROUTER_ASSOCIATION], + expected_kwargs={'bgpvpn_id': BGPVPN_ID} + ) + + def test_bgpvpn_router_associations(self): + self.verify_list( + self.proxy.bgpvpn_router_associations, + bgpvpn_router_association.BgpVpnRouterAssociation, + method_args=[BGPVPN_ID, ], + expected_args=[], + expected_kwargs={'bgpvpn_id': BGPVPN_ID} + ) + + def test_bgpvpn_router_association_update(self): + self.verify_update( + self.proxy.update_bgpvpn_router_association, + bgpvpn_router_association.BgpVpnRouterAssociation, + method_args=[BGPVPN_ID, self.ROUTER_ASSOCIATION], + method_kwargs={}, + expected_args=[self.ROUTER_ASSOCIATION], + expected_kwargs={'bgpvpn_id': BGPVPN_ID} + ) diff --git a/releasenotes/notes/network_add_bgpvpn_resources-b3bd0b568c3c99db.yaml b/releasenotes/notes/network_add_bgpvpn_resources-b3bd0b568c3c99db.yaml new file mode 100644 index 000000000..daa777f32 --- /dev/null +++ b/releasenotes/notes/network_add_bgpvpn_resources-b3bd0b568c3c99db.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Add BGPVPN, BGPVPN Network Association, BGPVPN Port Association, + and BGPVPN Router Association resources and introduce support + for CRUD operations for these. + From b06c27b9c958bc79e94e6031f8037fd86e4541ba Mon Sep 17 00:00:00 2001 From: Mridula Joshi Date: Tue, 21 Feb 2023 13:42:59 +0000 Subject: [PATCH 3205/3836] Adds SDK support for ``glance cache-delete`` Change-Id: I5da43bd6ffed8d6ba91e94c1320e05c8512ba08a --- doc/source/user/proxies/image_v2.rst | 8 ++++++++ openstack/image/v2/_proxy.py | 13 +++++++++++++ openstack/image/v2/cache.py | 1 + openstack/tests/unit/image/v2/test_cache.py | 1 + openstack/tests/unit/image/v2/test_proxy.py | 6 ++++++ 5 files changed, 29 insertions(+) diff --git a/doc/source/user/proxies/image_v2.rst b/doc/source/user/proxies/image_v2.rst index 9c283c67a..5327eaa55 100644 --- a/doc/source/user/proxies/image_v2.rst +++ b/doc/source/user/proxies/image_v2.rst @@ -82,3 +82,11 @@ Helpers .. autoclass:: openstack.image.v2._proxy.Proxy :noindex: :members: wait_for_delete + + +Cache Operations +^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.image.v2._proxy.Proxy + :noindex: + :members: cache_delete_image diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 0ea8fefd8..cf00639dd 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -78,6 +78,19 @@ class Proxy(proxy.Proxy): def get_image_cache(self): return self._get(_cache.Cache, requires_id=False) + def cache_delete_image(self, image, ignore_missing=True): + """Delete an image from cache. + + :param image: The value can be either the name of an image or a + :class:`~openstack.image.v2.image.Image` + instance. + :param bool ignore_missing: When set to ``False``, + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the metadef namespace does not exist. + :returns: ``None`` + """ + self._delete(_cache.Cache, image, ignore_missing=ignore_missing) + # ====== IMAGES ====== def create_image( self, diff --git a/openstack/image/v2/cache.py b/openstack/image/v2/cache.py index fdc8630e1..e63e954d5 100644 --- a/openstack/image/v2/cache.py +++ b/openstack/image/v2/cache.py @@ -25,6 +25,7 @@ class Cache(resource.Resource): base_path = '/cache' allow_fetch = True + allow_delete = True _max_microversion = '2.14' diff --git a/openstack/tests/unit/image/v2/test_cache.py b/openstack/tests/unit/image/v2/test_cache.py index 70c41294f..fc1cfc678 100644 --- a/openstack/tests/unit/image/v2/test_cache.py +++ b/openstack/tests/unit/image/v2/test_cache.py @@ -35,6 +35,7 @@ def test_basic(self): self.assertIsNone(sot.resource_key) self.assertEqual('/cache', sot.base_path) self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_delete) def test_make_it(self): sot = cache.Cache(**EXAMPLE) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 491e702fb..4a98e2632 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -895,3 +895,9 @@ def test_image_cache_get(self): 'requires_id': False }, ) + + def test_cache_image_delete(self): + self.verify_delete( + self.proxy.cache_delete_image, + _cache.Cache, + ) From 79e8c83da82d0e13e2f54eaec837286724d9e0c3 Mon Sep 17 00:00:00 2001 From: Kafilat Adeleke Date: Thu, 24 Jun 2021 09:49:57 +0000 Subject: [PATCH 3206/3836] Add share snapshot instance resource Introduce Share snapshot instance class with basic methods to list, and get to shared file system storage service. Change-Id: I14ccb77453d300afc6f387338e752f32ae57af20 Co-Authored-By: Reynaldo Bontje --- .../user/proxies/shared_file_system.rst | 10 ++++ .../resources/shared_file_system/index.rst | 1 + .../v2/share_snapshot_instance.rst | 13 +++++ openstack/shared_file_system/v2/_proxy.py | 40 ++++++++++++++- .../v2/share_snapshot_instance.py | 47 +++++++++++++++++ .../test_share_snapshot_instance.py | 37 ++++++++++++++ .../unit/shared_file_system/v2/test_proxy.py | 36 +++++++++++++ .../v2/test_share_snapshot_instance.py | 50 +++++++++++++++++++ ...tance-to-shared-file-4d935f12d67bf59d.yaml | 5 ++ 9 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 doc/source/user/resources/shared_file_system/v2/share_snapshot_instance.rst create mode 100644 openstack/shared_file_system/v2/share_snapshot_instance.py create mode 100644 openstack/tests/functional/shared_file_system/test_share_snapshot_instance.py create mode 100644 openstack/tests/unit/shared_file_system/v2/test_share_snapshot_instance.py create mode 100644 releasenotes/notes/add-share-snapshot-instance-to-shared-file-4d935f12d67bf59d.yaml diff --git a/doc/source/user/proxies/shared_file_system.rst b/doc/source/user/proxies/shared_file_system.rst index 07581978f..e5c5cddd4 100644 --- a/doc/source/user/proxies/shared_file_system.rst +++ b/doc/source/user/proxies/shared_file_system.rst @@ -80,6 +80,16 @@ service. :members: share_snapshots, get_share_snapshot, delete_share_snapshot, update_share_snapshot, create_share_snapshot +Shared File System Share Snapshot Instances +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Interact with Share Snapshot Instances supported by the +Shared File Systems service. + +.. autoclass:: openstack.shared_file_system.v2._proxy.Proxy + :noindex: + :members: share_snapshot_instances, get_share_snapshot_instance + Shared File System Share Networks ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/shared_file_system/index.rst b/doc/source/user/resources/shared_file_system/index.rst index d769a0a49..3ae1cead5 100644 --- a/doc/source/user/resources/shared_file_system/index.rst +++ b/doc/source/user/resources/shared_file_system/index.rst @@ -10,4 +10,5 @@ Shared File System service resources v2/share v2/user_message v2/share_snapshot + v2/share_snapshot_instance v2/share_network diff --git a/doc/source/user/resources/shared_file_system/v2/share_snapshot_instance.rst b/doc/source/user/resources/shared_file_system/v2/share_snapshot_instance.rst new file mode 100644 index 000000000..918459990 --- /dev/null +++ b/doc/source/user/resources/shared_file_system/v2/share_snapshot_instance.rst @@ -0,0 +1,13 @@ +openstack.shared_file_system.v2.share_snapshot_instance +======================================================= + +.. automodule:: openstack.shared_file_system.v2.share_snapshot_instance + +The ShareSnapshotInstance Class +------------------------------- + +The ``ShareSnapshotInstance`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.shared_file_system.v2.share_snapshot_instance.ShareSnapshotInstance + :members: diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index 9325d9834..33307dab5 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -20,6 +20,9 @@ from openstack.shared_file_system.v2 import ( share_snapshot as _share_snapshot ) +from openstack.shared_file_system.v2 import ( + share_snapshot_instance as _share_snapshot_instance +) from openstack.shared_file_system.v2 import ( storage_pool as _storage_pool ) @@ -38,7 +41,9 @@ class Proxy(proxy.Proxy): "user_message": _user_message.UserMessage, "limit": _limit.Limit, "share": _share.Share, - "share_network": _share_network.ShareNetwork + "share_network": _share_network.ShareNetwork, + "share_snapshot_instance": + _share_snapshot_instance.ShareSnapshotInstance, } def availability_zones(self): @@ -331,6 +336,39 @@ def wait_for_delete(self, res, interval=2, wait=120): """ return resource.wait_for_delete(self, res, interval, wait) + def share_snapshot_instances(self, details=True, **query): + """Lists all share snapshot instances with details. + + :param bool details: Whether to fetch detailed resource + descriptions. Defaults to True. + :param kwargs query: Optional query parameters to be sent to limit + the share snapshot instance being returned. + Available parameters include: + + * snapshot_id: The UUID of the share’s base snapshot to filter + the request based on. + * project_id: The project ID of the user or service making the + request. + + :returns: A generator of share snapshot instance resources + :rtype: :class:`~openstack.shared_file_system.v2. + share_snapshot_instance.ShareSnapshotInstance` + """ + base_path = '/snapshot-instances/detail' if details else None + return self._list(_share_snapshot_instance.ShareSnapshotInstance, + base_path=base_path, **query) + + def get_share_snapshot_instance(self, snapshot_instance_id): + """Lists details of a single share snapshot instance + + :param snapshot_instance_id: The ID of the snapshot instance to get + :returns: Details of the identified snapshot instance + :rtype: :class:`~openstack.shared_file_system.v2. + share_snapshot_instance.ShareSnapshotInstance` + """ + return self._get(_share_snapshot_instance.ShareSnapshotInstance, + snapshot_instance_id) + def share_networks(self, details=True, **query): """Lists all share networks with details. diff --git a/openstack/shared_file_system/v2/share_snapshot_instance.py b/openstack/shared_file_system/v2/share_snapshot_instance.py new file mode 100644 index 000000000..6b0acb96b --- /dev/null +++ b/openstack/shared_file_system/v2/share_snapshot_instance.py @@ -0,0 +1,47 @@ +# 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 resource + + +class ShareSnapshotInstance(resource.Resource): + resource_key = "snapshot_instance" + resources_key = "snapshot_instances" + base_path = "/snapshot-instances" + + # capabilities + allow_create = False + allow_fetch = True + allow_commit = False + allow_delete = False + allow_list = True + allow_head = False + + #: Properties + #: The date and time stamp when the resource was created within the + #: service’s database. + created_at = resource.Body("created_at", type=str) + #: The progress of the snapshot creation. + progress = resource.Body("progress", type=str) + #: Provider location of the snapshot on the backend. + provider_location = resource.Body("provider_location", type=str) + #: The UUID of the share. + share_id = resource.Body("share_id", type=str) + #: The UUID of the share instance. + share_instance_id = resource.Body("share_instance_id", type=str) + #: The UUID of the snapshot. + snapshot_id = resource.Body("snapshot_id", type=str) + #: The snapshot instance status. + status = resource.Body("status", type=str) + #: The date and time stamp when the resource was updated within the + #: service’s database. + updated_at = resource.Body("updated_at", type=str) diff --git a/openstack/tests/functional/shared_file_system/test_share_snapshot_instance.py b/openstack/tests/functional/shared_file_system/test_share_snapshot_instance.py new file mode 100644 index 000000000..0a7fc1ad6 --- /dev/null +++ b/openstack/tests/functional/shared_file_system/test_share_snapshot_instance.py @@ -0,0 +1,37 @@ +# 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.tests.functional.shared_file_system import base + + +class ShareSnapshotInstanceTest(base.BaseSharedFileSystemTest): + + def setUp(self): + super(ShareSnapshotInstanceTest, self).setUp() + + self.SHARE_NAME = self.getUniqueString() + my_share = self.create_share( + name=self.SHARE_NAME, size=2, share_type="dhss_false", + share_protocol='NFS', description=None) + self.SHARE_ID = my_share.id + self.create_share_snapshot( + share_id=self.SHARE_ID + ) + + def test_share_snapshot_instances(self): + sots = \ + self.operator_cloud.shared_file_system.share_snapshot_instances() + self.assertGreater(len(list(sots)), 0) + for sot in sots: + for attribute in ('id', 'name', 'created_at', 'updated_at'): + self.assertTrue(hasattr(sot, attribute)) + self.assertIsInstance(getattr(sot, attribute), 'str') diff --git a/openstack/tests/unit/shared_file_system/v2/test_proxy.py b/openstack/tests/unit/shared_file_system/v2/test_proxy.py index 9c5c879f3..82eeb3a5e 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_proxy.py +++ b/openstack/tests/unit/shared_file_system/v2/test_proxy.py @@ -17,6 +17,7 @@ from openstack.shared_file_system.v2 import share from openstack.shared_file_system.v2 import share_network from openstack.shared_file_system.v2 import share_snapshot +from openstack.shared_file_system.v2 import share_snapshot_instance from openstack.shared_file_system.v2 import storage_pool from openstack.shared_file_system.v2 import user_message from openstack.tests.unit import test_proxy_base @@ -175,6 +176,41 @@ def test_wait_for_delete(self, mock_wait): mock_wait.assert_called_once_with(self.proxy, mock_resource, 2, 120) +class TestShareSnapshotInstanceResource(test_proxy_base.TestProxyBase): + + def setUp(self): + super(TestShareSnapshotInstanceResource, self).setUp() + self.proxy = _proxy.Proxy(self.session) + + def test_share_snapshot_instances(self): + self.verify_list( + self.proxy.share_snapshot_instances, + share_snapshot_instance.ShareSnapshotInstance) + + def test_share_snapshot_instance_detailed(self): + self.verify_list(self.proxy.share_snapshot_instances, + share_snapshot_instance.ShareSnapshotInstance, + method_kwargs={ + "details": True, + "query": {'snapshot_id': 'fake'} + }, + expected_kwargs={"query": {'snapshot_id': 'fake'}}) + + def test_share_snapshot_instance_not_detailed(self): + self.verify_list(self.proxy.share_snapshot_instances, + share_snapshot_instance.ShareSnapshotInstance, + method_kwargs={ + "details": False, + "query": {'snapshot_id': 'fake'} + }, + expected_kwargs={"query": {'snapshot_id': 'fake'}}) + + def test_share_snapshot_instance_get(self): + self.verify_get( + self.proxy.get_share_snapshot_instance, + share_snapshot_instance.ShareSnapshotInstance) + + class TestShareNetworkResource(test_proxy_base.TestProxyBase): def setUp(self): diff --git a/openstack/tests/unit/shared_file_system/v2/test_share_snapshot_instance.py b/openstack/tests/unit/shared_file_system/v2/test_share_snapshot_instance.py new file mode 100644 index 000000000..7998d0a9f --- /dev/null +++ b/openstack/tests/unit/shared_file_system/v2/test_share_snapshot_instance.py @@ -0,0 +1,50 @@ +# 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.shared_file_system.v2 import share_snapshot_instance +from openstack.tests.unit import base + +EXAMPLE = { + "status": "available", + "share_id": "618599ab-09a1-432d-973a-c102564c7fec", + "share_instance_id": "8edff0cb-e5ce-4bab-aa99-afe02ed6a76a", + "snapshot_id": "d447de19-a6d3-40b3-ae9f-895c86798924", + "progress": "100%", + "created_at": "2021-06-04T00:44:52.000000", + "id": "275516e8-c998-4e78-a41e-7dd3a03e71cd", + "provider_location": "/path/to/fake...", + "updated_at": "2017-06-04T00:44:54.000000" +} + + +class TestShareSnapshotInstances(base.TestCase): + + def test_basic(self): + instances = share_snapshot_instance.ShareSnapshotInstance() + self.assertEqual('snapshot_instance', instances.resource_key) + self.assertEqual('snapshot_instances', instances.resources_key) + self.assertEqual('/snapshot-instances', instances.base_path) + self.assertTrue(instances.allow_list) + + def test_make_share_snapshot_instance(self): + instance = share_snapshot_instance.ShareSnapshotInstance(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], instance.id) + self.assertEqual(EXAMPLE['share_id'], instance.share_id) + self.assertEqual( + EXAMPLE['share_instance_id'], instance.share_instance_id) + self.assertEqual(EXAMPLE['snapshot_id'], instance.snapshot_id) + self.assertEqual(EXAMPLE['status'], instance.status) + self.assertEqual(EXAMPLE['progress'], instance.progress) + self.assertEqual(EXAMPLE['created_at'], instance.created_at) + self.assertEqual(EXAMPLE['updated_at'], instance.updated_at) + self.assertEqual( + EXAMPLE['provider_location'], instance.provider_location) diff --git a/releasenotes/notes/add-share-snapshot-instance-to-shared-file-4d935f12d67bf59d.yaml b/releasenotes/notes/add-share-snapshot-instance-to-shared-file-4d935f12d67bf59d.yaml new file mode 100644 index 000000000..385bc44f9 --- /dev/null +++ b/releasenotes/notes/add-share-snapshot-instance-to-shared-file-4d935f12d67bf59d.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added support to list and get share snapshot instances + on the shared file system service. From 61422fd8b18e9265f9ddf24c159a4f82c8da6a0d Mon Sep 17 00:00:00 2001 From: Nils Magnus Date: Wed, 22 Mar 2023 16:47:36 +0000 Subject: [PATCH 3207/3836] add a new vendor profile for the Swiss Open Telekom Cloud The Swiss Open Telekom Cloud is a technically independent, but very similar community cloud to the Open Telekom Cloud operated by T-Systems. That's why the vendor profile `otc.json` can't be extended for it. Change-Id: I9e2181ba3a9c1c6926aad45233b064ab68dace72 --- openstack/config/vendors/otc-swiss.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 openstack/config/vendors/otc-swiss.json diff --git a/openstack/config/vendors/otc-swiss.json b/openstack/config/vendors/otc-swiss.json new file mode 100644 index 000000000..0b5eef416 --- /dev/null +++ b/openstack/config/vendors/otc-swiss.json @@ -0,0 +1,15 @@ +{ + "name": "otc-swiss", + "profile": { + "auth": { + "auth_url": "iam-pub.eu-ch2.sc.otc.t-systems.com/v3" + }, + "regions": [ + "eu-ch2" + ], + "identity_api_version": "3", + "interface": "public", + "image_format": "qcow2", + "vendor_hook": "otcextensions.sdk:load" + } +} From 76724972cb6a2c6f85c62e144a08f6c6f1c1f7e2 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 24 Mar 2023 11:43:36 +0000 Subject: [PATCH 3208/3836] config: Split 'OS_AUTH_METHODS' As the plural name would suggest, this can contain multiple items. This is easy to do in 'clouds.yaml' but we need to explicitly split the string loaded from the environment variable. Do this. Change-Id: If6cfe2b3152557933f894143faec4c99ed5364f2 Signed-off-by: Stephen Finucane Story: 2010661 Task: 47713 --- openstack/config/loader.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openstack/config/loader.py b/openstack/config/loader.py index c3d04c7a7..a62f7fd95 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -79,6 +79,7 @@ ] BOOL_KEYS = ('insecure', 'cache') +CSV_KEYS = ('auth_methods',) FORMAT_EXCLUSIONS = frozenset(['password']) @@ -1065,6 +1066,11 @@ def magic_fixes(self, config): if type(config[key]) is not bool: config[key] = get_boolean(config[key]) + for key in CSV_KEYS: + if key in config: + if isinstance(config[key], str): + config[key] = config[key].split(',') + # TODO(mordred): Special casing auth_url here. We should # come back to this betterer later so that it's # more generalized From dff6625fe6be423c2ff5aab4394d2bd02a9dcaa5 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 24 Mar 2023 15:38:06 +0000 Subject: [PATCH 3209/3836] config: Load additional options for v3multifactor As the name would suggest, 'v3multifactor' uses multiple factors for authentication. As a result, we need to register the configuration options for each required auth method. If we don't do this, we won't move the required configuration options for auth out of the top level config object into the 'auth' key. This affects users who are configuring via environment variables or config options. Normally registering of configuration options is handled by the 'MultiFactor.load_from_options' [1][2] method but there doesn't appear to be a way to "register" the auth methods without actually loading the plugin. As a result, if we encounter this auth type then we need to do this registration of extra options manually. There's a probably a TODO for keystoneauth to provide a mechanism for this but I don't know what that would look like right now. [1] https://github.com/openstack/keystoneauth/blob/5.1.2/keystoneauth1/loading/_plugins/identity/v3.py#L332-L340 [2] https://github.com/openstack/keystoneauth/blob/5.1.2/keystoneauth1/loading/_plugins/identity/v3.py#L323-L329 Change-Id: I1f02133be373fa1f8facfd016586395fa2379a3e Signed-off-by: Stephen Finucane Story: 2010661 Task: 47714 --- openstack/config/loader.py | 20 ++++++++++++++++++- ...h_type-v3multifactor-049cf52573d9e00e.yaml | 12 +++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/fix-os_auth_type-v3multifactor-049cf52573d9e00e.yaml diff --git a/openstack/config/loader.py b/openstack/config/loader.py index a62f7fd95..dfddce046 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -933,7 +933,25 @@ def _get_auth_loader(self, config): # That it does not exist in keystoneauth is irrelvant- it not # doing what they want causes them sorrow. config['auth_type'] = 'admin_token' - return loading.get_plugin_loader(config['auth_type']) + + loader = loading.get_plugin_loader(config['auth_type']) + + # As the name would suggest, v3multifactor uses multiple factors for + # authentication. As a result, we need to register the configuration + # options for each required auth method. Normally, this is handled by + # the 'MultiFactor.load_from_options' method but there doesn't appear + # to be a way to "register" the auth methods without actually loading + # the plugin. As a result, if we encounter this auth type then we need + # to do this registration of extra options manually. + # FIXME(stephenfin): We need to provide a mechanism to extend the + # options in keystoneauth1.loading._plugins.identity.v3.MultiAuth + # without calling 'load_from_options'. + if config['auth_type'] == 'v3multifactor': + # We use '.get' since we can't be sure this key is set yet - + # validation happens later, in _validate_auth + loader._methods = config.get('auth_methods') + + return loader def _validate_auth(self, config, loader): # May throw a keystoneauth1.exceptions.NoMatchingPlugin diff --git a/releasenotes/notes/fix-os_auth_type-v3multifactor-049cf52573d9e00e.yaml b/releasenotes/notes/fix-os_auth_type-v3multifactor-049cf52573d9e00e.yaml new file mode 100644 index 000000000..73a288cd6 --- /dev/null +++ b/releasenotes/notes/fix-os_auth_type-v3multifactor-049cf52573d9e00e.yaml @@ -0,0 +1,12 @@ +--- +fixes: + - | + It is now possible to configure ``v3multifactor`` auth type using + environment variables. For example: + + export OS_AUTH_TYPE=v3multifactor + export OS_AUTH_METHODS=v3password,v3totp + export OS_USERNAME=admin + export OS_PASSWORD=password + export OS_PASSCODE=12345 + openstack server list From 27e66984bf699cce8b34016595ee156281aa4ede Mon Sep 17 00:00:00 2001 From: Mridula Joshi Date: Mon, 20 Feb 2023 05:12:36 +0000 Subject: [PATCH 3210/3836] Adding support for glance cache-queue Command Change-Id: I5296fe41b541a3dd40669e49e431828ab3c8e107 --- doc/source/user/proxies/image_v2.rst | 3 ++- openstack/image/v2/_proxy.py | 7 ++++++- openstack/image/v2/cache.py | 18 ++++++++++++++++++ openstack/tests/unit/image/v2/test_cache.py | 15 +++++++++++++++ openstack/tests/unit/image/v2/test_proxy.py | 12 ++++++++++++ ...d-image-cache-support-78477e1686c52e56.yaml | 4 ++++ 6 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/add-image-cache-support-78477e1686c52e56.yaml diff --git a/doc/source/user/proxies/image_v2.rst b/doc/source/user/proxies/image_v2.rst index 5327eaa55..c838fd898 100644 --- a/doc/source/user/proxies/image_v2.rst +++ b/doc/source/user/proxies/image_v2.rst @@ -89,4 +89,5 @@ Cache Operations .. autoclass:: openstack.image.v2._proxy.Proxy :noindex: - :members: cache_delete_image + :members: cache_delete_image, queue_image, get_image_cache + diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index cf00639dd..6de411237 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -89,7 +89,12 @@ def cache_delete_image(self, image, ignore_missing=True): the metadef namespace does not exist. :returns: ``None`` """ - self._delete(_cache.Cache, image, ignore_missing=ignore_missing) + return self._delete(_cache.Cache, image, ignore_missing=ignore_missing) + + def queue_image(self, image_id): + """Queue image(s) for caching.""" + cache = self._get_resource(_cache.Cache, None) + return cache.queue(self, image_id) # ====== IMAGES ====== def create_image( diff --git a/openstack/image/v2/cache.py b/openstack/image/v2/cache.py index e63e954d5..e9d59072c 100644 --- a/openstack/image/v2/cache.py +++ b/openstack/image/v2/cache.py @@ -10,7 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import exceptions from openstack import resource +from openstack import utils class CachedImage(resource.Resource): @@ -26,9 +28,25 @@ class Cache(resource.Resource): allow_fetch = True allow_delete = True + allow_create = True _max_microversion = '2.14' cached_images = resource.Body('cached_images', type=list, list_type=CachedImage) queued_images = resource.Body('queued_images', type=list) + + def queue(self, session, image, *, microversion=None): + """Queue an image into cache. + :param session: The session to use for making this request + :param image: The image to be queued into cache. + :returns: The server response + """ + if microversion is None: + microversion = self._get_microversion(session, action='commit') + image_id = resource.Resource._get_id(image) + url = utils.urljoin(self.base_path, image_id) + + response = session.put(url, microversion=microversion) + exceptions.raise_from_response(response) + return response diff --git a/openstack/tests/unit/image/v2/test_cache.py b/openstack/tests/unit/image/v2/test_cache.py index fc1cfc678..6f29dbdf7 100644 --- a/openstack/tests/unit/image/v2/test_cache.py +++ b/openstack/tests/unit/image/v2/test_cache.py @@ -10,6 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from unittest import mock + +from openstack import exceptions from openstack.image.v2 import cache from openstack.tests.unit import base @@ -44,3 +47,15 @@ def test_make_it(self): sot.cached_images, ) self.assertEqual(EXAMPLE['queued_images'], sot.queued_images) + + @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) + def test_queue(self): + sot = cache.Cache() + sess = mock.Mock() + sess.put = mock.Mock() + sess.default_microversion = '2.14' + + sot.queue(sess, image='image_id') + + sess.put.assert_called_with('cache/image_id', + microversion=sess.default_microversion) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 4a98e2632..b32026a0d 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -26,6 +26,7 @@ from openstack.image.v2 import schema as _schema from openstack.image.v2 import service_info as _service_info from openstack.image.v2 import task as _task +from openstack import proxy as proxy_base from openstack.tests.unit.image.v2 import test_image as fake_image from openstack.tests.unit import test_proxy_base @@ -901,3 +902,14 @@ def test_cache_image_delete(self): self.proxy.cache_delete_image, _cache.Cache, ) + + @mock.patch.object(proxy_base.Proxy, '_get_resource') + def test_image_queue(self, mock_get_resource): + fake_cache = _cache.Cache() + mock_get_resource.return_value = fake_cache + self._verify( + "openstack.image.v2.cache.Cache.queue", + self.proxy.queue_image, + method_args=['image-id'], + expected_args=[self.proxy, 'image-id']) + mock_get_resource.assert_called_once_with(_cache.Cache, None) diff --git a/releasenotes/notes/add-image-cache-support-78477e1686c52e56.yaml b/releasenotes/notes/add-image-cache-support-78477e1686c52e56.yaml new file mode 100644 index 000000000..36dc0fb83 --- /dev/null +++ b/releasenotes/notes/add-image-cache-support-78477e1686c52e56.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add support for glance Cache API. From 54b257220f06b3eb215550bcc2f757d62d61d7dd Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 27 Mar 2023 11:10:30 +0100 Subject: [PATCH 3211/3836] Use custom warnings, not logging.warning We were using logging.warning() to warn the user about fields that had been removed in recent API versions or behavior that was now considered deprecated in SDK. This was the wrong API to use. We shouldn't have been logging, we have been using 'warnings'. From the Python docs [1]: Task you want to perform: Issue a warning regarding a particular runtime event warnings.warn() in library code if the issue is avoidable and the client application should be modified to eliminate the warning logging.warning() if there is nothing the client application can do about the situation, but the event should still be noted Based on this, introduce a new module, 'openstack.warnings', containing a number of custom 'DeprecationWarning' subclasses. 'DeprecationWarning' isn't show by default in most cases [2] but users can opt-in to showing them and do so selectively. For example, they may wish to ignore warnings about fields that have been removed in recent API versions while raising errors if they are relying on deprecated SDK behavior. [1] https://docs.python.org/3/howto/logging.html#when-to-use-logging [2] https://docs.python.org/3/library/exceptions.html#DeprecationWarning Change-Id: I3846e8fcffdb5de2afe64365952d90b5ecb0f74a Signed-off-by: Stephen Finucane --- doc/source/user/index.rst | 1 + doc/source/user/warnings.rst | 18 +++++++++ openstack/resource.py | 37 +++++++++---------- openstack/warnings.py | 31 ++++++++++++++++ .../switch-to-warnings-333955d19afc99ca.yaml | 7 ++++ 5 files changed, 75 insertions(+), 19 deletions(-) create mode 100644 doc/source/user/warnings.rst create mode 100644 openstack/warnings.py create mode 100644 releasenotes/notes/switch-to-warnings-333955d19afc99ca.yaml diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index 51a242047..a78322772 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -162,6 +162,7 @@ can be customized. resource service_description utils + warnings Presentations ------------- diff --git a/doc/source/user/warnings.rst b/doc/source/user/warnings.rst new file mode 100644 index 000000000..a0053a3a3 --- /dev/null +++ b/doc/source/user/warnings.rst @@ -0,0 +1,18 @@ +Warnings +======== + +openstacksdk uses the `warnings`__ infrastructure to warn users about +deprecated resources and resource fields, as well as deprecated behavior in +openstacksdk itself. Currently, these warnings are all derived from +``DeprecationWarning``. In Python, deprecation warnings are silenced by +default. You must turn them on using the ``-Wa`` Python command line option or +the ``PYTHONWARNINGS`` environment variable. If you are writing an application +that uses openstacksdk, you may wish to enable some of these warnings during +test runs to ensure you migrate away from deprecated behavior. + +Available warnings +------------------ + +.. automodule:: openstack.warnings + +.. __: https://docs.python.org/3/library/warnings.html diff --git a/openstack/resource.py b/openstack/resource.py index 3f02faddf..f5a205670 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -48,6 +48,7 @@ class that represent a remote resource. The attributes that from openstack import exceptions from openstack import format from openstack import utils +from openstack import warnings as os_warnings _SEEN_FORMAT = '{name}_seen' @@ -196,29 +197,27 @@ def __get__(self, instance, owner): return None # This warning are pretty intruisive. Every time attribute is accessed - # a warning is being thrown. In Neutron clients we have way too many - # places that still refer to tenant_id even they may also properly - # support project_id. For now we can silence tenant_id warnings and do - # this here rather then addining support for something similar to - # "suppress_deprecation_warning". + # a warning is being thrown. In neutron clients we have way too many + # places that still refer to tenant_id even though they may also + # properly support project_id. For now we silence tenant_id warnings. if self.name != "tenant_id": self.warn_if_deprecated_property(value) return _convert_type(value, self.type, self.list_type) def warn_if_deprecated_property(self, value): deprecated = object.__getattribute__(self, 'deprecated') - deprecate_reason = object.__getattribute__(self, 'deprecation_reason') - - if value and deprecated and not self.already_warned_deprecation: - self.already_warned_deprecation = True - if not deprecate_reason: - LOG.warning( - "The option [%s] has been deprecated. " - "Please avoid using it.", + deprecation_reason = object.__getattribute__( + self, 'deprecation_reason', + ) + + if value and deprecated: + warnings.warn( + "The field %r has been deprecated. %s" % ( self.name, - ) - else: - LOG.warning(deprecate_reason) + deprecation_reason or "Avoid usage." + ), + os_warnings.RemovedFieldWarning, + ) return value def __set__(self, instance, value): @@ -676,10 +675,10 @@ def __getitem__(self, name): for attr, component in self._attributes_iterator(tuple([Body])): if component.name == name: warnings.warn( - 'Access to "%s[%s]" is deprecated. ' - 'Please access using "%s.%s" attribute.' + "Access to '%s[%s]' is deprecated. " + "Use '%s.%s' attribute instead" % (self.__class__, name, self.__class__, attr), - DeprecationWarning, + os_warnings.LegacyAPIWarning, ) return getattr(self, attr) if self._allow_unknown_attrs_in_body: diff --git a/openstack/warnings.py b/openstack/warnings.py new file mode 100644 index 000000000..b4be45549 --- /dev/null +++ b/openstack/warnings.py @@ -0,0 +1,31 @@ +# 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. + + +class OpenStackDeprecationWarning(DeprecationWarning): + """Base class for warnings about deprecated features in openstacksdk.""" + + +class RemovedResourceWarning(OpenStackDeprecationWarning): + """Indicates that a resource has been removed in newer API versions and + should not be used. + """ + + +class RemovedFieldWarning(OpenStackDeprecationWarning): + """Indicates that a field has been removed in newer API versions and should + not be used. + """ + + +class LegacyAPIWarning(OpenStackDeprecationWarning): + """Indicates an API that is in 'legacy' status, a long term deprecation.""" diff --git a/releasenotes/notes/switch-to-warnings-333955d19afc99ca.yaml b/releasenotes/notes/switch-to-warnings-333955d19afc99ca.yaml new file mode 100644 index 000000000..a55c71f41 --- /dev/null +++ b/releasenotes/notes/switch-to-warnings-333955d19afc99ca.yaml @@ -0,0 +1,7 @@ +--- +upgrade: + - | + Warnings about deprecated behavior or deprecated/modified APIs are now + raised using the ``warnings`` module, rather than the ``logging`` module. + This allows users to filter these warnings or silence them entirely if + necessary. From 1ca0f0c3f3b6c6b93b2283695059267e98fca662 Mon Sep 17 00:00:00 2001 From: tutkuna Date: Fri, 15 Oct 2021 22:42:29 -0400 Subject: [PATCH 3212/3836] Add share instances to shared file systems Change-Id: I7e1f8bd4218858a8e483a402b3906706840a47a5 Co-Authored-By: Reynaldo Bontje --- doc/source/user/guides/shared_file_system.rst | 37 ++++++ .../user/proxies/shared_file_system.rst | 13 ++ .../resources/shared_file_system/index.rst | 3 +- .../shared_file_system/v2/share_instance.rst | 13 ++ .../shared_file_system/share_instances.py | 41 +++++++ openstack/shared_file_system/v2/_proxy.py | 54 +++++++++ .../shared_file_system/v2/share_instance.py | 83 +++++++++++++ .../shared_file_system/test_share_instance.py | 68 +++++++++++ .../unit/shared_file_system/v2/test_proxy.py | 26 ++++ .../v2/test_share_instance.py | 111 ++++++++++++++++++ ...syste-share_instance-fffaea2d3a77ba24.yaml | 6 + 11 files changed, 454 insertions(+), 1 deletion(-) create mode 100644 doc/source/user/resources/shared_file_system/v2/share_instance.rst create mode 100644 examples/shared_file_system/share_instances.py create mode 100644 openstack/shared_file_system/v2/share_instance.py create mode 100644 openstack/tests/functional/shared_file_system/test_share_instance.py create mode 100644 openstack/tests/unit/shared_file_system/v2/test_share_instance.py create mode 100644 releasenotes/notes/add-shared-file-syste-share_instance-fffaea2d3a77ba24.yaml diff --git a/doc/source/user/guides/shared_file_system.rst b/doc/source/user/guides/shared_file_system.rst index 40ffc3f2a..1aee6a36f 100644 --- a/doc/source/user/guides/shared_file_system.rst +++ b/doc/source/user/guides/shared_file_system.rst @@ -20,3 +20,40 @@ of the share in other availability zones. .. literalinclude:: ../examples/shared_file_system/availability_zones.py :pyobject: list_availability_zones + + +Share Instances +--------------- + +Administrators can list, show information for, explicitly set the state of, +and force-delete share instances. + +.. literalinclude:: ../examples/shared_file_system/share_instances.py + :pyobject: share_instances + + +Get Share Instance +------------------ + +Shows details for a single share instance. + +.. literalinclude:: ../examples/shared_file_system/share_instances.py + :pyobject: get_share_instance + + +Reset Share Instance Status +--------------------------- + +Explicitly updates the state of a share instance. + +.. literalinclude:: ../examples/shared_file_system/share_instances.py + :pyobject: reset_share_instance_status + + +Delete Share Instance +--------------------- + +Force-deletes a share instance. + +.. literalinclude:: ../examples/shared_file_system/share_instances.py + :pyobject: delete_share_instance diff --git a/doc/source/user/proxies/shared_file_system.rst b/doc/source/user/proxies/shared_file_system.rst index e5c5cddd4..6d4a34ac9 100644 --- a/doc/source/user/proxies/shared_file_system.rst +++ b/doc/source/user/proxies/shared_file_system.rst @@ -100,3 +100,16 @@ Create and manipulate Share Networks with the Shared File Systems service. :noindex: :members: share_networks, get_share_network, delete_share_network, update_share_network, create_share_network + +Shared File System Share Instances +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Administrators can list, show information for, explicitly set the +state of, and force-delete share instances within the Shared File +Systems Service. + +.. autoclass:: openstack.shared_file_system.v2._proxy.Proxy + :noindex: + :members: share_instances, get_share_instance, + reset_share_instance_status, + delete_share_instance diff --git a/doc/source/user/resources/shared_file_system/index.rst b/doc/source/user/resources/shared_file_system/index.rst index 3ae1cead5..56d40c533 100644 --- a/doc/source/user/resources/shared_file_system/index.rst +++ b/doc/source/user/resources/shared_file_system/index.rst @@ -8,7 +8,8 @@ Shared File System service resources v2/storage_pool v2/limit v2/share - v2/user_message + v2/share_instance v2/share_snapshot v2/share_snapshot_instance v2/share_network + v2/user_message diff --git a/doc/source/user/resources/shared_file_system/v2/share_instance.rst b/doc/source/user/resources/shared_file_system/v2/share_instance.rst new file mode 100644 index 000000000..0b058335b --- /dev/null +++ b/doc/source/user/resources/shared_file_system/v2/share_instance.rst @@ -0,0 +1,13 @@ +openstack.shared_file_system.v2.share_instance +============================================== + +.. automodule:: openstack.shared_file_system.v2.share_instance + +The ShareInstance Class +----------------------- + +The ``ShareInstance`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.shared_file_system.v2.share_instance.ShareInstance + :members: diff --git a/examples/shared_file_system/share_instances.py b/examples/shared_file_system/share_instances.py new file mode 100644 index 000000000..93a6cbf0d --- /dev/null +++ b/examples/shared_file_system/share_instances.py @@ -0,0 +1,41 @@ +# 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. + +""" +List resources from the Shared File System service. + +For a full guide see +https://docs.openstack.org/openstacksdk/latest/user/guides/shared_file_system.html +""" + + +def share_instances(conn, **query): + print('List all share instances:') + for si in conn.share.share_instances(**query): + print(si) + + +def get_share_instance(conn, share_instance_id): + print('Get share instance with given Id:') + share_instance = conn.share.get_share_instance(share_instance_id) + print(share_instance) + + +def reset_share_instance_status(conn, share_instance_id, status): + print('Reset the status of the share instance with the given ' + 'share_instance_id to the given status') + conn.share.reset_share_instance_status(share_instance_id, status) + + +def delete_share_instance(conn, share_instance_id): + print('Force-delete the share instance with the given share_instance_id') + conn.share.delete_share_instance(share_instance_id) diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index 33307dab5..eae2b0994 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -31,6 +31,7 @@ ) from openstack.shared_file_system.v2 import limit as _limit from openstack.shared_file_system.v2 import share as _share +from openstack.shared_file_system.v2 import share_instance as _share_instance class Proxy(proxy.Proxy): @@ -44,6 +45,7 @@ class Proxy(proxy.Proxy): "share_network": _share_network.ShareNetwork, "share_snapshot_instance": _share_snapshot_instance.ShareSnapshotInstance, + "share_instance": _share_instance.ShareInstance, } def availability_zones(self): @@ -435,3 +437,55 @@ def create_share_network(self, **attrs): share_network.ShareNetwork` """ return self._create(_share_network.ShareNetwork, **attrs) + + def share_instances(self, **query): + """Lists all share instances. + + :param kwargs query: Optional query parameters to be sent to limit + the share instances being returned. Available parameters include: + + * export_location_id: The export location UUID that can be used + to filter share instances. + * export_location_path: The export location path that can be used + to filter share instances. + + :returns: Details of share instances resources + :rtype: :class:`~openstack.shared_file_system.v2. + share_instance.ShareInstance` + """ + return self._list( + _share_instance.ShareInstance, **query) + + def get_share_instance(self, share_instance_id): + """Shows details for a single share instance + + :param share_instance_id: The UUID of the share instance to get + + :returns: Details of the identified share instance + :rtype: :class:`~openstack.shared_file_system.v2. + share_instance.ShareInstance` + """ + return self._get(_share_instance.ShareInstance, share_instance_id) + + def reset_share_instance_status(self, share_instance_id, status): + """Explicitly updates the state of a share instance. + + :param share_instance_id: The UUID of the share instance to reset. + :param status: The share or share instance status to be set. + + :returns: ``None`` + """ + res = self._get_resource(_share_instance.ShareInstance, + share_instance_id) + res.reset_status(self, status) + + def delete_share_instance(self, share_instance_id): + """Force-deletes a share instance + + :param share_instance: The ID of the share instance to delete + + :returns: ``None`` + """ + res = self._get_resource(_share_instance.ShareInstance, + share_instance_id) + res.force_delete(self) diff --git a/openstack/shared_file_system/v2/share_instance.py b/openstack/shared_file_system/v2/share_instance.py new file mode 100644 index 000000000..d2873a18f --- /dev/null +++ b/openstack/shared_file_system/v2/share_instance.py @@ -0,0 +1,83 @@ +# 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 import resource +from openstack import utils + + +class ShareInstance(resource.Resource): + resource_key = "share_instance" + resources_key = "share_instances" + base_path = "/share_instances" + + # capabilities + allow_create = False + allow_fetch = True + allow_commit = False + allow_delete = False + allow_list = True + allow_head = False + + #: Properties + #: The share instance access rules status. A valid value is active, + #: error, or syncing. + access_rules_status = resource.Body("access_rules_status", type=str) + #: The name of the availability zone the share exists within. + availability_zone = resource.Body("availability_zone", type=str) + #: If the share instance has its cast_rules_to_readonly attribute + #: set to True, all existing access rules be cast to read/only. + cast_rules_to_readonly = resource.Body("cast_rules_to_readonly", type=bool) + #: The date and time stamp when the resource was created within the + #: service’s database. + created_at = resource.Body("created_at", type=str) + #: The host name of the service back end that the resource is + #: contained within. + host = resource.Body("host", type=str) + #: The progress of the share creation. + progress = resource.Body("progress", type=str) + #: The share replica state. Has set value only when replication is used. + #: List of possible values: active, in_sync, out_of_sync, error + replica_state = resource.Body("replica_state", type=str) + #: The UUID of the share to which the share instance belongs to. + share_id = resource.Body("share_id", type=str) + #: The share network ID where the resource is exported to. + share_network_id = resource.Body("share_network_id", type=str) + #: The UUID of the share server. + share_server_id = resource.Body("share_server_id", type=str) + #: The share or share instance status. + status = resource.Body("status", type=str) + + def _action(self, session, body, action='patch', microversion=None): + """Perform share instance actions given the message body""" + url = utils.urljoin(self.base_path, self.id, 'action') + headers = {'Accept': ''} + extra_attrs = {} + if microversion: + # Set microversion override + extra_attrs['microversion'] = microversion + else: + extra_attrs['microversion'] = \ + self._get_microversion(session, action=action) + response = session.post(url, json=body, headers=headers, **extra_attrs) + exceptions.raise_from_response(response) + return response + + def reset_status(self, session, reset_status): + """Reset share instance to given status""" + body = {"reset_status": {"status": reset_status}} + self._action(session, body) + + def force_delete(self, session): + """Force delete share instance""" + body = {"force_delete": None} + self._action(session, body, action='delete') diff --git a/openstack/tests/functional/shared_file_system/test_share_instance.py b/openstack/tests/functional/shared_file_system/test_share_instance.py new file mode 100644 index 000000000..86feea3c0 --- /dev/null +++ b/openstack/tests/functional/shared_file_system/test_share_instance.py @@ -0,0 +1,68 @@ +# 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 resource +from openstack.shared_file_system.v2 import share_instance as _share_instance +from openstack.tests.functional.shared_file_system import base + + +class ShareInstanceTest(base.BaseSharedFileSystemTest): + + min_microversion = '2.7' + + def setUp(self): + super(ShareInstanceTest, self).setUp() + + self.SHARE_NAME = self.getUniqueString() + my_share = self.create_share( + name=self.SHARE_NAME, size=2, share_type="dhss_false", + share_protocol='NFS', description=None) + self.SHARE_ID = my_share.id + instances_list = self.operator_cloud.share.share_instances() + self.SHARE_INSTANCE_ID = None + for i in instances_list: + if i.share_id == self.SHARE_ID: + self.SHARE_INSTANCE_ID = i.id + + def test_get(self): + sot = self.operator_cloud.share.get_share_instance( + self.SHARE_INSTANCE_ID) + assert isinstance(sot, _share_instance.ShareInstance) + self.assertEqual(self.SHARE_INSTANCE_ID, sot.id) + + def test_list_share_instances(self): + share_instances = self.operator_cloud.share.share_instances() + self.assertGreater(len(list(share_instances)), 0) + for share_instance in share_instances: + for attribute in ('id', 'name', 'created_at', + 'access_rules_status', + 'availability_zone'): + self.assertTrue(hasattr(share_instance, attribute)) + + def test_reset(self): + res = self.operator_cloud.share.reset_share_instance_status( + self.SHARE_INSTANCE_ID, 'error') + self.assertIsNone(res) + sot = self.operator_cloud.share.get_share_instance( + self.SHARE_INSTANCE_ID) + self.assertEqual('error', sot.status) + + def test_delete(self): + sot = self.operator_cloud.share.get_share_instance( + self.SHARE_INSTANCE_ID) + fdel = self.operator_cloud.share.delete_share_instance( + self.SHARE_INSTANCE_ID) + resource.wait_for_delete(self.operator_cloud.share, sot, + wait=self._wait_for_timeout, + interval=2) + self.assertIsNone(fdel) diff --git a/openstack/tests/unit/shared_file_system/v2/test_proxy.py b/openstack/tests/unit/shared_file_system/v2/test_proxy.py index 82eeb3a5e..364fe5a2e 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_proxy.py +++ b/openstack/tests/unit/shared_file_system/v2/test_proxy.py @@ -15,6 +15,7 @@ from openstack.shared_file_system.v2 import _proxy from openstack.shared_file_system.v2 import limit from openstack.shared_file_system.v2 import share +from openstack.shared_file_system.v2 import share_instance from openstack.shared_file_system.v2 import share_network from openstack.shared_file_system.v2 import share_snapshot from openstack.shared_file_system.v2 import share_snapshot_instance @@ -61,6 +62,31 @@ def test_share_create(self): def test_share_update(self): self.verify_update(self.proxy.update_share, share.Share) + def test_share_instances(self): + self.verify_list(self.proxy.share_instances, + share_instance.ShareInstance) + + def test_share_instance_get(self): + self.verify_get(self.proxy.get_share_instance, + share_instance.ShareInstance) + + def test_share_instance_reset(self): + self._verify( + "openstack.shared_file_system.v2.share_instance." + + "ShareInstance.reset_status", + self.proxy.reset_share_instance_status, + method_args=['id', 'available'], + expected_args=[self.proxy, 'available'], + ) + + def test_share_instance_delete(self): + self._verify( + "openstack.shared_file_system.v2.share_instance." + + "ShareInstance.force_delete", + self.proxy.delete_share_instance, + method_args=['id'], + expected_args=[self.proxy]) + @mock.patch("openstack.resource.wait_for_status") def test_wait_for(self, mock_wait): mock_resource = mock.Mock() diff --git a/openstack/tests/unit/shared_file_system/v2/test_share_instance.py b/openstack/tests/unit/shared_file_system/v2/test_share_instance.py new file mode 100644 index 000000000..338dc09fa --- /dev/null +++ b/openstack/tests/unit/shared_file_system/v2/test_share_instance.py @@ -0,0 +1,111 @@ +# 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 unittest import mock + +from keystoneauth1 import adapter + +from openstack.shared_file_system.v2 import share_instance +from openstack.tests.unit import base + +IDENTIFIER = "75559a8b-c90c-42a7-bda2-edbe86acfb7b" + +EXAMPLE = { + "status": "available", + "progress": "100%", + "share_id": "d94a8548-2079-4be0-b21c-0a887acd31ca", + "availability_zone": "nova", + "replica_state": None, + "created_at": "2015-09-07T08:51:34.000000", + "cast_rules_to_readonly": False, + "share_network_id": "713df749-aac0-4a54-af52-10f6c991e80c", + "share_server_id": "ba11930a-bf1a-4aa7-bae4-a8dfbaa3cc73", + "host": "manila2@generic1#GENERIC1", + "access_rules_status": "active", + "id": IDENTIFIER +} + + +class TestShareInstances(base.TestCase): + + def test_basic(self): + share_instance_resource = share_instance.ShareInstance() + self.assertEqual('share_instances', + share_instance_resource.resources_key) + self.assertEqual('/share_instances', share_instance_resource.base_path) + self.assertTrue(share_instance_resource.allow_list) + self.assertFalse(share_instance_resource.allow_create) + self.assertTrue(share_instance_resource.allow_fetch) + self.assertFalse(share_instance_resource.allow_commit) + self.assertFalse(share_instance_resource.allow_delete) + + def test_make_share_instances(self): + share_instance_resource = share_instance.ShareInstance(**EXAMPLE) + self.assertEqual(EXAMPLE['status'], share_instance_resource.status) + self.assertEqual(EXAMPLE['progress'], share_instance_resource.progress) + self.assertEqual(EXAMPLE['share_id'], share_instance_resource.share_id) + self.assertEqual(EXAMPLE['availability_zone'], + share_instance_resource.availability_zone) + self.assertEqual(EXAMPLE['replica_state'], + share_instance_resource.replica_state) + self.assertEqual(EXAMPLE['created_at'], + share_instance_resource.created_at) + self.assertEqual(EXAMPLE['cast_rules_to_readonly'], + share_instance_resource.cast_rules_to_readonly) + self.assertEqual(EXAMPLE['share_network_id'], + share_instance_resource.share_network_id) + self.assertEqual(EXAMPLE['share_server_id'], + share_instance_resource.share_server_id) + self.assertEqual(EXAMPLE['host'], share_instance_resource.host) + self.assertEqual(EXAMPLE['access_rules_status'], + share_instance_resource.access_rules_status) + self.assertEqual(EXAMPLE['id'], share_instance_resource.id) + + +class TestShareInstanceActions(TestShareInstances): + + def setUp(self): + super(TestShareInstanceActions, self).setUp() + self.resp = mock.Mock() + self.resp.body = None + self.resp.status_code = 200 + self.resp.json = mock.Mock(return_value=self.resp.body) + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.default_microversion = '3.0' + self.sess.post = mock.Mock(return_value=self.resp) + self.sess._get_connection = mock.Mock(return_value=self.cloud) + + def test_reset_status(self): + sot = share_instance.ShareInstance(**EXAMPLE) + microversion = sot._get_microversion(self.sess, action='patch') + + self.assertIsNone(sot.reset_status(self.sess, 'active')) + + url = f'share_instances/{IDENTIFIER}/action' + body = {"reset_status": {"status": 'active'}} + headers = {'Accept': ''} + self.sess.post.assert_called_with( + url, json=body, headers=headers, + microversion=microversion) + + def test_force_delete(self): + sot = share_instance.ShareInstance(**EXAMPLE) + microversion = sot._get_microversion(self.sess, action='delete') + + self.assertIsNone(sot.force_delete(self.sess)) + + url = f'share_instances/{IDENTIFIER}/action' + body = {'force_delete': None} + headers = {'Accept': ''} + self.sess.post.assert_called_with( + url, json=body, headers=headers, + microversion=microversion) diff --git a/releasenotes/notes/add-shared-file-syste-share_instance-fffaea2d3a77ba24.yaml b/releasenotes/notes/add-shared-file-syste-share_instance-fffaea2d3a77ba24.yaml new file mode 100644 index 000000000..f14a7a4cf --- /dev/null +++ b/releasenotes/notes/add-shared-file-syste-share_instance-fffaea2d3a77ba24.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Added support to list, get, reset status of, + and force delete share instances + (from shared file system service). From 4e3f50c16daee16a2d01d41c6f34c69f053f7c12 Mon Sep 17 00:00:00 2001 From: Romain Dupont Date: Tue, 28 Mar 2023 09:43:28 +0000 Subject: [PATCH 3213/3836] modify ovh and ovh-us vendor config Change-Id: I76853895ed4d2658b9e1420e305584ff19960f23 --- openstack/config/vendors/ovh-us.json | 5 ++++- openstack/config/vendors/ovh.json | 31 ++-------------------------- 2 files changed, 6 insertions(+), 30 deletions(-) diff --git a/openstack/config/vendors/ovh-us.json b/openstack/config/vendors/ovh-us.json index d388d7d91..4bbb95661 100644 --- a/openstack/config/vendors/ovh-us.json +++ b/openstack/config/vendors/ovh-us.json @@ -7,7 +7,10 @@ "project_domain_name": "Default" }, "regions": [ - "US-EAST-VA-1" + "US-EAST-VA-1", + "US-WEST-OR-1", + "US-EAST-VA", + "US-WEST-OR" ], "identity_api_version": "3", "floating_ip_source": "None" diff --git a/openstack/config/vendors/ovh.json b/openstack/config/vendors/ovh.json index 09e675836..f65f9c67f 100644 --- a/openstack/config/vendors/ovh.json +++ b/openstack/config/vendors/ovh.json @@ -1,33 +1,6 @@ { "name": "ovh", "profile": { - "auth": { - "auth_url": "https://auth.cloud.ovh.net/", - "user_domain_name": "Default", - "project_domain_name": "Default" - }, - "regions": [ - "BHS", - "BHS1", - "BHS3", - "BHS5", - "DE", - "DE1", - "GRA", - "GRA1", - "GRA5", - "GRA7", - "SBG", - "SBG1", - "SBG5", - "UK", - "UK1", - "WAW", - "WAW1", - "SYD1", - "SGP1" - ], - "identity_api_version": "3", - "floating_ip_source": "None" + "profile": "https://ovhcloud.com" } -} +} \ No newline at end of file From 50711b4662a99230d1ed349f772c72cfa3433581 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 28 Mar 2023 15:49:22 +0200 Subject: [PATCH 3214/3836] Prepare acceptance tests for real clouds - reworked job not to use custom modules, but rather send direct API requests (due to mess with ensuring openstacksdk availability for Ansible). - add exclude for pre-commit-config to stop it from complaining on zuul.yaml - removed too verbose logging from functests with list of images and flavors (on real clouds this is simply too much). Change-Id: I555127f410b696e1584dc07cafac25597ab1abeb --- .pre-commit-config.yaml | 1 + .zuul.yaml | 61 +++++++++++---- openstack/tests/functional/base.py | 4 +- playbooks/acceptance/library | 1 - playbooks/acceptance/post.yaml | 48 +++++++++--- playbooks/acceptance/pre.yaml | 79 +++++++++++--------- playbooks/acceptance/run-with-devstack.yaml | 70 +---------------- playbooks/library/os_auth.py | 45 ----------- roles/revoke_token/README.rst | 0 roles/revoke_token/library/os_auth_revoke.py | 72 ------------------ roles/revoke_token/tasks/main.yaml | 7 -- 11 files changed, 134 insertions(+), 254 deletions(-) delete mode 120000 playbooks/acceptance/library delete mode 100644 playbooks/library/os_auth.py delete mode 100644 roles/revoke_token/README.rst delete mode 100644 roles/revoke_token/library/os_auth_revoke.py delete mode 100644 roles/revoke_token/tasks/main.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c01a5d710..5814ebb93 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,6 +16,7 @@ repos: - id: debug-statements - id: check-yaml files: .*\.(yaml|yml)$ + exclude: '^.zuul.yaml' - repo: local hooks: - id: flake8 diff --git a/.zuul.yaml b/.zuul.yaml index 5244a4df7..4db226942 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -439,16 +439,43 @@ - job: name: openstacksdk-acceptance-base parent: openstack-tox - description: Acceptance test of the OpenStackSDK on real clouds + description: | + Acceptance test of the OpenStackSDK on real clouds. + + .. zuul:jobsvar::openstack_credentials + :type: dict + + This is expected to be a Zuul Secret with these keys: + + .. zuul:jobvar: auth + :type: dict + + Dictionary with authentication information with mandatory auth_url + and others. The structure mimics `clouds.yaml` structure. + + By default all jobs that inherit from here are non voting. + + attempts: 1 + voting: false pre-run: - playbooks/acceptance/pre.yaml post-run: - playbooks/acceptance/post.yaml + vars: + tox_envlist: acceptance-regular-user + tox_environment: + OPENSTACKSDK_DEMO_CLOUD: acceptance + OS_CLOUD: acceptance + OS_TEST_CLOUD: acceptance +# Acceptance tests for devstack are different from running for real cloud since +# we need to actually deploy devstack first and API is available only on the +# devstack host. - job: name: openstacksdk-acceptance-devstack parent: openstacksdk-functional-devstack - description: Acceptance test of the OpenStackSDK on real clouds + description: Acceptance test of the OpenStackSDK on real clouds. + attempts: 1 run: - playbooks/acceptance/run-with-devstack.yaml post-run: @@ -459,17 +486,25 @@ OPENSTACKSDK_DEMO_CLOUD: acceptance OS_CLOUD: acceptance OS_TEST_CLOUD: acceptance - openstack_credentials: - auth: - auth_url: "https://{{ hostvars['controller']['nodepool']['private_ipv4'] }}/identity" - username: demo - password: secretadmin - project_domain_id: default - project_name: demo - user_domain_id: default - identity_api_version: '3' - region_name: RegionOne - volume_api_version: '3' + auth_url: "https://{{ hostvars['controller']['nodepool']['private_ipv4'] }}/identity" + secrets: + - secret: credentials-devstack + name: openstack_credentials + +# Devstack secret is not specifying auth_url because of how Zuul treats secrets. +# Auth_url comes extra in the job vars and is being used if no auth_url in the +# secret is present. +- secret: + name: credentials-devstack + data: + auth: + username: demo + password: secretadmin + project_domain_id: default + project_name: demo + user_domain_id: default + region_name: RegionOne + verify: false - project-template: name: openstacksdk-functional-tips diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index 381aece28..42cf40b99 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -106,7 +106,7 @@ def _pick_flavor(self): return None flavors = self.user_cloud.list_flavors(get_extra=False) - self.add_info_on_exception('flavors', flavors) + # self.add_info_on_exception('flavors', flavors) flavor_name = os.environ.get('OPENSTACKSDK_FLAVOR') @@ -146,7 +146,7 @@ def _pick_image(self): return None images = self.user_cloud.list_images() - self.add_info_on_exception('images', images) + # self.add_info_on_exception('images', images) image_name = os.environ.get('OPENSTACKSDK_IMAGE') diff --git a/playbooks/acceptance/library b/playbooks/acceptance/library deleted file mode 120000 index 53bed9684..000000000 --- a/playbooks/acceptance/library +++ /dev/null @@ -1 +0,0 @@ -../library \ No newline at end of file diff --git a/playbooks/acceptance/post.yaml b/playbooks/acceptance/post.yaml index 4e3e00e82..32d0f80ad 100644 --- a/playbooks/acceptance/post.yaml +++ b/playbooks/acceptance/post.yaml @@ -1,18 +1,42 @@ -- hosts: localhost +--- +# This could be running on localhost only, but then the devstack job would need +# to perform API call on the worker node. To keep the code a bit less crazy +# rather address all hosts and perform certain steps on the localhost (zuul +# executor). +- hosts: all tasks: # TODO: # - clean the resources, which might have been created - # - revoke the temp token explicitly - - name: read token - command: "cat {{ zuul.executor.work_root }}/.{{ zuul.build }}" - register: token_data + + # Token is saved on the zuul executor node + - name: Check token file + delegate_to: localhost + ansible.builtin.stat: + path: "{{ zuul.executor.work_root }}/.{{ zuul.build }}" + register: token_file + + # no_log is important since content WILL in logs + - name: Read the token from file + delegate_to: localhost no_log: true + ansible.builtin.slurp: + src: "{{ token_file.stat.path }}" + register: token_data + when: "token_file.stat.exists" - - name: delete data file - command: "shred {{ zuul.executor.work_root }}/.{{ zuul.build }}" + - name: Delete data file + delegate_to: localhost + command: "shred {{ token_file.stat.path }}" + when: "token_file.stat.exists" - - include_role: - name: revoke_token - vars: - cloud: "{{ openstack_credentials }}" - token: "{{ token_data.stdout }}" + # no_log is important since content WILL appear in logs + - name: Revoke token + no_log: true + ansible.builtin.uri: + url: "{{ openstack_credentials.auth.auth_url | default(auth_url) }}/v3/auth/tokens" + method: "DELETE" + headers: + X-Auth-Token: "{{ token_data['content'] | b64decode }}" + X-Subject-Token: "{{ token_data['content'] | b64decode }}" + status_code: 204 + when: "token_file.stat.exists and 'content' in token_data" diff --git a/playbooks/acceptance/pre.yaml b/playbooks/acceptance/pre.yaml index 078fd2040..091c9a32e 100644 --- a/playbooks/acceptance/pre.yaml +++ b/playbooks/acceptance/pre.yaml @@ -1,40 +1,45 @@ +--- - hosts: all tasks: - name: Get temporary token for the cloud - # nolog is important to keep job-output.json clean + # nolog is important since content WILL appear in logs no_log: true - os_auth: - cloud: - profile: "{{ openstack_credentials.profile | default(omit) }}" + ansible.builtin.uri: + url: "{{ openstack_credentials.auth.auth_url | default(auth_url) }}/v3/auth/tokens" + method: "POST" + body_format: "json" + body: auth: - auth_url: "{{ openstack_credentials.auth.auth_url }}" - username: "{{ openstack_credentials.auth.username }}" - password: "{{ openstack_credentials.auth.password }}" - user_domain_name: "{{ openstack_credentials.auth.user_domain_name | default(omit) }}" - user_domain_id: "{{ openstack_credentials.auth.user_domain_id | default(omit) }}" - domain_name: "{{ openstack_credentials.auth.domain_name | default(omit) }}" - domain_id: "{{ openstack_credentials.auth.domain_id | default(omit) }}" - project_name: "{{ openstack_credentials.auth.project_name | default(omit) }}" - project_id: "{{ openstack_credentials.auth.project_id | default(omit) }}" - project_domain_name: "{{ openstack_credentials.auth.project_domain_name | default(omit) }}" - project_domain_id: "{{ openstack_credentials.auth.project_domain_id | default(omit) }}" + identity: + methods: ["password"] + password: + user: + name: "{{ openstack_credentials.auth.username | default(omit) }}" + id: "{{ openstack_credentials.auth.user_id | default(omit) }}" + password: "{{ openstack_credentials.auth.password }}" + domain: + name: "{{ openstack_credentials.auth.user_domain_name | default(omit) }}" + id: "{{ openstack_credentials.auth.user_domain_id | default(omit) }}" + scope: + project: + name: "{{ openstack_credentials.auth.project_name | default(omit) }}" + id: "{{ openstack_credentials.auth.project_id | default(omit) }}" + domain: + name: "{{ openstack_credentials.auth.project_domain_name | default(omit) }}" + id: "{{ openstack_credentials.auth.project_domain_id | default(omit) }}" + return_content: true + status_code: 201 register: os_auth - delegate_to: localhost - name: Verify token + # nolog is important since content WILL appear in logs no_log: true - os_auth: - cloud: - profile: "{{ openstack_credentials.profile | default(omit) }}" - auth_type: token - auth: - auth_url: "{{ openstack_credentials.auth.auth_url }}" - token: "{{ os_auth.auth_token }}" - project_name: "{{ openstack_credentials.auth.project_name | default(omit) }}" - project_id: "{{ openstack_credentials.auth.project_id | default(omit) }}" - project_domain_id: "{{ openstack_credentials.auth.project_domain_id | default(omit) }}" - project_domain_name: "{{ openstack_credentials.auth.project_domain_name | default(omit) }}" - delegate_to: localhost + ansible.builtin.uri: + url: "{{ openstack_credentials.auth.auth_url | default(auth_url) }}/v3/auth/tokens" + method: "GET" + headers: + X-Auth-Token: "{{ os_auth.x_subject_token }}" + X-Subject-Token: "{{ os_auth.x_subject_token }}" - name: Include deploy-clouds-config role include_role: @@ -43,18 +48,22 @@ cloud_config: clouds: acceptance: - profile: "{{ openstack_credentials.profile | default(omit) }}" + profile: "{{ openstack_credentials.profile | default('') }}" auth_type: "token" auth: - auth_url: "{{ openstack_credentials.auth.auth_url | default(omit) }}" - project_name: "{{ openstack_credentials.auth.project_name | default(omit) }}" - token: "{{ os_auth.auth_token }}" + auth_url: "{{ openstack_credentials.auth.auth_url | default(auth_url) }}" + project_name: "{{ openstack_credentials.auth.project_name | default('') }}" + project_domain_id: "{{ openstack_credentials.auth.project_domain_id | default('') }}" + project_domain_name: "{{ openstack_credentials.auth.project_domain_name | default('') }}" + token: "{{ os_auth.x_subject_token }}" + region_name: "{{ openstack_credentials.region_name | default('') }}" + verify: "{{ openstack_credentials.verify | default(true) }}" # Intruders might want to corrupt clouds.yaml to avoid revoking token in the post phase # To prevent this we save token on the executor for later use. - - name: Save token + - name: Save the token delegate_to: localhost copy: dest: "{{ zuul.executor.work_root }}/.{{ zuul.build }}" - content: "{{ os_auth.auth_token }}" - mode: "0440" + content: "{{ os_auth.x_subject_token }}" + mode: "0640" diff --git a/playbooks/acceptance/run-with-devstack.yaml b/playbooks/acceptance/run-with-devstack.yaml index a8e8b3522..26dcdf68e 100644 --- a/playbooks/acceptance/run-with-devstack.yaml +++ b/playbooks/acceptance/run-with-devstack.yaml @@ -1,75 +1,11 @@ +--- # Need to actually start devstack first - hosts: all roles: - run-devstack -# Prepare local clouds.yaml -# We can't rely on pre.yaml, since it is specifically delegates to -# localhost, while on devstack it will not work unless APIs are available -# over the net. -- hosts: all - tasks: - - name: Get temporary token for the cloud - # nolog is important to keep job-output.json clean - no_log: true - os_auth: - cloud: - profile: "{{ openstack_credentials.profile | default(omit) }}" - auth: - auth_url: "{{ openstack_credentials.auth.auth_url }}" - username: "{{ openstack_credentials.auth.username }}" - password: "{{ openstack_credentials.auth.password }}" - user_domain_name: "{{ openstack_credentials.auth.user_domain_name | default(omit) }}" - user_domain_id: "{{ openstack_credentials.auth.user_domain_id | default(omit) }}" - domain_name: "{{ openstack_credentials.auth.domain_name | default(omit) }}" - domain_id: "{{ openstack_credentials.auth.domain_id | default(omit) }}" - project_name: "{{ openstack_credentials.auth.project_name | default(omit) }}" - project_id: "{{ openstack_credentials.auth.project_id | default(omit) }}" - project_domain_name: "{{ openstack_credentials.auth.project_domain_name | default(omit) }}" - project_domain_id: "{{ openstack_credentials.auth.project_domain_id | default(omit) }}" - register: os_auth - - - name: Verify token - # nolog is important to keep job-output.json clean - no_log: true - os_auth: - cloud: - profile: "{{ openstack_credentials.profile | default(omit) }}" - auth_type: token - auth: - auth_url: "{{ openstack_credentials.auth.auth_url }}" - token: "{{ os_auth.auth_token }}" - project_name: "{{ openstack_credentials.auth.project_name | default(omit) }}" - project_id: "{{ openstack_credentials.auth.project_id | default(omit) }}" - project_domain_name: "{{ openstack_credentials.auth.project_domain_name | default(omit) }}" - project_domain_id: "{{ openstack_credentials.auth.project_domain_id | default(omit) }}" - - - name: Include deploy-clouds-config role - include_role: - name: deploy-clouds-config - vars: - cloud_config: - clouds: - acceptance: - profile: "{{ openstack_credentials.profile | default(omit) }}" - auth_type: "token" - auth: - - auth_url: "{{ openstack_credentials.auth.auth_url }}" - project_name: "{{ openstack_credentials.auth.project_name | default(omit) }}" - project_domain_id: "{{ openstack_credentials.auth.project_domain_id | default(omit) }}" - token: "{{ os_auth.auth_token }}" - verify: false - - # Intruders might want to corrupt clouds.yaml to avoid revoking token in - # the post phase. To prevent this we save token on the executor for later - # use. - - name: Save token - delegate_to: localhost - copy: - dest: "{{ zuul.executor.work_root }}/.{{ zuul.build }}" - content: "{{ os_auth.auth_token }}" - mode: "0640" +- name: Get the token + ansible.builtin.import_playbook: pre.yaml # Run the rest - hosts: all diff --git a/playbooks/library/os_auth.py b/playbooks/library/os_auth.py deleted file mode 100644 index 48903a085..000000000 --- a/playbooks/library/os_auth.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python3 -# -# 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. - -""" -Utility to get Keystone token -""" -from ansible.module_utils.basic import AnsibleModule - -import openstack - - -def get_cloud(cloud): - if isinstance(cloud, dict): - config = openstack.config.loader.OpenStackConfig().get_one(**cloud) - return openstack.connection.Connection(config=config) - else: - return openstack.connect(cloud=cloud) - - -def main(): - module = AnsibleModule( - argument_spec=dict( - cloud=dict(required=True, type='raw', no_log=True), - ) - ) - cloud = get_cloud(module.params.get('cloud')) - module.exit_json( - changed=True, - auth_token=cloud.auth_token - ) - - -if __name__ == '__main__': - main() diff --git a/roles/revoke_token/README.rst b/roles/revoke_token/README.rst deleted file mode 100644 index e69de29bb..000000000 diff --git a/roles/revoke_token/library/os_auth_revoke.py b/roles/revoke_token/library/os_auth_revoke.py deleted file mode 100644 index 85a16a462..000000000 --- a/roles/revoke_token/library/os_auth_revoke.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright 2014 Rackspace Australia -# Copyright 2018 Red Hat, 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. - -""" -Utility to revoke Keystone token -""" - -import logging -import traceback - -from ansible.module_utils.basic import AnsibleModule -import keystoneauth1.exceptions -import requests -import requests.exceptions - -import openstack - - -def get_cloud(cloud): - if isinstance(cloud, dict): - config = openstack.config.loader.OpenStackConfig().get_one(**cloud) - return openstack.connection.Connection(config=config) - else: - return openstack.connect(cloud=cloud) - - -def main(): - module = AnsibleModule( - argument_spec=dict( - cloud=dict(required=True, type='raw', no_log=True), - revoke_token=dict(required=True, type='str', no_log=True) - ) - ) - - p = module.params - cloud = get_cloud(p.get('cloud')) - try: - cloud.identity.delete( - '/auth/tokens', - headers={ - 'X-Subject-Token': p.get('revoke_token') - } - ) - except (keystoneauth1.exceptions.http.HttpError, - requests.exceptions.RequestException): - s = "Error performing token revoke" - logging.exception(s) - s += "\n" + traceback.format_exc() - module.fail_json( - changed=False, - msg=s, - cloud=cloud.name, - region_name=cloud.config.region_name) - module.exit_json(changed=True) - - -if __name__ == '__main__': - main() diff --git a/roles/revoke_token/tasks/main.yaml b/roles/revoke_token/tasks/main.yaml deleted file mode 100644 index d7730eb08..000000000 --- a/roles/revoke_token/tasks/main.yaml +++ /dev/null @@ -1,7 +0,0 @@ -- name: Revoke token - delegate_to: localhost - no_log: true - os_auth_revoke: - cloud: "{{ cloud }}" - revoke_token: "{{ token }}" - failed_when: false From b0d98dbba1813da72a117409ae4d0a94a8fd4985 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 31 Mar 2023 09:59:52 +0200 Subject: [PATCH 3215/3836] Rework zuul config Current zuul.yaml file is simply way to big to be able to navigate in it easily. We are going to add some more acceptance jobs which bring quite big secrets and this will make it even less maintainable. Split .zuul.yaml into few zuul.d/* entries using Zuul capability to have few "project" entries and get them merged. Change-Id: I9c6a85d4d6f1cdfcb5a339bca4293d54d5cf8746 --- zuul.d/acceptance-jobs.yaml | 77 ++++++++++ .zuul.yaml => zuul.d/functional-jobs.yaml | 170 +--------------------- zuul.d/metal-jobs.yaml | 32 ++++ zuul.d/project.yaml | 48 ++++++ 4 files changed, 159 insertions(+), 168 deletions(-) create mode 100644 zuul.d/acceptance-jobs.yaml rename .zuul.yaml => zuul.d/functional-jobs.yaml (72%) create mode 100644 zuul.d/metal-jobs.yaml create mode 100644 zuul.d/project.yaml diff --git a/zuul.d/acceptance-jobs.yaml b/zuul.d/acceptance-jobs.yaml new file mode 100644 index 000000000..00a298da4 --- /dev/null +++ b/zuul.d/acceptance-jobs.yaml @@ -0,0 +1,77 @@ +--- +- job: + name: openstacksdk-acceptance-base + parent: openstack-tox + description: | + Acceptance test of the OpenStackSDK on real clouds. + + .. zuul:jobsvar::openstack_credentials + :type: dict + + This is expected to be a Zuul Secret with these keys: + + .. zuul:jobvar: auth + :type: dict + + Dictionary with authentication information with mandatory auth_url + and others. The structure mimics `clouds.yaml` structure. + + By default all jobs that inherit from here are non voting. + + attempts: 1 + voting: false + pre-run: + - playbooks/acceptance/pre.yaml + post-run: + - playbooks/acceptance/post.yaml + vars: + tox_envlist: acceptance-regular-user + tox_environment: + OPENSTACKSDK_DEMO_CLOUD: acceptance + OS_CLOUD: acceptance + OS_TEST_CLOUD: acceptance + +# Acceptance tests for devstack are different from running for real cloud since +# we need to actually deploy devstack first and API is available only on the +# devstack host. +- job: + name: openstacksdk-acceptance-devstack + parent: openstacksdk-functional-devstack + description: Acceptance test of the OpenStackSDK on real clouds. + attempts: 1 + run: + - playbooks/acceptance/run-with-devstack.yaml + post-run: + - playbooks/acceptance/post.yaml + vars: + tox_envlist: acceptance-regular-user + tox_environment: + OPENSTACKSDK_DEMO_CLOUD: acceptance + OS_CLOUD: acceptance + OS_TEST_CLOUD: acceptance + auth_url: "https://{{ hostvars['controller']['nodepool']['private_ipv4'] }}/identity" + secrets: + - secret: credentials-devstack + name: openstack_credentials + +# Devstack secret is not specifying auth_url because of how Zuul treats secrets. +# Auth_url comes extra in the job vars and is being used if no auth_url in the +# secret is present. +- secret: + name: credentials-devstack + data: + auth: + username: demo + password: secretadmin + project_domain_id: default + project_name: demo + user_domain_id: default + region_name: RegionOne + verify: false + +# We define additional project entity not to handle acceptance jobs in +# already complex enough general project entity. +- project: + post-review: + jobs: + - openstacksdk-acceptance-devstack diff --git a/.zuul.yaml b/zuul.d/functional-jobs.yaml similarity index 72% rename from .zuul.yaml rename to zuul.d/functional-jobs.yaml index 4db226942..6e825b641 100644 --- a/.zuul.yaml +++ b/zuul.d/functional-jobs.yaml @@ -1,27 +1,5 @@ -- job: - name: openstacksdk-tox-py38-tips - parent: openstack-tox-py38 - description: | - Run tox python 38 unittests against master of important libs - vars: - tox_install_siblings: true - zuul_work_dir: src/opendev.org/openstack/openstacksdk - # openstacksdk in required-projects so that osc and keystoneauth - # can add the job as well - required-projects: - - openstack/keystoneauth - - openstack/openstacksdk - - openstack/os-client-config - -- project-template: - name: openstacksdk-tox-tips - check: - jobs: - - openstacksdk-tox-py38-tips - gate: - jobs: - - openstacksdk-tox-py38-tips - +--- +# Definitions of functional jobs - job: name: openstacksdk-functional-devstack-minimum parent: devstack-tox-functional @@ -417,95 +395,6 @@ OPENSTACKSDK_HAS_MANILA: 1 OPENSTACKSDK_TESTS_SUBDIR: shared_file_system -- job: - name: metalsmith-integration-openstacksdk-src - parent: metalsmith-integration-glance-netboot-cirros-direct - required-projects: - - openstack/openstacksdk - -- job: - name: bifrost-integration-openstacksdk-src - parent: bifrost-integration-tinyipa-ubuntu-focal - required-projects: - - openstack/ansible-collections-openstack - - openstack/openstacksdk - -- job: - name: ironic-inspector-tempest-openstacksdk-src - parent: ironic-inspector-tempest - required-projects: - - openstack/openstacksdk - -- job: - name: openstacksdk-acceptance-base - parent: openstack-tox - description: | - Acceptance test of the OpenStackSDK on real clouds. - - .. zuul:jobsvar::openstack_credentials - :type: dict - - This is expected to be a Zuul Secret with these keys: - - .. zuul:jobvar: auth - :type: dict - - Dictionary with authentication information with mandatory auth_url - and others. The structure mimics `clouds.yaml` structure. - - By default all jobs that inherit from here are non voting. - - attempts: 1 - voting: false - pre-run: - - playbooks/acceptance/pre.yaml - post-run: - - playbooks/acceptance/post.yaml - vars: - tox_envlist: acceptance-regular-user - tox_environment: - OPENSTACKSDK_DEMO_CLOUD: acceptance - OS_CLOUD: acceptance - OS_TEST_CLOUD: acceptance - -# Acceptance tests for devstack are different from running for real cloud since -# we need to actually deploy devstack first and API is available only on the -# devstack host. -- job: - name: openstacksdk-acceptance-devstack - parent: openstacksdk-functional-devstack - description: Acceptance test of the OpenStackSDK on real clouds. - attempts: 1 - run: - - playbooks/acceptance/run-with-devstack.yaml - post-run: - - playbooks/acceptance/post.yaml - vars: - tox_envlist: acceptance-regular-user - tox_environment: - OPENSTACKSDK_DEMO_CLOUD: acceptance - OS_CLOUD: acceptance - OS_TEST_CLOUD: acceptance - auth_url: "https://{{ hostvars['controller']['nodepool']['private_ipv4'] }}/identity" - secrets: - - secret: credentials-devstack - name: openstack_credentials - -# Devstack secret is not specifying auth_url because of how Zuul treats secrets. -# Auth_url comes extra in the job vars and is being used if no auth_url in the -# secret is present. -- secret: - name: credentials-devstack - data: - auth: - username: demo - password: secretadmin - project_domain_id: default - project_name: demo - user_domain_id: default - region_name: RegionOne - verify: false - - project-template: name: openstacksdk-functional-tips check: @@ -514,58 +403,3 @@ gate: jobs: - openstacksdk-functional-devstack-tips - -- project: - templates: - - check-requirements - - openstack-python3-antelope-jobs - - openstacksdk-functional-tips - - openstacksdk-tox-tips - - os-client-config-tox-tips - - osc-tox-unit-tips - - publish-openstack-docs-pti - - release-notes-jobs-python3 - check: - jobs: - - opendev-buildset-registry - - nodepool-build-image-siblings: - voting: false - - dib-nodepool-functional-openstack-centos-8-stream-src: - voting: false - - openstacksdk-functional-devstack - - openstacksdk-functional-devstack-networking - - openstacksdk-functional-devstack-senlin - - openstacksdk-functional-devstack-magnum: - voting: false - - openstacksdk-functional-devstack-manila: - voting: false - - openstacksdk-functional-devstack-masakari: - voting: false - - openstacksdk-functional-devstack-ironic: - voting: false - - openstacksdk-functional-devstack-legacy: - voting: false - - osc-functional-devstack-tips: - voting: false - # Ironic jobs, non-voting to avoid tight coupling - - ironic-inspector-tempest-openstacksdk-src: - voting: false - - bifrost-integration-openstacksdk-src: - voting: false - - metalsmith-integration-openstacksdk-src: - voting: false - - ansible-collections-openstack-functional-devstack: - voting: false - post-review: - jobs: - - openstacksdk-acceptance-devstack - gate: - jobs: - - opendev-buildset-registry - - nodepool-build-image-siblings: - voting: false - - dib-nodepool-functional-openstack-centos-8-stream-src: - voting: false - - openstacksdk-functional-devstack - - openstacksdk-functional-devstack-networking - - openstacksdk-functional-devstack-senlin diff --git a/zuul.d/metal-jobs.yaml b/zuul.d/metal-jobs.yaml new file mode 100644 index 000000000..6d9291575 --- /dev/null +++ b/zuul.d/metal-jobs.yaml @@ -0,0 +1,32 @@ +--- +# Definitions of Ironic based jobs with a dedicated project entry to keep them +# out of general entry. +- job: + name: metalsmith-integration-openstacksdk-src + parent: metalsmith-integration-glance-netboot-cirros-direct + required-projects: + - openstack/openstacksdk + +- job: + name: bifrost-integration-openstacksdk-src + parent: bifrost-integration-tinyipa-ubuntu-focal + required-projects: + - openstack/ansible-collections-openstack + - openstack/openstacksdk + +- job: + name: ironic-inspector-tempest-openstacksdk-src + parent: ironic-inspector-tempest + required-projects: + - openstack/openstacksdk + +- project: + check: + jobs: + # Ironic jobs, non-voting to avoid tight coupling + - ironic-inspector-tempest-openstacksdk-src: + voting: false + - bifrost-integration-openstacksdk-src: + voting: false + - metalsmith-integration-openstacksdk-src: + voting: false diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml new file mode 100644 index 000000000..ae88fa224 --- /dev/null +++ b/zuul.d/project.yaml @@ -0,0 +1,48 @@ +--- +# Central project entity. It pulls general templates and basic jobs. +# functional-jobs, metal-jobs and acceptance-jobs are being +# merged with this entity into singe one. +- project: + templates: + - check-requirements + - openstack-python3-antelope-jobs + - openstacksdk-functional-tips + - openstacksdk-tox-tips + - os-client-config-tox-tips + - osc-tox-unit-tips + - publish-openstack-docs-pti + - release-notes-jobs-python3 + check: + jobs: + - opendev-buildset-registry + - nodepool-build-image-siblings: + voting: false + - dib-nodepool-functional-openstack-centos-8-stream-src: + voting: false + - openstacksdk-functional-devstack + - openstacksdk-functional-devstack-networking + - openstacksdk-functional-devstack-senlin + - openstacksdk-functional-devstack-magnum: + voting: false + - openstacksdk-functional-devstack-manila: + voting: false + - openstacksdk-functional-devstack-masakari: + voting: false + - openstacksdk-functional-devstack-ironic: + voting: false + - openstacksdk-functional-devstack-legacy: + voting: false + - osc-functional-devstack-tips: + voting: false + - ansible-collections-openstack-functional-devstack: + voting: false + gate: + jobs: + - opendev-buildset-registry + - nodepool-build-image-siblings: + voting: false + - dib-nodepool-functional-openstack-centos-8-stream-src: + voting: false + - openstacksdk-functional-devstack + - openstacksdk-functional-devstack-networking + - openstacksdk-functional-devstack-senlin From a2f3b54f8a8a36b8cfd8be319d8cbc6cd26919ac Mon Sep 17 00:00:00 2001 From: "Dr. Jens Harbott" Date: Fri, 31 Mar 2023 18:32:20 +0200 Subject: [PATCH 3216/3836] Drop legacy job that is always failing Change-Id: Id576dc2676ac175635fbc85dda665c7346785179 --- zuul.d/functional-jobs.yaml | 9 --------- zuul.d/project.yaml | 2 -- 2 files changed, 11 deletions(-) diff --git a/zuul.d/functional-jobs.yaml b/zuul.d/functional-jobs.yaml index 6e825b641..794711ccf 100644 --- a/zuul.d/functional-jobs.yaml +++ b/zuul.d/functional-jobs.yaml @@ -74,15 +74,6 @@ DEFAULT: osapi_max_limit: 6 -- job: - name: openstacksdk-functional-devstack-legacy - parent: openstacksdk-functional-devstack-base - description: | - Run openstacksdk functional tests against a legacy devstack - nodeset: openstack-single-node-bionic - voting: false - override-branch: stable/ussuri - - job: name: openstacksdk-functional-devstack parent: openstacksdk-functional-devstack-base diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index ae88fa224..f0ca77a1c 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -30,8 +30,6 @@ voting: false - openstacksdk-functional-devstack-ironic: voting: false - - openstacksdk-functional-devstack-legacy: - voting: false - osc-functional-devstack-tips: voting: false - ansible-collections-openstack-functional-devstack: From 7bbef2f060d4f67dea859552e4c735060afa00d1 Mon Sep 17 00:00:00 2001 From: "Dr. Jens Harbott" Date: Fri, 31 Mar 2023 18:33:23 +0200 Subject: [PATCH 3217/3836] Change python3 jobs template to latest version Change-Id: I198cf76556fbc2921cb0fd012ac39b70bd1a8be6 --- zuul.d/project.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index f0ca77a1c..6f9cb948b 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -5,7 +5,7 @@ - project: templates: - check-requirements - - openstack-python3-antelope-jobs + - openstack-python3-jobs - openstacksdk-functional-tips - openstacksdk-tox-tips - os-client-config-tox-tips From a7456043289e833fc459fec7144bf1f0c1ddddc0 Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Tue, 21 Feb 2023 20:12:20 +0000 Subject: [PATCH 3218/3836] Add Designate (DNS) zone share API This patch adds support for the zone share API. Change-Id: I4cabb6444f90ffaf110ec0d0f16015b93bbd07f8 --- doc/source/user/proxies/dns.rst | 8 ++ doc/source/user/resources/dns/index.rst | 1 + .../user/resources/dns/v2/zone_share.rst | 12 ++ openstack/dns/v2/_proxy.py | 96 +++++++++++++- openstack/dns/v2/zone.py | 7 ++ openstack/dns/v2/zone_share.py | 45 +++++++ openstack/dns/version.py | 25 ++++ .../tests/functional/dns/v2/test_zone.py | 23 ++++ .../functional/dns/v2/test_zone_share.py | 119 ++++++++++++++++++ openstack/tests/unit/dns/test_version.py | 42 +++++++ openstack/tests/unit/dns/v2/test_proxy.py | 43 ++++++- openstack/tests/unit/dns/v2/test_zone.py | 4 +- .../tests/unit/dns/v2/test_zone_share.py | 63 ++++++++++ ...d-dns-zone-share-api-374e71cac504917f.yaml | 4 + 14 files changed, 487 insertions(+), 5 deletions(-) create mode 100644 doc/source/user/resources/dns/v2/zone_share.rst create mode 100644 openstack/dns/v2/zone_share.py create mode 100644 openstack/dns/version.py create mode 100644 openstack/tests/functional/dns/v2/test_zone_share.py create mode 100644 openstack/tests/unit/dns/test_version.py create mode 100644 openstack/tests/unit/dns/v2/test_zone_share.py create mode 100644 releasenotes/notes/add-dns-zone-share-api-374e71cac504917f.yaml diff --git a/doc/source/user/proxies/dns.rst b/doc/source/user/proxies/dns.rst index e85db5dbd..8c53c5418 100644 --- a/doc/source/user/proxies/dns.rst +++ b/doc/source/user/proxies/dns.rst @@ -60,3 +60,11 @@ Zone Transfer Operations create_zone_transfer_request, update_zone_transfer_request, delete_zone_transfer_request, zone_transfer_accepts, get_zone_transfer_accept, create_zone_transfer_accept + +Zone Share Operations +^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.dns.v2._proxy.Proxy + :noindex: + :members: create_zone_share, delete_zone_share, get_zone_share, + find_zone_share, zone_shares diff --git a/doc/source/user/resources/dns/index.rst b/doc/source/user/resources/dns/index.rst index a8d0c9360..77860a6a0 100644 --- a/doc/source/user/resources/dns/index.rst +++ b/doc/source/user/resources/dns/index.rst @@ -8,5 +8,6 @@ DNS Resources v2/zone_transfer v2/zone_export v2/zone_import + v2/zone_share v2/floating_ip v2/recordset diff --git a/doc/source/user/resources/dns/v2/zone_share.rst b/doc/source/user/resources/dns/v2/zone_share.rst new file mode 100644 index 000000000..5b0d02836 --- /dev/null +++ b/doc/source/user/resources/dns/v2/zone_share.rst @@ -0,0 +1,12 @@ +openstack.dns.v2.zone_share +=========================== + +.. automodule:: openstack.dns.v2.zone_share + +The ZoneShare Class +------------------- + +The ``DNS`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.dns.v2.zone_share.ZoneShare + :members: diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py index 1f147b7e3..85bc1cbb0 100644 --- a/openstack/dns/v2/_proxy.py +++ b/openstack/dns/v2/_proxy.py @@ -15,6 +15,7 @@ from openstack.dns.v2 import zone as _zone from openstack.dns.v2 import zone_export as _zone_export from openstack.dns.v2 import zone_import as _zone_import +from openstack.dns.v2 import zone_share as _zone_share from openstack.dns.v2 import zone_transfer as _zone_transfer from openstack import proxy @@ -26,6 +27,7 @@ class Proxy(proxy.Proxy): "zone": _zone.Zone, "zone_export": _zone_export.ZoneExport, "zone_import": _zone_import.ZoneImport, + "zone_share": _zone_share.ZoneShare, "zone_transfer_request": _zone_transfer.ZoneTransferRequest, } @@ -69,7 +71,7 @@ def get_zone(self, zone): """ return self._get(_zone.Zone, zone) - def delete_zone(self, zone, ignore_missing=True): + def delete_zone(self, zone, ignore_missing=True, delete_shares=False): """Delete a zone :param zone: The value can be the ID of a zone @@ -79,11 +81,14 @@ def delete_zone(self, zone, ignore_missing=True): the zone does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent zone. + :param bool delete_shares: When True, delete the zone shares along with + the zone. :returns: Zone been deleted :rtype: :class:`~openstack.dns.v2.zone.Zone` """ - return self._delete(_zone.Zone, zone, ignore_missing=ignore_missing) + return self._delete(_zone.Zone, zone, ignore_missing=ignore_missing, + delete_shares=delete_shares) def update_zone(self, zone, **attrs): """Update zone attributes @@ -536,6 +541,93 @@ def create_zone_transfer_accept(self, **attrs): """ return self._create(_zone_transfer.ZoneTransferAccept, **attrs) + # ======== Zone Shares ======== + def zone_shares(self, zone, **query): + """Retrieve a generator of zone sharess + + :param zone: The zone ID or a + :class:`~openstack.dns.v2.zone.Zone` instance + :param dict query: Optional query parameters to be sent to limit the + resources being returned. + + * `target_project_id`: The target project ID field. + + :returns: A generator of zone shares + :class:`~openstack.dns.v2.zone_share.ZoneShare` instances. + """ + zone_obj = self._get_resource(_zone.Zone, zone) + return self._list(_zone_share.ZoneShare, zone_id=zone_obj.id, **query) + + def get_zone_share(self, zone, zone_share): + """Get a zone share + + :param zone: The value can be the ID of a zone + or a :class:`~openstack.dns.v2.zone.Zone` instance. + :param zone_share: The zone_share can be either the ID of the zone + share or a :class:`~openstack.dns.v2.zone_share.ZoneShare` instance + that the zone share belongs to. + + :returns: ZoneShare instance. + :rtype: :class:`~openstack.dns.v2.zone_share.ZoneShare` + """ + zone_obj = self._get_resource(_zone.Zone, zone) + return self._get(_zone_share.ZoneShare, zone_share, + zone_id=zone_obj.id) + + def find_zone_share(self, zone, zone_share_id, ignore_missing=True): + """Find a single zone share + + :param zone: The value can be the ID of a zone + or a :class:`~openstack.dns.v2.zone.Zone` instance. + :param zone_share_id: The zone share ID + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the zone share does not exist. + When set to ``True``, None will be returned when attempting to + find a nonexistent zone share. + + :returns: :class:`~openstack.dns.v2.zone_share.ZoneShare` + """ + zone_obj = self._get_resource(_zone.Zone, zone) + return self._find(_zone_share.ZoneShare, zone_share_id, + ignore_missing=ignore_missing, zone_id=zone_obj.id) + + def create_zone_share(self, zone, **attrs): + """Create a new zone share from attributes + + :param zone: The zone ID or a + :class:`~openstack.dns.v2.zone.Zone` instance + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.dns.v2.zone_share.ZoneShare`, + comprised of the properties on the ZoneShare class. + + :returns: The results of zone share creation + :rtype: :class:`~openstack.dns.v2.zone_share.ZoneShare` + """ + zone_obj = self._get_resource(_zone.Zone, zone) + return self._create(_zone_share.ZoneShare, zone_id=zone_obj.id, + **attrs) + + def delete_zone_share(self, zone, zone_share, ignore_missing=True): + """Delete a zone share + + :param zone: The zone ID or a + :class:`~openstack.dns.v2.zone.Zone` instance + :param zone_share: The zone_share can be either the ID of the zone + share or a :class:`~openstack.dns.v2.zone_share.ZoneShare` instance + that the zone share belongs to. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the zone share does not exist. + When set to ``True``, no exception will be set when attempting to + delete a nonexistent zone share. + + :returns: ``None`` + """ + zone_obj = self._get_resource(_zone.Zone, zone) + self._delete(_zone_share.ZoneShare, zone_share, + ignore_missing=ignore_missing, zone_id=zone_obj.id) + def _get_cleanup_dependencies(self): # DNS may depend on floating ip return { diff --git a/openstack/dns/v2/zone.py b/openstack/dns/v2/zone.py index bc8c4c09b..c794d9e0b 100644 --- a/openstack/dns/v2/zone.py +++ b/openstack/dns/v2/zone.py @@ -79,6 +79,13 @@ class Zone(_base.Resource): type = resource.Body('type') #: Timestamp when the zone was last updated updated_at = resource.Body('updated_at') + #: Whether the zone is shared with other projects + #: *Type: bool* + is_shared = resource.Body('shared') + + # Headers for DELETE requests + #: If true, delete any existing zone shares along with the zone + delete_shares = resource.Header('x-designate-delete-shares', type=bool) def _action(self, session, action, body): """Preform actions given the message body. diff --git a/openstack/dns/v2/zone_share.py b/openstack/dns/v2/zone_share.py new file mode 100644 index 000000000..b778b1620 --- /dev/null +++ b/openstack/dns/v2/zone_share.py @@ -0,0 +1,45 @@ +# 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.dns.v2 import _base +from openstack import resource + + +class ZoneShare(_base.Resource): + """DNS ZONE Share Resource""" + + resources_key = 'shared_zones' + base_path = '/zones/%(zone_id)s/shares' + + # capabilities + allow_create = True + allow_delete = True + allow_fetch = True + allow_list = True + + _query_mapping = resource.QueryParameters('target_project_id') + + # Properties + #: Timestamp when the share was created. + created_at = resource.Body('created_at') + #: Timestamp when the member was last updated. + updated_at = resource.Body('updated_at') + #: The zone ID of the zone being shared. + zone_id = resource.Body('zone_id') + #: The project ID that owns the share. + project_id = resource.Body('project_id') + #: The target project ID that the zone is shared with. + target_project_id = resource.Body('target_project_id') + + # URI Properties + #: The ID of the zone being shared. + zone_id = resource.URI('zone_id') diff --git a/openstack/dns/version.py b/openstack/dns/version.py new file mode 100644 index 000000000..e9bd971a3 --- /dev/null +++ b/openstack/dns/version.py @@ -0,0 +1,25 @@ +# 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 resource + + +class Version(resource.Resource): + resource_key = 'version' + resources_key = 'versions' + base_path = '/' + + # capabilities + allow_list = True + + # Properties + links = resource.Body('links') + status = resource.Body('status') diff --git a/openstack/tests/functional/dns/v2/test_zone.py b/openstack/tests/functional/dns/v2/test_zone.py index 8acf77330..147b161f7 100644 --- a/openstack/tests/functional/dns/v2/test_zone.py +++ b/openstack/tests/functional/dns/v2/test_zone.py @@ -12,6 +12,7 @@ import random from openstack import connection +from openstack import exceptions from openstack.tests.functional import base @@ -66,3 +67,25 @@ def test_create_rs(self): ttl=3600, records=['192.168.1.1'] )) + + def test_delete_zone_with_shares(self): + zone_name = 'example-{0}.org.'.format(random.randint(1, 10000)) + zone = self.conn.dns.create_zone( + name=zone_name, + email='joe@example.org', + type='PRIMARY', + ttl=7200, + description='example zone' + ) + self.addCleanup(self.conn.dns.delete_zone, zone) + + demo_project_id = self.operator_cloud.get_project('demo')['id'] + zone_share = self.conn.dns.create_zone_share( + zone, target_project_id=demo_project_id) + self.addCleanup(self.conn.dns.delete_zone_share, zone, zone_share) + + # Test that we cannot delete a zone with shares + self.assertRaises(exceptions.BadRequestException, + self.conn.dns.delete_zone, zone) + + self.conn.dns.delete_zone(zone, delete_shares=True) diff --git a/openstack/tests/functional/dns/v2/test_zone_share.py b/openstack/tests/functional/dns/v2/test_zone_share.py new file mode 100644 index 000000000..1473ae46d --- /dev/null +++ b/openstack/tests/functional/dns/v2/test_zone_share.py @@ -0,0 +1,119 @@ +# 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 random + +from openstack import exceptions +from openstack.tests.functional import base + + +class TestZoneShare(base.BaseFunctionalTest): + + def setUp(self): + super(TestZoneShare, self).setUp() + self.require_service('dns') + if not self.user_cloud: + self.skipTest("The demo cloud is required for this test") + + # Note: zone deletion is not an immediate operation, so each time + # chose a new zone name for a test + # getUniqueString is not guaranteed to return unique string between + # different tests of the same class. + self.ZONE_NAME = 'example-{0}.org.'.format(random.randint(1, 10000)) + + self.zone = self.operator_cloud.dns.create_zone( + name=self.ZONE_NAME, + email='joe@example.org', + type='PRIMARY', + ttl=7200, + description='example zone for sdk zone share tests' + ) + self.addCleanup(self.operator_cloud.dns.delete_zone, self.zone, + delete_shares=True) + + self.project_id = self.operator_cloud.session.get_project_id() + self.demo_project_id = self.user_cloud.session.get_project_id() + + def test_create_delete_zone_share(self): + zone_share = self.operator_cloud.dns.create_zone_share( + self.zone, target_project_id=self.demo_project_id) + self.addCleanup(self.operator_cloud.dns.delete_zone_share, self.zone, + zone_share) + + self.assertEqual(self.zone.id, zone_share.zone_id) + self.assertEqual(self.project_id, zone_share.project_id) + self.assertEqual(self.demo_project_id, zone_share.target_project_id) + self.assertIsNotNone(zone_share.id) + self.assertIsNotNone(zone_share.created_at) + self.assertIsNone(zone_share.updated_at) + + def test_get_zone_share(self): + orig_zone_share = self.operator_cloud.dns.create_zone_share( + self.zone, target_project_id=self.demo_project_id) + self.addCleanup(self.operator_cloud.dns.delete_zone_share, self.zone, + orig_zone_share) + + zone_share = self.operator_cloud.dns.get_zone_share(self.zone, + orig_zone_share) + + self.assertEqual(self.zone.id, zone_share.zone_id) + self.assertEqual(self.project_id, zone_share.project_id) + self.assertEqual(self.demo_project_id, zone_share.target_project_id) + self.assertEqual(orig_zone_share.id, zone_share.id) + self.assertEqual(orig_zone_share.created_at, zone_share.created_at) + self.assertEqual(orig_zone_share.updated_at, zone_share.updated_at) + + def test_find_zone_share(self): + orig_zone_share = self.operator_cloud.dns.create_zone_share( + self.zone, target_project_id=self.demo_project_id) + self.addCleanup(self.operator_cloud.dns.delete_zone_share, self.zone, + orig_zone_share) + + zone_share = self.operator_cloud.dns.find_zone_share( + self.zone, orig_zone_share.id) + + self.assertEqual(self.zone.id, zone_share.zone_id) + self.assertEqual(self.project_id, zone_share.project_id) + self.assertEqual(self.demo_project_id, zone_share.target_project_id) + self.assertEqual(orig_zone_share.id, zone_share.id) + self.assertEqual(orig_zone_share.created_at, zone_share.created_at) + self.assertEqual(orig_zone_share.updated_at, zone_share.updated_at) + + def test_find_zone_share_ignore_missing(self): + zone_share = self.operator_cloud.dns.find_zone_share(self.zone, + 'bogus_id') + self.assertIsNone(zone_share) + + def test_find_zone_share_ignore_missing_false(self): + self.assertRaises(exceptions.ResourceNotFound, + self.operator_cloud.dns.find_zone_share, + self.zone, 'bogus_id', ignore_missing=False) + + def test_list_zone_shares(self): + zone_share = self.operator_cloud.dns.create_zone_share( + self.zone, target_project_id=self.demo_project_id) + self.addCleanup(self.operator_cloud.dns.delete_zone_share, self.zone, + zone_share) + + target_ids = [o.target_project_id for o in + self.operator_cloud.dns.zone_shares(self.zone)] + self.assertIn(self.demo_project_id, target_ids) + + def test_list_zone_shares_with_target_id(self): + zone_share = self.operator_cloud.dns.create_zone_share( + self.zone, target_project_id=self.demo_project_id) + self.addCleanup(self.operator_cloud.dns.delete_zone_share, self.zone, + zone_share) + + target_ids = [o.target_project_id for o in + self.operator_cloud.dns.zone_shares( + self.zone, target_project_id=self.demo_project_id)] + self.assertIn(self.demo_project_id, target_ids) diff --git a/openstack/tests/unit/dns/test_version.py b/openstack/tests/unit/dns/test_version.py new file mode 100644 index 000000000..be84a03bb --- /dev/null +++ b/openstack/tests/unit/dns/test_version.py @@ -0,0 +1,42 @@ +# 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.dns import version +from openstack.tests.unit import base + + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'id': IDENTIFIER, + 'links': '2', + 'status': '3', +} + + +class TestVersion(base.TestCase): + + def test_basic(self): + sot = version.Version() + self.assertEqual('version', sot.resource_key) + self.assertEqual('versions', sot.resources_key) + self.assertEqual('/', sot.base_path) + self.assertFalse(sot.allow_create) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = version.Version(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['links'], sot.links) + self.assertEqual(EXAMPLE['status'], sot.status) diff --git a/openstack/tests/unit/dns/v2/test_proxy.py b/openstack/tests/unit/dns/v2/test_proxy.py index ebabc8034..f80c56c34 100644 --- a/openstack/tests/unit/dns/v2/test_proxy.py +++ b/openstack/tests/unit/dns/v2/test_proxy.py @@ -16,6 +16,7 @@ from openstack.dns.v2 import zone from openstack.dns.v2 import zone_export from openstack.dns.v2 import zone_import +from openstack.dns.v2 import zone_share from openstack.dns.v2 import zone_transfer from openstack.tests.unit import test_proxy_base @@ -34,8 +35,9 @@ def test_zone_create(self): 'prepend_key': False}) def test_zone_delete(self): - self.verify_delete(self.proxy.delete_zone, - zone.Zone, True) + self.verify_delete( + self.proxy.delete_zone, zone.Zone, True, + expected_kwargs={'ignore_missing': True, 'delete_shares': False}) def test_zone_find(self): self.verify_find(self.proxy.find_zone, zone.Zone) @@ -213,3 +215,40 @@ def test_zone_transfer_accepts(self): def test_zone_transfer_accept_create(self): self.verify_create(self.proxy.create_zone_transfer_accept, zone_transfer.ZoneTransferAccept) + + +class TestDnsZoneShare(TestDnsProxy): + def test_zone_share_create(self): + self.verify_create(self.proxy.create_zone_share, zone_share.ZoneShare, + method_kwargs={'zone': 'bogus_id'}, + expected_kwargs={'zone_id': 'bogus_id'}) + + def test_zone_share_delete(self): + self.verify_delete( + self.proxy.delete_zone_share, zone_share.ZoneShare, + ignore_missing=True, + method_args={'zone': 'bogus_id', 'zone_share': 'bogus_id'}, + expected_args=['zone_share'], + expected_kwargs={'zone_id': 'zone', 'ignore_missing': True}) + + def test_zone_share_find(self): + self.verify_find( + self.proxy.find_zone_share, zone_share.ZoneShare, + method_args=['zone'], + expected_args=['zone'], + expected_kwargs={'zone_id': 'resource_name', + 'ignore_missing': True}) + + def test_zone_share_get(self): + self.verify_get( + self.proxy.get_zone_share, zone_share.ZoneShare, + method_args=['zone', 'zone_share'], + expected_args=['zone_share'], + expected_kwargs={'zone_id': 'zone'}) + + def test_zone_shares(self): + self.verify_list( + self.proxy.zone_shares, zone_share.ZoneShare, + method_args=['zone'], + expected_args=[], + expected_kwargs={'zone_id': 'zone'}) diff --git a/openstack/tests/unit/dns/v2/test_zone.py b/openstack/tests/unit/dns/v2/test_zone.py index 190e71a83..cbc41c699 100644 --- a/openstack/tests/unit/dns/v2/test_zone.py +++ b/openstack/tests/unit/dns/v2/test_zone.py @@ -28,7 +28,8 @@ 'type': 'PRIMARY', 'ttl': 7200, 'description': 'This is an example zone.', - 'status': 'ACTIVE' + 'status': 'ACTIVE', + 'shared': False } @@ -76,6 +77,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['type'], sot.type) self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['status'], sot.status) + self.assertEqual(EXAMPLE['shared'], sot.is_shared) def test_abandon(self): sot = zone.Zone(**EXAMPLE) diff --git a/openstack/tests/unit/dns/v2/test_zone_share.py b/openstack/tests/unit/dns/v2/test_zone_share.py new file mode 100644 index 000000000..0c5d04ce7 --- /dev/null +++ b/openstack/tests/unit/dns/v2/test_zone_share.py @@ -0,0 +1,63 @@ +# 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 unittest import mock + +from keystoneauth1 import adapter + +from openstack.dns.v2 import zone_share +from openstack.tests.unit import base + + +class TestZoneShare(base.TestCase): + + def setUp(self): + super(TestZoneShare, self).setUp() + self.resp = mock.Mock() + self.resp.body = None + self.resp.json = mock.Mock(return_value=self.resp.body) + self.resp.status_code = 200 + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.post = mock.Mock(return_value=self.resp) + self.sess.default_microversion = None + + def test_basic(self): + sot = zone_share.ZoneShare() + self.assertEqual(None, sot.resource_key) + self.assertEqual('shared_zones', sot.resources_key) + self.assertEqual('/zones/%(zone_id)s/shares', sot.base_path) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_delete) + self.assertFalse(sot.allow_commit) + + self.assertDictEqual({'target_project_id': 'target_project_id', + 'limit': 'limit', 'marker': 'marker'}, + sot._query_mapping._mapping) + + def test_make_it(self): + share_id = 'bogus_id' + zone_id = 'bogus_zone_id' + project_id = 'bogus_project_id' + target_id = 'bogus_target_id' + expected = { + 'id': share_id, + 'zone_id': zone_id, + 'project_id': project_id, + 'target_project_id': target_id} + + sot = zone_share.ZoneShare(**expected) + self.assertEqual(share_id, sot.id) + self.assertEqual(zone_id, sot.zone_id) + self.assertEqual(project_id, sot.project_id) + self.assertEqual(target_id, sot.target_project_id) diff --git a/releasenotes/notes/add-dns-zone-share-api-374e71cac504917f.yaml b/releasenotes/notes/add-dns-zone-share-api-374e71cac504917f.yaml new file mode 100644 index 000000000..1541c1744 --- /dev/null +++ b/releasenotes/notes/add-dns-zone-share-api-374e71cac504917f.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add Designate (DNS) support for zone shares. From 6b0206a556a4a85d8b42e851dee6be2fa4e6c7e7 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 31 Mar 2023 09:47:17 +0200 Subject: [PATCH 3219/3836] Add Cleura acceptance tests - add cleura credentials - add acceptance-cleura job Change-Id: Ib151d43e5a154c09fdd62ee4a86bad4bf43fd081 --- zuul.d/acceptance-jobs.yaml | 79 +++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/zuul.d/acceptance-jobs.yaml b/zuul.d/acceptance-jobs.yaml index 00a298da4..3a94fdc92 100644 --- a/zuul.d/acceptance-jobs.yaml +++ b/zuul.d/acceptance-jobs.yaml @@ -1,4 +1,8 @@ --- +- semaphore: + name: acceptance-cleura + max: 1 + - job: name: openstacksdk-acceptance-base parent: openstack-tox @@ -20,6 +24,7 @@ attempts: 1 voting: false + timeout: 3600 pre-run: - playbooks/acceptance/pre.yaml post-run: @@ -54,6 +59,17 @@ - secret: credentials-devstack name: openstack_credentials +- job: + name: openstacksdk-acceptance-cleura + parent: openstacksdk-acceptance-base + description: Acceptance tests of the OpenStackSDK on Cleura + semaphores: + - name: acceptance-cleura + secrets: + - secret: credentials-cleura + name: openstack_credentials + pass-to-parent: true + # Devstack secret is not specifying auth_url because of how Zuul treats secrets. # Auth_url comes extra in the job vars and is being used if no auth_url in the # secret is present. @@ -69,9 +85,72 @@ region_name: RegionOne verify: false +# Contact: tobias [xdot] rydberg [mat] citynetwork [xdot] eu +- secret: + name: credentials-cleura + data: + auth: + auth_url: https://fra1.citycloud.com:5000 + user_domain_name: !encrypted/pkcs1-oaep + - B2+GBOl0HqQJ0umGR/8y6Y1SS+O6h7OK6rTa54797UavexKVxx2RZ144wPmW+IogX2QU2 + tWtGBveQnZTpI19nxlnLmQQA+YSz8RIzJoFuStBmiITyCHQnvRJPc7kObjnZJLuoVwCT2 + Rl3u1iGzJb/ZZvVDjvYH2ZW7a6aH+Ct7HfB+CGhvhETeoMAFDgb29QJ5U/T3OkVdTMwCY + XDtdwg2JvoErd2gnNCqYDcIiOMO6lXKcc+35VQtGMGfoaUvu+iMlEi9pJqbdVd7qz5lgY + AWBPG1mYt1mOaP8RRvzywhyRPnnnFgfUe2rf2ZozEUa7j4ObwXt7D8oRYXm+USEpk+YfD + 9V3CvGvAgmPuuidGWwnZdPcNX/w/VW5p9oWRgJFYChb5+XCu7y0tFJX/usduZEY9/MvJs + Iv0+OFf1TXc29qFqwGYVSyfimBroGFdYXmHSwK7wHJ1GUsdSRhQz4eYIdk+6c4LNx9JgO + 5Z+3Q29tlh9WwuuQKE/JlKJ/1I9LC0RmyJyxSaiTLDiL+7J2O/hULmyZimbXVcYuXqDdo + KAdPryYhmWWyBFkZfUa88GxwVf+WDLQqXhv+CDGRusbW2opVvv6p7NUwLh9PPOGnRLsS2 + y1fZDVtz60ZMp8MQPACYjlzvc2lF5Z1Cvskr3O9KbT27V7AyLXmU+tbMrDLpC0= + project_domain_name: !encrypted/pkcs1-oaep + - B2+GBOl0HqQJ0umGR/8y6Y1SS+O6h7OK6rTa54797UavexKVxx2RZ144wPmW+IogX2QU2 + tWtGBveQnZTpI19nxlnLmQQA+YSz8RIzJoFuStBmiITyCHQnvRJPc7kObjnZJLuoVwCT2 + Rl3u1iGzJb/ZZvVDjvYH2ZW7a6aH+Ct7HfB+CGhvhETeoMAFDgb29QJ5U/T3OkVdTMwCY + XDtdwg2JvoErd2gnNCqYDcIiOMO6lXKcc+35VQtGMGfoaUvu+iMlEi9pJqbdVd7qz5lgY + AWBPG1mYt1mOaP8RRvzywhyRPnnnFgfUe2rf2ZozEUa7j4ObwXt7D8oRYXm+USEpk+YfD + 9V3CvGvAgmPuuidGWwnZdPcNX/w/VW5p9oWRgJFYChb5+XCu7y0tFJX/usduZEY9/MvJs + Iv0+OFf1TXc29qFqwGYVSyfimBroGFdYXmHSwK7wHJ1GUsdSRhQz4eYIdk+6c4LNx9JgO + 5Z+3Q29tlh9WwuuQKE/JlKJ/1I9LC0RmyJyxSaiTLDiL+7J2O/hULmyZimbXVcYuXqDdo + KAdPryYhmWWyBFkZfUa88GxwVf+WDLQqXhv+CDGRusbW2opVvv6p7NUwLh9PPOGnRLsS2 + y1fZDVtz60ZMp8MQPACYjlzvc2lF5Z1Cvskr3O9KbT27V7AyLXmU+tbMrDLpC0= + project_name: !encrypted/pkcs1-oaep + - IRSHyf964g3q7vHY08reyx69cGDLG/+kkEnZ4fs4qiwBw1RL1wKW3r3Omi1PLXDHHCHfC + jlRrwvZh80CzG3nqt94WSiASjn4XvZtCV0++UZxCkdEs/2SXN1YYpBGLqotM91NhQHCpo + Xu6KD7U8ckZgjAQFzV/rF7pnFSvzb14PQqBiQ4Ei7nFyrg6sW20ratjC+pBboUORPvPjG + wuY/lt8kRXYnPlI/oeFngXMl/WD7z5k0kLwUcg/z9x3uF6b6xozR8Vzjal13RR7FU5Tu7 + T5Qr8uREPHlK8aU90XnNrlJqIAfIFuAlmZCeckIMlVqGjGBekI2W/zPXhL/SjR2SNeTIl + SwKfInnT0SfGqKTAjgPJAocZSNppt4ql1EsS3Rdp8SQ0EGW7pXs73svexNRhh4k1m7gM1 + 54OoyS2wtMaTR3Q3L92ZuT2DdxmPbvXThbRO5P2g0yDpp/HuWkQyHq9b1tZD+p7akU7p+ + g8fIQFKFueFP0T6XszQSPySjjaTZOWd0CQC2oTlivcf7oZ4etp22Zh7IDCXWLX39C2LkF + XLBaEa9LRxn1UwJ2bz2nUPjqDsOz2nRskC9Yz0XOOEKMokJ4POj+uac1iRfAf+hAGd9uE + 7rNIp/7oV5ABOimJ5bgCI1SWAsz2F1lRq+bulzbONLmWfPik52bo/elXTxRais= + username: !encrypted/pkcs1-oaep + - bTHRzdAYEKXeFhrU3sBRN19ygO2t2zzXdeuB4DQq7Q+7VW7Apo8Vo1eaqpqjUnpI2jPG+ + DJSg0ZG3tUsnRwwKo3N8RzwFNWj5wcUEtHjmFgMmLBvlv9Jv6OeN7R7AH7b21agTMTvwz + X7hGWbYSEgDLn80uNTwcm4GVA0mycXDtIvZ4lPiCGkUJYav9++YbGYzDyiy2pBgVU0r5G + 7GTO+cHQWUw+LL/scBijL4khLIxiHNgUNNfgAYOI/JQ720DxXSDF30SN8fRy0H0jl54Gr + w0exl9QPBjI+o+qvFKq2Bni8dTp96MaC8pDxP/1/R8mEMYD2Ei3Ame1dfeUz7OgrQfpQv + xlDSE/sM2/g0PG3YlpG+aCllZ1el2qM/B+pyq5JXf26swp1RdjehvUSIi3gQaqkC3qpRt + 2FgZDKdHW6PYRmRlCphS5WK1otdCQEvyJ+s4QB4PooMcD4rqAf5hURGd5zr/aajqmEgXX + eJKeLQrQD+4yJWeopcq7a66R3LeL07Dko2LWWlL6adGeQ5yd3eIZK7zwObTVE64DSbXDs + 3UI8U6Qa3EMlrfEk8TXcK1QW2EM1JFiPBSm9e8zojTtg/caAyROXgn1T9qv0FKMcJZrOo + Qt+n7vv1wkCSUoUEQFIadcMUn5EoXeTcRbjAOsRFN/OOh6+4jyNTh17cOC5dkc= + password: !encrypted/pkcs1-oaep + - FbeRKkCs2YlDYm944EUuUbY2mVcTwSgE00gMZokmXR2WjKqRsuLFpkOe9opndwqV1tUyj + mxAGizoGlzI+Lg8VnS47zShM+UqgaNzC148iY+WBuLXAEoxS3c9Gxz03Gm/Q2Tu6MJoCG + OY8JvQkq+pjwkV61sIawTfQRTZkwjFO8F/viSOuF75PDZthY5SuMN5MEJ8B8Ska0WNbjw + Edo623gZnyZsPvZwnqnP+yK0HW0smohKkvjHPZ5SGFiQ0G3eTSHaL5wrYWbkcZ5Gb4UgX + x1edebv0ata0fZ8nhIwTrDIVe9icuijuV1ZkvHMGPvB50fkup4/QyObx6QUhL6D0mXaK5 + fIq+dgrzkvcoODrwpXvBVxjNYnM+DBeMbN0V8d4vDvsRPsWCxIenETse1gD0PJyXx29br + /Vild1xO1JnxoU469fl/gzdntyoV/QaLDteLKMFJISAFuVrcCEUz63s37iKAy6LnCtv/J + PjciFvc2OR0cGUC/an3xtmqi18GWcWdinaBA0+OEnArdOdSc79MTZnMifICAeCQ3yiEnA + 001hbBrRYTHgitpo4gYJOFMVufhcfvq6yB9wi3MqvpKP8wGH2SyNz7y5Gy9zbUgQFsRP7 + 2h3LRDRCVGYBVgBLD5mcIMn93HddOko8Q8RO8qVZM13R39dgGAi0KMEhF3bpjA= + # We define additional project entity not to handle acceptance jobs in # already complex enough general project entity. - project: post-review: jobs: - openstacksdk-acceptance-devstack + - openstacksdk-acceptance-cleura From 5322620511844f4f541588974e5dfa6115c911b1 Mon Sep 17 00:00:00 2001 From: Theo Gindre Date: Mon, 3 Apr 2023 14:01:45 +0200 Subject: [PATCH 3220/3836] ssh key change This patch allows rebuild to change the ssh key used by instance Change-Id: I876769b196a9a6014794b264de6e09a904b91a3a --- openstack/compute/v2/server.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 56a7fe6d7..dd1446d17 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -309,7 +309,7 @@ def force_delete(self, session): def rebuild(self, session, image, name=None, admin_password=None, preserve_ephemeral=None, access_ipv4=None, access_ipv6=None, - metadata=None, user_data=None): + metadata=None, user_data=None, key_name=None): """Rebuild the server with the given arguments.""" action = { 'imageRef': resource.Resource._get_id(image) @@ -328,6 +328,8 @@ def rebuild(self, session, image, name=None, admin_password=None, action['metadata'] = metadata if user_data is not None: action['user_data'] = user_data + if key_name is not None: + action['key_name'] = key_name body = {'rebuild': action} response = self._action(session, body) From 1921a195c60ab14d379e843bd875394c58fde8f7 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 19 Dec 2022 16:07:41 +0000 Subject: [PATCH 3221/3836] compute: Add Server.restore, restore_server proxy method Change-Id: I3c7222255f03797c8ae599090b482f317cb3c87c Signed-off-by: Stephen Finucane --- doc/source/user/proxies/compute.rst | 6 +++--- openstack/compute/v2/_proxy.py | 12 +++++++++++- openstack/compute/v2/server.py | 4 ++++ openstack/tests/unit/compute/v2/test_proxy.py | 7 +++++++ openstack/tests/unit/compute/v2/test_server.py | 14 ++++++++++++++ .../compute-restore-server-020bf091acc9f8df.yaml | 6 ++++++ 6 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/compute-restore-server-020bf091acc9f8df.yaml diff --git a/doc/source/user/proxies/compute.rst b/doc/source/user/proxies/compute.rst index 698a134c2..b2b693bbf 100644 --- a/doc/source/user/proxies/compute.rst +++ b/doc/source/user/proxies/compute.rst @@ -39,9 +39,9 @@ Starting, Stopping, etc. .. autoclass:: openstack.compute.v2._proxy.Proxy :noindex: :members: start_server, stop_server, suspend_server, resume_server, - reboot_server, shelve_server, unshelve_server, lock_server, - unlock_server, pause_server, unpause_server, rescue_server, - unrescue_server, evacuate_server, migrate_server, + reboot_server, restore_server, shelve_server, unshelve_server, + lock_server, unlock_server, pause_server, unpause_server, + rescue_server, unrescue_server, evacuate_server, migrate_server, get_server_console_output, live_migrate_server Modifying a Server diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 445dbe11d..19fc16148 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -1099,6 +1099,16 @@ def stop_server(self, server): server = self._get_resource(_server.Server, server) server.stop(self) + def restore_server(self, server): + """Restore a soft-deleted server. + + :param server: Either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance. + :returns: None + """ + server = self._get_resource(_server.Server, server) + server.restore(self) + def shelve_server(self, server): """Shelves a server. @@ -1115,7 +1125,7 @@ def shelve_server(self, server): server.shelve(self) def unshelve_server(self, server): - """Unselves or restores a shelved server. + """Unshelves or restores a shelved server. Policy defaults enable only users with administrative role or the owner of the server to perform this operation. Cloud provides could diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 56a7fe6d7..ccc7e7b34 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -487,6 +487,10 @@ def stop(self, session): body = {"os-stop": None} self._action(session, body) + def restore(self, session): + body = {"restore": None} + self._action(session, body) + def shelve(self, session): body = {"shelve": None} self._action(session, body) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 12a0c41e6..cece6643e 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -1096,6 +1096,13 @@ def test_server_stop(self): method_args=["value"], expected_args=[self.proxy]) + def test_server_restore(self): + self._verify( + "openstack.compute.v2.server.Server.restore", + self.proxy.restore_server, + method_args=["value"], + expected_args=[self.proxy]) + def test_server_shelve(self): self._verify( "openstack.compute.v2.server.Server.shelve", diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 74aeeecc1..0676e4515 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -835,6 +835,20 @@ def test_stop(self): microversion=self.sess.default_microversion, ) + def test_restore(self): + sot = server.Server(**EXAMPLE) + + res = sot.restore(self.sess) + + self.assertIsNone(res) + url = 'servers/IDENTIFIER/action' + body = {'restore': None} + headers = {'Accept': ''} + self.sess.post.assert_called_with( + url, json=body, headers=headers, + microversion=self.sess.default_microversion, + ) + def test_shelve(self): sot = server.Server(**EXAMPLE) diff --git a/releasenotes/notes/compute-restore-server-020bf091acc9f8df.yaml b/releasenotes/notes/compute-restore-server-020bf091acc9f8df.yaml new file mode 100644 index 000000000..9269c4742 --- /dev/null +++ b/releasenotes/notes/compute-restore-server-020bf091acc9f8df.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + The ``openstack.compute.v2.server.Server`` object now provides a + ``restore`` method to restore it from a soft-deleted state, while the + compute proxy method provides an equivalent ``restore_server`` method. From 5b3ad87934a869ffd41bce90d9c9cb45970aaa49 Mon Sep 17 00:00:00 2001 From: Tobias Rydberg Date: Tue, 4 Apr 2023 07:53:19 +0000 Subject: [PATCH 3222/3836] Removing region Lon1. Updating block_storage_api_version. Adding image_format. Change-Id: I5325f3503cdc2133418b6b9ae868482feb86200f --- openstack/config/vendors/citycloud.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openstack/config/vendors/citycloud.json b/openstack/config/vendors/citycloud.json index b48978c27..53057076a 100644 --- a/openstack/config/vendors/citycloud.json +++ b/openstack/config/vendors/citycloud.json @@ -7,14 +7,14 @@ "regions": [ "Buf1", "Fra1", - "Lon1", "Sto2", "Kna1", "dx1", "tky1" ], "requires_floating_ip": true, - "block_storage_api_version": "1", - "identity_api_version": "3" + "block_storage_api_version": "3", + "identity_api_version": "3", + "image_format": "raw" } } From a9614648c6a705bf85a6cf8d134b3125304b318b Mon Sep 17 00:00:00 2001 From: Mridula Joshi Date: Thu, 23 Feb 2023 16:36:02 +0000 Subject: [PATCH 3223/3836] Adds Support for ``glance cache-clear`` Change-Id: Id7d4fe1b0092d0befccffc0fc779e4aef861cdfc --- doc/source/user/proxies/image_v2.rst | 3 +-- openstack/image/v2/_proxy.py | 10 ++++++++++ openstack/image/v2/cache.py | 14 ++++++++++++++ openstack/tests/unit/image/v2/test_cache.py | 9 +++++++++ openstack/tests/unit/image/v2/test_proxy.py | 11 +++++++++++ 5 files changed, 45 insertions(+), 2 deletions(-) diff --git a/doc/source/user/proxies/image_v2.rst b/doc/source/user/proxies/image_v2.rst index c838fd898..83c933269 100644 --- a/doc/source/user/proxies/image_v2.rst +++ b/doc/source/user/proxies/image_v2.rst @@ -89,5 +89,4 @@ Cache Operations .. autoclass:: openstack.image.v2._proxy.Proxy :noindex: - :members: cache_delete_image, queue_image, get_image_cache - + :members: cache_delete_image, queue_image, get_image_cache, clear_cache diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 6de411237..a199394f7 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -96,7 +96,17 @@ def queue_image(self, image_id): cache = self._get_resource(_cache.Cache, None) return cache.queue(self, image_id) + def clear_cache(self, target): + """ Clear all images from cache, queue or both + + :param target: Specify which target you want to clear + One of: ``both``(default), ``cache``, ``queue``. + """ + cache = self._get_resource(_cache.Cache, None) + return cache.clear(self, target) + # ====== IMAGES ====== + def create_image( self, name, diff --git a/openstack/image/v2/cache.py b/openstack/image/v2/cache.py index e9d59072c..09297b73d 100644 --- a/openstack/image/v2/cache.py +++ b/openstack/image/v2/cache.py @@ -50,3 +50,17 @@ def queue(self, session, image, *, microversion=None): response = session.put(url, microversion=microversion) exceptions.raise_from_response(response) return response + + def clear(self, session, target='both'): + """Clears the cache. + :param session: The session to use for making this request + :param target: Specify which target you want to clear + One of: ``both``(default), ``cache``, ``queue``. + :returns: The server response + """ + headers = {} + if target != "both": + headers = {'x-image-cache-clear-target': target} + response = session.delete(self.base_path, headers=headers) + exceptions.raise_from_response(response) + return response diff --git a/openstack/tests/unit/image/v2/test_cache.py b/openstack/tests/unit/image/v2/test_cache.py index 6f29dbdf7..73e3359a8 100644 --- a/openstack/tests/unit/image/v2/test_cache.py +++ b/openstack/tests/unit/image/v2/test_cache.py @@ -59,3 +59,12 @@ def test_queue(self): sess.put.assert_called_with('cache/image_id', microversion=sess.default_microversion) + + @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) + def test_clear(self): + sot = cache.Cache(**EXAMPLE) + session = mock.Mock() + session.delete = mock.Mock() + + sot.clear(session, 'both') + session.delete.assert_called_with('/cache', headers={}) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index b32026a0d..cad1fae0e 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -913,3 +913,14 @@ def test_image_queue(self, mock_get_resource): method_args=['image-id'], expected_args=[self.proxy, 'image-id']) mock_get_resource.assert_called_once_with(_cache.Cache, None) + + @mock.patch.object(proxy_base.Proxy, '_get_resource') + def test_image_clear_cache(self, mock_get_resource): + fake_cache = _cache.Cache() + mock_get_resource.return_value = fake_cache + self._verify( + "openstack.image.v2.cache.Cache.clear", + self.proxy.clear_cache, + method_args=['both'], + expected_args=[self.proxy, 'both']) + mock_get_resource.assert_called_once_with(_cache.Cache, None) From ac194f985f02821bea38ccc38d1256a2250ed471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Ribaud?= Date: Mon, 22 Aug 2022 12:24:57 +0200 Subject: [PATCH 3224/3836] Microversion 2.91: Support specifying destination host to unshelve This patch adds ``host`` to openstacksdk unshelve api. This can help administrators to specify a ``host`` to unshelve a shelve offloaded server starting from 2.91 microversion. Implements: blueprint unshelve-to-host Change-Id: I34c5989be4710c863cce24d6f55bd1faae86cd52 --- openstack/compute/v2/server.py | 31 ++++++++++-- .../tests/unit/compute/v2/test_server.py | 50 +++++++++++++++++++ ...lve-to-specific-host-84666d440dce4a73.yaml | 7 +++ 3 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/unshelve-to-specific-host-84666d440dce4a73.yaml diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index fef30fd1d..e2bd75ba6 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -40,6 +40,11 @@ class Server(resource.Resource, metadata.MetadataMixin, tag.TagMixin): allow_delete = True allow_list = True + # Sentinel used to differentiate API called without parameter or None + # Ex unshelve API can be called without an availability_zone or with + # availability_zone = None to unpin the az. + _sentinel = object() + _query_mapping = resource.QueryParameters( "auto_disk_config", "availability_zone", "created_at", "description", "flavor", @@ -65,7 +70,7 @@ class Server(resource.Resource, metadata.MetadataMixin, tag.TagMixin): **tag.TagMixin._tag_query_parameters ) - _max_microversion = '2.73' + _max_microversion = '2.91' #: A list of dictionaries holding links relevant to this server. links = resource.Body('links') @@ -497,10 +502,26 @@ def shelve(self, session): body = {"shelve": None} self._action(session, body) - def unshelve(self, session, availability_zone=None): - body = {"unshelve": None} - if availability_zone: - body["unshelve"] = {"availability_zone": availability_zone} + def unshelve(self, session, availability_zone=_sentinel, host=None): + """ + Unshelve -- Unshelve the server. + + :param availability_zone: If specified the instance will be unshelved + to the availability_zone. + If None is passed the instance defined availability_zone is unpin + and the instance will be scheduled to any availability_zone (free + scheduling). + If not specified the instance will be unshelved to either its + defined availability_zone or any availability_zone + (free scheduling). + :param host: If specified the host to unshelve the instance. + """ + data = {} + if host: + data["host"] = host + if availability_zone is None or isinstance(availability_zone, str): + data["availability_zone"] = availability_zone + body = {'unshelve': data or None} self._action(session, body) def migrate(self, session): diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 0676e4515..0a6861dc0 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -893,6 +893,56 @@ def test_unshelve_availability_zone(self): microversion=self.sess.default_microversion, ) + def test_unshelve_unpin_az(self): + sot = server.Server(**EXAMPLE) + + res = sot.unshelve(self.sess, availability_zone=None) + + self.assertIsNone(res) + url = 'servers/IDENTIFIER/action' + body = {"unshelve": { + "availability_zone": None + }} + headers = {'Accept': ''} + self.sess.post.assert_called_with( + url, json=body, headers=headers, + microversion=self.sess.default_microversion) + + def test_unshelve_host(self): + sot = server.Server(**EXAMPLE) + + res = sot.unshelve(self.sess, host=sot.hypervisor_hostname) + + self.assertIsNone(res) + url = 'servers/IDENTIFIER/action' + body = {"unshelve": { + "host": sot.hypervisor_hostname + }} + headers = {'Accept': ''} + self.sess.post.assert_called_with( + url, json=body, headers=headers, + microversion=self.sess.default_microversion) + + def test_unshelve_host_and_availability_zone(self): + sot = server.Server(**EXAMPLE) + + res = sot.unshelve( + self.sess, + availability_zone=sot.availability_zone, + host=sot.hypervisor_hostname + ) + + self.assertIsNone(res) + url = 'servers/IDENTIFIER/action' + body = {"unshelve": { + "availability_zone": sot.availability_zone, + "host": sot.hypervisor_hostname + }} + headers = {'Accept': ''} + self.sess.post.assert_called_with( + url, json=body, headers=headers, + microversion=self.sess.default_microversion) + def test_migrate(self): sot = server.Server(**EXAMPLE) diff --git a/releasenotes/notes/unshelve-to-specific-host-84666d440dce4a73.yaml b/releasenotes/notes/unshelve-to-specific-host-84666d440dce4a73.yaml new file mode 100644 index 000000000..5a73890a2 --- /dev/null +++ b/releasenotes/notes/unshelve-to-specific-host-84666d440dce4a73.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Add SDK support for Nova microversion 2.91. This microversion + allows specifying a destination host to unshelve a shelve + offloaded server. And availability zone can be set to None to unpin + the availability zone of a server. From ecde40285380beaeeced7f32c17f8dce027f68ca Mon Sep 17 00:00:00 2001 From: arkaruki Date: Thu, 18 Feb 2021 17:27:26 +0000 Subject: [PATCH 3225/3836] Add export location resource to shared file system Introduce ShareExportLocations class with list method to shared file system storage service. Change-Id: Ia0663b64b8417de010f1b507252d76429bf47054 Co-Authored-By: Samuel Loegering --- openstack/shared_file_system/v2/_proxy.py | 31 +++++++++++++ .../v2/share_export_locations.py | 44 +++++++++++++++++++ .../test_export_locations.py | 42 ++++++++++++++++++ .../v2/test_share_export_locations.py | 44 +++++++++++++++++++ ...tems-export-location-a27c1741880c384b.yaml | 5 +++ 5 files changed, 166 insertions(+) create mode 100644 openstack/shared_file_system/v2/share_export_locations.py create mode 100644 openstack/tests/functional/shared_file_system/test_export_locations.py create mode 100644 openstack/tests/unit/shared_file_system/v2/test_share_export_locations.py create mode 100644 releasenotes/notes/add-shared-file-systems-export-location-a27c1741880c384b.yaml diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index eae2b0994..25ef24a96 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -14,6 +14,9 @@ from openstack import resource from openstack.shared_file_system.v2 import ( availability_zone as _availability_zone) +from openstack.shared_file_system.v2 import ( + share_export_locations as _share_export_locations +) from openstack.shared_file_system.v2 import ( share_network as _share_network ) @@ -46,6 +49,8 @@ class Proxy(proxy.Proxy): "share_snapshot_instance": _share_snapshot_instance.ShareSnapshotInstance, "share_instance": _share_instance.ShareInstance, + "share_export_locations": + _share_export_locations.ShareExportLocation, } def availability_zones(self): @@ -489,3 +494,29 @@ def delete_share_instance(self, share_instance_id): res = self._get_resource(_share_instance.ShareInstance, share_instance_id) res.force_delete(self) + + def export_locations(self, share_id): + """List all export locations with details + + :param share_id: The ID of the share to list export locations from + :returns: List of export locations + :rtype: List of :class:`~openstack.shared_filesystem_storage.v2. + share_export_locations.ShareExportLocations` + """ + return self._list(_share_export_locations.ShareExportLocation, + share_id=share_id) + + def get_export_location(self, export_location, share_id): + """List details of export location + + :param export_location: The export location resource to get + :param share_id: The ID of the share to get export locations from + :returns: Details of identified export location + :rtype: :class:`~openstack.shared_filesystem_storage.v2. + share_export_locations.ShareExportLocations` + """ + + export_location_id = resource.Resource._get_id(export_location) + return self._get( + _share_export_locations.ShareExportLocation, + export_location_id, share_id=share_id) diff --git a/openstack/shared_file_system/v2/share_export_locations.py b/openstack/shared_file_system/v2/share_export_locations.py new file mode 100644 index 000000000..f18bf49f0 --- /dev/null +++ b/openstack/shared_file_system/v2/share_export_locations.py @@ -0,0 +1,44 @@ +# 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 resource + + +class ShareExportLocation(resource.Resource): + resource_key = "export_location" + resources_key = "export_locations" + base_path = "/shares/%(share_id)s/export_locations" + + # capabilities + allow_list = True + allow_fetch = True + allow_create = False + allow_commit = False + allow_delete = False + allow_head = False + + #: Properties + # The share ID, part of the URI for export locations + share_id = resource.URI("share_id", type='str') + #: The path of the export location. + path = resource.Body("path", type=str) + #: Indicate if export location is preferred. + is_preferred = resource.Body("preferred", type=bool) + #: The share instance ID of the export location. + share_instance_id = resource.Body("share_instance_id", type=str) + #: Indicate if export location is admin only. + is_admin = resource.Body("is_admin_only", type=bool) + #: Indicate when the export location is created at + created_at = resource.Body("created_at", type=str) + #: Indicate when the export location is updated at + updated_at = resource.Body("updated_at", type=str) diff --git a/openstack/tests/functional/shared_file_system/test_export_locations.py b/openstack/tests/functional/shared_file_system/test_export_locations.py new file mode 100644 index 000000000..3391590f2 --- /dev/null +++ b/openstack/tests/functional/shared_file_system/test_export_locations.py @@ -0,0 +1,42 @@ +# 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.tests.functional.shared_file_system import base + + +class TestExportLocation(base.BaseSharedFileSystemTest): + + min_microversion = '2.9' + + def setUp(self): + super().setUp() + + self.SHARE_NAME = self.getUniqueString() + my_share = self.create_share( + name=self.SHARE_NAME, size=2, share_type="dhss_false", + share_protocol='NFS', description=None) + self.SHARE_ID = my_share.id + + def test_export_locations(self): + exs = self.user_cloud.shared_file_system.export_locations( + self.SHARE_ID + ) + self.assertGreater(len(list(exs)), 0) + for ex in exs: + for attribute in ( + 'id', 'path', 'share_instance_id', + 'updated_at', 'created_at'): + self.assertTrue(hasattr(ex, attribute)) + self.assertIsInstance(getattr(ex, attribute), 'str') + for attribute in ('is_preferred', 'is_admin'): + self.assertTrue(hasattr(ex, attribute)) + self.assertIsInstance(getattr(ex, attribute), 'bool') diff --git a/openstack/tests/unit/shared_file_system/v2/test_share_export_locations.py b/openstack/tests/unit/shared_file_system/v2/test_share_export_locations.py new file mode 100644 index 000000000..0e9940708 --- /dev/null +++ b/openstack/tests/unit/shared_file_system/v2/test_share_export_locations.py @@ -0,0 +1,44 @@ +# 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.shared_file_system.v2 import share_export_locations as el +from openstack.tests.unit import base + + +IDENTIFIER = '08a87d37-5ca2-4308-86c5-cba06d8d796c' +EXAMPLE = { + "id": "f87589cb-f4bc-4a9b-b481-ab701206eb85", + "path": ("199.19.213.225:/opt/stack/data/manila/mnt/" + "share-6ba490c5-5225-4c3b-9982-14b8f475c6d9"), + "preferred": False, + "share_instance_id": "6ba490c5-5225-4c3b-9982-14b8f475c6d9", + "is_admin_only": False +} + + +class TestShareExportLocations(base.TestCase): + + def test_basic(self): + export = el.ShareExportLocation() + self.assertEqual('export_locations', export.resources_key) + self.assertEqual( + '/shares/%(share_id)s/export_locations', export.base_path) + self.assertTrue(export.allow_list) + + def test_share_export_locations(self): + export = el.ShareExportLocation(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], export.id) + self.assertEqual(EXAMPLE['path'], export.path) + self.assertEqual(EXAMPLE['preferred'], export.is_preferred) + self.assertEqual( + EXAMPLE['share_instance_id'], export.share_instance_id) + self.assertEqual(EXAMPLE['is_admin_only'], export.is_admin) diff --git a/releasenotes/notes/add-shared-file-systems-export-location-a27c1741880c384b.yaml b/releasenotes/notes/add-shared-file-systems-export-location-a27c1741880c384b.yaml new file mode 100644 index 000000000..df1a1243a --- /dev/null +++ b/releasenotes/notes/add-shared-file-systems-export-location-a27c1741880c384b.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added support to list and show Export Locations + for shares from the Shared File Systems service. From c3e77fc61e8082f281ce3f37cef95b30b9180668 Mon Sep 17 00:00:00 2001 From: Reynaldo Bontje Date: Wed, 29 Mar 2023 14:36:54 -0500 Subject: [PATCH 3226/3836] Add resize/extend share actions. Adds the resize/extend actions from the Share actions API. Includes the resize_share method in the proxy. Change-Id: I9c852360b2e71f6e0a2cfd45c0a77690220379cd --- doc/source/user/guides/shared_file_system.rst | 13 +++ .../user/proxies/shared_file_system.rst | 2 +- examples/shared_file_system/shares.py | 33 +++++++ openstack/shared_file_system/v2/_proxy.py | 35 ++++++++ openstack/shared_file_system/v2/share.py | 54 ++++++++++- .../shared_file_system/test_share.py | 90 +++++++++++++++++++ .../unit/shared_file_system/v2/test_proxy.py | 24 +++++ .../unit/shared_file_system/v2/test_share.py | 62 ++++++++++++- ...-system-share-resize-ddd650c2e32fed34.yaml | 4 + 9 files changed, 312 insertions(+), 5 deletions(-) create mode 100644 examples/shared_file_system/shares.py create mode 100644 releasenotes/notes/add-shared-file-system-share-resize-ddd650c2e32fed34.yaml diff --git a/doc/source/user/guides/shared_file_system.rst b/doc/source/user/guides/shared_file_system.rst index 1aee6a36f..86c25a6b0 100644 --- a/doc/source/user/guides/shared_file_system.rst +++ b/doc/source/user/guides/shared_file_system.rst @@ -57,3 +57,16 @@ Force-deletes a share instance. .. literalinclude:: ../examples/shared_file_system/share_instances.py :pyobject: delete_share_instance + + +Resize Share +------------ + +Shared File System shares can be resized (extended or shrunk) to a given +size. For details on resizing shares, refer to the +`Manila docs `_. + +.. literalinclude:: ../examples/shared_file_system/shares.py + :pyobject: resize_share +.. literalinclude:: ../examples/shared_file_system/shares.py + :pyobject: resize_shares_without_shrink diff --git a/doc/source/user/proxies/shared_file_system.rst b/doc/source/user/proxies/shared_file_system.rst index 6d4a34ac9..53de3bdb2 100644 --- a/doc/source/user/proxies/shared_file_system.rst +++ b/doc/source/user/proxies/shared_file_system.rst @@ -33,7 +33,7 @@ service. .. autoclass:: openstack.shared_file_system.v2._proxy.Proxy :noindex: :members: shares, get_share, delete_share, update_share, create_share, - revert_share_to_snapshot + revert_share_to_snapshot, resize_share Shared File System Storage Pools diff --git a/examples/shared_file_system/shares.py b/examples/shared_file_system/shares.py new file mode 100644 index 000000000..9328b5048 --- /dev/null +++ b/examples/shared_file_system/shares.py @@ -0,0 +1,33 @@ +# 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. + + +def resize_share(conn, share_id, share_size): + # Be explicit about not wanting to use force if the share + # will be extended. + use_force = False + print('Resize the share to the given size:') + conn.share.resize_share(share_id, share_size, use_force) + + +def resize_shares_without_shrink(conn, min_size): + # Sometimes, extending shares without shrinking + # them (effectively setting a min size) is desirable. + + # Get list of shares from the connection. + shares = conn.share.shares() + + # Loop over the shares: + for share in shares: + # Extend shares smaller than min_size to min_size, + # but don't shrink shares larger than min_size. + conn.share.resize_share(share.id, min_size, no_shrink=True) diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index eae2b0994..a4f6492c0 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -158,6 +158,41 @@ def revert_share_to_snapshot(self, share_id, snapshot_id): res = self._get(_share.Share, share_id) res.revert_to_snapshot(self, snapshot_id) + def resize_share( + self, + share_id, + new_size, + no_shrink=False, + no_extend=False, + force=False + ): + """Resizes a share, extending/shrinking the share as needed. + + :param share_id: The ID of the share to resize + :param new_size: The new size of the share in GiBs. If new_size is + the same as the current size, then nothing is done. + :param bool no_shrink: If set to True, the given share is not shrunk, + even if shrinking the share is required to get the share to the + given size. This could be useful for extending shares to a minimum + size, while not shrinking shares to the given size. This defaults + to False. + :param bool no_extend: If set to True, the given share is not + extended, even if extending the share is required to get the share + to the given size. This could be useful for shrinking shares to a + maximum size, while not extending smaller shares to that maximum + size. This defaults to False. + :param bool force: Whether or not force should be used, + in the case where the share should be extended. + :returns: ``None`` + """ + + res = self._get(_share.Share, share_id) + + if new_size > res.size and no_extend is not True: + res.extend_share(self, new_size, force) + elif new_size < res.size and no_shrink is not True: + res.shrink_share(self, new_size) + def wait_for_status(self, res, status='active', failures=None, interval=2, wait=120): """Wait for a resource to be in a particular status. diff --git a/openstack/shared_file_system/v2/share.py b/openstack/shared_file_system/v2/share.py index 75cc636e2..526e60475 100644 --- a/openstack/shared_file_system/v2/share.py +++ b/openstack/shared_file_system/v2/share.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import exceptions from openstack import resource from openstack import utils @@ -102,12 +103,59 @@ class Share(resource.Resource): #: Display description for updating description display_description = resource.Body("display_description", type=str) - def _action(self, session, body): + def _action(self, session, body, action='patch', microversion=None): + """Perform share instance actions given the message body""" url = utils.urljoin(self.base_path, self.id, 'action') headers = {'Accept': ''} - session.post( - url, json=body, headers=headers) + + if microversion is None: + microversion = \ + self._get_microversion(session, action=action) + + response = session.post( + url, + json=body, + headers=headers, + microversion=microversion) + + exceptions.raise_from_response(response) + return response + + def extend_share(self, session, new_size, force=False): + """Extend the share size. + + :param float new_size: The new size of the share + in GiB. + :param bool force: Whether or not to use force, bypassing + the scheduler. Requires admin privileges. Defaults to False. + :returns: The result of the action. + :rtype: ``None`` + """ + + extend_body = {"new_size": new_size} + + if force is True: + extend_body['force'] = True + + body = {"extend": extend_body} + self._action(session, body) + + def shrink_share(self, session, new_size): + """Shrink the share size. + + :param float new_size: The new size of the share + in GiB. + :returns: ``None`` + """ + + body = {"shrink": {'new_size': new_size}} + self._action(session, body) def revert_to_snapshot(self, session, snapshot_id): + """Revert the share to the given snapshot. + + :param str snapshot_id: The id of the snapshot to revert to. + :returns: ``None`` + """ body = {"revert": {"snapshot_id": snapshot_id}} self._action(session, body) diff --git a/openstack/tests/functional/shared_file_system/test_share.py b/openstack/tests/functional/shared_file_system/test_share.py index a134140cd..4c9f309f3 100644 --- a/openstack/tests/functional/shared_file_system/test_share.py +++ b/openstack/tests/functional/shared_file_system/test_share.py @@ -24,6 +24,7 @@ def setUp(self): name=self.SHARE_NAME, size=2, share_type="dhss_false", share_protocol='NFS', description=None) self.SHARE_ID = my_share.id + self.SHARE_SIZE = my_share.size my_share_snapshot = self.create_share_snapshot( share_id=self.SHARE_ID ) @@ -60,3 +61,92 @@ def test_revert_share_to_snapshot(self): interval=5, wait=self._wait_for_timeout) self.assertIsNotNone(get_reverted_share.id) + + def test_resize_share_larger(self): + larger_size = 3 + self.user_cloud.share.resize_share( + self.SHARE_ID, larger_size) + + get_resized_share = self.user_cloud.share.get_share( + self.SHARE_ID) + + self.user_cloud.share.wait_for_status( + get_resized_share, + status='available', + failures=['error'], + interval=5, + wait=self._wait_for_timeout) + self.assertEqual(larger_size, get_resized_share.size) + + def test_resize_share_smaller(self): + # Resize to 3 GiB + smaller_size = 1 + + self.user_cloud.share.resize_share( + self.SHARE_ID, smaller_size) + + get_resized_share = self.user_cloud.share.get_share( + self.SHARE_ID) + + self.user_cloud.share.wait_for_status( + get_resized_share, + status='available', + failures=['error'], + interval=5, + wait=self._wait_for_timeout) + self.assertEqual(smaller_size, get_resized_share.size) + + def test_resize_share_larger_no_extend(self): + larger_size = 3 + + self.user_cloud.share.resize_share( + self.SHARE_ID, larger_size, no_extend=True) + + get_resized_share = self.user_cloud.share.get_share( + self.SHARE_ID) + + self.user_cloud.share.wait_for_status( + get_resized_share, + status='available', + failures=['error'], + interval=5, + wait=self._wait_for_timeout) + + # Assert that no change was made. + self.assertEqual(self.SHARE_SIZE, get_resized_share.size) + + def test_resize_share_smaller_no_shrink(self): + smaller_size = 1 + + self.user_cloud.share.resize_share( + self.SHARE_ID, smaller_size, no_shrink=True) + + get_resized_share = self.user_cloud.share.get_share( + self.SHARE_ID) + + self.user_cloud.share.wait_for_status( + get_resized_share, + status='available', + failures=['error'], + interval=5, + wait=self._wait_for_timeout) + + # Assert that no change was made. + self.assertEqual(self.SHARE_SIZE, get_resized_share.size) + + def test_resize_share_with_force(self): + # Resize to 3 GiB + larger_size = 3 + self.user_cloud.share.resize_share( + self.SHARE_ID, larger_size, force=True) + + get_resized_share = self.user_cloud.share.get_share( + self.SHARE_ID) + + self.user_cloud.share.wait_for_status( + get_resized_share, + status='available', + failures=['error'], + interval=5, + wait=self._wait_for_timeout) + self.assertEqual(larger_size, get_resized_share.size) diff --git a/openstack/tests/unit/shared_file_system/v2/test_proxy.py b/openstack/tests/unit/shared_file_system/v2/test_proxy.py index 364fe5a2e..9c2a00be3 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_proxy.py +++ b/openstack/tests/unit/shared_file_system/v2/test_proxy.py @@ -62,6 +62,30 @@ def test_share_create(self): def test_share_update(self): self.verify_update(self.proxy.update_share, share.Share) + def test_share_resize_extend(self): + mock_share = share.Share(size=10, id='fakeId') + self.proxy._get = mock.Mock(return_value=mock_share) + + self._verify( + "openstack.shared_file_system.v2.share." + + "Share.extend_share", + self.proxy.resize_share, + method_args=['fakeId', 20], + expected_args=[self.proxy, 20, False], + ) + + def test_share_resize_shrink(self): + mock_share = share.Share(size=30, id='fakeId') + self.proxy._get = mock.Mock(return_value=mock_share) + + self._verify( + "openstack.shared_file_system.v2.share." + + "Share.shrink_share", + self.proxy.resize_share, + method_args=['fakeId', 20], + expected_args=[self.proxy, 20], + ) + def test_share_instances(self): self.verify_list(self.proxy.share_instances, share_instance.ShareInstance) diff --git a/openstack/tests/unit/shared_file_system/v2/test_share.py b/openstack/tests/unit/shared_file_system/v2/test_share.py index 44e8fff3c..e19d5b51e 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_share.py +++ b/openstack/tests/unit/shared_file_system/v2/test_share.py @@ -10,13 +10,18 @@ # License for the specific language governing permissions and limitations # under the License. +from unittest import mock + +from keystoneauth1 import adapter + from openstack.shared_file_system.v2 import share from openstack.tests.unit import base + IDENTIFIER = '08a87d37-5ca2-4308-86c5-cba06d8d796c' EXAMPLE = { "id": IDENTIFIER, - "size": 1, + "size": 2, "availability_zone": "manila-zone-1", "created_at": "2021-02-11T17:38:00.000000", "status": "available", @@ -109,3 +114,58 @@ def test_make_shares(self): self.assertEqual(EXAMPLE['share_server_id'], shares_resource.share_server_id) self.assertEqual(EXAMPLE['host'], shares_resource.host) + + +class TestShareActions(TestShares): + def setUp(self): + super().setUp() + self.resp = mock.Mock() + self.resp.body = None + self.resp.status_code = 202 + self.resp.json = mock.Mock(return_value=self.resp.body) + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.default_microversion = '3.0' + self.sess.post = mock.Mock(return_value=self.resp) + self.sess._get_connection = mock.Mock(return_value=self.cloud) + + def test_shrink_share(self): + sot = share.Share(**EXAMPLE) + microversion = sot._get_microversion(self.sess, action='patch') + + self.assertIsNone(sot.shrink_share(self.sess, new_size=1)) + + url = f'shares/{IDENTIFIER}/action' + body = {"shrink": {"new_size": 1}} + headers = {'Accept': ''} + + self.sess.post.assert_called_with( + url, json=body, headers=headers, + microversion=microversion) + + def test_extend_share(self): + sot = share.Share(**EXAMPLE) + microversion = sot._get_microversion(self.sess, action='patch') + + self.assertIsNone(sot.extend_share(self.sess, new_size=3)) + + url = f'shares/{IDENTIFIER}/action' + body = {"extend": {"new_size": 3}} + headers = {'Accept': ''} + + self.sess.post.assert_called_with( + url, json=body, headers=headers, + microversion=microversion) + + def test_revert_to_snapshot(self): + sot = share.Share(**EXAMPLE) + microversion = sot._get_microversion(self.sess, action='patch') + + self.assertIsNone(sot.revert_to_snapshot(self.sess, "fake_id")) + + url = f'shares/{IDENTIFIER}/action' + body = {"revert": {"snapshot_id": "fake_id"}} + headers = {'Accept': ''} + + self.sess.post.assert_called_with( + url, json=body, headers=headers, + microversion=microversion) diff --git a/releasenotes/notes/add-shared-file-system-share-resize-ddd650c2e32fed34.yaml b/releasenotes/notes/add-shared-file-system-share-resize-ddd650c2e32fed34.yaml new file mode 100644 index 000000000..b1adea626 --- /dev/null +++ b/releasenotes/notes/add-shared-file-system-share-resize-ddd650c2e32fed34.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Added support for shrink/extend share actions. From aab02350650be4e49b2fca36d69df66da6feab40 Mon Sep 17 00:00:00 2001 From: Reynaldo Bontje Date: Tue, 21 Mar 2023 10:51:56 -0500 Subject: [PATCH 3227/3836] Allow key overrides in create and fetch methods Provides override arguments so that subresources that require different envelope/resource keys for requests/responses don't need to override base resource methods in a hacky manner. It also makes it so subresources don't need to "reinvent the wheel" as often by making custom request/response processing. These two patterns are relatively common and are often caused by the need to override these resource/envelope keys. For the sake of consistency across the SDK, this patch moves the logic needed in these cases into the base resource. Change-Id: I9d928a55539c17ab54d983165afc7912cd0926f8 --- openstack/resource.py | 88 ++++++++++++++++----- openstack/tests/unit/test_resource.py | 107 ++++++++++++++++++++++++-- 2 files changed, 171 insertions(+), 24 deletions(-) diff --git a/openstack/resource.py b/openstack/resource.py index a0a484136..54bbe8e5c 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1152,7 +1152,13 @@ def _pack_attrs_under_properties(self, body, attrs): body['properties'] = props return body - def _prepare_request_body(self, patch, prepend_key): + def _prepare_request_body( + self, + patch, + prepend_key, + *, + resource_request_key=None, + ): if patch: if not self._store_unknown_attrs_as_properties: # Default case @@ -1180,8 +1186,11 @@ def _prepare_request_body(self, patch, prepend_key): self._body.dirty ) - if prepend_key and self.resource_key is not None: - body = {self.resource_key: body} + if prepend_key: + if resource_request_key is not None: + body = {resource_request_key: body} + elif self.resource_key is not None: + body = {self.resource_key: body} return body def _prepare_request( @@ -1191,6 +1200,8 @@ def _prepare_request( patch=False, base_path=None, params=None, + *, + resource_request_key=None, **kwargs, ): """Prepare a request to be sent to the server @@ -1210,7 +1221,14 @@ def _prepare_request( if requires_id is None: requires_id = self.requires_id - body = self._prepare_request_body(patch, prepend_key) + # Conditionally construct arguments for _prepare_request_body + request_kwargs = { + "patch": patch, + "prepend_key": prepend_key + } + if resource_request_key is not None: + request_kwargs['resource_request_key'] = resource_request_key + body = self._prepare_request_body(**request_kwargs) # TODO(mordred) Ensure headers have string values better than this headers = {} @@ -1237,7 +1255,14 @@ def _prepare_request( return _Request(uri, body, headers) - def _translate_response(self, response, has_body=None, error_message=None): + def _translate_response( + self, + response, + has_body=None, + error_message=None, + *, + resource_response_key=None, + ): """Given a KSA response, inflate this instance with its data DELETE operations don't return a body, so only try to work @@ -1254,7 +1279,9 @@ def _translate_response(self, response, has_body=None, error_message=None): if has_body: try: body = response.json() - if self.resource_key and self.resource_key in body: + if resource_response_key and resource_response_key in body: + body = body[resource_response_key] + elif self.resource_key and self.resource_key in body: body = body[self.resource_key] # Do not allow keys called "self" through. Glance chose @@ -1412,6 +1439,8 @@ def create( prepend_key=True, base_path=None, *, + resource_request_key=None, + resource_response_key=None, microversion=None, **params ): @@ -1424,6 +1453,12 @@ def create( True. :param str base_path: Base part of the URI for creating resources, if different from :data:`~openstack.resource.Resource.base_path`. + :param str resource_request_key: Overrides the usage of + self.resource_key when prepending a key to the request body. + Ignored if `prepend_key` is false. + :param str resource_response_key: Overrides the usage of + self.resource_key when processing response bodies. + Ignored if `prepend_key` is false. :param str microversion: API version to override the negotiated one. :param dict params: Additional params to pass. :return: This :class:`Resource` instance. @@ -1442,15 +1477,20 @@ def create( else self.create_method == 'PUT' ) + # Construct request arguments. + request_kwargs = { + "requires_id": requires_id, + "prepend_key": prepend_key, + "base_path": base_path, + } + if resource_request_key is not None: + request_kwargs['resource_request_key'] = resource_request_key + if self.create_exclude_id_from_body: self._body._dirty.discard("id") if self.create_method == 'PUT': - request = self._prepare_request( - requires_id=requires_id, - prepend_key=prepend_key, - base_path=base_path, - ) + request = self._prepare_request(**request_kwargs) response = session.put( request.url, json=request.body, @@ -1459,11 +1499,7 @@ def create( params=params, ) elif self.create_method == 'POST': - request = self._prepare_request( - requires_id=requires_id, - prepend_key=prepend_key, - base_path=base_path, - ) + request = self._prepare_request(**request_kwargs) response = session.post( request.url, json=request.body, @@ -1482,11 +1518,22 @@ def create( else self.create_returns_body ) self.microversion = microversion - self._translate_response(response, has_body=has_body) + + response_kwargs = { + "has_body": has_body, + } + if resource_response_key is not None: + response_kwargs['resource_response_key'] = resource_response_key + + self._translate_response(response, **response_kwargs) # direct comparision to False since we need to rule out None if self.has_body and self.create_returns_body is False: # fetch the body if it's required but not returned by create - return self.fetch(session) + fetch_kwargs = {} + if resource_response_key is not None: + fetch_kwargs = \ + {'resource_response_key': resource_response_key} + return self.fetch(session, **fetch_kwargs) return self @classmethod @@ -1603,6 +1650,7 @@ def fetch( error_message=None, skip_cache=False, *, + resource_response_key=None, microversion=None, **params, ): @@ -1618,6 +1666,8 @@ def fetch( requested object does not exist. :param bool skip_cache: A boolean indicating whether optional API cache should be skipped for this invocation. + :param str resource_response_key: Overrides the usage of + self.resource_key when processing the response body. :param str microversion: API version to override the negotiated one. :param dict params: Additional parameters that can be consumed. :return: This :class:`Resource` instance. @@ -1646,6 +1696,8 @@ def fetch( kwargs['error_message'] = error_message self.microversion = microversion + if resource_response_key is not None: + kwargs['resource_response_key'] = resource_response_key self._translate_response(response, **kwargs) return self diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index ed73a4d24..259eff672 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1095,7 +1095,7 @@ def test__prepare_request_missing_id(self): self.assertRaises(exceptions.InvalidRequest, sot._prepare_request, requires_id=True) - def test__prepare_request_with_key(self): + def test__prepare_request_with_resource_key(self): key = "key" class Test(resource.Resource): @@ -1115,6 +1115,30 @@ class Test(resource.Resource): self.assertEqual({key: {"x": body_value}}, result.body) self.assertEqual({"y": header_value}, result.headers) + def test__prepare_request_with_override_key(self): + default_key = "key" + override_key = "other_key" + + class Test(resource.Resource): + base_path = "/something" + resource_key = default_key + body_attr = resource.Body("x") + header_attr = resource.Header("y") + + body_value = "body" + header_value = "header" + sot = Test(body_attr=body_value, header_attr=header_value, + _synchronized=False) + + result = sot._prepare_request( + requires_id=False, + prepend_key=True, + resource_request_key=override_key) + + self.assertEqual("/something", result.url) + self.assertEqual({override_key: {"x": body_value}}, result.body) + self.assertEqual({"y": header_value}, result.headers) + def test__prepare_request_with_patch(self): class Test(resource.Resource): commit_jsonpatch = True @@ -1528,7 +1552,8 @@ class Test(resource.Resource): def _test_create(self, cls, requires_id=False, prepend_key=False, microversion=None, base_path=None, params=None, - id_marked_dirty=True, explicit_microversion=None): + id_marked_dirty=True, explicit_microversion=None, + resource_request_key=None, resource_response_key=None): id = "id" if requires_id else None sot = cls(id=id) sot._prepare_request = mock.Mock(return_value=self.request) @@ -1540,14 +1565,20 @@ def _test_create(self, cls, requires_id=False, prepend_key=False, kwargs['microversion'] = explicit_microversion microversion = explicit_microversion result = sot.create(self.session, prepend_key=prepend_key, - base_path=base_path, **kwargs) + base_path=base_path, + resource_request_key=resource_request_key, + resource_response_key=resource_response_key, + **kwargs) id_is_dirty = ('id' in sot._body._dirty) self.assertEqual(id_marked_dirty, id_is_dirty) + prepare_kwargs = {} + if resource_request_key is not None: + prepare_kwargs['resource_request_key'] = resource_request_key sot._prepare_request.assert_called_once_with( requires_id=requires_id, prepend_key=prepend_key, - base_path=base_path) + base_path=base_path, **prepare_kwargs) if requires_id: self.session.put.assert_called_once_with( self.request.url, @@ -1560,8 +1591,13 @@ def _test_create(self, cls, requires_id=False, prepend_key=False, microversion=microversion, params=params) self.assertEqual(sot.microversion, microversion) - sot._translate_response.assert_called_once_with(self.response, - has_body=sot.has_body) + res_kwargs = {} + if resource_response_key is not None: + res_kwargs['resource_response_key'] = resource_response_key + sot._translate_response.assert_called_once_with( + self.response, + has_body=sot.has_body, + **res_kwargs) self.assertEqual(result, sot) def test_put_create(self): @@ -1625,6 +1661,49 @@ class Test(resource.Resource): self._test_create(Test, requires_id=False, prepend_key=True) + def test_post_create_override_request_key(self): + class Test(resource.Resource): + service = self.service_name + base_path = self.base_path + allow_create = True + create_method = 'POST' + resource_key = 'SomeKey' + + self._test_create( + Test, + requires_id=False, + prepend_key=True, + resource_request_key="OtherKey") + + def test_post_create_override_response_key(self): + class Test(resource.Resource): + service = self.service_name + base_path = self.base_path + allow_create = True + create_method = 'POST' + resource_key = 'SomeKey' + + self._test_create( + Test, + requires_id=False, + prepend_key=True, + resource_response_key="OtherKey") + + def test_post_create_override_key_both(self): + class Test(resource.Resource): + service = self.service_name + base_path = self.base_path + allow_create = True + create_method = 'POST' + resource_key = 'SomeKey' + + self._test_create( + Test, + requires_id=False, + prepend_key=True, + resource_request_key="OtherKey", + resource_response_key="SomeOtherKey") + def test_post_create_base_path(self): class Test(resource.Resource): service = self.service_name @@ -1658,6 +1737,22 @@ def test_fetch(self): self.sot._translate_response.assert_called_once_with(self.response) self.assertEqual(result, self.sot) + def test_fetch_with_override_key(self): + result = self.sot.fetch( + self.session, resource_response_key="SomeKey") + + self.sot._prepare_request.assert_called_once_with( + requires_id=True, base_path=None) + self.session.get.assert_called_once_with( + self.request.url, microversion=None, params={}, + skip_cache=False) + + self.assertIsNone(self.sot.microversion) + self.sot._translate_response.assert_called_once_with( + self.response, + resource_response_key="SomeKey") + self.assertEqual(result, self.sot) + def test_fetch_with_params(self): result = self.sot.fetch(self.session, fields='a,b') From 7f87b6dd9520ecfc9047696d16f1e8d7b0afe86d Mon Sep 17 00:00:00 2001 From: ashrod98 Date: Sun, 28 Mar 2021 19:45:58 +0000 Subject: [PATCH 3228/3836] Add share network subnet resource to shared file system Introduce Share Network Subnet Class with basic methods including list, get, create and delete to shared file system storage service. Co-Authored-By: Reynaldo Bontje Change-Id: I781eb8d19c6af8097c9de4293bb975361e524344 --- .../user/proxies/shared_file_system.rst | 13 +++ .../resources/shared_file_system/index.rst | 1 + .../v2/share_network_subnet.rst | 13 +++ openstack/shared_file_system/v2/_proxy.py | 91 +++++++++++++++++-- .../v2/share_network_subnet.py | 62 +++++++++++++ .../test_share_network_subnet.py | 75 +++++++++++++++ .../unit/shared_file_system/v2/test_proxy.py | 44 +++++++++ .../v2/test_share_network_subnet.py | 65 +++++++++++++ ...ubnet-to-shared-file-b5de3ce6ca723209.yaml | 5 + 9 files changed, 360 insertions(+), 9 deletions(-) create mode 100644 doc/source/user/resources/shared_file_system/v2/share_network_subnet.rst create mode 100644 openstack/shared_file_system/v2/share_network_subnet.py create mode 100644 openstack/tests/functional/shared_file_system/test_share_network_subnet.py create mode 100644 openstack/tests/unit/shared_file_system/v2/test_share_network_subnet.py create mode 100644 releasenotes/notes/add-share-network-subnet-to-shared-file-b5de3ce6ca723209.yaml diff --git a/doc/source/user/proxies/shared_file_system.rst b/doc/source/user/proxies/shared_file_system.rst index 53de3bdb2..96897c408 100644 --- a/doc/source/user/proxies/shared_file_system.rst +++ b/doc/source/user/proxies/shared_file_system.rst @@ -101,6 +101,7 @@ Create and manipulate Share Networks with the Shared File Systems service. :members: share_networks, get_share_network, delete_share_network, update_share_network, create_share_network + Shared File System Share Instances ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -113,3 +114,15 @@ Systems Service. :members: share_instances, get_share_instance, reset_share_instance_status, delete_share_instance + + +Shared File System Share Network Subnets +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Create and manipulate Share Network Subnets with the Shared File Systems +service. + +.. autoclass:: openstack.shared_file_system.v2._proxy.Proxy + :noindex: + :members: share_network_subnets, get_share_network_subnet, + create_share_network_subnet, delete_share_network_subnet diff --git a/doc/source/user/resources/shared_file_system/index.rst b/doc/source/user/resources/shared_file_system/index.rst index 56d40c533..8c60006f7 100644 --- a/doc/source/user/resources/shared_file_system/index.rst +++ b/doc/source/user/resources/shared_file_system/index.rst @@ -9,6 +9,7 @@ Shared File System service resources v2/limit v2/share v2/share_instance + v2/share_network_subnet v2/share_snapshot v2/share_snapshot_instance v2/share_network diff --git a/doc/source/user/resources/shared_file_system/v2/share_network_subnet.rst b/doc/source/user/resources/shared_file_system/v2/share_network_subnet.rst new file mode 100644 index 000000000..95638a60b --- /dev/null +++ b/doc/source/user/resources/shared_file_system/v2/share_network_subnet.rst @@ -0,0 +1,13 @@ +openstack.shared_file_system.v2.share_network_subnet +==================================================== + +.. automodule:: openstack.shared_file_system.v2.share_network_subnet + +The ShareNetworkSubnet Class +---------------------------- + +The ``ShareNetworkSubnet`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.shared_file_system.v2.share_network_subnet.ShareNetworkSubnet + :members: diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index a9a7d0609..e9c801914 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -20,6 +20,9 @@ from openstack.shared_file_system.v2 import ( share_network as _share_network ) +from openstack.shared_file_system.v2 import ( + share_network_subnet as _share_network_subnet +) from openstack.shared_file_system.v2 import ( share_snapshot as _share_snapshot ) @@ -46,6 +49,7 @@ class Proxy(proxy.Proxy): "limit": _limit.Limit, "share": _share.Share, "share_network": _share_network.ShareNetwork, + "share_network_subnet": _share_network_subnet.ShareNetworkSubnet, "share_snapshot_instance": _share_snapshot_instance.ShareSnapshotInstance, "share_instance": _share_instance.ShareInstance, @@ -316,8 +320,8 @@ def share_snapshots(self, details=True, **query): * project_id: The ID of the user or service making the API request. :returns: A generator of manila share snapshot resources - :rtype: :class:`~openstack.shared_file_system.v2. - share_snapshot.ShareSnapshot` + :rtype: + :class:`~openstack.shared_file_system.v2.share_snapshot.ShareSnapshot` """ base_path = '/snapshots/detail' if details else None return self._list( @@ -328,8 +332,8 @@ def get_share_snapshot(self, snapshot_id): :param snapshot_id: The ID of the snapshot to get :returns: Details of the identified share snapshot - :rtype: :class:`~openstack.shared_file_system.v2. - share_snapshot.ShareSnapshot` + :rtype: + :class:`~openstack.shared_file_system.v2.share_snapshot.ShareSnapshot` """ return self._get(_share_snapshot.ShareSnapshot, snapshot_id) @@ -337,8 +341,8 @@ def create_share_snapshot(self, **attrs): """Creates a share snapshot from attributes :returns: Details of the new share snapshot - :rtype: :class:`~openstack.shared_file_system.v2. - share_snapshot.ShareSnapshot` + :rtype: + :class:`~openstack.shared_file_system.v2.share_snapshot.ShareSnapshot` """ return self._create(_share_snapshot.ShareSnapshot, **attrs) @@ -348,8 +352,8 @@ def update_share_snapshot(self, snapshot_id, **attrs): :param snapshot_id: The ID of the snapshot to update :pram dict attrs: The attributes to update on the snapshot :returns: the updated share snapshot - :rtype: :class:`~openstack.shared_file_system.v2. - share_snapshot.ShareSnapshot` + :rtype: + :class:`~openstack.shared_file_system.v2.share_snapshot.ShareSnapshot` """ return self._update(_share_snapshot.ShareSnapshot, snapshot_id, **attrs) @@ -364,8 +368,77 @@ def delete_share_snapshot(self, snapshot_id, ignore_missing=True): self._delete(_share_snapshot.ShareSnapshot, snapshot_id, ignore_missing=ignore_missing) + # ========= Network Subnets ========== + def share_network_subnets(self, share_network_id): + """Lists all share network subnets with details. + + :param share_network_id: The id of the share network for which + Share Network Subnets should be listed. + :returns: A generator of manila share network subnets + :rtype: + :class:`~openstack.shared_file_system.v2.share_network_subnet.ShareNetworkSubnet` + """ + return self._list( + _share_network_subnet.ShareNetworkSubnet, + share_network_id=share_network_id) + + def get_share_network_subnet( + self, share_network_id, share_network_subnet_id, + ): + """Lists details of a single share network subnet. + + :param share_network_id: The id of the share network associated + with the Share Network Subnet. + :param share_network_subnet_id: The id of the Share Network Subnet + to retrieve. + :returns: Details of the identified share network subnet + :rtype: + :class:`~openstack.shared_file_system.v2.share_network_subnet.ShareNetworkSubnet` + """ + + return self._get( + _share_network_subnet.ShareNetworkSubnet, + share_network_subnet_id, + share_network_id=share_network_id) + + def create_share_network_subnet(self, share_network_id, **attrs): + """Creates a share network subnet from attributes + + :param share_network_id: The id of the share network wthin which the + the Share Network Subnet should be created. + :param dict attrs: Attributes which will be used to create + a share network subnet. + :returns: Details of the new share network subnet. + :rtype: + :class:`~openstack.shared_file_system.v2.share_network_subnet.ShareNetworkSubnet` + """ + return self._create( + _share_network_subnet.ShareNetworkSubnet, + **attrs, + share_network_id=share_network_id) + + def delete_share_network_subnet( + self, share_network_id, share_network_subnet, ignore_missing=True + ): + """Deletes a share network subnet. + + :param share_network_id: The id of the Share Network associated with + the Share Network Subnet. + :param share_network_subnet: The id of the Share Network Subnet + which should be deleted. + :returns: Result of the ``delete`` + :rtype: None + """ + + self._delete( + _share_network_subnet.ShareNetworkSubnet, + share_network_subnet, + share_network_id=share_network_id, + ignore_missing=ignore_missing) + def wait_for_delete(self, res, interval=2, wait=120): """Wait for a resource to be deleted. + :param res: The resource to wait on to be deleted. :type resource: A :class:`~openstack.resource.Resource` object. :param interval: Number of seconds to wait before to consecutive @@ -374,7 +447,7 @@ def wait_for_delete(self, res, interval=2, wait=120): Default to 120. :returns: The resource is returned on success. :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition - to delete failed to occur in the specified seconds. + to delete failed to occur in the specified seconds. """ return resource.wait_for_delete(self, res, interval, wait) diff --git a/openstack/shared_file_system/v2/share_network_subnet.py b/openstack/shared_file_system/v2/share_network_subnet.py new file mode 100644 index 000000000..7dc3cdacd --- /dev/null +++ b/openstack/shared_file_system/v2/share_network_subnet.py @@ -0,0 +1,62 @@ +# 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 resource + + +class ShareNetworkSubnet(resource.Resource): + resource_key = "share_network_subnet" + resources_key = "share_network_subnets" + base_path = "/share-networks/%(share_network_id)s/subnets" + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = False + allow_delete = True + allow_list = True + + #: Properties + #: The share nerwork ID, part of the URI for share network subnets. + share_network_id = resource.URI("share_network_id", type=str) + + #: The name of the availability zone that the share network + #: subnet belongs to. + availability_zone = resource.Body("availability_zone", type=str) + #: The IP block from which to allocate the network, in CIDR notation. + cidr = resource.Body("cidr", type=str) + #: Date and time the share network subnet was created at. + created_at = resource.Body("created_at") + #: The gateway of a share network subnet. + gateway = resource.Body("gateway", type=str) + #: The IP version of the network. + ip_version = resource.Body("ip_version", type=int) + #: The MTU of a share network subnet. + mtu = resource.Body("mtu", type=str) + #: The network type. A valid value is VLAN, VXLAN, GRE, or flat + network_type = resource.Body("network_type", type=str) + #: The name of the neutron network. + neutron_net_id = resource.Body("neutron_net_id", type=str) + #: The ID of the neitron subnet. + neutron_subnet_id = resource.Body("neutron_subnet_id", type=str) + #: The segmentation ID. + segmentation_id = resource.Body('segmentation_id', type=int) + #: The name of the share network that the share network subnet belongs to. + share_network_name = resource.Body("share_network_name", type=str) + #: Date and time the share network subnet was last updated at. + updated_at = resource.Body("updated_at", type=str) + + def create(self, session, **kwargs): + return super().\ + create(session, + resource_request_key='share-network-subnet', + **kwargs) diff --git a/openstack/tests/functional/shared_file_system/test_share_network_subnet.py b/openstack/tests/functional/shared_file_system/test_share_network_subnet.py new file mode 100644 index 000000000..8f9c5b839 --- /dev/null +++ b/openstack/tests/functional/shared_file_system/test_share_network_subnet.py @@ -0,0 +1,75 @@ +# 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.shared_file_system.v2 import ( + share_network_subnet as _share_network_subnet +) +from openstack.tests.functional.shared_file_system import base + + +class ShareNetworkSubnetTest(base.BaseSharedFileSystemTest): + + def setUp(self): + super().setUp() + + zones = self.user_cloud.shared_file_system.\ + availability_zones() + first_zone = next(zones) + + self.SHARE_NETWORK_NAME = self.getUniqueString() + snt = self.user_cloud.shared_file_system.create_share_network( + name=self.SHARE_NETWORK_NAME) + self.assertIsNotNone(snt) + self.assertIsNotNone(snt.id) + self.SHARE_NETWORK_ID = snt.id + snsb = self.user_cloud.shared_file_system.\ + create_share_network_subnet(self.SHARE_NETWORK_ID, + availability_zone=first_zone.name) + self.assertIsNotNone(snsb) + self.assertIsNotNone(snsb.id) + self.SHARE_NETWORK_SUBNET_ID = snsb.id + + def tearDown(self): + subnet = self.user_cloud.shared_file_system.\ + get_share_network_subnet(self.SHARE_NETWORK_ID, + self.SHARE_NETWORK_SUBNET_ID) + fdel = self.user_cloud.shared_file_system.\ + delete_share_network_subnet(self.SHARE_NETWORK_ID, + self.SHARE_NETWORK_SUBNET_ID, + ignore_missing=True) + self.assertIsNone(fdel) + self.user_cloud.shared_file_system.\ + wait_for_delete(subnet) + sot = self.user_cloud.shared_file_system.\ + delete_share_network(self.SHARE_NETWORK_ID, + ignore_missing=True) + self.assertIsNone(sot) + super().tearDown() + + def test_get(self): + sub = self.user_cloud.shared_file_system.\ + get_share_network_subnet(self.SHARE_NETWORK_ID, + self.SHARE_NETWORK_SUBNET_ID) + assert isinstance(sub, _share_network_subnet.ShareNetworkSubnet) + + def test_list(self): + subs = self.user_cloud.shared_file_system.share_network_subnets( + self.SHARE_NETWORK_ID) + self.assertGreater(len(list(subs)), 0) + for sub in subs: + for attribute in ('id', 'name', 'created_at', 'updated_at', + 'share_network_id', 'availability_zone', + 'cidr', 'gateway', 'ip_version', 'mtu', + 'network_type', 'neutron_net_id', + 'neutron_subnet_id', 'segmentation_id', + 'share_network_name'): + self.assertTrue(hasattr(sub, attribute)) diff --git a/openstack/tests/unit/shared_file_system/v2/test_proxy.py b/openstack/tests/unit/shared_file_system/v2/test_proxy.py index 9c2a00be3..9748866ce 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_proxy.py +++ b/openstack/tests/unit/shared_file_system/v2/test_proxy.py @@ -17,6 +17,7 @@ from openstack.shared_file_system.v2 import share from openstack.shared_file_system.v2 import share_instance from openstack.shared_file_system.v2 import share_network +from openstack.shared_file_system.v2 import share_network_subnet from openstack.shared_file_system.v2 import share_snapshot from openstack.shared_file_system.v2 import share_snapshot_instance from openstack.shared_file_system.v2 import storage_pool @@ -299,3 +300,46 @@ def test_share_network_create(self): def test_share_network_update(self): self.verify_update( self.proxy.update_share_network, share_network.ShareNetwork) + + +class TestShareNetworkSubnetResource(test_proxy_base.TestProxyBase): + + def setUp(self): + super(TestShareNetworkSubnetResource, self).setUp() + self.proxy = _proxy.Proxy(self.session) + + def test_share_network_subnets(self): + self.verify_list( + self.proxy.share_network_subnets, + share_network_subnet.ShareNetworkSubnet, + method_args=["test_share"], + expected_args=[], + expected_kwargs={"share_network_id": "test_share"}) + + def test_share_network_subnet_get(self): + self.verify_get( + self.proxy.get_share_network_subnet, + share_network_subnet.ShareNetworkSubnet, + method_args=["fake_network_id", "fake_sub_network_id"], + expected_args=['fake_sub_network_id'], + expected_kwargs={'share_network_id': 'fake_network_id'}) + + def test_share_network_subnet_create(self): + self.verify_create( + self.proxy.create_share_network_subnet, + share_network_subnet.ShareNetworkSubnet, + method_args=["fake_network_id"], + method_kwargs={"p1": "v1"}, + expected_args=[], + expected_kwargs={ + "share_network_id": "fake_network_id", + "p1": "v1"}) + + def test_share_network_subnet_delete(self): + self.verify_delete( + self.proxy.delete_share_network_subnet, + share_network_subnet.ShareNetworkSubnet, + False, + method_args=["fake_network_id", "fake_sub_network_id"], + expected_args=["fake_sub_network_id"], + expected_kwargs={'share_network_id': 'fake_network_id'}) diff --git a/openstack/tests/unit/shared_file_system/v2/test_share_network_subnet.py b/openstack/tests/unit/shared_file_system/v2/test_share_network_subnet.py new file mode 100644 index 000000000..30eddaf48 --- /dev/null +++ b/openstack/tests/unit/shared_file_system/v2/test_share_network_subnet.py @@ -0,0 +1,65 @@ +# 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.shared_file_system.v2 import share_network_subnet as SNS +from openstack.tests.unit import base + +IDENTIFIER = '9cd5a59f-4d22-496f-8b1a-ea4860c24d39' +EXAMPLE = { + "id": IDENTIFIER, + "availability_zone": None, + "share_network_id": "652ef887-b805-4328-b65a-b88c64cb69ec", + "share_network_name": None, + "created_at": "2021-02-24T02:45:59.000000", + "segmentation_id": None, + "neutron_subnet_id": None, + "updated_at": None, + "neutron_net_id": None, + "ip_version": None, + "cidr": None, + "network_type": None, + "mtu": None, + "gateway": None +} + + +class TestShareNetworkSubnet(base.TestCase): + + def test_basic(self): + SNS_resource = SNS.ShareNetworkSubnet() + self.assertEqual('share_network_subnets', SNS_resource.resources_key) + self.assertEqual('/share-networks/%(share_network_id)s/subnets', + SNS_resource.base_path) + self.assertTrue(SNS_resource.allow_list) + + def test_make_share_network_subnet(self): + SNS_resource = SNS.ShareNetworkSubnet(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], SNS_resource.id) + self.assertEqual(EXAMPLE['availability_zone'], + SNS_resource.availability_zone) + self.assertEqual(EXAMPLE['share_network_id'], + SNS_resource.share_network_id) + self.assertEqual(EXAMPLE['share_network_name'], + SNS_resource.share_network_name) + self.assertEqual(EXAMPLE['created_at'], SNS_resource.created_at) + self.assertEqual(EXAMPLE['segmentation_id'], + SNS_resource.segmentation_id) + self.assertEqual(EXAMPLE['neutron_subnet_id'], + SNS_resource.neutron_subnet_id) + self.assertEqual(EXAMPLE['updated_at'], SNS_resource.updated_at) + self.assertEqual(EXAMPLE['neutron_net_id'], + SNS_resource.neutron_net_id) + self.assertEqual(EXAMPLE['ip_version'], SNS_resource.ip_version) + self.assertEqual(EXAMPLE['cidr'], SNS_resource.cidr) + self.assertEqual(EXAMPLE['network_type'], SNS_resource.network_type) + self.assertEqual(EXAMPLE['mtu'], SNS_resource.mtu) + self.assertEqual(EXAMPLE['gateway'], SNS_resource.gateway) diff --git a/releasenotes/notes/add-share-network-subnet-to-shared-file-b5de3ce6ca723209.yaml b/releasenotes/notes/add-share-network-subnet-to-shared-file-b5de3ce6ca723209.yaml new file mode 100644 index 000000000..c61e18c6d --- /dev/null +++ b/releasenotes/notes/add-share-network-subnet-to-shared-file-b5de3ce6ca723209.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added support to create, list, get, and delete share network + subnets on the shared file system service. From 9153d6704ba648136cf0f67e1ccdf4cbb7a417c7 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 20 Apr 2023 15:49:26 +0100 Subject: [PATCH 3229/3836] image: Don't envelope properties The Glance v2 API is weird, in that it allows and places arbitrary properties in the root envelope of requests and responses respectively rather than under a 'properties' or 'metadata' key like other services. We were using a properties key temporarily as an envelope so we can format these extra properties specifically. However, we were forgetting to remove this envelope when creating an bare image without uploading data. Correct this. Change-Id: Idd2dd85e2fee364348328b5f25096f565cd47241 Signed-off-by: Stephen Finucane Story: 2009127 Task: 43041 --- openstack/image/v2/_proxy.py | 2 ++ openstack/tests/functional/image/v2/test_image.py | 5 ----- openstack/tests/unit/image/v2/test_proxy.py | 12 +++++++----- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index cf00639dd..0ef6c9af3 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -318,6 +318,8 @@ def create_image( **image_kwargs, ) else: + properties = image_kwargs.pop('properties', {}) + image_kwargs.update(self._make_v2_image_params(meta, properties)) image_kwargs['name'] = name image = self._create(_image.Image, **image_kwargs) diff --git a/openstack/tests/functional/image/v2/test_image.py b/openstack/tests/functional/image/v2/test_image.py index 359428f48..9e70048c6 100644 --- a/openstack/tests/functional/image/v2/test_image.py +++ b/openstack/tests/functional/image/v2/test_image.py @@ -28,11 +28,6 @@ def setUp(self): name=TEST_IMAGE_NAME, disk_format='raw', container_format='bare', - # TODO(mordred): This is not doing what people think it is doing. - # This is EPICLY broken. However, rather than fixing it as it is, - # we need to just replace the image upload code with the stuff - # from shade. Figuring out mapping the crap-tastic arbitrary - # extra key-value pairs into Resource is going to be fun. properties={ 'description': 'This is not an image', }, diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 4a98e2632..d7f12b9b9 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -95,7 +95,7 @@ def test_image_create_conflicting_options(self): ) self.assertIn('filename and data are mutually exclusive', str(exc)) - def test_image_create_minimal(self): + def test_image_create(self): self.verify_create( self.proxy.create_image, _image.Image, @@ -104,12 +104,16 @@ def test_image_create_minimal(self): 'disk_format': 'fake_dformat', 'container_format': 'fake_cformat', 'allow_duplicates': True, + 'is_protected': True, }, expected_kwargs={ 'name': 'fake', 'disk_format': 'fake_dformat', 'container_format': 'fake_cformat', - 'properties': mock.ANY, + 'is_protected': True, + 'owner_specified.openstack.md5': '', + 'owner_specified.openstack.object': 'images/fake', + 'owner_specified.openstack.sha256': '', }, ) @@ -186,9 +190,7 @@ def test_image_create_validate_checksum_data_binary(self): meta={}, properties={ self.proxy._IMAGE_MD5_KEY: '144c9defac04969c7bfad8efaa8ea194', - self.proxy._IMAGE_SHA256_KEY: 'b5d54c39e66671c9731b9f471e585' - 'd8262cd4f54963f0c93082d8dcf33' - '4d4c78', + self.proxy._IMAGE_SHA256_KEY: 'b5d54c39e66671c9731b9f471e585d8262cd4f54963f0c93082d8dcf334d4c78', # noqa: E501 self.proxy._IMAGE_OBJECT_KEY: 'bare/fake', }, timeout=3600, From 0bc0d4b83204eb8a755d8e92663a430721b7bd8e Mon Sep 17 00:00:00 2001 From: Jay Faulkner Date: Fri, 21 Apr 2023 15:00:29 -0700 Subject: [PATCH 3230/3836] Add support for Ironic node shard attribute Nodes can now have shards set and retrieved. This should enable use of shard functionality for openstacksdk clients. Change-Id: I30ae18a6338ec687c3377501acad0568c2520e1c --- openstack/baremetal/v1/node.py | 3 +++ openstack/tests/unit/baremetal/v1/test_node.py | 1 + releasenotes/notes/ironic-node-shard-35f2557c3dbfff1d.yaml | 4 ++++ 3 files changed, 8 insertions(+) create mode 100644 releasenotes/notes/ironic-node-shard-35f2557c3dbfff1d.yaml diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index df58c0326..2d33f8dd1 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -190,6 +190,9 @@ class Node(_common.ListMixin, resource.Resource): #: A string to be used by external schedulers to identify this node as a #: unit of a specific type of resource. Added in API microversion 1.21. resource_class = resource.Body("resource_class") + #: A string indicating the shard this node belongs to. Added in API + #: microversion 1,82. + shard = resource.Body("shard") #: Links to the collection of states. states = resource.Body("states", type=list) #: The requested state if a provisioning action has been requested. For diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 84987cef4..dbed1662d 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -83,6 +83,7 @@ "reservation": None, "resource_class": None, "secure_boot": True, + "shard": "TestShard", "states": [ { "href": "http://127.0.0.1:6385/v1/nodes//states", diff --git a/releasenotes/notes/ironic-node-shard-35f2557c3dbfff1d.yaml b/releasenotes/notes/ironic-node-shard-35f2557c3dbfff1d.yaml new file mode 100644 index 000000000..d4e334f7a --- /dev/null +++ b/releasenotes/notes/ironic-node-shard-35f2557c3dbfff1d.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Adds support for Node shards to baremetal service. From 56e36607365ac9fd325e92f1a41eeafc2dd7b03d Mon Sep 17 00:00:00 2001 From: Kafilat Adeleke Date: Sun, 12 Sep 2021 08:03:44 +0000 Subject: [PATCH 3231/3836] Add share access rules to shared file system Introduce Share Access Rules class with basic methods including create, view, list, get and delete to shared file system storage service Change-Id: I120212e6f1a01644479dc9bc4547b022049c49a1 Co-Authored-By: Samuel Loegering (cherry picked from commit 012b5a128a9318e116ca77f29c7f4dc8b88637db) --- .../user/proxies/shared_file_system.rst | 12 +++ .../resources/shared_file_system/index.rst | 1 + .../v2/share_access_rule.rst | 13 +++ openstack/proxy.py | 1 - openstack/shared_file_system/v2/_proxy.py | 55 ++++++++++++ .../v2/share_access_rule.py | 86 +++++++++++++++++++ .../test_share_access_rule.py | 70 +++++++++++++++ .../unit/shared_file_system/v2/test_proxy.py | 37 ++++++++ .../v2/test_share_access_rule.py | 58 +++++++++++++ ...rules-to-shared-file-362bee34f7331186.yaml | 5 ++ 10 files changed, 337 insertions(+), 1 deletion(-) create mode 100644 doc/source/user/resources/shared_file_system/v2/share_access_rule.rst create mode 100644 openstack/shared_file_system/v2/share_access_rule.py create mode 100644 openstack/tests/functional/shared_file_system/test_share_access_rule.py create mode 100644 openstack/tests/unit/shared_file_system/v2/test_share_access_rule.py create mode 100644 releasenotes/notes/add-share-access-rules-to-shared-file-362bee34f7331186.yaml diff --git a/doc/source/user/proxies/shared_file_system.rst b/doc/source/user/proxies/shared_file_system.rst index 96897c408..a3cd6d128 100644 --- a/doc/source/user/proxies/shared_file_system.rst +++ b/doc/source/user/proxies/shared_file_system.rst @@ -126,3 +126,15 @@ service. :noindex: :members: share_network_subnets, get_share_network_subnet, create_share_network_subnet, delete_share_network_subnet + + +Shared File System Share Access Rules +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Create, View, and Delete access rules for shares from the +Shared File Systems service. + +.. autoclass:: openstack.shared_file_system.v2._proxy.Proxy + :noindex: + :members: access_rules, get_access_rule, create_access_rule, + delete_access_rule diff --git a/doc/source/user/resources/shared_file_system/index.rst b/doc/source/user/resources/shared_file_system/index.rst index 8c60006f7..84dd66ed8 100644 --- a/doc/source/user/resources/shared_file_system/index.rst +++ b/doc/source/user/resources/shared_file_system/index.rst @@ -14,3 +14,4 @@ Shared File System service resources v2/share_snapshot_instance v2/share_network v2/user_message + v2/share_access_rule diff --git a/doc/source/user/resources/shared_file_system/v2/share_access_rule.rst b/doc/source/user/resources/shared_file_system/v2/share_access_rule.rst new file mode 100644 index 000000000..eec6b43c0 --- /dev/null +++ b/doc/source/user/resources/shared_file_system/v2/share_access_rule.rst @@ -0,0 +1,13 @@ +openstack.shared_file_system.v2.share_access_rule +================================================= + +.. automodule:: openstack.shared_file_system.v2.share_access_rule + +The ShareAccessRule Class +------------------------- + +The ``ShareAccessRule`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.shared_file_system.v2.share_access_rule.ShareAccessRule + :members: diff --git a/openstack/proxy.py b/openstack/proxy.py index 5519b9d9a..45d94767b 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -689,7 +689,6 @@ def _list( :class:`~openstack.resource.Resource` that doesn't match the ``resource_type``. """ - data = resource_type.list( self, paginated=paginated, base_path=base_path, **attrs diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index e9c801914..91832e54a 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -14,6 +14,9 @@ from openstack import resource from openstack.shared_file_system.v2 import ( availability_zone as _availability_zone) +from openstack.shared_file_system.v2 import ( + share_access_rule as _share_access_rule +) from openstack.shared_file_system.v2 import ( share_export_locations as _share_export_locations ) @@ -55,6 +58,7 @@ class Proxy(proxy.Proxy): "share_instance": _share_instance.ShareInstance, "share_export_locations": _share_export_locations.ShareExportLocation, + "share_access_rule": _share_access_rule.ShareAccessRule, } def availability_zones(self): @@ -628,3 +632,54 @@ def get_export_location(self, export_location, share_id): return self._get( _share_export_locations.ShareExportLocation, export_location_id, share_id=share_id) + + def access_rules(self, share, **query): + """Lists the access rules on a share. + + :returns: A generator of the share access rules. + :rtype: :class:`~openstack.shared_file_system.v2. + share_access_rules.ShareAccessRules` + """ + share = self._get_resource(_share.Share, share) + return self._list( + _share_access_rule.ShareAccessRule, + share_id=share.id, **query) + + def get_access_rule(self, access_id): + """List details of an access rule. + + :param access_id: The id of the access rule to get + :returns: Details of the identified access rule. + :rtype: :class:`~openstack.shared_file_system.v2. + share_access_rules.ShareAccessRules` + """ + return self._get( + _share_access_rule.ShareAccessRule, access_id) + + def create_access_rule(self, share_id, **attrs): + """Creates an access rule from attributes + + :returns: Details of the new access rule + :param share_id: The ID of the share + :param dict attrs: Attributes which will be used to create + a :class:`~openstack.shared_file_system.v2. + share_access_rules.ShareAccessRules`, comprised of the + properties on the ShareAccessRules class. + :rtype: :class:`~openstack.shared_file_system.v2. + share_access_rules.ShareAccessRules` + """ + base_path = "/shares/%s/action" % (share_id,) + return self._create( + _share_access_rule.ShareAccessRule, base_path=base_path, **attrs) + + def delete_access_rule(self, access_id, share_id, ignore_missing=True): + """Deletes an access rule + + :param access_id: The id of the access rule to get + :param share_id: The ID of the share + + :rtype: ``None`` + """ + res = self._get_resource( + _share_access_rule.ShareAccessRule, access_id) + res.delete(self, share_id, ignore_missing=ignore_missing) diff --git a/openstack/shared_file_system/v2/share_access_rule.py b/openstack/shared_file_system/v2/share_access_rule.py new file mode 100644 index 000000000..c240f9b07 --- /dev/null +++ b/openstack/shared_file_system/v2/share_access_rule.py @@ -0,0 +1,86 @@ +# 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 import resource +from openstack import utils + + +class ShareAccessRule(resource.Resource): + resource_key = "share_access_rule" + resources_key = "access_list" + base_path = "/share-access-rules" + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = False + allow_delete = True + allow_list = True + allow_head = False + + _query_mapping = resource.QueryParameters("share_id") + + #: Properties + #: The access credential of the entity granted share access. + access_key = resource.Body("access_key", type=str) + #: The access level to the share. + access_level = resource.Body("access_level", type=str) + #: The object of the access rule. + access_list = resource.Body("access_list", type=str) + #: The value that defines the access. + access_to = resource.Body("access_to", type=str) + #: The access rule type. + access_type = resource.Body("access_type", type=str) + #: The date and time stamp when the resource was created within the + #: service’s database. + created_at = resource.Body("created_at", type=str) + #: One or more access rule metadata key and value pairs as a dictionary + #: of strings. + metadata = resource.Body("metadata", type=dict) + #: The UUID of the share to which you are granted or denied access. + share_id = resource.Body("share_id", type=str) + #: The state of the access rule. + state = resource.Body("state", type=str) + #: The date and time stamp when the resource was last updated within + #: the service’s database. + updated_at = resource.Body("updated_at", type=str) + + def _action(self, session, body, url, + action='patch', microversion=None): + headers = {'Accept': ''} + + if microversion is None: + microversion = \ + self._get_microversion(session, action=action) + + session.post( + url, + json=body, + headers=headers, + microversion=microversion) + + def create(self, session, **kwargs): + return super().create(session, + resource_request_key='allow_access', + resource_response_key='access', + **kwargs) + + def delete(self, session, share_id, ignore_missing=True): + body = {'deny_access': {'access_id' : self.id}} + url = utils.urljoin('/shares', share_id, 'action') + try: + response = self._action(session, body, url) + except exceptions.ResourceNotFound: + if not ignore_missing: + raise + return response diff --git a/openstack/tests/functional/shared_file_system/test_share_access_rule.py b/openstack/tests/functional/shared_file_system/test_share_access_rule.py new file mode 100644 index 000000000..b5e56d907 --- /dev/null +++ b/openstack/tests/functional/shared_file_system/test_share_access_rule.py @@ -0,0 +1,70 @@ +# 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.tests.functional.shared_file_system import base + + +class ShareAccessRuleTest(base.BaseSharedFileSystemTest): + + def setUp(self): + super(ShareAccessRuleTest, self).setUp() + + self.SHARE_NAME = self.getUniqueString() + mys = self.create_share( + name=self.SHARE_NAME, size=2, share_type="dhss_false", + share_protocol='NFS', description=None) + self.user_cloud.shared_file_system.wait_for_status( + mys, + status='available', + failures=['error'], + interval=5, + wait=self._wait_for_timeout) + self.assertIsNotNone(mys) + self.assertIsNotNone(mys.id) + self.SHARE_ID = mys.id + self.SHARE = mys + access_rule = self.user_cloud.share.create_access_rule( + self.SHARE_ID, + access_level="rw", + access_type="ip", + access_to="0.0.0.0/0" + ) + self.ACCESS_ID = access_rule.id + self.RESOURCE_KEY = access_rule.resource_key + + def tearDown(self): + acr = self.user_cloud.share.delete_access_rule( + self.ACCESS_ID, + self.SHARE_ID, + ignore_missing=True + ) + + self.assertIsNone(acr) + super(ShareAccessRuleTest, self).tearDown() + + def test_get_access_rule(self): + sot = self.user_cloud.shared_file_system.get_access_rule( + self.ACCESS_ID + ) + self.assertEqual(self.ACCESS_ID, sot.id) + + def test_list_access_rules(self): + rules = self.user_cloud.shared_file_system.access_rules( + self.SHARE, + details=True + ) + self.assertGreater(len(list(rules)), 0) + for rule in rules: + for attribute in ('id', 'created_at', 'updated_at', + 'access_level', 'access_type', 'access_to', + 'share_id', 'access_key', 'metadata'): + self.assertTrue(hasattr(rule, attribute)) diff --git a/openstack/tests/unit/shared_file_system/v2/test_proxy.py b/openstack/tests/unit/shared_file_system/v2/test_proxy.py index 9748866ce..cb3a1bd0f 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_proxy.py +++ b/openstack/tests/unit/shared_file_system/v2/test_proxy.py @@ -12,6 +12,9 @@ from unittest import mock +from openstack.shared_file_system.v2 import ( + share_access_rule +) from openstack.shared_file_system.v2 import _proxy from openstack.shared_file_system.v2 import limit from openstack.shared_file_system.v2 import share @@ -343,3 +346,37 @@ def test_share_network_subnet_delete(self): method_args=["fake_network_id", "fake_sub_network_id"], expected_args=["fake_sub_network_id"], expected_kwargs={'share_network_id': 'fake_network_id'}) + + +class TestAccessRuleProxy(test_proxy_base.TestProxyBase): + + def setUp(self): + super(TestAccessRuleProxy, self).setUp() + self.proxy = _proxy.Proxy(self.session) + + def test_access_ruless(self): + self.verify_list( + self.proxy.access_rules, + share_access_rule.ShareAccessRule, + method_args=["test_share"], + expected_args=[], + expected_kwargs={"share_id": "test_share"}) + + def test_access_rules_get(self): + self.verify_get( + self.proxy.get_access_rule, share_access_rule.ShareAccessRule) + + def test_access_rules_create(self): + self.verify_create( + self.proxy.create_access_rule, + share_access_rule.ShareAccessRule, + method_args=["share_id"], + expected_args=[]) + + def test_access_rules_delete(self): + self._verify( + "openstack.shared_file_system.v2.share_access_rule." + + "ShareAccessRule.delete", + self.proxy.delete_access_rule, + method_args=['access_id', 'share_id', 'ignore_missing'], + expected_args=[self.proxy , 'share_id']) diff --git a/openstack/tests/unit/shared_file_system/v2/test_share_access_rule.py b/openstack/tests/unit/shared_file_system/v2/test_share_access_rule.py new file mode 100644 index 000000000..83762db54 --- /dev/null +++ b/openstack/tests/unit/shared_file_system/v2/test_share_access_rule.py @@ -0,0 +1,58 @@ +# 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.shared_file_system.v2 import share_access_rule +from openstack.tests.unit import base + +EXAMPLE = { + "access_level": "rw", + "state": "error", + "id": "507bf114-36f2-4f56-8cf4-857985ca87c1", + "share_id": "fb213952-2352-41b4-ad7b-2c4c69d13eef", + "access_type": "cert", + "access_to": "example.com", + "access_key": None, + "created_at": "2021-09-12T02:01:04.000000", + "updated_at": "2021-09-12T02:01:04.000000", + "metadata": { + "key1": "value1", + "key2": "value2"} +} + + +class TestShareAccessRule(base.TestCase): + + def test_basic(self): + rules_resource = share_access_rule.ShareAccessRule() + self.assertEqual('access_list', rules_resource.resources_key) + self.assertEqual('/share-access-rules', rules_resource.base_path) + self.assertTrue(rules_resource.allow_list) + + self.assertDictEqual({ + "limit": "limit", + "marker": "marker", + "share_id": "share_id" + }, + rules_resource._query_mapping._mapping) + + def test_make_share_access_rules(self): + rules_resource = share_access_rule.ShareAccessRule(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], rules_resource.id) + self.assertEqual(EXAMPLE['access_level'], rules_resource.access_level) + self.assertEqual(EXAMPLE['state'], rules_resource.state) + self.assertEqual(EXAMPLE['id'], rules_resource.id) + self.assertEqual(EXAMPLE['access_type'], rules_resource.access_type) + self.assertEqual(EXAMPLE['access_to'], rules_resource.access_to) + self.assertEqual(EXAMPLE['access_key'], rules_resource.access_key) + self.assertEqual(EXAMPLE['created_at'], rules_resource.created_at) + self.assertEqual(EXAMPLE['updated_at'], rules_resource.updated_at) + self.assertEqual(EXAMPLE['metadata'], rules_resource.metadata) diff --git a/releasenotes/notes/add-share-access-rules-to-shared-file-362bee34f7331186.yaml b/releasenotes/notes/add-share-access-rules-to-shared-file-362bee34f7331186.yaml new file mode 100644 index 000000000..005de8728 --- /dev/null +++ b/releasenotes/notes/add-share-access-rules-to-shared-file-362bee34f7331186.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added support to create, list, get and delete share access rules with the + shared file system service. From 22f89abf1fb802beb45ba9a21a09841159109c23 Mon Sep 17 00:00:00 2001 From: Tobias Henkel Date: Mon, 19 Aug 2019 20:03:08 +0200 Subject: [PATCH 3232/3836] Add link to image sharing api docs The add|update_member methods of images take keyword arguments which are documented in the api reference. Thus add a link to the docs so it can be found easier. Change-Id: I37eb80be164f179b4afc2dc36be3c5e10b3e7e52 --- openstack/image/v2/_proxy.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index a199394f7..6794eac3f 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -997,6 +997,10 @@ def add_member(self, image, **attrs): a :class:`~openstack.image.v2.member.Member`, comprised of the properties on the Member class. + See `Image Sharing Reference + `__ + for details. + :returns: The results of member creation :rtype: :class:`~openstack.image.v2.member.Member` """ @@ -1093,6 +1097,10 @@ def update_member(self, member, image, **attrs): :param attrs: The attributes to update on the member represented by ``member``. + See `Image Sharing Reference + `__ + for details. + :returns: The updated member :rtype: :class:`~openstack.image.v2.member.Member` """ From 395a77298ecd79623b1af75ad0dc7653f5e4eb61 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 26 Apr 2023 11:33:04 +0100 Subject: [PATCH 3233/3836] Blackify openstack.compute The first step in black'ification. Black used with the '-l 79 -S' flags. A future change will ignore this commit in git-blame history by adding a 'git-blame-ignore-revs' file. Change-Id: Ic8e372a7ca999414ad93fb88e03b92798052cc3e Signed-off-by: Stephen Finucane --- openstack/compute/compute_service.py | 2 +- openstack/compute/v2/_proxy.py | 252 ++++++++++++------ openstack/compute/v2/aggregate.py | 6 +- openstack/compute/v2/flavor.py | 46 ++-- openstack/compute/v2/hypervisor.py | 9 +- openstack/compute/v2/image.py | 8 +- openstack/compute/v2/keypair.py | 3 +- openstack/compute/v2/limits.py | 45 ++-- openstack/compute/v2/quota_set.py | 6 +- openstack/compute/v2/server.py | 151 ++++++++--- openstack/compute/v2/server_ip.py | 21 +- openstack/compute/v2/server_remote_console.py | 16 +- openstack/compute/v2/service.py | 25 +- openstack/compute/v2/usage.py | 4 +- 14 files changed, 387 insertions(+), 207 deletions(-) diff --git a/openstack/compute/compute_service.py b/openstack/compute/compute_service.py index 03397cbe8..3b4a7c00c 100644 --- a/openstack/compute/compute_service.py +++ b/openstack/compute/compute_service.py @@ -18,5 +18,5 @@ class ComputeService(service_description.ServiceDescription): """The compute service.""" supported_versions = { - '2': _proxy.Proxy + '2': _proxy.Proxy, } diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 19fc16148..7d4eb1095 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -64,7 +64,7 @@ class Proxy(proxy.Proxy): "server_remote_console": _src.ServerRemoteConsole, "service": _service.Service, "usage": _usage.Usage, - "volume_attachment": _volume_attachment.VolumeAttachment + "volume_attachment": _volume_attachment.VolumeAttachment, } # ========== Extensions ========== @@ -400,8 +400,11 @@ def delete_aggregate(self, aggregate, ignore_missing=True): :returns: ``None`` """ - self._delete(_aggregate.Aggregate, aggregate, - ignore_missing=ignore_missing) + self._delete( + _aggregate.Aggregate, + aggregate, + ignore_missing=ignore_missing, + ) def add_host_to_aggregate(self, aggregate, host): """Adds a host to an aggregate @@ -638,8 +641,12 @@ def delete_keypair(self, keypair, ignore_missing=True, user_id=None): :returns: ``None`` """ attrs = {'user_id': user_id} if user_id else {} - self._delete(_keypair.Keypair, keypair, ignore_missing=ignore_missing, - **attrs) + self._delete( + _keypair.Keypair, + keypair, + ignore_missing=ignore_missing, + **attrs, + ) def get_keypair(self, keypair, user_id=None): """Get a single keypair @@ -934,8 +941,14 @@ def revert_server_resize(self, server): server = self._get_resource(_server.Server, server) server.revert_resize(self) - def create_server_image(self, server, name, metadata=None, wait=False, - timeout=120): + def create_server_image( + self, + server, + name, + metadata=None, + wait=False, + timeout=120, + ): """Create an image from a server :param server: Either the ID of a server or a @@ -1048,8 +1061,7 @@ def rescue_server(self, server, admin_pass=None, image_ref=None): :returns: None """ server = self._get_resource(_server.Server, server) - server.rescue(self, admin_pass=admin_pass, - image_ref=image_ref) + server.rescue(self, admin_pass=admin_pass, image_ref=image_ref) def unrescue_server(self, server): """Unrescues a server and changes its status to ``ACTIVE``. @@ -1076,8 +1088,7 @@ def evacuate_server(self, server, host=None, admin_pass=None, force=None): :returns: None """ server = self._get_resource(_server.Server, server) - server.evacuate(self, host=host, admin_pass=admin_pass, - force=force) + server.evacuate(self, host=host, admin_pass=admin_pass, force=force) def start_server(self, server): """Starts a stopped server and changes its state to ``ACTIVE``. @@ -1236,8 +1247,7 @@ def add_floating_ip_to_server(self, server, address, fixed_address=None): :returns: None """ server = self._get_resource(_server.Server, server) - server.add_floating_ip(self, address, - fixed_address=fixed_address) + server.add_floating_ip(self, address, fixed_address=fixed_address) def remove_floating_ip_from_server(self, server, address): """Removes a floating IP address from a server instance. @@ -1267,13 +1277,20 @@ def create_server_interface(self, server, **attrs): :rtype: :class:`~openstack.compute.v2.server_interface.ServerInterface` """ server_id = resource.Resource._get_id(server) - return self._create(_server_interface.ServerInterface, - server_id=server_id, **attrs) + return self._create( + _server_interface.ServerInterface, + server_id=server_id, + **attrs, + ) # TODO(stephenfin): Does this work? There's no 'value' parameter for the # call to '_delete' - def delete_server_interface(self, server_interface, server=None, - ignore_missing=True): + def delete_server_interface( + self, + server_interface, + server=None, + ignore_missing=True, + ): """Delete a server interface :param server_interface: @@ -1292,14 +1309,19 @@ def delete_server_interface(self, server_interface, server=None, :returns: ``None`` """ - server_id = self._get_uri_attribute(server_interface, server, - "server_id") + server_id = self._get_uri_attribute( + server_interface, + server, + "server_id", + ) server_interface = resource.Resource._get_id(server_interface) - self._delete(_server_interface.ServerInterface, - server_interface, - server_id=server_id, - ignore_missing=ignore_missing) + self._delete( + _server_interface.ServerInterface, + server_interface, + server_id=server_id, + ignore_missing=ignore_missing, + ) def get_server_interface(self, server_interface, server=None): """Get a single server interface @@ -1318,12 +1340,18 @@ def get_server_interface(self, server_interface, server=None): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - server_id = self._get_uri_attribute(server_interface, server, - "server_id") + server_id = self._get_uri_attribute( + server_interface, + server, + "server_id", + ) server_interface = resource.Resource._get_id(server_interface) - return self._get(_server_interface.ServerInterface, - server_id=server_id, port_id=server_interface) + return self._get( + _server_interface.ServerInterface, + server_id=server_id, + port_id=server_interface, + ) def server_interfaces(self, server, **query): """Return a generator of server interfaces @@ -1337,8 +1365,11 @@ def server_interfaces(self, server, **query): :rtype: :class:`~openstack.compute.v2.server_interface.ServerInterface` """ server_id = resource.Resource._get_id(server) - return self._list(_server_interface.ServerInterface, - server_id=server_id, **query) + return self._list( + _server_interface.ServerInterface, + server_id=server_id, + **query, + ) def server_ips(self, server, network_label=None): """Return a generator of server IPs @@ -1352,8 +1383,11 @@ def server_ips(self, server, network_label=None): :rtype: :class:`~openstack.compute.v2.server_ip.ServerIP` """ server_id = resource.Resource._get_id(server) - return self._list(server_ip.ServerIP, - server_id=server_id, network_label=network_label) + return self._list( + server_ip.ServerIP, + server_id=server_id, + network_label=network_label, + ) def availability_zones(self, details=False): """Return a generator of availability zones @@ -1370,7 +1404,8 @@ def availability_zones(self, details=False): return self._list( availability_zone.AvailabilityZone, - base_path=base_path) + base_path=base_path, + ) # ========== Server Metadata ========== @@ -1455,8 +1490,11 @@ def delete_server_group(self, server_group, ignore_missing=True): :returns: ``None`` """ - self._delete(_server_group.ServerGroup, server_group, - ignore_missing=ignore_missing) + self._delete( + _server_group.ServerGroup, + server_group, + ignore_missing=ignore_missing, + ) def find_server_group( self, @@ -1543,7 +1581,8 @@ def hypervisors(self, details=False, **query): ): # Until 2.53 we need to use other API base_path = '/os-hypervisors/{pattern}/search'.format( - pattern=query.pop('hypervisor_hostname_pattern')) + pattern=query.pop('hypervisor_hostname_pattern') + ) return self._list(_hypervisor.Hypervisor, base_path=base_path, **query) def find_hypervisor( @@ -1611,7 +1650,11 @@ def get_hypervisor_uptime(self, hypervisor): # ========== Services ========== def update_service_forced_down( - self, service, host=None, binary=None, forced=True + self, + service, + host=None, + binary=None, + forced=True, ): """Update service forced_down information @@ -1626,23 +1669,26 @@ def update_service_forced_down( :rtype: class: `~openstack.compute.v2.service.Service` """ if utils.supports_microversion(self, '2.53'): - return self.update_service( - service, forced_down=forced) + return self.update_service(service, forced_down=forced) service = self._get_resource(_service.Service, service) - if ( - (not host or not binary) - and (not service.host or not service.binary) + if (not host or not binary) and ( + not service.host or not service.binary ): raise ValueError( 'Either service instance should have host and binary ' - 'or they should be passed') + 'or they should be passed' + ) service.set_forced_down(self, host, binary, forced) force_service_down = update_service_forced_down def disable_service( - self, service, host=None, binary=None, disabled_reason=None + self, + service, + host=None, + binary=None, + disabled_reason=None, ): """Disable a service @@ -1656,17 +1702,13 @@ def disable_service( :rtype: class: `~openstack.compute.v2.service.Service` """ if utils.supports_microversion(self, '2.53'): - attrs = { - 'status': 'disabled' - } + attrs = {'status': 'disabled'} if disabled_reason: attrs['disabled_reason'] = disabled_reason - return self.update_service( - service, **attrs) + return self.update_service(service, **attrs) service = self._get_resource(_service.Service, service) - return service.disable( - self, host, binary, disabled_reason) + return service.disable(self, host, binary, disabled_reason) def enable_service(self, service, host=None, binary=None): """Enable a service @@ -1680,8 +1722,7 @@ def enable_service(self, service, host=None, binary=None): :rtype: class: `~openstack.compute.v2.service.Service` """ if utils.supports_microversion(self, '2.53'): - return self.update_service( - service, status='enabled') + return self.update_service(service, status='enabled') service = self._get_resource(_service.Service, service) return service.enable(self, host, binary) @@ -1732,8 +1773,7 @@ def delete_service(self, service, ignore_missing=True): :returns: ``None`` """ - self._delete( - _service.Service, service, ignore_missing=ignore_missing) + self._delete(_service.Service, service, ignore_missing=ignore_missing) def update_service(self, service, **attrs): """Update a service @@ -1813,7 +1853,11 @@ def create_volume_attachment(self, server, volume=None, **attrs): ) def update_volume_attachment( - self, server, volume, volume_id=None, **attrs, + self, + server, + volume, + volume_id=None, + **attrs, ): """Update a volume attachment @@ -1860,16 +1904,14 @@ def _verify_server_volume_args(self, server, volume): # if we have even partial type information and things look as they # should, we can assume the user did the right thing - if ( - isinstance(server, _server.Server) - or isinstance(volume, _volume.Volume) + if isinstance(server, _server.Server) or isinstance( + volume, _volume.Volume ): return server, volume # conversely, if there's type info and things appear off, tell the user - if ( - isinstance(server, _volume.Volume) - or isinstance(volume, _server.Server) + if isinstance(server, _volume.Volume) or isinstance( + volume, _server.Server ): warnings.warn(deprecation_msg, DeprecationWarning) return volume, server @@ -1972,7 +2014,11 @@ def migrate_server(self, server): server.migrate(self) def live_migrate_server( - self, server, host=None, force=False, block_migration=None, + self, + server, + host=None, + force=False, + block_migration=None, ): """Live migrate a server from one host to target host @@ -1995,13 +2041,17 @@ def live_migrate_server( """ server = self._get_resource(_server.Server, server) server.live_migrate( - self, host, + self, + host, force=force, block_migration=block_migration, ) def abort_server_migration( - self, server_migration, server, ignore_missing=True, + self, + server_migration, + server, + ignore_missing=True, ): """Abort an in-progress server migration @@ -2022,7 +2072,9 @@ def abort_server_migration( :returns: ``None`` """ server_id = self._get_uri_attribute( - server_migration, server, 'server_id', + server_migration, + server, + 'server_id', ) server_migration = resource.Resource._get_id(server_migration) @@ -2048,7 +2100,9 @@ def force_complete_server_migration(self, server_migration, server=None): :returns: ``None`` """ server_id = self._get_uri_attribute( - server_migration, server, 'server_id', + server_migration, + server, + 'server_id', ) server_migration = self._get_resource( _server_migration.ServerMigration, @@ -2085,7 +2139,9 @@ def get_server_migration( when no resource can be found. """ server_id = self._get_uri_attribute( - server_migration, server, 'server_id', + server_migration, + server, + 'server_id', ) server_migration = resource.Resource._get_id(server_migration) @@ -2140,8 +2196,11 @@ def get_server_diagnostics(self, server): when no resource can be found. """ server_id = self._get_resource(_server.Server, server).id - return self._get(_server_diagnostics.ServerDiagnostics, - server_id=server_id, requires_id=False) + return self._get( + _server_diagnostics.ServerDiagnostics, + server_id=server_id, + requires_id=False, + ) # ========== Project usage ============ @@ -2194,8 +2253,11 @@ def create_server_remote_console(self, server, **attrs): :class:`~openstack.compute.v2.server_remote_console.ServerRemoteConsole` """ server_id = resource.Resource._get_id(server) - return self._create(_src.ServerRemoteConsole, - server_id=server_id, **attrs) + return self._create( + _src.ServerRemoteConsole, + server_id=server_id, + **attrs, + ) def get_server_console_url(self, server, console_type): """Create a remote console on the server. @@ -2248,7 +2310,8 @@ def create_console(self, server, console_type, console_protocol=None): _src.ServerRemoteConsole, server_id=server.id, type=console_type, - protocol=console_protocol) + protocol=console_protocol, + ) return console.to_dict() else: return server.get_console_url(self, console_type) @@ -2271,7 +2334,9 @@ def get_quota_set(self, project, usage=False, **query): """ project = self._get_resource(_project.Project, project) res = self._get_resource( - _quota_set.QuotaSet, None, project_id=project.id, + _quota_set.QuotaSet, + None, + project_id=project.id, ) return res.fetch(self, usage=usage, **query) @@ -2288,7 +2353,9 @@ def get_quota_set_defaults(self, project): """ project = self._get_resource(_project.Project, project) res = self._get_resource( - _quota_set.QuotaSet, None, project_id=project.id, + _quota_set.QuotaSet, + None, + project_id=project.id, ) return res.fetch(self, base_path='/os-quota-sets/defaults') @@ -2304,7 +2371,10 @@ def revert_quota_set(self, project, **query): """ project = self._get_resource(_project.Project, project) res = self._get_resource( - _quota_set.QuotaSet, None, project_id=project.id) + _quota_set.QuotaSet, + None, + project_id=project.id, + ) if not query: query = {} @@ -2373,7 +2443,12 @@ def server_actions(self, server): # ========== Utilities ========== def wait_for_server( - self, server, status='ACTIVE', failures=None, interval=2, wait=120, + self, + server, + status='ACTIVE', + failures=None, + interval=2, + wait=120, ): """Wait for a server to be in a particular status. @@ -2400,7 +2475,12 @@ def wait_for_server( """ failures = ['ERROR'] if failures is None else failures return resource.wait_for_status( - self, server, status, failures, interval, wait, + self, + server, + status, + failures, + interval, + wait, ) def wait_for_delete(self, res, interval=2, wait=120): @@ -2420,14 +2500,17 @@ def wait_for_delete(self, res, interval=2, wait=120): def _get_cleanup_dependencies(self): return { - 'compute': { - 'before': ['block_storage', 'network', 'identity'] - } + 'compute': {'before': ['block_storage', 'network', 'identity']} } - def _service_cleanup(self, dry_run=True, client_status_queue=None, - identified_resources=None, - filters=None, resource_evaluation_fn=None): + def _service_cleanup( + self, + dry_run=True, + client_status_queue=None, + identified_resources=None, + filters=None, + resource_evaluation_fn=None, + ): servers = [] for obj in self.servers(): need_delete = self._service_cleanup_del_res( @@ -2437,7 +2520,8 @@ def _service_cleanup(self, dry_run=True, client_status_queue=None, client_status_queue=client_status_queue, identified_resources=identified_resources, filters=filters, - resource_evaluation_fn=resource_evaluation_fn) + resource_evaluation_fn=resource_evaluation_fn, + ) if not dry_run and need_delete: # In the dry run we identified, that server will go. To propely # identify consequences we need to tell others, that the port diff --git a/openstack/compute/v2/aggregate.py b/openstack/compute/v2/aggregate.py index b256be3ec..e3f47ed9b 100644 --- a/openstack/compute/v2/aggregate.py +++ b/openstack/compute/v2/aggregate.py @@ -52,8 +52,7 @@ class Aggregate(resource.Resource): def _action(self, session, body, microversion=None): """Preform aggregate actions given the message body.""" url = utils.urljoin(self.base_path, self.id, 'action') - response = session.post( - url, json=body, microversion=microversion) + response = session.post(url, json=body, microversion=microversion) exceptions.raise_from_response(response) aggregate = Aggregate() aggregate._translate_response(response=response) @@ -79,6 +78,7 @@ def precache_images(self, session, images): body = {'cache': images} url = utils.urljoin(self.base_path, self.id, 'images') response = session.post( - url, json=body, microversion=self._max_microversion) + url, json=body, microversion=self._max_microversion + ) exceptions.raise_from_response(response) # This API has no result diff --git a/openstack/compute/v2/flavor.py b/openstack/compute/v2/flavor.py index 55555873c..10975e3f9 100644 --- a/openstack/compute/v2/flavor.py +++ b/openstack/compute/v2/flavor.py @@ -28,9 +28,12 @@ class Flavor(resource.Resource): allow_commit = True _query_mapping = resource.QueryParameters( - "sort_key", "sort_dir", "is_public", + "sort_key", + "sort_dir", + "is_public", min_disk="minDisk", - min_ram="minRam") + min_ram="minRam", + ) # extra_specs introduced in 2.61 _max_microversion = '2.61' @@ -47,7 +50,8 @@ class Flavor(resource.Resource): #: ``True`` if this is a publicly visible flavor. ``False`` if this is #: a private image. *Type: bool* is_public = resource.Body( - 'os-flavor-access:is_public', type=bool, default=True) + 'os-flavor-access:is_public', type=bool, default=True + ) #: The amount of RAM (in MB) this flavor offers. *Type: int* ram = resource.Body('ram', type=int, default=0) #: The number of virtual CPUs this flavor offers. *Type: int* @@ -55,8 +59,7 @@ class Flavor(resource.Resource): #: Size of the swap partitions. swap = resource.Body('swap', default=0) #: Size of the ephemeral data disk attached to this server. *Type: int* - ephemeral = resource.Body( - 'OS-FLV-EXT-DATA:ephemeral', type=int, default=0) + ephemeral = resource.Body('OS-FLV-EXT-DATA:ephemeral', type=int, default=0) #: ``True`` if this flavor is disabled, ``False`` if not. *Type: bool* is_disabled = resource.Body('OS-FLV-DISABLED:disabled', type=bool) #: The bandwidth scaling factor this flavor receives on the network. @@ -91,7 +94,7 @@ def list( session, paginated=True, base_path='/flavors/detail', - **params + **params, ): # Find will invoke list when name was passed. Since we want to return # flavor with details (same as direct get) we need to swap default here @@ -101,9 +104,8 @@ def list( # Force it to string to avoid requests skipping it. params['is_public'] = 'None' return super(Flavor, cls).list( - session, paginated=paginated, - base_path=base_path, - **params) + session, paginated=paginated, base_path=base_path, **params + ) def _action(self, session, body, microversion=None): """Preform flavor actions given the message body.""" @@ -113,8 +115,7 @@ def _action(self, session, body, microversion=None): if microversion: # Do not reset microversion if it is set on a session level attrs['microversion'] = microversion - response = session.post( - url, json=body, headers=headers, **attrs) + response = session.post(url, json=body, headers=headers, **attrs) exceptions.raise_from_response(response) return response @@ -161,9 +162,8 @@ def create_extra_specs(self, session, specs): url = utils.urljoin(Flavor.base_path, self.id, 'os-extra_specs') microversion = self._get_microversion(session, action='create') response = session.post( - url, - json={'extra_specs': specs}, - microversion=microversion) + url, json={'extra_specs': specs}, microversion=microversion + ) exceptions.raise_from_response(response) specs = response.json().get('extra_specs', {}) self._update(extra_specs=specs) @@ -171,8 +171,7 @@ def create_extra_specs(self, session, specs): def get_extra_specs_property(self, session, prop): """Get individual extra_spec property""" - url = utils.urljoin(Flavor.base_path, self.id, - 'os-extra_specs', prop) + url = utils.urljoin(Flavor.base_path, self.id, 'os-extra_specs', prop) microversion = self._get_microversion(session, action='fetch') response = session.get(url, microversion=microversion) exceptions.raise_from_response(response) @@ -181,25 +180,20 @@ def get_extra_specs_property(self, session, prop): def update_extra_specs_property(self, session, prop, val): """Update An Extra Spec For A Flavor""" - url = utils.urljoin(Flavor.base_path, self.id, - 'os-extra_specs', prop) + url = utils.urljoin(Flavor.base_path, self.id, 'os-extra_specs', prop) microversion = self._get_microversion(session, action='commit') response = session.put( - url, - json={prop: val}, - microversion=microversion) + url, json={prop: val}, microversion=microversion + ) exceptions.raise_from_response(response) val = response.json().get(prop) return val def delete_extra_specs_property(self, session, prop): """Delete An Extra Spec For A Flavor""" - url = utils.urljoin(Flavor.base_path, self.id, - 'os-extra_specs', prop) + url = utils.urljoin(Flavor.base_path, self.id, 'os-extra_specs', prop) microversion = self._get_microversion(session, action='delete') - response = session.delete( - url, - microversion=microversion) + response = session.delete(url, microversion=microversion) exceptions.raise_from_response(response) diff --git a/openstack/compute/v2/hypervisor.py b/openstack/compute/v2/hypervisor.py index 34bcba2f3..8f10cd204 100644 --- a/openstack/compute/v2/hypervisor.py +++ b/openstack/compute/v2/hypervisor.py @@ -86,14 +86,15 @@ def get_uptime(self, session): Updates uptime attribute of the hypervisor object """ warnings.warn( - "This call is deprecated and is only available until Nova 2.88") + "This call is deprecated and is only available until Nova 2.88" + ) if utils.supports_microversion(session, '2.88'): raise exceptions.SDKException( - 'Hypervisor.get_uptime is not supported anymore') + 'Hypervisor.get_uptime is not supported anymore' + ) url = utils.urljoin(self.base_path, self.id, 'uptime') microversion = self._get_microversion(session, action='fetch') - response = session.get( - url, microversion=microversion) + response = session.get(url, microversion=microversion) self._translate_response(response) return self diff --git a/openstack/compute/v2/image.py b/openstack/compute/v2/image.py index 4ec498f38..24a3e5a2d 100644 --- a/openstack/compute/v2/image.py +++ b/openstack/compute/v2/image.py @@ -24,10 +24,14 @@ class Image(resource.Resource, metadata.MetadataMixin): allow_list = True _query_mapping = resource.QueryParameters( - "server", "name", "status", "type", + "server", + "name", + "status", + "type", min_disk="minDisk", min_ram="minRam", - changes_since="changes-since") + changes_since="changes-since", + ) # Properties #: Links pertaining to this image. This is a list of dictionaries, diff --git a/openstack/compute/v2/keypair.py b/openstack/compute/v2/keypair.py index e3a1eab2c..17567b52f 100644 --- a/openstack/compute/v2/keypair.py +++ b/openstack/compute/v2/keypair.py @@ -18,8 +18,7 @@ class Keypair(resource.Resource): resources_key = 'keypairs' base_path = '/os-keypairs' - _query_mapping = resource.QueryParameters( - 'user_id') + _query_mapping = resource.QueryParameters('user_id') # capabilities allow_create = True diff --git a/openstack/compute/v2/limits.py b/openstack/compute/v2/limits.py index fb5833be1..2e2e68432 100644 --- a/openstack/compute/v2/limits.py +++ b/openstack/compute/v2/limits.py @@ -25,13 +25,16 @@ class AbsoluteLimits(resource.Resource): personality_size = resource.Body("maxPersonalitySize", deprecated=True) #: The maximum amount of security group rules allowed. security_group_rules = resource.Body( - "maxSecurityGroupRules", aka="max_security_group_rules") + "maxSecurityGroupRules", aka="max_security_group_rules" + ) #: The maximum amount of security groups allowed. security_groups = resource.Body( - "maxSecurityGroups", aka="max_security_groups") + "maxSecurityGroups", aka="max_security_groups" + ) #: The amount of security groups currently in use. security_groups_used = resource.Body( - "totalSecurityGroupsUsed", aka="total_security_groups_used") + "totalSecurityGroupsUsed", aka="total_security_groups_used" + ) #: The number of key-value pairs that can be set as server metadata. server_meta = resource.Body("maxServerMeta", aka="max_server_meta") #: The maximum amount of cores. @@ -40,15 +43,18 @@ class AbsoluteLimits(resource.Resource): total_cores_used = resource.Body("totalCoresUsed", aka="total_cores_used") #: The maximum amount of floating IPs. floating_ips = resource.Body( - "maxTotalFloatingIps", aka="max_total_floating_ips") + "maxTotalFloatingIps", aka="max_total_floating_ips" + ) #: The amount of floating IPs currently in use. floating_ips_used = resource.Body( - "totalFloatingIpsUsed", aka="total_floating_ips_used") + "totalFloatingIpsUsed", aka="total_floating_ips_used" + ) #: The maximum amount of instances. instances = resource.Body("maxTotalInstances", aka="max_total_instances") #: The amount of instances currently in use. instances_used = resource.Body( - "totalInstancesUsed", aka="total_instances_used") + "totalInstancesUsed", aka="total_instances_used" + ) #: The maximum amount of keypairs. keypairs = resource.Body("maxTotalKeypairs", aka="max_total_keypairs") #: The maximum RAM size in megabytes. @@ -59,10 +65,12 @@ class AbsoluteLimits(resource.Resource): server_groups = resource.Body("maxServerGroups", aka="max_server_groups") #: The amount of server groups currently in use. server_groups_used = resource.Body( - "totalServerGroupsUsed", aka="total_server_groups_used") + "totalServerGroupsUsed", aka="total_server_groups_used" + ) #: The maximum number of members in a server group. server_group_members = resource.Body( - "maxServerGroupMembers", aka="max_server_group_members") + "maxServerGroupMembers", aka="max_server_group_members" + ) class RateLimit(resource.Resource): @@ -83,15 +91,20 @@ class Limits(resource.Resource): allow_fetch = True - _query_mapping = resource.QueryParameters( - 'tenant_id' - ) + _query_mapping = resource.QueryParameters('tenant_id') absolute = resource.Body("absolute", type=AbsoluteLimits) rate = resource.Body("rate", type=list, list_type=RateLimit) - def fetch(self, session, requires_id=False, error_message=None, - base_path=None, skip_cache=False, **params): + def fetch( + self, + session, + requires_id=False, + error_message=None, + base_path=None, + skip_cache=False, + **params + ): """Get the Limits resource. :param session: The session to use for making this request. @@ -103,8 +116,10 @@ def fetch(self, session, requires_id=False, error_message=None, # TODO(mordred) We shouldn't have to subclass just to declare # requires_id = False. return super(Limits, self).fetch( - session=session, requires_id=requires_id, + session=session, + requires_id=requires_id, error_message=error_message, base_path=base_path, skip_cache=skip_cache, - **params) + **params + ) diff --git a/openstack/compute/v2/quota_set.py b/openstack/compute/v2/quota_set.py index 86100de14..847c5ae0c 100644 --- a/openstack/compute/v2/quota_set.py +++ b/openstack/compute/v2/quota_set.py @@ -31,10 +31,12 @@ class QuotaSet(quota_set.QuotaSet): force = resource.Body('force', type=bool) #: The number of allowed bytes of content for each injected file. injected_file_content_bytes = resource.Body( - 'injected_file_content_bytes', type=int) + 'injected_file_content_bytes', type=int + ) #: The number of allowed bytes for each injected file path. injected_file_path_bytes = resource.Body( - 'injected_file_path_bytes', type=int) + 'injected_file_path_bytes', type=int + ) #: The number of allowed injected files for each tenant. injected_files = resource.Body('injected_files', type=int) #: The number of allowed servers for each tenant. diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index e2bd75ba6..ddbc163e5 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -24,7 +24,7 @@ 'xvpvnc': 'os-getVNCConsole', 'spice-html5': 'os-getSPICEConsole', 'rdp-html5': 'os-getRDPConsole', - 'serial': 'os-getSerialConsole' + 'serial': 'os-getSerialConsole', } @@ -46,15 +46,33 @@ class Server(resource.Resource, metadata.MetadataMixin, tag.TagMixin): _sentinel = object() _query_mapping = resource.QueryParameters( - "auto_disk_config", "availability_zone", - "created_at", "description", "flavor", - "hostname", "image", "kernel_id", "key_name", - "launch_index", "launched_at", "locked_by", "name", - "node", "power_state", "progress", "project_id", "ramdisk_id", - "reservation_id", "root_device_name", - "status", "task_state", "terminated_at", "user_id", + "auto_disk_config", + "availability_zone", + "created_at", + "description", + "flavor", + "hostname", + "image", + "kernel_id", + "key_name", + "launch_index", + "launched_at", + "locked_by", + "name", + "node", + "power_state", + "progress", + "project_id", + "ramdisk_id", + "reservation_id", + "root_device_name", + "status", + "task_state", + "terminated_at", + "user_id", "vm_state", - "sort_key", "sort_dir", + "sort_key", + "sort_dir", access_ipv4="access_ip_v4", access_ipv6="access_ip_v6", has_config_drive="config_drive", @@ -67,7 +85,7 @@ class Server(resource.Resource, metadata.MetadataMixin, tag.TagMixin): changes_before="changes-before", id="uuid", all_projects="all_tenants", - **tag.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters, ) _max_microversion = '2.91' @@ -93,7 +111,8 @@ class Server(resource.Resource, metadata.MetadataMixin, tag.TagMixin): aka='volumes', type=list, list_type=volume_attachment.VolumeAttachment, - default=[]) + default=[], + ) #: The name of the availability zone this server is a part of. availability_zone = resource.Body('OS-EXT-AZ:availability_zone') #: Enables fine grained control of the block device mapping for an @@ -202,8 +221,9 @@ class Server(resource.Resource, metadata.MetadataMixin, tag.TagMixin): scheduler_hints = resource.Body('OS-SCH-HNT:scheduler_hints', type=dict) #: A list of applicable security groups. Each group contains keys for #: description, name, id, and rules. - security_groups = resource.Body('security_groups', - type=list, list_type=dict) + security_groups = resource.Body( + 'security_groups', type=list, list_type=dict + ) #: The UUIDs of the server groups to which the server belongs. #: Currently this can contain at most one entry. server_groups = resource.Body('server_groups', type=list) @@ -220,7 +240,8 @@ class Server(resource.Resource, metadata.MetadataMixin, tag.TagMixin): #: A list of trusted certificate IDs, that were used during image #: signature verification to verify the signing certificate. trusted_image_certificates = resource.Body( - 'trusted_image_certificates', type=list) + 'trusted_image_certificates', type=list + ) #: Timestamp of when this server was last updated. updated_at = resource.Body('updated') #: Configuration information or scripts to use upon launch. @@ -231,11 +252,18 @@ class Server(resource.Resource, metadata.MetadataMixin, tag.TagMixin): #: The VM state of this server. vm_state = resource.Body('OS-EXT-STS:vm_state') - def _prepare_request(self, requires_id=True, prepend_key=True, - base_path=None, **kwargs): - request = super(Server, self)._prepare_request(requires_id=requires_id, - prepend_key=prepend_key, - base_path=base_path) + def _prepare_request( + self, + requires_id=True, + prepend_key=True, + base_path=None, + **kwargs, + ): + request = super(Server, self)._prepare_request( + requires_id=requires_id, + prepend_key=prepend_key, + base_path=base_path, + ) server_body = request.body[self.resource_key] @@ -311,14 +339,21 @@ def force_delete(self, session): body = {'forceDelete': None} self._action(session, body) - def rebuild(self, session, image, name=None, admin_password=None, - preserve_ephemeral=None, - access_ipv4=None, access_ipv6=None, - metadata=None, user_data=None, key_name=None): + def rebuild( + self, + session, + image, + name=None, + admin_password=None, + preserve_ephemeral=None, + access_ipv4=None, + access_ipv6=None, + metadata=None, + user_data=None, + key_name=None, + ): """Rebuild the server with the given arguments.""" - action = { - 'imageRef': resource.Resource._get_id(image) - } + action = {'imageRef': resource.Resource._get_id(image)} if preserve_ephemeral is not None: action['preserve_ephemeral'] = preserve_ephemeral if name is not None: @@ -431,7 +466,7 @@ def backup(self, session, name, backup_type, rotation): "createBackup": { "name": name, "backup_type": backup_type, - "rotation": rotation + "rotation": rotation, } } self._action(session, body) @@ -547,24 +582,36 @@ def get_console_url(self, session, console_type): resp = self._action(session, body) return resp.json().get('console') - def live_migrate(self, session, host, force, block_migration, - disk_over_commit=False): + def live_migrate( + self, + session, + host, + force, + block_migration, + disk_over_commit=False, + ): if utils.supports_microversion(session, '2.30'): return self._live_migrate_30( - session, host, + session, + host, force=force, - block_migration=block_migration) + block_migration=block_migration, + ) elif utils.supports_microversion(session, '2.25'): return self._live_migrate_25( - session, host, + session, + host, force=force, - block_migration=block_migration) + block_migration=block_migration, + ) else: return self._live_migrate( - session, host, + session, + host, force=force, block_migration=block_migration, - disk_over_commit=disk_over_commit) + disk_over_commit=disk_over_commit, + ) def _live_migrate_30(self, session, host, force, block_migration): microversion = '2.30' @@ -577,7 +624,10 @@ def _live_migrate_30(self, session, host, force, block_migration): if force: body['force'] = force self._action( - session, {'os-migrateLive': body}, microversion=microversion) + session, + {'os-migrateLive': body}, + microversion=microversion, + ) def _live_migrate_25(self, session, host, force, block_migration): microversion = '2.25' @@ -594,12 +644,22 @@ def _live_migrate_25(self, session, host, force, block_migration): " possible to disable. It is recommended to not use 'host'" " at all on this cloud as it is inherently unsafe, but if" " it is unavoidable, please supply 'force=True' so that it" - " is clear you understand the risks.") + " is clear you understand the risks." + ) self._action( - session, {'os-migrateLive': body}, microversion=microversion) + session, + {'os-migrateLive': body}, + microversion=microversion, + ) - def _live_migrate(self, session, host, force, block_migration, - disk_over_commit): + def _live_migrate( + self, + session, + host, + force, + block_migration, + disk_over_commit, + ): microversion = None body = { 'host': None, @@ -607,7 +667,8 @@ def _live_migrate(self, session, host, force, block_migration, if block_migration == 'auto': raise ValueError( "Live migration on this cloud does not support 'auto' as" - " a parameter to block_migration, but only True and False.") + " a parameter to block_migration, but only True and False." + ) body['block_migration'] = block_migration or False body['disk_over_commit'] = disk_over_commit or False if host: @@ -619,9 +680,13 @@ def _live_migrate(self, session, host, force, block_migration, " possible to disable. It is recommended to not use 'host'" " at all on this cloud as it is inherently unsafe, but if" " it is unavoidable, please supply 'force=True' so that it" - " is clear you understand the risks.") + " is clear you understand the risks." + ) self._action( - session, {'os-migrateLive': body}, microversion=microversion) + session, + {'os-migrateLive': body}, + microversion=microversion, + ) def fetch_topology(self, session): utils.require_microversion(session, 2.78) diff --git a/openstack/compute/v2/server_ip.py b/openstack/compute/v2/server_ip.py index 710437793..e76e71f83 100644 --- a/openstack/compute/v2/server_ip.py +++ b/openstack/compute/v2/server_ip.py @@ -32,8 +32,15 @@ class ServerIP(resource.Resource): version = resource.Body('version') @classmethod - def list(cls, session, paginated=False, server_id=None, - network_label=None, base_path=None, **params): + def list( + cls, + session, + paginated=False, + server_id=None, + network_label=None, + base_path=None, + **params + ): if base_path is None: base_path = cls.base_path @@ -43,7 +50,7 @@ def list(cls, session, paginated=False, server_id=None, if network_label is not None: url = utils.urljoin(url, network_label) - resp = session.get(url,) + resp = session.get(url) resp = resp.json() if network_label is None: @@ -51,6 +58,8 @@ def list(cls, session, paginated=False, server_id=None, for label, addresses in resp.items(): for address in addresses: - yield cls.existing(network_label=label, - address=address["addr"], - version=address["version"]) + yield cls.existing( + network_label=label, + address=address["addr"], + version=address["version"], + ) diff --git a/openstack/compute/v2/server_remote_console.py b/openstack/compute/v2/server_remote_console.py index 2cafdb9f9..bc66feb92 100644 --- a/openstack/compute/v2/server_remote_console.py +++ b/openstack/compute/v2/server_remote_console.py @@ -19,7 +19,7 @@ 'spice-html5': 'spice', 'rdp-html5': 'rdp', 'serial': 'serial', - 'webmks': 'mks' + 'webmks': 'mks', } @@ -47,16 +47,14 @@ class ServerRemoteConsole(resource.Resource): def create(self, session, prepend_key=True, base_path=None, **params): if not self.protocol: - self.protocol = \ - CONSOLE_TYPE_PROTOCOL_MAPPING.get(self.type) + self.protocol = CONSOLE_TYPE_PROTOCOL_MAPPING.get(self.type) if ( not utils.supports_microversion(session, '2.8') and self.type == 'webmks' ): - raise ValueError('Console type webmks is not supported on ' - 'server side') + raise ValueError( + 'Console type webmks is not supported on ' 'server side' + ) return super(ServerRemoteConsole, self).create( - session, - prepend_key=prepend_key, - base_path=base_path, - **params) + session, prepend_key=prepend_key, base_path=base_path, **params + ) diff --git a/openstack/compute/v2/service.py b/openstack/compute/v2/service.py index 4694928ed..e35526582 100644 --- a/openstack/compute/v2/service.py +++ b/openstack/compute/v2/service.py @@ -26,7 +26,9 @@ class Service(resource.Resource): allow_delete = True _query_mapping = resource.QueryParameters( - 'name', 'binary', 'host', + 'name', + 'binary', + 'host', name='binary', ) @@ -73,7 +75,7 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): result = maybe_result else: msg = "More than one %s exists with the name '%s'." - msg = (msg % (cls.__name__, name_or_id)) + msg = msg % (cls.__name__, name_or_id) raise exceptions.DuplicateResource(msg) if result is not None: @@ -82,12 +84,16 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): if ignore_missing: return None raise exceptions.ResourceNotFound( - "No %s found for %s" % (cls.__name__, name_or_id)) + "No %s found for %s" % (cls.__name__, name_or_id) + ) def commit(self, session, prepend_key=False, **kwargs): # we need to set prepend_key to false return super(Service, self).commit( - session, prepend_key=prepend_key, **kwargs) + session, + prepend_key=prepend_key, + **kwargs, + ) def _action(self, session, action, body, microversion=None): if not microversion: @@ -97,9 +103,7 @@ def _action(self, session, action, body, microversion=None): self._translate_response(response) return self - def set_forced_down( - self, session, host=None, binary=None, forced=False - ): + def set_forced_down(self, session, host=None, binary=None, forced=False): """Update forced_down information of a service.""" microversion = session.default_microversion body = {} @@ -118,8 +122,11 @@ def set_forced_down( # This will not work with newest microversions return self._action( - session, 'force-down', body, - microversion=microversion) + session, + 'force-down', + body, + microversion=microversion, + ) force_down = set_forced_down diff --git a/openstack/compute/v2/usage.py b/openstack/compute/v2/usage.py index 0d20f6de0..976a65012 100644 --- a/openstack/compute/v2/usage.py +++ b/openstack/compute/v2/usage.py @@ -79,7 +79,9 @@ class Usage(resource.Resource): project_id = resource.Body('tenant_id') #: A list of the server usage objects. server_usages = resource.Body( - 'server_usages', type=list, list_type=ServerUsage, + 'server_usages', + type=list, + list_type=ServerUsage, ) #: Multiplying the server disk size (in GiB) by hours the server exists, #: and then adding that all together for each server. From 69735d3bd8fd874a9817c26b5b009921110fb416 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 3 May 2023 11:58:51 +0100 Subject: [PATCH 3234/3836] Blackify openstack.compute (tests) Change Ic8e372a7ca999414ad93fb88e03b92798052cc3e ran black over the openstack.compute module but forgot the associated tests. Correct this. Black used with the '-l 79 -S' flags. Change-Id: I6462d1423b57ff604e1ede977d27a9dd4f2c9c50 Signed-off-by: Stephen Finucane --- .../functional/compute/v2/test_extension.py | 1 - .../functional/compute/v2/test_flavor.py | 45 +- .../functional/compute/v2/test_hypervisor.py | 1 - .../tests/functional/compute/v2/test_image.py | 1 - .../functional/compute/v2/test_keypair.py | 7 +- .../functional/compute/v2/test_limits.py | 1 - .../functional/compute/v2/test_quota_set.py | 18 +- .../functional/compute/v2/test_server.py | 23 +- .../functional/compute/v2/test_service.py | 10 +- openstack/tests/unit/compute/test_version.py | 1 - .../tests/unit/compute/v2/test_aggregate.py | 15 +- .../unit/compute/v2/test_availability_zone.py | 3 +- .../tests/unit/compute/v2/test_extension.py | 1 - .../tests/unit/compute/v2/test_flavor.py | 92 ++- .../tests/unit/compute/v2/test_hypervisor.py | 62 +- openstack/tests/unit/compute/v2/test_image.py | 27 +- .../tests/unit/compute/v2/test_keypair.py | 11 +- .../tests/unit/compute/v2/test_limits.py | 170 +++-- .../tests/unit/compute/v2/test_migration.py | 1 - openstack/tests/unit/compute/v2/test_proxy.py | 608 +++++++++------- .../tests/unit/compute/v2/test_server.py | 689 ++++++++++-------- .../unit/compute/v2/test_server_actions.py | 3 +- .../compute/v2/test_server_diagnostics.py | 20 +- .../unit/compute/v2/test_server_group.py | 12 +- .../unit/compute/v2/test_server_interface.py | 3 +- .../tests/unit/compute/v2/test_server_ip.py | 30 +- .../unit/compute/v2/test_server_migration.py | 12 +- .../compute/v2/test_server_remote_console.py | 22 +- .../tests/unit/compute/v2/test_service.py | 88 ++- .../unit/compute/v2/test_volume_attachment.py | 18 +- 30 files changed, 1120 insertions(+), 875 deletions(-) diff --git a/openstack/tests/functional/compute/v2/test_extension.py b/openstack/tests/functional/compute/v2/test_extension.py index e4ff2a408..8bb0e83ce 100644 --- a/openstack/tests/functional/compute/v2/test_extension.py +++ b/openstack/tests/functional/compute/v2/test_extension.py @@ -15,7 +15,6 @@ class TestExtension(base.BaseFunctionalTest): - def test_list(self): extensions = list(self.conn.compute.extensions()) self.assertGreater(len(extensions), 0) diff --git a/openstack/tests/functional/compute/v2/test_flavor.py b/openstack/tests/functional/compute/v2/test_flavor.py index 13281edcb..8b72657f8 100644 --- a/openstack/tests/functional/compute/v2/test_flavor.py +++ b/openstack/tests/functional/compute/v2/test_flavor.py @@ -16,7 +16,6 @@ class TestFlavor(base.BaseFunctionalTest): - def setUp(self): super(TestFlavor, self).setUp() self.new_item_name = self.getUniqueString('flavor') @@ -42,14 +41,18 @@ def test_find_flavors_by_name(self): self.assertEqual(rslt.name, self.one_flavor.name) def test_find_flavors_no_match_ignore_true(self): - rslt = self.conn.compute.find_flavor("not a flavor", - ignore_missing=True) + rslt = self.conn.compute.find_flavor( + "not a flavor", ignore_missing=True + ) self.assertIsNone(rslt) def test_find_flavors_no_match_ignore_false(self): - self.assertRaises(exceptions.ResourceNotFound, - self.conn.compute.find_flavor, - "not a flavor", ignore_missing=False) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.compute.find_flavor, + "not a flavor", + ignore_missing=False, + ) def test_list_flavors(self): pub_flavor_name = self.new_item_name + '_public' @@ -81,11 +84,8 @@ def test_list_flavors(self): def test_flavor_access(self): flavor_name = uuid.uuid4().hex flv = self.operator_cloud.compute.create_flavor( - is_public=False, - name=flavor_name, - ram=128, - vcpus=1, - disk=0) + is_public=False, name=flavor_name, ram=128, vcpus=1, disk=0 + ) self.addCleanup(self.conn.compute.delete_flavor, flv.id) # Validate the 'demo' user cannot see the new flavor flv_cmp = self.user_cloud.compute.find_flavor(flavor_name) @@ -101,34 +101,29 @@ def test_flavor_access(self): # Now give 'demo' access self.operator_cloud.compute.flavor_add_tenant_access( - flv.id, project['id']) + flv.id, project['id'] + ) # Now see if the 'demo' user has access to it - flv_cmp = self.user_cloud.compute.find_flavor( - flavor_name) + flv_cmp = self.user_cloud.compute.find_flavor(flavor_name) self.assertIsNotNone(flv_cmp) # Now remove 'demo' access and check we can't find it self.operator_cloud.compute.flavor_remove_tenant_access( - flv.id, project['id']) + flv.id, project['id'] + ) - flv_cmp = self.user_cloud.compute.find_flavor( - flavor_name) + flv_cmp = self.user_cloud.compute.find_flavor(flavor_name) self.assertIsNone(flv_cmp) def test_extra_props_calls(self): flavor_name = uuid.uuid4().hex flv = self.conn.compute.create_flavor( - is_public=False, - name=flavor_name, - ram=128, - vcpus=1, - disk=0) + is_public=False, name=flavor_name, ram=128, vcpus=1, disk=0 + ) self.addCleanup(self.conn.compute.delete_flavor, flv.id) # Create extra_specs - specs = { - 'a': 'b' - } + specs = {'a': 'b'} self.conn.compute.create_flavor_extra_specs(flv, extra_specs=specs) # verify specs flv_cmp = self.conn.compute.fetch_flavor_extra_specs(flv) diff --git a/openstack/tests/functional/compute/v2/test_hypervisor.py b/openstack/tests/functional/compute/v2/test_hypervisor.py index 383a51dc8..2edeffc20 100644 --- a/openstack/tests/functional/compute/v2/test_hypervisor.py +++ b/openstack/tests/functional/compute/v2/test_hypervisor.py @@ -14,7 +14,6 @@ class TestHypervisor(base.BaseFunctionalTest): - def setUp(self): super(TestHypervisor, self).setUp() diff --git a/openstack/tests/functional/compute/v2/test_image.py b/openstack/tests/functional/compute/v2/test_image.py index 46b4c9cbf..5b6e131f2 100644 --- a/openstack/tests/functional/compute/v2/test_image.py +++ b/openstack/tests/functional/compute/v2/test_image.py @@ -16,7 +16,6 @@ class TestImage(base.BaseFunctionalTest): - def test_images(self): images = list(self.conn.compute.images()) self.assertGreater(len(images), 0) diff --git a/openstack/tests/functional/compute/v2/test_keypair.py b/openstack/tests/functional/compute/v2/test_keypair.py index f76707174..d642ca74d 100644 --- a/openstack/tests/functional/compute/v2/test_keypair.py +++ b/openstack/tests/functional/compute/v2/test_keypair.py @@ -16,7 +16,6 @@ class TestKeypair(base.BaseFunctionalTest): - def setUp(self): super(TestKeypair, self).setUp() @@ -50,7 +49,6 @@ def test_list(self): class TestKeypairAdmin(base.BaseFunctionalTest): - def setUp(self): super(TestKeypairAdmin, self).setUp() self._set_operator_cloud(interface='admin') @@ -58,8 +56,9 @@ def setUp(self): self.NAME = self.getUniqueString().split('.')[-1] self.USER = self.operator_cloud.list_users()[0] - sot = self.conn.compute.create_keypair(name=self.NAME, - user_id=self.USER.id) + sot = self.conn.compute.create_keypair( + name=self.NAME, user_id=self.USER.id + ) assert isinstance(sot, keypair.Keypair) self.assertEqual(self.NAME, sot.name) self.assertEqual(self.USER.id, sot.user_id) diff --git a/openstack/tests/functional/compute/v2/test_limits.py b/openstack/tests/functional/compute/v2/test_limits.py index 27c362ae2..382fcd20b 100644 --- a/openstack/tests/functional/compute/v2/test_limits.py +++ b/openstack/tests/functional/compute/v2/test_limits.py @@ -14,7 +14,6 @@ class TestLimits(base.BaseFunctionalTest): - def test_limits(self): sot = self.conn.compute.get_limits() self.assertIsNotNone(sot.absolute['instances']) diff --git a/openstack/tests/functional/compute/v2/test_quota_set.py b/openstack/tests/functional/compute/v2/test_quota_set.py index 5fa04431d..ba7461bef 100644 --- a/openstack/tests/functional/compute/v2/test_quota_set.py +++ b/openstack/tests/functional/compute/v2/test_quota_set.py @@ -14,35 +14,31 @@ class TestQS(base.BaseFunctionalTest): - def test_qs(self): - sot = self.conn.compute.get_quota_set( - self.conn.current_project_id - ) + sot = self.conn.compute.get_quota_set(self.conn.current_project_id) self.assertIsNotNone(sot.key_pairs) def test_qs_user(self): sot = self.conn.compute.get_quota_set( self.conn.current_project_id, - user_id=self.conn.session.auth.get_user_id(self.conn.compute) + user_id=self.conn.session.auth.get_user_id(self.conn.compute), ) self.assertIsNotNone(sot.key_pairs) def test_update(self): - sot = self.conn.compute.get_quota_set( - self.conn.current_project_id - ) + sot = self.conn.compute.get_quota_set(self.conn.current_project_id) self.conn.compute.update_quota_set( sot, query={ 'user_id': self.conn.session.auth.get_user_id( - self.conn.compute) + self.conn.compute + ) }, - key_pairs=100 + key_pairs=100, ) def test_revert(self): self.conn.compute.revert_quota_set( self.conn.current_project_id, - user_id=self.conn.session.auth.get_user_id(self.conn.compute) + user_id=self.conn.session.auth.get_user_id(self.conn.compute), ) diff --git a/openstack/tests/functional/compute/v2/test_server.py b/openstack/tests/functional/compute/v2/test_server.py index 8803607a4..c19566edb 100644 --- a/openstack/tests/functional/compute/v2/test_server.py +++ b/openstack/tests/functional/compute/v2/test_server.py @@ -16,7 +16,6 @@ class TestServerAdmin(ft_base.BaseComputeTest): - def setUp(self): super(TestServerAdmin, self).setUp() self._set_operator_cloud(interface='admin') @@ -47,8 +46,9 @@ def setUp(self): def tearDown(self): sot = self.conn.compute.delete_server(self.server.id) - self.conn.compute.wait_for_delete(self.server, - wait=self._wait_for_timeout) + self.conn.compute.wait_for_delete( + self.server, wait=self._wait_for_timeout + ) self.assertIsNone(sot) super(TestServerAdmin, self).tearDown() @@ -65,7 +65,6 @@ def test_get(self): class TestServer(ft_base.BaseComputeTest): - def setUp(self): super(TestServer, self).setUp() self.NAME = self.getUniqueString() @@ -75,9 +74,8 @@ def setUp(self): self.cidr = '10.99.99.0/16' self.network, self.subnet = test_network.create_network( - self.conn, - self.NAME, - self.cidr) + self.conn, self.NAME, self.cidr + ) self.assertIsNotNone(self.network) sot = self.conn.compute.create_server( @@ -95,8 +93,9 @@ def tearDown(self): sot = self.conn.compute.delete_server(self.server.id) self.assertIsNone(sot) # Need to wait for the stack to go away before network delete - self.conn.compute.wait_for_delete(self.server, - wait=self._wait_for_timeout) + self.conn.compute.wait_for_delete( + self.server, wait=self._wait_for_timeout + ) test_network.delete_network(self.conn, self.network, self.subnet) super(TestServer, self).tearDown() @@ -166,13 +165,15 @@ def test_server_metadata(self): # delete metadata self.conn.compute.delete_server_metadata( - test_server, test_server.metadata.keys()) + test_server, test_server.metadata.keys() + ) test_server = self.conn.compute.get_server_metadata(test_server) self.assertFalse(test_server.metadata) def test_server_remote_console(self): console = self.conn.compute.create_server_remote_console( - self.server, protocol='vnc', type='novnc') + self.server, protocol='vnc', type='novnc' + ) self.assertEqual('vnc', console.protocol) self.assertEqual('novnc', console.type) self.assertTrue(console.url.startswith('http')) diff --git a/openstack/tests/functional/compute/v2/test_service.py b/openstack/tests/functional/compute/v2/test_service.py index 31323cc88..2403aaf0e 100644 --- a/openstack/tests/functional/compute/v2/test_service.py +++ b/openstack/tests/functional/compute/v2/test_service.py @@ -14,7 +14,6 @@ class TestService(base.BaseFunctionalTest): - def setUp(self): super(TestService, self).setUp() self._set_operator_cloud(interface='admin') @@ -34,9 +33,11 @@ def test_update(self): for srv in self.conn.compute.services(): if srv.name == 'nova-compute': self.conn.compute.update_service_forced_down( - srv, None, None, True) + srv, None, None, True + ) self.conn.compute.update_service_forced_down( - srv, srv.host, srv.binary, False) + srv, srv.host, srv.binary, False + ) self.conn.compute.update_service(srv, status='enabled') def test_find(self): @@ -44,4 +45,5 @@ def test_find(self): if srv.name != 'nova-conductor': # In devstack there are 2 nova-conductor instances on same host self.conn.compute.find_service( - srv.name, host=srv.host, ignore_missing=False) + srv.name, host=srv.host, ignore_missing=False + ) diff --git a/openstack/tests/unit/compute/test_version.py b/openstack/tests/unit/compute/test_version.py index 9bf6bff23..f33ca9f57 100644 --- a/openstack/tests/unit/compute/test_version.py +++ b/openstack/tests/unit/compute/test_version.py @@ -24,7 +24,6 @@ class TestVersion(base.TestCase): - def test_basic(self): sot = version.Version() self.assertEqual('version', sot.resource_key) diff --git a/openstack/tests/unit/compute/v2/test_aggregate.py b/openstack/tests/unit/compute/v2/test_aggregate.py index 220587dbb..9bc0f6b3c 100644 --- a/openstack/tests/unit/compute/v2/test_aggregate.py +++ b/openstack/tests/unit/compute/v2/test_aggregate.py @@ -29,12 +29,11 @@ "deleted_at": None, "id": 4, "uuid": IDENTIFIER, - "metadata": {"type": "public", "family": "m-family"} + "metadata": {"type": "public", "family": "m-family"}, } class TestAggregate(base.TestCase): - def setUp(self): super(TestAggregate, self).setUp() self.resp = mock.Mock() @@ -76,8 +75,7 @@ def test_add_host(self): url = 'os-aggregates/4/action' body = {"add_host": {"host": "host1"}} - self.sess.post.assert_called_with( - url, json=body, microversion=None) + self.sess.post.assert_called_with(url, json=body, microversion=None) def test_remove_host(self): sot = aggregate.Aggregate(**EXAMPLE) @@ -86,8 +84,7 @@ def test_remove_host(self): url = 'os-aggregates/4/action' body = {"remove_host": {"host": "host1"}} - self.sess.post.assert_called_with( - url, json=body, microversion=None) + self.sess.post.assert_called_with(url, json=body, microversion=None) def test_set_metadata(self): sot = aggregate.Aggregate(**EXAMPLE) @@ -96,8 +93,7 @@ def test_set_metadata(self): url = 'os-aggregates/4/action' body = {"set_metadata": {"metadata": {"key: value"}}} - self.sess.post.assert_called_with( - url, json=body, microversion=None) + self.sess.post.assert_called_with(url, json=body, microversion=None) def test_precache_image(self): sot = aggregate.Aggregate(**EXAMPLE) @@ -107,4 +103,5 @@ def test_precache_image(self): url = 'os-aggregates/4/images' body = {"cache": ['1']} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) diff --git a/openstack/tests/unit/compute/v2/test_availability_zone.py b/openstack/tests/unit/compute/v2/test_availability_zone.py index e454140e0..c71d96f00 100644 --- a/openstack/tests/unit/compute/v2/test_availability_zone.py +++ b/openstack/tests/unit/compute/v2/test_availability_zone.py @@ -19,12 +19,11 @@ 'id': IDENTIFIER, 'zoneState': 'available', 'hosts': 'host1', - 'zoneName': 'zone1' + 'zoneName': 'zone1', } class TestAvailabilityZone(base.TestCase): - def test_basic(self): sot = az.AvailabilityZone() self.assertEqual('availabilityZoneInfo', sot.resources_key) diff --git a/openstack/tests/unit/compute/v2/test_extension.py b/openstack/tests/unit/compute/v2/test_extension.py index 08f4930a1..ddf54bd9e 100644 --- a/openstack/tests/unit/compute/v2/test_extension.py +++ b/openstack/tests/unit/compute/v2/test_extension.py @@ -26,7 +26,6 @@ class TestExtension(base.TestCase): - def test_basic(self): sot = extension.Extension() self.assertEqual('extension', sot.resource_key) diff --git a/openstack/tests/unit/compute/v2/test_flavor.py b/openstack/tests/unit/compute/v2/test_flavor.py index 6cc5134c1..d3bc42350 100644 --- a/openstack/tests/unit/compute/v2/test_flavor.py +++ b/openstack/tests/unit/compute/v2/test_flavor.py @@ -31,7 +31,7 @@ 'swap': 8, 'OS-FLV-EXT-DATA:ephemeral': 9, 'OS-FLV-DISABLED:disabled': False, - 'rxtx_factor': 11.0 + 'rxtx_factor': 11.0, } DEFAULTS_EXAMPLE = { 'links': '2', @@ -41,7 +41,6 @@ class TestFlavor(base.TestCase): - def setUp(self): super(TestFlavor, self).setUp() self.sess = mock.Mock(spec=adapter.Adapter) @@ -59,14 +58,18 @@ def test_basic(self): self.assertTrue(sot.allow_list) self.assertTrue(sot.allow_commit) - self.assertDictEqual({"sort_key": "sort_key", - "sort_dir": "sort_dir", - "min_disk": "minDisk", - "min_ram": "minRam", - "limit": "limit", - "marker": "marker", - "is_public": "is_public"}, - sot._query_mapping._mapping) + self.assertDictEqual( + { + "sort_key": "sort_key", + "sort_dir": "sort_dir", + "min_disk": "minDisk", + "min_ram": "minRam", + "limit": "limit", + "marker": "marker", + "is_public": "is_public", + }, + sot._query_mapping._mapping, + ) def test_make_basic(self): sot = flavor.Flavor(**BASIC_EXAMPLE) @@ -74,15 +77,18 @@ def test_make_basic(self): self.assertEqual(BASIC_EXAMPLE['name'], sot.name) self.assertEqual(BASIC_EXAMPLE['description'], sot.description) self.assertEqual(BASIC_EXAMPLE['disk'], sot.disk) - self.assertEqual(BASIC_EXAMPLE['os-flavor-access:is_public'], - sot.is_public) + self.assertEqual( + BASIC_EXAMPLE['os-flavor-access:is_public'], sot.is_public + ) self.assertEqual(BASIC_EXAMPLE['ram'], sot.ram) self.assertEqual(BASIC_EXAMPLE['vcpus'], sot.vcpus) self.assertEqual(BASIC_EXAMPLE['swap'], sot.swap) - self.assertEqual(BASIC_EXAMPLE['OS-FLV-EXT-DATA:ephemeral'], - sot.ephemeral) - self.assertEqual(BASIC_EXAMPLE['OS-FLV-DISABLED:disabled'], - sot.is_disabled) + self.assertEqual( + BASIC_EXAMPLE['OS-FLV-EXT-DATA:ephemeral'], sot.ephemeral + ) + self.assertEqual( + BASIC_EXAMPLE['OS-FLV-DISABLED:disabled'], sot.is_disabled + ) self.assertEqual(BASIC_EXAMPLE['rxtx_factor'], sot.rxtx_factor) def test_make_defaults(self): @@ -119,10 +125,8 @@ def test_add_tenant_access(self): self.sess.post.assert_called_with( 'flavors/IDENTIFIER/action', - json={ - 'addTenantAccess': { - 'tenant': 'fake_tenant'}}, - headers={'Accept': ''} + json={'addTenantAccess': {'tenant': 'fake_tenant'}}, + headers={'Accept': ''}, ) def test_remove_tenant_access(self): @@ -137,19 +141,18 @@ def test_remove_tenant_access(self): self.sess.post.assert_called_with( 'flavors/IDENTIFIER/action', - json={ - 'removeTenantAccess': { - 'tenant': 'fake_tenant'}}, - headers={'Accept': ''} + json={'removeTenantAccess': {'tenant': 'fake_tenant'}}, + headers={'Accept': ''}, ) def test_get_flavor_access(self): sot = flavor.Flavor(**BASIC_EXAMPLE) resp = mock.Mock() - resp.body = {'flavor_access': [ - {'flavor_id': 'fake_flavor', - 'tenant_id': 'fake_tenant'} - ]} + resp.body = { + 'flavor_access': [ + {'flavor_id': 'fake_flavor', 'tenant_id': 'fake_tenant'} + ] + } resp.json = mock.Mock(return_value=resp.body) resp.status_code = 200 self.sess.get = mock.Mock(return_value=resp) @@ -165,11 +168,7 @@ def test_get_flavor_access(self): def test_fetch_extra_specs(self): sot = flavor.Flavor(**BASIC_EXAMPLE) resp = mock.Mock() - resp.body = { - 'extra_specs': - {'a': 'b', - 'c': 'd'} - } + resp.body = {'extra_specs': {'a': 'b', 'c': 'd'}} resp.json = mock.Mock(return_value=resp.body) resp.status_code = 200 self.sess.get = mock.Mock(return_value=resp) @@ -178,7 +177,7 @@ def test_fetch_extra_specs(self): self.sess.get.assert_called_with( 'flavors/IDENTIFIER/os-extra_specs', - microversion=self.sess.default_microversion + microversion=self.sess.default_microversion, ) self.assertEqual(resp.body['extra_specs'], rsp.extra_specs) @@ -186,14 +185,9 @@ def test_fetch_extra_specs(self): def test_create_extra_specs(self): sot = flavor.Flavor(**BASIC_EXAMPLE) - specs = { - 'a': 'b', - 'c': 'd' - } + specs = {'a': 'b', 'c': 'd'} resp = mock.Mock() - resp.body = { - 'extra_specs': specs - } + resp.body = {'extra_specs': specs} resp.json = mock.Mock(return_value=resp.body) resp.status_code = 200 self.sess.post = mock.Mock(return_value=resp) @@ -203,7 +197,7 @@ def test_create_extra_specs(self): self.sess.post.assert_called_with( 'flavors/IDENTIFIER/os-extra_specs', json={'extra_specs': specs}, - microversion=self.sess.default_microversion + microversion=self.sess.default_microversion, ) self.assertEqual(resp.body['extra_specs'], rsp.extra_specs) @@ -212,9 +206,7 @@ def test_create_extra_specs(self): def test_get_extra_specs_property(self): sot = flavor.Flavor(**BASIC_EXAMPLE) resp = mock.Mock() - resp.body = { - 'a': 'b' - } + resp.body = {'a': 'b'} resp.json = mock.Mock(return_value=resp.body) resp.status_code = 200 self.sess.get = mock.Mock(return_value=resp) @@ -223,7 +215,7 @@ def test_get_extra_specs_property(self): self.sess.get.assert_called_with( 'flavors/IDENTIFIER/os-extra_specs/a', - microversion=self.sess.default_microversion + microversion=self.sess.default_microversion, ) self.assertEqual('b', rsp) @@ -231,9 +223,7 @@ def test_get_extra_specs_property(self): def test_update_extra_specs_property(self): sot = flavor.Flavor(**BASIC_EXAMPLE) resp = mock.Mock() - resp.body = { - 'a': 'b' - } + resp.body = {'a': 'b'} resp.json = mock.Mock(return_value=resp.body) resp.status_code = 200 self.sess.put = mock.Mock(return_value=resp) @@ -243,7 +233,7 @@ def test_update_extra_specs_property(self): self.sess.put.assert_called_with( 'flavors/IDENTIFIER/os-extra_specs/a', json={'a': 'b'}, - microversion=self.sess.default_microversion + microversion=self.sess.default_microversion, ) self.assertEqual('b', rsp) @@ -260,7 +250,7 @@ def test_delete_extra_specs_property(self): self.sess.delete.assert_called_with( 'flavors/IDENTIFIER/os-extra_specs/a', - microversion=self.sess.default_microversion + microversion=self.sess.default_microversion, ) self.assertIsNone(rsp) diff --git a/openstack/tests/unit/compute/v2/test_hypervisor.py b/openstack/tests/unit/compute/v2/test_hypervisor.py index e6e33189b..7c63f0a9a 100644 --- a/openstack/tests/unit/compute/v2/test_hypervisor.py +++ b/openstack/tests/unit/compute/v2/test_hypervisor.py @@ -25,27 +25,20 @@ "arch": "x86_64", "model": "Nehalem", "vendor": "Intel", - "features": [ - "pge", - "clflush" - ], - "topology": { - "cores": 1, - "threads": 1, - "sockets": 4 - } + "features": ["pge", "clflush"], + "topology": {"cores": 1, "threads": 1, "sockets": 4}, }, "state": "up", "status": "enabled", "servers": [ { "name": "test_server1", - "uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + "uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", }, { "name": "test_server2", - "uuid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" - } + "uuid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + }, ], "host_ip": "1.1.1.1", "hypervisor_hostname": "fake-mini", @@ -54,11 +47,12 @@ "id": "b1e43b5f-eec1-44e0-9f10-7b4945c0226d", "uptime": ( " 08:32:11 up 93 days, 18:25, 12 users, " - "load average: 0.20, 0.12, 0.14"), + "load average: 0.20, 0.12, 0.14" + ), "service": { "host": "043b3cacf6f34c90a7245151fc8ebcda", "id": "5d343e1d-938e-4284-b98b-6a2b5406ba76", - "disabled_reason": None + "disabled_reason": None, }, # deprecated attributes "vcpus_used": 0, @@ -76,7 +70,6 @@ class TestHypervisor(base.TestCase): - def setUp(self): super(TestHypervisor, self).setUp() self.sess = mock.Mock(spec=adapter.Adapter) @@ -91,12 +84,15 @@ def test_basic(self): self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_list) - self.assertDictEqual({'hypervisor_hostname_pattern': - 'hypervisor_hostname_pattern', - 'limit': 'limit', - 'marker': 'marker', - 'with_servers': 'with_servers'}, - sot._query_mapping._mapping) + self.assertDictEqual( + { + 'hypervisor_hostname_pattern': 'hypervisor_hostname_pattern', + 'limit': 'limit', + 'marker': 'marker', + 'with_servers': 'with_servers', + }, + sot._query_mapping._mapping, + ) def test_make_it(self): sot = hypervisor.Hypervisor(**EXAMPLE) @@ -126,8 +122,11 @@ def test_make_it(self): self.assertEqual(EXAMPLE['local_gb'], sot.local_disk_size) self.assertEqual(EXAMPLE['free_ram_mb'], sot.memory_free) - @mock.patch('openstack.utils.supports_microversion', autospec=True, - return_value=False) + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=False, + ) def test_get_uptime(self, mv_mock): sot = hypervisor.Hypervisor(**copy.deepcopy(EXAMPLE)) rsp = { @@ -136,7 +135,7 @@ def test_get_uptime(self, mv_mock): "id": sot.id, "state": "up", "status": "enabled", - "uptime": "08:32:11 up 93 days, 18:25, 12 users" + "uptime": "08:32:11 up 93 days, 18:25, 12 users", } } resp = mock.Mock() @@ -149,17 +148,16 @@ def test_get_uptime(self, mv_mock): hyp = sot.get_uptime(self.sess) self.sess.get.assert_called_with( 'os-hypervisors/{id}/uptime'.format(id=sot.id), - microversion=self.sess.default_microversion + microversion=self.sess.default_microversion, ) self.assertEqual(rsp['hypervisor']['uptime'], hyp.uptime) self.assertEqual(rsp['hypervisor']['status'], sot.status) - @mock.patch('openstack.utils.supports_microversion', autospec=True, - return_value=True) + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=True, + ) def test_get_uptime_after_2_88(self, mv_mock): sot = hypervisor.Hypervisor(**copy.deepcopy(EXAMPLE)) - self.assertRaises( - exceptions.SDKException, - sot.get_uptime, - self.sess - ) + self.assertRaises(exceptions.SDKException, sot.get_uptime, self.sess) diff --git a/openstack/tests/unit/compute/v2/test_image.py b/openstack/tests/unit/compute/v2/test_image.py index a0b4957d5..01019da42 100644 --- a/openstack/tests/unit/compute/v2/test_image.py +++ b/openstack/tests/unit/compute/v2/test_image.py @@ -27,12 +27,11 @@ 'progress': 5, 'status': '6', 'updated': '2015-03-09T12:15:57.233772', - 'OS-EXT-IMG-SIZE:size': 8 + 'OS-EXT-IMG-SIZE:size': 8, } class TestImage(base.TestCase): - def test_basic(self): sot = image.Image() self.assertEqual('image', sot.resource_key) @@ -44,16 +43,20 @@ def test_basic(self): self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertDictEqual({"server": "server", - "name": "name", - "status": "status", - "type": "type", - "min_disk": "minDisk", - "min_ram": "minRam", - "changes_since": "changes-since", - "limit": "limit", - "marker": "marker"}, - sot._query_mapping._mapping) + self.assertDictEqual( + { + "server": "server", + "name": "name", + "status": "status", + "type": "type", + "min_disk": "minDisk", + "min_ram": "minRam", + "changes_since": "changes-since", + "limit": "limit", + "marker": "marker", + }, + sot._query_mapping._mapping, + ) def test_make_basic(self): sot = image.Image(**EXAMPLE) diff --git a/openstack/tests/unit/compute/v2/test_keypair.py b/openstack/tests/unit/compute/v2/test_keypair.py index ee5aaecd8..d471d2fc2 100644 --- a/openstack/tests/unit/compute/v2/test_keypair.py +++ b/openstack/tests/unit/compute/v2/test_keypair.py @@ -22,12 +22,11 @@ 'public_key': '3', 'private_key': '4', 'type': 'ssh', - 'user_id': '5' + 'user_id': '5', } class TestKeypair(base.TestCase): - def test_basic(self): sot = keypair.Keypair() self.assertEqual('keypair', sot.resource_key) @@ -39,10 +38,10 @@ def test_basic(self): self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertDictEqual({'limit': 'limit', - 'marker': 'marker', - 'user_id': 'user_id'}, - sot._query_mapping._mapping) + self.assertDictEqual( + {'limit': 'limit', 'marker': 'marker', 'user_id': 'user_id'}, + sot._query_mapping._mapping, + ) def test_make_it(self): sot = keypair.Keypair(**EXAMPLE) diff --git a/openstack/tests/unit/compute/v2/test_limits.py b/openstack/tests/unit/compute/v2/test_limits.py index 3c44d90cb..90da84d9d 100644 --- a/openstack/tests/unit/compute/v2/test_limits.py +++ b/openstack/tests/unit/compute/v2/test_limits.py @@ -35,7 +35,7 @@ "totalRAMUsed": 4, "totalInstancesUsed": 5, "totalServerGroupsUsed": 6, - "totalCoresUsed": 7 + "totalCoresUsed": 7, } RATE_LIMIT = { @@ -45,23 +45,17 @@ "remaining": 120, "unit": "MINUTE", "value": 120, - "verb": "POST" + "verb": "POST", }, ], "regex": ".*", - "uri": "*" + "uri": "*", } -LIMITS_BODY = { - "limits": { - "absolute": ABSOLUTE_LIMITS, - "rate": [RATE_LIMIT] - } -} +LIMITS_BODY = {"limits": {"absolute": ABSOLUTE_LIMITS, "rate": [RATE_LIMIT]}} class TestAbsoluteLimits(base.TestCase): - def test_basic(self): sot = limits.AbsoluteLimits() self.assertIsNone(sot.resource_key) @@ -76,38 +70,44 @@ def test_basic(self): def test_make_it(self): sot = limits.AbsoluteLimits(**ABSOLUTE_LIMITS) self.assertEqual(ABSOLUTE_LIMITS["maxImageMeta"], sot.image_meta) - self.assertEqual(ABSOLUTE_LIMITS["maxSecurityGroupRules"], - sot.security_group_rules) - self.assertEqual(ABSOLUTE_LIMITS["maxSecurityGroups"], - sot.security_groups) + self.assertEqual( + ABSOLUTE_LIMITS["maxSecurityGroupRules"], sot.security_group_rules + ) + self.assertEqual( + ABSOLUTE_LIMITS["maxSecurityGroups"], sot.security_groups + ) self.assertEqual(ABSOLUTE_LIMITS["maxServerMeta"], sot.server_meta) self.assertEqual(ABSOLUTE_LIMITS["maxTotalCores"], sot.total_cores) - self.assertEqual(ABSOLUTE_LIMITS["maxTotalFloatingIps"], - sot.floating_ips) - self.assertEqual(ABSOLUTE_LIMITS["maxTotalInstances"], - sot.instances) - self.assertEqual(ABSOLUTE_LIMITS["maxTotalKeypairs"], - sot.keypairs) - self.assertEqual(ABSOLUTE_LIMITS["maxTotalRAMSize"], - sot.total_ram) + self.assertEqual( + ABSOLUTE_LIMITS["maxTotalFloatingIps"], sot.floating_ips + ) + self.assertEqual(ABSOLUTE_LIMITS["maxTotalInstances"], sot.instances) + self.assertEqual(ABSOLUTE_LIMITS["maxTotalKeypairs"], sot.keypairs) + self.assertEqual(ABSOLUTE_LIMITS["maxTotalRAMSize"], sot.total_ram) self.assertEqual(ABSOLUTE_LIMITS["maxServerGroups"], sot.server_groups) - self.assertEqual(ABSOLUTE_LIMITS["maxServerGroupMembers"], - sot.server_group_members) - self.assertEqual(ABSOLUTE_LIMITS["totalFloatingIpsUsed"], - sot.floating_ips_used) - self.assertEqual(ABSOLUTE_LIMITS["totalSecurityGroupsUsed"], - sot.security_groups_used) + self.assertEqual( + ABSOLUTE_LIMITS["maxServerGroupMembers"], sot.server_group_members + ) + self.assertEqual( + ABSOLUTE_LIMITS["totalFloatingIpsUsed"], sot.floating_ips_used + ) + self.assertEqual( + ABSOLUTE_LIMITS["totalSecurityGroupsUsed"], + sot.security_groups_used, + ) self.assertEqual(ABSOLUTE_LIMITS["totalRAMUsed"], sot.total_ram_used) - self.assertEqual(ABSOLUTE_LIMITS["totalInstancesUsed"], - sot.instances_used) - self.assertEqual(ABSOLUTE_LIMITS["totalServerGroupsUsed"], - sot.server_groups_used) - self.assertEqual(ABSOLUTE_LIMITS["totalCoresUsed"], - sot.total_cores_used) + self.assertEqual( + ABSOLUTE_LIMITS["totalInstancesUsed"], sot.instances_used + ) + self.assertEqual( + ABSOLUTE_LIMITS["totalServerGroupsUsed"], sot.server_groups_used + ) + self.assertEqual( + ABSOLUTE_LIMITS["totalCoresUsed"], sot.total_cores_used + ) class TestRateLimit(base.TestCase): - def test_basic(self): sot = limits.RateLimit() self.assertIsNone(sot.resource_key) @@ -128,7 +128,6 @@ def test_make_it(self): class TestLimits(base.TestCase): - def test_basic(self): sot = limits.Limits() self.assertEqual("limits", sot.resource_key) @@ -139,11 +138,7 @@ def test_basic(self): self.assertFalse(sot.allow_delete) self.assertFalse(sot.allow_list) self.assertDictEqual( - { - 'limit': 'limit', - 'marker': 'marker', - 'tenant_id': 'tenant_id' - }, + {'limit': 'limit', 'marker': 'marker', 'tenant_id': 'tenant_id'}, sot._query_mapping._mapping, ) @@ -158,40 +153,62 @@ def test_get(self): sot = limits.Limits().fetch(sess) - self.assertEqual(ABSOLUTE_LIMITS["maxImageMeta"], - sot.absolute.image_meta) - self.assertEqual(ABSOLUTE_LIMITS["maxSecurityGroupRules"], - sot.absolute.security_group_rules) - self.assertEqual(ABSOLUTE_LIMITS["maxSecurityGroups"], - sot.absolute.security_groups) - self.assertEqual(ABSOLUTE_LIMITS["maxServerMeta"], - sot.absolute.server_meta) - self.assertEqual(ABSOLUTE_LIMITS["maxTotalCores"], - sot.absolute.total_cores) - self.assertEqual(ABSOLUTE_LIMITS["maxTotalFloatingIps"], - sot.absolute.floating_ips) - self.assertEqual(ABSOLUTE_LIMITS["maxTotalInstances"], - sot.absolute.instances) - self.assertEqual(ABSOLUTE_LIMITS["maxTotalKeypairs"], - sot.absolute.keypairs) - self.assertEqual(ABSOLUTE_LIMITS["maxTotalRAMSize"], - sot.absolute.total_ram) - self.assertEqual(ABSOLUTE_LIMITS["maxServerGroups"], - sot.absolute.server_groups) - self.assertEqual(ABSOLUTE_LIMITS["maxServerGroupMembers"], - sot.absolute.server_group_members) - self.assertEqual(ABSOLUTE_LIMITS["totalFloatingIpsUsed"], - sot.absolute.floating_ips_used) - self.assertEqual(ABSOLUTE_LIMITS["totalSecurityGroupsUsed"], - sot.absolute.security_groups_used) - self.assertEqual(ABSOLUTE_LIMITS["totalRAMUsed"], - sot.absolute.total_ram_used) - self.assertEqual(ABSOLUTE_LIMITS["totalInstancesUsed"], - sot.absolute.instances_used) - self.assertEqual(ABSOLUTE_LIMITS["totalServerGroupsUsed"], - sot.absolute.server_groups_used) - self.assertEqual(ABSOLUTE_LIMITS["totalCoresUsed"], - sot.absolute.total_cores_used) + self.assertEqual( + ABSOLUTE_LIMITS["maxImageMeta"], sot.absolute.image_meta + ) + self.assertEqual( + ABSOLUTE_LIMITS["maxSecurityGroupRules"], + sot.absolute.security_group_rules, + ) + self.assertEqual( + ABSOLUTE_LIMITS["maxSecurityGroups"], sot.absolute.security_groups + ) + self.assertEqual( + ABSOLUTE_LIMITS["maxServerMeta"], sot.absolute.server_meta + ) + self.assertEqual( + ABSOLUTE_LIMITS["maxTotalCores"], sot.absolute.total_cores + ) + self.assertEqual( + ABSOLUTE_LIMITS["maxTotalFloatingIps"], sot.absolute.floating_ips + ) + self.assertEqual( + ABSOLUTE_LIMITS["maxTotalInstances"], sot.absolute.instances + ) + self.assertEqual( + ABSOLUTE_LIMITS["maxTotalKeypairs"], sot.absolute.keypairs + ) + self.assertEqual( + ABSOLUTE_LIMITS["maxTotalRAMSize"], sot.absolute.total_ram + ) + self.assertEqual( + ABSOLUTE_LIMITS["maxServerGroups"], sot.absolute.server_groups + ) + self.assertEqual( + ABSOLUTE_LIMITS["maxServerGroupMembers"], + sot.absolute.server_group_members, + ) + self.assertEqual( + ABSOLUTE_LIMITS["totalFloatingIpsUsed"], + sot.absolute.floating_ips_used, + ) + self.assertEqual( + ABSOLUTE_LIMITS["totalSecurityGroupsUsed"], + sot.absolute.security_groups_used, + ) + self.assertEqual( + ABSOLUTE_LIMITS["totalRAMUsed"], sot.absolute.total_ram_used + ) + self.assertEqual( + ABSOLUTE_LIMITS["totalInstancesUsed"], sot.absolute.instances_used + ) + self.assertEqual( + ABSOLUTE_LIMITS["totalServerGroupsUsed"], + sot.absolute.server_groups_used, + ) + self.assertEqual( + ABSOLUTE_LIMITS["totalCoresUsed"], sot.absolute.total_cores_used + ) self.assertEqual(RATE_LIMIT["uri"], sot.rate[0].uri) self.assertEqual(RATE_LIMIT["regex"], sot.rate[0].regex) @@ -204,4 +221,5 @@ def test_get(self): self.assertEqual(RATE_LIMIT["uri"], dsot['rate'][0]['uri']) self.assertEqual( ABSOLUTE_LIMITS["totalSecurityGroupsUsed"], - dsot['absolute']['security_groups_used']) + dsot['absolute']['security_groups_used'], + ) diff --git a/openstack/tests/unit/compute/v2/test_migration.py b/openstack/tests/unit/compute/v2/test_migration.py index ad4315767..739916b16 100644 --- a/openstack/tests/unit/compute/v2/test_migration.py +++ b/openstack/tests/unit/compute/v2/test_migration.py @@ -33,7 +33,6 @@ class TestMigration(base.TestCase): - def test_basic(self): sot = migration.Migration() self.assertIsNone(sot.resource_key) # we don't support fetch diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index cece6643e..2d488acd1 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -65,9 +65,10 @@ def test_flavor_find(self): def test_flavor_find_query(self): self.verify_find( - self.proxy.find_flavor, flavor.Flavor, + self.proxy.find_flavor, + flavor.Flavor, method_kwargs={"a": "b"}, - expected_kwargs={"a": "b", "ignore_missing": True} + expected_kwargs={"a": "b", "ignore_missing": True}, ) def test_flavor_find_fetch_extra(self): @@ -84,7 +85,7 @@ def test_flavor_find_fetch_extra(self): method_kwargs={'get_extra_specs': True}, expected_result=res, expected_args=[flavor.Flavor, 'res'], - expected_kwargs={'ignore_missing': True} + expected_kwargs={'ignore_missing': True}, ) mocked.assert_called_once() @@ -101,7 +102,7 @@ def test_flavor_find_skip_fetch_extra(self): method_args=['res', True], expected_result=res, expected_args=[flavor.Flavor, 'res'], - expected_kwargs={'ignore_missing': True} + expected_kwargs={'ignore_missing': True}, ) mocked.assert_not_called() @@ -117,7 +118,7 @@ def test_flavor_get_no_extra(self): self.proxy.get_flavor, method_args=['res'], expected_result=res, - expected_args=[flavor.Flavor, 'res'] + expected_args=[flavor.Flavor, 'res'], ) mocked.assert_not_called() @@ -133,7 +134,7 @@ def test_flavor_get_fetch_extra(self): self.proxy.get_flavor, method_args=['res', True], expected_result=res, - expected_args=[flavor.Flavor, 'res'] + expected_args=[flavor.Flavor, 'res'], ) mocked.assert_called_once() @@ -149,7 +150,7 @@ def test_flavor_get_skip_fetch_extra(self): self.proxy.get_flavor, method_args=['res', True], expected_result=res, - expected_args=[flavor.Flavor, 'res'] + expected_args=[flavor.Flavor, 'res'], ) mocked.assert_not_called() @@ -161,8 +162,7 @@ def test_flavors_detailed(self, fetch_mock, list_mock): self.assertIsNotNone(r) fetch_mock.assert_not_called() list_mock.assert_called_with( - flavor.Flavor, - base_path="/flavors/detail" + flavor.Flavor, base_path="/flavors/detail" ) @mock.patch("openstack.proxy.Proxy._list") @@ -172,10 +172,7 @@ def test_flavors_not_detailed(self, fetch_mock, list_mock): for r in res: self.assertIsNotNone(r) fetch_mock.assert_not_called() - list_mock.assert_called_with( - flavor.Flavor, - base_path="/flavors" - ) + list_mock.assert_called_with(flavor.Flavor, base_path="/flavors") @mock.patch("openstack.proxy.Proxy._list") @mock.patch("openstack.compute.v2.flavor.Flavor.fetch_extra_specs") @@ -184,9 +181,7 @@ def test_flavors_query(self, fetch_mock, list_mock): for r in res: fetch_mock.assert_called_with(self.proxy) list_mock.assert_called_with( - flavor.Flavor, - base_path="/flavors", - a="b" + flavor.Flavor, base_path="/flavors", a="b" ) @mock.patch("openstack.proxy.Proxy._list") @@ -195,38 +190,39 @@ def test_flavors_get_extra(self, fetch_mock, list_mock): res = self.proxy.flavors(details=False, get_extra_specs=True) for r in res: fetch_mock.assert_called_with(self.proxy) - list_mock.assert_called_with( - flavor.Flavor, - base_path="/flavors" - ) + list_mock.assert_called_with(flavor.Flavor, base_path="/flavors") def test_flavor_get_access(self): self._verify( "openstack.compute.v2.flavor.Flavor.get_access", self.proxy.get_flavor_access, method_args=["value"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_flavor_add_tenant_access(self): self._verify( "openstack.compute.v2.flavor.Flavor.add_tenant_access", self.proxy.flavor_add_tenant_access, method_args=["value", "fake-tenant"], - expected_args=[self.proxy, "fake-tenant"]) + expected_args=[self.proxy, "fake-tenant"], + ) def test_flavor_remove_tenant_access(self): self._verify( "openstack.compute.v2.flavor.Flavor.remove_tenant_access", self.proxy.flavor_remove_tenant_access, method_args=["value", "fake-tenant"], - expected_args=[self.proxy, "fake-tenant"]) + expected_args=[self.proxy, "fake-tenant"], + ) def test_flavor_fetch_extra_specs(self): self._verify( "openstack.compute.v2.flavor.Flavor.fetch_extra_specs", self.proxy.fetch_flavor_extra_specs, method_args=["value"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_create_flavor_extra_specs(self): self._verify( @@ -234,28 +230,32 @@ def test_create_flavor_extra_specs(self): self.proxy.create_flavor_extra_specs, method_args=["value", {'a': 'b'}], expected_args=[self.proxy], - expected_kwargs={"specs": {'a': 'b'}}) + expected_kwargs={"specs": {'a': 'b'}}, + ) def test_get_flavor_extra_specs_prop(self): self._verify( "openstack.compute.v2.flavor.Flavor.get_extra_specs_property", self.proxy.get_flavor_extra_specs_property, method_args=["value", "prop"], - expected_args=[self.proxy, "prop"]) + expected_args=[self.proxy, "prop"], + ) def test_update_flavor_extra_specs_prop(self): self._verify( "openstack.compute.v2.flavor.Flavor.update_extra_specs_property", self.proxy.update_flavor_extra_specs_property, method_args=["value", "prop", "val"], - expected_args=[self.proxy, "prop", "val"]) + expected_args=[self.proxy, "prop", "val"], + ) def test_delete_flavor_extra_specs_prop(self): self._verify( "openstack.compute.v2.flavor.Flavor.delete_extra_specs_property", self.proxy.delete_flavor_extra_specs_property, method_args=["value", "prop"], - expected_args=[self.proxy, "prop"]) + expected_args=[self.proxy, "prop"], + ) class TestKeyPair(TestComputeProxy): @@ -270,10 +270,11 @@ def test_keypair_delete_ignore(self): def test_keypair_delete_user_id(self): self.verify_delete( - self.proxy.delete_keypair, keypair.Keypair, + self.proxy.delete_keypair, + keypair.Keypair, True, method_kwargs={'user_id': 'fake_user'}, - expected_kwargs={'user_id': 'fake_user'} + expected_kwargs={'user_id': 'fake_user'}, ) def test_keypair_find(self): @@ -281,9 +282,10 @@ def test_keypair_find(self): def test_keypair_find_user_id(self): self.verify_find( - self.proxy.find_keypair, keypair.Keypair, + self.proxy.find_keypair, + keypair.Keypair, method_kwargs={'user_id': 'fake_user'}, - expected_kwargs={'user_id': 'fake_user'} + expected_kwargs={'user_id': 'fake_user'}, ) def test_keypair_get(self): @@ -291,9 +293,10 @@ def test_keypair_get(self): def test_keypair_get_user_id(self): self.verify_get( - self.proxy.get_keypair, keypair.Keypair, + self.proxy.get_keypair, + keypair.Keypair, method_kwargs={'user_id': 'fake_user'}, - expected_kwargs={'user_id': 'fake_user'} + expected_kwargs={'user_id': 'fake_user'}, ) def test_keypairs(self): @@ -301,9 +304,10 @@ def test_keypairs(self): def test_keypairs_user_id(self): self.verify_list( - self.proxy.keypairs, keypair.Keypair, + self.proxy.keypairs, + keypair.Keypair, method_kwargs={'user_id': 'fake_user'}, - expected_kwargs={'user_id': 'fake_user'} + expected_kwargs={'user_id': 'fake_user'}, ) @@ -313,11 +317,13 @@ def test_aggregate_create(self): def test_aggregate_delete(self): self.verify_delete( - self.proxy.delete_aggregate, aggregate.Aggregate, False) + self.proxy.delete_aggregate, aggregate.Aggregate, False + ) def test_aggregate_delete_ignore(self): self.verify_delete( - self.proxy.delete_aggregate, aggregate.Aggregate, True) + self.proxy.delete_aggregate, aggregate.Aggregate, True + ) def test_aggregate_find(self): self.verify_find(self.proxy.find_aggregate, aggregate.Aggregate) @@ -336,53 +342,64 @@ def test_aggregate_add_host(self): "openstack.compute.v2.aggregate.Aggregate.add_host", self.proxy.add_host_to_aggregate, method_args=["value", "host"], - expected_args=[self.proxy, "host"]) + expected_args=[self.proxy, "host"], + ) def test_aggregate_remove_host(self): self._verify( "openstack.compute.v2.aggregate.Aggregate.remove_host", self.proxy.remove_host_from_aggregate, method_args=["value", "host"], - expected_args=[self.proxy, "host"]) + expected_args=[self.proxy, "host"], + ) def test_aggregate_set_metadata(self): self._verify( "openstack.compute.v2.aggregate.Aggregate.set_metadata", self.proxy.set_aggregate_metadata, method_args=["value", {'a': 'b'}], - expected_args=[self.proxy, {'a': 'b'}]) + expected_args=[self.proxy, {'a': 'b'}], + ) def test_aggregate_precache_image(self): self._verify( "openstack.compute.v2.aggregate.Aggregate.precache_images", self.proxy.aggregate_precache_images, method_args=["value", '1'], - expected_args=[self.proxy, [{'id': '1'}]]) + expected_args=[self.proxy, [{'id': '1'}]], + ) def test_aggregate_precache_images(self): self._verify( "openstack.compute.v2.aggregate.Aggregate.precache_images", self.proxy.aggregate_precache_images, method_args=["value", ['1', '2']], - expected_args=[self.proxy, [{'id': '1'}, {'id': '2'}]]) + expected_args=[self.proxy, [{'id': '1'}, {'id': '2'}]], + ) class TestService(TestComputeProxy): def test_services(self): self.verify_list(self.proxy.services, service.Service) - @mock.patch('openstack.utils.supports_microversion', autospec=True, - return_value=False) + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=False, + ) def test_enable_service_252(self, mv_mock): self._verify( 'openstack.compute.v2.service.Service.enable', self.proxy.enable_service, method_args=["value", "host1", "nova-compute"], - expected_args=[self.proxy, "host1", "nova-compute"] + expected_args=[self.proxy, "host1", "nova-compute"], ) - @mock.patch('openstack.utils.supports_microversion', autospec=True, - return_value=True) + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=True, + ) def test_enable_service_253(self, mv_mock): self._verify( 'openstack.proxy.Proxy._update', @@ -390,20 +407,27 @@ def test_enable_service_253(self, mv_mock): method_args=["value"], method_kwargs={}, expected_args=[service.Service, "value"], - expected_kwargs={'status': 'enabled'} + expected_kwargs={'status': 'enabled'}, ) - @mock.patch('openstack.utils.supports_microversion', autospec=True, - return_value=False) + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=False, + ) def test_disable_service_252(self, mv_mock): self._verify( 'openstack.compute.v2.service.Service.disable', self.proxy.disable_service, method_args=["value", "host1", "nova-compute"], - expected_args=[self.proxy, "host1", "nova-compute", None]) + expected_args=[self.proxy, "host1", "nova-compute", None], + ) - @mock.patch('openstack.utils.supports_microversion', autospec=True, - return_value=True) + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=True, + ) def test_disable_service_253(self, mv_mock): self._verify( 'openstack.proxy.Proxy._update', @@ -413,36 +437,49 @@ def test_disable_service_253(self, mv_mock): expected_args=[service.Service, "value"], expected_kwargs={ 'status': 'disabled', - 'disabled_reason': 'some_reason' - } + 'disabled_reason': 'some_reason', + }, ) - @mock.patch('openstack.utils.supports_microversion', autospec=True, - return_value=False) + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=False, + ) def test_force_service_down_252(self, mv_mock): self._verify( 'openstack.compute.v2.service.Service.set_forced_down', self.proxy.update_service_forced_down, method_args=["value", "host1", "nova-compute"], - expected_args=[self.proxy, "host1", "nova-compute", True]) + expected_args=[self.proxy, "host1", "nova-compute", True], + ) - @mock.patch('openstack.utils.supports_microversion', autospec=True, - return_value=False) + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=False, + ) def test_force_service_down_252_empty_vals(self, mv_mock): self.assertRaises( ValueError, self.proxy.update_service_forced_down, - "value", None, None + "value", + None, + None, ) - @mock.patch('openstack.utils.supports_microversion', autospec=True, - return_value=False) + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=False, + ) def test_force_service_down_252_empty_vals_svc(self, mv_mock): self._verify( 'openstack.compute.v2.service.Service.set_forced_down', self.proxy.update_service_forced_down, method_args=[{'host': 'a', 'binary': 'b'}, None, None], - expected_args=[self.proxy, None, None, True]) + expected_args=[self.proxy, None, None, True], + ) def test_find_service(self): self.verify_find( @@ -455,12 +492,11 @@ def test_find_service_args(self): self.proxy.find_service, service.Service, method_kwargs={'host': 'h1'}, - expected_kwargs={'host': 'h1'} + expected_kwargs={'host': 'h1'}, ) class TestVolumeAttachment(TestComputeProxy): - def test_volume_attachment_create(self): self.verify_create( self.proxy.create_volume_attachment, @@ -611,19 +647,27 @@ def test_volume_attachments(self): class TestHypervisor(TestComputeProxy): - def test_hypervisors_not_detailed(self): - self.verify_list(self.proxy.hypervisors, hypervisor.Hypervisor, - method_kwargs={"details": False}, - expected_kwargs={}) + self.verify_list( + self.proxy.hypervisors, + hypervisor.Hypervisor, + method_kwargs={"details": False}, + expected_kwargs={}, + ) def test_hypervisors_detailed(self): - self.verify_list(self.proxy.hypervisors, hypervisor.HypervisorDetail, - method_kwargs={"details": True}, - expected_kwargs={}) + self.verify_list( + self.proxy.hypervisors, + hypervisor.HypervisorDetail, + method_kwargs={"details": True}, + expected_kwargs={}, + ) - @mock.patch('openstack.utils.supports_microversion', autospec=True, - return_value=False) + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=False, + ) def test_hypervisors_search_before_253_no_qp(self, sm): self.verify_list( self.proxy.hypervisors, @@ -633,8 +677,11 @@ def test_hypervisors_search_before_253_no_qp(self, sm): expected_kwargs={}, ) - @mock.patch('openstack.utils.supports_microversion', autospec=True, - return_value=False) + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=False, + ) def test_hypervisors_search_before_253(self, sm): self.verify_list( self.proxy.hypervisors, @@ -644,42 +691,48 @@ def test_hypervisors_search_before_253(self, sm): expected_kwargs={}, ) - @mock.patch('openstack.utils.supports_microversion', autospec=True, - return_value=True) + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=True, + ) def test_hypervisors_search_after_253(self, sm): self.verify_list( self.proxy.hypervisors, hypervisor.Hypervisor, method_kwargs={'hypervisor_hostname_pattern': 'substring'}, base_path=None, - expected_kwargs={'hypervisor_hostname_pattern': 'substring'} + expected_kwargs={'hypervisor_hostname_pattern': 'substring'}, ) def test_find_hypervisor_detail(self): - self.verify_find(self.proxy.find_hypervisor, - hypervisor.Hypervisor, - expected_kwargs={ - 'list_base_path': '/os-hypervisors/detail', - 'ignore_missing': True}) + self.verify_find( + self.proxy.find_hypervisor, + hypervisor.Hypervisor, + expected_kwargs={ + 'list_base_path': '/os-hypervisors/detail', + 'ignore_missing': True, + }, + ) def test_find_hypervisor_no_detail(self): - self.verify_find(self.proxy.find_hypervisor, - hypervisor.Hypervisor, - method_kwargs={'details': False}, - expected_kwargs={ - 'list_base_path': None, - 'ignore_missing': True}) + self.verify_find( + self.proxy.find_hypervisor, + hypervisor.Hypervisor, + method_kwargs={'details': False}, + expected_kwargs={'list_base_path': None, 'ignore_missing': True}, + ) def test_get_hypervisor(self): - self.verify_get(self.proxy.get_hypervisor, - hypervisor.Hypervisor) + self.verify_get(self.proxy.get_hypervisor, hypervisor.Hypervisor) def test_get_hypervisor_uptime(self): self._verify( "openstack.compute.v2.hypervisor.Hypervisor.get_uptime", self.proxy.get_hypervisor_uptime, method_args=["value"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) class TestCompute(TestComputeProxy): @@ -746,10 +799,12 @@ def test_limits_get(self): ) def test_server_interface_create(self): - self.verify_create(self.proxy.create_server_interface, - server_interface.ServerInterface, - method_kwargs={"server": "test_id"}, - expected_kwargs={"server_id": "test_id"}) + self.verify_create( + self.proxy.create_server_interface, + server_interface.ServerInterface, + method_kwargs={"server": "test_id"}, + expected_kwargs={"server_id": "test_id"}, + ) def test_server_interface_delete(self): self.proxy._get_uri_attribute = lambda *args: args[1] @@ -766,7 +821,8 @@ def test_server_interface_delete(self): method_args=[test_interface], method_kwargs={"server": server_id}, expected_args=[server_interface.ServerInterface, interface_id], - expected_kwargs={"server_id": server_id, "ignore_missing": True}) + expected_kwargs={"server_id": server_id, "ignore_missing": True}, + ) # Case2: ServerInterface ID is provided as value self._verify( @@ -775,14 +831,18 @@ def test_server_interface_delete(self): method_args=[interface_id], method_kwargs={"server": server_id}, expected_args=[server_interface.ServerInterface, interface_id], - expected_kwargs={"server_id": server_id, "ignore_missing": True}) + expected_kwargs={"server_id": server_id, "ignore_missing": True}, + ) def test_server_interface_delete_ignore(self): self.proxy._get_uri_attribute = lambda *args: args[1] - self.verify_delete(self.proxy.delete_server_interface, - server_interface.ServerInterface, True, - method_kwargs={"server": "test_id"}, - expected_kwargs={"server_id": "test_id"}) + self.verify_delete( + self.proxy.delete_server_interface, + server_interface.ServerInterface, + True, + method_kwargs={"server": "test_id"}, + expected_kwargs={"server_id": "test_id"}, + ) def test_server_interface_get(self): self.proxy._get_uri_attribute = lambda *args: args[1] @@ -799,7 +859,8 @@ def test_server_interface_get(self): method_args=[test_interface], method_kwargs={"server": server_id}, expected_args=[server_interface.ServerInterface], - expected_kwargs={"port_id": interface_id, "server_id": server_id}) + expected_kwargs={"port_id": interface_id, "server_id": server_id}, + ) # Case2: ServerInterface ID is provided as value self._verify( @@ -808,29 +869,39 @@ def test_server_interface_get(self): method_args=[interface_id], method_kwargs={"server": server_id}, expected_args=[server_interface.ServerInterface], - expected_kwargs={"port_id": interface_id, "server_id": server_id}) + expected_kwargs={"port_id": interface_id, "server_id": server_id}, + ) def test_server_interfaces(self): - self.verify_list(self.proxy.server_interfaces, - server_interface.ServerInterface, - method_args=["test_id"], - expected_args=[], - expected_kwargs={"server_id": "test_id"}) + self.verify_list( + self.proxy.server_interfaces, + server_interface.ServerInterface, + method_args=["test_id"], + expected_args=[], + expected_kwargs={"server_id": "test_id"}, + ) def test_server_ips_with_network_label(self): - self.verify_list(self.proxy.server_ips, server_ip.ServerIP, - method_args=["test_id"], - method_kwargs={"network_label": "test_label"}, - expected_args=[], - expected_kwargs={"server_id": "test_id", - "network_label": "test_label"}) + self.verify_list( + self.proxy.server_ips, + server_ip.ServerIP, + method_args=["test_id"], + method_kwargs={"network_label": "test_label"}, + expected_args=[], + expected_kwargs={ + "server_id": "test_id", + "network_label": "test_label", + }, + ) def test_server_ips_without_network_label(self): - self.verify_list(self.proxy.server_ips, server_ip.ServerIP, - method_args=["test_id"], - expected_args=[], - expected_kwargs={"server_id": "test_id", - "network_label": None}) + self.verify_list( + self.proxy.server_ips, + server_ip.ServerIP, + method_args=["test_id"], + expected_args=[], + expected_kwargs={"server_id": "test_id", "network_label": None}, + ) def test_server_create_attrs(self): self.verify_create(self.proxy.create_server, server.Server) @@ -846,7 +917,8 @@ def test_server_force_delete(self): "openstack.compute.v2.server.Server.force_delete", self.proxy.delete_server, method_args=["value", False, True], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_server_find(self): self.verify_find( @@ -863,17 +935,24 @@ def test_server_get(self): self.verify_get(self.proxy.get_server, server.Server) def test_servers_detailed(self): - self.verify_list(self.proxy.servers, server.Server, - method_kwargs={"details": True, - "changes_since": 1, "image": 2}, - expected_kwargs={"changes_since": 1, "image": 2, - "base_path": "/servers/detail"}) + self.verify_list( + self.proxy.servers, + server.Server, + method_kwargs={"details": True, "changes_since": 1, "image": 2}, + expected_kwargs={ + "changes_since": 1, + "image": 2, + "base_path": "/servers/detail", + }, + ) def test_servers_not_detailed(self): - self.verify_list(self.proxy.servers, server.Server, - method_kwargs={"details": False, - "changes_since": 1, "image": 2}, - expected_kwargs={"changes_since": 1, "image": 2}) + self.verify_list( + self.proxy.servers, + server.Server, + method_kwargs={"details": False, "changes_since": 1, "image": 2}, + expected_kwargs={"changes_since": 1, "image": 2}, + ) def test_server_update(self): self.verify_update(self.proxy.update_server, server.Server) @@ -883,28 +962,32 @@ def test_server_wait_for(self): self.verify_wait_for_status( self.proxy.wait_for_server, method_args=[value], - expected_args=[self.proxy, value, 'ACTIVE', ['ERROR'], 2, 120]) + expected_args=[self.proxy, value, 'ACTIVE', ['ERROR'], 2, 120], + ) def test_server_resize(self): self._verify( "openstack.compute.v2.server.Server.resize", self.proxy.resize_server, method_args=["value", "test-flavor"], - expected_args=[self.proxy, "test-flavor"]) + expected_args=[self.proxy, "test-flavor"], + ) def test_server_confirm_resize(self): self._verify( "openstack.compute.v2.server.Server.confirm_resize", self.proxy.confirm_server_resize, method_args=["value"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_server_revert_resize(self): self._verify( "openstack.compute.v2.server.Server.revert_resize", self.proxy.revert_server_resize, method_args=["value"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_server_rebuild(self): id = 'test_image_id' @@ -921,13 +1004,16 @@ def test_server_rebuild(self): "name": "test_server", "admin_password": "test_pass", "metadata": {"k1": "v1"}, - "image": image_obj}, + "image": image_obj, + }, expected_args=[self.proxy], expected_kwargs={ "name": "test_server", "admin_password": "test_pass", "metadata": {"k1": "v1"}, - "image": image_obj}) + "image": image_obj, + }, + ) # Case2: image name or id is provided self._verify( @@ -938,27 +1024,32 @@ def test_server_rebuild(self): "name": "test_server", "admin_password": "test_pass", "metadata": {"k1": "v1"}, - "image": id}, + "image": id, + }, expected_args=[self.proxy], expected_kwargs={ "name": "test_server", "admin_password": "test_pass", "metadata": {"k1": "v1"}, - "image": id}) + "image": id, + }, + ) def test_add_fixed_ip_to_server(self): self._verify( "openstack.compute.v2.server.Server.add_fixed_ip", self.proxy.add_fixed_ip_to_server, method_args=["value", "network-id"], - expected_args=[self.proxy, "network-id"]) + expected_args=[self.proxy, "network-id"], + ) def test_fixed_ip_from_server(self): self._verify( "openstack.compute.v2.server.Server.remove_fixed_ip", self.proxy.remove_fixed_ip_from_server, method_args=["value", "address"], - expected_args=[self.proxy, "address"]) + expected_args=[self.proxy, "address"], + ) def test_floating_ip_to_server(self): self._verify( @@ -966,7 +1057,8 @@ def test_floating_ip_to_server(self): self.proxy.add_floating_ip_to_server, method_args=["value", "floating-ip"], expected_args=[self.proxy, "floating-ip"], - expected_kwargs={'fixed_address': None}) + expected_kwargs={'fixed_address': None}, + ) def test_add_floating_ip_to_server_with_fixed_addr(self): self._verify( @@ -974,49 +1066,56 @@ def test_add_floating_ip_to_server_with_fixed_addr(self): self.proxy.add_floating_ip_to_server, method_args=["value", "floating-ip", 'fixed-addr'], expected_args=[self.proxy, "floating-ip"], - expected_kwargs={'fixed_address': 'fixed-addr'}) + expected_kwargs={'fixed_address': 'fixed-addr'}, + ) def test_remove_floating_ip_from_server(self): self._verify( "openstack.compute.v2.server.Server.remove_floating_ip", self.proxy.remove_floating_ip_from_server, method_args=["value", "address"], - expected_args=[self.proxy, "address"]) + expected_args=[self.proxy, "address"], + ) def test_server_backup(self): self._verify( "openstack.compute.v2.server.Server.backup", self.proxy.backup_server, method_args=["value", "name", "daily", 1], - expected_args=[self.proxy, "name", "daily", 1]) + expected_args=[self.proxy, "name", "daily", 1], + ) def test_server_pause(self): self._verify( "openstack.compute.v2.server.Server.pause", self.proxy.pause_server, method_args=["value"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_server_unpause(self): self._verify( "openstack.compute.v2.server.Server.unpause", self.proxy.unpause_server, method_args=["value"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_server_suspend(self): self._verify( "openstack.compute.v2.server.Server.suspend", self.proxy.suspend_server, method_args=["value"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_server_resume(self): self._verify( "openstack.compute.v2.server.Server.resume", self.proxy.resume_server, method_args=["value"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_server_lock(self): self._verify( @@ -1024,7 +1123,8 @@ def test_server_lock(self): self.proxy.lock_server, method_args=["value"], expected_args=[self.proxy], - expected_kwargs={"locked_reason": None}) + expected_kwargs={"locked_reason": None}, + ) def test_server_lock_with_options(self): self._verify( @@ -1033,14 +1133,16 @@ def test_server_lock_with_options(self): method_args=["value"], method_kwargs={"locked_reason": "Because why not"}, expected_args=[self.proxy], - expected_kwargs={"locked_reason": "Because why not"}) + expected_kwargs={"locked_reason": "Because why not"}, + ) def test_server_unlock(self): self._verify( "openstack.compute.v2.server.Server.unlock", self.proxy.unlock_server, method_args=["value"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_server_rescue(self): self._verify( @@ -1048,7 +1150,8 @@ def test_server_rescue(self): self.proxy.rescue_server, method_args=["value"], expected_args=[self.proxy], - expected_kwargs={"admin_pass": None, "image_ref": None}) + expected_kwargs={"admin_pass": None, "image_ref": None}, + ) def test_server_rescue_with_options(self): self._verify( @@ -1056,14 +1159,16 @@ def test_server_rescue_with_options(self): self.proxy.rescue_server, method_args=["value", 'PASS', 'IMG'], expected_args=[self.proxy], - expected_kwargs={"admin_pass": 'PASS', "image_ref": 'IMG'}) + expected_kwargs={"admin_pass": 'PASS', "image_ref": 'IMG'}, + ) def test_server_unrescue(self): self._verify( "openstack.compute.v2.server.Server.unrescue", self.proxy.unrescue_server, method_args=["value"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_server_evacuate(self): self._verify( @@ -1071,7 +1176,8 @@ def test_server_evacuate(self): self.proxy.evacuate_server, method_args=["value"], expected_args=[self.proxy], - expected_kwargs={"host": None, "admin_pass": None, "force": None}) + expected_kwargs={"host": None, "admin_pass": None, "force": None}, + ) def test_server_evacuate_with_options(self): self._verify( @@ -1080,49 +1186,59 @@ def test_server_evacuate_with_options(self): method_args=["value", 'HOST2', 'NEW_PASS', True], expected_args=[self.proxy], expected_kwargs={ - "host": "HOST2", "admin_pass": 'NEW_PASS', "force": True}) + "host": "HOST2", + "admin_pass": 'NEW_PASS', + "force": True, + }, + ) def test_server_start(self): self._verify( "openstack.compute.v2.server.Server.start", self.proxy.start_server, method_args=["value"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_server_stop(self): self._verify( "openstack.compute.v2.server.Server.stop", self.proxy.stop_server, method_args=["value"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_server_restore(self): self._verify( "openstack.compute.v2.server.Server.restore", self.proxy.restore_server, method_args=["value"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_server_shelve(self): self._verify( "openstack.compute.v2.server.Server.shelve", self.proxy.shelve_server, method_args=["value"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_server_unshelve(self): self._verify( "openstack.compute.v2.server.Server.unshelve", self.proxy.unshelve_server, method_args=["value"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_server_trigger_dump(self): self._verify( "openstack.compute.v2.server.Server.trigger_crash_dump", self.proxy.trigger_server_crash_dump, method_args=["value"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_get_server_output(self): self._verify( @@ -1130,26 +1246,32 @@ def test_get_server_output(self): self.proxy.get_server_console_output, method_args=["value"], expected_args=[self.proxy], - expected_kwargs={"length": None}) + expected_kwargs={"length": None}, + ) self._verify( "openstack.compute.v2.server.Server.get_console_output", self.proxy.get_server_console_output, method_args=["value", 1], expected_args=[self.proxy], - expected_kwargs={"length": 1}) + expected_kwargs={"length": 1}, + ) def test_availability_zones_not_detailed(self): - self.verify_list(self.proxy.availability_zones, - az.AvailabilityZone, - method_kwargs={"details": False}, - expected_kwargs={}) + self.verify_list( + self.proxy.availability_zones, + az.AvailabilityZone, + method_kwargs={"details": False}, + expected_kwargs={}, + ) def test_availability_zones_detailed(self): - self.verify_list(self.proxy.availability_zones, - az.AvailabilityZoneDetail, - method_kwargs={"details": True}, - expected_kwargs={}) + self.verify_list( + self.proxy.availability_zones, + az.AvailabilityZoneDetail, + method_kwargs={"details": True}, + expected_kwargs={}, + ) def test_get_all_server_metadata(self): self._verify( @@ -1157,7 +1279,8 @@ def test_get_all_server_metadata(self): self.proxy.get_server_metadata, method_args=["value"], expected_args=[self.proxy], - expected_result=server.Server(id="value", metadata={})) + expected_result=server.Server(id="value", metadata={}), + ) def test_set_server_metadata(self): kwargs = {"a": "1", "b": "2"} @@ -1170,7 +1293,7 @@ def test_set_server_metadata(self): method_result=server.Server.existing(id=id, metadata=kwargs), expected_args=[self.proxy], expected_kwargs={'metadata': kwargs}, - expected_result=server.Server.existing(id=id, metadata=kwargs) + expected_result=server.Server.existing(id=id, metadata=kwargs), ) def test_delete_server_metadata(self): @@ -1179,12 +1302,14 @@ def test_delete_server_metadata(self): self.proxy.delete_server_metadata, expected_result=None, method_args=["value", ["key"]], - expected_args=[self.proxy, "key"]) + expected_args=[self.proxy, "key"], + ) def test_create_image(self): metadata = {'k1': 'v1'} - with mock.patch('openstack.compute.v2.server.Server.create_image') \ - as ci_mock: + with mock.patch( + 'openstack.compute.v2.server.Server.create_image' + ) as ci_mock: ci_mock.return_value = 'image_id' connection_mock = mock.Mock() @@ -1193,32 +1318,32 @@ def test_create_image(self): self.proxy._connection = connection_mock rsp = self.proxy.create_server_image( - 'server', 'image_name', metadata, wait=True, timeout=1) - - ci_mock.assert_called_with( - self.proxy, - 'image_name', - metadata + 'server', 'image_name', metadata, wait=True, timeout=1 ) + ci_mock.assert_called_with(self.proxy, 'image_name', metadata) + self.proxy._connection.get_image.assert_called_with('image_id') self.proxy._connection.wait_for_image.assert_called_with( - 'image', - timeout=1) + 'image', timeout=1 + ) self.assertEqual(connection_mock.wait_for_image(), rsp) def test_server_group_create(self): - self.verify_create(self.proxy.create_server_group, - server_group.ServerGroup) + self.verify_create( + self.proxy.create_server_group, server_group.ServerGroup + ) def test_server_group_delete(self): - self.verify_delete(self.proxy.delete_server_group, - server_group.ServerGroup, False) + self.verify_delete( + self.proxy.delete_server_group, server_group.ServerGroup, False + ) def test_server_group_delete_ignore(self): - self.verify_delete(self.proxy.delete_server_group, - server_group.ServerGroup, True) + self.verify_delete( + self.proxy.delete_server_group, server_group.ServerGroup, True + ) def test_server_group_find(self): self.verify_find( @@ -1229,8 +1354,7 @@ def test_server_group_find(self): ) def test_server_group_get(self): - self.verify_get(self.proxy.get_server_group, - server_group.ServerGroup) + self.verify_get(self.proxy.get_server_group, server_group.ServerGroup) def test_server_groups(self): self.verify_list(self.proxy.server_groups, server_group.ServerGroup) @@ -1241,7 +1365,8 @@ def test_live_migrate_server(self): self.proxy.live_migrate_server, method_args=["value", "host1", False], expected_args=[self.proxy, "host1"], - expected_kwargs={'force': False, 'block_migration': None}) + expected_kwargs={'force': False, 'block_migration': None}, + ) def test_abort_server_migration(self): self._verify( @@ -1306,21 +1431,24 @@ def test_fetch_security_groups(self): 'openstack.compute.v2.server.Server.fetch_security_groups', self.proxy.fetch_server_security_groups, method_args=["value"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_add_security_groups(self): self._verify( 'openstack.compute.v2.server.Server.add_security_group', self.proxy.add_security_group_to_server, method_args=["value", {'id': 'id', 'name': 'sg'}], - expected_args=[self.proxy, 'sg']) + expected_args=[self.proxy, 'sg'], + ) def test_remove_security_groups(self): self._verify( 'openstack.compute.v2.server.Server.remove_security_group', self.proxy.remove_security_group_from_server, method_args=["value", {'id': 'id', 'name': 'sg'}], - expected_args=[self.proxy, 'sg']) + expected_args=[self.proxy, 'sg'], + ) def test_usages(self): self.verify_list(self.proxy.usages, usage.Usage) @@ -1335,7 +1463,7 @@ def test_usages__with_kwargs(self): method_kwargs={'start': start, 'end': end}, expected_kwargs={ 'start': start.isoformat(), - 'end': end.isoformat() + 'end': end.isoformat(), }, ) @@ -1361,7 +1489,7 @@ def test_get_usage__with_kwargs(self): expected_args=[self.proxy], expected_kwargs={ 'start': start.isoformat(), - 'end': end.isoformat() + 'end': end.isoformat(), }, ) @@ -1370,25 +1498,24 @@ def test_create_server_remote_console(self): self.proxy.create_server_remote_console, server_remote_console.ServerRemoteConsole, method_kwargs={"server": "test_id", "type": "fake"}, - expected_kwargs={"server_id": "test_id", "type": "fake"}) + expected_kwargs={"server_id": "test_id", "type": "fake"}, + ) def test_get_console_url(self): self._verify( 'openstack.compute.v2.server.Server.get_console_url', self.proxy.get_server_console_url, method_args=["value", "console_type"], - expected_args=[self.proxy, "console_type"]) + expected_args=[self.proxy, "console_type"], + ) @mock.patch('openstack.utils.supports_microversion', autospec=True) @mock.patch('openstack.compute.v2._proxy.Proxy._create', autospec=True) - @mock.patch('openstack.compute.v2.server.Server.get_console_url', - autospec=True) + @mock.patch( + 'openstack.compute.v2.server.Server.get_console_url', autospec=True + ) def test_create_console_mv_old(self, sgc, rcc, smv): - console_fake = { - 'url': 'a', - 'type': 'b', - 'protocol': 'c' - } + console_fake = {'url': 'a', 'type': 'b', 'protocol': 'c'} smv.return_value = False sgc.return_value = console_fake ret = self.proxy.create_console('fake_server', 'fake_type') @@ -1399,27 +1526,27 @@ def test_create_console_mv_old(self, sgc, rcc, smv): @mock.patch('openstack.utils.supports_microversion', autospec=True) @mock.patch('openstack.compute.v2._proxy.Proxy._create', autospec=True) - @mock.patch('openstack.compute.v2.server.Server.get_console_url', - autospec=True) + @mock.patch( + 'openstack.compute.v2.server.Server.get_console_url', autospec=True + ) def test_create_console_mv_2_6(self, sgc, rcc, smv): - console_fake = { - 'url': 'a', - 'type': 'b', - 'protocol': 'c' - } + console_fake = {'url': 'a', 'type': 'b', 'protocol': 'c'} # Test server_remote_console is triggered when mv>=2.6 smv.return_value = True rcc.return_value = server_remote_console.ServerRemoteConsole( - **console_fake) + **console_fake + ) ret = self.proxy.create_console('fake_server', 'fake_type') smv.assert_called_once_with(self.proxy, '2.6') sgc.assert_not_called() - rcc.assert_called_with(mock.ANY, - server_remote_console.ServerRemoteConsole, - server_id='fake_server', - type='fake_type', - protocol=None) + rcc.assert_called_with( + mock.ANY, + server_remote_console.ServerRemoteConsole, + server_id='fake_server', + type='fake_type', + protocol=None, + ) self.assertEqual(console_fake['url'], ret['url']) @@ -1436,7 +1563,7 @@ def test_get(self): 'usage': False, }, method_result=quota_set.QuotaSet(), - expected_result=quota_set.QuotaSet() + expected_result=quota_set.QuotaSet(), ) def test_get_query(self): @@ -1444,17 +1571,14 @@ def test_get_query(self): 'openstack.resource.Resource.fetch', self.proxy.get_quota_set, method_args=['prj'], - method_kwargs={ - 'usage': True, - 'user_id': 'uid' - }, + method_kwargs={'usage': True, 'user_id': 'uid'}, expected_args=[self.proxy], expected_kwargs={ 'error_message': None, 'requires_id': False, 'usage': True, - 'user_id': 'uid' - } + 'user_id': 'uid', + }, ) def test_get_defaults(self): @@ -1466,8 +1590,8 @@ def test_get_defaults(self): expected_kwargs={ 'error_message': None, 'requires_id': False, - 'base_path': '/os-quota-sets/defaults' - } + 'base_path': '/os-quota-sets/defaults', + }, ) def test_reset(self): @@ -1477,9 +1601,7 @@ def test_reset(self): method_args=['prj'], method_kwargs={'user_id': 'uid'}, expected_args=[self.proxy], - expected_kwargs={ - 'user_id': 'uid' - } + expected_kwargs={'user_id': 'uid'}, ) @mock.patch('openstack.proxy.Proxy._get_resource', autospec=True) @@ -1495,19 +1617,12 @@ def test_update(self, gr_mock): 'a': 'b', }, expected_args=[self.proxy], - expected_kwargs={ - 'user_id': 'uid' - } - ) - gr_mock.assert_called_with( - self.proxy, - quota_set.QuotaSet, - 'qs', a='b' + expected_kwargs={'user_id': 'uid'}, ) + gr_mock.assert_called_with(self.proxy, quota_set.QuotaSet, 'qs', a='b') class TestServerAction(TestComputeProxy): - def test_server_action_get(self): self._verify( 'openstack.proxy.Proxy._get', @@ -1516,7 +1631,8 @@ def test_server_action_get(self): method_kwargs={'server': 'server_id'}, expected_args=[server_action.ServerAction], expected_kwargs={ - 'request_id': 'request_id', 'server_id': 'server_id', + 'request_id': 'request_id', + 'server_id': 'server_id', }, ) diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 0a6861dc0..33ca69d96 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -46,7 +46,7 @@ 'OS-EXT-IPS-MAC:mac_addr': 'aa:bb:cc:dd:ee:ff', 'OS-EXT-IPS:type': 'fixed', 'addr': '192.168.0.3', - 'version': 4 + 'version': 4, } ] }, @@ -60,7 +60,7 @@ 'ephemeral': 0, 'extra_specs': { 'hw:cpu_policy': 'dedicated', - 'hw:mem_page_size': '2048' + 'hw:mem_page_size': '2048', }, 'original_name': 'm1.tiny.specs', 'ram': 512, @@ -75,49 +75,42 @@ 'links': [ { 'href': 'http://openstack.example.com/images/70a599e0', - 'rel': 'bookmark' + 'rel': 'bookmark', } - ] + ], }, 'key_name': 'dummy', 'links': [ { 'href': 'http://openstack.example.com/v2.1/servers/9168b536', - 'rel': 'self' + 'rel': 'self', }, { 'href': 'http://openstack.example.com/servers/9168b536', - 'rel': 'bookmark' - } + 'rel': 'bookmark', + }, ], 'locked': True, - 'metadata': { - 'My Server Name': 'Apache1' - }, + 'metadata': {'My Server Name': 'Apache1'}, 'name': 'new-server-test', 'networks': 'auto', 'os-extended-volumes:volumes_attached': [], 'progress': 0, - 'security_groups': [ - { - 'name': 'default' - } - ], + 'security_groups': [{'name': 'default'}], 'server_groups': ['3caf4187-8010-491f-b6f5-a4a68a40371e'], 'status': 'ACTIVE', 'tags': [], 'tenant_id': '6f70656e737461636b20342065766572', 'trusted_image_certificates': [ '0b5d2c72-12cc-4ba6-a8d7-3ff5cc1d8cb8', - '674736e3-f25c-405c-8362-bbf991e0ce0a' + '674736e3-f25c-405c-8362-bbf991e0ce0a', ], 'updated': '2017-02-14T19:24:00Z', - 'user_id': 'fake' + 'user_id': 'fake', } class TestServer(base.TestCase): - def setUp(self): super(TestServer, self).setUp() self.resp = mock.Mock() @@ -140,53 +133,56 @@ def test_basic(self): self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertDictEqual({"access_ipv4": "access_ip_v4", - "access_ipv6": "access_ip_v6", - "auto_disk_config": "auto_disk_config", - "availability_zone": "availability_zone", - "changes_before": "changes-before", - "changes_since": "changes-since", - "compute_host": "host", - "has_config_drive": "config_drive", - "created_at": "created_at", - "description": "description", - "flavor": "flavor", - "hostname": "hostname", - "image": "image", - "ipv4_address": "ip", - "ipv6_address": "ip6", - "id": "uuid", - "deleted_only": "deleted", - "is_soft_deleted": "soft_deleted", - "kernel_id": "kernel_id", - "key_name": "key_name", - "launch_index": "launch_index", - "launched_at": "launched_at", - "limit": "limit", - "locked_by": "locked_by", - "marker": "marker", - "name": "name", - "node": "node", - "power_state": "power_state", - "progress": "progress", - "project_id": "project_id", - "ramdisk_id": "ramdisk_id", - "reservation_id": "reservation_id", - "root_device_name": "root_device_name", - "sort_dir": "sort_dir", - "sort_key": "sort_key", - "status": "status", - "task_state": "task_state", - "terminated_at": "terminated_at", - "user_id": "user_id", - "vm_state": "vm_state", - "all_projects": "all_tenants", - "tags": "tags", - "any_tags": "tags-any", - "not_tags": "not-tags", - "not_any_tags": "not-tags-any", - }, - sot._query_mapping._mapping) + self.assertDictEqual( + { + "access_ipv4": "access_ip_v4", + "access_ipv6": "access_ip_v6", + "auto_disk_config": "auto_disk_config", + "availability_zone": "availability_zone", + "changes_before": "changes-before", + "changes_since": "changes-since", + "compute_host": "host", + "has_config_drive": "config_drive", + "created_at": "created_at", + "description": "description", + "flavor": "flavor", + "hostname": "hostname", + "image": "image", + "ipv4_address": "ip", + "ipv6_address": "ip6", + "id": "uuid", + "deleted_only": "deleted", + "is_soft_deleted": "soft_deleted", + "kernel_id": "kernel_id", + "key_name": "key_name", + "launch_index": "launch_index", + "launched_at": "launched_at", + "limit": "limit", + "locked_by": "locked_by", + "marker": "marker", + "name": "name", + "node": "node", + "power_state": "power_state", + "progress": "progress", + "project_id": "project_id", + "ramdisk_id": "ramdisk_id", + "reservation_id": "reservation_id", + "root_device_name": "root_device_name", + "sort_dir": "sort_dir", + "sort_key": "sort_key", + "status": "status", + "task_state": "task_state", + "terminated_at": "terminated_at", + "user_id": "user_id", + "vm_state": "vm_state", + "all_projects": "all_tenants", + "tags": "tags", + "any_tags": "tags-any", + "not_tags": "not-tags", + "not_any_tags": "not-tags-any", + }, + sot._query_mapping._mapping, + ) def test_make_it(self): sot = server.Server(**EXAMPLE) @@ -214,44 +210,54 @@ def test_make_it(self): self.assertEqual(EXAMPLE['user_id'], sot.user_id) self.assertEqual(EXAMPLE['key_name'], sot.key_name) self.assertEqual(EXAMPLE['OS-DCF:diskConfig'], sot.disk_config) - self.assertEqual(EXAMPLE['OS-EXT-AZ:availability_zone'], - sot.availability_zone) + self.assertEqual( + EXAMPLE['OS-EXT-AZ:availability_zone'], sot.availability_zone + ) self.assertEqual(EXAMPLE['OS-EXT-STS:power_state'], sot.power_state) self.assertEqual(EXAMPLE['OS-EXT-STS:task_state'], sot.task_state) self.assertEqual(EXAMPLE['OS-EXT-STS:vm_state'], sot.vm_state) - self.assertEqual(EXAMPLE['os-extended-volumes:volumes_attached'], - sot.attached_volumes) + self.assertEqual( + EXAMPLE['os-extended-volumes:volumes_attached'], + sot.attached_volumes, + ) self.assertEqual(EXAMPLE['OS-SRV-USG:launched_at'], sot.launched_at) - self.assertEqual(EXAMPLE['OS-SRV-USG:terminated_at'], - sot.terminated_at) + self.assertEqual( + EXAMPLE['OS-SRV-USG:terminated_at'], sot.terminated_at + ) self.assertEqual(EXAMPLE['security_groups'], sot.security_groups) self.assertEqual(EXAMPLE['adminPass'], sot.admin_password) - self.assertEqual(EXAMPLE['block_device_mapping_v2'], - sot.block_device_mapping) - self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:host'], - sot.compute_host) - self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:hostname'], - sot.hostname) - self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:hypervisor_hostname'], - sot.hypervisor_hostname) - self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:instance_name'], - sot.instance_name) - self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:kernel_id'], - sot.kernel_id) - self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:launch_index'], - sot.launch_index) - self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:ramdisk_id'], - sot.ramdisk_id) - self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:reservation_id'], - sot.reservation_id) - self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:root_device_name'], - sot.root_device_name) - self.assertEqual(EXAMPLE['OS-SCH-HNT:scheduler_hints'], - sot.scheduler_hints) + self.assertEqual( + EXAMPLE['block_device_mapping_v2'], sot.block_device_mapping + ) + self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:host'], sot.compute_host) + self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:hostname'], sot.hostname) + self.assertEqual( + EXAMPLE['OS-EXT-SRV-ATTR:hypervisor_hostname'], + sot.hypervisor_hostname, + ) + self.assertEqual( + EXAMPLE['OS-EXT-SRV-ATTR:instance_name'], sot.instance_name + ) + self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:kernel_id'], sot.kernel_id) + self.assertEqual( + EXAMPLE['OS-EXT-SRV-ATTR:launch_index'], sot.launch_index + ) + self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:ramdisk_id'], sot.ramdisk_id) + self.assertEqual( + EXAMPLE['OS-EXT-SRV-ATTR:reservation_id'], sot.reservation_id + ) + self.assertEqual( + EXAMPLE['OS-EXT-SRV-ATTR:root_device_name'], sot.root_device_name + ) + self.assertEqual( + EXAMPLE['OS-SCH-HNT:scheduler_hints'], sot.scheduler_hints + ) self.assertEqual(EXAMPLE['OS-EXT-SRV-ATTR:user_data'], sot.user_data) self.assertEqual(EXAMPLE['locked'], sot.is_locked) - self.assertEqual(EXAMPLE['trusted_image_certificates'], - sot.trusted_image_certificates) + self.assertEqual( + EXAMPLE['trusted_image_certificates'], + sot.trusted_image_certificates, + ) def test_to_dict_flavor(self): # Ensure to_dict properly resolves flavor and uses defaults for not @@ -265,22 +271,31 @@ def test__prepare_server(self): data = 2 hints = {"hint": 3} - sot = server.Server(id=1, availability_zone=zone, user_data=data, - scheduler_hints=hints, min_count=2, max_count=3) + sot = server.Server( + id=1, + availability_zone=zone, + user_data=data, + scheduler_hints=hints, + min_count=2, + max_count=3, + ) request = sot._prepare_request() - self.assertNotIn("OS-EXT-AZ:availability_zone", - request.body[sot.resource_key]) - self.assertEqual(request.body[sot.resource_key]["availability_zone"], - zone) + self.assertNotIn( + "OS-EXT-AZ:availability_zone", request.body[sot.resource_key] + ) + self.assertEqual( + request.body[sot.resource_key]["availability_zone"], zone + ) - self.assertNotIn("OS-EXT-SRV-ATTR:user_data", - request.body[sot.resource_key]) - self.assertEqual(request.body[sot.resource_key]["user_data"], - data) + self.assertNotIn( + "OS-EXT-SRV-ATTR:user_data", request.body[sot.resource_key] + ) + self.assertEqual(request.body[sot.resource_key]["user_data"], data) - self.assertNotIn("OS-SCH-HNT:scheduler_hints", - request.body[sot.resource_key]) + self.assertNotIn( + "OS-SCH-HNT:scheduler_hints", request.body[sot.resource_key] + ) self.assertEqual(request.body["OS-SCH-HNT:scheduler_hints"], hints) self.assertEqual(2, request.body[sot.resource_key]['min_count']) @@ -295,7 +310,9 @@ def test_change_password(self): body = {"changePassword": {"adminPass": "a"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -308,7 +325,9 @@ def test_reboot(self): body = {"reboot": {"type": "HARD"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -321,7 +340,9 @@ def test_force_delete(self): body = {'forceDelete': None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -331,12 +352,15 @@ def test_rebuild(self): sot._translate_response = lambda arg: arg result = sot.rebuild( - self.sess, name='noo', admin_password='seekr3t', - image='http://image/1', access_ipv4="12.34.56.78", + self.sess, + name='noo', + admin_password='seekr3t', + image='http://image/1', + access_ipv4="12.34.56.78", access_ipv6="fe80::100", metadata={"meta var": "meta val"}, user_data="ZWNobyAiaGVsbG8gd29ybGQi", - preserve_ephemeral=False + preserve_ephemeral=False, ) self.assertIsInstance(result, server.Server) @@ -351,12 +375,14 @@ def test_rebuild(self): "accessIPv6": "fe80::100", "metadata": {"meta var": "meta val"}, "user_data": "ZWNobyAiaGVsbG8gd29ybGQi", - "preserve_ephemeral": False + "preserve_ephemeral": False, } } headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -365,9 +391,12 @@ def test_rebuild_minimal(self): # Let the translate pass through, that portion is tested elsewhere sot._translate_response = lambda arg: arg - result = sot.rebuild(self.sess, name='nootoo', - admin_password='seekr3two', - image='http://image/2') + result = sot.rebuild( + self.sess, + name='nootoo', + admin_password='seekr3two', + image='http://image/2', + ) self.assertIsInstance(result, server.Server) @@ -381,7 +410,9 @@ def test_rebuild_minimal(self): } headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -394,7 +425,9 @@ def test_resize(self): body = {"resize": {"flavorRef": "2"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -407,7 +440,9 @@ def test_confirm_resize(self): body = {"confirmResize": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -420,7 +455,9 @@ def test_revert_resize(self): body = {"revertResize": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -440,16 +477,19 @@ def test_create_image_header(self): self.sess.post.return_value = rsp - self.endpoint_data = mock.Mock(spec=['min_microversion', - 'max_microversion'], - min_microversion=None, - max_microversion='2.44') + self.endpoint_data = mock.Mock( + spec=['min_microversion', 'max_microversion'], + min_microversion=None, + max_microversion='2.44', + ) self.sess.get_endpoint_data.return_value = self.endpoint_data image_id = sot.create_image(self.sess, name, metadata) self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -471,17 +511,19 @@ def test_create_image_microver(self): self.sess.post.return_value = rsp - self.endpoint_data = mock.Mock(spec=['min_microversion', - 'max_microversion'], - min_microversion='2.1', - max_microversion='2.56') + self.endpoint_data = mock.Mock( + spec=['min_microversion', 'max_microversion'], + min_microversion='2.1', + max_microversion='2.56', + ) self.sess.get_endpoint_data.return_value = self.endpoint_data self.sess.default_microversion = None image_id = sot.create_image(self.sess, name, metadata) self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion='2.45') + url, json=body, headers=headers, microversion='2.45' + ) self.assertEqual('dummy3', image_id) @@ -499,17 +541,19 @@ def test_create_image_minimal(self): self.sess.post.return_value = rsp - self.endpoint_data = mock.Mock(spec=['min_microversion', - 'max_microversion'], - min_microversion='2.1', - max_microversion='2.56') + self.endpoint_data = mock.Mock( + spec=['min_microversion', 'max_microversion'], + min_microversion='2.1', + max_microversion='2.56', + ) self.sess.get_endpoint_data.return_value = self.endpoint_data self.sess.default_microversion = None self.assertIsNone(self.resp.body, sot.create_image(self.sess, name)) self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion='2.45') + url, json=body, headers=headers, microversion='2.45' + ) def test_add_security_group(self): sot = server.Server(**EXAMPLE) @@ -520,7 +564,9 @@ def test_add_security_group(self): body = {"addSecurityGroup": {"name": "group"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -533,7 +579,9 @@ def test_remove_security_group(self): body = {"removeSecurityGroup": {"name": "group"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -546,7 +594,9 @@ def test_reset_state(self): body = {"os-resetState": {"state": 'active'}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -560,7 +610,9 @@ def test_add_fixed_ip(self): body = {"addFixedIp": {"networkId": "NETWORK-ID"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -574,7 +626,9 @@ def test_remove_fixed_ip(self): body = {"removeFixedIp": {"address": "ADDRESS"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -588,7 +642,9 @@ def test_add_floating_ip(self): body = {"addFloatingIp": {"address": "FLOATING-IP"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -599,11 +655,17 @@ def test_add_floating_ip_with_fixed_addr(self): self.assertIsNone(res) url = 'servers/IDENTIFIER/action' - body = {"addFloatingIp": {"address": "FLOATING-IP", - "fixed_address": "FIXED-ADDR"}} + body = { + "addFloatingIp": { + "address": "FLOATING-IP", + "fixed_address": "FIXED-ADDR", + } + } headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -617,7 +679,9 @@ def test_remove_floating_ip(self): body = {"removeFloatingIp": {"address": "I-AM-FLOATING"}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -628,11 +692,18 @@ def test_backup(self): self.assertIsNone(res) url = 'servers/IDENTIFIER/action' - body = {"createBackup": {"name": "name", "backup_type": "daily", - "rotation": 1}} + body = { + "createBackup": { + "name": "name", + "backup_type": "daily", + "rotation": 1, + } + } headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -646,7 +717,9 @@ def test_pause(self): body = {"pause": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -660,7 +733,9 @@ def test_unpause(self): body = {"unpause": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -674,7 +749,9 @@ def test_suspend(self): body = {"suspend": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -688,7 +765,9 @@ def test_resume(self): body = {"resume": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -702,7 +781,9 @@ def test_lock(self): body = {"lock": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -716,7 +797,9 @@ def test_lock_with_options(self): body = {'lock': {'locked_reason': 'Because why not'}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -730,7 +813,9 @@ def test_unlock(self): body = {"unlock": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -744,7 +829,9 @@ def test_rescue(self): body = {"rescue": {}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -755,11 +842,14 @@ def test_rescue_with_options(self): self.assertIsNone(res) url = 'servers/IDENTIFIER/action' - body = {"rescue": {'adminPass': 'SECRET', - 'rescue_image_ref': 'IMG-ID'}} + body = { + "rescue": {'adminPass': 'SECRET', 'rescue_image_ref': 'IMG-ID'} + } headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -773,7 +863,9 @@ def test_unrescue(self): body = {"unrescue": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -787,23 +879,33 @@ def test_evacuate(self): body = {"evacuate": {}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) def test_evacuate_with_options(self): sot = server.Server(**EXAMPLE) - res = sot.evacuate(self.sess, host='HOST2', admin_pass='NEW_PASS', - force=True) + res = sot.evacuate( + self.sess, host='HOST2', admin_pass='NEW_PASS', force=True + ) self.assertIsNone(res) url = 'servers/IDENTIFIER/action' - body = {"evacuate": {'host': 'HOST2', 'adminPass': 'NEW_PASS', - 'force': True}} + body = { + "evacuate": { + 'host': 'HOST2', + 'adminPass': 'NEW_PASS', + 'force': True, + } + } headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -817,7 +919,9 @@ def test_start(self): body = {"os-start": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -831,7 +935,9 @@ def test_stop(self): body = {"os-stop": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -845,7 +951,9 @@ def test_restore(self): body = {'restore': None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -859,7 +967,9 @@ def test_shelve(self): body = {"shelve": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -873,7 +983,9 @@ def test_unshelve(self): body = {"unshelve": None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -884,12 +996,12 @@ def test_unshelve_availability_zone(self): self.assertIsNone(res) url = 'servers/IDENTIFIER/action' - body = {"unshelve": { - "availability_zone": sot.availability_zone - }} + body = {"unshelve": {"availability_zone": sot.availability_zone}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -900,13 +1012,14 @@ def test_unshelve_unpin_az(self): self.assertIsNone(res) url = 'servers/IDENTIFIER/action' - body = {"unshelve": { - "availability_zone": None - }} + body = {"unshelve": {"availability_zone": None}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, - microversion=self.sess.default_microversion) + url, + json=body, + headers=headers, + microversion=self.sess.default_microversion, + ) def test_unshelve_host(self): sot = server.Server(**EXAMPLE) @@ -915,13 +1028,14 @@ def test_unshelve_host(self): self.assertIsNone(res) url = 'servers/IDENTIFIER/action' - body = {"unshelve": { - "host": sot.hypervisor_hostname - }} + body = {"unshelve": {"host": sot.hypervisor_hostname}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, - microversion=self.sess.default_microversion) + url, + json=body, + headers=headers, + microversion=self.sess.default_microversion, + ) def test_unshelve_host_and_availability_zone(self): sot = server.Server(**EXAMPLE) @@ -929,19 +1043,24 @@ def test_unshelve_host_and_availability_zone(self): res = sot.unshelve( self.sess, availability_zone=sot.availability_zone, - host=sot.hypervisor_hostname + host=sot.hypervisor_hostname, ) self.assertIsNone(res) url = 'servers/IDENTIFIER/action' - body = {"unshelve": { - "availability_zone": sot.availability_zone, - "host": sot.hypervisor_hostname - }} + body = { + "unshelve": { + "availability_zone": sot.availability_zone, + "host": sot.hypervisor_hostname, + } + } headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, - microversion=self.sess.default_microversion) + url, + json=body, + headers=headers, + microversion=self.sess.default_microversion, + ) def test_migrate(self): sot = server.Server(**EXAMPLE) @@ -954,7 +1073,9 @@ def test_migrate(self): headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -968,7 +1089,9 @@ def test_trigger_crash_dump(self): body = {'trigger_crash_dump': None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -982,7 +1105,9 @@ def test_get_console_output(self): body = {'os-getConsoleOutput': {}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -994,7 +1119,9 @@ def test_get_console_output(self): headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -1048,11 +1175,9 @@ def test_get_console_url(self): microversion=self.sess.default_microversion, ) - self.assertRaises(ValueError, - sot.get_console_url, - self.sess, - 'fake_type' - ) + self.assertRaises( + ValueError, sot.get_console_url, self.sess, 'fake_type' + ) def test_live_migrate_no_force(self): sot = server.Server(**EXAMPLE) @@ -1060,15 +1185,18 @@ def test_live_migrate_no_force(self): class FakeEndpointData: min_microversion = None max_microversion = None + self.sess.get_endpoint_data.return_value = FakeEndpointData() ex = self.assertRaises( ValueError, sot.live_migrate, - self.sess, host='HOST2', force=False, block_migration=False) - self.assertIn( - "Live migration on this cloud implies 'force'", - str(ex)) + self.sess, + host='HOST2', + force=False, + block_migration=False, + ) + self.assertIn("Live migration on this cloud implies 'force'", str(ex)) def test_live_migrate_no_microversion_force_true(self): sot = server.Server(**EXAMPLE) @@ -1076,11 +1204,16 @@ def test_live_migrate_no_microversion_force_true(self): class FakeEndpointData: min_microversion = None max_microversion = None + self.sess.get_endpoint_data.return_value = FakeEndpointData() res = sot.live_migrate( - self.sess, host='HOST2', force=True, block_migration=True, - disk_over_commit=True) + self.sess, + host='HOST2', + force=True, + block_migration=True, + disk_over_commit=True, + ) self.assertIsNone(res) url = 'servers/IDENTIFIER/action' @@ -1088,13 +1221,15 @@ class FakeEndpointData: 'os-migrateLive': { 'host': 'HOST2', 'disk_over_commit': True, - 'block_migration': True + 'block_migration': True, } } headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, + url, + json=body, + headers=headers, microversion=self.sess.default_microversion, ) @@ -1104,11 +1239,13 @@ def test_live_migrate_25(self): class FakeEndpointData: min_microversion = '2.1' max_microversion = '2.25' + self.sess.get_endpoint_data.return_value = FakeEndpointData() self.sess.default_microversion = None res = sot.live_migrate( - self.sess, host='HOST2', force=True, block_migration=False) + self.sess, host='HOST2', force=True, block_migration=False + ) self.assertIsNone(res) url = 'servers/IDENTIFIER/action' @@ -1121,7 +1258,8 @@ class FakeEndpointData: headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion='2.25') + url, json=body, headers=headers, microversion='2.25' + ) def test_live_migrate_25_default_block(self): sot = server.Server(**EXAMPLE) @@ -1129,11 +1267,13 @@ def test_live_migrate_25_default_block(self): class FakeEndpointData: min_microversion = '2.1' max_microversion = '2.25' + self.sess.get_endpoint_data.return_value = FakeEndpointData() self.sess.default_microversion = None res = sot.live_migrate( - self.sess, host='HOST2', force=True, block_migration=None) + self.sess, host='HOST2', force=True, block_migration=None + ) self.assertIsNone(res) url = 'servers/IDENTIFIER/action' @@ -1146,7 +1286,8 @@ class FakeEndpointData: headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion='2.25') + url, json=body, headers=headers, microversion='2.25' + ) def test_live_migrate_30(self): sot = server.Server(**EXAMPLE) @@ -1154,24 +1295,22 @@ def test_live_migrate_30(self): class FakeEndpointData: min_microversion = '2.1' max_microversion = '2.30' + self.sess.get_endpoint_data.return_value = FakeEndpointData() self.sess.default_microversion = None res = sot.live_migrate( - self.sess, host='HOST2', force=False, block_migration=False) + self.sess, host='HOST2', force=False, block_migration=False + ) self.assertIsNone(res) url = 'servers/IDENTIFIER/action' - body = { - 'os-migrateLive': { - 'block_migration': False, - 'host': 'HOST2' - } - } + body = {'os-migrateLive': {'block_migration': False, 'host': 'HOST2'}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion='2.30') + url, json=body, headers=headers, microversion='2.30' + ) def test_live_migrate_30_force(self): sot = server.Server(**EXAMPLE) @@ -1179,11 +1318,13 @@ def test_live_migrate_30_force(self): class FakeEndpointData: min_microversion = '2.1' max_microversion = '2.30' + self.sess.get_endpoint_data.return_value = FakeEndpointData() self.sess.default_microversion = None res = sot.live_migrate( - self.sess, host='HOST2', force=True, block_migration=None) + self.sess, host='HOST2', force=True, block_migration=None + ) self.assertIsNone(res) url = 'servers/IDENTIFIER/action' @@ -1197,7 +1338,8 @@ class FakeEndpointData: headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion='2.30') + url, json=body, headers=headers, microversion='2.30' + ) def test_get_topology(self): sot = server.Server(**EXAMPLE) @@ -1205,6 +1347,7 @@ def test_get_topology(self): class FakeEndpointData: min_microversion = '2.1' max_microversion = '2.78' + self.sess.get_endpoint_data.return_value = FakeEndpointData() self.sess.default_microversion = None @@ -1213,43 +1356,21 @@ class FakeEndpointData: topology = { "nodes": [ { - "cpu_pinning": { - "0": 0, - "1": 5 - }, + "cpu_pinning": {"0": 0, "1": 5}, "host_node": 0, "memory_mb": 1024, - "siblings": [ - [ - 0, - 1 - ] - ], - "vcpu_set": [ - 0, - 1 - ] + "siblings": [[0, 1]], + "vcpu_set": [0, 1], }, { - "cpu_pinning": { - "2": 1, - "3": 8 - }, + "cpu_pinning": {"2": 1, "3": 8}, "host_node": 1, "memory_mb": 2048, - "siblings": [ - [ - 2, - 3 - ] - ], - "vcpu_set": [ - 2, - 3 - ] - } + "siblings": [[2, 3]], + "vcpu_set": [2, 3], + }, ], - "pagesize_kb": 4 + "pagesize_kb": 4, } response.status_code = 200 @@ -1269,37 +1390,37 @@ def test_get_security_groups(self): response = mock.Mock() - sgs = [{ - 'description': 'default', - 'id': 1, - 'name': 'default', - 'rules': [ - { - 'direction': 'egress', - 'ethertype': 'IPv6', - 'id': '3c0e45ff-adaf-4124-b083-bf390e5482ff', - 'port_range_max': None, - 'port_range_min': None, - 'protocol': None, - 'remote_group_id': None, - 'remote_ip_prefix': None, - 'security_group_id': '1', - 'project_id': 'e4f50856753b4dc6afee5fa6b9b6c550', - 'revision_number': 1, - 'tags': ['tag1,tag2'], - 'tenant_id': 'e4f50856753b4dc6afee5fa6b9b6c550', - 'created_at': '2018-03-19T19:16:56Z', - 'updated_at': '2018-03-19T19:16:56Z', - 'description': '' - } - ], - 'tenant_id': 'e4f50856753b4dc6afee5fa6b9b6c550' - }] + sgs = [ + { + 'description': 'default', + 'id': 1, + 'name': 'default', + 'rules': [ + { + 'direction': 'egress', + 'ethertype': 'IPv6', + 'id': '3c0e45ff-adaf-4124-b083-bf390e5482ff', + 'port_range_max': None, + 'port_range_min': None, + 'protocol': None, + 'remote_group_id': None, + 'remote_ip_prefix': None, + 'security_group_id': '1', + 'project_id': 'e4f50856753b4dc6afee5fa6b9b6c550', + 'revision_number': 1, + 'tags': ['tag1,tag2'], + 'tenant_id': 'e4f50856753b4dc6afee5fa6b9b6c550', + 'created_at': '2018-03-19T19:16:56Z', + 'updated_at': '2018-03-19T19:16:56Z', + 'description': '', + } + ], + 'tenant_id': 'e4f50856753b4dc6afee5fa6b9b6c550', + } + ] response.status_code = 200 - response.json.return_value = { - 'security_groups': sgs - } + response.json.return_value = {'security_groups': sgs} self.sess.get.return_value = response sot.fetch_security_groups(self.sess) diff --git a/openstack/tests/unit/compute/v2/test_server_actions.py b/openstack/tests/unit/compute/v2/test_server_actions.py index 881634c35..14d94b0ec 100644 --- a/openstack/tests/unit/compute/v2/test_server_actions.py +++ b/openstack/tests/unit/compute/v2/test_server_actions.py @@ -26,7 +26,7 @@ 'result': 'Success', 'start_time': '2018-04-25T01:26:36.539271', 'traceback': None, - 'details': None + 'details': None, } ], 'instance_uuid': '4bf3473b-d550-4b65-9409-292d44ab14a2', @@ -40,7 +40,6 @@ class TestServerAction(base.TestCase): - def setUp(self): super().setUp() self.resp = mock.Mock() diff --git a/openstack/tests/unit/compute/v2/test_server_diagnostics.py b/openstack/tests/unit/compute/v2/test_server_diagnostics.py index d4dc5753c..f71939df3 100644 --- a/openstack/tests/unit/compute/v2/test_server_diagnostics.py +++ b/openstack/tests/unit/compute/v2/test_server_diagnostics.py @@ -17,29 +17,20 @@ IDENTIFIER = 'IDENTIFIER' EXAMPLE = { "config_drive": True, - "cpu_details": [ - { - "id": 0, - "time": 17300000000, - "utilisation": 15 - } - ], + "cpu_details": [{"id": 0, "time": 17300000000, "utilisation": 15}], "disk_details": [ { "errors_count": 1, "read_bytes": 262144, "read_requests": 112, "write_bytes": 5778432, - "write_requests": 488 + "write_requests": 488, } ], "driver": "libvirt", "hypervisor": "kvm", "hypervisor_os": "ubuntu", - "memory_details": { - "maximum": 524288, - "used": 0 - }, + "memory_details": {"maximum": 524288, "used": 0}, "nic_details": [ { "mac_address": "01:23:45:67:89:ab", @@ -52,19 +43,18 @@ "tx_errors": 400, "tx_octets": 140208, "tx_packets": 662, - "tx_rate": 600 + "tx_rate": 600, } ], "num_cpus": 1, "num_disks": 1, "num_nics": 1, "state": "running", - "uptime": 46664 + "uptime": 46664, } class TestServerInterface(base.TestCase): - def test_basic(self): sot = server_diagnostics.ServerDiagnostics() self.assertEqual('diagnostics', sot.resource_key) diff --git a/openstack/tests/unit/compute/v2/test_server_group.py b/openstack/tests/unit/compute/v2/test_server_group.py index cec61ef2a..b1eb6737e 100644 --- a/openstack/tests/unit/compute/v2/test_server_group.py +++ b/openstack/tests/unit/compute/v2/test_server_group.py @@ -27,7 +27,6 @@ class TestServerGroup(base.TestCase): - def test_basic(self): sot = server_group.ServerGroup() self.assertEqual('server_group', sot.resource_key) @@ -39,9 +38,14 @@ def test_basic(self): self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertDictEqual({"all_projects": "all_projects", - "limit": "limit", "marker": "marker"}, - sot._query_mapping._mapping) + self.assertDictEqual( + { + "all_projects": "all_projects", + "limit": "limit", + "marker": "marker", + }, + sot._query_mapping._mapping, + ) def test_make_it(self): sot = server_group.ServerGroup(**EXAMPLE) diff --git a/openstack/tests/unit/compute/v2/test_server_interface.py b/openstack/tests/unit/compute/v2/test_server_interface.py index e506f5637..74194a812 100644 --- a/openstack/tests/unit/compute/v2/test_server_interface.py +++ b/openstack/tests/unit/compute/v2/test_server_interface.py @@ -19,7 +19,7 @@ 'fixed_ips': [ { 'ip_address': '192.168.1.1', - 'subnet_id': 'f8a6e8f8-c2ec-497c-9f23-da9616de54ef' + 'subnet_id': 'f8a6e8f8-c2ec-497c-9f23-da9616de54ef', } ], 'mac_addr': '2', @@ -32,7 +32,6 @@ class TestServerInterface(base.TestCase): - def test_basic(self): sot = server_interface.ServerInterface() self.assertEqual('interfaceAttachment', sot.resource_key) diff --git a/openstack/tests/unit/compute/v2/test_server_ip.py b/openstack/tests/unit/compute/v2/test_server_ip.py index f20e311f2..fa2790281 100644 --- a/openstack/tests/unit/compute/v2/test_server_ip.py +++ b/openstack/tests/unit/compute/v2/test_server_ip.py @@ -24,7 +24,6 @@ class TestServerIP(base.TestCase): - def test_basic(self): sot = server_ip.ServerIP() self.assertEqual('addresses', sot.resources_key) @@ -46,10 +45,17 @@ def test_list(self): resp = mock.Mock() sess.get.return_value = resp resp.json.return_value = { - "addresses": {"label1": [{"version": 1, "addr": "a1"}, - {"version": 2, "addr": "a2"}], - "label2": [{"version": 3, "addr": "a3"}, - {"version": 4, "addr": "a4"}]}} + "addresses": { + "label1": [ + {"version": 1, "addr": "a1"}, + {"version": 2, "addr": "a2"}, + ], + "label2": [ + {"version": 3, "addr": "a3"}, + {"version": 4, "addr": "a4"}, + ], + } + } ips = list(server_ip.ServerIP.list(sess, server_id=IDENTIFIER)) @@ -78,13 +84,15 @@ def test_list_network_label(self): sess = mock.Mock() resp = mock.Mock() sess.get.return_value = resp - resp.json.return_value = {label: [{"version": 1, - "addr": "a1"}, - {"version": 2, - "addr": "a2"}]} + resp.json.return_value = { + label: [{"version": 1, "addr": "a1"}, {"version": 2, "addr": "a2"}] + } - ips = list(server_ip.ServerIP.list(sess, server_id=IDENTIFIER, - network_label=label)) + ips = list( + server_ip.ServerIP.list( + sess, server_id=IDENTIFIER, network_label=label + ) + ) self.assertEqual(2, len(ips)) ips = sorted(ips, key=lambda ip: ip.version) diff --git a/openstack/tests/unit/compute/v2/test_server_migration.py b/openstack/tests/unit/compute/v2/test_server_migration.py index b652d38c8..cc309f683 100644 --- a/openstack/tests/unit/compute/v2/test_server_migration.py +++ b/openstack/tests/unit/compute/v2/test_server_migration.py @@ -38,7 +38,6 @@ class TestServerMigration(base.TestCase): - def setUp(self): super().setUp() self.resp = mock.Mock() @@ -96,7 +95,9 @@ def test_make_it(self): self.assertEqual(EXAMPLE['disk_total_bytes'], sot.disk_total_bytes) @mock.patch.object( - server_migration.ServerMigration, '_get_session', lambda self, x: x, + server_migration.ServerMigration, + '_get_session', + lambda self, x: x, ) def test_force_complete(self): sot = server_migration.ServerMigration(**EXAMPLE) @@ -104,9 +105,12 @@ def test_force_complete(self): self.assertIsNone(sot.force_complete(self.sess)) url = 'servers/%s/migrations/%s/action' % ( - EXAMPLE['server_uuid'], EXAMPLE['id'] + EXAMPLE['server_uuid'], + EXAMPLE['id'], ) body = {'force_complete': None} self.sess.post.assert_called_with( - url, microversion=mock.ANY, json=body, + url, + microversion=mock.ANY, + json=body, ) diff --git a/openstack/tests/unit/compute/v2/test_server_remote_console.py b/openstack/tests/unit/compute/v2/test_server_remote_console.py index 78708cb15..96844eb3a 100644 --- a/openstack/tests/unit/compute/v2/test_server_remote_console.py +++ b/openstack/tests/unit/compute/v2/test_server_remote_console.py @@ -19,15 +19,10 @@ IDENTIFIER = 'IDENTIFIER' -EXAMPLE = { - 'protocol': 'rdp', - 'type': 'rdp', - 'url': 'fake' -} +EXAMPLE = {'protocol': 'rdp', 'type': 'rdp', 'url': 'fake'} class TestServerRemoteConsole(base.TestCase): - def setUp(self): super(TestServerRemoteConsole, self).setUp() self.sess = mock.Mock(spec=adapter.Adapter) @@ -43,8 +38,9 @@ def setUp(self): def test_basic(self): sot = server_remote_console.ServerRemoteConsole() self.assertEqual('remote_console', sot.resource_key) - self.assertEqual('/servers/%(server_id)s/remote-consoles', - sot.base_path) + self.assertEqual( + '/servers/%(server_id)s/remote-consoles', sot.base_path + ) self.assertTrue(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_commit) @@ -57,15 +53,13 @@ def test_make_it(self): def test_create_type_mks_old(self): sot = server_remote_console.ServerRemoteConsole( - server_id='fake_server', type='webmks') + server_id='fake_server', type='webmks' + ) class FakeEndpointData: min_microversion = '2' max_microversion = '2.5' + self.sess.get_endpoint_data.return_value = FakeEndpointData() - self.assertRaises( - ValueError, - sot.create, - self.sess - ) + self.assertRaises(ValueError, sot.create, self.sess) diff --git a/openstack/tests/unit/compute/v2/test_service.py b/openstack/tests/unit/compute/v2/test_service.py index 3f45a690f..65bd629cc 100644 --- a/openstack/tests/unit/compute/v2/test_service.py +++ b/openstack/tests/unit/compute/v2/test_service.py @@ -23,12 +23,11 @@ 'host': 'host1', 'status': 'enabled', 'state': 'up', - 'zone': 'nova' + 'zone': 'nova', } class TestService(base.TestCase): - def setUp(self): super(TestService, self).setUp() self.resp = mock.Mock() @@ -49,14 +48,16 @@ def test_basic(self): self.assertTrue(sot.allow_list) self.assertFalse(sot.allow_fetch) - self.assertDictEqual({ - 'binary': 'binary', - 'host': 'host', - 'limit': 'limit', - 'marker': 'marker', - 'name': 'binary', - }, - sot._query_mapping._mapping) + self.assertDictEqual( + { + 'binary': 'binary', + 'host': 'host', + 'limit': 'limit', + 'marker': 'marker', + 'name': 'binary', + }, + sot._query_mapping._mapping, + ) def test_make_it(self): sot = service.Service(**EXAMPLE) @@ -91,16 +92,14 @@ def test_find_with_id_single_match(self): list_mock.return_value = data sot = service.Service.find( - self.sess, '2', ignore_missing=True, - binary='bin1', host='host' + self.sess, '2', ignore_missing=True, binary='bin1', host='host' ) self.assertEqual(data[1], sot) # Verify find when ID is int sot = service.Service.find( - self.sess, 1, ignore_missing=True, - binary='bin1', host='host' + self.sess, 1, ignore_missing=True, binary='bin1', host='host' ) self.assertEqual(data[0], sot) @@ -113,9 +112,11 @@ def test_find_no_match(self): with mock.patch.object(service.Service, 'list') as list_mock: list_mock.return_value = data - self.assertIsNone(service.Service.find( - self.sess, 'fake', ignore_missing=True, host='host' - )) + self.assertIsNone( + service.Service.find( + self.sess, 'fake', ignore_missing=True, host='host' + ) + ) def test_find_no_match_exception(self): data = [ @@ -128,7 +129,10 @@ def test_find_no_match_exception(self): self.assertRaises( exceptions.ResourceNotFound, service.Service.find, - self.sess, 'fake', ignore_missing=False, host='host' + self.sess, + 'fake', + ignore_missing=False, + host='host', ) def test_find_multiple_match(self): @@ -142,11 +146,17 @@ def test_find_multiple_match(self): self.assertRaises( exceptions.DuplicateResource, service.Service.find, - self.sess, 'bin1', ignore_missing=False, host='host' + self.sess, + 'bin1', + ignore_missing=False, + host='host', ) - @mock.patch('openstack.utils.supports_microversion', autospec=True, - return_value=False) + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=False, + ) def test_set_forced_down_before_211(self, mv_mock): sot = service.Service(**EXAMPLE) @@ -159,10 +169,14 @@ def test_set_forced_down_before_211(self, mv_mock): 'host': 'host1', } self.sess.put.assert_called_with( - url, json=body, microversion=self.sess.default_microversion) - - @mock.patch('openstack.utils.supports_microversion', autospec=True, - return_value=True) + url, json=body, microversion=self.sess.default_microversion + ) + + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=True, + ) def test_set_forced_down_after_211(self, mv_mock): sot = service.Service(**EXAMPLE) @@ -175,11 +189,13 @@ def test_set_forced_down_after_211(self, mv_mock): 'host': 'host1', 'forced_down': True, } - self.sess.put.assert_called_with( - url, json=body, microversion='2.11') + self.sess.put.assert_called_with(url, json=body, microversion='2.11') - @mock.patch('openstack.utils.supports_microversion', autospec=True, - return_value=True) + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=True, + ) def test_set_forced_down_after_253(self, mv_mock): sot = service.Service(**EXAMPLE) @@ -192,8 +208,7 @@ def test_set_forced_down_after_253(self, mv_mock): 'host': sot.host, 'forced_down': True, } - self.sess.put.assert_called_with( - url, json=body, microversion='2.11') + self.sess.put.assert_called_with(url, json=body, microversion='2.11') def test_enable(self): sot = service.Service(**EXAMPLE) @@ -207,7 +222,8 @@ def test_enable(self): 'host': 'host1', } self.sess.put.assert_called_with( - url, json=body, microversion=self.sess.default_microversion) + url, json=body, microversion=self.sess.default_microversion + ) def test_disable(self): sot = service.Service(**EXAMPLE) @@ -221,7 +237,8 @@ def test_disable(self): 'host': 'host1', } self.sess.put.assert_called_with( - url, json=body, microversion=self.sess.default_microversion) + url, json=body, microversion=self.sess.default_microversion + ) def test_disable_with_reason(self): sot = service.Service(**EXAMPLE) @@ -235,7 +252,8 @@ def test_disable_with_reason(self): body = { 'binary': 'nova-compute', 'host': 'host1', - 'disabled_reason': reason + 'disabled_reason': reason, } self.sess.put.assert_called_with( - url, json=body, microversion=self.sess.default_microversion) + url, json=body, microversion=self.sess.default_microversion + ) diff --git a/openstack/tests/unit/compute/v2/test_volume_attachment.py b/openstack/tests/unit/compute/v2/test_volume_attachment.py index 04218a80b..5091463ef 100644 --- a/openstack/tests/unit/compute/v2/test_volume_attachment.py +++ b/openstack/tests/unit/compute/v2/test_volume_attachment.py @@ -26,29 +26,31 @@ class TestServerInterface(base.TestCase): - def test_basic(self): sot = volume_attachment.VolumeAttachment() self.assertEqual('volumeAttachment', sot.resource_key) self.assertEqual('volumeAttachments', sot.resources_key) - self.assertEqual('/servers/%(server_id)s/os-volume_attachments', - sot.base_path) + self.assertEqual( + '/servers/%(server_id)s/os-volume_attachments', + sot.base_path, + ) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertDictEqual({"limit": "limit", - "offset": "offset", - "marker": "marker"}, - sot._query_mapping._mapping) + self.assertDictEqual( + {"limit": "limit", "offset": "offset", "marker": "marker"}, + sot._query_mapping._mapping, + ) def test_make_it(self): sot = volume_attachment.VolumeAttachment(**EXAMPLE) self.assertEqual(EXAMPLE['volumeId'], sot.id) self.assertEqual(EXAMPLE['attachment_id'], sot.attachment_id) self.assertEqual( - EXAMPLE['delete_on_termination'], sot.delete_on_termination, + EXAMPLE['delete_on_termination'], + sot.delete_on_termination, ) self.assertEqual(EXAMPLE['device'], sot.device) # FIXME(stephenfin): This conflicts since there is a server ID in the From 6baf11f606f697dac2a54ee744a534f71cb9a35a Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 3 May 2023 11:38:08 +0100 Subject: [PATCH 3235/3836] Prepare for hacking 6.x This brings in a new version of flake8 as well as additional checks of its own. Address all of them in advance of the bump. Change-Id: I870bd8110106d4714db7ead94429fad6a74beb88 Signed-off-by: Stephen Finucane --- openstack/config/vendors/citycloud.json | 2 +- openstack/object_store/v1/obj.py | 4 ++-- openstack/resource.py | 5 +++-- openstack/tests/functional/cloud/test_compute.py | 4 ++-- openstack/tests/functional/cloud/test_quotas.py | 2 +- openstack/tests/unit/base.py | 4 ++-- openstack/tests/unit/cloud/test_image.py | 12 ++++++------ openstack/tests/unit/compute/v2/test_server_ip.py | 12 ++++++------ openstack/tests/unit/config/test_cloud_config.py | 4 ++-- openstack/tests/unit/test_missing_version.py | 3 +-- openstack/utils.py | 7 ++++--- .../network_add_bgp_resources-c182dc2873d6db18.yaml | 2 +- 12 files changed, 31 insertions(+), 30 deletions(-) diff --git a/openstack/config/vendors/citycloud.json b/openstack/config/vendors/citycloud.json index 53057076a..a1c765195 100644 --- a/openstack/config/vendors/citycloud.json +++ b/openstack/config/vendors/citycloud.json @@ -15,6 +15,6 @@ "requires_floating_ip": true, "block_storage_api_version": "3", "identity_api_version": "3", - "image_format": "raw" + "image_format": "raw" } } diff --git a/openstack/object_store/v1/obj.py b/openstack/object_store/v1/obj.py index 7c9b7693b..095a2a56e 100644 --- a/openstack/object_store/v1/obj.py +++ b/openstack/object_store/v1/obj.py @@ -263,10 +263,10 @@ def delete_metadata(self, session, keys): attr_keys_to_delete = set() for key in keys: if key == 'delete_after': - del(metadata['delete_at']) + del metadata['delete_at'] else: if key in metadata: - del(metadata[key]) + del metadata[key] # Delete the attribute from the local copy of the object. # Metadata that doesn't have Component attributes is # handled by self.metadata being reset when we run diff --git a/openstack/resource.py b/openstack/resource.py index 54bbe8e5c..fd85daca8 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -99,7 +99,7 @@ class _BaseComponent: deprecation_reason = None #: Control field used to manage the deprecation warning. We want to warn - # only once when the attribute is retrieved in the code. + #: only once when the attribute is retrieved in the code. already_warned_deprecation = False def __init__( @@ -1009,7 +1009,8 @@ def _from_munch(cls, obj, synchronized=True, connection=None): return cls(_synchronized=synchronized, connection=connection, **obj) def _attr_to_dict(self, attr, to_munch): - """For a given attribute, convert it into a form suitable for a dict value. + """For a given attribute, convert it into a form suitable for a dict + value. :param bool attr: Attribute name to convert diff --git a/openstack/tests/functional/cloud/test_compute.py b/openstack/tests/functional/cloud/test_compute.py index d0fefb20f..c97ef5fe6 100644 --- a/openstack/tests/functional/cloud/test_compute.py +++ b/openstack/tests/functional/cloud/test_compute.py @@ -200,7 +200,7 @@ def test_get_server_console(self): # returning a string tests that the call is correct. Testing that # the cloud returns actual data in the output is out of scope. log = self.user_cloud._get_server_console_output(server_id=server.id) - self.assertTrue(isinstance(log, str)) + self.assertIsInstance(log, str) def test_get_server_console_name_or_id(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) @@ -210,7 +210,7 @@ def test_get_server_console_name_or_id(self): flavor=self.flavor, wait=True) log = self.user_cloud.get_server_console(server=self.server_name) - self.assertTrue(isinstance(log, str)) + self.assertIsInstance(log, str) def test_list_availability_zone_names(self): self.assertEqual( diff --git a/openstack/tests/functional/cloud/test_quotas.py b/openstack/tests/functional/cloud/test_quotas.py index 2d06167a4..f23c6ae59 100644 --- a/openstack/tests/functional/cloud/test_quotas.py +++ b/openstack/tests/functional/cloud/test_quotas.py @@ -117,4 +117,4 @@ def test_get_quotas_details(self): quota_val = quota_details[quota] if quota_val: for expected_key in expected_keys: - self.assertTrue(expected_key in quota_val) + self.assertIn(expected_key, quota_val) diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index e2b6226d9..2b0b28217 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -760,8 +760,8 @@ def assertResourceEqual(self, actual, expected, resource_type): ) def assertResourceListEqual(self, actual, expected, resource_type): - """Helper for the assertEqual which compares Resource lists object against - dictionary representing expected state. + """Helper for the assertEqual which compares Resource lists object + against dictionary representing expected state. :param list actual: List of actual objects. :param listexpected: List of dictionaries representing expected diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 96ca6445d..dfd9be7fb 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -561,9 +561,9 @@ def test_create_image_task(self): ) image_no_checksums = self.fake_image_dict.copy() - del(image_no_checksums['owner_specified.openstack.md5']) - del(image_no_checksums['owner_specified.openstack.sha256']) - del(image_no_checksums['owner_specified.openstack.object']) + del image_no_checksums['owner_specified.openstack.md5'] + del image_no_checksums['owner_specified.openstack.sha256'] + del image_no_checksums['owner_specified.openstack.object'] self.register_uris([ dict(method='GET', @@ -736,9 +736,9 @@ def test_delete_image_task(self): object_path = self.fake_image_dict['owner_specified.openstack.object'] image_no_checksums = self.fake_image_dict.copy() - del(image_no_checksums['owner_specified.openstack.md5']) - del(image_no_checksums['owner_specified.openstack.sha256']) - del(image_no_checksums['owner_specified.openstack.object']) + del image_no_checksums['owner_specified.openstack.md5'] + del image_no_checksums['owner_specified.openstack.sha256'] + del image_no_checksums['owner_specified.openstack.object'] self.register_uris([ dict(method='GET', diff --git a/openstack/tests/unit/compute/v2/test_server_ip.py b/openstack/tests/unit/compute/v2/test_server_ip.py index fa2790281..833fae769 100644 --- a/openstack/tests/unit/compute/v2/test_server_ip.py +++ b/openstack/tests/unit/compute/v2/test_server_ip.py @@ -62,19 +62,19 @@ def test_list(self): self.assertEqual(4, len(ips)) ips = sorted(ips, key=lambda ip: ip.version) - self.assertEqual(type(ips[0]), server_ip.ServerIP) + self.assertIsInstance(ips[0], server_ip.ServerIP) self.assertEqual(ips[0].network_label, "label1") self.assertEqual(ips[0].address, "a1") self.assertEqual(ips[0].version, 1) - self.assertEqual(type(ips[1]), server_ip.ServerIP) + self.assertIsInstance(ips[1], server_ip.ServerIP) self.assertEqual(ips[1].network_label, "label1") self.assertEqual(ips[1].address, "a2") self.assertEqual(ips[1].version, 2) - self.assertEqual(type(ips[2]), server_ip.ServerIP) + self.assertIsInstance(ips[2], server_ip.ServerIP) self.assertEqual(ips[2].network_label, "label2") self.assertEqual(ips[2].address, "a3") self.assertEqual(ips[2].version, 3) - self.assertEqual(type(ips[3]), server_ip.ServerIP) + self.assertIsInstance(ips[3], server_ip.ServerIP) self.assertEqual(ips[3].network_label, "label2") self.assertEqual(ips[3].address, "a4") self.assertEqual(ips[3].version, 4) @@ -97,11 +97,11 @@ def test_list_network_label(self): self.assertEqual(2, len(ips)) ips = sorted(ips, key=lambda ip: ip.version) - self.assertEqual(type(ips[0]), server_ip.ServerIP) + self.assertIsInstance(ips[0], server_ip.ServerIP) self.assertEqual(ips[0].network_label, label) self.assertEqual(ips[0].address, "a1") self.assertEqual(ips[0].version, 1) - self.assertEqual(type(ips[1]), server_ip.ServerIP) + self.assertIsInstance(ips[1], server_ip.ServerIP) self.assertEqual(ips[1].network_label, label) self.assertEqual(ips[1].address, "a2") self.assertEqual(ips[1].version, 2) diff --git a/openstack/tests/unit/config/test_cloud_config.py b/openstack/tests/unit/config/test_cloud_config.py index fd6d30316..098cee0cb 100644 --- a/openstack/tests/unit/config/test_cloud_config.py +++ b/openstack/tests/unit/config/test_cloud_config.py @@ -64,8 +64,8 @@ def test_arbitrary_attributes(self): def test_iteration(self): cc = cloud_region.CloudRegion("test1", "region-al", fake_config_dict) - self.assertTrue('a' in cc) - self.assertFalse('x' in cc) + self.assertIn('a', cc) + self.assertNotIn('x', cc) def test_equality(self): cc1 = cloud_region.CloudRegion("test1", "region-al", fake_config_dict) diff --git a/openstack/tests/unit/test_missing_version.py b/openstack/tests/unit/test_missing_version.py index 6e1d5b975..c1a948f71 100644 --- a/openstack/tests/unit/test_missing_version.py +++ b/openstack/tests/unit/test_missing_version.py @@ -40,6 +40,5 @@ def test_unsupported_version(self): def test_unsupported_version_override(self): self.cloud.config.config['image_api_version'] = '7' - self.assertTrue(isinstance(self.cloud.image, proxy.Proxy)) - + self.assertIsInstance(self.cloud.image, proxy.Proxy) self.assert_calls() diff --git a/openstack/utils.py b/openstack/utils.py index 7c23e9cf8..c058f77b0 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -442,9 +442,10 @@ def __setattr__(self, k, v): object.__setattr__(self, k, v) def __delattr__(self, k): - """Deletes attribute k if it exists, otherwise deletes key k. A KeyError - raised by deleting the key--such as when the key is missing--will - propagate as an AttributeError instead. + """Deletes attribute k if it exists, otherwise deletes key k. + + A KeyError raised by deleting the key - such as when the key is missing + - will propagate as an AttributeError instead. """ try: # Throws exception if not in prototype chain diff --git a/releasenotes/notes/network_add_bgp_resources-c182dc2873d6db18.yaml b/releasenotes/notes/network_add_bgp_resources-c182dc2873d6db18.yaml index b87c8d556..5356c7773 100644 --- a/releasenotes/notes/network_add_bgp_resources-c182dc2873d6db18.yaml +++ b/releasenotes/notes/network_add_bgp_resources-c182dc2873d6db18.yaml @@ -2,7 +2,7 @@ features: - | Add BGP Speaker and BGP Peer resources, and introduce support for CRUD - operations for these. Additional REST operations introduced for speakers: + operations for these. Additional REST operations introduced for speakers: add_bgp_peer, remove_bgp_peer, add_gateway_network, remove_gateway_network, get_advertised_routes, get_bgp_dragents, add_bgp_speaker_to_draget, remove_bgp_speaker_from_dragent. From 93256267699e2fd015745bfdd21c8118cb20087d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 3 May 2023 11:55:34 +0100 Subject: [PATCH 3236/3836] Use pre-commit for 'pep8' tox target, bump versions We want to bump the versions of hacking, but doing so requires changes in two places: '.pre-commit-config.yaml' and 'tox.ini'. This is silly: we can simply use tox to handle pre-commit and leave all other dependencies to pre-commit. Do this, migrating doc8 to pre-commit and bumping the other dependencies in the process. Change-Id: I26fa07145129d3ef9cb17693427ed70e55dbaaf5 Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 11 ++++++++--- tox.ini | 9 ++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5814ebb93..09c805ab4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ default_language_version: python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.4.0 + rev: v4.4.0 hooks: - id: trailing-whitespace - id: mixed-line-ending @@ -16,13 +16,18 @@ repos: - id: debug-statements - id: check-yaml files: .*\.(yaml|yml)$ - exclude: '^.zuul.yaml' + exclude: '^zuul.d/.*$' + - repo: https://github.com/PyCQA/doc8 + rev: v1.1.1 + hooks: + - id: doc8 - repo: local hooks: - id: flake8 name: flake8 additional_dependencies: - - hacking>=3.0.1,<3.1.0 + - hacking>=6.0.1,<6.1.0 + - flake8-import-order>=0.18.2,<0.19.0 language: python entry: flake8 files: '^.*\.py$' diff --git a/tox.ini b/tox.ini index 6602a2901..24ec06df8 100644 --- a/tox.ini +++ b/tox.ini @@ -58,14 +58,9 @@ commands = [testenv:pep8] deps = - hacking>=3.1.0,<4.0.0 # Apache-2.0 - flake8-import-order>=0.17.1 # LGPLv3 - pycodestyle>=2.0.0,<2.7.0 # MIT - Pygments>=2.2.0 # BSD - doc8>=0.8.0 # Apache 2.0 + pre-commit commands = - flake8 {posargs} - doc8 doc/source README.rst + pre-commit run --all-files --show-diff-on-failure [testenv:venv] deps = From bcf99f3433ceecf9a210d0aa0580a67645ccf7ee Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 3 May 2023 11:18:47 +0100 Subject: [PATCH 3237/3836] Blackify openstack.image Black used with the '-l 79 -S' flags. A future change will ignore this commit in git-blame history by adding a 'git-blame-ignore-revs' file. Change-Id: Ibd4a585a225f60b26ed394da118c52a29091710d Signed-off-by: Stephen Finucane --- openstack/image/_download.py | 18 +- openstack/image/image_signer.py | 2 +- openstack/image/v1/image.py | 14 +- openstack/image/v2/_proxy.py | 2 +- openstack/image/v2/cache.py | 7 +- openstack/image/v2/image.py | 58 ++++-- .../tests/functional/image/v2/test_image.py | 1 - .../image/v2/test_metadef_namespace.py | 3 +- .../image/v2/test_metadef_resource_type.py | 24 +-- .../image/v2/test_metadef_schema.py | 1 - .../tests/functional/image/v2/test_schema.py | 1 - .../tests/functional/image/v2/test_task.py | 1 - openstack/tests/unit/image/v1/test_image.py | 1 - openstack/tests/unit/image/v2/test_cache.py | 21 +- openstack/tests/unit/image/v2/test_image.py | 185 +++++++++++------- .../unit/image/v2/test_metadef_namespace.py | 2 +- .../image/v2/test_metadef_resource_type.py | 5 +- .../test_metadef_resource_type_association.py | 5 +- .../unit/image/v2/test_metadef_schema.py | 89 +++------ openstack/tests/unit/image/v2/test_proxy.py | 20 +- openstack/tests/unit/image/v2/test_schema.py | 37 ++-- .../tests/unit/image/v2/test_service_info.py | 7 +- openstack/tests/unit/image/v2/test_task.py | 28 +-- 23 files changed, 276 insertions(+), 256 deletions(-) diff --git a/openstack/image/_download.py b/openstack/image/_download.py index a55cde66c..cf0e687f0 100644 --- a/openstack/image/_download.py +++ b/openstack/image/_download.py @@ -20,11 +20,11 @@ def _verify_checksum(md5, checksum): digest = md5.hexdigest() if digest != checksum: raise exceptions.InvalidResponse( - "checksum mismatch: %s != %s" % (checksum, digest)) + "checksum mismatch: %s != %s" % (checksum, digest) + ) class DownloadMixin: - def download(self, session, stream=False, output=None, chunk_size=1024): """Download the data contained in an image""" # TODO(briancurtin): This method should probably offload the get @@ -53,8 +53,7 @@ def download(self, session, stream=False, output=None, chunk_size=1024): md5.update(chunk) else: with open(output, 'wb') as fd: - for chunk in resp.iter_content( - chunk_size=chunk_size): + for chunk in resp.iter_content(chunk_size=chunk_size): fd.write(chunk) md5.update(chunk) _verify_checksum(md5, checksum) @@ -62,7 +61,8 @@ def download(self, session, stream=False, output=None, chunk_size=1024): return resp except Exception as e: raise exceptions.SDKException( - "Unable to download image: %s" % e) + "Unable to download image: %s" % e + ) # if we are returning the repsonse object, ensure that it # has the content-md5 header so that the caller doesn't # need to jump through the same hoops through which we @@ -72,10 +72,12 @@ def download(self, session, stream=False, output=None, chunk_size=1024): return resp if checksum is not None: - _verify_checksum(utils.md5(resp.content, usedforsecurity=False), - checksum) + _verify_checksum( + utils.md5(resp.content, usedforsecurity=False), checksum + ) else: session.log.warning( - "Unable to verify the integrity of image %s", (self.id)) + "Unable to verify the integrity of image %s", (self.id) + ) return resp diff --git a/openstack/image/image_signer.py b/openstack/image/image_signer.py index c9ef993c3..4c88e4c3a 100644 --- a/openstack/image/image_signer.py +++ b/openstack/image/image_signer.py @@ -37,7 +37,7 @@ def __init__(self, hash_method='SHA-256', padding_method='RSA-PSS'): padding_types = { 'RSA-PSS': padding.PSS( mgf=padding.MGF1(HASH_METHODS[hash_method]), - salt_length=padding.PSS.MAX_LENGTH + salt_length=padding.PSS.MAX_LENGTH, ) } # informational attributes diff --git a/openstack/image/v1/image.py b/openstack/image/v1/image.py index 20d66ad8b..c53941224 100644 --- a/openstack/image/v1/image.py +++ b/openstack/image/v1/image.py @@ -32,8 +32,12 @@ class Image(resource.Resource, _download.DownloadMixin): _store_unknown_attrs_as_properties = True _query_mapping = resource.QueryParameters( - 'name', 'container_format', 'disk_format', - 'status', 'size_min', 'size_max' + 'name', + 'container_format', + 'disk_format', + 'status', + 'size_min', + 'size_max', ) #: Hash of the image data used. The Image service uses this value @@ -114,7 +118,8 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): match = cls.existing( id=name_or_id, connection=session._get_connection(), - **params) + **params, + ) return match.fetch(session, **params) except exceptions.NotFoundException: pass @@ -130,4 +135,5 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): if ignore_missing: return None raise exceptions.ResourceNotFound( - "No %s found for %s" % (cls.__name__, name_or_id)) + "No %s found for %s" % (cls.__name__, name_or_id) + ) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 6794eac3f..54927a50e 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -97,7 +97,7 @@ def queue_image(self, image_id): return cache.queue(self, image_id) def clear_cache(self, target): - """ Clear all images from cache, queue or both + """Clear all images from cache, queue or both :param target: Specify which target you want to clear One of: ``both``(default), ``cache``, ``queue``. diff --git a/openstack/image/v2/cache.py b/openstack/image/v2/cache.py index 09297b73d..54cc0d92e 100644 --- a/openstack/image/v2/cache.py +++ b/openstack/image/v2/cache.py @@ -32,8 +32,11 @@ class Cache(resource.Resource): _max_microversion = '2.14' - cached_images = resource.Body('cached_images', type=list, - list_type=CachedImage) + cached_images = resource.Body( + 'cached_images', + type=list, + list_type=CachedImage, + ) queued_images = resource.Body('queued_images', type=list) def queue(self, session, image, *, microversion=None): diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index 6b71897bb..d4ed035ec 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -150,7 +150,9 @@ class Image(resource.Resource, tag.TagMixin, _download.DownloadMixin): #: Optional property allows created servers to have a different bandwidth #: cap than that defined in the network they are attached to. instance_type_rxtx_factor = resource.Body( - "instance_type_rxtx_factor", type=float) + "instance_type_rxtx_factor", + type=float, + ) # For snapshot images, this is the UUID of the server used to #: create this image. instance_uuid = resource.Body('instance_uuid') @@ -221,7 +223,9 @@ class Image(resource.Resource, tag.TagMixin, _download.DownloadMixin): #: number of guest vCPUs. This makes the network performance scale #: across a number of vCPUs. is_hw_vif_multiqueue_enabled = resource.Body( - 'hw_vif_multiqueue_enabled', type=bool) + 'hw_vif_multiqueue_enabled', + type=bool, + ) #: If true, enables the BIOS bootmenu. is_hw_boot_menu_enabled = resource.Body('hw_boot_menu', type=bool) #: The virtual SCSI or IDE controller used by the hypervisor. @@ -247,7 +251,7 @@ class Image(resource.Resource, tag.TagMixin, _download.DownloadMixin): def _action(self, session, action): """Call an action on an image ID.""" url = utils.urljoin(self.base_path, self.id, 'actions', action) - return session.post(url,) + return session.post(url) def deactivate(self, session): """Deactivate an image @@ -275,9 +279,11 @@ def upload(self, session, *, data=None): if data: self.data = data url = utils.urljoin(self.base_path, self.id, 'file') - return session.put(url, data=self.data, - headers={"Content-Type": "application/octet-stream", - "Accept": ""}) + return session.put( + url, + data=self.data, + headers={"Content-Type": "application/octet-stream", "Accept": ""}, + ) def stage(self, session, *, data=None): """Stage binary image data into an existing image @@ -292,9 +298,10 @@ def stage(self, session, *, data=None): url = utils.urljoin(self.base_path, self.id, 'stage') response = session.put( - url, data=self.data, - headers={"Content-Type": "application/octet-stream", - "Accept": ""}) + url, + data=self.data, + headers={"Content-Type": "application/octet-stream", "Accept": ""}, + ) self._translate_response(response, has_body=False) return self @@ -339,8 +346,9 @@ def import_image( if remote_region and remote_image_id: if remote_service_interface: - data['method']['glance_service_interface'] = \ - remote_service_interface + data['method'][ + 'glance_service_interface' + ] = remote_service_interface data['method']['glance_region'] = remote_region data['method']['glance_image_id'] = remote_image_id @@ -367,16 +375,24 @@ def _consume_header_attrs(self, attrs): return super()._consume_header_attrs(attrs) - def _prepare_request(self, requires_id=None, prepend_key=False, - patch=False, base_path=None, **kwargs): - request = super(Image, self)._prepare_request(requires_id=requires_id, - prepend_key=prepend_key, - patch=patch, - base_path=base_path) + def _prepare_request( + self, + requires_id=None, + prepend_key=False, + patch=False, + base_path=None, + **kwargs, + ): + request = super(Image, self)._prepare_request( + requires_id=requires_id, + prepend_key=prepend_key, + patch=patch, + base_path=base_path, + ) if patch: headers = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', - 'Accept': '' + 'Accept': '', } request.headers.update(headers) @@ -385,8 +401,7 @@ def _prepare_request(self, requires_id=None, prepend_key=False, @classmethod def find(cls, session, name_or_id, ignore_missing=True, **params): # Do a regular search first (ignoring missing) - result = super(Image, cls).find(session, name_or_id, True, - **params) + result = super(Image, cls).find(session, name_or_id, True, **params) if result: return result @@ -402,4 +417,5 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): if ignore_missing: return None raise exceptions.ResourceNotFound( - "No %s found for %s" % (cls.__name__, name_or_id)) + "No %s found for %s" % (cls.__name__, name_or_id) + ) diff --git a/openstack/tests/functional/image/v2/test_image.py b/openstack/tests/functional/image/v2/test_image.py index 359428f48..5c46448a1 100644 --- a/openstack/tests/functional/image/v2/test_image.py +++ b/openstack/tests/functional/image/v2/test_image.py @@ -19,7 +19,6 @@ class TestImage(base.BaseImageTest): - def setUp(self): super().setUp() diff --git a/openstack/tests/functional/image/v2/test_metadef_namespace.py b/openstack/tests/functional/image/v2/test_metadef_namespace.py index 02273cdb8..90c2d7b20 100644 --- a/openstack/tests/functional/image/v2/test_metadef_namespace.py +++ b/openstack/tests/functional/image/v2/test_metadef_namespace.py @@ -45,7 +45,8 @@ def test_metadef_namespace(self): self.metadef_namespace.namespace ) self.assertEqual( - self.metadef_namespace.namespace, metadef_namespace.namespace, + self.metadef_namespace.namespace, + metadef_namespace.namespace, ) # (no find_metadef_namespace method) diff --git a/openstack/tests/functional/image/v2/test_metadef_resource_type.py b/openstack/tests/functional/image/v2/test_metadef_resource_type.py index 520f38911..7663f6950 100644 --- a/openstack/tests/functional/image/v2/test_metadef_resource_type.py +++ b/openstack/tests/functional/image/v2/test_metadef_resource_type.py @@ -16,7 +16,6 @@ class TestMetadefResourceType(base.BaseImageTest): - def setUp(self): super().setUp() @@ -33,14 +32,14 @@ def setUp(self): resource_type_name = 'test-resource-type' resource_type = {'name': resource_type_name} - self.metadef_resource_type = \ + self.metadef_resource_type = ( self.conn.image.create_metadef_resource_type_association( - metadef_namespace=namespace, - **resource_type + metadef_namespace=namespace, **resource_type ) + ) self.assertIsInstance( self.metadef_resource_type, - _metadef_resource_type.MetadefResourceTypeAssociation + _metadef_resource_type.MetadefResourceTypeAssociation, ) self.assertEqual(resource_type_name, self.metadef_resource_type.name) @@ -54,12 +53,14 @@ def tearDown(self): def test_metadef_resource_types(self): # list resource type associations - associations = list(self.conn.image.metadef_resource_type_associations( - metadef_namespace=self.metadef_namespace)) + associations = list( + self.conn.image.metadef_resource_type_associations( + metadef_namespace=self.metadef_namespace + ) + ) self.assertIn( - self.metadef_resource_type.name, - {a.name for a in associations} + self.metadef_resource_type.name, {a.name for a in associations} ) # (no find_metadef_resource_type_association method) @@ -68,12 +69,11 @@ def test_metadef_resource_types(self): resource_types = list(self.conn.image.metadef_resource_types()) self.assertIn( - self.metadef_resource_type.name, - {t.name for t in resource_types} + self.metadef_resource_type.name, {t.name for t in resource_types} ) # delete self.conn.image.delete_metadef_resource_type_association( self.metadef_resource_type, - metadef_namespace=self.metadef_namespace + metadef_namespace=self.metadef_namespace, ) diff --git a/openstack/tests/functional/image/v2/test_metadef_schema.py b/openstack/tests/functional/image/v2/test_metadef_schema.py index ae5fe3384..6ef26fbe1 100644 --- a/openstack/tests/functional/image/v2/test_metadef_schema.py +++ b/openstack/tests/functional/image/v2/test_metadef_schema.py @@ -15,7 +15,6 @@ class TestMetadefSchema(base.BaseImageTest): - def test_get_metadef_namespace_schema(self): metadef_schema = self.conn.image.get_metadef_namespace_schema() self.assertIsNotNone(metadef_schema) diff --git a/openstack/tests/functional/image/v2/test_schema.py b/openstack/tests/functional/image/v2/test_schema.py index f9b3186ca..6b16894b0 100644 --- a/openstack/tests/functional/image/v2/test_schema.py +++ b/openstack/tests/functional/image/v2/test_schema.py @@ -15,7 +15,6 @@ class TestSchema(base.BaseImageTest): - def test_get_images_schema(self): schema = self.conn.image.get_images_schema() self.assertIsNotNone(schema) diff --git a/openstack/tests/functional/image/v2/test_task.py b/openstack/tests/functional/image/v2/test_task.py index fb9b2c775..19fc52736 100644 --- a/openstack/tests/functional/image/v2/test_task.py +++ b/openstack/tests/functional/image/v2/test_task.py @@ -14,7 +14,6 @@ class TestTask(base.BaseImageTest): - def test_tasks(self): tasks = list(self.conn.image.tasks()) # NOTE(stephenfin): Yes, this is a dumb test. Basically all that we're diff --git a/openstack/tests/unit/image/v1/test_image.py b/openstack/tests/unit/image/v1/test_image.py index 9809b91a6..75df92d7a 100644 --- a/openstack/tests/unit/image/v1/test_image.py +++ b/openstack/tests/unit/image/v1/test_image.py @@ -37,7 +37,6 @@ class TestImage(base.TestCase): - def test_basic(self): sot = image.Image() self.assertEqual('image', sot.resource_key) diff --git a/openstack/tests/unit/image/v2/test_cache.py b/openstack/tests/unit/image/v2/test_cache.py index 73e3359a8..81415c948 100644 --- a/openstack/tests/unit/image/v2/test_cache.py +++ b/openstack/tests/unit/image/v2/test_cache.py @@ -19,16 +19,18 @@ EXAMPLE = { 'cached_images': [ - {'hits': 0, - 'image_id': '1a56983c-f71f-490b-a7ac-6b321a18935a', - 'last_accessed': 1671699579.444378, - 'last_modified': 1671699579.444378, - 'size': 0}, + { + 'hits': 0, + 'image_id': '1a56983c-f71f-490b-a7ac-6b321a18935a', + 'last_accessed': 1671699579.444378, + 'last_modified': 1671699579.444378, + 'size': 0, + }, ], 'queued_images': [ '3a4560a1-e585-443e-9b39-553b46ec92d1', - '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810' - ] + '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', + ], } @@ -57,8 +59,9 @@ def test_queue(self): sot.queue(sess, image='image_id') - sess.put.assert_called_with('cache/image_id', - microversion=sess.default_microversion) + sess.put.assert_called_with( + 'cache/image_id', microversion=sess.default_microversion + ) @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) def test_clear(self): diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 4596a4821..5f9e34066 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -35,7 +35,10 @@ 'min_disk': 5, 'name': '6', 'owner': '7', - 'properties': {'a': 'z', 'b': 'y', }, + 'properties': { + 'a': 'z', + 'b': 'y', + }, 'protected': False, 'status': '8', 'tags': ['g', 'h', 'i'], @@ -114,7 +117,6 @@ def json(self): class TestImage(base.TestCase): - def setUp(self): super(TestImage, self).setUp() self.resp = mock.Mock() @@ -159,7 +161,7 @@ def test_basic(self): 'status': 'status', 'tag': 'tag', 'updated_at': 'updated_at', - 'visibility': 'visibility' + 'visibility': 'visibility', }, sot._query_mapping._mapping, ) @@ -194,8 +196,9 @@ def test_make_it(self): self.assertEqual(EXAMPLE['metadata'], sot.metadata) self.assertEqual(EXAMPLE['architecture'], sot.architecture) self.assertEqual(EXAMPLE['hypervisor_type'], sot.hypervisor_type) - self.assertEqual(EXAMPLE['instance_type_rxtx_factor'], - sot.instance_type_rxtx_factor) + self.assertEqual( + EXAMPLE['instance_type_rxtx_factor'], sot.instance_type_rxtx_factor + ) self.assertEqual(EXAMPLE['instance_uuid'], sot.instance_uuid) self.assertEqual(EXAMPLE['img_config_drive'], sot.needs_config_drive) self.assertEqual(EXAMPLE['kernel_id'], sot.kernel_id) @@ -211,23 +214,27 @@ def test_make_it(self): self.assertEqual(EXAMPLE['hw_rng_model'], sot.hw_rng_model) self.assertEqual(EXAMPLE['hw_machine_type'], sot.hw_machine_type) self.assertEqual(EXAMPLE['hw_scsi_model'], sot.hw_scsi_model) - self.assertEqual(EXAMPLE['hw_serial_port_count'], - sot.hw_serial_port_count) + self.assertEqual( + EXAMPLE['hw_serial_port_count'], sot.hw_serial_port_count + ) self.assertEqual(EXAMPLE['hw_video_model'], sot.hw_video_model) self.assertEqual(EXAMPLE['hw_video_ram'], sot.hw_video_ram) self.assertEqual(EXAMPLE['hw_watchdog_action'], sot.hw_watchdog_action) self.assertEqual(EXAMPLE['os_command_line'], sot.os_command_line) self.assertEqual(EXAMPLE['hw_vif_model'], sot.hw_vif_model) - self.assertEqual(EXAMPLE['hw_vif_multiqueue_enabled'], - sot.is_hw_vif_multiqueue_enabled) + self.assertEqual( + EXAMPLE['hw_vif_multiqueue_enabled'], + sot.is_hw_vif_multiqueue_enabled, + ) self.assertEqual(EXAMPLE['hw_boot_menu'], sot.is_hw_boot_menu_enabled) self.assertEqual(EXAMPLE['vmware_adaptertype'], sot.vmware_adaptertype) self.assertEqual(EXAMPLE['vmware_ostype'], sot.vmware_ostype) self.assertEqual(EXAMPLE['auto_disk_config'], sot.has_auto_disk_config) self.assertEqual(EXAMPLE['os_type'], sot.os_type) self.assertEqual(EXAMPLE['os_admin_user'], sot.os_admin_user) - self.assertEqual(EXAMPLE['hw_qemu_guest_agent'], - sot.hw_qemu_guest_agent) + self.assertEqual( + EXAMPLE['hw_qemu_guest_agent'], sot.hw_qemu_guest_agent + ) self.assertEqual(EXAMPLE['os_require_quiesce'], sot.os_require_quiesce) def test_deactivate(self): @@ -267,9 +274,7 @@ def test_import_image(self): json = {"method": {"name": "web-download", "uri": "such-a-good-uri"}} sot.import_image(self.sess, "web-download", uri="such-a-good-uri") self.sess.post.assert_called_with( - 'images/IDENTIFIER/import', - headers={}, - json=json + 'images/IDENTIFIER/import', headers={}, json=json ) def test_import_image_with_uri_not_web_download(self): @@ -279,7 +284,7 @@ def test_import_image_with_uri_not_web_download(self): self.sess.post.assert_called_with( 'images/IDENTIFIER/import', headers={}, - json={"method": {"name": "glance-direct"}} + json={"method": {"name": "glance-direct"}}, ) def test_import_image_with_store(self): @@ -302,7 +307,7 @@ def test_import_image_with_store(self): self.sess.post.assert_called_with( 'images/IDENTIFIER/import', headers={'X-Image-Meta-Store': 'ceph_1'}, - json=json + json=json, ) def test_import_image_with_stores(self): @@ -353,21 +358,21 @@ def test_upload(self): sot = image.Image(**EXAMPLE) self.assertIsNotNone(sot.upload(self.sess)) - self.sess.put.assert_called_with('images/IDENTIFIER/file', - data=sot.data, - headers={"Content-Type": - "application/octet-stream", - "Accept": ""}) + self.sess.put.assert_called_with( + 'images/IDENTIFIER/file', + data=sot.data, + headers={"Content-Type": "application/octet-stream", "Accept": ""}, + ) def test_stage(self): sot = image.Image(**EXAMPLE) self.assertIsNotNone(sot.stage(self.sess)) - self.sess.put.assert_called_with('images/IDENTIFIER/stage', - data=sot.data, - headers={"Content-Type": - "application/octet-stream", - "Accept": ""}) + self.sess.put.assert_called_with( + 'images/IDENTIFIER/stage', + data=sot.data, + headers={"Content-Type": "application/octet-stream", "Accept": ""}, + ) def test_stage_error(self): sot = image.Image(**EXAMPLE) @@ -380,13 +385,17 @@ def test_download_checksum_match(self): resp = FakeResponse( b"abc", - headers={"Content-MD5": "900150983cd24fb0d6963f7d28e17f72", - "Content-Type": "application/octet-stream"}) + headers={ + "Content-MD5": "900150983cd24fb0d6963f7d28e17f72", + "Content-Type": "application/octet-stream", + }, + ) self.sess.get.return_value = resp rv = sot.download(self.sess) - self.sess.get.assert_called_with('images/IDENTIFIER/file', - stream=False) + self.sess.get.assert_called_with( + 'images/IDENTIFIER/file', stream=False + ) self.assertEqual(rv, resp) @@ -395,8 +404,11 @@ def test_download_checksum_mismatch(self): resp = FakeResponse( b"abc", - headers={"Content-MD5": "the wrong checksum", - "Content-Type": "application/octet-stream"}) + headers={ + "Content-MD5": "the wrong checksum", + "Content-Type": "application/octet-stream", + }, + ) self.sess.get.return_value = resp self.assertRaises(exceptions.InvalidResponse, sot.download, self.sess) @@ -405,19 +417,25 @@ def test_download_no_checksum_header(self): sot = image.Image(**EXAMPLE) resp1 = FakeResponse( - b"abc", headers={"Content-Type": "application/octet-stream"}) + b"abc", headers={"Content-Type": "application/octet-stream"} + ) - resp2 = FakeResponse( - {"checksum": "900150983cd24fb0d6963f7d28e17f72"}) + resp2 = FakeResponse({"checksum": "900150983cd24fb0d6963f7d28e17f72"}) self.sess.get.side_effect = [resp1, resp2] rv = sot.download(self.sess) self.sess.get.assert_has_calls( - [mock.call('images/IDENTIFIER/file', - stream=False), - mock.call('images/IDENTIFIER', microversion=None, params={}, - skip_cache=False)]) + [ + mock.call('images/IDENTIFIER/file', stream=False), + mock.call( + 'images/IDENTIFIER', + microversion=None, + params={}, + skip_cache=False, + ), + ] + ) self.assertEqual(rv, resp1) @@ -425,7 +443,8 @@ def test_download_no_checksum_at_all2(self): sot = image.Image(**EXAMPLE) resp1 = FakeResponse( - b"abc", headers={"Content-Type": "application/octet-stream"}) + b"abc", headers={"Content-Type": "application/octet-stream"} + ) resp2 = FakeResponse({"checksum": None}) @@ -434,20 +453,26 @@ def test_download_no_checksum_at_all2(self): with self.assertLogs(logger='openstack', level="WARNING") as log: rv = sot.download(self.sess) - self.assertEqual(len(log.records), 1, - "Too many warnings were logged") self.assertEqual( - "Unable to verify the integrity of image %s", - log.records[0].msg) + len(log.records), 1, "Too many warnings were logged" + ) self.assertEqual( - (sot.id,), - log.records[0].args) + "Unable to verify the integrity of image %s", + log.records[0].msg, + ) + self.assertEqual((sot.id,), log.records[0].args) self.sess.get.assert_has_calls( - [mock.call('images/IDENTIFIER/file', - stream=False), - mock.call('images/IDENTIFIER', microversion=None, params={}, - skip_cache=False)]) + [ + mock.call('images/IDENTIFIER/file', stream=False), + mock.call( + 'images/IDENTIFIER', + microversion=None, + params={}, + skip_cache=False, + ), + ] + ) self.assertEqual(rv, resp1) @@ -456,8 +481,11 @@ def test_download_stream(self): resp = FakeResponse( b"abc", - headers={"Content-MD5": "900150983cd24fb0d6963f7d28e17f72", - "Content-Type": "application/octet-stream"}) + headers={ + "Content-MD5": "900150983cd24fb0d6963f7d28e17f72", + "Content-Type": "application/octet-stream", + }, + ) self.sess.get.return_value = resp rv = sot.download(self.sess, stream=True) @@ -472,8 +500,9 @@ def test_image_download_output_fd(self): response.status_code = 200 response.iter_content.return_value = [b'01', b'02'] response.headers = { - 'Content-MD5': - calculate_md5_checksum(response.iter_content.return_value) + 'Content-MD5': calculate_md5_checksum( + response.iter_content.return_value + ) } self.sess.get = mock.Mock(return_value=response) sot.download(self.sess, output=output_file) @@ -486,8 +515,9 @@ def test_image_download_output_file(self): response.status_code = 200 response.iter_content.return_value = [b'01', b'02'] response.headers = { - 'Content-MD5': - calculate_md5_checksum(response.iter_content.return_value) + 'Content-MD5': calculate_md5_checksum( + response.iter_content.return_value + ) } self.sess.get = mock.Mock(return_value=response) @@ -513,9 +543,10 @@ def test_image_update(self): resp.status_code = 200 self.sess.patch.return_value = resp - value = [{"value": "fake_name", "op": "replace", "path": "/name"}, - {"value": "fake_value", "op": "add", - "path": "/instance_uuid"}] + value = [ + {"value": "fake_name", "op": "replace", "path": "/name"}, + {"value": "fake_value", "op": "add", "path": "/instance_uuid"}, + ] sot.name = 'fake_name' sot.instance_uuid = 'fake_value' @@ -527,7 +558,8 @@ def test_image_update(self): self.assertEqual(url, call_args[0]) self.assertEqual( sorted(value, key=operator.itemgetter('value')), - sorted(call_kwargs['json'], key=operator.itemgetter('value'))) + sorted(call_kwargs['json'], key=operator.itemgetter('value')), + ) def test_image_find(self): sot = image.Image() @@ -539,20 +571,33 @@ def test_image_find(self): # Then list with no results FakeResponse({'images': []}), # And finally new list of hidden images with one searched - FakeResponse({'images': [EXAMPLE]}) - + FakeResponse({'images': [EXAMPLE]}), ] result = sot.find(self.sess, EXAMPLE['name']) - self.sess.get.assert_has_calls([ - mock.call('images/' + EXAMPLE['name'], microversion=None, - params={}, skip_cache=False), - mock.call('/images', headers={'Accept': 'application/json'}, - microversion=None, params={'name': EXAMPLE['name']}), - mock.call('/images', headers={'Accept': 'application/json'}, - microversion=None, params={'os_hidden': True}) - ]) + self.sess.get.assert_has_calls( + [ + mock.call( + 'images/' + EXAMPLE['name'], + microversion=None, + params={}, + skip_cache=False, + ), + mock.call( + '/images', + headers={'Accept': 'application/json'}, + microversion=None, + params={'name': EXAMPLE['name']}, + ), + mock.call( + '/images', + headers={'Accept': 'application/json'}, + microversion=None, + params={'os_hidden': True}, + ), + ] + ) self.assertIsInstance(result, image.Image) self.assertEqual(IDENTIFIER, result.id) diff --git a/openstack/tests/unit/image/v2/test_metadef_namespace.py b/openstack/tests/unit/image/v2/test_metadef_namespace.py index 8fdd03fd8..b55e99c34 100644 --- a/openstack/tests/unit/image/v2/test_metadef_namespace.py +++ b/openstack/tests/unit/image/v2/test_metadef_namespace.py @@ -67,7 +67,7 @@ def test_make_it(self): 'resource_types': 'resource_types', 'sort_dir': 'sort_dir', 'sort_key': 'sort_key', - 'visibility': 'visibility' + 'visibility': 'visibility', }, sot._query_mapping._mapping, ) diff --git a/openstack/tests/unit/image/v2/test_metadef_resource_type.py b/openstack/tests/unit/image/v2/test_metadef_resource_type.py index 571828458..50bf40b80 100644 --- a/openstack/tests/unit/image/v2/test_metadef_resource_type.py +++ b/openstack/tests/unit/image/v2/test_metadef_resource_type.py @@ -14,10 +14,7 @@ from openstack.tests.unit import base -EXAMPLE = { - "name": "OS::Nova::Aggregate", - "created_at": "2022-07-09T04:10:37Z" -} +EXAMPLE = {"name": "OS::Nova::Aggregate", "created_at": "2022-07-09T04:10:37Z"} class TestMetadefResourceType(base.TestCase): diff --git a/openstack/tests/unit/image/v2/test_metadef_resource_type_association.py b/openstack/tests/unit/image/v2/test_metadef_resource_type_association.py index fcd430a51..e9d34923c 100644 --- a/openstack/tests/unit/image/v2/test_metadef_resource_type_association.py +++ b/openstack/tests/unit/image/v2/test_metadef_resource_type_association.py @@ -18,7 +18,7 @@ "name": "OS::Cinder::Volume", "prefix": "CIM_PASD_", "properties_target": "image", - "created_at": "2022-07-09T04:10:38Z" + "created_at": "2022-07-09T04:10:38Z", } @@ -29,7 +29,8 @@ def test_basic(self): self.assertEqual('resource_type_associations', sot.resources_key) self.assertEqual( '/metadefs/namespaces/%(namespace_name)s/resource_types', - sot.base_path) + sot.base_path, + ) self.assertTrue(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_commit) diff --git a/openstack/tests/unit/image/v2/test_metadef_schema.py b/openstack/tests/unit/image/v2/test_metadef_schema.py index e26d89823..7b56c9619 100644 --- a/openstack/tests/unit/image/v2/test_metadef_schema.py +++ b/openstack/tests/unit/image/v2/test_metadef_schema.py @@ -21,101 +21,67 @@ 'namespace': { 'type': 'string', 'description': 'The unique namespace text.', - 'maxLength': 80 + 'maxLength': 80, }, 'visibility': { 'type': 'string', 'description': 'Scope of namespace accessibility.', - 'enum': [ - 'public', - 'private' - ] + 'enum': ['public', 'private'], }, 'created_at': { 'type': 'string', 'readOnly': True, 'description': 'Date and time of namespace creation', - 'format': 'date-time' + 'format': 'date-time', }, 'resource_type_associations': { 'type': 'array', 'items': { 'type': 'object', 'properties': { - 'name': { - 'type': 'string' - }, - 'prefix': { - 'type': 'string' - }, - 'properties_target': { - 'type': 'string' - } - } - } - }, - 'properties': { - '$ref': '#/definitions/property' + 'name': {'type': 'string'}, + 'prefix': {'type': 'string'}, + 'properties_target': {'type': 'string'}, + }, + }, }, + 'properties': {'$ref': '#/definitions/property'}, 'objects': { 'type': 'array', 'items': { 'type': 'object', 'properties': { - 'name': { - 'type': 'string' - }, - 'description': { - 'type': 'string' - }, - 'required': { - '$ref': '#/definitions/stringArray' - }, - 'properties': { - '$ref': '#/definitions/property' - } - } - } + 'name': {'type': 'string'}, + 'description': {'type': 'string'}, + 'required': {'$ref': '#/definitions/stringArray'}, + 'properties': {'$ref': '#/definitions/property'}, + }, + }, }, 'tags': { 'type': 'array', 'items': { 'type': 'object', - 'properties': { - 'name': { - 'type': 'string' - } - } - } - } + 'properties': {'name': {'type': 'string'}}, + }, + }, }, 'additionalProperties': False, 'definitions': { - 'positiveInteger': { - 'type': 'integer', - 'minimum': 0 - }, + 'positiveInteger': {'type': 'integer', 'minimum': 0}, 'positiveIntegerDefault0': { 'allOf': [ - { - '$ref': '#/definitions/positiveInteger' - }, - { - 'default': 0 - } + {'$ref': '#/definitions/positiveInteger'}, + {'default': 0}, ] }, 'stringArray': { 'type': 'array', - 'items': { - 'type': 'string' - }, - 'uniqueItems': True - } + 'items': {'type': 'string'}, + 'uniqueItems': True, + }, }, - 'required': [ - 'namespace' - ] + 'required': ['namespace'], } @@ -135,7 +101,8 @@ def test_make_it(self): sot = metadef_schema.MetadefSchema(**EXAMPLE) self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['properties'], sot.properties) - self.assertEqual(EXAMPLE['additionalProperties'], - sot.additional_properties) + self.assertEqual( + EXAMPLE['additionalProperties'], sot.additional_properties + ) self.assertEqual(EXAMPLE['definitions'], sot.definitions) self.assertEqual(EXAMPLE['required'], sot.required) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index cad1fae0e..9752eb7f9 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -571,7 +571,7 @@ class TestMetadefResourceType(TestImageProxy): def test_metadef_resource_types(self): self.verify_list( self.proxy.metadef_resource_types, - _metadef_resource_type.MetadefResourceType + _metadef_resource_type.MetadefResourceType, ) @@ -581,7 +581,7 @@ def test_create_metadef_resource_type_association(self): self.proxy.create_metadef_resource_type_association, _metadef_resource_type.MetadefResourceTypeAssociation, method_kwargs={'metadef_namespace': 'namespace_name'}, - expected_kwargs={'namespace_name': 'namespace_name'} + expected_kwargs={'namespace_name': 'namespace_name'}, ) def test_delete_metadef_resource_type_association(self): @@ -590,7 +590,7 @@ def test_delete_metadef_resource_type_association(self): _metadef_resource_type.MetadefResourceTypeAssociation, False, method_kwargs={'metadef_namespace': 'namespace_name'}, - expected_kwargs={'namespace_name': 'namespace_name'} + expected_kwargs={'namespace_name': 'namespace_name'}, ) def test_delete_metadef_resource_type_association_ignore(self): @@ -599,7 +599,7 @@ def test_delete_metadef_resource_type_association_ignore(self): _metadef_resource_type.MetadefResourceTypeAssociation, True, method_kwargs={'metadef_namespace': 'namespace_name'}, - expected_kwargs={'namespace_name': 'namespace_name'} + expected_kwargs={'namespace_name': 'namespace_name'}, ) def test_metadef_resource_type_associations(self): @@ -607,7 +607,7 @@ def test_metadef_resource_type_associations(self): self.proxy.metadef_resource_type_associations, _metadef_resource_type.MetadefResourceTypeAssociation, method_kwargs={'metadef_namespace': 'namespace_name'}, - expected_kwargs={'namespace_name': 'namespace_name'} + expected_kwargs={'namespace_name': 'namespace_name'}, ) @@ -892,9 +892,7 @@ def test_image_cache_get(self): "openstack.proxy.Proxy._get", self.proxy.get_image_cache, expected_args=[_cache.Cache], - expected_kwargs={ - 'requires_id': False - }, + expected_kwargs={'requires_id': False}, ) def test_cache_image_delete(self): @@ -911,7 +909,8 @@ def test_image_queue(self, mock_get_resource): "openstack.image.v2.cache.Cache.queue", self.proxy.queue_image, method_args=['image-id'], - expected_args=[self.proxy, 'image-id']) + expected_args=[self.proxy, 'image-id'], + ) mock_get_resource.assert_called_once_with(_cache.Cache, None) @mock.patch.object(proxy_base.Proxy, '_get_resource') @@ -922,5 +921,6 @@ def test_image_clear_cache(self, mock_get_resource): "openstack.image.v2.cache.Cache.clear", self.proxy.clear_cache, method_args=['both'], - expected_args=[self.proxy, 'both']) + expected_args=[self.proxy, 'both'], + ) mock_get_resource.assert_called_once_with(_cache.Cache, None) diff --git a/openstack/tests/unit/image/v2/test_schema.py b/openstack/tests/unit/image/v2/test_schema.py index ed5cddb15..56807f955 100644 --- a/openstack/tests/unit/image/v2/test_schema.py +++ b/openstack/tests/unit/image/v2/test_schema.py @@ -16,39 +16,25 @@ IDENTIFIER = 'IDENTIFIER' EXAMPLE = { - 'additionalProperties': { - 'type': 'string' - }, + 'additionalProperties': {'type': 'string'}, 'links': [ - { - 'href': '{self}', - 'rel': 'self' - }, - { - 'href': '{file}', - 'rel': 'enclosure' - }, - { - 'href': '{schema}', - 'rel': 'describedby' - } + {'href': '{self}', 'rel': 'self'}, + {'href': '{file}', 'rel': 'enclosure'}, + {'href': '{schema}', 'rel': 'describedby'}, ], 'name': 'image', 'properties': { 'architecture': { 'description': 'Operating system architecture', 'is_base': False, - 'type': 'string' + 'type': 'string', }, 'visibility': { 'description': 'Scope of image accessibility', - 'enum': [ - 'public', - 'private' - ], - 'type': 'string' - } - } + 'enum': ['public', 'private'], + 'type': 'string', + }, + }, } @@ -68,5 +54,6 @@ def test_make_it(self): sot = schema.Schema(**EXAMPLE) self.assertEqual(EXAMPLE['properties'], sot.properties) self.assertEqual(EXAMPLE['name'], sot.name) - self.assertEqual(EXAMPLE['additionalProperties'], - sot.additional_properties) + self.assertEqual( + EXAMPLE['additionalProperties'], sot.additional_properties + ) diff --git a/openstack/tests/unit/image/v2/test_service_info.py b/openstack/tests/unit/image/v2/test_service_info.py index 8e3ad4a51..92c53c3f3 100644 --- a/openstack/tests/unit/image/v2/test_service_info.py +++ b/openstack/tests/unit/image/v2/test_service_info.py @@ -22,16 +22,13 @@ 'import-methods': { 'description': 'Import methods available.', 'type': 'array', - 'value': [ - 'glance-direct', - 'web-download' - ] + 'value': ['glance-direct', 'web-download'], } } EXAMPLE_STORE = { 'id': IDENTIFIER, 'description': 'Fast access to rbd store', - 'default': True + 'default': True, } diff --git a/openstack/tests/unit/image/v2/test_task.py b/openstack/tests/unit/image/v2/test_task.py index 6b1e3f35b..c93e1388e 100644 --- a/openstack/tests/unit/image/v2/test_task.py +++ b/openstack/tests/unit/image/v2/test_task.py @@ -19,12 +19,9 @@ 'created_at': '2016-06-24T14:40:19Z', 'id': IDENTIFIER, 'input': { - 'image_properties': { - 'container_format': 'ovf', - 'disk_format': 'vhd' - }, + 'image_properties': {'container_format': 'ovf', 'disk_format': 'vhd'}, 'import_from': 'http://example.com', - 'import_from_format': 'qcow2' + 'import_from_format': 'qcow2', }, 'message': 'message', 'owner': 'fa6c8c1600f4444281658a23ee6da8e8', @@ -32,7 +29,7 @@ 'schema': '/v2/schemas/task', 'status': 'processing', 'type': 'import', - 'updated_at': '2016-06-24T14:40:20Z' + 'updated_at': '2016-06-24T14:40:20Z', } @@ -48,14 +45,17 @@ def test_basic(self): self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertDictEqual({'limit': 'limit', - 'marker': 'marker', - 'sort_dir': 'sort_dir', - 'sort_key': 'sort_key', - 'status': 'status', - 'type': 'type', - }, - sot._query_mapping._mapping) + self.assertDictEqual( + { + 'limit': 'limit', + 'marker': 'marker', + 'sort_dir': 'sort_dir', + 'sort_key': 'sort_key', + 'status': 'status', + 'type': 'type', + }, + sot._query_mapping._mapping, + ) def test_make_it(self): sot = task.Task(**EXAMPLE) From f526b990f31de03a1b6181a4724976e1b86a654a Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 3 May 2023 12:07:08 +0100 Subject: [PATCH 3238/3836] Blackify openstack.network We disable the H301 check since it clashes with black. Black used with the '-l 79 -S' flags. A future change will ignore this commit in git-blame history by adding a 'git-blame-ignore-revs' file. Change-Id: Ib987ac513c4f5e527bf4122b784d0a69856b903e Signed-off-by: Stephen Finucane --- openstack/network/v2/_base.py | 21 +- openstack/network/v2/_proxy.py | 1588 +++++++++++------ openstack/network/v2/address_group.py | 9 +- openstack/network/v2/address_scope.py | 4 +- openstack/network/v2/agent.py | 15 +- openstack/network/v2/availability_zone.py | 4 +- openstack/network/v2/bgp_speaker.py | 6 +- openstack/network/v2/firewall_group.py | 12 +- openstack/network/v2/firewall_policy.py | 7 +- openstack/network/v2/firewall_rule.py | 17 +- openstack/network/v2/flavor.py | 19 +- openstack/network/v2/floating_ip.py | 17 +- openstack/network/v2/health_monitor.py | 10 +- openstack/network/v2/listener.py | 12 +- openstack/network/v2/load_balancer.py | 12 +- openstack/network/v2/local_ip.py | 13 +- openstack/network/v2/local_ip_association.py | 6 +- openstack/network/v2/metering_label.py | 4 +- openstack/network/v2/metering_label_rule.py | 22 +- openstack/network/v2/ndp_proxy.py | 12 +- openstack/network/v2/network.py | 15 +- .../network/v2/network_ip_availability.py | 6 +- openstack/network/v2/network_segment_range.py | 13 +- openstack/network/v2/pool.py | 10 +- openstack/network/v2/pool_member.py | 6 +- openstack/network/v2/port.py | 39 +- openstack/network/v2/port_forwarding.py | 4 +- openstack/network/v2/qos_policy.py | 4 +- openstack/network/v2/qos_rule_type.py | 6 +- openstack/network/v2/quota.py | 10 +- openstack/network/v2/rbac_policy.py | 8 +- openstack/network/v2/router.py | 18 +- openstack/network/v2/security_group.py | 12 +- openstack/network/v2/security_group_rule.py | 23 +- openstack/network/v2/segment.py | 8 +- openstack/network/v2/service_profile.py | 4 +- openstack/network/v2/service_provider.py | 5 +- openstack/network/v2/subnet.py | 17 +- openstack/network/v2/subnet_pool.py | 8 +- openstack/network/v2/tap_flow.py | 6 +- openstack/network/v2/tap_service.py | 3 +- openstack/network/v2/trunk.py | 6 +- openstack/network/v2/vpn_endpoint_group.py | 7 +- openstack/network/v2/vpn_ike_policy.py | 11 +- openstack/network/v2/vpn_ipsec_policy.py | 9 +- .../network/v2/vpn_ipsec_site_connection.py | 23 +- openstack/network/v2/vpn_service.py | 12 +- .../network/v2/test_address_group.py | 6 +- .../network/v2/test_address_scope.py | 6 +- .../v2/test_agent_add_remove_network.py | 6 +- .../v2/test_agent_add_remove_router.py | 6 +- .../v2/test_auto_allocated_topology.py | 13 +- .../tests/functional/network/v2/test_bgp.py | 40 +- .../functional/network/v2/test_bgpvpn.py | 102 +- .../functional/network/v2/test_dvr_router.py | 9 +- ...test_firewall_rule_insert_remove_policy.py | 9 +- .../functional/network/v2/test_flavor.py | 7 +- .../functional/network/v2/test_floating_ip.py | 7 +- .../functional/network/v2/test_local_ip.py | 4 +- .../network/v2/test_local_ip_association.py | 5 +- .../v2/test_qos_bandwidth_limit_rule.py | 2 +- .../network/v2/test_qos_dscp_marking_rule.py | 2 +- .../v2/test_qos_minimum_bandwidth_rule.py | 13 +- .../v2/test_qos_minimum_packet_rate_rule.py | 2 +- .../network/v2/test_qos_rule_type.py | 6 +- .../network/v2/test_service_profile.py | 16 +- .../tests/functional/network/v2/test_taas.py | 41 +- .../functional/network/v2/test_vpnaas.py | 4 +- openstack/tests/unit/network/test_version.py | 1 - .../unit/network/v2/test_address_group.py | 23 +- .../unit/network/v2/test_address_scope.py | 1 - openstack/tests/unit/network/v2/test_agent.py | 33 +- .../v2/test_auto_allocated_topology.py | 1 - .../unit/network/v2/test_availability_zone.py | 1 - .../tests/unit/network/v2/test_bgp_peer.py | 13 +- .../tests/unit/network/v2/test_bgp_speaker.py | 22 +- .../tests/unit/network/v2/test_bgpvpn.py | 33 +- .../tests/unit/network/v2/test_extension.py | 1 - .../unit/network/v2/test_firewall_group.py | 12 +- .../unit/network/v2/test_firewall_policy.py | 7 +- .../unit/network/v2/test_firewall_rule.py | 9 +- .../tests/unit/network/v2/test_flavor.py | 32 +- .../tests/unit/network/v2/test_floating_ip.py | 54 +- .../unit/network/v2/test_health_monitor.py | 1 - .../network/v2/test_l3_conntrack_helper.py | 7 +- .../tests/unit/network/v2/test_listener.py | 9 +- .../unit/network/v2/test_load_balancer.py | 6 +- .../tests/unit/network/v2/test_local_ip.py | 29 +- .../network/v2/test_local_ip_association.py | 21 +- .../unit/network/v2/test_metering_label.py | 1 - .../network/v2/test_metering_label_rule.py | 13 +- .../tests/unit/network/v2/test_ndp_proxy.py | 1 - .../tests/unit/network/v2/test_network.py | 79 +- .../v2/test_network_ip_availability.py | 35 +- .../network/v2/test_network_segment_range.py | 11 +- openstack/tests/unit/network/v2/test_pool.py | 11 +- .../tests/unit/network/v2/test_pool_member.py | 1 - openstack/tests/unit/network/v2/test_port.py | 110 +- .../unit/network/v2/test_port_forwarding.py | 28 +- openstack/tests/unit/network/v2/test_proxy.py | 1560 ++++++++++------ .../v2/test_qos_bandwidth_limit_rule.py | 4 +- .../network/v2/test_qos_dscp_marking_rule.py | 6 +- .../v2/test_qos_minimum_bandwidth_rule.py | 4 +- .../v2/test_qos_minimum_packet_rate_rule.py | 4 +- .../tests/unit/network/v2/test_qos_policy.py | 3 +- .../unit/network/v2/test_qos_rule_type.py | 58 +- openstack/tests/unit/network/v2/test_quota.py | 15 +- .../tests/unit/network/v2/test_rbac_policy.py | 4 +- .../tests/unit/network/v2/test_router.py | 60 +- .../unit/network/v2/test_security_group.py | 50 +- .../network/v2/test_security_group_rule.py | 60 +- .../tests/unit/network/v2/test_segment.py | 1 - .../unit/network/v2/test_service_profile.py | 28 +- .../unit/network/v2/test_service_provider.py | 1 - .../tests/unit/network/v2/test_subnet.py | 1 - .../tests/unit/network/v2/test_subnet_pool.py | 6 +- .../tests/unit/network/v2/test_tap_flow.py | 21 +- .../tests/unit/network/v2/test_tap_service.py | 21 +- openstack/tests/unit/network/v2/test_trunk.py | 32 +- .../network/v2/test_vpn_endpoint_group.py | 11 +- .../unit/network/v2/test_vpn_ikepolicy.py | 13 +- .../v2/test_vpn_ipsec_site_connection.py | 3 +- .../unit/network/v2/test_vpn_ipsecpolicy.py | 11 +- .../tests/unit/network/v2/test_vpn_service.py | 4 +- tox.ini | 3 +- 125 files changed, 3130 insertions(+), 1869 deletions(-) diff --git a/openstack/network/v2/_base.py b/openstack/network/v2/_base.py index cee129f0e..e151e8144 100644 --- a/openstack/network/v2/_base.py +++ b/openstack/network/v2/_base.py @@ -18,12 +18,23 @@ class NetworkResource(resource.Resource): _allow_unknown_attrs_in_body = True - def _prepare_request(self, requires_id=None, prepend_key=False, - patch=False, base_path=None, params=None, - if_revision=None, **kwargs): + def _prepare_request( + self, + requires_id=None, + prepend_key=False, + patch=False, + base_path=None, + params=None, + if_revision=None, + **kwargs + ): req = super(NetworkResource, self)._prepare_request( - requires_id=requires_id, prepend_key=prepend_key, patch=patch, - base_path=base_path, params=params) + requires_id=requires_id, + prepend_key=prepend_key, + patch=patch, + base_path=base_path, + params=params, + ) if if_revision is not None: req.headers['If-Match'] = "revision_number=%d" % if_revision return req diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index dd7140abc..c7a3a60bc 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -18,18 +18,22 @@ from openstack.network.v2 import address_group as _address_group from openstack.network.v2 import address_scope as _address_scope from openstack.network.v2 import agent as _agent -from openstack.network.v2 import auto_allocated_topology as \ - _auto_allocated_topology +from openstack.network.v2 import ( + auto_allocated_topology as _auto_allocated_topology, +) from openstack.network.v2 import availability_zone from openstack.network.v2 import bgp_peer as _bgp_peer from openstack.network.v2 import bgp_speaker as _bgp_speaker from openstack.network.v2 import bgpvpn as _bgpvpn -from openstack.network.v2 import bgpvpn_network_association as \ - _bgpvpn_network_association -from openstack.network.v2 import bgpvpn_port_association as \ - _bgpvpn_port_association -from openstack.network.v2 import bgpvpn_router_association as \ - _bgpvpn_router_association +from openstack.network.v2 import ( + bgpvpn_network_association as _bgpvpn_network_association, +) +from openstack.network.v2 import ( + bgpvpn_port_association as _bgpvpn_port_association, +) +from openstack.network.v2 import ( + bgpvpn_router_association as _bgpvpn_router_association, +) from openstack.network.v2 import extension from openstack.network.v2 import firewall_group as _firewall_group from openstack.network.v2 import firewall_policy as _firewall_policy @@ -47,20 +51,25 @@ from openstack.network.v2 import ndp_proxy as _ndp_proxy from openstack.network.v2 import network as _network from openstack.network.v2 import network_ip_availability -from openstack.network.v2 import network_segment_range as \ - _network_segment_range +from openstack.network.v2 import ( + network_segment_range as _network_segment_range, +) from openstack.network.v2 import pool as _pool from openstack.network.v2 import pool_member as _pool_member from openstack.network.v2 import port as _port from openstack.network.v2 import port_forwarding as _port_forwarding -from openstack.network.v2 import qos_bandwidth_limit_rule as \ - _qos_bandwidth_limit_rule -from openstack.network.v2 import qos_dscp_marking_rule as \ - _qos_dscp_marking_rule -from openstack.network.v2 import qos_minimum_bandwidth_rule as \ - _qos_minimum_bandwidth_rule -from openstack.network.v2 import qos_minimum_packet_rate_rule as \ - _qos_minimum_packet_rate_rule +from openstack.network.v2 import ( + qos_bandwidth_limit_rule as _qos_bandwidth_limit_rule, +) +from openstack.network.v2 import ( + qos_dscp_marking_rule as _qos_dscp_marking_rule, +) +from openstack.network.v2 import ( + qos_minimum_bandwidth_rule as _qos_minimum_bandwidth_rule, +) +from openstack.network.v2 import ( + qos_minimum_packet_rate_rule as _qos_minimum_packet_rate_rule, +) from openstack.network.v2 import qos_policy as _qos_policy from openstack.network.v2 import qos_rule_type as _qos_rule_type from openstack.network.v2 import quota as _quota @@ -79,8 +88,9 @@ from openstack.network.v2 import vpn_endpoint_group as _vpn_endpoint_group from openstack.network.v2 import vpn_ike_policy as _ike_policy from openstack.network.v2 import vpn_ipsec_policy as _ipsec_policy -from openstack.network.v2 import vpn_ipsec_site_connection as \ - _ipsec_site_connection +from openstack.network.v2 import ( + vpn_ipsec_site_connection as _ipsec_site_connection, +) from openstack.network.v2 import vpn_service as _vpn_service from openstack import proxy @@ -92,18 +102,22 @@ class Proxy(proxy.Proxy, Generic[T]): "address_group": _address_group.AddressGroup, "address_scope": _address_scope.AddressScope, "agent": _agent.Agent, - "auto_allocated_topology": - _auto_allocated_topology.AutoAllocatedTopology, + "auto_allocated_topology": ( + _auto_allocated_topology.AutoAllocatedTopology + ), "availability_zone": availability_zone.AvailabilityZone, "bgp_peer": _bgp_peer.BgpPeer, "bgp_speaker": _bgp_speaker.BgpSpeaker, "bgpvpn": _bgpvpn.BgpVpn, - "bgpvpn_network_association": - _bgpvpn_network_association.BgpVpnNetworkAssociation, - "bgpvpn_port_association": - _bgpvpn_port_association.BgpVpnPortAssociation, - "bgpvpn_router_association": - _bgpvpn_router_association.BgpVpnRouterAssociation, + "bgpvpn_network_association": ( + _bgpvpn_network_association.BgpVpnNetworkAssociation + ), + "bgpvpn_port_association": ( + _bgpvpn_port_association.BgpVpnPortAssociation + ), + "bgpvpn_router_association": ( + _bgpvpn_router_association.BgpVpnRouterAssociation + ), "extension": extension.Extension, "firewall_group": _firewall_group.FirewallGroup, "firewall_policy": _firewall_policy.FirewallPolicy, @@ -120,20 +134,24 @@ class Proxy(proxy.Proxy, Generic[T]): "metering_label_rule": _metering_label_rule.MeteringLabelRule, "ndp_proxy": _ndp_proxy.NDPProxy, "network": _network.Network, - "network_ip_availability": - network_ip_availability.NetworkIPAvailability, + "network_ip_availability": ( + network_ip_availability.NetworkIPAvailability + ), "network_segment_range": _network_segment_range.NetworkSegmentRange, "pool": _pool.Pool, "pool_member": _pool_member.PoolMember, "port": _port.Port, "port_forwarding": _port_forwarding.PortForwarding, - "qos_bandwidth_limit_rule": - _qos_bandwidth_limit_rule.QoSBandwidthLimitRule, + "qos_bandwidth_limit_rule": ( + _qos_bandwidth_limit_rule.QoSBandwidthLimitRule + ), "qos_dscp_marking_rule": _qos_dscp_marking_rule.QoSDSCPMarkingRule, - "qos_minimum_bandwidth_rule": - _qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, - "qos_minimum_packet_rate_rule": - _qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule, + "qos_minimum_bandwidth_rule": ( + _qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule + ), + "qos_minimum_packet_rate_rule": ( + _qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule + ), "qos_policy": _qos_policy.QoSPolicy, "qos_rule_type": _qos_rule_type.QoSRuleType, "quota": _quota.Quota, @@ -152,20 +170,33 @@ class Proxy(proxy.Proxy, Generic[T]): "vpn_endpoint_group": _vpn_endpoint_group.VpnEndpointGroup, "vpn_ike_policy": _ike_policy.VpnIkePolicy, "vpn_ipsec_policy": _ipsec_policy.VpnIpsecPolicy, - "vpn_ipsec_site_connection": - _ipsec_site_connection.VpnIPSecSiteConnection, + "vpn_ipsec_site_connection": ( + _ipsec_site_connection.VpnIPSecSiteConnection + ), "vpn_service": _vpn_service.VpnService, } @proxy._check_resource(strict=False) - def _update(self, resource_type: Type[T], value, base_path=None, - if_revision=None, **attrs) -> T: + def _update( + self, + resource_type: Type[T], + value, + base_path=None, + if_revision=None, + **attrs, + ) -> T: res = self._get_resource(resource_type, value, **attrs) return res.commit(self, base_path=base_path, if_revision=if_revision) @proxy._check_resource(strict=False) - def _delete(self, resource_type: Type[T], value, ignore_missing=True, - if_revision=None, **attrs) -> Optional[T]: + def _delete( + self, + resource_type: Type[T], + value, + ignore_missing=True, + if_revision=None, + **attrs, + ) -> Optional[T]: res = self._get_resource(resource_type, value, **attrs) try: @@ -204,8 +235,11 @@ def delete_address_group(self, address_group, ignore_missing=True): :returns: ``None`` """ - self._delete(_address_group.AddressGroup, address_group, - ignore_missing=ignore_missing) + self._delete( + _address_group.AddressGroup, + address_group, + ignore_missing=ignore_missing, + ) def find_address_group(self, name_or_id, ignore_missing=True, **query): """Find a single address group @@ -221,8 +255,12 @@ def find_address_group(self, name_or_id, ignore_missing=True, **query): :returns: One :class:`~openstack.network.v2.address_group.AddressGroup` or None """ - return self._find(_address_group.AddressGroup, name_or_id, - ignore_missing=ignore_missing, **query) + return self._find( + _address_group.AddressGroup, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_address_group(self, address_group): """Get a single address group @@ -251,8 +289,9 @@ def address_groups(self, **query): """ return self._list(_address_group.AddressGroup, **query) - def update_address_group(self, address_group, - **attrs) -> _address_group.AddressGroup: + def update_address_group( + self, address_group, **attrs + ) -> _address_group.AddressGroup: """Update an address group :param address_group: Either the ID of an address group or a @@ -263,8 +302,9 @@ def update_address_group(self, address_group, :returns: The updated address group :rtype: :class:`~openstack.network.v2.address_group.AddressGroup` """ - return self._update(_address_group.AddressGroup, address_group, - **attrs) + return self._update( + _address_group.AddressGroup, address_group, **attrs + ) def add_addresses_to_address_group(self, address_group, addresses): """Add addresses to a address group @@ -317,8 +357,11 @@ def delete_address_scope(self, address_scope, ignore_missing=True): :returns: ``None`` """ - self._delete(_address_scope.AddressScope, address_scope, - ignore_missing=ignore_missing) + self._delete( + _address_scope.AddressScope, + address_scope, + ignore_missing=ignore_missing, + ) def find_address_scope(self, name_or_id, ignore_missing=True, **query): """Find a single address scope @@ -334,8 +377,12 @@ def find_address_scope(self, name_or_id, ignore_missing=True, **query): :returns: One :class:`~openstack.network.v2.address_scope.AddressScope` or None """ - return self._find(_address_scope.AddressScope, name_or_id, - ignore_missing=ignore_missing, **query) + return self._find( + _address_scope.AddressScope, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_address_scope(self, address_scope): """Get a single address scope @@ -376,8 +423,9 @@ def update_address_scope(self, address_scope, **attrs): :returns: The updated address scope :rtype: :class:`~openstack.network.v2.address_scope.AddressScope` """ - return self._update(_address_scope.AddressScope, address_scope, - **attrs) + return self._update( + _address_scope.AddressScope, address_scope, **attrs + ) def agents(self, **query): """Return a generator of network agents @@ -451,8 +499,9 @@ def dhcp_agent_hosting_networks(self, agent, **query): :return: A generator of networks """ agent_obj = self._get_resource(_agent.Agent, agent) - return self._list(_network.DHCPAgentHostingNetwork, - agent_id=agent_obj.id, **query) + return self._list( + _network.DHCPAgentHostingNetwork, agent_id=agent_obj.id, **query + ) def add_dhcp_agent_to_network(self, agent, network): """Add a DHCP Agent to a network @@ -488,8 +537,9 @@ def network_hosting_dhcp_agents(self, network, **query): :return: A generator of hosted DHCP agents """ net = self._get_resource(_network.Network, network) - return self._list(_agent.NetworkHostingDHCPAgent, network_id=net.id, - **query) + return self._list( + _agent.NetworkHostingDHCPAgent, network_id=net.id, **query + ) def get_auto_allocated_topology(self, project=None): """Get the auto-allocated topology of a given tenant @@ -505,11 +555,13 @@ def get_auto_allocated_topology(self, project=None): # If project option is not given, grab project id from session if project is None: project = self.get_project_id() - return self._get(_auto_allocated_topology.AutoAllocatedTopology, - project) + return self._get( + _auto_allocated_topology.AutoAllocatedTopology, project + ) - def delete_auto_allocated_topology(self, project=None, - ignore_missing=False): + def delete_auto_allocated_topology( + self, project=None, ignore_missing=False + ): """Delete auto-allocated topology :param project: @@ -526,8 +578,11 @@ def delete_auto_allocated_topology(self, project=None, # If project option is not given, grab project id from session if project is None: project = self.get_project_id() - self._delete(_auto_allocated_topology.AutoAllocatedTopology, - project, ignore_missing=ignore_missing) + self._delete( + _auto_allocated_topology.AutoAllocatedTopology, + project, + ignore_missing=ignore_missing, + ) def validate_auto_allocated_topology(self, project=None): """Validate the resources for auto allocation @@ -543,8 +598,11 @@ def validate_auto_allocated_topology(self, project=None): # If project option is not given, grab project id from session if project is None: project = self.get_project_id() - return self._get(_auto_allocated_topology.ValidateTopology, - project=project, requires_id=False) + return self._get( + _auto_allocated_topology.ValidateTopology, + project=project, + requires_id=False, + ) def availability_zones(self, **query): """Return a generator of availability zones @@ -570,9 +628,13 @@ def delete_bgp_peer(self, peer, ignore_missing=True): self._delete(_bgp_peer.BgpPeer, peer, ignore_missing=ignore_missing) def find_bgp_peer(self, name_or_id, ignore_missing=True, **query): - """"Find a single BGP Peer""" - return self._find(_bgp_peer.BgpPeer, name_or_id, - ignore_missing=ignore_missing, **query) + """ "Find a single BGP Peer""" + return self._find( + _bgp_peer.BgpPeer, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_bgp_peer(self, peer): """Get a signle BGP Peer""" @@ -592,13 +654,18 @@ def create_bgp_speaker(self, **attrs): def delete_bgp_speaker(self, speaker, ignore_missing=True): """Delete a BGP Speaker""" - self._delete(_bgp_speaker.BgpSpeaker, speaker, - ignore_missing=ignore_missing) + self._delete( + _bgp_speaker.BgpSpeaker, speaker, ignore_missing=ignore_missing + ) def find_bgp_speaker(self, name_or_id, ignore_missing=True, **query): - """"Find a single BGP Peer""" - return self._find(_bgp_speaker.BgpSpeaker, name_or_id, - ignore_missing=ignore_missing, **query) + """ "Find a single BGP Peer""" + return self._find( + _bgp_speaker.BgpSpeaker, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_bgp_speaker(self, speaker): """Get a signle BGP Speaker""" @@ -689,7 +756,7 @@ def delete_bgpvpn(self, bgpvpn, ignore_missing=True): self._delete(_bgpvpn.BgpVpn, bgpvpn, ignore_missing=ignore_missing) def find_bgpvpn(self, name_or_id, ignore_missing=True, **query): - """"Find a single BGPVPN + """ "Find a single BGPVPN :param name_or_id: The name or ID of a BGPVPN. :param bool ignore_missing: When set to ``False`` @@ -702,8 +769,9 @@ def find_bgpvpn(self, name_or_id, ignore_missing=True, **query): :returns: One :class:`~openstack.network.v2.bgpvpn.BGPVPN` or None """ - return self._find(_bgpvpn.BgpVpn, name_or_id, - ignore_missing=ignore_missing, **query) + return self._find( + _bgpvpn.BgpVpn, name_or_id, ignore_missing=ignore_missing, **query + ) def get_bgpvpn(self, bgpvpn): """Get a signle BGPVPN @@ -758,10 +826,13 @@ def create_bgpvpn_network_association(self, bgpvpn, **attrs): bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) return self._create( _bgpvpn_network_association.BgpVpnNetworkAssociation, - bgpvpn_id=bgpvpn_res.id, **attrs) + bgpvpn_id=bgpvpn_res.id, + **attrs, + ) - def delete_bgpvpn_network_association(self, bgpvpn, net_association, - ignore_missing=True): + def delete_bgpvpn_network_association( + self, bgpvpn, net_association, ignore_missing=True + ): """Delete a BGPVPN Network Association :param bgpvpn: The value can be either the ID of a bgpvpn or @@ -781,8 +852,10 @@ def delete_bgpvpn_network_association(self, bgpvpn, net_association, bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) self._delete( _bgpvpn_network_association.BgpVpnNetworkAssociation, - net_association, ignore_missing=ignore_missing, - bgpvpn_id=bgpvpn_res.id) + net_association, + ignore_missing=ignore_missing, + bgpvpn_id=bgpvpn_res.id, + ) def get_bgpvpn_network_association(self, bgpvpn, net_association): """Get a signle BGPVPN Network Association @@ -802,7 +875,9 @@ def get_bgpvpn_network_association(self, bgpvpn, net_association): bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) return self._get( _bgpvpn_network_association.BgpVpnNetworkAssociation, - net_association, bgpvpn_id=bgpvpn_res.id) + net_association, + bgpvpn_id=bgpvpn_res.id, + ) def bgpvpn_network_associations(self, bgpvpn, **query): """Return a generator of BGP VPN Network Associations @@ -819,7 +894,9 @@ def bgpvpn_network_associations(self, bgpvpn, **query): bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) return self._list( _bgpvpn_network_association.BgpVpnNetworkAssociation, - bgpvpn_id=bgpvpn_res.id, **query) + bgpvpn_id=bgpvpn_res.id, + **query, + ) def create_bgpvpn_port_association(self, bgpvpn, **attrs): """Create a new BGPVPN Port Association @@ -838,10 +915,13 @@ def create_bgpvpn_port_association(self, bgpvpn, **attrs): bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) return self._create( _bgpvpn_port_association.BgpVpnPortAssociation, - bgpvpn_id=bgpvpn_res.id, **attrs) + bgpvpn_id=bgpvpn_res.id, + **attrs, + ) - def delete_bgpvpn_port_association(self, bgpvpn, port_association, - ignore_missing=True): + def delete_bgpvpn_port_association( + self, bgpvpn, port_association, ignore_missing=True + ): """Delete a BGPVPN Port Association :param bgpvpn: The value can be either the ID of a bgpvpn or @@ -861,12 +941,15 @@ def delete_bgpvpn_port_association(self, bgpvpn, port_association, bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) self._delete( _bgpvpn_port_association.BgpVpnPortAssociation, - port_association, ignore_missing=ignore_missing, - bgpvpn_id=bgpvpn_res.id) + port_association, + ignore_missing=ignore_missing, + bgpvpn_id=bgpvpn_res.id, + ) - def find_bgpvpn_port_association(self, name_or_id, bgpvpn_id, - ignore_missing=True, **query): - """"Find a single BGPVPN Port Association + def find_bgpvpn_port_association( + self, name_or_id, bgpvpn_id, ignore_missing=True, **query + ): + """ "Find a single BGPVPN Port Association :param name_or_id: The name or ID of a BgpVpnNetworkAssociation. :param bgpvpn_id: The value can be the ID of a BGPVPN. @@ -883,7 +966,10 @@ def find_bgpvpn_port_association(self, name_or_id, bgpvpn_id, return self._find( _bgpvpn_port_association.BgpVpnPortAssociation, name_or_id, - ignore_missing=ignore_missing, bgpvpn_id=bgpvpn_id, **query) + ignore_missing=ignore_missing, + bgpvpn_id=bgpvpn_id, + **query, + ) def get_bgpvpn_port_association(self, bgpvpn, port_association): """Get a signle BGPVPN Port Association @@ -903,10 +989,13 @@ def get_bgpvpn_port_association(self, bgpvpn, port_association): bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) return self._get( _bgpvpn_port_association.BgpVpnPortAssociation, - port_association, bgpvpn_id=bgpvpn_res.id) + port_association, + bgpvpn_id=bgpvpn_res.id, + ) - def update_bgpvpn_port_association(self, bgpvpn, port_association, - **attrs): + def update_bgpvpn_port_association( + self, bgpvpn, port_association, **attrs + ): """Update a BPGPN Port Association :param bgpvpn: Either the ID of a BGPVPN or a @@ -924,7 +1013,10 @@ def update_bgpvpn_port_association(self, bgpvpn, port_association, bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) return self._update( _bgpvpn_port_association.BgpVpnPortAssociation, - port_association, bgpvpn_id=bgpvpn_res.id, **attrs) + port_association, + bgpvpn_id=bgpvpn_res.id, + **attrs, + ) def bgpvpn_port_associations(self, bgpvpn, **query): """Return a generator of BGP VPN Port Associations @@ -941,7 +1033,9 @@ def bgpvpn_port_associations(self, bgpvpn, **query): bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) return self._list( _bgpvpn_port_association.BgpVpnPortAssociation, - bgpvpn_id=bgpvpn_res.id, **query) + bgpvpn_id=bgpvpn_res.id, + **query, + ) def create_bgpvpn_router_association(self, bgpvpn, **attrs): """Create a new BGPVPN Router Association @@ -960,10 +1054,13 @@ def create_bgpvpn_router_association(self, bgpvpn, **attrs): bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) return self._create( _bgpvpn_router_association.BgpVpnRouterAssociation, - bgpvpn_id=bgpvpn_res.id, **attrs) + bgpvpn_id=bgpvpn_res.id, + **attrs, + ) - def delete_bgpvpn_router_association(self, bgpvpn, router_association, - ignore_missing=True): + def delete_bgpvpn_router_association( + self, bgpvpn, router_association, ignore_missing=True + ): """Delete a BGPVPN Router Association :param bgpvpn: The value can be either the ID of a bgpvpn or @@ -983,8 +1080,10 @@ def delete_bgpvpn_router_association(self, bgpvpn, router_association, bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) self._delete( _bgpvpn_router_association.BgpVpnRouterAssociation, - router_association, ignore_missing=ignore_missing, - bgpvpn_id=bgpvpn_res.id) + router_association, + ignore_missing=ignore_missing, + bgpvpn_id=bgpvpn_res.id, + ) def get_bgpvpn_router_association(self, bgpvpn, router_association): """Get a signle BGPVPN Router Association @@ -1004,10 +1103,13 @@ def get_bgpvpn_router_association(self, bgpvpn, router_association): bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) return self._get( _bgpvpn_router_association.BgpVpnRouterAssociation, - router_association, bgpvpn_id=bgpvpn_res.id) + router_association, + bgpvpn_id=bgpvpn_res.id, + ) - def update_bgpvpn_router_association(self, bgpvpn, - router_association, **attrs): + def update_bgpvpn_router_association( + self, bgpvpn, router_association, **attrs + ): """Update a BPGPN Router Association :param dict query: Optional query parameters to be sent to limit @@ -1020,7 +1122,10 @@ def update_bgpvpn_router_association(self, bgpvpn, bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) return self._update( _bgpvpn_router_association.BgpVpnRouterAssociation, - router_association, bgpvpn_id=bgpvpn_res.id, **attrs) + router_association, + bgpvpn_id=bgpvpn_res.id, + **attrs, + ) def bgpvpn_router_associations(self, bgpvpn, **query): """Return a generator of BGP VPN router Associations @@ -1037,7 +1142,9 @@ def bgpvpn_router_associations(self, bgpvpn, **query): bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) return self._list( _bgpvpn_router_association.BgpVpnRouterAssociation, - bgpvpn_id=bgpvpn_res.id, **query) + bgpvpn_id=bgpvpn_res.id, + **query, + ) def find_extension(self, name_or_id, ignore_missing=True, **query): """Find a single extension @@ -1053,8 +1160,12 @@ def find_extension(self, name_or_id, ignore_missing=True, **query): :returns: One :class:`~openstack.network.v2.extension.Extension` or None """ - return self._find(extension.Extension, name_or_id, - ignore_missing=ignore_missing, **query) + return self._find( + extension.Extension, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def extensions(self, **query): """Return a generator of extensions @@ -1109,8 +1220,9 @@ def find_flavor(self, name_or_id, ignore_missing=True, **query): underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.flavor.Flavor` or None """ - return self._find(_flavor.Flavor, name_or_id, - ignore_missing=ignore_missing, **query) + return self._find( + _flavor.Flavor, name_or_id, ignore_missing=ignore_missing, **query + ) def get_flavor(self, flavor): """Get a single network service flavor @@ -1169,12 +1281,15 @@ def associate_flavor_with_service_profile(self, flavor, service_profile): """ flavor = self._get_resource(_flavor.Flavor, flavor) service_profile = self._get_resource( - _service_profile.ServiceProfile, service_profile) + _service_profile.ServiceProfile, service_profile + ) return flavor.associate_flavor_with_service_profile( - self, service_profile.id) + self, service_profile.id + ) def disassociate_flavor_from_service_profile( - self, flavor, service_profile): + self, flavor, service_profile + ): """Disassociate network flavor from service profile. :param flavor: @@ -1188,9 +1303,11 @@ def disassociate_flavor_from_service_profile( """ flavor = self._get_resource(_flavor.Flavor, flavor) service_profile = self._get_resource( - _service_profile.ServiceProfile, service_profile) + _service_profile.ServiceProfile, service_profile + ) return flavor.disassociate_flavor_from_service_profile( - self, service_profile.id) + self, service_profile.id + ) def create_local_ip(self, **attrs): """Create a new local ip from attributes @@ -1220,8 +1337,12 @@ def delete_local_ip(self, local_ip, ignore_missing=True, if_revision=None): :returns: ``None`` """ - self._delete(_local_ip.LocalIP, local_ip, - ignore_missing=ignore_missing, if_revision=if_revision) + self._delete( + _local_ip.LocalIP, + local_ip, + ignore_missing=ignore_missing, + if_revision=if_revision, + ) def find_local_ip(self, name_or_id, ignore_missing=True, **query): """Find a local IP @@ -1237,8 +1358,12 @@ def find_local_ip(self, name_or_id, ignore_missing=True, **query): :returns: One :class:`~openstack.network.v2.local_ip.LocalIP` or None """ - return self._find(_local_ip.LocalIP, name_or_id, - ignore_missing=ignore_missing, **query) + return self._find( + _local_ip.LocalIP, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_local_ip(self, local_ip): """Get a single local ip @@ -1286,8 +1411,9 @@ def update_local_ip(self, local_ip, if_revision=None, **attrs): :returns: The updated ip :rtype: :class:`~openstack.network.v2.local_ip.LocalIP` """ - return self._update(_local_ip.LocalIP, local_ip, - if_revision=if_revision, **attrs) + return self._update( + _local_ip.LocalIP, local_ip, if_revision=if_revision, **attrs + ) def create_local_ip_association(self, local_ip, **attrs): """Create a new local ip association from attributes @@ -1305,11 +1431,15 @@ def create_local_ip_association(self, local_ip, **attrs): :class:`~openstack.network.v2.local_ip_association.LocalIPAssociation` """ local_ip = self._get_resource(_local_ip.LocalIP, local_ip) - return self._create(_local_ip_association.LocalIPAssociation, - local_ip_id=local_ip.id, **attrs) + return self._create( + _local_ip_association.LocalIPAssociation, + local_ip_id=local_ip.id, + **attrs, + ) - def delete_local_ip_association(self, local_ip, fixed_port_id, - ignore_missing=True, if_revision=None): + def delete_local_ip_association( + self, local_ip, fixed_port_id, ignore_missing=True, if_revision=None + ): """Delete a local ip association :param local_ip: The value can be the ID of a Local IP or a @@ -1330,12 +1460,17 @@ def delete_local_ip_association(self, local_ip, fixed_port_id, :returns: ``None`` """ local_ip = self._get_resource(_local_ip.LocalIP, local_ip) - self._delete(_local_ip_association.LocalIPAssociation, fixed_port_id, - local_ip_id=local_ip.id, - ignore_missing=ignore_missing, if_revision=if_revision) + self._delete( + _local_ip_association.LocalIPAssociation, + fixed_port_id, + local_ip_id=local_ip.id, + ignore_missing=ignore_missing, + if_revision=if_revision, + ) - def find_local_ip_association(self, name_or_id, local_ip, - ignore_missing=True, **query): + def find_local_ip_association( + self, name_or_id, local_ip, ignore_missing=True, **query + ): """Find a local ip association :param name_or_id: The name or ID of local ip association. @@ -1354,9 +1489,13 @@ def find_local_ip_association(self, name_or_id, local_ip, or None """ local_ip = self._get_resource(_local_ip.LocalIP, local_ip) - return self._find(_local_ip_association.LocalIPAssociation, name_or_id, - local_ip_id=local_ip.id, - ignore_missing=ignore_missing, **query) + return self._find( + _local_ip_association.LocalIPAssociation, + name_or_id, + local_ip_id=local_ip.id, + ignore_missing=ignore_missing, + **query, + ) def get_local_ip_association(self, local_ip_association, local_ip): """Get a single local ip association @@ -1375,9 +1514,11 @@ def get_local_ip_association(self, local_ip_association, local_ip): when no resource can be found. """ local_ip = self._get_resource(_local_ip.LocalIP, local_ip) - return self._get(_local_ip_association.LocalIPAssociation, - local_ip_association, - local_ip_id=local_ip.id) + return self._get( + _local_ip_association.LocalIPAssociation, + local_ip_association, + local_ip_id=local_ip.id, + ) def local_ip_associations(self, local_ip, **query): """Return a generator of local ip associations @@ -1398,8 +1539,11 @@ def local_ip_associations(self, local_ip, **query): :class:`~openstack.network.v2.local_ip_association.LocalIPAssociation` """ local_ip = self._get_resource(_local_ip.LocalIP, local_ip) - return self._list(_local_ip_association.LocalIPAssociation, - local_ip_id=local_ip.id, **query) + return self._list( + _local_ip_association.LocalIPAssociation, + local_ip_id=local_ip.id, + **query, + ) def create_ip(self, **attrs): """Create a new floating ip from attributes @@ -1429,8 +1573,12 @@ def delete_ip(self, floating_ip, ignore_missing=True, if_revision=None): :returns: ``None`` """ - self._delete(_floating_ip.FloatingIP, floating_ip, - ignore_missing=ignore_missing, if_revision=if_revision) + self._delete( + _floating_ip.FloatingIP, + floating_ip, + ignore_missing=ignore_missing, + if_revision=if_revision, + ) def find_available_ip(self): """Find an available IP @@ -1454,8 +1602,12 @@ def find_ip(self, name_or_id, ignore_missing=True, **query): :returns: One :class:`~openstack.network.v2.floating_ip.FloatingIP` or None """ - return self._find(_floating_ip.FloatingIP, name_or_id, - ignore_missing=ignore_missing, **query) + return self._find( + _floating_ip.FloatingIP, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_ip(self, floating_ip): """Get a single floating ip @@ -1509,8 +1661,12 @@ def update_ip(self, floating_ip, if_revision=None, **attrs): :returns: The updated ip :rtype: :class:`~openstack.network.v2.floating_ip.FloatingIP` """ - return self._update(_floating_ip.FloatingIP, floating_ip, - if_revision=if_revision, **attrs) + return self._update( + _floating_ip.FloatingIP, + floating_ip, + if_revision=if_revision, + **attrs, + ) def create_port_forwarding(self, **attrs): """Create a new floating ip port forwarding from attributes @@ -1540,11 +1696,15 @@ def get_port_forwarding(self, port_forwarding, floating_ip): when no resource can be found. """ floating_ip = self._get_resource(_floating_ip.FloatingIP, floating_ip) - return self._get(_port_forwarding.PortForwarding, port_forwarding, - floatingip_id=floating_ip.id) + return self._get( + _port_forwarding.PortForwarding, + port_forwarding, + floatingip_id=floating_ip.id, + ) - def find_port_forwarding(self, pf_id, floating_ip, ignore_missing=True, - **query): + def find_port_forwarding( + self, pf_id, floating_ip, ignore_missing=True, **query + ): """Find a single port forwarding :param pf_id: The ID of a port forwarding. @@ -1563,12 +1723,17 @@ def find_port_forwarding(self, pf_id, floating_ip, ignore_missing=True, or None """ floating_ip = self._get_resource(_floating_ip.FloatingIP, floating_ip) - return self._find(_port_forwarding.PortForwarding, pf_id, - floatingip_id=floating_ip.id, - ignore_missing=ignore_missing, **query) + return self._find( + _port_forwarding.PortForwarding, + pf_id, + floatingip_id=floating_ip.id, + ignore_missing=ignore_missing, + **query, + ) - def delete_port_forwarding(self, port_forwarding, floating_ip, - ignore_missing=True): + def delete_port_forwarding( + self, port_forwarding, floating_ip, ignore_missing=True + ): """Delete a port forwarding :param port_forwarding: The value can be the ID of a port forwarding @@ -1586,9 +1751,12 @@ def delete_port_forwarding(self, port_forwarding, floating_ip, :returns: ``None`` """ fip = self._get_resource(_floating_ip.FloatingIP, floating_ip) - self._delete(_port_forwarding.PortForwarding, port_forwarding, - floatingip_id=fip.id, - ignore_missing=ignore_missing) + self._delete( + _port_forwarding.PortForwarding, + port_forwarding, + floatingip_id=fip.id, + ignore_missing=ignore_missing, + ) def port_forwardings(self, floating_ip, **query): """Return a generator of port forwardings @@ -1607,8 +1775,9 @@ def port_forwardings(self, floating_ip, **query): :rtype: :class:`~openstack.network.v2.port_forwarding.PortForwarding` """ fip = self._get_resource(_floating_ip.FloatingIP, floating_ip) - return self._list(_port_forwarding.PortForwarding, - floatingip_id=fip.id, **query) + return self._list( + _port_forwarding.PortForwarding, floatingip_id=fip.id, **query + ) def update_port_forwarding(self, port_forwarding, floating_ip, **attrs): """Update a port forwarding @@ -1626,8 +1795,12 @@ def update_port_forwarding(self, port_forwarding, floating_ip, **attrs): :rtype: :class:`~openstack.network.v2.port_forwarding.PortForwarding` """ fip = self._get_resource(_floating_ip.FloatingIP, floating_ip) - return self._update(_port_forwarding.PortForwarding, - port_forwarding, floatingip_id=fip.id, **attrs) + return self._update( + _port_forwarding.PortForwarding, + port_forwarding, + floatingip_id=fip.id, + **attrs, + ) def create_health_monitor(self, **attrs): """Create a new health monitor from attributes @@ -1656,8 +1829,11 @@ def delete_health_monitor(self, health_monitor, ignore_missing=True): :returns: ``None`` """ - self._delete(_health_monitor.HealthMonitor, health_monitor, - ignore_missing=ignore_missing) + self._delete( + _health_monitor.HealthMonitor, + health_monitor, + ignore_missing=ignore_missing, + ) def find_health_monitor(self, name_or_id, ignore_missing=True, **query): """Find a single health monitor @@ -1674,8 +1850,12 @@ def find_health_monitor(self, name_or_id, ignore_missing=True, **query): :class:`~openstack.network.v2.health_monitor.HealthMonitor` or None """ - return self._find(_health_monitor.HealthMonitor, - name_or_id, ignore_missing=ignore_missing, **query) + return self._find( + _health_monitor.HealthMonitor, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_health_monitor(self, health_monitor): """Get a single health monitor @@ -1731,8 +1911,9 @@ def update_health_monitor(self, health_monitor, **attrs): :returns: The updated health monitor :rtype: :class:`~openstack.network.v2.health_monitor.HealthMonitor` """ - return self._update(_health_monitor.HealthMonitor, health_monitor, - **attrs) + return self._update( + _health_monitor.HealthMonitor, health_monitor, **attrs + ) def create_listener(self, **attrs): """Create a new listener from attributes @@ -1759,8 +1940,9 @@ def delete_listener(self, listener, ignore_missing=True): :returns: ``None`` """ - self._delete(_listener.Listener, listener, - ignore_missing=ignore_missing) + self._delete( + _listener.Listener, listener, ignore_missing=ignore_missing + ) def find_listener(self, name_or_id, ignore_missing=True, **query): """Find a single listener @@ -1775,8 +1957,12 @@ def find_listener(self, name_or_id, ignore_missing=True, **query): underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.listener.Listener` or None """ - return self._find(_listener.Listener, name_or_id, - ignore_missing=ignore_missing, **query) + return self._find( + _listener.Listener, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_listener(self, listener): """Get a single listener @@ -1854,8 +2040,11 @@ def delete_load_balancer(self, load_balancer, ignore_missing=True): :returns: ``None`` """ - self._delete(_load_balancer.LoadBalancer, load_balancer, - ignore_missing=ignore_missing) + self._delete( + _load_balancer.LoadBalancer, + load_balancer, + ignore_missing=ignore_missing, + ) def find_load_balancer(self, name_or_id, ignore_missing=True, **query): """Find a single load balancer @@ -1871,8 +2060,12 @@ def find_load_balancer(self, name_or_id, ignore_missing=True, **query): :returns: One :class:`~openstack.network.v2.load_balancer.LoadBalancer` or None """ - return self._find(_load_balancer.LoadBalancer, name_or_id, - ignore_missing=ignore_missing, **query) + return self._find( + _load_balancer.LoadBalancer, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_load_balancer(self, load_balancer): """Get a single load balancer @@ -1910,8 +2103,9 @@ def update_load_balancer(self, load_balancer, **attrs): :returns: The updated load balancer :rtype: :class:`~openstack.network.v2.load_balancer.LoadBalancer` """ - return self._update(_load_balancer.LoadBalancer, load_balancer, - **attrs) + return self._update( + _load_balancer.LoadBalancer, load_balancer, **attrs + ) def create_metering_label(self, **attrs): """Create a new metering label from attributes @@ -1940,8 +2134,11 @@ def delete_metering_label(self, metering_label, ignore_missing=True): :returns: ``None`` """ - self._delete(_metering_label.MeteringLabel, metering_label, - ignore_missing=ignore_missing) + self._delete( + _metering_label.MeteringLabel, + metering_label, + ignore_missing=ignore_missing, + ) def find_metering_label(self, name_or_id, ignore_missing=True, **query): """Find a single metering label @@ -1958,8 +2155,12 @@ def find_metering_label(self, name_or_id, ignore_missing=True, **query): :class:`~openstack.network.v2.metering_label.MeteringLabel` or None """ - return self._find(_metering_label.MeteringLabel, name_or_id, - ignore_missing=ignore_missing, **query) + return self._find( + _metering_label.MeteringLabel, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_metering_label(self, metering_label): """Get a single metering label @@ -2005,8 +2206,9 @@ def update_metering_label(self, metering_label, **attrs): :returns: The updated metering label :rtype: :class:`~openstack.network.v2.metering_label.MeteringLabel` """ - return self._update(_metering_label.MeteringLabel, metering_label, - **attrs) + return self._update( + _metering_label.MeteringLabel, metering_label, **attrs + ) def create_metering_label_rule(self, **attrs): """Create a new metering label rule from attributes @@ -2021,8 +2223,9 @@ def create_metering_label_rule(self, **attrs): """ return self._create(_metering_label_rule.MeteringLabelRule, **attrs) - def delete_metering_label_rule(self, metering_label_rule, - ignore_missing=True): + def delete_metering_label_rule( + self, metering_label_rule, ignore_missing=True + ): """Delete a metering label rule :param metering_label_rule: @@ -2038,11 +2241,15 @@ def delete_metering_label_rule(self, metering_label_rule, :returns: ``None`` """ - self._delete(_metering_label_rule.MeteringLabelRule, - metering_label_rule, ignore_missing=ignore_missing) + self._delete( + _metering_label_rule.MeteringLabelRule, + metering_label_rule, + ignore_missing=ignore_missing, + ) - def find_metering_label_rule(self, name_or_id, ignore_missing=True, - **query): + def find_metering_label_rule( + self, name_or_id, ignore_missing=True, **query + ): """Find a single metering label rule :param name_or_id: The name or ID of a metering label rule. @@ -2057,8 +2264,12 @@ def find_metering_label_rule(self, name_or_id, ignore_missing=True, :class:`~openstack.network.v2.metering_label_rule.MeteringLabelRule` or None """ - return self._find(_metering_label_rule.MeteringLabelRule, name_or_id, - ignore_missing=ignore_missing, **query) + return self._find( + _metering_label_rule.MeteringLabelRule, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_metering_label_rule(self, metering_label_rule): """Get a single metering label rule @@ -2073,8 +2284,9 @@ def get_metering_label_rule(self, metering_label_rule): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get(_metering_label_rule.MeteringLabelRule, - metering_label_rule) + return self._get( + _metering_label_rule.MeteringLabelRule, metering_label_rule + ) def metering_label_rules(self, **query): """Return a generator of metering label rules @@ -2111,8 +2323,11 @@ def update_metering_label_rule(self, metering_label_rule, **attrs): :rtype: :class:`~openstack.network.v2.metering_label_rule.MeteringLabelRule` """ - return self._update(_metering_label_rule.MeteringLabelRule, - metering_label_rule, **attrs) + return self._update( + _metering_label_rule.MeteringLabelRule, + metering_label_rule, + **attrs, + ) def create_network(self, **attrs): """Create a new network from attributes @@ -2142,8 +2357,12 @@ def delete_network(self, network, ignore_missing=True, if_revision=None): :returns: ``None`` """ - self._delete(_network.Network, network, ignore_missing=ignore_missing, - if_revision=if_revision) + self._delete( + _network.Network, + network, + ignore_missing=ignore_missing, + if_revision=if_revision, + ) def find_network(self, name_or_id, ignore_missing=True, **query): """Find a single network @@ -2158,8 +2377,12 @@ def find_network(self, name_or_id, ignore_missing=True, **query): underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.network.Network` or None """ - return self._find(_network.Network, name_or_id, - ignore_missing=ignore_missing, **query) + return self._find( + _network.Network, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_network(self, network): """Get a single network @@ -2215,11 +2438,13 @@ def update_network(self, network, if_revision=None, **attrs): :returns: The updated network :rtype: :class:`~openstack.network.v2.network.Network` """ - return self._update(_network.Network, network, if_revision=if_revision, - **attrs) + return self._update( + _network.Network, network, if_revision=if_revision, **attrs + ) - def find_network_ip_availability(self, name_or_id, ignore_missing=True, - **query): + def find_network_ip_availability( + self, name_or_id, ignore_missing=True, **query + ): """Find IP availability of a network :param name_or_id: The name or ID of a network. @@ -2234,8 +2459,12 @@ def find_network_ip_availability(self, name_or_id, ignore_missing=True, :class:`~openstack.network.v2.network_ip_availability.NetworkIPAvailability` or None """ - return self._find(network_ip_availability.NetworkIPAvailability, - name_or_id, ignore_missing=ignore_missing, **query) + return self._find( + network_ip_availability.NetworkIPAvailability, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_network_ip_availability(self, network): """Get IP availability of a network @@ -2249,8 +2478,9 @@ def get_network_ip_availability(self, network): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get(network_ip_availability.NetworkIPAvailability, - network) + return self._get( + network_ip_availability.NetworkIPAvailability, network + ) def network_ip_availabilities(self, **query): """Return a generator of network ip availabilities @@ -2269,8 +2499,9 @@ def network_ip_availabilities(self, **query): :rtype: :class:`~openstack.network.v2.network_ip_availability.NetworkIPAvailability` """ - return self._list(network_ip_availability.NetworkIPAvailability, - **query) + return self._list( + network_ip_availability.NetworkIPAvailability, **query + ) def create_network_segment_range(self, **attrs): """Create a new network segment range from attributes @@ -2284,11 +2515,13 @@ def create_network_segment_range(self, **attrs): :rtype: :class:`~openstack.network.v2.network_segment_range.NetworkSegmentRange` """ - return self._create(_network_segment_range.NetworkSegmentRange, - **attrs) + return self._create( + _network_segment_range.NetworkSegmentRange, **attrs + ) - def delete_network_segment_range(self, network_segment_range, - ignore_missing=True): + def delete_network_segment_range( + self, network_segment_range, ignore_missing=True + ): """Delete a network segment range :param network_segment_range: The value can be either the ID of a @@ -2303,11 +2536,15 @@ def delete_network_segment_range(self, network_segment_range, :returns: ``None`` """ - self._delete(_network_segment_range.NetworkSegmentRange, - network_segment_range, ignore_missing=ignore_missing) + self._delete( + _network_segment_range.NetworkSegmentRange, + network_segment_range, + ignore_missing=ignore_missing, + ) - def find_network_segment_range(self, name_or_id, ignore_missing=True, - **query): + def find_network_segment_range( + self, name_or_id, ignore_missing=True, **query + ): """Find a single network segment range :param name_or_id: The name or ID of a network segment range. @@ -2322,8 +2559,12 @@ def find_network_segment_range(self, name_or_id, ignore_missing=True, :class:`~openstack.network.v2.network_segment_range.NetworkSegmentRange` or None """ - return self._find(_network_segment_range.NetworkSegmentRange, - name_or_id, ignore_missing=ignore_missing, **query) + return self._find( + _network_segment_range.NetworkSegmentRange, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_network_segment_range(self, network_segment_range): """Get a single network segment range @@ -2338,8 +2579,9 @@ def get_network_segment_range(self, network_segment_range): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get(_network_segment_range.NetworkSegmentRange, - network_segment_range) + return self._get( + _network_segment_range.NetworkSegmentRange, network_segment_range + ) def network_segment_ranges(self, **query): """Return a generator of network segment ranges @@ -2386,8 +2628,11 @@ def update_network_segment_range(self, network_segment_range, **attrs): :rtype: :class:`~openstack.network.v2._network_segment_range.NetworkSegmentRange` """ - return self._update(_network_segment_range.NetworkSegmentRange, - network_segment_range, **attrs) + return self._update( + _network_segment_range.NetworkSegmentRange, + network_segment_range, + **attrs, + ) def create_pool(self, **attrs): """Create a new pool from attributes @@ -2429,8 +2674,9 @@ def find_pool(self, name_or_id, ignore_missing=True, **query): underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.pool.Pool` or None """ - return self._find(_pool.Pool, name_or_id, - ignore_missing=ignore_missing, **query) + return self._find( + _pool.Pool, name_or_id, ignore_missing=ignore_missing, **query + ) def get_pool(self, pool): """Get a single pool @@ -2497,8 +2743,9 @@ def create_pool_member(self, pool, **attrs): :rtype: :class:`~openstack.network.v2.pool_member.PoolMember` """ poolobj = self._get_resource(_pool.Pool, pool) - return self._create(_pool_member.PoolMember, pool_id=poolobj.id, - **attrs) + return self._create( + _pool_member.PoolMember, pool_id=poolobj.id, **attrs + ) def delete_pool_member(self, pool_member, pool, ignore_missing=True): """Delete a pool member @@ -2518,8 +2765,12 @@ def delete_pool_member(self, pool_member, pool, ignore_missing=True): :returns: ``None`` """ poolobj = self._get_resource(_pool.Pool, pool) - self._delete(_pool_member.PoolMember, pool_member, - ignore_missing=ignore_missing, pool_id=poolobj.id) + self._delete( + _pool_member.PoolMember, + pool_member, + ignore_missing=ignore_missing, + pool_id=poolobj.id, + ) def find_pool_member(self, name_or_id, pool, ignore_missing=True, **query): """Find a single pool member @@ -2539,9 +2790,13 @@ def find_pool_member(self, name_or_id, pool, ignore_missing=True, **query): or None """ poolobj = self._get_resource(_pool.Pool, pool) - return self._find(_pool_member.PoolMember, name_or_id, - ignore_missing=ignore_missing, pool_id=poolobj.id, - **query) + return self._find( + _pool_member.PoolMember, + name_or_id, + ignore_missing=ignore_missing, + pool_id=poolobj.id, + **query, + ) def get_pool_member(self, pool_member, pool): """Get a single pool member @@ -2558,8 +2813,9 @@ def get_pool_member(self, pool_member, pool): when no resource can be found. """ poolobj = self._get_resource(_pool.Pool, pool) - return self._get(_pool_member.PoolMember, pool_member, - pool_id=poolobj.id) + return self._get( + _pool_member.PoolMember, pool_member, pool_id=poolobj.id + ) def pool_members(self, pool, **query): """Return a generator of pool members @@ -2604,8 +2860,9 @@ def update_pool_member(self, pool_member, pool, **attrs): :rtype: :class:`~openstack.network.v2.pool_member.PoolMember` """ poolobj = self._get_resource(_pool.Pool, pool) - return self._update(_pool_member.PoolMember, pool_member, - pool_id=poolobj.id, **attrs) + return self._update( + _pool_member.PoolMember, pool_member, pool_id=poolobj.id, **attrs + ) def create_port(self, **attrs): """Create a new port from attributes @@ -2646,8 +2903,12 @@ def delete_port(self, port, ignore_missing=True, if_revision=None): :returns: ``None`` """ - self._delete(_port.Port, port, ignore_missing=ignore_missing, - if_revision=if_revision) + self._delete( + _port.Port, + port, + ignore_missing=ignore_missing, + if_revision=if_revision, + ) def find_port(self, name_or_id, ignore_missing=True, **query): """Find a single port @@ -2662,8 +2923,9 @@ def find_port(self, name_or_id, ignore_missing=True, **query): underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.port.Port` or None """ - return self._find(_port.Port, name_or_id, - ignore_missing=ignore_missing, **query) + return self._find( + _port.Port, name_or_id, ignore_missing=ignore_missing, **query + ) def get_port(self, port): """Get a single port @@ -2714,8 +2976,7 @@ def update_port(self, port, if_revision=None, **attrs) -> _port.Port: :returns: The updated port :rtype: :class:`~openstack.network.v2.port.Port` """ - return self._update(_port.Port, port, if_revision=if_revision, - **attrs) + return self._update(_port.Port, port, if_revision=if_revision, **attrs) def add_ip_to_port(self, port, ip): ip.port_id = port.id @@ -2751,11 +3012,15 @@ def create_qos_bandwidth_limit_rule(self, qos_policy, **attrs): :class:`~openstack.network.v2.qos_bandwidth_limit_rule.QoSBandwidthLimitRule` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) - return self._create(_qos_bandwidth_limit_rule.QoSBandwidthLimitRule, - qos_policy_id=policy.id, **attrs) + return self._create( + _qos_bandwidth_limit_rule.QoSBandwidthLimitRule, + qos_policy_id=policy.id, + **attrs, + ) - def delete_qos_bandwidth_limit_rule(self, qos_rule, qos_policy, - ignore_missing=True): + def delete_qos_bandwidth_limit_rule( + self, qos_rule, qos_policy, ignore_missing=True + ): """Delete a bandwidth limit rule :param qos_rule: The value can be either the ID of a bandwidth limit @@ -2774,12 +3039,16 @@ def delete_qos_bandwidth_limit_rule(self, qos_rule, qos_policy, :returns: ``None`` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) - self._delete(_qos_bandwidth_limit_rule.QoSBandwidthLimitRule, - qos_rule, ignore_missing=ignore_missing, - qos_policy_id=policy.id) + self._delete( + _qos_bandwidth_limit_rule.QoSBandwidthLimitRule, + qos_rule, + ignore_missing=ignore_missing, + qos_policy_id=policy.id, + ) - def find_qos_bandwidth_limit_rule(self, qos_rule_id, qos_policy, - ignore_missing=True, **query): + def find_qos_bandwidth_limit_rule( + self, qos_rule_id, qos_policy, ignore_missing=True, **query + ): """Find a bandwidth limit rule :param qos_rule_id: The ID of a bandwidth limit rule. @@ -2798,9 +3067,13 @@ def find_qos_bandwidth_limit_rule(self, qos_rule_id, qos_policy, or None """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) - return self._find(_qos_bandwidth_limit_rule.QoSBandwidthLimitRule, - qos_rule_id, ignore_missing=ignore_missing, - qos_policy_id=policy.id, **query) + return self._find( + _qos_bandwidth_limit_rule.QoSBandwidthLimitRule, + qos_rule_id, + ignore_missing=ignore_missing, + qos_policy_id=policy.id, + **query, + ) def get_qos_bandwidth_limit_rule(self, qos_rule, qos_policy): """Get a single bandwidth limit rule @@ -2818,8 +3091,11 @@ def get_qos_bandwidth_limit_rule(self, qos_rule, qos_policy): when no resource can be found. """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) - return self._get(_qos_bandwidth_limit_rule.QoSBandwidthLimitRule, - qos_rule, qos_policy_id=policy.id) + return self._get( + _qos_bandwidth_limit_rule.QoSBandwidthLimitRule, + qos_rule, + qos_policy_id=policy.id, + ) def qos_bandwidth_limit_rules(self, qos_policy, **query): """Return a generator of bandwidth limit rules @@ -2834,11 +3110,17 @@ def qos_bandwidth_limit_rules(self, qos_policy, **query): :class:`~openstack.network.v2.qos_bandwidth_limit_rule.QoSBandwidthLimitRule` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) - return self._list(_qos_bandwidth_limit_rule.QoSBandwidthLimitRule, - qos_policy_id=policy.id, **query) + return self._list( + _qos_bandwidth_limit_rule.QoSBandwidthLimitRule, + qos_policy_id=policy.id, + **query, + ) def update_qos_bandwidth_limit_rule( - self, qos_rule, qos_policy, **attrs, + self, + qos_rule, + qos_policy, + **attrs, ): """Update a bandwidth limit rule @@ -2856,8 +3138,12 @@ def update_qos_bandwidth_limit_rule( :class:`~openstack.network.v2.qos_bandwidth_limit_rule.QoSBandwidthLimitRule` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) - return self._update(_qos_bandwidth_limit_rule.QoSBandwidthLimitRule, - qos_rule, qos_policy_id=policy.id, **attrs) + return self._update( + _qos_bandwidth_limit_rule.QoSBandwidthLimitRule, + qos_rule, + qos_policy_id=policy.id, + **attrs, + ) def create_qos_dscp_marking_rule(self, qos_policy, **attrs): """Create a new QoS DSCP marking rule @@ -2876,11 +3162,15 @@ def create_qos_dscp_marking_rule(self, qos_policy, **attrs): :class:`~openstack.network.v2.qos_dscp_marking_rule.QoSDSCPMarkingRule` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) - return self._create(_qos_dscp_marking_rule.QoSDSCPMarkingRule, - qos_policy_id=policy.id, **attrs) + return self._create( + _qos_dscp_marking_rule.QoSDSCPMarkingRule, + qos_policy_id=policy.id, + **attrs, + ) - def delete_qos_dscp_marking_rule(self, qos_rule, qos_policy, - ignore_missing=True): + def delete_qos_dscp_marking_rule( + self, qos_rule, qos_policy, ignore_missing=True + ): """Delete a QoS DSCP marking rule :param qos_rule: The value can be either the ID of a minimum bandwidth @@ -2899,12 +3189,16 @@ def delete_qos_dscp_marking_rule(self, qos_rule, qos_policy, :returns: ``None`` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) - self._delete(_qos_dscp_marking_rule.QoSDSCPMarkingRule, - qos_rule, ignore_missing=ignore_missing, - qos_policy_id=policy.id) + self._delete( + _qos_dscp_marking_rule.QoSDSCPMarkingRule, + qos_rule, + ignore_missing=ignore_missing, + qos_policy_id=policy.id, + ) - def find_qos_dscp_marking_rule(self, qos_rule_id, qos_policy, - ignore_missing=True, **query): + def find_qos_dscp_marking_rule( + self, qos_rule_id, qos_policy, ignore_missing=True, **query + ): """Find a QoS DSCP marking rule :param qos_rule_id: The ID of a QoS DSCP marking rule. @@ -2923,9 +3217,13 @@ def find_qos_dscp_marking_rule(self, qos_rule_id, qos_policy, or None """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) - return self._find(_qos_dscp_marking_rule.QoSDSCPMarkingRule, - qos_rule_id, ignore_missing=ignore_missing, - qos_policy_id=policy.id, **query) + return self._find( + _qos_dscp_marking_rule.QoSDSCPMarkingRule, + qos_rule_id, + ignore_missing=ignore_missing, + qos_policy_id=policy.id, + **query, + ) def get_qos_dscp_marking_rule(self, qos_rule, qos_policy): """Get a single QoS DSCP marking rule @@ -2943,8 +3241,11 @@ def get_qos_dscp_marking_rule(self, qos_rule, qos_policy): when no resource can be found. """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) - return self._get(_qos_dscp_marking_rule.QoSDSCPMarkingRule, - qos_rule, qos_policy_id=policy.id) + return self._get( + _qos_dscp_marking_rule.QoSDSCPMarkingRule, + qos_rule, + qos_policy_id=policy.id, + ) def qos_dscp_marking_rules(self, qos_policy, **query): """Return a generator of QoS DSCP marking rules @@ -2959,8 +3260,11 @@ def qos_dscp_marking_rules(self, qos_policy, **query): :class:`~openstack.network.v2.qos_dscp_marking_rule.QoSDSCPMarkingRule` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) - return self._list(_qos_dscp_marking_rule.QoSDSCPMarkingRule, - qos_policy_id=policy.id, **query) + return self._list( + _qos_dscp_marking_rule.QoSDSCPMarkingRule, + qos_policy_id=policy.id, + **query, + ) def update_qos_dscp_marking_rule(self, qos_rule, qos_policy, **attrs): """Update a QoS DSCP marking rule @@ -2979,8 +3283,12 @@ def update_qos_dscp_marking_rule(self, qos_rule, qos_policy, **attrs): :class:`~openstack.network.v2.qos_dscp_marking_rule.QoSDSCPMarkingRule` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) - return self._update(_qos_dscp_marking_rule.QoSDSCPMarkingRule, - qos_rule, qos_policy_id=policy.id, **attrs) + return self._update( + _qos_dscp_marking_rule.QoSDSCPMarkingRule, + qos_rule, + qos_policy_id=policy.id, + **attrs, + ) def create_qos_minimum_bandwidth_rule(self, qos_policy, **attrs): """Create a new minimum bandwidth rule @@ -3001,10 +3309,13 @@ def create_qos_minimum_bandwidth_rule(self, qos_policy, **attrs): policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._create( _qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, - qos_policy_id=policy.id, **attrs) + qos_policy_id=policy.id, + **attrs, + ) - def delete_qos_minimum_bandwidth_rule(self, qos_rule, qos_policy, - ignore_missing=True): + def delete_qos_minimum_bandwidth_rule( + self, qos_rule, qos_policy, ignore_missing=True + ): """Delete a minimum bandwidth rule :param qos_rule: The value can be either the ID of a minimum bandwidth @@ -3023,12 +3334,16 @@ def delete_qos_minimum_bandwidth_rule(self, qos_rule, qos_policy, :returns: ``None`` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) - self._delete(_qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, - qos_rule, ignore_missing=ignore_missing, - qos_policy_id=policy.id) + self._delete( + _qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, + qos_rule, + ignore_missing=ignore_missing, + qos_policy_id=policy.id, + ) - def find_qos_minimum_bandwidth_rule(self, qos_rule_id, qos_policy, - ignore_missing=True, **query): + def find_qos_minimum_bandwidth_rule( + self, qos_rule_id, qos_policy, ignore_missing=True, **query + ): """Find a minimum bandwidth rule :param qos_rule_id: The ID of a minimum bandwidth rule. @@ -3047,9 +3362,13 @@ def find_qos_minimum_bandwidth_rule(self, qos_rule_id, qos_policy, or None """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) - return self._find(_qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, - qos_rule_id, ignore_missing=ignore_missing, - qos_policy_id=policy.id, **query) + return self._find( + _qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, + qos_rule_id, + ignore_missing=ignore_missing, + qos_policy_id=policy.id, + **query, + ) def get_qos_minimum_bandwidth_rule(self, qos_rule, qos_policy): """Get a single minimum bandwidth rule @@ -3069,8 +3388,11 @@ def get_qos_minimum_bandwidth_rule(self, qos_rule, qos_policy): when no resource can be found. """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) - return self._get(_qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, - qos_rule, qos_policy_id=policy.id) + return self._get( + _qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, + qos_rule, + qos_policy_id=policy.id, + ) def qos_minimum_bandwidth_rules(self, qos_policy, **query): """Return a generator of minimum bandwidth rules @@ -3085,11 +3407,13 @@ def qos_minimum_bandwidth_rules(self, qos_policy, **query): :class:`~openstack.network.v2.qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) - return self._list(_qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, - qos_policy_id=policy.id, **query) + return self._list( + _qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, + qos_policy_id=policy.id, + **query, + ) - def update_qos_minimum_bandwidth_rule(self, qos_rule, qos_policy, - **attrs): + def update_qos_minimum_bandwidth_rule(self, qos_rule, qos_policy, **attrs): """Update a minimum bandwidth rule :param qos_rule: Either the id of a minimum bandwidth rule or a @@ -3107,9 +3431,12 @@ def update_qos_minimum_bandwidth_rule(self, qos_rule, qos_policy, :class:`~openstack.network.v2.qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) - return self._update(_qos_minimum_bandwidth_rule. - QoSMinimumBandwidthRule, qos_rule, - qos_policy_id=policy.id, **attrs) + return self._update( + _qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, + qos_rule, + qos_policy_id=policy.id, + **attrs, + ) def create_qos_minimum_packet_rate_rule(self, qos_policy, **attrs): """Create a new minimum packet rate rule @@ -3128,10 +3455,13 @@ def create_qos_minimum_packet_rate_rule(self, qos_policy, **attrs): policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._create( _qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule, - qos_policy_id=policy.id, **attrs) + qos_policy_id=policy.id, + **attrs, + ) - def delete_qos_minimum_packet_rate_rule(self, qos_rule, qos_policy, - ignore_missing=True): + def delete_qos_minimum_packet_rate_rule( + self, qos_rule, qos_policy, ignore_missing=True + ): """Delete a minimum packet rate rule :param qos_rule: The value can be either the ID of a minimum packet @@ -3150,12 +3480,16 @@ def delete_qos_minimum_packet_rate_rule(self, qos_rule, qos_policy, :returns: ``None`` """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) - self._delete(_qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule, - qos_rule, ignore_missing=ignore_missing, - qos_policy_id=policy.id) + self._delete( + _qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule, + qos_rule, + ignore_missing=ignore_missing, + qos_policy_id=policy.id, + ) - def find_qos_minimum_packet_rate_rule(self, qos_rule_id, qos_policy, - ignore_missing=True, **query): + def find_qos_minimum_packet_rate_rule( + self, qos_rule_id, qos_policy, ignore_missing=True, **query + ): """Find a minimum packet rate rule :param qos_rule_id: The ID of a minimum packet rate rule. @@ -3175,8 +3509,11 @@ def find_qos_minimum_packet_rate_rule(self, qos_rule_id, qos_policy, policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._find( _qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule, - qos_rule_id, ignore_missing=ignore_missing, - qos_policy_id=policy.id, **query) + qos_rule_id, + ignore_missing=ignore_missing, + qos_policy_id=policy.id, + **query, + ) def get_qos_minimum_packet_rate_rule(self, qos_rule, qos_policy): """Get a single minimum packet rate rule @@ -3196,7 +3533,9 @@ def get_qos_minimum_packet_rate_rule(self, qos_rule, qos_policy): policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._get( _qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule, - qos_rule, qos_policy_id=policy.id) + qos_rule, + qos_policy_id=policy.id, + ) def qos_minimum_packet_rate_rules(self, qos_policy, **query): """Return a generator of minimum packet rate rules @@ -3213,10 +3552,13 @@ def qos_minimum_packet_rate_rules(self, qos_policy, **query): policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) return self._list( _qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule, - qos_policy_id=policy.id, **query) + qos_policy_id=policy.id, + **query, + ) - def update_qos_minimum_packet_rate_rule(self, qos_rule, qos_policy, - **attrs): + def update_qos_minimum_packet_rate_rule( + self, qos_rule, qos_policy, **attrs + ): """Update a minimum packet rate rule :param qos_rule: Either the id of a minimum packet rate rule or a @@ -3267,8 +3609,9 @@ def delete_qos_policy(self, qos_policy, ignore_missing=True): :returns: ``None`` """ - self._delete(_qos_policy.QoSPolicy, qos_policy, - ignore_missing=ignore_missing) + self._delete( + _qos_policy.QoSPolicy, qos_policy, ignore_missing=ignore_missing + ) def find_qos_policy(self, name_or_id, ignore_missing=True, **query): """Find a single QoS policy @@ -3284,8 +3627,12 @@ def find_qos_policy(self, name_or_id, ignore_missing=True, **query): :returns: One :class:`~openstack.network.v2.qos_policy.QoSPolicy` or None """ - return self._find(_qos_policy.QoSPolicy, name_or_id, - ignore_missing=ignore_missing, **query) + return self._find( + _qos_policy.QoSPolicy, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_qos_policy(self, qos_policy): """Get a single QoS policy @@ -3341,8 +3688,11 @@ def find_qos_rule_type(self, rule_type_name, ignore_missing=True): :returns: One :class:`~openstack.network.v2.qos_rule_type.QoSRuleType` or None """ - return self._find(_qos_rule_type.QoSRuleType, rule_type_name, - ignore_missing=ignore_missing) + return self._find( + _qos_rule_type.QoSRuleType, + rule_type_name, + ignore_missing=ignore_missing, + ) def get_qos_rule_type(self, qos_rule_type): """Get details about single QoS rule type @@ -3404,8 +3754,9 @@ def get_quota(self, quota, details=False): """ if details: quota_obj = self._get_resource(_quota.Quota, quota) - quota = self._get(_quota.QuotaDetails, project=quota_obj.id, - requires_id=False) + quota = self._get( + _quota.QuotaDetails, project=quota_obj.id, requires_id=False + ) else: quota = self._get(_quota.Quota, quota) return quota @@ -3423,8 +3774,9 @@ def get_quota_default(self, quota): when no resource can be found. """ quota_obj = self._get_resource(_quota.Quota, quota) - return self._get(_quota.QuotaDefault, project=quota_obj.id, - requires_id=False) + return self._get( + _quota.QuotaDefault, project=quota_obj.id, requires_id=False + ) def quotas(self, **query): """Return a generator of quotas @@ -3478,8 +3830,9 @@ def delete_rbac_policy(self, rbac_policy, ignore_missing=True): :returns: ``None`` """ - self._delete(_rbac_policy.RBACPolicy, rbac_policy, - ignore_missing=ignore_missing) + self._delete( + _rbac_policy.RBACPolicy, rbac_policy, ignore_missing=ignore_missing + ) def find_rbac_policy(self, rbac_policy, ignore_missing=True, **query): """Find a single RBAC policy @@ -3495,8 +3848,12 @@ def find_rbac_policy(self, rbac_policy, ignore_missing=True, **query): :returns: One :class:`~openstack.network.v2.rbac_policy.RBACPolicy` or None """ - return self._find(_rbac_policy.RBACPolicy, rbac_policy, - ignore_missing=ignore_missing, **query) + return self._find( + _rbac_policy.RBACPolicy, + rbac_policy, + ignore_missing=ignore_missing, + **query, + ) def get_rbac_policy(self, rbac_policy): """Get a single RBAC policy @@ -3568,8 +3925,12 @@ def delete_router(self, router, ignore_missing=True, if_revision=None): :returns: ``None`` """ - self._delete(_router.Router, router, ignore_missing=ignore_missing, - if_revision=if_revision) + self._delete( + _router.Router, + router, + ignore_missing=ignore_missing, + if_revision=if_revision, + ) def find_router(self, name_or_id, ignore_missing=True, **query): """Find a single router @@ -3584,8 +3945,9 @@ def find_router(self, name_or_id, ignore_missing=True, **query): underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.router.Router` or None """ - return self._find(_router.Router, name_or_id, - ignore_missing=ignore_missing, **query) + return self._find( + _router.Router, name_or_id, ignore_missing=ignore_missing, **query + ) def get_router(self, router): """Get a single router @@ -3633,8 +3995,9 @@ def update_router(self, router, if_revision=None, **attrs): :returns: The updated router :rtype: :class:`~openstack.network.v2.router.Router` """ - return self._update(_router.Router, router, if_revision=if_revision, - **attrs) + return self._update( + _router.Router, router, if_revision=if_revision, **attrs + ) def add_interface_to_router(self, router, subnet_id=None, port_id=None): """Add Interface to a router @@ -3654,8 +4017,9 @@ def add_interface_to_router(self, router, subnet_id=None, port_id=None): router = self._get_resource(_router.Router, router) return router.add_interface(self, **body) - def remove_interface_from_router(self, router, subnet_id=None, - port_id=None): + def remove_interface_from_router( + self, router, subnet_id=None, port_id=None + ): """Remove Interface from a router :param router: Either the router ID or an instance of @@ -3802,8 +4166,7 @@ def get_ndp_proxy(self, ndp_proxy): """ return self._get(_ndp_proxy.NDPProxy, ndp_proxy) - def find_ndp_proxy(self, ndp_proxy_id, - ignore_missing=True, **query): + def find_ndp_proxy(self, ndp_proxy_id, ignore_missing=True, **query): """Find a single ndp proxy :param ndp_proxy_id: The ID of a ndp proxy. @@ -3817,9 +4180,11 @@ def find_ndp_proxy(self, ndp_proxy_id, One :class:`~openstack.network.v2.ndp_proxy.NDPProxy` or None """ return self._find( - _ndp_proxy.NDPProxy, ndp_proxy_id, + _ndp_proxy.NDPProxy, + ndp_proxy_id, ignore_missing=ignore_missing, - **query) + **query, + ) def delete_ndp_proxy(self, ndp_proxy, ignore_missing=True): """Delete a ndp proxy @@ -3835,8 +4200,8 @@ def delete_ndp_proxy(self, ndp_proxy, ignore_missing=True): :returns: ``None`` """ self._delete( - _ndp_proxy.NDPProxy, ndp_proxy, - ignore_missing=ignore_missing) + _ndp_proxy.NDPProxy, ndp_proxy, ignore_missing=ignore_missing + ) def ndp_proxies(self, **query): """Return a generator of ndp proxies @@ -3893,8 +4258,11 @@ def delete_firewall_group(self, firewall_group, ignore_missing=True): :returns: ``None`` """ - self._delete(_firewall_group.FirewallGroup, firewall_group, - ignore_missing=ignore_missing) + self._delete( + _firewall_group.FirewallGroup, + firewall_group, + ignore_missing=ignore_missing, + ) def find_firewall_group(self, name_or_id, ignore_missing=True, **query): """Find a single firewall group @@ -3910,8 +4278,12 @@ def find_firewall_group(self, name_or_id, ignore_missing=True, **query): :returns: One :class:`~openstack.network.v2.firewall_group.FirewallGroup` or None """ - return self._find(_firewall_group.FirewallGroup, - name_or_id, ignore_missing=ignore_missing, **query) + return self._find( + _firewall_group.FirewallGroup, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_firewall_group(self, firewall_group): """Get a single firewall group @@ -3963,8 +4335,9 @@ def update_firewall_group(self, firewall_group, **attrs): :returns: The updated firewall group :rtype: :class:`~openstack.network.v2.firewall_group.FirewallGroup` """ - return self._update(_firewall_group.FirewallGroup, firewall_group, - **attrs) + return self._update( + _firewall_group.FirewallGroup, firewall_group, **attrs + ) def create_firewall_policy(self, **attrs): """Create a new firewall policy from attributes @@ -3993,8 +4366,11 @@ def delete_firewall_policy(self, firewall_policy, ignore_missing=True): :returns: ``None`` """ - self._delete(_firewall_policy.FirewallPolicy, firewall_policy, - ignore_missing=ignore_missing) + self._delete( + _firewall_policy.FirewallPolicy, + firewall_policy, + ignore_missing=ignore_missing, + ) def find_firewall_policy(self, name_or_id, ignore_missing=True, **query): """Find a single firewall policy @@ -4011,8 +4387,12 @@ def find_firewall_policy(self, name_or_id, ignore_missing=True, **query): :class:`~openstack.network.v2.firewall_policy.FirewallPolicy` or None """ - return self._find(_firewall_policy.FirewallPolicy, - name_or_id, ignore_missing=ignore_missing, **query) + return self._find( + _firewall_policy.FirewallPolicy, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_firewall_policy(self, firewall_policy): """Get a single firewall policy @@ -4059,11 +4439,17 @@ def update_firewall_policy(self, firewall_policy, **attrs): :returns: The updated firewall policy :rtype: :class:`~openstack.network.v2.firewall_policy.FirewallPolicy` """ - return self._update(_firewall_policy.FirewallPolicy, firewall_policy, - **attrs) + return self._update( + _firewall_policy.FirewallPolicy, firewall_policy, **attrs + ) - def insert_rule_into_policy(self, firewall_policy_id, firewall_rule_id, - insert_after=None, insert_before=None): + def insert_rule_into_policy( + self, + firewall_policy_id, + firewall_rule_id, + insert_after=None, + insert_before=None, + ): """Insert a firewall_rule into a firewall_policy in order :param firewall_policy_id: The ID of the firewall policy. @@ -4077,11 +4463,14 @@ def insert_rule_into_policy(self, firewall_policy_id, firewall_rule_id, :returns: The updated firewall policy :rtype: :class:`~openstack.network.v2.firewall_policy.FirewallPolicy` """ - body = {'firewall_rule_id': firewall_rule_id, - 'insert_after': insert_after, - 'insert_before': insert_before} - policy = self._get_resource(_firewall_policy.FirewallPolicy, - firewall_policy_id) + body = { + 'firewall_rule_id': firewall_rule_id, + 'insert_after': insert_after, + 'insert_before': insert_before, + } + policy = self._get_resource( + _firewall_policy.FirewallPolicy, firewall_policy_id + ) return policy.insert_rule(self, **body) def remove_rule_from_policy(self, firewall_policy_id, firewall_rule_id): @@ -4094,8 +4483,9 @@ def remove_rule_from_policy(self, firewall_policy_id, firewall_rule_id): :rtype: :class:`~openstack.network.v2.firewall_policy.FirewallPolicy` """ body = {'firewall_rule_id': firewall_rule_id} - policy = self._get_resource(_firewall_policy.FirewallPolicy, - firewall_policy_id) + policy = self._get_resource( + _firewall_policy.FirewallPolicy, firewall_policy_id + ) return policy.remove_rule(self, **body) def create_firewall_rule(self, **attrs): @@ -4125,8 +4515,11 @@ def delete_firewall_rule(self, firewall_rule, ignore_missing=True): :returns: ``None`` """ - self._delete(_firewall_rule.FirewallRule, firewall_rule, - ignore_missing=ignore_missing) + self._delete( + _firewall_rule.FirewallRule, + firewall_rule, + ignore_missing=ignore_missing, + ) def find_firewall_rule(self, name_or_id, ignore_missing=True, **query): """Find a single firewall rule @@ -4143,8 +4536,12 @@ def find_firewall_rule(self, name_or_id, ignore_missing=True, **query): :class:`~openstack.network.v2.firewall_rule.FirewallRule` or None """ - return self._find(_firewall_rule.FirewallRule, - name_or_id, ignore_missing=ignore_missing, **query) + return self._find( + _firewall_rule.FirewallRule, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_firewall_rule(self, firewall_rule): """Get a single firewall rule @@ -4202,8 +4599,9 @@ def update_firewall_rule(self, firewall_rule, **attrs): :returns: The updated firewall rule :rtype: :class:`~openstack.network.v2.firewall_rule.FirewallRule` """ - return self._update(_firewall_rule.FirewallRule, firewall_rule, - **attrs) + return self._update( + _firewall_rule.FirewallRule, firewall_rule, **attrs + ) def create_security_group(self, **attrs): """Create a new security group from attributes @@ -4217,8 +4615,9 @@ def create_security_group(self, **attrs): """ return self._create(_security_group.SecurityGroup, **attrs) - def delete_security_group(self, security_group, ignore_missing=True, - if_revision=None): + def delete_security_group( + self, security_group, ignore_missing=True, if_revision=None + ): """Delete a security group :param security_group: @@ -4235,8 +4634,12 @@ def delete_security_group(self, security_group, ignore_missing=True, :returns: ``None`` """ - self._delete(_security_group.SecurityGroup, security_group, - ignore_missing=ignore_missing, if_revision=if_revision) + self._delete( + _security_group.SecurityGroup, + security_group, + ignore_missing=ignore_missing, + if_revision=if_revision, + ) def find_security_group(self, name_or_id, ignore_missing=True, **query): """Find a single security group @@ -4253,8 +4656,12 @@ def find_security_group(self, name_or_id, ignore_missing=True, **query): :class:`~openstack.network.v2.security_group.SecurityGroup` or None """ - return self._find(_security_group.SecurityGroup, name_or_id, - ignore_missing=ignore_missing, **query) + return self._find( + _security_group.SecurityGroup, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_security_group(self, security_group): """Get a single security group @@ -4301,8 +4708,12 @@ def update_security_group(self, security_group, if_revision=None, **attrs): :returns: The updated security group :rtype: :class:`~openstack.network.v2.security_group.SecurityGroup` """ - return self._update(_security_group.SecurityGroup, security_group, - if_revision=if_revision, **attrs) + return self._update( + _security_group.SecurityGroup, + security_group, + if_revision=if_revision, + **attrs, + ) def create_security_group_rule(self, **attrs): """Create a new security group rule from attributes @@ -4333,8 +4744,9 @@ def create_security_group_rules(self, data): """ return self._bulk_create(_security_group_rule.SecurityGroupRule, data) - def delete_security_group_rule(self, security_group_rule, - ignore_missing=True, if_revision=None): + def delete_security_group_rule( + self, security_group_rule, ignore_missing=True, if_revision=None + ): """Delete a security group rule :param security_group_rule: @@ -4352,12 +4764,16 @@ def delete_security_group_rule(self, security_group_rule, :returns: ``None`` """ - self._delete(_security_group_rule.SecurityGroupRule, - security_group_rule, ignore_missing=ignore_missing, - if_revision=if_revision) + self._delete( + _security_group_rule.SecurityGroupRule, + security_group_rule, + ignore_missing=ignore_missing, + if_revision=if_revision, + ) - def find_security_group_rule(self, name_or_id, ignore_missing=True, - **query): + def find_security_group_rule( + self, name_or_id, ignore_missing=True, **query + ): """Find a single security group rule :param str name_or_id: The ID of a security group rule. @@ -4372,8 +4788,12 @@ def find_security_group_rule(self, name_or_id, ignore_missing=True, :class:`~openstack.network.v2.security_group_rule.SecurityGroupRule` or None """ - return self._find(_security_group_rule.SecurityGroupRule, - name_or_id, ignore_missing=ignore_missing, **query) + return self._find( + _security_group_rule.SecurityGroupRule, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_security_group_rule(self, security_group_rule): """Get a single security group rule @@ -4388,8 +4808,9 @@ def get_security_group_rule(self, security_group_rule): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get(_security_group_rule.SecurityGroupRule, - security_group_rule) + return self._get( + _security_group_rule.SecurityGroupRule, security_group_rule + ) def security_group_rules(self, **query): """Return a generator of security group rules @@ -4454,8 +4875,12 @@ def find_segment(self, name_or_id, ignore_missing=True, **query): underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.segment.Segment` or None """ - return self._find(_segment.Segment, name_or_id, - ignore_missing=ignore_missing, **query) + return self._find( + _segment.Segment, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_segment(self, segment): """Get a single segment @@ -4541,8 +4966,11 @@ def delete_service_profile(self, service_profile, ignore_missing=True): :returns: ``None`` """ - self._delete(_service_profile.ServiceProfile, service_profile, - ignore_missing=ignore_missing) + self._delete( + _service_profile.ServiceProfile, + service_profile, + ignore_missing=ignore_missing, + ) def find_service_profile(self, name_or_id, ignore_missing=True, **query): """Find a single network service flavor profile @@ -4559,8 +4987,12 @@ def find_service_profile(self, name_or_id, ignore_missing=True, **query): :class:`~openstack.network.v2.service_profile.ServiceProfile` or None """ - return self._find(_service_profile.ServiceProfile, name_or_id, - ignore_missing=ignore_missing, **query) + return self._find( + _service_profile.ServiceProfile, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_service_profile(self, service_profile): """Get a single network service flavor profile @@ -4604,8 +5036,9 @@ def update_service_profile(self, service_profile, **attrs): :returns: The updated service profile :rtype: :class:`~openstack.network.v2.service_profile.ServiceProfile` """ - return self._update(_service_profile.ServiceProfile, service_profile, - **attrs) + return self._update( + _service_profile.ServiceProfile, service_profile, **attrs + ) def create_subnet(self, **attrs): """Create a new subnet from attributes @@ -4634,8 +5067,12 @@ def delete_subnet(self, subnet, ignore_missing=True, if_revision=None): :returns: ``None`` """ - self._delete(_subnet.Subnet, subnet, ignore_missing=ignore_missing, - if_revision=if_revision) + self._delete( + _subnet.Subnet, + subnet, + ignore_missing=ignore_missing, + if_revision=if_revision, + ) def find_subnet(self, name_or_id, ignore_missing=True, **query): """Find a single subnet @@ -4650,8 +5087,9 @@ def find_subnet(self, name_or_id, ignore_missing=True, **query): underlying methods. such as query filters. :returns: One :class:`~openstack.network.v2.subnet.Subnet` or None """ - return self._find(_subnet.Subnet, name_or_id, - ignore_missing=ignore_missing, **query) + return self._find( + _subnet.Subnet, name_or_id, ignore_missing=ignore_missing, **query + ) def get_subnet(self, subnet): """Get a single subnet @@ -4702,8 +5140,9 @@ def update_subnet(self, subnet, if_revision=None, **attrs): :returns: The updated subnet :rtype: :class:`~openstack.network.v2.subnet.Subnet` """ - return self._update(_subnet.Subnet, subnet, if_revision=if_revision, - **attrs) + return self._update( + _subnet.Subnet, subnet, if_revision=if_revision, **attrs + ) def create_subnet_pool(self, **attrs): """Create a new subnet pool from attributes @@ -4730,8 +5169,9 @@ def delete_subnet_pool(self, subnet_pool, ignore_missing=True): :returns: ``None`` """ - self._delete(_subnet_pool.SubnetPool, subnet_pool, - ignore_missing=ignore_missing) + self._delete( + _subnet_pool.SubnetPool, subnet_pool, ignore_missing=ignore_missing + ) def find_subnet_pool(self, name_or_id, ignore_missing=True, **query): """Find a single subnet pool @@ -4747,8 +5187,12 @@ def find_subnet_pool(self, name_or_id, ignore_missing=True, **query): :returns: One :class:`~openstack.network.v2.subnet_pool.SubnetPool` or None """ - return self._find(_subnet_pool.SubnetPool, name_or_id, - ignore_missing=ignore_missing, **query) + return self._find( + _subnet_pool.SubnetPool, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_subnet_pool(self, subnet_pool): """Get a single subnet pool @@ -4801,8 +5245,9 @@ def _check_tag_support(resource): resource.tags except AttributeError: raise exceptions.InvalidRequest( - '%s resource does not support tag' % - resource.__class__.__name__) + '%s resource does not support tag' + % resource.__class__.__name__ + ) def set_tags(self, resource, tags): """Replace tags of a specified resource with specified tags @@ -4854,8 +5299,9 @@ def find_trunk(self, name_or_id, ignore_missing=True, **query): :returns: One :class:`~openstack.network.v2.trunk.Trunk` or None """ - return self._find(_trunk.Trunk, name_or_id, - ignore_missing=ignore_missing, **query) + return self._find( + _trunk.Trunk, name_or_id, ignore_missing=ignore_missing, **query + ) def get_trunk(self, trunk): """Get a single trunk @@ -4948,8 +5394,7 @@ def create_vpn_endpoint_group(self, **attrs): :rtype: :class:`~openstack.network.v2.vpn_endpoint_group.VpnEndpointGroup` """ - return self._create( - _vpn_endpoint_group.VpnEndpointGroup, **attrs) + return self._create(_vpn_endpoint_group.VpnEndpointGroup, **attrs) def delete_vpn_endpoint_group( self, vpn_endpoint_group, ignore_missing=True @@ -4969,8 +5414,10 @@ def delete_vpn_endpoint_group( :returns: ``None`` """ self._delete( - _vpn_endpoint_group.VpnEndpointGroup, vpn_endpoint_group, - ignore_missing=ignore_missing) + _vpn_endpoint_group.VpnEndpointGroup, + vpn_endpoint_group, + ignore_missing=ignore_missing, + ) def find_vpn_endpoint_group( self, name_or_id, ignore_missing=True, **query @@ -4990,8 +5437,11 @@ def find_vpn_endpoint_group( or None """ return self._find( - _vpn_endpoint_group.VpnEndpointGroup, name_or_id, - ignore_missing=ignore_missing, **query) + _vpn_endpoint_group.VpnEndpointGroup, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_vpn_endpoint_group(self, vpn_endpoint_group): """Get a single vpn service @@ -5007,7 +5457,8 @@ def get_vpn_endpoint_group(self, vpn_endpoint_group): when no resource can be found. """ return self._get( - _vpn_endpoint_group.VpnEndpointGroup, vpn_endpoint_group) + _vpn_endpoint_group.VpnEndpointGroup, vpn_endpoint_group + ) def vpn_endpoint_groups(self, **query): """Return a generator of vpn services @@ -5035,7 +5486,8 @@ def update_vpn_endpoint_group(self, vpn_endpoint_group, **attrs): :class:`~openstack.network.v2.vpn_endpoint_group.VpnEndpointGroup` """ return self._update( - _vpn_endpoint_group.VpnEndpointGroup, vpn_endpoint_group, **attrs) + _vpn_endpoint_group.VpnEndpointGroup, vpn_endpoint_group, **attrs + ) # ========== IPsec Site Connection ========== def create_vpn_ipsec_site_connection(self, **attrs): @@ -5050,8 +5502,8 @@ def create_vpn_ipsec_site_connection(self, **attrs): :class:`~openstack.network.v2.vpn_ipsec_site_connection.VpnIPSecSiteConnection` """ return self._create( - _ipsec_site_connection.VpnIPSecSiteConnection, - **attrs) + _ipsec_site_connection.VpnIPSecSiteConnection, **attrs + ) def find_vpn_ipsec_site_connection( self, name_or_id, ignore_missing=True, **query @@ -5072,7 +5524,10 @@ def find_vpn_ipsec_site_connection( """ return self._find( _ipsec_site_connection.VpnIPSecSiteConnection, - name_or_id, ignore_missing=ignore_missing, **query) + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_vpn_ipsec_site_connection(self, ipsec_site_connection): """Get a single IPsec site connection @@ -5089,7 +5544,8 @@ def get_vpn_ipsec_site_connection(self, ipsec_site_connection): """ return self._get( _ipsec_site_connection.VpnIPSecSiteConnection, - ipsec_site_connection) + ipsec_site_connection, + ) def vpn_ipsec_site_connections(self, **query): """Return a generator of IPsec site connections @@ -5102,7 +5558,8 @@ def vpn_ipsec_site_connections(self, **query): :class:`~openstack.network.v2.vpn_ipsec_site_connection.VpnIPSecSiteConnection` """ return self._list( - _ipsec_site_connection.VpnIPSecSiteConnection, **query) + _ipsec_site_connection.VpnIPSecSiteConnection, **query + ) def update_vpn_ipsec_site_connection(self, ipsec_site_connection, **attrs): """Update a IPsec site connection @@ -5120,7 +5577,9 @@ def update_vpn_ipsec_site_connection(self, ipsec_site_connection, **attrs): """ return self._update( _ipsec_site_connection.VpnIPSecSiteConnection, - ipsec_site_connection, **attrs) + ipsec_site_connection, + **attrs, + ) def delete_vpn_ipsec_site_connection( self, ipsec_site_connection, ignore_missing=True @@ -5142,7 +5601,9 @@ def delete_vpn_ipsec_site_connection( """ self._delete( _ipsec_site_connection.VpnIPSecSiteConnection, - ipsec_site_connection, ignore_missing=ignore_missing) + ipsec_site_connection, + ignore_missing=ignore_missing, + ) # ========== IKEPolicy ========== def create_vpn_ike_policy(self, **attrs): @@ -5155,12 +5616,9 @@ def create_vpn_ike_policy(self, **attrs): :returns: The results of ike policy creation :rtype: :class:`~openstack.network.v2.vpn_ike_policy.VpnIkePolicy` """ - return self._create( - _ike_policy.VpnIkePolicy, **attrs) + return self._create(_ike_policy.VpnIkePolicy, **attrs) - def find_vpn_ike_policy( - self, name_or_id, ignore_missing=True, **query - ): + def find_vpn_ike_policy(self, name_or_id, ignore_missing=True, **query): """Find a single ike policy :param name_or_id: The name or ID of an IKE policy. @@ -5175,8 +5633,11 @@ def find_vpn_ike_policy( :class:`~openstack.network.v2.vpn_ike_policy.VpnIkePolicy` or None. """ return self._find( - _ike_policy.VpnIkePolicy, name_or_id, - ignore_missing=ignore_missing, **query) + _ike_policy.VpnIkePolicy, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_vpn_ike_policy(self, ike_policy): """Get a single ike policy @@ -5191,8 +5652,7 @@ def get_vpn_ike_policy(self, ike_policy): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get( - _ike_policy.VpnIkePolicy, ike_policy) + return self._get(_ike_policy.VpnIkePolicy, ike_policy) def vpn_ike_policies(self, **query): """Return a generator of IKE policies @@ -5217,8 +5677,7 @@ def update_vpn_ike_policy(self, ike_policy, **attrs): :returns: The updated ike policy :rtype: :class:`~openstack.network.v2.vpn_ike_policy.VpnIkePolicy` """ - return self._update( - _ike_policy.VpnIkePolicy, ike_policy, **attrs) + return self._update(_ike_policy.VpnIkePolicy, ike_policy, **attrs) def delete_vpn_ike_policy(self, ike_policy, ignore_missing=True): """Delete an IKE policy @@ -5236,8 +5695,8 @@ def delete_vpn_ike_policy(self, ike_policy, ignore_missing=True): :returns: ``None`` """ self._delete( - _ike_policy.VpnIkePolicy, ike_policy, - ignore_missing=ignore_missing) + _ike_policy.VpnIkePolicy, ike_policy, ignore_missing=ignore_missing + ) # ========== IPSecPolicy ========== def create_vpn_ipsec_policy(self, **attrs): @@ -5250,12 +5709,9 @@ def create_vpn_ipsec_policy(self, **attrs): :returns: The results of IPsec policy creation :rtype: :class:`~openstack.network.v2.vpn_ipsec_policy.VpnIpsecPolicy` """ - return self._create( - _ipsec_policy.VpnIpsecPolicy, **attrs) + return self._create(_ipsec_policy.VpnIpsecPolicy, **attrs) - def find_vpn_ipsec_policy( - self, name_or_id, ignore_missing=True, **query - ): + def find_vpn_ipsec_policy(self, name_or_id, ignore_missing=True, **query): """Find a single IPsec policy :param name_or_id: The name or ID of an IPsec policy. @@ -5271,8 +5727,11 @@ def find_vpn_ipsec_policy( or None. """ return self._find( - _ipsec_policy.VpnIpsecPolicy, name_or_id, - ignore_missing=ignore_missing, **query) + _ipsec_policy.VpnIpsecPolicy, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_vpn_ipsec_policy(self, ipsec_policy): """Get a single IPsec policy @@ -5287,8 +5746,7 @@ def get_vpn_ipsec_policy(self, ipsec_policy): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get( - _ipsec_policy.VpnIpsecPolicy, ipsec_policy) + return self._get(_ipsec_policy.VpnIpsecPolicy, ipsec_policy) def vpn_ipsec_policies(self, **query): """Return a generator of IPsec policies @@ -5299,8 +5757,7 @@ def vpn_ipsec_policies(self, **query): :returns: A generator of IPsec policy objects :rtype: :class:`~openstack.network.v2.vpn_ipsec_policy.VpnIpsecPolicy` """ - return self._list( - _ipsec_policy.VpnIpsecPolicy, **query) + return self._list(_ipsec_policy.VpnIpsecPolicy, **query) def update_vpn_ipsec_policy(self, ipsec_policy, **attrs): """Update an IPsec policy @@ -5315,7 +5772,8 @@ def update_vpn_ipsec_policy(self, ipsec_policy, **attrs): :rtype: :class:`~openstack.network.v2.vpn_ipsec_policy.VpnIpsecPolicy` """ return self._update( - _ipsec_policy.VpnIpsecPolicy, ipsec_policy, **attrs) + _ipsec_policy.VpnIpsecPolicy, ipsec_policy, **attrs + ) def delete_vpn_ipsec_policy(self, ipsec_policy, ignore_missing=True): """Delete an IPsec policy @@ -5334,8 +5792,10 @@ def delete_vpn_ipsec_policy(self, ipsec_policy, ignore_missing=True): :returns: ``None`` """ self._delete( - _ipsec_policy.VpnIpsecPolicy, ipsec_policy, - ignore_missing=ignore_missing) + _ipsec_policy.VpnIpsecPolicy, + ipsec_policy, + ignore_missing=ignore_missing, + ) # ========== VPN Service ========== def create_vpn_service(self, **attrs): @@ -5364,8 +5824,9 @@ def delete_vpn_service(self, vpn_service, ignore_missing=True): :returns: ``None`` """ - self._delete(_vpn_service.VpnService, vpn_service, - ignore_missing=ignore_missing) + self._delete( + _vpn_service.VpnService, vpn_service, ignore_missing=ignore_missing + ) def find_vpn_service(self, name_or_id, ignore_missing=True, **query): """Find a single vpn service @@ -5381,8 +5842,12 @@ def find_vpn_service(self, name_or_id, ignore_missing=True, **query): :returns: One :class:`~openstack.network.v2.vpn_service.VpnService` or None """ - return self._find(_vpn_service.VpnService, name_or_id, - ignore_missing=ignore_missing, **query) + return self._find( + _vpn_service.VpnService, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_vpn_service(self, vpn_service): """Get a single vpn service @@ -5436,11 +5901,15 @@ def create_floating_ip_port_forwarding(self, floating_ip, **attrs): :rtype: :class:`~openstack.network.v2.port_forwarding.PortForwarding` """ floatingip = self._get_resource(_floating_ip.FloatingIP, floating_ip) - return self._create(_port_forwarding.PortForwarding, - floatingip_id=floatingip.id, **attrs) + return self._create( + _port_forwarding.PortForwarding, + floatingip_id=floatingip.id, + **attrs, + ) - def delete_floating_ip_port_forwarding(self, floating_ip, port_forwarding, - ignore_missing=True): + def delete_floating_ip_port_forwarding( + self, floating_ip, port_forwarding, ignore_missing=True + ): """Delete a floating IP port forwarding. :param floating_ip: The value can be either the ID of a floating ip @@ -5459,12 +5928,16 @@ def delete_floating_ip_port_forwarding(self, floating_ip, port_forwarding, :returns: ``None`` """ floatingip = self._get_resource(_floating_ip.FloatingIP, floating_ip) - self._delete(_port_forwarding.PortForwarding, - port_forwarding, ignore_missing=ignore_missing, - floatingip_id=floatingip.id) + self._delete( + _port_forwarding.PortForwarding, + port_forwarding, + ignore_missing=ignore_missing, + floatingip_id=floatingip.id, + ) - def find_floating_ip_port_forwarding(self, floating_ip, port_forwarding_id, - ignore_missing=True, **query): + def find_floating_ip_port_forwarding( + self, floating_ip, port_forwarding_id, ignore_missing=True, **query + ): """Find a floating ip port forwarding :param floating_ip: The value can be the ID of the Floating IP that the @@ -5483,9 +5956,13 @@ def find_floating_ip_port_forwarding(self, floating_ip, port_forwarding_id, or None """ floatingip = self._get_resource(_floating_ip.FloatingIP, floating_ip) - return self._find(_port_forwarding.PortForwarding, - port_forwarding_id, ignore_missing=ignore_missing, - floatingip_id=floatingip.id, **query) + return self._find( + _port_forwarding.PortForwarding, + port_forwarding_id, + ignore_missing=ignore_missing, + floatingip_id=floatingip.id, + **query, + ) def get_floating_ip_port_forwarding(self, floating_ip, port_forwarding): """Get a floating ip port forwarding @@ -5503,8 +5980,11 @@ def get_floating_ip_port_forwarding(self, floating_ip, port_forwarding): when no resource can be found. """ floatingip = self._get_resource(_floating_ip.FloatingIP, floating_ip) - return self._get(_port_forwarding.PortForwarding, port_forwarding, - floatingip_id=floatingip.id) + return self._get( + _port_forwarding.PortForwarding, + port_forwarding, + floatingip_id=floatingip.id, + ) def floating_ip_port_forwardings(self, floating_ip, **query): """Return a generator of floating ip port forwarding @@ -5520,11 +6000,15 @@ def floating_ip_port_forwardings(self, floating_ip, **query): :class:`~openstack.network.v2.port_forwarding.PortForwarding` """ floatingip = self._get_resource(_floating_ip.FloatingIP, floating_ip) - return self._list(_port_forwarding.PortForwarding, - floatingip_id=floatingip.id, **query) + return self._list( + _port_forwarding.PortForwarding, + floatingip_id=floatingip.id, + **query, + ) - def update_floating_ip_port_forwarding(self, floating_ip, port_forwarding, - **attrs): + def update_floating_ip_port_forwarding( + self, floating_ip, port_forwarding, **attrs + ): """Update a floating ip port forwarding :param floating_ip: The value can be the ID of the Floating IP that the @@ -5541,8 +6025,12 @@ def update_floating_ip_port_forwarding(self, floating_ip, port_forwarding, :rtype: :class:`~openstack.network.v2.port_forwarding.PortForwarding` """ floatingip = self._get_resource(_floating_ip.FloatingIP, floating_ip) - return self._update(_port_forwarding.PortForwarding, port_forwarding, - floatingip_id=floatingip.id, **attrs) + return self._update( + _port_forwarding.PortForwarding, + port_forwarding, + floatingip_id=floatingip.id, + **attrs, + ) def create_conntrack_helper(self, router, **attrs): """Create a new L3 conntrack helper from attributes @@ -5558,8 +6046,9 @@ def create_conntrack_helper(self, router, **attrs): :class:`~openstack.network.v2.l3_conntrack_helper.ConntrackHelper` """ router = self._get_resource(_router.Router, router) - return self._create(_l3_conntrack_helper.ConntrackHelper, - router_id=router.id, **attrs) + return self._create( + _l3_conntrack_helper.ConntrackHelper, router_id=router.id, **attrs + ) def conntrack_helpers(self, router, **query): """Return a generator of conntrack helpers @@ -5573,8 +6062,9 @@ def conntrack_helpers(self, router, **query): :class:`~openstack.network.v2.l3_conntrack_helper.ConntrackHelper` """ router = self._get_resource(_router.Router, router) - return self._list(_l3_conntrack_helper.ConntrackHelper, - router_id=router.id, **query) + return self._list( + _l3_conntrack_helper.ConntrackHelper, router_id=router.id, **query + ) def get_conntrack_helper(self, conntrack_helper, router): """Get a single L3 conntrack helper @@ -5592,8 +6082,11 @@ def get_conntrack_helper(self, conntrack_helper, router): when no resource can be found. """ router = self._get_resource(_router.Router, router) - return self._get(_l3_conntrack_helper.ConntrackHelper, - conntrack_helper, router_id=router.id) + return self._get( + _l3_conntrack_helper.ConntrackHelper, + conntrack_helper, + router_id=router.id, + ) def update_conntrack_helper(self, conntrack_helper, router, **attrs): """Update a L3 conntrack_helper @@ -5613,11 +6106,16 @@ def update_conntrack_helper(self, conntrack_helper, router, **attrs): """ router = self._get_resource(_router.Router, router) - return self._update(_l3_conntrack_helper.ConntrackHelper, - conntrack_helper, router_id=router.id, **attrs) + return self._update( + _l3_conntrack_helper.ConntrackHelper, + conntrack_helper, + router_id=router.id, + **attrs, + ) - def delete_conntrack_helper(self, conntrack_helper, router, - ignore_missing=True): + def delete_conntrack_helper( + self, conntrack_helper, router, ignore_missing=True + ): """Delete a L3 conntrack_helper :param conntrack_helper: The value can be the ID of a L3 conntrack @@ -5635,9 +6133,12 @@ def delete_conntrack_helper(self, conntrack_helper, router, :returns: ``None`` """ router = self._get_resource(_router.Router, router) - self._delete(_l3_conntrack_helper.ConntrackHelper, - conntrack_helper, router_id=router.id, - ignore_missing=ignore_missing) + self._delete( + _l3_conntrack_helper.ConntrackHelper, + conntrack_helper, + router_id=router.id, + ignore_missing=ignore_missing, + ) def create_tap_flow(self, **attrs): """Create a new Tap Flow from attributes""" @@ -5645,13 +6146,18 @@ def create_tap_flow(self, **attrs): def delete_tap_flow(self, tap_flow, ignore_missing=True): """Delete a Tap Flow""" - self._delete(_tap_flow.TapFlow, tap_flow, - ignore_missing=ignore_missing) + self._delete( + _tap_flow.TapFlow, tap_flow, ignore_missing=ignore_missing + ) def find_tap_flow(self, name_or_id, ignore_missing=True, **query): - """"Find a single Tap Service""" - return self._find(_tap_flow.TapFlow, name_or_id, - ignore_missing=ignore_missing, **query) + """ "Find a single Tap Service""" + return self._find( + _tap_flow.TapFlow, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_tap_flow(self, tap_flow): """Get a signle Tap Flow""" @@ -5671,13 +6177,18 @@ def create_tap_service(self, **attrs): def delete_tap_service(self, tap_service, ignore_missing=True): """Delete a Tap Service""" - self._delete(_tap_service.TapService, tap_service, - ignore_missing=ignore_missing) + self._delete( + _tap_service.TapService, tap_service, ignore_missing=ignore_missing + ) def find_tap_service(self, name_or_id, ignore_missing=True, **query): - """"Find a single Tap Service""" - return self._find(_tap_service.TapService, name_or_id, - ignore_missing=ignore_missing, **query) + """ "Find a single Tap Service""" + return self._find( + _tap_service.TapService, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) def get_tap_service(self, tap_service): """Get a signle Tap Service""" @@ -5692,15 +6203,16 @@ def tap_services(self, **query): return self._list(_tap_service.TapService, **query) def _get_cleanup_dependencies(self): - return { - 'network': { - 'before': ['identity'] - } - } - - def _service_cleanup(self, dry_run=True, client_status_queue=None, - identified_resources=None, - filters=None, resource_evaluation_fn=None): + return {'network': {'before': ['identity']}} + + def _service_cleanup( + self, + dry_run=True, + client_status_queue=None, + identified_resources=None, + filters=None, + resource_evaluation_fn=None, + ): project_id = self.get_project_id() # Delete floating_ips in the project if no filters defined OR all # filters are matching and port_id is empty @@ -5712,7 +6224,8 @@ def _service_cleanup(self, dry_run=True, client_status_queue=None, client_status_queue=client_status_queue, identified_resources=identified_resources, filters=filters, - resource_evaluation_fn=fip_cleanup_evaluation) + resource_evaluation_fn=fip_cleanup_evaluation, + ) # Delete (try to delete) all security groups in the project # Let's hope we can't drop SG in use @@ -5725,22 +6238,20 @@ def _service_cleanup(self, dry_run=True, client_status_queue=None, client_status_queue=client_status_queue, identified_resources=identified_resources, filters=filters, - resource_evaluation_fn=resource_evaluation_fn) + resource_evaluation_fn=resource_evaluation_fn, + ) # Networks are crazy, try to delete router+net+subnet # if there are no "other" ports allocated on the net for net in self.networks(project_id=project_id): network_has_ports_allocated = False router_if = list() - for port in self.ports( - project_id=project_id, - network_id=net.id - ): + for port in self.ports(project_id=project_id, network_id=net.id): self.log.debug('Looking at port %s' % port) if port.device_owner in [ 'network:router_interface', 'network:router_interface_distributed', - 'network:ha_router_replicated_interface' + 'network:ha_router_replicated_interface', ]: router_if.append(port) elif port.device_owner == 'network:dhcp': @@ -5768,7 +6279,8 @@ def _service_cleanup(self, dry_run=True, client_status_queue=None, client_status_queue=None, identified_resources=None, filters=filters, - resource_evaluation_fn=resource_evaluation_fn) + resource_evaluation_fn=resource_evaluation_fn, + ) if not network_must_be_deleted: # If not - check another net continue @@ -5780,8 +6292,8 @@ def _service_cleanup(self, dry_run=True, client_status_queue=None, if not dry_run: try: self.remove_interface_from_router( - router=port.device_id, - port_id=port.id) + router=port.device_id, port_id=port.id + ) except exceptions.SDKException: self.log.error('Cannot delete object %s' % obj) # router disconnected, drop it @@ -5792,12 +6304,10 @@ def _service_cleanup(self, dry_run=True, client_status_queue=None, client_status_queue=client_status_queue, identified_resources=identified_resources, filters=None, - resource_evaluation_fn=None) + resource_evaluation_fn=None, + ) # Drop ports not belonging to anybody - for port in self.ports( - project_id=project_id, - network_id=net.id - ): + for port in self.ports(project_id=project_id, network_id=net.id): if port.device_owner is None or port.device_owner == '': self._service_cleanup_del_res( self.delete_port, @@ -5806,13 +6316,11 @@ def _service_cleanup(self, dry_run=True, client_status_queue=None, client_status_queue=client_status_queue, identified_resources=identified_resources, filters=None, - resource_evaluation_fn=None) + resource_evaluation_fn=None, + ) # Drop all subnets in the net (no further conditions) - for obj in self.subnets( - project_id=project_id, - network_id=net.id - ): + for obj in self.subnets(project_id=project_id, network_id=net.id): self._service_cleanup_del_res( self.delete_subnet, obj, @@ -5820,7 +6328,8 @@ def _service_cleanup(self, dry_run=True, client_status_queue=None, client_status_queue=client_status_queue, identified_resources=identified_resources, filters=None, - resource_evaluation_fn=None) + resource_evaluation_fn=None, + ) # And now the network itself (we are here definitely only if we # need that) @@ -5831,7 +6340,8 @@ def _service_cleanup(self, dry_run=True, client_status_queue=None, client_status_queue=client_status_queue, identified_resources=identified_resources, filters=None, - resource_evaluation_fn=None) + resource_evaluation_fn=None, + ) # It might happen, that we have routers not attached to anything for obj in self.routers(): @@ -5844,7 +6354,8 @@ def _service_cleanup(self, dry_run=True, client_status_queue=None, client_status_queue=client_status_queue, identified_resources=identified_resources, filters=None, - resource_evaluation_fn=None) + resource_evaluation_fn=None, + ) def fip_cleanup_evaluation(obj, identified_resources=None, filters=None): @@ -5855,13 +6366,10 @@ def fip_cleanup_evaluation(obj, identified_resources=None, filters=None): identified by other services for deletion. :param dict filters: dictionary with parameters """ - if ( - filters is not None - and ( - obj.port_id is not None - and identified_resources - and obj.port_id not in identified_resources - ) + if filters is not None and ( + obj.port_id is not None + and identified_resources + and obj.port_id not in identified_resources ): # If filters are set, but port is not empty and will not be empty - # skip diff --git a/openstack/network/v2/address_group.py b/openstack/network/v2/address_group.py index 3fb256f5f..73bbc6344 100644 --- a/openstack/network/v2/address_group.py +++ b/openstack/network/v2/address_group.py @@ -17,6 +17,7 @@ class AddressGroup(resource.Resource): """Address group extension.""" + resource_key = 'address_group' resources_key = 'address_groups' base_path = '/address-groups' @@ -31,9 +32,11 @@ class AddressGroup(resource.Resource): _allow_unknown_attrs_in_body = True _query_mapping = resource.QueryParameters( - "sort_key", "sort_dir", - 'name', 'description', - 'project_id' + "sort_key", + "sort_dir", + 'name', + 'description', + 'project_id', ) # Properties diff --git a/openstack/network/v2/address_scope.py b/openstack/network/v2/address_scope.py index 1ad1cbd18..3b1aac26e 100644 --- a/openstack/network/v2/address_scope.py +++ b/openstack/network/v2/address_scope.py @@ -15,6 +15,7 @@ class AddressScope(resource.Resource): """Address scope extension.""" + resource_key = 'address_scope' resources_key = 'address_scopes' base_path = '/address-scopes' @@ -29,7 +30,8 @@ class AddressScope(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'name', 'ip_version', + 'name', + 'ip_version', 'project_id', is_shared='shared', ) diff --git a/openstack/network/v2/agent.py b/openstack/network/v2/agent.py index 3c39ddb2b..ff5f664da 100644 --- a/openstack/network/v2/agent.py +++ b/openstack/network/v2/agent.py @@ -17,6 +17,7 @@ class Agent(resource.Resource): """Neutron agent extension.""" + resource_key = 'agent' resources_key = 'agents' base_path = '/agents' @@ -32,9 +33,14 @@ class Agent(resource.Resource): # NOTE: We skip query for JSON fields and datetime fields _query_mapping = resource.QueryParameters( - 'agent_type', 'availability_zone', 'binary', 'description', 'host', + 'agent_type', + 'availability_zone', + 'binary', + 'description', + 'host', 'topic', - is_admin_state_up='admin_state_up', is_alive='alive', + is_admin_state_up='admin_state_up', + is_alive='alive', ) # Properties @@ -85,8 +91,9 @@ def add_agent_to_network(self, session, network_id): def remove_agent_from_network(self, session, network_id): body = {'network_id': network_id} - url = utils.urljoin(self.base_path, self.id, 'dhcp-networks', - network_id) + url = utils.urljoin( + self.base_path, self.id, 'dhcp-networks', network_id + ) session.delete(url, json=body) def add_router_to_agent(self, session, router): diff --git a/openstack/network/v2/availability_zone.py b/openstack/network/v2/availability_zone.py index 3e5449d45..9879d53b4 100644 --- a/openstack/network/v2/availability_zone.py +++ b/openstack/network/v2/availability_zone.py @@ -30,7 +30,9 @@ class AvailabilityZone(_resource.Resource): # NOTE: We don't support query by state yet because there is a mapping # at neutron side difficult to map. _query_mapping = _resource.QueryParameters( - name='availability_zone', resource='agent_type') + name='availability_zone', + resource='agent_type', + ) # Properties #: Name of the availability zone. diff --git a/openstack/network/v2/bgp_speaker.py b/openstack/network/v2/bgp_speaker.py index ad553e4af..8c96764a1 100644 --- a/openstack/network/v2/bgp_speaker.py +++ b/openstack/network/v2/bgp_speaker.py @@ -43,7 +43,8 @@ class BgpSpeaker(resource.Resource): #: Whether to enable or disable the advertisement of floating ip host #: routes by the BGP Speaker. True by default. advertise_floating_ip_host_routes = resource.Body( - 'advertise_floating_ip_host_routes') + 'advertise_floating_ip_host_routes' + ) #: Whether to enable or disable the advertisement of tenant network #: routes by the BGP Speaker. True by default. advertise_tenant_networks = resource.Body('advertise_tenant_networks') @@ -164,6 +165,5 @@ def remove_bgp_speaker_from_dragent(self, session, bgp_agent_id): :param bgp_agent_id: The id of the dynamic routing agent from which remove the speaker. """ - url = utils.urljoin('agents', bgp_agent_id, - 'bgp-drinstances', self.id) + url = utils.urljoin('agents', bgp_agent_id, 'bgp-drinstances', self.id) session.delete(url) diff --git a/openstack/network/v2/firewall_group.py b/openstack/network/v2/firewall_group.py index 93db5b404..4f16c1c35 100644 --- a/openstack/network/v2/firewall_group.py +++ b/openstack/network/v2/firewall_group.py @@ -31,9 +31,15 @@ class FirewallGroup(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'description', 'egress_firewall_policy_id', - 'ingress_firewall_policy_id', 'name', 'shared', 'status', 'ports', - 'project_id') + 'description', + 'egress_firewall_policy_id', + 'ingress_firewall_policy_id', + 'name', + 'shared', + 'status', + 'ports', + 'project_id', + ) # Properties #: The administrative state of the firewall group, which is up (true) or diff --git a/openstack/network/v2/firewall_policy.py b/openstack/network/v2/firewall_policy.py index d4dc182e8..4d760bc08 100644 --- a/openstack/network/v2/firewall_policy.py +++ b/openstack/network/v2/firewall_policy.py @@ -33,7 +33,12 @@ class FirewallPolicy(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'description', 'firewall_rules', 'name', 'project_id', 'shared') + 'description', + 'firewall_rules', + 'name', + 'project_id', + 'shared', + ) # Properties #: Each time that the firewall policy or its associated rules are changed, diff --git a/openstack/network/v2/firewall_rule.py b/openstack/network/v2/firewall_rule.py index 8d5ed5056..da2df0d95 100644 --- a/openstack/network/v2/firewall_rule.py +++ b/openstack/network/v2/firewall_rule.py @@ -31,9 +31,20 @@ class FirewallRule(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'action', 'description', 'destination_ip_address', 'name', - 'destination_port', 'enabled', 'ip_version', 'project_id', 'protocol', - 'shared', 'source_ip_address', 'source_port', 'firewall_policy_id') + 'action', + 'description', + 'destination_ip_address', + 'name', + 'destination_port', + 'enabled', + 'ip_version', + 'project_id', + 'protocol', + 'shared', + 'source_ip_address', + 'source_port', + 'firewall_policy_id', + ) # Properties #: The action that the API performs on traffic that matches the firewall diff --git a/openstack/network/v2/flavor.py b/openstack/network/v2/flavor.py index 361465c4d..97f35efb4 100644 --- a/openstack/network/v2/flavor.py +++ b/openstack/network/v2/flavor.py @@ -29,7 +29,11 @@ class Flavor(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'description', 'name', 'service_type', is_enabled='enabled') + 'description', + 'name', + 'service_type', + is_enabled='enabled', + ) # properties #: description for the flavor @@ -44,7 +48,8 @@ class Flavor(resource.Resource): service_profile_ids = resource.Body('service_profiles', type=list) def associate_flavor_with_service_profile( - self, session, service_profile_id=None): + self, session, service_profile_id=None + ): flavor_id = self.id url = utils.urljoin(self.base_path, flavor_id, 'service_profiles') body = {"service_profile": {"id": service_profile_id}} @@ -52,9 +57,13 @@ def associate_flavor_with_service_profile( return resp.json() def disassociate_flavor_from_service_profile( - self, session, service_profile_id=None): + self, session, service_profile_id=None + ): flavor_id = self.id url = utils.urljoin( - self.base_path, flavor_id, 'service_profiles', service_profile_id) - session.delete(url,) + self.base_path, flavor_id, 'service_profiles', service_profile_id + ) + session.delete( + url, + ) return None diff --git a/openstack/network/v2/floating_ip.py b/openstack/network/v2/floating_ip.py index 76383b2f6..17db58ea7 100644 --- a/openstack/network/v2/floating_ip.py +++ b/openstack/network/v2/floating_ip.py @@ -30,12 +30,19 @@ class FloatingIP(_base.NetworkResource, tag.TagMixin): # For backward compatibility include tenant_id as query param _query_mapping = resource.QueryParameters( - 'description', 'fixed_ip_address', - 'floating_ip_address', 'floating_network_id', - 'port_id', 'router_id', 'status', 'subnet_id', - 'project_id', 'tenant_id', + 'description', + 'fixed_ip_address', + 'floating_ip_address', + 'floating_network_id', + 'port_id', + 'router_id', + 'status', + 'subnet_id', + 'project_id', + 'tenant_id', tenant_id='project_id', - **tag.TagMixin._tag_query_parameters) + **tag.TagMixin._tag_query_parameters + ) # Properties #: Timestamp at which the floating IP was created. diff --git a/openstack/network/v2/health_monitor.py b/openstack/network/v2/health_monitor.py index 7859d0b12..bf853cfdf 100644 --- a/openstack/network/v2/health_monitor.py +++ b/openstack/network/v2/health_monitor.py @@ -28,8 +28,14 @@ class HealthMonitor(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'delay', 'expected_codes', 'http_method', 'max_retries', - 'timeout', 'type', 'url_path', 'project_id', + 'delay', + 'expected_codes', + 'http_method', + 'max_retries', + 'timeout', + 'type', + 'url_path', + 'project_id', is_admin_state_up='adminstate_up', ) diff --git a/openstack/network/v2/listener.py b/openstack/network/v2/listener.py index 376650a0e..d46ff5788 100644 --- a/openstack/network/v2/listener.py +++ b/openstack/network/v2/listener.py @@ -28,9 +28,15 @@ class Listener(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'connection_limit', 'default_pool_id', 'default_tls_container_ref', - 'description', 'name', 'project_id', 'protocol', 'protocol_port', - is_admin_state_up='admin_state_up' + 'connection_limit', + 'default_pool_id', + 'default_tls_container_ref', + 'description', + 'name', + 'project_id', + 'protocol', + 'protocol_port', + is_admin_state_up='admin_state_up', ) # Properties diff --git a/openstack/network/v2/load_balancer.py b/openstack/network/v2/load_balancer.py index 6d4e07b3c..08f505b0d 100644 --- a/openstack/network/v2/load_balancer.py +++ b/openstack/network/v2/load_balancer.py @@ -28,9 +28,15 @@ class LoadBalancer(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'description', 'name', 'project_id', 'provider', 'provisioning_status', - 'tenant_id', 'vip_address', 'vip_subnet_id', - is_admin_state_up='admin_state_up' + 'description', + 'name', + 'project_id', + 'provider', + 'provisioning_status', + 'tenant_id', + 'vip_address', + 'vip_subnet_id', + is_admin_state_up='admin_state_up', ) # Properties diff --git a/openstack/network/v2/local_ip.py b/openstack/network/v2/local_ip.py index b2393b367..74beb123c 100644 --- a/openstack/network/v2/local_ip.py +++ b/openstack/network/v2/local_ip.py @@ -18,6 +18,7 @@ class LocalIP(resource.Resource): """Local IP extension.""" + resource_name = "local ip" resource_key = "local_ip" resources_key = "local_ips" @@ -33,10 +34,14 @@ class LocalIP(resource.Resource): _allow_unknown_attrs_in_body = True _query_mapping = resource.QueryParameters( - "sort_key", "sort_dir", - 'name', 'description', - 'project_id', 'network_id', - 'local_port_id', 'local_ip_address', + "sort_key", + "sort_dir", + 'name', + 'description', + 'project_id', + 'network_id', + 'local_port_id', + 'local_ip_address', 'ip_mode', ) diff --git a/openstack/network/v2/local_ip_association.py b/openstack/network/v2/local_ip_association.py index 310c6d6f2..c8ecfad09 100644 --- a/openstack/network/v2/local_ip_association.py +++ b/openstack/network/v2/local_ip_association.py @@ -18,6 +18,7 @@ class LocalIPAssociation(resource.Resource): """Local IP extension.""" + resource_key = "port_association" resources_key = "port_associations" base_path = "/local_ips/%(local_ip_id)s/port_associations" @@ -32,8 +33,11 @@ class LocalIPAssociation(resource.Resource): _allow_unknown_attrs_in_body = True _query_mapping = resource.QueryParameters( - 'fixed_port_id', 'fixed_ip', 'host', + 'fixed_port_id', + 'fixed_ip', + 'host', ) + # Properties #: The fixed port ID. fixed_port_id = resource.Body('fixed_port_id') diff --git a/openstack/network/v2/metering_label.py b/openstack/network/v2/metering_label.py index 6bedb4ad3..0bef6b11c 100644 --- a/openstack/network/v2/metering_label.py +++ b/openstack/network/v2/metering_label.py @@ -28,7 +28,9 @@ class MeteringLabel(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'description', 'name', 'project_id', + 'description', + 'name', + 'project_id', is_shared='shared', ) diff --git a/openstack/network/v2/metering_label_rule.py b/openstack/network/v2/metering_label_rule.py index a4c286345..85c7e6ae4 100644 --- a/openstack/network/v2/metering_label_rule.py +++ b/openstack/network/v2/metering_label_rule.py @@ -28,8 +28,12 @@ class MeteringLabelRule(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'direction', 'metering_label_id', 'remote_ip_prefix', - 'source_ip_prefix', 'destination_ip_prefix', 'project_id', + 'direction', + 'metering_label_id', + 'remote_ip_prefix', + 'source_ip_prefix', + 'destination_ip_prefix', + 'project_id', ) # Properties @@ -49,13 +53,15 @@ class MeteringLabelRule(resource.Resource): tenant_id = resource.Body('tenant_id', deprecated=True) #: The remote IP prefix to be associated with this metering label rule. remote_ip_prefix = resource.Body( - 'remote_ip_prefix', deprecated=True, + 'remote_ip_prefix', + deprecated=True, deprecation_reason="The use of 'remote_ip_prefix' in metering label " - "rules is deprecated and will be removed in future " - "releases. One should use instead, the " - "'source_ip_prefix' and/or 'destination_ip_prefix' " - "parameters. For more details, you can check the " - "spec: https://review.opendev.org/#/c/744702/.") + "rules is deprecated and will be removed in future " + "releases. One should use instead, the " + "'source_ip_prefix' and/or 'destination_ip_prefix' " + "parameters. For more details, you can check the " + "spec: https://review.opendev.org/#/c/744702/.", + ) #: The source IP prefix to be associated with this metering label rule. source_ip_prefix = resource.Body('source_ip_prefix') diff --git a/openstack/network/v2/ndp_proxy.py b/openstack/network/v2/ndp_proxy.py index cff0b8302..ae75927cf 100644 --- a/openstack/network/v2/ndp_proxy.py +++ b/openstack/network/v2/ndp_proxy.py @@ -29,9 +29,15 @@ class NDPProxy(resource.Resource): _allow_unknown_attrs_in_body = True _query_mapping = resource.QueryParameters( - "sort_key", "sort_dir", - 'name', 'description', 'project_id', - 'router_id', 'port_id', 'ip_address') + "sort_key", + "sort_dir", + 'name', + 'description', + 'project_id', + 'router_id', + 'port_id', + 'ip_address', + ) # Properties #: Timestamp at which the NDP proxy was created. diff --git a/openstack/network/v2/network.py b/openstack/network/v2/network.py index 074bd39c1..9f9f94f0a 100644 --- a/openstack/network/v2/network.py +++ b/openstack/network/v2/network.py @@ -28,7 +28,9 @@ class Network(_base.NetworkResource, tag.TagMixin): # NOTE: We don't support query on list or datetime fields yet _query_mapping = resource.QueryParameters( - 'description', 'name', 'status', + 'description', + 'name', + 'status', 'project_id', ipv4_address_scope_id='ipv4_address_scope', ipv6_address_scope_id='ipv6_address_scope', @@ -68,13 +70,14 @@ class Network(_base.NetworkResource, tag.TagMixin): #: The port security status, which is enabled ``True`` or disabled #: ``False``. *Type: bool* *Default: False* #: Available for multiple provider extensions. - is_port_security_enabled = resource.Body('port_security_enabled', - type=bool, - default=False) + is_port_security_enabled = resource.Body( + 'port_security_enabled', type=bool, default=False + ) #: Whether or not the router is external. #: *Type: bool* *Default: False* - is_router_external = resource.Body('router:external', type=bool, - default=False) + is_router_external = resource.Body( + 'router:external', type=bool, default=False + ) #: Indicates whether this network is shared across all tenants. #: By default, only administrative users can change this value. #: *Type: bool* diff --git a/openstack/network/v2/network_ip_availability.py b/openstack/network/v2/network_ip_availability.py index a1b1d8d41..8410ece5a 100644 --- a/openstack/network/v2/network_ip_availability.py +++ b/openstack/network/v2/network_ip_availability.py @@ -29,8 +29,10 @@ class NetworkIPAvailability(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'ip_version', 'network_id', 'network_name', - 'project_id' + 'ip_version', + 'network_id', + 'network_name', + 'project_id', ) # Properties diff --git a/openstack/network/v2/network_segment_range.py b/openstack/network/v2/network_segment_range.py index bb7e7ff41..67b844537 100644 --- a/openstack/network/v2/network_segment_range.py +++ b/openstack/network/v2/network_segment_range.py @@ -31,9 +31,16 @@ class NetworkSegmentRange(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'name', 'default', 'shared', 'project_id', - 'network_type', 'physical_network', 'minimum', 'maximum', - 'used', 'available' + 'name', + 'default', + 'shared', + 'project_id', + 'network_type', + 'physical_network', + 'minimum', + 'maximum', + 'used', + 'available', ) # Properties diff --git a/openstack/network/v2/pool.py b/openstack/network/v2/pool.py index e5215a712..7d2ee31da 100644 --- a/openstack/network/v2/pool.py +++ b/openstack/network/v2/pool.py @@ -28,8 +28,14 @@ class Pool(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'description', 'lb_algorithm', 'name', - 'protocol', 'provider', 'subnet_id', 'virtual_ip_id', 'listener_id', + 'description', + 'lb_algorithm', + 'name', + 'protocol', + 'provider', + 'subnet_id', + 'virtual_ip_id', + 'listener_id', 'project_id', is_admin_state_up='admin_state_up', load_balancer_id='loadbalancer_id', diff --git a/openstack/network/v2/pool_member.py b/openstack/network/v2/pool_member.py index aba6fb9cc..02f01e6e5 100644 --- a/openstack/network/v2/pool_member.py +++ b/openstack/network/v2/pool_member.py @@ -28,7 +28,11 @@ class PoolMember(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'address', 'name', 'protocol_port', 'subnet_id', 'weight', + 'address', + 'name', + 'protocol_port', + 'subnet_id', + 'weight', 'project_id', is_admin_state_up='admin_state_up', ) diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index 30de433f0..c0fba5d2d 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -30,11 +30,25 @@ class Port(_base.NetworkResource, tag.TagMixin): # NOTE: we skip query on list or datetime fields for now _query_mapping = resource.QueryParameters( - 'binding:host_id', 'binding:profile', 'binding:vif_details', - 'binding:vif_type', 'binding:vnic_type', - 'description', 'device_id', 'device_owner', 'fields', 'fixed_ips', - 'id', 'ip_address', 'mac_address', 'name', 'network_id', 'status', - 'subnet_id', 'project_id', 'security_groups', + 'binding:host_id', + 'binding:profile', + 'binding:vif_details', + 'binding:vif_type', + 'binding:vnic_type', + 'description', + 'device_id', + 'device_owner', + 'fields', + 'fixed_ips', + 'id', + 'ip_address', + 'mac_address', + 'name', + 'network_id', + 'status', + 'subnet_id', + 'project_id', + 'security_groups', is_admin_state_up='admin_state_up', is_port_security_enabled='port_security_enabled', security_group_ids='security_groups', @@ -44,8 +58,9 @@ class Port(_base.NetworkResource, tag.TagMixin): # Properties #: Allowed address pairs list. Dictionary key ``ip_address`` is required #: and key ``mac_address`` is optional. - allowed_address_pairs: List[dict] = resource.Body('allowed_address_pairs', - type=list) + allowed_address_pairs: List[dict] = resource.Body( + 'allowed_address_pairs', type=list + ) #: The ID of the host where the port is allocated. In some cases, #: different implementations can run on different hosts. binding_host_id = resource.Body('binding:host_id') @@ -104,8 +119,9 @@ class Port(_base.NetworkResource, tag.TagMixin): is_admin_state_up = resource.Body('admin_state_up', type=bool) #: The port security status, which is enabled ``True`` or disabled #: ``False``. *Type: bool* *Default: False* - is_port_security_enabled = resource.Body('port_security_enabled', - type=bool, default=False) + is_port_security_enabled = resource.Body( + 'port_security_enabled', type=bool, default=False + ) #: The MAC address of an allowed address pair. mac_address = resource.Body('mac_address') #: The port name. @@ -120,8 +136,9 @@ class Port(_base.NetworkResource, tag.TagMixin): #: Tenant_id (deprecated attribute). tenant_id = resource.Body('tenant_id', deprecated=True) #: Whether to propagate uplink status of the port. *Type: bool* - propagate_uplink_status = resource.Body('propagate_uplink_status', - type=bool) + propagate_uplink_status = resource.Body( + 'propagate_uplink_status', type=bool + ) #: Read-only. The ID of the QoS policy attached to the network where the # port is bound. qos_network_policy_id = resource.Body('qos_network_policy_id') diff --git a/openstack/network/v2/port_forwarding.py b/openstack/network/v2/port_forwarding.py index aca7873fc..77b92928b 100644 --- a/openstack/network/v2/port_forwarding.py +++ b/openstack/network/v2/port_forwarding.py @@ -30,7 +30,9 @@ class PortForwarding(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'internal_port_id', 'external_port', 'protocol' + 'internal_port_id', + 'external_port', + 'protocol', ) # Properties diff --git a/openstack/network/v2/qos_policy.py b/openstack/network/v2/qos_policy.py index ec5e98918..6967735e4 100644 --- a/openstack/network/v2/qos_policy.py +++ b/openstack/network/v2/qos_policy.py @@ -29,7 +29,9 @@ class QoSPolicy(resource.Resource, tag.TagMixin): allow_list = True _query_mapping = resource.QueryParameters( - 'name', 'description', 'is_default', + 'name', + 'description', + 'is_default', 'project_id', is_shared='shared', **tag.TagMixin._tag_query_parameters diff --git a/openstack/network/v2/qos_rule_type.py b/openstack/network/v2/qos_rule_type.py index c115b8d6b..7aff99e7d 100644 --- a/openstack/network/v2/qos_rule_type.py +++ b/openstack/network/v2/qos_rule_type.py @@ -28,7 +28,11 @@ class QoSRuleType(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'type', 'drivers', 'all_rules', 'all_supported') + 'type', + 'drivers', + 'all_rules', + 'all_supported', + ) # Properties #: QoS rule type name. diff --git a/openstack/network/v2/quota.py b/openstack/network/v2/quota.py index 1c0c827df..f3b79743a 100644 --- a/openstack/network/v2/quota.py +++ b/openstack/network/v2/quota.py @@ -60,10 +60,12 @@ class Quota(resource.Resource): #: The maximum amount of security groups you can create. *Type: int* security_groups = resource.Body('security_group', type=int) - def _prepare_request(self, requires_id=True, prepend_key=False, - base_path=None, **kwargs): - _request = super(Quota, self)._prepare_request(requires_id, - prepend_key) + def _prepare_request( + self, requires_id=True, prepend_key=False, base_path=None, **kwargs + ): + _request = super(Quota, self)._prepare_request( + requires_id, prepend_key + ) if self.resource_key in _request.body: _body = _request.body[self.resource_key] else: diff --git a/openstack/network/v2/rbac_policy.py b/openstack/network/v2/rbac_policy.py index f3178dcd9..00ac27692 100644 --- a/openstack/network/v2/rbac_policy.py +++ b/openstack/network/v2/rbac_policy.py @@ -28,8 +28,12 @@ class RBACPolicy(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'action', 'object_id', 'object_type', 'project_id', - 'target_project_id', target_project_id='target_tenant', + 'action', + 'object_id', + 'object_type', + 'project_id', + 'target_project_id', + target_project_id='target_tenant', ) # Properties diff --git a/openstack/network/v2/router.py b/openstack/network/v2/router.py index 9b5ef557c..30e10ca98 100644 --- a/openstack/network/v2/router.py +++ b/openstack/network/v2/router.py @@ -30,7 +30,11 @@ class Router(_base.NetworkResource, tag.TagMixin): # NOTE: We don't support query on datetime, list or dict fields _query_mapping = resource.QueryParameters( - 'description', 'flavor_id', 'name', 'status', 'project_id', + 'description', + 'flavor_id', + 'name', + 'status', + 'project_id', is_admin_state_up='admin_state_up', is_distributed='distributed', is_ha='ha', @@ -40,8 +44,9 @@ class Router(_base.NetworkResource, tag.TagMixin): # Properties #: Availability zone hints to use when scheduling the router. #: *Type: list of availability zone names* - availability_zone_hints = resource.Body('availability_zone_hints', - type=list) + availability_zone_hints = resource.Body( + 'availability_zone_hints', type=list + ) #: Availability zones for the router. #: *Type: list of availability zone names* availability_zones = resource.Body('availability_zones', type=list) @@ -155,8 +160,7 @@ def add_gateway(self, session, **body): :returns: The body of the response as a dictionary. """ - url = utils.urljoin(self.base_path, self.id, - 'add_gateway_router') + url = utils.urljoin(self.base_path, self.id, 'add_gateway_router') resp = session.put(url, json=body) return resp.json() @@ -169,8 +173,7 @@ def remove_gateway(self, session, **body): :returns: The body of the response as a dictionary. """ - url = utils.urljoin(self.base_path, self.id, - 'remove_gateway_router') + url = utils.urljoin(self.base_path, self.id, 'remove_gateway_router') resp = session.put(url, json=body) return resp.json() @@ -188,4 +191,5 @@ class L3AgentRouter(Router): allow_delete = False allow_list = True + # NOTE: No query parameter is supported diff --git a/openstack/network/v2/security_group.py b/openstack/network/v2/security_group.py index cf2d23534..1cc9aaabf 100644 --- a/openstack/network/v2/security_group.py +++ b/openstack/network/v2/security_group.py @@ -27,8 +27,16 @@ class SecurityGroup(_base.NetworkResource, tag.TagMixin): allow_list = True _query_mapping = resource.QueryParameters( - 'description', 'fields', 'id', 'name', 'stateful', 'project_id', - 'tenant_id', 'revision_number', 'sort_dir', 'sort_key', + 'description', + 'fields', + 'id', + 'name', + 'stateful', + 'project_id', + 'tenant_id', + 'revision_number', + 'sort_dir', + 'sort_key', **tag.TagMixin._tag_query_parameters ) diff --git a/openstack/network/v2/security_group_rule.py b/openstack/network/v2/security_group_rule.py index dd5b4c8e1..369a70de2 100644 --- a/openstack/network/v2/security_group_rule.py +++ b/openstack/network/v2/security_group_rule.py @@ -27,13 +27,21 @@ class SecurityGroupRule(_base.NetworkResource, tag.TagMixin): allow_list = True _query_mapping = resource.QueryParameters( - 'description', 'direction', 'id', 'protocol', - 'remote_group_id', 'security_group_id', + 'description', + 'direction', + 'id', + 'protocol', + 'remote_group_id', + 'security_group_id', 'remote_address_group_id', - 'port_range_max', 'port_range_min', - 'remote_ip_prefix', 'revision_number', - 'project_id', 'tenant_id', - 'sort_dir', 'sort_key', + 'port_range_max', + 'port_range_min', + 'remote_ip_prefix', + 'revision_number', + 'project_id', + 'tenant_id', + 'sort_dir', + 'sort_key', ether_type='ethertype', **tag.TagMixin._tag_query_parameters ) @@ -89,7 +97,8 @@ class SecurityGroupRule(_base.NetworkResource, tag.TagMixin): def _prepare_request(self, *args, **kwargs): _request = super(SecurityGroupRule, self)._prepare_request( - *args, **kwargs) + *args, **kwargs + ) # Old versions of Neutron do not handle being passed a # remote_address_group_id and raise and error. Remove it from # the body if it is blank. diff --git a/openstack/network/v2/segment.py b/openstack/network/v2/segment.py index 81a18996d..62cd325b3 100644 --- a/openstack/network/v2/segment.py +++ b/openstack/network/v2/segment.py @@ -28,8 +28,12 @@ class Segment(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'description', 'name', 'network_id', 'network_type', - 'physical_network', 'segmentation_id', + 'description', + 'name', + 'network_id', + 'network_type', + 'physical_network', + 'segmentation_id', ) # Properties diff --git a/openstack/network/v2/service_profile.py b/openstack/network/v2/service_profile.py index 26cf7bc6b..b6502164f 100644 --- a/openstack/network/v2/service_profile.py +++ b/openstack/network/v2/service_profile.py @@ -28,7 +28,9 @@ class ServiceProfile(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'description', 'driver', 'project_id', + 'description', + 'driver', + 'project_id', is_enabled='enabled', ) # Properties diff --git a/openstack/network/v2/service_provider.py b/openstack/network/v2/service_provider.py index a5e9260ee..ef2e4b004 100644 --- a/openstack/network/v2/service_provider.py +++ b/openstack/network/v2/service_provider.py @@ -27,8 +27,9 @@ class ServiceProvider(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'service_type', 'name', - is_default='default' + 'service_type', + 'name', + is_default='default', ) # Properties diff --git a/openstack/network/v2/subnet.py b/openstack/network/v2/subnet.py index d219e1ec8..8d83feb27 100644 --- a/openstack/network/v2/subnet.py +++ b/openstack/network/v2/subnet.py @@ -28,9 +28,17 @@ class Subnet(_base.NetworkResource, tag.TagMixin): # NOTE: Query on list or datetime fields are currently not supported. _query_mapping = resource.QueryParameters( - 'cidr', 'description', 'gateway_ip', 'ip_version', - 'ipv6_address_mode', 'ipv6_ra_mode', 'name', 'network_id', - 'segment_id', 'dns_publish_fixed_ip', 'project_id', + 'cidr', + 'description', + 'gateway_ip', + 'ip_version', + 'ipv6_address_mode', + 'ipv6_ra_mode', + 'name', + 'network_id', + 'segment_id', + 'dns_publish_fixed_ip', + 'project_id', is_dhcp_enabled='enable_dhcp', subnet_pool_id='subnetpool_id', use_default_subnet_pool='use_default_subnetpool', @@ -87,6 +95,5 @@ class Subnet(_base.NetworkResource, tag.TagMixin): updated_at = resource.Body('updated_at') #: Whether to use the default subnet pool to obtain a CIDR. use_default_subnet_pool = resource.Body( - 'use_default_subnetpool', - type=bool + 'use_default_subnetpool', type=bool ) diff --git a/openstack/network/v2/subnet_pool.py b/openstack/network/v2/subnet_pool.py index 99a24da32..22e3e05e0 100644 --- a/openstack/network/v2/subnet_pool.py +++ b/openstack/network/v2/subnet_pool.py @@ -28,8 +28,12 @@ class SubnetPool(resource.Resource, tag.TagMixin): allow_list = True _query_mapping = resource.QueryParameters( - 'address_scope_id', 'description', 'ip_version', 'is_default', - 'name', 'project_id', + 'address_scope_id', + 'description', + 'ip_version', + 'is_default', + 'name', + 'project_id', is_shared='shared', **tag.TagMixin._tag_query_parameters ) diff --git a/openstack/network/v2/tap_flow.py b/openstack/network/v2/tap_flow.py index 9707170db..e892d69fa 100644 --- a/openstack/network/v2/tap_flow.py +++ b/openstack/network/v2/tap_flow.py @@ -30,8 +30,10 @@ class TapFlow(resource.Resource): _allow_unknown_attrs_in_body = True _query_mapping = resource.QueryParameters( - "sort_key", "sort_dir", - 'name', 'project_id' + "sort_key", + "sort_dir", + 'name', + 'project_id', ) # Properties diff --git a/openstack/network/v2/tap_service.py b/openstack/network/v2/tap_service.py index 527b1e0a6..ecb2825fb 100644 --- a/openstack/network/v2/tap_service.py +++ b/openstack/network/v2/tap_service.py @@ -30,8 +30,7 @@ class TapService(resource.Resource): _allow_unknown_attrs_in_body = True _query_mapping = resource.QueryParameters( - "sort_key", "sort_dir", - 'name', 'project_id' + "sort_key", "sort_dir", 'name', 'project_id' ) # Properties diff --git a/openstack/network/v2/trunk.py b/openstack/network/v2/trunk.py index 82f1bede8..2ec34c681 100644 --- a/openstack/network/v2/trunk.py +++ b/openstack/network/v2/trunk.py @@ -30,7 +30,11 @@ class Trunk(resource.Resource, tag.TagMixin): allow_list = True _query_mapping = resource.QueryParameters( - 'name', 'description', 'port_id', 'status', 'sub_ports', + 'name', + 'description', + 'port_id', + 'status', + 'sub_ports', 'project_id', is_admin_state_up='admin_state_up', **tag.TagMixin._tag_query_parameters diff --git a/openstack/network/v2/vpn_endpoint_group.py b/openstack/network/v2/vpn_endpoint_group.py index 12a0a4e59..063f9e301 100644 --- a/openstack/network/v2/vpn_endpoint_group.py +++ b/openstack/network/v2/vpn_endpoint_group.py @@ -28,8 +28,11 @@ class VpnEndpointGroup(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'description', 'name', 'project_id', 'tenant_id', - type='endpoint_type' + 'description', + 'name', + 'project_id', + 'tenant_id', + type='endpoint_type', ) # Properties diff --git a/openstack/network/v2/vpn_ike_policy.py b/openstack/network/v2/vpn_ike_policy.py index 9cc585ae0..db1ff6f7f 100644 --- a/openstack/network/v2/vpn_ike_policy.py +++ b/openstack/network/v2/vpn_ike_policy.py @@ -15,6 +15,7 @@ class VpnIkePolicy(resource.Resource): """VPN IKE policy extension.""" + resource_key = 'ikepolicy' resources_key = 'ikepolicies' base_path = '/vpn/ikepolicies' @@ -27,8 +28,14 @@ class VpnIkePolicy(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'auth_algorithm', 'description', 'encryption_algorithm', 'ike_version', - 'name', 'pfs', 'project_id', 'phase1_negotiation_mode', + 'auth_algorithm', + 'description', + 'encryption_algorithm', + 'ike_version', + 'name', + 'pfs', + 'project_id', + 'phase1_negotiation_mode', ) # Properties diff --git a/openstack/network/v2/vpn_ipsec_policy.py b/openstack/network/v2/vpn_ipsec_policy.py index 5b685b33e..9c9e09ec9 100644 --- a/openstack/network/v2/vpn_ipsec_policy.py +++ b/openstack/network/v2/vpn_ipsec_policy.py @@ -26,8 +26,13 @@ class VpnIpsecPolicy(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'auth_algorithm', 'description', 'encryption_algorithm', 'name', 'pfs', - 'project_id', 'phase1_negotiation_mode', + 'auth_algorithm', + 'description', + 'encryption_algorithm', + 'name', + 'pfs', + 'project_id', + 'phase1_negotiation_mode', ) # Properties diff --git a/openstack/network/v2/vpn_ipsec_site_connection.py b/openstack/network/v2/vpn_ipsec_site_connection.py index f74c2c5ee..ed2bc31e8 100644 --- a/openstack/network/v2/vpn_ipsec_site_connection.py +++ b/openstack/network/v2/vpn_ipsec_site_connection.py @@ -26,11 +26,24 @@ class VpnIPSecSiteConnection(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'auth_mode', 'description', 'ikepolicy_id', 'ipsecpolicy_id', - 'initiator', 'local_ep_group_id', 'peer_address', 'local_id', - 'mtu', 'name', 'peer_id', 'project_id', 'psk', 'peer_ep_group_id', - 'route_mode', 'vpnservice_id', 'status', - is_admin_state_up='admin_state_up' + 'auth_mode', + 'description', + 'ikepolicy_id', + 'ipsecpolicy_id', + 'initiator', + 'local_ep_group_id', + 'peer_address', + 'local_id', + 'mtu', + 'name', + 'peer_id', + 'project_id', + 'psk', + 'peer_ep_group_id', + 'route_mode', + 'vpnservice_id', + 'status', + is_admin_state_up='admin_state_up', ) # Properties diff --git a/openstack/network/v2/vpn_service.py b/openstack/network/v2/vpn_service.py index ef887b5a2..c03c8a6f0 100644 --- a/openstack/network/v2/vpn_service.py +++ b/openstack/network/v2/vpn_service.py @@ -28,9 +28,15 @@ class VpnService(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'description', 'external_v4_ip', 'external_v6_ip', 'name', 'router_id', - 'project_id', 'tenant_id', 'subnet_id', - is_admin_state_up='admin_state_up' + 'description', + 'external_v4_ip', + 'external_v6_ip', + 'name', + 'router_id', + 'project_id', + 'tenant_id', + 'subnet_id', + is_admin_state_up='admin_state_up', ) # Properties diff --git a/openstack/tests/functional/network/v2/test_address_group.py b/openstack/tests/functional/network/v2/test_address_group.py index 66ac5885a..933e86834 100644 --- a/openstack/tests/functional/network/v2/test_address_group.py +++ b/openstack/tests/functional/network/v2/test_address_group.py @@ -46,13 +46,15 @@ def setUp(self): def tearDown(self): sot = self.user_cloud.network.delete_address_group( - self.ADDRESS_GROUP_ID) + self.ADDRESS_GROUP_ID + ) self.assertIsNone(sot) super(TestAddressGroup, self).tearDown() def test_find(self): sot = self.user_cloud.network.find_address_group( - self.ADDRESS_GROUP_NAME) + self.ADDRESS_GROUP_NAME + ) self.assertEqual(self.ADDRESS_GROUP_ID, sot.id) def test_get(self): diff --git a/openstack/tests/functional/network/v2/test_address_scope.py b/openstack/tests/functional/network/v2/test_address_scope.py index 9b5193be7..76ac60611 100644 --- a/openstack/tests/functional/network/v2/test_address_scope.py +++ b/openstack/tests/functional/network/v2/test_address_scope.py @@ -36,13 +36,15 @@ def setUp(self): def tearDown(self): sot = self.user_cloud.network.delete_address_scope( - self.ADDRESS_SCOPE_ID) + self.ADDRESS_SCOPE_ID + ) self.assertIsNone(sot) super(TestAddressScope, self).tearDown() def test_find(self): sot = self.user_cloud.network.find_address_scope( - self.ADDRESS_SCOPE_NAME) + self.ADDRESS_SCOPE_NAME + ) self.assertEqual(self.ADDRESS_SCOPE_ID, sot.id) def test_get(self): diff --git a/openstack/tests/functional/network/v2/test_agent_add_remove_network.py b/openstack/tests/functional/network/v2/test_agent_add_remove_network.py index 10cdbcd84..723e227f2 100644 --- a/openstack/tests/functional/network/v2/test_agent_add_remove_network.py +++ b/openstack/tests/functional/network/v2/test_agent_add_remove_network.py @@ -54,12 +54,14 @@ def test_add_remove_agent(self): def _verify_add(self, network): net = self.user_cloud.network.dhcp_agent_hosting_networks( - self.AGENT_ID) + self.AGENT_ID + ) net_ids = [n.id for n in net] self.assertIn(self.NETWORK_ID, net_ids) def _verify_remove(self, network): net = self.user_cloud.network.dhcp_agent_hosting_networks( - self.AGENT_ID) + self.AGENT_ID + ) net_ids = [n.id for n in net] self.assertNotIn(self.NETWORK_ID, net_ids) diff --git a/openstack/tests/functional/network/v2/test_agent_add_remove_router.py b/openstack/tests/functional/network/v2/test_agent_add_remove_router.py index 0b1ddca37..20313a67a 100644 --- a/openstack/tests/functional/network/v2/test_agent_add_remove_router.py +++ b/openstack/tests/functional/network/v2/test_agent_add_remove_router.py @@ -27,7 +27,8 @@ def setUp(self): self.ROUTER_NAME = "router-name-" + self.getUniqueString("router-name") self.ROUTER = self.user_cloud.network.create_router( - name=self.ROUTER_NAME) + name=self.ROUTER_NAME + ) self.addCleanup(self.user_cloud.network.delete_router, self.ROUTER) assert isinstance(self.ROUTER, router.Router) agent_list = list(self.user_cloud.network.agents()) @@ -47,7 +48,8 @@ def test_add_router_to_agent(self): def test_remove_router_from_agent(self): self.user_cloud.network.remove_router_from_agent( - self.AGENT, self.ROUTER) + self.AGENT, self.ROUTER + ) rots = self.user_cloud.network.agent_hosted_routers(self.AGENT) routers = [router.id for router in rots] self.assertNotIn(self.ROUTER.id, routers) diff --git a/openstack/tests/functional/network/v2/test_auto_allocated_topology.py b/openstack/tests/functional/network/v2/test_auto_allocated_topology.py index ab961e8d6..73c070bfb 100644 --- a/openstack/tests/functional/network/v2/test_auto_allocated_topology.py +++ b/openstack/tests/functional/network/v2/test_auto_allocated_topology.py @@ -32,13 +32,14 @@ def setUp(self): ) projects = [ - o.project_id - for o in self.operator_cloud.network.networks()] + o.project_id for o in self.operator_cloud.network.networks() + ] self.PROJECT_ID = projects[0] def tearDown(self): res = self.operator_cloud.network.delete_auto_allocated_topology( - self.PROJECT_ID) + self.PROJECT_ID + ) self.assertIsNone(res) super(TestAutoAllocatedTopology, self).tearDown() @@ -63,7 +64,8 @@ def test_show_no_project_option(self): def test_show_project_option(self): top = self.operator_cloud.network.get_auto_allocated_topology( - self.PROJECT_ID) + self.PROJECT_ID + ) network = self.operator_cloud.network.get_network(top.id) self.assertEqual(top.project_id, network.project_id) self.assertEqual(top.id, network.id) @@ -73,4 +75,5 @@ def _set_network_external(self, networks): for network in networks: if network.name == "public": self.operator_cloud.network.update_network( - network, is_default=True) + network, is_default=True + ) diff --git a/openstack/tests/functional/network/v2/test_bgp.py b/openstack/tests/functional/network/v2/test_bgp.py index a495f4107..5c1698620 100644 --- a/openstack/tests/functional/network/v2/test_bgp.py +++ b/openstack/tests/functional/network/v2/test_bgp.py @@ -16,7 +16,6 @@ class TestBGPSpeaker(base.BaseFunctionalTest): - def setUp(self): super().setUp() self.LOCAL_AS = 101 @@ -30,14 +29,19 @@ def setUp(self): self.skipTest("Neutron BGP Dynamic Routing Extension disabled") bgp_speaker = self.operator_cloud.network.create_bgp_speaker( - ip_version=self.IP_VERSION, local_as=self.LOCAL_AS, - name=self.SPEAKER_NAME) + ip_version=self.IP_VERSION, + local_as=self.LOCAL_AS, + name=self.SPEAKER_NAME, + ) assert isinstance(bgp_speaker, _bgp_speaker.BgpSpeaker) self.SPEAKER = bgp_speaker bgp_peer = self.operator_cloud.network.create_bgp_peer( - name=self.PEER_NAME, auth_type='none', - remote_as=self.REMOTE_AS, peer_ip=self.PEER_IP) + name=self.PEER_NAME, + auth_type='none', + remote_as=self.REMOTE_AS, + peer_ip=self.PEER_IP, + ) assert isinstance(bgp_peer, _bgp_peer.BgpPeer) self.PEER = bgp_peer @@ -62,13 +66,15 @@ def test_get_bgp_speaker(self): self.assertEqual(self.LOCAL_AS, sot.local_as) def test_list_bgp_speakers(self): - speaker_ids = [sp.id for sp in - self.operator_cloud.network.bgp_speakers()] + speaker_ids = [ + sp.id for sp in self.operator_cloud.network.bgp_speakers() + ] self.assertIn(self.SPEAKER.id, speaker_ids) def test_update_bgp_speaker(self): sot = self.operator_cloud.network.update_bgp_speaker( - self.SPEAKER.id, advertise_floating_ip_host_routes=False) + self.SPEAKER.id, advertise_floating_ip_host_routes=False + ) self.assertFalse(sot.advertise_floating_ip_host_routes) def test_find_bgp_peer(self): @@ -88,18 +94,21 @@ def test_list_bgp_peers(self): def test_update_bgp_peer(self): name = 'new_peer_name' + self.getUniqueString() sot = self.operator_cloud.network.update_bgp_peer( - self.PEER.id, name=name) + self.PEER.id, name=name + ) self.assertEqual(name, sot.name) def test_add_remove_peer_to_speaker(self): self.operator_cloud.network.add_bgp_peer_to_speaker( - self.SPEAKER.id, self.PEER.id) + self.SPEAKER.id, self.PEER.id + ) sot = self.operator_cloud.network.get_bgp_speaker(self.SPEAKER.id) self.assertEqual([self.PEER.id], sot.peers) # Remove the peer self.operator_cloud.network.remove_bgp_peer_from_speaker( - self.SPEAKER.id, self.PEER.id) + self.SPEAKER.id, self.PEER.id + ) sot = self.operator_cloud.network.get_bgp_speaker(self.SPEAKER.id) self.assertEqual([], sot.peers) @@ -107,17 +116,20 @@ def test_add_remove_gw_network_to_speaker(self): net_name = 'my_network' + self.getUniqueString() net = self.user_cloud.create_network(name=net_name) self.operator_cloud.network.add_gateway_network_to_speaker( - self.SPEAKER.id, net.id) + self.SPEAKER.id, net.id + ) sot = self.operator_cloud.network.get_bgp_speaker(self.SPEAKER.id) self.assertEqual([net.id], sot.networks) # Remove the network self.operator_cloud.network.remove_gateway_network_from_speaker( - self.SPEAKER.id, net.id) + self.SPEAKER.id, net.id + ) sot = self.operator_cloud.network.get_bgp_speaker(self.SPEAKER.id) self.assertEqual([], sot.networks) def test_get_advertised_routes_of_speaker(self): sot = self.operator_cloud.network.get_advertised_routes_of_speaker( - self.SPEAKER.id) + self.SPEAKER.id + ) self.assertEqual({'advertised_routes': []}, sot) diff --git a/openstack/tests/functional/network/v2/test_bgpvpn.py b/openstack/tests/functional/network/v2/test_bgpvpn.py index f706b4a29..176648182 100644 --- a/openstack/tests/functional/network/v2/test_bgpvpn.py +++ b/openstack/tests/functional/network/v2/test_bgpvpn.py @@ -11,11 +11,13 @@ # under the License. from openstack.network.v2 import bgpvpn as _bgpvpn -from openstack.network.v2 import bgpvpn_network_association as \ - _bgpvpn_net_assoc +from openstack.network.v2 import ( + bgpvpn_network_association as _bgpvpn_net_assoc, +) from openstack.network.v2 import bgpvpn_port_association as _bgpvpn_port_assoc -from openstack.network.v2 import bgpvpn_router_association as \ - _bgpvpn_router_assoc +from openstack.network.v2 import ( + bgpvpn_router_association as _bgpvpn_router_assoc, +) from openstack.network.v2 import network as _network from openstack.network.v2 import port as _port from openstack.network.v2 import router as _router @@ -24,7 +26,6 @@ class TestBGPVPN(base.BaseFunctionalTest): - def setUp(self): super().setUp() @@ -36,8 +37,8 @@ def setUp(self): self.CIDR = "10.101.0.0/24" self.ROUTE_DISTINGUISHERS = ['64512:1777', '64512:1888', '64512:1999'] self.VNI = 1000 - self.ROUTE_TARGETS = '64512:1444', - self.IMPORT_TARGETS = '64512:1555', + self.ROUTE_TARGETS = ('64512:1444',) + self.IMPORT_TARGETS = ('64512:1555',) self.EXPORT_TARGETS = '64512:1666' self.TYPE = 'l3' @@ -66,48 +67,60 @@ def setUp(self): self.SUBNET = subnet port = self.operator_cloud.network.create_port( - name=self.PORT_NAME, network_id=self.NETWORK.id) + name=self.PORT_NAME, network_id=self.NETWORK.id + ) assert isinstance(port, _port.Port) self.PORT = port router = self.operator_cloud.network.create_router( - name=self.ROUTER_NAME) + name=self.ROUTER_NAME + ) assert isinstance(router, _router.Router) self.ROUTER = router net_assoc = ( self.operator_cloud.network.create_bgpvpn_network_association( - self.BGPVPN, network_id=self.NETWORK.id)) - assert isinstance(net_assoc, - _bgpvpn_net_assoc.BgpVpnNetworkAssociation) + self.BGPVPN, network_id=self.NETWORK.id + ) + ) + assert isinstance( + net_assoc, _bgpvpn_net_assoc.BgpVpnNetworkAssociation + ) self.NET_ASSOC = net_assoc port_assoc = ( self.operator_cloud.network.create_bgpvpn_port_association( - self.BGPVPN, port_id=self.PORT.id)) - assert isinstance(port_assoc, - _bgpvpn_port_assoc.BgpVpnPortAssociation) + self.BGPVPN, port_id=self.PORT.id + ) + ) + assert isinstance(port_assoc, _bgpvpn_port_assoc.BgpVpnPortAssociation) self.PORT_ASSOC = port_assoc router_assoc = ( self.operator_cloud.network.create_bgpvpn_router_association( - self.BGPVPN, router_id=self.ROUTER.id)) - assert isinstance(router_assoc, - _bgpvpn_router_assoc.BgpVpnRouterAssociation) + self.BGPVPN, router_id=self.ROUTER.id + ) + ) + assert isinstance( + router_assoc, _bgpvpn_router_assoc.BgpVpnRouterAssociation + ) self.ROUTER_ASSOC = router_assoc def tearDown(self): sot = self.operator_cloud.network.delete_bgpvpn(self.BGPVPN.id) self.assertIsNone(sot) sot = self.operator_cloud.network.delete_bgpvpn_network_association( - self.BGPVPN.id, self.NET_ASSOC.id) + self.BGPVPN.id, self.NET_ASSOC.id + ) self.assertIsNone(sot) sot = self.operator_cloud.network.delete_bgpvpn_port_association( - self.BGPVPN.id, self.PORT_ASSOC.id) + self.BGPVPN.id, self.PORT_ASSOC.id + ) self.assertIsNone(sot) sot = self.operator_cloud.network.delete_bgpvpn_router_association( - self.BGPVPN.id, self.ROUTER_ASSOC.id) + self.BGPVPN.id, self.ROUTER_ASSOC.id + ) self.assertIsNone(sot) sot = self.operator_cloud.network.delete_router(self.ROUTER) @@ -135,47 +148,64 @@ def test_get_bgpvpn(self): self.assertEqual(list(self.IMPORT_TARGETS), sot.import_targets) def test_list_bgpvpns(self): - bgpvpn_ids = [bgpvpn.id for bgpvpn in - self.operator_cloud.network.bgpvpns()] + bgpvpn_ids = [ + bgpvpn.id for bgpvpn in self.operator_cloud.network.bgpvpns() + ] self.assertIn(self.BGPVPN.id, bgpvpn_ids) def test_update_bgpvpn(self): sot = self.operator_cloud.network.update_bgpvpn( - self.BGPVPN.id, import_targets='64512:1333') + self.BGPVPN.id, import_targets='64512:1333' + ) self.assertEqual(['64512:1333'], sot.import_targets) def test_get_bgpvpnnetwork_association(self): sot = self.operator_cloud.network.get_bgpvpn_network_association( - self.BGPVPN.id, self.NET_ASSOC.id) + self.BGPVPN.id, self.NET_ASSOC.id + ) self.assertEqual(self.NETWORK.id, sot.network_id) def test_list_bgpvpn_network_associations(self): net_assoc_ids = [ - net_assoc.id for net_assoc in - self.operator_cloud.network.bgpvpn_network_associations( - self.BGPVPN.id)] + net_assoc.id + for net_assoc in ( + self.operator_cloud.network.bgpvpn_network_associations( + self.BGPVPN.id + ) + ) + ] self.assertIn(self.NET_ASSOC.id, net_assoc_ids) def test_get_bgpvpn_port_association(self): sot = self.operator_cloud.network.get_bgpvpn_port_association( - self.BGPVPN.id, self.PORT_ASSOC.id) + self.BGPVPN.id, self.PORT_ASSOC.id + ) self.assertEqual(self.PORT.id, sot.port_id) def test_list_bgpvpn_port_associations(self): port_assoc_ids = [ - port_assoc.id for port_assoc in - self.operator_cloud.network.bgpvpn_port_associations( - self.BGPVPN.id)] + port_assoc.id + for port_assoc in ( + self.operator_cloud.network.bgpvpn_port_associations( + self.BGPVPN.id + ) + ) + ] self.assertIn(self.PORT_ASSOC.id, port_assoc_ids) def test_get_bgpvpn_router_association(self): sot = self.operator_cloud.network.get_bgpvpn_router_association( - self.BGPVPN.id, self.ROUTER_ASSOC.id) + self.BGPVPN.id, self.ROUTER_ASSOC.id + ) self.assertEqual(self.ROUTER.id, sot.router_id) def test_list_bgpvpn_router_associations(self): router_assoc_ids = [ - router_assoc.id for router_assoc in - self.operator_cloud.network.bgpvpn_router_associations( - self.BGPVPN.id)] + router_assoc.id + for router_assoc in ( + self.operator_cloud.network.bgpvpn_router_associations( + self.BGPVPN.id + ) + ) + ] self.assertIn(self.ROUTER_ASSOC.id, router_assoc_ids) diff --git a/openstack/tests/functional/network/v2/test_dvr_router.py b/openstack/tests/functional/network/v2/test_dvr_router.py index 1c4bf1b37..db595ab89 100644 --- a/openstack/tests/functional/network/v2/test_dvr_router.py +++ b/openstack/tests/functional/network/v2/test_dvr_router.py @@ -31,14 +31,16 @@ def setUp(self): self.NAME = self.getUniqueString() self.UPDATE_NAME = self.getUniqueString() sot = self.operator_cloud.network.create_router( - name=self.NAME, distributed=True) + name=self.NAME, distributed=True + ) assert isinstance(sot, router.Router) self.assertEqual(self.NAME, sot.name) self.ID = sot.id def tearDown(self): sot = self.operator_cloud.network.delete_router( - self.ID, ignore_missing=False) + self.ID, ignore_missing=False + ) self.assertIsNone(sot) super(TestDVRRouter, self).tearDown() @@ -60,5 +62,6 @@ def test_list(self): def test_update(self): sot = self.operator_cloud.network.update_router( - self.ID, name=self.UPDATE_NAME) + self.ID, name=self.UPDATE_NAME + ) self.assertEqual(self.UPDATE_NAME, sot.name) diff --git a/openstack/tests/functional/network/v2/test_firewall_rule_insert_remove_policy.py b/openstack/tests/functional/network/v2/test_firewall_rule_insert_remove_policy.py index 6eba7d65d..8be7e3508 100644 --- a/openstack/tests/functional/network/v2/test_firewall_rule_insert_remove_policy.py +++ b/openstack/tests/functional/network/v2/test_firewall_rule_insert_remove_policy.py @@ -34,15 +34,18 @@ def setUp(self): if not self.user_cloud._has_neutron_extension("fwaas_v2"): self.skipTest("fwaas_v2 service not supported by cloud") rul1 = self.user_cloud.network.create_firewall_rule( - name=self.RULE1_NAME) + name=self.RULE1_NAME + ) assert isinstance(rul1, firewall_rule.FirewallRule) self.assertEqual(self.RULE1_NAME, rul1.name) rul2 = self.user_cloud.network.create_firewall_rule( - name=self.RULE2_NAME) + name=self.RULE2_NAME + ) assert isinstance(rul2, firewall_rule.FirewallRule) self.assertEqual(self.RULE2_NAME, rul2.name) pol = self.user_cloud.network.create_firewall_policy( - name=self.POLICY_NAME) + name=self.POLICY_NAME + ) assert isinstance(pol, firewall_policy.FirewallPolicy) self.assertEqual(self.POLICY_NAME, pol.name) self.RULE1_ID = rul1.id diff --git a/openstack/tests/functional/network/v2/test_flavor.py b/openstack/tests/functional/network/v2/test_flavor.py index 1f8a8befb..c0106eb59 100644 --- a/openstack/tests/functional/network/v2/test_flavor.py +++ b/openstack/tests/functional/network/v2/test_flavor.py @@ -97,8 +97,7 @@ def test_associate_disassociate_flavor_with_service_profile(self): ) self.assertIsNotNone(response) - response = self.operator_cloud.network \ - .disassociate_flavor_from_service_profile( - self.ID, self.service_profiles.id - ) + response = self.operator_cloud.network.disassociate_flavor_from_service_profile( # noqa: E501 + self.ID, self.service_profiles.id + ) self.assertIsNone(response) diff --git a/openstack/tests/functional/network/v2/test_floating_ip.py b/openstack/tests/functional/network/v2/test_floating_ip.py index 1a0bda1a0..df73aa02c 100644 --- a/openstack/tests/functional/network/v2/test_floating_ip.py +++ b/openstack/tests/functional/network/v2/test_floating_ip.py @@ -95,10 +95,9 @@ def setUp(self): fip_args = dict( floating_network_id=self.EXT_NET_ID, ) - if ( - self.user_cloud._has_neutron_extension("dns-integration") - and self.user_cloud.has_service("dns") - ): + if self.user_cloud._has_neutron_extension( + "dns-integration" + ) and self.user_cloud.has_service("dns"): self.is_dns_supported = True fip_args.update( dict(dns_domain=self.DNS_DOMAIN, dns_name=self.DNS_NAME) diff --git a/openstack/tests/functional/network/v2/test_local_ip.py b/openstack/tests/functional/network/v2/test_local_ip.py index 9489a2df7..0a21c9a7d 100644 --- a/openstack/tests/functional/network/v2/test_local_ip.py +++ b/openstack/tests/functional/network/v2/test_local_ip.py @@ -55,8 +55,8 @@ def test_get(self): def test_list(self): names = [ - local_ip.name - for local_ip in self.user_cloud.network.local_ips()] + local_ip.name for local_ip in self.user_cloud.network.local_ips() + ] self.assertIn(self.LOCAL_IP_NAME, names) def test_update(self): diff --git a/openstack/tests/functional/network/v2/test_local_ip_association.py b/openstack/tests/functional/network/v2/test_local_ip_association.py index a3f2c019d..4976fc43c 100644 --- a/openstack/tests/functional/network/v2/test_local_ip_association.py +++ b/openstack/tests/functional/network/v2/test_local_ip_association.py @@ -31,12 +31,13 @@ def setUp(self): self.LOCAL_IP_ID = self.getUniqueString() self.FIXED_PORT_ID = self.getUniqueString() self.FIXED_IP = self.getUniqueString() - local_ip_association = self.user_cloud.network \ - .create_local_ip_association( + local_ip_association = ( + self.user_cloud.network.create_local_ip_association( local_ip=self.LOCAL_IP_ID, fixed_port_id=self.FIXED_PORT_ID, fixed_ip=self.FIXED_IP, ) + ) assert isinstance( local_ip_association, _local_ip_association.LocalIPAssociation ) diff --git a/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py b/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py index edd5fb1d1..3f0e43c1c 100644 --- a/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py @@ -12,7 +12,7 @@ from openstack.network.v2 import ( - qos_bandwidth_limit_rule as _qos_bandwidth_limit_rule + qos_bandwidth_limit_rule as _qos_bandwidth_limit_rule, ) from openstack.tests.functional import base diff --git a/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py b/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py index 6eca0cc38..ac4849774 100644 --- a/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py +++ b/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py @@ -12,7 +12,7 @@ from openstack.network.v2 import ( - qos_dscp_marking_rule as _qos_dscp_marking_rule + qos_dscp_marking_rule as _qos_dscp_marking_rule, ) from openstack.tests.functional import base diff --git a/openstack/tests/functional/network/v2/test_qos_minimum_bandwidth_rule.py b/openstack/tests/functional/network/v2/test_qos_minimum_bandwidth_rule.py index 5efbc8c2c..d293e9f2b 100644 --- a/openstack/tests/functional/network/v2/test_qos_minimum_bandwidth_rule.py +++ b/openstack/tests/functional/network/v2/test_qos_minimum_bandwidth_rule.py @@ -12,7 +12,7 @@ from openstack.network.v2 import ( - qos_minimum_bandwidth_rule as _qos_minimum_bandwidth_rule + qos_minimum_bandwidth_rule as _qos_minimum_bandwidth_rule, ) from openstack.tests.functional import base @@ -35,7 +35,8 @@ def setUp(self): # Skip the tests if qos-bw-limit-direction extension is not enabled. if not self.operator_cloud.network.find_extension( - "qos-bw-limit-direction"): + "qos-bw-limit-direction" + ): self.skipTest("Network qos-bw-limit-direction extension disabled") self.QOS_POLICY_NAME = self.getUniqueString() @@ -45,12 +46,13 @@ def setUp(self): shared=self.QOS_IS_SHARED, ) self.QOS_POLICY_ID = qos_policy.id - qos_min_bw_rule = self.operator_cloud.network \ - .create_qos_minimum_bandwidth_rule( + qos_min_bw_rule = ( + self.operator_cloud.network.create_qos_minimum_bandwidth_rule( self.QOS_POLICY_ID, direction=self.RULE_DIRECTION, min_kbps=self.RULE_MIN_KBPS, ) + ) assert isinstance( qos_min_bw_rule, _qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, @@ -64,7 +66,8 @@ def tearDown(self): self.RULE_ID, self.QOS_POLICY_ID ) qos_policy = self.operator_cloud.network.delete_qos_policy( - self.QOS_POLICY_ID) + self.QOS_POLICY_ID + ) self.assertIsNone(rule) self.assertIsNone(qos_policy) super(TestQoSMinimumBandwidthRule, self).tearDown() diff --git a/openstack/tests/functional/network/v2/test_qos_minimum_packet_rate_rule.py b/openstack/tests/functional/network/v2/test_qos_minimum_packet_rate_rule.py index c3aa77070..0e35f2681 100644 --- a/openstack/tests/functional/network/v2/test_qos_minimum_packet_rate_rule.py +++ b/openstack/tests/functional/network/v2/test_qos_minimum_packet_rate_rule.py @@ -12,7 +12,7 @@ from openstack.network.v2 import ( - qos_minimum_packet_rate_rule as _qos_minimum_packet_rate_rule + qos_minimum_packet_rate_rule as _qos_minimum_packet_rate_rule, ) from openstack.tests.functional import base diff --git a/openstack/tests/functional/network/v2/test_qos_rule_type.py b/openstack/tests/functional/network/v2/test_qos_rule_type.py index a0d662371..3f44c9e7a 100644 --- a/openstack/tests/functional/network/v2/test_qos_rule_type.py +++ b/openstack/tests/functional/network/v2/test_qos_rule_type.py @@ -25,12 +25,14 @@ def setUp(self): # Skip the tests if qos-rule-type-details extension is not enabled. if not self.operator_cloud.network.find_extension( - "qos-rule-type-details"): + "qos-rule-type-details" + ): self.skipTest("Network qos-rule-type-details extension disabled") def test_find(self): sot = self.operator_cloud.network.find_qos_rule_type( - self.QOS_RULE_TYPE) + self.QOS_RULE_TYPE + ) self.assertEqual(self.QOS_RULE_TYPE, sot.type) self.assertIsInstance(sot.drivers, list) diff --git a/openstack/tests/functional/network/v2/test_service_profile.py b/openstack/tests/functional/network/v2/test_service_profile.py index 7bce681fa..86c1e8fb3 100644 --- a/openstack/tests/functional/network/v2/test_service_profile.py +++ b/openstack/tests/functional/network/v2/test_service_profile.py @@ -55,18 +55,20 @@ def tearDown(self): def test_find(self): self.user_cloud.network.find_service_profile( - name_or_id="not_existing", - ignore_missing=True) + name_or_id="not_existing", ignore_missing=True + ) if self.operator_cloud and self.ID: - service_profiles = self.operator_cloud.network \ - .find_service_profile(self.ID) + service_profiles = ( + self.operator_cloud.network.find_service_profile(self.ID) + ) self.assertEqual(self.METAINFO, service_profiles.meta_info) def test_get(self): if not self.ID: self.skipTest("ServiceProfile was not created") service_profiles = self.operator_cloud.network.get_service_profile( - self.ID) + self.ID + ) self.assertEqual(self.METAINFO, service_profiles.meta_info) self.assertEqual( self.SERVICE_PROFILE_DESCRIPTION, service_profiles.description @@ -86,8 +88,8 @@ def test_list(self): # Test as operator if self.operator_cloud: metainfos = [ - f.meta_info for f in - self.operator_cloud.network.service_profiles() + f.meta_info + for f in self.operator_cloud.network.service_profiles() ] if self.ID: self.assertIn(self.METAINFO, metainfos) diff --git a/openstack/tests/functional/network/v2/test_taas.py b/openstack/tests/functional/network/v2/test_taas.py index 20eacafa5..f31f14bf3 100644 --- a/openstack/tests/functional/network/v2/test_taas.py +++ b/openstack/tests/functional/network/v2/test_taas.py @@ -18,7 +18,6 @@ class TestTapService(base.BaseFunctionalTest): - def setUp(self): super().setUp() if not self.user_cloud.network.find_extension("taas"): @@ -35,32 +34,38 @@ def setUp(self): self.FLOW_NET_ID = net.id port = self.user_cloud.network.create_port( - network_id=self.SERVICE_NET_ID) + network_id=self.SERVICE_NET_ID + ) assert isinstance(port, _port.Port) self.SERVICE_PORT_ID = port.id - port = self.user_cloud.network.create_port( - network_id=self.FLOW_NET_ID) + port = self.user_cloud.network.create_port(network_id=self.FLOW_NET_ID) assert isinstance(port, _port.Port) self.FLOW_PORT_ID = port.id tap_service = self.user_cloud.network.create_tap_service( - name=self.TAP_S_NAME, port_id=self.SERVICE_PORT_ID) + name=self.TAP_S_NAME, port_id=self.SERVICE_PORT_ID + ) assert isinstance(tap_service, _tap_service.TapService) self.TAP_SERVICE = tap_service tap_flow = self.user_cloud.network.create_tap_flow( - name=self.TAP_F_NAME, tap_service_id=self.TAP_SERVICE.id, - source_port=self.FLOW_PORT_ID, direction='BOTH') + name=self.TAP_F_NAME, + tap_service_id=self.TAP_SERVICE.id, + source_port=self.FLOW_PORT_ID, + direction='BOTH', + ) assert isinstance(tap_flow, _tap_flow.TapFlow) self.TAP_FLOW = tap_flow def tearDown(self): - sot = self.user_cloud.network.delete_tap_flow(self.TAP_FLOW.id, - ignore_missing=False) + sot = self.user_cloud.network.delete_tap_flow( + self.TAP_FLOW.id, ignore_missing=False + ) self.assertIsNone(sot) - sot = self.user_cloud.network.delete_tap_service(self.TAP_SERVICE.id, - ignore_missing=False) + sot = self.user_cloud.network.delete_tap_service( + self.TAP_SERVICE.id, ignore_missing=False + ) self.assertIsNone(sot) sot = self.user_cloud.network.delete_port(self.SERVICE_PORT_ID) self.assertIsNone(sot) @@ -83,14 +88,16 @@ def test_get_tap_service(self): self.assertEqual(self.TAP_S_NAME, sot.name) def test_list_tap_services(self): - tap_service_ids = [ts.id for ts in - self.user_cloud.network.tap_services()] + tap_service_ids = [ + ts.id for ts in self.user_cloud.network.tap_services() + ] self.assertIn(self.TAP_SERVICE.id, tap_service_ids) def test_update_tap_service(self): description = 'My tap service' sot = self.user_cloud.network.update_tap_service( - self.TAP_SERVICE.id, description=description) + self.TAP_SERVICE.id, description=description + ) self.assertEqual(description, sot.description) def test_find_tap_flow(self): @@ -108,12 +115,12 @@ def test_get_tap_flow(self): self.assertEqual('BOTH', sot.direction) def test_list_tap_flows(self): - tap_flow_ids = [tf.id for tf in - self.user_cloud.network.tap_flows()] + tap_flow_ids = [tf.id for tf in self.user_cloud.network.tap_flows()] self.assertIn(self.TAP_FLOW.id, tap_flow_ids) def test_update_tap_flow(self): description = 'My tap flow' sot = self.user_cloud.network.update_tap_flow( - self.TAP_FLOW.id, description=description) + self.TAP_FLOW.id, description=description + ) self.assertEqual(description, sot.description) diff --git a/openstack/tests/functional/network/v2/test_vpnaas.py b/openstack/tests/functional/network/v2/test_vpnaas.py index 55ed989da..bfd89169c 100644 --- a/openstack/tests/functional/network/v2/test_vpnaas.py +++ b/openstack/tests/functional/network/v2/test_vpnaas.py @@ -39,9 +39,7 @@ def tearDown(self): super(TestVpnIkePolicy, self).tearDown() def test_list(self): - policies = [ - f.name for f in - self.user_cloud.network.vpn_ike_policies()] + policies = [f.name for f in self.user_cloud.network.vpn_ike_policies()] self.assertIn(self.IKEPOLICY_NAME, policies) def test_find(self): diff --git a/openstack/tests/unit/network/test_version.py b/openstack/tests/unit/network/test_version.py index 77df42ba6..64724160c 100644 --- a/openstack/tests/unit/network/test_version.py +++ b/openstack/tests/unit/network/test_version.py @@ -23,7 +23,6 @@ class TestVersion(base.TestCase): - def test_basic(self): sot = version.Version() self.assertEqual('version', sot.resource_key) diff --git a/openstack/tests/unit/network/v2/test_address_group.py b/openstack/tests/unit/network/v2/test_address_group.py index dab387afd..b9f9d6803 100644 --- a/openstack/tests/unit/network/v2/test_address_group.py +++ b/openstack/tests/unit/network/v2/test_address_group.py @@ -20,12 +20,11 @@ 'name': '1', 'description': '2', 'project_id': '3', - 'addresses': ['10.0.0.1/32'] + 'addresses': ['10.0.0.1/32'], } class TestAddressGroup(base.TestCase): - def test_basic(self): sot = address_group.AddressGroup() self.assertEqual('address_group', sot.resource_key) @@ -37,14 +36,18 @@ def test_basic(self): self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertDictEqual({"name": "name", - "description": "description", - "project_id": "project_id", - "sort_key": "sort_key", - "sort_dir": "sort_dir", - "limit": "limit", - "marker": "marker"}, - sot._query_mapping._mapping) + self.assertDictEqual( + { + "name": "name", + "description": "description", + "project_id": "project_id", + "sort_key": "sort_key", + "sort_dir": "sort_dir", + "limit": "limit", + "marker": "marker", + }, + sot._query_mapping._mapping, + ) def test_make_it(self): sot = address_group.AddressGroup(**EXAMPLE) diff --git a/openstack/tests/unit/network/v2/test_address_scope.py b/openstack/tests/unit/network/v2/test_address_scope.py index 0badd9faf..a00e91456 100644 --- a/openstack/tests/unit/network/v2/test_address_scope.py +++ b/openstack/tests/unit/network/v2/test_address_scope.py @@ -25,7 +25,6 @@ class TestAddressScope(base.TestCase): - def test_basic(self): sot = address_scope.AddressScope() self.assertEqual('address_scope', sot.resource_key) diff --git a/openstack/tests/unit/network/v2/test_agent.py b/openstack/tests/unit/network/v2/test_agent.py index ae2bf4858..80d4b8abe 100644 --- a/openstack/tests/unit/network/v2/test_agent.py +++ b/openstack/tests/unit/network/v2/test_agent.py @@ -31,12 +31,11 @@ 'resources_synced': False, 'started_at': '2016-07-09T12:14:57.233772', 'topic': 'test-topic', - 'ha_state': 'active' + 'ha_state': 'active', } class TestAgent(base.TestCase): - def test_basic(self): sot = agent.Agent() self.assertEqual('agent', sot.resource_key) @@ -53,8 +52,7 @@ def test_make_it(self): self.assertTrue(sot.is_admin_state_up) self.assertEqual(EXAMPLE['agent_type'], sot.agent_type) self.assertTrue(sot.is_alive) - self.assertEqual(EXAMPLE['availability_zone'], - sot.availability_zone) + self.assertEqual(EXAMPLE['availability_zone'], sot.availability_zone) self.assertEqual(EXAMPLE['binary'], sot.binary) self.assertEqual(EXAMPLE['configurations'], sot.configuration) self.assertEqual(EXAMPLE['created_at'], sot.created_at) @@ -79,8 +77,7 @@ def test_add_agent_to_network(self): self.assertEqual(response.body, net.add_agent_to_network(sess, **body)) url = 'agents/IDENTIFIER/dhcp-networks' - sess.post.assert_called_with(url, - json=body) + sess.post.assert_called_with(url, json=body) def test_remove_agent_from_network(self): # Remove agent from agent @@ -90,8 +87,9 @@ def test_remove_agent_from_network(self): self.assertIsNone(net.remove_agent_from_network(sess, network_id)) body = {'network_id': {}} - sess.delete.assert_called_with('agents/IDENTIFIER/dhcp-networks/', - json=body) + sess.delete.assert_called_with( + 'agents/IDENTIFIER/dhcp-networks/', json=body + ) def test_add_router_to_agent(self): # Add router to agent @@ -102,12 +100,12 @@ def test_add_router_to_agent(self): sess = mock.Mock() sess.post = mock.Mock(return_value=response) router_id = '1' - self.assertEqual(response.body, - sot.add_router_to_agent(sess, router_id)) + self.assertEqual( + response.body, sot.add_router_to_agent(sess, router_id) + ) body = {'router_id': router_id} url = 'agents/IDENTIFIER/l3-routers' - sess.post.assert_called_with(url, - json=body) + sess.post.assert_called_with(url, json=body) def test_remove_router_from_agent(self): # Remove router from agent @@ -117,17 +115,16 @@ def test_remove_router_from_agent(self): self.assertIsNone(sot.remove_router_from_agent(sess, router_id)) body = {'router_id': {}} - sess.delete.assert_called_with('agents/IDENTIFIER/l3-routers/', - json=body) + sess.delete.assert_called_with( + 'agents/IDENTIFIER/l3-routers/', json=body + ) def test_get_bgp_speakers_hosted_by_dragent(self): sot = agent.Agent(**EXAMPLE) sess = mock.Mock() response = mock.Mock() response.body = { - 'bgp_speakers': [ - {'name': 'bgp_speaker_1', 'ip_version': 4} - ] + 'bgp_speakers': [{'name': 'bgp_speaker_1', 'ip_version': 4}] } response.json = mock.Mock(return_value=response.body) response.status_code = 200 @@ -139,7 +136,6 @@ def test_get_bgp_speakers_hosted_by_dragent(self): class TestNetworkHostingDHCPAgent(base.TestCase): - def test_basic(self): net = agent.NetworkHostingDHCPAgent() self.assertEqual('agent', net.resource_key) @@ -154,7 +150,6 @@ def test_basic(self): class TestRouterL3Agent(base.TestCase): - def test_basic(self): sot = agent.RouterL3Agent() self.assertEqual('agent', sot.resource_key) diff --git a/openstack/tests/unit/network/v2/test_auto_allocated_topology.py b/openstack/tests/unit/network/v2/test_auto_allocated_topology.py index 973de536c..acb3bccc8 100644 --- a/openstack/tests/unit/network/v2/test_auto_allocated_topology.py +++ b/openstack/tests/unit/network/v2/test_auto_allocated_topology.py @@ -21,7 +21,6 @@ class TestAutoAllocatedTopology(base.TestCase): - def test_basic(self): topo = auto_allocated_topology.AutoAllocatedTopology self.assertEqual('auto_allocated_topology', topo.resource_key) diff --git a/openstack/tests/unit/network/v2/test_availability_zone.py b/openstack/tests/unit/network/v2/test_availability_zone.py index ef5f22336..2961d6c14 100644 --- a/openstack/tests/unit/network/v2/test_availability_zone.py +++ b/openstack/tests/unit/network/v2/test_availability_zone.py @@ -24,7 +24,6 @@ class TestAvailabilityZone(base.TestCase): - def test_basic(self): sot = availability_zone.AvailabilityZone() self.assertEqual('availability_zone', sot.resource_key) diff --git a/openstack/tests/unit/network/v2/test_bgp_peer.py b/openstack/tests/unit/network/v2/test_bgp_peer.py index 06aac5fa1..7842725ad 100644 --- a/openstack/tests/unit/network/v2/test_bgp_peer.py +++ b/openstack/tests/unit/network/v2/test_bgp_peer.py @@ -21,12 +21,11 @@ 'name': 'bgp-peer', 'peer_ip': '10.0.0.3', 'id': IDENTIFIER, - 'project_id': '42' + 'project_id': '42', } class TestBgpPeer(base.TestCase): - def test_basic(self): sot = bgp_peer.BgpPeer() self.assertEqual('bgp_peer', sot.resource_key) @@ -48,7 +47,9 @@ def test_make_it(self): self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertDictEqual( - {'limit': 'limit', - 'marker': 'marker', - }, - sot._query_mapping._mapping) + { + 'limit': 'limit', + 'marker': 'marker', + }, + sot._query_mapping._mapping, + ) diff --git a/openstack/tests/unit/network/v2/test_bgp_speaker.py b/openstack/tests/unit/network/v2/test_bgp_speaker.py index 8f78d3ac3..839efdd51 100644 --- a/openstack/tests/unit/network/v2/test_bgp_speaker.py +++ b/openstack/tests/unit/network/v2/test_bgp_speaker.py @@ -26,7 +26,7 @@ 'advertise_tenant_networks': 'true', 'local_as': 1000, 'networks': [], - 'project_id': '42' + 'project_id': '42', } @@ -47,17 +47,21 @@ def test_make_it(self): self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['ip_version'], sot.ip_version) - self.assertEqual(EXAMPLE['advertise_floating_ip_host_routes'], - sot.advertise_floating_ip_host_routes) + self.assertEqual( + EXAMPLE['advertise_floating_ip_host_routes'], + sot.advertise_floating_ip_host_routes, + ) self.assertEqual(EXAMPLE['local_as'], sot.local_as) self.assertEqual(EXAMPLE['networks'], sot.networks) self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertDictEqual( - {'limit': 'limit', - 'marker': 'marker', - }, - sot._query_mapping._mapping) + { + 'limit': 'limit', + 'marker': 'marker', + }, + sot._query_mapping._mapping, + ) def test_add_bgp_peer(self): sot = bgp_speaker.BgpSpeaker(**EXAMPLE) @@ -143,9 +147,7 @@ def test_get_bgp_dragents(self): sot = bgp_speaker.BgpSpeaker(**EXAMPLE) response = mock.Mock() response.body = { - 'agents': [ - {'binary': 'neutron-bgp-dragent', 'alive': True} - ] + 'agents': [{'binary': 'neutron-bgp-dragent', 'alive': True}] } response.json = mock.Mock(return_value=response.body) response.status_code = 200 diff --git a/openstack/tests/unit/network/v2/test_bgpvpn.py b/openstack/tests/unit/network/v2/test_bgpvpn.py index af2a661be..c6e4671e0 100644 --- a/openstack/tests/unit/network/v2/test_bgpvpn.py +++ b/openstack/tests/unit/network/v2/test_bgpvpn.py @@ -35,7 +35,6 @@ class TestBgpVpn(base.TestCase): - def test_basic(self): sot = bgpvpn.BgpVpn() self.assertEqual('bgpvpn', sot.resource_key) @@ -52,38 +51,37 @@ def test_make_it(self): self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['project_id'], sot.project_id) - self.assertEqual(EXAMPLE['route_distinguishers'], - sot.route_distinguishers) + self.assertEqual( + EXAMPLE['route_distinguishers'], sot.route_distinguishers + ) self.assertEqual(EXAMPLE['route_targets'], sot.route_targets) self.assertEqual(EXAMPLE['import_targets'], sot.import_targets) self.assertEqual(EXAMPLE['export_targets'], sot.export_targets) self.assertDictEqual( - {'limit': 'limit', - 'marker': 'marker', - }, - sot._query_mapping._mapping) + { + 'limit': 'limit', + 'marker': 'marker', + }, + sot._query_mapping._mapping, + ) def test_create_bgpvpn_network_association(self): test_bpgvpn = bgpvpn.BgpVpn(**EXAMPLE) test_net = network.Network(**{'name': 'foo_net', 'id': NET_ID}) sot = bgpvpn_network_association.BgpVpnNetworkAssociation( - bgpvn_id=test_bpgvpn.id, - network_id=test_net.id + bgpvn_id=test_bpgvpn.id, network_id=test_net.id ) self.assertEqual(test_net.id, sot.network_id) self.assertEqual(test_bpgvpn.id, sot.bgpvn_id) def test_create_bgpvpn_port_association(self): test_bpgvpn = bgpvpn.BgpVpn(**EXAMPLE) - test_port = port.Port(**{ - 'name': 'foo_port', - 'id': PORT_ID, - 'network_id': NET_ID - }) + test_port = port.Port( + **{'name': 'foo_port', 'id': PORT_ID, 'network_id': NET_ID} + ) sot = bgpvpn_port_association.BgpVpnPortAssociation( - bgpvn_id=test_bpgvpn.id, - port_id=test_port.id + bgpvn_id=test_bpgvpn.id, port_id=test_port.id ) self.assertEqual(test_port.id, sot.port_id) self.assertEqual(test_bpgvpn.id, sot.bgpvn_id) @@ -92,8 +90,7 @@ def test_create_bgpvpn_router_association(self): test_bpgvpn = bgpvpn.BgpVpn(**EXAMPLE) test_router = router.Router(**{'name': 'foo_port'}) sot = bgpvpn_router_association.BgpVpnRouterAssociation( - bgpvn_id=test_bpgvpn.id, - router_id=test_router.id + bgpvn_id=test_bpgvpn.id, router_id=test_router.id ) self.assertEqual(test_router.id, sot.router_id) self.assertEqual(test_bpgvpn.id, sot.bgpvn_id) diff --git a/openstack/tests/unit/network/v2/test_extension.py b/openstack/tests/unit/network/v2/test_extension.py index 4ae763445..6a6e6e32e 100644 --- a/openstack/tests/unit/network/v2/test_extension.py +++ b/openstack/tests/unit/network/v2/test_extension.py @@ -25,7 +25,6 @@ class TestExtension(base.TestCase): - def test_basic(self): sot = extension.Extension() self.assertEqual('extension', sot.resource_key) diff --git a/openstack/tests/unit/network/v2/test_firewall_group.py b/openstack/tests/unit/network/v2/test_firewall_group.py index 77749f20c..9eb3bb4c9 100644 --- a/openstack/tests/unit/network/v2/test_firewall_group.py +++ b/openstack/tests/unit/network/v2/test_firewall_group.py @@ -32,7 +32,6 @@ class TestFirewallGroup(testtools.TestCase): - def test_basic(self): sot = firewall_group.FirewallGroup() self.assertEqual('firewall_group', sot.resource_key) @@ -48,10 +47,13 @@ def test_make_it(self): sot = firewall_group.FirewallGroup(**EXAMPLE) self.assertEqual(EXAMPLE['description'], sot.description) self.assertEqual(EXAMPLE['name'], sot.name) - self.assertEqual(EXAMPLE['egress_firewall_policy_id'], - sot.egress_firewall_policy_id) - self.assertEqual(EXAMPLE['ingress_firewall_policy_id'], - sot.ingress_firewall_policy_id) + self.assertEqual( + EXAMPLE['egress_firewall_policy_id'], sot.egress_firewall_policy_id + ) + self.assertEqual( + EXAMPLE['ingress_firewall_policy_id'], + sot.ingress_firewall_policy_id, + ) self.assertEqual(EXAMPLE['shared'], sot.shared) self.assertEqual(EXAMPLE['status'], sot.status) self.assertEqual(list, type(sot.ports)) diff --git a/openstack/tests/unit/network/v2/test_firewall_policy.py b/openstack/tests/unit/network/v2/test_firewall_policy.py index bf5b60fea..2d2312515 100644 --- a/openstack/tests/unit/network/v2/test_firewall_policy.py +++ b/openstack/tests/unit/network/v2/test_firewall_policy.py @@ -21,15 +21,16 @@ EXAMPLE = { 'description': '1', 'name': '2', - 'firewall_rules': ['a30b0ec2-a468-4b1c-8dbf-928ded2a57a8', - '8d562e98-24f3-46e1-bbf3-d9347c0a67ee'], + 'firewall_rules': [ + 'a30b0ec2-a468-4b1c-8dbf-928ded2a57a8', + '8d562e98-24f3-46e1-bbf3-d9347c0a67ee', + ], 'shared': True, 'project_id': '4', } class TestFirewallPolicy(testtools.TestCase): - def test_basic(self): sot = firewall_policy.FirewallPolicy() self.assertEqual('firewall_policy', sot.resource_key) diff --git a/openstack/tests/unit/network/v2/test_firewall_rule.py b/openstack/tests/unit/network/v2/test_firewall_rule.py index d00f060cd..63c764d83 100644 --- a/openstack/tests/unit/network/v2/test_firewall_rule.py +++ b/openstack/tests/unit/network/v2/test_firewall_rule.py @@ -34,7 +34,6 @@ class TestFirewallRule(testtools.TestCase): - def test_basic(self): sot = firewall_rule.FirewallRule() self.assertEqual('firewall_rule', sot.resource_key) @@ -50,15 +49,15 @@ def test_make_it(self): sot = firewall_rule.FirewallRule(**EXAMPLE) self.assertEqual(EXAMPLE['action'], sot.action) self.assertEqual(EXAMPLE['description'], sot.description) - self.assertEqual(EXAMPLE['destination_ip_address'], - sot.destination_ip_address) + self.assertEqual( + EXAMPLE['destination_ip_address'], sot.destination_ip_address + ) self.assertEqual(EXAMPLE['destination_port'], sot.destination_port) self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['enabled'], sot.enabled) self.assertEqual(EXAMPLE['ip_version'], sot.ip_version) self.assertEqual(EXAMPLE['protocol'], sot.protocol) self.assertEqual(EXAMPLE['shared'], sot.shared) - self.assertEqual(EXAMPLE['source_ip_address'], - sot.source_ip_address) + self.assertEqual(EXAMPLE['source_ip_address'], sot.source_ip_address) self.assertEqual(EXAMPLE['source_port'], sot.source_port) self.assertEqual(EXAMPLE['project_id'], sot.project_id) diff --git a/openstack/tests/unit/network/v2/test_flavor.py b/openstack/tests/unit/network/v2/test_flavor.py index 7ba81829e..27246d0b7 100644 --- a/openstack/tests/unit/network/v2/test_flavor.py +++ b/openstack/tests/unit/network/v2/test_flavor.py @@ -52,13 +52,17 @@ def test_make_it(self): def test_make_it_with_optional(self): flavors = flavor.Flavor(**EXAMPLE_WITH_OPTIONAL) self.assertEqual(EXAMPLE_WITH_OPTIONAL['name'], flavors.name) - self.assertEqual(EXAMPLE_WITH_OPTIONAL['service_type'], - flavors.service_type) - self.assertEqual(EXAMPLE_WITH_OPTIONAL['description'], - flavors.description) + self.assertEqual( + EXAMPLE_WITH_OPTIONAL['service_type'], flavors.service_type + ) + self.assertEqual( + EXAMPLE_WITH_OPTIONAL['description'], flavors.description + ) self.assertEqual(EXAMPLE_WITH_OPTIONAL['enabled'], flavors.is_enabled) - self.assertEqual(EXAMPLE_WITH_OPTIONAL['service_profiles'], - flavors.service_profile_ids) + self.assertEqual( + EXAMPLE_WITH_OPTIONAL['service_profiles'], + flavors.service_profile_ids, + ) def test_associate_flavor_with_service_profile(self): flav = flavor.Flavor(EXAMPLE) @@ -71,12 +75,12 @@ def test_associate_flavor_with_service_profile(self): sess.post = mock.Mock(return_value=response) flav.id = 'IDENTIFIER' self.assertEqual( - response.body, flav.associate_flavor_with_service_profile( - sess, '1')) + response.body, + flav.associate_flavor_with_service_profile(sess, '1'), + ) url = 'flavors/IDENTIFIER/service_profiles' - sess.post.assert_called_with(url, - json=response.body) + sess.post.assert_called_with(url, json=response.body) def test_disassociate_flavor_from_service_profile(self): flav = flavor.Flavor(EXAMPLE) @@ -86,8 +90,10 @@ def test_disassociate_flavor_from_service_profile(self): sess.post = mock.Mock(return_value=response) flav.id = 'IDENTIFIER' self.assertEqual( - None, flav.disassociate_flavor_from_service_profile( - sess, '1')) + None, flav.disassociate_flavor_from_service_profile(sess, '1') + ) url = 'flavors/IDENTIFIER/service_profiles/1' - sess.delete.assert_called_with(url,) + sess.delete.assert_called_with( + url, + ) diff --git a/openstack/tests/unit/network/v2/test_floating_ip.py b/openstack/tests/unit/network/v2/test_floating_ip.py index c00466837..53b8cff9d 100644 --- a/openstack/tests/unit/network/v2/test_floating_ip.py +++ b/openstack/tests/unit/network/v2/test_floating_ip.py @@ -34,12 +34,11 @@ 'revision_number': 12, 'updated_at': '13', 'subnet_id': '14', - 'tags': ['15', '16'] + 'tags': ['15', '16'], } class TestFloatingIP(base.TestCase): - def test_basic(self): sot = floating_ip.FloatingIP() self.assertEqual('floatingip', sot.resource_key) @@ -55,10 +54,12 @@ def test_make_it(self): sot = floating_ip.FloatingIP(**EXAMPLE) self.assertEqual(EXAMPLE['created_at'], sot.created_at) self.assertEqual(EXAMPLE['fixed_ip_address'], sot.fixed_ip_address) - self.assertEqual(EXAMPLE['floating_ip_address'], - sot.floating_ip_address) - self.assertEqual(EXAMPLE['floating_network_id'], - sot.floating_network_id) + self.assertEqual( + EXAMPLE['floating_ip_address'], sot.floating_ip_address + ) + self.assertEqual( + EXAMPLE['floating_network_id'], sot.floating_network_id + ) self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['port_id'], sot.port_id) self.assertEqual(EXAMPLE['project_id'], sot.project_id) @@ -73,24 +74,26 @@ def test_make_it(self): self.assertEqual(EXAMPLE['tags'], sot.tags) self.assertDictEqual( - {'limit': 'limit', - 'marker': 'marker', - 'description': 'description', - 'project_id': 'project_id', - 'tenant_id': 'project_id', - 'status': 'status', - 'port_id': 'port_id', - 'subnet_id': 'subnet_id', - 'router_id': 'router_id', - 'fixed_ip_address': 'fixed_ip_address', - 'floating_ip_address': 'floating_ip_address', - 'floating_network_id': 'floating_network_id', - 'tags': 'tags', - 'any_tags': 'tags-any', - 'not_tags': 'not-tags', - 'not_any_tags': 'not-tags-any', - }, - sot._query_mapping._mapping) + { + 'limit': 'limit', + 'marker': 'marker', + 'description': 'description', + 'project_id': 'project_id', + 'tenant_id': 'project_id', + 'status': 'status', + 'port_id': 'port_id', + 'subnet_id': 'subnet_id', + 'router_id': 'router_id', + 'fixed_ip_address': 'fixed_ip_address', + 'floating_ip_address': 'floating_ip_address', + 'floating_network_id': 'floating_network_id', + 'tags': 'tags', + 'any_tags': 'tags-any', + 'not_tags': 'not-tags', + 'not_any_tags': 'not-tags-any', + }, + sot._query_mapping._mapping, + ) def test_find_available(self): mock_session = mock.Mock(spec=proxy.Proxy) @@ -111,7 +114,8 @@ def test_find_available(self): floating_ip.FloatingIP.base_path, headers={'Accept': 'application/json'}, params={}, - microversion=None) + microversion=None, + ) def test_find_available_nada(self): mock_session = mock.Mock(spec=proxy.Proxy) diff --git a/openstack/tests/unit/network/v2/test_health_monitor.py b/openstack/tests/unit/network/v2/test_health_monitor.py index a101e361c..fd857717a 100644 --- a/openstack/tests/unit/network/v2/test_health_monitor.py +++ b/openstack/tests/unit/network/v2/test_health_monitor.py @@ -33,7 +33,6 @@ class TestHealthMonitor(base.TestCase): - def test_basic(self): sot = health_monitor.HealthMonitor() self.assertEqual('healthmonitor', sot.resource_key) diff --git a/openstack/tests/unit/network/v2/test_l3_conntrack_helper.py b/openstack/tests/unit/network/v2/test_l3_conntrack_helper.py index 38527b50c..4136475e9 100644 --- a/openstack/tests/unit/network/v2/test_l3_conntrack_helper.py +++ b/openstack/tests/unit/network/v2/test_l3_conntrack_helper.py @@ -18,19 +18,18 @@ 'id': 'ct_helper_id', 'protocol': 'udp', 'port': 69, - 'helper': 'tftp' + 'helper': 'tftp', } class TestL3ConntrackHelper(base.TestCase): - def test_basic(self): sot = l3_conntrack_helper.ConntrackHelper() self.assertEqual('conntrack_helper', sot.resource_key) self.assertEqual('conntrack_helpers', sot.resources_key) self.assertEqual( - '/routers/%(router_id)s/conntrack_helpers', - sot.base_path) + '/routers/%(router_id)s/conntrack_helpers', sot.base_path + ) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_listener.py b/openstack/tests/unit/network/v2/test_listener.py index fca356e08..fea5259f4 100644 --- a/openstack/tests/unit/network/v2/test_listener.py +++ b/openstack/tests/unit/network/v2/test_listener.py @@ -33,7 +33,6 @@ class TestListener(base.TestCase): - def test_basic(self): sot = listener.Listener() self.assertEqual('listener', sot.resource_key) @@ -58,7 +57,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['protocol'], sot.protocol) self.assertEqual(EXAMPLE['protocol_port'], sot.protocol_port) - self.assertEqual(EXAMPLE['default_tls_container_ref'], - sot.default_tls_container_ref) - self.assertEqual(EXAMPLE['sni_container_refs'], - sot.sni_container_refs) + self.assertEqual( + EXAMPLE['default_tls_container_ref'], sot.default_tls_container_ref + ) + self.assertEqual(EXAMPLE['sni_container_refs'], sot.sni_container_refs) diff --git a/openstack/tests/unit/network/v2/test_load_balancer.py b/openstack/tests/unit/network/v2/test_load_balancer.py index f6f1a51dc..6ff4725b2 100644 --- a/openstack/tests/unit/network/v2/test_load_balancer.py +++ b/openstack/tests/unit/network/v2/test_load_balancer.py @@ -33,7 +33,6 @@ class TestLoadBalancer(base.TestCase): - def test_basic(self): sot = load_balancer.LoadBalancer() self.assertEqual('loadbalancer', sot.resource_key) @@ -53,8 +52,9 @@ def test_make_it(self): self.assertEqual(EXAMPLE['listeners'], sot.listener_ids) self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['operating_status'], sot.operating_status) - self.assertEqual(EXAMPLE['provisioning_status'], - sot.provisioning_status) + self.assertEqual( + EXAMPLE['provisioning_status'], sot.provisioning_status + ) self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['vip_address'], sot.vip_address) self.assertEqual(EXAMPLE['vip_subnet_id'], sot.vip_subnet_id) diff --git a/openstack/tests/unit/network/v2/test_local_ip.py b/openstack/tests/unit/network/v2/test_local_ip.py index 4752665f9..29ab1dacc 100644 --- a/openstack/tests/unit/network/v2/test_local_ip.py +++ b/openstack/tests/unit/network/v2/test_local_ip.py @@ -33,7 +33,6 @@ class TestLocalIP(base.TestCase): - def test_basic(self): sot = local_ip.LocalIP() self.assertEqual('local_ip', sot.resource_key) @@ -45,18 +44,22 @@ def test_basic(self): self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertDictEqual({"name": "name", - "description": "description", - "project_id": "project_id", - "network_id": "network_id", - "local_port_id": "local_port_id", - "local_ip_address": "local_ip_address", - "ip_mode": "ip_mode", - "sort_key": "sort_key", - "sort_dir": "sort_dir", - "limit": "limit", - "marker": "marker"}, - sot._query_mapping._mapping) + self.assertDictEqual( + { + "name": "name", + "description": "description", + "project_id": "project_id", + "network_id": "network_id", + "local_port_id": "local_port_id", + "local_ip_address": "local_ip_address", + "ip_mode": "ip_mode", + "sort_key": "sort_key", + "sort_dir": "sort_dir", + "limit": "limit", + "marker": "marker", + }, + sot._query_mapping._mapping, + ) def test_make_it(self): sot = local_ip.LocalIP(**EXAMPLE) diff --git a/openstack/tests/unit/network/v2/test_local_ip_association.py b/openstack/tests/unit/network/v2/test_local_ip_association.py index 602b999da..b94da0abe 100644 --- a/openstack/tests/unit/network/v2/test_local_ip_association.py +++ b/openstack/tests/unit/network/v2/test_local_ip_association.py @@ -26,13 +26,13 @@ class TestLocalIP(base.TestCase): - def test_basic(self): sot = local_ip_association.LocalIPAssociation() self.assertEqual('port_association', sot.resource_key) self.assertEqual('port_associations', sot.resources_key) - self.assertEqual('/local_ips/%(local_ip_id)s/port_associations', - sot.base_path) + self.assertEqual( + '/local_ips/%(local_ip_id)s/port_associations', sot.base_path + ) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) @@ -40,12 +40,15 @@ def test_basic(self): self.assertTrue(sot.allow_list) self.assertDictEqual( - {'fixed_port_id': 'fixed_port_id', - 'fixed_ip': 'fixed_ip', - 'host': 'host', - 'limit': 'limit', - 'marker': 'marker'}, - sot._query_mapping._mapping) + { + 'fixed_port_id': 'fixed_port_id', + 'fixed_ip': 'fixed_ip', + 'host': 'host', + 'limit': 'limit', + 'marker': 'marker', + }, + sot._query_mapping._mapping, + ) def test_make_it(self): sot = local_ip_association.LocalIPAssociation(**EXAMPLE) diff --git a/openstack/tests/unit/network/v2/test_metering_label.py b/openstack/tests/unit/network/v2/test_metering_label.py index 2a300346f..be177d6f9 100644 --- a/openstack/tests/unit/network/v2/test_metering_label.py +++ b/openstack/tests/unit/network/v2/test_metering_label.py @@ -25,7 +25,6 @@ class TestMeteringLabel(base.TestCase): - def test_basic(self): sot = metering_label.MeteringLabel() self.assertEqual('metering_label', sot.resource_key) diff --git a/openstack/tests/unit/network/v2/test_metering_label_rule.py b/openstack/tests/unit/network/v2/test_metering_label_rule.py index 95146acaf..fa6340708 100644 --- a/openstack/tests/unit/network/v2/test_metering_label_rule.py +++ b/openstack/tests/unit/network/v2/test_metering_label_rule.py @@ -26,7 +26,6 @@ class TestMeteringLabelRule(base.TestCase): - def test_basic(self): sot = metering_label_rule.MeteringLabelRule() self.assertEqual('metering_label_rule', sot.resource_key) @@ -57,12 +56,16 @@ def test_make_it_source_and_destination(self): self.assertFalse(sot.is_excluded) self.assertEqual(custom_example['id'], sot.id) self.assertEqual( - custom_example['metering_label_id'], sot.metering_label_id) + custom_example['metering_label_id'], sot.metering_label_id + ) self.assertEqual(custom_example['project_id'], sot.project_id) self.assertEqual( - custom_example['remote_ip_prefix'], sot.remote_ip_prefix) + custom_example['remote_ip_prefix'], sot.remote_ip_prefix + ) self.assertEqual( - custom_example['source_ip_prefix'], sot.source_ip_prefix) + custom_example['source_ip_prefix'], sot.source_ip_prefix + ) self.assertEqual( - custom_example['destination_ip_prefix'], sot.destination_ip_prefix) + custom_example['destination_ip_prefix'], sot.destination_ip_prefix + ) diff --git a/openstack/tests/unit/network/v2/test_ndp_proxy.py b/openstack/tests/unit/network/v2/test_ndp_proxy.py index 4d7d7576e..56ac1aa84 100644 --- a/openstack/tests/unit/network/v2/test_ndp_proxy.py +++ b/openstack/tests/unit/network/v2/test_ndp_proxy.py @@ -26,7 +26,6 @@ class TestNDPProxy(base.TestCase): - def test_basic(self): sot = ndp_proxy.NDPProxy() self.assertEqual('ndp_proxy', sot.resource_key) diff --git a/openstack/tests/unit/network/v2/test_network.py b/openstack/tests/unit/network/v2/test_network.py index 7f9257c65..17ea946e5 100644 --- a/openstack/tests/unit/network/v2/test_network.py +++ b/openstack/tests/unit/network/v2/test_network.py @@ -46,7 +46,6 @@ class TestNetwork(base.TestCase): - def test_basic(self): sot = network.Network() self.assertEqual('network', sot.resource_key) @@ -61,29 +60,34 @@ def test_basic(self): def test_make_it(self): sot = network.Network(**EXAMPLE) self.assertTrue(sot.is_admin_state_up) - self.assertEqual(EXAMPLE['availability_zone_hints'], - sot.availability_zone_hints) - self.assertEqual(EXAMPLE['availability_zones'], - sot.availability_zones) + self.assertEqual( + EXAMPLE['availability_zone_hints'], sot.availability_zone_hints + ) + self.assertEqual(EXAMPLE['availability_zones'], sot.availability_zones) self.assertEqual(EXAMPLE['created_at'], sot.created_at) self.assertEqual(EXAMPLE['description'], sot.description) self.assertEqual(EXAMPLE['dns_domain'], sot.dns_domain) self.assertEqual(EXAMPLE['id'], sot.id) - self.assertEqual(EXAMPLE['ipv4_address_scope'], - sot.ipv4_address_scope_id) - self.assertEqual(EXAMPLE['ipv6_address_scope'], - sot.ipv6_address_scope_id) + self.assertEqual( + EXAMPLE['ipv4_address_scope'], sot.ipv4_address_scope_id + ) + self.assertEqual( + EXAMPLE['ipv6_address_scope'], sot.ipv6_address_scope_id + ) self.assertFalse(sot.is_default) self.assertEqual(EXAMPLE['mtu'], sot.mtu) self.assertEqual(EXAMPLE['name'], sot.name) self.assertTrue(sot.is_port_security_enabled) self.assertEqual(EXAMPLE['project_id'], sot.project_id) - self.assertEqual(EXAMPLE['provider:network_type'], - sot.provider_network_type) - self.assertEqual(EXAMPLE['provider:physical_network'], - sot.provider_physical_network) - self.assertEqual(EXAMPLE['provider:segmentation_id'], - sot.provider_segmentation_id) + self.assertEqual( + EXAMPLE['provider:network_type'], sot.provider_network_type + ) + self.assertEqual( + EXAMPLE['provider:physical_network'], sot.provider_physical_network + ) + self.assertEqual( + EXAMPLE['provider:segmentation_id'], sot.provider_segmentation_id + ) self.assertEqual(EXAMPLE['qos_policy_id'], sot.qos_policy_id) self.assertEqual(EXAMPLE['revision_number'], sot.revision_number) self.assertTrue(sot.is_router_external) @@ -95,31 +99,32 @@ def test_make_it(self): self.assertEqual(EXAMPLE['vlan_transparent'], sot.is_vlan_transparent) self.assertDictEqual( - {'limit': 'limit', - 'marker': 'marker', - 'description': 'description', - 'name': 'name', - 'project_id': 'project_id', - 'status': 'status', - 'ipv4_address_scope_id': 'ipv4_address_scope', - 'ipv6_address_scope_id': 'ipv6_address_scope', - 'is_admin_state_up': 'admin_state_up', - 'is_port_security_enabled': 'port_security_enabled', - 'is_router_external': 'router:external', - 'is_shared': 'shared', - 'provider_network_type': 'provider:network_type', - 'provider_physical_network': 'provider:physical_network', - 'provider_segmentation_id': 'provider:segmentation_id', - 'tags': 'tags', - 'any_tags': 'tags-any', - 'not_tags': 'not-tags', - 'not_any_tags': 'not-tags-any', - }, - sot._query_mapping._mapping) + { + 'limit': 'limit', + 'marker': 'marker', + 'description': 'description', + 'name': 'name', + 'project_id': 'project_id', + 'status': 'status', + 'ipv4_address_scope_id': 'ipv4_address_scope', + 'ipv6_address_scope_id': 'ipv6_address_scope', + 'is_admin_state_up': 'admin_state_up', + 'is_port_security_enabled': 'port_security_enabled', + 'is_router_external': 'router:external', + 'is_shared': 'shared', + 'provider_network_type': 'provider:network_type', + 'provider_physical_network': 'provider:physical_network', + 'provider_segmentation_id': 'provider:segmentation_id', + 'tags': 'tags', + 'any_tags': 'tags-any', + 'not_tags': 'not-tags', + 'not_any_tags': 'not-tags-any', + }, + sot._query_mapping._mapping, + ) class TestDHCPAgentHostingNetwork(base.TestCase): - def test_basic(self): net = network.DHCPAgentHostingNetwork() self.assertEqual('network', net.resource_key) diff --git a/openstack/tests/unit/network/v2/test_network_ip_availability.py b/openstack/tests/unit/network/v2/test_network_ip_availability.py index dcb8ad7b0..0d033ab36 100644 --- a/openstack/tests/unit/network/v2/test_network_ip_availability.py +++ b/openstack/tests/unit/network/v2/test_network_ip_availability.py @@ -27,11 +27,16 @@ EXAMPLE_WITH_OPTIONAL = { 'network_id': IDENTIFIER, 'network_name': 'private', - 'subnet_ip_availability': [{"used_ips": 3, "subnet_id": - "2e4db1d6-ab2d-4bb1-93bb-a003fdbc9b39", - "subnet_name": "private-subnet", - "ip_version": 6, "cidr": "fd91:c3ba:e818::/64", - "total_ips": 18446744073709551614}], + 'subnet_ip_availability': [ + { + "used_ips": 3, + "subnet_id": "2e4db1d6-ab2d-4bb1-93bb-a003fdbc9b39", + "subnet_name": "private-subnet", + "ip_version": 6, + "cidr": "fd91:c3ba:e818::/64", + "total_ips": 18446744073709551614, + } + ], 'project_id': '2', 'total_ips': 1844, 'used_ips': 6, @@ -39,7 +44,6 @@ class TestNetworkIPAvailability(base.TestCase): - def test_basic(self): sot = network_ip_availability.NetworkIPAvailability() self.assertEqual('network_ip_availability', sot.resource_key) @@ -56,20 +60,25 @@ def test_make_it(self): sot = network_ip_availability.NetworkIPAvailability(**EXAMPLE) self.assertEqual(EXAMPLE['network_id'], sot.network_id) self.assertEqual(EXAMPLE['network_name'], sot.network_name) - self.assertEqual(EXAMPLE['subnet_ip_availability'], - sot.subnet_ip_availability) + self.assertEqual( + EXAMPLE['subnet_ip_availability'], sot.subnet_ip_availability + ) self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['total_ips'], sot.total_ips) self.assertEqual(EXAMPLE['used_ips'], sot.used_ips) def test_make_it_with_optional(self): sot = network_ip_availability.NetworkIPAvailability( - **EXAMPLE_WITH_OPTIONAL) + **EXAMPLE_WITH_OPTIONAL + ) self.assertEqual(EXAMPLE_WITH_OPTIONAL['network_id'], sot.network_id) - self.assertEqual(EXAMPLE_WITH_OPTIONAL['network_name'], - sot.network_name) - self.assertEqual(EXAMPLE_WITH_OPTIONAL['subnet_ip_availability'], - sot.subnet_ip_availability) + self.assertEqual( + EXAMPLE_WITH_OPTIONAL['network_name'], sot.network_name + ) + self.assertEqual( + EXAMPLE_WITH_OPTIONAL['subnet_ip_availability'], + sot.subnet_ip_availability, + ) self.assertEqual(EXAMPLE_WITH_OPTIONAL['project_id'], sot.project_id) self.assertEqual(EXAMPLE_WITH_OPTIONAL['total_ips'], sot.total_ips) self.assertEqual(EXAMPLE_WITH_OPTIONAL['used_ips'], sot.used_ips) diff --git a/openstack/tests/unit/network/v2/test_network_segment_range.py b/openstack/tests/unit/network/v2/test_network_segment_range.py index 2e32f299d..636ab5995 100644 --- a/openstack/tests/unit/network/v2/test_network_segment_range.py +++ b/openstack/tests/unit/network/v2/test_network_segment_range.py @@ -34,12 +34,12 @@ class TestNetworkSegmentRange(base.TestCase): - def test_basic(self): test_seg_range = network_segment_range.NetworkSegmentRange() self.assertEqual('network_segment_range', test_seg_range.resource_key) - self.assertEqual('network_segment_ranges', - test_seg_range.resources_key) + self.assertEqual( + 'network_segment_ranges', test_seg_range.resources_key + ) self.assertEqual('/network_segment_ranges', test_seg_range.base_path) self.assertTrue(test_seg_range.allow_create) @@ -56,8 +56,9 @@ def test_make_it(self): self.assertEqual(EXAMPLE['shared'], test_seg_range.shared) self.assertEqual(EXAMPLE['project_id'], test_seg_range.project_id) self.assertEqual(EXAMPLE['network_type'], test_seg_range.network_type) - self.assertEqual(EXAMPLE['physical_network'], - test_seg_range.physical_network) + self.assertEqual( + EXAMPLE['physical_network'], test_seg_range.physical_network + ) self.assertEqual(EXAMPLE['minimum'], test_seg_range.minimum) self.assertEqual(EXAMPLE['maximum'], test_seg_range.maximum) self.assertEqual(EXAMPLE['used'], test_seg_range.used) diff --git a/openstack/tests/unit/network/v2/test_pool.py b/openstack/tests/unit/network/v2/test_pool.py index 36098582b..ab9509cd8 100644 --- a/openstack/tests/unit/network/v2/test_pool.py +++ b/openstack/tests/unit/network/v2/test_pool.py @@ -41,7 +41,6 @@ class TestPool(base.TestCase): - def test_basic(self): sot = pool.Pool() self.assertEqual('pool', sot.resource_key) @@ -59,8 +58,9 @@ def test_make_it(self): self.assertEqual(EXAMPLE['description'], sot.description) self.assertEqual(EXAMPLE['healthmonitor_id'], sot.health_monitor_id) self.assertEqual(EXAMPLE['health_monitors'], sot.health_monitor_ids) - self.assertEqual(EXAMPLE['health_monitor_status'], - sot.health_monitor_status) + self.assertEqual( + EXAMPLE['health_monitor_status'], sot.health_monitor_status + ) self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['lb_algorithm'], sot.lb_algorithm) self.assertEqual(EXAMPLE['listeners'], sot.listener_ids) @@ -70,8 +70,9 @@ def test_make_it(self): self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['protocol'], sot.protocol) self.assertEqual(EXAMPLE['provider'], sot.provider) - self.assertEqual(EXAMPLE['session_persistence'], - sot.session_persistence) + self.assertEqual( + EXAMPLE['session_persistence'], sot.session_persistence + ) self.assertEqual(EXAMPLE['status'], sot.status) self.assertEqual(EXAMPLE['status_description'], sot.status_description) self.assertEqual(EXAMPLE['subnet_id'], sot.subnet_id) diff --git a/openstack/tests/unit/network/v2/test_pool_member.py b/openstack/tests/unit/network/v2/test_pool_member.py index 478b2fefb..7e836f1ac 100644 --- a/openstack/tests/unit/network/v2/test_pool_member.py +++ b/openstack/tests/unit/network/v2/test_pool_member.py @@ -29,7 +29,6 @@ class TestPoolMember(base.TestCase): - def test_basic(self): sot = pool_member.PoolMember() self.assertEqual('member', sot.resource_key) diff --git a/openstack/tests/unit/network/v2/test_port.py b/openstack/tests/unit/network/v2/test_port.py index e9fc863bf..8e06a9f35 100644 --- a/openstack/tests/unit/network/v2/test_port.py +++ b/openstack/tests/unit/network/v2/test_port.py @@ -45,28 +45,32 @@ 'qos_policy_id': '21', 'propagate_uplink_status': False, 'resource_request': { - 'required': [ - 'CUSTOM_PHYSNET_PUBLIC', 'CUSTOM_VNIC_TYPE_NORMAL'], + 'required': ['CUSTOM_PHYSNET_PUBLIC', 'CUSTOM_VNIC_TYPE_NORMAL'], 'resources': { 'NET_BW_EGR_KILOBIT_PER_SEC': 1, - 'NET_BW_IGR_KILOBIT_PER_SEC': 2}}, + 'NET_BW_IGR_KILOBIT_PER_SEC': 2, + }, + }, 'revision_number': 22, 'security_groups': ['23'], 'status': '25', 'project_id': '26', 'trunk_details': { 'trunk_id': '27', - 'sub_ports': [{ - 'port_id': '28', - 'segmentation_id': 29, - 'segmentation_type': '30', - 'mac_address': '31'}]}, + 'sub_ports': [ + { + 'port_id': '28', + 'segmentation_id': 29, + 'segmentation_type': '30', + 'mac_address': '31', + } + ], + }, 'updated_at': '2016-07-09T12:14:57.233772', } class TestPort(base.TestCase): - def test_basic(self): sot = port.Port() self.assertEqual('port', sot.resource_key) @@ -78,46 +82,51 @@ def test_basic(self): self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertDictEqual({"binding:host_id": "binding:host_id", - "binding:profile": "binding:profile", - "binding:vif_details": "binding:vif_details", - "binding:vif_type": "binding:vif_type", - "binding:vnic_type": "binding:vnic_type", - "description": "description", - "device_id": "device_id", - "device_owner": "device_owner", - "fields": "fields", - "fixed_ips": "fixed_ips", - "id": "id", - "ip_address": "ip_address", - "mac_address": "mac_address", - "name": "name", - "network_id": "network_id", - "security_groups": "security_groups", - "status": "status", - "subnet_id": "subnet_id", - "is_admin_state_up": "admin_state_up", - "is_port_security_enabled": - "port_security_enabled", - "project_id": "project_id", - "security_group_ids": "security_groups", - "limit": "limit", - "marker": "marker", - "any_tags": "tags-any", - "not_any_tags": "not-tags-any", - "not_tags": "not-tags", - "tags": "tags"}, - sot._query_mapping._mapping) + self.assertDictEqual( + { + "binding:host_id": "binding:host_id", + "binding:profile": "binding:profile", + "binding:vif_details": "binding:vif_details", + "binding:vif_type": "binding:vif_type", + "binding:vnic_type": "binding:vnic_type", + "description": "description", + "device_id": "device_id", + "device_owner": "device_owner", + "fields": "fields", + "fixed_ips": "fixed_ips", + "id": "id", + "ip_address": "ip_address", + "mac_address": "mac_address", + "name": "name", + "network_id": "network_id", + "security_groups": "security_groups", + "status": "status", + "subnet_id": "subnet_id", + "is_admin_state_up": "admin_state_up", + "is_port_security_enabled": "port_security_enabled", + "project_id": "project_id", + "security_group_ids": "security_groups", + "limit": "limit", + "marker": "marker", + "any_tags": "tags-any", + "not_any_tags": "not-tags-any", + "not_tags": "not-tags", + "tags": "tags", + }, + sot._query_mapping._mapping, + ) def test_make_it(self): sot = port.Port(**EXAMPLE) self.assertTrue(sot.is_admin_state_up) - self.assertEqual(EXAMPLE['allowed_address_pairs'], - sot.allowed_address_pairs) + self.assertEqual( + EXAMPLE['allowed_address_pairs'], sot.allowed_address_pairs + ) self.assertEqual(EXAMPLE['binding:host_id'], sot.binding_host_id) self.assertEqual(EXAMPLE['binding:profile'], sot.binding_profile) - self.assertEqual(EXAMPLE['binding:vif_details'], - sot.binding_vif_details) + self.assertEqual( + EXAMPLE['binding:vif_details'], sot.binding_vif_details + ) self.assertEqual(EXAMPLE['binding:vif_type'], sot.binding_vif_type) self.assertEqual(EXAMPLE['binding:vnic_type'], sot.binding_vnic_type) self.assertEqual(EXAMPLE['created_at'], sot.created_at) @@ -136,14 +145,17 @@ def test_make_it(self): self.assertEqual(EXAMPLE['mac_address'], sot.mac_address) self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['network_id'], sot.network_id) - self.assertEqual(EXAMPLE['numa_affinity_policy'], - sot.numa_affinity_policy) + self.assertEqual( + EXAMPLE['numa_affinity_policy'], sot.numa_affinity_policy + ) self.assertTrue(sot.is_port_security_enabled) - self.assertEqual(EXAMPLE['qos_network_policy_id'], - sot.qos_network_policy_id) + self.assertEqual( + EXAMPLE['qos_network_policy_id'], sot.qos_network_policy_id + ) self.assertEqual(EXAMPLE['qos_policy_id'], sot.qos_policy_id) - self.assertEqual(EXAMPLE['propagate_uplink_status'], - sot.propagate_uplink_status) + self.assertEqual( + EXAMPLE['propagate_uplink_status'], sot.propagate_uplink_status + ) self.assertEqual(EXAMPLE['resource_request'], sot.resource_request) self.assertEqual(EXAMPLE['revision_number'], sot.revision_number) self.assertEqual(EXAMPLE['security_groups'], sot.security_group_ids) diff --git a/openstack/tests/unit/network/v2/test_port_forwarding.py b/openstack/tests/unit/network/v2/test_port_forwarding.py index 8748cae9e..a95ea9fba 100644 --- a/openstack/tests/unit/network/v2/test_port_forwarding.py +++ b/openstack/tests/unit/network/v2/test_port_forwarding.py @@ -22,39 +22,43 @@ 'internal_port': 80, 'internal_port_id': 'internal-port-uuid', 'external_port': 8080, - 'description': 'description' + 'description': 'description', } class TestFloatingIP(base.TestCase): - def test_basic(self): sot = port_forwarding.PortForwarding() self.assertEqual('port_forwarding', sot.resource_key) self.assertEqual('port_forwardings', sot.resources_key) self.assertEqual( - '/floatingips/%(floatingip_id)s/port_forwardings', - sot.base_path) + '/floatingips/%(floatingip_id)s/port_forwardings', sot.base_path + ) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertDictEqual({'internal_port_id': 'internal_port_id', - 'external_port': 'external_port', - 'limit': 'limit', - 'marker': 'marker', - 'protocol': 'protocol'}, - sot._query_mapping._mapping) + self.assertDictEqual( + { + 'internal_port_id': 'internal_port_id', + 'external_port': 'external_port', + 'limit': 'limit', + 'marker': 'marker', + 'protocol': 'protocol', + }, + sot._query_mapping._mapping, + ) def test_make_it(self): sot = port_forwarding.PortForwarding(**EXAMPLE) self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['floatingip_id'], sot.floatingip_id) self.assertEqual(EXAMPLE['protocol'], sot.protocol) - self.assertEqual(EXAMPLE['internal_ip_address'], - sot.internal_ip_address) + self.assertEqual( + EXAMPLE['internal_ip_address'], sot.internal_ip_address + ) self.assertEqual(EXAMPLE['internal_port'], sot.internal_port) self.assertEqual(EXAMPLE['internal_port_id'], sot.internal_port_id) self.assertEqual(EXAMPLE['external_port'], sot.external_port) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 57403f83f..3b5132846 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -90,9 +90,16 @@ def setUp(self): self.proxy = _proxy.Proxy(self.session) def verify_update( - self, test_method, resource_type, base_path=None, *, - method_args=None, method_kwargs=None, - expected_args=None, expected_kwargs=None, expected_result="result", + self, + test_method, + resource_type, + base_path=None, + *, + method_args=None, + method_kwargs=None, + expected_args=None, + expected_kwargs=None, + expected_result="result", mock_method="openstack.network.v2._proxy.Proxy._update", ): super().verify_update( @@ -104,12 +111,19 @@ def verify_update( expected_args=expected_args, expected_kwargs=expected_kwargs, expected_result=expected_result, - mock_method=mock_method) + mock_method=mock_method, + ) def verify_delete( - self, test_method, resource_type, ignore_missing=True, *, - method_args=None, method_kwargs=None, - expected_args=None, expected_kwargs=None, + self, + test_method, + resource_type, + ignore_missing=True, + *, + method_args=None, + method_kwargs=None, + expected_args=None, + expected_kwargs=None, mock_method="openstack.network.v2._proxy.Proxy._delete", ): super().verify_delete( @@ -120,94 +134,105 @@ def verify_delete( method_kwargs=method_kwargs, expected_args=expected_args, expected_kwargs=expected_kwargs, - mock_method=mock_method) + mock_method=mock_method, + ) class TestNetworkAddressGroup(TestNetworkProxy): def test_address_group_create_attrs(self): - self.verify_create(self.proxy.create_address_group, - address_group.AddressGroup) + self.verify_create( + self.proxy.create_address_group, address_group.AddressGroup + ) def test_address_group_delete(self): - self.verify_delete(self.proxy.delete_address_group, - address_group.AddressGroup, - False) + self.verify_delete( + self.proxy.delete_address_group, address_group.AddressGroup, False + ) def test_address_group_delete_ignore(self): - self.verify_delete(self.proxy.delete_address_group, - address_group.AddressGroup, - True) + self.verify_delete( + self.proxy.delete_address_group, address_group.AddressGroup, True + ) def test_address_group_find(self): - self.verify_find(self.proxy.find_address_group, - address_group.AddressGroup) + self.verify_find( + self.proxy.find_address_group, address_group.AddressGroup + ) def test_address_group_get(self): - self.verify_get(self.proxy.get_address_group, - address_group.AddressGroup) + self.verify_get( + self.proxy.get_address_group, address_group.AddressGroup + ) def test_address_groups(self): - self.verify_list(self.proxy.address_groups, - address_group.AddressGroup) + self.verify_list(self.proxy.address_groups, address_group.AddressGroup) def test_address_group_update(self): - self.verify_update(self.proxy.update_address_group, - address_group.AddressGroup) + self.verify_update( + self.proxy.update_address_group, address_group.AddressGroup + ) - @mock.patch('openstack.network.v2._proxy.Proxy.' - 'add_addresses_to_address_group') + @mock.patch( + 'openstack.network.v2._proxy.Proxy.' 'add_addresses_to_address_group' + ) def test_add_addresses_to_address_group(self, add_addresses): data = mock.sentinel - self.proxy.add_addresses_to_address_group(address_group.AddressGroup, - data) + self.proxy.add_addresses_to_address_group( + address_group.AddressGroup, data + ) - add_addresses.assert_called_once_with(address_group.AddressGroup, - data) + add_addresses.assert_called_once_with(address_group.AddressGroup, data) - @mock.patch('openstack.network.v2._proxy.Proxy.' - 'remove_addresses_from_address_group') + @mock.patch( + 'openstack.network.v2._proxy.Proxy.' + 'remove_addresses_from_address_group' + ) def test_remove_addresses_from_address_group(self, remove_addresses): data = mock.sentinel self.proxy.remove_addresses_from_address_group( - address_group.AddressGroup, - data) + address_group.AddressGroup, data + ) - remove_addresses.assert_called_once_with(address_group.AddressGroup, - data) + remove_addresses.assert_called_once_with( + address_group.AddressGroup, data + ) class TestNetworkAddressScope(TestNetworkProxy): def test_address_scope_create_attrs(self): - self.verify_create(self.proxy.create_address_scope, - address_scope.AddressScope) + self.verify_create( + self.proxy.create_address_scope, address_scope.AddressScope + ) def test_address_scope_delete(self): - self.verify_delete(self.proxy.delete_address_scope, - address_scope.AddressScope, - False) + self.verify_delete( + self.proxy.delete_address_scope, address_scope.AddressScope, False + ) def test_address_scope_delete_ignore(self): - self.verify_delete(self.proxy.delete_address_scope, - address_scope.AddressScope, - True) + self.verify_delete( + self.proxy.delete_address_scope, address_scope.AddressScope, True + ) def test_address_scope_find(self): - self.verify_find(self.proxy.find_address_scope, - address_scope.AddressScope) + self.verify_find( + self.proxy.find_address_scope, address_scope.AddressScope + ) def test_address_scope_get(self): - self.verify_get(self.proxy.get_address_scope, - address_scope.AddressScope) + self.verify_get( + self.proxy.get_address_scope, address_scope.AddressScope + ) def test_address_scopes(self): - self.verify_list(self.proxy.address_scopes, - address_scope.AddressScope) + self.verify_list(self.proxy.address_scopes, address_scope.AddressScope) def test_address_scope_update(self): - self.verify_update(self.proxy.update_address_scope, - address_scope.AddressScope) + self.verify_update( + self.proxy.update_address_scope, address_scope.AddressScope + ) class TestNetworkAgent(TestNetworkProxy): @@ -227,15 +252,15 @@ def test_agent_update(self): class TestNetworkAvailability(TestNetworkProxy): def test_availability_zones(self): self.verify_list( - self.proxy.availability_zones, - availability_zone.AvailabilityZone) + self.proxy.availability_zones, availability_zone.AvailabilityZone + ) def test_dhcp_agent_hosting_networks(self): self.verify_list( self.proxy.dhcp_agent_hosting_networks, network.DHCPAgentHostingNetwork, method_kwargs={'agent': AGENT_ID}, - expected_kwargs={'agent_id': AGENT_ID} + expected_kwargs={'agent_id': AGENT_ID}, ) def test_network_hosting_dhcp_agents(self): @@ -243,7 +268,7 @@ def test_network_hosting_dhcp_agents(self): self.proxy.network_hosting_dhcp_agents, agent.NetworkHostingDHCPAgent, method_kwargs={'network': NETWORK_ID}, - expected_kwargs={'network_id': NETWORK_ID} + expected_kwargs={'network_id': NETWORK_ID}, ) @@ -258,17 +283,29 @@ def test_floating_ip_create_attrs(self): self.verify_create(self.proxy.create_ip, floating_ip.FloatingIP) def test_floating_ip_delete(self): - self.verify_delete(self.proxy.delete_ip, floating_ip.FloatingIP, - False, expected_kwargs={'if_revision': None}) + self.verify_delete( + self.proxy.delete_ip, + floating_ip.FloatingIP, + False, + expected_kwargs={'if_revision': None}, + ) def test_floating_ip_delete_ignore(self): - self.verify_delete(self.proxy.delete_ip, floating_ip.FloatingIP, - True, expected_kwargs={'if_revision': None}) + self.verify_delete( + self.proxy.delete_ip, + floating_ip.FloatingIP, + True, + expected_kwargs={'if_revision': None}, + ) def test_floating_ip_delete_if_revision(self): - self.verify_delete(self.proxy.delete_ip, floating_ip.FloatingIP, - True, method_kwargs={'if_revision': 42}, - expected_kwargs={'if_revision': 42}) + self.verify_delete( + self.proxy.delete_ip, + floating_ip.FloatingIP, + True, + method_kwargs={'if_revision': 42}, + expected_kwargs={'if_revision': 42}, + ) def test_floating_ip_find(self): self.verify_find(self.proxy.find_ip, floating_ip.FloatingIP) @@ -280,46 +317,60 @@ def test_ips(self): self.verify_list(self.proxy.ips, floating_ip.FloatingIP) def test_floating_ip_update(self): - self.verify_update(self.proxy.update_ip, floating_ip.FloatingIP, - expected_kwargs={'x': 1, 'y': 2, 'z': 3, - 'if_revision': None}) + self.verify_update( + self.proxy.update_ip, + floating_ip.FloatingIP, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, 'if_revision': None}, + ) def test_floating_ip_update_if_revision(self): - self.verify_update(self.proxy.update_ip, floating_ip.FloatingIP, - method_kwargs={'x': 1, 'y': 2, 'z': 3, - 'if_revision': 42}, - expected_kwargs={'x': 1, 'y': 2, 'z': 3, - 'if_revision': 42}) + self.verify_update( + self.proxy.update_ip, + floating_ip.FloatingIP, + method_kwargs={'x': 1, 'y': 2, 'z': 3, 'if_revision': 42}, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, 'if_revision': 42}, + ) class TestNetworkHealthMonitor(TestNetworkProxy): def test_health_monitor_create_attrs(self): - self.verify_create(self.proxy.create_health_monitor, - health_monitor.HealthMonitor) + self.verify_create( + self.proxy.create_health_monitor, health_monitor.HealthMonitor + ) def test_health_monitor_delete(self): - self.verify_delete(self.proxy.delete_health_monitor, - health_monitor.HealthMonitor, False) + self.verify_delete( + self.proxy.delete_health_monitor, + health_monitor.HealthMonitor, + False, + ) def test_health_monitor_delete_ignore(self): - self.verify_delete(self.proxy.delete_health_monitor, - health_monitor.HealthMonitor, True) + self.verify_delete( + self.proxy.delete_health_monitor, + health_monitor.HealthMonitor, + True, + ) def test_health_monitor_find(self): - self.verify_find(self.proxy.find_health_monitor, - health_monitor.HealthMonitor) + self.verify_find( + self.proxy.find_health_monitor, health_monitor.HealthMonitor + ) def test_health_monitor_get(self): - self.verify_get(self.proxy.get_health_monitor, - health_monitor.HealthMonitor) + self.verify_get( + self.proxy.get_health_monitor, health_monitor.HealthMonitor + ) def test_health_monitors(self): - self.verify_list(self.proxy.health_monitors, - health_monitor.HealthMonitor) + self.verify_list( + self.proxy.health_monitors, health_monitor.HealthMonitor + ) def test_health_monitor_update(self): - self.verify_update(self.proxy.update_health_monitor, - health_monitor.HealthMonitor) + self.verify_update( + self.proxy.update_health_monitor, health_monitor.HealthMonitor + ) class TestNetworkListener(TestNetworkProxy): @@ -327,12 +378,12 @@ def test_listener_create_attrs(self): self.verify_create(self.proxy.create_listener, listener.Listener) def test_listener_delete(self): - self.verify_delete(self.proxy.delete_listener, - listener.Listener, False) + self.verify_delete( + self.proxy.delete_listener, listener.Listener, False + ) def test_listener_delete_ignore(self): - self.verify_delete(self.proxy.delete_listener, - listener.Listener, True) + self.verify_delete(self.proxy.delete_listener, listener.Listener, True) def test_listener_find(self): self.verify_find(self.proxy.find_listener, listener.Listener) @@ -349,90 +400,122 @@ def test_listener_update(self): class TestNetworkLoadBalancer(TestNetworkProxy): def test_load_balancer_create_attrs(self): - self.verify_create(self.proxy.create_load_balancer, - load_balancer.LoadBalancer) + self.verify_create( + self.proxy.create_load_balancer, load_balancer.LoadBalancer + ) def test_load_balancer_delete(self): - self.verify_delete(self.proxy.delete_load_balancer, - load_balancer.LoadBalancer, False) + self.verify_delete( + self.proxy.delete_load_balancer, load_balancer.LoadBalancer, False + ) def test_load_balancer_delete_ignore(self): - self.verify_delete(self.proxy.delete_load_balancer, - load_balancer.LoadBalancer, True) + self.verify_delete( + self.proxy.delete_load_balancer, load_balancer.LoadBalancer, True + ) def test_load_balancer_find(self): - self.verify_find(self.proxy.find_load_balancer, - load_balancer.LoadBalancer) + self.verify_find( + self.proxy.find_load_balancer, load_balancer.LoadBalancer + ) def test_load_balancer_get(self): - self.verify_get(self.proxy.get_load_balancer, - load_balancer.LoadBalancer) + self.verify_get( + self.proxy.get_load_balancer, load_balancer.LoadBalancer + ) def test_load_balancers(self): - self.verify_list(self.proxy.load_balancers, - load_balancer.LoadBalancer) + self.verify_list(self.proxy.load_balancers, load_balancer.LoadBalancer) def test_load_balancer_update(self): - self.verify_update(self.proxy.update_load_balancer, - load_balancer.LoadBalancer) + self.verify_update( + self.proxy.update_load_balancer, load_balancer.LoadBalancer + ) class TestNetworkMeteringLabel(TestNetworkProxy): def test_metering_label_create_attrs(self): - self.verify_create(self.proxy.create_metering_label, - metering_label.MeteringLabel) + self.verify_create( + self.proxy.create_metering_label, metering_label.MeteringLabel + ) def test_metering_label_delete(self): - self.verify_delete(self.proxy.delete_metering_label, - metering_label.MeteringLabel, False) + self.verify_delete( + self.proxy.delete_metering_label, + metering_label.MeteringLabel, + False, + ) def test_metering_label_delete_ignore(self): - self.verify_delete(self.proxy.delete_metering_label, - metering_label.MeteringLabel, True) + self.verify_delete( + self.proxy.delete_metering_label, + metering_label.MeteringLabel, + True, + ) def test_metering_label_find(self): - self.verify_find(self.proxy.find_metering_label, - metering_label.MeteringLabel) + self.verify_find( + self.proxy.find_metering_label, metering_label.MeteringLabel + ) def test_metering_label_get(self): - self.verify_get(self.proxy.get_metering_label, - metering_label.MeteringLabel) + self.verify_get( + self.proxy.get_metering_label, metering_label.MeteringLabel + ) def test_metering_labels(self): - self.verify_list(self.proxy.metering_labels, - metering_label.MeteringLabel) + self.verify_list( + self.proxy.metering_labels, metering_label.MeteringLabel + ) def test_metering_label_update(self): - self.verify_update(self.proxy.update_metering_label, - metering_label.MeteringLabel) + self.verify_update( + self.proxy.update_metering_label, metering_label.MeteringLabel + ) def test_metering_label_rule_create_attrs(self): - self.verify_create(self.proxy.create_metering_label_rule, - metering_label_rule.MeteringLabelRule) + self.verify_create( + self.proxy.create_metering_label_rule, + metering_label_rule.MeteringLabelRule, + ) def test_metering_label_rule_delete(self): - self.verify_delete(self.proxy.delete_metering_label_rule, - metering_label_rule.MeteringLabelRule, False) + self.verify_delete( + self.proxy.delete_metering_label_rule, + metering_label_rule.MeteringLabelRule, + False, + ) def test_metering_label_rule_delete_ignore(self): - self.verify_delete(self.proxy.delete_metering_label_rule, - metering_label_rule.MeteringLabelRule, True) + self.verify_delete( + self.proxy.delete_metering_label_rule, + metering_label_rule.MeteringLabelRule, + True, + ) def test_metering_label_rule_find(self): - self.verify_find(self.proxy.find_metering_label_rule, - metering_label_rule.MeteringLabelRule) + self.verify_find( + self.proxy.find_metering_label_rule, + metering_label_rule.MeteringLabelRule, + ) def test_metering_label_rule_get(self): - self.verify_get(self.proxy.get_metering_label_rule, - metering_label_rule.MeteringLabelRule) + self.verify_get( + self.proxy.get_metering_label_rule, + metering_label_rule.MeteringLabelRule, + ) def test_metering_label_rules(self): - self.verify_list(self.proxy.metering_label_rules, - metering_label_rule.MeteringLabelRule) + self.verify_list( + self.proxy.metering_label_rules, + metering_label_rule.MeteringLabelRule, + ) def test_metering_label_rule_update(self): - self.verify_update(self.proxy.update_metering_label_rule, - metering_label_rule.MeteringLabelRule) + self.verify_update( + self.proxy.update_metering_label_rule, + metering_label_rule.MeteringLabelRule, + ) class TestNetworkNetwork(TestNetworkProxy): @@ -440,17 +523,29 @@ def test_network_create_attrs(self): self.verify_create(self.proxy.create_network, network.Network) def test_network_delete(self): - self.verify_delete(self.proxy.delete_network, network.Network, False, - expected_kwargs={'if_revision': None}) + self.verify_delete( + self.proxy.delete_network, + network.Network, + False, + expected_kwargs={'if_revision': None}, + ) def test_network_delete_ignore(self): - self.verify_delete(self.proxy.delete_network, network.Network, True, - expected_kwargs={'if_revision': None}) + self.verify_delete( + self.proxy.delete_network, + network.Network, + True, + expected_kwargs={'if_revision': None}, + ) def test_network_delete_if_revision(self): - self.verify_delete(self.proxy.delete_network, network.Network, True, - method_kwargs={'if_revision': 42}, - expected_kwargs={'if_revision': 42}) + self.verify_delete( + self.proxy.delete_network, + network.Network, + True, + method_kwargs={'if_revision': 42}, + expected_kwargs={'if_revision': 42}, + ) def test_network_find(self): self.verify_find(self.proxy.find_network, network.Network) @@ -462,7 +557,8 @@ def test_network_find_with_filter(self): method_args=["net1"], method_kwargs={"project_id": "1"}, expected_args=[network.Network, "net1"], - expected_kwargs={"project_id": "1", "ignore_missing": True}) + expected_kwargs={"project_id": "1", "ignore_missing": True}, + ) def test_network_get(self): self.verify_get(self.proxy.get_network, network.Network) @@ -471,16 +567,19 @@ def test_networks(self): self.verify_list(self.proxy.networks, network.Network) def test_network_update(self): - self.verify_update(self.proxy.update_network, network.Network, - expected_kwargs={'x': 1, 'y': 2, 'z': 3, - 'if_revision': None}) + self.verify_update( + self.proxy.update_network, + network.Network, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, 'if_revision': None}, + ) def test_network_update_if_revision(self): - self.verify_update(self.proxy.update_network, network.Network, - method_kwargs={'x': 1, 'y': 2, 'z': 3, - 'if_revision': 42}, - expected_kwargs={'x': 1, 'y': 2, 'z': 3, - 'if_revision': 42}) + self.verify_update( + self.proxy.update_network, + network.Network, + method_kwargs={'x': 1, 'y': 2, 'z': 3, 'if_revision': 42}, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, 'if_revision': 42}, + ) class TestNetworkFlavor(TestNetworkProxy): @@ -508,17 +607,29 @@ def test_local_ip_create_attrs(self): self.verify_create(self.proxy.create_local_ip, local_ip.LocalIP) def test_local_ip_delete(self): - self.verify_delete(self.proxy.delete_local_ip, local_ip.LocalIP, - False, expected_kwargs={'if_revision': None}) + self.verify_delete( + self.proxy.delete_local_ip, + local_ip.LocalIP, + False, + expected_kwargs={'if_revision': None}, + ) def test_local_ip_delete_ignore(self): - self.verify_delete(self.proxy.delete_local_ip, local_ip.LocalIP, - True, expected_kwargs={'if_revision': None}) + self.verify_delete( + self.proxy.delete_local_ip, + local_ip.LocalIP, + True, + expected_kwargs={'if_revision': None}, + ) def test_local_ip_delete_if_revision(self): - self.verify_delete(self.proxy.delete_local_ip, local_ip.LocalIP, - True, method_kwargs={'if_revision': 42}, - expected_kwargs={'if_revision': 42}) + self.verify_delete( + self.proxy.delete_local_ip, + local_ip.LocalIP, + True, + method_kwargs={'if_revision': 42}, + expected_kwargs={'if_revision': 42}, + ) def test_local_ip_find(self): self.verify_find(self.proxy.find_local_ip, local_ip.LocalIP) @@ -530,24 +641,29 @@ def test_local_ips(self): self.verify_list(self.proxy.local_ips, local_ip.LocalIP) def test_local_ip_update(self): - self.verify_update(self.proxy.update_local_ip, local_ip.LocalIP, - expected_kwargs={'x': 1, 'y': 2, 'z': 3, - 'if_revision': None}) + self.verify_update( + self.proxy.update_local_ip, + local_ip.LocalIP, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, 'if_revision': None}, + ) def test_local_ip_update_if_revision(self): - self.verify_update(self.proxy.update_local_ip, local_ip.LocalIP, - method_kwargs={'x': 1, 'y': 2, 'z': 3, - 'if_revision': 42}, - expected_kwargs={'x': 1, 'y': 2, 'z': 3, - 'if_revision': 42}) + self.verify_update( + self.proxy.update_local_ip, + local_ip.LocalIP, + method_kwargs={'x': 1, 'y': 2, 'z': 3, 'if_revision': 42}, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, 'if_revision': 42}, + ) class TestNetworkLocalIpAssociation(TestNetworkProxy): def test_local_ip_association_create_attrs(self): - self.verify_create(self.proxy.create_local_ip_association, - local_ip_association.LocalIPAssociation, - method_kwargs={'local_ip': LOCAL_IP_ID}, - expected_kwargs={'local_ip_id': LOCAL_IP_ID}) + self.verify_create( + self.proxy.create_local_ip_association, + local_ip_association.LocalIPAssociation, + method_kwargs={'local_ip': LOCAL_IP_ID}, + expected_kwargs={'local_ip_id': LOCAL_IP_ID}, + ) def test_local_ip_association_delete(self): self.verify_delete( @@ -556,8 +672,8 @@ def test_local_ip_association_delete(self): ignore_missing=False, method_args=[LOCAL_IP_ID, "resource_or_id"], expected_args=["resource_or_id"], - expected_kwargs={'if_revision': None, - 'local_ip_id': LOCAL_IP_ID}) + expected_kwargs={'if_revision': None, 'local_ip_id': LOCAL_IP_ID}, + ) def test_local_ip_association_delete_ignore(self): self.verify_delete( @@ -566,8 +682,8 @@ def test_local_ip_association_delete_ignore(self): ignore_missing=True, method_args=[LOCAL_IP_ID, "resource_or_id"], expected_args=["resource_or_id"], - expected_kwargs={'if_revision': None, - 'local_ip_id': LOCAL_IP_ID}) + expected_kwargs={'if_revision': None, 'local_ip_id': LOCAL_IP_ID}, + ) def test_local_ip_association_find(self): lip = local_ip.LocalIP.new(id=LOCAL_IP_ID) @@ -578,9 +694,13 @@ def test_local_ip_association_find(self): method_args=['local_ip_association_id', lip], expected_args=[ local_ip_association.LocalIPAssociation, - 'local_ip_association_id'], + 'local_ip_association_id', + ], expected_kwargs={ - 'ignore_missing': True, 'local_ip_id': LOCAL_IP_ID}) + 'ignore_missing': True, + 'local_ip_id': LOCAL_IP_ID, + }, + ) def test_local_ip_association_get(self): lip = local_ip.LocalIP.new(id=LOCAL_IP_ID) @@ -591,60 +711,80 @@ def test_local_ip_association_get(self): method_args=['local_ip_association_id', lip], expected_args=[ local_ip_association.LocalIPAssociation, - 'local_ip_association_id'], - expected_kwargs={'local_ip_id': LOCAL_IP_ID}) + 'local_ip_association_id', + ], + expected_kwargs={'local_ip_id': LOCAL_IP_ID}, + ) def test_local_ip_associations(self): - self.verify_list(self.proxy.local_ip_associations, - local_ip_association.LocalIPAssociation, - method_kwargs={'local_ip': LOCAL_IP_ID}, - expected_kwargs={'local_ip_id': LOCAL_IP_ID}) + self.verify_list( + self.proxy.local_ip_associations, + local_ip_association.LocalIPAssociation, + method_kwargs={'local_ip': LOCAL_IP_ID}, + expected_kwargs={'local_ip_id': LOCAL_IP_ID}, + ) class TestNetworkServiceProfile(TestNetworkProxy): def test_service_profile_create_attrs(self): - self.verify_create(self.proxy.create_service_profile, - service_profile.ServiceProfile) + self.verify_create( + self.proxy.create_service_profile, service_profile.ServiceProfile + ) def test_service_profile_delete(self): - self.verify_delete(self.proxy.delete_service_profile, - service_profile.ServiceProfile, True) + self.verify_delete( + self.proxy.delete_service_profile, + service_profile.ServiceProfile, + True, + ) def test_service_profile_find(self): - self.verify_find(self.proxy.find_service_profile, - service_profile.ServiceProfile) + self.verify_find( + self.proxy.find_service_profile, service_profile.ServiceProfile + ) def test_service_profile_get(self): - self.verify_get(self.proxy.get_service_profile, - service_profile.ServiceProfile) + self.verify_get( + self.proxy.get_service_profile, service_profile.ServiceProfile + ) def test_service_profiles(self): - self.verify_list(self.proxy.service_profiles, - service_profile.ServiceProfile) + self.verify_list( + self.proxy.service_profiles, service_profile.ServiceProfile + ) def test_service_profile_update(self): - self.verify_update(self.proxy.update_service_profile, - service_profile.ServiceProfile) + self.verify_update( + self.proxy.update_service_profile, service_profile.ServiceProfile + ) class TestNetworkIpAvailability(TestNetworkProxy): def test_network_ip_availability_find(self): - self.verify_find(self.proxy.find_network_ip_availability, - network_ip_availability.NetworkIPAvailability) + self.verify_find( + self.proxy.find_network_ip_availability, + network_ip_availability.NetworkIPAvailability, + ) def test_network_ip_availability_get(self): - self.verify_get(self.proxy.get_network_ip_availability, - network_ip_availability.NetworkIPAvailability) + self.verify_get( + self.proxy.get_network_ip_availability, + network_ip_availability.NetworkIPAvailability, + ) def test_network_ip_availabilities(self): - self.verify_list(self.proxy.network_ip_availabilities, - network_ip_availability.NetworkIPAvailability) + self.verify_list( + self.proxy.network_ip_availabilities, + network_ip_availability.NetworkIPAvailability, + ) def test_pool_member_create_attrs(self): - self.verify_create(self.proxy.create_pool_member, - pool_member.PoolMember, - method_kwargs={"pool": "test_id"}, - expected_kwargs={"pool_id": "test_id"}) + self.verify_create( + self.proxy.create_pool_member, + pool_member.PoolMember, + method_kwargs={"pool": "test_id"}, + expected_kwargs={"pool_id": "test_id"}, + ) class TestNetworkPoolMember(TestNetworkProxy): @@ -654,7 +794,8 @@ def test_pool_member_delete(self): pool_member.PoolMember, ignore_missing=False, method_kwargs={"pool": "test_id"}, - expected_kwargs={"pool_id": "test_id"}) + expected_kwargs={"pool_id": "test_id"}, + ) def test_pool_member_delete_ignore(self): self.verify_delete( @@ -662,7 +803,8 @@ def test_pool_member_delete_ignore(self): pool_member.PoolMember, ignore_missing=True, method_kwargs={"pool": "test_id"}, - expected_kwargs={"pool_id": "test_id"}) + expected_kwargs={"pool_id": "test_id"}, + ) def test_pool_member_find(self): self._verify( @@ -670,7 +812,8 @@ def test_pool_member_find(self): self.proxy.find_pool_member, method_args=["MEMBER", "POOL"], expected_args=[pool_member.PoolMember, "MEMBER"], - expected_kwargs={"pool_id": "POOL", "ignore_missing": True}) + expected_kwargs={"pool_id": "POOL", "ignore_missing": True}, + ) def test_pool_member_get(self): self._verify( @@ -678,14 +821,17 @@ def test_pool_member_get(self): self.proxy.get_pool_member, method_args=["MEMBER", "POOL"], expected_args=[pool_member.PoolMember, "MEMBER"], - expected_kwargs={"pool_id": "POOL"}) + expected_kwargs={"pool_id": "POOL"}, + ) def test_pool_members(self): self.verify_list( - self.proxy.pool_members, pool_member.PoolMember, + self.proxy.pool_members, + pool_member.PoolMember, method_args=["test_id"], expected_args=[], - expected_kwargs={"pool_id": "test_id"}) + expected_kwargs={"pool_id": "test_id"}, + ) def test_pool_member_update(self): self._verify( @@ -693,7 +839,8 @@ def test_pool_member_update(self): self.proxy.update_pool_member, method_args=["MEMBER", "POOL"], expected_args=[pool_member.PoolMember, "MEMBER"], - expected_kwargs={"pool_id": "POOL"}) + expected_kwargs={"pool_id": "POOL"}, + ) class TestNetworkPool(TestNetworkProxy): @@ -722,17 +869,29 @@ def test_port_create_attrs(self): self.verify_create(self.proxy.create_port, port.Port) def test_port_delete(self): - self.verify_delete(self.proxy.delete_port, port.Port, False, - expected_kwargs={'if_revision': None}) + self.verify_delete( + self.proxy.delete_port, + port.Port, + False, + expected_kwargs={'if_revision': None}, + ) def test_port_delete_ignore(self): - self.verify_delete(self.proxy.delete_port, port.Port, True, - expected_kwargs={'if_revision': None}) + self.verify_delete( + self.proxy.delete_port, + port.Port, + True, + expected_kwargs={'if_revision': None}, + ) def test_port_delete_if_revision(self): - self.verify_delete(self.proxy.delete_port, port.Port, True, - method_kwargs={'if_revision': 42}, - expected_kwargs={'if_revision': 42}) + self.verify_delete( + self.proxy.delete_port, + port.Port, + True, + method_kwargs={'if_revision': 42}, + expected_kwargs={'if_revision': 42}, + ) def test_port_find(self): self.verify_find(self.proxy.find_port, port.Port) @@ -744,16 +903,19 @@ def test_ports(self): self.verify_list(self.proxy.ports, port.Port) def test_port_update(self): - self.verify_update(self.proxy.update_port, port.Port, - expected_kwargs={'x': 1, 'y': 2, 'z': 3, - 'if_revision': None}) + self.verify_update( + self.proxy.update_port, + port.Port, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, 'if_revision': None}, + ) def test_port_update_if_revision(self): - self.verify_update(self.proxy.update_port, port.Port, - method_kwargs={'x': 1, 'y': 2, 'z': 3, - 'if_revision': 42}, - expected_kwargs={'x': 1, 'y': 2, 'z': 3, - 'if_revision': 42}) + self.verify_update( + self.proxy.update_port, + port.Port, + method_kwargs={'x': 1, 'y': 2, 'z': 3, 'if_revision': 42}, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, 'if_revision': 42}, + ) @mock.patch('openstack.network.v2._proxy.Proxy._bulk_create') def test_ports_create(self, bc): @@ -770,7 +932,8 @@ def test_qos_bandwidth_limit_rule_create_attrs(self): self.proxy.create_qos_bandwidth_limit_rule, qos_bandwidth_limit_rule.QoSBandwidthLimitRule, method_kwargs={'qos_policy': QOS_POLICY_ID}, - expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}, + ) def test_qos_bandwidth_limit_rule_delete(self): self.verify_delete( @@ -779,7 +942,8 @@ def test_qos_bandwidth_limit_rule_delete(self): ignore_missing=False, method_args=["resource_or_id", QOS_POLICY_ID], expected_args=["resource_or_id"], - expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}, + ) def test_qos_bandwidth_limit_rule_delete_ignore(self): self.verify_delete( @@ -788,7 +952,8 @@ def test_qos_bandwidth_limit_rule_delete_ignore(self): ignore_missing=True, method_args=["resource_or_id", QOS_POLICY_ID], expected_args=["resource_or_id"], - expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}, + ) def test_qos_bandwidth_limit_rule_find(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) @@ -798,23 +963,29 @@ def test_qos_bandwidth_limit_rule_find(self): method_args=['rule_id', policy], expected_args=[ qos_bandwidth_limit_rule.QoSBandwidthLimitRule, - 'rule_id'], + 'rule_id', + ], expected_kwargs={ - 'ignore_missing': True, 'qos_policy_id': QOS_POLICY_ID}) + 'ignore_missing': True, + 'qos_policy_id': QOS_POLICY_ID, + }, + ) def test_qos_bandwidth_limit_rule_get(self): self.verify_get( self.proxy.get_qos_bandwidth_limit_rule, qos_bandwidth_limit_rule.QoSBandwidthLimitRule, method_kwargs={'qos_policy': QOS_POLICY_ID}, - expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}, + ) def test_qos_bandwidth_limit_rules(self): self.verify_list( self.proxy.qos_bandwidth_limit_rules, qos_bandwidth_limit_rule.QoSBandwidthLimitRule, method_kwargs={'qos_policy': QOS_POLICY_ID}, - expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}, + ) def test_qos_bandwidth_limit_rule_update(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) @@ -825,8 +996,10 @@ def test_qos_bandwidth_limit_rule_update(self): method_kwargs={'foo': 'bar'}, expected_args=[ qos_bandwidth_limit_rule.QoSBandwidthLimitRule, - 'rule_id'], - expected_kwargs={'qos_policy_id': QOS_POLICY_ID, 'foo': 'bar'}) + 'rule_id', + ], + expected_kwargs={'qos_policy_id': QOS_POLICY_ID, 'foo': 'bar'}, + ) class TestNetworkQosDscpMarking(TestNetworkProxy): @@ -835,7 +1008,8 @@ def test_qos_dscp_marking_rule_create_attrs(self): self.proxy.create_qos_dscp_marking_rule, qos_dscp_marking_rule.QoSDSCPMarkingRule, method_kwargs={'qos_policy': QOS_POLICY_ID}, - expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}, + ) def test_qos_dscp_marking_rule_delete(self): self.verify_delete( @@ -844,7 +1018,8 @@ def test_qos_dscp_marking_rule_delete(self): ignore_missing=False, method_args=["resource_or_id", QOS_POLICY_ID], expected_args=["resource_or_id"], - expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}, + ) def test_qos_dscp_marking_rule_delete_ignore(self): self.verify_delete( @@ -853,7 +1028,8 @@ def test_qos_dscp_marking_rule_delete_ignore(self): ignore_missing=True, method_args=["resource_or_id", QOS_POLICY_ID], expected_args=["resource_or_id"], - expected_kwargs={'qos_policy_id': QOS_POLICY_ID}, ) + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}, + ) def test_qos_dscp_marking_rule_find(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) @@ -862,23 +1038,30 @@ def test_qos_dscp_marking_rule_find(self): self.proxy.find_qos_dscp_marking_rule, method_args=['rule_id', policy], expected_args=[ - qos_dscp_marking_rule.QoSDSCPMarkingRule, 'rule_id'], + qos_dscp_marking_rule.QoSDSCPMarkingRule, + 'rule_id', + ], expected_kwargs={ - 'ignore_missing': True, 'qos_policy_id': QOS_POLICY_ID}) + 'ignore_missing': True, + 'qos_policy_id': QOS_POLICY_ID, + }, + ) def test_qos_dscp_marking_rule_get(self): self.verify_get( self.proxy.get_qos_dscp_marking_rule, qos_dscp_marking_rule.QoSDSCPMarkingRule, method_kwargs={'qos_policy': QOS_POLICY_ID}, - expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}, + ) def test_qos_dscp_marking_rules(self): self.verify_list( self.proxy.qos_dscp_marking_rules, qos_dscp_marking_rule.QoSDSCPMarkingRule, method_kwargs={'qos_policy': QOS_POLICY_ID}, - expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}, + ) def test_qos_dscp_marking_rule_update(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) @@ -889,8 +1072,10 @@ def test_qos_dscp_marking_rule_update(self): method_kwargs={'foo': 'bar'}, expected_args=[ qos_dscp_marking_rule.QoSDSCPMarkingRule, - 'rule_id'], - expected_kwargs={'qos_policy_id': QOS_POLICY_ID, 'foo': 'bar'}) + 'rule_id', + ], + expected_kwargs={'qos_policy_id': QOS_POLICY_ID, 'foo': 'bar'}, + ) class TestNetworkQosMinimumBandwidth(TestNetworkProxy): @@ -899,7 +1084,8 @@ def test_qos_minimum_bandwidth_rule_create_attrs(self): self.proxy.create_qos_minimum_bandwidth_rule, qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, method_kwargs={'qos_policy': QOS_POLICY_ID}, - expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}, + ) def test_qos_minimum_bandwidth_rule_delete(self): self.verify_delete( @@ -908,7 +1094,8 @@ def test_qos_minimum_bandwidth_rule_delete(self): ignore_missing=False, method_args=["resource_or_id", QOS_POLICY_ID], expected_args=["resource_or_id"], - expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}, + ) def test_qos_minimum_bandwidth_rule_delete_ignore(self): self.verify_delete( @@ -917,7 +1104,8 @@ def test_qos_minimum_bandwidth_rule_delete_ignore(self): ignore_missing=True, method_args=["resource_or_id", QOS_POLICY_ID], expected_args=["resource_or_id"], - expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}, + ) def test_qos_minimum_bandwidth_rule_find(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) @@ -927,23 +1115,29 @@ def test_qos_minimum_bandwidth_rule_find(self): method_args=['rule_id', policy], expected_args=[ qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, - 'rule_id'], + 'rule_id', + ], expected_kwargs={ - 'ignore_missing': True, 'qos_policy_id': QOS_POLICY_ID}) + 'ignore_missing': True, + 'qos_policy_id': QOS_POLICY_ID, + }, + ) def test_qos_minimum_bandwidth_rule_get(self): self.verify_get( self.proxy.get_qos_minimum_bandwidth_rule, qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, method_kwargs={'qos_policy': QOS_POLICY_ID}, - expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}, + ) def test_qos_minimum_bandwidth_rules(self): self.verify_list( self.proxy.qos_minimum_bandwidth_rules, qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, method_kwargs={'qos_policy': QOS_POLICY_ID}, - expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}, + ) def test_qos_minimum_bandwidth_rule_update(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) @@ -954,9 +1148,10 @@ def test_qos_minimum_bandwidth_rule_update(self): method_kwargs={'foo': 'bar'}, expected_args=[ qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule, - 'rule_id'], - expected_kwargs={ - 'qos_policy_id': QOS_POLICY_ID, 'foo': 'bar'}) + 'rule_id', + ], + expected_kwargs={'qos_policy_id': QOS_POLICY_ID, 'foo': 'bar'}, + ) class TestNetworkQosMinimumPacketRate(TestNetworkProxy): @@ -965,7 +1160,8 @@ def test_qos_minimum_packet_rate_rule_create_attrs(self): self.proxy.create_qos_minimum_packet_rate_rule, qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule, method_kwargs={'qos_policy': QOS_POLICY_ID}, - expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}, + ) def test_qos_minimum_packet_rate_rule_delete(self): self.verify_delete( @@ -974,7 +1170,8 @@ def test_qos_minimum_packet_rate_rule_delete(self): ignore_missing=False, method_args=["resource_or_id", QOS_POLICY_ID], expected_args=["resource_or_id"], - expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}, + ) def test_qos_minimum_packet_rate_rule_delete_ignore(self): self.verify_delete( @@ -983,7 +1180,8 @@ def test_qos_minimum_packet_rate_rule_delete_ignore(self): ignore_missing=True, method_args=["resource_or_id", QOS_POLICY_ID], expected_args=["resource_or_id"], - expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}, + ) def test_qos_minimum_packet_rate_rule_find(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) @@ -993,23 +1191,29 @@ def test_qos_minimum_packet_rate_rule_find(self): method_args=['rule_id', policy], expected_args=[ qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule, - 'rule_id'], + 'rule_id', + ], expected_kwargs={ - 'ignore_missing': True, 'qos_policy_id': QOS_POLICY_ID}) + 'ignore_missing': True, + 'qos_policy_id': QOS_POLICY_ID, + }, + ) def test_qos_minimum_packet_rate_rule_get(self): self.verify_get( self.proxy.get_qos_minimum_packet_rate_rule, qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule, method_kwargs={'qos_policy': QOS_POLICY_ID}, - expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}, + ) def test_qos_minimum_packet_rate_rules(self): self.verify_list( self.proxy.qos_minimum_packet_rate_rules, qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule, method_kwargs={'qos_policy': QOS_POLICY_ID}, - expected_kwargs={'qos_policy_id': QOS_POLICY_ID}) + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}, + ) def test_qos_minimum_packet_rate_rule_update(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) @@ -1020,19 +1224,22 @@ def test_qos_minimum_packet_rate_rule_update(self): method_kwargs={'foo': 'bar'}, expected_args=[ qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule, - 'rule_id'], - expected_kwargs={ - 'qos_policy_id': QOS_POLICY_ID, 'foo': 'bar'}) + 'rule_id', + ], + expected_kwargs={'qos_policy_id': QOS_POLICY_ID, 'foo': 'bar'}, + ) class TestNetworkQosRuleType(TestNetworkProxy): def test_qos_rule_type_find(self): - self.verify_find(self.proxy.find_qos_rule_type, - qos_rule_type.QoSRuleType) + self.verify_find( + self.proxy.find_qos_rule_type, qos_rule_type.QoSRuleType + ) def test_qos_rule_type_get(self): - self.verify_get(self.proxy.get_qos_rule_type, - qos_rule_type.QoSRuleType) + self.verify_get( + self.proxy.get_qos_rule_type, qos_rule_type.QoSRuleType + ) def test_qos_rule_types(self): self.verify_list(self.proxy.qos_rule_types, qos_rule_type.QoSRuleType) @@ -1058,8 +1265,8 @@ def test_quota_get_details(self, mock_get): method_args=['QUOTA_ID'], method_kwargs={'details': True}, expected_args=[quota.QuotaDetails], - expected_kwargs={ - 'project': fake_quota.id, 'requires_id': False}) + expected_kwargs={'project': fake_quota.id, 'requires_id': False}, + ) mock_get.assert_called_once_with(quota.Quota, 'QUOTA_ID') @mock.patch.object(proxy_base.Proxy, "_get_resource") @@ -1071,8 +1278,8 @@ def test_quota_default_get(self, mock_get): self.proxy.get_quota_default, method_args=['QUOTA_ID'], expected_args=[quota.QuotaDefault], - expected_kwargs={ - 'project': fake_quota.id, 'requires_id': False}) + expected_kwargs={'project': fake_quota.id, 'requires_id': False}, + ) mock_get.assert_called_once_with(quota.Quota, 'QUOTA_ID') def test_quotas(self): @@ -1084,16 +1291,19 @@ def test_quota_update(self): class TestNetworkRbacPolicy(TestNetworkProxy): def test_rbac_policy_create_attrs(self): - self.verify_create(self.proxy.create_rbac_policy, - rbac_policy.RBACPolicy) + self.verify_create( + self.proxy.create_rbac_policy, rbac_policy.RBACPolicy + ) def test_rbac_policy_delete(self): - self.verify_delete(self.proxy.delete_rbac_policy, - rbac_policy.RBACPolicy, False) + self.verify_delete( + self.proxy.delete_rbac_policy, rbac_policy.RBACPolicy, False + ) def test_rbac_policy_delete_ignore(self): - self.verify_delete(self.proxy.delete_rbac_policy, - rbac_policy.RBACPolicy, True) + self.verify_delete( + self.proxy.delete_rbac_policy, rbac_policy.RBACPolicy, True + ) def test_rbac_policy_find(self): self.verify_find(self.proxy.find_rbac_policy, rbac_policy.RBACPolicy) @@ -1105,8 +1315,9 @@ def test_rbac_policies(self): self.verify_list(self.proxy.rbac_policies, rbac_policy.RBACPolicy) def test_rbac_policy_update(self): - self.verify_update(self.proxy.update_rbac_policy, - rbac_policy.RBACPolicy) + self.verify_update( + self.proxy.update_rbac_policy, rbac_policy.RBACPolicy + ) class TestNetworkRouter(TestNetworkProxy): @@ -1114,17 +1325,29 @@ def test_router_create_attrs(self): self.verify_create(self.proxy.create_router, router.Router) def test_router_delete(self): - self.verify_delete(self.proxy.delete_router, router.Router, False, - expected_kwargs={'if_revision': None}) + self.verify_delete( + self.proxy.delete_router, + router.Router, + False, + expected_kwargs={'if_revision': None}, + ) def test_router_delete_ignore(self): - self.verify_delete(self.proxy.delete_router, router.Router, True, - expected_kwargs={'if_revision': None}) + self.verify_delete( + self.proxy.delete_router, + router.Router, + True, + expected_kwargs={'if_revision': None}, + ) def test_router_delete_if_revision(self): - self.verify_delete(self.proxy.delete_router, router.Router, True, - method_kwargs={'if_revision': 42}, - expected_kwargs={'if_revision': 42}) + self.verify_delete( + self.proxy.delete_router, + router.Router, + True, + method_kwargs={'if_revision': 42}, + expected_kwargs={'if_revision': 42}, + ) def test_router_find(self): self.verify_find(self.proxy.find_router, router.Router) @@ -1136,21 +1359,25 @@ def test_routers(self): self.verify_list(self.proxy.routers, router.Router) def test_router_update(self): - self.verify_update(self.proxy.update_router, router.Router, - expected_kwargs={'x': 1, 'y': 2, 'z': 3, - 'if_revision': None}) + self.verify_update( + self.proxy.update_router, + router.Router, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, 'if_revision': None}, + ) def test_router_update_if_revision(self): - self.verify_update(self.proxy.update_router, router.Router, - method_kwargs={'x': 1, 'y': 2, 'z': 3, - 'if_revision': 42}, - expected_kwargs={'x': 1, 'y': 2, 'z': 3, - 'if_revision': 42}) + self.verify_update( + self.proxy.update_router, + router.Router, + method_kwargs={'x': 1, 'y': 2, 'z': 3, 'if_revision': 42}, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, 'if_revision': 42}, + ) @mock.patch.object(proxy_base.Proxy, '_get_resource') @mock.patch.object(router.Router, 'add_interface') - def test_add_interface_to_router_with_port(self, mock_add_interface, - mock_get): + def test_add_interface_to_router_with_port( + self, mock_add_interface, mock_get + ): x_router = router.Router.new(id="ROUTER_ID") mock_get.return_value = x_router @@ -1160,13 +1387,15 @@ def test_add_interface_to_router_with_port(self, mock_add_interface, method_args=["FAKE_ROUTER"], method_kwargs={"port_id": "PORT"}, expected_args=[self.proxy], - expected_kwargs={"port_id": "PORT"}) + expected_kwargs={"port_id": "PORT"}, + ) mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") @mock.patch.object(proxy_base.Proxy, '_get_resource') @mock.patch.object(router.Router, 'add_interface') - def test_add_interface_to_router_with_subnet(self, mock_add_interface, - mock_get): + def test_add_interface_to_router_with_subnet( + self, mock_add_interface, mock_get + ): x_router = router.Router.new(id="ROUTER_ID") mock_get.return_value = x_router @@ -1176,13 +1405,15 @@ def test_add_interface_to_router_with_subnet(self, mock_add_interface, method_args=["FAKE_ROUTER"], method_kwargs={"subnet_id": "SUBNET"}, expected_args=[self.proxy], - expected_kwargs={"subnet_id": "SUBNET"}) + expected_kwargs={"subnet_id": "SUBNET"}, + ) mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") @mock.patch.object(proxy_base.Proxy, '_get_resource') @mock.patch.object(router.Router, 'remove_interface') - def test_remove_interface_from_router_with_port(self, mock_remove, - mock_get): + def test_remove_interface_from_router_with_port( + self, mock_remove, mock_get + ): x_router = router.Router.new(id="ROUTER_ID") mock_get.return_value = x_router @@ -1192,13 +1423,15 @@ def test_remove_interface_from_router_with_port(self, mock_remove, method_args=["FAKE_ROUTER"], method_kwargs={"port_id": "PORT"}, expected_args=[self.proxy], - expected_kwargs={"port_id": "PORT"}) + expected_kwargs={"port_id": "PORT"}, + ) mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") @mock.patch.object(proxy_base.Proxy, '_get_resource') @mock.patch.object(router.Router, 'remove_interface') - def test_remove_interface_from_router_with_subnet(self, mock_remove, - mock_get): + def test_remove_interface_from_router_with_subnet( + self, mock_remove, mock_get + ): x_router = router.Router.new(id="ROUTER_ID") mock_get.return_value = x_router @@ -1208,13 +1441,13 @@ def test_remove_interface_from_router_with_subnet(self, mock_remove, method_args=["FAKE_ROUTER"], method_kwargs={"subnet_id": "SUBNET"}, expected_args=[self.proxy], - expected_kwargs={"subnet_id": "SUBNET"}) + expected_kwargs={"subnet_id": "SUBNET"}, + ) mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") @mock.patch.object(proxy_base.Proxy, '_get_resource') @mock.patch.object(router.Router, 'add_extra_routes') - def test_add_extra_routes_to_router( - self, mock_add_extra_routes, mock_get): + def test_add_extra_routes_to_router(self, mock_add_extra_routes, mock_get): x_router = router.Router.new(id="ROUTER_ID") mock_get.return_value = x_router @@ -1224,13 +1457,15 @@ def test_add_extra_routes_to_router( method_args=["FAKE_ROUTER"], method_kwargs={"body": {"router": {"routes": []}}}, expected_args=[self.proxy], - expected_kwargs={"body": {"router": {"routes": []}}}) + expected_kwargs={"body": {"router": {"routes": []}}}, + ) mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") @mock.patch.object(proxy_base.Proxy, '_get_resource') @mock.patch.object(router.Router, 'remove_extra_routes') def test_remove_extra_routes_from_router( - self, mock_remove_extra_routes, mock_get): + self, mock_remove_extra_routes, mock_get + ): x_router = router.Router.new(id="ROUTER_ID") mock_get.return_value = x_router @@ -1240,7 +1475,8 @@ def test_remove_extra_routes_from_router( method_args=["FAKE_ROUTER"], method_kwargs={"body": {"router": {"routes": []}}}, expected_args=[self.proxy], - expected_kwargs={"body": {"router": {"routes": []}}}) + expected_kwargs={"body": {"router": {"routes": []}}}, + ) mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") @mock.patch.object(proxy_base.Proxy, '_get_resource') @@ -1255,7 +1491,8 @@ def test_add_gateway_to_router(self, mock_add, mock_get): method_args=["FAKE_ROUTER"], method_kwargs={"foo": "bar"}, expected_args=[self.proxy], - expected_kwargs={"foo": "bar"}) + expected_kwargs={"foo": "bar"}, + ) mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") @mock.patch.object(proxy_base.Proxy, '_get_resource') @@ -1270,7 +1507,8 @@ def test_remove_gateway_from_router(self, mock_remove, mock_get): method_args=["FAKE_ROUTER"], method_kwargs={"foo": "bar"}, expected_args=[self.proxy], - expected_kwargs={"foo": "bar"}) + expected_kwargs={"foo": "bar"}, + ) mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") def test_router_hosting_l3_agents_list(self): @@ -1292,202 +1530,276 @@ def test_agent_hosted_routers_list(self): class TestNetworkFirewallGroup(TestNetworkProxy): def test_firewall_group_create_attrs(self): - self.verify_create(self.proxy.create_firewall_group, - firewall_group.FirewallGroup) + self.verify_create( + self.proxy.create_firewall_group, firewall_group.FirewallGroup + ) def test_firewall_group_delete(self): - self.verify_delete(self.proxy.delete_firewall_group, - firewall_group.FirewallGroup, False) + self.verify_delete( + self.proxy.delete_firewall_group, + firewall_group.FirewallGroup, + False, + ) def test_firewall_group_delete_ignore(self): - self.verify_delete(self.proxy.delete_firewall_group, - firewall_group.FirewallGroup, True) + self.verify_delete( + self.proxy.delete_firewall_group, + firewall_group.FirewallGroup, + True, + ) def test_firewall_group_find(self): - self.verify_find(self.proxy.find_firewall_group, - firewall_group.FirewallGroup) + self.verify_find( + self.proxy.find_firewall_group, firewall_group.FirewallGroup + ) def test_firewall_group_get(self): - self.verify_get(self.proxy.get_firewall_group, - firewall_group.FirewallGroup) + self.verify_get( + self.proxy.get_firewall_group, firewall_group.FirewallGroup + ) def test_firewall_groups(self): - self.verify_list(self.proxy.firewall_groups, - firewall_group.FirewallGroup) + self.verify_list( + self.proxy.firewall_groups, firewall_group.FirewallGroup + ) def test_firewall_group_update(self): - self.verify_update(self.proxy.update_firewall_group, - firewall_group.FirewallGroup) + self.verify_update( + self.proxy.update_firewall_group, firewall_group.FirewallGroup + ) class TestNetworkPolicy(TestNetworkProxy): def test_firewall_policy_create_attrs(self): - self.verify_create(self.proxy.create_firewall_policy, - firewall_policy.FirewallPolicy) + self.verify_create( + self.proxy.create_firewall_policy, firewall_policy.FirewallPolicy + ) def test_firewall_policy_delete(self): - self.verify_delete(self.proxy.delete_firewall_policy, - firewall_policy.FirewallPolicy, False) + self.verify_delete( + self.proxy.delete_firewall_policy, + firewall_policy.FirewallPolicy, + False, + ) def test_firewall_policy_delete_ignore(self): - self.verify_delete(self.proxy.delete_firewall_policy, - firewall_policy.FirewallPolicy, True) + self.verify_delete( + self.proxy.delete_firewall_policy, + firewall_policy.FirewallPolicy, + True, + ) def test_firewall_policy_find(self): - self.verify_find(self.proxy.find_firewall_policy, - firewall_policy.FirewallPolicy) + self.verify_find( + self.proxy.find_firewall_policy, firewall_policy.FirewallPolicy + ) def test_firewall_policy_get(self): - self.verify_get(self.proxy.get_firewall_policy, - firewall_policy.FirewallPolicy) + self.verify_get( + self.proxy.get_firewall_policy, firewall_policy.FirewallPolicy + ) def test_firewall_policies(self): - self.verify_list(self.proxy.firewall_policies, - firewall_policy.FirewallPolicy) + self.verify_list( + self.proxy.firewall_policies, firewall_policy.FirewallPolicy + ) def test_firewall_policy_update(self): - self.verify_update(self.proxy.update_firewall_policy, - firewall_policy.FirewallPolicy) + self.verify_update( + self.proxy.update_firewall_policy, firewall_policy.FirewallPolicy + ) class TestNetworkRule(TestNetworkProxy): def test_firewall_rule_create_attrs(self): - self.verify_create(self.proxy.create_firewall_rule, - firewall_rule.FirewallRule) + self.verify_create( + self.proxy.create_firewall_rule, firewall_rule.FirewallRule + ) def test_firewall_rule_delete(self): - self.verify_delete(self.proxy.delete_firewall_rule, - firewall_rule.FirewallRule, False) + self.verify_delete( + self.proxy.delete_firewall_rule, firewall_rule.FirewallRule, False + ) def test_firewall_rule_delete_ignore(self): - self.verify_delete(self.proxy.delete_firewall_rule, - firewall_rule.FirewallRule, True) + self.verify_delete( + self.proxy.delete_firewall_rule, firewall_rule.FirewallRule, True + ) def test_firewall_rule_find(self): - self.verify_find(self.proxy.find_firewall_rule, - firewall_rule.FirewallRule) + self.verify_find( + self.proxy.find_firewall_rule, firewall_rule.FirewallRule + ) def test_firewall_rule_get(self): - self.verify_get(self.proxy.get_firewall_rule, - firewall_rule.FirewallRule) + self.verify_get( + self.proxy.get_firewall_rule, firewall_rule.FirewallRule + ) def test_firewall_rules(self): - self.verify_list(self.proxy.firewall_rules, - firewall_rule.FirewallRule) + self.verify_list(self.proxy.firewall_rules, firewall_rule.FirewallRule) def test_firewall_rule_update(self): - self.verify_update(self.proxy.update_firewall_rule, - firewall_rule.FirewallRule) + self.verify_update( + self.proxy.update_firewall_rule, firewall_rule.FirewallRule + ) class TestNetworkNetworkSegment(TestNetworkProxy): def test_network_segment_range_create_attrs(self): - self.verify_create(self.proxy.create_network_segment_range, - network_segment_range.NetworkSegmentRange) + self.verify_create( + self.proxy.create_network_segment_range, + network_segment_range.NetworkSegmentRange, + ) def test_network_segment_range_delete(self): - self.verify_delete(self.proxy.delete_network_segment_range, - network_segment_range.NetworkSegmentRange, False) + self.verify_delete( + self.proxy.delete_network_segment_range, + network_segment_range.NetworkSegmentRange, + False, + ) def test_network_segment_range_delete_ignore(self): - self.verify_delete(self.proxy.delete_network_segment_range, - network_segment_range.NetworkSegmentRange, True) + self.verify_delete( + self.proxy.delete_network_segment_range, + network_segment_range.NetworkSegmentRange, + True, + ) def test_network_segment_range_find(self): - self.verify_find(self.proxy.find_network_segment_range, - network_segment_range.NetworkSegmentRange) + self.verify_find( + self.proxy.find_network_segment_range, + network_segment_range.NetworkSegmentRange, + ) def test_network_segment_range_get(self): - self.verify_get(self.proxy.get_network_segment_range, - network_segment_range.NetworkSegmentRange) + self.verify_get( + self.proxy.get_network_segment_range, + network_segment_range.NetworkSegmentRange, + ) def test_network_segment_ranges(self): - self.verify_list(self.proxy.network_segment_ranges, - network_segment_range.NetworkSegmentRange) + self.verify_list( + self.proxy.network_segment_ranges, + network_segment_range.NetworkSegmentRange, + ) def test_network_segment_range_update(self): - self.verify_update(self.proxy.update_network_segment_range, - network_segment_range.NetworkSegmentRange) + self.verify_update( + self.proxy.update_network_segment_range, + network_segment_range.NetworkSegmentRange, + ) class TestNetworkSecurityGroup(TestNetworkProxy): def test_security_group_create_attrs(self): - self.verify_create(self.proxy.create_security_group, - security_group.SecurityGroup) + self.verify_create( + self.proxy.create_security_group, security_group.SecurityGroup + ) def test_security_group_delete(self): - self.verify_delete(self.proxy.delete_security_group, - security_group.SecurityGroup, False, - expected_kwargs={'if_revision': None}) + self.verify_delete( + self.proxy.delete_security_group, + security_group.SecurityGroup, + False, + expected_kwargs={'if_revision': None}, + ) def test_security_group_delete_ignore(self): - self.verify_delete(self.proxy.delete_security_group, - security_group.SecurityGroup, True, - expected_kwargs={'if_revision': None}) + self.verify_delete( + self.proxy.delete_security_group, + security_group.SecurityGroup, + True, + expected_kwargs={'if_revision': None}, + ) def test_security_group_delete_if_revision(self): - self.verify_delete(self.proxy.delete_security_group, - security_group.SecurityGroup, True, - method_kwargs={'if_revision': 42}, - expected_kwargs={'if_revision': 42}) + self.verify_delete( + self.proxy.delete_security_group, + security_group.SecurityGroup, + True, + method_kwargs={'if_revision': 42}, + expected_kwargs={'if_revision': 42}, + ) def test_security_group_find(self): - self.verify_find(self.proxy.find_security_group, - security_group.SecurityGroup) + self.verify_find( + self.proxy.find_security_group, security_group.SecurityGroup + ) def test_security_group_get(self): - self.verify_get(self.proxy.get_security_group, - security_group.SecurityGroup) + self.verify_get( + self.proxy.get_security_group, security_group.SecurityGroup + ) def test_security_groups(self): - self.verify_list(self.proxy.security_groups, - security_group.SecurityGroup) + self.verify_list( + self.proxy.security_groups, security_group.SecurityGroup + ) def test_security_group_update(self): - self.verify_update(self.proxy.update_security_group, - security_group.SecurityGroup, - expected_kwargs={'x': 1, 'y': 2, 'z': 3, - 'if_revision': None}) + self.verify_update( + self.proxy.update_security_group, + security_group.SecurityGroup, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, 'if_revision': None}, + ) def test_security_group_update_if_revision(self): - self.verify_update(self.proxy.update_security_group, - security_group.SecurityGroup, - method_kwargs={'x': 1, 'y': 2, 'z': 3, - 'if_revision': 42}, - expected_kwargs={'x': 1, 'y': 2, 'z': 3, - 'if_revision': 42}) + self.verify_update( + self.proxy.update_security_group, + security_group.SecurityGroup, + method_kwargs={'x': 1, 'y': 2, 'z': 3, 'if_revision': 42}, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, 'if_revision': 42}, + ) def test_security_group_rule_create_attrs(self): - self.verify_create(self.proxy.create_security_group_rule, - security_group_rule.SecurityGroupRule) + self.verify_create( + self.proxy.create_security_group_rule, + security_group_rule.SecurityGroupRule, + ) def test_security_group_rule_delete(self): - self.verify_delete(self.proxy.delete_security_group_rule, - security_group_rule.SecurityGroupRule, False, - expected_kwargs={'if_revision': None}) + self.verify_delete( + self.proxy.delete_security_group_rule, + security_group_rule.SecurityGroupRule, + False, + expected_kwargs={'if_revision': None}, + ) def test_security_group_rule_delete_ignore(self): - self.verify_delete(self.proxy.delete_security_group_rule, - security_group_rule.SecurityGroupRule, True, - expected_kwargs={'if_revision': None}) + self.verify_delete( + self.proxy.delete_security_group_rule, + security_group_rule.SecurityGroupRule, + True, + expected_kwargs={'if_revision': None}, + ) def test_security_group_rule_delete_if_revision(self): - self.verify_delete(self.proxy.delete_security_group_rule, - security_group_rule.SecurityGroupRule, True, - method_kwargs={'if_revision': 42}, - expected_kwargs={'if_revision': 42}) + self.verify_delete( + self.proxy.delete_security_group_rule, + security_group_rule.SecurityGroupRule, + True, + method_kwargs={'if_revision': 42}, + expected_kwargs={'if_revision': 42}, + ) def test_security_group_rule_find(self): - self.verify_find(self.proxy.find_security_group_rule, - security_group_rule.SecurityGroupRule) + self.verify_find( + self.proxy.find_security_group_rule, + security_group_rule.SecurityGroupRule, + ) def test_security_group_rule_get(self): - self.verify_get(self.proxy.get_security_group_rule, - security_group_rule.SecurityGroupRule) + self.verify_get( + self.proxy.get_security_group_rule, + security_group_rule.SecurityGroupRule, + ) def test_security_group_rules(self): - self.verify_list(self.proxy.security_group_rules, - security_group_rule.SecurityGroupRule) + self.verify_list( + self.proxy.security_group_rules, + security_group_rule.SecurityGroupRule, + ) @mock.patch('openstack.network.v2._proxy.Proxy._bulk_create') def test_security_group_rules_create(self, bc): @@ -1526,17 +1838,29 @@ def test_subnet_create_attrs(self): self.verify_create(self.proxy.create_subnet, subnet.Subnet) def test_subnet_delete(self): - self.verify_delete(self.proxy.delete_subnet, subnet.Subnet, False, - expected_kwargs={'if_revision': None}) + self.verify_delete( + self.proxy.delete_subnet, + subnet.Subnet, + False, + expected_kwargs={'if_revision': None}, + ) def test_subnet_delete_ignore(self): - self.verify_delete(self.proxy.delete_subnet, subnet.Subnet, True, - expected_kwargs={'if_revision': None}) + self.verify_delete( + self.proxy.delete_subnet, + subnet.Subnet, + True, + expected_kwargs={'if_revision': None}, + ) def test_subnet_delete_if_revision(self): - self.verify_delete(self.proxy.delete_subnet, subnet.Subnet, True, - method_kwargs={'if_revision': 42}, - expected_kwargs={'if_revision': 42}) + self.verify_delete( + self.proxy.delete_subnet, + subnet.Subnet, + True, + method_kwargs={'if_revision': 42}, + expected_kwargs={'if_revision': 42}, + ) def test_subnet_find(self): self.verify_find(self.proxy.find_subnet, subnet.Subnet) @@ -1548,203 +1872,231 @@ def test_subnets(self): self.verify_list(self.proxy.subnets, subnet.Subnet) def test_subnet_update(self): - self.verify_update(self.proxy.update_subnet, subnet.Subnet, - expected_kwargs={'x': 1, 'y': 2, 'z': 3, - 'if_revision': None}) + self.verify_update( + self.proxy.update_subnet, + subnet.Subnet, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, 'if_revision': None}, + ) def test_subnet_pool_create_attrs(self): - self.verify_create(self.proxy.create_subnet_pool, - subnet_pool.SubnetPool) + self.verify_create( + self.proxy.create_subnet_pool, subnet_pool.SubnetPool + ) def test_subnet_pool_delete(self): - self.verify_delete(self.proxy.delete_subnet_pool, - subnet_pool.SubnetPool, False) + self.verify_delete( + self.proxy.delete_subnet_pool, subnet_pool.SubnetPool, False + ) def test_subnet_pool_delete_ignore(self): - self.verify_delete(self.proxy.delete_subnet_pool, - subnet_pool.SubnetPool, True) + self.verify_delete( + self.proxy.delete_subnet_pool, subnet_pool.SubnetPool, True + ) def test_subnet_pool_find(self): - self.verify_find(self.proxy.find_subnet_pool, - subnet_pool.SubnetPool) + self.verify_find(self.proxy.find_subnet_pool, subnet_pool.SubnetPool) def test_subnet_pool_get(self): - self.verify_get(self.proxy.get_subnet_pool, - subnet_pool.SubnetPool) + self.verify_get(self.proxy.get_subnet_pool, subnet_pool.SubnetPool) def test_subnet_pools(self): - self.verify_list(self.proxy.subnet_pools, - subnet_pool.SubnetPool) + self.verify_list(self.proxy.subnet_pools, subnet_pool.SubnetPool) def test_subnet_pool_update(self): - self.verify_update(self.proxy.update_subnet_pool, - subnet_pool.SubnetPool) + self.verify_update( + self.proxy.update_subnet_pool, subnet_pool.SubnetPool + ) class TestNetworkVpnEndpointGroup(TestNetworkProxy): def test_vpn_endpoint_group_create_attrs(self): self.verify_create( self.proxy.create_vpn_endpoint_group, - vpn_endpoint_group.VpnEndpointGroup) + vpn_endpoint_group.VpnEndpointGroup, + ) def test_vpn_endpoint_group_delete(self): self.verify_delete( self.proxy.delete_vpn_endpoint_group, - vpn_endpoint_group.VpnEndpointGroup, False) + vpn_endpoint_group.VpnEndpointGroup, + False, + ) def test_vpn_endpoint_group_delete_ignore(self): self.verify_delete( self.proxy.delete_vpn_endpoint_group, - vpn_endpoint_group.VpnEndpointGroup, True) + vpn_endpoint_group.VpnEndpointGroup, + True, + ) def test_vpn_endpoint_group_find(self): self.verify_find( self.proxy.find_vpn_endpoint_group, - vpn_endpoint_group.VpnEndpointGroup) + vpn_endpoint_group.VpnEndpointGroup, + ) def test_vpn_endpoint_group_get(self): self.verify_get( self.proxy.get_vpn_endpoint_group, - vpn_endpoint_group.VpnEndpointGroup) + vpn_endpoint_group.VpnEndpointGroup, + ) def test_vpn_endpoint_groups(self): self.verify_list( - self.proxy.vpn_endpoint_groups, - vpn_endpoint_group.VpnEndpointGroup) + self.proxy.vpn_endpoint_groups, vpn_endpoint_group.VpnEndpointGroup + ) def test_vpn_endpoint_group_update(self): self.verify_update( self.proxy.update_vpn_endpoint_group, - vpn_endpoint_group.VpnEndpointGroup) + vpn_endpoint_group.VpnEndpointGroup, + ) class TestNetworkVpnSiteConnection(TestNetworkProxy): def test_ipsec_site_connection_create_attrs(self): self.verify_create( self.proxy.create_vpn_ipsec_site_connection, - vpn_ipsec_site_connection.VpnIPSecSiteConnection) + vpn_ipsec_site_connection.VpnIPSecSiteConnection, + ) def test_ipsec_site_connection_delete(self): self.verify_delete( self.proxy.delete_vpn_ipsec_site_connection, - vpn_ipsec_site_connection.VpnIPSecSiteConnection, False) + vpn_ipsec_site_connection.VpnIPSecSiteConnection, + False, + ) def test_ipsec_site_connection_delete_ignore(self): self.verify_delete( self.proxy.delete_vpn_ipsec_site_connection, - vpn_ipsec_site_connection.VpnIPSecSiteConnection, True) + vpn_ipsec_site_connection.VpnIPSecSiteConnection, + True, + ) def test_ipsec_site_connection_find(self): self.verify_find( self.proxy.find_vpn_ipsec_site_connection, - vpn_ipsec_site_connection.VpnIPSecSiteConnection) + vpn_ipsec_site_connection.VpnIPSecSiteConnection, + ) def test_ipsec_site_connection_get(self): self.verify_get( self.proxy.get_vpn_ipsec_site_connection, - vpn_ipsec_site_connection.VpnIPSecSiteConnection) + vpn_ipsec_site_connection.VpnIPSecSiteConnection, + ) def test_ipsec_site_connections(self): self.verify_list( self.proxy.vpn_ipsec_site_connections, - vpn_ipsec_site_connection.VpnIPSecSiteConnection) + vpn_ipsec_site_connection.VpnIPSecSiteConnection, + ) def test_ipsec_site_connection_update(self): self.verify_update( self.proxy.update_vpn_ipsec_site_connection, - vpn_ipsec_site_connection.VpnIPSecSiteConnection) + vpn_ipsec_site_connection.VpnIPSecSiteConnection, + ) class TestNetworkVpnIkePolicy(TestNetworkProxy): def test_ike_policy_create_attrs(self): self.verify_create( - self.proxy.create_vpn_ike_policy, - vpn_ike_policy.VpnIkePolicy) + self.proxy.create_vpn_ike_policy, vpn_ike_policy.VpnIkePolicy + ) def test_ike_policy_delete(self): self.verify_delete( self.proxy.delete_vpn_ike_policy, - vpn_ike_policy.VpnIkePolicy, False) + vpn_ike_policy.VpnIkePolicy, + False, + ) def test_ike_policy_delete_ignore(self): self.verify_delete( - self.proxy.delete_vpn_ike_policy, - vpn_ike_policy.VpnIkePolicy, True) + self.proxy.delete_vpn_ike_policy, vpn_ike_policy.VpnIkePolicy, True + ) def test_ike_policy_find(self): self.verify_find( - self.proxy.find_vpn_ike_policy, - vpn_ike_policy.VpnIkePolicy) + self.proxy.find_vpn_ike_policy, vpn_ike_policy.VpnIkePolicy + ) def test_ike_policy_get(self): self.verify_get( - self.proxy.get_vpn_ike_policy, - vpn_ike_policy.VpnIkePolicy) + self.proxy.get_vpn_ike_policy, vpn_ike_policy.VpnIkePolicy + ) def test_ike_policies(self): self.verify_list( - self.proxy.vpn_ike_policies, - vpn_ike_policy.VpnIkePolicy) + self.proxy.vpn_ike_policies, vpn_ike_policy.VpnIkePolicy + ) def test_ike_policy_update(self): self.verify_update( - self.proxy.update_vpn_ike_policy, - vpn_ike_policy.VpnIkePolicy) + self.proxy.update_vpn_ike_policy, vpn_ike_policy.VpnIkePolicy + ) class TestNetworkVpnIpsecPolicy(TestNetworkProxy): def test_ipsec_policy_create_attrs(self): self.verify_create( - self.proxy.create_vpn_ipsec_policy, - vpn_ipsec_policy.VpnIpsecPolicy) + self.proxy.create_vpn_ipsec_policy, vpn_ipsec_policy.VpnIpsecPolicy + ) def test_ipsec_policy_delete(self): self.verify_delete( self.proxy.delete_vpn_ipsec_policy, - vpn_ipsec_policy.VpnIpsecPolicy, False) + vpn_ipsec_policy.VpnIpsecPolicy, + False, + ) def test_ipsec_policy_delete_ignore(self): self.verify_delete( self.proxy.delete_vpn_ipsec_policy, - vpn_ipsec_policy.VpnIpsecPolicy, True) + vpn_ipsec_policy.VpnIpsecPolicy, + True, + ) def test_ipsec_policy_find(self): self.verify_find( - self.proxy.find_vpn_ipsec_policy, - vpn_ipsec_policy.VpnIpsecPolicy) + self.proxy.find_vpn_ipsec_policy, vpn_ipsec_policy.VpnIpsecPolicy + ) def test_ipsec_policy_get(self): self.verify_get( - self.proxy.get_vpn_ipsec_policy, - vpn_ipsec_policy.VpnIpsecPolicy) + self.proxy.get_vpn_ipsec_policy, vpn_ipsec_policy.VpnIpsecPolicy + ) def test_ipsec_policies(self): self.verify_list( - self.proxy.vpn_ipsec_policies, - vpn_ipsec_policy.VpnIpsecPolicy) + self.proxy.vpn_ipsec_policies, vpn_ipsec_policy.VpnIpsecPolicy + ) def test_ipsec_policy_update(self): self.verify_update( - self.proxy.update_vpn_ipsec_policy, - vpn_ipsec_policy.VpnIpsecPolicy) + self.proxy.update_vpn_ipsec_policy, vpn_ipsec_policy.VpnIpsecPolicy + ) class TestNetworkVpnService(TestNetworkProxy): def test_vpn_service_create_attrs(self): - self.verify_create(self.proxy.create_vpn_service, - vpn_service.VpnService) + self.verify_create( + self.proxy.create_vpn_service, vpn_service.VpnService + ) def test_vpn_service_delete(self): - self.verify_delete(self.proxy.delete_vpn_service, - vpn_service.VpnService, False) + self.verify_delete( + self.proxy.delete_vpn_service, vpn_service.VpnService, False + ) def test_vpn_service_delete_ignore(self): - self.verify_delete(self.proxy.delete_vpn_service, - vpn_service.VpnService, True) + self.verify_delete( + self.proxy.delete_vpn_service, vpn_service.VpnService, True + ) def test_vpn_service_find(self): - self.verify_find(self.proxy.find_vpn_service, - vpn_service.VpnService) + self.verify_find(self.proxy.find_vpn_service, vpn_service.VpnService) def test_vpn_service_get(self): self.verify_get(self.proxy.get_vpn_service, vpn_service.VpnService) @@ -1753,38 +2105,50 @@ def test_vpn_services(self): self.verify_list(self.proxy.vpn_services, vpn_service.VpnService) def test_vpn_service_update(self): - self.verify_update(self.proxy.update_vpn_service, - vpn_service.VpnService) + self.verify_update( + self.proxy.update_vpn_service, vpn_service.VpnService + ) class TestNetworkServiceProvider(TestNetworkProxy): def test_service_provider(self): - self.verify_list(self.proxy.service_providers, - service_provider.ServiceProvider) + self.verify_list( + self.proxy.service_providers, service_provider.ServiceProvider + ) class TestNetworkAutoAllocatedTopology(TestNetworkProxy): def test_auto_allocated_topology_get(self): - self.verify_get(self.proxy.get_auto_allocated_topology, - auto_allocated_topology.AutoAllocatedTopology) + self.verify_get( + self.proxy.get_auto_allocated_topology, + auto_allocated_topology.AutoAllocatedTopology, + ) def test_auto_allocated_topology_delete(self): - self.verify_delete(self.proxy.delete_auto_allocated_topology, - auto_allocated_topology.AutoAllocatedTopology, - False) + self.verify_delete( + self.proxy.delete_auto_allocated_topology, + auto_allocated_topology.AutoAllocatedTopology, + False, + ) def test_auto_allocated_topology_delete_ignore(self): - self.verify_delete(self.proxy.delete_auto_allocated_topology, - auto_allocated_topology.AutoAllocatedTopology, - True) + self.verify_delete( + self.proxy.delete_auto_allocated_topology, + auto_allocated_topology.AutoAllocatedTopology, + True, + ) def test_validate_topology(self): - self.verify_get(self.proxy.validate_auto_allocated_topology, - auto_allocated_topology.ValidateTopology, - method_args=[mock.sentinel.project_id], - expected_args=[], - expected_kwargs={"project": mock.sentinel.project_id, - "requires_id": False}) + self.verify_get( + self.proxy.validate_auto_allocated_topology, + auto_allocated_topology.ValidateTopology, + method_args=[mock.sentinel.project_id], + expected_args=[], + expected_kwargs={ + "project": mock.sentinel.project_id, + "requires_id": False, + }, + ) class TestNetworkTags(TestNetworkProxy): @@ -1795,23 +2159,29 @@ def test_set_tags(self): self.proxy.set_tags, method_args=[x_network, ['TAG1', 'TAG2']], expected_args=[self.proxy, ['TAG1', 'TAG2']], - expected_result=mock.sentinel.result_set_tags) + expected_result=mock.sentinel.result_set_tags, + ) @mock.patch('openstack.network.v2.network.Network.set_tags') def test_set_tags_resource_without_tag_suport(self, mock_set_tags): no_tag_resource = object() - self.assertRaises(exceptions.InvalidRequest, - self.proxy.set_tags, - no_tag_resource, ['TAG1', 'TAG2']) + self.assertRaises( + exceptions.InvalidRequest, + self.proxy.set_tags, + no_tag_resource, + ['TAG1', 'TAG2'], + ) self.assertEqual(0, mock_set_tags.call_count) class TestNetworkFloatingIp(TestNetworkProxy): def test_create_floating_ip_port_forwarding(self): - self.verify_create(self.proxy.create_floating_ip_port_forwarding, - port_forwarding.PortForwarding, - method_kwargs={'floating_ip': FIP_ID}, - expected_kwargs={'floatingip_id': FIP_ID}) + self.verify_create( + self.proxy.create_floating_ip_port_forwarding, + port_forwarding.PortForwarding, + method_kwargs={'floating_ip': FIP_ID}, + expected_kwargs={'floatingip_id': FIP_ID}, + ) def test_delete_floating_ip_port_forwarding(self): self.verify_delete( @@ -1820,7 +2190,8 @@ def test_delete_floating_ip_port_forwarding(self): ignore_missing=False, method_args=[FIP_ID, "resource_or_id"], expected_args=["resource_or_id"], - expected_kwargs={'floatingip_id': FIP_ID}) + expected_kwargs={'floatingip_id': FIP_ID}, + ) def test_delete_floating_ip_port_forwarding_ignore(self): self.verify_delete( @@ -1829,7 +2200,8 @@ def test_delete_floating_ip_port_forwarding_ignore(self): ignore_missing=True, method_args=[FIP_ID, "resource_or_id"], expected_args=["resource_or_id"], - expected_kwargs={'floatingip_id': FIP_ID}) + expected_kwargs={'floatingip_id': FIP_ID}, + ) def test_find_floating_ip_port_forwarding(self): fip = floating_ip.FloatingIP.new(id=FIP_ID) @@ -1839,9 +2211,10 @@ def test_find_floating_ip_port_forwarding(self): method_args=[fip, 'port_forwarding_id'], expected_args=[ port_forwarding.PortForwarding, - 'port_forwarding_id'], - expected_kwargs={ - 'ignore_missing': True, 'floatingip_id': FIP_ID}) + 'port_forwarding_id', + ], + expected_kwargs={'ignore_missing': True, 'floatingip_id': FIP_ID}, + ) def test_get_floating_ip_port_forwarding(self): fip = floating_ip.FloatingIP.new(id=FIP_ID) @@ -1851,14 +2224,18 @@ def test_get_floating_ip_port_forwarding(self): method_args=[fip, 'port_forwarding_id'], expected_args=[ port_forwarding.PortForwarding, - 'port_forwarding_id'], - expected_kwargs={'floatingip_id': FIP_ID}) + 'port_forwarding_id', + ], + expected_kwargs={'floatingip_id': FIP_ID}, + ) def test_floating_ip_port_forwardings(self): - self.verify_list(self.proxy.floating_ip_port_forwardings, - port_forwarding.PortForwarding, - method_kwargs={'floating_ip': FIP_ID}, - expected_kwargs={'floatingip_id': FIP_ID}) + self.verify_list( + self.proxy.floating_ip_port_forwardings, + port_forwarding.PortForwarding, + method_kwargs={'floating_ip': FIP_ID}, + expected_kwargs={'floatingip_id': FIP_ID}, + ) def test_update_floating_ip_port_forwarding(self): fip = floating_ip.FloatingIP.new(id=FIP_ID) @@ -1869,14 +2246,18 @@ def test_update_floating_ip_port_forwarding(self): method_kwargs={'foo': 'bar'}, expected_args=[ port_forwarding.PortForwarding, - 'port_forwarding_id'], - expected_kwargs={'floatingip_id': FIP_ID, 'foo': 'bar'}) + 'port_forwarding_id', + ], + expected_kwargs={'floatingip_id': FIP_ID, 'foo': 'bar'}, + ) def test_create_l3_conntrack_helper(self): - self.verify_create(self.proxy.create_conntrack_helper, - l3_conntrack_helper.ConntrackHelper, - method_kwargs={'router': ROUTER_ID}, - expected_kwargs={'router_id': ROUTER_ID}) + self.verify_create( + self.proxy.create_conntrack_helper, + l3_conntrack_helper.ConntrackHelper, + method_kwargs={'router': ROUTER_ID}, + expected_kwargs={'router_id': ROUTER_ID}, + ) def test_delete_l3_conntrack_helper(self): r = router.Router.new(id=ROUTER_ID) @@ -1886,7 +2267,8 @@ def test_delete_l3_conntrack_helper(self): ignore_missing=False, method_args=['resource_or_id', r], expected_args=['resource_or_id'], - expected_kwargs={'router_id': ROUTER_ID},) + expected_kwargs={'router_id': ROUTER_ID}, + ) def test_delete_l3_conntrack_helper_ignore(self): r = router.Router.new(id=ROUTER_ID) @@ -1896,7 +2278,8 @@ def test_delete_l3_conntrack_helper_ignore(self): ignore_missing=True, method_args=['resource_or_id', r], expected_args=['resource_or_id'], - expected_kwargs={'router_id': ROUTER_ID},) + expected_kwargs={'router_id': ROUTER_ID}, + ) def test_get_l3_conntrack_helper(self): r = router.Router.new(id=ROUTER_ID) @@ -1906,15 +2289,19 @@ def test_get_l3_conntrack_helper(self): method_args=['conntrack_helper_id', r], expected_args=[ l3_conntrack_helper.ConntrackHelper, - 'conntrack_helper_id'], - expected_kwargs={'router_id': ROUTER_ID}) + 'conntrack_helper_id', + ], + expected_kwargs={'router_id': ROUTER_ID}, + ) def test_l3_conntrack_helpers(self): - self.verify_list(self.proxy.conntrack_helpers, - l3_conntrack_helper.ConntrackHelper, - method_args=[ROUTER_ID], - expected_args=[], - expected_kwargs={'router_id': ROUTER_ID}) + self.verify_list( + self.proxy.conntrack_helpers, + l3_conntrack_helper.ConntrackHelper, + method_args=[ROUTER_ID], + expected_args=[], + expected_kwargs={'router_id': ROUTER_ID}, + ) def test_update_l3_conntrack_helper(self): r = router.Router.new(id=ROUTER_ID) @@ -1925,8 +2312,10 @@ def test_update_l3_conntrack_helper(self): method_kwargs={'foo': 'bar'}, expected_args=[ l3_conntrack_helper.ConntrackHelper, - 'conntrack_helper_id'], - expected_kwargs={'router_id': ROUTER_ID, 'foo': 'bar'}) + 'conntrack_helper_id', + ], + expected_kwargs={'router_id': ROUTER_ID, 'foo': 'bar'}, + ) class TestNetworkNDPProxy(TestNetworkProxy): @@ -1934,12 +2323,14 @@ def test_ndp_proxy_create_attrs(self): self.verify_create(self.proxy.create_ndp_proxy, ndp_proxy.NDPProxy) def test_ndp_proxy_delete(self): - self.verify_delete(self.proxy.delete_ndp_proxy, ndp_proxy.NDPProxy, - False) + self.verify_delete( + self.proxy.delete_ndp_proxy, ndp_proxy.NDPProxy, False + ) def test_ndp_proxy_delete_ignore(self): - self.verify_delete(self.proxy.delete_ndp_proxy, ndp_proxy.NDPProxy, - True) + self.verify_delete( + self.proxy.delete_ndp_proxy, ndp_proxy.NDPProxy, True + ) def test_ndp_proxy_find(self): self.verify_find(self.proxy.find_ndp_proxy, ndp_proxy.NDPProxy) @@ -1955,18 +2346,20 @@ def test_ndp_proxy_update(self): class TestNetworkBGP(TestNetworkProxy): - def test_bgp_speaker_create(self): - self.verify_create(self.proxy.create_bgp_speaker, - bgp_speaker.BgpSpeaker) + self.verify_create( + self.proxy.create_bgp_speaker, bgp_speaker.BgpSpeaker + ) def test_bgp_speaker_delete(self): - self.verify_delete(self.proxy.delete_bgp_speaker, - bgp_speaker.BgpSpeaker, False) + self.verify_delete( + self.proxy.delete_bgp_speaker, bgp_speaker.BgpSpeaker, False + ) def test_bgp_speaker_delete_ignore(self): - self.verify_delete(self.proxy.delete_bgp_speaker, - bgp_speaker.BgpSpeaker, True) + self.verify_delete( + self.proxy.delete_bgp_speaker, bgp_speaker.BgpSpeaker, True + ) def test_bgp_speaker_find(self): self.verify_find(self.proxy.find_bgp_speaker, bgp_speaker.BgpSpeaker) @@ -1978,15 +2371,15 @@ def test_bgp_speakers(self): self.verify_list(self.proxy.bgp_speakers, bgp_speaker.BgpSpeaker) def test_bgp_speaker_update(self): - self.verify_update(self.proxy.update_bgp_speaker, - bgp_speaker.BgpSpeaker) + self.verify_update( + self.proxy.update_bgp_speaker, bgp_speaker.BgpSpeaker + ) def test_bgp_peer_create(self): self.verify_create(self.proxy.create_bgp_peer, bgp_peer.BgpPeer) def test_bgp_peer_delete(self): - self.verify_delete(self.proxy.delete_bgp_peer, - bgp_peer.BgpPeer, False) + self.verify_delete(self.proxy.delete_bgp_peer, bgp_peer.BgpPeer, False) def test_bgp_peer_delete_ignore(self): self.verify_delete(self.proxy.delete_bgp_peer, bgp_peer.BgpPeer, True) @@ -2013,12 +2406,10 @@ def test_bgpvpn_create(self): self.verify_create(self.proxy.create_bgpvpn, bgpvpn.BgpVpn) def test_bgpvpn_delete(self): - self.verify_delete(self.proxy.delete_bgpvpn, - bgpvpn.BgpVpn, False) + self.verify_delete(self.proxy.delete_bgpvpn, bgpvpn.BgpVpn, False) def test_bgpvpn_delete_ignore(self): - self.verify_delete(self.proxy.delete_bgpvpn, - bgpvpn.BgpVpn, True) + self.verify_delete(self.proxy.delete_bgpvpn, bgpvpn.BgpVpn, True) def test_bgpvpn_find(self): self.verify_find(self.proxy.find_bgpvpn, bgpvpn.BgpVpn) @@ -2037,7 +2428,7 @@ def test_bgpvpn_network_association_create(self): self.proxy.create_bgpvpn_network_association, bgpvpn_network_association.BgpVpnNetworkAssociation, method_kwargs={'bgpvpn': BGPVPN_ID}, - expected_kwargs={'bgpvpn_id': BGPVPN_ID} + expected_kwargs={'bgpvpn_id': BGPVPN_ID}, ) def test_bgpvpn_network_association_delete(self): @@ -2047,8 +2438,7 @@ def test_bgpvpn_network_association_delete(self): False, method_args=[BGPVPN_ID, self.NETWORK_ASSOCIATION], expected_args=[self.NETWORK_ASSOCIATION], - expected_kwargs={'ignore_missing': False, - 'bgpvpn_id': BGPVPN_ID} + expected_kwargs={'ignore_missing': False, 'bgpvpn_id': BGPVPN_ID}, ) def test_bgpvpn_network_association_delete_ignore(self): @@ -2058,8 +2448,7 @@ def test_bgpvpn_network_association_delete_ignore(self): True, method_args=[BGPVPN_ID, self.NETWORK_ASSOCIATION], expected_args=[self.NETWORK_ASSOCIATION], - expected_kwargs={'ignore_missing': True, - 'bgpvpn_id': BGPVPN_ID} + expected_kwargs={'ignore_missing': True, 'bgpvpn_id': BGPVPN_ID}, ) def test_bgpvpn_network_association_get(self): @@ -2068,16 +2457,18 @@ def test_bgpvpn_network_association_get(self): bgpvpn_network_association.BgpVpnNetworkAssociation, method_args=[BGPVPN_ID, self.NETWORK_ASSOCIATION], expected_args=[self.NETWORK_ASSOCIATION], - expected_kwargs={'bgpvpn_id': BGPVPN_ID} + expected_kwargs={'bgpvpn_id': BGPVPN_ID}, ) def test_bgpvpn_network_associations(self): self.verify_list( self.proxy.bgpvpn_network_associations, bgpvpn_network_association.BgpVpnNetworkAssociation, - method_args=[BGPVPN_ID, ], + method_args=[ + BGPVPN_ID, + ], expected_args=[], - expected_kwargs={'bgpvpn_id': BGPVPN_ID} + expected_kwargs={'bgpvpn_id': BGPVPN_ID}, ) def test_bgpvpn_port_association_create(self): @@ -2085,7 +2476,7 @@ def test_bgpvpn_port_association_create(self): self.proxy.create_bgpvpn_port_association, bgpvpn_port_association.BgpVpnPortAssociation, method_kwargs={'bgpvpn': BGPVPN_ID}, - expected_kwargs={'bgpvpn_id': BGPVPN_ID} + expected_kwargs={'bgpvpn_id': BGPVPN_ID}, ) def test_bgpvpn_port_association_delete(self): @@ -2095,8 +2486,7 @@ def test_bgpvpn_port_association_delete(self): False, method_args=[BGPVPN_ID, self.PORT_ASSOCIATION], expected_args=[self.PORT_ASSOCIATION], - expected_kwargs={'ignore_missing': False, - 'bgpvpn_id': BGPVPN_ID} + expected_kwargs={'ignore_missing': False, 'bgpvpn_id': BGPVPN_ID}, ) def test_bgpvpn_port_association_delete_ignore(self): @@ -2106,8 +2496,7 @@ def test_bgpvpn_port_association_delete_ignore(self): True, method_args=[BGPVPN_ID, self.PORT_ASSOCIATION], expected_args=[self.PORT_ASSOCIATION], - expected_kwargs={'ignore_missing': True, - 'bgpvpn_id': BGPVPN_ID} + expected_kwargs={'ignore_missing': True, 'bgpvpn_id': BGPVPN_ID}, ) def test_bgpvpn_port_association_find(self): @@ -2117,8 +2506,7 @@ def test_bgpvpn_port_association_find(self): method_args=[BGPVPN_ID], expected_args=['resource_name'], method_kwargs={'ignore_missing': True}, - expected_kwargs={'ignore_missing': True, - 'bgpvpn_id': BGPVPN_ID}, + expected_kwargs={'ignore_missing': True, 'bgpvpn_id': BGPVPN_ID}, ) def test_bgpvpn_port_association_get(self): @@ -2127,16 +2515,18 @@ def test_bgpvpn_port_association_get(self): bgpvpn_port_association.BgpVpnPortAssociation, method_args=[BGPVPN_ID, self.PORT_ASSOCIATION], expected_args=[self.PORT_ASSOCIATION], - expected_kwargs={'bgpvpn_id': BGPVPN_ID} + expected_kwargs={'bgpvpn_id': BGPVPN_ID}, ) def test_bgpvpn_port_associations(self): self.verify_list( self.proxy.bgpvpn_port_associations, bgpvpn_port_association.BgpVpnPortAssociation, - method_args=[BGPVPN_ID, ], + method_args=[ + BGPVPN_ID, + ], expected_args=[], - expected_kwargs={'bgpvpn_id': BGPVPN_ID} + expected_kwargs={'bgpvpn_id': BGPVPN_ID}, ) def test_bgpvpn_port_association_update(self): @@ -2146,7 +2536,7 @@ def test_bgpvpn_port_association_update(self): method_args=[BGPVPN_ID, self.PORT_ASSOCIATION], method_kwargs={}, expected_args=[self.PORT_ASSOCIATION], - expected_kwargs={'bgpvpn_id': BGPVPN_ID} + expected_kwargs={'bgpvpn_id': BGPVPN_ID}, ) def test_bgpvpn_router_association_create(self): @@ -2154,7 +2544,7 @@ def test_bgpvpn_router_association_create(self): self.proxy.create_bgpvpn_router_association, bgpvpn_router_association.BgpVpnRouterAssociation, method_kwargs={'bgpvpn': BGPVPN_ID}, - expected_kwargs={'bgpvpn_id': BGPVPN_ID} + expected_kwargs={'bgpvpn_id': BGPVPN_ID}, ) def test_bgpvpn_router_association_delete(self): @@ -2164,8 +2554,7 @@ def test_bgpvpn_router_association_delete(self): False, method_args=[BGPVPN_ID, self.ROUTER_ASSOCIATION], expected_args=[self.ROUTER_ASSOCIATION], - expected_kwargs={'ignore_missing': False, - 'bgpvpn_id': BGPVPN_ID} + expected_kwargs={'ignore_missing': False, 'bgpvpn_id': BGPVPN_ID}, ) def test_bgpvpn_router_association_delete_ignore(self): @@ -2175,8 +2564,7 @@ def test_bgpvpn_router_association_delete_ignore(self): True, method_args=[BGPVPN_ID, self.ROUTER_ASSOCIATION], expected_args=[self.ROUTER_ASSOCIATION], - expected_kwargs={'ignore_missing': True, - 'bgpvpn_id': BGPVPN_ID} + expected_kwargs={'ignore_missing': True, 'bgpvpn_id': BGPVPN_ID}, ) def test_bgpvpn_router_association_get(self): @@ -2185,16 +2573,18 @@ def test_bgpvpn_router_association_get(self): bgpvpn_router_association.BgpVpnRouterAssociation, method_args=[BGPVPN_ID, self.ROUTER_ASSOCIATION], expected_args=[self.ROUTER_ASSOCIATION], - expected_kwargs={'bgpvpn_id': BGPVPN_ID} + expected_kwargs={'bgpvpn_id': BGPVPN_ID}, ) def test_bgpvpn_router_associations(self): self.verify_list( self.proxy.bgpvpn_router_associations, bgpvpn_router_association.BgpVpnRouterAssociation, - method_args=[BGPVPN_ID, ], + method_args=[ + BGPVPN_ID, + ], expected_args=[], - expected_kwargs={'bgpvpn_id': BGPVPN_ID} + expected_kwargs={'bgpvpn_id': BGPVPN_ID}, ) def test_bgpvpn_router_association_update(self): @@ -2204,5 +2594,5 @@ def test_bgpvpn_router_association_update(self): method_args=[BGPVPN_ID, self.ROUTER_ASSOCIATION], method_kwargs={}, expected_args=[self.ROUTER_ASSOCIATION], - expected_kwargs={'bgpvpn_id': BGPVPN_ID} + expected_kwargs={'bgpvpn_id': BGPVPN_ID}, ) diff --git a/openstack/tests/unit/network/v2/test_qos_bandwidth_limit_rule.py b/openstack/tests/unit/network/v2/test_qos_bandwidth_limit_rule.py index 581004e10..bb3c59e49 100644 --- a/openstack/tests/unit/network/v2/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/unit/network/v2/test_qos_bandwidth_limit_rule.py @@ -26,14 +26,14 @@ class TestQoSBandwidthLimitRule(base.TestCase): - def test_basic(self): sot = qos_bandwidth_limit_rule.QoSBandwidthLimitRule() self.assertEqual('bandwidth_limit_rule', sot.resource_key) self.assertEqual('bandwidth_limit_rules', sot.resources_key) self.assertEqual( '/qos/policies/%(qos_policy_id)s/bandwidth_limit_rules', - sot.base_path) + sot.base_path, + ) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_qos_dscp_marking_rule.py b/openstack/tests/unit/network/v2/test_qos_dscp_marking_rule.py index 02a71a2e4..0df8a59fa 100644 --- a/openstack/tests/unit/network/v2/test_qos_dscp_marking_rule.py +++ b/openstack/tests/unit/network/v2/test_qos_dscp_marking_rule.py @@ -24,13 +24,13 @@ class TestQoSDSCPMarkingRule(base.TestCase): - def test_basic(self): sot = qos_dscp_marking_rule.QoSDSCPMarkingRule() self.assertEqual('dscp_marking_rule', sot.resource_key) self.assertEqual('dscp_marking_rules', sot.resources_key) - self.assertEqual('/qos/policies/%(qos_policy_id)s/dscp_marking_rules', - sot.base_path) + self.assertEqual( + '/qos/policies/%(qos_policy_id)s/dscp_marking_rules', sot.base_path + ) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_qos_minimum_bandwidth_rule.py b/openstack/tests/unit/network/v2/test_qos_minimum_bandwidth_rule.py index b42335bd7..6e725cee9 100644 --- a/openstack/tests/unit/network/v2/test_qos_minimum_bandwidth_rule.py +++ b/openstack/tests/unit/network/v2/test_qos_minimum_bandwidth_rule.py @@ -25,14 +25,14 @@ class TestQoSMinimumBandwidthRule(base.TestCase): - def test_basic(self): sot = qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule() self.assertEqual('minimum_bandwidth_rule', sot.resource_key) self.assertEqual('minimum_bandwidth_rules', sot.resources_key) self.assertEqual( '/qos/policies/%(qos_policy_id)s/minimum_bandwidth_rules', - sot.base_path) + sot.base_path, + ) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_qos_minimum_packet_rate_rule.py b/openstack/tests/unit/network/v2/test_qos_minimum_packet_rate_rule.py index ebf430957..9ae6d3bea 100644 --- a/openstack/tests/unit/network/v2/test_qos_minimum_packet_rate_rule.py +++ b/openstack/tests/unit/network/v2/test_qos_minimum_packet_rate_rule.py @@ -25,14 +25,14 @@ class TestQoSMinimumPacketRateRule(base.TestCase): - def test_basic(self): sot = qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule() self.assertEqual('minimum_packet_rate_rule', sot.resource_key) self.assertEqual('minimum_packet_rate_rules', sot.resources_key) self.assertEqual( '/qos/policies/%(qos_policy_id)s/minimum_packet_rate_rules', - sot.base_path) + sot.base_path, + ) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/network/v2/test_qos_policy.py b/openstack/tests/unit/network/v2/test_qos_policy.py index 3908ebf4f..2cb22999b 100644 --- a/openstack/tests/unit/network/v2/test_qos_policy.py +++ b/openstack/tests/unit/network/v2/test_qos_policy.py @@ -24,12 +24,11 @@ 'project_id': '2', 'rules': [uuid.uuid4().hex], 'is_default': False, - 'tags': ['3'] + 'tags': ['3'], } class TestQoSPolicy(base.TestCase): - def test_basic(self): sot = qos_policy.QoSPolicy() self.assertEqual('policy', sot.resource_key) diff --git a/openstack/tests/unit/network/v2/test_qos_rule_type.py b/openstack/tests/unit/network/v2/test_qos_rule_type.py index 994eaa939..81faa977d 100644 --- a/openstack/tests/unit/network/v2/test_qos_rule_type.py +++ b/openstack/tests/unit/network/v2/test_qos_rule_type.py @@ -16,27 +16,32 @@ EXAMPLE = { 'type': 'bandwidth_limit', - 'drivers': [{ - 'name': 'openvswitch', - 'supported_parameters': [{ - 'parameter_values': {'start': 0, 'end': 2147483647}, - 'parameter_type': 'range', - 'parameter_name': 'max_kbps' - }, { - 'parameter_values': ['ingress', 'egress'], - 'parameter_type': 'choices', - 'parameter_name': 'direction' - }, { - 'parameter_values': {'start': 0, 'end': 2147483647}, - 'parameter_type': 'range', - 'parameter_name': 'max_burst_kbps' - }] - }] + 'drivers': [ + { + 'name': 'openvswitch', + 'supported_parameters': [ + { + 'parameter_values': {'start': 0, 'end': 2147483647}, + 'parameter_type': 'range', + 'parameter_name': 'max_kbps', + }, + { + 'parameter_values': ['ingress', 'egress'], + 'parameter_type': 'choices', + 'parameter_name': 'direction', + }, + { + 'parameter_values': {'start': 0, 'end': 2147483647}, + 'parameter_type': 'range', + 'parameter_name': 'max_burst_kbps', + }, + ], + } + ], } class TestQoSRuleType(base.TestCase): - def test_basic(self): sot = qos_rule_type.QoSRuleType() self.assertEqual('rule_type', sot.resource_key) @@ -47,14 +52,17 @@ def test_basic(self): self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertEqual({'type': 'type', - 'drivers': 'drivers', - 'all_rules': 'all_rules', - 'all_supported': 'all_supported', - 'limit': 'limit', - 'marker': 'marker', - }, - sot._query_mapping._mapping) + self.assertEqual( + { + 'type': 'type', + 'drivers': 'drivers', + 'all_rules': 'all_rules', + 'all_supported': 'all_supported', + 'limit': 'limit', + 'marker': 'marker', + }, + sot._query_mapping._mapping, + ) def test_make_it(self): sot = qos_rule_type.QoSRuleType(**EXAMPLE) diff --git a/openstack/tests/unit/network/v2/test_quota.py b/openstack/tests/unit/network/v2/test_quota.py index e114a5b08..ed0f66877 100644 --- a/openstack/tests/unit/network/v2/test_quota.py +++ b/openstack/tests/unit/network/v2/test_quota.py @@ -37,7 +37,6 @@ class TestQuota(base.TestCase): - def test_basic(self): sot = quota.Quota() self.assertEqual('quota', sot.resource_key) @@ -58,8 +57,9 @@ def test_make_it(self): self.assertEqual(EXAMPLE['router'], sot.routers) self.assertEqual(EXAMPLE['subnet'], sot.subnets) self.assertEqual(EXAMPLE['subnetpool'], sot.subnet_pools) - self.assertEqual(EXAMPLE['security_group_rule'], - sot.security_group_rules) + self.assertEqual( + EXAMPLE['security_group_rule'], sot.security_group_rules + ) self.assertEqual(EXAMPLE['security_group'], sot.security_groups) self.assertEqual(EXAMPLE['rbac_policy'], sot.rbac_policies) self.assertEqual(EXAMPLE['healthmonitor'], sot.health_monitors) @@ -79,12 +79,10 @@ def test_alternate_id(self): my_project_id = 'my-tenant-id' body = {'project_id': my_project_id, 'network': 12345} quota_obj = quota.Quota(**body) - self.assertEqual(my_project_id, - resource.Resource._get_id(quota_obj)) + self.assertEqual(my_project_id, resource.Resource._get_id(quota_obj)) class TestQuotaDefault(base.TestCase): - def test_basic(self): sot = quota.QuotaDefault() self.assertEqual('quota', sot.resource_key) @@ -105,8 +103,9 @@ def test_make_it(self): self.assertEqual(EXAMPLE['router'], sot.routers) self.assertEqual(EXAMPLE['subnet'], sot.subnets) self.assertEqual(EXAMPLE['subnetpool'], sot.subnet_pools) - self.assertEqual(EXAMPLE['security_group_rule'], - sot.security_group_rules) + self.assertEqual( + EXAMPLE['security_group_rule'], sot.security_group_rules + ) self.assertEqual(EXAMPLE['security_group'], sot.security_groups) self.assertEqual(EXAMPLE['rbac_policy'], sot.rbac_policies) self.assertEqual(EXAMPLE['healthmonitor'], sot.health_monitors) diff --git a/openstack/tests/unit/network/v2/test_rbac_policy.py b/openstack/tests/unit/network/v2/test_rbac_policy.py index fa8aadae0..3c625f48e 100644 --- a/openstack/tests/unit/network/v2/test_rbac_policy.py +++ b/openstack/tests/unit/network/v2/test_rbac_policy.py @@ -25,7 +25,6 @@ class TestRBACPolicy(base.TestCase): - def test_basic(self): sot = rbac_policy.RBACPolicy() self.assertEqual('rbac_policy', sot.resource_key) @@ -47,7 +46,8 @@ def test_basic(self): 'limit': 'limit', 'marker': 'marker', }, - sot._query_mapping._mapping) + sot._query_mapping._mapping, + ) def test_make_it(self): sot = rbac_policy.RBACPolicy(**EXAMPLE) diff --git a/openstack/tests/unit/network/v2/test_router.py b/openstack/tests/unit/network/v2/test_router.py index 5f4dc3c53..b3e23b6ef 100644 --- a/openstack/tests/unit/network/v2/test_router.py +++ b/openstack/tests/unit/network/v2/test_router.py @@ -48,22 +48,18 @@ 'external_gateway_info': { 'network_id': '1', 'enable_snat': True, - 'external_fixed_ips': [] + 'external_fixed_ips': [], }, 'ha': True, 'id': IDENTIFIER, 'name': 'router1', - 'routes': [{ - 'nexthop': '172.24.4.20', - 'destination': '10.0.3.1/24' - }], + 'routes': [{'nexthop': '172.24.4.20', 'destination': '10.0.3.1/24'}], 'status': 'ACTIVE', 'project_id': '2', } class TestRouter(base.TestCase): - def test_basic(self): sot = router.Router() self.assertEqual('router', sot.resource_key) @@ -78,16 +74,17 @@ def test_basic(self): def test_make_it(self): sot = router.Router(**EXAMPLE) self.assertTrue(sot.is_admin_state_up) - self.assertEqual(EXAMPLE['availability_zone_hints'], - sot.availability_zone_hints) - self.assertEqual(EXAMPLE['availability_zones'], - sot.availability_zones) + self.assertEqual( + EXAMPLE['availability_zone_hints'], sot.availability_zone_hints + ) + self.assertEqual(EXAMPLE['availability_zones'], sot.availability_zones) self.assertEqual(EXAMPLE['created_at'], sot.created_at) self.assertEqual(EXAMPLE['description'], sot.description) self.assertTrue(sot.enable_ndp_proxy) self.assertFalse(sot.is_distributed) - self.assertEqual(EXAMPLE['external_gateway_info'], - sot.external_gateway_info) + self.assertEqual( + EXAMPLE['external_gateway_info'], sot.external_gateway_info + ) self.assertEqual(EXAMPLE['flavor_id'], sot.flavor_id) self.assertFalse(sot.is_ha) self.assertEqual(EXAMPLE['id'], sot.id) @@ -101,15 +98,19 @@ def test_make_it(self): def test_make_it_with_optional(self): sot = router.Router(**EXAMPLE_WITH_OPTIONAL) self.assertFalse(sot.is_admin_state_up) - self.assertEqual(EXAMPLE_WITH_OPTIONAL['availability_zone_hints'], - sot.availability_zone_hints) - self.assertEqual(EXAMPLE_WITH_OPTIONAL['availability_zones'], - sot.availability_zones) - self.assertEqual(EXAMPLE_WITH_OPTIONAL['description'], - sot.description) + self.assertEqual( + EXAMPLE_WITH_OPTIONAL['availability_zone_hints'], + sot.availability_zone_hints, + ) + self.assertEqual( + EXAMPLE_WITH_OPTIONAL['availability_zones'], sot.availability_zones + ) + self.assertEqual(EXAMPLE_WITH_OPTIONAL['description'], sot.description) self.assertTrue(sot.is_distributed) - self.assertEqual(EXAMPLE_WITH_OPTIONAL['external_gateway_info'], - sot.external_gateway_info) + self.assertEqual( + EXAMPLE_WITH_OPTIONAL['external_gateway_info'], + sot.external_gateway_info, + ) self.assertTrue(sot.is_ha) self.assertEqual(EXAMPLE_WITH_OPTIONAL['id'], sot.id) self.assertEqual(EXAMPLE_WITH_OPTIONAL['name'], sot.name) @@ -130,8 +131,7 @@ def test_add_interface_subnet(self): self.assertEqual(response.body, sot.add_interface(sess, **body)) url = 'routers/IDENTIFIER/add_router_interface' - sess.put.assert_called_with(url, - json=body) + sess.put.assert_called_with(url, json=body) def test_add_interface_port(self): # Add port to a router @@ -147,8 +147,7 @@ def test_add_interface_port(self): self.assertEqual(response.body, sot.add_interface(sess, **body)) url = 'routers/IDENTIFIER/add_router_interface' - sess.put.assert_called_with(url, - json=body) + sess.put.assert_called_with(url, json=body) def test_remove_interface_subnet(self): # Remove subnet from a router @@ -163,8 +162,7 @@ def test_remove_interface_subnet(self): self.assertEqual(response.body, sot.remove_interface(sess, **body)) url = 'routers/IDENTIFIER/remove_router_interface' - sess.put.assert_called_with(url, - json=body) + sess.put.assert_called_with(url, json=body) def test_remove_interface_port(self): # Remove port from a router @@ -179,8 +177,7 @@ def test_remove_interface_port(self): self.assertEqual(response.body, sot.remove_interface(sess, **body)) url = 'routers/IDENTIFIER/remove_router_interface' - sess.put.assert_called_with(url, - json=body) + sess.put.assert_called_with(url, json=body) def test_add_interface_4xx(self): # Neutron may return 4xx, we have to raise if that happens @@ -259,8 +256,7 @@ def test_add_router_gateway(self): self.assertEqual(response.body, sot.add_gateway(sess, **body)) url = 'routers/IDENTIFIER/add_gateway_router' - sess.put.assert_called_with(url, - json=body) + sess.put.assert_called_with(url, json=body) def test_remove_router_gateway(self): # Remove gateway to a router @@ -274,12 +270,10 @@ def test_remove_router_gateway(self): self.assertEqual(response.body, sot.remove_gateway(sess, **body)) url = 'routers/IDENTIFIER/remove_gateway_router' - sess.put.assert_called_with(url, - json=body) + sess.put.assert_called_with(url, json=body) class TestL3AgentRouters(base.TestCase): - def test_basic(self): sot = router.L3AgentRouter() self.assertEqual('router', sot.resource_key) diff --git a/openstack/tests/unit/network/v2/test_security_group.py b/openstack/tests/unit/network/v2/test_security_group.py index 13ff1b4ee..3512b9a06 100644 --- a/openstack/tests/unit/network/v2/test_security_group.py +++ b/openstack/tests/unit/network/v2/test_security_group.py @@ -21,8 +21,7 @@ "direction": "egress", "remote_ip_prefix": None, "protocol": None, - "ethertype": - "IPv6", + "ethertype": "IPv6", "project_id": "4", "port_range_max": None, "port_range_min": None, @@ -60,12 +59,11 @@ 'project_id': '4', 'project_id': '4', 'updated_at': '2016-10-14T12:16:57.233772', - 'tags': ['5'] + 'tags': ['5'], } class TestSecurityGroup(base.TestCase): - def test_basic(self): sot = security_group.SecurityGroup() self.assertEqual('security_group', sot.resource_key) @@ -77,24 +75,27 @@ def test_basic(self): self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertDictEqual({'any_tags': 'tags-any', - 'description': 'description', - 'fields': 'fields', - 'id': 'id', - 'limit': 'limit', - 'marker': 'marker', - 'name': 'name', - 'not_any_tags': 'not-tags-any', - 'not_tags': 'not-tags', - 'tenant_id': 'tenant_id', - 'revision_number': 'revision_number', - 'sort_dir': 'sort_dir', - 'sort_key': 'sort_key', - 'tags': 'tags', - 'project_id': 'project_id', - 'stateful': 'stateful', - }, - sot._query_mapping._mapping) + self.assertDictEqual( + { + 'any_tags': 'tags-any', + 'description': 'description', + 'fields': 'fields', + 'id': 'id', + 'limit': 'limit', + 'marker': 'marker', + 'name': 'name', + 'not_any_tags': 'not-tags-any', + 'not_tags': 'not-tags', + 'tenant_id': 'tenant_id', + 'revision_number': 'revision_number', + 'sort_dir': 'sort_dir', + 'sort_key': 'sort_key', + 'tags': 'tags', + 'project_id': 'project_id', + 'stateful': 'stateful', + }, + sot._query_mapping._mapping, + ) def test_make_it(self): sot = security_group.SecurityGroup(**EXAMPLE) @@ -103,8 +104,9 @@ def test_make_it(self): self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['revision_number'], sot.revision_number) - self.assertEqual(EXAMPLE['security_group_rules'], - sot.security_group_rules) + self.assertEqual( + EXAMPLE['security_group_rules'], sot.security_group_rules + ) self.assertEqual(dict, type(sot.security_group_rules[0])) self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['project_id'], sot.project_id) diff --git a/openstack/tests/unit/network/v2/test_security_group_rule.py b/openstack/tests/unit/network/v2/test_security_group_rule.py index eca85dfeb..c9e82c80a 100644 --- a/openstack/tests/unit/network/v2/test_security_group_rule.py +++ b/openstack/tests/unit/network/v2/test_security_group_rule.py @@ -31,12 +31,11 @@ 'project_id': '11', 'project_id': '11', 'updated_at': '12', - 'remote_address_group_id': '13' + 'remote_address_group_id': '13', } class TestSecurityGroupRule(base.TestCase): - def test_basic(self): sot = security_group_rule.SecurityGroupRule() self.assertEqual('security_group_rule', sot.resource_key) @@ -48,31 +47,33 @@ def test_basic(self): self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertDictEqual({'any_tags': 'tags-any', - 'description': 'description', - 'direction': 'direction', - 'id': 'id', - 'ether_type': 'ethertype', - 'limit': 'limit', - 'marker': 'marker', - 'not_any_tags': 'not-tags-any', - 'not_tags': 'not-tags', - 'port_range_max': 'port_range_max', - 'port_range_min': 'port_range_min', - 'tenant_id': 'tenant_id', - 'protocol': 'protocol', - 'remote_group_id': 'remote_group_id', - 'remote_address_group_id': - 'remote_address_group_id', - 'remote_ip_prefix': 'remote_ip_prefix', - 'revision_number': 'revision_number', - 'security_group_id': 'security_group_id', - 'sort_dir': 'sort_dir', - 'sort_key': 'sort_key', - 'tags': 'tags', - 'project_id': 'project_id' - }, - sot._query_mapping._mapping) + self.assertDictEqual( + { + 'any_tags': 'tags-any', + 'description': 'description', + 'direction': 'direction', + 'id': 'id', + 'ether_type': 'ethertype', + 'limit': 'limit', + 'marker': 'marker', + 'not_any_tags': 'not-tags-any', + 'not_tags': 'not-tags', + 'port_range_max': 'port_range_max', + 'port_range_min': 'port_range_min', + 'tenant_id': 'tenant_id', + 'protocol': 'protocol', + 'remote_group_id': 'remote_group_id', + 'remote_address_group_id': 'remote_address_group_id', + 'remote_ip_prefix': 'remote_ip_prefix', + 'revision_number': 'revision_number', + 'security_group_id': 'security_group_id', + 'sort_dir': 'sort_dir', + 'sort_key': 'sort_key', + 'tags': 'tags', + 'project_id': 'project_id', + }, + sot._query_mapping._mapping, + ) def test_make_it(self): sot = security_group_rule.SecurityGroupRule(**EXAMPLE) @@ -85,8 +86,9 @@ def test_make_it(self): self.assertEqual(EXAMPLE['port_range_min'], sot.port_range_min) self.assertEqual(EXAMPLE['protocol'], sot.protocol) self.assertEqual(EXAMPLE['remote_group_id'], sot.remote_group_id) - self.assertEqual(EXAMPLE['remote_address_group_id'], - sot.remote_address_group_id) + self.assertEqual( + EXAMPLE['remote_address_group_id'], sot.remote_address_group_id + ) self.assertEqual(EXAMPLE['remote_ip_prefix'], sot.remote_ip_prefix) self.assertEqual(EXAMPLE['revision_number'], sot.revision_number) self.assertEqual(EXAMPLE['security_group_id'], sot.security_group_id) diff --git a/openstack/tests/unit/network/v2/test_segment.py b/openstack/tests/unit/network/v2/test_segment.py index 4130f6650..329151f84 100644 --- a/openstack/tests/unit/network/v2/test_segment.py +++ b/openstack/tests/unit/network/v2/test_segment.py @@ -27,7 +27,6 @@ class TestSegment(base.TestCase): - def test_basic(self): sot = segment.Segment() self.assertEqual('segment', sot.resource_key) diff --git a/openstack/tests/unit/network/v2/test_service_profile.py b/openstack/tests/unit/network/v2/test_service_profile.py index bd877c45b..f213c17f1 100644 --- a/openstack/tests/unit/network/v2/test_service_profile.py +++ b/openstack/tests/unit/network/v2/test_service_profile.py @@ -46,14 +46,20 @@ def test_make_it(self): def test_make_it_with_optional(self): service_profiles = service_profile.ServiceProfile( - **EXAMPLE_WITH_OPTIONAL) - self.assertEqual(EXAMPLE_WITH_OPTIONAL['description'], - service_profiles.description) - self.assertEqual(EXAMPLE_WITH_OPTIONAL['driver'], - service_profiles.driver) - self.assertEqual(EXAMPLE_WITH_OPTIONAL['enabled'], - service_profiles.is_enabled) - self.assertEqual(EXAMPLE_WITH_OPTIONAL['metainfo'], - service_profiles.meta_info) - self.assertEqual(EXAMPLE_WITH_OPTIONAL['project_id'], - service_profiles.project_id) + **EXAMPLE_WITH_OPTIONAL + ) + self.assertEqual( + EXAMPLE_WITH_OPTIONAL['description'], service_profiles.description + ) + self.assertEqual( + EXAMPLE_WITH_OPTIONAL['driver'], service_profiles.driver + ) + self.assertEqual( + EXAMPLE_WITH_OPTIONAL['enabled'], service_profiles.is_enabled + ) + self.assertEqual( + EXAMPLE_WITH_OPTIONAL['metainfo'], service_profiles.meta_info + ) + self.assertEqual( + EXAMPLE_WITH_OPTIONAL['project_id'], service_profiles.project_id + ) diff --git a/openstack/tests/unit/network/v2/test_service_provider.py b/openstack/tests/unit/network/v2/test_service_provider.py index ad578bff9..aa90903a4 100644 --- a/openstack/tests/unit/network/v2/test_service_provider.py +++ b/openstack/tests/unit/network/v2/test_service_provider.py @@ -23,7 +23,6 @@ class TestServiceProvider(base.TestCase): - def test_basic(self): sot = service_provider.ServiceProvider() diff --git a/openstack/tests/unit/network/v2/test_subnet.py b/openstack/tests/unit/network/v2/test_subnet.py index 1a9b216f2..ae4bbf977 100644 --- a/openstack/tests/unit/network/v2/test_subnet.py +++ b/openstack/tests/unit/network/v2/test_subnet.py @@ -42,7 +42,6 @@ class TestSubnet(base.TestCase): - def test_basic(self): sot = subnet.Subnet() self.assertEqual('subnet', sot.resource_key) diff --git a/openstack/tests/unit/network/v2/test_subnet_pool.py b/openstack/tests/unit/network/v2/test_subnet_pool.py index 81dea5c1e..8d04f500e 100644 --- a/openstack/tests/unit/network/v2/test_subnet_pool.py +++ b/openstack/tests/unit/network/v2/test_subnet_pool.py @@ -36,7 +36,6 @@ class TestSubnetpool(base.TestCase): - def test_basic(self): sot = subnet_pool.SubnetPool() self.assertEqual('subnetpool', sot.resource_key) @@ -52,8 +51,9 @@ def test_make_it(self): sot = subnet_pool.SubnetPool(**EXAMPLE) self.assertEqual(EXAMPLE['address_scope_id'], sot.address_scope_id) self.assertEqual(EXAMPLE['created_at'], sot.created_at) - self.assertEqual(EXAMPLE['default_prefixlen'], - sot.default_prefix_length) + self.assertEqual( + EXAMPLE['default_prefixlen'], sot.default_prefix_length + ) self.assertEqual(EXAMPLE['default_quota'], sot.default_quota) self.assertEqual(EXAMPLE['description'], sot.description) self.assertEqual(EXAMPLE['id'], sot.id) diff --git a/openstack/tests/unit/network/v2/test_tap_flow.py b/openstack/tests/unit/network/v2/test_tap_flow.py index fdeaeaffb..1eb382a6d 100644 --- a/openstack/tests/unit/network/v2/test_tap_flow.py +++ b/openstack/tests/unit/network/v2/test_tap_flow.py @@ -20,12 +20,11 @@ 'source_port': '1234', 'tap_service_id': '4321', 'id': IDENTIFIER, - 'project_id': '42' + 'project_id': '42', } class TestTapFlow(base.TestCase): - def test_basic(self): sot = tap_flow.TapFlow() self.assertEqual('tap_flow', sot.resource_key) @@ -46,11 +45,13 @@ def test_make_it(self): self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertDictEqual( - {'limit': 'limit', - 'marker': 'marker', - 'name': 'name', - 'project_id': 'project_id', - 'sort_key': 'sort_key', - 'sort_dir': 'sort_dir', - }, - sot._query_mapping._mapping) + { + 'limit': 'limit', + 'marker': 'marker', + 'name': 'name', + 'project_id': 'project_id', + 'sort_key': 'sort_key', + 'sort_dir': 'sort_dir', + }, + sot._query_mapping._mapping, + ) diff --git a/openstack/tests/unit/network/v2/test_tap_service.py b/openstack/tests/unit/network/v2/test_tap_service.py index 1892ffa50..b20da6f24 100644 --- a/openstack/tests/unit/network/v2/test_tap_service.py +++ b/openstack/tests/unit/network/v2/test_tap_service.py @@ -19,12 +19,11 @@ 'name': 'my_tap_service', 'port_id': '1234', 'id': IDENTIFIER, - 'project_id': '42' + 'project_id': '42', } class TestTapService(base.TestCase): - def test_basic(self): sot = tap_service.TapService() self.assertEqual('tap_service', sot.resource_key) @@ -44,11 +43,13 @@ def test_make_it(self): self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertDictEqual( - {'limit': 'limit', - 'marker': 'marker', - 'name': 'name', - 'project_id': 'project_id', - 'sort_key': 'sort_key', - 'sort_dir': 'sort_dir', - }, - sot._query_mapping._mapping) + { + 'limit': 'limit', + 'marker': 'marker', + 'name': 'name', + 'project_id': 'project_id', + 'sort_key': 'sort_key', + 'sort_dir': 'sort_dir', + }, + sot._query_mapping._mapping, + ) diff --git a/openstack/tests/unit/network/v2/test_trunk.py b/openstack/tests/unit/network/v2/test_trunk.py index 5f509c675..3842668a3 100644 --- a/openstack/tests/unit/network/v2/test_trunk.py +++ b/openstack/tests/unit/network/v2/test_trunk.py @@ -26,17 +26,17 @@ 'admin_state_up': True, 'port_id': 'fake_port_id', 'status': 'ACTIVE', - 'sub_ports': [{ - 'port_id': 'subport_port_id', - 'segmentation_id': 1234, - 'segmentation_type': 'vlan' - }] - + 'sub_ports': [ + { + 'port_id': 'subport_port_id', + 'segmentation_id': 1234, + 'segmentation_type': 'vlan', + } + ], } class TestTrunk(base.TestCase): - def test_basic(self): sot = trunk.Trunk() self.assertEqual('trunk', sot.resource_key) @@ -71,8 +71,13 @@ def test_add_subports_4xx(self): response.headers = {'content-type': 'application/json'} sess = mock.Mock() sess.put = mock.Mock(return_value=response) - subports = [{'port_id': 'abc', 'segmentation_id': '123', - 'segmentation_type': 'vlan'}] + subports = [ + { + 'port_id': 'abc', + 'segmentation_id': '123', + 'segmentation_type': 'vlan', + } + ] with testtools.ExpectedException(exceptions.ResourceNotFound, msg): sot.add_subports(sess, subports) @@ -88,7 +93,12 @@ def test_delete_subports_4xx(self): response.headers = {'content-type': 'application/json'} sess = mock.Mock() sess.put = mock.Mock(return_value=response) - subports = [{'port_id': 'abc', 'segmentation_id': '123', - 'segmentation_type': 'vlan'}] + subports = [ + { + 'port_id': 'abc', + 'segmentation_id': '123', + 'segmentation_type': 'vlan', + } + ] with testtools.ExpectedException(exceptions.ResourceNotFound, msg): sot.delete_subports(sess, subports) diff --git a/openstack/tests/unit/network/v2/test_vpn_endpoint_group.py b/openstack/tests/unit/network/v2/test_vpn_endpoint_group.py index 4c98666c9..f5a658810 100644 --- a/openstack/tests/unit/network/v2/test_vpn_endpoint_group.py +++ b/openstack/tests/unit/network/v2/test_vpn_endpoint_group.py @@ -19,18 +19,14 @@ "description": "", "project_id": "4ad57e7ce0b24fca8f12b9834d91079d", "tenant_id": "4ad57e7ce0b24fca8f12b9834d91079d", - "endpoints": [ - "10.2.0.0/24", - "10.3.0.0/24" - ], + "endpoints": ["10.2.0.0/24", "10.3.0.0/24"], "type": "cidr", "id": "6ecd9cf3-ca64-46c7-863f-f2eb1b9e838a", - "name": "peers" + "name": "peers", } class TestVpnEndpointGroup(base.TestCase): - def test_basic(self): sot = vpn_endpoint_group.VpnEndpointGroup() self.assertEqual('endpoint_group', sot.resource_key) @@ -61,4 +57,5 @@ def test_make_it(self): 'tenant_id': 'tenant_id', 'type': 'endpoint_type', }, - sot._query_mapping._mapping) + sot._query_mapping._mapping, + ) diff --git a/openstack/tests/unit/network/v2/test_vpn_ikepolicy.py b/openstack/tests/unit/network/v2/test_vpn_ikepolicy.py index b88271ac0..a52164478 100644 --- a/openstack/tests/unit/network/v2/test_vpn_ikepolicy.py +++ b/openstack/tests/unit/network/v2/test_vpn_ikepolicy.py @@ -25,12 +25,11 @@ "project_id": "7", "phase1_negotiation_mode": "8", "units": "9", - "value": 10 + "value": 10, } class TestVpnIkePolicy(base.TestCase): - def test_basic(self): sot = vpn_ike_policy.VpnIkePolicy() self.assertEqual('ikepolicy', sot.resource_key) @@ -46,14 +45,16 @@ def test_make_it(self): sot = vpn_ike_policy.VpnIkePolicy(**EXAMPLE) self.assertEqual(EXAMPLE['auth_algorithm'], sot.auth_algorithm) self.assertEqual(EXAMPLE['description'], sot.description) - self.assertEqual(EXAMPLE['encryption_algorithm'], - sot.encryption_algorithm) + self.assertEqual( + EXAMPLE['encryption_algorithm'], sot.encryption_algorithm + ) self.assertEqual(EXAMPLE['ike_version'], sot.ike_version) self.assertEqual(EXAMPLE['lifetime'], sot.lifetime) self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['pfs'], sot.pfs) self.assertEqual(EXAMPLE['project_id'], sot.project_id) - self.assertEqual(EXAMPLE['phase1_negotiation_mode'], - sot.phase1_negotiation_mode) + self.assertEqual( + EXAMPLE['phase1_negotiation_mode'], sot.phase1_negotiation_mode + ) self.assertEqual(EXAMPLE['units'], sot.units) self.assertEqual(EXAMPLE['value'], sot.value) diff --git a/openstack/tests/unit/network/v2/test_vpn_ipsec_site_connection.py b/openstack/tests/unit/network/v2/test_vpn_ipsec_site_connection.py index 48e406e55..459f9e5f0 100644 --- a/openstack/tests/unit/network/v2/test_vpn_ipsec_site_connection.py +++ b/openstack/tests/unit/network/v2/test_vpn_ipsec_site_connection.py @@ -37,12 +37,11 @@ "dpd": {'a': 5}, "timeout": 16, "action": "17", - "local_id": "18" + "local_id": "18", } class TestVpnIPSecSiteConnection(base.TestCase): - def test_basic(self): sot = vpn_ipsec_site_connection.VpnIPSecSiteConnection() self.assertEqual('ipsec_site_connection', sot.resource_key) diff --git a/openstack/tests/unit/network/v2/test_vpn_ipsecpolicy.py b/openstack/tests/unit/network/v2/test_vpn_ipsecpolicy.py index fd6f7c88e..0d945d085 100644 --- a/openstack/tests/unit/network/v2/test_vpn_ipsecpolicy.py +++ b/openstack/tests/unit/network/v2/test_vpn_ipsecpolicy.py @@ -23,12 +23,11 @@ "pfs": "6", "project_id": "7", "units": "9", - "value": 10 + "value": 10, } class TestVpnIpsecPolicy(base.TestCase): - def test_basic(self): sot = vpn_ipsec_policy.VpnIpsecPolicy() self.assertEqual('ipsecpolicy', sot.resource_key) @@ -44,8 +43,9 @@ def test_make_it(self): sot = vpn_ipsec_policy.VpnIpsecPolicy(**EXAMPLE) self.assertEqual(EXAMPLE['auth_algorithm'], sot.auth_algorithm) self.assertEqual(EXAMPLE['description'], sot.description) - self.assertEqual(EXAMPLE['encryption_algorithm'], - sot.encryption_algorithm) + self.assertEqual( + EXAMPLE['encryption_algorithm'], sot.encryption_algorithm + ) self.assertEqual(EXAMPLE['lifetime'], sot.lifetime) self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['pfs'], sot.pfs) @@ -65,4 +65,5 @@ def test_make_it(self): 'project_id': 'project_id', 'phase1_negotiation_mode': 'phase1_negotiation_mode', }, - sot._query_mapping._mapping) + sot._query_mapping._mapping, + ) diff --git a/openstack/tests/unit/network/v2/test_vpn_service.py b/openstack/tests/unit/network/v2/test_vpn_service.py index 8b4c29236..72b69d3f0 100644 --- a/openstack/tests/unit/network/v2/test_vpn_service.py +++ b/openstack/tests/unit/network/v2/test_vpn_service.py @@ -30,7 +30,6 @@ class TestVpnService(base.TestCase): - def test_basic(self): sot = vpn_service.VpnService() self.assertEqual('vpnservice', sot.resource_key) @@ -69,4 +68,5 @@ def test_make_it(self): 'subnet_id': 'subnet_id', 'is_admin_state_up': 'admin_state_up', }, - sot._query_mapping._mapping) + sot._query_mapping._mapping, + ) diff --git a/tox.ini b/tox.ini index 24ec06df8..13a0c3a3b 100644 --- a/tox.ini +++ b/tox.ini @@ -129,11 +129,12 @@ application-import-names = openstack # if they fix ALL of the occurances of one and only one of them. # E203 Black will put spaces after colons in list comprehensions # H238 New Style Classes are the default in Python3 +# H301 Black will put commas after imports that can't fit on one line # H4 Are about docstrings and there's just a huge pile of pre-existing issues. # W503 Is supposed to be off by default but in the latest pycodestyle isn't. # Also, both openstacksdk and Donald Knuth disagree with the rule. Line # breaks should occur before the binary operator for readability. -ignore = E203, H238, H4, W503 +ignore = E203, H301, H238, H4, W503 import-order-style = pep8 show-source = True exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build,openstack/_services_mixin.py From 542ddaa1ad5cfc9b9876de3de0759941c9a9ea83 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 3 May 2023 12:11:13 +0100 Subject: [PATCH 3239/3836] Blackify openstack.identity Black used with the '-l 79 -S' flags. A future change will ignore this commit in git-blame history by adding a 'git-blame-ignore-revs' file. Change-Id: I2faa1f6c0a41946b3672bafc430b53a09611aea4 Signed-off-by: Stephen Finucane --- openstack/identity/v2/_proxy.py | 16 +- openstack/identity/v2/extension.py | 3 +- openstack/identity/v3/_proxy.py | 259 +++++++++++------- openstack/identity/v3/credential.py | 3 +- openstack/identity/v3/domain.py | 54 ++-- openstack/identity/v3/endpoint.py | 4 +- openstack/identity/v3/group.py | 24 +- openstack/identity/v3/limit.py | 3 +- openstack/identity/v3/project.py | 54 ++-- openstack/identity/v3/registered_limit.py | 3 +- openstack/identity/v3/role.py | 3 +- openstack/identity/v3/role_assignment.py | 17 +- openstack/identity/v3/system.py | 48 ++-- openstack/identity/v3/trust.py | 3 +- openstack/identity/version.py | 3 +- .../v3/test_application_credential.py | 19 +- openstack/tests/unit/identity/test_version.py | 1 - .../tests/unit/identity/v2/test_extension.py | 1 - openstack/tests/unit/identity/v2/test_role.py | 1 - .../tests/unit/identity/v2/test_tenant.py | 1 - openstack/tests/unit/identity/v2/test_user.py | 1 - .../v3/test_application_credential.py | 15 +- .../tests/unit/identity/v3/test_credential.py | 4 +- .../tests/unit/identity/v3/test_domain.py | 94 +++---- .../tests/unit/identity/v3/test_endpoint.py | 4 +- .../identity/v3/test_federation_protocol.py | 7 +- .../tests/unit/identity/v3/test_group.py | 24 +- .../identity/v3/test_identity_provider.py | 4 +- .../tests/unit/identity/v3/test_limit.py | 8 +- .../tests/unit/identity/v3/test_mapping.py | 4 +- .../tests/unit/identity/v3/test_policy.py | 1 - .../tests/unit/identity/v3/test_project.py | 99 +++---- .../tests/unit/identity/v3/test_proxy.py | 119 ++++---- .../tests/unit/identity/v3/test_region.py | 4 +- .../unit/identity/v3/test_registered_limit.py | 8 +- openstack/tests/unit/identity/v3/test_role.py | 6 +- .../unit/identity/v3/test_role_assignment.py | 6 +- .../v3/test_role_domain_group_assignment.py | 11 +- .../v3/test_role_domain_user_assignment.py | 11 +- .../v3/test_role_project_group_assignment.py | 13 +- .../v3/test_role_project_user_assignment.py | 11 +- .../v3/test_role_system_group_assignment.py | 13 +- .../v3/test_role_system_user_assignment.py | 13 +- .../tests/unit/identity/v3/test_service.py | 4 +- .../tests/unit/identity/v3/test_trust.py | 6 +- openstack/tests/unit/identity/v3/test_user.py | 12 +- 46 files changed, 552 insertions(+), 470 deletions(-) diff --git a/openstack/identity/v2/_proxy.py b/openstack/identity/v2/_proxy.py index 23772af45..f0a96f580 100644 --- a/openstack/identity/v2/_proxy.py +++ b/openstack/identity/v2/_proxy.py @@ -18,7 +18,6 @@ class Proxy(proxy.Proxy): - def extensions(self): """Retrieve a generator of extensions @@ -78,8 +77,9 @@ def find_role(self, name_or_id, ignore_missing=True): attempting to find a nonexistent resource. :returns: One :class:`~openstack.identity.v2.role.Role` or None """ - return self._find(_role.Role, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _role.Role, name_or_id, ignore_missing=ignore_missing + ) def get_role(self, role): """Get a single role @@ -155,8 +155,9 @@ def find_tenant(self, name_or_id, ignore_missing=True): attempting to find a nonexistent resource. :returns: One :class:`~openstack.identity.v2.tenant.Tenant` or None """ - return self._find(_tenant.Tenant, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _tenant.Tenant, name_or_id, ignore_missing=ignore_missing + ) def get_tenant(self, tenant): """Get a single tenant @@ -232,8 +233,9 @@ def find_user(self, name_or_id, ignore_missing=True): attempting to find a nonexistent resource. :returns: One :class:`~openstack.identity.v2.user.User` or None """ - return self._find(_user.User, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _user.User, name_or_id, ignore_missing=ignore_missing + ) def get_user(self, user): """Get a single user diff --git a/openstack/identity/v2/extension.py b/openstack/identity/v2/extension.py index 93f71ad95..1e0d26428 100644 --- a/openstack/identity/v2/extension.py +++ b/openstack/identity/v2/extension.py @@ -48,8 +48,7 @@ def list(cls, session, paginated=False, base_path=None, **params): if base_path is None: base_path = cls.base_path - resp = session.get(base_path, - params=params) + resp = session.get(base_path, params=params) resp = resp.json() for data in resp[cls.resources_key]['values']: yield cls.existing(**data) diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 7228bf209..d772f75c0 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -11,8 +11,9 @@ # under the License. import openstack.exceptions as exception -from openstack.identity.v3 import application_credential as \ - _application_credential +from openstack.identity.v3 import ( + application_credential as _application_credential, +) from openstack.identity.v3 import credential as _credential from openstack.identity.v3 import domain as _domain from openstack.identity.v3 import endpoint as _endpoint @@ -27,18 +28,24 @@ from openstack.identity.v3 import registered_limit as _registered_limit from openstack.identity.v3 import role as _role from openstack.identity.v3 import role_assignment as _role_assignment -from openstack.identity.v3 import role_domain_group_assignment \ - as _role_domain_group_assignment -from openstack.identity.v3 import role_domain_user_assignment \ - as _role_domain_user_assignment -from openstack.identity.v3 import role_project_group_assignment \ - as _role_project_group_assignment -from openstack.identity.v3 import role_project_user_assignment \ - as _role_project_user_assignment -from openstack.identity.v3 import role_system_group_assignment \ - as _role_system_group_assignment -from openstack.identity.v3 import role_system_user_assignment \ - as _role_system_user_assignment +from openstack.identity.v3 import ( + role_domain_group_assignment as _role_domain_group_assignment, +) +from openstack.identity.v3 import ( + role_domain_user_assignment as _role_domain_user_assignment, +) +from openstack.identity.v3 import ( + role_project_group_assignment as _role_project_group_assignment, +) +from openstack.identity.v3 import ( + role_project_user_assignment as _role_project_user_assignment, +) +from openstack.identity.v3 import ( + role_system_group_assignment as _role_system_group_assignment, +) +from openstack.identity.v3 import ( + role_system_user_assignment as _role_system_user_assignment, +) from openstack.identity.v3 import service as _service from openstack.identity.v3 import system as _system from openstack.identity.v3 import trust as _trust @@ -49,8 +56,7 @@ class Proxy(proxy.Proxy): _resource_registry = { - "application_credential": - _application_credential.ApplicationCredential, + "application_credential": _application_credential.ApplicationCredential, # noqa: E501 "credential": _credential.Credential, "domain": _domain.Domain, "endpoint": _endpoint.Endpoint, @@ -65,18 +71,12 @@ class Proxy(proxy.Proxy): "registered_limit": _registered_limit.RegisteredLimit, "role": _role.Role, "role_assignment": _role_assignment.RoleAssignment, - "role_domain_group_assignment": - _role_domain_group_assignment.RoleDomainGroupAssignment, - "role_domain_user_assignment": - _role_domain_user_assignment.RoleDomainUserAssignment, - "role_project_group_assignment": - _role_project_group_assignment.RoleProjectGroupAssignment, - "role_project_user_assignment": - _role_project_user_assignment.RoleProjectUserAssignment, - "role_system_group_assignment": - _role_system_group_assignment.RoleSystemGroupAssignment, - "role_system_user_assignment": - _role_system_user_assignment.RoleSystemUserAssignment, + "role_domain_group_assignment": _role_domain_group_assignment.RoleDomainGroupAssignment, # noqa: E501 + "role_domain_user_assignment": _role_domain_user_assignment.RoleDomainUserAssignment, # noqa: E501 + "role_project_group_assignment": _role_project_group_assignment.RoleProjectGroupAssignment, # noqa: E501 + "role_project_user_assignment": _role_project_user_assignment.RoleProjectUserAssignment, # noqa: E501 + "role_system_group_assignment": _role_system_group_assignment.RoleSystemGroupAssignment, # noqa: E501 + "role_system_user_assignment": _role_system_user_assignment.RoleSystemUserAssignment, # noqa: E501 "service": _service.Service, "system": _system.System, "trust": _trust.Trust, @@ -108,8 +108,9 @@ def delete_credential(self, credential, ignore_missing=True): :returns: ``None`` """ - self._delete(_credential.Credential, credential, - ignore_missing=ignore_missing) + self._delete( + _credential.Credential, credential, ignore_missing=ignore_missing + ) def find_credential(self, name_or_id, ignore_missing=True): """Find a single credential @@ -123,8 +124,9 @@ def find_credential(self, name_or_id, ignore_missing=True): :returns: One :class:`~openstack.identity.v3.credential.Credential` or None """ - return self._find(_credential.Credential, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _credential.Credential, name_or_id, ignore_missing=ignore_missing + ) def get_credential(self, credential): """Get a single credential @@ -201,8 +203,9 @@ def find_domain(self, name_or_id, ignore_missing=True): attempting to find a nonexistent resource. :returns: One :class:`~openstack.identity.v3.domain.Domain` or None """ - return self._find(_domain.Domain, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _domain.Domain, name_or_id, ignore_missing=ignore_missing + ) def get_domain(self, domain): """Get a single domain @@ -266,8 +269,9 @@ def delete_endpoint(self, endpoint, ignore_missing=True): :returns: ``None`` """ - self._delete(_endpoint.Endpoint, endpoint, - ignore_missing=ignore_missing) + self._delete( + _endpoint.Endpoint, endpoint, ignore_missing=ignore_missing + ) def find_endpoint(self, name_or_id, ignore_missing=True): """Find a single endpoint @@ -280,8 +284,9 @@ def find_endpoint(self, name_or_id, ignore_missing=True): attempting to find a nonexistent resource. :returns: One :class:`~openstack.identity.v3.endpoint.Endpoint` or None """ - return self._find(_endpoint.Endpoint, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _endpoint.Endpoint, name_or_id, ignore_missing=ignore_missing + ) def get_endpoint(self, endpoint): """Get a single endpoint @@ -453,8 +458,7 @@ def group_users(self, group, **attrs): :return: List of :class:`~openstack.identity.v3.user.User` """ group = self._get_resource(_group.Group, group) - base_path = utils.urljoin( - group.base_path, group.id, 'users') + base_path = utils.urljoin(group.base_path, group.id, 'users') users = self._list(_user.User, base_path=base_path, **attrs) return users @@ -496,8 +500,9 @@ def find_policy(self, name_or_id, ignore_missing=True): attempting to find a nonexistent resource. :returns: One :class:`~openstack.identity.v3.policy.Policy` or None """ - return self._find(_policy.Policy, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _policy.Policy, name_or_id, ignore_missing=ignore_missing + ) def get_policy(self, policy): """Get a single policy @@ -671,8 +676,9 @@ def find_service(self, name_or_id, ignore_missing=True): attempting to find a nonexistent resource. :returns: One :class:`~openstack.identity.v3.service.Service` or None """ - return self._find(_service.Service, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _service.Service, name_or_id, ignore_missing=ignore_missing + ) def get_service(self, service): """Get a single service @@ -831,8 +837,9 @@ def find_trust(self, name_or_id, ignore_missing=True): attempting to find a nonexistent resource. :returns: One :class:`~openstack.identity.v3.trust.Trust` or None """ - return self._find(_trust.Trust, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _trust.Trust, name_or_id, ignore_missing=ignore_missing + ) def get_trust(self, trust): """Get a single trust @@ -896,8 +903,9 @@ def find_region(self, name_or_id, ignore_missing=True): attempting to find a nonexistent region. :returns: One :class:`~openstack.identity.v3.region.Region` or None """ - return self._find(_region.Region, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _region.Region, name_or_id, ignore_missing=ignore_missing + ) def get_region(self, region): """Get a single region @@ -1017,8 +1025,9 @@ def update_role(self, role, **attrs): """ return self._update(_role.Role, role, **attrs) - def role_assignments_filter(self, domain=None, project=None, system=None, - group=None, user=None): + def role_assignments_filter( + self, domain=None, project=None, system=None, group=None, user=None + ): """Retrieve a generator of roles assigned to user/group :param domain: Either the ID of a domain or a @@ -1038,19 +1047,23 @@ def role_assignments_filter(self, domain=None, project=None, system=None, """ if domain and project and system: raise exception.InvalidRequest( - 'Only one of domain, project, or system can be specified') + 'Only one of domain, project, or system can be specified' + ) if domain is None and project is None and system is None: raise exception.InvalidRequest( - 'Either domain, project, or system should be specified') + 'Either domain, project, or system should be specified' + ) if group and user: raise exception.InvalidRequest( - 'Only one of group or user can be specified') + 'Only one of group or user can be specified' + ) if group is None and user is None: raise exception.InvalidRequest( - 'Either group or user should be specified') + 'Either group or user should be specified' + ) if domain: domain = self._get_resource(_domain.Domain, domain) @@ -1058,36 +1071,48 @@ def role_assignments_filter(self, domain=None, project=None, system=None, group = self._get_resource(_group.Group, group) return self._list( _role_domain_group_assignment.RoleDomainGroupAssignment, - domain_id=domain.id, group_id=group.id) + domain_id=domain.id, + group_id=group.id, + ) else: user = self._get_resource(_user.User, user) return self._list( _role_domain_user_assignment.RoleDomainUserAssignment, - domain_id=domain.id, user_id=user.id) + domain_id=domain.id, + user_id=user.id, + ) elif project: project = self._get_resource(_project.Project, project) if group: group = self._get_resource(_group.Group, group) return self._list( _role_project_group_assignment.RoleProjectGroupAssignment, - project_id=project.id, group_id=group.id) + project_id=project.id, + group_id=group.id, + ) else: user = self._get_resource(_user.User, user) return self._list( _role_project_user_assignment.RoleProjectUserAssignment, - project_id=project.id, user_id=user.id) + project_id=project.id, + user_id=user.id, + ) else: system = self._get_resource(_project.System, system) if group: group = self._get_resource(_group.Group, group) return self._list( _role_system_group_assignment.RoleSystemGroupAssignment, - system_id=system.id, group_id=group.id) + system_id=system.id, + group_id=group.id, + ) else: user = self._get_resource(_user.User, user) return self._list( _role_system_user_assignment.RoleSystemUserAssignment, - system_id=system.id, user_id=user.id) + system_id=system.id, + user_id=user.id, + ) def role_assignments(self, **query): """Retrieve a generator of role assignments @@ -1154,8 +1179,9 @@ def update_registered_limit(self, registered_limit, **attrs): :rtype: :class: `~openstack.identity.v3.registered_limit.RegisteredLimit` """ - return self._update(_registered_limit.RegisteredLimit, - registered_limit, **attrs) + return self._update( + _registered_limit.RegisteredLimit, registered_limit, **attrs + ) def delete_registered_limit(self, registered_limit, ignore_missing=True): """Delete a registered_limit @@ -1172,8 +1198,11 @@ def delete_registered_limit(self, registered_limit, ignore_missing=True): :returns: ``None`` """ - self._delete(_registered_limit.RegisteredLimit, registered_limit, - ignore_missing=ignore_missing) + self._delete( + _registered_limit.RegisteredLimit, + registered_limit, + ignore_missing=ignore_missing, + ) def limits(self, **query): """Retrieve a generator of limits @@ -1222,8 +1251,7 @@ def update_limit(self, limit, **attrs): :returns: The updated limit. :rtype: :class:`~openstack.identity.v3.limit.Limit` """ - return self._update(_limit.Limit, - limit, **attrs) + return self._update(_limit.Limit, limit, **attrs) def delete_limit(self, limit, ignore_missing=True): """Delete a limit @@ -1237,8 +1265,7 @@ def delete_limit(self, limit, ignore_missing=True): :returns: ``None`` """ - self._delete(limit.Limit, limit, - ignore_missing=ignore_missing) + self._delete(limit.Limit, limit, ignore_missing=ignore_missing) def assign_domain_role_to_user(self, domain, user, role): """Assign role to user on a domain @@ -1542,8 +1569,11 @@ def application_credentials(self, user, **query): :class:`~openstack.identity.v3.application_credential.ApplicationCredential` """ user = self._get_resource(_user.User, user) - return self._list(_application_credential.ApplicationCredential, - user_id=user.id, **query) + return self._list( + _application_credential.ApplicationCredential, + user_id=user.id, + **query, + ) def get_application_credential(self, user, application_credential): """Get a single application credential @@ -1562,9 +1592,11 @@ def get_application_credential(self, user, application_credential): resource can be found. """ user = self._get_resource(_user.User, user) - return self._get(_application_credential.ApplicationCredential, - application_credential, - user_id=user.id) + return self._get( + _application_credential.ApplicationCredential, + application_credential, + user_id=user.id, + ) def create_application_credential(self, user, name, **attrs): """Create a new application credential from attributes @@ -1584,9 +1616,12 @@ def create_application_credential(self, user, name, **attrs): """ user = self._get_resource(_user.User, user) - return self._create(_application_credential.ApplicationCredential, - name=name, - user_id=user.id, **attrs) + return self._create( + _application_credential.ApplicationCredential, + name=name, + user_id=user.id, + **attrs, + ) def find_application_credential( self, @@ -1619,8 +1654,9 @@ def find_application_credential( **query, ) - def delete_application_credential(self, user, application_credential, - ignore_missing=True): + def delete_application_credential( + self, user, application_credential, ignore_missing=True + ): """Delete an application credential :param user: Either the ID of a user or a @@ -1638,10 +1674,12 @@ def delete_application_credential(self, user, application_credential, :returns: ``None`` """ user = self._get_resource(_user.User, user) - self._delete(_application_credential.ApplicationCredential, - application_credential, - user_id=user.id, - ignore_missing=ignore_missing) + self._delete( + _application_credential.ApplicationCredential, + application_credential, + user_id=user.id, + ignore_missing=ignore_missing, + ) def create_federation_protocol(self, idp_id, **attrs): """Create a new federation protocol from attributes @@ -1663,11 +1701,13 @@ def create_federation_protocol(self, idp_id, **attrs): idp_cls = _identity_provider.IdentityProvider if isinstance(idp_id, idp_cls): idp_id = idp_id.id - return self._create(_federation_protocol.FederationProtocol, - idp_id=idp_id, **attrs) + return self._create( + _federation_protocol.FederationProtocol, idp_id=idp_id, **attrs + ) - def delete_federation_protocol(self, idp_id, protocol, - ignore_missing=True): + def delete_federation_protocol( + self, idp_id, protocol, ignore_missing=True + ): """Delete a federation protocol :param idp_id: The ID of the identity provider or a @@ -1693,8 +1733,9 @@ def delete_federation_protocol(self, idp_id, protocol, idp_cls = _identity_provider.IdentityProvider if isinstance(idp_id, idp_cls): idp_id = idp_id.id - self._delete(cls, protocol, - ignore_missing=ignore_missing, idp_id=idp_id) + self._delete( + cls, protocol, ignore_missing=ignore_missing, idp_id=idp_id + ) def find_federation_protocol(self, idp_id, protocol, ignore_missing=True): """Find a single federation protocol @@ -1714,8 +1755,12 @@ def find_federation_protocol(self, idp_id, protocol, ignore_missing=True): idp_cls = _identity_provider.IdentityProvider if isinstance(idp_id, idp_cls): idp_id = idp_id.id - return self._find(_federation_protocol.FederationProtocol, protocol, - ignore_missing=ignore_missing, idp_id=idp_id) + return self._find( + _federation_protocol.FederationProtocol, + protocol, + ignore_missing=ignore_missing, + idp_id=idp_id, + ) def get_federation_protocol(self, idp_id, protocol): """Get a single federation protocol @@ -1759,8 +1804,9 @@ def federation_protocols(self, idp_id, **query): idp_cls = _identity_provider.IdentityProvider if isinstance(idp_id, idp_cls): idp_id = idp_id.id - return self._list(_federation_protocol.FederationProtocol, - idp_id=idp_id, **query) + return self._list( + _federation_protocol.FederationProtocol, idp_id=idp_id, **query + ) def update_federation_protocol(self, idp_id, protocol, **attrs): """Update a federation protocol @@ -1827,8 +1873,9 @@ def find_mapping(self, name_or_id, ignore_missing=True): attempting to find a nonexistent resource. :returns: One :class:`~openstack.identity.v3.mapping.Mapping` or None """ - return self._find(_mapping.Mapping, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _mapping.Mapping, name_or_id, ignore_missing=ignore_missing + ) def get_mapping(self, mapping): """Get a single mapping @@ -1894,8 +1941,11 @@ def delete_identity_provider(self, identity_provider, ignore_missing=True): :returns: ``None`` """ - self._delete(_identity_provider.IdentityProvider, identity_provider, - ignore_missing=ignore_missing) + self._delete( + _identity_provider.IdentityProvider, + identity_provider, + ignore_missing=ignore_missing, + ) def find_identity_provider(self, name_or_id, ignore_missing=True): """Find a single identity provider @@ -1910,8 +1960,11 @@ def find_identity_provider(self, name_or_id, ignore_missing=True): :rtype: :class:`~openstack.identity.v3.identity_provider.IdentityProvider` """ - return self._find(_identity_provider.IdentityProvider, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _identity_provider.IdentityProvider, + name_or_id, + ignore_missing=ignore_missing, + ) def get_identity_provider(self, identity_provider): """Get a single mapping @@ -1926,8 +1979,9 @@ def get_identity_provider(self, identity_provider): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get(_identity_provider.IdentityProvider, - identity_provider) + return self._get( + _identity_provider.IdentityProvider, identity_provider + ) def identity_providers(self, **query): """Retrieve a generator of identity providers @@ -1954,5 +2008,6 @@ def update_identity_provider(self, identity_provider, **attrs): :rtype: :class:`~openstack.identity.v3.identity_provider.IdentityProvider` """ - return self._update(_identity_provider.IdentityProvider, - identity_provider, **attrs) + return self._update( + _identity_provider.IdentityProvider, identity_provider, **attrs + ) diff --git a/openstack/identity/v3/credential.py b/openstack/identity/v3/credential.py index c388dbf96..48a38b827 100644 --- a/openstack/identity/v3/credential.py +++ b/openstack/identity/v3/credential.py @@ -27,7 +27,8 @@ class Credential(resource.Resource): commit_method = 'PATCH' _query_mapping = resource.QueryParameters( - 'type', 'user_id', + 'type', + 'user_id', ) # Properties diff --git a/openstack/identity/v3/domain.py b/openstack/identity/v3/domain.py index bf842a176..832143195 100644 --- a/openstack/identity/v3/domain.py +++ b/openstack/identity/v3/domain.py @@ -50,54 +50,72 @@ class Domain(resource.Resource): def assign_role_to_user(self, session, user, role): """Assign role to user on domain""" - url = utils.urljoin(self.base_path, self.id, 'users', - user.id, 'roles', role.id) - resp = session.put(url,) + url = utils.urljoin( + self.base_path, self.id, 'users', user.id, 'roles', role.id + ) + resp = session.put( + url, + ) if resp.status_code == 204: return True return False def validate_user_has_role(self, session, user, role): """Validates that a user has a role on a domain""" - url = utils.urljoin(self.base_path, self.id, 'users', - user.id, 'roles', role.id) - resp = session.head(url,) + url = utils.urljoin( + self.base_path, self.id, 'users', user.id, 'roles', role.id + ) + resp = session.head( + url, + ) if resp.status_code == 204: return True return False def unassign_role_from_user(self, session, user, role): """Unassigns a role from a user on a domain""" - url = utils.urljoin(self.base_path, self.id, 'users', - user.id, 'roles', role.id) - resp = session.delete(url,) + url = utils.urljoin( + self.base_path, self.id, 'users', user.id, 'roles', role.id + ) + resp = session.delete( + url, + ) if resp.status_code == 204: return True return False def assign_role_to_group(self, session, group, role): """Assign role to group on domain""" - url = utils.urljoin(self.base_path, self.id, 'groups', - group.id, 'roles', role.id) - resp = session.put(url,) + url = utils.urljoin( + self.base_path, self.id, 'groups', group.id, 'roles', role.id + ) + resp = session.put( + url, + ) if resp.status_code == 204: return True return False def validate_group_has_role(self, session, group, role): """Validates that a group has a role on a domain""" - url = utils.urljoin(self.base_path, self.id, 'groups', - group.id, 'roles', role.id) - resp = session.head(url,) + url = utils.urljoin( + self.base_path, self.id, 'groups', group.id, 'roles', role.id + ) + resp = session.head( + url, + ) if resp.status_code == 204: return True return False def unassign_role_from_group(self, session, group, role): """Unassigns a role from a group on a domain""" - url = utils.urljoin(self.base_path, self.id, 'groups', - group.id, 'roles', role.id) - resp = session.delete(url,) + url = utils.urljoin( + self.base_path, self.id, 'groups', group.id, 'roles', role.id + ) + resp = session.delete( + url, + ) if resp.status_code == 204: return True return False diff --git a/openstack/identity/v3/endpoint.py b/openstack/identity/v3/endpoint.py index 59298aeb0..bc58135c2 100644 --- a/openstack/identity/v3/endpoint.py +++ b/openstack/identity/v3/endpoint.py @@ -27,7 +27,9 @@ class Endpoint(resource.Resource): commit_method = 'PATCH' _query_mapping = resource.QueryParameters( - 'interface', 'region_id', 'service_id', + 'interface', + 'region_id', + 'service_id', ) # Properties diff --git a/openstack/identity/v3/group.py b/openstack/identity/v3/group.py index 41d876356..c1c11d9a7 100644 --- a/openstack/identity/v3/group.py +++ b/openstack/identity/v3/group.py @@ -29,7 +29,8 @@ class Group(resource.Resource): commit_method = 'PATCH' _query_mapping = resource.QueryParameters( - 'domain_id', 'name', + 'domain_id', + 'name', ) # Properties @@ -45,23 +46,26 @@ class Group(resource.Resource): def add_user(self, session, user): """Add user to the group""" - url = utils.urljoin( - self.base_path, self.id, 'users', user.id) - resp = session.put(url,) + url = utils.urljoin(self.base_path, self.id, 'users', user.id) + resp = session.put( + url, + ) exceptions.raise_from_response(resp) def remove_user(self, session, user): """Remove user from the group""" - url = utils.urljoin( - self.base_path, self.id, 'users', user.id) - resp = session.delete(url,) + url = utils.urljoin(self.base_path, self.id, 'users', user.id) + resp = session.delete( + url, + ) exceptions.raise_from_response(resp) def check_user(self, session, user): """Check whether user belongs to group""" - url = utils.urljoin( - self.base_path, self.id, 'users', user.id) - resp = session.head(url,) + url = utils.urljoin(self.base_path, self.id, 'users', user.id) + resp = session.head( + url, + ) if resp.status_code == 404: # If we recieve 404 - treat this as False, # rather then returning exception diff --git a/openstack/identity/v3/limit.py b/openstack/identity/v3/limit.py index 280e6ca80..7918024a7 100644 --- a/openstack/identity/v3/limit.py +++ b/openstack/identity/v3/limit.py @@ -28,7 +28,8 @@ class Limit(resource.Resource): commit_jsonpatch = True _query_mapping = resource.QueryParameters( - 'service_id', 'region_id', 'resource_name', 'project_id') + 'service_id', 'region_id', 'resource_name', 'project_id' + ) # Properties #: User-facing description of the registered_limit. *Type: string* diff --git a/openstack/identity/v3/project.py b/openstack/identity/v3/project.py index f90664a9e..7b08e3e53 100644 --- a/openstack/identity/v3/project.py +++ b/openstack/identity/v3/project.py @@ -64,54 +64,72 @@ class Project(resource.Resource, tag.TagMixin): def assign_role_to_user(self, session, user, role): """Assign role to user on project""" - url = utils.urljoin(self.base_path, self.id, 'users', - user.id, 'roles', role.id) - resp = session.put(url,) + url = utils.urljoin( + self.base_path, self.id, 'users', user.id, 'roles', role.id + ) + resp = session.put( + url, + ) if resp.status_code == 204: return True return False def validate_user_has_role(self, session, user, role): """Validates that a user has a role on a project""" - url = utils.urljoin(self.base_path, self.id, 'users', - user.id, 'roles', role.id) - resp = session.head(url,) + url = utils.urljoin( + self.base_path, self.id, 'users', user.id, 'roles', role.id + ) + resp = session.head( + url, + ) if resp.status_code == 204: return True return False def unassign_role_from_user(self, session, user, role): """Unassigns a role from a user on a project""" - url = utils.urljoin(self.base_path, self.id, 'users', - user.id, 'roles', role.id) - resp = session.delete(url,) + url = utils.urljoin( + self.base_path, self.id, 'users', user.id, 'roles', role.id + ) + resp = session.delete( + url, + ) if resp.status_code == 204: return True return False def assign_role_to_group(self, session, group, role): """Assign role to group on project""" - url = utils.urljoin(self.base_path, self.id, 'groups', - group.id, 'roles', role.id) - resp = session.put(url,) + url = utils.urljoin( + self.base_path, self.id, 'groups', group.id, 'roles', role.id + ) + resp = session.put( + url, + ) if resp.status_code == 204: return True return False def validate_group_has_role(self, session, group, role): """Validates that a group has a role on a project""" - url = utils.urljoin(self.base_path, self.id, 'groups', - group.id, 'roles', role.id) - resp = session.head(url,) + url = utils.urljoin( + self.base_path, self.id, 'groups', group.id, 'roles', role.id + ) + resp = session.head( + url, + ) if resp.status_code == 204: return True return False def unassign_role_from_group(self, session, group, role): """Unassigns a role from a group on a project""" - url = utils.urljoin(self.base_path, self.id, 'groups', - group.id, 'roles', role.id) - resp = session.delete(url,) + url = utils.urljoin( + self.base_path, self.id, 'groups', group.id, 'roles', role.id + ) + resp = session.delete( + url, + ) if resp.status_code == 204: return True return False diff --git a/openstack/identity/v3/registered_limit.py b/openstack/identity/v3/registered_limit.py index 227f212c2..7e8b67d66 100644 --- a/openstack/identity/v3/registered_limit.py +++ b/openstack/identity/v3/registered_limit.py @@ -28,7 +28,8 @@ class RegisteredLimit(resource.Resource): commit_jsonpatch = True _query_mapping = resource.QueryParameters( - 'service_id', 'region_id', 'resource_name') + 'service_id', 'region_id', 'resource_name' + ) # Properties #: User-facing description of the registered_limit. *Type: string* diff --git a/openstack/identity/v3/role.py b/openstack/identity/v3/role.py index 76bda233e..ddca5cd05 100644 --- a/openstack/identity/v3/role.py +++ b/openstack/identity/v3/role.py @@ -26,8 +26,7 @@ class Role(resource.Resource): allow_list = True commit_method = 'PATCH' - _query_mapping = resource.QueryParameters( - 'name', 'domain_id') + _query_mapping = resource.QueryParameters('name', 'domain_id') # Properties #: Unique role name, within the owning domain. *Type: string* diff --git a/openstack/identity/v3/role_assignment.py b/openstack/identity/v3/role_assignment.py index a12cce27a..e47dec027 100644 --- a/openstack/identity/v3/role_assignment.py +++ b/openstack/identity/v3/role_assignment.py @@ -22,10 +22,19 @@ class RoleAssignment(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'group_id', 'role_id', 'scope_domain_id', 'scope_project_id', - 'user_id', 'effective', 'include_names', 'include_subtree', - role_id='role.id', user_id='user.id', group_id='group.id', - scope_project_id='scope.project.id', scope_domain_id='scope.domain.id', + 'group_id', + 'role_id', + 'scope_domain_id', + 'scope_project_id', + 'user_id', + 'effective', + 'include_names', + 'include_subtree', + role_id='role.id', + user_id='user.id', + group_id='group.id', + scope_project_id='scope.project.id', + scope_domain_id='scope.domain.id', scope_system='scope.system', ) diff --git a/openstack/identity/v3/system.py b/openstack/identity/v3/system.py index 9ceea3f76..70652202b 100644 --- a/openstack/identity/v3/system.py +++ b/openstack/identity/v3/system.py @@ -22,54 +22,66 @@ class System(resource.Resource): def assign_role_to_user(self, session, user, role): """Assign role to user on system""" - url = utils.urljoin(self.base_path, 'users', user.id, - 'roles', role.id) - resp = session.put(url,) + url = utils.urljoin(self.base_path, 'users', user.id, 'roles', role.id) + resp = session.put( + url, + ) if resp.status_code == 204: return True return False def validate_user_has_role(self, session, user, role): """Validates that a user has a role on a system""" - url = utils.urljoin(self.base_path, 'users', user.id, - 'roles', role.id) - resp = session.head(url,) + url = utils.urljoin(self.base_path, 'users', user.id, 'roles', role.id) + resp = session.head( + url, + ) if resp.status_code == 204: return True return False def unassign_role_from_user(self, session, user, role): """Unassigns a role from a user on a system""" - url = utils.urljoin(self.base_path, 'users', user.id, - 'roles', role.id) - resp = session.delete(url,) + url = utils.urljoin(self.base_path, 'users', user.id, 'roles', role.id) + resp = session.delete( + url, + ) if resp.status_code == 204: return True return False def assign_role_to_group(self, session, group, role): """Assign role to group on system""" - url = utils.urljoin(self.base_path, 'groups', group.id, - 'roles', role.id) - resp = session.put(url,) + url = utils.urljoin( + self.base_path, 'groups', group.id, 'roles', role.id + ) + resp = session.put( + url, + ) if resp.status_code == 204: return True return False def validate_group_has_role(self, session, group, role): """Validates that a group has a role on a system""" - url = utils.urljoin(self.base_path, 'groups', group.id, - 'roles', role.id) - resp = session.head(url,) + url = utils.urljoin( + self.base_path, 'groups', group.id, 'roles', role.id + ) + resp = session.head( + url, + ) if resp.status_code == 204: return True return False def unassign_role_from_group(self, session, group, role): """Unassigns a role from a group on a system""" - url = utils.urljoin(self.base_path, 'groups', group.id, - 'roles', role.id) - resp = session.delete(url,) + url = utils.urljoin( + self.base_path, 'groups', group.id, 'roles', role.id + ) + resp = session.delete( + url, + ) if resp.status_code == 204: return True return False diff --git a/openstack/identity/v3/trust.py b/openstack/identity/v3/trust.py index dfc5f8279..d8f734751 100644 --- a/openstack/identity/v3/trust.py +++ b/openstack/identity/v3/trust.py @@ -26,7 +26,8 @@ class Trust(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'trustor_user_id', 'trustee_user_id') + 'trustor_user_id', 'trustee_user_id' + ) # Properties #: A boolean indicating whether the trust can be issued by the trustee as diff --git a/openstack/identity/version.py b/openstack/identity/version.py index ea1575f81..ee329edc6 100644 --- a/openstack/identity/version.py +++ b/openstack/identity/version.py @@ -32,8 +32,7 @@ def list(cls, session, paginated=False, base_path=None, **params): if base_path is None: base_path = cls.base_path - resp = session.get(base_path, - params=params) + resp = session.get(base_path, params=params) resp = resp.json() for data in resp[cls.resources_key]['values']: yield cls.existing(**data) diff --git a/openstack/tests/functional/identity/v3/test_application_credential.py b/openstack/tests/functional/identity/v3/test_application_credential.py index d9cef028d..cce6670bf 100644 --- a/openstack/tests/functional/identity/v3/test_application_credential.py +++ b/openstack/tests/functional/identity/v3/test_application_credential.py @@ -15,7 +15,6 @@ class TestApplicationCredentials(base.BaseFunctionalTest): - def setUp(self): super(TestApplicationCredentials, self).setUp() self.user_id = self.operator_cloud.current_user_id @@ -24,8 +23,11 @@ def _create_application_credentials(self): app_creds = self.conn.identity.create_application_credential( user=self.user_id, name='app_cred' ) - self.addCleanup(self.conn.identity.delete_application_credential, - self.user_id, app_creds['id']) + self.addCleanup( + self.conn.identity.delete_application_credential, + self.user_id, + app_creds['id'], + ) return app_creds def test_create_application_credentials(self): @@ -61,8 +63,9 @@ def test_delete_application_credential(self): self.conn.identity.delete_application_credential( user=self.user_id, application_credential=app_creds['id'] ) - self.assertRaises(exceptions.NotFoundException, - self.conn.identity.get_application_credential, - user=self.user_id, - application_credential=app_creds['id'] - ) + self.assertRaises( + exceptions.NotFoundException, + self.conn.identity.get_application_credential, + user=self.user_id, + application_credential=app_creds['id'], + ) diff --git a/openstack/tests/unit/identity/test_version.py b/openstack/tests/unit/identity/test_version.py index be9728f0b..8ec4a52fd 100644 --- a/openstack/tests/unit/identity/test_version.py +++ b/openstack/tests/unit/identity/test_version.py @@ -25,7 +25,6 @@ class TestVersion(base.TestCase): - def test_basic(self): sot = version.Version() self.assertEqual('version', sot.resource_key) diff --git a/openstack/tests/unit/identity/v2/test_extension.py b/openstack/tests/unit/identity/v2/test_extension.py index 1d0bd9da5..912e79b59 100644 --- a/openstack/tests/unit/identity/v2/test_extension.py +++ b/openstack/tests/unit/identity/v2/test_extension.py @@ -27,7 +27,6 @@ class TestExtension(base.TestCase): - def test_basic(self): sot = extension.Extension() self.assertEqual('extension', sot.resource_key) diff --git a/openstack/tests/unit/identity/v2/test_role.py b/openstack/tests/unit/identity/v2/test_role.py index c14ceaf35..cc19192bb 100644 --- a/openstack/tests/unit/identity/v2/test_role.py +++ b/openstack/tests/unit/identity/v2/test_role.py @@ -24,7 +24,6 @@ class TestRole(base.TestCase): - def test_basic(self): sot = role.Role() self.assertEqual('role', sot.resource_key) diff --git a/openstack/tests/unit/identity/v2/test_tenant.py b/openstack/tests/unit/identity/v2/test_tenant.py index 5145cd802..e3fe61c29 100644 --- a/openstack/tests/unit/identity/v2/test_tenant.py +++ b/openstack/tests/unit/identity/v2/test_tenant.py @@ -24,7 +24,6 @@ class TestTenant(base.TestCase): - def test_basic(self): sot = tenant.Tenant() self.assertEqual('tenant', sot.resource_key) diff --git a/openstack/tests/unit/identity/v2/test_user.py b/openstack/tests/unit/identity/v2/test_user.py index c99616b71..63b069b9e 100644 --- a/openstack/tests/unit/identity/v2/test_user.py +++ b/openstack/tests/unit/identity/v2/test_user.py @@ -24,7 +24,6 @@ class TestUser(base.TestCase): - def test_basic(self): sot = user.User() self.assertEqual('user', sot.resource_key) diff --git a/openstack/tests/unit/identity/v3/test_application_credential.py b/openstack/tests/unit/identity/v3/test_application_credential.py index f11b4604b..231759331 100644 --- a/openstack/tests/unit/identity/v3/test_application_credential.py +++ b/openstack/tests/unit/identity/v3/test_application_credential.py @@ -15,29 +15,26 @@ EXAMPLE = { - "user": { - "id": "8ac43bb0926245cead88676a96c750d3"}, + "user": {"id": "8ac43bb0926245cead88676a96c750d3"}, "name": 'monitoring', "secret": 'rEaqvJka48mpv', - "roles": [ - {"name": "Reader"} - ], + "roles": [{"name": "Reader"}], "expires_at": '2018-02-27T18:30:59Z', "description": "Application credential for monitoring", "unrestricted": "False", "project_id": "3", - "links": {"self": "http://example.com/v3/application_credential_1"} + "links": {"self": "http://example.com/v3/application_credential_1"}, } class TestApplicationCredential(base.TestCase): - def test_basic(self): sot = application_credential.ApplicationCredential() self.assertEqual('application_credential', sot.resource_key) self.assertEqual('application_credentials', sot.resources_key) - self.assertEqual('/users/%(user_id)s/application_credentials', - sot.base_path) + self.assertEqual( + '/users/%(user_id)s/application_credentials', sot.base_path + ) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) diff --git a/openstack/tests/unit/identity/v3/test_credential.py b/openstack/tests/unit/identity/v3/test_credential.py index ed77e1ca7..06ad8ca70 100644 --- a/openstack/tests/unit/identity/v3/test_credential.py +++ b/openstack/tests/unit/identity/v3/test_credential.py @@ -25,7 +25,6 @@ class TestCredential(base.TestCase): - def test_basic(self): sot = credential.Credential() self.assertEqual('credential', sot.resource_key) @@ -45,7 +44,8 @@ def test_basic(self): 'limit': 'limit', 'marker': 'marker', }, - sot._query_mapping._mapping) + sot._query_mapping._mapping, + ) def test_make_it(self): sot = credential.Credential(**EXAMPLE) diff --git a/openstack/tests/unit/identity/v3/test_domain.py b/openstack/tests/unit/identity/v3/test_domain.py index 8a4df7dec..bc8c5b100 100644 --- a/openstack/tests/unit/identity/v3/test_domain.py +++ b/openstack/tests/unit/identity/v3/test_domain.py @@ -32,7 +32,6 @@ class TestDomain(base.TestCase): - def setUp(self): super(TestDomain, self).setUp() self.sess = mock.Mock(spec=adapter.Adapter) @@ -67,7 +66,8 @@ def test_basic(self): 'limit': 'limit', 'marker': 'marker', }, - sot._query_mapping._mapping) + sot._query_mapping._mapping, + ) def test_make_it(self): sot = domain.Domain(**EXAMPLE) @@ -84,12 +84,11 @@ def test_assign_role_to_user_good(self): self.assertTrue( sot.assign_role_to_user( - self.sess, - user.User(id='1'), - role.Role(id='2'))) + self.sess, user.User(id='1'), role.Role(id='2') + ) + ) - self.sess.put.assert_called_with( - 'domains/IDENTIFIER/users/1/roles/2') + self.sess.put.assert_called_with('domains/IDENTIFIER/users/1/roles/2') def test_assign_role_to_user_bad(self): sot = domain.Domain(**EXAMPLE) @@ -98,9 +97,9 @@ def test_assign_role_to_user_bad(self): self.assertFalse( sot.assign_role_to_user( - self.sess, - user.User(id='1'), - role.Role(id='2'))) + self.sess, user.User(id='1'), role.Role(id='2') + ) + ) def test_validate_user_has_role_good(self): sot = domain.Domain(**EXAMPLE) @@ -109,12 +108,11 @@ def test_validate_user_has_role_good(self): self.assertTrue( sot.validate_user_has_role( - self.sess, - user.User(id='1'), - role.Role(id='2'))) + self.sess, user.User(id='1'), role.Role(id='2') + ) + ) - self.sess.head.assert_called_with( - 'domains/IDENTIFIER/users/1/roles/2') + self.sess.head.assert_called_with('domains/IDENTIFIER/users/1/roles/2') def test_validate_user_has_role_bad(self): sot = domain.Domain(**EXAMPLE) @@ -123,9 +121,9 @@ def test_validate_user_has_role_bad(self): self.assertFalse( sot.validate_user_has_role( - self.sess, - user.User(id='1'), - role.Role(id='2'))) + self.sess, user.User(id='1'), role.Role(id='2') + ) + ) def test_unassign_role_from_user_good(self): sot = domain.Domain(**EXAMPLE) @@ -134,12 +132,13 @@ def test_unassign_role_from_user_good(self): self.assertTrue( sot.unassign_role_from_user( - self.sess, - user.User(id='1'), - role.Role(id='2'))) + self.sess, user.User(id='1'), role.Role(id='2') + ) + ) self.sess.delete.assert_called_with( - 'domains/IDENTIFIER/users/1/roles/2') + 'domains/IDENTIFIER/users/1/roles/2' + ) def test_unassign_role_from_user_bad(self): sot = domain.Domain(**EXAMPLE) @@ -148,9 +147,9 @@ def test_unassign_role_from_user_bad(self): self.assertFalse( sot.unassign_role_from_user( - self.sess, - user.User(id='1'), - role.Role(id='2'))) + self.sess, user.User(id='1'), role.Role(id='2') + ) + ) def test_assign_role_to_group_good(self): sot = domain.Domain(**EXAMPLE) @@ -159,12 +158,11 @@ def test_assign_role_to_group_good(self): self.assertTrue( sot.assign_role_to_group( - self.sess, - group.Group(id='1'), - role.Role(id='2'))) + self.sess, group.Group(id='1'), role.Role(id='2') + ) + ) - self.sess.put.assert_called_with( - 'domains/IDENTIFIER/groups/1/roles/2') + self.sess.put.assert_called_with('domains/IDENTIFIER/groups/1/roles/2') def test_assign_role_to_group_bad(self): sot = domain.Domain(**EXAMPLE) @@ -173,9 +171,9 @@ def test_assign_role_to_group_bad(self): self.assertFalse( sot.assign_role_to_group( - self.sess, - group.Group(id='1'), - role.Role(id='2'))) + self.sess, group.Group(id='1'), role.Role(id='2') + ) + ) def test_validate_group_has_role_good(self): sot = domain.Domain(**EXAMPLE) @@ -184,12 +182,13 @@ def test_validate_group_has_role_good(self): self.assertTrue( sot.validate_group_has_role( - self.sess, - group.Group(id='1'), - role.Role(id='2'))) + self.sess, group.Group(id='1'), role.Role(id='2') + ) + ) self.sess.head.assert_called_with( - 'domains/IDENTIFIER/groups/1/roles/2') + 'domains/IDENTIFIER/groups/1/roles/2' + ) def test_validate_group_has_role_bad(self): sot = domain.Domain(**EXAMPLE) @@ -198,9 +197,9 @@ def test_validate_group_has_role_bad(self): self.assertFalse( sot.validate_group_has_role( - self.sess, - group.Group(id='1'), - role.Role(id='2'))) + self.sess, group.Group(id='1'), role.Role(id='2') + ) + ) def test_unassign_role_from_group_good(self): sot = domain.Domain(**EXAMPLE) @@ -209,12 +208,13 @@ def test_unassign_role_from_group_good(self): self.assertTrue( sot.unassign_role_from_group( - self.sess, - group.Group(id='1'), - role.Role(id='2'))) + self.sess, group.Group(id='1'), role.Role(id='2') + ) + ) self.sess.delete.assert_called_with( - 'domains/IDENTIFIER/groups/1/roles/2') + 'domains/IDENTIFIER/groups/1/roles/2' + ) def test_unassign_role_from_group_bad(self): sot = domain.Domain(**EXAMPLE) @@ -223,6 +223,6 @@ def test_unassign_role_from_group_bad(self): self.assertFalse( sot.unassign_role_from_group( - self.sess, - group.Group(id='1'), - role.Role(id='2'))) + self.sess, group.Group(id='1'), role.Role(id='2') + ) + ) diff --git a/openstack/tests/unit/identity/v3/test_endpoint.py b/openstack/tests/unit/identity/v3/test_endpoint.py index b7c538e66..8ee404d21 100644 --- a/openstack/tests/unit/identity/v3/test_endpoint.py +++ b/openstack/tests/unit/identity/v3/test_endpoint.py @@ -27,7 +27,6 @@ class TestEndpoint(base.TestCase): - def test_basic(self): sot = endpoint.Endpoint() self.assertEqual('endpoint', sot.resource_key) @@ -47,7 +46,8 @@ def test_basic(self): 'limit': 'limit', 'marker': 'marker', }, - sot._query_mapping._mapping) + sot._query_mapping._mapping, + ) def test_make_it(self): sot = endpoint.Endpoint(**EXAMPLE) diff --git a/openstack/tests/unit/identity/v3/test_federation_protocol.py b/openstack/tests/unit/identity/v3/test_federation_protocol.py index b8ec024b1..4aee07cbb 100644 --- a/openstack/tests/unit/identity/v3/test_federation_protocol.py +++ b/openstack/tests/unit/identity/v3/test_federation_protocol.py @@ -23,14 +23,14 @@ class TestFederationProtocol(base.TestCase): - def test_basic(self): sot = federation_protocol.FederationProtocol() self.assertEqual('protocol', sot.resource_key) self.assertEqual('protocols', sot.resources_key) self.assertEqual( '/OS-FEDERATION/identity_providers/%(idp_id)s/protocols', - sot.base_path) + sot.base_path, + ) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_commit) @@ -46,7 +46,8 @@ def test_basic(self): 'limit': 'limit', 'marker': 'marker', }, - sot._query_mapping._mapping) + sot._query_mapping._mapping, + ) def test_make_it(self): sot = federation_protocol.FederationProtocol(**EXAMPLE) diff --git a/openstack/tests/unit/identity/v3/test_group.py b/openstack/tests/unit/identity/v3/test_group.py index 46b17ae58..3f8d4e344 100644 --- a/openstack/tests/unit/identity/v3/test_group.py +++ b/openstack/tests/unit/identity/v3/test_group.py @@ -29,7 +29,6 @@ class TestGroup(base.TestCase): - def setUp(self): super(TestGroup, self).setUp() self.sess = mock.Mock(spec=adapter.Adapter) @@ -59,7 +58,8 @@ def test_basic(self): 'limit': 'limit', 'marker': 'marker', }, - sot._query_mapping._mapping) + sot._query_mapping._mapping, + ) def test_make_it(self): sot = group.Group(**EXAMPLE) @@ -73,32 +73,24 @@ def test_add_user(self): resp = self.good_resp self.sess.put = mock.Mock(return_value=resp) - sot.add_user( - self.sess, user.User(id='1')) + sot.add_user(self.sess, user.User(id='1')) - self.sess.put.assert_called_with( - 'groups/IDENTIFIER/users/1') + self.sess.put.assert_called_with('groups/IDENTIFIER/users/1') def test_remove_user(self): sot = group.Group(**EXAMPLE) resp = self.good_resp self.sess.delete = mock.Mock(return_value=resp) - sot.remove_user( - self.sess, user.User(id='1')) + sot.remove_user(self.sess, user.User(id='1')) - self.sess.delete.assert_called_with( - 'groups/IDENTIFIER/users/1') + self.sess.delete.assert_called_with('groups/IDENTIFIER/users/1') def test_check_user(self): sot = group.Group(**EXAMPLE) resp = self.good_resp self.sess.head = mock.Mock(return_value=resp) - self.assertTrue( - sot.check_user( - self.sess, - user.User(id='1'))) + self.assertTrue(sot.check_user(self.sess, user.User(id='1'))) - self.sess.head.assert_called_with( - 'groups/IDENTIFIER/users/1') + self.sess.head.assert_called_with('groups/IDENTIFIER/users/1') diff --git a/openstack/tests/unit/identity/v3/test_identity_provider.py b/openstack/tests/unit/identity/v3/test_identity_provider.py index a733bdd46..c11f4e07e 100644 --- a/openstack/tests/unit/identity/v3/test_identity_provider.py +++ b/openstack/tests/unit/identity/v3/test_identity_provider.py @@ -25,7 +25,6 @@ class TestIdentityProvider(base.TestCase): - def test_basic(self): sot = identity_provider.IdentityProvider() self.assertEqual('identity_provider', sot.resource_key) @@ -47,7 +46,8 @@ def test_basic(self): 'marker': 'marker', 'is_enabled': 'enabled', }, - sot._query_mapping._mapping) + sot._query_mapping._mapping, + ) def test_make_it(self): sot = identity_provider.IdentityProvider(**EXAMPLE) diff --git a/openstack/tests/unit/identity/v3/test_limit.py b/openstack/tests/unit/identity/v3/test_limit.py index 6d2bcf413..7e4dc346e 100644 --- a/openstack/tests/unit/identity/v3/test_limit.py +++ b/openstack/tests/unit/identity/v3/test_limit.py @@ -21,12 +21,11 @@ "resource_limit": 10, "project_id": 'a8455cdd4249498f99b63d5af2fb4bc8', "description": "compute cores for project 123", - "links": {"self": "http://example.com/v3/limit_1"} + "links": {"self": "http://example.com/v3/limit_1"}, } class TestLimit(base.TestCase): - def test_basic(self): sot = limit.Limit() self.assertEqual('limits', sot.resources_key) @@ -45,9 +44,10 @@ def test_basic(self): 'resource_name': 'resource_name', 'project_id': 'project_id', 'marker': 'marker', - 'limit': 'limit' + 'limit': 'limit', }, - sot._query_mapping._mapping) + sot._query_mapping._mapping, + ) def test_make_it(self): sot = limit.Limit(**EXAMPLE) diff --git a/openstack/tests/unit/identity/v3/test_mapping.py b/openstack/tests/unit/identity/v3/test_mapping.py index 9b67d618f..dc32fed93 100644 --- a/openstack/tests/unit/identity/v3/test_mapping.py +++ b/openstack/tests/unit/identity/v3/test_mapping.py @@ -22,7 +22,6 @@ class TestMapping(base.TestCase): - def test_basic(self): sot = mapping.Mapping() self.assertEqual('mapping', sot.resource_key) @@ -41,7 +40,8 @@ def test_basic(self): 'limit': 'limit', 'marker': 'marker', }, - sot._query_mapping._mapping) + sot._query_mapping._mapping, + ) def test_make_it(self): sot = mapping.Mapping(**EXAMPLE) diff --git a/openstack/tests/unit/identity/v3/test_policy.py b/openstack/tests/unit/identity/v3/test_policy.py index 4d5ec6317..cb06f40ac 100644 --- a/openstack/tests/unit/identity/v3/test_policy.py +++ b/openstack/tests/unit/identity/v3/test_policy.py @@ -26,7 +26,6 @@ class TestPolicy(base.TestCase): - def test_basic(self): sot = policy.Policy() self.assertEqual('policy', sot.resource_key) diff --git a/openstack/tests/unit/identity/v3/test_project.py b/openstack/tests/unit/identity/v3/test_project.py index d5919f24e..23577f638 100644 --- a/openstack/tests/unit/identity/v3/test_project.py +++ b/openstack/tests/unit/identity/v3/test_project.py @@ -30,14 +30,11 @@ 'is_domain': False, 'name': '5', 'parent_id': '6', - 'options': { - 'foo': 'bar' - } + 'options': {'foo': 'bar'}, } class TestProject(base.TestCase): - def setUp(self): super(TestProject, self).setUp() self.sess = mock.Mock(spec=adapter.Adapter) @@ -79,7 +76,8 @@ def test_basic(self): 'not_tags': 'not-tags', 'not_any_tags': 'not-tags-any', }, - sot._query_mapping._mapping) + sot._query_mapping._mapping, + ) def test_make_it(self): sot = project.Project(**EXAMPLE) @@ -99,12 +97,11 @@ def test_assign_role_to_user_good(self): self.assertTrue( sot.assign_role_to_user( - self.sess, - user.User(id='1'), - role.Role(id='2'))) + self.sess, user.User(id='1'), role.Role(id='2') + ) + ) - self.sess.put.assert_called_with( - 'projects/IDENTIFIER/users/1/roles/2') + self.sess.put.assert_called_with('projects/IDENTIFIER/users/1/roles/2') def test_assign_role_to_user_bad(self): sot = project.Project(**EXAMPLE) @@ -113,9 +110,9 @@ def test_assign_role_to_user_bad(self): self.assertFalse( sot.assign_role_to_user( - self.sess, - user.User(id='1'), - role.Role(id='2'))) + self.sess, user.User(id='1'), role.Role(id='2') + ) + ) def test_validate_user_has_role_good(self): sot = project.Project(**EXAMPLE) @@ -124,12 +121,13 @@ def test_validate_user_has_role_good(self): self.assertTrue( sot.validate_user_has_role( - self.sess, - user.User(id='1'), - role.Role(id='2'))) + self.sess, user.User(id='1'), role.Role(id='2') + ) + ) self.sess.head.assert_called_with( - 'projects/IDENTIFIER/users/1/roles/2') + 'projects/IDENTIFIER/users/1/roles/2' + ) def test_validate_user_has_role_bad(self): sot = project.Project(**EXAMPLE) @@ -138,9 +136,9 @@ def test_validate_user_has_role_bad(self): self.assertFalse( sot.validate_user_has_role( - self.sess, - user.User(id='1'), - role.Role(id='2'))) + self.sess, user.User(id='1'), role.Role(id='2') + ) + ) def test_unassign_role_from_user_good(self): sot = project.Project(**EXAMPLE) @@ -149,12 +147,13 @@ def test_unassign_role_from_user_good(self): self.assertTrue( sot.unassign_role_from_user( - self.sess, - user.User(id='1'), - role.Role(id='2'))) + self.sess, user.User(id='1'), role.Role(id='2') + ) + ) self.sess.delete.assert_called_with( - 'projects/IDENTIFIER/users/1/roles/2') + 'projects/IDENTIFIER/users/1/roles/2' + ) def test_unassign_role_from_user_bad(self): sot = project.Project(**EXAMPLE) @@ -163,9 +162,9 @@ def test_unassign_role_from_user_bad(self): self.assertFalse( sot.unassign_role_from_user( - self.sess, - user.User(id='1'), - role.Role(id='2'))) + self.sess, user.User(id='1'), role.Role(id='2') + ) + ) def test_assign_role_to_group_good(self): sot = project.Project(**EXAMPLE) @@ -174,12 +173,13 @@ def test_assign_role_to_group_good(self): self.assertTrue( sot.assign_role_to_group( - self.sess, - group.Group(id='1'), - role.Role(id='2'))) + self.sess, group.Group(id='1'), role.Role(id='2') + ) + ) self.sess.put.assert_called_with( - 'projects/IDENTIFIER/groups/1/roles/2') + 'projects/IDENTIFIER/groups/1/roles/2' + ) def test_assign_role_to_group_bad(self): sot = project.Project(**EXAMPLE) @@ -188,9 +188,9 @@ def test_assign_role_to_group_bad(self): self.assertFalse( sot.assign_role_to_group( - self.sess, - group.Group(id='1'), - role.Role(id='2'))) + self.sess, group.Group(id='1'), role.Role(id='2') + ) + ) def test_validate_group_has_role_good(self): sot = project.Project(**EXAMPLE) @@ -199,12 +199,13 @@ def test_validate_group_has_role_good(self): self.assertTrue( sot.validate_group_has_role( - self.sess, - group.Group(id='1'), - role.Role(id='2'))) + self.sess, group.Group(id='1'), role.Role(id='2') + ) + ) self.sess.head.assert_called_with( - 'projects/IDENTIFIER/groups/1/roles/2') + 'projects/IDENTIFIER/groups/1/roles/2' + ) def test_validate_group_has_role_bad(self): sot = project.Project(**EXAMPLE) @@ -213,9 +214,9 @@ def test_validate_group_has_role_bad(self): self.assertFalse( sot.validate_group_has_role( - self.sess, - group.Group(id='1'), - role.Role(id='2'))) + self.sess, group.Group(id='1'), role.Role(id='2') + ) + ) def test_unassign_role_from_group_good(self): sot = project.Project(**EXAMPLE) @@ -224,12 +225,13 @@ def test_unassign_role_from_group_good(self): self.assertTrue( sot.unassign_role_from_group( - self.sess, - group.Group(id='1'), - role.Role(id='2'))) + self.sess, group.Group(id='1'), role.Role(id='2') + ) + ) self.sess.delete.assert_called_with( - 'projects/IDENTIFIER/groups/1/roles/2') + 'projects/IDENTIFIER/groups/1/roles/2' + ) def test_unassign_role_from_group_bad(self): sot = project.Project(**EXAMPLE) @@ -238,13 +240,12 @@ def test_unassign_role_from_group_bad(self): self.assertFalse( sot.unassign_role_from_group( - self.sess, - group.Group(id='1'), - role.Role(id='2'))) + self.sess, group.Group(id='1'), role.Role(id='2') + ) + ) class TestUserProject(base.TestCase): - def test_basic(self): sot = project.UserProject() self.assertEqual('project', sot.resource_key) diff --git a/openstack/tests/unit/identity/v3/test_proxy.py b/openstack/tests/unit/identity/v3/test_proxy.py index eb9eb981a..4c40dff37 100644 --- a/openstack/tests/unit/identity/v3/test_proxy.py +++ b/openstack/tests/unit/identity/v3/test_proxy.py @@ -36,18 +36,18 @@ def setUp(self): class TestIdentityProxyCredential(TestIdentityProxyBase): - def test_credential_create_attrs(self): - self.verify_create(self.proxy.create_credential, - credential.Credential) + self.verify_create(self.proxy.create_credential, credential.Credential) def test_credential_delete(self): - self.verify_delete(self.proxy.delete_credential, - credential.Credential, False) + self.verify_delete( + self.proxy.delete_credential, credential.Credential, False + ) def test_credential_delete_ignore(self): - self.verify_delete(self.proxy.delete_credential, - credential.Credential, True) + self.verify_delete( + self.proxy.delete_credential, credential.Credential, True + ) def test_credential_find(self): self.verify_find(self.proxy.find_credential, credential.Credential) @@ -63,7 +63,6 @@ def test_credential_update(self): class TestIdentityProxyDomain(TestIdentityProxyBase): - def test_domain_create_attrs(self): self.verify_create(self.proxy.create_domain, domain.Domain) @@ -87,17 +86,16 @@ def test_domain_update(self): class TestIdentityProxyEndpoint(TestIdentityProxyBase): - def test_endpoint_create_attrs(self): self.verify_create(self.proxy.create_endpoint, endpoint.Endpoint) def test_endpoint_delete(self): - self.verify_delete(self.proxy.delete_endpoint, - endpoint.Endpoint, False) + self.verify_delete( + self.proxy.delete_endpoint, endpoint.Endpoint, False + ) def test_endpoint_delete_ignore(self): - self.verify_delete(self.proxy.delete_endpoint, - endpoint.Endpoint, True) + self.verify_delete(self.proxy.delete_endpoint, endpoint.Endpoint, True) def test_endpoint_find(self): self.verify_find(self.proxy.find_endpoint, endpoint.Endpoint) @@ -113,7 +111,6 @@ def test_endpoint_update(self): class TestIdentityProxyGroup(TestIdentityProxyBase): - def test_group_create_attrs(self): self.verify_create(self.proxy.create_group, group.Group) @@ -143,7 +140,7 @@ def test_add_user_to_group(self): expected_args=[ self.proxy, self.proxy._get_resource(user.User, 'uid'), - ] + ], ) def test_remove_user_from_group(self): @@ -154,7 +151,7 @@ def test_remove_user_from_group(self): expected_args=[ self.proxy, self.proxy._get_resource(user.User, 'uid'), - ] + ], ) def test_check_user_in_group(self): @@ -165,18 +162,19 @@ def test_check_user_in_group(self): expected_args=[ self.proxy, self.proxy._get_resource(user.User, 'uid'), - ] + ], ) def test_group_users(self): self.verify_list( - self.proxy.group_users, user.User, + self.proxy.group_users, + user.User, method_kwargs={"group": 'group', "attrs": 1}, - expected_kwargs={"attrs": 1}) + expected_kwargs={"attrs": 1}, + ) class TestIdentityProxyPolicy(TestIdentityProxyBase): - def test_policy_create_attrs(self): self.verify_create(self.proxy.create_policy, policy.Policy) @@ -200,7 +198,6 @@ def test_policy_update(self): class TestIdentityProxyProject(TestIdentityProxyBase): - def test_project_create_attrs(self): self.verify_create(self.proxy.create_project, project.Project) @@ -224,7 +221,7 @@ def test_user_projects(self): self.proxy.user_projects, project.UserProject, method_kwargs={'user': USER_ID}, - expected_kwargs={'user_id': USER_ID} + expected_kwargs={'user_id': USER_ID}, ) def test_project_update(self): @@ -232,7 +229,6 @@ def test_project_update(self): class TestIdentityProxyService(TestIdentityProxyBase): - def test_service_create_attrs(self): self.verify_create(self.proxy.create_service, service.Service) @@ -256,7 +252,6 @@ def test_service_update(self): class TestIdentityProxyUser(TestIdentityProxyBase): - def test_user_create_attrs(self): self.verify_create(self.proxy.create_user, user.User) @@ -280,7 +275,6 @@ def test_user_update(self): class TestIdentityProxyTrust(TestIdentityProxyBase): - def test_trust_create_attrs(self): self.verify_create(self.proxy.create_trust, trust.Trust) @@ -301,7 +295,6 @@ def test_trusts(self): class TestIdentityProxyRegion(TestIdentityProxyBase): - def test_region_create_attrs(self): self.verify_create(self.proxy.create_region, region.Region) @@ -325,7 +318,6 @@ def test_region_update(self): class TestIdentityProxyRole(TestIdentityProxyBase): - def test_role_create_attrs(self): self.verify_create(self.proxy.create_role, role.Role) @@ -349,7 +341,6 @@ def test_role_update(self): class TestIdentityProxyRoleAssignments(TestIdentityProxyBase): - def test_assign_domain_role_to_user(self): self._verify( "openstack.identity.v3.domain.Domain.assign_role_to_user", @@ -359,8 +350,8 @@ def test_assign_domain_role_to_user(self): expected_args=[ self.proxy, self.proxy._get_resource(user.User, 'uid'), - self.proxy._get_resource(role.Role, 'rid') - ] + self.proxy._get_resource(role.Role, 'rid'), + ], ) def test_unassign_domain_role_from_user(self): @@ -372,8 +363,8 @@ def test_unassign_domain_role_from_user(self): expected_args=[ self.proxy, self.proxy._get_resource(user.User, 'uid'), - self.proxy._get_resource(role.Role, 'rid') - ] + self.proxy._get_resource(role.Role, 'rid'), + ], ) def test_validate_user_has_domain_role(self): @@ -385,8 +376,8 @@ def test_validate_user_has_domain_role(self): expected_args=[ self.proxy, self.proxy._get_resource(user.User, 'uid'), - self.proxy._get_resource(role.Role, 'rid') - ] + self.proxy._get_resource(role.Role, 'rid'), + ], ) def test_assign_domain_role_to_group(self): @@ -398,8 +389,8 @@ def test_assign_domain_role_to_group(self): expected_args=[ self.proxy, self.proxy._get_resource(group.Group, 'uid'), - self.proxy._get_resource(role.Role, 'rid') - ] + self.proxy._get_resource(role.Role, 'rid'), + ], ) def test_unassign_domain_role_from_group(self): @@ -411,8 +402,8 @@ def test_unassign_domain_role_from_group(self): expected_args=[ self.proxy, self.proxy._get_resource(group.Group, 'uid'), - self.proxy._get_resource(role.Role, 'rid') - ] + self.proxy._get_resource(role.Role, 'rid'), + ], ) def test_validate_group_has_domain_role(self): @@ -424,8 +415,8 @@ def test_validate_group_has_domain_role(self): expected_args=[ self.proxy, self.proxy._get_resource(group.Group, 'uid'), - self.proxy._get_resource(role.Role, 'rid') - ] + self.proxy._get_resource(role.Role, 'rid'), + ], ) def test_assign_project_role_to_user(self): @@ -437,8 +428,8 @@ def test_assign_project_role_to_user(self): expected_args=[ self.proxy, self.proxy._get_resource(user.User, 'uid'), - self.proxy._get_resource(role.Role, 'rid') - ] + self.proxy._get_resource(role.Role, 'rid'), + ], ) def test_unassign_project_role_from_user(self): @@ -450,8 +441,8 @@ def test_unassign_project_role_from_user(self): expected_args=[ self.proxy, self.proxy._get_resource(user.User, 'uid'), - self.proxy._get_resource(role.Role, 'rid') - ] + self.proxy._get_resource(role.Role, 'rid'), + ], ) def test_validate_user_has_project_role(self): @@ -463,8 +454,8 @@ def test_validate_user_has_project_role(self): expected_args=[ self.proxy, self.proxy._get_resource(user.User, 'uid'), - self.proxy._get_resource(role.Role, 'rid') - ] + self.proxy._get_resource(role.Role, 'rid'), + ], ) def test_assign_project_role_to_group(self): @@ -476,8 +467,8 @@ def test_assign_project_role_to_group(self): expected_args=[ self.proxy, self.proxy._get_resource(group.Group, 'uid'), - self.proxy._get_resource(role.Role, 'rid') - ] + self.proxy._get_resource(role.Role, 'rid'), + ], ) def test_unassign_project_role_from_group(self): @@ -489,8 +480,8 @@ def test_unassign_project_role_from_group(self): expected_args=[ self.proxy, self.proxy._get_resource(group.Group, 'uid'), - self.proxy._get_resource(role.Role, 'rid') - ] + self.proxy._get_resource(role.Role, 'rid'), + ], ) def test_validate_group_has_project_role(self): @@ -502,8 +493,8 @@ def test_validate_group_has_project_role(self): expected_args=[ self.proxy, self.proxy._get_resource(group.Group, 'uid'), - self.proxy._get_resource(role.Role, 'rid') - ] + self.proxy._get_resource(role.Role, 'rid'), + ], ) def test_assign_system_role_to_user(self): @@ -514,8 +505,8 @@ def test_assign_system_role_to_user(self): expected_args=[ self.proxy, self.proxy._get_resource(user.User, 'uid'), - self.proxy._get_resource(role.Role, 'rid') - ] + self.proxy._get_resource(role.Role, 'rid'), + ], ) def test_unassign_system_role_from_user(self): @@ -526,8 +517,8 @@ def test_unassign_system_role_from_user(self): expected_args=[ self.proxy, self.proxy._get_resource(user.User, 'uid'), - self.proxy._get_resource(role.Role, 'rid') - ] + self.proxy._get_resource(role.Role, 'rid'), + ], ) def test_validate_user_has_system_role(self): @@ -538,8 +529,8 @@ def test_validate_user_has_system_role(self): expected_args=[ self.proxy, self.proxy._get_resource(user.User, 'uid'), - self.proxy._get_resource(role.Role, 'rid') - ] + self.proxy._get_resource(role.Role, 'rid'), + ], ) def test_assign_system_role_to_group(self): @@ -550,8 +541,8 @@ def test_assign_system_role_to_group(self): expected_args=[ self.proxy, self.proxy._get_resource(group.Group, 'uid'), - self.proxy._get_resource(role.Role, 'rid') - ] + self.proxy._get_resource(role.Role, 'rid'), + ], ) def test_unassign_system_role_from_group(self): @@ -562,8 +553,8 @@ def test_unassign_system_role_from_group(self): expected_args=[ self.proxy, self.proxy._get_resource(group.Group, 'uid'), - self.proxy._get_resource(role.Role, 'rid') - ] + self.proxy._get_resource(role.Role, 'rid'), + ], ) def test_validate_group_has_system_role(self): @@ -574,6 +565,6 @@ def test_validate_group_has_system_role(self): expected_args=[ self.proxy, self.proxy._get_resource(group.Group, 'uid'), - self.proxy._get_resource(role.Role, 'rid') - ] + self.proxy._get_resource(role.Role, 'rid'), + ], ) diff --git a/openstack/tests/unit/identity/v3/test_region.py b/openstack/tests/unit/identity/v3/test_region.py index a9a4ffc73..c1eb2cada 100644 --- a/openstack/tests/unit/identity/v3/test_region.py +++ b/openstack/tests/unit/identity/v3/test_region.py @@ -24,7 +24,6 @@ class TestRegion(base.TestCase): - def test_basic(self): sot = region.Region() self.assertEqual('region', sot.resource_key) @@ -43,7 +42,8 @@ def test_basic(self): 'limit': 'limit', 'marker': 'marker', }, - sot._query_mapping._mapping) + sot._query_mapping._mapping, + ) def test_make_it(self): sot = region.Region(**EXAMPLE) diff --git a/openstack/tests/unit/identity/v3/test_registered_limit.py b/openstack/tests/unit/identity/v3/test_registered_limit.py index 4764d9648..3a64d7936 100644 --- a/openstack/tests/unit/identity/v3/test_registered_limit.py +++ b/openstack/tests/unit/identity/v3/test_registered_limit.py @@ -20,12 +20,11 @@ "resource_name": 'cores', "default_limit": 10, "description": "compute cores", - "links": {"self": "http://example.com/v3/registered_limit_1"} + "links": {"self": "http://example.com/v3/registered_limit_1"}, } class TestRegistered_limit(base.TestCase): - def test_basic(self): sot = registered_limit.RegisteredLimit() self.assertEqual('registered_limit', sot.resource_key) @@ -44,9 +43,10 @@ def test_basic(self): 'region_id': 'region_id', 'resource_name': 'resource_name', 'marker': 'marker', - 'limit': 'limit' + 'limit': 'limit', }, - sot._query_mapping._mapping) + sot._query_mapping._mapping, + ) def test_make_it(self): sot = registered_limit.RegisteredLimit(**EXAMPLE) diff --git a/openstack/tests/unit/identity/v3/test_role.py b/openstack/tests/unit/identity/v3/test_role.py index 3f59f02cb..c9cc49c92 100644 --- a/openstack/tests/unit/identity/v3/test_role.py +++ b/openstack/tests/unit/identity/v3/test_role.py @@ -20,12 +20,11 @@ 'links': {'self': 'http://example.com/user1'}, 'name': '2', 'description': 'test description for role', - 'domain_id': 'default' + 'domain_id': 'default', } class TestRole(base.TestCase): - def test_basic(self): sot = role.Role() self.assertEqual('role', sot.resource_key) @@ -45,7 +44,8 @@ def test_basic(self): 'limit': 'limit', 'marker': 'marker', }, - sot._query_mapping._mapping) + sot._query_mapping._mapping, + ) def test_make_it(self): sot = role.Role(**EXAMPLE) diff --git a/openstack/tests/unit/identity/v3/test_role_assignment.py b/openstack/tests/unit/identity/v3/test_role_assignment.py index b141baff5..35c224e1a 100644 --- a/openstack/tests/unit/identity/v3/test_role_assignment.py +++ b/openstack/tests/unit/identity/v3/test_role_assignment.py @@ -20,18 +20,16 @@ 'links': {'self': 'http://example.com/user1'}, 'scope': {'domain': {'id': '2'}}, 'user': {'id': '3'}, - 'group': {'id': '4'} + 'group': {'id': '4'}, } class TestRoleAssignment(base.TestCase): - def test_basic(self): sot = role_assignment.RoleAssignment() self.assertEqual('role_assignment', sot.resource_key) self.assertEqual('role_assignments', sot.resources_key) - self.assertEqual('/role_assignments', - sot.base_path) + self.assertEqual('/role_assignments', sot.base_path) self.assertTrue(sot.allow_list) def test_make_it(self): diff --git a/openstack/tests/unit/identity/v3/test_role_domain_group_assignment.py b/openstack/tests/unit/identity/v3/test_role_domain_group_assignment.py index b41ad7db8..7e4a84877 100644 --- a/openstack/tests/unit/identity/v3/test_role_domain_group_assignment.py +++ b/openstack/tests/unit/identity/v3/test_role_domain_group_assignment.py @@ -20,23 +20,22 @@ 'links': {'self': 'http://example.com/user1'}, 'name': '2', 'domain_id': '3', - 'group_id': '4' + 'group_id': '4', } class TestRoleDomainGroupAssignment(base.TestCase): - def test_basic(self): sot = role_domain_group_assignment.RoleDomainGroupAssignment() self.assertEqual('role', sot.resource_key) self.assertEqual('roles', sot.resources_key) - self.assertEqual('/domains/%(domain_id)s/groups/%(group_id)s/roles', - sot.base_path) + self.assertEqual( + '/domains/%(domain_id)s/groups/%(group_id)s/roles', sot.base_path + ) self.assertTrue(sot.allow_list) def test_make_it(self): - sot = \ - role_domain_group_assignment.RoleDomainGroupAssignment(**EXAMPLE) + sot = role_domain_group_assignment.RoleDomainGroupAssignment(**EXAMPLE) self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['links'], sot.links) self.assertEqual(EXAMPLE['name'], sot.name) diff --git a/openstack/tests/unit/identity/v3/test_role_domain_user_assignment.py b/openstack/tests/unit/identity/v3/test_role_domain_user_assignment.py index 20fa07ac8..8e5a4901c 100644 --- a/openstack/tests/unit/identity/v3/test_role_domain_user_assignment.py +++ b/openstack/tests/unit/identity/v3/test_role_domain_user_assignment.py @@ -20,23 +20,22 @@ 'links': {'self': 'http://example.com/user1'}, 'name': '2', 'domain_id': '3', - 'user_id': '4' + 'user_id': '4', } class TestRoleDomainUserAssignment(base.TestCase): - def test_basic(self): sot = role_domain_user_assignment.RoleDomainUserAssignment() self.assertEqual('role', sot.resource_key) self.assertEqual('roles', sot.resources_key) - self.assertEqual('/domains/%(domain_id)s/users/%(user_id)s/roles', - sot.base_path) + self.assertEqual( + '/domains/%(domain_id)s/users/%(user_id)s/roles', sot.base_path + ) self.assertTrue(sot.allow_list) def test_make_it(self): - sot = \ - role_domain_user_assignment.RoleDomainUserAssignment(**EXAMPLE) + sot = role_domain_user_assignment.RoleDomainUserAssignment(**EXAMPLE) self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['links'], sot.links) self.assertEqual(EXAMPLE['name'], sot.name) diff --git a/openstack/tests/unit/identity/v3/test_role_project_group_assignment.py b/openstack/tests/unit/identity/v3/test_role_project_group_assignment.py index ea3bc2772..08926e82d 100644 --- a/openstack/tests/unit/identity/v3/test_role_project_group_assignment.py +++ b/openstack/tests/unit/identity/v3/test_role_project_group_assignment.py @@ -20,23 +20,24 @@ 'links': {'self': 'http://example.com/user1'}, 'name': '2', 'project_id': '3', - 'group_id': '4' + 'group_id': '4', } class TestRoleProjectGroupAssignment(base.TestCase): - def test_basic(self): sot = role_project_group_assignment.RoleProjectGroupAssignment() self.assertEqual('role', sot.resource_key) self.assertEqual('roles', sot.resources_key) - self.assertEqual('/projects/%(project_id)s/groups/%(group_id)s/roles', - sot.base_path) + self.assertEqual( + '/projects/%(project_id)s/groups/%(group_id)s/roles', sot.base_path + ) self.assertTrue(sot.allow_list) def test_make_it(self): - sot = \ - role_project_group_assignment.RoleProjectGroupAssignment(**EXAMPLE) + sot = role_project_group_assignment.RoleProjectGroupAssignment( + **EXAMPLE + ) self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['links'], sot.links) self.assertEqual(EXAMPLE['name'], sot.name) diff --git a/openstack/tests/unit/identity/v3/test_role_project_user_assignment.py b/openstack/tests/unit/identity/v3/test_role_project_user_assignment.py index 55448784b..3c72969ab 100644 --- a/openstack/tests/unit/identity/v3/test_role_project_user_assignment.py +++ b/openstack/tests/unit/identity/v3/test_role_project_user_assignment.py @@ -20,23 +20,22 @@ 'links': {'self': 'http://example.com/user1'}, 'name': '2', 'project_id': '3', - 'user_id': '4' + 'user_id': '4', } class TestRoleProjectUserAssignment(base.TestCase): - def test_basic(self): sot = role_project_user_assignment.RoleProjectUserAssignment() self.assertEqual('role', sot.resource_key) self.assertEqual('roles', sot.resources_key) - self.assertEqual('/projects/%(project_id)s/users/%(user_id)s/roles', - sot.base_path) + self.assertEqual( + '/projects/%(project_id)s/users/%(user_id)s/roles', sot.base_path + ) self.assertTrue(sot.allow_list) def test_make_it(self): - sot = \ - role_project_user_assignment.RoleProjectUserAssignment(**EXAMPLE) + sot = role_project_user_assignment.RoleProjectUserAssignment(**EXAMPLE) self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['links'], sot.links) self.assertEqual(EXAMPLE['name'], sot.name) diff --git a/openstack/tests/unit/identity/v3/test_role_system_group_assignment.py b/openstack/tests/unit/identity/v3/test_role_system_group_assignment.py index 17b8ec14b..7a1b3a411 100644 --- a/openstack/tests/unit/identity/v3/test_role_system_group_assignment.py +++ b/openstack/tests/unit/identity/v3/test_role_system_group_assignment.py @@ -15,26 +15,19 @@ IDENTIFIER = 'IDENTIFIER' -EXAMPLE = { - 'id': IDENTIFIER, - 'name': '2', - 'group_id': '4' -} +EXAMPLE = {'id': IDENTIFIER, 'name': '2', 'group_id': '4'} class TestRoleSystemGroupAssignment(base.TestCase): - def test_basic(self): sot = role_system_group_assignment.RoleSystemGroupAssignment() self.assertEqual('role', sot.resource_key) self.assertEqual('roles', sot.resources_key) - self.assertEqual('/system/groups/%(group_id)s/roles', - sot.base_path) + self.assertEqual('/system/groups/%(group_id)s/roles', sot.base_path) self.assertTrue(sot.allow_list) def test_make_it(self): - sot = \ - role_system_group_assignment.RoleSystemGroupAssignment(**EXAMPLE) + sot = role_system_group_assignment.RoleSystemGroupAssignment(**EXAMPLE) self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['group_id'], sot.group_id) diff --git a/openstack/tests/unit/identity/v3/test_role_system_user_assignment.py b/openstack/tests/unit/identity/v3/test_role_system_user_assignment.py index 98f9cf68a..00547f9f0 100644 --- a/openstack/tests/unit/identity/v3/test_role_system_user_assignment.py +++ b/openstack/tests/unit/identity/v3/test_role_system_user_assignment.py @@ -15,25 +15,18 @@ IDENTIFIER = 'IDENTIFIER' -EXAMPLE = { - 'id': IDENTIFIER, - 'name': '2', - 'user_id': '4' -} +EXAMPLE = {'id': IDENTIFIER, 'name': '2', 'user_id': '4'} class TestRoleSystemUserAssignment(base.TestCase): - def test_basic(self): sot = role_system_user_assignment.RoleSystemUserAssignment() self.assertEqual('role', sot.resource_key) self.assertEqual('roles', sot.resources_key) - self.assertEqual('/system/users/%(user_id)s/roles', - sot.base_path) + self.assertEqual('/system/users/%(user_id)s/roles', sot.base_path) self.assertTrue(sot.allow_list) def test_make_it(self): - sot = \ - role_system_user_assignment.RoleSystemUserAssignment(**EXAMPLE) + sot = role_system_user_assignment.RoleSystemUserAssignment(**EXAMPLE) self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['name'], sot.name) diff --git a/openstack/tests/unit/identity/v3/test_service.py b/openstack/tests/unit/identity/v3/test_service.py index e03b2486d..a957c957a 100644 --- a/openstack/tests/unit/identity/v3/test_service.py +++ b/openstack/tests/unit/identity/v3/test_service.py @@ -26,7 +26,6 @@ class TestService(base.TestCase): - def test_basic(self): sot = service.Service() self.assertEqual('service', sot.resource_key) @@ -46,7 +45,8 @@ def test_basic(self): 'limit': 'limit', 'marker': 'marker', }, - sot._query_mapping._mapping) + sot._query_mapping._mapping, + ) def test_make_it(self): sot = service.Service(**EXAMPLE) diff --git a/openstack/tests/unit/identity/v3/test_trust.py b/openstack/tests/unit/identity/v3/test_trust.py index 6a8dd7bea..9dfe2d4c0 100644 --- a/openstack/tests/unit/identity/v3/test_trust.py +++ b/openstack/tests/unit/identity/v3/test_trust.py @@ -33,7 +33,6 @@ class TestTrust(base.TestCase): - def test_basic(self): sot = trust.Trust() self.assertEqual('trust', sot.resource_key) @@ -53,8 +52,9 @@ def test_make_it(self): self.assertEqual(EXAMPLE['links'], sot.links) self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['role_links'], sot.role_links) - self.assertEqual(EXAMPLE['redelegated_trust_id'], - sot.redelegated_trust_id) + self.assertEqual( + EXAMPLE['redelegated_trust_id'], sot.redelegated_trust_id + ) self.assertEqual(EXAMPLE['remaining_uses'], sot.remaining_uses) self.assertEqual(EXAMPLE['trustee_user_id'], sot.trustee_user_id) self.assertEqual(EXAMPLE['trustor_user_id'], sot.trustor_user_id) diff --git a/openstack/tests/unit/identity/v3/test_user.py b/openstack/tests/unit/identity/v3/test_user.py index 0cd7f4cb5..070279d93 100644 --- a/openstack/tests/unit/identity/v3/test_user.py +++ b/openstack/tests/unit/identity/v3/test_user.py @@ -30,7 +30,6 @@ class TestUser(base.TestCase): - def test_basic(self): sot = user.User() self.assertEqual('user', sot.resource_key) @@ -52,12 +51,12 @@ def test_basic(self): 'limit': 'limit', 'marker': 'marker', }, - sot._query_mapping._mapping) + sot._query_mapping._mapping, + ) def test_make_it(self): sot = user.User(**EXAMPLE) - self.assertEqual(EXAMPLE['default_project_id'], - sot.default_project_id) + self.assertEqual(EXAMPLE['default_project_id'], sot.default_project_id) self.assertEqual(EXAMPLE['description'], sot.description) self.assertEqual(EXAMPLE['domain_id'], sot.domain_id) self.assertEqual(EXAMPLE['email'], sot.email) @@ -66,5 +65,6 @@ def test_make_it(self): self.assertEqual(EXAMPLE['links'], sot.links) self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['password'], sot.password) - self.assertEqual(EXAMPLE['password_expires_at'], - sot.password_expires_at) + self.assertEqual( + EXAMPLE['password_expires_at'], sot.password_expires_at + ) From 34da09f3125ccd0408f2e0019c85d95188fef573 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 3 May 2023 12:12:59 +0100 Subject: [PATCH 3240/3836] Blackify openstack.block_storage Black used with the '-l 79 -S' flags. A future change will ignore this commit in git-blame history by adding a 'git-blame-ignore-revs' file. Change-Id: I502d2788eb75e674e8b399034513996c81407216 Signed-off-by: Stephen Finucane --- openstack/block_storage/_base_proxy.py | 23 +- openstack/block_storage/v2/_proxy.py | 46 ++- openstack/block_storage/v2/backup.py | 76 +++-- openstack/block_storage/v2/snapshot.py | 11 +- openstack/block_storage/v2/volume.py | 53 ++-- openstack/block_storage/v3/_proxy.py | 157 +++++---- openstack/block_storage/v3/backup.py | 73 +++-- openstack/block_storage/v3/extension.py | 1 + openstack/block_storage/v3/group_snapshot.py | 8 +- openstack/block_storage/v3/group_type.py | 4 +- openstack/block_storage/v3/limits.py | 9 +- openstack/block_storage/v3/resource_filter.py | 1 + openstack/block_storage/v3/snapshot.py | 21 +- openstack/block_storage/v3/type.py | 8 +- openstack/block_storage/v3/volume.py | 79 +++-- .../block_storage/v2/test_backup.py | 23 +- .../block_storage/v2/test_snapshot.py | 24 +- .../functional/block_storage/v2/test_stats.py | 33 +- .../functional/block_storage/v2/test_type.py | 7 +- .../block_storage/v2/test_volume.py | 12 +- .../v3/test_availability_zone.py | 1 - .../block_storage/v3/test_backup.py | 47 +-- .../block_storage/v3/test_capabilities.py | 6 +- .../block_storage/v3/test_extension.py | 1 - .../block_storage/v3/test_limits.py | 1 - .../block_storage/v3/test_resource_filters.py | 1 - .../block_storage/v3/test_snapshot.py | 24 +- .../functional/block_storage/v3/test_type.py | 7 +- .../block_storage/v3/test_volume.py | 1 - .../unit/block_storage/v2/test_backup.py | 28 +- .../tests/unit/block_storage/v2/test_proxy.py | 129 ++++---- .../unit/block_storage/v2/test_snapshot.py | 26 +- .../tests/unit/block_storage/v2/test_stats.py | 31 +- .../tests/unit/block_storage/v2/test_type.py | 30 +- .../unit/block_storage/v2/test_volume.py | 160 ++++++---- .../v3/test_availability_zone.py | 7 +- .../unit/block_storage/v3/test_backup.py | 33 +- .../block_storage/v3/test_capabilities.py | 46 +-- .../unit/block_storage/v3/test_extension.py | 6 +- .../tests/unit/block_storage/v3/test_group.py | 21 +- .../unit/block_storage/v3/test_limits.py | 90 +++--- .../block_storage/v3/test_resource_filter.py | 28 +- .../unit/block_storage/v3/test_snapshot.py | 41 +-- .../tests/unit/block_storage/v3/test_type.py | 43 ++- .../block_storage/v3/test_type_encryption.py | 1 - .../unit/block_storage/v3/test_volume.py | 298 +++++++++++------- 46 files changed, 1011 insertions(+), 765 deletions(-) diff --git a/openstack/block_storage/_base_proxy.py b/openstack/block_storage/_base_proxy.py index b1fb4c056..43df38161 100644 --- a/openstack/block_storage/_base_proxy.py +++ b/openstack/block_storage/_base_proxy.py @@ -16,10 +16,16 @@ class BaseBlockStorageProxy(proxy.Proxy, metaclass=abc.ABCMeta): - def create_image( - self, name, volume, allow_duplicates, - container_format, disk_format, wait, timeout): + self, + name, + volume, + allow_duplicates, + container_format, + disk_format, + wait, + timeout, + ): if not disk_format: disk_format = self._connection.config.config['image_format'] if not container_format: @@ -33,7 +39,8 @@ def create_image( if not volume_obj: raise exceptions.SDKException( "Volume {volume} given to create_image could" - " not be found".format(volume=volume)) + " not be found".format(volume=volume) + ) volume_id = volume_obj['id'] data = self.post( '/volumes/{id}/action'.format(id=volume_id), @@ -42,7 +49,11 @@ def create_image( 'force': allow_duplicates, 'image_name': name, 'container_format': container_format, - 'disk_format': disk_format}}) + 'disk_format': disk_format, + } + }, + ) response = self._connection._get_and_munchify( - 'os-volume_upload_image', data) + 'os-volume_upload_image', data + ) return self._connection.image._existing_image(id=response['image_id']) diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index f2ceacaa6..19bcf4583 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -132,8 +132,9 @@ def delete_snapshot(self, snapshot, ignore_missing=True): :returns: ``None`` """ - self._delete(_snapshot.Snapshot, snapshot, - ignore_missing=ignore_missing) + self._delete( + _snapshot.Snapshot, snapshot, ignore_missing=ignore_missing + ) # ====== SNAPSHOT ACTIONS ====== def reset_snapshot(self, snapshot, status): @@ -398,9 +399,7 @@ def reset_volume_status( volume = self._get_resource(_volume.Volume, volume) volume.reset_status(self, status, attach_status, migration_status) - def attach_volume( - self, volume, mountpoint, instance=None, host_name=None - ): + def attach_volume(self, volume, mountpoint, instance=None, host_name=None): """Attaches a volume to a server. :param volume: The value can be either the ID of a volume or a @@ -414,9 +413,7 @@ def attach_volume( volume = self._get_resource(_volume.Volume, volume) volume.attach(self, mountpoint, instance, host_name) - def detach_volume( - self, volume, attachment, force=False, connector=None - ): + def detach_volume(self, volume, attachment, force=False, connector=None): """Detaches a volume from a server. :param volume: The value can be either the ID of a volume or a @@ -444,8 +441,7 @@ def unmanage_volume(self, volume): volume.unmanage(self) def migrate_volume( - self, volume, host=None, force_host_copy=False, - lock_volume=False + self, volume, host=None, force_host_copy=False, lock_volume=False ): """Migrates a volume to the specified host. @@ -466,9 +462,7 @@ def migrate_volume( volume = self._get_resource(_volume.Volume, volume) volume.migrate(self, host, force_host_copy, lock_volume) - def complete_volume_migration( - self, volume, new_volume, error=False - ): + def complete_volume_migration(self, volume, new_volume, error=False): """Complete the migration of a volume. :param volume: The value can be either the ID of a volume or a @@ -584,8 +578,7 @@ def delete_backup(self, backup, ignore_missing=True, force=False): :returns: ``None`` """ if not force: - self._delete( - _backup.Backup, backup, ignore_missing=ignore_missing) + self._delete(_backup.Backup, backup, ignore_missing=ignore_missing) else: backup = self._get_resource(_backup.Backup, backup) backup.force_delete(self) @@ -617,8 +610,9 @@ def reset_backup(self, backup, status): backup = self._get_resource(_backup.Backup, backup) backup.reset(self, status) - def wait_for_status(self, res, status='available', failures=None, - interval=2, wait=120): + def wait_for_status( + self, res, status='available', failures=None, interval=2, wait=120 + ): """Wait for a resource to be in a particular status. :param res: The resource to wait on to reach the specified status. @@ -641,7 +635,8 @@ def wait_for_status(self, res, status='available', failures=None, """ failures = ['error'] if failures is None else failures return resource.wait_for_status( - self, res, status, failures, interval, wait) + self, res, status, failures, interval, wait + ) def wait_for_delete(self, res, interval=2, wait=120): """Wait for a resource to be deleted. @@ -674,9 +669,9 @@ def get_quota_set(self, project, usage=False, **query): """ project = self._get_resource(_project.Project, project) res = self._get_resource( - _quota_set.QuotaSet, None, project_id=project.id) - return res.fetch( - self, usage=usage, **query) + _quota_set.QuotaSet, None, project_id=project.id + ) + return res.fetch(self, usage=usage, **query) def get_quota_set_defaults(self, project): """Show QuotaSet defaults for the project @@ -691,9 +686,9 @@ def get_quota_set_defaults(self, project): """ project = self._get_resource(_project.Project, project) res = self._get_resource( - _quota_set.QuotaSet, None, project_id=project.id) - return res.fetch( - self, base_path='/os-quota-sets/defaults') + _quota_set.QuotaSet, None, project_id=project.id + ) + return res.fetch(self, base_path='/os-quota-sets/defaults') def revert_quota_set(self, project, **query): """Reset Quota for the project/user. @@ -707,7 +702,8 @@ def revert_quota_set(self, project, **query): """ project = self._get_resource(_project.Project, project) res = self._get_resource( - _quota_set.QuotaSet, None, project_id=project.id) + _quota_set.QuotaSet, None, project_id=project.id + ) if not query: query = {} diff --git a/openstack/block_storage/v2/backup.py b/openstack/block_storage/v2/backup.py index cc4771b4d..3355424ef 100644 --- a/openstack/block_storage/v2/backup.py +++ b/openstack/block_storage/v2/backup.py @@ -16,14 +16,22 @@ class Backup(resource.Resource): """Volume Backup""" + resource_key = "backup" resources_key = "backups" base_path = "/backups" _query_mapping = resource.QueryParameters( - 'all_tenants', 'limit', 'marker', 'project_id', - 'name', 'status', 'volume_id', - 'sort_key', 'sort_dir') + 'all_tenants', + 'limit', + 'marker', + 'project_id', + 'name', + 'status', + 'volume_id', + 'sort_key', + 'sort_dir', + ) # capabilities allow_fetch = True @@ -97,35 +105,48 @@ def create(self, session, prepend_key=True, base_path=None, **params): session = self._get_session(session) microversion = self._get_microversion(session, action='create') - requires_id = (self.create_requires_id - if self.create_requires_id is not None - else self.create_method == 'PUT') + requires_id = ( + self.create_requires_id + if self.create_requires_id is not None + else self.create_method == 'PUT' + ) if self.create_exclude_id_from_body: self._body._dirty.discard("id") if self.create_method == 'POST': - request = self._prepare_request(requires_id=requires_id, - prepend_key=prepend_key, - base_path=base_path) + request = self._prepare_request( + requires_id=requires_id, + prepend_key=prepend_key, + base_path=base_path, + ) # NOTE(gtema) this is a funny example of when attribute # is called "incremental" on create, "is_incremental" on get # and use of "alias" or "aka" is not working for such conflict, # since our preferred attr name is exactly "is_incremental" body = request.body if 'is_incremental' in body['backup']: - body['backup']['incremental'] = \ - body['backup'].pop('is_incremental') - response = session.post(request.url, - json=request.body, headers=request.headers, - microversion=microversion, params=params) + body['backup']['incremental'] = body['backup'].pop( + 'is_incremental' + ) + response = session.post( + request.url, + json=request.body, + headers=request.headers, + microversion=microversion, + params=params, + ) else: # Just for safety of the implementation (since PUT removed) raise exceptions.ResourceFailure( - "Invalid create method: %s" % self.create_method) - - has_body = (self.has_body if self.create_returns_body is None - else self.create_returns_body) + "Invalid create method: %s" % self.create_method + ) + + has_body = ( + self.has_body + if self.create_returns_body is None + else self.create_returns_body + ) self.microversion = microversion self._translate_response(response, has_body=has_body) # direct comparision to False since we need to rule out None @@ -137,8 +158,9 @@ def create(self, session, prepend_key=True, base_path=None, **params): def _action(self, session, body, microversion=None): """Preform backup actions given the message body.""" url = utils.urljoin(self.base_path, self.id, 'action') - resp = session.post(url, json=body, - microversion=self._max_microversion) + resp = session.post( + url, json=body, microversion=self._max_microversion + ) exceptions.raise_from_response(resp) return resp @@ -157,22 +179,20 @@ def restore(self, session, volume_id=None, name=None): if name: body['restore']['name'] = name if not (volume_id or name): - raise exceptions.SDKException('Either of `name` or `volume_id`' - ' must be specified.') - response = session.post(url, - json=body) + raise exceptions.SDKException( + 'Either of `name` or `volume_id`' ' must be specified.' + ) + response = session.post(url, json=body) self._translate_response(response, has_body=False) return self def force_delete(self, session): - """Force backup deletion - """ + """Force backup deletion""" body = {'os-force_delete': {}} self._action(session, body) def reset(self, session, status): - """Reset the status of the backup - """ + """Reset the status of the backup""" body = {'os-reset_status': {'status': status}} self._action(session, body) diff --git a/openstack/block_storage/v2/snapshot.py b/openstack/block_storage/v2/snapshot.py index 49e5a99d0..c2b49d704 100644 --- a/openstack/block_storage/v2/snapshot.py +++ b/openstack/block_storage/v2/snapshot.py @@ -23,7 +23,8 @@ class Snapshot(resource.Resource, metadata.MetadataMixin): base_path = "/snapshots" _query_mapping = resource.QueryParameters( - 'name', 'status', 'volume_id', all_projects='all_tenants') + 'name', 'status', 'volume_id', all_projects='all_tenants' + ) # capabilities allow_fetch = True @@ -53,14 +54,14 @@ class Snapshot(resource.Resource, metadata.MetadataMixin): def _action(self, session, body, microversion=None): """Preform backup actions given the message body.""" url = utils.urljoin(self.base_path, self.id, 'action') - resp = session.post(url, json=body, - microversion=self._max_microversion) + resp = session.post( + url, json=body, microversion=self._max_microversion + ) exceptions.raise_from_response(resp) return resp def reset(self, session, status): - """Reset the status of the snapshot. - """ + """Reset the status of the snapshot.""" body = {'os-reset_status': {'status': status}} self._action(session, body) diff --git a/openstack/block_storage/v2/volume.py b/openstack/block_storage/v2/volume.py index 0233bc7f4..7c7843ddc 100644 --- a/openstack/block_storage/v2/volume.py +++ b/openstack/block_storage/v2/volume.py @@ -21,7 +21,8 @@ class Volume(resource.Resource, metadata.MetadataMixin): base_path = "/volumes" _query_mapping = resource.QueryParameters( - 'name', 'status', 'project_id', all_projects='all_tenants') + 'name', 'status', 'project_id', all_projects='all_tenants' + ) # capabilities allow_fetch = True @@ -45,7 +46,8 @@ class Volume(resource.Resource, metadata.MetadataMixin): description = resource.Body("description") #: Extended replication status on this volume. extended_replication_status = resource.Body( - "os-volume-replication:extended_status") + "os-volume-replication:extended_status" + ) #: The volume's current back-end. host = resource.Body("os-vol-host-attr:host") #: The ID of the image from which you want to create the volume. @@ -66,7 +68,8 @@ class Volume(resource.Resource, metadata.MetadataMixin): project_id = resource.Body("os-vol-tenant-attr:tenant_id") #: Data set by the replication driver replication_driver_data = resource.Body( - "os-volume-replication:driver_data") + "os-volume-replication:driver_data" + ) #: Status of replication on this volume. replication_status = resource.Body("replication_status") #: Scheduler hints for the volume @@ -111,24 +114,22 @@ def set_bootable_status(self, session, bootable=True): body = {'os-set_bootable': {'bootable': bootable}} self._action(session, body) - def reset_status( - self, session, status, attach_status, migration_status - ): + def reset_status(self, session, status, attach_status, migration_status): """Reset volume statuses (admin operation)""" - body = {'os-reset_status': { - 'status': status, - 'attach_status': attach_status, - 'migration_status': migration_status - }} + body = { + 'os-reset_status': { + 'status': status, + 'attach_status': attach_status, + 'migration_status': migration_status, + } + } self._action(session, body) - def attach( - self, session, mountpoint, instance - ): + def attach(self, session, mountpoint, instance): """Attach volume to server""" - body = {'os-attach': { - 'mountpoint': mountpoint, - 'instance_uuid': instance}} + body = { + 'os-attach': {'mountpoint': mountpoint, 'instance_uuid': instance} + } self._action(session, body) @@ -137,8 +138,7 @@ def detach(self, session, attachment, force=False): if not force: body = {'os-detach': {'attachment_id': attachment}} if force: - body = {'os-force_detach': { - 'attachment_id': attachment}} + body = {'os-force_detach': {'attachment_id': attachment}} self._action(session, body) @@ -150,16 +150,14 @@ def unmanage(self, session): def retype(self, session, new_type, migration_policy=None): """Change volume type""" - body = {'os-retype': { - 'new_type': new_type}} + body = {'os-retype': {'new_type': new_type}} if migration_policy: body['os-retype']['migration_policy'] = migration_policy self._action(session, body) def migrate( - self, session, host=None, force_host_copy=False, - lock_volume=False + self, session, host=None, force_host_copy=False, lock_volume=False ): """Migrate volume""" req = dict() @@ -175,9 +173,12 @@ def migrate( def complete_migration(self, session, new_volume_id, error=False): """Complete volume migration""" - body = {'os-migrate_volume_completion': { - 'new_volume': new_volume_id, - 'error': error}} + body = { + 'os-migrate_volume_completion': { + 'new_volume': new_volume_id, + 'error': error, + } + } self._action(session, body) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 106b09531..ec2147a0d 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -45,7 +45,7 @@ class Proxy(_base_proxy.BaseBlockStorageProxy): "snapshot": _snapshot.Snapshot, "stats_pools": _stats.Pools, "type": _type.Type, - "volume": _volume.Volume + "volume": _volume.Volume, } # ====== SNAPSHOTS ====== @@ -168,7 +168,8 @@ def delete_snapshot(self, snapshot, ignore_missing=True, force=False): """ if not force: self._delete( - _snapshot.Snapshot, snapshot, ignore_missing=ignore_missing) + _snapshot.Snapshot, snapshot, ignore_missing=ignore_missing + ) else: snapshot = self._get_resource(_snapshot.Snapshot, snapshot) snapshot.force_delete(self) @@ -405,9 +406,11 @@ def get_type_encryption(self, volume_type_id): """ volume_type = self._get_resource(_type.Type, volume_type_id) - return self._get(_type.TypeEncryption, - volume_type_id=volume_type.id, - requires_id=False) + return self._get( + _type.TypeEncryption, + volume_type_id=volume_type.id, + requires_id=False, + ) def create_type_encryption(self, volume_type, **attrs): """Create new type encryption from attributes @@ -425,11 +428,13 @@ def create_type_encryption(self, volume_type, **attrs): """ volume_type = self._get_resource(_type.Type, volume_type) - return self._create(_type.TypeEncryption, - volume_type_id=volume_type.id, **attrs) + return self._create( + _type.TypeEncryption, volume_type_id=volume_type.id, **attrs + ) - def delete_type_encryption(self, encryption=None, - volume_type=None, ignore_missing=True): + def delete_type_encryption( + self, encryption=None, volume_type=None, ignore_missing=True + ): """Delete type encryption attributes :param encryption: The value can be None or a @@ -452,12 +457,15 @@ def delete_type_encryption(self, encryption=None, if volume_type: volume_type = self._get_resource(_type.Type, volume_type) - encryption = self._get(_type.TypeEncryption, - volume_type=volume_type.id, - requires_id=False) + encryption = self._get( + _type.TypeEncryption, + volume_type=volume_type.id, + requires_id=False, + ) - self._delete(_type.TypeEncryption, encryption, - ignore_missing=ignore_missing) + self._delete( + _type.TypeEncryption, encryption, ignore_missing=ignore_missing + ) def update_type_encryption( self, @@ -725,9 +733,7 @@ def reset_volume_status( volume = self._get_resource(_volume.Volume, volume) volume.reset_status(self, status, attach_status, migration_status) - def revert_volume_to_snapshot( - self, volume, snapshot - ): + def revert_volume_to_snapshot(self, volume, snapshot): """Revert a volume to its latest snapshot. This method only support reverting a detached volume, and the @@ -744,9 +750,7 @@ def revert_volume_to_snapshot( snapshot = self._get_resource(_snapshot.Snapshot, snapshot) volume.revert_to_snapshot(self, snapshot.id) - def attach_volume( - self, volume, mountpoint, instance=None, host_name=None - ): + def attach_volume(self, volume, mountpoint, instance=None, host_name=None): """Attaches a volume to a server. :param volume: The value can be either the ID of a volume or a @@ -760,9 +764,7 @@ def attach_volume( volume = self._get_resource(_volume.Volume, volume) volume.attach(self, mountpoint, instance, host_name) - def detach_volume( - self, volume, attachment, force=False, connector=None - ): + def detach_volume(self, volume, attachment, force=False, connector=None): """Detaches a volume from a server. :param volume: The value can be either the ID of a volume or a @@ -784,13 +786,17 @@ def unmanage_volume(self, volume): :param volume: The value can be either the ID of a volume or a :class:`~openstack.block_storage.v3.volume.Volume` instance. - :returns: None """ + :returns: None""" volume = self._get_resource(_volume.Volume, volume) volume.unmanage(self) def migrate_volume( - self, volume, host=None, force_host_copy=False, - lock_volume=False, cluster=None + self, + volume, + host=None, + force_host_copy=False, + lock_volume=False, + cluster=None, ): """Migrates a volume to the specified host. @@ -816,9 +822,7 @@ def migrate_volume( volume = self._get_resource(_volume.Volume, volume) volume.migrate(self, host, force_host_copy, lock_volume, cluster) - def complete_volume_migration( - self, volume, new_volume, error=False - ): + def complete_volume_migration(self, volume, new_volume, error=False): """Complete the migration of a volume. :param volume: The value can be either the ID of a volume or a @@ -833,8 +837,14 @@ def complete_volume_migration( volume.complete_migration(self, new_volume, error) def upload_volume_to_image( - self, volume, image_name, force=False, disk_format=None, - container_format=None, visibility=None, protected=None + self, + volume, + image_name, + force=False, + disk_format=None, + container_format=None, + visibility=None, + protected=None, ): """Uploads the specified volume to image service. @@ -852,9 +862,13 @@ def upload_volume_to_image( """ volume = self._get_resource(_volume.Volume, volume) volume.upload_to_image( - self, image_name, force=force, disk_format=disk_format, - container_format=container_format, visibility=visibility, - protected=protected + self, + image_name, + force=force, + disk_format=disk_format, + container_format=container_format, + visibility=visibility, + protected=protected, ) def reserve_volume(self, volume): @@ -863,7 +877,7 @@ def reserve_volume(self, volume): :param volume: The value can be either the ID of a volume or a :class:`~openstack.block_storage.v3.volume.Volume` instance. - :returns: None """ + :returns: None""" volume = self._get_resource(_volume.Volume, volume) volume.reserve(self) @@ -873,7 +887,7 @@ def unreserve_volume(self, volume): :param volume: The value can be either the ID of a volume or a :class:`~openstack.block_storage.v3.volume.Volume` instance. - :returns: None """ + :returns: None""" volume = self._get_resource(_volume.Volume, volume) volume.unreserve(self) @@ -883,7 +897,7 @@ def begin_volume_detaching(self, volume): :param volume: The value can be either the ID of a volume or a :class:`~openstack.block_storage.v3.volume.Volume` instance. - :returns: None """ + :returns: None""" volume = self._get_resource(_volume.Volume, volume) volume.begin_detaching(self) @@ -893,7 +907,7 @@ def abort_volume_detaching(self, volume): :param volume: The value can be either the ID of a volume or a :class:`~openstack.block_storage.v3.volume.Volume` instance. - :returns: None """ + :returns: None""" volume = self._get_resource(_volume.Volume, volume) volume.abort_detaching(self) @@ -904,7 +918,7 @@ def init_volume_attachment(self, volume, connector): :class:`~openstack.block_storage.v3.volume.Volume` instance. :param dict connector: The connector object. - :returns: None """ + :returns: None""" volume = self._get_resource(_volume.Volume, volume) volume.init_attachment(self, connector) @@ -1022,8 +1036,7 @@ def delete_backup(self, backup, ignore_missing=True, force=False): :returns: ``None`` """ if not force: - self._delete( - _backup.Backup, backup, ignore_missing=ignore_missing) + self._delete(_backup.Backup, backup, ignore_missing=ignore_missing) else: backup = self._get_resource(_backup.Backup, backup) backup.force_delete(self) @@ -1295,7 +1308,8 @@ def reset_group_snapshot_state(self, group_snapshot, state): :returns: None """ resource = self._get_resource( - _group_snapshot.GroupSnapshot, group_snapshot) + _group_snapshot.GroupSnapshot, group_snapshot + ) resource.reset_state(self, state) def delete_group_snapshot(self, group_snapshot, ignore_missing=True): @@ -1307,8 +1321,10 @@ def delete_group_snapshot(self, group_snapshot, ignore_missing=True): :returns: None """ self._delete( - _group_snapshot.GroupSnapshot, group_snapshot, - ignore_missing=ignore_missing) + _group_snapshot.GroupSnapshot, + group_snapshot, + ignore_missing=ignore_missing, + ) # ====== GROUP TYPE ====== def get_group_type(self, group_type): @@ -1395,7 +1411,8 @@ def delete_group_type(self, group_type, ignore_missing=True): :returns: None """ self._delete( - _group_type.GroupType, group_type, ignore_missing=ignore_missing) + _group_type.GroupType, group_type, ignore_missing=ignore_missing + ) def update_group_type(self, group_type, **attrs): """Update a group_type @@ -1408,8 +1425,7 @@ def update_group_type(self, group_type, **attrs): :returns: The updated group type. :rtype: :class:`~openstack.block_storage.v3.group_type.GroupType` """ - return self._update( - _group_type.GroupType, group_type, **attrs) + return self._update(_group_type.GroupType, group_type, **attrs) def fetch_group_type_group_specs(self, group_type): """Lists group specs of a group type. @@ -1488,9 +1504,9 @@ def get_quota_set(self, project, usage=False, **query): """ project = self._get_resource(_project.Project, project) res = self._get_resource( - _quota_set.QuotaSet, None, project_id=project.id) - return res.fetch( - self, usage=usage, **query) + _quota_set.QuotaSet, None, project_id=project.id + ) + return res.fetch(self, usage=usage, **query) def get_quota_set_defaults(self, project): """Show QuotaSet defaults for the project @@ -1505,9 +1521,9 @@ def get_quota_set_defaults(self, project): """ project = self._get_resource(_project.Project, project) res = self._get_resource( - _quota_set.QuotaSet, None, project_id=project.id) - return res.fetch( - self, base_path='/os-quota-sets/defaults') + _quota_set.QuotaSet, None, project_id=project.id + ) + return res.fetch(self, base_path='/os-quota-sets/defaults') def revert_quota_set(self, project, **query): """Reset Quota for the project/user. @@ -1521,7 +1537,8 @@ def revert_quota_set(self, project, **query): """ project = self._get_resource(_project.Project, project) res = self._get_resource( - _quota_set.QuotaSet, None, project_id=project.id) + _quota_set.QuotaSet, None, project_id=project.id + ) return res.delete(self, **query) @@ -1561,7 +1578,12 @@ def extensions(self): # ====== UTILS ====== def wait_for_status( - self, res, status='available', failures=None, interval=2, wait=120, + self, + res, + status='available', + failures=None, + interval=2, + wait=120, ): """Wait for a resource to be in a particular status. @@ -1584,7 +1606,8 @@ def wait_for_status( """ failures = ['error'] if failures is None else failures return resource.wait_for_status( - self, res, status, failures, interval, wait) + self, res, status, failures, interval, wait + ) def wait_for_delete(self, res, interval=2, wait=120): """Wait for a resource to be deleted. @@ -1602,11 +1625,7 @@ def wait_for_delete(self, res, interval=2, wait=120): return resource.wait_for_delete(self, res, interval, wait) def _get_cleanup_dependencies(self): - return { - 'block_storage': { - 'before': [] - } - } + return {'block_storage': {'before': []}} def _service_cleanup( self, @@ -1614,7 +1633,7 @@ def _service_cleanup( client_status_queue=None, identified_resources=None, filters=None, - resource_evaluation_fn=None + resource_evaluation_fn=None, ): # It is not possible to delete backup if there are dependent backups. # In order to be able to do cleanup those is required to have multiple @@ -1634,7 +1653,8 @@ def _service_cleanup( client_status_queue=client_status_queue, identified_resources=identified_resources, filters=filters, - resource_evaluation_fn=resource_evaluation_fn) + resource_evaluation_fn=resource_evaluation_fn, + ) else: # Set initial iterations conditions need_backup_iteration = True @@ -1647,7 +1667,7 @@ def _service_cleanup( # To increase success chance sort backups by age, dependent # backups are logically younger. for obj in self.backups( - details=True, sort_key='created_at', sort_dir='desc' + details=True, sort_key='created_at', sort_dir='desc' ): if not obj.has_dependent_backups: # If no dependent backups - go with it @@ -1658,7 +1678,8 @@ def _service_cleanup( client_status_queue=client_status_queue, identified_resources=identified_resources, filters=filters, - resource_evaluation_fn=resource_evaluation_fn) + resource_evaluation_fn=resource_evaluation_fn, + ) if not dry_run and need_delete: backups.append(obj) else: @@ -1682,7 +1703,8 @@ def _service_cleanup( client_status_queue=client_status_queue, identified_resources=identified_resources, filters=filters, - resource_evaluation_fn=resource_evaluation_fn) + resource_evaluation_fn=resource_evaluation_fn, + ) if not dry_run and need_delete: snapshots.append(obj) @@ -1702,4 +1724,5 @@ def _service_cleanup( client_status_queue=client_status_queue, identified_resources=identified_resources, filters=filters, - resource_evaluation_fn=resource_evaluation_fn) + resource_evaluation_fn=resource_evaluation_fn, + ) diff --git a/openstack/block_storage/v3/backup.py b/openstack/block_storage/v3/backup.py index 9131e5914..74374e490 100644 --- a/openstack/block_storage/v3/backup.py +++ b/openstack/block_storage/v3/backup.py @@ -16,6 +16,7 @@ class Backup(resource.Resource): """Volume Backup""" + resource_key = "backup" resources_key = "backups" base_path = "/backups" @@ -24,9 +25,16 @@ class Backup(resource.Resource): # search (name~, status~, volume_id~). But this is not documented # officially and seem to require microversion be set _query_mapping = resource.QueryParameters( - 'all_tenants', 'limit', 'marker', 'project_id', - 'name', 'status', 'volume_id', - 'sort_key', 'sort_dir') + 'all_tenants', + 'limit', + 'marker', + 'project_id', + 'name', + 'status', + 'volume_id', + 'sort_key', + 'sort_dir', + ) # capabilities allow_fetch = True @@ -111,35 +119,48 @@ def create(self, session, prepend_key=True, base_path=None, **params): session = self._get_session(session) microversion = self._get_microversion(session, action='create') - requires_id = (self.create_requires_id - if self.create_requires_id is not None - else self.create_method == 'PUT') + requires_id = ( + self.create_requires_id + if self.create_requires_id is not None + else self.create_method == 'PUT' + ) if self.create_exclude_id_from_body: self._body._dirty.discard("id") if self.create_method == 'POST': - request = self._prepare_request(requires_id=requires_id, - prepend_key=prepend_key, - base_path=base_path) + request = self._prepare_request( + requires_id=requires_id, + prepend_key=prepend_key, + base_path=base_path, + ) # NOTE(gtema) this is a funny example of when attribute # is called "incremental" on create, "is_incremental" on get # and use of "alias" or "aka" is not working for such conflict, # since our preferred attr name is exactly "is_incremental" body = request.body if 'is_incremental' in body['backup']: - body['backup']['incremental'] = \ - body['backup'].pop('is_incremental') - response = session.post(request.url, - json=request.body, headers=request.headers, - microversion=microversion, params=params) + body['backup']['incremental'] = body['backup'].pop( + 'is_incremental' + ) + response = session.post( + request.url, + json=request.body, + headers=request.headers, + microversion=microversion, + params=params, + ) else: # Just for safety of the implementation (since PUT removed) raise exceptions.ResourceFailure( - "Invalid create method: %s" % self.create_method) - - has_body = (self.has_body if self.create_returns_body is None - else self.create_returns_body) + "Invalid create method: %s" % self.create_method + ) + + has_body = ( + self.has_body + if self.create_returns_body is None + else self.create_returns_body + ) self.microversion = microversion self._translate_response(response, has_body=has_body) # direct comparision to False since we need to rule out None @@ -151,8 +172,9 @@ def create(self, session, prepend_key=True, base_path=None, **params): def _action(self, session, body, microversion=None): """Preform backup actions given the message body.""" url = utils.urljoin(self.base_path, self.id, 'action') - resp = session.post(url, json=body, - microversion=self._max_microversion) + resp = session.post( + url, json=body, microversion=self._max_microversion + ) exceptions.raise_from_response(resp) return resp @@ -171,21 +193,20 @@ def restore(self, session, volume_id=None, name=None): if name: body['restore']['name'] = name if not (volume_id or name): - raise exceptions.SDKException('Either of `name` or `volume_id`' - ' must be specified.') + raise exceptions.SDKException( + 'Either of `name` or `volume_id`' ' must be specified.' + ) response = session.post(url, json=body) self._translate_response(response, has_body=False) return self def force_delete(self, session): - """Force backup deletion - """ + """Force backup deletion""" body = {'os-force_delete': {}} self._action(session, body) def reset(self, session, status): - """Reset the status of the backup - """ + """Reset the status of the backup""" body = {'os-reset_status': {'status': status}} self._action(session, body) diff --git a/openstack/block_storage/v3/extension.py b/openstack/block_storage/v3/extension.py index e2085e15c..a9de9df49 100644 --- a/openstack/block_storage/v3/extension.py +++ b/openstack/block_storage/v3/extension.py @@ -15,6 +15,7 @@ class Extension(resource.Resource): """Extension""" + resources_key = "extensions" base_path = "/extensions" diff --git a/openstack/block_storage/v3/group_snapshot.py b/openstack/block_storage/v3/group_snapshot.py index cb72541b5..8341890be 100644 --- a/openstack/block_storage/v3/group_snapshot.py +++ b/openstack/block_storage/v3/group_snapshot.py @@ -58,10 +58,14 @@ def _action(self, session, body, microversion=None): microversion = session.default_microversion else: microversion = utils.maximum_supported_microversion( - session, self._max_microversion, + session, + self._max_microversion, ) response = session.post( - url, json=body, headers=headers, microversion=microversion, + url, + json=body, + headers=headers, + microversion=microversion, ) exceptions.raise_from_response(response) return response diff --git a/openstack/block_storage/v3/group_type.py b/openstack/block_storage/v3/group_type.py index 3e89c8898..8ff929e38 100644 --- a/openstack/block_storage/v3/group_type.py +++ b/openstack/block_storage/v3/group_type.py @@ -71,7 +71,9 @@ def create_group_specs(self, session, specs): url = utils.urljoin(GroupType.base_path, self.id, 'group_specs') microversion = self._get_microversion(session, action='create') response = session.post( - url, json={'group_specs': specs}, microversion=microversion, + url, + json={'group_specs': specs}, + microversion=microversion, ) exceptions.raise_from_response(response) specs = response.json().get('group_specs', {}) diff --git a/openstack/block_storage/v3/limits.py b/openstack/block_storage/v3/limits.py index 9448669f3..490e4e4b8 100644 --- a/openstack/block_storage/v3/limits.py +++ b/openstack/block_storage/v3/limits.py @@ -17,19 +17,22 @@ class AbsoluteLimit(resource.Resource): #: Properties #: The maximum total amount of backups, in gibibytes (GiB). max_total_backup_gigabytes = resource.Body( - "maxTotalBackupGigabytes", type=int) + "maxTotalBackupGigabytes", type=int + ) #: The maximum number of backups. max_total_backups = resource.Body("maxTotalBackups", type=int) #: The maximum number of snapshots. max_total_snapshots = resource.Body("maxTotalSnapshots", type=int) #: The maximum total amount of volumes, in gibibytes (GiB). max_total_volume_gigabytes = resource.Body( - "maxTotalVolumeGigabytes", type=int) + "maxTotalVolumeGigabytes", type=int + ) #: The maximum number of volumes. max_total_volumes = resource.Body("maxTotalVolumes", type=int) #: The total number of backups gibibytes (GiB) used. total_backup_gigabytes_used = resource.Body( - "totalBackupGigabytesUsed", type=int) + "totalBackupGigabytesUsed", type=int + ) #: The total number of backups used. total_backups_used = resource.Body("totalBackupsUsed", type=int) #: The total number of gibibytes (GiB) used. diff --git a/openstack/block_storage/v3/resource_filter.py b/openstack/block_storage/v3/resource_filter.py index f26b46c09..c5b10ab74 100644 --- a/openstack/block_storage/v3/resource_filter.py +++ b/openstack/block_storage/v3/resource_filter.py @@ -15,6 +15,7 @@ class ResourceFilter(resource.Resource): """Resource Filter""" + resources_key = "resource_filters" base_path = "/resource_filters" diff --git a/openstack/block_storage/v3/snapshot.py b/openstack/block_storage/v3/snapshot.py index 4b81d598e..9028862b5 100644 --- a/openstack/block_storage/v3/snapshot.py +++ b/openstack/block_storage/v3/snapshot.py @@ -23,8 +23,8 @@ class Snapshot(resource.Resource, metadata.MetadataMixin): base_path = "/snapshots" _query_mapping = resource.QueryParameters( - 'name', 'status', 'volume_id', - 'project_id', all_projects='all_tenants') + 'name', 'status', 'volume_id', 'project_id', all_projects='all_tenants' + ) # capabilities allow_fetch = True @@ -58,28 +58,25 @@ class Snapshot(resource.Resource, metadata.MetadataMixin): def _action(self, session, body, microversion=None): """Preform backup actions given the message body.""" url = utils.urljoin(self.base_path, self.id, 'action') - resp = session.post(url, json=body, - microversion=self._max_microversion) + resp = session.post( + url, json=body, microversion=self._max_microversion + ) exceptions.raise_from_response(resp) return resp def force_delete(self, session): - """Force snapshot deletion. - """ + """Force snapshot deletion.""" body = {'os-force_delete': {}} self._action(session, body) def reset(self, session, status): - """Reset the status of the snapshot. - """ + """Reset the status of the snapshot.""" body = {'os-reset_status': {'status': status}} self._action(session, body) def set_status(self, session, status, progress=None): - """Update fields related to the status of a snapshot. - """ - body = {'os-update_snapshot_status': { - 'status': status}} + """Update fields related to the status of a snapshot.""" + body = {'os-update_snapshot_status': {'status': status}} if progress is not None: body['os-update_snapshot_status']['progress'] = progress self._action(session, body) diff --git a/openstack/block_storage/v3/type.py b/openstack/block_storage/v3/type.py index cb757199c..59931373e 100644 --- a/openstack/block_storage/v3/type.py +++ b/openstack/block_storage/v3/type.py @@ -37,13 +37,13 @@ class Type(resource.Resource): #: a private volume-type. *Type: bool* is_public = resource.Body('os-volume-type-access:is_public', type=bool) - def _extra_specs(self, method, key=None, delete=False, - extra_specs=None): + def _extra_specs(self, method, key=None, delete=False, extra_specs=None): extra_specs = extra_specs or {} for k, v in extra_specs.items(): if not isinstance(v, str): - raise ValueError("The value for %s (%s) must be " - "a text string" % (k, v)) + raise ValueError( + "The value for %s (%s) must be " "a text string" % (k, v) + ) if key is not None: url = utils.urljoin(self.base_path, self.id, "extra_specs", key) diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index 15128f818..5fa1337a6 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -23,8 +23,13 @@ class Volume(resource.Resource, metadata.MetadataMixin): base_path = "/volumes" _query_mapping = resource.QueryParameters( - 'name', 'status', 'project_id', 'created_at', 'updated_at', - all_projects='all_tenants') + 'name', + 'status', + 'project_id', + 'created_at', + 'updated_at', + all_projects='all_tenants', + ) # capabilities allow_fetch = True @@ -48,7 +53,8 @@ class Volume(resource.Resource, metadata.MetadataMixin): description = resource.Body("description") #: Extended replication status on this volume. extended_replication_status = resource.Body( - "os-volume-replication:extended_status") + "os-volume-replication:extended_status" + ) #: The ID of the group that the volume belongs to. group_id = resource.Body("group_id") #: The volume's current back-end. @@ -73,7 +79,8 @@ class Volume(resource.Resource, metadata.MetadataMixin): project_id = resource.Body("os-vol-tenant-attr:tenant_id") #: Data set by the replication driver replication_driver_data = resource.Body( - "os-volume-replication:driver_data") + "os-volume-replication:driver_data" + ) #: Status of replication on this volume. replication_status = resource.Body("replication_status") #: Scheduler hints for the volume @@ -108,8 +115,9 @@ def _action(self, session, body, microversion=None): # as both Volume and VolumeDetail instances can be acted on, but # the URL used is sans any additional /detail/ part. url = utils.urljoin(Volume.base_path, self.id, 'action') - resp = session.post(url, json=body, - microversion=self._max_microversion) + resp = session.post( + url, json=body, microversion=self._max_microversion + ) exceptions.raise_from_response(resp) return resp @@ -128,15 +136,15 @@ def set_readonly(self, session, readonly): body = {'os-update_readonly_flag': {'readonly': readonly}} self._action(session, body) - def reset_status( - self, session, status, attach_status, migration_status - ): + def reset_status(self, session, status, attach_status, migration_status): """Reset volume statuses (admin operation)""" - body = {'os-reset_status': { - 'status': status, - 'attach_status': attach_status, - 'migration_status': migration_status - }} + body = { + 'os-reset_status': { + 'status': status, + 'attach_status': attach_status, + 'migration_status': migration_status, + } + } self._action(session, body) def revert_to_snapshot(self, session, snapshot_id): @@ -145,12 +153,9 @@ def revert_to_snapshot(self, session, snapshot_id): body = {'revert': {'snapshot_id': snapshot_id}} self._action(session, body) - def attach( - self, session, mountpoint, instance=None, host_name=None - ): + def attach(self, session, mountpoint, instance=None, host_name=None): """Attach volume to server""" - body = {'os-attach': { - 'mountpoint': mountpoint}} + body = {'os-attach': {'mountpoint': mountpoint}} if instance is not None: body['os-attach']['instance_uuid'] = instance @@ -158,7 +163,8 @@ def attach( body['os-attach']['host_name'] = host_name else: raise ValueError( - 'Either instance_uuid or host_name must be specified') + 'Either instance_uuid or host_name must be specified' + ) self._action(session, body) @@ -167,8 +173,7 @@ def detach(self, session, attachment, force=False, connector=None): if not force: body = {'os-detach': {'attachment_id': attachment}} if force: - body = {'os-force_detach': { - 'attachment_id': attachment}} + body = {'os-force_detach': {'attachment_id': attachment}} if connector: body['os-force_detach']['connector'] = connector @@ -182,16 +187,19 @@ def unmanage(self, session): def retype(self, session, new_type, migration_policy=None): """Change volume type""" - body = {'os-retype': { - 'new_type': new_type}} + body = {'os-retype': {'new_type': new_type}} if migration_policy: body['os-retype']['migration_policy'] = migration_policy self._action(session, body) def migrate( - self, session, host=None, force_host_copy=False, - lock_volume=False, cluster=None + self, + session, + host=None, + force_host_copy=False, + lock_volume=False, + cluster=None, ): """Migrate volume""" req = dict() @@ -210,9 +218,12 @@ def migrate( def complete_migration(self, session, new_volume_id, error=False): """Complete volume migration""" - body = {'os-migrate_volume_completion': { - 'new_volume': new_volume_id, - 'error': error}} + body = { + 'os-migrate_volume_completion': { + 'new_volume': new_volume_id, + 'error': error, + } + } self._action(session, body) @@ -223,8 +234,14 @@ def force_delete(self, session): self._action(session, body) def upload_to_image( - self, session, image_name, force=False, disk_format=None, - container_format=None, visibility=None, protected=None + self, + session, + image_name, + force=False, + disk_format=None, + container_format=None, + visibility=None, + protected=None, ): """Upload the volume to image service""" req = dict(image_name=image_name, force=force) diff --git a/openstack/tests/functional/block_storage/v2/test_backup.py b/openstack/tests/functional/block_storage/v2/test_backup.py index 7684735de..6eb52c767 100644 --- a/openstack/tests/functional/block_storage/v2/test_backup.py +++ b/openstack/tests/functional/block_storage/v2/test_backup.py @@ -16,7 +16,6 @@ class TestBackup(base.BaseBlockStorageTest): - def setUp(self): super(TestBackup, self).setUp() @@ -29,37 +28,39 @@ def setUp(self): self.BACKUP_ID = None volume = self.user_cloud.block_storage.create_volume( - name=self.VOLUME_NAME, - size=1) + name=self.VOLUME_NAME, size=1 + ) self.user_cloud.block_storage.wait_for_status( volume, status='available', failures=['error'], interval=5, - wait=self._wait_for_timeout) + wait=self._wait_for_timeout, + ) assert isinstance(volume, _volume.Volume) self.VOLUME_ID = volume.id backup = self.user_cloud.block_storage.create_backup( - name=self.BACKUP_NAME, - volume_id=volume.id) + name=self.BACKUP_NAME, volume_id=volume.id + ) self.user_cloud.block_storage.wait_for_status( backup, status='available', failures=['error'], interval=5, - wait=self._wait_for_timeout) + wait=self._wait_for_timeout, + ) assert isinstance(backup, _backup.Backup) self.assertEqual(self.BACKUP_NAME, backup.name) self.BACKUP_ID = backup.id def tearDown(self): sot = self.user_cloud.block_storage.delete_backup( - self.BACKUP_ID, - ignore_missing=False) + self.BACKUP_ID, ignore_missing=False + ) sot = self.user_cloud.block_storage.delete_volume( - self.VOLUME_ID, - ignore_missing=False) + self.VOLUME_ID, ignore_missing=False + ) self.assertIsNone(sot) super(TestBackup, self).tearDown() diff --git a/openstack/tests/functional/block_storage/v2/test_snapshot.py b/openstack/tests/functional/block_storage/v2/test_snapshot.py index 18a7a77ab..600fe773d 100644 --- a/openstack/tests/functional/block_storage/v2/test_snapshot.py +++ b/openstack/tests/functional/block_storage/v2/test_snapshot.py @@ -17,7 +17,6 @@ class TestSnapshot(base.BaseBlockStorageTest): - def setUp(self): super(TestSnapshot, self).setUp() @@ -27,26 +26,28 @@ def setUp(self): self.VOLUME_ID = None volume = self.user_cloud.block_storage.create_volume( - name=self.VOLUME_NAME, - size=1) + name=self.VOLUME_NAME, size=1 + ) self.user_cloud.block_storage.wait_for_status( volume, status='available', failures=['error'], interval=2, - wait=self._wait_for_timeout) + wait=self._wait_for_timeout, + ) assert isinstance(volume, _volume.Volume) self.assertEqual(self.VOLUME_NAME, volume.name) self.VOLUME_ID = volume.id snapshot = self.user_cloud.block_storage.create_snapshot( - name=self.SNAPSHOT_NAME, - volume_id=self.VOLUME_ID) + name=self.SNAPSHOT_NAME, volume_id=self.VOLUME_ID + ) self.user_cloud.block_storage.wait_for_status( snapshot, status='available', failures=['error'], interval=2, - wait=self._wait_for_timeout) + wait=self._wait_for_timeout, + ) assert isinstance(snapshot, _snapshot.Snapshot) self.assertEqual(self.SNAPSHOT_NAME, snapshot.name) self.SNAPSHOT_ID = snapshot.id @@ -54,12 +55,15 @@ def setUp(self): def tearDown(self): snapshot = self.user_cloud.block_storage.get_snapshot(self.SNAPSHOT_ID) sot = self.user_cloud.block_storage.delete_snapshot( - snapshot, ignore_missing=False) + snapshot, ignore_missing=False + ) self.user_cloud.block_storage.wait_for_delete( - snapshot, interval=2, wait=self._wait_for_timeout) + snapshot, interval=2, wait=self._wait_for_timeout + ) self.assertIsNone(sot) sot = self.user_cloud.block_storage.delete_volume( - self.VOLUME_ID, ignore_missing=False) + self.VOLUME_ID, ignore_missing=False + ) self.assertIsNone(sot) super(TestSnapshot, self).tearDown() diff --git a/openstack/tests/functional/block_storage/v2/test_stats.py b/openstack/tests/functional/block_storage/v2/test_stats.py index 63175645f..5c8abf3b7 100644 --- a/openstack/tests/functional/block_storage/v2/test_stats.py +++ b/openstack/tests/functional/block_storage/v2/test_stats.py @@ -16,7 +16,6 @@ class TestStats(base.BaseBlockStorageTest): - def setUp(self): super(TestStats, self).setUp() @@ -25,16 +24,28 @@ def setUp(self): self.assertIsInstance(pool, _stats.Pools) def test_list(self): - capList = ['volume_backend_name', 'storage_protocol', - 'free_capacity_gb', 'driver_version', - 'goodness_function', 'QoS_support', - 'vendor_name', 'pool_name', 'thin_provisioning_support', - 'thick_provisioning_support', 'timestamp', - 'max_over_subscription_ratio', 'total_volumes', - 'total_capacity_gb', 'filter_function', - 'multiattach', 'provisioned_capacity_gb', - 'allocated_capacity_gb', 'reserved_percentage', - 'location_info'] + capList = [ + 'volume_backend_name', + 'storage_protocol', + 'free_capacity_gb', + 'driver_version', + 'goodness_function', + 'QoS_support', + 'vendor_name', + 'pool_name', + 'thin_provisioning_support', + 'thick_provisioning_support', + 'timestamp', + 'max_over_subscription_ratio', + 'total_volumes', + 'total_capacity_gb', + 'filter_function', + 'multiattach', + 'provisioned_capacity_gb', + 'allocated_capacity_gb', + 'reserved_percentage', + 'location_info', + ] capList.sort() pools = self.operator_cloud.block_storage.backend_pools() for pool in pools: diff --git a/openstack/tests/functional/block_storage/v2/test_type.py b/openstack/tests/functional/block_storage/v2/test_type.py index ce2052f43..a15cede3a 100644 --- a/openstack/tests/functional/block_storage/v2/test_type.py +++ b/openstack/tests/functional/block_storage/v2/test_type.py @@ -16,7 +16,6 @@ class TestType(base.BaseBlockStorageTest): - def setUp(self): super(TestType, self).setUp() @@ -24,14 +23,16 @@ def setUp(self): self.TYPE_ID = None sot = self.operator_cloud.block_storage.create_type( - name=self.TYPE_NAME) + name=self.TYPE_NAME + ) assert isinstance(sot, _type.Type) self.assertEqual(self.TYPE_NAME, sot.name) self.TYPE_ID = sot.id def tearDown(self): sot = self.operator_cloud.block_storage.delete_type( - self.TYPE_ID, ignore_missing=False) + self.TYPE_ID, ignore_missing=False + ) self.assertIsNone(sot) super(TestType, self).tearDown() diff --git a/openstack/tests/functional/block_storage/v2/test_volume.py b/openstack/tests/functional/block_storage/v2/test_volume.py index 67ac7627b..13f76592a 100644 --- a/openstack/tests/functional/block_storage/v2/test_volume.py +++ b/openstack/tests/functional/block_storage/v2/test_volume.py @@ -15,7 +15,6 @@ class TestVolume(base.BaseBlockStorageTest): - def setUp(self): super(TestVolume, self).setUp() @@ -26,22 +25,23 @@ def setUp(self): self.VOLUME_ID = None volume = self.user_cloud.block_storage.create_volume( - name=self.VOLUME_NAME, - size=1) + name=self.VOLUME_NAME, size=1 + ) self.user_cloud.block_storage.wait_for_status( volume, status='available', failures=['error'], interval=2, - wait=self._wait_for_timeout) + wait=self._wait_for_timeout, + ) assert isinstance(volume, _volume.Volume) self.assertEqual(self.VOLUME_NAME, volume.name) self.VOLUME_ID = volume.id def tearDown(self): sot = self.user_cloud.block_storage.delete_volume( - self.VOLUME_ID, - ignore_missing=False) + self.VOLUME_ID, ignore_missing=False + ) self.assertIsNone(sot) super(TestVolume, self).tearDown() diff --git a/openstack/tests/functional/block_storage/v3/test_availability_zone.py b/openstack/tests/functional/block_storage/v3/test_availability_zone.py index e17c52869..2c3c6e074 100644 --- a/openstack/tests/functional/block_storage/v3/test_availability_zone.py +++ b/openstack/tests/functional/block_storage/v3/test_availability_zone.py @@ -15,7 +15,6 @@ class TestAvailabilityZone(base.BaseFunctionalTest): - def test_list(self): availability_zones = list(self.conn.block_storage.availability_zones()) self.assertGreater(len(availability_zones), 0) diff --git a/openstack/tests/functional/block_storage/v3/test_backup.py b/openstack/tests/functional/block_storage/v3/test_backup.py index 2849419e5..d791bfb42 100644 --- a/openstack/tests/functional/block_storage/v3/test_backup.py +++ b/openstack/tests/functional/block_storage/v3/test_backup.py @@ -16,7 +16,6 @@ class TestBackup(base.BaseBlockStorageTest): - def setUp(self): super(TestBackup, self).setUp() @@ -29,38 +28,39 @@ def setUp(self): self.BACKUP_ID = None volume = self.user_cloud.block_storage.create_volume( - name=self.VOLUME_NAME, - size=1) + name=self.VOLUME_NAME, size=1 + ) self.user_cloud.block_storage.wait_for_status( volume, status='available', failures=['error'], interval=5, - wait=self._wait_for_timeout) + wait=self._wait_for_timeout, + ) assert isinstance(volume, _volume.Volume) self.VOLUME_ID = volume.id backup = self.user_cloud.block_storage.create_backup( - name=self.BACKUP_NAME, - volume_id=volume.id, - is_incremental=False) + name=self.BACKUP_NAME, volume_id=volume.id, is_incremental=False + ) self.user_cloud.block_storage.wait_for_status( backup, status='available', failures=['error'], interval=5, - wait=self._wait_for_timeout) + wait=self._wait_for_timeout, + ) assert isinstance(backup, _backup.Backup) self.assertEqual(self.BACKUP_NAME, backup.name) self.BACKUP_ID = backup.id def tearDown(self): sot = self.user_cloud.block_storage.delete_backup( - self.BACKUP_ID, - ignore_missing=False) + self.BACKUP_ID, ignore_missing=False + ) sot = self.user_cloud.block_storage.delete_volume( - self.VOLUME_ID, - ignore_missing=False) + self.VOLUME_ID, ignore_missing=False + ) self.assertIsNone(sot) super(TestBackup, self).tearDown() @@ -73,31 +73,34 @@ def test_create_metadata(self): metadata_backup = self.user_cloud.block_storage.create_backup( name=self.getUniqueString(), volume_id=self.VOLUME_ID, - metadata=dict(foo="bar")) + metadata=dict(foo="bar"), + ) self.user_cloud.block_storage.wait_for_status( metadata_backup, status='available', failures=['error'], interval=5, - wait=self._wait_for_timeout) + wait=self._wait_for_timeout, + ) self.user_cloud.block_storage.delete_backup( - metadata_backup.id, - ignore_missing=False) + metadata_backup.id, ignore_missing=False + ) def test_create_incremental(self): incremental_backup = self.user_cloud.block_storage.create_backup( name=self.getUniqueString(), volume_id=self.VOLUME_ID, - is_incremental=True) + is_incremental=True, + ) self.user_cloud.block_storage.wait_for_status( incremental_backup, status='available', failures=['error'], interval=5, - wait=self._wait_for_timeout) + wait=self._wait_for_timeout, + ) self.assertEqual(True, incremental_backup.is_incremental) self.user_cloud.block_storage.delete_backup( - incremental_backup.id, - ignore_missing=False) - self.user_cloud.block_storage.wait_for_delete( - incremental_backup) + incremental_backup.id, ignore_missing=False + ) + self.user_cloud.block_storage.wait_for_delete(incremental_backup) diff --git a/openstack/tests/functional/block_storage/v3/test_capabilities.py b/openstack/tests/functional/block_storage/v3/test_capabilities.py index 9dd96172e..3d419080d 100644 --- a/openstack/tests/functional/block_storage/v3/test_capabilities.py +++ b/openstack/tests/functional/block_storage/v3/test_capabilities.py @@ -15,10 +15,10 @@ class TestCapabilities(base.BaseBlockStorageTest): - def test_get(self): - response = ( - proxy._json_response(self.conn.block_storage.get('/os-hosts'))) + response = proxy._json_response( + self.conn.block_storage.get('/os-hosts') + ) host = response['hosts'][0]['host_name'] sot = self.conn.block_storage.get_capabilities(host) diff --git a/openstack/tests/functional/block_storage/v3/test_extension.py b/openstack/tests/functional/block_storage/v3/test_extension.py index 5ad84feda..48ec27118 100644 --- a/openstack/tests/functional/block_storage/v3/test_extension.py +++ b/openstack/tests/functional/block_storage/v3/test_extension.py @@ -14,7 +14,6 @@ class Extensions(base.BaseBlockStorageTest): - def test_get(self): extensions = list(self.conn.block_storage.extensions()) diff --git a/openstack/tests/functional/block_storage/v3/test_limits.py b/openstack/tests/functional/block_storage/v3/test_limits.py index bd7c9fd66..762f4f0b9 100644 --- a/openstack/tests/functional/block_storage/v3/test_limits.py +++ b/openstack/tests/functional/block_storage/v3/test_limits.py @@ -15,7 +15,6 @@ class TestLimits(base.BaseBlockStorageTest): - def test_get(self): sot = self.conn.block_storage.get_limits() self.assertIsNotNone(sot.absolute.max_total_backup_gigabytes) diff --git a/openstack/tests/functional/block_storage/v3/test_resource_filters.py b/openstack/tests/functional/block_storage/v3/test_resource_filters.py index 7380ab1de..9252a104c 100644 --- a/openstack/tests/functional/block_storage/v3/test_resource_filters.py +++ b/openstack/tests/functional/block_storage/v3/test_resource_filters.py @@ -15,7 +15,6 @@ class ResourceFilters(base.BaseBlockStorageTest): - def test_get(self): resource_filters = list(self.conn.block_storage.resource_filters()) diff --git a/openstack/tests/functional/block_storage/v3/test_snapshot.py b/openstack/tests/functional/block_storage/v3/test_snapshot.py index 2ffc1a6d0..1c96ea26e 100644 --- a/openstack/tests/functional/block_storage/v3/test_snapshot.py +++ b/openstack/tests/functional/block_storage/v3/test_snapshot.py @@ -17,7 +17,6 @@ class TestSnapshot(base.BaseBlockStorageTest): - def setUp(self): super(TestSnapshot, self).setUp() @@ -27,26 +26,28 @@ def setUp(self): self.VOLUME_ID = None volume = self.user_cloud.block_storage.create_volume( - name=self.VOLUME_NAME, - size=1) + name=self.VOLUME_NAME, size=1 + ) self.user_cloud.block_storage.wait_for_status( volume, status='available', failures=['error'], interval=2, - wait=self._wait_for_timeout) + wait=self._wait_for_timeout, + ) assert isinstance(volume, _volume.Volume) self.assertEqual(self.VOLUME_NAME, volume.name) self.VOLUME_ID = volume.id snapshot = self.user_cloud.block_storage.create_snapshot( - name=self.SNAPSHOT_NAME, - volume_id=self.VOLUME_ID) + name=self.SNAPSHOT_NAME, volume_id=self.VOLUME_ID + ) self.user_cloud.block_storage.wait_for_status( snapshot, status='available', failures=['error'], interval=2, - wait=self._wait_for_timeout) + wait=self._wait_for_timeout, + ) assert isinstance(snapshot, _snapshot.Snapshot) self.assertEqual(self.SNAPSHOT_NAME, snapshot.name) self.SNAPSHOT_ID = snapshot.id @@ -54,12 +55,15 @@ def setUp(self): def tearDown(self): snapshot = self.user_cloud.block_storage.get_snapshot(self.SNAPSHOT_ID) sot = self.user_cloud.block_storage.delete_snapshot( - snapshot, ignore_missing=False) + snapshot, ignore_missing=False + ) self.user_cloud.block_storage.wait_for_delete( - snapshot, interval=2, wait=self._wait_for_timeout) + snapshot, interval=2, wait=self._wait_for_timeout + ) self.assertIsNone(sot) sot = self.user_cloud.block_storage.delete_volume( - self.VOLUME_ID, ignore_missing=False) + self.VOLUME_ID, ignore_missing=False + ) self.assertIsNone(sot) super(TestSnapshot, self).tearDown() diff --git a/openstack/tests/functional/block_storage/v3/test_type.py b/openstack/tests/functional/block_storage/v3/test_type.py index db7ae74f0..97156f3c1 100644 --- a/openstack/tests/functional/block_storage/v3/test_type.py +++ b/openstack/tests/functional/block_storage/v3/test_type.py @@ -16,7 +16,6 @@ class TestType(base.BaseBlockStorageTest): - def setUp(self): super(TestType, self).setUp() @@ -26,14 +25,16 @@ def setUp(self): self.skip("Operator cloud must be set for this test") self._set_operator_cloud(block_storage_api_version='3') sot = self.operator_cloud.block_storage.create_type( - name=self.TYPE_NAME) + name=self.TYPE_NAME + ) assert isinstance(sot, _type.Type) self.assertEqual(self.TYPE_NAME, sot.name) self.TYPE_ID = sot.id def tearDown(self): sot = self.operator_cloud.block_storage.delete_type( - self.TYPE_ID, ignore_missing=False) + self.TYPE_ID, ignore_missing=False + ) self.assertIsNone(sot) super(TestType, self).tearDown() diff --git a/openstack/tests/functional/block_storage/v3/test_volume.py b/openstack/tests/functional/block_storage/v3/test_volume.py index c38ca1e6a..ec4caf0da 100644 --- a/openstack/tests/functional/block_storage/v3/test_volume.py +++ b/openstack/tests/functional/block_storage/v3/test_volume.py @@ -15,7 +15,6 @@ class TestVolume(base.BaseBlockStorageTest): - def setUp(self): super().setUp() diff --git a/openstack/tests/unit/block_storage/v2/test_backup.py b/openstack/tests/unit/block_storage/v2/test_backup.py index 3a937e408..b877a32d3 100644 --- a/openstack/tests/unit/block_storage/v2/test_backup.py +++ b/openstack/tests/unit/block_storage/v2/test_backup.py @@ -35,12 +35,11 @@ "status": "available", "volume_id": "e5185058-943a-4cb4-96d9-72c184c337d6", "is_incremental": True, - "has_dependent_backups": False + "has_dependent_backups": False, } class TestBackup(base.TestCase): - def setUp(self): super(TestBackup, self).setUp() self.resp = mock.Mock() @@ -75,9 +74,9 @@ def test_basic(self): "sort_dir": "sort_dir", "sort_key": "sort_key", "status": "status", - "volume_id": "volume_id" + "volume_id": "volume_id", }, - sot._query_mapping._mapping + sot._query_mapping._mapping, ) def test_create(self): @@ -95,8 +94,9 @@ def test_create(self): self.assertEqual(BACKUP["object_count"], sot.object_count) self.assertEqual(BACKUP["is_incremental"], sot.is_incremental) self.assertEqual(BACKUP["size"], sot.size) - self.assertEqual(BACKUP["has_dependent_backups"], - sot.has_dependent_backups) + self.assertEqual( + BACKUP["has_dependent_backups"], sot.has_dependent_backups + ) def test_create_incremental(self): sot = backup.Backup(is_incremental=True) @@ -118,7 +118,7 @@ def test_create_incremental(self): } }, microversion=None, - params={} + params={}, ) sot2.create(self.sess) @@ -131,7 +131,7 @@ def test_create_incremental(self): } }, microversion=None, - params={} + params={}, ) def test_restore(self): @@ -164,11 +164,7 @@ def test_restore_vol_id(self): def test_restore_no_params(self): sot = backup.Backup(**BACKUP) - self.assertRaises( - exceptions.SDKException, - sot.restore, - self.sess - ) + self.assertRaises(exceptions.SDKException, sot.restore, self.sess) def test_force_delete(self): sot = backup.Backup(**BACKUP) @@ -178,7 +174,8 @@ def test_force_delete(self): url = 'backups/%s/action' % FAKE_ID body = {'os-force_delete': {}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_reset(self): sot = backup.Backup(**BACKUP) @@ -188,4 +185,5 @@ def test_reset(self): url = 'backups/%s/action' % FAKE_ID body = {'os-reset_status': {'status': 'new_status'}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index c65675e9a..0dad6787c 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -23,14 +23,12 @@ class TestVolumeProxy(test_proxy_base.TestProxyBase): - def setUp(self): super().setUp() self.proxy = _proxy.Proxy(self.session) class TestVolume(TestVolumeProxy): - def test_volume_get(self): self.verify_get(self.proxy.get_volume, volume.Volume) @@ -53,7 +51,7 @@ def test_volumes_detailed(self): expected_kwargs={ "base_path": "/volumes/detail", "all_projects": True, - } + }, ) def test_volumes_not_detailed(self): @@ -79,7 +77,7 @@ def test_volume_delete_force(self): self.proxy.delete_volume, method_args=["value"], method_kwargs={"force": True}, - expected_args=[self.proxy] + expected_args=[self.proxy], ) def test_get_volume_metadata(self): @@ -88,7 +86,8 @@ def test_get_volume_metadata(self): self.proxy.get_volume_metadata, method_args=["value"], expected_args=[self.proxy], - expected_result=volume.Volume(id="value", metadata={})) + expected_result=volume.Volume(id="value", metadata={}), + ) def test_set_volume_metadata(self): kwargs = {"a": "1", "b": "2"} @@ -98,12 +97,11 @@ def test_set_volume_metadata(self): self.proxy.set_volume_metadata, method_args=[id], method_kwargs=kwargs, - method_result=volume.Volume.existing( - id=id, metadata=kwargs), + method_result=volume.Volume.existing(id=id, metadata=kwargs), expected_args=[self.proxy], expected_kwargs={'metadata': kwargs}, - expected_result=volume.Volume.existing( - id=id, metadata=kwargs)) + expected_result=volume.Volume.existing(id=id, metadata=kwargs), + ) def test_delete_volume_metadata(self): self._verify( @@ -111,7 +109,8 @@ def test_delete_volume_metadata(self): self.proxy.delete_volume_metadata, expected_result=None, method_args=["value", ["key"]], - expected_args=[self.proxy, "key"]) + expected_args=[self.proxy, "key"], + ) def test_backend_pools(self): self.verify_list(self.proxy.backend_pools, stats.Pools) @@ -121,31 +120,34 @@ def test_volume_wait_for(self): self.verify_wait_for_status( self.proxy.wait_for_status, method_args=[value], - expected_args=[self.proxy, value, 'available', ['error'], 2, 120]) + expected_args=[self.proxy, value, 'available', ['error'], 2, 120], + ) class TestVolumeActions(TestVolumeProxy): - def test_volume_extend(self): self._verify( "openstack.block_storage.v2.volume.Volume.extend", self.proxy.extend_volume, method_args=["value", "new-size"], - expected_args=[self.proxy, "new-size"]) + expected_args=[self.proxy, "new-size"], + ) def test_volume_set_bootable(self): self._verify( "openstack.block_storage.v2.volume.Volume.set_bootable_status", self.proxy.set_volume_bootable_status, method_args=["value", True], - expected_args=[self.proxy, True]) + expected_args=[self.proxy, True], + ) def test_volume_reset_volume_status(self): self._verify( "openstack.block_storage.v2.volume.Volume.reset_status", self.proxy.reset_volume_status, method_args=["value", '1', '2', '3'], - expected_args=[self.proxy, '1', '2', '3']) + expected_args=[self.proxy, '1', '2', '3'], + ) def test_attach_instance(self): self._verify( @@ -153,7 +155,8 @@ def test_attach_instance(self): self.proxy.attach_volume, method_args=["value", '1'], method_kwargs={'instance': '2'}, - expected_args=[self.proxy, '1', '2', None]) + expected_args=[self.proxy, '1', '2', None], + ) def test_attach_host(self): self._verify( @@ -161,60 +164,67 @@ def test_attach_host(self): self.proxy.attach_volume, method_args=["value", '1'], method_kwargs={'host_name': '3'}, - expected_args=[self.proxy, '1', None, '3']) + expected_args=[self.proxy, '1', None, '3'], + ) def test_detach_defaults(self): self._verify( "openstack.block_storage.v2.volume.Volume.detach", self.proxy.detach_volume, method_args=["value", '1'], - expected_args=[self.proxy, '1', False, None]) + expected_args=[self.proxy, '1', False, None], + ) def test_detach_force(self): self._verify( "openstack.block_storage.v2.volume.Volume.detach", self.proxy.detach_volume, method_args=["value", '1', True, {'a': 'b'}], - expected_args=[self.proxy, '1', True, {'a': 'b'}]) + expected_args=[self.proxy, '1', True, {'a': 'b'}], + ) def test_unmanage(self): self._verify( "openstack.block_storage.v2.volume.Volume.unmanage", self.proxy.unmanage_volume, method_args=["value"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_migrate_default(self): self._verify( "openstack.block_storage.v2.volume.Volume.migrate", self.proxy.migrate_volume, method_args=["value", '1'], - expected_args=[self.proxy, '1', False, False]) + expected_args=[self.proxy, '1', False, False], + ) def test_migrate_nondefault(self): self._verify( "openstack.block_storage.v2.volume.Volume.migrate", self.proxy.migrate_volume, method_args=["value", '1', True, True], - expected_args=[self.proxy, '1', True, True]) + expected_args=[self.proxy, '1', True, True], + ) def test_complete_migration(self): self._verify( "openstack.block_storage.v2.volume.Volume.complete_migration", self.proxy.complete_volume_migration, method_args=["value", '1'], - expected_args=[self.proxy, "1", False]) + expected_args=[self.proxy, "1", False], + ) def test_complete_migration_error(self): self._verify( "openstack.block_storage.v2.volume.Volume.complete_migration", self.proxy.complete_volume_migration, method_args=["value", "1", True], - expected_args=[self.proxy, "1", True]) + expected_args=[self.proxy, "1", True], + ) class TestBackup(TestVolumeProxy): - def test_backups_detailed(self): self.verify_list( self.proxy.backups, @@ -253,7 +263,7 @@ def test_backup_delete_force(self): self.proxy.delete_backup, method_args=["value"], method_kwargs={"force": True}, - expected_args=[self.proxy] + expected_args=[self.proxy], ) def test_backup_create_attrs(self): @@ -266,7 +276,7 @@ def test_backup_restore(self): method_args=['volume_id'], method_kwargs={'volume_id': 'vol_id', 'name': 'name'}, expected_args=[self.proxy], - expected_kwargs={'volume_id': 'vol_id', 'name': 'name'} + expected_kwargs={'volume_id': 'vol_id', 'name': 'name'}, ) def test_backup_reset(self): @@ -279,7 +289,6 @@ def test_backup_reset(self): class TestSnapshot(TestVolumeProxy): - def test_snapshot_get(self): self.verify_get(self.proxy.get_snapshot, snapshot.Snapshot) @@ -314,19 +323,20 @@ def test_snapshot_create_attrs(self): self.verify_create(self.proxy.create_snapshot, snapshot.Snapshot) def test_snapshot_delete(self): - self.verify_delete(self.proxy.delete_snapshot, - snapshot.Snapshot, False) + self.verify_delete( + self.proxy.delete_snapshot, snapshot.Snapshot, False + ) def test_snapshot_delete_ignore(self): - self.verify_delete(self.proxy.delete_snapshot, - snapshot.Snapshot, True) + self.verify_delete(self.proxy.delete_snapshot, snapshot.Snapshot, True) def test_reset(self): self._verify( "openstack.block_storage.v2.snapshot.Snapshot.reset", self.proxy.reset_snapshot, method_args=["value", "new_status"], - expected_args=[self.proxy, "new_status"]) + expected_args=[self.proxy, "new_status"], + ) def test_get_snapshot_metadata(self): self._verify( @@ -334,7 +344,8 @@ def test_get_snapshot_metadata(self): self.proxy.get_snapshot_metadata, method_args=["value"], expected_args=[self.proxy], - expected_result=snapshot.Snapshot(id="value", metadata={})) + expected_result=snapshot.Snapshot(id="value", metadata={}), + ) def test_set_snapshot_metadata(self): kwargs = {"a": "1", "b": "2"} @@ -344,12 +355,11 @@ def test_set_snapshot_metadata(self): self.proxy.set_snapshot_metadata, method_args=[id], method_kwargs=kwargs, - method_result=snapshot.Snapshot.existing( - id=id, metadata=kwargs), + method_result=snapshot.Snapshot.existing(id=id, metadata=kwargs), expected_args=[self.proxy], expected_kwargs={'metadata': kwargs}, - expected_result=snapshot.Snapshot.existing( - id=id, metadata=kwargs)) + expected_result=snapshot.Snapshot.existing(id=id, metadata=kwargs), + ) def test_delete_snapshot_metadata(self): self._verify( @@ -358,11 +368,11 @@ def test_delete_snapshot_metadata(self): self.proxy.delete_snapshot_metadata, expected_result=None, method_args=["value", ["key"]], - expected_args=[self.proxy, "key"]) + expected_args=[self.proxy, "key"], + ) class TestType(TestVolumeProxy): - def test_type_get(self): self.verify_get(self.proxy.get_type, type.Type) @@ -383,25 +393,27 @@ def test_type_get_private_access(self): "openstack.block_storage.v2.type.Type.get_private_access", self.proxy.get_type_access, method_args=["value"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_type_add_private_access(self): self._verify( "openstack.block_storage.v2.type.Type.add_private_access", self.proxy.add_type_access, method_args=["value", "a"], - expected_args=[self.proxy, "a"]) + expected_args=[self.proxy, "a"], + ) def test_type_remove_private_access(self): self._verify( "openstack.block_storage.v2.type.Type.remove_private_access", self.proxy.remove_type_access, method_args=["value", "a"], - expected_args=[self.proxy, "a"]) + expected_args=[self.proxy, "a"], + ) class TestQuota(TestVolumeProxy): - def test_get(self): self._verify( 'openstack.resource.Resource.fetch', @@ -414,7 +426,7 @@ def test_get(self): 'usage': False, }, method_result=quota_set.QuotaSet(), - expected_result=quota_set.QuotaSet() + expected_result=quota_set.QuotaSet(), ) def test_get_query(self): @@ -422,17 +434,14 @@ def test_get_query(self): 'openstack.resource.Resource.fetch', self.proxy.get_quota_set, method_args=['prj'], - method_kwargs={ - 'usage': True, - 'user_id': 'uid' - }, + method_kwargs={'usage': True, 'user_id': 'uid'}, expected_args=[self.proxy], expected_kwargs={ 'error_message': None, 'requires_id': False, 'usage': True, - 'user_id': 'uid' - } + 'user_id': 'uid', + }, ) def test_get_defaults(self): @@ -444,8 +453,8 @@ def test_get_defaults(self): expected_kwargs={ 'error_message': None, 'requires_id': False, - 'base_path': '/os-quota-sets/defaults' - } + 'base_path': '/os-quota-sets/defaults', + }, ) def test_reset(self): @@ -455,9 +464,7 @@ def test_reset(self): method_args=['prj'], method_kwargs={'user_id': 'uid'}, expected_args=[self.proxy], - expected_kwargs={ - 'user_id': 'uid' - } + expected_kwargs={'user_id': 'uid'}, ) @mock.patch('openstack.proxy.Proxy._get_resource', autospec=True) @@ -473,12 +480,6 @@ def test_update(self, gr_mock): 'a': 'b', }, expected_args=[self.proxy], - expected_kwargs={ - 'user_id': 'uid' - } - ) - gr_mock.assert_called_with( - self.proxy, - quota_set.QuotaSet, - 'qs', a='b' + expected_kwargs={'user_id': 'uid'}, ) + gr_mock.assert_called_with(self.proxy, quota_set.QuotaSet, 'qs', a='b') diff --git a/openstack/tests/unit/block_storage/v2/test_snapshot.py b/openstack/tests/unit/block_storage/v2/test_snapshot.py index 408264ec5..f81522f4d 100644 --- a/openstack/tests/unit/block_storage/v2/test_snapshot.py +++ b/openstack/tests/unit/block_storage/v2/test_snapshot.py @@ -34,8 +34,7 @@ DETAILS = { "os-extended-snapshot-attributes:progress": "100%", - "os-extended-snapshot-attributes:project_id": - "0c2eba2c5af04d3f9e9d0d410b371fde" + "os-extended-snapshot-attributes:project_id": "0c2eba2c5af04d3f9e9d0d410b371fde", # noqa: E501 } DETAILED_SNAPSHOT = SNAPSHOT.copy() @@ -43,7 +42,6 @@ class TestSnapshot(base.TestCase): - def test_basic(self): sot = snapshot.Snapshot(SNAPSHOT) self.assertEqual("snapshot", sot.resource_key) @@ -55,13 +53,17 @@ def test_basic(self): self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertDictEqual({"name": "name", - "status": "status", - "all_projects": "all_tenants", - "volume_id": "volume_id", - "limit": "limit", - "marker": "marker"}, - sot._query_mapping._mapping) + self.assertDictEqual( + { + "name": "name", + "status": "status", + "all_projects": "all_tenants", + "volume_id": "volume_id", + "limit": "limit", + "marker": "marker", + }, + sot._query_mapping._mapping, + ) def test_create_basic(self): sot = snapshot.Snapshot(**SNAPSHOT) @@ -77,7 +79,6 @@ def test_create_basic(self): class TestSnapshotActions(base.TestCase): - def setUp(self): super(TestSnapshotActions, self).setUp() self.resp = mock.Mock() @@ -99,4 +100,5 @@ def test_reset(self): url = 'snapshots/%s/action' % FAKE_ID body = {'os-reset_status': {'status': 'new_status'}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) diff --git a/openstack/tests/unit/block_storage/v2/test_stats.py b/openstack/tests/unit/block_storage/v2/test_stats.py index 10ea9d75d..029fcaba2 100644 --- a/openstack/tests/unit/block_storage/v2/test_stats.py +++ b/openstack/tests/unit/block_storage/v2/test_stats.py @@ -14,22 +14,22 @@ from openstack.tests.unit import base -POOLS = {"name": "pool1", - "capabilities": { - "updated": "2014-10-28T00=00=00-00=00", - "total_capacity": 1024, - "free_capacity": 100, - "volume_backend_name": "pool1", - "reserved_percentage": "0", - "driver_version": "1.0.0", - "storage_protocol": "iSCSI", - "QoS_support": "false" - } - } +POOLS = { + "name": "pool1", + "capabilities": { + "updated": "2014-10-28T00=00=00-00=00", + "total_capacity": 1024, + "free_capacity": 100, + "volume_backend_name": "pool1", + "reserved_percentage": "0", + "driver_version": "1.0.0", + "storage_protocol": "iSCSI", + "QoS_support": "false", + }, +} class TestBackendPools(base.TestCase): - def setUp(self): super(TestBackendPools, self).setUp() @@ -37,8 +37,9 @@ def test_basic(self): sot = stats.Pools(POOLS) self.assertEqual("", sot.resource_key) self.assertEqual("pools", sot.resources_key) - self.assertEqual("/scheduler-stats/get_pools?detail=True", - sot.base_path) + self.assertEqual( + "/scheduler-stats/get_pools?detail=True", sot.base_path + ) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_delete) diff --git a/openstack/tests/unit/block_storage/v2/test_type.py b/openstack/tests/unit/block_storage/v2/test_type.py index 337e982b3..967b4e143 100644 --- a/openstack/tests/unit/block_storage/v2/test_type.py +++ b/openstack/tests/unit/block_storage/v2/test_type.py @@ -19,17 +19,10 @@ FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff" -TYPE = { - "extra_specs": { - "capabilities": "gpu" - }, - "id": FAKE_ID, - "name": "SSD" -} +TYPE = {"extra_specs": {"capabilities": "gpu"}, "id": FAKE_ID, "name": "SSD"} class TestType(base.TestCase): - def setUp(self): super(TestType, self).setUp() self.extra_specs_result = {"extra_specs": {"go": "cubs", "boo": "sox"}} @@ -68,17 +61,20 @@ def test_get_private_access(self): response = mock.Mock() response.status_code = 200 - response.body = {"volume_type_access": [ - {"project_id": "a", "volume_type_id": "b"} - ]} + response.body = { + "volume_type_access": [{"project_id": "a", "volume_type_id": "b"}] + } response.json = mock.Mock(return_value=response.body) self.sess.get = mock.Mock(return_value=response) - self.assertEqual(response.body["volume_type_access"], - sot.get_private_access(self.sess)) + self.assertEqual( + response.body["volume_type_access"], + sot.get_private_access(self.sess), + ) self.sess.get.assert_called_with( - "types/%s/os-volume-type-access" % sot.id) + "types/%s/os-volume-type-access" % sot.id + ) def test_add_private_access(self): sot = type.Type(**TYPE) @@ -87,8 +83,7 @@ def test_add_private_access(self): url = "types/%s/action" % sot.id body = {"addProjectAccess": {"project": "a"}} - self.sess.post.assert_called_with( - url, json=body) + self.sess.post.assert_called_with(url, json=body) def test_remove_private_access(self): sot = type.Type(**TYPE) @@ -97,5 +92,4 @@ def test_remove_private_access(self): url = "types/%s/action" % sot.id body = {"removeProjectAccess": {"project": "a"}} - self.sess.post.assert_called_with( - url, json=body) + self.sess.post.assert_called_with(url, json=body) diff --git a/openstack/tests/unit/block_storage/v2/test_volume.py b/openstack/tests/unit/block_storage/v2/test_volume.py index a411f98ac..41e59f143 100644 --- a/openstack/tests/unit/block_storage/v2/test_volume.py +++ b/openstack/tests/unit/block_storage/v2/test_volume.py @@ -20,11 +20,13 @@ FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff" IMAGE_METADATA = { 'container_format': 'bare', - 'min_ram': '64', 'disk_format': u'qcow2', + 'min_ram': '64', + 'disk_format': u'qcow2', 'image_name': 'TestVM', 'image_id': '625d4f2c-cf67-4af3-afb6-c7220f766947', 'checksum': '64d7c1cd2b6f60c92c14662941cb7913', - 'min_disk': '0', u'size': '13167616' + 'min_disk': '0', + u'size': '13167616', } VOLUME = { @@ -57,14 +59,13 @@ "OS-SCH-HNT:scheduler_hints": { "same_host": [ "a0cf03a5-d921-4877-bb5c-86d26cf818e1", - "8c19174f-4220-44f0-824a-cd1eeef10287" + "8c19174f-4220-44f0-824a-cd1eeef10287", ] - } + }, } class TestVolume(base.TestCase): - def test_basic(self): sot = volume.Volume(VOLUME) self.assertEqual("volume", sot.resource_key) @@ -76,13 +77,17 @@ def test_basic(self): self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertDictEqual({"name": "name", - "status": "status", - "all_projects": "all_tenants", - "project_id": "project_id", - "limit": "limit", - "marker": "marker"}, - sot._query_mapping._mapping) + self.assertDictEqual( + { + "name": "name", + "status": "status", + "all_projects": "all_tenants", + "project_id": "project_id", + "limit": "limit", + "marker": "marker", + }, + sot._query_mapping._mapping, + ) def test_create(self): sot = volume.Volume(**VOLUME) @@ -98,33 +103,40 @@ def test_create(self): self.assertEqual(VOLUME["snapshot_id"], sot.snapshot_id) self.assertEqual(VOLUME["source_volid"], sot.source_volume_id) self.assertEqual(VOLUME["metadata"], sot.metadata) - self.assertEqual(VOLUME["volume_image_metadata"], - sot.volume_image_metadata) + self.assertEqual( + VOLUME["volume_image_metadata"], sot.volume_image_metadata + ) self.assertEqual(VOLUME["size"], sot.size) self.assertEqual(VOLUME["imageRef"], sot.image_id) self.assertEqual(VOLUME["os-vol-host-attr:host"], sot.host) - self.assertEqual(VOLUME["os-vol-tenant-attr:tenant_id"], - sot.project_id) - self.assertEqual(VOLUME["os-vol-mig-status-attr:migstat"], - sot.migration_status) - self.assertEqual(VOLUME["os-vol-mig-status-attr:name_id"], - sot.migration_id) - self.assertEqual(VOLUME["replication_status"], - sot.replication_status) + self.assertEqual( + VOLUME["os-vol-tenant-attr:tenant_id"], sot.project_id + ) + self.assertEqual( + VOLUME["os-vol-mig-status-attr:migstat"], sot.migration_status + ) + self.assertEqual( + VOLUME["os-vol-mig-status-attr:name_id"], sot.migration_id + ) + self.assertEqual(VOLUME["replication_status"], sot.replication_status) self.assertEqual( VOLUME["os-volume-replication:extended_status"], - sot.extended_replication_status) - self.assertEqual(VOLUME["consistencygroup_id"], - sot.consistency_group_id) - self.assertEqual(VOLUME["os-volume-replication:driver_data"], - sot.replication_driver_data) - self.assertDictEqual(VOLUME["OS-SCH-HNT:scheduler_hints"], - sot.scheduler_hints) + sot.extended_replication_status, + ) + self.assertEqual( + VOLUME["consistencygroup_id"], sot.consistency_group_id + ) + self.assertEqual( + VOLUME["os-volume-replication:driver_data"], + sot.replication_driver_data, + ) + self.assertDictEqual( + VOLUME["OS-SCH-HNT:scheduler_hints"], sot.scheduler_hints + ) self.assertFalse(sot.is_encrypted) class TestVolumeActions(TestVolume): - def setUp(self): super(TestVolumeActions, self).setUp() self.resp = mock.Mock() @@ -144,7 +156,8 @@ def test_extend(self): url = 'volumes/%s/action' % FAKE_ID body = {"os-extend": {"new_size": "20"}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_set_volume_bootable(self): sot = volume.Volume(**VOLUME) @@ -154,7 +167,8 @@ def test_set_volume_bootable(self): url = 'volumes/%s/action' % FAKE_ID body = {'os-set_bootable': {'bootable': True}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_set_volume_bootable_false(self): sot = volume.Volume(**VOLUME) @@ -164,7 +178,8 @@ def test_set_volume_bootable_false(self): url = 'volumes/%s/action' % FAKE_ID body = {'os-set_bootable': {'bootable': False}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_reset_status(self): sot = volume.Volume(**VOLUME) @@ -172,10 +187,16 @@ def test_reset_status(self): self.assertIsNone(sot.reset_status(self.sess, '1', '2', '3')) url = 'volumes/%s/action' % FAKE_ID - body = {'os-reset_status': {'status': '1', 'attach_status': '2', - 'migration_status': '3'}} + body = { + 'os-reset_status': { + 'status': '1', + 'attach_status': '2', + 'migration_status': '3', + } + } self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_attach_instance(self): sot = volume.Volume(**VOLUME) @@ -185,7 +206,8 @@ def test_attach_instance(self): url = 'volumes/%s/action' % FAKE_ID body = {'os-attach': {'mountpoint': '1', 'instance_uuid': '2'}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_detach(self): sot = volume.Volume(**VOLUME) @@ -195,18 +217,19 @@ def test_detach(self): url = 'volumes/%s/action' % FAKE_ID body = {'os-detach': {'attachment_id': '1'}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_detach_force(self): sot = volume.Volume(**VOLUME) - self.assertIsNone( - sot.detach(self.sess, '1', force=True)) + self.assertIsNone(sot.detach(self.sess, '1', force=True)) url = 'volumes/%s/action' % FAKE_ID body = {'os-force_detach': {'attachment_id': '1'}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_unmanage(self): sot = volume.Volume(**VOLUME) @@ -216,7 +239,8 @@ def test_unmanage(self): url = 'volumes/%s/action' % FAKE_ID body = {'os-unmanage': {}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_retype(self): sot = volume.Volume(**VOLUME) @@ -226,7 +250,8 @@ def test_retype(self): url = 'volumes/%s/action' % FAKE_ID body = {'os-retype': {'new_type': '1'}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_retype_mp(self): sot = volume.Volume(**VOLUME) @@ -236,7 +261,8 @@ def test_retype_mp(self): url = 'volumes/%s/action' % FAKE_ID body = {'os-retype': {'new_type': '1', 'migration_policy': '2'}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_migrate(self): sot = volume.Volume(**VOLUME) @@ -246,19 +272,29 @@ def test_migrate(self): url = 'volumes/%s/action' % FAKE_ID body = {'os-migrate_volume': {'host': '1'}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_migrate_flags(self): sot = volume.Volume(**VOLUME) - self.assertIsNone(sot.migrate(self.sess, host='1', - force_host_copy=True, lock_volume=True)) + self.assertIsNone( + sot.migrate( + self.sess, host='1', force_host_copy=True, lock_volume=True + ) + ) url = 'volumes/%s/action' % FAKE_ID - body = {'os-migrate_volume': {'host': '1', 'force_host_copy': True, - 'lock_volume': True}} + body = { + 'os-migrate_volume': { + 'host': '1', + 'force_host_copy': True, + 'lock_volume': True, + } + } self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_complete_migration(self): sot = volume.Volume(**VOLUME) @@ -266,22 +302,27 @@ def test_complete_migration(self): self.assertIsNone(sot.complete_migration(self.sess, new_volume_id='1')) url = 'volumes/%s/action' % FAKE_ID - body = {'os-migrate_volume_completion': {'new_volume': '1', 'error': - False}} + body = { + 'os-migrate_volume_completion': {'new_volume': '1', 'error': False} + } self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_complete_migration_error(self): sot = volume.Volume(**VOLUME) - self.assertIsNone(sot.complete_migration( - self.sess, new_volume_id='1', error=True)) + self.assertIsNone( + sot.complete_migration(self.sess, new_volume_id='1', error=True) + ) url = 'volumes/%s/action' % FAKE_ID - body = {'os-migrate_volume_completion': {'new_volume': '1', 'error': - True}} + body = { + 'os-migrate_volume_completion': {'new_volume': '1', 'error': True} + } self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_force_delete(self): sot = volume.Volume(**VOLUME) @@ -291,4 +332,5 @@ def test_force_delete(self): url = 'volumes/%s/action' % FAKE_ID body = {'os-force_delete': {}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) diff --git a/openstack/tests/unit/block_storage/v3/test_availability_zone.py b/openstack/tests/unit/block_storage/v3/test_availability_zone.py index 26db80615..f7431729c 100644 --- a/openstack/tests/unit/block_storage/v3/test_availability_zone.py +++ b/openstack/tests/unit/block_storage/v3/test_availability_zone.py @@ -17,15 +17,12 @@ IDENTIFIER = 'IDENTIFIER' EXAMPLE = { "id": IDENTIFIER, - "zoneState": { - "available": True - }, - "zoneName": "zone1" + "zoneState": {"available": True}, + "zoneName": "zone1", } class TestAvailabilityZone(base.TestCase): - def test_basic(self): sot = az.AvailabilityZone() self.assertEqual('availabilityZoneInfo', sot.resources_key) diff --git a/openstack/tests/unit/block_storage/v3/test_backup.py b/openstack/tests/unit/block_storage/v3/test_backup.py index 9e7174d8d..7ba33e6e2 100644 --- a/openstack/tests/unit/block_storage/v3/test_backup.py +++ b/openstack/tests/unit/block_storage/v3/test_backup.py @@ -39,12 +39,11 @@ "has_dependent_backups": False, "os-backup-project-attr:project_id": "2c67a14be9314c5dae2ee6c4ec90cf0b", "user_id": "515ba0dd59f84f25a6a084a45d8d93b2", - "metadata": {"key": "value"} + "metadata": {"key": "value"}, } class TestBackup(base.TestCase): - def setUp(self): super(TestBackup, self).setUp() self.resp = mock.Mock() @@ -80,9 +79,9 @@ def test_basic(self): "sort_dir": "sort_dir", "sort_key": "sort_key", "status": "status", - "volume_id": "volume_id" + "volume_id": "volume_id", }, - sot._query_mapping._mapping + sot._query_mapping._mapping, ) def test_create(self): @@ -100,10 +99,12 @@ def test_create(self): self.assertEqual(BACKUP["object_count"], sot.object_count) self.assertEqual(BACKUP["is_incremental"], sot.is_incremental) self.assertEqual(BACKUP["size"], sot.size) - self.assertEqual(BACKUP["has_dependent_backups"], - sot.has_dependent_backups) - self.assertEqual(BACKUP['os-backup-project-attr:project_id'], - sot.project_id) + self.assertEqual( + BACKUP["has_dependent_backups"], sot.has_dependent_backups + ) + self.assertEqual( + BACKUP['os-backup-project-attr:project_id'], sot.project_id + ) self.assertEqual(BACKUP['metadata'], sot.metadata) self.assertEqual(BACKUP['user_id'], sot.user_id) self.assertEqual(BACKUP['encryption_key_id'], sot.encryption_key_id) @@ -128,7 +129,7 @@ def test_create_incremental(self): } }, microversion="3.64", - params={} + params={}, ) sot2.create(self.sess) @@ -141,7 +142,7 @@ def test_create_incremental(self): } }, microversion="3.64", - params={} + params={}, ) def test_restore(self): @@ -174,11 +175,7 @@ def test_restore_vol_id(self): def test_restore_no_params(self): sot = backup.Backup(**BACKUP) - self.assertRaises( - exceptions.SDKException, - sot.restore, - self.sess - ) + self.assertRaises(exceptions.SDKException, sot.restore, self.sess) def test_force_delete(self): sot = backup.Backup(**BACKUP) @@ -188,7 +185,8 @@ def test_force_delete(self): url = 'backups/%s/action' % FAKE_ID body = {'os-force_delete': {}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_reset(self): sot = backup.Backup(**BACKUP) @@ -198,4 +196,5 @@ def test_reset(self): url = 'backups/%s/action' % FAKE_ID body = {'os-reset_status': {'status': 'new_status'}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) diff --git a/openstack/tests/unit/block_storage/v3/test_capabilities.py b/openstack/tests/unit/block_storage/v3/test_capabilities.py index ad52e0125..0eaef69ab 100644 --- a/openstack/tests/unit/block_storage/v3/test_capabilities.py +++ b/openstack/tests/unit/block_storage/v3/test_capabilities.py @@ -28,29 +28,28 @@ "compression": { "title": "Compression", "description": "Enables compression.", - "type": "boolean" + "type": "boolean", }, "qos": { "title": "QoS", "description": "Enables QoS.", - "type": "boolean" + "type": "boolean", }, "replication": { "title": "Replication", "description": "Enables replication.", - "type": "boolean" + "type": "boolean", }, "thin_provisioning": { "title": "Thin Provisioning", "description": "Sets thin provisioning.", - "type": "boolean" - } - } + "type": "boolean", + }, + }, } class TestCapabilites(base.TestCase): - def test_basic(self): capabilities_resource = capabilities.Capabilities() self.assertEqual(None, capabilities_resource.resource_key) @@ -65,28 +64,39 @@ def test_basic(self): def test_make_capabilities(self): capabilities_resource = capabilities.Capabilities(**CAPABILITIES) self.assertEqual( - CAPABILITIES["description"], capabilities_resource.description) + CAPABILITIES["description"], capabilities_resource.description + ) self.assertEqual( - CAPABILITIES["display_name"], capabilities_resource.display_name) + CAPABILITIES["display_name"], capabilities_resource.display_name + ) self.assertEqual( CAPABILITIES["driver_version"], - capabilities_resource.driver_version) + capabilities_resource.driver_version, + ) self.assertEqual( - CAPABILITIES["namespace"], capabilities_resource.namespace) + CAPABILITIES["namespace"], capabilities_resource.namespace + ) self.assertEqual( - CAPABILITIES["pool_name"], capabilities_resource.pool_name) + CAPABILITIES["pool_name"], capabilities_resource.pool_name + ) self.assertEqual( - CAPABILITIES["properties"], capabilities_resource.properties) + CAPABILITIES["properties"], capabilities_resource.properties + ) self.assertEqual( CAPABILITIES["replication_targets"], - capabilities_resource.replication_targets) + capabilities_resource.replication_targets, + ) self.assertEqual( CAPABILITIES["storage_protocol"], - capabilities_resource.storage_protocol) + capabilities_resource.storage_protocol, + ) self.assertEqual( - CAPABILITIES["vendor_name"], capabilities_resource.vendor_name) + CAPABILITIES["vendor_name"], capabilities_resource.vendor_name + ) self.assertEqual( - CAPABILITIES["visibility"], capabilities_resource.visibility) + CAPABILITIES["visibility"], capabilities_resource.visibility + ) self.assertEqual( CAPABILITIES["volume_backend_name"], - capabilities_resource.volume_backend_name) + capabilities_resource.volume_backend_name, + ) diff --git a/openstack/tests/unit/block_storage/v3/test_extension.py b/openstack/tests/unit/block_storage/v3/test_extension.py index 4d92f67e9..ca98da237 100644 --- a/openstack/tests/unit/block_storage/v3/test_extension.py +++ b/openstack/tests/unit/block_storage/v3/test_extension.py @@ -22,7 +22,6 @@ class TestExtension(base.TestCase): - def test_basic(self): extension_resource = extension.Extension() self.assertEqual('extensions', extension_resource.resources_key) @@ -36,6 +35,7 @@ def test_basic(self): def test_make_extension(self): extension_resource = extension.Extension(**EXTENSION) self.assertEqual(EXTENSION['alias'], extension_resource.alias) - self.assertEqual(EXTENSION['description'], - extension_resource.description) + self.assertEqual( + EXTENSION['description'], extension_resource.description + ) self.assertEqual(EXTENSION['updated'], extension_resource.updated) diff --git a/openstack/tests/unit/block_storage/v3/test_group.py b/openstack/tests/unit/block_storage/v3/test_group.py index f8cf11a4d..e75cf97ac 100644 --- a/openstack/tests/unit/block_storage/v3/test_group.py +++ b/openstack/tests/unit/block_storage/v3/test_group.py @@ -32,12 +32,11 @@ "volumes": ["a2cdf1ad-5497-4e57-bd7d-f573768f3d03"], "group_snapshot_id": None, "source_group_id": None, - "project_id": "7ccf4863071f44aeb8f141f65780c51b" + "project_id": "7ccf4863071f44aeb8f141f65780c51b", } class TestGroup(base.TestCase): - def test_basic(self): resource = group.Group() self.assertEqual("group", resource.resource_key) @@ -54,7 +53,8 @@ def test_make_resource(self): self.assertEqual(GROUP["id"], resource.id) self.assertEqual(GROUP["status"], resource.status) self.assertEqual( - GROUP["availability_zone"], resource.availability_zone) + GROUP["availability_zone"], resource.availability_zone + ) self.assertEqual(GROUP["created_at"], resource.created_at) self.assertEqual(GROUP["name"], resource.name) self.assertEqual(GROUP["description"], resource.description) @@ -62,13 +62,13 @@ def test_make_resource(self): self.assertEqual(GROUP["volume_types"], resource.volume_types) self.assertEqual(GROUP["volumes"], resource.volumes) self.assertEqual( - GROUP["group_snapshot_id"], resource.group_snapshot_id) + GROUP["group_snapshot_id"], resource.group_snapshot_id + ) self.assertEqual(GROUP["source_group_id"], resource.source_group_id) self.assertEqual(GROUP["project_id"], resource.project_id) class TestGroupAction(base.TestCase): - def setUp(self): super().setUp() self.resp = mock.Mock() @@ -90,7 +90,8 @@ def test_delete(self): url = 'groups/%s/action' % GROUP_ID body = {'delete': {'delete-volumes': False}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_reset(self): sot = group.Group(**GROUP) @@ -100,7 +101,9 @@ def test_reset(self): url = 'groups/%s/action' % GROUP_ID body = {'reset_status': {'status': 'new_status'}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion, + url, + json=body, + microversion=sot._max_microversion, ) def test_create_from_source(self): @@ -131,5 +134,7 @@ def test_create_from_source(self): }, } self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion, + url, + json=body, + microversion=sot._max_microversion, ) diff --git a/openstack/tests/unit/block_storage/v3/test_limits.py b/openstack/tests/unit/block_storage/v3/test_limits.py index d312b0fd1..2550092dd 100644 --- a/openstack/tests/unit/block_storage/v3/test_limits.py +++ b/openstack/tests/unit/block_storage/v3/test_limits.py @@ -23,7 +23,7 @@ "maxTotalVolumes": 10, "totalVolumesUsed": 2, "totalBackupsUsed": 3, - "totalGigabytesUsed": 2 + "totalGigabytesUsed": 2, } RATE_LIMIT = { @@ -31,23 +31,15 @@ "value": 80, "remaining": 80, "unit": "MINUTE", - "next-available": "2021-02-23T22:08:00Z" + "next-available": "2021-02-23T22:08:00Z", } -RATE_LIMITS = { - "regex": ".*", - "uri": "*", - "limit": [RATE_LIMIT] -} +RATE_LIMITS = {"regex": ".*", "uri": "*", "limit": [RATE_LIMIT]} -LIMIT = { - "rate": [RATE_LIMITS], - "absolute": ABSOLUTE_LIMIT -} +LIMIT = {"rate": [RATE_LIMITS], "absolute": ABSOLUTE_LIMIT} class TestAbsoluteLimit(base.TestCase): - def test_basic(self): limit_resource = limits.AbsoluteLimit() self.assertIsNone(limit_resource.resource_key) @@ -63,38 +55,45 @@ def test_make_absolute_limit(self): limit_resource = limits.AbsoluteLimit(**ABSOLUTE_LIMIT) self.assertEqual( ABSOLUTE_LIMIT['totalSnapshotsUsed'], - limit_resource.total_snapshots_used) + limit_resource.total_snapshots_used, + ) self.assertEqual( - ABSOLUTE_LIMIT['maxTotalBackups'], - limit_resource.max_total_backups) + ABSOLUTE_LIMIT['maxTotalBackups'], limit_resource.max_total_backups + ) self.assertEqual( ABSOLUTE_LIMIT['maxTotalVolumeGigabytes'], - limit_resource.max_total_volume_gigabytes) + limit_resource.max_total_volume_gigabytes, + ) self.assertEqual( ABSOLUTE_LIMIT['maxTotalSnapshots'], - limit_resource.max_total_snapshots) + limit_resource.max_total_snapshots, + ) self.assertEqual( ABSOLUTE_LIMIT['maxTotalBackupGigabytes'], - limit_resource.max_total_backup_gigabytes) + limit_resource.max_total_backup_gigabytes, + ) self.assertEqual( ABSOLUTE_LIMIT['totalBackupGigabytesUsed'], - limit_resource.total_backup_gigabytes_used) + limit_resource.total_backup_gigabytes_used, + ) self.assertEqual( - ABSOLUTE_LIMIT['maxTotalVolumes'], - limit_resource.max_total_volumes) + ABSOLUTE_LIMIT['maxTotalVolumes'], limit_resource.max_total_volumes + ) self.assertEqual( ABSOLUTE_LIMIT['totalVolumesUsed'], - limit_resource.total_volumes_used) + limit_resource.total_volumes_used, + ) self.assertEqual( ABSOLUTE_LIMIT['totalBackupsUsed'], - limit_resource.total_backups_used) + limit_resource.total_backups_used, + ) self.assertEqual( ABSOLUTE_LIMIT['totalGigabytesUsed'], - limit_resource.total_gigabytes_used) + limit_resource.total_gigabytes_used, + ) class TestRateLimit(base.TestCase): - def test_basic(self): limit_resource = limits.RateLimit() self.assertIsNone(limit_resource.resource_key) @@ -113,11 +112,11 @@ def test_make_rate_limit(self): self.assertEqual(RATE_LIMIT['remaining'], limit_resource.remaining) self.assertEqual(RATE_LIMIT['unit'], limit_resource.unit) self.assertEqual( - RATE_LIMIT['next-available'], limit_resource.next_available) + RATE_LIMIT['next-available'], limit_resource.next_available + ) class TestRateLimits(base.TestCase): - def test_basic(self): limit_resource = limits.RateLimits() self.assertIsNone(limit_resource.resource_key) @@ -135,7 +134,8 @@ def _test_rate_limit(self, expected, actual): self.assertEqual(expected[0]['remaining'], actual[0].remaining) self.assertEqual(expected[0]['unit'], actual[0].unit) self.assertEqual( - expected[0]['next-available'], actual[0].next_available) + expected[0]['next-available'], actual[0].next_available + ) def test_make_rate_limits(self): limit_resource = limits.RateLimits(**RATE_LIMITS) @@ -145,7 +145,6 @@ def test_make_rate_limits(self): class TestLimit(base.TestCase): - def test_basic(self): limit_resource = limits.Limit() self.assertEqual('limits', limit_resource.resource_key) @@ -158,28 +157,34 @@ def test_basic(self): def _test_absolute_limit(self, expected, actual): self.assertEqual( - expected['totalSnapshotsUsed'], actual.total_snapshots_used) - self.assertEqual( - expected['maxTotalBackups'], actual.max_total_backups) + expected['totalSnapshotsUsed'], actual.total_snapshots_used + ) + self.assertEqual(expected['maxTotalBackups'], actual.max_total_backups) self.assertEqual( expected['maxTotalVolumeGigabytes'], - actual.max_total_volume_gigabytes) + actual.max_total_volume_gigabytes, + ) self.assertEqual( - expected['maxTotalSnapshots'], actual.max_total_snapshots) + expected['maxTotalSnapshots'], actual.max_total_snapshots + ) self.assertEqual( expected['maxTotalBackupGigabytes'], - actual.max_total_backup_gigabytes) + actual.max_total_backup_gigabytes, + ) self.assertEqual( expected['totalBackupGigabytesUsed'], - actual.total_backup_gigabytes_used) - self.assertEqual( - expected['maxTotalVolumes'], actual.max_total_volumes) + actual.total_backup_gigabytes_used, + ) + self.assertEqual(expected['maxTotalVolumes'], actual.max_total_volumes) self.assertEqual( - expected['totalVolumesUsed'], actual.total_volumes_used) + expected['totalVolumesUsed'], actual.total_volumes_used + ) self.assertEqual( - expected['totalBackupsUsed'], actual.total_backups_used) + expected['totalBackupsUsed'], actual.total_backups_used + ) self.assertEqual( - expected['totalGigabytesUsed'], actual.total_gigabytes_used) + expected['totalGigabytesUsed'], actual.total_gigabytes_used + ) def _test_rate_limit(self, expected, actual): self.assertEqual(expected[0]['verb'], actual[0].verb) @@ -187,7 +192,8 @@ def _test_rate_limit(self, expected, actual): self.assertEqual(expected[0]['remaining'], actual[0].remaining) self.assertEqual(expected[0]['unit'], actual[0].unit) self.assertEqual( - expected[0]['next-available'], actual[0].next_available) + expected[0]['next-available'], actual[0].next_available + ) def _test_rate_limits(self, expected, actual): self.assertEqual(expected[0]['regex'], actual[0].regex) diff --git a/openstack/tests/unit/block_storage/v3/test_resource_filter.py b/openstack/tests/unit/block_storage/v3/test_resource_filter.py index 21fcc17e4..f5efa4a66 100644 --- a/openstack/tests/unit/block_storage/v3/test_resource_filter.py +++ b/openstack/tests/unit/block_storage/v3/test_resource_filter.py @@ -18,35 +18,29 @@ 'status', 'image_metadata', 'bootable', - 'migration_status' + 'migration_status', ], - 'resource': 'volume' + 'resource': 'volume', } class TestResourceFilter(base.TestCase): - def test_basic(self): resource = resource_filter.ResourceFilter() - self.assertEqual('resource_filters', - resource.resources_key) - self.assertEqual('/resource_filters', - resource.base_path) + self.assertEqual('resource_filters', resource.resources_key) + self.assertEqual('/resource_filters', resource.base_path) self.assertFalse(resource.allow_create) self.assertFalse(resource.allow_fetch) self.assertFalse(resource.allow_commit) self.assertFalse(resource.allow_delete) self.assertTrue(resource.allow_list) - self.assertDictEqual({"resource": "resource", - "limit": "limit", - "marker": "marker"}, - resource._query_mapping._mapping) + self.assertDictEqual( + {"resource": "resource", "limit": "limit", "marker": "marker"}, + resource._query_mapping._mapping, + ) def test_make_resource_filter(self): - resource = resource_filter.ResourceFilter( - **RESOURCE_FILTER) - self.assertEqual( - RESOURCE_FILTER['filters'], resource.filters) - self.assertEqual( - RESOURCE_FILTER['resource'], resource.resource) + resource = resource_filter.ResourceFilter(**RESOURCE_FILTER) + self.assertEqual(RESOURCE_FILTER['filters'], resource.filters) + self.assertEqual(RESOURCE_FILTER['resource'], resource.resource) diff --git a/openstack/tests/unit/block_storage/v3/test_snapshot.py b/openstack/tests/unit/block_storage/v3/test_snapshot.py index 13bef4a85..7361e1e35 100644 --- a/openstack/tests/unit/block_storage/v3/test_snapshot.py +++ b/openstack/tests/unit/block_storage/v3/test_snapshot.py @@ -31,13 +31,11 @@ "name": "snap-001", "force": "true", "os-extended-snapshot-attributes:progress": "100%", - "os-extended-snapshot-attributes:project_id": - "0c2eba2c5af04d3f9e9d0d410b371fde" + "os-extended-snapshot-attributes:project_id": "0c2eba2c5af04d3f9e9d0d410b371fde", # noqa: E501 } class TestSnapshot(base.TestCase): - def test_basic(self): sot = snapshot.Snapshot(SNAPSHOT) self.assertEqual("snapshot", sot.resource_key) @@ -49,14 +47,18 @@ def test_basic(self): self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertDictEqual({"name": "name", - "status": "status", - "all_projects": "all_tenants", - "project_id": "project_id", - "volume_id": "volume_id", - "limit": "limit", - "marker": "marker"}, - sot._query_mapping._mapping) + self.assertDictEqual( + { + "name": "name", + "status": "status", + "all_projects": "all_tenants", + "project_id": "project_id", + "volume_id": "volume_id", + "limit": "limit", + "marker": "marker", + }, + sot._query_mapping._mapping, + ) def test_create_basic(self): sot = snapshot.Snapshot(**SNAPSHOT) @@ -69,16 +71,16 @@ def test_create_basic(self): self.assertEqual(SNAPSHOT["size"], sot.size) self.assertEqual(SNAPSHOT["name"], sot.name) self.assertEqual( - SNAPSHOT["os-extended-snapshot-attributes:progress"], - sot.progress) + SNAPSHOT["os-extended-snapshot-attributes:progress"], sot.progress + ) self.assertEqual( SNAPSHOT["os-extended-snapshot-attributes:project_id"], - sot.project_id) + sot.project_id, + ) self.assertTrue(sot.is_forced) class TestSnapshotActions(base.TestCase): - def setUp(self): super(TestSnapshotActions, self).setUp() self.resp = mock.Mock() @@ -100,7 +102,8 @@ def test_force_delete(self): url = 'snapshots/%s/action' % FAKE_ID body = {'os-force_delete': {}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_reset(self): sot = snapshot.Snapshot(**SNAPSHOT) @@ -110,7 +113,8 @@ def test_reset(self): url = 'snapshots/%s/action' % FAKE_ID body = {'os-reset_status': {'status': 'new_status'}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_set_status(self): sot = snapshot.Snapshot(**SNAPSHOT) @@ -120,4 +124,5 @@ def test_set_status(self): url = 'snapshots/%s/action' % FAKE_ID body = {'os-update_snapshot_status': {'status': 'new_status'}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) diff --git a/openstack/tests/unit/block_storage/v3/test_type.py b/openstack/tests/unit/block_storage/v3/test_type.py index 28b285335..b1817c418 100644 --- a/openstack/tests/unit/block_storage/v3/test_type.py +++ b/openstack/tests/unit/block_storage/v3/test_type.py @@ -21,9 +21,7 @@ FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff" TYPE = { - "extra_specs": { - "capabilities": "gpu" - }, + "extra_specs": {"capabilities": "gpu"}, "id": FAKE_ID, "name": "SSD", "description": "Test type", @@ -31,7 +29,6 @@ class TestType(base.TestCase): - def setUp(self): super(TestType, self).setUp() self.extra_specs_result = {"extra_specs": {"go": "cubs", "boo": "sox"}} @@ -80,9 +77,11 @@ def test_set_extra_specs(self): result = sot.set_extra_specs(sess, **set_specs) self.assertEqual(result, self.extra_specs_result["extra_specs"]) - sess.post.assert_called_once_with("types/" + FAKE_ID + "/extra_specs", - headers={}, - json={"extra_specs": set_specs}) + sess.post.assert_called_once_with( + "types/" + FAKE_ID + "/extra_specs", + headers={}, + json={"extra_specs": set_specs}, + ) def test_set_extra_specs_error(self): sess = mock.Mock() @@ -99,7 +98,8 @@ def test_set_extra_specs_error(self): exceptions.BadRequestException, sot.set_extra_specs, sess, - **set_specs) + **set_specs + ) def test_delete_extra_specs(self): sess = mock.Mock() @@ -130,27 +130,28 @@ def test_delete_extra_specs_error(self): key = "hey" self.assertRaises( - exceptions.BadRequestException, - sot.delete_extra_specs, - sess, - [key]) + exceptions.BadRequestException, sot.delete_extra_specs, sess, [key] + ) def test_get_private_access(self): sot = type.Type(**TYPE) response = mock.Mock() response.status_code = 200 - response.body = {"volume_type_access": [ - {"project_id": "a", "volume_type_id": "b"} - ]} + response.body = { + "volume_type_access": [{"project_id": "a", "volume_type_id": "b"}] + } response.json = mock.Mock(return_value=response.body) self.sess.get = mock.Mock(return_value=response) - self.assertEqual(response.body["volume_type_access"], - sot.get_private_access(self.sess)) + self.assertEqual( + response.body["volume_type_access"], + sot.get_private_access(self.sess), + ) self.sess.get.assert_called_with( - "types/%s/os-volume-type-access" % sot.id) + "types/%s/os-volume-type-access" % sot.id + ) def test_add_private_access(self): sot = type.Type(**TYPE) @@ -159,8 +160,7 @@ def test_add_private_access(self): url = "types/%s/action" % sot.id body = {"addProjectAccess": {"project": "a"}} - self.sess.post.assert_called_with( - url, json=body) + self.sess.post.assert_called_with(url, json=body) def test_remove_private_access(self): sot = type.Type(**TYPE) @@ -169,5 +169,4 @@ def test_remove_private_access(self): url = "types/%s/action" % sot.id body = {"removeProjectAccess": {"project": "a"}} - self.sess.post.assert_called_with( - url, json=body) + self.sess.post.assert_called_with(url, json=body) diff --git a/openstack/tests/unit/block_storage/v3/test_type_encryption.py b/openstack/tests/unit/block_storage/v3/test_type_encryption.py index b448125a3..a0ebf22d6 100644 --- a/openstack/tests/unit/block_storage/v3/test_type_encryption.py +++ b/openstack/tests/unit/block_storage/v3/test_type_encryption.py @@ -31,7 +31,6 @@ class TestTypeEncryption(base.TestCase): - def test_basic(self): sot = type.TypeEncryption(**TYPE_ENC) self.assertEqual("encryption", sot.resource_key) diff --git a/openstack/tests/unit/block_storage/v3/test_volume.py b/openstack/tests/unit/block_storage/v3/test_volume.py index 9b2a76892..b8326ea98 100644 --- a/openstack/tests/unit/block_storage/v3/test_volume.py +++ b/openstack/tests/unit/block_storage/v3/test_volume.py @@ -21,11 +21,13 @@ FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff" IMAGE_METADATA = { 'container_format': 'bare', - 'min_ram': '64', 'disk_format': u'qcow2', + 'min_ram': '64', + 'disk_format': u'qcow2', 'image_name': 'TestVM', 'image_id': '625d4f2c-cf67-4af3-afb6-c7220f766947', 'checksum': '64d7c1cd2b6f60c92c14662941cb7913', - 'min_disk': '0', u'size': '13167616' + 'min_disk': '0', + u'size': '13167616', } VOLUME = { @@ -59,14 +61,13 @@ "OS-SCH-HNT:scheduler_hints": { "same_host": [ "a0cf03a5-d921-4877-bb5c-86d26cf818e1", - "8c19174f-4220-44f0-824a-cd1eeef10287" + "8c19174f-4220-44f0-824a-cd1eeef10287", ] - } + }, } class TestVolume(base.TestCase): - def test_basic(self): sot = volume.Volume(VOLUME) self.assertEqual("volume", sot.resource_key) @@ -78,15 +79,19 @@ def test_basic(self): self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertDictEqual({"name": "name", - "status": "status", - "all_projects": "all_tenants", - "project_id": "project_id", - "created_at": "created_at", - "updated_at": "updated_at", - "limit": "limit", - "marker": "marker"}, - sot._query_mapping._mapping) + self.assertDictEqual( + { + "name": "name", + "status": "status", + "all_projects": "all_tenants", + "project_id": "project_id", + "created_at": "created_at", + "updated_at": "updated_at", + "limit": "limit", + "marker": "marker", + }, + sot._query_mapping._mapping, + ) def test_create(self): sot = volume.Volume(**VOLUME) @@ -103,33 +108,40 @@ def test_create(self): self.assertEqual(VOLUME["source_volid"], sot.source_volume_id) self.assertEqual(VOLUME["metadata"], sot.metadata) self.assertEqual(VOLUME["multiattach"], sot.is_multiattach) - self.assertEqual(VOLUME["volume_image_metadata"], - sot.volume_image_metadata) + self.assertEqual( + VOLUME["volume_image_metadata"], sot.volume_image_metadata + ) self.assertEqual(VOLUME["size"], sot.size) self.assertEqual(VOLUME["imageRef"], sot.image_id) self.assertEqual(VOLUME["os-vol-host-attr:host"], sot.host) - self.assertEqual(VOLUME["os-vol-tenant-attr:tenant_id"], - sot.project_id) - self.assertEqual(VOLUME["os-vol-mig-status-attr:migstat"], - sot.migration_status) - self.assertEqual(VOLUME["os-vol-mig-status-attr:name_id"], - sot.migration_id) - self.assertEqual(VOLUME["replication_status"], - sot.replication_status) + self.assertEqual( + VOLUME["os-vol-tenant-attr:tenant_id"], sot.project_id + ) + self.assertEqual( + VOLUME["os-vol-mig-status-attr:migstat"], sot.migration_status + ) + self.assertEqual( + VOLUME["os-vol-mig-status-attr:name_id"], sot.migration_id + ) + self.assertEqual(VOLUME["replication_status"], sot.replication_status) self.assertEqual( VOLUME["os-volume-replication:extended_status"], - sot.extended_replication_status) - self.assertEqual(VOLUME["consistencygroup_id"], - sot.consistency_group_id) - self.assertEqual(VOLUME["os-volume-replication:driver_data"], - sot.replication_driver_data) + sot.extended_replication_status, + ) + self.assertEqual( + VOLUME["consistencygroup_id"], sot.consistency_group_id + ) + self.assertEqual( + VOLUME["os-volume-replication:driver_data"], + sot.replication_driver_data, + ) self.assertFalse(sot.is_encrypted) - self.assertDictEqual(VOLUME["OS-SCH-HNT:scheduler_hints"], - sot.scheduler_hints) + self.assertDictEqual( + VOLUME["OS-SCH-HNT:scheduler_hints"], sot.scheduler_hints + ) class TestVolumeActions(TestVolume): - def setUp(self): super(TestVolumeActions, self).setUp() self.resp = mock.Mock() @@ -149,7 +161,8 @@ def test_extend(self): url = 'volumes/%s/action' % FAKE_ID body = {"os-extend": {"new_size": "20"}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_set_volume_readonly(self): sot = volume.Volume(**VOLUME) @@ -159,7 +172,8 @@ def test_set_volume_readonly(self): url = 'volumes/%s/action' % FAKE_ID body = {'os-update_readonly_flag': {'readonly': True}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_set_volume_readonly_false(self): sot = volume.Volume(**VOLUME) @@ -169,7 +183,8 @@ def test_set_volume_readonly_false(self): url = 'volumes/%s/action' % FAKE_ID body = {'os-update_readonly_flag': {'readonly': False}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_set_volume_bootable(self): sot = volume.Volume(**VOLUME) @@ -179,7 +194,8 @@ def test_set_volume_bootable(self): url = 'volumes/%s/action' % FAKE_ID body = {'os-set_bootable': {'bootable': True}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_set_volume_bootable_false(self): sot = volume.Volume(**VOLUME) @@ -189,7 +205,8 @@ def test_set_volume_bootable_false(self): url = 'volumes/%s/action' % FAKE_ID body = {'os-set_bootable': {'bootable': False}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_reset_status(self): sot = volume.Volume(**VOLUME) @@ -197,25 +214,34 @@ def test_reset_status(self): self.assertIsNone(sot.reset_status(self.sess, '1', '2', '3')) url = 'volumes/%s/action' % FAKE_ID - body = {'os-reset_status': {'status': '1', 'attach_status': '2', - 'migration_status': '3'}} + body = { + 'os-reset_status': { + 'status': '1', + 'attach_status': '2', + 'migration_status': '3', + } + } self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) - @mock.patch('openstack.utils.require_microversion', autospec=True, - side_effect=[exceptions.SDKException()]) + @mock.patch( + 'openstack.utils.require_microversion', + autospec=True, + side_effect=[exceptions.SDKException()], + ) def test_revert_to_snapshot_before_340(self, mv_mock): sot = volume.Volume(**VOLUME) self.assertRaises( - exceptions.SDKException, - sot.revert_to_snapshot, - self.sess, - '1' + exceptions.SDKException, sot.revert_to_snapshot, self.sess, '1' ) - @mock.patch('openstack.utils.require_microversion', autospec=True, - side_effect=[None]) + @mock.patch( + 'openstack.utils.require_microversion', + autospec=True, + side_effect=[None], + ) def test_revert_to_snapshot_after_340(self, mv_mock): sot = volume.Volume(**VOLUME) @@ -224,7 +250,8 @@ def test_revert_to_snapshot_after_340(self, mv_mock): url = 'volumes/%s/action' % FAKE_ID body = {'revert': {'snapshot_id': '1'}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) mv_mock.assert_called_with(self.sess, '3.40') def test_attach_instance(self): @@ -235,7 +262,8 @@ def test_attach_instance(self): url = 'volumes/%s/action' % FAKE_ID body = {'os-attach': {'mountpoint': '1', 'instance_uuid': '2'}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_attach_host(self): sot = volume.Volume(**VOLUME) @@ -245,16 +273,13 @@ def test_attach_host(self): url = 'volumes/%s/action' % FAKE_ID body = {'os-attach': {'mountpoint': '1', 'host_name': '2'}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_attach_error(self): sot = volume.Volume(**VOLUME) - self.assertRaises( - ValueError, - sot.attach, - self.sess, - '1') + self.assertRaises(ValueError, sot.attach, self.sess, '1') def test_detach(self): sot = volume.Volume(**VOLUME) @@ -264,19 +289,23 @@ def test_detach(self): url = 'volumes/%s/action' % FAKE_ID body = {'os-detach': {'attachment_id': '1'}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_detach_force(self): sot = volume.Volume(**VOLUME) self.assertIsNone( - sot.detach(self.sess, '1', force=True, connector={'a': 'b'})) + sot.detach(self.sess, '1', force=True, connector={'a': 'b'}) + ) url = 'volumes/%s/action' % FAKE_ID - body = {'os-force_detach': {'attachment_id': '1', - 'connector': {'a': 'b'}}} + body = { + 'os-force_detach': {'attachment_id': '1', 'connector': {'a': 'b'}} + } self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_unmanage(self): sot = volume.Volume(**VOLUME) @@ -286,7 +315,8 @@ def test_unmanage(self): url = 'volumes/%s/action' % FAKE_ID body = {'os-unmanage': {}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_retype(self): sot = volume.Volume(**VOLUME) @@ -296,7 +326,8 @@ def test_retype(self): url = 'volumes/%s/action' % FAKE_ID body = {'os-retype': {'new_type': '1'}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_retype_mp(self): sot = volume.Volume(**VOLUME) @@ -306,7 +337,8 @@ def test_retype_mp(self): url = 'volumes/%s/action' % FAKE_ID body = {'os-retype': {'new_type': '1', 'migration_policy': '2'}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_migrate(self): sot = volume.Volume(**VOLUME) @@ -316,33 +348,55 @@ def test_migrate(self): url = 'volumes/%s/action' % FAKE_ID body = {'os-migrate_volume': {'host': '1'}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_migrate_flags(self): sot = volume.Volume(**VOLUME) - self.assertIsNone(sot.migrate(self.sess, host='1', - force_host_copy=True, lock_volume=True)) + self.assertIsNone( + sot.migrate( + self.sess, host='1', force_host_copy=True, lock_volume=True + ) + ) url = 'volumes/%s/action' % FAKE_ID - body = {'os-migrate_volume': {'host': '1', 'force_host_copy': True, - 'lock_volume': True}} + body = { + 'os-migrate_volume': { + 'host': '1', + 'force_host_copy': True, + 'lock_volume': True, + } + } self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) - @mock.patch('openstack.utils.require_microversion', autospec=True, - side_effect=[None]) + @mock.patch( + 'openstack.utils.require_microversion', + autospec=True, + side_effect=[None], + ) def test_migrate_cluster(self, mv_mock): sot = volume.Volume(**VOLUME) - self.assertIsNone(sot.migrate(self.sess, cluster='1', - force_host_copy=True, lock_volume=True)) + self.assertIsNone( + sot.migrate( + self.sess, cluster='1', force_host_copy=True, lock_volume=True + ) + ) url = 'volumes/%s/action' % FAKE_ID - body = {'os-migrate_volume': {'cluster': '1', 'force_host_copy': True, - 'lock_volume': True}} + body = { + 'os-migrate_volume': { + 'cluster': '1', + 'force_host_copy': True, + 'lock_volume': True, + } + } self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) mv_mock.assert_called_with(self.sess, '3.16') def test_complete_migration(self): @@ -351,22 +405,27 @@ def test_complete_migration(self): self.assertIsNone(sot.complete_migration(self.sess, new_volume_id='1')) url = 'volumes/%s/action' % FAKE_ID - body = {'os-migrate_volume_completion': {'new_volume': '1', 'error': - False}} + body = { + 'os-migrate_volume_completion': {'new_volume': '1', 'error': False} + } self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_complete_migration_error(self): sot = volume.Volume(**VOLUME) - self.assertIsNone(sot.complete_migration( - self.sess, new_volume_id='1', error=True)) + self.assertIsNone( + sot.complete_migration(self.sess, new_volume_id='1', error=True) + ) url = 'volumes/%s/action' % FAKE_ID - body = {'os-migrate_volume_completion': {'new_volume': '1', 'error': - True}} + body = { + 'os-migrate_volume_completion': {'new_volume': '1', 'error': True} + } self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_force_delete(self): sot = volume.Volume(**VOLUME) @@ -376,7 +435,8 @@ def test_force_delete(self): url = 'volumes/%s/action' % FAKE_ID body = {'os-force_delete': {}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_upload_image(self): sot = volume.Volume(**VOLUME) @@ -390,15 +450,16 @@ def test_upload_image(self): self.assertDictEqual({'a': 'b'}, sot.upload_to_image(self.sess, '1')) url = 'volumes/%s/action' % FAKE_ID - body = {'os-volume_upload_image': { - 'image_name': '1', - 'force': False - }} + body = {'os-volume_upload_image': {'image_name': '1', 'force': False}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) - @mock.patch('openstack.utils.require_microversion', autospec=True, - side_effect=[None]) + @mock.patch( + 'openstack.utils.require_microversion', + autospec=True, + side_effect=[None], + ) def test_upload_image_args(self, mv_mock): sot = volume.Volume(**VOLUME) @@ -410,21 +471,30 @@ def test_upload_image_args(self, mv_mock): self.assertDictEqual( {'a': 'b'}, - sot.upload_to_image(self.sess, '1', disk_format='2', - container_format='3', visibility='4', - protected='5')) + sot.upload_to_image( + self.sess, + '1', + disk_format='2', + container_format='3', + visibility='4', + protected='5', + ), + ) url = 'volumes/%s/action' % FAKE_ID - body = {'os-volume_upload_image': { - 'image_name': '1', - 'force': False, - 'disk_format': '2', - 'container_format': '3', - 'visibility': '4', - 'protected': '5' - }} + body = { + 'os-volume_upload_image': { + 'image_name': '1', + 'force': False, + 'disk_format': '2', + 'container_format': '3', + 'visibility': '4', + 'protected': '5', + } + } self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) mv_mock.assert_called_with(self.sess, '3.1') def test_reserve(self): @@ -435,7 +505,8 @@ def test_reserve(self): url = 'volumes/%s/action' % FAKE_ID body = {'os-reserve': {}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_unreserve(self): sot = volume.Volume(**VOLUME) @@ -445,7 +516,8 @@ def test_unreserve(self): url = 'volumes/%s/action' % FAKE_ID body = {'os-unreserve': {}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_begin_detaching(self): sot = volume.Volume(**VOLUME) @@ -455,7 +527,8 @@ def test_begin_detaching(self): url = 'volumes/%s/action' % FAKE_ID body = {'os-begin_detaching': {}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_abort_detaching(self): sot = volume.Volume(**VOLUME) @@ -465,7 +538,8 @@ def test_abort_detaching(self): url = 'volumes/%s/action' % FAKE_ID body = {'os-roll_detaching': {}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_init_attachment(self): sot = volume.Volume(**VOLUME) @@ -475,7 +549,8 @@ def test_init_attachment(self): url = 'volumes/%s/action' % FAKE_ID body = {'os-initialize_connection': {'connector': {'a': 'b'}}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) def test_terminate_attachment(self): sot = volume.Volume(**VOLUME) @@ -485,4 +560,5 @@ def test_terminate_attachment(self): url = 'volumes/%s/action' % FAKE_ID body = {'os-terminate_connection': {'connector': {'a': 'b'}}} self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion) + url, json=body, microversion=sot._max_microversion + ) From 4589e293e829950d2fd4c705cce2f7ce30ca9e29 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 3 May 2023 12:14:19 +0100 Subject: [PATCH 3241/3836] Blackify openstack.object_store Black used with the '-l 79 -S' flags. A future change will ignore this commit in git-blame history by adding a 'git-blame-ignore-revs' file. Change-Id: I9c6e6b898fc7e3a196725bd37a3b5bdc77060cd3 Signed-off-by: Stephen Finucane --- openstack/object_store/v1/_base.py | 26 +- openstack/object_store/v1/_proxy.py | 383 +++++++---- openstack/object_store/v1/account.py | 5 +- openstack/object_store/v1/container.py | 23 +- openstack/object_store/v1/info.py | 11 +- openstack/object_store/v1/obj.py | 61 +- .../object_store/v1/test_account.py | 1 - .../object_store/v1/test_container.py | 19 +- .../functional/object_store/v1/test_obj.py | 41 +- .../unit/object_store/v1/test_account.py | 75 ++- .../unit/object_store/v1/test_container.py | 155 +++-- .../tests/unit/object_store/v1/test_obj.py | 62 +- .../tests/unit/object_store/v1/test_proxy.py | 613 +++++++++++------- 13 files changed, 904 insertions(+), 571 deletions(-) diff --git a/openstack/object_store/v1/_base.py b/openstack/object_store/v1/_base.py index d429b13a1..19f4c4cdc 100644 --- a/openstack/object_store/v1/_base.py +++ b/openstack/object_store/v1/_base.py @@ -28,13 +28,13 @@ class BaseResource(resource.Resource): _last_headers = dict() def __init__(self, metadata=None, **attrs): - """Process and save metadata known at creation stage - """ + """Process and save metadata known at creation stage""" super().__init__(**attrs) if metadata is not None: for k, v in metadata.items(): if not k.lower().startswith( - self._custom_metadata_prefix.lower()): + self._custom_metadata_prefix.lower() + ): self.metadata[self._custom_metadata_prefix + k] = v else: self.metadata[k] = v @@ -62,8 +62,8 @@ def _calculate_headers(self, metadata): def set_metadata(self, session, metadata, refresh=True): request = self._prepare_request() response = session.post( - request.url, - headers=self._calculate_headers(metadata)) + request.url, headers=self._calculate_headers(metadata) + ) self._translate_response(response, has_body=False) if refresh: response = session.head(request.url) @@ -74,10 +74,11 @@ def delete_metadata(self, session, keys): request = self._prepare_request() headers = {key: '' for key in keys} response = session.post( - request.url, - headers=self._calculate_headers(headers)) + request.url, headers=self._calculate_headers(headers) + ) exceptions.raise_from_response( - response, error_message="Error deleting metadata keys") + response, error_message="Error deleting metadata keys" + ) return self def _set_metadata(self, headers): @@ -85,10 +86,8 @@ def _set_metadata(self, headers): for header in headers: # RADOS and other stuff in front may actually lowcase headers - if header.lower().startswith( - self._custom_metadata_prefix.lower() - ): - key = header[len(self._custom_metadata_prefix):].lower() + if header.lower().startswith(self._custom_metadata_prefix.lower()): + key = header[len(self._custom_metadata_prefix) :].lower() self.metadata[key] = headers[header] def _translate_response(self, response, has_body=None, error_message=None): @@ -98,5 +97,6 @@ def _translate_response(self, response, has_body=None, error_message=None): # pops known headers. self._last_headers = response.headers.copy() super(BaseResource, self)._translate_response( - response, has_body=has_body, error_message=error_message) + response, has_body=has_body, error_message=error_message + ) self._set_metadata(response.headers) diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index f6dc9328b..3bd4edf2a 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -40,7 +40,7 @@ class Proxy(proxy.Proxy): "account": _account.Account, "container": _container.Container, "info": _info.Info, - "object": _obj.Object + "object": _obj.Object, } skip_discovery = True @@ -60,19 +60,25 @@ def _extract_name(self, url, service_type=None, project_id=None): # Split url into parts and exclude potential project_id in some urls url_parts = [ - x for x in url_path.split('/') if ( + x + for x in url_path.split('/') + if ( x != project_id and ( not project_id or (project_id and x != 'AUTH_' + project_id) - )) + ) + ) ] # Strip leading version piece so that # GET /v1/AUTH_xxx # returns ['AUTH_xxx'] - if (url_parts[0] - and url_parts[0][0] == 'v' - and url_parts[0][1] and url_parts[0][1].isdigit()): + if ( + url_parts[0] + and url_parts[0][0] == 'v' + and url_parts[0][1] + and url_parts[0][1].isdigit() + ): url_parts = url_parts[1:] # Strip out anything that's empty or None @@ -152,8 +158,9 @@ def delete_container(self, container, ignore_missing=True): :returns: ``None`` """ - self._delete(_container.Container, container, - ignore_missing=ignore_missing) + self._delete( + _container.Container, container, ignore_missing=ignore_missing + ) def get_container_metadata(self, container): """Get metadata for a container @@ -219,8 +226,12 @@ def objects(self, container, **query): container = self._get_container_name(container=container) for obj in self._list( - _obj.Object, container=container, - paginated=True, format='json', **query): + _obj.Object, + container=container, + paginated=True, + format='json', + **query, + ): obj.container = container yield obj @@ -236,8 +247,12 @@ def _get_container_name(self, obj=None, container=None): raise ValueError("container must be specified") def get_object( - self, obj, container=None, resp_chunk_size=1024, - outfile=None, remember_content=False + self, + obj, + container=None, + resp_chunk_size=1024, + outfile=None, + remember_content=False, ): """Get the data associated with an object @@ -262,20 +277,17 @@ def get_object( :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - container_name = self._get_container_name( - obj=obj, container=container) + container_name = self._get_container_name(obj=obj, container=container) _object = self._get_resource( - _obj.Object, obj, - container=container_name) + _obj.Object, obj, container=container_name + ) request = _object._prepare_request() - get_stream = (outfile is not None) + get_stream = outfile is not None response = self.get( - request.url, - headers=request.headers, - stream=get_stream + request.url, headers=request.headers, stream=get_stream ) exceptions.raise_from_response(response) _object._translate_response(response, has_body=False) @@ -286,7 +298,8 @@ def get_object( else: outfile_handle = outfile for chunk in response.iter_content( - resp_chunk_size, decode_unicode=False): + resp_chunk_size, decode_unicode=False + ): outfile_handle.write(chunk) if isinstance(outfile, str): outfile_handle.close() @@ -308,10 +321,10 @@ def download_object(self, obj, container=None, **attrs): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - container_name = self._get_container_name( - obj=obj, container=container) + container_name = self._get_container_name(obj=obj, container=container) obj = self._get_resource( - _obj.Object, obj, container=container_name, **attrs) + _obj.Object, obj, container=container_name, **attrs + ) return obj.download(self) def stream_object(self, obj, container=None, chunk_size=1024, **attrs): @@ -326,18 +339,26 @@ def stream_object(self, obj, container=None, chunk_size=1024, **attrs): when no resource can be found. :returns: An iterator that iterates over chunk_size bytes """ - container_name = self._get_container_name( - obj=obj, container=container) + container_name = self._get_container_name(obj=obj, container=container) obj = self._get_resource( - _obj.Object, obj, container=container_name, **attrs) + _obj.Object, obj, container=container_name, **attrs + ) return obj.stream(self, chunk_size=chunk_size) def create_object( - self, container, name, filename=None, - md5=None, sha256=None, segment_size=None, - use_slo=True, metadata=None, - generate_checksums=None, data=None, - **headers): + self, + container, + name, + filename=None, + md5=None, + sha256=None, + segment_size=None, + use_slo=True, + metadata=None, + generate_checksums=None, + data=None, + **headers, + ): """Create a file object. Automatically uses large-object segments if needed. @@ -373,13 +394,14 @@ def create_object( """ if data is not None and filename: raise ValueError( - "Both filename and data given. Please choose one.") + "Both filename and data given. Please choose one." + ) if data is not None and not name: - raise ValueError( - "name is a required parameter when data is given") + raise ValueError("name is a required parameter when data is given") if data is not None and generate_checksums: raise ValueError( - "checksums cannot be generated with data parameter") + "checksums cannot be generated with data parameter" + ) if generate_checksums is None: if data is not None: generate_checksums = False @@ -400,17 +422,22 @@ def create_object( metadata[self._connection._OBJECT_SHA256_KEY] = sha256 container_name = self._get_container_name(container=container) - endpoint = '{container}/{name}'.format(container=container_name, - name=name) + endpoint = '{container}/{name}'.format( + container=container_name, name=name + ) if data is not None: self.log.debug( - "swift uploading data to %(endpoint)s", - {'endpoint': endpoint}) + "swift uploading data to %(endpoint)s", {'endpoint': endpoint} + ) return self._create( - _obj.Object, container=container_name, - name=name, data=data, metadata=metadata, - **headers) + _obj.Object, + container=container_name, + name=name, + data=data, + metadata=metadata, + **headers, + ) # segment_size gets used as a step value in a range call, so needs # to be an int @@ -423,7 +450,8 @@ def create_object( self._connection.log.debug( "swift uploading %(filename)s to %(endpoint)s", - {'filename': filename, 'endpoint': endpoint}) + {'filename': filename, 'endpoint': endpoint}, + ) if metadata is not None: # Rely on the class headers calculation for requested metadata @@ -435,8 +463,13 @@ def create_object( else: self._upload_large_object( - endpoint, filename, headers, - file_size, segment_size, use_slo) + endpoint, + filename, + headers, + file_size, + segment_size, + use_slo, + ) # Backwards compat upload_object = create_object @@ -461,8 +494,12 @@ def delete_object(self, obj, ignore_missing=True, container=None): """ container_name = self._get_container_name(obj, container) - self._delete(_obj.Object, obj, ignore_missing=ignore_missing, - container=container_name) + self._delete( + _obj.Object, + obj, + ignore_missing=ignore_missing, + container=container_name, + ) def get_object_metadata(self, obj, container=None): """Get metadata for an object. @@ -522,7 +559,8 @@ def delete_object_metadata(self, obj, container=None, keys=None): return res def is_object_stale( - self, container, name, filename, file_md5=None, file_sha256=None): + self, container, name, filename, file_md5=None, file_sha256=None + ): """Check to see if an object matches the hashes of a file. :param container: Name of the container. @@ -538,37 +576,45 @@ def is_object_stale( except exceptions.NotFoundException: self._connection.log.debug( "swift stale check, no object: {container}/{name}".format( - container=container, name=name)) + container=container, name=name + ) + ) return True if not (file_md5 or file_sha256): - (file_md5, file_sha256) = \ - utils._get_file_hashes(filename) + (file_md5, file_sha256) = utils._get_file_hashes(filename) md5_key = metadata.get( self._connection._OBJECT_MD5_KEY, - metadata.get(self._connection._SHADE_OBJECT_MD5_KEY, '')) + metadata.get(self._connection._SHADE_OBJECT_MD5_KEY, ''), + ) sha256_key = metadata.get( - self._connection._OBJECT_SHA256_KEY, metadata.get( - self._connection._SHADE_OBJECT_SHA256_KEY, '')) + self._connection._OBJECT_SHA256_KEY, + metadata.get(self._connection._SHADE_OBJECT_SHA256_KEY, ''), + ) up_to_date = utils._hashes_up_to_date( - md5=file_md5, sha256=file_sha256, - md5_key=md5_key, sha256_key=sha256_key) + md5=file_md5, + sha256=file_sha256, + md5_key=md5_key, + sha256_key=sha256_key, + ) if not up_to_date: self._connection.log.debug( "swift checksum mismatch: " " %(filename)s!=%(container)s/%(name)s", - {'filename': filename, 'container': container, 'name': name}) + {'filename': filename, 'container': container, 'name': name}, + ) return True self._connection.log.debug( "swift object up to date: %(container)s/%(name)s", - {'container': container, 'name': name}) + {'container': container, 'name': name}, + ) return False def _upload_large_object( - self, endpoint, filename, - headers, file_size, segment_size, use_slo): + self, endpoint, filename, headers, file_size, segment_size, use_slo + ): # If the object is big, we need to break it up into segments that # are no larger than segment_size, upload each of them individually # and then upload a manifest object. The segments can be uploaded in @@ -584,28 +630,32 @@ def _upload_large_object( # segment, the value a FileSegment file-like object that is a # slice of the data for the segment. segments = self._get_file_segments( - endpoint, filename, file_size, segment_size) + endpoint, filename, file_size, segment_size + ) # Schedule the segments for upload for name, segment in segments.items(): # Async call to put - schedules execution and returns a future segment_future = self._connection._pool_executor.submit( - self.put, - name, headers=headers, data=segment, - raise_exc=False) + self.put, name, headers=headers, data=segment, raise_exc=False + ) segment_futures.append(segment_future) # TODO(mordred) Collect etags from results to add to this manifest # dict. Then sort the list of dicts by path. - manifest.append(dict( - # While Object Storage usually expects the name to be - # urlencoded in most requests, the SLO manifest requires - # plain object names instead. - path='/{name}'.format(name=parse.unquote(name)), - size_bytes=segment.length)) + manifest.append( + dict( + # While Object Storage usually expects the name to be + # urlencoded in most requests, the SLO manifest requires + # plain object names instead. + path='/{name}'.format(name=parse.unquote(name)), + size_bytes=segment.length, + ) + ) # Try once and collect failed results to retry segment_results, retry_results = self._connection._wait_for_futures( - segment_futures, raise_on_error=False) + segment_futures, raise_on_error=False + ) self._add_etag_to_manifest(segment_results, manifest) @@ -616,37 +666,41 @@ def _upload_large_object( segment.seek(0) # Async call to put - schedules execution and returns a future segment_future = self._connection._pool_executor.submit( - self.put, - name, headers=headers, data=segment) + self.put, name, headers=headers, data=segment + ) # TODO(mordred) Collect etags from results to add to this manifest # dict. Then sort the list of dicts by path. retry_futures.append(segment_future) # If any segments fail the second time, just throw the error segment_results, retry_results = self._connection._wait_for_futures( - retry_futures, raise_on_error=True) + retry_futures, raise_on_error=True + ) self._add_etag_to_manifest(segment_results, manifest) try: if use_slo: return self._finish_large_object_slo( - endpoint, headers, manifest) + endpoint, headers, manifest + ) else: - return self._finish_large_object_dlo( - endpoint, headers) + return self._finish_large_object_dlo(endpoint, headers) except Exception: try: segment_prefix = endpoint.split('/')[-1] self.log.debug( "Failed to upload large object manifest for %s. " - "Removing segment uploads.", segment_prefix) + "Removing segment uploads.", + segment_prefix, + ) self._delete_autocreated_image_objects( - segment_prefix=segment_prefix) + segment_prefix=segment_prefix + ) except Exception: self.log.exception( - "Failed to cleanup image objects for %s:", - segment_prefix) + "Failed to cleanup image objects for %s:", segment_prefix + ) raise def _finish_large_object_slo(self, endpoint, headers, manifest): @@ -656,10 +710,13 @@ def _finish_large_object_slo(self, endpoint, headers, manifest): retries = 3 while True: try: - return exceptions.raise_from_response(self.put( - endpoint, - params={'multipart-manifest': 'put'}, - headers=headers, data=json.dumps(manifest)) + return exceptions.raise_from_response( + self.put( + endpoint, + params={'multipart-manifest': 'put'}, + headers=headers, + data=json.dumps(manifest), + ) ) except Exception: retries -= 1 @@ -673,7 +730,8 @@ def _finish_large_object_dlo(self, endpoint, headers): while True: try: return exceptions.raise_from_response( - self.put(endpoint, headers=headers)) + self.put(endpoint, headers=headers) + ) except Exception: retries -= 1 if retries == 0: @@ -681,8 +739,7 @@ def _finish_large_object_dlo(self, endpoint, headers): def _upload_object(self, endpoint, filename, headers): with open(filename, 'rb') as dt: - return self.put( - endpoint, headers=headers, data=dt) + return self.put(endpoint, headers=headers, data=dt) def _get_file_segments(self, endpoint, filename, file_size, segment_size): # Use an ordered dict here so that testing can replicate things @@ -690,10 +747,13 @@ def _get_file_segments(self, endpoint, filename, file_size, segment_size): for (index, offset) in enumerate(range(0, file_size, segment_size)): remaining = file_size - (index * segment_size) segment = _utils.FileSegment( - filename, offset, - segment_size if segment_size < remaining else remaining) + filename, + offset, + segment_size if segment_size < remaining else remaining, + ) name = '{endpoint}/{index:0>6}'.format( - endpoint=endpoint, index=index) + endpoint=endpoint, index=index + ) segments[name] = segment return segments @@ -710,7 +770,8 @@ def get_object_segment_size(self, segment_size): server_max_file_size = DEFAULT_MAX_FILE_SIZE self._connection.log.info( "Swift capabilities not supported. " - "Using default max file size.") + "Using default max file size." + ) else: raise else: @@ -740,9 +801,7 @@ def _add_etag_to_manifest(self, segment_results, manifest): continue name = self._object_name_from_url(result.url) for entry in manifest: - if entry['path'] == '/{name}'.format( - name=parse.unquote(name) - ): + if entry['path'] == '/{name}'.format(name=parse.unquote(name)): entry['etag'] = result.headers['Etag'] def get_info(self): @@ -788,12 +847,16 @@ def get_temp_url_key(self, container=None): temp_url_key = None if container: container_meta = self.get_container_metadata(container) - temp_url_key = (container_meta.meta_temp_url_key_2 - or container_meta.meta_temp_url_key) + temp_url_key = ( + container_meta.meta_temp_url_key_2 + or container_meta.meta_temp_url_key + ) if not temp_url_key: account_meta = self.get_account_metadata() - temp_url_key = (account_meta.meta_temp_url_key_2 - or account_meta.meta_temp_url_key) + temp_url_key = ( + account_meta.meta_temp_url_key_2 + or account_meta.meta_temp_url_key + ) if temp_url_key and not isinstance(temp_url_key, bytes): temp_url_key = temp_url_key.encode('utf8') return temp_url_key @@ -807,12 +870,20 @@ def _check_temp_url_key(self, container=None, temp_url_key=None): if not temp_url_key: raise exceptions.SDKException( 'temp_url_key was not given, nor was a temporary url key' - ' found for the account or the container.') + ' found for the account or the container.' + ) return temp_url_key def generate_form_signature( - self, container, object_prefix, redirect_url, max_file_size, - max_upload_count, timeout, temp_url_key=None): + self, + container, + object_prefix, + redirect_url, + max_file_size, + max_upload_count, + timeout, + temp_url_key=None, + ): """Generate a signature for a FormPost upload. :param container: The value can be the name of a container or a @@ -832,33 +903,50 @@ def generate_form_signature( max_file_size = int(max_file_size) if max_file_size < 1: raise exceptions.SDKException( - 'Please use a positive max_file_size value.') + 'Please use a positive max_file_size value.' + ) max_upload_count = int(max_upload_count) if max_upload_count < 1: raise exceptions.SDKException( - 'Please use a positive max_upload_count value.') + 'Please use a positive max_upload_count value.' + ) if timeout < 1: raise exceptions.SDKException( - 'Please use a positive value.') + 'Please use a positive value.' + ) expires = int(time.time() + int(timeout)) - temp_url_key = self._check_temp_url_key(container=container, - temp_url_key=temp_url_key) + temp_url_key = self._check_temp_url_key( + container=container, temp_url_key=temp_url_key + ) res = self._get_resource(_container.Container, container) endpoint = parse.urlparse(self.get_endpoint()) path = '/'.join([endpoint.path, res.name, object_prefix]) - data = '%s\n%s\n%s\n%s\n%s' % (path, redirect_url, max_file_size, - max_upload_count, expires) + data = '%s\n%s\n%s\n%s\n%s' % ( + path, + redirect_url, + max_file_size, + max_upload_count, + expires, + ) data = data.encode('utf8') sig = hmac.new(temp_url_key, data, sha1).hexdigest() return (expires, sig) def generate_temp_url( - self, path, seconds, method, absolute=False, prefix=False, - iso8601=False, ip_range=None, temp_url_key=None): + self, + path, + seconds, + method, + absolute=False, + prefix=False, + iso8601=False, + ip_range=None, + temp_url_key=None, + ): """Generates a temporary URL that gives unauthenticated access to the Swift object. @@ -894,7 +982,8 @@ def generate_temp_url( formats = ( EXPIRES_ISO8601_FORMAT, EXPIRES_ISO8601_FORMAT[:-1], - SHORT_EXPIRES_ISO8601_FORMAT) + SHORT_EXPIRES_ISO8601_FORMAT, + ) for f in formats: try: t = time.strptime(seconds, f) @@ -919,8 +1008,10 @@ def generate_temp_url( if timestamp < 0: raise ValueError() except ValueError: - raise ValueError('time must either be a whole number ' - 'or in specific ISO 8601 format.') + raise ValueError( + 'time must either be a whole number ' + 'or in specific ISO 8601 format.' + ) if isinstance(path, bytes): try: @@ -931,50 +1022,61 @@ def generate_temp_url( path_for_body = path parts = path_for_body.split('/', 4) - if len(parts) != 5 or parts[0] or not all( - parts[1:(4 if prefix else 5)]): + if ( + len(parts) != 5 + or parts[0] + or not all(parts[1 : (4 if prefix else 5)]) + ): if prefix: raise ValueError('path must at least contain /v1/a/c/') else: - raise ValueError('path must be full path to an object' - ' e.g. /v1/a/c/o') + raise ValueError( + 'path must be full path to an object' ' e.g. /v1/a/c/o' + ) standard_methods = ['GET', 'PUT', 'HEAD', 'POST', 'DELETE'] if method.upper() not in standard_methods: - self.log.warning('Non default HTTP method %s for tempurl ' - 'specified, possibly an error', method.upper()) + self.log.warning( + 'Non default HTTP method %s for tempurl ' + 'specified, possibly an error', + method.upper(), + ) if not absolute: expiration = int(time.time() + timestamp) else: expiration = timestamp - hmac_parts = [method.upper(), str(expiration), - ('prefix:' if prefix else '') + path_for_body] + hmac_parts = [ + method.upper(), + str(expiration), + ('prefix:' if prefix else '') + path_for_body, + ] if ip_range: if isinstance(ip_range, bytes): try: ip_range = ip_range.decode('utf-8') except UnicodeDecodeError: - raise ValueError( - 'ip_range must be representable as UTF-8' - ) + raise ValueError('ip_range must be representable as UTF-8') hmac_parts.insert(0, "ip=%s" % ip_range) hmac_body = u'\n'.join(hmac_parts) temp_url_key = self._check_temp_url_key(temp_url_key=temp_url_key) - sig = hmac.new(temp_url_key, hmac_body.encode('utf-8'), - sha1).hexdigest() + sig = hmac.new( + temp_url_key, hmac_body.encode('utf-8'), sha1 + ).hexdigest() if iso8601: expiration = time.strftime( - EXPIRES_ISO8601_FORMAT, time.gmtime(expiration)) + EXPIRES_ISO8601_FORMAT, time.gmtime(expiration) + ) temp_url = u'{path}?temp_url_sig={sig}&temp_url_expires={exp}'.format( - path=path_for_body, sig=sig, exp=expiration) + path=path_for_body, sig=sig, exp=expiration + ) if ip_range: temp_url += u'&temp_url_ip_range={}'.format(ip_range) @@ -1020,11 +1122,7 @@ def _delete_autocreated_image_objects( # ========== Project Cleanup ========== def _get_cleanup_dependencies(self): - return { - 'object_store': { - 'before': [] - } - } + return {'object_store': {'before': []}} def _service_cleanup( self, @@ -1032,7 +1130,7 @@ def _service_cleanup( client_status_queue=None, identified_resources=None, filters=None, - resource_evaluation_fn=None + resource_evaluation_fn=None, ): is_bulk_delete_supported = False bulk_delete_max_per_request = None @@ -1044,7 +1142,8 @@ def _service_cleanup( bulk_delete = caps.swift.get("bulk_delete", {}) is_bulk_delete_supported = bulk_delete is not None bulk_delete_max_per_request = bulk_delete.get( - "max_deletes_per_request", 100) + "max_deletes_per_request", 100 + ) elements = [] for cont in self.containers(): @@ -1058,7 +1157,8 @@ def _service_cleanup( client_status_queue=client_status_queue, identified_resources=identified_resources, filters=filters, - resource_evaluation_fn=resource_evaluation_fn) + resource_evaluation_fn=resource_evaluation_fn, + ) if need_delete: if not is_bulk_delete_supported and not dry_run: self.delete_object(obj, cont) @@ -1083,7 +1183,8 @@ def _service_cleanup( client_status_queue=client_status_queue, identified_resources=identified_resources, filters=filters, - resource_evaluation_fn=resource_evaluation_fn) + resource_evaluation_fn=resource_evaluation_fn, + ) def _bulk_delete(self, elements, dry_run=False): data = "\n".join([parse.quote(x) for x in elements]) @@ -1093,6 +1194,6 @@ def _bulk_delete(self, elements, dry_run=False): data=data, headers={ 'Content-Type': 'text/plain', - 'Accept': 'application/json' - } + 'Accept': 'application/json', + }, ) diff --git a/openstack/object_store/v1/account.py b/openstack/object_store/v1/account.py index eb3496bfa..e4d17db9e 100644 --- a/openstack/object_store/v1/account.py +++ b/openstack/object_store/v1/account.py @@ -28,8 +28,9 @@ class Account(_base.BaseResource): #: the account. account_bytes_used = resource.Header("x-account-bytes-used", type=int) #: The number of containers. - account_container_count = resource.Header("x-account-container-count", - type=int) + account_container_count = resource.Header( + "x-account-container-count", type=int + ) #: The number of objects in the account. account_object_count = resource.Header("x-account-object-count", type=int) #: The secret key value for temporary URLs. If not set, diff --git a/openstack/object_store/v1/container.py b/openstack/object_store/v1/container.py index b9c84fd22..5c225e2e0 100644 --- a/openstack/object_store/v1/container.py +++ b/openstack/object_store/v1/container.py @@ -25,7 +25,7 @@ class Container(_base.BaseResource): "read_ACL": "x-container-read", "write_ACL": "x-container-write", "sync_to": "x-container-sync-to", - "sync_key": "x-container-sync-key" + "sync_key": "x-container-sync-key", } base_path = "/" @@ -38,9 +38,7 @@ class Container(_base.BaseResource): allow_list = True allow_head = True - _query_mapping = resource.QueryParameters( - 'prefix', 'format' - ) + _query_mapping = resource.QueryParameters('prefix', 'format') # Container body data (when id=None) #: The name of the container. @@ -54,10 +52,12 @@ class Container(_base.BaseResource): # Container metadata (when id=name) #: The number of objects. object_count = resource.Header( - "x-container-object-count", type=int, alias='count') + "x-container-object-count", type=int, alias='count' + ) #: The count of bytes used in total. bytes_used = resource.Header( - "x-container-bytes-used", type=int, alias='bytes') + "x-container-bytes-used", type=int, alias='bytes' + ) #: The timestamp of the transaction. timestamp = resource.Header("x-timestamp") @@ -94,8 +94,9 @@ class Container(_base.BaseResource): #: If set to true, Object Storage guesses the content type based #: on the file extension and ignores the value sent in the #: Content-Type header, if present. *Type: bool* - is_content_type_detected = resource.Header("x-detect-content-type", - type=bool) + is_content_type_detected = resource.Header( + "x-detect-content-type", type=bool + ) #: Storage policy used by the container. #: It is not possible to change policy of an existing container @@ -136,9 +137,9 @@ def create(self, session, prepend_key=True, base_path=None): :data:`Resource.allow_create` is not set to ``True``. """ request = self._prepare_request( - requires_id=True, prepend_key=prepend_key, base_path=base_path) - response = session.put( - request.url, headers=request.headers) + requires_id=True, prepend_key=prepend_key, base_path=base_path + ) + response = session.put(request.url, headers=request.headers) self._translate_response(response, has_body=False) return self diff --git a/openstack/object_store/v1/info.py b/openstack/object_store/v1/info.py index 76e740540..a4f048eab 100644 --- a/openstack/object_store/v1/info.py +++ b/openstack/object_store/v1/info.py @@ -34,8 +34,12 @@ class Info(resource.Resource): tempurl = resource.Body("tempurl", type=dict) def fetch( - self, session, requires_id=False, - base_path=None, skip_cache=False, error_message=None + self, + session, + requires_id=False, + base_path=None, + skip_cache=False, + error_message=None, ): """Get a remote resource based on this instance. @@ -64,7 +68,8 @@ def fetch( session = self._get_session(session) endpoint = urllib.parse.urlparse(session.get_endpoint()) url = "{scheme}://{netloc}/info".format( - scheme=endpoint.scheme, netloc=endpoint.netloc) + scheme=endpoint.scheme, netloc=endpoint.netloc + ) microversion = self._get_microversion(session, action='fetch') response = session.get(url, microversion=microversion) diff --git a/openstack/object_store/v1/obj.py b/openstack/object_store/v1/obj.py index 095a2a56e..af95f2ca3 100644 --- a/openstack/object_store/v1/obj.py +++ b/openstack/object_store/v1/obj.py @@ -30,7 +30,7 @@ class Object(_base.BaseResource): "is_content_type_detected": "x-detect-content-type", "manifest": "x-object-manifest", # Rax hack - the need CORS as different header - "access_control_allow_origin": "access-control-allow-origin" + "access_control_allow_origin": "access-control-allow-origin", } base_path = "/%(container)s" @@ -44,10 +44,14 @@ class Object(_base.BaseResource): allow_head = True _query_mapping = resource.QueryParameters( - 'prefix', 'format', - 'temp_url_sig', 'temp_url_expires', - 'filename', 'multipart_manifest', 'symlink', - multipart_manifest='multipart-manifest' + 'prefix', + 'format', + 'temp_url_sig', + 'temp_url_expires', + 'filename', + 'multipart_manifest', + 'symlink', + multipart_manifest='multipart-manifest', ) # Data to be passed during a POST call to create an object on the server. @@ -117,7 +121,8 @@ class Object(_base.BaseResource): #: size of the response body. Instead it contains the size of #: the object, in bytes. content_length = resource.Header( - "content-length", type=int, alias='_bytes') + "content-length", type=int, alias='_bytes' + ) #: The MIME type of the object. content_type = resource.Header("content-type", alias="_content_type") #: The type of ranges that the object accepts. @@ -136,8 +141,9 @@ class Object(_base.BaseResource): etag = resource.Header("etag", alias='_hash') #: Set to True if this object is a static large object manifest object. #: *Type: bool* - is_static_large_object = resource.Header("x-static-large-object", - type=bool) + is_static_large_object = resource.Header( + "x-static-large-object", type=bool + ) #: If set, the value of the Content-Encoding metadata. #: If not set, this header is not returned by this operation. content_encoding = resource.Header("content-encoding") @@ -164,9 +170,8 @@ class Object(_base.BaseResource): #: The date and time that the object was created or the last #: time that the metadata was changed. last_modified_at = resource.Header( - "last-modified", - alias='_last_modified', - aka='updated_at') + "last-modified", alias='_last_modified', aka='updated_at' + ) # Headers for PUT and POST requests #: Set to chunked to enable chunked transfer encoding. If used, @@ -175,8 +180,9 @@ class Object(_base.BaseResource): #: If set to true, Object Storage guesses the content type based #: on the file extension and ignores the value sent in the #: Content-Type header, if present. *Type: bool* - is_content_type_detected = resource.Header("x-detect-content-type", - type=bool) + is_content_type_detected = resource.Header( + "x-detect-content-type", type=bool + ) #: If set, this is the name of an object used to create the new #: object by copying the X-Copy-From object. The value is in form #: {container}/{object}. You must UTF-8-encode and then URL-encode @@ -195,7 +201,8 @@ class Object(_base.BaseResource): #: CORS for RAX (deviating from standard) access_control_allow_origin = resource.Header( - "access-control-allow-origin") + "access-control-allow-origin" + ) has_body = False @@ -209,8 +216,9 @@ def __init__(self, data=None, **attrs): def set_metadata(self, session, metadata): # Filter out items with empty values so the create metadata behaviour # is the same as account and container - filtered_metadata = \ - {key: value for key, value in metadata.items() if value} + filtered_metadata = { + key: value for key, value in metadata.items() if value + } # Update from remote if we only have locally created information if not self.last_modified_at: @@ -281,9 +289,11 @@ def delete_metadata(self, session, keys): request = self._prepare_request() response = session.post( - request.url, headers=self._calculate_headers(metadata)) + request.url, headers=self._calculate_headers(metadata) + ) exceptions.raise_from_response( - response, error_message="Error deleting metadata keys") + response, error_message="Error deleting metadata keys" + ) # Only delete from local object if the remote delete was successful for key in attr_keys_to_delete: @@ -296,7 +306,8 @@ def _download(self, session, error_message=None, stream=False): request = self._prepare_request() response = session.get( - request.url, headers=request.headers, stream=stream) + request.url, headers=request.headers, stream=stream + ) exceptions.raise_from_response(response, error_message=error_message) return response @@ -306,16 +317,15 @@ def download(self, session, error_message=None): def stream(self, session, error_message=None, chunk_size=1024): response = self._download( - session, error_message=error_message, stream=True) + session, error_message=error_message, stream=True + ) return response.iter_content(chunk_size, decode_unicode=False) def create(self, session, base_path=None, **params): request = self._prepare_request(base_path=base_path) response = session.put( - request.url, - data=self.data, - headers=request.headers + request.url, data=self.data, headers=request.headers ) self._translate_response(response, has_body=False) return self @@ -339,6 +349,5 @@ def _raw_delete(self, session, microversion=None): headers['multipart-manifest'] = 'delete' return session.delete( - request.url, - headers=headers, - microversion=microversion) + request.url, headers=headers, microversion=microversion + ) diff --git a/openstack/tests/functional/object_store/v1/test_account.py b/openstack/tests/functional/object_store/v1/test_account.py index a71e63a4e..abdf821e7 100644 --- a/openstack/tests/functional/object_store/v1/test_account.py +++ b/openstack/tests/functional/object_store/v1/test_account.py @@ -14,7 +14,6 @@ class TestAccount(base.BaseFunctionalTest): - def setUp(self): super(TestAccount, self).setUp() self.require_service('object-store') diff --git a/openstack/tests/functional/object_store/v1/test_container.py b/openstack/tests/functional/object_store/v1/test_container.py index 25e0f7ad5..d0211497b 100644 --- a/openstack/tests/functional/object_store/v1/test_container.py +++ b/openstack/tests/functional/object_store/v1/test_container.py @@ -15,7 +15,6 @@ class TestContainer(base.BaseFunctionalTest): - def setUp(self): super(TestContainer, self).setUp() self.require_service('object-store') @@ -24,7 +23,9 @@ def setUp(self): container = self.conn.object_store.create_container(name=self.NAME) self.addEmptyCleanup( self.conn.object_store.delete_container, - self.NAME, ignore_missing=False) + self.NAME, + ignore_missing=False, + ) assert isinstance(container, _container.Container) self.assertEqual(self.NAME, container.name) @@ -43,21 +44,24 @@ def test_system_metadata(self): self.assertIsNone(container.read_ACL) self.assertIsNone(container.write_ACL) self.conn.object_store.set_container_metadata( - container, read_ACL='.r:*', write_ACL='demo:demo') + container, read_ACL='.r:*', write_ACL='demo:demo' + ) container = self.conn.object_store.get_container_metadata(self.NAME) self.assertEqual('.r:*', container.read_ACL) self.assertEqual('demo:demo', container.write_ACL) # update system metadata self.conn.object_store.set_container_metadata( - container, read_ACL='.r:demo') + container, read_ACL='.r:demo' + ) container = self.conn.object_store.get_container_metadata(self.NAME) self.assertEqual('.r:demo', container.read_ACL) self.assertEqual('demo:demo', container.write_ACL) # set system metadata and custom metadata self.conn.object_store.set_container_metadata( - container, k0='v0', sync_key='1234') + container, k0='v0', sync_key='1234' + ) container = self.conn.object_store.get_container_metadata(self.NAME) self.assertTrue(container.metadata) self.assertIn('k0', container.metadata) @@ -67,8 +71,9 @@ def test_system_metadata(self): self.assertEqual('1234', container.sync_key) # unset system metadata - self.conn.object_store.delete_container_metadata(container, - ['sync_key']) + self.conn.object_store.delete_container_metadata( + container, ['sync_key'] + ) container = self.conn.object_store.get_container_metadata(self.NAME) self.assertTrue(container.metadata) self.assertIn('k0', container.metadata) diff --git a/openstack/tests/functional/object_store/v1/test_obj.py b/openstack/tests/functional/object_store/v1/test_obj.py index fdbefcf6f..7012325b5 100644 --- a/openstack/tests/functional/object_store/v1/test_obj.py +++ b/openstack/tests/functional/object_store/v1/test_obj.py @@ -26,19 +26,25 @@ def setUp(self): self.conn.object_store.create_container(name=self.FOLDER) self.addCleanup(self.conn.object_store.delete_container, self.FOLDER) self.sot = self.conn.object_store.upload_object( - container=self.FOLDER, name=self.FILE, data=self.DATA) + container=self.FOLDER, name=self.FILE, data=self.DATA + ) self.addEmptyCleanup( - self.conn.object_store.delete_object, self.sot, - ignore_missing=False) + self.conn.object_store.delete_object, + self.sot, + ignore_missing=False, + ) def test_list(self): - names = [o.name for o - in self.conn.object_store.objects(container=self.FOLDER)] + names = [ + o.name + for o in self.conn.object_store.objects(container=self.FOLDER) + ] self.assertIn(self.FILE, names) def test_download_object(self): result = self.conn.object_store.download_object( - self.FILE, container=self.FOLDER) + self.FILE, container=self.FOLDER + ) self.assertEqual(self.DATA, result) result = self.conn.object_store.download_object(self.sot) self.assertEqual(self.DATA, result) @@ -46,25 +52,29 @@ def test_download_object(self): def test_system_metadata(self): # get system metadata obj = self.conn.object_store.get_object_metadata( - self.FILE, container=self.FOLDER) + self.FILE, container=self.FOLDER + ) # TODO(shade) obj.bytes is coming up None on python3 but not python2 # self.assertGreaterEqual(0, obj.bytes) self.assertIsNotNone(obj.etag) # set system metadata obj = self.conn.object_store.get_object_metadata( - self.FILE, container=self.FOLDER) + self.FILE, container=self.FOLDER + ) self.assertIsNone(obj.content_disposition) self.assertIsNone(obj.content_encoding) self.conn.object_store.set_object_metadata( - obj, content_disposition='attachment', content_encoding='gzip') + obj, content_disposition='attachment', content_encoding='gzip' + ) obj = self.conn.object_store.get_object_metadata(obj) self.assertEqual('attachment', obj.content_disposition) self.assertEqual('gzip', obj.content_encoding) # update system metadata self.conn.object_store.set_object_metadata( - obj, content_encoding='deflate') + obj, content_encoding='deflate' + ) obj = self.conn.object_store.get_object_metadata(obj) self.assertEqual('attachment', obj.content_disposition) self.assertEqual('deflate', obj.content_encoding) @@ -79,7 +89,8 @@ def test_system_metadata(self): # unset more system metadata self.conn.object_store.delete_object_metadata( - obj, keys=['content_disposition']) + obj, keys=['content_disposition'] + ) obj = self.conn.object_store.get_object_metadata(obj) self.assertIn('k0', obj.metadata) self.assertEqual('v0', obj.metadata['k0']) @@ -90,7 +101,8 @@ def test_system_metadata(self): def test_custom_metadata(self): # get custom metadata obj = self.conn.object_store.get_object_metadata( - self.FILE, container=self.FOLDER) + self.FILE, container=self.FOLDER + ) self.assertFalse(obj.metadata) # set no custom metadata @@ -112,8 +124,9 @@ def test_custom_metadata(self): self.assertEqual('v1', obj.metadata['k1']) # set more custom metadata by named object and container - self.conn.object_store.set_object_metadata(self.FILE, self.FOLDER, - k2='v2') + self.conn.object_store.set_object_metadata( + self.FILE, self.FOLDER, k2='v2' + ) obj = self.conn.object_store.get_object_metadata(obj) self.assertTrue(obj.metadata) self.assertEqual(2, len(obj.metadata)) diff --git a/openstack/tests/unit/object_store/v1/test_account.py b/openstack/tests/unit/object_store/v1/test_account.py index 61bd80f88..d0ca018d5 100644 --- a/openstack/tests/unit/object_store/v1/test_account.py +++ b/openstack/tests/unit/object_store/v1/test_account.py @@ -24,12 +24,11 @@ 'x-account-container-count': '678', 'content-type': 'text/plain; charset=utf-8', 'x-account-object-count': '98765', - 'x-timestamp': '1453413555.88937' + 'x-timestamp': '1453413555.88937', } class TestAccount(base.TestCase): - def setUp(self): super(TestAccount, self).setUp() self.endpoint = self.cloud.object_store.get_endpoint() + '/' @@ -49,28 +48,41 @@ def test_basic(self): def test_make_it(self): sot = account.Account(**ACCOUNT_EXAMPLE) self.assertIsNone(sot.id) - self.assertEqual(int(ACCOUNT_EXAMPLE['x-account-bytes-used']), - sot.account_bytes_used) - self.assertEqual(int(ACCOUNT_EXAMPLE['x-account-container-count']), - sot.account_container_count) - self.assertEqual(int(ACCOUNT_EXAMPLE['x-account-object-count']), - sot.account_object_count) + self.assertEqual( + int(ACCOUNT_EXAMPLE['x-account-bytes-used']), + sot.account_bytes_used, + ) + self.assertEqual( + int(ACCOUNT_EXAMPLE['x-account-container-count']), + sot.account_container_count, + ) + self.assertEqual( + int(ACCOUNT_EXAMPLE['x-account-object-count']), + sot.account_object_count, + ) self.assertEqual(ACCOUNT_EXAMPLE['x-timestamp'], sot.timestamp) def test_set_temp_url_key(self): sot = account.Account() key = 'super-secure-key' - self.register_uris([ - dict(method='POST', uri=self.endpoint, - status_code=204, - validate=dict( - headers={ - 'x-account-meta-temp-url-key': key})), - dict(method='HEAD', uri=self.endpoint, - headers={ - 'x-account-meta-temp-url-key': key}), - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.endpoint, + status_code=204, + validate=dict( + headers={'x-account-meta-temp-url-key': key} + ), + ), + dict( + method='HEAD', + uri=self.endpoint, + headers={'x-account-meta-temp-url-key': key}, + ), + ] + ) sot.set_temp_url_key(self.cloud.object_store, key) self.assert_calls() @@ -78,15 +90,22 @@ def test_set_account_temp_url_key_second(self): sot = account.Account() key = 'super-secure-key' - self.register_uris([ - dict(method='POST', uri=self.endpoint, - status_code=204, - validate=dict( - headers={ - 'x-account-meta-temp-url-key-2': key})), - dict(method='HEAD', uri=self.endpoint, - headers={ - 'x-account-meta-temp-url-key-2': key}), - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.endpoint, + status_code=204, + validate=dict( + headers={'x-account-meta-temp-url-key-2': key} + ), + ), + dict( + method='HEAD', + uri=self.endpoint, + headers={'x-account-meta-temp-url-key-2': key}, + ), + ] + ) sot.set_temp_url_key(self.cloud.object_store, key, secondary=True) self.assert_calls() diff --git a/openstack/tests/unit/object_store/v1/test_container.py b/openstack/tests/unit/object_store/v1/test_container.py index 3c58bf17d..dc1f2e539 100644 --- a/openstack/tests/unit/object_store/v1/test_container.py +++ b/openstack/tests/unit/object_store/v1/test_container.py @@ -17,13 +17,13 @@ class TestContainer(base.TestCase): - def setUp(self): super(TestContainer, self).setUp() self.container = self.getUniqueString() self.endpoint = self.cloud.object_store.get_endpoint() + '/' self.container_endpoint = '{endpoint}{container}'.format( - endpoint=self.endpoint, container=self.container) + endpoint=self.endpoint, container=self.container + ) self.body = { "count": 2, @@ -42,7 +42,7 @@ def setUp(self): 'x-history-location': 'history-location', 'content-type': 'application/json; charset=utf-8', 'x-timestamp': '1453414055.48672', - 'x-storage-policy': 'Gold' + 'x-storage-policy': 'Gold', } self.body_plus_headers = dict(self.body, **self.headers) @@ -81,49 +81,44 @@ def test_create_and_head(self): # Attributes from header self.assertEqual( int(self.body_plus_headers['x-container-object-count']), - sot.object_count) + sot.object_count, + ) self.assertEqual( int(self.body_plus_headers['x-container-bytes-used']), - sot.bytes_used) + sot.bytes_used, + ) self.assertEqual( - self.body_plus_headers['x-container-read'], - sot.read_ACL) + self.body_plus_headers['x-container-read'], sot.read_ACL + ) self.assertEqual( - self.body_plus_headers['x-container-write'], - sot.write_ACL) + self.body_plus_headers['x-container-write'], sot.write_ACL + ) self.assertEqual( - self.body_plus_headers['x-container-sync-to'], - sot.sync_to) + self.body_plus_headers['x-container-sync-to'], sot.sync_to + ) self.assertEqual( - self.body_plus_headers['x-container-sync-key'], - sot.sync_key) + self.body_plus_headers['x-container-sync-key'], sot.sync_key + ) self.assertEqual( self.body_plus_headers['x-versions-location'], - sot.versions_location) + sot.versions_location, + ) self.assertEqual( - self.body_plus_headers['x-history-location'], - sot.history_location) + self.body_plus_headers['x-history-location'], sot.history_location + ) self.assertEqual(self.body_plus_headers['x-timestamp'], sot.timestamp) - self.assertEqual(self.body_plus_headers['x-storage-policy'], - sot.storage_policy) + self.assertEqual( + self.body_plus_headers['x-storage-policy'], sot.storage_policy + ) def test_list(self): containers = [ - { - "count": 999, - "bytes": 12345, - "name": "container1" - }, - { - "count": 888, - "bytes": 54321, - "name": "container2" - } + {"count": 999, "bytes": 12345, "name": "container1"}, + {"count": 888, "bytes": 54321, "name": "container2"}, ] - self.register_uris([ - dict(method='GET', uri=self.endpoint, - json=containers) - ]) + self.register_uris( + [dict(method='GET', uri=self.endpoint, json=containers)] + ) response = container.Container.list(self.cloud.object_store) @@ -144,25 +139,32 @@ def _test_create_update(self, sot, sot_call, sess_method): "x-container-read": "some ACL", "x-container-write": "another ACL", "x-detect-content-type": 'True', - "X-Container-Meta-foo": "bar" + "X-Container-Meta-foo": "bar", } - self.register_uris([ - dict(method=sess_method, uri=self.container_endpoint, - json=self.body, - validate=dict(headers=headers)), - ]) + self.register_uris( + [ + dict( + method=sess_method, + uri=self.container_endpoint, + json=self.body, + validate=dict(headers=headers), + ), + ] + ) sot_call(self.cloud.object_store) self.assert_calls() def test_create(self): sot = container.Container.new( - name=self.container, metadata={'foo': 'bar'}) + name=self.container, metadata={'foo': 'bar'} + ) self._test_create_update(sot, sot.create, 'PUT') def test_commit(self): sot = container.Container.new( - name=self.container, metadata={'foo': 'bar'}) + name=self.container, metadata={'foo': 'bar'} + ) self._test_create_update(sot, sot.commit, 'POST') def test_to_dict_recursion(self): @@ -200,15 +202,22 @@ def test_to_json(self): 'versions_location': None, 'history_location': None, 'write_ACL': None, - 'storage_policy': None - }, json.loads(json.dumps(sot))) + 'storage_policy': None, + }, + json.loads(json.dumps(sot)), + ) def _test_no_headers(self, sot, sot_call, sess_method): headers = {} - self.register_uris([ - dict(method=sess_method, uri=self.container_endpoint, - validate=dict(headers=headers)) - ]) + self.register_uris( + [ + dict( + method=sess_method, + uri=self.container_endpoint, + validate=dict(headers=headers), + ) + ] + ) sot_call(self.cloud.object_store) def test_create_no_headers(self): @@ -225,16 +234,23 @@ def test_set_temp_url_key(self): sot = container.Container.new(name=self.container) key = self.getUniqueString() - self.register_uris([ - dict(method='POST', uri=self.container_endpoint, - status_code=204, - validate=dict( - headers={ - 'x-container-meta-temp-url-key': key})), - dict(method='HEAD', uri=self.container_endpoint, - headers={ - 'x-container-meta-temp-url-key': key}), - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.container_endpoint, + status_code=204, + validate=dict( + headers={'x-container-meta-temp-url-key': key} + ), + ), + dict( + method='HEAD', + uri=self.container_endpoint, + headers={'x-container-meta-temp-url-key': key}, + ), + ] + ) sot.set_temp_url_key(self.cloud.object_store, key) self.assert_calls() @@ -242,15 +258,22 @@ def test_set_temp_url_key_second(self): sot = container.Container.new(name=self.container) key = self.getUniqueString() - self.register_uris([ - dict(method='POST', uri=self.container_endpoint, - status_code=204, - validate=dict( - headers={ - 'x-container-meta-temp-url-key-2': key})), - dict(method='HEAD', uri=self.container_endpoint, - headers={ - 'x-container-meta-temp-url-key-2': key}), - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.container_endpoint, + status_code=204, + validate=dict( + headers={'x-container-meta-temp-url-key-2': key} + ), + ), + dict( + method='HEAD', + uri=self.container_endpoint, + headers={'x-container-meta-temp-url-key-2': key}, + ), + ] + ) sot.set_temp_url_key(self.cloud.object_store, key, secondary=True) self.assert_calls() diff --git a/openstack/tests/unit/object_store/v1/test_obj.py b/openstack/tests/unit/object_store/v1/test_obj.py index 30f58a3f4..f3f6cb7c1 100644 --- a/openstack/tests/unit/object_store/v1/test_obj.py +++ b/openstack/tests/unit/object_store/v1/test_obj.py @@ -27,7 +27,6 @@ class TestObject(base_test_object.BaseTestObject): - def setUp(self): super(TestObject, self).setUp() self.the_data = b'test body' @@ -39,7 +38,7 @@ def setUp(self): "last_modified": "2014-07-13T18:41:03.319240", "bytes": self.the_data_length, "name": self.object, - "content_type": "application/octet-stream" + "content_type": "application/octet-stream", } self.headers = { 'Content-Length': str(len(self.the_data)), @@ -78,9 +77,9 @@ def test_basic(self): 'prefix': 'prefix', 'symlink': 'symlink', 'temp_url_expires': 'temp_url_expires', - 'temp_url_sig': 'temp_url_sig' + 'temp_url_sig': 'temp_url_sig', }, - sot._query_mapping._mapping + sot._query_mapping._mapping, ) def test_new(self): @@ -95,8 +94,7 @@ def test_from_body(self): # Attributes from header self.assertEqual(self.container, sot.container) - self.assertEqual( - int(self.body['bytes']), sot.content_length) + self.assertEqual(int(self.body['bytes']), sot.content_length) self.assertEqual(self.body['last_modified'], sot.last_modified_at) self.assertEqual(self.body['hash'], sot.etag) self.assertEqual(self.body['content_type'], sot.content_type) @@ -108,7 +106,8 @@ def test_from_headers(self): # Attributes from header self.assertEqual(self.container, sot.container) self.assertEqual( - int(self.headers['Content-Length']), sot.content_length) + int(self.headers['Content-Length']), sot.content_length + ) self.assertEqual(self.headers['Accept-Ranges'], sot.accept_ranges) self.assertEqual(self.headers['Last-Modified'], sot.last_modified_at) self.assertEqual(self.headers['Etag'], sot.etag) @@ -129,16 +128,19 @@ def test_download(self): headers = { 'X-Newest': 'True', 'If-Match': self.headers['Etag'], - 'Accept': '*/*' + 'Accept': '*/*', } - self.register_uris([ - dict(method='GET', uri=self.object_endpoint, - headers=self.headers, - content=self.the_data, - validate=dict( - headers=headers - )) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.object_endpoint, + headers=self.headers, + content=self.the_data, + validate=dict(headers=headers), + ) + ] + ) sot = obj.Object.new(container=self.container, name=self.object) sot.is_newest = True # if_match is a list type, but we're passing a string. This tests @@ -153,19 +155,23 @@ def test_download(self): def _test_create(self, method, data): sot = obj.Object.new( - container=self.container, name=self.object, - data=data, metadata={'foo': 'bar'}) + container=self.container, + name=self.object, + data=data, + metadata={'foo': 'bar'}, + ) sot.is_newest = True - sent_headers = { - "x-newest": 'True', - "X-Object-Meta-foo": "bar" - } - self.register_uris([ - dict(method=method, uri=self.object_endpoint, - headers=self.headers, - validate=dict( - headers=sent_headers)) - ]) + sent_headers = {"x-newest": 'True', "X-Object-Meta-foo": "bar"} + self.register_uris( + [ + dict( + method=method, + uri=self.object_endpoint, + headers=self.headers, + validate=dict(headers=sent_headers), + ) + ] + ) rv = sot.create(self.cloud.object_store) self.assertEqual(rv.etag, self.headers['Etag']) diff --git a/openstack/tests/unit/object_store/v1/test_proxy.py b/openstack/tests/unit/object_store/v1/test_proxy.py index 64b74095f..72ba171a2 100644 --- a/openstack/tests/unit/object_store/v1/test_proxy.py +++ b/openstack/tests/unit/object_store/v1/test_proxy.py @@ -47,24 +47,30 @@ def setUp(self): self.container = self.getUniqueString() self.endpoint = self.cloud.object_store.get_endpoint() + '/' self.container_endpoint = '{endpoint}{container}'.format( - endpoint=self.endpoint, container=self.container) + endpoint=self.endpoint, container=self.container + ) def test_account_metadata_get(self): self.verify_head( - self.proxy.get_account_metadata, account.Account, - method_args=[]) + self.proxy.get_account_metadata, account.Account, method_args=[] + ) def test_container_metadata_get(self): - self.verify_head(self.proxy.get_container_metadata, - container.Container, method_args=["container"]) + self.verify_head( + self.proxy.get_container_metadata, + container.Container, + method_args=["container"], + ) def test_container_delete(self): - self.verify_delete(self.proxy.delete_container, - container.Container, False) + self.verify_delete( + self.proxy.delete_container, container.Container, False + ) def test_container_delete_ignore(self): - self.verify_delete(self.proxy.delete_container, - container.Container, True) + self.verify_delete( + self.proxy.delete_container, container.Container, True + ) def test_container_create_attrs(self): self.verify_create( @@ -72,7 +78,8 @@ def test_container_create_attrs(self): container.Container, method_args=['container_name'], expected_args=[], - expected_kwargs={'name': 'container_name', "x": 1, "y": 2, "z": 3}) + expected_kwargs={'name': 'container_name', "x": 1, "y": 2, "z": 3}, + ) def test_object_metadata_get(self): self._verify( @@ -81,7 +88,8 @@ def test_object_metadata_get(self): method_args=['object'], method_kwargs={'container': 'container'}, expected_args=[obj.Object, 'object'], - expected_kwargs={'container': 'container'}) + expected_kwargs={'container': 'container'}, + ) def _test_object_delete(self, ignore): expected_kwargs = { @@ -95,7 +103,8 @@ def _test_object_delete(self, ignore): method_args=["resource"], method_kwargs=expected_kwargs, expected_args=[obj.Object, "resource"], - expected_kwargs=expected_kwargs) + expected_kwargs=expected_kwargs, + ) def test_object_delete(self): self._test_object_delete(False) @@ -108,7 +117,7 @@ def test_object_create_attrs(self): "name": "test", "data": "data", "container": "name", - "metadata": {} + "metadata": {}, } self._verify( @@ -116,52 +125,57 @@ def test_object_create_attrs(self): self.proxy.upload_object, method_kwargs=kwargs, expected_args=[obj.Object], - expected_kwargs=kwargs) + expected_kwargs=kwargs, + ) def test_object_create_no_container(self): self.assertRaises(TypeError, self.proxy.upload_object) def test_object_get(self): with requests_mock.Mocker() as m: - m.get("%scontainer/object" % self.endpoint, - text="data") + m.get("%scontainer/object" % self.endpoint, text="data") res = self.proxy.get_object("object", container="container") self.assertIsNone(res.data) def test_object_get_write_file(self): with requests_mock.Mocker() as m: - m.get("%scontainer/object" % self.endpoint, - text="data") + m.get("%scontainer/object" % self.endpoint, text="data") with tempfile.NamedTemporaryFile() as f: self.proxy.get_object( - "object", container="container", - outfile=f.name) + "object", container="container", outfile=f.name + ) dt = open(f.name).read() self.assertEqual(dt, "data") def test_object_get_remember_content(self): with requests_mock.Mocker() as m: - m.get("%scontainer/object" % self.endpoint, - text="data") + m.get("%scontainer/object" % self.endpoint, text="data") res = self.proxy.get_object( - "object", container="container", - remember_content=True) + "object", container="container", remember_content=True + ) self.assertEqual(res.data, "data") def test_set_temp_url_key(self): key = 'super-secure-key' - self.register_uris([ - dict(method='POST', uri=self.endpoint, - status_code=204, - validate=dict( - headers={ - 'x-account-meta-temp-url-key': key})), - dict(method='HEAD', uri=self.endpoint, - headers={ - 'x-account-meta-temp-url-key': key}), - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.endpoint, + status_code=204, + validate=dict( + headers={'x-account-meta-temp-url-key': key} + ), + ), + dict( + method='HEAD', + uri=self.endpoint, + headers={'x-account-meta-temp-url-key': key}, + ), + ] + ) self.proxy.set_account_temp_url_key(key) self.assert_calls() @@ -169,16 +183,23 @@ def test_set_account_temp_url_key_second(self): key = 'super-secure-key' - self.register_uris([ - dict(method='POST', uri=self.endpoint, - status_code=204, - validate=dict( - headers={ - 'x-account-meta-temp-url-key-2': key})), - dict(method='HEAD', uri=self.endpoint, - headers={ - 'x-account-meta-temp-url-key-2': key}), - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.endpoint, + status_code=204, + validate=dict( + headers={'x-account-meta-temp-url-key-2': key} + ), + ), + dict( + method='HEAD', + uri=self.endpoint, + headers={'x-account-meta-temp-url-key-2': key}, + ), + ] + ) self.proxy.set_account_temp_url_key(key, secondary=True) self.assert_calls() @@ -186,16 +207,23 @@ def test_set_container_temp_url_key(self): key = 'super-secure-key' - self.register_uris([ - dict(method='POST', uri=self.container_endpoint, - status_code=204, - validate=dict( - headers={ - 'x-container-meta-temp-url-key': key})), - dict(method='HEAD', uri=self.container_endpoint, - headers={ - 'x-container-meta-temp-url-key': key}), - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.container_endpoint, + status_code=204, + validate=dict( + headers={'x-container-meta-temp-url-key': key} + ), + ), + dict( + method='HEAD', + uri=self.container_endpoint, + headers={'x-container-meta-temp-url-key': key}, + ), + ] + ) self.proxy.set_container_temp_url_key(self.container, key) self.assert_calls() @@ -203,18 +231,26 @@ def test_set_container_temp_url_key_second(self): key = 'super-secure-key' - self.register_uris([ - dict(method='POST', uri=self.container_endpoint, - status_code=204, - validate=dict( - headers={ - 'x-container-meta-temp-url-key-2': key})), - dict(method='HEAD', uri=self.container_endpoint, - headers={ - 'x-container-meta-temp-url-key-2': key}), - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.container_endpoint, + status_code=204, + validate=dict( + headers={'x-container-meta-temp-url-key-2': key} + ), + ), + dict( + method='HEAD', + uri=self.container_endpoint, + headers={'x-container-meta-temp-url-key-2': key}, + ), + ] + ) self.proxy.set_container_temp_url_key( - self.container, key, secondary=True) + self.container, key, secondary=True + ) self.assert_calls() def test_copy_object(self): @@ -222,9 +258,10 @@ def test_copy_object(self): def test_file_segment(self): file_size = 4200 - content = ''.join(random.choice( - string.ascii_uppercase + string.digits) - for _ in range(file_size)).encode('latin-1') + content = ''.join( + random.choice(string.ascii_uppercase + string.digits) + for _ in range(file_size) + ).encode('latin-1') self.imagefile = tempfile.NamedTemporaryFile(delete=False) self.imagefile.write(content) self.imagefile.close() @@ -233,50 +270,60 @@ def test_file_segment(self): endpoint='test_container/test_image', filename=self.imagefile.name, file_size=file_size, - segment_size=1000) + segment_size=1000, + ) self.assertEqual(len(segments), 5) segment_content = b'' for (index, (name, segment)) in enumerate(segments.items()): self.assertEqual( 'test_container/test_image/{index:0>6}'.format(index=index), - name) + name, + ) segment_content += segment.read() self.assertEqual(content, segment_content) class TestDownloadObject(base_test_object.BaseTestObject): - def setUp(self): super(TestDownloadObject, self).setUp() self.the_data = b'test body' - self.register_uris([ - dict(method='GET', uri=self.object_endpoint, - headers={ - 'Content-Length': str(len(self.the_data)), - 'Content-Type': 'application/octet-stream', - 'Accept-Ranges': 'bytes', - 'Last-Modified': 'Thu, 15 Dec 2016 13:34:14 GMT', - 'Etag': '"b5c454b44fbd5344793e3fb7e3850768"', - 'X-Timestamp': '1481808853.65009', - 'X-Trans-Id': 'tx68c2a2278f0c469bb6de1-005857ed80dfw1', - 'Date': 'Mon, 19 Dec 2016 14:24:00 GMT', - 'X-Static-Large-Object': 'True', - 'X-Object-Meta-Mtime': '1481513709.168512', - }, - content=self.the_data)]) + self.register_uris( + [ + dict( + method='GET', + uri=self.object_endpoint, + headers={ + 'Content-Length': str(len(self.the_data)), + 'Content-Type': 'application/octet-stream', + 'Accept-Ranges': 'bytes', + 'Last-Modified': 'Thu, 15 Dec 2016 13:34:14 GMT', + 'Etag': '"b5c454b44fbd5344793e3fb7e3850768"', + 'X-Timestamp': '1481808853.65009', + 'X-Trans-Id': 'tx68c2a2278f0c469bb6de1-005857ed80dfw1', + 'Date': 'Mon, 19 Dec 2016 14:24:00 GMT', + 'X-Static-Large-Object': 'True', + 'X-Object-Meta-Mtime': '1481513709.168512', + }, + content=self.the_data, + ) + ] + ) def test_download(self): data = self.cloud.object_store.download_object( - self.object, container=self.container) + self.object, container=self.container + ) self.assertEqual(data, self.the_data) self.assert_calls() def test_stream(self): chunk_size = 2 - for index, chunk in enumerate(self.cloud.object_store.stream_object( - self.object, container=self.container, - chunk_size=chunk_size)): + for index, chunk in enumerate( + self.cloud.object_store.stream_object( + self.object, container=self.container, chunk_size=chunk_size + ) + ): chunk_len = len(chunk) start = index * chunk_size end = start + chunk_len @@ -290,12 +337,17 @@ class TestExtractName(TestObjectStoreProxy): scenarios = [ ('discovery', dict(url='/', parts=['account'])), ('endpoints', dict(url='/endpoints', parts=['endpoints'])), - ('container', dict(url='/AUTH_123/container_name', - parts=['container'])), - ('object', dict(url='/container_name/object_name', - parts=['object'])), - ('object_long', dict(url='/v1/AUTH_123/cnt/path/deep/object_name', - parts=['object'])) + ( + 'container', + dict(url='/AUTH_123/container_name', parts=['container']), + ), + ('object', dict(url='/container_name/object_name', parts=['object'])), + ( + 'object_long', + dict( + url='/v1/AUTH_123/cnt/path/deep/object_name', parts=['object'] + ), + ), ] def test_extract_name(self): @@ -307,36 +359,44 @@ def test_extract_name(self): class TestTempURL(TestObjectStoreProxy): expires_iso8601_format = '%Y-%m-%dT%H:%M:%SZ' short_expires_iso8601_format = '%Y-%m-%d' - time_errmsg = ('time must either be a whole number or in specific ' - 'ISO 8601 format.') + time_errmsg = ( + 'time must either be a whole number or in specific ' 'ISO 8601 format.' + ) path_errmsg = 'path must be full path to an object e.g. /v1/a/c/o' url = '/v1/AUTH_account/c/o' seconds = 3600 key = 'correcthorsebatterystaple' method = 'GET' - expected_url = url + ('?temp_url_sig=temp_url_signature' - '&temp_url_expires=1400003600') - expected_body = '\n'.join([ - method, - '1400003600', - url, - ]).encode('utf-8') + expected_url = url + ( + '?temp_url_sig=temp_url_signature' '&temp_url_expires=1400003600' + ) + expected_body = '\n'.join( + [ + method, + '1400003600', + url, + ] + ).encode('utf-8') @mock.patch('hmac.HMAC') @mock.patch('time.time', return_value=1400000000) def test_generate_temp_url(self, time_mock, hmac_mock): hmac_mock().hexdigest.return_value = 'temp_url_signature' url = self.proxy.generate_temp_url( - self.url, self.seconds, self.method, temp_url_key=self.key) + self.url, self.seconds, self.method, temp_url_key=self.key + ) key = self.key if not isinstance(key, bytes): key = key.encode('utf-8') self.assertEqual(url, self.expected_url) - self.assertEqual(hmac_mock.mock_calls, [ - mock.call(), - mock.call(key, self.expected_body, sha1), - mock.call().hexdigest(), - ]) + self.assertEqual( + hmac_mock.mock_calls, + [ + mock.call(), + mock.call(key, self.expected_body, sha1), + mock.call().hexdigest(), + ], + ) self.assertIsInstance(url, type(self.url)) @mock.patch('hmac.HMAC') @@ -344,62 +404,83 @@ def test_generate_temp_url(self, time_mock, hmac_mock): def test_generate_temp_url_ip_range(self, time_mock, hmac_mock): hmac_mock().hexdigest.return_value = 'temp_url_signature' ip_ranges = [ - '1.2.3.4', '1.2.3.4/24', '2001:db8::', - b'1.2.3.4', b'1.2.3.4/24', b'2001:db8::', + '1.2.3.4', + '1.2.3.4/24', + '2001:db8::', + b'1.2.3.4', + b'1.2.3.4/24', + b'2001:db8::', ] path = '/v1/AUTH_account/c/o/' - expected_url = path + ('?temp_url_sig=temp_url_signature' - '&temp_url_expires=1400003600' - '&temp_url_ip_range=') + expected_url = path + ( + '?temp_url_sig=temp_url_signature' + '&temp_url_expires=1400003600' + '&temp_url_ip_range=' + ) for ip_range in ip_ranges: hmac_mock.reset_mock() url = self.proxy.generate_temp_url( - path, self.seconds, self.method, - temp_url_key=self.key, ip_range=ip_range) + path, + self.seconds, + self.method, + temp_url_key=self.key, + ip_range=ip_range, + ) key = self.key if not isinstance(key, bytes): key = key.encode('utf-8') if isinstance(ip_range, bytes): - ip_range_expected_url = ( - expected_url + ip_range.decode('utf-8') - ) - expected_body = '\n'.join([ - 'ip=' + ip_range.decode('utf-8'), - self.method, - '1400003600', - path, - ]).encode('utf-8') + ip_range_expected_url = expected_url + ip_range.decode('utf-8') + expected_body = '\n'.join( + [ + 'ip=' + ip_range.decode('utf-8'), + self.method, + '1400003600', + path, + ] + ).encode('utf-8') else: ip_range_expected_url = expected_url + ip_range - expected_body = '\n'.join([ - 'ip=' + ip_range, - self.method, - '1400003600', - path, - ]).encode('utf-8') + expected_body = '\n'.join( + [ + 'ip=' + ip_range, + self.method, + '1400003600', + path, + ] + ).encode('utf-8') self.assertEqual(url, ip_range_expected_url) - self.assertEqual(hmac_mock.mock_calls, [ - mock.call(key, expected_body, sha1), - mock.call().hexdigest(), - ]) + self.assertEqual( + hmac_mock.mock_calls, + [ + mock.call(key, expected_body, sha1), + mock.call().hexdigest(), + ], + ) self.assertIsInstance(url, type(path)) @mock.patch('hmac.HMAC') def test_generate_temp_url_iso8601_argument(self, hmac_mock): hmac_mock().hexdigest.return_value = 'temp_url_signature' url = self.proxy.generate_temp_url( - self.url, '2014-05-13T17:53:20Z', self.method, - temp_url_key=self.key) + self.url, + '2014-05-13T17:53:20Z', + self.method, + temp_url_key=self.key, + ) self.assertEqual(url, self.expected_url) # Don't care about absolute arg. - url = self.proxy.generate_temp_url(self.url, '2014-05-13T17:53:20Z', - self.method, - temp_url_key=self.key, - absolute=True) + url = self.proxy.generate_temp_url( + self.url, + '2014-05-13T17:53:20Z', + self.method, + temp_url_key=self.key, + absolute=True, + ) self.assertEqual(url, self.expected_url) lt = time.localtime() @@ -407,14 +488,16 @@ def test_generate_temp_url_iso8601_argument(self, hmac_mock): if not isinstance(self.expected_url, str): expected_url = self.expected_url.replace( - b'1400003600', bytes(str(int(time.mktime(lt))), - encoding='ascii')) + b'1400003600', + bytes(str(int(time.mktime(lt))), encoding='ascii'), + ) else: expected_url = self.expected_url.replace( - '1400003600', str(int(time.mktime(lt)))) - url = self.proxy.generate_temp_url(self.url, expires, - self.method, - temp_url_key=self.key) + '1400003600', str(int(time.mktime(lt))) + ) + url = self.proxy.generate_temp_url( + self.url, expires, self.method, temp_url_key=self.key + ) self.assertEqual(url, expected_url) expires = time.strftime(self.short_expires_iso8601_format, lt) @@ -422,39 +505,48 @@ def test_generate_temp_url_iso8601_argument(self, hmac_mock): if not isinstance(self.expected_url, str): expected_url = self.expected_url.replace( - b'1400003600', bytes(str(int(time.mktime(lt))), - encoding='ascii')) + b'1400003600', + bytes(str(int(time.mktime(lt))), encoding='ascii'), + ) else: expected_url = self.expected_url.replace( - '1400003600', str(int(time.mktime(lt)))) - url = self.proxy.generate_temp_url(self.url, expires, - self.method, - temp_url_key=self.key) + '1400003600', str(int(time.mktime(lt))) + ) + url = self.proxy.generate_temp_url( + self.url, expires, self.method, temp_url_key=self.key + ) self.assertEqual(url, expected_url) @mock.patch('hmac.HMAC') @mock.patch('time.time', return_value=1400000000) def test_generate_temp_url_iso8601_output(self, time_mock, hmac_mock): hmac_mock().hexdigest.return_value = 'temp_url_signature' - url = self.proxy.generate_temp_url(self.url, self.seconds, - self.method, - temp_url_key=self.key, - iso8601=True) + url = self.proxy.generate_temp_url( + self.url, + self.seconds, + self.method, + temp_url_key=self.key, + iso8601=True, + ) key = self.key if not isinstance(key, bytes): key = key.encode('utf-8') - expires = time.strftime(self.expires_iso8601_format, - time.gmtime(1400003600)) + expires = time.strftime( + self.expires_iso8601_format, time.gmtime(1400003600) + ) if not isinstance(self.url, str): self.assertTrue(url.endswith(bytes(expires, 'utf-8'))) else: self.assertTrue(url.endswith(expires)) - self.assertEqual(hmac_mock.mock_calls, [ - mock.call(), - mock.call(key, self.expected_body, sha1), - mock.call().hexdigest(), - ]) + self.assertEqual( + hmac_mock.mock_calls, + [ + mock.call(), + mock.call(key, self.expected_body, sha1), + mock.call().hexdigest(), + ], + ) self.assertIsInstance(url, type(self.url)) @mock.patch('hmac.HMAC') @@ -465,25 +557,36 @@ def test_generate_temp_url_prefix(self, time_mock, hmac_mock): for p in prefixes: hmac_mock.reset_mock() path = '/v1/AUTH_account/c/' + p - expected_url = path + ('?temp_url_sig=temp_url_signature' - '&temp_url_expires=1400003600' - '&temp_url_prefix=' + p) - expected_body = '\n'.join([ - self.method, - '1400003600', - 'prefix:' + path, - ]).encode('utf-8') + expected_url = path + ( + '?temp_url_sig=temp_url_signature' + '&temp_url_expires=1400003600' + '&temp_url_prefix=' + p + ) + expected_body = '\n'.join( + [ + self.method, + '1400003600', + 'prefix:' + path, + ] + ).encode('utf-8') url = self.proxy.generate_temp_url( - path, self.seconds, self.method, prefix=True, - temp_url_key=self.key) + path, + self.seconds, + self.method, + prefix=True, + temp_url_key=self.key, + ) key = self.key if not isinstance(key, bytes): key = key.encode('utf-8') self.assertEqual(url, expected_url) - self.assertEqual(hmac_mock.mock_calls, [ - mock.call(key, expected_body, sha1), - mock.call().hexdigest(), - ]) + self.assertEqual( + hmac_mock.mock_calls, + [ + mock.call(key, expected_body, sha1), + mock.call().hexdigest(), + ], + ) self.assertIsInstance(url, type(path)) @@ -491,94 +594,142 @@ def test_generate_temp_url_invalid_path(self): self.assertRaisesRegex( ValueError, 'path must be representable as UTF-8', - self.proxy.generate_temp_url, b'/v1/a/c/\xff', self.seconds, - self.method, temp_url_key=self.key) + self.proxy.generate_temp_url, + b'/v1/a/c/\xff', + self.seconds, + self.method, + temp_url_key=self.key, + ) @mock.patch('hmac.HMAC.hexdigest', return_value="temp_url_signature") def test_generate_absolute_expiry_temp_url(self, hmac_mock): if isinstance(self.expected_url, bytes): expected_url = self.expected_url.replace( - b'1400003600', b'2146636800') + b'1400003600', b'2146636800' + ) else: expected_url = self.expected_url.replace( - u'1400003600', u'2146636800') + u'1400003600', u'2146636800' + ) url = self.proxy.generate_temp_url( - self.url, 2146636800, self.method, absolute=True, - temp_url_key=self.key) + self.url, + 2146636800, + self.method, + absolute=True, + temp_url_key=self.key, + ) self.assertEqual(url, expected_url) def test_generate_temp_url_bad_time(self): - for bad_time in ['not_an_int', -1, 1.1, '-1', '1.1', '2015-05', - '2015-05-01T01:00']: + for bad_time in [ + 'not_an_int', + -1, + 1.1, + '-1', + '1.1', + '2015-05', + '2015-05-01T01:00', + ]: self.assertRaisesRegex( - ValueError, self.time_errmsg, - self.proxy.generate_temp_url, self.url, bad_time, - self.method, temp_url_key=self.key) + ValueError, + self.time_errmsg, + self.proxy.generate_temp_url, + self.url, + bad_time, + self.method, + temp_url_key=self.key, + ) def test_generate_temp_url_bad_path(self): - for bad_path in ['/v1/a/c', 'v1/a/c/o', 'blah/v1/a/c/o', '/v1//c/o', - '/v1/a/c/', '/v1/a/c']: + for bad_path in [ + '/v1/a/c', + 'v1/a/c/o', + 'blah/v1/a/c/o', + '/v1//c/o', + '/v1/a/c/', + '/v1/a/c', + ]: self.assertRaisesRegex( - ValueError, self.path_errmsg, - self.proxy.generate_temp_url, bad_path, 60, self.method, - temp_url_key=self.key) + ValueError, + self.path_errmsg, + self.proxy.generate_temp_url, + bad_path, + 60, + self.method, + temp_url_key=self.key, + ) class TestTempURLUnicodePathAndKey(TestTempURL): url = u'/v1/\u00e4/c/\u00f3' key = u'k\u00e9y' - expected_url = (u'%s?temp_url_sig=temp_url_signature' - u'&temp_url_expires=1400003600') % url - expected_body = u'\n'.join([ - u'GET', - u'1400003600', - url, - ]).encode('utf-8') + expected_url = ( + u'%s?temp_url_sig=temp_url_signature' u'&temp_url_expires=1400003600' + ) % url + expected_body = u'\n'.join( + [ + u'GET', + u'1400003600', + url, + ] + ).encode('utf-8') class TestTempURLUnicodePathBytesKey(TestTempURL): url = u'/v1/\u00e4/c/\u00f3' key = u'k\u00e9y'.encode('utf-8') - expected_url = (u'%s?temp_url_sig=temp_url_signature' - u'&temp_url_expires=1400003600') % url - expected_body = '\n'.join([ - u'GET', - u'1400003600', - url, - ]).encode('utf-8') + expected_url = ( + u'%s?temp_url_sig=temp_url_signature' u'&temp_url_expires=1400003600' + ) % url + expected_body = '\n'.join( + [ + u'GET', + u'1400003600', + url, + ] + ).encode('utf-8') class TestTempURLBytesPathUnicodeKey(TestTempURL): url = u'/v1/\u00e4/c/\u00f3'.encode('utf-8') key = u'k\u00e9y' - expected_url = url + (b'?temp_url_sig=temp_url_signature' - b'&temp_url_expires=1400003600') - expected_body = b'\n'.join([ - b'GET', - b'1400003600', - url, - ]) + expected_url = url + ( + b'?temp_url_sig=temp_url_signature' b'&temp_url_expires=1400003600' + ) + expected_body = b'\n'.join( + [ + b'GET', + b'1400003600', + url, + ] + ) class TestTempURLBytesPathAndKey(TestTempURL): url = u'/v1/\u00e4/c/\u00f3'.encode('utf-8') key = u'k\u00e9y'.encode('utf-8') - expected_url = url + (b'?temp_url_sig=temp_url_signature' - b'&temp_url_expires=1400003600') - expected_body = b'\n'.join([ - b'GET', - b'1400003600', - url, - ]) + expected_url = url + ( + b'?temp_url_sig=temp_url_signature' b'&temp_url_expires=1400003600' + ) + expected_body = b'\n'.join( + [ + b'GET', + b'1400003600', + url, + ] + ) class TestTempURLBytesPathAndNonUtf8Key(TestTempURL): url = u'/v1/\u00e4/c/\u00f3'.encode('utf-8') key = b'k\xffy' - expected_url = url + (b'?temp_url_sig=temp_url_signature' - b'&temp_url_expires=1400003600') - expected_body = b'\n'.join([ - b'GET', - b'1400003600', - url, - ]) + expected_url = url + ( + b'?temp_url_sig=temp_url_signature' b'&temp_url_expires=1400003600' + ) + expected_body = b'\n'.join( + [ + b'GET', + b'1400003600', + url, + ] + ) From f8e42017e756e383367145c4caf39de796babcba Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 5 May 2023 10:55:42 +0100 Subject: [PATCH 3242/3836] Blackify openstack.baremetal, openstack.baremetal_introspection Black used with the '-l 79 -S' flags. A future change will ignore this commit in git-blame history by adding a 'git-blame-ignore-revs' file. Change-Id: I1effcaff4f4c931b46541f8db44ed50c10104cad Signed-off-by: Stephen Finucane --- openstack/baremetal/configdrive.py | 77 ++- openstack/baremetal/v1/_common.py | 8 +- openstack/baremetal/v1/_proxy.py | 243 +++++--- openstack/baremetal/v1/allocation.py | 16 +- openstack/baremetal/v1/driver.py | 38 +- openstack/baremetal/v1/node.py | 521 +++++++++++------ openstack/baremetal/v1/port.py | 4 +- openstack/baremetal/v1/port_group.py | 8 +- openstack/baremetal/v1/volume_connector.py | 3 +- openstack/baremetal/v1/volume_target.py | 3 +- .../baremetal_introspection/v1/_proxy.py | 10 +- .../v1/introspection.py | 42 +- openstack/tests/functional/baremetal/base.py | 71 ++- .../baremetal/test_baremetal_allocation.py | 97 ++-- .../baremetal/test_baremetal_chassis.py | 34 +- .../test_baremetal_deploy_templates.py | 133 ++--- .../baremetal/test_baremetal_driver.py | 28 +- .../baremetal/test_baremetal_node.py | 188 +++--- .../baremetal/test_baremetal_port.py | 66 ++- .../baremetal/test_baremetal_port_group.py | 63 +- .../test_baremetal_volume_connector.py | 128 +++-- .../baremetal/test_baremetal_volume_target.py | 126 ++-- .../tests/unit/baremetal/test_configdrive.py | 44 +- .../tests/unit/baremetal/test_version.py | 1 - .../unit/baremetal/v1/test_allocation.py | 21 +- .../tests/unit/baremetal/v1/test_chassis.py | 23 +- .../tests/unit/baremetal/v1/test_conductor.py | 11 +- .../baremetal/v1/test_deploy_templates.py | 38 +- .../tests/unit/baremetal/v1/test_driver.py | 73 ++- .../tests/unit/baremetal/v1/test_node.py | 540 +++++++++++------- .../tests/unit/baremetal/v1/test_port.py | 20 +- .../unit/baremetal/v1/test_port_group.py | 22 +- .../tests/unit/baremetal/v1/test_proxy.py | 211 ++++--- .../baremetal/v1/test_volume_connector.py | 9 +- .../unit/baremetal/v1/test_volume_target.py | 9 +- .../baremetal_introspection/v1/test_proxy.py | 70 ++- 36 files changed, 1818 insertions(+), 1181 deletions(-) diff --git a/openstack/baremetal/configdrive.py b/openstack/baremetal/configdrive.py index e7e9e8892..bcf2fb689 100644 --- a/openstack/baremetal/configdrive.py +++ b/openstack/baremetal/configdrive.py @@ -23,8 +23,13 @@ @contextlib.contextmanager -def populate_directory(metadata, user_data=None, versions=None, - network_data=None, vendor_data=None): +def populate_directory( + metadata, + user_data=None, + versions=None, + network_data=None, + vendor_data=None, +): """Populate a directory with configdrive files. :param dict metadata: Metadata. @@ -46,21 +51,24 @@ def populate_directory(metadata, user_data=None, versions=None, json.dump(metadata, fp) if network_data: - with open(os.path.join(subdir, 'network_data.json'), - 'w') as fp: + with open( + os.path.join(subdir, 'network_data.json'), 'w' + ) as fp: json.dump(network_data, fp) if vendor_data: - with open(os.path.join(subdir, 'vendor_data2.json'), - 'w') as fp: + with open( + os.path.join(subdir, 'vendor_data2.json'), 'w' + ) as fp: json.dump(vendor_data, fp) if user_data: # Strictly speaking, user data is binary, but in many cases # it's actually a text (cloud-init, ignition, etc). flag = 't' if isinstance(user_data, str) else 'b' - with open(os.path.join(subdir, 'user_data'), - 'w%s' % flag) as fp: + with open( + os.path.join(subdir, 'user_data'), 'w%s' % flag + ) as fp: fp.write(user_data) yield d @@ -68,8 +76,13 @@ def populate_directory(metadata, user_data=None, versions=None, shutil.rmtree(d) -def build(metadata, user_data=None, versions=None, network_data=None, - vendor_data=None): +def build( + metadata, + user_data=None, + versions=None, + network_data=None, + vendor_data=None, +): """Make a configdrive compatible with the Bare Metal service. Requires the genisoimage utility to be available. @@ -81,8 +94,9 @@ def build(metadata, user_data=None, versions=None, network_data=None, :param dict vendor_data: Extra supplied vendor data. :return: configdrive contents as a base64-encoded string. """ - with populate_directory(metadata, user_data, versions, - network_data, vendor_data) as path: + with populate_directory( + metadata, user_data, versions, network_data, vendor_data + ) as path: return pack(path) @@ -100,16 +114,27 @@ def pack(path): cmds = ['genisoimage', 'mkisofs', 'xorrisofs'] for c in cmds: try: - p = subprocess.Popen([c, - '-o', tmpfile.name, - '-ldots', '-allow-lowercase', - '-allow-multidot', '-l', - '-publisher', 'metalsmith', - '-quiet', '-J', - '-r', '-V', 'config-2', - path], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + p = subprocess.Popen( + [ + c, + '-o', + tmpfile.name, + '-ldots', + '-allow-lowercase', + '-allow-multidot', + '-l', + '-publisher', + 'metalsmith', + '-quiet', + '-J', + '-r', + '-V', + 'config-2', + path, + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) except OSError as e: error = e else: @@ -120,14 +145,16 @@ def pack(path): raise RuntimeError( 'Error generating the configdrive. Make sure the ' '"genisoimage", "mkisofs" or "xorrisofs" tool is installed. ' - 'Error: %s' % error) + 'Error: %s' % error + ) stdout, stderr = p.communicate() if p.returncode != 0: raise RuntimeError( 'Error generating the configdrive.' - 'Stdout: "%(stdout)s". Stderr: "%(stderr)s"' % - {'stdout': stdout, 'stderr': stderr}) + 'Stdout: "%(stdout)s". Stderr: "%(stderr)s"' + % {'stdout': stdout, 'stderr': stderr} + ) tmpfile.seek(0) diff --git a/openstack/baremetal/v1/_common.py b/openstack/baremetal/v1/_common.py index 3cbb68ba8..626605e04 100644 --- a/openstack/baremetal/v1/_common.py +++ b/openstack/baremetal/v1/_common.py @@ -17,7 +17,7 @@ # HTTP Conflict - happens if a node is locked 409, # HTTP Service Unavailable happens if there's no free conductor - 503 + 503, ] """HTTP status codes that should be retried.""" @@ -88,7 +88,6 @@ class ListMixin: - @classmethod def list(cls, session, details=False, **params): """This method is a generator which yields resource objects. @@ -112,8 +111,9 @@ def list(cls, session, details=False, **params): base_path = cls.base_path if details: base_path += '/detail' - return super(ListMixin, cls).list(session, paginated=True, - base_path=base_path, **params) + return super(ListMixin, cls).list( + session, paginated=True, base_path=base_path, **params + ) def comma_separated_list(value): diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index cdc62ce32..0fd64aea2 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -63,8 +63,10 @@ def _get_with_fields(self, resource_type, value, fields=None): return res.fetch( self, error_message="No {resource_type} found for {value}".format( - resource_type=resource_type.__name__, value=value), - **kwargs) + resource_type=resource_type.__name__, value=value + ), + **kwargs + ) def chassis(self, details=False, **query): """Retrieve a generator of chassis. @@ -123,8 +125,9 @@ def find_chassis(self, name_or_id, ignore_missing=True): :returns: One :class:`~openstack.baremetal.v1.chassis.Chassis` object or None. """ - return self._find(_chassis.Chassis, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _chassis.Chassis, name_or_id, ignore_missing=ignore_missing + ) def get_chassis(self, chassis, fields=None): """Get a specific chassis. @@ -178,8 +181,9 @@ def delete_chassis(self, chassis, ignore_missing=True): :returns: The instance of the chassis which was deleted. :rtype: :class:`~openstack.baremetal.v1.chassis.Chassis`. """ - return self._delete(_chassis.Chassis, chassis, - ignore_missing=ignore_missing) + return self._delete( + _chassis.Chassis, chassis, ignore_missing=ignore_missing + ) def drivers(self, details=False, **query): """Retrieve a generator of drivers. @@ -221,8 +225,9 @@ def list_driver_vendor_passthru(self, driver): driver = self.get_driver(driver) return driver.list_vendor_passthru(self) - def call_driver_vendor_passthru(self, driver, - verb: str, method: str, body=None): + def call_driver_vendor_passthru( + self, driver, verb: str, method: str, body=None + ): """Call driver's vendor_passthru method. :param driver: The value can be the name of a driver or a @@ -311,8 +316,9 @@ def find_node(self, name_or_id, ignore_missing=True): :returns: One :class:`~openstack.baremetal.v1.node.Node` object or None. """ - return self._find(_node.Node, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _node.Node, name_or_id, ignore_missing=ignore_missing + ) def get_node(self, node, fields=None): """Get a specific node. @@ -345,8 +351,9 @@ def update_node(self, node, retry_on_conflict=True, **attrs): res = self._get_resource(_node.Node, node, **attrs) return res.commit(self, retry_on_conflict=retry_on_conflict) - def patch_node(self, node, patch, reset_interfaces=None, - retry_on_conflict=True): + def patch_node( + self, node, patch, reset_interfaces=None, retry_on_conflict=True + ): """Apply a JSON patch to the node. :param node: The value can be the name or ID of a node or a @@ -368,12 +375,24 @@ def patch_node(self, node, patch, reset_interfaces=None, :rtype: :class:`~openstack.baremetal.v1.node.Node` """ res = self._get_resource(_node.Node, node) - return res.patch(self, patch, retry_on_conflict=retry_on_conflict, - reset_interfaces=reset_interfaces) - - def set_node_provision_state(self, node, target, config_drive=None, - clean_steps=None, rescue_password=None, - wait=False, timeout=None, deploy_steps=None): + return res.patch( + self, + patch, + retry_on_conflict=retry_on_conflict, + reset_interfaces=reset_interfaces, + ) + + def set_node_provision_state( + self, + node, + target, + config_drive=None, + clean_steps=None, + rescue_password=None, + wait=False, + timeout=None, + deploy_steps=None, + ): """Run an action modifying node's provision state. This call is asynchronous, it will return success as soon as the Bare @@ -405,11 +424,16 @@ def set_node_provision_state(self, node, target, config_drive=None, invalid ``target``. """ res = self._get_resource(_node.Node, node) - return res.set_provision_state(self, target, config_drive=config_drive, - clean_steps=clean_steps, - rescue_password=rescue_password, - wait=wait, timeout=timeout, - deploy_steps=deploy_steps) + return res.set_provision_state( + self, + target, + config_drive=config_drive, + clean_steps=clean_steps, + rescue_password=rescue_password, + wait=wait, + timeout=timeout, + deploy_steps=deploy_steps, + ) def get_node_boot_device(self, node): """Get node boot device @@ -480,10 +504,14 @@ def inject_nmi_to_node(self, node): res = self._get_resource(_node.Node, node) res.inject_nmi(self) - def wait_for_nodes_provision_state(self, nodes, expected_state, - timeout=None, - abort_on_failed_state=True, - fail=True): + def wait_for_nodes_provision_state( + self, + nodes, + expected_state, + timeout=None, + abort_on_failed_state=True, + fail=True, + ): """Wait for the nodes to reach the expected state. :param nodes: List of nodes - name, ID or @@ -507,24 +535,27 @@ def wait_for_nodes_provision_state(self, nodes, expected_state, reaches an error state and ``abort_on_failed_state`` is ``True``. :raises: :class:`~openstack.exceptions.ResourceTimeout` on timeout. """ - log_nodes = ', '.join(n.id if isinstance(n, _node.Node) else n - for n in nodes) + log_nodes = ', '.join( + n.id if isinstance(n, _node.Node) else n for n in nodes + ) finished = [] failed = [] remaining = nodes try: for count in utils.iterate_timeout( - timeout, - "Timeout waiting for nodes %(nodes)s to reach " - "target state '%(state)s'" % {'nodes': log_nodes, - 'state': expected_state}): + timeout, + "Timeout waiting for nodes %(nodes)s to reach " + "target state '%(state)s'" + % {'nodes': log_nodes, 'state': expected_state}, + ): nodes = [self.get_node(n) for n in remaining] remaining = [] for n in nodes: try: - if n._check_state_reached(self, expected_state, - abort_on_failed_state): + if n._check_state_reached( + self, expected_state, abort_on_failed_state + ): finished.append(n) else: remaining.append(n) @@ -543,8 +574,11 @@ def wait_for_nodes_provision_state(self, nodes, expected_state, self.log.debug( 'Still waiting for nodes %(nodes)s to reach state ' '"%(target)s"', - {'nodes': ', '.join(n.id for n in remaining), - 'target': expected_state}) + { + 'nodes': ', '.join(n.id for n in remaining), + 'target': expected_state, + }, + ) except exceptions.ResourceTimeout: if fail: raise @@ -568,7 +602,8 @@ def set_node_power_state(self, node, target, wait=False, timeout=None): ``None`` (the default) means no client-side timeout. """ self._get_resource(_node.Node, node).set_power_state( - self, target, wait=wait, timeout=timeout) + self, target, wait=wait, timeout=timeout + ) def wait_for_node_power_state(self, node, expected_state, timeout=None): """Wait for the node to reach the power state. @@ -731,8 +766,9 @@ def find_port(self, name_or_id, ignore_missing=True): :returns: One :class:`~openstack.baremetal.v1.port.Port` object or None. """ - return self._find(_port.Port, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _port.Port, name_or_id, ignore_missing=ignore_missing + ) def get_port(self, port, fields=None): """Get a specific port. @@ -849,8 +885,9 @@ def find_port_group(self, name_or_id, ignore_missing=True): :returns: One :class:`~openstack.baremetal.v1.port_group.PortGroup` object or None. """ - return self._find(_portgroup.PortGroup, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _portgroup.PortGroup, name_or_id, ignore_missing=ignore_missing + ) def get_port_group(self, port_group, fields=None): """Get a specific port group. @@ -863,8 +900,9 @@ def get_port_group(self, port_group, fields=None): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no port group matching the name or ID could be found. """ - return self._get_with_fields(_portgroup.PortGroup, port_group, - fields=fields) + return self._get_with_fields( + _portgroup.PortGroup, port_group, fields=fields + ) def update_port_group(self, port_group, **attrs): """Update a port group. @@ -909,8 +947,9 @@ def delete_port_group(self, port_group, ignore_missing=True): :returns: The instance of the port group which was deleted. :rtype: :class:`~openstack.baremetal.v1.port_group.PortGroup`. """ - return self._delete(_portgroup.PortGroup, port_group, - ignore_missing=ignore_missing) + return self._delete( + _portgroup.PortGroup, port_group, ignore_missing=ignore_missing + ) def attach_vif_to_node(self, node, vif_id, retry_on_conflict=True): """Attach a VIF to the node. @@ -1026,8 +1065,9 @@ def get_allocation(self, allocation, fields=None): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no allocation matching the name or ID could be found. """ - return self._get_with_fields(_allocation.Allocation, allocation, - fields=fields) + return self._get_with_fields( + _allocation.Allocation, allocation, fields=fields + ) def update_allocation(self, allocation, **attrs): """Update an allocation. @@ -1052,8 +1092,9 @@ def patch_allocation(self, allocation, patch): :returns: The updated allocation. :rtype: :class:`~openstack.baremetal.v1.allocation.Allocation` """ - return self._get_resource(_allocation.Allocation, - allocation).patch(self, patch) + return self._get_resource(_allocation.Allocation, allocation).patch( + self, patch + ) def delete_allocation(self, allocation, ignore_missing=True): """Delete an allocation. @@ -1069,11 +1110,13 @@ def delete_allocation(self, allocation, ignore_missing=True): :returns: The instance of the allocation which was deleted. :rtype: :class:`~openstack.baremetal.v1.allocation.Allocation`. """ - return self._delete(_allocation.Allocation, allocation, - ignore_missing=ignore_missing) + return self._delete( + _allocation.Allocation, allocation, ignore_missing=ignore_missing + ) - def wait_for_allocation(self, allocation, timeout=None, - ignore_error=False): + def wait_for_allocation( + self, allocation, timeout=None, ignore_error=False + ): """Wait for the allocation to become active. :param allocation: The value can be the name or ID of an allocation or @@ -1252,8 +1295,11 @@ def find_volume_connector(self, vc_id, ignore_missing=True): :class:`~openstack.baremetal.v1.volumeconnector.VolumeConnector` object or None. """ - return self._find(_volumeconnector.VolumeConnector, vc_id, - ignore_missing=ignore_missing) + return self._find( + _volumeconnector.VolumeConnector, + vc_id, + ignore_missing=ignore_missing, + ) def get_volume_connector(self, volume_connector, fields=None): """Get a specific volume_connector. @@ -1269,9 +1315,9 @@ def get_volume_connector(self, volume_connector, fields=None): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no volume_connector matching the name or ID could be found.` """ - return self._get_with_fields(_volumeconnector.VolumeConnector, - volume_connector, - fields=fields) + return self._get_with_fields( + _volumeconnector.VolumeConnector, volume_connector, fields=fields + ) def update_volume_connector(self, volume_connector, **attrs): """Update a volume_connector. @@ -1287,8 +1333,9 @@ def update_volume_connector(self, volume_connector, **attrs): :rtype: :class:`~openstack.baremetal.v1.volume_connector.VolumeConnector` """ - return self._update(_volumeconnector.VolumeConnector, - volume_connector, **attrs) + return self._update( + _volumeconnector.VolumeConnector, volume_connector, **attrs + ) def patch_volume_connector(self, volume_connector, patch): """Apply a JSON patch to the volume_connector. @@ -1303,11 +1350,11 @@ def patch_volume_connector(self, volume_connector, patch): :rtype: :class:`~openstack.baremetal.v1.volume_connector.VolumeConnector.` """ - return self._get_resource(_volumeconnector.VolumeConnector, - volume_connector).patch(self, patch) + return self._get_resource( + _volumeconnector.VolumeConnector, volume_connector + ).patch(self, patch) - def delete_volume_connector(self, volume_connector, - ignore_missing=True): + def delete_volume_connector(self, volume_connector, ignore_missing=True): """Delete an volume_connector. :param volume_connector: The value can be either the ID of a @@ -1324,8 +1371,11 @@ def delete_volume_connector(self, volume_connector, :rtype: :class:`~openstack.baremetal.v1.volume_connector.VolumeConnector`. """ - return self._delete(_volumeconnector.VolumeConnector, - volume_connector, ignore_missing=ignore_missing) + return self._delete( + _volumeconnector.VolumeConnector, + volume_connector, + ignore_missing=ignore_missing, + ) def volume_targets(self, details=False, **query): """Retrieve a generator of volume_target. @@ -1392,8 +1442,9 @@ def find_volume_target(self, vt_id, ignore_missing=True): :class:`~openstack.baremetal.v1.volumetarget.VolumeTarget` object or None. """ - return self._find(_volumetarget.VolumeTarget, vt_id, - ignore_missing=ignore_missing) + return self._find( + _volumetarget.VolumeTarget, vt_id, ignore_missing=ignore_missing + ) def get_volume_target(self, volume_target, fields=None): """Get a specific volume_target. @@ -1409,9 +1460,9 @@ def get_volume_target(self, volume_target, fields=None): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no volume_target matching the name or ID could be found.` """ - return self._get_with_fields(_volumetarget.VolumeTarget, - volume_target, - fields=fields) + return self._get_with_fields( + _volumetarget.VolumeTarget, volume_target, fields=fields + ) def update_volume_target(self, volume_target, **attrs): """Update a volume_target. @@ -1426,8 +1477,7 @@ def update_volume_target(self, volume_target, **attrs): :rtype: :class:`~openstack.baremetal.v1.volume_target.VolumeTarget` """ - return self._update(_volumetarget.VolumeTarget, - volume_target, **attrs) + return self._update(_volumetarget.VolumeTarget, volume_target, **attrs) def patch_volume_target(self, volume_target, patch): """Apply a JSON patch to the volume_target. @@ -1442,11 +1492,11 @@ def patch_volume_target(self, volume_target, patch): :rtype: :class:`~openstack.baremetal.v1.volume_target.VolumeTarget.` """ - return self._get_resource(_volumetarget.VolumeTarget, - volume_target).patch(self, patch) + return self._get_resource( + _volumetarget.VolumeTarget, volume_target + ).patch(self, patch) - def delete_volume_target(self, volume_target, - ignore_missing=True): + def delete_volume_target(self, volume_target, ignore_missing=True): """Delete an volume_target. :param volume_target: The value can be either the ID of a @@ -1463,8 +1513,11 @@ def delete_volume_target(self, volume_target, :rtype: :class:`~openstack.baremetal.v1.volume_target.VolumeTarget`. """ - return self._delete(_volumetarget.VolumeTarget, - volume_target, ignore_missing=ignore_missing) + return self._delete( + _volumetarget.VolumeTarget, + volume_target, + ignore_missing=ignore_missing, + ) def deploy_templates(self, details=False, **query): """Retrieve a generator of deploy_templates. @@ -1506,11 +1559,11 @@ def update_deploy_template(self, deploy_template, **attrs): :rtype: :class:`~openstack.baremetal.v1.deploy_templates.DeployTemplate` """ - return self._update(_deploytemplates.DeployTemplate, - deploy_template, **attrs) + return self._update( + _deploytemplates.DeployTemplate, deploy_template, **attrs + ) - def delete_deploy_template(self, deploy_template, - ignore_missing=True): + def delete_deploy_template(self, deploy_template, ignore_missing=True): """Delete a deploy_template. :param deploy_template:The value can be @@ -1532,8 +1585,11 @@ def delete_deploy_template(self, deploy_template, :class:`~openstack.baremetal.v1.deploy_templates.DeployTemplate`. """ - return self._delete(_deploytemplates.DeployTemplate, - deploy_template, ignore_missing=ignore_missing) + return self._delete( + _deploytemplates.DeployTemplate, + deploy_template, + ignore_missing=ignore_missing, + ) def get_deploy_template(self, deploy_template, fields=None): """Get a specific deployment template. @@ -1551,8 +1607,9 @@ def get_deploy_template(self, deploy_template, fields=None): when no deployment template matching the name or ID could be found. """ - return self._get_with_fields(_deploytemplates.DeployTemplate, - deploy_template, fields=fields) + return self._get_with_fields( + _deploytemplates.DeployTemplate, deploy_template, fields=fields + ) def patch_deploy_template(self, deploy_template, patch): """Apply a JSON patch to the deploy_templates. @@ -1568,8 +1625,9 @@ def patch_deploy_template(self, deploy_template, patch): :rtype: :class:`~openstack.baremetal.v1.deploy_templates.DeployTemplate` """ - return self._get_resource(_deploytemplates.DeployTemplate, - deploy_template).patch(self, patch) + return self._get_resource( + _deploytemplates.DeployTemplate, deploy_template + ).patch(self, patch) def conductors(self, details=False, **query): """Retrieve a generator of conductors. @@ -1595,5 +1653,6 @@ def get_conductor(self, conductor, fields=None): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no conductor matching the name could be found. """ - return self._get_with_fields(_conductor.Conductor, - conductor, fields=fields) + return self._get_with_fields( + _conductor.Conductor, conductor, fields=fields + ) diff --git a/openstack/baremetal/v1/allocation.py b/openstack/baremetal/v1/allocation.py index a1b8b728b..c8887b4fa 100644 --- a/openstack/baremetal/v1/allocation.py +++ b/openstack/baremetal/v1/allocation.py @@ -32,7 +32,9 @@ class Allocation(_common.ListMixin, resource.Resource): commit_jsonpatch = True _query_mapping = resource.QueryParameters( - 'node', 'resource_class', 'state', + 'node', + 'resource_class', + 'state', fields={'type': _common.fields_type}, ) @@ -88,18 +90,20 @@ def wait(self, session, timeout=None, ignore_error=False): return self for count in utils.iterate_timeout( - timeout, - "Timeout waiting for the allocation %s" % self.id): + timeout, "Timeout waiting for the allocation %s" % self.id + ): self.fetch(session) if self.state == 'error' and not ignore_error: raise exceptions.ResourceFailure( - "Allocation %(allocation)s failed: %(error)s" % - {'allocation': self.id, 'error': self.last_error}) + "Allocation %(allocation)s failed: %(error)s" + % {'allocation': self.id, 'error': self.last_error} + ) elif self.state != 'allocating': return self session.log.debug( 'Still waiting for the allocation %(allocation)s ' 'to become active, the current state is %(state)s', - {'allocation': self.id, 'state': self.state}) + {'allocation': self.id, 'state': self.state}, + ) diff --git a/openstack/baremetal/v1/driver.py b/openstack/baremetal/v1/driver.py index 91311ae72..d8bfe4d5f 100644 --- a/openstack/baremetal/v1/driver.py +++ b/openstack/baremetal/v1/driver.py @@ -63,7 +63,8 @@ class Driver(resource.Resource): #: Default management interface implementation. #: Introduced in API microversion 1.30. default_management_interface = resource.Body( - "default_management_interface") + "default_management_interface" + ) #: Default network interface implementation. #: Introduced in API microversion 1.30. default_network_interface = resource.Body("default_network_interface") @@ -101,7 +102,8 @@ class Driver(resource.Resource): #: Enabled management interface implementations. #: Introduced in API microversion 1.30. enabled_management_interfaces = resource.Body( - "enabled_management_interfaces") + "enabled_management_interfaces" + ) #: Enabled network interface implementations. #: Introduced in API microversion 1.30. enabled_network_interfaces = resource.Body("enabled_network_interfaces") @@ -135,17 +137,18 @@ def list_vendor_passthru(self, session): """ session = self._get_session(session) request = self._prepare_request() - request.url = utils.urljoin( - request.url, 'vendor_passthru', 'methods') + request.url = utils.urljoin(request.url, 'vendor_passthru', 'methods') response = session.get(request.url, headers=request.headers) - msg = ("Failed to list list vendor_passthru methods for {driver_name}" - .format(driver_name=self.name)) - exceptions.raise_from_response(response, error_message=msg) + msg = "Failed to list list vendor_passthru methods for {driver_name}" + exceptions.raise_from_response( + response, error_message=msg.format(driver_name=self.name) + ) return response.json() - def call_vendor_passthru(self, session, - verb: str, method: str, body: dict = None): + def call_vendor_passthru( + self, session, verb: str, method: str, body: dict = None + ): """Call a vendor specific passthru method Contents of body are params passed to the hardware driver @@ -167,13 +170,18 @@ def call_vendor_passthru(self, session, session = self._get_session(session) request = self._prepare_request() request.url = utils.urljoin( - request.url, f'vendor_passthru?method={method}') + request.url, f'vendor_passthru?method={method}' + ) call = getattr(session, verb.lower()) response = call( - request.url, json=body, headers=request.headers, - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) - - msg = ("Failed call to method {method} on driver {driver_name}" - .format(method=method, driver_name=self.name)) + request.url, + json=body, + headers=request.headers, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) + + msg = "Failed call to method {method} on driver {driver_name}".format( + method=method, driver_name=self.name + ) exceptions.raise_from_response(response, error_message=msg) return response diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 2d33f8dd1..44b0ec407 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -51,8 +51,9 @@ class PowerAction(enum.Enum): """Reboot the node using soft power off.""" -class WaitResult(collections.namedtuple('WaitResult', - ['success', 'failure', 'timeout'])): +class WaitResult( + collections.namedtuple('WaitResult', ['success', 'failure', 'timeout']) +): """A named tuple representing a result of waiting for several nodes. Each component is a list of :class:`~openstack.baremetal.v1.node.Node` @@ -65,6 +66,7 @@ class WaitResult(collections.namedtuple('WaitResult', :ivar ~.failure: a list of :class:`~openstack.baremetal.v1.node.Node` objects that hit a failure. """ + __slots__ = () @@ -84,8 +86,12 @@ class Node(_common.ListMixin, resource.Resource): commit_jsonpatch = True _query_mapping = resource.QueryParameters( - 'associated', 'conductor_group', 'driver', 'fault', - 'provision_state', 'resource_class', + 'associated', + 'conductor_group', + 'driver', + 'fault', + 'provision_state', + 'resource_class', fields={'type': _common.fields_type}, instance_id='instance_uuid', is_maintenance='maintenance', @@ -292,21 +298,30 @@ def create(self, session, *args, **kwargs): # Verify that the requested provision state is reachable with # the API version we are going to use. try: - microversion = _common.STATE_VERSIONS[ - expected_provision_state] + microversion = _common.STATE_VERSIONS[expected_provision_state] except KeyError: raise ValueError( "Node's provision_state must be one of %s for creation, " - "got %s" % (', '.join(_common.STATE_VERSIONS), - expected_provision_state)) + "got %s" + % ( + ', '.join(_common.STATE_VERSIONS), + expected_provision_state, + ) + ) else: - error_message = ("Cannot create a node with initial provision " - "state %s" % expected_provision_state) + error_message = ( + "Cannot create a node with initial provision " + "state %s" % expected_provision_state + ) # Nodes cannot be created as available using new API versions - maximum = ('1.10' if expected_provision_state == 'available' - else None) + maximum = ( + '1.10' if expected_provision_state == 'available' else None + ) microversion = self._assert_microversion_for( - session, 'create', microversion, maximum=maximum, + session, + 'create', + microversion, + maximum=maximum, error_message=error_message, ) else: @@ -315,11 +330,14 @@ def create(self, session, *args, **kwargs): # Ironic cannot set provision_state itself, so marking it as unchanged self._clean_body_attrs({'provision_state'}) - super(Node, self).create(session, *args, microversion=microversion, - **kwargs) + super(Node, self).create( + session, *args, microversion=microversion, **kwargs + ) - if (expected_provision_state == 'manageable' - and self.provision_state != 'manageable'): + if ( + expected_provision_state == 'manageable' + and self.provision_state != 'manageable' + ): # Manageable is not reachable directly self.set_provision_state(session, 'manage', wait=True) @@ -334,17 +352,22 @@ def commit(self, session, *args, **kwargs): :return: This :class:`Node` instance. """ # These fields have to be set through separate API. - if ('maintenance_reason' in self._body.dirty - or 'maintenance' in self._body.dirty): + if ( + 'maintenance_reason' in self._body.dirty + or 'maintenance' in self._body.dirty + ): if not self.is_maintenance and self.maintenance_reason: if 'maintenance' in self._body.dirty: self.maintenance_reason = None else: - raise ValueError('Maintenance reason cannot be set when ' - 'maintenance is False') + raise ValueError( + 'Maintenance reason cannot be set when ' + 'maintenance is False' + ) if self.is_maintenance: self._do_maintenance_action( - session, 'put', {'reason': self.maintenance_reason}) + session, 'put', {'reason': self.maintenance_reason} + ) else: # This corresponds to setting maintenance=False and # maintenance_reason=None in the same request. @@ -358,9 +381,17 @@ def commit(self, session, *args, **kwargs): return super(Node, self).commit(session, *args, **kwargs) - def set_provision_state(self, session, target, config_drive=None, - clean_steps=None, rescue_password=None, - wait=False, timeout=None, deploy_steps=None): + def set_provision_state( + self, + session, + target, + config_drive=None, + clean_steps=None, + rescue_password=None, + wait=False, + timeout=None, + deploy_steps=None, + ): """Run an action modifying this node's provision state. This call is asynchronous, it will return success as soon as the Bare @@ -413,51 +444,65 @@ def set_provision_state(self, session, target, config_drive=None, body = {'target': target} if config_drive: if target not in ('active', 'rebuild'): - raise ValueError('Config drive can only be provided with ' - '"active" and "rebuild" targets') + raise ValueError( + 'Config drive can only be provided with ' + '"active" and "rebuild" targets' + ) # Not a typo - ironic accepts "configdrive" (without underscore) body['configdrive'] = config_drive if clean_steps is not None: if target != 'clean': - raise ValueError('Clean steps can only be provided with ' - '"clean" target') + raise ValueError( + 'Clean steps can only be provided with ' '"clean" target' + ) body['clean_steps'] = clean_steps if deploy_steps is not None: if target not in ('active', 'rebuild'): - raise ValueError('Deploy steps can only be provided with ' - '"deploy" and "rebuild" target') + raise ValueError( + 'Deploy steps can only be provided with ' + '"deploy" and "rebuild" target' + ) body['deploy_steps'] = deploy_steps if rescue_password is not None: if target != 'rescue': - raise ValueError('Rescue password can only be provided with ' - '"rescue" target') + raise ValueError( + 'Rescue password can only be provided with ' + '"rescue" target' + ) body['rescue_password'] = rescue_password if wait: try: expected_state = _common.EXPECTED_STATES[target] except KeyError: - raise ValueError('For target %s the expected state is not ' - 'known, cannot wait for it' % target) + raise ValueError( + 'For target %s the expected state is not ' + 'known, cannot wait for it' % target + ) request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'states', 'provision') response = session.put( - request.url, json=body, - headers=request.headers, microversion=version, - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + request.url, + json=body, + headers=request.headers, + microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) - msg = ("Failed to set provision state for bare metal node {node} " - "to {target}".format(node=self.id, target=target)) + msg = ( + "Failed to set provision state for bare metal node {node} " + "to {target}".format(node=self.id, target=target) + ) exceptions.raise_from_response(response, error_message=msg) if wait: - return self.wait_for_provision_state(session, - expected_state, - timeout=timeout) + return self.wait_for_provision_state( + session, expected_state, timeout=timeout + ) else: return self.fetch(session) @@ -475,10 +520,11 @@ def wait_for_power_state(self, session, expected_state, timeout=None): :raises: :class:`~openstack.exceptions.ResourceTimeout` on timeout. """ for count in utils.iterate_timeout( - timeout, - "Timeout waiting for node %(node)s to reach " - "power state '%(state)s'" % {'node': self.id, - 'state': expected_state}): + timeout, + "Timeout waiting for node %(node)s to reach " + "power state '%(state)s'" + % {'node': self.id, 'state': expected_state}, + ): self.fetch(session) if self.power_state == expected_state: return self @@ -486,11 +532,16 @@ def wait_for_power_state(self, session, expected_state, timeout=None): session.log.debug( 'Still waiting for node %(node)s to reach power state ' '"%(target)s", the current state is "%(state)s"', - {'node': self.id, 'target': expected_state, - 'state': self.power_state}) + { + 'node': self.id, + 'target': expected_state, + 'state': self.power_state, + }, + ) - def wait_for_provision_state(self, session, expected_state, timeout=None, - abort_on_failed_state=True): + def wait_for_provision_state( + self, session, expected_state, timeout=None, abort_on_failed_state=True + ): """Wait for the node to reach the expected state. :param session: The session to use for making this request. @@ -510,20 +561,26 @@ def wait_for_provision_state(self, session, expected_state, timeout=None, :raises: :class:`~openstack.exceptions.ResourceTimeout` on timeout. """ for count in utils.iterate_timeout( - timeout, - "Timeout waiting for node %(node)s to reach " - "target state '%(state)s'" % {'node': self.id, - 'state': expected_state}): + timeout, + "Timeout waiting for node %(node)s to reach " + "target state '%(state)s'" + % {'node': self.id, 'state': expected_state}, + ): self.fetch(session) - if self._check_state_reached(session, expected_state, - abort_on_failed_state): + if self._check_state_reached( + session, expected_state, abort_on_failed_state + ): return self session.log.debug( 'Still waiting for node %(node)s to reach state ' '"%(target)s", the current state is "%(state)s"', - {'node': self.id, 'target': expected_state, - 'state': self.provision_state}) + { + 'node': self.id, + 'target': expected_state, + 'state': self.provision_state, + }, + ) def wait_for_reservation(self, session, timeout=None): """Wait for a lock on the node to be released. @@ -552,9 +609,9 @@ def wait_for_reservation(self, session, timeout=None): return self for count in utils.iterate_timeout( - timeout, - "Timeout waiting for the lock to be released on node %s" % - self.id): + timeout, + "Timeout waiting for the lock to be released on node %s" % self.id, + ): self.fetch(session) if self.reservation is None: return self @@ -562,10 +619,12 @@ def wait_for_reservation(self, session, timeout=None): session.log.debug( 'Still waiting for the lock to be released on node ' '%(node)s, currently locked by conductor %(host)s', - {'node': self.id, 'host': self.reservation}) + {'node': self.id, 'host': self.reservation}, + ) - def _check_state_reached(self, session, expected_state, - abort_on_failed_state=True): + def _check_state_reached( + self, session, expected_state, abort_on_failed_state=True + ): """Wait for the node to reach the expected state. :param session: The session to use for making this request. @@ -581,29 +640,39 @@ def _check_state_reached(self, session, expected_state, reaches an error state and ``abort_on_failed_state`` is ``True``. """ # NOTE(dtantsur): microversion 1.2 changed None to available - if (self.provision_state == expected_state - or (expected_state == 'available' - and self.provision_state is None)): + if self.provision_state == expected_state or ( + expected_state == 'available' and self.provision_state is None + ): return True elif not abort_on_failed_state: return False - if (self.provision_state.endswith(' failed') - or self.provision_state == 'error'): + if ( + self.provision_state.endswith(' failed') + or self.provision_state == 'error' + ): raise exceptions.ResourceFailure( "Node %(node)s reached failure state \"%(state)s\"; " - "the last error is %(error)s" % - {'node': self.id, 'state': self.provision_state, - 'error': self.last_error}) + "the last error is %(error)s" + % { + 'node': self.id, + 'state': self.provision_state, + 'error': self.last_error, + } + ) # Special case: a failure state for "manage" transition can be # "enroll" - elif (expected_state == 'manageable' - and self.provision_state == 'enroll' and self.last_error): + elif ( + expected_state == 'manageable' + and self.provision_state == 'enroll' + and self.last_error + ): raise exceptions.ResourceFailure( "Node %(node)s could not reach state manageable: " "failed to verify management credentials; " - "the last error is %(error)s" % - {'node': self.id, 'error': self.last_error}) + "the last error is %(error)s" + % {'node': self.id, 'error': self.last_error} + ) def inject_nmi(self, session): """Inject NMI. @@ -630,7 +699,7 @@ def inject_nmi(self, session): retriable_status_codes=_common.RETRIABLE_STATUS_CODES, ) - msg = ("Failed to inject NMI to node {node}".format(node=self.id)) + msg = "Failed to inject NMI to node {node}".format(node=self.id) exceptions.raise_from_response(response, error_message=msg) def set_power_state(self, session, target, wait=False, timeout=None): @@ -654,8 +723,10 @@ def set_power_state(self, session, target, wait=False, timeout=None): try: expected = _common.EXPECTED_POWER_STATES[target] except KeyError: - raise ValueError("Cannot use target power state %s with wait, " - "the expected state is not known" % target) + raise ValueError( + "Cannot use target power state %s with wait, " + "the expected state is not known" % target + ) session = self._get_session(session) @@ -672,12 +743,17 @@ def set_power_state(self, session, target, wait=False, timeout=None): request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'states', 'power') response = session.put( - request.url, json=body, - headers=request.headers, microversion=version, - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + request.url, + json=body, + headers=request.headers, + microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) - msg = ("Failed to set power state for bare metal node {node} " - "to {target}".format(node=self.id, target=target)) + msg = ( + "Failed to set power state for bare metal node {node} " + "to {target}".format(node=self.id, target=target) + ) exceptions.raise_from_response(response, error_message=msg) if wait: @@ -704,8 +780,11 @@ def attach_vif(self, session, vif_id, retry_on_conflict=True): """ session = self._get_session(session) version = self._assert_microversion_for( - session, 'commit', _common.VIF_VERSION, - error_message=("Cannot use VIF attachment API")) + session, + 'commit', + _common.VIF_VERSION, + error_message=("Cannot use VIF attachment API"), + ) request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'vifs') @@ -714,12 +793,16 @@ def attach_vif(self, session, vif_id, retry_on_conflict=True): if not retry_on_conflict: retriable_status_codes = set(retriable_status_codes) - {409} response = session.post( - request.url, json=body, - headers=request.headers, microversion=version, - retriable_status_codes=retriable_status_codes) + request.url, + json=body, + headers=request.headers, + microversion=version, + retriable_status_codes=retriable_status_codes, + ) - msg = ("Failed to attach VIF {vif} to bare metal node {node}" - .format(node=self.id, vif=vif_id)) + msg = "Failed to attach VIF {vif} to bare metal node {node}".format( + node=self.id, vif=vif_id + ) exceptions.raise_from_response(response, error_message=msg) def detach_vif(self, session, vif_id, ignore_missing=True): @@ -742,23 +825,31 @@ def detach_vif(self, session, vif_id, ignore_missing=True): """ session = self._get_session(session) version = self._assert_microversion_for( - session, 'commit', _common.VIF_VERSION, - error_message=("Cannot use VIF attachment API")) + session, + 'commit', + _common.VIF_VERSION, + error_message=("Cannot use VIF attachment API"), + ) request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'vifs', vif_id) response = session.delete( - request.url, headers=request.headers, microversion=version, - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + request.url, + headers=request.headers, + microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) if ignore_missing and response.status_code == 400: session.log.debug( 'VIF %(vif)s was already removed from node %(node)s', - {'vif': vif_id, 'node': self.id}) + {'vif': vif_id, 'node': self.id}, + ) return False - msg = ("Failed to detach VIF {vif} from bare metal node {node}" - .format(node=self.id, vif=vif_id)) + msg = "Failed to detach VIF {vif} from bare metal node {node}".format( + node=self.id, vif=vif_id + ) exceptions.raise_from_response(response, error_message=msg) return True @@ -777,16 +868,21 @@ def list_vifs(self, session): """ session = self._get_session(session) version = self._assert_microversion_for( - session, 'fetch', _common.VIF_VERSION, - error_message=("Cannot use VIF attachment API")) + session, + 'fetch', + _common.VIF_VERSION, + error_message=("Cannot use VIF attachment API"), + ) request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'vifs') response = session.get( - request.url, headers=request.headers, microversion=version) + request.url, headers=request.headers, microversion=version + ) - msg = ("Failed to list VIFs attached to bare metal node {node}" - .format(node=self.id)) + msg = "Failed to list VIFs attached to bare metal node {node}".format( + node=self.id + ) exceptions.raise_from_response(response, error_message=msg) return [vif['id'] for vif in response.json()['vifs']] @@ -809,10 +905,11 @@ def validate(self, session, required=('boot', 'deploy', 'power')): request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'validate') - response = session.get(request.url, headers=request.headers, - microversion=version) + response = session.get( + request.url, headers=request.headers, microversion=version + ) - msg = ("Failed to validate node {node}".format(node=self.id)) + msg = "Failed to validate node {node}".format(node=self.id) exceptions.raise_from_response(response, error_message=msg) result = response.json() @@ -826,11 +923,15 @@ def validate(self, session, required=('boot', 'deploy', 'power')): if failed: raise exceptions.ValidationException( 'Validation failed for required interfaces of node {node}:' - ' {failures}'.format(node=self.id, - failures=', '.join(failed))) + ' {failures}'.format( + node=self.id, failures=', '.join(failed) + ) + ) - return {key: ValidationResult(value.get('result'), value.get('reason')) - for key, value in result.items()} + return { + key: ValidationResult(value.get('result'), value.get('reason')) + for key, value in result.items() + } def set_maintenance(self, session, reason=None): """Enable maintenance mode on the node. @@ -859,10 +960,14 @@ def _do_maintenance_action(self, session, verb, body=None): request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'maintenance') response = getattr(session, verb)( - request.url, json=body, - headers=request.headers, microversion=version) - msg = ("Failed to change maintenance mode for node {node}" - .format(node=self.id)) + request.url, + json=body, + headers=request.headers, + microversion=version, + ) + msg = "Failed to change maintenance mode for node {node}".format( + node=self.id + ) exceptions.raise_from_response(response, error_message=msg) def get_boot_device(self, session): @@ -901,12 +1006,14 @@ def set_boot_device(self, session, boot_device, persistent=False): body = {'boot_device': boot_device, 'persistent': persistent} response = session.put( - request.url, json=body, - headers=request.headers, microversion=version, - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + request.url, + json=body, + headers=request.headers, + microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) - msg = ("Failed to set boot device for node {node}" - .format(node=self.id)) + msg = "Failed to set boot device for node {node}".format(node=self.id) exceptions.raise_from_response(response, error_message=msg) def get_supported_boot_devices(self, session): @@ -945,23 +1052,27 @@ def set_boot_mode(self, session, target): :raises: ValueError if ``target`` is not one of 'uefi or 'bios'. """ session = self._get_session(session) - version = utils.pick_microversion(session, - _common.CHANGE_BOOT_MODE_VERSION) + version = utils.pick_microversion( + session, _common.CHANGE_BOOT_MODE_VERSION + ) request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'states', 'boot_mode') if target not in ('uefi', 'bios'): - raise ValueError("Unrecognized boot mode %s." - "Boot mode should be one of 'uefi' or 'bios'." - % target) + raise ValueError( + "Unrecognized boot mode %s." + "Boot mode should be one of 'uefi' or 'bios'." % target + ) body = {'target': target} response = session.put( - request.url, json=body, - headers=request.headers, microversion=version, - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + request.url, + json=body, + headers=request.headers, + microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) - msg = ("Failed to change boot mode for node {node}" - .format(node=self.id)) + msg = "Failed to change boot mode for node {node}".format(node=self.id) exceptions.raise_from_response(response, error_message=msg) def set_secure_boot(self, session, target): @@ -976,23 +1087,29 @@ def set_secure_boot(self, session, target): :raises: ValueError if ``target`` is not boolean. """ session = self._get_session(session) - version = utils.pick_microversion(session, - _common.CHANGE_BOOT_MODE_VERSION) + version = utils.pick_microversion( + session, _common.CHANGE_BOOT_MODE_VERSION + ) request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'states', 'secure_boot') if not isinstance(target, bool): - raise ValueError("Invalid target %s. It should be True or False " - "corresponding to secure boot state 'on' or 'off'" - % target) + raise ValueError( + "Invalid target %s. It should be True or False " + "corresponding to secure boot state 'on' or 'off'" % target + ) body = {'target': target} response = session.put( - request.url, json=body, - headers=request.headers, microversion=version, - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + request.url, + json=body, + headers=request.headers, + microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) - msg = ("Failed to change secure boot state for {node}" - .format(node=self.id)) + msg = "Failed to change secure boot state for {node}".format( + node=self.id + ) exceptions.raise_from_response(response, error_message=msg) def add_trait(self, session, trait): @@ -1006,12 +1123,16 @@ def add_trait(self, session, trait): request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'traits', trait) response = session.put( - request.url, json=None, - headers=request.headers, microversion=version, - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + request.url, + json=None, + headers=request.headers, + microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) - msg = ("Failed to add trait {trait} for node {node}" - .format(trait=trait, node=self.id)) + msg = "Failed to add trait {trait} for node {node}".format( + trait=trait, node=self.id + ) exceptions.raise_from_response(response, error_message=msg) self.traits = list(set(self.traits or ()) | {trait}) @@ -1034,18 +1155,24 @@ def remove_trait(self, session, trait, ignore_missing=True): request.url = utils.urljoin(request.url, 'traits', trait) response = session.delete( - request.url, headers=request.headers, microversion=version, - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + request.url, + headers=request.headers, + microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) if ignore_missing and response.status_code == 400: session.log.debug( 'Trait %(trait)s was already removed from node %(node)s', - {'trait': trait, 'node': self.id}) + {'trait': trait, 'node': self.id}, + ) return False - msg = ("Failed to remove trait {trait} from bare metal node {node}" - .format(node=self.id, trait=trait)) - exceptions.raise_from_response(response, error_message=msg) + msg = "Failed to remove trait {trait} from bare metal node {node}" + exceptions.raise_from_response( + response, + error_message=msg.format(node=self.id, trait=trait), + ) if self.traits: self.traits = list(set(self.traits) - {trait}) @@ -1069,12 +1196,14 @@ def set_traits(self, session, traits): body = {'traits': traits} response = session.put( - request.url, json=body, - headers=request.headers, microversion=version, - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + request.url, + json=body, + headers=request.headers, + microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) - msg = ("Failed to set traits for node {node}" - .format(node=self.id)) + msg = "Failed to set traits for node {node}".format(node=self.id) exceptions.raise_from_response(response, error_message=msg) self.traits = traits @@ -1091,18 +1220,25 @@ def call_vendor_passthru(self, session, verb, method, body=None): session = self._get_session(session) version = self._get_microversion(session, action='commit') request = self._prepare_request(requires_id=True) - request.url = utils.urljoin(request.url, 'vendor_passthru?method={}' - .format(method)) + request.url = utils.urljoin( + request.url, 'vendor_passthru?method={}'.format(method) + ) call = getattr(session, verb.lower()) response = call( - request.url, json=body, - headers=request.headers, microversion=version, - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + request.url, + json=body, + headers=request.headers, + microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) - msg = ("Failed to call vendor_passthru for node {node}, verb {verb}" - " and method {method}" - .format(node=self.id, verb=verb, method=method)) + msg = ( + "Failed to call vendor_passthru for node {node}, verb {verb}" + " and method {method}".format( + node=self.id, verb=verb, method=method + ) + ) exceptions.raise_from_response(response, error_message=msg) return response @@ -1119,11 +1255,15 @@ def list_vendor_passthru(self, session): request.url = utils.urljoin(request.url, 'vendor_passthru/methods') response = session.get( - request.url, headers=request.headers, microversion=version, - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + request.url, + headers=request.headers, + microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) - msg = ("Failed to list vendor_passthru methods for node {node}" - .format(node=self.id)) + msg = "Failed to list vendor_passthru methods for node {node}".format( + node=self.id + ) exceptions.raise_from_response(response, error_message=msg) return response.json() @@ -1156,8 +1296,7 @@ def set_console_mode(self, session, enabled): if not isinstance(enabled, bool): raise ValueError( "Invalid enabled %s. It should be True or False " - "corresponding to console enabled or disabled" - % enabled + "corresponding to console enabled or disabled" % enabled ) body = {'enabled': enabled} @@ -1174,8 +1313,16 @@ def set_console_mode(self, session, enabled): ) exceptions.raise_from_response(response, error_message=msg) - def patch(self, session, patch=None, prepend_key=True, has_body=True, - retry_on_conflict=None, base_path=None, reset_interfaces=None): + def patch( + self, + session, + patch=None, + prepend_key=True, + has_body=True, + retry_on_conflict=None, + base_path=None, + reset_interfaces=None, + ): if reset_interfaces is not None: # The id cannot be dirty for an commit @@ -1190,24 +1337,34 @@ def patch(self, session, patch=None, prepend_key=True, has_body=True, session = self._get_session(session) microversion = self._assert_microversion_for( - session, 'commit', _common.RESET_INTERFACES_VERSION) + session, 'commit', _common.RESET_INTERFACES_VERSION + ) params = [('reset_interfaces', reset_interfaces)] - request = self._prepare_request(requires_id=True, - prepend_key=prepend_key, - base_path=base_path, patch=True, - params=params) + request = self._prepare_request( + requires_id=True, + prepend_key=prepend_key, + base_path=base_path, + patch=True, + params=params, + ) if patch: request.body += self._convert_patch(patch) - return self._commit(session, request, 'PATCH', microversion, - has_body=has_body, - retry_on_conflict=retry_on_conflict) + return self._commit( + session, + request, + 'PATCH', + microversion, + has_body=has_body, + retry_on_conflict=retry_on_conflict, + ) else: - return super(Node, self).patch(session, patch=patch, - retry_on_conflict=retry_on_conflict) + return super(Node, self).patch( + session, patch=patch, retry_on_conflict=retry_on_conflict + ) NodeDetail = Node diff --git a/openstack/baremetal/v1/port.py b/openstack/baremetal/v1/port.py index a5a309526..79d3c6369 100644 --- a/openstack/baremetal/v1/port.py +++ b/openstack/baremetal/v1/port.py @@ -30,7 +30,9 @@ class Port(_common.ListMixin, resource.Resource): commit_jsonpatch = True _query_mapping = resource.QueryParameters( - 'address', 'node', 'portgroup', + 'address', + 'node', + 'portgroup', fields={'type': _common.fields_type}, node_id='node_uuid', ) diff --git a/openstack/baremetal/v1/port_group.py b/openstack/baremetal/v1/port_group.py index 2fb55267b..123a563e6 100644 --- a/openstack/baremetal/v1/port_group.py +++ b/openstack/baremetal/v1/port_group.py @@ -30,7 +30,8 @@ class PortGroup(_common.ListMixin, resource.Resource): commit_jsonpatch = True _query_mapping = resource.QueryParameters( - 'node', 'address', + 'node', + 'address', fields={'type': _common.fields_type}, ) @@ -52,8 +53,9 @@ class PortGroup(_common.ListMixin, resource.Resource): internal_info = resource.Body('internal_info') #: Whether ports that are members of this portgroup can be used as #: standalone ports. Added in API microversion 1.23. - is_standalone_ports_supported = resource.Body('standalone_ports_supported', - type=bool) + is_standalone_ports_supported = resource.Body( + 'standalone_ports_supported', type=bool + ) #: A list of relative links, including the self and bookmark links. links = resource.Body('links', type=list) #: Port bonding mode. Added in API microversion 1.26. diff --git a/openstack/baremetal/v1/volume_connector.py b/openstack/baremetal/v1/volume_connector.py index f7a1a2752..395e90cbb 100644 --- a/openstack/baremetal/v1/volume_connector.py +++ b/openstack/baremetal/v1/volume_connector.py @@ -30,7 +30,8 @@ class VolumeConnector(_common.ListMixin, resource.Resource): commit_jsonpatch = True _query_mapping = resource.QueryParameters( - 'node', 'detail', + 'node', + 'detail', fields={'type': _common.fields_type}, ) diff --git a/openstack/baremetal/v1/volume_target.py b/openstack/baremetal/v1/volume_target.py index a5762a479..52d6bd382 100644 --- a/openstack/baremetal/v1/volume_target.py +++ b/openstack/baremetal/v1/volume_target.py @@ -30,7 +30,8 @@ class VolumeTarget(_common.ListMixin, resource.Resource): commit_jsonpatch = True _query_mapping = resource.QueryParameters( - 'node', 'detail', + 'node', + 'detail', fields={'type': _common.fields_type}, ) diff --git a/openstack/baremetal_introspection/v1/_proxy.py b/openstack/baremetal_introspection/v1/_proxy.py index cec1bdfda..e75d9ac14 100644 --- a/openstack/baremetal_introspection/v1/_proxy.py +++ b/openstack/baremetal_introspection/v1/_proxy.py @@ -70,8 +70,9 @@ def start_introspection(self, node, manage_boot=None): :returns: :class:`~.introspection.Introspection` instance. """ node = self._get_resource(_node.Node, node) - res = _introspect.Introspection.new(connection=self._get_connection(), - id=node.id) + res = _introspect.Introspection.new( + connection=self._get_connection(), id=node.id + ) kwargs = {} if manage_boot is not None: kwargs['manage_boot'] = manage_boot @@ -126,8 +127,9 @@ def abort_introspection(self, introspection, ignore_missing=True): if not ignore_missing: raise - def wait_for_introspection(self, introspection, timeout=None, - ignore_error=False): + def wait_for_introspection( + self, introspection, timeout=None, ignore_error=False + ): """Wait for the introspection to finish. :param introspection: The value can be the name or ID of an diff --git a/openstack/baremetal_introspection/v1/introspection.py b/openstack/baremetal_introspection/v1/introspection.py index 06d38239d..7a0c2a67f 100644 --- a/openstack/baremetal_introspection/v1/introspection.py +++ b/openstack/baremetal_introspection/v1/introspection.py @@ -67,10 +67,12 @@ def abort(self, session): request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'abort') response = session.post( - request.url, headers=request.headers, microversion=version, - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) - msg = ("Failed to abort introspection for node {id}" - .format(id=self.id)) + request.url, + headers=request.headers, + microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) + msg = "Failed to abort introspection for node {id}".format(id=self.id) exceptions.raise_from_response(response, error_message=msg) def get_data(self, session, processed=True): @@ -89,16 +91,21 @@ def get_data(self, session, processed=True): """ session = self._get_session(session) - version = (self._get_microversion(session, action='fetch') - if processed else '1.17') + version = ( + self._get_microversion(session, action='fetch') + if processed + else '1.17' + ) request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'data') if not processed: request.url = utils.urljoin(request.url, 'unprocessed') response = session.get( - request.url, headers=request.headers, microversion=version) - msg = ("Failed to fetch introspection data for node {id}" - .format(id=self.id)) + request.url, headers=request.headers, microversion=version + ) + msg = "Failed to fetch introspection data for node {id}".format( + id=self.id + ) exceptions.raise_from_response(response, error_message=msg) return response.json() @@ -121,20 +128,23 @@ def wait(self, session, timeout=None, ignore_error=False): return self for count in utils.iterate_timeout( - timeout, - "Timeout waiting for introspection on node %s" % self.id): + timeout, "Timeout waiting for introspection on node %s" % self.id + ): self.fetch(session) if self._check_state(ignore_error): return self - _logger.debug('Still waiting for introspection of node %(node)s, ' - 'the current state is "%(state)s"', - {'node': self.id, 'state': self.state}) + _logger.debug( + 'Still waiting for introspection of node %(node)s, ' + 'the current state is "%(state)s"', + {'node': self.id, 'state': self.state}, + ) def _check_state(self, ignore_error): if self.state == 'error' and not ignore_error: raise exceptions.ResourceFailure( - "Introspection of node %(node)s failed: %(error)s" % - {'node': self.id, 'error': self.error}) + "Introspection of node %(node)s failed: %(error)s" + % {'node': self.id, 'error': self.error} + ) else: return self.is_finished diff --git a/openstack/tests/functional/baremetal/base.py b/openstack/tests/functional/baremetal/base.py index 948bc9dcb..b0e03696f 100644 --- a/openstack/tests/functional/baremetal/base.py +++ b/openstack/tests/functional/baremetal/base.py @@ -20,29 +20,36 @@ class BaseBaremetalTest(base.BaseFunctionalTest): def setUp(self): super(BaseBaremetalTest, self).setUp() - self.require_service('baremetal', - min_microversion=self.min_microversion) + self.require_service( + 'baremetal', min_microversion=self.min_microversion + ) def create_allocation(self, **kwargs): allocation = self.conn.baremetal.create_allocation(**kwargs) self.addCleanup( - lambda: self.conn.baremetal.delete_allocation(allocation.id, - ignore_missing=True)) + lambda: self.conn.baremetal.delete_allocation( + allocation.id, ignore_missing=True + ) + ) return allocation def create_chassis(self, **kwargs): chassis = self.conn.baremetal.create_chassis(**kwargs) self.addCleanup( - lambda: self.conn.baremetal.delete_chassis(chassis.id, - ignore_missing=True)) + lambda: self.conn.baremetal.delete_chassis( + chassis.id, ignore_missing=True + ) + ) return chassis def create_node(self, driver='fake-hardware', **kwargs): node = self.conn.baremetal.create_node(driver=driver, **kwargs) self.node_id = node.id self.addCleanup( - lambda: self.conn.baremetal.delete_node(self.node_id, - ignore_missing=True)) + lambda: self.conn.baremetal.delete_node( + self.node_id, ignore_missing=True + ) + ) self.assertIsNotNone(self.node_id) return node @@ -50,50 +57,58 @@ def create_port(self, node_id=None, **kwargs): node_id = node_id or self.node_id port = self.conn.baremetal.create_port(node_uuid=node_id, **kwargs) self.addCleanup( - lambda: self.conn.baremetal.delete_port(port.id, - ignore_missing=True)) + lambda: self.conn.baremetal.delete_port( + port.id, ignore_missing=True + ) + ) return port def create_port_group(self, node_id=None, **kwargs): node_id = node_id or self.node_id - port_group = self.conn.baremetal.create_port_group(node_uuid=node_id, - **kwargs) + port_group = self.conn.baremetal.create_port_group( + node_uuid=node_id, **kwargs + ) self.addCleanup( - lambda: self.conn.baremetal.delete_port_group(port_group.id, - ignore_missing=True)) + lambda: self.conn.baremetal.delete_port_group( + port_group.id, ignore_missing=True + ) + ) return port_group def create_volume_connector(self, node_id=None, **kwargs): node_id = node_id or self.node_id volume_connector = self.conn.baremetal.create_volume_connector( - node_uuid=node_id, **kwargs) + node_uuid=node_id, **kwargs + ) self.addCleanup( - lambda: - self.conn.baremetal.delete_volume_connector(volume_connector.id, - ignore_missing=True)) + lambda: self.conn.baremetal.delete_volume_connector( + volume_connector.id, ignore_missing=True + ) + ) return volume_connector def create_volume_target(self, node_id=None, **kwargs): node_id = node_id or self.node_id volume_target = self.conn.baremetal.create_volume_target( - node_uuid=node_id, **kwargs) + node_uuid=node_id, **kwargs + ) self.addCleanup( - lambda: - self.conn.baremetal.delete_volume_target(volume_target.id, - ignore_missing=True)) + lambda: self.conn.baremetal.delete_volume_target( + volume_target.id, ignore_missing=True + ) + ) return volume_target def create_deploy_template(self, **kwargs): - """Create a new deploy_template from attributes. - """ + """Create a new deploy_template from attributes.""" - deploy_template = self.conn.baremetal.create_deploy_template( - **kwargs) + deploy_template = self.conn.baremetal.create_deploy_template(**kwargs) self.addCleanup( lambda: self.conn.baremetal.delete_deploy_template( - deploy_template.id, - ignore_missing=True)) + deploy_template.id, ignore_missing=True + ) + ) return deploy_template diff --git a/openstack/tests/functional/baremetal/test_baremetal_allocation.py b/openstack/tests/functional/baremetal/test_baremetal_allocation.py index 2f93285b5..c6a3fa2d4 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_allocation.py +++ b/openstack/tests/functional/baremetal/test_baremetal_allocation.py @@ -17,7 +17,6 @@ class Base(base.BaseBaremetalTest): - def setUp(self): super(Base, self).setUp() # NOTE(dtantsur): generate a unique resource class to prevent parallel @@ -27,15 +26,15 @@ def setUp(self): def _create_available_node(self): node = self.create_node(resource_class=self.resource_class) - self.conn.baremetal.set_node_provision_state(node, 'manage', - wait=True) - self.conn.baremetal.set_node_provision_state(node, 'provide', - wait=True) + self.conn.baremetal.set_node_provision_state(node, 'manage', wait=True) + self.conn.baremetal.set_node_provision_state( + node, 'provide', wait=True + ) # Make sure the node has non-empty power state by forcing power off. self.conn.baremetal.set_node_power_state(node, 'power off') self.addCleanup( - lambda: self.conn.baremetal.update_node(node.id, - instance_id=None)) + lambda: self.conn.baremetal.update_node(node.id, instance_id=None) + ) return node @@ -56,7 +55,8 @@ def test_allocation_create_get_delete(self): self.assertIsNone(allocation.last_error) with_fields = self.conn.baremetal.get_allocation( - allocation.id, fields=['uuid', 'node_uuid']) + allocation.id, fields=['uuid', 'node_uuid'] + ) self.assertEqual(allocation.id, with_fields.id) self.assertIsNone(with_fields.state) @@ -64,21 +64,27 @@ def test_allocation_create_get_delete(self): self.assertEqual(allocation.id, node.allocation_id) self.conn.baremetal.delete_allocation(allocation, ignore_missing=False) - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.get_allocation, allocation.id) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.get_allocation, + allocation.id, + ) def test_allocation_list(self): allocation1 = self.create_allocation( - resource_class=self.resource_class) + resource_class=self.resource_class + ) allocation2 = self.create_allocation( - resource_class=self.resource_class + '-fail') + resource_class=self.resource_class + '-fail' + ) self.conn.baremetal.wait_for_allocation(allocation1) self.conn.baremetal.wait_for_allocation(allocation2, ignore_error=True) allocations = self.conn.baremetal.allocations() - self.assertEqual({p.id for p in allocations}, - {allocation1.id, allocation2.id}) + self.assertEqual( + {p.id for p in allocations}, {allocation1.id, allocation2.id} + ) allocations = self.conn.baremetal.allocations(state='active') self.assertEqual([p.id for p in allocations], [allocation1.id]) @@ -87,15 +93,19 @@ def test_allocation_list(self): self.assertEqual([p.id for p in allocations], [allocation1.id]) allocations = self.conn.baremetal.allocations( - resource_class=self.resource_class + '-fail') + resource_class=self.resource_class + '-fail' + ) self.assertEqual([p.id for p in allocations], [allocation2.id]) def test_allocation_negative_failure(self): allocation = self.create_allocation( - resource_class=self.resource_class + '-fail') - self.assertRaises(exceptions.SDKException, - self.conn.baremetal.wait_for_allocation, - allocation) + resource_class=self.resource_class + '-fail' + ) + self.assertRaises( + exceptions.SDKException, + self.conn.baremetal.wait_for_allocation, + allocation, + ) allocation = self.conn.baremetal.get_allocation(allocation.id) self.assertEqual('error', allocation.state) @@ -103,11 +113,17 @@ def test_allocation_negative_failure(self): def test_allocation_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.get_allocation, uuid) - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.delete_allocation, uuid, - ignore_missing=False) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.get_allocation, + uuid, + ) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.delete_allocation, + uuid, + ignore_missing=False, + ) self.assertIsNone(self.conn.baremetal.delete_allocation(uuid)) def test_allocation_fields(self): @@ -133,7 +149,8 @@ def test_allocation_update(self): self.assertEqual({}, allocation.extra) allocation = self.conn.baremetal.update_allocation( - allocation, name=name, extra={'answer': 42}) + allocation, name=name, extra={'answer': 42} + ) self.assertEqual(name, allocation.name) self.assertEqual({'answer': 42}, allocation.extra) @@ -142,8 +159,11 @@ def test_allocation_update(self): self.assertEqual({'answer': 42}, allocation.extra) self.conn.baremetal.delete_allocation(allocation, ignore_missing=False) - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.get_allocation, allocation.id) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.get_allocation, + allocation.id, + ) def test_allocation_patch(self): name = 'ossdk-name2' @@ -156,8 +176,12 @@ def test_allocation_patch(self): self.assertEqual({}, allocation.extra) allocation = self.conn.baremetal.patch_allocation( - allocation, [{'op': 'replace', 'path': '/name', 'value': name}, - {'op': 'add', 'path': '/extra/answer', 'value': 42}]) + allocation, + [ + {'op': 'replace', 'path': '/name', 'value': name}, + {'op': 'add', 'path': '/extra/answer', 'value': 42}, + ], + ) self.assertEqual(name, allocation.name) self.assertEqual({'answer': 42}, allocation.extra) @@ -166,8 +190,12 @@ def test_allocation_patch(self): self.assertEqual({'answer': 42}, allocation.extra) allocation = self.conn.baremetal.patch_allocation( - allocation, [{'op': 'remove', 'path': '/name'}, - {'op': 'remove', 'path': '/extra/answer'}]) + allocation, + [ + {'op': 'remove', 'path': '/name'}, + {'op': 'remove', 'path': '/extra/answer'}, + ], + ) self.assertIsNone(allocation.name) self.assertEqual({}, allocation.extra) @@ -176,5 +204,8 @@ def test_allocation_patch(self): self.assertEqual({}, allocation.extra) self.conn.baremetal.delete_allocation(allocation, ignore_missing=False) - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.get_allocation, allocation.id) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.get_allocation, + allocation.id, + ) diff --git a/openstack/tests/functional/baremetal/test_baremetal_chassis.py b/openstack/tests/functional/baremetal/test_baremetal_chassis.py index 0274eca3f..53ba687f5 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_chassis.py +++ b/openstack/tests/functional/baremetal/test_baremetal_chassis.py @@ -16,7 +16,6 @@ class TestBareMetalChassis(base.BaseBaremetalTest): - def test_chassis_create_get_delete(self): chassis = self.create_chassis() @@ -24,8 +23,11 @@ def test_chassis_create_get_delete(self): self.assertEqual(loaded.id, chassis.id) self.conn.baremetal.delete_chassis(chassis, ignore_missing=False) - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.get_chassis, chassis.id) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.get_chassis, + chassis.id, + ) def test_chassis_update(self): chassis = self.create_chassis() @@ -41,7 +43,8 @@ def test_chassis_patch(self): chassis = self.create_chassis() chassis = self.conn.baremetal.patch_chassis( - chassis, dict(path='/extra/answer', op='add', value=42)) + chassis, dict(path='/extra/answer', op='add', value=42) + ) self.assertEqual({'answer': 42}, chassis.extra) chassis = self.conn.baremetal.get_chassis(chassis.id) @@ -49,14 +52,21 @@ def test_chassis_patch(self): def test_chassis_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.get_chassis, uuid) - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.find_chassis, uuid, - ignore_missing=False) - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.delete_chassis, uuid, - ignore_missing=False) + self.assertRaises( + exceptions.ResourceNotFound, self.conn.baremetal.get_chassis, uuid + ) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.find_chassis, + uuid, + ignore_missing=False, + ) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.delete_chassis, + uuid, + ignore_missing=False, + ) self.assertIsNone(self.conn.baremetal.find_chassis(uuid)) self.assertIsNone(self.conn.baremetal.delete_chassis(uuid)) diff --git a/openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py b/openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py index eb59056dc..eec646477 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py +++ b/openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py @@ -27,27 +27,24 @@ def test_baremetal_deploy_create_get_delete(self): "interface": "bios", "step": "apply_configuration", "args": { - "settings": [ - { - "name": "LogicalProc", - "value": "Enabled" - } - ] + "settings": [{"name": "LogicalProc", "value": "Enabled"}] }, - "priority": 150 + "priority": 150, } ] deploy_template = self.create_deploy_template( - name='CUSTOM_DEPLOY_TEMPLATE', - steps=steps) - loaded = self.conn.baremetal.get_deploy_template( - deploy_template.id) + name='CUSTOM_DEPLOY_TEMPLATE', steps=steps + ) + loaded = self.conn.baremetal.get_deploy_template(deploy_template.id) self.assertEqual(loaded.id, deploy_template.id) - self.conn.baremetal.delete_deploy_template(deploy_template, - ignore_missing=False) - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.get_deploy_template, - deploy_template.id) + self.conn.baremetal.delete_deploy_template( + deploy_template, ignore_missing=False + ) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.get_deploy_template, + deploy_template.id, + ) def test_baremetal_deploy_template_list(self): steps = [ @@ -55,36 +52,33 @@ def test_baremetal_deploy_template_list(self): "interface": "bios", "step": "apply_configuration", "args": { - "settings": [ - { - "name": "LogicalProc", - "value": "Enabled" - } - ] + "settings": [{"name": "LogicalProc", "value": "Enabled"}] }, - "priority": 150 + "priority": 150, } ] deploy_template1 = self.create_deploy_template( - name='CUSTOM_DEPLOY_TEMPLATE1', - steps=steps) + name='CUSTOM_DEPLOY_TEMPLATE1', steps=steps + ) deploy_template2 = self.create_deploy_template( - name='CUSTOM_DEPLOY_TEMPLATE2', - steps=steps) + name='CUSTOM_DEPLOY_TEMPLATE2', steps=steps + ) deploy_templates = self.conn.baremetal.deploy_templates() ids = [template.id for template in deploy_templates] self.assertIn(deploy_template1.id, ids) self.assertIn(deploy_template2.id, ids) deploy_templates_with_details = self.conn.baremetal.deploy_templates( - details=True) + details=True + ) for dp in deploy_templates_with_details: self.assertIsNotNone(dp.id) self.assertIsNotNone(dp.name) deploy_tempalte_with_fields = self.conn.baremetal.deploy_templates( - fields=['uuid']) + fields=['uuid'] + ) for dp in deploy_tempalte_with_fields: self.assertIsNotNone(dp.id) self.assertIsNone(dp.name) @@ -95,31 +89,29 @@ def test_baremetal_deploy_list_update_delete(self): "interface": "bios", "step": "apply_configuration", "args": { - "settings": [ - { - "name": "LogicalProc", - "value": "Enabled" - } - ] + "settings": [{"name": "LogicalProc", "value": "Enabled"}] }, - "priority": 150 + "priority": 150, } ] deploy_template = self.create_deploy_template( - name='CUSTOM_DEPLOY_TEMPLATE4', - steps=steps) + name='CUSTOM_DEPLOY_TEMPLATE4', steps=steps + ) self.assertFalse(deploy_template.extra) deploy_template.extra = {'answer': 42} deploy_template = self.conn.baremetal.update_deploy_template( - deploy_template) + deploy_template + ) self.assertEqual({'answer': 42}, deploy_template.extra) deploy_template = self.conn.baremetal.get_deploy_template( - deploy_template.id) + deploy_template.id + ) - self.conn.baremetal.delete_deploy_template(deploy_template.id, - ignore_missing=False) + self.conn.baremetal.delete_deploy_template( + deploy_template.id, ignore_missing=False + ) def test_baremetal_deploy_update(self): steps = [ @@ -127,27 +119,24 @@ def test_baremetal_deploy_update(self): "interface": "bios", "step": "apply_configuration", "args": { - "settings": [ - { - "name": "LogicalProc", - "value": "Enabled" - } - ] + "settings": [{"name": "LogicalProc", "value": "Enabled"}] }, - "priority": 150 + "priority": 150, } ] deploy_template = self.create_deploy_template( - name='CUSTOM_DEPLOY_TEMPLATE4', - steps=steps) + name='CUSTOM_DEPLOY_TEMPLATE4', steps=steps + ) deploy_template.extra = {'answer': 42} deploy_template = self.conn.baremetal.update_deploy_template( - deploy_template) + deploy_template + ) self.assertEqual({'answer': 42}, deploy_template.extra) deploy_template = self.conn.baremetal.get_deploy_template( - deploy_template.id) + deploy_template.id + ) self.assertEqual({'answer': 42}, deploy_template.extra) def test_deploy_template_patch(self): @@ -157,34 +146,34 @@ def test_deploy_template_patch(self): "interface": "bios", "step": "apply_configuration", "args": { - "settings": [ - { - "name": "LogicalProc", - "value": "Enabled" - } - ] + "settings": [{"name": "LogicalProc", "value": "Enabled"}] }, - "priority": 150 + "priority": 150, } ] - deploy_template = self.create_deploy_template( - name=name, - steps=steps) + deploy_template = self.create_deploy_template(name=name, steps=steps) deploy_template = self.conn.baremetal.patch_deploy_template( - deploy_template, dict(path='/extra/answer', op='add', value=42)) + deploy_template, dict(path='/extra/answer', op='add', value=42) + ) self.assertEqual({'answer': 42}, deploy_template.extra) - self.assertEqual(name, - deploy_template.name) + self.assertEqual(name, deploy_template.name) deploy_template = self.conn.baremetal.get_deploy_template( - deploy_template.id) + deploy_template.id + ) self.assertEqual({'answer': 42}, deploy_template.extra) def test_deploy_template_negative_non_existing(self): uuid = "bbb45f41-d4bc-4307-8d1d-32f95ce1e920" - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.get_deploy_template, uuid) - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.delete_deploy_template, uuid, - ignore_missing=False) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.get_deploy_template, + uuid, + ) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.delete_deploy_template, + uuid, + ignore_missing=False, + ) self.assertIsNone(self.conn.baremetal.delete_deploy_template(uuid)) diff --git a/openstack/tests/functional/baremetal/test_baremetal_driver.py b/openstack/tests/functional/baremetal/test_baremetal_driver.py index 793330bb6..252ca2073 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_driver.py +++ b/openstack/tests/functional/baremetal/test_baremetal_driver.py @@ -16,7 +16,6 @@ class TestBareMetalDriver(base.BaseBaremetalTest): - def test_fake_hardware_get(self): driver = self.conn.baremetal.get_driver('fake-hardware') self.assertEqual('fake-hardware', driver.name) @@ -27,8 +26,11 @@ def test_fake_hardware_list(self): self.assertIn('fake-hardware', [d.name for d in drivers]) def test_driver_negative_non_existing(self): - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.get_driver, 'not-a-driver') + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.get_driver, + 'not-a-driver', + ) class TestBareMetalDriverDetails(base.BaseBaremetalTest): @@ -39,17 +41,21 @@ def test_fake_hardware_get(self): driver = self.conn.baremetal.get_driver('fake-hardware') self.assertEqual('fake-hardware', driver.name) for iface in ('boot', 'deploy', 'management', 'power'): - self.assertIn('fake', - getattr(driver, 'enabled_%s_interfaces' % iface)) - self.assertEqual('fake', - getattr(driver, 'default_%s_interface' % iface)) + self.assertIn( + 'fake', getattr(driver, 'enabled_%s_interfaces' % iface) + ) + self.assertEqual( + 'fake', getattr(driver, 'default_%s_interface' % iface) + ) self.assertNotEqual([], driver.hosts) def test_fake_hardware_list_details(self): drivers = self.conn.baremetal.drivers(details=True) driver = [d for d in drivers if d.name == 'fake-hardware'][0] for iface in ('boot', 'deploy', 'management', 'power'): - self.assertIn('fake', - getattr(driver, 'enabled_%s_interfaces' % iface)) - self.assertEqual('fake', - getattr(driver, 'default_%s_interface' % iface)) + self.assertIn( + 'fake', getattr(driver, 'enabled_%s_interfaces' % iface) + ) + self.assertEqual( + 'fake', getattr(driver, 'default_%s_interface' % iface) + ) diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index bdd6cd8b6..03a909064 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -27,17 +27,19 @@ def test_node_create_get_delete(self): # NOTE(dtantsur): get_node and find_node only differ in handing missing # nodes, otherwise they are identical. - for call, ident in [(self.conn.baremetal.get_node, self.node_id), - (self.conn.baremetal.get_node, 'node-name'), - (self.conn.baremetal.find_node, self.node_id), - (self.conn.baremetal.find_node, 'node-name')]: + for call, ident in [ + (self.conn.baremetal.get_node, self.node_id), + (self.conn.baremetal.get_node, 'node-name'), + (self.conn.baremetal.find_node, self.node_id), + (self.conn.baremetal.find_node, 'node-name'), + ]: found = call(ident) self.assertEqual(node.id, found.id) self.assertEqual(node.name, found.name) with_fields = self.conn.baremetal.get_node( - 'node-name', - fields=['uuid', 'driver', 'instance_id']) + 'node-name', fields=['uuid', 'driver', 'instance_id'] + ) self.assertEqual(node.id, with_fields.id) self.assertEqual(node.driver, with_fields.driver) self.assertIsNone(with_fields.name) @@ -47,8 +49,11 @@ def test_node_create_get_delete(self): self.assertIn(node.id, [n.id for n in nodes]) self.conn.baremetal.delete_node(node, ignore_missing=False) - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.get_node, self.node_id) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.get_node, + self.node_id, + ) def test_node_create_in_available(self): node = self.create_node(name='node-name', provision_state='available') @@ -57,8 +62,11 @@ def test_node_create_in_available(self): self.assertEqual(node.provision_state, 'available') self.conn.baremetal.delete_node(node, ignore_missing=False) - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.get_node, self.node_id) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.get_node, + self.node_id, + ) def test_node_update(self): node = self.create_node(name='node-name', extra={'foo': 'bar'}) @@ -66,8 +74,7 @@ def test_node_update(self): node.extra = {'answer': 42} instance_uuid = str(uuid.uuid4()) - node = self.conn.baremetal.update_node(node, - instance_id=instance_uuid) + node = self.conn.baremetal.update_node(node, instance_id=instance_uuid) self.assertEqual('new-name', node.name) self.assertEqual({'answer': 42}, node.extra) self.assertEqual(instance_uuid, node.instance_id) @@ -77,8 +84,7 @@ def test_node_update(self): self.assertEqual({'answer': 42}, node.extra) self.assertEqual(instance_uuid, node.instance_id) - node = self.conn.baremetal.update_node(node, - instance_id=None) + node = self.conn.baremetal.update_node(node, instance_id=None) self.assertIsNone(node.instance_id) node = self.conn.baremetal.get_node('new-name') @@ -88,9 +94,9 @@ def test_node_update_by_name(self): self.create_node(name='node-name', extra={'foo': 'bar'}) instance_uuid = str(uuid.uuid4()) - node = self.conn.baremetal.update_node('node-name', - instance_id=instance_uuid, - extra={'answer': 42}) + node = self.conn.baremetal.update_node( + 'node-name', instance_id=instance_uuid, extra={'answer': 42} + ) self.assertEqual({'answer': 42}, node.extra) self.assertEqual(instance_uuid, node.instance_id) @@ -98,8 +104,7 @@ def test_node_update_by_name(self): self.assertEqual({'answer': 42}, node.extra) self.assertEqual(instance_uuid, node.instance_id) - node = self.conn.baremetal.update_node('node-name', - instance_id=None) + node = self.conn.baremetal.update_node('node-name', instance_id=None) self.assertIsNone(node.instance_id) node = self.conn.baremetal.get_node('node-name') @@ -112,8 +117,11 @@ def test_node_patch(self): node = self.conn.baremetal.patch_node( node, - [dict(path='/instance_id', op='replace', value=instance_uuid), - dict(path='/extra/answer', op='add', value=42)]) + [ + dict(path='/instance_id', op='replace', value=instance_uuid), + dict(path='/extra/answer', op='add', value=42), + ], + ) self.assertEqual('new-name', node.name) self.assertEqual({'foo': 'bar', 'answer': 42}, node.extra) self.assertEqual(instance_uuid, node.instance_id) @@ -125,8 +133,11 @@ def test_node_patch(self): node = self.conn.baremetal.patch_node( node, - [dict(path='/instance_id', op='remove'), - dict(path='/extra/answer', op='remove')]) + [ + dict(path='/instance_id', op='remove'), + dict(path='/extra/answer', op='remove'), + ], + ) self.assertIsNone(node.instance_id) self.assertNotIn('answer', node.extra) @@ -136,12 +147,16 @@ def test_node_patch(self): def test_node_list_update_delete(self): self.create_node(name='node-name', extra={'foo': 'bar'}) - node = next(n for n in - self.conn.baremetal.nodes(details=True, - provision_state='enroll', - is_maintenance=False, - associated=False) - if n.name == 'node-name') + node = next( + n + for n in self.conn.baremetal.nodes( + details=True, + provision_state='enroll', + is_maintenance=False, + associated=False, + ) + if n.name == 'node-name' + ) self.assertEqual(node.extra, {'foo': 'bar'}) # This test checks that resources returned from listing are usable @@ -157,12 +172,12 @@ def test_node_create_in_enroll_provide(self): self.assertIsNone(node.power_state) self.assertFalse(node.is_maintenance) - self.conn.baremetal.set_node_provision_state(node, 'manage', - wait=True) + self.conn.baremetal.set_node_provision_state(node, 'manage', wait=True) self.assertEqual(node.provision_state, 'manageable') - self.conn.baremetal.set_node_provision_state(node, 'provide', - wait=True) + self.conn.baremetal.set_node_provision_state( + node, 'provide', wait=True + ) self.assertEqual(node.provision_state, 'available') def test_node_create_in_enroll_provide_by_name(self): @@ -175,12 +190,14 @@ def test_node_create_in_enroll_provide_by_name(self): self.assertIsNone(node.power_state) self.assertFalse(node.is_maintenance) - node = self.conn.baremetal.set_node_provision_state(name, 'manage', - wait=True) + node = self.conn.baremetal.set_node_provision_state( + name, 'manage', wait=True + ) self.assertEqual(node.provision_state, 'manageable') - node = self.conn.baremetal.set_node_provision_state(name, 'provide', - wait=True) + node = self.conn.baremetal.set_node_provision_state( + name, 'provide', wait=True + ) self.assertEqual(node.provision_state, 'available') def test_node_power_state(self): @@ -205,17 +222,27 @@ def test_node_validate(self): def test_node_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.get_node, uuid) - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.find_node, uuid, - ignore_missing=False) - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.delete_node, uuid, - ignore_missing=False) - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.update_node, uuid, - name='new-name') + self.assertRaises( + exceptions.ResourceNotFound, self.conn.baremetal.get_node, uuid + ) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.find_node, + uuid, + ignore_missing=False, + ) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.delete_node, + uuid, + ignore_missing=False, + ) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.update_node, + uuid, + name='new-name', + ) self.assertIsNone(self.conn.baremetal.find_node(uuid)) self.assertIsNone(self.conn.baremetal.delete_node(uuid)) @@ -287,8 +314,9 @@ def test_maintenance_via_update(self): self.assertIsNone(node.maintenance_reason) # Initial setting with the reason - node = self.conn.baremetal.update_node(node, is_maintenance=True, - maintenance_reason=reason) + node = self.conn.baremetal.update_node( + node, is_maintenance=True, maintenance_reason=reason + ) self.assertTrue(node.is_maintenance) self.assertEqual(reason, node.maintenance_reason) @@ -338,8 +366,9 @@ def test_retired(self): self.assertIsNone(node.retired_reason) # Set retired with reason - node = self.conn.baremetal.update_node(node, is_retired=True, - retired_reason=reason) + node = self.conn.baremetal.update_node( + node, is_retired=True, retired_reason=reason + ) self.assertTrue(node.is_retired) self.assertEqual(reason, node.retired_reason) @@ -354,7 +383,10 @@ def test_retired_in_available(self): # Set retired when node state available should fail! self.assertRaises( exceptions.ConflictException, - self.conn.baremetal.update_node, node, is_retired=True) + self.conn.baremetal.update_node, + node, + is_retired=True, + ) class TestBareMetalNodeFields(base.BaseBaremetalTest): @@ -364,7 +396,8 @@ class TestBareMetalNodeFields(base.BaseBaremetalTest): def test_node_fields(self): self.create_node() result = self.conn.baremetal.nodes( - fields=['uuid', 'name', 'instance_id']) + fields=['uuid', 'name', 'instance_id'] + ) for item in result: self.assertIsNotNone(item.id) self.assertIsNone(item.driver) @@ -384,21 +417,31 @@ def test_node_vif_attach_detach(self): # NOTE(dtantsur): The noop networking driver is completely noop - the # VIF list does not return anything of value. self.conn.baremetal.list_node_vifs(self.node) - res = self.conn.baremetal.detach_vif_from_node(self.node, self.vif_id, - ignore_missing=False) + res = self.conn.baremetal.detach_vif_from_node( + self.node, self.vif_id, ignore_missing=False + ) self.assertTrue(res) def test_node_vif_negative(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.attach_vif_to_node, - uuid, self.vif_id) - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.list_node_vifs, - uuid) - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.detach_vif_from_node, - uuid, self.vif_id, ignore_missing=False) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.attach_vif_to_node, + uuid, + self.vif_id, + ) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.list_node_vifs, + uuid, + ) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.detach_vif_from_node, + uuid, + self.vif_id, + ignore_missing=False, + ) class TestTraits(base.BaseBaremetalTest): @@ -419,14 +462,17 @@ def test_add_remove_node_trait(self): self.assertEqual(['CUSTOM_FAKE'], node.traits) self.conn.baremetal.add_node_trait(self.node, 'CUSTOM_REAL') - self.assertEqual(sorted(['CUSTOM_FAKE', 'CUSTOM_REAL']), - sorted(self.node.traits)) + self.assertEqual( + sorted(['CUSTOM_FAKE', 'CUSTOM_REAL']), sorted(self.node.traits) + ) node = self.conn.baremetal.get_node(self.node) - self.assertEqual(sorted(['CUSTOM_FAKE', 'CUSTOM_REAL']), - sorted(node.traits)) + self.assertEqual( + sorted(['CUSTOM_FAKE', 'CUSTOM_REAL']), sorted(node.traits) + ) - self.conn.baremetal.remove_node_trait(node, 'CUSTOM_FAKE', - ignore_missing=False) + self.conn.baremetal.remove_node_trait( + node, 'CUSTOM_FAKE', ignore_missing=False + ) self.assertEqual(['CUSTOM_REAL'], self.node.traits) node = self.conn.baremetal.get_node(self.node) self.assertEqual(['CUSTOM_REAL'], node.traits) diff --git a/openstack/tests/functional/baremetal/test_baremetal_port.py b/openstack/tests/functional/baremetal/test_baremetal_port.py index ee2488f67..38692d084 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_port.py +++ b/openstack/tests/functional/baremetal/test_baremetal_port.py @@ -16,7 +16,6 @@ class TestBareMetalPort(base.BaseBaremetalTest): - def setUp(self): super(TestBareMetalPort, self).setUp() self.node = self.create_node() @@ -34,21 +33,23 @@ def test_port_create_get_delete(self): self.assertIsNotNone(loaded.address) with_fields = self.conn.baremetal.get_port( - port.id, fields=['uuid', 'extra', 'node_id']) + port.id, fields=['uuid', 'extra', 'node_id'] + ) self.assertEqual(port.id, with_fields.id) self.assertIsNone(with_fields.address) self.conn.baremetal.delete_port(port, ignore_missing=False) - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.get_port, port.id) + self.assertRaises( + exceptions.ResourceNotFound, self.conn.baremetal.get_port, port.id + ) def test_port_list(self): node2 = self.create_node(name='test-node') - port1 = self.create_port(address='11:22:33:44:55:66', - node_id=node2.id) - port2 = self.create_port(address='11:22:33:44:55:77', - node_id=self.node.id) + port1 = self.create_port(address='11:22:33:44:55:66', node_id=node2.id) + port2 = self.create_port( + address='11:22:33:44:55:77', node_id=self.node.id + ) ports = self.conn.baremetal.ports(address='11:22:33:44:55:77') self.assertEqual([p.id for p in ports], [port2.id]) @@ -60,10 +61,16 @@ def test_port_list(self): self.assertEqual([p.id for p in ports], [port1.id]) def test_port_list_update_delete(self): - self.create_port(address='11:22:33:44:55:66', node_id=self.node.id, - extra={'foo': 'bar'}) - port = next(self.conn.baremetal.ports(details=True, - address='11:22:33:44:55:66')) + self.create_port( + address='11:22:33:44:55:66', + node_id=self.node.id, + extra={'foo': 'bar'}, + ) + port = next( + self.conn.baremetal.ports( + details=True, address='11:22:33:44:55:66' + ) + ) self.assertEqual(port.extra, {'foo': 'bar'}) # This test checks that resources returned from listing are usable @@ -88,7 +95,8 @@ def test_port_patch(self): port.address = '66:55:44:33:22:11' port = self.conn.baremetal.patch_port( - port, dict(path='/extra/answer', op='add', value=42)) + port, dict(path='/extra/answer', op='add', value=42) + ) self.assertEqual('66:55:44:33:22:11', port.address) self.assertEqual({'answer': 42}, port.extra) @@ -98,17 +106,27 @@ def test_port_patch(self): def test_port_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.get_port, uuid) - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.find_port, uuid, - ignore_missing=False) - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.delete_port, uuid, - ignore_missing=False) - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.update_port, uuid, - pxe_enabled=True) + self.assertRaises( + exceptions.ResourceNotFound, self.conn.baremetal.get_port, uuid + ) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.find_port, + uuid, + ignore_missing=False, + ) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.delete_port, + uuid, + ignore_missing=False, + ) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.update_port, + uuid, + pxe_enabled=True, + ) self.assertIsNone(self.conn.baremetal.find_port(uuid)) self.assertIsNone(self.conn.baremetal.delete_port(uuid)) diff --git a/openstack/tests/functional/baremetal/test_baremetal_port_group.py b/openstack/tests/functional/baremetal/test_baremetal_port_group.py index 3e5eeb0c8..b6001156c 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_port_group.py +++ b/openstack/tests/functional/baremetal/test_baremetal_port_group.py @@ -31,22 +31,27 @@ def test_port_group_create_get_delete(self): self.assertIsNotNone(loaded.node_id) with_fields = self.conn.baremetal.get_port_group( - port_group.id, fields=['uuid', 'extra']) + port_group.id, fields=['uuid', 'extra'] + ) self.assertEqual(port_group.id, with_fields.id) self.assertIsNone(with_fields.node_id) - self.conn.baremetal.delete_port_group(port_group, - ignore_missing=False) - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.get_port_group, port_group.id) + self.conn.baremetal.delete_port_group(port_group, ignore_missing=False) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.get_port_group, + port_group.id, + ) def test_port_list(self): node2 = self.create_node(name='test-node') - pg1 = self.create_port_group(address='11:22:33:44:55:66', - node_id=node2.id) - pg2 = self.create_port_group(address='11:22:33:44:55:77', - node_id=self.node.id) + pg1 = self.create_port_group( + address='11:22:33:44:55:66', node_id=node2.id + ) + pg2 = self.create_port_group( + address='11:22:33:44:55:77', node_id=self.node.id + ) pgs = self.conn.baremetal.port_groups(address='11:22:33:44:55:77') self.assertEqual([p.id for p in pgs], [pg2.id]) @@ -58,10 +63,14 @@ def test_port_list(self): self.assertEqual([p.id for p in pgs], [pg1.id]) def test_port_list_update_delete(self): - self.create_port_group(address='11:22:33:44:55:66', - extra={'foo': 'bar'}) - port_group = next(self.conn.baremetal.port_groups( - details=True, address='11:22:33:44:55:66')) + self.create_port_group( + address='11:22:33:44:55:66', extra={'foo': 'bar'} + ) + port_group = next( + self.conn.baremetal.port_groups( + details=True, address='11:22:33:44:55:66' + ) + ) self.assertEqual(port_group.extra, {'foo': 'bar'}) # This test checks that resources returned from listing are usable @@ -82,7 +91,8 @@ def test_port_group_patch(self): port_group = self.create_port_group() port_group = self.conn.baremetal.patch_port_group( - port_group, dict(path='/extra/answer', op='add', value=42)) + port_group, dict(path='/extra/answer', op='add', value=42) + ) self.assertEqual({'answer': 42}, port_group.extra) port_group = self.conn.baremetal.get_port_group(port_group.id) @@ -90,14 +100,23 @@ def test_port_group_patch(self): def test_port_group_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.get_port_group, uuid) - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.find_port_group, uuid, - ignore_missing=False) - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.delete_port_group, uuid, - ignore_missing=False) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.get_port_group, + uuid, + ) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.find_port_group, + uuid, + ignore_missing=False, + ) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.delete_port_group, + uuid, + ignore_missing=False, + ) self.assertIsNone(self.conn.baremetal.find_port_group(uuid)) self.assertIsNone(self.conn.baremetal.delete_port_group(uuid)) diff --git a/openstack/tests/functional/baremetal/test_baremetal_volume_connector.py b/openstack/tests/functional/baremetal/test_baremetal_volume_connector.py index 77ac90272..4459a0fc8 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_volume_connector.py +++ b/openstack/tests/functional/baremetal/test_baremetal_volume_connector.py @@ -25,47 +25,54 @@ def setUp(self): def test_volume_connector_create_get_delete(self): self.conn.baremetal.set_node_provision_state( - self.node, 'manage', wait=True) + self.node, 'manage', wait=True + ) self.conn.baremetal.set_node_power_state(self.node, 'power off') volume_connector = self.create_volume_connector( - connector_id='iqn.2017-07.org.openstack:01:d9a51732c3f', - type='iqn') + connector_id='iqn.2017-07.org.openstack:01:d9a51732c3f', type='iqn' + ) - loaded = self.conn.baremetal.get_volume_connector( - volume_connector.id) + loaded = self.conn.baremetal.get_volume_connector(volume_connector.id) self.assertEqual(loaded.id, volume_connector.id) self.assertIsNotNone(loaded.node_id) with_fields = self.conn.baremetal.get_volume_connector( - volume_connector.id, fields=['uuid', 'extra']) + volume_connector.id, fields=['uuid', 'extra'] + ) self.assertEqual(volume_connector.id, with_fields.id) self.assertIsNone(with_fields.node_id) - self.conn.baremetal.delete_volume_connector(volume_connector, - ignore_missing=False) - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.get_volume_connector, - volume_connector.id) + self.conn.baremetal.delete_volume_connector( + volume_connector, ignore_missing=False + ) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.get_volume_connector, + volume_connector.id, + ) def test_volume_connector_list(self): node2 = self.create_node(name='test-node') self.conn.baremetal.set_node_provision_state( - node2, 'manage', wait=True) + node2, 'manage', wait=True + ) self.conn.baremetal.set_node_power_state(node2, 'power off') self.conn.baremetal.set_node_provision_state( - self.node, 'manage', wait=True) + self.node, 'manage', wait=True + ) self.conn.baremetal.set_node_power_state(self.node, 'power off') vc1 = self.create_volume_connector( connector_id='iqn.2018-07.org.openstack:01:d9a514g2c32', node_id=node2.id, - type='iqn') + type='iqn', + ) vc2 = self.create_volume_connector( connector_id='iqn.2017-07.org.openstack:01:d9a51732c4g', node_id=self.node.id, - type='iqn') + type='iqn', + ) - vcs = self.conn.baremetal.volume_connectors( - node=self.node.id) + vcs = self.conn.baremetal.volume_connectors(node=self.node.id) self.assertEqual([v.id for v in vcs], [vc2.id]) vcs = self.conn.baremetal.volume_connectors(node=node2.id) @@ -76,86 +83,109 @@ def test_volume_connector_list(self): def test_volume_connector_list_update_delete(self): self.conn.baremetal.set_node_provision_state( - self.node, 'manage', wait=True) + self.node, 'manage', wait=True + ) self.conn.baremetal.set_node_power_state(self.node, 'power off') self.create_volume_connector( connector_id='iqn.2020-07.org.openstack:02:d9451472ce2', node_id=self.node.id, type='iqn', - extra={'foo': 'bar'}) - volume_connector = next(self.conn.baremetal.volume_connectors( - details=True, - node=self.node.id)) + extra={'foo': 'bar'}, + ) + volume_connector = next( + self.conn.baremetal.volume_connectors( + details=True, node=self.node.id + ) + ) self.assertEqual(volume_connector.extra, {'foo': 'bar'}) # This test checks that resources returned from listing are usable - self.conn.baremetal.update_volume_connector(volume_connector, - extra={'foo': 42}) - self.conn.baremetal.delete_volume_connector(volume_connector, - ignore_missing=False) + self.conn.baremetal.update_volume_connector( + volume_connector, extra={'foo': 42} + ) + self.conn.baremetal.delete_volume_connector( + volume_connector, ignore_missing=False + ) def test_volume_connector_update(self): self.conn.baremetal.set_node_provision_state( - self.node, 'manage', wait=True) + self.node, 'manage', wait=True + ) self.conn.baremetal.set_node_power_state(self.node, 'power off') volume_connector = self.create_volume_connector( connector_id='iqn.2019-07.org.openstack:03:de45b472c40', node_id=self.node.id, - type='iqn') + type='iqn', + ) volume_connector.extra = {'answer': 42} volume_connector = self.conn.baremetal.update_volume_connector( - volume_connector) + volume_connector + ) self.assertEqual({'answer': 42}, volume_connector.extra) volume_connector = self.conn.baremetal.get_volume_connector( - volume_connector.id) + volume_connector.id + ) self.assertEqual({'answer': 42}, volume_connector.extra) def test_volume_connector_patch(self): vol_conn_id = 'iqn.2020-07.org.openstack:04:de45b472c40' self.conn.baremetal.set_node_provision_state( - self.node, 'manage', wait=True) + self.node, 'manage', wait=True + ) self.conn.baremetal.set_node_power_state(self.node, 'power off') volume_connector = self.create_volume_connector( - connector_id=vol_conn_id, - node_id=self.node.id, - type='iqn') + connector_id=vol_conn_id, node_id=self.node.id, type='iqn' + ) volume_connector = self.conn.baremetal.patch_volume_connector( - volume_connector, dict(path='/extra/answer', op='add', value=42)) + volume_connector, dict(path='/extra/answer', op='add', value=42) + ) self.assertEqual({'answer': 42}, volume_connector.extra) - self.assertEqual(vol_conn_id, - volume_connector.connector_id) + self.assertEqual(vol_conn_id, volume_connector.connector_id) volume_connector = self.conn.baremetal.get_volume_connector( - volume_connector.id) + volume_connector.id + ) self.assertEqual({'answer': 42}, volume_connector.extra) def test_volume_connector_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.get_volume_connector, uuid) - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.find_volume_connector, uuid, - ignore_missing=False) - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.delete_volume_connector, uuid, - ignore_missing=False) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.get_volume_connector, + uuid, + ) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.find_volume_connector, + uuid, + ignore_missing=False, + ) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.delete_volume_connector, + uuid, + ignore_missing=False, + ) self.assertIsNone(self.conn.baremetal.find_volume_connector(uuid)) self.assertIsNone(self.conn.baremetal.delete_volume_connector(uuid)) def test_volume_connector_fields(self): self.create_node() self.conn.baremetal.set_node_provision_state( - self.node, 'manage', wait=True) + self.node, 'manage', wait=True + ) self.conn.baremetal.set_node_power_state(self.node, 'power off') self.create_volume_connector( connector_id='iqn.2018-08.org.openstack:04:de45f37c48', node_id=self.node.id, - type='iqn') + type='iqn', + ) result = self.conn.baremetal.volume_connectors( - fields=['uuid', 'node_id']) + fields=['uuid', 'node_id'] + ) for item in result: self.assertIsNotNone(item.id) self.assertIsNone(item.connector_id) diff --git a/openstack/tests/functional/baremetal/test_baremetal_volume_target.py b/openstack/tests/functional/baremetal/test_baremetal_volume_target.py index 7c65e2619..c4395435c 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_volume_target.py +++ b/openstack/tests/functional/baremetal/test_baremetal_volume_target.py @@ -25,50 +25,58 @@ def setUp(self): def test_volume_target_create_get_delete(self): self.conn.baremetal.set_node_provision_state( - self.node, 'manage', wait=True) + self.node, 'manage', wait=True + ) self.conn.baremetal.set_node_power_state(self.node, 'power off') volume_target = self.create_volume_target( boot_index=0, volume_id='04452bed-5367-4202-8bf5-de4335ac56d2', - volume_type='iscsi') + volume_type='iscsi', + ) - loaded = self.conn.baremetal.get_volume_target( - volume_target.id) + loaded = self.conn.baremetal.get_volume_target(volume_target.id) self.assertEqual(loaded.id, volume_target.id) self.assertIsNotNone(loaded.node_id) with_fields = self.conn.baremetal.get_volume_target( - volume_target.id, fields=['uuid', 'extra']) + volume_target.id, fields=['uuid', 'extra'] + ) self.assertEqual(volume_target.id, with_fields.id) self.assertIsNone(with_fields.node_id) - self.conn.baremetal.delete_volume_target(volume_target, - ignore_missing=False) - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.get_volume_target, - volume_target.id) + self.conn.baremetal.delete_volume_target( + volume_target, ignore_missing=False + ) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.get_volume_target, + volume_target.id, + ) def test_volume_target_list(self): node2 = self.create_node(name='test-node') self.conn.baremetal.set_node_provision_state( - node2, 'manage', wait=True) + node2, 'manage', wait=True + ) self.conn.baremetal.set_node_power_state(node2, 'power off') self.conn.baremetal.set_node_provision_state( - self.node, 'manage', wait=True) + self.node, 'manage', wait=True + ) self.conn.baremetal.set_node_power_state(self.node, 'power off') vt1 = self.create_volume_target( boot_index=0, volume_id='bd4d008c-7d31-463d-abf9-6c23d9d55f7f', node_id=node2.id, - volume_type='iscsi') + volume_type='iscsi', + ) vt2 = self.create_volume_target( boot_index=0, volume_id='04452bed-5367-4202-8bf5-de4335ac57c2', node_id=self.node.id, - volume_type='iscsi') + volume_type='iscsi', + ) - vts = self.conn.baremetal.volume_targets( - node=self.node.id) + vts = self.conn.baremetal.volume_targets(node=self.node.id) self.assertEqual([v.id for v in vts], [vt2.id]) vts = self.conn.baremetal.volume_targets(node=node2.id) @@ -83,7 +91,8 @@ def test_volume_target_list(self): self.assertIsNotNone(i.volume_type) vts_with_fields = self.conn.baremetal.volume_targets( - fields=['uuid', 'node_uuid']) + fields=['uuid', 'node_uuid'] + ) for i in vts_with_fields: self.assertIsNotNone(i.id) self.assertIsNone(i.volume_type) @@ -91,89 +100,104 @@ def test_volume_target_list(self): def test_volume_target_list_update_delete(self): self.conn.baremetal.set_node_provision_state( - self.node, 'manage', wait=True) + self.node, 'manage', wait=True + ) self.conn.baremetal.set_node_power_state(self.node, 'power off') self.create_volume_target( boot_index=0, volume_id='04452bed-5367-4202-8bf5-de4335ac57h3', node_id=self.node.id, volume_type='iscsi', - extra={'foo': 'bar'}) - volume_target = next(self.conn.baremetal.volume_targets( - details=True, - node=self.node.id)) + extra={'foo': 'bar'}, + ) + volume_target = next( + self.conn.baremetal.volume_targets(details=True, node=self.node.id) + ) self.assertEqual(volume_target.extra, {'foo': 'bar'}) # This test checks that resources returned from listing are usable - self.conn.baremetal.update_volume_target(volume_target, - extra={'foo': 42}) - self.conn.baremetal.delete_volume_target(volume_target, - ignore_missing=False) + self.conn.baremetal.update_volume_target( + volume_target, extra={'foo': 42} + ) + self.conn.baremetal.delete_volume_target( + volume_target, ignore_missing=False + ) def test_volume_target_update(self): self.conn.baremetal.set_node_provision_state( - self.node, 'manage', wait=True) + self.node, 'manage', wait=True + ) self.conn.baremetal.set_node_power_state(self.node, 'power off') volume_target = self.create_volume_target( boot_index=0, volume_id='04452bed-5367-4202-8bf5-de4335ac53h7', node_id=self.node.id, - volume_type='isci') + volume_type='isci', + ) volume_target.extra = {'answer': 42} - volume_target = self.conn.baremetal.update_volume_target( - volume_target) + volume_target = self.conn.baremetal.update_volume_target(volume_target) self.assertEqual({'answer': 42}, volume_target.extra) - volume_target = self.conn.baremetal.get_volume_target( - volume_target.id) + volume_target = self.conn.baremetal.get_volume_target(volume_target.id) self.assertEqual({'answer': 42}, volume_target.extra) def test_volume_target_patch(self): vol_targ_id = '04452bed-5367-4202-9cg6-de4335ac53h7' self.conn.baremetal.set_node_provision_state( - self.node, 'manage', wait=True) + self.node, 'manage', wait=True + ) self.conn.baremetal.set_node_power_state(self.node, 'power off') volume_target = self.create_volume_target( boot_index=0, volume_id=vol_targ_id, node_id=self.node.id, - volume_type='isci') + volume_type='isci', + ) volume_target = self.conn.baremetal.patch_volume_target( - volume_target, dict(path='/extra/answer', op='add', value=42)) + volume_target, dict(path='/extra/answer', op='add', value=42) + ) self.assertEqual({'answer': 42}, volume_target.extra) - self.assertEqual(vol_targ_id, - volume_target.volume_id) + self.assertEqual(vol_targ_id, volume_target.volume_id) - volume_target = self.conn.baremetal.get_volume_target( - volume_target.id) + volume_target = self.conn.baremetal.get_volume_target(volume_target.id) self.assertEqual({'answer': 42}, volume_target.extra) def test_volume_target_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.get_volume_target, uuid) - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.find_volume_target, uuid, - ignore_missing=False) - self.assertRaises(exceptions.ResourceNotFound, - self.conn.baremetal.delete_volume_target, uuid, - ignore_missing=False) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.get_volume_target, + uuid, + ) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.find_volume_target, + uuid, + ignore_missing=False, + ) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.delete_volume_target, + uuid, + ignore_missing=False, + ) self.assertIsNone(self.conn.baremetal.find_volume_target(uuid)) self.assertIsNone(self.conn.baremetal.delete_volume_target(uuid)) def test_volume_target_fields(self): self.create_node() self.conn.baremetal.set_node_provision_state( - self.node, 'manage', wait=True) + self.node, 'manage', wait=True + ) self.conn.baremetal.set_node_power_state(self.node, 'power off') self.create_volume_target( boot_index=0, volume_id='04452bed-5367-4202-8bf5-99ae634d8971', node_id=self.node.id, - volume_type='iscsi') - result = self.conn.baremetal.volume_targets( - fields=['uuid', 'node_id']) + volume_type='iscsi', + ) + result = self.conn.baremetal.volume_targets(fields=['uuid', 'node_id']) for item in result: self.assertIsNotNone(item.id) diff --git a/openstack/tests/unit/baremetal/test_configdrive.py b/openstack/tests/unit/baremetal/test_configdrive.py index 4f3b6ce20..6b694fa3f 100644 --- a/openstack/tests/unit/baremetal/test_configdrive.py +++ b/openstack/tests/unit/baremetal/test_configdrive.py @@ -23,24 +23,31 @@ class TestPopulateDirectory(testtools.TestCase): - def _check(self, metadata, user_data=None, network_data=None, - vendor_data=None): - with configdrive.populate_directory(metadata, - user_data=user_data, - network_data=network_data, - vendor_data=vendor_data) as d: + def _check( + self, metadata, user_data=None, network_data=None, vendor_data=None + ): + with configdrive.populate_directory( + metadata, + user_data=user_data, + network_data=network_data, + vendor_data=vendor_data, + ) as d: for version in ('2012-08-10', 'latest'): - with open(os.path.join(d, 'openstack', version, - 'meta_data.json')) as fp: + with open( + os.path.join(d, 'openstack', version, 'meta_data.json') + ) as fp: actual_metadata = json.load(fp) self.assertEqual(metadata, actual_metadata) - network_data_file = os.path.join(d, 'openstack', version, - 'network_data.json') - user_data_file = os.path.join(d, 'openstack', version, - 'user_data') - vendor_data_file = os.path.join(d, 'openstack', version, - 'vendor_data2.json') + network_data_file = os.path.join( + d, 'openstack', version, 'network_data.json' + ) + user_data_file = os.path.join( + d, 'openstack', version, 'user_data' + ) + vendor_data_file = os.path.join( + d, 'openstack', version, 'vendor_data2.json' + ) if network_data is None: self.assertFalse(os.path.exists(network_data_file)) @@ -83,17 +90,16 @@ def test_with_vendor_data(self): @mock.patch('subprocess.Popen', autospec=True) class TestPack(testtools.TestCase): - def test_no_genisoimage(self, mock_popen): mock_popen.side_effect = OSError - self.assertRaisesRegex(RuntimeError, "genisoimage", - configdrive.pack, "/fake") + self.assertRaisesRegex( + RuntimeError, "genisoimage", configdrive.pack, "/fake" + ) def test_genisoimage_fails(self, mock_popen): mock_popen.return_value.communicate.return_value = "", "BOOM" mock_popen.return_value.returncode = 1 - self.assertRaisesRegex(RuntimeError, "BOOM", - configdrive.pack, "/fake") + self.assertRaisesRegex(RuntimeError, "BOOM", configdrive.pack, "/fake") def test_success(self, mock_popen): mock_popen.return_value.communicate.return_value = "", "" diff --git a/openstack/tests/unit/baremetal/test_version.py b/openstack/tests/unit/baremetal/test_version.py index 520906143..b7c9e7338 100644 --- a/openstack/tests/unit/baremetal/test_version.py +++ b/openstack/tests/unit/baremetal/test_version.py @@ -24,7 +24,6 @@ class TestVersion(base.TestCase): - def test_basic(self): sot = version.Version() self.assertEqual('version', sot.resource_key) diff --git a/openstack/tests/unit/baremetal/v1/test_allocation.py b/openstack/tests/unit/baremetal/v1/test_allocation.py index 8eaaa8bd1..4c532d935 100644 --- a/openstack/tests/unit/baremetal/v1/test_allocation.py +++ b/openstack/tests/unit/baremetal/v1/test_allocation.py @@ -26,12 +26,12 @@ "links": [ { "href": "http://127.0.0.1:6385/v1/allocations/", - "rel": "self" + "rel": "self", }, { "href": "http://127.0.0.1:6385/allocations/", - "rel": "bookmark" - } + "rel": "bookmark", + }, ], "name": "test_allocation", "node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d", @@ -44,7 +44,6 @@ class TestAllocation(base.TestCase): - def test_basic(self): sot = allocation.Allocation() self.assertIsNone(sot.resource_key) @@ -75,7 +74,6 @@ def test_instantiate(self): @mock.patch('time.sleep', lambda _t: None) @mock.patch.object(allocation.Allocation, 'fetch', autospec=True) class TestWaitForAllocation(base.TestCase): - def setUp(self): super(TestWaitForAllocation, self).setUp() self.session = mock.Mock(spec=adapter.Adapter) @@ -116,8 +114,9 @@ def _side_effect(allocation, session): marker[0] = True mock_fetch.side_effect = _side_effect - self.assertRaises(exceptions.ResourceFailure, - self.allocation.wait, self.session) + self.assertRaises( + exceptions.ResourceFailure, self.allocation.wait, self.session + ) self.assertEqual(2, mock_fetch.call_count) def test_failure_ignored(self, mock_fetch): @@ -136,6 +135,10 @@ def _side_effect(allocation, session): self.assertEqual(2, mock_fetch.call_count) def test_timeout(self, mock_fetch): - self.assertRaises(exceptions.ResourceTimeout, - self.allocation.wait, self.session, timeout=0.001) + self.assertRaises( + exceptions.ResourceTimeout, + self.allocation.wait, + self.session, + timeout=0.001, + ) mock_fetch.assert_called_with(self.allocation, self.session) diff --git a/openstack/tests/unit/baremetal/v1/test_chassis.py b/openstack/tests/unit/baremetal/v1/test_chassis.py index df86ba7f7..f6fdbb603 100644 --- a/openstack/tests/unit/baremetal/v1/test_chassis.py +++ b/openstack/tests/unit/baremetal/v1/test_chassis.py @@ -19,32 +19,19 @@ "description": "Sample chassis", "extra": {}, "links": [ - { - "href": "http://127.0.0.1:6385/v1/chassis/ID", - "rel": "self" - }, - { - "href": "http://127.0.0.1:6385/chassis/ID", - "rel": "bookmark" - } + {"href": "http://127.0.0.1:6385/v1/chassis/ID", "rel": "self"}, + {"href": "http://127.0.0.1:6385/chassis/ID", "rel": "bookmark"}, ], "nodes": [ - { - "href": "http://127.0.0.1:6385/v1/chassis/ID/nodes", - "rel": "self" - }, - { - "href": "http://127.0.0.1:6385/chassis/ID/nodes", - "rel": "bookmark" - } + {"href": "http://127.0.0.1:6385/v1/chassis/ID/nodes", "rel": "self"}, + {"href": "http://127.0.0.1:6385/chassis/ID/nodes", "rel": "bookmark"}, ], "updated_at": None, - "uuid": "dff29d23-1ded-43b4-8ae1-5eebb3e30de1" + "uuid": "dff29d23-1ded-43b4-8ae1-5eebb3e30de1", } class TestChassis(base.TestCase): - def test_basic(self): sot = chassis.Chassis() self.assertIsNone(sot.resource_key) diff --git a/openstack/tests/unit/baremetal/v1/test_conductor.py b/openstack/tests/unit/baremetal/v1/test_conductor.py index 58cce1824..fdc1f6869 100644 --- a/openstack/tests/unit/baremetal/v1/test_conductor.py +++ b/openstack/tests/unit/baremetal/v1/test_conductor.py @@ -18,26 +18,23 @@ "links": [ { "href": "http://127.0.0.1:6385/v1/conductors/compute2.localdomain", - "rel": "self" + "rel": "self", }, { "href": "http://127.0.0.1:6385/conductors/compute2.localdomain", - "rel": "bookmark" - } + "rel": "bookmark", + }, ], "created_at": "2018-12-05T07:03:19+00:00", "hostname": "compute2.localdomain", "conductor_group": "", "updated_at": "2018-12-05T07:03:21+00:00", "alive": True, - "drivers": [ - "ipmi" - ] + "drivers": ["ipmi"], } class TestContainer(base.TestCase): - def test_basic(self): sot = conductor.Conductor() self.assertIsNone(sot.resource_key) diff --git a/openstack/tests/unit/baremetal/v1/test_deploy_templates.py b/openstack/tests/unit/baremetal/v1/test_deploy_templates.py index a7f5af632..6b622d4ff 100644 --- a/openstack/tests/unit/baremetal/v1/test_deploy_templates.py +++ b/openstack/tests/unit/baremetal/v1/test_deploy_templates.py @@ -18,40 +18,34 @@ "created_at": "2016-08-18T22:28:48.643434+11:11", "extra": {}, "links": [ - { - "href": """http://10.60.253.180:6385/v1/deploy_templates + { + "href": """http://10.60.253.180:6385/v1/deploy_templates /bbb45f41-d4bc-4307-8d1d-32f95ce1e920""", - "rel": "self" - }, + "rel": "self", + }, { - "href": """http://10.60.253.180:6385/deploy_templates + "href": """http://10.60.253.180:6385/deploy_templates /bbb45f41-d4bc-4307-8d1d-32f95ce1e920""", - "rel": "bookmark" - } + "rel": "bookmark", + }, ], "name": "CUSTOM_HYPERTHREADING_ON", "steps": [ - { - "args": { - "settings": [ - { - "name": "LogicalProc", - "value": "Enabled" - } - ] - }, - "interface": "bios", - "priority": 150, - "step": "apply_configuration" - } + { + "args": { + "settings": [{"name": "LogicalProc", "value": "Enabled"}] + }, + "interface": "bios", + "priority": 150, + "step": "apply_configuration", + } ], "updated_at": None, - "uuid": "bbb45f41-d4bc-4307-8d1d-32f95ce1e920" + "uuid": "bbb45f41-d4bc-4307-8d1d-32f95ce1e920", } class DeployTemplates(base.TestCase): - def test_basic(self): sot = deploy_templates.DeployTemplate() self.assertIsNone(sot.resource_key) diff --git a/openstack/tests/unit/baremetal/v1/test_driver.py b/openstack/tests/unit/baremetal/v1/test_driver.py index e4d1f6731..c009fd621 100644 --- a/openstack/tests/unit/baremetal/v1/test_driver.py +++ b/openstack/tests/unit/baremetal/v1/test_driver.py @@ -21,36 +21,32 @@ FAKE = { - "hosts": [ - "897ab1dad809" - ], + "hosts": ["897ab1dad809"], "links": [ { "href": "http://127.0.0.1:6385/v1/drivers/agent_ipmitool", - "rel": "self" + "rel": "self", }, { "href": "http://127.0.0.1:6385/drivers/agent_ipmitool", - "rel": "bookmark" - } + "rel": "bookmark", + }, ], "name": "agent_ipmitool", "properties": [ { - "href": - "http://127.0.0.1:6385/v1/drivers/agent_ipmitool/properties", - "rel": "self" + "href": "http://127.0.0.1:6385/v1/drivers/agent_ipmitool/properties", # noqa: E501 + "rel": "self", }, { "href": "http://127.0.0.1:6385/drivers/agent_ipmitool/properties", - "rel": "bookmark" - } - ] + "rel": "bookmark", + }, + ], } class TestDriver(base.TestCase): - def test_basic(self): sot = driver.Driver() self.assertIsNone(sot.resource_key) @@ -79,16 +75,19 @@ def test_list_vendor_passthru(self): 'async': True, 'attach': False, 'description': "Fake function that does nothing in background", - 'http_methods': ['GET', 'PUT', 'POST', 'DELETE'] + 'http_methods': ['GET', 'PUT', 'POST', 'DELETE'], } } self.session.get.return_value.json.return_value = ( - fake_vendor_passthru_info) + fake_vendor_passthru_info + ) result = sot.list_vendor_passthru(self.session) self.session.get.assert_called_once_with( 'drivers/{driver_name}/vendor_passthru/methods'.format( - driver_name=FAKE["name"]), - headers=mock.ANY) + driver_name=FAKE["name"] + ), + headers=mock.ANY, + ) self.assertEqual(result, fake_vendor_passthru_info) @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) @@ -99,33 +98,49 @@ def test_call_vendor_passthru(self): sot.call_vendor_passthru(self.session, 'GET', 'fake_vendor_method') self.session.get.assert_called_once_with( 'drivers/{}/vendor_passthru?method={}'.format( - FAKE["name"], 'fake_vendor_method'), + FAKE["name"], 'fake_vendor_method' + ), json=None, headers=mock.ANY, - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) # PUT - sot.call_vendor_passthru(self.session, 'PUT', 'fake_vendor_method', - body={"fake_param_key": "fake_param_value"}) + sot.call_vendor_passthru( + self.session, + 'PUT', + 'fake_vendor_method', + body={"fake_param_key": "fake_param_value"}, + ) self.session.put.assert_called_once_with( 'drivers/{}/vendor_passthru?method={}'.format( - FAKE["name"], 'fake_vendor_method'), + FAKE["name"], 'fake_vendor_method' + ), json={"fake_param_key": "fake_param_value"}, headers=mock.ANY, - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) # POST - sot.call_vendor_passthru(self.session, 'POST', 'fake_vendor_method', - body={"fake_param_key": "fake_param_value"}) + sot.call_vendor_passthru( + self.session, + 'POST', + 'fake_vendor_method', + body={"fake_param_key": "fake_param_value"}, + ) self.session.post.assert_called_once_with( 'drivers/{}/vendor_passthru?method={}'.format( - FAKE["name"], 'fake_vendor_method'), + FAKE["name"], 'fake_vendor_method' + ), json={"fake_param_key": "fake_param_value"}, headers=mock.ANY, - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) # DELETE sot.call_vendor_passthru(self.session, 'DELETE', 'fake_vendor_method') self.session.delete.assert_called_once_with( 'drivers/{}/vendor_passthru?method={}'.format( - FAKE["name"], 'fake_vendor_method'), + FAKE["name"], 'fake_vendor_method' + ), json=None, headers=mock.ANY, - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index dbed1662d..a3d0ecafd 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -29,10 +29,7 @@ "console_enabled": False, "created_at": "2016-08-18T22:28:48.643434+00:00", "driver": "agent_ipmitool", - "driver_info": { - "ipmi_password": "******", - "ipmi_username": "ADMIN" - }, + "driver_info": {"ipmi_password": "******", "ipmi_username": "ADMIN"}, "driver_internal_info": {}, "extra": {}, "inspection_finished_at": None, @@ -41,14 +38,8 @@ "instance_uuid": None, "last_error": None, "links": [ - { - "href": "http://127.0.0.1:6385/v1/nodes/", - "rel": "self" - }, - { - "href": "http://127.0.0.1:6385/nodes/", - "rel": "bookmark" - } + {"href": "http://127.0.0.1:6385/v1/nodes/", "rel": "self"}, + {"href": "http://127.0.0.1:6385/nodes/", "rel": "bookmark"}, ], "maintenance": False, "maintenance_reason": None, @@ -58,22 +49,22 @@ "portgroups": [ { "href": "http://127.0.0.1:6385/v1/nodes//portgroups", - "rel": "self" + "rel": "self", }, { "href": "http://127.0.0.1:6385/nodes//portgroups", - "rel": "bookmark" - } + "rel": "bookmark", + }, ], "ports": [ { "href": "http://127.0.0.1:6385/v1/nodes//ports", - "rel": "self" + "rel": "self", }, { "href": "http://127.0.0.1:6385/nodes//ports", - "rel": "bookmark" - } + "rel": "bookmark", + }, ], "power_state": None, "properties": {}, @@ -87,23 +78,22 @@ "states": [ { "href": "http://127.0.0.1:6385/v1/nodes//states", - "rel": "self" + "rel": "self", }, { "href": "http://127.0.0.1:6385/nodes//states", - "rel": "bookmark" - } + "rel": "bookmark", + }, ], "target_power_state": None, "target_provision_state": None, "target_raid_config": {}, "updated_at": None, - "uuid": "6d85703a-565d-469a-96ce-30b6de53079d" + "uuid": "6d85703a-565d-469a-96ce-30b6de53079d", } class TestNode(base.TestCase): - def test_basic(self): sot = node.Node() self.assertIsNone(sot.resource_key) @@ -128,8 +118,9 @@ def test_instantiate(self): self.assertEqual(FAKE['created_at'], sot.created_at) self.assertEqual(FAKE['driver'], sot.driver) self.assertEqual(FAKE['driver_info'], sot.driver_info) - self.assertEqual(FAKE['driver_internal_info'], - sot.driver_internal_info) + self.assertEqual( + FAKE['driver_internal_info'], sot.driver_internal_info + ) self.assertEqual(FAKE['extra'], sot.extra) self.assertEqual(FAKE['instance_info'], sot.instance_info) self.assertEqual(FAKE['instance_uuid'], sot.instance_id) @@ -151,8 +142,9 @@ def test_instantiate(self): self.assertEqual(FAKE['resource_class'], sot.resource_class) self.assertEqual(FAKE['secure_boot'], sot.is_secure_boot) self.assertEqual(FAKE['states'], sot.states) - self.assertEqual(FAKE['target_provision_state'], - sot.target_provision_state) + self.assertEqual( + FAKE['target_provision_state'], sot.target_provision_state + ) self.assertEqual(FAKE['target_power_state'], sot.target_power_state) self.assertEqual(FAKE['target_raid_config'], sot.target_raid_config) self.assertEqual(FAKE['updated_at'], sot.updated_at) @@ -188,10 +180,13 @@ def _get_side_effect(_self, session): mock_fetch.side_effect = _get_side_effect - self.assertRaisesRegex(exceptions.ResourceFailure, - 'failure state "deploy failed"', - self.node.wait_for_provision_state, - self.session, 'manageable') + self.assertRaisesRegex( + exceptions.ResourceFailure, + 'failure state "deploy failed"', + self.node.wait_for_provision_state, + self.session, + 'manageable', + ) def test_failure_error(self, mock_fetch): def _get_side_effect(_self, session): @@ -200,10 +195,13 @@ def _get_side_effect(_self, session): mock_fetch.side_effect = _get_side_effect - self.assertRaisesRegex(exceptions.ResourceFailure, - 'failure state "error"', - self.node.wait_for_provision_state, - self.session, 'manageable') + self.assertRaisesRegex( + exceptions.ResourceFailure, + 'failure state "error"', + self.node.wait_for_provision_state, + self.session, + 'manageable', + ) def test_enroll_as_failure(self, mock_fetch): def _get_side_effect(_self, session): @@ -213,15 +211,22 @@ def _get_side_effect(_self, session): mock_fetch.side_effect = _get_side_effect - self.assertRaisesRegex(exceptions.ResourceFailure, - 'failed to verify management credentials', - self.node.wait_for_provision_state, - self.session, 'manageable') + self.assertRaisesRegex( + exceptions.ResourceFailure, + 'failed to verify management credentials', + self.node.wait_for_provision_state, + self.session, + 'manageable', + ) def test_timeout(self, mock_fetch): - self.assertRaises(exceptions.ResourceTimeout, - self.node.wait_for_provision_state, - self.session, 'manageable', timeout=0.001) + self.assertRaises( + exceptions.ResourceTimeout, + self.node.wait_for_provision_state, + self.session, + 'manageable', + timeout=0.001, + ) def test_not_abort_on_failed_state(self, mock_fetch): def _get_side_effect(_self, session): @@ -230,10 +235,14 @@ def _get_side_effect(_self, session): mock_fetch.side_effect = _get_side_effect - self.assertRaises(exceptions.ResourceTimeout, - self.node.wait_for_provision_state, - self.session, 'manageable', timeout=0.001, - abort_on_failed_state=False) + self.assertRaises( + exceptions.ResourceTimeout, + self.node.wait_for_provision_state, + self.session, + 'manageable', + timeout=0.001, + abort_on_failed_state=False, + ) def _fake_assert(self, session, action, expected, error_message=None): @@ -244,12 +253,12 @@ def _fake_assert(self, session, action, expected, error_message=None): @mock.patch.object(node.Node, 'fetch', lambda self, session: self) @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) class TestNodeSetProvisionState(base.TestCase): - def setUp(self): super(TestNodeSetProvisionState, self).setUp() self.node = node.Node(**FAKE) - self.session = mock.Mock(spec=adapter.Adapter, - default_microversion=None) + self.session = mock.Mock( + spec=adapter.Adapter, default_microversion=None + ) def test_no_arguments(self): result = self.node.set_provision_state(self.session, 'active') @@ -257,8 +266,10 @@ def test_no_arguments(self): self.session.put.assert_called_once_with( 'nodes/%s/states/provision' % self.node.id, json={'target': 'active'}, - headers=mock.ANY, microversion=None, - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + headers=mock.ANY, + microversion=None, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) def test_manage(self): result = self.node.set_provision_state(self.session, 'manage') @@ -266,67 +277,80 @@ def test_manage(self): self.session.put.assert_called_once_with( 'nodes/%s/states/provision' % self.node.id, json={'target': 'manage'}, - headers=mock.ANY, microversion='1.4', - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + headers=mock.ANY, + microversion='1.4', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) def test_deploy_with_configdrive(self): - result = self.node.set_provision_state(self.session, 'active', - config_drive='abcd') + result = self.node.set_provision_state( + self.session, 'active', config_drive='abcd' + ) self.assertIs(result, self.node) self.session.put.assert_called_once_with( 'nodes/%s/states/provision' % self.node.id, json={'target': 'active', 'configdrive': 'abcd'}, - headers=mock.ANY, microversion=None, - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + headers=mock.ANY, + microversion=None, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) def test_rebuild_with_configdrive(self): - result = self.node.set_provision_state(self.session, 'rebuild', - config_drive='abcd') + result = self.node.set_provision_state( + self.session, 'rebuild', config_drive='abcd' + ) self.assertIs(result, self.node) self.session.put.assert_called_once_with( 'nodes/%s/states/provision' % self.node.id, json={'target': 'rebuild', 'configdrive': 'abcd'}, - headers=mock.ANY, microversion='1.35', - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + headers=mock.ANY, + microversion='1.35', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) def test_configdrive_as_dict(self): for target in ('rebuild', 'active'): self.session.put.reset_mock() result = self.node.set_provision_state( - self.session, target, config_drive={'user_data': 'abcd'}) + self.session, target, config_drive={'user_data': 'abcd'} + ) self.assertIs(result, self.node) self.session.put.assert_called_once_with( 'nodes/%s/states/provision' % self.node.id, json={'target': target, 'configdrive': {'user_data': 'abcd'}}, - headers=mock.ANY, microversion='1.56', - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + headers=mock.ANY, + microversion='1.56', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) def test_deploy_with_deploy_steps(self): deploy_steps = [{'interface': 'deploy', 'step': 'upgrade_fw'}] result = self.node.set_provision_state( - self.session, 'active', - deploy_steps=deploy_steps) + self.session, 'active', deploy_steps=deploy_steps + ) self.assertIs(result, self.node) self.session.put.assert_called_once_with( 'nodes/%s/states/provision' % self.node.id, json={'target': 'active', 'deploy_steps': deploy_steps}, - headers=mock.ANY, microversion='1.69', - retriable_status_codes=_common.RETRIABLE_STATUS_CODES + headers=mock.ANY, + microversion='1.69', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, ) def test_rebuild_with_deploy_steps(self): deploy_steps = [{'interface': 'deploy', 'step': 'upgrade_fw'}] result = self.node.set_provision_state( - self.session, 'rebuild', - deploy_steps=deploy_steps) + self.session, 'rebuild', deploy_steps=deploy_steps + ) self.assertIs(result, self.node) self.session.put.assert_called_once_with( 'nodes/%s/states/provision' % self.node.id, json={'target': 'rebuild', 'deploy_steps': deploy_steps}, - headers=mock.ANY, microversion='1.69', - retriable_status_codes=_common.RETRIABLE_STATUS_CODES + headers=mock.ANY, + microversion='1.69', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, ) @@ -334,7 +358,6 @@ def test_rebuild_with_deploy_steps(self): @mock.patch.object(node.Node, '_get_session', lambda self, x: x) @mock.patch.object(node.Node, 'set_provision_state', autospec=True) class TestNodeCreate(base.TestCase): - def setUp(self): super(TestNodeCreate, self).setUp() self.new_state = None @@ -352,9 +375,12 @@ def test_available_old_version(self, mock_prov): result = self.node.create(self.session) self.assertIs(result, self.node) self.session.post.assert_called_once_with( - mock.ANY, json={'driver': FAKE['driver']}, - headers=mock.ANY, microversion=self.session.default_microversion, - params={}) + mock.ANY, + json={'driver': FAKE['driver']}, + headers=mock.ANY, + microversion=self.session.default_microversion, + params={}, + ) self.assertFalse(mock_prov.called) def test_available_new_version(self, mock_prov): @@ -364,15 +390,19 @@ def test_available_new_version(self, mock_prov): result = self.node.create(self.session) self.assertIs(result, self.node) self.session.post.assert_called_once_with( - mock.ANY, json={'driver': FAKE['driver']}, - headers=mock.ANY, microversion='1.10', - params={}) + mock.ANY, + json={'driver': FAKE['driver']}, + headers=mock.ANY, + microversion='1.10', + params={}, + ) mock_prov.assert_not_called() def test_no_enroll_in_old_version(self, mock_prov): self.node.provision_state = 'enroll' - self.assertRaises(exceptions.NotSupported, - self.node.create, self.session) + self.assertRaises( + exceptions.NotSupported, self.node.create, self.session + ) self.assertFalse(self.session.post.called) self.assertFalse(mock_prov.called) @@ -384,15 +414,19 @@ def test_enroll_new_version(self, mock_prov): result = self.node.create(self.session) self.assertIs(result, self.node) self.session.post.assert_called_once_with( - mock.ANY, json={'driver': FAKE['driver']}, - headers=mock.ANY, microversion=self.session.default_microversion, - params={}) + mock.ANY, + json={'driver': FAKE['driver']}, + headers=mock.ANY, + microversion=self.session.default_microversion, + params={}, + ) self.assertFalse(mock_prov.called) def test_no_manageable_in_old_version(self, mock_prov): self.node.provision_state = 'manageable' - self.assertRaises(exceptions.NotSupported, - self.node.create, self.session) + self.assertRaises( + exceptions.NotSupported, self.node.create, self.session + ) self.assertFalse(self.session.post.called) self.assertFalse(mock_prov.called) @@ -404,11 +438,15 @@ def test_manageable_old_version(self, mock_prov): result = self.node.create(self.session) self.assertIs(result, self.node) self.session.post.assert_called_once_with( - mock.ANY, json={'driver': FAKE['driver']}, - headers=mock.ANY, microversion=self.session.default_microversion, - params={}) - mock_prov.assert_called_once_with(self.node, self.session, 'manage', - wait=True) + mock.ANY, + json={'driver': FAKE['driver']}, + headers=mock.ANY, + microversion=self.session.default_microversion, + params={}, + ) + mock_prov.assert_called_once_with( + self.node, self.session, 'manage', wait=True + ) def test_manageable_new_version(self, mock_prov): self.session.default_microversion = '1.11' @@ -418,55 +456,72 @@ def test_manageable_new_version(self, mock_prov): result = self.node.create(self.session) self.assertIs(result, self.node) self.session.post.assert_called_once_with( - mock.ANY, json={'driver': FAKE['driver']}, - headers=mock.ANY, microversion=self.session.default_microversion, - params={}) - mock_prov.assert_called_once_with(self.node, self.session, 'manage', - wait=True) + mock.ANY, + json={'driver': FAKE['driver']}, + headers=mock.ANY, + microversion=self.session.default_microversion, + params={}, + ) + mock_prov.assert_called_once_with( + self.node, self.session, 'manage', wait=True + ) @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) @mock.patch.object(node.Node, '_get_session', lambda self, x: x) class TestNodeVif(base.TestCase): - def setUp(self): super(TestNodeVif, self).setUp() self.session = mock.Mock(spec=adapter.Adapter) self.session.default_microversion = '1.28' self.session.log = mock.Mock() - self.node = node.Node(id='c29db401-b6a7-4530-af8e-20a720dee946', - driver=FAKE['driver']) + self.node = node.Node( + id='c29db401-b6a7-4530-af8e-20a720dee946', driver=FAKE['driver'] + ) self.vif_id = '714bdf6d-2386-4b5e-bd0d-bc036f04b1ef' def test_attach_vif(self): self.assertIsNone(self.node.attach_vif(self.session, self.vif_id)) self.session.post.assert_called_once_with( - 'nodes/%s/vifs' % self.node.id, json={'id': self.vif_id}, - headers=mock.ANY, microversion='1.28', - retriable_status_codes=[409, 503]) + 'nodes/%s/vifs' % self.node.id, + json={'id': self.vif_id}, + headers=mock.ANY, + microversion='1.28', + retriable_status_codes=[409, 503], + ) def test_attach_vif_no_retries(self): - self.assertIsNone(self.node.attach_vif(self.session, self.vif_id, - retry_on_conflict=False)) + self.assertIsNone( + self.node.attach_vif( + self.session, self.vif_id, retry_on_conflict=False + ) + ) self.session.post.assert_called_once_with( - 'nodes/%s/vifs' % self.node.id, json={'id': self.vif_id}, - headers=mock.ANY, microversion='1.28', - retriable_status_codes={503}) + 'nodes/%s/vifs' % self.node.id, + json={'id': self.vif_id}, + headers=mock.ANY, + microversion='1.28', + retriable_status_codes={503}, + ) def test_detach_vif_existing(self): self.assertTrue(self.node.detach_vif(self.session, self.vif_id)) self.session.delete.assert_called_once_with( 'nodes/%s/vifs/%s' % (self.node.id, self.vif_id), - headers=mock.ANY, microversion='1.28', - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + headers=mock.ANY, + microversion='1.28', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) def test_detach_vif_missing(self): self.session.delete.return_value.status_code = 400 self.assertFalse(self.node.detach_vif(self.session, self.vif_id)) self.session.delete.assert_called_once_with( 'nodes/%s/vifs/%s' % (self.node.id, self.vif_id), - headers=mock.ANY, microversion='1.28', - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + headers=mock.ANY, + microversion='1.28', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) def test_list_vifs(self): self.session.get.return_value.json.return_value = { @@ -479,25 +534,32 @@ def test_list_vifs(self): self.assertEqual(['1234', '5678'], res) self.session.get.assert_called_once_with( 'nodes/%s/vifs' % self.node.id, - headers=mock.ANY, microversion='1.28') + headers=mock.ANY, + microversion='1.28', + ) def test_incompatible_microversion(self): self.session.default_microversion = '1.1' - self.assertRaises(exceptions.NotSupported, - self.node.attach_vif, - self.session, self.vif_id) - self.assertRaises(exceptions.NotSupported, - self.node.detach_vif, - self.session, self.vif_id) - self.assertRaises(exceptions.NotSupported, - self.node.list_vifs, - self.session) + self.assertRaises( + exceptions.NotSupported, + self.node.attach_vif, + self.session, + self.vif_id, + ) + self.assertRaises( + exceptions.NotSupported, + self.node.detach_vif, + self.session, + self.vif_id, + ) + self.assertRaises( + exceptions.NotSupported, self.node.list_vifs, self.session + ) @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) @mock.patch.object(node.Node, '_get_session', lambda self, x: x) class TestNodeValidate(base.TestCase): - def setUp(self): super(TestNodeValidate, self).setUp() self.session = mock.Mock(spec=adapter.Adapter) @@ -510,7 +572,7 @@ def test_validate_ok(self): 'console': {'result': False, 'reason': 'Not configured'}, 'deploy': {'result': True}, 'inspect': {'result': None, 'reason': 'Not supported'}, - 'power': {'result': True} + 'power': {'result': True}, } result = self.node.validate(self.session) for iface in ('boot', 'deploy', 'power'): @@ -526,11 +588,14 @@ def test_validate_failed(self): 'console': {'result': False, 'reason': 'Not configured'}, 'deploy': {'result': False, 'reason': 'No deploy for you'}, 'inspect': {'result': None, 'reason': 'Not supported'}, - 'power': {'result': True} + 'power': {'result': True}, } - self.assertRaisesRegex(exceptions.ValidationException, - 'No deploy for you', - self.node.validate, self.session) + self.assertRaisesRegex( + exceptions.ValidationException, + 'No deploy for you', + self.node.validate, + self.session, + ) def test_validate_no_failure(self): self.session.get.return_value.json.return_value = { @@ -538,7 +603,7 @@ def test_validate_no_failure(self): 'console': {'result': False, 'reason': 'Not configured'}, 'deploy': {'result': False, 'reason': 'No deploy for you'}, 'inspect': {'result': None, 'reason': 'Not supported'}, - 'power': {'result': True} + 'power': {'result': True}, } result = self.node.validate(self.session, required=None) self.assertTrue(result['power'].result) @@ -554,7 +619,6 @@ def test_validate_no_failure(self): @mock.patch('time.sleep', lambda _t: None) @mock.patch.object(node.Node, 'fetch', autospec=True) class TestNodeWaitForReservation(base.TestCase): - def setUp(self): super(TestNodeWaitForReservation, self).setUp() self.session = mock.Mock(spec=adapter.Adapter) @@ -585,15 +649,17 @@ def _side_effect(node, session): def test_timeout(self, mock_fetch): self.node.reservation = 'example.com' - self.assertRaises(exceptions.ResourceTimeout, - self.node.wait_for_reservation, - self.session, timeout=0.001) + self.assertRaises( + exceptions.ResourceTimeout, + self.node.wait_for_reservation, + self.session, + timeout=0.001, + ) mock_fetch.assert_called_with(self.node, self.session) @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) class TestNodeInjectNMI(base.TestCase): - def setUp(self): super().setUp() self.node = node.Node(**FAKE) @@ -623,12 +689,12 @@ def test_incompatible_microversion(self): @mock.patch.object(node.Node, '_assert_microversion_for', _fake_assert) @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) class TestNodeSetPowerState(base.TestCase): - def setUp(self): super(TestNodeSetPowerState, self).setUp() self.node = node.Node(**FAKE) - self.session = mock.Mock(spec=adapter.Adapter, - default_microversion=None) + self.session = mock.Mock( + spec=adapter.Adapter, default_microversion=None + ) def test_power_on(self): self.node.set_power_state(self.session, 'power on') @@ -637,7 +703,8 @@ def test_power_on(self): json={'target': 'power on'}, headers=mock.ANY, microversion=None, - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) def test_soft_power_on(self): self.node.set_power_state(self.session, 'soft power off') @@ -646,20 +713,22 @@ def test_soft_power_on(self): json={'target': 'soft power off'}, headers=mock.ANY, microversion='1.27', - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) @mock.patch.object(node.Node, '_translate_response', mock.Mock()) @mock.patch.object(node.Node, '_get_session', lambda self, x: x) class TestNodeMaintenance(base.TestCase): - def setUp(self): super(TestNodeMaintenance, self).setUp() self.node = node.Node.existing(**FAKE) - self.session = mock.Mock(spec=adapter.Adapter, - default_microversion='1.1', - retriable_status_codes=None) + self.session = mock.Mock( + spec=adapter.Adapter, + default_microversion='1.1', + retriable_status_codes=None, + ) def test_set(self): self.node.set_maintenance(self.session) @@ -667,7 +736,8 @@ def test_set(self): 'nodes/%s/maintenance' % self.node.id, json={'reason': None}, headers=mock.ANY, - microversion=mock.ANY) + microversion=mock.ANY, + ) def test_set_with_reason(self): self.node.set_maintenance(self.session, 'No work on Monday') @@ -675,7 +745,8 @@ def test_set_with_reason(self): 'nodes/%s/maintenance' % self.node.id, json={'reason': 'No work on Monday'}, headers=mock.ANY, - microversion=mock.ANY) + microversion=mock.ANY, + ) def test_unset(self): self.node.unset_maintenance(self.session) @@ -683,7 +754,8 @@ def test_unset(self): 'nodes/%s/maintenance' % self.node.id, json=None, headers=mock.ANY, - microversion=mock.ANY) + microversion=mock.ANY, + ) def test_set_via_update(self): self.node.is_maintenance = True @@ -692,7 +764,8 @@ def test_set_via_update(self): 'nodes/%s/maintenance' % self.node.id, json={'reason': None}, headers=mock.ANY, - microversion=mock.ANY) + microversion=mock.ANY, + ) self.assertFalse(self.session.patch.called) @@ -704,7 +777,8 @@ def test_set_with_reason_via_update(self): 'nodes/%s/maintenance' % self.node.id, json={'reason': 'No work on Monday'}, headers=mock.ANY, - microversion=mock.ANY) + microversion=mock.ANY, + ) self.assertFalse(self.session.patch.called) def test_set_with_other_fields(self): @@ -715,13 +789,15 @@ def test_set_with_other_fields(self): 'nodes/%s/maintenance' % self.node.id, json={'reason': None}, headers=mock.ANY, - microversion=mock.ANY) + microversion=mock.ANY, + ) self.session.patch.assert_called_once_with( 'nodes/%s' % self.node.id, json=[{'path': '/name', 'op': 'replace', 'value': 'lazy-3000'}], headers=mock.ANY, - microversion=mock.ANY) + microversion=mock.ANY, + ) def test_set_with_reason_and_other_fields(self): self.node.is_maintenance = True @@ -732,13 +808,15 @@ def test_set_with_reason_and_other_fields(self): 'nodes/%s/maintenance' % self.node.id, json={'reason': 'No work on Monday'}, headers=mock.ANY, - microversion=mock.ANY) + microversion=mock.ANY, + ) self.session.patch.assert_called_once_with( 'nodes/%s' % self.node.id, json=[{'path': '/name', 'op': 'replace', 'value': 'lazy-3000'}], headers=mock.ANY, - microversion=mock.ANY) + microversion=mock.ANY, + ) def test_no_reason_without_maintenance(self): self.node.maintenance_reason = 'Can I?' @@ -755,7 +833,8 @@ def test_set_unset_maintenance(self): 'nodes/%s/maintenance' % self.node.id, json={'reason': 'No work on Monday'}, headers=mock.ANY, - microversion=mock.ANY) + microversion=mock.ANY, + ) self.node.is_maintenance = False self.node.commit(self.session) @@ -765,18 +844,19 @@ def test_set_unset_maintenance(self): 'nodes/%s/maintenance' % self.node.id, json=None, headers=mock.ANY, - microversion=mock.ANY) + microversion=mock.ANY, + ) @mock.patch.object(node.Node, 'fetch', lambda self, session: self) @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) class TestNodeBootDevice(base.TestCase): - def setUp(self): super().setUp() self.node = node.Node(**FAKE) - self.session = mock.Mock(spec=adapter.Adapter, - default_microversion='1.1') + self.session = mock.Mock( + spec=adapter.Adapter, default_microversion='1.1' + ) def test_get_boot_device(self): self.node.get_boot_device(self.session) @@ -792,8 +872,10 @@ def test_set_boot_device(self): self.session.put.assert_called_once_with( 'nodes/%s/management/boot_device' % self.node.id, json={'boot_device': 'pxe', 'persistent': False}, - headers=mock.ANY, microversion=mock.ANY, - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + headers=mock.ANY, + microversion=mock.ANY, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) def test_get_supported_boot_devices(self): self.node.get_supported_boot_devices(self.session) @@ -809,60 +891,66 @@ def test_get_supported_boot_devices(self): @mock.patch.object(node.Node, 'fetch', lambda self, session: self) @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) class TestNodeSetBootMode(base.TestCase): - def setUp(self): super(TestNodeSetBootMode, self).setUp() self.node = node.Node(**FAKE) - self.session = mock.Mock(spec=adapter.Adapter, - default_microversion='1.1') + self.session = mock.Mock( + spec=adapter.Adapter, default_microversion='1.1' + ) def test_node_set_boot_mode(self): self.node.set_boot_mode(self.session, 'uefi') self.session.put.assert_called_once_with( 'nodes/%s/states/boot_mode' % self.node.id, json={'target': 'uefi'}, - headers=mock.ANY, microversion=mock.ANY, - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + headers=mock.ANY, + microversion=mock.ANY, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) def test_node_set_boot_mode_invalid_mode(self): - self.assertRaises(ValueError, - self.node.set_boot_mode, self.session, 'invalid-efi') + self.assertRaises( + ValueError, self.node.set_boot_mode, self.session, 'invalid-efi' + ) @mock.patch.object(utils, 'pick_microversion', lambda session, v: v) @mock.patch.object(node.Node, 'fetch', lambda self, session: self) @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) class TestNodeSetSecureBoot(base.TestCase): - def setUp(self): super(TestNodeSetSecureBoot, self).setUp() self.node = node.Node(**FAKE) - self.session = mock.Mock(spec=adapter.Adapter, - default_microversion='1.1') + self.session = mock.Mock( + spec=adapter.Adapter, default_microversion='1.1' + ) def test_node_set_secure_boot(self): self.node.set_secure_boot(self.session, True) self.session.put.assert_called_once_with( 'nodes/%s/states/secure_boot' % self.node.id, json={'target': True}, - headers=mock.ANY, microversion=mock.ANY, - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + headers=mock.ANY, + microversion=mock.ANY, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) def test_node_set_secure_boot_invalid_none(self): - self.assertRaises(ValueError, - self.node.set_secure_boot, self.session, None) + self.assertRaises( + ValueError, self.node.set_secure_boot, self.session, None + ) @mock.patch.object(utils, 'pick_microversion', lambda session, v: v) @mock.patch.object(node.Node, 'fetch', lambda self, session: self) @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) class TestNodeTraits(base.TestCase): - def setUp(self): super(TestNodeTraits, self).setUp() self.node = node.Node(**FAKE) - self.session = mock.Mock(spec=adapter.Adapter, - default_microversion='1.37') + self.session = mock.Mock( + spec=adapter.Adapter, default_microversion='1.37' + ) self.session.log = mock.Mock() def test_node_add_trait(self): @@ -870,25 +958,31 @@ def test_node_add_trait(self): self.session.put.assert_called_once_with( 'nodes/%s/traits/%s' % (self.node.id, 'CUSTOM_FAKE'), json=None, - headers=mock.ANY, microversion='1.37', - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + headers=mock.ANY, + microversion='1.37', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) def test_remove_trait(self): - self.assertTrue(self.node.remove_trait(self.session, - 'CUSTOM_FAKE')) + self.assertTrue(self.node.remove_trait(self.session, 'CUSTOM_FAKE')) self.session.delete.assert_called_once_with( 'nodes/%s/traits/%s' % (self.node.id, 'CUSTOM_FAKE'), - headers=mock.ANY, microversion='1.37', - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + headers=mock.ANY, + microversion='1.37', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) def test_remove_trait_missing(self): self.session.delete.return_value.status_code = 400 - self.assertFalse(self.node.remove_trait(self.session, - 'CUSTOM_MISSING')) + self.assertFalse( + self.node.remove_trait(self.session, 'CUSTOM_MISSING') + ) self.session.delete.assert_called_once_with( 'nodes/%s/traits/%s' % (self.node.id, 'CUSTOM_MISSING'), - headers=mock.ANY, microversion='1.37', - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + headers=mock.ANY, + microversion='1.37', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) def test_set_traits(self): traits = ['CUSTOM_FAKE', 'CUSTOM_REAL', 'CUSTOM_MISSING'] @@ -896,19 +990,21 @@ def test_set_traits(self): self.session.put.assert_called_once_with( 'nodes/%s/traits' % self.node.id, json={'traits': ['CUSTOM_FAKE', 'CUSTOM_REAL', 'CUSTOM_MISSING']}, - headers=mock.ANY, microversion='1.37', - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + headers=mock.ANY, + microversion='1.37', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) @mock.patch.object(node.Node, '_assert_microversion_for', _fake_assert) @mock.patch.object(resource.Resource, 'patch', autospec=True) class TestNodePatch(base.TestCase): - def setUp(self): super(TestNodePatch, self).setUp() self.node = node.Node(**FAKE) - self.session = mock.Mock(spec=adapter.Adapter, - default_microversion=None) + self.session = mock.Mock( + spec=adapter.Adapter, default_microversion=None + ) self.session.log = mock.Mock() def test_node_patch(self, mock_patch): @@ -920,15 +1016,21 @@ def test_node_patch(self, mock_patch): @mock.patch.object(resource.Resource, '_prepare_request', autospec=True) @mock.patch.object(resource.Resource, '_commit', autospec=True) - def test_node_patch_reset_interfaces(self, mock__commit, mock_prepreq, - mock_patch): + def test_node_patch_reset_interfaces( + self, mock__commit, mock_prepreq, mock_patch + ): patch = {'path': 'test'} - self.node.patch(self.session, patch=patch, retry_on_conflict=True, - reset_interfaces=True) + self.node.patch( + self.session, + patch=patch, + retry_on_conflict=True, + reset_interfaces=True, + ) mock_prepreq.assert_called_once() prepreq_kwargs = mock_prepreq.call_args[1] - self.assertEqual(prepreq_kwargs['params'], - [('reset_interfaces', True)]) + self.assertEqual( + prepreq_kwargs['params'], [('reset_interfaces', True)] + ) mock__commit.assert_called_once() commit_args = mock__commit.call_args[0] commit_kwargs = mock__commit.call_args[1] @@ -959,9 +1061,13 @@ def _get_side_effect(_self, session): def test_timeout(self, mock_fetch): self.node.power_state = 'power on' - self.assertRaises(exceptions.ResourceTimeout, - self.node.wait_for_power_state, - self.session, 'power off', timeout=0.001) + self.assertRaises( + exceptions.ResourceTimeout, + self.node.wait_for_power_state, + self.session, + 'power off', + timeout=0.001, + ) @mock.patch.object(utils, 'pick_microversion', lambda session, v: v) @@ -971,50 +1077,60 @@ class TestNodePassthru(object): def setUp(self): super(TestNodePassthru, self).setUp() self.node = node.Node(**FAKE) - self.session = node.Mock(spec=adapter.Adapter, - default_microversion='1.37') + self.session = node.Mock( + spec=adapter.Adapter, default_microversion='1.37' + ) self.session.log = mock.Mock() def test_get_passthru(self): self.node.call_vendor_passthru(self.session, "GET", "test_method") self.session.get.assert_called_once_with( 'nodes/%s/vendor_passthru?method=test_method' % self.node.id, - headers=mock.ANY, microversion='1.37', - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + headers=mock.ANY, + microversion='1.37', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) def test_post_passthru(self): self.node.call_vendor_passthru(self.session, "POST", "test_method") self.session.post.assert_called_once_with( 'nodes/%s/vendor_passthru?method=test_method' % self.node.id, - headers=mock.ANY, microversion='1.37', - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + headers=mock.ANY, + microversion='1.37', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) def test_put_passthru(self): self.node.call_vendor_passthru(self.session, "PUT", "test_method") self.session.put.assert_called_once_with( 'nodes/%s/vendor_passthru?method=test_method' % self.node.id, - headers=mock.ANY, microversion='1.37', - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + headers=mock.ANY, + microversion='1.37', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) def test_delete_passthru(self): self.node.call_vendor_passthru(self.session, "DELETE", "test_method") self.session.delete.assert_called_once_with( 'nodes/%s/vendor_passthru?method=test_method' % self.node.id, - headers=mock.ANY, microversion='1.37', - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + headers=mock.ANY, + microversion='1.37', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) def test_list_passthru(self): self.node.list_vendor_passthru(self.session) self.session.get.assert_called_once_with( 'nodes/%s/vendor_passthru/methods' % self.node.id, - headers=mock.ANY, microversion='1.37', - retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + headers=mock.ANY, + microversion='1.37', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) @mock.patch.object(node.Node, 'fetch', lambda self, session: self) @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) class TestNodeConsole(base.TestCase): - def setUp(self): super().setUp() self.node = node.Node(**FAKE) diff --git a/openstack/tests/unit/baremetal/v1/test_port.py b/openstack/tests/unit/baremetal/v1/test_port.py index 396cd20ba..ed98f090e 100644 --- a/openstack/tests/unit/baremetal/v1/test_port.py +++ b/openstack/tests/unit/baremetal/v1/test_port.py @@ -20,30 +20,23 @@ "extra": {}, "internal_info": {}, "links": [ - { - "href": "http://127.0.0.1:6385/v1/ports/", - "rel": "self" - }, - { - "href": "http://127.0.0.1:6385/ports/", - "rel": "bookmark" - } + {"href": "http://127.0.0.1:6385/v1/ports/", "rel": "self"}, + {"href": "http://127.0.0.1:6385/ports/", "rel": "bookmark"}, ], "local_link_connection": { "port_id": "Ethernet3/1", "switch_id": "0a:1b:2c:3d:4e:5f", - "switch_info": "switch1" + "switch_info": "switch1", }, "node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d", "portgroup_uuid": "e43c722c-248e-4c6e-8ce8-0d8ff129387a", "pxe_enabled": True, "updated_at": None, - "uuid": "d2b30520-907d-46c8-bfee-c5586e6fb3a1" + "uuid": "d2b30520-907d-46c8-bfee-c5586e6fb3a1", } class TestPort(base.TestCase): - def test_basic(self): sot = port.Port() self.assertIsNone(sot.resource_key) @@ -64,8 +57,9 @@ def test_instantiate(self): self.assertEqual(FAKE['extra'], sot.extra) self.assertEqual(FAKE['internal_info'], sot.internal_info) self.assertEqual(FAKE['links'], sot.links) - self.assertEqual(FAKE['local_link_connection'], - sot.local_link_connection) + self.assertEqual( + FAKE['local_link_connection'], sot.local_link_connection + ) self.assertEqual(FAKE['node_uuid'], sot.node_id) self.assertEqual(FAKE['portgroup_uuid'], sot.port_group_id) self.assertEqual(FAKE['pxe_enabled'], sot.is_pxe_enabled) diff --git a/openstack/tests/unit/baremetal/v1/test_port_group.py b/openstack/tests/unit/baremetal/v1/test_port_group.py index 33af62a3e..ed5329b4a 100644 --- a/openstack/tests/unit/baremetal/v1/test_port_group.py +++ b/openstack/tests/unit/baremetal/v1/test_port_group.py @@ -20,26 +20,23 @@ "extra": {}, "internal_info": {}, "links": [ - { - "href": "http://127.0.0.1:6385/v1/portgroups/", - "rel": "self" - }, + {"href": "http://127.0.0.1:6385/v1/portgroups/", "rel": "self"}, { "href": "http://127.0.0.1:6385/portgroups/", - "rel": "bookmark" - } + "rel": "bookmark", + }, ], "name": "test_portgroup", "node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d", "ports": [ { "href": "http://127.0.0.1:6385/v1/portgroups//ports", - "rel": "self" + "rel": "self", }, { "href": "http://127.0.0.1:6385/portgroups//ports", - "rel": "bookmark" - } + "rel": "bookmark", + }, ], "standalone_ports_supported": True, "updated_at": None, @@ -48,7 +45,6 @@ class TestPortGroup(base.TestCase): - def test_basic(self): sot = port_group.PortGroup() self.assertIsNone(sot.resource_key) @@ -72,6 +68,8 @@ def test_instantiate(self): self.assertEqual(FAKE['name'], sot.name) self.assertEqual(FAKE['node_uuid'], sot.node_id) self.assertEqual(FAKE['ports'], sot.ports) - self.assertEqual(FAKE['standalone_ports_supported'], - sot.is_standalone_ports_supported) + self.assertEqual( + FAKE['standalone_ports_supported'], + sot.is_standalone_ports_supported, + ) self.assertEqual(FAKE['updated_at'], sot.updated_at) diff --git a/openstack/tests/unit/baremetal/v1/test_proxy.py b/openstack/tests/unit/baremetal/v1/test_proxy.py index 19efccc17..0b88bf0cb 100644 --- a/openstack/tests/unit/baremetal/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal/v1/test_proxy.py @@ -63,9 +63,12 @@ def test_find_chassis(self): self.verify_find(self.proxy.find_chassis, chassis.Chassis) def test_get_chassis(self): - self.verify_get(self.proxy.get_chassis, chassis.Chassis, - mock_method=_MOCK_METHOD, - expected_kwargs={'fields': None}) + self.verify_get( + self.proxy.get_chassis, + chassis.Chassis, + mock_method=_MOCK_METHOD, + expected_kwargs={'fields': None}, + ) def test_update_chassis(self): self.verify_update(self.proxy.update_chassis, chassis.Chassis) @@ -97,23 +100,29 @@ def test_find_node(self): self.verify_find(self.proxy.find_node, node.Node) def test_get_node(self): - self.verify_get(self.proxy.get_node, node.Node, - mock_method=_MOCK_METHOD, - expected_kwargs={'fields': None}) + self.verify_get( + self.proxy.get_node, + node.Node, + mock_method=_MOCK_METHOD, + expected_kwargs={'fields': None}, + ) @mock.patch.object(node.Node, 'commit', autospec=True) def test_update_node(self, mock_commit): self.proxy.update_node('uuid', instance_id='new value') - mock_commit.assert_called_once_with(mock.ANY, self.proxy, - retry_on_conflict=True) + mock_commit.assert_called_once_with( + mock.ANY, self.proxy, retry_on_conflict=True + ) self.assertEqual('new value', mock_commit.call_args[0][0].instance_id) @mock.patch.object(node.Node, 'commit', autospec=True) def test_update_node_no_retries(self, mock_commit): - self.proxy.update_node('uuid', instance_id='new value', - retry_on_conflict=False) - mock_commit.assert_called_once_with(mock.ANY, self.proxy, - retry_on_conflict=False) + self.proxy.update_node( + 'uuid', instance_id='new value', retry_on_conflict=False + ) + mock_commit.assert_called_once_with( + mock.ANY, self.proxy, retry_on_conflict=False + ) self.assertEqual('new value', mock_commit.call_args[0][0].instance_id) def test_delete_node(self): @@ -143,9 +152,12 @@ def test_find_port(self): self.verify_find(self.proxy.find_port, port.Port) def test_get_port(self): - self.verify_get(self.proxy.get_port, port.Port, - mock_method=_MOCK_METHOD, - expected_kwargs={'fields': None}) + self.verify_get( + self.proxy.get_port, + port.Port, + mock_method=_MOCK_METHOD, + expected_kwargs={'fields': None}, + ) def test_update_port(self): self.verify_update(self.proxy.update_port, port.Port) @@ -171,9 +183,12 @@ def test_port_groups_not_detailed(self, mock_list): mock_list.assert_called_once_with(self.proxy, details=False, query=1) def test_get_port_group(self): - self.verify_get(self.proxy.get_port_group, port_group.PortGroup, - mock_method=_MOCK_METHOD, - expected_kwargs={'fields': None}) + self.verify_get( + self.proxy.get_port_group, + port_group.PortGroup, + mock_method=_MOCK_METHOD, + expected_kwargs={'fields': None}, + ) class TestAllocation(TestBaremetalProxy): @@ -181,43 +196,57 @@ def test_create_allocation(self): self.verify_create(self.proxy.create_allocation, allocation.Allocation) def test_get_allocation(self): - self.verify_get(self.proxy.get_allocation, allocation.Allocation, - mock_method=_MOCK_METHOD, - expected_kwargs={'fields': None}) + self.verify_get( + self.proxy.get_allocation, + allocation.Allocation, + mock_method=_MOCK_METHOD, + expected_kwargs={'fields': None}, + ) def test_delete_allocation(self): - self.verify_delete(self.proxy.delete_allocation, allocation.Allocation, - False) + self.verify_delete( + self.proxy.delete_allocation, allocation.Allocation, False + ) def test_delete_allocation_ignore(self): - self.verify_delete(self.proxy.delete_allocation, allocation.Allocation, - True) + self.verify_delete( + self.proxy.delete_allocation, allocation.Allocation, True + ) class TestVolumeConnector(TestBaremetalProxy): def test_create_volume_connector(self): - self.verify_create(self.proxy.create_volume_connector, - volume_connector.VolumeConnector) + self.verify_create( + self.proxy.create_volume_connector, + volume_connector.VolumeConnector, + ) def test_find_volume_connector(self): - self.verify_find(self.proxy.find_volume_connector, - volume_connector.VolumeConnector) + self.verify_find( + self.proxy.find_volume_connector, volume_connector.VolumeConnector + ) def test_get_volume_connector(self): - self.verify_get(self.proxy.get_volume_connector, - volume_connector.VolumeConnector, - mock_method=_MOCK_METHOD, - expected_kwargs={'fields': None}) + self.verify_get( + self.proxy.get_volume_connector, + volume_connector.VolumeConnector, + mock_method=_MOCK_METHOD, + expected_kwargs={'fields': None}, + ) def test_delete_volume_connector(self): - self.verify_delete(self.proxy.delete_volume_connector, - volume_connector.VolumeConnector, - False) + self.verify_delete( + self.proxy.delete_volume_connector, + volume_connector.VolumeConnector, + False, + ) def test_delete_volume_connector_ignore(self): - self.verify_delete(self.proxy.delete_volume_connector, - volume_connector.VolumeConnector, - True) + self.verify_delete( + self.proxy.delete_volume_connector, + volume_connector.VolumeConnector, + True, + ) class TestVolumeTarget(TestBaremetalProxy): @@ -234,28 +263,32 @@ def test_volume_target_not_detailed(self, mock_list): mock_list.assert_called_once_with(self.proxy, query=1) def test_create_volume_target(self): - self.verify_create(self.proxy.create_volume_target, - volume_target.VolumeTarget) + self.verify_create( + self.proxy.create_volume_target, volume_target.VolumeTarget + ) def test_find_volume_target(self): - self.verify_find(self.proxy.find_volume_target, - volume_target.VolumeTarget) + self.verify_find( + self.proxy.find_volume_target, volume_target.VolumeTarget + ) def test_get_volume_target(self): - self.verify_get(self.proxy.get_volume_target, - volume_target.VolumeTarget, - mock_method=_MOCK_METHOD, - expected_kwargs={'fields': None}) + self.verify_get( + self.proxy.get_volume_target, + volume_target.VolumeTarget, + mock_method=_MOCK_METHOD, + expected_kwargs={'fields': None}, + ) def test_delete_volume_target(self): - self.verify_delete(self.proxy.delete_volume_target, - volume_target.VolumeTarget, - False) + self.verify_delete( + self.proxy.delete_volume_target, volume_target.VolumeTarget, False + ) def test_delete_volume_target_ignore(self): - self.verify_delete(self.proxy.delete_volume_target, - volume_target.VolumeTarget, - True) + self.verify_delete( + self.proxy.delete_volume_target, volume_target.VolumeTarget, True + ) class TestMisc(TestBaremetalProxy): @@ -263,35 +296,45 @@ class TestMisc(TestBaremetalProxy): def test__get_with_fields_none(self, mock_fetch): result = self.proxy._get_with_fields(node.Node, 'value') self.assertIs(result, mock_fetch.return_value) - mock_fetch.assert_called_once_with(mock.ANY, self.proxy, - error_message=mock.ANY) + mock_fetch.assert_called_once_with( + mock.ANY, self.proxy, error_message=mock.ANY + ) @mock.patch.object(node.Node, 'fetch', autospec=True) def test__get_with_fields_node(self, mock_fetch): result = self.proxy._get_with_fields( # Mix of server-side and client-side fields - node.Node, 'value', fields=['maintenance', 'id', 'instance_id']) + node.Node, + 'value', + fields=['maintenance', 'id', 'instance_id'], + ) self.assertIs(result, mock_fetch.return_value) mock_fetch.assert_called_once_with( - mock.ANY, self.proxy, error_message=mock.ANY, + mock.ANY, + self.proxy, + error_message=mock.ANY, # instance_id converted to server-side instance_uuid - fields='maintenance,uuid,instance_uuid') + fields='maintenance,uuid,instance_uuid', + ) @mock.patch.object(port.Port, 'fetch', autospec=True) def test__get_with_fields_port(self, mock_fetch): result = self.proxy._get_with_fields( - port.Port, 'value', fields=['address', 'id', 'node_id']) + port.Port, 'value', fields=['address', 'id', 'node_id'] + ) self.assertIs(result, mock_fetch.return_value) mock_fetch.assert_called_once_with( - mock.ANY, self.proxy, error_message=mock.ANY, + mock.ANY, + self.proxy, + error_message=mock.ANY, # node_id converted to server-side node_uuid - fields='address,uuid,node_uuid') + fields='address,uuid,node_uuid', + ) @mock.patch('time.sleep', lambda _sec: None) @mock.patch.object(_proxy.Proxy, 'get_node', autospec=True) class TestWaitForNodesProvisionState(base.TestCase): - def setUp(self): super(TestWaitForNodesProvisionState, self).setUp() self.session = mock.Mock() @@ -299,59 +342,67 @@ def setUp(self): def test_success(self, mock_get): # two attempts, one node succeeds after the 1st - nodes = [mock.Mock(spec=node.Node, id=str(i)) - for i in range(3)] + nodes = [mock.Mock(spec=node.Node, id=str(i)) for i in range(3)] for i, n in enumerate(nodes): # 1st attempt on 1st node, 2nd attempt on 2nd node n._check_state_reached.return_value = not (i % 2) mock_get.side_effect = nodes result = self.proxy.wait_for_nodes_provision_state( - ['abcd', node.Node(id='1234')], 'fake state') + ['abcd', node.Node(id='1234')], 'fake state' + ) self.assertEqual([nodes[0], nodes[2]], result) for n in nodes: n._check_state_reached.assert_called_once_with( - self.proxy, 'fake state', True) + self.proxy, 'fake state', True + ) def test_success_no_fail(self, mock_get): # two attempts, one node succeeds after the 1st - nodes = [mock.Mock(spec=node.Node, id=str(i)) - for i in range(3)] + nodes = [mock.Mock(spec=node.Node, id=str(i)) for i in range(3)] for i, n in enumerate(nodes): # 1st attempt on 1st node, 2nd attempt on 2nd node n._check_state_reached.return_value = not (i % 2) mock_get.side_effect = nodes result = self.proxy.wait_for_nodes_provision_state( - ['abcd', node.Node(id='1234')], 'fake state', fail=False) + ['abcd', node.Node(id='1234')], 'fake state', fail=False + ) self.assertEqual([nodes[0], nodes[2]], result.success) self.assertEqual([], result.failure) self.assertEqual([], result.timeout) for n in nodes: n._check_state_reached.assert_called_once_with( - self.proxy, 'fake state', True) + self.proxy, 'fake state', True + ) def test_timeout(self, mock_get): mock_get.return_value._check_state_reached.return_value = False mock_get.return_value.id = '1234' - self.assertRaises(exceptions.ResourceTimeout, - self.proxy.wait_for_nodes_provision_state, - ['abcd', node.Node(id='1234')], 'fake state', - timeout=0.001) + self.assertRaises( + exceptions.ResourceTimeout, + self.proxy.wait_for_nodes_provision_state, + ['abcd', node.Node(id='1234')], + 'fake state', + timeout=0.001, + ) mock_get.return_value._check_state_reached.assert_called_with( - self.proxy, 'fake state', True) + self.proxy, 'fake state', True + ) def test_timeout_no_fail(self, mock_get): mock_get.return_value._check_state_reached.return_value = False mock_get.return_value.id = '1234' result = self.proxy.wait_for_nodes_provision_state( - ['abcd'], 'fake state', timeout=0.001, fail=False) + ['abcd'], 'fake state', timeout=0.001, fail=False + ) mock_get.return_value._check_state_reached.assert_called_with( - self.proxy, 'fake state', True) + self.proxy, 'fake state', True + ) self.assertEqual([], result.success) self.assertEqual([mock_get.return_value], result.timeout) @@ -364,8 +415,9 @@ def _fake_get(_self, node): if result.id == '1': result._check_state_reached.return_value = True elif result.id == '2': - result._check_state_reached.side_effect = \ + result._check_state_reached.side_effect = ( exceptions.ResourceFailure("boom") + ) else: result._check_state_reached.return_value = False return result @@ -373,7 +425,8 @@ def _fake_get(_self, node): mock_get.side_effect = _fake_get result = self.proxy.wait_for_nodes_provision_state( - ['1', '2', '3'], 'fake state', timeout=0.001, fail=False) + ['1', '2', '3'], 'fake state', timeout=0.001, fail=False + ) self.assertEqual(['1'], [x.id for x in result.success]) self.assertEqual(['3'], [x.id for x in result.timeout]) diff --git a/openstack/tests/unit/baremetal/v1/test_volume_connector.py b/openstack/tests/unit/baremetal/v1/test_volume_connector.py index aa4a0eb9c..c148cc826 100644 --- a/openstack/tests/unit/baremetal/v1/test_volume_connector.py +++ b/openstack/tests/unit/baremetal/v1/test_volume_connector.py @@ -21,22 +21,21 @@ "links": [ { "href": "http://127.0.0.1:6385/v1/volume/connector/", - "rel": "self" + "rel": "self", }, { "href": "http://127.0.0.1:6385/volume/connector/", - "rel": "bookmark" - } + "rel": "bookmark", + }, ], "node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d", "type": "iqn", "updated_at": None, - "uuid": "9bf93e01-d728-47a3-ad4b-5e66a835037c" + "uuid": "9bf93e01-d728-47a3-ad4b-5e66a835037c", } class TestVolumeconnector(base.TestCase): - def test_basic(self): sot = volume_connector.VolumeConnector() self.assertIsNone(sot.resource_key) diff --git a/openstack/tests/unit/baremetal/v1/test_volume_target.py b/openstack/tests/unit/baremetal/v1/test_volume_target.py index f4b4d0c9a..057d7414b 100644 --- a/openstack/tests/unit/baremetal/v1/test_volume_target.py +++ b/openstack/tests/unit/baremetal/v1/test_volume_target.py @@ -21,24 +21,23 @@ "links": [ { "href": "http://127.0.0.1:6385/v1/volume/targets/", - "rel": "self" + "rel": "self", }, { "href": "http://127.0.0.1:6385/volume/targets/", - "rel": "bookmark" - } + "rel": "bookmark", + }, ], "node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d", "properties": {}, "updated_at": None, "uuid": "bd4d008c-7d31-463d-abf9-6c23d9d55f7f", "volume_id": "04452bed-5367-4202-8bf5-de4335ac56d2", - "volume_type": "iscsi" + "volume_type": "iscsi", } class TestVolumeTarget(base.TestCase): - def test_basic(self): sot = volume_target.VolumeTarget() self.assertIsNone(sot.resource_key) diff --git a/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py b/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py index c71fd3d66..6192028d1 100644 --- a/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py @@ -24,7 +24,6 @@ @mock.patch.object(introspection.Introspection, 'create', autospec=True) class TestStartIntrospection(base.TestCase): - def setUp(self): super(TestStartIntrospection, self).setUp() self.session = mock.Mock(spec=adapter.Adapter) @@ -44,27 +43,27 @@ def test_create_introspection_with_node(self, mock_create): def test_create_introspection_manage_boot(self, mock_create): self.proxy.start_introspection('abcd', manage_boot=False) - mock_create.assert_called_once_with(mock.ANY, self.proxy, - manage_boot=False) + mock_create.assert_called_once_with( + mock.ANY, self.proxy, manage_boot=False + ) introspect = mock_create.call_args[0][0] self.assertEqual('abcd', introspect.id) class TestBaremetalIntrospectionProxy(test_proxy_base.TestProxyBase): - def setUp(self): super(TestBaremetalIntrospectionProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) def test_get_introspection(self): - self.verify_get(self.proxy.get_introspection, - introspection.Introspection) + self.verify_get( + self.proxy.get_introspection, introspection.Introspection + ) @mock.patch('time.sleep', lambda _sec: None) @mock.patch.object(introspection.Introspection, 'fetch', autospec=True) class TestWaitForIntrospection(base.TestCase): - def setUp(self): super(TestWaitForIntrospection, self).setUp() self.session = mock.Mock(spec=adapter.Adapter) @@ -96,10 +95,12 @@ def _side_effect(allocation, session): self.assertEqual(2, mock_fetch.call_count) def test_timeout(self, mock_fetch): - self.assertRaises(exceptions.ResourceTimeout, - self.proxy.wait_for_introspection, - self.introspection, - timeout=0.001) + self.assertRaises( + exceptions.ResourceTimeout, + self.proxy.wait_for_introspection, + self.introspection, + timeout=0.001, + ) mock_fetch.assert_called_with(self.introspection, self.proxy) def test_failure(self, mock_fetch): @@ -109,9 +110,12 @@ def _side_effect(allocation, session): self.introspection.error = 'boom' mock_fetch.side_effect = _side_effect - self.assertRaisesRegex(exceptions.ResourceFailure, 'boom', - self.proxy.wait_for_introspection, - self.introspection) + self.assertRaisesRegex( + exceptions.ResourceFailure, + 'boom', + self.proxy.wait_for_introspection, + self.introspection, + ) mock_fetch.assert_called_once_with(self.introspection, self.proxy) def test_failure_ignored(self, mock_fetch): @@ -121,15 +125,15 @@ def _side_effect(allocation, session): self.introspection.error = 'boom' mock_fetch.side_effect = _side_effect - result = self.proxy.wait_for_introspection(self.introspection, - ignore_error=True) + result = self.proxy.wait_for_introspection( + self.introspection, ignore_error=True + ) self.assertIs(result, self.introspection) mock_fetch.assert_called_once_with(self.introspection, self.proxy) @mock.patch.object(_proxy.Proxy, 'request', autospec=True) class TestAbortIntrospection(base.TestCase): - def setUp(self): super(TestAbortIntrospection, self).setUp() self.session = mock.Mock(spec=adapter.Adapter) @@ -141,14 +145,17 @@ def test_abort(self, mock_request): mock_request.return_value.status_code = 202 self.proxy.abort_introspection(self.introspection) mock_request.assert_called_once_with( - self.proxy, 'introspection/1234/abort', 'POST', - headers=mock.ANY, microversion=mock.ANY, - retriable_status_codes=[409, 503]) + self.proxy, + 'introspection/1234/abort', + 'POST', + headers=mock.ANY, + microversion=mock.ANY, + retriable_status_codes=[409, 503], + ) @mock.patch.object(_proxy.Proxy, 'request', autospec=True) class TestGetData(base.TestCase): - def setUp(self): super(TestGetData, self).setUp() self.session = mock.Mock(spec=adapter.Adapter) @@ -160,15 +167,24 @@ def test_get_data(self, mock_request): mock_request.return_value.status_code = 200 data = self.proxy.get_introspection_data(self.introspection) mock_request.assert_called_once_with( - self.proxy, 'introspection/1234/data', 'GET', - headers=mock.ANY, microversion=mock.ANY) + self.proxy, + 'introspection/1234/data', + 'GET', + headers=mock.ANY, + microversion=mock.ANY, + ) self.assertIs(data, mock_request.return_value.json.return_value) def test_get_unprocessed_data(self, mock_request): mock_request.return_value.status_code = 200 - data = self.proxy.get_introspection_data(self.introspection, - processed=False) + data = self.proxy.get_introspection_data( + self.introspection, processed=False + ) mock_request.assert_called_once_with( - self.proxy, 'introspection/1234/data/unprocessed', 'GET', - headers=mock.ANY, microversion='1.17') + self.proxy, + 'introspection/1234/data/unprocessed', + 'GET', + headers=mock.ANY, + microversion='1.17', + ) self.assertIs(data, mock_request.return_value.json.return_value) From 82c2a534024cff7690620876723422a98e8f371a Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 5 May 2023 10:57:04 +0100 Subject: [PATCH 3243/3836] Blackify openstack.load_balancer Black used with the '-l 79 -S' flags. A future change will ignore this commit in git-blame history by adding a 'git-blame-ignore-revs' file. Change-Id: I4f3f4228b94230d3b2f52bed4e928df67f82017f Signed-off-by: Stephen Finucane --- openstack/load_balancer/v2/_proxy.py | 253 +++++---- openstack/load_balancer/v2/amphora.py | 30 +- .../load_balancer/v2/availability_zone.py | 9 +- openstack/load_balancer/v2/health_monitor.py | 20 +- openstack/load_balancer/v2/l7_policy.py | 14 +- openstack/load_balancer/v2/l7_rule.py | 17 +- openstack/load_balancer/v2/listener.py | 30 +- openstack/load_balancer/v2/load_balancer.py | 36 +- openstack/load_balancer/v2/member.py | 16 +- openstack/load_balancer/v2/pool.py | 20 +- openstack/load_balancer/v2/quota.py | 11 +- .../load_balancer/v2/test_load_balancer.py | 505 ++++++++++++------ .../tests/unit/load_balancer/test_amphora.py | 77 +-- .../load_balancer/test_availability_zone.py | 47 +- .../test_availability_zone_profile.py | 45 +- .../tests/unit/load_balancer/test_flavor.py | 28 +- .../unit/load_balancer/test_flavor_profile.py | 21 +- .../unit/load_balancer/test_health_monitor.py | 59 +- .../tests/unit/load_balancer/test_l7policy.py | 64 +-- .../tests/unit/load_balancer/test_l7rule.py | 61 ++- .../tests/unit/load_balancer/test_listener.py | 182 ++++--- .../unit/load_balancer/test_load_balancer.py | 169 +++--- .../tests/unit/load_balancer/test_member.py | 58 +- .../tests/unit/load_balancer/test_pool.py | 109 ++-- .../tests/unit/load_balancer/test_provider.py | 39 +- .../tests/unit/load_balancer/test_quota.py | 2 - .../tests/unit/load_balancer/test_version.py | 1 - .../tests/unit/load_balancer/v2/test_proxy.py | 405 ++++++++------ 28 files changed, 1386 insertions(+), 942 deletions(-) diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index b5b4d7f50..4e95023e1 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -12,8 +12,9 @@ from openstack.load_balancer.v2 import amphora as _amphora from openstack.load_balancer.v2 import availability_zone as _availability_zone -from openstack.load_balancer.v2 import availability_zone_profile as \ - _availability_zone_profile +from openstack.load_balancer.v2 import ( + availability_zone_profile as _availability_zone_profile, +) from openstack.load_balancer.v2 import flavor as _flavor from openstack.load_balancer.v2 import flavor_profile as _flavor_profile from openstack.load_balancer.v2 import health_monitor as _hm @@ -33,8 +34,7 @@ class Proxy(proxy.Proxy): _resource_registry = { "amphora": _amphora.Amphora, "availability_zone": _availability_zone.AvailabilityZone, - "availability_zone_profile": - _availability_zone_profile.AvailabilityZoneProfile, + "availability_zone_profile": _availability_zone_profile.AvailabilityZoneProfile, # noqa: E501 "flavor": _flavor.Flavor, "flavor_profile": _flavor_profile.FlavorProfile, "health_monitor": _hm.HealthMonitor, @@ -44,7 +44,7 @@ class Proxy(proxy.Proxy): "member": _member.Member, "pool": _pool.Pool, "provider": _provider.Provider, - "quota": _quota.Quota + "quota": _quota.Quota, } def create_load_balancer(self, **attrs): @@ -82,8 +82,9 @@ def get_load_balancer_statistics(self, load_balancer): :returns: One :class:`~openstack.load_balancer.v2.load_balancer.LoadBalancerStats` """ - return self._get(_lb.LoadBalancerStats, lb_id=load_balancer, - requires_id=False) + return self._get( + _lb.LoadBalancerStats, lb_id=load_balancer, requires_id=False + ) def load_balancers(self, **query): """Retrieve a generator of load balancers @@ -92,8 +93,9 @@ def load_balancers(self, **query): """ return self._list(_lb.LoadBalancer, **query) - def delete_load_balancer(self, load_balancer, ignore_missing=True, - cascade=False): + def delete_load_balancer( + self, load_balancer, ignore_missing=True, cascade=False + ): """Delete a load balancer :param load_balancer: The load_balancer can be either the ID or a @@ -111,8 +113,9 @@ def delete_load_balancer(self, load_balancer, ignore_missing=True, """ load_balancer = self._get_resource(_lb.LoadBalancer, load_balancer) load_balancer.cascade = cascade - return self._delete(_lb.LoadBalancer, load_balancer, - ignore_missing=ignore_missing) + return self._delete( + _lb.LoadBalancer, load_balancer, ignore_missing=ignore_missing + ) def find_load_balancer(self, name_or_id, ignore_missing=True): """Find a single load balancer @@ -126,8 +129,9 @@ def find_load_balancer(self, name_or_id, ignore_missing=True): :returns: ``None`` """ - return self._find(_lb.LoadBalancer, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _lb.LoadBalancer, name_or_id, ignore_missing=ignore_missing + ) def update_load_balancer(self, load_balancer, **attrs): """Update a load balancer @@ -143,8 +147,14 @@ def update_load_balancer(self, load_balancer, **attrs): """ return self._update(_lb.LoadBalancer, load_balancer, **attrs) - def wait_for_load_balancer(self, name_or_id, status='ACTIVE', - failures=['ERROR'], interval=2, wait=300): + def wait_for_load_balancer( + self, + name_or_id, + status='ACTIVE', + failures=['ERROR'], + interval=2, + wait=300, + ): """Wait for load balancer status :param name_or_id: The name or ID of the load balancer. @@ -167,8 +177,15 @@ def wait_for_load_balancer(self, name_or_id, status='ACTIVE', """ lb = self._find(_lb.LoadBalancer, name_or_id, ignore_missing=False) - return resource.wait_for_status(self, lb, status, failures, interval, - wait, attribute='provisioning_status') + return resource.wait_for_status( + self, + lb, + status, + failures, + interval, + wait, + attribute='provisioning_status', + ) def failover_load_balancer(self, load_balancer, **attrs): """Failover a load balancer @@ -206,8 +223,9 @@ def delete_listener(self, listener, ignore_missing=True): :returns: ``None`` """ - self._delete(_listener.Listener, listener, - ignore_missing=ignore_missing) + self._delete( + _listener.Listener, listener, ignore_missing=ignore_missing + ) def find_listener(self, name_or_id, ignore_missing=True): """Find a single listener @@ -222,8 +240,9 @@ def find_listener(self, name_or_id, ignore_missing=True): :returns: One :class:`~openstack.load_balancer.v2.listener.Listener` or None """ - return self._find(_listener.Listener, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _listener.Listener, name_or_id, ignore_missing=ignore_missing + ) def get_listener(self, listener): """Get a single listener @@ -250,8 +269,9 @@ def get_listener_statistics(self, listener): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get(_listener.ListenerStats, listener_id=listener, - requires_id=False) + return self._get( + _listener.ListenerStats, listener_id=listener, requires_id=False + ) def listeners(self, **query): """Return a generator of listeners @@ -322,8 +342,7 @@ def delete_pool(self, pool, ignore_missing=True): :returns: ``None`` """ - return self._delete(_pool.Pool, pool, - ignore_missing=ignore_missing) + return self._delete(_pool.Pool, pool, ignore_missing=ignore_missing) def find_pool(self, name_or_id, ignore_missing=True): """Find a single pool @@ -337,8 +356,9 @@ def find_pool(self, name_or_id, ignore_missing=True): :returns: ``None`` """ - return self._find(_pool.Pool, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _pool.Pool, name_or_id, ignore_missing=ignore_missing + ) def update_pool(self, pool, **attrs): """Update a pool @@ -368,8 +388,7 @@ def create_member(self, pool, **attrs): :rtype: :class:`~openstack.load_balancer.v2.member.Member` """ poolobj = self._get_resource(_pool.Pool, pool) - return self._create(_member.Member, pool_id=poolobj.id, - **attrs) + return self._create(_member.Member, pool_id=poolobj.id, **attrs) def delete_member(self, member, pool, ignore_missing=True): """Delete a member @@ -389,8 +408,12 @@ def delete_member(self, member, pool, ignore_missing=True): :returns: ``None`` """ poolobj = self._get_resource(_pool.Pool, pool) - self._delete(_member.Member, member, - ignore_missing=ignore_missing, pool_id=poolobj.id) + self._delete( + _member.Member, + member, + ignore_missing=ignore_missing, + pool_id=poolobj.id, + ) def find_member(self, name_or_id, pool, ignore_missing=True): """Find a single member @@ -409,8 +432,12 @@ def find_member(self, name_or_id, pool, ignore_missing=True): or None """ poolobj = self._get_resource(_pool.Pool, pool) - return self._find(_member.Member, name_or_id, - ignore_missing=ignore_missing, pool_id=poolobj.id) + return self._find( + _member.Member, + name_or_id, + ignore_missing=ignore_missing, + pool_id=poolobj.id, + ) def get_member(self, member, pool): """Get a single member @@ -427,8 +454,7 @@ def get_member(self, member, pool): when no resource can be found. """ poolobj = self._get_resource(_pool.Pool, pool) - return self._get(_member.Member, member, - pool_id=poolobj.id) + return self._get(_member.Member, member, pool_id=poolobj.id) def members(self, pool, **query): """Return a generator of members @@ -461,8 +487,9 @@ def update_member(self, member, pool, **attrs): :rtype: :class:`~openstack.load_balancer.v2.member.Member` """ poolobj = self._get_resource(_pool.Pool, pool) - return self._update(_member.Member, member, - pool_id=poolobj.id, **attrs) + return self._update( + _member.Member, member, pool_id=poolobj.id, **attrs + ) def find_health_monitor(self, name_or_id, ignore_missing=True): """Find a single health monitor @@ -483,8 +510,9 @@ def find_health_monitor(self, name_or_id, ignore_missing=True): :raises: :class:`openstack.exceptions.ResourceNotFound` if nothing is found and ignore_missing is ``False``. """ - return self._find(_hm.HealthMonitor, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _hm.HealthMonitor, name_or_id, ignore_missing=ignore_missing + ) def create_health_monitor(self, **attrs): """Create a new health monitor from attributes @@ -544,8 +572,9 @@ def delete_health_monitor(self, healthmonitor, ignore_missing=True): :returns: ``None`` """ - return self._delete(_hm.HealthMonitor, healthmonitor, - ignore_missing=ignore_missing) + return self._delete( + _hm.HealthMonitor, healthmonitor, ignore_missing=ignore_missing + ) def update_health_monitor(self, healthmonitor, **attrs): """Update a health monitor @@ -561,8 +590,7 @@ def update_health_monitor(self, healthmonitor, **attrs): :rtype: :class:`~openstack.load_balancer.v2.healthmonitor.HealthMonitor` """ - return self._update(_hm.HealthMonitor, healthmonitor, - **attrs) + return self._update(_hm.HealthMonitor, healthmonitor, **attrs) def create_l7_policy(self, **attrs): """Create a new l7policy from attributes @@ -589,8 +617,9 @@ def delete_l7_policy(self, l7_policy, ignore_missing=True): :returns: ``None`` """ - self._delete(_l7policy.L7Policy, l7_policy, - ignore_missing=ignore_missing) + self._delete( + _l7policy.L7Policy, l7_policy, ignore_missing=ignore_missing + ) def find_l7_policy(self, name_or_id, ignore_missing=True): """Find a single l7policy @@ -605,8 +634,9 @@ def find_l7_policy(self, name_or_id, ignore_missing=True): :returns: One :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` or None """ - return self._find(_l7policy.L7Policy, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _l7policy.L7Policy, name_or_id, ignore_missing=ignore_missing + ) def get_l7_policy(self, l7_policy): """Get a single l7policy @@ -660,8 +690,9 @@ def create_l7_rule(self, l7_policy, **attrs): :rtype: :class:`~openstack.load_balancer.v2.l7_rule.L7Rule` """ l7policyobj = self._get_resource(_l7policy.L7Policy, l7_policy) - return self._create(_l7rule.L7Rule, l7policy_id=l7policyobj.id, - **attrs) + return self._create( + _l7rule.L7Rule, l7policy_id=l7policyobj.id, **attrs + ) def delete_l7_rule(self, l7rule, l7_policy, ignore_missing=True): """Delete a l7rule @@ -680,8 +711,12 @@ def delete_l7_rule(self, l7rule, l7_policy, ignore_missing=True): :returns: ``None`` """ l7policyobj = self._get_resource(_l7policy.L7Policy, l7_policy) - self._delete(_l7rule.L7Rule, l7rule, ignore_missing=ignore_missing, - l7policy_id=l7policyobj.id) + self._delete( + _l7rule.L7Rule, + l7rule, + ignore_missing=ignore_missing, + l7policy_id=l7policyobj.id, + ) def find_l7_rule(self, name_or_id, l7_policy, ignore_missing=True): """Find a single l7rule @@ -700,9 +735,12 @@ def find_l7_rule(self, name_or_id, l7_policy, ignore_missing=True): or None """ l7policyobj = self._get_resource(_l7policy.L7Policy, l7_policy) - return self._find(_l7rule.L7Rule, name_or_id, - ignore_missing=ignore_missing, - l7policy_id=l7policyobj.id) + return self._find( + _l7rule.L7Rule, + name_or_id, + ignore_missing=ignore_missing, + l7policy_id=l7policyobj.id, + ) def get_l7_rule(self, l7rule, l7_policy): """Get a single l7rule @@ -719,8 +757,7 @@ def get_l7_rule(self, l7rule, l7_policy): when no resource can be found. """ l7policyobj = self._get_resource(_l7policy.L7Policy, l7_policy) - return self._get(_l7rule.L7Rule, l7rule, - l7policy_id=l7policyobj.id) + return self._get(_l7rule.L7Rule, l7rule, l7policy_id=l7policyobj.id) def l7_rules(self, l7_policy, **query): """Return a generator of l7rules @@ -753,8 +790,9 @@ def update_l7_rule(self, l7rule, l7_policy, **attrs): :rtype: :class:`~openstack.load_balancer.v2.l7_rule.L7Rule` """ l7policyobj = self._get_resource(_l7policy.L7Policy, l7_policy) - return self._update(_l7rule.L7Rule, l7rule, - l7policy_id=l7policyobj.id, **attrs) + return self._update( + _l7rule.L7Rule, l7rule, l7policy_id=l7policyobj.id, **attrs + ) def quotas(self, **query): """Return a generator of quotas @@ -833,8 +871,9 @@ def provider_flavor_capabilities(self, provider, **query): :returns: A generator of provider flavor capabilities instances """ - return self._list(_provider.ProviderFlavorCapabilities, - provider=provider, **query) + return self._list( + _provider.ProviderFlavorCapabilities, provider=provider, **query + ) def create_flavor_profile(self, **attrs): """Create a new flavor profile from attributes @@ -882,8 +921,11 @@ def delete_flavor_profile(self, flavor_profile, ignore_missing=True): :returns: ``None`` """ - self._delete(_flavor_profile.FlavorProfile, flavor_profile, - ignore_missing=ignore_missing) + self._delete( + _flavor_profile.FlavorProfile, + flavor_profile, + ignore_missing=ignore_missing, + ) def find_flavor_profile(self, name_or_id, ignore_missing=True): """Find a single flavor profile @@ -897,8 +939,11 @@ def find_flavor_profile(self, name_or_id, ignore_missing=True): :returns: ``None`` """ - return self._find(_flavor_profile.FlavorProfile, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _flavor_profile.FlavorProfile, + name_or_id, + ignore_missing=ignore_missing, + ) def update_flavor_profile(self, flavor_profile, **attrs): """Update a flavor profile @@ -913,8 +958,9 @@ def update_flavor_profile(self, flavor_profile, **attrs): :rtype: :class:`~openstack.load_balancer.v2.flavor_profile.FlavorProfile` """ - return self._update(_flavor_profile.FlavorProfile, flavor_profile, - **attrs) + return self._update( + _flavor_profile.FlavorProfile, flavor_profile, **attrs + ) def create_flavor(self, **attrs): """Create a new flavor from attributes @@ -973,8 +1019,9 @@ def find_flavor(self, name_or_id, ignore_missing=True): :returns: ``None`` """ - return self._find(_flavor.Flavor, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _flavor.Flavor, name_or_id, ignore_missing=ignore_missing + ) def update_flavor(self, flavor, **attrs): """Update a flavor @@ -1019,8 +1066,9 @@ def find_amphora(self, amphora_id, ignore_missing=True): :returns: ``None`` """ - return self._find(_amphora.Amphora, amphora_id, - ignore_missing=ignore_missing) + return self._find( + _amphora.Amphora, amphora_id, ignore_missing=ignore_missing + ) def configure_amphora(self, amphora_id, **attrs): """Update the configuration of an amphora agent @@ -1052,8 +1100,9 @@ def create_availability_zone_profile(self, **attrs): :rtype: :class:`~openstack.load_balancer.v2.availability_zone_profile.AvailabilityZoneProfile` """ - return self._create(_availability_zone_profile.AvailabilityZoneProfile, - **attrs) + return self._create( + _availability_zone_profile.AvailabilityZoneProfile, **attrs + ) def get_availability_zone_profile(self, *attrs): """Get an availability zone profile @@ -1066,19 +1115,22 @@ def get_availability_zone_profile(self, *attrs): :returns: One :class:`~openstack.load_balancer.v2.availability_zone_profile.AvailabilityZoneProfile` """ - return self._get(_availability_zone_profile.AvailabilityZoneProfile, - *attrs) + return self._get( + _availability_zone_profile.AvailabilityZoneProfile, *attrs + ) def availability_zone_profiles(self, **query): """Retrieve a generator of availability zone profiles :returns: A generator of availability zone profiles instances """ - return self._list(_availability_zone_profile.AvailabilityZoneProfile, - **query) + return self._list( + _availability_zone_profile.AvailabilityZoneProfile, **query + ) - def delete_availability_zone_profile(self, availability_zone_profile, - ignore_missing=True): + def delete_availability_zone_profile( + self, availability_zone_profile, ignore_missing=True + ): """Delete an availability zone profile :param availability_zone_profile: The availability_zone_profile can be @@ -1093,8 +1145,11 @@ def delete_availability_zone_profile(self, availability_zone_profile, :returns: ``None`` """ - self._delete(_availability_zone_profile.AvailabilityZoneProfile, - availability_zone_profile, ignore_missing=ignore_missing) + self._delete( + _availability_zone_profile.AvailabilityZoneProfile, + availability_zone_profile, + ignore_missing=ignore_missing, + ) def find_availability_zone_profile(self, name_or_id, ignore_missing=True): """Find a single availability zone profile @@ -1108,11 +1163,15 @@ def find_availability_zone_profile(self, name_or_id, ignore_missing=True): :returns: ``None`` """ - return self._find(_availability_zone_profile.AvailabilityZoneProfile, - name_or_id, ignore_missing=ignore_missing) + return self._find( + _availability_zone_profile.AvailabilityZoneProfile, + name_or_id, + ignore_missing=ignore_missing, + ) - def update_availability_zone_profile(self, availability_zone_profile, - **attrs): + def update_availability_zone_profile( + self, availability_zone_profile, **attrs + ): """Update an availability zone profile :param availability_zone_profile: The availability_zone_profile can be @@ -1126,8 +1185,11 @@ def update_availability_zone_profile(self, availability_zone_profile, :rtype: :class:`~openstack.load_balancer.v2.availability_zone_profile.AvailabilityZoneProfile` """ - return self._update(_availability_zone_profile.AvailabilityZoneProfile, - availability_zone_profile, **attrs) + return self._update( + _availability_zone_profile.AvailabilityZoneProfile, + availability_zone_profile, + **attrs + ) def create_availability_zone(self, **attrs): """Create a new availability zone from attributes @@ -1177,8 +1239,11 @@ def delete_availability_zone(self, availability_zone, ignore_missing=True): :returns: ``None`` """ - self._delete(_availability_zone.AvailabilityZone, availability_zone, - ignore_missing=ignore_missing) + self._delete( + _availability_zone.AvailabilityZone, + availability_zone, + ignore_missing=ignore_missing, + ) def find_availability_zone(self, name_or_id, ignore_missing=True): """Find a single availability zone @@ -1192,8 +1257,11 @@ def find_availability_zone(self, name_or_id, ignore_missing=True): :returns: ``None`` """ - return self._find(_availability_zone.AvailabilityZone, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _availability_zone.AvailabilityZone, + name_or_id, + ignore_missing=ignore_missing, + ) def update_availability_zone(self, availability_zone, **attrs): """Update an availability zone @@ -1209,5 +1277,6 @@ def update_availability_zone(self, availability_zone, **attrs): :rtype: :class:`~openstack.load_balancer.v2.availability_zone.AvailabilityZone` """ - return self._update(_availability_zone.AvailabilityZone, - availability_zone, **attrs) + return self._update( + _availability_zone.AvailabilityZone, availability_zone, **attrs + ) diff --git a/openstack/load_balancer/v2/amphora.py b/openstack/load_balancer/v2/amphora.py index f3e02b2f9..ac90c7c80 100644 --- a/openstack/load_balancer/v2/amphora.py +++ b/openstack/load_balancer/v2/amphora.py @@ -28,10 +28,26 @@ class Amphora(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'id', 'loadbalancer_id', 'compute_id', 'lb_network_ip', 'vrrp_ip', - 'ha_ip', 'vrrp_port_id', 'ha_port_id', 'cert_expiration', 'cert_busy', - 'role', 'status', 'vrrp_interface', 'vrrp_id', 'vrrp_priority', - 'cached_zone', 'created_at', 'updated_at', 'image_id', 'image_id' + 'id', + 'loadbalancer_id', + 'compute_id', + 'lb_network_ip', + 'vrrp_ip', + 'ha_ip', + 'vrrp_port_id', + 'ha_port_id', + 'cert_expiration', + 'cert_busy', + 'role', + 'status', + 'vrrp_interface', + 'vrrp_id', + 'vrrp_priority', + 'cached_zone', + 'created_at', + 'updated_at', + 'image_id', + 'image_id', ) # Properties @@ -99,7 +115,8 @@ class AmphoraConfig(resource.Resource): # way to pass has_body into this function, so overriding the method here. def commit(self, session, base_path=None): return super(AmphoraConfig, self).commit( - session, base_path=base_path, has_body=False) + session, base_path=base_path, has_body=False + ) class AmphoraFailover(resource.Resource): @@ -123,4 +140,5 @@ class AmphoraFailover(resource.Resource): # way to pass has_body into this function, so overriding the method here. def commit(self, session, base_path=None): return super(AmphoraFailover, self).commit( - session, base_path=base_path, has_body=False) + session, base_path=base_path, has_body=False + ) diff --git a/openstack/load_balancer/v2/availability_zone.py b/openstack/load_balancer/v2/availability_zone.py index 9be7a4167..4037fbf52 100644 --- a/openstack/load_balancer/v2/availability_zone.py +++ b/openstack/load_balancer/v2/availability_zone.py @@ -27,8 +27,10 @@ class AvailabilityZone(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'name', 'description', 'availability_zone_profile_id', - is_enabled='enabled' + 'name', + 'description', + 'availability_zone_profile_id', + is_enabled='enabled', ) # Properties @@ -38,6 +40,7 @@ class AvailabilityZone(resource.Resource): description = resource.Body('description') #: The associated availability zone profile ID availability_zone_profile_id = resource.Body( - 'availability_zone_profile_id') + 'availability_zone_profile_id' + ) #: Whether the availability zone is enabled for use or not. is_enabled = resource.Body('enabled') diff --git a/openstack/load_balancer/v2/health_monitor.py b/openstack/load_balancer/v2/health_monitor.py index 58b5b969f..6622c775f 100644 --- a/openstack/load_balancer/v2/health_monitor.py +++ b/openstack/load_balancer/v2/health_monitor.py @@ -26,10 +26,22 @@ class HealthMonitor(resource.Resource, tag.TagMixin): allow_commit = True _query_mapping = resource.QueryParameters( - 'name', 'created_at', 'updated_at', 'delay', 'expected_codes', - 'http_method', 'max_retries', 'max_retries_down', 'pool_id', - 'provisioning_status', 'operating_status', 'timeout', - 'project_id', 'type', 'url_path', is_admin_state_up='admin_state_up', + 'name', + 'created_at', + 'updated_at', + 'delay', + 'expected_codes', + 'http_method', + 'max_retries', + 'max_retries_down', + 'pool_id', + 'provisioning_status', + 'operating_status', + 'timeout', + 'project_id', + 'type', + 'url_path', + is_admin_state_up='admin_state_up', **tag.TagMixin._tag_query_parameters ) diff --git a/openstack/load_balancer/v2/l7_policy.py b/openstack/load_balancer/v2/l7_policy.py index 3587db9b1..35d2917dc 100644 --- a/openstack/load_balancer/v2/l7_policy.py +++ b/openstack/load_balancer/v2/l7_policy.py @@ -26,9 +26,17 @@ class L7Policy(resource.Resource, tag.TagMixin): allow_delete = True _query_mapping = resource.QueryParameters( - 'action', 'description', 'listener_id', 'name', 'position', - 'redirect_pool_id', 'redirect_url', 'provisioning_status', - 'operating_status', 'redirect_prefix', 'project_id', + 'action', + 'description', + 'listener_id', + 'name', + 'position', + 'redirect_pool_id', + 'redirect_url', + 'provisioning_status', + 'operating_status', + 'redirect_prefix', + 'project_id', is_admin_state_up='admin_state_up', **tag.TagMixin._tag_query_parameters ) diff --git a/openstack/load_balancer/v2/l7_rule.py b/openstack/load_balancer/v2/l7_rule.py index c2585b7e6..188f878ef 100644 --- a/openstack/load_balancer/v2/l7_rule.py +++ b/openstack/load_balancer/v2/l7_rule.py @@ -26,10 +26,19 @@ class L7Rule(resource.Resource, tag.TagMixin): allow_delete = True _query_mapping = resource.QueryParameters( - 'compare_type', 'created_at', 'invert', 'key', 'project_id', - 'provisioning_status', 'type', 'updated_at', 'rule_value', - 'operating_status', is_admin_state_up='admin_state_up', - l7_policy_id='l7policy_id', **tag.TagMixin._tag_query_parameters + 'compare_type', + 'created_at', + 'invert', + 'key', + 'project_id', + 'provisioning_status', + 'type', + 'updated_at', + 'rule_value', + 'operating_status', + is_admin_state_up='admin_state_up', + l7_policy_id='l7policy_id', + **tag.TagMixin._tag_query_parameters ) #: Properties diff --git a/openstack/load_balancer/v2/listener.py b/openstack/load_balancer/v2/listener.py index 2dcded468..1b71b85b4 100644 --- a/openstack/load_balancer/v2/listener.py +++ b/openstack/load_balancer/v2/listener.py @@ -26,13 +26,29 @@ class Listener(resource.Resource, tag.TagMixin): allow_list = True _query_mapping = resource.QueryParameters( - 'connection_limit', 'default_pool_id', 'default_tls_container_ref', - 'description', 'name', 'project_id', 'protocol', 'protocol_port', - 'created_at', 'updated_at', 'provisioning_status', 'operating_status', - 'sni_container_refs', 'insert_headers', 'load_balancer_id', - 'timeout_client_data', 'timeout_member_connect', - 'timeout_member_data', 'timeout_tcp_inspect', 'allowed_cidrs', - 'tls_ciphers', 'tls_versions', 'alpn_protocols', + 'connection_limit', + 'default_pool_id', + 'default_tls_container_ref', + 'description', + 'name', + 'project_id', + 'protocol', + 'protocol_port', + 'created_at', + 'updated_at', + 'provisioning_status', + 'operating_status', + 'sni_container_refs', + 'insert_headers', + 'load_balancer_id', + 'timeout_client_data', + 'timeout_member_connect', + 'timeout_member_data', + 'timeout_tcp_inspect', + 'allowed_cidrs', + 'tls_ciphers', + 'tls_versions', + 'alpn_protocols', is_admin_state_up='admin_state_up', **tag.TagMixin._tag_query_parameters ) diff --git a/openstack/load_balancer/v2/load_balancer.py b/openstack/load_balancer/v2/load_balancer.py index d54e560f5..34cc39be8 100644 --- a/openstack/load_balancer/v2/load_balancer.py +++ b/openstack/load_balancer/v2/load_balancer.py @@ -26,10 +26,20 @@ class LoadBalancer(resource.Resource, tag.TagMixin): allow_list = True _query_mapping = resource.QueryParameters( - 'description', 'flavor_id', 'name', 'project_id', 'provider', - 'vip_address', 'vip_network_id', 'vip_port_id', 'vip_subnet_id', - 'vip_qos_policy_id', 'provisioning_status', 'operating_status', - 'availability_zone', is_admin_state_up='admin_state_up', + 'description', + 'flavor_id', + 'name', + 'project_id', + 'provider', + 'vip_address', + 'vip_network_id', + 'vip_port_id', + 'vip_subnet_id', + 'vip_qos_policy_id', + 'provisioning_status', + 'operating_status', + 'availability_zone', + is_admin_state_up='admin_state_up', **tag.TagMixin._tag_query_parameters ) @@ -76,14 +86,17 @@ class LoadBalancer(resource.Resource, tag.TagMixin): def delete(self, session, error_message=None): request = self._prepare_request() params = {} - if (hasattr(self, 'cascade') and isinstance(self.cascade, bool) - and self.cascade): + if ( + hasattr(self, 'cascade') + and isinstance(self.cascade, bool) + and self.cascade + ): params['cascade'] = True - response = session.delete(request.url, - params=params) + response = session.delete(request.url, params=params) - self._translate_response(response, has_body=False, - error_message=error_message) + self._translate_response( + response, has_body=False, error_message=error_message + ) return self @@ -134,4 +147,5 @@ class LoadBalancerFailover(resource.Resource): # way to pass has_body into this function, so overriding the method here. def commit(self, session, base_path=None): return super(LoadBalancerFailover, self).commit( - session, base_path=base_path, has_body=False) + session, base_path=base_path, has_body=False + ) diff --git a/openstack/load_balancer/v2/member.py b/openstack/load_balancer/v2/member.py index 32ca43bda..4e1d9423e 100644 --- a/openstack/load_balancer/v2/member.py +++ b/openstack/load_balancer/v2/member.py @@ -26,9 +26,19 @@ class Member(resource.Resource, tag.TagMixin): allow_list = True _query_mapping = resource.QueryParameters( - 'address', 'name', 'protocol_port', 'subnet_id', 'weight', - 'created_at', 'updated_at', 'provisioning_status', 'operating_status', - 'project_id', 'monitor_address', 'monitor_port', 'backup', + 'address', + 'name', + 'protocol_port', + 'subnet_id', + 'weight', + 'created_at', + 'updated_at', + 'provisioning_status', + 'operating_status', + 'project_id', + 'monitor_address', + 'monitor_port', + 'backup', is_admin_state_up='admin_state_up', **tag.TagMixin._tag_query_parameters ) diff --git a/openstack/load_balancer/v2/pool.py b/openstack/load_balancer/v2/pool.py index 9af3d18a9..ac1422189 100644 --- a/openstack/load_balancer/v2/pool.py +++ b/openstack/load_balancer/v2/pool.py @@ -26,10 +26,22 @@ class Pool(resource.Resource, tag.TagMixin): allow_commit = True _query_mapping = resource.QueryParameters( - 'health_monitor_id', 'lb_algorithm', 'listener_id', 'loadbalancer_id', - 'description', 'name', 'project_id', 'protocol', - 'created_at', 'updated_at', 'provisioning_status', 'operating_status', - 'tls_enabled', 'tls_ciphers', 'tls_versions', 'alpn_protocols', + 'health_monitor_id', + 'lb_algorithm', + 'listener_id', + 'loadbalancer_id', + 'description', + 'name', + 'project_id', + 'protocol', + 'created_at', + 'updated_at', + 'provisioning_status', + 'operating_status', + 'tls_enabled', + 'tls_ciphers', + 'tls_versions', + 'alpn_protocols', is_admin_state_up='admin_state_up', **tag.TagMixin._tag_query_parameters ) diff --git a/openstack/load_balancer/v2/quota.py b/openstack/load_balancer/v2/quota.py index 8a0106970..8b46c5475 100644 --- a/openstack/load_balancer/v2/quota.py +++ b/openstack/load_balancer/v2/quota.py @@ -41,11 +41,12 @@ class Quota(resource.Resource): #: The ID of the project this quota is associated with. project_id = resource.Body('project_id', alternate_id=True) - def _prepare_request(self, requires_id=True, - base_path=None, prepend_key=False, **kwargs): - _request = super(Quota, self)._prepare_request(requires_id, - prepend_key, - base_path=base_path) + def _prepare_request( + self, requires_id=True, base_path=None, prepend_key=False, **kwargs + ): + _request = super(Quota, self)._prepare_request( + requires_id, prepend_key, base_path=base_path + ) if self.resource_key in _request.body: _body = _request.body[self.resource_key] else: diff --git a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py index 72d4d4bec..f9f819a98 100644 --- a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py +++ b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py @@ -84,56 +84,73 @@ def setUp(self): self.VIP_SUBNET_ID = subnets[0].id self.PROJECT_ID = self.conn.session.get_project_id() test_quota = self.conn.load_balancer.update_quota( - self.PROJECT_ID, **{'load_balancer': 100, - 'pool': 100, - 'listener': 100, - 'health_monitor': 100, - 'member': 100}) + self.PROJECT_ID, + **{ + 'load_balancer': 100, + 'pool': 100, + 'listener': 100, + 'health_monitor': 100, + 'member': 100, + } + ) assert isinstance(test_quota, quota.Quota) self.assertEqual(self.PROJECT_ID, test_quota.id) test_flavor_profile = self.conn.load_balancer.create_flavor_profile( - name=self.FLAVOR_PROFILE_NAME, provider_name=self.AMPHORA, - flavor_data=self.FLAVOR_DATA) + name=self.FLAVOR_PROFILE_NAME, + provider_name=self.AMPHORA, + flavor_data=self.FLAVOR_DATA, + ) assert isinstance(test_flavor_profile, flavor_profile.FlavorProfile) self.assertEqual(self.FLAVOR_PROFILE_NAME, test_flavor_profile.name) self.FLAVOR_PROFILE_ID = test_flavor_profile.id test_flavor = self.conn.load_balancer.create_flavor( - name=self.FLAVOR_NAME, flavor_profile_id=self.FLAVOR_PROFILE_ID, - is_enabled=True, description=self.DESCRIPTION) + name=self.FLAVOR_NAME, + flavor_profile_id=self.FLAVOR_PROFILE_ID, + is_enabled=True, + description=self.DESCRIPTION, + ) assert isinstance(test_flavor, flavor.Flavor) self.assertEqual(self.FLAVOR_NAME, test_flavor.name) self.FLAVOR_ID = test_flavor.id - test_az_profile = \ + test_az_profile = ( self.conn.load_balancer.create_availability_zone_profile( name=self.AVAILABILITY_ZONE_PROFILE_NAME, provider_name=self.AMPHORA, - availability_zone_data=self.AVAILABILITY_ZONE_DATA) - assert isinstance(test_az_profile, - availability_zone_profile.AvailabilityZoneProfile) - self.assertEqual(self.AVAILABILITY_ZONE_PROFILE_NAME, - test_az_profile.name) + availability_zone_data=self.AVAILABILITY_ZONE_DATA, + ) + ) + assert isinstance( + test_az_profile, availability_zone_profile.AvailabilityZoneProfile + ) + self.assertEqual( + self.AVAILABILITY_ZONE_PROFILE_NAME, test_az_profile.name + ) self.AVAILABILITY_ZONE_PROFILE_ID = test_az_profile.id test_az = self.conn.load_balancer.create_availability_zone( name=self.AVAILABILITY_ZONE_NAME, availability_zone_profile_id=self.AVAILABILITY_ZONE_PROFILE_ID, - is_enabled=True, description=self.DESCRIPTION) + is_enabled=True, + description=self.DESCRIPTION, + ) assert isinstance(test_az, availability_zone.AvailabilityZone) self.assertEqual(self.AVAILABILITY_ZONE_NAME, test_az.name) test_lb = self.conn.load_balancer.create_load_balancer( - name=self.LB_NAME, vip_subnet_id=self.VIP_SUBNET_ID, - project_id=self.PROJECT_ID) + name=self.LB_NAME, + vip_subnet_id=self.VIP_SUBNET_ID, + project_id=self.PROJECT_ID, + ) assert isinstance(test_lb, load_balancer.LoadBalancer) self.assertEqual(self.LB_NAME, test_lb.name) # Wait for the LB to go ACTIVE. On non-virtualization enabled hosts # it can take nova up to ten minutes to boot a VM. self.conn.load_balancer.wait_for_load_balancer( - test_lb.id, interval=1, - wait=self._wait_for_timeout) + test_lb.id, interval=1, wait=self._wait_for_timeout + ) self.LB_ID = test_lb.id amphorae = self.conn.load_balancer.amphorae(loadbalancer_id=self.LB_ID) @@ -141,113 +158,156 @@ def setUp(self): self.AMPHORA_ID = amp.id test_listener = self.conn.load_balancer.create_listener( - name=self.LISTENER_NAME, protocol=self.PROTOCOL, - protocol_port=self.PROTOCOL_PORT, loadbalancer_id=self.LB_ID) + name=self.LISTENER_NAME, + protocol=self.PROTOCOL, + protocol_port=self.PROTOCOL_PORT, + loadbalancer_id=self.LB_ID, + ) assert isinstance(test_listener, listener.Listener) self.assertEqual(self.LISTENER_NAME, test_listener.name) self.LISTENER_ID = test_listener.id self.conn.load_balancer.wait_for_load_balancer( - self.LB_ID, wait=self._wait_for_timeout) + self.LB_ID, wait=self._wait_for_timeout + ) test_pool = self.conn.load_balancer.create_pool( - name=self.POOL_NAME, protocol=self.PROTOCOL, - lb_algorithm=self.LB_ALGORITHM, listener_id=self.LISTENER_ID) + name=self.POOL_NAME, + protocol=self.PROTOCOL, + lb_algorithm=self.LB_ALGORITHM, + listener_id=self.LISTENER_ID, + ) assert isinstance(test_pool, pool.Pool) self.assertEqual(self.POOL_NAME, test_pool.name) self.POOL_ID = test_pool.id self.conn.load_balancer.wait_for_load_balancer( - self.LB_ID, wait=self._wait_for_timeout) + self.LB_ID, wait=self._wait_for_timeout + ) test_member = self.conn.load_balancer.create_member( - pool=self.POOL_ID, name=self.MEMBER_NAME, + pool=self.POOL_ID, + name=self.MEMBER_NAME, address=self.MEMBER_ADDRESS, - protocol_port=self.PROTOCOL_PORT, weight=self.WEIGHT) + protocol_port=self.PROTOCOL_PORT, + weight=self.WEIGHT, + ) assert isinstance(test_member, member.Member) self.assertEqual(self.MEMBER_NAME, test_member.name) self.MEMBER_ID = test_member.id self.conn.load_balancer.wait_for_load_balancer( - self.LB_ID, wait=self._wait_for_timeout) + self.LB_ID, wait=self._wait_for_timeout + ) test_hm = self.conn.load_balancer.create_health_monitor( - pool_id=self.POOL_ID, name=self.HM_NAME, delay=self.DELAY, - timeout=self.TIMEOUT, max_retries=self.MAX_RETRY, - type=self.HM_TYPE) + pool_id=self.POOL_ID, + name=self.HM_NAME, + delay=self.DELAY, + timeout=self.TIMEOUT, + max_retries=self.MAX_RETRY, + type=self.HM_TYPE, + ) assert isinstance(test_hm, health_monitor.HealthMonitor) self.assertEqual(self.HM_NAME, test_hm.name) self.HM_ID = test_hm.id self.conn.load_balancer.wait_for_load_balancer( - self.LB_ID, wait=self._wait_for_timeout) + self.LB_ID, wait=self._wait_for_timeout + ) test_l7policy = self.conn.load_balancer.create_l7_policy( - listener_id=self.LISTENER_ID, name=self.L7POLICY_NAME, - action=self.ACTION, redirect_url=self.REDIRECT_URL) + listener_id=self.LISTENER_ID, + name=self.L7POLICY_NAME, + action=self.ACTION, + redirect_url=self.REDIRECT_URL, + ) assert isinstance(test_l7policy, l7_policy.L7Policy) self.assertEqual(self.L7POLICY_NAME, test_l7policy.name) self.L7POLICY_ID = test_l7policy.id self.conn.load_balancer.wait_for_load_balancer( - self.LB_ID, wait=self._wait_for_timeout) + self.LB_ID, wait=self._wait_for_timeout + ) test_l7rule = self.conn.load_balancer.create_l7_rule( - l7_policy=self.L7POLICY_ID, compare_type=self.COMPARE_TYPE, - type=self.L7RULE_TYPE, value=self.L7RULE_VALUE) + l7_policy=self.L7POLICY_ID, + compare_type=self.COMPARE_TYPE, + type=self.L7RULE_TYPE, + value=self.L7RULE_VALUE, + ) assert isinstance(test_l7rule, l7_rule.L7Rule) self.assertEqual(self.COMPARE_TYPE, test_l7rule.compare_type) self.L7RULE_ID = test_l7rule.id self.conn.load_balancer.wait_for_load_balancer( - self.LB_ID, wait=self._wait_for_timeout) + self.LB_ID, wait=self._wait_for_timeout + ) def tearDown(self): self.conn.load_balancer.get_load_balancer(self.LB_ID) self.conn.load_balancer.wait_for_load_balancer( - self.LB_ID, wait=self._wait_for_timeout) + self.LB_ID, wait=self._wait_for_timeout + ) - self.conn.load_balancer.delete_quota(self.PROJECT_ID, - ignore_missing=False) + self.conn.load_balancer.delete_quota( + self.PROJECT_ID, ignore_missing=False + ) self.conn.load_balancer.delete_l7_rule( - self.L7RULE_ID, l7_policy=self.L7POLICY_ID, ignore_missing=False) + self.L7RULE_ID, l7_policy=self.L7POLICY_ID, ignore_missing=False + ) self.conn.load_balancer.wait_for_load_balancer( - self.LB_ID, wait=self._wait_for_timeout) + self.LB_ID, wait=self._wait_for_timeout + ) self.conn.load_balancer.delete_l7_policy( - self.L7POLICY_ID, ignore_missing=False) + self.L7POLICY_ID, ignore_missing=False + ) self.conn.load_balancer.wait_for_load_balancer( - self.LB_ID, wait=self._wait_for_timeout) + self.LB_ID, wait=self._wait_for_timeout + ) self.conn.load_balancer.delete_health_monitor( - self.HM_ID, ignore_missing=False) + self.HM_ID, ignore_missing=False + ) self.conn.load_balancer.wait_for_load_balancer( - self.LB_ID, wait=self._wait_for_timeout) + self.LB_ID, wait=self._wait_for_timeout + ) self.conn.load_balancer.delete_member( - self.MEMBER_ID, self.POOL_ID, ignore_missing=False) + self.MEMBER_ID, self.POOL_ID, ignore_missing=False + ) self.conn.load_balancer.wait_for_load_balancer( - self.LB_ID, wait=self._wait_for_timeout) + self.LB_ID, wait=self._wait_for_timeout + ) self.conn.load_balancer.delete_pool(self.POOL_ID, ignore_missing=False) self.conn.load_balancer.wait_for_load_balancer( - self.LB_ID, wait=self._wait_for_timeout) + self.LB_ID, wait=self._wait_for_timeout + ) - self.conn.load_balancer.delete_listener(self.LISTENER_ID, - ignore_missing=False) + self.conn.load_balancer.delete_listener( + self.LISTENER_ID, ignore_missing=False + ) self.conn.load_balancer.wait_for_load_balancer( - self.LB_ID, wait=self._wait_for_timeout) + self.LB_ID, wait=self._wait_for_timeout + ) self.conn.load_balancer.delete_load_balancer( - self.LB_ID, ignore_missing=False) + self.LB_ID, ignore_missing=False + ) super(TestLoadBalancer, self).tearDown() - self.conn.load_balancer.delete_flavor(self.FLAVOR_ID, - ignore_missing=False) + self.conn.load_balancer.delete_flavor( + self.FLAVOR_ID, ignore_missing=False + ) - self.conn.load_balancer.delete_flavor_profile(self.FLAVOR_PROFILE_ID, - ignore_missing=False) + self.conn.load_balancer.delete_flavor_profile( + self.FLAVOR_PROFILE_ID, ignore_missing=False + ) self.conn.load_balancer.delete_availability_zone( - self.AVAILABILITY_ZONE_NAME, ignore_missing=False) + self.AVAILABILITY_ZONE_NAME, ignore_missing=False + ) self.conn.load_balancer.delete_availability_zone_profile( - self.AVAILABILITY_ZONE_PROFILE_ID, ignore_missing=False) + self.AVAILABILITY_ZONE_PROFILE_ID, ignore_missing=False + ) def test_lb_find(self): test_lb = self.conn.load_balancer.find_load_balancer(self.LB_NAME) @@ -261,7 +321,8 @@ def test_lb_get(self): def test_lb_get_stats(self): test_lb_stats = self.conn.load_balancer.get_load_balancer_statistics( - self.LB_ID) + self.LB_ID + ) self.assertEqual(0, test_lb_stats.active_connections) self.assertEqual(0, test_lb_stats.bytes_in) self.assertEqual(0, test_lb_stats.bytes_out) @@ -274,29 +335,35 @@ def test_lb_list(self): def test_lb_update(self): self.conn.load_balancer.update_load_balancer( - self.LB_ID, name=self.UPDATE_NAME) + self.LB_ID, name=self.UPDATE_NAME + ) self.conn.load_balancer.wait_for_load_balancer( - self.LB_ID, wait=self._wait_for_timeout) + self.LB_ID, wait=self._wait_for_timeout + ) test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) self.assertEqual(self.UPDATE_NAME, test_lb.name) self.conn.load_balancer.update_load_balancer( - self.LB_ID, name=self.LB_NAME) + self.LB_ID, name=self.LB_NAME + ) self.conn.load_balancer.wait_for_load_balancer( - self.LB_ID, wait=self._wait_for_timeout) + self.LB_ID, wait=self._wait_for_timeout + ) test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) self.assertEqual(self.LB_NAME, test_lb.name) def test_lb_failover(self): self.conn.load_balancer.failover_load_balancer(self.LB_ID) self.conn.load_balancer.wait_for_load_balancer( - self.LB_ID, wait=self._wait_for_timeout) + self.LB_ID, wait=self._wait_for_timeout + ) test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) self.assertEqual(self.LB_NAME, test_lb.name) def test_listener_find(self): test_listener = self.conn.load_balancer.find_listener( - self.LISTENER_NAME) + self.LISTENER_NAME + ) self.assertEqual(self.LISTENER_ID, test_listener.id) def test_listener_get(self): @@ -308,7 +375,8 @@ def test_listener_get(self): def test_listener_get_stats(self): test_listener_stats = self.conn.load_balancer.get_listener_statistics( - self.LISTENER_ID) + self.LISTENER_ID + ) self.assertEqual(0, test_listener_stats.active_connections) self.assertEqual(0, test_listener_stats.bytes_in) self.assertEqual(0, test_listener_stats.bytes_out) @@ -323,16 +391,20 @@ def test_listener_update(self): self.conn.load_balancer.get_load_balancer(self.LB_ID) self.conn.load_balancer.update_listener( - self.LISTENER_ID, name=self.UPDATE_NAME) + self.LISTENER_ID, name=self.UPDATE_NAME + ) self.conn.load_balancer.wait_for_load_balancer( - self.LB_ID, wait=self._wait_for_timeout) + self.LB_ID, wait=self._wait_for_timeout + ) test_listener = self.conn.load_balancer.get_listener(self.LISTENER_ID) self.assertEqual(self.UPDATE_NAME, test_listener.name) self.conn.load_balancer.update_listener( - self.LISTENER_ID, name=self.LISTENER_NAME) + self.LISTENER_ID, name=self.LISTENER_NAME + ) self.conn.load_balancer.wait_for_load_balancer( - self.LB_ID, wait=self._wait_for_timeout) + self.LB_ID, wait=self._wait_for_timeout + ) test_listener = self.conn.load_balancer.get_listener(self.LISTENER_ID) self.assertEqual(self.LISTENER_NAME, test_listener.name) @@ -353,28 +425,32 @@ def test_pool_list(self): def test_pool_update(self): self.conn.load_balancer.get_load_balancer(self.LB_ID) - self.conn.load_balancer.update_pool(self.POOL_ID, - name=self.UPDATE_NAME) + self.conn.load_balancer.update_pool( + self.POOL_ID, name=self.UPDATE_NAME + ) self.conn.load_balancer.wait_for_load_balancer( - self.LB_ID, wait=self._wait_for_timeout) + self.LB_ID, wait=self._wait_for_timeout + ) test_pool = self.conn.load_balancer.get_pool(self.POOL_ID) self.assertEqual(self.UPDATE_NAME, test_pool.name) - self.conn.load_balancer.update_pool(self.POOL_ID, - name=self.POOL_NAME) + self.conn.load_balancer.update_pool(self.POOL_ID, name=self.POOL_NAME) self.conn.load_balancer.wait_for_load_balancer( - self.LB_ID, wait=self._wait_for_timeout) + self.LB_ID, wait=self._wait_for_timeout + ) test_pool = self.conn.load_balancer.get_pool(self.POOL_ID) self.assertEqual(self.POOL_NAME, test_pool.name) def test_member_find(self): - test_member = self.conn.load_balancer.find_member(self.MEMBER_NAME, - self.POOL_ID) + test_member = self.conn.load_balancer.find_member( + self.MEMBER_NAME, self.POOL_ID + ) self.assertEqual(self.MEMBER_ID, test_member.id) def test_member_get(self): - test_member = self.conn.load_balancer.get_member(self.MEMBER_ID, - self.POOL_ID) + test_member = self.conn.load_balancer.get_member( + self.MEMBER_ID, self.POOL_ID + ) self.assertEqual(self.MEMBER_NAME, test_member.name) self.assertEqual(self.MEMBER_ID, test_member.id) self.assertEqual(self.MEMBER_ADDRESS, test_member.address) @@ -382,27 +458,34 @@ def test_member_get(self): self.assertEqual(self.WEIGHT, test_member.weight) def test_member_list(self): - names = [mb.name for mb in self.conn.load_balancer.members( - self.POOL_ID)] + names = [ + mb.name for mb in self.conn.load_balancer.members(self.POOL_ID) + ] self.assertIn(self.MEMBER_NAME, names) def test_member_update(self): self.conn.load_balancer.get_load_balancer(self.LB_ID) - self.conn.load_balancer.update_member(self.MEMBER_ID, self.POOL_ID, - name=self.UPDATE_NAME) + self.conn.load_balancer.update_member( + self.MEMBER_ID, self.POOL_ID, name=self.UPDATE_NAME + ) self.conn.load_balancer.wait_for_load_balancer( - self.LB_ID, wait=self._wait_for_timeout) - test_member = self.conn.load_balancer.get_member(self.MEMBER_ID, - self.POOL_ID) + self.LB_ID, wait=self._wait_for_timeout + ) + test_member = self.conn.load_balancer.get_member( + self.MEMBER_ID, self.POOL_ID + ) self.assertEqual(self.UPDATE_NAME, test_member.name) - self.conn.load_balancer.update_member(self.MEMBER_ID, self.POOL_ID, - name=self.MEMBER_NAME) + self.conn.load_balancer.update_member( + self.MEMBER_ID, self.POOL_ID, name=self.MEMBER_NAME + ) self.conn.load_balancer.wait_for_load_balancer( - self.LB_ID, wait=self._wait_for_timeout) - test_member = self.conn.load_balancer.get_member(self.MEMBER_ID, - self.POOL_ID) + self.LB_ID, wait=self._wait_for_timeout + ) + test_member = self.conn.load_balancer.get_member( + self.MEMBER_ID, self.POOL_ID + ) self.assertEqual(self.MEMBER_NAME, test_member.name) def test_health_monitor_find(self): @@ -425,28 +508,34 @@ def test_health_monitor_list(self): def test_health_monitor_update(self): self.conn.load_balancer.get_load_balancer(self.LB_ID) - self.conn.load_balancer.update_health_monitor(self.HM_ID, - name=self.UPDATE_NAME) + self.conn.load_balancer.update_health_monitor( + self.HM_ID, name=self.UPDATE_NAME + ) self.conn.load_balancer.wait_for_load_balancer( - self.LB_ID, wait=self._wait_for_timeout) + self.LB_ID, wait=self._wait_for_timeout + ) test_hm = self.conn.load_balancer.get_health_monitor(self.HM_ID) self.assertEqual(self.UPDATE_NAME, test_hm.name) - self.conn.load_balancer.update_health_monitor(self.HM_ID, - name=self.HM_NAME) + self.conn.load_balancer.update_health_monitor( + self.HM_ID, name=self.HM_NAME + ) self.conn.load_balancer.wait_for_load_balancer( - self.LB_ID, wait=self._wait_for_timeout) + self.LB_ID, wait=self._wait_for_timeout + ) test_hm = self.conn.load_balancer.get_health_monitor(self.HM_ID) self.assertEqual(self.HM_NAME, test_hm.name) def test_l7_policy_find(self): test_l7_policy = self.conn.load_balancer.find_l7_policy( - self.L7POLICY_NAME) + self.L7POLICY_NAME + ) self.assertEqual(self.L7POLICY_ID, test_l7_policy.id) def test_l7_policy_get(self): test_l7_policy = self.conn.load_balancer.get_l7_policy( - self.L7POLICY_ID) + self.L7POLICY_ID + ) self.assertEqual(self.L7POLICY_NAME, test_l7_policy.name) self.assertEqual(self.L7POLICY_ID, test_l7_policy.id) self.assertEqual(self.ACTION, test_l7_policy.action) @@ -459,59 +548,80 @@ def test_l7_policy_update(self): self.conn.load_balancer.get_load_balancer(self.LB_ID) self.conn.load_balancer.update_l7_policy( - self.L7POLICY_ID, name=self.UPDATE_NAME) + self.L7POLICY_ID, name=self.UPDATE_NAME + ) self.conn.load_balancer.wait_for_load_balancer( - self.LB_ID, wait=self._wait_for_timeout) + self.LB_ID, wait=self._wait_for_timeout + ) test_l7_policy = self.conn.load_balancer.get_l7_policy( - self.L7POLICY_ID) + self.L7POLICY_ID + ) self.assertEqual(self.UPDATE_NAME, test_l7_policy.name) - self.conn.load_balancer.update_l7_policy(self.L7POLICY_ID, - name=self.L7POLICY_NAME) + self.conn.load_balancer.update_l7_policy( + self.L7POLICY_ID, name=self.L7POLICY_NAME + ) self.conn.load_balancer.wait_for_load_balancer( - self.LB_ID, wait=self._wait_for_timeout) + self.LB_ID, wait=self._wait_for_timeout + ) test_l7_policy = self.conn.load_balancer.get_l7_policy( - self.L7POLICY_ID) + self.L7POLICY_ID + ) self.assertEqual(self.L7POLICY_NAME, test_l7_policy.name) def test_l7_rule_find(self): test_l7_rule = self.conn.load_balancer.find_l7_rule( - self.L7RULE_ID, self.L7POLICY_ID) + self.L7RULE_ID, self.L7POLICY_ID + ) self.assertEqual(self.L7RULE_ID, test_l7_rule.id) self.assertEqual(self.L7RULE_TYPE, test_l7_rule.type) def test_l7_rule_get(self): test_l7_rule = self.conn.load_balancer.get_l7_rule( - self.L7RULE_ID, l7_policy=self.L7POLICY_ID) + self.L7RULE_ID, l7_policy=self.L7POLICY_ID + ) self.assertEqual(self.L7RULE_ID, test_l7_rule.id) self.assertEqual(self.COMPARE_TYPE, test_l7_rule.compare_type) self.assertEqual(self.L7RULE_TYPE, test_l7_rule.type) self.assertEqual(self.L7RULE_VALUE, test_l7_rule.rule_value) def test_l7_rule_list(self): - ids = [l7.id for l7 in self.conn.load_balancer.l7_rules( - l7_policy=self.L7POLICY_ID)] + ids = [ + l7.id + for l7 in self.conn.load_balancer.l7_rules( + l7_policy=self.L7POLICY_ID + ) + ] self.assertIn(self.L7RULE_ID, ids) def test_l7_rule_update(self): self.conn.load_balancer.get_load_balancer(self.LB_ID) - self.conn.load_balancer.update_l7_rule(self.L7RULE_ID, - l7_policy=self.L7POLICY_ID, - rule_value=self.UPDATE_NAME) + self.conn.load_balancer.update_l7_rule( + self.L7RULE_ID, + l7_policy=self.L7POLICY_ID, + rule_value=self.UPDATE_NAME, + ) self.conn.load_balancer.wait_for_load_balancer( - self.LB_ID, wait=self._wait_for_timeout) + self.LB_ID, wait=self._wait_for_timeout + ) test_l7_rule = self.conn.load_balancer.get_l7_rule( - self.L7RULE_ID, l7_policy=self.L7POLICY_ID) + self.L7RULE_ID, l7_policy=self.L7POLICY_ID + ) self.assertEqual(self.UPDATE_NAME, test_l7_rule.rule_value) - self.conn.load_balancer.update_l7_rule(self.L7RULE_ID, - l7_policy=self.L7POLICY_ID, - rule_value=self.L7RULE_VALUE) + self.conn.load_balancer.update_l7_rule( + self.L7RULE_ID, + l7_policy=self.L7POLICY_ID, + rule_value=self.L7RULE_VALUE, + ) self.conn.load_balancer.wait_for_load_balancer( - self.LB_ID, wait=self._wait_for_timeout) + self.LB_ID, wait=self._wait_for_timeout + ) test_l7_rule = self.conn.load_balancer.get_l7_rule( - self.L7RULE_ID, l7_policy=self.L7POLICY_ID,) + self.L7RULE_ID, + l7_policy=self.L7POLICY_ID, + ) self.assertEqual(self.L7RULE_VALUE, test_l7_rule.rule_value) def test_quota_list(self): @@ -527,7 +637,8 @@ def test_quota_update(self): for project_quota in self.conn.load_balancer.quotas(): self.conn.load_balancer.update_quota(project_quota, **attrs) new_quota = self.conn.load_balancer.get_quota( - project_quota.project_id) + project_quota.project_id + ) self.assertEqual(12345, new_quota.load_balancers) self.assertEqual(67890, new_quota.pools) @@ -538,23 +649,28 @@ def test_providers(self): providers = self.conn.load_balancer.providers() # Make sure our default provider is in the list self.assertTrue( - any(prov['name'] == self.AMPHORA for prov in providers)) + any(prov['name'] == self.AMPHORA for prov in providers) + ) def test_provider_flavor_capabilities(self): capabilities = self.conn.load_balancer.provider_flavor_capabilities( - self.AMPHORA) + self.AMPHORA + ) # Make sure a known capability is in the default provider - self.assertTrue(any( - cap['name'] == 'loadbalancer_topology' for cap in capabilities)) + self.assertTrue( + any(cap['name'] == 'loadbalancer_topology' for cap in capabilities) + ) def test_flavor_profile_find(self): test_profile = self.conn.load_balancer.find_flavor_profile( - self.FLAVOR_PROFILE_NAME) + self.FLAVOR_PROFILE_NAME + ) self.assertEqual(self.FLAVOR_PROFILE_ID, test_profile.id) def test_flavor_profile_get(self): test_flavor_profile = self.conn.load_balancer.get_flavor_profile( - self.FLAVOR_PROFILE_ID) + self.FLAVOR_PROFILE_ID + ) self.assertEqual(self.FLAVOR_PROFILE_NAME, test_flavor_profile.name) self.assertEqual(self.FLAVOR_PROFILE_ID, test_flavor_profile.id) self.assertEqual(self.AMPHORA, test_flavor_profile.provider_name) @@ -566,15 +682,19 @@ def test_flavor_profile_list(self): def test_flavor_profile_update(self): self.conn.load_balancer.update_flavor_profile( - self.FLAVOR_PROFILE_ID, name=self.UPDATE_NAME) + self.FLAVOR_PROFILE_ID, name=self.UPDATE_NAME + ) test_flavor_profile = self.conn.load_balancer.get_flavor_profile( - self.FLAVOR_PROFILE_ID) + self.FLAVOR_PROFILE_ID + ) self.assertEqual(self.UPDATE_NAME, test_flavor_profile.name) self.conn.load_balancer.update_flavor_profile( - self.FLAVOR_PROFILE_ID, name=self.FLAVOR_PROFILE_NAME) + self.FLAVOR_PROFILE_ID, name=self.FLAVOR_PROFILE_NAME + ) test_flavor_profile = self.conn.load_balancer.get_flavor_profile( - self.FLAVOR_PROFILE_ID) + self.FLAVOR_PROFILE_ID + ) self.assertEqual(self.FLAVOR_PROFILE_NAME, test_flavor_profile.name) def test_flavor_find(self): @@ -594,12 +714,14 @@ def test_flavor_list(self): def test_flavor_update(self): self.conn.load_balancer.update_flavor( - self.FLAVOR_ID, name=self.UPDATE_NAME) + self.FLAVOR_ID, name=self.UPDATE_NAME + ) test_flavor = self.conn.load_balancer.get_flavor(self.FLAVOR_ID) self.assertEqual(self.UPDATE_NAME, test_flavor.name) self.conn.load_balancer.update_flavor( - self.FLAVOR_ID, name=self.FLAVOR_NAME) + self.FLAVOR_ID, name=self.FLAVOR_NAME + ) test_flavor = self.conn.load_balancer.get_flavor(self.FLAVOR_ID) self.assertEqual(self.FLAVOR_NAME, test_flavor.name) @@ -627,75 +749,108 @@ def test_amphora_failover(self): def test_availability_zone_profile_find(self): test_profile = self.conn.load_balancer.find_availability_zone_profile( - self.AVAILABILITY_ZONE_PROFILE_NAME) + self.AVAILABILITY_ZONE_PROFILE_NAME + ) self.assertEqual(self.AVAILABILITY_ZONE_PROFILE_ID, test_profile.id) def test_availability_zone_profile_get(self): - test_availability_zone_profile = \ + test_availability_zone_profile = ( self.conn.load_balancer.get_availability_zone_profile( - self.AVAILABILITY_ZONE_PROFILE_ID) - self.assertEqual(self.AVAILABILITY_ZONE_PROFILE_NAME, - test_availability_zone_profile.name) - self.assertEqual(self.AVAILABILITY_ZONE_PROFILE_ID, - test_availability_zone_profile.id) - self.assertEqual(self.AMPHORA, - test_availability_zone_profile.provider_name) - self.assertEqual(self.AVAILABILITY_ZONE_DATA, - test_availability_zone_profile.availability_zone_data) + self.AVAILABILITY_ZONE_PROFILE_ID + ) + ) + self.assertEqual( + self.AVAILABILITY_ZONE_PROFILE_NAME, + test_availability_zone_profile.name, + ) + self.assertEqual( + self.AVAILABILITY_ZONE_PROFILE_ID, + test_availability_zone_profile.id, + ) + self.assertEqual( + self.AMPHORA, test_availability_zone_profile.provider_name + ) + self.assertEqual( + self.AVAILABILITY_ZONE_DATA, + test_availability_zone_profile.availability_zone_data, + ) def test_availability_zone_profile_list(self): - names = [az.name for az in - self.conn.load_balancer.availability_zone_profiles()] + names = [ + az.name + for az in self.conn.load_balancer.availability_zone_profiles() + ] self.assertIn(self.AVAILABILITY_ZONE_PROFILE_NAME, names) def test_availability_zone_profile_update(self): self.conn.load_balancer.update_availability_zone_profile( - self.AVAILABILITY_ZONE_PROFILE_ID, name=self.UPDATE_NAME) - test_availability_zone_profile = \ + self.AVAILABILITY_ZONE_PROFILE_ID, name=self.UPDATE_NAME + ) + test_availability_zone_profile = ( self.conn.load_balancer.get_availability_zone_profile( - self.AVAILABILITY_ZONE_PROFILE_ID) + self.AVAILABILITY_ZONE_PROFILE_ID + ) + ) self.assertEqual(self.UPDATE_NAME, test_availability_zone_profile.name) self.conn.load_balancer.update_availability_zone_profile( self.AVAILABILITY_ZONE_PROFILE_ID, - name=self.AVAILABILITY_ZONE_PROFILE_NAME) - test_availability_zone_profile = \ + name=self.AVAILABILITY_ZONE_PROFILE_NAME, + ) + test_availability_zone_profile = ( self.conn.load_balancer.get_availability_zone_profile( - self.AVAILABILITY_ZONE_PROFILE_ID) - self.assertEqual(self.AVAILABILITY_ZONE_PROFILE_NAME, - test_availability_zone_profile.name) + self.AVAILABILITY_ZONE_PROFILE_ID + ) + ) + self.assertEqual( + self.AVAILABILITY_ZONE_PROFILE_NAME, + test_availability_zone_profile.name, + ) def test_availability_zone_find(self): - test_availability_zone = \ + test_availability_zone = ( self.conn.load_balancer.find_availability_zone( - self.AVAILABILITY_ZONE_NAME) - self.assertEqual(self.AVAILABILITY_ZONE_NAME, - test_availability_zone.name) + self.AVAILABILITY_ZONE_NAME + ) + ) + self.assertEqual( + self.AVAILABILITY_ZONE_NAME, test_availability_zone.name + ) def test_availability_zone_get(self): test_availability_zone = self.conn.load_balancer.get_availability_zone( - self.AVAILABILITY_ZONE_NAME) - self.assertEqual(self.AVAILABILITY_ZONE_NAME, - test_availability_zone.name) + self.AVAILABILITY_ZONE_NAME + ) + self.assertEqual( + self.AVAILABILITY_ZONE_NAME, test_availability_zone.name + ) self.assertEqual(self.DESCRIPTION, test_availability_zone.description) - self.assertEqual(self.AVAILABILITY_ZONE_PROFILE_ID, - test_availability_zone.availability_zone_profile_id) + self.assertEqual( + self.AVAILABILITY_ZONE_PROFILE_ID, + test_availability_zone.availability_zone_profile_id, + ) def test_availability_zone_list(self): - names = [az.name for az in - self.conn.load_balancer.availability_zones()] + names = [ + az.name for az in self.conn.load_balancer.availability_zones() + ] self.assertIn(self.AVAILABILITY_ZONE_NAME, names) def test_availability_zone_update(self): self.conn.load_balancer.update_availability_zone( - self.AVAILABILITY_ZONE_NAME, description=self.UPDATE_DESCRIPTION) + self.AVAILABILITY_ZONE_NAME, description=self.UPDATE_DESCRIPTION + ) test_availability_zone = self.conn.load_balancer.get_availability_zone( - self.AVAILABILITY_ZONE_NAME) - self.assertEqual(self.UPDATE_DESCRIPTION, - test_availability_zone.description) + self.AVAILABILITY_ZONE_NAME + ) + self.assertEqual( + self.UPDATE_DESCRIPTION, test_availability_zone.description + ) self.conn.load_balancer.update_availability_zone( - self.AVAILABILITY_ZONE_NAME, description=self.DESCRIPTION) + self.AVAILABILITY_ZONE_NAME, description=self.DESCRIPTION + ) test_availability_zone = self.conn.load_balancer.get_availability_zone( - self.AVAILABILITY_ZONE_NAME) + self.AVAILABILITY_ZONE_NAME + ) self.assertEqual(self.DESCRIPTION, test_availability_zone.description) diff --git a/openstack/tests/unit/load_balancer/test_amphora.py b/openstack/tests/unit/load_balancer/test_amphora.py index 09c087a5a..04b93ca87 100644 --- a/openstack/tests/unit/load_balancer/test_amphora.py +++ b/openstack/tests/unit/load_balancer/test_amphora.py @@ -48,12 +48,11 @@ 'created_at': '2017-05-10T18:14:44', 'updated_at': '2017-05-10T23:08:12', 'image_id': IMAGE_ID, - 'compute_flavor': COMPUTE_FLAVOR + 'compute_flavor': COMPUTE_FLAVOR, } class TestAmphora(base.TestCase): - def test_basic(self): test_amphora = amphora.Amphora() self.assertEqual('amphora', test_amphora.resource_key) @@ -75,13 +74,15 @@ def test_make_it(self): self.assertEqual(EXAMPLE['ha_ip'], test_amphora.ha_ip) self.assertEqual(VRRP_PORT_ID, test_amphora.vrrp_port_id) self.assertEqual(HA_PORT_ID, test_amphora.ha_port_id) - self.assertEqual(EXAMPLE['cert_expiration'], - test_amphora.cert_expiration) + self.assertEqual( + EXAMPLE['cert_expiration'], test_amphora.cert_expiration + ) self.assertEqual(EXAMPLE['cert_busy'], test_amphora.cert_busy) self.assertEqual(EXAMPLE['role'], test_amphora.role) self.assertEqual(EXAMPLE['status'], test_amphora.status) - self.assertEqual(EXAMPLE['vrrp_interface'], - test_amphora.vrrp_interface) + self.assertEqual( + EXAMPLE['vrrp_interface'], test_amphora.vrrp_interface + ) self.assertEqual(EXAMPLE['vrrp_id'], test_amphora.vrrp_id) self.assertEqual(EXAMPLE['vrrp_priority'], test_amphora.vrrp_priority) self.assertEqual(EXAMPLE['cached_zone'], test_amphora.cached_zone) @@ -91,38 +92,41 @@ def test_make_it(self): self.assertEqual(COMPUTE_FLAVOR, test_amphora.compute_flavor) self.assertDictEqual( - {'limit': 'limit', - 'marker': 'marker', - 'id': 'id', - 'loadbalancer_id': 'loadbalancer_id', - 'compute_id': 'compute_id', - 'lb_network_ip': 'lb_network_ip', - 'vrrp_ip': 'vrrp_ip', - 'ha_ip': 'ha_ip', - 'vrrp_port_id': 'vrrp_port_id', - 'ha_port_id': 'ha_port_id', - 'cert_expiration': 'cert_expiration', - 'cert_busy': 'cert_busy', - 'role': 'role', - 'status': 'status', - 'vrrp_interface': 'vrrp_interface', - 'vrrp_id': 'vrrp_id', - 'vrrp_priority': 'vrrp_priority', - 'cached_zone': 'cached_zone', - 'created_at': 'created_at', - 'updated_at': 'updated_at', - 'image_id': 'image_id', - 'image_id': 'image_id' - }, - test_amphora._query_mapping._mapping) + { + 'limit': 'limit', + 'marker': 'marker', + 'id': 'id', + 'loadbalancer_id': 'loadbalancer_id', + 'compute_id': 'compute_id', + 'lb_network_ip': 'lb_network_ip', + 'vrrp_ip': 'vrrp_ip', + 'ha_ip': 'ha_ip', + 'vrrp_port_id': 'vrrp_port_id', + 'ha_port_id': 'ha_port_id', + 'cert_expiration': 'cert_expiration', + 'cert_busy': 'cert_busy', + 'role': 'role', + 'status': 'status', + 'vrrp_interface': 'vrrp_interface', + 'vrrp_id': 'vrrp_id', + 'vrrp_priority': 'vrrp_priority', + 'cached_zone': 'cached_zone', + 'created_at': 'created_at', + 'updated_at': 'updated_at', + 'image_id': 'image_id', + 'image_id': 'image_id', + }, + test_amphora._query_mapping._mapping, + ) class TestAmphoraConfig(base.TestCase): - def test_basic(self): test_amp_config = amphora.AmphoraConfig() - self.assertEqual('/octavia/amphorae/%(amphora_id)s/config', - test_amp_config.base_path) + self.assertEqual( + '/octavia/amphorae/%(amphora_id)s/config', + test_amp_config.base_path, + ) self.assertFalse(test_amp_config.allow_create) self.assertFalse(test_amp_config.allow_fetch) self.assertTrue(test_amp_config.allow_commit) @@ -131,11 +135,12 @@ def test_basic(self): class TestAmphoraFailover(base.TestCase): - def test_basic(self): test_amp_failover = amphora.AmphoraFailover() - self.assertEqual('/octavia/amphorae/%(amphora_id)s/failover', - test_amp_failover.base_path) + self.assertEqual( + '/octavia/amphorae/%(amphora_id)s/failover', + test_amp_failover.base_path, + ) self.assertFalse(test_amp_failover.allow_create) self.assertFalse(test_amp_failover.allow_fetch) self.assertTrue(test_amp_failover.allow_commit) diff --git a/openstack/tests/unit/load_balancer/test_availability_zone.py b/openstack/tests/unit/load_balancer/test_availability_zone.py index 63eb9fb52..2cdbad6ab 100644 --- a/openstack/tests/unit/load_balancer/test_availability_zone.py +++ b/openstack/tests/unit/load_balancer/test_availability_zone.py @@ -22,19 +22,22 @@ 'name': 'strawberry', 'description': 'tasty', 'is_enabled': False, - 'availability_zone_profile_id': AVAILABILITY_ZONE_PROFILE_ID} + 'availability_zone_profile_id': AVAILABILITY_ZONE_PROFILE_ID, +} class TestAvailabilityZone(base.TestCase): - def test_basic(self): test_availability_zone = availability_zone.AvailabilityZone() - self.assertEqual('availability_zone', - test_availability_zone.resource_key) - self.assertEqual('availability_zones', - test_availability_zone.resources_key) - self.assertEqual('/lbaas/availabilityzones', - test_availability_zone.base_path) + self.assertEqual( + 'availability_zone', test_availability_zone.resource_key + ) + self.assertEqual( + 'availability_zones', test_availability_zone.resources_key + ) + self.assertEqual( + '/lbaas/availabilityzones', test_availability_zone.base_path + ) self.assertTrue(test_availability_zone.allow_create) self.assertTrue(test_availability_zone.allow_fetch) self.assertTrue(test_availability_zone.allow_commit) @@ -44,17 +47,23 @@ def test_basic(self): def test_make_it(self): test_availability_zone = availability_zone.AvailabilityZone(**EXAMPLE) self.assertEqual(EXAMPLE['name'], test_availability_zone.name) - self.assertEqual(EXAMPLE['description'], - test_availability_zone.description) + self.assertEqual( + EXAMPLE['description'], test_availability_zone.description + ) self.assertFalse(test_availability_zone.is_enabled) - self.assertEqual(EXAMPLE['availability_zone_profile_id'], - test_availability_zone.availability_zone_profile_id) + self.assertEqual( + EXAMPLE['availability_zone_profile_id'], + test_availability_zone.availability_zone_profile_id, + ) self.assertDictEqual( - {'limit': 'limit', - 'marker': 'marker', - 'name': 'name', - 'description': 'description', - 'is_enabled': 'enabled', - 'availability_zone_profile_id': 'availability_zone_profile_id'}, - test_availability_zone._query_mapping._mapping) + { + 'limit': 'limit', + 'marker': 'marker', + 'name': 'name', + 'description': 'description', + 'is_enabled': 'enabled', + 'availability_zone_profile_id': 'availability_zone_profile_id', + }, + test_availability_zone._query_mapping._mapping, + ) diff --git a/openstack/tests/unit/load_balancer/test_availability_zone_profile.py b/openstack/tests/unit/load_balancer/test_availability_zone_profile.py index b6343772f..bdc87da18 100644 --- a/openstack/tests/unit/load_balancer/test_availability_zone_profile.py +++ b/openstack/tests/unit/load_balancer/test_availability_zone_profile.py @@ -22,19 +22,22 @@ 'id': IDENTIFIER, 'name': 'acidic', 'provider_name': 'best', - 'availability_zone_data': '{"loadbalancer_topology": "SINGLE"}'} + 'availability_zone_data': '{"loadbalancer_topology": "SINGLE"}', +} class TestAvailabilityZoneProfile(base.TestCase): - def test_basic(self): test_profile = availability_zone_profile.AvailabilityZoneProfile() - self.assertEqual('availability_zone_profile', - test_profile.resource_key) - self.assertEqual('availability_zone_profiles', - test_profile.resources_key) - self.assertEqual('/lbaas/availabilityzoneprofiles', - test_profile.base_path) + self.assertEqual( + 'availability_zone_profile', test_profile.resource_key + ) + self.assertEqual( + 'availability_zone_profiles', test_profile.resources_key + ) + self.assertEqual( + '/lbaas/availabilityzoneprofiles', test_profile.base_path + ) self.assertTrue(test_profile.allow_create) self.assertTrue(test_profile.allow_fetch) self.assertTrue(test_profile.allow_commit) @@ -43,18 +46,24 @@ def test_basic(self): def test_make_it(self): test_profile = availability_zone_profile.AvailabilityZoneProfile( - **EXAMPLE) + **EXAMPLE + ) self.assertEqual(EXAMPLE['id'], test_profile.id) self.assertEqual(EXAMPLE['name'], test_profile.name) self.assertEqual(EXAMPLE['provider_name'], test_profile.provider_name) - self.assertEqual(EXAMPLE['availability_zone_data'], - test_profile.availability_zone_data) + self.assertEqual( + EXAMPLE['availability_zone_data'], + test_profile.availability_zone_data, + ) self.assertDictEqual( - {'limit': 'limit', - 'marker': 'marker', - 'id': 'id', - 'name': 'name', - 'provider_name': 'provider_name', - 'availability_zone_data': 'availability_zone_data'}, - test_profile._query_mapping._mapping) + { + 'limit': 'limit', + 'marker': 'marker', + 'id': 'id', + 'name': 'name', + 'provider_name': 'provider_name', + 'availability_zone_data': 'availability_zone_data', + }, + test_profile._query_mapping._mapping, + ) diff --git a/openstack/tests/unit/load_balancer/test_flavor.py b/openstack/tests/unit/load_balancer/test_flavor.py index 5f4bec1ef..ce199efac 100644 --- a/openstack/tests/unit/load_balancer/test_flavor.py +++ b/openstack/tests/unit/load_balancer/test_flavor.py @@ -25,11 +25,11 @@ 'name': 'strawberry', 'description': 'tasty', 'is_enabled': False, - 'flavor_profile_id': FLAVOR_PROFILE_ID} + 'flavor_profile_id': FLAVOR_PROFILE_ID, +} class TestFlavor(base.TestCase): - def test_basic(self): test_flavor = flavor.Flavor() self.assertEqual('flavor', test_flavor.resource_key) @@ -47,15 +47,19 @@ def test_make_it(self): self.assertEqual(EXAMPLE['name'], test_flavor.name) self.assertEqual(EXAMPLE['description'], test_flavor.description) self.assertFalse(test_flavor.is_enabled) - self.assertEqual(EXAMPLE['flavor_profile_id'], - test_flavor.flavor_profile_id) + self.assertEqual( + EXAMPLE['flavor_profile_id'], test_flavor.flavor_profile_id + ) self.assertDictEqual( - {'limit': 'limit', - 'marker': 'marker', - 'id': 'id', - 'name': 'name', - 'description': 'description', - 'is_enabled': 'enabled', - 'flavor_profile_id': 'flavor_profile_id'}, - test_flavor._query_mapping._mapping) + { + 'limit': 'limit', + 'marker': 'marker', + 'id': 'id', + 'name': 'name', + 'description': 'description', + 'is_enabled': 'enabled', + 'flavor_profile_id': 'flavor_profile_id', + }, + test_flavor._query_mapping._mapping, + ) diff --git a/openstack/tests/unit/load_balancer/test_flavor_profile.py b/openstack/tests/unit/load_balancer/test_flavor_profile.py index 2266f4d1f..5b9d9fc7a 100644 --- a/openstack/tests/unit/load_balancer/test_flavor_profile.py +++ b/openstack/tests/unit/load_balancer/test_flavor_profile.py @@ -23,11 +23,11 @@ 'id': IDENTIFIER, 'name': 'acidic', 'provider_name': 'best', - 'flavor_data': '{"loadbalancer_topology": "SINGLE"}'} + 'flavor_data': '{"loadbalancer_topology": "SINGLE"}', +} class TestFlavorProfile(base.TestCase): - def test_basic(self): test_profile = flavor_profile.FlavorProfile() self.assertEqual('flavorprofile', test_profile.resource_key) @@ -47,10 +47,13 @@ def test_make_it(self): self.assertEqual(EXAMPLE['flavor_data'], test_profile.flavor_data) self.assertDictEqual( - {'limit': 'limit', - 'marker': 'marker', - 'id': 'id', - 'name': 'name', - 'provider_name': 'provider_name', - 'flavor_data': 'flavor_data'}, - test_profile._query_mapping._mapping) + { + 'limit': 'limit', + 'marker': 'marker', + 'id': 'id', + 'name': 'name', + 'provider_name': 'provider_name', + 'flavor_data': 'flavor_data', + }, + test_profile._query_mapping._mapping, + ) diff --git a/openstack/tests/unit/load_balancer/test_health_monitor.py b/openstack/tests/unit/load_balancer/test_health_monitor.py index 85ff52b7c..7db7329e4 100644 --- a/openstack/tests/unit/load_balancer/test_health_monitor.py +++ b/openstack/tests/unit/load_balancer/test_health_monitor.py @@ -35,12 +35,11 @@ 'timeout': 4, 'type': 'HTTP', 'updated_at': '2017-07-17T12:16:57.233772', - 'url_path': '/health_page.html' + 'url_path': '/health_page.html', } class TestPoolHealthMonitor(base.TestCase): - def test_basic(self): test_hm = health_monitor.HealthMonitor() self.assertEqual('healthmonitor', test_hm.resource_key) @@ -67,36 +66,38 @@ def test_make_it(self): self.assertEqual(EXAMPLE['pools'], test_hm.pools) self.assertEqual(EXAMPLE['pool_id'], test_hm.pool_id) self.assertEqual(EXAMPLE['project_id'], test_hm.project_id) - self.assertEqual(EXAMPLE['provisioning_status'], - test_hm.provisioning_status) + self.assertEqual( + EXAMPLE['provisioning_status'], test_hm.provisioning_status + ) self.assertEqual(EXAMPLE['timeout'], test_hm.timeout) self.assertEqual(EXAMPLE['type'], test_hm.type) self.assertEqual(EXAMPLE['updated_at'], test_hm.updated_at) self.assertEqual(EXAMPLE['url_path'], test_hm.url_path) self.assertDictEqual( - {'limit': 'limit', - 'marker': 'marker', - 'created_at': 'created_at', - 'updated_at': 'updated_at', - 'name': 'name', - 'project_id': 'project_id', - 'tags': 'tags', - 'any_tags': 'tags-any', - 'not_tags': 'not-tags', - 'not_any_tags': 'not-tags-any', - 'operating_status': 'operating_status', - 'provisioning_status': 'provisioning_status', - 'is_admin_state_up': 'admin_state_up', - - 'delay': 'delay', - 'expected_codes': 'expected_codes', - 'http_method': 'http_method', - 'max_retries': 'max_retries', - 'max_retries_down': 'max_retries_down', - 'pool_id': 'pool_id', - 'timeout': 'timeout', - 'type': 'type', - 'url_path': 'url_path' - }, - test_hm._query_mapping._mapping) + { + 'limit': 'limit', + 'marker': 'marker', + 'created_at': 'created_at', + 'updated_at': 'updated_at', + 'name': 'name', + 'project_id': 'project_id', + 'tags': 'tags', + 'any_tags': 'tags-any', + 'not_tags': 'not-tags', + 'not_any_tags': 'not-tags-any', + 'operating_status': 'operating_status', + 'provisioning_status': 'provisioning_status', + 'is_admin_state_up': 'admin_state_up', + 'delay': 'delay', + 'expected_codes': 'expected_codes', + 'http_method': 'http_method', + 'max_retries': 'max_retries', + 'max_retries_down': 'max_retries_down', + 'pool_id': 'pool_id', + 'timeout': 'timeout', + 'type': 'type', + 'url_path': 'url_path', + }, + test_hm._query_mapping._mapping, + ) diff --git a/openstack/tests/unit/load_balancer/test_l7policy.py b/openstack/tests/unit/load_balancer/test_l7policy.py index cf433fc2f..bb8768abc 100644 --- a/openstack/tests/unit/load_balancer/test_l7policy.py +++ b/openstack/tests/unit/load_balancer/test_l7policy.py @@ -37,7 +37,6 @@ class TestL7Policy(base.TestCase): - def test_basic(self): test_l7_policy = l7_policy.L7Policy() self.assertEqual('l7policy', test_l7_policy.resource_key) @@ -58,39 +57,44 @@ def test_make_it(self): self.assertEqual(EXAMPLE['id'], test_l7_policy.id) self.assertEqual(EXAMPLE['listener_id'], test_l7_policy.listener_id) self.assertEqual(EXAMPLE['name'], test_l7_policy.name) - self.assertEqual(EXAMPLE['operating_status'], - test_l7_policy.operating_status) + self.assertEqual( + EXAMPLE['operating_status'], test_l7_policy.operating_status + ) self.assertEqual(EXAMPLE['position'], test_l7_policy.position) self.assertEqual(EXAMPLE['project_id'], test_l7_policy.project_id) - self.assertEqual(EXAMPLE['provisioning_status'], - test_l7_policy.provisioning_status) - self.assertEqual(EXAMPLE['redirect_pool_id'], - test_l7_policy.redirect_pool_id) - self.assertEqual(EXAMPLE['redirect_prefix'], - test_l7_policy.redirect_prefix) + self.assertEqual( + EXAMPLE['provisioning_status'], test_l7_policy.provisioning_status + ) + self.assertEqual( + EXAMPLE['redirect_pool_id'], test_l7_policy.redirect_pool_id + ) + self.assertEqual( + EXAMPLE['redirect_prefix'], test_l7_policy.redirect_prefix + ) self.assertEqual(EXAMPLE['redirect_url'], test_l7_policy.redirect_url) self.assertEqual(EXAMPLE['rules'], test_l7_policy.rules) self.assertEqual(EXAMPLE['updated_at'], test_l7_policy.updated_at) self.assertDictEqual( - {'limit': 'limit', - 'marker': 'marker', - 'name': 'name', - 'description': 'description', - 'project_id': 'project_id', - 'tags': 'tags', - 'any_tags': 'tags-any', - 'not_tags': 'not-tags', - 'not_any_tags': 'not-tags-any', - 'operating_status': 'operating_status', - 'provisioning_status': 'provisioning_status', - 'is_admin_state_up': 'admin_state_up', - - 'action': 'action', - 'listener_id': 'listener_id', - 'position': 'position', - 'redirect_pool_id': 'redirect_pool_id', - 'redirect_url': 'redirect_url', - 'redirect_prefix': 'redirect_prefix' - }, - test_l7_policy._query_mapping._mapping) + { + 'limit': 'limit', + 'marker': 'marker', + 'name': 'name', + 'description': 'description', + 'project_id': 'project_id', + 'tags': 'tags', + 'any_tags': 'tags-any', + 'not_tags': 'not-tags', + 'not_any_tags': 'not-tags-any', + 'operating_status': 'operating_status', + 'provisioning_status': 'provisioning_status', + 'is_admin_state_up': 'admin_state_up', + 'action': 'action', + 'listener_id': 'listener_id', + 'position': 'position', + 'redirect_pool_id': 'redirect_pool_id', + 'redirect_url': 'redirect_url', + 'redirect_prefix': 'redirect_prefix', + }, + test_l7_policy._query_mapping._mapping, + ) diff --git a/openstack/tests/unit/load_balancer/test_l7rule.py b/openstack/tests/unit/load_balancer/test_l7rule.py index 0f3788091..acded4a33 100644 --- a/openstack/tests/unit/load_balancer/test_l7rule.py +++ b/openstack/tests/unit/load_balancer/test_l7rule.py @@ -29,18 +29,18 @@ 'provisioning_status': 'ACTIVE', 'type': 'COOKIE', 'updated_at': '2017-08-17T12:16:57.233772', - 'value': 'chocolate' + 'value': 'chocolate', } class TestL7Rule(base.TestCase): - def test_basic(self): test_l7rule = l7_rule.L7Rule() self.assertEqual('rule', test_l7rule.resource_key) self.assertEqual('rules', test_l7rule.resources_key) - self.assertEqual('/lbaas/l7policies/%(l7policy_id)s/rules', - test_l7rule.base_path) + self.assertEqual( + '/lbaas/l7policies/%(l7policy_id)s/rules', test_l7rule.base_path + ) self.assertTrue(test_l7rule.allow_create) self.assertTrue(test_l7rule.allow_fetch) self.assertTrue(test_l7rule.allow_commit) @@ -56,34 +56,37 @@ def test_make_it(self): self.assertEqual(EXAMPLE['invert'], test_l7rule.invert) self.assertEqual(EXAMPLE['key'], test_l7rule.key) self.assertEqual(EXAMPLE['l7_policy_id'], test_l7rule.l7_policy_id) - self.assertEqual(EXAMPLE['operating_status'], - test_l7rule.operating_status) + self.assertEqual( + EXAMPLE['operating_status'], test_l7rule.operating_status + ) self.assertEqual(EXAMPLE['project_id'], test_l7rule.project_id) - self.assertEqual(EXAMPLE['provisioning_status'], - test_l7rule.provisioning_status) + self.assertEqual( + EXAMPLE['provisioning_status'], test_l7rule.provisioning_status + ) self.assertEqual(EXAMPLE['type'], test_l7rule.type) self.assertEqual(EXAMPLE['updated_at'], test_l7rule.updated_at) self.assertEqual(EXAMPLE['value'], test_l7rule.rule_value) self.assertDictEqual( - {'limit': 'limit', - 'marker': 'marker', - 'created_at': 'created_at', - 'updated_at': 'updated_at', - 'project_id': 'project_id', - 'tags': 'tags', - 'any_tags': 'tags-any', - 'not_tags': 'not-tags', - 'not_any_tags': 'not-tags-any', - 'operating_status': 'operating_status', - 'provisioning_status': 'provisioning_status', - 'is_admin_state_up': 'admin_state_up', - - 'compare_type': 'compare_type', - 'invert': 'invert', - 'key': 'key', - 'type': 'type', - 'rule_value': 'rule_value', - 'l7_policy_id': 'l7policy_id' - }, - test_l7rule._query_mapping._mapping) + { + 'limit': 'limit', + 'marker': 'marker', + 'created_at': 'created_at', + 'updated_at': 'updated_at', + 'project_id': 'project_id', + 'tags': 'tags', + 'any_tags': 'tags-any', + 'not_tags': 'not-tags', + 'not_any_tags': 'not-tags-any', + 'operating_status': 'operating_status', + 'provisioning_status': 'provisioning_status', + 'is_admin_state_up': 'admin_state_up', + 'compare_type': 'compare_type', + 'invert': 'invert', + 'key': 'key', + 'type': 'type', + 'rule_value': 'rule_value', + 'l7_policy_id': 'l7policy_id', + }, + test_l7rule._query_mapping._mapping, + ) diff --git a/openstack/tests/unit/load_balancer/test_listener.py b/openstack/tests/unit/load_balancer/test_listener.py index c8928e77e..ca5da5250 100644 --- a/openstack/tests/unit/load_balancer/test_listener.py +++ b/openstack/tests/unit/load_balancer/test_listener.py @@ -31,8 +31,10 @@ 'project_id': uuid.uuid4(), 'protocol': 'TEST_PROTOCOL', 'protocol_port': 10, - 'default_tls_container_ref': ('http://198.51.100.10:9311/v1/containers/' - 'a570068c-d295-4780-91d4-3046a325db51'), + 'default_tls_container_ref': ( + 'http://198.51.100.10:9311/v1/containers/' + 'a570068c-d295-4780-91d4-3046a325db51' + ), 'sni_container_refs': [], 'created_at': '2017-07-17T12:14:57.233772', 'updated_at': '2017-07-17T12:16:57.233772', @@ -44,7 +46,7 @@ 'timeout_tcp_inspect': 0, 'tls_ciphers': 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256', 'tls_versions': ['TLSv1.1', 'TLSv1.2'], - 'alpn_protocols': ['h2', 'http/1.1', 'http/1.0'] + 'alpn_protocols': ['h2', 'http/1.1', 'http/1.0'], } EXAMPLE_STATS = { @@ -52,12 +54,11 @@ 'bytes_in': 2, 'bytes_out': 3, 'request_errors': 4, - 'total_connections': 5 + 'total_connections': 5, } class TestListener(base.TestCase): - def test_basic(self): test_listener = listener.Listener() self.assertEqual('listener', test_listener.resource_key) @@ -73,90 +74,103 @@ def test_make_it(self): test_listener = listener.Listener(**EXAMPLE) self.assertTrue(test_listener.is_admin_state_up) self.assertEqual(EXAMPLE['allowed_cidrs'], test_listener.allowed_cidrs) - self.assertEqual(EXAMPLE['connection_limit'], - test_listener.connection_limit) - self.assertEqual(EXAMPLE['default_pool_id'], - test_listener.default_pool_id) + self.assertEqual( + EXAMPLE['connection_limit'], test_listener.connection_limit + ) + self.assertEqual( + EXAMPLE['default_pool_id'], test_listener.default_pool_id + ) self.assertEqual(EXAMPLE['description'], test_listener.description) self.assertEqual(EXAMPLE['id'], test_listener.id) - self.assertEqual(EXAMPLE['insert_headers'], - test_listener.insert_headers) - self.assertEqual(EXAMPLE['l7policies'], - test_listener.l7_policies) - self.assertEqual(EXAMPLE['loadbalancers'], - test_listener.load_balancers) + self.assertEqual( + EXAMPLE['insert_headers'], test_listener.insert_headers + ) + self.assertEqual(EXAMPLE['l7policies'], test_listener.l7_policies) + self.assertEqual( + EXAMPLE['loadbalancers'], test_listener.load_balancers + ) self.assertEqual(EXAMPLE['name'], test_listener.name) self.assertEqual(EXAMPLE['project_id'], test_listener.project_id) self.assertEqual(EXAMPLE['protocol'], test_listener.protocol) self.assertEqual(EXAMPLE['protocol_port'], test_listener.protocol_port) - self.assertEqual(EXAMPLE['default_tls_container_ref'], - test_listener.default_tls_container_ref) - self.assertEqual(EXAMPLE['sni_container_refs'], - test_listener.sni_container_refs) + self.assertEqual( + EXAMPLE['default_tls_container_ref'], + test_listener.default_tls_container_ref, + ) + self.assertEqual( + EXAMPLE['sni_container_refs'], test_listener.sni_container_refs + ) self.assertEqual(EXAMPLE['created_at'], test_listener.created_at) self.assertEqual(EXAMPLE['updated_at'], test_listener.updated_at) - self.assertEqual(EXAMPLE['provisioning_status'], - test_listener.provisioning_status) - self.assertEqual(EXAMPLE['operating_status'], - test_listener.operating_status) - self.assertEqual(EXAMPLE['timeout_client_data'], - test_listener.timeout_client_data) - self.assertEqual(EXAMPLE['timeout_member_connect'], - test_listener.timeout_member_connect) - self.assertEqual(EXAMPLE['timeout_member_data'], - test_listener.timeout_member_data) - self.assertEqual(EXAMPLE['timeout_tcp_inspect'], - test_listener.timeout_tcp_inspect) - self.assertEqual(EXAMPLE['tls_ciphers'], - test_listener.tls_ciphers) - self.assertEqual(EXAMPLE['tls_versions'], - test_listener.tls_versions) - self.assertEqual(EXAMPLE['alpn_protocols'], - test_listener.alpn_protocols) + self.assertEqual( + EXAMPLE['provisioning_status'], test_listener.provisioning_status + ) + self.assertEqual( + EXAMPLE['operating_status'], test_listener.operating_status + ) + self.assertEqual( + EXAMPLE['timeout_client_data'], test_listener.timeout_client_data + ) + self.assertEqual( + EXAMPLE['timeout_member_connect'], + test_listener.timeout_member_connect, + ) + self.assertEqual( + EXAMPLE['timeout_member_data'], test_listener.timeout_member_data + ) + self.assertEqual( + EXAMPLE['timeout_tcp_inspect'], test_listener.timeout_tcp_inspect + ) + self.assertEqual(EXAMPLE['tls_ciphers'], test_listener.tls_ciphers) + self.assertEqual(EXAMPLE['tls_versions'], test_listener.tls_versions) + self.assertEqual( + EXAMPLE['alpn_protocols'], test_listener.alpn_protocols + ) self.assertDictEqual( - {'limit': 'limit', - 'marker': 'marker', - 'created_at': 'created_at', - 'updated_at': 'updated_at', - 'description': 'description', - 'name': 'name', - 'project_id': 'project_id', - 'tags': 'tags', - 'any_tags': 'tags-any', - 'not_tags': 'not-tags', - 'not_any_tags': 'not-tags-any', - 'operating_status': 'operating_status', - 'provisioning_status': 'provisioning_status', - 'is_admin_state_up': 'admin_state_up', - - 'allowed_cidrs': 'allowed_cidrs', - 'connection_limit': 'connection_limit', - 'default_pool_id': 'default_pool_id', - 'default_tls_container_ref': 'default_tls_container_ref', - 'sni_container_refs': 'sni_container_refs', - 'insert_headers': 'insert_headers', - 'load_balancer_id': 'load_balancer_id', - 'protocol': 'protocol', - 'protocol_port': 'protocol_port', - 'timeout_client_data': 'timeout_client_data', - 'timeout_member_connect': 'timeout_member_connect', - 'timeout_member_data': 'timeout_member_data', - 'timeout_tcp_inspect': 'timeout_tcp_inspect', - 'tls_ciphers': 'tls_ciphers', - 'tls_versions': 'tls_versions', - 'alpn_protocols': 'alpn_protocols', - }, - test_listener._query_mapping._mapping) + { + 'limit': 'limit', + 'marker': 'marker', + 'created_at': 'created_at', + 'updated_at': 'updated_at', + 'description': 'description', + 'name': 'name', + 'project_id': 'project_id', + 'tags': 'tags', + 'any_tags': 'tags-any', + 'not_tags': 'not-tags', + 'not_any_tags': 'not-tags-any', + 'operating_status': 'operating_status', + 'provisioning_status': 'provisioning_status', + 'is_admin_state_up': 'admin_state_up', + 'allowed_cidrs': 'allowed_cidrs', + 'connection_limit': 'connection_limit', + 'default_pool_id': 'default_pool_id', + 'default_tls_container_ref': 'default_tls_container_ref', + 'sni_container_refs': 'sni_container_refs', + 'insert_headers': 'insert_headers', + 'load_balancer_id': 'load_balancer_id', + 'protocol': 'protocol', + 'protocol_port': 'protocol_port', + 'timeout_client_data': 'timeout_client_data', + 'timeout_member_connect': 'timeout_member_connect', + 'timeout_member_data': 'timeout_member_data', + 'timeout_tcp_inspect': 'timeout_tcp_inspect', + 'tls_ciphers': 'tls_ciphers', + 'tls_versions': 'tls_versions', + 'alpn_protocols': 'alpn_protocols', + }, + test_listener._query_mapping._mapping, + ) class TestListenerStats(base.TestCase): - def test_basic(self): test_listener = listener.ListenerStats() self.assertEqual('stats', test_listener.resource_key) - self.assertEqual('/lbaas/listeners/%(listener_id)s/stats', - test_listener.base_path) + self.assertEqual( + '/lbaas/listeners/%(listener_id)s/stats', test_listener.base_path + ) self.assertFalse(test_listener.allow_create) self.assertTrue(test_listener.allow_fetch) self.assertFalse(test_listener.allow_delete) @@ -165,13 +179,15 @@ def test_basic(self): def test_make_it(self): test_listener = listener.ListenerStats(**EXAMPLE_STATS) - self.assertEqual(EXAMPLE_STATS['active_connections'], - test_listener.active_connections) - self.assertEqual(EXAMPLE_STATS['bytes_in'], - test_listener.bytes_in) - self.assertEqual(EXAMPLE_STATS['bytes_out'], - test_listener.bytes_out) - self.assertEqual(EXAMPLE_STATS['request_errors'], - test_listener.request_errors) - self.assertEqual(EXAMPLE_STATS['total_connections'], - test_listener.total_connections) + self.assertEqual( + EXAMPLE_STATS['active_connections'], + test_listener.active_connections, + ) + self.assertEqual(EXAMPLE_STATS['bytes_in'], test_listener.bytes_in) + self.assertEqual(EXAMPLE_STATS['bytes_out'], test_listener.bytes_out) + self.assertEqual( + EXAMPLE_STATS['request_errors'], test_listener.request_errors + ) + self.assertEqual( + EXAMPLE_STATS['total_connections'], test_listener.total_connections + ) diff --git a/openstack/tests/unit/load_balancer/test_load_balancer.py b/openstack/tests/unit/load_balancer/test_load_balancer.py index fb4f19cb1..db29ae883 100644 --- a/openstack/tests/unit/load_balancer/test_load_balancer.py +++ b/openstack/tests/unit/load_balancer/test_load_balancer.py @@ -38,14 +38,9 @@ 'vip_subnet_id': uuid.uuid4(), 'vip_qos_policy_id': uuid.uuid4(), 'additional_vips': [ - { - 'subnet_id': uuid.uuid4(), - 'ip_address': '192.0.2.6' - }, { - 'subnet_id': uuid.uuid4(), - 'ip_address': '192.0.2.7' - } - ] + {'subnet_id': uuid.uuid4(), 'ip_address': '192.0.2.6'}, + {'subnet_id': uuid.uuid4(), 'ip_address': '192.0.2.7'}, + ], } EXAMPLE_STATS = { @@ -53,18 +48,16 @@ 'bytes_in': 2, 'bytes_out': 3, 'request_errors': 4, - 'total_connections': 5 + 'total_connections': 5, } class TestLoadBalancer(base.TestCase): - def test_basic(self): test_load_balancer = load_balancer.LoadBalancer() self.assertEqual('loadbalancer', test_load_balancer.resource_key) self.assertEqual('loadbalancers', test_load_balancer.resources_key) - self.assertEqual('/lbaas/loadbalancers', - test_load_balancer.base_path) + self.assertEqual('/lbaas/loadbalancers', test_load_balancer.base_path) self.assertTrue(test_load_balancer.allow_create) self.assertTrue(test_load_balancer.allow_fetch) self.assertTrue(test_load_balancer.allow_delete) @@ -74,59 +67,72 @@ def test_basic(self): def test_make_it(self): test_load_balancer = load_balancer.LoadBalancer(**EXAMPLE) self.assertTrue(test_load_balancer.is_admin_state_up) - self.assertEqual(EXAMPLE['availability_zone'], - test_load_balancer.availability_zone) + self.assertEqual( + EXAMPLE['availability_zone'], test_load_balancer.availability_zone + ) self.assertEqual(EXAMPLE['created_at'], test_load_balancer.created_at) - self.assertEqual(EXAMPLE['description'], - test_load_balancer.description) + self.assertEqual( + EXAMPLE['description'], test_load_balancer.description + ) self.assertEqual(EXAMPLE['flavor_id'], test_load_balancer.flavor_id) self.assertEqual(EXAMPLE['id'], test_load_balancer.id) self.assertEqual(EXAMPLE['listeners'], test_load_balancer.listeners) self.assertEqual(EXAMPLE['name'], test_load_balancer.name) - self.assertEqual(EXAMPLE['operating_status'], - test_load_balancer.operating_status) + self.assertEqual( + EXAMPLE['operating_status'], test_load_balancer.operating_status + ) self.assertEqual(EXAMPLE['pools'], test_load_balancer.pools) self.assertEqual(EXAMPLE['project_id'], test_load_balancer.project_id) self.assertEqual(EXAMPLE['provider'], test_load_balancer.provider) - self.assertEqual(EXAMPLE['provisioning_status'], - test_load_balancer.provisioning_status) + self.assertEqual( + EXAMPLE['provisioning_status'], + test_load_balancer.provisioning_status, + ) self.assertEqual(EXAMPLE['updated_at'], test_load_balancer.updated_at) - self.assertEqual(EXAMPLE['vip_address'], - test_load_balancer.vip_address) - self.assertEqual(EXAMPLE['vip_network_id'], - test_load_balancer.vip_network_id) - self.assertEqual(EXAMPLE['vip_port_id'], - test_load_balancer.vip_port_id) - self.assertEqual(EXAMPLE['vip_subnet_id'], - test_load_balancer.vip_subnet_id) - self.assertEqual(EXAMPLE['vip_qos_policy_id'], - test_load_balancer.vip_qos_policy_id) - self.assertEqual(EXAMPLE['additional_vips'], - test_load_balancer.additional_vips) + self.assertEqual( + EXAMPLE['vip_address'], test_load_balancer.vip_address + ) + self.assertEqual( + EXAMPLE['vip_network_id'], test_load_balancer.vip_network_id + ) + self.assertEqual( + EXAMPLE['vip_port_id'], test_load_balancer.vip_port_id + ) + self.assertEqual( + EXAMPLE['vip_subnet_id'], test_load_balancer.vip_subnet_id + ) + self.assertEqual( + EXAMPLE['vip_qos_policy_id'], test_load_balancer.vip_qos_policy_id + ) + self.assertEqual( + EXAMPLE['additional_vips'], test_load_balancer.additional_vips + ) self.assertDictEqual( - {'limit': 'limit', - 'marker': 'marker', - 'availability_zone': 'availability_zone', - 'description': 'description', - 'flavor_id': 'flavor_id', - 'name': 'name', - 'project_id': 'project_id', - 'provider': 'provider', - 'operating_status': 'operating_status', - 'provisioning_status': 'provisioning_status', - 'is_admin_state_up': 'admin_state_up', - 'vip_address': 'vip_address', - 'vip_network_id': 'vip_network_id', - 'vip_port_id': 'vip_port_id', - 'vip_subnet_id': 'vip_subnet_id', - 'vip_qos_policy_id': 'vip_qos_policy_id', - 'tags': 'tags', - 'any_tags': 'tags-any', - 'not_tags': 'not-tags', - 'not_any_tags': 'not-tags-any', - }, - test_load_balancer._query_mapping._mapping) + { + 'limit': 'limit', + 'marker': 'marker', + 'availability_zone': 'availability_zone', + 'description': 'description', + 'flavor_id': 'flavor_id', + 'name': 'name', + 'project_id': 'project_id', + 'provider': 'provider', + 'operating_status': 'operating_status', + 'provisioning_status': 'provisioning_status', + 'is_admin_state_up': 'admin_state_up', + 'vip_address': 'vip_address', + 'vip_network_id': 'vip_network_id', + 'vip_port_id': 'vip_port_id', + 'vip_subnet_id': 'vip_subnet_id', + 'vip_qos_policy_id': 'vip_qos_policy_id', + 'tags': 'tags', + 'any_tags': 'tags-any', + 'not_tags': 'not-tags', + 'not_any_tags': 'not-tags-any', + }, + test_load_balancer._query_mapping._mapping, + ) def test_delete_non_cascade(self): sess = mock.Mock() @@ -138,12 +144,9 @@ def test_delete_non_cascade(self): sot._translate_response = mock.Mock() sot.delete(sess) - url = 'lbaas/loadbalancers/%(lb)s' % { - 'lb': EXAMPLE['id'] - } + url = 'lbaas/loadbalancers/%(lb)s' % {'lb': EXAMPLE['id']} params = {} - sess.delete.assert_called_with(url, - params=params) + sess.delete.assert_called_with(url, params=params) sot._translate_response.assert_called_once_with( resp, error_message=None, @@ -160,12 +163,9 @@ def test_delete_cascade(self): sot._translate_response = mock.Mock() sot.delete(sess) - url = 'lbaas/loadbalancers/%(lb)s' % { - 'lb': EXAMPLE['id'] - } + url = 'lbaas/loadbalancers/%(lb)s' % {'lb': EXAMPLE['id']} params = {'cascade': True} - sess.delete.assert_called_with(url, - params=params) + sess.delete.assert_called_with(url, params=params) sot._translate_response.assert_called_once_with( resp, error_message=None, @@ -174,12 +174,13 @@ def test_delete_cascade(self): class TestLoadBalancerStats(base.TestCase): - def test_basic(self): test_load_balancer = load_balancer.LoadBalancerStats() self.assertEqual('stats', test_load_balancer.resource_key) - self.assertEqual('/lbaas/loadbalancers/%(lb_id)s/stats', - test_load_balancer.base_path) + self.assertEqual( + '/lbaas/loadbalancers/%(lb_id)s/stats', + test_load_balancer.base_path, + ) self.assertFalse(test_load_balancer.allow_create) self.assertTrue(test_load_balancer.allow_fetch) self.assertFalse(test_load_balancer.allow_delete) @@ -188,24 +189,32 @@ def test_basic(self): def test_make_it(self): test_load_balancer = load_balancer.LoadBalancerStats(**EXAMPLE_STATS) - self.assertEqual(EXAMPLE_STATS['active_connections'], - test_load_balancer.active_connections) - self.assertEqual(EXAMPLE_STATS['bytes_in'], - test_load_balancer.bytes_in) - self.assertEqual(EXAMPLE_STATS['bytes_out'], - test_load_balancer.bytes_out) - self.assertEqual(EXAMPLE_STATS['request_errors'], - test_load_balancer.request_errors) - self.assertEqual(EXAMPLE_STATS['total_connections'], - test_load_balancer.total_connections) + self.assertEqual( + EXAMPLE_STATS['active_connections'], + test_load_balancer.active_connections, + ) + self.assertEqual( + EXAMPLE_STATS['bytes_in'], test_load_balancer.bytes_in + ) + self.assertEqual( + EXAMPLE_STATS['bytes_out'], test_load_balancer.bytes_out + ) + self.assertEqual( + EXAMPLE_STATS['request_errors'], test_load_balancer.request_errors + ) + self.assertEqual( + EXAMPLE_STATS['total_connections'], + test_load_balancer.total_connections, + ) class TestLoadBalancerFailover(base.TestCase): - def test_basic(self): test_load_balancer = load_balancer.LoadBalancerFailover() - self.assertEqual('/lbaas/loadbalancers/%(lb_id)s/failover', - test_load_balancer.base_path) + self.assertEqual( + '/lbaas/loadbalancers/%(lb_id)s/failover', + test_load_balancer.base_path, + ) self.assertFalse(test_load_balancer.allow_create) self.assertFalse(test_load_balancer.allow_fetch) self.assertFalse(test_load_balancer.allow_delete) diff --git a/openstack/tests/unit/load_balancer/test_member.py b/openstack/tests/unit/load_balancer/test_member.py index fe6d8f1ce..9f4ab8d14 100644 --- a/openstack/tests/unit/load_balancer/test_member.py +++ b/openstack/tests/unit/load_balancer/test_member.py @@ -34,13 +34,13 @@ class TestPoolMember(base.TestCase): - def test_basic(self): test_member = member.Member() self.assertEqual('member', test_member.resource_key) self.assertEqual('members', test_member.resources_key) - self.assertEqual('/lbaas/pools/%(pool_id)s/members', - test_member.base_path) + self.assertEqual( + '/lbaas/pools/%(pool_id)s/members', test_member.base_path + ) self.assertTrue(test_member.allow_create) self.assertTrue(test_member.allow_fetch) self.assertTrue(test_member.allow_commit) @@ -52,8 +52,9 @@ def test_make_it(self): self.assertEqual(EXAMPLE['address'], test_member.address) self.assertTrue(test_member.is_admin_state_up) self.assertEqual(EXAMPLE['id'], test_member.id) - self.assertEqual(EXAMPLE['monitor_address'], - test_member.monitor_address) + self.assertEqual( + EXAMPLE['monitor_address'], test_member.monitor_address + ) self.assertEqual(EXAMPLE['monitor_port'], test_member.monitor_port) self.assertEqual(EXAMPLE['name'], test_member.name) self.assertEqual(EXAMPLE['pool_id'], test_member.pool_id) @@ -64,26 +65,27 @@ def test_make_it(self): self.assertFalse(test_member.backup) self.assertDictEqual( - {'limit': 'limit', - 'marker': 'marker', - 'created_at': 'created_at', - 'updated_at': 'updated_at', - 'name': 'name', - 'project_id': 'project_id', - 'tags': 'tags', - 'any_tags': 'tags-any', - 'not_tags': 'not-tags', - 'not_any_tags': 'not-tags-any', - 'operating_status': 'operating_status', - 'provisioning_status': 'provisioning_status', - 'is_admin_state_up': 'admin_state_up', - - 'address': 'address', - 'protocol_port': 'protocol_port', - 'subnet_id': 'subnet_id', - 'weight': 'weight', - 'monitor_address': 'monitor_address', - 'monitor_port': 'monitor_port', - 'backup': 'backup' - }, - test_member._query_mapping._mapping) + { + 'limit': 'limit', + 'marker': 'marker', + 'created_at': 'created_at', + 'updated_at': 'updated_at', + 'name': 'name', + 'project_id': 'project_id', + 'tags': 'tags', + 'any_tags': 'tags-any', + 'not_tags': 'not-tags', + 'not_any_tags': 'not-tags-any', + 'operating_status': 'operating_status', + 'provisioning_status': 'provisioning_status', + 'is_admin_state_up': 'admin_state_up', + 'address': 'address', + 'protocol_port': 'protocol_port', + 'subnet_id': 'subnet_id', + 'weight': 'weight', + 'monitor_address': 'monitor_address', + 'monitor_port': 'monitor_port', + 'backup': 'backup', + }, + test_member._query_mapping._mapping, + ) diff --git a/openstack/tests/unit/load_balancer/test_pool.py b/openstack/tests/unit/load_balancer/test_pool.py index 5ba5e366f..1d1dfac79 100644 --- a/openstack/tests/unit/load_balancer/test_pool.py +++ b/openstack/tests/unit/load_balancer/test_pool.py @@ -44,7 +44,6 @@ class TestPool(base.TestCase): - def test_basic(self): test_pool = pool.Pool() self.assertEqual('pool', test_pool.resource_key) @@ -59,66 +58,62 @@ def test_basic(self): def test_make_it(self): test_pool = pool.Pool(**EXAMPLE) self.assertEqual(EXAMPLE['name'], test_pool.name), - self.assertEqual(EXAMPLE['description'], - test_pool.description) - self.assertEqual(EXAMPLE['admin_state_up'], - test_pool.is_admin_state_up) - self.assertEqual(EXAMPLE['provisioning_status'], - test_pool.provisioning_status) + self.assertEqual(EXAMPLE['description'], test_pool.description) + self.assertEqual( + EXAMPLE['admin_state_up'], test_pool.is_admin_state_up + ) + self.assertEqual( + EXAMPLE['provisioning_status'], test_pool.provisioning_status + ) self.assertEqual(EXAMPLE['protocol'], test_pool.protocol) - self.assertEqual(EXAMPLE['operating_status'], - test_pool.operating_status) + self.assertEqual( + EXAMPLE['operating_status'], test_pool.operating_status + ) self.assertEqual(EXAMPLE['listener_id'], test_pool.listener_id) - self.assertEqual(EXAMPLE['loadbalancer_id'], - test_pool.loadbalancer_id) - self.assertEqual(EXAMPLE['lb_algorithm'], - test_pool.lb_algorithm) - self.assertEqual(EXAMPLE['session_persistence'], - test_pool.session_persistence) - self.assertEqual(EXAMPLE['project_id'], - test_pool.project_id) - self.assertEqual(EXAMPLE['loadbalancers'], - test_pool.loadbalancers) - self.assertEqual(EXAMPLE['listeners'], - test_pool.listeners) + self.assertEqual(EXAMPLE['loadbalancer_id'], test_pool.loadbalancer_id) + self.assertEqual(EXAMPLE['lb_algorithm'], test_pool.lb_algorithm) + self.assertEqual( + EXAMPLE['session_persistence'], test_pool.session_persistence + ) + self.assertEqual(EXAMPLE['project_id'], test_pool.project_id) + self.assertEqual(EXAMPLE['loadbalancers'], test_pool.loadbalancers) + self.assertEqual(EXAMPLE['listeners'], test_pool.listeners) self.assertEqual(EXAMPLE['created_at'], test_pool.created_at) self.assertEqual(EXAMPLE['updated_at'], test_pool.updated_at) - self.assertEqual(EXAMPLE['health_monitor_id'], - test_pool.health_monitor_id) + self.assertEqual( + EXAMPLE['health_monitor_id'], test_pool.health_monitor_id + ) self.assertEqual(EXAMPLE['members'], test_pool.members) - self.assertEqual(EXAMPLE['tls_enabled'], - test_pool.tls_enabled) - self.assertEqual(EXAMPLE['tls_ciphers'], - test_pool.tls_ciphers) - self.assertEqual(EXAMPLE['tls_versions'], - test_pool.tls_versions) - self.assertEqual(EXAMPLE['alpn_protocols'], - test_pool.alpn_protocols) + self.assertEqual(EXAMPLE['tls_enabled'], test_pool.tls_enabled) + self.assertEqual(EXAMPLE['tls_ciphers'], test_pool.tls_ciphers) + self.assertEqual(EXAMPLE['tls_versions'], test_pool.tls_versions) + self.assertEqual(EXAMPLE['alpn_protocols'], test_pool.alpn_protocols) self.assertDictEqual( - {'limit': 'limit', - 'marker': 'marker', - 'created_at': 'created_at', - 'updated_at': 'updated_at', - 'description': 'description', - 'name': 'name', - 'project_id': 'project_id', - 'tags': 'tags', - 'any_tags': 'tags-any', - 'not_tags': 'not-tags', - 'not_any_tags': 'not-tags-any', - 'operating_status': 'operating_status', - 'provisioning_status': 'provisioning_status', - 'is_admin_state_up': 'admin_state_up', - - 'health_monitor_id': 'health_monitor_id', - 'lb_algorithm': 'lb_algorithm', - 'listener_id': 'listener_id', - 'loadbalancer_id': 'loadbalancer_id', - 'protocol': 'protocol', - 'tls_enabled': 'tls_enabled', - 'tls_ciphers': 'tls_ciphers', - 'tls_versions': 'tls_versions', - 'alpn_protocols': 'alpn_protocols', - }, - test_pool._query_mapping._mapping) + { + 'limit': 'limit', + 'marker': 'marker', + 'created_at': 'created_at', + 'updated_at': 'updated_at', + 'description': 'description', + 'name': 'name', + 'project_id': 'project_id', + 'tags': 'tags', + 'any_tags': 'tags-any', + 'not_tags': 'not-tags', + 'not_any_tags': 'not-tags-any', + 'operating_status': 'operating_status', + 'provisioning_status': 'provisioning_status', + 'is_admin_state_up': 'admin_state_up', + 'health_monitor_id': 'health_monitor_id', + 'lb_algorithm': 'lb_algorithm', + 'listener_id': 'listener_id', + 'loadbalancer_id': 'loadbalancer_id', + 'protocol': 'protocol', + 'tls_enabled': 'tls_enabled', + 'tls_ciphers': 'tls_ciphers', + 'tls_versions': 'tls_versions', + 'alpn_protocols': 'alpn_protocols', + }, + test_pool._query_mapping._mapping, + ) diff --git a/openstack/tests/unit/load_balancer/test_provider.py b/openstack/tests/unit/load_balancer/test_provider.py index 3b087419b..bcbf7ed09 100644 --- a/openstack/tests/unit/load_balancer/test_provider.py +++ b/openstack/tests/unit/load_balancer/test_provider.py @@ -16,14 +16,10 @@ from openstack.tests.unit import base -EXAMPLE = { - 'name': 'best', - 'description': 'The best provider' -} +EXAMPLE = {'name': 'best', 'description': 'The best provider'} class TestProvider(base.TestCase): - def test_basic(self): test_provider = provider.Provider() self.assertEqual('providers', test_provider.resources_key) @@ -40,20 +36,24 @@ def test_make_it(self): self.assertEqual(EXAMPLE['description'], test_provider.description) self.assertDictEqual( - {'limit': 'limit', - 'marker': 'marker', - 'name': 'name', - 'description': 'description'}, - test_provider._query_mapping._mapping) + { + 'limit': 'limit', + 'marker': 'marker', + 'name': 'name', + 'description': 'description', + }, + test_provider._query_mapping._mapping, + ) class TestProviderFlavorCapabilities(base.TestCase): - def test_basic(self): test_flav_cap = provider.ProviderFlavorCapabilities() self.assertEqual('flavor_capabilities', test_flav_cap.resources_key) - self.assertEqual('/lbaas/providers/%(provider)s/flavor_capabilities', - test_flav_cap.base_path) + self.assertEqual( + '/lbaas/providers/%(provider)s/flavor_capabilities', + test_flav_cap.base_path, + ) self.assertFalse(test_flav_cap.allow_create) self.assertFalse(test_flav_cap.allow_fetch) self.assertFalse(test_flav_cap.allow_commit) @@ -66,8 +66,11 @@ def test_make_it(self): self.assertEqual(EXAMPLE['description'], test_flav_cap.description) self.assertDictEqual( - {'limit': 'limit', - 'marker': 'marker', - 'name': 'name', - 'description': 'description'}, - test_flav_cap._query_mapping._mapping) + { + 'limit': 'limit', + 'marker': 'marker', + 'name': 'name', + 'description': 'description', + }, + test_flav_cap._query_mapping._mapping, + ) diff --git a/openstack/tests/unit/load_balancer/test_quota.py b/openstack/tests/unit/load_balancer/test_quota.py index 648dfc40a..e1ac59335 100644 --- a/openstack/tests/unit/load_balancer/test_quota.py +++ b/openstack/tests/unit/load_balancer/test_quota.py @@ -29,7 +29,6 @@ class TestQuota(base.TestCase): - def test_basic(self): sot = quota.Quota() self.assertEqual('quota', sot.resource_key) @@ -58,7 +57,6 @@ def test_prepare_request(self): class TestQuotaDefault(base.TestCase): - def test_basic(self): sot = quota.QuotaDefault() self.assertEqual('quota', sot.resource_key) diff --git a/openstack/tests/unit/load_balancer/test_version.py b/openstack/tests/unit/load_balancer/test_version.py index db3e53c1c..4cf818032 100644 --- a/openstack/tests/unit/load_balancer/test_version.py +++ b/openstack/tests/unit/load_balancer/test_version.py @@ -23,7 +23,6 @@ class TestVersion(base.TestCase): - def test_basic(self): sot = version.Version() self.assertEqual('version', sot.resource_key) diff --git a/openstack/tests/unit/load_balancer/v2/test_proxy.py b/openstack/tests/unit/load_balancer/v2/test_proxy.py index c045826e9..ee663969a 100644 --- a/openstack/tests/unit/load_balancer/v2/test_proxy.py +++ b/openstack/tests/unit/load_balancer/v2/test_proxy.py @@ -46,24 +46,22 @@ def setUp(self): self.proxy = _proxy.Proxy(self.session) def test_load_balancers(self): - self.verify_list(self.proxy.load_balancers, - lb.LoadBalancer) + self.verify_list(self.proxy.load_balancers, lb.LoadBalancer) def test_load_balancer_get(self): - self.verify_get(self.proxy.get_load_balancer, - lb.LoadBalancer) + self.verify_get(self.proxy.get_load_balancer, lb.LoadBalancer) def test_load_balancer_stats_get(self): - self.verify_get(self.proxy.get_load_balancer_statistics, - lb.LoadBalancerStats, - method_args=[self.LB_ID], - expected_args=[], - expected_kwargs={'lb_id': self.LB_ID, - 'requires_id': False}) + self.verify_get( + self.proxy.get_load_balancer_statistics, + lb.LoadBalancerStats, + method_args=[self.LB_ID], + expected_args=[], + expected_kwargs={'lb_id': self.LB_ID, 'requires_id': False}, + ) def test_load_balancer_create(self): - self.verify_create(self.proxy.create_load_balancer, - lb.LoadBalancer) + self.verify_create(self.proxy.create_load_balancer, lb.LoadBalancer) @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_load_balancer_delete_non_cascade(self, mock_get_resource): @@ -75,10 +73,12 @@ def test_load_balancer_delete_non_cascade(self, mock_get_resource): self.proxy.delete_load_balancer, method_args=["resource_or_id", True, False], expected_args=[lb.LoadBalancer, fake_load_balancer], - expected_kwargs={"ignore_missing": True}) + expected_kwargs={"ignore_missing": True}, + ) self.assertFalse(fake_load_balancer.cascade) - mock_get_resource.assert_called_once_with(lb.LoadBalancer, - "resource_or_id") + mock_get_resource.assert_called_once_with( + lb.LoadBalancer, "resource_or_id" + ) @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_load_balancer_delete_cascade(self, mock_get_resource): @@ -90,108 +90,108 @@ def test_load_balancer_delete_cascade(self, mock_get_resource): self.proxy.delete_load_balancer, method_args=["resource_or_id", True, True], expected_args=[lb.LoadBalancer, fake_load_balancer], - expected_kwargs={"ignore_missing": True}) + expected_kwargs={"ignore_missing": True}, + ) self.assertTrue(fake_load_balancer.cascade) - mock_get_resource.assert_called_once_with(lb.LoadBalancer, - "resource_or_id") + mock_get_resource.assert_called_once_with( + lb.LoadBalancer, "resource_or_id" + ) def test_load_balancer_find(self): - self.verify_find(self.proxy.find_load_balancer, - lb.LoadBalancer) + self.verify_find(self.proxy.find_load_balancer, lb.LoadBalancer) def test_load_balancer_update(self): - self.verify_update(self.proxy.update_load_balancer, - lb.LoadBalancer) + self.verify_update(self.proxy.update_load_balancer, lb.LoadBalancer) def test_load_balancer_failover(self): - self.verify_update(self.proxy.failover_load_balancer, - lb.LoadBalancerFailover, - method_args=[self.LB_ID], - expected_args=[], - expected_kwargs={'lb_id': self.LB_ID}) + self.verify_update( + self.proxy.failover_load_balancer, + lb.LoadBalancerFailover, + method_args=[self.LB_ID], + expected_args=[], + expected_kwargs={'lb_id': self.LB_ID}, + ) def test_listeners(self): - self.verify_list(self.proxy.listeners, - listener.Listener) + self.verify_list(self.proxy.listeners, listener.Listener) def test_listener_get(self): - self.verify_get(self.proxy.get_listener, - listener.Listener) + self.verify_get(self.proxy.get_listener, listener.Listener) def test_listener_stats_get(self): - self.verify_get(self.proxy.get_listener_statistics, - listener.ListenerStats, - method_args=[self.LISTENER_ID], - expected_args=[], - expected_kwargs={'listener_id': self.LISTENER_ID, - 'requires_id': False}) + self.verify_get( + self.proxy.get_listener_statistics, + listener.ListenerStats, + method_args=[self.LISTENER_ID], + expected_args=[], + expected_kwargs={ + 'listener_id': self.LISTENER_ID, + 'requires_id': False, + }, + ) def test_listener_create(self): - self.verify_create(self.proxy.create_listener, - listener.Listener) + self.verify_create(self.proxy.create_listener, listener.Listener) def test_listener_delete(self): - self.verify_delete(self.proxy.delete_listener, - listener.Listener, True) + self.verify_delete(self.proxy.delete_listener, listener.Listener, True) def test_listener_find(self): - self.verify_find(self.proxy.find_listener, - listener.Listener) + self.verify_find(self.proxy.find_listener, listener.Listener) def test_listener_update(self): - self.verify_update(self.proxy.update_listener, - listener.Listener) + self.verify_update(self.proxy.update_listener, listener.Listener) def test_pools(self): - self.verify_list(self.proxy.pools, - pool.Pool) + self.verify_list(self.proxy.pools, pool.Pool) def test_pool_get(self): - self.verify_get(self.proxy.get_pool, - pool.Pool) + self.verify_get(self.proxy.get_pool, pool.Pool) def test_pool_create(self): - self.verify_create(self.proxy.create_pool, - pool.Pool) + self.verify_create(self.proxy.create_pool, pool.Pool) def test_pool_delete(self): - self.verify_delete(self.proxy.delete_pool, - pool.Pool, True) + self.verify_delete(self.proxy.delete_pool, pool.Pool, True) def test_pool_find(self): - self.verify_find(self.proxy.find_pool, - pool.Pool) + self.verify_find(self.proxy.find_pool, pool.Pool) def test_pool_update(self): - self.verify_update(self.proxy.update_pool, - pool.Pool) + self.verify_update(self.proxy.update_pool, pool.Pool) def test_members(self): - self.verify_list(self.proxy.members, - member.Member, - method_kwargs={'pool': self.POOL_ID}, - expected_kwargs={'pool_id': self.POOL_ID}) + self.verify_list( + self.proxy.members, + member.Member, + method_kwargs={'pool': self.POOL_ID}, + expected_kwargs={'pool_id': self.POOL_ID}, + ) def test_member_get(self): - self.verify_get(self.proxy.get_member, - member.Member, - method_kwargs={'pool': self.POOL_ID}, - expected_kwargs={'pool_id': self.POOL_ID}) + self.verify_get( + self.proxy.get_member, + member.Member, + method_kwargs={'pool': self.POOL_ID}, + expected_kwargs={'pool_id': self.POOL_ID}, + ) def test_member_create(self): - self.verify_create(self.proxy.create_member, - member.Member, - method_kwargs={'pool': self.POOL_ID}, - expected_kwargs={'pool_id': self.POOL_ID}) + self.verify_create( + self.proxy.create_member, + member.Member, + method_kwargs={'pool': self.POOL_ID}, + expected_kwargs={'pool_id': self.POOL_ID}, + ) def test_member_delete(self): - self.verify_delete(self.proxy.delete_member, - member.Member, - ignore_missing=True, - method_kwargs={'pool': self.POOL_ID}, - expected_kwargs={ - 'pool_id': self.POOL_ID, - 'ignore_missing': True}) + self.verify_delete( + self.proxy.delete_member, + member.Member, + ignore_missing=True, + method_kwargs={'pool': self.POOL_ID}, + expected_kwargs={'pool_id': self.POOL_ID, 'ignore_missing': True}, + ) def test_member_find(self): self._verify( @@ -199,7 +199,8 @@ def test_member_find(self): self.proxy.find_member, method_args=["MEMBER", self.POOL_ID], expected_args=[member.Member, "MEMBER"], - expected_kwargs={"pool_id": self.POOL_ID, "ignore_missing": True}) + expected_kwargs={"pool_id": self.POOL_ID, "ignore_missing": True}, + ) def test_member_update(self): self._verify( @@ -207,80 +208,93 @@ def test_member_update(self): self.proxy.update_member, method_args=["MEMBER", self.POOL_ID], expected_args=[member.Member, "MEMBER"], - expected_kwargs={"pool_id": self.POOL_ID}) + expected_kwargs={"pool_id": self.POOL_ID}, + ) def test_health_monitors(self): - self.verify_list(self.proxy.health_monitors, - health_monitor.HealthMonitor) + self.verify_list( + self.proxy.health_monitors, health_monitor.HealthMonitor + ) def test_health_monitor_get(self): - self.verify_get(self.proxy.get_health_monitor, - health_monitor.HealthMonitor) + self.verify_get( + self.proxy.get_health_monitor, health_monitor.HealthMonitor + ) def test_health_monitor_create(self): - self.verify_create(self.proxy.create_health_monitor, - health_monitor.HealthMonitor) + self.verify_create( + self.proxy.create_health_monitor, health_monitor.HealthMonitor + ) def test_health_monitor_delete(self): - self.verify_delete(self.proxy.delete_health_monitor, - health_monitor.HealthMonitor, True) + self.verify_delete( + self.proxy.delete_health_monitor, + health_monitor.HealthMonitor, + True, + ) def test_health_monitor_find(self): - self.verify_find(self.proxy.find_health_monitor, - health_monitor.HealthMonitor) + self.verify_find( + self.proxy.find_health_monitor, health_monitor.HealthMonitor + ) def test_health_monitor_update(self): - self.verify_update(self.proxy.update_health_monitor, - health_monitor.HealthMonitor) + self.verify_update( + self.proxy.update_health_monitor, health_monitor.HealthMonitor + ) def test_l7_policies(self): - self.verify_list(self.proxy.l7_policies, - l7_policy.L7Policy) + self.verify_list(self.proxy.l7_policies, l7_policy.L7Policy) def test_l7_policy_get(self): - self.verify_get(self.proxy.get_l7_policy, - l7_policy.L7Policy) + self.verify_get(self.proxy.get_l7_policy, l7_policy.L7Policy) def test_l7_policy_create(self): - self.verify_create(self.proxy.create_l7_policy, - l7_policy.L7Policy) + self.verify_create(self.proxy.create_l7_policy, l7_policy.L7Policy) def test_l7_policy_delete(self): - self.verify_delete(self.proxy.delete_l7_policy, - l7_policy.L7Policy, True) + self.verify_delete( + self.proxy.delete_l7_policy, l7_policy.L7Policy, True + ) def test_l7_policy_find(self): - self.verify_find(self.proxy.find_l7_policy, - l7_policy.L7Policy) + self.verify_find(self.proxy.find_l7_policy, l7_policy.L7Policy) def test_l7_policy_update(self): - self.verify_update(self.proxy.update_l7_policy, - l7_policy.L7Policy) + self.verify_update(self.proxy.update_l7_policy, l7_policy.L7Policy) def test_l7_rules(self): - self.verify_list(self.proxy.l7_rules, - l7_rule.L7Rule, - method_kwargs={'l7_policy': self.L7_POLICY_ID}, - expected_kwargs={'l7policy_id': self.L7_POLICY_ID}) + self.verify_list( + self.proxy.l7_rules, + l7_rule.L7Rule, + method_kwargs={'l7_policy': self.L7_POLICY_ID}, + expected_kwargs={'l7policy_id': self.L7_POLICY_ID}, + ) def test_l7_rule_get(self): - self.verify_get(self.proxy.get_l7_rule, - l7_rule.L7Rule, - method_kwargs={'l7_policy': self.L7_POLICY_ID}, - expected_kwargs={'l7policy_id': self.L7_POLICY_ID}) + self.verify_get( + self.proxy.get_l7_rule, + l7_rule.L7Rule, + method_kwargs={'l7_policy': self.L7_POLICY_ID}, + expected_kwargs={'l7policy_id': self.L7_POLICY_ID}, + ) def test_l7_rule_create(self): - self.verify_create(self.proxy.create_l7_rule, - l7_rule.L7Rule, - method_kwargs={'l7_policy': self.L7_POLICY_ID}, - expected_kwargs={'l7policy_id': self.L7_POLICY_ID}) + self.verify_create( + self.proxy.create_l7_rule, + l7_rule.L7Rule, + method_kwargs={'l7_policy': self.L7_POLICY_ID}, + expected_kwargs={'l7policy_id': self.L7_POLICY_ID}, + ) def test_l7_rule_delete(self): - self.verify_delete(self.proxy.delete_l7_rule, - l7_rule.L7Rule, - ignore_missing=True, - method_kwargs={'l7_policy': self.L7_POLICY_ID}, - expected_kwargs={'l7policy_id': self.L7_POLICY_ID}) + self.verify_delete( + self.proxy.delete_l7_rule, + l7_rule.L7Rule, + ignore_missing=True, + method_kwargs={'l7_policy': self.L7_POLICY_ID}, + expected_kwargs={'l7policy_id': self.L7_POLICY_ID}, + ) def test_l7_rule_find(self): self._verify( @@ -289,7 +303,10 @@ def test_l7_rule_find(self): method_args=["RULE", self.L7_POLICY_ID], expected_args=[l7_rule.L7Rule, "RULE"], expected_kwargs={ - "l7policy_id": self.L7_POLICY_ID, "ignore_missing": True}) + "l7policy_id": self.L7_POLICY_ID, + "ignore_missing": True, + }, + ) def test_l7_rule_update(self): self._verify( @@ -297,7 +314,8 @@ def test_l7_rule_update(self): self.proxy.update_l7_rule, method_args=["RULE", self.L7_POLICY_ID], expected_args=[l7_rule.L7Rule, "RULE"], - expected_kwargs={"l7policy_id": self.L7_POLICY_ID}) + expected_kwargs={"l7policy_id": self.L7_POLICY_ID}, + ) def test_quotas(self): self.verify_list(self.proxy.quotas, quota.Quota) @@ -313,7 +331,8 @@ def test_quota_default_get(self): "openstack.proxy.Proxy._get", self.proxy.get_quota_default, expected_args=[quota.QuotaDefault], - expected_kwargs={'requires_id': False}) + expected_kwargs={'requires_id': False}, + ) def test_quota_delete(self): self.verify_delete(self.proxy.delete_quota, quota.Quota, False) @@ -325,35 +344,45 @@ def test_providers(self): self.verify_list(self.proxy.providers, provider.Provider) def test_provider_flavor_capabilities(self): - self.verify_list(self.proxy.provider_flavor_capabilities, - provider.ProviderFlavorCapabilities, - method_args=[self.AMPHORA], - expected_args=[], - expected_kwargs={'provider': self.AMPHORA}) + self.verify_list( + self.proxy.provider_flavor_capabilities, + provider.ProviderFlavorCapabilities, + method_args=[self.AMPHORA], + expected_args=[], + expected_kwargs={'provider': self.AMPHORA}, + ) def test_flavor_profiles(self): - self.verify_list(self.proxy.flavor_profiles, - flavor_profile.FlavorProfile) + self.verify_list( + self.proxy.flavor_profiles, flavor_profile.FlavorProfile + ) def test_flavor_profile_get(self): - self.verify_get(self.proxy.get_flavor_profile, - flavor_profile.FlavorProfile) + self.verify_get( + self.proxy.get_flavor_profile, flavor_profile.FlavorProfile + ) def test_flavor_profile_create(self): - self.verify_create(self.proxy.create_flavor_profile, - flavor_profile.FlavorProfile) + self.verify_create( + self.proxy.create_flavor_profile, flavor_profile.FlavorProfile + ) def test_flavor_profile_delete(self): - self.verify_delete(self.proxy.delete_flavor_profile, - flavor_profile.FlavorProfile, True) + self.verify_delete( + self.proxy.delete_flavor_profile, + flavor_profile.FlavorProfile, + True, + ) def test_flavor_profile_find(self): - self.verify_find(self.proxy.find_flavor_profile, - flavor_profile.FlavorProfile) + self.verify_find( + self.proxy.find_flavor_profile, flavor_profile.FlavorProfile + ) def test_flavor_profile_update(self): - self.verify_update(self.proxy.update_flavor_profile, - flavor_profile.FlavorProfile) + self.verify_update( + self.proxy.update_flavor_profile, flavor_profile.FlavorProfile + ) def test_flavors(self): self.verify_list(self.proxy.flavors, flavor.Flavor) @@ -383,64 +412,92 @@ def test_amphora_find(self): self.verify_find(self.proxy.find_amphora, amphora.Amphora) def test_amphora_configure(self): - self.verify_update(self.proxy.configure_amphora, - amphora.AmphoraConfig, - method_args=[self.AMPHORA_ID], - expected_args=[], - expected_kwargs={'amphora_id': self.AMPHORA_ID}) + self.verify_update( + self.proxy.configure_amphora, + amphora.AmphoraConfig, + method_args=[self.AMPHORA_ID], + expected_args=[], + expected_kwargs={'amphora_id': self.AMPHORA_ID}, + ) def test_amphora_failover(self): - self.verify_update(self.proxy.failover_amphora, - amphora.AmphoraFailover, - method_args=[self.AMPHORA_ID], - expected_args=[], - expected_kwargs={'amphora_id': self.AMPHORA_ID}) + self.verify_update( + self.proxy.failover_amphora, + amphora.AmphoraFailover, + method_args=[self.AMPHORA_ID], + expected_args=[], + expected_kwargs={'amphora_id': self.AMPHORA_ID}, + ) def test_availability_zone_profiles(self): - self.verify_list(self.proxy.availability_zone_profiles, - availability_zone_profile.AvailabilityZoneProfile) + self.verify_list( + self.proxy.availability_zone_profiles, + availability_zone_profile.AvailabilityZoneProfile, + ) def test_availability_zone_profile_get(self): - self.verify_get(self.proxy.get_availability_zone_profile, - availability_zone_profile.AvailabilityZoneProfile) + self.verify_get( + self.proxy.get_availability_zone_profile, + availability_zone_profile.AvailabilityZoneProfile, + ) def test_availability_zone_profile_create(self): - self.verify_create(self.proxy.create_availability_zone_profile, - availability_zone_profile.AvailabilityZoneProfile) + self.verify_create( + self.proxy.create_availability_zone_profile, + availability_zone_profile.AvailabilityZoneProfile, + ) def test_availability_zone_profile_delete(self): - self.verify_delete(self.proxy.delete_availability_zone_profile, - availability_zone_profile.AvailabilityZoneProfile, - True) + self.verify_delete( + self.proxy.delete_availability_zone_profile, + availability_zone_profile.AvailabilityZoneProfile, + True, + ) def test_availability_zone_profile_find(self): - self.verify_find(self.proxy.find_availability_zone_profile, - availability_zone_profile.AvailabilityZoneProfile) + self.verify_find( + self.proxy.find_availability_zone_profile, + availability_zone_profile.AvailabilityZoneProfile, + ) def test_availability_zone_profile_update(self): - self.verify_update(self.proxy.update_availability_zone_profile, - availability_zone_profile.AvailabilityZoneProfile) + self.verify_update( + self.proxy.update_availability_zone_profile, + availability_zone_profile.AvailabilityZoneProfile, + ) def test_availability_zones(self): - self.verify_list(self.proxy.availability_zones, - availability_zone.AvailabilityZone) + self.verify_list( + self.proxy.availability_zones, availability_zone.AvailabilityZone + ) def test_availability_zone_get(self): - self.verify_get(self.proxy.get_availability_zone, - availability_zone.AvailabilityZone) + self.verify_get( + self.proxy.get_availability_zone, + availability_zone.AvailabilityZone, + ) def test_availability_zone_create(self): - self.verify_create(self.proxy.create_availability_zone, - availability_zone.AvailabilityZone) + self.verify_create( + self.proxy.create_availability_zone, + availability_zone.AvailabilityZone, + ) def test_availability_zone_delete(self): - self.verify_delete(self.proxy.delete_availability_zone, - availability_zone.AvailabilityZone, True) + self.verify_delete( + self.proxy.delete_availability_zone, + availability_zone.AvailabilityZone, + True, + ) def test_availability_zone_find(self): - self.verify_find(self.proxy.find_availability_zone, - availability_zone.AvailabilityZone) + self.verify_find( + self.proxy.find_availability_zone, + availability_zone.AvailabilityZone, + ) def test_availability_zone_update(self): - self.verify_update(self.proxy.update_availability_zone, - availability_zone.AvailabilityZone) + self.verify_update( + self.proxy.update_availability_zone, + availability_zone.AvailabilityZone, + ) From 3d2511f98025d2d2826e13cea8be7545e90990f7 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 5 May 2023 10:59:36 +0100 Subject: [PATCH 3244/3836] Blackify openstack.shared_file_system Black used with the '-l 79 -S' flags. A future change will ignore this commit in git-blame history by adding a 'git-blame-ignore-revs' file. Change-Id: I54209e6cbfeec18a15771882d38730aca273238c Signed-off-by: Stephen Finucane --- .../shared_file_system_service.py | 1 + openstack/shared_file_system/v2/_proxy.py | 161 +++++++------- openstack/shared_file_system/v2/limit.py | 39 ++-- openstack/shared_file_system/v2/share.py | 24 +-- .../v2/share_access_rule.py | 24 +-- .../shared_file_system/v2/share_instance.py | 5 +- .../shared_file_system/v2/share_network.py | 12 +- .../v2/share_network_subnet.py | 7 +- .../shared_file_system/v2/share_snapshot.py | 7 +- .../shared_file_system/v2/storage_pool.py | 6 +- .../shared_file_system/v2/user_message.py | 4 +- .../functional/shared_file_system/base.py | 39 ++-- .../test_export_locations.py | 16 +- .../shared_file_system/test_limit.py | 31 +-- .../shared_file_system/test_share.py | 73 ++++--- .../test_share_access_rule.py | 35 +-- .../shared_file_system/test_share_instance.py | 42 ++-- .../shared_file_system/test_share_network.py | 20 +- .../test_share_network_subnet.py | 71 +++--- .../shared_file_system/test_share_snapshot.py | 50 +++-- .../test_share_snapshot_instance.py | 16 +- .../shared_file_system/test_storage_pool.py | 10 +- .../shared_file_system/test_user_message.py | 19 +- .../v2/test_availability_zone.py | 1 - .../unit/shared_file_system/v2/test_limit.py | 73 ++++--- .../unit/shared_file_system/v2/test_proxy.py | 202 ++++++++++-------- .../unit/shared_file_system/v2/test_share.py | 98 +++++---- .../v2/test_share_access_rule.py | 15 +- .../v2/test_share_export_locations.py | 15 +- .../v2/test_share_instance.py | 56 +++-- .../v2/test_share_network.py | 39 ++-- .../v2/test_share_network_subnet.py | 39 ++-- .../v2/test_share_snapshot.py | 35 ++- .../v2/test_share_snapshot_instance.py | 9 +- .../v2/test_storage_pool.py | 32 +-- .../v2/test_user_message.py | 32 ++- 36 files changed, 763 insertions(+), 595 deletions(-) diff --git a/openstack/shared_file_system/shared_file_system_service.py b/openstack/shared_file_system/shared_file_system_service.py index bf6c7541a..8ac842fcb 100644 --- a/openstack/shared_file_system/shared_file_system_service.py +++ b/openstack/shared_file_system/shared_file_system_service.py @@ -16,6 +16,7 @@ class SharedFilesystemService(service_description.ServiceDescription): """The shared file systems service.""" + supported_versions = { '2': _proxy.Proxy, } diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index 91832e54a..8402d49dd 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -13,34 +13,27 @@ from openstack import proxy from openstack import resource from openstack.shared_file_system.v2 import ( - availability_zone as _availability_zone) -from openstack.shared_file_system.v2 import ( - share_access_rule as _share_access_rule -) -from openstack.shared_file_system.v2 import ( - share_export_locations as _share_export_locations -) -from openstack.shared_file_system.v2 import ( - share_network as _share_network -) -from openstack.shared_file_system.v2 import ( - share_network_subnet as _share_network_subnet + availability_zone as _availability_zone, ) +from openstack.shared_file_system.v2 import limit as _limit +from openstack.shared_file_system.v2 import share as _share from openstack.shared_file_system.v2 import ( - share_snapshot as _share_snapshot + share_access_rule as _share_access_rule, ) from openstack.shared_file_system.v2 import ( - share_snapshot_instance as _share_snapshot_instance + share_export_locations as _share_export_locations, ) +from openstack.shared_file_system.v2 import share_instance as _share_instance +from openstack.shared_file_system.v2 import share_network as _share_network from openstack.shared_file_system.v2 import ( - storage_pool as _storage_pool + share_network_subnet as _share_network_subnet, ) +from openstack.shared_file_system.v2 import share_snapshot as _share_snapshot from openstack.shared_file_system.v2 import ( - user_message as _user_message + share_snapshot_instance as _share_snapshot_instance, ) -from openstack.shared_file_system.v2 import limit as _limit -from openstack.shared_file_system.v2 import share as _share -from openstack.shared_file_system.v2 import share_instance as _share_instance +from openstack.shared_file_system.v2 import storage_pool as _storage_pool +from openstack.shared_file_system.v2 import user_message as _user_message class Proxy(proxy.Proxy): @@ -53,11 +46,9 @@ class Proxy(proxy.Proxy): "share": _share.Share, "share_network": _share_network.ShareNetwork, "share_network_subnet": _share_network_subnet.ShareNetworkSubnet, - "share_snapshot_instance": - _share_snapshot_instance.ShareSnapshotInstance, + "share_snapshot_instance": _share_snapshot_instance.ShareSnapshotInstance, # noqa: E501 "share_instance": _share_instance.ShareInstance, - "share_export_locations": - _share_export_locations.ShareExportLocation, + "share_export_locations": _share_export_locations.ShareExportLocation, "share_access_rule": _share_access_rule.ShareAccessRule, } @@ -134,8 +125,7 @@ def delete_share(self, share, ignore_missing=True): :returns: Result of the ``delete`` :rtype: ``None`` """ - self._delete(_share.Share, share, - ignore_missing=ignore_missing) + self._delete(_share.Share, share, ignore_missing=ignore_missing) def update_share(self, share_id, **attrs): """Updates details of a single share. @@ -172,12 +162,7 @@ def revert_share_to_snapshot(self, share_id, snapshot_id): res.revert_to_snapshot(self, snapshot_id) def resize_share( - self, - share_id, - new_size, - no_shrink=False, - no_extend=False, - force=False + self, share_id, new_size, no_shrink=False, no_extend=False, force=False ): """Resizes a share, extending/shrinking the share as needed. @@ -206,8 +191,9 @@ def resize_share( elif new_size < res.size and no_shrink is not True: res.shrink_share(self, new_size) - def wait_for_status(self, res, status='active', failures=None, - interval=2, wait=120): + def wait_for_status( + self, res, status='active', failures=None, interval=2, wait=120 + ): """Wait for a resource to be in a particular status. :param res: The resource to wait on to reach the specified status. The resource must have a ``status`` attribute. @@ -229,7 +215,8 @@ def wait_for_status(self, res, status='active', failures=None, """ failures = [] if failures is None else failures return resource.wait_for_status( - self, res, status, failures, interval, wait) + self, res, status, failures, interval, wait + ) def storage_pools(self, details=True, **query): """Lists all back-end storage pools with details @@ -248,7 +235,8 @@ def storage_pools(self, details=True, **query): """ base_path = '/scheduler-stats/pools/detail' if details else None return self._list( - _storage_pool.StoragePool, base_path=base_path, **query) + _storage_pool.StoragePool, base_path=base_path, **query + ) def user_messages(self, **query): """List shared file system user messages @@ -278,8 +266,7 @@ def user_messages(self, **query): :rtype: :class:`~openstack.shared_file_system.v2.user_message.UserMessage` """ - return self._list( - _user_message.UserMessage, **query) + return self._list(_user_message.UserMessage, **query) def get_user_message(self, message_id): """List details of a single user message @@ -300,8 +287,10 @@ def delete_user_message(self, message_id, ignore_missing=True): :class:`~openstack.shared_file_system.v2.user_message.UserMessage` """ return self._delete( - _user_message.UserMessage, message_id, - ignore_missing=ignore_missing) + _user_message.UserMessage, + message_id, + ignore_missing=ignore_missing, + ) def limits(self, **query): """Lists all share limits. @@ -312,8 +301,7 @@ def limits(self, **query): :returns: A generator of manila share limits resources :rtype: :class:`~openstack.shared_file_system.v2.limit.Limit` """ - return self._list( - _limit.Limit, **query) + return self._list(_limit.Limit, **query) def share_snapshots(self, details=True, **query): """Lists all share snapshots with details. @@ -329,7 +317,8 @@ def share_snapshots(self, details=True, **query): """ base_path = '/snapshots/detail' if details else None return self._list( - _share_snapshot.ShareSnapshot, base_path=base_path, **query) + _share_snapshot.ShareSnapshot, base_path=base_path, **query + ) def get_share_snapshot(self, snapshot_id): """Lists details of a single share snapshot @@ -359,8 +348,9 @@ def update_share_snapshot(self, snapshot_id, **attrs): :rtype: :class:`~openstack.shared_file_system.v2.share_snapshot.ShareSnapshot` """ - return self._update(_share_snapshot.ShareSnapshot, snapshot_id, - **attrs) + return self._update( + _share_snapshot.ShareSnapshot, snapshot_id, **attrs + ) def delete_share_snapshot(self, snapshot_id, ignore_missing=True): """Deletes a single share snapshot @@ -369,8 +359,11 @@ def delete_share_snapshot(self, snapshot_id, ignore_missing=True): :returns: Result of the ``delete`` :rtype: ``None`` """ - self._delete(_share_snapshot.ShareSnapshot, snapshot_id, - ignore_missing=ignore_missing) + self._delete( + _share_snapshot.ShareSnapshot, + snapshot_id, + ignore_missing=ignore_missing, + ) # ========= Network Subnets ========== def share_network_subnets(self, share_network_id): @@ -384,10 +377,13 @@ def share_network_subnets(self, share_network_id): """ return self._list( _share_network_subnet.ShareNetworkSubnet, - share_network_id=share_network_id) + share_network_id=share_network_id, + ) def get_share_network_subnet( - self, share_network_id, share_network_subnet_id, + self, + share_network_id, + share_network_subnet_id, ): """Lists details of a single share network subnet. @@ -403,7 +399,8 @@ def get_share_network_subnet( return self._get( _share_network_subnet.ShareNetworkSubnet, share_network_subnet_id, - share_network_id=share_network_id) + share_network_id=share_network_id, + ) def create_share_network_subnet(self, share_network_id, **attrs): """Creates a share network subnet from attributes @@ -419,7 +416,8 @@ def create_share_network_subnet(self, share_network_id, **attrs): return self._create( _share_network_subnet.ShareNetworkSubnet, **attrs, - share_network_id=share_network_id) + share_network_id=share_network_id + ) def delete_share_network_subnet( self, share_network_id, share_network_subnet, ignore_missing=True @@ -438,7 +436,8 @@ def delete_share_network_subnet( _share_network_subnet.ShareNetworkSubnet, share_network_subnet, share_network_id=share_network_id, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, + ) def wait_for_delete(self, res, interval=2, wait=120): """Wait for a resource to be deleted. @@ -474,8 +473,11 @@ def share_snapshot_instances(self, details=True, **query): share_snapshot_instance.ShareSnapshotInstance` """ base_path = '/snapshot-instances/detail' if details else None - return self._list(_share_snapshot_instance.ShareSnapshotInstance, - base_path=base_path, **query) + return self._list( + _share_snapshot_instance.ShareSnapshotInstance, + base_path=base_path, + **query + ) def get_share_snapshot_instance(self, snapshot_instance_id): """Lists details of a single share snapshot instance @@ -485,8 +487,10 @@ def get_share_snapshot_instance(self, snapshot_instance_id): :rtype: :class:`~openstack.shared_file_system.v2. share_snapshot_instance.ShareSnapshotInstance` """ - return self._get(_share_snapshot_instance.ShareSnapshotInstance, - snapshot_instance_id) + return self._get( + _share_snapshot_instance.ShareSnapshotInstance, + snapshot_instance_id, + ) def share_networks(self, details=True, **query): """Lists all share networks with details. @@ -508,7 +512,8 @@ def share_networks(self, details=True, **query): """ base_path = '/share-networks/detail' if details else None return self._list( - _share_network.ShareNetwork, base_path=base_path, **query) + _share_network.ShareNetwork, base_path=base_path, **query + ) def get_share_network(self, share_network_id): """Lists details of a single share network @@ -527,8 +532,10 @@ def delete_share_network(self, share_network_id, ignore_missing=True): :rtype: ``None`` """ self._delete( - _share_network.ShareNetwork, share_network_id, - ignore_missing=ignore_missing) + _share_network.ShareNetwork, + share_network_id, + ignore_missing=ignore_missing, + ) def update_share_network(self, share_network_id, **attrs): """Updates details of a single share network. @@ -540,7 +547,8 @@ def update_share_network(self, share_network_id, **attrs): share_network.ShareNetwork` """ return self._update( - _share_network.ShareNetwork, share_network_id, **attrs) + _share_network.ShareNetwork, share_network_id, **attrs + ) def create_share_network(self, **attrs): """Creates a share network from attributes @@ -570,8 +578,7 @@ def share_instances(self, **query): :rtype: :class:`~openstack.shared_file_system.v2. share_instance.ShareInstance` """ - return self._list( - _share_instance.ShareInstance, **query) + return self._list(_share_instance.ShareInstance, **query) def get_share_instance(self, share_instance_id): """Shows details for a single share instance @@ -592,8 +599,9 @@ def reset_share_instance_status(self, share_instance_id, status): :returns: ``None`` """ - res = self._get_resource(_share_instance.ShareInstance, - share_instance_id) + res = self._get_resource( + _share_instance.ShareInstance, share_instance_id + ) res.reset_status(self, status) def delete_share_instance(self, share_instance_id): @@ -603,8 +611,9 @@ def delete_share_instance(self, share_instance_id): :returns: ``None`` """ - res = self._get_resource(_share_instance.ShareInstance, - share_instance_id) + res = self._get_resource( + _share_instance.ShareInstance, share_instance_id + ) res.force_delete(self) def export_locations(self, share_id): @@ -615,8 +624,9 @@ def export_locations(self, share_id): :rtype: List of :class:`~openstack.shared_filesystem_storage.v2. share_export_locations.ShareExportLocations` """ - return self._list(_share_export_locations.ShareExportLocation, - share_id=share_id) + return self._list( + _share_export_locations.ShareExportLocation, share_id=share_id + ) def get_export_location(self, export_location, share_id): """List details of export location @@ -631,7 +641,9 @@ def get_export_location(self, export_location, share_id): export_location_id = resource.Resource._get_id(export_location) return self._get( _share_export_locations.ShareExportLocation, - export_location_id, share_id=share_id) + export_location_id, + share_id=share_id, + ) def access_rules(self, share, **query): """Lists the access rules on a share. @@ -642,8 +654,8 @@ def access_rules(self, share, **query): """ share = self._get_resource(_share.Share, share) return self._list( - _share_access_rule.ShareAccessRule, - share_id=share.id, **query) + _share_access_rule.ShareAccessRule, share_id=share.id, **query + ) def get_access_rule(self, access_id): """List details of an access rule. @@ -653,8 +665,7 @@ def get_access_rule(self, access_id): :rtype: :class:`~openstack.shared_file_system.v2. share_access_rules.ShareAccessRules` """ - return self._get( - _share_access_rule.ShareAccessRule, access_id) + return self._get(_share_access_rule.ShareAccessRule, access_id) def create_access_rule(self, share_id, **attrs): """Creates an access rule from attributes @@ -670,7 +681,8 @@ def create_access_rule(self, share_id, **attrs): """ base_path = "/shares/%s/action" % (share_id,) return self._create( - _share_access_rule.ShareAccessRule, base_path=base_path, **attrs) + _share_access_rule.ShareAccessRule, base_path=base_path, **attrs + ) def delete_access_rule(self, access_id, share_id, ignore_missing=True): """Deletes an access rule @@ -680,6 +692,5 @@ def delete_access_rule(self, access_id, share_id, ignore_missing=True): :rtype: ``None`` """ - res = self._get_resource( - _share_access_rule.ShareAccessRule, access_id) + res = self._get_resource(_share_access_rule.ShareAccessRule, access_id) res.delete(self, share_id, ignore_missing=ignore_missing) diff --git a/openstack/shared_file_system/v2/limit.py b/openstack/shared_file_system/v2/limit.py index ef95ad1b2..3991dcb0a 100644 --- a/openstack/shared_file_system/v2/limit.py +++ b/openstack/shared_file_system/v2/limit.py @@ -29,47 +29,46 @@ class Limit(resource.Resource): #: The maximum number of replica gigabytes that are allowed #: in a project. maxTotalReplicaGigabytes = resource.Body( - "maxTotalReplicaGigabytes", type=int) + "maxTotalReplicaGigabytes", type=int + ) #: The total maximum number of shares that are allowed in a project. maxTotalShares = resource.Body("maxTotalShares", type=int) #: The total maximum number of share gigabytes that are allowed in a #: project. - maxTotalShareGigabytes = resource.Body( - "maxTotalShareGigabytes", type=int) + maxTotalShareGigabytes = resource.Body("maxTotalShareGigabytes", type=int) #: The total maximum number of share-networks that are allowed in a #: project. - maxTotalShareNetworks = resource.Body( - "maxTotalShareNetworks", type=int) + maxTotalShareNetworks = resource.Body("maxTotalShareNetworks", type=int) #: The total maximum number of share snapshots that are allowed in a #: project. - maxTotalShareSnapshots = resource.Body( - "maxTotalShareSnapshots", type=int) + maxTotalShareSnapshots = resource.Body("maxTotalShareSnapshots", type=int) #: The maximum number of share replicas that is allowed. - maxTotalShareReplicas = resource.Body( - "maxTotalShareReplicas", type=int) + maxTotalShareReplicas = resource.Body("maxTotalShareReplicas", type=int) #: The total maximum number of snapshot gigabytes that are allowed #: in a project. maxTotalSnapshotGigabytes = resource.Body( - "maxTotalSnapshotGigabytes", type=int) + "maxTotalSnapshotGigabytes", type=int + ) #: The total number of replica gigabytes used in a project by #: share replicas. totalReplicaGigabytesUsed = resource.Body( - "totalReplicaGigabytesUsed", type=int) + "totalReplicaGigabytesUsed", type=int + ) #: The total number of gigabytes used in a project by shares. totalShareGigabytesUsed = resource.Body( - "totalShareGigabytesUsed", type=int) + "totalShareGigabytesUsed", type=int + ) #: The total number of created shares in a project. - totalSharesUsed = resource.Body( - "totalSharesUsed", type=int) + totalSharesUsed = resource.Body("totalSharesUsed", type=int) #: The total number of created share-networks in a project. - totalShareNetworksUsed = resource.Body( - "totalShareNetworksUsed", type=int) + totalShareNetworksUsed = resource.Body("totalShareNetworksUsed", type=int) #: The total number of created share snapshots in a project. totalShareSnapshotsUsed = resource.Body( - "totalShareSnapshotsUsed", type=int) + "totalShareSnapshotsUsed", type=int + ) #: The total number of gigabytes used in a project by snapshots. totalSnapshotGigabytesUsed = resource.Body( - "totalSnapshotGigabytesUsed", type=int) + "totalSnapshotGigabytesUsed", type=int + ) #: The total number of created share replicas in a project. - totalShareReplicasUsed = resource.Body( - "totalShareReplicasUsed", type=int) + totalShareReplicasUsed = resource.Body("totalShareReplicasUsed", type=int) diff --git a/openstack/shared_file_system/v2/share.py b/openstack/shared_file_system/v2/share.py index 526e60475..cb13b07b0 100644 --- a/openstack/shared_file_system/v2/share.py +++ b/openstack/shared_file_system/v2/share.py @@ -46,18 +46,20 @@ class Share(resource.Resource): #: Whether or not this share supports snapshots that can be #: cloned into new shares. is_creating_new_share_from_snapshot_supported = resource.Body( - "create_share_from_snapshot_support", type=bool) + "create_share_from_snapshot_support", type=bool + ) #: Whether the share's snapshots can be mounted directly and access #: controlled independently or not. is_mounting_snapshot_supported = resource.Body( - "mount_snapshot_support", type=bool) + "mount_snapshot_support", type=bool + ) #: Whether the share can be reverted to its latest snapshot or not. is_reverting_to_snapshot_supported = resource.Body( - "revert_to_snapshot_support", type=bool) + "revert_to_snapshot_support", type=bool + ) #: An extra specification that filters back ends by whether the share #: supports snapshots or not. - is_snapshot_supported = resource.Body( - "snapshot_support", type=bool) + is_snapshot_supported = resource.Body("snapshot_support", type=bool) #: Indicates whether the share has replicas or not. is_replicated = resource.Body("has_replicas", type=bool) #: One or more metadata key and value pairs as a dictionary of strings. @@ -91,7 +93,8 @@ class Share(resource.Resource): #: The ID of the group snapshot instance that was used to create #: this share. source_share_group_snapshot_member_id = resource.Body( - "source_share_group_snapshot_member_id", type=str) + "source_share_group_snapshot_member_id", type=str + ) #: The share status status = resource.Body("status", type=str) #: For the share migration, the migration task state. @@ -109,14 +112,11 @@ def _action(self, session, body, action='patch', microversion=None): headers = {'Accept': ''} if microversion is None: - microversion = \ - self._get_microversion(session, action=action) + microversion = self._get_microversion(session, action=action) response = session.post( - url, - json=body, - headers=headers, - microversion=microversion) + url, json=body, headers=headers, microversion=microversion + ) exceptions.raise_from_response(response) return response diff --git a/openstack/shared_file_system/v2/share_access_rule.py b/openstack/shared_file_system/v2/share_access_rule.py index c240f9b07..48138c373 100644 --- a/openstack/shared_file_system/v2/share_access_rule.py +++ b/openstack/shared_file_system/v2/share_access_rule.py @@ -55,28 +55,26 @@ class ShareAccessRule(resource.Resource): #: the service’s database. updated_at = resource.Body("updated_at", type=str) - def _action(self, session, body, url, - action='patch', microversion=None): + def _action(self, session, body, url, action='patch', microversion=None): headers = {'Accept': ''} if microversion is None: - microversion = \ - self._get_microversion(session, action=action) + microversion = self._get_microversion(session, action=action) session.post( - url, - json=body, - headers=headers, - microversion=microversion) + url, json=body, headers=headers, microversion=microversion + ) def create(self, session, **kwargs): - return super().create(session, - resource_request_key='allow_access', - resource_response_key='access', - **kwargs) + return super().create( + session, + resource_request_key='allow_access', + resource_response_key='access', + **kwargs + ) def delete(self, session, share_id, ignore_missing=True): - body = {'deny_access': {'access_id' : self.id}} + body = {'deny_access': {'access_id': self.id}} url = utils.urljoin('/shares', share_id, 'action') try: response = self._action(session, body, url) diff --git a/openstack/shared_file_system/v2/share_instance.py b/openstack/shared_file_system/v2/share_instance.py index d2873a18f..a0d7bd056 100644 --- a/openstack/shared_file_system/v2/share_instance.py +++ b/openstack/shared_file_system/v2/share_instance.py @@ -66,8 +66,9 @@ def _action(self, session, body, action='patch', microversion=None): # Set microversion override extra_attrs['microversion'] = microversion else: - extra_attrs['microversion'] = \ - self._get_microversion(session, action=action) + extra_attrs['microversion'] = self._get_microversion( + session, action=action + ) response = session.post(url, json=body, headers=headers, **extra_attrs) exceptions.raise_from_response(response) return response diff --git a/openstack/shared_file_system/v2/share_network.py b/openstack/shared_file_system/v2/share_network.py index 1cdc1431f..72f088ff0 100644 --- a/openstack/shared_file_system/v2/share_network.py +++ b/openstack/shared_file_system/v2/share_network.py @@ -27,9 +27,15 @@ class ShareNetwork(resource.Resource): allow_head = False _query_mapping = resource.QueryParameters( - "project_id", "name", "description", - "created_since", "created_before", "security_service_id", - "limit", "offset", all_projects="all_tenants", + "project_id", + "name", + "description", + "created_since", + "created_before", + "security_service_id", + "limit", + "offset", + all_projects="all_tenants", ) #: Properties diff --git a/openstack/shared_file_system/v2/share_network_subnet.py b/openstack/shared_file_system/v2/share_network_subnet.py index 7dc3cdacd..5d248b183 100644 --- a/openstack/shared_file_system/v2/share_network_subnet.py +++ b/openstack/shared_file_system/v2/share_network_subnet.py @@ -56,7 +56,6 @@ class ShareNetworkSubnet(resource.Resource): updated_at = resource.Body("updated_at", type=str) def create(self, session, **kwargs): - return super().\ - create(session, - resource_request_key='share-network-subnet', - **kwargs) + return super().create( + session, resource_request_key='share-network-subnet', **kwargs + ) diff --git a/openstack/shared_file_system/v2/share_snapshot.py b/openstack/shared_file_system/v2/share_snapshot.py index c270c23aa..999f7af6e 100644 --- a/openstack/shared_file_system/v2/share_snapshot.py +++ b/openstack/shared_file_system/v2/share_snapshot.py @@ -26,9 +26,7 @@ class ShareSnapshot(resource.Resource): allow_list = True allow_head = False - _query_mapping = resource.QueryParameters( - "snapshot_id" - ) + _query_mapping = resource.QueryParameters("snapshot_id") #: Properties #: The date and time stamp when the resource was @@ -39,8 +37,7 @@ class ShareSnapshot(resource.Resource): #: The user defined name of the resource. display_name = resource.Body("display_name", type=str) #: The user defined description of the resource - display_description = resource.Body( - "display_description", type=str) + display_description = resource.Body("display_description", type=str) #: ID of the project that the snapshot belongs to. project_id = resource.Body("project_id", type=str) #: The UUID of the source share that was used to diff --git a/openstack/shared_file_system/v2/storage_pool.py b/openstack/shared_file_system/v2/storage_pool.py index a15c323c0..0f271e802 100644 --- a/openstack/shared_file_system/v2/storage_pool.py +++ b/openstack/shared_file_system/v2/storage_pool.py @@ -27,7 +27,11 @@ class StoragePool(resource.Resource): allow_head = False _query_mapping = resource.QueryParameters( - 'pool', 'backend', 'host', 'capabilities', 'share_type', + 'pool', + 'backend', + 'host', + 'capabilities', + 'share_type', ) #: Properties diff --git a/openstack/shared_file_system/v2/user_message.py b/openstack/shared_file_system/v2/user_message.py index 896e52dec..01b9da714 100644 --- a/openstack/shared_file_system/v2/user_message.py +++ b/openstack/shared_file_system/v2/user_message.py @@ -25,9 +25,7 @@ class UserMessage(resource.Resource): allow_list = True allow_head = False - _query_mapping = resource.QueryParameters( - "message_id" - ) + _query_mapping = resource.QueryParameters("message_id") _max_microversion = '2.37' diff --git a/openstack/tests/functional/shared_file_system/base.py b/openstack/tests/functional/shared_file_system/base.py index 2dac91937..c9ab00a2d 100644 --- a/openstack/tests/functional/shared_file_system/base.py +++ b/openstack/tests/functional/shared_file_system/base.py @@ -20,40 +20,49 @@ class BaseSharedFileSystemTest(base.BaseFunctionalTest): def setUp(self): super(BaseSharedFileSystemTest, self).setUp() - self.require_service('shared-file-system', - min_microversion=self.min_microversion) + self.require_service( + 'shared-file-system', min_microversion=self.min_microversion + ) self._set_operator_cloud(shared_file_system_api_version='2.63') self._set_user_cloud(shared_file_system_api_version='2.63') def create_share(self, **kwargs): share = self.user_cloud.share.create_share(**kwargs) - self.addCleanup(self.user_cloud.share.delete_share, - share.id, - ignore_missing=True) + self.addCleanup( + self.user_cloud.share.delete_share, share.id, ignore_missing=True + ) self.user_cloud.share.wait_for_status( share, status='available', failures=['error'], interval=5, - wait=self._wait_for_timeout) + wait=self._wait_for_timeout, + ) self.assertIsNotNone(share.id) return share def create_share_snapshot(self, share_id, **kwargs): share_snapshot = self.user_cloud.share.create_share_snapshot( - share_id=share_id, force=True) - self.addCleanup(resource.wait_for_delete, - self.user_cloud.share, share_snapshot, - wait=self._wait_for_timeout, - interval=2) - self.addCleanup(self.user_cloud.share.delete_share_snapshot, - share_snapshot.id, - ignore_missing=False) + share_id=share_id, force=True + ) + self.addCleanup( + resource.wait_for_delete, + self.user_cloud.share, + share_snapshot, + wait=self._wait_for_timeout, + interval=2, + ) + self.addCleanup( + self.user_cloud.share.delete_share_snapshot, + share_snapshot.id, + ignore_missing=False, + ) self.user_cloud.share.wait_for_status( share_snapshot, status='available', failures=['error'], interval=5, - wait=self._wait_for_timeout) + wait=self._wait_for_timeout, + ) self.assertIsNotNone(share_snapshot.id) return share_snapshot diff --git a/openstack/tests/functional/shared_file_system/test_export_locations.py b/openstack/tests/functional/shared_file_system/test_export_locations.py index 3391590f2..9db3c003e 100644 --- a/openstack/tests/functional/shared_file_system/test_export_locations.py +++ b/openstack/tests/functional/shared_file_system/test_export_locations.py @@ -22,8 +22,12 @@ def setUp(self): self.SHARE_NAME = self.getUniqueString() my_share = self.create_share( - name=self.SHARE_NAME, size=2, share_type="dhss_false", - share_protocol='NFS', description=None) + name=self.SHARE_NAME, + size=2, + share_type="dhss_false", + share_protocol='NFS', + description=None, + ) self.SHARE_ID = my_share.id def test_export_locations(self): @@ -33,8 +37,12 @@ def test_export_locations(self): self.assertGreater(len(list(exs)), 0) for ex in exs: for attribute in ( - 'id', 'path', 'share_instance_id', - 'updated_at', 'created_at'): + 'id', + 'path', + 'share_instance_id', + 'updated_at', + 'created_at', + ): self.assertTrue(hasattr(ex, attribute)) self.assertIsInstance(getattr(ex, attribute), 'str') for attribute in ('is_preferred', 'is_admin'): diff --git a/openstack/tests/functional/shared_file_system/test_limit.py b/openstack/tests/functional/shared_file_system/test_limit.py index 47cee5257..91f6b2ca5 100644 --- a/openstack/tests/functional/shared_file_system/test_limit.py +++ b/openstack/tests/functional/shared_file_system/test_limit.py @@ -14,23 +14,24 @@ class LimitTest(base.BaseSharedFileSystemTest): - def test_limits(self): limits = self.user_cloud.shared_file_system.limits() self.assertGreater(len(list(limits)), 0) for limit in limits: - for attribute in ("maxTotalReplicaGigabytes", - "maxTotalShares", - "maxTotalShareGigabytes", - "maxTotalShareNetworks", - "maxTotalShareSnapshots", - "maxTotalShareReplicas", - "maxTotalSnapshotGigabytes", - "totalReplicaGigabytesUsed", - "totalShareGigabytesUsed", - "totalSharesUsed", - "totalShareNetworksUsed", - "totalShareSnapshotsUsed", - "totalSnapshotGigabytesUsed", - "totalShareReplicasUsed"): + for attribute in ( + "maxTotalReplicaGigabytes", + "maxTotalShares", + "maxTotalShareGigabytes", + "maxTotalShareNetworks", + "maxTotalShareSnapshots", + "maxTotalShareReplicas", + "maxTotalSnapshotGigabytes", + "totalReplicaGigabytesUsed", + "totalShareGigabytesUsed", + "totalSharesUsed", + "totalShareNetworksUsed", + "totalShareSnapshotsUsed", + "totalSnapshotGigabytesUsed", + "totalShareReplicasUsed", + ): self.assertTrue(hasattr(limit, attribute)) diff --git a/openstack/tests/functional/shared_file_system/test_share.py b/openstack/tests/functional/shared_file_system/test_share.py index 4c9f309f3..f51ac17b5 100644 --- a/openstack/tests/functional/shared_file_system/test_share.py +++ b/openstack/tests/functional/shared_file_system/test_share.py @@ -15,19 +15,20 @@ class ShareTest(base.BaseSharedFileSystemTest): - def setUp(self): super(ShareTest, self).setUp() self.SHARE_NAME = self.getUniqueString() my_share = self.create_share( - name=self.SHARE_NAME, size=2, share_type="dhss_false", - share_protocol='NFS', description=None) + name=self.SHARE_NAME, + size=2, + share_type="dhss_false", + share_protocol='NFS', + description=None, + ) self.SHARE_ID = my_share.id self.SHARE_SIZE = my_share.size - my_share_snapshot = self.create_share_snapshot( - share_id=self.SHARE_ID - ) + my_share_snapshot = self.create_share_snapshot(share_id=self.SHARE_ID) self.SHARE_SNAPSHOT_ID = my_share_snapshot.id def test_get(self): @@ -44,73 +45,73 @@ def test_list_share(self): def test_update(self): updated_share = self.user_cloud.share.update_share( - self.SHARE_ID, display_description='updated share') - get_updated_share = self.user_cloud.share.get_share( - updated_share.id) + self.SHARE_ID, display_description='updated share' + ) + get_updated_share = self.user_cloud.share.get_share(updated_share.id) self.assertEqual('updated share', get_updated_share.description) def test_revert_share_to_snapshot(self): self.user_cloud.share.revert_share_to_snapshot( - self.SHARE_ID, self.SHARE_SNAPSHOT_ID) - get_reverted_share = self.user_cloud.share.get_share( - self.SHARE_ID) + self.SHARE_ID, self.SHARE_SNAPSHOT_ID + ) + get_reverted_share = self.user_cloud.share.get_share(self.SHARE_ID) self.user_cloud.share.wait_for_status( get_reverted_share, status='available', failures=['error'], interval=5, - wait=self._wait_for_timeout) + wait=self._wait_for_timeout, + ) self.assertIsNotNone(get_reverted_share.id) def test_resize_share_larger(self): larger_size = 3 - self.user_cloud.share.resize_share( - self.SHARE_ID, larger_size) + self.user_cloud.share.resize_share(self.SHARE_ID, larger_size) - get_resized_share = self.user_cloud.share.get_share( - self.SHARE_ID) + get_resized_share = self.user_cloud.share.get_share(self.SHARE_ID) self.user_cloud.share.wait_for_status( get_resized_share, status='available', failures=['error'], interval=5, - wait=self._wait_for_timeout) + wait=self._wait_for_timeout, + ) self.assertEqual(larger_size, get_resized_share.size) def test_resize_share_smaller(self): # Resize to 3 GiB smaller_size = 1 - self.user_cloud.share.resize_share( - self.SHARE_ID, smaller_size) + self.user_cloud.share.resize_share(self.SHARE_ID, smaller_size) - get_resized_share = self.user_cloud.share.get_share( - self.SHARE_ID) + get_resized_share = self.user_cloud.share.get_share(self.SHARE_ID) self.user_cloud.share.wait_for_status( get_resized_share, status='available', failures=['error'], interval=5, - wait=self._wait_for_timeout) + wait=self._wait_for_timeout, + ) self.assertEqual(smaller_size, get_resized_share.size) def test_resize_share_larger_no_extend(self): larger_size = 3 self.user_cloud.share.resize_share( - self.SHARE_ID, larger_size, no_extend=True) + self.SHARE_ID, larger_size, no_extend=True + ) - get_resized_share = self.user_cloud.share.get_share( - self.SHARE_ID) + get_resized_share = self.user_cloud.share.get_share(self.SHARE_ID) self.user_cloud.share.wait_for_status( get_resized_share, status='available', failures=['error'], interval=5, - wait=self._wait_for_timeout) + wait=self._wait_for_timeout, + ) # Assert that no change was made. self.assertEqual(self.SHARE_SIZE, get_resized_share.size) @@ -119,17 +120,18 @@ def test_resize_share_smaller_no_shrink(self): smaller_size = 1 self.user_cloud.share.resize_share( - self.SHARE_ID, smaller_size, no_shrink=True) + self.SHARE_ID, smaller_size, no_shrink=True + ) - get_resized_share = self.user_cloud.share.get_share( - self.SHARE_ID) + get_resized_share = self.user_cloud.share.get_share(self.SHARE_ID) self.user_cloud.share.wait_for_status( get_resized_share, status='available', failures=['error'], interval=5, - wait=self._wait_for_timeout) + wait=self._wait_for_timeout, + ) # Assert that no change was made. self.assertEqual(self.SHARE_SIZE, get_resized_share.size) @@ -138,15 +140,16 @@ def test_resize_share_with_force(self): # Resize to 3 GiB larger_size = 3 self.user_cloud.share.resize_share( - self.SHARE_ID, larger_size, force=True) + self.SHARE_ID, larger_size, force=True + ) - get_resized_share = self.user_cloud.share.get_share( - self.SHARE_ID) + get_resized_share = self.user_cloud.share.get_share(self.SHARE_ID) self.user_cloud.share.wait_for_status( get_resized_share, status='available', failures=['error'], interval=5, - wait=self._wait_for_timeout) + wait=self._wait_for_timeout, + ) self.assertEqual(larger_size, get_resized_share.size) diff --git a/openstack/tests/functional/shared_file_system/test_share_access_rule.py b/openstack/tests/functional/shared_file_system/test_share_access_rule.py index b5e56d907..a45ebf5db 100644 --- a/openstack/tests/functional/shared_file_system/test_share_access_rule.py +++ b/openstack/tests/functional/shared_file_system/test_share_access_rule.py @@ -14,20 +14,24 @@ class ShareAccessRuleTest(base.BaseSharedFileSystemTest): - def setUp(self): super(ShareAccessRuleTest, self).setUp() self.SHARE_NAME = self.getUniqueString() mys = self.create_share( - name=self.SHARE_NAME, size=2, share_type="dhss_false", - share_protocol='NFS', description=None) + name=self.SHARE_NAME, + size=2, + share_type="dhss_false", + share_protocol='NFS', + description=None, + ) self.user_cloud.shared_file_system.wait_for_status( mys, status='available', failures=['error'], interval=5, - wait=self._wait_for_timeout) + wait=self._wait_for_timeout, + ) self.assertIsNotNone(mys) self.assertIsNotNone(mys.id) self.SHARE_ID = mys.id @@ -36,16 +40,14 @@ def setUp(self): self.SHARE_ID, access_level="rw", access_type="ip", - access_to="0.0.0.0/0" + access_to="0.0.0.0/0", ) self.ACCESS_ID = access_rule.id self.RESOURCE_KEY = access_rule.resource_key def tearDown(self): acr = self.user_cloud.share.delete_access_rule( - self.ACCESS_ID, - self.SHARE_ID, - ignore_missing=True + self.ACCESS_ID, self.SHARE_ID, ignore_missing=True ) self.assertIsNone(acr) @@ -59,12 +61,19 @@ def test_get_access_rule(self): def test_list_access_rules(self): rules = self.user_cloud.shared_file_system.access_rules( - self.SHARE, - details=True + self.SHARE, details=True ) self.assertGreater(len(list(rules)), 0) for rule in rules: - for attribute in ('id', 'created_at', 'updated_at', - 'access_level', 'access_type', 'access_to', - 'share_id', 'access_key', 'metadata'): + for attribute in ( + 'id', + 'created_at', + 'updated_at', + 'access_level', + 'access_type', + 'access_to', + 'share_id', + 'access_key', + 'metadata', + ): self.assertTrue(hasattr(rule, attribute)) diff --git a/openstack/tests/functional/shared_file_system/test_share_instance.py b/openstack/tests/functional/shared_file_system/test_share_instance.py index 86feea3c0..01ada1eec 100644 --- a/openstack/tests/functional/shared_file_system/test_share_instance.py +++ b/openstack/tests/functional/shared_file_system/test_share_instance.py @@ -25,8 +25,12 @@ def setUp(self): self.SHARE_NAME = self.getUniqueString() my_share = self.create_share( - name=self.SHARE_NAME, size=2, share_type="dhss_false", - share_protocol='NFS', description=None) + name=self.SHARE_NAME, + size=2, + share_type="dhss_false", + share_protocol='NFS', + description=None, + ) self.SHARE_ID = my_share.id instances_list = self.operator_cloud.share.share_instances() self.SHARE_INSTANCE_ID = None @@ -36,7 +40,8 @@ def setUp(self): def test_get(self): sot = self.operator_cloud.share.get_share_instance( - self.SHARE_INSTANCE_ID) + self.SHARE_INSTANCE_ID + ) assert isinstance(sot, _share_instance.ShareInstance) self.assertEqual(self.SHARE_INSTANCE_ID, sot.id) @@ -44,25 +49,36 @@ def test_list_share_instances(self): share_instances = self.operator_cloud.share.share_instances() self.assertGreater(len(list(share_instances)), 0) for share_instance in share_instances: - for attribute in ('id', 'name', 'created_at', - 'access_rules_status', - 'availability_zone'): + for attribute in ( + 'id', + 'name', + 'created_at', + 'access_rules_status', + 'availability_zone', + ): self.assertTrue(hasattr(share_instance, attribute)) def test_reset(self): res = self.operator_cloud.share.reset_share_instance_status( - self.SHARE_INSTANCE_ID, 'error') + self.SHARE_INSTANCE_ID, 'error' + ) self.assertIsNone(res) sot = self.operator_cloud.share.get_share_instance( - self.SHARE_INSTANCE_ID) + self.SHARE_INSTANCE_ID + ) self.assertEqual('error', sot.status) def test_delete(self): sot = self.operator_cloud.share.get_share_instance( - self.SHARE_INSTANCE_ID) + self.SHARE_INSTANCE_ID + ) fdel = self.operator_cloud.share.delete_share_instance( - self.SHARE_INSTANCE_ID) - resource.wait_for_delete(self.operator_cloud.share, sot, - wait=self._wait_for_timeout, - interval=2) + self.SHARE_INSTANCE_ID + ) + resource.wait_for_delete( + self.operator_cloud.share, + sot, + wait=self._wait_for_timeout, + interval=2, + ) self.assertIsNone(fdel) diff --git a/openstack/tests/functional/shared_file_system/test_share_network.py b/openstack/tests/functional/shared_file_system/test_share_network.py index 72880df96..affc2143e 100644 --- a/openstack/tests/functional/shared_file_system/test_share_network.py +++ b/openstack/tests/functional/shared_file_system/test_share_network.py @@ -15,27 +15,28 @@ class ShareNetworkTest(base.BaseSharedFileSystemTest): - def setUp(self): super(ShareNetworkTest, self).setUp() self.SHARE_NETWORK_NAME = self.getUniqueString() snt = self.user_cloud.shared_file_system.create_share_network( - name=self.SHARE_NETWORK_NAME) + name=self.SHARE_NETWORK_NAME + ) self.assertIsNotNone(snt) self.assertIsNotNone(snt.id) self.SHARE_NETWORK_ID = snt.id def tearDown(self): sot = self.user_cloud.shared_file_system.delete_share_network( - self.SHARE_NETWORK_ID, - ignore_missing=True) + self.SHARE_NETWORK_ID, ignore_missing=True + ) self.assertIsNone(sot) super(ShareNetworkTest, self).tearDown() def test_get(self): sot = self.user_cloud.shared_file_system.get_share_network( - self.SHARE_NETWORK_ID) + self.SHARE_NETWORK_ID + ) assert isinstance(sot, _share_network.ShareNetwork) self.assertEqual(self.SHARE_NETWORK_ID, sot.id) @@ -50,12 +51,13 @@ def test_list_share_network(self): def test_delete_share_network(self): sot = self.user_cloud.shared_file_system.delete_share_network( - self.SHARE_NETWORK_ID) + self.SHARE_NETWORK_ID + ) self.assertIsNone(sot) def test_update(self): unt = self.user_cloud.shared_file_system.update_share_network( - self.SHARE_NETWORK_ID, description='updated share network') - get_unt = self.user_cloud.shared_file_system.get_share_network( - unt.id) + self.SHARE_NETWORK_ID, description='updated share network' + ) + get_unt = self.user_cloud.shared_file_system.get_share_network(unt.id) self.assertEqual('updated share network', get_unt.description) diff --git a/openstack/tests/functional/shared_file_system/test_share_network_subnet.py b/openstack/tests/functional/shared_file_system/test_share_network_subnet.py index 8f9c5b839..bfd096f19 100644 --- a/openstack/tests/functional/shared_file_system/test_share_network_subnet.py +++ b/openstack/tests/functional/shared_file_system/test_share_network_subnet.py @@ -11,65 +11,76 @@ # under the License. from openstack.shared_file_system.v2 import ( - share_network_subnet as _share_network_subnet + share_network_subnet as _share_network_subnet, ) from openstack.tests.functional.shared_file_system import base class ShareNetworkSubnetTest(base.BaseSharedFileSystemTest): - def setUp(self): super().setUp() - zones = self.user_cloud.shared_file_system.\ - availability_zones() + zones = self.user_cloud.shared_file_system.availability_zones() first_zone = next(zones) self.SHARE_NETWORK_NAME = self.getUniqueString() snt = self.user_cloud.shared_file_system.create_share_network( - name=self.SHARE_NETWORK_NAME) + name=self.SHARE_NETWORK_NAME + ) self.assertIsNotNone(snt) self.assertIsNotNone(snt.id) self.SHARE_NETWORK_ID = snt.id - snsb = self.user_cloud.shared_file_system.\ - create_share_network_subnet(self.SHARE_NETWORK_ID, - availability_zone=first_zone.name) + snsb = self.user_cloud.shared_file_system.create_share_network_subnet( + self.SHARE_NETWORK_ID, availability_zone=first_zone.name + ) self.assertIsNotNone(snsb) self.assertIsNotNone(snsb.id) self.SHARE_NETWORK_SUBNET_ID = snsb.id def tearDown(self): - subnet = self.user_cloud.shared_file_system.\ - get_share_network_subnet(self.SHARE_NETWORK_ID, - self.SHARE_NETWORK_SUBNET_ID) - fdel = self.user_cloud.shared_file_system.\ - delete_share_network_subnet(self.SHARE_NETWORK_ID, - self.SHARE_NETWORK_SUBNET_ID, - ignore_missing=True) + subnet = self.user_cloud.shared_file_system.get_share_network_subnet( + self.SHARE_NETWORK_ID, self.SHARE_NETWORK_SUBNET_ID + ) + fdel = self.user_cloud.shared_file_system.delete_share_network_subnet( + self.SHARE_NETWORK_ID, + self.SHARE_NETWORK_SUBNET_ID, + ignore_missing=True, + ) self.assertIsNone(fdel) - self.user_cloud.shared_file_system.\ - wait_for_delete(subnet) - sot = self.user_cloud.shared_file_system.\ - delete_share_network(self.SHARE_NETWORK_ID, - ignore_missing=True) + self.user_cloud.shared_file_system.wait_for_delete(subnet) + sot = self.user_cloud.shared_file_system.delete_share_network( + self.SHARE_NETWORK_ID, ignore_missing=True + ) self.assertIsNone(sot) super().tearDown() def test_get(self): - sub = self.user_cloud.shared_file_system.\ - get_share_network_subnet(self.SHARE_NETWORK_ID, - self.SHARE_NETWORK_SUBNET_ID) + sub = self.user_cloud.shared_file_system.get_share_network_subnet( + self.SHARE_NETWORK_ID, self.SHARE_NETWORK_SUBNET_ID + ) assert isinstance(sub, _share_network_subnet.ShareNetworkSubnet) def test_list(self): subs = self.user_cloud.shared_file_system.share_network_subnets( - self.SHARE_NETWORK_ID) + self.SHARE_NETWORK_ID + ) self.assertGreater(len(list(subs)), 0) for sub in subs: - for attribute in ('id', 'name', 'created_at', 'updated_at', - 'share_network_id', 'availability_zone', - 'cidr', 'gateway', 'ip_version', 'mtu', - 'network_type', 'neutron_net_id', - 'neutron_subnet_id', 'segmentation_id', - 'share_network_name'): + for attribute in ( + 'id', + 'name', + 'created_at', + 'updated_at', + 'share_network_id', + 'availability_zone', + 'cidr', + 'gateway', + 'ip_version', + 'mtu', + 'network_type', + 'neutron_net_id', + 'neutron_subnet_id', + 'segmentation_id', + 'share_network_name', + ): self.assertTrue(hasattr(sub, attribute)) diff --git a/openstack/tests/functional/shared_file_system/test_share_snapshot.py b/openstack/tests/functional/shared_file_system/test_share_snapshot.py index 43bf4229a..8134ea9ac 100644 --- a/openstack/tests/functional/shared_file_system/test_share_snapshot.py +++ b/openstack/tests/functional/shared_file_system/test_share_snapshot.py @@ -14,48 +14,56 @@ class ShareSnapshotTest(base.BaseSharedFileSystemTest): - def setUp(self): super(ShareSnapshotTest, self).setUp() self.SHARE_NAME = self.getUniqueString() self.SNAPSHOT_NAME = self.getUniqueString() my_share = self.operator_cloud.shared_file_system.create_share( - name=self.SHARE_NAME, size=2, share_type="dhss_false", - share_protocol='NFS', description=None) + name=self.SHARE_NAME, + size=2, + share_type="dhss_false", + share_protocol='NFS', + description=None, + ) self.operator_cloud.shared_file_system.wait_for_status( my_share, status='available', failures=['error'], interval=5, - wait=self._wait_for_timeout) + wait=self._wait_for_timeout, + ) self.assertIsNotNone(my_share) self.assertIsNotNone(my_share.id) self.SHARE_ID = my_share.id msp = self.operator_cloud.shared_file_system.create_share_snapshot( - share_id=self.SHARE_ID, name=self.SNAPSHOT_NAME, force=True) + share_id=self.SHARE_ID, name=self.SNAPSHOT_NAME, force=True + ) self.operator_cloud.shared_file_system.wait_for_status( msp, status='available', failures=['error'], interval=5, - wait=self._wait_for_timeout) + wait=self._wait_for_timeout, + ) self.assertIsNotNone(msp.id) self.SNAPSHOT_ID = msp.id def tearDown(self): snpt = self.operator_cloud.shared_file_system.get_share_snapshot( - self.SNAPSHOT_ID) + self.SNAPSHOT_ID + ) sot = self.operator_cloud.shared_file_system.delete_share_snapshot( snpt, ignore_missing=False ) self.operator_cloud.shared_file_system.wait_for_delete( - snpt, interval=2, wait=self._wait_for_timeout) + snpt, interval=2, wait=self._wait_for_timeout + ) self.assertIsNone(sot) sot = self.operator_cloud.shared_file_system.delete_share( - self.SHARE_ID, - ignore_missing=False) + self.SHARE_ID, ignore_missing=False + ) self.assertIsNone(sot) super(ShareSnapshotTest, self).tearDown() @@ -71,14 +79,26 @@ def test_list(self): ) self.assertGreater(len(list(snaps)), 0) for snap in snaps: - for attribute in ('id', 'name', 'created_at', 'updated_at', - 'description', 'share_id', 'share_proto', - 'share_size', 'size', 'status', 'user_id'): + for attribute in ( + 'id', + 'name', + 'created_at', + 'updated_at', + 'description', + 'share_id', + 'share_proto', + 'share_size', + 'size', + 'status', + 'user_id', + ): self.assertTrue(hasattr(snap, attribute)) def test_update(self): u_snap = self.operator_cloud.shared_file_system.update_share_snapshot( - self.SNAPSHOT_ID, display_description='updated share snapshot') + self.SNAPSHOT_ID, display_description='updated share snapshot' + ) get_u_snap = self.operator_cloud.shared_file_system.get_share_snapshot( - u_snap.id) + u_snap.id + ) self.assertEqual('updated share snapshot', get_u_snap.description) diff --git a/openstack/tests/functional/shared_file_system/test_share_snapshot_instance.py b/openstack/tests/functional/shared_file_system/test_share_snapshot_instance.py index 0a7fc1ad6..8eae2ee1b 100644 --- a/openstack/tests/functional/shared_file_system/test_share_snapshot_instance.py +++ b/openstack/tests/functional/shared_file_system/test_share_snapshot_instance.py @@ -14,22 +14,24 @@ class ShareSnapshotInstanceTest(base.BaseSharedFileSystemTest): - def setUp(self): super(ShareSnapshotInstanceTest, self).setUp() self.SHARE_NAME = self.getUniqueString() my_share = self.create_share( - name=self.SHARE_NAME, size=2, share_type="dhss_false", - share_protocol='NFS', description=None) - self.SHARE_ID = my_share.id - self.create_share_snapshot( - share_id=self.SHARE_ID + name=self.SHARE_NAME, + size=2, + share_type="dhss_false", + share_protocol='NFS', + description=None, ) + self.SHARE_ID = my_share.id + self.create_share_snapshot(share_id=self.SHARE_ID) def test_share_snapshot_instances(self): - sots = \ + sots = ( self.operator_cloud.shared_file_system.share_snapshot_instances() + ) self.assertGreater(len(list(sots)), 0) for sot in sots: for attribute in ('id', 'name', 'created_at', 'updated_at'): diff --git a/openstack/tests/functional/shared_file_system/test_storage_pool.py b/openstack/tests/functional/shared_file_system/test_storage_pool.py index fea60470a..f1be9c177 100644 --- a/openstack/tests/functional/shared_file_system/test_storage_pool.py +++ b/openstack/tests/functional/shared_file_system/test_storage_pool.py @@ -14,11 +14,15 @@ class StoragePoolTest(base.BaseSharedFileSystemTest): - def test_storage_pools(self): pools = self.operator_cloud.shared_file_system.storage_pools() self.assertGreater(len(list(pools)), 0) for pool in pools: - for attribute in ('pool', 'name', 'host', 'backend', - 'capabilities'): + for attribute in ( + 'pool', + 'name', + 'host', + 'backend', + 'capabilities', + ): self.assertTrue(hasattr(pool, attribute)) diff --git a/openstack/tests/functional/shared_file_system/test_user_message.py b/openstack/tests/functional/shared_file_system/test_user_message.py index 7af845496..b03e55c39 100644 --- a/openstack/tests/functional/shared_file_system/test_user_message.py +++ b/openstack/tests/functional/shared_file_system/test_user_message.py @@ -14,7 +14,6 @@ class UserMessageTest(base.BaseSharedFileSystemTest): - def test_user_messages(self): # TODO(kafilat): We must intentionally cause an asynchronous failure to # ensure that at least one user message exists; @@ -22,11 +21,19 @@ def test_user_messages(self): # self.assertGreater(len(list(u_messages)), 0) for u_message in u_messages: for attribute in ( - 'id', 'created_at', 'action_id', 'detail_id', - 'expires_at', 'message_level', 'project_id', 'request_id', - 'resource_id', 'resource_type', 'user_message'): + 'id', + 'created_at', + 'action_id', + 'detail_id', + 'expires_at', + 'message_level', + 'project_id', + 'request_id', + 'resource_id', + 'resource_type', + 'user_message', + ): self.assertTrue(hasattr(u_message, attribute)) self.assertIsInstance(getattr(u_message, attribute), str) - self.conn.shared_file_system.delete_user_message( - u_message) + self.conn.shared_file_system.delete_user_message(u_message) diff --git a/openstack/tests/unit/shared_file_system/v2/test_availability_zone.py b/openstack/tests/unit/shared_file_system/v2/test_availability_zone.py index d37a7328f..377a28121 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_availability_zone.py +++ b/openstack/tests/unit/shared_file_system/v2/test_availability_zone.py @@ -23,7 +23,6 @@ class TestAvailabilityZone(base.TestCase): - def test_basic(self): az_resource = az.AvailabilityZone() self.assertEqual('availability_zones', az_resource.resources_key) diff --git a/openstack/tests/unit/shared_file_system/v2/test_limit.py b/openstack/tests/unit/shared_file_system/v2/test_limit.py index e0d2c0b75..864e4a914 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_limit.py +++ b/openstack/tests/unit/shared_file_system/v2/test_limit.py @@ -27,12 +27,11 @@ "maxTotalShareReplicas": 100, "maxTotalReplicaGigabytes": 1000, "totalShareReplicasUsed": 0, - "totalReplicaGigabytesUsed": 0 + "totalReplicaGigabytesUsed": 0, } class TestLimit(base.TestCase): - def test_basic(self): limits = limit.Limit() self.assertEqual('limits', limits.resources_key) @@ -46,31 +45,45 @@ def test_basic(self): def test_make_limits(self): limits = limit.Limit(**EXAMPLE) - self.assertEqual(EXAMPLE['totalShareNetworksUsed'], - limits.totalShareNetworksUsed) - self.assertEqual(EXAMPLE['maxTotalShareGigabytes'], - limits.maxTotalShareGigabytes) - self.assertEqual(EXAMPLE['maxTotalShareNetworks'], - limits.maxTotalShareNetworks) - self.assertEqual(EXAMPLE['totalSharesUsed'], - limits.totalSharesUsed) - self.assertEqual(EXAMPLE['totalShareGigabytesUsed'], - limits.totalShareGigabytesUsed) - self.assertEqual(EXAMPLE['totalShareSnapshotsUsed'], - limits.totalShareSnapshotsUsed) - self.assertEqual(EXAMPLE['maxTotalShares'], - limits.maxTotalShares) - self.assertEqual(EXAMPLE['totalSnapshotGigabytesUsed'], - limits.totalSnapshotGigabytesUsed) - self.assertEqual(EXAMPLE['maxTotalSnapshotGigabytes'], - limits.maxTotalSnapshotGigabytes) - self.assertEqual(EXAMPLE['maxTotalShareSnapshots'], - limits.maxTotalShareSnapshots) - self.assertEqual(EXAMPLE['maxTotalShareReplicas'], - limits.maxTotalShareReplicas) - self.assertEqual(EXAMPLE['maxTotalReplicaGigabytes'], - limits.maxTotalReplicaGigabytes) - self.assertEqual(EXAMPLE['totalShareReplicasUsed'], - limits.totalShareReplicasUsed) - self.assertEqual(EXAMPLE['totalReplicaGigabytesUsed'], - limits.totalReplicaGigabytesUsed) + self.assertEqual( + EXAMPLE['totalShareNetworksUsed'], limits.totalShareNetworksUsed + ) + self.assertEqual( + EXAMPLE['maxTotalShareGigabytes'], limits.maxTotalShareGigabytes + ) + self.assertEqual( + EXAMPLE['maxTotalShareNetworks'], limits.maxTotalShareNetworks + ) + self.assertEqual(EXAMPLE['totalSharesUsed'], limits.totalSharesUsed) + self.assertEqual( + EXAMPLE['totalShareGigabytesUsed'], limits.totalShareGigabytesUsed + ) + self.assertEqual( + EXAMPLE['totalShareSnapshotsUsed'], limits.totalShareSnapshotsUsed + ) + self.assertEqual(EXAMPLE['maxTotalShares'], limits.maxTotalShares) + self.assertEqual( + EXAMPLE['totalSnapshotGigabytesUsed'], + limits.totalSnapshotGigabytesUsed, + ) + self.assertEqual( + EXAMPLE['maxTotalSnapshotGigabytes'], + limits.maxTotalSnapshotGigabytes, + ) + self.assertEqual( + EXAMPLE['maxTotalShareSnapshots'], limits.maxTotalShareSnapshots + ) + self.assertEqual( + EXAMPLE['maxTotalShareReplicas'], limits.maxTotalShareReplicas + ) + self.assertEqual( + EXAMPLE['maxTotalReplicaGigabytes'], + limits.maxTotalReplicaGigabytes, + ) + self.assertEqual( + EXAMPLE['totalShareReplicasUsed'], limits.totalShareReplicasUsed + ) + self.assertEqual( + EXAMPLE['totalReplicaGigabytesUsed'], + limits.totalReplicaGigabytesUsed, + ) diff --git a/openstack/tests/unit/shared_file_system/v2/test_proxy.py b/openstack/tests/unit/shared_file_system/v2/test_proxy.py index cb3a1bd0f..b3516073e 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_proxy.py +++ b/openstack/tests/unit/shared_file_system/v2/test_proxy.py @@ -12,12 +12,10 @@ from unittest import mock -from openstack.shared_file_system.v2 import ( - share_access_rule -) from openstack.shared_file_system.v2 import _proxy from openstack.shared_file_system.v2 import limit from openstack.shared_file_system.v2 import share +from openstack.shared_file_system.v2 import share_access_rule from openstack.shared_file_system.v2 import share_instance from openstack.shared_file_system.v2 import share_network from openstack.shared_file_system.v2 import share_network_subnet @@ -29,7 +27,6 @@ class TestSharedFileSystemProxy(test_proxy_base.TestProxyBase): - def setUp(self): super(TestSharedFileSystemProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) @@ -40,25 +37,29 @@ def test_shares(self): self.verify_list(self.proxy.shares, share.Share) def test_shares_detailed(self): - self.verify_list(self.proxy.shares, share.Share, - method_kwargs={"details": True, "query": 1}, - expected_kwargs={"query": 1}) + self.verify_list( + self.proxy.shares, + share.Share, + method_kwargs={"details": True, "query": 1}, + expected_kwargs={"query": 1}, + ) def test_shares_not_detailed(self): - self.verify_list(self.proxy.shares, share.Share, - method_kwargs={"details": False, "query": 1}, - expected_kwargs={"query": 1}) + self.verify_list( + self.proxy.shares, + share.Share, + method_kwargs={"details": False, "query": 1}, + expected_kwargs={"query": 1}, + ) def test_share_get(self): self.verify_get(self.proxy.get_share, share.Share) def test_share_delete(self): - self.verify_delete( - self.proxy.delete_share, share.Share, False) + self.verify_delete(self.proxy.delete_share, share.Share, False) def test_share_delete_ignore(self): - self.verify_delete( - self.proxy.delete_share, share.Share, True) + self.verify_delete(self.proxy.delete_share, share.Share, True) def test_share_create(self): self.verify_create(self.proxy.create_share, share.Share) @@ -71,8 +72,7 @@ def test_share_resize_extend(self): self.proxy._get = mock.Mock(return_value=mock_share) self._verify( - "openstack.shared_file_system.v2.share." - + "Share.extend_share", + "openstack.shared_file_system.v2.share." + "Share.extend_share", self.proxy.resize_share, method_args=['fakeId', 20], expected_args=[self.proxy, 20, False], @@ -83,20 +83,21 @@ def test_share_resize_shrink(self): self.proxy._get = mock.Mock(return_value=mock_share) self._verify( - "openstack.shared_file_system.v2.share." - + "Share.shrink_share", + "openstack.shared_file_system.v2.share." + "Share.shrink_share", self.proxy.resize_share, method_args=['fakeId', 20], expected_args=[self.proxy, 20], ) def test_share_instances(self): - self.verify_list(self.proxy.share_instances, - share_instance.ShareInstance) + self.verify_list( + self.proxy.share_instances, share_instance.ShareInstance + ) def test_share_instance_get(self): - self.verify_get(self.proxy.get_share_instance, - share_instance.ShareInstance) + self.verify_get( + self.proxy.get_share_instance, share_instance.ShareInstance + ) def test_share_instance_reset(self): self._verify( @@ -113,7 +114,8 @@ def test_share_instance_delete(self): + "ShareInstance.force_delete", self.proxy.delete_share_instance, method_args=['id'], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) @mock.patch("openstack.resource.wait_for_status") def test_wait_for(self, mock_wait): @@ -122,30 +124,33 @@ def test_wait_for(self, mock_wait): self.proxy.wait_for_status(mock_resource, 'ACTIVE') - mock_wait.assert_called_once_with(self.proxy, mock_resource, - 'ACTIVE', [], 2, 120) + mock_wait.assert_called_once_with( + self.proxy, mock_resource, 'ACTIVE', [], 2, 120 + ) class TestSharedFileSystemStoragePool(TestSharedFileSystemProxy): def test_storage_pools(self): - self.verify_list( - self.proxy.storage_pools, storage_pool.StoragePool) + self.verify_list(self.proxy.storage_pools, storage_pool.StoragePool) def test_storage_pool_detailed(self): self.verify_list( - self.proxy.storage_pools, storage_pool.StoragePool, + self.proxy.storage_pools, + storage_pool.StoragePool, method_kwargs={"details": True, "backend": "alpha"}, - expected_kwargs={"backend": "alpha"}) + expected_kwargs={"backend": "alpha"}, + ) def test_storage_pool_not_detailed(self): self.verify_list( - self.proxy.storage_pools, storage_pool.StoragePool, + self.proxy.storage_pools, + storage_pool.StoragePool, method_kwargs={"details": False, "backend": "alpha"}, - expected_kwargs={"backend": "alpha"}) + expected_kwargs={"backend": "alpha"}, + ) class TestUserMessageProxy(test_proxy_base.TestProxyBase): - def setUp(self): super(TestUserMessageProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) @@ -155,70 +160,83 @@ def test_user_messages(self): def test_user_messages_queried(self): self.verify_list( - self.proxy.user_messages, user_message.UserMessage, + self.proxy.user_messages, + user_message.UserMessage, method_kwargs={"action_id": "1"}, - expected_kwargs={"action_id": "1"}) + expected_kwargs={"action_id": "1"}, + ) def test_user_message_get(self): self.verify_get(self.proxy.get_user_message, user_message.UserMessage) def test_delete_user_message(self): self.verify_delete( - self.proxy.delete_user_message, user_message.UserMessage, False) + self.proxy.delete_user_message, user_message.UserMessage, False + ) def test_delete_user_message_true(self): self.verify_delete( - self.proxy.delete_user_message, user_message.UserMessage, True) + self.proxy.delete_user_message, user_message.UserMessage, True + ) def test_limit(self): self.verify_list(self.proxy.limits, limit.Limit) class TestShareSnapshotResource(test_proxy_base.TestProxyBase): - def setUp(self): super(TestShareSnapshotResource, self).setUp() self.proxy = _proxy.Proxy(self.session) def test_share_snapshots(self): self.verify_list( - self.proxy.share_snapshots, share_snapshot.ShareSnapshot) + self.proxy.share_snapshots, share_snapshot.ShareSnapshot + ) def test_share_snapshots_detailed(self): self.verify_list( self.proxy.share_snapshots, share_snapshot.ShareSnapshot, method_kwargs={"details": True, "name": "my_snapshot"}, - expected_kwargs={"name": "my_snapshot"}) + expected_kwargs={"name": "my_snapshot"}, + ) def test_share_snapshots_not_detailed(self): self.verify_list( self.proxy.share_snapshots, share_snapshot.ShareSnapshot, method_kwargs={"details": False, "name": "my_snapshot"}, - expected_kwargs={"name": "my_snapshot"}) + expected_kwargs={"name": "my_snapshot"}, + ) def test_share_snapshot_get(self): self.verify_get( - self.proxy.get_share_snapshot, share_snapshot.ShareSnapshot) + self.proxy.get_share_snapshot, share_snapshot.ShareSnapshot + ) def test_share_snapshot_delete(self): self.verify_delete( self.proxy.delete_share_snapshot, - share_snapshot.ShareSnapshot, False) + share_snapshot.ShareSnapshot, + False, + ) def test_share_snapshot_delete_ignore(self): self.verify_delete( self.proxy.delete_share_snapshot, - share_snapshot.ShareSnapshot, True) + share_snapshot.ShareSnapshot, + True, + ) def test_share_snapshot_create(self): self.verify_create( - self.proxy.create_share_snapshot, share_snapshot.ShareSnapshot) + self.proxy.create_share_snapshot, share_snapshot.ShareSnapshot + ) def test_share_snapshot_update(self): self.verify_update( - self.proxy.update_share_snapshot, share_snapshot.ShareSnapshot) + self.proxy.update_share_snapshot, share_snapshot.ShareSnapshot + ) @mock.patch("openstack.resource.wait_for_delete") def test_wait_for_delete(self, mock_wait): @@ -231,7 +249,6 @@ def test_wait_for_delete(self, mock_wait): class TestShareSnapshotInstanceResource(test_proxy_base.TestProxyBase): - def setUp(self): super(TestShareSnapshotInstanceResource, self).setUp() self.proxy = _proxy.Proxy(self.session) @@ -239,34 +256,33 @@ def setUp(self): def test_share_snapshot_instances(self): self.verify_list( self.proxy.share_snapshot_instances, - share_snapshot_instance.ShareSnapshotInstance) + share_snapshot_instance.ShareSnapshotInstance, + ) def test_share_snapshot_instance_detailed(self): - self.verify_list(self.proxy.share_snapshot_instances, - share_snapshot_instance.ShareSnapshotInstance, - method_kwargs={ - "details": True, - "query": {'snapshot_id': 'fake'} - }, - expected_kwargs={"query": {'snapshot_id': 'fake'}}) + self.verify_list( + self.proxy.share_snapshot_instances, + share_snapshot_instance.ShareSnapshotInstance, + method_kwargs={"details": True, "query": {'snapshot_id': 'fake'}}, + expected_kwargs={"query": {'snapshot_id': 'fake'}}, + ) def test_share_snapshot_instance_not_detailed(self): - self.verify_list(self.proxy.share_snapshot_instances, - share_snapshot_instance.ShareSnapshotInstance, - method_kwargs={ - "details": False, - "query": {'snapshot_id': 'fake'} - }, - expected_kwargs={"query": {'snapshot_id': 'fake'}}) + self.verify_list( + self.proxy.share_snapshot_instances, + share_snapshot_instance.ShareSnapshotInstance, + method_kwargs={"details": False, "query": {'snapshot_id': 'fake'}}, + expected_kwargs={"query": {'snapshot_id': 'fake'}}, + ) def test_share_snapshot_instance_get(self): self.verify_get( self.proxy.get_share_snapshot_instance, - share_snapshot_instance.ShareSnapshotInstance) + share_snapshot_instance.ShareSnapshotInstance, + ) class TestShareNetworkResource(test_proxy_base.TestProxyBase): - def setUp(self): super(TestShareNetworkResource, self).setUp() self.proxy = _proxy.Proxy(self.session) @@ -275,38 +291,48 @@ def test_share_networks(self): self.verify_list(self.proxy.share_networks, share_network.ShareNetwork) def test_share_networks_detailed(self): - self.verify_list(self.proxy.share_networks, share_network.ShareNetwork, - method_kwargs={"details": True, "name": "my_net"}, - expected_kwargs={"name": "my_net"}) + self.verify_list( + self.proxy.share_networks, + share_network.ShareNetwork, + method_kwargs={"details": True, "name": "my_net"}, + expected_kwargs={"name": "my_net"}, + ) def test_share_networks_not_detailed(self): - self.verify_list(self.proxy.share_networks, share_network.ShareNetwork, - method_kwargs={"details": False, "name": "my_net"}, - expected_kwargs={"name": "my_net"}) + self.verify_list( + self.proxy.share_networks, + share_network.ShareNetwork, + method_kwargs={"details": False, "name": "my_net"}, + expected_kwargs={"name": "my_net"}, + ) def test_share_network_get(self): self.verify_get( - self.proxy.get_share_network, share_network.ShareNetwork) + self.proxy.get_share_network, share_network.ShareNetwork + ) def test_share_network_delete(self): self.verify_delete( - self.proxy.delete_share_network, share_network.ShareNetwork, False) + self.proxy.delete_share_network, share_network.ShareNetwork, False + ) def test_share_network_delete_ignore(self): self.verify_delete( - self.proxy.delete_share_network, share_network.ShareNetwork, True) + self.proxy.delete_share_network, share_network.ShareNetwork, True + ) def test_share_network_create(self): self.verify_create( - self.proxy.create_share_network, share_network.ShareNetwork) + self.proxy.create_share_network, share_network.ShareNetwork + ) def test_share_network_update(self): self.verify_update( - self.proxy.update_share_network, share_network.ShareNetwork) + self.proxy.update_share_network, share_network.ShareNetwork + ) class TestShareNetworkSubnetResource(test_proxy_base.TestProxyBase): - def setUp(self): super(TestShareNetworkSubnetResource, self).setUp() self.proxy = _proxy.Proxy(self.session) @@ -317,7 +343,8 @@ def test_share_network_subnets(self): share_network_subnet.ShareNetworkSubnet, method_args=["test_share"], expected_args=[], - expected_kwargs={"share_network_id": "test_share"}) + expected_kwargs={"share_network_id": "test_share"}, + ) def test_share_network_subnet_get(self): self.verify_get( @@ -325,7 +352,8 @@ def test_share_network_subnet_get(self): share_network_subnet.ShareNetworkSubnet, method_args=["fake_network_id", "fake_sub_network_id"], expected_args=['fake_sub_network_id'], - expected_kwargs={'share_network_id': 'fake_network_id'}) + expected_kwargs={'share_network_id': 'fake_network_id'}, + ) def test_share_network_subnet_create(self): self.verify_create( @@ -336,7 +364,9 @@ def test_share_network_subnet_create(self): expected_args=[], expected_kwargs={ "share_network_id": "fake_network_id", - "p1": "v1"}) + "p1": "v1", + }, + ) def test_share_network_subnet_delete(self): self.verify_delete( @@ -345,11 +375,11 @@ def test_share_network_subnet_delete(self): False, method_args=["fake_network_id", "fake_sub_network_id"], expected_args=["fake_sub_network_id"], - expected_kwargs={'share_network_id': 'fake_network_id'}) + expected_kwargs={'share_network_id': 'fake_network_id'}, + ) class TestAccessRuleProxy(test_proxy_base.TestProxyBase): - def setUp(self): super(TestAccessRuleProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) @@ -360,18 +390,21 @@ def test_access_ruless(self): share_access_rule.ShareAccessRule, method_args=["test_share"], expected_args=[], - expected_kwargs={"share_id": "test_share"}) + expected_kwargs={"share_id": "test_share"}, + ) def test_access_rules_get(self): self.verify_get( - self.proxy.get_access_rule, share_access_rule.ShareAccessRule) + self.proxy.get_access_rule, share_access_rule.ShareAccessRule + ) def test_access_rules_create(self): self.verify_create( self.proxy.create_access_rule, share_access_rule.ShareAccessRule, method_args=["share_id"], - expected_args=[]) + expected_args=[], + ) def test_access_rules_delete(self): self._verify( @@ -379,4 +412,5 @@ def test_access_rules_delete(self): + "ShareAccessRule.delete", self.proxy.delete_access_rule, method_args=['access_id', 'share_id', 'ignore_missing'], - expected_args=[self.proxy , 'share_id']) + expected_args=[self.proxy, 'share_id'], + ) diff --git a/openstack/tests/unit/shared_file_system/v2/test_share.py b/openstack/tests/unit/shared_file_system/v2/test_share.py index e19d5b51e..3ddea2061 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_share.py +++ b/openstack/tests/unit/shared_file_system/v2/test_share.py @@ -49,12 +49,11 @@ "is_mounting_snapshot_supported": True, "progress": "100%", "share_server_id": None, - "host": "new@denver#lvm-single-pool" + "host": "new@denver#lvm-single-pool", } class TestShares(base.TestCase): - def test_basic(self): shares_resource = share.Share() self.assertEqual('shares', shares_resource.resources_key) @@ -69,50 +68,65 @@ def test_make_shares(self): shares_resource = share.Share(**EXAMPLE) self.assertEqual(EXAMPLE['id'], shares_resource.id) self.assertEqual(EXAMPLE['size'], shares_resource.size) - self.assertEqual(EXAMPLE['availability_zone'], - shares_resource.availability_zone) + self.assertEqual( + EXAMPLE['availability_zone'], shares_resource.availability_zone + ) self.assertEqual(EXAMPLE['created_at'], shares_resource.created_at) self.assertEqual(EXAMPLE['status'], shares_resource.status) self.assertEqual(EXAMPLE['name'], shares_resource.name) - self.assertEqual(EXAMPLE['description'], - shares_resource.description) + self.assertEqual(EXAMPLE['description'], shares_resource.description) self.assertEqual(EXAMPLE['project_id'], shares_resource.project_id) self.assertEqual(EXAMPLE['snapshot_id'], shares_resource.snapshot_id) - self.assertEqual(EXAMPLE['share_network_id'], - shares_resource.share_network_id) - self.assertEqual(EXAMPLE['share_protocol'], - shares_resource.share_protocol) + self.assertEqual( + EXAMPLE['share_network_id'], shares_resource.share_network_id + ) + self.assertEqual( + EXAMPLE['share_protocol'], shares_resource.share_protocol + ) self.assertEqual(EXAMPLE['metadata'], shares_resource.metadata) self.assertEqual(EXAMPLE['share_type'], shares_resource.share_type) self.assertEqual(EXAMPLE['is_public'], shares_resource.is_public) - self.assertEqual(EXAMPLE['is_snapshot_supported'], - shares_resource.is_snapshot_supported) + self.assertEqual( + EXAMPLE['is_snapshot_supported'], + shares_resource.is_snapshot_supported, + ) self.assertEqual(EXAMPLE['task_state'], shares_resource.task_state) - self.assertEqual(EXAMPLE['share_type_name'], - shares_resource.share_type_name) - self.assertEqual(EXAMPLE['access_rules_status'], - shares_resource.access_rules_status) - self.assertEqual(EXAMPLE['replication_type'], - shares_resource.replication_type) - self.assertEqual(EXAMPLE['is_replicated'], - shares_resource.is_replicated) + self.assertEqual( + EXAMPLE['share_type_name'], shares_resource.share_type_name + ) + self.assertEqual( + EXAMPLE['access_rules_status'], shares_resource.access_rules_status + ) + self.assertEqual( + EXAMPLE['replication_type'], shares_resource.replication_type + ) + self.assertEqual( + EXAMPLE['is_replicated'], shares_resource.is_replicated + ) self.assertEqual(EXAMPLE['user_id'], shares_resource.user_id) - self.assertEqual(EXAMPLE[ - 'is_creating_new_share_from_snapshot_supported'], - (shares_resource.is_creating_new_share_from_snapshot_supported)) - self.assertEqual(EXAMPLE['is_reverting_to_snapshot_supported'], - shares_resource.is_reverting_to_snapshot_supported) - self.assertEqual(EXAMPLE['share_group_id'], - shares_resource.share_group_id) - self.assertEqual(EXAMPLE[ - 'source_share_group_snapshot_member_id'], - shares_resource.source_share_group_snapshot_member_id) - self.assertEqual(EXAMPLE['is_mounting_snapshot_supported'], - shares_resource.is_mounting_snapshot_supported) - self.assertEqual(EXAMPLE['progress'], - shares_resource.progress) - self.assertEqual(EXAMPLE['share_server_id'], - shares_resource.share_server_id) + self.assertEqual( + EXAMPLE['is_creating_new_share_from_snapshot_supported'], + (shares_resource.is_creating_new_share_from_snapshot_supported), + ) + self.assertEqual( + EXAMPLE['is_reverting_to_snapshot_supported'], + shares_resource.is_reverting_to_snapshot_supported, + ) + self.assertEqual( + EXAMPLE['share_group_id'], shares_resource.share_group_id + ) + self.assertEqual( + EXAMPLE['source_share_group_snapshot_member_id'], + shares_resource.source_share_group_snapshot_member_id, + ) + self.assertEqual( + EXAMPLE['is_mounting_snapshot_supported'], + shares_resource.is_mounting_snapshot_supported, + ) + self.assertEqual(EXAMPLE['progress'], shares_resource.progress) + self.assertEqual( + EXAMPLE['share_server_id'], shares_resource.share_server_id + ) self.assertEqual(EXAMPLE['host'], shares_resource.host) @@ -139,8 +153,8 @@ def test_shrink_share(self): headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, - microversion=microversion) + url, json=body, headers=headers, microversion=microversion + ) def test_extend_share(self): sot = share.Share(**EXAMPLE) @@ -153,8 +167,8 @@ def test_extend_share(self): headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, - microversion=microversion) + url, json=body, headers=headers, microversion=microversion + ) def test_revert_to_snapshot(self): sot = share.Share(**EXAMPLE) @@ -167,5 +181,5 @@ def test_revert_to_snapshot(self): headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, - microversion=microversion) + url, json=body, headers=headers, microversion=microversion + ) diff --git a/openstack/tests/unit/shared_file_system/v2/test_share_access_rule.py b/openstack/tests/unit/shared_file_system/v2/test_share_access_rule.py index 83762db54..5699b62cc 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_share_access_rule.py +++ b/openstack/tests/unit/shared_file_system/v2/test_share_access_rule.py @@ -23,26 +23,21 @@ "access_key": None, "created_at": "2021-09-12T02:01:04.000000", "updated_at": "2021-09-12T02:01:04.000000", - "metadata": { - "key1": "value1", - "key2": "value2"} + "metadata": {"key1": "value1", "key2": "value2"}, } class TestShareAccessRule(base.TestCase): - def test_basic(self): rules_resource = share_access_rule.ShareAccessRule() self.assertEqual('access_list', rules_resource.resources_key) self.assertEqual('/share-access-rules', rules_resource.base_path) self.assertTrue(rules_resource.allow_list) - self.assertDictEqual({ - "limit": "limit", - "marker": "marker", - "share_id": "share_id" - }, - rules_resource._query_mapping._mapping) + self.assertDictEqual( + {"limit": "limit", "marker": "marker", "share_id": "share_id"}, + rules_resource._query_mapping._mapping, + ) def test_make_share_access_rules(self): rules_resource = share_access_rule.ShareAccessRule(**EXAMPLE) diff --git a/openstack/tests/unit/shared_file_system/v2/test_share_export_locations.py b/openstack/tests/unit/shared_file_system/v2/test_share_export_locations.py index 0e9940708..be467566e 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_share_export_locations.py +++ b/openstack/tests/unit/shared_file_system/v2/test_share_export_locations.py @@ -17,21 +17,23 @@ IDENTIFIER = '08a87d37-5ca2-4308-86c5-cba06d8d796c' EXAMPLE = { "id": "f87589cb-f4bc-4a9b-b481-ab701206eb85", - "path": ("199.19.213.225:/opt/stack/data/manila/mnt/" - "share-6ba490c5-5225-4c3b-9982-14b8f475c6d9"), + "path": ( + "199.19.213.225:/opt/stack/data/manila/mnt/" + "share-6ba490c5-5225-4c3b-9982-14b8f475c6d9" + ), "preferred": False, "share_instance_id": "6ba490c5-5225-4c3b-9982-14b8f475c6d9", - "is_admin_only": False + "is_admin_only": False, } class TestShareExportLocations(base.TestCase): - def test_basic(self): export = el.ShareExportLocation() self.assertEqual('export_locations', export.resources_key) self.assertEqual( - '/shares/%(share_id)s/export_locations', export.base_path) + '/shares/%(share_id)s/export_locations', export.base_path + ) self.assertTrue(export.allow_list) def test_share_export_locations(self): @@ -40,5 +42,6 @@ def test_share_export_locations(self): self.assertEqual(EXAMPLE['path'], export.path) self.assertEqual(EXAMPLE['preferred'], export.is_preferred) self.assertEqual( - EXAMPLE['share_instance_id'], export.share_instance_id) + EXAMPLE['share_instance_id'], export.share_instance_id + ) self.assertEqual(EXAMPLE['is_admin_only'], export.is_admin) diff --git a/openstack/tests/unit/shared_file_system/v2/test_share_instance.py b/openstack/tests/unit/shared_file_system/v2/test_share_instance.py index 338dc09fa..6dbc0a921 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_share_instance.py +++ b/openstack/tests/unit/shared_file_system/v2/test_share_instance.py @@ -31,16 +31,16 @@ "share_server_id": "ba11930a-bf1a-4aa7-bae4-a8dfbaa3cc73", "host": "manila2@generic1#GENERIC1", "access_rules_status": "active", - "id": IDENTIFIER + "id": IDENTIFIER, } class TestShareInstances(base.TestCase): - def test_basic(self): share_instance_resource = share_instance.ShareInstance() - self.assertEqual('share_instances', - share_instance_resource.resources_key) + self.assertEqual( + 'share_instances', share_instance_resource.resources_key + ) self.assertEqual('/share_instances', share_instance_resource.base_path) self.assertTrue(share_instance_resource.allow_list) self.assertFalse(share_instance_resource.allow_create) @@ -53,26 +53,36 @@ def test_make_share_instances(self): self.assertEqual(EXAMPLE['status'], share_instance_resource.status) self.assertEqual(EXAMPLE['progress'], share_instance_resource.progress) self.assertEqual(EXAMPLE['share_id'], share_instance_resource.share_id) - self.assertEqual(EXAMPLE['availability_zone'], - share_instance_resource.availability_zone) - self.assertEqual(EXAMPLE['replica_state'], - share_instance_resource.replica_state) - self.assertEqual(EXAMPLE['created_at'], - share_instance_resource.created_at) - self.assertEqual(EXAMPLE['cast_rules_to_readonly'], - share_instance_resource.cast_rules_to_readonly) - self.assertEqual(EXAMPLE['share_network_id'], - share_instance_resource.share_network_id) - self.assertEqual(EXAMPLE['share_server_id'], - share_instance_resource.share_server_id) + self.assertEqual( + EXAMPLE['availability_zone'], + share_instance_resource.availability_zone, + ) + self.assertEqual( + EXAMPLE['replica_state'], share_instance_resource.replica_state + ) + self.assertEqual( + EXAMPLE['created_at'], share_instance_resource.created_at + ) + self.assertEqual( + EXAMPLE['cast_rules_to_readonly'], + share_instance_resource.cast_rules_to_readonly, + ) + self.assertEqual( + EXAMPLE['share_network_id'], + share_instance_resource.share_network_id, + ) + self.assertEqual( + EXAMPLE['share_server_id'], share_instance_resource.share_server_id + ) self.assertEqual(EXAMPLE['host'], share_instance_resource.host) - self.assertEqual(EXAMPLE['access_rules_status'], - share_instance_resource.access_rules_status) + self.assertEqual( + EXAMPLE['access_rules_status'], + share_instance_resource.access_rules_status, + ) self.assertEqual(EXAMPLE['id'], share_instance_resource.id) class TestShareInstanceActions(TestShareInstances): - def setUp(self): super(TestShareInstanceActions, self).setUp() self.resp = mock.Mock() @@ -94,8 +104,8 @@ def test_reset_status(self): body = {"reset_status": {"status": 'active'}} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, - microversion=microversion) + url, json=body, headers=headers, microversion=microversion + ) def test_force_delete(self): sot = share_instance.ShareInstance(**EXAMPLE) @@ -107,5 +117,5 @@ def test_force_delete(self): body = {'force_delete': None} headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, - microversion=microversion) + url, json=body, headers=headers, microversion=microversion + ) diff --git a/openstack/tests/unit/shared_file_system/v2/test_share_network.py b/openstack/tests/unit/shared_file_system/v2/test_share_network.py index 505740ab4..6287aff0a 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_share_network.py +++ b/openstack/tests/unit/shared_file_system/v2/test_share_network.py @@ -21,12 +21,11 @@ "description": "My share network", "created_at": "2021-06-10T10:11:17.291981", "updated_at": None, - "share_network_subnets": [] + "share_network_subnets": [], } class TestShareNetwork(base.TestCase): - def test_basic(self): networks = share_network.ShareNetwork() self.assertEqual('share_networks', networks.resources_key) @@ -38,28 +37,28 @@ def test_basic(self): self.assertTrue(networks.allow_delete) self.assertFalse(networks.allow_head) - self.assertDictEqual({ - "limit": "limit", - "marker": "marker", - "project_id": "project_id", - "created_since": "created_since", - "created_before": "created_before", - "offset": "offset", - "security_service_id": "security_service_id", - "project_id": "project_id", - "all_projects": "all_tenants", - "name": "name", - "description": "description" - }, - networks._query_mapping._mapping) + self.assertDictEqual( + { + "limit": "limit", + "marker": "marker", + "project_id": "project_id", + "created_since": "created_since", + "created_before": "created_before", + "offset": "offset", + "security_service_id": "security_service_id", + "project_id": "project_id", + "all_projects": "all_tenants", + "name": "name", + "description": "description", + }, + networks._query_mapping._mapping, + ) def test_share_network(self): networks = share_network.ShareNetwork(**EXAMPLE) self.assertEqual(EXAMPLE['id'], networks.id) self.assertEqual(EXAMPLE['name'], networks.name) self.assertEqual(EXAMPLE['project_id'], networks.project_id) - self.assertEqual( - EXAMPLE['description'], networks.description) - self.assertEqual( - EXAMPLE['created_at'], networks.created_at) + self.assertEqual(EXAMPLE['description'], networks.description) + self.assertEqual(EXAMPLE['created_at'], networks.created_at) self.assertEqual(EXAMPLE['updated_at'], networks.updated_at) diff --git a/openstack/tests/unit/shared_file_system/v2/test_share_network_subnet.py b/openstack/tests/unit/shared_file_system/v2/test_share_network_subnet.py index 30eddaf48..702ab2a96 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_share_network_subnet.py +++ b/openstack/tests/unit/shared_file_system/v2/test_share_network_subnet.py @@ -28,36 +28,43 @@ "cidr": None, "network_type": None, "mtu": None, - "gateway": None + "gateway": None, } class TestShareNetworkSubnet(base.TestCase): - def test_basic(self): SNS_resource = SNS.ShareNetworkSubnet() self.assertEqual('share_network_subnets', SNS_resource.resources_key) - self.assertEqual('/share-networks/%(share_network_id)s/subnets', - SNS_resource.base_path) + self.assertEqual( + '/share-networks/%(share_network_id)s/subnets', + SNS_resource.base_path, + ) self.assertTrue(SNS_resource.allow_list) def test_make_share_network_subnet(self): SNS_resource = SNS.ShareNetworkSubnet(**EXAMPLE) self.assertEqual(EXAMPLE['id'], SNS_resource.id) - self.assertEqual(EXAMPLE['availability_zone'], - SNS_resource.availability_zone) - self.assertEqual(EXAMPLE['share_network_id'], - SNS_resource.share_network_id) - self.assertEqual(EXAMPLE['share_network_name'], - SNS_resource.share_network_name) + self.assertEqual( + EXAMPLE['availability_zone'], SNS_resource.availability_zone + ) + self.assertEqual( + EXAMPLE['share_network_id'], SNS_resource.share_network_id + ) + self.assertEqual( + EXAMPLE['share_network_name'], SNS_resource.share_network_name + ) self.assertEqual(EXAMPLE['created_at'], SNS_resource.created_at) - self.assertEqual(EXAMPLE['segmentation_id'], - SNS_resource.segmentation_id) - self.assertEqual(EXAMPLE['neutron_subnet_id'], - SNS_resource.neutron_subnet_id) + self.assertEqual( + EXAMPLE['segmentation_id'], SNS_resource.segmentation_id + ) + self.assertEqual( + EXAMPLE['neutron_subnet_id'], SNS_resource.neutron_subnet_id + ) self.assertEqual(EXAMPLE['updated_at'], SNS_resource.updated_at) - self.assertEqual(EXAMPLE['neutron_net_id'], - SNS_resource.neutron_net_id) + self.assertEqual( + EXAMPLE['neutron_net_id'], SNS_resource.neutron_net_id + ) self.assertEqual(EXAMPLE['ip_version'], SNS_resource.ip_version) self.assertEqual(EXAMPLE['cidr'], SNS_resource.cidr) self.assertEqual(EXAMPLE['network_type'], SNS_resource.network_type) diff --git a/openstack/tests/unit/shared_file_system/v2/test_share_snapshot.py b/openstack/tests/unit/shared_file_system/v2/test_share_snapshot.py index 744e7269e..3a6608a18 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_share_snapshot.py +++ b/openstack/tests/unit/shared_file_system/v2/test_share_snapshot.py @@ -25,41 +25,36 @@ "share_size": 1, "id": "6d221c1d-0200-461e-8d20-24b4776b9ddb", "project_id": "cadd7139bc3148b8973df097c0911016", - "size": 1 + "size": 1, } class TestShareSnapshot(base.TestCase): - def test_basic(self): snapshot_resource = share_snapshot.ShareSnapshot() - self.assertEqual( - 'snapshots', snapshot_resource.resources_key) + self.assertEqual('snapshots', snapshot_resource.resources_key) self.assertEqual('/snapshots', snapshot_resource.base_path) self.assertTrue(snapshot_resource.allow_list) - self.assertDictEqual({ - "limit": "limit", - "marker": "marker", - "snapshot_id": "snapshot_id" - }, - snapshot_resource._query_mapping._mapping) + self.assertDictEqual( + { + "limit": "limit", + "marker": "marker", + "snapshot_id": "snapshot_id", + }, + snapshot_resource._query_mapping._mapping, + ) def test_make_share_snapshot(self): snapshot_resource = share_snapshot.ShareSnapshot(**EXAMPLE) self.assertEqual(EXAMPLE['id'], snapshot_resource.id) self.assertEqual(EXAMPLE['share_id'], snapshot_resource.share_id) - self.assertEqual(EXAMPLE['user_id'], - snapshot_resource.user_id) + self.assertEqual(EXAMPLE['user_id'], snapshot_resource.user_id) self.assertEqual(EXAMPLE['created_at'], snapshot_resource.created_at) self.assertEqual(EXAMPLE['status'], snapshot_resource.status) self.assertEqual(EXAMPLE['name'], snapshot_resource.name) - self.assertEqual( - EXAMPLE['description'], snapshot_resource.description) - self.assertEqual( - EXAMPLE['share_proto'], snapshot_resource.share_proto) - self.assertEqual( - EXAMPLE['share_size'], snapshot_resource.share_size) - self.assertEqual( - EXAMPLE['project_id'], snapshot_resource.project_id) + self.assertEqual(EXAMPLE['description'], snapshot_resource.description) + self.assertEqual(EXAMPLE['share_proto'], snapshot_resource.share_proto) + self.assertEqual(EXAMPLE['share_size'], snapshot_resource.share_size) + self.assertEqual(EXAMPLE['project_id'], snapshot_resource.project_id) self.assertEqual(EXAMPLE['size'], snapshot_resource.size) diff --git a/openstack/tests/unit/shared_file_system/v2/test_share_snapshot_instance.py b/openstack/tests/unit/shared_file_system/v2/test_share_snapshot_instance.py index 7998d0a9f..708b4f346 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_share_snapshot_instance.py +++ b/openstack/tests/unit/shared_file_system/v2/test_share_snapshot_instance.py @@ -22,12 +22,11 @@ "created_at": "2021-06-04T00:44:52.000000", "id": "275516e8-c998-4e78-a41e-7dd3a03e71cd", "provider_location": "/path/to/fake...", - "updated_at": "2017-06-04T00:44:54.000000" + "updated_at": "2017-06-04T00:44:54.000000", } class TestShareSnapshotInstances(base.TestCase): - def test_basic(self): instances = share_snapshot_instance.ShareSnapshotInstance() self.assertEqual('snapshot_instance', instances.resource_key) @@ -40,11 +39,13 @@ def test_make_share_snapshot_instance(self): self.assertEqual(EXAMPLE['id'], instance.id) self.assertEqual(EXAMPLE['share_id'], instance.share_id) self.assertEqual( - EXAMPLE['share_instance_id'], instance.share_instance_id) + EXAMPLE['share_instance_id'], instance.share_instance_id + ) self.assertEqual(EXAMPLE['snapshot_id'], instance.snapshot_id) self.assertEqual(EXAMPLE['status'], instance.status) self.assertEqual(EXAMPLE['progress'], instance.progress) self.assertEqual(EXAMPLE['created_at'], instance.created_at) self.assertEqual(EXAMPLE['updated_at'], instance.updated_at) self.assertEqual( - EXAMPLE['provider_location'], instance.provider_location) + EXAMPLE['provider_location'], instance.provider_location + ) diff --git a/openstack/tests/unit/shared_file_system/v2/test_storage_pool.py b/openstack/tests/unit/shared_file_system/v2/test_storage_pool.py index bdd58e40f..b0e510a4c 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_storage_pool.py +++ b/openstack/tests/unit/shared_file_system/v2/test_storage_pool.py @@ -40,29 +40,30 @@ "replication_domain": None, "sg_consistent_snapshot_support": "pool", "ipv4_support": True, - "ipv6_support": False} + "ipv6_support": False, + }, } class TestStoragePool(base.TestCase): - def test_basic(self): pool_resource = storage_pool.StoragePool() self.assertEqual('pools', pool_resource.resources_key) - self.assertEqual( - '/scheduler-stats/pools', pool_resource.base_path) + self.assertEqual('/scheduler-stats/pools', pool_resource.base_path) self.assertTrue(pool_resource.allow_list) - self.assertDictEqual({ - 'pool': 'pool', - 'backend': 'backend', - 'host': 'host', - 'limit': 'limit', - 'marker': 'marker', - 'capabilities': 'capabilities', - 'share_type': 'share_type', - }, - pool_resource._query_mapping._mapping) + self.assertDictEqual( + { + 'pool': 'pool', + 'backend': 'backend', + 'host': 'host', + 'limit': 'limit', + 'marker': 'marker', + 'capabilities': 'capabilities', + 'share_type': 'share_type', + }, + pool_resource._query_mapping._mapping, + ) def test_make_storage_pool(self): pool_resource = storage_pool.StoragePool(**EXAMPLE) @@ -70,5 +71,4 @@ def test_make_storage_pool(self): self.assertEqual(EXAMPLE['host'], pool_resource.host) self.assertEqual(EXAMPLE['name'], pool_resource.name) self.assertEqual(EXAMPLE['backend'], pool_resource.backend) - self.assertEqual( - EXAMPLE['capabilities'], pool_resource.capabilities) + self.assertEqual(EXAMPLE['capabilities'], pool_resource.capabilities) diff --git a/openstack/tests/unit/shared_file_system/v2/test_user_message.py b/openstack/tests/unit/shared_file_system/v2/test_user_message.py index a718d4062..ed3123757 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_user_message.py +++ b/openstack/tests/unit/shared_file_system/v2/test_user_message.py @@ -29,12 +29,12 @@ "user_message": ( "allocate host: No storage could be allocated" "for this share request, Capabilities filter" - "didn't succeed.") + "didn't succeed." + ), } class TestUserMessage(base.TestCase): - def test_basic(self): message = user_message.UserMessage() self.assertEqual('messages', message.resources_key) @@ -46,29 +46,21 @@ def test_basic(self): self.assertTrue(message.allow_fetch) self.assertFalse(message.allow_head) - self.assertDictEqual({ - "limit": "limit", - "marker": "marker", - "message_id": "message_id" - }, - message._query_mapping._mapping) + self.assertDictEqual( + {"limit": "limit", "marker": "marker", "message_id": "message_id"}, + message._query_mapping._mapping, + ) def test_user_message(self): messages = user_message.UserMessage(**EXAMPLE) self.assertEqual(EXAMPLE['id'], messages.id) self.assertEqual(EXAMPLE['resource_id'], messages.resource_id) - self.assertEqual( - EXAMPLE['message_level'], messages.message_level) - self.assertEqual( - EXAMPLE['user_message'], messages.user_message) + self.assertEqual(EXAMPLE['message_level'], messages.message_level) + self.assertEqual(EXAMPLE['user_message'], messages.user_message) self.assertEqual(EXAMPLE['expires_at'], messages.expires_at) self.assertEqual(EXAMPLE['detail_id'], messages.detail_id) self.assertEqual(EXAMPLE['created_at'], messages.created_at) - self.assertEqual( - EXAMPLE['request_id'], messages.request_id) - self.assertEqual( - EXAMPLE['project_id'], messages.project_id) - self.assertEqual( - EXAMPLE['resource_type'], messages.resource_type) - self.assertEqual( - EXAMPLE['action_id'], messages.action_id) + self.assertEqual(EXAMPLE['request_id'], messages.request_id) + self.assertEqual(EXAMPLE['project_id'], messages.project_id) + self.assertEqual(EXAMPLE['resource_type'], messages.resource_type) + self.assertEqual(EXAMPLE['action_id'], messages.action_id) From 93d8f41713ec2128210bf0a8479a5f3872ce0382 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 5 May 2023 11:00:35 +0100 Subject: [PATCH 3245/3836] Blackify openstack.key_manager Black used with the '-l 79 -S' flags. A future change will ignore this commit in git-blame history by adding a 'git-blame-ignore-revs' file. Change-Id: Ief5e4e9fa2675c7e74b77feb54776c692deaf33f Signed-off-by: Stephen Finucane --- openstack/key_manager/v1/_format.py | 1 - openstack/key_manager/v1/_proxy.py | 20 +++--- openstack/key_manager/v1/container.py | 4 +- openstack/key_manager/v1/order.py | 3 +- openstack/key_manager/v1/secret.py | 37 +++++++---- .../unit/key_manager/v1/test_container.py | 3 +- .../tests/unit/key_manager/v1/test_order.py | 3 +- .../tests/unit/key_manager/v1/test_proxy.py | 14 ++-- .../tests/unit/key_manager/v1/test_secret.py | 65 +++++++++++-------- 9 files changed, 91 insertions(+), 59 deletions(-) diff --git a/openstack/key_manager/v1/_format.py b/openstack/key_manager/v1/_format.py index 56dbca774..8da3515ac 100644 --- a/openstack/key_manager/v1/_format.py +++ b/openstack/key_manager/v1/_format.py @@ -16,7 +16,6 @@ class HREFToUUID(format.Formatter): - @classmethod def deserialize(cls, value): """Convert a HREF to the UUID portion""" diff --git a/openstack/key_manager/v1/_proxy.py b/openstack/key_manager/v1/_proxy.py index c02b537e9..e3f5ee9cd 100644 --- a/openstack/key_manager/v1/_proxy.py +++ b/openstack/key_manager/v1/_proxy.py @@ -49,8 +49,9 @@ def delete_container(self, container, ignore_missing=True): :returns: ``None`` """ - self._delete(_container.Container, container, - ignore_missing=ignore_missing) + self._delete( + _container.Container, container, ignore_missing=ignore_missing + ) def find_container(self, name_or_id, ignore_missing=True): """Find a single container @@ -64,8 +65,9 @@ def find_container(self, name_or_id, ignore_missing=True): :returns: One :class:`~openstack.key_manager.v1.container.Container` or None """ - return self._find(_container.Container, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _container.Container, name_or_id, ignore_missing=ignore_missing + ) def get_container(self, container): """Get a single container @@ -143,8 +145,9 @@ def find_order(self, name_or_id, ignore_missing=True): attempting to find a nonexistent resource. :returns: One :class:`~openstack.key_manager.v1.order.Order` or None """ - return self._find(_order.Order, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _order.Order, name_or_id, ignore_missing=ignore_missing + ) def get_order(self, order): """Get a single order @@ -223,8 +226,9 @@ def find_secret(self, name_or_id, ignore_missing=True): :returns: One :class:`~openstack.key_manager.v1.secret.Secret` or None """ - return self._find(_secret.Secret, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _secret.Secret, name_or_id, ignore_missing=ignore_missing + ) def get_secret(self, secret): """Get a single secret diff --git a/openstack/key_manager/v1/container.py b/openstack/key_manager/v1/container.py index 4b14020ab..eebe5d038 100644 --- a/openstack/key_manager/v1/container.py +++ b/openstack/key_manager/v1/container.py @@ -30,8 +30,8 @@ class Container(resource.Resource): container_ref = resource.Body('container_ref') #: The ID for this container container_id = resource.Body( - 'container_ref', alternate_id=True, - type=_format.HREFToUUID) + 'container_ref', alternate_id=True, type=_format.HREFToUUID + ) #: The timestamp when this container was created. created_at = resource.Body('created') #: The name of this container diff --git a/openstack/key_manager/v1/order.py b/openstack/key_manager/v1/order.py index 1f7198c4e..350677f7f 100644 --- a/openstack/key_manager/v1/order.py +++ b/openstack/key_manager/v1/order.py @@ -36,7 +36,8 @@ class Order(resource.Resource): order_ref = resource.Body('order_ref') #: The ID of this order order_id = resource.Body( - 'order_ref', alternate_id=True, type=_format.HREFToUUID) + 'order_ref', alternate_id=True, type=_format.HREFToUUID + ) #: Secret href associated with the order secret_ref = resource.Body('secret_ref') #: Secret ID associated with the order diff --git a/openstack/key_manager/v1/secret.py b/openstack/key_manager/v1/secret.py index 7be69f997..aba4bca9f 100644 --- a/openstack/key_manager/v1/secret.py +++ b/openstack/key_manager/v1/secret.py @@ -27,11 +27,17 @@ class Secret(resource.Resource): allow_list = True _query_mapping = resource.QueryParameters( - "name", "mode", "bits", - "secret_type", "acl_only", - "created", "updated", - "expiration", "sort", - algorithm="alg") + "name", + "mode", + "bits", + "secret_type", + "acl_only", + "created", + "updated", + "expiration", + "sort", + algorithm="alg", + ) # Properties #: Metadata provided by a user or system for informational purposes @@ -59,7 +65,8 @@ class Secret(resource.Resource): # in all of OpenStack because of the departure from using actual IDs # that even this service can't even use itself. secret_id = resource.Body( - 'secret_ref', alternate_id=True, type=_format.HREFToUUID) + 'secret_ref', alternate_id=True, type=_format.HREFToUUID + ) #: Used to indicate the type of secret being stored. secret_type = resource.Body('secret_type') #: The status of this secret @@ -77,10 +84,17 @@ class Secret(resource.Resource): #: (required if payload is encoded) payload_content_encoding = resource.Body('payload_content_encoding') - def fetch(self, session, requires_id=True, - base_path=None, error_message=None, skip_cache=False): - request = self._prepare_request(requires_id=requires_id, - base_path=base_path) + def fetch( + self, + session, + requires_id=True, + base_path=None, + error_message=None, + skip_cache=False, + ): + request = self._prepare_request( + requires_id=requires_id, base_path=base_path + ) response = session.get(request.url).json() @@ -96,7 +110,8 @@ def fetch(self, session, requires_id=True, payload = session.get( utils.urljoin(request.url, "payload"), headers={"Accept": content_type}, - skip_cache=skip_cache) + skip_cache=skip_cache, + ) response["payload"] = payload.text # We already have the JSON here so don't call into _translate_response diff --git a/openstack/tests/unit/key_manager/v1/test_container.py b/openstack/tests/unit/key_manager/v1/test_container.py index 789d61c7c..bb43d6f5b 100644 --- a/openstack/tests/unit/key_manager/v1/test_container.py +++ b/openstack/tests/unit/key_manager/v1/test_container.py @@ -24,12 +24,11 @@ 'status': '5', 'type': '6', 'updated': '2015-03-09T12:15:57.233772', - 'consumers': ['7'] + 'consumers': ['7'], } class TestContainer(base.TestCase): - def test_basic(self): sot = container.Container() self.assertIsNone(sot.resource_key) diff --git a/openstack/tests/unit/key_manager/v1/test_order.py b/openstack/tests/unit/key_manager/v1/test_order.py index d9aee65f1..bdb1198f0 100644 --- a/openstack/tests/unit/key_manager/v1/test_order.py +++ b/openstack/tests/unit/key_manager/v1/test_order.py @@ -27,12 +27,11 @@ 'sub_status': '7', 'sub_status_message': '8', 'type': '9', - 'updated': '10' + 'updated': '10', } class TestOrder(base.TestCase): - def test_basic(self): sot = order.Order() self.assertIsNone(sot.resource_key) diff --git a/openstack/tests/unit/key_manager/v1/test_proxy.py b/openstack/tests/unit/key_manager/v1/test_proxy.py index f6d104939..e5ec2bb9b 100644 --- a/openstack/tests/unit/key_manager/v1/test_proxy.py +++ b/openstack/tests/unit/key_manager/v1/test_proxy.py @@ -28,12 +28,14 @@ def test_server_create_attrs(self): self.verify_create(self.proxy.create_container, container.Container) def test_container_delete(self): - self.verify_delete(self.proxy.delete_container, - container.Container, False) + self.verify_delete( + self.proxy.delete_container, container.Container, False + ) def test_container_delete_ignore(self): - self.verify_delete(self.proxy.delete_container, - container.Container, True) + self.verify_delete( + self.proxy.delete_container, container.Container, True + ) def test_container_find(self): self.verify_find(self.proxy.find_container, container.Container) @@ -87,8 +89,8 @@ def test_secret_find(self): def test_secret_get(self): self.verify_get(self.proxy.get_secret, secret.Secret) self.verify_get_overrided( - self.proxy, secret.Secret, - 'openstack.key_manager.v1.secret.Secret') + self.proxy, secret.Secret, 'openstack.key_manager.v1.secret.Secret' + ) def test_secrets(self): self.verify_list(self.proxy.secrets, secret.Secret) diff --git a/openstack/tests/unit/key_manager/v1/test_secret.py b/openstack/tests/unit/key_manager/v1/test_secret.py index 7202c54ae..819e2a930 100644 --- a/openstack/tests/unit/key_manager/v1/test_secret.py +++ b/openstack/tests/unit/key_manager/v1/test_secret.py @@ -31,12 +31,11 @@ 'secret_type': '9', 'payload': '10', 'payload_content_type': '11', - 'payload_content_encoding': '12' + 'payload_content_encoding': '12', } class TestSecret(base.TestCase): - def test_basic(self): sot = secret.Secret() self.assertIsNone(sot.resource_key) @@ -48,19 +47,23 @@ def test_basic(self): self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertDictEqual({"name": "name", - "mode": "mode", - "bits": "bits", - "secret_type": "secret_type", - "acl_only": "acl_only", - "created": "created", - "updated": "updated", - "expiration": "expiration", - "sort": "sort", - "algorithm": "alg", - "limit": "limit", - "marker": "marker"}, - sot._query_mapping._mapping) + self.assertDictEqual( + { + "name": "name", + "mode": "mode", + "bits": "bits", + "secret_type": "secret_type", + "acl_only": "acl_only", + "created": "created", + "updated": "updated", + "expiration": "expiration", + "sort": "sort", + "algorithm": "alg", + "limit": "limit", + "marker": "marker", + }, + sot._query_mapping._mapping, + ) def test_make_it(self): sot = secret.Secret(**EXAMPLE) @@ -77,10 +80,12 @@ def test_make_it(self): self.assertEqual(EXAMPLE['updated'], sot.updated_at) self.assertEqual(EXAMPLE['secret_type'], sot.secret_type) self.assertEqual(EXAMPLE['payload'], sot.payload) - self.assertEqual(EXAMPLE['payload_content_type'], - sot.payload_content_type) - self.assertEqual(EXAMPLE['payload_content_encoding'], - sot.payload_content_encoding) + self.assertEqual( + EXAMPLE['payload_content_type'], sot.payload_content_type + ) + self.assertEqual( + EXAMPLE['payload_content_encoding'], sot.payload_content_encoding + ) def test_get_no_payload(self): sot = secret.Secret(id="id") @@ -112,11 +117,17 @@ def _test_payload(self, sot, metadata, content_type): rv = sot.fetch(sess) sess.get.assert_has_calls( - [mock.call("secrets/id",), - mock.call( - "secrets/id/payload", - headers={"Accept": content_type}, - skip_cache=False)]) + [ + mock.call( + "secrets/id", + ), + mock.call( + "secrets/id/payload", + headers={"Accept": content_type}, + skip_cache=False, + ), + ] + ) self.assertEqual(rv.payload, payload) self.assertEqual(rv.status, metadata["status"]) @@ -129,7 +140,9 @@ def test_get_with_payload_from_argument(self): def test_get_with_payload_from_content_types(self): content_type = "some/type" - metadata = {"status": "fine", - "content_types": {"default": content_type}} + metadata = { + "status": "fine", + "content_types": {"default": content_type}, + } sot = secret.Secret(id="id") self._test_payload(sot, metadata, content_type) From 409f648ce506d7e768305f75025c4b01c5fa3008 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 5 May 2023 11:01:20 +0100 Subject: [PATCH 3246/3836] Blackify openstack.placement Black used with the '-l 79 -S' flags. A future change will ignore this commit in git-blame history by adding a 'git-blame-ignore-revs' file. Change-Id: If63b878036e2af53cfb4bc2ff29872253ba21597 Signed-off-by: Stephen Finucane --- openstack/placement/placement_service.py | 1 + openstack/placement/v1/_proxy.py | 14 ++++++++++---- openstack/placement/v1/resource_provider.py | 7 ++++++- .../placement/v1/test_resource_provider.py | 7 ++++--- openstack/tests/unit/placement/v1/test_proxy.py | 1 - .../tests/unit/placement/v1/test_resource_class.py | 5 ++--- .../unit/placement/v1/test_resource_provider.py | 7 ++++--- 7 files changed, 27 insertions(+), 15 deletions(-) diff --git a/openstack/placement/placement_service.py b/openstack/placement/placement_service.py index 045b36a88..2fab7a035 100644 --- a/openstack/placement/placement_service.py +++ b/openstack/placement/placement_service.py @@ -16,6 +16,7 @@ class PlacementService(service_description.ServiceDescription): """The placement service.""" + supported_versions = { '1': _proxy.Proxy, } diff --git a/openstack/placement/v1/_proxy.py b/openstack/placement/v1/_proxy.py index 67dee393c..fc6c92a44 100644 --- a/openstack/placement/v1/_proxy.py +++ b/openstack/placement/v1/_proxy.py @@ -70,7 +70,9 @@ class or an :rtype: :class:`~openstack.placement.v1.resource_class.ResourceClass` """ return self._update( - _resource_class.ResourceClass, resource_class, **attrs, + _resource_class.ResourceClass, + resource_class, + **attrs, ) def get_resource_class(self, resource_class): @@ -87,7 +89,8 @@ class or an resource class matching the criteria could be found. """ return self._get( - _resource_class.ResourceClass, resource_class, + _resource_class.ResourceClass, + resource_class, ) def resource_classes(self, **query): @@ -149,7 +152,9 @@ def update_resource_provider(self, resource_provider, **attrs): :rtype: :class:`~openstack.placement.v1.resource_provider.ResourceProvider` """ # noqa: E501 return self._update( - _resource_provider.ResourceProvider, resource_provider, **attrs, + _resource_provider.ResourceProvider, + resource_provider, + **attrs, ) def get_resource_provider(self, resource_provider): @@ -166,7 +171,8 @@ def get_resource_provider(self, resource_provider): resource provider matching the criteria could be found. """ return self._get( - _resource_provider.ResourceProvider, resource_provider, + _resource_provider.ResourceProvider, + resource_provider, ) def find_resource_provider(self, name_or_id, ignore_missing=True): diff --git a/openstack/placement/v1/resource_provider.py b/openstack/placement/v1/resource_provider.py index f9ab1a9b3..1d6ba5d83 100644 --- a/openstack/placement/v1/resource_provider.py +++ b/openstack/placement/v1/resource_provider.py @@ -29,7 +29,12 @@ class ResourceProvider(resource.Resource): # Filters _query_mapping = resource.QueryParameters( - 'name', 'member_of', 'resources', 'in_tree', 'required', id='uuid', + 'name', + 'member_of', + 'resources', + 'in_tree', + 'required', + id='uuid', ) # The parent_provider_uuid and root_provider_uuid fields were introduced in diff --git a/openstack/tests/functional/placement/v1/test_resource_provider.py b/openstack/tests/functional/placement/v1/test_resource_provider.py index 11bee5298..307c82f69 100644 --- a/openstack/tests/functional/placement/v1/test_resource_provider.py +++ b/openstack/tests/functional/placement/v1/test_resource_provider.py @@ -15,7 +15,6 @@ class TestResourceProvider(base.BaseFunctionalTest): - def setUp(self): super().setUp() self._set_operator_cloud(interface='admin') @@ -29,7 +28,8 @@ def setUp(self): def tearDown(self): sot = self.conn.placement.delete_resource_provider( - self._resource_provider) + self._resource_provider + ) self.assertIsNone(sot) super().tearDown() @@ -39,7 +39,8 @@ def test_find(self): def test_get(self): sot = self.conn.placement.get_resource_provider( - self._resource_provider.id) + self._resource_provider.id + ) self.assertEqual(self.NAME, sot.name) def test_list(self): diff --git a/openstack/tests/unit/placement/v1/test_proxy.py b/openstack/tests/unit/placement/v1/test_proxy.py index 014b7a5f9..2c9cdec9c 100644 --- a/openstack/tests/unit/placement/v1/test_proxy.py +++ b/openstack/tests/unit/placement/v1/test_proxy.py @@ -17,7 +17,6 @@ class TestPlacementProxy(test_proxy_base.TestProxyBase): - def setUp(self): super().setUp() self.proxy = _proxy.Proxy(self.session) diff --git a/openstack/tests/unit/placement/v1/test_resource_class.py b/openstack/tests/unit/placement/v1/test_resource_class.py index 9114dec34..09e1f0e17 100644 --- a/openstack/tests/unit/placement/v1/test_resource_class.py +++ b/openstack/tests/unit/placement/v1/test_resource_class.py @@ -19,7 +19,6 @@ class TestResourceClass(base.TestCase): - def test_basic(self): sot = rc.ResourceClass() self.assertEqual(None, sot.resource_key) @@ -33,8 +32,8 @@ def test_basic(self): self.assertFalse(sot.allow_patch) self.assertDictEqual( - {'limit': 'limit', 'marker': 'marker'}, - sot._query_mapping._mapping) + {'limit': 'limit', 'marker': 'marker'}, sot._query_mapping._mapping + ) def test_make_it(self): sot = rc.ResourceClass(**FAKE) diff --git a/openstack/tests/unit/placement/v1/test_resource_provider.py b/openstack/tests/unit/placement/v1/test_resource_provider.py index 0b13d0a2a..a63bcb22e 100644 --- a/openstack/tests/unit/placement/v1/test_resource_provider.py +++ b/openstack/tests/unit/placement/v1/test_resource_provider.py @@ -21,7 +21,6 @@ class TestResourceProvider(base.TestCase): - def test_basic(self): sot = rp.ResourceProvider() self.assertEqual(None, sot.resource_key) @@ -45,12 +44,14 @@ def test_basic(self): 'required': 'required', 'id': 'uuid', }, - sot._query_mapping._mapping) + sot._query_mapping._mapping, + ) def test_make_it(self): sot = rp.ResourceProvider(**FAKE) self.assertEqual(FAKE['uuid'], sot.id) self.assertEqual(FAKE['name'], sot.name) self.assertEqual( - FAKE['parent_provider_uuid'], sot.parent_provider_id, + FAKE['parent_provider_uuid'], + sot.parent_provider_id, ) From 874ea74103a0c833df7668a45b96b7145a8158a2 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 5 May 2023 11:01:48 +0100 Subject: [PATCH 3247/3836] Blackify openstack.orchestration Black used with the '-l 79 -S' flags. A future change will ignore this commit in git-blame history by adding a 'git-blame-ignore-revs' file. Change-Id: I77ee83b03379efbec18ba61166fd74ce5ee0e84b Signed-off-by: Stephen Finucane --- .../orchestration/util/environment_format.py | 21 +- openstack/orchestration/util/event_utils.py | 31 ++- .../orchestration/util/template_format.py | 11 +- .../orchestration/util/template_utils.py | 147 ++++++++---- openstack/orchestration/util/utils.py | 8 +- openstack/orchestration/v1/_proxy.py | 159 ++++++++----- openstack/orchestration/v1/resource.py | 5 +- openstack/orchestration/v1/software_config.py | 5 +- .../orchestration/v1/software_deployment.py | 6 +- openstack/orchestration/v1/stack.py | 72 ++++-- openstack/orchestration/v1/template.py | 10 +- .../functional/orchestration/v1/test_stack.py | 15 +- .../tests/unit/orchestration/test_version.py | 1 - .../tests/unit/orchestration/v1/test_proxy.py | 225 ++++++++++++------ .../unit/orchestration/v1/test_resource.py | 22 +- .../orchestration/v1/test_software_config.py | 1 - .../v1/test_software_deployment.py | 6 +- .../tests/unit/orchestration/v1/test_stack.py | 99 ++++---- .../v1/test_stack_environment.py | 26 +- .../unit/orchestration/v1/test_stack_files.py | 13 +- .../orchestration/v1/test_stack_template.py | 47 ++-- .../unit/orchestration/v1/test_template.py | 25 +- 22 files changed, 576 insertions(+), 379 deletions(-) diff --git a/openstack/orchestration/util/environment_format.py b/openstack/orchestration/util/environment_format.py index 8a9c9745c..7afbe06b9 100644 --- a/openstack/orchestration/util/environment_format.py +++ b/openstack/orchestration/util/environment_format.py @@ -16,13 +16,19 @@ SECTIONS = ( - PARAMETER_DEFAULTS, PARAMETERS, RESOURCE_REGISTRY, - ENCRYPTED_PARAM_NAMES, EVENT_SINKS, - PARAMETER_MERGE_STRATEGIES + PARAMETER_DEFAULTS, + PARAMETERS, + RESOURCE_REGISTRY, + ENCRYPTED_PARAM_NAMES, + EVENT_SINKS, + PARAMETER_MERGE_STRATEGIES, ) = ( - 'parameter_defaults', 'parameters', 'resource_registry', - 'encrypted_param_names', 'event_sinks', - 'parameter_merge_strategies' + 'parameter_defaults', + 'parameters', + 'resource_registry', + 'encrypted_param_names', + 'event_sinks', + 'parameter_merge_strategies', ) @@ -47,7 +53,8 @@ def parse(env_str): env = {} elif not isinstance(env, dict): raise ValueError( - 'The environment is not a valid YAML mapping data type.') + 'The environment is not a valid YAML mapping data type.' + ) for param in env: if param not in SECTIONS: diff --git a/openstack/orchestration/util/event_utils.py b/openstack/orchestration/util/event_utils.py index 6ae36a39d..763924cb0 100644 --- a/openstack/orchestration/util/event_utils.py +++ b/openstack/orchestration/util/event_utils.py @@ -30,8 +30,8 @@ def get_events(cloud, stack_id, event_args, marker=None, limit=None): event_args['limit'] = limit data = cloud._orchestration_client.get( - '/stacks/{id}/events'.format(id=stack_id), - params=params) + '/stacks/{id}/events'.format(id=stack_id), params=params + ) events = meta.get_and_munchify('events', data) # Show which stack the event comes from (for nested events) @@ -41,7 +41,8 @@ def get_events(cloud, stack_id, event_args, marker=None, limit=None): def poll_for_events( - cloud, stack_name, action=None, poll_period=5, marker=None): + cloud, stack_name, action=None, poll_period=5, marker=None +): """Continuously poll events and logs for performed action on stack.""" def stop_check_action(a): @@ -60,20 +61,26 @@ def stop_check_no_action(a): msg_template = "\n Stack %(name)s %(status)s \n" def is_stack_event(event): - if (event.get('resource_name', '') != stack_name - and event.get('physical_resource_id', '') != stack_name): + if ( + event.get('resource_name', '') != stack_name + and event.get('physical_resource_id', '') != stack_name + ): return False phys_id = event.get('physical_resource_id', '') - links = dict((link.get('rel'), - link.get('href')) for link in event.get('links', [])) + links = dict( + (link.get('rel'), link.get('href')) + for link in event.get('links', []) + ) stack_id = links.get('stack', phys_id).rsplit('/', 1)[-1] return stack_id == phys_id while True: events = get_events( - cloud, stack_id=stack_name, - event_args={'sort_dir': 'asc', 'marker': marker}) + cloud, + stack_id=stack_name, + event_args={'sort_dir': 'asc', 'marker': marker}, + ) if len(events) == 0: no_event_polls += 1 @@ -87,7 +94,8 @@ def is_stack_event(event): if is_stack_event(event): stack_status = getattr(event, 'resource_status', '') msg = msg_template % dict( - name=stack_name, status=stack_status) + name=stack_name, status=stack_status + ) if stop_check(stack_status): return stack_status, msg @@ -95,8 +103,7 @@ def is_stack_event(event): # after 2 polls with no events, fall back to a stack get stack = cloud.get_stack(stack_name, resolve_outputs=False) stack_status = stack['stack_status'] - msg = msg_template % dict( - name=stack_name, status=stack_status) + msg = msg_template % dict(name=stack_name, status=stack_status) if stop_check(stack_status): return stack_status, msg # go back to event polling again diff --git a/openstack/orchestration/util/template_format.py b/openstack/orchestration/util/template_format.py index f98e97fbb..618490811 100644 --- a/openstack/orchestration/util/template_format.py +++ b/openstack/orchestration/util/template_format.py @@ -36,7 +36,8 @@ def _construct_yaml_str(self, node): # openstack.common.jsonutils. Therefore, make unicode string out of timestamps # until jsonutils can handle dates. HeatYamlLoader.add_constructor( - u'tag:yaml.org,2002:timestamp', _construct_yaml_str) + u'tag:yaml.org,2002:timestamp', _construct_yaml_str +) def parse(tmpl_str): @@ -64,8 +65,10 @@ def parse(tmpl_str): if tpl is None: tpl = {} # Looking for supported version keys in the loaded template - if not ('HeatTemplateFormatVersion' in tpl - or 'heat_template_version' in tpl - or 'AWSTemplateFormatVersion' in tpl): + if not ( + 'HeatTemplateFormatVersion' in tpl + or 'heat_template_version' in tpl + or 'AWSTemplateFormatVersion' in tpl + ): raise ValueError("Template format version not found.") return tpl diff --git a/openstack/orchestration/util/template_utils.py b/openstack/orchestration/util/template_utils.py index 7280b54d6..f61ca7b92 100644 --- a/openstack/orchestration/util/template_utils.py +++ b/openstack/orchestration/util/template_utils.py @@ -23,9 +23,14 @@ from openstack.orchestration.util import utils -def get_template_contents(template_file=None, template_url=None, - template_object=None, object_request=None, - files=None, existing=False): +def get_template_contents( + template_file=None, + template_url=None, + template_object=None, + object_request=None, + files=None, + existing=False, +): is_object = False tpl = None @@ -46,11 +51,13 @@ def get_template_contents(template_file=None, template_url=None, else: raise exceptions.SDKException( 'Must provide one of template_file,' - ' template_url or template_object') + ' template_url or template_object' + ) if not tpl: raise exceptions.SDKException( - 'Could not fetch template from %s' % template_url) + 'Could not fetch template from %s' % template_url + ) try: if isinstance(tpl, bytes): @@ -58,35 +65,43 @@ def get_template_contents(template_file=None, template_url=None, template = template_format.parse(tpl) except ValueError as e: raise exceptions.SDKException( - 'Error parsing template %(url)s %(error)s' % - {'url': template_url, 'error': e}) + 'Error parsing template %(url)s %(error)s' + % {'url': template_url, 'error': e} + ) tmpl_base_url = utils.base_url_for_url(template_url) if files is None: files = {} - resolve_template_get_files(template, files, tmpl_base_url, is_object, - object_request) + resolve_template_get_files( + template, files, tmpl_base_url, is_object, object_request + ) return files, template -def resolve_template_get_files(template, files, template_base_url, - is_object=False, object_request=None): - +def resolve_template_get_files( + template, files, template_base_url, is_object=False, object_request=None +): def ignore_if(key, value): if key != 'get_file' and key != 'type': return True if not isinstance(value, str): return True - if (key == 'type' - and not value.endswith(('.yaml', '.template'))): + if key == 'type' and not value.endswith(('.yaml', '.template')): return True return False def recurse_if(value): return isinstance(value, (dict, list)) - get_file_contents(template, files, template_base_url, - ignore_if, recurse_if, is_object, object_request) + get_file_contents( + template, + files, + template_base_url, + ignore_if, + recurse_if, + is_object, + object_request, + ) def is_template(file_content): @@ -99,9 +114,15 @@ def is_template(file_content): return True -def get_file_contents(from_data, files, base_url=None, - ignore_if=None, recurse_if=None, - is_object=False, object_request=None): +def get_file_contents( + from_data, + files, + base_url=None, + ignore_if=None, + recurse_if=None, + is_object=False, + object_request=None, +): if recurse_if and recurse_if(from_data): if isinstance(from_data, dict): @@ -109,8 +130,15 @@ def get_file_contents(from_data, files, base_url=None, else: recurse_data = from_data for value in recurse_data: - get_file_contents(value, files, base_url, ignore_if, recurse_if, - is_object, object_request) + get_file_contents( + value, + files, + base_url, + ignore_if, + recurse_if, + is_object, + object_request, + ) if isinstance(from_data, dict): for key, value in from_data.items(): @@ -129,11 +157,14 @@ def get_file_contents(from_data, files, base_url=None, if is_template(file_content): if is_object: template = get_template_contents( - template_object=str_url, files=files, - object_request=object_request)[1] + template_object=str_url, + files=files, + object_request=object_request, + )[1] else: template = get_template_contents( - template_url=str_url, files=files)[1] + template_url=str_url, files=files + )[1] file_content = json.dumps(template) files[str_url] = file_content # replace the data value with the normalised absolute URL @@ -157,11 +188,14 @@ def deep_update(old, new): return old -def process_multiple_environments_and_files(env_paths=None, template=None, - template_url=None, - env_path_is_object=None, - object_request=None, - env_list_tracker=None): +def process_multiple_environments_and_files( + env_paths=None, + template=None, + template_url=None, + env_path_is_object=None, + object_request=None, + env_list_tracker=None, +): """Reads one or more environment files. Reads in each specified environment file and returns a dictionary @@ -204,7 +238,8 @@ def process_multiple_environments_and_files(env_paths=None, template=None, template_url=template_url, env_path_is_object=env_path_is_object, object_request=object_request, - include_env_in_files=include_env_in_files) + include_env_in_files=include_env_in_files, + ) # 'files' looks like {"filename1": contents, "filename2": contents} # so a simple update is enough for merging @@ -221,12 +256,14 @@ def process_multiple_environments_and_files(env_paths=None, template=None, return merged_files, merged_env -def process_environment_and_files(env_path=None, - template=None, - template_url=None, - env_path_is_object=None, - object_request=None, - include_env_in_files=False): +def process_environment_and_files( + env_path=None, + template=None, + template_url=None, + env_path_is_object=None, + object_request=None, + include_env_in_files=False, +): """Loads a single environment file. Returns an entry suitable for the files dict which maps the environment @@ -253,7 +290,10 @@ def process_environment_and_files(env_path=None, resolve_environment_urls( env.get('resource_registry'), files, - env_base_url, is_object=True, object_request=object_request) + env_base_url, + is_object=True, + object_request=object_request, + ) elif env_path: env_url = utils.normalise_file_path_to_url(env_path) @@ -263,9 +303,8 @@ def process_environment_and_files(env_path=None, env = environment_format.parse(raw_env) resolve_environment_urls( - env.get('resource_registry'), - files, - env_base_url) + env.get('resource_registry'), files, env_base_url + ) if include_env_in_files: files[env_url] = json.dumps(env) @@ -273,8 +312,13 @@ def process_environment_and_files(env_path=None, return files, env -def resolve_environment_urls(resource_registry, files, env_base_url, - is_object=False, object_request=None): +def resolve_environment_urls( + resource_registry, + files, + env_base_url, + is_object=False, + object_request=None, +): """Handles any resource URLs specified in an environment. :param resource_registry: mapping of type name to template filename @@ -302,11 +346,22 @@ def ignore_if(key, value): if key in ['hooks', 'restricted_actions']: return True - get_file_contents(rr, files, base_url, ignore_if, - is_object=is_object, object_request=object_request) + get_file_contents( + rr, + files, + base_url, + ignore_if, + is_object=is_object, + object_request=object_request, + ) for res_name, res_dict in rr.get('resources', {}).items(): res_base_url = res_dict.get('base_url', base_url) get_file_contents( - res_dict, files, res_base_url, ignore_if, - is_object=is_object, object_request=object_request) + res_dict, + files, + res_base_url, + ignore_if, + is_object=is_object, + object_request=object_request, + ) diff --git a/openstack/orchestration/util/utils.py b/openstack/orchestration/util/utils.py index d5e805195..6a166c574 100644 --- a/openstack/orchestration/util/utils.py +++ b/openstack/orchestration/util/utils.py @@ -40,8 +40,7 @@ def read_url_content(url): # TODO(mordred) Use requests content = request.urlopen(url).read() except error.URLError: - raise exceptions.SDKException( - 'Could not fetch contents for %s' % url) + raise exceptions.SDKException('Could not fetch contents for %s' % url) if content: try: @@ -52,8 +51,9 @@ def read_url_content(url): def resource_nested_identifier(rsrc): - nested_link = [link for link in rsrc.links or [] - if link.get('rel') == 'nested'] + nested_link = [ + link for link in rsrc.links or [] if link.get('rel') == 'nested' + ] if nested_link: nested_href = nested_link[0].get('href') nested_identifier = nested_href.split("/")[-2:] diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index d2c6367f0..2ac55c97f 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -36,23 +36,34 @@ class Proxy(proxy.Proxy): } def _extract_name_consume_url_parts(self, url_parts): - if (len(url_parts) == 3 and url_parts[0] == 'software_deployments' - and url_parts[1] == 'metadata'): + if ( + len(url_parts) == 3 + and url_parts[0] == 'software_deployments' + and url_parts[1] == 'metadata' + ): # Another nice example of totally different URL naming scheme, # which we need to repair /software_deployment/metadata/server_id - # just replace server_id with metadata to keep further logic return ['software_deployment', 'metadata'] - if (url_parts[0] == 'stacks' and len(url_parts) > 2 - and not url_parts[2] in ['preview', 'resources']): + if ( + url_parts[0] == 'stacks' + and len(url_parts) > 2 + and not url_parts[2] in ['preview', 'resources'] + ): # orchestrate introduce having stack name and id part of the URL # (/stacks/name/id/everything_else), so if on third position we # have not a known part - discard it, not to brake further logic del url_parts[2] return super(Proxy, self)._extract_name_consume_url_parts(url_parts) - def read_env_and_templates(self, template_file=None, template_url=None, - template_object=None, files=None, - environment_files=None): + def read_env_and_templates( + self, + template_file=None, + template_url=None, + template_object=None, + files=None, + environment_files=None, + ): """Read templates and environment content and prepares corresponding stack attributes @@ -70,16 +81,20 @@ def read_env_and_templates(self, template_file=None, template_url=None, envfiles = dict() tpl_files = None if environment_files: - envfiles, env = \ - template_utils.process_multiple_environments_and_files( - env_paths=environment_files) + ( + envfiles, + env, + ) = template_utils.process_multiple_environments_and_files( + env_paths=environment_files + ) stack_attrs['environment'] = env if template_file or template_url or template_object: tpl_files, template = template_utils.get_template_contents( template_file=template_file, template_url=template_url, template_object=template_object, - files=files) + files=files, + ) stack_attrs['template'] = template if tpl_files or envfiles: stack_attrs['files'] = dict( @@ -104,8 +119,9 @@ def create_stack(self, preview=False, **attrs): base_path = None if not preview else '/stacks/preview' return self._create(_stack.Stack, base_path=base_path, **attrs) - def find_stack(self, name_or_id, - ignore_missing=True, resolve_outputs=True): + def find_stack( + self, name_or_id, ignore_missing=True, resolve_outputs=True + ): """Find a single stack :param name_or_id: The name or ID of a stack. @@ -116,9 +132,12 @@ def find_stack(self, name_or_id, attempting to find a nonexistent resource. :returns: One :class:`~openstack.orchestration.v1.stack.Stack` or None """ - return self._find(_stack.Stack, name_or_id, - ignore_missing=ignore_missing, - resolve_outputs=resolve_outputs) + return self._find( + _stack.Stack, + name_or_id, + ignore_missing=ignore_missing, + resolve_outputs=resolve_outputs, + ) def stacks(self, **query): """Return a generator of stacks @@ -219,8 +238,12 @@ def get_stack_template(self, stack): else: obj = self._find(_stack.Stack, stack, ignore_missing=False) - return self._get(_stack_template.StackTemplate, requires_id=False, - stack_name=obj.name, stack_id=obj.id) + return self._get( + _stack_template.StackTemplate, + requires_id=False, + stack_name=obj.name, + stack_id=obj.id, + ) def get_stack_environment(self, stack): """Get environment used by a stack @@ -238,9 +261,12 @@ def get_stack_environment(self, stack): else: obj = self._find(_stack.Stack, stack, ignore_missing=False) - return self._get(_stack_environment.StackEnvironment, - requires_id=False, stack_name=obj.name, - stack_id=obj.id) + return self._get( + _stack_environment.StackEnvironment, + requires_id=False, + stack_name=obj.name, + stack_id=obj.id, + ) def get_stack_files(self, stack): """Get files used by a stack @@ -283,8 +309,9 @@ def resources(self, stack, **query): else: obj = self._find(_stack.Stack, stack, ignore_missing=False) - return self._list(_resource.Resource, stack_name=obj.name, - stack_id=obj.id, **query) + return self._list( + _resource.Resource, stack_name=obj.name, stack_id=obj.id, **query + ) def create_software_config(self, **attrs): """Create a new software config from attributes @@ -335,8 +362,9 @@ def delete_software_config(self, software_config, ignore_missing=True): attempting to delete a nonexistent software config. :returns: ``None`` """ - self._delete(_sc.SoftwareConfig, software_config, - ignore_missing=ignore_missing) + self._delete( + _sc.SoftwareConfig, software_config, ignore_missing=ignore_missing + ) def create_software_deployment(self, **attrs): """Create a new software deployment from attributes @@ -374,8 +402,9 @@ def get_software_deployment(self, software_deployment): """ return self._get(_sd.SoftwareDeployment, software_deployment) - def delete_software_deployment(self, software_deployment, - ignore_missing=True): + def delete_software_deployment( + self, software_deployment, ignore_missing=True + ): """Delete a software deployment :param software_deployment: The value can be either the ID of a @@ -388,8 +417,11 @@ def delete_software_deployment(self, software_deployment, attempting to delete a nonexistent software deployment. :returns: ``None`` """ - self._delete(_sd.SoftwareDeployment, software_deployment, - ignore_missing=ignore_missing) + self._delete( + _sd.SoftwareDeployment, + software_deployment, + ignore_missing=ignore_missing, + ) def update_software_deployment(self, software_deployment, **attrs): """Update a software deployment @@ -403,11 +435,13 @@ def update_software_deployment(self, software_deployment, **attrs): :rtype: :class:`~openstack.orchestration.v1.software_deployment.SoftwareDeployment` """ - return self._update(_sd.SoftwareDeployment, software_deployment, - **attrs) + return self._update( + _sd.SoftwareDeployment, software_deployment, **attrs + ) - def validate_template(self, template, environment=None, template_url=None, - ignore_errors=None): + def validate_template( + self, template, environment=None, template_url=None, ignore_errors=None + ): """Validates a template. :param template: The stack template on which the validation is @@ -429,15 +463,21 @@ def validate_template(self, template, environment=None, template_url=None, """ if template is None and template_url is None: raise exceptions.InvalidRequest( - "'template_url' must be specified when template is None") + "'template_url' must be specified when template is None" + ) tmpl = _template.Template.new() - return tmpl.validate(self, template, environment=environment, - template_url=template_url, - ignore_errors=ignore_errors) - - def wait_for_status(self, res, status='ACTIVE', failures=None, - interval=2, wait=120): + return tmpl.validate( + self, + template, + environment=environment, + template_url=template_url, + ignore_errors=ignore_errors, + ) + + def wait_for_status( + self, res, status='ACTIVE', failures=None, interval=2, wait=120 + ): """Wait for a resource to be in a particular status. :param res: The resource to wait on to reach the specified status. @@ -460,7 +500,8 @@ def wait_for_status(self, res, status='ACTIVE', failures=None, """ failures = [] if failures is None else failures return resource.wait_for_status( - self, res, status, failures, interval, wait) + self, res, status, failures, interval, wait + ) def wait_for_delete(self, res, interval=2, wait=120): """Wait for a resource to be deleted. @@ -478,26 +519,37 @@ def wait_for_delete(self, res, interval=2, wait=120): return resource.wait_for_delete(self, res, interval, wait) def get_template_contents( - self, template_file=None, template_url=None, - template_object=None, files=None): + self, + template_file=None, + template_url=None, + template_object=None, + files=None, + ): try: return template_utils.get_template_contents( - template_file=template_file, template_url=template_url, - template_object=template_object, files=files) + template_file=template_file, + template_url=template_url, + template_object=template_object, + files=files, + ) except Exception as e: raise exceptions.SDKException( - "Error in processing template files: %s" % str(e)) + "Error in processing template files: %s" % str(e) + ) def _get_cleanup_dependencies(self): return { - 'orchestration': { - 'before': ['compute', 'network', 'identity'] - } + 'orchestration': {'before': ['compute', 'network', 'identity']} } - def _service_cleanup(self, dry_run=True, client_status_queue=None, - identified_resources=None, - filters=None, resource_evaluation_fn=None): + def _service_cleanup( + self, + dry_run=True, + client_status_queue=None, + identified_resources=None, + filters=None, + resource_evaluation_fn=None, + ): stacks = [] for obj in self.stacks(): need_delete = self._service_cleanup_del_res( @@ -507,7 +559,8 @@ def _service_cleanup(self, dry_run=True, client_status_queue=None, client_status_queue=client_status_queue, identified_resources=identified_resources, filters=filters, - resource_evaluation_fn=resource_evaluation_fn) + resource_evaluation_fn=resource_evaluation_fn, + ) if not dry_run and need_delete: stacks.append(obj) diff --git a/openstack/orchestration/v1/resource.py b/openstack/orchestration/v1/resource.py index ddc466fda..8f7ad017b 100644 --- a/openstack/orchestration/v1/resource.py +++ b/openstack/orchestration/v1/resource.py @@ -31,8 +31,9 @@ class Resource(resource.Resource): links = resource.Body('links') #: ID of the logical resource, usually the literal name of the resource #: as it appears in the stack template. - logical_resource_id = resource.Body('logical_resource_id', - alternate_id=True) + logical_resource_id = resource.Body( + 'logical_resource_id', alternate_id=True + ) #: Name of the resource. name = resource.Body('resource_name') #: ID of the physical resource (if any) that backs up the resource. For diff --git a/openstack/orchestration/v1/software_config.py b/openstack/orchestration/v1/software_config.py index 4e2c128e7..21a9c9750 100644 --- a/openstack/orchestration/v1/software_config.py +++ b/openstack/orchestration/v1/software_config.py @@ -48,5 +48,6 @@ class SoftwareConfig(resource.Resource): def create(self, session, base_path=None): # This overrides the default behavior of resource creation because # heat doesn't accept resource_key in its request. - return super(SoftwareConfig, self).create(session, prepend_key=False, - base_path=base_path) + return super(SoftwareConfig, self).create( + session, prepend_key=False, base_path=base_path + ) diff --git a/openstack/orchestration/v1/software_deployment.py b/openstack/orchestration/v1/software_deployment.py index 2c091b342..9a2cbf6d8 100644 --- a/openstack/orchestration/v1/software_deployment.py +++ b/openstack/orchestration/v1/software_deployment.py @@ -53,10 +53,12 @@ def create(self, session, base_path=None): # This overrides the default behavior of resource creation because # heat doesn't accept resource_key in its request. return super(SoftwareDeployment, self).create( - session, prepend_key=False, base_path=base_path) + session, prepend_key=False, base_path=base_path + ) def commit(self, session, base_path=None): # This overrides the default behavior of resource creation because # heat doesn't accept resource_key in its request. return super(SoftwareDeployment, self).commit( - session, prepend_key=False, base_path=base_path) + session, prepend_key=False, base_path=base_path + ) diff --git a/openstack/orchestration/v1/stack.py b/openstack/orchestration/v1/stack.py index 511df404d..ef27225aa 100644 --- a/openstack/orchestration/v1/stack.py +++ b/openstack/orchestration/v1/stack.py @@ -29,8 +29,12 @@ class Stack(resource.Resource): allow_delete = True _query_mapping = resource.QueryParameters( - 'action', 'name', 'status', - 'project_id', 'owner_id', 'username', + 'action', + 'name', + 'status', + 'project_id', + 'owner_id', + 'username', project_id='tenant_id', **tag.TagMixin._tag_query_parameters ) @@ -111,14 +115,16 @@ class Stack(resource.Resource): def create(self, session, base_path=None): # This overrides the default behavior of resource creation because # heat doesn't accept resource_key in its request. - return super(Stack, self).create(session, prepend_key=False, - base_path=base_path) + return super(Stack, self).create( + session, prepend_key=False, base_path=base_path + ) def commit(self, session, base_path=None): # This overrides the default behavior of resource creation because # heat doesn't accept resource_key in its request. - return super(Stack, self).commit(session, prepend_key=False, - has_body=False, base_path=None) + return super(Stack, self).commit( + session, prepend_key=False, has_body=False, base_path=None + ) def update(self, session, preview=False): # This overrides the default behavior of resource update because @@ -127,16 +133,17 @@ def update(self, session, preview=False): if self.name and self.id: base_path = '/stacks/%(stack_name)s/%(stack_id)s' % { 'stack_name': self.name, - 'stack_id': self.id} + 'stack_id': self.id, + } elif self.name or self.id: # We have only one of name/id. Do not try to build a stacks/NAME/ID # path base_path = '/stacks/%(stack_identity)s' % { - 'stack_identity': self.name or self.id} + 'stack_identity': self.name or self.id + } request = self._prepare_request( - prepend_key=False, - requires_id=False, - base_path=base_path) + prepend_key=False, requires_id=False, base_path=base_path + ) microversion = self._get_microversion(session, action='commit') @@ -145,8 +152,11 @@ def update(self, session, preview=False): request_url = utils.urljoin(request_url, 'preview') response = session.put( - request_url, json=request.body, headers=request.headers, - microversion=microversion) + request_url, + json=request.body, + headers=request.headers, + microversion=microversion, + ) self.microversion = microversion self._translate_response(response, has_body=True) @@ -162,20 +172,28 @@ def check(self, session): return self._action(session, {'check': ''}) def abandon(self, session): - url = utils.urljoin(self.base_path, self.name, - self._get_id(self), 'abandon') + url = utils.urljoin( + self.base_path, self.name, self._get_id(self), 'abandon' + ) resp = session.delete(url) return resp.json() - def fetch(self, session, requires_id=True, - base_path=None, error_message=None, - skip_cache=False, resolve_outputs=True): + def fetch( + self, + session, + requires_id=True, + base_path=None, + error_message=None, + skip_cache=False, + resolve_outputs=True, + ): if not self.allow_fetch: raise exceptions.MethodNotSupported(self, "fetch") - request = self._prepare_request(requires_id=requires_id, - base_path=base_path) + request = self._prepare_request( + requires_id=requires_id, base_path=base_path + ) # session = self._get_session(session) microversion = self._get_microversion(session, action='fetch') @@ -185,7 +203,8 @@ def fetch(self, session, requires_id=True, if not resolve_outputs: request.url = request.url + '?resolve_outputs=False' response = session.get( - request.url, microversion=microversion, skip_cache=skip_cache) + request.url, microversion=microversion, skip_cache=skip_cache + ) kwargs = {} if error_message: kwargs['error_message'] = error_message @@ -195,7 +214,8 @@ def fetch(self, session, requires_id=True, if self and self.status in ['DELETE_COMPLETE', 'ADOPT_COMPLETE']: raise exceptions.ResourceNotFound( - "No stack found for %s" % self.id) + "No stack found for %s" % self.id + ) return self @classmethod @@ -227,9 +247,8 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): # Try to short-circuit by looking directly for a matching ID. try: match = cls.existing( - id=name_or_id, - connection=session._get_connection(), - **params) + id=name_or_id, connection=session._get_connection(), **params + ) return match.fetch(session, **params) except exceptions.NotFoundException: pass @@ -240,7 +259,8 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): if ignore_missing: return None raise exceptions.ResourceNotFound( - "No %s found for %s" % (cls.__name__, name_or_id)) + "No %s found for %s" % (cls.__name__, name_or_id) + ) StackPreview = Stack diff --git a/openstack/orchestration/v1/template.py b/openstack/orchestration/v1/template.py index 818e73992..64ec8aa27 100644 --- a/openstack/orchestration/v1/template.py +++ b/openstack/orchestration/v1/template.py @@ -32,8 +32,14 @@ class Template(resource.Resource): #: A list of parameter groups each contains a lsit of parameter names. parameter_groups = resource.Body('ParameterGroups', type=list) - def validate(self, session, template, environment=None, template_url=None, - ignore_errors=None): + def validate( + self, + session, + template, + environment=None, + template_url=None, + ignore_errors=None, + ): url = '/validate' body = {'template': template} diff --git a/openstack/tests/functional/orchestration/v1/test_stack.py b/openstack/tests/functional/orchestration/v1/test_stack.py index afc08c523..9b5b14f04 100644 --- a/openstack/tests/functional/orchestration/v1/test_stack.py +++ b/openstack/tests/functional/orchestration/v1/test_stack.py @@ -42,9 +42,8 @@ def setUp(self): # the shade layer. template['heat_template_version'] = '2013-05-23' self.network, self.subnet = test_network.create_network( - self.conn, - self.NAME, - self.cidr) + self.conn, self.NAME, self.cidr + ) parameters = { 'image': image.id, 'key_name': self.NAME, @@ -60,8 +59,11 @@ def setUp(self): self.stack = sot self.assertEqual(self.NAME, sot.name) self.conn.orchestration.wait_for_status( - sot, status='CREATE_COMPLETE', failures=['CREATE_FAILED'], - wait=self._wait_for_timeout) + sot, + status='CREATE_COMPLETE', + failures=['CREATE_FAILED'], + wait=self._wait_for_timeout, + ) def tearDown(self): self.conn.orchestration.delete_stack(self.stack, ignore_missing=False) @@ -69,7 +71,8 @@ def tearDown(self): # Need to wait for the stack to go away before network delete try: self.conn.orchestration.wait_for_status( - self.stack, 'DELETE_COMPLETE', wait=self._wait_for_timeout) + self.stack, 'DELETE_COMPLETE', wait=self._wait_for_timeout + ) except exceptions.ResourceNotFound: pass test_network.delete_network(self.conn, self.network, self.subnet) diff --git a/openstack/tests/unit/orchestration/test_version.py b/openstack/tests/unit/orchestration/test_version.py index 20fceb315..a8b91383e 100644 --- a/openstack/tests/unit/orchestration/test_version.py +++ b/openstack/tests/unit/orchestration/test_version.py @@ -23,7 +23,6 @@ class TestVersion(base.TestCase): - def test_basic(self): sot = version.Version() self.assertEqual('version', sot.resource_key) diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index 76406d5ba..bd1b212ec 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -42,11 +42,15 @@ def test_create_stack_preview(self): self.proxy.create_stack, stack.Stack, method_kwargs={"preview": True, "x": 1, "y": 2, "z": 3}, - expected_kwargs={"x": 1, "y": 2, "z": 3}) + expected_kwargs={"x": 1, "y": 2, "z": 3}, + ) def test_find_stack(self): - self.verify_find(self.proxy.find_stack, stack.Stack, - expected_kwargs={'resolve_outputs': True}) + self.verify_find( + self.proxy.find_stack, + stack.Stack, + expected_kwargs={'resolve_outputs': True}, + ) # mock_method="openstack.proxy.Proxy._find" # test_method=self.proxy.find_stack # method_kwargs = { @@ -78,12 +82,15 @@ def test_stacks(self): self.verify_list(self.proxy.stacks, stack.Stack) def test_get_stack(self): - self.verify_get(self.proxy.get_stack, stack.Stack, - method_kwargs={'resolve_outputs': False}, - expected_kwargs={'resolve_outputs': False}) + self.verify_get( + self.proxy.get_stack, + stack.Stack, + method_kwargs={'resolve_outputs': False}, + expected_kwargs={'resolve_outputs': False}, + ) self.verify_get_overrided( - self.proxy, stack.Stack, - 'openstack.orchestration.v1.stack.Stack') + self.proxy, stack.Stack, 'openstack.orchestration.v1.stack.Stack' + ) def test_update_stack(self): self._verify( @@ -92,7 +99,8 @@ def test_update_stack(self): expected_result='result', method_args=['stack'], method_kwargs={'preview': False}, - expected_args=[self.proxy, False]) + expected_args=[self.proxy, False], + ) def test_update_stack_preview(self): self._verify( @@ -101,7 +109,8 @@ def test_update_stack_preview(self): expected_result='result', method_args=['stack'], method_kwargs={'preview': True}, - expected_args=[self.proxy, True]) + expected_args=[self.proxy, True], + ) def test_abandon_stack(self): self._verify( @@ -109,7 +118,8 @@ def test_abandon_stack(self): self.proxy.abandon_stack, expected_result='result', method_args=['stack'], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_delete_stack(self): self.verify_delete(self.proxy.delete_stack, stack.Stack, False) @@ -154,9 +164,12 @@ def test_get_stack_environment_with_stack_identity(self, mock_find): expected_kwargs={ 'requires_id': False, 'stack_name': stack_name, - 'stack_id': stack_id}) - mock_find.assert_called_once_with(mock.ANY, 'IDENTITY', - ignore_missing=False) + 'stack_id': stack_id, + }, + ) + mock_find.assert_called_once_with( + mock.ANY, 'IDENTITY', ignore_missing=False + ) def test_get_stack_environment_with_stack_object(self): stack_id = '1234' @@ -171,7 +184,9 @@ def test_get_stack_environment_with_stack_object(self): expected_kwargs={ 'requires_id': False, 'stack_name': stack_name, - 'stack_id': stack_id}) + 'stack_id': stack_id, + }, + ) class TestOrchestrationStackFiles(TestOrchestrationProxy): @@ -187,8 +202,9 @@ def test_get_stack_files_with_stack_identity(self, mock_find, mock_fetch): res = self.proxy.get_stack_files('IDENTITY') self.assertEqual({'file': 'content'}, res) - mock_find.assert_called_once_with(mock.ANY, 'IDENTITY', - ignore_missing=False) + mock_find.assert_called_once_with( + mock.ANY, 'IDENTITY', ignore_missing=False + ) mock_fetch.assert_called_once_with(self.proxy) @mock.patch.object(stack_files.StackFiles, 'fetch') @@ -220,9 +236,12 @@ def test_get_stack_template_with_stack_identity(self, mock_find): expected_kwargs={ 'requires_id': False, 'stack_name': stack_name, - 'stack_id': stack_id}) - mock_find.assert_called_once_with(mock.ANY, 'IDENTITY', - ignore_missing=False) + 'stack_id': stack_id, + }, + ) + mock_find.assert_called_once_with( + mock.ANY, 'IDENTITY', ignore_missing=False + ) def test_get_stack_template_with_stack_object(self): stack_id = '1234' @@ -237,7 +256,9 @@ def test_get_stack_template_with_stack_object(self): expected_kwargs={ 'requires_id': False, 'stack_name': stack_name, - 'stack_id': stack_id}) + 'stack_id': stack_id, + }, + ) class TestOrchestrationResource(TestOrchestrationProxy): @@ -247,11 +268,13 @@ def test_resources_with_stack_object(self, mock_find): stack_name = 'test_stack' stk = stack.Stack(id=stack_id, name=stack_name) - self.verify_list(self.proxy.resources, resource.Resource, - method_args=[stk], - expected_args=[], - expected_kwargs={'stack_name': stack_name, - 'stack_id': stack_id}) + self.verify_list( + self.proxy.resources, + resource.Resource, + method_args=[stk], + expected_args=[], + expected_kwargs={'stack_name': stack_name, 'stack_id': stack_id}, + ) self.assertEqual(0, mock_find.call_count) @@ -262,31 +285,37 @@ def test_resources_with_stack_name(self, mock_find): stk = stack.Stack(id=stack_id, name=stack_name) mock_find.return_value = stk - self.verify_list(self.proxy.resources, resource.Resource, - method_args=[stack_id], - expected_args=[], - expected_kwargs={'stack_name': stack_name, - 'stack_id': stack_id}) + self.verify_list( + self.proxy.resources, + resource.Resource, + method_args=[stack_id], + expected_args=[], + expected_kwargs={'stack_name': stack_name, 'stack_id': stack_id}, + ) - mock_find.assert_called_once_with(mock.ANY, stack_id, - ignore_missing=False) + mock_find.assert_called_once_with( + mock.ANY, stack_id, ignore_missing=False + ) @mock.patch.object(stack.Stack, 'find') @mock.patch.object(resource.Resource, 'list') def test_resources_stack_not_found(self, mock_list, mock_find): stack_name = 'test_stack' mock_find.side_effect = exceptions.ResourceNotFound( - 'No stack found for test_stack') + 'No stack found for test_stack' + ) - ex = self.assertRaises(exceptions.ResourceNotFound, - self.proxy.resources, stack_name) + ex = self.assertRaises( + exceptions.ResourceNotFound, self.proxy.resources, stack_name + ) self.assertEqual('No stack found for test_stack', str(ex)) class TestOrchestrationSoftwareConfig(TestOrchestrationProxy): def test_create_software_config(self): - self.verify_create(self.proxy.create_software_config, - sc.SoftwareConfig) + self.verify_create( + self.proxy.create_software_config, sc.SoftwareConfig + ) def test_software_configs(self): self.verify_list(self.proxy.software_configs, sc.SoftwareConfig) @@ -295,34 +324,42 @@ def test_get_software_config(self): self.verify_get(self.proxy.get_software_config, sc.SoftwareConfig) def test_delete_software_config(self): - self.verify_delete(self.proxy.delete_software_config, - sc.SoftwareConfig, True) - self.verify_delete(self.proxy.delete_software_config, - sc.SoftwareConfig, False) + self.verify_delete( + self.proxy.delete_software_config, sc.SoftwareConfig, True + ) + self.verify_delete( + self.proxy.delete_software_config, sc.SoftwareConfig, False + ) class TestOrchestrationSoftwareDeployment(TestOrchestrationProxy): def test_create_software_deployment(self): - self.verify_create(self.proxy.create_software_deployment, - sd.SoftwareDeployment) + self.verify_create( + self.proxy.create_software_deployment, sd.SoftwareDeployment + ) def test_software_deployments(self): - self.verify_list(self.proxy.software_deployments, - sd.SoftwareDeployment) + self.verify_list( + self.proxy.software_deployments, sd.SoftwareDeployment + ) def test_get_software_deployment(self): - self.verify_get(self.proxy.get_software_deployment, - sd.SoftwareDeployment) + self.verify_get( + self.proxy.get_software_deployment, sd.SoftwareDeployment + ) def test_update_software_deployment(self): - self.verify_update(self.proxy.update_software_deployment, - sd.SoftwareDeployment) + self.verify_update( + self.proxy.update_software_deployment, sd.SoftwareDeployment + ) def test_delete_software_deployment(self): - self.verify_delete(self.proxy.delete_software_deployment, - sd.SoftwareDeployment, True) - self.verify_delete(self.proxy.delete_software_deployment, - sd.SoftwareDeployment, False) + self.verify_delete( + self.proxy.delete_software_deployment, sd.SoftwareDeployment, True + ) + self.verify_delete( + self.proxy.delete_software_deployment, sd.SoftwareDeployment, False + ) class TestOrchestrationTemplate(TestOrchestrationProxy): @@ -336,8 +373,12 @@ def test_validate_template(self, mock_validate): res = self.proxy.validate_template(tmpl, env, tmpl_url, ignore_errors) mock_validate.assert_called_once_with( - self.proxy, tmpl, environment=env, template_url=tmpl_url, - ignore_errors=ignore_errors) + self.proxy, + tmpl, + environment=env, + template_url=tmpl_url, + ignore_errors=ignore_errors, + ) self.assertEqual(mock_validate.return_value, res) def test_validate_template_no_env(self): @@ -349,11 +390,16 @@ def test_validate_template_no_env(self): self.assertIsInstance(res["files"], dict) def test_validate_template_invalid_request(self): - err = self.assertRaises(exceptions.InvalidRequest, - self.proxy.validate_template, - None, template_url=None) - self.assertEqual("'template_url' must be specified when template is " - "None", str(err)) + err = self.assertRaises( + exceptions.InvalidRequest, + self.proxy.validate_template, + None, + template_url=None, + ) + self.assertEqual( + "'template_url' must be specified when template is " "None", + str(err), + ) class TestExtractName(TestOrchestrationProxy): @@ -362,22 +408,47 @@ class TestExtractName(TestOrchestrationProxy): ('stacks', dict(url='/stacks', parts=['stacks'])), ('name_id', dict(url='/stacks/name/id', parts=['stack'])), ('identity', dict(url='/stacks/id', parts=['stack'])), - ('preview', dict(url='/stacks/name/preview', - parts=['stack', 'preview'])), - ('stack_act', dict(url='/stacks/name/id/preview', - parts=['stack', 'preview'])), - ('stack_subres', dict(url='/stacks/name/id/resources', - parts=['stack', 'resources'])), - ('stack_subres_id', dict(url='/stacks/name/id/resources/id', - parts=['stack', 'resource'])), - ('stack_subres_id_act', - dict(url='/stacks/name/id/resources/id/action', - parts=['stack', 'resource', 'action'])), - ('event', - dict(url='/stacks/ignore/ignore/resources/ignore/events/id', - parts=['stack', 'resource', 'event'])), - ('sd_metadata', dict(url='/software_deployments/metadata/ignore', - parts=['software_deployment', 'metadata'])) + ( + 'preview', + dict(url='/stacks/name/preview', parts=['stack', 'preview']), + ), + ( + 'stack_act', + dict(url='/stacks/name/id/preview', parts=['stack', 'preview']), + ), + ( + 'stack_subres', + dict( + url='/stacks/name/id/resources', parts=['stack', 'resources'] + ), + ), + ( + 'stack_subres_id', + dict( + url='/stacks/name/id/resources/id', parts=['stack', 'resource'] + ), + ), + ( + 'stack_subres_id_act', + dict( + url='/stacks/name/id/resources/id/action', + parts=['stack', 'resource', 'action'], + ), + ), + ( + 'event', + dict( + url='/stacks/ignore/ignore/resources/ignore/events/id', + parts=['stack', 'resource', 'event'], + ), + ), + ( + 'sd_metadata', + dict( + url='/software_deployments/metadata/ignore', + parts=['software_deployment', 'metadata'], + ), + ), ] def test_extract_name(self): diff --git a/openstack/tests/unit/orchestration/v1/test_resource.py b/openstack/tests/unit/orchestration/v1/test_resource.py index 2e3708a15..964d8b9d8 100644 --- a/openstack/tests/unit/orchestration/v1/test_resource.py +++ b/openstack/tests/unit/orchestration/v1/test_resource.py @@ -17,13 +17,10 @@ FAKE_ID = '32e39358-2422-4ad0-a1b5-dd60696bf564' FAKE_NAME = 'test_stack' FAKE = { - 'links': [{ - 'href': 'http://res_link', - 'rel': 'self' - }, { - 'href': 'http://stack_link', - 'rel': 'stack' - }], + 'links': [ + {'href': 'http://res_link', 'rel': 'self'}, + {'href': 'http://stack_link', 'rel': 'stack'}, + ], 'logical_resource_id': 'the_resource', 'name': 'the_resource', 'physical_resource_id': '9f38ab5a-37c8-4e40-9702-ce27fc5f6954', @@ -36,13 +33,13 @@ class TestResource(base.TestCase): - def test_basic(self): sot = resource.Resource() self.assertEqual('resource', sot.resource_key) self.assertEqual('resources', sot.resources_key) - self.assertEqual('/stacks/%(stack_name)s/%(stack_id)s/resources', - sot.base_path) + self.assertEqual( + '/stacks/%(stack_name)s/%(stack_id)s/resources', sot.base_path + ) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_retrieve) self.assertFalse(sot.allow_commit) @@ -54,8 +51,9 @@ def test_make_it(self): self.assertEqual(FAKE['links'], sot.links) self.assertEqual(FAKE['logical_resource_id'], sot.logical_resource_id) self.assertEqual(FAKE['name'], sot.name) - self.assertEqual(FAKE['physical_resource_id'], - sot.physical_resource_id) + self.assertEqual( + FAKE['physical_resource_id'], sot.physical_resource_id + ) self.assertEqual(FAKE['required_by'], sot.required_by) self.assertEqual(FAKE['resource_type'], sot.resource_type) self.assertEqual(FAKE['status'], sot.status) diff --git a/openstack/tests/unit/orchestration/v1/test_software_config.py b/openstack/tests/unit/orchestration/v1/test_software_config.py index 439b2e100..3b4aa3fd2 100644 --- a/openstack/tests/unit/orchestration/v1/test_software_config.py +++ b/openstack/tests/unit/orchestration/v1/test_software_config.py @@ -29,7 +29,6 @@ class TestSoftwareConfig(base.TestCase): - def test_basic(self): sot = software_config.SoftwareConfig() self.assertEqual('software_config', sot.resource_key) diff --git a/openstack/tests/unit/orchestration/v1/test_software_deployment.py b/openstack/tests/unit/orchestration/v1/test_software_deployment.py index 7c9640e79..8a67105e5 100644 --- a/openstack/tests/unit/orchestration/v1/test_software_deployment.py +++ b/openstack/tests/unit/orchestration/v1/test_software_deployment.py @@ -30,7 +30,6 @@ class TestSoftwareDeployment(base.TestCase): - def test_basic(self): sot = software_deployment.SoftwareDeployment() self.assertEqual('software_deployment', sot.resource_key) @@ -49,8 +48,9 @@ def test_make_it(self): self.assertEqual(FAKE['config_id'], sot.config_id) self.assertEqual(FAKE['creation_time'], sot.created_at) self.assertEqual(FAKE['server_id'], sot.server_id) - self.assertEqual(FAKE['stack_user_project_id'], - sot.stack_user_project_id) + self.assertEqual( + FAKE['stack_user_project_id'], sot.stack_user_project_id + ) self.assertEqual(FAKE['input_values'], sot.input_values) self.assertEqual(FAKE['output_values'], sot.output_values) self.assertEqual(FAKE['status'], sot.status) diff --git a/openstack/tests/unit/orchestration/v1/test_stack.py b/openstack/tests/unit/orchestration/v1/test_stack.py index 06912fe09..3503ff00b 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack.py +++ b/openstack/tests/unit/orchestration/v1/test_stack.py @@ -31,9 +31,7 @@ 'files': {'file1': 'content'}, 'files_container': 'dummy_container', 'id': FAKE_ID, - 'links': [{ - 'href': 'stacks/%s/%s' % (FAKE_NAME, FAKE_ID), - 'rel': 'self'}], + 'links': [{'href': 'stacks/%s/%s' % (FAKE_NAME, FAKE_ID), 'rel': 'self'}], 'notification_topics': '7', 'outputs': '8', 'parameters': {'OS::stack_id': '9'}, @@ -49,81 +47,81 @@ FAKE_CREATE_RESPONSE = { 'stack': { 'id': FAKE_ID, - 'links': [{ - 'href': 'stacks/%s/%s' % (FAKE_NAME, FAKE_ID), - 'rel': 'self'}]} + 'links': [ + {'href': 'stacks/%s/%s' % (FAKE_NAME, FAKE_ID), 'rel': 'self'} + ], + } } FAKE_UPDATE_PREVIEW_RESPONSE = { 'unchanged': [ { 'updated_time': 'datetime', 'resource_name': '', - 'physical_resource_id': '{resource id or ''}', + 'physical_resource_id': '{resource id or ' '}', 'resource_action': 'CREATE', 'resource_status': 'COMPLETE', 'resource_status_reason': '', 'resource_type': 'restype', 'stack_identity': '{stack_id}', - 'stack_name': '{stack_name}' + 'stack_name': '{stack_name}', } ], 'updated': [ { 'updated_time': 'datetime', 'resource_name': '', - 'physical_resource_id': '{resource id or ''}', + 'physical_resource_id': '{resource id or ' '}', 'resource_action': 'CREATE', 'resource_status': 'COMPLETE', 'resource_status_reason': '', 'resource_type': 'restype', 'stack_identity': '{stack_id}', - 'stack_name': '{stack_name}' + 'stack_name': '{stack_name}', } ], 'replaced': [ { 'updated_time': 'datetime', 'resource_name': '', - 'physical_resource_id': '{resource id or ''}', + 'physical_resource_id': '{resource id or ' '}', 'resource_action': 'CREATE', 'resource_status': 'COMPLETE', 'resource_status_reason': '', 'resource_type': 'restype', 'stack_identity': '{stack_id}', - 'stack_name': '{stack_name}' + 'stack_name': '{stack_name}', } ], 'added': [ { 'updated_time': 'datetime', 'resource_name': '', - 'physical_resource_id': '{resource id or ''}', + 'physical_resource_id': '{resource id or ' '}', 'resource_action': 'CREATE', 'resource_status': 'COMPLETE', 'resource_status_reason': '', 'resource_type': 'restype', 'stack_identity': '{stack_id}', - 'stack_name': '{stack_name}' + 'stack_name': '{stack_name}', } ], 'deleted': [ { 'updated_time': 'datetime', 'resource_name': '', - 'physical_resource_id': '{resource id or ''}', + 'physical_resource_id': '{resource id or ' '}', 'resource_action': 'CREATE', 'resource_status': 'COMPLETE', 'resource_status_reason': '', 'resource_type': 'restype', 'stack_identity': '{stack_id}', - 'stack_name': '{stack_name}' + 'stack_name': '{stack_name}', } - ] + ], } class TestStack(base.TestCase): - def test_basic(self): sot = stack.Stack() self.assertEqual('stack', sot.resource_key) @@ -150,7 +148,8 @@ def test_basic(self): 'tags': 'tags', 'username': 'username', }, - sot._query_mapping._mapping) + sot._query_mapping._mapping, + ) def test_make_it(self): sot = stack.Stack(**FAKE) @@ -165,16 +164,16 @@ def test_make_it(self): self.assertTrue(sot.is_rollback_disabled) self.assertEqual(FAKE['id'], sot.id) self.assertEqual(FAKE['links'], sot.links) - self.assertEqual(FAKE['notification_topics'], - sot.notification_topics) + self.assertEqual(FAKE['notification_topics'], sot.notification_topics) self.assertEqual(FAKE['outputs'], sot.outputs) self.assertEqual(FAKE['parameters'], sot.parameters) self.assertEqual(FAKE['name'], sot.name) self.assertEqual(FAKE['status'], sot.status) self.assertEqual(FAKE['status_reason'], sot.status_reason) self.assertEqual(FAKE['tags'], sot.tags) - self.assertEqual(FAKE['template_description'], - sot.template_description) + self.assertEqual( + FAKE['template_description'], sot.template_description + ) self.assertEqual(FAKE['template_url'], sot.template_url) self.assertEqual(FAKE['timeout_mins'], sot.timeout_mins) self.assertEqual(FAKE['updated_time'], sot.updated_at) @@ -186,8 +185,9 @@ def test_create(self, mock_create): res = sot.create(sess) - mock_create.assert_called_once_with(sess, prepend_key=False, - base_path=None) + mock_create.assert_called_once_with( + sess, prepend_key=False, base_path=None + ) self.assertEqual(mock_create.return_value, res) @mock.patch.object(resource.Resource, 'commit') @@ -197,9 +197,9 @@ def test_commit(self, mock_commit): res = sot.commit(sess) - mock_commit.assert_called_once_with(sess, prepend_key=False, - has_body=False, - base_path=None) + mock_commit.assert_called_once_with( + sess, prepend_key=False, has_body=False, base_path=None + ) self.assertEqual(mock_commit.return_value, res) def test_check(self): @@ -221,29 +221,33 @@ def test_fetch(self): sess.get = mock.Mock() sess.get.side_effect = [ test_resource.FakeResponse( - {'stack': {'stack_status': 'CREATE_COMPLETE'}}, 200), + {'stack': {'stack_status': 'CREATE_COMPLETE'}}, 200 + ), test_resource.FakeResponse( - {'stack': {'stack_status': 'CREATE_COMPLETE'}}, 200), + {'stack': {'stack_status': 'CREATE_COMPLETE'}}, 200 + ), exceptions.ResourceNotFound(message='oops'), test_resource.FakeResponse( - {'stack': {'stack_status': 'DELETE_COMPLETE'}}, 200) + {'stack': {'stack_status': 'DELETE_COMPLETE'}}, 200 + ), ] self.assertEqual(sot, sot.fetch(sess)) sess.get.assert_called_with( 'stacks/{id}'.format(id=sot.id), microversion=None, - skip_cache=False) + skip_cache=False, + ) sot.fetch(sess, resolve_outputs=False) sess.get.assert_called_with( 'stacks/{id}?resolve_outputs=False'.format(id=sot.id), microversion=None, - skip_cache=False) + skip_cache=False, + ) ex = self.assertRaises(exceptions.ResourceNotFound, sot.fetch, sess) self.assertEqual('oops', str(ex)) ex = self.assertRaises(exceptions.ResourceNotFound, sot.fetch, sess) - self.assertEqual('No stack found for %s' % FAKE_ID, - str(ex)) + self.assertEqual('No stack found for %s' % FAKE_ID, str(ex)) def test_abandon(self): sess = mock.Mock() @@ -260,7 +264,6 @@ def test_abandon(self): sess.delete.assert_called_with( 'stacks/%s/%s/abandon' % (FAKE_NAME, FAKE_ID), - ) def test_update(self): @@ -281,7 +284,7 @@ def test_update(self): '/stacks/%s/%s' % (FAKE_NAME, FAKE_ID), headers={}, microversion=None, - json=body + json=body, ) def test_update_preview(self): @@ -302,21 +305,15 @@ def test_update_preview(self): 'stacks/%s/%s/preview' % (FAKE_NAME, FAKE_ID), headers={}, microversion=None, - json=body + json=body, ) + self.assertEqual(FAKE_UPDATE_PREVIEW_RESPONSE['added'], ret.added) + self.assertEqual(FAKE_UPDATE_PREVIEW_RESPONSE['deleted'], ret.deleted) self.assertEqual( - FAKE_UPDATE_PREVIEW_RESPONSE['added'], - ret.added) - self.assertEqual( - FAKE_UPDATE_PREVIEW_RESPONSE['deleted'], - ret.deleted) - self.assertEqual( - FAKE_UPDATE_PREVIEW_RESPONSE['replaced'], - ret.replaced) - self.assertEqual( - FAKE_UPDATE_PREVIEW_RESPONSE['unchanged'], - ret.unchanged) + FAKE_UPDATE_PREVIEW_RESPONSE['replaced'], ret.replaced + ) self.assertEqual( - FAKE_UPDATE_PREVIEW_RESPONSE['updated'], - ret.updated) + FAKE_UPDATE_PREVIEW_RESPONSE['unchanged'], ret.unchanged + ) + self.assertEqual(FAKE_UPDATE_PREVIEW_RESPONSE['updated'], ret.updated) diff --git a/openstack/tests/unit/orchestration/v1/test_stack_environment.py b/openstack/tests/unit/orchestration/v1/test_stack_environment.py index 03c39f3fc..5c7291f9f 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack_environment.py +++ b/openstack/tests/unit/orchestration/v1/test_stack_environment.py @@ -16,27 +16,14 @@ FAKE = { 'encrypted_param_names': ['n1', 'n2'], - 'event_sinks': { - 's1': 'v1' - }, - 'parameters': { - 'key_name': { - 'type': 'string' - } - }, - 'parameter_defaults': { - 'p1': 'def1' - }, - 'resource_registry': { - 'resources': { - 'type1': 'type2' - } - }, + 'event_sinks': {'s1': 'v1'}, + 'parameters': {'key_name': {'type': 'string'}}, + 'parameter_defaults': {'p1': 'def1'}, + 'resource_registry': {'resources': {'type1': 'type2'}}, } class TestStackTemplate(base.TestCase): - def test_basic(self): sot = se.StackEnvironment() self.assertFalse(sot.allow_create) @@ -47,8 +34,9 @@ def test_basic(self): def test_make_it(self): sot = se.StackEnvironment(**FAKE) - self.assertEqual(FAKE['encrypted_param_names'], - sot.encrypted_param_names) + self.assertEqual( + FAKE['encrypted_param_names'], sot.encrypted_param_names + ) self.assertEqual(FAKE['event_sinks'], sot.event_sinks) self.assertEqual(FAKE['parameters'], sot.parameters) self.assertEqual(FAKE['parameter_defaults'], sot.parameter_defaults) diff --git a/openstack/tests/unit/orchestration/v1/test_stack_files.py b/openstack/tests/unit/orchestration/v1/test_stack_files.py index 7f510b7b9..1e6e5d729 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack_files.py +++ b/openstack/tests/unit/orchestration/v1/test_stack_files.py @@ -16,14 +16,10 @@ from openstack import resource from openstack.tests.unit import base -FAKE = { - 'stack_id': 'ID', - 'stack_name': 'NAME' -} +FAKE = {'stack_id': 'ID', 'stack_name': 'NAME'} class TestStackFiles(base.TestCase): - def test_basic(self): sot = sf.StackFiles() self.assertFalse(sot.allow_create) @@ -48,9 +44,10 @@ def test_get(self, mock_prepare_request): sot = sf.StackFiles(**FAKE) req = mock.MagicMock() - req.url = ('/stacks/%(stack_name)s/%(stack_id)s/files' % - {'stack_name': FAKE['stack_name'], - 'stack_id': FAKE['stack_id']}) + req.url = '/stacks/%(stack_name)s/%(stack_id)s/files' % { + 'stack_name': FAKE['stack_name'], + 'stack_id': FAKE['stack_id'], + } mock_prepare_request.return_value = req files = sot.fetch(sess) diff --git a/openstack/tests/unit/orchestration/v1/test_stack_template.py b/openstack/tests/unit/orchestration/v1/test_stack_template.py index 60ed9d1a5..1a9ab8e94 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack_template.py +++ b/openstack/tests/unit/orchestration/v1/test_stack_template.py @@ -19,25 +19,14 @@ FAKE = { 'description': 'template description', 'heat_template_version': '2014-10-16', - 'parameters': { - 'key_name': { - 'type': 'string' - } - }, - 'resources': { - 'resource1': { - 'type': 'ResourceType' - } - }, + 'parameters': {'key_name': {'type': 'string'}}, + 'resources': {'resource1': {'type': 'ResourceType'}}, 'conditions': {'cd1': True}, - 'outputs': { - 'key1': 'value1' - } + 'outputs': {'key1': 'value1'}, } class TestStackTemplate(base.TestCase): - def test_basic(self): sot = stack_template.StackTemplate() self.assertFalse(sot.allow_create) @@ -49,8 +38,9 @@ def test_basic(self): def test_make_it(self): sot = stack_template.StackTemplate(**FAKE) self.assertEqual(FAKE['description'], sot.description) - self.assertEqual(FAKE['heat_template_version'], - sot.heat_template_version) + self.assertEqual( + FAKE['heat_template_version'], sot.heat_template_version + ) self.assertEqual(FAKE['outputs'], sot.outputs) self.assertEqual(FAKE['parameters'], sot.parameters) self.assertEqual(FAKE['resources'], sot.resources) @@ -58,17 +48,28 @@ def test_make_it(self): def test_to_dict(self): fake_sot = copy.deepcopy(FAKE) - fake_sot['parameter_groups'] = [{ - "description": "server parameters", - "parameters": ["key_name", "image_id"], - "label": "server_parameters"}] + fake_sot['parameter_groups'] = [ + { + "description": "server parameters", + "parameters": ["key_name", "image_id"], + "label": "server_parameters", + } + ] fake_sot['location'] = None fake_sot['id'] = None fake_sot['name'] = None - for temp_version in ['2016-10-14', '2017-02-24', '2017-02-24', - '2017-09-01', '2018-03-02', 'newton', - 'ocata', 'pike', 'queens']: + for temp_version in [ + '2016-10-14', + '2017-02-24', + '2017-02-24', + '2017-09-01', + '2018-03-02', + 'newton', + 'ocata', + 'pike', + 'queens', + ]: fake_sot['heat_template_version'] = temp_version sot = stack_template.StackTemplate(**fake_sot) self.assertEqual(fake_sot, sot.to_dict()) diff --git a/openstack/tests/unit/orchestration/v1/test_template.py b/openstack/tests/unit/orchestration/v1/test_template.py index 0714c3518..27bb18376 100644 --- a/openstack/tests/unit/orchestration/v1/test_template.py +++ b/openstack/tests/unit/orchestration/v1/test_template.py @@ -18,20 +18,12 @@ FAKE = { 'Description': 'Blah blah', - 'Parameters': { - 'key_name': { - 'type': 'string' - } - }, - 'ParameterGroups': [{ - 'label': 'Group 1', - 'parameters': ['key_name'] - }] + 'Parameters': {'key_name': {'type': 'string'}}, + 'ParameterGroups': [{'label': 'Group 1', 'parameters': ['key_name']}], } class TestTemplate(base.TestCase): - def test_basic(self): sot = template.Template() self.assertFalse(sot.allow_create) @@ -55,8 +47,7 @@ def test_validate(self, mock_translate): sot.validate(sess, tmpl) - sess.post.assert_called_once_with( - '/validate', json=body) + sess.post.assert_called_once_with('/validate', json=body) mock_translate.assert_called_once_with(sess.post.return_value) @mock.patch.object(resource.Resource, '_translate_response') @@ -69,8 +60,7 @@ def test_validate_with_env(self, mock_translate): sot.validate(sess, tmpl, environment=env) - sess.post.assert_called_once_with( - '/validate', json=body) + sess.post.assert_called_once_with('/validate', json=body) mock_translate.assert_called_once_with(sess.post.return_value) @mock.patch.object(resource.Resource, '_translate_response') @@ -82,8 +72,7 @@ def test_validate_with_template_url(self, mock_translate): sot.validate(sess, None, template_url=template_url) - sess.post.assert_called_once_with( - '/validate', json=body) + sess.post.assert_called_once_with('/validate', json=body) mock_translate.assert_called_once_with(sess.post.return_value) @mock.patch.object(resource.Resource, '_translate_response') @@ -96,6 +85,6 @@ def test_validate_with_ignore_errors(self, mock_translate): sot.validate(sess, tmpl, ignore_errors='123,456') sess.post.assert_called_once_with( - '/validate?ignore_errors=123%2C456', - json=body) + '/validate?ignore_errors=123%2C456', json=body + ) mock_translate.assert_called_once_with(sess.post.return_value) From 9d3d986241ce110e8f6bdf3ecb19609dc417a10a Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 5 May 2023 11:02:17 +0100 Subject: [PATCH 3248/3836] Blackify openstack.workflow Black used with the '-l 79 -S' flags. A future change will ignore this commit in git-blame history by adding a 'git-blame-ignore-revs' file. Change-Id: I3573a93dc8fab163fda536d479cae0785b657cdb Signed-off-by: Stephen Finucane --- .../tests/unit/workflow/test_cron_trigger.py | 18 +++---- .../tests/unit/workflow/test_execution.py | 3 +- openstack/tests/unit/workflow/test_version.py | 1 - .../tests/unit/workflow/test_workflow.py | 1 - .../tests/unit/workflow/v2/test_proxy.py | 48 ++++++++----------- openstack/workflow/v2/_proxy.py | 25 ++++++---- openstack/workflow/v2/cron_trigger.py | 28 ++++++++--- openstack/workflow/v2/execution.py | 22 +++++---- openstack/workflow/v2/workflow.py | 19 ++++---- 9 files changed, 90 insertions(+), 75 deletions(-) diff --git a/openstack/tests/unit/workflow/test_cron_trigger.py b/openstack/tests/unit/workflow/test_cron_trigger.py index 02ce8a676..002f5b830 100644 --- a/openstack/tests/unit/workflow/test_cron_trigger.py +++ b/openstack/tests/unit/workflow/test_cron_trigger.py @@ -17,7 +17,7 @@ FAKE_INPUT = { 'cluster_id': '8c74607c-5a74-4490-9414-a3475b1926c2', 'node_id': 'fba2cc5d-706f-4631-9577-3956048d13a2', - 'flavor_id': '1' + 'flavor_id': '1', } FAKE_PARAMS = {} @@ -36,7 +36,6 @@ class TestCronTrigger(base.TestCase): - def test_basic(self): sot = cron_trigger.CronTrigger() self.assertEqual('cron_trigger', sot.resource_key) @@ -69,19 +68,20 @@ def test_basic(self): 'updated_at': 'updated_at', 'all_projects': 'all_projects', }, - sot._query_mapping._mapping + sot._query_mapping._mapping, ) def test_make_it(self): sot = cron_trigger.CronTrigger(**FAKE) self.assertEqual(FAKE['id'], sot.id) self.assertEqual(FAKE['pattern'], sot.pattern) - self.assertEqual(FAKE['remaining_executions'], - sot.remaining_executions) - self.assertEqual(FAKE['first_execution_time'], - sot.first_execution_time) - self.assertEqual(FAKE['next_execution_time'], - sot.next_execution_time) + self.assertEqual( + FAKE['remaining_executions'], sot.remaining_executions + ) + self.assertEqual( + FAKE['first_execution_time'], sot.first_execution_time + ) + self.assertEqual(FAKE['next_execution_time'], sot.next_execution_time) self.assertEqual(FAKE['workflow_name'], sot.workflow_name) self.assertEqual(FAKE['workflow_id'], sot.workflow_id) self.assertEqual(FAKE['workflow_input'], sot.workflow_input) diff --git a/openstack/tests/unit/workflow/test_execution.py b/openstack/tests/unit/workflow/test_execution.py index e31394406..0d0b7e9da 100644 --- a/openstack/tests/unit/workflow/test_execution.py +++ b/openstack/tests/unit/workflow/test_execution.py @@ -17,7 +17,7 @@ FAKE_INPUT = { 'cluster_id': '8c74607c-5a74-4490-9414-a3475b1926c2', 'node_id': 'fba2cc5d-706f-4631-9577-3956048d13a2', - 'flavor_id': '1' + 'flavor_id': '1', } FAKE = { @@ -28,7 +28,6 @@ class TestExecution(base.TestCase): - def setUp(self): super(TestExecution, self).setUp() diff --git a/openstack/tests/unit/workflow/test_version.py b/openstack/tests/unit/workflow/test_version.py index 89406afbe..c822c59c2 100644 --- a/openstack/tests/unit/workflow/test_version.py +++ b/openstack/tests/unit/workflow/test_version.py @@ -23,7 +23,6 @@ class TestVersion(base.TestCase): - def test_basic(self): sot = version.Version() self.assertEqual('version', sot.resource_key) diff --git a/openstack/tests/unit/workflow/test_workflow.py b/openstack/tests/unit/workflow/test_workflow.py index acf444e38..efab05db4 100644 --- a/openstack/tests/unit/workflow/test_workflow.py +++ b/openstack/tests/unit/workflow/test_workflow.py @@ -22,7 +22,6 @@ class TestWorkflow(base.TestCase): - def setUp(self): super(TestWorkflow, self).setUp() diff --git a/openstack/tests/unit/workflow/v2/test_proxy.py b/openstack/tests/unit/workflow/v2/test_proxy.py index 428b59fd6..551b57a0c 100644 --- a/openstack/tests/unit/workflow/v2/test_proxy.py +++ b/openstack/tests/unit/workflow/v2/test_proxy.py @@ -23,44 +23,36 @@ def setUp(self): self.proxy = _proxy.Proxy(self.session) def test_workflows(self): - self.verify_list(self.proxy.workflows, - workflow.Workflow) + self.verify_list(self.proxy.workflows, workflow.Workflow) def test_executions(self): - self.verify_list(self.proxy.executions, - execution.Execution) + self.verify_list(self.proxy.executions, execution.Execution) def test_workflow_get(self): - self.verify_get(self.proxy.get_workflow, - workflow.Workflow) + self.verify_get(self.proxy.get_workflow, workflow.Workflow) def test_execution_get(self): - self.verify_get(self.proxy.get_execution, - execution.Execution) + self.verify_get(self.proxy.get_execution, execution.Execution) def test_workflow_create(self): - self.verify_create(self.proxy.create_workflow, - workflow.Workflow) + self.verify_create(self.proxy.create_workflow, workflow.Workflow) def test_execution_create(self): - self.verify_create(self.proxy.create_execution, - execution.Execution) + self.verify_create(self.proxy.create_execution, execution.Execution) def test_workflow_delete(self): - self.verify_delete(self.proxy.delete_workflow, - workflow.Workflow, True) + self.verify_delete(self.proxy.delete_workflow, workflow.Workflow, True) def test_execution_delete(self): - self.verify_delete(self.proxy.delete_execution, - execution.Execution, True) + self.verify_delete( + self.proxy.delete_execution, execution.Execution, True + ) def test_workflow_find(self): - self.verify_find(self.proxy.find_workflow, - workflow.Workflow) + self.verify_find(self.proxy.find_workflow, workflow.Workflow) def test_execution_find(self): - self.verify_find(self.proxy.find_execution, - execution.Execution) + self.verify_find(self.proxy.find_execution, execution.Execution) class TestCronTriggerProxy(test_proxy_base.TestProxyBase): @@ -69,20 +61,20 @@ def setUp(self): self.proxy = _proxy.Proxy(self.session) def test_cron_triggers(self): - self.verify_list(self.proxy.cron_triggers, - cron_trigger.CronTrigger) + self.verify_list(self.proxy.cron_triggers, cron_trigger.CronTrigger) def test_cron_trigger_get(self): - self.verify_get(self.proxy.get_cron_trigger, - cron_trigger.CronTrigger) + self.verify_get(self.proxy.get_cron_trigger, cron_trigger.CronTrigger) def test_cron_trigger_create(self): - self.verify_create(self.proxy.create_cron_trigger, - cron_trigger.CronTrigger) + self.verify_create( + self.proxy.create_cron_trigger, cron_trigger.CronTrigger + ) def test_cron_trigger_delete(self): - self.verify_delete(self.proxy.delete_cron_trigger, - cron_trigger.CronTrigger, True) + self.verify_delete( + self.proxy.delete_cron_trigger, cron_trigger.CronTrigger, True + ) def test_cron_trigger_find(self): self.verify_find( diff --git a/openstack/workflow/v2/_proxy.py b/openstack/workflow/v2/_proxy.py index 915a76aa9..a12552ab8 100644 --- a/openstack/workflow/v2/_proxy.py +++ b/openstack/workflow/v2/_proxy.py @@ -78,8 +78,9 @@ def delete_workflow(self, value, ignore_missing=True): :returns: ``None`` """ - return self._delete(_workflow.Workflow, value, - ignore_missing=ignore_missing) + return self._delete( + _workflow.Workflow, value, ignore_missing=ignore_missing + ) def find_workflow(self, name_or_id, ignore_missing=True): """Find a single workflow @@ -93,8 +94,9 @@ def find_workflow(self, name_or_id, ignore_missing=True): :returns: One :class:`~openstack.compute.v2.workflow.Extension` or None """ - return self._find(_workflow.Workflow, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _workflow.Workflow, name_or_id, ignore_missing=ignore_missing + ) def create_execution(self, **attrs): """Create a new execution from attributes @@ -154,8 +156,9 @@ def delete_execution(self, value, ignore_missing=True): :returns: ``None`` """ - return self._delete(_execution.Execution, value, - ignore_missing=ignore_missing) + return self._delete( + _execution.Execution, value, ignore_missing=ignore_missing + ) def find_execution(self, name_or_id, ignore_missing=True): """Find a single execution @@ -169,8 +172,9 @@ def find_execution(self, name_or_id, ignore_missing=True): :returns: One :class:`~openstack.compute.v2.execution.Execution` or None """ - return self._find(_execution.Execution, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _execution.Execution, name_or_id, ignore_missing=ignore_missing + ) def create_cron_trigger(self, **attrs): """Create a new cron trigger from attributes @@ -232,8 +236,9 @@ def delete_cron_trigger(self, value, ignore_missing=True): :returns: ``None`` """ - return self._delete(_cron_trigger.CronTrigger, value, - ignore_missing=ignore_missing) + return self._delete( + _cron_trigger.CronTrigger, value, ignore_missing=ignore_missing + ) # TODO(stephenfin): Drop 'query' parameter or apply it consistently def find_cron_trigger( diff --git a/openstack/workflow/v2/cron_trigger.py b/openstack/workflow/v2/cron_trigger.py index f38f58fb7..2a12e61b2 100644 --- a/openstack/workflow/v2/cron_trigger.py +++ b/openstack/workflow/v2/cron_trigger.py @@ -25,11 +25,26 @@ class CronTrigger(resource.Resource): allow_delete = True _query_mapping = resource.QueryParameters( - 'marker', 'limit', 'sort_keys', 'sort_dirs', 'fields', 'name', - 'workflow_name', 'workflow_id', 'workflow_input', 'workflow_params', - 'scope', 'pattern', 'remaining_executions', 'project_id', - 'first_execution_time', 'next_execution_time', 'created_at', - 'updated_at', 'all_projects') + 'marker', + 'limit', + 'sort_keys', + 'sort_dirs', + 'fields', + 'name', + 'workflow_name', + 'workflow_id', + 'workflow_input', + 'workflow_params', + 'scope', + 'pattern', + 'remaining_executions', + 'project_id', + 'first_execution_time', + 'next_execution_time', + 'created_at', + 'updated_at', + 'all_projects', + ) #: The name of this Cron Trigger name = resource.Body("name") @@ -58,4 +73,5 @@ class CronTrigger(resource.Resource): def create(self, session, base_path=None): return super(CronTrigger, self).create( - session, prepend_key=False, base_path=base_path) + session, prepend_key=False, base_path=base_path + ) diff --git a/openstack/workflow/v2/execution.py b/openstack/workflow/v2/execution.py index 9fa73c80c..0f9633c76 100644 --- a/openstack/workflow/v2/execution.py +++ b/openstack/workflow/v2/execution.py @@ -25,8 +25,14 @@ class Execution(resource.Resource): allow_delete = True _query_mapping = resource.QueryParameters( - 'marker', 'limit', 'sort_keys', 'sort_dirs', 'fields', 'params', - 'include_output') + 'marker', + 'limit', + 'sort_keys', + 'sort_dirs', + 'fields', + 'params', + 'include_output', + ) #: The name of the workflow workflow_name = resource.Body("workflow_name") @@ -53,14 +59,14 @@ class Execution(resource.Resource): updated_at = resource.Body("updated_at") def create(self, session, prepend_key=True, base_path=None): - request = self._prepare_request(requires_id=False, - prepend_key=prepend_key, - base_path=base_path) + request = self._prepare_request( + requires_id=False, prepend_key=prepend_key, base_path=base_path + ) request_body = request.body["execution"] - response = session.post(request.url, - json=request_body, - headers=request.headers) + response = session.post( + request.url, json=request_body, headers=request.headers + ) self._translate_response(response, has_body=True) return self diff --git a/openstack/workflow/v2/workflow.py b/openstack/workflow/v2/workflow.py index e196c19b4..ef3e6b785 100644 --- a/openstack/workflow/v2/workflow.py +++ b/openstack/workflow/v2/workflow.py @@ -25,7 +25,8 @@ class Workflow(resource.Resource): allow_delete = True _query_mapping = resource.QueryParameters( - 'marker', 'limit', 'sort_keys', 'sort_dirs', 'fields') + 'marker', 'limit', 'sort_keys', 'sort_dirs', 'fields' + ) #: The name of this Workflow name = resource.Body("name") @@ -47,13 +48,11 @@ class Workflow(resource.Resource): updated_at = resource.Body("updated_at") def create(self, session, prepend_key=True, base_path=None): - request = self._prepare_request(requires_id=False, - prepend_key=prepend_key, - base_path=base_path) + request = self._prepare_request( + requires_id=False, prepend_key=prepend_key, base_path=base_path + ) - headers = { - "Content-Type": 'text/plain' - } + headers = {"Content-Type": 'text/plain'} kwargs = { "data": self.definition, } @@ -62,9 +61,9 @@ def create(self, session, prepend_key=True, base_path=None): uri = request.url + scope request.headers.update(headers) - response = session.post(uri, - json=None, - headers=request.headers, **kwargs) + response = session.post( + uri, json=None, headers=request.headers, **kwargs + ) self._translate_response(response, has_body=False) return self From 0e2b5d263fdf12e0c8a67503712afab2816ef2d0 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 5 May 2023 11:02:56 +0100 Subject: [PATCH 3249/3836] Blackify openstack.message Black used with the '-l 79 -S' flags. A future change will ignore this commit in git-blame history by adding a 'git-blame-ignore-revs' file. Change-Id: Ibb7c7931cb3a7a9a3ccc577475e3e6b1355ca4c9 Signed-off-by: Stephen Finucane --- openstack/message/v2/_proxy.py | 61 ++++--- openstack/message/v2/claim.py | 53 +++--- openstack/message/v2/message.py | 42 +++-- openstack/message/v2/queue.py | 45 +++-- openstack/message/v2/subscription.py | 45 +++-- openstack/tests/unit/message/test_version.py | 1 - openstack/tests/unit/message/v2/test_claim.py | 109 +++++++----- .../tests/unit/message/v2/test_message.py | 119 +++++++------ openstack/tests/unit/message/v2/test_proxy.py | 162 +++++++++++------- openstack/tests/unit/message/v2/test_queue.py | 68 ++++---- .../unit/message/v2/test_subscription.py | 88 ++++++---- 11 files changed, 459 insertions(+), 334 deletions(-) diff --git a/openstack/message/v2/_proxy.py b/openstack/message/v2/_proxy.py index 31741890c..0bb619455 100644 --- a/openstack/message/v2/_proxy.py +++ b/openstack/message/v2/_proxy.py @@ -91,8 +91,9 @@ def post_message(self, queue_name, messages): :returns: A string includes location of messages successfully posted. """ - message = self._get_resource(_message.Message, None, - queue_name=queue_name) + message = self._get_resource( + _message.Message, None, queue_name=queue_name + ) return message.post(self, messages) def messages(self, queue_name, **query): @@ -129,12 +130,14 @@ def get_message(self, queue_name, message): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no message matching the criteria could be found. """ - message = self._get_resource(_message.Message, message, - queue_name=queue_name) + message = self._get_resource( + _message.Message, message, queue_name=queue_name + ) return self._get(_message.Message, message) - def delete_message(self, queue_name, value, claim=None, - ignore_missing=True): + def delete_message( + self, queue_name, value, claim=None, ignore_missing=True + ): """Delete a message :param queue_name: The name of target queue to delete message from. @@ -152,11 +155,13 @@ def delete_message(self, queue_name, value, claim=None, :returns: ``None`` """ - message = self._get_resource(_message.Message, value, - queue_name=queue_name) + message = self._get_resource( + _message.Message, value, queue_name=queue_name + ) message.claim_id = resource.Resource._get_id(claim) - return self._delete(_message.Message, message, - ignore_missing=ignore_missing) + return self._delete( + _message.Message, message, ignore_missing=ignore_missing + ) def create_subscription(self, queue_name, **attrs): """Create a new subscription from attributes @@ -169,8 +174,9 @@ def create_subscription(self, queue_name, **attrs): :returns: The results of subscription creation :rtype: :class:`~openstack.message.v2.subscription.Subscription` """ - return self._create(_subscription.Subscription, queue_name=queue_name, - **attrs) + return self._create( + _subscription.Subscription, queue_name=queue_name, **attrs + ) def subscriptions(self, queue_name, **query): """Retrieve a generator of subscriptions @@ -203,9 +209,9 @@ def get_subscription(self, queue_name, subscription): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no subscription matching the criteria could be found. """ - subscription = self._get_resource(_subscription.Subscription, - subscription, - queue_name=queue_name) + subscription = self._get_resource( + _subscription.Subscription, subscription, queue_name=queue_name + ) return self._get(_subscription.Subscription, subscription) def delete_subscription(self, queue_name, value, ignore_missing=True): @@ -224,10 +230,14 @@ def delete_subscription(self, queue_name, value, ignore_missing=True): :returns: ``None`` """ - subscription = self._get_resource(_subscription.Subscription, value, - queue_name=queue_name) - return self._delete(_subscription.Subscription, subscription, - ignore_missing=ignore_missing) + subscription = self._get_resource( + _subscription.Subscription, value, queue_name=queue_name + ) + return self._delete( + _subscription.Subscription, + subscription, + ignore_missing=ignore_missing, + ) def create_claim(self, queue_name, **attrs): """Create a new claim from attributes @@ -268,8 +278,9 @@ def update_claim(self, queue_name, claim, **attrs): :returns: The results of claim update :rtype: :class:`~openstack.message.v2.claim.Claim` """ - return self._update(_claim.Claim, claim, queue_name=queue_name, - **attrs) + return self._update( + _claim.Claim, claim, queue_name=queue_name, **attrs + ) def delete_claim(self, queue_name, claim, ignore_missing=True): """Delete a claim @@ -285,5 +296,9 @@ def delete_claim(self, queue_name, claim, ignore_missing=True): :returns: ``None`` """ - return self._delete(_claim.Claim, claim, queue_name=queue_name, - ignore_missing=ignore_missing) + return self._delete( + _claim.Claim, + claim, + queue_name=queue_name, + ignore_missing=ignore_missing, + ) diff --git a/openstack/message/v2/claim.py b/openstack/message/v2/claim.py index 9c1afae47..1ce0544ce 100644 --- a/openstack/message/v2/claim.py +++ b/openstack/message/v2/claim.py @@ -62,16 +62,17 @@ def _translate_response(self, response, has_body=True): self.id = self.location.split("claims/")[1] def create(self, session, prepend_key=False, base_path=None): - request = self._prepare_request(requires_id=False, - prepend_key=prepend_key, - base_path=base_path) + request = self._prepare_request( + requires_id=False, prepend_key=prepend_key, base_path=base_path + ) headers = { "Client-ID": self.client_id or str(uuid.uuid4()), - "X-PROJECT-ID": self.project_id or session.get_project_id() + "X-PROJECT-ID": self.project_id or session.get_project_id(), } request.headers.update(headers) - response = session.post(request.url, - json=request.body, headers=request.headers) + response = session.post( + request.url, json=request.body, headers=request.headers + ) # For case no message was claimed successfully, 204 No Content # message will be returned. In other cases, we translate response @@ -81,34 +82,43 @@ def create(self, session, prepend_key=False, base_path=None): return self - def fetch(self, session, requires_id=True, - base_path=None, error_message=None, skip_cache=False): - request = self._prepare_request(requires_id=requires_id, - base_path=base_path) + def fetch( + self, + session, + requires_id=True, + base_path=None, + error_message=None, + skip_cache=False, + ): + request = self._prepare_request( + requires_id=requires_id, base_path=base_path + ) headers = { "Client-ID": self.client_id or str(uuid.uuid4()), - "X-PROJECT-ID": self.project_id or session.get_project_id() + "X-PROJECT-ID": self.project_id or session.get_project_id(), } request.headers.update(headers) response = session.get( - request.url, headers=request.headers, skip_cache=False) + request.url, headers=request.headers, skip_cache=False + ) self._translate_response(response) return self - def commit(self, session, prepend_key=False, has_body=False, - base_path=None): - request = self._prepare_request(prepend_key=prepend_key, - base_path=base_path) + def commit( + self, session, prepend_key=False, has_body=False, base_path=None + ): + request = self._prepare_request( + prepend_key=prepend_key, base_path=base_path + ) headers = { "Client-ID": self.client_id or str(uuid.uuid4()), - "X-PROJECT-ID": self.project_id or session.get_project_id() + "X-PROJECT-ID": self.project_id or session.get_project_id(), } request.headers.update(headers) - session.patch(request.url, - json=request.body, headers=request.headers) + session.patch(request.url, json=request.body, headers=request.headers) return self @@ -116,12 +126,11 @@ def delete(self, session): request = self._prepare_request() headers = { "Client-ID": self.client_id or str(uuid.uuid4()), - "X-PROJECT-ID": self.project_id or session.get_project_id() + "X-PROJECT-ID": self.project_id or session.get_project_id(), } request.headers.update(headers) - response = session.delete(request.url, - headers=request.headers) + response = session.delete(request.url, headers=request.headers) self._translate_response(response, has_body=False) return self diff --git a/openstack/message/v2/message.py b/openstack/message/v2/message.py index 9b12b878e..40db3a5e9 100644 --- a/openstack/message/v2/message.py +++ b/openstack/message/v2/message.py @@ -57,12 +57,13 @@ def post(self, session, messages): request = self._prepare_request(requires_id=False, prepend_key=True) headers = { "Client-ID": self.client_id or str(uuid.uuid4()), - "X-PROJECT-ID": self.project_id or session.get_project_id() + "X-PROJECT-ID": self.project_id or session.get_project_id(), } request.headers.update(headers) request.body = {'messages': messages} - response = session.post(request.url, - json=request.body, headers=request.headers) + response = session.post( + request.url, json=request.body, headers=request.headers + ) return response.json()['resources'] @@ -82,14 +83,13 @@ def list(cls, session, paginated=True, base_path=None, **params): uri = base_path % params headers = { "Client-ID": params.get('client_id', None) or str(uuid.uuid4()), - "X-PROJECT-ID": params.get('project_id', None - ) or session.get_project_id() + "X-PROJECT-ID": params.get('project_id', None) + or session.get_project_id(), } query_params = cls._query_mapping._transpose(params, cls) while more_data: - resp = session.get(uri, - headers=headers, params=query_params) + resp = session.get(uri, headers=headers, params=query_params) resp = resp.json() resp = resp[cls.resources_key] @@ -111,19 +111,26 @@ def list(cls, session, paginated=True, base_path=None, **params): query_params["limit"] = yielded query_params["marker"] = new_marker - def fetch(self, session, requires_id=True, - base_path=None, error_message=None, skip_cache=False): - request = self._prepare_request(requires_id=requires_id, - base_path=base_path) + def fetch( + self, + session, + requires_id=True, + base_path=None, + error_message=None, + skip_cache=False, + ): + request = self._prepare_request( + requires_id=requires_id, base_path=base_path + ) headers = { "Client-ID": self.client_id or str(uuid.uuid4()), - "X-PROJECT-ID": self.project_id or session.get_project_id() + "X-PROJECT-ID": self.project_id or session.get_project_id(), } request.headers.update(headers) - response = session.get(request.url, - headers=headers, - skip_cache=skip_cache) + response = session.get( + request.url, headers=headers, skip_cache=skip_cache + ) self._translate_response(response) return self @@ -132,7 +139,7 @@ def delete(self, session): request = self._prepare_request() headers = { "Client-ID": self.client_id or str(uuid.uuid4()), - "X-PROJECT-ID": self.project_id or session.get_project_id() + "X-PROJECT-ID": self.project_id or session.get_project_id(), } request.headers.update(headers) @@ -141,8 +148,7 @@ def delete(self, session): # rebuild the request URI if claim_id is not None. if self.claim_id: request.url += '?claim_id=%s' % self.claim_id - response = session.delete(request.url, - headers=headers) + response = session.delete(request.url, headers=headers) self._translate_response(response, has_body=False) return self diff --git a/openstack/message/v2/queue.py b/openstack/message/v2/queue.py index 560f52e3f..a256dee57 100644 --- a/openstack/message/v2/queue.py +++ b/openstack/message/v2/queue.py @@ -51,16 +51,17 @@ class Queue(resource.Resource): project_id = resource.Header("X-PROJECT-ID") def create(self, session, prepend_key=True, base_path=None): - request = self._prepare_request(requires_id=True, - prepend_key=prepend_key, - base_path=None) + request = self._prepare_request( + requires_id=True, prepend_key=prepend_key, base_path=None + ) headers = { "Client-ID": self.client_id or str(uuid.uuid4()), - "X-PROJECT-ID": self.project_id or session.get_project_id() + "X-PROJECT-ID": self.project_id or session.get_project_id(), } request.headers.update(headers) - response = session.put(request.url, - json=request.body, headers=request.headers) + response = session.put( + request.url, json=request.body, headers=request.headers + ) self._translate_response(response, has_body=False) return self @@ -82,13 +83,12 @@ def list(cls, session, paginated=False, base_path=None, **params): uri = base_path % params headers = { "Client-ID": params.get('client_id', None) or str(uuid.uuid4()), - "X-PROJECT-ID": params.get('project_id', None - ) or session.get_project_id() + "X-PROJECT-ID": params.get('project_id', None) + or session.get_project_id(), } while more_data: - resp = session.get(uri, - headers=headers, params=query_params) + resp = session.get(uri, headers=headers, params=query_params) resp = resp.json() resp = resp[cls.resources_key] @@ -110,17 +110,25 @@ def list(cls, session, paginated=False, base_path=None, **params): query_params["limit"] = yielded query_params["marker"] = new_marker - def fetch(self, session, requires_id=True, - base_path=None, error_message=None, skip_cache=False): - request = self._prepare_request(requires_id=requires_id, - base_path=base_path) + def fetch( + self, + session, + requires_id=True, + base_path=None, + error_message=None, + skip_cache=False, + ): + request = self._prepare_request( + requires_id=requires_id, base_path=base_path + ) headers = { "Client-ID": self.client_id or str(uuid.uuid4()), - "X-PROJECT-ID": self.project_id or session.get_project_id() + "X-PROJECT-ID": self.project_id or session.get_project_id(), } request.headers.update(headers) response = session.get( - request.url, headers=headers, skip_cache=skip_cache) + request.url, headers=headers, skip_cache=skip_cache + ) self._translate_response(response) return self @@ -129,11 +137,10 @@ def delete(self, session): request = self._prepare_request() headers = { "Client-ID": self.client_id or str(uuid.uuid4()), - "X-PROJECT-ID": self.project_id or session.get_project_id() + "X-PROJECT-ID": self.project_id or session.get_project_id(), } request.headers.update(headers) - response = session.delete(request.url, - headers=headers) + response = session.delete(request.url, headers=headers) self._translate_response(response, has_body=False) return self diff --git a/openstack/message/v2/subscription.py b/openstack/message/v2/subscription.py index f29d10d1a..d8d36e753 100644 --- a/openstack/message/v2/subscription.py +++ b/openstack/message/v2/subscription.py @@ -59,16 +59,17 @@ class Subscription(resource.Resource): project_id = resource.Header("X-PROJECT-ID") def create(self, session, prepend_key=True, base_path=None): - request = self._prepare_request(requires_id=False, - prepend_key=prepend_key, - base_path=base_path) + request = self._prepare_request( + requires_id=False, prepend_key=prepend_key, base_path=base_path + ) headers = { "Client-ID": self.client_id or str(uuid.uuid4()), - "X-PROJECT-ID": self.project_id or session.get_project_id() + "X-PROJECT-ID": self.project_id or session.get_project_id(), } request.headers.update(headers) - response = session.post(request.url, - json=request.body, headers=request.headers) + response = session.post( + request.url, json=request.body, headers=request.headers + ) self._translate_response(response) return self @@ -89,14 +90,13 @@ def list(cls, session, paginated=True, base_path=None, **params): uri = base_path % params headers = { "Client-ID": params.get('client_id', None) or str(uuid.uuid4()), - "X-PROJECT-ID": params.get('project_id', None - ) or session.get_project_id() + "X-PROJECT-ID": params.get('project_id', None) + or session.get_project_id(), } query_params = cls._query_mapping._transpose(params, cls) while more_data: - resp = session.get(uri, - headers=headers, params=query_params) + resp = session.get(uri, headers=headers, params=query_params) resp = resp.json() resp = resp[cls.resources_key] @@ -118,18 +118,26 @@ def list(cls, session, paginated=True, base_path=None, **params): query_params["limit"] = yielded query_params["marker"] = new_marker - def fetch(self, session, requires_id=True, - base_path=None, error_message=None, skip_cache=False): - request = self._prepare_request(requires_id=requires_id, - base_path=base_path) + def fetch( + self, + session, + requires_id=True, + base_path=None, + error_message=None, + skip_cache=False, + ): + request = self._prepare_request( + requires_id=requires_id, base_path=base_path + ) headers = { "Client-ID": self.client_id or str(uuid.uuid4()), - "X-PROJECT-ID": self.project_id or session.get_project_id() + "X-PROJECT-ID": self.project_id or session.get_project_id(), } request.headers.update(headers) response = session.get( - request.url, headers=request.headers, skip_cache=skip_cache) + request.url, headers=request.headers, skip_cache=skip_cache + ) self._translate_response(response) return self @@ -138,12 +146,11 @@ def delete(self, session): request = self._prepare_request() headers = { "Client-ID": self.client_id or str(uuid.uuid4()), - "X-PROJECT-ID": self.project_id or session.get_project_id() + "X-PROJECT-ID": self.project_id or session.get_project_id(), } request.headers.update(headers) - response = session.delete(request.url, - headers=request.headers) + response = session.delete(request.url, headers=request.headers) self._translate_response(response, has_body=False) return self diff --git a/openstack/tests/unit/message/test_version.py b/openstack/tests/unit/message/test_version.py index 8692bfc32..3d3d03efc 100644 --- a/openstack/tests/unit/message/test_version.py +++ b/openstack/tests/unit/message/test_version.py @@ -23,7 +23,6 @@ class TestVersion(base.TestCase): - def test_basic(self): sot = version.Version() self.assertEqual('version', sot.resource_key) diff --git a/openstack/tests/unit/message/v2/test_claim.py b/openstack/tests/unit/message/v2/test_claim.py index 673b4547d..f08c5aa30 100644 --- a/openstack/tests/unit/message/v2/test_claim.py +++ b/openstack/tests/unit/message/v2/test_claim.py @@ -24,7 +24,7 @@ "limit": 10, "messages": [{"id": "1"}, {"id": "2"}], "ttl": 3600, - "queue_name": "queue1" + "queue_name": "queue1", } @@ -37,7 +37,7 @@ "ttl": 3600, "queue_name": "queue1", "client_id": "OLD_CLIENT_ID", - "project_id": "OLD_PROJECT_ID" + "project_id": "OLD_PROJECT_ID", } @@ -77,10 +77,11 @@ def test_create_204_resp(self, mock_uuid): res = sot.create(sess) url = "/queues/%(queue)s/claims" % {"queue": FAKE.pop("queue_name")} - headers = {"Client-ID": "NEW_CLIENT_ID", - "X-PROJECT-ID": "NEW_PROJECT_ID"} - sess.post.assert_called_once_with(url, - headers=headers, json=FAKE) + headers = { + "Client-ID": "NEW_CLIENT_ID", + "X-PROJECT-ID": "NEW_PROJECT_ID", + } + sess.post.assert_called_once_with(url, headers=headers, json=FAKE) sess.get_project_id.assert_called_once_with() self.assertEqual(sot, res) @@ -99,10 +100,11 @@ def test_create_non_204_resp(self, mock_uuid): res = sot.create(sess) url = "/queues/%(queue)s/claims" % {"queue": FAKE.pop("queue_name")} - headers = {"Client-ID": "NEW_CLIENT_ID", - "X-PROJECT-ID": "NEW_PROJECT_ID"} - sess.post.assert_called_once_with(url, - headers=headers, json=FAKE) + headers = { + "Client-ID": "NEW_CLIENT_ID", + "X-PROJECT-ID": "NEW_PROJECT_ID", + } + sess.post.assert_called_once_with(url, headers=headers, json=FAKE) sess.get_project_id.assert_called_once_with() self.assertEqual(sot, res) sot._translate_response.assert_called_once_with(resp) @@ -119,10 +121,11 @@ def test_create_client_id_project_id_exist(self): res = sot.create(sess) url = "/queues/%(queue)s/claims" % {"queue": FAKE.pop("queue_name")} - headers = {"Client-ID": FAKE.pop("client_id"), - "X-PROJECT-ID": FAKE.pop("project_id")} - sess.post.assert_called_once_with(url, - headers=headers, json=FAKE) + headers = { + "Client-ID": FAKE.pop("client_id"), + "X-PROJECT-ID": FAKE.pop("project_id"), + } + sess.post.assert_called_once_with(url, headers=headers, json=FAKE) self.assertEqual(sot, res) @mock.patch.object(uuid, "uuid4") @@ -138,11 +141,14 @@ def test_get(self, mock_uuid): res = sot.fetch(sess) url = "queues/%(queue)s/claims/%(claim)s" % { - "queue": FAKE1["queue_name"], "claim": FAKE1["id"]} - headers = {"Client-ID": "NEW_CLIENT_ID", - "X-PROJECT-ID": "NEW_PROJECT_ID"} - sess.get.assert_called_with( - url, headers=headers, skip_cache=False) + "queue": FAKE1["queue_name"], + "claim": FAKE1["id"], + } + headers = { + "Client-ID": "NEW_CLIENT_ID", + "X-PROJECT-ID": "NEW_PROJECT_ID", + } + sess.get.assert_called_with(url, headers=headers, skip_cache=False) sess.get_project_id.assert_called_once_with() sot._translate_response.assert_called_once_with(resp) self.assertEqual(sot, res) @@ -157,11 +163,14 @@ def test_get_client_id_project_id_exist(self): res = sot.fetch(sess) url = "queues/%(queue)s/claims/%(claim)s" % { - "queue": FAKE2["queue_name"], "claim": FAKE2["id"]} - headers = {"Client-ID": "OLD_CLIENT_ID", - "X-PROJECT-ID": "OLD_PROJECT_ID"} - sess.get.assert_called_with( - url, headers=headers, skip_cache=False) + "queue": FAKE2["queue_name"], + "claim": FAKE2["id"], + } + headers = { + "Client-ID": "OLD_CLIENT_ID", + "X-PROJECT-ID": "OLD_PROJECT_ID", + } + sess.get.assert_called_with(url, headers=headers, skip_cache=False) sot._translate_response.assert_called_once_with(resp) self.assertEqual(sot, res) @@ -178,11 +187,14 @@ def test_update(self, mock_uuid): res = sot.commit(sess) url = "queues/%(queue)s/claims/%(claim)s" % { - "queue": FAKE.pop("queue_name"), "claim": FAKE["id"]} - headers = {"Client-ID": "NEW_CLIENT_ID", - "X-PROJECT-ID": "NEW_PROJECT_ID"} - sess.patch.assert_called_with(url, - headers=headers, json=FAKE) + "queue": FAKE.pop("queue_name"), + "claim": FAKE["id"], + } + headers = { + "Client-ID": "NEW_CLIENT_ID", + "X-PROJECT-ID": "NEW_PROJECT_ID", + } + sess.patch.assert_called_with(url, headers=headers, json=FAKE) sess.get_project_id.assert_called_once_with() self.assertEqual(sot, res) @@ -196,11 +208,14 @@ def test_update_client_id_project_id_exist(self): res = sot.commit(sess) url = "queues/%(queue)s/claims/%(claim)s" % { - "queue": FAKE.pop("queue_name"), "claim": FAKE["id"]} - headers = {"Client-ID": FAKE.pop("client_id"), - "X-PROJECT-ID": FAKE.pop("project_id")} - sess.patch.assert_called_with(url, - headers=headers, json=FAKE) + "queue": FAKE.pop("queue_name"), + "claim": FAKE["id"], + } + headers = { + "Client-ID": FAKE.pop("client_id"), + "X-PROJECT-ID": FAKE.pop("project_id"), + } + sess.patch.assert_called_with(url, headers=headers, json=FAKE) self.assertEqual(sot, res) @mock.patch.object(uuid, "uuid4") @@ -216,11 +231,14 @@ def test_delete(self, mock_uuid): sot.delete(sess) url = "queues/%(queue)s/claims/%(claim)s" % { - "queue": FAKE1["queue_name"], "claim": FAKE1["id"]} - headers = {"Client-ID": "NEW_CLIENT_ID", - "X-PROJECT-ID": "NEW_PROJECT_ID"} - sess.delete.assert_called_with(url, - headers=headers) + "queue": FAKE1["queue_name"], + "claim": FAKE1["id"], + } + headers = { + "Client-ID": "NEW_CLIENT_ID", + "X-PROJECT-ID": "NEW_PROJECT_ID", + } + sess.delete.assert_called_with(url, headers=headers) sess.get_project_id.assert_called_once_with() sot._translate_response.assert_called_once_with(resp, has_body=False) @@ -234,9 +252,12 @@ def test_delete_client_id_project_id_exist(self): sot.delete(sess) url = "queues/%(queue)s/claims/%(claim)s" % { - "queue": FAKE2["queue_name"], "claim": FAKE2["id"]} - headers = {"Client-ID": "OLD_CLIENT_ID", - "X-PROJECT-ID": "OLD_PROJECT_ID"} - sess.delete.assert_called_with(url, - headers=headers) + "queue": FAKE2["queue_name"], + "claim": FAKE2["id"], + } + headers = { + "Client-ID": "OLD_CLIENT_ID", + "X-PROJECT-ID": "OLD_PROJECT_ID", + } + sess.delete.assert_called_with(url, headers=headers) sot._translate_response.assert_called_once_with(resp, has_body=False) diff --git a/openstack/tests/unit/message/v2/test_message.py b/openstack/tests/unit/message/v2/test_message.py index a6af49ca5..b42c44159 100644 --- a/openstack/tests/unit/message/v2/test_message.py +++ b/openstack/tests/unit/message/v2/test_message.py @@ -21,12 +21,12 @@ 'body': { 'current_bytes': '0', 'event': 'BackupProgress', - 'total_bytes': '99614720' + 'total_bytes': '99614720', }, 'id': '578ee000508f153f256f717d', 'href': '/v2/queues/queue1/messages/578ee000508f153f256f717d', 'ttl': 3600, - 'queue_name': 'queue1' + 'queue_name': 'queue1', } @@ -35,14 +35,14 @@ 'body': { 'current_bytes': '0', 'event': 'BackupProgress', - 'total_bytes': '99614720' + 'total_bytes': '99614720', }, 'id': '578ee000508f153f256f717d', 'href': '/v2/queues/queue1/messages/578ee000508f153f256f717d', 'ttl': 3600, 'queue_name': 'queue1', 'client_id': 'OLD_CLIENT_ID', - 'project_id': 'OLD_PROJECT_ID' + 'project_id': 'OLD_PROJECT_ID', } @@ -80,25 +80,21 @@ def test_post(self, mock_uuid): sess.get_project_id.return_value = 'NEW_PROJECT_ID' mock_uuid.return_value = 'NEW_CLIENT_ID' messages = [ - { - 'body': {'key': 'value1'}, - 'ttl': 3600 - }, - { - 'body': {'key': 'value2'}, - 'ttl': 1800 - } + {'body': {'key': 'value1'}, 'ttl': 3600}, + {'body': {'key': 'value2'}, 'ttl': 1800}, ] sot = message.Message(**FAKE1) res = sot.post(sess, messages) url = '/queues/%(queue)s/messages' % {'queue': FAKE1['queue_name']} - headers = {'Client-ID': 'NEW_CLIENT_ID', - 'X-PROJECT-ID': 'NEW_PROJECT_ID'} - sess.post.assert_called_once_with(url, - headers=headers, - json={'messages': messages}) + headers = { + 'Client-ID': 'NEW_CLIENT_ID', + 'X-PROJECT-ID': 'NEW_PROJECT_ID', + } + sess.post.assert_called_once_with( + url, headers=headers, json={'messages': messages} + ) sess.get_project_id.assert_called_once_with() resp.json.assert_called_once_with() self.assertEqual(resources, res) @@ -113,25 +109,21 @@ def test_post_client_id_project_id_exist(self): ] resp.json.return_value = {'resources': resources} messages = [ - { - 'body': {'key': 'value1'}, - 'ttl': 3600 - }, - { - 'body': {'key': 'value2'}, - 'ttl': 1800 - } + {'body': {'key': 'value1'}, 'ttl': 3600}, + {'body': {'key': 'value2'}, 'ttl': 1800}, ] sot = message.Message(**FAKE2) res = sot.post(sess, messages) url = '/queues/%(queue)s/messages' % {'queue': FAKE2['queue_name']} - headers = {'Client-ID': 'OLD_CLIENT_ID', - 'X-PROJECT-ID': 'OLD_PROJECT_ID'} - sess.post.assert_called_once_with(url, - headers=headers, - json={'messages': messages}) + headers = { + 'Client-ID': 'OLD_CLIENT_ID', + 'X-PROJECT-ID': 'OLD_PROJECT_ID', + } + sess.post.assert_called_once_with( + url, headers=headers, json={'messages': messages} + ) resp.json.assert_called_once_with() self.assertEqual(resources, res) @@ -148,11 +140,14 @@ def test_get(self, mock_uuid): res = sot.fetch(sess) url = 'queues/%(queue)s/messages/%(message)s' % { - 'queue': FAKE1['queue_name'], 'message': FAKE1['id']} - headers = {'Client-ID': 'NEW_CLIENT_ID', - 'X-PROJECT-ID': 'NEW_PROJECT_ID'} - sess.get.assert_called_with( - url, headers=headers, skip_cache=False) + 'queue': FAKE1['queue_name'], + 'message': FAKE1['id'], + } + headers = { + 'Client-ID': 'NEW_CLIENT_ID', + 'X-PROJECT-ID': 'NEW_PROJECT_ID', + } + sess.get.assert_called_with(url, headers=headers, skip_cache=False) sess.get_project_id.assert_called_once_with() sot._translate_response.assert_called_once_with(resp) self.assertEqual(sot, res) @@ -167,14 +162,17 @@ def test_get_client_id_project_id_exist(self): res = sot.fetch(sess) url = 'queues/%(queue)s/messages/%(message)s' % { - 'queue': FAKE2['queue_name'], 'message': FAKE2['id']} + 'queue': FAKE2['queue_name'], + 'message': FAKE2['id'], + } sot = message.Message(**FAKE2) sot._translate_response = mock.Mock() res = sot.fetch(sess) - headers = {'Client-ID': 'OLD_CLIENT_ID', - 'X-PROJECT-ID': 'OLD_PROJECT_ID'} - sess.get.assert_called_with( - url, headers=headers, skip_cache=False) + headers = { + 'Client-ID': 'OLD_CLIENT_ID', + 'X-PROJECT-ID': 'OLD_PROJECT_ID', + } + sess.get.assert_called_with(url, headers=headers, skip_cache=False) sot._translate_response.assert_called_once_with(resp) self.assertEqual(sot, res) @@ -192,11 +190,14 @@ def test_delete_unclaimed(self, mock_uuid): sot.delete(sess) url = 'queues/%(queue)s/messages/%(message)s' % { - 'queue': FAKE1['queue_name'], 'message': FAKE1['id']} - headers = {'Client-ID': 'NEW_CLIENT_ID', - 'X-PROJECT-ID': 'NEW_PROJECT_ID'} - sess.delete.assert_called_with(url, - headers=headers) + 'queue': FAKE1['queue_name'], + 'message': FAKE1['id'], + } + headers = { + 'Client-ID': 'NEW_CLIENT_ID', + 'X-PROJECT-ID': 'NEW_PROJECT_ID', + } + sess.delete.assert_called_with(url, headers=headers) sess.get_project_id.assert_called_once_with() sot._translate_response.assert_called_once_with(resp, has_body=False) @@ -214,12 +215,15 @@ def test_delete_claimed(self, mock_uuid): sot.delete(sess) url = 'queues/%(queue)s/messages/%(message)s?claim_id=%(cid)s' % { - 'queue': FAKE1['queue_name'], 'message': FAKE1['id'], - 'cid': 'CLAIM_ID'} - headers = {'Client-ID': 'NEW_CLIENT_ID', - 'X-PROJECT-ID': 'NEW_PROJECT_ID'} - sess.delete.assert_called_with(url, - headers=headers) + 'queue': FAKE1['queue_name'], + 'message': FAKE1['id'], + 'cid': 'CLAIM_ID', + } + headers = { + 'Client-ID': 'NEW_CLIENT_ID', + 'X-PROJECT-ID': 'NEW_PROJECT_ID', + } + sess.delete.assert_called_with(url, headers=headers) sess.get_project_id.assert_called_once_with() sot._translate_response.assert_called_once_with(resp, has_body=False) @@ -234,9 +238,12 @@ def test_delete_client_id_project_id_exist(self): sot.delete(sess) url = 'queues/%(queue)s/messages/%(message)s' % { - 'queue': FAKE2['queue_name'], 'message': FAKE2['id']} - headers = {'Client-ID': 'OLD_CLIENT_ID', - 'X-PROJECT-ID': 'OLD_PROJECT_ID'} - sess.delete.assert_called_with(url, - headers=headers) + 'queue': FAKE2['queue_name'], + 'message': FAKE2['id'], + } + headers = { + 'Client-ID': 'OLD_CLIENT_ID', + 'X-PROJECT-ID': 'OLD_PROJECT_ID', + } + sess.delete.assert_called_with(url, headers=headers) sot._translate_response.assert_called_once_with(resp, has_body=False) diff --git a/openstack/tests/unit/message/v2/test_proxy.py b/openstack/tests/unit/message/v2/test_proxy.py index 8538b4dcb..19e3181de 100644 --- a/openstack/tests/unit/message/v2/test_proxy.py +++ b/openstack/tests/unit/message/v2/test_proxy.py @@ -36,8 +36,8 @@ def test_queue_create(self): def test_queue_get(self): self.verify_get(self.proxy.get_queue, queue.Queue) self.verify_get_overrided( - self.proxy, queue.Queue, - 'openstack.message.v2.queue.Queue') + self.proxy, queue.Queue, 'openstack.message.v2.queue.Queue' + ) def test_queues(self): self.verify_list(self.proxy.queues, queue.Queue) @@ -58,9 +58,11 @@ def test_message_post(self, mock_get_resource): "openstack.message.v2.message.Message.post", self.proxy.post_message, method_args=["test_queue", ["msg1", "msg2"]], - expected_args=[self.proxy, ["msg1", "msg2"]]) - mock_get_resource.assert_called_once_with(message.Message, None, - queue_name="test_queue") + expected_args=[self.proxy, ["msg1", "msg2"]], + ) + mock_get_resource.assert_called_once_with( + message.Message, None, queue_name="test_queue" + ) @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_message_get(self, mock_get_resource): @@ -69,18 +71,22 @@ def test_message_get(self, mock_get_resource): "openstack.proxy.Proxy._get", self.proxy.get_message, method_args=["test_queue", "resource_or_id"], - expected_args=[message.Message, "resource_or_id"]) - mock_get_resource.assert_called_once_with(message.Message, - "resource_or_id", - queue_name="test_queue") + expected_args=[message.Message, "resource_or_id"], + ) + mock_get_resource.assert_called_once_with( + message.Message, "resource_or_id", queue_name="test_queue" + ) self.verify_get_overrided( - self.proxy, message.Message, - 'openstack.message.v2.message.Message') + self.proxy, message.Message, 'openstack.message.v2.message.Message' + ) def test_messages(self): - self.verify_list(self.proxy.messages, message.Message, - method_kwargs={"queue_name": "test_queue"}, - expected_kwargs={"queue_name": "test_queue"}) + self.verify_list( + self.proxy.messages, + message.Message, + method_kwargs={"queue_name": "test_queue"}, + expected_kwargs={"queue_name": "test_queue"}, + ) @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_message_delete(self, mock_get_resource): @@ -92,11 +98,12 @@ def test_message_delete(self, mock_get_resource): self.proxy.delete_message, method_args=["test_queue", "resource_or_id", None, False], expected_args=[message.Message, fake_message], - expected_kwargs={"ignore_missing": False}) + expected_kwargs={"ignore_missing": False}, + ) self.assertIsNone(fake_message.claim_id) - mock_get_resource.assert_called_once_with(message.Message, - "resource_or_id", - queue_name="test_queue") + mock_get_resource.assert_called_once_with( + message.Message, "resource_or_id", queue_name="test_queue" + ) @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_message_delete_claimed(self, mock_get_resource): @@ -108,11 +115,12 @@ def test_message_delete_claimed(self, mock_get_resource): self.proxy.delete_message, method_args=["test_queue", "resource_or_id", "claim_id", False], expected_args=[message.Message, fake_message], - expected_kwargs={"ignore_missing": False}) + expected_kwargs={"ignore_missing": False}, + ) self.assertEqual("claim_id", fake_message.claim_id) - mock_get_resource.assert_called_once_with(message.Message, - "resource_or_id", - queue_name="test_queue") + mock_get_resource.assert_called_once_with( + message.Message, "resource_or_id", queue_name="test_queue" + ) @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_message_delete_ignore(self, mock_get_resource): @@ -124,11 +132,12 @@ def test_message_delete_ignore(self, mock_get_resource): self.proxy.delete_message, method_args=["test_queue", "resource_or_id", None, True], expected_args=[message.Message, fake_message], - expected_kwargs={"ignore_missing": True}) + expected_kwargs={"ignore_missing": True}, + ) self.assertIsNone(fake_message.claim_id) - mock_get_resource.assert_called_once_with(message.Message, - "resource_or_id", - queue_name="test_queue") + mock_get_resource.assert_called_once_with( + message.Message, "resource_or_id", queue_name="test_queue" + ) class TestMessageSubscription(TestMessageProxy): @@ -138,7 +147,8 @@ def test_subscription_create(self): self.proxy.create_subscription, method_args=["test_queue"], expected_args=[self.proxy], - expected_kwargs={"base_path": None}) + expected_kwargs={"base_path": None}, + ) @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_subscription_get(self, mock_get_resource): @@ -147,42 +157,58 @@ def test_subscription_get(self, mock_get_resource): "openstack.proxy.Proxy._get", self.proxy.get_subscription, method_args=["test_queue", "resource_or_id"], - expected_args=[subscription.Subscription, "resource_or_id"]) + expected_args=[subscription.Subscription, "resource_or_id"], + ) mock_get_resource.assert_called_once_with( - subscription.Subscription, "resource_or_id", - queue_name="test_queue") + subscription.Subscription, + "resource_or_id", + queue_name="test_queue", + ) self.verify_get_overrided( - self.proxy, subscription.Subscription, - 'openstack.message.v2.subscription.Subscription') + self.proxy, + subscription.Subscription, + 'openstack.message.v2.subscription.Subscription', + ) def test_subscriptions(self): - self.verify_list(self.proxy.subscriptions, subscription.Subscription, - method_kwargs={"queue_name": "test_queue"}, - expected_kwargs={"queue_name": "test_queue"}) + self.verify_list( + self.proxy.subscriptions, + subscription.Subscription, + method_kwargs={"queue_name": "test_queue"}, + expected_kwargs={"queue_name": "test_queue"}, + ) @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_subscription_delete(self, mock_get_resource): mock_get_resource.return_value = "test_subscription" - self.verify_delete(self.proxy.delete_subscription, - subscription.Subscription, - ignore_missing=False, - method_args=["test_queue", "resource_or_id"], - expected_args=["test_subscription"]) + self.verify_delete( + self.proxy.delete_subscription, + subscription.Subscription, + ignore_missing=False, + method_args=["test_queue", "resource_or_id"], + expected_args=["test_subscription"], + ) mock_get_resource.assert_called_once_with( - subscription.Subscription, "resource_or_id", - queue_name="test_queue") + subscription.Subscription, + "resource_or_id", + queue_name="test_queue", + ) @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_subscription_delete_ignore(self, mock_get_resource): mock_get_resource.return_value = "test_subscription" - self.verify_delete(self.proxy.delete_subscription, - subscription.Subscription, - ignore_missing=True, - method_args=["test_queue", "resource_or_id"], - expected_args=["test_subscription"]) + self.verify_delete( + self.proxy.delete_subscription, + subscription.Subscription, + ignore_missing=True, + method_args=["test_queue", "resource_or_id"], + expected_args=["test_subscription"], + ) mock_get_resource.assert_called_once_with( - subscription.Subscription, "resource_or_id", - queue_name="test_queue") + subscription.Subscription, + "resource_or_id", + queue_name="test_queue", + ) class TestMessageClaim(TestMessageProxy): @@ -192,7 +218,8 @@ def test_claim_create(self): self.proxy.create_claim, method_args=["test_queue"], expected_args=[self.proxy], - expected_kwargs={"base_path": None}) + expected_kwargs={"base_path": None}, + ) def test_claim_get(self): self._verify( @@ -200,10 +227,11 @@ def test_claim_get(self): self.proxy.get_claim, method_args=["test_queue", "resource_or_id"], expected_args=[claim.Claim, "resource_or_id"], - expected_kwargs={"queue_name": "test_queue"}) + expected_kwargs={"queue_name": "test_queue"}, + ) self.verify_get_overrided( - self.proxy, claim.Claim, - 'openstack.message.v2.claim.Claim') + self.proxy, claim.Claim, 'openstack.message.v2.claim.Claim' + ) def test_claim_update(self): self._verify( @@ -212,17 +240,21 @@ def test_claim_update(self): method_args=["test_queue", "resource_or_id"], method_kwargs={"k1": "v1"}, expected_args=[claim.Claim, "resource_or_id"], - expected_kwargs={"queue_name": "test_queue", "k1": "v1"}) + expected_kwargs={"queue_name": "test_queue", "k1": "v1"}, + ) def test_claim_delete(self): - self.verify_delete(self.proxy.delete_claim, - claim.Claim, - ignore_missing=False, - method_args=["test_queue", "test_claim"], - expected_args=["test_claim"], - expected_kwargs={ - "queue_name": "test_queue", - "ignore_missing": False}) + self.verify_delete( + self.proxy.delete_claim, + claim.Claim, + ignore_missing=False, + method_args=["test_queue", "test_claim"], + expected_args=["test_claim"], + expected_kwargs={ + "queue_name": "test_queue", + "ignore_missing": False, + }, + ) def test_claim_delete_ignore(self): self.verify_delete( @@ -232,5 +264,7 @@ def test_claim_delete_ignore(self): method_args=["test_queue", "test_claim"], expected_args=["test_claim"], expected_kwargs={ - "queue_name": "test_queue", "ignore_missing": True, - }) + "queue_name": "test_queue", + "ignore_missing": True, + }, + ) diff --git a/openstack/tests/unit/message/v2/test_queue.py b/openstack/tests/unit/message/v2/test_queue.py index 95ac8ec13..ca837d5ee 100644 --- a/openstack/tests/unit/message/v2/test_queue.py +++ b/openstack/tests/unit/message/v2/test_queue.py @@ -20,7 +20,7 @@ 'name': 'test_queue', 'description': 'Queue used for test.', '_default_message_ttl': 3600, - '_max_messages_post_size': 262144 + '_max_messages_post_size': 262144, } @@ -30,7 +30,7 @@ '_default_message_ttl': 3600, '_max_messages_post_size': 262144, 'client_id': 'OLD_CLIENT_ID', - 'project_id': 'OLD_PROJECT_ID' + 'project_id': 'OLD_PROJECT_ID', } @@ -49,10 +49,12 @@ def test_make_it(self): self.assertEqual(FAKE1['description'], sot.description) self.assertEqual(FAKE1['name'], sot.name) self.assertEqual(FAKE1['name'], sot.id) - self.assertEqual(FAKE1['_default_message_ttl'], - sot.default_message_ttl) - self.assertEqual(FAKE1['_max_messages_post_size'], - sot.max_messages_post_size) + self.assertEqual( + FAKE1['_default_message_ttl'], sot.default_message_ttl + ) + self.assertEqual( + FAKE1['_max_messages_post_size'], sot.max_messages_post_size + ) self.assertEqual(FAKE2['client_id'], sot.client_id) self.assertEqual(FAKE2['project_id'], sot.project_id) @@ -69,10 +71,11 @@ def test_create(self, mock_uuid): res = sot.create(sess) url = 'queues/%s' % FAKE1['name'] - headers = {'Client-ID': 'NEW_CLIENT_ID', - 'X-PROJECT-ID': 'NEW_PROJECT_ID'} - sess.put.assert_called_with(url, - headers=headers, json=FAKE1) + headers = { + 'Client-ID': 'NEW_CLIENT_ID', + 'X-PROJECT-ID': 'NEW_PROJECT_ID', + } + sess.put.assert_called_with(url, headers=headers, json=FAKE1) sess.get_project_id.assert_called_once_with() sot._translate_response.assert_called_once_with(resp, has_body=False) self.assertEqual(sot, res) @@ -87,10 +90,11 @@ def test_create_client_id_project_id_exist(self): res = sot.create(sess) url = 'queues/%s' % FAKE2['name'] - headers = {'Client-ID': 'OLD_CLIENT_ID', - 'X-PROJECT-ID': 'OLD_PROJECT_ID'} - sess.put.assert_called_with(url, - headers=headers, json=FAKE1) + headers = { + 'Client-ID': 'OLD_CLIENT_ID', + 'X-PROJECT-ID': 'OLD_PROJECT_ID', + } + sess.put.assert_called_with(url, headers=headers, json=FAKE1) sot._translate_response.assert_called_once_with(resp, has_body=False) self.assertEqual(sot, res) @@ -107,10 +111,11 @@ def test_get(self, mock_uuid): res = sot.fetch(sess) url = 'queues/%s' % FAKE1['name'] - headers = {'Client-ID': 'NEW_CLIENT_ID', - 'X-PROJECT-ID': 'NEW_PROJECT_ID'} - sess.get.assert_called_with( - url, headers=headers, skip_cache=False) + headers = { + 'Client-ID': 'NEW_CLIENT_ID', + 'X-PROJECT-ID': 'NEW_PROJECT_ID', + } + sess.get.assert_called_with(url, headers=headers, skip_cache=False) sess.get_project_id.assert_called_once_with() sot._translate_response.assert_called_once_with(resp) self.assertEqual(sot, res) @@ -125,10 +130,11 @@ def test_get_client_id_project_id_exist(self): res = sot.fetch(sess) url = 'queues/%s' % FAKE2['name'] - headers = {'Client-ID': 'OLD_CLIENT_ID', - 'X-PROJECT-ID': 'OLD_PROJECT_ID'} - sess.get.assert_called_with( - url, headers=headers, skip_cache=False) + headers = { + 'Client-ID': 'OLD_CLIENT_ID', + 'X-PROJECT-ID': 'OLD_PROJECT_ID', + } + sess.get.assert_called_with(url, headers=headers, skip_cache=False) sot._translate_response.assert_called_once_with(resp) self.assertEqual(sot, res) @@ -145,10 +151,11 @@ def test_delete(self, mock_uuid): sot.delete(sess) url = 'queues/%s' % FAKE1['name'] - headers = {'Client-ID': 'NEW_CLIENT_ID', - 'X-PROJECT-ID': 'NEW_PROJECT_ID'} - sess.delete.assert_called_with(url, - headers=headers) + headers = { + 'Client-ID': 'NEW_CLIENT_ID', + 'X-PROJECT-ID': 'NEW_PROJECT_ID', + } + sess.delete.assert_called_with(url, headers=headers) sess.get_project_id.assert_called_once_with() sot._translate_response.assert_called_once_with(resp, has_body=False) @@ -162,8 +169,9 @@ def test_delete_client_id_project_id_exist(self): sot.delete(sess) url = 'queues/%s' % FAKE2['name'] - headers = {'Client-ID': 'OLD_CLIENT_ID', - 'X-PROJECT-ID': 'OLD_PROJECT_ID'} - sess.delete.assert_called_with(url, - headers=headers) + headers = { + 'Client-ID': 'OLD_CLIENT_ID', + 'X-PROJECT-ID': 'OLD_PROJECT_ID', + } + sess.delete.assert_called_with(url, headers=headers) sot._translate_response.assert_called_once_with(resp, has_body=False) diff --git a/openstack/tests/unit/message/v2/test_subscription.py b/openstack/tests/unit/message/v2/test_subscription.py index 8e50c019d..1fb78a43d 100644 --- a/openstack/tests/unit/message/v2/test_subscription.py +++ b/openstack/tests/unit/message/v2/test_subscription.py @@ -25,10 +25,8 @@ "subscription_id": "576b54963990b48c644bb7e7", "source": "test", "ttl": 3600, - "options": { - "name": "test" - }, - "queue_name": "queue1" + "options": {"name": "test"}, + "queue_name": "queue1", } @@ -39,12 +37,10 @@ "subscription_id": "576b54963990b48c644bb7e7", "source": "test", "ttl": 3600, - "options": { - "name": "test" - }, + "options": {"name": "test"}, "queue_name": "queue1", "client_id": "OLD_CLIENT_ID", - "project_id": "OLD_PROJECT_ID" + "project_id": "OLD_PROJECT_ID", } @@ -85,11 +81,13 @@ def test_create(self, mock_uuid): res = sot.create(sess) url = "/queues/%(queue)s/subscriptions" % { - "queue": FAKE.pop("queue_name")} - headers = {"Client-ID": "NEW_CLIENT_ID", - "X-PROJECT-ID": "NEW_PROJECT_ID"} - sess.post.assert_called_once_with(url, - headers=headers, json=FAKE) + "queue": FAKE.pop("queue_name") + } + headers = { + "Client-ID": "NEW_CLIENT_ID", + "X-PROJECT-ID": "NEW_PROJECT_ID", + } + sess.post.assert_called_once_with(url, headers=headers, json=FAKE) sess.get_project_id.assert_called_once_with() self.assertEqual(sot, res) @@ -104,11 +102,13 @@ def test_create_client_id_project_id_exist(self): res = sot.create(sess) url = "/queues/%(queue)s/subscriptions" % { - "queue": FAKE.pop("queue_name")} - headers = {"Client-ID": FAKE.pop("client_id"), - "X-PROJECT-ID": FAKE.pop("project_id")} - sess.post.assert_called_once_with(url, - headers=headers, json=FAKE) + "queue": FAKE.pop("queue_name") + } + headers = { + "Client-ID": FAKE.pop("client_id"), + "X-PROJECT-ID": FAKE.pop("project_id"), + } + sess.post.assert_called_once_with(url, headers=headers, json=FAKE) self.assertEqual(sot, res) @mock.patch.object(uuid, "uuid4") @@ -124,11 +124,14 @@ def test_get(self, mock_uuid): res = sot.fetch(sess) url = "queues/%(queue)s/subscriptions/%(subscription)s" % { - "queue": FAKE1["queue_name"], "subscription": FAKE1["id"]} - headers = {"Client-ID": "NEW_CLIENT_ID", - "X-PROJECT-ID": "NEW_PROJECT_ID"} - sess.get.assert_called_with( - url, headers=headers, skip_cache=False) + "queue": FAKE1["queue_name"], + "subscription": FAKE1["id"], + } + headers = { + "Client-ID": "NEW_CLIENT_ID", + "X-PROJECT-ID": "NEW_PROJECT_ID", + } + sess.get.assert_called_with(url, headers=headers, skip_cache=False) sess.get_project_id.assert_called_once_with() sot._translate_response.assert_called_once_with(resp) self.assertEqual(sot, res) @@ -143,11 +146,14 @@ def test_get_client_id_project_id_exist(self): res = sot.fetch(sess) url = "queues/%(queue)s/subscriptions/%(subscription)s" % { - "queue": FAKE2["queue_name"], "subscription": FAKE2["id"]} - headers = {"Client-ID": "OLD_CLIENT_ID", - "X-PROJECT-ID": "OLD_PROJECT_ID"} - sess.get.assert_called_with( - url, headers=headers, skip_cache=False) + "queue": FAKE2["queue_name"], + "subscription": FAKE2["id"], + } + headers = { + "Client-ID": "OLD_CLIENT_ID", + "X-PROJECT-ID": "OLD_PROJECT_ID", + } + sess.get.assert_called_with(url, headers=headers, skip_cache=False) sot._translate_response.assert_called_once_with(resp) self.assertEqual(sot, res) @@ -164,11 +170,14 @@ def test_delete(self, mock_uuid): sot.delete(sess) url = "queues/%(queue)s/subscriptions/%(subscription)s" % { - "queue": FAKE1["queue_name"], "subscription": FAKE1["id"]} - headers = {"Client-ID": "NEW_CLIENT_ID", - "X-PROJECT-ID": "NEW_PROJECT_ID"} - sess.delete.assert_called_with(url, - headers=headers) + "queue": FAKE1["queue_name"], + "subscription": FAKE1["id"], + } + headers = { + "Client-ID": "NEW_CLIENT_ID", + "X-PROJECT-ID": "NEW_PROJECT_ID", + } + sess.delete.assert_called_with(url, headers=headers) sess.get_project_id.assert_called_once_with() sot._translate_response.assert_called_once_with(resp, has_body=False) @@ -182,9 +191,12 @@ def test_delete_client_id_project_id_exist(self): sot.delete(sess) url = "queues/%(queue)s/subscriptions/%(subscription)s" % { - "queue": FAKE2["queue_name"], "subscription": FAKE2["id"]} - headers = {"Client-ID": "OLD_CLIENT_ID", - "X-PROJECT-ID": "OLD_PROJECT_ID"} - sess.delete.assert_called_with(url, - headers=headers) + "queue": FAKE2["queue_name"], + "subscription": FAKE2["id"], + } + headers = { + "Client-ID": "OLD_CLIENT_ID", + "X-PROJECT-ID": "OLD_PROJECT_ID", + } + sess.delete.assert_called_with(url, headers=headers) sot._translate_response.assert_called_once_with(resp, has_body=False) From 19ec9ba383d14f4af6a1bb78dbbeaa6638ee8a4f Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 5 May 2023 11:06:30 +0100 Subject: [PATCH 3250/3836] Blackify openstack.database Black used with the '-l 79 -S' flags. A future change will ignore this commit in git-blame history by adding a 'git-blame-ignore-revs' file. Change-Id: If9eb7ba7a28b0bc56b7e9efc74760243b11e66ab Signed-off-by: Stephen Finucane --- openstack/database/v1/_proxy.py | 58 ++++++---- openstack/database/v1/instance.py | 8 +- openstack/database/v1/user.py | 5 +- .../tests/unit/database/v1/test_database.py | 1 - .../tests/unit/database/v1/test_flavor.py | 1 - .../tests/unit/database/v1/test_instance.py | 28 ++--- .../tests/unit/database/v1/test_proxy.py | 103 +++++++++++------- openstack/tests/unit/database/v1/test_user.py | 1 - 8 files changed, 128 insertions(+), 77 deletions(-) diff --git a/openstack/database/v1/_proxy.py b/openstack/database/v1/_proxy.py index 154debc31..4e3df801f 100644 --- a/openstack/database/v1/_proxy.py +++ b/openstack/database/v1/_proxy.py @@ -38,8 +38,9 @@ def create_database(self, instance, **attrs): :rtype: :class:`~openstack.database.v1.database.Database` """ instance = self._get_resource(_instance.Instance, instance) - return self._create(_database.Database, instance_id=instance.id, - **attrs) + return self._create( + _database.Database, instance_id=instance.id, **attrs + ) def delete_database(self, database, instance=None, ignore_missing=True): """Delete a database @@ -58,10 +59,15 @@ def delete_database(self, database, instance=None, ignore_missing=True): :returns: ``None`` """ - instance_id = self._get_uri_attribute(database, instance, - "instance_id") - self._delete(_database.Database, database, instance_id=instance_id, - ignore_missing=ignore_missing) + instance_id = self._get_uri_attribute( + database, instance, "instance_id" + ) + self._delete( + _database.Database, + database, + instance_id=instance_id, + ignore_missing=ignore_missing, + ) def find_database(self, name_or_id, instance, ignore_missing=True): """Find a single database @@ -77,9 +83,12 @@ def find_database(self, name_or_id, instance, ignore_missing=True): :returns: One :class:`~openstack.database.v1.database.Database` or None """ instance = self._get_resource(_instance.Instance, instance) - return self._find(_database.Database, name_or_id, - instance_id=instance.id, - ignore_missing=ignore_missing) + return self._find( + _database.Database, + name_or_id, + instance_id=instance.id, + ignore_missing=ignore_missing, + ) def databases(self, instance, **query): """Return a generator of databases @@ -124,8 +133,9 @@ def find_flavor(self, name_or_id, ignore_missing=True): attempting to find a nonexistent resource. :returns: One :class:`~openstack.database.v1.flavor.Flavor` or None """ - return self._find(_flavor.Flavor, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _flavor.Flavor, name_or_id, ignore_missing=ignore_missing + ) def get_flavor(self, flavor): """Get a single flavor @@ -175,8 +185,9 @@ def delete_instance(self, instance, ignore_missing=True): :returns: ``None`` """ - self._delete(_instance.Instance, instance, - ignore_missing=ignore_missing) + self._delete( + _instance.Instance, instance, ignore_missing=ignore_missing + ) def find_instance(self, name_or_id, ignore_missing=True): """Find a single instance @@ -189,8 +200,9 @@ def find_instance(self, name_or_id, ignore_missing=True): attempting to find a nonexistent resource. :returns: One :class:`~openstack.database.v1.instance.Instance` or None """ - return self._find(_instance.Instance, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _instance.Instance, name_or_id, ignore_missing=ignore_missing + ) def get_instance(self, instance): """Get a single instance @@ -262,8 +274,12 @@ def delete_user(self, user, instance=None, ignore_missing=True): :returns: ``None`` """ instance = self._get_resource(_instance.Instance, instance) - self._delete(_user.User, user, ignore_missing=ignore_missing, - instance_id=instance.id) + self._delete( + _user.User, + user, + ignore_missing=ignore_missing, + instance_id=instance.id, + ) def find_user(self, name_or_id, instance, ignore_missing=True): """Find a single user @@ -279,8 +295,12 @@ def find_user(self, name_or_id, instance, ignore_missing=True): :returns: One :class:`~openstack.database.v1.user.User` or None """ instance = self._get_resource(_instance.Instance, instance) - return self._find(_user.User, name_or_id, instance_id=instance.id, - ignore_missing=ignore_missing) + return self._find( + _user.User, + name_or_id, + instance_id=instance.id, + ignore_missing=ignore_missing, + ) def users(self, instance, **query): """Return a generator of users diff --git a/openstack/database/v1/instance.py b/openstack/database/v1/instance.py index 02c033bae..c24abaa4a 100644 --- a/openstack/database/v1/instance.py +++ b/openstack/database/v1/instance.py @@ -63,7 +63,9 @@ def enable_root_user(self, session): the login credentials. """ url = utils.urljoin(self.base_path, self.id, 'root') - resp = session.post(url,) + resp = session.post( + url, + ) return resp.json()['user'] def is_root_enabled(self, session): @@ -77,7 +79,9 @@ def is_root_enabled(self, session): instance or ``False`` otherwise. """ url = utils.urljoin(self.base_path, self.id, 'root') - resp = session.get(url,) + resp = session.get( + url, + ) return resp.json()['rootEnabled'] def restart(self, session): diff --git a/openstack/database/v1/user.py b/openstack/database/v1/user.py index 07e6fc72a..3e7d9a688 100644 --- a/openstack/database/v1/user.py +++ b/openstack/database/v1/user.py @@ -34,8 +34,9 @@ class User(resource.Resource): #: The password of the user password = resource.Body('password') - def _prepare_request(self, requires_id=True, prepend_key=True, - base_path=None, **kwargs): + def _prepare_request( + self, requires_id=True, prepend_key=True, base_path=None, **kwargs + ): """Prepare a request for the database service's create call User.create calls require the resources_key. diff --git a/openstack/tests/unit/database/v1/test_database.py b/openstack/tests/unit/database/v1/test_database.py index 63ac5270a..95781838c 100644 --- a/openstack/tests/unit/database/v1/test_database.py +++ b/openstack/tests/unit/database/v1/test_database.py @@ -25,7 +25,6 @@ class TestDatabase(base.TestCase): - def test_basic(self): sot = database.Database() self.assertEqual('database', sot.resource_key) diff --git a/openstack/tests/unit/database/v1/test_flavor.py b/openstack/tests/unit/database/v1/test_flavor.py index ef5f0f9b4..a6dacb4bf 100644 --- a/openstack/tests/unit/database/v1/test_flavor.py +++ b/openstack/tests/unit/database/v1/test_flavor.py @@ -24,7 +24,6 @@ class TestFlavor(base.TestCase): - def test_basic(self): sot = flavor.Flavor() self.assertEqual('flavor', sot.resource_key) diff --git a/openstack/tests/unit/database/v1/test_instance.py b/openstack/tests/unit/database/v1/test_instance.py index ef67afc3f..0a329d379 100644 --- a/openstack/tests/unit/database/v1/test_instance.py +++ b/openstack/tests/unit/database/v1/test_instance.py @@ -32,7 +32,6 @@ class TestInstance(base.TestCase): - def test_basic(self): sot = instance.Instance() self.assertEqual('instance', sot.resource_key) @@ -68,8 +67,10 @@ def test_enable_root_user(self): self.assertEqual(response.body['user'], sot.enable_root_user(sess)) - url = ("instances/%s/root" % IDENTIFIER) - sess.post.assert_called_with(url,) + url = "instances/%s/root" % IDENTIFIER + sess.post.assert_called_with( + url, + ) def test_is_root_enabled(self): sot = instance.Instance(**EXAMPLE) @@ -81,8 +82,10 @@ def test_is_root_enabled(self): self.assertTrue(sot.is_root_enabled(sess)) - url = ("instances/%s/root" % IDENTIFIER) - sess.get.assert_called_with(url,) + url = "instances/%s/root" % IDENTIFIER + sess.get.assert_called_with( + url, + ) def test_action_restart(self): sot = instance.Instance(**EXAMPLE) @@ -93,10 +96,9 @@ def test_action_restart(self): self.assertIsNone(sot.restart(sess)) - url = ("instances/%s/action" % IDENTIFIER) + url = "instances/%s/action" % IDENTIFIER body = {'restart': {}} - sess.post.assert_called_with(url, - json=body) + sess.post.assert_called_with(url, json=body) def test_action_resize(self): sot = instance.Instance(**EXAMPLE) @@ -108,10 +110,9 @@ def test_action_resize(self): self.assertIsNone(sot.resize(sess, flavor)) - url = ("instances/%s/action" % IDENTIFIER) + url = "instances/%s/action" % IDENTIFIER body = {'resize': {'flavorRef': flavor}} - sess.post.assert_called_with(url, - json=body) + sess.post.assert_called_with(url, json=body) def test_action_resize_volume(self): sot = instance.Instance(**EXAMPLE) @@ -123,7 +124,6 @@ def test_action_resize_volume(self): self.assertIsNone(sot.resize_volume(sess, size)) - url = ("instances/%s/action" % IDENTIFIER) + url = "instances/%s/action" % IDENTIFIER body = {'resize': {'volume': size}} - sess.post.assert_called_with(url, - json=body) + sess.post.assert_called_with(url, json=body) diff --git a/openstack/tests/unit/database/v1/test_proxy.py b/openstack/tests/unit/database/v1/test_proxy.py index 81f276dd8..98c641d5c 100644 --- a/openstack/tests/unit/database/v1/test_proxy.py +++ b/openstack/tests/unit/database/v1/test_proxy.py @@ -24,24 +24,30 @@ def setUp(self): self.proxy = _proxy.Proxy(self.session) def test_database_create_attrs(self): - self.verify_create(self.proxy.create_database, - database.Database, - method_kwargs={"instance": "id"}, - expected_kwargs={"instance_id": "id"}) + self.verify_create( + self.proxy.create_database, + database.Database, + method_kwargs={"instance": "id"}, + expected_kwargs={"instance_id": "id"}, + ) def test_database_delete(self): - self.verify_delete(self.proxy.delete_database, - database.Database, - ignore_missing=False, - method_kwargs={"instance": "test_id"}, - expected_kwargs={"instance_id": "test_id"}) + self.verify_delete( + self.proxy.delete_database, + database.Database, + ignore_missing=False, + method_kwargs={"instance": "test_id"}, + expected_kwargs={"instance_id": "test_id"}, + ) def test_database_delete_ignore(self): - self.verify_delete(self.proxy.delete_database, - database.Database, - ignore_missing=True, - method_kwargs={"instance": "test_id"}, - expected_kwargs={"instance_id": "test_id"}) + self.verify_delete( + self.proxy.delete_database, + database.Database, + ignore_missing=True, + method_kwargs={"instance": "test_id"}, + expected_kwargs={"instance_id": "test_id"}, + ) def test_database_find(self): self._verify( @@ -50,13 +56,19 @@ def test_database_find(self): method_args=["db", "instance"], expected_args=[database.Database, "db"], expected_kwargs={ - "instance_id": "instance", "ignore_missing": True}) + "instance_id": "instance", + "ignore_missing": True, + }, + ) def test_databases(self): - self.verify_list(self.proxy.databases, database.Database, - method_args=["id"], - expected_args=[], - expected_kwargs={"instance_id": "id"}) + self.verify_list( + self.proxy.databases, + database.Database, + method_args=["id"], + expected_args=[], + expected_kwargs={"instance_id": "id"}, + ) def test_database_get(self): self.verify_get(self.proxy.get_database, database.Database) @@ -74,12 +86,12 @@ def test_instance_create_attrs(self): self.verify_create(self.proxy.create_instance, instance.Instance) def test_instance_delete(self): - self.verify_delete(self.proxy.delete_instance, - instance.Instance, False) + self.verify_delete( + self.proxy.delete_instance, instance.Instance, False + ) def test_instance_delete_ignore(self): - self.verify_delete(self.proxy.delete_instance, - instance.Instance, True) + self.verify_delete(self.proxy.delete_instance, instance.Instance, True) def test_instance_find(self): self.verify_find(self.proxy.find_instance, instance.Instance) @@ -94,19 +106,30 @@ def test_instance_update(self): self.verify_update(self.proxy.update_instance, instance.Instance) def test_user_create_attrs(self): - self.verify_create(self.proxy.create_user, user.User, - method_kwargs={"instance": "id"}, - expected_kwargs={"instance_id": "id"}) + self.verify_create( + self.proxy.create_user, + user.User, + method_kwargs={"instance": "id"}, + expected_kwargs={"instance_id": "id"}, + ) def test_user_delete(self): - self.verify_delete(self.proxy.delete_user, user.User, False, - method_kwargs={"instance": "id"}, - expected_kwargs={"instance_id": "id"}) + self.verify_delete( + self.proxy.delete_user, + user.User, + False, + method_kwargs={"instance": "id"}, + expected_kwargs={"instance_id": "id"}, + ) def test_user_delete_ignore(self): - self.verify_delete(self.proxy.delete_user, user.User, True, - method_kwargs={"instance": "id"}, - expected_kwargs={"instance_id": "id"}) + self.verify_delete( + self.proxy.delete_user, + user.User, + True, + method_kwargs={"instance": "id"}, + expected_kwargs={"instance_id": "id"}, + ) def test_user_find(self): self._verify( @@ -115,13 +138,19 @@ def test_user_find(self): method_args=["user", "instance"], expected_args=[user.User, "user"], expected_kwargs={ - "instance_id": "instance", "ignore_missing": True}) + "instance_id": "instance", + "ignore_missing": True, + }, + ) def test_users(self): - self.verify_list(self.proxy.users, user.User, - method_args=["test_instance"], - expected_args=[], - expected_kwargs={"instance_id": "test_instance"}) + self.verify_list( + self.proxy.users, + user.User, + method_args=["test_instance"], + expected_args=[], + expected_kwargs={"instance_id": "test_instance"}, + ) def test_user_get(self): self.verify_get(self.proxy.get_user, user.User) diff --git a/openstack/tests/unit/database/v1/test_user.py b/openstack/tests/unit/database/v1/test_user.py index 6e37ff4a3..7d5ce73ae 100644 --- a/openstack/tests/unit/database/v1/test_user.py +++ b/openstack/tests/unit/database/v1/test_user.py @@ -24,7 +24,6 @@ class TestUser(base.TestCase): - def test_basic(self): sot = user.User() self.assertEqual('user', sot.resource_key) From 10018dbf5be5e19c87543a5931f6809006eba4c5 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 5 May 2023 11:03:20 +0100 Subject: [PATCH 3251/3836] Blackify openstack.dns Black used with the '-l 79 -S' flags. A future change will ignore this commit in git-blame history by adding a 'git-blame-ignore-revs' file. Change-Id: I5c62d40f41b4632e9f8c59c505a46f8b11866f67 Signed-off-by: Stephen Finucane --- openstack/dns/v2/_base.py | 15 +- openstack/dns/v2/_proxy.py | 121 ++++++---- openstack/dns/v2/floating_ip.py | 1 + openstack/dns/v2/recordset.py | 12 +- openstack/dns/v2/zone.py | 19 +- openstack/dns/v2/zone_export.py | 18 +- openstack/dns/v2/zone_import.py | 18 +- openstack/dns/v2/zone_transfer.py | 6 +- .../tests/functional/dns/v2/test_zone.py | 35 +-- .../functional/dns/v2/test_zone_share.py | 90 ++++--- openstack/tests/unit/dns/test_version.py | 1 - .../tests/unit/dns/v2/test_floating_ip.py | 7 +- openstack/tests/unit/dns/v2/test_proxy.py | 227 +++++++++++------- openstack/tests/unit/dns/v2/test_recordset.py | 29 +-- openstack/tests/unit/dns/v2/test_zone.py | 32 +-- .../tests/unit/dns/v2/test_zone_export.py | 27 ++- .../tests/unit/dns/v2/test_zone_import.py | 27 ++- .../tests/unit/dns/v2/test_zone_share.py | 15 +- .../tests/unit/dns/v2/test_zone_transfer.py | 31 +-- 19 files changed, 438 insertions(+), 293 deletions(-) diff --git a/openstack/dns/v2/_base.py b/openstack/dns/v2/_base.py index ae930853b..791d9d537 100644 --- a/openstack/dns/v2/_base.py +++ b/openstack/dns/v2/_base.py @@ -16,7 +16,6 @@ class Resource(resource.Resource): - @classmethod def find(cls, session, name_or_id, ignore_missing=True, **params): """Find a resource by its name or id. @@ -46,16 +45,17 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): # Try to short-circuit by looking directly for a matching ID. try: match = cls.existing( - id=name_or_id, - connection=session._get_connection(), - **params) + id=name_or_id, connection=session._get_connection(), **params + ) return match.fetch(session) except exceptions.SDKException: # DNS may return 400 when we try to do GET with name pass - if ('name' in cls._query_mapping._mapping.keys() - and 'name' not in params): + if ( + 'name' in cls._query_mapping._mapping.keys() + and 'name' not in params + ): params['name'] = name_or_id data = cls.list(session, **params) @@ -67,7 +67,8 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): if ignore_missing: return None raise exceptions.ResourceNotFound( - "No %s found for %s" % (cls.__name__, name_or_id)) + "No %s found for %s" % (cls.__name__, name_or_id) + ) @classmethod def _get_next_link(cls, uri, response, data, marker, limit, total_yielded): diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py index 85bc1cbb0..174093e99 100644 --- a/openstack/dns/v2/_proxy.py +++ b/openstack/dns/v2/_proxy.py @@ -87,8 +87,12 @@ def delete_zone(self, zone, ignore_missing=True, delete_shares=False): :returns: Zone been deleted :rtype: :class:`~openstack.dns.v2.zone.Zone` """ - return self._delete(_zone.Zone, zone, ignore_missing=ignore_missing, - delete_shares=delete_shares) + return self._delete( + _zone.Zone, + zone, + ignore_missing=ignore_missing, + delete_shares=delete_shares, + ) def update_zone(self, zone, **attrs): """Update zone attributes @@ -114,8 +118,9 @@ def find_zone(self, name_or_id, ignore_missing=True): :returns: :class:`~openstack.dns.v2.zone.Zone` """ - return self._find(_zone.Zone, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _zone.Zone, name_or_id, ignore_missing=ignore_missing + ) def abandon_zone(self, zone, **attrs): """Abandon Zone @@ -225,10 +230,10 @@ def delete_recordset(self, recordset, zone=None, ignore_missing=True): """ if zone: zone = self._get_resource(_zone.Zone, zone) - recordset = self._get( - _rs.Recordset, recordset, zone_id=zone.id) - return self._delete(_rs.Recordset, recordset, - ignore_missing=ignore_missing) + recordset = self._get(_rs.Recordset, recordset, zone_id=zone.id) + return self._delete( + _rs.Recordset, recordset, ignore_missing=ignore_missing + ) def find_recordset(self, zone, name_or_id, ignore_missing=True, **query): """Find a single recordset @@ -278,8 +283,9 @@ def create_zone_import(self, **attrs): :returns: The results of zone creation. :rtype: :class:`~openstack.dns.v2.zone_import.ZoneImport` """ - return self._create(_zone_import.ZoneImport, prepend_key=False, - **attrs) + return self._create( + _zone_import.ZoneImport, prepend_key=False, **attrs + ) def get_zone_import(self, zone_import): """Get a zone import record @@ -304,8 +310,9 @@ def delete_zone_import(self, zone_import, ignore_missing=True): :returns: None """ - return self._delete(_zone_import.ZoneImport, zone_import, - ignore_missing=ignore_missing) + return self._delete( + _zone_import.ZoneImport, zone_import, ignore_missing=ignore_missing + ) # ======== Zone Exports ======== def zone_exports(self, **query): @@ -335,11 +342,13 @@ def create_zone_export(self, zone, **attrs): :rtype: :class:`~openstack.dns.v2.zone_export.ZoneExport` """ zone = self._get_resource(_zone.Zone, zone) - return self._create(_zone_export.ZoneExport, - base_path='/zones/%(zone_id)s/tasks/export', - prepend_key=False, - zone_id=zone.id, - **attrs) + return self._create( + _zone_export.ZoneExport, + base_path='/zones/%(zone_id)s/tasks/export', + prepend_key=False, + zone_id=zone.id, + **attrs, + ) def get_zone_export(self, zone_export): """Get a zone export record @@ -359,8 +368,11 @@ def get_zone_export_text(self, zone_export): :returns: ZoneExport instance. :rtype: :class:`~openstack.dns.v2.zone_export.ZoneExport` """ - return self._get(_zone_export.ZoneExport, zone_export, - base_path='/zones/tasks/export/%(id)s/export') + return self._get( + _zone_export.ZoneExport, + zone_export, + base_path='/zones/tasks/export/%(id)s/export', + ) def delete_zone_export(self, zone_export, ignore_missing=True): """Delete a zone export @@ -375,8 +387,9 @@ def delete_zone_export(self, zone_export, ignore_missing=True): :returns: None """ - return self._delete(_zone_export.ZoneExport, zone_export, - ignore_missing=ignore_missing) + return self._delete( + _zone_export.ZoneExport, zone_export, ignore_missing=ignore_missing + ) # ======== FloatingIPs ======== def floating_ips(self, **query): @@ -473,7 +486,8 @@ def create_zone_transfer_request(self, zone, **attrs): base_path='/zones/%(zone_id)s/tasks/transfer_requests', prepend_key=False, zone_id=zone.id, - **attrs) + **attrs, + ) def update_zone_transfer_request(self, request, **attrs): """Update ZoneTransfer Request attributes @@ -485,8 +499,9 @@ def update_zone_transfer_request(self, request, **attrs): :rtype: :class:`~openstack.dns.v2.zone_transfer.ZoneTransferRequest` """ - return self._update(_zone_transfer.ZoneTransferRequest, - request, **attrs) + return self._update( + _zone_transfer.ZoneTransferRequest, request, **attrs + ) def delete_zone_transfer_request(self, request, ignore_missing=True): """Delete a ZoneTransfer Request @@ -502,8 +517,11 @@ def delete_zone_transfer_request(self, request, ignore_missing=True): :returns: None """ - return self._delete(_zone_transfer.ZoneTransferRequest, request, - ignore_missing=ignore_missing) + return self._delete( + _zone_transfer.ZoneTransferRequest, + request, + ignore_missing=ignore_missing, + ) def zone_transfer_accepts(self, **query): """Retrieve a generator of zone transfer accepts @@ -571,8 +589,9 @@ def get_zone_share(self, zone, zone_share): :rtype: :class:`~openstack.dns.v2.zone_share.ZoneShare` """ zone_obj = self._get_resource(_zone.Zone, zone) - return self._get(_zone_share.ZoneShare, zone_share, - zone_id=zone_obj.id) + return self._get( + _zone_share.ZoneShare, zone_share, zone_id=zone_obj.id + ) def find_zone_share(self, zone, zone_share_id, ignore_missing=True): """Find a single zone share @@ -589,8 +608,12 @@ def find_zone_share(self, zone, zone_share_id, ignore_missing=True): :returns: :class:`~openstack.dns.v2.zone_share.ZoneShare` """ zone_obj = self._get_resource(_zone.Zone, zone) - return self._find(_zone_share.ZoneShare, zone_share_id, - ignore_missing=ignore_missing, zone_id=zone_obj.id) + return self._find( + _zone_share.ZoneShare, + zone_share_id, + ignore_missing=ignore_missing, + zone_id=zone_obj.id, + ) def create_zone_share(self, zone, **attrs): """Create a new zone share from attributes @@ -605,8 +628,9 @@ def create_zone_share(self, zone, **attrs): :rtype: :class:`~openstack.dns.v2.zone_share.ZoneShare` """ zone_obj = self._get_resource(_zone.Zone, zone) - return self._create(_zone_share.ZoneShare, zone_id=zone_obj.id, - **attrs) + return self._create( + _zone_share.ZoneShare, zone_id=zone_obj.id, **attrs + ) def delete_zone_share(self, zone, zone_share, ignore_missing=True): """Delete a zone share @@ -625,20 +649,25 @@ def delete_zone_share(self, zone, zone_share, ignore_missing=True): :returns: ``None`` """ zone_obj = self._get_resource(_zone.Zone, zone) - self._delete(_zone_share.ZoneShare, zone_share, - ignore_missing=ignore_missing, zone_id=zone_obj.id) + self._delete( + _zone_share.ZoneShare, + zone_share, + ignore_missing=ignore_missing, + zone_id=zone_obj.id, + ) def _get_cleanup_dependencies(self): # DNS may depend on floating ip - return { - 'dns': { - 'before': ['network'] - } - } - - def _service_cleanup(self, dry_run=True, client_status_queue=False, - identified_resources=None, - filters=None, resource_evaluation_fn=None): + return {'dns': {'before': ['network']}} + + def _service_cleanup( + self, + dry_run=True, + client_status_queue=False, + identified_resources=None, + filters=None, + resource_evaluation_fn=None, + ): # Delete all zones for obj in self.zones(): self._service_cleanup_del_res( @@ -648,7 +677,8 @@ def _service_cleanup(self, dry_run=True, client_status_queue=False, client_status_queue=client_status_queue, identified_resources=identified_resources, filters=filters, - resource_evaluation_fn=resource_evaluation_fn) + resource_evaluation_fn=resource_evaluation_fn, + ) # Unset all floatingIPs # NOTE: FloatingIPs are not cleaned when filters are set for obj in self.floating_ips(): @@ -659,4 +689,5 @@ def _service_cleanup(self, dry_run=True, client_status_queue=False, client_status_queue=client_status_queue, identified_resources=identified_resources, filters=filters, - resource_evaluation_fn=resource_evaluation_fn) + resource_evaluation_fn=resource_evaluation_fn, + ) diff --git a/openstack/dns/v2/floating_ip.py b/openstack/dns/v2/floating_ip.py index cc7487b43..a79f88d71 100644 --- a/openstack/dns/v2/floating_ip.py +++ b/openstack/dns/v2/floating_ip.py @@ -16,6 +16,7 @@ class FloatingIP(_base.Resource): """DNS Floating IP Resource""" + resource_key = '' resources_key = 'floatingips' base_path = '/reverse/floatingips' diff --git a/openstack/dns/v2/recordset.py b/openstack/dns/v2/recordset.py index 9688657f9..09f37b4f4 100644 --- a/openstack/dns/v2/recordset.py +++ b/openstack/dns/v2/recordset.py @@ -16,6 +16,7 @@ class Recordset(_base.Resource): """DNS Recordset Resource""" + resources_key = 'recordsets' base_path = '/zones/%(zone_id)s/recordsets' @@ -27,8 +28,15 @@ class Recordset(_base.Resource): allow_list = True _query_mapping = resource.QueryParameters( - 'name', 'type', 'ttl', 'data', 'status', 'description', - 'limit', 'marker') + 'name', + 'type', + 'ttl', + 'data', + 'status', + 'description', + 'limit', + 'marker', + ) #: Properties #: current action in progress on the resource diff --git a/openstack/dns/v2/zone.py b/openstack/dns/v2/zone.py index c794d9e0b..a868da915 100644 --- a/openstack/dns/v2/zone.py +++ b/openstack/dns/v2/zone.py @@ -18,6 +18,7 @@ class Zone(_base.Resource): """DNS ZONE Resource""" + resources_key = 'zones' base_path = '/zones' @@ -31,8 +32,14 @@ class Zone(_base.Resource): commit_method = "PATCH" _query_mapping = resource.QueryParameters( - 'name', 'type', 'email', 'status', 'description', 'ttl', - 'limit', 'marker' + 'name', + 'type', + 'email', + 'status', + 'description', + 'ttl', + 'limit', + 'marker', ) #: Properties @@ -88,13 +95,9 @@ class Zone(_base.Resource): delete_shares = resource.Header('x-designate-delete-shares', type=bool) def _action(self, session, action, body): - """Preform actions given the message body. - - """ + """Preform actions given the message body.""" url = utils.urljoin(self.base_path, self.id, 'tasks', action) - response = session.post( - url, - json=body) + response = session.post(url, json=body) exceptions.raise_from_response(response) return response diff --git a/openstack/dns/v2/zone_export.py b/openstack/dns/v2/zone_export.py index 325051200..18cb4b1ac 100644 --- a/openstack/dns/v2/zone_export.py +++ b/openstack/dns/v2/zone_export.py @@ -17,6 +17,7 @@ class ZoneExport(_base.Resource): """DNS Zone Exports Resource""" + resource_key = '' resources_key = 'exports' base_path = '/zones/tasks/export' @@ -27,9 +28,7 @@ class ZoneExport(_base.Resource): allow_delete = True allow_list = True - _query_mapping = resource.QueryParameters( - 'zone_id', 'message', 'status' - ) + _query_mapping = resource.QueryParameters('zone_id', 'message', 'status') #: Properties #: Timestamp when the zone was created @@ -74,14 +73,13 @@ def create(self, session, prepend_key=True, base_path=None): microversion = self._get_microversion(session, action='create') # Create ZoneExport requires empty body # skip _prepare_request completely, since we need just empty body - request = resource._Request( - self.base_path, - None, - None + request = resource._Request(self.base_path, None, None) + response = session.post( + request.url, + json=request.body, + headers=request.headers, + microversion=microversion, ) - response = session.post(request.url, - json=request.body, headers=request.headers, - microversion=microversion) self.microversion = microversion self._translate_response(response) diff --git a/openstack/dns/v2/zone_import.py b/openstack/dns/v2/zone_import.py index 497832ffe..1ed3153bc 100644 --- a/openstack/dns/v2/zone_import.py +++ b/openstack/dns/v2/zone_import.py @@ -17,6 +17,7 @@ class ZoneImport(_base.Resource): """DNS Zone Import Resource""" + resource_key = '' resources_key = 'imports' base_path = '/zones/tasks/import' @@ -27,9 +28,7 @@ class ZoneImport(_base.Resource): allow_delete = True allow_list = True - _query_mapping = resource.QueryParameters( - 'zone_id', 'message', 'status' - ) + _query_mapping = resource.QueryParameters('zone_id', 'message', 'status') #: Properties #: Timestamp when the zone was created @@ -75,13 +74,14 @@ def create(self, session, prepend_key=True, base_path=None): # Create ZoneImport requires empty body and 'text/dns' as content-type # skip _prepare_request completely, since we need just empty body request = resource._Request( - self.base_path, - None, - {'content-type': 'text/dns'} + self.base_path, None, {'content-type': 'text/dns'} + ) + response = session.post( + request.url, + json=request.body, + headers=request.headers, + microversion=microversion, ) - response = session.post(request.url, - json=request.body, headers=request.headers, - microversion=microversion) self.microversion = microversion self._translate_response(response) diff --git a/openstack/dns/v2/zone_transfer.py b/openstack/dns/v2/zone_transfer.py index 417df265d..0c840184c 100644 --- a/openstack/dns/v2/zone_transfer.py +++ b/openstack/dns/v2/zone_transfer.py @@ -17,9 +17,7 @@ class ZoneTransferBase(_base.Resource): """DNS Zone Transfer Request/Accept Base Resource""" - _query_mapping = resource.QueryParameters( - 'status' - ) + _query_mapping = resource.QueryParameters('status') #: Properties #: Timestamp when the resource was created @@ -41,6 +39,7 @@ class ZoneTransferBase(_base.Resource): class ZoneTransferRequest(ZoneTransferBase): """DNS Zone Transfer Request Resource""" + base_path = '/zones/tasks/transfer_requests' resources_key = 'transfer_requests' @@ -62,6 +61,7 @@ class ZoneTransferRequest(ZoneTransferBase): class ZoneTransferAccept(ZoneTransferBase): """DNS Zone Transfer Accept Resource""" + base_path = '/zones/tasks/transfer_accepts' resources_key = 'transfer_accepts' diff --git a/openstack/tests/functional/dns/v2/test_zone.py b/openstack/tests/functional/dns/v2/test_zone.py index 147b161f7..7cea55aa0 100644 --- a/openstack/tests/functional/dns/v2/test_zone.py +++ b/openstack/tests/functional/dns/v2/test_zone.py @@ -17,7 +17,6 @@ class TestZone(base.BaseFunctionalTest): - def setUp(self): super(TestZone, self).setUp() self.require_service('dns') @@ -35,7 +34,7 @@ def setUp(self): email='joe@example.org', type='PRIMARY', ttl=7200, - description='example zone' + description='example zone', ) self.addCleanup(self.conn.dns.delete_zone, self.zone) @@ -55,18 +54,22 @@ def test_update_zone(self): current_ttl + 1, updated_zone_ttl, 'Failed, updated TTL value is:{} instead of expected:{}'.format( - updated_zone_ttl, current_ttl + 1)) + updated_zone_ttl, current_ttl + 1 + ), + ) def test_create_rs(self): zone = self.conn.dns.get_zone(self.zone) - self.assertIsNotNone(self.conn.dns.create_recordset( - zone=zone, - name='www.{zone}'.format(zone=zone.name), - type='A', - description='Example zone rec', - ttl=3600, - records=['192.168.1.1'] - )) + self.assertIsNotNone( + self.conn.dns.create_recordset( + zone=zone, + name='www.{zone}'.format(zone=zone.name), + type='A', + description='Example zone rec', + ttl=3600, + records=['192.168.1.1'], + ) + ) def test_delete_zone_with_shares(self): zone_name = 'example-{0}.org.'.format(random.randint(1, 10000)) @@ -75,17 +78,19 @@ def test_delete_zone_with_shares(self): email='joe@example.org', type='PRIMARY', ttl=7200, - description='example zone' + description='example zone', ) self.addCleanup(self.conn.dns.delete_zone, zone) demo_project_id = self.operator_cloud.get_project('demo')['id'] zone_share = self.conn.dns.create_zone_share( - zone, target_project_id=demo_project_id) + zone, target_project_id=demo_project_id + ) self.addCleanup(self.conn.dns.delete_zone_share, zone, zone_share) # Test that we cannot delete a zone with shares - self.assertRaises(exceptions.BadRequestException, - self.conn.dns.delete_zone, zone) + self.assertRaises( + exceptions.BadRequestException, self.conn.dns.delete_zone, zone + ) self.conn.dns.delete_zone(zone, delete_shares=True) diff --git a/openstack/tests/functional/dns/v2/test_zone_share.py b/openstack/tests/functional/dns/v2/test_zone_share.py index 1473ae46d..51980970c 100644 --- a/openstack/tests/functional/dns/v2/test_zone_share.py +++ b/openstack/tests/functional/dns/v2/test_zone_share.py @@ -16,7 +16,6 @@ class TestZoneShare(base.BaseFunctionalTest): - def setUp(self): super(TestZoneShare, self).setUp() self.require_service('dns') @@ -34,19 +33,22 @@ def setUp(self): email='joe@example.org', type='PRIMARY', ttl=7200, - description='example zone for sdk zone share tests' + description='example zone for sdk zone share tests', + ) + self.addCleanup( + self.operator_cloud.dns.delete_zone, self.zone, delete_shares=True ) - self.addCleanup(self.operator_cloud.dns.delete_zone, self.zone, - delete_shares=True) self.project_id = self.operator_cloud.session.get_project_id() self.demo_project_id = self.user_cloud.session.get_project_id() def test_create_delete_zone_share(self): zone_share = self.operator_cloud.dns.create_zone_share( - self.zone, target_project_id=self.demo_project_id) - self.addCleanup(self.operator_cloud.dns.delete_zone_share, self.zone, - zone_share) + self.zone, target_project_id=self.demo_project_id + ) + self.addCleanup( + self.operator_cloud.dns.delete_zone_share, self.zone, zone_share + ) self.assertEqual(self.zone.id, zone_share.zone_id) self.assertEqual(self.project_id, zone_share.project_id) @@ -57,12 +59,17 @@ def test_create_delete_zone_share(self): def test_get_zone_share(self): orig_zone_share = self.operator_cloud.dns.create_zone_share( - self.zone, target_project_id=self.demo_project_id) - self.addCleanup(self.operator_cloud.dns.delete_zone_share, self.zone, - orig_zone_share) + self.zone, target_project_id=self.demo_project_id + ) + self.addCleanup( + self.operator_cloud.dns.delete_zone_share, + self.zone, + orig_zone_share, + ) - zone_share = self.operator_cloud.dns.get_zone_share(self.zone, - orig_zone_share) + zone_share = self.operator_cloud.dns.get_zone_share( + self.zone, orig_zone_share + ) self.assertEqual(self.zone.id, zone_share.zone_id) self.assertEqual(self.project_id, zone_share.project_id) @@ -73,12 +80,17 @@ def test_get_zone_share(self): def test_find_zone_share(self): orig_zone_share = self.operator_cloud.dns.create_zone_share( - self.zone, target_project_id=self.demo_project_id) - self.addCleanup(self.operator_cloud.dns.delete_zone_share, self.zone, - orig_zone_share) + self.zone, target_project_id=self.demo_project_id + ) + self.addCleanup( + self.operator_cloud.dns.delete_zone_share, + self.zone, + orig_zone_share, + ) zone_share = self.operator_cloud.dns.find_zone_share( - self.zone, orig_zone_share.id) + self.zone, orig_zone_share.id + ) self.assertEqual(self.zone.id, zone_share.zone_id) self.assertEqual(self.project_id, zone_share.project_id) @@ -88,32 +100,46 @@ def test_find_zone_share(self): self.assertEqual(orig_zone_share.updated_at, zone_share.updated_at) def test_find_zone_share_ignore_missing(self): - zone_share = self.operator_cloud.dns.find_zone_share(self.zone, - 'bogus_id') + zone_share = self.operator_cloud.dns.find_zone_share( + self.zone, 'bogus_id' + ) self.assertIsNone(zone_share) def test_find_zone_share_ignore_missing_false(self): - self.assertRaises(exceptions.ResourceNotFound, - self.operator_cloud.dns.find_zone_share, - self.zone, 'bogus_id', ignore_missing=False) + self.assertRaises( + exceptions.ResourceNotFound, + self.operator_cloud.dns.find_zone_share, + self.zone, + 'bogus_id', + ignore_missing=False, + ) def test_list_zone_shares(self): zone_share = self.operator_cloud.dns.create_zone_share( - self.zone, target_project_id=self.demo_project_id) - self.addCleanup(self.operator_cloud.dns.delete_zone_share, self.zone, - zone_share) + self.zone, target_project_id=self.demo_project_id + ) + self.addCleanup( + self.operator_cloud.dns.delete_zone_share, self.zone, zone_share + ) - target_ids = [o.target_project_id for o in - self.operator_cloud.dns.zone_shares(self.zone)] + target_ids = [ + o.target_project_id + for o in self.operator_cloud.dns.zone_shares(self.zone) + ] self.assertIn(self.demo_project_id, target_ids) def test_list_zone_shares_with_target_id(self): zone_share = self.operator_cloud.dns.create_zone_share( - self.zone, target_project_id=self.demo_project_id) - self.addCleanup(self.operator_cloud.dns.delete_zone_share, self.zone, - zone_share) + self.zone, target_project_id=self.demo_project_id + ) + self.addCleanup( + self.operator_cloud.dns.delete_zone_share, self.zone, zone_share + ) - target_ids = [o.target_project_id for o in - self.operator_cloud.dns.zone_shares( - self.zone, target_project_id=self.demo_project_id)] + target_ids = [ + o.target_project_id + for o in self.operator_cloud.dns.zone_shares( + self.zone, target_project_id=self.demo_project_id + ) + ] self.assertIn(self.demo_project_id, target_ids) diff --git a/openstack/tests/unit/dns/test_version.py b/openstack/tests/unit/dns/test_version.py index be84a03bb..9d2424349 100644 --- a/openstack/tests/unit/dns/test_version.py +++ b/openstack/tests/unit/dns/test_version.py @@ -23,7 +23,6 @@ class TestVersion(base.TestCase): - def test_basic(self): sot = version.Version() self.assertEqual('version', sot.resource_key) diff --git a/openstack/tests/unit/dns/v2/test_floating_ip.py b/openstack/tests/unit/dns/v2/test_floating_ip.py index f1e6b19cd..c87a15235 100644 --- a/openstack/tests/unit/dns/v2/test_floating_ip.py +++ b/openstack/tests/unit/dns/v2/test_floating_ip.py @@ -19,18 +19,15 @@ 'status': 'PENDING', 'ptrdname': 'smtp.example.com.', 'description': 'This is a floating ip for 127.0.0.1', - 'links': { - 'self': 'dummylink/reverse/floatingips/RegionOne:id' - }, + 'links': {'self': 'dummylink/reverse/floatingips/RegionOne:id'}, 'ttl': 600, 'address': '172.24.4.10', 'action': 'CREATE', - 'id': IDENTIFIER + 'id': IDENTIFIER, } class TestFloatingIP(base.TestCase): - def test_basic(self): sot = fip.FloatingIP() self.assertEqual('', sot.resource_key) diff --git a/openstack/tests/unit/dns/v2/test_proxy.py b/openstack/tests/unit/dns/v2/test_proxy.py index f80c56c34..c4dc9f1e1 100644 --- a/openstack/tests/unit/dns/v2/test_proxy.py +++ b/openstack/tests/unit/dns/v2/test_proxy.py @@ -29,15 +29,20 @@ def setUp(self): class TestDnsZone(TestDnsProxy): def test_zone_create(self): - self.verify_create(self.proxy.create_zone, zone.Zone, - method_kwargs={'name': 'id'}, - expected_kwargs={'name': 'id', - 'prepend_key': False}) + self.verify_create( + self.proxy.create_zone, + zone.Zone, + method_kwargs={'name': 'id'}, + expected_kwargs={'name': 'id', 'prepend_key': False}, + ) def test_zone_delete(self): self.verify_delete( - self.proxy.delete_zone, zone.Zone, True, - expected_kwargs={'ignore_missing': True, 'delete_shares': False}) + self.proxy.delete_zone, + zone.Zone, + True, + expected_kwargs={'ignore_missing': True, 'delete_shares': False}, + ) def test_zone_find(self): self.verify_find(self.proxy.find_zone, zone.Zone) @@ -56,44 +61,57 @@ def test_zone_abandon(self): "openstack.dns.v2.zone.Zone.abandon", self.proxy.abandon_zone, method_args=[{'zone': 'id'}], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_zone_xfr(self): self._verify( "openstack.dns.v2.zone.Zone.xfr", self.proxy.xfr_zone, method_args=[{'zone': 'id'}], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) class TestDnsRecordset(TestDnsProxy): def test_recordset_create(self): - self.verify_create(self.proxy.create_recordset, recordset.Recordset, - method_kwargs={'zone': 'id'}, - expected_kwargs={'zone_id': 'id', - 'prepend_key': False}) + self.verify_create( + self.proxy.create_recordset, + recordset.Recordset, + method_kwargs={'zone': 'id'}, + expected_kwargs={'zone_id': 'id', 'prepend_key': False}, + ) def test_recordset_delete(self): - self.verify_delete(self.proxy.delete_recordset, - recordset.Recordset, True) + self.verify_delete( + self.proxy.delete_recordset, recordset.Recordset, True + ) def test_recordset_update(self): self.verify_update(self.proxy.update_recordset, recordset.Recordset) def test_recordset_get(self): - self.verify_get(self.proxy.get_recordset, recordset.Recordset, - method_kwargs={'zone': 'zid'}, - expected_kwargs={'zone_id': 'zid'} - ) + self.verify_get( + self.proxy.get_recordset, + recordset.Recordset, + method_kwargs={'zone': 'zid'}, + expected_kwargs={'zone_id': 'zid'}, + ) def test_recordsets(self): - self.verify_list(self.proxy.recordsets, recordset.Recordset, - expected_kwargs={'base_path': '/recordsets'}) + self.verify_list( + self.proxy.recordsets, + recordset.Recordset, + expected_kwargs={'base_path': '/recordsets'}, + ) def test_recordsets_zone(self): - self.verify_list(self.proxy.recordsets, recordset.Recordset, - method_kwargs={'zone': 'zid'}, - expected_kwargs={'zone_id': 'zid'}) + self.verify_list( + self.proxy.recordsets, + recordset.Recordset, + method_kwargs={'zone': 'zid'}, + expected_kwargs={'zone_id': 'zid'}, + ) def test_recordset_find(self): self._verify( @@ -102,7 +120,8 @@ def test_recordset_find(self): method_args=['zone', 'rs'], method_kwargs={}, expected_args=[recordset.Recordset, 'rs'], - expected_kwargs={'ignore_missing': True, 'zone_id': 'zone'}) + expected_kwargs={'ignore_missing': True, 'zone_id': 'zone'}, + ) class TestDnsFloatIP(TestDnsProxy): @@ -113,8 +132,9 @@ def test_floating_ip_get(self): self.verify_get(self.proxy.get_floating_ip, floating_ip.FloatingIP) def test_floating_ip_update(self): - self.verify_update(self.proxy.update_floating_ip, - floating_ip.FloatingIP) + self.verify_update( + self.proxy.update_floating_ip, floating_ip.FloatingIP + ) def test_floating_ip_unset(self): self._verify( @@ -123,13 +143,15 @@ def test_floating_ip_unset(self): method_args=['value'], method_kwargs={}, expected_args=[floating_ip.FloatingIP, 'value'], - expected_kwargs={'ptrdname': None}) + expected_kwargs={'ptrdname': None}, + ) class TestDnsZoneImport(TestDnsProxy): def test_zone_import_delete(self): - self.verify_delete(self.proxy.delete_zone_import, - zone_import.ZoneImport, True) + self.verify_delete( + self.proxy.delete_zone_import, zone_import.ZoneImport, True + ) def test_zone_import_get(self): self.verify_get(self.proxy.get_zone_import, zone_import.ZoneImport) @@ -138,117 +160,154 @@ def test_zone_imports(self): self.verify_list(self.proxy.zone_imports, zone_import.ZoneImport) def test_zone_import_create(self): - self.verify_create(self.proxy.create_zone_import, - zone_import.ZoneImport, - method_kwargs={'name': 'id'}, - expected_kwargs={'name': 'id', - 'prepend_key': False}) + self.verify_create( + self.proxy.create_zone_import, + zone_import.ZoneImport, + method_kwargs={'name': 'id'}, + expected_kwargs={'name': 'id', 'prepend_key': False}, + ) class TestDnsZoneExport(TestDnsProxy): def test_zone_export_delete(self): - self.verify_delete(self.proxy.delete_zone_export, - zone_export.ZoneExport, True) + self.verify_delete( + self.proxy.delete_zone_export, zone_export.ZoneExport, True + ) def test_zone_export_get(self): self.verify_get(self.proxy.get_zone_export, zone_export.ZoneExport) def test_zone_export_get_text(self): - self.verify_get(self.proxy.get_zone_export_text, - zone_export.ZoneExport, - method_args=[{'id': 'zone_export_id_value'}], - expected_kwargs={ - 'base_path': '/zones/tasks/export/%(id)s/export' - }) + self.verify_get( + self.proxy.get_zone_export_text, + zone_export.ZoneExport, + method_args=[{'id': 'zone_export_id_value'}], + expected_kwargs={'base_path': '/zones/tasks/export/%(id)s/export'}, + ) def test_zone_exports(self): self.verify_list(self.proxy.zone_exports, zone_export.ZoneExport) def test_zone_export_create(self): - self.verify_create(self.proxy.create_zone_export, - zone_export.ZoneExport, - method_args=[{'id': 'zone_id_value'}], - method_kwargs={'name': 'id'}, - expected_args=[], - expected_kwargs={'name': 'id', - 'zone_id': 'zone_id_value', - 'prepend_key': False}) + self.verify_create( + self.proxy.create_zone_export, + zone_export.ZoneExport, + method_args=[{'id': 'zone_id_value'}], + method_kwargs={'name': 'id'}, + expected_args=[], + expected_kwargs={ + 'name': 'id', + 'zone_id': 'zone_id_value', + 'prepend_key': False, + }, + ) class TestDnsZoneTransferRequest(TestDnsProxy): def test_zone_transfer_request_delete(self): - self.verify_delete(self.proxy.delete_zone_transfer_request, - zone_transfer.ZoneTransferRequest, True) + self.verify_delete( + self.proxy.delete_zone_transfer_request, + zone_transfer.ZoneTransferRequest, + True, + ) def test_zone_transfer_request_get(self): - self.verify_get(self.proxy.get_zone_transfer_request, - zone_transfer.ZoneTransferRequest) + self.verify_get( + self.proxy.get_zone_transfer_request, + zone_transfer.ZoneTransferRequest, + ) def test_zone_transfer_requests(self): - self.verify_list(self.proxy.zone_transfer_requests, - zone_transfer.ZoneTransferRequest) + self.verify_list( + self.proxy.zone_transfer_requests, + zone_transfer.ZoneTransferRequest, + ) def test_zone_transfer_request_create(self): - self.verify_create(self.proxy.create_zone_transfer_request, - zone_transfer.ZoneTransferRequest, - method_args=[{'id': 'zone_id_value'}], - method_kwargs={'name': 'id'}, - expected_args=[], - expected_kwargs={'name': 'id', - 'zone_id': 'zone_id_value', - 'prepend_key': False}) + self.verify_create( + self.proxy.create_zone_transfer_request, + zone_transfer.ZoneTransferRequest, + method_args=[{'id': 'zone_id_value'}], + method_kwargs={'name': 'id'}, + expected_args=[], + expected_kwargs={ + 'name': 'id', + 'zone_id': 'zone_id_value', + 'prepend_key': False, + }, + ) def test_zone_transfer_request_update(self): - self.verify_update(self.proxy.update_zone_transfer_request, - zone_transfer.ZoneTransferRequest) + self.verify_update( + self.proxy.update_zone_transfer_request, + zone_transfer.ZoneTransferRequest, + ) class TestDnsZoneTransferAccept(TestDnsProxy): def test_zone_transfer_accept_get(self): - self.verify_get(self.proxy.get_zone_transfer_accept, - zone_transfer.ZoneTransferAccept) + self.verify_get( + self.proxy.get_zone_transfer_accept, + zone_transfer.ZoneTransferAccept, + ) def test_zone_transfer_accepts(self): - self.verify_list(self.proxy.zone_transfer_accepts, - zone_transfer.ZoneTransferAccept) + self.verify_list( + self.proxy.zone_transfer_accepts, zone_transfer.ZoneTransferAccept + ) def test_zone_transfer_accept_create(self): - self.verify_create(self.proxy.create_zone_transfer_accept, - zone_transfer.ZoneTransferAccept) + self.verify_create( + self.proxy.create_zone_transfer_accept, + zone_transfer.ZoneTransferAccept, + ) class TestDnsZoneShare(TestDnsProxy): def test_zone_share_create(self): - self.verify_create(self.proxy.create_zone_share, zone_share.ZoneShare, - method_kwargs={'zone': 'bogus_id'}, - expected_kwargs={'zone_id': 'bogus_id'}) + self.verify_create( + self.proxy.create_zone_share, + zone_share.ZoneShare, + method_kwargs={'zone': 'bogus_id'}, + expected_kwargs={'zone_id': 'bogus_id'}, + ) def test_zone_share_delete(self): self.verify_delete( - self.proxy.delete_zone_share, zone_share.ZoneShare, + self.proxy.delete_zone_share, + zone_share.ZoneShare, ignore_missing=True, method_args={'zone': 'bogus_id', 'zone_share': 'bogus_id'}, expected_args=['zone_share'], - expected_kwargs={'zone_id': 'zone', 'ignore_missing': True}) + expected_kwargs={'zone_id': 'zone', 'ignore_missing': True}, + ) def test_zone_share_find(self): self.verify_find( - self.proxy.find_zone_share, zone_share.ZoneShare, + self.proxy.find_zone_share, + zone_share.ZoneShare, method_args=['zone'], expected_args=['zone'], - expected_kwargs={'zone_id': 'resource_name', - 'ignore_missing': True}) + expected_kwargs={ + 'zone_id': 'resource_name', + 'ignore_missing': True, + }, + ) def test_zone_share_get(self): self.verify_get( - self.proxy.get_zone_share, zone_share.ZoneShare, + self.proxy.get_zone_share, + zone_share.ZoneShare, method_args=['zone', 'zone_share'], expected_args=['zone_share'], - expected_kwargs={'zone_id': 'zone'}) + expected_kwargs={'zone_id': 'zone'}, + ) def test_zone_shares(self): self.verify_list( - self.proxy.zone_shares, zone_share.ZoneShare, + self.proxy.zone_shares, + zone_share.ZoneShare, method_args=['zone'], expected_args=[], - expected_kwargs={'zone_id': 'zone'}) + expected_kwargs={'zone_id': 'zone'}, + ) diff --git a/openstack/tests/unit/dns/v2/test_recordset.py b/openstack/tests/unit/dns/v2/test_recordset.py index a1eb2706c..6254524a4 100644 --- a/openstack/tests/unit/dns/v2/test_recordset.py +++ b/openstack/tests/unit/dns/v2/test_recordset.py @@ -18,9 +18,7 @@ EXAMPLE = { 'description': 'This is an example record set.', 'updated_at': None, - 'records': [ - '10.1.0.2' - ], + 'records': ['10.1.0.2'], 'ttl': 3600, 'id': IDENTIFIER, 'name': 'example.org.', @@ -31,12 +29,11 @@ 'version': 1, 'type': 'A', 'status': 'ACTIVE', - 'action': 'NONE' + 'action': 'NONE', } class TestRecordset(base.TestCase): - def test_basic(self): sot = recordset.Recordset() self.assertIsNone(sot.resource_key) @@ -48,15 +45,19 @@ def test_basic(self): self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) - self.assertDictEqual({'data': 'data', - 'description': 'description', - 'limit': 'limit', - 'marker': 'marker', - 'name': 'name', - 'status': 'status', - 'ttl': 'ttl', - 'type': 'type'}, - sot._query_mapping._mapping) + self.assertDictEqual( + { + 'data': 'data', + 'description': 'description', + 'limit': 'limit', + 'marker': 'marker', + 'name': 'name', + 'status': 'status', + 'ttl': 'ttl', + 'type': 'type', + }, + sot._query_mapping._mapping, + ) def test_make_it(self): sot = recordset.Recordset(**EXAMPLE) diff --git a/openstack/tests/unit/dns/v2/test_zone.py b/openstack/tests/unit/dns/v2/test_zone.py index cbc41c699..b916a3ac4 100644 --- a/openstack/tests/unit/dns/v2/test_zone.py +++ b/openstack/tests/unit/dns/v2/test_zone.py @@ -19,9 +19,7 @@ IDENTIFIER = 'NAME' EXAMPLE = { - 'attributes': { - 'tier': 'gold', 'ha': 'true' - }, + 'attributes': {'tier': 'gold', 'ha': 'true'}, 'id': IDENTIFIER, 'name': 'test.org', 'email': 'joe@example.org', @@ -29,12 +27,11 @@ 'ttl': 7200, 'description': 'This is an example zone.', 'status': 'ACTIVE', - 'shared': False + 'shared': False, } class TestZone(base.TestCase): - def setUp(self): super(TestZone, self).setUp() self.resp = mock.Mock() @@ -58,15 +55,19 @@ def test_basic(self): self.assertEqual('PATCH', sot.commit_method) - self.assertDictEqual({'description': 'description', - 'email': 'email', - 'limit': 'limit', - 'marker': 'marker', - 'name': 'name', - 'status': 'status', - 'ttl': 'ttl', - 'type': 'type'}, - sot._query_mapping._mapping) + self.assertDictEqual( + { + 'description': 'description', + 'email': 'email', + 'limit': 'limit', + 'marker': 'marker', + 'name': 'name', + 'status': 'status', + 'ttl': 'ttl', + 'type': 'type', + }, + sot._query_mapping._mapping, + ) def test_make_it(self): sot = zone.Zone(**EXAMPLE) @@ -83,6 +84,5 @@ def test_abandon(self): sot = zone.Zone(**EXAMPLE) self.assertIsNone(sot.abandon(self.sess)) self.sess.post.assert_called_with( - 'zones/NAME/tasks/abandon', - json=None + 'zones/NAME/tasks/abandon', json=None ) diff --git a/openstack/tests/unit/dns/v2/test_zone_export.py b/openstack/tests/unit/dns/v2/test_zone_export.py index a474c88fc..c058bd10f 100644 --- a/openstack/tests/unit/dns/v2/test_zone_export.py +++ b/openstack/tests/unit/dns/v2/test_zone_export.py @@ -23,7 +23,7 @@ 'zone_id': '6625198b-d67d-47dc-8d29-f90bd60f3ac4', 'links': { 'self': 'http://127.0.0.1:9001/v2/zones/tasks/exports/074e805e-f', - 'href': 'http://127.0.0.1:9001/v2/zones/6625198b-d67d-' + 'href': 'http://127.0.0.1:9001/v2/zones/6625198b-d67d-', }, 'created_at': '2015-05-08T15:43:42.000000', 'updated_at': '2015-05-08T15:43:43.000000', @@ -31,13 +31,12 @@ 'location': 'designate://v2/zones/tasks/exports/8ec17fe1/export', 'message': 'example.com. exported', 'project_id': 'noauth-project', - 'id': IDENTIFIER + 'id': IDENTIFIER, } @mock.patch.object(zone_export.ZoneExport, '_translate_response', mock.Mock()) class TestZoneExport(base.TestCase): - def test_basic(self): sot = zone_export.ZoneExport() self.assertEqual('', sot.resource_key) @@ -49,12 +48,16 @@ def test_basic(self): self.assertFalse(sot.allow_commit) self.assertTrue(sot.allow_delete) - self.assertDictEqual({'limit': 'limit', - 'marker': 'marker', - 'message': 'message', - 'status': 'status', - 'zone_id': 'zone_id'}, - sot._query_mapping._mapping) + self.assertDictEqual( + { + 'limit': 'limit', + 'marker': 'marker', + 'message': 'message', + 'status': 'status', + 'zone_id': 'zone_id', + }, + sot._query_mapping._mapping, + ) def test_make_it(self): sot = zone_export.ZoneExport(**EXAMPLE) @@ -76,6 +79,8 @@ def test_create(self): sot.create(self.session) self.session.post.assert_called_once_with( - mock.ANY, json=None, + mock.ANY, + json=None, headers=None, - microversion=self.session.default_microversion) + microversion=self.session.default_microversion, + ) diff --git a/openstack/tests/unit/dns/v2/test_zone_import.py b/openstack/tests/unit/dns/v2/test_zone_import.py index 74ec73c99..ac0e62190 100644 --- a/openstack/tests/unit/dns/v2/test_zone_import.py +++ b/openstack/tests/unit/dns/v2/test_zone_import.py @@ -22,20 +22,19 @@ 'zone_id': '6625198b-d67d-47dc-8d29-f90bd60f3ac4', 'links': { 'self': 'http://127.0.0.1:9001/v2/zones/tasks/imports/074e805e-f', - 'href': 'http://127.0.0.1:9001/v2/zones/6625198b-d67d-' + 'href': 'http://127.0.0.1:9001/v2/zones/6625198b-d67d-', }, 'created_at': '2015-05-08T15:43:42.000000', 'updated_at': '2015-05-08T15:43:43.000000', 'version': 2, 'message': 'example.com. imported', 'project_id': 'noauth-project', - 'id': IDENTIFIER + 'id': IDENTIFIER, } @mock.patch.object(zone_import.ZoneImport, '_translate_response', mock.Mock()) class TestZoneImport(base.TestCase): - def test_basic(self): sot = zone_import.ZoneImport() self.assertEqual('', sot.resource_key) @@ -47,12 +46,16 @@ def test_basic(self): self.assertFalse(sot.allow_commit) self.assertTrue(sot.allow_delete) - self.assertDictEqual({'limit': 'limit', - 'marker': 'marker', - 'message': 'message', - 'status': 'status', - 'zone_id': 'zone_id'}, - sot._query_mapping._mapping) + self.assertDictEqual( + { + 'limit': 'limit', + 'marker': 'marker', + 'message': 'message', + 'status': 'status', + 'zone_id': 'zone_id', + }, + sot._query_mapping._mapping, + ) def test_make_it(self): sot = zone_import.ZoneImport(**EXAMPLE) @@ -74,6 +77,8 @@ def test_create(self): sot.create(self.session) self.session.post.assert_called_once_with( - mock.ANY, json=None, + mock.ANY, + json=None, headers={'content-type': 'text/dns'}, - microversion=self.session.default_microversion) + microversion=self.session.default_microversion, + ) diff --git a/openstack/tests/unit/dns/v2/test_zone_share.py b/openstack/tests/unit/dns/v2/test_zone_share.py index 0c5d04ce7..c9b76ad3b 100644 --- a/openstack/tests/unit/dns/v2/test_zone_share.py +++ b/openstack/tests/unit/dns/v2/test_zone_share.py @@ -19,7 +19,6 @@ class TestZoneShare(base.TestCase): - def setUp(self): super(TestZoneShare, self).setUp() self.resp = mock.Mock() @@ -41,9 +40,14 @@ def test_basic(self): self.assertTrue(sot.allow_delete) self.assertFalse(sot.allow_commit) - self.assertDictEqual({'target_project_id': 'target_project_id', - 'limit': 'limit', 'marker': 'marker'}, - sot._query_mapping._mapping) + self.assertDictEqual( + { + 'target_project_id': 'target_project_id', + 'limit': 'limit', + 'marker': 'marker', + }, + sot._query_mapping._mapping, + ) def test_make_it(self): share_id = 'bogus_id' @@ -54,7 +58,8 @@ def test_make_it(self): 'id': share_id, 'zone_id': zone_id, 'project_id': project_id, - 'target_project_id': target_id} + 'target_project_id': target_id, + } sot = zone_share.ZoneShare(**expected) self.assertEqual(share_id, sot.id) diff --git a/openstack/tests/unit/dns/v2/test_zone_transfer.py b/openstack/tests/unit/dns/v2/test_zone_transfer.py index bc8ba1a80..b876a3634 100644 --- a/openstack/tests/unit/dns/v2/test_zone_transfer.py +++ b/openstack/tests/unit/dns/v2/test_zone_transfer.py @@ -35,12 +35,11 @@ 'key': 'FUGXMZ5N', 'project_id': '2e43de7ce3504a8fb90a45382532c37e', 'id': IDENTIFIER, - 'zone_transfer_request_id': '794fdf58-6e1d-41da-8b2d-16b6d10c8827' + 'zone_transfer_request_id': '794fdf58-6e1d-41da-8b2d-16b6d10c8827', } class TestZoneTransferRequest(base.TestCase): - def test_basic(self): sot = zone_transfer.ZoneTransferRequest() # self.assertEqual('', sot.resource_key) @@ -52,10 +51,10 @@ def test_basic(self): self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) - self.assertDictEqual({'limit': 'limit', - 'marker': 'marker', - 'status': 'status'}, - sot._query_mapping._mapping) + self.assertDictEqual( + {'limit': 'limit', 'marker': 'marker', 'status': 'status'}, + sot._query_mapping._mapping, + ) def test_make_it(self): sot = zone_transfer.ZoneTransferRequest(**EXAMPLE_REQUEST) @@ -66,14 +65,14 @@ def test_make_it(self): self.assertEqual(EXAMPLE_REQUEST['key'], sot.key) self.assertEqual(EXAMPLE_REQUEST['project_id'], sot.project_id) self.assertEqual(EXAMPLE_REQUEST['status'], sot.status) - self.assertEqual(EXAMPLE_REQUEST['target_project_id'], - sot.target_project_id) + self.assertEqual( + EXAMPLE_REQUEST['target_project_id'], sot.target_project_id + ) self.assertEqual(EXAMPLE_REQUEST['zone_id'], sot.zone_id) self.assertEqual(EXAMPLE_REQUEST['zone_name'], sot.zone_name) class TestZoneTransferAccept(base.TestCase): - def test_basic(self): sot = zone_transfer.ZoneTransferAccept() # self.assertEqual('', sot.resource_key) @@ -85,10 +84,10 @@ def test_basic(self): self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) - self.assertDictEqual({'limit': 'limit', - 'marker': 'marker', - 'status': 'status'}, - sot._query_mapping._mapping) + self.assertDictEqual( + {'limit': 'limit', 'marker': 'marker', 'status': 'status'}, + sot._query_mapping._mapping, + ) def test_make_it(self): sot = zone_transfer.ZoneTransferAccept(**EXAMPLE_ACCEPT) @@ -99,5 +98,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE_ACCEPT['project_id'], sot.project_id) self.assertEqual(EXAMPLE_ACCEPT['status'], sot.status) self.assertEqual(EXAMPLE_ACCEPT['zone_id'], sot.zone_id) - self.assertEqual(EXAMPLE_ACCEPT['zone_transfer_request_id'], - sot.zone_transfer_request_id) + self.assertEqual( + EXAMPLE_ACCEPT['zone_transfer_request_id'], + sot.zone_transfer_request_id, + ) From 33bed575013f11e4d408593e53c6c99ca66d6110 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 5 May 2023 11:03:51 +0100 Subject: [PATCH 3252/3836] Blackify openstack.instance_ha Black used with the '-l 79 -S' flags. A future change will ignore this commit in git-blame history by adding a 'git-blame-ignore-revs' file. Change-Id: Ifa4d0af2f9de7bd0635f3f87e2d241f2fe26ddf8 Signed-off-by: Stephen Finucane --- openstack/instance_ha/v1/_proxy.py | 18 ++-- openstack/instance_ha/v1/host.py | 9 +- openstack/instance_ha/v1/notification.py | 16 +++- openstack/instance_ha/v1/segment.py | 8 +- .../tests/functional/instance_ha/test_host.py | 50 ++++++---- .../functional/instance_ha/test_segment.py | 13 ++- .../tests/unit/instance_ha/v1/test_host.py | 30 +++--- .../unit/instance_ha/v1/test_notification.py | 94 ++++++++++++------- .../tests/unit/instance_ha/v1/test_proxy.py | 66 +++++++------ .../tests/unit/instance_ha/v1/test_segment.py | 21 +++-- 10 files changed, 200 insertions(+), 125 deletions(-) diff --git a/openstack/instance_ha/v1/_proxy.py b/openstack/instance_ha/v1/_proxy.py index ef1773759..13471d182 100644 --- a/openstack/instance_ha/v1/_proxy.py +++ b/openstack/instance_ha/v1/_proxy.py @@ -122,8 +122,9 @@ def delete_segment(self, segment, ignore_missing=True): attempting to delete a nonexistent segment. :returns: ``None`` """ - return self._delete(_segment.Segment, segment, - ignore_missing=ignore_missing) + return self._delete( + _segment.Segment, segment, ignore_missing=ignore_missing + ) def hosts(self, segment_id, **query): """Return a generator of hosts. @@ -182,8 +183,9 @@ def update_host(self, host, segment_id, **attrs): when segment_id is None. """ host_id = resource.Resource._get_id(host) - return self._update(_host.Host, host_id, segment_id=segment_id, - **attrs) + return self._update( + _host.Host, host_id, segment_id=segment_id, **attrs + ) def delete_host(self, host, segment_id=None, ignore_missing=True): """Delete the host. @@ -208,5 +210,9 @@ def delete_host(self, host, segment_id=None, ignore_missing=True): raise exceptions.InvalidRequest("'segment_id' must be specified.") host_id = resource.Resource._get_id(host) - return self._delete(_host.Host, host_id, segment_id=segment_id, - ignore_missing=ignore_missing) + return self._delete( + _host.Host, + host_id, + segment_id=segment_id, + ignore_missing=ignore_missing, + ) diff --git a/openstack/instance_ha/v1/host.py b/openstack/instance_ha/v1/host.py index 6ee6a4b4f..29cc94f9a 100644 --- a/openstack/instance_ha/v1/host.py +++ b/openstack/instance_ha/v1/host.py @@ -56,5 +56,10 @@ class Host(resource.Resource): failover_segment_id = resource.Body("failover_segment_id") _query_mapping = resource.QueryParameters( - "sort_key", "sort_dir", failover_segment_id="failover_segment_id", - type="type", on_maintenance="on_maintenance", reserved="reserved") + "sort_key", + "sort_dir", + failover_segment_id="failover_segment_id", + type="type", + on_maintenance="on_maintenance", + reserved="reserved", + ) diff --git a/openstack/instance_ha/v1/notification.py b/openstack/instance_ha/v1/notification.py index c83ded92a..2ddccbf4b 100644 --- a/openstack/instance_ha/v1/notification.py +++ b/openstack/instance_ha/v1/notification.py @@ -33,7 +33,8 @@ class RecoveryWorkflowDetailItem(resource.Resource): state = resource.Body("state") #: The progress details of this recovery workflow. progress_details = resource.Body( - "progress_details", type=list, list_type=ProgressDetailsItem) + "progress_details", type=list, list_type=ProgressDetailsItem + ) class Notification(resource.Resource): @@ -75,8 +76,15 @@ class Notification(resource.Resource): #: The recovery workflow details of this notification. recovery_workflow_details = resource.Body( "recovery_workflow_details", - type=list, list_type=RecoveryWorkflowDetailItem) + type=list, + list_type=RecoveryWorkflowDetailItem, + ) _query_mapping = resource.QueryParameters( - "sort_key", "sort_dir", source_host_uuid="source_host_uuid", - type="type", status="status", generated_since="generated-since") + "sort_key", + "sort_dir", + source_host_uuid="source_host_uuid", + type="type", + status="status", + generated_since="generated-since", + ) diff --git a/openstack/instance_ha/v1/segment.py b/openstack/instance_ha/v1/segment.py index e4841202e..814c83228 100644 --- a/openstack/instance_ha/v1/segment.py +++ b/openstack/instance_ha/v1/segment.py @@ -55,5 +55,9 @@ class Segment(resource.Resource): is_enabled = resource.Body("enabled", type=bool) _query_mapping = resource.QueryParameters( - "sort_key", "sort_dir", recovery_method="recovery_method", - service_type="service_type", is_enabled="enabled") + "sort_key", + "sort_dir", + recovery_method="recovery_method", + service_type="service_type", + is_enabled="enabled", + ) diff --git a/openstack/tests/functional/instance_ha/test_host.py b/openstack/tests/functional/instance_ha/test_host.py index f1f1a5372..bd6f0f4b2 100644 --- a/openstack/tests/functional/instance_ha/test_host.py +++ b/openstack/tests/functional/instance_ha/test_host.py @@ -25,48 +25,62 @@ def hypervisors(): if HYPERVISORS: return True HYPERVISORS = connection.Connection.list_hypervisors( - connection.from_config(cloud_name=base.TEST_CLOUD_NAME)) + connection.from_config(cloud_name=base.TEST_CLOUD_NAME) + ) return bool(HYPERVISORS) class TestHost(base.BaseFunctionalTest): - def setUp(self): super(TestHost, self).setUp() self.require_service('instance-ha') self.NAME = self.getUniqueString() if not hypervisors(): - self.skipTest("Skip TestHost as there are no hypervisors " - "configured in nova") + self.skipTest( + "Skip TestHost as there are no hypervisors " + "configured in nova" + ) # Create segment self.segment = self.conn.ha.create_segment( - name=self.NAME, recovery_method='auto', - service_type='COMPUTE') + name=self.NAME, recovery_method='auto', service_type='COMPUTE' + ) # Create valid host self.NAME = HYPERVISORS[0].name self.host = self.conn.ha.create_host( - segment_id=self.segment.uuid, name=self.NAME, type='COMPUTE', - control_attributes='SSH') + segment_id=self.segment.uuid, + name=self.NAME, + type='COMPUTE', + control_attributes='SSH', + ) # Delete host - self.addCleanup(self.conn.ha.delete_host, self.segment.uuid, - self.host.uuid) + self.addCleanup( + self.conn.ha.delete_host, self.segment.uuid, self.host.uuid + ) # Delete segment self.addCleanup(self.conn.ha.delete_segment, self.segment.uuid) def test_list(self): - names = [o.name for o in self.conn.ha.hosts( - self.segment.uuid, failover_segment_id=self.segment.uuid, - type='COMPUTE')] + names = [ + o.name + for o in self.conn.ha.hosts( + self.segment.uuid, + failover_segment_id=self.segment.uuid, + type='COMPUTE', + ) + ] self.assertIn(self.NAME, names) def test_update(self): - updated_host = self.conn.ha.update_host(self.host['uuid'], - segment_id=self.segment.uuid, - on_maintenance='True') - get_host = self.conn.ha.get_host(updated_host.uuid, - updated_host.segment_id) + updated_host = self.conn.ha.update_host( + self.host['uuid'], + segment_id=self.segment.uuid, + on_maintenance='True', + ) + get_host = self.conn.ha.get_host( + updated_host.uuid, updated_host.segment_id + ) self.assertEqual(True, get_host.on_maintenance) diff --git a/openstack/tests/functional/instance_ha/test_segment.py b/openstack/tests/functional/instance_ha/test_segment.py index 15d863778..a99d616e2 100644 --- a/openstack/tests/functional/instance_ha/test_segment.py +++ b/openstack/tests/functional/instance_ha/test_segment.py @@ -17,7 +17,6 @@ class TestSegment(base.BaseFunctionalTest): - def setUp(self): super(TestSegment, self).setUp() self.require_service('instance-ha') @@ -25,19 +24,19 @@ def setUp(self): # Create segment self.segment = self.conn.ha.create_segment( - name=self.NAME, recovery_method='auto', - service_type='COMPUTE') + name=self.NAME, recovery_method='auto', service_type='COMPUTE' + ) # Delete segment self.addCleanup(self.conn.ha.delete_segment, self.segment['uuid']) def test_list(self): - names = [o.name for o in self.conn.ha.segments( - recovery_method='auto')] + names = [o.name for o in self.conn.ha.segments(recovery_method='auto')] self.assertIn(self.NAME, names) def test_update(self): - updated_segment = self.conn.ha.update_segment(self.segment['uuid'], - name='UPDATED-NAME') + updated_segment = self.conn.ha.update_segment( + self.segment['uuid'], name='UPDATED-NAME' + ) get_updated_segment = self.conn.ha.get_segment(updated_segment.uuid) self.assertEqual('UPDATED-NAME', get_updated_segment.name) diff --git a/openstack/tests/unit/instance_ha/v1/test_host.py b/openstack/tests/unit/instance_ha/v1/test_host.py index 4b388a663..d1a79743c 100644 --- a/openstack/tests/unit/instance_ha/v1/test_host.py +++ b/openstack/tests/unit/instance_ha/v1/test_host.py @@ -18,10 +18,7 @@ FAKE_ID = "1c2f1795-ce78-4d4c-afd0-ce141fdb3952" FAKE_UUID = "11f7597f-87d2-4057-b754-ba611f989807" FAKE_HOST_ID = "c27dec16-ed4d-4ebe-8e77-f1e28ec32417" -FAKE_CONTROL_ATTRIBUTES = { - "mcastaddr": "239.255.1.1", - "mcastport": "5405" -} +FAKE_CONTROL_ATTRIBUTES = {"mcastaddr": "239.255.1.1", "mcastport": "5405"} HOST = { "id": FAKE_ID, "uuid": FAKE_UUID, @@ -33,12 +30,11 @@ "control_attributes": FAKE_CONTROL_ATTRIBUTES, "on_maintenance": False, "reserved": False, - "failover_segment_id": FAKE_HOST_ID + "failover_segment_id": FAKE_HOST_ID, } class TestHost(base.TestCase): - def test_basic(self): sot = host.Host(HOST) self.assertEqual("host", sot.resource_key) @@ -50,15 +46,19 @@ def test_basic(self): self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) - self.assertDictEqual({"failover_segment_id": "failover_segment_id", - "limit": "limit", - "marker": "marker", - "on_maintenance": "on_maintenance", - "reserved": "reserved", - "sort_dir": "sort_dir", - "sort_key": "sort_key", - "type": "type"}, - sot._query_mapping._mapping) + self.assertDictEqual( + { + "failover_segment_id": "failover_segment_id", + "limit": "limit", + "marker": "marker", + "on_maintenance": "on_maintenance", + "reserved": "reserved", + "sort_dir": "sort_dir", + "sort_key": "sort_key", + "type": "type", + }, + sot._query_mapping._mapping, + ) def test_create(self): sot = host.Host(**HOST) diff --git a/openstack/tests/unit/instance_ha/v1/test_notification.py b/openstack/tests/unit/instance_ha/v1/test_notification.py index d4fb269d4..4d41cd818 100644 --- a/openstack/tests/unit/instance_ha/v1/test_notification.py +++ b/openstack/tests/unit/instance_ha/v1/test_notification.py @@ -21,17 +21,26 @@ PAYLOAD = { "instance_uuid": "4032bc1d-d723-47f6-b5ac-b9b3e6dbb795", "vir_domain_event": "STOPPED_FAILED", - "event": "LIFECYCLE" + "event": "LIFECYCLE", } -PROGRESS_DETAILS = [{"timestamp": "2019-02-28 07:21:33.291810", - "progress": 1.0, - "message": "Skipping recovery for process " - "nova-compute as it is already disabled"}] +PROGRESS_DETAILS = [ + { + "timestamp": "2019-02-28 07:21:33.291810", + "progress": 1.0, + "message": "Skipping recovery for process " + "nova-compute as it is already disabled", + } +] -RECOVERY_WORKFLOW_DETAILS = [{"progress": 1.0, "state": "SUCCESS", - "name": "DisableComputeNodeTask", - "progress_details": PROGRESS_DETAILS}] +RECOVERY_WORKFLOW_DETAILS = [ + { + "progress": 1.0, + "state": "SUCCESS", + "name": "DisableComputeNodeTask", + "progress_details": PROGRESS_DETAILS, + } +] NOTIFICATION = { "id": FAKE_ID, @@ -44,12 +53,11 @@ "generated_time": "2018-03-21T00:00:00.000000", "payload": PAYLOAD, "source_host_uuid": FAKE_HOST_UUID, - "recovery_workflow_details": RECOVERY_WORKFLOW_DETAILS + "recovery_workflow_details": RECOVERY_WORKFLOW_DETAILS, } class TestNotification(base.TestCase): - def test_basic(self): sot = notification.Notification(NOTIFICATION) self.assertEqual("notification", sot.resource_key) @@ -61,22 +69,27 @@ def test_basic(self): self.assertFalse(sot.allow_commit) self.assertFalse(sot.allow_delete) - self.assertDictEqual({"generated_since": "generated-since", - "limit": "limit", - "marker": "marker", - "sort_dir": "sort_dir", - "sort_key": "sort_key", - "source_host_uuid": "source_host_uuid", - "status": "status", - "type": "type"}, - sot._query_mapping._mapping) + self.assertDictEqual( + { + "generated_since": "generated-since", + "limit": "limit", + "marker": "marker", + "sort_dir": "sort_dir", + "sort_key": "sort_key", + "source_host_uuid": "source_host_uuid", + "status": "status", + "type": "type", + }, + sot._query_mapping._mapping, + ) def test_create(self): sot = notification.Notification(**NOTIFICATION) rec_workflow_details = NOTIFICATION["recovery_workflow_details"][0] self.assertEqual(NOTIFICATION["id"], sot.id) self.assertEqual( - NOTIFICATION["notification_uuid"], sot.notification_uuid) + NOTIFICATION["notification_uuid"], sot.notification_uuid + ) self.assertEqual(NOTIFICATION["created_at"], sot.created_at) self.assertEqual(NOTIFICATION["updated_at"], sot.updated_at) self.assertEqual(NOTIFICATION["type"], sot.type) @@ -85,27 +98,40 @@ def test_create(self): self.assertEqual(NOTIFICATION["generated_time"], sot.generated_time) self.assertEqual(NOTIFICATION["payload"], sot.payload) self.assertEqual( - NOTIFICATION["source_host_uuid"], sot.source_host_uuid) - self.assertEqual(rec_workflow_details["name"], - sot.recovery_workflow_details[0].name) - self.assertEqual(rec_workflow_details["state"], - sot.recovery_workflow_details[0].state) - self.assertEqual(rec_workflow_details["progress"], - sot.recovery_workflow_details[0].progress) + NOTIFICATION["source_host_uuid"], sot.source_host_uuid + ) + self.assertEqual( + rec_workflow_details["name"], sot.recovery_workflow_details[0].name + ) + self.assertEqual( + rec_workflow_details["state"], + sot.recovery_workflow_details[0].state, + ) + self.assertEqual( + rec_workflow_details["progress"], + sot.recovery_workflow_details[0].progress, + ) self.assertEqual( rec_workflow_details["progress_details"][0]['progress'], - sot.recovery_workflow_details[0].progress_details[0].progress) + sot.recovery_workflow_details[0].progress_details[0].progress, + ) self.assertEqual( rec_workflow_details["progress_details"][0]['message'], - sot.recovery_workflow_details[0].progress_details[0].message) + sot.recovery_workflow_details[0].progress_details[0].message, + ) self.assertEqual( rec_workflow_details["progress_details"][0]['timestamp'], - sot.recovery_workflow_details[0].progress_details[0].timestamp) + sot.recovery_workflow_details[0].progress_details[0].timestamp, + ) self.assertIsInstance(sot.recovery_workflow_details, list) self.assertIsInstance( - sot.recovery_workflow_details[0].progress_details, list) - self.assertIsInstance(sot.recovery_workflow_details[0], - notification.RecoveryWorkflowDetailItem) + sot.recovery_workflow_details[0].progress_details, list + ) + self.assertIsInstance( + sot.recovery_workflow_details[0], + notification.RecoveryWorkflowDetailItem, + ) self.assertIsInstance( sot.recovery_workflow_details[0].progress_details[0], - notification.ProgressDetailsItem) + notification.ProgressDetailsItem, + ) diff --git a/openstack/tests/unit/instance_ha/v1/test_proxy.py b/openstack/tests/unit/instance_ha/v1/test_proxy.py index e2123ea75..2e54410c3 100644 --- a/openstack/tests/unit/instance_ha/v1/test_proxy.py +++ b/openstack/tests/unit/instance_ha/v1/test_proxy.py @@ -30,38 +30,48 @@ def setUp(self): class TestInstanceHaHosts(TestInstanceHaProxy): def test_hosts(self): - self.verify_list(self.proxy.hosts, - host.Host, - method_args=[SEGMENT_ID], - expected_args=[], - expected_kwargs={"segment_id": SEGMENT_ID}) + self.verify_list( + self.proxy.hosts, + host.Host, + method_args=[SEGMENT_ID], + expected_args=[], + expected_kwargs={"segment_id": SEGMENT_ID}, + ) def test_host_get(self): - self.verify_get(self.proxy.get_host, - host.Host, - method_args=[HOST_ID], - method_kwargs={"segment_id": SEGMENT_ID}, - expected_kwargs={"segment_id": SEGMENT_ID}) + self.verify_get( + self.proxy.get_host, + host.Host, + method_args=[HOST_ID], + method_kwargs={"segment_id": SEGMENT_ID}, + expected_kwargs={"segment_id": SEGMENT_ID}, + ) def test_host_create(self): - self.verify_create(self.proxy.create_host, - host.Host, - method_args=[SEGMENT_ID], - method_kwargs={}, - expected_args=[], - expected_kwargs={"segment_id": SEGMENT_ID}) + self.verify_create( + self.proxy.create_host, + host.Host, + method_args=[SEGMENT_ID], + method_kwargs={}, + expected_args=[], + expected_kwargs={"segment_id": SEGMENT_ID}, + ) def test_host_update(self): - self.verify_update(self.proxy.update_host, - host.Host, - method_kwargs={"segment_id": SEGMENT_ID}) + self.verify_update( + self.proxy.update_host, + host.Host, + method_kwargs={"segment_id": SEGMENT_ID}, + ) def test_host_delete(self): - self.verify_delete(self.proxy.delete_host, - host.Host, - True, - method_kwargs={"segment_id": SEGMENT_ID}, - expected_kwargs={"segment_id": SEGMENT_ID}) + self.verify_delete( + self.proxy.delete_host, + host.Host, + True, + method_kwargs={"segment_id": SEGMENT_ID}, + expected_kwargs={"segment_id": SEGMENT_ID}, + ) class TestInstanceHaNotifications(TestInstanceHaProxy): @@ -69,12 +79,12 @@ def test_notifications(self): self.verify_list(self.proxy.notifications, notification.Notification) def test_notification_get(self): - self.verify_get(self.proxy.get_notification, - notification.Notification) + self.verify_get(self.proxy.get_notification, notification.Notification) def test_notification_create(self): - self.verify_create(self.proxy.create_notification, - notification.Notification) + self.verify_create( + self.proxy.create_notification, notification.Notification + ) class TestInstanceHaSegments(TestInstanceHaProxy): diff --git a/openstack/tests/unit/instance_ha/v1/test_segment.py b/openstack/tests/unit/instance_ha/v1/test_segment.py index af43349b6..334265f1b 100644 --- a/openstack/tests/unit/instance_ha/v1/test_segment.py +++ b/openstack/tests/unit/instance_ha/v1/test_segment.py @@ -31,7 +31,6 @@ class TestSegment(base.TestCase): - def test_basic(self): sot = segment.Segment(SEGMENT) self.assertEqual("segment", sot.resource_key) @@ -43,14 +42,18 @@ def test_basic(self): self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) - self.assertDictEqual({"limit": "limit", - "marker": "marker", - "recovery_method": "recovery_method", - "service_type": "service_type", - "is_enabled": "enabled", - "sort_dir": "sort_dir", - "sort_key": "sort_key"}, - sot._query_mapping._mapping) + self.assertDictEqual( + { + "limit": "limit", + "marker": "marker", + "recovery_method": "recovery_method", + "service_type": "service_type", + "is_enabled": "enabled", + "sort_dir": "sort_dir", + "sort_key": "sort_key", + }, + sot._query_mapping._mapping, + ) def test_create(self): sot = segment.Segment(**SEGMENT) From 570b81f0ec3b3876aefbb223c78093f2a957bb01 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 5 May 2023 11:04:35 +0100 Subject: [PATCH 3253/3836] Blackify openstack.accelerator Black used with the '-l 79 -S' flags. A future change will ignore this commit in git-blame history by adding a 'git-blame-ignore-revs' file. Change-Id: Ib5351abb13b5f4acac5eb554b2fa48acd276592a Signed-off-by: Stephen Finucane --- openstack/accelerator/accelerator_service.py | 1 + openstack/accelerator/v2/_proxy.py | 21 ++++--- .../accelerator/v2/accelerator_request.py | 30 ++++++--- openstack/accelerator/v2/deployable.py | 24 ++++++-- openstack/accelerator/v2/device_profile.py | 6 +- .../tests/unit/accelerator/test_version.py | 1 - .../v2/test_accelerator_request.py | 13 ++-- .../unit/accelerator/v2/test_deployable.py | 1 - .../tests/unit/accelerator/v2/test_device.py | 1 - .../accelerator/v2/test_device_profile.py | 21 ++++--- .../tests/unit/accelerator/v2/test_proxy.py | 61 +++++++++++++------ 11 files changed, 116 insertions(+), 64 deletions(-) diff --git a/openstack/accelerator/accelerator_service.py b/openstack/accelerator/accelerator_service.py index f8210e23e..fc01ea666 100644 --- a/openstack/accelerator/accelerator_service.py +++ b/openstack/accelerator/accelerator_service.py @@ -16,6 +16,7 @@ class AcceleratorService(service_description.ServiceDescription): """The accelerator service.""" + supported_versions = { '2': _proxy_v2.Proxy, } diff --git a/openstack/accelerator/v2/_proxy.py b/openstack/accelerator/v2/_proxy.py index c443e026e..7f40ccace 100644 --- a/openstack/accelerator/v2/_proxy.py +++ b/openstack/accelerator/v2/_proxy.py @@ -18,7 +18,6 @@ class Proxy(proxy.Proxy): - def deployables(self, **query): """Retrieve a generator of deployables. @@ -45,8 +44,9 @@ def update_deployable(self, uuid, patch): :param patch: The information to reconfig. :returns: The results of FPGA reconfig. """ - return self._get_resource(_deployable.Deployable, - uuid).patch(self, patch) + return self._get_resource(_deployable.Deployable, uuid).patch( + self, patch + ) def devices(self, **query): """Retrieve a generator of devices. @@ -115,7 +115,8 @@ def delete_device_profile(self, device_profile, ignore_missing=True): return self._delete( _device_profile.DeviceProfile, device_profile, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, + ) def get_device_profile(self, uuid, fields=None): """Get a single device profile. @@ -146,7 +147,9 @@ def create_accelerator_request(self, **attrs): return self._create(_arq.AcceleratorRequest, **attrs) def delete_accelerator_request( - self, accelerator_request, ignore_missing=True, + self, + accelerator_request, + ignore_missing=True, ): """Delete a device profile @@ -164,7 +167,8 @@ def delete_accelerator_request( return self._delete( _arq.AcceleratorRequest, accelerator_request, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, + ) def get_accelerator_request(self, uuid, fields=None): """Get a single accelerator request. @@ -185,5 +189,6 @@ def update_accelerator_request(self, uuid, properties): that will bind/unbind the accelerator. :returns: True if bind/unbind succeeded, False otherwise. """ - return self._get_resource(_arq.AcceleratorRequest, - uuid).patch(self, properties) + return self._get_resource(_arq.AcceleratorRequest, uuid).patch( + self, properties + ) diff --git a/openstack/accelerator/v2/accelerator_request.py b/openstack/accelerator/v2/accelerator_request.py index f0c492bc8..543f7234a 100644 --- a/openstack/accelerator/v2/accelerator_request.py +++ b/openstack/accelerator/v2/accelerator_request.py @@ -56,8 +56,15 @@ def _convert_patch(self, patch): converted = {self.id: converted} return converted - def patch(self, session, patch=None, prepend_key=True, has_body=True, - retry_on_conflict=None, base_path=None): + def patch( + self, + session, + patch=None, + prepend_key=True, + has_body=True, + retry_on_conflict=None, + base_path=None, + ): # This overrides the default behavior of patch because # the PATCH method consumes a dict rather than a list. spec: # https://specs.openstack.org/openstack/cyborg-specs/specs/train/implemented/cyborg-api @@ -72,15 +79,21 @@ def patch(self, session, patch=None, prepend_key=True, has_body=True, if not self.allow_patch: raise exceptions.MethodNotSupported(self, "patch") - request = self._prepare_request(prepend_key=prepend_key, - base_path=base_path, patch=True) + request = self._prepare_request( + prepend_key=prepend_key, base_path=base_path, patch=True + ) microversion = self._get_microversion(session, action='patch') if patch: request.body = self._convert_patch(patch) - return self._commit(session, request, 'PATCH', microversion, - has_body=has_body, - retry_on_conflict=retry_on_conflict) + return self._commit( + session, + request, + 'PATCH', + microversion, + has_body=has_body, + retry_on_conflict=retry_on_conflict, + ) def _consume_attrs(self, mapping, attrs): # This overrides the default behavior of _consume_attrs because @@ -95,4 +108,5 @@ def create(self, session, base_path=None): # This overrides the default behavior of resource creation because # cyborg doesn't accept resource_key in its request. return super(AcceleratorRequest, self).create( - session, prepend_key=False, base_path=base_path) + session, prepend_key=False, base_path=base_path + ) diff --git a/openstack/accelerator/v2/deployable.py b/openstack/accelerator/v2/deployable.py index a499011fc..889853e40 100644 --- a/openstack/accelerator/v2/deployable.py +++ b/openstack/accelerator/v2/deployable.py @@ -41,8 +41,15 @@ class Deployable(resource.Resource): #: The timestamp when this deployable was updated. updated_at = resource.Body('updated_at') - def _commit(self, session, request, method, microversion, has_body=True, - retry_on_conflict=None): + def _commit( + self, + session, + request, + method, + microversion, + has_body=True, + retry_on_conflict=None, + ): session = self._get_session(session) kwargs = {} retriable_status_codes = set(session.retriable_status_codes or ()) @@ -57,12 +64,17 @@ def _commit(self, session, request, method, microversion, has_body=True, call = getattr(session, method.lower()) except AttributeError: raise exceptions.ResourceFailure( - "Invalid commit method: %s" % method) + "Invalid commit method: %s" % method + ) request.url = request.url + "/program" - response = call(request.url, json=request.body, - headers=request.headers, microversion=microversion, - **kwargs) + response = call( + request.url, + json=request.body, + headers=request.headers, + microversion=microversion, + **kwargs + ) self.microversion = microversion self._translate_response(response, has_body=has_body) return self diff --git a/openstack/accelerator/v2/device_profile.py b/openstack/accelerator/v2/device_profile.py index a66bc772e..15034d395 100644 --- a/openstack/accelerator/v2/device_profile.py +++ b/openstack/accelerator/v2/device_profile.py @@ -40,11 +40,13 @@ class DeviceProfile(resource.Resource): # cannot treat multiple DeviceProfiles in list. def _prepare_request_body(self, patch, prepend_key): body = super(DeviceProfile, self)._prepare_request_body( - patch, prepend_key) + patch, prepend_key + ) return [body] def create(self, session, base_path=None): # This overrides the default behavior of resource creation because # cyborg doesn't accept resource_key in its request. return super(DeviceProfile, self).create( - session, prepend_key=False, base_path=base_path) + session, prepend_key=False, base_path=base_path + ) diff --git a/openstack/tests/unit/accelerator/test_version.py b/openstack/tests/unit/accelerator/test_version.py index 315ffbbf8..93f3f0359 100644 --- a/openstack/tests/unit/accelerator/test_version.py +++ b/openstack/tests/unit/accelerator/test_version.py @@ -23,7 +23,6 @@ class TestVersion(base.TestCase): - def test_basic(self): sot = version.Version() self.assertEqual('version', sot.resource_key) diff --git a/openstack/tests/unit/accelerator/v2/test_accelerator_request.py b/openstack/tests/unit/accelerator/v2/test_accelerator_request.py index ad03131ac..641d60b7e 100644 --- a/openstack/tests/unit/accelerator/v2/test_accelerator_request.py +++ b/openstack/tests/unit/accelerator/v2/test_accelerator_request.py @@ -17,10 +17,9 @@ FAKE_ID = '0725b527-e51a-41df-ad22-adad5f4546ad' FAKE_RP_UUID = 'f4b7fe6c-8ab4-4914-a113-547af022935b' FAKE_INSTANCE_UUID = '1ce4a597-9836-4e02-bea1-a3a6cbe7b9f9' -FAKE_ATTACH_INFO_STR = '{"bus": "5e", '\ - '"device": "00", '\ - '"domain": "0000", '\ - '"function": "1"}' +FAKE_ATTACH_INFO_STR = ( + '{"bus": "5e", ' '"device": "00", ' '"domain": "0000", ' '"function": "1"}' +) FAKE = { 'uuid': FAKE_ID, @@ -34,7 +33,6 @@ class TestAcceleratorRequest(base.TestCase): - def test_basic(self): sot = arq.AcceleratorRequest() self.assertEqual('arq', sot.resource_key) @@ -51,8 +49,9 @@ def test_make_it(self): sot = arq.AcceleratorRequest(**FAKE) self.assertEqual(FAKE_ID, sot.uuid) self.assertEqual(FAKE['device_profile_name'], sot.device_profile_name) - self.assertEqual(FAKE['device_profile_group_id'], - sot.device_profile_group_id) + self.assertEqual( + FAKE['device_profile_group_id'], sot.device_profile_group_id + ) self.assertEqual(FAKE_RP_UUID, sot.device_rp_uuid) self.assertEqual(FAKE_INSTANCE_UUID, sot.instance_uuid) self.assertEqual(FAKE['attach_handle_type'], sot.attach_handle_type) diff --git a/openstack/tests/unit/accelerator/v2/test_deployable.py b/openstack/tests/unit/accelerator/v2/test_deployable.py index 0bd8061ab..87f1f885e 100644 --- a/openstack/tests/unit/accelerator/v2/test_deployable.py +++ b/openstack/tests/unit/accelerator/v2/test_deployable.py @@ -29,7 +29,6 @@ class TestDeployable(base.TestCase): - def test_basic(self): sot = deployable.Deployable() self.assertEqual('deployable', sot.resource_key) diff --git a/openstack/tests/unit/accelerator/v2/test_device.py b/openstack/tests/unit/accelerator/v2/test_device.py index 0151ff73f..c354d5222 100644 --- a/openstack/tests/unit/accelerator/v2/test_device.py +++ b/openstack/tests/unit/accelerator/v2/test_device.py @@ -30,7 +30,6 @@ class TestDevice(base.TestCase): - def test_basic(self): sot = device.Device() self.assertEqual('device', sot.resource_key) diff --git a/openstack/tests/unit/accelerator/v2/test_device_profile.py b/openstack/tests/unit/accelerator/v2/test_device_profile.py index ded8c4c69..f686acc20 100644 --- a/openstack/tests/unit/accelerator/v2/test_device_profile.py +++ b/openstack/tests/unit/accelerator/v2/test_device_profile.py @@ -19,21 +19,22 @@ "uuid": u"a95e10ae-b3e3-4eab-a513-1afae6f17c51", "name": u'afaas_example_1', "groups": [ - {"resources:ACCELERATOR_FPGA": "1", - "trait:CUSTOM_FPGA_INTEL_PAC_ARRIA10": "required", - "trait:CUSTOM_FUNCTION_ID_3AFB": "required", - }, - {"resources:CUSTOM_ACCELERATOR_FOO": "2", - "resources:CUSTOM_MEMORY": "200", - "trait:CUSTOM_TRAIT_ALWAYS": "required", - } + { + "resources:ACCELERATOR_FPGA": "1", + "trait:CUSTOM_FPGA_INTEL_PAC_ARRIA10": "required", + "trait:CUSTOM_FUNCTION_ID_3AFB": "required", + }, + { + "resources:CUSTOM_ACCELERATOR_FOO": "2", + "resources:CUSTOM_MEMORY": "200", + "trait:CUSTOM_TRAIT_ALWAYS": "required", + }, ], - 'description': 'description_test' + 'description': 'description_test', } class TestDeviceProfile(base.TestCase): - def test_basic(self): sot = device_profile.DeviceProfile() self.assertEqual('device_profile', sot.resource_key) diff --git a/openstack/tests/unit/accelerator/v2/test_proxy.py b/openstack/tests/unit/accelerator/v2/test_proxy.py index c4ebfe78a..848e75cff 100644 --- a/openstack/tests/unit/accelerator/v2/test_proxy.py +++ b/openstack/tests/unit/accelerator/v2/test_proxy.py @@ -30,43 +30,64 @@ def test_list_deployables(self): class TestAcceleratorDevice(TestAcceleratorProxy): def test_list_device_profile(self): - self.verify_list(self.proxy.device_profiles, - device_profile.DeviceProfile) + self.verify_list( + self.proxy.device_profiles, device_profile.DeviceProfile + ) def test_create_device_profile(self): - self.verify_create(self.proxy.create_device_profile, - device_profile.DeviceProfile) + self.verify_create( + self.proxy.create_device_profile, device_profile.DeviceProfile + ) def test_delete_device_profile(self): - self.verify_delete(self.proxy.delete_device_profile, - device_profile.DeviceProfile, False) + self.verify_delete( + self.proxy.delete_device_profile, + device_profile.DeviceProfile, + False, + ) def test_delete_device_profile_ignore(self): - self.verify_delete(self.proxy.delete_device_profile, - device_profile.DeviceProfile, True) + self.verify_delete( + self.proxy.delete_device_profile, + device_profile.DeviceProfile, + True, + ) def test_get_device_profile(self): - self.verify_get(self.proxy.get_device_profile, - device_profile.DeviceProfile) + self.verify_get( + self.proxy.get_device_profile, device_profile.DeviceProfile + ) class TestAcceleratorRequest(TestAcceleratorProxy): def test_list_accelerator_request(self): - self.verify_list(self.proxy.accelerator_requests, - accelerator_request.AcceleratorRequest) + self.verify_list( + self.proxy.accelerator_requests, + accelerator_request.AcceleratorRequest, + ) def test_create_accelerator_request(self): - self.verify_create(self.proxy.create_accelerator_request, - accelerator_request.AcceleratorRequest) + self.verify_create( + self.proxy.create_accelerator_request, + accelerator_request.AcceleratorRequest, + ) def test_delete_accelerator_request(self): - self.verify_delete(self.proxy.delete_accelerator_request, - accelerator_request.AcceleratorRequest, False) + self.verify_delete( + self.proxy.delete_accelerator_request, + accelerator_request.AcceleratorRequest, + False, + ) def test_delete_accelerator_request_ignore(self): - self.verify_delete(self.proxy.delete_accelerator_request, - accelerator_request.AcceleratorRequest, True) + self.verify_delete( + self.proxy.delete_accelerator_request, + accelerator_request.AcceleratorRequest, + True, + ) def test_get_accelerator_request(self): - self.verify_get(self.proxy.get_accelerator_request, - accelerator_request.AcceleratorRequest) + self.verify_get( + self.proxy.get_accelerator_request, + accelerator_request.AcceleratorRequest, + ) From 073abda5a94b12a319c79d6a9b8594036f95fc65 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 5 May 2023 11:05:19 +0100 Subject: [PATCH 3254/3836] Blackify openstack.container_infrastructure_management Black used with the '-l 79 -S' flags. A future change will ignore this commit in git-blame history by adding a 'git-blame-ignore-revs' file. Change-Id: I61806ea3ab9fa60466fa777fb403d1c07f23543c Signed-off-by: Stephen Finucane --- .../v1/_proxy.py | 10 +++++----- .../v1/test_cluster_certificate.py | 4 ++-- .../v1/test_cluster_template.py | 20 +++++++++++-------- .../v1/test_proxy.py | 5 ++--- .../v1/test_service.py | 2 +- 5 files changed, 22 insertions(+), 19 deletions(-) diff --git a/openstack/container_infrastructure_management/v1/_proxy.py b/openstack/container_infrastructure_management/v1/_proxy.py index 27adc7965..c0ee5204c 100644 --- a/openstack/container_infrastructure_management/v1/_proxy.py +++ b/openstack/container_infrastructure_management/v1/_proxy.py @@ -11,16 +11,16 @@ # under the License. from openstack.container_infrastructure_management.v1 import ( - cluster as _cluster + cluster as _cluster, ) from openstack.container_infrastructure_management.v1 import ( - cluster_certificate as _cluster_cert + cluster_certificate as _cluster_cert, ) from openstack.container_infrastructure_management.v1 import ( - cluster_template as _cluster_template + cluster_template as _cluster_template, ) from openstack.container_infrastructure_management.v1 import ( - service as _service + service as _service, ) from openstack import proxy @@ -30,7 +30,7 @@ class Proxy(proxy.Proxy): _resource_registry = { "cluster": _cluster.Cluster, "cluster_template": _cluster_template.ClusterTemplate, - "service": _service.Service + "service": _service.Service, } def create_cluster(self, **attrs): diff --git a/openstack/tests/unit/container_infrastructure_management/v1/test_cluster_certificate.py b/openstack/tests/unit/container_infrastructure_management/v1/test_cluster_certificate.py index 390bdd489..6435b9bbc 100644 --- a/openstack/tests/unit/container_infrastructure_management/v1/test_cluster_certificate.py +++ b/openstack/tests/unit/container_infrastructure_management/v1/test_cluster_certificate.py @@ -11,7 +11,7 @@ # under the License. from openstack.container_infrastructure_management.v1 import ( - cluster_certificate + cluster_certificate, ) from openstack.tests.unit import base @@ -19,7 +19,7 @@ "cluster_uuid": "0b4b766f-1500-44b3-9804-5a6e12fe6df4", "pem": "-----BEGIN CERTIFICATE-----\nMIICzDCCAbSgAwIBAgIQOOkVcEN7TNa9E80G", "bay_uuid": "0b4b766f-1500-44b3-9804-5a6e12fe6df4", - "csr": "-----BEGIN CERTIFICATE REQUEST-----\nMIIEfzCCAmcCAQAwFDESMBAGA1UE" + "csr": "-----BEGIN CERTIFICATE REQUEST-----\nMIIEfzCCAmcCAQAwFDESMBAGA1UE", } diff --git a/openstack/tests/unit/container_infrastructure_management/v1/test_cluster_template.py b/openstack/tests/unit/container_infrastructure_management/v1/test_cluster_template.py index a3a8b5272..d291a8568 100644 --- a/openstack/tests/unit/container_infrastructure_management/v1/test_cluster_template.py +++ b/openstack/tests/unit/container_infrastructure_management/v1/test_cluster_template.py @@ -67,12 +67,14 @@ def test_make_it(self): self.assertEqual(EXAMPLE['cluster_distro'], sot.cluster_distro) self.assertEqual(EXAMPLE['coe'], sot.coe) self.assertEqual(EXAMPLE['created_at'], sot.created_at) - self.assertEqual(EXAMPLE['docker_storage_driver'], - sot.docker_storage_driver) + self.assertEqual( + EXAMPLE['docker_storage_driver'], sot.docker_storage_driver + ) self.assertEqual(EXAMPLE['docker_volume_size'], sot.docker_volume_size) self.assertEqual(EXAMPLE['dns_nameserver'], sot.dns_nameserver) - self.assertEqual(EXAMPLE['external_network_id'], - sot.external_network_id) + self.assertEqual( + EXAMPLE['external_network_id'], sot.external_network_id + ) self.assertEqual(EXAMPLE['fixed_network'], sot.fixed_network) self.assertEqual(EXAMPLE['fixed_subnet'], sot.fixed_subnet) self.assertEqual(EXAMPLE['flavor_id'], sot.flavor_id) @@ -80,11 +82,13 @@ def test_make_it(self): self.assertEqual(EXAMPLE['https_proxy'], sot.https_proxy) self.assertEqual(EXAMPLE['image_id'], sot.image_id) self.assertEqual(EXAMPLE['insecure_registry'], sot.insecure_registry) - self.assertEqual(EXAMPLE['floating_ip_enabled'], - sot.is_floating_ip_enabled) + self.assertEqual( + EXAMPLE['floating_ip_enabled'], sot.is_floating_ip_enabled + ) self.assertEqual(EXAMPLE['hidden'], sot.is_hidden) - self.assertEqual(EXAMPLE['master_lb_enabled'], - sot.is_master_lb_enabled) + self.assertEqual( + EXAMPLE['master_lb_enabled'], sot.is_master_lb_enabled + ) self.assertEqual(EXAMPLE['tls_disabled'], sot.is_tls_disabled) self.assertEqual(EXAMPLE['public'], sot.is_public) self.assertEqual(EXAMPLE['registry_enabled'], sot.is_registry_enabled) diff --git a/openstack/tests/unit/container_infrastructure_management/v1/test_proxy.py b/openstack/tests/unit/container_infrastructure_management/v1/test_proxy.py index e352dd083..7724f3313 100644 --- a/openstack/tests/unit/container_infrastructure_management/v1/test_proxy.py +++ b/openstack/tests/unit/container_infrastructure_management/v1/test_proxy.py @@ -11,7 +11,7 @@ # under the License. from openstack.container_infrastructure_management.v1 import ( - cluster_certificate + cluster_certificate, ) from openstack.container_infrastructure_management.v1 import _proxy from openstack.container_infrastructure_management.v1 import cluster @@ -60,7 +60,7 @@ class TestClusterCertificate(TestMagnumProxy): def test_cluster_certificate_get(self): self.verify_get( self.proxy.get_cluster_certificate, - cluster_certificate.ClusterCertificate + cluster_certificate.ClusterCertificate, ) def test_cluster_certificate_create_attrs(self): @@ -114,7 +114,6 @@ def test_cluster_template_delete_ignore(self): class TestService(TestMagnumProxy): - def test_services(self): self.verify_list( self.proxy.services, diff --git a/openstack/tests/unit/container_infrastructure_management/v1/test_service.py b/openstack/tests/unit/container_infrastructure_management/v1/test_service.py index 908c72c9b..4ede15e09 100644 --- a/openstack/tests/unit/container_infrastructure_management/v1/test_service.py +++ b/openstack/tests/unit/container_infrastructure_management/v1/test_service.py @@ -21,7 +21,7 @@ "updated_at": "2016-08-25T01:13:16+00:00", "host": "magnum-manager", "disabled_reason": None, - "id": 1 + "id": 1, } From c2ff7336cecabc665e7bf04cbe87ef8d0c2e6f9f Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 5 May 2023 11:23:59 +0100 Subject: [PATCH 3255/3836] Blackify openstack.clustering Black used with the '-l 79 -S' flags. A future change will ignore this commit in git-blame history by adding a 'git-blame-ignore-revs' file. Change-Id: I4f7bb54ac0e751ab73479cf17f19593f6bc73014 Signed-off-by: Stephen Finucane --- openstack/clustering/v1/_async_resource.py | 5 +- openstack/clustering/v1/_proxy.py | 63 +++--- openstack/clustering/v1/action.py | 10 +- openstack/clustering/v1/cluster.py | 30 +-- openstack/clustering/v1/cluster_policy.py | 3 +- openstack/clustering/v1/event.py | 10 +- openstack/clustering/v1/node.py | 22 +-- openstack/clustering/v1/policy.py | 3 +- openstack/clustering/v1/profile.py | 3 +- openstack/clustering/v1/receiver.py | 10 +- .../functional/clustering/test_cluster.py | 37 ++-- .../tests/unit/clustering/test_version.py | 1 - .../tests/unit/clustering/v1/test_action.py | 1 - .../unit/clustering/v1/test_build_info.py | 3 +- .../tests/unit/clustering/v1/test_cluster.py | 69 +++---- .../unit/clustering/v1/test_cluster_attr.py | 6 +- .../unit/clustering/v1/test_cluster_policy.py | 22 ++- .../tests/unit/clustering/v1/test_event.py | 8 +- .../tests/unit/clustering/v1/test_node.py | 25 ++- .../tests/unit/clustering/v1/test_policy.py | 4 +- .../unit/clustering/v1/test_policy_type.py | 12 +- .../tests/unit/clustering/v1/test_profile.py | 6 +- .../unit/clustering/v1/test_profile_type.py | 17 +- .../tests/unit/clustering/v1/test_proxy.py | 186 +++++++++++------- .../tests/unit/clustering/v1/test_receiver.py | 6 +- .../tests/unit/clustering/v1/test_service.py | 1 - 26 files changed, 296 insertions(+), 267 deletions(-) diff --git a/openstack/clustering/v1/_async_resource.py b/openstack/clustering/v1/_async_resource.py index 819433902..060415d69 100644 --- a/openstack/clustering/v1/_async_resource.py +++ b/openstack/clustering/v1/_async_resource.py @@ -16,7 +16,6 @@ class AsyncResource(resource.Resource): - def delete(self, session, error_message=None): """Delete the remote resource based on this instance. @@ -39,6 +38,6 @@ def _delete_response(self, response, error_message=None): location = response.headers['Location'] action_id = location.split('/')[-1] action = _action.Action.existing( - id=action_id, - connection=self._connection) + id=action_id, connection=self._connection + ) return action diff --git a/openstack/clustering/v1/_proxy.py b/openstack/clustering/v1/_proxy.py index c22cd6461..aae729c61 100644 --- a/openstack/clustering/v1/_proxy.py +++ b/openstack/clustering/v1/_proxy.py @@ -132,8 +132,9 @@ def find_profile(self, name_or_id, ignore_missing=True): :returns: One :class:`~openstack.clustering.v1.profile.Profile` object or None """ - return self._find(_profile.Profile, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _profile.Profile, name_or_id, ignore_missing=ignore_missing + ) def get_profile(self, profile): """Get a single profile. @@ -231,8 +232,9 @@ def delete_cluster(self, cluster, ignore_missing=True, force_delete=False): server = self._get_resource(_cluster.Cluster, cluster) return server.force_delete(self) else: - return self._delete(_cluster.Cluster, cluster, - ignore_missing=ignore_missing) + return self._delete( + _cluster.Cluster, cluster, ignore_missing=ignore_missing + ) def find_cluster(self, name_or_id, ignore_missing=True): """Find a single cluster. @@ -246,8 +248,9 @@ def find_cluster(self, name_or_id, ignore_missing=True): :returns: One :class:`~openstack.clustering.v1.cluster.Cluster` object or None """ - return self._find(_cluster.Cluster, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _cluster.Cluster, name_or_id, ignore_missing=ignore_missing + ) def get_cluster(self, cluster): """Get a single cluster. @@ -495,8 +498,9 @@ def collect_cluster_attrs(self, cluster, path, **query): :returns: A dictionary containing the list of attribute values. """ - return self._list(_cluster_attr.ClusterAttr, cluster_id=cluster, - path=path) + return self._list( + _cluster_attr.ClusterAttr, cluster_id=cluster, path=path + ) def check_cluster(self, cluster, **params): """Check a cluster. @@ -569,8 +573,9 @@ def delete_node(self, node, ignore_missing=True, force_delete=False): server = self._get_resource(_node.Node, node) return server.force_delete(self) else: - return self._delete(_node.Node, node, - ignore_missing=ignore_missing) + return self._delete( + _node.Node, node, ignore_missing=ignore_missing + ) def find_node(self, name_or_id, ignore_missing=True): """Find a single node. @@ -584,8 +589,9 @@ def find_node(self, name_or_id, ignore_missing=True): :returns: One :class:`~openstack.clustering.v1.node.Node` object or None. """ - return self._find(_node.Node, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _node.Node, name_or_id, ignore_missing=ignore_missing + ) def get_node(self, node, details=False): """Get a single node. @@ -755,8 +761,9 @@ def find_policy(self, name_or_id, ignore_missing=True): :returns: A policy object or None. :rtype: :class:`~openstack.clustering.v1.policy.Policy` """ - return self._find(_policy.Policy, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _policy.Policy, name_or_id, ignore_missing=ignore_missing + ) def get_policy(self, policy): """Get a single policy. @@ -834,8 +841,9 @@ def cluster_policies(self, cluster, **query): :returns: A generator of cluster-policy binding instances. """ cluster_id = resource.Resource._get_id(cluster) - return self._list(_cluster_policy.ClusterPolicy, cluster_id=cluster_id, - **query) + return self._list( + _cluster_policy.ClusterPolicy, cluster_id=cluster_id, **query + ) def get_cluster_policy(self, cluster_policy, cluster): """Get a cluster-policy binding. @@ -851,8 +859,9 @@ def get_cluster_policy(self, cluster_policy, cluster): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no cluster-policy binding matching the criteria could be found. """ - return self._get(_cluster_policy.ClusterPolicy, cluster_policy, - cluster_id=cluster) + return self._get( + _cluster_policy.ClusterPolicy, cluster_policy, cluster_id=cluster + ) def create_receiver(self, **attrs): """Create a new receiver from attributes. @@ -890,8 +899,9 @@ def delete_receiver(self, receiver, ignore_missing=True): :returns: ``None`` """ - self._delete(_receiver.Receiver, receiver, - ignore_missing=ignore_missing) + self._delete( + _receiver.Receiver, receiver, ignore_missing=ignore_missing + ) def find_receiver(self, name_or_id, ignore_missing=True): """Find a single receiver. @@ -905,8 +915,9 @@ def find_receiver(self, name_or_id, ignore_missing=True): :returns: A receiver object or None. :rtype: :class:`~openstack.clustering.v1.receiver.Receiver` """ - return self._find(_receiver.Receiver, name_or_id, - ignore_missing=ignore_missing) + return self._find( + _receiver.Receiver, name_or_id, ignore_missing=ignore_missing + ) def get_receiver(self, receiver): """Get a single receiver. @@ -1035,8 +1046,9 @@ def events(self, **query): """ return self._list(_event.Event, **query) - def wait_for_status(self, res, status, failures=None, interval=2, - wait=120): + def wait_for_status( + self, res, status, failures=None, interval=2, wait=120 + ): """Wait for a resource to be in a particular status. :param res: The resource to wait on to reach the specified status. @@ -1059,7 +1071,8 @@ def wait_for_status(self, res, status, failures=None, interval=2, """ failures = [] if failures is None else failures return resource.wait_for_status( - self, res, status, failures, interval, wait) + self, res, status, failures, interval, wait + ) def wait_for_delete(self, res, interval=2, wait=120): """Wait for a resource to be deleted. diff --git a/openstack/clustering/v1/action.py b/openstack/clustering/v1/action.py index e63921ec3..9517cef24 100644 --- a/openstack/clustering/v1/action.py +++ b/openstack/clustering/v1/action.py @@ -27,8 +27,14 @@ class Action(resource.Resource): commit_method = 'PATCH' _query_mapping = resource.QueryParameters( - 'name', 'action', 'status', 'sort', 'global_project', - 'cluster_id', target_id='target') + 'name', + 'action', + 'status', + 'sort', + 'global_project', + 'cluster_id', + target_id='target', + ) # Properties #: Name of the action. diff --git a/openstack/clustering/v1/cluster.py b/openstack/clustering/v1/cluster.py index 09e866272..618d7af3b 100644 --- a/openstack/clustering/v1/cluster.py +++ b/openstack/clustering/v1/cluster.py @@ -29,7 +29,8 @@ class Cluster(_async_resource.AsyncResource, metadata.MetadataMixin): commit_method = 'PATCH' _query_mapping = resource.QueryParameters( - 'name', 'status', 'sort', 'global_project') + 'name', 'status', 'sort', 'global_project' + ) # Properties #: The name of the cluster. @@ -96,9 +97,7 @@ def add_nodes(self, session, nodes): def del_nodes(self, session, nodes, **params): data = {'nodes': nodes} data.update(params) - body = { - 'del_nodes': data - } + body = {'del_nodes': data} return self.action(session, body) def replace_nodes(self, session, nodes): @@ -126,17 +125,13 @@ def scale_in(self, session, count=None): return self.action(session, body) def resize(self, session, **params): - body = { - 'resize': params - } + body = {'resize': params} return self.action(session, body) def policy_attach(self, session, policy_id, **params): data = {'policy_id': policy_id} data.update(params) - body = { - 'policy_attach': data - } + body = {'policy_attach': data} return self.action(session, body) def policy_detach(self, session, policy_id): @@ -150,21 +145,15 @@ def policy_detach(self, session, policy_id): def policy_update(self, session, policy_id, **params): data = {'policy_id': policy_id} data.update(params) - body = { - 'policy_update': data - } + body = {'policy_update': data} return self.action(session, body) def check(self, session, **params): - body = { - 'check': params - } + body = {'check': params} return self.action(session, body) def recover(self, session, **params): - body = { - 'recover': params - } + body = {'recover': params} return self.action(session, body) def op(self, session, operation, **params): @@ -177,8 +166,7 @@ def op(self, session, operation, **params): :returns: A dictionary containing the action ID. """ url = utils.urljoin(self.base_path, self.id, 'ops') - resp = session.post(url, - json={operation: params}) + resp = session.post(url, json={operation: params}) return resp.json() def force_delete(self, session): diff --git a/openstack/clustering/v1/cluster_policy.py b/openstack/clustering/v1/cluster_policy.py index f9af351c7..a533c1c2b 100644 --- a/openstack/clustering/v1/cluster_policy.py +++ b/openstack/clustering/v1/cluster_policy.py @@ -23,7 +23,8 @@ class ClusterPolicy(resource.Resource): allow_fetch = True _query_mapping = resource.QueryParameters( - 'sort', 'policy_name', 'policy_type', is_enabled='enabled') + 'sort', 'policy_name', 'policy_type', is_enabled='enabled' + ) # Properties #: ID of the policy object. diff --git a/openstack/clustering/v1/event.py b/openstack/clustering/v1/event.py index d88b1c0d6..0111b75a3 100644 --- a/openstack/clustering/v1/event.py +++ b/openstack/clustering/v1/event.py @@ -24,8 +24,14 @@ class Event(resource.Resource): allow_fetch = True _query_mapping = resource.QueryParameters( - 'cluster_id', 'action', 'level', 'sort', 'global_project', - obj_id='oid', obj_name='oname', obj_type='otype', + 'cluster_id', + 'action', + 'level', + 'sort', + 'global_project', + obj_id='oid', + obj_name='oname', + obj_type='otype', ) # Properties diff --git a/openstack/clustering/v1/node.py b/openstack/clustering/v1/node.py index c914a7210..8229d24cb 100644 --- a/openstack/clustering/v1/node.py +++ b/openstack/clustering/v1/node.py @@ -30,8 +30,13 @@ class Node(_async_resource.AsyncResource): commit_method = 'PATCH' _query_mapping = resource.QueryParameters( - 'show_details', 'name', 'sort', 'global_project', 'cluster_id', - 'status') + 'show_details', + 'name', + 'sort', + 'global_project', + 'cluster_id', + 'status', + ) # Properties #: The name of the node. @@ -98,9 +103,7 @@ def check(self, session, **params): :param session: A session object used for sending request. :returns: A dictionary containing the action ID. """ - body = { - 'check': params - } + body = {'check': params} return self._action(session, body) def recover(self, session, **params): @@ -109,9 +112,7 @@ def recover(self, session, **params): :param session: A session object used for sending request. :returns: A dictionary containing the action ID. """ - body = { - 'recover': params - } + body = {'recover': params} return self._action(session, body) def op(self, session, operation, **params): @@ -124,8 +125,7 @@ def op(self, session, operation, **params): :returns: A dictionary containing the action ID. """ url = utils.urljoin(self.base_path, self.id, 'ops') - resp = session.post(url, - json={operation: params}) + resp = session.post(url, json={operation: params}) return resp.json() def adopt(self, session, preview=False, **params): @@ -143,7 +143,7 @@ def adopt(self, session, preview=False, **params): 'identity': params.get('identity'), 'overrides': params.get('overrides'), 'type': params.get('type'), - 'snapshot': params.get('snapshot') + 'snapshot': params.get('snapshot'), } else: path = 'adopt' diff --git a/openstack/clustering/v1/policy.py b/openstack/clustering/v1/policy.py index f6e8111f5..f3ffc03a4 100644 --- a/openstack/clustering/v1/policy.py +++ b/openstack/clustering/v1/policy.py @@ -28,7 +28,8 @@ class Policy(resource.Resource): commit_method = 'PATCH' _query_mapping = resource.QueryParameters( - 'name', 'type', 'sort', 'global_project') + 'name', 'type', 'sort', 'global_project' + ) # Properties #: The name of the policy. diff --git a/openstack/clustering/v1/profile.py b/openstack/clustering/v1/profile.py index 595de3946..3c654b526 100644 --- a/openstack/clustering/v1/profile.py +++ b/openstack/clustering/v1/profile.py @@ -28,7 +28,8 @@ class Profile(resource.Resource): commit_method = 'PATCH' _query_mapping = resource.QueryParameters( - 'sort', 'global_project', 'type', 'name') + 'sort', 'global_project', 'type', 'name' + ) # Bodyerties #: The name of the profile diff --git a/openstack/clustering/v1/receiver.py b/openstack/clustering/v1/receiver.py index 95faa06c9..913feadb4 100644 --- a/openstack/clustering/v1/receiver.py +++ b/openstack/clustering/v1/receiver.py @@ -28,8 +28,14 @@ class Receiver(resource.Resource): commit_method = 'PATCH' _query_mapping = resource.QueryParameters( - 'name', 'type', 'cluster_id', 'action', 'sort', 'global_project', - user_id='user') + 'name', + 'type', + 'cluster_id', + 'action', + 'sort', + 'global_project', + user_id='user', + ) # Properties #: The name of the receiver. diff --git a/openstack/tests/functional/clustering/test_cluster.py b/openstack/tests/functional/clustering/test_cluster.py index 9db6e18fa..8ce1a17c0 100644 --- a/openstack/tests/functional/clustering/test_cluster.py +++ b/openstack/tests/functional/clustering/test_cluster.py @@ -28,9 +28,8 @@ def setUp(self): self.cidr = '10.99.99.0/16' self.network, self.subnet = test_network.create_network( - self.conn, - self.getUniqueString(), - self.cidr) + self.conn, self.getUniqueString(), self.cidr + ) self.assertIsNotNone(self.network) profile_attrs = { @@ -42,8 +41,10 @@ def setUp(self): 'name': self.getUniqueString(), 'flavor': self.flavor.name, 'image': self.image.name, - 'networks': [{'network': self.network.id}] - }}} + 'networks': [{'network': self.network.id}], + }, + }, + } self.profile = self.conn.clustering.create_profile(**profile_attrs) self.assertIsNotNone(self.profile) @@ -59,15 +60,16 @@ def setUp(self): self.cluster = self.conn.clustering.create_cluster(**cluster_spec) self.conn.clustering.wait_for_status( - self.cluster, 'ACTIVE', - wait=self._wait_for_timeout) + self.cluster, 'ACTIVE', wait=self._wait_for_timeout + ) assert isinstance(self.cluster, cluster.Cluster) def tearDown(self): if self.cluster: self.conn.clustering.delete_cluster(self.cluster.id) self.conn.clustering.wait_for_delete( - self.cluster, wait=self._wait_for_timeout) + self.cluster, wait=self._wait_for_timeout + ) test_network.delete_network(self.conn, self.network, self.subnet) @@ -90,7 +92,8 @@ def test_list(self): def test_update(self): new_cluster_name = self.getUniqueString() sot = self.conn.clustering.update_cluster( - self.cluster, name=new_cluster_name, profile_only=False) + self.cluster, name=new_cluster_name, profile_only=False + ) time.sleep(2) sot = self.conn.clustering.get_cluster(self.cluster) @@ -98,10 +101,12 @@ def test_update(self): def test_delete(self): cluster_delete_action = self.conn.clustering.delete_cluster( - self.cluster.id) + self.cluster.id + ) - self.conn.clustering.wait_for_delete(self.cluster, - wait=self._wait_for_timeout) + self.conn.clustering.wait_for_delete( + self.cluster, wait=self._wait_for_timeout + ) action = self.conn.clustering.get_action(cluster_delete_action.id) self.assertEqual(action.target_id, self.cluster.id) @@ -112,10 +117,12 @@ def test_delete(self): def test_force_delete(self): cluster_delete_action = self.conn.clustering.delete_cluster( - self.cluster.id, False, True) + self.cluster.id, False, True + ) - self.conn.clustering.wait_for_delete(self.cluster, - wait=self._wait_for_timeout) + self.conn.clustering.wait_for_delete( + self.cluster, wait=self._wait_for_timeout + ) action = self.conn.clustering.get_action(cluster_delete_action.id) self.assertEqual(action.target_id, self.cluster.id) diff --git a/openstack/tests/unit/clustering/test_version.py b/openstack/tests/unit/clustering/test_version.py index 4908e52f4..497114bb1 100644 --- a/openstack/tests/unit/clustering/test_version.py +++ b/openstack/tests/unit/clustering/test_version.py @@ -23,7 +23,6 @@ class TestVersion(base.TestCase): - def test_basic(self): sot = version.Version() self.assertEqual('version', sot.resource_key) diff --git a/openstack/tests/unit/clustering/v1/test_action.py b/openstack/tests/unit/clustering/v1/test_action.py index 539967cf7..aeceb4317 100644 --- a/openstack/tests/unit/clustering/v1/test_action.py +++ b/openstack/tests/unit/clustering/v1/test_action.py @@ -45,7 +45,6 @@ class TestAction(base.TestCase): - def setUp(self): super(TestAction, self).setUp() diff --git a/openstack/tests/unit/clustering/v1/test_build_info.py b/openstack/tests/unit/clustering/v1/test_build_info.py index e94aa2d48..490dc3dd4 100644 --- a/openstack/tests/unit/clustering/v1/test_build_info.py +++ b/openstack/tests/unit/clustering/v1/test_build_info.py @@ -20,12 +20,11 @@ }, 'engine': { 'revision': '1.0.0', - } + }, } class TestBuildInfo(base.TestCase): - def setUp(self): super(TestBuildInfo, self).setUp() diff --git a/openstack/tests/unit/clustering/v1/test_cluster.py b/openstack/tests/unit/clustering/v1/test_cluster.py index 6bab19394..dc06ff43b 100644 --- a/openstack/tests/unit/clustering/v1/test_cluster.py +++ b/openstack/tests/unit/clustering/v1/test_cluster.py @@ -65,7 +65,6 @@ class TestCluster(base.TestCase): - def setUp(self): super(TestCluster, self).setUp() @@ -102,13 +101,17 @@ def test_instantiate(self): self.assertEqual(FAKE['dependents'], sot.dependents) self.assertTrue(sot.is_profile_only) - self.assertDictEqual({"limit": "limit", - "marker": "marker", - "name": "name", - "status": "status", - "sort": "sort", - "global_project": "global_project"}, - sot._query_mapping._mapping) + self.assertDictEqual( + { + "limit": "limit", + "marker": "marker", + "name": "name", + "status": "status", + "sort": "sort", + "global_project": "global_project", + }, + sot._query_mapping._mapping, + ) def test_scale_in(self): sot = cluster.Cluster(**FAKE) @@ -120,8 +123,7 @@ def test_scale_in(self): self.assertEqual('', sot.scale_in(sess, 3)) url = 'clusters/%s/actions' % sot.id body = {'scale_in': {'count': 3}} - sess.post.assert_called_once_with(url, - json=body) + sess.post.assert_called_once_with(url, json=body) def test_scale_out(self): sot = cluster.Cluster(**FAKE) @@ -133,8 +135,7 @@ def test_scale_out(self): self.assertEqual('', sot.scale_out(sess, 3)) url = 'clusters/%s/actions' % sot.id body = {'scale_out': {'count': 3}} - sess.post.assert_called_once_with(url, - json=body) + sess.post.assert_called_once_with(url, json=body) def test_resize(self): sot = cluster.Cluster(**FAKE) @@ -146,8 +147,7 @@ def test_resize(self): self.assertEqual('', sot.resize(sess, foo='bar', zoo=5)) url = 'clusters/%s/actions' % sot.id body = {'resize': {'foo': 'bar', 'zoo': 5}} - sess.post.assert_called_once_with(url, - json=body) + sess.post.assert_called_once_with(url, json=body) def test_add_nodes(self): sot = cluster.Cluster(**FAKE) @@ -159,8 +159,7 @@ def test_add_nodes(self): self.assertEqual('', sot.add_nodes(sess, ['node-33'])) url = 'clusters/%s/actions' % sot.id body = {'add_nodes': {'nodes': ['node-33']}} - sess.post.assert_called_once_with(url, - json=body) + sess.post.assert_called_once_with(url, json=body) def test_del_nodes(self): sot = cluster.Cluster(**FAKE) @@ -172,8 +171,7 @@ def test_del_nodes(self): self.assertEqual('', sot.del_nodes(sess, ['node-11'])) url = 'clusters/%s/actions' % sot.id body = {'del_nodes': {'nodes': ['node-11']}} - sess.post.assert_called_once_with(url, - json=body) + sess.post.assert_called_once_with(url, json=body) def test_del_nodes_with_params(self): sot = cluster.Cluster(**FAKE) @@ -193,8 +191,7 @@ def test_del_nodes_with_params(self): 'destroy_after_deletion': True, } } - sess.post.assert_called_once_with(url, - json=body) + sess.post.assert_called_once_with(url, json=body) def test_replace_nodes(self): sot = cluster.Cluster(**FAKE) @@ -206,8 +203,7 @@ def test_replace_nodes(self): self.assertEqual('', sot.replace_nodes(sess, {'node-22': 'node-44'})) url = 'clusters/%s/actions' % sot.id body = {'replace_nodes': {'nodes': {'node-22': 'node-44'}}} - sess.post.assert_called_once_with(url, - json=body) + sess.post.assert_called_once_with(url, json=body) def test_policy_attach(self): sot = cluster.Cluster(**FAKE) @@ -228,8 +224,7 @@ def test_policy_attach(self): 'enabled': True, } } - sess.post.assert_called_once_with(url, - json=body) + sess.post.assert_called_once_with(url, json=body) def test_policy_detach(self): sot = cluster.Cluster(**FAKE) @@ -242,8 +237,7 @@ def test_policy_detach(self): url = 'clusters/%s/actions' % sot.id body = {'policy_detach': {'policy_id': 'POLICY'}} - sess.post.assert_called_once_with(url, - json=body) + sess.post.assert_called_once_with(url, json=body) def test_policy_update(self): sot = cluster.Cluster(**FAKE) @@ -252,20 +246,12 @@ def test_policy_update(self): resp.json = mock.Mock(return_value='') sess = mock.Mock() sess.post = mock.Mock(return_value=resp) - params = { - 'enabled': False - } + params = {'enabled': False} self.assertEqual('', sot.policy_update(sess, 'POLICY', **params)) url = 'clusters/%s/actions' % sot.id - body = { - 'policy_update': { - 'policy_id': 'POLICY', - 'enabled': False - } - } - sess.post.assert_called_once_with(url, - json=body) + body = {'policy_update': {'policy_id': 'POLICY', 'enabled': False}} + sess.post.assert_called_once_with(url, json=body) def test_check(self): sot = cluster.Cluster(**FAKE) @@ -277,8 +263,7 @@ def test_check(self): self.assertEqual('', sot.check(sess)) url = 'clusters/%s/actions' % sot.id body = {'check': {}} - sess.post.assert_called_once_with(url, - json=body) + sess.post.assert_called_once_with(url, json=body) def test_recover(self): sot = cluster.Cluster(**FAKE) @@ -290,8 +275,7 @@ def test_recover(self): self.assertEqual('', sot.recover(sess)) url = 'clusters/%s/actions' % sot.id body = {'recover': {}} - sess.post.assert_called_once_with(url, - json=body) + sess.post.assert_called_once_with(url, json=body) def test_operation(self): sot = cluster.Cluster(**FAKE) @@ -303,8 +287,7 @@ def test_operation(self): self.assertEqual('', sot.op(sess, 'dance', style='tango')) url = 'clusters/%s/ops' % sot.id body = {'dance': {'style': 'tango'}} - sess.post.assert_called_once_with(url, - json=body) + sess.post.assert_called_once_with(url, json=body) def test_force_delete(self): sot = cluster.Cluster(**FAKE) diff --git a/openstack/tests/unit/clustering/v1/test_cluster_attr.py b/openstack/tests/unit/clustering/v1/test_cluster_attr.py index 9c88946b9..c0d8bd570 100644 --- a/openstack/tests/unit/clustering/v1/test_cluster_attr.py +++ b/openstack/tests/unit/clustering/v1/test_cluster_attr.py @@ -23,15 +23,15 @@ class TestClusterAttr(base.TestCase): - def setUp(self): super(TestClusterAttr, self).setUp() def test_basic(self): sot = ca.ClusterAttr() self.assertEqual('cluster_attributes', sot.resources_key) - self.assertEqual('/clusters/%(cluster_id)s/attrs/%(path)s', - sot.base_path) + self.assertEqual( + '/clusters/%(cluster_id)s/attrs/%(path)s', sot.base_path + ) self.assertTrue(sot.allow_list) def test_instantiate(self): diff --git a/openstack/tests/unit/clustering/v1/test_cluster_policy.py b/openstack/tests/unit/clustering/v1/test_cluster_policy.py index 21d4bf84b..c218b238b 100644 --- a/openstack/tests/unit/clustering/v1/test_cluster_policy.py +++ b/openstack/tests/unit/clustering/v1/test_cluster_policy.py @@ -26,7 +26,6 @@ class TestClusterPolicy(base.TestCase): - def setUp(self): super(TestClusterPolicy, self).setUp() @@ -34,18 +33,21 @@ def test_basic(self): sot = cluster_policy.ClusterPolicy() self.assertEqual('cluster_policy', sot.resource_key) self.assertEqual('cluster_policies', sot.resources_key) - self.assertEqual('/clusters/%(cluster_id)s/policies', - sot.base_path) + self.assertEqual('/clusters/%(cluster_id)s/policies', sot.base_path) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_list) - self.assertDictEqual({"policy_name": "policy_name", - "policy_type": "policy_type", - "is_enabled": "enabled", - "sort": "sort", - "limit": "limit", - "marker": "marker"}, - sot._query_mapping._mapping) + self.assertDictEqual( + { + "policy_name": "policy_name", + "policy_type": "policy_type", + "is_enabled": "enabled", + "sort": "sort", + "limit": "limit", + "marker": "marker", + }, + sot._query_mapping._mapping, + ) def test_instantiate(self): sot = cluster_policy.ClusterPolicy(**FAKE) diff --git a/openstack/tests/unit/clustering/v1/test_event.py b/openstack/tests/unit/clustering/v1/test_event.py index 01e489668..cad15ca78 100644 --- a/openstack/tests/unit/clustering/v1/test_event.py +++ b/openstack/tests/unit/clustering/v1/test_event.py @@ -28,16 +28,12 @@ 'timestamp': '2016-10-10T12:46:36.000000', 'user': '5e5bf8027826429c96af157f68dc9072', 'meta_data': { - "action": { - "created_at": "2019-07-13T13:18:18Z", - "outputs": {} - } - } + "action": {"created_at": "2019-07-13T13:18:18Z", "outputs": {}} + }, } class TestEvent(base.TestCase): - def setUp(self): super(TestEvent, self).setUp() diff --git a/openstack/tests/unit/clustering/v1/test_node.py b/openstack/tests/unit/clustering/v1/test_node.py index 29a1e5a53..28bdaf814 100644 --- a/openstack/tests/unit/clustering/v1/test_node.py +++ b/openstack/tests/unit/clustering/v1/test_node.py @@ -33,12 +33,11 @@ 'created_at': '2015-10-10T12:46:36.000000', 'updated_at': '2016-10-10T12:46:36.000000', 'init_at': '2015-10-10T12:46:36.000000', - 'tainted': True + 'tainted': True, } class TestNode(base.TestCase): - def test_basic(self): sot = node.Node() self.assertEqual('node', sot.resource_key) @@ -78,8 +77,7 @@ def test_check(self): self.assertEqual('', sot.check(sess)) url = 'nodes/%s/actions' % sot.id body = {'check': {}} - sess.post.assert_called_once_with(url, - json=body) + sess.post.assert_called_once_with(url, json=body) def test_recover(self): sot = node.Node(**FAKE) @@ -91,8 +89,7 @@ def test_recover(self): self.assertEqual('', sot.recover(sess)) url = 'nodes/%s/actions' % sot.id body = {'recover': {}} - sess.post.assert_called_once_with(url, - json=body) + sess.post.assert_called_once_with(url, json=body) def test_operation(self): sot = node.Node(**FAKE) @@ -103,8 +100,9 @@ def test_operation(self): sess.post = mock.Mock(return_value=resp) self.assertEqual('', sot.op(sess, 'dance', style='tango')) url = 'nodes/%s/ops' % sot.id - sess.post.assert_called_once_with(url, - json={'dance': {'style': 'tango'}}) + sess.post.assert_called_once_with( + url, json={'dance': {'style': 'tango'}} + ) def test_adopt_preview(self): sot = node.Node.new() @@ -118,12 +116,11 @@ def test_adopt_preview(self): 'identity': 'fake-resource-id', 'overrides': {}, 'type': 'os.nova.server-1.0', - 'snapshot': False + 'snapshot': False, } res = sot.adopt(sess, True, **attrs) self.assertEqual({"foo": "bar"}, res) - sess.post.assert_called_once_with("nodes/adopt-preview", - json=attrs) + sess.post.assert_called_once_with("nodes/adopt-preview", json=attrs) def test_adopt(self): sot = node.Node.new() @@ -136,8 +133,9 @@ def test_adopt(self): res = sot.adopt(sess, False, param="value") self.assertEqual(sot, res) - sess.post.assert_called_once_with("nodes/adopt", - json={"param": "value"}) + sess.post.assert_called_once_with( + "nodes/adopt", json={"param": "value"} + ) def test_force_delete(self): sot = node.Node(**FAKE) @@ -158,7 +156,6 @@ def test_force_delete(self): class TestNodeDetail(base.TestCase): - def test_basic(self): sot = node.NodeDetail() self.assertEqual('/nodes/%(node_id)s?show_details=True', sot.base_path) diff --git a/openstack/tests/unit/clustering/v1/test_policy.py b/openstack/tests/unit/clustering/v1/test_policy.py index 89b72ac02..f2fa94e41 100644 --- a/openstack/tests/unit/clustering/v1/test_policy.py +++ b/openstack/tests/unit/clustering/v1/test_policy.py @@ -28,7 +28,7 @@ 'grace_period': 60, 'reduce_desired_capacity': False, 'destroy_after_deletion': True, - } + }, }, 'project': '42d9e9663331431f97b75e25136307ff', 'domain': '204ccccd267b40aea871750116b5b184', @@ -41,7 +41,6 @@ class TestPolicy(base.TestCase): - def setUp(self): super(TestPolicy, self).setUp() @@ -70,7 +69,6 @@ def test_instantiate(self): class TestPolicyValidate(base.TestCase): - def setUp(self): super(TestPolicyValidate, self).setUp() diff --git a/openstack/tests/unit/clustering/v1/test_policy_type.py b/openstack/tests/unit/clustering/v1/test_policy_type.py index 045366811..7ada53e6d 100644 --- a/openstack/tests/unit/clustering/v1/test_policy_type.py +++ b/openstack/tests/unit/clustering/v1/test_policy_type.py @@ -16,20 +16,12 @@ FAKE = { 'name': 'FAKE_POLICY_TYPE', - 'schema': { - 'foo': 'bar' - }, - 'support_status': { - '1.0': [{ - 'status': 'supported', - 'since': '2016.10' - }] - } + 'schema': {'foo': 'bar'}, + 'support_status': {'1.0': [{'status': 'supported', 'since': '2016.10'}]}, } class TestPolicyType(base.TestCase): - def test_basic(self): sot = policy_type.PolicyType() self.assertEqual('policy_type', sot.resource_key) diff --git a/openstack/tests/unit/clustering/v1/test_profile.py b/openstack/tests/unit/clustering/v1/test_profile.py index d37d17853..de6d3bf7f 100644 --- a/openstack/tests/unit/clustering/v1/test_profile.py +++ b/openstack/tests/unit/clustering/v1/test_profile.py @@ -28,8 +28,8 @@ 'flavor': 1, 'image': 'cirros-0.3.2-x86_64-uec', 'key_name': 'oskey', - 'name': 'cirros_server' - } + 'name': 'cirros_server', + }, }, 'project': '42d9e9663331431f97b75e25136307ff', 'domain': '204ccccd267b40aea871750116b5b184', @@ -41,7 +41,6 @@ class TestProfile(base.TestCase): - def setUp(self): super(TestProfile, self).setUp() @@ -72,7 +71,6 @@ def test_instantiate(self): class TestProfileValidate(base.TestCase): - def setUp(self): super(TestProfileValidate, self).setUp() diff --git a/openstack/tests/unit/clustering/v1/test_profile_type.py b/openstack/tests/unit/clustering/v1/test_profile_type.py index e9e7d99e1..d2f084147 100644 --- a/openstack/tests/unit/clustering/v1/test_profile_type.py +++ b/openstack/tests/unit/clustering/v1/test_profile_type.py @@ -17,20 +17,19 @@ FAKE = { 'name': 'FAKE_PROFILE_TYPE', - 'schema': { - 'foo': 'bar' - }, + 'schema': {'foo': 'bar'}, 'support_status': { - '1.0': [{ - 'status': 'supported', - 'since': '2016.10', - }] - } + '1.0': [ + { + 'status': 'supported', + 'since': '2016.10', + } + ] + }, } class TestProfileType(base.TestCase): - def test_basic(self): sot = profile_type.ProfileType() self.assertEqual('profile_type', sot.resource_key) diff --git a/openstack/tests/unit/clustering/v1/test_proxy.py b/openstack/tests/unit/clustering/v1/test_proxy.py index 2f35248a5..1b4eb4c3a 100644 --- a/openstack/tests/unit/clustering/v1/test_proxy.py +++ b/openstack/tests/unit/clustering/v1/test_proxy.py @@ -36,17 +36,18 @@ def setUp(self): self.proxy = _proxy.Proxy(self.session) def test_build_info_get(self): - self.verify_get(self.proxy.get_build_info, build_info.BuildInfo, - method_args=[], - expected_kwargs={'requires_id': False}) + self.verify_get( + self.proxy.get_build_info, + build_info.BuildInfo, + method_args=[], + expected_kwargs={'requires_id': False}, + ) def test_profile_types(self): - self.verify_list(self.proxy.profile_types, - profile_type.ProfileType) + self.verify_list(self.proxy.profile_types, profile_type.ProfileType) def test_profile_type_get(self): - self.verify_get(self.proxy.get_profile_type, - profile_type.ProfileType) + self.verify_get(self.proxy.get_profile_type, profile_type.ProfileType) def test_policy_types(self): self.verify_list(self.proxy.policy_types, policy_type.PolicyType) @@ -58,8 +59,9 @@ def test_profile_create(self): self.verify_create(self.proxy.create_profile, profile.Profile) def test_profile_validate(self): - self.verify_create(self.proxy.validate_profile, - profile.ProfileValidate) + self.verify_create( + self.proxy.validate_profile, profile.ProfileValidate + ) def test_profile_delete(self): self.verify_delete(self.proxy.delete_profile, profile.Profile, False) @@ -74,9 +76,12 @@ def test_profile_get(self): self.verify_get(self.proxy.get_profile, profile.Profile) def test_profiles(self): - self.verify_list(self.proxy.profiles, profile.Profile, - method_kwargs={'limit': 2}, - expected_kwargs={'limit': 2}) + self.verify_list( + self.proxy.profiles, + profile.Profile, + method_kwargs={'limit': 2}, + expected_kwargs={'limit': 2}, + ) def test_profile_update(self): self.verify_update(self.proxy.update_profile, profile.Profile) @@ -95,7 +100,8 @@ def test_cluster_force_delete(self): "openstack.clustering.v1.cluster.Cluster.force_delete", self.proxy.delete_cluster, method_args=["value", False, True], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_cluster_find(self): self.verify_find(self.proxy.find_cluster, cluster.Cluster) @@ -104,16 +110,18 @@ def test_cluster_get(self): self.verify_get(self.proxy.get_cluster, cluster.Cluster) def test_clusters(self): - self.verify_list(self.proxy.clusters, cluster.Cluster, - method_kwargs={'limit': 2}, - expected_kwargs={'limit': 2}) + self.verify_list( + self.proxy.clusters, + cluster.Cluster, + method_kwargs={'limit': 2}, + expected_kwargs={'limit': 2}, + ) def test_cluster_update(self): self.verify_update(self.proxy.update_cluster, cluster.Cluster) def test_services(self): - self.verify_list(self.proxy.services, - service.Service) + self.verify_list(self.proxy.services, service.Service) @mock.patch.object(proxy_base.Proxy, '_find') def test_resize_cluster(self, mock_find): @@ -125,9 +133,11 @@ def test_resize_cluster(self, mock_find): method_args=["FAKE_CLUSTER"], method_kwargs={'k1': 'v1', 'k2': 'v2'}, expected_args=[self.proxy], - expected_kwargs={'k1': 'v1', 'k2': 'v2'}) - mock_find.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER", - ignore_missing=False) + expected_kwargs={'k1': 'v1', 'k2': 'v2'}, + ) + mock_find.assert_called_once_with( + cluster.Cluster, "FAKE_CLUSTER", ignore_missing=False + ) def test_resize_cluster_with_obj(self): mock_cluster = cluster.Cluster.new(id='FAKE_CLUSTER') @@ -137,15 +147,17 @@ def test_resize_cluster_with_obj(self): method_args=[mock_cluster], method_kwargs={'k1': 'v1', 'k2': 'v2'}, expected_args=[self.proxy], - expected_kwargs={'k1': 'v1', 'k2': 'v2'}) + expected_kwargs={'k1': 'v1', 'k2': 'v2'}, + ) def test_collect_cluster_attrs(self): - self.verify_list(self.proxy.collect_cluster_attrs, - cluster_attr.ClusterAttr, - method_args=['FAKE_ID', 'path.to.attr'], - expected_args=[], - expected_kwargs={'cluster_id': 'FAKE_ID', - 'path': 'path.to.attr'}) + self.verify_list( + self.proxy.collect_cluster_attrs, + cluster_attr.ClusterAttr, + method_args=['FAKE_ID', 'path.to.attr'], + expected_args=[], + expected_kwargs={'cluster_id': 'FAKE_ID', 'path': 'path.to.attr'}, + ) @mock.patch.object(proxy_base.Proxy, '_get_resource') def test_cluster_check(self, mock_get): @@ -155,7 +167,8 @@ def test_cluster_check(self, mock_get): "openstack.clustering.v1.cluster.Cluster.check", self.proxy.check_cluster, method_args=["FAKE_CLUSTER"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) mock_get.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER") @mock.patch.object(proxy_base.Proxy, '_get_resource') @@ -166,7 +179,8 @@ def test_cluster_recover(self, mock_get): "openstack.clustering.v1.cluster.Cluster.recover", self.proxy.recover_cluster, method_args=["FAKE_CLUSTER"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) mock_get.assert_called_once_with(cluster.Cluster, "FAKE_CLUSTER") def test_node_create(self): @@ -183,7 +197,8 @@ def test_node_force_delete(self): "openstack.clustering.v1.node.Node.force_delete", self.proxy.delete_node, method_args=["value", False, True], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) def test_node_find(self): self.verify_find(self.proxy.find_node, node.Node) @@ -198,12 +213,16 @@ def test_node_get_with_details(self): method_args=['NODE_ID'], method_kwargs={'details': True}, expected_args=[node.NodeDetail], - expected_kwargs={'node_id': 'NODE_ID', 'requires_id': False}) + expected_kwargs={'node_id': 'NODE_ID', 'requires_id': False}, + ) def test_nodes(self): - self.verify_list(self.proxy.nodes, node.Node, - method_kwargs={'limit': 2}, - expected_kwargs={'limit': 2}) + self.verify_list( + self.proxy.nodes, + node.Node, + method_kwargs={'limit': 2}, + expected_kwargs={'limit': 2}, + ) def test_node_update(self): self.verify_update(self.proxy.update_node, node.Node) @@ -216,7 +235,8 @@ def test_node_check(self, mock_get): "openstack.clustering.v1.node.Node.check", self.proxy.check_node, method_args=["FAKE_NODE"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) mock_get.assert_called_once_with(node.Node, "FAKE_NODE") @mock.patch.object(proxy_base.Proxy, '_get_resource') @@ -227,7 +247,8 @@ def test_node_recover(self, mock_get): "openstack.clustering.v1.node.Node.recover", self.proxy.recover_node, method_args=["FAKE_NODE"], - expected_args=[self.proxy]) + expected_args=[self.proxy], + ) mock_get.assert_called_once_with(node.Node, "FAKE_NODE") @mock.patch.object(proxy_base.Proxy, '_get_resource') @@ -239,7 +260,8 @@ def test_node_adopt(self, mock_get): self.proxy.adopt_node, method_kwargs={"preview": False, "foo": "bar"}, expected_args=[self.proxy], - expected_kwargs={"preview": False, "foo": "bar"}) + expected_kwargs={"preview": False, "foo": "bar"}, + ) mock_get.assert_called_once_with(node.Node, None) @@ -252,7 +274,8 @@ def test_node_adopt_preview(self, mock_get): self.proxy.adopt_node, method_kwargs={"preview": True, "foo": "bar"}, expected_args=[self.proxy], - expected_kwargs={"preview": True, "foo": "bar"}) + expected_kwargs={"preview": True, "foo": "bar"}, + ) mock_get.assert_called_once_with(node.Node, None) @@ -275,19 +298,24 @@ def test_policy_get(self): self.verify_get(self.proxy.get_policy, policy.Policy) def test_policies(self): - self.verify_list(self.proxy.policies, policy.Policy, - method_kwargs={'limit': 2}, - expected_kwargs={'limit': 2}) + self.verify_list( + self.proxy.policies, + policy.Policy, + method_kwargs={'limit': 2}, + expected_kwargs={'limit': 2}, + ) def test_policy_update(self): self.verify_update(self.proxy.update_policy, policy.Policy) def test_cluster_policies(self): - self.verify_list(self.proxy.cluster_policies, - cluster_policy.ClusterPolicy, - method_args=["FAKE_CLUSTER"], - expected_args=[], - expected_kwargs={"cluster_id": "FAKE_CLUSTER"}) + self.verify_list( + self.proxy.cluster_policies, + cluster_policy.ClusterPolicy, + method_args=["FAKE_CLUSTER"], + expected_args=[], + expected_kwargs={"cluster_id": "FAKE_CLUSTER"}, + ) def test_get_cluster_policy(self): fake_policy = cluster_policy.ClusterPolicy.new(id="FAKE_POLICY") @@ -300,7 +328,8 @@ def test_get_cluster_policy(self): method_args=[fake_policy, "FAKE_CLUSTER"], expected_args=[cluster_policy.ClusterPolicy, fake_policy], expected_kwargs={'cluster_id': 'FAKE_CLUSTER'}, - expected_result=fake_policy) + expected_result=fake_policy, + ) # Policy ID as input self._verify( @@ -308,7 +337,8 @@ def test_get_cluster_policy(self): self.proxy.get_cluster_policy, method_args=["FAKE_POLICY", "FAKE_CLUSTER"], expected_args=[cluster_policy.ClusterPolicy, "FAKE_POLICY"], - expected_kwargs={"cluster_id": "FAKE_CLUSTER"}) + expected_kwargs={"cluster_id": "FAKE_CLUSTER"}, + ) # Cluster object as input self._verify( @@ -316,7 +346,8 @@ def test_get_cluster_policy(self): self.proxy.get_cluster_policy, method_args=["FAKE_POLICY", fake_cluster], expected_args=[cluster_policy.ClusterPolicy, "FAKE_POLICY"], - expected_kwargs={"cluster_id": fake_cluster}) + expected_kwargs={"cluster_id": fake_cluster}, + ) def test_receiver_create(self): self.verify_create(self.proxy.create_receiver, receiver.Receiver) @@ -325,8 +356,9 @@ def test_receiver_update(self): self.verify_update(self.proxy.update_receiver, receiver.Receiver) def test_receiver_delete(self): - self.verify_delete(self.proxy.delete_receiver, receiver.Receiver, - False) + self.verify_delete( + self.proxy.delete_receiver, receiver.Receiver, False + ) def test_receiver_delete_ignore(self): self.verify_delete(self.proxy.delete_receiver, receiver.Receiver, True) @@ -338,17 +370,23 @@ def test_receiver_get(self): self.verify_get(self.proxy.get_receiver, receiver.Receiver) def test_receivers(self): - self.verify_list(self.proxy.receivers, receiver.Receiver, - method_kwargs={'limit': 2}, - expected_kwargs={'limit': 2}) + self.verify_list( + self.proxy.receivers, + receiver.Receiver, + method_kwargs={'limit': 2}, + expected_kwargs={'limit': 2}, + ) def test_action_get(self): self.verify_get(self.proxy.get_action, action.Action) def test_actions(self): - self.verify_list(self.proxy.actions, action.Action, - method_kwargs={'limit': 2}, - expected_kwargs={'limit': 2}) + self.verify_list( + self.proxy.actions, + action.Action, + method_kwargs={'limit': 2}, + expected_kwargs={'limit': 2}, + ) def test_action_update(self): self.verify_update(self.proxy.update_action, action.Action) @@ -357,9 +395,12 @@ def test_event_get(self): self.verify_get(self.proxy.get_event, event.Event) def test_events(self): - self.verify_list(self.proxy.events, event.Event, - method_kwargs={'limit': 2}, - expected_kwargs={'limit': 2}) + self.verify_list( + self.proxy.events, + event.Event, + method_kwargs={'limit': 2}, + expected_kwargs={'limit': 2}, + ) @mock.patch("openstack.resource.wait_for_status") def test_wait_for(self, mock_wait): @@ -368,8 +409,9 @@ def test_wait_for(self, mock_wait): self.proxy.wait_for_status(mock_resource, 'ACTIVE') - mock_wait.assert_called_once_with(self.proxy, mock_resource, - 'ACTIVE', [], 2, 120) + mock_wait.assert_called_once_with( + self.proxy, mock_resource, 'ACTIVE', [], 2, 120 + ) @mock.patch("openstack.resource.wait_for_status") def test_wait_for_params(self, mock_wait): @@ -378,8 +420,9 @@ def test_wait_for_params(self, mock_wait): self.proxy.wait_for_status(mock_resource, 'ACTIVE', ['ERROR'], 1, 2) - mock_wait.assert_called_once_with(self.proxy, mock_resource, - 'ACTIVE', ['ERROR'], 1, 2) + mock_wait.assert_called_once_with( + self.proxy, mock_resource, 'ACTIVE', ['ERROR'], 1, 2 + ) @mock.patch("openstack.resource.wait_for_delete") def test_wait_for_delete(self, mock_wait): @@ -405,7 +448,8 @@ def test_get_cluster_metadata(self): self.proxy.get_cluster_metadata, method_args=["value"], expected_args=[self.proxy], - expected_result=cluster.Cluster(id="value", metadata={})) + expected_result=cluster.Cluster(id="value", metadata={}), + ) def test_set_cluster_metadata(self): kwargs = {"a": "1", "b": "2"} @@ -415,12 +459,11 @@ def test_set_cluster_metadata(self): self.proxy.set_cluster_metadata, method_args=[id], method_kwargs=kwargs, - method_result=cluster.Cluster.existing( - id=id, metadata=kwargs), + method_result=cluster.Cluster.existing(id=id, metadata=kwargs), expected_args=[self.proxy], expected_kwargs={'metadata': kwargs}, - expected_result=cluster.Cluster.existing( - id=id, metadata=kwargs)) + expected_result=cluster.Cluster.existing(id=id, metadata=kwargs), + ) def test_delete_cluster_metadata(self): self._verify( @@ -428,4 +471,5 @@ def test_delete_cluster_metadata(self): self.proxy.delete_cluster_metadata, expected_result=None, method_args=["value", ["key"]], - expected_args=[self.proxy, "key"]) + expected_args=[self.proxy, "key"], + ) diff --git a/openstack/tests/unit/clustering/v1/test_receiver.py b/openstack/tests/unit/clustering/v1/test_receiver.py index 5a46d6b1d..73b33bb0d 100644 --- a/openstack/tests/unit/clustering/v1/test_receiver.py +++ b/openstack/tests/unit/clustering/v1/test_receiver.py @@ -26,10 +26,7 @@ 'created_at': '2015-10-10T12:46:36.000000', 'updated_at': '2016-10-10T12:46:36.000000', 'actor': {}, - 'params': { - 'adjustment_type': 'CHANGE_IN_CAPACITY', - 'adjustment': 2 - }, + 'params': {'adjustment_type': 'CHANGE_IN_CAPACITY', 'adjustment': 2}, 'channel': { 'alarm_url': 'http://host:port/webhooks/AN_ID/trigger?V=1', }, @@ -40,7 +37,6 @@ class TestReceiver(base.TestCase): - def setUp(self): super(TestReceiver, self).setUp() diff --git a/openstack/tests/unit/clustering/v1/test_service.py b/openstack/tests/unit/clustering/v1/test_service.py index 3824bebdf..4d62202f1 100644 --- a/openstack/tests/unit/clustering/v1/test_service.py +++ b/openstack/tests/unit/clustering/v1/test_service.py @@ -27,7 +27,6 @@ class TestService(base.TestCase): - def setUp(self): super(TestService, self).setUp() self.resp = mock.Mock() From 004c7352d0a4fb467a319ae9743eb6ca5ee9ce7f Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 5 May 2023 11:22:56 +0100 Subject: [PATCH 3256/3836] Blackify openstack.cloud Black used with the '-l 79 -S' flags. A future change will ignore this commit in git-blame history by adding a 'git-blame-ignore-revs' file. Change-Id: Ib58bb45ce8c29e5347ffc36d40d6f5d52b140c6b Signed-off-by: Stephen Finucane --- openstack/cloud/_accelerator.py | 4 +- openstack/cloud/_baremetal.py | 190 +- openstack/cloud/_block_storage.py | 84 +- openstack/cloud/_clustering.py | 4 +- openstack/cloud/_coe.py | 68 +- openstack/cloud/_compute.py | 486 +-- openstack/cloud/_dns.py | 51 +- openstack/cloud/_floating_ip.py | 520 ++- openstack/cloud/_identity.py | 278 +- openstack/cloud/_image.py | 79 +- openstack/cloud/_network.py | 748 +++-- openstack/cloud/_network_common.py | 138 +- openstack/cloud/_object_store.py | 105 +- openstack/cloud/_orchestration.py | 120 +- openstack/cloud/_security_group.py | 193 +- openstack/cloud/_utils.py | 89 +- openstack/cloud/cmd/inventory.py | 43 +- openstack/cloud/exc.py | 8 +- openstack/cloud/inventory.py | 32 +- openstack/cloud/meta.py | 111 +- openstack/cloud/openstackcloud.py | 147 +- .../tests/functional/cloud/test_aggregate.py | 18 +- .../cloud/test_cluster_templates.py | 29 +- .../tests/functional/cloud/test_clustering.py | 810 ++--- .../tests/functional/cloud/test_compute.py | 182 +- .../tests/functional/cloud/test_devstack.py | 14 +- .../tests/functional/cloud/test_domain.py | 34 +- .../tests/functional/cloud/test_endpoints.py | 84 +- .../tests/functional/cloud/test_flavor.py | 26 +- .../functional/cloud/test_floating_ip.py | 97 +- .../functional/cloud/test_floating_ip_pool.py | 3 +- .../tests/functional/cloud/test_groups.py | 18 +- .../tests/functional/cloud/test_identity.py | 250 +- .../tests/functional/cloud/test_image.py | 39 +- .../tests/functional/cloud/test_inventory.py | 9 +- .../tests/functional/cloud/test_keypairs.py | 4 +- .../tests/functional/cloud/test_limits.py | 1 - .../functional/cloud/test_magnum_services.py | 1 - .../tests/functional/cloud/test_network.py | 11 +- .../tests/functional/cloud/test_object.py | 157 +- openstack/tests/functional/cloud/test_port.py | 22 +- .../tests/functional/cloud/test_project.py | 45 +- .../functional/cloud/test_project_cleanup.py | 69 +- .../cloud/test_qos_bandwidth_limit_rule.py | 38 +- .../cloud/test_qos_dscp_marking_rule.py | 18 +- .../cloud/test_qos_minimum_bandwidth_rule.py | 18 +- .../tests/functional/cloud/test_qos_policy.py | 20 +- .../tests/functional/cloud/test_quotas.py | 51 +- .../functional/cloud/test_range_search.py | 18 +- .../tests/functional/cloud/test_recordset.py | 48 +- .../tests/functional/cloud/test_router.py | 144 +- .../functional/cloud/test_security_groups.py | 15 +- .../functional/cloud/test_server_group.py | 9 +- .../tests/functional/cloud/test_services.py | 58 +- .../tests/functional/cloud/test_stack.py | 30 +- .../tests/functional/cloud/test_users.py | 36 +- .../tests/functional/cloud/test_volume.py | 28 +- .../functional/cloud/test_volume_backup.py | 36 +- .../functional/cloud/test_volume_type.py | 71 +- openstack/tests/functional/cloud/test_zone.py | 11 +- openstack/tests/unit/cloud/test__utils.py | 128 +- .../tests/unit/cloud/test_accelerator.py | 443 ++- openstack/tests/unit/cloud/test_aggregate.py | 309 +- .../unit/cloud/test_availability_zones.py | 77 +- .../tests/unit/cloud/test_baremetal_node.py | 2947 ++++++++++------- .../tests/unit/cloud/test_baremetal_ports.py | 158 +- openstack/tests/unit/cloud/test_caching.py | 718 ++-- .../unit/cloud/test_cluster_templates.py | 238 +- openstack/tests/unit/cloud/test_clustering.py | 15 +- .../tests/unit/cloud/test_coe_clusters.py | 4 +- .../cloud/test_coe_clusters_certificate.py | 80 +- .../tests/unit/cloud/test_create_server.py | 2016 ++++++----- .../unit/cloud/test_create_volume_snapshot.py | 158 +- .../tests/unit/cloud/test_delete_server.py | 577 ++-- .../unit/cloud/test_delete_volume_snapshot.py | 132 +- .../tests/unit/cloud/test_domain_params.py | 24 +- openstack/tests/unit/cloud/test_domains.py | 300 +- openstack/tests/unit/cloud/test_endpoints.py | 393 ++- openstack/tests/unit/cloud/test_flavors.py | 424 ++- .../unit/cloud/test_floating_ip_common.py | 179 +- .../unit/cloud/test_floating_ip_neutron.py | 1953 ++++++----- .../tests/unit/cloud/test_floating_ip_nova.py | 416 ++- .../tests/unit/cloud/test_floating_ip_pool.py | 96 +- openstack/tests/unit/cloud/test_fwaas.py | 1983 +++++++---- openstack/tests/unit/cloud/test_groups.py | 157 +- .../tests/unit/cloud/test_identity_roles.py | 310 +- .../tests/unit/cloud/test_identity_users.py | 84 +- openstack/tests/unit/cloud/test_image.py | 2648 +++++++++------ .../tests/unit/cloud/test_image_snapshot.py | 133 +- openstack/tests/unit/cloud/test_inventory.py | 23 +- openstack/tests/unit/cloud/test_keypair.py | 199 +- openstack/tests/unit/cloud/test_limits.py | 148 +- .../tests/unit/cloud/test_magnum_services.py | 19 +- openstack/tests/unit/cloud/test_meta.py | 1316 +++++--- openstack/tests/unit/cloud/test_network.py | 580 ++-- openstack/tests/unit/cloud/test_object.py | 2136 +++++++----- .../tests/unit/cloud/test_openstackcloud.py | 66 +- openstack/tests/unit/cloud/test_operator.py | 123 +- .../tests/unit/cloud/test_operator_noauth.py | 199 +- openstack/tests/unit/cloud/test_port.py | 528 +-- openstack/tests/unit/cloud/test_project.py | 272 +- .../cloud/test_qos_bandwidth_limit_rule.py | 727 ++-- .../unit/cloud/test_qos_dscp_marking_rule.py | 528 ++- .../cloud/test_qos_minimum_bandwidth_rule.py | 532 ++- openstack/tests/unit/cloud/test_qos_policy.py | 550 +-- .../tests/unit/cloud/test_qos_rule_type.py | 233 +- openstack/tests/unit/cloud/test_quotas.py | 439 ++- .../tests/unit/cloud/test_rebuild_server.py | 358 +- openstack/tests/unit/cloud/test_recordset.py | 708 ++-- .../tests/unit/cloud/test_role_assignment.py | 2412 +++++++++----- openstack/tests/unit/cloud/test_router.py | 663 ++-- .../tests/unit/cloud/test_security_groups.py | 1255 ++++--- .../tests/unit/cloud/test_server_console.py | 90 +- .../unit/cloud/test_server_delete_metadata.py | 89 +- .../tests/unit/cloud/test_server_group.py | 74 +- .../unit/cloud/test_server_set_metadata.py | 86 +- openstack/tests/unit/cloud/test_services.py | 362 +- openstack/tests/unit/cloud/test_shade.py | 638 ++-- .../unit/cloud/test_shared_file_system.py | 22 +- openstack/tests/unit/cloud/test_stack.py | 1168 ++++--- openstack/tests/unit/cloud/test_subnet.py | 937 ++++-- .../tests/unit/cloud/test_update_server.py | 135 +- openstack/tests/unit/cloud/test_usage.py | 86 +- openstack/tests/unit/cloud/test_users.py | 237 +- openstack/tests/unit/cloud/test_volume.py | 824 +++-- .../tests/unit/cloud/test_volume_access.py | 368 +- .../tests/unit/cloud/test_volume_backups.py | 313 +- openstack/tests/unit/cloud/test_zone.py | 284 +- 128 files changed, 26621 insertions(+), 16276 deletions(-) diff --git a/openstack/cloud/_accelerator.py b/openstack/cloud/_accelerator.py index b4705815e..5cd7b124f 100644 --- a/openstack/cloud/_accelerator.py +++ b/openstack/cloud/_accelerator.py @@ -70,7 +70,7 @@ def delete_device_profile(self, name_or_id, filters): """ device_profile = self.accelerator.get_device_profile( name_or_id, - filters + filters, ) if device_profile is None: self.log.debug( @@ -104,7 +104,7 @@ def delete_accelerator_request(self, name_or_id, filters): """ accelerator_request = self.accelerator.get_accelerator_request( name_or_id, - filters + filters, ) if accelerator_request is None: self.log.debug( diff --git a/openstack/cloud/_baremetal.py b/openstack/cloud/_baremetal.py index 35895c35f..e856496b9 100644 --- a/openstack/cloud/_baremetal.py +++ b/openstack/cloud/_baremetal.py @@ -39,7 +39,8 @@ def _normalize_port_list(nics): except KeyError: raise TypeError( "Either 'address' or 'mac' must be provided " - "for port %s" % row) + "for port %s" % row + ) ports.append(dict(row, address=address)) return ports @@ -136,32 +137,34 @@ def inspect_machine(self, name_or_id, wait=False, timeout=3600): raise exc.OpenStackCloudException( "Refusing to inspect available machine %(node)s " "which is associated with an instance " - "(instance_uuid %(inst)s)" % - {'node': node.id, 'inst': node.instance_id}) + "(instance_uuid %(inst)s)" + % {'node': node.id, 'inst': node.instance_id} + ) return_to_available = True # NOTE(TheJulia): Changing available machine to managedable state # and due to state transitions we need to until that transition has # completed. - node = self.baremetal.set_node_provision_state(node, 'manage', - wait=True, - timeout=timeout) + node = self.baremetal.set_node_provision_state( + node, 'manage', wait=True, timeout=timeout + ) if node.provision_state not in ('manageable', 'inspect failed'): raise exc.OpenStackCloudException( "Machine %(node)s must be in 'manageable', 'inspect failed' " "or 'available' provision state to start inspection, the " - "current state is %(state)s" % - {'node': node.id, 'state': node.provision_state}) + "current state is %(state)s" + % {'node': node.id, 'state': node.provision_state} + ) - node = self.baremetal.set_node_provision_state(node, 'inspect', - wait=True, - timeout=timeout) + node = self.baremetal.set_node_provision_state( + node, 'inspect', wait=True, timeout=timeout + ) if return_to_available: - node = self.baremetal.set_node_provision_state(node, 'provide', - wait=True, - timeout=timeout) + node = self.baremetal.set_node_provision_state( + node, 'provide', wait=True, timeout=timeout + ) return node @@ -170,19 +173,27 @@ def _delete_node_on_error(self, node): try: yield except Exception as exc: - self.log.debug("cleaning up node %s because of an error: %s", - node.id, exc) + self.log.debug( + "cleaning up node %s because of an error: %s", node.id, exc + ) tb = sys.exc_info()[2] try: self.baremetal.delete_node(node) except Exception: - self.log.debug("could not remove node %s", node.id, - exc_info=True) + self.log.debug( + "could not remove node %s", node.id, exc_info=True + ) raise exc.with_traceback(tb) - def register_machine(self, nics, wait=False, timeout=3600, - lock_timeout=600, provision_state='available', - **kwargs): + def register_machine( + self, + nics, + wait=False, + timeout=3600, + lock_timeout=600, + provision_state='available', + **kwargs + ): """Register Baremetal with Ironic Allows for the registration of Baremetal nodes with Ironic @@ -233,9 +244,10 @@ def register_machine(self, nics, wait=False, timeout=3600, :returns: Current state of the node. """ if provision_state not in ('enroll', 'manageable', 'available'): - raise ValueError('Initial provision state must be enroll, ' - 'manageable or available, got %s' - % provision_state) + raise ValueError( + 'Initial provision state must be enroll, ' + 'manageable or available, got %s' % provision_state + ) # Available is tricky: it cannot be directly requested on newer API # versions, we need to go through cleaning. But we cannot go through @@ -246,19 +258,24 @@ def register_machine(self, nics, wait=False, timeout=3600, with self._delete_node_on_error(machine): # Making a node at least manageable - if (machine.provision_state == 'enroll' - and provision_state != 'enroll'): + if ( + machine.provision_state == 'enroll' + and provision_state != 'enroll' + ): machine = self.baremetal.set_node_provision_state( - machine, 'manage', wait=True, timeout=timeout) + machine, 'manage', wait=True, timeout=timeout + ) machine = self.baremetal.wait_for_node_reservation( - machine, timeout=lock_timeout) + machine, timeout=lock_timeout + ) # Create NICs before trying to run cleaning created_nics = [] try: for port in _normalize_port_list(nics): - nic = self.baremetal.create_port(node_id=machine.id, - **port) + nic = self.baremetal.create_port( + node_id=machine.id, **port + ) created_nics.append(nic.id) except Exception: @@ -269,10 +286,13 @@ def register_machine(self, nics, wait=False, timeout=3600, pass raise - if (machine.provision_state != 'available' - and provision_state == 'available'): + if ( + machine.provision_state != 'available' + and provision_state == 'available' + ): machine = self.baremetal.set_node_provision_state( - machine, 'provide', wait=wait, timeout=timeout) + machine, 'provide', wait=wait, timeout=timeout + ) return machine @@ -295,15 +315,18 @@ def unregister_machine(self, nics, uuid, wait=None, timeout=600): :raises: OpenStackCloudException on operation failure. """ if wait is not None: - warnings.warn("wait argument is deprecated and has no effect", - DeprecationWarning) + warnings.warn( + "wait argument is deprecated and has no effect", + DeprecationWarning, + ) machine = self.get_machine(uuid) invalid_states = ['active', 'cleaning', 'clean wait', 'clean failed'] if machine['provision_state'] in invalid_states: raise exc.OpenStackCloudException( "Error unregistering node '%s' due to current provision " - "state '%s'" % (uuid, machine['provision_state'])) + "state '%s'" % (uuid, machine['provision_state']) + ) # NOTE(TheJulia) There is a high possibility of a lock being present # if the machine was just moved through the state machine. This was @@ -314,7 +337,8 @@ def unregister_machine(self, nics, uuid, wait=None, timeout=600): except exc.OpenStackCloudException as e: raise exc.OpenStackCloudException( "Error unregistering node '%s': Exception occured while" - " waiting to be able to proceed: %s" % (machine['uuid'], e)) + " waiting to be able to proceed: %s" % (machine['uuid'], e) + ) for nic in _normalize_port_list(nics): try: @@ -381,32 +405,28 @@ def update_machine(self, name_or_id, **attrs): machine = self.get_machine(name_or_id) if not machine: raise exc.OpenStackCloudException( - "Machine update failed to find Machine: %s. " % name_or_id) + "Machine update failed to find Machine: %s. " % name_or_id + ) new_config = dict(machine._to_munch(), **attrs) try: patch = jsonpatch.JsonPatch.from_diff( - machine._to_munch(), - new_config) + machine._to_munch(), new_config + ) except Exception as e: raise exc.OpenStackCloudException( "Machine update failed - Error generating JSON patch object " "for submission to the API. Machine: %s Error: %s" - % (name_or_id, e)) + % (name_or_id, e) + ) if not patch: - return dict( - node=machine, - changes=None - ) + return dict(node=machine, changes=None) change_list = [change['path'] for change in patch] node = self.baremetal.update_node(machine, **attrs) - return dict( - node=node, - changes=change_list - ) + return dict(node=node, changes=change_list) def attach_port_to_machine(self, name_or_id, port_name_or_id): """Attach a virtual port to the bare metal machine. @@ -459,16 +479,16 @@ def validate_machine(self, name_or_id, for_deploy=True): self.baremetal.validate_node(name_or_id, required=ifaces) def validate_node(self, uuid): - warnings.warn('validate_node is deprecated, please use ' - 'validate_machine instead', DeprecationWarning) + warnings.warn( + 'validate_node is deprecated, please use ' + 'validate_machine instead', + DeprecationWarning, + ) self.baremetal.validate_node(uuid) - def node_set_provision_state(self, - name_or_id, - state, - configdrive=None, - wait=False, - timeout=3600): + def node_set_provision_state( + self, name_or_id, state, configdrive=None, wait=False, timeout=3600 + ): """Set Node Provision State Enables a user to provision a Machine and optionally define a @@ -495,15 +515,17 @@ def node_set_provision_state(self, :rtype: :class:`~openstack.baremetal.v1.node.Node`. """ node = self.baremetal.set_node_provision_state( - name_or_id, target=state, config_drive=configdrive, - wait=wait, timeout=timeout) + name_or_id, + target=state, + config_drive=configdrive, + wait=wait, + timeout=timeout, + ) return node def set_machine_maintenance_state( - self, - name_or_id, - state=True, - reason=None): + self, name_or_id, state=True, reason=None + ): """Set Baremetal Machine Maintenance State Sets Baremetal maintenance state and maintenance reason. @@ -587,28 +609,33 @@ def set_machine_power_reboot(self, name_or_id): """ self.baremetal.set_node_power_state(name_or_id, 'rebooting') - def activate_node(self, uuid, configdrive=None, - wait=False, timeout=1200): + def activate_node(self, uuid, configdrive=None, wait=False, timeout=1200): self.node_set_provision_state( - uuid, 'active', configdrive, wait=wait, timeout=timeout) + uuid, 'active', configdrive, wait=wait, timeout=timeout + ) - def deactivate_node(self, uuid, wait=False, - timeout=1200): + def deactivate_node(self, uuid, wait=False, timeout=1200): self.node_set_provision_state( - uuid, 'deleted', wait=wait, timeout=timeout) + uuid, 'deleted', wait=wait, timeout=timeout + ) def set_node_instance_info(self, uuid, patch): - warnings.warn("The set_node_instance_info call is deprecated, " - "use patch_machine or update_machine instead", - DeprecationWarning) + warnings.warn( + "The set_node_instance_info call is deprecated, " + "use patch_machine or update_machine instead", + DeprecationWarning, + ) return self.patch_machine(uuid, patch) def purge_node_instance_info(self, uuid): - warnings.warn("The purge_node_instance_info call is deprecated, " - "use patch_machine or update_machine instead", - DeprecationWarning) - return self.patch_machine(uuid, - dict(path='/instance_info', op='remove')) + warnings.warn( + "The purge_node_instance_info call is deprecated, " + "use patch_machine or update_machine instead", + DeprecationWarning, + ) + return self.patch_machine( + uuid, dict(path='/instance_info', op='remove') + ) def wait_for_baremetal_node_lock(self, node, timeout=30): """Wait for a baremetal node to have no lock. @@ -618,7 +645,10 @@ def wait_for_baremetal_node_lock(self, node, timeout=30): :raises: OpenStackCloudException upon client failure. :returns: None """ - warnings.warn("The wait_for_baremetal_node_lock call is deprecated " - "in favor of wait_for_node_reservation on the baremetal " - "proxy", DeprecationWarning) + warnings.warn( + "The wait_for_baremetal_node_lock call is deprecated " + "in favor of wait_for_node_reservation on the baremetal " + "proxy", + DeprecationWarning, + ) self.baremetal.wait_for_node_reservation(node, timeout) diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index 1ab4d3fac..c63ab716b 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -127,8 +127,7 @@ def get_volume_type(self, name_or_id, filters=None): :returns: A volume ``Type`` object if found, else None. """ - return _utils._get_entity( - self, 'volume_type', name_or_id, filters) + return _utils._get_entity(self, 'volume_type', name_or_id, filters) def create_volume( self, @@ -162,7 +161,9 @@ def create_volume( raise exc.OpenStackCloudException( "Image {image} was requested as the basis for a new" " volume, but was not found on the cloud".format( - image=image)) + image=image + ) + ) kwargs['imageRef'] = image_obj['id'] kwargs = self._get_volume_kwargs(kwargs) kwargs['size'] = size @@ -193,10 +194,10 @@ def update_volume(self, name_or_id, **kwargs): volume = self.get_volume(name_or_id) if not volume: raise exc.OpenStackCloudException( - "Volume %s not found." % name_or_id) + "Volume %s not found." % name_or_id + ) - volume = self.block_storage.update_volume( - volume, **kwargs) + volume = self.block_storage.update_volume(volume, **kwargs) self.list_volumes.invalidate(self) @@ -219,7 +220,9 @@ def set_volume_bootable(self, name_or_id, bootable=True): if not volume: raise exc.OpenStackCloudException( "Volume {name_or_id} does not exist".format( - name_or_id=name_or_id)) + name_or_id=name_or_id + ) + ) self.block_storage.set_volume_bootable_status(volume, bootable) @@ -249,7 +252,8 @@ def delete_volume( self.log.debug( "Volume %(name_or_id)s does not exist", {'name_or_id': name_or_id}, - exc_info=True) + exc_info=True, + ) return False try: self.block_storage.delete_volume(volume, force=force) @@ -297,10 +301,12 @@ def get_volume_limits(self, name_or_id=None): project_id = proj.id params['tenant_id'] = project_id error_msg = "{msg} for the project: {project} ".format( - msg=error_msg, project=name_or_id) + msg=error_msg, project=name_or_id + ) data = proxy._json_response( - self.block_storage.get('/limits', params=params)) + self.block_storage.get('/limits', params=params) + ) limits = self._get_and_munchify('limits', data) return limits @@ -413,22 +419,23 @@ def attach_volume( # If we got volume as dict we need to re-fetch it to be able to # use wait_for_status. volume = self.block_storage.get_volume(volume['id']) - self.block_storage.wait_for_status( - volume, 'in-use', wait=timeout) + self.block_storage.wait_for_status(volume, 'in-use', wait=timeout) return attachment def _get_volume_kwargs(self, kwargs): name = kwargs.pop('name', kwargs.pop('display_name', None)) - description = kwargs.pop('description', - kwargs.pop('display_description', None)) + description = kwargs.pop( + 'description', kwargs.pop('display_description', None) + ) if name: kwargs['name'] = name if description: kwargs['description'] = description return kwargs - @_utils.valid_kwargs('name', 'display_name', - 'description', 'display_description') + @_utils.valid_kwargs( + 'name', 'display_name', 'description', 'display_description' + ) def create_volume_snapshot( self, volume_id, @@ -459,7 +466,8 @@ def create_volume_snapshot( snapshot = self.block_storage.create_snapshot(**payload) if wait: snapshot = self.block_storage.wait_for_status( - snapshot, wait=timeout) + snapshot, wait=timeout + ) return snapshot @@ -499,8 +507,7 @@ def get_volume_snapshot(self, name_or_id, filters=None): :returns: A volume ``Snapshot`` object if found, else None. """ - return _utils._get_entity(self, 'volume_snapshot', name_or_id, - filters) + return _utils._get_entity(self, 'volume_snapshot', name_or_id, filters) def create_volume_backup( self, @@ -572,8 +579,7 @@ def get_volume_backup(self, name_or_id, filters=None): :returns: A volume ``Backup`` object if found, else None. """ - return _utils._get_entity(self, 'volume_backup', name_or_id, - filters) + return _utils._get_entity(self, 'volume_backup', name_or_id, filters) def list_volume_snapshots(self, detailed=True, filters=None): """List all volume snapshots. @@ -615,8 +621,9 @@ def list_volume_backups(self, detailed=True, filters=None): return list(self.block_storage.backups(details=detailed, **filters)) - def delete_volume_backup(self, name_or_id=None, force=False, wait=False, - timeout=None): + def delete_volume_backup( + self, name_or_id=None, force=False, wait=False, timeout=None + ): """Delete a volume backup. :param name_or_id: Name or unique ID of the volume backup. @@ -635,7 +642,8 @@ def delete_volume_backup(self, name_or_id=None, force=False, wait=False, return False self.block_storage.delete_backup( - volume_backup, ignore_missing=False, force=force) + volume_backup, ignore_missing=False, force=force + ) if wait: self.block_storage.wait_for_delete(volume_backup, wait=timeout) @@ -663,7 +671,8 @@ def delete_volume_snapshot( return False self.block_storage.delete_snapshot( - volumesnapshot, ignore_missing=False) + volumesnapshot, ignore_missing=False + ) if wait: self.block_storage.wait_for_delete(volumesnapshot, wait=timeout) @@ -695,8 +704,7 @@ def search_volumes(self, name_or_id=None, filters=None): :returns: A list of volume ``Volume`` objects, if any are found. """ volumes = self.list_volumes() - return _utils._filter_list( - volumes, name_or_id, filters) + return _utils._filter_list(volumes, name_or_id, filters) def search_volume_snapshots(self, name_or_id=None, filters=None): """Search for one or more volume snapshots. @@ -723,8 +731,7 @@ def search_volume_snapshots(self, name_or_id=None, filters=None): :returns: A list of volume ``Snapshot`` objects, if any are found. """ volumesnapshots = self.list_volume_snapshots() - return _utils._filter_list( - volumesnapshots, name_or_id, filters) + return _utils._filter_list(volumesnapshots, name_or_id, filters) def search_volume_backups(self, name_or_id=None, filters=None): """Search for one or more volume backups. @@ -751,8 +758,7 @@ def search_volume_backups(self, name_or_id=None, filters=None): :returns: A list of volume ``Backup`` objects, if any are found. """ volume_backups = self.list_volume_backups() - return _utils._filter_list( - volume_backups, name_or_id, filters) + return _utils._filter_list(volume_backups, name_or_id, filters) # TODO(stephenfin): Remove 'get_extra' in a future major version def search_volume_types( @@ -797,7 +803,8 @@ def get_volume_type_access(self, name_or_id): volume_type = self.get_volume_type(name_or_id) if not volume_type: raise exc.OpenStackCloudException( - "VolumeType not found: %s" % name_or_id) + "VolumeType not found: %s" % name_or_id + ) return self.block_storage.get_type_access(volume_type) @@ -814,7 +821,8 @@ def add_volume_type_access(self, name_or_id, project_id): volume_type = self.get_volume_type(name_or_id) if not volume_type: raise exc.OpenStackCloudException( - "VolumeType not found: %s" % name_or_id) + "VolumeType not found: %s" % name_or_id + ) self.block_storage.add_type_access(volume_type, project_id) @@ -829,7 +837,8 @@ def remove_volume_type_access(self, name_or_id, project_id): volume_type = self.get_volume_type(name_or_id) if not volume_type: raise exc.OpenStackCloudException( - "VolumeType not found: %s" % name_or_id) + "VolumeType not found: %s" % name_or_id + ) self.block_storage.remove_type_access(volume_type, project_id) def set_volume_quotas(self, name_or_id, **kwargs): @@ -842,12 +851,11 @@ def set_volume_quotas(self, name_or_id, **kwargs): quota does not exist. """ - proj = self.identity.find_project( - name_or_id, ignore_missing=False) + proj = self.identity.find_project(name_or_id, ignore_missing=False) self.block_storage.update_quota_set( - _qs.QuotaSet(project_id=proj.id), - **kwargs) + _qs.QuotaSet(project_id=proj.id), **kwargs + ) def get_volume_quotas(self, name_or_id): """Get volume quotas for a project diff --git a/openstack/cloud/_clustering.py b/openstack/cloud/_clustering.py index c04367693..73851f5e4 100644 --- a/openstack/cloud/_clustering.py +++ b/openstack/cloud/_clustering.py @@ -23,10 +23,12 @@ class ClusteringCloudMixin: def _clustering_client(self): if 'clustering' not in self._raw_clients: clustering_client = self._get_versioned_client( - 'clustering', min_version=1, max_version='1.latest') + 'clustering', min_version=1, max_version='1.latest' + ) self._raw_clients['clustering'] = clustering_client return self._raw_clients['clustering'] + # NOTE(gtema): work on getting rid of direct API calls showed that this # implementation never worked properly and tests in reality verifying wrong # things. Unless someone is really interested in this piece of code this will diff --git a/openstack/cloud/_coe.py b/openstack/cloud/_coe.py index 6533f3f36..cbbe63187 100644 --- a/openstack/cloud/_coe.py +++ b/openstack/cloud/_coe.py @@ -19,7 +19,6 @@ class CoeCloudMixin: - @_utils.cache_on_arguments() def list_coe_clusters(self): """List COE (Container Orchestration Engine) cluster. @@ -72,7 +71,10 @@ def get_coe_cluster(self, name_or_id, filters=None): return _utils._get_entity(self, 'coe_cluster', name_or_id, filters) def create_coe_cluster( - self, name, cluster_template_id, **kwargs, + self, + name, + cluster_template_id, + **kwargs, ): """Create a COE cluster based on given cluster template. @@ -133,11 +135,11 @@ def update_coe_cluster(self, name_or_id, **kwargs): cluster = self.get_coe_cluster(name_or_id) if not cluster: raise exc.OpenStackCloudException( - "COE cluster %s not found." % name_or_id) + "COE cluster %s not found." % name_or_id + ) cluster = self.container_infrastructure_management.update_cluster( - cluster, - **kwargs + cluster, **kwargs ) return cluster @@ -149,8 +151,11 @@ def get_coe_cluster_certificate(self, cluster_id): :returns: Details about the CA certificate for the given cluster. """ - return self.container_infrastructure_management\ - .get_cluster_certificate(cluster_id) + return ( + self.container_infrastructure_management.get_cluster_certificate( + cluster_id + ) + ) def sign_coe_cluster_certificate(self, cluster_id, csr): """Sign client key and generate the CA certificate for a cluster @@ -164,10 +169,9 @@ def sign_coe_cluster_certificate(self, cluster_id, csr): :raises: OpenStackCloudException on operation error. """ - return self.container_infrastructure_management\ - .create_cluster_certificate( - cluster_uuid=cluster_id, - csr=csr) + return self.container_infrastructure_management.create_cluster_certificate( # noqa: E501 + cluster_uuid=cluster_id, csr=csr + ) @_utils.cache_on_arguments() def list_cluster_templates(self, detail=False): @@ -182,10 +186,12 @@ def list_cluster_templates(self, detail=False): the OpenStack API call. """ return list( - self.container_infrastructure_management.cluster_templates()) + self.container_infrastructure_management.cluster_templates() + ) def search_cluster_templates( - self, name_or_id=None, filters=None, detail=False): + self, name_or_id=None, filters=None, detail=False + ): """Search cluster templates. :param name_or_id: cluster template name or ID. @@ -199,8 +205,7 @@ def search_cluster_templates( the OpenStack API call. """ cluster_templates = self.list_cluster_templates(detail=detail) - return _utils._filter_list( - cluster_templates, name_or_id, filters) + return _utils._filter_list(cluster_templates, name_or_id, filters) def get_cluster_template(self, name_or_id, filters=None, detail=False): """Get a cluster template by name or ID. @@ -225,11 +230,16 @@ def get_cluster_template(self, name_or_id, filters=None, detail=False): cluster template is found. """ return _utils._get_entity( - self, 'cluster_template', name_or_id, - filters=filters, detail=detail) + self, + 'cluster_template', + name_or_id, + filters=filters, + detail=detail, + ) def create_cluster_template( - self, name, image_id=None, keypair_id=None, coe=None, **kwargs): + self, name, image_id=None, keypair_id=None, coe=None, **kwargs + ): """Create a cluster template. :param string name: Name of the cluster template. @@ -243,14 +253,15 @@ def create_cluster_template( :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call """ - cluster_template = self.container_infrastructure_management \ - .create_cluster_template( + cluster_template = ( + self.container_infrastructure_management.create_cluster_template( name=name, image_id=image_id, keypair_id=keypair_id, coe=coe, **kwargs, ) + ) return cluster_template @@ -270,11 +281,13 @@ def delete_cluster_template(self, name_or_id): self.log.debug( "Cluster template %(name_or_id)s does not exist", {'name_or_id': name_or_id}, - exc_info=True) + exc_info=True, + ) return False self.container_infrastructure_management.delete_cluster_template( - cluster_template) + cluster_template + ) return True def update_cluster_template(self, name_or_id, **kwargs): @@ -289,13 +302,14 @@ def update_cluster_template(self, name_or_id, **kwargs): cluster_template = self.get_cluster_template(name_or_id) if not cluster_template: raise exc.OpenStackCloudException( - "Cluster template %s not found." % name_or_id) + "Cluster template %s not found." % name_or_id + ) - cluster_template = self.container_infrastructure_management \ - .update_cluster_template( - cluster_template, - **kwargs + cluster_template = ( + self.container_infrastructure_management.update_cluster_template( + cluster_template, **kwargs ) + ) return cluster_template diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 89e329816..b2ab77850 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -114,12 +114,15 @@ def get_flavor_by_ram(self, ram, include=None, get_extra=True): """ flavors = self.list_flavors(get_extra=get_extra) for flavor in sorted(flavors, key=operator.itemgetter('ram')): - if (flavor['ram'] >= ram - and (not include or include in flavor['name'])): + if flavor['ram'] >= ram and ( + not include or include in flavor['name'] + ): return flavor raise exc.OpenStackCloudException( "Could not find a flavor with {ram} and '{include}'".format( - ram=ram, include=include)) + ram=ram, include=include + ) + ) @_utils.cache_on_arguments() def _nova_extensions(self): @@ -155,8 +158,12 @@ def search_flavors(self, name_or_id=None, filters=None, get_extra=True): return _utils._filter_list(flavors, name_or_id, filters) def search_servers( - self, name_or_id=None, filters=None, detailed=False, - all_projects=False, bare=False, + self, + name_or_id=None, + filters=None, + detailed=False, + all_projects=False, + bare=False, ): """Search servers. @@ -169,7 +176,8 @@ def search_servers( criteria. """ servers = self.list_servers( - detailed=detailed, all_projects=all_projects, bare=bare) + detailed=detailed, all_projects=all_projects, bare=bare + ) return _utils._filter_list(servers, name_or_id, filters) def search_server_groups(self, name_or_id=None, filters=None): @@ -213,8 +221,8 @@ def list_availability_zone_names(self, unavailable=False): return ret except exceptions.SDKException: self.log.debug( - "Availability zone list could not be fetched", - exc_info=True) + "Availability zone list could not be fetched", exc_info=True + ) return [] @_utils.cache_on_arguments() @@ -226,8 +234,9 @@ def list_flavors(self, get_extra=False): clouds.yaml by setting openstack.cloud.get_extra_specs to False. :returns: A list of compute ``Flavor`` objects. """ - return list(self.compute.flavors( - details=True, get_extra_specs=get_extra)) + return list( + self.compute.flavors(details=True, get_extra_specs=get_extra) + ) def list_server_security_groups(self, server): """List all security groups associated with the given server. @@ -268,8 +277,9 @@ def _get_server_security_groups(self, server, security_groups): sg = self.get_security_group(sg) if sg is None: - self.log.debug('Security group %s not found for adding', - sg) + self.log.debug( + 'Security group %s not found for adding', sg + ) return None, None @@ -288,7 +298,8 @@ def add_server_security_groups(self, server, security_groups): :raises: ``OpenStackCloudException``, on operation error. """ server, security_groups = self._get_server_security_groups( - server, security_groups) + server, security_groups + ) if not (server and security_groups): return False @@ -310,7 +321,8 @@ def remove_server_security_groups(self, server, security_groups): :raises: ``OpenStackCloudException``, on operation error. """ server, security_groups = self._get_server_security_groups( - server, security_groups) + server, security_groups + ) if not (server and security_groups): return False @@ -327,7 +339,10 @@ def remove_server_security_groups(self, server, security_groups): # error? Nova returns ok if you try to add a group twice. self.log.debug( "The security group %s was not present on server %s so " - "no action was performed", sg.name, server.name) + "no action was performed", + sg.name, + server.name, + ) ret = False return ret @@ -377,7 +392,8 @@ def list_servers( self._servers = self._list_servers( detailed=detailed, all_projects=all_projects, - bare=bare) + bare=bare, + ) self._servers_time = time.time() finally: self._servers_lock.release() @@ -386,14 +402,15 @@ def list_servers( # list from the cloud, we still return a filtered list. return _utils._filter_list(self._servers, None, filters) - def _list_servers(self, detailed=False, all_projects=False, bare=False, - filters=None): + def _list_servers( + self, detailed=False, all_projects=False, bare=False, filters=None + ): filters = filters or {} return [ self._expand_server(server, detailed, bare) for server in self.compute.servers( - all_projects=all_projects, - **filters) + all_projects=all_projects, **filters + ) ] def list_server_groups(self): @@ -472,12 +489,15 @@ def get_flavor(self, name_or_id, filters=None, get_extra=True): if not filters: filters = {} flavor = self.compute.find_flavor( - name_or_id, get_extra_specs=get_extra, - ignore_missing=True, **filters) + name_or_id, + get_extra_specs=get_extra, + ignore_missing=True, + **filters, + ) return flavor def get_flavor_by_id(self, id, get_extra=False): - """ Get a flavor by ID + """Get a flavor by ID :param id: ID of the flavor. :param get_extra: Whether or not the list_flavors call should get the @@ -505,7 +525,8 @@ def get_server_console(self, server, length=None): if not server: raise exc.OpenStackCloudException( - "Console log requested for invalid server") + "Console log requested for invalid server" + ) try: return self._get_server_console_output(server['id'], length) @@ -514,8 +535,7 @@ def get_server_console(self, server, length=None): def _get_server_console_output(self, server_id, length=None): output = self.compute.get_server_console_output( - server=server_id, - length=length + server=server_id, length=length ) if 'output' in output: return output['output'] @@ -555,9 +575,12 @@ def get_server( the current auth scoped project. :returns: A compute ``Server`` object if found, else None. """ - searchfunc = functools.partial(self.search_servers, - detailed=detailed, bare=True, - all_projects=all_projects) + searchfunc = functools.partial( + self.search_servers, + detailed=detailed, + bare=True, + all_projects=all_projects, + ) server = _utils._get_entity(self, searchfunc, name_or_id, filters) return self._expand_server(server, detailed, bare) @@ -600,8 +623,7 @@ def get_server_group(self, name_or_id=None, filters=None): :returns: A compute ``ServerGroup`` object if found, else None. """ - return _utils._get_entity(self, 'server_group', name_or_id, - filters) + return _utils._get_entity(self, 'server_group', name_or_id, filters) def create_keypair(self, name, public_key=None): """Create a new keypair. @@ -664,10 +686,12 @@ def create_image_snapshot( if not server_obj: raise exc.OpenStackCloudException( "Server {server} could not be found and therefore" - " could not be snapshotted.".format(server=server)) + " could not be snapshotted.".format(server=server) + ) server = server_obj image = self.compute.create_server_image( - server, name=name, metadata=metadata, wait=wait, timeout=timeout) + server, name=name, metadata=metadata, wait=wait, timeout=timeout + ) return image def get_server_id(self, name_or_id): @@ -709,12 +733,25 @@ def get_server_meta(self, server): return dict(server_vars=server_vars, groups=groups) @_utils.valid_kwargs( - 'meta', 'files', 'userdata', 'description', - 'reservation_id', 'return_raw', 'min_count', - 'max_count', 'security_groups', 'key_name', - 'availability_zone', 'block_device_mapping', - 'block_device_mapping_v2', 'nics', 'scheduler_hints', - 'config_drive', 'admin_pass', 'disk_config') + 'meta', + 'files', + 'userdata', + 'description', + 'reservation_id', + 'return_raw', + 'min_count', + 'max_count', + 'security_groups', + 'key_name', + 'availability_zone', + 'block_device_mapping', + 'block_device_mapping_v2', + 'nics', + 'scheduler_hints', + 'config_drive', + 'admin_pass', + 'disk_config', + ) def create_server( self, name, @@ -818,10 +855,12 @@ def create_server( # after image in the argument list. Doh. if not flavor: raise TypeError( - "create_server() missing 1 required argument: 'flavor'") + "create_server() missing 1 required argument: 'flavor'" + ) if not image and not boot_volume: raise TypeError( - "create_server() requires either 'image' or 'boot_volume'") + "create_server() requires either 'image' or 'boot_volume'" + ) # TODO(mordred) Add support for description starting in 2.19 security_groups = kwargs.get('security_groups', []) @@ -836,11 +875,12 @@ def create_server( if user_data: kwargs['user_data'] = self._encode_server_userdata(user_data) for (desired, given) in ( - ('OS-DCF:diskConfig', 'disk_config'), - ('config_drive', 'config_drive'), - ('key_name', 'key_name'), - ('metadata', 'meta'), - ('adminPass', 'admin_pass')): + ('OS-DCF:diskConfig', 'disk_config'), + ('config_drive', 'config_drive'), + ('key_name', 'key_name'), + ('metadata', 'meta'), + ('adminPass', 'admin_pass'), + ): value = kwargs.pop(given, None) if value: kwargs[desired] = value @@ -850,7 +890,8 @@ def create_server( if not group_obj: raise exc.OpenStackCloudException( "Server Group {group} was requested but was not found" - " on the cloud".format(group=group)) + " on the cloud".format(group=group) + ) if 'scheduler_hints' not in kwargs: kwargs['scheduler_hints'] = {} kwargs['scheduler_hints']['group'] = group_obj['id'] @@ -865,7 +906,8 @@ def create_server( else: raise exc.OpenStackCloudException( 'nics parameter to create_server takes a list of dicts.' - ' Got: {nics}'.format(nics=kwargs['nics'])) + ' Got: {nics}'.format(nics=kwargs['nics']) + ) if network and ('nics' not in kwargs or not kwargs['nics']): nics = [] @@ -881,7 +923,10 @@ def create_server( 'Network {network} is not a valid network in' ' {cloud}:{region}'.format( network=network, - cloud=self.name, region=self._compute_region)) + cloud=self.name, + region=self._compute_region, + ) + ) nics.append({'net-id': network_obj['id']}) kwargs['nics'] = nics @@ -904,14 +949,17 @@ def create_server( if not nic_net: raise exc.OpenStackCloudException( "Requested network {net} could not be found.".format( - net=net_name)) + net=net_name + ) + ) net['uuid'] = nic_net['id'] for ip_key in ('v4-fixed-ip', 'v6-fixed-ip', 'fixed_ip'): fixed_ip = nic.pop(ip_key, None) if fixed_ip and net.get('fixed_ip'): raise exc.OpenStackCloudException( "Only one of v4-fixed-ip, v6-fixed-ip or fixed_ip" - " may be given") + " may be given" + ) if fixed_ip: net['fixed_ip'] = fixed_ip for key in ('port', 'port-id'): @@ -920,13 +968,13 @@ def create_server( # A tag supported only in server microversion 2.32-2.36 or >= 2.42 # Bumping the version to 2.42 to support the 'tag' implementation if 'tag' in nic: - utils.require_microversion( - self.compute, '2.42') + utils.require_microversion(self.compute, '2.42') net['tag'] = nic.pop('tag') if nic: raise exc.OpenStackCloudException( "Additional unsupported keys given for server network" - " creation: {keys}".format(keys=nic.keys())) + " creation: {keys}".format(keys=nic.keys()) + ) networks.append(net) if networks: kwargs['networks'] = networks @@ -954,10 +1002,14 @@ def create_server( boot_volume = root_volume kwargs = self._get_boot_from_volume_kwargs( - image=image, boot_from_volume=boot_from_volume, - boot_volume=boot_volume, volume_size=str(volume_size), + image=image, + boot_from_volume=boot_from_volume, + boot_volume=boot_volume, + volume_size=str(volume_size), terminate_volume=terminate_volume, - volumes=volumes, kwargs=kwargs) + volumes=volumes, + kwargs=kwargs, + ) kwargs['name'] = name @@ -977,14 +1029,18 @@ def create_server( server = self.compute.get_server(server.id) if server.status == 'ERROR': raise exc.OpenStackCloudCreateException( - resource='server', resource_id=server.id) + resource='server', resource_id=server.id + ) server = meta.add_server_interfaces(self, server) else: server = self.wait_for_server( server, - auto_ip=auto_ip, ips=ips, ip_pool=ip_pool, - reuse=reuse_ips, timeout=timeout, + auto_ip=auto_ip, + ips=ips, + ip_pool=ip_pool, + reuse=reuse_ips, + timeout=timeout, nat_destination=nat_destination, ) @@ -992,8 +1048,15 @@ def create_server( return server def _get_boot_from_volume_kwargs( - self, image, boot_from_volume, boot_volume, volume_size, - terminate_volume, volumes, kwargs): + self, + image, + boot_from_volume, + boot_volume, + volume_size, + terminate_volume, + volumes, + kwargs, + ): """Return block device mappings :param image: Image dict, name or id to boot with. @@ -1015,7 +1078,10 @@ def _get_boot_from_volume_kwargs( 'Volume {boot_volume} is not a valid volume' ' in {cloud}:{region}'.format( boot_volume=boot_volume, - cloud=self.name, region=self._compute_region)) + cloud=self.name, + region=self._compute_region, + ) + ) block_mapping = { 'boot_index': '0', 'delete_on_termination': terminate_volume, @@ -1036,7 +1102,10 @@ def _get_boot_from_volume_kwargs( 'Image {image} is not a valid image in' ' {cloud}:{region}'.format( image=image, - cloud=self.name, region=self._compute_region)) + cloud=self.name, + region=self._compute_region, + ) + ) block_mapping = { 'boot_index': '0', @@ -1066,7 +1135,10 @@ def _get_boot_from_volume_kwargs( 'Volume {volume} is not a valid volume' ' in {cloud}:{region}'.format( volume=volume, - cloud=self.name, region=self._compute_region)) + cloud=self.name, + region=self._compute_region, + ) + ) block_mapping = { 'boot_index': '-1', 'delete_on_termination': False, @@ -1080,8 +1152,15 @@ def _get_boot_from_volume_kwargs( return kwargs def wait_for_server( - self, server, auto_ip=True, ips=None, ip_pool=None, - reuse=True, timeout=180, nat_destination=None): + self, + server, + auto_ip=True, + ips=None, + ip_pool=None, + reuse=True, + timeout=180, + nat_destination=None, + ): """ Wait for a server to reach ACTIVE status. """ @@ -1094,11 +1173,12 @@ def wait_for_server( # There is no point in iterating faster than the list_servers cache for count in utils.iterate_timeout( - timeout, - timeout_message, - # if _SERVER_AGE is 0 we still want to wait a bit - # to be friendly with the server. - wait=self._SERVER_AGE or 2): + timeout, + timeout_message, + # if _SERVER_AGE is 0 we still want to wait a bit + # to be friendly with the server. + wait=self._SERVER_AGE or 2, + ): try: # Use the get_server call so that the list_servers # cache can be leveraged @@ -1116,10 +1196,15 @@ def wait_for_server( raise exc.OpenStackCloudTimeout(timeout_message) server = self.get_active_server( - server=server, reuse=reuse, - auto_ip=auto_ip, ips=ips, ip_pool=ip_pool, - wait=True, timeout=remaining_timeout, - nat_destination=nat_destination) + server=server, + reuse=reuse, + auto_ip=auto_ip, + ips=ips, + ip_pool=ip_pool, + wait=True, + timeout=remaining_timeout, + nat_destination=nat_destination, + ) if server is not None and server['status'] == 'ACTIVE': return server @@ -1136,43 +1221,58 @@ def get_active_server( nat_destination=None, ): if server['status'] == 'ERROR': - if ('fault' in server and server['fault'] is not None - and 'message' in server['fault']): + if ( + 'fault' in server + and server['fault'] is not None + and 'message' in server['fault'] + ): raise exc.OpenStackCloudException( "Error in creating the server." " Compute service reports fault: {reason}".format( - reason=server['fault']['message']), - extra_data=dict(server=server)) + reason=server['fault']['message'] + ), + extra_data=dict(server=server), + ) raise exc.OpenStackCloudException( "Error in creating the server" " (no further information available)", - extra_data=dict(server=server)) + extra_data=dict(server=server), + ) if server['status'] == 'ACTIVE': if 'addresses' in server and server['addresses']: return self.add_ips_to_server( - server, auto_ip, ips, ip_pool, reuse=reuse, + server, + auto_ip, + ips, + ip_pool, + reuse=reuse, nat_destination=nat_destination, - wait=wait, timeout=timeout) + wait=wait, + timeout=timeout, + ) self.log.debug( 'Server %(server)s reached ACTIVE state without' ' being allocated an IP address.' - ' Deleting server.', {'server': server['id']}) + ' Deleting server.', + {'server': server['id']}, + ) try: - self._delete_server( - server=server, wait=wait, timeout=timeout) + self._delete_server(server=server, wait=wait, timeout=timeout) except Exception as e: raise exc.OpenStackCloudException( 'Server reached ACTIVE state without being' ' allocated an IP address AND then could not' ' be deleted: {0}'.format(e), - extra_data=dict(server=server)) + extra_data=dict(server=server), + ) raise exc.OpenStackCloudException( 'Server reached ACTIVE state without being' ' allocated an IP address.', - extra_data=dict(server=server)) + extra_data=dict(server=server), + ) return None def rebuild_server( @@ -1202,17 +1302,12 @@ def rebuild_server( if admin_pass: kwargs['admin_password'] = admin_pass - server = self.compute.rebuild_server( - server_id, - **kwargs - ) + server = self.compute.rebuild_server(server_id, **kwargs) if not wait: - return self._expand_server( - server, bare=bare, detailed=detailed) + return self._expand_server(server, bare=bare, detailed=detailed) admin_pass = server.get('adminPass') or admin_pass - server = self.compute.wait_for_server( - server, wait=timeout) + server = self.compute.wait_for_server(server, wait=timeout) if server['status'] == 'ACTIVE': server.adminPass = admin_pass @@ -1231,7 +1326,8 @@ def set_server_metadata(self, name_or_id, metadata): server = self.get_server(name_or_id, bare=True) if not server: raise exc.OpenStackCloudException( - 'Invalid Server {server}'.format(server=name_or_id)) + 'Invalid Server {server}'.format(server=name_or_id) + ) self.compute.set_server_metadata(server=server.id, **metadata) @@ -1248,10 +1344,12 @@ def delete_server_metadata(self, name_or_id, metadata_keys): server = self.get_server(name_or_id, bare=True) if not server: raise exc.OpenStackCloudException( - 'Invalid Server {server}'.format(server=name_or_id)) + 'Invalid Server {server}'.format(server=name_or_id) + ) - self.compute.delete_server_metadata(server=server.id, - keys=metadata_keys) + self.compute.delete_server_metadata( + server=server.id, keys=metadata_keys + ) def delete_server( self, @@ -1275,8 +1373,7 @@ def delete_server( :raises: OpenStackCloudException on operation error. """ # If delete_ips is True, we need the server to not be bare. - server = self.compute.find_server( - name_or_id, ignore_missing=True) + server = self.compute.find_server(name_or_id, ignore_missing=True) if not server: return False @@ -1284,18 +1381,24 @@ def delete_server( # private method in order to avoid an unnecessary API call to get # a server we already have. return self._delete_server( - server, wait=wait, timeout=timeout, delete_ips=delete_ips, - delete_ip_retry=delete_ip_retry) + server, + wait=wait, + timeout=timeout, + delete_ips=delete_ips, + delete_ip_retry=delete_ip_retry, + ) def _delete_server_floating_ips(self, server, delete_ip_retry): # Does the server have floating ips in its # addresses dict? If not, skip this. server_floats = meta.find_nova_interfaces( - server['addresses'], ext_tag='floating') + server['addresses'], ext_tag='floating' + ) for fip in server_floats: try: - ip = self.get_floating_ip(id=None, filters={ - 'floating_ip_address': fip['addr']}) + ip = self.get_floating_ip( + id=None, filters={'floating_ip_address': fip['addr']} + ) except exc.OpenStackCloudURINotFound: # We're deleting. If it doesn't exist - awesome # NOTE(mordred) If the cloud is a nova FIP cloud but @@ -1304,19 +1407,24 @@ def _delete_server_floating_ips(self, server, delete_ip_retry): continue if not ip: continue - deleted = self.delete_floating_ip( - ip['id'], retry=delete_ip_retry) + deleted = self.delete_floating_ip(ip['id'], retry=delete_ip_retry) if not deleted: raise exc.OpenStackCloudException( "Tried to delete floating ip {floating_ip}" " associated with server {id} but there was" " an error deleting it. Not deleting server.".format( - floating_ip=ip['floating_ip_address'], - id=server['id'])) + floating_ip=ip['floating_ip_address'], id=server['id'] + ) + ) def _delete_server( - self, server, wait=False, timeout=180, delete_ips=False, - delete_ip_retry=1): + self, + server, + wait=False, + timeout=180, + delete_ips=False, + delete_ip_retry=1, + ): if not server: return False @@ -1324,8 +1432,7 @@ def _delete_server( self._delete_server_floating_ips(server, delete_ip_retry) try: - self.compute.delete_server( - server) + self.compute.delete_server(server) except exceptions.ResourceNotFound: return False except Exception: @@ -1339,9 +1446,11 @@ def _delete_server( # need to invalidate the cache. Avoid the extra API call if # caching is not enabled. reset_volume_cache = False - if (self.cache_enabled - and self.has_service('volume') - and self.get_volumes(server)): + if ( + self.cache_enabled + and self.has_service('volume') + and self.get_volumes(server) + ): reset_volume_cache = True if not isinstance(server, _server.Server): @@ -1349,8 +1458,7 @@ def _delete_server( # If this is the case - convert it into real server to be able to # use wait_for_delete server = _server.Server(id=server['id']) - self.compute.wait_for_delete( - server, wait=timeout) + self.compute.wait_for_delete(server, wait=timeout) if reset_volume_cache: self.list_volumes.invalidate(self) @@ -1360,8 +1468,7 @@ def _delete_server( self._servers_time = self._servers_time - self._SERVER_AGE return True - @_utils.valid_kwargs( - 'name', 'description') + @_utils.valid_kwargs('name', 'description') def update_server(self, name_or_id, detailed=False, bare=False, **kwargs): """Update a server. @@ -1377,13 +1484,9 @@ def update_server(self, name_or_id, detailed=False, bare=False, **kwargs): :returns: The updated compute ``Server`` object. :raises: OpenStackCloudException on operation error. """ - server = self.compute.find_server( - name_or_id, - ignore_missing=False - ) + server = self.compute.find_server(name_or_id, ignore_missing=False) - server = self.compute.update_server( - server, **kwargs) + server = self.compute.update_server(server, **kwargs) return self._expand_server(server, bare=bare, detailed=detailed) @@ -1395,16 +1498,12 @@ def create_server_group(self, name, policies=None, policy=None): :returns: The created compute ``ServerGroup`` object. :raises: OpenStackCloudException on operation error. """ - sg_attrs = { - 'name': name - } + sg_attrs = {'name': name} if policies: sg_attrs['policies'] = policies if policy: sg_attrs['policy'] = policy - return self.compute.create_server_group( - **sg_attrs - ) + return self.compute.create_server_group(**sg_attrs) def delete_server_group(self, name_or_id): """Delete a server group. @@ -1415,8 +1514,9 @@ def delete_server_group(self, name_or_id): """ server_group = self.get_server_group(name_or_id) if not server_group: - self.log.debug("Server group %s not found for deleting", - name_or_id) + self.log.debug( + "Server group %s not found for deleting", name_or_id + ) return False self.compute.delete_server_group(server_group, ignore_missing=False) @@ -1477,14 +1577,14 @@ def delete_flavor(self, name_or_id): try: flavor = self.compute.find_flavor(name_or_id) if not flavor: - self.log.debug( - "Flavor %s not found for deleting", name_or_id) + self.log.debug("Flavor %s not found for deleting", name_or_id) return False self.compute.delete_flavor(flavor) return True except exceptions.SDKException: raise exceptions.OpenStackCloudException( - "Unable to delete flavor {name}".format(name=name_or_id)) + "Unable to delete flavor {name}".format(name=name_or_id) + ) def set_flavor_specs(self, flavor_id, extra_specs): """Add extra specs to a flavor @@ -1545,9 +1645,7 @@ def list_hypervisors(self, filters={}): :returns: A list of compute ``Hypervisor`` objects. """ - return list(self.compute.hypervisors( - details=True, - **filters)) + return list(self.compute.hypervisors(details=True, **filters)) def search_aggregates(self, name_or_id=None, filters=None): """Seach host aggregates. @@ -1587,8 +1685,7 @@ def get_aggregate(self, name_or_id, filters=None): :returns: An aggregate dict or None if no matching aggregate is found. """ - return self.compute.find_aggregate( - name_or_id, ignore_missing=True) + return self.compute.find_aggregate(name_or_id, ignore_missing=True) def create_aggregate(self, name, availability_zone=None): """Create a new host aggregate. @@ -1599,8 +1696,7 @@ def create_aggregate(self, name, availability_zone=None): :raises: OpenStackCloudException on operation error. """ return self.compute.create_aggregate( - name=name, - availability_zone=availability_zone + name=name, availability_zone=availability_zone ) @_utils.valid_kwargs('name', 'availability_zone') @@ -1623,14 +1719,12 @@ def delete_aggregate(self, name_or_id): :returns: True if delete succeeded, False otherwise. :raises: OpenStackCloudException on operation error. """ - if ( - isinstance(name_or_id, (str, bytes)) - and not name_or_id.isdigit() - ): + if isinstance(name_or_id, (str, bytes)) and not name_or_id.isdigit(): aggregate = self.get_aggregate(name_or_id) if not aggregate: self.log.debug( - "Aggregate %s not found for deleting", name_or_id) + "Aggregate %s not found for deleting", name_or_id + ) return False name_or_id = aggregate.id try: @@ -1654,7 +1748,8 @@ def set_aggregate_metadata(self, name_or_id, metadata): aggregate = self.get_aggregate(name_or_id) if not aggregate: raise exc.OpenStackCloudException( - "Host aggregate %s not found." % name_or_id) + "Host aggregate %s not found." % name_or_id + ) return self.compute.set_aggregate_metadata(aggregate, metadata) @@ -1669,7 +1764,8 @@ def add_host_to_aggregate(self, name_or_id, host_name): aggregate = self.get_aggregate(name_or_id) if not aggregate: raise exc.OpenStackCloudException( - "Host aggregate %s not found." % name_or_id) + "Host aggregate %s not found." % name_or_id + ) return self.compute.add_host_to_aggregate(aggregate, host_name) @@ -1684,12 +1780,13 @@ def remove_host_from_aggregate(self, name_or_id, host_name): aggregate = self.get_aggregate(name_or_id) if not aggregate: raise exc.OpenStackCloudException( - "Host aggregate %s not found." % name_or_id) + "Host aggregate %s not found." % name_or_id + ) return self.compute.remove_host_from_aggregate(aggregate, host_name) def set_compute_quotas(self, name_or_id, **kwargs): - """ Set a quota in a project + """Set a quota in a project :param name_or_id: project name or id :param kwargs: key/value pairs of quota name and quota value @@ -1697,39 +1794,35 @@ def set_compute_quotas(self, name_or_id, **kwargs): :raises: OpenStackCloudException if the resource to set the quota does not exist. """ - proj = self.identity.find_project( - name_or_id, ignore_missing=False) + proj = self.identity.find_project(name_or_id, ignore_missing=False) kwargs['force'] = True self.compute.update_quota_set( - _qs.QuotaSet(project_id=proj.id), - **kwargs + _qs.QuotaSet(project_id=proj.id), **kwargs ) def get_compute_quotas(self, name_or_id): - """ Get quota for a project + """Get quota for a project :param name_or_id: project name or id :returns: A compute ``QuotaSet`` object if found, else None. :raises: OpenStackCloudException if it's not a valid project """ - proj = self.identity.find_project( - name_or_id, ignore_missing=False) + proj = self.identity.find_project(name_or_id, ignore_missing=False) return self.compute.get_quota_set(proj) def delete_compute_quotas(self, name_or_id): - """ Delete quota for a project + """Delete quota for a project :param name_or_id: project name or id :raises: OpenStackCloudException if it's not a valid project or the nova client call failed :returns: None """ - proj = self.identity.find_project( - name_or_id, ignore_missing=False) + proj = self.identity.find_project(name_or_id, ignore_missing=False) self.compute.revert_quota_set(proj) def get_compute_usage(self, name_or_id, start=None, end=None): - """ Get usage for a specific project + """Get usage for a specific project :param name_or_id: project name or id :param start: :class:`datetime.datetime` or string. Start date in UTC @@ -1741,6 +1834,7 @@ def get_compute_usage(self, name_or_id, start=None, end=None): :returns: A :class:`~openstack.compute.v2.usage.Usage` object """ + def parse_date(date): try: return iso8601.parse_date(date) @@ -1751,8 +1845,8 @@ def parse_date(date): raise exc.OpenStackCloudException( "Date given, {date}, is invalid. Please pass in a date" " string in ISO 8601 format -" - " YYYY-MM-DDTHH:MM:SS".format( - date=date)) + " YYYY-MM-DDTHH:MM:SS".format(date=date) + ) if isinstance(start, str): start = parse_date(start) @@ -1762,7 +1856,8 @@ def parse_date(date): proj = self.get_project(name_or_id) if not proj: raise exc.OpenStackCloudException( - "project does not exist: {name}".format(name=proj.id)) + "project does not exist: {name}".format(name=proj.id) + ) return self.compute.get_usage(proj, start, end) @@ -1830,22 +1925,28 @@ def _normalize_server(self, server): project_id = server.pop('project_id', project_id) az = _pop_or_get( - server, 'OS-EXT-AZ:availability_zone', None, self.strict_mode) + server, 'OS-EXT-AZ:availability_zone', None, self.strict_mode + ) # the server resource has this already, but it's missing az info # from the resource. # TODO(mordred) create_server is still normalizing servers that aren't # from the resource layer. ret['location'] = server.pop( - 'location', self._get_current_location( - project_id=project_id, zone=az)) + 'location', + self._get_current_location(project_id=project_id, zone=az), + ) # Ensure volumes is always in the server dict, even if empty ret['volumes'] = _pop_or_get( - server, 'os-extended-volumes:volumes_attached', - [], self.strict_mode) + server, + 'os-extended-volumes:volumes_attached', + [], + self.strict_mode, + ) config_drive = server.pop( - 'has_config_drive', server.pop('config_drive', False)) + 'has_config_drive', server.pop('config_drive', False) + ) ret['has_config_drive'] = _to_bool(config_drive) host_id = server.pop('hostId', server.pop('host_id', None)) @@ -1855,24 +1956,25 @@ def _normalize_server(self, server): # Leave these in so that the general properties handling works ret['disk_config'] = _pop_or_get( - server, 'OS-DCF:diskConfig', None, self.strict_mode) + server, 'OS-DCF:diskConfig', None, self.strict_mode + ) for key in ( - 'OS-EXT-STS:power_state', - 'OS-EXT-STS:task_state', - 'OS-EXT-STS:vm_state', - 'OS-SRV-USG:launched_at', - 'OS-SRV-USG:terminated_at', - 'OS-EXT-SRV-ATTR:hypervisor_hostname', - 'OS-EXT-SRV-ATTR:instance_name', - 'OS-EXT-SRV-ATTR:user_data', - 'OS-EXT-SRV-ATTR:host', - 'OS-EXT-SRV-ATTR:hostname', - 'OS-EXT-SRV-ATTR:kernel_id', - 'OS-EXT-SRV-ATTR:launch_index', - 'OS-EXT-SRV-ATTR:ramdisk_id', - 'OS-EXT-SRV-ATTR:reservation_id', - 'OS-EXT-SRV-ATTR:root_device_name', - 'OS-SCH-HNT:scheduler_hints', + 'OS-EXT-STS:power_state', + 'OS-EXT-STS:task_state', + 'OS-EXT-STS:vm_state', + 'OS-SRV-USG:launched_at', + 'OS-SRV-USG:terminated_at', + 'OS-EXT-SRV-ATTR:hypervisor_hostname', + 'OS-EXT-SRV-ATTR:instance_name', + 'OS-EXT-SRV-ATTR:user_data', + 'OS-EXT-SRV-ATTR:host', + 'OS-EXT-SRV-ATTR:hostname', + 'OS-EXT-SRV-ATTR:kernel_id', + 'OS-EXT-SRV-ATTR:launch_index', + 'OS-EXT-SRV-ATTR:ramdisk_id', + 'OS-EXT-SRV-ATTR:reservation_id', + 'OS-EXT-SRV-ATTR:root_device_name', + 'OS-SCH-HNT:scheduler_hints', ): short_key = key.split(':')[1] ret[short_key] = _pop_or_get(server, key, None, self.strict_mode) diff --git a/openstack/cloud/_dns.py b/openstack/cloud/_dns.py index 8efd07d6c..4001aa6bd 100644 --- a/openstack/cloud/_dns.py +++ b/openstack/cloud/_dns.py @@ -33,8 +33,7 @@ def list_zones(self, filters=None): """ if not filters: filters = {} - return list(self.dns.zones(allow_unknown_params=True, - **filters)) + return list(self.dns.zones(allow_unknown_params=True, **filters)) def get_zone(self, name_or_id, filters=None): """Get a zone by name or ID. @@ -49,7 +48,8 @@ def get_zone(self, name_or_id, filters=None): if not filters: filters = {} zone = self.dns.find_zone( - name_or_id=name_or_id, ignore_missing=True, **filters) + name_or_id=name_or_id, ignore_missing=True, **filters + ) if not zone: return None return zone @@ -58,8 +58,15 @@ def search_zones(self, name_or_id=None, filters=None): zones = self.list_zones(filters) return _utils._filter_list(zones, name_or_id, filters) - def create_zone(self, name, zone_type=None, email=None, description=None, - ttl=None, masters=None): + def create_zone( + self, + name, + zone_type=None, + email=None, + description=None, + ttl=None, + masters=None, + ): """Create a new zone. :param name: Name of the zone being created. @@ -82,8 +89,9 @@ def create_zone(self, name, zone_type=None, email=None, description=None, zone_type = zone_type.upper() if zone_type not in ('PRIMARY', 'SECONDARY'): raise exc.OpenStackCloudException( - "Invalid type %s, valid choices are PRIMARY or SECONDARY" % - zone_type) + "Invalid type %s, valid choices are PRIMARY or SECONDARY" + % zone_type + ) zone = { "name": name, @@ -125,7 +133,8 @@ def update_zone(self, name_or_id, **kwargs): zone = self.get_zone(name_or_id) if not zone: raise exc.OpenStackCloudException( - "Zone %s not found." % name_or_id) + "Zone %s not found." % name_or_id + ) return self.dns.update_zone(zone['id'], **kwargs) @@ -162,8 +171,7 @@ def list_recordsets(self, zone): else: zone_obj = self.get_zone(zone) if zone_obj is None: - raise exc.OpenStackCloudException( - "Zone %s not found." % zone) + raise exc.OpenStackCloudException("Zone %s not found." % zone) return list(self.dns.recordsets(zone_obj)) def get_recordset(self, zone, name_or_id): @@ -182,11 +190,11 @@ def get_recordset(self, zone, name_or_id): else: zone_obj = self.get_zone(zone) if not zone_obj: - raise exc.OpenStackCloudException( - "Zone %s not found." % zone) + raise exc.OpenStackCloudException("Zone %s not found." % zone) try: return self.dns.find_recordset( - zone=zone_obj, name_or_id=name_or_id, ignore_missing=False) + zone=zone_obj, name_or_id=name_or_id, ignore_missing=False + ) except Exception: return None @@ -194,8 +202,9 @@ def search_recordsets(self, zone, name_or_id=None, filters=None): recordsets = self.list_recordsets(zone=zone) return _utils._filter_list(recordsets, name_or_id, filters) - def create_recordset(self, zone, name, recordset_type, records, - description=None, ttl=None): + def create_recordset( + self, zone, name, recordset_type, records, description=None, ttl=None + ): """Create a recordset. :param zone: Name, ID or :class:`openstack.dns.v2.zone.Zone` instance @@ -216,17 +225,12 @@ def create_recordset(self, zone, name, recordset_type, records, else: zone_obj = self.get_zone(zone) if not zone_obj: - raise exc.OpenStackCloudException( - "Zone %s not found." % zone) + raise exc.OpenStackCloudException("Zone %s not found." % zone) # We capitalize the type in case the user sends in lowercase recordset_type = recordset_type.upper() - body = { - 'name': name, - 'type': recordset_type, - 'records': records - } + body = {'name': name, 'type': recordset_type, 'records': records} if description: body['description'] = description @@ -255,7 +259,8 @@ def update_recordset(self, zone, name_or_id, **kwargs): rs = self.get_recordset(zone, name_or_id) if not rs: raise exc.OpenStackCloudException( - "Recordset %s not found." % name_or_id) + "Recordset %s not found." % name_or_id + ) rs = self.dns.update_recordset(recordset=rs, **kwargs) diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index ee6ac0c3d..951173140 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -14,7 +14,6 @@ # We can't just use list, because sphinx gets confused by # openstack.resource.Resource.list and openstack.resource2.Resource.list import ipaddress -# import jsonpatch import threading import time import types # noqa @@ -30,7 +29,8 @@ _CONFIG_DOC_URL = ( "https://docs.openstack.org/openstacksdk/latest/" - "user/config/configuration.html") + "user/config/configuration.html" +) class FloatingIPCloudMixin: @@ -39,8 +39,7 @@ class FloatingIPCloudMixin: def __init__(self): self.private = self.config.config.get('private', False) - self._floating_ip_source = self.config.config.get( - 'floating_ip_source') + self._floating_ip_source = self.config.config.get('floating_ip_source') if self._floating_ip_source: if self._floating_ip_source.lower() == 'none': self._floating_ip_source = None @@ -68,7 +67,8 @@ def search_floating_ips(self, id=None, filters=None): # understand, obviously. warnings.warn( "search_floating_ips is deprecated. " - "Use search_resource instead.") + "Use search_resource instead." + ) if self._use_neutron_floating() and isinstance(filters, dict): return list(self.network.ips(**filters)) else: @@ -83,8 +83,7 @@ def _neutron_list_floating_ips(self, filters=None): def _nova_list_floating_ips(self): try: - data = proxy._json_response( - self.compute.get('/os-floating-ips')) + data = proxy._json_response(self.compute.get('/os-floating-ips')) except exc.OpenStackCloudURINotFound: return [] return self._get_and_munchify('floating_ips', data) @@ -137,10 +136,11 @@ def _list_floating_ips(self, filters=None): " using clouds.yaml to configure settings for your" " cloud(s), and you want to configure this setting," " you will need a clouds.yaml file. For more" - " information, please see %(doc_url)s", { + " information, please see %(doc_url)s", + { 'cloud': self.name, 'doc_url': _CONFIG_DOC_URL, - } + }, ) # We can't fallback to nova because we push-down filters. # We got a 404 which means neutron doesn't exist. If the @@ -148,7 +148,9 @@ def _list_floating_ips(self, filters=None): return [] self.log.debug( "Something went wrong talking to neutron API: " - "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) + "'%(msg)s'. Trying with Nova.", + {'msg': str(e)}, + ) # Fall-through, trying with Nova else: if filters: @@ -174,11 +176,13 @@ def list_floating_ip_pools(self): """ if not self._has_nova_extension('os-floating-ip-pools'): raise exc.OpenStackCloudUnavailableExtension( - 'Floating IP pools extension is not available on target cloud') + 'Floating IP pools extension is not available on target cloud' + ) data = proxy._json_response( self.compute.get('os-floating-ip-pools'), - error_message="Error fetching floating IP pool list") + error_message="Error fetching floating IP pool list", + ) pools = self._get_and_munchify('floating_ip_pools', data) return [{'name': p['name']} for p in pools] @@ -217,7 +221,7 @@ def list_floating_ips(self, filters=None): return _utils._filter_list(self._floating_ips, None, filters) def get_floating_ip_by_id(self, id): - """ Get a floating ip by ID + """Get a floating ip by ID :param id: ID of the floating ip. :returns: A floating ip @@ -231,12 +235,15 @@ def get_floating_ip_by_id(self, id): else: data = proxy._json_response( self.compute.get('/os-floating-ips/{id}'.format(id=id)), - error_message=error_message) + error_message=error_message, + ) return self._normalize_floating_ip( - self._get_and_munchify('floating_ip', data)) + self._get_and_munchify('floating_ip', data) + ) def _neutron_available_floating_ips( - self, network=None, project_id=None, server=None): + self, network=None, project_id=None, server=None + ): """Get a floating IP from a network. Return a list of available floating IPs or allocate a new one and @@ -271,8 +278,7 @@ def _neutron_available_floating_ips( if floating_network_id is None: raise exc.OpenStackCloudResourceNotFound( - "unable to find external network {net}".format( - net=network) + "unable to find external network {net}".format(net=network) ) else: floating_network_id = self._get_floating_network_id() @@ -285,14 +291,16 @@ def _neutron_available_floating_ips( floating_ips = self._list_floating_ips() available_ips = _utils._filter_list( - floating_ips, name_or_id=None, filters=filters) + floating_ips, name_or_id=None, filters=filters + ) if available_ips: return available_ips # No available IP found or we didn't try # allocate a new Floating IP f_ip = self._neutron_create_floating_ip( - network_id=floating_network_id, server=server) + network_id=floating_network_id, server=server + ) return [f_ip] @@ -311,23 +319,22 @@ def _nova_available_floating_ips(self, pool=None): """ with _utils.shade_exceptions( - "Unable to create floating IP in pool {pool}".format( - pool=pool)): + "Unable to create floating IP in pool {pool}".format(pool=pool) + ): if pool is None: pools = self.list_floating_ip_pools() if not pools: raise exc.OpenStackCloudResourceNotFound( - "unable to find a floating ip pool") + "unable to find a floating ip pool" + ) pool = pools[0]['name'] - filters = { - 'instance_id': None, - 'pool': pool - } + filters = {'instance_id': None, 'pool': pool} floating_ips = self._nova_list_floating_ips() available_ips = _utils._filter_list( - floating_ips, name_or_id=None, filters=filters) + floating_ips, name_or_id=None, filters=filters + ) if available_ips: return available_ips @@ -341,7 +348,8 @@ def _find_floating_network_by_router(self): """Find the network providing floating ips by looking at routers.""" if self._floating_network_by_router_lock.acquire( - not self._floating_network_by_router_run): + not self._floating_network_by_router_run + ): if self._floating_network_by_router_run: self._floating_network_by_router_lock.release() return self._floating_network_by_router @@ -349,7 +357,8 @@ def _find_floating_network_by_router(self): for router in self.list_routers(): if router['admin_state_up']: network_id = router.get( - 'external_gateway_info', {}).get('network_id') + 'external_gateway_info', {} + ).get('network_id') if network_id: self._floating_network_by_router = network_id finally: @@ -371,12 +380,15 @@ def available_floating_ip(self, network=None, server=None): if self._use_neutron_floating(): try: f_ips = self._neutron_available_floating_ips( - network=network, server=server) + network=network, server=server + ) return f_ips[0] except exc.OpenStackCloudURINotFound as e: self.log.debug( "Something went wrong talking to neutron API: " - "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) + "'%(msg)s'. Trying with Nova.", + {'msg': str(e)}, + ) # Fall-through, trying with Nova f_ips = self._normalize_floating_ips( @@ -395,12 +407,20 @@ def _get_floating_network_id(self): floating_network_id = floating_network else: raise exc.OpenStackCloudResourceNotFound( - "unable to find an external network") + "unable to find an external network" + ) return floating_network_id - def create_floating_ip(self, network=None, server=None, - fixed_address=None, nat_destination=None, - port=None, wait=False, timeout=60): + def create_floating_ip( + self, + network=None, + server=None, + fixed_address=None, + nat_destination=None, + port=None, + wait=False, + timeout=60, + ): """Allocate a new floating IP from a network or a pool. :param network: Name or ID of the network @@ -430,15 +450,20 @@ def create_floating_ip(self, network=None, server=None, if self._use_neutron_floating(): try: return self._neutron_create_floating_ip( - network_name_or_id=network, server=server, + network_name_or_id=network, + server=server, fixed_address=fixed_address, nat_destination=nat_destination, port=port, - wait=wait, timeout=timeout) + wait=wait, + timeout=timeout, + ) except exc.OpenStackCloudURINotFound as e: self.log.debug( "Something went wrong talking to neutron API: " - "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) + "'%(msg)s'. Trying with Nova.", + {'msg': str(e)}, + ) # Fall-through, trying with Nova if port: @@ -447,10 +472,12 @@ def create_floating_ip(self, network=None, server=None, " arbitrary floating-ip/port mappings. Please nudge" " your cloud provider to upgrade the networking stack" " to neutron, or alternately provide the server," - " fixed_address and nat_destination arguments as appropriate") + " fixed_address and nat_destination arguments as appropriate" + ) # Else, we are using Nova network f_ips = self._normalize_floating_ips( - [self._nova_create_floating_ip(pool=network)]) + [self._nova_create_floating_ip(pool=network)] + ) return f_ips[0] def _submit_create_fip(self, kwargs): @@ -458,10 +485,16 @@ def _submit_create_fip(self, kwargs): return self.network.create_ip(**kwargs) def _neutron_create_floating_ip( - self, network_name_or_id=None, server=None, - fixed_address=None, nat_destination=None, - port=None, - wait=False, timeout=60, network_id=None): + self, + network_name_or_id=None, + server=None, + fixed_address=None, + nat_destination=None, + port=None, + wait=False, + timeout=60, + network_id=None, + ): if not network_id: if network_name_or_id: @@ -470,7 +503,8 @@ def _neutron_create_floating_ip( except exceptions.ResourceNotFound: raise exc.OpenStackCloudResourceNotFound( "unable to find network for floating ips with ID " - "{0}".format(network_name_or_id)) + "{0}".format(network_name_or_id) + ) network_id = network['id'] else: network_id = self._get_floating_network_id() @@ -480,8 +514,10 @@ def _neutron_create_floating_ip( if not port: if server: (port_obj, fixed_ip_address) = self._nat_destination_port( - server, fixed_address=fixed_address, - nat_destination=nat_destination) + server, + fixed_address=fixed_address, + nat_destination=nat_destination, + ) if port_obj: port = port_obj['id'] if fixed_ip_address: @@ -499,57 +535,68 @@ def _neutron_create_floating_ip( if wait: try: for count in utils.iterate_timeout( - timeout, - "Timeout waiting for the floating IP" - " to be ACTIVE", - wait=self._FLOAT_AGE): + timeout, + "Timeout waiting for the floating IP" " to be ACTIVE", + wait=self._FLOAT_AGE, + ): fip = self.get_floating_ip(fip_id) if fip and fip['status'] == 'ACTIVE': break except exc.OpenStackCloudTimeout: self.log.error( "Timed out on floating ip %(fip)s becoming active." - " Deleting", {'fip': fip_id}) + " Deleting", + {'fip': fip_id}, + ) try: self.delete_floating_ip(fip_id) except Exception as e: self.log.error( "FIP LEAK: Attempted to delete floating ip " "%(fip)s but received %(exc)s exception: " - "%(err)s", {'fip': fip_id, 'exc': e.__class__, - 'err': str(e)}) + "%(err)s", + {'fip': fip_id, 'exc': e.__class__, 'err': str(e)}, + ) raise if fip['port_id'] != port: if server: raise exc.OpenStackCloudException( "Attempted to create FIP on port {port} for server" " {server} but FIP has port {port_id}".format( - port=port, port_id=fip['port_id'], - server=server['id'])) + port=port, + port_id=fip['port_id'], + server=server['id'], + ) + ) else: raise exc.OpenStackCloudException( "Attempted to create FIP on port {port}" - " but something went wrong".format(port=port)) + " but something went wrong".format(port=port) + ) return fip def _nova_create_floating_ip(self, pool=None): with _utils.shade_exceptions( - "Unable to create floating IP in pool {pool}".format( - pool=pool)): + "Unable to create floating IP in pool {pool}".format(pool=pool) + ): if pool is None: pools = self.list_floating_ip_pools() if not pools: raise exc.OpenStackCloudResourceNotFound( - "unable to find a floating ip pool") + "unable to find a floating ip pool" + ) pool = pools[0]['name'] - data = proxy._json_response(self.compute.post( - '/os-floating-ips', json=dict(pool=pool))) + data = proxy._json_response( + self.compute.post('/os-floating-ips', json=dict(pool=pool)) + ) pool_ip = self._get_and_munchify('floating_ip', data) # TODO(mordred) Remove this - it's just for compat data = proxy._json_response( - self.compute.get('/os-floating-ips/{id}'.format( - id=pool_ip['id']))) + self.compute.get( + '/os-floating-ips/{id}'.format(id=pool_ip['id']) + ) + ) return self._get_and_munchify('floating_ip', data) def delete_floating_ip(self, floating_ip_id, retry=1): @@ -589,8 +636,11 @@ def delete_floating_ip(self, floating_ip_id, retry=1): " {retry} times. Although the cloud did not indicate any errors" " the floating ip is still in existence. Aborting further" " operations.".format( - id=floating_ip_id, ip=f_ip['floating_ip_address'], - retry=retry + 1)) + id=floating_ip_id, + ip=f_ip['floating_ip_address'], + retry=retry + 1, + ) + ) def _delete_floating_ip(self, floating_ip_id): if self._use_neutron_floating(): @@ -599,14 +649,14 @@ def _delete_floating_ip(self, floating_ip_id): except exc.OpenStackCloudURINotFound as e: self.log.debug( "Something went wrong talking to neutron API: " - "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) + "'%(msg)s'. Trying with Nova.", + {'msg': str(e)}, + ) return self._nova_delete_floating_ip(floating_ip_id) def _neutron_delete_floating_ip(self, floating_ip_id): try: - self.network.delete_ip( - floating_ip_id, ignore_missing=False - ) + self.network.delete_ip(floating_ip_id, ignore_missing=False) except exceptions.ResourceNotFound: return False return True @@ -615,9 +665,12 @@ def _nova_delete_floating_ip(self, floating_ip_id): try: proxy._json_response( self.compute.delete( - '/os-floating-ips/{id}'.format(id=floating_ip_id)), + '/os-floating-ips/{id}'.format(id=floating_ip_id) + ), error_message='Unable to delete floating IP {fip_id}'.format( - fip_id=floating_ip_id)) + fip_id=floating_ip_id + ), + ) except exc.OpenStackCloudURINotFound: return False return True @@ -648,14 +701,23 @@ def delete_unattached_floating_ips(self, retry=1): if self._use_neutron_floating(): for ip in self.list_floating_ips(): if not bool(ip.port_id): - processed.append(self.delete_floating_ip( - floating_ip_id=ip['id'], retry=retry)) + processed.append( + self.delete_floating_ip( + floating_ip_id=ip['id'], retry=retry + ) + ) return len(processed) if all(processed) else False def _attach_ip_to_server( - self, server, floating_ip, - fixed_address=None, wait=False, - timeout=60, skip_attach=False, nat_destination=None): + self, + server, + floating_ip, + fixed_address=None, + wait=False, + timeout=60, + skip_attach=False, + nat_destination=None, + ): """Attach a floating IP to a server. :param server: Server dict @@ -685,8 +747,9 @@ def _attach_ip_to_server( # the server data and try again. There are some clouds, which # explicitely forbids FIP assign call if it is already assigned. server = self.get_server_by_id(server['id']) - ext_ip = meta.get_server_ip(server, ext_tag='floating', - public=True) + ext_ip = meta.get_server_ip( + server, ext_tag='floating', public=True + ) if ext_ip == floating_ip['floating_ip_address']: return server @@ -694,74 +757,84 @@ def _attach_ip_to_server( if not skip_attach: try: self._neutron_attach_ip_to_server( - server=server, floating_ip=floating_ip, + server=server, + floating_ip=floating_ip, fixed_address=fixed_address, - nat_destination=nat_destination) + nat_destination=nat_destination, + ) except exc.OpenStackCloudURINotFound as e: self.log.debug( "Something went wrong talking to neutron API: " - "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) + "'%(msg)s'. Trying with Nova.", + {'msg': str(e)}, + ) # Fall-through, trying with Nova else: # Nova network self._nova_attach_ip_to_server( - server_id=server['id'], floating_ip_id=floating_ip['id'], - fixed_address=fixed_address) + server_id=server['id'], + floating_ip_id=floating_ip['id'], + fixed_address=fixed_address, + ) if wait: # Wait for the address to be assigned to the server server_id = server['id'] for _ in utils.iterate_timeout( - timeout, - "Timeout waiting for the floating IP to be attached.", - wait=self._SERVER_AGE): + timeout, + "Timeout waiting for the floating IP to be attached.", + wait=self._SERVER_AGE, + ): server = self.get_server_by_id(server_id) ext_ip = meta.get_server_ip( - server, ext_tag='floating', public=True) + server, ext_tag='floating', public=True + ) if ext_ip == floating_ip['floating_ip_address']: return server return server def _neutron_attach_ip_to_server( - self, server, floating_ip, fixed_address=None, - nat_destination=None): + self, server, floating_ip, fixed_address=None, nat_destination=None + ): # Find an available port (port, fixed_address) = self._nat_destination_port( - server, fixed_address=fixed_address, - nat_destination=nat_destination) + server, + fixed_address=fixed_address, + nat_destination=nat_destination, + ) if not port: raise exc.OpenStackCloudException( - "unable to find a port for server {0}".format( - server['id'])) + "unable to find a port for server {0}".format(server['id']) + ) floating_ip_args = {'port_id': port['id']} if fixed_address is not None: floating_ip_args['fixed_ip_address'] = fixed_address - return self.network.update_ip( - floating_ip, - **floating_ip_args) + return self.network.update_ip(floating_ip, **floating_ip_args) - def _nova_attach_ip_to_server(self, server_id, floating_ip_id, - fixed_address=None): - f_ip = self.get_floating_ip( - id=floating_ip_id) + def _nova_attach_ip_to_server( + self, server_id, floating_ip_id, fixed_address=None + ): + f_ip = self.get_floating_ip(id=floating_ip_id) if f_ip is None: raise exc.OpenStackCloudException( - "unable to find floating IP {0}".format(floating_ip_id)) + "unable to find floating IP {0}".format(floating_ip_id) + ) error_message = "Error attaching IP {ip} to instance {id}".format( - ip=floating_ip_id, id=server_id) - body = { - 'address': f_ip['floating_ip_address'] - } + ip=floating_ip_id, id=server_id + ) + body = {'address': f_ip['floating_ip_address']} if fixed_address: body['fixed_address'] = fixed_address return proxy._json_response( self.compute.post( '/servers/{server_id}/action'.format(server_id=server_id), - json=dict(addFloatingIp=body)), - error_message=error_message) + json=dict(addFloatingIp=body), + ), + error_message=error_message, + ) def detach_ip_from_server(self, server_id, floating_ip_id): """Detach a floating IP from a server. @@ -777,31 +850,36 @@ def detach_ip_from_server(self, server_id, floating_ip_id): if self._use_neutron_floating(): try: return self._neutron_detach_ip_from_server( - server_id=server_id, floating_ip_id=floating_ip_id) + server_id=server_id, floating_ip_id=floating_ip_id + ) except exc.OpenStackCloudURINotFound as e: self.log.debug( "Something went wrong talking to neutron API: " - "'%(msg)s'. Trying with Nova.", {'msg': str(e)}) + "'%(msg)s'. Trying with Nova.", + {'msg': str(e)}, + ) # Fall-through, trying with Nova # Nova network self._nova_detach_ip_from_server( - server_id=server_id, floating_ip_id=floating_ip_id) + server_id=server_id, floating_ip_id=floating_ip_id + ) def _neutron_detach_ip_from_server(self, server_id, floating_ip_id): f_ip = self.get_floating_ip(id=floating_ip_id) if f_ip is None or not bool(f_ip.port_id): return False try: - self.network.update_ip( - floating_ip_id, - port_id=None - ) + self.network.update_ip(floating_ip_id, port_id=None) except exceptions.SDKException: raise exceptions.SDKException( - ("Error detaching IP {ip} from " - "server {server_id}".format( - ip=floating_ip_id, server_id=server_id))) + ( + "Error detaching IP {ip} from " + "server {server_id}".format( + ip=floating_ip_id, server_id=server_id + ) + ) + ) return True @@ -810,21 +888,33 @@ def _nova_detach_ip_from_server(self, server_id, floating_ip_id): f_ip = self.get_floating_ip(id=floating_ip_id) if f_ip is None: raise exc.OpenStackCloudException( - "unable to find floating IP {0}".format(floating_ip_id)) + "unable to find floating IP {0}".format(floating_ip_id) + ) error_message = "Error detaching IP {ip} from instance {id}".format( - ip=floating_ip_id, id=server_id) + ip=floating_ip_id, id=server_id + ) return proxy._json_response( self.compute.post( '/servers/{server_id}/action'.format(server_id=server_id), - json=dict(removeFloatingIp=dict( - address=f_ip['floating_ip_address']))), - error_message=error_message) + json=dict( + removeFloatingIp=dict(address=f_ip['floating_ip_address']) + ), + ), + error_message=error_message, + ) return True def _add_ip_from_pool( - self, server, network, fixed_address=None, reuse=True, - wait=False, timeout=60, nat_destination=None): + self, + server, + network, + fixed_address=None, + reuse=True, + wait=False, + timeout=60, + nat_destination=None, + ): """Add a floating IP to a server from a given pool This method reuses available IPs, when possible, or allocate new IPs @@ -851,9 +941,12 @@ def _add_ip_from_pool( start_time = time.time() f_ip = self.create_floating_ip( server=server, - network=network, nat_destination=nat_destination, + network=network, + nat_destination=nat_destination, fixed_address=fixed_address, - wait=wait, timeout=timeout) + wait=wait, + timeout=timeout, + ) timeout = timeout - (time.time() - start_time) # Wait for cache invalidation time so that we don't try # to attach the FIP a second time below @@ -866,12 +959,23 @@ def _add_ip_from_pool( # the attach function below to get back the server dict refreshed # with the FIP information. return self._attach_ip_to_server( - server=server, floating_ip=f_ip, fixed_address=fixed_address, - wait=wait, timeout=timeout, nat_destination=nat_destination) + server=server, + floating_ip=f_ip, + fixed_address=fixed_address, + wait=wait, + timeout=timeout, + nat_destination=nat_destination, + ) def add_ip_list( - self, server, ips, wait=False, timeout=60, - fixed_address=None, nat_destination=None): + self, + server, + ips, + wait=False, + timeout=60, + fixed_address=None, + nat_destination=None, + ): """Attach a list of IPs to a server. :param server: a server object @@ -896,10 +1000,16 @@ def add_ip_list( for ip in ips: f_ip = self.get_floating_ip( - id=None, filters={'floating_ip_address': ip}) + id=None, filters={'floating_ip_address': ip} + ) server = self._attach_ip_to_server( - server=server, floating_ip=f_ip, wait=wait, timeout=timeout, - fixed_address=fixed_address, nat_destination=nat_destination) + server=server, + floating_ip=f_ip, + wait=wait, + timeout=timeout, + fixed_address=fixed_address, + nat_destination=nat_destination, + ) return server def add_auto_ip(self, server, wait=False, timeout=60, reuse=True): @@ -925,7 +1035,8 @@ def add_auto_ip(self, server, wait=False, timeout=60, reuse=True): """ server = self._add_auto_ip( - server, wait=wait, timeout=timeout, reuse=reuse) + server, wait=wait, timeout=timeout, reuse=reuse + ) return server['interface_ip'] or None def _add_auto_ip(self, server, wait=False, timeout=60, reuse=True): @@ -936,7 +1047,8 @@ def _add_auto_ip(self, server, wait=False, timeout=60, reuse=True): else: start_time = time.time() f_ip = self.create_floating_ip( - server=server, wait=wait, timeout=timeout) + server=server, wait=wait, timeout=timeout + ) timeout = timeout - (time.time() - start_time) if server: # This gets passed in for both nova and neutron @@ -951,8 +1063,12 @@ def _add_auto_ip(self, server, wait=False, timeout=60, reuse=True): # the attach function below to get back the server dict refreshed # with the FIP information. return self._attach_ip_to_server( - server=server, floating_ip=f_ip, wait=wait, timeout=timeout, - skip_attach=skip_attach) + server=server, + floating_ip=f_ip, + wait=wait, + timeout=timeout, + skip_attach=skip_attach, + ) except exc.OpenStackCloudTimeout: if self._use_neutron_floating() and created: # We are here because we created an IP on the port @@ -962,36 +1078,60 @@ def _add_auto_ip(self, server, wait=False, timeout=60, reuse=True): "Timeout waiting for floating IP to become" " active. Floating IP %(ip)s:%(id)s was created for" " server %(server)s but is being deleted due to" - " activation failure.", { + " activation failure.", + { 'ip': f_ip['floating_ip_address'], 'id': f_ip['id'], - 'server': server['id']}) + 'server': server['id'], + }, + ) try: self.delete_floating_ip(f_ip['id']) except Exception as e: self.log.error( "FIP LEAK: Attempted to delete floating ip " "%(fip)s but received %(exc)s exception: %(err)s", - {'fip': f_ip['id'], 'exc': e.__class__, 'err': str(e)}) + {'fip': f_ip['id'], 'exc': e.__class__, 'err': str(e)}, + ) raise e raise def add_ips_to_server( - self, server, auto_ip=True, ips=None, ip_pool=None, - wait=False, timeout=60, reuse=True, fixed_address=None, - nat_destination=None): + self, + server, + auto_ip=True, + ips=None, + ip_pool=None, + wait=False, + timeout=60, + reuse=True, + fixed_address=None, + nat_destination=None, + ): if ip_pool: server = self._add_ip_from_pool( - server, ip_pool, reuse=reuse, wait=wait, timeout=timeout, - fixed_address=fixed_address, nat_destination=nat_destination) + server, + ip_pool, + reuse=reuse, + wait=wait, + timeout=timeout, + fixed_address=fixed_address, + nat_destination=nat_destination, + ) elif ips: server = self.add_ip_list( - server, ips, wait=wait, timeout=timeout, - fixed_address=fixed_address, nat_destination=nat_destination) + server, + ips, + wait=wait, + timeout=timeout, + fixed_address=fixed_address, + nat_destination=nat_destination, + ) elif auto_ip: if self._needs_floating_ip(server, nat_destination): server = self._add_auto_ip( - server, wait=wait, timeout=timeout, reuse=reuse) + server, wait=wait, timeout=timeout, reuse=reuse + ) return server def _needs_floating_ip(self, server, nat_destination): @@ -1026,18 +1166,30 @@ def _needs_floating_ip(self, server, nat_destination): # meta.add_server_interfaces() was not called server = self.compute.get_server(server) - if server['public_v4'] \ - or any([any([address['OS-EXT-IPS:type'] == 'floating' - for address in addresses]) - for addresses - in (server['addresses'] or {}).values()]): + if server['public_v4'] or any( + [ + any( + [ + address['OS-EXT-IPS:type'] == 'floating' + for address in addresses + ] + ) + for addresses in (server['addresses'] or {}).values() + ] + ): return False - if not server['private_v4'] \ - and not any([any([address['OS-EXT-IPS:type'] == 'fixed' - for address in addresses]) - for addresses - in (server['addresses'] or {}).values()]): + if not server['private_v4'] and not any( + [ + any( + [ + address['OS-EXT-IPS:type'] == 'fixed' + for address in addresses + ] + ) + for addresses in (server['addresses'] or {}).values() + ] + ): return False if self.private: @@ -1053,7 +1205,8 @@ def _needs_floating_ip(self, server, nat_destination): return False (port_obj, fixed_ip_address) = self._nat_destination_port( - server, nat_destination=nat_destination) + server, nat_destination=nat_destination + ) if not port_obj or not fixed_ip_address: return False @@ -1061,7 +1214,8 @@ def _needs_floating_ip(self, server, nat_destination): return True def _nat_destination_port( - self, server, fixed_address=None, nat_destination=None): + self, server, fixed_address=None, nat_destination=None + ): """Returns server port that is on a nat_destination network Find a port attached to the server which is on a network which @@ -1082,9 +1236,10 @@ def _nat_destination_port( else: timeout = None for count in utils.iterate_timeout( - timeout, - "Timeout waiting for port to show up in list", - wait=self._PORT_AGE): + timeout, + "Timeout waiting for port to show up in list", + wait=self._PORT_AGE, + ): try: port_filter = {'device_id': server['id']} ports = self.search_ports(filters=port_filter) @@ -1103,7 +1258,9 @@ def _nat_destination_port( 'NAT Destination {nat_destination} was configured' ' but not found on the cloud. Please check your' ' config and your cloud and try again.'.format( - nat_destination=nat_destination)) + nat_destination=nat_destination + ) + ) else: nat_network = self.get_nat_destination() @@ -1118,7 +1275,8 @@ def _nat_destination_port( ' nat_destination property of the networks list in' ' your clouds.yaml file. If you do not have a' ' clouds.yaml file, please make one - your setup' - ' is complicated.'.format(server=server['id'])) + ' is complicated.'.format(server=server['id']) + ) maybe_ports = [] for maybe_port in ports: @@ -1129,7 +1287,9 @@ def _nat_destination_port( 'No port on server {server} was found matching' ' your NAT destination network {dest}. Please ' ' check your config'.format( - server=server['id'], dest=nat_network['name'])) + server=server['id'], dest=nat_network['name'] + ) + ) ports = maybe_ports # Select the most recent available IPv4 address @@ -1139,9 +1299,8 @@ def _nat_destination_port( # if there are more than one, will be the arbitrary port we # select. for port in sorted( - ports, - key=lambda p: p.get('created_at', 0), - reverse=True): + ports, key=lambda p: p.get('created_at', 0), reverse=True + ): for address in port.get('fixed_ips', list()): try: ip = ipaddress.ip_address(address['ip_address']) @@ -1152,7 +1311,8 @@ def _nat_destination_port( return port, fixed_address raise exc.OpenStackCloudException( "unable to find a free fixed IPv4 address for server " - "{0}".format(server['id'])) + "{0}".format(server['id']) + ) # unfortunately a port can have more than one fixed IP: # we can't use the search_ports filtering for fixed_address as # they are contained in a list. e.g. @@ -1178,8 +1338,10 @@ def _has_floating_ips(self): return self._floating_ip_source in ('nova', 'neutron') def _use_neutron_floating(self): - return (self.has_service('network') - and self._floating_ip_source == 'neutron') + return ( + self.has_service('network') + and self._floating_ip_source == 'neutron' + ) def _normalize_floating_ips(self, ips): """Normalize the structure of floating IPs @@ -1210,16 +1372,13 @@ def _normalize_floating_ips(self, ips): ] """ - return [ - self._normalize_floating_ip(ip) for ip in ips - ] + return [self._normalize_floating_ip(ip) for ip in ips] def _normalize_floating_ip(self, ip): # Copy incoming floating ip because of shared dicts in unittests # Only import munch when we really need it - location = self._get_current_location( - project_id=ip.get('owner')) + location = self._get_current_location(project_id=ip.get('owner')) # This copy is to keep things from getting epically weird in tests ip = ip.copy() @@ -1228,7 +1387,8 @@ def _normalize_floating_ip(self, ip): fixed_ip_address = ip.pop('fixed_ip_address', ip.pop('fixed_ip', None)) floating_ip_address = ip.pop('floating_ip_address', ip.pop('ip', None)) network_id = ip.pop( - 'floating_network_id', ip.pop('network', ip.pop('pool', None))) + 'floating_network_id', ip.pop('network', ip.pop('pool', None)) + ) project_id = ip.pop('tenant_id', '') project_id = ip.pop('project_id', project_id) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index 5c8952797..a6cc6e29d 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -28,7 +28,8 @@ class IdentityCloudMixin: def _identity_client(self): if 'identity' not in self._raw_clients: self._raw_clients['identity'] = self._get_versioned_client( - 'identity', min_version=2, max_version='3.latest') + 'identity', min_version=2, max_version='3.latest' + ) return self._raw_clients['identity'] @_utils.cache_on_arguments() @@ -129,8 +130,9 @@ def get_project(self, name_or_id, filters=None, domain_id=None): :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ - return _utils._get_entity(self, 'project', name_or_id, filters, - domain_id=domain_id) + return _utils._get_entity( + self, 'project', name_or_id, filters, domain_id=domain_id + ) def update_project( self, @@ -178,7 +180,7 @@ def create_project( name=name, description=description, domain_id=domain_id, - is_enabled=enabled + is_enabled=enabled, ) if kwargs: attrs.update(kwargs) @@ -195,19 +197,19 @@ def delete_project(self, name_or_id, domain_id=None): """ try: project = self.identity.find_project( - name_or_id=name_or_id, - ignore_missing=True, - domain_id=domain_id + name_or_id=name_or_id, ignore_missing=True, domain_id=domain_id ) if not project: - self.log.debug( - "Project %s not found for deleting", name_or_id) + self.log.debug("Project %s not found for deleting", name_or_id) return False self.identity.delete_project(project) return True except exceptions.SDKException: - self.log.exception("Error in deleting project {project}".format( - project=name_or_id)) + self.log.exception( + "Error in deleting project {project}".format( + project=name_or_id + ) + ) return False @_utils.valid_kwargs('domain_id', 'name') @@ -299,8 +301,15 @@ def get_user_by_id(self, user_id, normalize=True): """ return self.identity.get_user(user_id) - @_utils.valid_kwargs('name', 'email', 'enabled', 'domain_id', 'password', - 'description', 'default_project') + @_utils.valid_kwargs( + 'name', + 'email', + 'enabled', + 'domain_id', + 'password', + 'description', + 'default_project', + ) def update_user(self, name_or_id, **kwargs): self.list_users.invalidate(self) user_kwargs = {} @@ -351,7 +360,8 @@ def delete_user(self, name_or_id, **kwargs): user = self.get_user(name_or_id, **kwargs) if not user: self.log.debug( - "User {0} not found for deleting".format(name_or_id)) + "User {0} not found for deleting".format(name_or_id) + ) return False self.identity.delete_user(user) @@ -359,21 +369,23 @@ def delete_user(self, name_or_id, **kwargs): return True except exceptions.SDKException: - self.log.exception("Error in deleting user {user}".format( - user=name_or_id - )) + self.log.exception( + "Error in deleting user {user}".format(user=name_or_id) + ) return False def _get_user_and_group(self, user_name_or_id, group_name_or_id): user = self.get_user(user_name_or_id) if not user: raise exc.OpenStackCloudException( - 'User {user} not found'.format(user=user_name_or_id)) + 'User {user} not found'.format(user=user_name_or_id) + ) group = self.get_group(group_name_or_id) if not group: raise exc.OpenStackCloudException( - 'Group {user} not found'.format(user=group_name_or_id)) + 'Group {user} not found'.format(user=group_name_or_id) + ) return (user, group) @@ -438,8 +450,9 @@ def create_service(self, name, enabled=True, **kwargs): return self.identity.create_service(**kwargs) - @_utils.valid_kwargs('name', 'enabled', 'type', 'service_type', - 'description') + @_utils.valid_kwargs( + 'name', 'enabled', 'type', 'service_type', 'description' + ) def update_service(self, name_or_id, **kwargs): # NOTE(SamYaple): Keystone v3 only accepts 'type' but shade accepts @@ -519,7 +532,8 @@ def delete_service(self, name_or_id): return True except exceptions.SDKException: self.log.exception( - 'Failed to delete service {id}'.format(id=service['id'])) + 'Failed to delete service {id}'.format(id=service['id']) + ) return False @_utils.valid_kwargs('public_url', 'internal_url', 'admin_url') @@ -560,31 +574,42 @@ def create_endpoint( if service is None: raise exc.OpenStackCloudException( "service {service} not found".format( - service=service_name_or_id)) + service=service_name_or_id + ) + ) endpoints_args = [] if url: # v3 in use, v3-like arguments, one endpoint created endpoints_args.append( - {'url': url, 'interface': interface, - 'service_id': service['id'], 'enabled': enabled, - 'region_id': region}) + { + 'url': url, + 'interface': interface, + 'service_id': service['id'], + 'enabled': enabled, + 'region_id': region, + } + ) else: # v3 in use, v2.0-like arguments, one endpoint created for each # interface url provided - endpoint_args = {'region_id': region, 'enabled': enabled, - 'service_id': service['id']} + endpoint_args = { + 'region_id': region, + 'enabled': enabled, + 'service_id': service['id'], + } if public_url: - endpoint_args.update({'url': public_url, - 'interface': 'public'}) + endpoint_args.update( + {'url': public_url, 'interface': 'public'} + ) endpoints_args.append(endpoint_args.copy()) if internal_url: - endpoint_args.update({'url': internal_url, - 'interface': 'internal'}) + endpoint_args.update( + {'url': internal_url, 'interface': 'internal'} + ) endpoints_args.append(endpoint_args.copy()) if admin_url: - endpoint_args.update({'url': admin_url, - 'interface': 'admin'}) + endpoint_args.update({'url': admin_url, 'interface': 'admin'}) endpoints_args.append(endpoint_args.copy()) endpoints = [] @@ -592,8 +617,9 @@ def create_endpoint( endpoints.append(self.identity.create_endpoint(**args)) return endpoints - @_utils.valid_kwargs('enabled', 'service_name_or_id', 'url', 'interface', - 'region') + @_utils.valid_kwargs( + 'enabled', 'service_name_or_id', 'url', 'interface', 'region' + ) def update_endpoint(self, endpoint_id, **kwargs): service_name_or_id = kwargs.pop('service_name_or_id', None) if service_name_or_id is not None: @@ -670,8 +696,7 @@ def delete_endpoint(self, id): self.identity.delete_endpoint(id) return True except exceptions.SDKException: - self.log.exception( - "Failed to delete endpoint {id}".format(id=id)) + self.log.exception("Failed to delete endpoint {id}".format(id=id)) return False def create_domain(self, name, description=None, enabled=True): @@ -746,7 +771,8 @@ def delete_domain(self, domain_id=None, name_or_id=None): dom = self.get_domain(name_or_id=name_or_id) if dom is None: self.log.debug( - "Domain %s not found for deleting", name_or_id) + "Domain %s not found for deleting", name_or_id + ) return False domain_id = dom['id'] @@ -963,8 +989,7 @@ def delete_group(self, name_or_id): try: group = self.identity.find_group(name_or_id) if group is None: - self.log.debug( - "Group %s not found for deleting", name_or_id) + self.log.debug("Group %s not found for deleting", name_or_id) return False self.identity.delete_group(group) @@ -974,7 +999,8 @@ def delete_group(self, name_or_id): except exceptions.SDKException: self.log.exception( - "Unable to delete group {name}".format(name=name_or_id)) + "Unable to delete group {name}".format(name=name_or_id) + ) return False def list_roles(self, **kwargs): @@ -1051,8 +1077,9 @@ def _keystone_v3_role_assignments(self, **filters): filters['scope.' + k + '.id'] = filters[k] del filters[k] if 'os_inherit_extension_inherited_to' in filters: - filters['scope.OS-INHERIT:inherited_to'] = ( - filters['os_inherit_extension_inherited_to']) + filters['scope.OS-INHERIT:inherited_to'] = filters[ + 'os_inherit_extension_inherited_to' + ] del filters['os_inherit_extension_inherited_to'] return list(self.identity.role_assignments(**filters)) @@ -1138,8 +1165,7 @@ def update_role(self, name_or_id, name, **kwargs): """ role = self.get_role(name_or_id, **kwargs) if role is None: - self.log.debug( - "Role %s not found for updating", name_or_id) + self.log.debug("Role %s not found for updating", name_or_id) return False return self.identity.update_role(role, name=name, **kwargs) @@ -1156,8 +1182,7 @@ def delete_role(self, name_or_id, **kwargs): """ role = self.get_role(name_or_id, **kwargs) if role is None: - self.log.debug( - "Role %s not found for deleting", name_or_id) + self.log.debug("Role %s not found for deleting", name_or_id) return False try: @@ -1165,17 +1190,25 @@ def delete_role(self, name_or_id, **kwargs): return True except exceptions.SDKExceptions: self.log.exception( - "Unable to delete role {name}".format( - name=name_or_id)) + "Unable to delete role {name}".format(name=name_or_id) + ) raise - def _get_grant_revoke_params(self, role, user=None, group=None, - project=None, domain=None, system=None): + def _get_grant_revoke_params( + self, + role, + user=None, + group=None, + project=None, + domain=None, + system=None, + ): data = {} search_args = {} if domain: data['domain'] = self.identity.find_domain( - domain, ignore_missing=False) + domain, ignore_missing=False + ) # We have domain. We should use it for further searching user, # group, role, project search_args['domain_id'] = data['domain'].id @@ -1183,33 +1216,47 @@ def _get_grant_revoke_params(self, role, user=None, group=None, data['role'] = self.identity.find_role(name_or_id=role) if not data['role']: raise exc.OpenStackCloudException( - 'Role {0} not found.'.format(role)) + 'Role {0} not found.'.format(role) + ) if user: # use cloud.get_user to save us from bad searching by name data['user'] = self.get_user(user, filters=search_args) if group: data['group'] = self.identity.find_group( - group, ignore_missing=False, **search_args) + group, ignore_missing=False, **search_args + ) if data.get('user') and data.get('group'): raise exc.OpenStackCloudException( - 'Specify either a group or a user, not both') + 'Specify either a group or a user, not both' + ) if data.get('user') is None and data.get('group') is None: raise exc.OpenStackCloudException( - 'Must specify either a user or a group') + 'Must specify either a user or a group' + ) if project is None and domain is None and system is None: raise exc.OpenStackCloudException( - 'Must specify either a domain, project or system') + 'Must specify either a domain, project or system' + ) if project: data['project'] = self.identity.find_project( - project, ignore_missing=False, **search_args) + project, ignore_missing=False, **search_args + ) return data - def grant_role(self, name_or_id, user=None, group=None, - project=None, domain=None, system=None, wait=False, - timeout=60): + def grant_role( + self, + name_or_id, + user=None, + group=None, + project=None, + domain=None, + system=None, + wait=False, + timeout=60, + ): """Grant a role to a user. :param string name_or_id: Name or unique ID of the role. @@ -1236,8 +1283,13 @@ def grant_role(self, name_or_id, user=None, group=None, :raise OpenStackCloudException: if the role cannot be granted """ data = self._get_grant_revoke_params( - name_or_id, user=user, group=group, - project=project, domain=domain, system=system) + name_or_id, + user=user, + group=group, + project=project, + domain=domain, + system=system, + ) user = data.get('user') group = data.get('group') @@ -1249,63 +1301,73 @@ def grant_role(self, name_or_id, user=None, group=None, # Proceed with project - precedence over domain and system if user: has_role = self.identity.validate_user_has_project_role( - project, user, role) + project, user, role + ) if has_role: self.log.debug('Assignment already exists') return False - self.identity.assign_project_role_to_user( - project, user, role) + self.identity.assign_project_role_to_user(project, user, role) else: has_role = self.identity.validate_group_has_project_role( - project, group, role) + project, group, role + ) if has_role: self.log.debug('Assignment already exists') return False self.identity.assign_project_role_to_group( - project, group, role) + project, group, role + ) elif domain: # Proceed with domain - precedence over system if user: has_role = self.identity.validate_user_has_domain_role( - domain, user, role) + domain, user, role + ) if has_role: self.log.debug('Assignment already exists') return False - self.identity.assign_domain_role_to_user( - domain, user, role) + self.identity.assign_domain_role_to_user(domain, user, role) else: has_role = self.identity.validate_group_has_domain_role( - domain, group, role) + domain, group, role + ) if has_role: self.log.debug('Assignment already exists') return False - self.identity.assign_domain_role_to_group( - domain, group, role) + self.identity.assign_domain_role_to_group(domain, group, role) else: # Proceed with system # System name must be 'all' due to checks performed in # _get_grant_revoke_params if user: has_role = self.identity.validate_user_has_system_role( - user, role, system) + user, role, system + ) if has_role: self.log.debug('Assignment already exists') return False - self.identity.assign_system_role_to_user( - user, role, system) + self.identity.assign_system_role_to_user(user, role, system) else: has_role = self.identity.validate_group_has_system_role( - group, role, system) + group, role, system + ) if has_role: self.log.debug('Assignment already exists') return False - self.identity.assign_system_role_to_group( - group, role, system) + self.identity.assign_system_role_to_group(group, role, system) return True - def revoke_role(self, name_or_id, user=None, group=None, - project=None, domain=None, system=None, - wait=False, timeout=60): + def revoke_role( + self, + name_or_id, + user=None, + group=None, + project=None, + domain=None, + system=None, + wait=False, + timeout=60, + ): """Revoke a role from a user. :param string name_or_id: Name or unique ID of the role. @@ -1329,8 +1391,13 @@ def revoke_role(self, name_or_id, user=None, group=None, :raise OpenStackCloudException: if the role cannot be removed """ data = self._get_grant_revoke_params( - name_or_id, user=user, group=group, - project=project, domain=domain, system=system) + name_or_id, + user=user, + group=group, + project=project, + domain=domain, + system=system, + ) user = data.get('user') group = data.get('group') @@ -1342,58 +1409,70 @@ def revoke_role(self, name_or_id, user=None, group=None, # Proceed with project - precedence over domain and system if user: has_role = self.identity.validate_user_has_project_role( - project, user, role) + project, user, role + ) if not has_role: self.log.debug('Assignment does not exists') return False self.identity.unassign_project_role_from_user( - project, user, role) + project, user, role + ) else: has_role = self.identity.validate_group_has_project_role( - project, group, role) + project, group, role + ) if not has_role: self.log.debug('Assignment does not exists') return False self.identity.unassign_project_role_from_group( - project, group, role) + project, group, role + ) elif domain: # Proceed with domain - precedence over system if user: has_role = self.identity.validate_user_has_domain_role( - domain, user, role) + domain, user, role + ) if not has_role: self.log.debug('Assignment does not exists') return False self.identity.unassign_domain_role_from_user( - domain, user, role) + domain, user, role + ) else: has_role = self.identity.validate_group_has_domain_role( - domain, group, role) + domain, group, role + ) if not has_role: self.log.debug('Assignment does not exists') return False self.identity.unassign_domain_role_from_group( - domain, group, role) + domain, group, role + ) else: # Proceed with system # System name must be 'all' due to checks performed in # _get_grant_revoke_params if user: has_role = self.identity.validate_user_has_system_role( - user, role, system) + user, role, system + ) if not has_role: self.log.debug('Assignment does not exist') return False self.identity.unassign_system_role_from_user( - user, role, system) + user, role, system + ) else: has_role = self.identity.validate_group_has_system_role( - group, role, system) + group, role, system + ) if not has_role: self.log.debug('Assignment does not exist') return False self.identity.unassign_system_role_from_group( - group, role, system) + group, role, system + ) return True def _get_identity_params(self, domain_id=None, project=None): @@ -1406,7 +1485,8 @@ def _get_identity_params(self, domain_id=None, project=None): if not domain_id: raise exc.OpenStackCloudException( "User or project creation requires an explicit" - " domain_id argument.") + " domain_id argument." + ) else: ret.update({'domain_id': domain_id}) diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py index 68174c995..1bd240fcb 100644 --- a/openstack/cloud/_image.py +++ b/openstack/cloud/_image.py @@ -46,7 +46,8 @@ def _raw_image_client(self): def _image_client(self): if 'image' not in self._raw_clients: self._raw_clients['image'] = self._get_versioned_client( - 'image', min_version=1, max_version='2.latest') + 'image', min_version=1, max_version='2.latest' + ) return self._raw_clients['image'] def search_images(self, name_or_id=None, filters=None): @@ -108,7 +109,7 @@ def get_image(self, name_or_id, filters=None): return _utils._get_entity(self, 'image', name_or_id, filters) def get_image_by_id(self, id): - """ Get a image by ID + """Get a image by ID :param id: ID of the image. :returns: An image :class:`openstack.image.v2.image.Image` object. @@ -145,20 +146,23 @@ def download_image( if output_path is None and output_file is None: raise exc.OpenStackCloudException( 'No output specified, an output path or file object' - ' is necessary to write the image data to') + ' is necessary to write the image data to' + ) elif output_path is not None and output_file is not None: raise exc.OpenStackCloudException( 'Both an output path and file object were provided,' - ' however only one can be used at once') + ' however only one can be used at once' + ) image = self.image.find_image(name_or_id) if not image: raise exc.OpenStackCloudResourceNotFound( - "No images with name or ID %s were found" % name_or_id, None) + "No images with name or ID %s were found" % name_or_id, None + ) return self.image.download_image( - image, output=output_file or output_path, - chunk_size=chunk_size) + image, output=output_file or output_path, chunk_size=chunk_size + ) def get_image_exclude(self, name_or_id, exclude): for image in self.search_images(name_or_id): @@ -184,7 +188,8 @@ def get_image_id(self, image_name, exclude=None): def wait_for_image(self, image, timeout=3600): image_id = image['id'] for count in utils.iterate_timeout( - timeout, "Timeout waiting for image to snapshot"): + timeout, "Timeout waiting for image to snapshot" + ): self.list_images.invalidate(self) image = self.get_image(image_id) if not image: @@ -193,7 +198,8 @@ def wait_for_image(self, image, timeout=3600): return image elif image['status'] == 'error': raise exc.OpenStackCloudException( - 'Image {image} hit error state'.format(image=image_id)) + 'Image {image} hit error state'.format(image=image_id) + ) def delete_image( self, @@ -222,17 +228,19 @@ def delete_image( # Task API means an image was uploaded to swift # TODO(gtema) does it make sense to move this into proxy? if self.image_api_use_tasks and ( - self.image._IMAGE_OBJECT_KEY in image.properties - or self.image._SHADE_IMAGE_OBJECT_KEY in image.properties): + self.image._IMAGE_OBJECT_KEY in image.properties + or self.image._SHADE_IMAGE_OBJECT_KEY in image.properties + ): (container, objname) = image.properties.get( - self.image._IMAGE_OBJECT_KEY, image.properties.get( - self.image._SHADE_IMAGE_OBJECT_KEY)).split('/', 1) + self.image._IMAGE_OBJECT_KEY, + image.properties.get(self.image._SHADE_IMAGE_OBJECT_KEY), + ).split('/', 1) self.delete_object(container=container, name=objname) if wait: for count in utils.iterate_timeout( - timeout, - "Timeout waiting for the image to be deleted."): + timeout, "Timeout waiting for the image to be deleted." + ): self._get_cache(None).invalidate() if self.get_image(image.id) is None: break @@ -307,38 +315,53 @@ def create_image( """ if volume: image = self.block_storage.create_image( - name=name, volume=volume, + name=name, + volume=volume, allow_duplicates=allow_duplicates, - container_format=container_format, disk_format=disk_format, - wait=wait, timeout=timeout) + container_format=container_format, + disk_format=disk_format, + wait=wait, + timeout=timeout, + ) else: image = self.image.create_image( - name, filename=filename, + name, + filename=filename, container=container, - md5=md5, sha256=sha256, - disk_format=disk_format, container_format=container_format, + md5=md5, + sha256=sha256, + disk_format=disk_format, + container_format=container_format, disable_vendor_agent=disable_vendor_agent, - wait=wait, timeout=timeout, tags=tags, - allow_duplicates=allow_duplicates, meta=meta, **kwargs) + wait=wait, + timeout=timeout, + tags=tags, + allow_duplicates=allow_duplicates, + meta=meta, + **kwargs, + ) self._get_cache(None).invalidate() if not wait: return image try: for count in utils.iterate_timeout( - timeout, - "Timeout waiting for the image to finish."): + timeout, "Timeout waiting for the image to finish." + ): image_obj = self.get_image(image.id) if image_obj and image_obj.status not in ('queued', 'saving'): return image_obj except exc.OpenStackCloudTimeout: self.log.debug( - "Timeout waiting for image to become ready. Deleting.") + "Timeout waiting for image to become ready. Deleting." + ) self.delete_image(image.id, wait=True) raise def update_image_properties( - self, image=None, name_or_id=None, meta=None, **properties): + self, image=None, name_or_id=None, meta=None, **properties + ): image = image or name_or_id return self.image.update_image_properties( - image=image, meta=meta, **properties) + image=image, meta=meta, **properties + ) diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index a4f5b81c2..ae7d96f94 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -1,4 +1,3 @@ - # 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 @@ -231,14 +230,14 @@ def get_qos_policy(self, name_or_id, filters=None): """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') + 'QoS extension is not available on target cloud' + ) if not filters: filters = {} return self.network.find_qos_policy( - name_or_id=name_or_id, - ignore_missing=True, - **filters) + name_or_id=name_or_id, ignore_missing=True, **filters + ) # TODO(stephenfin): Deprecate this in favour of the 'list' function def search_qos_policies(self, name_or_id=None, filters=None): @@ -254,7 +253,8 @@ def search_qos_policies(self, name_or_id=None, filters=None): """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') + 'QoS extension is not available on target cloud' + ) query = {} if name_or_id: @@ -271,7 +271,8 @@ def list_qos_rule_types(self, filters=None): """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') + 'QoS extension is not available on target cloud' + ) # Translate None from search interface to empty {} for kwargs below if not filters: @@ -302,12 +303,14 @@ def get_qos_rule_type_details(self, rule_type, filters=None): """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') + 'QoS extension is not available on target cloud' + ) if not self._has_neutron_extension('qos-rule-type-details'): raise exc.OpenStackCloudUnavailableExtension( 'qos-rule-type-details extension is not available ' - 'on target cloud') + 'on target cloud' + ) return self.network.get_qos_rule_type(rule_type) @@ -319,7 +322,8 @@ def list_qos_policies(self, filters=None): """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') + 'QoS extension is not available on target cloud' + ) # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} @@ -350,9 +354,8 @@ def get_network(self, name_or_id, filters=None): if not filters: filters = {} return self.network.find_network( - name_or_id=name_or_id, - ignore_missing=True, - **filters) + name_or_id=name_or_id, ignore_missing=True, **filters + ) def get_network_by_id(self, id): """Get a network by ID @@ -387,9 +390,8 @@ def get_router(self, name_or_id, filters=None): if not filters: filters = {} return self.network.find_router( - name_or_id=name_or_id, - ignore_missing=True, - **filters) + name_or_id=name_or_id, ignore_missing=True, **filters + ) # TODO(stephenfin): Deprecate 'filters'; users should use 'list' for this def get_subnet(self, name_or_id, filters=None): @@ -412,9 +414,8 @@ def get_subnet(self, name_or_id, filters=None): if not filters: filters = {} return self.network.find_subnet( - name_or_id=name_or_id, - ignore_missing=True, - **filters) + name_or_id=name_or_id, ignore_missing=True, **filters + ) def get_subnet_by_id(self, id): """Get a subnet by ID @@ -449,9 +450,8 @@ def get_port(self, name_or_id, filters=None): if not filters: filters = {} return self.network.find_port( - name_or_id=name_or_id, - ignore_missing=True, - **filters) + name_or_id=name_or_id, ignore_missing=True, **filters + ) def get_port_by_id(self, id): """Get a port by ID @@ -510,20 +510,26 @@ def create_network( if availability_zone_hints is not None: if not isinstance(availability_zone_hints, list): raise exc.OpenStackCloudException( - "Parameter 'availability_zone_hints' must be a list") + "Parameter 'availability_zone_hints' must be a list" + ) if not self._has_neutron_extension('network_availability_zone'): raise exc.OpenStackCloudUnavailableExtension( 'network_availability_zone extension is not available on ' - 'target cloud') + 'target cloud' + ) network['availability_zone_hints'] = availability_zone_hints if provider: if not isinstance(provider, dict): raise exc.OpenStackCloudException( - "Parameter 'provider' must be a dict") + "Parameter 'provider' must be a dict" + ) # Only pass what we know - for attr in ('physical_network', 'network_type', - 'segmentation_id'): + for attr in ( + 'physical_network', + 'network_type', + 'segmentation_id', + ): if attr in provider: arg = "provider:" + attr network[arg] = provider[attr] @@ -537,16 +543,19 @@ def create_network( if port_security_enabled is not None: if not isinstance(port_security_enabled, bool): raise exc.OpenStackCloudException( - "Parameter 'port_security_enabled' must be a bool") + "Parameter 'port_security_enabled' must be a bool" + ) network['port_security_enabled'] = port_security_enabled if mtu_size: if not isinstance(mtu_size, int): raise exc.OpenStackCloudException( - "Parameter 'mtu_size' must be an integer.") + "Parameter 'mtu_size' must be an integer." + ) if not mtu_size >= 68: raise exc.OpenStackCloudException( - "Parameter 'mtu_size' must be greater than 67.") + "Parameter 'mtu_size' must be greater than 67." + ) network['mtu'] = mtu_size @@ -559,9 +568,16 @@ def create_network( self._reset_network_caches() return network - @_utils.valid_kwargs("name", "shared", "admin_state_up", "external", - "provider", "mtu_size", "port_security_enabled", - "dns_domain") + @_utils.valid_kwargs( + "name", + "shared", + "admin_state_up", + "external", + "provider", + "mtu_size", + "port_security_enabled", + "dns_domain", + ) def update_network(self, name_or_id, **kwargs): """Update a network. @@ -586,9 +602,9 @@ def update_network(self, name_or_id, **kwargs): if provider: if not isinstance(provider, dict): raise exc.OpenStackCloudException( - "Parameter 'provider' must be a dict") - for key in ('physical_network', 'network_type', - 'segmentation_id'): + "Parameter 'provider' must be a dict" + ) + for key in ('physical_network', 'network_type', 'segmentation_id'): if key in provider: kwargs['provider:' + key] = provider.pop(key) @@ -598,21 +614,25 @@ def update_network(self, name_or_id, **kwargs): if 'port_security_enabled' in kwargs: if not isinstance(kwargs['port_security_enabled'], bool): raise exc.OpenStackCloudException( - "Parameter 'port_security_enabled' must be a bool") + "Parameter 'port_security_enabled' must be a bool" + ) if 'mtu_size' in kwargs: if not isinstance(kwargs['mtu_size'], int): raise exc.OpenStackCloudException( - "Parameter 'mtu_size' must be an integer.") + "Parameter 'mtu_size' must be an integer." + ) if kwargs['mtu_size'] < 68: raise exc.OpenStackCloudException( - "Parameter 'mtu_size' must be greater than 67.") + "Parameter 'mtu_size' must be greater than 67." + ) kwargs['mtu'] = kwargs.pop('mtu_size') network = self.get_network(name_or_id) if not network: raise exc.OpenStackCloudException( - "Network %s not found." % name_or_id) + "Network %s not found." % name_or_id + ) network = self.network.update_network(network, **kwargs) @@ -666,8 +686,7 @@ def get_network_quotas(self, name_or_id, details=False): :raises: OpenStackCloudException if it's not a valid project :returns: A network ``Quota`` object if found, else None. """ - proj = self.identity.find_project( - name_or_id, ignore_missing=False) + proj = self.identity.find_project(name_or_id, ignore_missing=False) return self.network.get_quota(proj.id, details) def get_network_extensions(self): @@ -692,10 +711,21 @@ def delete_network_quotas(self, name_or_id): self.network.delete_quota(proj.id) @_utils.valid_kwargs( - 'action', 'description', 'destination_firewall_group_id', - 'destination_ip_address', 'destination_port', 'enabled', 'ip_version', - 'name', 'project_id', 'protocol', 'shared', 'source_firewall_group_id', - 'source_ip_address', 'source_port') + 'action', + 'description', + 'destination_firewall_group_id', + 'destination_ip_address', + 'destination_port', + 'enabled', + 'ip_version', + 'name', + 'project_id', + 'protocol', + 'shared', + 'source_firewall_group_id', + 'source_ip_address', + 'source_port', + ) def create_firewall_rule(self, **kwargs): """ Creates firewall rule. @@ -753,12 +783,15 @@ def delete_firewall_rule(self, name_or_id, filters=None): filters = {} try: firewall_rule = self.network.find_firewall_rule( - name_or_id, ignore_missing=False, **filters) - self.network.delete_firewall_rule(firewall_rule, - ignore_missing=False) + name_or_id, ignore_missing=False, **filters + ) + self.network.delete_firewall_rule( + firewall_rule, ignore_missing=False + ) except exceptions.ResourceNotFound: - self.log.debug('Firewall rule %s not found for deleting', - name_or_id) + self.log.debug( + 'Firewall rule %s not found for deleting', name_or_id + ) return False return True @@ -789,9 +822,8 @@ def get_firewall_rule(self, name_or_id, filters=None): if not filters: filters = {} return self.network.find_firewall_rule( - name_or_id, - ignore_missing=True, - **filters) + name_or_id, ignore_missing=True, **filters + ) def list_firewall_rules(self, filters=None): """ @@ -820,10 +852,21 @@ def list_firewall_rules(self, filters=None): return list(self.network.firewall_rules(**filters)) @_utils.valid_kwargs( - 'action', 'description', 'destination_firewall_group_id', - 'destination_ip_address', 'destination_port', 'enabled', 'ip_version', - 'name', 'project_id', 'protocol', 'shared', 'source_firewall_group_id', - 'source_ip_address', 'source_port') + 'action', + 'description', + 'destination_firewall_group_id', + 'destination_ip_address', + 'destination_port', + 'enabled', + 'ip_version', + 'name', + 'project_id', + 'protocol', + 'shared', + 'source_firewall_group_id', + 'source_ip_address', + 'source_port', + ) def update_firewall_rule(self, name_or_id, filters=None, **kwargs): """ Updates firewall rule. @@ -853,7 +896,8 @@ def update_firewall_rule(self, name_or_id, filters=None, **kwargs): if not filters: filters = {} firewall_rule = self.network.find_firewall_rule( - name_or_id, ignore_missing=False, **filters) + name_or_id, ignore_missing=False, **filters + ) return self.network.update_firewall_rule(firewall_rule, **kwargs) @@ -875,12 +919,21 @@ def _get_firewall_rule_ids(self, name_or_id_list, filters=None): filters = {} ids_list = [] for name_or_id in name_or_id_list: - ids_list.append(self.network.find_firewall_rule( - name_or_id, ignore_missing=False, **filters)['id']) + ids_list.append( + self.network.find_firewall_rule( + name_or_id, ignore_missing=False, **filters + )['id'] + ) return ids_list - @_utils.valid_kwargs('audited', 'description', 'firewall_rules', 'name', - 'project_id', 'shared') + @_utils.valid_kwargs( + 'audited', + 'description', + 'firewall_rules', + 'name', + 'project_id', + 'shared', + ) def create_firewall_policy(self, **kwargs): """ Create firewall policy. @@ -900,7 +953,8 @@ def create_firewall_policy(self, **kwargs): """ if 'firewall_rules' in kwargs: kwargs['firewall_rules'] = self._get_firewall_rule_ids( - kwargs['firewall_rules']) + kwargs['firewall_rules'] + ) return self.network.create_firewall_policy(**kwargs) @@ -933,12 +987,15 @@ def delete_firewall_policy(self, name_or_id, filters=None): filters = {} try: firewall_policy = self.network.find_firewall_policy( - name_or_id, ignore_missing=False, **filters) - self.network.delete_firewall_policy(firewall_policy, - ignore_missing=False) + name_or_id, ignore_missing=False, **filters + ) + self.network.delete_firewall_policy( + firewall_policy, ignore_missing=False + ) except exceptions.ResourceNotFound: - self.log.debug('Firewall policy %s not found for deleting', - name_or_id) + self.log.debug( + 'Firewall policy %s not found for deleting', name_or_id + ) return False return True @@ -969,9 +1026,8 @@ def get_firewall_policy(self, name_or_id, filters=None): if not filters: filters = {} return self.network.find_firewall_policy( - name_or_id, - ignore_missing=True, - **filters) + name_or_id, ignore_missing=True, **filters + ) def list_firewall_policies(self, filters=None): """ @@ -999,8 +1055,14 @@ def list_firewall_policies(self, filters=None): filters = {} return list(self.network.firewall_policies(**filters)) - @_utils.valid_kwargs('audited', 'description', 'firewall_rules', 'name', - 'project_id', 'shared') + @_utils.valid_kwargs( + 'audited', + 'description', + 'firewall_rules', + 'name', + 'project_id', + 'shared', + ) def update_firewall_policy(self, name_or_id, filters=None, **kwargs): """ Updates firewall policy. @@ -1031,17 +1093,24 @@ def update_firewall_policy(self, name_or_id, filters=None, **kwargs): if not filters: filters = {} firewall_policy = self.network.find_firewall_policy( - name_or_id, ignore_missing=False, **filters) + name_or_id, ignore_missing=False, **filters + ) if 'firewall_rules' in kwargs: kwargs['firewall_rules'] = self._get_firewall_rule_ids( - kwargs['firewall_rules']) + kwargs['firewall_rules'] + ) return self.network.update_firewall_policy(firewall_policy, **kwargs) - def insert_rule_into_policy(self, name_or_id, rule_name_or_id, - insert_after=None, insert_before=None, - filters=None): + def insert_rule_into_policy( + self, + name_or_id, + rule_name_or_id, + insert_after=None, + insert_before=None, + filters=None, + ): """Add firewall rule to a policy. Adds firewall rule to the firewall_rules list of a firewall policy. @@ -1064,33 +1133,40 @@ def insert_rule_into_policy(self, name_or_id, rule_name_or_id, if not filters: filters = {} firewall_policy = self.network.find_firewall_policy( - name_or_id, ignore_missing=False, **filters) + name_or_id, ignore_missing=False, **filters + ) firewall_rule = self.network.find_firewall_rule( - rule_name_or_id, ignore_missing=False) + rule_name_or_id, ignore_missing=False + ) # short-circuit if rule already in firewall_rules list # the API can't do any re-ordering of existing rules if firewall_rule['id'] in firewall_policy['firewall_rules']: self.log.debug( 'Firewall rule %s already associated with firewall policy %s', - rule_name_or_id, name_or_id) + rule_name_or_id, + name_or_id, + ) return firewall_policy pos_params = {} if insert_after is not None: pos_params['insert_after'] = self.network.find_firewall_rule( - insert_after, ignore_missing=False)['id'] + insert_after, ignore_missing=False + )['id'] if insert_before is not None: pos_params['insert_before'] = self.network.find_firewall_rule( - insert_before, ignore_missing=False)['id'] + insert_before, ignore_missing=False + )['id'] - return self.network.insert_rule_into_policy(firewall_policy['id'], - firewall_rule['id'], - **pos_params) + return self.network.insert_rule_into_policy( + firewall_policy['id'], firewall_rule['id'], **pos_params + ) - def remove_rule_from_policy(self, name_or_id, rule_name_or_id, - filters=None): + def remove_rule_from_policy( + self, name_or_id, rule_name_or_id, filters=None + ): """ Remove firewall rule from firewall policy's firewall_rules list. Short-circuits and returns firewall policy early if firewall rule @@ -1107,14 +1183,16 @@ def remove_rule_from_policy(self, name_or_id, rule_name_or_id, if not filters: filters = {} firewall_policy = self.network.find_firewall_policy( - name_or_id, ignore_missing=False, **filters) + name_or_id, ignore_missing=False, **filters + ) firewall_rule = self.network.find_firewall_rule(rule_name_or_id) if not firewall_rule: # short-circuit: if firewall rule is not found, # return current firewall policy - self.log.debug('Firewall rule %s not found for removing', - rule_name_or_id) + self.log.debug( + 'Firewall rule %s not found for removing', rule_name_or_id + ) return firewall_policy if firewall_rule['id'] not in firewall_policy['firewall_rules']: @@ -1122,15 +1200,25 @@ def remove_rule_from_policy(self, name_or_id, rule_name_or_id, # log it to debug and return current firewall policy self.log.debug( 'Firewall rule %s not associated with firewall policy %s', - rule_name_or_id, name_or_id) + rule_name_or_id, + name_or_id, + ) return firewall_policy - return self.network.remove_rule_from_policy(firewall_policy['id'], - firewall_rule['id']) + return self.network.remove_rule_from_policy( + firewall_policy['id'], firewall_rule['id'] + ) @_utils.valid_kwargs( - 'admin_state_up', 'description', 'egress_firewall_policy', - 'ingress_firewall_policy', 'name', 'ports', 'project_id', 'shared') + 'admin_state_up', + 'description', + 'egress_firewall_policy', + 'ingress_firewall_policy', + 'name', + 'ports', + 'project_id', + 'shared', + ) def create_firewall_group(self, **kwargs): """ Creates firewall group. The keys egress_firewall_policy and @@ -1174,12 +1262,15 @@ def delete_firewall_group(self, name_or_id, filters=None): filters = {} try: firewall_group = self.network.find_firewall_group( - name_or_id, ignore_missing=False, **filters) - self.network.delete_firewall_group(firewall_group, - ignore_missing=False) + name_or_id, ignore_missing=False, **filters + ) + self.network.delete_firewall_group( + firewall_group, ignore_missing=False + ) except exceptions.ResourceNotFound: - self.log.debug('Firewall group %s not found for deleting', - name_or_id) + self.log.debug( + 'Firewall group %s not found for deleting', name_or_id + ) return False return True @@ -1196,9 +1287,8 @@ def get_firewall_group(self, name_or_id, filters=None): if not filters: filters = {} return self.network.find_firewall_group( - name_or_id, - ignore_missing=True, - **filters) + name_or_id, ignore_missing=True, **filters + ) def list_firewall_groups(self, filters=None): """ @@ -1211,8 +1301,15 @@ def list_firewall_groups(self, filters=None): return list(self.network.firewall_groups(**filters)) @_utils.valid_kwargs( - 'admin_state_up', 'description', 'egress_firewall_policy', - 'ingress_firewall_policy', 'name', 'ports', 'project_id', 'shared') + 'admin_state_up', + 'description', + 'egress_firewall_policy', + 'ingress_firewall_policy', + 'name', + 'ports', + 'project_id', + 'shared', + ) def update_firewall_group(self, name_or_id, filters=None, **kwargs): """ Updates firewall group. @@ -1234,7 +1331,8 @@ def update_firewall_group(self, name_or_id, filters=None, **kwargs): if not filters: filters = {} firewall_group = self.network.find_firewall_group( - name_or_id, ignore_missing=False, **filters) + name_or_id, ignore_missing=False, **filters + ) self._lookup_ingress_egress_firewall_policy_ids(kwargs) if 'ports' in kwargs: @@ -1260,12 +1358,14 @@ def _lookup_ingress_egress_firewall_policy_ids(self, firewall_group): val = None else: val = self.network.find_firewall_policy( - firewall_group[key], ignore_missing=False)['id'] + firewall_group[key], ignore_missing=False + )['id'] firewall_group[key + '_id'] = val del firewall_group[key] - @_utils.valid_kwargs("name", "description", "shared", "default", - "project_id") + @_utils.valid_kwargs( + "name", "description", "shared", "default", "project_id" + ) def create_qos_policy(self, **kwargs): """Create a QoS policy. @@ -1280,20 +1380,24 @@ def create_qos_policy(self, **kwargs): """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') + 'QoS extension is not available on target cloud' + ) default = kwargs.pop("default", None) if default is not None: if self._has_neutron_extension('qos-default'): kwargs['is_default'] = default else: - self.log.debug("'qos-default' extension is not available on " - "target cloud") + self.log.debug( + "'qos-default' extension is not available on " + "target cloud" + ) return self.network.create_qos_policy(**kwargs) - @_utils.valid_kwargs("name", "description", "shared", "default", - "project_id") + @_utils.valid_kwargs( + "name", "description", "shared", "default", "project_id" + ) def update_qos_policy(self, name_or_id, **kwargs): """Update an existing QoS policy. @@ -1308,15 +1412,18 @@ def update_qos_policy(self, name_or_id, **kwargs): """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') + 'QoS extension is not available on target cloud' + ) default = kwargs.pop("default", None) if default is not None: if self._has_neutron_extension('qos-default'): kwargs['is_default'] = default else: - self.log.debug("'qos-default' extension is not available on " - "target cloud") + self.log.debug( + "'qos-default' extension is not available on " + "target cloud" + ) if not kwargs: self.log.debug("No QoS policy data to update") @@ -1325,7 +1432,8 @@ def update_qos_policy(self, name_or_id, **kwargs): curr_policy = self.network.find_qos_policy(name_or_id) if not curr_policy: raise exc.OpenStackCloudException( - "QoS policy %s not found." % name_or_id) + "QoS policy %s not found." % name_or_id + ) return self.network.update_qos_policy(curr_policy, **kwargs) @@ -1340,7 +1448,8 @@ def delete_qos_policy(self, name_or_id): """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') + 'QoS extension is not available on target cloud' + ) policy = self.network.find_qos_policy(name_or_id) if not policy: self.log.debug("QoS policy %s not found for deleting", name_or_id) @@ -1384,20 +1493,26 @@ def list_qos_bandwidth_limit_rules(self, policy_name_or_id, filters=None): """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') + 'QoS extension is not available on target cloud' + ) policy = self.network.find_qos_policy(policy_name_or_id) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) + name_or_id=policy_name_or_id + ) + ) # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} - return list(self.network.qos_bandwidth_limit_rules( - qos_policy=policy, **filters)) + return list( + self.network.qos_bandwidth_limit_rules( + qos_policy=policy, **filters + ) + ) def get_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): """Get a QoS bandwidth limit rule by name or ID. @@ -1410,16 +1525,18 @@ def get_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') + 'QoS extension is not available on target cloud' + ) policy = self.network.find_qos_policy(policy_name_or_id) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) + name_or_id=policy_name_or_id + ) + ) - return self.network.get_qos_bandwidth_limit_rule( - rule_id, policy) + return self.network.get_qos_bandwidth_limit_rule(rule_id, policy) @_utils.valid_kwargs("max_burst_kbps", "direction") def create_qos_bandwidth_limit_rule( @@ -1442,28 +1559,33 @@ def create_qos_bandwidth_limit_rule( """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') + 'QoS extension is not available on target cloud' + ) policy = self.network.find_qos_policy(policy_name_or_id) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) + name_or_id=policy_name_or_id + ) + ) if kwargs.get("direction") is not None: if not self._has_neutron_extension('qos-bw-limit-direction'): kwargs.pop("direction") self.log.debug( "'qos-bw-limit-direction' extension is not available on " - "target cloud") + "target cloud" + ) kwargs['max_kbps'] = max_kbps return self.network.create_qos_bandwidth_limit_rule(policy, **kwargs) @_utils.valid_kwargs("max_kbps", "max_burst_kbps", "direction") - def update_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id, - **kwargs): + def update_qos_bandwidth_limit_rule( + self, policy_name_or_id, rule_id, **kwargs + ): """Update a QoS bandwidth limit rule. :param string policy_name_or_id: Name or ID of the QoS policy to which @@ -1479,37 +1601,43 @@ def update_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id, """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') + 'QoS extension is not available on target cloud' + ) policy = self.network.find_qos_policy( - policy_name_or_id, - ignore_missing=True) + policy_name_or_id, ignore_missing=True + ) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) + name_or_id=policy_name_or_id + ) + ) if kwargs.get("direction") is not None: if not self._has_neutron_extension('qos-bw-limit-direction'): kwargs.pop("direction") self.log.debug( "'qos-bw-limit-direction' extension is not available on " - "target cloud") + "target cloud" + ) if not kwargs: self.log.debug("No QoS bandwidth limit rule data to update") return curr_rule = self.network.get_qos_bandwidth_limit_rule( - qos_rule=rule_id, qos_policy=policy) + qos_rule=rule_id, qos_policy=policy + ) if not curr_rule: raise exc.OpenStackCloudException( "QoS bandwidth_limit_rule {rule_id} not found in policy " - "{policy_id}".format(rule_id=rule_id, - policy_id=policy['id'])) + "{policy_id}".format(rule_id=rule_id, policy_id=policy['id']) + ) return self.network.update_qos_bandwidth_limit_rule( - qos_rule=curr_rule, qos_policy=policy, **kwargs) + qos_rule=curr_rule, qos_policy=policy, **kwargs + ) def delete_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): """Delete a QoS bandwidth limit rule. @@ -1522,22 +1650,28 @@ def delete_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') + 'QoS extension is not available on target cloud' + ) policy = self.network.find_qos_policy(policy_name_or_id) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) + name_or_id=policy_name_or_id + ) + ) try: self.network.delete_qos_bandwidth_limit_rule( - rule_id, policy, ignore_missing=False) + rule_id, policy, ignore_missing=False + ) except exceptions.ResourceNotFound: self.log.debug( "QoS bandwidth limit rule {rule_id} not found in policy " - "{policy_id}. Ignoring.".format(rule_id=rule_id, - policy_id=policy['id'])) + "{policy_id}. Ignoring.".format( + rule_id=rule_id, policy_id=policy['id'] + ) + ) return False return True @@ -1576,14 +1710,18 @@ def list_qos_dscp_marking_rules(self, policy_name_or_id, filters=None): """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') + 'QoS extension is not available on target cloud' + ) policy = self.network.find_qos_policy( - policy_name_or_id, ignore_missing=True) + policy_name_or_id, ignore_missing=True + ) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) + name_or_id=policy_name_or_id + ) + ) # Translate None from search interface to empty {} for kwargs below if not filters: @@ -1601,13 +1739,16 @@ def get_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') + 'QoS extension is not available on target cloud' + ) policy = self.network.find_qos_policy(policy_name_or_id) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) + name_or_id=policy_name_or_id + ) + ) return self.network.get_qos_dscp_marking_rule(rule_id, policy) @@ -1626,20 +1767,25 @@ def create_qos_dscp_marking_rule( """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') + 'QoS extension is not available on target cloud' + ) policy = self.network.find_qos_policy(policy_name_or_id) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) + name_or_id=policy_name_or_id + ) + ) return self.network.create_qos_dscp_marking_rule( - policy, dscp_mark=dscp_mark) + policy, dscp_mark=dscp_mark + ) @_utils.valid_kwargs("dscp_mark") - def update_qos_dscp_marking_rule(self, policy_name_or_id, rule_id, - **kwargs): + def update_qos_dscp_marking_rule( + self, policy_name_or_id, rule_id, **kwargs + ): """Update a QoS DSCP marking rule. :param string policy_name_or_id: Name or ID of the QoS policy to which @@ -1651,28 +1797,31 @@ def update_qos_dscp_marking_rule(self, policy_name_or_id, rule_id, """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') + 'QoS extension is not available on target cloud' + ) policy = self.network.find_qos_policy(policy_name_or_id) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) + name_or_id=policy_name_or_id + ) + ) if not kwargs: self.log.debug("No QoS DSCP marking rule data to update") return - curr_rule = self.network.get_qos_dscp_marking_rule( - rule_id, policy) + curr_rule = self.network.get_qos_dscp_marking_rule(rule_id, policy) if not curr_rule: raise exc.OpenStackCloudException( "QoS dscp_marking_rule {rule_id} not found in policy " - "{policy_id}".format(rule_id=rule_id, - policy_id=policy['id'])) + "{policy_id}".format(rule_id=rule_id, policy_id=policy['id']) + ) return self.network.update_qos_dscp_marking_rule( - curr_rule, policy, **kwargs) + curr_rule, policy, **kwargs + ) def delete_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): """Delete a QoS DSCP marking rule. @@ -1685,22 +1834,28 @@ def delete_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') + 'QoS extension is not available on target cloud' + ) policy = self.network.find_qos_policy(policy_name_or_id) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) + name_or_id=policy_name_or_id + ) + ) try: self.network.delete_qos_dscp_marking_rule( - rule_id, policy, ignore_missing=False) + rule_id, policy, ignore_missing=False + ) except exceptions.ResourceNotFound: self.log.debug( "QoS DSCP marking rule {rule_id} not found in policy " - "{policy_id}. Ignoring.".format(rule_id=rule_id, - policy_id=policy['id'])) + "{policy_id}. Ignoring.".format( + rule_id=rule_id, policy_id=policy['id'] + ) + ) return False return True @@ -1725,11 +1880,13 @@ def search_qos_minimum_bandwidth_rules( OpenStack API call. """ rules = self.list_qos_minimum_bandwidth_rules( - policy_name_or_id, filters) + policy_name_or_id, filters + ) return _utils._filter_list(rules, rule_id, filters) - def list_qos_minimum_bandwidth_rules(self, policy_name_or_id, - filters=None): + def list_qos_minimum_bandwidth_rules( + self, policy_name_or_id, filters=None + ): """List all available QoS minimum bandwidth rules. :param string policy_name_or_id: Name or ID of the QoS policy from @@ -1741,21 +1898,24 @@ def list_qos_minimum_bandwidth_rules(self, policy_name_or_id, """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') + 'QoS extension is not available on target cloud' + ) policy = self.network.find_qos_policy(policy_name_or_id) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) + name_or_id=policy_name_or_id + ) + ) # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} return list( - self.network.qos_minimum_bandwidth_rules( - policy, **filters)) + self.network.qos_minimum_bandwidth_rules(policy, **filters) + ) def get_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): """Get a QoS minimum bandwidth rule by name or ID. @@ -1768,13 +1928,16 @@ def get_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') + 'QoS extension is not available on target cloud' + ) policy = self.network.find_qos_policy(policy_name_or_id) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) + name_or_id=policy_name_or_id + ) + ) return self.network.get_qos_minimum_bandwidth_rule(rule_id, policy) @@ -1797,13 +1960,16 @@ def create_qos_minimum_bandwidth_rule( """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') + 'QoS extension is not available on target cloud' + ) policy = self.network.find_qos_policy(policy_name_or_id) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) + name_or_id=policy_name_or_id + ) + ) kwargs['min_kbps'] = min_kbps @@ -1826,28 +1992,33 @@ def update_qos_minimum_bandwidth_rule( """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') + 'QoS extension is not available on target cloud' + ) policy = self.network.find_qos_policy(policy_name_or_id) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) + name_or_id=policy_name_or_id + ) + ) if not kwargs: self.log.debug("No QoS minimum bandwidth rule data to update") return curr_rule = self.network.get_qos_minimum_bandwidth_rule( - rule_id, policy) + rule_id, policy + ) if not curr_rule: raise exc.OpenStackCloudException( "QoS minimum_bandwidth_rule {rule_id} not found in policy " - "{policy_id}".format(rule_id=rule_id, - policy_id=policy['id'])) + "{policy_id}".format(rule_id=rule_id, policy_id=policy['id']) + ) return self.network.update_qos_minimum_bandwidth_rule( - curr_rule, policy, **kwargs) + curr_rule, policy, **kwargs + ) def delete_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): """Delete a QoS minimum bandwidth rule. @@ -1860,22 +2031,28 @@ def delete_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( - 'QoS extension is not available on target cloud') + 'QoS extension is not available on target cloud' + ) policy = self.network.find_qos_policy(policy_name_or_id) if not policy: raise exc.OpenStackCloudResourceNotFound( "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id)) + name_or_id=policy_name_or_id + ) + ) try: self.network.delete_qos_minimum_bandwidth_rule( - rule_id, policy, ignore_missing=False) + rule_id, policy, ignore_missing=False + ) except exceptions.ResourceNotFound: self.log.debug( "QoS minimum bandwidth rule {rule_id} not found in policy " - "{policy_id}. Ignoring.".format(rule_id=rule_id, - policy_id=policy['id'])) + "{policy_id}. Ignoring.".format( + rule_id=rule_id, policy_id=policy['id'] + ) + ) return False return True @@ -1893,9 +2070,7 @@ def add_router_interface(self, router, subnet_id=None, port_id=None): :raises: OpenStackCloudException on operation error. """ return self.network.add_interface_to_router( - router=router, - subnet_id=subnet_id, - port_id=port_id + router=router, subnet_id=subnet_id, port_id=port_id ) def remove_router_interface(self, router, subnet_id=None, port_id=None): @@ -1917,12 +2092,11 @@ def remove_router_interface(self, router, subnet_id=None, port_id=None): """ if not subnet_id and not port_id: raise ValueError( - "At least one of subnet_id or port_id must be supplied.") + "At least one of subnet_id or port_id must be supplied." + ) self.network.remove_interface_from_router( - router=router, - subnet_id=subnet_id, - port_id=port_id + router=router, subnet_id=subnet_id, port_id=port_id ) def list_router_interfaces(self, router, interface_type=None): @@ -1938,17 +2112,31 @@ def list_router_interfaces(self, router, interface_type=None): ports = list(self.network.ports(device_id=router['id'])) router_interfaces = ( - [port for port in ports - if (port['device_owner'] in - ['network:router_interface', - 'network:router_interface_distributed', - 'network:ha_router_replicated_interface']) - ] if not interface_type or interface_type == 'internal' else []) + [ + port + for port in ports + if ( + port['device_owner'] + in [ + 'network:router_interface', + 'network:router_interface_distributed', + 'network:ha_router_replicated_interface', + ] + ) + ] + if not interface_type or interface_type == 'internal' + else [] + ) router_gateways = ( - [port for port in ports - if port['device_owner'] == 'network:router_gateway' - ] if not interface_type or interface_type == 'external' else []) + [ + port + for port in ports + if port['device_owner'] == 'network:router_gateway' + ] + if not interface_type or interface_type == 'external' + else [] + ) return router_interfaces + router_gateways @@ -1985,9 +2173,7 @@ def create_router( :returns: The created network ``Router`` object. :raises: OpenStackCloudException on operation error. """ - router = { - 'admin_state_up': admin_state_up - } + router = {'admin_state_up': admin_state_up} if project_id is not None: router['project_id'] = project_id if name: @@ -2000,18 +2186,27 @@ def create_router( if availability_zone_hints is not None: if not isinstance(availability_zone_hints, list): raise exc.OpenStackCloudException( - "Parameter 'availability_zone_hints' must be a list") + "Parameter 'availability_zone_hints' must be a list" + ) if not self._has_neutron_extension('router_availability_zone'): raise exc.OpenStackCloudUnavailableExtension( 'router_availability_zone extension is not available on ' - 'target cloud') + 'target cloud' + ) router['availability_zone_hints'] = availability_zone_hints return self.network.create_router(**router) - def update_router(self, name_or_id, name=None, admin_state_up=None, - ext_gateway_net_id=None, enable_snat=None, - ext_fixed_ips=None, routes=None): + def update_router( + self, + name_or_id, + name=None, + admin_state_up=None, + ext_gateway_net_id=None, + enable_snat=None, + ext_fixed_ips=None, + routes=None, + ): """Update an existing logical router. :param string name_or_id: The name or UUID of the router to update. @@ -2063,7 +2258,8 @@ def update_router(self, name_or_id, name=None, admin_state_up=None, router['routes'] = routes else: self.log.warning( - 'extra routes extension is not available on target cloud') + 'extra routes extension is not available on target cloud' + ) if not router: self.log.debug("No router data to update") @@ -2072,7 +2268,8 @@ def update_router(self, name_or_id, name=None, admin_state_up=None, curr_router = self.get_router(name_or_id) if not curr_router: raise exc.OpenStackCloudException( - "Router %s not found." % name_or_id) + "Router %s not found." % name_or_id + ) return self.network.update_router(curr_router, **router) @@ -2186,20 +2383,24 @@ def create_subnet( network = self.get_network(network_name_or_id, filters) if not network: raise exc.OpenStackCloudException( - "Network %s not found." % network_name_or_id) + "Network %s not found." % network_name_or_id + ) if disable_gateway_ip and gateway_ip: raise exc.OpenStackCloudException( - 'arg:disable_gateway_ip is not allowed with arg:gateway_ip') + 'arg:disable_gateway_ip is not allowed with arg:gateway_ip' + ) if not cidr and not use_default_subnetpool: raise exc.OpenStackCloudException( - 'arg:cidr is required when a subnetpool is not used') + 'arg:cidr is required when a subnetpool is not used' + ) if cidr and use_default_subnetpool: raise exc.OpenStackCloudException( 'arg:cidr must be set to None when use_default_subnetpool == ' - 'True') + 'True' + ) # Be friendly on ip_version and allow strings if isinstance(ip_version, str): @@ -2207,15 +2408,19 @@ def create_subnet( ip_version = int(ip_version) except ValueError: raise exc.OpenStackCloudException( - 'ip_version must be an integer') + 'ip_version must be an integer' + ) # The body of the neutron message for the subnet we wish to create. # This includes attributes that are required or have defaults. - subnet = dict({ - 'network_id': network['id'], - 'ip_version': ip_version, - 'enable_dhcp': enable_dhcp, - }, **kwargs) + subnet = dict( + { + 'network_id': network['id'], + 'ip_version': ip_version, + 'enable_dhcp': enable_dhcp, + }, + **kwargs, + ) # Add optional attributes to the message. if cidr: @@ -2267,10 +2472,17 @@ def delete_subnet(self, name_or_id): return True - def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, - gateway_ip=None, disable_gateway_ip=None, - allocation_pools=None, dns_nameservers=None, - host_routes=None): + def update_subnet( + self, + name_or_id, + subnet_name=None, + enable_dhcp=None, + gateway_ip=None, + disable_gateway_ip=None, + allocation_pools=None, + dns_nameservers=None, + host_routes=None, + ): """Update an existing subnet. :param string name_or_id: Name or ID of the subnet to update. @@ -2337,24 +2549,42 @@ def update_subnet(self, name_or_id, subnet_name=None, enable_dhcp=None, if disable_gateway_ip and gateway_ip: raise exc.OpenStackCloudException( - 'arg:disable_gateway_ip is not allowed with arg:gateway_ip') + 'arg:disable_gateway_ip is not allowed with arg:gateway_ip' + ) curr_subnet = self.get_subnet(name_or_id) if not curr_subnet: raise exc.OpenStackCloudException( - "Subnet %s not found." % name_or_id) + "Subnet %s not found." % name_or_id + ) return self.network.update_subnet(curr_subnet, **subnet) - @_utils.valid_kwargs('name', 'admin_state_up', 'mac_address', 'fixed_ips', - 'subnet_id', 'ip_address', 'security_groups', - 'allowed_address_pairs', 'extra_dhcp_opts', - 'device_owner', 'device_id', 'binding:vnic_type', - 'binding:profile', 'port_security_enabled', - 'qos_policy_id', 'binding:host_id', 'project_id', - 'description', 'dns_domain', 'dns_name', - 'numa_affinity_policy', 'propagate_uplink_status', - 'mac_learning_enabled') + @_utils.valid_kwargs( + 'name', + 'admin_state_up', + 'mac_address', + 'fixed_ips', + 'subnet_id', + 'ip_address', + 'security_groups', + 'allowed_address_pairs', + 'extra_dhcp_opts', + 'device_owner', + 'device_id', + 'binding:vnic_type', + 'binding:profile', + 'port_security_enabled', + 'qos_policy_id', + 'binding:host_id', + 'project_id', + 'description', + 'dns_domain', + 'dns_name', + 'numa_affinity_policy', + 'propagate_uplink_status', + 'mac_learning_enabled', + ) def create_port(self, network_id, **kwargs): """Create a port @@ -2427,12 +2657,21 @@ def create_port(self, network_id, **kwargs): return self.network.create_port(**kwargs) - @_utils.valid_kwargs('name', 'admin_state_up', 'fixed_ips', - 'security_groups', 'allowed_address_pairs', - 'extra_dhcp_opts', 'device_owner', 'device_id', - 'binding:vnic_type', 'binding:profile', - 'port_security_enabled', 'qos_policy_id', - 'binding:host_id') + @_utils.valid_kwargs( + 'name', + 'admin_state_up', + 'fixed_ips', + 'security_groups', + 'allowed_address_pairs', + 'extra_dhcp_opts', + 'device_owner', + 'device_id', + 'binding:vnic_type', + 'binding:profile', + 'port_security_enabled', + 'qos_policy_id', + 'binding:host_id', + ) def update_port(self, name_or_id, **kwargs): """Update a port @@ -2491,7 +2730,8 @@ def update_port(self, name_or_id, **kwargs): port = self.get_port(name_or_id=name_or_id) if port is None: raise exc.OpenStackCloudException( - "failed to find port '{port}'".format(port=name_or_id)) + "failed to find port '{port}'".format(port=name_or_id) + ) return self.network.update_port(port, **kwargs) @@ -2531,12 +2771,14 @@ def _get_port_ids(self, name_or_id_list, filters=None): port = self.get_port(name_or_id, filters) if not port: raise exceptions.ResourceNotFound( - 'Port {id} not found'.format(id=name_or_id)) + 'Port {id} not found'.format(id=name_or_id) + ) ids_list.append(port['id']) return ids_list - def _build_external_gateway_info(self, ext_gateway_net_id, enable_snat, - ext_fixed_ips): + def _build_external_gateway_info( + self, ext_gateway_net_id, enable_snat, ext_fixed_ips + ): info = {} if ext_gateway_net_id: info['network_id'] = ext_gateway_net_id diff --git a/openstack/cloud/_network_common.py b/openstack/cloud/_network_common.py index ed6e4b969..aa774345e 100644 --- a/openstack/cloud/_network_common.py +++ b/openstack/cloud/_network_common.py @@ -20,8 +20,8 @@ class NetworkCommonCloudMixin: - """Shared networking functions used by FloatingIP, Network, Compute classes - """ + """Shared networking functions used by FloatingIP, Network, Compute + classes.""" def __init__(self): self._external_ipv4_names = self.config.get_external_ipv4_networks() @@ -33,9 +33,11 @@ def __init__(self): self._default_network = self.config.get_default_network() self._use_external_network = self.config.config.get( - 'use_external_network', True) + 'use_external_network', True + ) self._use_internal_network = self.config.config.get( - 'use_internal_network', True) + 'use_internal_network', True + ) self._networks_lock = threading.Lock() self._reset_network_caches() @@ -90,46 +92,63 @@ def _set_interesting_networks(self): for network in all_networks: # External IPv4 networks - if (network['name'] in self._external_ipv4_names - or network['id'] in self._external_ipv4_names): + if ( + network['name'] in self._external_ipv4_names + or network['id'] in self._external_ipv4_names + ): external_ipv4_networks.append(network) - elif ((network.is_router_external - or network.provider_physical_network) - and network['name'] not in self._internal_ipv4_names - and network['id'] not in self._internal_ipv4_names): + elif ( + ( + network.is_router_external + or network.provider_physical_network + ) + and network['name'] not in self._internal_ipv4_names + and network['id'] not in self._internal_ipv4_names + ): external_ipv4_networks.append(network) # Internal networks - if (network['name'] in self._internal_ipv4_names - or network['id'] in self._internal_ipv4_names): + if ( + network['name'] in self._internal_ipv4_names + or network['id'] in self._internal_ipv4_names + ): internal_ipv4_networks.append(network) - elif (not network.is_router_external - and not network.provider_physical_network - and network['name'] not in self._external_ipv4_names - and network['id'] not in self._external_ipv4_names): + elif ( + not network.is_router_external + and not network.provider_physical_network + and network['name'] not in self._external_ipv4_names + and network['id'] not in self._external_ipv4_names + ): internal_ipv4_networks.append(network) # External networks - if (network['name'] in self._external_ipv6_names - or network['id'] in self._external_ipv6_names): + if ( + network['name'] in self._external_ipv6_names + or network['id'] in self._external_ipv6_names + ): external_ipv6_networks.append(network) - elif (network.is_router_external - and network['name'] not in self._internal_ipv6_names - and network['id'] not in self._internal_ipv6_names): + elif ( + network.is_router_external + and network['name'] not in self._internal_ipv6_names + and network['id'] not in self._internal_ipv6_names + ): external_ipv6_networks.append(network) # Internal networks - if (network['name'] in self._internal_ipv6_names - or network['id'] in self._internal_ipv6_names): + if ( + network['name'] in self._internal_ipv6_names + or network['id'] in self._internal_ipv6_names + ): internal_ipv6_networks.append(network) - elif (not network.is_router_external - and network['name'] not in self._external_ipv6_names - and network['id'] not in self._external_ipv6_names): + elif ( + not network.is_router_external + and network['name'] not in self._external_ipv6_names + and network['id'] not in self._external_ipv6_names + ): internal_ipv6_networks.append(network) # External Floating IPv4 networks - if self._nat_source in ( - network['name'], network['id']): + if self._nat_source in (network['name'], network['id']): if nat_source: raise exc.OpenStackCloudException( 'Multiple networks were found matching' @@ -137,8 +156,8 @@ def _set_interesting_networks(self): ' to be the NAT source. Please check your' ' cloud resources. It is probably a good idea' ' to configure this network by ID rather than' - ' by name.'.format( - nat_net=self._nat_source)) + ' by name.'.format(nat_net=self._nat_source) + ) external_ipv4_floating_networks.append(network) nat_source = network elif self._nat_source is None: @@ -147,8 +166,7 @@ def _set_interesting_networks(self): nat_source = nat_source or network # NAT Destination - if self._nat_destination in ( - network['name'], network['id']): + if self._nat_destination in (network['name'], network['id']): if nat_destination: raise exc.OpenStackCloudException( 'Multiple networks were found matching' @@ -156,8 +174,8 @@ def _set_interesting_networks(self): ' to be the NAT destination. Please check your' ' cloud resources. It is probably a good idea' ' to configure this network by ID rather than' - ' by name.'.format( - nat_net=self._nat_destination)) + ' by name.'.format(nat_net=self._nat_destination) + ) nat_destination = network elif self._nat_destination is None: # TODO(mordred) need a config value for floating @@ -174,14 +192,16 @@ def _set_interesting_networks(self): for subnet in all_subnets: # TODO(mordred) trap for detecting more than # one network with a gateway_ip without a config - if ('gateway_ip' in subnet and subnet['gateway_ip'] - and network['id'] == subnet['network_id']): + if ( + 'gateway_ip' in subnet + and subnet['gateway_ip'] + and network['id'] == subnet['network_id'] + ): nat_destination = network break # Default network - if self._default_network in ( - network['name'], network['id']): + if self._default_network in (network['name'], network['id']): if default_network: raise exc.OpenStackCloudException( 'Multiple networks were found matching' @@ -190,8 +210,8 @@ def _set_interesting_networks(self): ' network. Please check your cloud resources.' ' It is probably a good idea' ' to configure this network by ID rather than' - ' by name.'.format( - default_net=self._default_network)) + ' by name.'.format(default_net=self._default_network) + ) default_network = network # Validate config vs. reality @@ -200,49 +220,57 @@ def _set_interesting_networks(self): raise exc.OpenStackCloudException( "Networks: {network} was provided for external IPv4" " access and those networks could not be found".format( - network=net_name)) + network=net_name + ) + ) for net_name in self._internal_ipv4_names: if net_name not in [net['name'] for net in internal_ipv4_networks]: raise exc.OpenStackCloudException( "Networks: {network} was provided for internal IPv4" " access and those networks could not be found".format( - network=net_name)) + network=net_name + ) + ) for net_name in self._external_ipv6_names: if net_name not in [net['name'] for net in external_ipv6_networks]: raise exc.OpenStackCloudException( "Networks: {network} was provided for external IPv6" " access and those networks could not be found".format( - network=net_name)) + network=net_name + ) + ) for net_name in self._internal_ipv6_names: if net_name not in [net['name'] for net in internal_ipv6_networks]: raise exc.OpenStackCloudException( "Networks: {network} was provided for internal IPv6" " access and those networks could not be found".format( - network=net_name)) + network=net_name + ) + ) if self._nat_destination and not nat_destination: raise exc.OpenStackCloudException( 'Network {network} was configured to be the' ' destination for inbound NAT but it could not be' - ' found'.format( - network=self._nat_destination)) + ' found'.format(network=self._nat_destination) + ) if self._nat_source and not nat_source: raise exc.OpenStackCloudException( 'Network {network} was configured to be the' ' source for inbound NAT but it could not be' - ' found'.format( - network=self._nat_source)) + ' found'.format(network=self._nat_source) + ) if self._default_network and not default_network: raise exc.OpenStackCloudException( 'Network {network} was configured to be the' ' default network interface but it could not be' - ' found'.format( - network=self._default_network)) + ' found'.format(network=self._default_network) + ) self._external_ipv4_networks = external_ipv4_networks self._external_ipv4_floating_networks = external_ipv4_floating_networks @@ -304,9 +332,8 @@ def get_external_networks(self): :returns: A list of network ``Network`` objects if any are found """ self._find_interesting_networks() - return ( - list(self._external_ipv4_networks) - + list(self._external_ipv6_networks) + return list(self._external_ipv4_networks) + list( + self._external_ipv6_networks ) def get_internal_networks(self): @@ -318,9 +345,8 @@ def get_internal_networks(self): :returns: A list of network ``Network`` objects if any are found """ self._find_interesting_networks() - return ( - list(self._internal_ipv4_networks) - + list(self._internal_ipv6_networks) + return list(self._internal_ipv4_networks) + list( + self._internal_ipv6_networks ) def get_external_ipv4_networks(self): diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index d4a392af1..945d4e00f 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -105,9 +105,7 @@ def create_container(self, name, public=False): container = self.get_container(name) if container: return container - attrs = dict( - name=name - ) + attrs = dict(name=name) if public: attrs['read_ACL'] = OBJECT_CONTAINER_ACLS['public'] container = self.object_store.create_container(**attrs) @@ -129,7 +127,9 @@ def delete_container(self, name): 'Attempt to delete container {container} failed. The' ' container is not empty. Please delete the objects' ' inside it before deleting the container'.format( - container=name)) + container=name + ) + ) def update_container(self, name, headers): """Update the metadata in a container. @@ -138,7 +138,8 @@ def update_container(self, name, headers): :param dict headers: Key/Value headers to set on the container. """ self.object_store.set_container_metadata( - name, refresh=False, **headers) + name, refresh=False, **headers + ) def set_container_access(self, name, access, refresh=False): """Set the access control list on a container. @@ -152,11 +153,10 @@ def set_container_access(self, name, access, refresh=False): if access not in OBJECT_CONTAINER_ACLS: raise exc.OpenStackCloudException( "Invalid container access specified: %s. Must be one of %s" - % (access, list(OBJECT_CONTAINER_ACLS.keys()))) + % (access, list(OBJECT_CONTAINER_ACLS.keys())) + ) return self.object_store.set_container_metadata( - name, - read_ACL=OBJECT_CONTAINER_ACLS[access], - refresh=refresh + name, read_ACL=OBJECT_CONTAINER_ACLS[access], refresh=refresh ) def get_container_access(self, name): @@ -179,7 +179,8 @@ def get_container_access(self, name): if str(acl) == str(value): return key raise exc.OpenStackCloudException( - "Could not determine container access for ACL: %s." % acl) + "Could not determine container access for ACL: %s." % acl + ) @_utils.cache_on_arguments() def get_object_capabilities(self): @@ -201,7 +202,8 @@ def get_object_segment_size(self, segment_size): return self.object_store.get_object_segment_size(segment_size) def is_object_stale( - self, container, name, filename, file_md5=None, file_sha256=None): + self, container, name, filename, file_md5=None, file_sha256=None + ): """Check to see if an object matches the hashes of a file. :param container: Name of the container. @@ -213,8 +215,11 @@ def is_object_stale( Defaults to None which means calculate locally. """ return self.object_store.is_object_stale( - container, name, filename, - file_md5=file_md5, file_sha256=file_sha256 + container, + name, + filename, + file_md5=file_md5, + file_sha256=file_sha256, ) def create_directory_marker_object(self, container, name, **headers): @@ -241,11 +246,8 @@ def create_directory_marker_object(self, container, name, **headers): headers['content-type'] = 'application/directory' return self.create_object( - container, - name, - data='', - generate_checksums=False, - **headers) + container, name, data='', generate_checksums=False, **headers + ) def create_object( self, @@ -295,12 +297,16 @@ def create_object( :raises: ``OpenStackCloudException`` on operation error. """ return self.object_store.create_object( - container, name, - filename=filename, data=data, - md5=md5, sha256=sha256, use_slo=use_slo, + container, + name, + filename=filename, + data=data, + md5=md5, + sha256=sha256, + use_slo=use_slo, generate_checksums=generate_checksums, metadata=metadata, - **headers + **headers, ) def update_object(self, container, name, metadata=None, **headers): @@ -317,8 +323,7 @@ def update_object(self, container, name, metadata=None, **headers): """ meta = metadata.copy() or {} meta.update(**headers) - self.object_store.set_object_metadata( - name, container, **meta) + self.object_store.set_object_metadata(name, container, **meta) def list_objects(self, container, full_listing=True, prefix=None): """List objects. @@ -330,10 +335,9 @@ def list_objects(self, container, full_listing=True, prefix=None): :returns: A list of object store ``Object`` objects. :raises: OpenStackCloudException on operation error. """ - return list(self.object_store.objects( - container=container, - prefix=prefix - )) + return list( + self.object_store.objects(container=container, prefix=prefix) + ) def search_objects(self, container, name=None, filters=None): """Search objects. @@ -364,7 +368,9 @@ def delete_object(self, container, name, meta=None): """ try: self.object_store.delete_object( - name, ignore_missing=False, container=container, + name, + ignore_missing=False, + container=container, ) return True except exceptions.SDKException: @@ -400,9 +406,7 @@ def get_object_metadata(self, container, name): :param name: :returns: The object metadata. """ - return self.object_store.get_object_metadata( - name, container - ).metadata + return self.object_store.get_object_metadata(name, container).metadata def get_object_raw(self, container, obj, query_string=None, stream=False): """Get a raw response object for an object. @@ -422,12 +426,12 @@ def _get_object_endpoint(self, container, obj=None, query_string=None): endpoint = urllib.parse.quote(container) if obj: endpoint = '{endpoint}/{object}'.format( - endpoint=endpoint, - object=urllib.parse.quote(obj) + endpoint=endpoint, object=urllib.parse.quote(obj) ) if query_string: endpoint = '{endpoint}?{query_string}'.format( - endpoint=endpoint, query_string=query_string) + endpoint=endpoint, query_string=query_string + ) return endpoint def stream_object( @@ -451,13 +455,21 @@ def stream_object( """ try: for ret in self.object_store.stream_object( - obj, container, chunk_size=resp_chunk_size): + obj, container, chunk_size=resp_chunk_size + ): yield ret except exceptions.ResourceNotFound: return - def get_object(self, container, obj, query_string=None, - resp_chunk_size=1024, outfile=None, stream=False): + def get_object( + self, + container, + obj, + query_string=None, + resp_chunk_size=1024, + outfile=None, + stream=False, + ): """Get the headers and body of an object :param string container: Name of the container. @@ -477,13 +489,13 @@ def get_object(self, container, obj, query_string=None, """ try: obj = self.object_store.get_object( - obj, container=container, + obj, + container=container, resp_chunk_size=resp_chunk_size, outfile=outfile, - remember_content=(outfile is None) + remember_content=(outfile is None), ) - headers = { - k.lower(): v for k, v in obj._last_headers.items()} + headers = {k.lower(): v for k, v in obj._last_headers.items()} return (headers, obj.data) except exceptions.ResourceNotFound: @@ -500,10 +512,13 @@ def _wait_for_futures(self, futures, raise_on_error=True): result = completed.result() exceptions.raise_from_response(result) results.append(result) - except (keystoneauth1.exceptions.RetriableConnectionFailure, - exceptions.HttpException) as e: + except ( + keystoneauth1.exceptions.RetriableConnectionFailure, + exceptions.HttpException, + ) as e: error_text = "Exception processing async task: {}".format( - str(e)) + str(e) + ) if raise_on_error: self.log.exception(error_text) raise diff --git a/openstack/cloud/_orchestration.py b/openstack/cloud/_orchestration.py index 0aaa27cdd..451f1ab77 100644 --- a/openstack/cloud/_orchestration.py +++ b/openstack/cloud/_orchestration.py @@ -41,20 +41,33 @@ def _orchestration_client(self): return self._raw_clients['orchestration'] def get_template_contents( - self, template_file=None, template_url=None, - template_object=None, files=None): + self, + template_file=None, + template_url=None, + template_object=None, + files=None, + ): return self.orchestration.get_template_contents( - template_file=template_file, template_url=template_url, - template_object=template_object, files=files) + template_file=template_file, + template_url=template_url, + template_object=template_object, + files=files, + ) def create_stack( - self, name, tags=None, - template_file=None, template_url=None, - template_object=None, files=None, - rollback=True, - wait=False, timeout=3600, - environment_files=None, - **parameters): + self, + name, + tags=None, + template_file=None, + template_url=None, + template_object=None, + files=None, + rollback=True, + wait=False, + timeout=3600, + environment_files=None, + **parameters + ): """Create a stack. :param string name: Name of the stack. @@ -83,27 +96,36 @@ def create_stack( tags=tags, is_rollback_disabled=not rollback, timeout_mins=timeout // 60, - parameters=parameters + parameters=parameters, + ) + params.update( + self.orchestration.read_env_and_templates( + template_file=template_file, + template_url=template_url, + template_object=template_object, + files=files, + environment_files=environment_files, + ) ) - params.update(self.orchestration.read_env_and_templates( - template_file=template_file, template_url=template_url, - template_object=template_object, files=files, - environment_files=environment_files - )) self.orchestration.create_stack(name=name, **params) if wait: - event_utils.poll_for_events(self, stack_name=name, - action='CREATE') + event_utils.poll_for_events(self, stack_name=name, action='CREATE') return self.get_stack(name) def update_stack( - self, name_or_id, - template_file=None, template_url=None, - template_object=None, files=None, - rollback=True, tags=None, - wait=False, timeout=3600, - environment_files=None, - **parameters): + self, + name_or_id, + template_file=None, + template_url=None, + template_object=None, + files=None, + rollback=True, + tags=None, + wait=False, + timeout=3600, + environment_files=None, + **parameters + ): """Update a stack. :param string name_or_id: Name or ID of the stack to update. @@ -131,27 +153,31 @@ def update_stack( tags=tags, is_rollback_disabled=not rollback, timeout_mins=timeout // 60, - parameters=parameters + parameters=parameters, + ) + params.update( + self.orchestration.read_env_and_templates( + template_file=template_file, + template_url=template_url, + template_object=template_object, + files=files, + environment_files=environment_files, + ) ) - params.update(self.orchestration.read_env_and_templates( - template_file=template_file, template_url=template_url, - template_object=template_object, files=files, - environment_files=environment_files - )) if wait: # find the last event to use as the marker events = event_utils.get_events( - self, name_or_id, event_args={'sort_dir': 'desc', 'limit': 1}) + self, name_or_id, event_args={'sort_dir': 'desc', 'limit': 1} + ) marker = events[0].id if events else None # Not to cause update of ID field pass stack as dict self.orchestration.update_stack(stack={'id': name_or_id}, **params) if wait: - event_utils.poll_for_events(self, - name_or_id, - action='UPDATE', - marker=marker) + event_utils.poll_for_events( + self, name_or_id, action='UPDATE', marker=marker + ) return self.get_stack(name_or_id) def delete_stack(self, name_or_id, wait=False): @@ -173,24 +199,26 @@ def delete_stack(self, name_or_id, wait=False): if wait: # find the last event to use as the marker events = event_utils.get_events( - self, name_or_id, event_args={'sort_dir': 'desc', 'limit': 1}) + self, name_or_id, event_args={'sort_dir': 'desc', 'limit': 1} + ) marker = events[0].id if events else None self.orchestration.delete_stack(stack) if wait: try: - event_utils.poll_for_events(self, - stack_name=name_or_id, - action='DELETE', - marker=marker) + event_utils.poll_for_events( + self, stack_name=name_or_id, action='DELETE', marker=marker + ) except exc.OpenStackCloudHTTPError: pass stack = self.get_stack(name_or_id, resolve_outputs=False) if stack and stack['stack_status'] == 'DELETE_FAILED': raise exc.OpenStackCloudException( "Failed to delete stack {id}: {reason}".format( - id=name_or_id, reason=stack['stack_status_reason'])) + id=name_or_id, reason=stack['stack_status_reason'] + ) + ) return True @@ -246,12 +274,12 @@ def _search_one_stack(name_or_id=None, filters=None): stack = self.orchestration.find_stack( name_or_id, ignore_missing=False, - resolve_outputs=resolve_outputs) + resolve_outputs=resolve_outputs, + ) if stack.status == 'DELETE_COMPLETE': return [] except exc.OpenStackCloudURINotFound: return [] return _utils._filter_list([stack], name_or_id, filters) - return _utils._get_entity( - self, _search_one_stack, name_or_id, filters) + return _utils._get_entity(self, _search_one_stack, name_or_id, filters) diff --git a/openstack/cloud/_security_group.py b/openstack/cloud/_security_group.py index b0bf33185..08d56e90c 100644 --- a/openstack/cloud/_security_group.py +++ b/openstack/cloud/_security_group.py @@ -59,15 +59,16 @@ def list_security_groups(self, filters=None): if self._use_neutron_secgroups(): # pass filters dict to the list to filter as much as possible on # the server side - return list( - self.network.security_groups(**filters)) + return list(self.network.security_groups(**filters)) # Handle nova security groups else: - data = proxy._json_response(self.compute.get( - '/os-security-groups', params=filters)) + data = proxy._json_response( + self.compute.get('/os-security-groups', params=filters) + ) return self._normalize_secgroups( - self._get_and_munchify('security_groups', data)) + self._get_and_munchify('security_groups', data) + ) def get_security_group(self, name_or_id, filters=None): """Get a security group by name or ID. @@ -93,11 +94,10 @@ def get_security_group(self, name_or_id, filters=None): or None if no matching security group is found. """ - return _utils._get_entity( - self, 'security_group', name_or_id, filters) + return _utils._get_entity(self, 'security_group', name_or_id, filters) def get_security_group_by_id(self, id): - """ Get a security group by ID + """Get a security group by ID :param id: ID of the security group. :returns: A security group @@ -107,20 +107,23 @@ def get_security_group_by_id(self, id): raise exc.OpenStackCloudUnavailableFeature( "Unavailable feature: security groups" ) - error_message = ("Error getting security group with" - " ID {id}".format(id=id)) + error_message = "Error getting security group with" " ID {id}".format( + id=id + ) if self._use_neutron_secgroups(): return self.network.get_security_group(id) else: data = proxy._json_response( - self.compute.get( - '/os-security-groups/{id}'.format(id=id)), - error_message=error_message) + self.compute.get('/os-security-groups/{id}'.format(id=id)), + error_message=error_message, + ) return self._normalize_secgroup( - self._get_and_munchify('security_group', data)) + self._get_and_munchify('security_group', data) + ) - def create_security_group(self, name, description, - project_id=None, stateful=None): + def create_security_group( + self, name, description, project_id=None, stateful=None + ): """Create a new security group :param string name: A name for the security group. @@ -145,22 +148,23 @@ def create_security_group(self, name, description, ) data = [] - security_group_json = { - 'name': name, 'description': description - } + security_group_json = {'name': name, 'description': description} if stateful is not None: security_group_json['stateful'] = stateful if project_id is not None: security_group_json['tenant_id'] = project_id if self._use_neutron_secgroups(): - return self.network.create_security_group( - **security_group_json) + return self.network.create_security_group(**security_group_json) else: - data = proxy._json_response(self.compute.post( - '/os-security-groups', - json={'security_group': security_group_json})) + data = proxy._json_response( + self.compute.post( + '/os-security-groups', + json={'security_group': security_group_json}, + ) + ) return self._normalize_secgroup( - self._get_and_munchify('security_group', data)) + self._get_and_munchify('security_group', data) + ) def delete_security_group(self, name_or_id): """Delete a security group @@ -183,18 +187,23 @@ def delete_security_group(self, name_or_id): # the delete. secgroup = self.get_security_group(name_or_id) if secgroup is None: - self.log.debug('Security group %s not found for deleting', - name_or_id) + self.log.debug( + 'Security group %s not found for deleting', name_or_id + ) return False if self._use_neutron_secgroups(): self.network.delete_security_group( - secgroup['id'], ignore_missing=False) + secgroup['id'], ignore_missing=False + ) return True else: - proxy._json_response(self.compute.delete( - '/os-security-groups/{id}'.format(id=secgroup['id']))) + proxy._json_response( + self.compute.delete( + '/os-security-groups/{id}'.format(id=secgroup['id']) + ) + ) return True @_utils.valid_kwargs('name', 'description', 'stateful') @@ -220,35 +229,38 @@ def update_security_group(self, name_or_id, **kwargs): if group is None: raise exc.OpenStackCloudException( - "Security group %s not found." % name_or_id) + "Security group %s not found." % name_or_id + ) if self._use_neutron_secgroups(): - return self.network.update_security_group( - group['id'], - **kwargs - ) + return self.network.update_security_group(group['id'], **kwargs) else: for key in ('name', 'description'): kwargs.setdefault(key, group[key]) data = proxy._json_response( self.compute.put( '/os-security-groups/{id}'.format(id=group['id']), - json={'security_group': kwargs})) + json={'security_group': kwargs}, + ) + ) return self._normalize_secgroup( - self._get_and_munchify('security_group', data)) - - def create_security_group_rule(self, - secgroup_name_or_id, - port_range_min=None, - port_range_max=None, - protocol=None, - remote_ip_prefix=None, - remote_group_id=None, - remote_address_group_id=None, - direction='ingress', - ethertype='IPv4', - project_id=None, - description=None): + self._get_and_munchify('security_group', data) + ) + + def create_security_group_rule( + self, + secgroup_name_or_id, + port_range_min=None, + port_range_max=None, + protocol=None, + remote_ip_prefix=None, + remote_group_id=None, + remote_address_group_id=None, + direction='ingress', + ethertype='IPv4', + project_id=None, + description=None, + ): """Create a new security group rule :param string secgroup_name_or_id: @@ -308,31 +320,32 @@ def create_security_group_rule(self, secgroup = self.get_security_group(secgroup_name_or_id) if not secgroup: raise exc.OpenStackCloudException( - "Security group %s not found." % secgroup_name_or_id) + "Security group %s not found." % secgroup_name_or_id + ) if self._use_neutron_secgroups(): # NOTE: Nova accepts -1 port numbers, but Neutron accepts None # as the equivalent value. rule_def = { 'security_group_id': secgroup['id'], - 'port_range_min': - None if port_range_min == -1 else port_range_min, - 'port_range_max': - None if port_range_max == -1 else port_range_max, + 'port_range_min': None + if port_range_min == -1 + else port_range_min, + 'port_range_max': None + if port_range_max == -1 + else port_range_max, 'protocol': protocol, 'remote_ip_prefix': remote_ip_prefix, 'remote_group_id': remote_group_id, 'remote_address_group_id': remote_address_group_id, 'direction': direction, - 'ethertype': ethertype + 'ethertype': ethertype, } if project_id is not None: rule_def['tenant_id'] = project_id if description is not None: rule_def["description"] = description - return self.network.create_security_group_rule( - **rule_def - ) + return self.network.create_security_group_rule(**rule_def) else: # NOTE: Neutron accepts None for protocol. Nova does not. if protocol is None: @@ -343,7 +356,8 @@ def create_security_group_rule(self, 'Rule creation failed: Nova does not support egress rules' ) raise exc.OpenStackCloudException( - 'No support for egress rules') + 'No support for egress rules' + ) # NOTE: Neutron accepts None for ports, but Nova requires -1 # as the equivalent value for ICMP. @@ -363,24 +377,28 @@ def create_security_group_rule(self, port_range_min = 1 port_range_max = 65535 - security_group_rule_dict = dict(security_group_rule=dict( - parent_group_id=secgroup['id'], - ip_protocol=protocol, - from_port=port_range_min, - to_port=port_range_max, - cidr=remote_ip_prefix, - group_id=remote_group_id - )) + security_group_rule_dict = dict( + security_group_rule=dict( + parent_group_id=secgroup['id'], + ip_protocol=protocol, + from_port=port_range_min, + to_port=port_range_max, + cidr=remote_ip_prefix, + group_id=remote_group_id, + ) + ) if project_id is not None: - security_group_rule_dict[ - 'security_group_rule']['tenant_id'] = project_id + security_group_rule_dict['security_group_rule'][ + 'tenant_id' + ] = project_id data = proxy._json_response( self.compute.post( - '/os-security-group-rules', - json=security_group_rule_dict - )) + '/os-security-group-rules', json=security_group_rule_dict + ) + ) return self._normalize_secgroup_rule( - self._get_and_munchify('security_group_rule', data)) + self._get_and_munchify('security_group_rule', data) + ) def delete_security_group_rule(self, rule_id): """Delete a security group rule @@ -401,8 +419,7 @@ def delete_security_group_rule(self, rule_id): if self._use_neutron_secgroups(): self.network.delete_security_group_rule( - rule_id, - ignore_missing=False + rule_id, ignore_missing=False ) return True @@ -410,7 +427,9 @@ def delete_security_group_rule(self, rule_id): try: exceptions.raise_from_response( self.compute.delete( - '/os-security-group-rules/{id}'.format(id=rule_id))) + '/os-security-group-rules/{id}'.format(id=rule_id) + ) + ) except exc.OpenStackCloudResourceNotFound: return False @@ -423,8 +442,9 @@ def _has_secgroups(self): return self.secgroup_source.lower() in ('nova', 'neutron') def _use_neutron_secgroups(self): - return (self.has_service('network') - and self.secgroup_source == 'neutron') + return ( + self.has_service('network') and self.secgroup_source == 'neutron' + ) def _normalize_secgroups(self, groups): """Normalize the structure of security groups @@ -454,7 +474,8 @@ def _normalize_secgroup(self, group): self._remove_novaclient_artifacts(group) rules = self._normalize_secgroup_rules( - group.pop('security_group_rules', group.pop('rules', []))) + group.pop('security_group_rules', group.pop('rules', [])) + ) project_id = group.pop('tenant_id', '') project_id = group.pop('project_id', project_id) @@ -506,14 +527,14 @@ def _normalize_secgroup_rule(self, rule): ret['direction'] = rule.pop('direction', 'ingress') ret['ethertype'] = rule.pop('ethertype', 'IPv4') port_range_min = rule.get( - 'port_range_min', rule.pop('from_port', None)) + 'port_range_min', rule.pop('from_port', None) + ) if port_range_min == -1: port_range_min = None if port_range_min is not None: port_range_min = int(port_range_min) ret['port_range_min'] = port_range_min - port_range_max = rule.pop( - 'port_range_max', rule.pop('to_port', None)) + port_range_max = rule.pop('port_range_max', rule.pop('to_port', None)) if port_range_max == -1: port_range_max = None if port_range_min is not None: @@ -521,9 +542,11 @@ def _normalize_secgroup_rule(self, rule): ret['port_range_max'] = port_range_max ret['protocol'] = rule.pop('protocol', rule.pop('ip_protocol', None)) ret['remote_ip_prefix'] = rule.pop( - 'remote_ip_prefix', rule.pop('ip_range', {}).get('cidr', None)) + 'remote_ip_prefix', rule.pop('ip_range', {}).get('cidr', None) + ) ret['security_group_id'] = rule.pop( - 'security_group_id', rule.pop('parent_group_id', None)) + 'security_group_id', rule.pop('parent_group_id', None) + ) ret['remote_group_id'] = rule.pop('remote_group_id', None) project_id = rule.pop('tenant_id', '') project_id = rule.pop('project_id', project_id) diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index 50bbb6d3e..77f0600f7 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -102,8 +102,9 @@ def _filter_list(data, name_or_id, filters): e_id = _make_unicode(e.get('id', None)) e_name = _make_unicode(e.get('name', None)) - if ((e_id and e_id == name_or_id) - or (e_name and e_name == name_or_id)): + if (e_id and e_id == name_or_id) or ( + e_name and e_name == name_or_id + ): identifier_matches.append(e) else: # Only try fnmatch if we don't match exactly @@ -112,8 +113,9 @@ def _filter_list(data, name_or_id, filters): # so that we log the bad pattern bad_pattern = True continue - if ((e_id and fn_reg.match(e_id)) - or (e_name and fn_reg.match(e_name))): + if (e_id and fn_reg.match(e_id)) or ( + e_name and fn_reg.match(e_name) + ): identifier_matches.append(e) if not identifier_matches and bad_pattern: log.debug("Bad pattern passed to fnmatch", exc_info=True) @@ -172,8 +174,9 @@ def _get_entity(cloud, resource, name_or_id, filters, **kwargs): # an additional call, it's simple enough to test to see if we got an # object and just short-circuit return it. - if (hasattr(name_or_id, 'id') - or (isinstance(name_or_id, dict) and 'id' in name_or_id)): + if hasattr(name_or_id, 'id') or ( + isinstance(name_or_id, dict) and 'id' in name_or_id + ): return name_or_id # If a uuid is passed short-circuit it calling the @@ -183,14 +186,18 @@ def _get_entity(cloud, resource, name_or_id, filters, **kwargs): if get_resource: return get_resource(name_or_id) - search = resource if callable(resource) else getattr( - cloud, 'search_%ss' % resource, None) + search = ( + resource + if callable(resource) + else getattr(cloud, 'search_%ss' % resource, None) + ) if search: entities = search(name_or_id, filters, **kwargs) if entities: if len(entities) > 1: raise exc.OpenStackCloudException( - "Multiple matches found for %s" % name_or_id) + "Multiple matches found for %s" % name_or_id + ) return entities[0] return None @@ -230,8 +237,10 @@ def func_wrapper(func, *args, **kwargs): if k not in argspec.args[1:] and k not in valid_args: raise TypeError( "{f}() got an unexpected keyword argument " - "'{arg}'".format(f=inspect.stack()[1][3], arg=k)) + "'{arg}'".format(f=inspect.stack()[1][3], arg=k) + ) return func(*args, **kwargs) + return func_wrapper @@ -244,6 +253,7 @@ def _func_wrap(f): @functools.wraps(f) def inner(*args, **kwargs): return f(*args, **kwargs) + return inner @@ -253,20 +263,23 @@ def cache_on_arguments(*cache_on_args, **cache_on_kwargs): def _inner_cache_on_arguments(func): def _cache_decorator(obj, *args, **kwargs): the_method = obj._get_cache(_cache_name).cache_on_arguments( - *cache_on_args, **cache_on_kwargs)( - _func_wrap(func.__get__(obj, type(obj)))) + *cache_on_args, **cache_on_kwargs + )(_func_wrap(func.__get__(obj, type(obj)))) return the_method(*args, **kwargs) def invalidate(obj, *args, **kwargs): - return obj._get_cache( - _cache_name).cache_on_arguments()(func).invalidate( - *args, **kwargs) + return ( + obj._get_cache(_cache_name) + .cache_on_arguments()(func) + .invalidate(*args, **kwargs) + ) _cache_decorator.invalidate = invalidate _cache_decorator.func = func _decorated_methods.append(func.__name__) return _cache_decorator + return _inner_cache_on_arguments @@ -320,7 +333,8 @@ def safe_dict_min(key, data): raise exc.OpenStackCloudException( "Search for minimum value failed. " "Value for {key} is not an integer: {value}".format( - key=key, value=d[key]) + key=key, value=d[key] + ) ) if (min_value is None) or (val < min_value): min_value = val @@ -352,16 +366,17 @@ def safe_dict_max(key, data): raise exc.OpenStackCloudException( "Search for maximum value failed. " "Value for {key} is not an integer: {value}".format( - key=key, value=d[key]) + key=key, value=d[key] + ) ) if (max_value is None) or (val > max_value): max_value = val return max_value -def _call_client_and_retry(client, url, retry_on=None, - call_retries=3, retry_wait=2, - **kwargs): +def _call_client_and_retry( + client, url, retry_on=None, call_retries=3, retry_wait=2, **kwargs +): """Method to provide retry operations. Some APIs utilize HTTP errors on certain operations to indicate that @@ -391,18 +406,17 @@ def _call_client_and_retry(client, url, retry_on=None, retry_on = [retry_on] count = 0 - while (count < call_retries): + while count < call_retries: count += 1 try: ret_val = client(url, **kwargs) except exc.OpenStackCloudHTTPError as e: - if (retry_on is not None - and e.response.status_code in retry_on): - log.debug('Received retryable error %(err)s, waiting ' - '%(wait)s seconds to retry', { - 'err': e.response.status_code, - 'wait': retry_wait - }) + if retry_on is not None and e.response.status_code in retry_on: + log.debug( + 'Received retryable error %(err)s, waiting ' + '%(wait)s seconds to retry', + {'err': e.response.status_code, 'wait': retry_wait}, + ) time.sleep(retry_wait) continue else: @@ -484,7 +498,8 @@ def range_filter(data, key, range_exp): # If parsing the range fails, it must be a bad value. if val_range is None: raise exc.OpenStackCloudException( - "Invalid range value: {value}".format(value=range_exp)) + "Invalid range value: {value}".format(value=range_exp) + ) op = val_range[0] if op: @@ -523,9 +538,7 @@ def generate_patches_from_kwargs(operation, **kwargs): """ patches = [] for k, v in kwargs.items(): - patch = {'op': operation, - 'value': v, - 'path': '/%s' % k} + patch = {'op': operation, 'value': v, 'path': '/%s' % k} patches.append(patch) return sorted(patches) @@ -568,11 +581,13 @@ def reset(self): def _format_uuid_string(string): - return (string.replace('urn:', '') - .replace('uuid:', '') - .strip('{}') - .replace('-', '') - .lower()) + return ( + string.replace('urn:', '') + .replace('uuid:', '') + .strip('{}') + .replace('-', '') + .lower() + ) def _is_uuid_like(val): diff --git a/openstack/cloud/cmd/inventory.py b/openstack/cloud/cmd/inventory.py index 548ddf15f..ce6cbeb97 100644 --- a/openstack/cloud/cmd/inventory.py +++ b/openstack/cloud/cmd/inventory.py @@ -32,20 +32,35 @@ def output_format_dict(data, use_yaml): def parse_args(): parser = argparse.ArgumentParser(description='OpenStack Inventory Module') - parser.add_argument('--refresh', action='store_true', - help='Refresh cached information') + parser.add_argument( + '--refresh', action='store_true', help='Refresh cached information' + ) group = parser.add_mutually_exclusive_group(required=True) - group.add_argument('--list', action='store_true', - help='List active servers') + group.add_argument( + '--list', action='store_true', help='List active servers' + ) group.add_argument('--host', help='List details about the specific host') - parser.add_argument('--private', action='store_true', default=False, - help='Use private IPs for interface_ip') - parser.add_argument('--cloud', default=None, - help='Return data for one cloud only') - parser.add_argument('--yaml', action='store_true', default=False, - help='Output data in nicely readable yaml') - parser.add_argument('--debug', action='store_true', default=False, - help='Enable debug output') + parser.add_argument( + '--private', + action='store_true', + default=False, + help='Use private IPs for interface_ip', + ) + parser.add_argument( + '--cloud', default=None, help='Return data for one cloud only' + ) + parser.add_argument( + '--yaml', + action='store_true', + default=False, + help='Output data in nicely readable yaml', + ) + parser.add_argument( + '--debug', + action='store_true', + default=False, + help='Enable debug output', + ) return parser.parse_args() @@ -54,8 +69,8 @@ def main(): try: openstack.enable_logging(debug=args.debug) inventory = openstack.cloud.inventory.OpenStackInventory( - refresh=args.refresh, private=args.private, - cloud=args.cloud) + refresh=args.refresh, private=args.private, cloud=args.cloud + ) if args.list: output = inventory.list_hosts() elif args.host: diff --git a/openstack/cloud/exc.py b/openstack/cloud/exc.py index d82d73e6c..3d75d4878 100644 --- a/openstack/cloud/exc.py +++ b/openstack/cloud/exc.py @@ -19,12 +19,14 @@ class OpenStackCloudCreateException(OpenStackCloudException): - def __init__(self, resource, resource_id, extra_data=None, **kwargs): super(OpenStackCloudCreateException, self).__init__( message="Error creating {resource}: {resource_id}".format( - resource=resource, resource_id=resource_id), - extra_data=extra_data, **kwargs) + resource=resource, resource_id=resource_id + ), + extra_data=extra_data, + **kwargs + ) self.resource_id = resource_id diff --git a/openstack/cloud/inventory.py b/openstack/cloud/inventory.py index f8e9040ba..42b9402dc 100644 --- a/openstack/cloud/inventory.py +++ b/openstack/cloud/inventory.py @@ -28,15 +28,23 @@ class OpenStackInventory: extra_config = None def __init__( - self, config_files=None, refresh=False, private=False, - config_key=None, config_defaults=None, cloud=None, - use_direct_get=False): + self, + config_files=None, + refresh=False, + private=False, + config_key=None, + config_defaults=None, + cloud=None, + use_direct_get=False, + ): if config_files is None: config_files = [] config = loader.OpenStackConfig( - config_files=loader.CONFIG_FILES + config_files) + config_files=loader.CONFIG_FILES + config_files + ) self.extra_config = config.get_extra_config( - config_key, config_defaults) + config_key, config_defaults + ) if cloud is None: self.clouds = [ @@ -44,9 +52,7 @@ def __init__( for cloud_region in config.get_all() ] else: - self.clouds = [ - connection.Connection(config=config.get_one(cloud)) - ] + self.clouds = [connection.Connection(config=config.get_one(cloud))] if private: for cloud in self.clouds: @@ -57,15 +63,17 @@ def __init__( for cloud in self.clouds: cloud._cache.invalidate() - def list_hosts(self, expand=True, fail_on_cloud_config=True, - all_projects=False): + def list_hosts( + self, expand=True, fail_on_cloud_config=True, all_projects=False + ): hostvars = [] for cloud in self.clouds: try: # Cycle on servers - for server in cloud.list_servers(detailed=expand, - all_projects=all_projects): + for server in cloud.list_servers( + detailed=expand, all_projects=all_projects + ): hostvars.append(server) except exceptions.OpenStackCloudException: # Don't fail on one particular cloud as others may work diff --git a/openstack/cloud/meta.py b/openstack/cloud/meta.py index 5622aeaf0..c1e6f692f 100644 --- a/openstack/cloud/meta.py +++ b/openstack/cloud/meta.py @@ -23,8 +23,9 @@ NON_CALLABLES = (str, bool, dict, int, float, list, type(None)) -def find_nova_interfaces(addresses, ext_tag=None, key_name=None, version=4, - mac_addr=None): +def find_nova_interfaces( + addresses, ext_tag=None, key_name=None, version=4, mac_addr=None +): ret = [] for (k, v) in iter(addresses.items()): if key_name is not None and k != key_name: @@ -64,10 +65,12 @@ def find_nova_interfaces(addresses, ext_tag=None, key_name=None, version=4, return ret -def find_nova_addresses(addresses, ext_tag=None, key_name=None, version=4, - mac_addr=None): - interfaces = find_nova_interfaces(addresses, ext_tag, key_name, version, - mac_addr) +def find_nova_addresses( + addresses, ext_tag=None, key_name=None, version=4, mac_addr=None +): + interfaces = find_nova_interfaces( + addresses, ext_tag, key_name, version, mac_addr + ) floating_addrs = [] fixed_addrs = [] for i in interfaces: @@ -91,8 +94,7 @@ def get_server_ip(server, public=False, cloud_public=True, **kwargs): private ip we expect shade to be able to reach """ addrs = find_nova_addresses(server['addresses'], **kwargs) - return find_best_address( - addrs, public=public, cloud_public=cloud_public) + return find_best_address(addrs, public=public, cloud_public=cloud_public) def get_server_private_ip(server, cloud=None): @@ -126,30 +128,34 @@ def get_server_private_ip(server, cloud=None): int_nets = cloud.get_internal_ipv4_networks() for int_net in int_nets: int_ip = get_server_ip( - server, key_name=int_net['name'], + server, + key_name=int_net['name'], ext_tag='fixed', cloud_public=not cloud.private, - mac_addr=fip_mac) + mac_addr=fip_mac, + ) if int_ip is not None: return int_ip # Try a second time without the fixed tag. This is for old nova-network # results that do not have the fixed/floating tag. for int_net in int_nets: int_ip = get_server_ip( - server, key_name=int_net['name'], + server, + key_name=int_net['name'], cloud_public=not cloud.private, - mac_addr=fip_mac) + mac_addr=fip_mac, + ) if int_ip is not None: return int_ip ip = get_server_ip( - server, ext_tag='fixed', key_name='private', mac_addr=fip_mac) + server, ext_tag='fixed', key_name='private', mac_addr=fip_mac + ) if ip: return ip # Last resort, and Rackspace - return get_server_ip( - server, key_name='private') + return get_server_ip(server, key_name='private') def get_server_external_ipv4(cloud, server): @@ -183,8 +189,11 @@ def get_server_external_ipv4(cloud, server): ext_nets = cloud.get_external_ipv4_networks() for ext_net in ext_nets: ext_ip = get_server_ip( - server, key_name=ext_net['name'], public=True, - cloud_public=not cloud.private) + server, + key_name=ext_net['name'], + public=True, + cloud_public=not cloud.private, + ) if ext_ip is not None: return ext_ip @@ -192,8 +201,8 @@ def get_server_external_ipv4(cloud, server): # Much as I might find floating IPs annoying, if it has one, that's # almost certainly the one that wants to be used ext_ip = get_server_ip( - server, ext_tag='floating', public=True, - cloud_public=not cloud.private) + server, ext_tag='floating', public=True, cloud_public=not cloud.private + ) if ext_ip is not None: return ext_ip @@ -203,8 +212,8 @@ def get_server_external_ipv4(cloud, server): # Try to get an address from a network named 'public' ext_ip = get_server_ip( - server, key_name='public', public=True, - cloud_public=not cloud.private) + server, key_name='public', public=True, cloud_public=not cloud.private + ) if ext_ip is not None: return ext_ip @@ -238,15 +247,21 @@ def find_best_address(addresses, public=False, cloud_public=True): for address in addresses: try: for count in utils.iterate_timeout( - 5, "Timeout waiting for %s" % address, wait=0.1): + 5, "Timeout waiting for %s" % address, wait=0.1 + ): # Return the first one that is reachable try: for res in socket.getaddrinfo( - address, 22, socket.AF_UNSPEC, - socket.SOCK_STREAM, 0): + address, + 22, + socket.AF_UNSPEC, + socket.SOCK_STREAM, + 0, + ): family, socktype, proto, _, sa = res connect_socket = socket.socket( - family, socktype, proto) + family, socktype, proto + ) connect_socket.settimeout(1) connect_socket.connect(sa) return address @@ -265,12 +280,13 @@ def find_best_address(addresses, public=False, cloud_public=True): "The cloud returned multiple addresses %s:, and we could not " "connect to port 22 on either. That might be what you wanted, " "but we have no clue what's going on, so we picked the first one " - "%s" % (addresses, addresses[0])) + "%s" % (addresses, addresses[0]) + ) return addresses[0] def get_server_external_ipv6(server): - """ Get an IPv6 address reachable from outside the cloud. + """Get an IPv6 address reachable from outside the cloud. This function assumes that if a server has an IPv6 address, that address is reachable from outside the cloud. @@ -286,7 +302,7 @@ def get_server_external_ipv6(server): def get_server_default_ip(cloud, server): - """ Get the configured 'default' address + """Get the configured 'default' address It is possible in clouds.yaml to configure for a cloud a network that is the 'default_interface'. This is the network that should be used @@ -299,22 +315,26 @@ def get_server_default_ip(cloud, server): """ ext_net = cloud.get_default_network() if ext_net: - if (cloud._local_ipv6 and not cloud.force_ipv4): + if cloud._local_ipv6 and not cloud.force_ipv4: # try 6 first, fall back to four versions = [6, 4] else: versions = [4] for version in versions: ext_ip = get_server_ip( - server, key_name=ext_net['name'], version=version, public=True, - cloud_public=not cloud.private) + server, + key_name=ext_net['name'], + version=version, + public=True, + cloud_public=not cloud.private, + ) if ext_ip is not None: return ext_ip return None def _get_interface_ip(cloud, server): - """ Get the interface IP for the server + """Get the interface IP for the server Interface IP is the IP that should be used for communicating with the server. It is: @@ -329,7 +349,7 @@ def _get_interface_ip(cloud, server): if cloud.private and server['private_v4']: return server['private_v4'] - if (server['public_v6'] and cloud._local_ipv6 and not cloud.force_ipv4): + if server['public_v6'] and cloud._local_ipv6 and not cloud.force_ipv4: return server['public_v6'] else: return server['public_v4'] @@ -404,15 +424,19 @@ def _get_supplemental_addresses(cloud, server): try: # Don't bother doing this before the server is active, it's a waste # of an API call while polling for a server to come up - if (cloud.has_service('network') - and cloud._has_floating_ips() - and server['status'] == 'ACTIVE'): + if ( + cloud.has_service('network') + and cloud._has_floating_ips() + and server['status'] == 'ACTIVE' + ): for port in cloud.search_ports( - filters=dict(device_id=server['id'])): + filters=dict(device_id=server['id']) + ): # This SHOULD return one and only one FIP - but doing it as a # search/list lets the logic work regardless for fip in cloud.search_floating_ips( - filters=dict(port_id=port['id'])): + filters=dict(port_id=port['id']) + ): fixed_net = fixed_ip_mapping.get(fip['fixed_ip_address']) if fixed_net is None: log = _log.setup_logging('openstack') @@ -422,10 +446,12 @@ def _get_supplemental_addresses(cloud, server): " with the floating ip in the neutron listing" " does not exist in the nova listing. Something" " is exceptionally broken.", - dict(fip=fip['id'], server=server['id'])) + dict(fip=fip['id'], server=server['id']), + ) else: server['addresses'][fixed_net].append( - _make_address_dict(fip, port)) + _make_address_dict(fip, port) + ) except exc.OpenStackCloudException: # If something goes wrong with a cloud call, that's cool - this is # an attempt to provide additional data and should not block forward @@ -485,8 +511,7 @@ def get_hostvars_from_server(cloud, server, mounts=None): expand_server_vars if caching is not set up. If caching is set up, the extra cost should be minimal. """ - server_vars = obj_to_munch( - add_server_interfaces(cloud, server)) + server_vars = obj_to_munch(add_server_interfaces(cloud, server)) flavor_id = server['flavor'].get('id') if flavor_id: @@ -539,7 +564,7 @@ def get_hostvars_from_server(cloud, server, mounts=None): def obj_to_munch(obj): - """ Turn an object with attributes into a dict suitable for serializing. + """Turn an object with attributes into a dict suitable for serializing. Some of the things that are returned in OpenStack are objects with attributes. That's awesome - except when you want to expose them as JSON diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 9b96e58db..76f73b500 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -12,6 +12,7 @@ import copy import functools import queue + # import types so that we can reference ListType in sphinx param declarations. # We can't just use list, because sphinx gets confused by # openstack.resource.Resource.list and openstack.resource2.Resource.list @@ -60,6 +61,7 @@ class _OpenStackCloudMixin: :param bool strict: Only return documented attributes for each resource as per the Data Model contract. (Default False) """ + _OBJECT_MD5_KEY = 'x-sdk-md5' _OBJECT_SHA256_KEY = 'x-sdk-sha256' _OBJECT_AUTOCREATE_KEY = 'x-sdk-autocreated' @@ -90,7 +92,8 @@ def __init__(self): # cert verification if not self.verify: self.log.debug( - "Turning off Insecure SSL warnings since verify=False") + "Turning off Insecure SSL warnings since verify=False" + ) category = requestsexceptions.InsecureRequestWarning if category: # InsecureRequestWarning references a Warning class or is None @@ -131,19 +134,20 @@ def invalidate(self): meth_obj = getattr(self, method, None) if not meth_obj: continue - if (hasattr(meth_obj, 'invalidate') - and hasattr(meth_obj, 'func')): + if hasattr(meth_obj, 'invalidate') and hasattr( + meth_obj, 'func' + ): new_func = functools.partial(meth_obj.func, self) new_func.invalidate = _fake_invalidate setattr(self, method, new_func) # Uncoditionally create cache even with a "null" backend self._cache = self._make_cache( - cache_class, cache_expiration_time, cache_arguments) + cache_class, cache_expiration_time, cache_arguments + ) expirations = self.config.get_cache_expirations() for expire_key in expirations.keys(): - self._cache_expirations[expire_key] = \ - expirations[expire_key] + self._cache_expirations[expire_key] = expirations[expire_key] # TODO(gtema): delete in next change self._SERVER_AGE = 0 @@ -159,7 +163,8 @@ def invalidate(self): self._raw_clients = {} self._local_ipv6 = ( - _utils.localhost_supports_ipv6() if not self.force_ipv4 else False) + _utils.localhost_supports_ipv6() if not self.force_ipv4 else False + ) def connect_as(self, **kwargs): """Make a new OpenStackCloud object with new auth context. @@ -191,7 +196,8 @@ def connect_as(self, **kwargs): config = openstack.config.OpenStackConfig( app_name=self.config._app_name, app_version=self.config._app_version, - load_yaml_config=False) + load_yaml_config=False, + ) params = copy.deepcopy(self.config.config) # Remove profile from current cloud so that overridding works params.pop('profile', None) @@ -298,7 +304,8 @@ def global_request(self, global_request_id): app_name=self.config._app_name, app_version=self.config._app_version, discovery_cache=self.session._discovery_cache, - **params) + **params + ) # Override the cloud name so that logging/location work right cloud_region._name = self.name @@ -313,9 +320,8 @@ def _make_cache(self, cache_class, expiration_time, arguments): return dogpile.cache.make_region( function_key_generator=self._make_cache_key ).configure( - cache_class, - expiration_time=expiration_time, - arguments=arguments) + cache_class, expiration_time=expiration_time, arguments=arguments + ) def _make_cache_key(self, namespace, fn): fname = fn.__name__ @@ -329,10 +335,11 @@ def generate_key(*args, **kwargs): arg_key = '' kw_keys = sorted(kwargs.keys()) kwargs_key = ','.join( - ['%s:%s' % (k, kwargs[k]) for k in kw_keys if k != 'cache']) - ans = "_".join( - [str(name_key), fname, arg_key, kwargs_key]) + ['%s:%s' % (k, kwargs[k]) for k in kw_keys if k != 'cache'] + ) + ans = "_".join([str(name_key), fname, arg_key, kwargs_key]) return ans + return generate_key def _get_cache(self, resource_name): @@ -349,7 +356,8 @@ def _get_major_version_id(self, version): return version def _get_versioned_client( - self, service_type, min_version=None, max_version=None): + self, service_type, min_version=None, max_version=None + ): config_version = self.config.get_api_version(service_type) config_major = self._get_major_version_id(config_version) max_major = self._get_major_version_id(max_version) @@ -372,7 +380,9 @@ def _get_versioned_client( " but shade understands a minimum of {min_version}".format( config_version=config_version, service_type=service_type, - min_version=min_version)) + min_version=min_version, + ) + ) elif max_major and config_major > max_major: raise exc.OpenStackCloudException( "Version {config_version} requested for {service_type}" @@ -380,10 +390,13 @@ def _get_versioned_client( " {max_version}".format( config_version=config_version, service_type=service_type, - max_version=max_version)) + max_version=max_version, + ) + ) request_min_version = config_version request_max_version = '{version}.latest'.format( - version=config_major) + version=config_major + ) adapter = proxy._ShadeAdapter( session=self.session, service_type=self.config.get_service_type(service_type), @@ -397,7 +410,8 @@ def _get_versioned_client( prometheus_histogram=self.config.get_prometheus_histogram(), influxdb_client=self.config.get_influxdb_client(), min_version=request_min_version, - max_version=request_max_version) + max_version=request_max_version, + ) if adapter.get_endpoint(): return adapter @@ -409,12 +423,14 @@ def _get_versioned_client( endpoint_override=self.config.get_endpoint(service_type), region_name=self.config.get_region_name(service_type), min_version=min_version, - max_version=max_version) + max_version=max_version, + ) # data.api_version can be None if no version was detected, such # as with neutron api_version = adapter.get_api_major_version( - endpoint_override=self.config.get_endpoint(service_type)) + endpoint_override=self.config.get_endpoint(service_type) + ) api_major = self._get_major_version_id(api_version) # If we detect a different version that was configured, warn the user. @@ -430,7 +446,9 @@ def _get_versioned_client( ' your config.'.format( service_type=service_type, config_version=config_version, - api_version='.'.join([str(f) for f in api_version]))) + api_version='.'.join([str(f) for f in api_version]), + ) + ) self.log.debug(warning_msg) warnings.warn(warning_msg) return adapter @@ -438,19 +456,22 @@ def _get_versioned_client( # TODO(shade) This should be replaced with using openstack Connection # object. def _get_raw_client( - self, service_type, api_version=None, endpoint_override=None): + self, service_type, api_version=None, endpoint_override=None + ): return proxy._ShadeAdapter( session=self.session, service_type=self.config.get_service_type(service_type), service_name=self.config.get_service_name(service_type), interface=self.config.get_interface(service_type), - endpoint_override=self.config.get_endpoint( - service_type) or endpoint_override, - region_name=self.config.get_region_name(service_type)) + endpoint_override=self.config.get_endpoint(service_type) + or endpoint_override, + region_name=self.config.get_region_name(service_type), + ) def _is_client_version(self, client, version): client_name = '_{client}_client'.format( - client=client.replace('-', '_')) + client=client.replace('-', '_') + ) client = getattr(self, client_name) return client._version_matches(version) @@ -458,7 +479,8 @@ def _is_client_version(self, client, version): def _application_catalog_client(self): if 'application-catalog' not in self._raw_clients: self._raw_clients['application-catalog'] = self._get_raw_client( - 'application-catalog') + 'application-catalog' + ) return self._raw_clients['application-catalog'] @property @@ -478,6 +500,7 @@ def pprint(self, resource): """Wrapper around pprint that groks munch objects""" # import late since this is a utility function import pprint + new_resource = _utils._dictify_resource(resource) pprint.pprint(new_resource) @@ -485,6 +508,7 @@ def pformat(self, resource): """Wrapper around pformat that groks munch objects""" # import late since this is a utility function import pprint + new_resource = _utils._dictify_resource(resource) return pprint.pformat(new_resource) @@ -521,7 +545,8 @@ def endpoint_for(self, service_type, interface=None, region_name=None): return self.config.get_endpoint_from_catalog( service_type=service_type, interface=interface, - region_name=region_name) + region_name=region_name, + ) @property def auth_token(self): @@ -600,10 +625,9 @@ def _get_identity_location(self): region_name=None, zone=None, project=utils.Munch( - id=None, - name=None, - domain_id=None, - domain_name=None)) + id=None, name=None, domain_id=None, domain_name=None + ), + ) def _get_project_id_param_dict(self, name_or_id): if name_or_id: @@ -628,7 +652,8 @@ def _get_domain_id_param_dict(self, domain_id): if not domain_id: raise exc.OpenStackCloudException( "User or project creation requires an explicit" - " domain_id argument.") + " domain_id argument." + ) else: return {'domain_id': domain_id} else: @@ -714,7 +739,8 @@ def get_session_endpoint(self, service_key, **kwargs): return self.config.get_session_endpoint(service_key, **kwargs) except keystoneauth1.exceptions.catalog.EndpointNotFound as e: self.log.debug( - "Endpoint not found in %s cloud: %s", self.name, str(e)) + "Endpoint not found in %s cloud: %s", self.name, str(e) + ) endpoint = None except exc.OpenStackCloudException: raise @@ -725,17 +751,22 @@ def get_session_endpoint(self, service_key, **kwargs): service=service_key, cloud=self.name, region=self.config.get_region_name(service_key), - error=str(e))) + error=str(e), + ) + ) return endpoint def has_service(self, service_key, version=None): if not self.config.has_service(service_key): # TODO(mordred) add a stamp here so that we only report this once - if not (service_key in self._disable_warnings - and self._disable_warnings[service_key]): + if not ( + service_key in self._disable_warnings + and self._disable_warnings[service_key] + ): self.log.debug( - "Disabling %(service_key)s entry in catalog" - " per config", {'service_key': service_key}) + "Disabling %(service_key)s entry in catalog" " per config", + {'service_key': service_key}, + ) self._disable_warnings[service_key] = True return False try: @@ -786,26 +817,23 @@ def search_resources( (service_name, resource_name) = resource_type.split('.') if not hasattr(self, service_name): raise exceptions.SDKException( - "service %s is not existing/enabled" % - service_name + "service %s is not existing/enabled" % service_name ) service_proxy = getattr(self, service_name) try: resource_type = service_proxy._resource_registry[resource_name] except KeyError: raise exceptions.SDKException( - "Resource %s is not known in service %s" % - (resource_name, service_name) + "Resource %s is not known in service %s" + % (resource_name, service_name) ) if name_or_id: # name_or_id is definitely not None try: resource_by_id = service_proxy._get( - resource_type, - name_or_id, - *get_args, - **get_kwargs) + resource_type, name_or_id, *get_args, **get_kwargs + ) return [resource_by_id] except exceptions.ResourceNotFound: pass @@ -817,11 +845,9 @@ def search_resources( filters["name"] = name_or_id list_kwargs.update(filters) - return list(service_proxy._list( - resource_type, - *list_args, - **list_kwargs - )) + return list( + service_proxy._list(resource_type, *list_args, **list_kwargs) + ) def project_cleanup( self, @@ -829,7 +855,7 @@ def project_cleanup( wait_timeout=120, status_queue=None, filters=None, - resource_evaluation_fn=None + resource_evaluation_fn=None, ): """Cleanup the project resources. @@ -866,7 +892,7 @@ def project_cleanup( dependencies.update(deps) except ( exceptions.NotSupported, - exceptions.ServiceDisabledException + exceptions.ServiceDisabledException, ): # Cloud may include endpoint in catalog but not # implement the service or disable it @@ -895,7 +921,7 @@ def project_cleanup( client_status_queue=status_queue, identified_resources=cleanup_resources, filters=filters, - resource_evaluation_fn=resource_evaluation_fn + resource_evaluation_fn=resource_evaluation_fn, ) except exceptions.ServiceDisabledException: # same reason as above @@ -908,9 +934,10 @@ def project_cleanup( dep_graph.node_done(service) for count in utils.iterate_timeout( - timeout=wait_timeout, - message="Timeout waiting for cleanup to finish", - wait=1): + timeout=wait_timeout, + message="Timeout waiting for cleanup to finish", + wait=1, + ): if dep_graph.is_complete(): return diff --git a/openstack/tests/functional/cloud/test_aggregate.py b/openstack/tests/functional/cloud/test_aggregate.py index e96b64f3d..abd920e62 100644 --- a/openstack/tests/functional/cloud/test_aggregate.py +++ b/openstack/tests/functional/cloud/test_aggregate.py @@ -21,7 +21,6 @@ class TestAggregate(base.BaseFunctionalTest): - def test_aggregates(self): if not self.operator_cloud: self.skipTest("Operator cloud is required for this test") @@ -30,31 +29,28 @@ def test_aggregates(self): self.addCleanup(self.cleanup, aggregate_name) aggregate = self.operator_cloud.create_aggregate(aggregate_name) - aggregate_ids = [v['id'] - for v in self.operator_cloud.list_aggregates()] + aggregate_ids = [ + v['id'] for v in self.operator_cloud.list_aggregates() + ] self.assertIn(aggregate['id'], aggregate_ids) aggregate = self.operator_cloud.update_aggregate( - aggregate_name, - availability_zone=availability_zone + aggregate_name, availability_zone=availability_zone ) self.assertEqual(availability_zone, aggregate['availability_zone']) aggregate = self.operator_cloud.set_aggregate_metadata( - aggregate_name, - {'key': 'value'} + aggregate_name, {'key': 'value'} ) self.assertIn('key', aggregate['metadata']) aggregate = self.operator_cloud.set_aggregate_metadata( - aggregate_name, - {'key': None} + aggregate_name, {'key': None} ) self.assertNotIn('key', aggregate['metadata']) # Validate that we can delete by name - self.assertTrue( - self.operator_cloud.delete_aggregate(aggregate_name)) + self.assertTrue(self.operator_cloud.delete_aggregate(aggregate_name)) def cleanup(self, aggregate_name): aggregate = self.operator_cloud.get_aggregate(aggregate_name) diff --git a/openstack/tests/functional/cloud/test_cluster_templates.py b/openstack/tests/functional/cloud/test_cluster_templates.py index afc304bba..897950a22 100644 --- a/openstack/tests/functional/cloud/test_cluster_templates.py +++ b/openstack/tests/functional/cloud/test_cluster_templates.py @@ -26,7 +26,6 @@ class TestClusterTemplate(base.BaseFunctionalTest): - def setUp(self): super(TestClusterTemplate, self).setUp() if not self.user_cloud.has_service( @@ -52,8 +51,16 @@ def test_cluster_templates(self): # generate a keypair to add to nova subprocess.call( - ['ssh-keygen', '-t', 'rsa', '-N', '', '-f', - '%s/id_rsa_sdk' % self.ssh_directory]) + [ + 'ssh-keygen', + '-t', + 'rsa', + '-N', + '', + '-f', + '%s/id_rsa_sdk' % self.ssh_directory, + ] + ) # add keypair to nova with open('%s/id_rsa_sdk.pub' % self.ssh_directory) as f: @@ -62,8 +69,8 @@ def test_cluster_templates(self): # Test we can create a cluster_template and we get it returned self.ct = self.user_cloud.create_cluster_template( - name=name, image_id=image_id, - keypair_id=keypair_id, coe=coe) + name=name, image_id=image_id, keypair_id=keypair_id, coe=coe + ) self.assertEqual(self.ct['name'], name) self.assertEqual(self.ct['image_id'], image_id) self.assertEqual(self.ct['keypair_id'], keypair_id) @@ -80,7 +87,8 @@ def test_cluster_templates(self): # Test we get the same cluster_template with the # get_cluster_template method cluster_template_get = self.user_cloud.get_cluster_template( - self.ct['uuid']) + self.ct['uuid'] + ) self.assertEqual(cluster_template_get['uuid'], self.ct['uuid']) # Test the get method also works by name @@ -90,14 +98,15 @@ def test_cluster_templates(self): # Test we can update a field on the cluster_template and only that # field is updated cluster_template_update = self.user_cloud.update_cluster_template( - self.ct, tls_disabled=True) - self.assertEqual( - cluster_template_update['uuid'], self.ct['uuid']) + self.ct, tls_disabled=True + ) + self.assertEqual(cluster_template_update['uuid'], self.ct['uuid']) self.assertTrue(cluster_template_update['tls_disabled']) # Test we can delete and get True returned cluster_template_delete = self.user_cloud.delete_cluster_template( - self.ct['uuid']) + self.ct['uuid'] + ) self.assertTrue(cluster_template_delete) def cleanup(self, name): diff --git a/openstack/tests/functional/cloud/test_clustering.py b/openstack/tests/functional/cloud/test_clustering.py index 80fd8a7a7..96bb3302a 100644 --- a/openstack/tests/functional/cloud/test_clustering.py +++ b/openstack/tests/functional/cloud/test_clustering.py @@ -24,8 +24,9 @@ from openstack.tests.functional import base -def wait_for_status(client, client_args, field, value, check_interval=1, - timeout=60): +def wait_for_status( + client, client_args, field, value, check_interval=1, timeout=60 +): """Wait for an OpenStack resource to enter a specified state :param client: An uncalled client resource to be called with resource_args @@ -55,15 +56,15 @@ def wait_for_status(client, client_args, field, value, check_interval=1, def wait_for_create(client, client_args, check_interval=1, timeout=60): """Wait for an OpenStack resource to be created - :param client: An uncalled client resource to be called with resource_args - :param client_args: Arguments to be passed to client - :param name: Name of the resource (for logging) - :param check_interval: Interval between checks - :param timeout: Time in seconds to wait for status to update. - :returns: True if openstack.exceptions.NotFoundException is caught - :raises: TimeoutException + :param client: An uncalled client resource to be called with resource_args + :param client_args: Arguments to be passed to client + :param name: Name of the resource (for logging) + :param check_interval: Interval between checks + :param timeout: Time in seconds to wait for status to update. + :returns: True if openstack.exceptions.NotFoundException is caught + :raises: TimeoutException - """ + """ resource = client(**client_args) start = time.time() @@ -106,7 +107,6 @@ def wait_for_delete(client, client_args, check_interval=1, timeout=60): class TestClustering(base.BaseFunctionalTest): - def setUp(self): super(TestClustering, self).setUp() self.skipTest('clustering service not supported by cloud') @@ -117,24 +117,19 @@ def test_create_profile(self): "properties": { "flavor": self.flavor.name, "image": self.image.name, - "networks": [ - { - "network": "private" - } - ], - "security_groups": [ - "default" - ] + "networks": [{"network": "private"}], + "security_groups": ["default"], }, "type": "os.nova.server", - "version": 1.0 + "version": 1.0, } self.addDetail('profile', content.text_content(profile_name)) # Test that we can create a profile and we get it returned - profile = self.user_cloud.create_cluster_profile(name=profile_name, - spec=spec) + profile = self.user_cloud.create_cluster_profile( + name=profile_name, spec=spec + ) self.addCleanup(self.cleanup_profile, profile['id']) @@ -147,24 +142,19 @@ def test_create_cluster(self): "properties": { "flavor": self.flavor.name, "image": self.image.name, - "networks": [ - { - "network": "private" - } - ], - "security_groups": [ - "default" - ] + "networks": [{"network": "private"}], + "security_groups": ["default"], }, "type": "os.nova.server", - "version": 1.0 + "version": 1.0, } self.addDetail('profile', content.text_content(profile_name)) # Test that we can create a profile and we get it returned - profile = self.user_cloud.create_cluster_profile(name=profile_name, - spec=spec) + profile = self.user_cloud.create_cluster_profile( + name=profile_name, spec=spec + ) self.addCleanup(self.cleanup_profile, profile['id']) @@ -175,15 +165,18 @@ def test_create_cluster(self): # Test that we can create a cluster and we get it returned cluster = self.user_cloud.create_cluster( - name=cluster_name, profile=profile, - desired_capacity=desired_capacity) + name=cluster_name, + profile=profile, + desired_capacity=desired_capacity, + ) self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) self.assertEqual(cluster['cluster']['name'], cluster_name) self.assertEqual(cluster['cluster']['profile_id'], profile['id']) - self.assertEqual(cluster['cluster']['desired_capacity'], - desired_capacity) + self.assertEqual( + cluster['cluster']['desired_capacity'], desired_capacity + ) def test_get_cluster_by_id(self): profile_name = "test_profile" @@ -191,24 +184,19 @@ def test_get_cluster_by_id(self): "properties": { "flavor": self.flavor.name, "image": self.image.name, - "networks": [ - { - "network": "private" - } - ], - "security_groups": [ - "default" - ] + "networks": [{"network": "private"}], + "security_groups": ["default"], }, "type": "os.nova.server", - "version": 1.0 + "version": 1.0, } self.addDetail('profile', content.text_content(profile_name)) # Test that we can create a profile and we get it returned - profile = self.user_cloud.create_cluster_profile(name=profile_name, - spec=spec) + profile = self.user_cloud.create_cluster_profile( + name=profile_name, spec=spec + ) self.addCleanup(self.cleanup_profile, profile['id']) cluster_name = 'example_cluster' @@ -218,14 +206,17 @@ def test_get_cluster_by_id(self): # Test that we can create a cluster and we get it returned cluster = self.user_cloud.create_cluster( - name=cluster_name, profile=profile, - desired_capacity=desired_capacity) + name=cluster_name, + profile=profile, + desired_capacity=desired_capacity, + ) self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) # Test that we get the same cluster with the get_cluster method cluster_get = self.user_cloud.get_cluster_by_id( - cluster['cluster']['id']) + cluster['cluster']['id'] + ) self.assertEqual(cluster_get['id'], cluster['cluster']['id']) def test_update_cluster(self): @@ -234,24 +225,19 @@ def test_update_cluster(self): "properties": { "flavor": self.flavor.name, "image": self.image.name, - "networks": [ - { - "network": "private" - } - ], - "security_groups": [ - "default" - ] + "networks": [{"network": "private"}], + "security_groups": ["default"], }, "type": "os.nova.server", - "version": 1.0 + "version": 1.0, } self.addDetail('profile', content.text_content(profile_name)) # Test that we can create a profile and we get it returned - profile = self.user_cloud.create_cluster_profile(name=profile_name, - spec=spec) + profile = self.user_cloud.create_cluster_profile( + name=profile_name, spec=spec + ) self.addCleanup(self.cleanup_profile, profile['id']) @@ -262,30 +248,40 @@ def test_update_cluster(self): # Test that we can create a cluster and we get it returned cluster = self.user_cloud.create_cluster( - name=cluster_name, profile=profile, - desired_capacity=desired_capacity) + name=cluster_name, + profile=profile, + desired_capacity=desired_capacity, + ) self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) # Test that we can update a field on the cluster and only that field # is updated - self.user_cloud.update_cluster(cluster['cluster']['id'], - new_name='new_cluster_name') + self.user_cloud.update_cluster( + cluster['cluster']['id'], new_name='new_cluster_name' + ) wait = wait_for_status( self.user_cloud.get_cluster_by_id, - {'name_or_id': cluster['cluster']['id']}, 'status', 'ACTIVE') + {'name_or_id': cluster['cluster']['id']}, + 'status', + 'ACTIVE', + ) self.assertTrue(wait) cluster_update = self.user_cloud.get_cluster_by_id( - cluster['cluster']['id']) + cluster['cluster']['id'] + ) self.assertEqual(cluster_update['id'], cluster['cluster']['id']) self.assertEqual(cluster_update['name'], 'new_cluster_name') - self.assertEqual(cluster_update['profile_id'], - cluster['cluster']['profile_id']) - self.assertEqual(cluster_update['desired_capacity'], - cluster['cluster']['desired_capacity']) + self.assertEqual( + cluster_update['profile_id'], cluster['cluster']['profile_id'] + ) + self.assertEqual( + cluster_update['desired_capacity'], + cluster['cluster']['desired_capacity'], + ) def test_create_cluster_policy(self): policy_name = 'example_policy' @@ -294,20 +290,21 @@ def test_create_cluster_policy(self): "adjustment": { "min_step": 1, "number": 1, - "type": "CHANGE_IN_CAPACITY" + "type": "CHANGE_IN_CAPACITY", }, - "event": "CLUSTER_SCALE_IN" + "event": "CLUSTER_SCALE_IN", }, "type": "senlin.policy.scaling", - "version": "1.0" + "version": "1.0", } self.addDetail('policy', content.text_content(policy_name)) # Test that we can create a policy and we get it returned - policy = self.user_cloud.create_cluster_policy(name=policy_name, - spec=spec) + policy = self.user_cloud.create_cluster_policy( + name=policy_name, spec=spec + ) self.addCleanup(self.cleanup_policy, policy['id']) @@ -320,24 +317,19 @@ def test_attach_policy_to_cluster(self): "properties": { "flavor": self.flavor.name, "image": self.image.name, - "networks": [ - { - "network": "private" - } - ], - "security_groups": [ - "default" - ] + "networks": [{"network": "private"}], + "security_groups": ["default"], }, "type": "os.nova.server", - "version": 1.0 + "version": 1.0, } self.addDetail('profile', content.text_content(profile_name)) # Test that we can create a profile and we get it returned - profile = self.user_cloud.create_cluster_profile(name=profile_name, - spec=spec) + profile = self.user_cloud.create_cluster_profile( + name=profile_name, spec=spec + ) self.addCleanup(self.cleanup_profile, profile['id']) @@ -348,8 +340,10 @@ def test_attach_policy_to_cluster(self): # Test that we can create a cluster and we get it returned cluster = self.user_cloud.create_cluster( - name=cluster_name, profile=profile, - desired_capacity=desired_capacity) + name=cluster_name, + profile=profile, + desired_capacity=desired_capacity, + ) self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) @@ -359,33 +353,36 @@ def test_attach_policy_to_cluster(self): "adjustment": { "min_step": 1, "number": 1, - "type": "CHANGE_IN_CAPACITY" + "type": "CHANGE_IN_CAPACITY", }, - "event": "CLUSTER_SCALE_IN" + "event": "CLUSTER_SCALE_IN", }, "type": "senlin.policy.scaling", - "version": "1.0" + "version": "1.0", } self.addDetail('policy', content.text_content(policy_name)) # Test that we can create a policy and we get it returned - policy = self.user_cloud.create_cluster_policy(name=policy_name, - spec=spec) + policy = self.user_cloud.create_cluster_policy( + name=policy_name, spec=spec + ) - self.addCleanup(self.cleanup_policy, policy['id'], - cluster['cluster']['id']) + self.addCleanup( + self.cleanup_policy, policy['id'], cluster['cluster']['id'] + ) # Test that we can attach policy to cluster and get True returned attach_cluster = self.user_cloud.get_cluster_by_id( - cluster['cluster']['id']) - attach_policy = self.user_cloud.get_cluster_policy_by_id( - policy['id']) + cluster['cluster']['id'] + ) + attach_policy = self.user_cloud.get_cluster_policy_by_id(policy['id']) policy_attach = self.user_cloud.attach_policy_to_cluster( - attach_cluster, attach_policy, is_enabled=True) + attach_cluster, attach_policy, is_enabled=True + ) self.assertTrue(policy_attach) def test_detach_policy_from_cluster(self): @@ -394,24 +391,19 @@ def test_detach_policy_from_cluster(self): "properties": { "flavor": self.flavor.name, "image": self.image.name, - "networks": [ - { - "network": "private" - } - ], - "security_groups": [ - "default" - ] + "networks": [{"network": "private"}], + "security_groups": ["default"], }, "type": "os.nova.server", - "version": 1.0 + "version": 1.0, } self.addDetail('profile', content.text_content(profile_name)) # Test that we can create a profile and we get it returned - profile = self.user_cloud.create_cluster_profile(name=profile_name, - spec=spec) + profile = self.user_cloud.create_cluster_profile( + name=profile_name, spec=spec + ) self.addCleanup(self.cleanup_profile, profile['id']) @@ -422,8 +414,10 @@ def test_detach_policy_from_cluster(self): # Test that we can create a cluster and we get it returned cluster = self.user_cloud.create_cluster( - name=cluster_name, profile=profile, - desired_capacity=desired_capacity) + name=cluster_name, + profile=profile, + desired_capacity=desired_capacity, + ) self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) @@ -433,39 +427,45 @@ def test_detach_policy_from_cluster(self): "adjustment": { "min_step": 1, "number": 1, - "type": "CHANGE_IN_CAPACITY" + "type": "CHANGE_IN_CAPACITY", }, - "event": "CLUSTER_SCALE_IN" + "event": "CLUSTER_SCALE_IN", }, "type": "senlin.policy.scaling", - "version": "1.0" + "version": "1.0", } self.addDetail('policy', content.text_content(policy_name)) # Test that we can create a policy and we get it returned - policy = self.user_cloud.create_cluster_policy(name=policy_name, - spec=spec) + policy = self.user_cloud.create_cluster_policy( + name=policy_name, spec=spec + ) - self.addCleanup(self.cleanup_policy, policy['id'], - cluster['cluster']['id']) + self.addCleanup( + self.cleanup_policy, policy['id'], cluster['cluster']['id'] + ) attach_cluster = self.user_cloud.get_cluster_by_id( - cluster['cluster']['id']) - attach_policy = self.user_cloud.get_cluster_policy_by_id( - policy['id']) + cluster['cluster']['id'] + ) + attach_policy = self.user_cloud.get_cluster_policy_by_id(policy['id']) self.user_cloud.attach_policy_to_cluster( - attach_cluster, attach_policy, is_enabled=True) + attach_cluster, attach_policy, is_enabled=True + ) wait = wait_for_status( self.user_cloud.get_cluster_by_id, - {'name_or_id': cluster['cluster']['id']}, 'policies', - ['{policy}'.format(policy=policy['id'])]) + {'name_or_id': cluster['cluster']['id']}, + 'policies', + ['{policy}'.format(policy=policy['id'])], + ) policy_detach = self.user_cloud.detach_policy_from_cluster( - attach_cluster, attach_policy) + attach_cluster, attach_policy + ) self.assertTrue(policy_detach) self.assertTrue(wait) @@ -476,24 +476,19 @@ def test_get_policy_on_cluster_by_id(self): "properties": { "flavor": self.flavor.name, "image": self.image.name, - "networks": [ - { - "network": "private" - } - ], - "security_groups": [ - "default" - ] + "networks": [{"network": "private"}], + "security_groups": ["default"], }, "type": "os.nova.server", - "version": 1.0 + "version": 1.0, } self.addDetail('profile', content.text_content(profile_name)) # Test that we can create a profile and we get it returned - profile = self.user_cloud.create_cluster_profile(name=profile_name, - spec=spec) + profile = self.user_cloud.create_cluster_profile( + name=profile_name, spec=spec + ) self.addCleanup(self.cleanup_profile, profile['id']) @@ -504,8 +499,10 @@ def test_get_policy_on_cluster_by_id(self): # Test that we can create a cluster and we get it returned cluster = self.user_cloud.create_cluster( - name=cluster_name, profile=profile, - desired_capacity=desired_capacity) + name=cluster_name, + profile=profile, + desired_capacity=desired_capacity, + ) self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) @@ -515,50 +512,58 @@ def test_get_policy_on_cluster_by_id(self): "adjustment": { "min_step": 1, "number": 1, - "type": "CHANGE_IN_CAPACITY" + "type": "CHANGE_IN_CAPACITY", }, - "event": "CLUSTER_SCALE_IN" + "event": "CLUSTER_SCALE_IN", }, "type": "senlin.policy.scaling", - "version": "1.0" + "version": "1.0", } self.addDetail('policy', content.text_content(policy_name)) # Test that we can create a policy and we get it returned - policy = self.user_cloud.create_cluster_policy(name=policy_name, - spec=spec) + policy = self.user_cloud.create_cluster_policy( + name=policy_name, spec=spec + ) - self.addCleanup(self.cleanup_policy, policy['id'], - cluster['cluster']['id']) + self.addCleanup( + self.cleanup_policy, policy['id'], cluster['cluster']['id'] + ) # Test that we can attach policy to cluster and get True returned attach_cluster = self.user_cloud.get_cluster_by_id( - cluster['cluster']['id']) - attach_policy = self.user_cloud.get_cluster_policy_by_id( - policy['id']) + cluster['cluster']['id'] + ) + attach_policy = self.user_cloud.get_cluster_policy_by_id(policy['id']) policy_attach = self.user_cloud.attach_policy_to_cluster( - attach_cluster, attach_policy, is_enabled=True) + attach_cluster, attach_policy, is_enabled=True + ) self.assertTrue(policy_attach) wait = wait_for_status( self.user_cloud.get_cluster_by_id, - {'name_or_id': cluster['cluster']['id']}, 'policies', - ["{policy}".format(policy=policy['id'])]) + {'name_or_id': cluster['cluster']['id']}, + 'policies', + ["{policy}".format(policy=policy['id'])], + ) # Test that we get the same policy with the get_policy_on_cluster # method cluster_policy_get = self.user_cloud.get_policy_on_cluster( - cluster['cluster']["id"], policy['id']) + cluster['cluster']["id"], policy['id'] + ) - self.assertEqual(cluster_policy_get['cluster_id'], - cluster['cluster']["id"]) - self.assertEqual(cluster_policy_get['cluster_name'], - cluster['cluster']["name"]) + self.assertEqual( + cluster_policy_get['cluster_id'], cluster['cluster']["id"] + ) + self.assertEqual( + cluster_policy_get['cluster_name'], cluster['cluster']["name"] + ) self.assertEqual(cluster_policy_get['policy_id'], policy['id']), self.assertEqual(cluster_policy_get['policy_name'], policy['name']) self.assertTrue(wait) @@ -569,24 +574,19 @@ def test_list_policies_on_cluster(self): "properties": { "flavor": self.flavor.name, "image": self.image.name, - "networks": [ - { - "network": "private" - } - ], - "security_groups": [ - "default" - ] + "networks": [{"network": "private"}], + "security_groups": ["default"], }, "type": "os.nova.server", - "version": 1.0 + "version": 1.0, } self.addDetail('profile', content.text_content(profile_name)) # Test that we can create a profile and we get it returned - profile = self.user_cloud.create_cluster_profile(name=profile_name, - spec=spec) + profile = self.user_cloud.create_cluster_profile( + name=profile_name, spec=spec + ) self.addCleanup(self.cleanup_profile, profile['id']) @@ -597,8 +597,10 @@ def test_list_policies_on_cluster(self): # Test that we can create a cluster and we get it returned cluster = self.user_cloud.create_cluster( - name=cluster_name, profile=profile, - desired_capacity=desired_capacity) + name=cluster_name, + profile=profile, + desired_capacity=desired_capacity, + ) self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) @@ -608,48 +610,53 @@ def test_list_policies_on_cluster(self): "adjustment": { "min_step": 1, "number": 1, - "type": "CHANGE_IN_CAPACITY" + "type": "CHANGE_IN_CAPACITY", }, - "event": "CLUSTER_SCALE_IN" + "event": "CLUSTER_SCALE_IN", }, "type": "senlin.policy.scaling", - "version": "1.0" + "version": "1.0", } self.addDetail('policy', content.text_content(policy_name)) # Test that we can create a policy and we get it returned - policy = self.user_cloud.create_cluster_policy(name=policy_name, - spec=spec) + policy = self.user_cloud.create_cluster_policy( + name=policy_name, spec=spec + ) - self.addCleanup(self.cleanup_policy, policy['id'], - cluster['cluster']['id']) + self.addCleanup( + self.cleanup_policy, policy['id'], cluster['cluster']['id'] + ) attach_cluster = self.user_cloud.get_cluster_by_id( - cluster['cluster']['id']) - attach_policy = self.user_cloud.get_cluster_policy_by_id( - policy['id']) + cluster['cluster']['id'] + ) + attach_policy = self.user_cloud.get_cluster_policy_by_id(policy['id']) self.user_cloud.attach_policy_to_cluster( - attach_cluster, attach_policy, is_enabled=True) + attach_cluster, attach_policy, is_enabled=True + ) wait = wait_for_status( self.user_cloud.get_cluster_by_id, - {'name_or_id': cluster['cluster']['id']}, 'policies', - ["{policy}".format(policy=policy['id'])]) + {'name_or_id': cluster['cluster']['id']}, + 'policies', + ["{policy}".format(policy=policy['id'])], + ) cluster_policy = self.user_cloud.get_policy_on_cluster( - name_or_id=cluster['cluster']['id'], - policy_name_or_id=policy['id']) + name_or_id=cluster['cluster']['id'], policy_name_or_id=policy['id'] + ) policy_list = {"cluster_policies": [cluster_policy]} # Test that we can list the policies on a cluster cluster_policies = self.user_cloud.list_policies_on_cluster( - cluster['cluster']["id"]) - self.assertEqual( - cluster_policies, policy_list) + cluster['cluster']["id"] + ) + self.assertEqual(cluster_policies, policy_list) self.assertTrue(wait) def test_create_cluster_receiver(self): @@ -658,24 +665,19 @@ def test_create_cluster_receiver(self): "properties": { "flavor": self.flavor.name, "image": self.image.name, - "networks": [ - { - "network": "private" - } - ], - "security_groups": [ - "default" - ] + "networks": [{"network": "private"}], + "security_groups": ["default"], }, "type": "os.nova.server", - "version": 1.0 + "version": 1.0, } self.addDetail('profile', content.text_content(profile_name)) # Test that we can create a profile and we get it returned - profile = self.user_cloud.create_cluster_profile(name=profile_name, - spec=spec) + profile = self.user_cloud.create_cluster_profile( + name=profile_name, spec=spec + ) self.addCleanup(self.cleanup_profile, profile['id']) @@ -686,8 +688,10 @@ def test_create_cluster_receiver(self): # Test that we can create a cluster and we get it returned cluster = self.user_cloud.create_cluster( - name=cluster_name, profile=profile, - desired_capacity=desired_capacity) + name=cluster_name, + profile=profile, + desired_capacity=desired_capacity, + ) self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) @@ -699,9 +703,11 @@ def test_create_cluster_receiver(self): # Test that we can create a receiver and we get it returned receiver = self.user_cloud.create_cluster_receiver( - name=receiver_name, receiver_type=receiver_type, + name=receiver_name, + receiver_type=receiver_type, cluster_name_or_id=cluster['cluster']['id'], - action='CLUSTER_SCALE_OUT') + action='CLUSTER_SCALE_OUT', + ) self.addCleanup(self.cleanup_receiver, receiver['id']) @@ -715,24 +721,19 @@ def test_list_cluster_receivers(self): "properties": { "flavor": self.flavor.name, "image": self.image.name, - "networks": [ - { - "network": "private" - } - ], - "security_groups": [ - "default" - ] + "networks": [{"network": "private"}], + "security_groups": ["default"], }, "type": "os.nova.server", - "version": 1.0 + "version": 1.0, } self.addDetail('profile', content.text_content(profile_name)) # Test that we can create a profile and we get it returned - profile = self.user_cloud.create_cluster_profile(name=profile_name, - spec=spec) + profile = self.user_cloud.create_cluster_profile( + name=profile_name, spec=spec + ) self.addCleanup(self.cleanup_profile, profile['id']) @@ -743,8 +744,10 @@ def test_list_cluster_receivers(self): # Test that we can create a cluster and we get it returned cluster = self.user_cloud.create_cluster( - name=cluster_name, profile=profile, - desired_capacity=desired_capacity) + name=cluster_name, + profile=profile, + desired_capacity=desired_capacity, + ) self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) @@ -756,14 +759,17 @@ def test_list_cluster_receivers(self): # Test that we can create a receiver and we get it returned receiver = self.user_cloud.create_cluster_receiver( - name=receiver_name, receiver_type=receiver_type, + name=receiver_name, + receiver_type=receiver_type, cluster_name_or_id=cluster['cluster']['id'], - action='CLUSTER_SCALE_OUT') + action='CLUSTER_SCALE_OUT', + ) self.addCleanup(self.cleanup_receiver, receiver['id']) get_receiver = self.user_cloud.get_cluster_receiver_by_id( - receiver['id']) + receiver['id'] + ) receiver_list = {"receivers": [get_receiver]} # Test that we can list receivers @@ -777,24 +783,19 @@ def test_delete_cluster(self): "properties": { "flavor": self.flavor.name, "image": self.image.name, - "networks": [ - { - "network": "private" - } - ], - "security_groups": [ - "default" - ] + "networks": [{"network": "private"}], + "security_groups": ["default"], }, "type": "os.nova.server", - "version": 1.0 + "version": 1.0, } self.addDetail('profile', content.text_content(profile_name)) # Test that we can create a profile and we get it returned - profile = self.user_cloud.create_cluster_profile(name=profile_name, - spec=spec) + profile = self.user_cloud.create_cluster_profile( + name=profile_name, spec=spec + ) self.addCleanup(self.cleanup_profile, profile['id']) @@ -805,8 +806,10 @@ def test_delete_cluster(self): # Test that we can create a cluster and we get it returned cluster = self.user_cloud.create_cluster( - name=cluster_name, profile=profile, - desired_capacity=desired_capacity) + name=cluster_name, + profile=profile, + desired_capacity=desired_capacity, + ) self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) @@ -816,31 +819,33 @@ def test_delete_cluster(self): "adjustment": { "min_step": 1, "number": 1, - "type": "CHANGE_IN_CAPACITY" + "type": "CHANGE_IN_CAPACITY", }, - "event": "CLUSTER_SCALE_IN" + "event": "CLUSTER_SCALE_IN", }, "type": "senlin.policy.scaling", - "version": "1.0" + "version": "1.0", } self.addDetail('policy', content.text_content(policy_name)) # Test that we can create a policy and we get it returned - policy = self.user_cloud.create_cluster_policy(name=policy_name, - spec=spec) + policy = self.user_cloud.create_cluster_policy( + name=policy_name, spec=spec + ) self.addCleanup(self.cleanup_policy, policy['id']) # Test that we can attach policy to cluster and get True returned attach_cluster = self.user_cloud.get_cluster_by_id( - cluster['cluster']['id']) - attach_policy = self.user_cloud.get_cluster_policy_by_id( - policy['id']) + cluster['cluster']['id'] + ) + attach_policy = self.user_cloud.get_cluster_policy_by_id(policy['id']) self.user_cloud.attach_policy_to_cluster( - attach_cluster, attach_policy, is_enabled=True) + attach_cluster, attach_policy, is_enabled=True + ) receiver_name = "example_receiver" receiver_type = "webhook" @@ -850,13 +855,16 @@ def test_delete_cluster(self): # Test that we can create a receiver and we get it returned self.user_cloud.create_cluster_receiver( - name=receiver_name, receiver_type=receiver_type, + name=receiver_name, + receiver_type=receiver_type, cluster_name_or_id=cluster['cluster']['id'], - action='CLUSTER_SCALE_OUT') + action='CLUSTER_SCALE_OUT', + ) # Test that we can delete cluster and get True returned cluster_delete = self.user_cloud.delete_cluster( - cluster['cluster']['id']) + cluster['cluster']['id'] + ) self.assertTrue(cluster_delete) def test_list_clusters(self): @@ -865,24 +873,19 @@ def test_list_clusters(self): "properties": { "flavor": self.flavor.name, "image": self.image.name, - "networks": [ - { - "network": "private" - } - ], - "security_groups": [ - "default" - ] + "networks": [{"network": "private"}], + "security_groups": ["default"], }, "type": "os.nova.server", - "version": 1.0 + "version": 1.0, } self.addDetail('profile', content.text_content(profile_name)) # Test that we can create a profile and we get it returned - profile = self.user_cloud.create_cluster_profile(name=profile_name, - spec=spec) + profile = self.user_cloud.create_cluster_profile( + name=profile_name, spec=spec + ) self.addCleanup(self.cleanup_profile, profile['id']) @@ -893,17 +896,23 @@ def test_list_clusters(self): # Test that we can create a cluster and we get it returned cluster = self.user_cloud.create_cluster( - name=cluster_name, profile=profile, - desired_capacity=desired_capacity) + name=cluster_name, + profile=profile, + desired_capacity=desired_capacity, + ) self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) wait = wait_for_status( self.user_cloud.get_cluster_by_id, - {'name_or_id': cluster['cluster']['id']}, 'status', 'ACTIVE') + {'name_or_id': cluster['cluster']['id']}, + 'status', + 'ACTIVE', + ) get_cluster = self.user_cloud.get_cluster_by_id( - cluster['cluster']['id']) + cluster['cluster']['id'] + ) # Test that we can list clusters clusters = self.user_cloud.list_clusters() @@ -916,24 +925,19 @@ def test_update_policy_on_cluster(self): "properties": { "flavor": self.flavor.name, "image": self.image.name, - "networks": [ - { - "network": "private" - } - ], - "security_groups": [ - "default" - ] + "networks": [{"network": "private"}], + "security_groups": ["default"], }, "type": "os.nova.server", - "version": 1.0 + "version": 1.0, } self.addDetail('profile', content.text_content(profile_name)) # Test that we can create a profile and we get it returned - profile = self.user_cloud.create_cluster_profile(name=profile_name, - spec=spec) + profile = self.user_cloud.create_cluster_profile( + name=profile_name, spec=spec + ) self.addCleanup(self.cleanup_profile, profile['id']) @@ -944,8 +948,10 @@ def test_update_policy_on_cluster(self): # Test that we can create a cluster and we get it returned cluster = self.user_cloud.create_cluster( - name=cluster_name, profile=profile, - desired_capacity=desired_capacity) + name=cluster_name, + profile=profile, + desired_capacity=desired_capacity, + ) self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) @@ -955,56 +961,68 @@ def test_update_policy_on_cluster(self): "adjustment": { "min_step": 1, "number": 1, - "type": "CHANGE_IN_CAPACITY" + "type": "CHANGE_IN_CAPACITY", }, - "event": "CLUSTER_SCALE_IN" + "event": "CLUSTER_SCALE_IN", }, "type": "senlin.policy.scaling", - "version": "1.0" + "version": "1.0", } self.addDetail('policy', content.text_content(policy_name)) # Test that we can create a policy and we get it returned - policy = self.user_cloud.create_cluster_policy(name=policy_name, - spec=spec) + policy = self.user_cloud.create_cluster_policy( + name=policy_name, spec=spec + ) - self.addCleanup(self.cleanup_policy, policy['id'], - cluster['cluster']['id']) + self.addCleanup( + self.cleanup_policy, policy['id'], cluster['cluster']['id'] + ) # Test that we can attach policy to cluster and get True returned attach_cluster = self.user_cloud.get_cluster_by_id( - cluster['cluster']['id']) - attach_policy = self.user_cloud.get_cluster_policy_by_id( - policy['id']) + cluster['cluster']['id'] + ) + attach_policy = self.user_cloud.get_cluster_policy_by_id(policy['id']) self.user_cloud.attach_policy_to_cluster( - attach_cluster, attach_policy, is_enabled=True) + attach_cluster, attach_policy, is_enabled=True + ) wait_attach = wait_for_status( self.user_cloud.get_cluster_by_id, - {'name_or_id': cluster['cluster']['id']}, 'policies', - ["{policy}".format(policy=policy['id'])]) + {'name_or_id': cluster['cluster']['id']}, + 'policies', + ["{policy}".format(policy=policy['id'])], + ) get_old_policy = self.user_cloud.get_policy_on_cluster( - cluster['cluster']["id"], policy['id']) + cluster['cluster']["id"], policy['id'] + ) # Test that we can update the policy on cluster policy_update = self.user_cloud.update_policy_on_cluster( - attach_cluster, attach_policy, is_enabled=False) + attach_cluster, attach_policy, is_enabled=False + ) get_old_policy.update({'enabled': False}) wait_update = wait_for_status( self.user_cloud.get_policy_on_cluster, - {'name_or_id': cluster['cluster']['id'], - 'policy_name_or_id': policy['id']}, 'enabled', - False) + { + 'name_or_id': cluster['cluster']['id'], + 'policy_name_or_id': policy['id'], + }, + 'enabled', + False, + ) get_new_policy = self.user_cloud.get_policy_on_cluster( - cluster['cluster']["id"], policy['id']) + cluster['cluster']["id"], policy['id'] + ) get_old_policy['last_op'] = None get_new_policy['last_op'] = None @@ -1020,31 +1038,28 @@ def test_list_cluster_profiles(self): "properties": { "flavor": self.flavor.name, "image": self.image.name, - "networks": [ - { - "network": "private" - } - ], - "security_groups": [ - "default" - ] + "networks": [{"network": "private"}], + "security_groups": ["default"], }, "type": "os.nova.server", - "version": 1.0 + "version": 1.0, } self.addDetail('profile', content.text_content(profile_name)) # Test that we can create a profile and we get it returned - profile = self.user_cloud.create_cluster_profile(name=profile_name, - spec=spec) + profile = self.user_cloud.create_cluster_profile( + name=profile_name, spec=spec + ) self.addCleanup(self.cleanup_profile, profile['id']) # Test that we can list profiles - wait = wait_for_create(self.user_cloud.get_cluster_profile_by_id, - {'name_or_id': profile['id']}) + wait = wait_for_create( + self.user_cloud.get_cluster_profile_by_id, + {'name_or_id': profile['id']}, + ) get_profile = self.user_cloud.get_cluster_profile_by_id(profile['id']) @@ -1058,24 +1073,19 @@ def test_get_cluster_profile_by_id(self): "properties": { "flavor": self.flavor.name, "image": self.image.name, - "networks": [ - { - "network": "private" - } - ], - "security_groups": [ - "default" - ] + "networks": [{"network": "private"}], + "security_groups": ["default"], }, "type": "os.nova.server", - "version": 1.0 + "version": 1.0, } self.addDetail('profile', content.text_content(profile_name)) # Test that we can create a profile and we get it returned - profile = self.user_cloud.create_cluster_profile(name=profile_name, - spec=spec) + profile = self.user_cloud.create_cluster_profile( + name=profile_name, spec=spec + ) self.addCleanup(self.cleanup_profile, profile['id']) @@ -1096,24 +1106,19 @@ def test_update_cluster_profile(self): "properties": { "flavor": self.flavor.name, "image": self.image.name, - "networks": [ - { - "network": "private" - } - ], - "security_groups": [ - "default" - ] + "networks": [{"network": "private"}], + "security_groups": ["default"], }, "type": "os.nova.server", - "version": 1.0 + "version": 1.0, } self.addDetail('profile', content.text_content(profile_name)) # Test that we can create a profile and we get it returned - profile = self.user_cloud.create_cluster_profile(name=profile_name, - spec=spec) + profile = self.user_cloud.create_cluster_profile( + name=profile_name, spec=spec + ) self.addCleanup(self.cleanup_profile, profile['id']) @@ -1121,7 +1126,8 @@ def test_update_cluster_profile(self): # is updated profile_update = self.user_cloud.update_cluster_profile( - profile['id'], new_name='new_profile_name') + profile['id'], new_name='new_profile_name' + ) self.assertEqual(profile_update['profile']['id'], profile['id']) self.assertEqual(profile_update['profile']['spec'], profile['spec']) self.assertEqual(profile_update['profile']['name'], 'new_profile_name') @@ -1132,24 +1138,19 @@ def test_delete_cluster_profile(self): "properties": { "flavor": self.flavor.name, "image": self.image.name, - "networks": [ - { - "network": "private" - } - ], - "security_groups": [ - "default" - ] + "networks": [{"network": "private"}], + "security_groups": ["default"], }, "type": "os.nova.server", - "version": 1.0 + "version": 1.0, } self.addDetail('profile', content.text_content(profile_name)) # Test that we can create a profile and we get it returned - profile = self.user_cloud.create_cluster_profile(name=profile_name, - spec=spec) + profile = self.user_cloud.create_cluster_profile( + name=profile_name, spec=spec + ) self.addCleanup(self.cleanup_profile, profile['id']) @@ -1164,20 +1165,21 @@ def test_list_cluster_policies(self): "adjustment": { "min_step": 1, "number": 1, - "type": "CHANGE_IN_CAPACITY" + "type": "CHANGE_IN_CAPACITY", }, - "event": "CLUSTER_SCALE_IN" + "event": "CLUSTER_SCALE_IN", }, "type": "senlin.policy.scaling", - "version": "1.0" + "version": "1.0", } self.addDetail('policy', content.text_content(policy_name)) # Test that we can create a policy and we get it returned - policy = self.user_cloud.create_cluster_policy(name=policy_name, - spec=spec) + policy = self.user_cloud.create_cluster_policy( + name=policy_name, spec=spec + ) self.addCleanup(self.cleanup_policy, policy['id']) @@ -1202,20 +1204,21 @@ def test_get_cluster_policy_by_id(self): "adjustment": { "min_step": 1, "number": 1, - "type": "CHANGE_IN_CAPACITY" + "type": "CHANGE_IN_CAPACITY", }, - "event": "CLUSTER_SCALE_IN" + "event": "CLUSTER_SCALE_IN", }, "type": "senlin.policy.scaling", - "version": "1.0" + "version": "1.0", } self.addDetail('policy', content.text_content(policy_name)) # Test that we can create a policy and we get it returned - policy = self.user_cloud.create_cluster_policy(name=policy_name, - spec=spec) + policy = self.user_cloud.create_cluster_policy( + name=policy_name, spec=spec + ) self.addCleanup(self.cleanup_policy, policy['id']) @@ -1238,20 +1241,21 @@ def test_update_cluster_policy(self): "adjustment": { "min_step": 1, "number": 1, - "type": "CHANGE_IN_CAPACITY" + "type": "CHANGE_IN_CAPACITY", }, - "event": "CLUSTER_SCALE_IN" + "event": "CLUSTER_SCALE_IN", }, "type": "senlin.policy.scaling", - "version": "1.0" + "version": "1.0", } self.addDetail('policy', content.text_content(policy_name)) # Test that we can create a policy and we get it returned - policy = self.user_cloud.create_cluster_policy(name=policy_name, - spec=spec) + policy = self.user_cloud.create_cluster_policy( + name=policy_name, spec=spec + ) self.addCleanup(self.cleanup_policy, policy['id']) @@ -1259,7 +1263,8 @@ def test_update_cluster_policy(self): # is updated policy_update = self.user_cloud.update_cluster_policy( - policy['id'], new_name='new_policy_name') + policy['id'], new_name='new_policy_name' + ) self.assertEqual(policy_update['policy']['id'], policy['id']) self.assertEqual(policy_update['policy']['spec'], policy['spec']) self.assertEqual(policy_update['policy']['name'], 'new_policy_name') @@ -1271,26 +1276,26 @@ def test_delete_cluster_policy(self): "adjustment": { "min_step": 1, "number": 1, - "type": "CHANGE_IN_CAPACITY" + "type": "CHANGE_IN_CAPACITY", }, - "event": "CLUSTER_SCALE_IN" + "event": "CLUSTER_SCALE_IN", }, "type": "senlin.policy.scaling", - "version": "1.0" + "version": "1.0", } self.addDetail('policy', content.text_content(policy_name)) # Test that we can create a policy and we get it returned - policy = self.user_cloud.create_cluster_policy(name=policy_name, - spec=spec) + policy = self.user_cloud.create_cluster_policy( + name=policy_name, spec=spec + ) self.addCleanup(self.cleanup_policy, policy['id']) # Test that we can delete a policy and get True returned - policy_delete = self.user_cloud.delete_cluster_policy( - policy['id']) + policy_delete = self.user_cloud.delete_cluster_policy(policy['id']) self.assertTrue(policy_delete) def test_get_cluster_receiver_by_id(self): @@ -1299,24 +1304,19 @@ def test_get_cluster_receiver_by_id(self): "properties": { "flavor": self.flavor.name, "image": self.image.name, - "networks": [ - { - "network": "private" - } - ], - "security_groups": [ - "default" - ] + "networks": [{"network": "private"}], + "security_groups": ["default"], }, "type": "os.nova.server", - "version": 1.0 + "version": 1.0, } self.addDetail('profile', content.text_content(profile_name)) # Test that we can create a profile and we get it returned - profile = self.user_cloud.create_cluster_profile(name=profile_name, - spec=spec) + profile = self.user_cloud.create_cluster_profile( + name=profile_name, spec=spec + ) self.addCleanup(self.cleanup_profile, profile['id']) @@ -1327,8 +1327,10 @@ def test_get_cluster_receiver_by_id(self): # Test that we can create a cluster and we get it returned cluster = self.user_cloud.create_cluster( - name=cluster_name, profile=profile, - desired_capacity=desired_capacity) + name=cluster_name, + profile=profile, + desired_capacity=desired_capacity, + ) self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) @@ -1340,16 +1342,19 @@ def test_get_cluster_receiver_by_id(self): # Test that we can create a receiver and we get it returned receiver = self.user_cloud.create_cluster_receiver( - name=receiver_name, receiver_type=receiver_type, + name=receiver_name, + receiver_type=receiver_type, cluster_name_or_id=cluster['cluster']['id'], - action='CLUSTER_SCALE_OUT') + action='CLUSTER_SCALE_OUT', + ) self.addCleanup(self.cleanup_receiver, receiver['id']) # Test that we get the same receiver with the get_receiver method receiver_get = self.user_cloud.get_cluster_receiver_by_id( - receiver['id']) + receiver['id'] + ) self.assertEqual(receiver_get['id'], receiver["id"]) def test_update_cluster_receiver(self): @@ -1358,24 +1363,19 @@ def test_update_cluster_receiver(self): "properties": { "flavor": self.flavor.name, "image": self.image.name, - "networks": [ - { - "network": "private" - } - ], - "security_groups": [ - "default" - ] + "networks": [{"network": "private"}], + "security_groups": ["default"], }, "type": "os.nova.server", - "version": 1.0 + "version": 1.0, } self.addDetail('profile', content.text_content(profile_name)) # Test that we can create a profile and we get it returned - profile = self.user_cloud.create_cluster_profile(name=profile_name, - spec=spec) + profile = self.user_cloud.create_cluster_profile( + name=profile_name, spec=spec + ) self.addCleanup(self.cleanup_profile, profile['id']) @@ -1386,8 +1386,10 @@ def test_update_cluster_receiver(self): # Test that we can create a cluster and we get it returned cluster = self.user_cloud.create_cluster( - name=cluster_name, profile=profile, - desired_capacity=desired_capacity) + name=cluster_name, + profile=profile, + desired_capacity=desired_capacity, + ) self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) @@ -1399,9 +1401,11 @@ def test_update_cluster_receiver(self): # Test that we can create a receiver and we get it returned receiver = self.user_cloud.create_cluster_receiver( - name=receiver_name, receiver_type=receiver_type, + name=receiver_name, + receiver_type=receiver_type, cluster_name_or_id=cluster['cluster']['id'], - action='CLUSTER_SCALE_OUT') + action='CLUSTER_SCALE_OUT', + ) self.addCleanup(self.cleanup_receiver, receiver['id']) @@ -1409,13 +1413,16 @@ def test_update_cluster_receiver(self): # is updated receiver_update = self.user_cloud.update_cluster_receiver( - receiver['id'], new_name='new_receiver_name') + receiver['id'], new_name='new_receiver_name' + ) self.assertEqual(receiver_update['receiver']['id'], receiver['id']) self.assertEqual(receiver_update['receiver']['type'], receiver['type']) - self.assertEqual(receiver_update['receiver']['cluster_id'], - receiver['cluster_id']) - self.assertEqual(receiver_update['receiver']['name'], - 'new_receiver_name') + self.assertEqual( + receiver_update['receiver']['cluster_id'], receiver['cluster_id'] + ) + self.assertEqual( + receiver_update['receiver']['name'], 'new_receiver_name' + ) def cleanup_profile(self, name): time.sleep(5) @@ -1431,8 +1438,9 @@ def cleanup_policy(self, name, cluster_name=None): if cluster_name is not None: cluster = self.user_cloud.get_cluster_by_id(cluster_name) policy = self.user_cloud.get_cluster_policy_by_id(name) - policy_status = \ - self.user_cloud.get_cluster_by_id(cluster['id'])['policies'] + policy_status = self.user_cloud.get_cluster_by_id(cluster['id'])[ + 'policies' + ] if policy_status != []: self.user_cloud.detach_policy_from_cluster(cluster, policy) self.user_cloud.delete_cluster_policy(name) diff --git a/openstack/tests/functional/cloud/test_compute.py b/openstack/tests/functional/cloud/test_compute.py index c97ef5fe6..becb67cce 100644 --- a/openstack/tests/functional/cloud/test_compute.py +++ b/openstack/tests/functional/cloud/test_compute.py @@ -59,7 +59,8 @@ def _cleanup_servers_and_volumes(self, server_name): self.user_cloud.delete_server(server.name) for volume in volumes: self.operator_cloud.delete_volume( - volume.id, wait=False, force=True) + volume.id, wait=False, force=True + ) def test_create_and_delete_server(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) @@ -67,13 +68,15 @@ def test_create_and_delete_server(self): name=self.server_name, image=self.image, flavor=self.flavor, - wait=True) + wait=True, + ) self.assertEqual(self.server_name, server['name']) self.assertEqual(self.image.id, server['image']['id']) self.assertEqual(self.flavor.name, server['flavor']['original_name']) self.assertIsNotNone(server['adminPass']) self.assertTrue( - self.user_cloud.delete_server(self.server_name, wait=True)) + self.user_cloud.delete_server(self.server_name, wait=True) + ) srv = self.user_cloud.get_server(self.server_name) self.assertTrue(srv is None or srv.status.lower() == 'deleted') @@ -84,14 +87,17 @@ def test_create_and_delete_server_auto_ip_delete_ips(self): image=self.image, flavor=self.flavor, auto_ip=True, - wait=True) + wait=True, + ) self.assertEqual(self.server_name, server['name']) self.assertEqual(self.image.id, server['image']['id']) self.assertEqual(self.flavor.name, server['flavor']['original_name']) self.assertIsNotNone(server['adminPass']) self.assertTrue( self.user_cloud.delete_server( - self.server_name, wait=True, delete_ips=True)) + self.server_name, wait=True, delete_ips=True + ) + ) srv = self.user_cloud.get_server(self.server_name) self.assertTrue(srv is None or srv.status.lower() == 'deleted') @@ -100,8 +106,8 @@ def test_attach_detach_volume(self): server_name = self.getUniqueString() self.addCleanup(self._cleanup_servers_and_volumes, server_name) server = self.user_cloud.create_server( - name=server_name, image=self.image, flavor=self.flavor, - wait=True) + name=server_name, image=self.image, flavor=self.flavor, wait=True + ) volume = self.user_cloud.create_volume(1) vol_attachment = self.user_cloud.attach_volume(server, volume) for key in ('device', 'serverId', 'volumeId'): @@ -116,14 +122,16 @@ def test_create_and_delete_server_with_config_drive(self): image=self.image, flavor=self.flavor, config_drive=True, - wait=True) + wait=True, + ) self.assertEqual(self.server_name, server['name']) self.assertEqual(self.image.id, server['image']['id']) self.assertEqual(self.flavor.name, server['flavor']['original_name']) self.assertTrue(server['has_config_drive']) self.assertIsNotNone(server['adminPass']) self.assertTrue( - self.user_cloud.delete_server(self.server_name, wait=True)) + self.user_cloud.delete_server(self.server_name, wait=True) + ) srv = self.user_cloud.get_server(self.server_name) self.assertTrue(srv is None or srv.status.lower() == 'deleted') @@ -137,15 +145,16 @@ def test_create_and_delete_server_with_config_drive_none(self): image=self.image, flavor=self.flavor, config_drive=None, - wait=True) + wait=True, + ) self.assertEqual(self.server_name, server['name']) self.assertEqual(self.image.id, server['image']['id']) self.assertEqual(self.flavor.name, server['flavor']['original_name']) self.assertFalse(server['has_config_drive']) self.assertIsNotNone(server['adminPass']) self.assertTrue( - self.user_cloud.delete_server( - self.server_name, wait=True)) + self.user_cloud.delete_server(self.server_name, wait=True) + ) srv = self.user_cloud.get_server(self.server_name) self.assertTrue(srv is None or srv.status.lower() == 'deleted') @@ -157,7 +166,8 @@ def test_list_all_servers(self): name=self.server_name, image=self.image, flavor=self.flavor, - wait=True) + wait=True, + ) # We're going to get servers from other tests, but that's ok, as long # as we get the server we created with the demo user. found_server = False @@ -171,7 +181,8 @@ def test_list_all_servers_bad_permissions(self): self.assertRaises( exc.OpenStackCloudException, self.user_cloud.list_servers, - all_projects=True) + all_projects=True, + ) def test_create_server_image_flavor_dict(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) @@ -179,13 +190,15 @@ def test_create_server_image_flavor_dict(self): name=self.server_name, image={'id': self.image.id}, flavor={'id': self.flavor.id}, - wait=True) + wait=True, + ) self.assertEqual(self.server_name, server['name']) self.assertEqual(self.image.id, server['image']['id']) self.assertEqual(self.flavor.name, server['flavor']['original_name']) self.assertIsNotNone(server['adminPass']) self.assertTrue( - self.user_cloud.delete_server(self.server_name, wait=True)) + self.user_cloud.delete_server(self.server_name, wait=True) + ) srv = self.user_cloud.get_server(self.server_name) self.assertTrue(srv is None or srv.status.lower() == 'deleted') @@ -195,7 +208,8 @@ def test_get_server_console(self): name=self.server_name, image=self.image, flavor=self.flavor, - wait=True) + wait=True, + ) # _get_server_console_output does not trap HTTP exceptions, so this # returning a string tests that the call is correct. Testing that # the cloud returns actual data in the output is out of scope. @@ -208,19 +222,22 @@ def test_get_server_console_name_or_id(self): name=self.server_name, image=self.image, flavor=self.flavor, - wait=True) + wait=True, + ) log = self.user_cloud.get_server_console(server=self.server_name) self.assertIsInstance(log, str) def test_list_availability_zone_names(self): self.assertEqual( - ['nova'], self.user_cloud.list_availability_zone_names()) + ['nova'], self.user_cloud.list_availability_zone_names() + ) def test_get_server_console_bad_server(self): self.assertRaises( exc.OpenStackCloudException, self.user_cloud.get_server_console, - server=self.server_name) + server=self.server_name, + ) def test_create_and_delete_server_with_admin_pass(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) @@ -229,27 +246,33 @@ def test_create_and_delete_server_with_admin_pass(self): image=self.image, flavor=self.flavor, admin_pass='sheiqu9loegahSh', - wait=True) + wait=True, + ) self.assertEqual(self.server_name, server['name']) self.assertEqual(self.image.id, server['image']['id']) self.assertEqual(self.flavor.name, server['flavor']['original_name']) self.assertEqual(server['adminPass'], 'sheiqu9loegahSh') self.assertTrue( - self.user_cloud.delete_server(self.server_name, wait=True)) + self.user_cloud.delete_server(self.server_name, wait=True) + ) srv = self.user_cloud.get_server(self.server_name) self.assertTrue(srv is None or srv.status.lower() == 'deleted') def test_get_image_id(self): self.assertEqual( - self.image.id, self.user_cloud.get_image_id(self.image.id)) + self.image.id, self.user_cloud.get_image_id(self.image.id) + ) self.assertEqual( - self.image.id, self.user_cloud.get_image_id(self.image.name)) + self.image.id, self.user_cloud.get_image_id(self.image.name) + ) def test_get_image_name(self): self.assertEqual( - self.image.name, self.user_cloud.get_image_name(self.image.id)) + self.image.name, self.user_cloud.get_image_name(self.image.id) + ) self.assertEqual( - self.image.name, self.user_cloud.get_image_name(self.image.name)) + self.image.name, self.user_cloud.get_image_name(self.image.name) + ) def _assert_volume_attach(self, server, volume_id=None, image=''): self.assertEqual(self.server_name, server['name']) @@ -277,7 +300,8 @@ def test_create_boot_from_volume_image(self): flavor=self.flavor, boot_from_volume=True, volume_size=1, - wait=True) + wait=True, + ) volume_id = self._assert_volume_attach(server) volume = self.user_cloud.get_volume(volume_id) self.assertIsNotNone(volume) @@ -296,13 +320,18 @@ def _wait_for_detach(self, volume_id): # deleting a server that had had a volume attached. Yay for eventual # consistency! for count in utils.iterate_timeout( - 60, - 'Timeout waiting for volume {volume_id} to detach'.format( - volume_id=volume_id)): + 60, + 'Timeout waiting for volume {volume_id} to detach'.format( + volume_id=volume_id + ), + ): volume = self.user_cloud.get_volume(volume_id) if volume.status in ( - 'available', 'error', - 'error_restoring', 'error_extending'): + 'available', + 'error', + 'error_restoring', + 'error_extending', + ): return def test_create_terminate_volume_image(self): @@ -317,10 +346,12 @@ def test_create_terminate_volume_image(self): boot_from_volume=True, terminate_volume=True, volume_size=1, - wait=True) + wait=True, + ) volume_id = self._assert_volume_attach(server) self.assertTrue( - self.user_cloud.delete_server(self.server_name, wait=True)) + self.user_cloud.delete_server(self.server_name, wait=True) + ) volume = self.user_cloud.get_volume(volume_id) # We can either get None (if the volume delete was quick), or a volume # that is in the process of being deleted. @@ -335,7 +366,8 @@ def test_create_boot_from_volume_preexisting(self): self.skipTest('volume service not supported by cloud') self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) volume = self.user_cloud.create_volume( - size=1, name=self.server_name, image=self.image, wait=True) + size=1, name=self.server_name, image=self.image, wait=True + ) self.addCleanup(self.user_cloud.delete_volume, volume.id) server = self.user_cloud.create_server( name=self.server_name, @@ -343,10 +375,12 @@ def test_create_boot_from_volume_preexisting(self): flavor=self.flavor, boot_volume=volume, volume_size=1, - wait=True) + wait=True, + ) volume_id = self._assert_volume_attach(server, volume_id=volume['id']) self.assertTrue( - self.user_cloud.delete_server(self.server_name, wait=True)) + self.user_cloud.delete_server(self.server_name, wait=True) + ) volume = self.user_cloud.get_volume(volume_id) self.assertIsNotNone(volume) self.assertEqual(volume['name'], volume['display_name']) @@ -364,7 +398,8 @@ def test_create_boot_attach_volume(self): self.skipTest('volume service not supported by cloud') self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) volume = self.user_cloud.create_volume( - size=1, name=self.server_name, image=self.image, wait=True) + size=1, name=self.server_name, image=self.image, wait=True + ) self.addCleanup(self.user_cloud.delete_volume, volume['id']) server = self.user_cloud.create_server( name=self.server_name, @@ -372,11 +407,14 @@ def test_create_boot_attach_volume(self): image=self.image, boot_from_volume=False, volumes=[volume], - wait=True) + wait=True, + ) volume_id = self._assert_volume_attach( - server, volume_id=volume['id'], image={'id': self.image['id']}) + server, volume_id=volume['id'], image={'id': self.image['id']} + ) self.assertTrue( - self.user_cloud.delete_server(self.server_name, wait=True)) + self.user_cloud.delete_server(self.server_name, wait=True) + ) volume = self.user_cloud.get_volume(volume_id) self.assertIsNotNone(volume) self.assertEqual(volume['name'], volume['display_name']) @@ -393,7 +431,8 @@ def test_create_boot_from_volume_preexisting_terminate(self): self.skipTest('volume service not supported by cloud') self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) volume = self.user_cloud.create_volume( - size=1, name=self.server_name, image=self.image, wait=True) + size=1, name=self.server_name, image=self.image, wait=True + ) server = self.user_cloud.create_server( name=self.server_name, image=None, @@ -401,10 +440,12 @@ def test_create_boot_from_volume_preexisting_terminate(self): boot_volume=volume, terminate_volume=True, volume_size=1, - wait=True) + wait=True, + ) volume_id = self._assert_volume_attach(server, volume_id=volume['id']) self.assertTrue( - self.user_cloud.delete_server(self.server_name, wait=True)) + self.user_cloud.delete_server(self.server_name, wait=True) + ) volume = self.user_cloud.get_volume(volume_id) # We can either get None (if the volume delete was quick), or a volume # that is in the process of being deleted. @@ -420,9 +461,11 @@ def test_create_image_snapshot_wait_active(self): image=self.image, flavor=self.flavor, admin_pass='sheiqu9loegahSh', - wait=True) - image = self.user_cloud.create_image_snapshot('test-snapshot', server, - wait=True) + wait=True, + ) + image = self.user_cloud.create_image_snapshot( + 'test-snapshot', server, wait=True + ) self.addCleanup(self.user_cloud.delete_image, image['id']) self.assertEqual('active', image['status']) @@ -432,24 +475,32 @@ def test_set_and_delete_metadata(self): name=self.server_name, image=self.image, flavor=self.flavor, - wait=True) - self.user_cloud.set_server_metadata(self.server_name, - {'key1': 'value1', - 'key2': 'value2'}) + wait=True, + ) + self.user_cloud.set_server_metadata( + self.server_name, {'key1': 'value1', 'key2': 'value2'} + ) updated_server = self.user_cloud.get_server(self.server_name) - self.assertEqual(set(updated_server.metadata.items()), - set({'key1': 'value1', 'key2': 'value2'}.items())) + self.assertEqual( + set(updated_server.metadata.items()), + set({'key1': 'value1', 'key2': 'value2'}.items()), + ) - self.user_cloud.set_server_metadata(self.server_name, - {'key2': 'value3'}) + self.user_cloud.set_server_metadata( + self.server_name, {'key2': 'value3'} + ) updated_server = self.user_cloud.get_server(self.server_name) - self.assertEqual(set(updated_server.metadata.items()), - set({'key1': 'value1', 'key2': 'value3'}.items())) + self.assertEqual( + set(updated_server.metadata.items()), + set({'key1': 'value1', 'key2': 'value3'}.items()), + ) self.user_cloud.delete_server_metadata(self.server_name, ['key2']) updated_server = self.user_cloud.get_server(self.server_name) - self.assertEqual(set(updated_server.metadata.items()), - set({'key1': 'value1'}.items())) + self.assertEqual( + set(updated_server.metadata.items()), + set({'key1': 'value1'}.items()), + ) self.user_cloud.delete_server_metadata(self.server_name, ['key1']) updated_server = self.user_cloud.get_server(self.server_name) @@ -458,7 +509,9 @@ def test_set_and_delete_metadata(self): self.assertRaises( exc.OpenStackCloudURINotFound, self.user_cloud.delete_server_metadata, - self.server_name, ['key1']) + self.server_name, + ['key1'], + ) def test_update_server(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) @@ -466,10 +519,10 @@ def test_update_server(self): name=self.server_name, image=self.image, flavor=self.flavor, - wait=True) + wait=True, + ) server_updated = self.user_cloud.update_server( - self.server_name, - name='new_name' + self.server_name, name='new_name' ) self.assertEqual('new_name', server_updated['name']) @@ -484,7 +537,8 @@ def test_get_compute_usage(self): name=self.server_name, image=self.image, flavor=self.flavor, - wait=True) + wait=True, + ) start = datetime.datetime.now() - datetime.timedelta(seconds=5) usage = self.operator_cloud.get_compute_usage('demo', start) self.add_info_on_exception('usage', usage) diff --git a/openstack/tests/functional/cloud/test_devstack.py b/openstack/tests/functional/cloud/test_devstack.py index 52cced22a..a6754009a 100644 --- a/openstack/tests/functional/cloud/test_devstack.py +++ b/openstack/tests/functional/cloud/test_devstack.py @@ -30,16 +30,18 @@ class TestDevstack(base.BaseFunctionalTest): scenarios = [ ('designate', dict(env='DESIGNATE', service='dns')), ('heat', dict(env='HEAT', service='orchestration')), - ('magnum', dict( - env='MAGNUM', - service='container-infrastructure-management' - )), + ( + 'magnum', + dict(env='MAGNUM', service='container-infrastructure-management'), + ), ('neutron', dict(env='NEUTRON', service='network')), ('octavia', dict(env='OCTAVIA', service='load-balancer')), ('swift', dict(env='SWIFT', service='object-store')), ] def test_has_service(self): - if os.environ.get( - 'OPENSTACKSDK_HAS_{env}'.format(env=self.env), '0') == '1': + if ( + os.environ.get('OPENSTACKSDK_HAS_{env}'.format(env=self.env), '0') + == '1' + ): self.assertTrue(self.user_cloud.has_service(self.service)) diff --git a/openstack/tests/functional/cloud/test_domain.py b/openstack/tests/functional/cloud/test_domain.py index 447864356..f5f2ea210 100644 --- a/openstack/tests/functional/cloud/test_domain.py +++ b/openstack/tests/functional/cloud/test_domain.py @@ -22,7 +22,6 @@ class TestDomain(base.BaseFunctionalTest): - def setUp(self): super(TestDomain, self).setUp() if not self.operator_cloud: @@ -47,14 +46,16 @@ def _cleanup_domains(self): # Raise an error: we must make users aware that something went # wrong raise openstack.cloud.OpenStackCloudException( - '\n'.join(exception_list)) + '\n'.join(exception_list) + ) def test_search_domains(self): domain_name = self.domain_prefix + '_search' # Shouldn't find any domain with this name yet results = self.operator_cloud.search_domains( - filters=dict(name=domain_name)) + filters=dict(name=domain_name) + ) self.assertEqual(0, len(results)) # Now create a new domain @@ -63,7 +64,8 @@ def test_search_domains(self): # Now we should find only the new domain results = self.operator_cloud.search_domains( - filters=dict(name=domain_name)) + filters=dict(name=domain_name) + ) self.assertEqual(1, len(results)) self.assertEqual(domain_name, results[0]['name']) @@ -74,13 +76,17 @@ def test_search_domains(self): def test_update_domain(self): domain = self.operator_cloud.create_domain( - self.domain_prefix, 'description') + self.domain_prefix, 'description' + ) self.assertEqual(self.domain_prefix, domain['name']) self.assertEqual('description', domain['description']) self.assertTrue(domain['enabled']) updated = self.operator_cloud.update_domain( - domain['id'], name='updated name', - description='updated description', enabled=False) + domain['id'], + name='updated name', + description='updated description', + enabled=False, + ) self.assertEqual('updated name', updated['name']) self.assertEqual('updated description', updated['description']) self.assertFalse(updated['enabled']) @@ -91,14 +97,16 @@ def test_update_domain(self): name_or_id='updated name', name='updated name 2', description='updated description 2', - enabled=True) + enabled=True, + ) self.assertEqual('updated name 2', updated['name']) self.assertEqual('updated description 2', updated['description']) self.assertTrue(updated['enabled']) def test_delete_domain(self): - domain = self.operator_cloud.create_domain(self.domain_prefix, - 'description') + domain = self.operator_cloud.create_domain( + self.domain_prefix, 'description' + ) self.assertEqual(self.domain_prefix, domain['name']) self.assertEqual('description', domain['description']) self.assertTrue(domain['enabled']) @@ -107,7 +115,8 @@ def test_delete_domain(self): # Now we delete domain by name with name_or_id domain = self.operator_cloud.create_domain( - self.domain_prefix, 'description') + self.domain_prefix, 'description' + ) self.assertEqual(self.domain_prefix, domain['name']) self.assertEqual('description', domain['description']) self.assertTrue(domain['enabled']) @@ -117,7 +126,8 @@ def test_delete_domain(self): # Finally, we assert we get False from delete_domain if domain does # not exist domain = self.operator_cloud.create_domain( - self.domain_prefix, 'description') + self.domain_prefix, 'description' + ) self.assertEqual(self.domain_prefix, domain['name']) self.assertEqual('description', domain['description']) self.assertTrue(domain['enabled']) diff --git a/openstack/tests/functional/cloud/test_endpoints.py b/openstack/tests/functional/cloud/test_endpoints.py index 2381f9775..d27dd9dda 100644 --- a/openstack/tests/functional/cloud/test_endpoints.py +++ b/openstack/tests/functional/cloud/test_endpoints.py @@ -29,8 +29,14 @@ class TestEndpoints(base.KeystoneBaseFunctionalTest): - endpoint_attributes = ['id', 'region', 'publicurl', 'internalurl', - 'service_id', 'adminurl'] + endpoint_attributes = [ + 'id', + 'region', + 'publicurl', + 'internalurl', + 'service_id', + 'adminurl', + ] def setUp(self): super(TestEndpoints, self).setUp() @@ -39,7 +45,8 @@ def setUp(self): # Generate a random name for services and regions in this test self.new_item_name = 'test_' + ''.join( - random.choice(string.ascii_lowercase) for _ in range(5)) + random.choice(string.ascii_lowercase) for _ in range(5) + ) self.addCleanup(self._cleanup_services) self.addCleanup(self._cleanup_endpoints) @@ -47,8 +54,9 @@ def setUp(self): def _cleanup_endpoints(self): exception_list = list() for e in self.operator_cloud.list_endpoints(): - if e.get('region') is not None and \ - e['region'].startswith(self.new_item_name): + if e.get('region') is not None and e['region'].startswith( + self.new_item_name + ): try: self.operator_cloud.delete_endpoint(id=e['id']) except Exception as e: @@ -63,8 +71,9 @@ def _cleanup_endpoints(self): def _cleanup_services(self): exception_list = list() for s in self.operator_cloud.list_services(): - if s['name'] is not None and \ - s['name'].startswith(self.new_item_name): + if s['name'] is not None and s['name'].startswith( + self.new_item_name + ): try: self.operator_cloud.delete_service(name_or_id=s['id']) except Exception as e: @@ -82,15 +91,18 @@ def test_create_endpoint(self): region = list(self.operator_cloud.identity.regions())[0].id service = self.operator_cloud.create_service( - name=service_name, type='test_type', - description='this is a test description') + name=service_name, + type='test_type', + description='this is a test description', + ) endpoints = self.operator_cloud.create_endpoint( service_name_or_id=service['id'], public_url='http://public.test/', internal_url='http://internal.test/', admin_url='http://admin.url/', - region=region) + region=region, + ) self.assertNotEqual([], endpoints) self.assertIsNotNone(endpoints[0].get('id')) @@ -99,7 +111,8 @@ def test_create_endpoint(self): endpoints = self.operator_cloud.create_endpoint( service_name_or_id=service['id'], public_url='http://public.test/', - region=region) + region=region, + ) self.assertNotEqual([], endpoints) self.assertIsNotNone(endpoints[0].get('id')) @@ -108,32 +121,38 @@ def test_update_endpoint(self): ver = self.operator_cloud.config.get_api_version('identity') if ver.startswith('2'): # NOTE(SamYaple): Update endpoint only works with v3 api - self.assertRaises(OpenStackCloudUnavailableFeature, - self.operator_cloud.update_endpoint, - 'endpoint_id1') + self.assertRaises( + OpenStackCloudUnavailableFeature, + self.operator_cloud.update_endpoint, + 'endpoint_id1', + ) else: # service operations require existing region. Do not test updating # region for now region = list(self.operator_cloud.identity.regions())[0].id service = self.operator_cloud.create_service( - name='service1', type='test_type') + name='service1', type='test_type' + ) endpoint = self.operator_cloud.create_endpoint( service_name_or_id=service['id'], url='http://admin.url/', interface='admin', region=region, - enabled=False)[0] + enabled=False, + )[0] new_service = self.operator_cloud.create_service( - name='service2', type='test_type') + name='service2', type='test_type' + ) new_endpoint = self.operator_cloud.update_endpoint( endpoint.id, service_name_or_id=new_service.id, url='http://public.url/', interface='public', region=region, - enabled=True) + enabled=True, + ) self.assertEqual(new_endpoint.url, 'http://public.url/') self.assertEqual(new_endpoint.interface, 'public') @@ -147,14 +166,17 @@ def test_list_endpoints(self): region = list(self.operator_cloud.identity.regions())[0].id service = self.operator_cloud.create_service( - name=service_name, type='test_type', - description='this is a test description') + name=service_name, + type='test_type', + description='this is a test description', + ) endpoints = self.operator_cloud.create_endpoint( service_name_or_id=service['id'], public_url='http://public.test/', internal_url='http://internal.test/', - region=region) + region=region, + ) observed_endpoints = self.operator_cloud.list_endpoints() found = False @@ -170,10 +192,10 @@ def test_list_endpoints(self): elif e['interface'] == 'public': self.assertEqual('http://public.test/', e['url']) else: - self.assertEqual('http://public.test/', - e['publicurl']) - self.assertEqual('http://internal.test/', - e['internalurl']) + self.assertEqual('http://public.test/', e['publicurl']) + self.assertEqual( + 'http://internal.test/', e['internalurl'] + ) self.assertEqual(region, e['region_id']) self.assertTrue(found, msg='new endpoint not found in endpoints list!') @@ -184,14 +206,17 @@ def test_delete_endpoint(self): region = list(self.operator_cloud.identity.regions())[0].id service = self.operator_cloud.create_service( - name=service_name, type='test_type', - description='this is a test description') + name=service_name, + type='test_type', + description='this is a test description', + ) endpoints = self.operator_cloud.create_endpoint( service_name_or_id=service['id'], public_url='http://public.test/', internal_url='http://internal.test/', - region=region) + region=region, + ) self.assertNotEqual([], endpoints) for endpoint in endpoints: @@ -204,5 +229,4 @@ def test_delete_endpoint(self): if e['id'] == endpoint['id']: found = True break - self.assertEqual( - False, found, message='new endpoint was not deleted!') + self.assertEqual(False, found, message='new endpoint was not deleted!') diff --git a/openstack/tests/functional/cloud/test_flavor.py b/openstack/tests/functional/cloud/test_flavor.py index f1a76b7ff..5d3e94241 100644 --- a/openstack/tests/functional/cloud/test_flavor.py +++ b/openstack/tests/functional/cloud/test_flavor.py @@ -24,7 +24,6 @@ class TestFlavor(base.BaseFunctionalTest): - def setUp(self): super(TestFlavor, self).setUp() @@ -56,8 +55,14 @@ def test_create_flavor(self): flavor_name = self.new_item_name + '_create' flavor_kwargs = dict( - name=flavor_name, ram=1024, vcpus=2, disk=10, ephemeral=5, - swap=100, rxtx_factor=1.5, is_public=True + name=flavor_name, + ram=1024, + vcpus=2, + disk=10, + ephemeral=5, + swap=100, + rxtx_factor=1.5, + is_public=True, ) flavor = self.operator_cloud.create_flavor(**flavor_kwargs) @@ -144,8 +149,9 @@ def test_flavor_access(self): self.assertEqual(project['id'], acls[0]['tenant_id']) # Now revoke the access and make sure we can't find it - self.operator_cloud.remove_flavor_access(new_flavor['id'], - project['id']) + self.operator_cloud.remove_flavor_access( + new_flavor['id'], project['id'] + ) flavors = self.user_cloud.search_flavors(priv_flavor_name) self.assertEqual(0, len(flavors)) @@ -157,9 +163,7 @@ def test_set_unset_flavor_specs(self): self.skipTest("Operator cloud is required for this test") flavor_name = self.new_item_name + '_spec_test' - kwargs = dict( - name=flavor_name, ram=1024, vcpus=2, disk=10 - ) + kwargs = dict(name=flavor_name, ram=1024, vcpus=2, disk=10) new_flavor = self.operator_cloud.create_flavor(**kwargs) # Expect no extra_specs @@ -169,7 +173,8 @@ def test_set_unset_flavor_specs(self): extra_specs = {'foo': 'aaa', 'bar': 'bbb'} self.operator_cloud.set_flavor_specs(new_flavor['id'], extra_specs) mod_flavor = self.operator_cloud.get_flavor( - new_flavor['id'], get_extra=True) + new_flavor['id'], get_extra=True + ) # Verify extra_specs were set self.assertIn('extra_specs', mod_flavor) @@ -178,7 +183,8 @@ def test_set_unset_flavor_specs(self): # Unset the 'foo' value self.operator_cloud.unset_flavor_specs(mod_flavor['id'], ['foo']) mod_flavor = self.operator_cloud.get_flavor_by_id( - new_flavor['id'], get_extra=True) + new_flavor['id'], get_extra=True + ) # Verify 'foo' is unset and 'bar' is still set self.assertEqual({'bar': 'bbb'}, mod_flavor['extra_specs']) diff --git a/openstack/tests/functional/cloud/test_floating_ip.py b/openstack/tests/functional/cloud/test_floating_ip.py index 23610c4a3..34e631059 100644 --- a/openstack/tests/functional/cloud/test_floating_ip.py +++ b/openstack/tests/functional/cloud/test_floating_ip.py @@ -54,12 +54,14 @@ def _cleanup_network(self): try: if r['name'].startswith(self.new_item_name): self.user_cloud.update_router( - r, ext_gateway_net_id=None) + r, ext_gateway_net_id=None + ) for s in self.user_cloud.list_subnets(): if s['name'].startswith(self.new_item_name): try: self.user_cloud.remove_router_interface( - r, subnet_id=s['id']) + r, subnet_id=s['id'] + ) except Exception: pass self.user_cloud.delete_router(r.id) @@ -93,7 +95,9 @@ def _cleanup_network(self): self.addDetail( 'exceptions', content.text_content( - '\n'.join([str(ex) for ex in exception_list]))) + '\n'.join([str(ex) for ex in exception_list]) + ), + ) exc = exception_list[0] raise exc @@ -121,8 +125,10 @@ def _cleanup_ips(self, server): fixed_ip = meta.get_server_private_ip(server) for ip in self.user_cloud.list_floating_ips(): - if (ip.get('fixed_ip', None) == fixed_ip - or ip.get('fixed_ip_address', None) == fixed_ip): + if ( + ip.get('fixed_ip', None) == fixed_ip + or ip.get('fixed_ip_address', None) == fixed_ip + ): try: self.user_cloud.delete_floating_ip(ip.id) except Exception as e: @@ -138,42 +144,49 @@ def _setup_networks(self): if self.user_cloud.has_service('network'): # Create a network self.test_net = self.user_cloud.create_network( - name=self.new_item_name + '_net') + name=self.new_item_name + '_net' + ) # Create a subnet on it self.test_subnet = self.user_cloud.create_subnet( subnet_name=self.new_item_name + '_subnet', network_name_or_id=self.test_net['id'], cidr='10.24.4.0/24', - enable_dhcp=True + enable_dhcp=True, ) # Create a router self.test_router = self.user_cloud.create_router( - name=self.new_item_name + '_router') + name=self.new_item_name + '_router' + ) # Attach the router to an external network ext_nets = self.user_cloud.search_networks( - filters={'router:external': True}) + filters={'router:external': True} + ) self.user_cloud.update_router( name_or_id=self.test_router['id'], - ext_gateway_net_id=ext_nets[0]['id']) + ext_gateway_net_id=ext_nets[0]['id'], + ) # Attach the router to the internal subnet self.user_cloud.add_router_interface( - self.test_router, subnet_id=self.test_subnet['id']) + self.test_router, subnet_id=self.test_subnet['id'] + ) # Select the network for creating new servers self.nic = {'net-id': self.test_net['id']} self.addDetail( 'networks-neutron', - content.text_content(pprint.pformat( - self.user_cloud.list_networks()))) + content.text_content( + pprint.pformat(self.user_cloud.list_networks()) + ), + ) else: # Find network names for nova-net data = proxy._json_response( - self.user_cloud._conn.compute.get('/os-tenant-networks')) + self.user_cloud._conn.compute.get('/os-tenant-networks') + ) nets = meta.get_and_munchify('networks', data) self.addDetail( - 'networks-nova', - content.text_content(pprint.pformat( - nets))) + 'networks-nova', content.text_content(pprint.pformat(nets)) + ) self.nic = {'net-id': nets[0].id} def test_private_ip(self): @@ -181,27 +194,36 @@ def test_private_ip(self): new_server = self.user_cloud.get_openstack_vars( self.user_cloud.create_server( - wait=True, name=self.new_item_name + '_server', + wait=True, + name=self.new_item_name + '_server', image=self.image, - flavor=self.flavor, nics=[self.nic])) + flavor=self.flavor, + nics=[self.nic], + ) + ) self.addDetail( - 'server', content.text_content(pprint.pformat(new_server))) + 'server', content.text_content(pprint.pformat(new_server)) + ) self.assertNotEqual(new_server['private_v4'], '') def test_add_auto_ip(self): self._setup_networks() new_server = self.user_cloud.create_server( - wait=True, name=self.new_item_name + '_server', + wait=True, + name=self.new_item_name + '_server', image=self.image, - flavor=self.flavor, nics=[self.nic]) + flavor=self.flavor, + nics=[self.nic], + ) # ToDo: remove the following iteration when create_server waits for # the IP to be attached ip = None for _ in utils.iterate_timeout( - self.timeout, "Timeout waiting for IP address to be attached"): + self.timeout, "Timeout waiting for IP address to be attached" + ): ip = meta.get_server_external_ipv4(self.user_cloud, new_server) if ip is not None: break @@ -213,15 +235,19 @@ def test_detach_ip_from_server(self): self._setup_networks() new_server = self.user_cloud.create_server( - wait=True, name=self.new_item_name + '_server', + wait=True, + name=self.new_item_name + '_server', image=self.image, - flavor=self.flavor, nics=[self.nic]) + flavor=self.flavor, + nics=[self.nic], + ) # ToDo: remove the following iteration when create_server waits for # the IP to be attached ip = None for _ in utils.iterate_timeout( - self.timeout, "Timeout waiting for IP address to be attached"): + self.timeout, "Timeout waiting for IP address to be attached" + ): ip = meta.get_server_external_ipv4(self.user_cloud, new_server) if ip is not None: break @@ -230,15 +256,18 @@ def test_detach_ip_from_server(self): self.addCleanup(self._cleanup_ips, new_server) f_ip = self.user_cloud.get_floating_ip( - id=None, filters={'floating_ip_address': ip}) + id=None, filters={'floating_ip_address': ip} + ) self.user_cloud.detach_ip_from_server( - server_id=new_server.id, floating_ip_id=f_ip['id']) + server_id=new_server.id, floating_ip_id=f_ip['id'] + ) def test_list_floating_ips(self): if self.operator_cloud: fip_admin = self.operator_cloud.create_floating_ip() self.addCleanup( - self.operator_cloud.delete_floating_ip, fip_admin.id) + self.operator_cloud.delete_floating_ip, fip_admin.id + ) fip_user = self.user_cloud.create_floating_ip() self.addCleanup(self.user_cloud.delete_floating_ip, fip_user.id) @@ -260,7 +289,8 @@ def test_list_floating_ips(self): # Ask Neutron for only a subset of all the FIPs. if self.operator_cloud: filtered_fip_id_list = [ - fip.id for fip in self.operator_cloud.list_floating_ips( + fip.id + for fip in self.operator_cloud.list_floating_ips( {'tenant_id': self.user_cloud.current_project_id} ) ] @@ -275,9 +305,10 @@ def test_list_floating_ips(self): if self.operator_cloud: self.assertNotIn(fip_user.id, fip_op_id_list) self.assertRaisesRegex( - ValueError, "Nova-network don't support server-side.*", + ValueError, + "Nova-network don't support server-side.*", self.operator_cloud.list_floating_ips, - filters={'foo': 'bar'} + filters={'foo': 'bar'}, ) def test_search_floating_ips(self): @@ -286,7 +317,7 @@ def test_search_floating_ips(self): self.assertIn( fip_user['id'], - [fip.id for fip in self.user_cloud.search_floating_ips()] + [fip.id for fip in self.user_cloud.search_floating_ips()], ) def test_get_floating_ip_by_id(self): diff --git a/openstack/tests/functional/cloud/test_floating_ip_pool.py b/openstack/tests/functional/cloud/test_floating_ip_pool.py index 2eba99af1..30e84fc3a 100644 --- a/openstack/tests/functional/cloud/test_floating_ip_pool.py +++ b/openstack/tests/functional/cloud/test_floating_ip_pool.py @@ -38,8 +38,7 @@ def setUp(self): if not self.user_cloud._has_nova_extension('os-floating-ip-pools'): # Skipping this test is floating-ip-pool extension is not # available on the testing cloud - self.skip( - 'Floating IP pools extension is not available') + self.skip('Floating IP pools extension is not available') def test_list_floating_ip_pools(self): pools = self.user_cloud.list_floating_ip_pools() diff --git a/openstack/tests/functional/cloud/test_groups.py b/openstack/tests/functional/cloud/test_groups.py index 3ef2a5626..9415b337f 100644 --- a/openstack/tests/functional/cloud/test_groups.py +++ b/openstack/tests/functional/cloud/test_groups.py @@ -22,7 +22,6 @@ class TestGroup(base.BaseFunctionalTest): - def setUp(self): super(TestGroup, self).setUp() if not self.operator_cloud: @@ -48,7 +47,8 @@ def _cleanup_groups(self): # Raise an error: we must make users aware that something went # wrong raise openstack.cloud.OpenStackCloudException( - '\n'.join(exception_list)) + '\n'.join(exception_list) + ) def test_create_group(self): group_name = self.group_prefix + '_create' @@ -68,7 +68,8 @@ def test_delete_group(self): self.assertTrue(self.operator_cloud.delete_group(group_name)) results = self.operator_cloud.search_groups( - filters=dict(name=group_name)) + filters=dict(name=group_name) + ) self.assertEqual(0, len(results)) def test_delete_group_not_exists(self): @@ -79,7 +80,8 @@ def test_search_groups(self): # Shouldn't find any group with this name yet results = self.operator_cloud.search_groups( - filters=dict(name=group_name)) + filters=dict(name=group_name) + ) self.assertEqual(0, len(results)) # Now create a new group @@ -88,7 +90,8 @@ def test_search_groups(self): # Now we should find only the new group results = self.operator_cloud.search_groups( - filters=dict(name=group_name)) + filters=dict(name=group_name) + ) self.assertEqual(1, len(results)) self.assertEqual(group_name, results[0]['name']) @@ -103,8 +106,7 @@ def test_update_group(self): updated_group_name = group_name + '_xyz' updated_group_desc = group_desc + ' updated' updated_group = self.operator_cloud.update_group( - group_name, - name=updated_group_name, - description=updated_group_desc) + group_name, name=updated_group_name, description=updated_group_desc + ) self.assertEqual(updated_group_name, updated_group['name']) self.assertEqual(updated_group_desc, updated_group['description']) diff --git a/openstack/tests/functional/cloud/test_identity.py b/openstack/tests/functional/cloud/test_identity.py index 118cf33f9..5fc004628 100644 --- a/openstack/tests/functional/cloud/test_identity.py +++ b/openstack/tests/functional/cloud/test_identity.py @@ -30,7 +30,8 @@ def setUp(self): if not self.operator_cloud: self.skipTest("Operator cloud is required for this test") self.role_prefix = 'test_role' + ''.join( - random.choice(string.ascii_lowercase) for _ in range(5)) + random.choice(string.ascii_lowercase) for _ in range(5) + ) self.user_prefix = self.getUniqueString('user') self.group_prefix = self.getUniqueString('group') @@ -133,7 +134,8 @@ def test_list_role_assignments_v2(self): user = self.operator_cloud.get_user('demo') project = self.operator_cloud.get_project('demo') assignments = self.operator_cloud.list_role_assignments( - filters={'user': user['id'], 'project': project['id']}) + filters={'user': user['id'], 'project': project['id']} + ) self.assertIsInstance(assignments, list) self.assertGreater(len(assignments), 0) @@ -142,25 +144,35 @@ def test_grant_revoke_role_user_project(self): user_email = 'nobody@nowhere.com' role_name = self.role_prefix + '_grant_user_project' role = self.operator_cloud.create_role(role_name) - user = self._create_user(name=user_name, - email=user_email, - default_project='demo') - self.assertTrue(self.operator_cloud.grant_role( - role_name, user=user['id'], project='demo', wait=True)) - assignments = self.operator_cloud.list_role_assignments({ - 'role': role['id'], - 'user': user['id'], - 'project': self.operator_cloud.get_project('demo')['id'] - }) + user = self._create_user( + name=user_name, email=user_email, default_project='demo' + ) + self.assertTrue( + self.operator_cloud.grant_role( + role_name, user=user['id'], project='demo', wait=True + ) + ) + assignments = self.operator_cloud.list_role_assignments( + { + 'role': role['id'], + 'user': user['id'], + 'project': self.operator_cloud.get_project('demo')['id'], + } + ) self.assertIsInstance(assignments, list) self.assertEqual(1, len(assignments)) - self.assertTrue(self.operator_cloud.revoke_role( - role_name, user=user['id'], project='demo', wait=True)) - assignments = self.operator_cloud.list_role_assignments({ - 'role': role['id'], - 'user': user['id'], - 'project': self.operator_cloud.get_project('demo')['id'] - }) + self.assertTrue( + self.operator_cloud.revoke_role( + role_name, user=user['id'], project='demo', wait=True + ) + ) + assignments = self.operator_cloud.list_role_assignments( + { + 'role': role['id'], + 'user': user['id'], + 'project': self.operator_cloud.get_project('demo')['id'], + } + ) self.assertIsInstance(assignments, list) self.assertEqual(0, len(assignments)) @@ -171,25 +183,34 @@ def test_grant_revoke_role_group_project(self): role = self.operator_cloud.create_role(role_name) group_name = self.group_prefix + '_group_project' group = self.operator_cloud.create_group( - name=group_name, - description='test group', - domain='default') - self.assertTrue(self.operator_cloud.grant_role( - role_name, group=group['id'], project='demo')) - assignments = self.operator_cloud.list_role_assignments({ - 'role': role['id'], - 'group': group['id'], - 'project': self.operator_cloud.get_project('demo')['id'] - }) + name=group_name, description='test group', domain='default' + ) + self.assertTrue( + self.operator_cloud.grant_role( + role_name, group=group['id'], project='demo' + ) + ) + assignments = self.operator_cloud.list_role_assignments( + { + 'role': role['id'], + 'group': group['id'], + 'project': self.operator_cloud.get_project('demo')['id'], + } + ) self.assertIsInstance(assignments, list) self.assertEqual(1, len(assignments)) - self.assertTrue(self.operator_cloud.revoke_role( - role_name, group=group['id'], project='demo')) - assignments = self.operator_cloud.list_role_assignments({ - 'role': role['id'], - 'group': group['id'], - 'project': self.operator_cloud.get_project('demo')['id'] - }) + self.assertTrue( + self.operator_cloud.revoke_role( + role_name, group=group['id'], project='demo' + ) + ) + assignments = self.operator_cloud.list_role_assignments( + { + 'role': role['id'], + 'group': group['id'], + 'project': self.operator_cloud.get_project('demo')['id'], + } + ) self.assertIsInstance(assignments, list) self.assertEqual(0, len(assignments)) @@ -200,25 +221,35 @@ def test_grant_revoke_role_user_domain(self): role = self.operator_cloud.create_role(role_name) user_name = self.user_prefix + '_user_domain' user_email = 'nobody@nowhere.com' - user = self._create_user(name=user_name, - email=user_email, - default_project='demo') - self.assertTrue(self.operator_cloud.grant_role( - role_name, user=user['id'], domain='default')) - assignments = self.operator_cloud.list_role_assignments({ - 'role': role['id'], - 'user': user['id'], - 'domain': self.operator_cloud.get_domain('default')['id'] - }) + user = self._create_user( + name=user_name, email=user_email, default_project='demo' + ) + self.assertTrue( + self.operator_cloud.grant_role( + role_name, user=user['id'], domain='default' + ) + ) + assignments = self.operator_cloud.list_role_assignments( + { + 'role': role['id'], + 'user': user['id'], + 'domain': self.operator_cloud.get_domain('default')['id'], + } + ) self.assertIsInstance(assignments, list) self.assertEqual(1, len(assignments)) - self.assertTrue(self.operator_cloud.revoke_role( - role_name, user=user['id'], domain='default')) - assignments = self.operator_cloud.list_role_assignments({ - 'role': role['id'], - 'user': user['id'], - 'domain': self.operator_cloud.get_domain('default')['id'] - }) + self.assertTrue( + self.operator_cloud.revoke_role( + role_name, user=user['id'], domain='default' + ) + ) + assignments = self.operator_cloud.list_role_assignments( + { + 'role': role['id'], + 'user': user['id'], + 'domain': self.operator_cloud.get_domain('default')['id'], + } + ) self.assertIsInstance(assignments, list) self.assertEqual(0, len(assignments)) @@ -229,25 +260,34 @@ def test_grant_revoke_role_group_domain(self): role = self.operator_cloud.create_role(role_name) group_name = self.group_prefix + '_group_domain' group = self.operator_cloud.create_group( - name=group_name, - description='test group', - domain='default') - self.assertTrue(self.operator_cloud.grant_role( - role_name, group=group['id'], domain='default')) - assignments = self.operator_cloud.list_role_assignments({ - 'role': role['id'], - 'group': group['id'], - 'domain': self.operator_cloud.get_domain('default')['id'] - }) + name=group_name, description='test group', domain='default' + ) + self.assertTrue( + self.operator_cloud.grant_role( + role_name, group=group['id'], domain='default' + ) + ) + assignments = self.operator_cloud.list_role_assignments( + { + 'role': role['id'], + 'group': group['id'], + 'domain': self.operator_cloud.get_domain('default')['id'], + } + ) self.assertIsInstance(assignments, list) self.assertEqual(1, len(assignments)) - self.assertTrue(self.operator_cloud.revoke_role( - role_name, group=group['id'], domain='default')) - assignments = self.operator_cloud.list_role_assignments({ - 'role': role['id'], - 'group': group['id'], - 'domain': self.operator_cloud.get_domain('default')['id'] - }) + self.assertTrue( + self.operator_cloud.revoke_role( + role_name, group=group['id'], domain='default' + ) + ) + assignments = self.operator_cloud.list_role_assignments( + { + 'role': role['id'], + 'group': group['id'], + 'domain': self.operator_cloud.get_domain('default')['id'], + } + ) self.assertIsInstance(assignments, list) self.assertEqual(0, len(assignments)) @@ -256,25 +296,27 @@ def test_grant_revoke_role_user_system(self): role = self.operator_cloud.create_role(role_name) user_name = self.user_prefix + '_user_system' user_email = 'nobody@nowhere.com' - user = self._create_user(name=user_name, - email=user_email, - default_project='demo') - self.assertTrue(self.operator_cloud.grant_role( - role_name, user=user['id'], system='all')) - assignments = self.operator_cloud.list_role_assignments({ - 'role': role['id'], - 'user': user['id'], - 'system': 'all' - }) + user = self._create_user( + name=user_name, email=user_email, default_project='demo' + ) + self.assertTrue( + self.operator_cloud.grant_role( + role_name, user=user['id'], system='all' + ) + ) + assignments = self.operator_cloud.list_role_assignments( + {'role': role['id'], 'user': user['id'], 'system': 'all'} + ) self.assertIsInstance(assignments, list) self.assertEqual(1, len(assignments)) - self.assertTrue(self.operator_cloud.revoke_role( - role_name, user=user['id'], system='all')) - assignments = self.operator_cloud.list_role_assignments({ - 'role': role['id'], - 'user': user['id'], - 'system': 'all' - }) + self.assertTrue( + self.operator_cloud.revoke_role( + role_name, user=user['id'], system='all' + ) + ) + assignments = self.operator_cloud.list_role_assignments( + {'role': role['id'], 'user': user['id'], 'system': 'all'} + ) self.assertIsInstance(assignments, list) self.assertEqual(0, len(assignments)) @@ -285,23 +327,25 @@ def test_grant_revoke_role_group_system(self): role = self.operator_cloud.create_role(role_name) group_name = self.group_prefix + '_group_system' group = self.operator_cloud.create_group( - name=group_name, - description='test group') - self.assertTrue(self.operator_cloud.grant_role( - role_name, group=group['id'], system='all')) - assignments = self.operator_cloud.list_role_assignments({ - 'role': role['id'], - 'group': group['id'], - 'system': 'all' - }) + name=group_name, description='test group' + ) + self.assertTrue( + self.operator_cloud.grant_role( + role_name, group=group['id'], system='all' + ) + ) + assignments = self.operator_cloud.list_role_assignments( + {'role': role['id'], 'group': group['id'], 'system': 'all'} + ) self.assertIsInstance(assignments, list) self.assertEqual(1, len(assignments)) - self.assertTrue(self.operator_cloud.revoke_role( - role_name, group=group['id'], system='all')) - assignments = self.operator_cloud.list_role_assignments({ - 'role': role['id'], - 'group': group['id'], - 'system': 'all' - }) + self.assertTrue( + self.operator_cloud.revoke_role( + role_name, group=group['id'], system='all' + ) + ) + assignments = self.operator_cloud.list_role_assignments( + {'role': role['id'], 'group': group['id'], 'system': 'all'} + ) self.assertIsInstance(assignments, list) self.assertEqual(0, len(assignments)) diff --git a/openstack/tests/functional/cloud/test_image.py b/openstack/tests/functional/cloud/test_image.py index b6e43fbb0..489f2c1a2 100644 --- a/openstack/tests/functional/cloud/test_image.py +++ b/openstack/tests/functional/cloud/test_image.py @@ -25,7 +25,6 @@ class TestImage(base.BaseFunctionalTest): - def test_create_image(self): test_image = tempfile.NamedTemporaryFile(delete=False) test_image.write(b'\0' * 1024 * 1024) @@ -40,7 +39,8 @@ def test_create_image(self): min_disk=10, min_ram=1024, tags=['custom'], - wait=True) + wait=True, + ) finally: self.user_cloud.delete_image(image_name, wait=True) @@ -57,13 +57,16 @@ def test_download_image(self): container_format='bare', min_disk=10, min_ram=1024, - wait=True) + wait=True, + ) self.addCleanup(self.user_cloud.delete_image, image_name, wait=True) output = os.path.join(tempfile.gettempdir(), self.getUniqueString()) self.user_cloud.download_image(image_name, output) self.addCleanup(os.remove, output) - self.assertTrue(filecmp.cmp(test_image.name, output), - "Downloaded contents don't match created image") + self.assertTrue( + filecmp.cmp(test_image.name, output), + "Downloaded contents don't match created image", + ) def test_create_image_skip_duplicate(self): test_image = tempfile.NamedTemporaryFile(delete=False) @@ -79,7 +82,8 @@ def test_create_image_skip_duplicate(self): min_disk=10, min_ram=1024, validate_checksum=True, - wait=True) + wait=True, + ) second_image = self.user_cloud.create_image( name=image_name, filename=test_image.name, @@ -88,7 +92,8 @@ def test_create_image_skip_duplicate(self): min_disk=10, min_ram=1024, validate_checksum=True, - wait=True) + wait=True, + ) self.assertEqual(first_image.id, second_image.id) finally: self.user_cloud.delete_image(image_name, wait=True) @@ -108,7 +113,8 @@ def test_create_image_force_duplicate(self): container_format='bare', min_disk=10, min_ram=1024, - wait=True) + wait=True, + ) second_image = self.user_cloud.create_image( name=image_name, filename=test_image.name, @@ -117,7 +123,8 @@ def test_create_image_force_duplicate(self): min_disk=10, min_ram=1024, allow_duplicates=True, - wait=True) + wait=True, + ) self.assertNotEqual(first_image.id, second_image.id) finally: if first_image: @@ -138,11 +145,11 @@ def test_create_image_update_properties(self): container_format='bare', min_disk=10, min_ram=1024, - wait=True) + wait=True, + ) self.user_cloud.update_image_properties( - image=image, - name=image_name, - foo='bar') + image=image, name=image_name, foo='bar' + ) image = self.user_cloud.get_image(image_name) self.assertIn('foo', image.properties) self.assertEqual(image.properties['foo'], 'bar') @@ -158,7 +165,8 @@ def test_create_image_without_filename(self): min_disk=10, min_ram=1024, allow_duplicates=True, - wait=False) + wait=False, + ) self.assertEqual(image_name, image.name) self.user_cloud.delete_image(image.id, wait=True) @@ -175,7 +183,8 @@ def test_get_image_by_id(self): container_format='bare', min_disk=10, min_ram=1024, - wait=True) + wait=True, + ) image = self.user_cloud.get_image_by_id(image.id) self.assertEqual(image_name, image.name) self.assertEqual('raw', image.disk_format) diff --git a/openstack/tests/functional/cloud/test_inventory.py b/openstack/tests/functional/cloud/test_inventory.py index 79de47a83..c5235fc0b 100644 --- a/openstack/tests/functional/cloud/test_inventory.py +++ b/openstack/tests/functional/cloud/test_inventory.py @@ -35,8 +35,13 @@ def setUp(self): self.server_name = self.getUniqueString('inventory') self.addCleanup(self._cleanup_server) server = self.operator_cloud.create_server( - name=self.server_name, image=self.image, flavor=self.flavor, - wait=True, auto_ip=True, network='public') + name=self.server_name, + image=self.image, + flavor=self.flavor, + wait=True, + auto_ip=True, + network='public', + ) self.server_id = server['id'] def _cleanup_server(self): diff --git a/openstack/tests/functional/cloud/test_keypairs.py b/openstack/tests/functional/cloud/test_keypairs.py index 80eee3844..64b58d186 100644 --- a/openstack/tests/functional/cloud/test_keypairs.py +++ b/openstack/tests/functional/cloud/test_keypairs.py @@ -21,7 +21,6 @@ class TestKeypairs(base.BaseFunctionalTest): - def test_create_and_delete(self): '''Test creating and deleting keypairs functionality''' name = self.getUniqueString('keypair') @@ -46,7 +45,8 @@ def test_create_and_delete_with_key(self): name = self.getUniqueString('keypair') self.addCleanup(self.user_cloud.delete_keypair, name) keypair = self.user_cloud.create_keypair( - name=name, public_key=fakes.FAKE_PUBLIC_KEY) + name=name, public_key=fakes.FAKE_PUBLIC_KEY + ) self.assertEqual(keypair['name'], name) self.assertIsNotNone(keypair['public_key']) self.assertIsNone(keypair['private_key']) diff --git a/openstack/tests/functional/cloud/test_limits.py b/openstack/tests/functional/cloud/test_limits.py index 160adaca6..5e9f7bc83 100644 --- a/openstack/tests/functional/cloud/test_limits.py +++ b/openstack/tests/functional/cloud/test_limits.py @@ -21,7 +21,6 @@ class TestUsage(base.BaseFunctionalTest): - def test_get_our_compute_limits(self): '''Test quotas functionality''' limits = self.user_cloud.get_compute_limits() diff --git a/openstack/tests/functional/cloud/test_magnum_services.py b/openstack/tests/functional/cloud/test_magnum_services.py index b971f1383..1690b5d23 100644 --- a/openstack/tests/functional/cloud/test_magnum_services.py +++ b/openstack/tests/functional/cloud/test_magnum_services.py @@ -21,7 +21,6 @@ class TestMagnumServices(base.BaseFunctionalTest): - def setUp(self): super(TestMagnumServices, self).setUp() if not self.user_cloud.has_service( diff --git a/openstack/tests/functional/cloud/test_network.py b/openstack/tests/functional/cloud/test_network.py index d4cfe4267..60c3a0b74 100644 --- a/openstack/tests/functional/cloud/test_network.py +++ b/openstack/tests/functional/cloud/test_network.py @@ -84,7 +84,8 @@ def test_create_network_advanced(self): def test_create_network_provider_flat(self): existing_public = self.operator_cloud.search_networks( - filters={'provider:network_type': 'flat'}) + filters={'provider:network_type': 'flat'} + ) if existing_public: self.skipTest('Physical network already allocated') net1 = self.operator_cloud.create_network( @@ -93,7 +94,7 @@ def test_create_network_provider_flat(self): provider={ 'physical_network': 'public', 'network_type': 'flat', - } + }, ) self.assertIn('id', net1) self.assertEqual(self.network_name, net1['name']) @@ -117,10 +118,12 @@ def test_list_networks_filtered(self): net1 = self.operator_cloud.create_network(name=self.network_name) self.assertIsNotNone(net1) net2 = self.operator_cloud.create_network( - name=self.network_name + 'other') + name=self.network_name + 'other' + ) self.assertIsNotNone(net2) match = self.operator_cloud.list_networks( - filters=dict(name=self.network_name)) + filters=dict(name=self.network_name) + ) self.assertEqual(1, len(match)) self.assertEqual(net1['name'], match[0]['name']) diff --git a/openstack/tests/functional/cloud/test_object.py b/openstack/tests/functional/cloud/test_object.py index cc707f53d..0b3e274d7 100644 --- a/openstack/tests/functional/cloud/test_object.py +++ b/openstack/tests/functional/cloud/test_object.py @@ -28,7 +28,6 @@ class TestObject(base.BaseFunctionalTest): - def setUp(self): super(TestObject, self).setUp() if not self.user_cloud.has_service('object-store'): @@ -41,69 +40,84 @@ def test_create_object(self): self.addCleanup(self.user_cloud.delete_container, container_name) self.user_cloud.create_container(container_name) container = self.user_cloud.get_container(container_name) + self.assertEqual(container_name, container.name) self.assertEqual( - container_name, container.name) - self.assertEqual( - [], - self.user_cloud.list_containers(prefix='somethin')) + [], self.user_cloud.list_containers(prefix='somethin') + ) sizes = ( (64 * 1024, 1), # 64K, one segment - (64 * 1024, 5) # 64MB, 5 segments + (64 * 1024, 5), # 64MB, 5 segments ) for size, nseg in sizes: segment_size = int(round(size / nseg)) with tempfile.NamedTemporaryFile() as fake_file: - fake_content = ''.join(random.SystemRandom().choice( - string.ascii_uppercase + string.digits) - for _ in range(size)).encode('latin-1') + fake_content = ''.join( + random.SystemRandom().choice( + string.ascii_uppercase + string.digits + ) + for _ in range(size) + ).encode('latin-1') fake_file.write(fake_content) fake_file.flush() name = 'test-%d' % size self.addCleanup( - self.user_cloud.delete_object, container_name, name) + self.user_cloud.delete_object, container_name, name + ) self.user_cloud.create_object( - container_name, name, + container_name, + name, fake_file.name, segment_size=segment_size, - metadata={'foo': 'bar'}) - self.assertFalse(self.user_cloud.is_object_stale( - container_name, name, - fake_file.name - )) + metadata={'foo': 'bar'}, + ) + self.assertFalse( + self.user_cloud.is_object_stale( + container_name, name, fake_file.name + ) + ) self.assertEqual( - 'bar', self.user_cloud.get_object_metadata( - container_name, name)['foo'] + 'bar', + self.user_cloud.get_object_metadata(container_name, name)[ + 'foo' + ], + ) + self.user_cloud.update_object( + container=container_name, + name=name, + metadata={'testk': 'testv'}, ) - self.user_cloud.update_object(container=container_name, name=name, - metadata={'testk': 'testv'}) self.assertEqual( - 'testv', self.user_cloud.get_object_metadata( - container_name, name)['testk'] + 'testv', + self.user_cloud.get_object_metadata(container_name, name)[ + 'testk' + ], ) try: self.assertIsNotNone( - self.user_cloud.get_object(container_name, name)) + self.user_cloud.get_object(container_name, name) + ) except exc.OpenStackCloudException as e: self.addDetail( 'failed_response', - content.text_content(str(e.response.headers))) + content.text_content(str(e.response.headers)), + ) self.addDetail( - 'failed_response', - content.text_content(e.response.text)) + 'failed_response', content.text_content(e.response.text) + ) self.assertEqual( - name, - self.user_cloud.list_objects(container_name)[0]['name']) + name, self.user_cloud.list_objects(container_name)[0]['name'] + ) self.assertEqual( - [], - self.user_cloud.list_objects(container_name, - prefix='abc')) + [], self.user_cloud.list_objects(container_name, prefix='abc') + ) self.assertTrue( - self.user_cloud.delete_object(container_name, name)) + self.user_cloud.delete_object(container_name, name) + ) self.assertEqual([], self.user_cloud.list_objects(container_name)) self.assertEqual( - container_name, - self.user_cloud.get_container(container_name).name) + container_name, self.user_cloud.get_container(container_name).name + ) self.user_cloud.delete_container(container_name) def test_download_object_to_file(self): @@ -112,64 +126,83 @@ def test_download_object_to_file(self): self.addDetail('container', content.text_content(container_name)) self.addCleanup(self.user_cloud.delete_container, container_name) self.user_cloud.create_container(container_name) - self.assertEqual(container_name, - self.user_cloud.list_containers()[0]['name']) + self.assertEqual( + container_name, self.user_cloud.list_containers()[0]['name'] + ) sizes = ( (64 * 1024, 1), # 64K, one segment - (64 * 1024, 5) # 64MB, 5 segments + (64 * 1024, 5), # 64MB, 5 segments ) for size, nseg in sizes: fake_content = '' segment_size = int(round(size / nseg)) with tempfile.NamedTemporaryFile() as fake_file: - fake_content = ''.join(random.SystemRandom().choice( - string.ascii_uppercase + string.digits) - for _ in range(size)).encode('latin-1') + fake_content = ''.join( + random.SystemRandom().choice( + string.ascii_uppercase + string.digits + ) + for _ in range(size) + ).encode('latin-1') fake_file.write(fake_content) fake_file.flush() name = 'test-%d' % size self.addCleanup( - self.user_cloud.delete_object, container_name, name) + self.user_cloud.delete_object, container_name, name + ) self.user_cloud.create_object( - container_name, name, + container_name, + name, fake_file.name, segment_size=segment_size, - metadata={'foo': 'bar'}) - self.assertFalse(self.user_cloud.is_object_stale( - container_name, name, - fake_file.name - )) + metadata={'foo': 'bar'}, + ) + self.assertFalse( + self.user_cloud.is_object_stale( + container_name, name, fake_file.name + ) + ) self.assertEqual( - 'bar', self.user_cloud.get_object_metadata( - container_name, name)['foo'] + 'bar', + self.user_cloud.get_object_metadata(container_name, name)[ + 'foo' + ], + ) + self.user_cloud.update_object( + container=container_name, + name=name, + metadata={'testk': 'testv'}, ) - self.user_cloud.update_object(container=container_name, name=name, - metadata={'testk': 'testv'}) self.assertEqual( - 'testv', self.user_cloud.get_object_metadata( - container_name, name)['testk'] + 'testv', + self.user_cloud.get_object_metadata(container_name, name)[ + 'testk' + ], ) try: with tempfile.NamedTemporaryFile() as fake_file: self.user_cloud.get_object( - container_name, name, outfile=fake_file.name) + container_name, name, outfile=fake_file.name + ) downloaded_content = open(fake_file.name, 'rb').read() self.assertEqual(fake_content, downloaded_content) except exc.OpenStackCloudException as e: self.addDetail( 'failed_response', - content.text_content(str(e.response.headers))) + content.text_content(str(e.response.headers)), + ) self.addDetail( - 'failed_response', - content.text_content(e.response.text)) + 'failed_response', content.text_content(e.response.text) + ) raise self.assertEqual( - name, - self.user_cloud.list_objects(container_name)[0]['name']) + name, self.user_cloud.list_objects(container_name)[0]['name'] + ) self.assertTrue( - self.user_cloud.delete_object(container_name, name)) + self.user_cloud.delete_object(container_name, name) + ) self.assertEqual([], self.user_cloud.list_objects(container_name)) - self.assertEqual(container_name, - self.user_cloud.list_containers()[0]['name']) + self.assertEqual( + container_name, self.user_cloud.list_containers()[0]['name'] + ) self.user_cloud.delete_container(container_name) diff --git a/openstack/tests/functional/cloud/test_port.py b/openstack/tests/functional/cloud/test_port.py index a798a20f9..c36923334 100644 --- a/openstack/tests/functional/cloud/test_port.py +++ b/openstack/tests/functional/cloud/test_port.py @@ -27,7 +27,6 @@ class TestPort(base.BaseFunctionalTest): - def setUp(self): super(TestPort, self).setUp() # Skip Neutron tests if neutron is not present @@ -40,7 +39,8 @@ def setUp(self): # Generate a unique port name to allow concurrent tests self.new_port_name = 'test_' + ''.join( - random.choice(string.ascii_lowercase) for _ in range(5)) + random.choice(string.ascii_lowercase) for _ in range(5) + ) self.addCleanup(self._cleanup_ports) @@ -65,7 +65,8 @@ def test_create_port(self): port_name = self.new_port_name + '_create' port = self.user_cloud.create_port( - network_id=self.net.id, name=port_name) + network_id=self.net.id, name=port_name + ) self.assertIsInstance(port, dict) self.assertIn('id', port) self.assertEqual(port.get('name'), port_name) @@ -74,7 +75,8 @@ def test_get_port(self): port_name = self.new_port_name + '_get' port = self.user_cloud.create_port( - network_id=self.net.id, name=port_name) + network_id=self.net.id, name=port_name + ) self.assertIsInstance(port, dict) self.assertIn('id', port) self.assertEqual(port.get('name'), port_name) @@ -89,7 +91,8 @@ def test_get_port_by_id(self): port_name = self.new_port_name + '_get_by_id' port = self.user_cloud.create_port( - network_id=self.net.id, name=port_name) + network_id=self.net.id, name=port_name + ) self.assertIsInstance(port, dict) self.assertIn('id', port) self.assertEqual(port.get('name'), port_name) @@ -104,11 +107,11 @@ def test_update_port(self): port_name = self.new_port_name + '_update' new_port_name = port_name + '_new' - self.user_cloud.create_port( - network_id=self.net.id, name=port_name) + self.user_cloud.create_port(network_id=self.net.id, name=port_name) port = self.user_cloud.update_port( - name_or_id=port_name, name=new_port_name) + name_or_id=port_name, name=new_port_name + ) self.assertIsInstance(port, dict) self.assertEqual(port.get('name'), new_port_name) @@ -129,7 +132,8 @@ def test_delete_port(self): port_name = self.new_port_name + '_delete' port = self.user_cloud.create_port( - network_id=self.net.id, name=port_name) + network_id=self.net.id, name=port_name + ) self.assertIsInstance(port, dict) self.assertIn('id', port) self.assertEqual(port.get('name'), port_name) diff --git a/openstack/tests/functional/cloud/test_project.py b/openstack/tests/functional/cloud/test_project.py index 26390f7db..d4a4647c6 100644 --- a/openstack/tests/functional/cloud/test_project.py +++ b/openstack/tests/functional/cloud/test_project.py @@ -25,7 +25,6 @@ class TestProject(base.KeystoneBaseFunctionalTest): - def setUp(self): super(TestProject, self).setUp() if not self.operator_cloud: @@ -54,8 +53,9 @@ def test_create_project(self): 'description': 'test_create_project', } if self.identity_version == '3': - params['domain_id'] = \ - self.operator_cloud.get_domain('default')['id'] + params['domain_id'] = self.operator_cloud.get_domain('default')[ + 'id' + ] project = self.operator_cloud.create_project(**params) @@ -66,15 +66,23 @@ def test_create_project(self): user_id = self.operator_cloud.current_user_id # Grant the current user access to the project - self.assertTrue(self.operator_cloud.grant_role( - 'member', user=user_id, project=project['id'], wait=True)) + self.assertTrue( + self.operator_cloud.grant_role( + 'member', user=user_id, project=project['id'], wait=True + ) + ) self.addCleanup( self.operator_cloud.revoke_role, - 'member', user=user_id, project=project['id'], wait=True) + 'member', + user=user_id, + project=project['id'], + wait=True, + ) new_cloud = self.operator_cloud.connect_as_project(project) self.add_info_on_exception( - 'new_cloud_config', pprint.pformat(new_cloud.config.config)) + 'new_cloud_config', pprint.pformat(new_cloud.config.config) + ) location = new_cloud.current_location self.assertEqual(project_name, location['project']['name']) @@ -84,15 +92,17 @@ def test_update_project(self): params = { 'name': project_name, 'description': 'test_update_project', - 'enabled': True + 'enabled': True, } if self.identity_version == '3': - params['domain_id'] = \ - self.operator_cloud.get_domain('default')['id'] + params['domain_id'] = self.operator_cloud.get_domain('default')[ + 'id' + ] project = self.operator_cloud.create_project(**params) updated_project = self.operator_cloud.update_project( - project_name, enabled=False, description='new') + project_name, enabled=False, description='new' + ) self.assertIsNotNone(updated_project) self.assertEqual(project['id'], updated_project['id']) self.assertEqual(project['name'], updated_project['name']) @@ -102,12 +112,14 @@ def test_update_project(self): # Revert the description and verify the project is still disabled updated_project = self.operator_cloud.update_project( - project_name, description=params['description']) + project_name, description=params['description'] + ) self.assertIsNotNone(updated_project) self.assertEqual(project['id'], updated_project['id']) self.assertEqual(project['name'], updated_project['name']) - self.assertEqual(project['description'], - updated_project['description']) + self.assertEqual( + project['description'], updated_project['description'] + ) self.assertTrue(project['enabled']) self.assertFalse(updated_project['enabled']) @@ -115,8 +127,9 @@ def test_delete_project(self): project_name = self.new_project_name + '_delete' params = {'name': project_name} if self.identity_version == '3': - params['domain_id'] = \ - self.operator_cloud.get_domain('default')['id'] + params['domain_id'] = self.operator_cloud.get_domain('default')[ + 'id' + ] project = self.operator_cloud.create_project(**params) self.assertIsNotNone(project) self.assertTrue(self.operator_cloud.delete_project(project['id'])) diff --git a/openstack/tests/functional/cloud/test_project_cleanup.py b/openstack/tests/functional/cloud/test_project_cleanup.py index 9016358cc..8d661d0ce 100644 --- a/openstack/tests/functional/cloud/test_project_cleanup.py +++ b/openstack/tests/functional/cloud/test_project_cleanup.py @@ -48,8 +48,8 @@ def _create_network_resources(self): name=self.getUniqueString('router') ) conn.network.add_interface_to_router( - self.router.id, - subnet_id=self.subnet.id) + self.router.id, subnet_id=self.subnet.id + ) def test_cleanup(self): self._create_network_resources() @@ -60,7 +60,8 @@ def test_cleanup(self): dry_run=True, wait_timeout=120, status_queue=status_queue, - filters={'created_at': '2000-01-01'}) + filters={'created_at': '2000-01-01'}, + ) self.assertTrue(status_queue.empty()) @@ -71,7 +72,8 @@ def test_cleanup(self): wait_timeout=120, status_queue=status_queue, filters={'created_at': '2200-01-01'}, - resource_evaluation_fn=lambda x, y, z: False) + resource_evaluation_fn=lambda x, y, z: False, + ) self.assertTrue(status_queue.empty()) @@ -80,7 +82,8 @@ def test_cleanup(self): dry_run=True, wait_timeout=120, status_queue=status_queue, - filters={'created_at': '2200-01-01'}) + filters={'created_at': '2200-01-01'}, + ) objects = [] while not status_queue.empty(): @@ -92,9 +95,8 @@ def test_cleanup(self): # Fourth round - dry run with no filters, ensure everything identified self.conn.project_cleanup( - dry_run=True, - wait_timeout=120, - status_queue=status_queue) + dry_run=True, wait_timeout=120, status_queue=status_queue + ) objects = [] while not status_queue.empty(): @@ -109,9 +111,8 @@ def test_cleanup(self): # Last round - do a real cleanup self.conn.project_cleanup( - dry_run=False, - wait_timeout=600, - status_queue=status_queue) + dry_run=False, wait_timeout=600, status_queue=status_queue + ) objects = [] while not status_queue.empty(): @@ -136,10 +137,12 @@ def test_block_storage_cleanup(self): b1 = self.conn.block_storage.create_backup(volume_id=vol.id) self.conn.block_storage.wait_for_status(b1) b2 = self.conn.block_storage.create_backup( - volume_id=vol.id, is_incremental=True, snapshot_id=s1.id) + volume_id=vol.id, is_incremental=True, snapshot_id=s1.id + ) self.conn.block_storage.wait_for_status(b2) b3 = self.conn.block_storage.create_backup( - volume_id=vol.id, is_incremental=True, snapshot_id=s1.id) + volume_id=vol.id, is_incremental=True, snapshot_id=s1.id + ) self.conn.block_storage.wait_for_status(b3) # First round - check no resources are old enough @@ -147,7 +150,8 @@ def test_block_storage_cleanup(self): dry_run=True, wait_timeout=120, status_queue=status_queue, - filters={'created_at': '2000-01-01'}) + filters={'created_at': '2000-01-01'}, + ) self.assertTrue(status_queue.empty()) @@ -158,7 +162,8 @@ def test_block_storage_cleanup(self): wait_timeout=120, status_queue=status_queue, filters={'created_at': '2200-01-01'}, - resource_evaluation_fn=lambda x, y, z: False) + resource_evaluation_fn=lambda x, y, z: False, + ) self.assertTrue(status_queue.empty()) @@ -167,7 +172,8 @@ def test_block_storage_cleanup(self): dry_run=True, wait_timeout=120, status_queue=status_queue, - filters={'created_at': '2200-01-01'}) + filters={'created_at': '2200-01-01'}, + ) objects = [] while not status_queue.empty(): @@ -179,9 +185,8 @@ def test_block_storage_cleanup(self): # Fourth round - dry run with no filters, ensure everything identified self.conn.project_cleanup( - dry_run=True, - wait_timeout=120, - status_queue=status_queue) + dry_run=True, wait_timeout=120, status_queue=status_queue + ) objects = [] while not status_queue.empty(): @@ -196,9 +201,8 @@ def test_block_storage_cleanup(self): # Last round - do a real cleanup self.conn.project_cleanup( - dry_run=False, - wait_timeout=600, - status_queue=status_queue) + dry_run=False, wait_timeout=600, status_queue=status_queue + ) # Ensure no backups remain self.assertEqual(0, len(list(self.conn.block_storage.backups()))) # Ensure no snapshots remain @@ -212,14 +216,16 @@ def test_cleanup_swift(self): self.conn.object_store.create_container('test_cleanup') for i in range(1, 10): self.conn.object_store.create_object( - "test_cleanup", f"test{i}", data="test{i}") + "test_cleanup", f"test{i}", data="test{i}" + ) # First round - check no resources are old enough self.conn.project_cleanup( dry_run=True, wait_timeout=120, status_queue=status_queue, - filters={'updated_at': '2000-01-01'}) + filters={'updated_at': '2000-01-01'}, + ) self.assertTrue(status_queue.empty()) @@ -228,7 +234,8 @@ def test_cleanup_swift(self): dry_run=True, wait_timeout=120, status_queue=status_queue, - filters={'updated_at': '2200-01-01'}) + filters={'updated_at': '2200-01-01'}, + ) objects = [] while not status_queue.empty(): objects.append(status_queue.get()) @@ -238,19 +245,15 @@ def test_cleanup_swift(self): self.assertIn('test1', obj_names) # Ensure object still exists - obj = self.conn.object_store.get_object( - "test1", "test_cleanup") + obj = self.conn.object_store.get_object("test1", "test_cleanup") self.assertIsNotNone(obj) # Last round - do a real cleanup self.conn.project_cleanup( - dry_run=False, - wait_timeout=600, - status_queue=status_queue) + dry_run=False, wait_timeout=600, status_queue=status_queue + ) objects.clear() while not status_queue.empty(): objects.append(status_queue.get()) - self.assertIsNone( - self.conn.get_container('test_container') - ) + self.assertIsNone(self.conn.get_container('test_container')) diff --git a/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py b/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py index e91098df2..3f618698c 100644 --- a/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py @@ -50,59 +50,61 @@ def test_qos_bandwidth_limit_rule_lifecycle(self): # Create bw limit rule rule = self.operator_cloud.create_qos_bandwidth_limit_rule( - self.policy['id'], - max_kbps=max_kbps, - max_burst_kbps=max_burst_kbps) + self.policy['id'], max_kbps=max_kbps, max_burst_kbps=max_burst_kbps + ) self.assertIn('id', rule) self.assertEqual(max_kbps, rule['max_kbps']) self.assertEqual(max_burst_kbps, rule['max_burst_kbps']) # Now try to update rule updated_rule = self.operator_cloud.update_qos_bandwidth_limit_rule( - self.policy['id'], - rule['id'], - max_kbps=updated_max_kbps) + self.policy['id'], rule['id'], max_kbps=updated_max_kbps + ) self.assertIn('id', updated_rule) self.assertEqual(updated_max_kbps, updated_rule['max_kbps']) self.assertEqual(max_burst_kbps, updated_rule['max_burst_kbps']) # List rules from policy policy_rules = self.operator_cloud.list_qos_bandwidth_limit_rules( - self.policy['id']) + self.policy['id'] + ) self.assertEqual([updated_rule], policy_rules) # Delete rule self.operator_cloud.delete_qos_bandwidth_limit_rule( - self.policy['id'], updated_rule['id']) + self.policy['id'], updated_rule['id'] + ) # Check if there is no rules in policy policy_rules = self.operator_cloud.list_qos_bandwidth_limit_rules( - self.policy['id']) + self.policy['id'] + ) self.assertEqual([], policy_rules) def test_create_qos_bandwidth_limit_rule_direction(self): if not self.operator_cloud._has_neutron_extension( - 'qos-bw-limit-direction'): - self.skipTest("'qos-bw-limit-direction' network extension " - "not supported by cloud") + 'qos-bw-limit-direction' + ): + self.skipTest( + "'qos-bw-limit-direction' network extension " + "not supported by cloud" + ) max_kbps = 1500 direction = "ingress" updated_direction = "egress" # Create bw limit rule rule = self.operator_cloud.create_qos_bandwidth_limit_rule( - self.policy['id'], - max_kbps=max_kbps, - direction=direction) + self.policy['id'], max_kbps=max_kbps, direction=direction + ) self.assertIn('id', rule) self.assertEqual(max_kbps, rule['max_kbps']) self.assertEqual(direction, rule['direction']) # Now try to update direction in rule updated_rule = self.operator_cloud.update_qos_bandwidth_limit_rule( - self.policy['id'], - rule['id'], - direction=updated_direction) + self.policy['id'], rule['id'], direction=updated_direction + ) self.assertIn('id', updated_rule) self.assertEqual(max_kbps, updated_rule['max_kbps']) self.assertEqual(updated_direction, updated_rule['direction']) diff --git a/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py b/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py index aff8813b3..b5f100010 100644 --- a/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py +++ b/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py @@ -49,29 +49,31 @@ def test_qos_dscp_marking_rule_lifecycle(self): # Create DSCP marking rule rule = self.operator_cloud.create_qos_dscp_marking_rule( - self.policy['id'], - dscp_mark=dscp_mark) + self.policy['id'], dscp_mark=dscp_mark + ) self.assertIn('id', rule) self.assertEqual(dscp_mark, rule['dscp_mark']) # Now try to update rule updated_rule = self.operator_cloud.update_qos_dscp_marking_rule( - self.policy['id'], - rule['id'], - dscp_mark=updated_dscp_mark) + self.policy['id'], rule['id'], dscp_mark=updated_dscp_mark + ) self.assertIn('id', updated_rule) self.assertEqual(updated_dscp_mark, updated_rule['dscp_mark']) # List rules from policy policy_rules = self.operator_cloud.list_qos_dscp_marking_rules( - self.policy['id']) + self.policy['id'] + ) self.assertEqual([updated_rule], policy_rules) # Delete rule self.operator_cloud.delete_qos_dscp_marking_rule( - self.policy['id'], updated_rule['id']) + self.policy['id'], updated_rule['id'] + ) # Check if there is no rules in policy policy_rules = self.operator_cloud.list_qos_dscp_marking_rules( - self.policy['id']) + self.policy['id'] + ) self.assertEqual([], policy_rules) diff --git a/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py b/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py index 8cc16a893..43d3c5b9c 100644 --- a/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py +++ b/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py @@ -49,29 +49,31 @@ def test_qos_minimum_bandwidth_rule_lifecycle(self): # Create min bw rule rule = self.operator_cloud.create_qos_minimum_bandwidth_rule( - self.policy['id'], - min_kbps=min_kbps) + self.policy['id'], min_kbps=min_kbps + ) self.assertIn('id', rule) self.assertEqual(min_kbps, rule['min_kbps']) # Now try to update rule updated_rule = self.operator_cloud.update_qos_minimum_bandwidth_rule( - self.policy['id'], - rule['id'], - min_kbps=updated_min_kbps) + self.policy['id'], rule['id'], min_kbps=updated_min_kbps + ) self.assertIn('id', updated_rule) self.assertEqual(updated_min_kbps, updated_rule['min_kbps']) # List rules from policy policy_rules = self.operator_cloud.list_qos_minimum_bandwidth_rules( - self.policy['id']) + self.policy['id'] + ) self.assertEqual([updated_rule], policy_rules) # Delete rule self.operator_cloud.delete_qos_minimum_bandwidth_rule( - self.policy['id'], updated_rule['id']) + self.policy['id'], updated_rule['id'] + ) # Check if there is no rules in policy policy_rules = self.operator_cloud.list_qos_minimum_bandwidth_rules( - self.policy['id']) + self.policy['id'] + ) self.assertEqual([], policy_rules) diff --git a/openstack/tests/functional/cloud/test_qos_policy.py b/openstack/tests/functional/cloud/test_qos_policy.py index 6bd3150ce..b55b17d2f 100644 --- a/openstack/tests/functional/cloud/test_qos_policy.py +++ b/openstack/tests/functional/cloud/test_qos_policy.py @@ -56,7 +56,8 @@ def test_create_qos_policy_basic(self): def test_create_qos_policy_shared(self): policy = self.operator_cloud.create_qos_policy( - name=self.policy_name, shared=True) + name=self.policy_name, shared=True + ) self.assertIn('id', policy) self.assertEqual(self.policy_name, policy['name']) self.assertTrue(policy['is_shared']) @@ -64,10 +65,12 @@ def test_create_qos_policy_shared(self): def test_create_qos_policy_default(self): if not self.operator_cloud._has_neutron_extension('qos-default'): - self.skipTest("'qos-default' network extension not supported " - "by cloud") + self.skipTest( + "'qos-default' network extension not supported " "by cloud" + ) policy = self.operator_cloud.create_qos_policy( - name=self.policy_name, default=True) + name=self.policy_name, default=True + ) self.assertIn('id', policy) self.assertEqual(self.policy_name, policy['name']) self.assertFalse(policy['is_shared']) @@ -80,7 +83,8 @@ def test_update_qos_policy(self): self.assertFalse(policy['is_default']) updated_policy = self.operator_cloud.update_qos_policy( - policy['id'], shared=True, default=True) + policy['id'], shared=True, default=True + ) self.assertEqual(self.policy_name, updated_policy['name']) self.assertTrue(updated_policy['is_shared']) self.assertTrue(updated_policy['is_default']) @@ -89,9 +93,11 @@ def test_list_qos_policies_filtered(self): policy1 = self.operator_cloud.create_qos_policy(name=self.policy_name) self.assertIsNotNone(policy1) policy2 = self.operator_cloud.create_qos_policy( - name=self.policy_name + 'other') + name=self.policy_name + 'other' + ) self.assertIsNotNone(policy2) match = self.operator_cloud.list_qos_policies( - filters=dict(name=self.policy_name)) + filters=dict(name=self.policy_name) + ) self.assertEqual(1, len(match)) self.assertEqual(policy1['name'], match[0]['name']) diff --git a/openstack/tests/functional/cloud/test_quotas.py b/openstack/tests/functional/cloud/test_quotas.py index f23c6ae59..15525f5d2 100644 --- a/openstack/tests/functional/cloud/test_quotas.py +++ b/openstack/tests/functional/cloud/test_quotas.py @@ -21,11 +21,9 @@ class TestComputeQuotas(base.BaseFunctionalTest): - def test_get_quotas(self): '''Test quotas functionality''' - self.user_cloud.get_compute_quotas( - self.user_cloud.current_project_id) + self.user_cloud.get_compute_quotas(self.user_cloud.current_project_id) def test_set_quotas(self): '''Test quotas functionality''' @@ -36,15 +34,15 @@ def test_set_quotas(self): cores = quotas['cores'] self.operator_cloud.set_compute_quotas('demo', cores=cores + 1) self.assertEqual( - cores + 1, - self.operator_cloud.get_compute_quotas('demo')['cores']) + cores + 1, self.operator_cloud.get_compute_quotas('demo')['cores'] + ) self.operator_cloud.delete_compute_quotas('demo') self.assertEqual( - cores, self.operator_cloud.get_compute_quotas('demo')['cores']) + cores, self.operator_cloud.get_compute_quotas('demo')['cores'] + ) class TestVolumeQuotas(base.BaseFunctionalTest): - def setUp(self): super(TestVolumeQuotas, self).setUp() if not self.user_cloud.has_service('volume'): @@ -52,9 +50,7 @@ def setUp(self): def test_get_quotas(self): '''Test get quotas functionality''' - self.user_cloud.get_volume_quotas( - self.user_cloud.current_project_id - ) + self.user_cloud.get_volume_quotas(self.user_cloud.current_project_id) def test_set_quotas(self): '''Test set quotas functionality''' @@ -66,19 +62,18 @@ def test_set_quotas(self): self.operator_cloud.set_volume_quotas('demo', volumes=volumes + 1) self.assertEqual( volumes + 1, - self.operator_cloud.get_volume_quotas('demo')['volumes']) + self.operator_cloud.get_volume_quotas('demo')['volumes'], + ) self.operator_cloud.delete_volume_quotas('demo') self.assertEqual( - volumes, - self.operator_cloud.get_volume_quotas('demo')['volumes']) + volumes, self.operator_cloud.get_volume_quotas('demo')['volumes'] + ) class TestNetworkQuotas(base.BaseFunctionalTest): - def test_get_quotas(self): '''Test get quotas functionality''' - self.user_cloud.get_network_quotas( - self.user_cloud.current_project_id) + self.user_cloud.get_network_quotas(self.user_cloud.current_project_id) def test_quotas(self): '''Test quotas functionality''' @@ -92,11 +87,12 @@ def test_quotas(self): self.operator_cloud.set_network_quotas('demo', networks=network + 1) self.assertEqual( network + 1, - self.operator_cloud.get_network_quotas('demo')['networks']) + self.operator_cloud.get_network_quotas('demo')['networks'], + ) self.operator_cloud.delete_network_quotas('demo') self.assertEqual( - network, - self.operator_cloud.get_network_quotas('demo')['networks']) + network, self.operator_cloud.get_network_quotas('demo')['networks'] + ) def test_get_quotas_details(self): if not self.operator_cloud: @@ -105,14 +101,21 @@ def test_get_quotas_details(self): self.skipTest('network service not supported by cloud') quotas = [ - 'floating_ips', 'networks', 'ports', - 'rbac_policies', 'routers', 'subnets', - 'subnet_pools', 'security_group_rules', - 'security_groups'] + 'floating_ips', + 'networks', + 'ports', + 'rbac_policies', + 'routers', + 'subnets', + 'subnet_pools', + 'security_group_rules', + 'security_groups', + ] expected_keys = ['limit', 'used', 'reserved'] '''Test getting details about quota usage''' quota_details = self.operator_cloud.get_network_quotas( - 'demo', details=True) + 'demo', details=True + ) for quota in quotas: quota_val = quota_details[quota] if quota_val: diff --git a/openstack/tests/functional/cloud/test_range_search.py b/openstack/tests/functional/cloud/test_range_search.py index ab4870d7f..c0a186c0a 100644 --- a/openstack/tests/functional/cloud/test_range_search.py +++ b/openstack/tests/functional/cloud/test_range_search.py @@ -17,7 +17,6 @@ class TestRangeSearch(base.BaseFunctionalTest): - def _filter_m1_flavors(self, results): """The m1 flavors are the original devstack flavors""" new_results = [] @@ -30,7 +29,10 @@ def test_range_search_bad_range(self): flavors = self.user_cloud.list_flavors(get_extra=False) self.assertRaises( exc.OpenStackCloudException, - self.user_cloud.range_search, flavors, {"ram": "<1a0"}) + self.user_cloud.range_search, + flavors, + {"ram": "<1a0"}, + ) def test_range_search_exact(self): flavors = self.user_cloud.list_flavors(get_extra=False) @@ -103,7 +105,8 @@ def test_range_search_ge(self): def test_range_search_multi_1(self): flavors = self.user_cloud.list_flavors(get_extra=False) result = self.user_cloud.range_search( - flavors, {"ram": "MIN", "vcpus": "MIN"}) + flavors, {"ram": "MIN", "vcpus": "MIN"} + ) self.assertIsInstance(result, list) self.assertEqual(1, len(result)) # older devstack does not have cirros256 @@ -112,7 +115,8 @@ def test_range_search_multi_1(self): def test_range_search_multi_2(self): flavors = self.user_cloud.list_flavors(get_extra=False) result = self.user_cloud.range_search( - flavors, {"ram": "<1024", "vcpus": "MIN"}) + flavors, {"ram": "<1024", "vcpus": "MIN"} + ) self.assertIsInstance(result, list) result = self._filter_m1_flavors(result) self.assertEqual(1, len(result)) @@ -122,7 +126,8 @@ def test_range_search_multi_2(self): def test_range_search_multi_3(self): flavors = self.user_cloud.list_flavors(get_extra=False) result = self.user_cloud.range_search( - flavors, {"ram": ">=4096", "vcpus": "<6"}) + flavors, {"ram": ">=4096", "vcpus": "<6"} + ) self.assertIsInstance(result, list) result = self._filter_m1_flavors(result) self.assertEqual(2, len(result)) @@ -133,7 +138,8 @@ def test_range_search_multi_3(self): def test_range_search_multi_4(self): flavors = self.user_cloud.list_flavors(get_extra=False) result = self.user_cloud.range_search( - flavors, {"ram": ">=4096", "vcpus": "MAX"}) + flavors, {"ram": ">=4096", "vcpus": "MAX"} + ) self.assertIsInstance(result, list) self.assertEqual(1, len(result)) # This is the only result that should have max vcpu diff --git a/openstack/tests/functional/cloud/test_recordset.py b/openstack/tests/functional/cloud/test_recordset.py index 3b8550b46..709547a47 100644 --- a/openstack/tests/functional/cloud/test_recordset.py +++ b/openstack/tests/functional/cloud/test_recordset.py @@ -25,7 +25,6 @@ class TestRecordset(base.BaseFunctionalTest): - def setUp(self): super(TestRecordset, self).setUp() if not self.user_cloud.has_service('dns'): @@ -50,11 +49,9 @@ def test_recordsets_with_zone_id(self): zone_obj = self.user_cloud.create_zone(name=zone, email=email) # Test we can create a recordset and we get it returned - created_recordset = self.user_cloud.create_recordset(zone_obj['id'], - name, - type_, - records, - description, ttl) + created_recordset = self.user_cloud.create_recordset( + zone_obj['id'], name, type_, records, description, ttl + ) self.addCleanup(self.cleanup, zone, created_recordset['id']) self.assertEqual(created_recordset['zone_id'], zone_obj['id']) @@ -65,20 +62,22 @@ def test_recordsets_with_zone_id(self): self.assertEqual(created_recordset['ttl'], ttl) # Test that we can list recordsets - recordsets = self.user_cloud.list_recordsets(zone_obj['id'],) + recordsets = self.user_cloud.list_recordsets( + zone_obj['id'], + ) self.assertIsNotNone(recordsets) # Test we get the same recordset with the get_recordset method - get_recordset = self.user_cloud.get_recordset(zone_obj['id'], - created_recordset['id']) + get_recordset = self.user_cloud.get_recordset( + zone_obj['id'], created_recordset['id'] + ) self.assertEqual(get_recordset['id'], created_recordset['id']) # Test we can update a field on the recordset and only that field # is updated updated_recordset = self.user_cloud.update_recordset( - zone_obj['id'], - created_recordset['id'], - ttl=7200) + zone_obj['id'], created_recordset['id'], ttl=7200 + ) self.assertEqual(updated_recordset['id'], created_recordset['id']) self.assertEqual(updated_recordset['name'], name) self.assertEqual(updated_recordset['type'], type_.upper()) @@ -88,7 +87,8 @@ def test_recordsets_with_zone_id(self): # Test we can delete and get True returned deleted_recordset = self.user_cloud.delete_recordset( - zone, created_recordset['id']) + zone, created_recordset['id'] + ) self.assertTrue(deleted_recordset) def test_recordsets_with_zone_name(self): @@ -110,9 +110,9 @@ def test_recordsets_with_zone_name(self): zone_obj = self.user_cloud.create_zone(name=zone, email=email) # Test we can create a recordset and we get it returned - created_recordset = self.user_cloud.create_recordset(zone, name, type_, - records, - description, ttl) + created_recordset = self.user_cloud.create_recordset( + zone, name, type_, records, description, ttl + ) self.addCleanup(self.cleanup, zone, created_recordset['id']) self.assertEqual(created_recordset['zone_id'], zone_obj['id']) @@ -127,16 +127,16 @@ def test_recordsets_with_zone_name(self): self.assertIsNotNone(recordsets) # Test we get the same recordset with the get_recordset method - get_recordset = self.user_cloud.get_recordset(zone, - created_recordset['id']) + get_recordset = self.user_cloud.get_recordset( + zone, created_recordset['id'] + ) self.assertEqual(get_recordset['id'], created_recordset['id']) # Test we can update a field on the recordset and only that field # is updated updated_recordset = self.user_cloud.update_recordset( - zone_obj['id'], - created_recordset['id'], - ttl=7200) + zone_obj['id'], created_recordset['id'], ttl=7200 + ) self.assertEqual(updated_recordset['id'], created_recordset['id']) self.assertEqual(updated_recordset['name'], name) self.assertEqual(updated_recordset['type'], type_.upper()) @@ -146,10 +146,10 @@ def test_recordsets_with_zone_name(self): # Test we can delete and get True returned deleted_recordset = self.user_cloud.delete_recordset( - zone, created_recordset['id']) + zone, created_recordset['id'] + ) self.assertTrue(deleted_recordset) def cleanup(self, zone_name, recordset_id): - self.user_cloud.delete_recordset( - zone_name, recordset_id) + self.user_cloud.delete_recordset(zone_name, recordset_id) self.user_cloud.delete_zone(zone_name) diff --git a/openstack/tests/functional/cloud/test_router.py b/openstack/tests/functional/cloud/test_router.py index ef1dbc2d9..75dcf8d9c 100644 --- a/openstack/tests/functional/cloud/test_router.py +++ b/openstack/tests/functional/cloud/test_router.py @@ -24,8 +24,13 @@ EXPECTED_TOPLEVEL_FIELDS = ( - 'id', 'name', 'is_admin_state_up', 'external_gateway_info', - 'project_id', 'routes', 'status' + 'id', + 'name', + 'is_admin_state_up', + 'external_gateway_info', + 'project_id', + 'routes', + 'status', ) EXPECTED_GW_INFO_FIELDS = ('network_id', 'enable_snat', 'external_fixed_ips') @@ -90,7 +95,8 @@ def _cleanup_subnets(self): def test_create_router_basic(self): net1_name = self.network_prefix + '_net1' net1 = self.operator_cloud.create_network( - name=net1_name, external=True) + name=net1_name, external=True + ) router_name = self.router_prefix + '_create_basic' router = self.operator_cloud.create_router( @@ -117,14 +123,15 @@ def test_create_router_project(self): proj_id = project['id'] net1_name = self.network_prefix + '_net1' net1 = self.operator_cloud.create_network( - name=net1_name, external=True, project_id=proj_id) + name=net1_name, external=True, project_id=proj_id + ) router_name = self.router_prefix + '_create_project' router = self.operator_cloud.create_router( name=router_name, admin_state_up=True, ext_gateway_net_id=net1['id'], - project_id=proj_id + project_id=proj_id, ) for field in EXPECTED_TOPLEVEL_FIELDS: @@ -140,9 +147,9 @@ def test_create_router_project(self): self.assertEqual(net1['id'], ext_gw_info['network_id']) self.assertTrue(ext_gw_info['enable_snat']) - def _create_and_verify_advanced_router(self, - external_cidr, - external_gateway_ip=None): + def _create_and_verify_advanced_router( + self, external_cidr, external_gateway_ip=None + ): # external_cidr must be passed in as unicode (u'') # NOTE(Shrews): The arguments are needed because these tests # will run in parallel and we want to make sure that each test @@ -150,10 +157,13 @@ def _create_and_verify_advanced_router(self, net1_name = self.network_prefix + '_net1' sub1_name = self.subnet_prefix + '_sub1' net1 = self.operator_cloud.create_network( - name=net1_name, external=True) + name=net1_name, external=True + ) sub1 = self.operator_cloud.create_subnet( - net1['id'], external_cidr, subnet_name=sub1_name, - gateway_ip=external_gateway_ip + net1['id'], + external_cidr, + subnet_name=sub1_name, + gateway_ip=external_gateway_ip, ) ip_net = ipaddress.IPv4Network(external_cidr) @@ -165,9 +175,7 @@ def _create_and_verify_advanced_router(self, admin_state_up=False, ext_gateway_net_id=net1['id'], enable_snat=False, - ext_fixed_ips=[ - {'subnet_id': sub1['id'], 'ip_address': last_ip} - ] + ext_fixed_ips=[{'subnet_id': sub1['id'], 'ip_address': last_ip}], ) for field in EXPECTED_TOPLEVEL_FIELDS: @@ -183,12 +191,10 @@ def _create_and_verify_advanced_router(self, self.assertEqual(1, len(ext_gw_info['external_fixed_ips'])) self.assertEqual( - sub1['id'], - ext_gw_info['external_fixed_ips'][0]['subnet_id'] + sub1['id'], ext_gw_info['external_fixed_ips'][0]['subnet_id'] ) self.assertEqual( - last_ip, - ext_gw_info['external_fixed_ips'][0]['ip_address'] + last_ip, ext_gw_info['external_fixed_ips'][0]['ip_address'] ) return router @@ -198,20 +204,25 @@ def test_create_router_advanced(self): def test_add_remove_router_interface(self): router = self._create_and_verify_advanced_router( - external_cidr=u'10.3.3.0/24') + external_cidr=u'10.3.3.0/24' + ) net_name = self.network_prefix + '_intnet1' sub_name = self.subnet_prefix + '_intsub1' net = self.operator_cloud.create_network(name=net_name) sub = self.operator_cloud.create_subnet( - net['id'], '10.4.4.0/24', subnet_name=sub_name, - gateway_ip='10.4.4.1' + net['id'], + '10.4.4.0/24', + subnet_name=sub_name, + gateway_ip='10.4.4.1', ) iface = self.operator_cloud.add_router_interface( - router, subnet_id=sub['id']) + router, subnet_id=sub['id'] + ) self.assertIsNone( self.operator_cloud.remove_router_interface( - router, subnet_id=sub['id']) + router, subnet_id=sub['id'] + ) ) # Test return values *after* the interface is detached so the @@ -224,25 +235,32 @@ def test_add_remove_router_interface(self): def test_list_router_interfaces(self): router = self._create_and_verify_advanced_router( - external_cidr=u'10.5.5.0/24') + external_cidr=u'10.5.5.0/24' + ) net_name = self.network_prefix + '_intnet1' sub_name = self.subnet_prefix + '_intsub1' net = self.operator_cloud.create_network(name=net_name) sub = self.operator_cloud.create_subnet( - net['id'], '10.6.6.0/24', subnet_name=sub_name, - gateway_ip='10.6.6.1' + net['id'], + '10.6.6.0/24', + subnet_name=sub_name, + gateway_ip='10.6.6.1', ) iface = self.operator_cloud.add_router_interface( - router, subnet_id=sub['id']) + router, subnet_id=sub['id'] + ) all_ifaces = self.operator_cloud.list_router_interfaces(router) int_ifaces = self.operator_cloud.list_router_interfaces( - router, interface_type='internal') + router, interface_type='internal' + ) ext_ifaces = self.operator_cloud.list_router_interfaces( - router, interface_type='external') + router, interface_type='external' + ) self.assertIsNone( self.operator_cloud.remove_router_interface( - router, subnet_id=sub['id']) + router, subnet_id=sub['id'] + ) ) # Test return values *after* the interface is detached so the @@ -253,17 +271,21 @@ def test_list_router_interfaces(self): self.assertEqual(1, len(ext_ifaces)) ext_fixed_ips = router['external_gateway_info']['external_fixed_ips'] - self.assertEqual(ext_fixed_ips[0]['subnet_id'], - ext_ifaces[0]['fixed_ips'][0]['subnet_id']) + self.assertEqual( + ext_fixed_ips[0]['subnet_id'], + ext_ifaces[0]['fixed_ips'][0]['subnet_id'], + ) self.assertEqual(sub['id'], int_ifaces[0]['fixed_ips'][0]['subnet_id']) def test_update_router_name(self): router = self._create_and_verify_advanced_router( - external_cidr=u'10.7.7.0/24') + external_cidr=u'10.7.7.0/24' + ) new_name = self.router_prefix + '_update_name' updated = self.operator_cloud.update_router( - router['id'], name=new_name) + router['id'], name=new_name + ) self.assertIsNotNone(updated) for field in EXPECTED_TOPLEVEL_FIELDS: @@ -275,20 +297,20 @@ def test_update_router_name(self): # Validate nothing else changed self.assertEqual(router['status'], updated['status']) self.assertEqual(router['admin_state_up'], updated['admin_state_up']) - self.assertEqual(router['external_gateway_info'], - updated['external_gateway_info']) + self.assertEqual( + router['external_gateway_info'], updated['external_gateway_info'] + ) def test_update_router_routes(self): router = self._create_and_verify_advanced_router( - external_cidr=u'10.7.7.0/24') + external_cidr=u'10.7.7.0/24' + ) - routes = [{ - "destination": "10.7.7.0/24", - "nexthop": "10.7.7.99" - }] + routes = [{"destination": "10.7.7.0/24", "nexthop": "10.7.7.99"}] updated = self.operator_cloud.update_router( - router['id'], routes=routes) + router['id'], routes=routes + ) self.assertIsNotNone(updated) for field in EXPECTED_TOPLEVEL_FIELDS: @@ -300,15 +322,18 @@ def test_update_router_routes(self): # Validate nothing else changed self.assertEqual(router['status'], updated['status']) self.assertEqual(router['admin_state_up'], updated['admin_state_up']) - self.assertEqual(router['external_gateway_info'], - updated['external_gateway_info']) + self.assertEqual( + router['external_gateway_info'], updated['external_gateway_info'] + ) def test_update_router_admin_state(self): router = self._create_and_verify_advanced_router( - external_cidr=u'10.8.8.0/24') + external_cidr=u'10.8.8.0/24' + ) updated = self.operator_cloud.update_router( - router['id'], admin_state_up=True) + router['id'], admin_state_up=True + ) self.assertIsNotNone(updated) for field in EXPECTED_TOPLEVEL_FIELDS: @@ -316,25 +341,30 @@ def test_update_router_admin_state(self): # admin_state_up is the only change we expect self.assertTrue(updated['admin_state_up']) - self.assertNotEqual(router['admin_state_up'], - updated['admin_state_up']) + self.assertNotEqual( + router['admin_state_up'], updated['admin_state_up'] + ) # Validate nothing else changed self.assertEqual(router['status'], updated['status']) self.assertEqual(router['name'], updated['name']) - self.assertEqual(router['external_gateway_info'], - updated['external_gateway_info']) + self.assertEqual( + router['external_gateway_info'], updated['external_gateway_info'] + ) def test_update_router_ext_gw_info(self): router = self._create_and_verify_advanced_router( - external_cidr=u'10.9.9.0/24') + external_cidr=u'10.9.9.0/24' + ) # create a new subnet existing_net_id = router['external_gateway_info']['network_id'] sub_name = self.subnet_prefix + '_update' sub = self.operator_cloud.create_subnet( - existing_net_id, '10.10.10.0/24', subnet_name=sub_name, - gateway_ip='10.10.10.1' + existing_net_id, + '10.10.10.0/24', + subnet_name=sub_name, + gateway_ip='10.10.10.1', ) updated = self.operator_cloud.update_router( @@ -342,7 +372,7 @@ def test_update_router_ext_gw_info(self): ext_gateway_net_id=existing_net_id, ext_fixed_ips=[ {'subnet_id': sub['id'], 'ip_address': '10.10.10.77'} - ] + ], ) self.assertIsNotNone(updated) @@ -353,12 +383,10 @@ def test_update_router_ext_gw_info(self): ext_gw_info = updated['external_gateway_info'] self.assertEqual(1, len(ext_gw_info['external_fixed_ips'])) self.assertEqual( - sub['id'], - ext_gw_info['external_fixed_ips'][0]['subnet_id'] + sub['id'], ext_gw_info['external_fixed_ips'][0]['subnet_id'] ) self.assertEqual( - '10.10.10.77', - ext_gw_info['external_fixed_ips'][0]['ip_address'] + '10.10.10.77', ext_gw_info['external_fixed_ips'][0]['ip_address'] ) # Validate nothing else changed diff --git a/openstack/tests/functional/cloud/test_security_groups.py b/openstack/tests/functional/cloud/test_security_groups.py index 54ceda6fc..3ad785c9a 100644 --- a/openstack/tests/functional/cloud/test_security_groups.py +++ b/openstack/tests/functional/cloud/test_security_groups.py @@ -23,7 +23,8 @@ class TestSecurityGroups(base.BaseFunctionalTest): def test_create_list_security_groups(self): sg1 = self.user_cloud.create_security_group( - name="sg1", description="sg1") + name="sg1", description="sg1" + ) self.addCleanup(self.user_cloud.delete_security_group, sg1['id']) if self.user_cloud.has_service('network'): # Neutron defaults to all_tenants=1 when admin @@ -39,10 +40,12 @@ def test_create_list_security_groups_operator(self): self.skipTest("Operator cloud is required for this test") sg1 = self.user_cloud.create_security_group( - name="sg1", description="sg1") + name="sg1", description="sg1" + ) self.addCleanup(self.user_cloud.delete_security_group, sg1['id']) sg2 = self.operator_cloud.create_security_group( - name="sg2", description="sg2") + name="sg2", description="sg2" + ) self.addCleanup(self.operator_cloud.delete_security_group, sg2['id']) if self.user_cloud.has_service('network'): @@ -53,7 +56,8 @@ def test_create_list_security_groups_operator(self): # Filter by tenant_id (filtering by project_id won't work with # Keystone V2) sg_list = self.operator_cloud.list_security_groups( - filters={'tenant_id': self.user_cloud.current_project_id}) + filters={'tenant_id': self.user_cloud.current_project_id} + ) self.assertIn(sg1['id'], [sg['id'] for sg in sg_list]) self.assertNotIn(sg2['id'], [sg['id'] for sg in sg_list]) @@ -64,7 +68,8 @@ def test_create_list_security_groups_operator(self): self.assertNotIn(sg1['id'], [sg['id'] for sg in sg_list]) sg_list = self.operator_cloud.list_security_groups( - filters={'all_tenants': 1}) + filters={'all_tenants': 1} + ) self.assertIn(sg1['id'], [sg['id'] for sg in sg_list]) def test_get_security_group_by_id(self): diff --git a/openstack/tests/functional/cloud/test_server_group.py b/openstack/tests/functional/cloud/test_server_group.py index a1f645039..77d31d38d 100644 --- a/openstack/tests/functional/cloud/test_server_group.py +++ b/openstack/tests/functional/cloud/test_server_group.py @@ -21,15 +21,16 @@ class TestServerGroup(base.BaseFunctionalTest): - def test_server_group(self): server_group_name = self.getUniqueString() self.addCleanup(self.cleanup, server_group_name) server_group = self.user_cloud.create_server_group( - server_group_name, ['affinity']) + server_group_name, ['affinity'] + ) - server_group_ids = [v['id'] - for v in self.user_cloud.list_server_groups()] + server_group_ids = [ + v['id'] for v in self.user_cloud.list_server_groups() + ] self.assertIn(server_group['id'], server_group_ids) self.user_cloud.delete_server_group(server_group_name) diff --git a/openstack/tests/functional/cloud/test_services.py b/openstack/tests/functional/cloud/test_services.py index 70c85d58d..30ba4f5a4 100644 --- a/openstack/tests/functional/cloud/test_services.py +++ b/openstack/tests/functional/cloud/test_services.py @@ -38,15 +38,17 @@ def setUp(self): # Generate a random name for services in this test self.new_service_name = 'test_' + ''.join( - random.choice(string.ascii_lowercase) for _ in range(5)) + random.choice(string.ascii_lowercase) for _ in range(5) + ) self.addCleanup(self._cleanup_services) def _cleanup_services(self): exception_list = list() for s in self.operator_cloud.list_services(): - if s['name'] is not None and \ - s['name'].startswith(self.new_service_name): + if s['name'] is not None and s['name'].startswith( + self.new_service_name + ): try: self.operator_cloud.delete_service(name_or_id=s['id']) except Exception as e: @@ -60,45 +62,57 @@ def _cleanup_services(self): def test_create_service(self): service = self.operator_cloud.create_service( - name=self.new_service_name + '_create', type='test_type', - description='this is a test description') + name=self.new_service_name + '_create', + type='test_type', + description='this is a test description', + ) self.assertIsNotNone(service.get('id')) def test_update_service(self): ver = self.operator_cloud.config.get_api_version('identity') if ver.startswith('2'): # NOTE(SamYaple): Update service only works with v3 api - self.assertRaises(OpenStackCloudUnavailableFeature, - self.operator_cloud.update_service, - 'service_id', name='new name') + self.assertRaises( + OpenStackCloudUnavailableFeature, + self.operator_cloud.update_service, + 'service_id', + name='new name', + ) else: service = self.operator_cloud.create_service( - name=self.new_service_name + '_create', type='test_type', - description='this is a test description', enabled=True) + name=self.new_service_name + '_create', + type='test_type', + description='this is a test description', + enabled=True, + ) new_service = self.operator_cloud.update_service( service.id, name=self.new_service_name + '_update', description='this is an updated description', - enabled=False + enabled=False, + ) + self.assertEqual( + new_service.name, self.new_service_name + '_update' + ) + self.assertEqual( + new_service.description, 'this is an updated description' ) - self.assertEqual(new_service.name, - self.new_service_name + '_update') - self.assertEqual(new_service.description, - 'this is an updated description') self.assertFalse(new_service.is_enabled) self.assertEqual(service.id, new_service.id) def test_list_services(self): service = self.operator_cloud.create_service( - name=self.new_service_name + '_list', type='test_type') + name=self.new_service_name + '_list', type='test_type' + ) observed_services = self.operator_cloud.list_services() self.assertIsInstance(observed_services, list) found = False for s in observed_services: # Test all attributes are returned if s['id'] == service['id']: - self.assertEqual(self.new_service_name + '_list', - s.get('name')) + self.assertEqual( + self.new_service_name + '_list', s.get('name') + ) self.assertEqual('test_type', s.get('type')) found = True self.assertTrue(found, msg='new service not found in service list!') @@ -106,8 +120,8 @@ def test_list_services(self): def test_delete_service_by_name(self): # Test delete by name service = self.operator_cloud.create_service( - name=self.new_service_name + '_delete_by_name', - type='test_type') + name=self.new_service_name + '_delete_by_name', type='test_type' + ) self.operator_cloud.delete_service(name_or_id=service['name']) observed_services = self.operator_cloud.list_services() found = False @@ -120,8 +134,8 @@ def test_delete_service_by_name(self): def test_delete_service_by_id(self): # Test delete by id service = self.operator_cloud.create_service( - name=self.new_service_name + '_delete_by_id', - type='test_type') + name=self.new_service_name + '_delete_by_id', type='test_type' + ) self.operator_cloud.delete_service(name_or_id=service['id']) observed_services = self.operator_cloud.list_services() found = False diff --git a/openstack/tests/functional/cloud/test_stack.py b/openstack/tests/functional/cloud/test_stack.py index 0df37c462..ee2ae9775 100644 --- a/openstack/tests/functional/cloud/test_stack.py +++ b/openstack/tests/functional/cloud/test_stack.py @@ -73,7 +73,6 @@ class TestStack(base.BaseFunctionalTest): - def setUp(self): super(TestStack, self).setUp() if not self.user_cloud.has_service('orchestration'): @@ -88,10 +87,12 @@ def test_stack_validation(self): test_template.write(validate_template.encode('utf-8')) test_template.close() stack_name = self.getUniqueString('validate_template') - self.assertRaises(exc.OpenStackCloudException, - self.user_cloud.create_stack, - name=stack_name, - template_file=test_template.name) + self.assertRaises( + exc.OpenStackCloudException, + self.user_cloud.create_stack, + name=stack_name, + template_file=test_template.name, + ) def test_stack_simple(self): test_template = tempfile.NamedTemporaryFile(delete=False) @@ -100,9 +101,8 @@ def test_stack_simple(self): self.stack_name = self.getUniqueString('simple_stack') self.addCleanup(self._cleanup_stack) stack = self.user_cloud.create_stack( - name=self.stack_name, - template_file=test_template.name, - wait=True) + name=self.stack_name, template_file=test_template.name, wait=True + ) # assert expected values in stack self.assertEqual('CREATE_COMPLETE', stack['stack_status']) @@ -121,9 +121,8 @@ def test_stack_simple(self): # update with no changes stack = self.user_cloud.update_stack( - self.stack_name, - template_file=test_template.name, - wait=True) + self.stack_name, template_file=test_template.name, wait=True + ) # assert no change in updated stack self.assertEqual('UPDATE_COMPLETE', stack['stack_status']) @@ -135,7 +134,8 @@ def test_stack_simple(self): self.stack_name, template_file=test_template.name, wait=True, - length=12) + length=12, + ) # assert changed output in updated stack stack = self.user_cloud.get_stack(self.stack_name) @@ -147,7 +147,8 @@ def test_stack_simple(self): def test_stack_nested(self): test_template = tempfile.NamedTemporaryFile( - suffix='.yaml', delete=False) + suffix='.yaml', delete=False + ) test_template.write(root_template.encode('utf-8')) test_template.close() @@ -166,7 +167,8 @@ def test_stack_nested(self): name=self.stack_name, template_file=test_template.name, environment_files=[env.name], - wait=True) + wait=True, + ) # assert expected values in stack self.assertEqual('CREATE_COMPLETE', stack['stack_status']) diff --git a/openstack/tests/functional/cloud/test_users.py b/openstack/tests/functional/cloud/test_users.py index 3dc94a220..32e58b231 100644 --- a/openstack/tests/functional/cloud/test_users.py +++ b/openstack/tests/functional/cloud/test_users.py @@ -105,7 +105,8 @@ def test_update_user(self): email='somebody@nowhere.com', enabled=False, password='secret', - description='') + description='', + ) self.assertIsNotNone(new_user) self.assertEqual(user['id'], new_user['id']) self.assertEqual(user_name + '2', new_user['name']) @@ -115,30 +116,37 @@ def test_update_user(self): def test_update_user_password(self): user_name = self.user_prefix + '_password' user_email = 'nobody@nowhere.com' - user = self._create_user(name=user_name, - email=user_email, - password='old_secret') + user = self._create_user( + name=user_name, email=user_email, password='old_secret' + ) self.assertIsNotNone(user) self.assertTrue(user['enabled']) # This should work for both v2 and v3 new_user = self.operator_cloud.update_user( - user['id'], password='new_secret') + user['id'], password='new_secret' + ) self.assertIsNotNone(new_user) self.assertEqual(user['id'], new_user['id']) self.assertEqual(user_name, new_user['name']) self.assertEqual(user_email, new_user['email']) self.assertTrue(new_user['enabled']) - self.assertTrue(self.operator_cloud.grant_role( - 'member', user=user['id'], project='demo', wait=True)) + self.assertTrue( + self.operator_cloud.grant_role( + 'member', user=user['id'], project='demo', wait=True + ) + ) self.addCleanup( self.operator_cloud.revoke_role, - 'member', user=user['id'], project='demo', wait=True) + 'member', + user=user['id'], + project='demo', + wait=True, + ) new_cloud = self.operator_cloud.connect_as( - user_id=user['id'], - password='new_secret', - project_name='demo') + user_id=user['id'], password='new_secret', project_name='demo' + ) self.assertIsNotNone(new_cloud) location = new_cloud.current_location @@ -166,9 +174,11 @@ def test_users_and_groups(self): # Add the user to the group self.operator_cloud.add_user_to_group(user_name, group_name) self.assertTrue( - self.operator_cloud.is_user_in_group(user_name, group_name)) + self.operator_cloud.is_user_in_group(user_name, group_name) + ) # Remove them from the group self.operator_cloud.remove_user_from_group(user_name, group_name) self.assertFalse( - self.operator_cloud.is_user_in_group(user_name, group_name)) + self.operator_cloud.is_user_in_group(user_name, group_name) + ) diff --git a/openstack/tests/functional/cloud/test_volume.py b/openstack/tests/functional/cloud/test_volume.py index 7b355f928..722f59967 100644 --- a/openstack/tests/functional/cloud/test_volume.py +++ b/openstack/tests/functional/cloud/test_volume.py @@ -43,10 +43,10 @@ def test_volumes(self): self.addDetail('volume', content.text_content(volume_name)) self.addCleanup(self.cleanup, volume_name, snapshot_name=snapshot_name) volume = self.user_cloud.create_volume( - display_name=volume_name, size=1) + display_name=volume_name, size=1 + ) snapshot = self.user_cloud.create_volume_snapshot( - volume['id'], - display_name=snapshot_name + volume['id'], display_name=snapshot_name ) ret_volume = self.user_cloud.get_volume_by_id(volume['id']) @@ -60,7 +60,8 @@ def test_volumes(self): self.assertIn(snapshot['id'], snapshot_ids) ret_snapshot = self.user_cloud.get_volume_snapshot_by_id( - snapshot['id']) + snapshot['id'] + ) self.assertEqual(snapshot['id'], ret_snapshot['id']) self.user_cloud.delete_volume_snapshot(snapshot_name, wait=True) @@ -73,9 +74,11 @@ def test_volume_to_image(self): self.addDetail('volume', content.text_content(volume_name)) self.addCleanup(self.cleanup, volume_name, image_name=image_name) volume = self.user_cloud.create_volume( - display_name=volume_name, size=1) + display_name=volume_name, size=1 + ) image = self.user_cloud.create_image( - image_name, volume=volume, wait=True) + image_name, volume=volume, wait=True + ) volume_ids = [v['id'] for v in self.user_cloud.list_volumes()] self.assertIn(volume['id'], volume_ids) @@ -93,7 +96,8 @@ def cleanup(self, volume, snapshot_name=None, image_name=None): snapshot = self.user_cloud.get_volume_snapshot(snapshot_name) if snapshot: self.user_cloud.delete_volume_snapshot( - snapshot_name, wait=True) + snapshot_name, wait=True + ) if image_name: image = self.user_cloud.get_image(image_name) if image: @@ -108,7 +112,8 @@ def cleanup(self, volume, snapshot_name=None, image_name=None): self.user_cloud.delete_volume(v, wait=False) try: for count in utils.iterate_timeout( - 180, "Timeout waiting for volume cleanup"): + 180, "Timeout waiting for volume cleanup" + ): found = False for existing in self.user_cloud.list_volumes(): for v in volume: @@ -127,7 +132,8 @@ def cleanup(self, volume, snapshot_name=None, image_name=None): for v in volume: if v['id'] == existing['id']: self.operator_cloud.delete_volume( - v, wait=False, force=True) + v, wait=False, force=True + ) def test_list_volumes_pagination(self): '''Test pagination for list volumes functionality''' @@ -146,9 +152,7 @@ def test_list_volumes_pagination(self): for i in self.user_cloud.list_volumes(): if i['name'] and i['name'].startswith(self.id()): result.append(i['id']) - self.assertEqual( - sorted([i['id'] for i in volumes]), - sorted(result)) + self.assertEqual(sorted([i['id'] for i in volumes]), sorted(result)) def test_update_volume(self): name, desc = self.getUniqueString('name'), self.getUniqueString('desc') diff --git a/openstack/tests/functional/cloud/test_volume_backup.py b/openstack/tests/functional/cloud/test_volume_backup.py index c864f88b8..ce3c89e1c 100644 --- a/openstack/tests/functional/cloud/test_volume_backup.py +++ b/openstack/tests/functional/cloud/test_volume_backup.py @@ -27,14 +27,18 @@ def setUp(self): def test_create_get_delete_volume_backup(self): volume = self.user_cloud.create_volume( - display_name=self.getUniqueString(), size=1) + display_name=self.getUniqueString(), size=1 + ) self.addCleanup(self.user_cloud.delete_volume, volume['id']) backup_name_1 = self.getUniqueString() backup_desc_1 = self.getUniqueString() backup = self.user_cloud.create_volume_backup( - volume_id=volume['id'], name=backup_name_1, - description=backup_desc_1, wait=True) + volume_id=volume['id'], + name=backup_name_1, + description=backup_desc_1, + wait=True, + ) self.assertEqual(backup_name_1, backup['name']) backup = self.user_cloud.get_volume_backup(backup['id']) @@ -48,11 +52,13 @@ def test_create_get_delete_volume_backup_from_snapshot(self): volume = self.user_cloud.create_volume(size=1) snapshot = self.user_cloud.create_volume_snapshot(volume['id']) self.addCleanup(self.user_cloud.delete_volume, volume['id']) - self.addCleanup(self.user_cloud.delete_volume_snapshot, snapshot['id'], - wait=True) + self.addCleanup( + self.user_cloud.delete_volume_snapshot, snapshot['id'], wait=True + ) backup = self.user_cloud.create_volume_backup( - volume_id=volume['id'], snapshot_id=snapshot['id'], wait=True) + volume_id=volume['id'], snapshot_id=snapshot['id'], wait=True + ) backup = self.user_cloud.get_volume_backup(backup['id']) self.assertEqual(backup['snapshot_id'], snapshot['id']) @@ -65,9 +71,11 @@ def test_create_get_delete_incremental_volume_backup(self): self.addCleanup(self.user_cloud.delete_volume, volume['id']) full_backup = self.user_cloud.create_volume_backup( - volume_id=volume['id'], wait=True) + volume_id=volume['id'], wait=True + ) incr_backup = self.user_cloud.create_volume_backup( - volume_id=volume['id'], incremental=True, wait=True) + volume_id=volume['id'], incremental=True, wait=True + ) full_backup = self.user_cloud.get_volume_backup(full_backup['id']) incr_backup = self.user_cloud.get_volume_backup(incr_backup['id']) @@ -81,7 +89,8 @@ def test_create_get_delete_incremental_volume_backup(self): def test_list_volume_backups(self): vol1 = self.user_cloud.create_volume( - display_name=self.getUniqueString(), size=1) + display_name=self.getUniqueString(), size=1 + ) self.addCleanup(self.user_cloud.delete_volume, vol1['id']) # We create 2 volumes to create 2 backups. We could have created 2 @@ -89,12 +98,14 @@ def test_list_volume_backups(self): # to be race-condition prone. And I didn't want to use an ugly sleep() # here. vol2 = self.user_cloud.create_volume( - display_name=self.getUniqueString(), size=1) + display_name=self.getUniqueString(), size=1 + ) self.addCleanup(self.user_cloud.delete_volume, vol2['id']) backup_name_1 = self.getUniqueString() backup = self.user_cloud.create_volume_backup( - volume_id=vol1['id'], name=backup_name_1) + volume_id=vol1['id'], name=backup_name_1 + ) self.addCleanup(self.user_cloud.delete_volume_backup, backup['id']) backup = self.user_cloud.create_volume_backup(volume_id=vol2['id']) @@ -104,6 +115,7 @@ def test_list_volume_backups(self): self.assertEqual(2, len(backups)) backups = self.user_cloud.list_volume_backups( - search_opts={"name": backup_name_1}) + search_opts={"name": backup_name_1} + ) self.assertEqual(1, len(backups)) self.assertEqual(backup_name_1, backups[0]['name']) diff --git a/openstack/tests/functional/cloud/test_volume_type.py b/openstack/tests/functional/cloud/test_volume_type.py index d71ee014c..ab8b0e5a6 100644 --- a/openstack/tests/functional/cloud/test_volume_type.py +++ b/openstack/tests/functional/cloud/test_volume_type.py @@ -25,7 +25,6 @@ class TestVolumeType(base.BaseFunctionalTest): - def _assert_project(self, volume_name_or_id, project_id, allowed=True): acls = self.operator_cloud.get_volume_type_access(volume_name_or_id) allowed_projects = [x.get('project_id') for x in acls] @@ -40,83 +39,87 @@ def setUp(self): volume_type = { "name": 'test-volume-type', "description": None, - "os-volume-type-access:is_public": False} + "os-volume-type-access:is_public": False, + } self.operator_cloud.block_storage.post( - '/types', json={'volume_type': volume_type}) + '/types', json={'volume_type': volume_type} + ) def tearDown(self): ret = self.operator_cloud.get_volume_type('test-volume-type') if ret.get('id'): self.operator_cloud.block_storage.delete( - '/types/{volume_type_id}'.format(volume_type_id=ret.id)) + '/types/{volume_type_id}'.format(volume_type_id=ret.id) + ) super(TestVolumeType, self).tearDown() def test_list_volume_types(self): volume_types = self.operator_cloud.list_volume_types() self.assertTrue(volume_types) - self.assertTrue(any( - x for x in volume_types if x.name == 'test-volume-type')) + self.assertTrue( + any(x for x in volume_types if x.name == 'test-volume-type') + ) def test_add_remove_volume_type_access(self): volume_type = self.operator_cloud.get_volume_type('test-volume-type') self.assertEqual('test-volume-type', volume_type.name) self.operator_cloud.add_volume_type_access( - 'test-volume-type', - self.operator_cloud.current_project_id) + 'test-volume-type', self.operator_cloud.current_project_id + ) self._assert_project( - 'test-volume-type', self.operator_cloud.current_project_id, - allowed=True) + 'test-volume-type', + self.operator_cloud.current_project_id, + allowed=True, + ) self.operator_cloud.remove_volume_type_access( - 'test-volume-type', - self.operator_cloud.current_project_id) + 'test-volume-type', self.operator_cloud.current_project_id + ) self._assert_project( - 'test-volume-type', self.operator_cloud.current_project_id, - allowed=False) + 'test-volume-type', + self.operator_cloud.current_project_id, + allowed=False, + ) def test_add_volume_type_access_missing_project(self): # Project id is not valitaded and it may not exist. self.operator_cloud.add_volume_type_access( - 'test-volume-type', - '00000000000000000000000000000000') + 'test-volume-type', '00000000000000000000000000000000' + ) self.operator_cloud.remove_volume_type_access( - 'test-volume-type', - '00000000000000000000000000000000') + 'test-volume-type', '00000000000000000000000000000000' + ) def test_add_volume_type_access_missing_volume(self): with testtools.ExpectedException( - exc.OpenStackCloudException, - "VolumeType not found.*" + exc.OpenStackCloudException, "VolumeType not found.*" ): self.operator_cloud.add_volume_type_access( - 'MISSING_VOLUME_TYPE', - self.operator_cloud.current_project_id) + 'MISSING_VOLUME_TYPE', self.operator_cloud.current_project_id + ) def test_remove_volume_type_access_missing_volume(self): with testtools.ExpectedException( - exc.OpenStackCloudException, - "VolumeType not found.*" + exc.OpenStackCloudException, "VolumeType not found.*" ): self.operator_cloud.remove_volume_type_access( - 'MISSING_VOLUME_TYPE', - self.operator_cloud.current_project_id) + 'MISSING_VOLUME_TYPE', self.operator_cloud.current_project_id + ) def test_add_volume_type_access_bad_project(self): with testtools.ExpectedException( - exc.OpenStackCloudBadRequest, - "Unable to authorize.*" + exc.OpenStackCloudBadRequest, "Unable to authorize.*" ): self.operator_cloud.add_volume_type_access( - 'test-volume-type', - 'BAD_PROJECT_ID') + 'test-volume-type', 'BAD_PROJECT_ID' + ) def test_remove_volume_type_access_missing_project(self): with testtools.ExpectedException( - exc.OpenStackCloudURINotFound, - "Unable to revoke.*" + exc.OpenStackCloudURINotFound, "Unable to revoke.*" ): self.operator_cloud.remove_volume_type_access( - 'test-volume-type', - '00000000000000000000000000000000') + 'test-volume-type', '00000000000000000000000000000000' + ) diff --git a/openstack/tests/functional/cloud/test_zone.py b/openstack/tests/functional/cloud/test_zone.py index 31ea5de15..c9625fe0f 100644 --- a/openstack/tests/functional/cloud/test_zone.py +++ b/openstack/tests/functional/cloud/test_zone.py @@ -23,7 +23,6 @@ class TestZone(base.BaseFunctionalTest): - def setUp(self): super(TestZone, self).setUp() if not self.user_cloud.has_service('dns'): @@ -43,9 +42,13 @@ def test_zones(self): # Test we can create a zone and we get it returned zone = self.user_cloud.create_zone( - name=name, zone_type=zone_type, email=email, - description=description, ttl=ttl, - masters=masters) + name=name, + zone_type=zone_type, + email=email, + description=description, + ttl=ttl, + masters=masters, + ) self.assertEqual(zone['name'], name) self.assertEqual(zone['type'], zone_type.upper()) self.assertEqual(zone['email'], email) diff --git a/openstack/tests/unit/cloud/test__utils.py b/openstack/tests/unit/cloud/test__utils.py index 20cabae4f..de5a37cde 100644 --- a/openstack/tests/unit/cloud/test__utils.py +++ b/openstack/tests/unit/cloud/test__utils.py @@ -32,7 +32,6 @@ class TestUtils(base.TestCase): - def test__filter_list_name_or_id(self): el1 = dict(id=100, name='donald') el2 = dict(id=200, name='pluto') @@ -85,18 +84,28 @@ def test__filter_list_name_or_id_glob_not_found(self): self.assertEqual([], ret) def test__filter_list_unicode(self): - el1 = dict(id=100, name=u'中文', last='duck', - other=dict(category='duck', financial=dict(status='poor'))) - el2 = dict(id=200, name=u'中文', last='trump', - other=dict(category='human', financial=dict(status='rich'))) - el3 = dict(id=300, name='donald', last='ronald mac', - other=dict(category='clown', financial=dict(status='rich'))) + el1 = dict( + id=100, + name=u'中文', + last='duck', + other=dict(category='duck', financial=dict(status='poor')), + ) + el2 = dict( + id=200, + name=u'中文', + last='trump', + other=dict(category='human', financial=dict(status='rich')), + ) + el3 = dict( + id=300, + name='donald', + last='ronald mac', + other=dict(category='clown', financial=dict(status='rich')), + ) data = [el1, el2, el3] ret = _utils._filter_list( - data, u'中文', - {'other': { - 'financial': {'status': 'rich'} - }}) + data, u'中文', {'other': {'financial': {'status': 'rich'}}} + ) self.assertEqual([el2], ret) def test__filter_list_filter(self): @@ -114,30 +123,47 @@ def test__filter_list_filter_jmespath(self): self.assertEqual([el1], ret) def test__filter_list_dict1(self): - el1 = dict(id=100, name='donald', last='duck', - other=dict(category='duck')) - el2 = dict(id=200, name='donald', last='trump', - other=dict(category='human')) - el3 = dict(id=300, name='donald', last='ronald mac', - other=dict(category='clown')) + el1 = dict( + id=100, name='donald', last='duck', other=dict(category='duck') + ) + el2 = dict( + id=200, name='donald', last='trump', other=dict(category='human') + ) + el3 = dict( + id=300, + name='donald', + last='ronald mac', + other=dict(category='clown'), + ) data = [el1, el2, el3] ret = _utils._filter_list( - data, 'donald', {'other': {'category': 'clown'}}) + data, 'donald', {'other': {'category': 'clown'}} + ) self.assertEqual([el3], ret) def test__filter_list_dict2(self): - el1 = dict(id=100, name='donald', last='duck', - other=dict(category='duck', financial=dict(status='poor'))) - el2 = dict(id=200, name='donald', last='trump', - other=dict(category='human', financial=dict(status='rich'))) - el3 = dict(id=300, name='donald', last='ronald mac', - other=dict(category='clown', financial=dict(status='rich'))) + el1 = dict( + id=100, + name='donald', + last='duck', + other=dict(category='duck', financial=dict(status='poor')), + ) + el2 = dict( + id=200, + name='donald', + last='trump', + other=dict(category='human', financial=dict(status='rich')), + ) + el3 = dict( + id=300, + name='donald', + last='ronald mac', + other=dict(category='clown', financial=dict(status='rich')), + ) data = [el1, el2, el3] ret = _utils._filter_list( - data, 'donald', - {'other': { - 'financial': {'status': 'rich'} - }}) + data, 'donald', {'other': {'financial': {'status': 'rich'}}} + ) self.assertEqual([el2, el3], ret) def test_safe_dict_min_ints(self): @@ -176,7 +202,7 @@ def test_safe_dict_min_not_int(self): with testtools.ExpectedException( exc.OpenStackCloudException, "Search for minimum value failed. " - "Value for f1 is not an integer: aaa" + "Value for f1 is not an integer: aaa", ): _utils.safe_dict_min('f1', data) @@ -216,7 +242,7 @@ def test_safe_dict_max_not_int(self): with testtools.ExpectedException( exc.OpenStackCloudException, "Search for maximum value failed. " - "Value for f1 is not an integer: aaa" + "Value for f1 is not an integer: aaa", ): _utils.safe_dict_max('f1', data) @@ -282,15 +308,13 @@ def test_range_filter_exact(self): def test_range_filter_invalid_int(self): with testtools.ExpectedException( - exc.OpenStackCloudException, - "Invalid range value: <1A0" + exc.OpenStackCloudException, "Invalid range value: <1A0" ): _utils.range_filter(RANGE_DATA, "key1", "<1A0") def test_range_filter_invalid_op(self): with testtools.ExpectedException( - exc.OpenStackCloudException, - "Invalid range value: <>100" + exc.OpenStackCloudException, "Invalid range value: <>100" ): _utils.range_filter(RANGE_DATA, "key1", "<>100") @@ -330,8 +354,16 @@ def test_get_entity_no_uuid_like(self): def test_get_entity_pass_uuid(self): uuid = uuid4().hex self.cloud.use_direct_get = True - resources = ['flavor', 'image', 'volume', 'network', - 'subnet', 'port', 'floating_ip', 'security_group'] + resources = [ + 'flavor', + 'image', + 'volume', + 'network', + 'subnet', + 'port', + 'floating_ip', + 'security_group', + ] for r in resources: f = 'get_%s_by_id' % r with mock.patch.object(self.cloud, f) as get: @@ -340,8 +372,16 @@ def test_get_entity_pass_uuid(self): def test_get_entity_pass_search_methods(self): self.cloud.use_direct_get = True - resources = ['flavor', 'image', 'volume', 'network', - 'subnet', 'port', 'floating_ip', 'security_group'] + resources = [ + 'flavor', + 'image', + 'volume', + 'network', + 'subnet', + 'port', + 'floating_ip', + 'security_group', + ] filters = {} name = 'name_no_uuid' for r in resources: @@ -351,8 +391,16 @@ def test_get_entity_pass_search_methods(self): search.assert_called_once_with(name, filters) def test_get_entity_get_and_search(self): - resources = ['flavor', 'image', 'volume', 'network', - 'subnet', 'port', 'floating_ip', 'security_group'] + resources = [ + 'flavor', + 'image', + 'volume', + 'network', + 'subnet', + 'port', + 'floating_ip', + 'security_group', + ] for r in resources: self.assertTrue(hasattr(self.cloud, 'get_%s_by_id' % r)) self.assertTrue(hasattr(self.cloud, 'search_%ss' % r)) diff --git a/openstack/tests/unit/cloud/test_accelerator.py b/openstack/tests/unit/cloud/test_accelerator.py index 39dcf761e..e42757073 100644 --- a/openstack/tests/unit/cloud/test_accelerator.py +++ b/openstack/tests/unit/cloud/test_accelerator.py @@ -23,7 +23,7 @@ 'parent_id': None, 'root_id': 1, 'num_accelerators': 4, - 'device_id': 0 + 'device_id': 0, } DEV_UUID = uuid.uuid4().hex @@ -40,14 +40,16 @@ DEV_PROF_UUID = uuid.uuid4().hex DEV_PROF_GROUPS = [ - {"resources:ACCELERATOR_FPGA": "1", - "trait:CUSTOM_FPGA_INTEL_PAC_ARRIA10": "required", - "trait:CUSTOM_FUNCTION_ID_3AFB": "required", - }, - {"resources:CUSTOM_ACCELERATOR_FOO": "2", - "resources:CUSTOM_MEMORY": "200", - "trait:CUSTOM_TRAIT_ALWAYS": "required", - } + { + "resources:ACCELERATOR_FPGA": "1", + "trait:CUSTOM_FPGA_INTEL_PAC_ARRIA10": "required", + "trait:CUSTOM_FUNCTION_ID_3AFB": "required", + }, + { + "resources:CUSTOM_ACCELERATOR_FOO": "2", + "resources:CUSTOM_MEMORY": "200", + "trait:CUSTOM_TRAIT_ALWAYS": "required", + }, ] DEV_PROF_DICT = { "id": 1, @@ -61,10 +63,9 @@ ARQ_UUID = uuid.uuid4().hex ARQ_DEV_RP_UUID = uuid.uuid4().hex ARQ_INSTANCE_UUID = uuid.uuid4().hex -ARQ_ATTACH_INFO_STR = '{"bus": "5e", '\ - '"device": "00", '\ - '"domain": "0000", '\ - '"function": "1"}' +ARQ_ATTACH_INFO_STR = ( + '{"bus": "5e", ' '"device": "00", ' '"domain": "0000", ' '"function": "1"}' +) ARQ_DICT = { 'uuid': ARQ_UUID, 'hostname': 'test_hostname', @@ -85,36 +86,41 @@ def setUp(self): self.use_cyborg() def test_list_deployables(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'accelerator', - 'public', - append=['v2', 'deployables']), - json={'deployables': [DEP_DICT]} - ), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'accelerator', 'public', append=['v2', 'deployables'] + ), + json={'deployables': [DEP_DICT]}, + ), + ] + ) dep_list = self.cloud.list_deployables() self.assertEqual(len(dep_list), 1) self.assertEqual(dep_list[0].id, DEP_DICT['uuid']) self.assertEqual(dep_list[0].name, DEP_DICT['name']) self.assertEqual(dep_list[0].parent_id, DEP_DICT['parent_id']) self.assertEqual(dep_list[0].root_id, DEP_DICT['root_id']) - self.assertEqual(dep_list[0].num_accelerators, - DEP_DICT['num_accelerators']) + self.assertEqual( + dep_list[0].num_accelerators, DEP_DICT['num_accelerators'] + ) self.assertEqual(dep_list[0].device_id, DEP_DICT['device_id']) self.assert_calls() def test_list_devices(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'accelerator', - 'public', - append=['v2', 'devices']), - json={'devices': [DEV_DICT]} - ), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'accelerator', 'public', append=['v2', 'devices'] + ), + json={'devices': [DEV_DICT]}, + ), + ] + ) dev_list = self.cloud.list_devices() self.assertEqual(len(dev_list), 1) self.assertEqual(dev_list[0].id, DEV_DICT['id']) @@ -123,22 +129,28 @@ def test_list_devices(self): self.assertEqual(dev_list[0].type, DEV_DICT['type']) self.assertEqual(dev_list[0].vendor, DEV_DICT['vendor']) self.assertEqual(dev_list[0].model, DEV_DICT['model']) - self.assertEqual(dev_list[0].std_board_info, - DEV_DICT['std_board_info']) - self.assertEqual(dev_list[0].vendor_board_info, - DEV_DICT['vendor_board_info']) + self.assertEqual( + dev_list[0].std_board_info, DEV_DICT['std_board_info'] + ) + self.assertEqual( + dev_list[0].vendor_board_info, DEV_DICT['vendor_board_info'] + ) self.assert_calls() def test_list_device_profiles(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'accelerator', - 'public', - append=['v2', 'device_profiles']), - json={'device_profiles': [DEV_PROF_DICT]} - ), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'accelerator', + 'public', + append=['v2', 'device_profiles'], + ), + json={'device_profiles': [DEV_PROF_DICT]}, + ), + ] + ) dev_prof_list = self.cloud.list_device_profiles() self.assertEqual(len(dev_prof_list), 1) self.assertEqual(dev_prof_list[0].id, DEV_PROF_DICT['id']) @@ -148,183 +160,248 @@ def test_list_device_profiles(self): self.assert_calls() def test_create_device_profile(self): - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'accelerator', - 'public', - append=['v2', 'device_profiles']), - json=NEW_DEV_PROF_DICT) - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'accelerator', + 'public', + append=['v2', 'device_profiles'], + ), + json=NEW_DEV_PROF_DICT, + ) + ] + ) attrs = { 'name': NEW_DEV_PROF_DICT['name'], - 'groups': NEW_DEV_PROF_DICT['groups'] + 'groups': NEW_DEV_PROF_DICT['groups'], } - self.assertTrue( - self.cloud.create_device_profile( - attrs - ) - ) + self.assertTrue(self.cloud.create_device_profile(attrs)) self.assert_calls() def test_delete_device_profile(self, filters=None): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'accelerator', - 'public', - append=['v2', 'device_profiles', DEV_PROF_DICT['name']]), - json={"device_profiles": [DEV_PROF_DICT]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'accelerator', - 'public', - append=['v2', 'device_profiles', DEV_PROF_DICT['name']]), - json=DEV_PROF_DICT) - - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'accelerator', + 'public', + append=[ + 'v2', + 'device_profiles', + DEV_PROF_DICT['name'], + ], + ), + json={"device_profiles": [DEV_PROF_DICT]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'accelerator', + 'public', + append=[ + 'v2', + 'device_profiles', + DEV_PROF_DICT['name'], + ], + ), + json=DEV_PROF_DICT, + ), + ] + ) self.assertTrue( - self.cloud.delete_device_profile( - DEV_PROF_DICT['name'], - filters - ) + self.cloud.delete_device_profile(DEV_PROF_DICT['name'], filters) ) self.assert_calls() def test_list_accelerator_requests(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'accelerator', - 'public', - append=['v2', 'accelerator_requests']), - json={'arqs': [ARQ_DICT]} - ), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'accelerator', + 'public', + append=['v2', 'accelerator_requests'], + ), + json={'arqs': [ARQ_DICT]}, + ), + ] + ) arq_list = self.cloud.list_accelerator_requests() self.assertEqual(len(arq_list), 1) self.assertEqual(arq_list[0].uuid, ARQ_DICT['uuid']) - self.assertEqual(arq_list[0].device_profile_name, - ARQ_DICT['device_profile_name']) - self.assertEqual(arq_list[0].device_profile_group_id, - ARQ_DICT['device_profile_group_id']) - self.assertEqual(arq_list[0].device_rp_uuid, - ARQ_DICT['device_rp_uuid']) - self.assertEqual(arq_list[0].instance_uuid, - ARQ_DICT['instance_uuid']) - self.assertEqual(arq_list[0].attach_handle_type, - ARQ_DICT['attach_handle_type']) - self.assertEqual(arq_list[0].attach_handle_info, - ARQ_DICT['attach_handle_info']) + self.assertEqual( + arq_list[0].device_profile_name, ARQ_DICT['device_profile_name'] + ) + self.assertEqual( + arq_list[0].device_profile_group_id, + ARQ_DICT['device_profile_group_id'], + ) + self.assertEqual( + arq_list[0].device_rp_uuid, ARQ_DICT['device_rp_uuid'] + ) + self.assertEqual(arq_list[0].instance_uuid, ARQ_DICT['instance_uuid']) + self.assertEqual( + arq_list[0].attach_handle_type, ARQ_DICT['attach_handle_type'] + ) + self.assertEqual( + arq_list[0].attach_handle_info, ARQ_DICT['attach_handle_info'] + ) self.assert_calls() def test_create_accelerator_request(self): - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'accelerator', - 'public', - append=['v2', 'accelerator_requests']), - json=NEW_ARQ_DICT - ), - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'accelerator', + 'public', + append=['v2', 'accelerator_requests'], + ), + json=NEW_ARQ_DICT, + ), + ] + ) attrs = { 'device_profile_name': NEW_ARQ_DICT['device_profile_name'], - 'device_profile_group_id': NEW_ARQ_DICT['device_profile_group_id'] + 'device_profile_group_id': NEW_ARQ_DICT['device_profile_group_id'], } - self.assertTrue( - self.cloud.create_accelerator_request( - attrs - ) - ) + self.assertTrue(self.cloud.create_accelerator_request(attrs)) self.assert_calls() def test_delete_accelerator_request(self, filters=None): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'accelerator', - 'public', - append=['v2', 'accelerator_requests', ARQ_DICT['uuid']]), - json={"accelerator_requests": [ARQ_DICT]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'accelerator', - 'public', - append=['v2', 'accelerator_requests', ARQ_DICT['uuid']]), - json=ARQ_DICT) - - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'accelerator', + 'public', + append=[ + 'v2', + 'accelerator_requests', + ARQ_DICT['uuid'], + ], + ), + json={"accelerator_requests": [ARQ_DICT]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'accelerator', + 'public', + append=[ + 'v2', + 'accelerator_requests', + ARQ_DICT['uuid'], + ], + ), + json=ARQ_DICT, + ), + ] + ) self.assertTrue( - self.cloud.delete_accelerator_request( - ARQ_DICT['uuid'], - filters - ) + self.cloud.delete_accelerator_request(ARQ_DICT['uuid'], filters) ) self.assert_calls() def test_bind_accelerator_request(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'accelerator', - 'public', - append=['v2', 'accelerator_requests', ARQ_DICT['uuid']]), - json={"accelerator_requests": [ARQ_DICT]}), - dict(method='PATCH', - uri=self.get_mock_url( - 'accelerator', - 'public', - append=['v2', 'accelerator_requests', ARQ_DICT['uuid']]), - json=ARQ_DICT) - ]) - properties = [{'path': '/hostname', - 'value': ARQ_DICT['hostname'], - 'op': 'add'}, - {'path': '/instance_uuid', - 'value': ARQ_DICT['instance_uuid'], - 'op': 'add'}, - {'path': '/device_rp_uuid', - 'value': ARQ_DICT['device_rp_uuid'], - 'op': 'add'}] + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'accelerator', + 'public', + append=[ + 'v2', + 'accelerator_requests', + ARQ_DICT['uuid'], + ], + ), + json={"accelerator_requests": [ARQ_DICT]}, + ), + dict( + method='PATCH', + uri=self.get_mock_url( + 'accelerator', + 'public', + append=[ + 'v2', + 'accelerator_requests', + ARQ_DICT['uuid'], + ], + ), + json=ARQ_DICT, + ), + ] + ) + properties = [ + {'path': '/hostname', 'value': ARQ_DICT['hostname'], 'op': 'add'}, + { + 'path': '/instance_uuid', + 'value': ARQ_DICT['instance_uuid'], + 'op': 'add', + }, + { + 'path': '/device_rp_uuid', + 'value': ARQ_DICT['device_rp_uuid'], + 'op': 'add', + }, + ] self.assertTrue( - self.cloud.bind_accelerator_request( - ARQ_DICT['uuid'], properties - ) + self.cloud.bind_accelerator_request(ARQ_DICT['uuid'], properties) ) self.assert_calls() def test_unbind_accelerator_request(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'accelerator', - 'public', - append=['v2', 'accelerator_requests', ARQ_DICT['uuid']]), - json={"accelerator_requests": [ARQ_DICT]}), - dict(method='PATCH', - uri=self.get_mock_url( - 'accelerator', - 'public', - append=['v2', 'accelerator_requests', ARQ_DICT['uuid']]), - json=ARQ_DICT) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'accelerator', + 'public', + append=[ + 'v2', + 'accelerator_requests', + ARQ_DICT['uuid'], + ], + ), + json={"accelerator_requests": [ARQ_DICT]}, + ), + dict( + method='PATCH', + uri=self.get_mock_url( + 'accelerator', + 'public', + append=[ + 'v2', + 'accelerator_requests', + ARQ_DICT['uuid'], + ], + ), + json=ARQ_DICT, + ), + ] + ) - properties = [{'path': '/hostname', - 'op': 'remove'}, - {'path': '/instance_uuid', - 'op': 'remove'}, - {'path': '/device_rp_uuid', - 'op': 'remove'}] + properties = [ + {'path': '/hostname', 'op': 'remove'}, + {'path': '/instance_uuid', 'op': 'remove'}, + {'path': '/device_rp_uuid', 'op': 'remove'}, + ] self.assertTrue( - self.cloud.unbind_accelerator_request( - ARQ_DICT['uuid'], properties - ) + self.cloud.unbind_accelerator_request(ARQ_DICT['uuid'], properties) ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_aggregate.py b/openstack/tests/unit/cloud/test_aggregate.py index deec61c12..d5c265513 100644 --- a/openstack/tests/unit/cloud/test_aggregate.py +++ b/openstack/tests/unit/cloud/test_aggregate.py @@ -15,7 +15,6 @@ class TestAggregate(base.TestCase): - def setUp(self): super(TestAggregate, self).setUp() self.aggregate_name = self.getUniqueString('aggregate') @@ -27,17 +26,25 @@ def test_create_aggregate(self): del create_aggregate['metadata'] del create_aggregate['hosts'] - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', append=['os-aggregates']), - json={'aggregate': create_aggregate}, - validate=dict(json={ - 'aggregate': { - 'name': self.aggregate_name, - 'availability_zone': None, - }})), - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates'] + ), + json={'aggregate': create_aggregate}, + validate=dict( + json={ + 'aggregate': { + 'name': self.aggregate_name, + 'availability_zone': None, + } + } + ), + ), + ] + ) self.cloud.create_aggregate(name=self.aggregate_name) self.assert_calls() @@ -45,100 +52,144 @@ def test_create_aggregate(self): def test_create_aggregate_with_az(self): availability_zone = 'az1' az_aggregate = fakes.make_fake_aggregate( - 1, self.aggregate_name, availability_zone=availability_zone) + 1, self.aggregate_name, availability_zone=availability_zone + ) create_aggregate = az_aggregate.copy() del create_aggregate['metadata'] del create_aggregate['hosts'] - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', append=['os-aggregates']), - json={'aggregate': create_aggregate}, - validate=dict(json={ - 'aggregate': { - 'name': self.aggregate_name, - 'availability_zone': availability_zone, - }})), - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates'] + ), + json={'aggregate': create_aggregate}, + validate=dict( + json={ + 'aggregate': { + 'name': self.aggregate_name, + 'availability_zone': availability_zone, + } + } + ), + ), + ] + ) self.cloud.create_aggregate( - name=self.aggregate_name, availability_zone=availability_zone) + name=self.aggregate_name, availability_zone=availability_zone + ) self.assert_calls() def test_delete_aggregate(self): - self.register_uris([ - dict(method='DELETE', - uri=self.get_mock_url( - 'compute', 'public', append=['os-aggregates', '1'])), - ]) + self.register_uris( + [ + dict( + method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates', '1'] + ), + ), + ] + ) self.assertTrue(self.cloud.delete_aggregate('1')) self.assert_calls() def test_delete_aggregate_by_name(self): - self.register_uris([ - dict( - method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['os-aggregates', - self.aggregate_name] - ), - status_code=404, - ), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['os-aggregates']), - json={'aggregates': [self.fake_aggregate]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'compute', 'public', append=['os-aggregates', '1'])), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['os-aggregates', self.aggregate_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates'] + ), + json={'aggregates': [self.fake_aggregate]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates', '1'] + ), + ), + ] + ) self.assertTrue(self.cloud.delete_aggregate(self.aggregate_name)) self.assert_calls() def test_update_aggregate_set_az(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['os-aggregates', '1']), - json=self.fake_aggregate), - dict(method='PUT', - uri=self.get_mock_url( - 'compute', 'public', append=['os-aggregates', '1']), - json={'aggregate': self.fake_aggregate}, - validate=dict( - json={ - 'aggregate': { - 'availability_zone': 'az', - }})), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates', '1'] + ), + json=self.fake_aggregate, + ), + dict( + method='PUT', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates', '1'] + ), + json={'aggregate': self.fake_aggregate}, + validate=dict( + json={ + 'aggregate': { + 'availability_zone': 'az', + } + } + ), + ), + ] + ) self.cloud.update_aggregate(1, availability_zone='az') self.assert_calls() def test_update_aggregate_unset_az(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['os-aggregates', '1']), - json=self.fake_aggregate), - dict(method='PUT', - uri=self.get_mock_url( - 'compute', 'public', append=['os-aggregates', '1']), - json={'aggregate': self.fake_aggregate}, - validate=dict( - json={ - 'aggregate': { - 'availability_zone': None, - }})), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates', '1'] + ), + json=self.fake_aggregate, + ), + dict( + method='PUT', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates', '1'] + ), + json={'aggregate': self.fake_aggregate}, + validate=dict( + json={ + 'aggregate': { + 'availability_zone': None, + } + } + ), + ), + ] + ) self.cloud.update_aggregate(1, availability_zone=None) @@ -146,57 +197,83 @@ def test_update_aggregate_unset_az(self): def test_set_aggregate_metadata(self): metadata = {'key': 'value'} - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['os-aggregates', '1']), - json=self.fake_aggregate), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', - append=['os-aggregates', '1', 'action']), - json={'aggregate': self.fake_aggregate}, - validate=dict( - json={'set_metadata': {'metadata': metadata}})), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates', '1'] + ), + json=self.fake_aggregate, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', + 'public', + append=['os-aggregates', '1', 'action'], + ), + json={'aggregate': self.fake_aggregate}, + validate=dict( + json={'set_metadata': {'metadata': metadata}} + ), + ), + ] + ) self.cloud.set_aggregate_metadata('1', metadata) self.assert_calls() def test_add_host_to_aggregate(self): hostname = 'host1' - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['os-aggregates', '1']), - json=self.fake_aggregate), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', - append=['os-aggregates', '1', 'action']), - json={'aggregate': self.fake_aggregate}, - validate=dict( - json={'add_host': {'host': hostname}})), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates', '1'] + ), + json=self.fake_aggregate, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', + 'public', + append=['os-aggregates', '1', 'action'], + ), + json={'aggregate': self.fake_aggregate}, + validate=dict(json={'add_host': {'host': hostname}}), + ), + ] + ) self.cloud.add_host_to_aggregate('1', hostname) self.assert_calls() def test_remove_host_from_aggregate(self): hostname = 'host1' - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['os-aggregates', '1']), - json=self.fake_aggregate), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', - append=['os-aggregates', '1', 'action']), - json={'aggregate': self.fake_aggregate}, - validate=dict( - json={'remove_host': {'host': hostname}})), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-aggregates', '1'] + ), + json=self.fake_aggregate, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', + 'public', + append=['os-aggregates', '1', 'action'], + ), + json={'aggregate': self.fake_aggregate}, + validate=dict(json={'remove_host': {'host': hostname}}), + ), + ] + ) self.cloud.remove_host_from_aggregate('1', hostname) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_availability_zones.py b/openstack/tests/unit/cloud/test_availability_zones.py index 05a25bd60..68d67a64b 100644 --- a/openstack/tests/unit/cloud/test_availability_zones.py +++ b/openstack/tests/unit/cloud/test_availability_zones.py @@ -17,62 +17,63 @@ _fake_zone_list = { "availabilityZoneInfo": [ - { - "hosts": None, - "zoneName": "az1", - "zoneState": { - "available": True - } - }, - { - "hosts": None, - "zoneName": "nova", - "zoneState": { - "available": False - } - } + {"hosts": None, "zoneName": "az1", "zoneState": {"available": True}}, + {"hosts": None, "zoneName": "nova", "zoneState": {"available": False}}, ] } class TestAvailabilityZoneNames(base.TestCase): - def test_list_availability_zone_names(self): - self.register_uris([ - dict(method='GET', - uri='{endpoint}/os-availability-zone'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json=_fake_zone_list), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/os-availability-zone'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json=_fake_zone_list, + ), + ] + ) - self.assertEqual( - ['az1'], self.cloud.list_availability_zone_names()) + self.assertEqual(['az1'], self.cloud.list_availability_zone_names()) self.assert_calls() def test_unauthorized_availability_zone_names(self): - self.register_uris([ - dict(method='GET', - uri='{endpoint}/os-availability-zone'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - status_code=403), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/os-availability-zone'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + status_code=403, + ), + ] + ) - self.assertEqual( - [], self.cloud.list_availability_zone_names()) + self.assertEqual([], self.cloud.list_availability_zone_names()) self.assert_calls() def test_list_all_availability_zone_names(self): - self.register_uris([ - dict(method='GET', - uri='{endpoint}/os-availability-zone'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json=_fake_zone_list), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/os-availability-zone'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json=_fake_zone_list, + ), + ] + ) self.assertEqual( ['az1', 'nova'], - self.cloud.list_availability_zone_names(unavailable=True)) + self.cloud.list_availability_zone_names(unavailable=True), + ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_baremetal_node.py b/openstack/tests/unit/cloud/test_baremetal_node.py index 888258bef..71d9a9027 100644 --- a/openstack/tests/unit/cloud/test_baremetal_node.py +++ b/openstack/tests/unit/cloud/test_baremetal_node.py @@ -29,26 +29,31 @@ class TestBaremetalNode(base.IronicTestCase): - def setUp(self): super(TestBaremetalNode, self).setUp() self.fake_baremetal_node = fakes.make_fake_machine( - self.name, self.uuid) + self.name, self.uuid + ) # TODO(TheJulia): Some tests below have fake ports, # since they are required in some processes. Lets refactor # them at some point to use self.fake_baremetal_port. self.fake_baremetal_port = fakes.make_fake_port( - '00:01:02:03:04:05', - node_id=self.uuid) + '00:01:02:03:04:05', node_id=self.uuid + ) def test_list_machines(self): fake_baremetal_two = fakes.make_fake_machine('two', str(uuid.uuid4())) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(resource='nodes'), - json={'nodes': [self.fake_baremetal_node, - fake_baremetal_two]}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(resource='nodes'), + json={ + 'nodes': [self.fake_baremetal_node, fake_baremetal_two] + }, + ), + ] + ) machines = self.cloud.list_machines() self.assertEqual(2, len(machines)) @@ -57,40 +62,54 @@ def test_list_machines(self): self.assert_calls() def test_get_machine(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + ] + ) machine = self.cloud.get_machine(self.fake_baremetal_node['uuid']) - self.assertEqual(machine['uuid'], - self.fake_baremetal_node['uuid']) + self.assertEqual(machine['uuid'], self.fake_baremetal_node['uuid']) self.assert_calls() def test_get_machine_by_mac(self): mac_address = '00:01:02:03:04:05' node_uuid = self.fake_baremetal_node['uuid'] - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - resource='ports', - append=['detail'], - qs_elements=['address=%s' % mac_address]), - json={'ports': [{'address': mac_address, - 'node_uuid': node_uuid}]}), - dict(method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='ports', + append=['detail'], + qs_elements=['address=%s' % mac_address], + ), + json={ + 'ports': [ + {'address': mac_address, 'node_uuid': node_uuid} + ] + }, + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + ] + ) machine = self.cloud.get_machine_by_mac(mac_address) - self.assertEqual(machine['uuid'], - self.fake_baremetal_node['uuid']) + self.assertEqual(machine['uuid'], self.fake_baremetal_node['uuid']) self.assert_calls() def test_validate_machine(self): @@ -112,15 +131,20 @@ def test_validate_machine(self): }, 'foo': { 'result': False, - }} - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'validate']), - json=validate_return), - ]) + }, + } + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], 'validate'], + ), + json=validate_return, + ), + ] + ) self.cloud.validate_machine(self.fake_baremetal_node['uuid']) self.assert_calls() @@ -136,17 +160,23 @@ def test_validate_machine_not_for_deploy(self): }, 'foo': { 'result': False, - }} - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'validate']), - json=validate_return), - ]) - self.cloud.validate_machine(self.fake_baremetal_node['uuid'], - for_deploy=False) + }, + } + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], 'validate'], + ), + json=validate_return, + ), + ] + ) + self.cloud.validate_machine( + self.fake_baremetal_node['uuid'], for_deploy=False + ) self.assert_calls() @@ -160,15 +190,20 @@ def test_deprecated_validate_node(self): }, 'foo': { 'result': False, - }} - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'validate']), - json=validate_return), - ]) + }, + } + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], 'validate'], + ), + json=validate_return, + ), + ] + ) self.cloud.validate_node(self.fake_baremetal_node['uuid']) self.assert_calls() @@ -183,114 +218,139 @@ def test_validate_machine_raises_exception(self): 'result': True, 'reason': None, }, - 'foo': { - 'result': True - }} - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'validate']), - json=validate_return), - ]) + 'foo': {'result': True}, + } + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], 'validate'], + ), + json=validate_return, + ), + ] + ) self.assertRaises( exceptions.ValidationException, self.cloud.validate_machine, - self.fake_baremetal_node['uuid']) + self.fake_baremetal_node['uuid'], + ) self.assert_calls() def test_patch_machine(self): - test_patch = [{ - 'op': 'remove', - 'path': '/instance_info'}] + test_patch = [{'op': 'remove', 'path': '/instance_info'}] self.fake_baremetal_node['instance_info'] = {} - self.register_uris([ - dict(method='PATCH', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node, - validate=dict(json=test_patch)), - ]) + self.register_uris( + [ + dict( + method='PATCH', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + validate=dict(json=test_patch), + ), + ] + ) result = self.cloud.patch_machine( - self.fake_baremetal_node['uuid'], test_patch) + self.fake_baremetal_node['uuid'], test_patch + ) self.assertEqual(self.fake_baremetal_node['uuid'], result['uuid']) self.assert_calls() def test_set_node_instance_info(self): - test_patch = [{ - 'op': 'add', - 'path': '/foo', - 'value': 'bar'}] - self.register_uris([ - dict(method='PATCH', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node, - validate=dict(json=test_patch)), - ]) + test_patch = [{'op': 'add', 'path': '/foo', 'value': 'bar'}] + self.register_uris( + [ + dict( + method='PATCH', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + validate=dict(json=test_patch), + ), + ] + ) self.cloud.set_node_instance_info( - self.fake_baremetal_node['uuid'], test_patch) + self.fake_baremetal_node['uuid'], test_patch + ) self.assert_calls() def test_purge_node_instance_info(self): - test_patch = [{ - 'op': 'remove', - 'path': '/instance_info'}] + test_patch = [{'op': 'remove', 'path': '/instance_info'}] self.fake_baremetal_node['instance_info'] = {} - self.register_uris([ - dict(method='PATCH', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node, - validate=dict(json=test_patch)), - ]) - self.cloud.purge_node_instance_info( - self.fake_baremetal_node['uuid']) + self.register_uris( + [ + dict( + method='PATCH', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + validate=dict(json=test_patch), + ), + ] + ) + self.cloud.purge_node_instance_info(self.fake_baremetal_node['uuid']) self.assert_calls() def test_inspect_machine_fail_active(self): self.fake_baremetal_node['provision_state'] = 'active' - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + ] + ) self.assertRaises( exc.OpenStackCloudException, self.cloud.inspect_machine, self.fake_baremetal_node['uuid'], wait=True, - timeout=1) + timeout=1, + ) self.assert_calls() def test_inspect_machine_fail_associated(self): self.fake_baremetal_node['provision_state'] = 'available' self.fake_baremetal_node['instance_uuid'] = '1234' - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + ] + ) self.assertRaisesRegex( exc.OpenStackCloudException, 'associated with an instance', self.cloud.inspect_machine, self.fake_baremetal_node['uuid'], wait=True, - timeout=1) + timeout=1, + ) self.assert_calls() @@ -301,33 +361,46 @@ def test_inspect_machine_failed(self): inspecting_node['provision_state'] = 'inspecting' finished_node = self.fake_baremetal_node.copy() finished_node['provision_state'] = 'manageable' - self.register_uris([ - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'provision']), - validate=dict(json={'target': 'inspect'})), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=inspecting_node), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=finished_node), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'provision', + ], + ), + validate=dict(json={'target': 'inspect'}), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=inspecting_node, + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=finished_node, + ), + ] + ) self.cloud.inspect_machine(self.fake_baremetal_node['uuid']) @@ -337,33 +410,46 @@ def test_inspect_machine_manageable(self): self.fake_baremetal_node['provision_state'] = 'manageable' inspecting_node = self.fake_baremetal_node.copy() inspecting_node['provision_state'] = 'inspecting' - self.register_uris([ - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'provision']), - validate=dict(json={'target': 'inspect'})), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=inspecting_node), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'provision', + ], + ), + validate=dict(json={'target': 'inspect'}), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=inspecting_node, + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + ] + ) self.cloud.inspect_machine(self.fake_baremetal_node['uuid']) self.assert_calls() @@ -374,53 +460,78 @@ def test_inspect_machine_available(self): manageable_node = self.fake_baremetal_node.copy() manageable_node['provision_state'] = 'manageable' - self.register_uris([ - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=available_node), - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'provision']), - validate=dict(json={'target': 'manage'})), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=manageable_node), - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'provision']), - validate=dict(json={'target': 'inspect'})), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=manageable_node), - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'provision']), - validate=dict(json={'target': 'provide'})), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=available_node), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=available_node, + ), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'provision', + ], + ), + validate=dict(json={'target': 'manage'}), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=manageable_node, + ), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'provision', + ], + ), + validate=dict(json={'target': 'inspect'}), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=manageable_node, + ), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'provision', + ], + ), + validate=dict(json={'target': 'provide'}), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=available_node, + ), + ] + ) self.cloud.inspect_machine(self.fake_baremetal_node['uuid']) self.assert_calls() @@ -433,67 +544,97 @@ def test_inspect_machine_available_wait(self): inspecting_node = self.fake_baremetal_node.copy() inspecting_node['provision_state'] = 'inspecting' - self.register_uris([ - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=available_node), - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'provision']), - validate=dict(json={'target': 'manage'})), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=available_node), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=manageable_node), - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'provision']), - validate=dict(json={'target': 'inspect'})), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=inspecting_node), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=manageable_node), - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'provision']), - validate=dict(json={'target': 'provide'})), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=available_node), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=available_node, + ), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'provision', + ], + ), + validate=dict(json={'target': 'manage'}), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=available_node, + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=manageable_node, + ), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'provision', + ], + ), + validate=dict(json={'target': 'inspect'}), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=inspecting_node, + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=manageable_node, + ), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'provision', + ], + ), + validate=dict(json={'target': 'provide'}), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=available_node, + ), + ] + ) self.cloud.inspect_machine( - self.fake_baremetal_node['uuid'], wait=True, timeout=1) + self.fake_baremetal_node['uuid'], wait=True, timeout=1 + ) self.assert_calls() @@ -501,41 +642,57 @@ def test_inspect_machine_wait(self): self.fake_baremetal_node['provision_state'] = 'manageable' inspecting_node = self.fake_baremetal_node.copy() inspecting_node['provision_state'] = 'inspecting' - self.register_uris([ - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'provision']), - validate=dict(json={'target': 'inspect'})), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=inspecting_node), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=inspecting_node), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'provision', + ], + ), + validate=dict(json={'target': 'inspect'}), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=inspecting_node, + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=inspecting_node, + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + ] + ) self.cloud.inspect_machine( - self.fake_baremetal_node['uuid'], wait=True, timeout=1) + self.fake_baremetal_node['uuid'], wait=True, timeout=1 + ) self.assert_calls() @@ -546,113 +703,164 @@ def test_inspect_machine_inspect_failed(self): inspect_fail_node = self.fake_baremetal_node.copy() inspect_fail_node['provision_state'] = 'inspect failed' inspect_fail_node['last_error'] = 'Earth Imploded' - self.register_uris([ - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'provision']), - validate=dict(json={'target': 'inspect'})), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=inspecting_node), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=inspect_fail_node), - ]) - self.assertRaises(exc.OpenStackCloudException, - self.cloud.inspect_machine, - self.fake_baremetal_node['uuid'], - wait=True, timeout=1) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'provision', + ], + ), + validate=dict(json={'target': 'inspect'}), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=inspecting_node, + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=inspect_fail_node, + ), + ] + ) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.inspect_machine, + self.fake_baremetal_node['uuid'], + wait=True, + timeout=1, + ) self.assert_calls() def test_set_machine_maintenace_state(self): - self.register_uris([ - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'maintenance']), - validate=dict(json={'reason': 'no reason'})), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - ]) + self.register_uris( + [ + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'maintenance', + ], + ), + validate=dict(json={'reason': 'no reason'}), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + ] + ) self.cloud.set_machine_maintenance_state( - self.fake_baremetal_node['uuid'], True, reason='no reason') + self.fake_baremetal_node['uuid'], True, reason='no reason' + ) self.assert_calls() def test_set_machine_maintenace_state_false(self): - self.register_uris([ - dict( - method='DELETE', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'maintenance'])), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - ]) + self.register_uris( + [ + dict( + method='DELETE', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'maintenance', + ], + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + ] + ) self.cloud.set_machine_maintenance_state( - self.fake_baremetal_node['uuid'], False) + self.fake_baremetal_node['uuid'], False + ) self.assert_calls def test_remove_machine_from_maintenance(self): - self.register_uris([ - dict( - method='DELETE', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'maintenance'])), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - ]) + self.register_uris( + [ + dict( + method='DELETE', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'maintenance', + ], + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + ] + ) self.cloud.remove_machine_from_maintenance( - self.fake_baremetal_node['uuid']) + self.fake_baremetal_node['uuid'] + ) self.assert_calls() def test_set_machine_power_on(self): - self.register_uris([ - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'power']), - validate=dict(json={'target': 'power on'})), - ]) + self.register_uris( + [ + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'power', + ], + ), + validate=dict(json={'target': 'power on'}), + ), + ] + ) return_value = self.cloud.set_machine_power_on( - self.fake_baremetal_node['uuid']) + self.fake_baremetal_node['uuid'] + ) self.assertIsNone(return_value) self.assert_calls() @@ -660,84 +868,127 @@ def test_set_machine_power_on(self): def test_set_machine_power_on_with_retires(self): # NOTE(TheJulia): This logic ends up testing power on/off and reboot # as they all utilize the same helper method. - self.register_uris([ - dict( - method='PUT', - status_code=503, - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'power']), - validate=dict(json={'target': 'power on'})), - dict( - method='PUT', - status_code=409, - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'power']), - validate=dict(json={'target': 'power on'})), - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'power']), - validate=dict(json={'target': 'power on'})), - ]) + self.register_uris( + [ + dict( + method='PUT', + status_code=503, + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'power', + ], + ), + validate=dict(json={'target': 'power on'}), + ), + dict( + method='PUT', + status_code=409, + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'power', + ], + ), + validate=dict(json={'target': 'power on'}), + ), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'power', + ], + ), + validate=dict(json={'target': 'power on'}), + ), + ] + ) return_value = self.cloud.set_machine_power_on( - self.fake_baremetal_node['uuid']) + self.fake_baremetal_node['uuid'] + ) self.assertIsNone(return_value) self.assert_calls() def test_set_machine_power_off(self): - self.register_uris([ - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'power']), - validate=dict(json={'target': 'power off'})), - ]) + self.register_uris( + [ + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'power', + ], + ), + validate=dict(json={'target': 'power off'}), + ), + ] + ) return_value = self.cloud.set_machine_power_off( - self.fake_baremetal_node['uuid']) + self.fake_baremetal_node['uuid'] + ) self.assertIsNone(return_value) self.assert_calls() def test_set_machine_power_reboot(self): - self.register_uris([ - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'power']), - validate=dict(json={'target': 'rebooting'})), - ]) + self.register_uris( + [ + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'power', + ], + ), + validate=dict(json={'target': 'rebooting'}), + ), + ] + ) return_value = self.cloud.set_machine_power_reboot( - self.fake_baremetal_node['uuid']) + self.fake_baremetal_node['uuid'] + ) self.assertIsNone(return_value) self.assert_calls() def test_set_machine_power_reboot_failure(self): - self.register_uris([ - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'power']), - status_code=400, - json={'error': 'invalid'}, - validate=dict(json={'target': 'rebooting'})), - ]) - self.assertRaises(exc.OpenStackCloudException, - self.cloud.set_machine_power_reboot, - self.fake_baremetal_node['uuid']) + self.register_uris( + [ + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'power', + ], + ), + status_code=400, + json={'error': 'invalid'}, + validate=dict(json={'target': 'rebooting'}), + ), + ] + ) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.set_machine_power_reboot, + self.fake_baremetal_node['uuid'], + ) self.assert_calls() @@ -746,25 +997,40 @@ def test_node_set_provision_state(self): deploy_node['provision_state'] = 'deploying' active_node = self.fake_baremetal_node.copy() active_node['provision_state'] = 'active' - self.register_uris([ - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'provision']), - validate=dict(json={'target': 'active', - 'configdrive': 'http://host/file'})), - dict(method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - ]) + self.register_uris( + [ + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'provision', + ], + ), + validate=dict( + json={ + 'target': 'active', + 'configdrive': 'http://host/file', + } + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + ] + ) result = self.cloud.node_set_provision_state( self.fake_baremetal_node['uuid'], 'active', - configdrive='http://host/file') + configdrive='http://host/file', + ) self.assertEqual(self.fake_baremetal_node['uuid'], result['uuid']) self.assert_calls() @@ -774,43 +1040,76 @@ def test_node_set_provision_state_with_retries(self): deploy_node['provision_state'] = 'deploying' active_node = self.fake_baremetal_node.copy() active_node['provision_state'] = 'active' - self.register_uris([ - dict( - method='PUT', - status_code=409, - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'provision']), - validate=dict(json={'target': 'active', - 'configdrive': 'http://host/file'})), - dict( - method='PUT', - status_code=503, - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'provision']), - validate=dict(json={'target': 'active', - 'configdrive': 'http://host/file'})), - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'provision']), - validate=dict(json={'target': 'active', - 'configdrive': 'http://host/file'})), - dict(method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - ]) + self.register_uris( + [ + dict( + method='PUT', + status_code=409, + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'provision', + ], + ), + validate=dict( + json={ + 'target': 'active', + 'configdrive': 'http://host/file', + } + ), + ), + dict( + method='PUT', + status_code=503, + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'provision', + ], + ), + validate=dict( + json={ + 'target': 'active', + 'configdrive': 'http://host/file', + } + ), + ), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'provision', + ], + ), + validate=dict( + json={ + 'target': 'active', + 'configdrive': 'http://host/file', + } + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + ] + ) self.cloud.node_set_provision_state( self.fake_baremetal_node['uuid'], 'active', - configdrive='http://host/file') + configdrive='http://host/file', + ) self.assert_calls() @@ -820,34 +1119,49 @@ def test_node_set_provision_state_wait_timeout(self): active_node = self.fake_baremetal_node.copy() active_node['provision_state'] = 'active' self.fake_baremetal_node['provision_state'] = 'available' - self.register_uris([ - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'provision']), - validate=dict(json={'target': 'active'})), - dict(method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - dict(method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=deploy_node), - dict(method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=active_node), - ]) + self.register_uris( + [ + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'provision', + ], + ), + validate=dict(json={'target': 'active'}), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=deploy_node, + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=active_node, + ), + ] + ) return_value = self.cloud.node_set_provision_state( - self.fake_baremetal_node['uuid'], - 'active', - wait=True) + self.fake_baremetal_node['uuid'], 'active', wait=True + ) self.assertSubdict(active_node, return_value) self.assert_calls() @@ -855,20 +1169,30 @@ def test_node_set_provision_state_wait_timeout(self): def test_node_set_provision_state_wait_timeout_fails(self): # Intentionally time out. self.fake_baremetal_node['provision_state'] = 'deploy wait' - self.register_uris([ - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'provision']), - validate=dict(json={'target': 'active'})), - dict(method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - ]) + self.register_uris( + [ + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'provision', + ], + ), + validate=dict(json={'target': 'active'}), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + ] + ) self.assertRaises( exc.OpenStackCloudException, @@ -876,51 +1200,71 @@ def test_node_set_provision_state_wait_timeout_fails(self): self.fake_baremetal_node['uuid'], 'active', wait=True, - timeout=0.001) + timeout=0.001, + ) self.assert_calls() def test_node_set_provision_state_wait_success(self): self.fake_baremetal_node['provision_state'] = 'active' - self.register_uris([ - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'provision']), - validate=dict(json={'target': 'active'})), - dict(method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - ]) + self.register_uris( + [ + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'provision', + ], + ), + validate=dict(json={'target': 'active'}), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + ] + ) return_value = self.cloud.node_set_provision_state( - self.fake_baremetal_node['uuid'], - 'active', - wait=True) + self.fake_baremetal_node['uuid'], 'active', wait=True + ) self.assertSubdict(self.fake_baremetal_node, return_value) self.assert_calls() def test_node_set_provision_state_wait_failure_cases(self): self.fake_baremetal_node['provision_state'] = 'foo failed' - self.register_uris([ - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'provision']), - validate=dict(json={'target': 'active'})), - dict(method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - ]) + self.register_uris( + [ + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'provision', + ], + ), + validate=dict(json={'target': 'active'}), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + ] + ) self.assertRaises( exc.OpenStackCloudException, @@ -928,7 +1272,8 @@ def test_node_set_provision_state_wait_failure_cases(self): self.fake_baremetal_node['uuid'], 'active', wait=True, - timeout=300) + timeout=300, + ) self.assert_calls() @@ -936,29 +1281,41 @@ def test_node_set_provision_state_wait_provide(self): self.fake_baremetal_node['provision_state'] = 'manageable' available_node = self.fake_baremetal_node.copy() available_node['provision_state'] = 'available' - self.register_uris([ - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'provision']), - validate=dict(json={'target': 'provide'})), - dict(method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - dict(method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=available_node), - ]) + self.register_uris( + [ + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'provision', + ], + ), + validate=dict(json={'target': 'provide'}), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=available_node, + ), + ] + ) return_value = self.cloud.node_set_provision_state( - self.fake_baremetal_node['uuid'], - 'provide', - wait=True) + self.fake_baremetal_node['uuid'], 'provide', wait=True + ) self.assertSubdict(available_node, return_value) self.assert_calls() @@ -967,22 +1324,31 @@ def test_wait_for_baremetal_node_lock_locked(self): self.fake_baremetal_node['reservation'] = 'conductor0' unlocked_node = self.fake_baremetal_node.copy() unlocked_node['reservation'] = None - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - dict(method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=unlocked_node), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=unlocked_node, + ), + ] + ) self.assertIsNone( self.cloud.wait_for_baremetal_node_lock( - self.fake_baremetal_node, - timeout=1)) + self.fake_baremetal_node, timeout=1 + ) + ) self.assert_calls() @@ -990,73 +1356,105 @@ def test_wait_for_baremetal_node_lock_not_locked(self): self.fake_baremetal_node['reservation'] = None self.assertIsNone( self.cloud.wait_for_baremetal_node_lock( - self.fake_baremetal_node, - timeout=1)) + self.fake_baremetal_node, timeout=1 + ) + ) # NOTE(dtantsur): service discovery apparently requires 3 calls self.assertEqual(3, len(self.adapter.request_history)) def test_wait_for_baremetal_node_lock_timeout(self): self.fake_baremetal_node['reservation'] = 'conductor0' - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + ] + ) self.assertRaises( exc.OpenStackCloudException, self.cloud.wait_for_baremetal_node_lock, self.fake_baremetal_node, - timeout=0.001) + timeout=0.001, + ) self.assert_calls() def test_activate_node(self): self.fake_baremetal_node['provision_state'] = 'active' - self.register_uris([ - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'provision']), - validate=dict(json={'target': 'active', - 'configdrive': 'http://host/file'})), - dict(method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - ]) + self.register_uris( + [ + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'provision', + ], + ), + validate=dict( + json={ + 'target': 'active', + 'configdrive': 'http://host/file', + } + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + ] + ) return_value = self.cloud.activate_node( self.fake_baremetal_node['uuid'], configdrive='http://host/file', - wait=True) + wait=True, + ) self.assertIsNone(return_value) self.assert_calls() def test_deactivate_node(self): self.fake_baremetal_node['provision_state'] = 'available' - self.register_uris([ - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'provision']), - validate=dict(json={'target': 'deleted'})), - dict(method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - ]) + self.register_uris( + [ + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'provision', + ], + ), + validate=dict(json={'target': 'deleted'}), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + ] + ) return_value = self.cloud.deactivate_node( - self.fake_baremetal_node['uuid'], - wait=True) + self.fake_baremetal_node['uuid'], wait=True + ) self.assertIsNone(return_value) self.assert_calls() @@ -1073,25 +1471,29 @@ def test_register_machine(self): 'driver_info': None, 'name': self.fake_baremetal_node['name'], 'properties': None, - 'uuid': node_uuid} + 'uuid': node_uuid, + } self.fake_baremetal_node['provision_state'] = 'available' if 'provision_state' in node_to_post: node_to_post.pop('provision_state') - self.register_uris([ - dict( - method='POST', - uri=self.get_mock_url( - resource='nodes'), - json=self.fake_baremetal_node, - validate=dict(json=node_to_post)), - dict( - method='POST', - uri=self.get_mock_url( - resource='ports'), - validate=dict(json={'address': mac_address, - 'node_uuid': node_uuid}), - json=self.fake_baremetal_port), - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url(resource='nodes'), + json=self.fake_baremetal_node, + validate=dict(json=node_to_post), + ), + dict( + method='POST', + uri=self.get_mock_url(resource='ports'), + validate=dict( + json={'address': mac_address, 'node_uuid': node_uuid} + ), + json=self.fake_baremetal_port, + ), + ] + ) return_value = self.cloud.register_machine(nics, **node_to_post) self.assertEqual(self.uuid, return_value.id) @@ -1112,54 +1514,75 @@ def test_register_machine_enroll(self): 'driver_info': None, 'name': self.fake_baremetal_node['name'], 'properties': None, - 'uuid': node_uuid} + 'uuid': node_uuid, + } self.fake_baremetal_node['provision_state'] = 'enroll' manageable_node = self.fake_baremetal_node.copy() manageable_node['provision_state'] = 'manageable' available_node = self.fake_baremetal_node.copy() available_node['provision_state'] = 'available' - self.register_uris([ - dict( - method='POST', - uri=self.get_mock_url( - resource='nodes'), - validate=dict(json=node_to_post), - json=self.fake_baremetal_node), - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'provision']), - validate=dict(json={'target': 'manage'})), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=manageable_node), - dict( - method='POST', - uri=self.get_mock_url( - resource='ports'), - validate=dict(json={'address': mac_address, - 'node_uuid': node_uuid, - 'pxe_enabled': False}), - json=self.fake_baremetal_port), - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'provision']), - validate=dict(json={'target': 'provide'})), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=available_node), - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url(resource='nodes'), + validate=dict(json=node_to_post), + json=self.fake_baremetal_node, + ), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'provision', + ], + ), + validate=dict(json={'target': 'manage'}), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=manageable_node, + ), + dict( + method='POST', + uri=self.get_mock_url(resource='ports'), + validate=dict( + json={ + 'address': mac_address, + 'node_uuid': node_uuid, + 'pxe_enabled': False, + } + ), + json=self.fake_baremetal_port, + ), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'provision', + ], + ), + validate=dict(json={'target': 'provide'}), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=available_node, + ), + ] + ) return_value = self.cloud.register_machine(nics, **node_to_post) self.assertSubdict(available_node, return_value) @@ -1175,67 +1598,90 @@ def test_register_machine_enroll_wait(self): 'driver_info': None, 'name': self.fake_baremetal_node['name'], 'properties': None, - 'uuid': node_uuid} + 'uuid': node_uuid, + } self.fake_baremetal_node['provision_state'] = 'enroll' manageable_node = self.fake_baremetal_node.copy() manageable_node['provision_state'] = 'manageable' available_node = self.fake_baremetal_node.copy() available_node['provision_state'] = 'available' - self.register_uris([ - dict( - method='POST', - uri=self.get_mock_url( - resource='nodes'), - validate=dict(json=node_to_post), - json=self.fake_baremetal_node), - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'provision']), - validate=dict(json={'target': 'manage'})), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=manageable_node), - dict( - method='POST', - uri=self.get_mock_url( - resource='ports'), - validate=dict(json={'address': mac_address, - 'node_uuid': node_uuid}), - json=self.fake_baremetal_port), - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'provision']), - validate=dict(json={'target': 'provide'})), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=manageable_node), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=available_node), - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url(resource='nodes'), + validate=dict(json=node_to_post), + json=self.fake_baremetal_node, + ), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'provision', + ], + ), + validate=dict(json={'target': 'manage'}), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=manageable_node, + ), + dict( + method='POST', + uri=self.get_mock_url(resource='ports'), + validate=dict( + json={'address': mac_address, 'node_uuid': node_uuid} + ), + json=self.fake_baremetal_port, + ), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'provision', + ], + ), + validate=dict(json={'target': 'provide'}), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=manageable_node, + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=available_node, + ), + ] + ) return_value = self.cloud.register_machine( - nics, wait=True, **node_to_post) + nics, wait=True, **node_to_post + ) self.assertSubdict(available_node, return_value) self.assert_calls() @@ -1250,44 +1696,57 @@ def test_register_machine_enroll_failure(self): 'driver_info': None, 'name': self.fake_baremetal_node['name'], 'properties': None, - 'uuid': node_uuid} + 'uuid': node_uuid, + } self.fake_baremetal_node['provision_state'] = 'enroll' failed_node = self.fake_baremetal_node.copy() failed_node['reservation'] = 'conductor0' failed_node['provision_state'] = 'enroll' failed_node['last_error'] = 'kaboom!' - self.register_uris([ - dict( - method='POST', - uri=self.get_mock_url( - resource='nodes'), - json=self.fake_baremetal_node, - validate=dict(json=node_to_post)), - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'provision']), - validate=dict(json={'target': 'manage'})), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=failed_node), - dict( - method='DELETE', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']])), - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url(resource='nodes'), + json=self.fake_baremetal_node, + validate=dict(json=node_to_post), + ), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'provision', + ], + ), + validate=dict(json={'target': 'manage'}), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=failed_node, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + ), + ] + ) self.assertRaises( exc.OpenStackCloudException, self.cloud.register_machine, nics, - **node_to_post) + **node_to_post + ) self.assert_calls() def test_register_machine_enroll_timeout(self): @@ -1300,37 +1759,49 @@ def test_register_machine_enroll_timeout(self): 'driver_info': None, 'name': self.fake_baremetal_node['name'], 'properties': None, - 'uuid': node_uuid} + 'uuid': node_uuid, + } self.fake_baremetal_node['provision_state'] = 'enroll' busy_node = self.fake_baremetal_node.copy() busy_node['reservation'] = 'conductor0' busy_node['provision_state'] = 'verifying' - self.register_uris([ - dict( - method='POST', - uri=self.get_mock_url( - resource='nodes'), - json=self.fake_baremetal_node, - validate=dict(json=node_to_post)), - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'provision']), - validate=dict(json={'target': 'manage'})), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=busy_node), - dict( - method='DELETE', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']])), - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url(resource='nodes'), + json=self.fake_baremetal_node, + validate=dict(json=node_to_post), + ), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'provision', + ], + ), + validate=dict(json={'target': 'manage'}), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=busy_node, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + ), + ] + ) # NOTE(TheJulia): This test shortcircuits the timeout loop # such that it executes only once. The very last returned # state to the API is essentially a busy state that we @@ -1341,7 +1812,8 @@ def test_register_machine_enroll_timeout(self): nics, timeout=0.001, lock_timeout=0.001, - **node_to_post) + **node_to_post + ) self.assert_calls() def test_register_machine_enroll_timeout_wait(self): @@ -1354,63 +1826,84 @@ def test_register_machine_enroll_timeout_wait(self): 'driver_info': None, 'name': self.fake_baremetal_node['name'], 'properties': None, - 'uuid': node_uuid} + 'uuid': node_uuid, + } self.fake_baremetal_node['provision_state'] = 'enroll' manageable_node = self.fake_baremetal_node.copy() manageable_node['provision_state'] = 'manageable' - self.register_uris([ - dict( - method='POST', - uri=self.get_mock_url( - resource='nodes'), - json=self.fake_baremetal_node, - validate=dict(json=node_to_post)), - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'provision']), - validate=dict(json={'target': 'manage'})), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=manageable_node), - dict( - method='POST', - uri=self.get_mock_url( - resource='ports'), - validate=dict(json={'address': mac_address, - 'node_uuid': node_uuid}), - json=self.fake_baremetal_port), - dict( - method='PUT', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], - 'states', 'provision']), - validate=dict(json={'target': 'provide'})), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - dict( - method='DELETE', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']])), - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url(resource='nodes'), + json=self.fake_baremetal_node, + validate=dict(json=node_to_post), + ), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'provision', + ], + ), + validate=dict(json={'target': 'manage'}), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=manageable_node, + ), + dict( + method='POST', + uri=self.get_mock_url(resource='ports'), + validate=dict( + json={'address': mac_address, 'node_uuid': node_uuid} + ), + json=self.fake_baremetal_port, + ), + dict( + method='PUT', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'states', + 'provision', + ], + ), + validate=dict(json={'target': 'provide'}), + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + ), + ] + ) self.assertRaises( exc.OpenStackCloudException, self.cloud.register_machine, nics, wait=True, timeout=0.001, - **node_to_post) + **node_to_post + ) self.assert_calls() def test_register_machine_port_create_failed(self): @@ -1423,33 +1916,42 @@ def test_register_machine_port_create_failed(self): 'driver_info': None, 'name': self.fake_baremetal_node['name'], 'properties': None, - 'uuid': node_uuid} + 'uuid': node_uuid, + } self.fake_baremetal_node['provision_state'] = 'available' - self.register_uris([ - dict( - method='POST', - uri=self.get_mock_url( - resource='nodes'), - json=self.fake_baremetal_node, - validate=dict(json=node_to_post)), - dict( - method='POST', - uri=self.get_mock_url( - resource='ports'), - status_code=400, - json={'error': 'no ports for you'}, - validate=dict(json={'address': mac_address, - 'node_uuid': node_uuid})), - dict( - method='DELETE', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']])), - ]) - self.assertRaisesRegex(exc.OpenStackCloudException, - 'no ports for you', - self.cloud.register_machine, - nics, **node_to_post) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url(resource='nodes'), + json=self.fake_baremetal_node, + validate=dict(json=node_to_post), + ), + dict( + method='POST', + uri=self.get_mock_url(resource='ports'), + status_code=400, + json={'error': 'no ports for you'}, + validate=dict( + json={'address': mac_address, 'node_uuid': node_uuid} + ), + ), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + ), + ] + ) + self.assertRaisesRegex( + exc.OpenStackCloudException, + 'no ports for you', + self.cloud.register_machine, + nics, + **node_to_post + ) self.assert_calls() @@ -1465,45 +1967,57 @@ def test_register_machine_several_ports_create_failed(self): 'driver_info': None, 'name': self.fake_baremetal_node['name'], 'properties': None, - 'uuid': node_uuid} + 'uuid': node_uuid, + } self.fake_baremetal_node['provision_state'] = 'available' - self.register_uris([ - dict( - method='POST', - uri=self.get_mock_url( - resource='nodes'), - json=self.fake_baremetal_node, - validate=dict(json=node_to_post)), - dict( - method='POST', - uri=self.get_mock_url( - resource='ports'), - validate=dict(json={'address': mac_address, - 'node_uuid': node_uuid}), - json=self.fake_baremetal_port), - dict( - method='POST', - uri=self.get_mock_url( - resource='ports'), - status_code=400, - json={'error': 'no ports for you'}, - validate=dict(json={'address': mac_address2, - 'node_uuid': node_uuid})), - dict( - method='DELETE', - uri=self.get_mock_url( - resource='ports', - append=[self.fake_baremetal_port['uuid']])), - dict( - method='DELETE', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']])), - ]) - self.assertRaisesRegex(exc.OpenStackCloudException, - 'no ports for you', - self.cloud.register_machine, - nics, **node_to_post) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url(resource='nodes'), + json=self.fake_baremetal_node, + validate=dict(json=node_to_post), + ), + dict( + method='POST', + uri=self.get_mock_url(resource='ports'), + validate=dict( + json={'address': mac_address, 'node_uuid': node_uuid} + ), + json=self.fake_baremetal_port, + ), + dict( + method='POST', + uri=self.get_mock_url(resource='ports'), + status_code=400, + json={'error': 'no ports for you'}, + validate=dict( + json={'address': mac_address2, 'node_uuid': node_uuid} + ), + ), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='ports', + append=[self.fake_baremetal_port['uuid']], + ), + ), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + ), + ] + ) + self.assertRaisesRegex( + exc.OpenStackCloudException, + 'no ports for you', + self.cloud.register_machine, + nics, + **node_to_post + ) self.assert_calls() @@ -1514,35 +2028,50 @@ def test_unregister_machine(self): # NOTE(TheJulia): The two values below should be the same. port_node_uuid = self.fake_baremetal_port['node_uuid'] self.fake_baremetal_node['provision_state'] = 'available' - self.register_uris([ - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - dict( - method='GET', - uri=self.get_mock_url( - resource='ports', - qs_elements=['address=%s' % mac_address]), - json={'ports': [{'address': mac_address, - 'node_uuid': port_node_uuid, - 'uuid': port_uuid}]}), - dict( - method='DELETE', - uri=self.get_mock_url( - resource='ports', - append=[self.fake_baremetal_port['uuid']])), - dict( - method='DELETE', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']])), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='ports', + qs_elements=['address=%s' % mac_address], + ), + json={ + 'ports': [ + { + 'address': mac_address, + 'node_uuid': port_node_uuid, + 'uuid': port_uuid, + } + ] + }, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='ports', + append=[self.fake_baremetal_port['uuid']], + ), + ), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + ), + ] + ) - self.cloud.unregister_machine( - nics, self.fake_baremetal_node['uuid']) + self.cloud.unregister_machine(nics, self.fake_baremetal_node['uuid']) self.assert_calls() @@ -1551,26 +2080,33 @@ def test_unregister_machine_locked_timeout(self): nics = [{'mac': mac_address}] self.fake_baremetal_node['provision_state'] = 'available' self.fake_baremetal_node['reservation'] = 'conductor99' - self.register_uris([ - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + ] + ) self.assertRaises( exc.OpenStackCloudException, self.cloud.unregister_machine, nics, self.fake_baremetal_node['uuid'], - timeout=0.001) + timeout=0.001, + ) self.assert_calls() def test_unregister_machine_retries(self): @@ -1580,53 +2116,74 @@ def test_unregister_machine_retries(self): # NOTE(TheJulia): The two values below should be the same. port_node_uuid = self.fake_baremetal_port['node_uuid'] self.fake_baremetal_node['provision_state'] = 'available' - self.register_uris([ - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - dict( - method='GET', - uri=self.get_mock_url( - resource='ports', - qs_elements=['address=%s' % mac_address]), - json={'ports': [{'address': mac_address, - 'node_uuid': port_node_uuid, - 'uuid': port_uuid}]}), - dict( - method='DELETE', - status_code=503, - uri=self.get_mock_url( - resource='ports', - append=[self.fake_baremetal_port['uuid']])), - dict( - method='DELETE', - status_code=409, - uri=self.get_mock_url( - resource='ports', - append=[self.fake_baremetal_port['uuid']])), - dict( - method='DELETE', - uri=self.get_mock_url( - resource='ports', - append=[self.fake_baremetal_port['uuid']])), - dict( - method='DELETE', - status_code=409, - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']])), - dict( - method='DELETE', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']])), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='ports', + qs_elements=['address=%s' % mac_address], + ), + json={ + 'ports': [ + { + 'address': mac_address, + 'node_uuid': port_node_uuid, + 'uuid': port_uuid, + } + ] + }, + ), + dict( + method='DELETE', + status_code=503, + uri=self.get_mock_url( + resource='ports', + append=[self.fake_baremetal_port['uuid']], + ), + ), + dict( + method='DELETE', + status_code=409, + uri=self.get_mock_url( + resource='ports', + append=[self.fake_baremetal_port['uuid']], + ), + ), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='ports', + append=[self.fake_baremetal_port['uuid']], + ), + ), + dict( + method='DELETE', + status_code=409, + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + ), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + ), + ] + ) - self.cloud.unregister_machine( - nics, self.fake_baremetal_node['uuid']) + self.cloud.unregister_machine(nics, self.fake_baremetal_node['uuid']) self.assert_calls() @@ -1644,8 +2201,11 @@ def test_unregister_machine_unavailable(self): method='GET', uri=self.get_mock_url( resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node)) + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ) + ) self.register_uris(url_list) @@ -1654,21 +2214,28 @@ def test_unregister_machine_unavailable(self): exc.OpenStackCloudException, self.cloud.unregister_machine, nics, - self.fake_baremetal_node['uuid']) + self.fake_baremetal_node['uuid'], + ) self.assert_calls() def test_update_machine_patch_no_action(self): - self.register_uris([dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + ] + ) # NOTE(TheJulia): This is just testing mechanics. update_dict = self.cloud.update_machine( - self.fake_baremetal_node['uuid']) + self.fake_baremetal_node['uuid'] + ) self.assertIsNone(update_dict['changes']) self.assertSubdict(self.fake_baremetal_node, update_dict['node']) @@ -1676,90 +2243,121 @@ def test_update_machine_patch_no_action(self): def test_attach_port_to_machine(self): vif_id = '953ccbee-e854-450f-95fe-fe5e40d611ec' - self.register_uris([ - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - dict( - method='GET', - uri=self.get_mock_url( - service_type='network', - resource='ports', - base_url_append='v2.0', - append=[vif_id]), - json={'id': vif_id}), - dict( - method='POST', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], 'vifs'])), - ]) - self.cloud.attach_port_to_machine(self.fake_baremetal_node['uuid'], - vif_id) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + dict( + method='GET', + uri=self.get_mock_url( + service_type='network', + resource='ports', + base_url_append='v2.0', + append=[vif_id], + ), + json={'id': vif_id}, + ), + dict( + method='POST', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], 'vifs'], + ), + ), + ] + ) + self.cloud.attach_port_to_machine( + self.fake_baremetal_node['uuid'], vif_id + ) self.assert_calls() def test_detach_port_from_machine(self): vif_id = '953ccbee-e854-450f-95fe-fe5e40d611ec' - self.register_uris([ - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - dict( - method='GET', - uri=self.get_mock_url( - service_type='network', - resource='ports', - base_url_append='v2.0', - append=[vif_id]), - json={'id': vif_id}), - dict( - method='DELETE', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], 'vifs', - vif_id])), - ]) - self.cloud.detach_port_from_machine(self.fake_baremetal_node['uuid'], - vif_id) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + dict( + method='GET', + uri=self.get_mock_url( + service_type='network', + resource='ports', + base_url_append='v2.0', + append=[vif_id], + ), + json={'id': vif_id}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='nodes', + append=[ + self.fake_baremetal_node['uuid'], + 'vifs', + vif_id, + ], + ), + ), + ] + ) + self.cloud.detach_port_from_machine( + self.fake_baremetal_node['uuid'], vif_id + ) self.assert_calls() def test_list_ports_attached_to_machine(self): vif_id = '953ccbee-e854-450f-95fe-fe5e40d611ec' fake_port = {'id': vif_id, 'name': 'test'} - self.register_uris([ - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), - dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid'], 'vifs']), - json={'vifs': [{'id': vif_id}]}), - dict( - method='GET', - uri=self.get_mock_url( - service_type='network', - resource='ports', - base_url_append='v2.0', - append=[vif_id]), - json=fake_port), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid']], + ), + json=self.fake_baremetal_node, + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', + append=[self.fake_baremetal_node['uuid'], 'vifs'], + ), + json={'vifs': [{'id': vif_id}]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + service_type='network', + resource='ports', + base_url_append='v2.0', + append=[vif_id], + ), + json=fake_port, + ), + ] + ) res = self.cloud.list_ports_attached_to_machine( - self.fake_baremetal_node['uuid']) + self.fake_baremetal_node['uuid'] + ) self.assert_calls() self.assertEqual( [_port.Port(**fake_port).to_dict(computed=False)], - [i.to_dict(computed=False) for i in res]) + [i.to_dict(computed=False) for i in res], + ) class TestUpdateMachinePatch(base.IronicTestCase): @@ -1772,7 +2370,8 @@ class TestUpdateMachinePatch(base.IronicTestCase): def setUp(self): super(TestUpdateMachinePatch, self).setUp() self.fake_baremetal_node = fakes.make_fake_machine( - self.name, self.uuid) + self.name, self.uuid + ) def test_update_machine_patch(self): # The model has evolved over time, create the field if @@ -1782,32 +2381,41 @@ def test_update_machine_patch(self): value_to_send = self.fake_baremetal_node[self.field_name] if self.changed: value_to_send = self.new_value - uris = [dict( - method='GET', - uri=self.get_mock_url( - resource='nodes', - append=[self.fake_baremetal_node['uuid']]), - json=self.fake_baremetal_node), + uris = [ + dict( + method='GET', + uri=self.get_mock_url( + resource='nodes', append=[self.fake_baremetal_node['uuid']] + ), + json=self.fake_baremetal_node, + ), ] if self.changed: - test_patch = [{ - 'op': 'replace', - 'path': '/' + self.field_name, - 'value': value_to_send}] + test_patch = [ + { + 'op': 'replace', + 'path': '/' + self.field_name, + 'value': value_to_send, + } + ] uris.append( dict( method='PATCH', uri=self.get_mock_url( resource='nodes', - append=[self.fake_baremetal_node['uuid']]), + append=[self.fake_baremetal_node['uuid']], + ), json=self.fake_baremetal_node, - validate=dict(json=test_patch))) + validate=dict(json=test_patch), + ) + ) self.register_uris(uris) call_args = {self.field_name: value_to_send} update_dict = self.cloud.update_machine( - self.fake_baremetal_node['uuid'], **call_args) + self.fake_baremetal_node['uuid'], **call_args + ) if self.changed: self.assertEqual(['/' + self.field_name], update_dict['changes']) @@ -1819,27 +2427,50 @@ def test_update_machine_patch(self): scenarios = [ ('chassis_uuid', dict(field_name='chassis_uuid', changed=False)), - ('chassis_uuid_changed', - dict(field_name='chassis_uuid', changed=True, - new_value='meow')), + ( + 'chassis_uuid_changed', + dict(field_name='chassis_uuid', changed=True, new_value='meow'), + ), ('driver', dict(field_name='driver', changed=False)), - ('driver_changed', dict(field_name='driver', changed=True, - new_value='meow')), + ( + 'driver_changed', + dict(field_name='driver', changed=True, new_value='meow'), + ), ('driver_info', dict(field_name='driver_info', changed=False)), - ('driver_info_changed', dict(field_name='driver_info', changed=True, - new_value={'cat': 'meow'})), + ( + 'driver_info_changed', + dict( + field_name='driver_info', + changed=True, + new_value={'cat': 'meow'}, + ), + ), ('instance_info', dict(field_name='instance_info', changed=False)), - ('instance_info_changed', - dict(field_name='instance_info', changed=True, - new_value={'cat': 'meow'})), + ( + 'instance_info_changed', + dict( + field_name='instance_info', + changed=True, + new_value={'cat': 'meow'}, + ), + ), ('instance_uuid', dict(field_name='instance_uuid', changed=False)), - ('instance_uuid_changed', - dict(field_name='instance_uuid', changed=True, - new_value='meow')), + ( + 'instance_uuid_changed', + dict(field_name='instance_uuid', changed=True, new_value='meow'), + ), ('name', dict(field_name='name', changed=False)), - ('name_changed', dict(field_name='name', changed=True, - new_value='meow')), + ( + 'name_changed', + dict(field_name='name', changed=True, new_value='meow'), + ), ('properties', dict(field_name='properties', changed=False)), - ('properties_changed', dict(field_name='properties', changed=True, - new_value={'cat': 'meow'})) + ( + 'properties_changed', + dict( + field_name='properties', + changed=True, + new_value={'cat': 'meow'}, + ), + ), ] diff --git a/openstack/tests/unit/cloud/test_baremetal_ports.py b/openstack/tests/unit/cloud/test_baremetal_ports.py index 4ff9b63ef..cd2a2152b 100644 --- a/openstack/tests/unit/cloud/test_baremetal_ports.py +++ b/openstack/tests/unit/cloud/test_baremetal_ports.py @@ -25,28 +25,36 @@ class TestBaremetalPort(base.IronicTestCase): - def setUp(self): super(TestBaremetalPort, self).setUp() self.fake_baremetal_node = fakes.make_fake_machine( - self.name, self.uuid) + self.name, self.uuid + ) # TODO(TheJulia): Some tests below have fake ports, # since they are required in some processes. Lets refactor # them at some point to use self.fake_baremetal_port. self.fake_baremetal_port = fakes.make_fake_port( - '00:01:02:03:04:05', - node_id=self.uuid) + '00:01:02:03:04:05', node_id=self.uuid + ) self.fake_baremetal_port2 = fakes.make_fake_port( - '0a:0b:0c:0d:0e:0f', - node_id=self.uuid) + '0a:0b:0c:0d:0e:0f', node_id=self.uuid + ) def test_list_nics(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(resource='ports', append=['detail']), - json={'ports': [self.fake_baremetal_port, - self.fake_baremetal_port2]}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(resource='ports', append=['detail']), + json={ + 'ports': [ + self.fake_baremetal_port, + self.fake_baremetal_port2, + ] + }, + ), + ] + ) return_value = self.cloud.list_nics() self.assertEqual(2, len(return_value)) @@ -54,59 +62,86 @@ def test_list_nics(self): self.assert_calls() def test_list_nics_failure(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(resource='ports', append=['detail']), - status_code=400) - ]) - self.assertRaises(exc.OpenStackCloudException, - self.cloud.list_nics) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(resource='ports', append=['detail']), + status_code=400, + ) + ] + ) + self.assertRaises(exc.OpenStackCloudException, self.cloud.list_nics) self.assert_calls() def test_list_nics_for_machine(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - resource='ports', - append=['detail'], - qs_elements=['node_uuid=%s' % - self.fake_baremetal_node['uuid']]), - json={'ports': [self.fake_baremetal_port, - self.fake_baremetal_port2]}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='ports', + append=['detail'], + qs_elements=[ + 'node_uuid=%s' % self.fake_baremetal_node['uuid'] + ], + ), + json={ + 'ports': [ + self.fake_baremetal_port, + self.fake_baremetal_port2, + ] + }, + ), + ] + ) return_value = self.cloud.list_nics_for_machine( - self.fake_baremetal_node['uuid']) + self.fake_baremetal_node['uuid'] + ) self.assertEqual(2, len(return_value)) self.assertSubdict(self.fake_baremetal_port, return_value[0]) self.assert_calls() def test_list_nics_for_machine_failure(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - resource='ports', - append=['detail'], - qs_elements=['node_uuid=%s' % - self.fake_baremetal_node['uuid']]), - status_code=400) - ]) - - self.assertRaises(exc.OpenStackCloudException, - self.cloud.list_nics_for_machine, - self.fake_baremetal_node['uuid']) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='ports', + append=['detail'], + qs_elements=[ + 'node_uuid=%s' % self.fake_baremetal_node['uuid'] + ], + ), + status_code=400, + ) + ] + ) + + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.list_nics_for_machine, + self.fake_baremetal_node['uuid'], + ) self.assert_calls() def test_get_nic_by_mac(self): mac = self.fake_baremetal_port['address'] - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - resource='ports', - append=['detail'], - qs_elements=['address=%s' % mac]), - json={'ports': [self.fake_baremetal_port]}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='ports', + append=['detail'], + qs_elements=['address=%s' % mac], + ), + json={'ports': [self.fake_baremetal_port]}, + ), + ] + ) return_value = self.cloud.get_nic_by_mac(mac) @@ -115,14 +150,19 @@ def test_get_nic_by_mac(self): def test_get_nic_by_mac_failure(self): mac = self.fake_baremetal_port['address'] - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - resource='ports', - append=['detail'], - qs_elements=['address=%s' % mac]), - json={'ports': []}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='ports', + append=['detail'], + qs_elements=['address=%s' % mac], + ), + json={'ports': []}, + ), + ] + ) self.assertIsNone(self.cloud.get_nic_by_mac(mac)) diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py index 7b700737d..a1fd697f8 100644 --- a/openstack/tests/unit/cloud/test_caching.py +++ b/openstack/tests/unit/cloud/test_caching.py @@ -39,26 +39,23 @@ def _(msg): _TASK_PROPERTIES = { "id": { "description": _("An identifier for the task"), - "pattern": _('^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}' - '-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$'), - "type": "string" + "pattern": _( + '^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}' + '-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$' + ), + "type": "string", }, "type": { "description": _("The type of task represented by this content"), "enum": [ "import", ], - "type": "string" + "type": "string", }, "status": { "description": _("The current status of this task"), - "enum": [ - "pending", - "processing", - "success", - "failure" - ], - "type": "string" + "enum": ["pending", "processing", "success", "failure"], + "type": "string", }, "input": { "description": _("The parameters required by task, JSON blob"), @@ -70,50 +67,55 @@ def _(msg): }, "owner": { "description": _("An identifier for the owner of this task"), - "type": "string" + "type": "string", }, "message": { - "description": _("Human-readable informative message only included" - " when appropriate (usually on failure)"), + "description": _( + "Human-readable informative message only included" + " when appropriate (usually on failure)" + ), "type": "string", }, "expires_at": { - "description": _("Datetime when this resource would be" - " subject to removal"), - "type": ["null", "string"] + "description": _( + "Datetime when this resource would be" " subject to removal" + ), + "type": ["null", "string"], }, "created_at": { "description": _("Datetime when this resource was created"), - "type": "string" + "type": "string", }, "updated_at": { "description": _("Datetime when this resource was updated"), - "type": "string" + "type": "string", }, 'self': {'type': 'string'}, - 'schema': {'type': 'string'} + 'schema': {'type': 'string'}, } _TASK_SCHEMA = dict( - name='Task', properties=_TASK_PROPERTIES, + name='Task', + properties=_TASK_PROPERTIES, additionalProperties=False, ) class TestMemoryCache(base.TestCase): - def setUp(self): super(TestMemoryCache, self).setUp( - cloud_config_fixture='clouds_cache.yaml') + cloud_config_fixture='clouds_cache.yaml' + ) def _compare_images(self, exp, real): self.assertDictEqual( _image.Image(**exp).to_dict(computed=False), - real.to_dict(computed=False)) + real.to_dict(computed=False), + ) def _compare_volumes(self, exp, real): self.assertDictEqual( _volume.Volume(**exp).to_dict(computed=False), - real.to_dict(computed=False) + real.to_dict(computed=False), ) def test_openstack_cloud(self): @@ -122,13 +124,13 @@ def test_openstack_cloud(self): def _compare_projects(self, exp, real): self.assertDictEqual( _project.Project(**exp).to_dict(computed=False), - real.to_dict(computed=False) + real.to_dict(computed=False), ) def _compare_users(self, exp, real): self.assertDictEqual( _user.User(**exp).to_dict(computed=False), - real.to_dict(computed=False) + real.to_dict(computed=False), ) def test_list_projects_v3(self): @@ -137,28 +139,42 @@ def test_list_projects_v3(self): project_list = [project_one, project_two] first_response = {'projects': [project_one.json_response['project']]} - second_response = {'projects': [p.json_response['project'] - for p in project_list]} + second_response = { + 'projects': [p.json_response['project'] for p in project_list] + } mock_uri = self.get_mock_url( - service_type='identity', resource='projects', - base_url_append='v3') + service_type='identity', resource='projects', base_url_append='v3' + ) - self.register_uris([ - dict(method='GET', uri=mock_uri, status_code=200, - json=first_response), - dict(method='GET', uri=mock_uri, status_code=200, - json=second_response)]) + self.register_uris( + [ + dict( + method='GET', + uri=mock_uri, + status_code=200, + json=first_response, + ), + dict( + method='GET', + uri=mock_uri, + status_code=200, + json=second_response, + ), + ] + ) - for a, b in zip(first_response['projects'], - self.cloud.list_projects()): + for a, b in zip( + first_response['projects'], self.cloud.list_projects() + ): self._compare_projects(a, b) # invalidate the list_projects cache self.cloud.list_projects.invalidate(self.cloud) - for a, b in zip(second_response['projects'], - self.cloud.list_projects()): + for a, b in zip( + second_response['projects'], self.cloud.list_projects() + ): self._compare_projects(a, b) self.assert_calls() @@ -166,13 +182,18 @@ def test_list_projects_v3(self): def test_list_servers_no_herd(self): self.cloud._SERVER_AGE = 2 fake_server = fakes.make_fake_server('1234', 'name') - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': [fake_server]}), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + json={'servers': [fake_server]}, + ), + ] + ) with concurrent.futures.ThreadPoolExecutor(16) as pool: for i in range(16): pool.submit(lambda: self.cloud.list_servers(bare=True)) @@ -183,125 +204,180 @@ def test_list_servers_no_herd(self): self.assert_calls() def test_list_volumes(self): - fake_volume = fakes.FakeVolume('volume1', 'available', - 'Volume 1 Display Name') + fake_volume = fakes.FakeVolume( + 'volume1', 'available', 'Volume 1 Display Name' + ) fake_volume_dict = meta.obj_to_munch(fake_volume) - fake_volume2 = fakes.FakeVolume('volume2', 'available', - 'Volume 2 Display Name') + fake_volume2 = fakes.FakeVolume( + 'volume2', 'available', 'Volume 2 Display Name' + ) fake_volume2_dict = meta.obj_to_munch(fake_volume2) - self.register_uris([ - self.get_cinder_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', 'detail']), - json={'volumes': [fake_volume_dict]}), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', 'detail']), - json={'volumes': [fake_volume_dict, fake_volume2_dict]})]) - - for a, b in zip([fake_volume_dict], - self.cloud.list_volumes()): + self.register_uris( + [ + self.get_cinder_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', 'detail'] + ), + json={'volumes': [fake_volume_dict]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', 'detail'] + ), + json={'volumes': [fake_volume_dict, fake_volume2_dict]}, + ), + ] + ) + + for a, b in zip([fake_volume_dict], self.cloud.list_volumes()): self._compare_volumes(a, b) # this call should hit the cache - for a, b in zip([fake_volume_dict], - self.cloud.list_volumes()): + for a, b in zip([fake_volume_dict], self.cloud.list_volumes()): self._compare_volumes(a, b) self.cloud.list_volumes.invalidate(self.cloud) - for a, b in zip([fake_volume_dict, fake_volume2_dict], - self.cloud.list_volumes()): + for a, b in zip( + [fake_volume_dict, fake_volume2_dict], self.cloud.list_volumes() + ): self._compare_volumes(a, b) self.assert_calls() def test_list_volumes_creating_invalidates(self): - fake_volume = fakes.FakeVolume('volume1', 'creating', - 'Volume 1 Display Name') + fake_volume = fakes.FakeVolume( + 'volume1', 'creating', 'Volume 1 Display Name' + ) fake_volume_dict = meta.obj_to_munch(fake_volume) - fake_volume2 = fakes.FakeVolume('volume2', 'available', - 'Volume 2 Display Name') + fake_volume2 = fakes.FakeVolume( + 'volume2', 'available', 'Volume 2 Display Name' + ) fake_volume2_dict = meta.obj_to_munch(fake_volume2) - self.register_uris([ - self.get_cinder_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', 'detail']), - json={'volumes': [fake_volume_dict]}), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', 'detail']), - json={'volumes': [fake_volume_dict, fake_volume2_dict]})]) - for a, b in zip([fake_volume_dict], - self.cloud.list_volumes()): + self.register_uris( + [ + self.get_cinder_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', 'detail'] + ), + json={'volumes': [fake_volume_dict]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', 'detail'] + ), + json={'volumes': [fake_volume_dict, fake_volume2_dict]}, + ), + ] + ) + for a, b in zip([fake_volume_dict], self.cloud.list_volumes()): self._compare_volumes(a, b) - for a, b in zip([fake_volume_dict, fake_volume2_dict], - self.cloud.list_volumes()): + for a, b in zip( + [fake_volume_dict, fake_volume2_dict], self.cloud.list_volumes() + ): self._compare_volumes(a, b) self.assert_calls() def test_create_volume_invalidates(self): fake_volb4 = meta.obj_to_munch( - fakes.FakeVolume('volume1', 'available', '')) + fakes.FakeVolume('volume1', 'available', '') + ) _id = '12345' fake_vol_creating = meta.obj_to_munch( - fakes.FakeVolume(_id, 'creating', '')) + fakes.FakeVolume(_id, 'creating', '') + ) fake_vol_avail = meta.obj_to_munch( - fakes.FakeVolume(_id, 'available', '')) + fakes.FakeVolume(_id, 'available', '') + ) def now_deleting(request, context): fake_vol_avail['status'] = 'deleting' - self.register_uris([ - self.get_cinder_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', 'detail']), - json={'volumes': [fake_volb4]}), - dict(method='POST', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes']), - json={'volume': fake_vol_creating}), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', _id]), - json={'volume': fake_vol_creating}), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', _id]), - json={'volume': fake_vol_avail}), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', 'detail']), - json={'volumes': [fake_volb4, fake_vol_avail]}), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['volumes', _id]), - json={'volume': fake_vol_avail}), - dict(method='DELETE', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', _id]), - json=now_deleting), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', _id]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', 'detail']), - json={'volumes': [fake_volb4, fake_vol_avail]}), - ]) + self.register_uris( + [ + self.get_cinder_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', 'detail'] + ), + json={'volumes': [fake_volb4]}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes'] + ), + json={'volume': fake_vol_creating}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', _id] + ), + json={'volume': fake_vol_creating}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', _id] + ), + json={'volume': fake_vol_avail}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', 'detail'] + ), + json={'volumes': [fake_volb4, fake_vol_avail]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', _id] + ), + json={'volume': fake_vol_avail}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', _id] + ), + json=now_deleting, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', _id] + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', 'detail'] + ), + json={'volumes': [fake_volb4, fake_vol_avail]}, + ), + ] + ) for a, b in zip([fake_volb4], self.cloud.list_volumes()): self._compare_volumes(a, b) - volume = dict(display_name='junk_vol', - size=1, - display_description='test junk volume') + volume = dict( + display_name='junk_vol', + size=1, + display_description='test junk volume', + ) self.cloud.create_volume(wait=True, timeout=2, **volume) # If cache was not invalidated, we would not see our own volume here # because the first volume was available and thus would already be # cached. - for a, b in zip([fake_volb4, fake_vol_avail], - self.cloud.list_volumes()): + for a, b in zip( + [fake_volb4, fake_vol_avail], self.cloud.list_volumes() + ): self._compare_volumes(a, b) self.cloud.delete_volume(_id) # And now delete and check same thing since list is cached as all @@ -312,14 +388,20 @@ def now_deleting(request, context): def test_list_users(self): user_data = self._get_user_data(email='test@example.com') - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - service_type='identity', - resource='users', - base_url_append='v3'), - status_code=200, - json={'users': [user_data.json_response['user']]})]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + service_type='identity', + resource='users', + base_url_append='v3', + ), + status_code=200, + json={'users': [user_data.json_response['user']]}, + ) + ] + ) users = self.cloud.list_users() self.assertEqual(1, len(users)) self.assertEqual(user_data.user_id, users[0]['id']) @@ -336,14 +418,14 @@ def test_modify_user_invalidates_cache(self): new_req = {'user': {'email': new_resp['user']['email']}} mock_users_url = self.get_mock_url( - service_type='identity', - interface='admin', - resource='users') + service_type='identity', interface='admin', resource='users' + ) mock_user_resource_url = self.get_mock_url( service_type='identity', interface='admin', resource='users', - append=[user_data.user_id]) + append=[user_data.user_id], + ) empty_user_list_resp = {'users': []} users_list_resp = {'users': [user_data.json_response['user']]} @@ -354,35 +436,68 @@ def test_modify_user_invalidates_cache(self): uris_to_mock = [ # Inital User List is Empty - dict(method='GET', uri=mock_users_url, status_code=200, - json=empty_user_list_resp), + dict( + method='GET', + uri=mock_users_url, + status_code=200, + json=empty_user_list_resp, + ), # POST to create the user # GET to get the user data after POST - dict(method='POST', uri=mock_users_url, status_code=200, - json=user_data.json_response, - validate=dict(json=user_data.json_request)), + dict( + method='POST', + uri=mock_users_url, + status_code=200, + json=user_data.json_response, + validate=dict(json=user_data.json_request), + ), # List Users Call - dict(method='GET', uri=mock_users_url, status_code=200, - json=users_list_resp), + dict( + method='GET', + uri=mock_users_url, + status_code=200, + json=users_list_resp, + ), # List users to get ID for update # Get user using user_id from list # Update user # Get updated user - dict(method='GET', uri=mock_users_url, status_code=200, - json=users_list_resp), - dict(method='PUT', uri=mock_user_resource_url, status_code=200, - json=new_resp, validate=dict(json=new_req)), + dict( + method='GET', + uri=mock_users_url, + status_code=200, + json=users_list_resp, + ), + dict( + method='PUT', + uri=mock_user_resource_url, + status_code=200, + json=new_resp, + validate=dict(json=new_req), + ), # List Users Call - dict(method='GET', uri=mock_users_url, status_code=200, - json=updated_users_list_resp), + dict( + method='GET', + uri=mock_users_url, + status_code=200, + json=updated_users_list_resp, + ), # List User to get ID for delete # delete user - dict(method='GET', uri=mock_users_url, status_code=200, - json=updated_users_list_resp), + dict( + method='GET', + uri=mock_users_url, + status_code=200, + json=updated_users_list_resp, + ), dict(method='DELETE', uri=mock_user_resource_url, status_code=204), # List Users Call (empty post delete) - dict(method='GET', uri=mock_users_url, status_code=200, - json=empty_user_list_resp) + dict( + method='GET', + uri=mock_users_url, + status_code=200, + json=empty_user_list_resp, + ), ] self.register_uris(uris_to_mock) @@ -391,8 +506,9 @@ def test_modify_user_invalidates_cache(self): self.assertEqual([], self.cloud.list_users()) # now add one - created = self.cloud.create_user(name=user_data.name, - email=user_data.email) + created = self.cloud.create_user( + name=user_data.name, email=user_data.email + ) self.assertEqual(user_data.user_id, created['id']) self.assertEqual(user_data.name, created['name']) self.assertEqual(user_data.email, created['email']) @@ -403,8 +519,9 @@ def test_modify_user_invalidates_cache(self): self.assertEqual(user_data.email, users[0]['email']) # Update and check to see if it is updated - updated = self.cloud.update_user(user_data.user_id, - email=new_resp['user']['email']) + updated = self.cloud.update_user( + user_data.user_id, email=new_resp['user']['email'] + ) self.assertEqual(user_data.user_id, updated.id) self.assertEqual(user_data.name, updated.name) self.assertEqual(new_resp['user']['email'], updated.email) @@ -420,17 +537,26 @@ def test_modify_user_invalidates_cache(self): def test_list_flavors(self): mock_uri = '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT) + endpoint=fakes.COMPUTE_ENDPOINT + ) uris_to_mock = [ - dict(method='GET', uri=mock_uri, - validate=dict( - headers={'OpenStack-API-Version': 'compute 2.53'}), - json={'flavors': []}), - dict(method='GET', uri=mock_uri, - validate=dict( - headers={'OpenStack-API-Version': 'compute 2.53'}), - json={'flavors': fakes.FAKE_FLAVOR_LIST}) + dict( + method='GET', + uri=mock_uri, + validate=dict( + headers={'OpenStack-API-Version': 'compute 2.53'} + ), + json={'flavors': []}, + ), + dict( + method='GET', + uri=mock_uri, + validate=dict( + headers={'OpenStack-API-Version': 'compute 2.53'} + ), + json={'flavors': fakes.FAKE_FLAVOR_LIST}, + ), ] self.use_compute_discovery() @@ -442,9 +568,7 @@ def test_list_flavors(self): self.cloud.list_flavors.invalidate(self.cloud) self.assertResourceListEqual( - self.cloud.list_flavors(), - fakes.FAKE_FLAVOR_LIST, - _flavor.Flavor + self.cloud.list_flavors(), fakes.FAKE_FLAVOR_LIST, _flavor.Flavor ) self.assert_calls() @@ -454,23 +578,32 @@ def test_list_images(self): self.use_glance() fake_image = fakes.make_fake_image(image_id='42') - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url('image', 'public', - append=['v2', 'images']), - json={'images': []}), - dict(method='GET', - uri=self.get_mock_url('image', 'public', - append=['v2', 'images']), - json={'images': [fake_image]}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'image', 'public', append=['v2', 'images'] + ), + json={'images': []}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', 'public', append=['v2', 'images'] + ), + json={'images': [fake_image]}, + ), + ] + ) self.assertEqual([], self.cloud.list_images()) self.assertEqual([], self.cloud.list_images()) self.cloud.list_images.invalidate(self.cloud) - [self._compare_images(a, b) for a, b in zip( - [fake_image], - self.cloud.list_images())] + [ + self._compare_images(a, b) + for a, b in zip([fake_image], self.cloud.list_images()) + ] self.assert_calls() @@ -479,23 +612,30 @@ def test_list_images_caches_deleted_status(self): deleted_image_id = self.getUniqueString() deleted_image = fakes.make_fake_image( - image_id=deleted_image_id, status='deleted') + image_id=deleted_image_id, status='deleted' + ) active_image_id = self.getUniqueString() active_image = fakes.make_fake_image(image_id=active_image_id) list_return = {'images': [active_image, deleted_image]} - self.register_uris([ - dict(method='GET', - uri='https://image.example.com/v2/images', - json=list_return), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://image.example.com/v2/images', + json=list_return, + ), + ] + ) - [self._compare_images(a, b) for a, b in zip( - [active_image], - self.cloud.list_images())] + [ + self._compare_images(a, b) + for a, b in zip([active_image], self.cloud.list_images()) + ] - [self._compare_images(a, b) for a, b in zip( - [active_image], - self.cloud.list_images())] + [ + self._compare_images(a, b) + for a, b in zip([active_image], self.cloud.list_images()) + ] # We should only have one call self.assert_calls() @@ -507,29 +647,38 @@ def test_cache_no_cloud_name(self): fi = fakes.make_fake_image(image_id=self.getUniqueString()) fi2 = fakes.make_fake_image(image_id=self.getUniqueString()) - self.register_uris([ - dict(method='GET', - uri='https://image.example.com/v2/images', - json={'images': [fi]}), - dict(method='GET', - uri='https://image.example.com/v2/images', - json={'images': [fi, fi2]}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://image.example.com/v2/images', + json={'images': [fi]}, + ), + dict( + method='GET', + uri='https://image.example.com/v2/images', + json={'images': [fi, fi2]}, + ), + ] + ) - [self._compare_images(a, b) for a, b in zip( - [fi], - self.cloud.list_images())] + [ + self._compare_images(a, b) + for a, b in zip([fi], self.cloud.list_images()) + ] # Now test that the list was cached - [self._compare_images(a, b) for a, b in zip( - [fi], - self.cloud.list_images())] + [ + self._compare_images(a, b) + for a, b in zip([fi], self.cloud.list_images()) + ] # Invalidation too self.cloud.list_images.invalidate(self.cloud) - [self._compare_images(a, b) for a, b in zip( - [fi, fi2], - self.cloud.list_images())] + [ + self._compare_images(a, b) + for a, b in zip([fi, fi2], self.cloud.list_images()) + ] def test_list_ports_filtered(self): down_port = test_port.TestPort.mock_neutron_port_create_rep['port'] @@ -537,21 +686,31 @@ def test_list_ports_filtered(self): active_port['status'] = 'ACTIVE' # We're testing to make sure a query string is passed when we're # caching (cache by url), and that the results are still filtered. - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports'], - qs_elements=['status=DOWN']), - json={'ports': [ - down_port, - active_port, - ]}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'ports'], + qs_elements=['status=DOWN'], + ), + json={ + 'ports': [ + down_port, + active_port, + ] + }, + ), + ] + ) ports = self.cloud.list_ports(filters={'status': 'DOWN'}) for a, b in zip([down_port], ports): self.assertDictEqual( _port.Port(**a).to_dict(computed=False), - b.to_dict(computed=False)) + b.to_dict(computed=False), + ) self.assert_calls() @@ -565,41 +724,56 @@ class TestCacheIgnoresQueuedStatus(base.TestCase): def setUp(self): super(TestCacheIgnoresQueuedStatus, self).setUp( - cloud_config_fixture='clouds_cache.yaml') + cloud_config_fixture='clouds_cache.yaml' + ) self.use_glance() active_image_id = self.getUniqueString() self.active_image = fakes.make_fake_image( - image_id=active_image_id, status=self.status) + image_id=active_image_id, status=self.status + ) self.active_list_return = {'images': [self.active_image]} steady_image_id = self.getUniqueString() self.steady_image = fakes.make_fake_image(image_id=steady_image_id) self.steady_list_return = { - 'images': [self.active_image, self.steady_image]} + 'images': [self.active_image, self.steady_image] + } def _compare_images(self, exp, real): self.assertDictEqual( _image.Image(**exp).to_dict(computed=False), - real.to_dict(computed=False)) + real.to_dict(computed=False), + ) def test_list_images_ignores_pending_status(self): - self.register_uris([ - dict(method='GET', - uri='https://image.example.com/v2/images', - json=self.active_list_return), - dict(method='GET', - uri='https://image.example.com/v2/images', - json=self.steady_list_return), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://image.example.com/v2/images', + json=self.active_list_return, + ), + dict( + method='GET', + uri='https://image.example.com/v2/images', + json=self.steady_list_return, + ), + ] + ) - [self._compare_images(a, b) for a, b in zip( - [self.active_image], - self.cloud.list_images())] + [ + self._compare_images(a, b) + for a, b in zip([self.active_image], self.cloud.list_images()) + ] # Should expect steady_image to appear if active wasn't cached - [self._compare_images(a, b) for a, b in zip( - [self.active_image, self.steady_image], - self.cloud.list_images())] + [ + self._compare_images(a, b) + for a, b in zip( + [self.active_image, self.steady_image], + self.cloud.list_images(), + ) + ] class TestCacheSteadyStatus(base.TestCase): @@ -611,45 +785,53 @@ class TestCacheSteadyStatus(base.TestCase): def setUp(self): super(TestCacheSteadyStatus, self).setUp( - cloud_config_fixture='clouds_cache.yaml') + cloud_config_fixture='clouds_cache.yaml' + ) self.use_glance() active_image_id = self.getUniqueString() self.active_image = fakes.make_fake_image( - image_id=active_image_id, status=self.status) + image_id=active_image_id, status=self.status + ) self.active_list_return = {'images': [self.active_image]} def _compare_images(self, exp, real): self.assertDictEqual( _image.Image(**exp).to_dict(computed=False), - real.to_dict(computed=False)) + real.to_dict(computed=False), + ) def test_list_images_caches_steady_status(self): - self.register_uris([ - dict(method='GET', - uri='https://image.example.com/v2/images', - json=self.active_list_return), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://image.example.com/v2/images', + json=self.active_list_return, + ), + ] + ) - [self._compare_images(a, b) for a, b in zip( - [self.active_image], - self.cloud.list_images())] + [ + self._compare_images(a, b) + for a, b in zip([self.active_image], self.cloud.list_images()) + ] - [self._compare_images(a, b) for a, b in zip( - [self.active_image], - self.cloud.list_images())] + [ + self._compare_images(a, b) + for a, b in zip([self.active_image], self.cloud.list_images()) + ] # We should only have one call self.assert_calls() class TestBogusAuth(base.TestCase): - def setUp(self): super(TestBogusAuth, self).setUp( - cloud_config_fixture='clouds_cache.yaml') + cloud_config_fixture='clouds_cache.yaml' + ) def test_get_auth_bogus(self): with testtools.ExpectedException(exceptions.ConfigException): - openstack.connect( - cloud='_bogus_test_', config=self.config) + openstack.connect(cloud='_bogus_test_', config=self.config) diff --git a/openstack/tests/unit/cloud/test_cluster_templates.py b/openstack/tests/unit/cloud/test_cluster_templates.py index c6f5ad70f..bddf07fc8 100644 --- a/openstack/tests/unit/cloud/test_cluster_templates.py +++ b/openstack/tests/unit/cloud/test_cluster_templates.py @@ -49,7 +49,6 @@ class TestClusterTemplates(base.TestCase): - def _compare_clustertemplates(self, exp, real): self.assertDictEqual( cluster_template.ClusterTemplate(**exp).to_dict(computed=False), @@ -57,20 +56,30 @@ def _compare_clustertemplates(self, exp, real): ) def get_mock_url( - self, - service_type='container-infrastructure-management', - base_url_append=None, append=None, resource=None): + self, + service_type='container-infrastructure-management', + base_url_append=None, + append=None, + resource=None, + ): return super(TestClusterTemplates, self).get_mock_url( - service_type=service_type, resource=resource, - append=append, base_url_append=base_url_append) + service_type=service_type, + resource=resource, + append=append, + base_url_append=base_url_append, + ) def test_list_cluster_templates_without_detail(self): - self.register_uris([ - dict( - method='GET', - uri=self.get_mock_url(resource='clustertemplates'), - json=dict(clustertemplates=[cluster_template_obj]))]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(resource='clustertemplates'), + json=dict(clustertemplates=[cluster_template_obj]), + ) + ] + ) cluster_templates_list = self.cloud.list_cluster_templates() self._compare_clustertemplates( cluster_template_obj, @@ -79,11 +88,15 @@ def test_list_cluster_templates_without_detail(self): self.assert_calls() def test_list_cluster_templates_with_detail(self): - self.register_uris([ - dict( - method='GET', - uri=self.get_mock_url(resource='clustertemplates'), - json=dict(clustertemplates=[cluster_template_obj]))]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(resource='clustertemplates'), + json=dict(clustertemplates=[cluster_template_obj]), + ) + ] + ) cluster_templates_list = self.cloud.list_cluster_templates(detail=True) self._compare_clustertemplates( cluster_template_obj, @@ -92,14 +105,19 @@ def test_list_cluster_templates_with_detail(self): self.assert_calls() def test_search_cluster_templates_by_name(self): - self.register_uris([ - dict( - method='GET', - uri=self.get_mock_url(resource='clustertemplates'), - json=dict(clustertemplates=[cluster_template_obj]))]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(resource='clustertemplates'), + json=dict(clustertemplates=[cluster_template_obj]), + ) + ] + ) cluster_templates = self.cloud.search_cluster_templates( - name_or_id='fake-cluster-template') + name_or_id='fake-cluster-template' + ) self.assertEqual(1, len(cluster_templates)) self.assertEqual('fake-uuid', cluster_templates[0]['uuid']) @@ -107,24 +125,33 @@ def test_search_cluster_templates_by_name(self): def test_search_cluster_templates_not_found(self): - self.register_uris([ - dict( - method='GET', - uri=self.get_mock_url(resource='clustertemplates'), - json=dict(clustertemplates=[cluster_template_obj]))]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(resource='clustertemplates'), + json=dict(clustertemplates=[cluster_template_obj]), + ) + ] + ) cluster_templates = self.cloud.search_cluster_templates( - name_or_id='non-existent') + name_or_id='non-existent' + ) self.assertEqual(0, len(cluster_templates)) self.assert_calls() def test_get_cluster_template(self): - self.register_uris([ - dict( - method='GET', - uri=self.get_mock_url(resource='clustertemplates'), - json=dict(clustertemplates=[cluster_template_obj]))]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(resource='clustertemplates'), + json=dict(clustertemplates=[cluster_template_obj]), + ) + ] + ) r = self.cloud.get_cluster_template('fake-cluster-template') self.assertIsNotNone(r) @@ -135,41 +162,52 @@ def test_get_cluster_template(self): self.assert_calls() def test_get_cluster_template_not_found(self): - self.register_uris([ - dict( - method='GET', - uri=self.get_mock_url(resource='clustertemplates'), - json=dict(clustertemplates=[]))]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(resource='clustertemplates'), + json=dict(clustertemplates=[]), + ) + ] + ) r = self.cloud.get_cluster_template('doesNotExist') self.assertIsNone(r) self.assert_calls() def test_create_cluster_template(self): json_response = cluster_template_obj.copy() - kwargs = dict(name=cluster_template_obj['name'], - image_id=cluster_template_obj['image_id'], - keypair_id=cluster_template_obj['keypair_id'], - coe=cluster_template_obj['coe']) - self.register_uris([ - dict( - method='POST', - uri=self.get_mock_url(resource='clustertemplates'), - json=json_response, - validate=dict(json=kwargs))]) - response = self.cloud.create_cluster_template(**kwargs) - self._compare_clustertemplates( - json_response, - response + kwargs = dict( + name=cluster_template_obj['name'], + image_id=cluster_template_obj['image_id'], + keypair_id=cluster_template_obj['keypair_id'], + coe=cluster_template_obj['coe'], + ) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url(resource='clustertemplates'), + json=json_response, + validate=dict(json=kwargs), + ) + ] ) + response = self.cloud.create_cluster_template(**kwargs) + self._compare_clustertemplates(json_response, response) self.assert_calls() def test_create_cluster_template_exception(self): - self.register_uris([ - dict( - method='POST', - uri=self.get_mock_url(resource='clustertemplates'), - status_code=403)]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url(resource='clustertemplates'), + status_code=403, + ) + ] + ) # TODO(mordred) requests here doens't give us a great story # for matching the old error message text. Investigate plumbing # an error message in to the adapter call so that we can give a @@ -177,54 +215,72 @@ def test_create_cluster_template_exception(self): # OpenStackCloudException - but for some reason testtools will not # match the more specific HTTPError, even though it's a subclass # of OpenStackCloudException. - with testtools.ExpectedException( - exceptions.ForbiddenException): + with testtools.ExpectedException(exceptions.ForbiddenException): self.cloud.create_cluster_template('fake-cluster-template') self.assert_calls() def test_delete_cluster_template(self): - self.register_uris([ - dict( - method='GET', - uri=self.get_mock_url(resource='clustertemplates'), - json=dict(clustertemplates=[cluster_template_obj])), - dict( - method='DELETE', - uri=self.get_mock_url(resource='clustertemplates/fake-uuid')), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(resource='clustertemplates'), + json=dict(clustertemplates=[cluster_template_obj]), + ), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='clustertemplates/fake-uuid' + ), + ), + ] + ) self.cloud.delete_cluster_template('fake-uuid') self.assert_calls() def test_update_cluster_template(self): - self.register_uris([ - dict( - method='GET', - uri=self.get_mock_url(resource='clustertemplates'), - json=dict(clustertemplates=[cluster_template_obj])), - dict( - method='PATCH', - uri=self.get_mock_url(resource='clustertemplates/fake-uuid'), - status_code=200, - validate=dict( - json=[{ - 'op': 'replace', - 'path': '/name', - 'value': 'new-cluster-template' - }] - )), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(resource='clustertemplates'), + json=dict(clustertemplates=[cluster_template_obj]), + ), + dict( + method='PATCH', + uri=self.get_mock_url( + resource='clustertemplates/fake-uuid' + ), + status_code=200, + validate=dict( + json=[ + { + 'op': 'replace', + 'path': '/name', + 'value': 'new-cluster-template', + } + ] + ), + ), + ] + ) new_name = 'new-cluster-template' updated = self.cloud.update_cluster_template( - 'fake-uuid', name=new_name) + 'fake-uuid', name=new_name + ) self.assertEqual(new_name, updated.name) self.assert_calls() def test_coe_get_cluster_template(self): - self.register_uris([ - dict( - method='GET', - uri=self.get_mock_url(resource='clustertemplates'), - json=dict(clustertemplates=[cluster_template_obj]))]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(resource='clustertemplates'), + json=dict(clustertemplates=[cluster_template_obj]), + ) + ] + ) r = self.cloud.get_cluster_template('fake-cluster-template') self.assertIsNotNone(r) diff --git a/openstack/tests/unit/cloud/test_clustering.py b/openstack/tests/unit/cloud/test_clustering.py index 062e62861..7c0d1dc4a 100644 --- a/openstack/tests/unit/cloud/test_clustering.py +++ b/openstack/tests/unit/cloud/test_clustering.py @@ -24,14 +24,10 @@ 'max_size': 1, 'min_size': 1, 'timeout': 100, - 'metadata': {} + 'metadata': {}, } -PROFILE_DICT = { - 'name': 'fake-profile-name', - 'spec': {}, - 'metadata': {} -} +PROFILE_DICT = {'name': 'fake-profile-name', 'spec': {}, 'metadata': {}} POLICY_DICT = { 'name': 'fake-profile-name', @@ -43,7 +39,7 @@ 'cluster_id': 'fake-cluster-id', 'name': 'fake-receiver-name', 'params': {}, - 'type': 'webhook' + 'type': 'webhook', } NEW_CLUSTERING_DICT = copy.copy(CLUSTERING_DICT) @@ -57,7 +53,6 @@ class TestClustering(base.TestCase): - def assertAreInstances(self, elements, elem_type): for e in elements: self.assertIsInstance(e, elem_type) @@ -65,12 +60,14 @@ def assertAreInstances(self, elements, elem_type): def _compare_clusters(self, exp, real): self.assertEqual( cluster.Cluster(**exp).to_dict(computed=False), - real.to_dict(computed=False)) + real.to_dict(computed=False), + ) def setUp(self): super(TestClustering, self).setUp() self.use_senlin() + # def test_create_cluster(self): # self.register_uris([ # dict(method='GET', diff --git a/openstack/tests/unit/cloud/test_coe_clusters.py b/openstack/tests/unit/cloud/test_coe_clusters.py index e05ba774c..833b86584 100644 --- a/openstack/tests/unit/cloud/test_coe_clusters.py +++ b/openstack/tests/unit/cloud/test_coe_clusters.py @@ -212,7 +212,5 @@ def test_update_coe_cluster(self): ), ] ) - self.cloud.update_coe_cluster( - coe_cluster_obj["uuid"], node_count=3 - ) + self.cloud.update_coe_cluster(coe_cluster_obj["uuid"], node_count=3) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_coe_clusters_certificate.py b/openstack/tests/unit/cloud/test_coe_clusters_certificate.py index 56c42a846..57aabe7f4 100644 --- a/openstack/tests/unit/cloud/test_coe_clusters_certificate.py +++ b/openstack/tests/unit/cloud/test_coe_clusters_certificate.py @@ -12,7 +12,7 @@ from openstack.container_infrastructure_management.v1 import ( - cluster_certificate + cluster_certificate, ) from openstack.tests.unit import base @@ -20,7 +20,7 @@ cluster_uuid="43e305ce-3a5f-412a-8a14-087834c34c8c", pem="-----BEGIN CERTIFICATE-----\nMIIDAO\n-----END CERTIFICATE-----\n", bay_uuid="43e305ce-3a5f-412a-8a14-087834c34c8c", - links=[] + links=[], ) coe_cluster_signed_cert_obj = dict( @@ -28,50 +28,72 @@ pem='-----BEGIN CERTIFICATE-----\nMIIDAO\n-----END CERTIFICATE-----', bay_uuid='43e305ce-3a5f-412a-8a14-087834c34c8c', links=[], - csr=('-----BEGIN CERTIFICATE REQUEST-----\nMIICfz==' - '\n-----END CERTIFICATE REQUEST-----\n') + csr=( + '-----BEGIN CERTIFICATE REQUEST-----\nMIICfz==' + '\n-----END CERTIFICATE REQUEST-----\n' + ), ) class TestCOEClusters(base.TestCase): def _compare_cluster_certs(self, exp, real): self.assertDictEqual( - cluster_certificate.ClusterCertificate( - **exp).to_dict(computed=False), + cluster_certificate.ClusterCertificate(**exp).to_dict( + computed=False + ), real.to_dict(computed=False), ) def get_mock_url( - self, - service_type='container-infrastructure-management', - base_url_append=None, append=None, resource=None): + self, + service_type='container-infrastructure-management', + base_url_append=None, + append=None, + resource=None, + ): return super(TestCOEClusters, self).get_mock_url( - service_type=service_type, resource=resource, - append=append, base_url_append=base_url_append) + service_type=service_type, + resource=resource, + append=append, + base_url_append=base_url_append, + ) def test_get_coe_cluster_certificate(self): - self.register_uris([dict( - method='GET', - uri=self.get_mock_url( - resource='certificates', - append=[coe_cluster_ca_obj['cluster_uuid']]), - json=coe_cluster_ca_obj) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='certificates', + append=[coe_cluster_ca_obj['cluster_uuid']], + ), + json=coe_cluster_ca_obj, + ) + ] + ) ca_cert = self.cloud.get_coe_cluster_certificate( - coe_cluster_ca_obj['cluster_uuid']) - self._compare_cluster_certs( - coe_cluster_ca_obj, - ca_cert) + coe_cluster_ca_obj['cluster_uuid'] + ) + self._compare_cluster_certs(coe_cluster_ca_obj, ca_cert) self.assert_calls() def test_sign_coe_cluster_certificate(self): - self.register_uris([dict( - method='POST', - uri=self.get_mock_url(resource='certificates'), - json={"cluster_uuid": coe_cluster_signed_cert_obj['cluster_uuid'], - "csr": coe_cluster_signed_cert_obj['csr']} - )]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url(resource='certificates'), + json={ + "cluster_uuid": coe_cluster_signed_cert_obj[ + 'cluster_uuid' + ], + "csr": coe_cluster_signed_cert_obj['csr'], + }, + ) + ] + ) self.cloud.sign_coe_cluster_certificate( coe_cluster_signed_cert_obj['cluster_uuid'], - coe_cluster_signed_cert_obj['csr']) + coe_cluster_signed_cert_obj['csr'], + ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_create_server.py b/openstack/tests/unit/cloud/test_create_server.py index 5fb868479..4b3c1e543 100644 --- a/openstack/tests/unit/cloud/test_create_server.py +++ b/openstack/tests/unit/cloud/test_create_server.py @@ -42,32 +42,51 @@ def test_create_server_with_get_exception(self): raises an exception in create_server. """ build_server = fakes.make_fake_server('1234', '', 'BUILD') - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': []}), - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', append=['servers']), - json={'server': build_server}, - validate=dict( - json={'server': { - u'flavorRef': u'flavor-id', - u'imageRef': u'image-id', - u'max_count': 1, - u'min_count': 1, - u'name': u'server-name', - 'networks': 'auto'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - status_code=404), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': []}, + ), + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'] + ), + json={'server': build_server}, + validate=dict( + json={ + 'server': { + 'flavorRef': 'flavor-id', + 'imageRef': 'image-id', + 'max_count': 1, + 'min_count': 1, + 'name': 'server-name', + 'networks': 'auto', + } + } + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'] + ), + status_code=404, + ), + ] + ) self.assertRaises( - exc.OpenStackCloudException, self.cloud.create_server, - 'server-name', {'id': 'image-id'}, {'id': 'flavor-id'}) + exc.OpenStackCloudException, + self.cloud.create_server, + 'server-name', + {'id': 'image-id'}, + {'id': 'flavor-id'}, + ) self.assert_calls() def test_create_server_with_server_error(self): @@ -77,32 +96,51 @@ def test_create_server_with_server_error(self): """ build_server = fakes.make_fake_server('1234', '', 'BUILD') error_server = fakes.make_fake_server('1234', '', 'ERROR') - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': []}), - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', append=['servers']), - json={'server': build_server}, - validate=dict( - json={'server': { - u'flavorRef': u'flavor-id', - u'imageRef': u'image-id', - u'max_count': 1, - u'min_count': 1, - u'name': u'server-name', - 'networks': 'auto'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': error_server}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': []}, + ), + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'] + ), + json={'server': build_server}, + validate=dict( + json={ + 'server': { + 'flavorRef': 'flavor-id', + 'imageRef': 'image-id', + 'max_count': 1, + 'min_count': 1, + 'name': 'server-name', + 'networks': 'auto', + } + } + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'] + ), + json={'server': error_server}, + ), + ] + ) self.assertRaises( - exc.OpenStackCloudException, self.cloud.create_server, - 'server-name', {'id': 'image-id'}, {'id': 'flavor-id'}) + exc.OpenStackCloudException, + self.cloud.create_server, + 'server-name', + {'id': 'image-id'}, + {'id': 'flavor-id'}, + ) self.assert_calls() def test_create_server_wait_server_error(self): @@ -112,38 +150,59 @@ def test_create_server_wait_server_error(self): """ build_server = fakes.make_fake_server('1234', '', 'BUILD') error_server = fakes.make_fake_server('1234', '', 'ERROR') - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': []}), - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', append=['servers']), - json={'server': build_server}, - validate=dict( - json={'server': { - u'flavorRef': u'flavor-id', - u'imageRef': u'image-id', - u'max_count': 1, - u'min_count': 1, - u'name': u'server-name', - 'networks': 'auto'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': [build_server]}), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': [error_server]}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': []}, + ), + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'] + ), + json={'server': build_server}, + validate=dict( + json={ + 'server': { + 'flavorRef': 'flavor-id', + 'imageRef': 'image-id', + 'max_count': 1, + 'min_count': 1, + 'name': 'server-name', + 'networks': 'auto', + } + } + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + json={'servers': [build_server]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + json={'servers': [error_server]}, + ), + ] + ) self.assertRaises( exc.OpenStackCloudException, self.cloud.create_server, - 'server-name', dict(id='image-id'), - dict(id='flavor-id'), wait=True) + 'server-name', + dict(id='image-id'), + dict(id='flavor-id'), + wait=True, + ) self.assert_calls() @@ -153,35 +212,53 @@ def test_create_server_with_timeout(self): exception in create_server. """ fake_server = fakes.make_fake_server('1234', '', 'BUILD') - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': []}), - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', append=['servers']), - json={'server': fake_server}, - validate=dict( - json={'server': { - u'flavorRef': u'flavor-id', - u'imageRef': u'image-id', - u'max_count': 1, - u'min_count': 1, - u'name': u'server-name', - 'networks': 'auto'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': [fake_server]}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': []}, + ), + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'] + ), + json={'server': fake_server}, + validate=dict( + json={ + 'server': { + 'flavorRef': 'flavor-id', + 'imageRef': 'image-id', + 'max_count': 1, + 'min_count': 1, + 'name': 'server-name', + 'networks': 'auto', + } + } + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + json={'servers': [fake_server]}, + ), + ] + ) self.assertRaises( exc.OpenStackCloudTimeout, self.cloud.create_server, 'server-name', - dict(id='image-id'), dict(id='flavor-id'), - wait=True, timeout=0.01) + dict(id='image-id'), + dict(id='flavor-id'), + wait=True, + timeout=0.01, + ) # We poll at the end, so we don't know real counts self.assert_calls(do_count=False) @@ -191,35 +268,51 @@ def test_create_server_no_wait(self): create call returns the server instance. """ fake_server = fakes.make_fake_server('1234', '', 'BUILD') - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': []}), - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', append=['servers']), - json={'server': fake_server}, - validate=dict( - json={'server': { - u'flavorRef': u'flavor-id', - u'imageRef': u'image-id', - u'max_count': 1, - u'min_count': 1, - u'name': u'server-name', - 'networks': 'auto'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': fake_server}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': []}, + ), + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'] + ), + json={'server': fake_server}, + validate=dict( + json={ + 'server': { + 'flavorRef': 'flavor-id', + 'imageRef': 'image-id', + 'max_count': 1, + 'min_count': 1, + 'name': 'server-name', + 'networks': 'auto', + } + } + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'] + ), + json={'server': fake_server}, + ), + ] + ) self.assertDictEqual( server.Server(**fake_server).to_dict(computed=False), self.cloud.create_server( name='server-name', image=dict(id='image-id'), - flavor=dict(id='flavor-id')).to_dict(computed=False) + flavor=dict(id='flavor-id'), + ).to_dict(computed=False), ) self.assert_calls() @@ -229,37 +322,54 @@ def test_create_server_config_drive(self): Test that config_drive gets passed in properly """ fake_server = fakes.make_fake_server('1234', '', 'BUILD') - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': []}), - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', append=['servers']), - json={'server': fake_server}, - validate=dict( - json={'server': { - u'flavorRef': u'flavor-id', - u'imageRef': u'image-id', - u'config_drive': True, - u'max_count': 1, - u'min_count': 1, - u'name': u'server-name', - 'networks': 'auto'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': fake_server}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': []}, + ), + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'] + ), + json={'server': fake_server}, + validate=dict( + json={ + 'server': { + 'flavorRef': 'flavor-id', + 'imageRef': 'image-id', + 'config_drive': True, + 'max_count': 1, + 'min_count': 1, + 'name': 'server-name', + 'networks': 'auto', + } + } + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'] + ), + json={'server': fake_server}, + ), + ] + ) self.assertDictEqual( server.Server(**fake_server).to_dict(computed=False), self.cloud.create_server( name='server-name', image=dict(id='image-id'), flavor=dict(id='flavor-id'), - config_drive=True).to_dict(computed=False)) + config_drive=True, + ).to_dict(computed=False), + ) self.assert_calls() @@ -268,36 +378,52 @@ def test_create_server_config_drive_none(self): Test that config_drive gets not passed in properly """ fake_server = fakes.make_fake_server('1234', '', 'BUILD') - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': []}), - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', append=['servers']), - json={'server': fake_server}, - validate=dict( - json={'server': { - u'flavorRef': u'flavor-id', - u'imageRef': u'image-id', - u'max_count': 1, - u'min_count': 1, - u'name': u'server-name', - 'networks': 'auto'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': fake_server}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': []}, + ), + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'] + ), + json={'server': fake_server}, + validate=dict( + json={ + 'server': { + 'flavorRef': 'flavor-id', + 'imageRef': 'image-id', + 'max_count': 1, + 'min_count': 1, + 'name': 'server-name', + 'networks': 'auto', + } + } + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'] + ), + json={'server': fake_server}, + ), + ] + ) self.assertEqual( server.Server(**fake_server).to_dict(computed=False), self.cloud.create_server( name='server-name', image=dict(id='image-id'), flavor=dict(id='flavor-id'), - config_drive=None).to_dict(computed=False) + config_drive=None, + ).to_dict(computed=False), ) self.assert_calls() @@ -309,37 +435,56 @@ def test_create_server_with_admin_pass_no_wait(self): admin_pass = self.getUniqueString('password') fake_server = fakes.make_fake_server('1234', '', 'BUILD') fake_create_server = fakes.make_fake_server( - '1234', '', 'BUILD', admin_pass=admin_pass) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': []}), - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', append=['servers']), - json={'server': fake_create_server}, - validate=dict( - json={'server': { - u'adminPass': admin_pass, - u'flavorRef': u'flavor-id', - u'imageRef': u'image-id', - u'max_count': 1, - u'min_count': 1, - u'name': u'server-name', - 'networks': 'auto'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': fake_server}), - ]) + '1234', '', 'BUILD', admin_pass=admin_pass + ) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': []}, + ), + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'] + ), + json={'server': fake_create_server}, + validate=dict( + json={ + 'server': { + 'adminPass': admin_pass, + 'flavorRef': 'flavor-id', + 'imageRef': 'image-id', + 'max_count': 1, + 'min_count': 1, + 'name': 'server-name', + 'networks': 'auto', + } + } + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'] + ), + json={'server': fake_server}, + ), + ] + ) self.assertEqual( admin_pass, self.cloud.create_server( - name='server-name', image=dict(id='image-id'), + name='server-name', + image=dict(id='image-id'), flavor=dict(id='flavor-id'), - admin_pass=admin_pass)['admin_password']) + admin_pass=admin_pass, + )['admin_password'], + ) self.assert_calls() @@ -351,43 +496,58 @@ def test_create_server_with_admin_pass_wait(self, mock_wait): admin_pass = self.getUniqueString('password') fake_server = fakes.make_fake_server('1234', '', 'BUILD') fake_server_with_pass = fakes.make_fake_server( - '1234', '', 'BUILD', admin_pass=admin_pass) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': []}), - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', append=['servers']), - json={'server': fake_server_with_pass}, - validate=dict( - json={'server': { - u'flavorRef': u'flavor-id', - u'imageRef': u'image-id', - u'max_count': 1, - u'min_count': 1, - u'adminPass': admin_pass, - u'name': u'server-name', - 'networks': 'auto'}})), - ]) + '1234', '', 'BUILD', admin_pass=admin_pass + ) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': []}, + ), + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'] + ), + json={'server': fake_server_with_pass}, + validate=dict( + json={ + 'server': { + 'flavorRef': 'flavor-id', + 'imageRef': 'image-id', + 'max_count': 1, + 'min_count': 1, + 'adminPass': admin_pass, + 'name': 'server-name', + 'networks': 'auto', + } + } + ), + ), + ] + ) # The wait returns non-password server mock_wait.return_value = server.Server(**fake_server) new_server = self.cloud.create_server( - name='server-name', image=dict(id='image-id'), + name='server-name', + image=dict(id='image-id'), flavor=dict(id='flavor-id'), - admin_pass=admin_pass, wait=True) + admin_pass=admin_pass, + wait=True, + ) # Assert that we did wait self.assertTrue(mock_wait.called) # Even with the wait, we should still get back a passworded server self.assertEqual( - new_server['admin_password'], - fake_server_with_pass['adminPass'] + new_server['admin_password'], fake_server_with_pass['adminPass'] ) self.assert_calls() @@ -396,40 +556,59 @@ def test_create_server_user_data_base64(self): Test that a server passed user-data sends it base64 encoded. """ user_data = self.getUniqueString('user_data') - user_data_b64 = base64.b64encode( - user_data.encode('utf-8')).decode('utf-8') + user_data_b64 = base64.b64encode(user_data.encode('utf-8')).decode( + 'utf-8' + ) fake_server = fakes.make_fake_server('1234', '', 'BUILD') fake_server['user_data'] = user_data - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': []}), - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', append=['servers']), - json={'server': fake_server}, - validate=dict( - json={'server': { - u'flavorRef': u'flavor-id', - u'imageRef': u'image-id', - u'max_count': 1, - u'min_count': 1, - u'user_data': user_data_b64, - u'name': u'server-name', - 'networks': 'auto'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': fake_server}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': []}, + ), + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'] + ), + json={'server': fake_server}, + validate=dict( + json={ + 'server': { + 'flavorRef': 'flavor-id', + 'imageRef': 'image-id', + 'max_count': 1, + 'min_count': 1, + 'user_data': user_data_b64, + 'name': 'server-name', + 'networks': 'auto', + } + } + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'] + ), + json={'server': fake_server}, + ), + ] + ) self.cloud.create_server( - name='server-name', image=dict(id='image-id'), + name='server-name', + image=dict(id='image-id'), flavor=dict(id='flavor-id'), - userdata=user_data, wait=False) + userdata=user_data, + wait=False, + ) self.assert_calls() @@ -445,26 +624,45 @@ def test_wait_for_server(self, mock_get_server, mock_get_active_server): active_server = {'id': 'fake_server_id', 'status': 'ACTIVE'} mock_get_server.side_effect = iter([building_server, active_server]) - mock_get_active_server.side_effect = iter([ - building_server, active_server]) + mock_get_active_server.side_effect = iter( + [building_server, active_server] + ) server = self.cloud.wait_for_server(building_server) self.assertEqual(2, mock_get_server.call_count) - mock_get_server.assert_has_calls([ - mock.call(building_server['id']), - mock.call(active_server['id']), - ]) + mock_get_server.assert_has_calls( + [ + mock.call(building_server['id']), + mock.call(active_server['id']), + ] + ) self.assertEqual(2, mock_get_active_server.call_count) - mock_get_active_server.assert_has_calls([ - mock.call(server=building_server, reuse=True, auto_ip=True, - ips=None, ip_pool=None, wait=True, timeout=mock.ANY, - nat_destination=None), - mock.call(server=active_server, reuse=True, auto_ip=True, - ips=None, ip_pool=None, wait=True, timeout=mock.ANY, - nat_destination=None), - ]) + mock_get_active_server.assert_has_calls( + [ + mock.call( + server=building_server, + reuse=True, + auto_ip=True, + ips=None, + ip_pool=None, + wait=True, + timeout=mock.ANY, + nat_destination=None, + ), + mock.call( + server=active_server, + reuse=True, + auto_ip=True, + ips=None, + ip_pool=None, + wait=True, + timeout=mock.ANY, + nat_destination=None, + ), + ] + ) self.assertEqual('ACTIVE', server['status']) @@ -476,102 +674,153 @@ def test_create_server_wait(self, mock_wait): # TODO(mordred) Make this a full proper response fake_server = fakes.make_fake_server('1234', '', 'BUILD') - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': []}), - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', append=['servers']), - json={'server': fake_server}, - validate=dict( - json={'server': { - u'flavorRef': u'flavor-id', - u'imageRef': u'image-id', - u'max_count': 1, - u'min_count': 1, - u'name': u'server-name', - 'networks': 'auto'}})), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': []}, + ), + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'] + ), + json={'server': fake_server}, + validate=dict( + json={ + 'server': { + 'flavorRef': 'flavor-id', + 'imageRef': 'image-id', + 'max_count': 1, + 'min_count': 1, + 'name': 'server-name', + 'networks': 'auto', + } + } + ), + ), + ] + ) self.cloud.create_server( - 'server-name', - dict(id='image-id'), dict(id='flavor-id'), wait=True), + 'server-name', dict(id='image-id'), dict(id='flavor-id'), + wait=True, + ), # This is a pretty dirty hack to ensure we in principle use object with # expected properties srv = server.Server.existing( connection=self.cloud, - min_count=1, max_count=1, + min_count=1, + max_count=1, networks='auto', imageRef='image-id', flavorRef='flavor-id', - **fake_server) + **fake_server + ) mock_wait.assert_called_once_with( srv, - auto_ip=True, ips=None, - ip_pool=None, reuse=True, timeout=180, + auto_ip=True, + ips=None, + ip_pool=None, + reuse=True, + timeout=180, nat_destination=None, ) self.assert_calls() @mock.patch.object(connection.Connection, 'add_ips_to_server') - def test_create_server_no_addresses( - self, mock_add_ips_to_server): + def test_create_server_no_addresses(self, mock_add_ips_to_server): """ Test that create_server with a wait throws an exception if the server doesn't have addresses. """ build_server = fakes.make_fake_server('1234', '', 'BUILD') fake_server = fakes.make_fake_server( - '1234', '', 'ACTIVE', addresses={}) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': []}), - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', append=['servers']), - json={'server': build_server}, - validate=dict( - json={'server': { - u'flavorRef': u'flavor-id', - u'imageRef': u'image-id', - u'max_count': 1, - u'min_count': 1, - u'name': u'server-name', - 'networks': 'auto'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': [build_server]}), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': [fake_server]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports'], - qs_elements=['device_id=1234']), - json={'ports': []}), - dict(method='DELETE', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234'])), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - status_code=404), - ]) + '1234', '', 'ACTIVE', addresses={} + ) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': []}, + ), + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'] + ), + json={'server': build_server}, + validate=dict( + json={ + 'server': { + 'flavorRef': 'flavor-id', + 'imageRef': 'image-id', + 'max_count': 1, + 'min_count': 1, + 'name': 'server-name', + 'networks': 'auto', + } + } + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + json={'servers': [build_server]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + json={'servers': [fake_server]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'ports'], + qs_elements=['device_id=1234'], + ), + json={'ports': []}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'] + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'] + ), + status_code=404, + ), + ] + ) mock_add_ips_to_server.return_value = fake_server self.cloud._SERVER_AGE = 0 self.assertRaises( - exc.OpenStackCloudException, self.cloud.create_server, - 'server-name', {'id': 'image-id'}, {'id': 'flavor-id'}, - wait=True) + exc.OpenStackCloudException, + self.cloud.create_server, + 'server-name', + {'id': 'image-id'}, + {'id': 'flavor-id'}, + wait=True, + ) self.assert_calls() @@ -581,51 +830,77 @@ def test_create_server_network_with_no_nics(self): attempt to get the network for the server. """ build_server = fakes.make_fake_server('1234', '', 'BUILD') - network = { - 'id': 'network-id', - 'name': 'network-name' - } - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks', 'network-name']), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks'], - qs_elements=['name=network-name']), - json={'networks': [network]}), - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', append=['servers']), - json={'server': build_server}, - validate=dict( - json={'server': { - u'flavorRef': u'flavor-id', - u'imageRef': u'image-id', - u'max_count': 1, - u'min_count': 1, - u'networks': [{u'uuid': u'network-id'}], - u'name': u'server-name'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': build_server}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': [network]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets']), - json={'subnets': []}), - ]) + network = {'id': 'network-id', 'name': 'network-name'} + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks', 'network-name'], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks'], + qs_elements=['name=network-name'], + ), + json={'networks': [network]}, + ), + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'] + ), + json={'server': build_server}, + validate=dict( + json={ + 'server': { + 'flavorRef': 'flavor-id', + 'imageRef': 'image-id', + 'max_count': 1, + 'min_count': 1, + 'networks': [{'uuid': 'network-id'}], + 'name': 'server-name', + } + } + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'] + ), + json={'server': build_server}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': [network]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets'] + ), + json={'subnets': []}, + ), + ] + ) self.cloud.create_server( 'server-name', - dict(id='image-id'), dict(id='flavor-id'), network='network-name') + dict(id='image-id'), + dict(id='flavor-id'), + network='network-name', + ) self.assert_calls() def test_create_server_network_with_empty_nics(self): @@ -633,52 +908,79 @@ def test_create_server_network_with_empty_nics(self): Verify that if 'network' is supplied, along with an empty 'nics' list, it's treated the same as if 'nics' were not included. """ - network = { - 'id': 'network-id', - 'name': 'network-name' - } + network = {'id': 'network-id', 'name': 'network-name'} build_server = fakes.make_fake_server('1234', '', 'BUILD') - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks', 'network-name']), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks'], - qs_elements=['name=network-name']), - json={'networks': [network]}), - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', append=['servers']), - json={'server': build_server}, - validate=dict( - json={'server': { - u'flavorRef': u'flavor-id', - u'imageRef': u'image-id', - u'max_count': 1, - u'min_count': 1, - u'networks': [{u'uuid': u'network-id'}], - u'name': u'server-name'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': build_server}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': [network]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets']), - json={'subnets': []}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks', 'network-name'], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks'], + qs_elements=['name=network-name'], + ), + json={'networks': [network]}, + ), + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'] + ), + json={'server': build_server}, + validate=dict( + json={ + 'server': { + 'flavorRef': 'flavor-id', + 'imageRef': 'image-id', + 'max_count': 1, + 'min_count': 1, + 'networks': [{'uuid': 'network-id'}], + 'name': 'server-name', + } + } + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'] + ), + json={'server': build_server}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': [network]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets'] + ), + json={'subnets': []}, + ), + ] + ) self.cloud.create_server( - 'server-name', dict(id='image-id'), dict(id='flavor-id'), - network='network-name', nics=[]) + 'server-name', + dict(id='image-id'), + dict(id='flavor-id'), + network='network-name', + nics=[], + ) self.assert_calls() def test_create_server_network_fixed_ip(self): @@ -686,42 +988,60 @@ def test_create_server_network_fixed_ip(self): Verify that if 'fixed_ip' is supplied in nics, we pass it to networks appropriately. """ - network = { - 'id': 'network-id', - 'name': 'network-name' - } + network = {'id': 'network-id', 'name': 'network-name'} fixed_ip = '10.0.0.1' build_server = fakes.make_fake_server('1234', '', 'BUILD') - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', append=['servers']), - json={'server': build_server}, - validate=dict( - json={'server': { - u'flavorRef': u'flavor-id', - u'imageRef': u'image-id', - u'max_count': 1, - u'min_count': 1, - u'networks': [{'fixed_ip': fixed_ip}], - u'name': u'server-name'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': build_server}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': [network]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets']), - json={'subnets': []}), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'] + ), + json={'server': build_server}, + validate=dict( + json={ + 'server': { + 'flavorRef': 'flavor-id', + 'imageRef': 'image-id', + 'max_count': 1, + 'min_count': 1, + 'networks': [{'fixed_ip': fixed_ip}], + 'name': 'server-name', + } + } + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'] + ), + json={'server': build_server}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': [network]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets'] + ), + json={'subnets': []}, + ), + ] + ) self.cloud.create_server( - 'server-name', dict(id='image-id'), dict(id='flavor-id'), - nics=[{'fixed_ip': fixed_ip}]) + 'server-name', + dict(id='image-id'), + dict(id='flavor-id'), + nics=[{'fixed_ip': fixed_ip}], + ) self.assert_calls() def test_create_server_network_v4_fixed_ip(self): @@ -729,42 +1049,60 @@ def test_create_server_network_v4_fixed_ip(self): Verify that if 'v4-fixed-ip' is supplied in nics, we pass it to networks appropriately. """ - network = { - 'id': 'network-id', - 'name': 'network-name' - } + network = {'id': 'network-id', 'name': 'network-name'} fixed_ip = '10.0.0.1' build_server = fakes.make_fake_server('1234', '', 'BUILD') - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', append=['servers']), - json={'server': build_server}, - validate=dict( - json={'server': { - u'flavorRef': u'flavor-id', - u'imageRef': u'image-id', - u'max_count': 1, - u'min_count': 1, - u'networks': [{'fixed_ip': fixed_ip}], - u'name': u'server-name'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': build_server}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': [network]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets']), - json={'subnets': []}), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'] + ), + json={'server': build_server}, + validate=dict( + json={ + 'server': { + 'flavorRef': 'flavor-id', + 'imageRef': 'image-id', + 'max_count': 1, + 'min_count': 1, + 'networks': [{'fixed_ip': fixed_ip}], + 'name': 'server-name', + } + } + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'] + ), + json={'server': build_server}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': [network]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets'] + ), + json={'subnets': []}, + ), + ] + ) self.cloud.create_server( - 'server-name', dict(id='image-id'), dict(id='flavor-id'), - nics=[{'fixed_ip': fixed_ip}]) + 'server-name', + dict(id='image-id'), + dict(id='flavor-id'), + nics=[{'fixed_ip': fixed_ip}], + ) self.assert_calls() def test_create_server_network_v6_fixed_ip(self): @@ -772,44 +1110,62 @@ def test_create_server_network_v6_fixed_ip(self): Verify that if 'v6-fixed-ip' is supplied in nics, we pass it to networks appropriately. """ - network = { - 'id': 'network-id', - 'name': 'network-name' - } + network = {'id': 'network-id', 'name': 'network-name'} # Note - it doesn't actually have to be a v6 address - it's just # an alias. fixed_ip = 'fe80::28da:5fff:fe57:13ed' build_server = fakes.make_fake_server('1234', '', 'BUILD') - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', append=['servers']), - json={'server': build_server}, - validate=dict( - json={'server': { - u'flavorRef': u'flavor-id', - u'imageRef': u'image-id', - u'max_count': 1, - u'min_count': 1, - u'networks': [{'fixed_ip': fixed_ip}], - u'name': u'server-name'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': build_server}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': [network]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets']), - json={'subnets': []}), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'] + ), + json={'server': build_server}, + validate=dict( + json={ + 'server': { + 'flavorRef': 'flavor-id', + 'imageRef': 'image-id', + 'max_count': 1, + 'min_count': 1, + 'networks': [{'fixed_ip': fixed_ip}], + 'name': 'server-name', + } + } + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'] + ), + json={'server': build_server}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': [network]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets'] + ), + json={'subnets': []}, + ), + ] + ) self.cloud.create_server( - 'server-name', dict(id='image-id'), dict(id='flavor-id'), - nics=[{'fixed_ip': fixed_ip}]) + 'server-name', + dict(id='image-id'), + dict(id='flavor-id'), + nics=[{'fixed_ip': fixed_ip}], + ) self.assert_calls() def test_create_server_network_fixed_ip_conflicts(self): @@ -822,12 +1178,13 @@ def test_create_server_network_fixed_ip_conflicts(self): self.use_nothing() fixed_ip = '10.0.0.1' self.assertRaises( - exc.OpenStackCloudException, self.cloud.create_server, - 'server-name', dict(id='image-id'), dict(id='flavor-id'), - nics=[{ - 'fixed_ip': fixed_ip, - 'v4-fixed-ip': fixed_ip - }]) + exc.OpenStackCloudException, + self.cloud.create_server, + 'server-name', + dict(id='image-id'), + dict(id='flavor-id'), + nics=[{'fixed_ip': fixed_ip, 'v4-fixed-ip': fixed_ip}], + ) self.assert_calls() def test_create_server_get_flavor_image(self): @@ -839,41 +1196,67 @@ def test_create_server_get_flavor_image(self): build_server = fakes.make_fake_server('1234', '', 'BUILD') active_server = fakes.make_fake_server('1234', '', 'BUILD') - self.register_uris([ - dict(method='GET', - uri='https://image.example.com/v2/images', - json=fake_image_search_return), - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['flavors', 'vanilla'], - qs_elements=[]), - json=fakes.FAKE_FLAVOR), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', append=['servers']), - json={'server': build_server}, - validate=dict( - json={'server': { - u'flavorRef': fakes.FLAVOR_ID, - u'imageRef': image_id, - u'max_count': 1, - u'min_count': 1, - u'networks': [{u'uuid': u'some-network'}], - u'name': u'server-name'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': active_server}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': []}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://image.example.com/v2/images', + json=fake_image_search_return, + ), + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['flavors', 'vanilla'], + qs_elements=[], + ), + json=fakes.FAKE_FLAVOR, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'] + ), + json={'server': build_server}, + validate=dict( + json={ + 'server': { + 'flavorRef': fakes.FLAVOR_ID, + 'imageRef': image_id, + 'max_count': 1, + 'min_count': 1, + 'networks': [{'uuid': 'some-network'}], + 'name': 'server-name', + } + } + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'] + ), + json={'server': active_server}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': []}, + ), + ] + ) self.cloud.create_server( - 'server-name', image_id, 'vanilla', - nics=[{'net-id': 'some-network'}], wait=False) + 'server-name', + image_id, + 'vanilla', + nics=[{'net-id': 'some-network'}], + wait=False, + ) self.assert_calls() @@ -884,33 +1267,52 @@ def test_create_server_nics_port_id(self): image_id = uuid.uuid4().hex port_id = uuid.uuid4().hex - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', append=['servers']), - json={'server': build_server}, - validate=dict( - json={'server': { - u'flavorRef': fakes.FLAVOR_ID, - u'imageRef': image_id, - u'max_count': 1, - u'min_count': 1, - u'networks': [{u'port': port_id}], - u'name': u'server-name'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': active_server}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': []}), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'] + ), + json={'server': build_server}, + validate=dict( + json={ + 'server': { + 'flavorRef': fakes.FLAVOR_ID, + 'imageRef': image_id, + 'max_count': 1, + 'min_count': 1, + 'networks': [{'port': port_id}], + 'name': 'server-name', + } + } + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'] + ), + json={'server': active_server}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': []}, + ), + ] + ) self.cloud.create_server( - 'server-name', dict(id=image_id), dict(id=fakes.FLAVOR_ID), - nics=[{'port-id': port_id}], wait=False) + 'server-name', + dict(id=image_id), + dict(id=fakes.FLAVOR_ID), + nics=[{'port-id': port_id}], + wait=False, + ) self.assert_calls() @@ -918,49 +1320,68 @@ def test_create_boot_attach_volume(self): build_server = fakes.make_fake_server('1234', '', 'BUILD') active_server = fakes.make_fake_server('1234', '', 'BUILD') - vol = {'id': 'volume001', 'status': 'available', - 'name': '', 'attachments': []} + vol = { + 'id': 'volume001', + 'status': 'available', + 'name': '', + 'attachments': [], + } volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': []}), - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', append=['servers']), - json={'server': build_server}, - validate=dict( - json={'server': { - u'flavorRef': 'flavor-id', - u'imageRef': 'image-id', - u'max_count': 1, - u'min_count': 1, - u'block_device_mapping_v2': [ - { - u'boot_index': 0, - u'delete_on_termination': True, - u'destination_type': u'local', - u'source_type': u'image', - u'uuid': u'image-id' - }, - { - u'boot_index': u'-1', - u'delete_on_termination': False, - u'destination_type': u'volume', - u'source_type': u'volume', - u'uuid': u'volume001' - } - ], - u'name': u'server-name', - 'networks': 'auto'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': active_server}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': []}, + ), + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'] + ), + json={'server': build_server}, + validate=dict( + json={ + 'server': { + 'flavorRef': 'flavor-id', + 'imageRef': 'image-id', + 'max_count': 1, + 'min_count': 1, + 'block_device_mapping_v2': [ + { + 'boot_index': 0, + 'delete_on_termination': True, + 'destination_type': 'local', + 'source_type': 'image', + 'uuid': 'image-id', + }, + { + 'boot_index': '-1', + 'delete_on_termination': False, + 'destination_type': 'volume', + 'source_type': 'volume', + 'uuid': 'volume001', + }, + ], + 'name': 'server-name', + 'networks': 'auto', + } + } + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'] + ), + json={'server': active_server}, + ), + ] + ) self.cloud.create_server( name='server-name', @@ -968,7 +1389,8 @@ def test_create_boot_attach_volume(self): flavor=dict(id='flavor-id'), boot_from_volume=False, volumes=[volume], - wait=False) + wait=False, + ) self.assert_calls() @@ -976,36 +1398,54 @@ def test_create_boot_from_volume_image_terminate(self): build_server = fakes.make_fake_server('1234', '', 'BUILD') active_server = fakes.make_fake_server('1234', '', 'BUILD') - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': []}), - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', append=['servers']), - json={'server': build_server}, - validate=dict( - json={'server': { - u'flavorRef': 'flavor-id', - u'imageRef': '', - u'max_count': 1, - u'min_count': 1, - u'block_device_mapping_v2': [{ - u'boot_index': u'0', - u'delete_on_termination': True, - u'destination_type': u'volume', - u'source_type': u'image', - u'uuid': u'image-id', - u'volume_size': u'1'}], - u'name': u'server-name', - 'networks': 'auto'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': active_server}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': []}, + ), + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'] + ), + json={'server': build_server}, + validate=dict( + json={ + 'server': { + 'flavorRef': 'flavor-id', + 'imageRef': '', + 'max_count': 1, + 'min_count': 1, + 'block_device_mapping_v2': [ + { + 'boot_index': '0', + 'delete_on_termination': True, + 'destination_type': 'volume', + 'source_type': 'image', + 'uuid': 'image-id', + 'volume_size': '1', + } + ], + 'name': 'server-name', + 'networks': 'auto', + } + } + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'] + ), + json={'server': active_server}, + ), + ] + ) self.cloud.create_server( name='server-name', @@ -1014,7 +1454,8 @@ def test_create_boot_from_volume_image_terminate(self): boot_from_volume=True, terminate_volume=True, volume_size=1, - wait=False) + wait=False, + ) self.assert_calls() @@ -1028,36 +1469,53 @@ def test_create_server_scheduler_hints(self): fake_server = fakes.make_fake_server('1234', '', 'BUILD') fake_server['scheduler_hints'] = scheduler_hints - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': []}), - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', append=['servers']), - json={'server': fake_server}, - validate=dict( - json={ - 'server': { - u'flavorRef': u'flavor-id', - u'imageRef': u'image-id', - u'max_count': 1, - u'min_count': 1, - u'name': u'server-name', - 'networks': 'auto'}, - u'OS-SCH-HNT:scheduler_hints': scheduler_hints, })), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': fake_server}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': []}, + ), + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'] + ), + json={'server': fake_server}, + validate=dict( + json={ + 'server': { + 'flavorRef': 'flavor-id', + 'imageRef': 'image-id', + 'max_count': 1, + 'min_count': 1, + 'name': 'server-name', + 'networks': 'auto', + }, + 'OS-SCH-HNT:scheduler_hints': scheduler_hints, + } + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'] + ), + json={'server': fake_server}, + ), + ] + ) self.cloud.create_server( - name='server-name', image=dict(id='image-id'), + name='server-name', + image=dict(id='image-id'), flavor=dict(id='flavor-id'), - scheduler_hints=scheduler_hints, wait=False) + scheduler_hints=scheduler_hints, + wait=False, + ) self.assert_calls() @@ -1070,7 +1528,8 @@ def test_create_server_scheduler_hints_group_merge(self): group_name = self.getUniqueString('server-group') policies = ['affinity'] fake_group = fakes.make_fake_server_group( - group_id, group_name, policies) + group_id, group_name, policies + ) # The scheduler hints we pass in scheduler_hints = { @@ -1086,42 +1545,61 @@ def test_create_server_scheduler_hints_group_merge(self): fake_server = fakes.make_fake_server('1234', '', 'BUILD') fake_server['scheduler_hints'] = scheduler_hints_merged - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['os-server-groups']), - json={'server_groups': [fake_group]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': []}), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', append=['servers']), - json={'server': fake_server}, - validate=dict( - json={ - 'server': { - u'flavorRef': u'flavor-id', - u'imageRef': u'image-id', - u'max_count': 1, - u'min_count': 1, - u'name': u'server-name', - 'networks': 'auto'}, - u'OS-SCH-HNT:scheduler_hints': scheduler_hints_merged, - })), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': fake_server}), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-server-groups'] + ), + json={'server_groups': [fake_group]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': []}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'] + ), + json={'server': fake_server}, + validate=dict( + json={ + 'server': { + 'flavorRef': 'flavor-id', + 'imageRef': 'image-id', + 'max_count': 1, + 'min_count': 1, + 'name': 'server-name', + 'networks': 'auto', + }, + 'OS-SCH-HNT:scheduler_hints': scheduler_hints_merged, # noqa: E501 + } + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'] + ), + json={'server': fake_server}, + ), + ] + ) self.cloud.create_server( - name='server-name', image=dict(id='image-id'), + name='server-name', + image=dict(id='image-id'), flavor=dict(id='flavor-id'), - scheduler_hints=dict(scheduler_hints), group=group_name, - wait=False) + scheduler_hints=dict(scheduler_hints), + group=group_name, + wait=False, + ) self.assert_calls() @@ -1135,7 +1613,8 @@ def test_create_server_scheduler_hints_group_override(self): group_name = self.getUniqueString('server-group') policies = ['affinity'] fake_group = fakes.make_fake_server_group( - group_id_param, group_name, policies) + group_id_param, group_name, policies + ) # The scheduler hints we pass in that are expected to be ignored in # POST call @@ -1151,41 +1630,60 @@ def test_create_server_scheduler_hints_group_override(self): fake_server = fakes.make_fake_server('1234', '', 'BUILD') fake_server['scheduler_hints'] = group_scheduler_hints - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['os-server-groups']), - json={'server_groups': [fake_group]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': []}), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', append=['servers']), - json={'server': fake_server}, - validate=dict( - json={ - 'server': { - u'flavorRef': u'flavor-id', - u'imageRef': u'image-id', - u'max_count': 1, - u'min_count': 1, - u'name': u'server-name', - 'networks': 'auto'}, - u'OS-SCH-HNT:scheduler_hints': group_scheduler_hints, - })), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - json={'server': fake_server}), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-server-groups'] + ), + json={'server_groups': [fake_group]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': []}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['servers'] + ), + json={'server': fake_server}, + validate=dict( + json={ + 'server': { + 'flavorRef': 'flavor-id', + 'imageRef': 'image-id', + 'max_count': 1, + 'min_count': 1, + 'name': 'server-name', + 'networks': 'auto', + }, + 'OS-SCH-HNT:scheduler_hints': group_scheduler_hints, # noqa: E501 + } + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'] + ), + json={'server': fake_server}, + ), + ] + ) self.cloud.create_server( - name='server-name', image=dict(id='image-id'), + name='server-name', + image=dict(id='image-id'), flavor=dict(id='flavor-id'), - scheduler_hints=dict(scheduler_hints), group=group_name, - wait=False) + scheduler_hints=dict(scheduler_hints), + group=group_name, + wait=False, + ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_create_volume_snapshot.py b/openstack/tests/unit/cloud/test_create_volume_snapshot.py index e77f18905..ed8421475 100644 --- a/openstack/tests/unit/cloud/test_create_volume_snapshot.py +++ b/openstack/tests/unit/cloud/test_create_volume_snapshot.py @@ -25,7 +25,6 @@ class TestCreateVolumeSnapshot(base.TestCase): - def setUp(self): super(TestCreateVolumeSnapshot, self).setUp() self.use_cinder() @@ -33,7 +32,8 @@ def setUp(self): def _compare_snapshots(self, exp, real): self.assertDictEqual( snapshot.Snapshot(**exp).to_dict(computed=False), - real.to_dict(computed=False)) + real.to_dict(computed=False), + ) def test_create_volume_snapshot_wait(self): """ @@ -42,32 +42,46 @@ def test_create_volume_snapshot_wait(self): """ snapshot_id = '5678' volume_id = '1234' - build_snapshot = fakes.FakeVolumeSnapshot(snapshot_id, 'creating', - 'foo', 'derpysnapshot') + build_snapshot = fakes.FakeVolumeSnapshot( + snapshot_id, 'creating', 'foo', 'derpysnapshot' + ) build_snapshot_dict = meta.obj_to_munch(build_snapshot) - fake_snapshot = fakes.FakeVolumeSnapshot(snapshot_id, 'available', - 'foo', 'derpysnapshot') + fake_snapshot = fakes.FakeVolumeSnapshot( + snapshot_id, 'available', 'foo', 'derpysnapshot' + ) fake_snapshot_dict = meta.obj_to_munch(fake_snapshot) - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'volumev3', 'public', append=['snapshots']), - json={'snapshot': build_snapshot_dict}, - validate=dict(json={ - 'snapshot': {'volume_id': '1234'}})), - dict(method='GET', - uri=self.get_mock_url('volumev3', 'public', - append=['snapshots', snapshot_id]), - json={'snapshot': build_snapshot_dict}), - dict(method='GET', - uri=self.get_mock_url('volumev3', 'public', - append=['snapshots', snapshot_id]), - json={'snapshot': fake_snapshot_dict})]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'volumev3', 'public', append=['snapshots'] + ), + json={'snapshot': build_snapshot_dict}, + validate=dict(json={'snapshot': {'volume_id': '1234'}}), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['snapshots', snapshot_id] + ), + json={'snapshot': build_snapshot_dict}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['snapshots', snapshot_id] + ), + json={'snapshot': fake_snapshot_dict}, + ), + ] + ) self._compare_snapshots( fake_snapshot_dict, - self.cloud.create_volume_snapshot(volume_id=volume_id, wait=True)) + self.cloud.create_volume_snapshot(volume_id=volume_id, wait=True), + ) self.assert_calls() def test_create_volume_snapshot_with_timeout(self): @@ -77,26 +91,38 @@ def test_create_volume_snapshot_with_timeout(self): """ snapshot_id = '5678' volume_id = '1234' - build_snapshot = fakes.FakeVolumeSnapshot(snapshot_id, 'creating', - 'foo', 'derpysnapshot') + build_snapshot = fakes.FakeVolumeSnapshot( + snapshot_id, 'creating', 'foo', 'derpysnapshot' + ) build_snapshot_dict = meta.obj_to_munch(build_snapshot) - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'volumev3', 'public', append=['snapshots']), - json={'snapshot': build_snapshot_dict}, - validate=dict(json={ - 'snapshot': {'volume_id': '1234'}})), - dict(method='GET', - uri=self.get_mock_url('volumev3', 'public', - append=['snapshots', snapshot_id]), - json={'snapshot': build_snapshot_dict})]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'volumev3', 'public', append=['snapshots'] + ), + json={'snapshot': build_snapshot_dict}, + validate=dict(json={'snapshot': {'volume_id': '1234'}}), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['snapshots', snapshot_id] + ), + json={'snapshot': build_snapshot_dict}, + ), + ] + ) self.assertRaises( exc.OpenStackCloudTimeout, - self.cloud.create_volume_snapshot, volume_id=volume_id, - wait=True, timeout=0.01) + self.cloud.create_volume_snapshot, + volume_id=volume_id, + wait=True, + timeout=0.01, + ) self.assert_calls(do_count=False) def test_create_volume_snapshot_with_error(self): @@ -106,31 +132,47 @@ def test_create_volume_snapshot_with_error(self): """ snapshot_id = '5678' volume_id = '1234' - build_snapshot = fakes.FakeVolumeSnapshot(snapshot_id, 'creating', - 'bar', 'derpysnapshot') + build_snapshot = fakes.FakeVolumeSnapshot( + snapshot_id, 'creating', 'bar', 'derpysnapshot' + ) build_snapshot_dict = meta.obj_to_munch(build_snapshot) - error_snapshot = fakes.FakeVolumeSnapshot(snapshot_id, 'error', - 'blah', 'derpysnapshot') + error_snapshot = fakes.FakeVolumeSnapshot( + snapshot_id, 'error', 'blah', 'derpysnapshot' + ) error_snapshot_dict = meta.obj_to_munch(error_snapshot) - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'volumev3', 'public', append=['snapshots']), - json={'snapshot': build_snapshot_dict}, - validate=dict(json={ - 'snapshot': {'volume_id': '1234'}})), - dict(method='GET', - uri=self.get_mock_url('volumev3', 'public', - append=['snapshots', snapshot_id]), - json={'snapshot': build_snapshot_dict}), - dict(method='GET', - uri=self.get_mock_url('volumev3', 'public', - append=['snapshots', snapshot_id]), - json={'snapshot': error_snapshot_dict})]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'volumev3', 'public', append=['snapshots'] + ), + json={'snapshot': build_snapshot_dict}, + validate=dict(json={'snapshot': {'volume_id': '1234'}}), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['snapshots', snapshot_id] + ), + json={'snapshot': build_snapshot_dict}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['snapshots', snapshot_id] + ), + json={'snapshot': error_snapshot_dict}, + ), + ] + ) self.assertRaises( exc.OpenStackCloudException, - self.cloud.create_volume_snapshot, volume_id=volume_id, - wait=True, timeout=5) + self.cloud.create_volume_snapshot, + volume_id=volume_id, + wait=True, + timeout=5, + ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_delete_server.py b/openstack/tests/unit/cloud/test_delete_server.py index 562e7ffbe..9a508cfab 100644 --- a/openstack/tests/unit/cloud/test_delete_server.py +++ b/openstack/tests/unit/cloud/test_delete_server.py @@ -24,27 +24,39 @@ class TestDeleteServer(base.TestCase): - def test_delete_server(self): """ Test that server delete is called when wait=False """ server = fakes.make_fake_server('1234', 'daffy', 'ACTIVE') - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'daffy']), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'], - qs_elements=['name=daffy']), - json={'servers': [server]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234'])), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'daffy'] + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', 'detail'], + qs_elements=['name=daffy'], + ), + json={'servers': [server]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'] + ), + ), + ] + ) self.assertTrue(self.cloud.delete_server('daffy', wait=False)) self.assert_calls() @@ -53,35 +65,55 @@ def test_delete_server_already_gone(self): """ Test that we return immediately when server is already gone """ - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'tweety']), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'], - qs_elements=['name=tweety']), - json={'servers': []}), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'tweety'] + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', 'detail'], + qs_elements=['name=tweety'], + ), + json={'servers': []}, + ), + ] + ) self.assertFalse(self.cloud.delete_server('tweety', wait=False)) self.assert_calls() def test_delete_server_already_gone_wait(self): - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'speedy']), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'], - qs_elements=['name=speedy']), - json={'servers': []}), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'speedy'] + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', 'detail'], + qs_elements=['name=speedy'], + ), + json={'servers': []}, + ), + ] + ) self.assertFalse(self.cloud.delete_server('speedy', wait=True)) self.assert_calls() @@ -90,29 +122,48 @@ def test_delete_server_wait_for_deleted(self): Test that delete_server waits for the server to be gone """ server = fakes.make_fake_server('9999', 'wily', 'ACTIVE') - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'wily']), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'], - qs_elements=['name=wily']), - json={'servers': [server]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '9999'])), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '9999']), - json={'server': server}), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '9999']), - status_code=404), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'wily'] + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', 'detail'], + qs_elements=['name=wily'], + ), + json={'servers': [server]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '9999'] + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '9999'] + ), + json={'server': server}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '9999'] + ), + status_code=404, + ), + ] + ) self.assertTrue(self.cloud.delete_server('wily', wait=True)) self.assert_calls() @@ -122,27 +173,42 @@ def test_delete_server_fails(self): Test that delete_server raises non-404 exceptions """ server = fakes.make_fake_server('1212', 'speedy', 'ACTIVE') - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'speedy']), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'], - qs_elements=['name=speedy']), - json={'servers': [server]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1212']), - status_code=400), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'speedy'] + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', 'detail'], + qs_elements=['name=speedy'], + ), + json={'servers': [server]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1212'] + ), + status_code=400, + ), + ] + ) self.assertRaises( shade_exc.OpenStackCloudException, - self.cloud.delete_server, 'speedy', - wait=False) + self.cloud.delete_server, + 'speedy', + wait=False, + ) self.assert_calls() @@ -156,24 +222,38 @@ def fake_has_service(service_type): if service_type == 'volume': return False return orig_has_service(service_type) + self.cloud.has_service = fake_has_service server = fakes.make_fake_server('1234', 'porky', 'ACTIVE') - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'porky']), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'], - qs_elements=['name=porky']), - json={'servers': [server]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234'])), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'porky'] + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', 'detail'], + qs_elements=['name=porky'], + ), + json={'servers': [server]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'] + ), + ), + ] + ) self.assertTrue(self.cloud.delete_server('porky', wait=False)) self.assert_calls() @@ -185,50 +265,84 @@ def test_delete_server_delete_ips(self): server = fakes.make_fake_server('1234', 'porky', 'ACTIVE') fip_id = uuid.uuid4().hex - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'porky']), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'], - qs_elements=['name=porky']), - json={'servers': [server]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips'], - qs_elements=['floating_ip_address=172.24.5.5']), - complete_qs=True, - json={'floatingips': [{ - 'router_id': 'd23abc8d-2991-4a55-ba98-2aaea84cc72f', - 'tenant_id': '4969c491a3c74ee4af974e6d800c62de', - 'floating_network_id': '376da547-b977-4cfe-9cba7', - 'fixed_ip_address': '10.0.0.4', - 'floating_ip_address': '172.24.5.5', - 'port_id': 'ce705c24-c1ef-408a-bda3-7bbd946164ac', - 'id': fip_id, - 'status': 'ACTIVE'}]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'floatingips', fip_id])), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips']), - complete_qs=True, - json={'floatingips': []}), - dict(method='DELETE', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234'])), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - status_code=404), - ]) - self.assertTrue(self.cloud.delete_server( - 'porky', wait=True, delete_ips=True)) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'porky'] + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', 'detail'], + qs_elements=['name=porky'], + ), + json={'servers': [server]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'floatingips'], + qs_elements=['floating_ip_address=172.24.5.5'], + ), + complete_qs=True, + json={ + 'floatingips': [ + { + 'router_id': 'd23abc8d-2991-4a55-ba98-2aaea84cc72f', # noqa: E501 + 'tenant_id': '4969c491a3c74ee4af974e6d800c62de', # noqa: E501 + 'floating_network_id': '376da547-b977-4cfe-9cba7', # noqa: E501 + 'fixed_ip_address': '10.0.0.4', + 'floating_ip_address': '172.24.5.5', + 'port_id': 'ce705c24-c1ef-408a-bda3-7bbd946164ac', # noqa: E501 + 'id': fip_id, + 'status': 'ACTIVE', + } + ] + }, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'floatingips', fip_id], + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips'] + ), + complete_qs=True, + json={'floatingips': []}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'] + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'] + ), + status_code=404, + ), + ] + ) + self.assertTrue( + self.cloud.delete_server('porky', wait=True, delete_ips=True) + ) self.assert_calls() @@ -238,33 +352,55 @@ def test_delete_server_delete_ips_bad_neutron(self): """ server = fakes.make_fake_server('1234', 'porky', 'ACTIVE') - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'porky']), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'], - qs_elements=['name=porky']), - json={'servers': [server]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips'], - qs_elements=['floating_ip_address=172.24.5.5']), - complete_qs=True, - status_code=404), - dict(method='DELETE', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234'])), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - status_code=404), - ]) - self.assertTrue(self.cloud.delete_server( - 'porky', wait=True, delete_ips=True)) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'porky'] + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', 'detail'], + qs_elements=['name=porky'], + ), + json={'servers': [server]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'floatingips'], + qs_elements=['floating_ip_address=172.24.5.5'], + ), + complete_qs=True, + status_code=404, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'] + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'] + ), + status_code=404, + ), + ] + ) + self.assertTrue( + self.cloud.delete_server('porky', wait=True, delete_ips=True) + ) self.assert_calls() @@ -275,44 +411,73 @@ def test_delete_server_delete_fips_nova(self): self.cloud._floating_ip_source = 'nova' server = fakes.make_fake_server('1234', 'porky', 'ACTIVE') - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'porky']), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'], - qs_elements=['name=porky']), - json={'servers': [server]}), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['os-floating-ips']), - json={'floating_ips': [ - { - 'fixed_ip': None, - 'id': 1, - 'instance_id': None, - 'ip': '172.24.5.5', - 'pool': 'nova' - }]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'compute', 'public', append=['os-floating-ips', '1'])), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['os-floating-ips']), - json={'floating_ips': []}), - dict(method='DELETE', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234'])), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', '1234']), - status_code=404), - ]) - self.assertTrue(self.cloud.delete_server( - 'porky', wait=True, delete_ips=True)) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'porky'] + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', 'detail'], + qs_elements=['name=porky'], + ), + json={'servers': [server]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-floating-ips'] + ), + json={ + 'floating_ips': [ + { + 'fixed_ip': None, + 'id': 1, + 'instance_id': None, + 'ip': '172.24.5.5', + 'pool': 'nova', + } + ] + }, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', append=['os-floating-ips', '1'] + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-floating-ips'] + ), + json={'floating_ips': []}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'] + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', '1234'] + ), + status_code=404, + ), + ] + ) + self.assertTrue( + self.cloud.delete_server('porky', wait=True, delete_ips=True) + ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_delete_volume_snapshot.py b/openstack/tests/unit/cloud/test_delete_volume_snapshot.py index 1b607d21c..c70f938ee 100644 --- a/openstack/tests/unit/cloud/test_delete_volume_snapshot.py +++ b/openstack/tests/unit/cloud/test_delete_volume_snapshot.py @@ -24,7 +24,6 @@ class TestDeleteVolumeSnapshot(base.TestCase): - def setUp(self): super(TestDeleteVolumeSnapshot, self).setUp() self.use_cinder() @@ -34,23 +33,34 @@ def test_delete_volume_snapshot(self): Test that delete_volume_snapshot without a wait returns True instance when the volume snapshot deletes. """ - fake_snapshot = fakes.FakeVolumeSnapshot('1234', 'available', - 'foo', 'derpysnapshot') + fake_snapshot = fakes.FakeVolumeSnapshot( + '1234', 'available', 'foo', 'derpysnapshot' + ) fake_snapshot_dict = meta.obj_to_munch(fake_snapshot) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['snapshots', 'detail']), - json={'snapshots': [fake_snapshot_dict]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['snapshots', fake_snapshot_dict['id']]))]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['snapshots', 'detail'] + ), + json={'snapshots': [fake_snapshot_dict]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'volumev3', + 'public', + append=['snapshots', fake_snapshot_dict['id']], + ), + ), + ] + ) self.assertTrue( - self.cloud.delete_volume_snapshot(name_or_id='1234', wait=False)) + self.cloud.delete_volume_snapshot(name_or_id='1234', wait=False) + ) self.assert_calls() def test_delete_volume_snapshot_with_error(self): @@ -58,24 +68,36 @@ def test_delete_volume_snapshot_with_error(self): Test that a exception while deleting a volume snapshot will cause an OpenStackCloudException. """ - fake_snapshot = fakes.FakeVolumeSnapshot('1234', 'available', - 'foo', 'derpysnapshot') + fake_snapshot = fakes.FakeVolumeSnapshot( + '1234', 'available', 'foo', 'derpysnapshot' + ) fake_snapshot_dict = meta.obj_to_munch(fake_snapshot) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['snapshots', 'detail']), - json={'snapshots': [fake_snapshot_dict]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['snapshots', fake_snapshot_dict['id']]), - status_code=404)]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['snapshots', 'detail'] + ), + json={'snapshots': [fake_snapshot_dict]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'volumev3', + 'public', + append=['snapshots', fake_snapshot_dict['id']], + ), + status_code=404, + ), + ] + ) self.assertRaises( exc.OpenStackCloudException, - self.cloud.delete_volume_snapshot, name_or_id='1234') + self.cloud.delete_volume_snapshot, + name_or_id='1234', + ) self.assert_calls() def test_delete_volume_snapshot_with_timeout(self): @@ -83,29 +105,43 @@ def test_delete_volume_snapshot_with_timeout(self): Test that a timeout while waiting for the volume snapshot to delete raises an exception in delete_volume_snapshot. """ - fake_snapshot = fakes.FakeVolumeSnapshot('1234', 'available', - 'foo', 'derpysnapshot') + fake_snapshot = fakes.FakeVolumeSnapshot( + '1234', 'available', 'foo', 'derpysnapshot' + ) fake_snapshot_dict = meta.obj_to_munch(fake_snapshot) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['snapshots', 'detail']), - json={'snapshots': [fake_snapshot_dict]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['snapshots', fake_snapshot_dict['id']])), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['snapshots', '1234']), - json={'snapshot': fake_snapshot_dict}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['snapshots', 'detail'] + ), + json={'snapshots': [fake_snapshot_dict]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'volumev3', + 'public', + append=['snapshots', fake_snapshot_dict['id']], + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['snapshots', '1234'] + ), + json={'snapshot': fake_snapshot_dict}, + ), + ] + ) self.assertRaises( exc.OpenStackCloudTimeout, - self.cloud.delete_volume_snapshot, name_or_id='1234', - wait=True, timeout=0.01) + self.cloud.delete_volume_snapshot, + name_or_id='1234', + wait=True, + timeout=0.01, + ) self.assert_calls(do_count=False) diff --git a/openstack/tests/unit/cloud/test_domain_params.py b/openstack/tests/unit/cloud/test_domain_params.py index 4f5e1ab6d..fd52e89b7 100644 --- a/openstack/tests/unit/cloud/test_domain_params.py +++ b/openstack/tests/unit/cloud/test_domain_params.py @@ -15,17 +15,23 @@ class TestDomainParams(base.TestCase): - def test_identity_params_v3(self): project_data = self._get_project_data(v3=True) - self.register_uris([ - dict(method='GET', - uri='https://identity.example.com/v3/projects', - json=dict(projects=[project_data.json_response['project']])) - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://identity.example.com/v3/projects', + json=dict( + projects=[project_data.json_response['project']] + ), + ) + ] + ) ret = self.cloud._get_identity_params( - domain_id='5678', project=project_data.project_name) + domain_id='5678', project=project_data.project_name + ) self.assertIn('default_project_id', ret) self.assertEqual(ret['default_project_id'], project_data.project_id) self.assertIn('domain_id', ret) @@ -39,6 +45,8 @@ def test_identity_params_v3_no_domain(self): self.assertRaises( exc.OpenStackCloudException, self.cloud._get_identity_params, - domain_id=None, project=project_data.project_name) + domain_id=None, + project=project_data.project_name, + ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_domains.py b/openstack/tests/unit/cloud/test_domains.py index 29392e492..824377975 100644 --- a/openstack/tests/unit/cloud/test_domains.py +++ b/openstack/tests/unit/cloud/test_domains.py @@ -23,36 +23,54 @@ class TestDomains(base.TestCase): - - def get_mock_url(self, service_type='identity', - resource='domains', - append=None, base_url_append='v3', - qs_elements=None): + def get_mock_url( + self, + service_type='identity', + resource='domains', + append=None, + base_url_append='v3', + qs_elements=None, + ): return super(TestDomains, self).get_mock_url( - service_type=service_type, resource=resource, - append=append, base_url_append=base_url_append, - qs_elements=qs_elements) + service_type=service_type, + resource=resource, + append=append, + base_url_append=base_url_append, + qs_elements=qs_elements, + ) def test_list_domains(self): domain_data = self._get_domain_data() - self.register_uris([ - dict(method='GET', uri=self.get_mock_url(), status_code=200, - json={'domains': [domain_data.json_response['domain']]})]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'domains': [domain_data.json_response['domain']]}, + ) + ] + ) domains = self.cloud.list_domains() self.assertThat(len(domains), matchers.Equals(1)) - self.assertThat(domains[0].name, - matchers.Equals(domain_data.domain_name)) - self.assertThat(domains[0].id, - matchers.Equals(domain_data.domain_id)) + self.assertThat( + domains[0].name, matchers.Equals(domain_data.domain_name) + ) + self.assertThat(domains[0].id, matchers.Equals(domain_data.domain_id)) self.assert_calls() def test_get_domain(self): domain_data = self._get_domain_data() - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(append=[domain_data.domain_id]), - status_code=200, - json=domain_data.json_response)]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(append=[domain_data.domain_id]), + status_code=200, + json=domain_data.json_response, + ) + ] + ) domain = self.cloud.get_domain(domain_id=domain_data.domain_id) self.assertThat(domain.id, matchers.Equals(domain_data.domain_id)) self.assertThat(domain.name, matchers.Equals(domain_data.domain_name)) @@ -61,57 +79,86 @@ def test_get_domain(self): def test_get_domain_with_name_or_id(self): domain_data = self._get_domain_data() response = {'domains': [domain_data.json_response['domain']]} - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(append=[domain_data.domain_id]), - status_code=200, - json=domain_data.json_response), - dict(method='GET', - uri=self.get_mock_url(append=[domain_data.domain_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - qs_elements=['name=' + domain_data.domain_name] - ), - status_code=200, - json=response), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(append=[domain_data.domain_id]), + status_code=200, + json=domain_data.json_response, + ), + dict( + method='GET', + uri=self.get_mock_url(append=[domain_data.domain_name]), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + qs_elements=['name=' + domain_data.domain_name] + ), + status_code=200, + json=response, + ), + ] + ) domain = self.cloud.get_domain(name_or_id=domain_data.domain_id) domain_by_name = self.cloud.get_domain( - name_or_id=domain_data.domain_name) + name_or_id=domain_data.domain_name + ) self.assertThat(domain.id, matchers.Equals(domain_data.domain_id)) self.assertThat(domain.name, matchers.Equals(domain_data.domain_name)) - self.assertThat(domain_by_name.id, - matchers.Equals(domain_data.domain_id)) - self.assertThat(domain_by_name.name, - matchers.Equals(domain_data.domain_name)) + self.assertThat( + domain_by_name.id, matchers.Equals(domain_data.domain_id) + ) + self.assertThat( + domain_by_name.name, matchers.Equals(domain_data.domain_name) + ) self.assert_calls() def test_create_domain(self): - domain_data = self._get_domain_data(description=uuid.uuid4().hex, - enabled=True) - self.register_uris([ - dict(method='POST', uri=self.get_mock_url(), status_code=200, - json=domain_data.json_response, - validate=dict(json=domain_data.json_request))]) + domain_data = self._get_domain_data( + description=uuid.uuid4().hex, enabled=True + ) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url(), + status_code=200, + json=domain_data.json_response, + validate=dict(json=domain_data.json_request), + ) + ] + ) domain = self.cloud.create_domain( - domain_data.domain_name, domain_data.description) + domain_data.domain_name, domain_data.description + ) self.assertThat(domain.id, matchers.Equals(domain_data.domain_id)) self.assertThat(domain.name, matchers.Equals(domain_data.domain_name)) self.assertThat( - domain.description, matchers.Equals(domain_data.description)) + domain.description, matchers.Equals(domain_data.description) + ) self.assert_calls() def test_create_domain_exception(self): - domain_data = self._get_domain_data(domain_name='domain_name', - enabled=True) + domain_data = self._get_domain_data( + domain_name='domain_name', enabled=True + ) with testtools.ExpectedException( openstack.cloud.OpenStackCloudBadRequest ): - self.register_uris([ - dict(method='POST', uri=self.get_mock_url(), status_code=400, - json=domain_data.json_response, - validate=dict(json=domain_data.json_request))]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url(), + status_code=400, + json=domain_data.json_response, + validate=dict(json=domain_data.json_request), + ) + ] + ) self.cloud.create_domain('domain_name') self.assert_calls() @@ -120,11 +167,20 @@ def test_delete_domain(self): new_resp = domain_data.json_response.copy() new_resp['domain']['enabled'] = False domain_resource_uri = self.get_mock_url(append=[domain_data.domain_id]) - self.register_uris([ - dict(method='PATCH', uri=domain_resource_uri, status_code=200, - json=new_resp, - validate=dict(json={'domain': {'enabled': False}})), - dict(method='DELETE', uri=domain_resource_uri, status_code=204)]) + self.register_uris( + [ + dict( + method='PATCH', + uri=domain_resource_uri, + status_code=200, + json=new_resp, + validate=dict(json={'domain': {'enabled': False}}), + ), + dict( + method='DELETE', uri=domain_resource_uri, status_code=204 + ), + ] + ) self.cloud.delete_domain(domain_data.domain_id) self.assert_calls() @@ -134,15 +190,26 @@ def test_delete_domain_name_or_id(self): new_resp['domain']['enabled'] = False domain_resource_uri = self.get_mock_url(append=[domain_data.domain_id]) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(append=[domain_data.domain_id]), - status_code=200, - json={'domain': domain_data.json_response['domain']}), - dict(method='PATCH', uri=domain_resource_uri, status_code=200, - json=new_resp, - validate=dict(json={'domain': {'enabled': False}})), - dict(method='DELETE', uri=domain_resource_uri, status_code=204)]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(append=[domain_data.domain_id]), + status_code=200, + json={'domain': domain_data.json_response['domain']}, + ), + dict( + method='PATCH', + uri=domain_resource_uri, + status_code=200, + json=new_resp, + validate=dict(json={'domain': {'enabled': False}}), + ), + dict( + method='DELETE', uri=domain_resource_uri, status_code=204 + ), + ] + ) self.cloud.delete_domain(name_or_id=domain_data.domain_id) self.assert_calls() @@ -156,11 +223,20 @@ def test_delete_domain_exception(self): new_resp = domain_data.json_response.copy() new_resp['domain']['enabled'] = False domain_resource_uri = self.get_mock_url(append=[domain_data.domain_id]) - self.register_uris([ - dict(method='PATCH', uri=domain_resource_uri, status_code=200, - json=new_resp, - validate=dict(json={'domain': {'enabled': False}})), - dict(method='DELETE', uri=domain_resource_uri, status_code=404)]) + self.register_uris( + [ + dict( + method='PATCH', + uri=domain_resource_uri, + status_code=200, + json=new_resp, + validate=dict(json={'domain': {'enabled': False}}), + ), + dict( + method='DELETE', uri=domain_resource_uri, status_code=404 + ), + ] + ) with testtools.ExpectedException( openstack.exceptions.ResourceNotFound ): @@ -169,53 +245,81 @@ def test_delete_domain_exception(self): def test_update_domain(self): domain_data = self._get_domain_data( - description=self.getUniqueString('domainDesc')) + description=self.getUniqueString('domainDesc') + ) domain_resource_uri = self.get_mock_url(append=[domain_data.domain_id]) - self.register_uris([ - dict(method='PATCH', uri=domain_resource_uri, status_code=200, - json=domain_data.json_response, - validate=dict(json=domain_data.json_request))]) + self.register_uris( + [ + dict( + method='PATCH', + uri=domain_resource_uri, + status_code=200, + json=domain_data.json_response, + validate=dict(json=domain_data.json_request), + ) + ] + ) domain = self.cloud.update_domain( domain_data.domain_id, name=domain_data.domain_name, - description=domain_data.description) + description=domain_data.description, + ) self.assertThat(domain.id, matchers.Equals(domain_data.domain_id)) self.assertThat(domain.name, matchers.Equals(domain_data.domain_name)) self.assertThat( - domain.description, matchers.Equals(domain_data.description)) + domain.description, matchers.Equals(domain_data.description) + ) self.assert_calls() def test_update_domain_name_or_id(self): domain_data = self._get_domain_data( - description=self.getUniqueString('domainDesc')) + description=self.getUniqueString('domainDesc') + ) domain_resource_uri = self.get_mock_url(append=[domain_data.domain_id]) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(append=[domain_data.domain_id]), - status_code=200, - json={'domain': domain_data.json_response['domain']}), - dict(method='PATCH', uri=domain_resource_uri, status_code=200, - json=domain_data.json_response, - validate=dict(json=domain_data.json_request))]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(append=[domain_data.domain_id]), + status_code=200, + json={'domain': domain_data.json_response['domain']}, + ), + dict( + method='PATCH', + uri=domain_resource_uri, + status_code=200, + json=domain_data.json_response, + validate=dict(json=domain_data.json_request), + ), + ] + ) domain = self.cloud.update_domain( name_or_id=domain_data.domain_id, name=domain_data.domain_name, - description=domain_data.description) + description=domain_data.description, + ) self.assertThat(domain.id, matchers.Equals(domain_data.domain_id)) self.assertThat(domain.name, matchers.Equals(domain_data.domain_name)) self.assertThat( - domain.description, matchers.Equals(domain_data.description)) + domain.description, matchers.Equals(domain_data.description) + ) self.assert_calls() def test_update_domain_exception(self): domain_data = self._get_domain_data( - description=self.getUniqueString('domainDesc')) - self.register_uris([ - dict(method='PATCH', - uri=self.get_mock_url(append=[domain_data.domain_id]), - status_code=409, - json=domain_data.json_response, - validate=dict(json={'domain': {'enabled': False}}))]) + description=self.getUniqueString('domainDesc') + ) + self.register_uris( + [ + dict( + method='PATCH', + uri=self.get_mock_url(append=[domain_data.domain_id]), + status_code=409, + json=domain_data.json_response, + validate=dict(json={'domain': {'enabled': False}}), + ) + ] + ) with testtools.ExpectedException( openstack.exceptions.ConflictException ): diff --git a/openstack/tests/unit/cloud/test_endpoints.py b/openstack/tests/unit/cloud/test_endpoints.py index 476da65a9..6077464b5 100644 --- a/openstack/tests/unit/cloud/test_endpoints.py +++ b/openstack/tests/unit/cloud/test_endpoints.py @@ -27,11 +27,17 @@ class TestCloudEndpoints(base.TestCase): - - def get_mock_url(self, service_type='identity', interface='public', - resource='endpoints', append=None, base_url_append='v3'): + def get_mock_url( + self, + service_type='identity', + interface='public', + resource='endpoints', + append=None, + base_url_append='v3', + ): return super(TestCloudEndpoints, self).get_mock_url( - service_type, interface, resource, append, base_url_append) + service_type, interface, resource, append, base_url_append + ) def _dummy_url(self): return 'https://%s.example.com/' % uuid.uuid4().hex @@ -39,148 +45,207 @@ def _dummy_url(self): def test_create_endpoint_v3(self): service_data = self._get_service_data() public_endpoint_data = self._get_endpoint_v3_data( - service_id=service_data.service_id, interface='public', - url=self._dummy_url()) + service_id=service_data.service_id, + interface='public', + url=self._dummy_url(), + ) public_endpoint_data_disabled = self._get_endpoint_v3_data( - service_id=service_data.service_id, interface='public', - url=self._dummy_url(), enabled=False) + service_id=service_data.service_id, + interface='public', + url=self._dummy_url(), + enabled=False, + ) admin_endpoint_data = self._get_endpoint_v3_data( - service_id=service_data.service_id, interface='admin', - url=self._dummy_url(), region=public_endpoint_data.region_id) + service_id=service_data.service_id, + interface='admin', + url=self._dummy_url(), + region=public_endpoint_data.region_id, + ) internal_endpoint_data = self._get_endpoint_v3_data( - service_id=service_data.service_id, interface='internal', - url=self._dummy_url(), region=public_endpoint_data.region_id) + service_id=service_data.service_id, + interface='internal', + url=self._dummy_url(), + region=public_endpoint_data.region_id, + ) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(resource='services'), - status_code=200, - json={'services': [ - service_data.json_response_v3['service']]}), - dict(method='POST', - uri=self.get_mock_url(), - status_code=200, - json=public_endpoint_data_disabled.json_response, - validate=dict( - json=public_endpoint_data_disabled.json_request)), - dict(method='GET', - uri=self.get_mock_url(resource='services'), - status_code=200, - json={'services': [ - service_data.json_response_v3['service']]}), - dict(method='POST', - uri=self.get_mock_url(), - status_code=200, - json=public_endpoint_data.json_response, - validate=dict(json=public_endpoint_data.json_request)), - dict(method='POST', - uri=self.get_mock_url(), - status_code=200, - json=internal_endpoint_data.json_response, - validate=dict(json=internal_endpoint_data.json_request)), - dict(method='POST', - uri=self.get_mock_url(), - status_code=200, - json=admin_endpoint_data.json_response, - validate=dict(json=admin_endpoint_data.json_request)), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(resource='services'), + status_code=200, + json={ + 'services': [service_data.json_response_v3['service']] + }, + ), + dict( + method='POST', + uri=self.get_mock_url(), + status_code=200, + json=public_endpoint_data_disabled.json_response, + validate=dict( + json=public_endpoint_data_disabled.json_request + ), + ), + dict( + method='GET', + uri=self.get_mock_url(resource='services'), + status_code=200, + json={ + 'services': [service_data.json_response_v3['service']] + }, + ), + dict( + method='POST', + uri=self.get_mock_url(), + status_code=200, + json=public_endpoint_data.json_response, + validate=dict(json=public_endpoint_data.json_request), + ), + dict( + method='POST', + uri=self.get_mock_url(), + status_code=200, + json=internal_endpoint_data.json_response, + validate=dict(json=internal_endpoint_data.json_request), + ), + dict( + method='POST', + uri=self.get_mock_url(), + status_code=200, + json=admin_endpoint_data.json_response, + validate=dict(json=admin_endpoint_data.json_request), + ), + ] + ) endpoints = self.cloud.create_endpoint( service_name_or_id=service_data.service_id, region=public_endpoint_data_disabled.region_id, url=public_endpoint_data_disabled.url, interface=public_endpoint_data_disabled.interface, - enabled=False) + enabled=False, + ) # Test endpoint values self.assertThat( endpoints[0].id, - matchers.Equals(public_endpoint_data_disabled.endpoint_id)) - self.assertThat(endpoints[0].url, - matchers.Equals(public_endpoint_data_disabled.url)) + matchers.Equals(public_endpoint_data_disabled.endpoint_id), + ) + self.assertThat( + endpoints[0].url, + matchers.Equals(public_endpoint_data_disabled.url), + ) self.assertThat( endpoints[0].interface, - matchers.Equals(public_endpoint_data_disabled.interface)) + matchers.Equals(public_endpoint_data_disabled.interface), + ) self.assertThat( endpoints[0].region_id, - matchers.Equals(public_endpoint_data_disabled.region_id)) + matchers.Equals(public_endpoint_data_disabled.region_id), + ) self.assertThat( endpoints[0].region_id, - matchers.Equals(public_endpoint_data_disabled.region_id)) - self.assertThat(endpoints[0].is_enabled, - matchers.Equals(public_endpoint_data_disabled.enabled)) + matchers.Equals(public_endpoint_data_disabled.region_id), + ) + self.assertThat( + endpoints[0].is_enabled, + matchers.Equals(public_endpoint_data_disabled.enabled), + ) endpoints_2on3 = self.cloud.create_endpoint( service_name_or_id=service_data.service_id, region=public_endpoint_data.region_id, public_url=public_endpoint_data.url, internal_url=internal_endpoint_data.url, - admin_url=admin_endpoint_data.url) + admin_url=admin_endpoint_data.url, + ) # Three endpoints should be returned, public, internal, and admin self.assertThat(len(endpoints_2on3), matchers.Equals(3)) # test keys and values are correct for each endpoint created for result, reference in zip( - endpoints_2on3, [public_endpoint_data, - internal_endpoint_data, - admin_endpoint_data] + endpoints_2on3, + [ + public_endpoint_data, + internal_endpoint_data, + admin_endpoint_data, + ], ): self.assertThat(result.id, matchers.Equals(reference.endpoint_id)) self.assertThat(result.url, matchers.Equals(reference.url)) - self.assertThat(result.interface, - matchers.Equals(reference.interface)) - self.assertThat(result.region_id, - matchers.Equals(reference.region_id)) - self.assertThat(result.is_enabled, - matchers.Equals(reference.enabled)) + self.assertThat( + result.interface, matchers.Equals(reference.interface) + ) + self.assertThat( + result.region_id, matchers.Equals(reference.region_id) + ) + self.assertThat( + result.is_enabled, matchers.Equals(reference.enabled) + ) self.assert_calls() def test_update_endpoint_v3(self): service_data = self._get_service_data() dummy_url = self._dummy_url() endpoint_data = self._get_endpoint_v3_data( - service_id=service_data.service_id, interface='admin', - enabled=False) + service_id=service_data.service_id, + interface='admin', + enabled=False, + ) reference_request = endpoint_data.json_request.copy() reference_request['endpoint']['url'] = dummy_url - self.register_uris([ - dict(method='PATCH', - uri=self.get_mock_url(append=[endpoint_data.endpoint_id]), - status_code=200, - json=endpoint_data.json_response, - validate=dict(json=reference_request)) - ]) + self.register_uris( + [ + dict( + method='PATCH', + uri=self.get_mock_url(append=[endpoint_data.endpoint_id]), + status_code=200, + json=endpoint_data.json_response, + validate=dict(json=reference_request), + ) + ] + ) endpoint = self.cloud.update_endpoint( endpoint_data.endpoint_id, service_name_or_id=service_data.service_id, region=endpoint_data.region_id, url=dummy_url, interface=endpoint_data.interface, - enabled=False + enabled=False, ) # test keys and values are correct - self.assertThat(endpoint.id, - matchers.Equals(endpoint_data.endpoint_id)) - self.assertThat(endpoint.service_id, - matchers.Equals(service_data.service_id)) - self.assertThat(endpoint.url, - matchers.Equals(endpoint_data.url)) - self.assertThat(endpoint.interface, - matchers.Equals(endpoint_data.interface)) + self.assertThat( + endpoint.id, matchers.Equals(endpoint_data.endpoint_id) + ) + self.assertThat( + endpoint.service_id, matchers.Equals(service_data.service_id) + ) + self.assertThat(endpoint.url, matchers.Equals(endpoint_data.url)) + self.assertThat( + endpoint.interface, matchers.Equals(endpoint_data.interface) + ) self.assert_calls() def test_list_endpoints(self): endpoints_data = [self._get_endpoint_v3_data() for e in range(1, 10)] - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(), - status_code=200, - json={'endpoints': [e.json_response['endpoint'] - for e in endpoints_data]}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(), + status_code=200, + json={ + 'endpoints': [ + e.json_response['endpoint'] for e in endpoints_data + ] + }, + ) + ] + ) endpoints = self.cloud.list_endpoints() # test we are getting exactly len(self.mock_endpoints) elements @@ -188,58 +253,89 @@ def test_list_endpoints(self): # test keys and values are correct for i, ep in enumerate(endpoints_data): - self.assertThat(endpoints[i].id, - matchers.Equals(ep.endpoint_id)) - self.assertThat(endpoints[i].service_id, - matchers.Equals(ep.service_id)) - self.assertThat(endpoints[i].url, - matchers.Equals(ep.url)) - self.assertThat(endpoints[i].interface, - matchers.Equals(ep.interface)) + self.assertThat(endpoints[i].id, matchers.Equals(ep.endpoint_id)) + self.assertThat( + endpoints[i].service_id, matchers.Equals(ep.service_id) + ) + self.assertThat(endpoints[i].url, matchers.Equals(ep.url)) + self.assertThat( + endpoints[i].interface, matchers.Equals(ep.interface) + ) self.assert_calls() def test_search_endpoints(self): - endpoints_data = [self._get_endpoint_v3_data(region='region1') - for e in range(0, 2)] - endpoints_data.extend([self._get_endpoint_v3_data() - for e in range(1, 8)]) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(), - status_code=200, - json={'endpoints': [e.json_response['endpoint'] - for e in endpoints_data]}), - dict(method='GET', - uri=self.get_mock_url(), - status_code=200, - json={'endpoints': [e.json_response['endpoint'] - for e in endpoints_data]}), - dict(method='GET', - uri=self.get_mock_url(), - status_code=200, - json={'endpoints': [e.json_response['endpoint'] - for e in endpoints_data]}), - dict(method='GET', - uri=self.get_mock_url(), - status_code=200, - json={'endpoints': [e.json_response['endpoint'] - for e in endpoints_data]}) - ]) + endpoints_data = [ + self._get_endpoint_v3_data(region='region1') for e in range(0, 2) + ] + endpoints_data.extend( + [self._get_endpoint_v3_data() for e in range(1, 8)] + ) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(), + status_code=200, + json={ + 'endpoints': [ + e.json_response['endpoint'] for e in endpoints_data + ] + }, + ), + dict( + method='GET', + uri=self.get_mock_url(), + status_code=200, + json={ + 'endpoints': [ + e.json_response['endpoint'] for e in endpoints_data + ] + }, + ), + dict( + method='GET', + uri=self.get_mock_url(), + status_code=200, + json={ + 'endpoints': [ + e.json_response['endpoint'] for e in endpoints_data + ] + }, + ), + dict( + method='GET', + uri=self.get_mock_url(), + status_code=200, + json={ + 'endpoints': [ + e.json_response['endpoint'] for e in endpoints_data + ] + }, + ), + ] + ) # Search by id endpoints = self.cloud.search_endpoints( - id=endpoints_data[-1].endpoint_id) + id=endpoints_data[-1].endpoint_id + ) # # test we are getting exactly 1 element self.assertEqual(1, len(endpoints)) - self.assertThat(endpoints[0].id, - matchers.Equals(endpoints_data[-1].endpoint_id)) - self.assertThat(endpoints[0].service_id, - matchers.Equals(endpoints_data[-1].service_id)) - self.assertThat(endpoints[0].url, - matchers.Equals(endpoints_data[-1].url)) - self.assertThat(endpoints[0].interface, - matchers.Equals(endpoints_data[-1].interface)) + self.assertThat( + endpoints[0].id, matchers.Equals(endpoints_data[-1].endpoint_id) + ) + self.assertThat( + endpoints[0].service_id, + matchers.Equals(endpoints_data[-1].service_id), + ) + self.assertThat( + endpoints[0].url, matchers.Equals(endpoints_data[-1].url) + ) + self.assertThat( + endpoints[0].interface, + matchers.Equals(endpoints_data[-1].interface), + ) # Not found endpoints = self.cloud.search_endpoints(id='!invalid!') @@ -247,13 +343,15 @@ def test_search_endpoints(self): # Multiple matches endpoints = self.cloud.search_endpoints( - filters={'region_id': 'region1'}) + filters={'region_id': 'region1'} + ) # # test we are getting exactly 2 elements self.assertEqual(2, len(endpoints)) # test we are getting the correct response for region/region_id compat endpoints = self.cloud.search_endpoints( - filters={'region_id': 'region1'}) + filters={'region_id': 'region1'} + ) # # test we are getting exactly 2 elements, this is v3 self.assertEqual(2, len(endpoints)) @@ -261,16 +359,23 @@ def test_search_endpoints(self): def test_delete_endpoint(self): endpoint_data = self._get_endpoint_v3_data() - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(), - status_code=200, - json={'endpoints': [ - endpoint_data.json_response['endpoint']]}), - dict(method='DELETE', - uri=self.get_mock_url(append=[endpoint_data.endpoint_id]), - status_code=204) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(), + status_code=200, + json={ + 'endpoints': [endpoint_data.json_response['endpoint']] + }, + ), + dict( + method='DELETE', + uri=self.get_mock_url(append=[endpoint_data.endpoint_id]), + status_code=204, + ), + ] + ) # Delete by id self.cloud.delete_endpoint(id=endpoint_data.endpoint_id) diff --git a/openstack/tests/unit/cloud/test_flavors.py b/openstack/tests/unit/cloud/test_flavors.py index 9ca45f4ad..98e5c4efd 100644 --- a/openstack/tests/unit/cloud/test_flavors.py +++ b/openstack/tests/unit/cloud/test_flavors.py @@ -17,7 +17,6 @@ class TestFlavors(base.TestCase): - def setUp(self): super(TestFlavors, self).setUp() # self.use_compute_discovery() @@ -25,55 +24,85 @@ def setUp(self): def test_create_flavor(self): self.use_compute_discovery() - self.register_uris([ - dict(method='POST', - uri='{endpoint}/flavors'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'flavor': fakes.FAKE_FLAVOR}, - validate=dict( - json={ - 'flavor': { - "name": "vanilla", - "description": None, - "ram": 65536, - "vcpus": 24, - "swap": 0, - "os-flavor-access:is_public": True, - "rxtx_factor": 1.0, - "OS-FLV-EXT-DATA:ephemeral": 0, - "disk": 1600, - "id": None}}))]) + self.register_uris( + [ + dict( + method='POST', + uri='{endpoint}/flavors'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'flavor': fakes.FAKE_FLAVOR}, + validate=dict( + json={ + 'flavor': { + "name": "vanilla", + "description": None, + "ram": 65536, + "vcpus": 24, + "swap": 0, + "os-flavor-access:is_public": True, + "rxtx_factor": 1.0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "disk": 1600, + "id": None, + } + } + ), + ) + ] + ) self.cloud.create_flavor( - 'vanilla', ram=65536, disk=1600, vcpus=24, + 'vanilla', + ram=65536, + disk=1600, + vcpus=24, ) self.assert_calls() def test_delete_flavor(self): self.use_compute_discovery() - self.register_uris([ - dict(method='GET', - uri='{endpoint}/flavors/vanilla'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json=fakes.FAKE_FLAVOR), - dict(method='DELETE', - uri='{endpoint}/flavors/{id}'.format( - endpoint=fakes.COMPUTE_ENDPOINT, id=fakes.FLAVOR_ID))]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/flavors/vanilla'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json=fakes.FAKE_FLAVOR, + ), + dict( + method='DELETE', + uri='{endpoint}/flavors/{id}'.format( + endpoint=fakes.COMPUTE_ENDPOINT, id=fakes.FLAVOR_ID + ), + ), + ] + ) self.assertTrue(self.cloud.delete_flavor('vanilla')) self.assert_calls() def test_delete_flavor_not_found(self): self.use_compute_discovery() - self.register_uris([ - dict(method='GET', - uri='{endpoint}/flavors/invalid'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - status_code=404), - dict(method='GET', - uri='{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'flavors': fakes.FAKE_FLAVOR_LIST})]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/flavors/invalid'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + status_code=404, + ), + dict( + method='GET', + uri='{endpoint}/flavors/detail?is_public=None'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'flavors': fakes.FAKE_FLAVOR_LIST}, + ), + ] + ) self.assertFalse(self.cloud.delete_flavor('invalid')) @@ -81,30 +110,48 @@ def test_delete_flavor_not_found(self): def test_delete_flavor_exception(self): self.use_compute_discovery() - self.register_uris([ - dict(method='GET', - uri='{endpoint}/flavors/vanilla'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json=fakes.FAKE_FLAVOR), - dict(method='GET', - uri='{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'flavors': fakes.FAKE_FLAVOR_LIST}), - dict(method='DELETE', - uri='{endpoint}/flavors/{id}'.format( - endpoint=fakes.COMPUTE_ENDPOINT, id=fakes.FLAVOR_ID), - status_code=503)]) - - self.assertRaises(openstack.cloud.OpenStackCloudException, - self.cloud.delete_flavor, 'vanilla') + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/flavors/vanilla'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json=fakes.FAKE_FLAVOR, + ), + dict( + method='GET', + uri='{endpoint}/flavors/detail?is_public=None'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'flavors': fakes.FAKE_FLAVOR_LIST}, + ), + dict( + method='DELETE', + uri='{endpoint}/flavors/{id}'.format( + endpoint=fakes.COMPUTE_ENDPOINT, id=fakes.FLAVOR_ID + ), + status_code=503, + ), + ] + ) + + self.assertRaises( + openstack.cloud.OpenStackCloudException, + self.cloud.delete_flavor, + 'vanilla', + ) def test_list_flavors(self): self.use_compute_discovery() uris_to_mock = [ - dict(method='GET', - uri='{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'flavors': fakes.FAKE_FLAVOR_LIST}), + dict( + method='GET', + uri='{endpoint}/flavors/detail?is_public=None'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'flavors': fakes.FAKE_FLAVOR_LIST}, + ), ] self.register_uris(uris_to_mock) @@ -126,17 +173,26 @@ def test_list_flavors(self): def test_list_flavors_with_extra(self): self.use_compute_discovery() uris_to_mock = [ - dict(method='GET', - uri='{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'flavors': fakes.FAKE_FLAVOR_LIST}), + dict( + method='GET', + uri='{endpoint}/flavors/detail?is_public=None'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'flavors': fakes.FAKE_FLAVOR_LIST}, + ), ] - uris_to_mock.extend([ - dict(method='GET', - uri='{endpoint}/flavors/{id}/os-extra_specs'.format( - endpoint=fakes.COMPUTE_ENDPOINT, id=flavor['id']), - json={'extra_specs': {}}) - for flavor in fakes.FAKE_FLAVOR_LIST]) + uris_to_mock.extend( + [ + dict( + method='GET', + uri='{endpoint}/flavors/{id}/os-extra_specs'.format( + endpoint=fakes.COMPUTE_ENDPOINT, id=flavor['id'] + ), + json={'extra_specs': {}}, + ) + for flavor in fakes.FAKE_FLAVOR_LIST + ] + ) self.register_uris(uris_to_mock) flavors = self.cloud.list_flavors(get_extra=True) @@ -157,17 +213,26 @@ def test_list_flavors_with_extra(self): def test_get_flavor_by_ram(self): self.use_compute_discovery() uris_to_mock = [ - dict(method='GET', - uri='{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'flavors': fakes.FAKE_FLAVOR_LIST}), + dict( + method='GET', + uri='{endpoint}/flavors/detail?is_public=None'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'flavors': fakes.FAKE_FLAVOR_LIST}, + ), ] - uris_to_mock.extend([ - dict(method='GET', - uri='{endpoint}/flavors/{id}/os-extra_specs'.format( - endpoint=fakes.COMPUTE_ENDPOINT, id=flavor['id']), - json={'extra_specs': {}}) - for flavor in fakes.FAKE_FLAVOR_LIST]) + uris_to_mock.extend( + [ + dict( + method='GET', + uri='{endpoint}/flavors/{id}/os-extra_specs'.format( + endpoint=fakes.COMPUTE_ENDPOINT, id=flavor['id'] + ), + json={'extra_specs': {}}, + ) + for flavor in fakes.FAKE_FLAVOR_LIST + ] + ) self.register_uris(uris_to_mock) flavor = self.cloud.get_flavor_by_ram(ram=250) @@ -176,47 +241,69 @@ def test_get_flavor_by_ram(self): def test_get_flavor_by_ram_and_include(self): self.use_compute_discovery() uris_to_mock = [ - dict(method='GET', - uri='{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'flavors': fakes.FAKE_FLAVOR_LIST}), + dict( + method='GET', + uri='{endpoint}/flavors/detail?is_public=None'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'flavors': fakes.FAKE_FLAVOR_LIST}, + ), ] - uris_to_mock.extend([ - dict(method='GET', - uri='{endpoint}/flavors/{id}/os-extra_specs'.format( - endpoint=fakes.COMPUTE_ENDPOINT, id=flavor['id']), - json={'extra_specs': {}}) - for flavor in fakes.FAKE_FLAVOR_LIST]) + uris_to_mock.extend( + [ + dict( + method='GET', + uri='{endpoint}/flavors/{id}/os-extra_specs'.format( + endpoint=fakes.COMPUTE_ENDPOINT, id=flavor['id'] + ), + json={'extra_specs': {}}, + ) + for flavor in fakes.FAKE_FLAVOR_LIST + ] + ) self.register_uris(uris_to_mock) flavor = self.cloud.get_flavor_by_ram(ram=150, include='strawberry') self.assertEqual(fakes.STRAWBERRY_FLAVOR_ID, flavor['id']) def test_get_flavor_by_ram_not_found(self): self.use_compute_discovery() - self.register_uris([ - dict(method='GET', - uri='{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'flavors': []})]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/flavors/detail?is_public=None'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'flavors': []}, + ) + ] + ) self.assertRaises( openstack.cloud.OpenStackCloudException, self.cloud.get_flavor_by_ram, - ram=100) + ram=100, + ) def test_get_flavor_string_and_int(self): self.use_compute_discovery() flavor_resource_uri = '{endpoint}/flavors/1/os-extra_specs'.format( - endpoint=fakes.COMPUTE_ENDPOINT) + endpoint=fakes.COMPUTE_ENDPOINT + ) flavor = fakes.make_fake_flavor('1', 'vanilla') flavor_json = {'extra_specs': {}} - self.register_uris([ - dict(method='GET', - uri='{endpoint}/flavors/1'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json=flavor), - dict(method='GET', uri=flavor_resource_uri, json=flavor_json), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/flavors/1'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json=flavor, + ), + dict(method='GET', uri=flavor_resource_uri, json=flavor_json), + ] + ) flavor1 = self.cloud.get_flavor('1') self.assertEqual('1', flavor1['id']) @@ -226,11 +313,17 @@ def test_get_flavor_string_and_int(self): def test_set_flavor_specs(self): self.use_compute_discovery() extra_specs = dict(key1='value1') - self.register_uris([ - dict(method='POST', - uri='{endpoint}/flavors/{id}/os-extra_specs'.format( - endpoint=fakes.COMPUTE_ENDPOINT, id=1), - json=dict(extra_specs=extra_specs))]) + self.register_uris( + [ + dict( + method='POST', + uri='{endpoint}/flavors/{id}/os-extra_specs'.format( + endpoint=fakes.COMPUTE_ENDPOINT, id=1 + ), + json=dict(extra_specs=extra_specs), + ) + ] + ) self.cloud.set_flavor_specs(1, extra_specs) self.assert_calls() @@ -238,62 +331,97 @@ def test_set_flavor_specs(self): def test_unset_flavor_specs(self): self.use_compute_discovery() keys = ['key1', 'key2'] - self.register_uris([ - dict(method='DELETE', - uri='{endpoint}/flavors/{id}/os-extra_specs/{key}'.format( - endpoint=fakes.COMPUTE_ENDPOINT, id=1, key=key)) - for key in keys]) + self.register_uris( + [ + dict( + method='DELETE', + uri='{endpoint}/flavors/{id}/os-extra_specs/{key}'.format( + endpoint=fakes.COMPUTE_ENDPOINT, id=1, key=key + ), + ) + for key in keys + ] + ) self.cloud.unset_flavor_specs(1, keys) self.assert_calls() def test_add_flavor_access(self): - self.register_uris([ - dict(method='POST', - uri='{endpoint}/flavors/{id}/action'.format( - endpoint=fakes.COMPUTE_ENDPOINT, id='flavor_id'), - json={ - 'flavor_access': [{ - 'flavor_id': 'flavor_id', 'tenant_id': 'tenant_id'}]}, - validate=dict( - json={'addTenantAccess': {'tenant': 'tenant_id'}}))]) + self.register_uris( + [ + dict( + method='POST', + uri='{endpoint}/flavors/{id}/action'.format( + endpoint=fakes.COMPUTE_ENDPOINT, id='flavor_id' + ), + json={ + 'flavor_access': [ + { + 'flavor_id': 'flavor_id', + 'tenant_id': 'tenant_id', + } + ] + }, + validate=dict( + json={'addTenantAccess': {'tenant': 'tenant_id'}} + ), + ) + ] + ) self.cloud.add_flavor_access('flavor_id', 'tenant_id') self.assert_calls() def test_remove_flavor_access(self): - self.register_uris([ - dict(method='POST', - uri='{endpoint}/flavors/{id}/action'.format( - endpoint=fakes.COMPUTE_ENDPOINT, id='flavor_id'), - json={'flavor_access': []}, - validate=dict( - json={'removeTenantAccess': {'tenant': 'tenant_id'}}))]) + self.register_uris( + [ + dict( + method='POST', + uri='{endpoint}/flavors/{id}/action'.format( + endpoint=fakes.COMPUTE_ENDPOINT, id='flavor_id' + ), + json={'flavor_access': []}, + validate=dict( + json={'removeTenantAccess': {'tenant': 'tenant_id'}} + ), + ) + ] + ) self.cloud.remove_flavor_access('flavor_id', 'tenant_id') self.assert_calls() def test_list_flavor_access(self): - self.register_uris([ - dict(method='GET', - uri='{endpoint}/flavors/vanilla/os-flavor-access'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={ - 'flavor_access': [ - {'flavor_id': 'vanilla', 'tenant_id': 'tenant_id'}]}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/flavors/vanilla/os-flavor-access'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={ + 'flavor_access': [ + {'flavor_id': 'vanilla', 'tenant_id': 'tenant_id'} + ] + }, + ) + ] + ) self.cloud.list_flavor_access('vanilla') self.assert_calls() def test_get_flavor_by_id(self): self.use_compute_discovery() flavor_uri = '{endpoint}/flavors/1'.format( - endpoint=fakes.COMPUTE_ENDPOINT) + endpoint=fakes.COMPUTE_ENDPOINT + ) flavor_json = {'flavor': fakes.make_fake_flavor('1', 'vanilla')} - self.register_uris([ - dict(method='GET', uri=flavor_uri, json=flavor_json), - ]) + self.register_uris( + [ + dict(method='GET', uri=flavor_uri, json=flavor_json), + ] + ) flavor1 = self.cloud.get_flavor_by_id('1') self.assertEqual('1', flavor1['id']) @@ -305,16 +433,22 @@ def test_get_flavor_by_id(self): def test_get_flavor_with_extra_specs(self): self.use_compute_discovery() flavor_uri = '{endpoint}/flavors/1'.format( - endpoint=fakes.COMPUTE_ENDPOINT) + endpoint=fakes.COMPUTE_ENDPOINT + ) flavor_extra_uri = '{endpoint}/flavors/1/os-extra_specs'.format( - endpoint=fakes.COMPUTE_ENDPOINT) + endpoint=fakes.COMPUTE_ENDPOINT + ) flavor_json = {'flavor': fakes.make_fake_flavor('1', 'vanilla')} flavor_extra_json = {'extra_specs': {'name': 'test'}} - self.register_uris([ - dict(method='GET', uri=flavor_uri, json=flavor_json), - dict(method='GET', uri=flavor_extra_uri, json=flavor_extra_json), - ]) + self.register_uris( + [ + dict(method='GET', uri=flavor_uri, json=flavor_json), + dict( + method='GET', uri=flavor_extra_uri, json=flavor_extra_json + ), + ] + ) flavor1 = self.cloud.get_flavor_by_id('1', get_extra=True) self.assertEqual('1', flavor1['id']) diff --git a/openstack/tests/unit/cloud/test_floating_ip_common.py b/openstack/tests/unit/cloud/test_floating_ip_common.py index 6687e37da..420af74de 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_common.py +++ b/openstack/tests/unit/cloud/test_floating_ip_common.py @@ -29,16 +29,20 @@ class TestFloatingIP(base.TestCase): - @patch.object(connection.Connection, 'get_floating_ip') @patch.object(connection.Connection, '_attach_ip_to_server') @patch.object(connection.Connection, 'available_floating_ip') def test_add_auto_ip( - self, mock_available_floating_ip, mock_attach_ip_to_server, - mock_get_floating_ip): + self, + mock_available_floating_ip, + mock_attach_ip_to_server, + mock_get_floating_ip, + ): server_dict = fakes.make_fake_server( - server_id='server-id', name='test-server', status="ACTIVE", - addresses={} + server_id='server-id', + name='test-server', + status="ACTIVE", + addresses={}, ) floating_ip_dict = { "id": "this-is-a-floating-ip-id", @@ -47,7 +51,7 @@ def test_add_auto_ip( "floating_ip_address": "203.0.113.29", "network": "this-is-a-net-or-pool-id", "attached": False, - "status": "ACTIVE" + "status": "ACTIVE", } mock_available_floating_ip.return_value = floating_ip_dict @@ -55,51 +59,63 @@ def test_add_auto_ip( self.cloud.add_auto_ip(server=server_dict) mock_attach_ip_to_server.assert_called_with( - timeout=60, wait=False, server=server_dict, - floating_ip=floating_ip_dict, skip_attach=False) + timeout=60, + wait=False, + server=server_dict, + floating_ip=floating_ip_dict, + skip_attach=False, + ) @patch.object(connection.Connection, '_add_ip_from_pool') def test_add_ips_to_server_pool(self, mock_add_ip_from_pool): server_dict = fakes.make_fake_server( - server_id='romeo', name='test-server', status="ACTIVE", - addresses={}) + server_id='romeo', + name='test-server', + status="ACTIVE", + addresses={}, + ) pool = 'nova' self.cloud.add_ips_to_server(server_dict, ip_pool=pool) mock_add_ip_from_pool.assert_called_with( - server_dict, pool, reuse=True, wait=False, timeout=60, - fixed_address=None, nat_destination=None) + server_dict, + pool, + reuse=True, + wait=False, + timeout=60, + fixed_address=None, + nat_destination=None, + ) @patch.object(connection.Connection, 'has_service') @patch.object(connection.Connection, 'get_floating_ip') @patch.object(connection.Connection, '_add_auto_ip') def test_add_ips_to_server_ipv6_only( - self, mock_add_auto_ip, - mock_get_floating_ip, - mock_has_service): + self, mock_add_auto_ip, mock_get_floating_ip, mock_has_service + ): self.cloud._floating_ip_source = None self.cloud.force_ipv4 = False self.cloud._local_ipv6 = True mock_has_service.return_value = False server = fakes.make_fake_server( - server_id='server-id', name='test-server', status="ACTIVE", + server_id='server-id', + name='test-server', + status="ACTIVE", addresses={ - 'private': [{ - 'addr': "10.223.160.141", - 'version': 4 - }], - 'public': [{ - u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:ae:7d:42', - u'OS-EXT-IPS:type': u'fixed', - 'addr': "2001:4800:7819:103:be76:4eff:fe05:8525", - 'version': 6 - }] - } + 'private': [{'addr': "10.223.160.141", 'version': 4}], + 'public': [ + { + u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:ae:7d:42', + u'OS-EXT-IPS:type': u'fixed', + 'addr': "2001:4800:7819:103:be76:4eff:fe05:8525", + 'version': 6, + } + ], + }, ) server_dict = meta.add_server_interfaces( - self.cloud, - _server.Server(**server) + self.cloud, _server.Server(**server) ) new_server = self.cloud.add_ips_to_server(server=server_dict) @@ -107,80 +123,79 @@ def test_add_ips_to_server_ipv6_only( mock_add_auto_ip.assert_not_called() self.assertEqual( new_server['interface_ip'], - '2001:4800:7819:103:be76:4eff:fe05:8525') + '2001:4800:7819:103:be76:4eff:fe05:8525', + ) self.assertEqual(new_server['private_v4'], '10.223.160.141') self.assertEqual(new_server['public_v4'], '') self.assertEqual( - new_server['public_v6'], '2001:4800:7819:103:be76:4eff:fe05:8525') + new_server['public_v6'], '2001:4800:7819:103:be76:4eff:fe05:8525' + ) @patch.object(connection.Connection, 'has_service') @patch.object(connection.Connection, 'get_floating_ip') @patch.object(connection.Connection, '_add_auto_ip') def test_add_ips_to_server_rackspace( - self, mock_add_auto_ip, - mock_get_floating_ip, - mock_has_service): + self, mock_add_auto_ip, mock_get_floating_ip, mock_has_service + ): self.cloud._floating_ip_source = None self.cloud.force_ipv4 = False self.cloud._local_ipv6 = True mock_has_service.return_value = False server = fakes.make_fake_server( - server_id='server-id', name='test-server', status="ACTIVE", + server_id='server-id', + name='test-server', + status="ACTIVE", addresses={ - 'private': [{ - 'addr': "10.223.160.141", - 'version': 4 - }], - 'public': [{ - 'addr': "104.130.246.91", - 'version': 4 - }, { - 'addr': "2001:4800:7819:103:be76:4eff:fe05:8525", - 'version': 6 - }] - } + 'private': [{'addr': "10.223.160.141", 'version': 4}], + 'public': [ + {'addr': "104.130.246.91", 'version': 4}, + { + 'addr': "2001:4800:7819:103:be76:4eff:fe05:8525", + 'version': 6, + }, + ], + }, ) server_dict = meta.add_server_interfaces( - self.cloud, - _server.Server(**server)) + self.cloud, _server.Server(**server) + ) new_server = self.cloud.add_ips_to_server(server=server_dict) mock_get_floating_ip.assert_not_called() mock_add_auto_ip.assert_not_called() self.assertEqual( new_server['interface_ip'], - '2001:4800:7819:103:be76:4eff:fe05:8525') + '2001:4800:7819:103:be76:4eff:fe05:8525', + ) @patch.object(connection.Connection, 'has_service') @patch.object(connection.Connection, 'get_floating_ip') @patch.object(connection.Connection, '_add_auto_ip') def test_add_ips_to_server_rackspace_local_ipv4( - self, mock_add_auto_ip, - mock_get_floating_ip, - mock_has_service): + self, mock_add_auto_ip, mock_get_floating_ip, mock_has_service + ): self.cloud._floating_ip_source = None self.cloud.force_ipv4 = False self.cloud._local_ipv6 = False mock_has_service.return_value = False server = fakes.make_fake_server( - server_id='server-id', name='test-server', status="ACTIVE", + server_id='server-id', + name='test-server', + status="ACTIVE", addresses={ - 'private': [{ - 'addr': "10.223.160.141", - 'version': 4 - }], - 'public': [{ - 'addr': "104.130.246.91", - 'version': 4 - }, { - 'addr': "2001:4800:7819:103:be76:4eff:fe05:8525", - 'version': 6 - }] - } + 'private': [{'addr': "10.223.160.141", 'version': 4}], + 'public': [ + {'addr': "104.130.246.91", 'version': 4}, + { + 'addr': "2001:4800:7819:103:be76:4eff:fe05:8525", + 'version': 6, + }, + ], + }, ) server_dict = meta.add_server_interfaces( - self.cloud, - _server.Server(**server)) + self.cloud, _server.Server(**server) + ) new_server = self.cloud.add_ips_to_server(server=server_dict) mock_get_floating_ip.assert_not_called() @@ -190,24 +205,35 @@ def test_add_ips_to_server_rackspace_local_ipv4( @patch.object(connection.Connection, 'add_ip_list') def test_add_ips_to_server_ip_list(self, mock_add_ip_list): server_dict = fakes.make_fake_server( - server_id='server-id', name='test-server', status="ACTIVE", - addresses={}) + server_id='server-id', + name='test-server', + status="ACTIVE", + addresses={}, + ) ips = ['203.0.113.29', '172.24.4.229'] self.cloud.add_ips_to_server(server_dict, ips=ips) mock_add_ip_list.assert_called_with( - server_dict, ips, wait=False, timeout=60, + server_dict, + ips, + wait=False, + timeout=60, fixed_address=None, - nat_destination=None) + nat_destination=None, + ) @patch.object(connection.Connection, '_needs_floating_ip') @patch.object(connection.Connection, '_add_auto_ip') def test_add_ips_to_server_auto_ip( - self, mock_add_auto_ip, mock_needs_floating_ip): + self, mock_add_auto_ip, mock_needs_floating_ip + ): server_dict = fakes.make_fake_server( - server_id='server-id', name='test-server', status="ACTIVE", - addresses={}) + server_id='server-id', + name='test-server', + status="ACTIVE", + addresses={}, + ) # TODO(mordred) REMOVE THIS MOCK WHEN THE NEXT PATCH LANDS # SERIOUSLY THIS TIME. NEXT PATCH - WHICH SHOULD ADD MOCKS FOR @@ -218,4 +244,5 @@ def test_add_ips_to_server_auto_ip( self.cloud.add_ips_to_server(server_dict) mock_add_auto_ip.assert_called_with( - server_dict, wait=False, timeout=60, reuse=True) + server_dict, wait=False, timeout=60, reuse=True + ) diff --git a/openstack/tests/unit/cloud/test_floating_ip_neutron.py b/openstack/tests/unit/cloud/test_floating_ip_neutron.py index d2fb8710b..9f85c9fab 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_neutron.py +++ b/openstack/tests/unit/cloud/test_floating_ip_neutron.py @@ -39,7 +39,7 @@ class TestFloatingIP(base.TestCase): 'floating_ip_address': '172.24.4.229', 'port_id': 'ce705c24-c1ef-408a-bda3-7bbd946164ac', 'id': '2f245a7b-796b-4f26-9cf9-9e82d248fda7', - 'status': 'ACTIVE' + 'status': 'ACTIVE', }, { 'router_id': None, @@ -49,8 +49,8 @@ class TestFloatingIP(base.TestCase): 'floating_ip_address': '203.0.113.30', 'port_id': None, 'id': '61cea855-49cb-4846-997d-801b70c71bdd', - 'status': 'DOWN' - } + 'status': 'DOWN', + }, ] } @@ -63,7 +63,7 @@ class TestFloatingIP(base.TestCase): 'port_id': None, 'router_id': None, 'status': 'ACTIVE', - 'tenant_id': '4969c491a3c74ee4af974e6d800c62df' + 'tenant_id': '4969c491a3c74ee4af974e6d800c62df', } } @@ -76,15 +76,13 @@ class TestFloatingIP(base.TestCase): 'port_id': 'ce705c24-c1ef-408a-bda3-7bbd946164ac', 'router_id': None, 'status': 'ACTIVE', - 'tenant_id': '4969c491a3c74ee4af974e6d800c62df' + 'tenant_id': '4969c491a3c74ee4af974e6d800c62df', } } mock_get_network_rep = { 'status': 'ACTIVE', - 'subnets': [ - '54d6f61d-db07-451c-9ab3-b9609b6b6f0b' - ], + 'subnets': ['54d6f61d-db07-451c-9ab3-b9609b6b6f0b'], 'name': 'my-network', 'provider:physical_network': None, 'admin_state_up': True, @@ -93,7 +91,7 @@ class TestFloatingIP(base.TestCase): 'router:external': True, 'shared': True, 'id': 'my-network-id', - 'provider:segmentation_id': None + 'provider:segmentation_id': None, } mock_search_ports_rep = [ @@ -109,7 +107,7 @@ class TestFloatingIP(base.TestCase): 'extra_dhcp_opts': [], 'binding:vif_details': { 'port_filter': True, - 'ovs_hybrid_plug': True + 'ovs_hybrid_plug': True, }, 'binding:vif_type': 'ovs', 'device_owner': 'compute:None', @@ -119,12 +117,12 @@ class TestFloatingIP(base.TestCase): 'fixed_ips': [ { 'subnet_id': '008ba151-0b8c-4a67-98b5-0d2b87666062', - 'ip_address': u'172.24.4.2' + 'ip_address': '172.24.4.2', } ], 'id': 'ce705c24-c1ef-408a-bda3-7bbd946164ac', 'security_groups': [], - 'device_id': 'server-id' + 'device_id': 'server-id', } ] @@ -136,20 +134,32 @@ def setUp(self): super(TestFloatingIP, self).setUp() self.fake_server = fakes.make_fake_server( - 'server-id', '', 'ACTIVE', - addresses={u'test_pnztt_net': [{ - u'OS-EXT-IPS:type': u'fixed', - u'addr': '192.0.2.129', - u'version': 4, - u'OS-EXT-IPS-MAC:mac_addr': - u'fa:16:3e:ae:7d:42'}]}) + 'server-id', + '', + 'ACTIVE', + addresses={ + 'test_pnztt_net': [ + { + 'OS-EXT-IPS:type': 'fixed', + 'addr': '192.0.2.129', + 'version': 4, + 'OS-EXT-IPS-MAC:mac_addr': 'fa:16:3e:ae:7d:42', + } + ] + }, + ) self.floating_ip = self.mock_floating_ip_list_rep['floatingips'][0] def test_list_floating_ips(self): - self.register_uris([ - dict(method='GET', - uri='https://network.example.com/v2.0/floatingips', - json=self.mock_floating_ip_list_rep)]) + self.register_uris( + [ + dict( + method='GET', + uri='https://network.example.com/v2.0/floatingips', + json=self.mock_floating_ip_list_rep, + ) + ] + ) floating_ips = self.cloud.list_floating_ips() @@ -161,24 +171,37 @@ def test_list_floating_ips(self): def test_list_floating_ips_with_filters(self): - self.register_uris([ - dict(method='GET', - uri=('https://network.example.com/v2.0/floatingips?' - 'description=42'), - json={'floatingips': []})]) + self.register_uris( + [ + dict( + method='GET', + uri=( + 'https://network.example.com/v2.0/floatingips?' + 'description=42' + ), + json={'floatingips': []}, + ) + ] + ) self.cloud.list_floating_ips(filters={'description': 42}) self.assert_calls() def test_search_floating_ips(self): - self.register_uris([ - dict(method='GET', - uri=('https://network.example.com/v2.0/floatingips'), - json=self.mock_floating_ip_list_rep)]) + self.register_uris( + [ + dict( + method='GET', + uri=('https://network.example.com/v2.0/floatingips'), + json=self.mock_floating_ip_list_rep, + ) + ] + ) floating_ips = self.cloud.search_floating_ips( - filters={'updated_at': 'never'}) + filters={'updated_at': 'never'} + ) self.assertIsInstance(floating_ips, list) self.assertAreInstances(floating_ips, dict) @@ -186,32 +209,43 @@ def test_search_floating_ips(self): self.assert_calls() def test_get_floating_ip(self): - self.register_uris([ - dict(method='GET', - uri='https://network.example.com/v2.0/floatingips', - json=self.mock_floating_ip_list_rep)]) + self.register_uris( + [ + dict( + method='GET', + uri='https://network.example.com/v2.0/floatingips', + json=self.mock_floating_ip_list_rep, + ) + ] + ) floating_ip = self.cloud.get_floating_ip( - id='2f245a7b-796b-4f26-9cf9-9e82d248fda7') + id='2f245a7b-796b-4f26-9cf9-9e82d248fda7' + ) self.assertIsInstance(floating_ip, dict) self.assertEqual('172.24.4.229', floating_ip['floating_ip_address']) self.assertEqual( self.mock_floating_ip_list_rep['floatingips'][0]['tenant_id'], - floating_ip['project_id'] + floating_ip['project_id'], ) self.assertEqual( self.mock_floating_ip_list_rep['floatingips'][0]['tenant_id'], - floating_ip['tenant_id'] + floating_ip['tenant_id'], ) self.assertIn('location', floating_ip) self.assert_calls() def test_get_floating_ip_not_found(self): - self.register_uris([ - dict(method='GET', - uri='https://network.example.com/v2.0/floatingips', - json=self.mock_floating_ip_list_rep)]) + self.register_uris( + [ + dict( + method='GET', + uri='https://network.example.com/v2.0/floatingips', + json=self.mock_floating_ip_list_rep, + ) + ] + ) floating_ip = self.cloud.get_floating_ip(id='non-existent') @@ -220,11 +254,16 @@ def test_get_floating_ip_not_found(self): def test_get_floating_ip_by_id(self): fid = self.mock_floating_ip_new_rep['floatingip']['id'] - self.register_uris([ - dict(method='GET', - uri='https://network.example.com/v2.0/floatingips/' - '{id}'.format(id=fid), - json=self.mock_floating_ip_new_rep)]) + self.register_uris( + [ + dict( + method='GET', + uri='https://network.example.com/v2.0/floatingips/' + '{id}'.format(id=fid), + json=self.mock_floating_ip_new_rep, + ) + ] + ) floating_ip = self.cloud.get_floating_ip_by_id(id=fid) @@ -232,83 +271,122 @@ def test_get_floating_ip_by_id(self): self.assertEqual('172.24.4.229', floating_ip['floating_ip_address']) self.assertEqual( self.mock_floating_ip_new_rep['floatingip']['tenant_id'], - floating_ip['project_id'] + floating_ip['project_id'], ) self.assertEqual( self.mock_floating_ip_new_rep['floatingip']['tenant_id'], - floating_ip['tenant_id'] + floating_ip['tenant_id'], ) self.assertIn('location', floating_ip) self.assert_calls() def test_create_floating_ip(self): - self.register_uris([ - dict(method='GET', - uri='https://network.example.com/v2.0/networks/my-network', - status_code=404), - dict(method='GET', - uri='https://network.example.com/v2.0/networks' - '?name=my-network', - json={'networks': [self.mock_get_network_rep]}), - dict(method='POST', - uri='https://network.example.com/v2.0/floatingips', - json=self.mock_floating_ip_new_rep, - validate=dict( - json={'floatingip': { - 'floating_network_id': 'my-network-id'}})) - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://network.example.com/v2.0/networks/my-network', + status_code=404, + ), + dict( + method='GET', + uri='https://network.example.com/v2.0/networks' + '?name=my-network', + json={'networks': [self.mock_get_network_rep]}, + ), + dict( + method='POST', + uri='https://network.example.com/v2.0/floatingips', + json=self.mock_floating_ip_new_rep, + validate=dict( + json={ + 'floatingip': { + 'floating_network_id': 'my-network-id' + } + } + ), + ), + ] + ) ip = self.cloud.create_floating_ip(network='my-network') self.assertEqual( self.mock_floating_ip_new_rep['floatingip']['floating_ip_address'], - ip['floating_ip_address']) + ip['floating_ip_address'], + ) self.assert_calls() def test_create_floating_ip_port_bad_response(self): - self.register_uris([ - dict(method='GET', - uri='https://network.example.com/v2.0/networks/my-network', - json=self.mock_get_network_rep), - dict(method='POST', - uri='https://network.example.com/v2.0/floatingips', - json=self.mock_floating_ip_new_rep, - validate=dict( - json={'floatingip': { - 'floating_network_id': 'my-network-id', - 'port_id': u'ce705c24-c1ef-408a-bda3-7bbd946164ab'}})) - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://network.example.com/v2.0/networks/my-network', + json=self.mock_get_network_rep, + ), + dict( + method='POST', + uri='https://network.example.com/v2.0/floatingips', + json=self.mock_floating_ip_new_rep, + validate=dict( + json={ + 'floatingip': { + 'floating_network_id': 'my-network-id', + 'port_id': 'ce705c24-c1ef-408a-bda3-7bbd946164ab', # noqa: E501 + } + } + ), + ), + ] + ) # Fails because we requested a port and the returned FIP has no port self.assertRaises( exc.OpenStackCloudException, self.cloud.create_floating_ip, - network='my-network', port='ce705c24-c1ef-408a-bda3-7bbd946164ab') + network='my-network', + port='ce705c24-c1ef-408a-bda3-7bbd946164ab', + ) self.assert_calls() def test_create_floating_ip_port(self): - self.register_uris([ - dict(method='GET', - uri='https://network.example.com/v2.0/networks/my-network', - status_code=404), - dict(method='GET', - uri='https://network.example.com/v2.0/networks' - '?name=my-network', - json={'networks': [self.mock_get_network_rep]}), - dict(method='POST', - uri='https://network.example.com/v2.0/floatingips', - json=self.mock_floating_ip_port_rep, - validate=dict( - json={'floatingip': { - 'floating_network_id': 'my-network-id', - 'port_id': u'ce705c24-c1ef-408a-bda3-7bbd946164ac'}})) - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://network.example.com/v2.0/networks/my-network', + status_code=404, + ), + dict( + method='GET', + uri='https://network.example.com/v2.0/networks' + '?name=my-network', + json={'networks': [self.mock_get_network_rep]}, + ), + dict( + method='POST', + uri='https://network.example.com/v2.0/floatingips', + json=self.mock_floating_ip_port_rep, + validate=dict( + json={ + 'floatingip': { + 'floating_network_id': 'my-network-id', + 'port_id': 'ce705c24-c1ef-408a-bda3-7bbd946164ac', # noqa: E501 + } + } + ), + ), + ] + ) ip = self.cloud.create_floating_ip( - network='my-network', port='ce705c24-c1ef-408a-bda3-7bbd946164ac') + network='my-network', port='ce705c24-c1ef-408a-bda3-7bbd946164ac' + ) self.assertEqual( self.mock_floating_ip_new_rep['floatingip']['floating_ip_address'], - ip['floating_ip_address']) + ip['floating_ip_address'], + ) self.assert_calls() def test_neutron_available_floating_ips(self): @@ -316,21 +394,37 @@ def test_neutron_available_floating_ips(self): Test without specifying a network name. """ fips_mock_uri = 'https://network.example.com/v2.0/floatingips' - self.register_uris([ - dict(method='GET', - uri='https://network.example.com/v2.0/networks', - json={'networks': [self.mock_get_network_rep]}), - dict(method='GET', - uri='https://network.example.com/v2.0/subnets', - json={'subnets': []}), - dict(method='GET', uri=fips_mock_uri, json={'floatingips': []}), - dict(method='POST', uri=fips_mock_uri, - json=self.mock_floating_ip_new_rep, - validate=dict(json={ - 'floatingip': { - 'floating_network_id': self.mock_get_network_rep['id'] - }})) - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://network.example.com/v2.0/networks', + json={'networks': [self.mock_get_network_rep]}, + ), + dict( + method='GET', + uri='https://network.example.com/v2.0/subnets', + json={'subnets': []}, + ), + dict( + method='GET', uri=fips_mock_uri, json={'floatingips': []} + ), + dict( + method='POST', + uri=fips_mock_uri, + json=self.mock_floating_ip_new_rep, + validate=dict( + json={ + 'floatingip': { + 'floating_network_id': self.mock_get_network_rep[ # noqa: E501 + 'id' + ] + } + } + ), + ), + ] + ) # Test if first network is selected if no network is given self.cloud._neutron_available_floating_ips() @@ -341,21 +435,37 @@ def test_neutron_available_floating_ips_network(self): Test with specifying a network name. """ fips_mock_uri = 'https://network.example.com/v2.0/floatingips' - self.register_uris([ - dict(method='GET', - uri='https://network.example.com/v2.0/networks', - json={'networks': [self.mock_get_network_rep]}), - dict(method='GET', - uri='https://network.example.com/v2.0/subnets', - json={'subnets': []}), - dict(method='GET', uri=fips_mock_uri, json={'floatingips': []}), - dict(method='POST', uri=fips_mock_uri, - json=self.mock_floating_ip_new_rep, - validate=dict(json={ - 'floatingip': { - 'floating_network_id': self.mock_get_network_rep['id'] - }})) - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://network.example.com/v2.0/networks', + json={'networks': [self.mock_get_network_rep]}, + ), + dict( + method='GET', + uri='https://network.example.com/v2.0/subnets', + json={'subnets': []}, + ), + dict( + method='GET', uri=fips_mock_uri, json={'floatingips': []} + ), + dict( + method='POST', + uri=fips_mock_uri, + json=self.mock_floating_ip_new_rep, + validate=dict( + json={ + 'floatingip': { + 'floating_network_id': self.mock_get_network_rep[ # noqa: E501 + 'id' + ] + } + } + ), + ), + ] + ) # Test if first network is selected if no network is given self.cloud._neutron_available_floating_ips( @@ -367,249 +477,342 @@ def test_neutron_available_floating_ips_invalid_network(self): """ Test with an invalid network name. """ - self.register_uris([ - dict(method='GET', - uri='https://network.example.com/v2.0/networks', - json={'networks': [self.mock_get_network_rep]}), - dict(method='GET', - uri='https://network.example.com/v2.0/subnets', - json={'subnets': []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://network.example.com/v2.0/networks', + json={'networks': [self.mock_get_network_rep]}, + ), + dict( + method='GET', + uri='https://network.example.com/v2.0/subnets', + json={'subnets': []}, + ), + ] + ) self.assertRaises( exc.OpenStackCloudException, self.cloud._neutron_available_floating_ips, - network='INVALID') + network='INVALID', + ) self.assert_calls() def test_auto_ip_pool_no_reuse(self): # payloads taken from citycloud - self.register_uris([ - dict(method='GET', - uri='https://network.example.com/v2.0/networks/ext-net', - status_code=404), - dict(method='GET', - uri='https://network.example.com/v2.0/networks?name=ext-net', - json={"networks": [{ - "status": "ACTIVE", - "subnets": [ - "df3e17fa-a4b2-47ae-9015-bc93eb076ba2", - "6b0c3dc9-b0b8-4d87-976a-7f2ebf13e7ec", - "fc541f48-fc7f-48c0-a063-18de6ee7bdd7"], - "availability_zone_hints": [], - "availability_zones": ["nova"], - "name": "ext-net", - "admin_state_up": True, - "tenant_id": "a564613210ee43708b8a7fc6274ebd63", - "tags": [], - "ipv6_address_scope": "9f03124f-89af-483a-b6fd-10f08079db4d", # noqa - "mtu": 0, - "is_default": False, - "router:external": True, - "ipv4_address_scope": None, - "shared": False, - "id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", - "description": None - }]}), - dict(method='GET', - uri='https://network.example.com/v2.0/ports' - '?device_id=f80e3ad0-e13e-41d4-8e9c-be79bccdb8f7', - json={"ports": [{ - "status": "ACTIVE", - "created_at": "2017-02-06T20:59:45", - "description": "", - "allowed_address_pairs": [], - "admin_state_up": True, - "network_id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", - "dns_name": None, - "extra_dhcp_opts": [], - "mac_address": "fa:16:3e:e8:7f:03", - "updated_at": "2017-02-06T20:59:49", - "name": "", - "device_owner": "compute:None", - "tenant_id": "65222a4d09ea4c68934fa1028c77f394", - "binding:vnic_type": "normal", - "fixed_ips": [{ - "subnet_id": "f0ad1df5-53ee-473f-b86b-3604ea5591e9", - "ip_address": "10.4.0.16"}], - "id": "a767944e-057a-47d1-a669-824a21b8fb7b", - "security_groups": [ - "9fb5ba44-5c46-4357-8e60-8b55526cab54"], - "device_id": "f80e3ad0-e13e-41d4-8e9c-be79bccdb8f7", - }]}), - - dict(method='POST', - uri='https://network.example.com/v2.0/floatingips', - json={"floatingip": { - "router_id": "9de9c787-8f89-4a53-8468-a5533d6d7fd1", - "status": "DOWN", - "description": "", - "dns_domain": "", - "floating_network_id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", # noqa - "fixed_ip_address": "10.4.0.16", - "floating_ip_address": "89.40.216.153", - "port_id": "a767944e-057a-47d1-a669-824a21b8fb7b", - "id": "e69179dc-a904-4c9a-a4c9-891e2ecb984c", - "dns_name": "", - "tenant_id": "65222a4d09ea4c68934fa1028c77f394" - }}, - validate=dict(json={"floatingip": { - "floating_network_id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", # noqa - "fixed_ip_address": "10.4.0.16", - "port_id": "a767944e-057a-47d1-a669-824a21b8fb7b", - }})), - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri='{endpoint}/servers/detail'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={"servers": [{ - "status": "ACTIVE", - "updated": "2017-02-06T20:59:49Z", - "addresses": { - "private": [{ - "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:e8:7f:03", - "version": 4, - "addr": "10.4.0.16", - "OS-EXT-IPS:type": "fixed" - }, { - "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:e8:7f:03", - "version": 4, - "addr": "89.40.216.153", - "OS-EXT-IPS:type": "floating" - }]}, - "key_name": None, - "image": {"id": "95e4c449-8abf-486e-97d9-dc3f82417d2d"}, - "OS-EXT-STS:task_state": None, - "OS-EXT-STS:vm_state": "active", - "OS-SRV-USG:launched_at": "2017-02-06T20:59:48.000000", - "flavor": {"id": "2186bd79-a05e-4953-9dde-ddefb63c88d4"}, - "id": "f80e3ad0-e13e-41d4-8e9c-be79bccdb8f7", - "security_groups": [{"name": "default"}], - "OS-SRV-USG:terminated_at": None, - "OS-EXT-AZ:availability_zone": "nova", - "user_id": "c17534835f8f42bf98fc367e0bf35e09", - "name": "testmt", - "created": "2017-02-06T20:59:44Z", - "tenant_id": "65222a4d09ea4c68934fa1028c77f394", - "OS-DCF:diskConfig": "MANUAL", - "os-extended-volumes:volumes_attached": [], - "accessIPv4": "", - "accessIPv6": "", - "progress": 0, - "OS-EXT-STS:power_state": 1, - "config_drive": "", - "metadata": {} - }]}), - dict(method='GET', - uri='https://network.example.com/v2.0/networks', - json={"networks": [{ - "status": "ACTIVE", - "subnets": [ - "df3e17fa-a4b2-47ae-9015-bc93eb076ba2", - "6b0c3dc9-b0b8-4d87-976a-7f2ebf13e7ec", - "fc541f48-fc7f-48c0-a063-18de6ee7bdd7"], - "availability_zone_hints": [], - "availability_zones": ["nova"], - "name": "ext-net", - "admin_state_up": True, - "tenant_id": "a564613210ee43708b8a7fc6274ebd63", - "tags": [], - "ipv6_address_scope": "9f03124f-89af-483a-b6fd-10f08079db4d", # noqa - "mtu": 0, - "is_default": False, - "router:external": True, - "ipv4_address_scope": None, - "shared": False, - "id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", - "description": None - }, { - "status": "ACTIVE", - "subnets": ["f0ad1df5-53ee-473f-b86b-3604ea5591e9"], - "availability_zone_hints": [], - "availability_zones": ["nova"], - "name": "private", - "admin_state_up": True, - "tenant_id": "65222a4d09ea4c68934fa1028c77f394", - "created_at": "2016-10-22T13:46:26", - "tags": [], - "updated_at": "2016-10-22T13:46:26", - "ipv6_address_scope": None, - "router:external": False, - "ipv4_address_scope": None, - "shared": False, - "mtu": 1450, - "id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", - "description": "" - }]}), - dict(method='GET', - uri='https://network.example.com/v2.0/subnets', - json={"subnets": [{ - "description": "", - "enable_dhcp": True, - "network_id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", - "tenant_id": "65222a4d09ea4c68934fa1028c77f394", - "created_at": "2016-10-22T13:46:26", - "dns_nameservers": [ - "89.36.90.101", - "89.36.90.102"], - "updated_at": "2016-10-22T13:46:26", - "gateway_ip": "10.4.0.1", - "ipv6_ra_mode": None, - "allocation_pools": [{ - "start": "10.4.0.2", - "end": "10.4.0.200"}], - "host_routes": [], - "ip_version": 4, - "ipv6_address_mode": None, - "cidr": "10.4.0.0/24", - "id": "f0ad1df5-53ee-473f-b86b-3604ea5591e9", - "subnetpool_id": None, - "name": "private-subnet-ipv4", - }]})]) + self.register_uris( + [ + dict( + method='GET', + uri='https://network.example.com/v2.0/networks/ext-net', + status_code=404, + ), + dict( + method='GET', + uri='https://network.example.com/v2.0/networks?name=ext-net', # noqa: E501 + json={ + "networks": [ + { + "status": "ACTIVE", + "subnets": [ + "df3e17fa-a4b2-47ae-9015-bc93eb076ba2", + "6b0c3dc9-b0b8-4d87-976a-7f2ebf13e7ec", + "fc541f48-fc7f-48c0-a063-18de6ee7bdd7", + ], + "availability_zone_hints": [], + "availability_zones": ["nova"], + "name": "ext-net", + "admin_state_up": True, + "tenant_id": "a564613210ee43708b8a7fc6274ebd63", # noqa: E501 + "tags": [], + "ipv6_address_scope": "9f03124f-89af-483a-b6fd-10f08079db4d", # noqa: E501 + "mtu": 0, + "is_default": False, + "router:external": True, + "ipv4_address_scope": None, + "shared": False, + "id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", + "description": None, + } + ] + }, + ), + dict( + method='GET', + uri='https://network.example.com/v2.0/ports' + '?device_id=f80e3ad0-e13e-41d4-8e9c-be79bccdb8f7', + json={ + "ports": [ + { + "status": "ACTIVE", + "created_at": "2017-02-06T20:59:45", + "description": "", + "allowed_address_pairs": [], + "admin_state_up": True, + "network_id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", # noqa: E501 + "dns_name": None, + "extra_dhcp_opts": [], + "mac_address": "fa:16:3e:e8:7f:03", + "updated_at": "2017-02-06T20:59:49", + "name": "", + "device_owner": "compute:None", + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", # noqa: E501 + "binding:vnic_type": "normal", + "fixed_ips": [ + { + "subnet_id": "f0ad1df5-53ee-473f-b86b-3604ea5591e9", # noqa: E501 + "ip_address": "10.4.0.16", + } + ], + "id": "a767944e-057a-47d1-a669-824a21b8fb7b", + "security_groups": [ + "9fb5ba44-5c46-4357-8e60-8b55526cab54" + ], + "device_id": "f80e3ad0-e13e-41d4-8e9c-be79bccdb8f7", # noqa: E501 + } + ] + }, + ), + dict( + method='POST', + uri='https://network.example.com/v2.0/floatingips', + json={ + "floatingip": { + "router_id": "9de9c787-8f89-4a53-8468-a5533d6d7fd1", # noqa: E501 + "status": "DOWN", + "description": "", + "dns_domain": "", + "floating_network_id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", # noqa: E501 + "fixed_ip_address": "10.4.0.16", + "floating_ip_address": "89.40.216.153", + "port_id": "a767944e-057a-47d1-a669-824a21b8fb7b", + "id": "e69179dc-a904-4c9a-a4c9-891e2ecb984c", + "dns_name": "", + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", + } + }, + validate=dict( + json={ + "floatingip": { + "floating_network_id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", # noqa: E501 + "fixed_ip_address": "10.4.0.16", + "port_id": "a767944e-057a-47d1-a669-824a21b8fb7b", # noqa: E501 + } + } + ), + ), + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri='{endpoint}/servers/detail'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={ + "servers": [ + { + "status": "ACTIVE", + "updated": "2017-02-06T20:59:49Z", + "addresses": { + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:e8:7f:03", # noqa: E501 + "version": 4, + "addr": "10.4.0.16", + "OS-EXT-IPS:type": "fixed", + }, + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:e8:7f:03", # noqa: E501 + "version": 4, + "addr": "89.40.216.153", + "OS-EXT-IPS:type": "floating", + }, + ] + }, + "key_name": None, + "image": { + "id": "95e4c449-8abf-486e-97d9-dc3f82417d2d" # noqa: E501 + }, + "OS-EXT-STS:task_state": None, + "OS-EXT-STS:vm_state": "active", + "OS-SRV-USG:launched_at": "2017-02-06T20:59:48.000000", # noqa: E501 + "flavor": { + "id": "2186bd79-a05e-4953-9dde-ddefb63c88d4" # noqa: E501 + }, + "id": "f80e3ad0-e13e-41d4-8e9c-be79bccdb8f7", + "security_groups": [{"name": "default"}], + "OS-SRV-USG:terminated_at": None, + "OS-EXT-AZ:availability_zone": "nova", + "user_id": "c17534835f8f42bf98fc367e0bf35e09", + "name": "testmt", + "created": "2017-02-06T20:59:44Z", + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", # noqa: E501 + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": {}, + } + ] + }, + ), + dict( + method='GET', + uri='https://network.example.com/v2.0/networks', + json={ + "networks": [ + { + "status": "ACTIVE", + "subnets": [ + "df3e17fa-a4b2-47ae-9015-bc93eb076ba2", + "6b0c3dc9-b0b8-4d87-976a-7f2ebf13e7ec", + "fc541f48-fc7f-48c0-a063-18de6ee7bdd7", + ], + "availability_zone_hints": [], + "availability_zones": ["nova"], + "name": "ext-net", + "admin_state_up": True, + "tenant_id": "a564613210ee43708b8a7fc6274ebd63", # noqa: E501 + "tags": [], + "ipv6_address_scope": "9f03124f-89af-483a-b6fd-10f08079db4d", # noqa: E501 + "mtu": 0, + "is_default": False, + "router:external": True, + "ipv4_address_scope": None, + "shared": False, + "id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", + "description": None, + }, + { + "status": "ACTIVE", + "subnets": [ + "f0ad1df5-53ee-473f-b86b-3604ea5591e9" + ], + "availability_zone_hints": [], + "availability_zones": ["nova"], + "name": "private", + "admin_state_up": True, + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", # noqa: E501 + "created_at": "2016-10-22T13:46:26", + "tags": [], + "updated_at": "2016-10-22T13:46:26", + "ipv6_address_scope": None, + "router:external": False, + "ipv4_address_scope": None, + "shared": False, + "mtu": 1450, + "id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", + "description": "", + }, + ] + }, + ), + dict( + method='GET', + uri='https://network.example.com/v2.0/subnets', + json={ + "subnets": [ + { + "description": "", + "enable_dhcp": True, + "network_id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", # noqa: E501 + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", # noqa: E501 + "created_at": "2016-10-22T13:46:26", + "dns_nameservers": [ + "89.36.90.101", + "89.36.90.102", + ], + "updated_at": "2016-10-22T13:46:26", + "gateway_ip": "10.4.0.1", + "ipv6_ra_mode": None, + "allocation_pools": [ + {"start": "10.4.0.2", "end": "10.4.0.200"} + ], + "host_routes": [], + "ip_version": 4, + "ipv6_address_mode": None, + "cidr": "10.4.0.0/24", + "id": "f0ad1df5-53ee-473f-b86b-3604ea5591e9", + "subnetpool_id": None, + "name": "private-subnet-ipv4", + } + ] + }, + ), + ] + ) self.cloud.add_ips_to_server( utils.Munch( id='f80e3ad0-e13e-41d4-8e9c-be79bccdb8f7', addresses={ - "private": [{ - "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:e8:7f:03", - "version": 4, - "addr": "10.4.0.16", - "OS-EXT-IPS:type": "fixed" - }]}), - ip_pool='ext-net', reuse=False) + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:e8:7f:03", + "version": 4, + "addr": "10.4.0.16", + "OS-EXT-IPS:type": "fixed", + } + ] + }, + ), + ip_pool='ext-net', + reuse=False, + ) self.assert_calls() def test_available_floating_ip_new(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': [self.mock_get_network_rep]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets']), - json={'subnets': []}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips']), - json={'floatingips': []}), - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips']), - validate=dict( - json={'floatingip': { - 'floating_network_id': 'my-network-id'}}), - json=self.mock_floating_ip_new_rep) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': [self.mock_get_network_rep]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets'] + ), + json={'subnets': []}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips'] + ), + json={'floatingips': []}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips'] + ), + validate=dict( + json={ + 'floatingip': { + 'floating_network_id': 'my-network-id' + } + } + ), + json=self.mock_floating_ip_new_rep, + ), + ] + ) ip = self.cloud.available_floating_ip(network='my-network') self.assertEqual( self.mock_floating_ip_new_rep['floatingip']['floating_ip_address'], - ip['floating_ip_address']) + ip['floating_ip_address'], + ) self.assert_calls() def test_delete_floating_ip_existing(self): @@ -619,38 +822,62 @@ def test_delete_floating_ip_existing(self): 'floating_ip_address': '172.99.106.167', 'status': 'ACTIVE', } - self.register_uris([ - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'floatingips/{0}'.format(fip_id)]), - json={}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips']), - json={'floatingips': [fake_fip]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'floatingips/{0}'.format(fip_id)]), - json={}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips']), - json={'floatingips': [fake_fip]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'floatingips/{0}'.format(fip_id)]), - json={}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips']), - json={'floatingips': []}), - ]) + self.register_uris( + [ + dict( + method='DELETE', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'floatingips/{0}'.format(fip_id)], + ), + json={}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips'] + ), + json={'floatingips': [fake_fip]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'floatingips/{0}'.format(fip_id)], + ), + json={}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips'] + ), + json={'floatingips': [fake_fip]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'floatingips/{0}'.format(fip_id)], + ), + json={}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips'] + ), + json={'floatingips': []}, + ), + ] + ) self.assertTrue( - self.cloud.delete_floating_ip(floating_ip_id=fip_id, retry=2)) + self.cloud.delete_floating_ip(floating_ip_id=fip_id, retry=2) + ) self.assert_calls() def test_delete_floating_ip_existing_down(self): @@ -665,29 +892,46 @@ def test_delete_floating_ip_existing_down(self): 'floating_ip_address': '172.99.106.167', 'status': 'DOWN', } - self.register_uris([ - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'floatingips/{0}'.format(fip_id)]), - json={}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips']), - json={'floatingips': [fake_fip]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'floatingips/{0}'.format(fip_id)]), - json={}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips']), - json={'floatingips': [down_fip]}), - ]) + self.register_uris( + [ + dict( + method='DELETE', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'floatingips/{0}'.format(fip_id)], + ), + json={}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips'] + ), + json={'floatingips': [fake_fip]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'floatingips/{0}'.format(fip_id)], + ), + json={}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips'] + ), + json={'floatingips': [down_fip]}, + ), + ] + ) self.assertTrue( - self.cloud.delete_floating_ip(floating_ip_id=fip_id, retry=2)) + self.cloud.delete_floating_ip(floating_ip_id=fip_id, retry=2) + ) self.assert_calls() def test_delete_floating_ip_existing_no_delete(self): @@ -697,50 +941,81 @@ def test_delete_floating_ip_existing_no_delete(self): 'floating_ip_address': '172.99.106.167', 'status': 'ACTIVE', } - self.register_uris([ - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'floatingips/{0}'.format(fip_id)]), - json={}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips']), - json={'floatingips': [fake_fip]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'floatingips/{0}'.format(fip_id)]), - json={}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips']), - json={'floatingips': [fake_fip]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'floatingips/{0}'.format(fip_id)]), - json={}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips']), - json={'floatingips': [fake_fip]}), - ]) + self.register_uris( + [ + dict( + method='DELETE', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'floatingips/{0}'.format(fip_id)], + ), + json={}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips'] + ), + json={'floatingips': [fake_fip]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'floatingips/{0}'.format(fip_id)], + ), + json={}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips'] + ), + json={'floatingips': [fake_fip]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'floatingips/{0}'.format(fip_id)], + ), + json={}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips'] + ), + json={'floatingips': [fake_fip]}, + ), + ] + ) self.assertRaises( exc.OpenStackCloudException, self.cloud.delete_floating_ip, - floating_ip_id=fip_id, retry=2) + floating_ip_id=fip_id, + retry=2, + ) self.assert_calls() def test_delete_floating_ip_not_found(self): - self.register_uris([ - dict(method='DELETE', - uri=('https://network.example.com/v2.0/floatingips/' - 'a-wild-id-appears'), - status_code=404)]) + self.register_uris( + [ + dict( + method='DELETE', + uri=( + 'https://network.example.com/v2.0/floatingips/' + 'a-wild-id-appears' + ), + status_code=404, + ) + ] + ) - ret = self.cloud.delete_floating_ip( - floating_ip_id='a-wild-id-appears') + ret = self.cloud.delete_floating_ip(floating_ip_id='a-wild-id-appears') self.assertFalse(ret) self.assert_calls() @@ -750,52 +1025,78 @@ def test_attach_ip_to_server(self): fip.update({'status': 'DOWN', 'port_id': None, 'router_id': None}) device_id = self.fake_server['id'] - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports'], - qs_elements=["device_id={0}".format(device_id)]), - json={'ports': self.mock_search_ports_rep}), - dict(method='PUT', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'floatingips/{0}'.format( - fip['id'])]), - json={'floatingip': - self.mock_floating_ip_list_rep['floatingips'][0]}, - validate=dict( - json={'floatingip': { - 'port_id': self.mock_search_ports_rep[0]['id'], - 'fixed_ip_address': self.mock_search_ports_rep[0][ - 'fixed_ips'][0]['ip_address']}})), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'ports'], + qs_elements=["device_id={0}".format(device_id)], + ), + json={'ports': self.mock_search_ports_rep}, + ), + dict( + method='PUT', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'floatingips/{0}'.format(fip['id'])], + ), + json={ + 'floatingip': self.mock_floating_ip_list_rep[ + 'floatingips' + ][0] + }, + validate=dict( + json={ + 'floatingip': { + 'port_id': self.mock_search_ports_rep[0]['id'], + 'fixed_ip_address': self.mock_search_ports_rep[ + 0 + ]['fixed_ips'][0]['ip_address'], + } + } + ), + ), + ] + ) self.cloud._attach_ip_to_server( server=self.fake_server, - floating_ip=self.cloud._normalize_floating_ip(fip)) + floating_ip=self.cloud._normalize_floating_ip(fip), + ) self.assert_calls() def test_detach_ip_from_server(self): fip = self.mock_floating_ip_new_rep['floatingip'] attached_fip = copy.copy(fip) attached_fip['port_id'] = 'server-port-id' - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips']), - json={'floatingips': [attached_fip]}), - dict(method='PUT', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'floatingips/{0}'.format( - fip['id'])]), - json={'floatingip': fip}, - validate=dict( - json={'floatingip': {'port_id': None}})) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips'] + ), + json={'floatingips': [attached_fip]}, + ), + dict( + method='PUT', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'floatingips/{0}'.format(fip['id'])], + ), + json={'floatingip': fip}, + validate=dict(json={'floatingip': {'port_id': None}}), + ), + ] + ) self.cloud.detach_ip_from_server( - server_id='server-id', - floating_ip_id=fip['id']) + server_id='server-id', floating_ip_id=fip['id'] + ) self.assert_calls() def test_add_ip_from_pool(self): @@ -803,106 +1104,165 @@ def test_add_ip_from_pool(self): fip = self.mock_floating_ip_new_rep['floatingip'] fixed_ip = self.mock_search_ports_rep[0]['fixed_ips'][0]['ip_address'] port_id = self.mock_search_ports_rep[0]['id'] - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': [network]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets']), - json={'subnets': []}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips']), - json={'floatingips': [fip]}), - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips']), - json={'floatingip': fip}, - validate=dict( - json={'floatingip': { - 'floating_network_id': network['id']}})), - dict(method="GET", - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports'], - qs_elements=[ - "device_id={0}".format(self.fake_server['id'])]), - json={'ports': self.mock_search_ports_rep}), - dict(method='PUT', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'floatingips/{0}'.format( - fip['id'])]), - json={'floatingip': fip}, - validate=dict( - json={'floatingip': { - 'fixed_ip_address': fixed_ip, - 'port_id': port_id}})), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': [network]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets'] + ), + json={'subnets': []}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips'] + ), + json={'floatingips': [fip]}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips'] + ), + json={'floatingip': fip}, + validate=dict( + json={ + 'floatingip': { + 'floating_network_id': network['id'] + } + } + ), + ), + dict( + method="GET", + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'ports'], + qs_elements=[ + "device_id={0}".format(self.fake_server['id']) + ], + ), + json={'ports': self.mock_search_ports_rep}, + ), + dict( + method='PUT', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'floatingips/{0}'.format(fip['id'])], + ), + json={'floatingip': fip}, + validate=dict( + json={ + 'floatingip': { + 'fixed_ip_address': fixed_ip, + 'port_id': port_id, + } + } + ), + ), + ] + ) server = self.cloud._add_ip_from_pool( server=self.fake_server, network=network['id'], - fixed_address=fixed_ip) + fixed_address=fixed_ip, + ) self.assertEqual(server, self.fake_server) self.assert_calls() def test_cleanup_floating_ips(self): - floating_ips = [{ - "id": "this-is-a-floating-ip-id", - "fixed_ip_address": None, - "internal_network": None, - "floating_ip_address": "203.0.113.29", - "network": "this-is-a-net-or-pool-id", - "port_id": None, - "status": "ACTIVE" - }, { - "id": "this-is-a-second-floating-ip-id", - "fixed_ip_address": None, - "internal_network": None, - "floating_ip_address": "203.0.113.30", - "network": "this-is-a-net-or-pool-id", - "port_id": None, - "status": "ACTIVE" - }, { - "id": "this-is-an-attached-floating-ip-id", - "fixed_ip_address": None, - "internal_network": None, - "floating_ip_address": "203.0.113.29", - "network": "this-is-a-net-or-pool-id", - "attached": True, - "port_id": "this-is-id-of-port-with-fip", - "status": "ACTIVE" - }] - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips']), - json={'floatingips': floating_ips}), - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'floatingips/{0}'.format( - floating_ips[0]['id'])]), - json={}), - # First IP has been deleted now, return just the second - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips']), - json={'floatingips': floating_ips[1:]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'floatingips/{0}'.format( - floating_ips[1]['id'])]), - json={}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips']), - json={'floatingips': [floating_ips[2]]}), - ]) + floating_ips = [ + { + "id": "this-is-a-floating-ip-id", + "fixed_ip_address": None, + "internal_network": None, + "floating_ip_address": "203.0.113.29", + "network": "this-is-a-net-or-pool-id", + "port_id": None, + "status": "ACTIVE", + }, + { + "id": "this-is-a-second-floating-ip-id", + "fixed_ip_address": None, + "internal_network": None, + "floating_ip_address": "203.0.113.30", + "network": "this-is-a-net-or-pool-id", + "port_id": None, + "status": "ACTIVE", + }, + { + "id": "this-is-an-attached-floating-ip-id", + "fixed_ip_address": None, + "internal_network": None, + "floating_ip_address": "203.0.113.29", + "network": "this-is-a-net-or-pool-id", + "attached": True, + "port_id": "this-is-id-of-port-with-fip", + "status": "ACTIVE", + }, + ] + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips'] + ), + json={'floatingips': floating_ips}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'floatingips/{0}'.format(floating_ips[0]['id']), + ], + ), + json={}, + ), + # First IP has been deleted now, return just the second + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips'] + ), + json={'floatingips': floating_ips[1:]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'floatingips/{0}'.format(floating_ips[1]['id']), + ], + ), + json={}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips'] + ), + json={'floatingips': [floating_ips[2]]}, + ), + ] + ) cleaned_up = self.cloud.delete_unattached_floating_ips() self.assertEqual(cleaned_up, 2) self.assert_calls() @@ -913,135 +1273,166 @@ def test_create_floating_ip_no_port(self): "device_id": "some-server", 'created_at': datetime.datetime.now().isoformat(), 'fixed_ips': [ - { - 'subnet_id': 'subnet-id', - 'ip_address': '172.24.4.2' - } + {'subnet_id': 'subnet-id', 'ip_address': '172.24.4.2'} ], } - floating_ip = { - "id": "floating-ip-id", - "port_id": None - } - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': [self.mock_get_network_rep]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets']), - json={'subnets': []}), - dict(method="GET", - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports'], - qs_elements=['device_id=some-server']), - json={'ports': [server_port]}), - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'floatingips']), - json={'floatingip': floating_ip}) - ]) + floating_ip = {"id": "floating-ip-id", "port_id": None} + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': [self.mock_get_network_rep]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets'] + ), + json={'subnets': []}, + ), + dict( + method="GET", + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'ports'], + qs_elements=['device_id=some-server'], + ), + json={'ports': [server_port]}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'floatingips'] + ), + json={'floatingip': floating_ip}, + ), + ] + ) self.assertRaises( exc.OpenStackCloudException, self.cloud._neutron_create_floating_ip, - server=dict(id='some-server')) + server=dict(id='some-server'), + ) self.assert_calls() def test_find_nat_source_inferred(self): # payloads contrived but based on ones from citycloud - self.register_uris([ - dict(method='GET', - uri='https://network.example.com/v2.0/networks', - json={"networks": [{ - "status": "ACTIVE", - "subnets": [ - "df3e17fa-a4b2-47ae-9015-bc93eb076ba2", - "6b0c3dc9-b0b8-4d87-976a-7f2ebf13e7ec", - "fc541f48-fc7f-48c0-a063-18de6ee7bdd7"], - "availability_zone_hints": [], - "availability_zones": ["nova"], - "name": "ext-net", - "admin_state_up": True, - "tenant_id": "a564613210ee43708b8a7fc6274ebd63", - "tags": [], - "ipv6_address_scope": "9f03124f-89af-483a-b6fd-10f08079db4d", # noqa - "mtu": 0, - "is_default": False, - "router:external": True, - "ipv4_address_scope": None, - "shared": False, - "id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", - "description": None - }, { - "status": "ACTIVE", - "subnets": [ - "df3e17fa-a4b2-47ae-9015-bc93eb076ba2", - "6b0c3dc9-b0b8-4d87-976a-7f2ebf13e7ec", - "fc541f48-fc7f-48c0-a063-18de6ee7bdd7"], - "availability_zone_hints": [], - "availability_zones": ["nova"], - "name": "my-network", - "admin_state_up": True, - "tenant_id": "a564613210ee43708b8a7fc6274ebd63", - "tags": [], - "ipv6_address_scope": "9f03124f-89af-483a-b6fd-10f08079db4d", # noqa - "mtu": 0, - "is_default": False, - "router:external": True, - "ipv4_address_scope": None, - "shared": False, - "id": "0232c17f-2096-49bc-b205-d3dcd9a30ebg", - "description": None - }, { - "status": "ACTIVE", - "subnets": ["f0ad1df5-53ee-473f-b86b-3604ea5591e9"], - "availability_zone_hints": [], - "availability_zones": ["nova"], - "name": "private", - "admin_state_up": True, - "tenant_id": "65222a4d09ea4c68934fa1028c77f394", - "created_at": "2016-10-22T13:46:26", - "tags": [], - "updated_at": "2016-10-22T13:46:26", - "ipv6_address_scope": None, - "router:external": False, - "ipv4_address_scope": None, - "shared": False, - "mtu": 1450, - "id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", - "description": "" - }]}), - dict(method='GET', - uri='https://network.example.com/v2.0/subnets', - json={"subnets": [{ - "description": "", - "enable_dhcp": True, - "network_id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", - "tenant_id": "65222a4d09ea4c68934fa1028c77f394", - "created_at": "2016-10-22T13:46:26", - "dns_nameservers": [ - "89.36.90.101", - "89.36.90.102"], - "updated_at": "2016-10-22T13:46:26", - "gateway_ip": "10.4.0.1", - "ipv6_ra_mode": None, - "allocation_pools": [{ - "start": "10.4.0.2", - "end": "10.4.0.200"}], - "host_routes": [], - "ip_version": 4, - "ipv6_address_mode": None, - "cidr": "10.4.0.0/24", - "id": "f0ad1df5-53ee-473f-b86b-3604ea5591e9", - "subnetpool_id": None, - "name": "private-subnet-ipv4", - }]}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://network.example.com/v2.0/networks', + json={ + "networks": [ + { + "status": "ACTIVE", + "subnets": [ + "df3e17fa-a4b2-47ae-9015-bc93eb076ba2", + "6b0c3dc9-b0b8-4d87-976a-7f2ebf13e7ec", + "fc541f48-fc7f-48c0-a063-18de6ee7bdd7", + ], + "availability_zone_hints": [], + "availability_zones": ["nova"], + "name": "ext-net", + "admin_state_up": True, + "tenant_id": "a564613210ee43708b8a7fc6274ebd63", # noqa: E501 + "tags": [], + "ipv6_address_scope": "9f03124f-89af-483a-b6fd-10f08079db4d", # noqa: E501 + "mtu": 0, + "is_default": False, + "router:external": True, + "ipv4_address_scope": None, + "shared": False, + "id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", + "description": None, + }, + { + "status": "ACTIVE", + "subnets": [ + "df3e17fa-a4b2-47ae-9015-bc93eb076ba2", + "6b0c3dc9-b0b8-4d87-976a-7f2ebf13e7ec", + "fc541f48-fc7f-48c0-a063-18de6ee7bdd7", + ], + "availability_zone_hints": [], + "availability_zones": ["nova"], + "name": "my-network", + "admin_state_up": True, + "tenant_id": "a564613210ee43708b8a7fc6274ebd63", # noqa: E501 + "tags": [], + "ipv6_address_scope": "9f03124f-89af-483a-b6fd-10f08079db4d", # noqa: E501 + "mtu": 0, + "is_default": False, + "router:external": True, + "ipv4_address_scope": None, + "shared": False, + "id": "0232c17f-2096-49bc-b205-d3dcd9a30ebg", + "description": None, + }, + { + "status": "ACTIVE", + "subnets": [ + "f0ad1df5-53ee-473f-b86b-3604ea5591e9" + ], + "availability_zone_hints": [], + "availability_zones": ["nova"], + "name": "private", + "admin_state_up": True, + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", # noqa: E501 + "created_at": "2016-10-22T13:46:26", + "tags": [], + "updated_at": "2016-10-22T13:46:26", + "ipv6_address_scope": None, + "router:external": False, + "ipv4_address_scope": None, + "shared": False, + "mtu": 1450, + "id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", + "description": "", + }, + ] + }, + ), + dict( + method='GET', + uri='https://network.example.com/v2.0/subnets', + json={ + "subnets": [ + { + "description": "", + "enable_dhcp": True, + "network_id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", # noqa: E501 + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", # noqa: E501 + "created_at": "2016-10-22T13:46:26", + "dns_nameservers": [ + "89.36.90.101", + "89.36.90.102", + ], + "updated_at": "2016-10-22T13:46:26", + "gateway_ip": "10.4.0.1", + "ipv6_ra_mode": None, + "allocation_pools": [ + {"start": "10.4.0.2", "end": "10.4.0.200"} + ], + "host_routes": [], + "ip_version": 4, + "ipv6_address_mode": None, + "cidr": "10.4.0.0/24", + "id": "f0ad1df5-53ee-473f-b86b-3604ea5591e9", + "subnetpool_id": None, + "name": "private-subnet-ipv4", + } + ] + }, + ), + ] + ) - self.assertEqual( - 'ext-net', self.cloud.get_nat_source()['name']) + self.assertEqual('ext-net', self.cloud.get_nat_source()['name']) self.assert_calls() @@ -1049,96 +1440,116 @@ def test_find_nat_source_config(self): self.cloud._nat_source = 'my-network' # payloads contrived but based on ones from citycloud - self.register_uris([ - dict(method='GET', - uri='https://network.example.com/v2.0/networks', - json={"networks": [{ - "status": "ACTIVE", - "subnets": [ - "df3e17fa-a4b2-47ae-9015-bc93eb076ba2", - "6b0c3dc9-b0b8-4d87-976a-7f2ebf13e7ec", - "fc541f48-fc7f-48c0-a063-18de6ee7bdd7"], - "availability_zone_hints": [], - "availability_zones": ["nova"], - "name": "ext-net", - "admin_state_up": True, - "tenant_id": "a564613210ee43708b8a7fc6274ebd63", - "tags": [], - "ipv6_address_scope": "9f03124f-89af-483a-b6fd-10f08079db4d", # noqa - "mtu": 0, - "is_default": False, - "router:external": True, - "ipv4_address_scope": None, - "shared": False, - "id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", - "description": None - }, { - "status": "ACTIVE", - "subnets": [ - "df3e17fa-a4b2-47ae-9015-bc93eb076ba2", - "6b0c3dc9-b0b8-4d87-976a-7f2ebf13e7ec", - "fc541f48-fc7f-48c0-a063-18de6ee7bdd7"], - "availability_zone_hints": [], - "availability_zones": ["nova"], - "name": "my-network", - "admin_state_up": True, - "tenant_id": "a564613210ee43708b8a7fc6274ebd63", - "tags": [], - "ipv6_address_scope": "9f03124f-89af-483a-b6fd-10f08079db4d", # noqa - "mtu": 0, - "is_default": False, - "router:external": True, - "ipv4_address_scope": None, - "shared": False, - "id": "0232c17f-2096-49bc-b205-d3dcd9a30ebg", - "description": None - }, { - "status": "ACTIVE", - "subnets": ["f0ad1df5-53ee-473f-b86b-3604ea5591e9"], - "availability_zone_hints": [], - "availability_zones": ["nova"], - "name": "private", - "admin_state_up": True, - "tenant_id": "65222a4d09ea4c68934fa1028c77f394", - "created_at": "2016-10-22T13:46:26", - "tags": [], - "updated_at": "2016-10-22T13:46:26", - "ipv6_address_scope": None, - "router:external": False, - "ipv4_address_scope": None, - "shared": False, - "mtu": 1450, - "id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", - "description": "" - }]}), - dict(method='GET', - uri='https://network.example.com/v2.0/subnets', - json={"subnets": [{ - "description": "", - "enable_dhcp": True, - "network_id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", - "tenant_id": "65222a4d09ea4c68934fa1028c77f394", - "created_at": "2016-10-22T13:46:26", - "dns_nameservers": [ - "89.36.90.101", - "89.36.90.102"], - "updated_at": "2016-10-22T13:46:26", - "gateway_ip": "10.4.0.1", - "ipv6_ra_mode": None, - "allocation_pools": [{ - "start": "10.4.0.2", - "end": "10.4.0.200"}], - "host_routes": [], - "ip_version": 4, - "ipv6_address_mode": None, - "cidr": "10.4.0.0/24", - "id": "f0ad1df5-53ee-473f-b86b-3604ea5591e9", - "subnetpool_id": None, - "name": "private-subnet-ipv4", - }]}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://network.example.com/v2.0/networks', + json={ + "networks": [ + { + "status": "ACTIVE", + "subnets": [ + "df3e17fa-a4b2-47ae-9015-bc93eb076ba2", + "6b0c3dc9-b0b8-4d87-976a-7f2ebf13e7ec", + "fc541f48-fc7f-48c0-a063-18de6ee7bdd7", + ], + "availability_zone_hints": [], + "availability_zones": ["nova"], + "name": "ext-net", + "admin_state_up": True, + "tenant_id": "a564613210ee43708b8a7fc6274ebd63", # noqa: E501 + "tags": [], + "ipv6_address_scope": "9f03124f-89af-483a-b6fd-10f08079db4d", # noqa: E501 + "mtu": 0, + "is_default": False, + "router:external": True, + "ipv4_address_scope": None, + "shared": False, + "id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", + "description": None, + }, + { + "status": "ACTIVE", + "subnets": [ + "df3e17fa-a4b2-47ae-9015-bc93eb076ba2", + "6b0c3dc9-b0b8-4d87-976a-7f2ebf13e7ec", + "fc541f48-fc7f-48c0-a063-18de6ee7bdd7", + ], + "availability_zone_hints": [], + "availability_zones": ["nova"], + "name": "my-network", + "admin_state_up": True, + "tenant_id": "a564613210ee43708b8a7fc6274ebd63", # noqa: E501 + "tags": [], + "ipv6_address_scope": "9f03124f-89af-483a-b6fd-10f08079db4d", # noqa: E501 + "mtu": 0, + "is_default": False, + "router:external": True, + "ipv4_address_scope": None, + "shared": False, + "id": "0232c17f-2096-49bc-b205-d3dcd9a30ebg", + "description": None, + }, + { + "status": "ACTIVE", + "subnets": [ + "f0ad1df5-53ee-473f-b86b-3604ea5591e9" + ], + "availability_zone_hints": [], + "availability_zones": ["nova"], + "name": "private", + "admin_state_up": True, + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", # noqa: E501 + "created_at": "2016-10-22T13:46:26", + "tags": [], + "updated_at": "2016-10-22T13:46:26", + "ipv6_address_scope": None, + "router:external": False, + "ipv4_address_scope": None, + "shared": False, + "mtu": 1450, + "id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", + "description": "", + }, + ] + }, + ), + dict( + method='GET', + uri='https://network.example.com/v2.0/subnets', + json={ + "subnets": [ + { + "description": "", + "enable_dhcp": True, + "network_id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", # noqa: E501 + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", # noqa: E501 + "created_at": "2016-10-22T13:46:26", + "dns_nameservers": [ + "89.36.90.101", + "89.36.90.102", + ], + "updated_at": "2016-10-22T13:46:26", + "gateway_ip": "10.4.0.1", + "ipv6_ra_mode": None, + "allocation_pools": [ + {"start": "10.4.0.2", "end": "10.4.0.200"} + ], + "host_routes": [], + "ip_version": 4, + "ipv6_address_mode": None, + "cidr": "10.4.0.0/24", + "id": "f0ad1df5-53ee-473f-b86b-3604ea5591e9", + "subnetpool_id": None, + "name": "private-subnet-ipv4", + } + ] + }, + ), + ] + ) - self.assertEqual( - 'my-network', self.cloud.get_nat_source()['name']) + self.assertEqual('my-network', self.cloud.get_nat_source()['name']) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_floating_ip_nova.py b/openstack/tests/unit/cloud/test_floating_ip_nova.py index 7d49e19d8..406d504f6 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_nova.py +++ b/openstack/tests/unit/cloud/test_floating_ip_nova.py @@ -28,6 +28,7 @@ def fake_has_service(s): if s == 'network': return False return has_service(s) + return fake_has_service @@ -38,27 +39,28 @@ class TestFloatingIP(base.TestCase): 'id': 1, 'instance_id': None, 'ip': '203.0.113.1', - 'pool': 'nova' + 'pool': 'nova', }, { 'fixed_ip': None, 'id': 2, 'instance_id': None, 'ip': '203.0.113.2', - 'pool': 'nova' + 'pool': 'nova', }, { 'fixed_ip': '192.0.2.3', 'id': 29, 'instance_id': 'myself', 'ip': '198.51.100.29', - 'pool': 'black_hole' - } + 'pool': 'black_hole', + }, ] mock_floating_ip_pools = [ {'id': 'pool1_id', 'name': 'nova'}, - {'id': 'pool2_id', 'name': 'pool2'}] + {'id': 'pool2_id', 'name': 'pool2'}, + ] def assertAreInstances(self, elements, elem_type): for e in elements: @@ -68,23 +70,36 @@ def setUp(self): super(TestFloatingIP, self).setUp() self.fake_server = fakes.make_fake_server( - 'server-id', '', 'ACTIVE', - addresses={u'test_pnztt_net': [{ - u'OS-EXT-IPS:type': u'fixed', - u'addr': '192.0.2.129', - u'version': 4, - u'OS-EXT-IPS-MAC:mac_addr': - u'fa:16:3e:ae:7d:42'}]}) + 'server-id', + '', + 'ACTIVE', + addresses={ + u'test_pnztt_net': [ + { + u'OS-EXT-IPS:type': u'fixed', + u'addr': '192.0.2.129', + u'version': 4, + u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:ae:7d:42', + } + ] + }, + ) self.cloud.has_service = get_fake_has_service(self.cloud.has_service) def test_list_floating_ips(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url('compute', append=['os-floating-ips']), - json={'floating_ips': self.mock_floating_ip_list_rep}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'compute', append=['os-floating-ips'] + ), + json={'floating_ips': self.mock_floating_ip_list_rep}, + ), + ] + ) floating_ips = self.cloud.list_floating_ips() self.assertIsInstance(floating_ips, list) @@ -95,19 +110,28 @@ def test_list_floating_ips(self): def test_list_floating_ips_with_filters(self): self.assertRaisesRegex( - ValueError, "Nova-network don't support server-side", - self.cloud.list_floating_ips, filters={'Foo': 42} + ValueError, + "Nova-network don't support server-side", + self.cloud.list_floating_ips, + filters={'Foo': 42}, ) def test_search_floating_ips(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url('compute', append=['os-floating-ips']), - json={'floating_ips': self.mock_floating_ip_list_rep}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'compute', append=['os-floating-ips'] + ), + json={'floating_ips': self.mock_floating_ip_list_rep}, + ), + ] + ) floating_ips = self.cloud.search_floating_ips( - filters={'attached': False}) + filters={'attached': False} + ) self.assertIsInstance(floating_ips, list) self.assertEqual(2, len(floating_ips)) @@ -116,11 +140,17 @@ def test_search_floating_ips(self): self.assert_calls() def test_get_floating_ip(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url('compute', append=['os-floating-ips']), - json={'floating_ips': self.mock_floating_ip_list_rep}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'compute', append=['os-floating-ips'] + ), + json={'floating_ips': self.mock_floating_ip_list_rep}, + ), + ] + ) floating_ip = self.cloud.get_floating_ip(id='29') @@ -130,11 +160,17 @@ def test_get_floating_ip(self): self.assert_calls() def test_get_floating_ip_not_found(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url('compute', append=['os-floating-ips']), - json={'floating_ips': self.mock_floating_ip_list_rep}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'compute', append=['os-floating-ips'] + ), + json={'floating_ips': self.mock_floating_ip_list_rep}, + ), + ] + ) floating_ip = self.cloud.get_floating_ip(id='666') @@ -143,12 +179,17 @@ def test_get_floating_ip_not_found(self): self.assert_calls() def test_get_floating_ip_by_id(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url('compute', append=['os-floating-ips', - '1']), - json={'floating_ip': self.mock_floating_ip_list_rep[0]}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'compute', append=['os-floating-ips', '1'] + ), + json={'floating_ip': self.mock_floating_ip_list_rep[0]}, + ), + ] + ) floating_ip = self.cloud.get_floating_ip_by_id(id='1') @@ -157,161 +198,240 @@ def test_get_floating_ip_by_id(self): self.assert_calls() def test_create_floating_ip(self): - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url('compute', append=['os-floating-ips']), - json={'floating_ip': self.mock_floating_ip_list_rep[1]}, - validate=dict( - json={'pool': 'nova'})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', - append=['os-floating-ips', '2']), - json={'floating_ip': self.mock_floating_ip_list_rep[1]}), - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'compute', append=['os-floating-ips'] + ), + json={'floating_ip': self.mock_floating_ip_list_rep[1]}, + validate=dict(json={'pool': 'nova'}), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', append=['os-floating-ips', '2'] + ), + json={'floating_ip': self.mock_floating_ip_list_rep[1]}, + ), + ] + ) self.cloud.create_floating_ip(network='nova') self.assert_calls() def test_available_floating_ip_existing(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url('compute', append=['os-floating-ips']), - json={'floating_ips': self.mock_floating_ip_list_rep[:1]}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'compute', append=['os-floating-ips'] + ), + json={'floating_ips': self.mock_floating_ip_list_rep[:1]}, + ), + ] + ) ip = self.cloud.available_floating_ip(network='nova') - self.assertEqual(self.mock_floating_ip_list_rep[0]['ip'], - ip['floating_ip_address']) + self.assertEqual( + self.mock_floating_ip_list_rep[0]['ip'], ip['floating_ip_address'] + ) self.assert_calls() def test_available_floating_ip_new(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url('compute', append=['os-floating-ips']), - json={'floating_ips': []}), - dict(method='POST', - uri=self.get_mock_url('compute', append=['os-floating-ips']), - json={'floating_ip': self.mock_floating_ip_list_rep[0]}, - validate=dict( - json={'pool': 'nova'})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', - append=['os-floating-ips', '1']), - json={'floating_ip': self.mock_floating_ip_list_rep[0]}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'compute', append=['os-floating-ips'] + ), + json={'floating_ips': []}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', append=['os-floating-ips'] + ), + json={'floating_ip': self.mock_floating_ip_list_rep[0]}, + validate=dict(json={'pool': 'nova'}), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', append=['os-floating-ips', '1'] + ), + json={'floating_ip': self.mock_floating_ip_list_rep[0]}, + ), + ] + ) ip = self.cloud.available_floating_ip(network='nova') - self.assertEqual(self.mock_floating_ip_list_rep[0]['ip'], - ip['floating_ip_address']) + self.assertEqual( + self.mock_floating_ip_list_rep[0]['ip'], ip['floating_ip_address'] + ) self.assert_calls() def test_delete_floating_ip_existing(self): - self.register_uris([ - dict(method='DELETE', - uri=self.get_mock_url( - 'compute', - append=['os-floating-ips', 'a-wild-id-appears'])), - dict(method='GET', - uri=self.get_mock_url('compute', append=['os-floating-ips']), - json={'floating_ips': []}), - ]) + self.register_uris( + [ + dict( + method='DELETE', + uri=self.get_mock_url( + 'compute', + append=['os-floating-ips', 'a-wild-id-appears'], + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', append=['os-floating-ips'] + ), + json={'floating_ips': []}, + ), + ] + ) - ret = self.cloud.delete_floating_ip( - floating_ip_id='a-wild-id-appears') + ret = self.cloud.delete_floating_ip(floating_ip_id='a-wild-id-appears') self.assertTrue(ret) self.assert_calls() def test_delete_floating_ip_not_found(self): - self.register_uris([ - dict(method='DELETE', - uri=self.get_mock_url( - 'compute', - append=['os-floating-ips', 'a-wild-id-appears']), - status_code=404), - ]) + self.register_uris( + [ + dict( + method='DELETE', + uri=self.get_mock_url( + 'compute', + append=['os-floating-ips', 'a-wild-id-appears'], + ), + status_code=404, + ), + ] + ) - ret = self.cloud.delete_floating_ip( - floating_ip_id='a-wild-id-appears') + ret = self.cloud.delete_floating_ip(floating_ip_id='a-wild-id-appears') self.assertFalse(ret) self.assert_calls() def test_attach_ip_to_server(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url('compute', append=['os-floating-ips']), - json={'floating_ips': self.mock_floating_ip_list_rep}), - dict(method='POST', - uri=self.get_mock_url( - 'compute', - append=['servers', self.fake_server['id'], 'action']), - validate=dict( - json={ - "addFloatingIp": { - "address": "203.0.113.1", - "fixed_address": "192.0.2.129", - }})), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'compute', append=['os-floating-ips'] + ), + json={'floating_ips': self.mock_floating_ip_list_rep}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', + append=['servers', self.fake_server['id'], 'action'], + ), + validate=dict( + json={ + "addFloatingIp": { + "address": "203.0.113.1", + "fixed_address": "192.0.2.129", + } + } + ), + ), + ] + ) self.cloud._attach_ip_to_server( server=self.fake_server, floating_ip=self.cloud._normalize_floating_ip( - self.mock_floating_ip_list_rep[0]), - fixed_address='192.0.2.129') + self.mock_floating_ip_list_rep[0] + ), + fixed_address='192.0.2.129', + ) self.assert_calls() def test_detach_ip_from_server(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url('compute', append=['os-floating-ips']), - json={'floating_ips': self.mock_floating_ip_list_rep}), - dict(method='POST', - uri=self.get_mock_url( - 'compute', - append=['servers', self.fake_server['id'], 'action']), - validate=dict( - json={ - "removeFloatingIp": { - "address": "203.0.113.1", - }})), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'compute', append=['os-floating-ips'] + ), + json={'floating_ips': self.mock_floating_ip_list_rep}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', + append=['servers', self.fake_server['id'], 'action'], + ), + validate=dict( + json={ + "removeFloatingIp": { + "address": "203.0.113.1", + } + } + ), + ), + ] + ) self.cloud.detach_ip_from_server( - server_id='server-id', floating_ip_id=1) + server_id='server-id', floating_ip_id=1 + ) self.assert_calls() def test_add_ip_from_pool(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url('compute', append=['os-floating-ips']), - json={'floating_ips': self.mock_floating_ip_list_rep}), - dict(method='GET', - uri=self.get_mock_url('compute', append=['os-floating-ips']), - json={'floating_ips': self.mock_floating_ip_list_rep}), - dict(method='POST', - uri=self.get_mock_url( - 'compute', - append=['servers', self.fake_server['id'], 'action']), - validate=dict( - json={ - "addFloatingIp": { - "address": "203.0.113.1", - "fixed_address": "192.0.2.129", - }})), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'compute', append=['os-floating-ips'] + ), + json={'floating_ips': self.mock_floating_ip_list_rep}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', append=['os-floating-ips'] + ), + json={'floating_ips': self.mock_floating_ip_list_rep}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', + append=['servers', self.fake_server['id'], 'action'], + ), + validate=dict( + json={ + "addFloatingIp": { + "address": "203.0.113.1", + "fixed_address": "192.0.2.129", + } + } + ), + ), + ] + ) server = self.cloud._add_ip_from_pool( server=self.fake_server, network='nova', - fixed_address='192.0.2.129') + fixed_address='192.0.2.129', + ) self.assertEqual(server, self.fake_server) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_floating_ip_pool.py b/openstack/tests/unit/cloud/test_floating_ip_pool.py index 7e7132b05..6c4d87b0e 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_pool.py +++ b/openstack/tests/unit/cloud/test_floating_ip_pool.py @@ -25,27 +25,39 @@ class TestFloatingIPPool(base.TestCase): - pools = [{'name': u'public'}] + pools = [{'name': 'public'}] def test_list_floating_ip_pools(self): - self.register_uris([ - dict(method='GET', - uri='{endpoint}/extensions'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'extensions': [{ - u'alias': u'os-floating-ip-pools', - u'updated': u'2014-12-03T00:00:00Z', - u'name': u'FloatingIpPools', - u'links': [], - u'namespace': - u'http://docs.openstack.org/compute/ext/fake_xml', - u'description': u'Floating IPs support.'}]}), - dict(method='GET', - uri='{endpoint}/os-floating-ip-pools'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={"floating_ip_pools": [{"name": "public"}]}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/extensions'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={ + 'extensions': [ + { + 'alias': 'os-floating-ip-pools', + 'updated': '2014-12-03T00:00:00Z', + 'name': 'FloatingIpPools', + 'links': [], + 'namespace': 'http://docs.openstack.org/compute/ext/fake_xml', # noqa: E501 + 'description': 'Floating IPs support.', + } + ] + }, + ), + dict( + method='GET', + uri='{endpoint}/os-floating-ip-pools'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={"floating_ip_pools": [{"name": "public"}]}, + ), + ] + ) floating_ip_pools = self.cloud.list_floating_ip_pools() @@ -55,24 +67,38 @@ def test_list_floating_ip_pools(self): def test_list_floating_ip_pools_exception(self): - self.register_uris([ - dict(method='GET', - uri='{endpoint}/extensions'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'extensions': [{ - u'alias': u'os-floating-ip-pools', - u'updated': u'2014-12-03T00:00:00Z', - u'name': u'FloatingIpPools', - u'links': [], - u'namespace': - u'http://docs.openstack.org/compute/ext/fake_xml', - u'description': u'Floating IPs support.'}]}), - dict(method='GET', - uri='{endpoint}/os-floating-ip-pools'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - status_code=404)]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/extensions'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={ + 'extensions': [ + { + 'alias': 'os-floating-ip-pools', + 'updated': '2014-12-03T00:00:00Z', + 'name': 'FloatingIpPools', + 'links': [], + 'namespace': 'http://docs.openstack.org/compute/ext/fake_xml', # noqa: E501 + 'description': 'Floating IPs support.', + } + ] + }, + ), + dict( + method='GET', + uri='{endpoint}/os-floating-ip-pools'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + status_code=404, + ), + ] + ) self.assertRaises( - OpenStackCloudException, self.cloud.list_floating_ip_pools) + OpenStackCloudException, self.cloud.list_floating_ip_pools + ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_fwaas.py b/openstack/tests/unit/cloud/test_fwaas.py index 959b3d113..708e12d27 100644 --- a/openstack/tests/unit/cloud/test_fwaas.py +++ b/openstack/tests/unit/cloud/test_fwaas.py @@ -23,9 +23,12 @@ class FirewallTestCase(base.TestCase): def _make_mock_url(self, *args, **params): params_list = ['='.join([k, v]) for k, v in params.items()] - return self.get_mock_url('network', 'public', - append=['v2.0', 'fwaas'] + list(args), - qs_elements=params_list or None) + return self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'fwaas'] + list(args), + qs_elements=params_list or None, + ) class TestFirewallRule(FirewallTestCase): @@ -44,27 +47,31 @@ class TestFirewallRule(FirewallTestCase): 'protocol': 'tcp', 'shared': False, 'source_ip_address': None, - 'source_port': None + 'source_port': None, } mock_firewall_rule = None def setUp(self, cloud_config_fixture='clouds.yaml'): super(TestFirewallRule, self).setUp() self.mock_firewall_rule = FirewallRule( - connection=self.cloud, - **self._mock_firewall_rule_attrs).to_dict() + connection=self.cloud, **self._mock_firewall_rule_attrs + ).to_dict() def test_create_firewall_rule(self): # attributes that are passed to the tested function passed_attrs = self._mock_firewall_rule_attrs.copy() del passed_attrs['id'] - self.register_uris([ - # no validate due to added location key - dict(method='POST', - uri=self._make_mock_url('firewall_rules'), - json={'firewall_rule': self.mock_firewall_rule.copy()}) - ]) + self.register_uris( + [ + # no validate due to added location key + dict( + method='POST', + uri=self._make_mock_url('firewall_rules'), + json={'firewall_rule': self.mock_firewall_rule.copy()}, + ) + ] + ) r = self.cloud.create_firewall_rule(**passed_attrs) self.assertDictEqual(self.mock_firewall_rule, r.to_dict()) self.assert_calls() @@ -73,154 +80,241 @@ def test_create_firewall_rule_bad_protocol(self): bad_rule = self._mock_firewall_rule_attrs.copy() del bad_rule['id'] # id not allowed bad_rule['ip_version'] = 5 - self.register_uris([ - # no validate due to added location key - dict(method='POST', - uri=self._make_mock_url('firewall_rules'), - status_code=400, - json={}) - ]) - self.assertRaises(exceptions.BadRequestException, - self.cloud.create_firewall_rule, **bad_rule) + self.register_uris( + [ + # no validate due to added location key + dict( + method='POST', + uri=self._make_mock_url('firewall_rules'), + status_code=400, + json={}, + ) + ] + ) + self.assertRaises( + exceptions.BadRequestException, + self.cloud.create_firewall_rule, + **bad_rule + ) self.assert_calls() def test_delete_firewall_rule(self): - self.register_uris([ - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_rules', - self.firewall_rule_name), - status_code=404), - dict(method='GET', - uri=self._make_mock_url('firewall_rules', - name=self.firewall_rule_name), - json={'firewall_rules': [self.mock_firewall_rule]}), - dict(method='DELETE', - uri=self._make_mock_url('firewall_rules', - self.firewall_rule_id), - json={}, status_code=204) - ]) + self.register_uris( + [ + dict( + method='GET', # short-circuit + uri=self._make_mock_url( + 'firewall_rules', self.firewall_rule_name + ), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_rules', name=self.firewall_rule_name + ), + json={'firewall_rules': [self.mock_firewall_rule]}, + ), + dict( + method='DELETE', + uri=self._make_mock_url( + 'firewall_rules', self.firewall_rule_id + ), + json={}, + status_code=204, + ), + ] + ) self.assertTrue( - self.cloud.delete_firewall_rule(self.firewall_rule_name)) + self.cloud.delete_firewall_rule(self.firewall_rule_name) + ) self.assert_calls() def test_delete_firewall_rule_filters(self): filters = {'project_id': self.mock_firewall_rule['project_id']} - self.register_uris([ - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_rules', - self.firewall_rule_name, - **filters), - status_code=404), - dict(method='GET', - uri=self._make_mock_url( - 'firewall_rules', - name=self.firewall_rule_name, **filters), - json={'firewall_rules': [self.mock_firewall_rule]}, ), - dict(method='DELETE', - uri=self._make_mock_url('firewall_rules', - self.firewall_rule_id), - json={}, status_code=204), - ]) + self.register_uris( + [ + dict( + method='GET', # short-circuit + uri=self._make_mock_url( + 'firewall_rules', self.firewall_rule_name, **filters + ), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_rules', + name=self.firewall_rule_name, + **filters + ), + json={'firewall_rules': [self.mock_firewall_rule]}, + ), + dict( + method='DELETE', + uri=self._make_mock_url( + 'firewall_rules', self.firewall_rule_id + ), + json={}, + status_code=204, + ), + ] + ) self.assertTrue( - self.cloud.delete_firewall_rule(self.firewall_rule_name, filters)) + self.cloud.delete_firewall_rule(self.firewall_rule_name, filters) + ) self.assert_calls() def test_delete_firewall_rule_not_found(self): - self.register_uris([ - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_rules', - self.firewall_rule_name), - status_code=404), - dict(method='GET', - uri=self._make_mock_url('firewall_rules'), - json={'firewall_rules': []}) - ]) - - with mock.patch.object(self.cloud.network, 'delete_firewall_rule'), \ - mock.patch.object(self.cloud.log, 'debug'): + self.register_uris( + [ + dict( + method='GET', # short-circuit + uri=self._make_mock_url( + 'firewall_rules', self.firewall_rule_name + ), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url('firewall_rules'), + json={'firewall_rules': []}, + ), + ] + ) + + with mock.patch.object( + self.cloud.network, 'delete_firewall_rule' + ), mock.patch.object(self.cloud.log, 'debug'): self.assertFalse( - self.cloud.delete_firewall_rule(self.firewall_rule_name)) + self.cloud.delete_firewall_rule(self.firewall_rule_name) + ) self.cloud.network.delete_firewall_rule.assert_not_called() self.cloud.log.debug.assert_called_once() def test_delete_firewall_multiple_matches(self): - self.register_uris([ - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_rules', - self.firewall_rule_name), - status_code=404), - dict(method='GET', - uri=self._make_mock_url('firewall_rules', - name=self.firewall_rule_name), - json={'firewall_rules': [self.mock_firewall_rule, - self.mock_firewall_rule]}) - ]) - self.assertRaises(exceptions.DuplicateResource, - self.cloud.delete_firewall_rule, - self.firewall_rule_name) + self.register_uris( + [ + dict( + method='GET', # short-circuit + uri=self._make_mock_url( + 'firewall_rules', self.firewall_rule_name + ), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_rules', name=self.firewall_rule_name + ), + json={ + 'firewall_rules': [ + self.mock_firewall_rule, + self.mock_firewall_rule, + ] + }, + ), + ] + ) + self.assertRaises( + exceptions.DuplicateResource, + self.cloud.delete_firewall_rule, + self.firewall_rule_name, + ) self.assert_calls() def test_get_firewall_rule(self): - self.register_uris([ - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_rules', - self.firewall_rule_name), - status_code=404), - dict(method='GET', - uri=self._make_mock_url('firewall_rules', - name=self.firewall_rule_name), - json={'firewall_rules': [self.mock_firewall_rule]}) - ]) + self.register_uris( + [ + dict( + method='GET', # short-circuit + uri=self._make_mock_url( + 'firewall_rules', self.firewall_rule_name + ), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_rules', name=self.firewall_rule_name + ), + json={'firewall_rules': [self.mock_firewall_rule]}, + ), + ] + ) r = self.cloud.get_firewall_rule(self.firewall_rule_name) self.assertDictEqual(self.mock_firewall_rule, r) self.assert_calls() def test_get_firewall_rule_not_found(self): name = 'not_found' - self.register_uris([ - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_rules', name), - status_code=404), - dict(method='GET', - uri=self._make_mock_url('firewall_rules', name=name), - json={'firewall_rules': []}) - ]) + self.register_uris( + [ + dict( + method='GET', # short-circuit + uri=self._make_mock_url('firewall_rules', name), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url('firewall_rules', name=name), + json={'firewall_rules': []}, + ), + ] + ) self.assertIsNone(self.cloud.get_firewall_rule(name)) self.assert_calls() def test_list_firewall_rules(self): - self.register_uris([ - dict(method='GET', - uri=self._make_mock_url('firewall_rules'), - json={'firewall_rules': [self.mock_firewall_rule]}) - ]) - self.assertDictEqual(self.mock_firewall_rule, - self.cloud.list_firewall_rules()[0]) + self.register_uris( + [ + dict( + method='GET', + uri=self._make_mock_url('firewall_rules'), + json={'firewall_rules': [self.mock_firewall_rule]}, + ) + ] + ) + self.assertDictEqual( + self.mock_firewall_rule, self.cloud.list_firewall_rules()[0] + ) self.assert_calls() def test_update_firewall_rule(self): params = {'description': 'UpdatedDescription'} updated = self.mock_firewall_rule.copy() updated.update(params) - self.register_uris([ - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_rules', - self.firewall_rule_name), - status_code=404), - dict(method='GET', - uri=self._make_mock_url('firewall_rules', - name=self.firewall_rule_name), - json={'firewall_rules': [self.mock_firewall_rule]}), - dict(method='PUT', - uri=self._make_mock_url('firewall_rules', - self.firewall_rule_id), - json={'firewall_rule': updated}, - validate=dict(json={'firewall_rule': params})) - ]) + self.register_uris( + [ + dict( + method='GET', # short-circuit + uri=self._make_mock_url( + 'firewall_rules', self.firewall_rule_name + ), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_rules', name=self.firewall_rule_name + ), + json={'firewall_rules': [self.mock_firewall_rule]}, + ), + dict( + method='PUT', + uri=self._make_mock_url( + 'firewall_rules', self.firewall_rule_id + ), + json={'firewall_rule': updated}, + validate=dict(json={'firewall_rule': params}), + ), + ] + ) self.assertDictEqual( updated, - self.cloud.update_firewall_rule(self.firewall_rule_name, **params)) + self.cloud.update_firewall_rule(self.firewall_rule_name, **params), + ) self.assert_calls() def test_update_firewall_rule_filters(self): @@ -230,23 +324,30 @@ def test_update_firewall_rule_filters(self): updated.update(params) updated_dict = self._mock_firewall_rule_attrs.copy() updated_dict.update(params) - self.register_uris([ - dict( - method='GET', - uri=self._make_mock_url( - 'firewall_rules', self.firewall_rule_name, **filters), - json={'firewall_rule': self._mock_firewall_rule_attrs}), - dict( - method='PUT', - uri=self._make_mock_url( - 'firewall_rules', self.firewall_rule_id), - json={'firewall_rule': updated_dict}, - validate={ - 'json': {'firewall_rule': params}, - }) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_rules', self.firewall_rule_name, **filters + ), + json={'firewall_rule': self._mock_firewall_rule_attrs}, + ), + dict( + method='PUT', + uri=self._make_mock_url( + 'firewall_rules', self.firewall_rule_id + ), + json={'firewall_rule': updated_dict}, + validate={ + 'json': {'firewall_rule': params}, + }, + ), + ] + ) updated_rule = self.cloud.update_firewall_rule( - self.firewall_rule_name, filters, **params) + self.firewall_rule_name, filters, **params + ) self.assertDictEqual(updated, updated_rule) self.assert_calls() @@ -261,15 +362,15 @@ class TestFirewallPolicy(FirewallTestCase): 'id': firewall_policy_id, 'name': firewall_policy_name, 'project_id': 'b64238cb-a25d-41af-9ee1-42deb4587d20', - 'shared': False + 'shared': False, } mock_firewall_policy = None def setUp(self, cloud_config_fixture='clouds.yaml'): super(TestFirewallPolicy, self).setUp() self.mock_firewall_policy = FirewallPolicy( - connection=self.cloud, - **self._mock_firewall_policy_attrs).to_dict() + connection=self.cloud, **self._mock_firewall_policy_attrs + ).to_dict() def test_create_firewall_policy(self): # attributes that are passed to the tested method @@ -285,23 +386,35 @@ def test_create_firewall_policy(self): validate_attrs = deepcopy(created_attrs) del validate_attrs['id'] - self.register_uris([ - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_rules', - TestFirewallRule.firewall_rule_name), - status_code=404), - dict(method='GET', - uri=self._make_mock_url( - 'firewall_rules', - name=TestFirewallRule.firewall_rule_name), - json={'firewall_rules': [ - TestFirewallRule._mock_firewall_rule_attrs]}), - dict(method='POST', - uri=self._make_mock_url('firewall_policies'), - json={'firewall_policy': created_attrs}, - validate=dict( - json={'firewall_policy': validate_attrs})) - ]) + self.register_uris( + [ + dict( + method='GET', # short-circuit + uri=self._make_mock_url( + 'firewall_rules', TestFirewallRule.firewall_rule_name + ), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_rules', + name=TestFirewallRule.firewall_rule_name, + ), + json={ + 'firewall_rules': [ + TestFirewallRule._mock_firewall_rule_attrs + ] + }, + ), + dict( + method='POST', + uri=self._make_mock_url('firewall_policies'), + json={'firewall_policy': created_attrs}, + validate=dict(json={'firewall_policy': validate_attrs}), + ), + ] + ) res = self.cloud.create_firewall_policy(**passed_attrs) self.assertDictEqual(created_policy, res.to_dict()) self.assert_calls() @@ -309,200 +422,309 @@ def test_create_firewall_policy(self): def test_create_firewall_policy_rule_not_found(self): posted_policy = deepcopy(self._mock_firewall_policy_attrs) del posted_policy['id'] - self.register_uris([ - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_rules', - posted_policy['firewall_rules'][0]), - status_code=404), - dict(method='GET', - uri=self._make_mock_url( - 'firewall_rules', - name=posted_policy['firewall_rules'][0]), - json={'firewall_rules': []}) - ]) + self.register_uris( + [ + dict( + method='GET', # short-circuit + uri=self._make_mock_url( + 'firewall_rules', posted_policy['firewall_rules'][0] + ), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_rules', + name=posted_policy['firewall_rules'][0], + ), + json={'firewall_rules': []}, + ), + ] + ) with mock.patch.object(self.cloud.network, 'create_firewall_policy'): - self.assertRaises(exceptions.ResourceNotFound, - self.cloud.create_firewall_policy, - **posted_policy) + self.assertRaises( + exceptions.ResourceNotFound, + self.cloud.create_firewall_policy, + **posted_policy + ) self.cloud.network.create_firewall_policy.assert_not_called() self.assert_calls() def test_delete_firewall_policy(self): - self.register_uris([ - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_policies', - self.firewall_policy_name), - status_code=404), - dict(method='GET', - uri=self._make_mock_url('firewall_policies', - name=self.firewall_policy_name), - json={'firewall_policies': [self.mock_firewall_policy]}), - dict(method='DELETE', - uri=self._make_mock_url('firewall_policies', - self.firewall_policy_id), - json={}, status_code=204) - ]) + self.register_uris( + [ + dict( + method='GET', # short-circuit + uri=self._make_mock_url( + 'firewall_policies', self.firewall_policy_name + ), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_policies', name=self.firewall_policy_name + ), + json={'firewall_policies': [self.mock_firewall_policy]}, + ), + dict( + method='DELETE', + uri=self._make_mock_url( + 'firewall_policies', self.firewall_policy_id + ), + json={}, + status_code=204, + ), + ] + ) with mock.patch.object(self.cloud.log, 'debug'): self.assertTrue( - self.cloud.delete_firewall_policy(self.firewall_policy_name)) + self.cloud.delete_firewall_policy(self.firewall_policy_name) + ) self.assert_calls() self.cloud.log.debug.assert_not_called() def test_delete_firewall_policy_filters(self): filters = {'project_id': self.mock_firewall_policy['project_id']} - self.register_uris([ - dict(method='DELETE', - uri=self._make_mock_url('firewall_policies', - self.firewall_policy_id), - json={}, status_code=204) - ]) - - with mock.patch.object(self.cloud.network, 'find_firewall_policy', - return_value=self.mock_firewall_policy), \ - mock.patch.object(self.cloud.log, 'debug'): + self.register_uris( + [ + dict( + method='DELETE', + uri=self._make_mock_url( + 'firewall_policies', self.firewall_policy_id + ), + json={}, + status_code=204, + ) + ] + ) + + with mock.patch.object( + self.cloud.network, + 'find_firewall_policy', + return_value=self.mock_firewall_policy, + ), mock.patch.object(self.cloud.log, 'debug'): self.assertTrue( - self.cloud.delete_firewall_policy(self.firewall_policy_name, - filters)) + self.cloud.delete_firewall_policy( + self.firewall_policy_name, filters + ) + ) self.assert_calls() self.cloud.network.find_firewall_policy.assert_called_once_with( - self.firewall_policy_name, ignore_missing=False, **filters) + self.firewall_policy_name, ignore_missing=False, **filters + ) self.cloud.log.debug.assert_not_called() def test_delete_firewall_policy_not_found(self): - self.register_uris([ - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_policies', - self.firewall_policy_name), - status_code=404), - dict(method='GET', - uri=self._make_mock_url('firewall_policies', - name=self.firewall_policy_name), - json={'firewall_policies': []}) - ]) + self.register_uris( + [ + dict( + method='GET', # short-circuit + uri=self._make_mock_url( + 'firewall_policies', self.firewall_policy_name + ), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_policies', name=self.firewall_policy_name + ), + json={'firewall_policies': []}, + ), + ] + ) with mock.patch.object(self.cloud.log, 'debug'): self.assertFalse( - self.cloud.delete_firewall_policy(self.firewall_policy_name)) + self.cloud.delete_firewall_policy(self.firewall_policy_name) + ) self.assert_calls() self.cloud.log.debug.assert_called_once() def test_get_firewall_policy(self): - self.register_uris([ - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_policies', - self.firewall_policy_name), - status_code=404), - dict(method='GET', - uri=self._make_mock_url('firewall_policies', - name=self.firewall_policy_name), - json={'firewall_policies': [self.mock_firewall_policy]}) - ]) - self.assertDictEqual(self.mock_firewall_policy, - self.cloud.get_firewall_policy( - self.firewall_policy_name)) + self.register_uris( + [ + dict( + method='GET', # short-circuit + uri=self._make_mock_url( + 'firewall_policies', self.firewall_policy_name + ), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_policies', name=self.firewall_policy_name + ), + json={'firewall_policies': [self.mock_firewall_policy]}, + ), + ] + ) + self.assertDictEqual( + self.mock_firewall_policy, + self.cloud.get_firewall_policy(self.firewall_policy_name), + ) self.assert_calls() def test_get_firewall_policy_not_found(self): name = 'not_found' - self.register_uris([ - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_policies', name), - status_code=404), - dict(method='GET', - uri=self._make_mock_url('firewall_policies', name=name), - json={'firewall_policies': []}) - ]) + self.register_uris( + [ + dict( + method='GET', # short-circuit + uri=self._make_mock_url('firewall_policies', name), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url('firewall_policies', name=name), + json={'firewall_policies': []}, + ), + ] + ) self.assertIsNone(self.cloud.get_firewall_policy(name)) self.assert_calls() def test_list_firewall_policies(self): - self.register_uris([ - dict(method='GET', - uri=self._make_mock_url('firewall_policies'), - json={'firewall_policies': [ - self.mock_firewall_policy.copy(), - self.mock_firewall_policy.copy()]}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self._make_mock_url('firewall_policies'), + json={ + 'firewall_policies': [ + self.mock_firewall_policy.copy(), + self.mock_firewall_policy.copy(), + ] + }, + ) + ] + ) policy = FirewallPolicy( - connection=self.cloud, - **self.mock_firewall_policy) - self.assertListEqual(self.cloud.list_firewall_policies(), - [policy, policy]) + connection=self.cloud, **self.mock_firewall_policy + ) + self.assertListEqual( + self.cloud.list_firewall_policies(), [policy, policy] + ) self.assert_calls() def test_list_firewall_policies_filters(self): filters = {'project_id': self.mock_firewall_policy['project_id']} - self.register_uris([ - dict(method='GET', - uri=self._make_mock_url('firewall_policies', **filters), - json={'firewall_policies': [ - self.mock_firewall_policy]}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self._make_mock_url('firewall_policies', **filters), + json={'firewall_policies': [self.mock_firewall_policy]}, + ) + ] + ) self.assertListEqual( - self.cloud.list_firewall_policies(filters), [ + self.cloud.list_firewall_policies(filters), + [ FirewallPolicy( - connection=self.cloud, - **self.mock_firewall_policy)]) + connection=self.cloud, **self.mock_firewall_policy + ) + ], + ) self.assert_calls() def test_update_firewall_policy(self): lookup_rule = FirewallRule( - connection=self.cloud, - **TestFirewallRule._mock_firewall_rule_attrs).to_dict() - params = {'firewall_rules': [lookup_rule['id']], - 'description': 'updated!'} + connection=self.cloud, **TestFirewallRule._mock_firewall_rule_attrs + ).to_dict() + params = { + 'firewall_rules': [lookup_rule['id']], + 'description': 'updated!', + } retrieved_policy = deepcopy(self.mock_firewall_policy) del retrieved_policy['firewall_rules'][0] updated_policy = deepcopy(self.mock_firewall_policy) updated_policy.update(params) - self.register_uris([ - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_policies', - self.firewall_policy_name), - status_code=404), - dict(method='GET', - uri=self._make_mock_url('firewall_policies', - name=self.firewall_policy_name), - json={'firewall_policies': [retrieved_policy]}), - dict(method='GET', - uri=self._make_mock_url('firewall_rules', lookup_rule['id']), - json={'firewall_rule': lookup_rule}), - dict(method='PUT', - uri=self._make_mock_url('firewall_policies', - self.firewall_policy_id), - json={'firewall_policy': updated_policy}, - validate=dict(json={'firewall_policy': params})) - ]) - self.assertDictEqual(updated_policy, - self.cloud.update_firewall_policy( - self.firewall_policy_name, **params)) + self.register_uris( + [ + dict( + method='GET', # short-circuit + uri=self._make_mock_url( + 'firewall_policies', self.firewall_policy_name + ), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_policies', name=self.firewall_policy_name + ), + json={'firewall_policies': [retrieved_policy]}, + ), + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_rules', lookup_rule['id'] + ), + json={'firewall_rule': lookup_rule}, + ), + dict( + method='PUT', + uri=self._make_mock_url( + 'firewall_policies', self.firewall_policy_id + ), + json={'firewall_policy': updated_policy}, + validate=dict(json={'firewall_policy': params}), + ), + ] + ) + self.assertDictEqual( + updated_policy, + self.cloud.update_firewall_policy( + self.firewall_policy_name, **params + ), + ) self.assert_calls() def test_update_firewall_policy_no_rules(self): params = {'description': 'updated!'} updated_policy = deepcopy(self.mock_firewall_policy) updated_policy.update(params) - self.register_uris([ - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_policies', - self.firewall_policy_name), - status_code=404), - dict(method='GET', - uri=self._make_mock_url('firewall_policies', - name=self.firewall_policy_name), - json={'firewall_policies': [ - deepcopy(self.mock_firewall_policy)]}), - dict(method='PUT', - uri=self._make_mock_url('firewall_policies', - self.firewall_policy_id), - json={'firewall_policy': updated_policy}, - validate=dict(json={'firewall_policy': params})), - ]) - self.assertDictEqual(updated_policy, - self.cloud.update_firewall_policy( - self.firewall_policy_name, **params)) + self.register_uris( + [ + dict( + method='GET', # short-circuit + uri=self._make_mock_url( + 'firewall_policies', self.firewall_policy_name + ), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_policies', name=self.firewall_policy_name + ), + json={ + 'firewall_policies': [ + deepcopy(self.mock_firewall_policy) + ] + }, + ), + dict( + method='PUT', + uri=self._make_mock_url( + 'firewall_policies', self.firewall_policy_id + ), + json={'firewall_policy': updated_policy}, + validate=dict(json={'firewall_policy': params}), + ), + ] + ) + self.assertDictEqual( + updated_policy, + self.cloud.update_firewall_policy( + self.firewall_policy_name, **params + ), + ) self.assert_calls() def test_update_firewall_policy_filters(self): @@ -511,90 +733,135 @@ def test_update_firewall_policy_filters(self): updated_policy = deepcopy(self.mock_firewall_policy) updated_policy.update(params) - self.register_uris([ - dict(method='PUT', - uri=self._make_mock_url('firewall_policies', - self.firewall_policy_id), - json={'firewall_policy': updated_policy}, - validate=dict(json={'firewall_policy': params})), - ]) - - with mock.patch.object(self.cloud.network, 'find_firewall_policy', - return_value=deepcopy( - self.mock_firewall_policy)): + self.register_uris( + [ + dict( + method='PUT', + uri=self._make_mock_url( + 'firewall_policies', self.firewall_policy_id + ), + json={'firewall_policy': updated_policy}, + validate=dict(json={'firewall_policy': params}), + ), + ] + ) + + with mock.patch.object( + self.cloud.network, + 'find_firewall_policy', + return_value=deepcopy(self.mock_firewall_policy), + ): self.assertDictEqual( updated_policy, - self.cloud.update_firewall_policy(self.firewall_policy_name, - filters, **params)) + self.cloud.update_firewall_policy( + self.firewall_policy_name, filters, **params + ), + ) self.assert_calls() self.cloud.network.find_firewall_policy.assert_called_once_with( - self.firewall_policy_name, ignore_missing=False, **filters) + self.firewall_policy_name, ignore_missing=False, **filters + ) def test_insert_rule_into_policy(self): rule0 = FirewallRule( - connection=self.cloud, - **TestFirewallRule._mock_firewall_rule_attrs) + connection=self.cloud, **TestFirewallRule._mock_firewall_rule_attrs + ) - _rule1_attrs = deepcopy( - TestFirewallRule._mock_firewall_rule_attrs) - _rule1_attrs.update(id='8068fc06-0e72-43f2-a76f-a51a33b46e08', - name='after_rule') + _rule1_attrs = deepcopy(TestFirewallRule._mock_firewall_rule_attrs) + _rule1_attrs.update( + id='8068fc06-0e72-43f2-a76f-a51a33b46e08', name='after_rule' + ) rule1 = FirewallRule(**_rule1_attrs) _rule2_attrs = deepcopy(TestFirewallRule._mock_firewall_rule_attrs) - _rule2_attrs.update(id='c716382d-183b-475d-b500-dcc762f45ce3', - name='before_rule') + _rule2_attrs.update( + id='c716382d-183b-475d-b500-dcc762f45ce3', name='before_rule' + ) rule2 = FirewallRule(**_rule2_attrs) retrieved_policy = deepcopy(self.mock_firewall_policy) retrieved_policy['firewall_rules'] = [rule1['id'], rule2['id']] updated_policy = deepcopy(self.mock_firewall_policy) - updated_policy['firewall_rules'] = [rule0['id'], rule1['id'], - rule2['id']] - self.register_uris([ - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_policies', - self.firewall_policy_name), - status_code=404), - dict(method='GET', # get policy - uri=self._make_mock_url('firewall_policies', - name=self.firewall_policy_name), - json={'firewall_policies': [retrieved_policy]}), - - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_rules', rule0['name']), - status_code=404), - dict(method='GET', # get rule to add - uri=self._make_mock_url('firewall_rules', name=rule0['name']), - json={'firewall_rules': [rule0]}), - - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_rules', rule1['name']), - status_code=404), - dict(method='GET', # get after rule - uri=self._make_mock_url('firewall_rules', name=rule1['name']), - json={'firewall_rules': [rule1]}), - - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_rules', rule2['name']), - status_code=404), - dict(method='GET', # get before rule - uri=self._make_mock_url('firewall_rules', name=rule2['name']), - json={'firewall_rules': [rule2]}), - - dict(method='PUT', # add rule - uri=self._make_mock_url('firewall_policies', - self.firewall_policy_id, - 'insert_rule'), - json=updated_policy, - validate=dict(json={'firewall_rule_id': rule0['id'], - 'insert_after': rule1['id'], - 'insert_before': rule2['id']})), - ]) + updated_policy['firewall_rules'] = [ + rule0['id'], + rule1['id'], + rule2['id'], + ] + self.register_uris( + [ + dict( + method='GET', # short-circuit + uri=self._make_mock_url( + 'firewall_policies', self.firewall_policy_name + ), + status_code=404, + ), + dict( + method='GET', # get policy + uri=self._make_mock_url( + 'firewall_policies', name=self.firewall_policy_name + ), + json={'firewall_policies': [retrieved_policy]}, + ), + dict( + method='GET', # short-circuit + uri=self._make_mock_url('firewall_rules', rule0['name']), + status_code=404, + ), + dict( + method='GET', # get rule to add + uri=self._make_mock_url( + 'firewall_rules', name=rule0['name'] + ), + json={'firewall_rules': [rule0]}, + ), + dict( + method='GET', # short-circuit + uri=self._make_mock_url('firewall_rules', rule1['name']), + status_code=404, + ), + dict( + method='GET', # get after rule + uri=self._make_mock_url( + 'firewall_rules', name=rule1['name'] + ), + json={'firewall_rules': [rule1]}, + ), + dict( + method='GET', # short-circuit + uri=self._make_mock_url('firewall_rules', rule2['name']), + status_code=404, + ), + dict( + method='GET', # get before rule + uri=self._make_mock_url( + 'firewall_rules', name=rule2['name'] + ), + json={'firewall_rules': [rule2]}, + ), + dict( + method='PUT', # add rule + uri=self._make_mock_url( + 'firewall_policies', + self.firewall_policy_id, + 'insert_rule', + ), + json=updated_policy, + validate=dict( + json={ + 'firewall_rule_id': rule0['id'], + 'insert_after': rule1['id'], + 'insert_before': rule2['id'], + } + ), + ), + ] + ) r = self.cloud.insert_rule_into_policy( name_or_id=self.firewall_policy_name, rule_name_or_id=rule0['name'], insert_after=rule1['name'], - insert_before=rule2['name']) + insert_before=rule2['name'], + ) self.assertDictEqual(updated_policy, r.to_dict()) self.assert_calls() @@ -607,89 +874,140 @@ def test_insert_rule_into_policy_compact(self): retrieved_policy['firewall_rules'] = [] updated_policy = deepcopy(retrieved_policy) updated_policy['firewall_rules'].append(rule['id']) - self.register_uris([ - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_policies', - self.firewall_policy_name), - status_code=404), - dict(method='GET', - uri=self._make_mock_url('firewall_policies', - name=self.firewall_policy_name), - json={'firewall_policies': [retrieved_policy]}), - - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_rules', rule['name']), - status_code=404), - dict(method='GET', - uri=self._make_mock_url('firewall_rules', name=rule['name']), - json={'firewall_rules': [rule]}), - - dict(method='PUT', - uri=self._make_mock_url('firewall_policies', - retrieved_policy['id'], - 'insert_rule'), - json=updated_policy, - validate=dict(json={'firewall_rule_id': rule['id'], - 'insert_after': None, - 'insert_before': None})) - ]) - r = self.cloud.insert_rule_into_policy(self.firewall_policy_name, - rule['name']) + self.register_uris( + [ + dict( + method='GET', # short-circuit + uri=self._make_mock_url( + 'firewall_policies', self.firewall_policy_name + ), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_policies', name=self.firewall_policy_name + ), + json={'firewall_policies': [retrieved_policy]}, + ), + dict( + method='GET', # short-circuit + uri=self._make_mock_url('firewall_rules', rule['name']), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_rules', name=rule['name'] + ), + json={'firewall_rules': [rule]}, + ), + dict( + method='PUT', + uri=self._make_mock_url( + 'firewall_policies', + retrieved_policy['id'], + 'insert_rule', + ), + json=updated_policy, + validate=dict( + json={ + 'firewall_rule_id': rule['id'], + 'insert_after': None, + 'insert_before': None, + } + ), + ), + ] + ) + r = self.cloud.insert_rule_into_policy( + self.firewall_policy_name, rule['name'] + ) self.assertDictEqual(updated_policy, r.to_dict()) self.assert_calls() def test_insert_rule_into_policy_not_found(self): policy_name = 'bogus_policy' - self.register_uris([ - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_policies', policy_name), - status_code=404), - dict(method='GET', - uri=self._make_mock_url('firewall_policies', - name=policy_name), - json={'firewall_policies': []}) - ]) + self.register_uris( + [ + dict( + method='GET', # short-circuit + uri=self._make_mock_url('firewall_policies', policy_name), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_policies', name=policy_name + ), + json={'firewall_policies': []}, + ), + ] + ) with mock.patch.object(self.cloud.network, 'find_firewall_rule'): - self.assertRaises(exceptions.ResourceNotFound, - self.cloud.insert_rule_into_policy, - policy_name, 'bogus_rule') + self.assertRaises( + exceptions.ResourceNotFound, + self.cloud.insert_rule_into_policy, + policy_name, + 'bogus_rule', + ) self.assert_calls() self.cloud.network.find_firewall_rule.assert_not_called() def test_insert_rule_into_policy_rule_not_found(self): rule_name = 'unknown_rule' - self.register_uris([ - dict(method='GET', - uri=self._make_mock_url('firewall_policies', - self.firewall_policy_id), - json={'firewall_policy': self.mock_firewall_policy}), - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_rules', rule_name), - status_code=404), - dict(method='GET', - uri=self._make_mock_url('firewall_rules', name=rule_name), - json={'firewall_rules': []}) - ]) - self.assertRaises(exceptions.ResourceNotFound, - self.cloud.insert_rule_into_policy, - self.firewall_policy_id, rule_name) + self.register_uris( + [ + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_policies', self.firewall_policy_id + ), + json={'firewall_policy': self.mock_firewall_policy}, + ), + dict( + method='GET', # short-circuit + uri=self._make_mock_url('firewall_rules', rule_name), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url('firewall_rules', name=rule_name), + json={'firewall_rules': []}, + ), + ] + ) + self.assertRaises( + exceptions.ResourceNotFound, + self.cloud.insert_rule_into_policy, + self.firewall_policy_id, + rule_name, + ) self.assert_calls() def test_insert_rule_into_policy_already_associated(self): rule = FirewallRule( - **TestFirewallRule._mock_firewall_rule_attrs).to_dict() + **TestFirewallRule._mock_firewall_rule_attrs + ).to_dict() policy = deepcopy(self.mock_firewall_policy) policy['firewall_rules'] = [rule['id']] - self.register_uris([ - dict(method='GET', - uri=self._make_mock_url('firewall_policies', - self.firewall_policy_id), - json={'firewall_policy': policy}), - dict(method='GET', - uri=self._make_mock_url('firewall_rules', rule['id']), - json={'firewall_rule': rule}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_policies', self.firewall_policy_id + ), + json={'firewall_policy': policy}, + ), + dict( + method='GET', + uri=self._make_mock_url('firewall_rules', rule['id']), + json={'firewall_rule': rule}, + ), + ] + ) with mock.patch.object(self.cloud.log, 'debug'): r = self.cloud.insert_rule_into_policy(policy['id'], rule['id']) @@ -706,50 +1024,75 @@ def test_remove_rule_from_policy(self): updated_policy = deepcopy(self.mock_firewall_policy) del updated_policy['firewall_rules'][0] - self.register_uris([ - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_policies', policy_name), - status_code=404), - dict(method='GET', - uri=self._make_mock_url('firewall_policies', - name=policy_name), - json={'firewall_policies': [retrieved_policy]}), - - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_rules', rule['name']), - status_code=404), - dict(method='GET', - uri=self._make_mock_url('firewall_rules', name=rule['name']), - json={'firewall_rules': [rule]}), - - dict(method='PUT', - uri=self._make_mock_url('firewall_policies', - self.firewall_policy_id, - 'remove_rule'), - json=updated_policy, - validate=dict(json={'firewall_rule_id': rule['id']})) - ]) + self.register_uris( + [ + dict( + method='GET', # short-circuit + uri=self._make_mock_url('firewall_policies', policy_name), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_policies', name=policy_name + ), + json={'firewall_policies': [retrieved_policy]}, + ), + dict( + method='GET', # short-circuit + uri=self._make_mock_url('firewall_rules', rule['name']), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_rules', name=rule['name'] + ), + json={'firewall_rules': [rule]}, + ), + dict( + method='PUT', + uri=self._make_mock_url( + 'firewall_policies', + self.firewall_policy_id, + 'remove_rule', + ), + json=updated_policy, + validate=dict(json={'firewall_rule_id': rule['id']}), + ), + ] + ) r = self.cloud.remove_rule_from_policy(policy_name, rule['name']) self.assertDictEqual(updated_policy, r.to_dict()) self.assert_calls() def test_remove_rule_from_policy_not_found(self): - self.register_uris([ - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_policies', - self.firewall_policy_name), - status_code=404), - dict(method='GET', - uri=self._make_mock_url('firewall_policies', - name=self.firewall_policy_name), - json={'firewall_policies': []}) - ]) + self.register_uris( + [ + dict( + method='GET', # short-circuit + uri=self._make_mock_url( + 'firewall_policies', self.firewall_policy_name + ), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_policies', name=self.firewall_policy_name + ), + json={'firewall_policies': []}, + ), + ] + ) with mock.patch.object(self.cloud.network, 'find_firewall_rule'): - self.assertRaises(exceptions.ResourceNotFound, - self.cloud.remove_rule_from_policy, - self.firewall_policy_name, - TestFirewallRule.firewall_rule_name) + self.assertRaises( + exceptions.ResourceNotFound, + self.cloud.remove_rule_from_policy, + self.firewall_policy_name, + TestFirewallRule.firewall_rule_name, + ) self.assert_calls() self.cloud.network.find_firewall_rule.assert_not_called() @@ -757,41 +1100,60 @@ def test_remove_rule_from_policy_rule_not_found(self): retrieved_policy = deepcopy(self.mock_firewall_policy) rule = FirewallRule(**TestFirewallRule._mock_firewall_rule_attrs) retrieved_policy['firewall_rules'][0] = rule['id'] - self.register_uris([ - dict(method='GET', - uri=self._make_mock_url('firewall_policies', - self.firewall_policy_id), - json={'firewall_policy': retrieved_policy}), - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_rules', - rule['name']), - status_code=404), - dict(method='GET', - uri=self._make_mock_url('firewall_rules', name=rule['name']), - json={'firewall_rules': []}) - ]) - r = self.cloud.remove_rule_from_policy(self.firewall_policy_id, - rule['name']) + self.register_uris( + [ + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_policies', self.firewall_policy_id + ), + json={'firewall_policy': retrieved_policy}, + ), + dict( + method='GET', # short-circuit + uri=self._make_mock_url('firewall_rules', rule['name']), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_rules', name=rule['name'] + ), + json={'firewall_rules': []}, + ), + ] + ) + r = self.cloud.remove_rule_from_policy( + self.firewall_policy_id, rule['name'] + ) self.assertDictEqual(retrieved_policy, r.to_dict()) self.assert_calls() def test_remove_rule_from_policy_not_associated(self): rule = FirewallRule( - **TestFirewallRule._mock_firewall_rule_attrs).to_dict() + **TestFirewallRule._mock_firewall_rule_attrs + ).to_dict() policy = deepcopy(self.mock_firewall_policy) del policy['firewall_rules'][0] - self.register_uris([ - dict(method='GET', - uri=self._make_mock_url('firewall_policies', policy['id']), - json={'firewall_policy': policy}), - dict(method='GET', - uri=self._make_mock_url('firewall_rules', rule['id']), - json={'firewall_rule': rule}) - ]) - - with mock.patch.object(self.cloud.network, 'remove_rule_from_policy'),\ - mock.patch.object(self.cloud.log, 'debug'): + self.register_uris( + [ + dict( + method='GET', + uri=self._make_mock_url('firewall_policies', policy['id']), + json={'firewall_policy': policy}, + ), + dict( + method='GET', + uri=self._make_mock_url('firewall_rules', rule['id']), + json={'firewall_rule': rule}, + ), + ] + ) + + with mock.patch.object( + self.cloud.network, 'remove_rule_from_policy' + ), mock.patch.object(self.cloud.log, 'debug'): r = self.cloud.remove_rule_from_policy(policy['id'], rule['id']) self.assertDictEqual(policy, r.to_dict()) self.assert_calls() @@ -804,15 +1166,15 @@ class TestFirewallGroup(FirewallTestCase): firewall_group_name = 'max_security_group' mock_port = { 'name': 'mock_port', - 'id': '7d90977c-45ec-467e-a16d-dcaed772a161' + 'id': '7d90977c-45ec-467e-a16d-dcaed772a161', } _mock_egress_policy_attrs = { 'id': '34335e5b-44af-4ffd-9dcf-518133f897c7', - 'name': 'safe_outgoing_data' + 'name': 'safe_outgoing_data', } _mock_ingress_policy_attrs = { 'id': 'cd28fb50-85d0-4f36-89af-50fac08ac174', - 'name': 'bad_incoming_data' + 'name': 'bad_incoming_data', } _mock_firewall_group_attrs = { 'admin_state_up': True, @@ -823,7 +1185,7 @@ class TestFirewallGroup(FirewallTestCase): 'name': firewall_group_name, 'ports': [mock_port['name']], 'project_id': 'da347b09-0b4f-4994-a3ef-05d13eaecb2c', - 'shared': False + 'shared': False, } _mock_returned_firewall_group_attrs = { 'admin_state_up': True, @@ -836,7 +1198,7 @@ class TestFirewallGroup(FirewallTestCase): 'name': firewall_group_name, 'ports': [mock_port['id']], 'project_id': 'da347b09-0b4f-4994-a3ef-05d13eaecb2c', - 'shared': False + 'shared': False, } mock_egress_policy = None mock_ingress_policy = None @@ -846,17 +1208,17 @@ class TestFirewallGroup(FirewallTestCase): def setUp(self, cloud_config_fixture='clouds.yaml'): super(TestFirewallGroup, self).setUp() self.mock_egress_policy = FirewallPolicy( - connection=self.cloud, - **self._mock_egress_policy_attrs).to_dict() + connection=self.cloud, **self._mock_egress_policy_attrs + ).to_dict() self.mock_ingress_policy = FirewallPolicy( - connection=self.cloud, - **self._mock_ingress_policy_attrs).to_dict() + connection=self.cloud, **self._mock_ingress_policy_attrs + ).to_dict() self.mock_firewall_group = FirewallGroup( - connection=self.cloud, - **self._mock_firewall_group_attrs).to_dict() + connection=self.cloud, **self._mock_firewall_group_attrs + ).to_dict() self.mock_returned_firewall_group = FirewallGroup( - connection=self.cloud, - **self._mock_returned_firewall_group_attrs).to_dict() + connection=self.cloud, **self._mock_returned_firewall_group_attrs + ).to_dict() def test_create_firewall_group(self): create_group_attrs = self._mock_firewall_group_attrs.copy() @@ -865,43 +1227,69 @@ def test_create_firewall_group(self): del posted_group_attrs['egress_firewall_policy'] del posted_group_attrs['ingress_firewall_policy'] del posted_group_attrs['id'] - self.register_uris([ - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_policies', - self.mock_egress_policy['name']), - status_code=404), - dict(method='GET', - uri=self._make_mock_url('firewall_policies', - name=self.mock_egress_policy['name']), - json={'firewall_policies': [self.mock_egress_policy]}), - - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_policies', - self.mock_ingress_policy['name']), - status_code=404), - dict(method='GET', - uri=self._make_mock_url( - 'firewall_policies', - name=self.mock_ingress_policy['name']), - json={'firewall_policies': [self.mock_ingress_policy]}), - - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'ports', self.mock_port['name']]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'ports'], - qs_elements=['name=%s' % self.mock_port['name']]), - json={'ports': [self.mock_port]}), - dict(method='POST', - uri=self._make_mock_url('firewall_groups'), - json={'firewall_group': deepcopy( - self.mock_returned_firewall_group)}, - validate=dict(json={'firewall_group': posted_group_attrs})) - ]) + self.register_uris( + [ + dict( + method='GET', # short-circuit + uri=self._make_mock_url( + 'firewall_policies', self.mock_egress_policy['name'] + ), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_policies', + name=self.mock_egress_policy['name'], + ), + json={'firewall_policies': [self.mock_egress_policy]}, + ), + dict( + method='GET', # short-circuit + uri=self._make_mock_url( + 'firewall_policies', self.mock_ingress_policy['name'] + ), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_policies', + name=self.mock_ingress_policy['name'], + ), + json={'firewall_policies': [self.mock_ingress_policy]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'ports', self.mock_port['name']], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'ports'], + qs_elements=['name=%s' % self.mock_port['name']], + ), + json={'ports': [self.mock_port]}, + ), + dict( + method='POST', + uri=self._make_mock_url('firewall_groups'), + json={ + 'firewall_group': deepcopy( + self.mock_returned_firewall_group + ) + }, + validate=dict(json={'firewall_group': posted_group_attrs}), + ), + ] + ) r = self.cloud.create_firewall_group(**create_group_attrs) self.assertDictEqual(self.mock_returned_firewall_group, r.to_dict()) self.assert_calls() @@ -915,129 +1303,193 @@ def test_create_firewall_group_compact(self): del firewall_group['egress_firewall_policy'] del firewall_group['ingress_firewall_policy'] created_firewall = deepcopy(firewall_group) - created_firewall.update(egress_firewall_policy_id=None, - ingress_firewall_policy_id=None, - ports=[]) + created_firewall.update( + egress_firewall_policy_id=None, + ingress_firewall_policy_id=None, + ports=[], + ) del firewall_group['id'] - self.register_uris([ - dict(method='POST', - uri=self._make_mock_url('firewall_groups'), - json={'firewall_group': created_firewall}, - validate=dict(json={'firewall_group': firewall_group})) - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self._make_mock_url('firewall_groups'), + json={'firewall_group': created_firewall}, + validate=dict(json={'firewall_group': firewall_group}), + ) + ] + ) r = self.cloud.create_firewall_group(**firewall_group) self.assertDictEqual( - FirewallGroup( - connection=self.cloud, - **created_firewall).to_dict(), - r.to_dict()) + FirewallGroup(connection=self.cloud, **created_firewall).to_dict(), + r.to_dict(), + ) self.assert_calls() def test_delete_firewall_group(self): - self.register_uris([ - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_groups', - self.firewall_group_name), - status_code=404), - dict(method='GET', - uri=self._make_mock_url('firewall_groups', - name=self.firewall_group_name), - json={'firewall_groups': [ - deepcopy(self.mock_returned_firewall_group)]}), - dict(method='DELETE', - uri=self._make_mock_url('firewall_groups', - self.firewall_group_id), - status_code=204) - ]) + self.register_uris( + [ + dict( + method='GET', # short-circuit + uri=self._make_mock_url( + 'firewall_groups', self.firewall_group_name + ), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_groups', name=self.firewall_group_name + ), + json={ + 'firewall_groups': [ + deepcopy(self.mock_returned_firewall_group) + ] + }, + ), + dict( + method='DELETE', + uri=self._make_mock_url( + 'firewall_groups', self.firewall_group_id + ), + status_code=204, + ), + ] + ) self.assertTrue( - self.cloud.delete_firewall_group(self.firewall_group_name)) + self.cloud.delete_firewall_group(self.firewall_group_name) + ) self.assert_calls() def test_delete_firewall_group_filters(self): filters = {'project_id': self.mock_firewall_group['project_id']} - self.register_uris([ - dict(method='DELETE', - uri=self._make_mock_url('firewall_groups', - self.firewall_group_id), - status_code=204) - ]) - - with mock.patch.object(self.cloud.network, 'find_firewall_group', - return_value=deepcopy( - self.mock_firewall_group)): + self.register_uris( + [ + dict( + method='DELETE', + uri=self._make_mock_url( + 'firewall_groups', self.firewall_group_id + ), + status_code=204, + ) + ] + ) + + with mock.patch.object( + self.cloud.network, + 'find_firewall_group', + return_value=deepcopy(self.mock_firewall_group), + ): self.assertTrue( - self.cloud.delete_firewall_group(self.firewall_group_name, - filters)) + self.cloud.delete_firewall_group( + self.firewall_group_name, filters + ) + ) self.assert_calls() self.cloud.network.find_firewall_group.assert_called_once_with( - self.firewall_group_name, ignore_missing=False, **filters) + self.firewall_group_name, ignore_missing=False, **filters + ) def test_delete_firewall_group_not_found(self): - self.register_uris([ - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_groups', - self.firewall_group_name), - status_code=404), - dict(method='GET', - uri=self._make_mock_url('firewall_groups', - name=self.firewall_group_name), - json={'firewall_groups': []}) - ]) + self.register_uris( + [ + dict( + method='GET', # short-circuit + uri=self._make_mock_url( + 'firewall_groups', self.firewall_group_name + ), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_groups', name=self.firewall_group_name + ), + json={'firewall_groups': []}, + ), + ] + ) with mock.patch.object(self.cloud.log, 'debug'): self.assertFalse( - self.cloud.delete_firewall_group(self.firewall_group_name)) + self.cloud.delete_firewall_group(self.firewall_group_name) + ) self.assert_calls() self.cloud.log.debug.assert_called_once() def test_get_firewall_group(self): returned_group = deepcopy(self.mock_returned_firewall_group) - self.register_uris([ - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_groups', - self.firewall_group_name), - status_code=404), - dict(method='GET', - uri=self._make_mock_url('firewall_groups', - name=self.firewall_group_name), - json={'firewall_groups': [returned_group]}) - ]) + self.register_uris( + [ + dict( + method='GET', # short-circuit + uri=self._make_mock_url( + 'firewall_groups', self.firewall_group_name + ), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_groups', name=self.firewall_group_name + ), + json={'firewall_groups': [returned_group]}, + ), + ] + ) self.assertDictEqual( returned_group, - self.cloud.get_firewall_group(self.firewall_group_name)) + self.cloud.get_firewall_group(self.firewall_group_name), + ) self.assert_calls() def test_get_firewall_group_not_found(self): name = 'not_found' - self.register_uris([ - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_groups', name), - status_code=404), - dict(method='GET', - uri=self._make_mock_url('firewall_groups', name=name), - json={'firewall_groups': []}) - ]) + self.register_uris( + [ + dict( + method='GET', # short-circuit + uri=self._make_mock_url('firewall_groups', name), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url('firewall_groups', name=name), + json={'firewall_groups': []}, + ), + ] + ) self.assertIsNone(self.cloud.get_firewall_group(name)) self.assert_calls() def test_get_firewall_group_by_id(self): returned_group = deepcopy(self.mock_returned_firewall_group) - self.register_uris([ - dict(method='GET', - uri=self._make_mock_url('firewall_groups', - self.firewall_group_id), - json={'firewall_group': returned_group})]) + self.register_uris( + [ + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_groups', self.firewall_group_id + ), + json={'firewall_group': returned_group}, + ) + ] + ) r = self.cloud.get_firewall_group(self.firewall_group_id) self.assertDictEqual(returned_group, r.to_dict()) self.assert_calls() def test_list_firewall_groups(self): returned_attrs = deepcopy(self.mock_returned_firewall_group) - self.register_uris([ - dict(method='GET', - uri=self._make_mock_url('firewall_groups'), - json={'firewall_groups': [returned_attrs, returned_attrs]}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self._make_mock_url('firewall_groups'), + json={'firewall_groups': [returned_attrs, returned_attrs]}, + ) + ] + ) group = FirewallGroup(connection=self.cloud, **returned_attrs) self.assertListEqual([group, group], self.cloud.list_firewall_groups()) self.assert_calls() @@ -1047,7 +1499,7 @@ def test_update_firewall_group(self): 'description': 'updated!', 'egress_firewall_policy': self.mock_egress_policy['name'], 'ingress_firewall_policy': self.mock_ingress_policy['name'], - 'ports': [self.mock_port['name']] + 'ports': [self.mock_port['name']], } updated_group = deepcopy(self.mock_returned_firewall_group) updated_group['description'] = params['description'] @@ -1057,65 +1509,110 @@ def test_update_firewall_group(self): returned_group.update( ingress_firewall_policy_id=None, egress_firewall_policy_id=None, - ports=[]) - self.register_uris([ - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_groups', - self.firewall_group_name), - status_code=404), - dict(method='GET', - uri=self._make_mock_url('firewall_groups', - name=self.firewall_group_name), - json={'firewall_groups': [returned_group]}), - - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_policies', - self.mock_egress_policy['name']), - status_code=404), - dict(method='GET', - uri=self._make_mock_url('firewall_policies', - name=self.mock_egress_policy['name']), - json={'firewall_policies': [ - deepcopy(self.mock_egress_policy)]}), - - dict(method='GET', # short-circuit - uri=self._make_mock_url('firewall_policies', - self.mock_ingress_policy['name']), - status_code=404), - dict(method='GET', - uri=self._make_mock_url( - 'firewall_policies', - name=self.mock_ingress_policy['name']), - json={'firewall_policies': [ - deepcopy(self.mock_ingress_policy)]}), - - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'ports', self.mock_port['name']]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'ports'], - qs_elements=['name=%s' % self.mock_port['name']]), - json={'ports': [self.mock_port]}), - dict(method='PUT', - uri=self._make_mock_url('firewall_groups', - self.firewall_group_id), - json={'firewall_group': updated_group}, - validate=dict(json={'firewall_group': { - 'description': params['description'], - 'egress_firewall_policy_id': - self.mock_egress_policy['id'], - 'ingress_firewall_policy_id': - self.mock_ingress_policy['id'], - 'ports': [self.mock_port['id']] - }})) - ]) - self.assertDictEqual(updated_group, - self.cloud.update_firewall_group( - self.firewall_group_name, **params)) + ports=[], + ) + self.register_uris( + [ + dict( + method='GET', # short-circuit + uri=self._make_mock_url( + 'firewall_groups', self.firewall_group_name + ), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_groups', name=self.firewall_group_name + ), + json={'firewall_groups': [returned_group]}, + ), + dict( + method='GET', # short-circuit + uri=self._make_mock_url( + 'firewall_policies', self.mock_egress_policy['name'] + ), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_policies', + name=self.mock_egress_policy['name'], + ), + json={ + 'firewall_policies': [ + deepcopy(self.mock_egress_policy) + ] + }, + ), + dict( + method='GET', # short-circuit + uri=self._make_mock_url( + 'firewall_policies', self.mock_ingress_policy['name'] + ), + status_code=404, + ), + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_policies', + name=self.mock_ingress_policy['name'], + ), + json={ + 'firewall_policies': [ + deepcopy(self.mock_ingress_policy) + ] + }, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'ports', self.mock_port['name']], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'ports'], + qs_elements=['name=%s' % self.mock_port['name']], + ), + json={'ports': [self.mock_port]}, + ), + dict( + method='PUT', + uri=self._make_mock_url( + 'firewall_groups', self.firewall_group_id + ), + json={'firewall_group': updated_group}, + validate=dict( + json={ + 'firewall_group': { + 'description': params['description'], + 'egress_firewall_policy_id': self.mock_egress_policy[ # noqa: E501 + 'id' + ], + 'ingress_firewall_policy_id': self.mock_ingress_policy[ # noqa: E501 + 'id' + ], + 'ports': [self.mock_port['id']], + } + } + ), + ), + ] + ) + self.assertDictEqual( + updated_group, + self.cloud.update_firewall_group( + self.firewall_group_name, **params + ), + ) self.assert_calls() def test_update_firewall_group_compact(self): @@ -1123,65 +1620,99 @@ def test_update_firewall_group_compact(self): updated_group = deepcopy(self.mock_returned_firewall_group) updated_group.update(params) - self.register_uris([ - dict(method='GET', - uri=self._make_mock_url('firewall_groups', - self.firewall_group_id), - json={'firewall_group': deepcopy( - self.mock_returned_firewall_group)}), - dict(method='PUT', - uri=self._make_mock_url('firewall_groups', - self.firewall_group_id), - json={'firewall_group': updated_group}, - validate=dict(json={'firewall_group': params})) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_groups', self.firewall_group_id + ), + json={ + 'firewall_group': deepcopy( + self.mock_returned_firewall_group + ) + }, + ), + dict( + method='PUT', + uri=self._make_mock_url( + 'firewall_groups', self.firewall_group_id + ), + json={'firewall_group': updated_group}, + validate=dict(json={'firewall_group': params}), + ), + ] + ) self.assertDictEqual( updated_group, - self.cloud.update_firewall_group(self.firewall_group_id, **params)) + self.cloud.update_firewall_group(self.firewall_group_id, **params), + ) self.assert_calls() def test_update_firewall_group_filters(self): filters = {'project_id': self.mock_firewall_group['project_id']} params = {'description': 'updated again!'} updated_group = deepcopy(self.mock_returned_firewall_group) - self.register_uris([ - dict(method='PUT', - uri=self._make_mock_url('firewall_groups', - self.firewall_group_id), - json={'firewall_group': updated_group}, - validate=dict(json={'firewall_group': params})) - ]) - - with mock.patch.object(self.cloud.network, 'find_firewall_group', - return_value=deepcopy( - self.mock_firewall_group)): - r = self.cloud.update_firewall_group(self.firewall_group_name, - filters, **params) + self.register_uris( + [ + dict( + method='PUT', + uri=self._make_mock_url( + 'firewall_groups', self.firewall_group_id + ), + json={'firewall_group': updated_group}, + validate=dict(json={'firewall_group': params}), + ) + ] + ) + + with mock.patch.object( + self.cloud.network, + 'find_firewall_group', + return_value=deepcopy(self.mock_firewall_group), + ): + r = self.cloud.update_firewall_group( + self.firewall_group_name, filters, **params + ) self.assertDictEqual(updated_group, r.to_dict()) self.assert_calls() self.cloud.network.find_firewall_group.assert_called_once_with( - self.firewall_group_name, ignore_missing=False, **filters) + self.firewall_group_name, ignore_missing=False, **filters + ) def test_update_firewall_group_unset_policies(self): - transformed_params = {'ingress_firewall_policy_id': None, - 'egress_firewall_policy_id': None} + transformed_params = { + 'ingress_firewall_policy_id': None, + 'egress_firewall_policy_id': None, + } updated_group = deepcopy(self.mock_returned_firewall_group) updated_group.update(**transformed_params) returned_group = deepcopy(self.mock_returned_firewall_group) - self.register_uris([ - dict(method='GET', - uri=self._make_mock_url('firewall_groups', - self.firewall_group_id), - json={'firewall_group': returned_group}), - dict(method='PUT', - uri=self._make_mock_url('firewall_groups', - self.firewall_group_id), - json={'firewall_group': updated_group}, - validate=dict(json={'firewall_group': transformed_params})) - ]) - self.assertDictEqual(updated_group, - self.cloud.update_firewall_group( - self.firewall_group_id, - ingress_firewall_policy=None, - egress_firewall_policy=None)) + self.register_uris( + [ + dict( + method='GET', + uri=self._make_mock_url( + 'firewall_groups', self.firewall_group_id + ), + json={'firewall_group': returned_group}, + ), + dict( + method='PUT', + uri=self._make_mock_url( + 'firewall_groups', self.firewall_group_id + ), + json={'firewall_group': updated_group}, + validate=dict(json={'firewall_group': transformed_params}), + ), + ] + ) + self.assertDictEqual( + updated_group, + self.cloud.update_firewall_group( + self.firewall_group_id, + ingress_firewall_policy=None, + egress_firewall_policy=None, + ), + ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_groups.py b/openstack/tests/unit/cloud/test_groups.py index d83e51ce3..dab4bc3e6 100644 --- a/openstack/tests/unit/cloud/test_groups.py +++ b/openstack/tests/unit/cloud/test_groups.py @@ -17,86 +17,129 @@ class TestGroups(base.TestCase): def setUp(self, cloud_config_fixture='clouds.yaml'): super(TestGroups, self).setUp( - cloud_config_fixture=cloud_config_fixture) + cloud_config_fixture=cloud_config_fixture + ) self.addCleanup(self.assert_calls) - def get_mock_url(self, service_type='identity', interface='public', - resource='groups', append=None, base_url_append='v3'): + def get_mock_url( + self, + service_type='identity', + interface='public', + resource='groups', + append=None, + base_url_append='v3', + ): return super(TestGroups, self).get_mock_url( - service_type='identity', interface=interface, resource=resource, - append=append, base_url_append=base_url_append) + service_type='identity', + interface=interface, + resource=resource, + append=append, + base_url_append=base_url_append, + ) def test_list_groups(self): group_data = self._get_group_data() - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(), - status_code=200, - json={'groups': [group_data.json_response['group']]}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'groups': [group_data.json_response['group']]}, + ) + ] + ) self.cloud.list_groups() def test_get_group(self): group_data = self._get_group_data() - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(), - status_code=200, - json={'groups': [group_data.json_response['group']]}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'groups': [group_data.json_response['group']]}, + ), + ] + ) self.cloud.get_group(group_data.group_id) def test_delete_group(self): group_data = self._get_group_data() - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - append=[group_data.group_id]), - status_code=200, - json={'group': group_data.json_response['group']}), - dict(method='DELETE', - uri=self.get_mock_url(append=[group_data.group_id]), - status_code=204), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(append=[group_data.group_id]), + status_code=200, + json={'group': group_data.json_response['group']}, + ), + dict( + method='DELETE', + uri=self.get_mock_url(append=[group_data.group_id]), + status_code=204, + ), + ] + ) self.assertTrue(self.cloud.delete_group(group_data.group_id)) def test_create_group(self): domain_data = self._get_domain_data() group_data = self._get_group_data(domain_id=domain_data.domain_id) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(resource='domains', - append=[domain_data.domain_id]), - status_code=200, - json=domain_data.json_response), - dict(method='POST', - uri=self.get_mock_url(), - status_code=200, - json=group_data.json_response, - validate=dict(json=group_data.json_request)) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='domains', append=[domain_data.domain_id] + ), + status_code=200, + json=domain_data.json_response, + ), + dict( + method='POST', + uri=self.get_mock_url(), + status_code=200, + json=group_data.json_response, + validate=dict(json=group_data.json_request), + ), + ] + ) self.cloud.create_group( - name=group_data.group_name, description=group_data.description, - domain=group_data.domain_id) + name=group_data.group_name, + description=group_data.description, + domain=group_data.domain_id, + ) def test_update_group(self): group_data = self._get_group_data() # Domain ID is not sent group_data.json_request['group'].pop('domain_id') - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - append=[group_data.group_id]), - status_code=200, - json={'group': group_data.json_response['group']}), - dict(method='PATCH', - uri=self.get_mock_url( - append=[group_data.group_id]), - status_code=200, - json=group_data.json_response, - validate=dict(json={ - 'group': {'name': 'new_name', 'description': - 'new_description'}})) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(append=[group_data.group_id]), + status_code=200, + json={'group': group_data.json_response['group']}, + ), + dict( + method='PATCH', + uri=self.get_mock_url(append=[group_data.group_id]), + status_code=200, + json=group_data.json_response, + validate=dict( + json={ + 'group': { + 'name': 'new_name', + 'description': 'new_description', + } + } + ), + ), + ] + ) self.cloud.update_group( - group_data.group_id, 'new_name', 'new_description') + group_data.group_id, 'new_name', 'new_description' + ) diff --git a/openstack/tests/unit/cloud/test_identity_roles.py b/openstack/tests/unit/cloud/test_identity_roles.py index 045552325..c89562834 100644 --- a/openstack/tests/unit/cloud/test_identity_roles.py +++ b/openstack/tests/unit/cloud/test_identity_roles.py @@ -23,46 +23,65 @@ "links": {"assignment": "http://example"}, "role": {"id": "123456"}, "scope": {"domain": {"id": "161718"}}, - "user": {"id": "313233"} + "user": {"id": "313233"}, }, { "links": {"assignment": "http://example"}, "group": {"id": "101112"}, "role": {"id": "123456"}, - "scope": {"project": {"id": "456789"}} - } + "scope": {"project": {"id": "456789"}}, + }, ] class TestIdentityRoles(base.TestCase): - - def get_mock_url(self, service_type='identity', interface='public', - resource='roles', append=None, base_url_append='v3', - qs_elements=None): + def get_mock_url( + self, + service_type='identity', + interface='public', + resource='roles', + append=None, + base_url_append='v3', + qs_elements=None, + ): return super(TestIdentityRoles, self).get_mock_url( - service_type, interface, resource, append, base_url_append, - qs_elements) + service_type, + interface, + resource, + append, + base_url_append, + qs_elements, + ) def test_list_roles(self): role_data = self._get_role_data() - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(), - status_code=200, - json={'roles': [role_data.json_response['role']]}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'roles': [role_data.json_response['role']]}, + ) + ] + ) self.cloud.list_roles() self.assert_calls() def test_list_role_by_name(self): role_data = self._get_role_data() - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - qs_elements=['name={0}'.format(role_data.role_name)]), - status_code=200, - json={'roles': [role_data.json_response['role']]}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + qs_elements=['name={0}'.format(role_data.role_name)] + ), + status_code=200, + json={'roles': [role_data.json_response['role']]}, + ) + ] + ) role = self.cloud.list_roles(name=role_data.role_name)[0] self.assertIsNotNone(role) @@ -72,12 +91,16 @@ def test_list_role_by_name(self): def test_get_role_by_name(self): role_data = self._get_role_data() - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(), - status_code=200, - json={'roles': [role_data.json_response['role']]}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'roles': [role_data.json_response['role']]}, + ) + ] + ) role = self.cloud.get_role(role_data.role_name) self.assertIsNotNone(role) @@ -87,12 +110,16 @@ def test_get_role_by_name(self): def test_get_role_by_id(self): role_data = self._get_role_data() - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(), - status_code=200, - json={'roles': [role_data.json_response['role']]}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'roles': [role_data.json_response['role']]}, + ) + ] + ) role = self.cloud.get_role(role_data.role_id) self.assertIsNotNone(role) @@ -102,13 +129,17 @@ def test_get_role_by_id(self): def test_create_role(self): role_data = self._get_role_data() - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url(), - status_code=200, - json=role_data.json_response, - validate=dict(json=role_data.json_request)) - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url(), + status_code=200, + json=role_data.json_response, + validate=dict(json=role_data.json_request), + ) + ] + ) role = self.cloud.create_role(role_data.role_name) @@ -120,20 +151,25 @@ def test_create_role(self): def test_update_role(self): role_data = self._get_role_data() req = {'role': {'name': 'new_name'}} - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(), - status_code=200, - json={'roles': [role_data.json_response['role']]}), - dict(method='PATCH', - uri=self.get_mock_url(append=[role_data.role_id]), - status_code=200, - json=role_data.json_response, - validate=dict(json=req)) - ]) - - role = self.cloud.update_role( - role_data.role_id, 'new_name') + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'roles': [role_data.json_response['role']]}, + ), + dict( + method='PATCH', + uri=self.get_mock_url(append=[role_data.role_id]), + status_code=200, + json=role_data.json_response, + validate=dict(json=req), + ), + ] + ) + + role = self.cloud.update_role(role_data.role_id, 'new_name') self.assertIsNotNone(role) self.assertThat(role.name, matchers.Equals(role_data.role_name)) @@ -142,30 +178,42 @@ def test_update_role(self): def test_delete_role_by_id(self): role_data = self._get_role_data() - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(), - status_code=200, - json={'roles': [role_data.json_response['role']]}), - dict(method='DELETE', - uri=self.get_mock_url(append=[role_data.role_id]), - status_code=204) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'roles': [role_data.json_response['role']]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url(append=[role_data.role_id]), + status_code=204, + ), + ] + ) role = self.cloud.delete_role(role_data.role_id) self.assertThat(role, matchers.Equals(True)) self.assert_calls() def test_delete_role_by_name(self): role_data = self._get_role_data() - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(), - status_code=200, - json={'roles': [role_data.json_response['role']]}), - dict(method='DELETE', - uri=self.get_mock_url(append=[role_data.role_id]), - status_code=204) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'roles': [role_data.json_response['role']]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url(append=[role_data.role_id]), + status_code=204, + ), + ] + ) role = self.cloud.delete_role(role_data.role_name) self.assertThat(role, matchers.Equals(True)) self.assert_calls() @@ -177,78 +225,102 @@ def test_list_role_assignments(self): project_data = self._get_project_data(domain_id=domain_data.domain_id) role_data = self._get_role_data() response = [ - {'links': 'https://example.com', - 'role': {'id': role_data.role_id}, - 'scope': {'domain': {'id': domain_data.domain_id}}, - 'user': {'id': user_data.user_id}}, - {'links': 'https://example.com', - 'role': {'id': role_data.role_id}, - 'scope': {'project': {'id': project_data.project_id}}, - 'group': {'id': group_data.group_id}}, + { + 'links': 'https://example.com', + 'role': {'id': role_data.role_id}, + 'scope': {'domain': {'id': domain_data.domain_id}}, + 'user': {'id': user_data.user_id}, + }, + { + 'links': 'https://example.com', + 'role': {'id': role_data.role_id}, + 'scope': {'project': {'id': project_data.project_id}}, + 'group': {'id': group_data.group_id}, + }, ] - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments'), - status_code=200, - json={'role_assignments': response}, - complete_qs=True) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(resource='role_assignments'), + status_code=200, + json={'role_assignments': response}, + complete_qs=True, + ) + ] + ) ret = self.cloud.list_role_assignments() self.assertThat(len(ret), matchers.Equals(2)) self.assertThat(ret[0].user['id'], matchers.Equals(user_data.user_id)) self.assertThat(ret[0].role['id'], matchers.Equals(role_data.role_id)) self.assertThat( ret[0].scope['domain']['id'], - matchers.Equals(domain_data.domain_id)) + matchers.Equals(domain_data.domain_id), + ) self.assertThat( - ret[1].group['id'], - matchers.Equals(group_data.group_id)) + ret[1].group['id'], matchers.Equals(group_data.group_id) + ) self.assertThat(ret[1].role['id'], matchers.Equals(role_data.role_id)) self.assertThat( ret[1].scope['project']['id'], - matchers.Equals(project_data.project_id)) + matchers.Equals(project_data.project_id), + ) def test_list_role_assignments_filters(self): domain_data = self._get_domain_data() user_data = self._get_user_data(domain_id=domain_data.domain_id) role_data = self._get_role_data() response = [ - {'links': 'https://example.com', - 'role': {'id': role_data.role_id}, - 'scope': {'domain': {'id': domain_data.domain_id}}, - 'user': {'id': user_data.user_id}} + { + 'links': 'https://example.com', + 'role': {'id': role_data.role_id}, + 'scope': {'domain': {'id': domain_data.domain_id}}, + 'user': {'id': user_data.user_id}, + } ] - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - resource='role_assignments', - qs_elements=['scope.domain.id=%s' % domain_data.domain_id, - 'user.id=%s' % user_data.user_id, - 'effective=True']), - status_code=200, - json={'role_assignments': response}, - complete_qs=True) - ]) - params = dict(user=user_data.user_id, domain=domain_data.domain_id, - effective=True) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='role_assignments', + qs_elements=[ + 'scope.domain.id=%s' % domain_data.domain_id, + 'user.id=%s' % user_data.user_id, + 'effective=True', + ], + ), + status_code=200, + json={'role_assignments': response}, + complete_qs=True, + ) + ] + ) + params = dict( + user=user_data.user_id, + domain=domain_data.domain_id, + effective=True, + ) ret = self.cloud.list_role_assignments(filters=params) self.assertThat(len(ret), matchers.Equals(1)) self.assertThat(ret[0].user['id'], matchers.Equals(user_data.user_id)) self.assertThat(ret[0].role['id'], matchers.Equals(role_data.role_id)) self.assertThat( ret[0].scope['domain']['id'], - matchers.Equals(domain_data.domain_id)) + matchers.Equals(domain_data.domain_id), + ) def test_list_role_assignments_exception(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(resource='role_assignments'), - status_code=403) - ]) - with testtools.ExpectedException( - exceptions.ForbiddenException - ): + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(resource='role_assignments'), + status_code=403, + ) + ] + ) + with testtools.ExpectedException(exceptions.ForbiddenException): self.cloud.list_role_assignments() self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_identity_users.py b/openstack/tests/unit/cloud/test_identity_users.py index 943029d67..93b85500a 100644 --- a/openstack/tests/unit/cloud/test_identity_users.py +++ b/openstack/tests/unit/cloud/test_identity_users.py @@ -16,29 +16,46 @@ class TestIdentityUsers(base.TestCase): - - def get_mock_url(self, service_type='identity', interface='public', - resource='users', append=None, base_url_append='v3', - qs_elements=None): + def get_mock_url( + self, + service_type='identity', + interface='public', + resource='users', + append=None, + base_url_append='v3', + qs_elements=None, + ): return super(TestIdentityUsers, self).get_mock_url( - service_type, interface, resource, append, base_url_append, - qs_elements) + service_type, + interface, + resource, + append, + base_url_append, + qs_elements, + ) def test_create_user(self): domain_data = self._get_domain_data() - user_data = self._get_user_data("myusername", "mypassword", - domain_id=domain_data.domain_id) - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url(), - status_code=200, - json=user_data.json_response, - validate=dict(json=user_data.json_request)) - ]) + user_data = self._get_user_data( + "myusername", "mypassword", domain_id=domain_data.domain_id + ) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url(), + status_code=200, + json=user_data.json_response, + validate=dict(json=user_data.json_request), + ) + ] + ) - user = self.cloud.create_user(user_data.name, - password=user_data.password, - domain_id=domain_data.domain_id) + user = self.cloud.create_user( + user_data.name, + password=user_data.password, + domain_id=domain_data.domain_id, + ) self.assertIsNotNone(user) self.assertThat(user.name, matchers.Equals(user_data.name)) @@ -46,22 +63,29 @@ def test_create_user(self): def test_create_user_without_password(self): domain_data = self._get_domain_data() - user_data = self._get_user_data("myusername", - domain_id=domain_data.domain_id) + user_data = self._get_user_data( + "myusername", domain_id=domain_data.domain_id + ) user_data._replace( password=None, - json_request=user_data.json_request["user"].pop("password")) + json_request=user_data.json_request["user"].pop("password"), + ) - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url(), - status_code=200, - json=user_data.json_response, - validate=dict(json=user_data.json_request)) - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url(), + status_code=200, + json=user_data.json_response, + validate=dict(json=user_data.json_request), + ) + ] + ) - user = self.cloud.create_user(user_data.name, - domain_id=domain_data.domain_id) + user = self.cloud.create_user( + user_data.name, domain_id=domain_data.domain_id + ) self.assertIsNotNone(user) self.assertThat(user.name, matchers.Equals(user_data.name)) diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index dfd9be7fb..8e8b4b733 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -30,20 +30,20 @@ class BaseTestImage(base.TestCase): - def setUp(self): super(BaseTestImage, self).setUp() self.image_id = str(uuid.uuid4()) self.image_name = self.getUniqueString('image') - self.object_name = u'images/{name}'.format(name=self.image_name) + self.object_name = 'images/{name}'.format(name=self.image_name) self.imagefile = tempfile.NamedTemporaryFile(delete=False) data = b'\2\0' self.imagefile.write(data) self.imagefile.close() self.output = data self.fake_image_dict = fakes.make_fake_image( - image_id=self.image_id, image_name=self.image_name, - data=self.imagefile.name + image_id=self.image_id, + image_name=self.image_name, + data=self.imagefile.name, ) self.fake_search_return = {'images': [self.fake_image_dict]} self.container_name = self.getUniqueString('container') @@ -51,16 +51,17 @@ def setUp(self): def _compare_images(self, exp, real): self.assertDictEqual( image.Image(**exp).to_dict(computed=False), - real.to_dict(computed=False)) + real.to_dict(computed=False), + ) def _compare_images_v1(self, exp, real): self.assertDictEqual( image_v1.Image(**exp).to_dict(computed=False), - real.to_dict(computed=False)) + real.to_dict(computed=False), + ) class TestImage(BaseTestImage): - def setUp(self): super(TestImage, self).setUp() self.use_glance() @@ -71,9 +72,9 @@ def test_config_v1(self): # because glance has a bug where it doesn't return https properly. self.assertEqual( 'https://image.example.com/v1/', - self.cloud._image_client.get_endpoint()) - self.assertEqual( - '1', self.cloud_config.get_api_version('image')) + self.cloud._image_client.get_endpoint(), + ) + self.assertEqual('1', self.cloud_config.get_api_version('image')) def test_config_v2(self): self.cloud.config.config['image_api_version'] = '2' @@ -81,57 +82,89 @@ def test_config_v2(self): # because glance has a bug where it doesn't return https properly. self.assertEqual( 'https://image.example.com/v2/', - self.cloud._image_client.get_endpoint()) - self.assertEqual( - '2', self.cloud_config.get_api_version('image')) + self.cloud._image_client.get_endpoint(), + ) + self.assertEqual('2', self.cloud_config.get_api_version('image')) def test_download_image_no_output(self): - self.assertRaises(exc.OpenStackCloudException, - self.cloud.download_image, self.image_name) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.download_image, + self.image_name, + ) def test_download_image_two_outputs(self): fake_fd = io.BytesIO() - self.assertRaises(exc.OpenStackCloudException, - self.cloud.download_image, self.image_name, - output_path='fake_path', output_file=fake_fd) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.download_image, + self.image_name, + output_path='fake_path', + output_file=fake_fd, + ) def test_download_image_no_images_found(self): - self.register_uris([ - dict(method='GET', - uri='https://image.example.com/v2/images/{name}'.format( - name=self.image_name), - status_code=404), - dict(method='GET', - uri='https://image.example.com/v2/images?name={name}'.format( - name=self.image_name), - json=dict(images=[])), - dict(method='GET', - uri='https://image.example.com/v2/images?os_hidden=True', - json=dict(images=[]))]) - self.assertRaises(exc.OpenStackCloudResourceNotFound, - self.cloud.download_image, self.image_name, - output_path='fake_path') + self.register_uris( + [ + dict( + method='GET', + uri='https://image.example.com/v2/images/{name}'.format( + name=self.image_name + ), + status_code=404, + ), + dict( + method='GET', + uri='https://image.example.com/v2/images?name={name}'.format( # noqa: E501 + name=self.image_name + ), + json=dict(images=[]), + ), + dict( + method='GET', + uri='https://image.example.com/v2/images?os_hidden=True', + json=dict(images=[]), + ), + ] + ) + self.assertRaises( + exc.OpenStackCloudResourceNotFound, + self.cloud.download_image, + self.image_name, + output_path='fake_path', + ) self.assert_calls() def _register_image_mocks(self): - self.register_uris([ - dict(method='GET', - uri='https://image.example.com/v2/images/{name}'.format( - name=self.image_name), - status_code=404), - dict(method='GET', - uri='https://image.example.com/v2/images?name={name}'.format( - name=self.image_name), - json=self.fake_search_return), - dict(method='GET', - uri='https://image.example.com/v2/images/{id}/file'.format( - id=self.image_id), - content=self.output, - headers={ - 'Content-Type': 'application/octet-stream', - 'Content-MD5': self.fake_image_dict['checksum'] - }) - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://image.example.com/v2/images/{name}'.format( + name=self.image_name + ), + status_code=404, + ), + dict( + method='GET', + uri='https://image.example.com/v2/images?name={name}'.format( # noqa: E501 + name=self.image_name + ), + json=self.fake_search_return, + ), + dict( + method='GET', + uri='https://image.example.com/v2/images/{id}/file'.format( + id=self.image_id + ), + content=self.output, + headers={ + 'Content-Type': 'application/octet-stream', + 'Content-MD5': self.fake_image_dict['checksum'], + }, + ), + ] + ) def test_download_image_with_fd(self): self._register_image_mocks() @@ -145,62 +178,82 @@ def test_download_image_with_path(self): self._register_image_mocks() output_file = tempfile.NamedTemporaryFile() self.cloud.download_image( - self.image_name, output_path=output_file.name) + self.image_name, output_path=output_file.name + ) output_file.seek(0) self.assertEqual(output_file.read(), self.output) self.assert_calls() def test_get_image_name(self, cloud=None): cloud = cloud or self.cloud - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - json=self.fake_search_return), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - json=self.fake_search_return), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2' + ), + json=self.fake_search_return, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2' + ), + json=self.fake_search_return, + ), + ] + ) + self.assertEqual(self.image_name, cloud.get_image_name(self.image_id)) self.assertEqual( - self.image_name, cloud.get_image_name(self.image_id)) - self.assertEqual( - self.image_name, cloud.get_image_name(self.image_name)) + self.image_name, cloud.get_image_name(self.image_name) + ) self.assert_calls() def test_get_image_by_id(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images', self.image_id], - base_url_append='v2'), - json=self.fake_image_dict) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images', self.image_id], + base_url_append='v2', + ), + json=self.fake_image_dict, + ) + ] + ) self._compare_images( - self.fake_image_dict, - self.cloud.get_image_by_id(self.image_id) + self.fake_image_dict, self.cloud.get_image_by_id(self.image_id) ) self.assert_calls() def test_get_image_id(self, cloud=None): cloud = cloud or self.cloud - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - json=self.fake_search_return), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - json=self.fake_search_return), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2' + ), + json=self.fake_search_return, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2' + ), + json=self.fake_search_return, + ), + ] + ) - self.assertEqual( - self.image_id, cloud.get_image_id(self.image_id)) - self.assertEqual( - self.image_id, cloud.get_image_id(self.image_name)) + self.assertEqual(self.image_id, cloud.get_image_id(self.image_id)) + self.assertEqual(self.image_id, cloud.get_image_id(self.image_name)) self.assert_calls() @@ -213,338 +266,512 @@ def test_get_image_id_operator(self): self.test_get_image_id(cloud=self.cloud) def test_empty_list_images(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - json={'images': []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2' + ), + json={'images': []}, + ) + ] + ) self.assertEqual([], self.cloud.list_images()) self.assert_calls() def test_list_images(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - json=self.fake_search_return) - ]) - [self._compare_images(a, b) for a, b in zip( - [self.fake_image_dict], - self.cloud.list_images())] + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2' + ), + json=self.fake_search_return, + ) + ] + ) + [ + self._compare_images(a, b) + for a, b in zip([self.fake_image_dict], self.cloud.list_images()) + ] self.assert_calls() def test_list_images_show_all(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2', - qs_elements=['member_status=all']), - json=self.fake_search_return) - ]) - [self._compare_images(a, b) for a, b in zip( - [self.fake_image_dict], - self.cloud.list_images(show_all=True))] + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images'], + base_url_append='v2', + qs_elements=['member_status=all'], + ), + json=self.fake_search_return, + ) + ] + ) + [ + self._compare_images(a, b) + for a, b in zip( + [self.fake_image_dict], self.cloud.list_images(show_all=True) + ) + ] self.assert_calls() def test_list_images_show_all_deleted(self): deleted_image = self.fake_image_dict.copy() deleted_image['status'] = 'deleted' - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2', - qs_elements=['member_status=all']), - json={'images': [self.fake_image_dict, deleted_image]}) - ]) - [self._compare_images(a, b) for a, b in zip( - [self.fake_image_dict], - self.cloud.list_images(show_all=True))] + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images'], + base_url_append='v2', + qs_elements=['member_status=all'], + ), + json={'images': [self.fake_image_dict, deleted_image]}, + ) + ] + ) + [ + self._compare_images(a, b) + for a, b in zip( + [self.fake_image_dict], self.cloud.list_images(show_all=True) + ) + ] self.assert_calls() def test_list_images_no_filter_deleted(self): deleted_image = self.fake_image_dict.copy() deleted_image['status'] = 'deleted' - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - json={'images': [self.fake_image_dict, deleted_image]}) - ]) - [self._compare_images(a, b) for a, b in zip( - [self.fake_image_dict], - self.cloud.list_images(filter_deleted=False))] + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2' + ), + json={'images': [self.fake_image_dict, deleted_image]}, + ) + ] + ) + [ + self._compare_images(a, b) + for a, b in zip( + [self.fake_image_dict], + self.cloud.list_images(filter_deleted=False), + ) + ] self.assert_calls() def test_list_images_filter_deleted(self): deleted_image = self.fake_image_dict.copy() deleted_image['status'] = 'deleted' - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - json={'images': [self.fake_image_dict, deleted_image]}) - ]) - [self._compare_images(a, b) for a, b in zip( - [self.fake_image_dict], - self.cloud.list_images())] + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2' + ), + json={'images': [self.fake_image_dict, deleted_image]}, + ) + ] + ) + [ + self._compare_images(a, b) + for a, b in zip([self.fake_image_dict], self.cloud.list_images()) + ] self.assert_calls() def test_list_images_string_properties(self): image_dict = self.fake_image_dict.copy() image_dict['properties'] = 'list,of,properties' - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - json={'images': [image_dict]}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2' + ), + json={'images': [image_dict]}, + ), + ] + ) images = self.cloud.list_images() - [self._compare_images(a, b) for a, b in zip( - [image_dict], - images)] + [self._compare_images(a, b) for a, b in zip([image_dict], images)] self.assertEqual( - images[0]['properties']['properties'], - 'list,of,properties') + images[0]['properties']['properties'], 'list,of,properties' + ) self.assert_calls() def test_list_images_paginated(self): marker = str(uuid.uuid4()) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - json={'images': [self.fake_image_dict], - 'next': '/v2/images?marker={marker}'.format( - marker=marker)}), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2', - qs_elements=['marker={marker}'.format(marker=marker)]), - json=self.fake_search_return) - ]) - [self._compare_images(a, b) for a, b in zip( - [self.fake_image_dict], - self.cloud.list_images())] + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2' + ), + json={ + 'images': [self.fake_image_dict], + 'next': '/v2/images?marker={marker}'.format( + marker=marker + ), + }, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images'], + base_url_append='v2', + qs_elements=['marker={marker}'.format(marker=marker)], + ), + json=self.fake_search_return, + ), + ] + ) + [ + self._compare_images(a, b) + for a, b in zip([self.fake_image_dict], self.cloud.list_images()) + ] self.assert_calls() def test_create_image_put_v2_no_import(self): self.cloud.image_api_use_tasks = False - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images', self.image_name], - base_url_append='v2'), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], - base_url_append='v2', - qs_elements=['name=' + self.image_name]), - validate=dict(), - json={'images': []}), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], - base_url_append='v2', - qs_elements=['os_hidden=True']), - json={'images': []}), - dict(method='POST', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - json=self.fake_image_dict, - validate=dict( - json={ - u'container_format': u'bare', - u'disk_format': u'qcow2', - u'name': self.image_name, - u'owner_specified.openstack.md5': - self.fake_image_dict[ - 'owner_specified.openstack.md5'], - u'owner_specified.openstack.object': self.object_name, - u'owner_specified.openstack.sha256': - self.fake_image_dict[ - 'owner_specified.openstack.sha256'], - u'visibility': u'private', - u'tags': [u'tag1', u'tag2']}) - ), - dict(method='PUT', - uri=self.get_mock_url( - 'image', append=['images', self.image_id, 'file'], - base_url_append='v2'), - request_headers={'Content-Type': 'application/octet-stream'}), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images', self.fake_image_dict['id']], - base_url_append='v2' - ), - json=self.fake_image_dict), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - complete_qs=True, - json=self.fake_search_return) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images', self.image_name], + base_url_append='v2', + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images'], + base_url_append='v2', + qs_elements=['name=' + self.image_name], + ), + validate=dict(), + json={'images': []}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images'], + base_url_append='v2', + qs_elements=['os_hidden=True'], + ), + json={'images': []}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2' + ), + json=self.fake_image_dict, + validate=dict( + json={ + 'container_format': 'bare', + 'disk_format': 'qcow2', + 'name': self.image_name, + 'owner_specified.openstack.md5': self.fake_image_dict[ # noqa: E501 + 'owner_specified.openstack.md5' + ], + 'owner_specified.openstack.object': self.object_name, # noqa: E501 + 'owner_specified.openstack.sha256': self.fake_image_dict[ # noqa: E501 + 'owner_specified.openstack.sha256' + ], + 'visibility': 'private', + 'tags': ['tag1', 'tag2'], + } + ), + ), + dict( + method='PUT', + uri=self.get_mock_url( + 'image', + append=['images', self.image_id, 'file'], + base_url_append='v2', + ), + request_headers={ + 'Content-Type': 'application/octet-stream' + }, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images', self.fake_image_dict['id']], + base_url_append='v2', + ), + json=self.fake_image_dict, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2' + ), + complete_qs=True, + json=self.fake_search_return, + ), + ] + ) self.cloud.create_image( - self.image_name, self.imagefile.name, wait=True, timeout=1, + self.image_name, + self.imagefile.name, + wait=True, + timeout=1, tags=['tag1', 'tag2'], - is_public=False, validate_checksum=True) + is_public=False, + validate_checksum=True, + ) self.assert_calls() - self.assertEqual(self.adapter.request_history[7].text.read(), - self.output) + self.assertEqual( + self.adapter.request_history[7].text.read(), self.output + ) def test_create_image_put_v2_import_supported(self): self.cloud.image_api_use_tasks = False - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images', self.image_name], - base_url_append='v2'), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], - base_url_append='v2', - qs_elements=['name=' + self.image_name]), - validate=dict(), - json={'images': []}), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], - base_url_append='v2', - qs_elements=['os_hidden=True']), - json={'images': []}), - dict(method='POST', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - json=self.fake_image_dict, - headers={ - 'OpenStack-image-import-methods': IMPORT_METHODS, - }, - validate=dict( - json={ - u'container_format': u'bare', - u'disk_format': u'qcow2', - u'name': self.image_name, - u'owner_specified.openstack.md5': - self.fake_image_dict[ - 'owner_specified.openstack.md5'], - u'owner_specified.openstack.object': self.object_name, - u'owner_specified.openstack.sha256': - self.fake_image_dict[ - 'owner_specified.openstack.sha256'], - u'visibility': u'private', - u'tags': [u'tag1', u'tag2']}) - ), - dict(method='PUT', - uri=self.get_mock_url( - 'image', append=['images', self.image_id, 'file'], - base_url_append='v2'), - request_headers={'Content-Type': 'application/octet-stream'}), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images', self.fake_image_dict['id']], - base_url_append='v2' - ), - json=self.fake_image_dict), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - complete_qs=True, - json=self.fake_search_return) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images', self.image_name], + base_url_append='v2', + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images'], + base_url_append='v2', + qs_elements=['name=' + self.image_name], + ), + validate=dict(), + json={'images': []}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images'], + base_url_append='v2', + qs_elements=['os_hidden=True'], + ), + json={'images': []}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2' + ), + json=self.fake_image_dict, + headers={ + 'OpenStack-image-import-methods': IMPORT_METHODS, + }, + validate=dict( + json={ + 'container_format': 'bare', + 'disk_format': 'qcow2', + 'name': self.image_name, + 'owner_specified.openstack.md5': self.fake_image_dict[ # noqa: E501 + 'owner_specified.openstack.md5' + ], + 'owner_specified.openstack.object': self.object_name, # noqa: E501 + 'owner_specified.openstack.sha256': self.fake_image_dict[ # noqa: E501 + 'owner_specified.openstack.sha256' + ], + 'visibility': 'private', + 'tags': ['tag1', 'tag2'], + } + ), + ), + dict( + method='PUT', + uri=self.get_mock_url( + 'image', + append=['images', self.image_id, 'file'], + base_url_append='v2', + ), + request_headers={ + 'Content-Type': 'application/octet-stream' + }, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images', self.fake_image_dict['id']], + base_url_append='v2', + ), + json=self.fake_image_dict, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2' + ), + complete_qs=True, + json=self.fake_search_return, + ), + ] + ) self.cloud.create_image( - self.image_name, self.imagefile.name, wait=True, timeout=1, + self.image_name, + self.imagefile.name, + wait=True, + timeout=1, tags=['tag1', 'tag2'], - is_public=False, validate_checksum=True) + is_public=False, + validate_checksum=True, + ) self.assert_calls() - self.assertEqual(self.adapter.request_history[7].text.read(), - self.output) + self.assertEqual( + self.adapter.request_history[7].text.read(), self.output + ) def test_create_image_use_import(self): self.cloud.image_api_use_tasks = False - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images', self.image_name], - base_url_append='v2'), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], - base_url_append='v2', - qs_elements=['name=' + self.image_name]), - validate=dict(), - json={'images': []}), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], - base_url_append='v2', - qs_elements=['os_hidden=True']), - json={'images': []}), - dict(method='POST', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - json=self.fake_image_dict, - headers={ - 'OpenStack-image-import-methods': IMPORT_METHODS, - }, - validate=dict( - json={ - u'container_format': u'bare', - u'disk_format': u'qcow2', - u'name': self.image_name, - u'owner_specified.openstack.md5': - self.fake_image_dict[ - 'owner_specified.openstack.md5'], - u'owner_specified.openstack.object': self.object_name, - u'owner_specified.openstack.sha256': - self.fake_image_dict[ - 'owner_specified.openstack.sha256'], - u'visibility': u'private', - u'tags': [u'tag1', u'tag2']}) - ), - dict(method='PUT', - uri=self.get_mock_url( - 'image', append=['images', self.image_id, 'stage'], - base_url_append='v2'), - request_headers={'Content-Type': 'application/octet-stream'}), - dict(method='POST', - uri=self.get_mock_url( - 'image', append=['images', self.image_id, 'import'], - base_url_append='v2'), - json={'method': {'name': 'glance-direct'}}), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images', self.fake_image_dict['id']], - base_url_append='v2' - ), - json=self.fake_image_dict), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - complete_qs=True, - json=self.fake_search_return) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images', self.image_name], + base_url_append='v2', + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images'], + base_url_append='v2', + qs_elements=['name=' + self.image_name], + ), + validate=dict(), + json={'images': []}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images'], + base_url_append='v2', + qs_elements=['os_hidden=True'], + ), + json={'images': []}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2' + ), + json=self.fake_image_dict, + headers={ + 'OpenStack-image-import-methods': IMPORT_METHODS, + }, + validate=dict( + json={ + 'container_format': 'bare', + 'disk_format': 'qcow2', + 'name': self.image_name, + 'owner_specified.openstack.md5': self.fake_image_dict[ # noqa: E501 + 'owner_specified.openstack.md5' + ], + 'owner_specified.openstack.object': self.object_name, # noqa: E501 + 'owner_specified.openstack.sha256': self.fake_image_dict[ # noqa: E501 + 'owner_specified.openstack.sha256' + ], + 'visibility': 'private', + 'tags': ['tag1', 'tag2'], + } + ), + ), + dict( + method='PUT', + uri=self.get_mock_url( + 'image', + append=['images', self.image_id, 'stage'], + base_url_append='v2', + ), + request_headers={ + 'Content-Type': 'application/octet-stream' + }, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'image', + append=['images', self.image_id, 'import'], + base_url_append='v2', + ), + json={'method': {'name': 'glance-direct'}}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images', self.fake_image_dict['id']], + base_url_append='v2', + ), + json=self.fake_image_dict, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2' + ), + complete_qs=True, + json=self.fake_search_return, + ), + ] + ) self.cloud.create_image( - self.image_name, self.imagefile.name, wait=True, timeout=1, + self.image_name, + self.imagefile.name, + wait=True, + timeout=1, tags=['tag1', 'tag2'], - is_public=False, validate_checksum=True, + is_public=False, + validate_checksum=True, use_import=True, ) self.assert_calls() - self.assertEqual(self.adapter.request_history[7].text.read(), - self.output) + self.assertEqual( + self.adapter.request_history[7].text.read(), self.output + ) def test_create_image_task(self): self.cloud.image_api_use_tasks = True @@ -565,159 +792,254 @@ def test_create_image_task(self): del image_no_checksums['owner_specified.openstack.sha256'] del image_no_checksums['owner_specified.openstack.object'] - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images', self.image_name], - base_url_append='v2'), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], - base_url_append='v2', - qs_elements=['name=' + self.image_name]), - validate=dict(), - json={'images': []}), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], - base_url_append='v2', - qs_elements=['os_hidden=True']), - json={'images': []}), - dict(method='HEAD', - uri='{endpoint}/{container}'.format( - endpoint=endpoint, container=self.container_name), - status_code=404), - dict(method='PUT', - uri='{endpoint}/{container}'.format( - endpoint=endpoint, container=self.container_name), - status_code=201, - headers={'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', - 'Content-Length': '0', - 'Content-Type': 'text/html; charset=UTF-8'}), - dict(method='HEAD', - uri='{endpoint}/{container}'.format( - endpoint=endpoint, container=self.container_name), - headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}), - dict(method='GET', - # This is explicitly not using get_mock_url because that - # gets us a project-id oriented URL. - uri='https://object-store.example.com/info', - json=dict( - swift={'max_file_size': 1000}, - slo={'min_segment_size': 500})), - dict(method='HEAD', - uri='{endpoint}/{container}/{object}'.format( - endpoint=endpoint, container=self.container_name, - object=self.image_name), - status_code=404), - dict(method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=endpoint, container=self.container_name, - object=self.image_name), - status_code=201, - validate=dict( - headers={'X-Object-Meta-x-sdk-md5': - self.fake_image_dict[ - 'owner_specified.openstack.md5'], - 'X-Object-Meta-x-sdk-sha256': - self.fake_image_dict[ - 'owner_specified.openstack.sha256']}) - ), - dict(method='POST', - uri=self.get_mock_url( - 'image', append=['tasks'], base_url_append='v2'), - json={'id': task_id, 'status': 'processing'}, - validate=dict( - json=dict( - type='import', input={ - 'import_from': '{container}/{object}'.format( - container=self.container_name, - object=self.image_name), - 'image_properties': {'name': self.image_name}})) - ), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['tasks', task_id], base_url_append='v2'), - status_code=503, text='Random error'), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['tasks', task_id], base_url_append='v2'), - json=args), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images', self.image_id], - base_url_append='v2'), - json=image_no_checksums), - dict(method='PATCH', - uri=self.get_mock_url( - 'image', append=['images', self.image_id], - base_url_append='v2'), - validate=dict( - json=sorted([ - {u'op': u'add', - u'value': '{container}/{object}'.format( - container=self.container_name, - object=self.image_name), - u'path': u'/owner_specified.openstack.object'}, - {u'op': u'add', u'value': - self.fake_image_dict[ - 'owner_specified.openstack.md5'], - u'path': u'/owner_specified.openstack.md5'}, - {u'op': u'add', u'value': - self.fake_image_dict[ - 'owner_specified.openstack.sha256'], - u'path': u'/owner_specified.openstack.sha256'}], - key=operator.itemgetter('path')), - headers={ - 'Content-Type': - 'application/openstack-images-v2.1-json-patch'}), - json=self.fake_search_return - ), - dict(method='HEAD', - uri='{endpoint}/{container}/{object}'.format( - endpoint=endpoint, container=self.container_name, - object=self.image_name), - headers={ - 'X-Timestamp': '1429036140.50253', - 'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1', - 'Content-Length': '1290170880', - 'Last-Modified': 'Tue, 14 Apr 2015 18:29:01 GMT', - 'X-Object-Meta-X-Sdk-Sha256': - self.fake_image_dict[ - 'owner_specified.openstack.sha256'], - 'X-Object-Meta-X-Sdk-Md5': - self.fake_image_dict[ - 'owner_specified.openstack.md5'], - 'Date': 'Thu, 16 Nov 2017 15:24:30 GMT', - 'Accept-Ranges': 'bytes', - 'Content-Type': 'application/octet-stream', - 'Etag': fakes.NO_MD5}), - dict(method='DELETE', - uri='{endpoint}/{container}/{object}'.format( - endpoint=endpoint, container=self.container_name, - object=self.image_name)), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - complete_qs=True, - json=self.fake_search_return) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images', self.image_name], + base_url_append='v2', + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images'], + base_url_append='v2', + qs_elements=['name=' + self.image_name], + ), + validate=dict(), + json={'images': []}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images'], + base_url_append='v2', + qs_elements=['os_hidden=True'], + ), + json={'images': []}, + ), + dict( + method='HEAD', + uri='{endpoint}/{container}'.format( + endpoint=endpoint, container=self.container_name + ), + status_code=404, + ), + dict( + method='PUT', + uri='{endpoint}/{container}'.format( + endpoint=endpoint, container=self.container_name + ), + status_code=201, + headers={ + 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=UTF-8', + }, + ), + dict( + method='HEAD', + uri='{endpoint}/{container}'.format( + endpoint=endpoint, container=self.container_name + ), + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8', + }, + ), + dict( + method='GET', + # This is explicitly not using get_mock_url because that + # gets us a project-id oriented URL. + uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': 1000}, + slo={'min_segment_size': 500}, + ), + ), + dict( + method='HEAD', + uri='{endpoint}/{container}/{object}'.format( + endpoint=endpoint, + container=self.container_name, + object=self.image_name, + ), + status_code=404, + ), + dict( + method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=endpoint, + container=self.container_name, + object=self.image_name, + ), + status_code=201, + validate=dict( + headers={ + 'X-Object-Meta-x-sdk-md5': self.fake_image_dict[ + 'owner_specified.openstack.md5' + ], + 'X-Object-Meta-x-sdk-sha256': self.fake_image_dict[ + 'owner_specified.openstack.sha256' + ], + } + ), + ), + dict( + method='POST', + uri=self.get_mock_url( + 'image', append=['tasks'], base_url_append='v2' + ), + json={'id': task_id, 'status': 'processing'}, + validate=dict( + json=dict( + type='import', + input={ + 'import_from': '{container}/{object}'.format( + container=self.container_name, + object=self.image_name, + ), + 'image_properties': {'name': self.image_name}, + }, + ) + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['tasks', task_id], + base_url_append='v2', + ), + status_code=503, + text='Random error', + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['tasks', task_id], + base_url_append='v2', + ), + json=args, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images', self.image_id], + base_url_append='v2', + ), + json=image_no_checksums, + ), + dict( + method='PATCH', + uri=self.get_mock_url( + 'image', + append=['images', self.image_id], + base_url_append='v2', + ), + validate=dict( + json=sorted( + [ + { + 'op': 'add', + 'value': '{container}/{object}'.format( + container=self.container_name, + object=self.image_name, + ), + 'path': '/owner_specified.openstack.object', # noqa: E501 + }, + { + 'op': 'add', + 'value': self.fake_image_dict[ + 'owner_specified.openstack.md5' + ], + 'path': '/owner_specified.openstack.md5', + }, + { + 'op': 'add', + 'value': self.fake_image_dict[ + 'owner_specified.openstack.sha256' + ], + 'path': '/owner_specified.openstack.sha256', # noqa: E501 + }, + ], + key=operator.itemgetter('path'), + ), + headers={ + 'Content-Type': 'application/openstack-images-v2.1-json-patch' # noqa: E501 + }, + ), + json=self.fake_search_return, + ), + dict( + method='HEAD', + uri='{endpoint}/{container}/{object}'.format( + endpoint=endpoint, + container=self.container_name, + object=self.image_name, + ), + headers={ + 'X-Timestamp': '1429036140.50253', + 'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1', + 'Content-Length': '1290170880', + 'Last-Modified': 'Tue, 14 Apr 2015 18:29:01 GMT', + 'X-Object-Meta-X-Sdk-Sha256': self.fake_image_dict[ + 'owner_specified.openstack.sha256' + ], + 'X-Object-Meta-X-Sdk-Md5': self.fake_image_dict[ + 'owner_specified.openstack.md5' + ], + 'Date': 'Thu, 16 Nov 2017 15:24:30 GMT', + 'Accept-Ranges': 'bytes', + 'Content-Type': 'application/octet-stream', + 'Etag': fakes.NO_MD5, + }, + ), + dict( + method='DELETE', + uri='{endpoint}/{container}/{object}'.format( + endpoint=endpoint, + container=self.container_name, + object=self.image_name, + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2' + ), + complete_qs=True, + json=self.fake_search_return, + ), + ] + ) self.cloud.create_image( - self.image_name, self.imagefile.name, wait=True, timeout=1, - disk_format='vhd', container_format='ovf', - is_public=False, validate_checksum=True, - container=self.container_name) + self.image_name, + self.imagefile.name, + wait=True, + timeout=1, + disk_format='vhd', + container_format='ovf', + is_public=False, + validate_checksum=True, + container=self.container_name, + ) self.assert_calls() @@ -725,7 +1047,8 @@ def test_delete_autocreated_no_tasks(self): self.use_keystone_v3() self.cloud.image_api_use_tasks = False deleted = self.cloud.delete_autocreated_image_objects( - container=self.container_name) + container=self.container_name + ) self.assertFalse(deleted) self.assert_calls([]) @@ -740,38 +1063,51 @@ def test_delete_image_task(self): del image_no_checksums['owner_specified.openstack.sha256'] del image_no_checksums['owner_specified.openstack.object'] - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - json=self.fake_search_return), - dict(method='DELETE', - uri='https://image.example.com/v2/images/{id}'.format( - id=self.image_id)), - dict(method='HEAD', - uri='{endpoint}/{object}'.format( - endpoint=endpoint, - object=object_path), - headers={ - 'X-Timestamp': '1429036140.50253', - 'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1', - 'Content-Length': '1290170880', - 'Last-Modified': 'Tue, 14 Apr 2015 18:29:01 GMT', - 'X-Object-Meta-X-Sdk-Sha256': - self.fake_image_dict[ - 'owner_specified.openstack.sha256'], - 'X-Object-Meta-X-Sdk-Md5': - self.fake_image_dict[ - 'owner_specified.openstack.md5'], - 'Date': 'Thu, 16 Nov 2017 15:24:30 GMT', - 'Accept-Ranges': 'bytes', - 'Content-Type': 'application/octet-stream', - 'Etag': fakes.NO_MD5}), - dict(method='DELETE', - uri='{endpoint}/{object}'.format( - endpoint=endpoint, - object=object_path)), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2' + ), + json=self.fake_search_return, + ), + dict( + method='DELETE', + uri='https://image.example.com/v2/images/{id}'.format( + id=self.image_id + ), + ), + dict( + method='HEAD', + uri='{endpoint}/{object}'.format( + endpoint=endpoint, object=object_path + ), + headers={ + 'X-Timestamp': '1429036140.50253', + 'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1', + 'Content-Length': '1290170880', + 'Last-Modified': 'Tue, 14 Apr 2015 18:29:01 GMT', + 'X-Object-Meta-X-Sdk-Sha256': self.fake_image_dict[ + 'owner_specified.openstack.sha256' + ], + 'X-Object-Meta-X-Sdk-Md5': self.fake_image_dict[ + 'owner_specified.openstack.md5' + ], + 'Date': 'Thu, 16 Nov 2017 15:24:30 GMT', + 'Accept-Ranges': 'bytes', + 'Content-Type': 'application/octet-stream', + 'Etag': fakes.NO_MD5, + }, + ), + dict( + method='DELETE', + uri='{endpoint}/{object}'.format( + endpoint=endpoint, object=object_path + ), + ), + ] + ) self.cloud.delete_image(self.image_id) @@ -783,69 +1119,91 @@ def test_delete_autocreated_image_objects(self): endpoint = self.cloud._object_store_client.get_endpoint() other_image = self.getUniqueString('no-delete') - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - service_type='object-store', - resource=self.container_name, - qs_elements=['format=json']), - json=[{ - 'content_type': 'application/octet-stream', - 'bytes': 1437258240, - 'hash': '249219347276c331b87bf1ac2152d9af', - 'last_modified': '2015-02-16T17:50:05.289600', - 'name': other_image, - }, { - 'content_type': 'application/octet-stream', - 'bytes': 1290170880, - 'hash': fakes.NO_MD5, - 'last_modified': '2015-04-14T18:29:00.502530', - 'name': self.image_name, - }]), - dict(method='HEAD', - uri=self.get_mock_url( - service_type='object-store', - resource=self.container_name, - append=[other_image]), - headers={ - 'X-Timestamp': '1429036140.50253', - 'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1', - 'Content-Length': '1290170880', - 'Last-Modified': 'Tue, 14 Apr 2015 18:29:01 GMT', - 'X-Object-Meta-X-Shade-Sha256': 'does not matter', - 'X-Object-Meta-X-Shade-Md5': 'does not matter', - 'Date': 'Thu, 16 Nov 2017 15:24:30 GMT', - 'Accept-Ranges': 'bytes', - 'Content-Type': 'application/octet-stream', - 'Etag': '249219347276c331b87bf1ac2152d9af', - }), - dict(method='HEAD', - uri=self.get_mock_url( - service_type='object-store', - resource=self.container_name, - append=[self.image_name]), - headers={ - 'X-Timestamp': '1429036140.50253', - 'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1', - 'Content-Length': '1290170880', - 'Last-Modified': 'Tue, 14 Apr 2015 18:29:01 GMT', - 'X-Object-Meta-X-Shade-Sha256': fakes.NO_SHA256, - 'X-Object-Meta-X-Shade-Md5': fakes.NO_MD5, - 'Date': 'Thu, 16 Nov 2017 15:24:30 GMT', - 'Accept-Ranges': 'bytes', - 'Content-Type': 'application/octet-stream', - ('X-Object-Meta-' - + self.cloud._OBJECT_AUTOCREATE_KEY): 'true', - 'Etag': fakes.NO_MD5, - 'X-Static-Large-Object': 'false'}), - dict(method='DELETE', - uri='{endpoint}/{container}/{object}'.format( - endpoint=endpoint, container=self.container_name, - object=self.image_name)), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + service_type='object-store', + resource=self.container_name, + qs_elements=['format=json'], + ), + json=[ + { + 'content_type': 'application/octet-stream', + 'bytes': 1437258240, + 'hash': '249219347276c331b87bf1ac2152d9af', + 'last_modified': '2015-02-16T17:50:05.289600', + 'name': other_image, + }, + { + 'content_type': 'application/octet-stream', + 'bytes': 1290170880, + 'hash': fakes.NO_MD5, + 'last_modified': '2015-04-14T18:29:00.502530', + 'name': self.image_name, + }, + ], + ), + dict( + method='HEAD', + uri=self.get_mock_url( + service_type='object-store', + resource=self.container_name, + append=[other_image], + ), + headers={ + 'X-Timestamp': '1429036140.50253', + 'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1', + 'Content-Length': '1290170880', + 'Last-Modified': 'Tue, 14 Apr 2015 18:29:01 GMT', + 'X-Object-Meta-X-Shade-Sha256': 'does not matter', + 'X-Object-Meta-X-Shade-Md5': 'does not matter', + 'Date': 'Thu, 16 Nov 2017 15:24:30 GMT', + 'Accept-Ranges': 'bytes', + 'Content-Type': 'application/octet-stream', + 'Etag': '249219347276c331b87bf1ac2152d9af', + }, + ), + dict( + method='HEAD', + uri=self.get_mock_url( + service_type='object-store', + resource=self.container_name, + append=[self.image_name], + ), + headers={ + 'X-Timestamp': '1429036140.50253', + 'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1', + 'Content-Length': '1290170880', + 'Last-Modified': 'Tue, 14 Apr 2015 18:29:01 GMT', + 'X-Object-Meta-X-Shade-Sha256': fakes.NO_SHA256, + 'X-Object-Meta-X-Shade-Md5': fakes.NO_MD5, + 'Date': 'Thu, 16 Nov 2017 15:24:30 GMT', + 'Accept-Ranges': 'bytes', + 'Content-Type': 'application/octet-stream', + ( + 'X-Object-Meta-' + + self.cloud._OBJECT_AUTOCREATE_KEY + ): 'true', + 'Etag': fakes.NO_MD5, + 'X-Static-Large-Object': 'false', + }, + ), + dict( + method='DELETE', + uri='{endpoint}/{container}/{object}'.format( + endpoint=endpoint, + container=self.container_name, + object=self.image_name, + ), + ), + ] + ) deleted = self.cloud.delete_autocreated_image_objects( - container=self.container_name) + container=self.container_name + ) self.assertTrue(deleted) self.assert_calls() @@ -858,113 +1216,169 @@ def _call_create_image(self, name, **kwargs): imagefile.write(b'\0') imagefile.close() self.cloud.create_image( - name, imagefile.name, wait=True, timeout=1, - is_public=False, validate_checksum=True, **kwargs) + name, + imagefile.name, + wait=True, + timeout=1, + is_public=False, + validate_checksum=True, + **kwargs + ) def test_create_image_put_v1(self): self.cloud.config.config['image_api_version'] = '1' - args = {'name': self.image_name, - 'container_format': 'bare', 'disk_format': 'qcow2', - 'properties': { - 'owner_specified.openstack.md5': fakes.NO_MD5, - 'owner_specified.openstack.sha256': fakes.NO_SHA256, - 'owner_specified.openstack.object': 'images/{name}'.format( - name=self.image_name), - 'is_public': False}} + args = { + 'name': self.image_name, + 'container_format': 'bare', + 'disk_format': 'qcow2', + 'properties': { + 'owner_specified.openstack.md5': fakes.NO_MD5, + 'owner_specified.openstack.sha256': fakes.NO_SHA256, + 'owner_specified.openstack.object': 'images/{name}'.format( + name=self.image_name + ), + 'is_public': False, + }, + } ret = args.copy() ret['id'] = self.image_id ret['status'] = 'success' - self.register_uris([ - dict(method='GET', - uri='https://image.example.com/v1/images/' + self.image_name, - status_code=404), - dict(method='GET', - uri='https://image.example.com/v1/images/detail?name=' - + self.image_name, - json={'images': []}), - dict(method='POST', - uri='https://image.example.com/v1/images', - json={'image': ret}, - validate=dict(json=args)), - dict(method='PUT', - uri='https://image.example.com/v1/images/{id}'.format( - id=self.image_id), - json={'image': ret}, - validate=dict(headers={ - 'x-image-meta-checksum': fakes.NO_MD5, - 'x-glance-registry-purge-props': 'false' - })), - dict(method='GET', - uri='https://image.example.com/v1/images/detail', - json={'images': [ret]}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://image.example.com/v1/images/' + + self.image_name, + status_code=404, + ), + dict( + method='GET', + uri='https://image.example.com/v1/images/detail?name=' + + self.image_name, + json={'images': []}, + ), + dict( + method='POST', + uri='https://image.example.com/v1/images', + json={'image': ret}, + validate=dict(json=args), + ), + dict( + method='PUT', + uri='https://image.example.com/v1/images/{id}'.format( + id=self.image_id + ), + json={'image': ret}, + validate=dict( + headers={ + 'x-image-meta-checksum': fakes.NO_MD5, + 'x-glance-registry-purge-props': 'false', + } + ), + ), + dict( + method='GET', + uri='https://image.example.com/v1/images/detail', + json={'images': [ret]}, + ), + ] + ) self._call_create_image(self.image_name) - [self._compare_images_v1(b, a) for a, b in zip( - self.cloud.list_images(), [ret])] + [ + self._compare_images_v1(b, a) + for a, b in zip(self.cloud.list_images(), [ret]) + ] def test_create_image_put_v1_bad_delete(self): self.cloud.config.config['image_api_version'] = '1' - args = {'name': self.image_name, - 'container_format': 'bare', 'disk_format': 'qcow2', - 'properties': { - 'owner_specified.openstack.md5': fakes.NO_MD5, - 'owner_specified.openstack.sha256': fakes.NO_SHA256, - 'owner_specified.openstack.object': 'images/{name}'.format( - name=self.image_name), - 'is_public': False}, - 'validate_checksum': True} + args = { + 'name': self.image_name, + 'container_format': 'bare', + 'disk_format': 'qcow2', + 'properties': { + 'owner_specified.openstack.md5': fakes.NO_MD5, + 'owner_specified.openstack.sha256': fakes.NO_SHA256, + 'owner_specified.openstack.object': 'images/{name}'.format( + name=self.image_name + ), + 'is_public': False, + }, + 'validate_checksum': True, + } ret = args.copy() ret['id'] = self.image_id ret['status'] = 'success' - self.register_uris([ - dict(method='GET', - uri='https://image.example.com/v1/images/' + self.image_name, - status_code=404), - dict(method='GET', - uri='https://image.example.com/v1/images/detail?name=' - + self.image_name, - json={'images': []}), - dict(method='POST', - uri='https://image.example.com/v1/images', - json={'image': ret}, - validate=dict(json=args)), - dict(method='PUT', - uri='https://image.example.com/v1/images/{id}'.format( - id=self.image_id), - status_code=400, - validate=dict(headers={ - 'x-image-meta-checksum': fakes.NO_MD5, - 'x-glance-registry-purge-props': 'false' - })), - dict(method='DELETE', - uri='https://image.example.com/v1/images/{id}'.format( - id=self.image_id), - json={'images': [ret]}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://image.example.com/v1/images/' + + self.image_name, + status_code=404, + ), + dict( + method='GET', + uri='https://image.example.com/v1/images/detail?name=' + + self.image_name, + json={'images': []}, + ), + dict( + method='POST', + uri='https://image.example.com/v1/images', + json={'image': ret}, + validate=dict(json=args), + ), + dict( + method='PUT', + uri='https://image.example.com/v1/images/{id}'.format( + id=self.image_id + ), + status_code=400, + validate=dict( + headers={ + 'x-image-meta-checksum': fakes.NO_MD5, + 'x-glance-registry-purge-props': 'false', + } + ), + ), + dict( + method='DELETE', + uri='https://image.example.com/v1/images/{id}'.format( + id=self.image_id + ), + json={'images': [ret]}, + ), + ] + ) self.assertRaises( exc.OpenStackCloudHTTPError, self._call_create_image, - self.image_name) + self.image_name, + ) self.assert_calls() def test_update_image_no_patch(self): self.cloud.image_api_use_tasks = False - args = {'name': self.image_name, - 'container_format': 'bare', 'disk_format': 'qcow2', - 'owner_specified.openstack.md5': fakes.NO_MD5, - 'owner_specified.openstack.sha256': fakes.NO_SHA256, - 'owner_specified.openstack.object': 'images/{name}'.format( - name=self.image_name), - 'visibility': 'private'} + args = { + 'name': self.image_name, + 'container_format': 'bare', + 'disk_format': 'qcow2', + 'owner_specified.openstack.md5': fakes.NO_MD5, + 'owner_specified.openstack.sha256': fakes.NO_SHA256, + 'owner_specified.openstack.object': 'images/{name}'.format( + name=self.image_name + ), + 'visibility': 'private', + } ret = args.copy() ret['id'] = self.image_id @@ -972,67 +1386,98 @@ def test_update_image_no_patch(self): self.cloud.update_image_properties( image=image.Image.existing(**ret), - **{'owner_specified.openstack.object': 'images/{name}'.format( - name=self.image_name)}) + **{ + 'owner_specified.openstack.object': 'images/{name}'.format( + name=self.image_name + ) + } + ) self.assert_calls() def test_create_image_put_v2_bad_delete(self): self.cloud.image_api_use_tasks = False - args = {'name': self.image_name, - 'container_format': 'bare', 'disk_format': 'qcow2', - 'owner_specified.openstack.md5': fakes.NO_MD5, - 'owner_specified.openstack.sha256': fakes.NO_SHA256, - 'owner_specified.openstack.object': 'images/{name}'.format( - name=self.image_name), - 'visibility': 'private'} + args = { + 'name': self.image_name, + 'container_format': 'bare', + 'disk_format': 'qcow2', + 'owner_specified.openstack.md5': fakes.NO_MD5, + 'owner_specified.openstack.sha256': fakes.NO_SHA256, + 'owner_specified.openstack.object': 'images/{name}'.format( + name=self.image_name + ), + 'visibility': 'private', + } ret = args.copy() ret['id'] = self.image_id ret['status'] = 'success' - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images', self.image_name], - base_url_append='v2'), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], - base_url_append='v2', - qs_elements=['name=' + self.image_name]), - validate=dict(), - json={'images': []}), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], - base_url_append='v2', - qs_elements=['os_hidden=True']), - json={'images': []}), - dict(method='POST', - uri='https://image.example.com/v2/images', - json=ret, - validate=dict(json=args)), - dict(method='PUT', - uri='https://image.example.com/v2/images/{id}/file'.format( - id=self.image_id), - status_code=400, - validate=dict( - headers={ - 'Content-Type': 'application/octet-stream', - }, - )), - dict(method='DELETE', - uri='https://image.example.com/v2/images/{id}'.format( - id=self.image_id)), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images', self.image_name], + base_url_append='v2', + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images'], + base_url_append='v2', + qs_elements=['name=' + self.image_name], + ), + validate=dict(), + json={'images': []}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images'], + base_url_append='v2', + qs_elements=['os_hidden=True'], + ), + json={'images': []}, + ), + dict( + method='POST', + uri='https://image.example.com/v2/images', + json=ret, + validate=dict(json=args), + ), + dict( + method='PUT', + uri='https://image.example.com/v2/images/{id}/file'.format( + id=self.image_id + ), + status_code=400, + validate=dict( + headers={ + 'Content-Type': 'application/octet-stream', + }, + ), + ), + dict( + method='DELETE', + uri='https://image.example.com/v2/images/{id}'.format( + id=self.image_id + ), + ), + ] + ) self.assertRaises( exc.OpenStackCloudHTTPError, self._call_create_image, - self.image_name) + self.image_name, + ) self.assert_calls() @@ -1044,47 +1489,69 @@ def test_create_image_put_v2_wrong_checksum_delete(self): fake_image['owner_specified.openstack.md5'] = 'a' fake_image['owner_specified.openstack.sha256'] = 'b' - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - json=self.fake_image_dict, - validate=dict( - json={ - u'container_format': u'bare', - u'disk_format': u'qcow2', - u'name': self.image_name, - u'owner_specified.openstack.md5': - fake_image[ - 'owner_specified.openstack.md5'], - u'owner_specified.openstack.object': self.object_name, - u'owner_specified.openstack.sha256': - fake_image[ - 'owner_specified.openstack.sha256'], - u'visibility': u'private'}) - ), - dict(method='PUT', - uri=self.get_mock_url( - 'image', append=['images', self.image_id, 'file'], - base_url_append='v2'), - request_headers={'Content-Type': 'application/octet-stream'}), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images', self.fake_image_dict['id']], - base_url_append='v2' - ), - json=fake_image), - dict(method='DELETE', - uri='https://image.example.com/v2/images/{id}'.format( - id=self.image_id)) - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2' + ), + json=self.fake_image_dict, + validate=dict( + json={ + 'container_format': 'bare', + 'disk_format': 'qcow2', + 'name': self.image_name, + 'owner_specified.openstack.md5': fake_image[ + 'owner_specified.openstack.md5' + ], + 'owner_specified.openstack.object': self.object_name, # noqa: E501 + 'owner_specified.openstack.sha256': fake_image[ + 'owner_specified.openstack.sha256' + ], + 'visibility': 'private', + } + ), + ), + dict( + method='PUT', + uri=self.get_mock_url( + 'image', + append=['images', self.image_id, 'file'], + base_url_append='v2', + ), + request_headers={ + 'Content-Type': 'application/octet-stream' + }, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images', self.fake_image_dict['id']], + base_url_append='v2', + ), + json=fake_image, + ), + dict( + method='DELETE', + uri='https://image.example.com/v2/images/{id}'.format( + id=self.image_id + ), + ), + ] + ) self.assertRaises( exceptions.SDKException, self.cloud.create_image, - self.image_name, self.imagefile.name, - is_public=False, md5='a', sha256='b', - allow_duplicates=True, validate_checksum=True + self.image_name, + self.imagefile.name, + is_public=False, + md5='a', + sha256='b', + allow_duplicates=True, + validate_checksum=True, ) self.assert_calls() @@ -1094,211 +1561,303 @@ def test_create_image_put_bad_int(self): self.assertRaises( exc.OpenStackCloudException, - self._call_create_image, self.image_name, + self._call_create_image, + self.image_name, allow_duplicates=True, - min_disk='fish', min_ram=0) + min_disk='fish', + min_ram=0, + ) self.assert_calls() def test_create_image_put_user_int(self): self.cloud.image_api_use_tasks = False - args = {'name': self.image_name, - 'container_format': 'bare', 'disk_format': u'qcow2', - 'owner_specified.openstack.md5': fakes.NO_MD5, - 'owner_specified.openstack.sha256': fakes.NO_SHA256, - 'owner_specified.openstack.object': 'images/{name}'.format( - name=self.image_name), - 'int_v': '12345', - 'visibility': 'private', - 'min_disk': 0, 'min_ram': 0} + args = { + 'name': self.image_name, + 'container_format': 'bare', + 'disk_format': 'qcow2', + 'owner_specified.openstack.md5': fakes.NO_MD5, + 'owner_specified.openstack.sha256': fakes.NO_SHA256, + 'owner_specified.openstack.object': 'images/{name}'.format( + name=self.image_name + ), + 'int_v': '12345', + 'visibility': 'private', + 'min_disk': 0, + 'min_ram': 0, + } ret = args.copy() ret['id'] = self.image_id ret['status'] = 'success' - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images', self.image_name], - base_url_append='v2'), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], - base_url_append='v2', - qs_elements=['name=' + self.image_name]), - validate=dict(), - json={'images': []}), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], - base_url_append='v2', - qs_elements=['os_hidden=True']), - json={'images': []}), - dict(method='POST', - uri='https://image.example.com/v2/images', - json=ret, - validate=dict(json=args)), - dict(method='PUT', - uri='https://image.example.com/v2/images/{id}/file'.format( - id=self.image_id), - validate=dict( - headers={ - 'Content-Type': 'application/octet-stream', - }, - )), - dict(method='GET', - uri='https://image.example.com/v2/images/{id}'.format( - id=self.image_id - ), - json=ret), - dict(method='GET', - uri='https://image.example.com/v2/images', - complete_qs=True, - json={'images': [ret]}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images', self.image_name], + base_url_append='v2', + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images'], + base_url_append='v2', + qs_elements=['name=' + self.image_name], + ), + validate=dict(), + json={'images': []}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images'], + base_url_append='v2', + qs_elements=['os_hidden=True'], + ), + json={'images': []}, + ), + dict( + method='POST', + uri='https://image.example.com/v2/images', + json=ret, + validate=dict(json=args), + ), + dict( + method='PUT', + uri='https://image.example.com/v2/images/{id}/file'.format( + id=self.image_id + ), + validate=dict( + headers={ + 'Content-Type': 'application/octet-stream', + }, + ), + ), + dict( + method='GET', + uri='https://image.example.com/v2/images/{id}'.format( + id=self.image_id + ), + json=ret, + ), + dict( + method='GET', + uri='https://image.example.com/v2/images', + complete_qs=True, + json={'images': [ret]}, + ), + ] + ) self._call_create_image( - self.image_name, min_disk='0', min_ram=0, int_v=12345) + self.image_name, min_disk='0', min_ram=0, int_v=12345 + ) self.assert_calls() def test_create_image_put_meta_int(self): self.cloud.image_api_use_tasks = False - args = {'name': self.image_name, - 'container_format': 'bare', 'disk_format': u'qcow2', - 'owner_specified.openstack.md5': fakes.NO_MD5, - 'owner_specified.openstack.sha256': fakes.NO_SHA256, - 'owner_specified.openstack.object': 'images/{name}'.format( - name=self.image_name), - 'int_v': 12345, - 'visibility': 'private', - 'min_disk': 0, 'min_ram': 0} + args = { + 'name': self.image_name, + 'container_format': 'bare', + 'disk_format': 'qcow2', + 'owner_specified.openstack.md5': fakes.NO_MD5, + 'owner_specified.openstack.sha256': fakes.NO_SHA256, + 'owner_specified.openstack.object': 'images/{name}'.format( + name=self.image_name + ), + 'int_v': 12345, + 'visibility': 'private', + 'min_disk': 0, + 'min_ram': 0, + } ret = args.copy() ret['id'] = self.image_id ret['status'] = 'success' ret['checksum'] = fakes.NO_MD5 - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images', self.image_name], - base_url_append='v2'), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], - base_url_append='v2', - qs_elements=['name=' + self.image_name]), - validate=dict(), - json={'images': []}), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], - base_url_append='v2', - qs_elements=['os_hidden=True']), - json={'images': []}), - dict(method='POST', - uri='https://image.example.com/v2/images', - json=ret, - validate=dict(json=args)), - dict(method='PUT', - uri='https://image.example.com/v2/images/{id}/file'.format( - id=self.image_id), - validate=dict( - headers={ - 'Content-Type': 'application/octet-stream', - }, - )), - dict(method='GET', - uri='https://image.example.com/v2/images/{id}'.format( - id=self.image_id - ), - json=ret), - dict(method='GET', - uri='https://image.example.com/v2/images', - complete_qs=True, - json={'images': [ret]}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images', self.image_name], + base_url_append='v2', + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images'], + base_url_append='v2', + qs_elements=['name=' + self.image_name], + ), + validate=dict(), + json={'images': []}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images'], + base_url_append='v2', + qs_elements=['os_hidden=True'], + ), + json={'images': []}, + ), + dict( + method='POST', + uri='https://image.example.com/v2/images', + json=ret, + validate=dict(json=args), + ), + dict( + method='PUT', + uri='https://image.example.com/v2/images/{id}/file'.format( + id=self.image_id + ), + validate=dict( + headers={ + 'Content-Type': 'application/octet-stream', + }, + ), + ), + dict( + method='GET', + uri='https://image.example.com/v2/images/{id}'.format( + id=self.image_id + ), + json=ret, + ), + dict( + method='GET', + uri='https://image.example.com/v2/images', + complete_qs=True, + json={'images': [ret]}, + ), + ] + ) self._call_create_image( - self.image_name, min_disk='0', min_ram=0, meta={'int_v': 12345}) + self.image_name, min_disk='0', min_ram=0, meta={'int_v': 12345} + ) self.assert_calls() def test_create_image_put_protected(self): self.cloud.image_api_use_tasks = False - args = {'name': self.image_name, - 'container_format': 'bare', 'disk_format': u'qcow2', - 'owner_specified.openstack.md5': fakes.NO_MD5, - 'owner_specified.openstack.sha256': fakes.NO_SHA256, - 'owner_specified.openstack.object': 'images/{name}'.format( - name=self.image_name), - 'int_v': '12345', - 'protected': False, - 'visibility': 'private', - 'min_disk': 0, 'min_ram': 0} + args = { + 'name': self.image_name, + 'container_format': 'bare', + 'disk_format': 'qcow2', + 'owner_specified.openstack.md5': fakes.NO_MD5, + 'owner_specified.openstack.sha256': fakes.NO_SHA256, + 'owner_specified.openstack.object': 'images/{name}'.format( + name=self.image_name + ), + 'int_v': '12345', + 'protected': False, + 'visibility': 'private', + 'min_disk': 0, + 'min_ram': 0, + } ret = args.copy() ret['id'] = self.image_id ret['status'] = 'success' ret['checksum'] = fakes.NO_MD5 - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images', self.image_name], - base_url_append='v2'), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], - base_url_append='v2', - qs_elements=['name=' + self.image_name]), - validate=dict(), - json={'images': []}), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], - base_url_append='v2', - qs_elements=['os_hidden=True']), - json={'images': []}), - dict(method='POST', - uri='https://image.example.com/v2/images', - json=ret, - validate=dict(json=args)), - dict(method='PUT', - uri='https://image.example.com/v2/images/{id}/file'.format( - id=self.image_id), - validate=dict( - headers={ - 'Content-Type': 'application/octet-stream', - }, - )), - dict(method='GET', - uri='https://image.example.com/v2/images/{id}'.format( - id=self.image_id - ), - json=ret), - dict(method='GET', - uri='https://image.example.com/v2/images', - complete_qs=True, - json={'images': [ret]}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images', self.image_name], + base_url_append='v2', + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images'], + base_url_append='v2', + qs_elements=['name=' + self.image_name], + ), + validate=dict(), + json={'images': []}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images'], + base_url_append='v2', + qs_elements=['os_hidden=True'], + ), + json={'images': []}, + ), + dict( + method='POST', + uri='https://image.example.com/v2/images', + json=ret, + validate=dict(json=args), + ), + dict( + method='PUT', + uri='https://image.example.com/v2/images/{id}/file'.format( + id=self.image_id + ), + validate=dict( + headers={ + 'Content-Type': 'application/octet-stream', + }, + ), + ), + dict( + method='GET', + uri='https://image.example.com/v2/images/{id}'.format( + id=self.image_id + ), + json=ret, + ), + dict( + method='GET', + uri='https://image.example.com/v2/images', + complete_qs=True, + json={'images': [ret]}, + ), + ] + ) self._call_create_image( - self.image_name, min_disk='0', min_ram=0, - properties={'int_v': 12345}, is_protected=False) + self.image_name, + min_disk='0', + min_ram=0, + properties={'int_v': 12345}, + is_protected=False, + ) self.assert_calls() class TestImageSuburl(BaseTestImage): - def setUp(self): super(TestImageSuburl, self).setUp() self.os_fixture.use_suburl() @@ -1306,43 +1865,66 @@ def setUp(self): self.use_keystone_v3() self.use_glance( image_version_json='image-version-suburl.json', - image_discovery_url='https://example.com/image') + image_discovery_url='https://example.com/image', + ) def test_list_images(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - json=self.fake_search_return) - ]) - [self._compare_images(b, a) for a, b in zip( - self.cloud.list_images(), - [self.fake_image_dict])] + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2' + ), + json=self.fake_search_return, + ) + ] + ) + [ + self._compare_images(b, a) + for a, b in zip(self.cloud.list_images(), [self.fake_image_dict]) + ] self.assert_calls() def test_list_images_paginated(self): marker = str(uuid.uuid4()) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - json={'images': [self.fake_image_dict], - 'next': '/v2/images?marker={marker}'.format( - marker=marker)}), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2', - qs_elements=['marker={marker}'.format(marker=marker)]), - json=self.fake_search_return) - ]) - [self._compare_images(b, a) for a, b in zip( - self.cloud.list_images(), - [self.fake_image_dict, self.fake_image_dict])] + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2' + ), + json={ + 'images': [self.fake_image_dict], + 'next': '/v2/images?marker={marker}'.format( + marker=marker + ), + }, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images'], + base_url_append='v2', + qs_elements=['marker={marker}'.format(marker=marker)], + ), + json=self.fake_search_return, + ), + ] + ) + [ + self._compare_images(b, a) + for a, b in zip( + self.cloud.list_images(), + [self.fake_image_dict, self.fake_image_dict], + ) + ] self.assert_calls() class TestImageV1Only(base.TestCase): - def setUp(self): super(TestImageV1Only, self).setUp() self.use_glance(image_version_json='image-version-v1.json') @@ -1354,7 +1936,8 @@ def test_config_v1(self): # because glance has a bug where it doesn't return https properly. self.assertEqual( 'https://image.example.com/v1/', - self.cloud._image_client.get_endpoint()) + self.cloud._image_client.get_endpoint(), + ) self.assertTrue(self.cloud._is_client_version('image', 1)) def test_config_v2(self): @@ -1363,12 +1946,12 @@ def test_config_v2(self): # because glance has a bug where it doesn't return https properly. self.assertEqual( 'https://image.example.com/v1/', - self.cloud._image_client.get_endpoint()) + self.cloud._image_client.get_endpoint(), + ) self.assertFalse(self.cloud._is_client_version('image', 2)) class TestImageV2Only(base.TestCase): - def setUp(self): super(TestImageV2Only, self).setUp() self.use_glance(image_version_json='image-version-v2.json') @@ -1379,7 +1962,8 @@ def test_config_v1(self): # because glance has a bug where it doesn't return https properly. self.assertEqual( 'https://image.example.com/v2/', - self.cloud._image_client.get_endpoint()) + self.cloud._image_client.get_endpoint(), + ) self.assertTrue(self.cloud._is_client_version('image', 2)) def test_config_v2(self): @@ -1388,82 +1972,118 @@ def test_config_v2(self): # because glance has a bug where it doesn't return https properly. self.assertEqual( 'https://image.example.com/v2/', - self.cloud._image_client.get_endpoint()) + self.cloud._image_client.get_endpoint(), + ) self.assertTrue(self.cloud._is_client_version('image', 2)) class TestImageVolume(BaseTestImage): - def setUp(self): super(TestImageVolume, self).setUp() self.volume_id = str(uuid.uuid4()) def test_create_image_volume(self): - self.register_uris([ - self.get_cinder_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'volumev3', append=['volumes', self.volume_id, 'action']), - json={'os-volume_upload_image': {'image_id': self.image_id}}, - validate=dict(json={ - u'os-volume_upload_image': { - u'container_format': u'bare', - u'disk_format': u'qcow2', - u'force': False, - u'image_name': u'fake_image'}}) - ), - # NOTE(notmorgan): Glance discovery happens here, insert the - # glance discovery mock at this point, DO NOT use the - # .use_glance() method, that is intended only for use in - # .setUp - self.get_glance_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - json=self.fake_search_return) - ]) + self.register_uris( + [ + self.get_cinder_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'volumev3', + append=['volumes', self.volume_id, 'action'], + ), + json={ + 'os-volume_upload_image': {'image_id': self.image_id} + }, + validate=dict( + json={ + 'os-volume_upload_image': { + 'container_format': 'bare', + 'disk_format': 'qcow2', + 'force': False, + 'image_name': 'fake_image', + } + } + ), + ), + # NOTE(notmorgan): Glance discovery happens here, insert the + # glance discovery mock at this point, DO NOT use the + # .use_glance() method, that is intended only for use in + # .setUp + self.get_glance_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2' + ), + json=self.fake_search_return, + ), + ] + ) self.cloud.create_image( - 'fake_image', self.imagefile.name, wait=True, timeout=1, - volume={'id': self.volume_id}) + 'fake_image', + self.imagefile.name, + wait=True, + timeout=1, + volume={'id': self.volume_id}, + ) self.assert_calls() def test_create_image_volume_duplicate(self): - self.register_uris([ - self.get_cinder_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'volumev3', append=['volumes', self.volume_id, 'action']), - json={'os-volume_upload_image': {'image_id': self.image_id}}, - validate=dict(json={ - u'os-volume_upload_image': { - u'container_format': u'bare', - u'disk_format': u'qcow2', - u'force': True, - u'image_name': u'fake_image'}}) - ), - # NOTE(notmorgan): Glance discovery happens here, insert the - # glance discovery mock at this point, DO NOT use the - # .use_glance() method, that is intended only for use in - # .setUp - self.get_glance_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - json=self.fake_search_return) - ]) + self.register_uris( + [ + self.get_cinder_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'volumev3', + append=['volumes', self.volume_id, 'action'], + ), + json={ + 'os-volume_upload_image': {'image_id': self.image_id} + }, + validate=dict( + json={ + 'os-volume_upload_image': { + 'container_format': 'bare', + 'disk_format': 'qcow2', + 'force': True, + 'image_name': 'fake_image', + } + } + ), + ), + # NOTE(notmorgan): Glance discovery happens here, insert the + # glance discovery mock at this point, DO NOT use the + # .use_glance() method, that is intended only for use in + # .setUp + self.get_glance_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2' + ), + json=self.fake_search_return, + ), + ] + ) self.cloud.create_image( - 'fake_image', self.imagefile.name, wait=True, timeout=1, - volume={'id': self.volume_id}, allow_duplicates=True) + 'fake_image', + self.imagefile.name, + wait=True, + timeout=1, + volume={'id': self.volume_id}, + allow_duplicates=True, + ) self.assert_calls() class TestImageBrokenDiscovery(base.TestCase): - def setUp(self): super(TestImageBrokenDiscovery, self).setUp() self.use_glance(image_version_json='image-version-broken.json') @@ -1473,14 +2093,20 @@ def test_url_fix(self): # host. This is testing that what is discovered is https, because # that's what's in the catalog, and image.example.com for the same # reason. - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2'), - json={'images': []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'image', append=['images'], base_url_append='v2' + ), + json={'images': []}, + ) + ] + ) self.assertEqual([], self.cloud.list_images()) self.assertEqual( self.cloud._image_client.get_endpoint(), - 'https://image.example.com/v2/') + 'https://image.example.com/v2/', + ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_image_snapshot.py b/openstack/tests/unit/cloud/test_image_snapshot.py index 218ee8de2..2e33be1a4 100644 --- a/openstack/tests/unit/cloud/test_image_snapshot.py +++ b/openstack/tests/unit/cloud/test_image_snapshot.py @@ -20,47 +20,59 @@ class TestImageSnapshot(base.TestCase): - def setUp(self): super(TestImageSnapshot, self).setUp() self.server_id = str(uuid.uuid4()) self.image_id = str(uuid.uuid4()) self.server_name = self.getUniqueString('name') self.fake_server = fakes.make_fake_server( - self.server_id, self.server_name) + self.server_id, self.server_name + ) def test_create_image_snapshot_wait_until_active_never_active(self): snapshot_name = 'test-snapshot' fake_image = fakes.make_fake_image(self.image_id, status='pending') - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict( - method='POST', - uri='{endpoint}/servers/{server_id}/action'.format( - endpoint=fakes.COMPUTE_ENDPOINT, - server_id=self.server_id), - headers=dict( - Location='{endpoint}/images/{image_id}'.format( - endpoint='https://images.example.com', - image_id=self.image_id)), - validate=dict( - json={ - "createImage": { - "name": snapshot_name, - "metadata": {}, - }})), - self.get_glance_discovery_mock_dict(), - dict( - method='GET', - uri='https://image.example.com/v2/images', - json=dict(images=[fake_image])), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri='{endpoint}/servers/{server_id}/action'.format( + endpoint=fakes.COMPUTE_ENDPOINT, + server_id=self.server_id, + ), + headers=dict( + Location='{endpoint}/images/{image_id}'.format( + endpoint='https://images.example.com', + image_id=self.image_id, + ) + ), + validate=dict( + json={ + "createImage": { + "name": snapshot_name, + "metadata": {}, + } + } + ), + ), + self.get_glance_discovery_mock_dict(), + dict( + method='GET', + uri='https://image.example.com/v2/images', + json=dict(images=[fake_image]), + ), + ] + ) self.assertRaises( exc.OpenStackCloudTimeout, self.cloud.create_image_snapshot, - snapshot_name, dict(id=self.server_id), - wait=True, timeout=0.01) + snapshot_name, + dict(id=self.server_id), + wait=True, + timeout=0.01, + ) # After the fifth call, we just keep polling get images for status. # Due to mocking sleep, we have no clue how many times we'll call it. @@ -70,35 +82,46 @@ def test_create_image_snapshot_wait_active(self): snapshot_name = 'test-snapshot' pending_image = fakes.make_fake_image(self.image_id, status='pending') fake_image = fakes.make_fake_image(self.image_id) - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict( - method='POST', - uri='{endpoint}/servers/{server_id}/action'.format( - endpoint=fakes.COMPUTE_ENDPOINT, - server_id=self.server_id), - headers=dict( - Location='{endpoint}/images/{image_id}'.format( - endpoint='https://images.example.com', - image_id=self.image_id)), - validate=dict( - json={ - "createImage": { - "name": snapshot_name, - "metadata": {}, - }})), - self.get_glance_discovery_mock_dict(), - dict( - method='GET', - uri='https://image.example.com/v2/images', - json=dict(images=[pending_image])), - dict( - method='GET', - uri='https://image.example.com/v2/images', - json=dict(images=[fake_image])), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri='{endpoint}/servers/{server_id}/action'.format( + endpoint=fakes.COMPUTE_ENDPOINT, + server_id=self.server_id, + ), + headers=dict( + Location='{endpoint}/images/{image_id}'.format( + endpoint='https://images.example.com', + image_id=self.image_id, + ) + ), + validate=dict( + json={ + "createImage": { + "name": snapshot_name, + "metadata": {}, + } + } + ), + ), + self.get_glance_discovery_mock_dict(), + dict( + method='GET', + uri='https://image.example.com/v2/images', + json=dict(images=[pending_image]), + ), + dict( + method='GET', + uri='https://image.example.com/v2/images', + json=dict(images=[fake_image]), + ), + ] + ) image = self.cloud.create_image_snapshot( - 'test-snapshot', dict(id=self.server_id), wait=True, timeout=2) + 'test-snapshot', dict(id=self.server_id), wait=True, timeout=2 + ) self.assertEqual(image['id'], self.image_id) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_inventory.py b/openstack/tests/unit/cloud/test_inventory.py index e51f271d7..c6520897e 100644 --- a/openstack/tests/unit/cloud/test_inventory.py +++ b/openstack/tests/unit/cloud/test_inventory.py @@ -19,7 +19,6 @@ class TestInventory(base.TestCase): - def setUp(self): super(TestInventory, self).setUp() @@ -50,8 +49,7 @@ def test__init_one_cloud(self, mock_cloud, mock_config): self.assertIsInstance(inv.clouds, list) self.assertEqual(1, len(inv.clouds)) self.assertFalse(mock_config.return_value.get_all.called) - mock_config.return_value.get_one.assert_called_once_with( - 'supercloud') + mock_config.return_value.get_one.assert_called_once_with('supercloud') @mock.patch("openstack.config.loader.OpenStackConfig") @mock.patch("openstack.connection.Connection") @@ -68,8 +66,9 @@ def test_list_hosts(self, mock_cloud, mock_config): ret = inv.list_hosts() - inv.clouds[0].list_servers.assert_called_once_with(detailed=True, - all_projects=False) + inv.clouds[0].list_servers.assert_called_once_with( + detailed=True, all_projects=False + ) self.assertFalse(inv.clouds[0].get_openstack_vars.called) self.assertEqual([server], ret) @@ -81,16 +80,17 @@ def test_list_hosts_no_detail(self, mock_cloud, mock_config): inv = inventory.OpenStackInventory() server = self.cloud._normalize_server( - fakes.make_fake_server( - '1234', 'test', 'ACTIVE', addresses={})) + fakes.make_fake_server('1234', 'test', 'ACTIVE', addresses={}) + ) self.assertIsInstance(inv.clouds, list) self.assertEqual(1, len(inv.clouds)) inv.clouds[0].list_servers.return_value = [server] inv.list_hosts(expand=False) - inv.clouds[0].list_servers.assert_called_once_with(detailed=False, - all_projects=False) + inv.clouds[0].list_servers.assert_called_once_with( + detailed=False, all_projects=False + ) self.assertFalse(inv.clouds[0].get_openstack_vars.called) @mock.patch("openstack.config.loader.OpenStackConfig") @@ -108,8 +108,9 @@ def test_list_hosts_all_projects(self, mock_cloud, mock_config): ret = inv.list_hosts(all_projects=True) - inv.clouds[0].list_servers.assert_called_once_with(detailed=True, - all_projects=True) + inv.clouds[0].list_servers.assert_called_once_with( + detailed=True, all_projects=True + ) self.assertFalse(inv.clouds[0].get_openstack_vars.called) self.assertEqual([server], ret) diff --git a/openstack/tests/unit/cloud/test_keypair.py b/openstack/tests/unit/cloud/test_keypair.py index e29c7306a..20410609f 100644 --- a/openstack/tests/unit/cloud/test_keypair.py +++ b/openstack/tests/unit/cloud/test_keypair.py @@ -19,29 +19,41 @@ class TestKeypair(base.TestCase): - def setUp(self): super(TestKeypair, self).setUp() self.keyname = self.getUniqueString('key') self.key = fakes.make_fake_keypair(self.keyname) - self.useFixture(fixtures.MonkeyPatch( - 'openstack.utils.maximum_supported_microversion', - lambda *args, **kwargs: '2.10')) + self.useFixture( + fixtures.MonkeyPatch( + 'openstack.utils.maximum_supported_microversion', + lambda *args, **kwargs: '2.10', + ) + ) def test_create_keypair(self): - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', append=['os-keypairs']), - json={'keypair': self.key}, - validate=dict(json={ - 'keypair': { - 'name': self.key['name'], - 'public_key': self.key['public_key']}})), - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['os-keypairs'] + ), + json={'keypair': self.key}, + validate=dict( + json={ + 'keypair': { + 'name': self.key['name'], + 'public_key': self.key['public_key'], + } + } + ), + ), + ] + ) new_key = self.cloud.create_keypair( - self.keyname, self.key['public_key']) + self.keyname, self.key['public_key'] + ) new_key_cmp = new_key.to_dict(ignore_none=True) new_key_cmp.pop('location') new_key_cmp.pop('id') @@ -50,97 +62,140 @@ def test_create_keypair(self): self.assert_calls() def test_create_keypair_exception(self): - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', append=['os-keypairs']), - status_code=400, - validate=dict(json={ - 'keypair': { - 'name': self.key['name'], - 'public_key': self.key['public_key']}})), - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['os-keypairs'] + ), + status_code=400, + validate=dict( + json={ + 'keypair': { + 'name': self.key['name'], + 'public_key': self.key['public_key'], + } + } + ), + ), + ] + ) self.assertRaises( exc.OpenStackCloudException, self.cloud.create_keypair, - self.keyname, self.key['public_key']) + self.keyname, + self.key['public_key'], + ) self.assert_calls() def test_delete_keypair(self): - self.register_uris([ - dict(method='DELETE', - uri=self.get_mock_url( - 'compute', 'public', - append=['os-keypairs', self.keyname]), - status_code=202), - ]) + self.register_uris( + [ + dict( + method='DELETE', + uri=self.get_mock_url( + 'compute', + 'public', + append=['os-keypairs', self.keyname], + ), + status_code=202, + ), + ] + ) self.assertTrue(self.cloud.delete_keypair(self.keyname)) self.assert_calls() def test_delete_keypair_not_found(self): - self.register_uris([ - dict(method='DELETE', - uri=self.get_mock_url( - 'compute', 'public', - append=['os-keypairs', self.keyname]), - status_code=404), - ]) + self.register_uris( + [ + dict( + method='DELETE', + uri=self.get_mock_url( + 'compute', + 'public', + append=['os-keypairs', self.keyname], + ), + status_code=404, + ), + ] + ) self.assertFalse(self.cloud.delete_keypair(self.keyname)) self.assert_calls() def test_list_keypairs(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['os-keypairs']), - json={'keypairs': [{'keypair': self.key}]}), - - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-keypairs'] + ), + json={'keypairs': [{'keypair': self.key}]}, + ), + ] + ) keypairs = self.cloud.list_keypairs() self.assertEqual(len(keypairs), 1) self.assertEqual(keypairs[0].name, self.key['name']) self.assert_calls() def test_list_keypairs_empty_filters(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['os-keypairs']), - json={'keypairs': [{'keypair': self.key}]}), - - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-keypairs'] + ), + json={'keypairs': [{'keypair': self.key}]}, + ), + ] + ) keypairs = self.cloud.list_keypairs(filters=None) self.assertEqual(len(keypairs), 1) self.assertEqual(keypairs[0].name, self.key['name']) self.assert_calls() def test_list_keypairs_notempty_filters(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['os-keypairs'], - qs_elements=['user_id=b']), - json={'keypairs': [{'keypair': self.key}]}), - - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['os-keypairs'], + qs_elements=['user_id=b'], + ), + json={'keypairs': [{'keypair': self.key}]}, + ), + ] + ) keypairs = self.cloud.list_keypairs( - filters={'user_id': 'b', 'fake': 'dummy'}) + filters={'user_id': 'b', 'fake': 'dummy'} + ) self.assertEqual(len(keypairs), 1) self.assertEqual(keypairs[0].name, self.key['name']) self.assert_calls() def test_list_keypairs_exception(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['os-keypairs']), - status_code=400), - - ]) - self.assertRaises(exc.OpenStackCloudException, - self.cloud.list_keypairs) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-keypairs'] + ), + status_code=400, + ), + ] + ) + self.assertRaises( + exc.OpenStackCloudException, self.cloud.list_keypairs + ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_limits.py b/openstack/tests/unit/cloud/test_limits.py index f731a1ae7..8481ca50d 100644 --- a/openstack/tests/unit/cloud/test_limits.py +++ b/openstack/tests/unit/cloud/test_limits.py @@ -14,81 +14,93 @@ class TestLimits(base.TestCase): - def test_get_compute_limits(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['limits']), - json={ - "limits": { - "absolute": { - "maxImageMeta": 128, - "maxPersonality": 5, - "maxPersonalitySize": 10240, - "maxSecurityGroupRules": 20, - "maxSecurityGroups": 10, - "maxServerMeta": 128, - "maxTotalCores": 20, - "maxTotalFloatingIps": 10, - "maxTotalInstances": 10, - "maxTotalKeypairs": 100, - "maxTotalRAMSize": 51200, - "maxServerGroups": 10, - "maxServerGroupMembers": 10, - "totalCoresUsed": 0, - "totalInstancesUsed": 0, - "totalRAMUsed": 0, - "totalSecurityGroupsUsed": 0, - "totalFloatingIpsUsed": 0, - "totalServerGroupsUsed": 0 - }, - "rate": [] - } - }), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['limits'] + ), + json={ + "limits": { + "absolute": { + "maxImageMeta": 128, + "maxPersonality": 5, + "maxPersonalitySize": 10240, + "maxSecurityGroupRules": 20, + "maxSecurityGroups": 10, + "maxServerMeta": 128, + "maxTotalCores": 20, + "maxTotalFloatingIps": 10, + "maxTotalInstances": 10, + "maxTotalKeypairs": 100, + "maxTotalRAMSize": 51200, + "maxServerGroups": 10, + "maxServerGroupMembers": 10, + "totalCoresUsed": 0, + "totalInstancesUsed": 0, + "totalRAMUsed": 0, + "totalSecurityGroupsUsed": 0, + "totalFloatingIpsUsed": 0, + "totalServerGroupsUsed": 0, + }, + "rate": [], + } + }, + ), + ] + ) self.cloud.get_compute_limits() self.assert_calls() def test_other_get_compute_limits(self): - project = self.mock_for_keystone_projects(project_count=1, - list_get=True)[0] - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['limits'], - qs_elements=[ - 'tenant_id={id}'.format(id=project.project_id) - ]), - json={ - "limits": { - "absolute": { - "maxImageMeta": 128, - "maxPersonality": 5, - "maxPersonalitySize": 10240, - "maxSecurityGroupRules": 20, - "maxSecurityGroups": 10, - "maxServerMeta": 128, - "maxTotalCores": 20, - "maxTotalFloatingIps": 10, - "maxTotalInstances": 10, - "maxTotalKeypairs": 100, - "maxTotalRAMSize": 51200, - "maxServerGroups": 10, - "maxServerGroupMembers": 10, - "totalCoresUsed": 0, - "totalInstancesUsed": 0, - "totalRAMUsed": 0, - "totalSecurityGroupsUsed": 0, - "totalFloatingIpsUsed": 0, - "totalServerGroupsUsed": 0 - }, - "rate": [] - } - }), - ]) + project = self.mock_for_keystone_projects( + project_count=1, list_get=True + )[0] + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['limits'], + qs_elements=[ + 'tenant_id={id}'.format(id=project.project_id) + ], + ), + json={ + "limits": { + "absolute": { + "maxImageMeta": 128, + "maxPersonality": 5, + "maxPersonalitySize": 10240, + "maxSecurityGroupRules": 20, + "maxSecurityGroups": 10, + "maxServerMeta": 128, + "maxTotalCores": 20, + "maxTotalFloatingIps": 10, + "maxTotalInstances": 10, + "maxTotalKeypairs": 100, + "maxTotalRAMSize": 51200, + "maxServerGroups": 10, + "maxServerGroupMembers": 10, + "totalCoresUsed": 0, + "totalInstancesUsed": 0, + "totalRAMUsed": 0, + "totalSecurityGroupsUsed": 0, + "totalFloatingIpsUsed": 0, + "totalServerGroupsUsed": 0, + }, + "rate": [], + } + }, + ), + ] + ) self.cloud.get_compute_limits(project.project_id) diff --git a/openstack/tests/unit/cloud/test_magnum_services.py b/openstack/tests/unit/cloud/test_magnum_services.py index 94ff8da85..d30531d4b 100644 --- a/openstack/tests/unit/cloud/test_magnum_services.py +++ b/openstack/tests/unit/cloud/test_magnum_services.py @@ -27,14 +27,19 @@ class TestMagnumServices(base.TestCase): - def test_list_magnum_services(self): - self.register_uris([dict( - method='GET', - uri=self.get_mock_url( - service_type='container-infrastructure-management', - resource='mservices'), - json=dict(mservices=[magnum_service_obj]))]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + service_type='container-infrastructure-management', + resource='mservices', + ), + json=dict(mservices=[magnum_service_obj]), + ) + ] + ) mservices_list = self.cloud.list_magnum_services() self.assertEqual( mservices_list[0].to_dict(computed=False), diff --git a/openstack/tests/unit/cloud/test_meta.py b/openstack/tests/unit/cloud/test_meta.py index 39f0bb4a9..56d95648d 100644 --- a/openstack/tests/unit/cloud/test_meta.py +++ b/openstack/tests/unit/cloud/test_meta.py @@ -88,12 +88,14 @@ def get_default_network(self): server_id='test-id-0', name='test-id-0', status='ACTIVE', - addresses={'private': [{'OS-EXT-IPS:type': 'fixed', - 'addr': PRIVATE_V4, - 'version': 4}], - 'public': [{'OS-EXT-IPS:type': 'floating', - 'addr': PUBLIC_V4, - 'version': 4}]}, + addresses={ + 'private': [ + {'OS-EXT-IPS:type': 'fixed', 'addr': PRIVATE_V4, 'version': 4} + ], + 'public': [ + {'OS-EXT-IPS:type': 'floating', 'addr': PUBLIC_V4, 'version': 4} + ], + }, flavor={'id': '101'}, image={'id': '471c2475-da2f-47ac-aba5-cb4aa3d546f5'}, ) @@ -108,17 +110,14 @@ def get_default_network(self): u'dns_nameservers': [], u'ipv6_ra_mode': None, u'allocation_pools': [ - { - u'start': u'10.10.10.2', - u'end': u'10.10.10.254' - } + {u'start': u'10.10.10.2', u'end': u'10.10.10.254'} ], u'gateway_ip': u'10.10.10.1', u'ipv6_address_mode': None, u'ip_version': 4, u'host_routes': [], u'cidr': u'10.10.10.0/24', - u'id': u'14025a85-436e-4418-b0ee-f5b12a50f9b4' + u'id': u'14025a85-436e-4418-b0ee-f5b12a50f9b4', }, ] @@ -132,7 +131,7 @@ def get_default_network(self): u'shared': True, u'status': u'ACTIVE', u'subnets': [u'cf785ee0-6cc9-4712-be3d-0bf6c86cf455'], - u'tenant_id': u'7a1ca9f7cc4e4b13ac0ed2957f1e8c32' + u'tenant_id': u'7a1ca9f7cc4e4b13ac0ed2957f1e8c32', }, { u'admin_state_up': True, @@ -143,7 +142,7 @@ def get_default_network(self): u'shared': False, u'status': u'ACTIVE', u'subnets': [u'a47910bc-f649-45db-98ec-e2421c413f4e'], - u'tenant_id': u'7e9c4d5842b3451d94417bd0af03a0f4' + u'tenant_id': u'7e9c4d5842b3451d94417bd0af03a0f4', }, { u'admin_state_up': True, @@ -153,17 +152,19 @@ def get_default_network(self): u'router:external': True, u'shared': True, u'status': u'ACTIVE', - u'subnets': [u'9c21d704-a8b9-409a-b56d-501cb518d380', - u'7cb0ce07-64c3-4a3d-92d3-6f11419b45b9'], - u'tenant_id': u'7a1ca9f7cc4e4b13ac0ed2957f1e8c32' - } + u'subnets': [ + u'9c21d704-a8b9-409a-b56d-501cb518d380', + u'7cb0ce07-64c3-4a3d-92d3-6f11419b45b9', + ], + u'tenant_id': u'7a1ca9f7cc4e4b13ac0ed2957f1e8c32', + }, ] OSIC_SUBNETS = [ { - u'allocation_pools': [{ - u'end': u'172.99.106.254', - u'start': u'172.99.106.5'}], + u'allocation_pools': [ + {u'end': u'172.99.106.254', u'start': u'172.99.106.5'} + ], u'cidr': u'172.99.106.0/24', u'dns_nameservers': [u'69.20.0.164', u'69.20.0.196'], u'enable_dhcp': True, @@ -176,11 +177,10 @@ def get_default_network(self): u'name': u'GATEWAY_NET', u'network_id': u'7004a83a-13d3-4dcd-8cf5-52af1ace4cae', u'subnetpool_id': None, - u'tenant_id': u'7a1ca9f7cc4e4b13ac0ed2957f1e8c32' + u'tenant_id': u'7a1ca9f7cc4e4b13ac0ed2957f1e8c32', }, { - u'allocation_pools': [{ - u'end': u'10.0.1.254', u'start': u'10.0.1.2'}], + u'allocation_pools': [{u'end': u'10.0.1.254', u'start': u'10.0.1.2'}], u'cidr': u'10.0.1.0/24', u'dns_nameservers': [u'8.8.8.8', u'8.8.4.4'], u'enable_dhcp': True, @@ -193,11 +193,12 @@ def get_default_network(self): u'name': u'openstackjenkins-subnet1', u'network_id': u'405abfcc-77dc-49b2-a271-139619ac9b26', u'subnetpool_id': None, - u'tenant_id': u'7e9c4d5842b3451d94417bd0af03a0f4' + u'tenant_id': u'7e9c4d5842b3451d94417bd0af03a0f4', }, { - u'allocation_pools': [{ - u'end': u'10.255.255.254', u'start': u'10.0.0.2'}], + u'allocation_pools': [ + {u'end': u'10.255.255.254', u'start': u'10.0.0.2'} + ], u'cidr': u'10.0.0.0/8', u'dns_nameservers': [u'8.8.8.8', u'8.8.4.4'], u'enable_dhcp': True, @@ -210,12 +211,15 @@ def get_default_network(self): u'name': u'GATEWAY_SUBNET_V6V4', u'network_id': u'54753d2c-0a58-4928-9b32-084c59dd20a6', u'subnetpool_id': None, - u'tenant_id': u'7a1ca9f7cc4e4b13ac0ed2957f1e8c32' + u'tenant_id': u'7a1ca9f7cc4e4b13ac0ed2957f1e8c32', }, { - u'allocation_pools': [{ - u'end': u'2001:4800:1ae1:18:ffff:ffff:ffff:ffff', - u'start': u'2001:4800:1ae1:18::2'}], + u'allocation_pools': [ + { + u'end': u'2001:4800:1ae1:18:ffff:ffff:ffff:ffff', + u'start': u'2001:4800:1ae1:18::2', + } + ], u'cidr': u'2001:4800:1ae1:18::/64', u'dns_nameservers': [u'2001:4860:4860::8888'], u'enable_dhcp': True, @@ -228,133 +232,212 @@ def get_default_network(self): u'name': u'GATEWAY_SUBNET_V6V6', u'network_id': u'54753d2c-0a58-4928-9b32-084c59dd20a6', u'subnetpool_id': None, - u'tenant_id': u'7a1ca9f7cc4e4b13ac0ed2957f1e8c32' - } + u'tenant_id': u'7a1ca9f7cc4e4b13ac0ed2957f1e8c32', + }, ] class TestMeta(base.TestCase): def test_find_nova_addresses_key_name(self): # Note 198.51.100.0/24 is TEST-NET-2 from rfc5737 - addrs = {'public': [{'addr': '198.51.100.1', 'version': 4}], - 'private': [{'addr': '192.0.2.5', 'version': 4}]} + addrs = { + 'public': [{'addr': '198.51.100.1', 'version': 4}], + 'private': [{'addr': '192.0.2.5', 'version': 4}], + } self.assertEqual( ['198.51.100.1'], - meta.find_nova_addresses(addrs, key_name='public')) + meta.find_nova_addresses(addrs, key_name='public'), + ) self.assertEqual([], meta.find_nova_addresses(addrs, key_name='foo')) def test_find_nova_addresses_ext_tag(self): - addrs = {'public': [{'OS-EXT-IPS:type': 'fixed', - 'addr': '198.51.100.2', - 'version': 4}]} + addrs = { + 'public': [ + { + 'OS-EXT-IPS:type': 'fixed', + 'addr': '198.51.100.2', + 'version': 4, + } + ] + } self.assertEqual( - ['198.51.100.2'], meta.find_nova_addresses(addrs, ext_tag='fixed')) + ['198.51.100.2'], meta.find_nova_addresses(addrs, ext_tag='fixed') + ) self.assertEqual([], meta.find_nova_addresses(addrs, ext_tag='foo')) def test_find_nova_addresses_key_name_and_ext_tag(self): - addrs = {'public': [{'OS-EXT-IPS:type': 'fixed', - 'addr': '198.51.100.2', - 'version': 4}]} + addrs = { + 'public': [ + { + 'OS-EXT-IPS:type': 'fixed', + 'addr': '198.51.100.2', + 'version': 4, + } + ] + } + self.assertEqual( + ['198.51.100.2'], + meta.find_nova_addresses( + addrs, key_name='public', ext_tag='fixed' + ), + ) self.assertEqual( - ['198.51.100.2'], meta.find_nova_addresses( - addrs, key_name='public', ext_tag='fixed')) - self.assertEqual([], meta.find_nova_addresses( - addrs, key_name='public', ext_tag='foo')) - self.assertEqual([], meta.find_nova_addresses( - addrs, key_name='bar', ext_tag='fixed')) + [], + meta.find_nova_addresses(addrs, key_name='public', ext_tag='foo'), + ) + self.assertEqual( + [], + meta.find_nova_addresses(addrs, key_name='bar', ext_tag='fixed'), + ) def test_find_nova_addresses_all(self): - addrs = {'public': [{'OS-EXT-IPS:type': 'fixed', - 'addr': '198.51.100.2', - 'version': 4}]} + addrs = { + 'public': [ + { + 'OS-EXT-IPS:type': 'fixed', + 'addr': '198.51.100.2', + 'version': 4, + } + ] + } self.assertEqual( - ['198.51.100.2'], meta.find_nova_addresses( - addrs, key_name='public', ext_tag='fixed', version=4)) - self.assertEqual([], meta.find_nova_addresses( - addrs, key_name='public', ext_tag='fixed', version=6)) + ['198.51.100.2'], + meta.find_nova_addresses( + addrs, key_name='public', ext_tag='fixed', version=4 + ), + ) + self.assertEqual( + [], + meta.find_nova_addresses( + addrs, key_name='public', ext_tag='fixed', version=6 + ), + ) def test_find_nova_addresses_floating_first(self): # Note 198.51.100.0/24 is TEST-NET-2 from rfc5737 addrs = { - 'private': [{ - 'addr': '192.0.2.5', - 'version': 4, - 'OS-EXT-IPS:type': 'fixed'}], - 'public': [{ - 'addr': '198.51.100.1', - 'version': 4, - 'OS-EXT-IPS:type': 'floating'}]} + 'private': [ + {'addr': '192.0.2.5', 'version': 4, 'OS-EXT-IPS:type': 'fixed'} + ], + 'public': [ + { + 'addr': '198.51.100.1', + 'version': 4, + 'OS-EXT-IPS:type': 'floating', + } + ], + } self.assertEqual( - ['198.51.100.1', '192.0.2.5'], - meta.find_nova_addresses(addrs)) + ['198.51.100.1', '192.0.2.5'], meta.find_nova_addresses(addrs) + ) def test_get_server_ip(self): srv = meta.obj_to_munch(standard_fake_server) + self.assertEqual(PRIVATE_V4, meta.get_server_ip(srv, ext_tag='fixed')) self.assertEqual( - PRIVATE_V4, meta.get_server_ip(srv, ext_tag='fixed')) - self.assertEqual( - PUBLIC_V4, meta.get_server_ip(srv, ext_tag='floating')) + PUBLIC_V4, meta.get_server_ip(srv, ext_tag='floating') + ) def test_get_server_private_ip(self): - self.register_uris([ - dict(method='GET', - uri='https://network.example.com/v2.0/networks', - json={'networks': [{ - 'id': 'test-net-id', - 'name': 'test-net-name'}]} - ), - dict(method='GET', - uri='https://network.example.com/v2.0/subnets', - json={'subnets': SUBNETS_WITH_NAT}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://network.example.com/v2.0/networks', + json={ + 'networks': [ + {'id': 'test-net-id', 'name': 'test-net-name'} + ] + }, + ), + dict( + method='GET', + uri='https://network.example.com/v2.0/subnets', + json={'subnets': SUBNETS_WITH_NAT}, + ), + ] + ) srv = fakes.make_fake_server( - server_id='test-id', name='test-name', status='ACTIVE', - addresses={'private': [{'OS-EXT-IPS:type': 'fixed', - 'addr': PRIVATE_V4, - 'version': 4}], - 'public': [{'OS-EXT-IPS:type': 'floating', - 'addr': PUBLIC_V4, - 'version': 4}]} + server_id='test-id', + name='test-name', + status='ACTIVE', + addresses={ + 'private': [ + { + 'OS-EXT-IPS:type': 'fixed', + 'addr': PRIVATE_V4, + 'version': 4, + } + ], + 'public': [ + { + 'OS-EXT-IPS:type': 'floating', + 'addr': PUBLIC_V4, + 'version': 4, + } + ], + }, ) self.assertEqual( - PRIVATE_V4, meta.get_server_private_ip(srv, self.cloud)) + PRIVATE_V4, meta.get_server_private_ip(srv, self.cloud) + ) self.assert_calls() def test_get_server_multiple_private_ip(self): - self.register_uris([ - dict(method='GET', - uri='https://network.example.com/v2.0/networks', - json={'networks': [{ - 'id': 'test-net-id', - 'name': 'test-net'}]} - ), - dict(method='GET', - uri='https://network.example.com/v2.0/subnets', - json={'subnets': SUBNETS_WITH_NAT}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://network.example.com/v2.0/networks', + json={ + 'networks': [{'id': 'test-net-id', 'name': 'test-net'}] + }, + ), + dict( + method='GET', + uri='https://network.example.com/v2.0/subnets', + json={'subnets': SUBNETS_WITH_NAT}, + ), + ] + ) shared_mac = '11:22:33:44:55:66' distinct_mac = '66:55:44:33:22:11' srv = fakes.make_fake_server( - server_id='test-id', name='test-name', status='ACTIVE', - addresses={'test-net': [{'OS-EXT-IPS:type': 'fixed', - 'OS-EXT-IPS-MAC:mac_addr': distinct_mac, - 'addr': '10.0.0.100', - 'version': 4}, - {'OS-EXT-IPS:type': 'fixed', - 'OS-EXT-IPS-MAC:mac_addr': shared_mac, - 'addr': '10.0.0.101', - 'version': 4}], - 'public': [{'OS-EXT-IPS:type': 'floating', - 'OS-EXT-IPS-MAC:mac_addr': shared_mac, - 'addr': PUBLIC_V4, - 'version': 4}]} + server_id='test-id', + name='test-name', + status='ACTIVE', + addresses={ + 'test-net': [ + { + 'OS-EXT-IPS:type': 'fixed', + 'OS-EXT-IPS-MAC:mac_addr': distinct_mac, + 'addr': '10.0.0.100', + 'version': 4, + }, + { + 'OS-EXT-IPS:type': 'fixed', + 'OS-EXT-IPS-MAC:mac_addr': shared_mac, + 'addr': '10.0.0.101', + 'version': 4, + }, + ], + 'public': [ + { + 'OS-EXT-IPS:type': 'floating', + 'OS-EXT-IPS-MAC:mac_addr': shared_mac, + 'addr': PUBLIC_V4, + 'version': 4, + } + ], + }, ) self.assertEqual( - '10.0.0.101', meta.get_server_private_ip(srv, self.cloud)) + '10.0.0.101', meta.get_server_private_ip(srv, self.cloud) + ) self.assert_calls() @mock.patch.object(connection.Connection, 'has_service') @@ -362,9 +445,12 @@ def test_get_server_multiple_private_ip(self): @mock.patch.object(connection.Connection, 'get_image_name') @mock.patch.object(connection.Connection, 'get_flavor_name') def test_get_server_private_ip_devstack( - self, - mock_get_flavor_name, mock_get_image_name, - mock_get_volumes, mock_has_service): + self, + mock_get_flavor_name, + mock_get_image_name, + mock_get_volumes, + mock_has_service, + ): mock_get_image_name.return_value = 'cirros-0.3.4-x86_64-uec' mock_get_flavor_name.return_value = 'm1.tiny' @@ -372,61 +458,92 @@ def test_get_server_private_ip_devstack( mock_has_service.return_value = True fake_server = fakes.make_fake_server( - server_id='test-id', name='test-name', status='ACTIVE', + server_id='test-id', + name='test-name', + status='ACTIVE', flavor={u'id': u'1'}, image={ 'name': u'cirros-0.3.4-x86_64-uec', - u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1'}, - addresses={u'test_pnztt_net': [{ - u'OS-EXT-IPS:type': u'fixed', - u'addr': PRIVATE_V4, - u'version': 4, - u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:ae:7d:42' - }]} + u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1', + }, + addresses={ + u'test_pnztt_net': [ + { + u'OS-EXT-IPS:type': u'fixed', + u'addr': PRIVATE_V4, + u'version': 4, + u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:ae:7d:42', + } + ] + }, + ) + + self.register_uris( + [ + dict( + method='GET', + uri=( + 'https://network.example.com/v2.0/ports?' + 'device_id=test-id' + ), + json={ + 'ports': [ + { + 'id': 'test_port_id', + 'mac_address': 'fa:16:3e:ae:7d:42', + 'device_id': 'test-id', + } + ] + }, + ), + dict( + method='GET', + uri=( + 'https://network.example.com/v2.0/' + 'floatingips?port_id=test_port_id' + ), + json={'floatingips': []}, + ), + dict( + method='GET', + uri='https://network.example.com/v2.0/networks', + json={ + 'networks': [ + { + 'id': 'test_pnztt_net', + 'name': 'test_pnztt_net', + 'router:external': False, + }, + {'id': 'private', 'name': 'private'}, + ] + }, + ), + dict( + method='GET', + uri='https://network.example.com/v2.0/subnets', + json={'subnets': SUBNETS_WITH_NAT}, + ), + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', fake_server['id']], + ), + json=fake_server, + ), + dict( + method='GET', + uri='{endpoint}/servers/test-id/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'security_groups': []}, + ), + ] ) - self.register_uris([ - dict(method='GET', - uri=('https://network.example.com/v2.0/ports?' - 'device_id=test-id'), - json={'ports': [{ - 'id': 'test_port_id', - 'mac_address': 'fa:16:3e:ae:7d:42', - 'device_id': 'test-id'}]} - ), - dict(method='GET', - uri=('https://network.example.com/v2.0/' - 'floatingips?port_id=test_port_id'), - json={'floatingips': []}), - - dict(method='GET', - uri='https://network.example.com/v2.0/networks', - json={'networks': [ - {'id': 'test_pnztt_net', - 'name': 'test_pnztt_net', - 'router:external': False - }, - {'id': 'private', - 'name': 'private'}]} - ), - dict(method='GET', - uri='https://network.example.com/v2.0/subnets', - json={'subnets': SUBNETS_WITH_NAT}), - - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', fake_server['id']]), - json=fake_server), - dict(method='GET', - uri='{endpoint}/servers/test-id/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_groups': []}) - ]) - - srv = self.cloud.get_openstack_vars( - _server.Server(**fake_server)) + srv = self.cloud.get_openstack_vars(_server.Server(**fake_server)) self.assertEqual(PRIVATE_V4, srv['private_v4']) self.assert_calls() @@ -435,9 +552,8 @@ def test_get_server_private_ip_devstack( @mock.patch.object(connection.Connection, 'get_image_name') @mock.patch.object(connection.Connection, 'get_flavor_name') def test_get_server_private_ip_no_fip( - self, - mock_get_flavor_name, mock_get_image_name, - mock_get_volumes): + self, mock_get_flavor_name, mock_get_image_name, mock_get_volumes + ): self.cloud._floating_ip_source = None mock_get_image_name.return_value = 'cirros-0.3.4-x86_64-uec' @@ -445,47 +561,68 @@ def test_get_server_private_ip_no_fip( mock_get_volumes.return_value = [] fake_server = fakes.make_fake_server( - server_id='test-id', name='test-name', status='ACTIVE', + server_id='test-id', + name='test-name', + status='ACTIVE', flavor={u'id': u'1'}, image={ 'name': u'cirros-0.3.4-x86_64-uec', - u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1'}, - addresses={u'test_pnztt_net': [{ - u'OS-EXT-IPS:type': u'fixed', - u'addr': PRIVATE_V4, - u'version': 4, - u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:ae:7d:42' - }]} + u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1', + }, + addresses={ + u'test_pnztt_net': [ + { + u'OS-EXT-IPS:type': u'fixed', + u'addr': PRIVATE_V4, + u'version': 4, + u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:ae:7d:42', + } + ] + }, ) - self.register_uris([ - dict(method='GET', - uri='https://network.example.com/v2.0/networks', - json={'networks': [ - {'id': 'test_pnztt_net', - 'name': 'test_pnztt_net', - 'router:external': False, - }, - {'id': 'private', - 'name': 'private'}]} - ), - dict(method='GET', - uri='https://network.example.com/v2.0/subnets', - json={'subnets': SUBNETS_WITH_NAT}), - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', fake_server['id']]), - json=fake_server), - dict(method='GET', - uri='{endpoint}/servers/test-id/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_groups': []}) - ]) - - srv = self.cloud.get_openstack_vars( - _server.Server(**fake_server)) + self.register_uris( + [ + dict( + method='GET', + uri='https://network.example.com/v2.0/networks', + json={ + 'networks': [ + { + 'id': 'test_pnztt_net', + 'name': 'test_pnztt_net', + 'router:external': False, + }, + {'id': 'private', 'name': 'private'}, + ] + }, + ), + dict( + method='GET', + uri='https://network.example.com/v2.0/subnets', + json={'subnets': SUBNETS_WITH_NAT}, + ), + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', fake_server['id']], + ), + json=fake_server, + ), + dict( + method='GET', + uri='{endpoint}/servers/test-id/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'security_groups': []}, + ), + ] + ) + + srv = self.cloud.get_openstack_vars(_server.Server(**fake_server)) self.assertEqual(PRIVATE_V4, srv['private_v4']) self.assert_calls() @@ -494,56 +631,74 @@ def test_get_server_private_ip_no_fip( @mock.patch.object(connection.Connection, 'get_image_name') @mock.patch.object(connection.Connection, 'get_flavor_name') def test_get_server_cloud_no_fips( - self, - mock_get_flavor_name, mock_get_image_name, - mock_get_volumes): + self, mock_get_flavor_name, mock_get_image_name, mock_get_volumes + ): self.cloud._floating_ip_source = None mock_get_image_name.return_value = 'cirros-0.3.4-x86_64-uec' mock_get_flavor_name.return_value = 'm1.tiny' mock_get_volumes.return_value = [] fake_server = fakes.make_fake_server( - server_id='test-id', name='test-name', status='ACTIVE', + server_id='test-id', + name='test-name', + status='ACTIVE', flavor={u'id': u'1'}, image={ 'name': u'cirros-0.3.4-x86_64-uec', - u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1'}, - addresses={u'test_pnztt_net': [{ - u'addr': PRIVATE_V4, - u'version': 4, - }]} + u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1', + }, + addresses={ + u'test_pnztt_net': [ + { + u'addr': PRIVATE_V4, + u'version': 4, + } + ] + }, ) - self.register_uris([ - dict(method='GET', - uri='https://network.example.com/v2.0/networks', - json={'networks': [ - { - 'id': 'test_pnztt_net', - 'name': 'test_pnztt_net', - 'router:external': False, - }, - { - 'id': 'private', - 'name': 'private'}]} - ), - dict(method='GET', - uri='https://network.example.com/v2.0/subnets', - json={'subnets': SUBNETS_WITH_NAT}), - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', fake_server['id']]), - json=fake_server), - dict(method='GET', - uri='{endpoint}/servers/test-id/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_groups': []}) - ]) - - srv = self.cloud.get_openstack_vars( - _server.Server(**fake_server)) + self.register_uris( + [ + dict( + method='GET', + uri='https://network.example.com/v2.0/networks', + json={ + 'networks': [ + { + 'id': 'test_pnztt_net', + 'name': 'test_pnztt_net', + 'router:external': False, + }, + {'id': 'private', 'name': 'private'}, + ] + }, + ), + dict( + method='GET', + uri='https://network.example.com/v2.0/subnets', + json={'subnets': SUBNETS_WITH_NAT}, + ), + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', fake_server['id']], + ), + json=fake_server, + ), + dict( + method='GET', + uri='{endpoint}/servers/test-id/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'security_groups': []}, + ), + ] + ) + + srv = self.cloud.get_openstack_vars(_server.Server(**fake_server)) self.assertEqual(PRIVATE_V4, srv['private_v4']) self.assert_calls() @@ -553,76 +708,116 @@ def test_get_server_cloud_no_fips( @mock.patch.object(connection.Connection, 'get_image_name') @mock.patch.object(connection.Connection, 'get_flavor_name') def test_get_server_cloud_missing_fips( - self, - mock_get_flavor_name, mock_get_image_name, - mock_get_volumes, mock_has_service): + self, + mock_get_flavor_name, + mock_get_image_name, + mock_get_volumes, + mock_has_service, + ): mock_get_image_name.return_value = 'cirros-0.3.4-x86_64-uec' mock_get_flavor_name.return_value = 'm1.tiny' mock_get_volumes.return_value = [] mock_has_service.return_value = True fake_server = fakes.make_fake_server( - server_id='test-id', name='test-name', status='ACTIVE', + server_id='test-id', + name='test-name', + status='ACTIVE', flavor={u'id': u'1'}, image={ 'name': u'cirros-0.3.4-x86_64-uec', - u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1'}, - addresses={u'test_pnztt_net': [{ - u'addr': PRIVATE_V4, - u'version': 4, - 'OS-EXT-IPS-MAC:mac_addr': 'fa:16:3e:ae:7d:42', - }]} + u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1', + }, + addresses={ + u'test_pnztt_net': [ + { + u'addr': PRIVATE_V4, + u'version': 4, + 'OS-EXT-IPS-MAC:mac_addr': 'fa:16:3e:ae:7d:42', + } + ] + }, + ) + + self.register_uris( + [ + # self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=( + 'https://network.example.com/v2.0/ports?' + 'device_id=test-id' + ), + json={ + 'ports': [ + { + 'id': 'test_port_id', + 'mac_address': 'fa:16:3e:ae:7d:42', + 'device_id': 'test-id', + } + ] + }, + ), + dict( + method='GET', + uri=( + 'https://network.example.com/v2.0/floatingips' + '?port_id=test_port_id' + ), + json={ + 'floatingips': [ + { + 'id': 'floating-ip-id', + 'port_id': 'test_port_id', + 'fixed_ip_address': PRIVATE_V4, + 'floating_ip_address': PUBLIC_V4, + } + ] + }, + ), + dict( + method='GET', + uri='https://network.example.com/v2.0/networks', + json={ + 'networks': [ + { + 'id': 'test_pnztt_net', + 'name': 'test_pnztt_net', + 'router:external': False, + }, + { + 'id': 'private', + 'name': 'private', + }, + ] + }, + ), + dict( + method='GET', + uri='https://network.example.com/v2.0/subnets', + json={'subnets': SUBNETS_WITH_NAT}, + ), + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', fake_server['id']], + ), + json=fake_server, + ), + dict( + method='GET', + uri='{endpoint}/servers/test-id/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'security_groups': []}, + ), + ] ) - self.register_uris([ - # self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=('https://network.example.com/v2.0/ports?' - 'device_id=test-id'), - json={'ports': [{ - 'id': 'test_port_id', - 'mac_address': 'fa:16:3e:ae:7d:42', - 'device_id': 'test-id'}]} - ), - dict(method='GET', - uri=('https://network.example.com/v2.0/floatingips' - '?port_id=test_port_id'), - json={'floatingips': [{ - 'id': 'floating-ip-id', - 'port_id': 'test_port_id', - 'fixed_ip_address': PRIVATE_V4, - 'floating_ip_address': PUBLIC_V4, - }]}), - dict(method='GET', - uri='https://network.example.com/v2.0/networks', - json={'networks': [ - { - 'id': 'test_pnztt_net', - 'name': 'test_pnztt_net', - 'router:external': False, - }, - { - 'id': 'private', - 'name': 'private', - } - ]}), - dict(method='GET', - uri='https://network.example.com/v2.0/subnets', - json={'subnets': SUBNETS_WITH_NAT}), - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', fake_server['id']]), - json=fake_server), - dict(method='GET', - uri='{endpoint}/servers/test-id/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_groups': []}) - ]) - - srv = self.cloud.get_openstack_vars( - _server.Server(**fake_server)) + srv = self.cloud.get_openstack_vars(_server.Server(**fake_server)) self.assertEqual(PUBLIC_V4, srv['public_v4']) self.assert_calls() @@ -631,8 +826,8 @@ def test_get_server_cloud_missing_fips( @mock.patch.object(connection.Connection, 'get_image_name') @mock.patch.object(connection.Connection, 'get_flavor_name') def test_get_server_cloud_rackspace_v6( - self, mock_get_flavor_name, mock_get_image_name, - mock_get_volumes): + self, mock_get_flavor_name, mock_get_image_name, mock_get_volumes + ): self.cloud.config.config['has_network'] = False self.cloud._floating_ip_source = None self.cloud.force_ipv4 = False @@ -641,56 +836,66 @@ def test_get_server_cloud_rackspace_v6( mock_get_flavor_name.return_value = 'm1.tiny' mock_get_volumes.return_value = [] fake_server = fakes.make_fake_server( - server_id='test-id', name='test-name', status='ACTIVE', + server_id='test-id', + name='test-name', + status='ACTIVE', flavor={u'id': u'1'}, image={ 'name': u'cirros-0.3.4-x86_64-uec', - u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1'}, + u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1', + }, addresses={ - 'private': [{ - 'addr': "10.223.160.141", - 'version': 4 - }], - 'public': [{ - 'addr': "104.130.246.91", - 'version': 4 - }, { - 'addr': "2001:4800:7819:103:be76:4eff:fe05:8525", - 'version': 6 - }] - } + 'private': [{'addr': "10.223.160.141", 'version': 4}], + 'public': [ + {'addr': "104.130.246.91", 'version': 4}, + { + 'addr': "2001:4800:7819:103:be76:4eff:fe05:8525", + 'version': 6, + }, + ], + }, + ) + + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', fake_server['id']], + ), + json=fake_server, + ), + dict( + method='GET', + uri='{endpoint}/servers/test-id/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'security_groups': []}, + ), + ] ) - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', fake_server['id']]), - json=fake_server), - dict(method='GET', - uri='{endpoint}/servers/test-id/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_groups': []}) - ]) - - srv = self.cloud.get_openstack_vars( - _server.Server(**fake_server)) + srv = self.cloud.get_openstack_vars(_server.Server(**fake_server)) self.assertEqual("10.223.160.141", srv['private_v4']) self.assertEqual("104.130.246.91", srv['public_v4']) self.assertEqual( - "2001:4800:7819:103:be76:4eff:fe05:8525", srv['public_v6']) + "2001:4800:7819:103:be76:4eff:fe05:8525", srv['public_v6'] + ) self.assertEqual( - "2001:4800:7819:103:be76:4eff:fe05:8525", srv['interface_ip']) + "2001:4800:7819:103:be76:4eff:fe05:8525", srv['interface_ip'] + ) self.assert_calls() @mock.patch.object(connection.Connection, 'get_volumes') @mock.patch.object(connection.Connection, 'get_image_name') @mock.patch.object(connection.Connection, 'get_flavor_name') def test_get_server_cloud_osic_split( - self, mock_get_flavor_name, mock_get_image_name, - mock_get_volumes): + self, mock_get_flavor_name, mock_get_image_name, mock_get_volumes + ): self.cloud._floating_ip_source = None self.cloud.force_ipv4 = False self.cloud._local_ipv6 = True @@ -703,75 +908,99 @@ def test_get_server_cloud_osic_split( mock_get_volumes.return_value = [] fake_server = fakes.make_fake_server( - server_id='test-id', name='test-name', status='ACTIVE', + server_id='test-id', + name='test-name', + status='ACTIVE', flavor={u'id': u'1'}, image={ 'name': u'cirros-0.3.4-x86_64-uec', - u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1'}, + u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1', + }, addresses={ - 'private': [{ - 'addr': "10.223.160.141", - 'version': 4 - }], - 'public': [{ - 'addr': "104.130.246.91", - 'version': 4 - }, { - 'addr': "2001:4800:7819:103:be76:4eff:fe05:8525", - 'version': 6 - }] - } + 'private': [{'addr': "10.223.160.141", 'version': 4}], + 'public': [ + {'addr': "104.130.246.91", 'version': 4}, + { + 'addr': "2001:4800:7819:103:be76:4eff:fe05:8525", + 'version': 6, + }, + ], + }, + ) + + self.register_uris( + [ + dict( + method='GET', + uri='https://network.example.com/v2.0/networks', + json={'networks': OSIC_NETWORKS}, + ), + dict( + method='GET', + uri='https://network.example.com/v2.0/subnets', + json={'subnets': OSIC_SUBNETS}, + ), + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', fake_server['id']], + ), + json=fake_server, + ), + dict( + method='GET', + uri='{endpoint}/servers/test-id/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'security_groups': []}, + ), + ] ) - self.register_uris([ - dict(method='GET', - uri='https://network.example.com/v2.0/networks', - json={'networks': OSIC_NETWORKS}), - dict(method='GET', - uri='https://network.example.com/v2.0/subnets', - json={'subnets': OSIC_SUBNETS}), - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', fake_server['id']]), - json=fake_server), - dict(method='GET', - uri='{endpoint}/servers/test-id/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_groups': []}) - ]) - - srv = self.cloud.get_openstack_vars( - _server.Server(**fake_server)) + srv = self.cloud.get_openstack_vars(_server.Server(**fake_server)) self.assertEqual("10.223.160.141", srv['private_v4']) self.assertEqual("104.130.246.91", srv['public_v4']) self.assertEqual( - "2001:4800:7819:103:be76:4eff:fe05:8525", srv['public_v6']) + "2001:4800:7819:103:be76:4eff:fe05:8525", srv['public_v6'] + ) self.assertEqual( - "2001:4800:7819:103:be76:4eff:fe05:8525", srv['interface_ip']) + "2001:4800:7819:103:be76:4eff:fe05:8525", srv['interface_ip'] + ) self.assert_calls() def test_get_server_external_ipv4_neutron(self): # Testing Clouds with Neutron - self.register_uris([ - dict(method='GET', - uri='https://network.example.com/v2.0/networks', - json={'networks': [{ - 'id': 'test-net-id', - 'name': 'test-net', - 'router:external': True - }]}), - dict(method='GET', - uri='https://network.example.com/v2.0/subnets', - json={'subnets': SUBNETS_WITH_NAT}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://network.example.com/v2.0/networks', + json={ + 'networks': [ + { + 'id': 'test-net-id', + 'name': 'test-net', + 'router:external': True, + } + ] + }, + ), + dict( + method='GET', + uri='https://network.example.com/v2.0/subnets', + json={'subnets': SUBNETS_WITH_NAT}, + ), + ] + ) srv = fakes.make_fake_server( - server_id='test-id', name='test-name', status='ACTIVE', - addresses={'test-net': [{ - 'addr': PUBLIC_V4, - 'version': 4}]}, + server_id='test-id', + name='test-name', + status='ACTIVE', + addresses={'test-net': [{'addr': PUBLIC_V4, 'version': 4}]}, ) ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) @@ -780,25 +1009,35 @@ def test_get_server_external_ipv4_neutron(self): def test_get_server_external_provider_ipv4_neutron(self): # Testing Clouds with Neutron - self.register_uris([ - dict(method='GET', - uri='https://network.example.com/v2.0/networks', - json={'networks': [{ - 'id': 'test-net-id', - 'name': 'test-net', - 'provider:network_type': 'vlan', - 'provider:physical_network': 'vlan', - }]}), - dict(method='GET', - uri='https://network.example.com/v2.0/subnets', - json={'subnets': SUBNETS_WITH_NAT}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://network.example.com/v2.0/networks', + json={ + 'networks': [ + { + 'id': 'test-net-id', + 'name': 'test-net', + 'provider:network_type': 'vlan', + 'provider:physical_network': 'vlan', + } + ] + }, + ), + dict( + method='GET', + uri='https://network.example.com/v2.0/subnets', + json={'subnets': SUBNETS_WITH_NAT}, + ), + ] + ) srv = fakes.make_fake_server( - server_id='test-id', name='test-name', status='ACTIVE', - addresses={'test-net': [{ - 'addr': PUBLIC_V4, - 'version': 4}]}, + server_id='test-id', + name='test-name', + status='ACTIVE', + addresses={'test-net': [{'addr': PUBLIC_V4, 'version': 4}]}, ) ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) @@ -807,28 +1046,39 @@ def test_get_server_external_provider_ipv4_neutron(self): def test_get_server_internal_provider_ipv4_neutron(self): # Testing Clouds with Neutron - self.register_uris([ - dict(method='GET', - uri='https://network.example.com/v2.0/networks', - json={'networks': [{ - 'id': 'test-net-id', - 'name': 'test-net', - 'router:external': False, - 'provider:network_type': 'vxlan', - 'provider:physical_network': None, - }]}), - dict(method='GET', - uri='https://network.example.com/v2.0/subnets', - json={'subnets': SUBNETS_WITH_NAT}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://network.example.com/v2.0/networks', + json={ + 'networks': [ + { + 'id': 'test-net-id', + 'name': 'test-net', + 'router:external': False, + 'provider:network_type': 'vxlan', + 'provider:physical_network': None, + } + ] + }, + ), + dict( + method='GET', + uri='https://network.example.com/v2.0/subnets', + json={'subnets': SUBNETS_WITH_NAT}, + ), + ] + ) srv = fakes.make_fake_server( - server_id='test-id', name='test-name', status='ACTIVE', - addresses={'test-net': [{ - 'addr': PRIVATE_V4, - 'version': 4}]}, + server_id='test-id', + name='test-name', + status='ACTIVE', + addresses={'test-net': [{'addr': PRIVATE_V4, 'version': 4}]}, ) self.assertIsNone( - meta.get_server_external_ipv4(cloud=self.cloud, server=srv)) + meta.get_server_external_ipv4(cloud=self.cloud, server=srv) + ) int_ip = meta.get_server_private_ip(cloud=self.cloud, server=srv) self.assertEqual(PRIVATE_V4, int_ip) @@ -836,24 +1086,34 @@ def test_get_server_internal_provider_ipv4_neutron(self): def test_get_server_external_none_ipv4_neutron(self): # Testing Clouds with Neutron - self.register_uris([ - dict(method='GET', - uri='https://network.example.com/v2.0/networks', - json={'networks': [{ - 'id': 'test-net-id', - 'name': 'test-net', - 'router:external': False, - }]}), - dict(method='GET', - uri='https://network.example.com/v2.0/subnets', - json={'subnets': SUBNETS_WITH_NAT}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://network.example.com/v2.0/networks', + json={ + 'networks': [ + { + 'id': 'test-net-id', + 'name': 'test-net', + 'router:external': False, + } + ] + }, + ), + dict( + method='GET', + uri='https://network.example.com/v2.0/subnets', + json={'subnets': SUBNETS_WITH_NAT}, + ), + ] + ) srv = fakes.make_fake_server( - server_id='test-id', name='test-name', status='ACTIVE', - addresses={'test-net': [{ - 'addr': PUBLIC_V4, - 'version': 4}]}, + server_id='test-id', + name='test-name', + status='ACTIVE', + addresses={'test-net': [{'addr': PUBLIC_V4, 'version': 4}]}, ) ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) @@ -862,7 +1122,8 @@ def test_get_server_external_none_ipv4_neutron(self): def test_get_server_external_ipv4_neutron_accessIPv4(self): srv = fakes.make_fake_server( - server_id='test-id', name='test-name', status='ACTIVE') + server_id='test-id', name='test-name', status='ACTIVE' + ) srv['accessIPv4'] = PUBLIC_V4 ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) @@ -870,7 +1131,8 @@ def test_get_server_external_ipv4_neutron_accessIPv4(self): def test_get_server_external_ipv4_neutron_accessIPv6(self): srv = fakes.make_fake_server( - server_id='test-id', name='test-name', status='ACTIVE') + server_id='test-id', name='test-name', status='ACTIVE' + ) srv['accessIPv6'] = PUBLIC_V6 ip = meta.get_server_external_ipv6(server=srv) @@ -878,14 +1140,21 @@ def test_get_server_external_ipv4_neutron_accessIPv6(self): def test_get_server_external_ipv4_neutron_exception(self): # Testing Clouds with a non working Neutron - self.register_uris([ - dict(method='GET', - uri='https://network.example.com/v2.0/networks', - status_code=404)]) + self.register_uris( + [ + dict( + method='GET', + uri='https://network.example.com/v2.0/networks', + status_code=404, + ) + ] + ) srv = fakes.make_fake_server( - server_id='test-id', name='test-name', status='ACTIVE', - addresses={'public': [{'addr': PUBLIC_V4, 'version': 4}]} + server_id='test-id', + name='test-name', + status='ACTIVE', + addresses={'public': [{'addr': PUBLIC_V4, 'version': 4}]}, ) ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) @@ -897,8 +1166,11 @@ def test_get_server_external_ipv4_nova_public(self): self.cloud.config.config['has_network'] = False srv = fakes.make_fake_server( - server_id='test-id', name='test-name', status='ACTIVE', - addresses={'public': [{'addr': PUBLIC_V4, 'version': 4}]}) + server_id='test-id', + name='test-name', + status='ACTIVE', + addresses={'public': [{'addr': PUBLIC_V4, 'version': 4}]}, + ) ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) self.assertEqual(PUBLIC_V4, ip) @@ -908,48 +1180,58 @@ def test_get_server_external_ipv4_nova_none(self): self.cloud.config.config['has_network'] = False srv = fakes.make_fake_server( - server_id='test-id', name='test-name', status='ACTIVE', - addresses={'test-net': [{'addr': PRIVATE_V4}]}) + server_id='test-id', + name='test-name', + status='ACTIVE', + addresses={'test-net': [{'addr': PRIVATE_V4}]}, + ) ip = meta.get_server_external_ipv4(cloud=self.cloud, server=srv) self.assertIsNone(ip) def test_get_server_external_ipv6(self): srv = fakes.make_fake_server( - server_id='test-id', name='test-name', status='ACTIVE', + server_id='test-id', + name='test-name', + status='ACTIVE', addresses={ 'test-net': [ {'addr': PUBLIC_V4, 'version': 4}, - {'addr': PUBLIC_V6, 'version': 6} + {'addr': PUBLIC_V6, 'version': 6}, ] - } + }, ) ip = meta.get_server_external_ipv6(srv) self.assertEqual(PUBLIC_V6, ip) def test_get_groups_from_server(self): - server_vars = {'flavor': 'test-flavor', - 'image': 'test-image', - 'az': 'test-az'} + server_vars = { + 'flavor': 'test-flavor', + 'image': 'test-image', + 'az': 'test-az', + } self.assertEqual( - ['test-name', - 'test-region', - 'test-name_test-region', - 'test-group', - 'instance-test-id-0', - 'meta-group_test-group', - 'test-az', - 'test-region_test-az', - 'test-name_test-region_test-az'], + [ + 'test-name', + 'test-region', + 'test-name_test-region', + 'test-group', + 'instance-test-id-0', + 'meta-group_test-group', + 'test-az', + 'test-region_test-az', + 'test-name_test-region_test-az', + ], meta.get_groups_from_server( FakeCloud(), meta.obj_to_munch(standard_fake_server), - server_vars - ) + server_vars, + ), ) def test_obj_list_to_munch(self): """Test conversion of a list of objects to a list of dictonaries""" + class obj0: value = 0 @@ -962,33 +1244,35 @@ class obj1: self.assertEqual(new_list[1]['value'], 1) @mock.patch.object(FakeCloud, 'list_server_security_groups') - def test_get_security_groups(self, - mock_list_server_security_groups): + def test_get_security_groups(self, mock_list_server_security_groups): '''This test verifies that calling get_hostvars_froms_server ultimately calls list_server_security_groups, and that the return value from list_server_security_groups ends up in server['security_groups'].''' mock_list_server_security_groups.return_value = [ - {'name': 'testgroup', 'id': '1'}] + {'name': 'testgroup', 'id': '1'} + ] server = meta.obj_to_munch(standard_fake_server) hostvars = meta.get_hostvars_from_server(FakeCloud(), server) mock_list_server_security_groups.assert_called_once_with(server) - self.assertEqual('testgroup', - hostvars['security_groups'][0]['name']) + self.assertEqual('testgroup', hostvars['security_groups'][0]['name']) @mock.patch.object(meta, 'get_server_external_ipv6') @mock.patch.object(meta, 'get_server_external_ipv4') def test_basic_hostvars( - self, mock_get_server_external_ipv4, - mock_get_server_external_ipv6): + self, mock_get_server_external_ipv4, mock_get_server_external_ipv6 + ): mock_get_server_external_ipv4.return_value = PUBLIC_V4 mock_get_server_external_ipv6.return_value = PUBLIC_V6 hostvars = meta.get_hostvars_from_server( - FakeCloud(), self.cloud._normalize_server( - meta.obj_to_munch(standard_fake_server))) + FakeCloud(), + self.cloud._normalize_server( + meta.obj_to_munch(standard_fake_server) + ), + ) self.assertNotIn('links', hostvars) self.assertEqual(PRIVATE_V4, hostvars['private_v4']) self.assertEqual(PUBLIC_V4, hostvars['public_v4']) @@ -1000,13 +1284,16 @@ def test_basic_hostvars( self.assertEqual('_test_cloud_', hostvars['location']['cloud']) self.assertEqual('RegionOne', hostvars['location']['region_name']) self.assertEqual( - fakes.PROJECT_ID, hostvars['location']['project']['id']) + fakes.PROJECT_ID, hostvars['location']['project']['id'] + ) self.assertEqual("test-image-name", hostvars['image']['name']) self.assertEqual( - standard_fake_server['image']['id'], hostvars['image']['id']) + standard_fake_server['image']['id'], hostvars['image']['id'] + ) self.assertNotIn('links', hostvars['image']) self.assertEqual( - standard_fake_server['flavor']['id'], hostvars['flavor']['id']) + standard_fake_server['flavor']['id'], hostvars['flavor']['id'] + ) self.assertEqual("test-flavor-name", hostvars['flavor']['name']) self.assertNotIn('links', hostvars['flavor']) # test having volumes @@ -1016,15 +1303,16 @@ def test_basic_hostvars( @mock.patch.object(meta, 'get_server_external_ipv6') @mock.patch.object(meta, 'get_server_external_ipv4') def test_ipv4_hostvars( - self, mock_get_server_external_ipv4, - mock_get_server_external_ipv6): + self, mock_get_server_external_ipv4, mock_get_server_external_ipv6 + ): mock_get_server_external_ipv4.return_value = PUBLIC_V4 mock_get_server_external_ipv6.return_value = PUBLIC_V6 fake_cloud = FakeCloud() fake_cloud.force_ipv4 = True hostvars = meta.get_hostvars_from_server( - fake_cloud, meta.obj_to_munch(standard_fake_server)) + fake_cloud, meta.obj_to_munch(standard_fake_server) + ) self.assertEqual(PUBLIC_V4, hostvars['interface_ip']) self.assertEqual('', hostvars['public_v6']) @@ -1035,7 +1323,8 @@ def test_private_interface_ip(self, mock_get_server_external_ipv4): cloud = FakeCloud() cloud.private = True hostvars = meta.get_hostvars_from_server( - cloud, meta.obj_to_munch(standard_fake_server)) + cloud, meta.obj_to_munch(standard_fake_server) + ) self.assertEqual(PRIVATE_V4, hostvars['interface_ip']) @mock.patch.object(meta, 'get_server_external_ipv4') @@ -1045,7 +1334,8 @@ def test_image_string(self, mock_get_server_external_ipv4): server = standard_fake_server server['image'] = 'fake-image-id' hostvars = meta.get_hostvars_from_server( - FakeCloud(), meta.obj_to_munch(server)) + FakeCloud(), meta.obj_to_munch(server) + ) self.assertEqual('fake-image-id', hostvars['image']['id']) def test_az(self): @@ -1056,25 +1346,31 @@ def test_az(self): self.assertEqual('az1', hostvars['az']) def test_current_location(self): - self.assertEqual({ - 'cloud': '_test_cloud_', - 'project': { + self.assertEqual( + { + 'cloud': '_test_cloud_', + 'project': { + 'id': mock.ANY, + 'name': 'admin', + 'domain_id': None, + 'domain_name': 'default', + }, + 'region_name': u'RegionOne', + 'zone': None, + }, + self.cloud.current_location, + ) + + def test_current_project(self): + self.assertEqual( + { 'id': mock.ANY, 'name': 'admin', 'domain_id': None, - 'domain_name': 'default' + 'domain_name': 'default', }, - 'region_name': u'RegionOne', - 'zone': None}, - self.cloud.current_location) - - def test_current_project(self): - self.assertEqual({ - 'id': mock.ANY, - 'name': 'admin', - 'domain_id': None, - 'domain_name': 'default'}, - self.cloud.current_project) + self.cloud.current_project, + ) def test_has_volume(self): mock_cloud = mock.MagicMock() @@ -1083,11 +1379,13 @@ def test_has_volume(self): id='volume1', status='available', name='Volume 1 Display Name', - attachments=[{'device': '/dev/sda0'}]) + attachments=[{'device': '/dev/sda0'}], + ) fake_volume_dict = meta.obj_to_munch(fake_volume) mock_cloud.get_volumes.return_value = [fake_volume_dict] hostvars = meta.get_hostvars_from_server( - mock_cloud, meta.obj_to_munch(standard_fake_server)) + mock_cloud, meta.obj_to_munch(standard_fake_server) + ) self.assertEqual('volume1', hostvars['volumes'][0]['id']) self.assertEqual('/dev/sda0', hostvars['volumes'][0]['device']) @@ -1095,7 +1393,8 @@ def test_has_no_volume_service(self): fake_cloud = FakeCloud() fake_cloud.service_val = False hostvars = meta.get_hostvars_from_server( - fake_cloud, meta.obj_to_munch(standard_fake_server)) + fake_cloud, meta.obj_to_munch(standard_fake_server) + ) self.assertEqual([], hostvars['volumes']) def test_unknown_volume_exception(self): @@ -1106,12 +1405,14 @@ class FakeException(Exception): def side_effect(*args): raise FakeException("No Volumes") + mock_cloud.get_volumes.side_effect = side_effect self.assertRaises( FakeException, meta.get_hostvars_from_server, mock_cloud, - meta.obj_to_munch(standard_fake_server)) + meta.obj_to_munch(standard_fake_server), + ) def test_obj_to_munch(self): cloud = FakeCloud() @@ -1127,6 +1428,7 @@ def test_obj_to_munch(self): def test_obj_to_munch_subclass(self): class FakeObjDict(dict): additional = 1 + obj = FakeObjDict(foo='bar') obj_dict = meta.obj_to_munch(obj) self.assertIn('additional', obj_dict) diff --git a/openstack/tests/unit/cloud/test_network.py b/openstack/tests/unit/cloud/test_network.py index 93a3dad24..860efc3a1 100644 --- a/openstack/tests/unit/cloud/test_network.py +++ b/openstack/tests/unit/cloud/test_network.py @@ -57,7 +57,7 @@ class TestNetwork(base.TestCase): "updated": "2015-01-01T10:00:00-00:00", "description": "Availability zone support for router.", "links": [], - "name": "Network Availability Zone" + "name": "Network Availability Zone", } enabled_neutron_extensions = [network_availability_zone_extension] @@ -65,66 +65,99 @@ class TestNetwork(base.TestCase): def _compare_networks(self, exp, real): self.assertDictEqual( _network.Network(**exp).to_dict(computed=False), - real.to_dict(computed=False)) + real.to_dict(computed=False), + ) def test_list_networks(self): net1 = {'id': '1', 'name': 'net1'} net2 = {'id': '2', 'name': 'net2'} - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': [net1, net2]}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': [net1, net2]}, + ) + ] + ) nets = self.cloud.list_networks() self.assertEqual( - [_network.Network(**i).to_dict(computed=False) for i in [ - net1, net2]], - [i.to_dict(computed=False) for i in nets]) + [ + _network.Network(**i).to_dict(computed=False) + for i in [net1, net2] + ], + [i.to_dict(computed=False) for i in nets], + ) self.assert_calls() def test_list_networks_filtered(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks'], - qs_elements=["name=test"]), - json={'networks': []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks'], + qs_elements=["name=test"], + ), + json={'networks': []}, + ) + ] + ) self.cloud.list_networks(filters={'name': 'test'}) self.assert_calls() def test_create_network(self): - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'network': self.mock_new_network_rep}, - validate=dict( - json={'network': { - 'admin_state_up': True, - 'name': 'netname'}})) - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'network': self.mock_new_network_rep}, + validate=dict( + json={ + 'network': { + 'admin_state_up': True, + 'name': 'netname', + } + } + ), + ) + ] + ) network = self.cloud.create_network("netname") - self._compare_networks( - self.mock_new_network_rep, network) + self._compare_networks(self.mock_new_network_rep, network) self.assert_calls() def test_create_network_specific_tenant(self): project_id = "project_id_value" mock_new_network_rep = copy.copy(self.mock_new_network_rep) mock_new_network_rep['project_id'] = project_id - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'network': mock_new_network_rep}, - validate=dict( - json={'network': { - 'admin_state_up': True, - 'name': 'netname', - 'project_id': project_id}})) - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'network': mock_new_network_rep}, + validate=dict( + json={ + 'network': { + 'admin_state_up': True, + 'name': 'netname', + 'project_id': project_id, + } + } + ), + ) + ] + ) network = self.cloud.create_network("netname", project_id=project_id) self._compare_networks(mock_new_network_rep, network) self.assert_calls() @@ -132,45 +165,57 @@ def test_create_network_specific_tenant(self): def test_create_network_external(self): mock_new_network_rep = copy.copy(self.mock_new_network_rep) mock_new_network_rep['router:external'] = True - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'network': mock_new_network_rep}, - validate=dict( - json={'network': { - 'admin_state_up': True, - 'name': 'netname', - 'router:external': True}})) - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'network': mock_new_network_rep}, + validate=dict( + json={ + 'network': { + 'admin_state_up': True, + 'name': 'netname', + 'router:external': True, + } + } + ), + ) + ] + ) network = self.cloud.create_network("netname", external=True) self._compare_networks(mock_new_network_rep, network) self.assert_calls() def test_create_network_provider(self): - provider_opts = {'physical_network': 'mynet', - 'network_type': 'vlan', - 'segmentation_id': 'vlan1'} + provider_opts = { + 'physical_network': 'mynet', + 'network_type': 'vlan', + 'segmentation_id': 'vlan1', + } new_network_provider_opts = { 'provider:physical_network': 'mynet', 'provider:network_type': 'vlan', - 'provider:segmentation_id': 'vlan1' + 'provider:segmentation_id': 'vlan1', } mock_new_network_rep = copy.copy(self.mock_new_network_rep) mock_new_network_rep.update(new_network_provider_opts) - expected_send_params = { - 'admin_state_up': True, - 'name': 'netname' - } + expected_send_params = {'admin_state_up': True, 'name': 'netname'} expected_send_params.update(new_network_provider_opts) - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'network': mock_new_network_rep}, - validate=dict( - json={'network': expected_send_params})) - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'network': mock_new_network_rep}, + validate=dict(json={'network': expected_send_params}), + ) + ] + ) network = self.cloud.create_network("netname", provider=provider_opts) self._compare_networks(mock_new_network_rep, network) self.assert_calls() @@ -179,89 +224,122 @@ def test_update_network_provider(self): network_id = "test-net-id" network_name = "network" network = {'id': network_id, 'name': network_name} - provider_opts = {'physical_network': 'mynet', - 'network_type': 'vlan', - 'segmentation_id': 'vlan1', - 'should_not_be_passed': 1} + provider_opts = { + 'physical_network': 'mynet', + 'network_type': 'vlan', + 'segmentation_id': 'vlan1', + 'should_not_be_passed': 1, + } update_network_provider_opts = { 'provider:physical_network': 'mynet', 'provider:network_type': 'vlan', - 'provider:segmentation_id': 'vlan1' + 'provider:segmentation_id': 'vlan1', } mock_update_rep = copy.copy(self.mock_new_network_rep) mock_update_rep.update(update_network_provider_opts) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks', network_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks'], - qs_elements=['name=%s' % network_name]), - json={'networks': [network]}), - dict(method='PUT', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks', network_id]), - json={'network': mock_update_rep}, - validate=dict( - json={'network': update_network_provider_opts})) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks', network_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks'], + qs_elements=['name=%s' % network_name], + ), + json={'networks': [network]}, + ), + dict( + method='PUT', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks', network_id], + ), + json={'network': mock_update_rep}, + validate=dict( + json={'network': update_network_provider_opts} + ), + ), + ] + ) network = self.cloud.update_network( - network_name, - provider=provider_opts + network_name, provider=provider_opts ) self._compare_networks(mock_update_rep, network) self.assert_calls() def test_create_network_with_availability_zone_hints(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'network': self.mock_new_network_rep}, - validate=dict( - json={'network': { - 'admin_state_up': True, - 'name': 'netname', - 'availability_zone_hints': ['nova']}})) - ]) - network = self.cloud.create_network("netname", - availability_zone_hints=['nova']) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': self.enabled_neutron_extensions}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'network': self.mock_new_network_rep}, + validate=dict( + json={ + 'network': { + 'admin_state_up': True, + 'name': 'netname', + 'availability_zone_hints': ['nova'], + } + } + ), + ), + ] + ) + network = self.cloud.create_network( + "netname", availability_zone_hints=['nova'] + ) self._compare_networks(self.mock_new_network_rep, network) self.assert_calls() def test_create_network_provider_ignored_value(self): - provider_opts = {'physical_network': 'mynet', - 'network_type': 'vlan', - 'segmentation_id': 'vlan1', - 'should_not_be_passed': 1} + provider_opts = { + 'physical_network': 'mynet', + 'network_type': 'vlan', + 'segmentation_id': 'vlan1', + 'should_not_be_passed': 1, + } new_network_provider_opts = { 'provider:physical_network': 'mynet', 'provider:network_type': 'vlan', - 'provider:segmentation_id': 'vlan1' + 'provider:segmentation_id': 'vlan1', } mock_new_network_rep = copy.copy(self.mock_new_network_rep) mock_new_network_rep.update(new_network_provider_opts) - expected_send_params = { - 'admin_state_up': True, - 'name': 'netname' - } + expected_send_params = {'admin_state_up': True, 'name': 'netname'} expected_send_params.update(new_network_provider_opts) - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'network': mock_new_network_rep}, - validate=dict( - json={'network': expected_send_params})) - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'network': mock_new_network_rep}, + validate=dict(json={'network': expected_send_params}), + ) + ] + ) network = self.cloud.create_network("netname", provider=provider_opts) self._compare_networks(mock_new_network_rep, network) self.assert_calls() @@ -270,16 +348,17 @@ def test_create_network_wrong_availability_zone_hints_type(self): azh_opts = "invalid" with testtools.ExpectedException( openstack.cloud.OpenStackCloudException, - "Parameter 'availability_zone_hints' must be a list" + "Parameter 'availability_zone_hints' must be a list", ): - self.cloud.create_network("netname", - availability_zone_hints=azh_opts) + self.cloud.create_network( + "netname", availability_zone_hints=azh_opts + ) def test_create_network_provider_wrong_type(self): provider_opts = "invalid" with testtools.ExpectedException( openstack.cloud.OpenStackCloudException, - "Parameter 'provider' must be a dict" + "Parameter 'provider' must be a dict", ): self.cloud.create_network("netname", provider=provider_opts) @@ -287,20 +366,28 @@ def test_create_network_port_security_disabled(self): port_security_state = False mock_new_network_rep = copy.copy(self.mock_new_network_rep) mock_new_network_rep['port_security_enabled'] = port_security_state - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'network': mock_new_network_rep}, - validate=dict( - json={'network': { - 'admin_state_up': True, - 'name': 'netname', - 'port_security_enabled': port_security_state}})) - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'network': mock_new_network_rep}, + validate=dict( + json={ + 'network': { + 'admin_state_up': True, + 'name': 'netname', + 'port_security_enabled': port_security_state, + } + } + ), + ) + ] + ) network = self.cloud.create_network( - "netname", - port_security_enabled=port_security_state + "netname", port_security_enabled=port_security_state ) self._compare_networks(mock_new_network_rep, network) self.assert_calls() @@ -309,34 +396,41 @@ def test_create_network_with_mtu(self): mtu_size = 1500 mock_new_network_rep = copy.copy(self.mock_new_network_rep) mock_new_network_rep['mtu'] = mtu_size - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'network': mock_new_network_rep}, - validate=dict( - json={'network': { - 'admin_state_up': True, - 'name': 'netname', - 'mtu': mtu_size}})) - ]) - network = self.cloud.create_network("netname", - mtu_size=mtu_size - ) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'network': mock_new_network_rep}, + validate=dict( + json={ + 'network': { + 'admin_state_up': True, + 'name': 'netname', + 'mtu': mtu_size, + } + } + ), + ) + ] + ) + network = self.cloud.create_network("netname", mtu_size=mtu_size) self._compare_networks(mock_new_network_rep, network) self.assert_calls() def test_create_network_with_wrong_mtu_size(self): with testtools.ExpectedException( - openstack.cloud.OpenStackCloudException, - "Parameter 'mtu_size' must be greater than 67." + openstack.cloud.OpenStackCloudException, + "Parameter 'mtu_size' must be greater than 67.", ): self.cloud.create_network("netname", mtu_size=42) def test_create_network_with_wrong_mtu_type(self): with testtools.ExpectedException( - openstack.cloud.OpenStackCloudException, - "Parameter 'mtu_size' must be an integer." + openstack.cloud.OpenStackCloudException, + "Parameter 'mtu_size' must be an integer.", ): self.cloud.create_network("netname", mtu_size="fourty_two") @@ -344,39 +438,65 @@ def test_delete_network(self): network_id = "test-net-id" network_name = "network" network = {'id': network_id, 'name': network_name} - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks', network_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks'], - qs_elements=['name=%s' % network_name]), - json={'networks': [network]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks', network_id]), - json={}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks', network_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks'], + qs_elements=['name=%s' % network_name], + ), + json={'networks': [network]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks', network_id], + ), + json={}, + ), + ] + ) self.assertTrue(self.cloud.delete_network(network_name)) self.assert_calls() def test_delete_network_not_found(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks', 'test-net']), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks'], - qs_elements=['name=test-net']), - json={'networks': []}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks', 'test-net'], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks'], + qs_elements=['name=test-net'], + ), + json={'networks': []}, + ), + ] + ) self.assertFalse(self.cloud.delete_network('test-net')) self.assert_calls() @@ -384,37 +504,61 @@ def test_delete_network_exception(self): network_id = "test-net-id" network_name = "network" network = {'id': network_id, 'name': network_name} - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks', network_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks'], - qs_elements=['name=%s' % network_name]), - json={'networks': [network]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks', network_id]), - status_code=503) - ]) - self.assertRaises(openstack.cloud.OpenStackCloudException, - self.cloud.delete_network, network_name) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks', network_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks'], + qs_elements=['name=%s' % network_name], + ), + json={'networks': [network]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks', network_id], + ), + status_code=503, + ), + ] + ) + self.assertRaises( + openstack.cloud.OpenStackCloudException, + self.cloud.delete_network, + network_name, + ) self.assert_calls() def test_get_network_by_id(self): network_id = "test-net-id" network_name = "network" network = {'id': network_id, 'name': network_name} - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks', network_id]), - json={'network': network}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks', network_id], + ), + json={'network': network}, + ) + ] + ) self.assertTrue(self.cloud.get_network_by_id(network_id)) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_object.py b/openstack/tests/unit/cloud/test_object.py index 8635c951b..53461dc3c 100644 --- a/openstack/tests/unit/cloud/test_object.py +++ b/openstack/tests/unit/cloud/test_object.py @@ -29,7 +29,6 @@ class BaseTestObject(base.TestCase): - def setUp(self): super(BaseTestObject, self).setUp() @@ -37,98 +36,126 @@ def setUp(self): self.object = self.getUniqueString() self.endpoint = self.cloud._object_store_client.get_endpoint() self.container_endpoint = '{endpoint}/{container}'.format( - endpoint=self.endpoint, container=self.container) + endpoint=self.endpoint, container=self.container + ) self.object_endpoint = '{endpoint}/{object}'.format( - endpoint=self.container_endpoint, object=self.object) + endpoint=self.container_endpoint, object=self.object + ) def _compare_containers(self, exp, real): self.assertDictEqual( - container.Container(**exp).to_dict( - computed=False), - real.to_dict(computed=False)) + container.Container(**exp).to_dict(computed=False), + real.to_dict(computed=False), + ) def _compare_objects(self, exp, real): self.assertDictEqual( - obj.Object(**exp).to_dict( - computed=False), - real.to_dict(computed=False)) + obj.Object(**exp).to_dict(computed=False), + real.to_dict(computed=False), + ) class TestObject(BaseTestObject): - def test_create_container(self): """Test creating a (private) container""" - self.register_uris([ - dict(method='HEAD', uri=self.container_endpoint, status_code=404), - dict(method='PUT', uri=self.container_endpoint, - status_code=201, - headers={ - 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', - 'Content-Length': '0', - 'Content-Type': 'text/html; charset=UTF-8', - }), - dict(method='HEAD', uri=self.container_endpoint, - headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}) - ]) + self.register_uris( + [ + dict( + method='HEAD', uri=self.container_endpoint, status_code=404 + ), + dict( + method='PUT', + uri=self.container_endpoint, + status_code=201, + headers={ + 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=UTF-8', + }, + ), + dict( + method='HEAD', + uri=self.container_endpoint, + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8', + }, + ), + ] + ) self.cloud.create_container(self.container) self.assert_calls() def test_create_container_public(self): """Test creating a public container""" - self.register_uris([ - dict(method='HEAD', uri=self.container_endpoint, - status_code=404), - dict(method='PUT', uri=self.container_endpoint, - status_code=201, - headers={ - 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', - 'Content-Length': '0', - 'Content-Type': 'text/html; charset=UTF-8', - 'x-container-read': - oc_oc.OBJECT_CONTAINER_ACLS[ - 'public'], - }), - dict(method='HEAD', uri=self.container_endpoint, - headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}) - ]) + self.register_uris( + [ + dict( + method='HEAD', uri=self.container_endpoint, status_code=404 + ), + dict( + method='PUT', + uri=self.container_endpoint, + status_code=201, + headers={ + 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=UTF-8', + 'x-container-read': oc_oc.OBJECT_CONTAINER_ACLS[ + 'public' + ], + }, + ), + dict( + method='HEAD', + uri=self.container_endpoint, + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8', + }, + ), + ] + ) self.cloud.create_container(self.container, public=True) self.assert_calls() def test_create_container_exists(self): """Test creating a container that exists.""" - self.register_uris([ - dict(method='HEAD', uri=self.container_endpoint, - headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}) - ]) + self.register_uris( + [ + dict( + method='HEAD', + uri=self.container_endpoint, + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8', + }, + ) + ] + ) container = self.cloud.create_container(self.container) @@ -136,17 +163,24 @@ def test_create_container_exists(self): self.assertIsNotNone(container) def test_delete_container(self): - self.register_uris([ - dict(method='DELETE', uri=self.container_endpoint)]) + self.register_uris( + [dict(method='DELETE', uri=self.container_endpoint)] + ) self.assertTrue(self.cloud.delete_container(self.container)) self.assert_calls() def test_delete_container_404(self): """No exception when deleting a container that does not exist""" - self.register_uris([ - dict(method='DELETE', uri=self.container_endpoint, - status_code=404)]) + self.register_uris( + [ + dict( + method='DELETE', + uri=self.container_endpoint, + status_code=404, + ) + ] + ) self.assertFalse(self.cloud.delete_container(self.container)) self.assert_calls() @@ -154,22 +188,34 @@ def test_delete_container_404(self): def test_delete_container_error(self): """Non-404 swift error re-raised as OSCE""" # 409 happens if the container is not empty - self.register_uris([ - dict(method='DELETE', uri=self.container_endpoint, - status_code=409)]) + self.register_uris( + [ + dict( + method='DELETE', + uri=self.container_endpoint, + status_code=409, + ) + ] + ) self.assertRaises( openstack.cloud.OpenStackCloudException, - self.cloud.delete_container, self.container) + self.cloud.delete_container, + self.container, + ) self.assert_calls() def test_update_container(self): - headers = { - 'x-container-read': - oc_oc.OBJECT_CONTAINER_ACLS['public']} - self.register_uris([ - dict(method='POST', uri=self.container_endpoint, - status_code=204, - validate=dict(headers=headers))]) + headers = {'x-container-read': oc_oc.OBJECT_CONTAINER_ACLS['public']} + self.register_uris( + [ + dict( + method='POST', + uri=self.container_endpoint, + status_code=204, + validate=dict(headers=headers), + ) + ] + ) self.cloud.update_container(self.container, headers) self.assert_calls() @@ -181,37 +227,56 @@ def test_update_container_error(self): # method, and I cannot make a synthetic failure to validate a real # error code. So we're really just testing the shade adapter error # raising logic here, rather than anything specific to swift. - self.register_uris([ - dict(method='POST', uri=self.container_endpoint, - status_code=409)]) + self.register_uris( + [dict(method='POST', uri=self.container_endpoint, status_code=409)] + ) self.assertRaises( openstack.cloud.OpenStackCloudException, - self.cloud.update_container, self.container, dict(foo='bar')) + self.cloud.update_container, + self.container, + dict(foo='bar'), + ) self.assert_calls() def test_set_container_access_public(self): - self.register_uris([ - dict(method='POST', uri=self.container_endpoint, - status_code=204, - validate=dict( - headers={ - 'x-container-read': - oc_oc.OBJECT_CONTAINER_ACLS[ - 'public']}))]) + self.register_uris( + [ + dict( + method='POST', + uri=self.container_endpoint, + status_code=204, + validate=dict( + headers={ + 'x-container-read': oc_oc.OBJECT_CONTAINER_ACLS[ + 'public' + ] + } + ), + ) + ] + ) self.cloud.set_container_access(self.container, 'public') self.assert_calls() def test_set_container_access_private(self): - self.register_uris([ - dict(method='POST', uri=self.container_endpoint, - status_code=204, - validate=dict( - headers={ - 'x-container-read': - oc_oc.OBJECT_CONTAINER_ACLS[ - 'private']}))]) + self.register_uris( + [ + dict( + method='POST', + uri=self.container_endpoint, + status_code=204, + validate=dict( + headers={ + 'x-container-read': oc_oc.OBJECT_CONTAINER_ACLS[ + 'private' + ] + } + ), + ) + ] + ) self.cloud.set_container_access(self.container, 'private') @@ -220,47 +285,69 @@ def test_set_container_access_private(self): def test_set_container_access_invalid(self): self.assertRaises( openstack.cloud.OpenStackCloudException, - self.cloud.set_container_access, self.container, 'invalid') + self.cloud.set_container_access, + self.container, + 'invalid', + ) def test_get_container_access(self): - self.register_uris([ - dict(method='HEAD', uri=self.container_endpoint, - headers={ - 'x-container-read': - str(oc_oc.OBJECT_CONTAINER_ACLS[ - 'public'])})]) + self.register_uris( + [ + dict( + method='HEAD', + uri=self.container_endpoint, + headers={ + 'x-container-read': str( + oc_oc.OBJECT_CONTAINER_ACLS['public'] + ) + }, + ) + ] + ) access = self.cloud.get_container_access(self.container) self.assertEqual('public', access) def test_get_container_invalid(self): - self.register_uris([ - dict(method='HEAD', uri=self.container_endpoint, - headers={'x-container-read': 'invalid'})]) + self.register_uris( + [ + dict( + method='HEAD', + uri=self.container_endpoint, + headers={'x-container-read': 'invalid'}, + ) + ] + ) with testtools.ExpectedException( - exc.OpenStackCloudException, - "Could not determine container access for ACL: invalid" + exc.OpenStackCloudException, + "Could not determine container access for ACL: invalid", ): self.cloud.get_container_access(self.container) def test_get_container_access_not_found(self): - self.register_uris([ - dict(method='HEAD', uri=self.container_endpoint, - status_code=404)]) + self.register_uris( + [dict(method='HEAD', uri=self.container_endpoint, status_code=404)] + ) with testtools.ExpectedException( - exc.OpenStackCloudException, - "Container not found: %s" % self.container + exc.OpenStackCloudException, + "Container not found: %s" % self.container, ): self.cloud.get_container_access(self.container) def test_list_containers(self): - endpoint = '{endpoint}/'.format( - endpoint=self.endpoint) - containers = [ - {u'count': 0, u'bytes': 0, u'name': self.container}] - - self.register_uris([dict(method='GET', uri=endpoint, complete_qs=True, - json=containers)]) + endpoint = '{endpoint}/'.format(endpoint=self.endpoint) + containers = [{u'count': 0, u'bytes': 0, u'name': self.container}] + + self.register_uris( + [ + dict( + method='GET', + uri=endpoint, + complete_qs=True, + json=containers, + ) + ] + ) ret = self.cloud.list_containers() @@ -269,13 +356,21 @@ def test_list_containers(self): self._compare_containers(a, b) def test_list_containers_exception(self): - endpoint = '{endpoint}/'.format( - endpoint=self.endpoint) - self.register_uris([dict(method='GET', uri=endpoint, complete_qs=True, - status_code=416)]) + endpoint = '{endpoint}/'.format(endpoint=self.endpoint) + self.register_uris( + [ + dict( + method='GET', + uri=endpoint, + complete_qs=True, + status_code=416, + ) + ] + ) self.assertRaises( - exc.OpenStackCloudException, self.cloud.list_containers) + exc.OpenStackCloudException, self.cloud.list_containers + ) self.assert_calls() @mock.patch('time.time', autospec=True) @@ -283,20 +378,26 @@ def test_generate_form_signature_container_key(self, mock_time): mock_time.return_value = 12345 - self.register_uris([ - dict(method='HEAD', uri=self.container_endpoint, - headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'X-Container-Meta-Temp-Url-Key': 'amazingly-secure-key', - 'Content-Type': 'text/plain; charset=utf-8'}) - ]) + self.register_uris( + [ + dict( + method='HEAD', + uri=self.container_endpoint, + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'X-Container-Meta-Temp-Url-Key': 'amazingly-secure-key', # noqa: E501 + 'Content-Type': 'text/plain; charset=utf-8', + }, + ) + ] + ) self.assertEqual( (13345, '60731fb66d46c97cdcb79b6154363179c500b9d9'), self.cloud.object_store.generate_form_signature( @@ -304,7 +405,11 @@ def test_generate_form_signature_container_key(self, mock_time): object_prefix='prefix/location', redirect_url='https://example.com/location', max_file_size=1024 * 1024 * 1024, - max_upload_count=10, timeout=1000, temp_url_key=None)) + max_upload_count=10, + timeout=1000, + temp_url_key=None, + ), + ) self.assert_calls() @mock.patch('time.time', autospec=True) @@ -312,22 +417,32 @@ def test_generate_form_signature_account_key(self, mock_time): mock_time.return_value = 12345 - self.register_uris([ - dict(method='HEAD', uri=self.container_endpoint, - headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}), - dict(method='HEAD', uri=self.endpoint + '/', - headers={ - 'X-Account-Meta-Temp-Url-Key': 'amazingly-secure-key'}), - ]) + self.register_uris( + [ + dict( + method='HEAD', + uri=self.container_endpoint, + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8', + }, + ), + dict( + method='HEAD', + uri=self.endpoint + '/', + headers={ + 'X-Account-Meta-Temp-Url-Key': 'amazingly-secure-key' + }, + ), + ] + ) self.assertEqual( (13345, '3cb9bc83d5a4136421bb2c1f58b963740566646f'), self.cloud.object_store.generate_form_signature( @@ -335,7 +450,11 @@ def test_generate_form_signature_account_key(self, mock_time): object_prefix='prefix/location', redirect_url='https://example.com/location', max_file_size=1024 * 1024 * 1024, - max_upload_count=10, timeout=1000, temp_url_key=None)) + max_upload_count=10, + timeout=1000, + temp_url_key=None, + ), + ) self.assert_calls() @mock.patch('time.time') @@ -350,27 +469,35 @@ def test_generate_form_signature_key_argument(self, mock_time): object_prefix='prefix/location', redirect_url='https://example.com/location', max_file_size=1024 * 1024 * 1024, - max_upload_count=10, timeout=1000, - temp_url_key='amazingly-secure-key')) + max_upload_count=10, + timeout=1000, + temp_url_key='amazingly-secure-key', + ), + ) self.assert_calls() def test_generate_form_signature_no_key(self): - self.register_uris([ - dict(method='HEAD', uri=self.container_endpoint, - headers={ - 'Content-Length': '0', - 'X-Container-Object-Count': '0', - 'Accept-Ranges': 'bytes', - 'X-Storage-Policy': 'Policy-0', - 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', - 'X-Timestamp': '1481912480.41664', - 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', - 'X-Container-Bytes-Used': '0', - 'Content-Type': 'text/plain; charset=utf-8'}), - dict(method='HEAD', uri=self.endpoint + '/', - headers={}), - ]) + self.register_uris( + [ + dict( + method='HEAD', + uri=self.container_endpoint, + headers={ + 'Content-Length': '0', + 'X-Container-Object-Count': '0', + 'Accept-Ranges': 'bytes', + 'X-Storage-Policy': 'Policy-0', + 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', + 'X-Timestamp': '1481912480.41664', + 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', + 'X-Container-Bytes-Used': '0', + 'Content-Type': 'text/plain; charset=utf-8', + }, + ), + dict(method='HEAD', uri=self.endpoint + '/', headers={}), + ] + ) self.assertRaises( exceptions.SDKException, self.cloud.object_store.generate_form_signature, @@ -378,23 +505,33 @@ def test_generate_form_signature_no_key(self): object_prefix='prefix/location', redirect_url='https://example.com/location', max_file_size=1024 * 1024 * 1024, - max_upload_count=10, timeout=1000, temp_url_key=None) + max_upload_count=10, + timeout=1000, + temp_url_key=None, + ) self.assert_calls() def test_set_account_temp_url_key(self): key = 'super-secure-key' - self.register_uris([ - dict(method='POST', uri=self.endpoint + '/', - status_code=204, - validate=dict( - headers={ - 'x-account-meta-temp-url-key': key})), - dict(method='HEAD', uri=self.endpoint + '/', - headers={ - 'x-account-meta-temp-url-key': key}), - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.endpoint + '/', + status_code=204, + validate=dict( + headers={'x-account-meta-temp-url-key': key} + ), + ), + dict( + method='HEAD', + uri=self.endpoint + '/', + headers={'x-account-meta-temp-url-key': key}, + ), + ] + ) self.cloud.object_store.set_account_temp_url_key(key) self.assert_calls() @@ -402,16 +539,23 @@ def test_set_account_temp_url_key_secondary(self): key = 'super-secure-key' - self.register_uris([ - dict(method='POST', uri=self.endpoint + '/', - status_code=204, - validate=dict( - headers={ - 'x-account-meta-temp-url-key-2': key})), - dict(method='HEAD', uri=self.endpoint + '/', - headers={ - 'x-account-meta-temp-url-key-2': key}), - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.endpoint + '/', + status_code=204, + validate=dict( + headers={'x-account-meta-temp-url-key-2': key} + ), + ), + dict( + method='HEAD', + uri=self.endpoint + '/', + headers={'x-account-meta-temp-url-key-2': key}, + ), + ] + ) self.cloud.object_store.set_account_temp_url_key(key, secondary=True) self.assert_calls() @@ -419,16 +563,23 @@ def test_set_container_temp_url_key(self): key = 'super-secure-key' - self.register_uris([ - dict(method='POST', uri=self.container_endpoint, - status_code=204, - validate=dict( - headers={ - 'x-container-meta-temp-url-key': key})), - dict(method='HEAD', uri=self.container_endpoint, - headers={ - 'x-container-meta-temp-url-key': key}), - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.container_endpoint, + status_code=204, + validate=dict( + headers={'x-container-meta-temp-url-key': key} + ), + ), + dict( + method='HEAD', + uri=self.container_endpoint, + headers={'x-container-meta-temp-url-key': key}, + ), + ] + ) self.cloud.object_store.set_container_temp_url_key(self.container, key) self.assert_calls() @@ -436,33 +587,46 @@ def test_set_container_temp_url_key_secondary(self): key = 'super-secure-key' - self.register_uris([ - dict(method='POST', uri=self.container_endpoint, - status_code=204, - validate=dict( - headers={ - 'x-container-meta-temp-url-key-2': key})), - dict(method='HEAD', uri=self.container_endpoint, - headers={ - 'x-container-meta-temp-url-key-2': key}), - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.container_endpoint, + status_code=204, + validate=dict( + headers={'x-container-meta-temp-url-key-2': key} + ), + ), + dict( + method='HEAD', + uri=self.container_endpoint, + headers={'x-container-meta-temp-url-key-2': key}, + ), + ] + ) self.cloud.object_store.set_container_temp_url_key( - self.container, key, secondary=True) + self.container, key, secondary=True + ) self.assert_calls() def test_list_objects(self): endpoint = '{endpoint}?format=json'.format( - endpoint=self.container_endpoint) + endpoint=self.container_endpoint + ) - objects = [{ - u'bytes': 20304400896, - u'last_modified': u'2016-12-15T13:34:13.650090', - u'hash': u'daaf9ed2106d09bba96cf193d866445e', - u'name': self.object, - u'content_type': u'application/octet-stream'}] + objects = [ + { + u'bytes': 20304400896, + u'last_modified': u'2016-12-15T13:34:13.650090', + u'hash': u'daaf9ed2106d09bba96cf193d866445e', + u'name': self.object, + u'content_type': u'application/octet-stream', + } + ] - self.register_uris([dict(method='GET', uri=endpoint, complete_qs=True, - json=objects)]) + self.register_uris( + [dict(method='GET', uri=endpoint, complete_qs=True, json=objects)] + ) ret = self.cloud.list_objects(self.container) @@ -472,17 +636,22 @@ def test_list_objects(self): def test_list_objects_with_prefix(self): endpoint = '{endpoint}?format=json&prefix=test'.format( - endpoint=self.container_endpoint) + endpoint=self.container_endpoint + ) - objects = [{ - u'bytes': 20304400896, - u'last_modified': u'2016-12-15T13:34:13.650090', - u'hash': u'daaf9ed2106d09bba96cf193d866445e', - u'name': self.object, - u'content_type': u'application/octet-stream'}] + objects = [ + { + u'bytes': 20304400896, + u'last_modified': u'2016-12-15T13:34:13.650090', + u'hash': u'daaf9ed2106d09bba96cf193d866445e', + u'name': self.object, + u'content_type': u'application/octet-stream', + } + ] - self.register_uris([dict(method='GET', uri=endpoint, complete_qs=True, - json=objects)]) + self.register_uris( + [dict(method='GET', uri=endpoint, complete_qs=True, json=objects)] + ) ret = self.cloud.list_objects(self.container, prefix='test') @@ -492,27 +661,47 @@ def test_list_objects_with_prefix(self): def test_list_objects_exception(self): endpoint = '{endpoint}?format=json'.format( - endpoint=self.container_endpoint) - self.register_uris([dict(method='GET', uri=endpoint, complete_qs=True, - status_code=416)]) + endpoint=self.container_endpoint + ) + self.register_uris( + [ + dict( + method='GET', + uri=endpoint, + complete_qs=True, + status_code=416, + ) + ] + ) self.assertRaises( exc.OpenStackCloudException, - self.cloud.list_objects, self.container) + self.cloud.list_objects, + self.container, + ) self.assert_calls() def test_delete_object(self): - self.register_uris([ - dict(method='HEAD', uri=self.object_endpoint, - headers={'X-Object-Meta': 'foo'}), - dict(method='DELETE', uri=self.object_endpoint, status_code=204)]) + self.register_uris( + [ + dict( + method='HEAD', + uri=self.object_endpoint, + headers={'X-Object-Meta': 'foo'}, + ), + dict( + method='DELETE', uri=self.object_endpoint, status_code=204 + ), + ] + ) self.assertTrue(self.cloud.delete_object(self.container, self.object)) self.assert_calls() def test_delete_object_not_found(self): - self.register_uris([dict(method='HEAD', uri=self.object_endpoint, - status_code=404)]) + self.register_uris( + [dict(method='HEAD', uri=self.object_endpoint, status_code=404)] + ) self.assertFalse(self.cloud.delete_object(self.container, self.object)) @@ -533,21 +722,27 @@ def test_get_object(self): } response_headers = {k.lower(): v for k, v in headers.items()} text = 'test body' - self.register_uris([ - dict(method='GET', uri=self.object_endpoint, - headers={ - 'Content-Length': '20304400896', - 'Content-Type': 'application/octet-stream', - 'Accept-Ranges': 'bytes', - 'Last-Modified': 'Thu, 15 Dec 2016 13:34:14 GMT', - 'Etag': '"b5c454b44fbd5344793e3fb7e3850768"', - 'X-Timestamp': '1481808853.65009', - 'X-Trans-Id': 'tx68c2a2278f0c469bb6de1-005857ed80dfw1', - 'Date': 'Mon, 19 Dec 2016 14:24:00 GMT', - 'X-Static-Large-Object': 'True', - 'X-Object-Meta-Mtime': '1481513709.168512', - }, - text='test body')]) + self.register_uris( + [ + dict( + method='GET', + uri=self.object_endpoint, + headers={ + 'Content-Length': '20304400896', + 'Content-Type': 'application/octet-stream', + 'Accept-Ranges': 'bytes', + 'Last-Modified': 'Thu, 15 Dec 2016 13:34:14 GMT', + 'Etag': '"b5c454b44fbd5344793e3fb7e3850768"', + 'X-Timestamp': '1481808853.65009', + 'X-Trans-Id': 'tx68c2a2278f0c469bb6de1-005857ed80dfw1', + 'Date': 'Mon, 19 Dec 2016 14:24:00 GMT', + 'X-Static-Large-Object': 'True', + 'X-Object-Meta-Mtime': '1481513709.168512', + }, + text='test body', + ) + ] + ) resp = self.cloud.get_object(self.container, self.object) @@ -557,21 +752,27 @@ def test_get_object(self): def test_stream_object(self): text = b'test body' - self.register_uris([ - dict(method='GET', uri=self.object_endpoint, - headers={ - 'Content-Length': '20304400896', - 'Content-Type': 'application/octet-stream', - 'Accept-Ranges': 'bytes', - 'Last-Modified': 'Thu, 15 Dec 2016 13:34:14 GMT', - 'Etag': '"b5c454b44fbd5344793e3fb7e3850768"', - 'X-Timestamp': '1481808853.65009', - 'X-Trans-Id': 'tx68c2a2278f0c469bb6de1-005857ed80dfw1', - 'Date': 'Mon, 19 Dec 2016 14:24:00 GMT', - 'X-Static-Large-Object': 'True', - 'X-Object-Meta-Mtime': '1481513709.168512', - }, - text='test body')]) + self.register_uris( + [ + dict( + method='GET', + uri=self.object_endpoint, + headers={ + 'Content-Length': '20304400896', + 'Content-Type': 'application/octet-stream', + 'Accept-Ranges': 'bytes', + 'Last-Modified': 'Thu, 15 Dec 2016 13:34:14 GMT', + 'Etag': '"b5c454b44fbd5344793e3fb7e3850768"', + 'X-Timestamp': '1481808853.65009', + 'X-Trans-Id': 'tx68c2a2278f0c469bb6de1-005857ed80dfw1', + 'Date': 'Mon, 19 Dec 2016 14:24:00 GMT', + 'X-Static-Large-Object': 'True', + 'X-Object-Meta-Mtime': '1481513709.168512', + }, + text='test body', + ) + ] + ) response_text = b'' for data in self.cloud.stream_object(self.container, self.object): @@ -582,9 +783,11 @@ def test_stream_object(self): self.assertEqual(text, response_text) def test_stream_object_not_found(self): - self.register_uris([ - dict(method='GET', uri=self.object_endpoint, status_code=404), - ]) + self.register_uris( + [ + dict(method='GET', uri=self.object_endpoint, status_code=404), + ] + ) response_text = b'' for data in self.cloud.stream_object(self.container, self.object): @@ -595,21 +798,25 @@ def test_stream_object_not_found(self): self.assertEqual(b'', response_text) def test_get_object_not_found(self): - self.register_uris([dict(method='GET', - uri=self.object_endpoint, status_code=404)]) + self.register_uris( + [dict(method='GET', uri=self.object_endpoint, status_code=404)] + ) self.assertIsNone(self.cloud.get_object(self.container, self.object)) self.assert_calls() def test_get_object_exception(self): - self.register_uris([dict(method='GET', uri=self.object_endpoint, - status_code=416)]) + self.register_uris( + [dict(method='GET', uri=self.object_endpoint, status_code=416)] + ) self.assertRaises( openstack.cloud.OpenStackCloudException, self.cloud.get_object, - self.container, self.object) + self.container, + self.object, + ) self.assert_calls() @@ -617,49 +824,78 @@ def test_get_object_segment_size_below_min(self): # Register directly becuase we make multiple calls. The number # of calls we make isn't interesting - what we do with the return # values is. Don't run assert_calls for the same reason. - self.register_uris([ - dict(method='GET', uri='https://object-store.example.com/info', - json=dict( - swift={'max_file_size': 1000}, - slo={'min_segment_size': 500}), - headers={'Content-Type': 'application/json'})]) + self.register_uris( + [ + dict( + method='GET', + uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': 1000}, + slo={'min_segment_size': 500}, + ), + headers={'Content-Type': 'application/json'}, + ) + ] + ) self.assertEqual(500, self.cloud.get_object_segment_size(400)) self.assertEqual(900, self.cloud.get_object_segment_size(900)) self.assertEqual(1000, self.cloud.get_object_segment_size(1000)) self.assertEqual(1000, self.cloud.get_object_segment_size(1100)) def test_get_object_segment_size_http_404(self): - self.register_uris([ - dict(method='GET', uri='https://object-store.example.com/info', - status_code=404, reason='Not Found')]) - self.assertEqual(_proxy.DEFAULT_OBJECT_SEGMENT_SIZE, - self.cloud.get_object_segment_size(None)) + self.register_uris( + [ + dict( + method='GET', + uri='https://object-store.example.com/info', + status_code=404, + reason='Not Found', + ) + ] + ) + self.assertEqual( + _proxy.DEFAULT_OBJECT_SEGMENT_SIZE, + self.cloud.get_object_segment_size(None), + ) self.assert_calls() def test_get_object_segment_size_http_412(self): - self.register_uris([ - dict(method='GET', uri='https://object-store.example.com/info', - status_code=412, reason='Precondition failed')]) + self.register_uris( + [ + dict( + method='GET', + uri='https://object-store.example.com/info', + status_code=412, + reason='Precondition failed', + ) + ] + ) self.assertEqual( _proxy.DEFAULT_OBJECT_SEGMENT_SIZE, - self.cloud.get_object_segment_size(None)) + self.cloud.get_object_segment_size(None), + ) self.assert_calls() def test_update_container_cors(self): headers = { 'X-Container-Meta-Web-Index': 'index.html', - 'X-Container-Meta-Access-Control-Allow-Origin': '*' + 'X-Container-Meta-Access-Control-Allow-Origin': '*', } - self.register_uris([ - dict(method='POST', uri=self.container_endpoint, - status_code=204, - validate=dict(headers=headers))]) + self.register_uris( + [ + dict( + method='POST', + uri=self.container_endpoint, + status_code=204, + validate=dict(headers=headers), + ) + ] + ) self.cloud.update_container(self.container, headers=headers) self.assert_calls() class TestObjectUploads(BaseTestObject): - def setUp(self): super(TestObjectUploads, self).setUp() @@ -667,83 +903,112 @@ def setUp(self): self.object_file = tempfile.NamedTemporaryFile(delete=False) self.object_file.write(self.content) self.object_file.close() - (self.md5, self.sha256) = utils._get_file_hashes( - self.object_file.name) + (self.md5, self.sha256) = utils._get_file_hashes(self.object_file.name) self.endpoint = self.cloud._object_store_client.get_endpoint() def test_create_object(self): - self.register_uris([ - dict(method='GET', - uri='https://object-store.example.com/info', - json=dict( - swift={'max_file_size': 1000}, - slo={'min_segment_size': 500})), - dict(method='HEAD', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, container=self.container, - object=self.object), - status_code=404), - dict(method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=201, - validate=dict( - headers={ - 'x-object-meta-x-sdk-md5': self.md5, - 'x-object-meta-x-sdk-sha256': self.sha256, - })) - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': 1000}, + slo={'min_segment_size': 500}, + ), + ), + dict( + method='HEAD', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + status_code=404, + ), + dict( + method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + status_code=201, + validate=dict( + headers={ + 'x-object-meta-x-sdk-md5': self.md5, + 'x-object-meta-x-sdk-sha256': self.sha256, + } + ), + ), + ] + ) self.cloud.create_object( - container=self.container, name=self.object, - filename=self.object_file.name) + container=self.container, + name=self.object, + filename=self.object_file.name, + ) self.assert_calls() def test_create_object_index_rax(self): - self.register_uris([ - dict(method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object='index.html'), - status_code=201, - validate=dict( - headers={ - 'access-control-allow-origin': '*', - 'content-type': 'text/html' - })) - ]) + self.register_uris( + [ + dict( + method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, + object='index.html', + ), + status_code=201, + validate=dict( + headers={ + 'access-control-allow-origin': '*', + 'content-type': 'text/html', + } + ), + ) + ] + ) headers = { 'access-control-allow-origin': '*', - 'content-type': 'text/html' + 'content-type': 'text/html', } self.cloud.create_object( - self.container, name='index.html', - data='', - **headers) + self.container, name='index.html', data='', **headers + ) self.assert_calls() def test_create_directory_marker_object(self): - self.register_uris([ - dict(method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=201, - validate=dict( - headers={ - 'content-type': 'application/directory', - })) - ]) + self.register_uris( + [ + dict( + method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + status_code=201, + validate=dict( + headers={ + 'content-type': 'application/directory', + } + ), + ) + ] + ) self.cloud.create_directory_marker_object( - container=self.container, name=self.object) + container=self.container, name=self.object + ) self.assert_calls() @@ -753,55 +1018,80 @@ def test_create_dynamic_large_object(self): min_file_size = 1 uris_to_mock = [ - dict(method='GET', - uri='https://object-store.example.com/info', - json=dict( - swift={'max_file_size': max_file_size}, - slo={'min_segment_size': min_file_size})), - dict(method='HEAD', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, container=self.container, - object=self.object), - status_code=404) + dict( + method='GET', + uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': max_file_size}, + slo={'min_segment_size': min_file_size}, + ), + ), + dict( + method='HEAD', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + status_code=404, + ), ] uris_to_mock.extend( - [dict(method='PUT', - uri='{endpoint}/{container}/{object}/{index:0>6}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - index=index), - status_code=201) + [ + dict( + method='PUT', + uri='{endpoint}/{container}/{object}/{index:0>6}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + index=index, + ), + status_code=201, + ) for index, offset in enumerate( - range(0, len(self.content), max_file_size))] + range(0, len(self.content), max_file_size) + ) + ] ) uris_to_mock.append( - dict(method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=201, - validate=dict( - headers={ - 'x-object-manifest': '{container}/{object}'.format( - container=self.container, object=self.object), - 'x-object-meta-x-sdk-md5': self.md5, - 'x-object-meta-x-sdk-sha256': self.sha256, - }))) + dict( + method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + status_code=201, + validate=dict( + headers={ + 'x-object-manifest': '{container}/{object}'.format( + container=self.container, object=self.object + ), + 'x-object-meta-x-sdk-md5': self.md5, + 'x-object-meta-x-sdk-sha256': self.sha256, + } + ), + ) + ) self.register_uris(uris_to_mock) self.cloud.create_object( - container=self.container, name=self.object, - filename=self.object_file.name, use_slo=False) + container=self.container, + name=self.object, + filename=self.object_file.name, + use_slo=False, + ) # After call 3, order become indeterminate because of thread pool self.assert_calls(stop_after=3) for key, value in self.calls[-1]['headers'].items(): self.assertEqual( - value, self.adapter.request_history[-1].headers[key], - 'header mismatch in manifest call') + value, + self.adapter.request_history[-1].headers[key], + 'header mismatch in manifest call', + ) def test_create_static_large_object(self): @@ -809,88 +1099,118 @@ def test_create_static_large_object(self): min_file_size = 1 uris_to_mock = [ - dict(method='GET', uri='https://object-store.example.com/info', - json=dict( - swift={'max_file_size': max_file_size}, - slo={'min_segment_size': min_file_size})), - dict(method='HEAD', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=404) + dict( + method='GET', + uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': max_file_size}, + slo={'min_segment_size': min_file_size}, + ), + ), + dict( + method='HEAD', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + status_code=404, + ), ] - uris_to_mock.extend([ - dict(method='PUT', - uri='{endpoint}/{container}/{object}/{index:0>6}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - index=index), - status_code=201, - headers=dict(Etag='etag{index}'.format(index=index))) - for index, offset in enumerate( - range(0, len(self.content), max_file_size)) - ]) + uris_to_mock.extend( + [ + dict( + method='PUT', + uri='{endpoint}/{container}/{object}/{index:0>6}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + index=index, + ), + status_code=201, + headers=dict(Etag='etag{index}'.format(index=index)), + ) + for index, offset in enumerate( + range(0, len(self.content), max_file_size) + ) + ] + ) uris_to_mock.append( - dict(method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=201, - validate=dict( - params={ - 'multipart-manifest', 'put' - }, - headers={ - 'x-object-meta-x-sdk-md5': self.md5, - 'x-object-meta-x-sdk-sha256': self.sha256, - }))) + dict( + method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + status_code=201, + validate=dict( + params={'multipart-manifest', 'put'}, + headers={ + 'x-object-meta-x-sdk-md5': self.md5, + 'x-object-meta-x-sdk-sha256': self.sha256, + }, + ), + ) + ) self.register_uris(uris_to_mock) self.cloud.create_object( - container=self.container, name=self.object, - filename=self.object_file.name, use_slo=True) + container=self.container, + name=self.object, + filename=self.object_file.name, + use_slo=True, + ) # After call 3, order become indeterminate because of thread pool self.assert_calls(stop_after=3) for key, value in self.calls[-1]['headers'].items(): self.assertEqual( - value, self.adapter.request_history[-1].headers[key], - 'header mismatch in manifest call') + value, + self.adapter.request_history[-1].headers[key], + 'header mismatch in manifest call', + ) base_object = '/{container}/{object}'.format( - container=self.container, - object=self.object) + container=self.container, object=self.object + ) - self.assertEqual([ - { - 'path': "{base_object}/000000".format( - base_object=base_object), - 'size_bytes': 25, - 'etag': 'etag0', - }, - { - 'path': "{base_object}/000001".format( - base_object=base_object), - 'size_bytes': 25, - 'etag': 'etag1', - }, - { - 'path': "{base_object}/000002".format( - base_object=base_object), - 'size_bytes': 25, - 'etag': 'etag2', - }, - { - 'path': "{base_object}/000003".format( - base_object=base_object), - 'size_bytes': len(self.object) - 75, - 'etag': 'etag3', - }, - ], self.adapter.request_history[-1].json()) + self.assertEqual( + [ + { + 'path': "{base_object}/000000".format( + base_object=base_object + ), + 'size_bytes': 25, + 'etag': 'etag0', + }, + { + 'path': "{base_object}/000001".format( + base_object=base_object + ), + 'size_bytes': 25, + 'etag': 'etag1', + }, + { + 'path': "{base_object}/000002".format( + base_object=base_object + ), + 'size_bytes': 25, + 'etag': 'etag2', + }, + { + 'path': "{base_object}/000003".format( + base_object=base_object + ), + 'size_bytes': len(self.object) - 75, + 'etag': 'etag3', + }, + ], + self.adapter.request_history[-1].json(), + ) def test_slo_manifest_retry(self): """ @@ -901,117 +1221,154 @@ def test_slo_manifest_retry(self): min_file_size = 1 uris_to_mock = [ - dict(method='GET', uri='https://object-store.example.com/info', - json=dict( - swift={'max_file_size': max_file_size}, - slo={'min_segment_size': min_file_size})), - dict(method='HEAD', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=404) + dict( + method='GET', + uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': max_file_size}, + slo={'min_segment_size': min_file_size}, + ), + ), + dict( + method='HEAD', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + status_code=404, + ), ] - uris_to_mock.extend([ - dict(method='PUT', - uri='{endpoint}/{container}/{object}/{index:0>6}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - index=index), - status_code=201, - headers=dict(Etag='etag{index}'.format(index=index))) - for index, offset in enumerate( - range(0, len(self.content), max_file_size)) - ]) + uris_to_mock.extend( + [ + dict( + method='PUT', + uri='{endpoint}/{container}/{object}/{index:0>6}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + index=index, + ), + status_code=201, + headers=dict(Etag='etag{index}'.format(index=index)), + ) + for index, offset in enumerate( + range(0, len(self.content), max_file_size) + ) + ] + ) # manifest file upload calls - uris_to_mock.extend([ - dict(method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=400, - validate=dict( - params={ - 'multipart-manifest', 'put' - }, - headers={ - 'x-object-meta-x-sdk-md5': self.md5, - 'x-object-meta-x-sdk-sha256': self.sha256, - })), - dict(method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=400, - validate=dict( - params={ - 'multipart-manifest', 'put' - }, - headers={ - 'x-object-meta-x-sdk-md5': self.md5, - 'x-object-meta-x-sdk-sha256': self.sha256, - })), - dict(method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=201, - validate=dict( - params={ - 'multipart-manifest', 'put' - }, - headers={ - 'x-object-meta-x-sdk-md5': self.md5, - 'x-object-meta-x-sdk-sha256': self.sha256, - })), - ]) + uris_to_mock.extend( + [ + dict( + method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + status_code=400, + validate=dict( + params={'multipart-manifest', 'put'}, + headers={ + 'x-object-meta-x-sdk-md5': self.md5, + 'x-object-meta-x-sdk-sha256': self.sha256, + }, + ), + ), + dict( + method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + status_code=400, + validate=dict( + params={'multipart-manifest', 'put'}, + headers={ + 'x-object-meta-x-sdk-md5': self.md5, + 'x-object-meta-x-sdk-sha256': self.sha256, + }, + ), + ), + dict( + method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + status_code=201, + validate=dict( + params={'multipart-manifest', 'put'}, + headers={ + 'x-object-meta-x-sdk-md5': self.md5, + 'x-object-meta-x-sdk-sha256': self.sha256, + }, + ), + ), + ] + ) self.register_uris(uris_to_mock) self.cloud.create_object( - container=self.container, name=self.object, - filename=self.object_file.name, use_slo=True) + container=self.container, + name=self.object, + filename=self.object_file.name, + use_slo=True, + ) # After call 3, order become indeterminate because of thread pool self.assert_calls(stop_after=3) for key, value in self.calls[-1]['headers'].items(): self.assertEqual( - value, self.adapter.request_history[-1].headers[key], - 'header mismatch in manifest call') + value, + self.adapter.request_history[-1].headers[key], + 'header mismatch in manifest call', + ) base_object = '/{container}/{object}'.format( - container=self.container, - object=self.object) + container=self.container, object=self.object + ) - self.assertEqual([ - { - 'path': "{base_object}/000000".format( - base_object=base_object), - 'size_bytes': 25, - 'etag': 'etag0', - }, - { - 'path': "{base_object}/000001".format( - base_object=base_object), - 'size_bytes': 25, - 'etag': 'etag1', - }, - { - 'path': "{base_object}/000002".format( - base_object=base_object), - 'size_bytes': 25, - 'etag': 'etag2', - }, - { - 'path': "{base_object}/000003".format( - base_object=base_object), - 'size_bytes': len(self.object) - 75, - 'etag': 'etag3', - }, - ], self.adapter.request_history[-1].json()) + self.assertEqual( + [ + { + 'path': "{base_object}/000000".format( + base_object=base_object + ), + 'size_bytes': 25, + 'etag': 'etag0', + }, + { + 'path': "{base_object}/000001".format( + base_object=base_object + ), + 'size_bytes': 25, + 'etag': 'etag1', + }, + { + 'path': "{base_object}/000002".format( + base_object=base_object + ), + 'size_bytes': 25, + 'etag': 'etag2', + }, + { + 'path': "{base_object}/000003".format( + base_object=base_object + ), + 'size_bytes': len(self.object) - 75, + 'etag': 'etag3', + }, + ], + self.adapter.request_history[-1].json(), + ) def test_slo_manifest_fail(self): """ @@ -1023,114 +1380,148 @@ def test_slo_manifest_fail(self): min_file_size = 1 uris_to_mock = [ - dict(method='GET', uri='https://object-store.example.com/info', - json=dict( - swift={'max_file_size': max_file_size}, - slo={'min_segment_size': min_file_size})), - dict(method='HEAD', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=404) + dict( + method='GET', + uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': max_file_size}, + slo={'min_segment_size': min_file_size}, + ), + ), + dict( + method='HEAD', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + status_code=404, + ), ] - uris_to_mock.extend([ - dict(method='PUT', - uri='{endpoint}/{container}/{object}/{index:0>6}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - index=index), - status_code=201, - headers=dict(Etag='etag{index}'.format(index=index))) - for index, offset in enumerate( - range(0, len(self.content), max_file_size)) - ]) + uris_to_mock.extend( + [ + dict( + method='PUT', + uri='{endpoint}/{container}/{object}/{index:0>6}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + index=index, + ), + status_code=201, + headers=dict(Etag='etag{index}'.format(index=index)), + ) + for index, offset in enumerate( + range(0, len(self.content), max_file_size) + ) + ] + ) # manifest file upload calls - uris_to_mock.extend([ - dict(method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=400, - validate=dict( - params={ - 'multipart-manifest', 'put' - }, - headers={ - 'x-object-meta-x-sdk-md5': self.md5, - 'x-object-meta-x-sdk-sha256': self.sha256, - })), - dict(method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=400, - validate=dict( - params={ - 'multipart-manifest', 'put' - }, - headers={ - 'x-object-meta-x-sdk-md5': self.md5, - 'x-object-meta-x-sdk-sha256': self.sha256, - })), - dict(method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=400, - validate=dict( - params={ - 'multipart-manifest', 'put' - }, - headers={ - 'x-object-meta-x-sdk-md5': self.md5, - 'x-object-meta-x-sdk-sha256': self.sha256, - })), - ]) + uris_to_mock.extend( + [ + dict( + method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + status_code=400, + validate=dict( + params={'multipart-manifest', 'put'}, + headers={ + 'x-object-meta-x-sdk-md5': self.md5, + 'x-object-meta-x-sdk-sha256': self.sha256, + }, + ), + ), + dict( + method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + status_code=400, + validate=dict( + params={'multipart-manifest', 'put'}, + headers={ + 'x-object-meta-x-sdk-md5': self.md5, + 'x-object-meta-x-sdk-sha256': self.sha256, + }, + ), + ), + dict( + method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + status_code=400, + validate=dict( + params={'multipart-manifest', 'put'}, + headers={ + 'x-object-meta-x-sdk-md5': self.md5, + 'x-object-meta-x-sdk-sha256': self.sha256, + }, + ), + ), + ] + ) # Cleaning up image upload segments involves calling the # delete_autocreated_image_objects() API method which will list # objects (LIST), get the object metadata (HEAD), then delete the # object (DELETE). - uris_to_mock.extend([ - dict(method='GET', - uri='{endpoint}/images?format=json&prefix={prefix}'.format( - endpoint=self.endpoint, - prefix=self.object), - complete_qs=True, - json=[{ - 'content_type': 'application/octet-stream', - 'bytes': 1437258240, - 'hash': '249219347276c331b87bf1ac2152d9af', - 'last_modified': '2015-02-16T17:50:05.289600', - 'name': self.object - }]), - - dict(method='HEAD', - uri='{endpoint}/images/{object}'.format( - endpoint=self.endpoint, - object=self.object), - headers={ - 'X-Timestamp': '1429036140.50253', - 'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1', - 'Content-Length': '1290170880', - 'Last-Modified': 'Tue, 14 Apr 2015 18:29:01 GMT', - 'X-Object-Meta-x-sdk-autocreated': 'true', - 'X-Object-Meta-X-Shade-Sha256': 'does not matter', - 'X-Object-Meta-X-Shade-Md5': 'does not matter', - 'Date': 'Thu, 16 Nov 2017 15:24:30 GMT', - 'Accept-Ranges': 'bytes', - 'X-Static-Large-Object': 'false', - 'Content-Type': 'application/octet-stream', - 'Etag': '249219347276c331b87bf1ac2152d9af', - }), - - dict(method='DELETE', - uri='{endpoint}/images/{object}'.format( - endpoint=self.endpoint, object=self.object)) - ]) + uris_to_mock.extend( + [ + dict( + method='GET', + uri='{endpoint}/images?format=json&prefix={prefix}'.format( + endpoint=self.endpoint, prefix=self.object + ), + complete_qs=True, + json=[ + { + 'content_type': 'application/octet-stream', + 'bytes': 1437258240, + 'hash': '249219347276c331b87bf1ac2152d9af', + 'last_modified': '2015-02-16T17:50:05.289600', + 'name': self.object, + } + ], + ), + dict( + method='HEAD', + uri='{endpoint}/images/{object}'.format( + endpoint=self.endpoint, object=self.object + ), + headers={ + 'X-Timestamp': '1429036140.50253', + 'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1', + 'Content-Length': '1290170880', + 'Last-Modified': 'Tue, 14 Apr 2015 18:29:01 GMT', + 'X-Object-Meta-x-sdk-autocreated': 'true', + 'X-Object-Meta-X-Shade-Sha256': 'does not matter', + 'X-Object-Meta-X-Shade-Md5': 'does not matter', + 'Date': 'Thu, 16 Nov 2017 15:24:30 GMT', + 'Accept-Ranges': 'bytes', + 'X-Static-Large-Object': 'false', + 'Content-Type': 'application/octet-stream', + 'Etag': '249219347276c331b87bf1ac2152d9af', + }, + ), + dict( + method='DELETE', + uri='{endpoint}/images/{object}'.format( + endpoint=self.endpoint, object=self.object + ), + ), + ] + ) self.register_uris(uris_to_mock) @@ -1141,8 +1532,11 @@ def test_slo_manifest_fail(self): self.assertRaises( exc.OpenStackCloudException, self.cloud.create_object, - container=self.container, name=self.object, - filename=self.object_file.name, use_slo=True) + container=self.container, + name=self.object, + filename=self.object_file.name, + use_slo=True, + ) # After call 3, order become indeterminate because of thread pool self.assert_calls(stop_after=3) @@ -1152,52 +1546,81 @@ def test_object_segment_retry_failure(self): max_file_size = 25 min_file_size = 1 - self.register_uris([ - dict(method='GET', uri='https://object-store.example.com/info', - json=dict( - swift={'max_file_size': max_file_size}, - slo={'min_segment_size': min_file_size})), - dict(method='HEAD', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=404), - dict(method='PUT', - uri='{endpoint}/{container}/{object}/000000'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object), - status_code=201), - dict(method='PUT', - uri='{endpoint}/{container}/{object}/000001'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object), - status_code=201), - dict(method='PUT', - uri='{endpoint}/{container}/{object}/000002'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object), - status_code=201), - dict(method='PUT', - uri='{endpoint}/{container}/{object}/000003'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object), - status_code=501), - dict(method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=201) - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': max_file_size}, + slo={'min_segment_size': min_file_size}, + ), + ), + dict( + method='HEAD', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + status_code=404, + ), + dict( + method='PUT', + uri='{endpoint}/{container}/{object}/000000'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + status_code=201, + ), + dict( + method='PUT', + uri='{endpoint}/{container}/{object}/000001'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + status_code=201, + ), + dict( + method='PUT', + uri='{endpoint}/{container}/{object}/000002'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + status_code=201, + ), + dict( + method='PUT', + uri='{endpoint}/{container}/{object}/000003'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + status_code=501, + ), + dict( + method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + status_code=201, + ), + ] + ) self.assertRaises( exc.OpenStackCloudException, self.cloud.create_object, - container=self.container, name=self.object, - filename=self.object_file.name, use_slo=True) + container=self.container, + name=self.object, + filename=self.object_file.name, + use_slo=True, + ) # After call 3, order become indeterminate because of thread pool self.assert_calls(stop_after=3) @@ -1207,152 +1630,213 @@ def test_object_segment_retries(self): max_file_size = 25 min_file_size = 1 - self.register_uris([ - dict(method='GET', uri='https://object-store.example.com/info', - json=dict( - swift={'max_file_size': max_file_size}, - slo={'min_segment_size': min_file_size})), - dict(method='HEAD', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=404), - dict(method='PUT', - uri='{endpoint}/{container}/{object}/000000'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object), - headers={'etag': 'etag0'}, - status_code=201), - dict(method='PUT', - uri='{endpoint}/{container}/{object}/000001'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object), - headers={'etag': 'etag1'}, - status_code=201), - dict(method='PUT', - uri='{endpoint}/{container}/{object}/000002'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object), - headers={'etag': 'etag2'}, - status_code=201), - dict(method='PUT', - uri='{endpoint}/{container}/{object}/000003'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object), - status_code=501), - dict(method='PUT', - uri='{endpoint}/{container}/{object}/000003'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object), - status_code=201, - headers={'etag': 'etag3'}), - dict(method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=201, - validate=dict( - params={ - 'multipart-manifest', 'put' - }, - headers={ - 'x-object-meta-x-sdk-md5': self.md5, - 'x-object-meta-x-sdk-sha256': self.sha256, - })) - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': max_file_size}, + slo={'min_segment_size': min_file_size}, + ), + ), + dict( + method='HEAD', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + status_code=404, + ), + dict( + method='PUT', + uri='{endpoint}/{container}/{object}/000000'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + headers={'etag': 'etag0'}, + status_code=201, + ), + dict( + method='PUT', + uri='{endpoint}/{container}/{object}/000001'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + headers={'etag': 'etag1'}, + status_code=201, + ), + dict( + method='PUT', + uri='{endpoint}/{container}/{object}/000002'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + headers={'etag': 'etag2'}, + status_code=201, + ), + dict( + method='PUT', + uri='{endpoint}/{container}/{object}/000003'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + status_code=501, + ), + dict( + method='PUT', + uri='{endpoint}/{container}/{object}/000003'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + status_code=201, + headers={'etag': 'etag3'}, + ), + dict( + method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + status_code=201, + validate=dict( + params={'multipart-manifest', 'put'}, + headers={ + 'x-object-meta-x-sdk-md5': self.md5, + 'x-object-meta-x-sdk-sha256': self.sha256, + }, + ), + ), + ] + ) self.cloud.create_object( - container=self.container, name=self.object, - filename=self.object_file.name, use_slo=True) + container=self.container, + name=self.object, + filename=self.object_file.name, + use_slo=True, + ) # After call 3, order become indeterminate because of thread pool self.assert_calls(stop_after=3) for key, value in self.calls[-1]['headers'].items(): self.assertEqual( - value, self.adapter.request_history[-1].headers[key], - 'header mismatch in manifest call') + value, + self.adapter.request_history[-1].headers[key], + 'header mismatch in manifest call', + ) base_object = '/{container}/{object}'.format( - container=self.container, - object=self.object) + container=self.container, object=self.object + ) - self.assertEqual([ - { - 'path': "{base_object}/000000".format( - base_object=base_object), - 'size_bytes': 25, - 'etag': 'etag0', - }, - { - 'path': "{base_object}/000001".format( - base_object=base_object), - 'size_bytes': 25, - 'etag': 'etag1', - }, - { - 'path': "{base_object}/000002".format( - base_object=base_object), - 'size_bytes': 25, - 'etag': 'etag2', - }, - { - 'path': "{base_object}/000003".format( - base_object=base_object), - 'size_bytes': len(self.object) - 75, - 'etag': 'etag3', - }, - ], self.adapter.request_history[-1].json()) + self.assertEqual( + [ + { + 'path': "{base_object}/000000".format( + base_object=base_object + ), + 'size_bytes': 25, + 'etag': 'etag0', + }, + { + 'path': "{base_object}/000001".format( + base_object=base_object + ), + 'size_bytes': 25, + 'etag': 'etag1', + }, + { + 'path': "{base_object}/000002".format( + base_object=base_object + ), + 'size_bytes': 25, + 'etag': 'etag2', + }, + { + 'path': "{base_object}/000003".format( + base_object=base_object + ), + 'size_bytes': len(self.object) - 75, + 'etag': 'etag3', + }, + ], + self.adapter.request_history[-1].json(), + ) def test_create_object_skip_checksum(self): - self.register_uris([ - dict(method='GET', - uri='https://object-store.example.com/info', - json=dict( - swift={'max_file_size': 1000}, - slo={'min_segment_size': 500})), - dict(method='HEAD', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, container=self.container, - object=self.object), - status_code=200), - dict(method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=201, - validate=dict(headers={})), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://object-store.example.com/info', + json=dict( + swift={'max_file_size': 1000}, + slo={'min_segment_size': 500}, + ), + ), + dict( + method='HEAD', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + status_code=200, + ), + dict( + method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + status_code=201, + validate=dict(headers={}), + ), + ] + ) self.cloud.create_object( - container=self.container, name=self.object, + container=self.container, + name=self.object, filename=self.object_file.name, - generate_checksums=False) + generate_checksums=False, + ) self.assert_calls() def test_create_object_data(self): - self.register_uris([ - dict(method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, object=self.object), - status_code=201, - validate=dict( - headers={}, - data=self.content, - )), - ]) + self.register_uris( + [ + dict( + method='PUT', + uri='{endpoint}/{container}/{object}'.format( + endpoint=self.endpoint, + container=self.container, + object=self.object, + ), + status_code=201, + validate=dict( + headers={}, + data=self.content, + ), + ), + ] + ) self.cloud.create_object( - container=self.container, name=self.object, - data=self.content) + container=self.container, name=self.object, data=self.content + ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_openstackcloud.py b/openstack/tests/unit/cloud/test_openstackcloud.py index 07d227995..d2a9e33b3 100644 --- a/openstack/tests/unit/cloud/test_openstackcloud.py +++ b/openstack/tests/unit/cloud/test_openstackcloud.py @@ -19,7 +19,6 @@ class TestSearch(base.TestCase): - class FakeResource(resource.Resource): allow_fetch = True allow_list = True @@ -33,9 +32,7 @@ def setUp(self): self.session._sdk_connection = self.cloud self.session._get = mock.Mock() self.session._list = mock.Mock() - self.session._resource_registry = dict( - fake=self.FakeResource - ) + self.session._resource_registry = dict(fake=self.FakeResource) # Set the mock into the cloud connection setattr(self.cloud, "mock_session", self.session) @@ -44,7 +41,7 @@ def test_raises_unknown_service(self): exceptions.SDKException, self.cloud.search_resources, "wrong_service.wrong_resource", - "name" + "name", ) def test_raises_unknown_resource(self): @@ -52,44 +49,33 @@ def test_raises_unknown_resource(self): exceptions.SDKException, self.cloud.search_resources, "mock_session.wrong_resource", - "name" + "name", ) def test_search_resources_get_finds(self): self.session._get.return_value = self.FakeResource(foo="bar") - ret = self.cloud.search_resources( - "mock_session.fake", - "fake_name" - ) - self.session._get.assert_called_with( - self.FakeResource, "fake_name") + ret = self.cloud.search_resources("mock_session.fake", "fake_name") + self.session._get.assert_called_with(self.FakeResource, "fake_name") self.assertEqual(1, len(ret)) self.assertEqual( - self.FakeResource(foo="bar").to_dict(), - ret[0].to_dict() + self.FakeResource(foo="bar").to_dict(), ret[0].to_dict() ) def test_search_resources_list(self): self.session._get.side_effect = exceptions.ResourceNotFound - self.session._list.return_value = [ - self.FakeResource(foo="bar") - ] + self.session._list.return_value = [self.FakeResource(foo="bar")] - ret = self.cloud.search_resources( - "mock_session.fake", - "fake_name" - ) - self.session._get.assert_called_with( - self.FakeResource, "fake_name") + ret = self.cloud.search_resources("mock_session.fake", "fake_name") + self.session._get.assert_called_with(self.FakeResource, "fake_name") self.session._list.assert_called_with( - self.FakeResource, name="fake_name") + self.FakeResource, name="fake_name" + ) self.assertEqual(1, len(ret)) self.assertEqual( - self.FakeResource(foo="bar").to_dict(), - ret[0].to_dict() + self.FakeResource(foo="bar").to_dict(), ret[0].to_dict() ) def test_search_resources_args(self): @@ -103,33 +89,27 @@ def test_search_resources_args(self): get_kwargs={"getkwarg1": "1"}, list_args=["listarg1"], list_kwargs={"listkwarg1": "1"}, - filter1="foo" + filter1="foo", ) self.session._get.assert_called_with( - self.FakeResource, "fake_name", - "getarg1", getkwarg1="1") + self.FakeResource, "fake_name", "getarg1", getkwarg1="1" + ) self.session._list.assert_called_with( self.FakeResource, - "listarg1", listkwarg1="1", - name="fake_name", filter1="foo" + "listarg1", + listkwarg1="1", + name="fake_name", + filter1="foo", ) def test_search_resources_name_empty(self): - self.session._list.return_value = [ - self.FakeResource(foo="bar") - ] + self.session._list.return_value = [self.FakeResource(foo="bar")] - ret = self.cloud.search_resources( - "mock_session.fake", - None, - foo="bar" - ) + ret = self.cloud.search_resources("mock_session.fake", None, foo="bar") self.session._get.assert_not_called() - self.session._list.assert_called_with( - self.FakeResource, foo="bar") + self.session._list.assert_called_with(self.FakeResource, foo="bar") self.assertEqual(1, len(ret)) self.assertEqual( - self.FakeResource(foo="bar").to_dict(), - ret[0].to_dict() + self.FakeResource(foo="bar").to_dict(), ret[0].to_dict() ) diff --git a/openstack/tests/unit/cloud/test_operator.py b/openstack/tests/unit/cloud/test_operator.py index 95078716c..32a2da484 100644 --- a/openstack/tests/unit/cloud/test_operator.py +++ b/openstack/tests/unit/cloud/test_operator.py @@ -22,7 +22,6 @@ class TestOperatorCloud(base.TestCase): - def test_get_image_name(self): self.use_glance() @@ -30,14 +29,20 @@ def test_get_image_name(self): fake_image = fakes.make_fake_image(image_id=image_id) list_return = {'images': [fake_image]} - self.register_uris([ - dict(method='GET', - uri='https://image.example.com/v2/images', - json=list_return), - dict(method='GET', - uri='https://image.example.com/v2/images', - json=list_return), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://image.example.com/v2/images', + json=list_return, + ), + dict( + method='GET', + uri='https://image.example.com/v2/images', + json=list_return, + ), + ] + ) self.assertEqual('fake_image', self.cloud.get_image_name(image_id)) self.assertEqual('fake_image', self.cloud.get_image_name('fake_image')) @@ -51,14 +56,20 @@ def test_get_image_id(self): fake_image = fakes.make_fake_image(image_id=image_id) list_return = {'images': [fake_image]} - self.register_uris([ - dict(method='GET', - uri='https://image.example.com/v2/images', - json=list_return), - dict(method='GET', - uri='https://image.example.com/v2/images', - json=list_return), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://image.example.com/v2/images', + json=list_return, + ), + dict( + method='GET', + uri='https://image.example.com/v2/images', + json=list_return, + ), + ] + ) self.assertEqual(image_id, self.cloud.get_image_id(image_id)) self.assertEqual(image_id, self.cloud.get_image_id('fake_image')) @@ -72,15 +83,17 @@ class FakeException(Exception): def side_effect(*args, **kwargs): raise FakeException("No service") + session_mock = mock.Mock() session_mock.get_endpoint.side_effect = side_effect get_session_mock.return_value = session_mock self.cloud.name = 'testcloud' self.cloud.config.config['region_name'] = 'testregion' with testtools.ExpectedException( - exc.OpenStackCloudException, - "Error getting image endpoint on testcloud:testregion:" - " No service"): + exc.OpenStackCloudException, + "Error getting image endpoint on testcloud:testregion:" + " No service", + ): self.cloud.get_session_endpoint("image") @mock.patch.object(cloud_region.CloudRegion, 'get_session') @@ -97,8 +110,11 @@ def test_get_session_endpoint_identity(self, get_session_mock): get_session_mock.return_value = session_mock self.cloud.get_session_endpoint('identity') kwargs = dict( - interface='public', region_name='RegionOne', - service_name=None, service_type='identity') + interface='public', + region_name='RegionOne', + service_name=None, + service_type='identity', + ) session_mock.get_endpoint.assert_called_with(**kwargs) @@ -122,23 +138,23 @@ def test_list_hypervisors(self): uuid1 = uuid.uuid4().hex uuid2 = uuid.uuid4().hex self.use_compute_discovery() - self.register_uris([ - dict( - method='GET', - uri='https://compute.example.com/v2.1/os-hypervisors/detail', - json={ - 'hypervisors': [ - fakes.make_fake_hypervisor(uuid1, 'testserver1'), - fakes.make_fake_hypervisor(uuid2, 'testserver2'), - ] - }, - validate={ - 'headers': { - 'OpenStack-API-Version': 'compute 2.53' - } - } - ), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://compute.example.com/v2.1/os-hypervisors/detail', # noqa: E501 + json={ + 'hypervisors': [ + fakes.make_fake_hypervisor(uuid1, 'testserver1'), + fakes.make_fake_hypervisor(uuid2, 'testserver2'), + ] + }, + validate={ + 'headers': {'OpenStack-API-Version': 'compute 2.53'} + }, + ), + ] + ) r = self.cloud.list_hypervisors() @@ -154,19 +170,22 @@ def test_list_old_hypervisors(self): '''This test verifies that calling list_hypervisors on a pre-2.53 cloud calls the old version.''' self.use_compute_discovery( - compute_version_json='old-compute-version.json') - self.register_uris([ - dict( - method='GET', - uri='https://compute.example.com/v2.1/os-hypervisors/detail', - json={ - 'hypervisors': [ - fakes.make_fake_hypervisor('1', 'testserver1'), - fakes.make_fake_hypervisor('2', 'testserver2'), - ] - } - ), - ]) + compute_version_json='old-compute-version.json' + ) + self.register_uris( + [ + dict( + method='GET', + uri='https://compute.example.com/v2.1/os-hypervisors/detail', # noqa: E501 + json={ + 'hypervisors': [ + fakes.make_fake_hypervisor('1', 'testserver1'), + fakes.make_fake_hypervisor('2', 'testserver2'), + ] + }, + ), + ] + ) r = self.cloud.list_hypervisors() diff --git a/openstack/tests/unit/cloud/test_operator_noauth.py b/openstack/tests/unit/cloud/test_operator_noauth.py index 1c098962a..1dacd43ac 100644 --- a/openstack/tests/unit/cloud/test_operator_noauth.py +++ b/openstack/tests/unit/cloud/test_operator_noauth.py @@ -32,19 +32,34 @@ def setUp(self): # By clearing the URI registry, we remove all calls to a keystone # catalog or getting a token self._uri_registry.clear() - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - service_type='baremetal', base_url_append='v1'), - json={'id': 'v1', - 'links': [{"href": "https://baremetal.example.com/v1", - "rel": "self"}]}), - dict(method='GET', - uri=self.get_mock_url( - service_type='baremetal', base_url_append='v1', - resource='nodes'), - json={'nodes': []}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + service_type='baremetal', base_url_append='v1' + ), + json={ + 'id': 'v1', + 'links': [ + { + "href": "https://baremetal.example.com/v1", + "rel": "self", + } + ], + }, + ), + dict( + method='GET', + uri=self.get_mock_url( + service_type='baremetal', + base_url_append='v1', + resource='nodes', + ), + json={'nodes': []}, + ), + ] + ) def test_ironic_noauth_none_auth_type(self): """Test noauth selection for Ironic in OpenStackCloud @@ -58,7 +73,8 @@ def test_ironic_noauth_none_auth_type(self): # client library. self.cloud_noauth = openstack.connect( auth_type='none', - baremetal_endpoint_override="https://baremetal.example.com/v1") + baremetal_endpoint_override="https://baremetal.example.com/v1", + ) self.cloud_noauth.list_machines() @@ -92,8 +108,9 @@ def test_ironic_noauth_admin_token_auth_type(self): self.cloud_noauth = openstack.connect( auth_type='admin_token', auth=dict( - endpoint='https://baremetal.example.com/v1', - token='ignored')) + endpoint='https://baremetal.example.com/v1', token='ignored' + ), + ) self.cloud_noauth.list_machines() @@ -116,65 +133,94 @@ def setUp(self): # By clearing the URI registry, we remove all calls to a keystone # catalog or getting a token self._uri_registry.clear() - self.register_uris([ - dict(method='GET', - uri='https://baremetal.example.com/', - json={ - "default_version": { - "status": "CURRENT", - "min_version": "1.1", - "version": "1.46", - "id": "v1", - "links": [{ - "href": "https://baremetal.example.com/v1", - "rel": "self" - }]}, - "versions": [{ - "status": "CURRENT", - "min_version": "1.1", - "version": "1.46", - "id": "v1", - "links": [{ - "href": "https://baremetal.example.com/v1", - "rel": "self" - }]}], - "name": "OpenStack Ironic API", - "description": "Ironic is an OpenStack project." - }), - dict(method='GET', - uri=self.get_mock_url( - service_type='baremetal', base_url_append='v1'), - json={ - "media_types": [{ - "base": "application/json", - "type": "application/vnd.openstack.ironic.v1+json" - }], - "links": [{ - "href": "https://baremetal.example.com/v1", - "rel": "self" - }], - "ports": [{ - "href": "https://baremetal.example.com/v1/ports/", - "rel": "self" - }, { - "href": "https://baremetal.example.com/ports/", - "rel": "bookmark" - }], - "nodes": [{ - "href": "https://baremetal.example.com/v1/nodes/", - "rel": "self" - }, { - "href": "https://baremetal.example.com/nodes/", - "rel": "bookmark" - }], - "id": "v1" - }), - dict(method='GET', - uri=self.get_mock_url( - service_type='baremetal', base_url_append='v1', - resource='nodes'), - json={'nodes': []}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://baremetal.example.com/', + json={ + "default_version": { + "status": "CURRENT", + "min_version": "1.1", + "version": "1.46", + "id": "v1", + "links": [ + { + "href": "https://baremetal.example.com/v1", + "rel": "self", + } + ], + }, + "versions": [ + { + "status": "CURRENT", + "min_version": "1.1", + "version": "1.46", + "id": "v1", + "links": [ + { + "href": "https://baremetal.example.com/v1", # noqa: E501 + "rel": "self", + } + ], + } + ], + "name": "OpenStack Ironic API", + "description": "Ironic is an OpenStack project.", + }, + ), + dict( + method='GET', + uri=self.get_mock_url( + service_type='baremetal', base_url_append='v1' + ), + json={ + "media_types": [ + { + "base": "application/json", + "type": "application/vnd.openstack.ironic.v1+json", # noqa: E501 + } + ], + "links": [ + { + "href": "https://baremetal.example.com/v1", + "rel": "self", + } + ], + "ports": [ + { + "href": "https://baremetal.example.com/v1/ports/", # noqa: E501 + "rel": "self", + }, + { + "href": "https://baremetal.example.com/ports/", + "rel": "bookmark", + }, + ], + "nodes": [ + { + "href": "https://baremetal.example.com/v1/nodes/", # noqa: E501 + "rel": "self", + }, + { + "href": "https://baremetal.example.com/nodes/", + "rel": "bookmark", + }, + ], + "id": "v1", + }, + ), + dict( + method='GET', + uri=self.get_mock_url( + service_type='baremetal', + base_url_append='v1', + resource='nodes', + ), + json={'nodes': []}, + ), + ] + ) def test_ironic_noauth_none_auth_type(self): """Test noauth selection for Ironic in OpenStackCloud @@ -188,7 +234,8 @@ def test_ironic_noauth_none_auth_type(self): # client library. self.cloud_noauth = openstack.connect( auth_type='none', - baremetal_endpoint_override="https://baremetal.example.com") + baremetal_endpoint_override="https://baremetal.example.com", + ) self.cloud_noauth.list_machines() diff --git a/openstack/tests/unit/cloud/test_port.py b/openstack/tests/unit/cloud/test_port.py index 04e5726de..0a21aba6f 100644 --- a/openstack/tests/unit/cloud/test_port.py +++ b/openstack/tests/unit/cloud/test_port.py @@ -42,14 +42,11 @@ class TestPort(base.TestCase): 'mac_address': '50:1c:0d:e4:f0:0d', 'binding:profile': {}, 'fixed_ips': [ - { - 'subnet_id': 'test-subnet-id', - 'ip_address': '29.29.29.29' - } + {'subnet_id': 'test-subnet-id', 'ip_address': '29.29.29.29'} ], 'id': 'test-port-id', 'security_groups': [], - 'device_id': '' + 'device_id': '', } } @@ -70,14 +67,11 @@ class TestPort(base.TestCase): 'mac_address': '50:1c:0d:e4:f0:0d', 'binding:profile': {}, 'fixed_ips': [ - { - 'subnet_id': 'test-subnet-id', - 'ip_address': '29.29.29.29' - } + {'subnet_id': 'test-subnet-id', 'ip_address': '29.29.29.29'} ], 'id': 'test-port-id', 'security_groups': [], - 'device_id': '' + 'device_id': '', } } @@ -94,7 +88,7 @@ class TestPort(base.TestCase): 'extra_dhcp_opts': [], 'binding:vif_details': { 'port_filter': True, - 'ovs_hybrid_plug': True + 'ovs_hybrid_plug': True, }, 'binding:vif_type': 'ovs', 'device_owner': 'network:router_gateway', @@ -104,12 +98,12 @@ class TestPort(base.TestCase): 'fixed_ips': [ { 'subnet_id': '008ba151-0b8c-4a67-98b5-0d2b87666062', - 'ip_address': '172.24.4.2' + 'ip_address': '172.24.4.2', } ], 'id': 'd80b1a3b-4fc1-49f3-952e-1e2ab7081d8b', 'security_groups': [], - 'device_id': '9ae135f4-b6e0-4dad-9e91-3c223e385824' + 'device_id': '9ae135f4-b6e0-4dad-9e91-3c223e385824', }, { 'status': 'ACTIVE', @@ -122,7 +116,7 @@ class TestPort(base.TestCase): 'extra_dhcp_opts': [], 'binding:vif_details': { 'port_filter': True, - 'ovs_hybrid_plug': True + 'ovs_hybrid_plug': True, }, 'binding:vif_type': 'ovs', 'device_owner': 'network:router_interface', @@ -132,104 +126,155 @@ class TestPort(base.TestCase): 'fixed_ips': [ { 'subnet_id': '288bf4a1-51ba-43b6-9d0a-520e9005db17', - 'ip_address': '10.0.0.1' + 'ip_address': '10.0.0.1', } ], 'id': 'f71a6703-d6de-4be1-a91a-a570ede1d159', 'security_groups': [], - 'device_id': '9ae135f4-b6e0-4dad-9e91-3c223e385824' - } + 'device_id': '9ae135f4-b6e0-4dad-9e91-3c223e385824', + }, ] } def _compare_ports(self, exp, real): self.assertDictEqual( _port.Port(**exp).to_dict(computed=False), - real.to_dict(computed=False)) + real.to_dict(computed=False), + ) def test_create_port(self): - self.register_uris([ - dict(method="POST", - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports']), - json=self.mock_neutron_port_create_rep, - validate=dict( - json={'port': { - 'network_id': 'test-net-id', - 'name': 'test-port-name', - 'admin_state_up': True}})) - ]) + self.register_uris( + [ + dict( + method="POST", + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports'] + ), + json=self.mock_neutron_port_create_rep, + validate=dict( + json={ + 'port': { + 'network_id': 'test-net-id', + 'name': 'test-port-name', + 'admin_state_up': True, + } + } + ), + ) + ] + ) port = self.cloud.create_port( - network_id='test-net-id', name='test-port-name', - admin_state_up=True) + network_id='test-net-id', + name='test-port-name', + admin_state_up=True, + ) self._compare_ports(self.mock_neutron_port_create_rep['port'], port) self.assert_calls() def test_create_port_parameters(self): """Test that we detect invalid arguments passed to create_port""" self.assertRaises( - TypeError, self.cloud.create_port, - network_id='test-net-id', nome='test-port-name', - stato_amministrativo_porta=True) + TypeError, + self.cloud.create_port, + network_id='test-net-id', + nome='test-port-name', + stato_amministrativo_porta=True, + ) def test_create_port_exception(self): - self.register_uris([ - dict(method="POST", - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports']), - status_code=500, - validate=dict( - json={'port': { - 'network_id': 'test-net-id', - 'name': 'test-port-name', - 'admin_state_up': True}})) - ]) + self.register_uris( + [ + dict( + method="POST", + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports'] + ), + status_code=500, + validate=dict( + json={ + 'port': { + 'network_id': 'test-net-id', + 'name': 'test-port-name', + 'admin_state_up': True, + } + } + ), + ) + ] + ) self.assertRaises( - OpenStackCloudException, self.cloud.create_port, - network_id='test-net-id', name='test-port-name', - admin_state_up=True) + OpenStackCloudException, + self.cloud.create_port, + network_id='test-net-id', + name='test-port-name', + admin_state_up=True, + ) self.assert_calls() def test_create_port_with_project(self): self.mock_neutron_port_create_rep["port"].update( { 'project_id': 'test-project-id', - }) - self.register_uris([ - dict(method="POST", - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports']), - json=self.mock_neutron_port_create_rep, - validate=dict( - json={'port': { - 'network_id': 'test-net-id', - 'project_id': 'test-project-id', - 'name': 'test-port-name', - 'admin_state_up': True}})) - ]) + } + ) + self.register_uris( + [ + dict( + method="POST", + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports'] + ), + json=self.mock_neutron_port_create_rep, + validate=dict( + json={ + 'port': { + 'network_id': 'test-net-id', + 'project_id': 'test-project-id', + 'name': 'test-port-name', + 'admin_state_up': True, + } + } + ), + ) + ] + ) port = self.cloud.create_port( - network_id='test-net-id', name='test-port-name', - admin_state_up=True, project_id='test-project-id') + network_id='test-net-id', + name='test-port-name', + admin_state_up=True, + project_id='test-project-id', + ) self._compare_ports(self.mock_neutron_port_create_rep['port'], port) self.assert_calls() def test_update_port(self): port_id = 'd80b1a3b-4fc1-49f3-952e-1e2ab7081d8b' - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports', port_id]), - json=dict(port=self.mock_neutron_port_list_rep['ports'][0])), - dict(method='PUT', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'ports', port_id]), - json=self.mock_neutron_port_update_rep, - validate=dict( - json={'port': {'name': 'test-port-name-updated'}})) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports', port_id] + ), + json=dict( + port=self.mock_neutron_port_list_rep['ports'][0] + ), + ), + dict( + method='PUT', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports', port_id] + ), + json=self.mock_neutron_port_update_rep, + validate=dict( + json={'port': {'name': 'test-port-name-updated'}} + ), + ), + ] + ) port = self.cloud.update_port( - name_or_id=port_id, name='test-port-name-updated') + name_or_id=port_id, name='test-port-name-updated' + ) self._compare_ports(self.mock_neutron_port_update_rep['port'], port) self.assert_calls() @@ -237,72 +282,107 @@ def test_update_port(self): def test_update_port_parameters(self): """Test that we detect invalid arguments passed to update_port""" self.assertRaises( - TypeError, self.cloud.update_port, - name_or_id='test-port-id', nome='test-port-name-updated') + TypeError, + self.cloud.update_port, + name_or_id='test-port-id', + nome='test-port-name-updated', + ) def test_update_port_exception(self): port_id = 'd80b1a3b-4fc1-49f3-952e-1e2ab7081d8b' - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports', port_id]), - json=self.mock_neutron_port_list_rep), - dict(method='PUT', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'ports', port_id]), - status_code=500, - validate=dict( - json={'port': {'name': 'test-port-name-updated'}})) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports', port_id] + ), + json=self.mock_neutron_port_list_rep, + ), + dict( + method='PUT', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports', port_id] + ), + status_code=500, + validate=dict( + json={'port': {'name': 'test-port-name-updated'}} + ), + ), + ] + ) self.assertRaises( - OpenStackCloudException, self.cloud.update_port, + OpenStackCloudException, + self.cloud.update_port, name_or_id='d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b', - name='test-port-name-updated') + name='test-port-name-updated', + ) self.assert_calls() def test_list_ports(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports']), - json=self.mock_neutron_port_list_rep) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports'] + ), + json=self.mock_neutron_port_list_rep, + ) + ] + ) ports = self.cloud.list_ports() for a, b in zip(self.mock_neutron_port_list_rep['ports'], ports): self._compare_ports(a, b) self.assert_calls() def test_list_ports_filtered(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports'], - qs_elements=['status=DOWN']), - json=self.mock_neutron_port_list_rep) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'ports'], + qs_elements=['status=DOWN'], + ), + json=self.mock_neutron_port_list_rep, + ) + ] + ) ports = self.cloud.list_ports(filters={'status': 'DOWN'}) for a, b in zip(self.mock_neutron_port_list_rep['ports'], ports): self._compare_ports(a, b) self.assert_calls() def test_list_ports_exception(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports']), - status_code=500) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports'] + ), + status_code=500, + ) + ] + ) self.assertRaises(OpenStackCloudException, self.cloud.list_ports) def test_search_ports_by_id(self): port_id = 'f71a6703-d6de-4be1-a91a-a570ede1d159' - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports']), - json=self.mock_neutron_port_list_rep) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports'] + ), + json=self.mock_neutron_port_list_rep, + ) + ] + ) ports = self.cloud.search_ports(name_or_id=port_id) self.assertEqual(1, len(ports)) @@ -311,12 +391,17 @@ def test_search_ports_by_id(self): def test_search_ports_by_name(self): port_name = "first-port" - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports']), - json=self.mock_neutron_port_list_rep) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports'] + ), + json=self.mock_neutron_port_list_rep, + ) + ] + ) ports = self.cloud.search_ports(name_or_id=port_name) self.assertEqual(1, len(ports)) @@ -324,51 +409,80 @@ def test_search_ports_by_name(self): self.assert_calls() def test_search_ports_not_found(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports']), - json=self.mock_neutron_port_list_rep) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports'] + ), + json=self.mock_neutron_port_list_rep, + ) + ] + ) ports = self.cloud.search_ports(name_or_id='non-existent') self.assertEqual(0, len(ports)) self.assert_calls() def test_delete_port(self): port_id = 'd80b1a3b-4fc1-49f3-952e-1e2ab7081d8b' - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'ports', 'first-port']), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports'], - qs_elements=['name=first-port']), - json=self.mock_neutron_port_list_rep), - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'ports', port_id]), - json={}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'ports', 'first-port'], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'ports'], + qs_elements=['name=first-port'], + ), + json=self.mock_neutron_port_list_rep, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'ports', port_id] + ), + json={}, + ), + ] + ) self.assertTrue(self.cloud.delete_port(name_or_id='first-port')) def test_delete_port_not_found(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports', - 'non-existent']), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports'], - qs_elements=['name=non-existent']), - json={'ports': []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'ports', 'non-existent'], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'ports'], + qs_elements=['name=non-existent'], + ), + json={'ports': []}, + ), + ] + ) self.assertFalse(self.cloud.delete_port(name_or_id='non-existent')) self.assert_calls() @@ -376,50 +490,78 @@ def test_delete_subnet_multiple_found(self): port_name = "port-name" port1 = dict(id='123', name=port_name) port2 = dict(id='456', name=port_name) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports', port_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports'], - qs_elements=['name=%s' % port_name]), - json={'ports': [port1, port2]}) - ]) - self.assertRaises(OpenStackCloudException, - self.cloud.delete_port, port_name) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'ports', port_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'ports'], + qs_elements=['name=%s' % port_name], + ), + json={'ports': [port1, port2]}, + ), + ] + ) + self.assertRaises( + OpenStackCloudException, self.cloud.delete_port, port_name + ) self.assert_calls() def test_delete_subnet_multiple_using_id(self): port_name = "port-name" port1 = dict(id='123', name=port_name) port2 = dict(id='456', name=port_name) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'ports', port1['id']]), - json={'ports': [port1, port2]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'ports', port1['id']]), - json={}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'ports', port1['id']], + ), + json={'ports': [port1, port2]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'ports', port1['id']], + ), + json={}, + ), + ] + ) self.assertTrue(self.cloud.delete_port(name_or_id=port1['id'])) self.assert_calls() def test_get_port_by_id(self): fake_port = dict(id='123', name='456') - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', - 'ports', - fake_port['id']]), - json={'port': fake_port}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'ports', fake_port['id']], + ), + json={'port': fake_port}, + ) + ] + ) r = self.cloud.get_port_by_id(fake_port['id']) self.assertIsNotNone(r) self._compare_ports(fake_port, r) diff --git a/openstack/tests/unit/cloud/test_project.py b/openstack/tests/unit/cloud/test_project.py index b7361b97e..dde72440d 100644 --- a/openstack/tests/unit/cloud/test_project.py +++ b/openstack/tests/unit/cloud/test_project.py @@ -21,10 +21,16 @@ class TestProject(base.TestCase): - - def get_mock_url(self, service_type='identity', interface='public', - resource=None, append=None, base_url_append=None, - v3=True, qs_elements=None): + def get_mock_url( + self, + service_type='identity', + interface='public', + resource=None, + append=None, + base_url_append=None, + v3=True, + qs_elements=None, + ): if v3 and resource is None: resource = 'projects' elif not v3 and resource is None: @@ -32,170 +38,238 @@ def get_mock_url(self, service_type='identity', interface='public', if base_url_append is None and v3: base_url_append = 'v3' return super(TestProject, self).get_mock_url( - service_type=service_type, interface=interface, resource=resource, - append=append, base_url_append=base_url_append, - qs_elements=qs_elements) + service_type=service_type, + interface=interface, + resource=resource, + append=append, + base_url_append=base_url_append, + qs_elements=qs_elements, + ) - def test_create_project_v3(self,): + def test_create_project_v3( + self, + ): project_data = self._get_project_data( description=self.getUniqueString('projectDesc'), - parent_id=uuid.uuid4().hex) + parent_id=uuid.uuid4().hex, + ) reference_req = project_data.json_request.copy() reference_req['project']['enabled'] = True - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url(), - status_code=200, - json=project_data.json_response, - validate=dict(json=reference_req)) - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url(), + status_code=200, + json=project_data.json_response, + validate=dict(json=reference_req), + ) + ] + ) project = self.cloud.create_project( name=project_data.project_name, description=project_data.description, domain_id=project_data.domain_id, - parent_id=project_data.parent_id) + parent_id=project_data.parent_id, + ) self.assertThat(project.id, matchers.Equals(project_data.project_id)) self.assertThat( - project.name, matchers.Equals(project_data.project_name)) + project.name, matchers.Equals(project_data.project_name) + ) self.assertThat( - project.description, matchers.Equals(project_data.description)) + project.description, matchers.Equals(project_data.description) + ) self.assertThat( - project.domain_id, matchers.Equals(project_data.domain_id)) + project.domain_id, matchers.Equals(project_data.domain_id) + ) self.assert_calls() def test_delete_project_v3(self): project_data = self._get_project_data(v3=False) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(append=[project_data.project_id]), - status_code=200, - json=project_data.json_response), - dict(method='DELETE', - uri=self.get_mock_url(append=[project_data.project_id]), - status_code=204) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(append=[project_data.project_id]), + status_code=200, + json=project_data.json_response, + ), + dict( + method='DELETE', + uri=self.get_mock_url(append=[project_data.project_id]), + status_code=204, + ), + ] + ) self.cloud.delete_project(project_data.project_id) self.assert_calls() def test_update_project_not_found(self): project_data = self._get_project_data() - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(append=[project_data.project_id]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - qs_elements=['name=' + project_data.project_id]), - status_code=200, - json={'projects': []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(append=[project_data.project_id]), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + qs_elements=['name=' + project_data.project_id] + ), + status_code=200, + json={'projects': []}, + ), + ] + ) # NOTE(notmorgan): This test (and shade) does not represent a case # where the project is in the project list but a 404 is raised when # the PATCH is issued. This is a bug in shade and should be fixed, # shade will raise an attribute error instead of the proper # project not found exception. with testtools.ExpectedException( - openstack.cloud.OpenStackCloudException, - "Project %s not found." % project_data.project_id + openstack.cloud.OpenStackCloudException, + "Project %s not found." % project_data.project_id, ): self.cloud.update_project(project_data.project_id) self.assert_calls() def test_update_project_v3(self): project_data = self._get_project_data( - description=self.getUniqueString('projectDesc')) + description=self.getUniqueString('projectDesc') + ) reference_req = project_data.json_request.copy() # Remove elements not actually sent in the update reference_req['project'].pop('domain_id') reference_req['project'].pop('name') reference_req['project'].pop('enabled') - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - append=[project_data.project_id], - qs_elements=['domain_id=' + project_data.domain_id]), - status_code=200, - json={'projects': [project_data.json_response['project']]}), - dict(method='PATCH', - uri=self.get_mock_url(append=[project_data.project_id]), - status_code=200, json=project_data.json_response, - validate=dict(json=reference_req)) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + append=[project_data.project_id], + qs_elements=['domain_id=' + project_data.domain_id], + ), + status_code=200, + json={'projects': [project_data.json_response['project']]}, + ), + dict( + method='PATCH', + uri=self.get_mock_url(append=[project_data.project_id]), + status_code=200, + json=project_data.json_response, + validate=dict(json=reference_req), + ), + ] + ) project = self.cloud.update_project( project_data.project_id, description=project_data.description, - domain_id=project_data.domain_id) + domain_id=project_data.domain_id, + ) self.assertThat(project.id, matchers.Equals(project_data.project_id)) self.assertThat( - project.name, matchers.Equals(project_data.project_name)) + project.name, matchers.Equals(project_data.project_name) + ) self.assertThat( - project.description, matchers.Equals(project_data.description)) + project.description, matchers.Equals(project_data.description) + ) self.assert_calls() def test_list_projects_v3(self): project_data = self._get_project_data( - description=self.getUniqueString('projectDesc')) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - resource=('projects?domain_id=%s' % - project_data.domain_id)), - status_code=200, - json={'projects': [project_data.json_response['project']]}) - ]) + description=self.getUniqueString('projectDesc') + ) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource=( + 'projects?domain_id=%s' % project_data.domain_id + ) + ), + status_code=200, + json={'projects': [project_data.json_response['project']]}, + ) + ] + ) projects = self.cloud.list_projects(project_data.domain_id) self.assertThat(len(projects), matchers.Equals(1)) self.assertThat( - projects[0].id, matchers.Equals(project_data.project_id)) + projects[0].id, matchers.Equals(project_data.project_id) + ) self.assert_calls() def test_list_projects_v3_kwarg(self): project_data = self._get_project_data( - description=self.getUniqueString('projectDesc')) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - resource=('projects?domain_id=%s' % - project_data.domain_id)), - status_code=200, - json={'projects': [project_data.json_response['project']]}) - ]) - projects = self.cloud.list_projects( - domain_id=project_data.domain_id) + description=self.getUniqueString('projectDesc') + ) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource=( + 'projects?domain_id=%s' % project_data.domain_id + ) + ), + status_code=200, + json={'projects': [project_data.json_response['project']]}, + ) + ] + ) + projects = self.cloud.list_projects(domain_id=project_data.domain_id) self.assertThat(len(projects), matchers.Equals(1)) self.assertThat( - projects[0].id, matchers.Equals(project_data.project_id)) + projects[0].id, matchers.Equals(project_data.project_id) + ) self.assert_calls() def test_list_projects_search_compat(self): project_data = self._get_project_data( - description=self.getUniqueString('projectDesc')) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(), - status_code=200, - json={'projects': [project_data.json_response['project']]}) - ]) + description=self.getUniqueString('projectDesc') + ) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'projects': [project_data.json_response['project']]}, + ) + ] + ) projects = self.cloud.search_projects(project_data.project_id) self.assertThat(len(projects), matchers.Equals(1)) self.assertThat( - projects[0].id, matchers.Equals(project_data.project_id)) + projects[0].id, matchers.Equals(project_data.project_id) + ) self.assert_calls() def test_list_projects_search_compat_v3(self): project_data = self._get_project_data( - description=self.getUniqueString('projectDesc')) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - resource=('projects?domain_id=%s' % - project_data.domain_id)), - status_code=200, - json={'projects': [project_data.json_response['project']]}) - ]) - projects = self.cloud.search_projects( - domain_id=project_data.domain_id) + description=self.getUniqueString('projectDesc') + ) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource=( + 'projects?domain_id=%s' % project_data.domain_id + ) + ), + status_code=200, + json={'projects': [project_data.json_response['project']]}, + ) + ] + ) + projects = self.cloud.search_projects(domain_id=project_data.domain_id) self.assertThat(len(projects), matchers.Equals(1)) self.assertThat( - projects[0].id, matchers.Equals(project_data.project_id)) + projects[0].id, matchers.Equals(project_data.project_id) + ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py b/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py index 8febafa3c..468f4abf7 100644 --- a/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py @@ -38,14 +38,14 @@ class TestQosBandwidthLimitRule(base.TestCase): 'project_id': project_id, 'tenant_id': project_id, 'shared': False, - 'is_default': False + 'is_default': False, } mock_rule = { 'id': rule_id, 'max_kbps': rule_max_kbps, 'max_burst_kbps': rule_max_burst, - 'direction': 'egress' + 'direction': 'egress', } qos_extension = { @@ -53,7 +53,7 @@ class TestQosBandwidthLimitRule(base.TestCase): "name": "Quality of Service", "links": [], "alias": "qos", - "description": "The Quality of Service extension." + "description": "The Quality of Service extension.", } qos_bw_limit_direction_extension = { @@ -61,316 +61,561 @@ class TestQosBandwidthLimitRule(base.TestCase): "name": "Direction for QoS bandwidth limit rule", "links": [], "alias": "qos-bw-limit-direction", - "description": ("Allow to configure QoS bandwidth limit rule with " - "specific direction: ingress or egress") + "description": ( + "Allow to configure QoS bandwidth limit rule with " + "specific direction: ingress or egress" + ), } - enabled_neutron_extensions = [qos_extension, - qos_bw_limit_direction_extension] + enabled_neutron_extensions = [ + qos_extension, + qos_bw_limit_direction_extension, + ] def _compare_rules(self, exp, real): self.assertDictEqual( qos_bandwidth_limit_rule.QoSBandwidthLimitRule(**exp).to_dict( - computed=False), - real.to_dict(computed=False)) + computed=False + ), + real.to_dict(computed=False), + ) def test_get_qos_bandwidth_limit_rule(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name]), - json={'policies': [self.mock_policy]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id, - 'bandwidth_limit_rules', self.rule_id]), - json={'bandwidth_limit_rule': self.mock_rule}) - ]) - r = self.cloud.get_qos_bandwidth_limit_rule(self.policy_name, - self.rule_id) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': self.enabled_neutron_extensions}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', self.policy_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name], + ), + json={'policies': [self.mock_policy]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'qos', + 'policies', + self.policy_id, + 'bandwidth_limit_rules', + self.rule_id, + ], + ), + json={'bandwidth_limit_rule': self.mock_rule}, + ), + ] + ) + r = self.cloud.get_qos_bandwidth_limit_rule( + self.policy_name, self.rule_id + ) self._compare_rules(self.mock_rule, r) self.assert_calls() def test_get_qos_bandwidth_limit_rule_no_qos_policy_found(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name]), - json={'policies': []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': self.enabled_neutron_extensions}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', self.policy_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name], + ), + json={'policies': []}, + ), + ] + ) self.assertRaises( exc.OpenStackCloudResourceNotFound, self.cloud.get_qos_bandwidth_limit_rule, - self.policy_name, self.rule_id) + self.policy_name, + self.rule_id, + ) self.assert_calls() def test_get_qos_bandwidth_limit_rule_no_qos_extension(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': []}, + ) + ] + ) self.assertRaises( exc.OpenStackCloudException, self.cloud.get_qos_bandwidth_limit_rule, - self.policy_name, self.rule_id) + self.policy_name, + self.rule_id, + ) self.assert_calls() def test_create_qos_bandwidth_limit_rule(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name]), - json={'policies': [self.mock_policy]}), - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id, - 'bandwidth_limit_rules']), - json={'bandwidth_limit_rule': self.mock_rule}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': self.enabled_neutron_extensions}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', self.policy_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name], + ), + json={'policies': [self.mock_policy]}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'qos', + 'policies', + self.policy_id, + 'bandwidth_limit_rules', + ], + ), + json={'bandwidth_limit_rule': self.mock_rule}, + ), + ] + ) rule = self.cloud.create_qos_bandwidth_limit_rule( - self.policy_name, max_kbps=self.rule_max_kbps) + self.policy_name, max_kbps=self.rule_max_kbps + ) self._compare_rules(self.mock_rule, rule) self.assert_calls() def test_create_qos_bandwidth_limit_rule_no_qos_extension(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': []}, + ) + ] + ) self.assertRaises( exc.OpenStackCloudException, - self.cloud.create_qos_bandwidth_limit_rule, self.policy_name, - max_kbps=100) + self.cloud.create_qos_bandwidth_limit_rule, + self.policy_name, + max_kbps=100, + ) self.assert_calls() def test_create_qos_bandwidth_limit_rule_no_qos_direction_extension(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': [self.qos_extension]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name]), - json={'policies': [self.mock_policy]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': [self.qos_extension]}), - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id, - 'bandwidth_limit_rules']), - json={'bandwidth_limit_rule': self.mock_rule}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': [self.qos_extension]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', self.policy_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name], + ), + json={'policies': [self.mock_policy]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': [self.qos_extension]}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'qos', + 'policies', + self.policy_id, + 'bandwidth_limit_rules', + ], + ), + json={'bandwidth_limit_rule': self.mock_rule}, + ), + ] + ) rule = self.cloud.create_qos_bandwidth_limit_rule( - self.policy_name, max_kbps=self.rule_max_kbps, direction="ingress") + self.policy_name, max_kbps=self.rule_max_kbps, direction="ingress" + ) self._compare_rules(self.mock_rule, rule) self.assert_calls() def test_update_qos_bandwidth_limit_rule(self): expected_rule = copy.copy(self.mock_rule) expected_rule['max_kbps'] = self.rule_max_kbps + 100 - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': [self.qos_extension]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id]), - json=self.mock_policy), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id, - 'bandwidth_limit_rules', self.rule_id]), - json={'bandwidth_limit_rule': self.mock_rule}), - dict(method='PUT', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id, - 'bandwidth_limit_rules', self.rule_id]), - json={'bandwidth_limit_rule': expected_rule}, - validate=dict( - json={'bandwidth_limit_rule': { - 'max_kbps': self.rule_max_kbps + 100}})) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': [self.qos_extension]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', self.policy_id], + ), + json=self.mock_policy, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'qos', + 'policies', + self.policy_id, + 'bandwidth_limit_rules', + self.rule_id, + ], + ), + json={'bandwidth_limit_rule': self.mock_rule}, + ), + dict( + method='PUT', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'qos', + 'policies', + self.policy_id, + 'bandwidth_limit_rules', + self.rule_id, + ], + ), + json={'bandwidth_limit_rule': expected_rule}, + validate=dict( + json={ + 'bandwidth_limit_rule': { + 'max_kbps': self.rule_max_kbps + 100 + } + } + ), + ), + ] + ) rule = self.cloud.update_qos_bandwidth_limit_rule( - self.policy_id, self.rule_id, max_kbps=self.rule_max_kbps + 100) + self.policy_id, self.rule_id, max_kbps=self.rule_max_kbps + 100 + ) self._compare_rules(expected_rule, rule) self.assert_calls() def test_update_qos_bandwidth_limit_rule_no_qos_extension(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': []}, + ) + ] + ) self.assertRaises( exc.OpenStackCloudException, self.cloud.update_qos_bandwidth_limit_rule, - self.policy_id, self.rule_id, max_kbps=2000) + self.policy_id, + self.rule_id, + max_kbps=2000, + ) self.assert_calls() def test_update_qos_bandwidth_limit_rule_no_qos_direction_extension(self): expected_rule = copy.copy(self.mock_rule) expected_rule['direction'] = self.rule_max_kbps + 100 - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': [self.qos_extension]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id]), - json=self.mock_policy), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': [self.qos_extension]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id, - 'bandwidth_limit_rules', self.rule_id]), - json={'bandwidth_limit_rule': self.mock_rule}), - dict(method='PUT', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id, - 'bandwidth_limit_rules', - self.rule_id]), - json={'bandwidth_limit_rule': expected_rule}, - validate=dict( - json={'bandwidth_limit_rule': { - 'max_kbps': self.rule_max_kbps + 100}})) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': [self.qos_extension]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', self.policy_id], + ), + json=self.mock_policy, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': [self.qos_extension]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'qos', + 'policies', + self.policy_id, + 'bandwidth_limit_rules', + self.rule_id, + ], + ), + json={'bandwidth_limit_rule': self.mock_rule}, + ), + dict( + method='PUT', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'qos', + 'policies', + self.policy_id, + 'bandwidth_limit_rules', + self.rule_id, + ], + ), + json={'bandwidth_limit_rule': expected_rule}, + validate=dict( + json={ + 'bandwidth_limit_rule': { + 'max_kbps': self.rule_max_kbps + 100 + } + } + ), + ), + ] + ) rule = self.cloud.update_qos_bandwidth_limit_rule( - self.policy_id, self.rule_id, max_kbps=self.rule_max_kbps + 100, - direction="ingress") + self.policy_id, + self.rule_id, + max_kbps=self.rule_max_kbps + 100, + direction="ingress", + ) # Even if there was attempt to change direction to 'ingress' it should # be not changed in returned rule self._compare_rules(expected_rule, rule) self.assert_calls() def test_delete_qos_bandwidth_limit_rule(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name]), - json={'policies': [self.mock_policy]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id, - 'bandwidth_limit_rules', self.rule_id]), - json={}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': self.enabled_neutron_extensions}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', self.policy_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name], + ), + json={'policies': [self.mock_policy]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'qos', + 'policies', + self.policy_id, + 'bandwidth_limit_rules', + self.rule_id, + ], + ), + json={}, + ), + ] + ) self.assertTrue( self.cloud.delete_qos_bandwidth_limit_rule( - self.policy_name, self.rule_id)) + self.policy_name, self.rule_id + ) + ) self.assert_calls() def test_delete_qos_bandwidth_limit_rule_no_qos_extension(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': []}, + ) + ] + ) self.assertRaises( exc.OpenStackCloudException, self.cloud.delete_qos_bandwidth_limit_rule, - self.policy_name, self.rule_id) + self.policy_name, + self.rule_id, + ) self.assert_calls() def test_delete_qos_bandwidth_limit_rule_not_found(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name]), - json={'policies': [self.mock_policy]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id, - 'bandwidth_limit_rules', self.rule_id]), - status_code=404) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': self.enabled_neutron_extensions}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', self.policy_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name], + ), + json={'policies': [self.mock_policy]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'qos', + 'policies', + self.policy_id, + 'bandwidth_limit_rules', + self.rule_id, + ], + ), + status_code=404, + ), + ] + ) self.assertFalse( self.cloud.delete_qos_bandwidth_limit_rule( - self.policy_name, self.rule_id)) + self.policy_name, self.rule_id + ) + ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py b/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py index 629b4cf7c..1b864b1a6 100644 --- a/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py +++ b/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py @@ -37,7 +37,7 @@ class TestQosDscpMarkingRule(base.TestCase): 'project_id': project_id, 'tenant_id': project_id, 'shared': False, - 'is_default': False + 'is_default': False, } mock_rule = { @@ -50,7 +50,7 @@ class TestQosDscpMarkingRule(base.TestCase): "name": "Quality of Service", "links": [], "alias": "qos", - "description": "The Quality of Service extension." + "description": "The Quality of Service extension.", } enabled_neutron_extensions = [qos_extension] @@ -58,233 +58,409 @@ class TestQosDscpMarkingRule(base.TestCase): def _compare_rules(self, exp, real): self.assertDictEqual( qos_dscp_marking_rule.QoSDSCPMarkingRule(**exp).to_dict( - computed=False), - real.to_dict(computed=False)) + computed=False + ), + real.to_dict(computed=False), + ) def test_get_qos_dscp_marking_rule(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name]), - json={'policies': [self.mock_policy]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id, - 'dscp_marking_rules', self.rule_id]), - json={'dscp_marking_rule': self.mock_rule}) - ]) - r = self.cloud.get_qos_dscp_marking_rule(self.policy_name, - self.rule_id) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': self.enabled_neutron_extensions}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', self.policy_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name], + ), + json={'policies': [self.mock_policy]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'qos', + 'policies', + self.policy_id, + 'dscp_marking_rules', + self.rule_id, + ], + ), + json={'dscp_marking_rule': self.mock_rule}, + ), + ] + ) + r = self.cloud.get_qos_dscp_marking_rule( + self.policy_name, self.rule_id + ) self._compare_rules(self.mock_rule, r) self.assert_calls() def test_get_qos_dscp_marking_rule_no_qos_policy_found(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name]), - json={'policies': []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': self.enabled_neutron_extensions}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', self.policy_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name], + ), + json={'policies': []}, + ), + ] + ) self.assertRaises( exc.OpenStackCloudResourceNotFound, self.cloud.get_qos_dscp_marking_rule, - self.policy_name, self.rule_id) + self.policy_name, + self.rule_id, + ) self.assert_calls() def test_get_qos_dscp_marking_rule_no_qos_extension(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': []}, + ) + ] + ) self.assertRaises( exc.OpenStackCloudException, self.cloud.get_qos_dscp_marking_rule, - self.policy_name, self.rule_id) + self.policy_name, + self.rule_id, + ) self.assert_calls() def test_create_qos_dscp_marking_rule(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name]), - json={'policies': [self.mock_policy]}), - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id, - 'dscp_marking_rules']), - json={'dscp_marking_rule': self.mock_rule}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': self.enabled_neutron_extensions}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', self.policy_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name], + ), + json={'policies': [self.mock_policy]}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'qos', + 'policies', + self.policy_id, + 'dscp_marking_rules', + ], + ), + json={'dscp_marking_rule': self.mock_rule}, + ), + ] + ) rule = self.cloud.create_qos_dscp_marking_rule( - self.policy_name, dscp_mark=self.rule_dscp_mark) + self.policy_name, dscp_mark=self.rule_dscp_mark + ) self._compare_rules(self.mock_rule, rule) self.assert_calls() def test_create_qos_dscp_marking_rule_no_qos_extension(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': []}, + ) + ] + ) self.assertRaises( exc.OpenStackCloudException, - self.cloud.create_qos_dscp_marking_rule, self.policy_name, - dscp_mark=16) + self.cloud.create_qos_dscp_marking_rule, + self.policy_name, + dscp_mark=16, + ) self.assert_calls() def test_update_qos_dscp_marking_rule(self): new_dscp_mark_value = 16 expected_rule = copy.copy(self.mock_rule) expected_rule['dscp_mark'] = new_dscp_mark_value - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id]), - json=self.mock_policy), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id, - 'dscp_marking_rules', self.rule_id]), - json={'dscp_marking_rule': self.mock_rule}), - dict(method='PUT', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id, - 'dscp_marking_rules', self.rule_id]), - json={'dscp_marking_rule': expected_rule}, - validate=dict( - json={'dscp_marking_rule': { - 'dscp_mark': new_dscp_mark_value}})) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': self.enabled_neutron_extensions}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', self.policy_id], + ), + json=self.mock_policy, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'qos', + 'policies', + self.policy_id, + 'dscp_marking_rules', + self.rule_id, + ], + ), + json={'dscp_marking_rule': self.mock_rule}, + ), + dict( + method='PUT', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'qos', + 'policies', + self.policy_id, + 'dscp_marking_rules', + self.rule_id, + ], + ), + json={'dscp_marking_rule': expected_rule}, + validate=dict( + json={ + 'dscp_marking_rule': { + 'dscp_mark': new_dscp_mark_value + } + } + ), + ), + ] + ) rule = self.cloud.update_qos_dscp_marking_rule( - self.policy_id, self.rule_id, dscp_mark=new_dscp_mark_value) + self.policy_id, self.rule_id, dscp_mark=new_dscp_mark_value + ) self._compare_rules(expected_rule, rule) self.assert_calls() def test_update_qos_dscp_marking_rule_no_qos_extension(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': []}, + ) + ] + ) self.assertRaises( exc.OpenStackCloudException, self.cloud.update_qos_dscp_marking_rule, - self.policy_id, self.rule_id, dscp_mark=8) + self.policy_id, + self.rule_id, + dscp_mark=8, + ) self.assert_calls() def test_delete_qos_dscp_marking_rule(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name]), - json={'policies': [self.mock_policy]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id, - 'dscp_marking_rules', - self.rule_id]), - json={}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': self.enabled_neutron_extensions}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', self.policy_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name], + ), + json={'policies': [self.mock_policy]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'qos', + 'policies', + self.policy_id, + 'dscp_marking_rules', + self.rule_id, + ], + ), + json={}, + ), + ] + ) self.assertTrue( self.cloud.delete_qos_dscp_marking_rule( - self.policy_name, self.rule_id)) + self.policy_name, self.rule_id + ) + ) self.assert_calls() def test_delete_qos_dscp_marking_rule_no_qos_extension(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': []}, + ) + ] + ) self.assertRaises( exc.OpenStackCloudException, self.cloud.delete_qos_dscp_marking_rule, - self.policy_name, self.rule_id) + self.policy_name, + self.rule_id, + ) self.assert_calls() def test_delete_qos_dscp_marking_rule_not_found(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name]), - json={'policies': [self.mock_policy]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id, - 'dscp_marking_rules', self.rule_id]), - status_code=404) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': self.enabled_neutron_extensions}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', self.policy_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name], + ), + json={'policies': [self.mock_policy]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'qos', + 'policies', + self.policy_id, + 'dscp_marking_rules', + self.rule_id, + ], + ), + status_code=404, + ), + ] + ) self.assertFalse( self.cloud.delete_qos_dscp_marking_rule( - self.policy_name, self.rule_id)) + self.policy_name, self.rule_id + ) + ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py b/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py index 6ee89a8ca..411d6d18f 100644 --- a/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py +++ b/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py @@ -37,13 +37,13 @@ class TestQosMinimumBandwidthRule(base.TestCase): 'project_id': project_id, 'tenant_id': project_id, 'shared': False, - 'is_default': False + 'is_default': False, } mock_rule = { 'id': rule_id, 'min_kbps': rule_min_kbps, - 'direction': 'egress' + 'direction': 'egress', } qos_extension = { @@ -51,240 +51,416 @@ class TestQosMinimumBandwidthRule(base.TestCase): "name": "Quality of Service", "links": [], "alias": "qos", - "description": "The Quality of Service extension." + "description": "The Quality of Service extension.", } enabled_neutron_extensions = [qos_extension] def _compare_rules(self, exp, real): self.assertDictEqual( - qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule( - **exp).to_dict(computed=False), - real.to_dict(computed=False)) + qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule(**exp).to_dict( + computed=False + ), + real.to_dict(computed=False), + ) def test_get_qos_minimum_bandwidth_rule(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name]), - json={'policies': [self.mock_policy]}), - - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id, - 'minimum_bandwidth_rules', self.rule_id]), - json={'minimum_bandwidth_rule': self.mock_rule}) - ]) - r = self.cloud.get_qos_minimum_bandwidth_rule(self.policy_name, - self.rule_id) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': self.enabled_neutron_extensions}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', self.policy_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name], + ), + json={'policies': [self.mock_policy]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'qos', + 'policies', + self.policy_id, + 'minimum_bandwidth_rules', + self.rule_id, + ], + ), + json={'minimum_bandwidth_rule': self.mock_rule}, + ), + ] + ) + r = self.cloud.get_qos_minimum_bandwidth_rule( + self.policy_name, self.rule_id + ) self._compare_rules(self.mock_rule, r) self.assert_calls() def test_get_qos_minimum_bandwidth_rule_no_qos_policy_found(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name]), - json={'policies': []}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': self.enabled_neutron_extensions}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', self.policy_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name], + ), + json={'policies': []}, + ), + ] + ) self.assertRaises( exc.OpenStackCloudResourceNotFound, self.cloud.get_qos_minimum_bandwidth_rule, - self.policy_name, self.rule_id) + self.policy_name, + self.rule_id, + ) self.assert_calls() def test_get_qos_minimum_bandwidth_rule_no_qos_extension(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': []}, + ) + ] + ) self.assertRaises( exc.OpenStackCloudException, self.cloud.get_qos_minimum_bandwidth_rule, - self.policy_name, self.rule_id) + self.policy_name, + self.rule_id, + ) self.assert_calls() def test_create_qos_minimum_bandwidth_rule(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name]), - json={'policies': [self.mock_policy]}), - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id, - 'minimum_bandwidth_rules']), - json={'minimum_bandwidth_rule': self.mock_rule}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': self.enabled_neutron_extensions}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', self.policy_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name], + ), + json={'policies': [self.mock_policy]}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'qos', + 'policies', + self.policy_id, + 'minimum_bandwidth_rules', + ], + ), + json={'minimum_bandwidth_rule': self.mock_rule}, + ), + ] + ) rule = self.cloud.create_qos_minimum_bandwidth_rule( - self.policy_name, min_kbps=self.rule_min_kbps) + self.policy_name, min_kbps=self.rule_min_kbps + ) self._compare_rules(self.mock_rule, rule) self.assert_calls() def test_create_qos_minimum_bandwidth_rule_no_qos_extension(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': []}, + ) + ] + ) self.assertRaises( exc.OpenStackCloudException, - self.cloud.create_qos_minimum_bandwidth_rule, self.policy_name, - min_kbps=100) + self.cloud.create_qos_minimum_bandwidth_rule, + self.policy_name, + min_kbps=100, + ) self.assert_calls() def test_update_qos_minimum_bandwidth_rule(self): expected_rule = copy.copy(self.mock_rule) expected_rule['min_kbps'] = self.rule_min_kbps + 100 - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id]), - json=self.mock_policy), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id, - 'minimum_bandwidth_rules', self.rule_id]), - json={'minimum_bandwidth_rule': self.mock_rule}), - dict(method='PUT', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id, - 'minimum_bandwidth_rules', self.rule_id]), - json={'minimum_bandwidth_rule': expected_rule}, - validate=dict( - json={'minimum_bandwidth_rule': { - 'min_kbps': self.rule_min_kbps + 100}})) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': self.enabled_neutron_extensions}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', self.policy_id], + ), + json=self.mock_policy, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'qos', + 'policies', + self.policy_id, + 'minimum_bandwidth_rules', + self.rule_id, + ], + ), + json={'minimum_bandwidth_rule': self.mock_rule}, + ), + dict( + method='PUT', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'qos', + 'policies', + self.policy_id, + 'minimum_bandwidth_rules', + self.rule_id, + ], + ), + json={'minimum_bandwidth_rule': expected_rule}, + validate=dict( + json={ + 'minimum_bandwidth_rule': { + 'min_kbps': self.rule_min_kbps + 100 + } + } + ), + ), + ] + ) rule = self.cloud.update_qos_minimum_bandwidth_rule( - self.policy_id, self.rule_id, min_kbps=self.rule_min_kbps + 100) + self.policy_id, self.rule_id, min_kbps=self.rule_min_kbps + 100 + ) self._compare_rules(expected_rule, rule) self.assert_calls() def test_update_qos_minimum_bandwidth_rule_no_qos_extension(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': []}, + ) + ] + ) self.assertRaises( exc.OpenStackCloudException, self.cloud.update_qos_minimum_bandwidth_rule, - self.policy_id, self.rule_id, min_kbps=2000) + self.policy_id, + self.rule_id, + min_kbps=2000, + ) self.assert_calls() def test_delete_qos_minimum_bandwidth_rule(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name]), - json={'policies': [self.mock_policy]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id, - 'minimum_bandwidth_rules', self.rule_id]), - json={}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': self.enabled_neutron_extensions}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', self.policy_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name], + ), + json={'policies': [self.mock_policy]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'qos', + 'policies', + self.policy_id, + 'minimum_bandwidth_rules', + self.rule_id, + ], + ), + json={}, + ), + ] + ) self.assertTrue( self.cloud.delete_qos_minimum_bandwidth_rule( - self.policy_name, self.rule_id)) + self.policy_name, self.rule_id + ) + ) self.assert_calls() def test_delete_qos_minimum_bandwidth_rule_no_qos_extension(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': []}, + ) + ] + ) self.assertRaises( exc.OpenStackCloudException, self.cloud.delete_qos_minimum_bandwidth_rule, - self.policy_name, self.rule_id) + self.policy_name, + self.rule_id, + ) self.assert_calls() def test_delete_qos_minimum_bandwidth_rule_not_found(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name]), - json={'policies': [self.mock_policy]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id, - 'minimum_bandwidth_rules', self.rule_id]), - status_code=404) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': self.enabled_neutron_extensions}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', self.policy_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name], + ), + json={'policies': [self.mock_policy]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'qos', + 'policies', + self.policy_id, + 'minimum_bandwidth_rules', + self.rule_id, + ], + ), + status_code=404, + ), + ] + ) self.assertFalse( self.cloud.delete_qos_minimum_bandwidth_rule( - self.policy_name, self.rule_id)) + self.policy_name, self.rule_id + ) + ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_qos_policy.py b/openstack/tests/unit/cloud/test_qos_policy.py index 70da59daa..7bfd77334 100644 --- a/openstack/tests/unit/cloud/test_qos_policy.py +++ b/openstack/tests/unit/cloud/test_qos_policy.py @@ -43,7 +43,7 @@ class TestQosPolicy(base.TestCase): "name": "Quality of Service", "links": [], "alias": "qos", - "description": "The Quality of Service extension." + "description": "The Quality of Service extension.", } qos_default_extension = { @@ -51,7 +51,7 @@ class TestQosPolicy(base.TestCase): "name": "QoS default policy", "links": [], "alias": "qos-default", - "description": "Expose the QoS default policy per project" + "description": "Expose the QoS default policy per project", } enabled_neutron_extensions = [qos_extension, qos_default_extension] @@ -59,269 +59,417 @@ class TestQosPolicy(base.TestCase): def _compare_policies(self, exp, real): self.assertDictEqual( _policy.QoSPolicy(**exp).to_dict(computed=False), - real.to_dict(computed=False)) + real.to_dict(computed=False), + ) def test_get_qos_policy(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', - 'policies', self.policy_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name]), - json={'policies': [self.mock_policy]}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': self.enabled_neutron_extensions}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', self.policy_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name], + ), + json={'policies': [self.mock_policy]}, + ), + ] + ) r = self.cloud.get_qos_policy(self.policy_name) self.assertIsNotNone(r) self._compare_policies(self.mock_policy, r) self.assert_calls() def test_get_qos_policy_no_qos_extension(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': []}, + ) + ] + ) self.assertRaises( exc.OpenStackCloudException, - self.cloud.get_qos_policy, self.policy_name) + self.cloud.get_qos_policy, + self.policy_name, + ) self.assert_calls() def test_create_qos_policy(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies']), - json={'policy': self.mock_policy}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': self.enabled_neutron_extensions}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'qos', 'policies'] + ), + json={'policy': self.mock_policy}, + ), + ] + ) policy = self.cloud.create_qos_policy( - name=self.policy_name, project_id=self.project_id) + name=self.policy_name, project_id=self.project_id + ) self._compare_policies(self.mock_policy, policy) self.assert_calls() def test_create_qos_policy_no_qos_extension(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': []}, + ) + ] + ) self.assertRaises( exc.OpenStackCloudException, - self.cloud.create_qos_policy, name=self.policy_name) + self.cloud.create_qos_policy, + name=self.policy_name, + ) self.assert_calls() def test_create_qos_policy_no_qos_default_extension(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': [self.qos_extension]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': [self.qos_extension]}), - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies']), - json={'policy': self.mock_policy}, - validate=dict( - json={'policy': { - 'name': self.policy_name, - 'project_id': self.project_id}})) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': [self.qos_extension]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': [self.qos_extension]}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'qos', 'policies'] + ), + json={'policy': self.mock_policy}, + validate=dict( + json={ + 'policy': { + 'name': self.policy_name, + 'project_id': self.project_id, + } + } + ), + ), + ] + ) policy = self.cloud.create_qos_policy( - name=self.policy_name, project_id=self.project_id, default=True) + name=self.policy_name, project_id=self.project_id, default=True + ) self._compare_policies(self.mock_policy, policy) self.assert_calls() def test_delete_qos_policy(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', - self.policy_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name]), - json={'policies': [self.mock_policy]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id]), - json={}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': self.enabled_neutron_extensions}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', self.policy_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name], + ), + json={'policies': [self.mock_policy]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', self.policy_id], + ), + json={}, + ), + ] + ) self.assertTrue(self.cloud.delete_qos_policy(self.policy_name)) self.assert_calls() def test_delete_qos_policy_no_qos_extension(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': []}, + ) + ] + ) self.assertRaises( exc.OpenStackCloudException, - self.cloud.delete_qos_policy, self.policy_name) + self.cloud.delete_qos_policy, + self.policy_name, + ) self.assert_calls() def test_delete_qos_policy_not_found(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', 'goofy']), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies'], - qs_elements=['name=goofy']), - json={'policies': []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': self.enabled_neutron_extensions}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', 'goofy'], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=goofy'], + ), + json={'policies': []}, + ), + ] + ) self.assertFalse(self.cloud.delete_qos_policy('goofy')) self.assert_calls() def test_delete_qos_policy_multiple_found(self): policy1 = dict(id='123', name=self.policy_name) policy2 = dict(id='456', name=self.policy_name) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', - self.policy_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name]), - json={'policies': [policy1, policy2]}), - ]) - self.assertRaises(exc.OpenStackCloudException, - self.cloud.delete_qos_policy, - self.policy_name) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': self.enabled_neutron_extensions}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', self.policy_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies'], + qs_elements=['name=%s' % self.policy_name], + ), + json={'policies': [policy1, policy2]}, + ), + ] + ) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.delete_qos_policy, + self.policy_name, + ) self.assert_calls() def test_delete_qos_policy_using_id(self): policy1 = self.mock_policy - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', policy1['id']]), - json=policy1), - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id]), - json={}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': self.enabled_neutron_extensions}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', policy1['id']], + ), + json=policy1, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', self.policy_id], + ), + json={}, + ), + ] + ) self.assertTrue(self.cloud.delete_qos_policy(policy1['id'])) self.assert_calls() def test_update_qos_policy(self): expected_policy = copy.copy(self.mock_policy) expected_policy['name'] = 'goofy' - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id]), - json=self.mock_policy), - dict(method='PUT', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id]), - json={'policy': expected_policy}, - validate=dict( - json={'policy': {'name': 'goofy'}})) - ]) - policy = self.cloud.update_qos_policy( - self.policy_id, name='goofy') + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': self.enabled_neutron_extensions}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', self.policy_id], + ), + json=self.mock_policy, + ), + dict( + method='PUT', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', self.policy_id], + ), + json={'policy': expected_policy}, + validate=dict(json={'policy': {'name': 'goofy'}}), + ), + ] + ) + policy = self.cloud.update_qos_policy(self.policy_id, name='goofy') self._compare_policies(expected_policy, policy) self.assert_calls() def test_update_qos_policy_no_qos_extension(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': []}, + ) + ] + ) self.assertRaises( exc.OpenStackCloudException, - self.cloud.update_qos_policy, self.policy_id, name="goofy") + self.cloud.update_qos_policy, + self.policy_id, + name="goofy", + ) self.assert_calls() def test_update_qos_policy_no_qos_default_extension(self): expected_policy = copy.copy(self.mock_policy) expected_policy['name'] = 'goofy' - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': [self.qos_extension]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': [self.qos_extension]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id]), - json=self.mock_policy), - dict(method='PUT', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'policies', self.policy_id]), - json={'policy': expected_policy}, - validate=dict( - json={'policy': {'name': "goofy"}})) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': [self.qos_extension]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': [self.qos_extension]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', self.policy_id], + ), + json=self.mock_policy, + ), + dict( + method='PUT', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'policies', self.policy_id], + ), + json={'policy': expected_policy}, + validate=dict(json={'policy': {'name': "goofy"}}), + ), + ] + ) policy = self.cloud.update_qos_policy( - self.policy_id, name='goofy', default=True) + self.policy_id, name='goofy', default=True + ) self._compare_policies(expected_policy, policy) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_qos_rule_type.py b/openstack/tests/unit/cloud/test_qos_rule_type.py index 91244f8d7..878e162f1 100644 --- a/openstack/tests/unit/cloud/test_qos_rule_type.py +++ b/openstack/tests/unit/cloud/test_qos_rule_type.py @@ -27,131 +27,192 @@ class TestQosRuleType(base.TestCase): "name": "Quality of Service", "links": [], "alias": "qos", - "description": "The Quality of Service extension." + "description": "The Quality of Service extension.", } qos_rule_type_details_extension = { "updated": "2017-06-22T10:00:00-00:00", "name": "Details of QoS rule types", "links": [], "alias": "qos-rule-type-details", - "description": ("Expose details about QoS rule types supported by " - "loaded backend drivers") + "description": ( + "Expose details about QoS rule types supported by " + "loaded backend drivers" + ), } - mock_rule_type_bandwidth_limit = { - 'type': 'bandwidth_limit' - } - mock_rule_type_dscp_marking = { - 'type': 'dscp_marking' - } + mock_rule_type_bandwidth_limit = {'type': 'bandwidth_limit'} + mock_rule_type_dscp_marking = {'type': 'dscp_marking'} mock_rule_types = [ - mock_rule_type_bandwidth_limit, mock_rule_type_dscp_marking] + mock_rule_type_bandwidth_limit, + mock_rule_type_dscp_marking, + ] mock_rule_type_details = { - 'drivers': [{ - 'name': 'linuxbridge', - 'supported_parameters': [{ - 'parameter_values': {'start': 0, 'end': 2147483647}, - 'parameter_type': 'range', - 'parameter_name': u'max_kbps' - }, { - 'parameter_values': ['ingress', 'egress'], - 'parameter_type': 'choices', - 'parameter_name': u'direction' - }, { - 'parameter_values': {'start': 0, 'end': 2147483647}, - 'parameter_type': 'range', - 'parameter_name': 'max_burst_kbps' - }] - }], - 'type': rule_type_name + 'drivers': [ + { + 'name': 'linuxbridge', + 'supported_parameters': [ + { + 'parameter_values': {'start': 0, 'end': 2147483647}, + 'parameter_type': 'range', + 'parameter_name': u'max_kbps', + }, + { + 'parameter_values': ['ingress', 'egress'], + 'parameter_type': 'choices', + 'parameter_name': u'direction', + }, + { + 'parameter_values': {'start': 0, 'end': 2147483647}, + 'parameter_type': 'range', + 'parameter_name': 'max_burst_kbps', + }, + ], + } + ], + 'type': rule_type_name, } def _compare_rule_types(self, exp, real): self.assertDictEqual( qos_rule_type.QoSRuleType(**exp).to_dict(computed=False), - real.to_dict(computed=False)) + real.to_dict(computed=False), + ) def test_list_qos_rule_types(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': [self.qos_extension]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'rule-types']), - json={'rule_types': self.mock_rule_types}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': [self.qos_extension]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'qos', 'rule-types'], + ), + json={'rule_types': self.mock_rule_types}, + ), + ] + ) rule_types = self.cloud.list_qos_rule_types() for a, b in zip(self.mock_rule_types, rule_types): self._compare_rule_types(a, b) self.assert_calls() def test_list_qos_rule_types_no_qos_extension(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': []}) - ]) - self.assertRaises(exc.OpenStackCloudException, - self.cloud.list_qos_rule_types) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': []}, + ) + ] + ) + self.assertRaises( + exc.OpenStackCloudException, self.cloud.list_qos_rule_types + ) self.assert_calls() def test_get_qos_rule_type_details(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': [ - self.qos_extension, - self.qos_rule_type_details_extension]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': [ - self.qos_extension, - self.qos_rule_type_details_extension]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'qos', 'rule-types', - self.rule_type_name]), - json={'rule_type': self.mock_rule_type_details}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={ + 'extensions': [ + self.qos_extension, + self.qos_rule_type_details_extension, + ] + }, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={ + 'extensions': [ + self.qos_extension, + self.qos_rule_type_details_extension, + ] + }, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'qos', + 'rule-types', + self.rule_type_name, + ], + ), + json={'rule_type': self.mock_rule_type_details}, + ), + ] + ) self._compare_rule_types( self.mock_rule_type_details, - self.cloud.get_qos_rule_type_details(self.rule_type_name) + self.cloud.get_qos_rule_type_details(self.rule_type_name), ) self.assert_calls() def test_get_qos_rule_type_details_no_qos_extension(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': []}, + ) + ] + ) self.assertRaises( exc.OpenStackCloudException, - self.cloud.get_qos_rule_type_details, self.rule_type_name) + self.cloud.get_qos_rule_type_details, + self.rule_type_name, + ) self.assert_calls() def test_get_qos_rule_type_details_no_qos_details_extension(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': [self.qos_extension]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': [self.qos_extension]}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': [self.qos_extension]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': [self.qos_extension]}, + ), + ] + ) self.assertRaises( exc.OpenStackCloudException, - self.cloud.get_qos_rule_type_details, self.rule_type_name) + self.cloud.get_qos_rule_type_details, + self.rule_type_name, + ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_quotas.py b/openstack/tests/unit/cloud/test_quotas.py index 00da3985d..3ea399919 100644 --- a/openstack/tests/unit/cloud/test_quotas.py +++ b/openstack/tests/unit/cloud/test_quotas.py @@ -28,37 +28,45 @@ "security_group_rules": 20, "security_groups": 45, "server_groups": 10, - "server_group_members": 10 + "server_group_members": 10, } class TestQuotas(base.TestCase): def setUp(self, cloud_config_fixture='clouds.yaml'): super(TestQuotas, self).setUp( - cloud_config_fixture=cloud_config_fixture) + cloud_config_fixture=cloud_config_fixture + ) def test_update_quotas(self): project = self._get_project_data() - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'identity', 'public', - append=['v3', 'projects', project.project_id]), - json={'project': project.json_response['project']}), - self.get_nova_discovery_mock_dict(), - dict(method='PUT', - uri=self.get_mock_url( - 'compute', 'public', - append=['os-quota-sets', project.project_id]), - json={'quota_set': fake_quota_set}, - validate=dict( - json={ - 'quota_set': { - 'cores': 1, - 'force': True - }})), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'identity', + 'public', + append=['v3', 'projects', project.project_id], + ), + json={'project': project.json_response['project']}, + ), + self.get_nova_discovery_mock_dict(), + dict( + method='PUT', + uri=self.get_mock_url( + 'compute', + 'public', + append=['os-quota-sets', project.project_id], + ), + json={'quota_set': fake_quota_set}, + validate=dict( + json={'quota_set': {'cores': 1, 'force': True}} + ), + ), + ] + ) self.cloud.set_compute_quotas(project.project_id, cores=1) @@ -67,41 +75,64 @@ def test_update_quotas(self): def test_update_quotas_bad_request(self): project = self._get_project_data() - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'identity', 'public', - append=['v3', 'projects', project.project_id]), - json={'project': project.json_response['project']}), - self.get_nova_discovery_mock_dict(), - dict(method='PUT', - uri=self.get_mock_url( - 'compute', 'public', - append=['os-quota-sets', project.project_id]), - status_code=400), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'identity', + 'public', + append=['v3', 'projects', project.project_id], + ), + json={'project': project.json_response['project']}, + ), + self.get_nova_discovery_mock_dict(), + dict( + method='PUT', + uri=self.get_mock_url( + 'compute', + 'public', + append=['os-quota-sets', project.project_id], + ), + status_code=400, + ), + ] + ) - self.assertRaises(exc.OpenStackCloudException, - self.cloud.set_compute_quotas, project.project_id) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.set_compute_quotas, + project.project_id, + ) self.assert_calls() def test_get_quotas(self): project = self._get_project_data() - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'identity', 'public', - append=['v3', 'projects', project.project_id]), - json={'project': project.json_response['project']}), - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', - append=['os-quota-sets', project.project_id], - qs_elements=['usage=False']), - json={'quota_set': fake_quota_set}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'identity', + 'public', + append=['v3', 'projects', project.project_id], + ), + json={'project': project.json_response['project']}, + ), + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['os-quota-sets', project.project_id], + qs_elements=['usage=False'], + ), + json={'quota_set': fake_quota_set}, + ), + ] + ) self.cloud.get_compute_quotas(project.project_id) @@ -110,18 +141,28 @@ def test_get_quotas(self): def test_delete_quotas(self): project = self._get_project_data() - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'identity', 'public', - append=['v3', 'projects', project.project_id]), - json={'project': project.json_response['project']}), - self.get_nova_discovery_mock_dict(), - dict(method='DELETE', - uri=self.get_mock_url( - 'compute', 'public', - append=['os-quota-sets', project.project_id])), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'identity', + 'public', + append=['v3', 'projects', project.project_id], + ), + json={'project': project.json_response['project']}, + ), + self.get_nova_discovery_mock_dict(), + dict( + method='DELETE', + uri=self.get_mock_url( + 'compute', + 'public', + append=['os-quota-sets', project.project_id], + ), + ), + ] + ) self.cloud.delete_compute_quotas(project.project_id) @@ -130,72 +171,109 @@ def test_delete_quotas(self): def test_cinder_update_quotas(self): project = self._get_project_data() - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'identity', 'public', - append=['v3', 'projects', project.project_id]), - json={'project': project.json_response['project']}), - self.get_cinder_discovery_mock_dict(), - dict(method='PUT', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['os-quota-sets', project.project_id]), - json=dict(quota_set={'volumes': 1}), - validate=dict( - json={'quota_set': { - 'volumes': 1}}))]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'identity', + 'public', + append=['v3', 'projects', project.project_id], + ), + json={'project': project.json_response['project']}, + ), + self.get_cinder_discovery_mock_dict(), + dict( + method='PUT', + uri=self.get_mock_url( + 'volumev3', + 'public', + append=['os-quota-sets', project.project_id], + ), + json=dict(quota_set={'volumes': 1}), + validate=dict(json={'quota_set': {'volumes': 1}}), + ), + ] + ) self.cloud.set_volume_quotas(project.project_id, volumes=1) self.assert_calls() def test_cinder_get_quotas(self): project = self._get_project_data() - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'identity', 'public', - append=['v3', 'projects', project.project_id]), - json={'project': project.json_response['project']}), - self.get_cinder_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['os-quota-sets', project.project_id], - qs_elements=['usage=False']), - json=dict(quota_set={'snapshots': 10, 'volumes': 20}))]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'identity', + 'public', + append=['v3', 'projects', project.project_id], + ), + json={'project': project.json_response['project']}, + ), + self.get_cinder_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', + 'public', + append=['os-quota-sets', project.project_id], + qs_elements=['usage=False'], + ), + json=dict(quota_set={'snapshots': 10, 'volumes': 20}), + ), + ] + ) self.cloud.get_volume_quotas(project.project_id) self.assert_calls() def test_cinder_delete_quotas(self): project = self._get_project_data() - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'identity', 'public', - append=['v3', 'projects', project.project_id]), - json={'project': project.json_response['project']}), - self.get_cinder_discovery_mock_dict(), - dict(method='DELETE', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['os-quota-sets', project.project_id]))]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'identity', + 'public', + append=['v3', 'projects', project.project_id], + ), + json={'project': project.json_response['project']}, + ), + self.get_cinder_discovery_mock_dict(), + dict( + method='DELETE', + uri=self.get_mock_url( + 'volumev3', + 'public', + append=['os-quota-sets', project.project_id], + ), + ), + ] + ) self.cloud.delete_volume_quotas(project.project_id) self.assert_calls() def test_neutron_update_quotas(self): - project = self.mock_for_keystone_projects(project_count=1, - list_get=True)[0] - self.register_uris([ - dict(method='PUT', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'quotas', project.project_id]), - json={}, - validate=dict( - json={'quota': {'network': 1}})) - ]) + project = self.mock_for_keystone_projects( + project_count=1, list_get=True + )[0] + self.register_uris( + [ + dict( + method='PUT', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'quotas', project.project_id], + ), + json={}, + validate=dict(json={'quota': {'network': 1}}), + ) + ] + ) self.cloud.set_network_quotas(project.project_id, network=1) self.assert_calls() @@ -209,19 +287,27 @@ def test_neutron_get_quotas(self): 'security_group': 10, 'router': 10, 'rbac_policy': 10, - 'port': 500 + 'port': 500, } - project = self.mock_for_keystone_projects(project_count=1, - id_get=True)[0] - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'quotas', project.project_id]), - json={'quota': quota}) - ]) + project = self.mock_for_keystone_projects( + project_count=1, id_get=True + )[0] + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'quotas', project.project_id], + ), + json={'quota': quota}, + ) + ] + ) received_quota = self.cloud.get_network_quotas( - project.project_id).to_dict(computed=False) + project.project_id + ).to_dict(computed=False) expected_quota = _quota.Quota(**quota).to_dict(computed=False) received_quota.pop('id') received_quota.pop('name') @@ -234,69 +320,62 @@ def test_neutron_get_quotas(self): def test_neutron_get_quotas_details(self): quota_details = { - 'subnet': { - 'limit': 100, - 'used': 7, - 'reserved': 0}, - 'network': { - 'limit': 100, - 'used': 6, - 'reserved': 0}, - 'floatingip': { - 'limit': 50, - 'used': 0, - 'reserved': 0}, - 'subnetpool': { - 'limit': -1, - 'used': 2, - 'reserved': 0}, - 'security_group_rule': { - 'limit': 100, - 'used': 4, - 'reserved': 0}, - 'security_group': { - 'limit': 10, - 'used': 1, - 'reserved': 0}, - 'router': { - 'limit': 10, - 'used': 2, - 'reserved': 0}, - 'rbac_policy': { - 'limit': 10, - 'used': 2, - 'reserved': 0}, - 'port': { - 'limit': 500, - 'used': 7, - 'reserved': 0} + 'subnet': {'limit': 100, 'used': 7, 'reserved': 0}, + 'network': {'limit': 100, 'used': 6, 'reserved': 0}, + 'floatingip': {'limit': 50, 'used': 0, 'reserved': 0}, + 'subnetpool': {'limit': -1, 'used': 2, 'reserved': 0}, + 'security_group_rule': {'limit': 100, 'used': 4, 'reserved': 0}, + 'security_group': {'limit': 10, 'used': 1, 'reserved': 0}, + 'router': {'limit': 10, 'used': 2, 'reserved': 0}, + 'rbac_policy': {'limit': 10, 'used': 2, 'reserved': 0}, + 'port': {'limit': 500, 'used': 7, 'reserved': 0}, } - project = self.mock_for_keystone_projects(project_count=1, - id_get=True)[0] - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'quotas', - project.project_id, 'details']), - json={'quota': quota_details}) - ]) + project = self.mock_for_keystone_projects( + project_count=1, id_get=True + )[0] + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'quotas', + project.project_id, + 'details', + ], + ), + json={'quota': quota_details}, + ) + ] + ) received_quota_details = self.cloud.get_network_quotas( - project.project_id, details=True) + project.project_id, details=True + ) self.assertDictEqual( _quota.QuotaDetails(**quota_details).to_dict(computed=False), - received_quota_details.to_dict(computed=False)) + received_quota_details.to_dict(computed=False), + ) self.assert_calls() def test_neutron_delete_quotas(self): - project = self.mock_for_keystone_projects(project_count=1, - list_get=True)[0] - self.register_uris([ - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'quotas', project.project_id]), - json={}) - ]) + project = self.mock_for_keystone_projects( + project_count=1, list_get=True + )[0] + self.register_uris( + [ + dict( + method='DELETE', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'quotas', project.project_id], + ), + json={}, + ) + ] + ) self.cloud.delete_network_quotas(project.project_id) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_rebuild_server.py b/openstack/tests/unit/cloud/test_rebuild_server.py index 460d79aa9..cba4101db 100644 --- a/openstack/tests/unit/cloud/test_rebuild_server.py +++ b/openstack/tests/unit/cloud/test_rebuild_server.py @@ -27,41 +27,50 @@ class TestRebuildServer(base.TestCase): - def setUp(self): super(TestRebuildServer, self).setUp() self.server_id = str(uuid.uuid4()) self.server_name = self.getUniqueString('name') self.fake_server = fakes.make_fake_server( - self.server_id, self.server_name) + self.server_id, self.server_name + ) self.rebuild_server = fakes.make_fake_server( - self.server_id, self.server_name, 'REBUILD') + self.server_id, self.server_name, 'REBUILD' + ) self.error_server = fakes.make_fake_server( - self.server_id, self.server_name, 'ERROR') + self.server_id, self.server_name, 'ERROR' + ) def test_rebuild_server_rebuild_exception(self): """ Test that an exception in the rebuild raises an exception in rebuild_server. """ - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', self.server_id, 'action']), - status_code=400, - validate=dict( - json={ - 'rebuild': { - 'imageRef': 'a', - 'adminPass': 'b'}})), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', self.server_id, 'action'], + ), + status_code=400, + validate=dict( + json={'rebuild': {'imageRef': 'a', 'adminPass': 'b'}} + ), + ), + ] + ) self.assertRaises( exc.OpenStackCloudException, self.cloud.rebuild_server, - self.fake_server['id'], "a", "b") + self.fake_server['id'], + "a", + "b", + ) self.assert_calls() @@ -70,25 +79,35 @@ def test_rebuild_server_server_error(self): Test that a server error while waiting for the server to rebuild raises an exception in rebuild_server. """ - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', self.server_id, 'action']), - json={'server': self.rebuild_server}, - validate=dict( - json={ - 'rebuild': { - 'imageRef': 'a'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', self.server_id]), - json={'server': self.error_server}), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', self.server_id, 'action'], + ), + json={'server': self.rebuild_server}, + validate=dict(json={'rebuild': {'imageRef': 'a'}}), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', self.server_id] + ), + json={'server': self.error_server}, + ), + ] + ) self.assertRaises( exc.OpenStackCloudException, - self.cloud.rebuild_server, self.fake_server['id'], "a", wait=True) + self.cloud.rebuild_server, + self.fake_server['id'], + "a", + wait=True, + ) self.assert_calls() @@ -97,26 +116,36 @@ def test_rebuild_server_timeout(self): Test that a timeout while waiting for the server to rebuild raises an exception in rebuild_server. """ - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', self.server_id, 'action']), - json={'server': self.rebuild_server}, - validate=dict( - json={ - 'rebuild': { - 'imageRef': 'a'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', self.server_id]), - json={'server': self.rebuild_server}), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', self.server_id, 'action'], + ), + json={'server': self.rebuild_server}, + validate=dict(json={'rebuild': {'imageRef': 'a'}}), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', self.server_id] + ), + json={'server': self.rebuild_server}, + ), + ] + ) self.assertRaises( exc.OpenStackCloudTimeout, self.cloud.rebuild_server, - self.fake_server['id'], "a", wait=True, timeout=0.001) + self.fake_server['id'], + "a", + wait=True, + timeout=0.001, + ) self.assert_calls(do_count=False) @@ -125,25 +154,32 @@ def test_rebuild_server_no_wait(self): Test that rebuild_server with no wait and no exception in the rebuild call returns the server instance. """ - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', self.server_id, 'action']), - json={'server': self.rebuild_server}, - validate=dict( - json={ - 'rebuild': { - 'imageRef': 'a'}})), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': []}), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', self.server_id, 'action'], + ), + json={'server': self.rebuild_server}, + validate=dict(json={'rebuild': {'imageRef': 'a'}}), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': []}, + ), + ] + ) self.assertEqual( self.rebuild_server['status'], - self.cloud.rebuild_server(self.fake_server['id'], "a")['status']) + self.cloud.rebuild_server(self.fake_server['id'], "a")['status'], + ) self.assert_calls() @@ -155,28 +191,38 @@ def test_rebuild_server_with_admin_pass_no_wait(self): rebuild_server = self.rebuild_server.copy() rebuild_server['adminPass'] = password - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', self.server_id, 'action']), - json={'server': rebuild_server}, - validate=dict( - json={ - 'rebuild': { - 'imageRef': 'a', - 'adminPass': password}})), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': []}), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', self.server_id, 'action'], + ), + json={'server': rebuild_server}, + validate=dict( + json={ + 'rebuild': {'imageRef': 'a', 'adminPass': password} + } + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': []}, + ), + ] + ) self.assertEqual( password, self.cloud.rebuild_server( - self.fake_server['id'], 'a', - admin_pass=password)['adminPass']) + self.fake_server['id'], 'a', admin_pass=password + )['adminPass'], + ) self.assert_calls() @@ -188,37 +234,53 @@ def test_rebuild_server_with_admin_pass_wait(self): rebuild_server = self.rebuild_server.copy() rebuild_server['adminPass'] = password - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', self.server_id, 'action']), - json={'server': rebuild_server}, - validate=dict( - json={ - 'rebuild': { - 'imageRef': 'a', - 'adminPass': password}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', self.server_id]), - json={'server': self.rebuild_server}), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', self.server_id]), - json={'server': self.fake_server}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': []}), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', self.server_id, 'action'], + ), + json={'server': rebuild_server}, + validate=dict( + json={ + 'rebuild': {'imageRef': 'a', 'adminPass': password} + } + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', self.server_id] + ), + json={'server': self.rebuild_server}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', self.server_id] + ), + json={'server': self.fake_server}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': []}, + ), + ] + ) self.assertEqual( password, self.cloud.rebuild_server( - self.fake_server['id'], 'a', - admin_pass=password, wait=True)['adminPass']) + self.fake_server['id'], 'a', admin_pass=password, wait=True + )['adminPass'], + ) self.assert_calls() @@ -227,33 +289,47 @@ def test_rebuild_server_wait(self): Test that rebuild_server with a wait returns the server instance when its status changes to "ACTIVE". """ - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', self.server_id, 'action']), - json={'server': self.rebuild_server}, - validate=dict( - json={ - 'rebuild': { - 'imageRef': 'a'}})), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', self.server_id]), - json={'server': self.rebuild_server}), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', self.server_id]), - json={'server': self.fake_server}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': []}), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', self.server_id, 'action'], + ), + json={'server': self.rebuild_server}, + validate=dict(json={'rebuild': {'imageRef': 'a'}}), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', self.server_id] + ), + json={'server': self.rebuild_server}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', self.server_id] + ), + json={'server': self.fake_server}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': []}, + ), + ] + ) self.assertEqual( 'ACTIVE', - self.cloud.rebuild_server( - self.fake_server['id'], 'a', wait=True)['status']) + self.cloud.rebuild_server(self.fake_server['id'], 'a', wait=True)[ + 'status' + ], + ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_recordset.py b/openstack/tests/unit/cloud/test_recordset.py index 734842331..b5e376030 100644 --- a/openstack/tests/unit/cloud/test_recordset.py +++ b/openstack/tests/unit/cloud/test_recordset.py @@ -25,7 +25,7 @@ 'records': ['192.168.1.1'], 'id': '1', 'zone_id': zone['id'], - 'zone_name': zone['name'] + 'zone_name': zone['name'], } @@ -34,7 +34,6 @@ class RecordsetTestWrapper(test_zone.ZoneTestWrapper): class TestRecordset(base.TestCase): - def setUp(self): super(TestRecordset, self).setUp() self.use_designate() @@ -42,31 +41,45 @@ def setUp(self): def test_create_recordset_zoneid(self): fake_zone = test_zone.ZoneTestWrapper(self, zone) fake_rs = RecordsetTestWrapper(self, recordset) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones', fake_zone['id']]), - json=fake_zone.get_get_response_json()), - dict(method='POST', - uri=self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones', zone['id'], 'recordsets']), - json=fake_rs.get_create_response_json(), - validate=dict(json={ - "records": fake_rs['records'], - "type": fake_rs['type'], - "name": fake_rs['name'], - "description": fake_rs['description'], - "ttl": fake_rs['ttl'] - })), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones', fake_zone['id']], + ), + json=fake_zone.get_get_response_json(), + ), + dict( + method='POST', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones', zone['id'], 'recordsets'], + ), + json=fake_rs.get_create_response_json(), + validate=dict( + json={ + "records": fake_rs['records'], + "type": fake_rs['type'], + "name": fake_rs['name'], + "description": fake_rs['description'], + "ttl": fake_rs['ttl'], + } + ), + ), + ] + ) rs = self.cloud.create_recordset( zone=fake_zone['id'], name=fake_rs['name'], recordset_type=fake_rs['type'], records=fake_rs['records'], description=fake_rs['description'], - ttl=fake_rs['ttl']) + ttl=fake_rs['ttl'], + ) fake_rs.cmp(rs) self.assert_calls() @@ -74,66 +87,102 @@ def test_create_recordset_zoneid(self): def test_create_recordset_zonename(self): fake_zone = test_zone.ZoneTestWrapper(self, zone) fake_rs = RecordsetTestWrapper(self, recordset) - self.register_uris([ - # try by directly - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones', fake_zone['name']]), - status_code=404), - # list with name - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones'], - qs_elements=[ - 'name={name}'.format(name=fake_zone['name'])]), - json={'zones': [fake_zone.get_get_response_json()]}), - dict(method='POST', - uri=self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones', zone['id'], 'recordsets']), - json=fake_rs.get_create_response_json(), - validate=dict(json={ - "records": fake_rs['records'], - "type": fake_rs['type'], - "name": fake_rs['name'], - "description": fake_rs['description'], - "ttl": fake_rs['ttl'] - })), - ]) + self.register_uris( + [ + # try by directly + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones', fake_zone['name']], + ), + status_code=404, + ), + # list with name + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones'], + qs_elements=[ + 'name={name}'.format(name=fake_zone['name']) + ], + ), + json={'zones': [fake_zone.get_get_response_json()]}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones', zone['id'], 'recordsets'], + ), + json=fake_rs.get_create_response_json(), + validate=dict( + json={ + "records": fake_rs['records'], + "type": fake_rs['type'], + "name": fake_rs['name'], + "description": fake_rs['description'], + "ttl": fake_rs['ttl'], + } + ), + ), + ] + ) rs = self.cloud.create_recordset( zone=fake_zone['name'], name=fake_rs['name'], recordset_type=fake_rs['type'], records=fake_rs['records'], description=fake_rs['description'], - ttl=fake_rs['ttl']) + ttl=fake_rs['ttl'], + ) fake_rs.cmp(rs) self.assert_calls() def test_create_recordset_exception(self): fake_zone = test_zone.ZoneTestWrapper(self, zone) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones', fake_zone['id']]), - json=fake_zone.get_get_response_json()), - dict(method='POST', - uri=self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones', zone['id'], 'recordsets']), - status_code=500, - validate=dict(json={ - 'name': 'www2.example.net.', - 'records': ['192.168.1.2'], - 'type': 'A'})), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones', fake_zone['id']], + ), + json=fake_zone.get_get_response_json(), + ), + dict( + method='POST', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones', zone['id'], 'recordsets'], + ), + status_code=500, + validate=dict( + json={ + 'name': 'www2.example.net.', + 'records': ['192.168.1.2'], + 'type': 'A', + } + ), + ), + ] + ) self.assertRaises( exceptions.SDKException, self.cloud.create_recordset, - fake_zone['id'], 'www2.example.net.', 'a', ['192.168.1.2'] + fake_zone['id'], + 'www2.example.net.', + 'a', + ['192.168.1.2'], ) self.assert_calls() @@ -145,46 +194,82 @@ def test_update_recordset(self): expected_recordset = recordset.copy() expected_recordset['ttl'] = new_ttl updated_rs = RecordsetTestWrapper(self, expected_recordset) - self.register_uris([ - # try by directly - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones', fake_zone['name']]), - status_code=404), - # list with name - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones'], - qs_elements=[ - 'name={name}'.format(name=fake_zone['name'])]), - json={'zones': [fake_zone.get_get_response_json()]}), - # try directly - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones', fake_zone['id'], - 'recordsets', fake_rs['name']]), - status_code=404), - # list with name - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones', fake_zone['id'], - 'recordsets'], - qs_elements=['name={name}'.format(name=fake_rs['name'])]), - json={'recordsets': [fake_rs.get_get_response_json()]}), - # update - dict(method='PUT', - uri=self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones', fake_zone['id'], - 'recordsets', fake_rs['id']]), - json=updated_rs.get_get_response_json(), - validate=dict(json={'ttl': new_ttl})) - ]) + self.register_uris( + [ + # try by directly + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones', fake_zone['name']], + ), + status_code=404, + ), + # list with name + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones'], + qs_elements=[ + 'name={name}'.format(name=fake_zone['name']) + ], + ), + json={'zones': [fake_zone.get_get_response_json()]}, + ), + # try directly + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=[ + 'v2', + 'zones', + fake_zone['id'], + 'recordsets', + fake_rs['name'], + ], + ), + status_code=404, + ), + # list with name + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones', fake_zone['id'], 'recordsets'], + qs_elements=[ + 'name={name}'.format(name=fake_rs['name']) + ], + ), + json={'recordsets': [fake_rs.get_get_response_json()]}, + ), + # update + dict( + method='PUT', + uri=self.get_mock_url( + 'dns', + 'public', + append=[ + 'v2', + 'zones', + fake_zone['id'], + 'recordsets', + fake_rs['id'], + ], + ), + json=updated_rs.get_get_response_json(), + validate=dict(json={'ttl': new_ttl}), + ), + ] + ) res = self.cloud.update_recordset( - fake_zone['name'], fake_rs['name'], ttl=new_ttl) + fake_zone['name'], fake_rs['name'], ttl=new_ttl + ) updated_rs.cmp(res) self.assert_calls() @@ -192,37 +277,65 @@ def test_update_recordset(self): def test_list_recordsets(self): fake_zone = test_zone.ZoneTestWrapper(self, zone) fake_rs = RecordsetTestWrapper(self, recordset) - self.register_uris([ - # try by directly - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones', fake_zone['id']]), - json=fake_zone.get_get_response_json()), - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones', fake_zone['id'], 'recordsets']), - json={'recordsets': [fake_rs.get_get_response_json()], - 'links': { - 'next': self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones', fake_zone['id'], - 'recordsets'], - qs_elements=['limit=1', 'marker=asd']), - 'self': self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones', fake_zone['id'], - 'recordsets?limit=1'])}, - 'metadata':{'total_count': 2}}), - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones', fake_zone['id'], 'recordsets'], - qs_elements=[ - 'limit=1', 'marker=asd']), - json={'recordsets': [fake_rs.get_get_response_json()]}), - ]) + self.register_uris( + [ + # try by directly + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones', fake_zone['id']], + ), + json=fake_zone.get_get_response_json(), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones', fake_zone['id'], 'recordsets'], + ), + json={ + 'recordsets': [fake_rs.get_get_response_json()], + 'links': { + 'next': self.get_mock_url( + 'dns', + 'public', + append=[ + 'v2', + 'zones', + fake_zone['id'], + 'recordsets', + ], + qs_elements=['limit=1', 'marker=asd'], + ), + 'self': self.get_mock_url( + 'dns', + 'public', + append=[ + 'v2', + 'zones', + fake_zone['id'], + 'recordsets?limit=1', + ], + ), + }, + 'metadata': {'total_count': 2}, + }, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones', fake_zone['id'], 'recordsets'], + qs_elements=['limit=1', 'marker=asd'], + ), + json={'recordsets': [fake_rs.get_get_response_json()]}, + ), + ] + ) res = self.cloud.list_recordsets(fake_zone['id']) self.assertEqual(2, len(res)) @@ -231,72 +344,128 @@ def test_list_recordsets(self): def test_delete_recordset(self): fake_zone = test_zone.ZoneTestWrapper(self, zone) fake_rs = RecordsetTestWrapper(self, recordset) - self.register_uris([ - # try by directly - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones', fake_zone['name']]), - status_code=404), - # list with name - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones'], - qs_elements=[ - 'name={name}'.format(name=fake_zone['name'])]), - json={'zones': [fake_zone.get_get_response_json()]}), - # try directly - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones', fake_zone['id'], - 'recordsets', fake_rs['name']]), - status_code=404), - # list with name - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones', fake_zone['id'], - 'recordsets'], - qs_elements=[ - 'name={name}'.format(name=fake_rs['name'])]), - json={'recordsets': [fake_rs.get_get_response_json()]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones', zone['id'], - 'recordsets', fake_rs['id']]), - status_code=202) - ]) + self.register_uris( + [ + # try by directly + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones', fake_zone['name']], + ), + status_code=404, + ), + # list with name + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones'], + qs_elements=[ + 'name={name}'.format(name=fake_zone['name']) + ], + ), + json={'zones': [fake_zone.get_get_response_json()]}, + ), + # try directly + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=[ + 'v2', + 'zones', + fake_zone['id'], + 'recordsets', + fake_rs['name'], + ], + ), + status_code=404, + ), + # list with name + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones', fake_zone['id'], 'recordsets'], + qs_elements=[ + 'name={name}'.format(name=fake_rs['name']) + ], + ), + json={'recordsets': [fake_rs.get_get_response_json()]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'dns', + 'public', + append=[ + 'v2', + 'zones', + zone['id'], + 'recordsets', + fake_rs['id'], + ], + ), + status_code=202, + ), + ] + ) self.assertTrue( - self.cloud.delete_recordset(fake_zone['name'], fake_rs['name'])) + self.cloud.delete_recordset(fake_zone['name'], fake_rs['name']) + ) self.assert_calls() def test_get_recordset_by_id(self): fake_zone = test_zone.ZoneTestWrapper(self, zone) fake_rs = RecordsetTestWrapper(self, recordset) - self.register_uris([ - # try by directly - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones', fake_zone['name']]), - status_code=404), - # list with name - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones'], - qs_elements=[ - 'name={name}'.format(name=fake_zone['name'])]), - json={'zones': [fake_zone.get_get_response_json()]}), - # try directly - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones', fake_zone['id'], - 'recordsets', fake_rs['id']]), - json=fake_rs.get_get_response_json()) - ]) + self.register_uris( + [ + # try by directly + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones', fake_zone['name']], + ), + status_code=404, + ), + # list with name + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones'], + qs_elements=[ + 'name={name}'.format(name=fake_zone['name']) + ], + ), + json={'zones': [fake_zone.get_get_response_json()]}, + ), + # try directly + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=[ + 'v2', + 'zones', + fake_zone['id'], + 'recordsets', + fake_rs['id'], + ], + ), + json=fake_rs.get_get_response_json(), + ), + ] + ) res = self.cloud.get_recordset(fake_zone['name'], fake_rs['id']) fake_rs.cmp(res) self.assert_calls() @@ -304,64 +473,109 @@ def test_get_recordset_by_id(self): def test_get_recordset_by_name(self): fake_zone = test_zone.ZoneTestWrapper(self, zone) fake_rs = RecordsetTestWrapper(self, recordset) - self.register_uris([ - # try by directly - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones', fake_zone['name']]), - status_code=404), - # list with name - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones'], - qs_elements=[ - 'name={name}'.format(name=fake_zone['name'])]), - json={'zones': [fake_zone.get_get_response_json()]}), - # try directly - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones', fake_zone['id'], - 'recordsets', fake_rs['name']]), - status_code=404), - # list with name - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones', fake_zone['id'], - 'recordsets'], - qs_elements=['name={name}'.format(name=fake_rs['name'])]), - json={'recordsets': [fake_rs.get_get_response_json()]}) - ]) + self.register_uris( + [ + # try by directly + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones', fake_zone['name']], + ), + status_code=404, + ), + # list with name + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones'], + qs_elements=[ + 'name={name}'.format(name=fake_zone['name']) + ], + ), + json={'zones': [fake_zone.get_get_response_json()]}, + ), + # try directly + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=[ + 'v2', + 'zones', + fake_zone['id'], + 'recordsets', + fake_rs['name'], + ], + ), + status_code=404, + ), + # list with name + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones', fake_zone['id'], 'recordsets'], + qs_elements=[ + 'name={name}'.format(name=fake_rs['name']) + ], + ), + json={'recordsets': [fake_rs.get_get_response_json()]}, + ), + ] + ) res = self.cloud.get_recordset(fake_zone['name'], fake_rs['name']) fake_rs.cmp(res) self.assert_calls() def test_get_recordset_not_found_returns_false(self): fake_zone = test_zone.ZoneTestWrapper(self, zone) - self.register_uris([ - # try by directly - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones', fake_zone['id']]), - json=fake_zone.get_get_response_json()), - # try directly - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones', fake_zone['id'], - 'recordsets', 'fake']), - status_code=404), - # list with name - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones', fake_zone['id'], - 'recordsets'], - qs_elements=['name=fake']), - json={'recordsets': []}) - ]) + self.register_uris( + [ + # try by directly + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones', fake_zone['id']], + ), + json=fake_zone.get_get_response_json(), + ), + # try directly + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=[ + 'v2', + 'zones', + fake_zone['id'], + 'recordsets', + 'fake', + ], + ), + status_code=404, + ), + # list with name + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones', fake_zone['id'], 'recordsets'], + qs_elements=['name=fake'], + ), + json={'recordsets': []}, + ), + ] + ) res = self.cloud.get_recordset(fake_zone['id'], 'fake') self.assertFalse(res) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_role_assignment.py b/openstack/tests/unit/cloud/test_role_assignment.py index 48b54f71a..5f2d465af 100644 --- a/openstack/tests/unit/cloud/test_role_assignment.py +++ b/openstack/tests/unit/cloud/test_role_assignment.py @@ -19,58 +19,84 @@ class TestRoleAssignment(base.TestCase): - - def _build_role_assignment_response(self, role_id, scope_type, scope_id, - entity_type, entity_id): + def _build_role_assignment_response( + self, role_id, scope_type, scope_id, entity_type, entity_id + ): self.assertThat(['group', 'user'], matchers.Contains(entity_type)) self.assertThat(['project', 'domain'], matchers.Contains(scope_type)) # NOTE(notmorgan): Links are thrown out by shade, but we construct them # for corectness. - link_str = ('https://identity.example.com/identity/v3/{scope_t}s' - '/{scopeid}/{entity_t}s/{entityid}/roles/{roleid}') - return [{ - 'links': {'assignment': link_str.format( - scope_t=scope_type, scopeid=scope_id, entity_t=entity_type, - entityid=entity_id, roleid=role_id)}, - 'role': {'id': role_id}, - 'scope': {scope_type: {'id': scope_id}}, - entity_type: {'id': entity_id} - }] + link_str = ( + 'https://identity.example.com/identity/v3/{scope_t}s' + '/{scopeid}/{entity_t}s/{entityid}/roles/{roleid}' + ) + return [ + { + 'links': { + 'assignment': link_str.format( + scope_t=scope_type, + scopeid=scope_id, + entity_t=entity_type, + entityid=entity_id, + roleid=role_id, + ) + }, + 'role': {'id': role_id}, + 'scope': {scope_type: {'id': scope_id}}, + entity_type: {'id': entity_id}, + } + ] def setUp(self, cloud_config_fixture='clouds.yaml'): super(TestRoleAssignment, self).setUp(cloud_config_fixture) self.role_data = self._get_role_data() self.domain_data = self._get_domain_data() self.user_data = self._get_user_data( - domain_id=self.domain_data.domain_id) + domain_id=self.domain_data.domain_id + ) self.project_data = self._get_project_data( - domain_id=self.domain_data.domain_id) + domain_id=self.domain_data.domain_id + ) self.project_data_v2 = self._get_project_data( project_name=self.project_data.project_name, project_id=self.project_data.project_id, - v3=False) + v3=False, + ) self.group_data = self._get_group_data( - domain_id=self.domain_data.domain_id) + domain_id=self.domain_data.domain_id + ) self.user_project_assignment = self._build_role_assignment_response( - role_id=self.role_data.role_id, scope_type='project', - scope_id=self.project_data.project_id, entity_type='user', - entity_id=self.user_data.user_id) + role_id=self.role_data.role_id, + scope_type='project', + scope_id=self.project_data.project_id, + entity_type='user', + entity_id=self.user_data.user_id, + ) self.group_project_assignment = self._build_role_assignment_response( - role_id=self.role_data.role_id, scope_type='project', - scope_id=self.project_data.project_id, entity_type='group', - entity_id=self.group_data.group_id) + role_id=self.role_data.role_id, + scope_type='project', + scope_id=self.project_data.project_id, + entity_type='group', + entity_id=self.group_data.group_id, + ) self.user_domain_assignment = self._build_role_assignment_response( - role_id=self.role_data.role_id, scope_type='domain', - scope_id=self.domain_data.domain_id, entity_type='user', - entity_id=self.user_data.user_id) + role_id=self.role_data.role_id, + scope_type='domain', + scope_id=self.domain_data.domain_id, + entity_type='user', + entity_id=self.user_data.user_id, + ) self.group_domain_assignment = self._build_role_assignment_response( - role_id=self.role_data.role_id, scope_type='domain', - scope_id=self.domain_data.domain_id, entity_type='group', - entity_id=self.group_data.group_id) + role_id=self.role_data.role_id, + scope_type='domain', + scope_id=self.domain_data.domain_id, + entity_type='group', + entity_id=self.group_data.group_id, + ) # Cleanup of instances to ensure garbage collection/no leaking memory # in tests. @@ -85,25 +111,40 @@ def setUp(self, cloud_config_fixture='clouds.yaml'): self.addCleanup(delattr, self, 'user_domain_assignment') self.addCleanup(delattr, self, 'group_domain_assignment') - def get_mock_url(self, service_type='identity', interface='public', - resource='role_assignments', append=None, - base_url_append='v3', qs_elements=None): + def get_mock_url( + self, + service_type='identity', + interface='public', + resource='role_assignments', + append=None, + base_url_append='v3', + qs_elements=None, + ): return super(TestRoleAssignment, self).get_mock_url( - service_type, interface, resource, append, base_url_append, - qs_elements) + service_type, + interface, + resource, + append, + base_url_append, + qs_elements, + ) - def __get(self, resource, data, attr, qs_elements, use_name=False, - is_found=True): + def __get( + self, resource, data, attr, qs_elements, use_name=False, is_found=True + ): if not use_name: if is_found: - return [dict( - method='GET', - uri=self.get_mock_url( - resource=resource + 's', # do roles from role - append=[getattr(data, attr)], - qs_elements=qs_elements), - status_code=200, - json=data.json_response) + return [ + dict( + method='GET', + uri=self.get_mock_url( + resource=resource + 's', # do roles from role + append=[getattr(data, attr)], + qs_elements=qs_elements, + ), + status_code=200, + json=data.json_response, + ) ] else: return [ @@ -112,15 +153,19 @@ def __get(self, resource, data, attr, qs_elements, use_name=False, uri=self.get_mock_url( resource=resource + 's', # do roles from role append=[getattr(data, attr)], - qs_elements=qs_elements), - status_code=404), + qs_elements=qs_elements, + ), + status_code=404, + ), dict( method='GET', uri=self.get_mock_url( resource=resource + 's', # do roles from role - qs_elements=qs_elements), + qs_elements=qs_elements, + ), status_code=200, - json={(resource + 's'): []}) + json={(resource + 's'): []}, + ), ] else: return [ @@ -129,1187 +174,1771 @@ def __get(self, resource, data, attr, qs_elements, use_name=False, uri=self.get_mock_url( resource=resource + 's', append=[getattr(data, attr)], - qs_elements=qs_elements), - status_code=404), + qs_elements=qs_elements, + ), + status_code=404, + ), dict( method='GET', uri=self.get_mock_url( resource=resource + 's', - qs_elements=[ - 'name=' + getattr(data, attr)] + qs_elements), + qs_elements=['name=' + getattr(data, attr)] + + qs_elements, + ), status_code=200, - json={ - (resource + 's'): [data.json_response[resource]]}) + json={(resource + 's'): [data.json_response[resource]]}, + ), ] def __user_mocks(self, user_data, use_name, is_found=True): uri_mocks = [] if not use_name: - uri_mocks.append(dict( - method='GET', - uri=self.get_mock_url(resource='users'), - status_code=200, - json={'users': [user_data.json_response['user']] if is_found - else []})) + uri_mocks.append( + dict( + method='GET', + uri=self.get_mock_url(resource='users'), + status_code=200, + json={ + 'users': [user_data.json_response['user']] + if is_found + else [] + }, + ) + ) else: - uri_mocks.append(dict( - method='GET', - uri=self.get_mock_url( - resource='users', - qs_elements=[ - 'name=' + user_data.name - ] - ), - status_code=200, - json={'users': [user_data.json_response['user']] if is_found - else []})) + uri_mocks.append( + dict( + method='GET', + uri=self.get_mock_url( + resource='users', + qs_elements=['name=' + user_data.name], + ), + status_code=200, + json={ + 'users': [user_data.json_response['user']] + if is_found + else [] + }, + ) + ) return uri_mocks def _get_mock_role_query_urls( - self, role_data, domain_data=None, project_data=None, - group_data=None, user_data=None, - use_role_name=False, use_domain_name=False, use_project_name=False, - use_group_name=False, use_user_name=False, use_domain_in_query=True + self, + role_data, + domain_data=None, + project_data=None, + group_data=None, + user_data=None, + use_role_name=False, + use_domain_name=False, + use_project_name=False, + use_group_name=False, + use_user_name=False, + use_domain_in_query=True, ): - """Build uri mocks for querying role assignments - """ + """Build uri mocks for querying role assignments""" uri_mocks = [] if domain_data: uri_mocks.extend( self.__get( - 'domain', domain_data, + 'domain', + domain_data, 'domain_id' if not use_domain_name else 'domain_name', - [], use_name=use_domain_name) + [], + use_name=use_domain_name, + ) ) qs_elements = [] if domain_data and use_domain_in_query: - qs_elements = [ - 'domain_id=' + domain_data.domain_id - ] + qs_elements = ['domain_id=' + domain_data.domain_id] uri_mocks.extend( self.__get( - 'role', role_data, + 'role', + role_data, 'role_id' if not use_role_name else 'role_name', - [], use_name=use_role_name) + [], + use_name=use_role_name, + ) ) if user_data: - uri_mocks.extend(self.__user_mocks( - user_data, use_user_name, is_found=True)) + uri_mocks.extend( + self.__user_mocks(user_data, use_user_name, is_found=True) + ) if group_data: uri_mocks.extend( self.__get( - 'group', group_data, + 'group', + group_data, 'group_id' if not use_group_name else 'group_name', - qs_elements, use_name=use_group_name) + qs_elements, + use_name=use_group_name, + ) ) if project_data: uri_mocks.extend( self.__get( - 'project', project_data, + 'project', + project_data, 'project_id' if not use_project_name else 'project_name', - qs_elements, use_name=use_project_name) + qs_elements, + use_name=use_project_name, + ) ) return uri_mocks def test_grant_role_user_id_project(self): uris = self._get_mock_role_query_urls( - self.role_data, project_data=self.project_data, - user_data=self.user_data, use_role_name=True) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='projects', - append=[ - self.project_data.project_id, - 'users', self.user_data.user_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=404), - dict(method='PUT', - uri=self.get_mock_url( - resource='projects', - append=[self.project_data.project_id, - 'users', self.user_data.user_id, - 'roles', self.role_data.role_id]), - status_code=200), - ]) + self.role_data, + project_data=self.project_data, + user_data=self.user_data, + use_role_name=True, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='projects', + append=[ + self.project_data.project_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=404, + ), + dict( + method='PUT', + uri=self.get_mock_url( + resource='projects', + append=[ + self.project_data.project_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id, + ], + ), + status_code=200, + ), + ] + ) self.register_uris(uris) self.assertTrue( self.cloud.grant_role( self.role_data.role_name, user=self.user_data.user_id, - project=self.project_data.project_id)) + project=self.project_data.project_id, + ) + ) self.assert_calls() def test_grant_role_user_name_project(self): uris = self._get_mock_role_query_urls( self.role_data, - project_data=self.project_data, user_data=self.user_data, - use_role_name=True, use_user_name=True) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='projects', - append=[ - self.project_data.project_id, - 'users', self.user_data.user_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=404), - dict(method='PUT', - uri=self.get_mock_url( - resource='projects', - append=[self.project_data.project_id, - 'users', self.user_data.user_id, - 'roles', self.role_data.role_id]), - status_code=200), - ]) + project_data=self.project_data, + user_data=self.user_data, + use_role_name=True, + use_user_name=True, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='projects', + append=[ + self.project_data.project_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=404, + ), + dict( + method='PUT', + uri=self.get_mock_url( + resource='projects', + append=[ + self.project_data.project_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id, + ], + ), + status_code=200, + ), + ] + ) self.register_uris(uris) self.assertTrue( self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, - project=self.project_data.project_id)) + project=self.project_data.project_id, + ) + ) def test_grant_role_user_id_project_exists(self): uris = self._get_mock_role_query_urls( self.role_data, - project_data=self.project_data, user_data=self.user_data) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='projects', - append=[ - self.project_data.project_id, - 'users', self.user_data.user_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=204), - ]) + project_data=self.project_data, + user_data=self.user_data, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='projects', + append=[ + self.project_data.project_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=204, + ), + ] + ) self.register_uris(uris) - self.assertFalse(self.cloud.grant_role( - self.role_data.role_id, - user=self.user_data.user_id, - project=self.project_data.project_id)) + self.assertFalse( + self.cloud.grant_role( + self.role_data.role_id, + user=self.user_data.user_id, + project=self.project_data.project_id, + ) + ) self.assert_calls() def test_grant_role_user_name_project_exists(self): uris = self._get_mock_role_query_urls( self.role_data, - project_data=self.project_data, user_data=self.user_data, - use_role_name=True, use_user_name=True) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='projects', - append=[ - self.project_data.project_id, - 'users', self.user_data.user_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=204), - ]) + project_data=self.project_data, + user_data=self.user_data, + use_role_name=True, + use_user_name=True, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='projects', + append=[ + self.project_data.project_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=204, + ), + ] + ) self.register_uris(uris) - self.assertFalse(self.cloud.grant_role( - self.role_data.role_name, - user=self.user_data.name, - project=self.project_data.project_id)) + self.assertFalse( + self.cloud.grant_role( + self.role_data.role_name, + user=self.user_data.name, + project=self.project_data.project_id, + ) + ) self.assert_calls() def test_grant_role_group_id_project(self): uris = self._get_mock_role_query_urls( self.role_data, - project_data=self.project_data, group_data=self.group_data, - use_role_name=True) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='projects', - append=[ - self.project_data.project_id, - 'groups', self.group_data.group_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=404), - dict(method='PUT', - uri=self.get_mock_url( - resource='projects', - append=[self.project_data.project_id, - 'groups', self.group_data.group_id, - 'roles', self.role_data.role_id]), - status_code=200), - ]) + project_data=self.project_data, + group_data=self.group_data, + use_role_name=True, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='projects', + append=[ + self.project_data.project_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=404, + ), + dict( + method='PUT', + uri=self.get_mock_url( + resource='projects', + append=[ + self.project_data.project_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id, + ], + ), + status_code=200, + ), + ] + ) self.register_uris(uris) self.assertTrue( self.cloud.grant_role( self.role_data.role_name, group=self.group_data.group_id, - project=self.project_data.project_id)) + project=self.project_data.project_id, + ) + ) self.assert_calls() def test_grant_role_group_name_project(self): uris = self._get_mock_role_query_urls( self.role_data, - project_data=self.project_data, group_data=self.group_data, - use_role_name=True, use_group_name=True) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='projects', - append=[ - self.project_data.project_id, - 'groups', self.group_data.group_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=404), - dict(method='PUT', - uri=self.get_mock_url( - resource='projects', - append=[self.project_data.project_id, - 'groups', self.group_data.group_id, - 'roles', self.role_data.role_id]), - status_code=200), - ]) + project_data=self.project_data, + group_data=self.group_data, + use_role_name=True, + use_group_name=True, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='projects', + append=[ + self.project_data.project_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=404, + ), + dict( + method='PUT', + uri=self.get_mock_url( + resource='projects', + append=[ + self.project_data.project_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id, + ], + ), + status_code=200, + ), + ] + ) self.register_uris(uris) self.assertTrue( self.cloud.grant_role( self.role_data.role_name, group=self.group_data.group_name, - project=self.project_data.project_id)) + project=self.project_data.project_id, + ) + ) self.assert_calls() def test_grant_role_group_id_project_exists(self): uris = self._get_mock_role_query_urls( self.role_data, - project_data=self.project_data, group_data=self.group_data) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='projects', - append=[ - self.project_data.project_id, - 'groups', self.group_data.group_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=204), - ]) + project_data=self.project_data, + group_data=self.group_data, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='projects', + append=[ + self.project_data.project_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=204, + ), + ] + ) self.register_uris(uris) - self.assertFalse(self.cloud.grant_role( - self.role_data.role_id, - group=self.group_data.group_id, - project=self.project_data.project_id)) + self.assertFalse( + self.cloud.grant_role( + self.role_data.role_id, + group=self.group_data.group_id, + project=self.project_data.project_id, + ) + ) self.assert_calls() def test_grant_role_group_name_project_exists(self): uris = self._get_mock_role_query_urls( self.role_data, - project_data=self.project_data, group_data=self.group_data, - use_role_name=True, use_group_name=True) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='projects', - append=[ - self.project_data.project_id, - 'groups', self.group_data.group_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=204), - ]) + project_data=self.project_data, + group_data=self.group_data, + use_role_name=True, + use_group_name=True, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='projects', + append=[ + self.project_data.project_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=204, + ), + ] + ) self.register_uris(uris) - self.assertFalse(self.cloud.grant_role( - self.role_data.role_name, - group=self.group_data.group_name, - project=self.project_data.project_id)) + self.assertFalse( + self.cloud.grant_role( + self.role_data.role_name, + group=self.group_data.group_name, + project=self.project_data.project_id, + ) + ) self.assert_calls() -# ===== Domain + # ===== Domain def test_grant_role_user_id_domain(self): uris = self._get_mock_role_query_urls( - self.role_data, domain_data=self.domain_data, - user_data=self.user_data, use_role_name=True) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='domains', - append=[ - self.domain_data.domain_id, - 'users', self.user_data.user_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=404), - dict(method='PUT', - uri=self.get_mock_url( - resource='domains', - append=[self.domain_data.domain_id, - 'users', self.user_data.user_id, - 'roles', self.role_data.role_id]), - status_code=200), - ]) + self.role_data, + domain_data=self.domain_data, + user_data=self.user_data, + use_role_name=True, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=404, + ), + dict( + method='PUT', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id, + ], + ), + status_code=200, + ), + ] + ) self.register_uris(uris) self.assertTrue( self.cloud.grant_role( self.role_data.role_name, user=self.user_data.user_id, - domain=self.domain_data.domain_id)) + domain=self.domain_data.domain_id, + ) + ) self.assert_calls() def test_grant_role_user_name_domain(self): uris = self._get_mock_role_query_urls( self.role_data, - domain_data=self.domain_data, user_data=self.user_data, - use_role_name=True, use_user_name=True) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='domains', - append=[ - self.domain_data.domain_id, - 'users', self.user_data.user_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=404), - dict(method='PUT', - uri=self.get_mock_url( - resource='domains', - append=[self.domain_data.domain_id, - 'users', self.user_data.user_id, - 'roles', self.role_data.role_id]), - status_code=200), - ]) + domain_data=self.domain_data, + user_data=self.user_data, + use_role_name=True, + use_user_name=True, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=404, + ), + dict( + method='PUT', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id, + ], + ), + status_code=200, + ), + ] + ) self.register_uris(uris) self.assertTrue( self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, - domain=self.domain_data.domain_id)) + domain=self.domain_data.domain_id, + ) + ) def test_grant_role_user_id_domain_exists(self): uris = self._get_mock_role_query_urls( self.role_data, - domain_data=self.domain_data, user_data=self.user_data) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='domains', - append=[ - self.domain_data.domain_id, - 'users', self.user_data.user_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=204), - ]) + domain_data=self.domain_data, + user_data=self.user_data, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=204, + ), + ] + ) self.register_uris(uris) - self.assertFalse(self.cloud.grant_role( - self.role_data.role_id, - user=self.user_data.user_id, - domain=self.domain_data.domain_id)) + self.assertFalse( + self.cloud.grant_role( + self.role_data.role_id, + user=self.user_data.user_id, + domain=self.domain_data.domain_id, + ) + ) self.assert_calls() def test_grant_role_user_name_domain_exists(self): uris = self._get_mock_role_query_urls( self.role_data, - domain_data=self.domain_data, user_data=self.user_data, - use_role_name=True, use_user_name=True) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='domains', - append=[ - self.domain_data.domain_id, - 'users', self.user_data.user_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=204), - ]) + domain_data=self.domain_data, + user_data=self.user_data, + use_role_name=True, + use_user_name=True, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=204, + ), + ] + ) self.register_uris(uris) - self.assertFalse(self.cloud.grant_role( - self.role_data.role_name, - user=self.user_data.name, - domain=self.domain_data.domain_id)) + self.assertFalse( + self.cloud.grant_role( + self.role_data.role_name, + user=self.user_data.name, + domain=self.domain_data.domain_id, + ) + ) self.assert_calls() def test_grant_role_group_id_domain(self): uris = self._get_mock_role_query_urls( self.role_data, - domain_data=self.domain_data, group_data=self.group_data, - use_role_name=True) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='domains', - append=[ - self.domain_data.domain_id, - 'groups', self.group_data.group_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=404), - dict(method='PUT', - uri=self.get_mock_url( - resource='domains', - append=[self.domain_data.domain_id, - 'groups', self.group_data.group_id, - 'roles', self.role_data.role_id]), - status_code=200), - ]) + domain_data=self.domain_data, + group_data=self.group_data, + use_role_name=True, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=404, + ), + dict( + method='PUT', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id, + ], + ), + status_code=200, + ), + ] + ) self.register_uris(uris) self.assertTrue( self.cloud.grant_role( self.role_data.role_name, group=self.group_data.group_id, - domain=self.domain_data.domain_id)) + domain=self.domain_data.domain_id, + ) + ) self.assert_calls() def test_grant_role_group_name_domain(self): uris = self._get_mock_role_query_urls( self.role_data, - domain_data=self.domain_data, group_data=self.group_data, - use_role_name=True, use_group_name=True) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='domains', - append=[ - self.domain_data.domain_id, - 'groups', self.group_data.group_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=404), - dict(method='PUT', - uri=self.get_mock_url( - resource='domains', - append=[self.domain_data.domain_id, - 'groups', self.group_data.group_id, - 'roles', self.role_data.role_id]), - status_code=200), - ]) + domain_data=self.domain_data, + group_data=self.group_data, + use_role_name=True, + use_group_name=True, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=404, + ), + dict( + method='PUT', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id, + ], + ), + status_code=200, + ), + ] + ) self.register_uris(uris) self.assertTrue( self.cloud.grant_role( self.role_data.role_name, group=self.group_data.group_name, - domain=self.domain_data.domain_id)) + domain=self.domain_data.domain_id, + ) + ) self.assert_calls() def test_grant_role_group_id_domain_exists(self): uris = self._get_mock_role_query_urls( self.role_data, - domain_data=self.domain_data, group_data=self.group_data) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='domains', - append=[ - self.domain_data.domain_id, - 'groups', self.group_data.group_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=204), - ]) + domain_data=self.domain_data, + group_data=self.group_data, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=204, + ), + ] + ) self.register_uris(uris) - self.assertFalse(self.cloud.grant_role( - self.role_data.role_id, - group=self.group_data.group_id, - domain=self.domain_data.domain_id)) + self.assertFalse( + self.cloud.grant_role( + self.role_data.role_id, + group=self.group_data.group_id, + domain=self.domain_data.domain_id, + ) + ) self.assert_calls() def test_grant_role_group_name_domain_exists(self): uris = self._get_mock_role_query_urls( self.role_data, - domain_data=self.domain_data, group_data=self.group_data, - use_role_name=True, use_group_name=True) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='domains', - append=[ - self.domain_data.domain_id, - 'groups', self.group_data.group_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=204), - ]) + domain_data=self.domain_data, + group_data=self.group_data, + use_role_name=True, + use_group_name=True, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=204, + ), + ] + ) self.register_uris(uris) - self.assertFalse(self.cloud.grant_role( - self.role_data.role_name, - group=self.group_data.group_name, - domain=self.domain_data.domain_id)) + self.assertFalse( + self.cloud.grant_role( + self.role_data.role_name, + group=self.group_data.group_name, + domain=self.domain_data.domain_id, + ) + ) self.assert_calls() -# ==== Revoke + # ==== Revoke def test_revoke_role_user_id_project(self): uris = self._get_mock_role_query_urls( - self.role_data, project_data=self.project_data, - user_data=self.user_data, use_role_name=True) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='projects', - append=[ - self.project_data.project_id, - 'users', self.user_data.user_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=204), - dict(method='DELETE', - uri=self.get_mock_url( - resource='projects', - append=[self.project_data.project_id, - 'users', self.user_data.user_id, - 'roles', self.role_data.role_id]), - status_code=200), - ]) + self.role_data, + project_data=self.project_data, + user_data=self.user_data, + use_role_name=True, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='projects', + append=[ + self.project_data.project_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=204, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='projects', + append=[ + self.project_data.project_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id, + ], + ), + status_code=200, + ), + ] + ) self.register_uris(uris) self.assertTrue( self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.user_id, - project=self.project_data.project_id)) - self.assert_calls() + project=self.project_data.project_id, + ) + ) + self.assert_calls() def test_revoke_role_user_name_project(self): uris = self._get_mock_role_query_urls( self.role_data, - project_data=self.project_data, user_data=self.user_data, - use_role_name=True, use_user_name=True) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='projects', - append=[ - self.project_data.project_id, - 'users', self.user_data.user_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=204), - dict(method='DELETE', - uri=self.get_mock_url( - resource='projects', - append=[self.project_data.project_id, - 'users', self.user_data.user_id, - 'roles', self.role_data.role_id]), - status_code=200), - ]) + project_data=self.project_data, + user_data=self.user_data, + use_role_name=True, + use_user_name=True, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='projects', + append=[ + self.project_data.project_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=204, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='projects', + append=[ + self.project_data.project_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id, + ], + ), + status_code=200, + ), + ] + ) self.register_uris(uris) self.assertTrue( self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name, - project=self.project_data.project_id)) + project=self.project_data.project_id, + ) + ) def test_revoke_role_user_id_project_not_exists(self): uris = self._get_mock_role_query_urls( self.role_data, - project_data=self.project_data, user_data=self.user_data) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='projects', - append=[ - self.project_data.project_id, - 'users', self.user_data.user_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=404), - ]) + project_data=self.project_data, + user_data=self.user_data, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='projects', + append=[ + self.project_data.project_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=404, + ), + ] + ) self.register_uris(uris) - self.assertFalse(self.cloud.revoke_role( - self.role_data.role_id, - user=self.user_data.user_id, - project=self.project_data.project_id)) + self.assertFalse( + self.cloud.revoke_role( + self.role_data.role_id, + user=self.user_data.user_id, + project=self.project_data.project_id, + ) + ) self.assert_calls() def test_revoke_role_user_name_project_not_exists(self): uris = self._get_mock_role_query_urls( self.role_data, - project_data=self.project_data, user_data=self.user_data, - use_role_name=True, use_user_name=True) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='projects', - append=[ - self.project_data.project_id, - 'users', self.user_data.user_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=404), - ]) + project_data=self.project_data, + user_data=self.user_data, + use_role_name=True, + use_user_name=True, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='projects', + append=[ + self.project_data.project_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=404, + ), + ] + ) self.register_uris(uris) - self.assertFalse(self.cloud.revoke_role( - self.role_data.role_name, - user=self.user_data.name, - project=self.project_data.project_id)) + self.assertFalse( + self.cloud.revoke_role( + self.role_data.role_name, + user=self.user_data.name, + project=self.project_data.project_id, + ) + ) self.assert_calls() def test_revoke_role_group_id_project(self): uris = self._get_mock_role_query_urls( self.role_data, - project_data=self.project_data, group_data=self.group_data, - use_role_name=True) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='projects', - append=[ - self.project_data.project_id, - 'groups', self.group_data.group_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=204), - dict(method='DELETE', - uri=self.get_mock_url( - resource='projects', - append=[self.project_data.project_id, - 'groups', self.group_data.group_id, - 'roles', self.role_data.role_id]), - status_code=200), - ]) + project_data=self.project_data, + group_data=self.group_data, + use_role_name=True, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='projects', + append=[ + self.project_data.project_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=204, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='projects', + append=[ + self.project_data.project_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id, + ], + ), + status_code=200, + ), + ] + ) self.register_uris(uris) self.assertTrue( self.cloud.revoke_role( self.role_data.role_name, group=self.group_data.group_id, - project=self.project_data.project_id)) + project=self.project_data.project_id, + ) + ) self.assert_calls() def test_revoke_role_group_name_project(self): uris = self._get_mock_role_query_urls( self.role_data, - project_data=self.project_data, group_data=self.group_data, - use_role_name=True, use_group_name=True) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='projects', - append=[ - self.project_data.project_id, - 'groups', self.group_data.group_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=204), - dict(method='DELETE', - uri=self.get_mock_url( - resource='projects', - append=[self.project_data.project_id, - 'groups', self.group_data.group_id, - 'roles', self.role_data.role_id]), - status_code=200), - ]) + project_data=self.project_data, + group_data=self.group_data, + use_role_name=True, + use_group_name=True, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='projects', + append=[ + self.project_data.project_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=204, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='projects', + append=[ + self.project_data.project_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id, + ], + ), + status_code=200, + ), + ] + ) self.register_uris(uris) self.assertTrue( self.cloud.revoke_role( self.role_data.role_name, group=self.group_data.group_name, - project=self.project_data.project_id)) + project=self.project_data.project_id, + ) + ) self.assert_calls() def test_revoke_role_group_id_project_not_exists(self): uris = self._get_mock_role_query_urls( self.role_data, - project_data=self.project_data, group_data=self.group_data) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='projects', - append=[ - self.project_data.project_id, - 'groups', self.group_data.group_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=404), - ]) + project_data=self.project_data, + group_data=self.group_data, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='projects', + append=[ + self.project_data.project_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=404, + ), + ] + ) self.register_uris(uris) - self.assertFalse(self.cloud.revoke_role( - self.role_data.role_id, - group=self.group_data.group_id, - project=self.project_data.project_id)) + self.assertFalse( + self.cloud.revoke_role( + self.role_data.role_id, + group=self.group_data.group_id, + project=self.project_data.project_id, + ) + ) self.assert_calls() def test_revoke_role_group_name_project_not_exists(self): uris = self._get_mock_role_query_urls( self.role_data, - project_data=self.project_data, group_data=self.group_data, - use_role_name=True, use_group_name=True) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='projects', - append=[ - self.project_data.project_id, - 'groups', self.group_data.group_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=404), - ]) + project_data=self.project_data, + group_data=self.group_data, + use_role_name=True, + use_group_name=True, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='projects', + append=[ + self.project_data.project_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=404, + ), + ] + ) self.register_uris(uris) - self.assertFalse(self.cloud.revoke_role( - self.role_data.role_name, - group=self.group_data.group_name, - project=self.project_data.project_id)) + self.assertFalse( + self.cloud.revoke_role( + self.role_data.role_name, + group=self.group_data.group_name, + project=self.project_data.project_id, + ) + ) self.assert_calls() -# ==== Domain + # ==== Domain def test_revoke_role_user_id_domain(self): uris = self._get_mock_role_query_urls( - self.role_data, domain_data=self.domain_data, - user_data=self.user_data, use_role_name=True) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='domains', - append=[ - self.domain_data.domain_id, - 'users', self.user_data.user_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=204), - dict(method='DELETE', - uri=self.get_mock_url( - resource='domains', - append=[self.domain_data.domain_id, - 'users', self.user_data.user_id, - 'roles', self.role_data.role_id]), - status_code=200), - ]) + self.role_data, + domain_data=self.domain_data, + user_data=self.user_data, + use_role_name=True, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=204, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id, + ], + ), + status_code=200, + ), + ] + ) self.register_uris(uris) self.assertTrue( self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.user_id, - domain=self.domain_data.domain_id)) + domain=self.domain_data.domain_id, + ) + ) self.assert_calls() def test_revoke_role_user_name_domain(self): uris = self._get_mock_role_query_urls( self.role_data, - domain_data=self.domain_data, user_data=self.user_data, - use_role_name=True, use_user_name=True) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='domains', - append=[ - self.domain_data.domain_id, - 'users', self.user_data.user_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=204), - dict(method='DELETE', - uri=self.get_mock_url( - resource='domains', - append=[self.domain_data.domain_id, - 'users', self.user_data.user_id, - 'roles', self.role_data.role_id]), - status_code=200), - ]) + domain_data=self.domain_data, + user_data=self.user_data, + use_role_name=True, + use_user_name=True, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=204, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id, + ], + ), + status_code=200, + ), + ] + ) self.register_uris(uris) self.assertTrue( self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name, - domain=self.domain_data.domain_id)) + domain=self.domain_data.domain_id, + ) + ) def test_revoke_role_user_id_domain_not_exists(self): uris = self._get_mock_role_query_urls( self.role_data, - domain_data=self.domain_data, user_data=self.user_data) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='domains', - append=[ - self.domain_data.domain_id, - 'users', self.user_data.user_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=404), - ]) + domain_data=self.domain_data, + user_data=self.user_data, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=404, + ), + ] + ) self.register_uris(uris) - self.assertFalse(self.cloud.revoke_role( - self.role_data.role_id, - user=self.user_data.user_id, - domain=self.domain_data.domain_id)) + self.assertFalse( + self.cloud.revoke_role( + self.role_data.role_id, + user=self.user_data.user_id, + domain=self.domain_data.domain_id, + ) + ) self.assert_calls() def test_revoke_role_user_name_domain_not_exists(self): uris = self._get_mock_role_query_urls( self.role_data, - domain_data=self.domain_data, user_data=self.user_data, - use_role_name=True, use_user_name=True) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='domains', - append=[ - self.domain_data.domain_id, - 'users', self.user_data.user_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=404), - ]) + domain_data=self.domain_data, + user_data=self.user_data, + use_role_name=True, + use_user_name=True, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=404, + ), + ] + ) self.register_uris(uris) - self.assertFalse(self.cloud.revoke_role( - self.role_data.role_name, - user=self.user_data.name, - domain=self.domain_data.domain_id)) + self.assertFalse( + self.cloud.revoke_role( + self.role_data.role_name, + user=self.user_data.name, + domain=self.domain_data.domain_id, + ) + ) self.assert_calls() def test_revoke_role_group_id_domain(self): uris = self._get_mock_role_query_urls( self.role_data, - domain_data=self.domain_data, group_data=self.group_data, - use_role_name=True) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='domains', - append=[ - self.domain_data.domain_id, - 'groups', self.group_data.group_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=204), - dict(method='DELETE', - uri=self.get_mock_url( - resource='domains', - append=[self.domain_data.domain_id, - 'groups', self.group_data.group_id, - 'roles', self.role_data.role_id]), - status_code=200), - ]) + domain_data=self.domain_data, + group_data=self.group_data, + use_role_name=True, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=204, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id, + ], + ), + status_code=200, + ), + ] + ) self.register_uris(uris) self.assertTrue( self.cloud.revoke_role( self.role_data.role_name, group=self.group_data.group_id, - domain=self.domain_data.domain_id)) + domain=self.domain_data.domain_id, + ) + ) self.assert_calls() def test_revoke_role_group_name_domain(self): uris = self._get_mock_role_query_urls( self.role_data, - domain_data=self.domain_data, group_data=self.group_data, - use_role_name=True, use_group_name=True) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='domains', - append=[ - self.domain_data.domain_id, - 'groups', self.group_data.group_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=204), - dict(method='DELETE', - uri=self.get_mock_url( - resource='domains', - append=[self.domain_data.domain_id, - 'groups', self.group_data.group_id, - 'roles', self.role_data.role_id]), - status_code=200), - ]) + domain_data=self.domain_data, + group_data=self.group_data, + use_role_name=True, + use_group_name=True, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=204, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id, + ], + ), + status_code=200, + ), + ] + ) self.register_uris(uris) self.assertTrue( self.cloud.revoke_role( self.role_data.role_name, group=self.group_data.group_name, - domain=self.domain_data.domain_id)) + domain=self.domain_data.domain_id, + ) + ) self.assert_calls() def test_revoke_role_group_id_domain_not_exists(self): uris = self._get_mock_role_query_urls( self.role_data, - domain_data=self.domain_data, group_data=self.group_data) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='domains', - append=[ - self.domain_data.domain_id, - 'groups', self.group_data.group_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=404), - ]) + domain_data=self.domain_data, + group_data=self.group_data, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=404, + ), + ] + ) self.register_uris(uris) - self.assertFalse(self.cloud.revoke_role( - self.role_data.role_id, - group=self.group_data.group_id, - domain=self.domain_data.domain_id)) + self.assertFalse( + self.cloud.revoke_role( + self.role_data.role_id, + group=self.group_data.group_id, + domain=self.domain_data.domain_id, + ) + ) self.assert_calls() def test_revoke_role_group_name_domain_not_exists(self): uris = self._get_mock_role_query_urls( self.role_data, - domain_data=self.domain_data, group_data=self.group_data, - use_role_name=True, use_group_name=True) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='domains', - append=[ - self.domain_data.domain_id, - 'groups', self.group_data.group_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=404), - ]) + domain_data=self.domain_data, + group_data=self.group_data, + use_role_name=True, + use_group_name=True, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='domains', + append=[ + self.domain_data.domain_id, + 'groups', + self.group_data.group_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=404, + ), + ] + ) self.register_uris(uris) - self.assertFalse(self.cloud.revoke_role( - self.role_data.role_name, - group=self.group_data.group_name, - domain=self.domain_data.domain_id)) + self.assertFalse( + self.cloud.revoke_role( + self.role_data.role_name, + group=self.group_data.group_name, + domain=self.domain_data.domain_id, + ) + ) self.assert_calls() def test_grant_no_role(self): uris = self.__get( - 'domain', self.domain_data, 'domain_name', [], use_name=True) - uris.extend([ - dict(method='GET', - uri=self.get_mock_url( - resource='roles', - append=[self.role_data.role_name], - ), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - resource='roles', - qs_elements=[ - 'name=' + self.role_data.role_name, - ]), - status_code=200, - json={'roles': []}) - ]) + 'domain', self.domain_data, 'domain_name', [], use_name=True + ) + uris.extend( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='roles', + append=[self.role_data.role_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='roles', + qs_elements=[ + 'name=' + self.role_data.role_name, + ], + ), + status_code=200, + json={'roles': []}, + ), + ] + ) self.register_uris(uris) with testtools.ExpectedException( exc.OpenStackCloudException, - 'Role {0} not found'.format(self.role_data.role_name) + 'Role {0} not found'.format(self.role_data.role_name), ): self.cloud.grant_role( self.role_data.role_name, group=self.group_data.group_name, - domain=self.domain_data.domain_name) + domain=self.domain_data.domain_name, + ) self.assert_calls() def test_revoke_no_role(self): uris = self.__get( - 'domain', self.domain_data, 'domain_name', [], use_name=True) - uris.extend([ - dict(method='GET', - uri=self.get_mock_url( - resource='roles', - append=[self.role_data.role_name], - ), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - resource='roles', - qs_elements=[ - 'name=' + self.role_data.role_name, - ]), - status_code=200, - json={'roles': []}) - ]) + 'domain', self.domain_data, 'domain_name', [], use_name=True + ) + uris.extend( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='roles', + append=[self.role_data.role_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='roles', + qs_elements=[ + 'name=' + self.role_data.role_name, + ], + ), + status_code=200, + json={'roles': []}, + ), + ] + ) self.register_uris(uris) with testtools.ExpectedException( exc.OpenStackCloudException, - 'Role {0} not found'.format(self.role_data.role_name) + 'Role {0} not found'.format(self.role_data.role_name), ): self.cloud.revoke_role( self.role_data.role_name, group=self.group_data.group_name, - domain=self.domain_data.domain_name) + domain=self.domain_data.domain_name, + ) self.assert_calls() def test_grant_no_user_or_group_specified(self): uris = self.__get( - 'role', self.role_data, 'role_name', [], use_name=True) + 'role', self.role_data, 'role_name', [], use_name=True + ) self.register_uris(uris) with testtools.ExpectedException( - exc.OpenStackCloudException, - 'Must specify either a user or a group' + exc.OpenStackCloudException, + 'Must specify either a user or a group', ): self.cloud.grant_role(self.role_data.role_name) self.assert_calls() def test_revoke_no_user_or_group_specified(self): uris = self.__get( - 'role', self.role_data, 'role_name', [], use_name=True) + 'role', self.role_data, 'role_name', [], use_name=True + ) self.register_uris(uris) with testtools.ExpectedException( - exc.OpenStackCloudException, - 'Must specify either a user or a group' + exc.OpenStackCloudException, + 'Must specify either a user or a group', ): self.cloud.revoke_role(self.role_data.role_name) self.assert_calls() def test_grant_no_user_or_group(self): uris = self.__get( - 'role', self.role_data, 'role_name', [], use_name=True) - uris.extend(self.__user_mocks(self.user_data, use_name=True, - is_found=False)) + 'role', self.role_data, 'role_name', [], use_name=True + ) + uris.extend( + self.__user_mocks(self.user_data, use_name=True, is_found=False) + ) self.register_uris(uris) with testtools.ExpectedException( exc.OpenStackCloudException, - 'Must specify either a user or a group' + 'Must specify either a user or a group', ): self.cloud.grant_role( - self.role_data.role_name, - user=self.user_data.name) + self.role_data.role_name, user=self.user_data.name + ) self.assert_calls() def test_revoke_no_user_or_group(self): uris = self.__get( - 'role', self.role_data, 'role_name', [], use_name=True) - uris.extend(self.__user_mocks(self.user_data, use_name=True, - is_found=False)) + 'role', self.role_data, 'role_name', [], use_name=True + ) + uris.extend( + self.__user_mocks(self.user_data, use_name=True, is_found=False) + ) self.register_uris(uris) with testtools.ExpectedException( exc.OpenStackCloudException, - 'Must specify either a user or a group' + 'Must specify either a user or a group', ): self.cloud.revoke_role( - self.role_data.role_name, - user=self.user_data.name) + self.role_data.role_name, user=self.user_data.name + ) self.assert_calls() def test_grant_both_user_and_group(self): uris = self.__get( - 'role', self.role_data, 'role_name', [], use_name=True) + 'role', self.role_data, 'role_name', [], use_name=True + ) uris.extend(self.__user_mocks(self.user_data, use_name=True)) - uris.extend(self.__get( - 'group', self.group_data, 'group_name', [], use_name=True)) + uris.extend( + self.__get( + 'group', self.group_data, 'group_name', [], use_name=True + ) + ) self.register_uris(uris) with testtools.ExpectedException( exc.OpenStackCloudException, - 'Specify either a group or a user, not both' + 'Specify either a group or a user, not both', ): self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, - group=self.group_data.group_name) + group=self.group_data.group_name, + ) self.assert_calls() def test_revoke_both_user_and_group(self): uris = self.__get( - 'role', self.role_data, 'role_name', [], use_name=True) + 'role', self.role_data, 'role_name', [], use_name=True + ) uris.extend(self.__user_mocks(self.user_data, use_name=True)) - uris.extend(self.__get( - 'group', self.group_data, 'group_name', [], use_name=True)) + uris.extend( + self.__get( + 'group', self.group_data, 'group_name', [], use_name=True + ) + ) self.register_uris(uris) with testtools.ExpectedException( exc.OpenStackCloudException, - 'Specify either a group or a user, not both' + 'Specify either a group or a user, not both', ): self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name, - group=self.group_data.group_name) + group=self.group_data.group_name, + ) def test_grant_both_project_and_domain(self): uris = self._get_mock_role_query_urls( self.role_data, - project_data=self.project_data, user_data=self.user_data, + project_data=self.project_data, + user_data=self.user_data, domain_data=self.domain_data, - use_role_name=True, use_user_name=True, use_project_name=True, - use_domain_name=True, use_domain_in_query=True) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='projects', - append=[ - self.project_data.project_id, - 'users', self.user_data.user_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=404), - dict(method='PUT', - uri=self.get_mock_url( - resource='projects', - append=[self.project_data.project_id, - 'users', self.user_data.user_id, - 'roles', self.role_data.role_id]), - status_code=200), - ]) + use_role_name=True, + use_user_name=True, + use_project_name=True, + use_domain_name=True, + use_domain_in_query=True, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='projects', + append=[ + self.project_data.project_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=404, + ), + dict( + method='PUT', + uri=self.get_mock_url( + resource='projects', + append=[ + self.project_data.project_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id, + ], + ), + status_code=200, + ), + ] + ) self.register_uris(uris) self.assertTrue( @@ -1317,34 +1946,55 @@ def test_grant_both_project_and_domain(self): self.role_data.role_name, user=self.user_data.name, project=self.project_data.project_name, - domain=self.domain_data.domain_name)) + domain=self.domain_data.domain_name, + ) + ) def test_revoke_both_project_and_domain(self): uris = self._get_mock_role_query_urls( self.role_data, - project_data=self.project_data, user_data=self.user_data, + project_data=self.project_data, + user_data=self.user_data, domain_data=self.domain_data, - use_role_name=True, use_user_name=True, use_project_name=True, - use_domain_name=True, use_domain_in_query=True) - uris.extend([ - dict(method='HEAD', - uri=self.get_mock_url( - resource='projects', - append=[ - self.project_data.project_id, - 'users', self.user_data.user_id, - 'roles', self.role_data.role_id - ]), - complete_qs=True, - status_code=204), - dict(method='DELETE', - uri=self.get_mock_url( - resource='projects', - append=[self.project_data.project_id, - 'users', self.user_data.user_id, - 'roles', self.role_data.role_id]), - status_code=200), - ]) + use_role_name=True, + use_user_name=True, + use_project_name=True, + use_domain_name=True, + use_domain_in_query=True, + ) + uris.extend( + [ + dict( + method='HEAD', + uri=self.get_mock_url( + resource='projects', + append=[ + self.project_data.project_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id, + ], + ), + complete_qs=True, + status_code=204, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + resource='projects', + append=[ + self.project_data.project_id, + 'users', + self.user_data.user_id, + 'roles', + self.role_data.role_id, + ], + ), + status_code=200, + ), + ] + ) self.register_uris(uris) self.assertTrue( @@ -1352,76 +2002,98 @@ def test_revoke_both_project_and_domain(self): self.role_data.role_name, user=self.user_data.name, project=self.project_data.project_name, - domain=self.domain_data.domain_name)) + domain=self.domain_data.domain_name, + ) + ) def test_grant_no_project_or_domain(self): uris = self._get_mock_role_query_urls( self.role_data, user_data=self.user_data, - use_role_name=True, use_user_name=True) + use_role_name=True, + use_user_name=True, + ) self.register_uris(uris) with testtools.ExpectedException( exc.OpenStackCloudException, - 'Must specify either a domain, project or system' + 'Must specify either a domain, project or system', ): self.cloud.grant_role( - self.role_data.role_name, - user=self.user_data.name) + self.role_data.role_name, user=self.user_data.name + ) self.assert_calls() def test_revoke_no_project_or_domain_or_system(self): uris = self._get_mock_role_query_urls( self.role_data, user_data=self.user_data, - use_role_name=True, use_user_name=True) + use_role_name=True, + use_user_name=True, + ) self.register_uris(uris) with testtools.ExpectedException( exc.OpenStackCloudException, - 'Must specify either a domain, project or system' + 'Must specify either a domain, project or system', ): self.cloud.revoke_role( - self.role_data.role_name, - user=self.user_data.name) + self.role_data.role_name, user=self.user_data.name + ) self.assert_calls() def test_grant_bad_domain_exception(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - resource='domains', append=['baddomain']), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - resource='domains', - qs_elements=['name=baddomain']), - status_code=404) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='domains', append=['baddomain'] + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='domains', qs_elements=['name=baddomain'] + ), + status_code=404, + ), + ] + ) with testtools.ExpectedException(exc.OpenStackCloudURINotFound): self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, - domain='baddomain') + domain='baddomain', + ) self.assert_calls() def test_revoke_bad_domain_exception(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - resource='domains', append=['baddomain']), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - resource='domains', - qs_elements=['name=baddomain']), - status_code=404) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='domains', append=['baddomain'] + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='domains', qs_elements=['name=baddomain'] + ), + status_code=404, + ), + ] + ) with testtools.ExpectedException(exc.OpenStackCloudURINotFound): self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name, - domain='baddomain') + domain='baddomain', + ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_router.py b/openstack/tests/unit/cloud/test_router.py index 6f4fd4c67..a23ec6acc 100644 --- a/openstack/tests/unit/cloud/test_router.py +++ b/openstack/tests/unit/cloud/test_router.py @@ -41,14 +41,9 @@ class TestRouter(base.TestCase): 'id': router_id, 'name': router_name, 'project_id': u'861808a93da0484ea1767967c4df8a23', - 'routes': [ - { - "destination": "179.24.1.0/24", - "nexthop": "172.24.3.99" - } - ], + 'routes': [{"destination": "179.24.1.0/24", "nexthop": "172.24.3.99"}], 'status': u'ACTIVE', - 'tenant_id': u'861808a93da0484ea1767967c4df8a23' + 'tenant_id': u'861808a93da0484ea1767967c4df8a23', } mock_router_interface_rep = { @@ -58,7 +53,7 @@ class TestRouter(base.TestCase): 'subnet_ids': [subnet_id], 'port_id': '23999891-78b3-4a6b-818d-d1b713f67848', 'id': '57076620-dcfb-42ed-8ad6-79ccb4a79ed2', - 'request_ids': ['req-f1b0b1b4-ae51-4ef9-b371-0cc3c3402cf7'] + 'request_ids': ['req-f1b0b1b4-ae51-4ef9-b371-0cc3c3402cf7'], } router_availability_zone_extension = { @@ -66,7 +61,7 @@ class TestRouter(base.TestCase): "updated": "2015-01-01T10:00:00-00:00", "description": "Availability zone support for router.", "links": [], - "name": "Router Availability Zone" + "name": "Router Availability Zone", } router_extraroute_extension = { @@ -74,66 +69,100 @@ class TestRouter(base.TestCase): "updated": "2015-01-01T10:00:00-00:00", "description": "extra routes extension for router.", "links": [], - "name": "Extra Routes" + "name": "Extra Routes", } enabled_neutron_extensions = [ router_availability_zone_extension, - router_extraroute_extension] + router_extraroute_extension, + ] def _compare_routers(self, exp, real): self.assertDictEqual( _router.Router(**exp).to_dict(computed=False), - real.to_dict(computed=False)) + real.to_dict(computed=False), + ) def test_get_router(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'routers', self.router_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers'], - qs_elements=['name=%s' % self.router_name]), - json={'routers': [self.mock_router_rep]}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'routers', self.router_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'routers'], + qs_elements=['name=%s' % self.router_name], + ), + json={'routers': [self.mock_router_rep]}, + ), + ] + ) r = self.cloud.get_router(self.router_name) self.assertIsNotNone(r) self._compare_routers(self.mock_router_rep, r) self.assert_calls() def test_get_router_not_found(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'routers', 'mickey']), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers'], - qs_elements=['name=mickey']), - json={'routers': []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'routers', 'mickey'], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'routers'], + qs_elements=['name=mickey'], + ), + json={'routers': []}, + ), + ] + ) r = self.cloud.get_router('mickey') self.assertIsNone(r) self.assert_calls() def test_create_router(self): - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers']), - json={'router': self.mock_router_rep}, - validate=dict( - json={'router': { - 'name': self.router_name, - 'admin_state_up': True}})) - ]) - new_router = self.cloud.create_router(name=self.router_name, - admin_state_up=True) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'routers'] + ), + json={'router': self.mock_router_rep}, + validate=dict( + json={ + 'router': { + 'name': self.router_name, + 'admin_state_up': True, + } + } + ), + ) + ] + ) + new_router = self.cloud.create_router( + name=self.router_name, admin_state_up=True + ) self._compare_routers(self.mock_router_rep, new_router) self.assert_calls() @@ -143,136 +172,213 @@ def test_create_router_specific_tenant(self): mock_router_rep = copy.copy(self.mock_router_rep) mock_router_rep['tenant_id'] = new_router_tenant_id mock_router_rep['project_id'] = new_router_tenant_id - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers']), - json={'router': mock_router_rep}, - validate=dict( - json={'router': { - 'name': self.router_name, - 'admin_state_up': True, - 'project_id': new_router_tenant_id}})) - ]) - - self.cloud.create_router(self.router_name, - project_id=new_router_tenant_id) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'routers'] + ), + json={'router': mock_router_rep}, + validate=dict( + json={ + 'router': { + 'name': self.router_name, + 'admin_state_up': True, + 'project_id': new_router_tenant_id, + } + } + ), + ) + ] + ) + + self.cloud.create_router( + self.router_name, project_id=new_router_tenant_id + ) self.assert_calls() def test_create_router_with_availability_zone_hints(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers']), - json={'router': self.mock_router_rep}, - validate=dict( - json={'router': { - 'name': self.router_name, - 'admin_state_up': True, - 'availability_zone_hints': ['nova']}})) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': self.enabled_neutron_extensions}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'routers'] + ), + json={'router': self.mock_router_rep}, + validate=dict( + json={ + 'router': { + 'name': self.router_name, + 'admin_state_up': True, + 'availability_zone_hints': ['nova'], + } + } + ), + ), + ] + ) self.cloud.create_router( - name=self.router_name, admin_state_up=True, - availability_zone_hints=['nova']) + name=self.router_name, + admin_state_up=True, + availability_zone_hints=['nova'], + ) self.assert_calls() def test_create_router_without_enable_snat(self): """Do not send enable_snat when not given.""" - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers']), - json={'router': self.mock_router_rep}, - validate=dict( - json={'router': { - 'name': self.router_name, - 'admin_state_up': True}})) - ]) - self.cloud.create_router( - name=self.router_name, admin_state_up=True) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'routers'] + ), + json={'router': self.mock_router_rep}, + validate=dict( + json={ + 'router': { + 'name': self.router_name, + 'admin_state_up': True, + } + } + ), + ) + ] + ) + self.cloud.create_router(name=self.router_name, admin_state_up=True) self.assert_calls() def test_create_router_with_enable_snat_True(self): """Send enable_snat when it is True.""" - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers']), - json={'router': self.mock_router_rep}, - validate=dict( - json={'router': { - 'name': self.router_name, - 'admin_state_up': True, - 'external_gateway_info': {'enable_snat': True}}})) - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'routers'] + ), + json={'router': self.mock_router_rep}, + validate=dict( + json={ + 'router': { + 'name': self.router_name, + 'admin_state_up': True, + 'external_gateway_info': {'enable_snat': True}, + } + } + ), + ) + ] + ) self.cloud.create_router( - name=self.router_name, admin_state_up=True, enable_snat=True) + name=self.router_name, admin_state_up=True, enable_snat=True + ) self.assert_calls() def test_create_router_with_enable_snat_False(self): """Send enable_snat when it is False.""" - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers']), - json={'router': self.mock_router_rep}, - validate=dict( - json={'router': { - 'name': self.router_name, - 'external_gateway_info': {'enable_snat': False}, - 'admin_state_up': True}})) - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'routers'] + ), + json={'router': self.mock_router_rep}, + validate=dict( + json={ + 'router': { + 'name': self.router_name, + 'external_gateway_info': { + 'enable_snat': False + }, + 'admin_state_up': True, + } + } + ), + ) + ] + ) self.cloud.create_router( - name=self.router_name, admin_state_up=True, enable_snat=False) + name=self.router_name, admin_state_up=True, enable_snat=False + ) self.assert_calls() def test_create_router_wrong_availability_zone_hints_type(self): azh_opts = "invalid" with testtools.ExpectedException( exc.OpenStackCloudException, - "Parameter 'availability_zone_hints' must be a list" + "Parameter 'availability_zone_hints' must be a list", ): self.cloud.create_router( - name=self.router_name, admin_state_up=True, - availability_zone_hints=azh_opts) + name=self.router_name, + admin_state_up=True, + availability_zone_hints=azh_opts, + ) def test_add_router_interface(self): - self.register_uris([ - dict(method='PUT', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'routers', self.router_id, - 'add_router_interface']), - json={'port': self.mock_router_interface_rep}, - validate=dict( - json={'subnet_id': self.subnet_id})) - ]) + self.register_uris( + [ + dict( + method='PUT', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'routers', + self.router_id, + 'add_router_interface', + ], + ), + json={'port': self.mock_router_interface_rep}, + validate=dict(json={'subnet_id': self.subnet_id}), + ) + ] + ) self.cloud.add_router_interface( - {'id': self.router_id}, subnet_id=self.subnet_id) + {'id': self.router_id}, subnet_id=self.subnet_id + ) self.assert_calls() def test_remove_router_interface(self): - self.register_uris([ - dict(method='PUT', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'routers', self.router_id, - 'remove_router_interface']), - json={'port': self.mock_router_interface_rep}, - validate=dict( - json={'subnet_id': self.subnet_id})) - ]) + self.register_uris( + [ + dict( + method='PUT', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'routers', + self.router_id, + 'remove_router_interface', + ], + ), + json={'port': self.mock_router_interface_rep}, + validate=dict(json={'subnet_id': self.subnet_id}), + ) + ] + ) self.cloud.remove_router_interface( - {'id': self.router_id}, subnet_id=self.subnet_id) + {'id': self.router_id}, subnet_id=self.subnet_id + ) self.assert_calls() def test_remove_router_interface_missing_argument(self): - self.assertRaises(ValueError, self.cloud.remove_router_interface, - {'id': '123'}) + self.assertRaises( + ValueError, self.cloud.remove_router_interface, {'id': '123'} + ) def test_update_router(self): new_router_name = "mickey" @@ -283,114 +389,179 @@ def test_update_router(self): # validate_calls() asserts that these requests are done in order, # but the extensions call is only called if a non-None value is # passed in 'routes' - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json={'extensions': self.enabled_neutron_extensions}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers', - self.router_id]), - json=self.mock_router_rep), - dict(method='PUT', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'routers', self.router_id]), - json={'router': expected_router_rep}, - validate=dict( - json={'router': { - 'name': new_router_name, - 'routes': new_routes}})) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json={'extensions': self.enabled_neutron_extensions}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'routers', self.router_id], + ), + json=self.mock_router_rep, + ), + dict( + method='PUT', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'routers', self.router_id], + ), + json={'router': expected_router_rep}, + validate=dict( + json={ + 'router': { + 'name': new_router_name, + 'routes': new_routes, + } + } + ), + ), + ] + ) new_router = self.cloud.update_router( - self.router_id, name=new_router_name, routes=new_routes) + self.router_id, name=new_router_name, routes=new_routes + ) self._compare_routers(expected_router_rep, new_router) self.assert_calls() def test_delete_router(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'routers', self.router_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers'], - qs_elements=['name=%s' % self.router_name]), - json={'routers': [self.mock_router_rep]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'routers', self.router_id]), - json={}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'routers', self.router_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'routers'], + qs_elements=['name=%s' % self.router_name], + ), + json={'routers': [self.mock_router_rep]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'routers', self.router_id], + ), + json={}, + ), + ] + ) self.assertTrue(self.cloud.delete_router(self.router_name)) self.assert_calls() def test_delete_router_not_found(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'routers', self.router_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers'], - qs_elements=['name=%s' % self.router_name]), - json={'routers': []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'routers', self.router_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'routers'], + qs_elements=['name=%s' % self.router_name], + ), + json={'routers': []}, + ), + ] + ) self.assertFalse(self.cloud.delete_router(self.router_name)) self.assert_calls() def test_delete_router_multiple_found(self): router1 = dict(id='123', name='mickey') router2 = dict(id='456', name='mickey') - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'routers', 'mickey']), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'routers'], - qs_elements=['name=mickey']), - json={'routers': [router1, router2]}) - ]) - self.assertRaises(exc.OpenStackCloudException, - self.cloud.delete_router, - 'mickey') + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'routers', 'mickey'], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'routers'], + qs_elements=['name=mickey'], + ), + json={'routers': [router1, router2]}, + ), + ] + ) + self.assertRaises( + exc.OpenStackCloudException, self.cloud.delete_router, 'mickey' + ) self.assert_calls() - def _test_list_router_interfaces(self, router, interface_type, - expected_result=None): + def _test_list_router_interfaces( + self, router, interface_type, expected_result=None + ): internal_ports = [ { 'id': 'internal_port_id', - 'fixed_ips': [{ - 'subnet_id': 'internal_subnet_id', - 'ip_address': "10.0.0.1" - }], + 'fixed_ips': [ + { + 'subnet_id': 'internal_subnet_id', + 'ip_address': "10.0.0.1", + } + ], 'device_id': self.router_id, - 'device_owner': device_owner + 'device_owner': device_owner, } - for device_owner in ['network:router_interface', - 'network:ha_router_replicated_interface', - 'network:router_interface_distributed']] - - external_ports = [{ - 'id': 'external_port_id', - 'fixed_ips': [{ - 'subnet_id': 'external_subnet_id', - 'ip_address': "1.2.3.4" - }], - 'device_id': self.router_id, - 'device_owner': 'network:router_gateway' - }] + for device_owner in [ + 'network:router_interface', + 'network:ha_router_replicated_interface', + 'network:router_interface_distributed', + ] + ] + + external_ports = [ + { + 'id': 'external_port_id', + 'fixed_ips': [ + { + 'subnet_id': 'external_subnet_id', + 'ip_address': "1.2.3.4", + } + ], + 'device_id': self.router_id, + 'device_owner': 'network:router_gateway', + } + ] if expected_result is None: if interface_type == "internal": @@ -400,37 +571,43 @@ def _test_list_router_interfaces(self, router, interface_type, else: expected_result = internal_ports + external_ports - mock_uri = dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'ports'], - qs_elements=["device_id=%s" % self.router_id]), - json={'ports': (internal_ports + external_ports)}) + mock_uri = dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'ports'], + qs_elements=["device_id=%s" % self.router_id], + ), + json={'ports': (internal_ports + external_ports)}, + ) self.register_uris([mock_uri]) ret = self.cloud.list_router_interfaces(router, interface_type) self.assertEqual( [_port.Port(**i).to_dict(computed=False) for i in expected_result], - [i.to_dict(computed=False) for i in ret] + [i.to_dict(computed=False) for i in ret], ) self.assert_calls() router = { 'id': router_id, 'external_gateway_info': { - 'external_fixed_ips': [{ - 'subnet_id': 'external_subnet_id', - 'ip_address': '1.2.3.4'}] - } + 'external_fixed_ips': [ + {'subnet_id': 'external_subnet_id', 'ip_address': '1.2.3.4'} + ] + }, } def test_list_router_interfaces_all(self): - self._test_list_router_interfaces(self.router, - interface_type=None) + self._test_list_router_interfaces(self.router, interface_type=None) def test_list_router_interfaces_internal(self): - self._test_list_router_interfaces(self.router, - interface_type="internal") + self._test_list_router_interfaces( + self.router, interface_type="internal" + ) def test_list_router_interfaces_external(self): - self._test_list_router_interfaces(self.router, - interface_type="external") + self._test_list_router_interfaces( + self.router, interface_type="external" + ) diff --git a/openstack/tests/unit/cloud/test_security_groups.py b/openstack/tests/unit/cloud/test_security_groups.py index ebac22bf5..c76ca430b 100644 --- a/openstack/tests/unit/cloud/test_security_groups.py +++ b/openstack/tests/unit/cloud/test_security_groups.py @@ -25,9 +25,14 @@ name='neutron-sec-group', description='Test Neutron security group', rules=[ - dict(id='1', port_range_min=80, port_range_max=81, - protocol='tcp', remote_ip_prefix='0.0.0.0/0') - ] + dict( + id='1', + port_range_min=80, + port_range_max=81, + protocol='tcp', + remote_ip_prefix='0.0.0.0/0', + ) + ], ) @@ -37,43 +42,58 @@ description='Test Nova security group #1', rules=[ fakes.make_fake_nova_security_group_rule( - id='2', from_port=8000, to_port=8001, ip_protocol='tcp', - cidr='0.0.0.0/0'), - ] + id='2', + from_port=8000, + to_port=8001, + ip_protocol='tcp', + cidr='0.0.0.0/0', + ), + ], ) class TestSecurityGroups(base.TestCase): - def setUp(self): super(TestSecurityGroups, self).setUp() self.has_neutron = True def fake_has_service(*args, **kwargs): return self.has_neutron + self.cloud.has_service = fake_has_service def test_list_security_groups_neutron(self): project_id = 42 self.cloud.secgroup_source = 'neutron' - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'security-groups'], - qs_elements=["project_id=%s" % project_id]), - json={'security_groups': [neutron_grp_dict]}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'security-groups'], + qs_elements=["project_id=%s" % project_id], + ), + json={'security_groups': [neutron_grp_dict]}, + ) + ] + ) self.cloud.list_security_groups(filters={'project_id': project_id}) self.assert_calls() def test_list_security_groups_nova(self): - self.register_uris([ - dict(method='GET', - uri='{endpoint}/os-security-groups?project_id=42'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_groups': []}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/os-security-groups?project_id=42'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'security_groups': []}, + ), + ] + ) self.cloud.secgroup_source = 'nova' self.has_neutron = False self.cloud.list_security_groups(filters={'project_id': 42}) @@ -84,25 +104,35 @@ def test_list_security_groups_none(self): self.cloud.secgroup_source = None self.has_neutron = False - self.assertRaises(openstack.cloud.OpenStackCloudUnavailableFeature, - self.cloud.list_security_groups) + self.assertRaises( + openstack.cloud.OpenStackCloudUnavailableFeature, + self.cloud.list_security_groups, + ) def test_delete_security_group_neutron(self): sg_id = neutron_grp_dict['id'] self.cloud.secgroup_source = 'neutron' - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'security-groups']), - json={'security_groups': [neutron_grp_dict]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'security-groups', '%s' % sg_id]), - status_code=200, - json={}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'security-groups'] + ), + json={'security_groups': [neutron_grp_dict]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'security-groups', '%s' % sg_id], + ), + status_code=200, + json={}, + ), + ] + ) self.assertTrue(self.cloud.delete_security_group('1')) self.assert_calls() @@ -110,27 +140,39 @@ def test_delete_security_group_nova(self): self.cloud.secgroup_source = 'nova' self.has_neutron = False nova_return = [nova_grp_dict] - self.register_uris([ - dict(method='GET', - uri='{endpoint}/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_groups': nova_return}), - dict(method='DELETE', - uri='{endpoint}/os-security-groups/2'.format( - endpoint=fakes.COMPUTE_ENDPOINT)), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'security_groups': nova_return}, + ), + dict( + method='DELETE', + uri='{endpoint}/os-security-groups/2'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + ), + ] + ) self.cloud.delete_security_group('2') self.assert_calls() def test_delete_security_group_neutron_not_found(self): self.cloud.secgroup_source = 'neutron' - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'security-groups']), - json={'security_groups': [neutron_grp_dict]}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'security-groups'] + ), + json={'security_groups': [neutron_grp_dict]}, + ) + ] + ) self.assertFalse(self.cloud.delete_security_group('10')) self.assert_calls() @@ -138,41 +180,53 @@ def test_delete_security_group_nova_not_found(self): self.cloud.secgroup_source = 'nova' self.has_neutron = False nova_return = [nova_grp_dict] - self.register_uris([ - dict(method='GET', - uri='{endpoint}/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_groups': nova_return}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'security_groups': nova_return}, + ), + ] + ) self.assertFalse(self.cloud.delete_security_group('doesNotExist')) def test_delete_security_group_none(self): self.cloud.secgroup_source = None - self.assertRaises(openstack.cloud.OpenStackCloudUnavailableFeature, - self.cloud.delete_security_group, - 'doesNotExist') + self.assertRaises( + openstack.cloud.OpenStackCloudUnavailableFeature, + self.cloud.delete_security_group, + 'doesNotExist', + ) def test_create_security_group_neutron(self): self.cloud.secgroup_source = 'neutron' group_name = self.getUniqueString() group_desc = self.getUniqueString('description') new_group = fakes.make_fake_neutron_security_group( - id='2', - name=group_name, - description=group_desc, - rules=[]) - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'security-groups']), - json={'security_group': new_group}, - validate=dict( - json={'security_group': { - 'name': group_name, - 'description': group_desc - }})) - ]) + id='2', name=group_name, description=group_desc, rules=[] + ) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'security-groups'] + ), + json={'security_group': new_group}, + validate=dict( + json={ + 'security_group': { + 'name': group_name, + 'description': group_desc, + } + } + ), + ) + ] + ) r = self.cloud.create_security_group(group_name, group_desc) self.assertEqual(group_name, r['name']) @@ -185,32 +239,40 @@ def test_create_security_group_neutron_specific_tenant(self): self.cloud.secgroup_source = 'neutron' project_id = "861808a93da0484ea1767967c4df8a23" group_name = self.getUniqueString() - group_desc = 'security group from' \ - ' test_create_security_group_neutron_specific_tenant' + group_desc = ( + 'security group from' + ' test_create_security_group_neutron_specific_tenant' + ) new_group = fakes.make_fake_neutron_security_group( id='2', name=group_name, description=group_desc, project_id=project_id, - rules=[]) - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'security-groups']), - json={'security_group': new_group}, - validate=dict( - json={'security_group': { - 'name': group_name, - 'description': group_desc, - 'tenant_id': project_id - }})) - ]) + rules=[], + ) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'security-groups'] + ), + json={'security_group': new_group}, + validate=dict( + json={ + 'security_group': { + 'name': group_name, + 'description': group_desc, + 'tenant_id': project_id, + } + } + ), + ) + ] + ) r = self.cloud.create_security_group( - group_name, - group_desc, - project_id + group_name, group_desc, project_id ) self.assertEqual(group_name, r['name']) self.assertEqual(group_desc, r['description']) @@ -227,23 +289,32 @@ def test_create_security_group_stateless_neutron(self): name=group_name, description=group_desc, stateful=False, - rules=[]) - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'security-groups']), - json={'security_group': new_group}, - validate=dict( - json={'security_group': { - 'name': group_name, - 'description': group_desc, - 'stateful': False - }})) - ]) - - r = self.cloud.create_security_group(group_name, group_desc, - stateful=False) + rules=[], + ) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'security-groups'] + ), + json={'security_group': new_group}, + validate=dict( + json={ + 'security_group': { + 'name': group_name, + 'description': group_desc, + 'stateful': False, + } + } + ), + ) + ] + ) + + r = self.cloud.create_security_group( + group_name, group_desc, stateful=False + ) self.assertEqual(group_name, r['name']) self.assertEqual(group_desc, r['description']) self.assertEqual(False, r['stateful']) @@ -254,21 +325,27 @@ def test_create_security_group_nova(self): self.has_neutron = False group_desc = self.getUniqueString('description') new_group = fakes.make_fake_nova_security_group( - id='2', - name=group_name, - description=group_desc, - rules=[]) - self.register_uris([ - dict(method='POST', - uri='{endpoint}/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_group': new_group}, - validate=dict(json={ - 'security_group': { - 'name': group_name, - 'description': group_desc - }})), - ]) + id='2', name=group_name, description=group_desc, rules=[] + ) + self.register_uris( + [ + dict( + method='POST', + uri='{endpoint}/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'security_group': new_group}, + validate=dict( + json={ + 'security_group': { + 'name': group_name, + 'description': group_desc, + } + } + ), + ), + ] + ) self.cloud.secgroup_source = 'nova' r = self.cloud.create_security_group(group_name, group_desc) @@ -280,9 +357,12 @@ def test_create_security_group_nova(self): def test_create_security_group_none(self): self.cloud.secgroup_source = None self.has_neutron = False - self.assertRaises(openstack.cloud.OpenStackCloudUnavailableFeature, - self.cloud.create_security_group, - '', '') + self.assertRaises( + openstack.cloud.OpenStackCloudUnavailableFeature, + self.cloud.create_security_group, + '', + '', + ) def test_update_security_group_neutron(self): self.cloud.secgroup_source = 'neutron' @@ -291,22 +371,37 @@ def test_update_security_group_neutron(self): update_return = neutron_grp_dict.copy() update_return['name'] = new_name update_return['stateful'] = False - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'security-groups']), - json={'security_groups': [neutron_grp_dict]}), - dict(method='PUT', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'security-groups', '%s' % sg_id]), - json={'security_group': update_return}, - validate=dict(json={ - 'security_group': {'name': new_name, 'stateful': False}})) - ]) - r = self.cloud.update_security_group(sg_id, name=new_name, - stateful=False) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'security-groups'] + ), + json={'security_groups': [neutron_grp_dict]}, + ), + dict( + method='PUT', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'security-groups', '%s' % sg_id], + ), + json={'security_group': update_return}, + validate=dict( + json={ + 'security_group': { + 'name': new_name, + 'stateful': False, + } + } + ), + ), + ] + ) + r = self.cloud.update_security_group( + sg_id, name=new_name, stateful=False + ) self.assertEqual(r['name'], new_name) self.assertEqual(r['stateful'], False) self.assert_calls() @@ -319,26 +414,38 @@ def test_update_security_group_nova(self): update_return = nova_grp_dict.copy() update_return['name'] = new_name - self.register_uris([ - dict(method='GET', - uri='{endpoint}/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_groups': nova_return}), - dict(method='PUT', - uri='{endpoint}/os-security-groups/2'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_group': update_return}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'security_groups': nova_return}, + ), + dict( + method='PUT', + uri='{endpoint}/os-security-groups/2'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'security_group': update_return}, + ), + ] + ) r = self.cloud.update_security_group( - nova_grp_dict['id'], name=new_name) + nova_grp_dict['id'], name=new_name + ) self.assertEqual(r['name'], new_name) self.assert_calls() def test_update_security_group_bad_kwarg(self): - self.assertRaises(TypeError, - self.cloud.update_security_group, - 'doesNotExist', bad_arg='') + self.assertRaises( + TypeError, + self.cloud.update_security_group, + 'doesNotExist', + bad_arg='', + ) def test_create_security_group_rule_neutron(self): self.cloud.secgroup_source = 'neutron' @@ -350,7 +457,7 @@ def test_create_security_group_rule_neutron(self): remote_group_id='456', remote_address_group_id='1234-5678', direction='egress', - ethertype='IPv6' + ethertype='IPv6', ) expected_args = copy.copy(args) # For neutron, -1 port should be converted to None @@ -362,24 +469,30 @@ def test_create_security_group_rule_neutron(self): expected_new_rule['tenant_id'] = None expected_new_rule['project_id'] = expected_new_rule['tenant_id'] - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'security-groups']), - json={'security_groups': [neutron_grp_dict]}), - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'security-group-rules']), - json={'security_group_rule': expected_new_rule}, - validate=dict(json={ - 'security_group_rule': expected_args})) - ]) - new_rule = self.cloud.create_security_group_rule( - secgroup_name_or_id=neutron_grp_dict['id'], **args).to_dict( - original_names=True + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'security-groups'] + ), + json={'security_groups': [neutron_grp_dict]}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'security-group-rules'], + ), + json={'security_group_rule': expected_new_rule}, + validate=dict(json={'security_group_rule': expected_args}), + ), + ] ) + new_rule = self.cloud.create_security_group_rule( + secgroup_name_or_id=neutron_grp_dict['id'], **args + ).to_dict(original_names=True) # NOTE(gtema): don't check location and not relevant properties # in new rule new_rule.pop('created_at') @@ -403,7 +516,7 @@ def test_create_security_group_rule_neutron_specific_tenant(self): remote_address_group_id=None, direction='egress', ethertype='IPv6', - project_id='861808a93da0484ea1767967c4df8a23' + project_id='861808a93da0484ea1767967c4df8a23', ) expected_args = copy.copy(args) # For neutron, -1 port should be converted to None @@ -420,24 +533,30 @@ def test_create_security_group_rule_neutron_specific_tenant(self): # JSON; see SecurityGroupRule where it is removed. expected_args.pop('remote_address_group_id') - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'security-groups']), - json={'security_groups': [neutron_grp_dict]}), - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'security-group-rules']), - json={'security_group_rule': expected_new_rule}, - validate=dict(json={ - 'security_group_rule': expected_args})) - ]) - new_rule = self.cloud.create_security_group_rule( - secgroup_name_or_id=neutron_grp_dict['id'], ** args).to_dict( - original_names=True + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'security-groups'] + ), + json={'security_groups': [neutron_grp_dict]}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'security-group-rules'], + ), + json={'security_group_rule': expected_new_rule}, + validate=dict(json={'security_group_rule': expected_args}), + ), + ] ) + new_rule = self.cloud.create_security_group_rule( + secgroup_name_or_id=neutron_grp_dict['id'], **args + ).to_dict(original_names=True) # NOTE(slaweq): don't check location and properties in new rule new_rule.pop('created_at') new_rule.pop('description') @@ -456,31 +575,52 @@ def test_create_security_group_rule_nova(self): nova_return = [nova_grp_dict] new_rule = fakes.make_fake_nova_security_group_rule( - id='xyz', from_port=1, to_port=2000, ip_protocol='tcp', - cidr='1.2.3.4/32') - - self.register_uris([ - dict(method='GET', - uri='{endpoint}/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_groups': nova_return}), - dict(method='POST', - uri='{endpoint}/os-security-group-rules'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_group_rule': new_rule}, - validate=dict(json={ - "security_group_rule": { - "from_port": 1, - "ip_protocol": "tcp", - "to_port": 2000, - "parent_group_id": "2", - "cidr": "1.2.3.4/32", - "group_id": "123"}})), - ]) + id='xyz', + from_port=1, + to_port=2000, + ip_protocol='tcp', + cidr='1.2.3.4/32', + ) + + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'security_groups': nova_return}, + ), + dict( + method='POST', + uri='{endpoint}/os-security-group-rules'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'security_group_rule': new_rule}, + validate=dict( + json={ + "security_group_rule": { + "from_port": 1, + "ip_protocol": "tcp", + "to_port": 2000, + "parent_group_id": "2", + "cidr": "1.2.3.4/32", + "group_id": "123", + } + } + ), + ), + ] + ) self.cloud.create_security_group_rule( - '2', port_range_min=1, port_range_max=2000, protocol='tcp', - remote_ip_prefix='1.2.3.4/32', remote_group_id='123') + '2', + port_range_min=1, + port_range_max=2000, + protocol='tcp', + remote_ip_prefix='1.2.3.4/32', + remote_group_id='123', + ) self.assert_calls() @@ -490,65 +630,100 @@ def test_create_security_group_rule_nova_no_ports(self): self.cloud.secgroup_source = 'nova' new_rule = fakes.make_fake_nova_security_group_rule( - id='xyz', from_port=1, to_port=65535, ip_protocol='tcp', - cidr='1.2.3.4/32') + id='xyz', + from_port=1, + to_port=65535, + ip_protocol='tcp', + cidr='1.2.3.4/32', + ) nova_return = [nova_grp_dict] - self.register_uris([ - dict(method='GET', - uri='{endpoint}/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_groups': nova_return}), - dict(method='POST', - uri='{endpoint}/os-security-group-rules'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_group_rule': new_rule}, - validate=dict(json={ - "security_group_rule": { - "from_port": 1, - "ip_protocol": "tcp", - "to_port": 65535, - "parent_group_id": "2", - "cidr": "1.2.3.4/32", - "group_id": "123"}})), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'security_groups': nova_return}, + ), + dict( + method='POST', + uri='{endpoint}/os-security-group-rules'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'security_group_rule': new_rule}, + validate=dict( + json={ + "security_group_rule": { + "from_port": 1, + "ip_protocol": "tcp", + "to_port": 65535, + "parent_group_id": "2", + "cidr": "1.2.3.4/32", + "group_id": "123", + } + } + ), + ), + ] + ) self.cloud.create_security_group_rule( - '2', protocol='tcp', - remote_ip_prefix='1.2.3.4/32', remote_group_id='123') + '2', + protocol='tcp', + remote_ip_prefix='1.2.3.4/32', + remote_group_id='123', + ) self.assert_calls() def test_create_security_group_rule_none(self): self.has_neutron = False self.cloud.secgroup_source = None - self.assertRaises(openstack.cloud.OpenStackCloudUnavailableFeature, - self.cloud.create_security_group_rule, - '') + self.assertRaises( + openstack.cloud.OpenStackCloudUnavailableFeature, + self.cloud.create_security_group_rule, + '', + ) def test_delete_security_group_rule_neutron(self): rule_id = "xyz" self.cloud.secgroup_source = 'neutron' - self.register_uris([ - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'security-group-rules', - '%s' % rule_id]), - json={}) - ]) + self.register_uris( + [ + dict( + method='DELETE', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'security-group-rules', + '%s' % rule_id, + ], + ), + json={}, + ) + ] + ) self.assertTrue(self.cloud.delete_security_group_rule(rule_id)) self.assert_calls() def test_delete_security_group_rule_nova(self): self.has_neutron = False self.cloud.secgroup_source = 'nova' - self.register_uris([ - dict(method='DELETE', - uri='{endpoint}/os-security-group-rules/xyz'.format( - endpoint=fakes.COMPUTE_ENDPOINT)), - ]) + self.register_uris( + [ + dict( + method='DELETE', + uri='{endpoint}/os-security-group-rules/xyz'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + ), + ] + ) r = self.cloud.delete_security_group_rule('xyz') self.assertTrue(r) self.assert_calls() @@ -556,32 +731,43 @@ def test_delete_security_group_rule_nova(self): def test_delete_security_group_rule_none(self): self.has_neutron = False self.cloud.secgroup_source = None - self.assertRaises(openstack.cloud.OpenStackCloudUnavailableFeature, - self.cloud.delete_security_group_rule, - '') + self.assertRaises( + openstack.cloud.OpenStackCloudUnavailableFeature, + self.cloud.delete_security_group_rule, + '', + ) def test_delete_security_group_rule_not_found(self): rule_id = "doesNotExist" self.cloud.secgroup_source = 'neutron' - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'security-groups']), - json={'security_groups': [neutron_grp_dict]}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'security-groups'] + ), + json={'security_groups': [neutron_grp_dict]}, + ) + ] + ) self.assertFalse(self.cloud.delete_security_group(rule_id)) self.assert_calls() def test_delete_security_group_rule_not_found_nova(self): self.has_neutron = False self.cloud.secgroup_source = 'nova' - self.register_uris([ - dict(method='GET', - uri='{endpoint}/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_groups': [nova_grp_dict]}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'security_groups': [nova_grp_dict]}, + ), + ] + ) r = self.cloud.delete_security_group('doesNotExist') self.assertFalse(r) @@ -590,16 +776,23 @@ def test_delete_security_group_rule_not_found_nova(self): def test_nova_egress_security_group_rule(self): self.has_neutron = False self.cloud.secgroup_source = 'nova' - self.register_uris([ - dict(method='GET', - uri='{endpoint}/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_groups': [nova_grp_dict]}), - ]) - self.assertRaises(openstack.cloud.OpenStackCloudException, - self.cloud.create_security_group_rule, - secgroup_name_or_id='nova-sec-group', - direction='egress') + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'security_groups': [nova_grp_dict]}, + ), + ] + ) + self.assertRaises( + openstack.cloud.OpenStackCloudException, + self.cloud.create_security_group_rule, + secgroup_name_or_id='nova-sec-group', + direction='egress', + ) self.assert_calls() @@ -608,24 +801,30 @@ def test_list_server_security_groups_nova(self): server = fakes.make_fake_server('1234', 'server-name', 'ACTIVE') - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', server['id']]), - json=server), - dict( - method='GET', - uri='{endpoint}/servers/{id}/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT, - id=server['id']), - json={'security_groups': [nova_grp_dict]}), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', server['id']] + ), + json=server, + ), + dict( + method='GET', + uri='{endpoint}/servers/{id}/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT, id=server['id'] + ), + json={'security_groups': [nova_grp_dict]}, + ), + ] + ) groups = self.cloud.list_server_security_groups(server) self.assertEqual( groups[0]['rules'][0]['ip_range']['cidr'], - nova_grp_dict['rules'][0]['ip_range']['cidr']) + nova_grp_dict['rules'][0]['ip_range']['cidr'], + ) self.assert_calls() @@ -641,25 +840,31 @@ def test_add_security_group_to_server_nova(self): self.has_neutron = False self.cloud.secgroup_source = 'nova' - self.register_uris([ - dict( - method='GET', - uri='{endpoint}/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT, + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT, + ), + json={'security_groups': [nova_grp_dict]}, ), - json={'security_groups': [nova_grp_dict]}), - self.get_nova_discovery_mock_dict(), - dict( - method='POST', - uri='%s/servers/%s/action' % (fakes.COMPUTE_ENDPOINT, '1234'), - validate=dict( - json={'addSecurityGroup': {'name': 'nova-sec-group'}}), - status_code=202, - ), - ]) + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri='%s/servers/%s/action' + % (fakes.COMPUTE_ENDPOINT, '1234'), + validate=dict( + json={'addSecurityGroup': {'name': 'nova-sec-group'}} + ), + status_code=202, + ), + ] + ) ret = self.cloud.add_server_security_groups( - dict(id='1234'), 'nova-sec-group') + dict(id='1234'), 'nova-sec-group' + ) self.assertTrue(ret) @@ -672,27 +877,42 @@ def test_add_security_group_to_server_neutron(self): # use neutron for secgroup list and return an existing fake self.cloud.secgroup_source = 'neutron' - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', 'detail']), - json={'servers': [fake_server]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'security-groups']), - json={'security_groups': [neutron_grp_dict]}), - dict(method='POST', - uri='%s/servers/%s/action' % (fakes.COMPUTE_ENDPOINT, '1234'), - validate=dict( - json={'addSecurityGroup': {'name': 'neutron-sec-group'}}), - status_code=202), - ]) - - self.assertTrue(self.cloud.add_server_security_groups( - 'server-name', 'neutron-sec-group')) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + json={'servers': [fake_server]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'security-groups'] + ), + json={'security_groups': [neutron_grp_dict]}, + ), + dict( + method='POST', + uri='%s/servers/%s/action' + % (fakes.COMPUTE_ENDPOINT, '1234'), + validate=dict( + json={ + 'addSecurityGroup': {'name': 'neutron-sec-group'} + } + ), + status_code=202, + ), + ] + ) + + self.assertTrue( + self.cloud.add_server_security_groups( + 'server-name', 'neutron-sec-group' + ) + ) self.assert_calls() def test_remove_security_group_from_server_nova(self): @@ -700,23 +920,32 @@ def test_remove_security_group_from_server_nova(self): self.has_neutron = False self.cloud.secgroup_source = 'nova' - self.register_uris([ - dict( - method='GET', - uri='{endpoint}/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_groups': [nova_grp_dict]}), - self.get_nova_discovery_mock_dict(), - dict( - method='POST', - uri='%s/servers/%s/action' % (fakes.COMPUTE_ENDPOINT, '1234'), - validate=dict( - json={'removeSecurityGroup': {'name': 'nova-sec-group'}}), - ), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'security_groups': [nova_grp_dict]}, + ), + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri='%s/servers/%s/action' + % (fakes.COMPUTE_ENDPOINT, '1234'), + validate=dict( + json={ + 'removeSecurityGroup': {'name': 'nova-sec-group'} + } + ), + ), + ] + ) ret = self.cloud.remove_server_security_groups( - dict(id='1234'), 'nova-sec-group') + dict(id='1234'), 'nova-sec-group' + ) self.assertTrue(ret) self.assert_calls() @@ -729,25 +958,37 @@ def test_remove_security_group_from_server_neutron(self): self.cloud.secgroup_source = 'neutron' validate = {'removeSecurityGroup': {'name': 'neutron-sec-group'}} - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', 'detail']), - json={'servers': [fake_server]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'security-groups']), - json={'security_groups': [neutron_grp_dict]}), - dict(method='POST', - uri='%s/servers/%s/action' % (fakes.COMPUTE_ENDPOINT, '1234'), - validate=dict(json=validate)), - ]) - - self.assertTrue(self.cloud.remove_server_security_groups( - 'server-name', 'neutron-sec-group')) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + json={'servers': [fake_server]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'security-groups'] + ), + json={'security_groups': [neutron_grp_dict]}, + ), + dict( + method='POST', + uri='%s/servers/%s/action' + % (fakes.COMPUTE_ENDPOINT, '1234'), + validate=dict(json=validate), + ), + ] + ) + + self.assertTrue( + self.cloud.remove_server_security_groups( + 'server-name', 'neutron-sec-group' + ) + ) self.assert_calls() def test_add_bad_security_group_to_server_nova(self): @@ -757,22 +998,29 @@ def test_add_bad_security_group_to_server_nova(self): # use nova for secgroup list and return an existing fake self.has_neutron = False self.cloud.secgroup_source = 'nova' - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict( - method='GET', - uri='{endpoint}/servers/detail'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'servers': [fake_server]}), - dict( - method='GET', - uri='{endpoint}/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'security_groups': [nova_grp_dict]}), - ]) - - ret = self.cloud.add_server_security_groups('server-name', - 'unknown-sec-group') + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri='{endpoint}/servers/detail'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'servers': [fake_server]}, + ), + dict( + method='GET', + uri='{endpoint}/os-security-groups'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'security_groups': [nova_grp_dict]}, + ), + ] + ) + + ret = self.cloud.add_server_security_groups( + 'server-name', 'unknown-sec-group' + ) self.assertFalse(ret) self.assert_calls() @@ -784,69 +1032,96 @@ def test_add_bad_security_group_to_server_neutron(self): # use neutron for secgroup list and return an existing fake self.cloud.secgroup_source = 'neutron' - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', 'detail']), - json={'servers': [fake_server]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'security-groups']), - json={'security_groups': [neutron_grp_dict]}) - ]) - self.assertFalse(self.cloud.add_server_security_groups( - 'server-name', 'unknown-sec-group')) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + json={'servers': [fake_server]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'security-groups'] + ), + json={'security_groups': [neutron_grp_dict]}, + ), + ] + ) + self.assertFalse( + self.cloud.add_server_security_groups( + 'server-name', 'unknown-sec-group' + ) + ) self.assert_calls() def test_add_security_group_to_bad_server(self): # fake to get server by name, server-name must match fake_server = fakes.make_fake_server('1234', 'server-name', 'ACTIVE') - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict( - method='GET', - uri='{endpoint}/servers/detail'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={'servers': [fake_server]}), - ]) - - ret = self.cloud.add_server_security_groups('unknown-server-name', - 'nova-sec-group') + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri='{endpoint}/servers/detail'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={'servers': [fake_server]}, + ), + ] + ) + + ret = self.cloud.add_server_security_groups( + 'unknown-server-name', 'nova-sec-group' + ) self.assertFalse(ret) self.assert_calls() def test_get_security_group_by_id_neutron(self): self.cloud.secgroup_source = 'neutron' - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', - 'security-groups', - neutron_grp_dict['id']]), - json={'security_group': neutron_grp_dict}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'security-groups', + neutron_grp_dict['id'], + ], + ), + json={'security_group': neutron_grp_dict}, + ) + ] + ) ret_sg = self.cloud.get_security_group_by_id(neutron_grp_dict['id']) self.assertEqual(neutron_grp_dict['id'], ret_sg['id']) self.assertEqual(neutron_grp_dict['name'], ret_sg['name']) - self.assertEqual(neutron_grp_dict['description'], - ret_sg['description']) + self.assertEqual( + neutron_grp_dict['description'], ret_sg['description'] + ) self.assertEqual(neutron_grp_dict['stateful'], ret_sg['stateful']) self.assert_calls() def test_get_security_group_by_id_nova(self): - self.register_uris([ - dict(method='GET', - uri='{endpoint}/os-security-groups/{id}'.format( - endpoint=fakes.COMPUTE_ENDPOINT, - id=nova_grp_dict['id']), - json={'security_group': nova_grp_dict}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/os-security-groups/{id}'.format( + endpoint=fakes.COMPUTE_ENDPOINT, id=nova_grp_dict['id'] + ), + json={'security_group': nova_grp_dict}, + ), + ] + ) self.cloud.secgroup_source = 'nova' self.has_neutron = False ret_sg = self.cloud.get_security_group_by_id(nova_grp_dict['id']) @@ -860,9 +1135,15 @@ def test_normalize_secgroups(self): name='nova_secgroup', description='A Nova security group', rules=[ - dict(id='123', from_port=80, to_port=81, ip_protocol='tcp', - ip_range={'cidr': '0.0.0.0/0'}, parent_group_id='xyz123') - ] + dict( + id='123', + from_port=80, + to_port=81, + ip_protocol='tcp', + ip_range={'cidr': '0.0.0.0/0'}, + parent_group_id='xyz123', + ) + ], ) expected = dict( id='abc123', @@ -878,24 +1159,37 @@ def test_normalize_secgroups(self): domain_name='default', id='1c36b64c840a42cd9e9b931a369337f0', domain_id=None, - name='admin'), - cloud='_test_cloud_'), + name='admin', + ), + cloud='_test_cloud_', + ), security_group_rules=[ - dict(id='123', direction='ingress', ethertype='IPv4', - port_range_min=80, port_range_max=81, protocol='tcp', - remote_ip_prefix='0.0.0.0/0', security_group_id='xyz123', - project_id='', tenant_id='', properties={}, - remote_group_id=None, - location=dict( - region_name='RegionOne', - zone=None, - project=dict( - domain_name='default', - id='1c36b64c840a42cd9e9b931a369337f0', - domain_id=None, - name='admin'), - cloud='_test_cloud_')) - ] + dict( + id='123', + direction='ingress', + ethertype='IPv4', + port_range_min=80, + port_range_max=81, + protocol='tcp', + remote_ip_prefix='0.0.0.0/0', + security_group_id='xyz123', + project_id='', + tenant_id='', + properties={}, + remote_group_id=None, + location=dict( + region_name='RegionOne', + zone=None, + project=dict( + domain_name='default', + id='1c36b64c840a42cd9e9b931a369337f0', + domain_id=None, + name='admin', + ), + cloud='_test_cloud_', + ), + ) + ], ) # Set secgroup source to nova for this test as stateful parameter # is only valid for neutron security groups. @@ -910,9 +1204,15 @@ def test_normalize_secgroups_negone_port(self): name='nova_secgroup', description='A Nova security group with -1 ports', rules=[ - dict(id='123', from_port=-1, to_port=-1, ip_protocol='icmp', - ip_range={'cidr': '0.0.0.0/0'}, parent_group_id='xyz123') - ] + dict( + id='123', + from_port=-1, + to_port=-1, + ip_protocol='icmp', + ip_range={'cidr': '0.0.0.0/0'}, + parent_group_id='xyz123', + ) + ], ) retval = self.cloud._normalize_secgroup(nova_secgroup) self.assertIsNone(retval['security_group_rules'][0]['port_range_min']) @@ -920,24 +1220,41 @@ def test_normalize_secgroups_negone_port(self): def test_normalize_secgroup_rules(self): nova_rules = [ - dict(id='123', from_port=80, to_port=81, ip_protocol='tcp', - ip_range={'cidr': '0.0.0.0/0'}, parent_group_id='xyz123') + dict( + id='123', + from_port=80, + to_port=81, + ip_protocol='tcp', + ip_range={'cidr': '0.0.0.0/0'}, + parent_group_id='xyz123', + ) ] expected = [ - dict(id='123', direction='ingress', ethertype='IPv4', - port_range_min=80, port_range_max=81, protocol='tcp', - remote_ip_prefix='0.0.0.0/0', security_group_id='xyz123', - tenant_id='', project_id='', remote_group_id=None, - properties={}, - location=dict( - region_name='RegionOne', - zone=None, - project=dict( - domain_name='default', - id='1c36b64c840a42cd9e9b931a369337f0', - domain_id=None, - name='admin'), - cloud='_test_cloud_')) + dict( + id='123', + direction='ingress', + ethertype='IPv4', + port_range_min=80, + port_range_max=81, + protocol='tcp', + remote_ip_prefix='0.0.0.0/0', + security_group_id='xyz123', + tenant_id='', + project_id='', + remote_group_id=None, + properties={}, + location=dict( + region_name='RegionOne', + zone=None, + project=dict( + domain_name='default', + id='1c36b64c840a42cd9e9b931a369337f0', + domain_id=None, + name='admin', + ), + cloud='_test_cloud_', + ), + ) ] retval = self.cloud._normalize_secgroup_rules(nova_rules) self.assertEqual(expected, retval) diff --git a/openstack/tests/unit/cloud/test_server_console.py b/openstack/tests/unit/cloud/test_server_console.py index 9b885a654..1364422ba 100644 --- a/openstack/tests/unit/cloud/test_server_console.py +++ b/openstack/tests/unit/cloud/test_server_console.py @@ -17,66 +17,80 @@ class TestServerConsole(base.TestCase): - def setUp(self): super(TestServerConsole, self).setUp() self.server_id = str(uuid.uuid4()) self.server_name = self.getUniqueString('name') self.server = fakes.make_fake_server( - server_id=self.server_id, name=self.server_name) + server_id=self.server_id, name=self.server_name + ) self.output = self.getUniqueString('output') def test_get_server_console_dict(self): - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri='{endpoint}/servers/{id}/action'.format( - endpoint=fakes.COMPUTE_ENDPOINT, - id=self.server_id), - json={"output": self.output}, - validate=dict( - json={'os-getConsoleOutput': {'length': 5}})) - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri='{endpoint}/servers/{id}/action'.format( + endpoint=fakes.COMPUTE_ENDPOINT, id=self.server_id + ), + json={"output": self.output}, + validate=dict(json={'os-getConsoleOutput': {'length': 5}}), + ), + ] + ) self.assertEqual( - self.output, self.cloud.get_server_console(self.server, 5)) + self.output, self.cloud.get_server_console(self.server, 5) + ) self.assert_calls() def test_get_server_console_name_or_id(self): - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri='{endpoint}/servers/detail'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json={"servers": [self.server]}), - dict(method='POST', - uri='{endpoint}/servers/{id}/action'.format( - endpoint=fakes.COMPUTE_ENDPOINT, - id=self.server_id), - json={"output": self.output}, - validate=dict( - json={'os-getConsoleOutput': {}})) - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri='{endpoint}/servers/detail'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json={"servers": [self.server]}, + ), + dict( + method='POST', + uri='{endpoint}/servers/{id}/action'.format( + endpoint=fakes.COMPUTE_ENDPOINT, id=self.server_id + ), + json={"output": self.output}, + validate=dict(json={'os-getConsoleOutput': {}}), + ), + ] + ) self.assertEqual( - self.output, self.cloud.get_server_console(self.server['id'])) + self.output, self.cloud.get_server_console(self.server['id']) + ) self.assert_calls() def test_get_server_console_no_console(self): - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri='{endpoint}/servers/{id}/action'.format( - endpoint=fakes.COMPUTE_ENDPOINT, - id=self.server_id), - status_code=400, - validate=dict( - json={'os-getConsoleOutput': {}})) - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri='{endpoint}/servers/{id}/action'.format( + endpoint=fakes.COMPUTE_ENDPOINT, id=self.server_id + ), + status_code=400, + validate=dict(json={'os-getConsoleOutput': {}}), + ), + ] + ) self.assertEqual('', self.cloud.get_server_console(self.server)) diff --git a/openstack/tests/unit/cloud/test_server_delete_metadata.py b/openstack/tests/unit/cloud/test_server_delete_metadata.py index ab0b94997..0183ae182 100644 --- a/openstack/tests/unit/cloud/test_server_delete_metadata.py +++ b/openstack/tests/unit/cloud/test_server_delete_metadata.py @@ -25,52 +25,81 @@ class TestServerDeleteMetadata(base.TestCase): - def setUp(self): super(TestServerDeleteMetadata, self).setUp() self.server_id = str(uuid.uuid4()) self.server_name = self.getUniqueString('name') self.fake_server = fakes.make_fake_server( - self.server_id, self.server_name) + self.server_id, self.server_name + ) def test_server_delete_metadata_with_exception(self): """ Test that a missing metadata throws an exception. """ - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': [self.fake_server]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', self.fake_server['id'], - 'metadata', 'key']), - status_code=404), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + json={'servers': [self.fake_server]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'compute', + 'public', + append=[ + 'servers', + self.fake_server['id'], + 'metadata', + 'key', + ], + ), + status_code=404, + ), + ] + ) self.assertRaises( - OpenStackCloudURINotFound, self.cloud.delete_server_metadata, - self.server_name, ['key']) + OpenStackCloudURINotFound, + self.cloud.delete_server_metadata, + self.server_name, + ['key'], + ) self.assert_calls() def test_server_delete_metadata(self): - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': [self.fake_server]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', self.fake_server['id'], - 'metadata', 'key']), - status_code=200), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + json={'servers': [self.fake_server]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'compute', + 'public', + append=[ + 'servers', + self.fake_server['id'], + 'metadata', + 'key', + ], + ), + status_code=200, + ), + ] + ) self.cloud.delete_server_metadata(self.server_id, ['key']) diff --git a/openstack/tests/unit/cloud/test_server_group.py b/openstack/tests/unit/cloud/test_server_group.py index ef29523ce..5e6d987c3 100644 --- a/openstack/tests/unit/cloud/test_server_group.py +++ b/openstack/tests/unit/cloud/test_server_group.py @@ -18,48 +18,66 @@ class TestServerGroup(base.TestCase): - def setUp(self): super(TestServerGroup, self).setUp() self.group_id = uuid.uuid4().hex self.group_name = self.getUniqueString('server-group') self.policies = ['affinity'] self.fake_group = fakes.make_fake_server_group( - self.group_id, self.group_name, self.policies) + self.group_id, self.group_name, self.policies + ) def test_create_server_group(self): - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', append=['os-server-groups']), - json={'server_group': self.fake_group}, - validate=dict( - json={'server_group': { - 'name': self.group_name, - 'policies': self.policies, - }})), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', 'public', append=['os-server-groups'] + ), + json={'server_group': self.fake_group}, + validate=dict( + json={ + 'server_group': { + 'name': self.group_name, + 'policies': self.policies, + } + } + ), + ), + ] + ) - self.cloud.create_server_group(name=self.group_name, - policies=self.policies) + self.cloud.create_server_group( + name=self.group_name, policies=self.policies + ) self.assert_calls() def test_delete_server_group(self): - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['os-server-groups']), - json={'server_groups': [self.fake_group]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'compute', 'public', - append=['os-server-groups', self.group_id]), - json={'server_groups': [self.fake_group]}), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['os-server-groups'] + ), + json={'server_groups': [self.fake_group]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'compute', + 'public', + append=['os-server-groups', self.group_id], + ), + json={'server_groups': [self.fake_group]}, + ), + ] + ) self.assertTrue(self.cloud.delete_server_group(self.group_name)) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_server_set_metadata.py b/openstack/tests/unit/cloud/test_server_set_metadata.py index 72af0efcd..75bc0facd 100644 --- a/openstack/tests/unit/cloud/test_server_set_metadata.py +++ b/openstack/tests/unit/cloud/test_server_set_metadata.py @@ -25,53 +25,73 @@ class TestServerSetMetadata(base.TestCase): - def setUp(self): super(TestServerSetMetadata, self).setUp() self.server_id = str(uuid.uuid4()) self.server_name = self.getUniqueString('name') self.fake_server = fakes.make_fake_server( - self.server_id, self.server_name) + self.server_id, self.server_name + ) def test_server_set_metadata_with_exception(self): - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': [self.fake_server]}), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', self.fake_server['id'], - 'metadata']), - validate=dict(json={'metadata': {'meta': 'data'}}), - json={}, - status_code=400), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + json={'servers': [self.fake_server]}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', self.fake_server['id'], 'metadata'], + ), + validate=dict(json={'metadata': {'meta': 'data'}}), + json={}, + status_code=400, + ), + ] + ) self.assertRaises( - OpenStackCloudBadRequest, self.cloud.set_server_metadata, - self.server_name, {'meta': 'data'}) + OpenStackCloudBadRequest, + self.cloud.set_server_metadata, + self.server_name, + {'meta': 'data'}, + ) self.assert_calls() def test_server_set_metadata(self): metadata = {'meta': 'data'} - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': [self.fake_server]}), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', self.fake_server['id'], 'metadata']), - validate=dict(json={'metadata': metadata}), - status_code=200, - json={'metadata': metadata}), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + json={'servers': [self.fake_server]}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', self.fake_server['id'], 'metadata'], + ), + validate=dict(json={'metadata': metadata}), + status_code=200, + json={'metadata': metadata}, + ), + ] + ) self.cloud.set_server_metadata(self.server_id, metadata) diff --git a/openstack/tests/unit/cloud/test_services.py b/openstack/tests/unit/cloud/test_services.py index fb4f5a387..c89695e3a 100644 --- a/openstack/tests/unit/cloud/test_services.py +++ b/openstack/tests/unit/cloud/test_services.py @@ -26,43 +26,59 @@ class CloudServices(base.TestCase): - def setUp(self, cloud_config_fixture='clouds.yaml'): super(CloudServices, self).setUp(cloud_config_fixture) - def get_mock_url(self, service_type='identity', interface='public', - resource='services', append=None, base_url_append='v3'): + def get_mock_url( + self, + service_type='identity', + interface='public', + resource='services', + append=None, + base_url_append='v3', + ): return super(CloudServices, self).get_mock_url( - service_type, interface, resource, append, base_url_append) + service_type, interface, resource, append, base_url_append + ) def test_create_service_v3(self): - service_data = self._get_service_data(name='a service', type='network', - description='A test service') - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url(), - status_code=200, - json=service_data.json_response_v3, - validate=dict(json={'service': service_data.json_request})) - ]) + service_data = self._get_service_data( + name='a service', type='network', description='A test service' + ) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url(), + status_code=200, + json=service_data.json_response_v3, + validate=dict(json={'service': service_data.json_request}), + ) + ] + ) service = self.cloud.create_service( name=service_data.service_name, service_type=service_data.service_type, - description=service_data.description) - self.assertThat(service.name, - matchers.Equals(service_data.service_name)) + description=service_data.description, + ) + self.assertThat( + service.name, matchers.Equals(service_data.service_name) + ) self.assertThat(service.id, matchers.Equals(service_data.service_id)) - self.assertThat(service.description, - matchers.Equals(service_data.description)) - self.assertThat(service.type, - matchers.Equals(service_data.service_type)) + self.assertThat( + service.description, matchers.Equals(service_data.description) + ) + self.assertThat( + service.type, matchers.Equals(service_data.service_type) + ) self.assert_calls() def test_update_service_v3(self): - service_data = self._get_service_data(name='a service', type='network', - description='A test service') + service_data = self._get_service_data( + name='a service', type='network', description='A test service' + ) request = service_data.json_request.copy() request['enabled'] = False resp = service_data.json_response_v3.copy() @@ -70,81 +86,114 @@ def test_update_service_v3(self): request.pop('description') request.pop('name') request.pop('type') - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(), - status_code=200, - json={'services': [resp['service']]}), - dict(method='PATCH', - uri=self.get_mock_url(append=[service_data.service_id]), - status_code=200, - json=resp, - validate=dict(json={'service': request})) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(), + status_code=200, + json={'services': [resp['service']]}, + ), + dict( + method='PATCH', + uri=self.get_mock_url(append=[service_data.service_id]), + status_code=200, + json=resp, + validate=dict(json={'service': request}), + ), + ] + ) service = self.cloud.update_service( - service_data.service_id, enabled=False) - self.assertThat(service.name, - matchers.Equals(service_data.service_name)) + service_data.service_id, enabled=False + ) + self.assertThat( + service.name, matchers.Equals(service_data.service_name) + ) self.assertThat(service.id, matchers.Equals(service_data.service_id)) - self.assertThat(service.description, - matchers.Equals(service_data.description)) - self.assertThat(service.type, - matchers.Equals(service_data.service_type)) + self.assertThat( + service.description, matchers.Equals(service_data.description) + ) + self.assertThat( + service.type, matchers.Equals(service_data.service_type) + ) self.assert_calls() def test_list_services(self): service_data = self._get_service_data() - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(), - status_code=200, - json={'services': [service_data.json_response_v3['service']]}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(), + status_code=200, + json={ + 'services': [service_data.json_response_v3['service']] + }, + ) + ] + ) services = self.cloud.list_services() self.assertThat(len(services), matchers.Equals(1)) - self.assertThat(services[0].id, - matchers.Equals(service_data.service_id)) - self.assertThat(services[0].name, - matchers.Equals(service_data.service_name)) - self.assertThat(services[0].type, - matchers.Equals(service_data.service_type)) + self.assertThat( + services[0].id, matchers.Equals(service_data.service_id) + ) + self.assertThat( + services[0].name, matchers.Equals(service_data.service_name) + ) + self.assertThat( + services[0].type, matchers.Equals(service_data.service_type) + ) self.assert_calls() def test_get_service(self): service_data = self._get_service_data() service2_data = self._get_service_data() - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(), - status_code=200, - json={'services': [ - service_data.json_response_v3['service'], - service2_data.json_response_v3['service']]}), - dict(method='GET', - uri=self.get_mock_url(), - status_code=200, - json={'services': [ - service_data.json_response_v3['service'], - service2_data.json_response_v3['service']]}), - dict(method='GET', - uri=self.get_mock_url(), - status_code=200, - json={'services': [ - service_data.json_response_v3['service'], - service2_data.json_response_v3['service']]}), - dict(method='GET', - uri=self.get_mock_url(), - status_code=400), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(), + status_code=200, + json={ + 'services': [ + service_data.json_response_v3['service'], + service2_data.json_response_v3['service'], + ] + }, + ), + dict( + method='GET', + uri=self.get_mock_url(), + status_code=200, + json={ + 'services': [ + service_data.json_response_v3['service'], + service2_data.json_response_v3['service'], + ] + }, + ), + dict( + method='GET', + uri=self.get_mock_url(), + status_code=200, + json={ + 'services': [ + service_data.json_response_v3['service'], + service2_data.json_response_v3['service'], + ] + }, + ), + dict(method='GET', uri=self.get_mock_url(), status_code=400), + ] + ) # Search by id service = self.cloud.get_service(name_or_id=service_data.service_id) self.assertThat(service.id, matchers.Equals(service_data.service_id)) # Search by name - service = self.cloud.get_service( - name_or_id=service_data.service_name) + service = self.cloud.get_service(name_or_id=service_data.service_name) # test we are getting exactly 1 element self.assertThat(service.id, matchers.Equals(service_data.service_id)) @@ -154,55 +203,85 @@ def test_get_service(self): # Multiple matches # test we are getting an Exception - self.assertRaises(OpenStackCloudException, self.cloud.get_service, - name_or_id=None, filters={'type': 'type2'}) + self.assertRaises( + OpenStackCloudException, + self.cloud.get_service, + name_or_id=None, + filters={'type': 'type2'}, + ) self.assert_calls() def test_search_services(self): service_data = self._get_service_data() service2_data = self._get_service_data(type=service_data.service_type) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(), - status_code=200, - json={'services': [ - service_data.json_response_v3['service'], - service2_data.json_response_v3['service']]}), - dict(method='GET', - uri=self.get_mock_url(), - status_code=200, - json={'services': [ - service_data.json_response_v3['service'], - service2_data.json_response_v3['service']]}), - dict(method='GET', - uri=self.get_mock_url(), - status_code=200, - json={'services': [ - service_data.json_response_v3['service'], - service2_data.json_response_v3['service']]}), - dict(method='GET', - uri=self.get_mock_url(), - status_code=200, - json={'services': [ - service_data.json_response_v3['service'], - service2_data.json_response_v3['service']]}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(), + status_code=200, + json={ + 'services': [ + service_data.json_response_v3['service'], + service2_data.json_response_v3['service'], + ] + }, + ), + dict( + method='GET', + uri=self.get_mock_url(), + status_code=200, + json={ + 'services': [ + service_data.json_response_v3['service'], + service2_data.json_response_v3['service'], + ] + }, + ), + dict( + method='GET', + uri=self.get_mock_url(), + status_code=200, + json={ + 'services': [ + service_data.json_response_v3['service'], + service2_data.json_response_v3['service'], + ] + }, + ), + dict( + method='GET', + uri=self.get_mock_url(), + status_code=200, + json={ + 'services': [ + service_data.json_response_v3['service'], + service2_data.json_response_v3['service'], + ] + }, + ), + ] + ) # Search by id services = self.cloud.search_services( - name_or_id=service_data.service_id) + name_or_id=service_data.service_id + ) # test we are getting exactly 1 element self.assertThat(len(services), matchers.Equals(1)) - self.assertThat(services[0].id, - matchers.Equals(service_data.service_id)) + self.assertThat( + services[0].id, matchers.Equals(service_data.service_id) + ) # Search by name services = self.cloud.search_services( - name_or_id=service_data.service_name) + name_or_id=service_data.service_name + ) # test we are getting exactly 1 element self.assertThat(len(services), matchers.Equals(1)) - self.assertThat(services[0].name, - matchers.Equals(service_data.service_name)) + self.assertThat( + services[0].name, matchers.Equals(service_data.service_name) + ) # Not found services = self.cloud.search_services(name_or_id='!INVALID!') @@ -210,35 +289,50 @@ def test_search_services(self): # Multiple matches services = self.cloud.search_services( - filters={'type': service_data.service_type}) + filters={'type': service_data.service_type} + ) # test we are getting exactly 2 elements self.assertThat(len(services), matchers.Equals(2)) - self.assertThat(services[0].id, - matchers.Equals(service_data.service_id)) - self.assertThat(services[1].id, - matchers.Equals(service2_data.service_id)) + self.assertThat( + services[0].id, matchers.Equals(service_data.service_id) + ) + self.assertThat( + services[1].id, matchers.Equals(service2_data.service_id) + ) self.assert_calls() def test_delete_service(self): service_data = self._get_service_data() - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url(), - status_code=200, - json={'services': [ - service_data.json_response_v3['service']]}), - dict(method='DELETE', - uri=self.get_mock_url(append=[service_data.service_id]), - status_code=204), - dict(method='GET', - uri=self.get_mock_url(), - status_code=200, - json={'services': [ - service_data.json_response_v3['service']]}), - dict(method='DELETE', - uri=self.get_mock_url(append=[service_data.service_id]), - status_code=204) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(), + status_code=200, + json={ + 'services': [service_data.json_response_v3['service']] + }, + ), + dict( + method='DELETE', + uri=self.get_mock_url(append=[service_data.service_id]), + status_code=204, + ), + dict( + method='GET', + uri=self.get_mock_url(), + status_code=200, + json={ + 'services': [service_data.json_response_v3['service']] + }, + ), + dict( + method='DELETE', + uri=self.get_mock_url(append=[service_data.service_id]), + status_code=204, + ), + ] + ) # Delete by name self.cloud.delete_service(name_or_id=service_data.service_name) diff --git a/openstack/tests/unit/cloud/test_shade.py b/openstack/tests/unit/cloud/test_shade.py index 91a8377ff..9bc80f332 100644 --- a/openstack/tests/unit/cloud/test_shade.py +++ b/openstack/tests/unit/cloud/test_shade.py @@ -34,7 +34,6 @@ class TestShade(base.TestCase): - def setUp(self): # This set of tests are not testing neutron, they're testing # rebuilding servers, but we do several network calls in service @@ -60,31 +59,35 @@ def test_endpoint_for(self): self.cloud.config.config['dns_endpoint_override'] = dns_override self.assertEqual( 'https://compute.example.com/v2.1/', - self.cloud.endpoint_for('compute')) + self.cloud.endpoint_for('compute'), + ) self.assertEqual( 'https://internal.compute.example.com/v2.1/', - self.cloud.endpoint_for('compute', interface='internal')) + self.cloud.endpoint_for('compute', interface='internal'), + ) self.assertIsNone( - self.cloud.endpoint_for('compute', region_name='unknown-region')) - self.assertEqual( - dns_override, - self.cloud.endpoint_for('dns')) + self.cloud.endpoint_for('compute', region_name='unknown-region') + ) + self.assertEqual(dns_override, self.cloud.endpoint_for('dns')) def test_connect_as(self): # Do initial auth/catalog steps # This should authenticate a second time, but should not # need a second identity discovery project_name = 'test_project' - self.register_uris([ - self.get_keystone_v3_token(project_name=project_name), - self.get_nova_discovery_mock_dict(), - dict( - method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': []}, - ), - ]) + self.register_uris( + [ + self.get_keystone_v3_token(project_name=project_name), + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + json={'servers': []}, + ), + ] + ) c2 = self.cloud.connect_as(project_name=project_name) self.assertEqual(c2.list_servers(), []) @@ -95,16 +98,19 @@ def test_connect_as_context(self): # This should authenticate a second time, but should not # need a second identity discovery project_name = 'test_project' - self.register_uris([ - self.get_keystone_v3_token(project_name=project_name), - self.get_nova_discovery_mock_dict(), - dict( - method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': []}, - ), - ]) + self.register_uris( + [ + self.get_keystone_v3_token(project_name=project_name), + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + json={'servers': []}, + ), + ] + ) with self.cloud.connect_as(project_name=project_name) as c2: self.assertEqual(c2.list_servers(), []) @@ -126,17 +132,21 @@ def test_get_image_not_found(self, mock_search): def test_global_request_id(self): request_id = uuid.uuid4().hex - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict( - method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': []}, - validate=dict( - headers={'X-Openstack-Request-Id': request_id}), - ), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + json={'servers': []}, + validate=dict( + headers={'X-Openstack-Request-Id': request_id} + ), + ), + ] + ) cloud2 = self.cloud.global_request(request_id) self.assertEqual([], cloud2.list_servers()) @@ -145,17 +155,21 @@ def test_global_request_id(self): def test_global_request_id_context(self): request_id = uuid.uuid4().hex - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict( - method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': []}, - validate=dict( - headers={'X-Openstack-Request-Id': request_id}), - ), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + json={'servers': []}, + validate=dict( + headers={'X-Openstack-Request-Id': request_id} + ), + ), + ] + ) with self.cloud.global_request(request_id) as c2: self.assertEqual([], c2.list_servers()) @@ -166,13 +180,18 @@ def test_get_server(self): server1 = fakes.make_fake_server('123', 'mickey') server2 = fakes.make_fake_server('345', 'mouse') - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': [server1, server2]}), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + json={'servers': [server1, server2]}, + ), + ] + ) r = self.cloud.get_server('mickey') self.assertIsNotNone(r) @@ -181,13 +200,18 @@ def test_get_server(self): self.assert_calls() def test_get_server_not_found(self): - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': []}), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + json={'servers': []}, + ), + ] + ) r = self.cloud.get_server('doesNotExist') self.assertIsNone(r) @@ -195,16 +219,20 @@ def test_get_server_not_found(self): self.assert_calls() def test_list_servers_exception(self): - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - status_code=400) - ]) - - self.assertRaises(exc.OpenStackCloudException, - self.cloud.list_servers) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + status_code=400, + ), + ] + ) + + self.assertRaises(exc.OpenStackCloudException, self.cloud.list_servers) self.assert_calls() @@ -218,13 +246,18 @@ def test_list_servers(self): server_id = str(uuid.uuid4()) server_name = self.getUniqueString('name') fake_server = fakes.make_fake_server(server_id, server_name) - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': [fake_server]}), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + json={'servers': [fake_server]}, + ), + ] + ) r = self.cloud.list_servers() @@ -243,30 +276,31 @@ def test_list_server_private_ip(self): "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:b4:a3:07", "version": 4, "addr": "10.4.0.13", - "OS-EXT-IPS:type": "fixed" - }, { + "OS-EXT-IPS:type": "fixed", + }, + { "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:b4:a3:07", "version": 4, "addr": "89.40.216.229", - "OS-EXT-IPS:type": "floating" - }]}, + "OS-EXT-IPS:type": "floating", + }, + ] + }, "links": [ + {"href": "http://example.com/images/95e4c4", "rel": "self"}, { "href": "http://example.com/images/95e4c4", - "rel": "self" - }, { - "href": "http://example.com/images/95e4c4", - "rel": "bookmark" - } + "rel": "bookmark", + }, ], "image": { "id": "95e4c449-8abf-486e-97d9-dc3f82417d2d", "links": [ { "href": "http://example.com/images/95e4c4", - "rel": "bookmark" + "rel": "bookmark", } - ] + ], }, "OS-EXT-STS:vm_state": "active", "OS-SRV-USG:launched_at": "2018-03-01T02:44:50.000000", @@ -275,9 +309,9 @@ def test_list_server_private_ip(self): "links": [ { "href": "http://example.com/flavors/95e4c4", - "rel": "bookmark" + "rel": "bookmark", } - ] + ], }, "id": "97fe35e9-756a-41a2-960a-1d057d2c9ee4", "security_groups": [{"name": "default"}], @@ -298,55 +332,59 @@ def test_list_server_private_ip(self): "created": "2018-03-01T02:44:46Z", "tenant_id": "65222a4d09ea4c68934fa1028c77f394", "os-extended-volumes:volumes_attached": [], - "config_drive": "" + "config_drive": "", + } + fake_networks = { + "networks": [ + { + "status": "ACTIVE", + "router:external": True, + "availability_zone_hints": [], + "availability_zones": ["nova"], + "description": None, + "subnets": [ + "df3e17fa-a4b2-47ae-9015-bc93eb076ba2", + "6b0c3dc9-b0b8-4d87-976a-7f2ebf13e7ec", + "fc541f48-fc7f-48c0-a063-18de6ee7bdd7", + ], + "shared": False, + "tenant_id": "a564613210ee43708b8a7fc6274ebd63", + "tags": [], + "ipv6_address_scope": "9f03124f-89af-483a-b6fd-10f08079db4d", # noqa: E501 + "mtu": 1550, + "is_default": False, + "admin_state_up": True, + "revision_number": 0, + "ipv4_address_scope": None, + "port_security_enabled": True, + "project_id": "a564613210ee43708b8a7fc6274ebd63", + "id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", + "name": "ext-net", + }, + { + "status": "ACTIVE", + "router:external": False, + "availability_zone_hints": [], + "availability_zones": ["nova"], + "description": "", + "subnets": ["f0ad1df5-53ee-473f-b86b-3604ea5591e9"], + "shared": False, + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", + "created_at": "2016-10-22T13:46:26Z", + "tags": [], + "ipv6_address_scope": None, + "updated_at": "2016-10-22T13:46:26Z", + "admin_state_up": True, + "mtu": 1500, + "revision_number": 0, + "ipv4_address_scope": None, + "port_security_enabled": True, + "project_id": "65222a4d09ea4c68934fa1028c77f394", + "id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", + "name": "private", + }, + ] } - fake_networks = {"networks": [ - { - "status": "ACTIVE", - "router:external": True, - "availability_zone_hints": [], - "availability_zones": ["nova"], - "description": None, - "subnets": [ - "df3e17fa-a4b2-47ae-9015-bc93eb076ba2", - "6b0c3dc9-b0b8-4d87-976a-7f2ebf13e7ec", - "fc541f48-fc7f-48c0-a063-18de6ee7bdd7" - ], - "shared": False, - "tenant_id": "a564613210ee43708b8a7fc6274ebd63", - "tags": [], - "ipv6_address_scope": "9f03124f-89af-483a-b6fd-10f08079db4d", - "mtu": 1550, - "is_default": False, - "admin_state_up": True, - "revision_number": 0, - "ipv4_address_scope": None, - "port_security_enabled": True, - "project_id": "a564613210ee43708b8a7fc6274ebd63", - "id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", - "name": "ext-net" - }, { - "status": "ACTIVE", - "router:external": False, - "availability_zone_hints": [], - "availability_zones": ["nova"], - "description": "", - "subnets": ["f0ad1df5-53ee-473f-b86b-3604ea5591e9"], - "shared": False, - "tenant_id": "65222a4d09ea4c68934fa1028c77f394", - "created_at": "2016-10-22T13:46:26Z", - "tags": [], - "ipv6_address_scope": None, - "updated_at": "2016-10-22T13:46:26Z", - "admin_state_up": True, - "mtu": 1500, - "revision_number": 0, - "ipv4_address_scope": None, - "port_security_enabled": True, - "project_id": "65222a4d09ea4c68934fa1028c77f394", - "id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", - "name": "private" - }]} fake_subnets = { "subnets": [ { @@ -362,10 +400,8 @@ def test_list_server_private_ip(self): "gateway_ip": "10.24.4.1", "ipv6_ra_mode": None, "allocation_pools": [ - { - "start": "10.24.4.2", - "end": "10.24.4.254" - }], + {"start": "10.24.4.2", "end": "10.24.4.254"} + ], "host_routes": [], "revision_number": 0, "ip_version": 4, @@ -374,8 +410,9 @@ def test_list_server_private_ip(self): "project_id": "65222a4d09ea4c68934fa1028c77f394", "id": "3f0642d9-4644-4dff-af25-bcf64f739698", "subnetpool_id": None, - "name": "foo_subnet" - }, { + "name": "foo_subnet", + }, + { "service_types": [], "description": "", "enable_dhcp": True, @@ -388,10 +425,8 @@ def test_list_server_private_ip(self): "gateway_ip": "10.4.0.1", "ipv6_ra_mode": None, "allocation_pools": [ - { - "start": "10.4.0.2", - "end": "10.4.0.200" - }], + {"start": "10.4.0.2", "end": "10.4.0.200"} + ], "host_routes": [], "revision_number": 0, "ip_version": 4, @@ -400,23 +435,36 @@ def test_list_server_private_ip(self): "project_id": "65222a4d09ea4c68934fa1028c77f394", "id": "f0ad1df5-53ee-473f-b86b-3604ea5591e9", "subnetpool_id": None, - "name": "private-subnet-ipv4" - }]} - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': [fake_server]}), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json=fake_networks), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets']), - json=fake_subnets) - ]) + "name": "private-subnet-ipv4", + }, + ] + } + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + json={'servers': [fake_server]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json=fake_networks, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets'] + ), + json=fake_subnets, + ), + ] + ) r = self.cloud.get_server('97fe35e9-756a-41a2-960a-1d057d2c9ee4') @@ -427,15 +475,22 @@ def test_list_server_private_ip(self): def test_list_servers_all_projects(self): '''This test verifies that when list_servers is called with `all_projects=True` that it passes `all_tenants=True` to nova.''' - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'], - qs_elements=['all_tenants=True']), - complete_qs=True, - json={'servers': []}), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', 'detail'], + qs_elements=['all_tenants=True'], + ), + complete_qs=True, + json={'servers': []}, + ), + ] + ) self.cloud.list_servers(all_projects=True) @@ -444,38 +499,47 @@ def test_list_servers_all_projects(self): def test_list_servers_filters(self): '''This test verifies that when list_servers is called with `filters` dict that it passes it to nova.''' - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'], - qs_elements=[ - 'deleted=True', - 'changes-since=2014-12-03T00:00:00Z' - ]), - complete_qs=True, - json={'servers': []}), - ]) - - self.cloud.list_servers(filters={ - 'deleted': True, - 'changes-since': '2014-12-03T00:00:00Z' - }) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', 'detail'], + qs_elements=[ + 'deleted=True', + 'changes-since=2014-12-03T00:00:00Z', + ], + ), + complete_qs=True, + json={'servers': []}, + ), + ] + ) + + self.cloud.list_servers( + filters={'deleted': True, 'changes-since': '2014-12-03T00:00:00Z'} + ) self.assert_calls() def test_iterate_timeout_bad_wait(self): with testtools.ExpectedException( - exc.OpenStackCloudException, - "Wait value must be an int or float value."): + exc.OpenStackCloudException, + "Wait value must be an int or float value.", + ): for count in utils.iterate_timeout( - 1, "test_iterate_timeout_bad_wait", wait="timeishard"): + 1, "test_iterate_timeout_bad_wait", wait="timeishard" + ): pass @mock.patch('time.sleep') def test_iterate_timeout_str_wait(self, mock_sleep): iter = utils.iterate_timeout( - 10, "test_iterate_timeout_str_wait", wait="1.6") + 10, "test_iterate_timeout_str_wait", wait="1.6" + ) next(iter) next(iter) mock_sleep.assert_called_with(1.6) @@ -483,7 +547,8 @@ def test_iterate_timeout_str_wait(self, mock_sleep): @mock.patch('time.sleep') def test_iterate_timeout_int_wait(self, mock_sleep): iter = utils.iterate_timeout( - 10, "test_iterate_timeout_int_wait", wait=1) + 10, "test_iterate_timeout_int_wait", wait=1 + ) next(iter) next(iter) mock_sleep.assert_called_with(1.0) @@ -491,9 +556,7 @@ def test_iterate_timeout_int_wait(self, mock_sleep): @mock.patch('time.sleep') def test_iterate_timeout_timeout(self, mock_sleep): message = "timeout test" - with testtools.ExpectedException( - exc.OpenStackCloudTimeout, - message): + with testtools.ExpectedException(exc.OpenStackCloudTimeout, message): for count in utils.iterate_timeout(0.1, message, wait=1): pass mock_sleep.assert_called_with(1.0) @@ -506,7 +569,7 @@ def test__nova_extensions(self): "links": [], "namespace": "http://openstack.org/compute/ext/fake_xml", "alias": "NMN", - "description": "Multiple network support." + "description": "Multiple network support.", }, { "updated": "2014-12-03T00:00:00Z", @@ -514,30 +577,40 @@ def test__nova_extensions(self): "links": [], "namespace": "http://openstack.org/compute/ext/fake_xml", "alias": "OS-DCF", - "description": "Disk Management Extension." + "description": "Disk Management Extension.", }, ] - self.register_uris([ - dict(method='GET', - uri='{endpoint}/extensions'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json=dict(extensions=body)) - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/extensions'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json=dict(extensions=body), + ) + ] + ) extensions = self.cloud._nova_extensions() self.assertEqual(set(['NMN', 'OS-DCF']), extensions) self.assert_calls() def test__nova_extensions_fails(self): - self.register_uris([ - dict(method='GET', - uri='{endpoint}/extensions'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - status_code=404), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/extensions'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + status_code=404, + ), + ] + ) self.assertRaises( - exceptions.ResourceNotFound, - self.cloud._nova_extensions) + exceptions.ResourceNotFound, self.cloud._nova_extensions + ) self.assert_calls() @@ -549,7 +622,7 @@ def test__has_nova_extension(self): "links": [], "namespace": "http://openstack.org/compute/ext/fake_xml", "alias": "NMN", - "description": "Multiple network support." + "description": "Multiple network support.", }, { "updated": "2014-12-03T00:00:00Z", @@ -557,15 +630,20 @@ def test__has_nova_extension(self): "links": [], "namespace": "http://openstack.org/compute/ext/fake_xml", "alias": "OS-DCF", - "description": "Disk Management Extension." + "description": "Disk Management Extension.", }, ] - self.register_uris([ - dict(method='GET', - uri='{endpoint}/extensions'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json=dict(extensions=body)) - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/extensions'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json=dict(extensions=body), + ) + ] + ) self.assertTrue(self.cloud._has_nova_extension('NMN')) self.assert_calls() @@ -578,7 +656,7 @@ def test__has_nova_extension_missing(self): "links": [], "namespace": "http://openstack.org/compute/ext/fake_xml", "alias": "NMN", - "description": "Multiple network support." + "description": "Multiple network support.", }, { "updated": "2014-12-03T00:00:00Z", @@ -586,15 +664,20 @@ def test__has_nova_extension_missing(self): "links": [], "namespace": "http://openstack.org/compute/ext/fake_xml", "alias": "OS-DCF", - "description": "Disk Management Extension." + "description": "Disk Management Extension.", }, ] - self.register_uris([ - dict(method='GET', - uri='{endpoint}/extensions'.format( - endpoint=fakes.COMPUTE_ENDPOINT), - json=dict(extensions=body)) - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/extensions'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json=dict(extensions=body), + ) + ] + ) self.assertFalse(self.cloud._has_nova_extension('invalid')) self.assert_calls() @@ -606,38 +689,45 @@ def test__neutron_extensions(self): "name": "Distributed Virtual Router", "links": [], "alias": "dvr", - "description": - "Enables configuration of Distributed Virtual Routers." + "description": "Enables configuration of Distributed Virtual Routers.", # noqa: E501 }, { "updated": "2013-07-23T10:00:00-00:00", "name": "Allowed Address Pairs", "links": [], "alias": "allowed-address-pairs", - "description": "Provides allowed address pairs" + "description": "Provides allowed address pairs", }, ] - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json=dict(extensions=body)) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json=dict(extensions=body), + ) + ] + ) extensions = self.cloud._neutron_extensions() self.assertEqual(set(['dvr', 'allowed-address-pairs']), extensions) self.assert_calls() def test__neutron_extensions_fails(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - status_code=404) - ]) - with testtools.ExpectedException( - exceptions.ResourceNotFound - ): + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + status_code=404, + ) + ] + ) + with testtools.ExpectedException(exceptions.ResourceNotFound): self.cloud._neutron_extensions() self.assert_calls() @@ -649,23 +739,27 @@ def test__has_neutron_extension(self): "name": "Distributed Virtual Router", "links": [], "alias": "dvr", - "description": - "Enables configuration of Distributed Virtual Routers." + "description": "Enables configuration of Distributed Virtual Routers.", # noqa: E501 }, { "updated": "2013-07-23T10:00:00-00:00", "name": "Allowed Address Pairs", "links": [], "alias": "allowed-address-pairs", - "description": "Provides allowed address pairs" + "description": "Provides allowed address pairs", }, ] - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json=dict(extensions=body)) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json=dict(extensions=body), + ) + ] + ) self.assertTrue(self.cloud._has_neutron_extension('dvr')) self.assert_calls() @@ -676,23 +770,27 @@ def test__has_neutron_extension_missing(self): "name": "Distributed Virtual Router", "links": [], "alias": "dvr", - "description": - "Enables configuration of Distributed Virtual Routers." + "description": "Enables configuration of Distributed Virtual Routers.", # noqa: E501 }, { "updated": "2013-07-23T10:00:00-00:00", "name": "Allowed Address Pairs", "links": [], "alias": "allowed-address-pairs", - "description": "Provides allowed address pairs" + "description": "Provides allowed address pairs", }, ] - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions']), - json=dict(extensions=body)) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json=dict(extensions=body), + ) + ] + ) self.assertFalse(self.cloud._has_neutron_extension('invalid')) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_shared_file_system.py b/openstack/tests/unit/cloud/test_shared_file_system.py index 51f3a25df..2463b5fe8 100644 --- a/openstack/tests/unit/cloud/test_shared_file_system.py +++ b/openstack/tests/unit/cloud/test_shared_file_system.py @@ -25,20 +25,24 @@ class TestSharedFileSystem(base.TestCase): - def setUp(self): super(TestSharedFileSystem, self).setUp() self.use_manila() def test_list_availability_zones(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'shared-file-system', - 'public', - append=['v2', 'availability-zones']), - json={'availability_zones': [MANILA_AZ_DICT]}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'shared-file-system', + 'public', + append=['v2', 'availability-zones'], + ), + json={'availability_zones': [MANILA_AZ_DICT]}, + ), + ] + ) az_list = self.cloud.list_share_availability_zones() self.assertEqual(len(az_list), 1) self.assertEqual(MANILA_AZ_DICT['id'], az_list[0].id) diff --git a/openstack/tests/unit/cloud/test_stack.py b/openstack/tests/unit/cloud/test_stack.py index 827a465ad..85297d0a7 100644 --- a/openstack/tests/unit/cloud/test_stack.py +++ b/openstack/tests/unit/cloud/test_stack.py @@ -21,7 +21,6 @@ class TestStack(base.TestCase): - def setUp(self): super(TestStack, self).setUp() self.stack_id = self.getUniqueString('id') @@ -32,21 +31,27 @@ def setUp(self): def _compare_stacks(self, exp, real): self.assertDictEqual( stack.Stack(**exp).to_dict(computed=False), - real.to_dict(computed=False)) + real.to_dict(computed=False), + ) def test_list_stacks(self): fake_stacks = [ self.stack, fakes.make_fake_stack( - self.getUniqueString('id'), - self.getUniqueString('name')) + self.getUniqueString('id'), self.getUniqueString('name') + ), ] - self.register_uris([ - dict(method='GET', - uri='{endpoint}/stacks'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT), - json={"stacks": fake_stacks}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/stacks'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT + ), + json={"stacks": fake_stacks}, + ), + ] + ) stacks = self.cloud.list_stacks() [self._compare_stacks(b, a) for a, b in zip(stacks, fake_stacks)] @@ -56,32 +61,43 @@ def test_list_stacks_filters(self): fake_stacks = [ self.stack, fakes.make_fake_stack( - self.getUniqueString('id'), - self.getUniqueString('name')) + self.getUniqueString('id'), self.getUniqueString('name') + ), ] - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'orchestration', 'public', - append=['stacks'], - qs_elements=['name=a', 'status=b'], - ), - json={"stacks": fake_stacks}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'orchestration', + 'public', + append=['stacks'], + qs_elements=['name=a', 'status=b'], + ), + json={"stacks": fake_stacks}, + ), + ] + ) stacks = self.cloud.list_stacks(name='a', status='b') [self._compare_stacks(b, a) for a, b in zip(stacks, fake_stacks)] self.assert_calls() def test_list_stacks_exception(self): - self.register_uris([ - dict(method='GET', - uri='{endpoint}/stacks'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT), - status_code=404) - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/stacks'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT + ), + status_code=404, + ) + ] + ) with testtools.ExpectedException( - openstack.cloud.OpenStackCloudURINotFound): + openstack.cloud.OpenStackCloudURINotFound + ): self.cloud.list_stacks() self.assert_calls() @@ -89,15 +105,20 @@ def test_search_stacks(self): fake_stacks = [ self.stack, fakes.make_fake_stack( - self.getUniqueString('id'), - self.getUniqueString('name')) + self.getUniqueString('id'), self.getUniqueString('name') + ), ] - self.register_uris([ - dict(method='GET', - uri='{endpoint}/stacks'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT), - json={"stacks": fake_stacks}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/stacks'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT + ), + json={"stacks": fake_stacks}, + ), + ] + ) stacks = self.cloud.search_stacks() [self._compare_stacks(b, a) for a, b in zip(stacks, fake_stacks)] self.assert_calls() @@ -108,205 +129,322 @@ def test_search_stacks_filters(self): fakes.make_fake_stack( self.getUniqueString('id'), self.getUniqueString('name'), - status='CREATE_FAILED') + status='CREATE_FAILED', + ), ] - self.register_uris([ - dict(method='GET', - uri='{endpoint}/stacks'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT), - json={"stacks": fake_stacks}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/stacks'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT + ), + json={"stacks": fake_stacks}, + ), + ] + ) filters = {'status': 'FAILED'} stacks = self.cloud.search_stacks(filters=filters) [self._compare_stacks(b, a) for a, b in zip(stacks, fake_stacks)] self.assert_calls() def test_search_stacks_exception(self): - self.register_uris([ - dict(method='GET', - uri='{endpoint}/stacks'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT), - status_code=404) - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/stacks'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT + ), + status_code=404, + ) + ] + ) with testtools.ExpectedException( - openstack.cloud.OpenStackCloudURINotFound): + openstack.cloud.OpenStackCloudURINotFound + ): self.cloud.search_stacks() def test_delete_stack(self): resolve = 'resolve_outputs=False' - self.register_uris([ - dict(method='GET', - uri='{endpoint}/stacks/{name}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name, resolve=resolve), - status_code=302, - headers=dict( - location='{endpoint}/stacks/{name}/{id}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name, - resolve=resolve))), - dict(method='GET', - uri='{endpoint}/stacks/{name}/{id}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name, resolve=resolve), - json={"stack": self.stack}), - dict(method='DELETE', - uri='{endpoint}/stacks/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id)), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/stacks/{name}?{resolve}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name, + resolve=resolve, + ), + status_code=302, + headers=dict( + location='{endpoint}/stacks/{name}/{id}?{resolve}'.format( # noqa: E501 + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + name=self.stack_name, + resolve=resolve, + ) + ), + ), + dict( + method='GET', + uri='{endpoint}/stacks/{name}/{id}?{resolve}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + name=self.stack_name, + resolve=resolve, + ), + json={"stack": self.stack}, + ), + dict( + method='DELETE', + uri='{endpoint}/stacks/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, id=self.stack_id + ), + ), + ] + ) self.assertTrue(self.cloud.delete_stack(self.stack_name)) self.assert_calls() def test_delete_stack_not_found(self): resolve = 'resolve_outputs=False' - self.register_uris([ - dict(method='GET', - uri='{endpoint}/stacks/stack_name?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, resolve=resolve), - status_code=404), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/stacks/stack_name?{resolve}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, resolve=resolve + ), + status_code=404, + ), + ] + ) self.assertFalse(self.cloud.delete_stack('stack_name')) self.assert_calls() def test_delete_stack_exception(self): resolve = 'resolve_outputs=False' - self.register_uris([ - dict(method='GET', - uri='{endpoint}/stacks/{id}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, resolve=resolve), - status_code=302, - headers=dict( - location='{endpoint}/stacks/{name}/{id}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name, - resolve=resolve))), - dict(method='GET', - uri='{endpoint}/stacks/{name}/{id}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name, resolve=resolve), - json={"stack": self.stack}), - dict(method='DELETE', - uri='{endpoint}/stacks/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id), - status_code=400, - reason="ouch"), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/stacks/{id}?{resolve}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + resolve=resolve, + ), + status_code=302, + headers=dict( + location='{endpoint}/stacks/{name}/{id}?{resolve}'.format( # noqa: E501 + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + name=self.stack_name, + resolve=resolve, + ) + ), + ), + dict( + method='GET', + uri='{endpoint}/stacks/{name}/{id}?{resolve}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + name=self.stack_name, + resolve=resolve, + ), + json={"stack": self.stack}, + ), + dict( + method='DELETE', + uri='{endpoint}/stacks/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, id=self.stack_id + ), + status_code=400, + reason="ouch", + ), + ] + ) with testtools.ExpectedException( - openstack.cloud.OpenStackCloudBadRequest): + openstack.cloud.OpenStackCloudBadRequest + ): self.cloud.delete_stack(self.stack_id) self.assert_calls() def test_delete_stack_by_name_wait(self): marker_event = fakes.make_fake_stack_event( - self.stack_id, self.stack_name, status='CREATE_COMPLETE', - resource_name='name') + self.stack_id, + self.stack_name, + status='CREATE_COMPLETE', + resource_name='name', + ) marker_qs = 'marker={e_id}&sort_dir=asc'.format( - e_id=marker_event['id']) + e_id=marker_event['id'] + ) resolve = 'resolve_outputs=False' - self.register_uris([ - dict(method='GET', - uri='{endpoint}/stacks/{name}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name, - resolve=resolve), - status_code=302, - headers=dict( - location='{endpoint}/stacks/{name}/{id}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name, - resolve=resolve))), - dict(method='GET', - uri='{endpoint}/stacks/{name}/{id}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name, resolve=resolve), - json={"stack": self.stack}), - dict(method='GET', - uri='{endpoint}/stacks/{name}/events?{qs}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name, - qs='limit=1&sort_dir=desc'), - complete_qs=True, - json={"events": [marker_event]}), - dict(method='DELETE', - uri='{endpoint}/stacks/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id)), - dict(method='GET', - uri='{endpoint}/stacks/{name}/events?{qs}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name, - qs=marker_qs), - complete_qs=True, - json={"events": [ - fakes.make_fake_stack_event( - self.stack_id, self.stack_name, - status='DELETE_COMPLETE', resource_name='name'), - ]}), - dict(method='GET', - uri='{endpoint}/stacks/{name}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name, resolve=resolve), - status_code=404), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/stacks/{name}?{resolve}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name, + resolve=resolve, + ), + status_code=302, + headers=dict( + location='{endpoint}/stacks/{name}/{id}?{resolve}'.format( # noqa: E501 + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + name=self.stack_name, + resolve=resolve, + ) + ), + ), + dict( + method='GET', + uri='{endpoint}/stacks/{name}/{id}?{resolve}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + name=self.stack_name, + resolve=resolve, + ), + json={"stack": self.stack}, + ), + dict( + method='GET', + uri='{endpoint}/stacks/{name}/events?{qs}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name, + qs='limit=1&sort_dir=desc', + ), + complete_qs=True, + json={"events": [marker_event]}, + ), + dict( + method='DELETE', + uri='{endpoint}/stacks/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, id=self.stack_id + ), + ), + dict( + method='GET', + uri='{endpoint}/stacks/{name}/events?{qs}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name, + qs=marker_qs, + ), + complete_qs=True, + json={ + "events": [ + fakes.make_fake_stack_event( + self.stack_id, + self.stack_name, + status='DELETE_COMPLETE', + resource_name='name', + ), + ] + }, + ), + dict( + method='GET', + uri='{endpoint}/stacks/{name}?{resolve}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name, + resolve=resolve, + ), + status_code=404, + ), + ] + ) self.assertTrue(self.cloud.delete_stack(self.stack_name, wait=True)) self.assert_calls() def test_delete_stack_by_id_wait(self): marker_event = fakes.make_fake_stack_event( - self.stack_id, self.stack_name, status='CREATE_COMPLETE', - resource_name='name') + self.stack_id, + self.stack_name, + status='CREATE_COMPLETE', + resource_name='name', + ) marker_qs = 'marker={e_id}&sort_dir=asc'.format( - e_id=marker_event['id']) + e_id=marker_event['id'] + ) resolve = 'resolve_outputs=False' - self.register_uris([ - dict(method='GET', - uri='{endpoint}/stacks/{id}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - resolve=resolve), - status_code=302, - headers=dict( - location='{endpoint}/stacks/{name}/{id}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name, - resolve=resolve))), - dict(method='GET', - uri='{endpoint}/stacks/{name}/{id}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name, resolve=resolve), - json={"stack": self.stack}), - dict(method='GET', - uri='{endpoint}/stacks/{id}/events?{qs}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - qs='limit=1&sort_dir=desc'), - complete_qs=True, - json={"events": [marker_event]}), - dict(method='DELETE', - uri='{endpoint}/stacks/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id)), - dict(method='GET', - uri='{endpoint}/stacks/{id}/events?{qs}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - qs=marker_qs), - complete_qs=True, - json={"events": [ - fakes.make_fake_stack_event( - self.stack_id, self.stack_name, - status='DELETE_COMPLETE'), - ]}), - dict(method='GET', - uri='{endpoint}/stacks/{id}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, resolve=resolve), - status_code=404), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/stacks/{id}?{resolve}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + resolve=resolve, + ), + status_code=302, + headers=dict( + location='{endpoint}/stacks/{name}/{id}?{resolve}'.format( # noqa: E501 + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + name=self.stack_name, + resolve=resolve, + ) + ), + ), + dict( + method='GET', + uri='{endpoint}/stacks/{name}/{id}?{resolve}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + name=self.stack_name, + resolve=resolve, + ), + json={"stack": self.stack}, + ), + dict( + method='GET', + uri='{endpoint}/stacks/{id}/events?{qs}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + qs='limit=1&sort_dir=desc', + ), + complete_qs=True, + json={"events": [marker_event]}, + ), + dict( + method='DELETE', + uri='{endpoint}/stacks/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, id=self.stack_id + ), + ), + dict( + method='GET', + uri='{endpoint}/stacks/{id}/events?{qs}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + qs=marker_qs, + ), + complete_qs=True, + json={ + "events": [ + fakes.make_fake_stack_event( + self.stack_id, + self.stack_name, + status='DELETE_COMPLETE', + ), + ] + }, + ), + dict( + method='GET', + uri='{endpoint}/stacks/{id}?{resolve}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + resolve=resolve, + ), + status_code=404, + ), + ] + ) self.assertTrue(self.cloud.delete_stack(self.stack_id, wait=True)) self.assert_calls() @@ -315,67 +453,106 @@ def test_delete_stack_wait_failed(self): failed_stack = self.stack.copy() failed_stack['stack_status'] = 'DELETE_FAILED' marker_event = fakes.make_fake_stack_event( - self.stack_id, self.stack_name, status='CREATE_COMPLETE') + self.stack_id, self.stack_name, status='CREATE_COMPLETE' + ) marker_qs = 'marker={e_id}&sort_dir=asc'.format( - e_id=marker_event['id']) + e_id=marker_event['id'] + ) resolve = 'resolve_outputs=False' - self.register_uris([ - dict(method='GET', - uri='{endpoint}/stacks/{id}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, resolve=resolve), - status_code=302, - headers=dict( - location='{endpoint}/stacks/{name}/{id}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name, - resolve=resolve))), - dict(method='GET', - uri='{endpoint}/stacks/{name}/{id}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name, resolve=resolve), - json={"stack": self.stack}), - dict(method='GET', - uri='{endpoint}/stacks/{id}/events?{qs}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - qs='limit=1&sort_dir=desc'), - complete_qs=True, - json={"events": [marker_event]}), - dict(method='DELETE', - uri='{endpoint}/stacks/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id)), - dict(method='GET', - uri='{endpoint}/stacks/{id}/events?{qs}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - qs=marker_qs), - complete_qs=True, - json={"events": [ - fakes.make_fake_stack_event( - self.stack_id, self.stack_name, - status='DELETE_COMPLETE'), - ]}), - dict(method='GET', - uri='{endpoint}/stacks/{id}?resolve_outputs=False'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id), - status_code=302, - headers=dict( - location='{endpoint}/stacks/{name}/{id}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name, - resolve=resolve))), - dict(method='GET', - uri='{endpoint}/stacks/{name}/{id}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name, resolve=resolve), - json={"stack": failed_stack}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/stacks/{id}?{resolve}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + resolve=resolve, + ), + status_code=302, + headers=dict( + location='{endpoint}/stacks/{name}/{id}?{resolve}'.format( # noqa: E501 + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + name=self.stack_name, + resolve=resolve, + ) + ), + ), + dict( + method='GET', + uri='{endpoint}/stacks/{name}/{id}?{resolve}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + name=self.stack_name, + resolve=resolve, + ), + json={"stack": self.stack}, + ), + dict( + method='GET', + uri='{endpoint}/stacks/{id}/events?{qs}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + qs='limit=1&sort_dir=desc', + ), + complete_qs=True, + json={"events": [marker_event]}, + ), + dict( + method='DELETE', + uri='{endpoint}/stacks/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, id=self.stack_id + ), + ), + dict( + method='GET', + uri='{endpoint}/stacks/{id}/events?{qs}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + qs=marker_qs, + ), + complete_qs=True, + json={ + "events": [ + fakes.make_fake_stack_event( + self.stack_id, + self.stack_name, + status='DELETE_COMPLETE', + ), + ] + }, + ), + dict( + method='GET', + uri='{endpoint}/stacks/{id}?resolve_outputs=False'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, id=self.stack_id + ), + status_code=302, + headers=dict( + location='{endpoint}/stacks/{name}/{id}?{resolve}'.format( # noqa: E501 + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + name=self.stack_name, + resolve=resolve, + ) + ), + ), + dict( + method='GET', + uri='{endpoint}/stacks/{name}/{id}?{resolve}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + name=self.stack_name, + resolve=resolve, + ), + json={"stack": failed_stack}, + ), + ] + ) with testtools.ExpectedException( - openstack.cloud.OpenStackCloudException): + openstack.cloud.OpenStackCloudException + ): self.cloud.delete_stack(self.stack_id, wait=True) self.assert_calls() @@ -384,42 +561,56 @@ def test_create_stack(self): test_template = tempfile.NamedTemporaryFile(delete=False) test_template.write(fakes.FAKE_TEMPLATE.encode('utf-8')) test_template.close() - self.register_uris([ - dict( - method='POST', uri='{endpoint}/stacks'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT), - json={"stack": self.stack}, - validate=dict( - json={ - 'disable_rollback': False, - 'parameters': {}, - 'stack_name': self.stack_name, - 'tags': self.stack_tag, - 'template': fakes.FAKE_TEMPLATE_CONTENT, - 'timeout_mins': 60} - )), - dict( - method='GET', - uri='{endpoint}/stacks/{name}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name), - status_code=302, - headers=dict( - location='{endpoint}/stacks/{name}/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name))), - dict( - method='GET', - uri='{endpoint}/stacks/{name}/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name), - json={"stack": self.stack}), - ]) + self.register_uris( + [ + dict( + method='POST', + uri='{endpoint}/stacks'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT + ), + json={"stack": self.stack}, + validate=dict( + json={ + 'disable_rollback': False, + 'parameters': {}, + 'stack_name': self.stack_name, + 'tags': self.stack_tag, + 'template': fakes.FAKE_TEMPLATE_CONTENT, + 'timeout_mins': 60, + } + ), + ), + dict( + method='GET', + uri='{endpoint}/stacks/{name}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name, + ), + status_code=302, + headers=dict( + location='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + name=self.stack_name, + ) + ), + ), + dict( + method='GET', + uri='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + name=self.stack_name, + ), + json={"stack": self.stack}, + ), + ] + ) self.cloud.create_stack( self.stack_name, tags=self.stack_tag, - template_file=test_template.name + template_file=test_template.name, ) self.assert_calls() @@ -430,53 +621,74 @@ def test_create_stack_wait(self): test_template.write(fakes.FAKE_TEMPLATE.encode('utf-8')) test_template.close() - self.register_uris([ - dict( - method='POST', uri='{endpoint}/stacks'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT), - json={"stack": self.stack}, - validate=dict( + self.register_uris( + [ + dict( + method='POST', + uri='{endpoint}/stacks'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT + ), + json={"stack": self.stack}, + validate=dict( + json={ + 'disable_rollback': False, + 'parameters': {}, + 'stack_name': self.stack_name, + 'tags': self.stack_tag, + 'template': fakes.FAKE_TEMPLATE_CONTENT, + 'timeout_mins': 60, + } + ), + ), + dict( + method='GET', + uri='{endpoint}/stacks/{name}/events?sort_dir=asc'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name, + ), json={ - 'disable_rollback': False, - 'parameters': {}, - 'stack_name': self.stack_name, - 'tags': self.stack_tag, - 'template': fakes.FAKE_TEMPLATE_CONTENT, - 'timeout_mins': 60} - )), - dict( - method='GET', - uri='{endpoint}/stacks/{name}/events?sort_dir=asc'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name), - json={"events": [ - fakes.make_fake_stack_event( - self.stack_id, self.stack_name, - status='CREATE_COMPLETE', - resource_name='name'), - ]}), - dict( - method='GET', - uri='{endpoint}/stacks/{name}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name), - status_code=302, - headers=dict( - location='{endpoint}/stacks/{name}/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name))), - dict( - method='GET', - uri='{endpoint}/stacks/{name}/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name), - json={"stack": self.stack}), - ]) + "events": [ + fakes.make_fake_stack_event( + self.stack_id, + self.stack_name, + status='CREATE_COMPLETE', + resource_name='name', + ), + ] + }, + ), + dict( + method='GET', + uri='{endpoint}/stacks/{name}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name, + ), + status_code=302, + headers=dict( + location='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + name=self.stack_name, + ) + ), + ), + dict( + method='GET', + uri='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + name=self.stack_name, + ), + json={"stack": self.stack}, + ), + ] + ) self.cloud.create_stack( self.stack_name, tags=self.stack_tag, template_file=test_template.name, - wait=True) + wait=True, + ) self.assert_calls() @@ -485,129 +697,183 @@ def test_update_stack(self): test_template.write(fakes.FAKE_TEMPLATE.encode('utf-8')) test_template.close() - self.register_uris([ - dict( - method='PUT', - uri='{endpoint}/stacks/{name}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name), - validate=dict( - json={ - 'disable_rollback': False, - 'parameters': {}, - 'tags': self.stack_tag, - 'template': fakes.FAKE_TEMPLATE_CONTENT, - 'timeout_mins': 60}), - json={}), - dict( - method='GET', - uri='{endpoint}/stacks/{name}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name), - status_code=302, - headers=dict( - location='{endpoint}/stacks/{name}/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name))), - dict( - method='GET', - uri='{endpoint}/stacks/{name}/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name), - json={"stack": self.stack}), - ]) + self.register_uris( + [ + dict( + method='PUT', + uri='{endpoint}/stacks/{name}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name, + ), + validate=dict( + json={ + 'disable_rollback': False, + 'parameters': {}, + 'tags': self.stack_tag, + 'template': fakes.FAKE_TEMPLATE_CONTENT, + 'timeout_mins': 60, + } + ), + json={}, + ), + dict( + method='GET', + uri='{endpoint}/stacks/{name}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name, + ), + status_code=302, + headers=dict( + location='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + name=self.stack_name, + ) + ), + ), + dict( + method='GET', + uri='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + name=self.stack_name, + ), + json={"stack": self.stack}, + ), + ] + ) self.cloud.update_stack( self.stack_name, tags=self.stack_tag, - template_file=test_template.name) + template_file=test_template.name, + ) self.assert_calls() def test_update_stack_wait(self): marker_event = fakes.make_fake_stack_event( - self.stack_id, self.stack_name, status='CREATE_COMPLETE', - resource_name='name') + self.stack_id, + self.stack_name, + status='CREATE_COMPLETE', + resource_name='name', + ) marker_qs = 'marker={e_id}&sort_dir=asc'.format( - e_id=marker_event['id']) + e_id=marker_event['id'] + ) test_template = tempfile.NamedTemporaryFile(delete=False) test_template.write(fakes.FAKE_TEMPLATE.encode('utf-8')) test_template.close() - self.register_uris([ - dict( - method='GET', - uri='{endpoint}/stacks/{name}/events?{qs}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name, - qs='limit=1&sort_dir=desc'), - json={"events": [marker_event]}), - dict( - method='PUT', - uri='{endpoint}/stacks/{name}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name), - validate=dict( + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/stacks/{name}/events?{qs}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name, + qs='limit=1&sort_dir=desc', + ), + json={"events": [marker_event]}, + ), + dict( + method='PUT', + uri='{endpoint}/stacks/{name}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name, + ), + validate=dict( + json={ + 'disable_rollback': False, + 'parameters': {}, + 'tags': self.stack_tag, + 'template': fakes.FAKE_TEMPLATE_CONTENT, + 'timeout_mins': 60, + } + ), + json={}, + ), + dict( + method='GET', + uri='{endpoint}/stacks/{name}/events?{qs}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name, + qs=marker_qs, + ), json={ - 'disable_rollback': False, - 'parameters': {}, - 'tags': self.stack_tag, - 'template': fakes.FAKE_TEMPLATE_CONTENT, - 'timeout_mins': 60}), - json={}), - dict( - method='GET', - uri='{endpoint}/stacks/{name}/events?{qs}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name, - qs=marker_qs), - json={"events": [ - fakes.make_fake_stack_event( - self.stack_id, self.stack_name, - status='UPDATE_COMPLETE', - resource_name='name'), - ]}), - dict( - method='GET', - uri='{endpoint}/stacks/{name}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name), - status_code=302, - headers=dict( - location='{endpoint}/stacks/{name}/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name))), - dict( - method='GET', - uri='{endpoint}/stacks/{name}/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name), - json={"stack": self.stack}), - ]) + "events": [ + fakes.make_fake_stack_event( + self.stack_id, + self.stack_name, + status='UPDATE_COMPLETE', + resource_name='name', + ), + ] + }, + ), + dict( + method='GET', + uri='{endpoint}/stacks/{name}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name, + ), + status_code=302, + headers=dict( + location='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + name=self.stack_name, + ) + ), + ), + dict( + method='GET', + uri='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + name=self.stack_name, + ), + json={"stack": self.stack}, + ), + ] + ) self.cloud.update_stack( self.stack_name, tags=self.stack_tag, template_file=test_template.name, - wait=True) + wait=True, + ) self.assert_calls() def test_get_stack(self): - self.register_uris([ - dict(method='GET', - uri='{endpoint}/stacks/{name}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name), - status_code=302, - headers=dict( - location='{endpoint}/stacks/{name}/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name))), - dict(method='GET', - uri='{endpoint}/stacks/{name}/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name), - json={"stack": self.stack}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/stacks/{name}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name, + ), + status_code=302, + headers=dict( + location='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + name=self.stack_name, + ) + ), + ), + dict( + method='GET', + uri='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + name=self.stack_name, + ), + json={"stack": self.stack}, + ), + ] + ) res = self.cloud.get_stack(self.stack_name) self.assertIsNotNone(res) @@ -620,22 +886,34 @@ def test_get_stack(self): def test_get_stack_in_progress(self): in_progress = self.stack.copy() in_progress['stack_status'] = 'CREATE_IN_PROGRESS' - self.register_uris([ - dict(method='GET', - uri='{endpoint}/stacks/{name}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name), - status_code=302, - headers=dict( - location='{endpoint}/stacks/{name}/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name))), - dict(method='GET', - uri='{endpoint}/stacks/{name}/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, name=self.stack_name), - json={"stack": in_progress}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/stacks/{name}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + name=self.stack_name, + ), + status_code=302, + headers=dict( + location='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + name=self.stack_name, + ) + ), + ), + dict( + method='GET', + uri='{endpoint}/stacks/{name}/{id}'.format( + endpoint=fakes.ORCHESTRATION_ENDPOINT, + id=self.stack_id, + name=self.stack_name, + ), + json={"stack": in_progress}, + ), + ] + ) res = self.cloud.get_stack(self.stack_name) self.assertIsNotNone(res) diff --git a/openstack/tests/unit/cloud/test_subnet.py b/openstack/tests/unit/cloud/test_subnet.py index d6db4939b..fe95f4912 100644 --- a/openstack/tests/unit/cloud/test_subnet.py +++ b/openstack/tests/unit/cloud/test_subnet.py @@ -37,10 +37,9 @@ class TestSubnet(base.TestCase): } mock_subnet_rep = { - 'allocation_pools': [{ - 'start': '192.168.199.2', - 'end': '192.168.199.254' - }], + 'allocation_pools': [ + {'start': '192.168.199.2', 'end': '192.168.199.254'} + ], 'cidr': subnet_cidr, 'created_at': '2017-04-24T20:22:23Z', 'description': '', @@ -58,48 +57,63 @@ class TestSubnet(base.TestCase): 'revision_number': 2, 'service_types': [], 'subnetpool_id': None, - 'tags': [] + 'tags': [], } mock_subnetpool_rep = { 'id': 'f49a1319-423a-4ee6-ba54-1d95a4f6cc68', - 'prefixes': [ - '172.16.0.0/16' - ] + 'prefixes': ['172.16.0.0/16'], } def _compare_subnets(self, exp, real): self.assertDictEqual( _subnet.Subnet(**exp).to_dict(computed=False), - real.to_dict(computed=False)) + real.to_dict(computed=False), + ) def test_get_subnet(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'subnets', self.subnet_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets'], - qs_elements=['name=%s' % self.subnet_name]), - json={'subnets': [self.mock_subnet_rep]}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'subnets', self.subnet_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'subnets'], + qs_elements=['name=%s' % self.subnet_name], + ), + json={'subnets': [self.mock_subnet_rep]}, + ), + ] + ) r = self.cloud.get_subnet(self.subnet_name) self.assertIsNotNone(r) self._compare_subnets(self.mock_subnet_rep, r) self.assert_calls() def test_get_subnet_by_id(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', - 'subnets', - self.subnet_id]), - json={'subnet': self.mock_subnet_rep}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'subnets', self.subnet_id], + ), + json={'subnet': self.mock_subnet_rep}, + ) + ] + ) r = self.cloud.get_subnet_by_id(self.subnet_id) self.assertIsNotNone(r) self._compare_subnets(self.mock_subnet_rep, r) @@ -113,90 +127,138 @@ def test_create_subnet(self): mock_subnet_rep['allocation_pools'] = pool mock_subnet_rep['dns_nameservers'] = dns mock_subnet_rep['host_routes'] = routes - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks', self.network_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks'], - qs_elements=['name=%s' % self.network_name]), - json={'networks': [self.mock_network_rep]}), - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets']), - json={'subnet': mock_subnet_rep}, - validate=dict( - json={'subnet': { - 'cidr': self.subnet_cidr, - 'enable_dhcp': False, - 'ip_version': 4, - 'network_id': self.mock_network_rep['id'], - 'allocation_pools': pool, - 'dns_nameservers': dns, - 'host_routes': routes}})) - ]) - subnet = self.cloud.create_subnet(self.network_name, self.subnet_cidr, - allocation_pools=pool, - dns_nameservers=dns, - host_routes=routes) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks', self.network_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks'], + qs_elements=['name=%s' % self.network_name], + ), + json={'networks': [self.mock_network_rep]}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets'] + ), + json={'subnet': mock_subnet_rep}, + validate=dict( + json={ + 'subnet': { + 'cidr': self.subnet_cidr, + 'enable_dhcp': False, + 'ip_version': 4, + 'network_id': self.mock_network_rep['id'], + 'allocation_pools': pool, + 'dns_nameservers': dns, + 'host_routes': routes, + } + } + ), + ), + ] + ) + subnet = self.cloud.create_subnet( + self.network_name, + self.subnet_cidr, + allocation_pools=pool, + dns_nameservers=dns, + host_routes=routes, + ) self._compare_subnets(mock_subnet_rep, subnet) self.assert_calls() def test_create_subnet_string_ip_version(self): '''Allow ip_version as a string''' - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks', self.network_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks'], - qs_elements=['name=%s' % self.network_name]), - json={'networks': [self.mock_network_rep]}), - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets']), - json={'subnet': self.mock_subnet_rep}, - validate=dict( - json={'subnet': { - 'cidr': self.subnet_cidr, - 'enable_dhcp': False, - 'ip_version': 4, - 'network_id': self.mock_network_rep['id']}})) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks', self.network_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks'], + qs_elements=['name=%s' % self.network_name], + ), + json={'networks': [self.mock_network_rep]}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets'] + ), + json={'subnet': self.mock_subnet_rep}, + validate=dict( + json={ + 'subnet': { + 'cidr': self.subnet_cidr, + 'enable_dhcp': False, + 'ip_version': 4, + 'network_id': self.mock_network_rep['id'], + } + } + ), + ), + ] + ) subnet = self.cloud.create_subnet( - self.network_name, self.subnet_cidr, ip_version='4') + self.network_name, self.subnet_cidr, ip_version='4' + ) self._compare_subnets(self.mock_subnet_rep, subnet) self.assert_calls() def test_create_subnet_bad_ip_version(self): '''String ip_versions must be convertable to int''' - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks', self.network_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks'], - qs_elements=['name=%s' % self.network_name]), - json={'networks': [self.mock_network_rep]}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks', self.network_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks'], + qs_elements=['name=%s' % self.network_name], + ), + json={'networks': [self.mock_network_rep]}, + ), + ] + ) with testtools.ExpectedException( - exc.OpenStackCloudException, - "ip_version must be an integer" + exc.OpenStackCloudException, "ip_version must be an integer" ): self.cloud.create_subnet( - self.network_name, self.subnet_cidr, ip_version='4x') + self.network_name, self.subnet_cidr, ip_version='4x' + ) self.assert_calls() def test_create_subnet_without_gateway_ip(self): @@ -206,36 +268,56 @@ def test_create_subnet_without_gateway_ip(self): mock_subnet_rep['allocation_pools'] = pool mock_subnet_rep['dns_nameservers'] = dns mock_subnet_rep['gateway_ip'] = None - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks', self.network_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks'], - qs_elements=['name=%s' % self.network_name]), - json={'networks': [self.mock_network_rep]}), - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets']), - json={'subnet': mock_subnet_rep}, - validate=dict( - json={'subnet': { - 'cidr': self.subnet_cidr, - 'enable_dhcp': False, - 'ip_version': 4, - 'network_id': self.mock_network_rep['id'], - 'allocation_pools': pool, - 'gateway_ip': None, - 'dns_nameservers': dns}})) - ]) - subnet = self.cloud.create_subnet(self.network_name, self.subnet_cidr, - allocation_pools=pool, - dns_nameservers=dns, - disable_gateway_ip=True) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks', self.network_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks'], + qs_elements=['name=%s' % self.network_name], + ), + json={'networks': [self.mock_network_rep]}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets'] + ), + json={'subnet': mock_subnet_rep}, + validate=dict( + json={ + 'subnet': { + 'cidr': self.subnet_cidr, + 'enable_dhcp': False, + 'ip_version': 4, + 'network_id': self.mock_network_rep['id'], + 'allocation_pools': pool, + 'gateway_ip': None, + 'dns_nameservers': dns, + } + } + ), + ), + ] + ) + subnet = self.cloud.create_subnet( + self.network_name, + self.subnet_cidr, + allocation_pools=pool, + dns_nameservers=dns, + disable_gateway_ip=True, + ) self._compare_subnets(mock_subnet_rep, subnet) self.assert_calls() @@ -247,98 +329,158 @@ def test_create_subnet_with_gateway_ip(self): mock_subnet_rep['allocation_pools'] = pool mock_subnet_rep['dns_nameservers'] = dns mock_subnet_rep['gateway_ip'] = gateway - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks', self.network_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks'], - qs_elements=['name=%s' % self.network_name]), - json={'networks': [self.mock_network_rep]}), - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets']), - json={'subnet': mock_subnet_rep}, - validate=dict( - json={'subnet': { - 'cidr': self.subnet_cidr, - 'enable_dhcp': False, - 'ip_version': 4, - 'network_id': self.mock_network_rep['id'], - 'allocation_pools': pool, - 'gateway_ip': gateway, - 'dns_nameservers': dns}})) - ]) - subnet = self.cloud.create_subnet(self.network_name, self.subnet_cidr, - allocation_pools=pool, - dns_nameservers=dns, - gateway_ip=gateway) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks', self.network_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks'], + qs_elements=['name=%s' % self.network_name], + ), + json={'networks': [self.mock_network_rep]}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets'] + ), + json={'subnet': mock_subnet_rep}, + validate=dict( + json={ + 'subnet': { + 'cidr': self.subnet_cidr, + 'enable_dhcp': False, + 'ip_version': 4, + 'network_id': self.mock_network_rep['id'], + 'allocation_pools': pool, + 'gateway_ip': gateway, + 'dns_nameservers': dns, + } + } + ), + ), + ] + ) + subnet = self.cloud.create_subnet( + self.network_name, + self.subnet_cidr, + allocation_pools=pool, + dns_nameservers=dns, + gateway_ip=gateway, + ) self._compare_subnets(mock_subnet_rep, subnet) self.assert_calls() def test_create_subnet_conflict_gw_ops(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks', 'kooky']), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks'], - qs_elements=['name=kooky']), - json={'networks': [self.mock_network_rep]}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks', 'kooky'], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks'], + qs_elements=['name=kooky'], + ), + json={'networks': [self.mock_network_rep]}, + ), + ] + ) gateway = '192.168.200.3' - self.assertRaises(exc.OpenStackCloudException, - self.cloud.create_subnet, 'kooky', - self.subnet_cidr, gateway_ip=gateway, - disable_gateway_ip=True) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.create_subnet, + 'kooky', + self.subnet_cidr, + gateway_ip=gateway, + disable_gateway_ip=True, + ) self.assert_calls() def test_create_subnet_bad_network(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks', 'duck']), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks'], - qs_elements=['name=duck']), - json={'networks': [self.mock_network_rep]}), - ]) - self.assertRaises(exc.OpenStackCloudException, - self.cloud.create_subnet, - 'duck', self.subnet_cidr) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks', 'duck'], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks'], + qs_elements=['name=duck'], + ), + json={'networks': [self.mock_network_rep]}, + ), + ] + ) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.create_subnet, + 'duck', + self.subnet_cidr, + ) self.assert_calls() def test_create_subnet_non_unique_network(self): net1 = dict(id='123', name=self.network_name) net2 = dict(id='456', name=self.network_name) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks', self.network_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks'], - qs_elements=['name=%s' % self.network_name]), - json={'networks': [net1, net2]}), - ]) - self.assertRaises(exc.OpenStackCloudException, - self.cloud.create_subnet, - self.network_name, self.subnet_cidr) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks', self.network_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks'], + qs_elements=['name=%s' % self.network_name], + ), + json={'networks': [net1, net2]}, + ), + ] + ) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.create_subnet, + self.network_name, + self.subnet_cidr, + ) self.assert_calls() def test_create_subnet_from_subnetpool_with_prefixlen(self): @@ -355,138 +497,213 @@ def test_create_subnet_from_subnetpool_with_prefixlen(self): mock_subnet_rep['subnetpool_id'] = self.mock_subnetpool_rep['id'] mock_subnet_rep['cidr'] = self.subnetpool_cidr mock_subnet_rep['id'] = id - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks', self.network_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'networks'], - qs_elements=['name=%s' % self.network_name]), - json={'networks': [self.mock_network_rep]}), - dict(method='POST', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets']), - json={'subnet': mock_subnet_rep}, - validate=dict( - json={'subnet': { - 'enable_dhcp': False, - 'ip_version': 4, - 'network_id': self.mock_network_rep['id'], - 'allocation_pools': pool, - 'dns_nameservers': dns, - 'use_default_subnetpool': True, - 'prefixlen': self.prefix_length, - 'host_routes': routes}})) - ]) - subnet = self.cloud.create_subnet(self.network_name, - allocation_pools=pool, - dns_nameservers=dns, - use_default_subnetpool=True, - prefixlen=self.prefix_length, - host_routes=routes) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks', self.network_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks'], + qs_elements=['name=%s' % self.network_name], + ), + json={'networks': [self.mock_network_rep]}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets'] + ), + json={'subnet': mock_subnet_rep}, + validate=dict( + json={ + 'subnet': { + 'enable_dhcp': False, + 'ip_version': 4, + 'network_id': self.mock_network_rep['id'], + 'allocation_pools': pool, + 'dns_nameservers': dns, + 'use_default_subnetpool': True, + 'prefixlen': self.prefix_length, + 'host_routes': routes, + } + } + ), + ), + ] + ) + subnet = self.cloud.create_subnet( + self.network_name, + allocation_pools=pool, + dns_nameservers=dns, + use_default_subnetpool=True, + prefixlen=self.prefix_length, + host_routes=routes, + ) mock_subnet_rep.update( - { - 'prefixlen': self.prefix_length, - 'use_default_subnetpool': True - }) + {'prefixlen': self.prefix_length, 'use_default_subnetpool': True} + ) self._compare_subnets(mock_subnet_rep, subnet) self.assert_calls() def test_delete_subnet(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'subnets', self.subnet_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets'], - qs_elements=['name=%s' % self.subnet_name]), - json={'subnets': [self.mock_subnet_rep]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'subnets', self.subnet_id]), - json={}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'subnets', self.subnet_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'subnets'], + qs_elements=['name=%s' % self.subnet_name], + ), + json={'subnets': [self.mock_subnet_rep]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'subnets', self.subnet_id], + ), + json={}, + ), + ] + ) self.assertTrue(self.cloud.delete_subnet(self.subnet_name)) self.assert_calls() def test_delete_subnet_not_found(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'subnets', 'goofy']), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets'], - qs_elements=['name=goofy']), - json={'subnets': []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'subnets', 'goofy'], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'subnets'], + qs_elements=['name=goofy'], + ), + json={'subnets': []}, + ), + ] + ) self.assertFalse(self.cloud.delete_subnet('goofy')) self.assert_calls() def test_delete_subnet_multiple_found(self): subnet1 = dict(id='123', name=self.subnet_name) subnet2 = dict(id='456', name=self.subnet_name) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'subnets', self.subnet_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets'], - qs_elements=['name=%s' % self.subnet_name]), - json={'subnets': [subnet1, subnet2]}) - ]) - self.assertRaises(exc.OpenStackCloudException, - self.cloud.delete_subnet, - self.subnet_name) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'subnets', self.subnet_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'subnets'], + qs_elements=['name=%s' % self.subnet_name], + ), + json={'subnets': [subnet1, subnet2]}, + ), + ] + ) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.delete_subnet, + self.subnet_name, + ) self.assert_calls() def test_delete_subnet_using_id(self): subnet1 = dict(id='123', name=self.subnet_name) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets', - subnet1['id']]), - json=subnet1), - dict(method='DELETE', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'subnets', subnet1['id']]), - json={}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'subnets', subnet1['id']], + ), + json=subnet1, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'subnets', subnet1['id']], + ), + json={}, + ), + ] + ) self.assertTrue(self.cloud.delete_subnet(subnet1['id'])) self.assert_calls() def test_update_subnet(self): expected_subnet = copy.copy(self.mock_subnet_rep) expected_subnet['name'] = 'goofy' - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'subnets', self.subnet_id]), - json=self.mock_subnet_rep), - dict(method='PUT', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'subnets', self.subnet_id]), - json={'subnet': expected_subnet}, - validate=dict( - json={'subnet': {'name': 'goofy'}})) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'subnets', self.subnet_id], + ), + json=self.mock_subnet_rep, + ), + dict( + method='PUT', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'subnets', self.subnet_id], + ), + json={'subnet': expected_subnet}, + validate=dict(json={'subnet': {'name': 'goofy'}}), + ), + ] + ) subnet = self.cloud.update_subnet(self.subnet_id, subnet_name='goofy') self._compare_subnets(expected_subnet, subnet) self.assert_calls() @@ -495,20 +712,29 @@ def test_update_subnet_gateway_ip(self): expected_subnet = copy.copy(self.mock_subnet_rep) gateway = '192.168.199.3' expected_subnet['gateway_ip'] = gateway - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'subnets', self.subnet_id]), - json=self.mock_subnet_rep), - dict(method='PUT', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'subnets', self.subnet_id]), - json={'subnet': expected_subnet}, - validate=dict( - json={'subnet': {'gateway_ip': gateway}})) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'subnets', self.subnet_id], + ), + json=self.mock_subnet_rep, + ), + dict( + method='PUT', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'subnets', self.subnet_id], + ), + json={'subnet': expected_subnet}, + validate=dict(json={'subnet': {'gateway_ip': gateway}}), + ), + ] + ) subnet = self.cloud.update_subnet(self.subnet_id, gateway_ip=gateway) self._compare_subnets(expected_subnet, subnet) self.assert_calls() @@ -516,27 +742,40 @@ def test_update_subnet_gateway_ip(self): def test_update_subnet_disable_gateway_ip(self): expected_subnet = copy.copy(self.mock_subnet_rep) expected_subnet['gateway_ip'] = None - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'subnets', self.subnet_id]), - json=self.mock_subnet_rep), - dict(method='PUT', - uri=self.get_mock_url( - 'network', 'public', - append=['v2.0', 'subnets', self.subnet_id]), - json={'subnet': expected_subnet}, - validate=dict( - json={'subnet': {'gateway_ip': None}})) - ]) - subnet = self.cloud.update_subnet(self.subnet_id, - disable_gateway_ip=True) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'subnets', self.subnet_id], + ), + json=self.mock_subnet_rep, + ), + dict( + method='PUT', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'subnets', self.subnet_id], + ), + json={'subnet': expected_subnet}, + validate=dict(json={'subnet': {'gateway_ip': None}}), + ), + ] + ) + subnet = self.cloud.update_subnet( + self.subnet_id, disable_gateway_ip=True + ) self._compare_subnets(expected_subnet, subnet) self.assert_calls() def test_update_subnet_conflict_gw_ops(self): - self.assertRaises(exc.OpenStackCloudException, - self.cloud.update_subnet, - self.subnet_id, gateway_ip="192.168.199.3", - disable_gateway_ip=True) + self.assertRaises( + exc.OpenStackCloudException, + self.cloud.update_subnet, + self.subnet_id, + gateway_ip="192.168.199.3", + disable_gateway_ip=True, + ) diff --git a/openstack/tests/unit/cloud/test_update_server.py b/openstack/tests/unit/cloud/test_update_server.py index 7af7db1d5..88c61b32b 100644 --- a/openstack/tests/unit/cloud/test_update_server.py +++ b/openstack/tests/unit/cloud/test_update_server.py @@ -25,42 +25,60 @@ class TestUpdateServer(base.TestCase): - def setUp(self): super(TestUpdateServer, self).setUp() self.server_id = str(uuid.uuid4()) self.server_name = self.getUniqueString('name') self.updated_server_name = self.getUniqueString('name2') self.fake_server = fakes.make_fake_server( - self.server_id, self.server_name) + self.server_id, self.server_name + ) def test_update_server_with_update_exception(self): """ Test that an exception in the update raises an exception in update_server. """ - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', self.server_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'], - qs_elements=['name=%s' % self.server_name]), - json={'servers': [self.fake_server]}), - dict(method='PUT', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', self.server_id]), - status_code=400, - validate=dict( - json={'server': {'name': self.updated_server_name}})), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', self.server_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', 'detail'], + qs_elements=['name=%s' % self.server_name], + ), + json={'servers': [self.fake_server]}, + ), + dict( + method='PUT', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', self.server_id] + ), + status_code=400, + validate=dict( + json={'server': {'name': self.updated_server_name}} + ), + ), + ] + ) self.assertRaises( - OpenStackCloudException, self.cloud.update_server, - self.server_name, name=self.updated_server_name) + OpenStackCloudException, + self.cloud.update_server, + self.server_name, + name=self.updated_server_name, + ) self.assert_calls() @@ -69,34 +87,55 @@ def test_update_server_name(self): Test that update_server updates the name without raising any exception """ fake_update_server = fakes.make_fake_server( - self.server_id, self.updated_server_name) + self.server_id, self.updated_server_name + ) - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', self.server_name]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'], - qs_elements=['name=%s' % self.server_name]), - json={'servers': [self.fake_server]}), - dict(method='PUT', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', self.server_id]), - json={'server': fake_update_server}, - validate=dict( - json={'server': {'name': self.updated_server_name}})), - dict(method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks']), - json={'networks': []}), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', self.server_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', 'detail'], + qs_elements=['name=%s' % self.server_name], + ), + json={'servers': [self.fake_server]}, + ), + dict( + method='PUT', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', self.server_id] + ), + json={'server': fake_update_server}, + validate=dict( + json={'server': {'name': self.updated_server_name}} + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={'networks': []}, + ), + ] + ) self.assertEqual( self.updated_server_name, self.cloud.update_server( - self.server_name, name=self.updated_server_name)['name']) + self.server_name, name=self.updated_server_name + )['name'], + ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_usage.py b/openstack/tests/unit/cloud/test_usage.py index 29cea236c..1c13f7ad8 100644 --- a/openstack/tests/unit/cloud/test_usage.py +++ b/openstack/tests/unit/cloud/test_usage.py @@ -18,48 +18,56 @@ class TestUsage(base.TestCase): - def test_get_usage(self): - project = self.mock_for_keystone_projects(project_count=1, - list_get=True)[0] + project = self.mock_for_keystone_projects( + project_count=1, list_get=True + )[0] start = end = datetime.datetime.now() - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', - append=['os-simple-tenant-usage', project.project_id], - qs_elements=[ - 'start={now}'.format(now=start.isoformat()), - 'end={now}'.format(now=end.isoformat()), - ]), - json={"tenant_usage": { - "server_usages": [ - { - "ended_at": None, - "flavor": "m1.tiny", - "hours": 1.0, - "instance_id": uuid.uuid4().hex, - "local_gb": 1, - "memory_mb": 512, - "name": "instance-2", - "started_at": "2012-10-08T20:10:44.541277", - "state": "active", - "tenant_id": "6f70656e737461636b20342065766572", - "uptime": 3600, - "vcpus": 1 - } - ], - "start": "2012-10-08T20:10:44.587336", - "stop": "2012-10-08T21:10:44.587336", - "tenant_id": "6f70656e737461636b20342065766572", - "total_hours": 1.0, - "total_local_gb_usage": 1.0, - "total_memory_mb_usage": 512.0, - "total_vcpus_usage": 1.0 - }}) - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['os-simple-tenant-usage', project.project_id], + qs_elements=[ + 'start={now}'.format(now=start.isoformat()), + 'end={now}'.format(now=end.isoformat()), + ], + ), + json={ + "tenant_usage": { + "server_usages": [ + { + "ended_at": None, + "flavor": "m1.tiny", + "hours": 1.0, + "instance_id": uuid.uuid4().hex, + "local_gb": 1, + "memory_mb": 512, + "name": "instance-2", + "started_at": "2012-10-08T20:10:44.541277", + "state": "active", + "tenant_id": "6f70656e737461636b20342065766572", # noqa: E501 + "uptime": 3600, + "vcpus": 1, + } + ], + "start": "2012-10-08T20:10:44.587336", + "stop": "2012-10-08T21:10:44.587336", + "tenant_id": "6f70656e737461636b20342065766572", + "total_hours": 1.0, + "total_local_gb_usage": 1.0, + "total_memory_mb_usage": 512.0, + "total_vcpus_usage": 1.0, + } + }, + ), + ] + ) self.cloud.get_compute_usage(project.project_id, start, end) diff --git a/openstack/tests/unit/cloud/test_users.py b/openstack/tests/unit/cloud/test_users.py index 955e93f29..29620591d 100644 --- a/openstack/tests/unit/cloud/test_users.py +++ b/openstack/tests/unit/cloud/test_users.py @@ -19,16 +19,19 @@ class TestUsers(base.TestCase): - - def _get_keystone_mock_url(self, resource, append=None, v3=True, - qs_elements=None): + def _get_keystone_mock_url( + self, resource, append=None, v3=True, qs_elements=None + ): base_url_append = None if v3: base_url_append = 'v3' return self.get_mock_url( - service_type='identity', resource=resource, - append=append, base_url_append=base_url_append, - qs_elements=qs_elements) + service_type='identity', + resource=resource, + append=append, + base_url_append=base_url_append, + qs_elements=qs_elements, + ) def _get_user_list(self, user_data): uri = self._get_keystone_mock_url(resource='users') @@ -40,26 +43,34 @@ def _get_user_list(self, user_data): 'self': uri, 'previous': None, 'next': None, - } + }, } def test_create_user_v3(self): user_data = self._get_user_data( domain_id=uuid.uuid4().hex, - description=self.getUniqueString('description')) - - self.register_uris([ - dict(method='POST', - uri=self._get_keystone_mock_url(resource='users'), - status_code=200, json=user_data.json_response, - validate=dict(json=user_data.json_request)), - ]) + description=self.getUniqueString('description'), + ) + + self.register_uris( + [ + dict( + method='POST', + uri=self._get_keystone_mock_url(resource='users'), + status_code=200, + json=user_data.json_response, + validate=dict(json=user_data.json_request), + ), + ] + ) user = self.cloud.create_user( - name=user_data.name, email=user_data.email, + name=user_data.name, + email=user_data.email, password=user_data.password, description=user_data.description, - domain_id=user_data.domain_id) + domain_id=user_data.domain_id, + ) self.assertEqual(user_data.name, user.name) self.assertEqual(user_data.email, user.email) @@ -68,59 +79,89 @@ def test_create_user_v3(self): self.assert_calls() def test_create_user_v3_no_domain(self): - user_data = self._get_user_data(domain_id=uuid.uuid4().hex, - email='test@example.com') + user_data = self._get_user_data( + domain_id=uuid.uuid4().hex, email='test@example.com' + ) with testtools.ExpectedException( - openstack.cloud.OpenStackCloudException, - "User or project creation requires an explicit" - " domain_id argument." + openstack.cloud.OpenStackCloudException, + "User or project creation requires an explicit" + " domain_id argument.", ): self.cloud.create_user( - name=user_data.name, email=user_data.email, - password=user_data.password) + name=user_data.name, + email=user_data.email, + password=user_data.password, + ) def test_delete_user(self): user_data = self._get_user_data(domain_id=uuid.uuid4().hex) user_resource_uri = self._get_keystone_mock_url( - resource='users', append=[user_data.user_id]) - - self.register_uris([ - dict(method='GET', - uri=self._get_keystone_mock_url( - resource='users', - qs_elements=['name=%s' % user_data.name]), - status_code=200, - json=self._get_user_list(user_data)), - dict(method='DELETE', uri=user_resource_uri, status_code=204)]) + resource='users', append=[user_data.user_id] + ) + + self.register_uris( + [ + dict( + method='GET', + uri=self._get_keystone_mock_url( + resource='users', + qs_elements=['name=%s' % user_data.name], + ), + status_code=200, + json=self._get_user_list(user_data), + ), + dict(method='DELETE', uri=user_resource_uri, status_code=204), + ] + ) self.cloud.delete_user(user_data.name) self.assert_calls() def test_delete_user_not_found(self): - self.register_uris([ - dict(method='GET', - uri=self._get_keystone_mock_url(resource='users'), - status_code=200, json={'users': []})]) + self.register_uris( + [ + dict( + method='GET', + uri=self._get_keystone_mock_url(resource='users'), + status_code=200, + json={'users': []}, + ) + ] + ) self.assertFalse(self.cloud.delete_user(self.getUniqueString())) def test_add_user_to_group(self): user_data = self._get_user_data() group_data = self._get_group_data() - self.register_uris([ - dict(method='GET', - uri=self._get_keystone_mock_url(resource='users'), - status_code=200, - json=self._get_user_list(user_data)), - dict(method='GET', - uri=self._get_keystone_mock_url(resource='groups'), - status_code=200, - json={'groups': [group_data.json_response['group']]}), - dict(method='PUT', - uri=self._get_keystone_mock_url( - resource='groups', - append=[group_data.group_id, 'users', user_data.user_id]), - status_code=200)]) + self.register_uris( + [ + dict( + method='GET', + uri=self._get_keystone_mock_url(resource='users'), + status_code=200, + json=self._get_user_list(user_data), + ), + dict( + method='GET', + uri=self._get_keystone_mock_url(resource='groups'), + status_code=200, + json={'groups': [group_data.json_response['group']]}, + ), + dict( + method='PUT', + uri=self._get_keystone_mock_url( + resource='groups', + append=[ + group_data.group_id, + 'users', + user_data.user_id, + ], + ), + status_code=200, + ), + ] + ) self.cloud.add_user_to_group(user_data.user_id, group_data.group_id) self.assert_calls() @@ -128,43 +169,73 @@ def test_is_user_in_group(self): user_data = self._get_user_data() group_data = self._get_group_data() - self.register_uris([ - dict(method='GET', - uri=self._get_keystone_mock_url(resource='users'), - status_code=200, - json=self._get_user_list(user_data)), - dict(method='GET', - uri=self._get_keystone_mock_url(resource='groups'), - status_code=200, - json={'groups': [group_data.json_response['group']]}), - dict(method='HEAD', - uri=self._get_keystone_mock_url( - resource='groups', - append=[group_data.group_id, 'users', user_data.user_id]), - status_code=204)]) - - self.assertTrue(self.cloud.is_user_in_group( - user_data.user_id, group_data.group_id)) + self.register_uris( + [ + dict( + method='GET', + uri=self._get_keystone_mock_url(resource='users'), + status_code=200, + json=self._get_user_list(user_data), + ), + dict( + method='GET', + uri=self._get_keystone_mock_url(resource='groups'), + status_code=200, + json={'groups': [group_data.json_response['group']]}, + ), + dict( + method='HEAD', + uri=self._get_keystone_mock_url( + resource='groups', + append=[ + group_data.group_id, + 'users', + user_data.user_id, + ], + ), + status_code=204, + ), + ] + ) + + self.assertTrue( + self.cloud.is_user_in_group(user_data.user_id, group_data.group_id) + ) self.assert_calls() def test_remove_user_from_group(self): user_data = self._get_user_data() group_data = self._get_group_data() - self.register_uris([ - dict(method='GET', - uri=self._get_keystone_mock_url(resource='users'), - json=self._get_user_list(user_data)), - dict(method='GET', - uri=self._get_keystone_mock_url(resource='groups'), - status_code=200, - json={'groups': [group_data.json_response['group']]}), - dict(method='DELETE', - uri=self._get_keystone_mock_url( - resource='groups', - append=[group_data.group_id, 'users', user_data.user_id]), - status_code=204)]) + self.register_uris( + [ + dict( + method='GET', + uri=self._get_keystone_mock_url(resource='users'), + json=self._get_user_list(user_data), + ), + dict( + method='GET', + uri=self._get_keystone_mock_url(resource='groups'), + status_code=200, + json={'groups': [group_data.json_response['group']]}, + ), + dict( + method='DELETE', + uri=self._get_keystone_mock_url( + resource='groups', + append=[ + group_data.group_id, + 'users', + user_data.user_id, + ], + ), + status_code=204, + ), + ] + ) self.cloud.remove_user_from_group( - user_data.user_id, group_data.group_id) + user_data.user_id, group_data.group_id + ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_volume.py b/openstack/tests/unit/cloud/test_volume.py index 1f6ff2f2e..2cd42dd2e 100644 --- a/openstack/tests/unit/cloud/test_volume.py +++ b/openstack/tests/unit/cloud/test_volume.py @@ -22,59 +22,88 @@ class TestVolume(base.TestCase): - def _compare_volumes(self, exp, real): self.assertDictEqual( volume.Volume(**exp).to_dict(computed=False), - real.to_dict(computed=False) + real.to_dict(computed=False), ) def _compare_volume_attachments(self, exp, real): self.assertDictEqual( volume_attachment.VolumeAttachment(**exp).to_dict(computed=False), - real.to_dict(computed=False) + real.to_dict(computed=False), ) def test_attach_volume(self): server = dict(id='server001') - vol = {'id': 'volume001', 'status': 'available', - 'name': '', 'attachments': []} + vol = { + 'id': 'volume001', + 'status': 'available', + 'name': '', + 'attachments': [], + } volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) - rattach = {'server_id': server['id'], 'device': 'device001', - 'volumeId': volume['id'], 'id': 'attachmentId'} - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', server['id'], - 'os-volume_attachments']), - json={'volumeAttachment': rattach}, - validate=dict(json={ - 'volumeAttachment': { - 'volumeId': vol['id']}}) - )]) + rattach = { + 'server_id': server['id'], + 'device': 'device001', + 'volumeId': volume['id'], + 'id': 'attachmentId', + } + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', + 'public', + append=[ + 'servers', + server['id'], + 'os-volume_attachments', + ], + ), + json={'volumeAttachment': rattach}, + validate=dict( + json={'volumeAttachment': {'volumeId': vol['id']}} + ), + ), + ] + ) ret = self.cloud.attach_volume(server, volume, wait=False) self._compare_volume_attachments(rattach, ret) self.assert_calls() def test_attach_volume_exception(self): server = dict(id='server001') - vol = {'id': 'volume001', 'status': 'available', - 'name': '', 'attachments': []} + vol = { + 'id': 'volume001', + 'status': 'available', + 'name': '', + 'attachments': [], + } volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', server['id'], - 'os-volume_attachments']), - status_code=404, - validate=dict(json={ - 'volumeAttachment': { - 'volumeId': vol['id']}}) - )]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', + 'public', + append=[ + 'servers', + server['id'], + 'os-volume_attachments', + ], + ), + status_code=404, + validate=dict( + json={'volumeAttachment': {'volumeId': vol['id']}} + ), + ), + ] + ) with testtools.ExpectedException( openstack.cloud.OpenStackCloudURINotFound ): @@ -83,36 +112,60 @@ def test_attach_volume_exception(self): def test_attach_volume_wait(self): server = dict(id='server001') - vol = {'id': 'volume001', 'status': 'available', - 'name': '', 'attachments': []} + vol = { + 'id': 'volume001', + 'status': 'available', + 'name': '', + 'attachments': [], + } volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) - vol['attachments'] = [{'server_id': server['id'], - 'device': 'device001'}] + vol['attachments'] = [ + {'server_id': server['id'], 'device': 'device001'} + ] vol['status'] = 'in-use' attached_volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) - rattach = {'server_id': server['id'], 'device': 'device001', - 'volumeId': volume['id'], 'id': 'attachmentId'} - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', server['id'], - 'os-volume_attachments']), - json={'volumeAttachment': rattach}, - validate=dict(json={ - 'volumeAttachment': { - 'volumeId': vol['id']}})), - self.get_cinder_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', vol['id']]), - json={'volume': volume}), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', vol['id']]), - json={'volume': attached_volume}) - ]) + rattach = { + 'server_id': server['id'], + 'device': 'device001', + 'volumeId': volume['id'], + 'id': 'attachmentId', + } + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', + 'public', + append=[ + 'servers', + server['id'], + 'os-volume_attachments', + ], + ), + json={'volumeAttachment': rattach}, + validate=dict( + json={'volumeAttachment': {'volumeId': vol['id']}} + ), + ), + self.get_cinder_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', vol['id']] + ), + json={'volume': volume}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', vol['id']] + ), + json={'volume': attached_volume}, + ), + ] + ) # defaults to wait=True ret = self.cloud.attach_volume(server, volume) self._compare_volume_attachments(rattach, ret) @@ -120,38 +173,59 @@ def test_attach_volume_wait(self): def test_attach_volume_wait_error(self): server = dict(id='server001') - vol = {'id': 'volume001', 'status': 'available', - 'name': '', 'attachments': []} + vol = { + 'id': 'volume001', + 'status': 'available', + 'name': '', + 'attachments': [], + } volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) vol['status'] = 'error' errored_volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) - rattach = {'server_id': server['id'], 'device': 'device001', - 'volumeId': volume['id'], 'id': 'attachmentId'} - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', server['id'], - 'os-volume_attachments']), - json={'volumeAttachment': rattach}, - validate=dict(json={ - 'volumeAttachment': { - 'volumeId': vol['id']}})), - self.get_cinder_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', volume['id']]), - json={'volume': errored_volume}), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', volume['id']]), - json={'volume': errored_volume}) - ]) + rattach = { + 'server_id': server['id'], + 'device': 'device001', + 'volumeId': volume['id'], + 'id': 'attachmentId', + } + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'compute', + 'public', + append=[ + 'servers', + server['id'], + 'os-volume_attachments', + ], + ), + json={'volumeAttachment': rattach}, + validate=dict( + json={'volumeAttachment': {'volumeId': vol['id']}} + ), + ), + self.get_cinder_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', volume['id']] + ), + json={'volume': errored_volume}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', volume['id']] + ), + json={'volume': errored_volume}, + ), + ] + ) - with testtools.ExpectedException( - openstack.exceptions.ResourceFailure - ): + with testtools.ExpectedException(openstack.exceptions.ResourceFailure): self.cloud.attach_volume(server, volume) self.assert_calls() @@ -161,8 +235,8 @@ def test_attach_volume_not_available(self): with testtools.ExpectedException( openstack.cloud.OpenStackCloudException, - "Volume %s is not available. Status is '%s'" % ( - volume['id'], volume['status']) + "Volume %s is not available. Status is '%s'" + % (volume['id'], volume['status']), ): self.cloud.attach_volume(server, volume) self.assertEqual(0, len(self.adapter.request_history)) @@ -170,59 +244,85 @@ def test_attach_volume_not_available(self): def test_attach_volume_already_attached(self): device_id = 'device001' server = dict(id='server001') - volume = dict(id='volume001', - attachments=[ - {'server_id': 'server001', 'device': device_id} - ]) + volume = dict( + id='volume001', + attachments=[{'server_id': 'server001', 'device': device_id}], + ) with testtools.ExpectedException( openstack.cloud.OpenStackCloudException, - "Volume %s already attached to server %s on device %s" % ( - volume['id'], server['id'], device_id) + "Volume %s already attached to server %s on device %s" + % (volume['id'], server['id'], device_id), ): self.cloud.attach_volume(server, volume) self.assertEqual(0, len(self.adapter.request_history)) def test_detach_volume(self): server = dict(id='server001') - volume = dict(id='volume001', - attachments=[ - {'server_id': 'server001', 'device': 'device001'} - ]) - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', server['id']]), - json={'server': server}), - dict(method='DELETE', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', server['id'], - 'os-volume_attachments', volume['id']]))]) + volume = dict( + id='volume001', + attachments=[{'server_id': 'server001', 'device': 'device001'}], + ) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', server['id']] + ), + json={'server': server}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'compute', + 'public', + append=[ + 'servers', + server['id'], + 'os-volume_attachments', + volume['id'], + ], + ), + ), + ] + ) self.cloud.detach_volume(server, volume, wait=False) self.assert_calls() def test_detach_volume_exception(self): server = dict(id='server001') - volume = dict(id='volume001', - attachments=[ - {'server_id': 'server001', 'device': 'device001'} - ]) - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', server['id']]), - json={'server': server}), - dict(method='DELETE', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', server['id'], - 'os-volume_attachments', volume['id']]), - status_code=404)]) + volume = dict( + id='volume001', + attachments=[{'server_id': 'server001', 'device': 'device001'}], + ) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', server['id']] + ), + json={'server': server}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'compute', + 'public', + append=[ + 'servers', + server['id'], + 'os-volume_attachments', + volume['id'], + ], + ), + status_code=404, + ), + ] + ) with testtools.ExpectedException( openstack.cloud.OpenStackCloudURINotFound ): @@ -232,225 +332,363 @@ def test_detach_volume_exception(self): def test_detach_volume_wait(self): server = dict(id='server001') attachments = [{'server_id': 'server001', 'device': 'device001'}] - vol = {'id': 'volume001', 'status': 'attached', 'name': '', - 'attachments': attachments} + vol = { + 'id': 'volume001', + 'status': 'attached', + 'name': '', + 'attachments': attachments, + } volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) vol['status'] = 'available' vol['attachments'] = [] avail_volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', server['id']]), - json={'server': server}), - dict(method='DELETE', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', server['id'], - 'os-volume_attachments', volume.id])), - self.get_cinder_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', 'detail']), - json={'volumes': [avail_volume]})]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', server['id']] + ), + json={'server': server}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'compute', + 'public', + append=[ + 'servers', + server['id'], + 'os-volume_attachments', + volume.id, + ], + ), + ), + self.get_cinder_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', 'detail'] + ), + json={'volumes': [avail_volume]}, + ), + ] + ) self.cloud.detach_volume(server, volume) self.assert_calls() def test_detach_volume_wait_error(self): server = dict(id='server001') attachments = [{'server_id': 'server001', 'device': 'device001'}] - vol = {'id': 'volume001', 'status': 'attached', 'name': '', - 'attachments': attachments} + vol = { + 'id': 'volume001', + 'status': 'attached', + 'name': '', + 'attachments': attachments, + } volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) vol['status'] = 'error' vol['attachments'] = [] errored_volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', server['id']]), - json={'server': server}), - dict(method='DELETE', - uri=self.get_mock_url( - 'compute', 'public', - append=['servers', server['id'], - 'os-volume_attachments', volume.id])), - self.get_cinder_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', 'detail']), - json={'volumes': [errored_volume]}), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['volumes', errored_volume['id']]), - json={'volume': errored_volume}) - ]) - with testtools.ExpectedException( - openstack.exceptions.ResourceFailure - ): + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', server['id']] + ), + json={'server': server}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'compute', + 'public', + append=[ + 'servers', + server['id'], + 'os-volume_attachments', + volume.id, + ], + ), + ), + self.get_cinder_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', 'detail'] + ), + json={'volumes': [errored_volume]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', + 'public', + append=['volumes', errored_volume['id']], + ), + json={'volume': errored_volume}, + ), + ] + ) + with testtools.ExpectedException(openstack.exceptions.ResourceFailure): self.cloud.detach_volume(server, volume) self.assert_calls() def test_delete_volume_deletes(self): - vol = {'id': 'volume001', 'status': 'attached', - 'name': '', 'attachments': []} + vol = { + 'id': 'volume001', + 'status': 'attached', + 'name': '', + 'attachments': [], + } volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) - self.register_uris([ - self.get_cinder_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', volume.id]), - json={'volumes': [volume]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', volume.id])), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', volume.id]), - status_code=404)]) + self.register_uris( + [ + self.get_cinder_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', volume.id] + ), + json={'volumes': [volume]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', volume.id] + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', volume.id] + ), + status_code=404, + ), + ] + ) self.assertTrue(self.cloud.delete_volume(volume['id'])) self.assert_calls() def test_delete_volume_gone_away(self): - vol = {'id': 'volume001', 'status': 'attached', - 'name': '', 'attachments': []} + vol = { + 'id': 'volume001', + 'status': 'attached', + 'name': '', + 'attachments': [], + } volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) - self.register_uris([ - self.get_cinder_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', volume.id]), - json=volume), - dict(method='DELETE', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', volume.id]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', volume.id]), - status_code=404), - ]) + self.register_uris( + [ + self.get_cinder_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', volume.id] + ), + json=volume, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', volume.id] + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', volume.id] + ), + status_code=404, + ), + ] + ) self.assertTrue(self.cloud.delete_volume(volume['id'])) self.assert_calls() def test_delete_volume_force(self): - vol = {'id': 'volume001', 'status': 'attached', - 'name': '', 'attachments': []} + vol = { + 'id': 'volume001', + 'status': 'attached', + 'name': '', + 'attachments': [], + } volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) - self.register_uris([ - self.get_cinder_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', volume['id']]), - json={'volumes': [volume]}), - dict(method='POST', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['volumes', volume.id, 'action']), - validate=dict( - json={'os-force_delete': {}})), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', volume['id']]), - status_code=404)]) + self.register_uris( + [ + self.get_cinder_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', volume['id']] + ), + json={'volumes': [volume]}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'volumev3', + 'public', + append=['volumes', volume.id, 'action'], + ), + validate=dict(json={'os-force_delete': {}}), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', volume['id']] + ), + status_code=404, + ), + ] + ) self.assertTrue(self.cloud.delete_volume(volume['id'], force=True)) self.assert_calls() def test_set_volume_bootable(self): - vol = {'id': 'volume001', 'status': 'attached', - 'name': '', 'attachments': []} + vol = { + 'id': 'volume001', + 'status': 'attached', + 'name': '', + 'attachments': [], + } volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) - self.register_uris([ - self.get_cinder_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', 'detail']), - json={'volumes': [volume]}), - dict(method='POST', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['volumes', volume.id, 'action']), - json={'os-set_bootable': {'bootable': True}}), - ]) + self.register_uris( + [ + self.get_cinder_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', 'detail'] + ), + json={'volumes': [volume]}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'volumev3', + 'public', + append=['volumes', volume.id, 'action'], + ), + json={'os-set_bootable': {'bootable': True}}, + ), + ] + ) self.cloud.set_volume_bootable(volume['id']) self.assert_calls() def test_set_volume_bootable_false(self): - vol = {'id': 'volume001', 'status': 'attached', - 'name': '', 'attachments': []} + vol = { + 'id': 'volume001', + 'status': 'attached', + 'name': '', + 'attachments': [], + } volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) - self.register_uris([ - self.get_cinder_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', 'detail']), - json={'volumes': [volume]}), - dict(method='POST', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['volumes', volume.id, 'action']), - json={'os-set_bootable': {'bootable': False}}), - ]) + self.register_uris( + [ + self.get_cinder_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', 'detail'] + ), + json={'volumes': [volume]}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'volumev3', + 'public', + append=['volumes', volume.id, 'action'], + ), + json={'os-set_bootable': {'bootable': False}}, + ), + ] + ) self.cloud.set_volume_bootable(volume['id']) self.assert_calls() def test_get_volume_by_id(self): vol1 = meta.obj_to_munch(fakes.FakeVolume('01', 'available', 'vol1')) - self.register_uris([ - self.get_cinder_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['volumes', '01']), - json={'volume': vol1} - ) - ]) + self.register_uris( + [ + self.get_cinder_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', '01'] + ), + json={'volume': vol1}, + ), + ] + ) self._compare_volumes(vol1, self.cloud.get_volume_by_id('01')) self.assert_calls() def test_create_volume(self): vol1 = meta.obj_to_munch(fakes.FakeVolume('01', 'available', 'vol1')) - self.register_uris([ - self.get_cinder_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes']), - json={'volume': vol1}, - validate=dict(json={ - 'volume': { - 'size': 50, - 'name': 'vol1', - }})), - ]) + self.register_uris( + [ + self.get_cinder_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes'] + ), + json={'volume': vol1}, + validate=dict( + json={ + 'volume': { + 'size': 50, + 'name': 'vol1', + } + } + ), + ), + ] + ) self.cloud.create_volume(50, name='vol1') self.assert_calls() def test_create_bootable_volume(self): vol1 = meta.obj_to_munch(fakes.FakeVolume('01', 'available', 'vol1')) - self.register_uris([ - self.get_cinder_discovery_mock_dict(), - dict(method='POST', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes']), - json={'volume': vol1}, - validate=dict(json={ - 'volume': { - 'size': 50, - 'name': 'vol1', - }})), - dict(method='POST', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['volumes', '01', 'action']), - validate=dict( - json={'os-set_bootable': {'bootable': True}})), - ]) + self.register_uris( + [ + self.get_cinder_discovery_mock_dict(), + dict( + method='POST', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes'] + ), + json={'volume': vol1}, + validate=dict( + json={ + 'volume': { + 'size': 50, + 'name': 'vol1', + } + } + ), + ), + dict( + method='POST', + uri=self.get_mock_url( + 'volumev3', + 'public', + append=['volumes', '01', 'action'], + ), + validate=dict( + json={'os-set_bootable': {'bootable': True}} + ), + ), + ] + ) self.cloud.create_volume(50, name='vol1', bootable=True) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_volume_access.py b/openstack/tests/unit/cloud/test_volume_access.py index 8768df694..43cbb1df1 100644 --- a/openstack/tests/unit/cloud/test_volume_access.py +++ b/openstack/tests/unit/cloud/test_volume_access.py @@ -26,165 +26,281 @@ def setUp(self): def test_list_volume_types(self): volume_type = dict( - id='voltype01', description='volume type description', - name='name', is_public=False) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['types']), - json={'volume_types': [volume_type]})]) + id='voltype01', + description='volume type description', + name='name', + is_public=False, + ) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['types'] + ), + json={'volume_types': [volume_type]}, + ) + ] + ) self.assertTrue(self.cloud.list_volume_types()) self.assert_calls() def test_get_volume_type(self): volume_type = dict( - id='voltype01', description='volume type description', name='name', - is_public=False) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['types']), - json={'volume_types': [volume_type]})]) + id='voltype01', + description='volume type description', + name='name', + is_public=False, + ) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['types'] + ), + json={'volume_types': [volume_type]}, + ) + ] + ) volume_type_got = self.cloud.get_volume_type(volume_type['name']) self.assertEqual(volume_type_got.id, volume_type['id']) def test_get_volume_type_access(self): volume_type = dict( - id='voltype01', description='volume type description', name='name', - is_public=False) + id='voltype01', + description='volume type description', + name='name', + is_public=False, + ) volume_type_access = [ dict(volume_type_id='voltype01', name='name', project_id='prj01'), - dict(volume_type_id='voltype01', name='name', project_id='prj02') + dict(volume_type_id='voltype01', name='name', project_id='prj02'), ] - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['types']), - json={'volume_types': [volume_type]}), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['types', volume_type['id'], - 'os-volume-type-access']), - json={'volume_type_access': volume_type_access})]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['types'] + ), + json={'volume_types': [volume_type]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', + 'public', + append=[ + 'types', + volume_type['id'], + 'os-volume-type-access', + ], + ), + json={'volume_type_access': volume_type_access}, + ), + ] + ) self.assertEqual( - len(self.cloud.get_volume_type_access(volume_type['name'])), 2) + len(self.cloud.get_volume_type_access(volume_type['name'])), 2 + ) self.assert_calls() def test_remove_volume_type_access(self): volume_type = dict( - id='voltype01', description='volume type description', name='name', - is_public=False) - project_001 = dict(volume_type_id='voltype01', name='name', - project_id='prj01') - project_002 = dict(volume_type_id='voltype01', name='name', - project_id='prj02') + id='voltype01', + description='volume type description', + name='name', + is_public=False, + ) + project_001 = dict( + volume_type_id='voltype01', name='name', project_id='prj01' + ) + project_002 = dict( + volume_type_id='voltype01', name='name', project_id='prj02' + ) volume_type_access = [project_001, project_002] - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['types']), - json={'volume_types': [volume_type]}), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['types', volume_type['id'], - 'os-volume-type-access']), - json={'volume_type_access': volume_type_access}), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['types']), - json={'volume_types': [volume_type]}), - dict(method='POST', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['types', volume_type['id'], 'action']), - json={'removeProjectAccess': { - 'project': project_001['project_id']}}, - validate=dict( - json={'removeProjectAccess': { - 'project': project_001['project_id']}})), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['types']), - json={'volume_types': [volume_type]}), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['types', volume_type['id'], - 'os-volume-type-access']), - json={'volume_type_access': [project_001]})]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['types'] + ), + json={'volume_types': [volume_type]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', + 'public', + append=[ + 'types', + volume_type['id'], + 'os-volume-type-access', + ], + ), + json={'volume_type_access': volume_type_access}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['types'] + ), + json={'volume_types': [volume_type]}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'volumev3', + 'public', + append=['types', volume_type['id'], 'action'], + ), + json={ + 'removeProjectAccess': { + 'project': project_001['project_id'] + } + }, + validate=dict( + json={ + 'removeProjectAccess': { + 'project': project_001['project_id'] + } + } + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['types'] + ), + json={'volume_types': [volume_type]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', + 'public', + append=[ + 'types', + volume_type['id'], + 'os-volume-type-access', + ], + ), + json={'volume_type_access': [project_001]}, + ), + ] + ) self.assertEqual( - len(self.cloud.get_volume_type_access( - volume_type['name'])), 2) + len(self.cloud.get_volume_type_access(volume_type['name'])), 2 + ) self.cloud.remove_volume_type_access( - volume_type['name'], project_001['project_id']) + volume_type['name'], project_001['project_id'] + ) self.assertEqual( - len(self.cloud.get_volume_type_access(volume_type['name'])), 1) + len(self.cloud.get_volume_type_access(volume_type['name'])), 1 + ) self.assert_calls() def test_add_volume_type_access(self): volume_type = dict( - id='voltype01', description='volume type description', name='name', - is_public=False) - project_001 = dict(volume_type_id='voltype01', name='name', - project_id='prj01') - project_002 = dict(volume_type_id='voltype01', name='name', - project_id='prj02') + id='voltype01', + description='volume type description', + name='name', + is_public=False, + ) + project_001 = dict( + volume_type_id='voltype01', name='name', project_id='prj01' + ) + project_002 = dict( + volume_type_id='voltype01', name='name', project_id='prj02' + ) volume_type_access = [project_001, project_002] - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['types']), - json={'volume_types': [volume_type]}), - dict(method='POST', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['types', volume_type['id'], 'action']), - json={'addProjectAccess': { - 'project': project_002['project_id']}}, - validate=dict( - json={'addProjectAccess': { - 'project': project_002['project_id']}})), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['types']), - json={'volume_types': [volume_type]}), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['types', volume_type['id'], - 'os-volume-type-access']), - json={'volume_type_access': volume_type_access})]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['types'] + ), + json={'volume_types': [volume_type]}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'volumev3', + 'public', + append=['types', volume_type['id'], 'action'], + ), + json={ + 'addProjectAccess': { + 'project': project_002['project_id'] + } + }, + validate=dict( + json={ + 'addProjectAccess': { + 'project': project_002['project_id'] + } + } + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['types'] + ), + json={'volume_types': [volume_type]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', + 'public', + append=[ + 'types', + volume_type['id'], + 'os-volume-type-access', + ], + ), + json={'volume_type_access': volume_type_access}, + ), + ] + ) self.cloud.add_volume_type_access( - volume_type['name'], project_002['project_id']) + volume_type['name'], project_002['project_id'] + ) self.assertEqual( - len(self.cloud.get_volume_type_access(volume_type['name'])), 2) + len(self.cloud.get_volume_type_access(volume_type['name'])), 2 + ) self.assert_calls() def test_add_volume_type_access_missing(self): volume_type = dict( - id='voltype01', description='volume type description', name='name', - is_public=False) - project_001 = dict(volume_type_id='voltype01', name='name', - project_id='prj01') - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['types']), - json={'volume_types': [volume_type]})]) + id='voltype01', + description='volume type description', + name='name', + is_public=False, + ) + project_001 = dict( + volume_type_id='voltype01', name='name', project_id='prj01' + ) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['types'] + ), + json={'volume_types': [volume_type]}, + ) + ] + ) with testtools.ExpectedException( - openstack.cloud.OpenStackCloudException, - "VolumeType not found: MISSING"): + openstack.cloud.OpenStackCloudException, + "VolumeType not found: MISSING", + ): self.cloud.add_volume_type_access( - "MISSING", project_001['project_id']) + "MISSING", project_001['project_id'] + ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_volume_backups.py b/openstack/tests/unit/cloud/test_volume_backups.py index 5329f3934..41a779241 100644 --- a/openstack/tests/unit/cloud/test_volume_backups.py +++ b/openstack/tests/unit/cloud/test_volume_backups.py @@ -21,20 +21,28 @@ def setUp(self): def _compare_backups(self, exp, real): self.assertDictEqual( backup.Backup(**exp).to_dict(computed=False), - real.to_dict(computed=False)) + real.to_dict(computed=False), + ) def test_search_volume_backups(self): name = 'Volume1' vol1 = {'name': name, 'availability_zone': 'az1'} vol2 = {'name': name, 'availability_zone': 'az1'} vol3 = {'name': 'Volume2', 'availability_zone': 'az2'} - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['backups', 'detail']), - json={"backups": [vol1, vol2, vol3]})]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['backups', 'detail'] + ), + json={"backups": [vol1, vol2, vol3]}, + ) + ] + ) result = self.cloud.search_volume_backups( - name, {'availability_zone': 'az1'}) + name, {'availability_zone': 'az1'} + ) self.assertEqual(len(result), 2) for a, b in zip([vol1, vol2], result): self._compare_backups(a, b) @@ -45,26 +53,43 @@ def test_get_volume_backup(self): vol1 = {'name': name, 'availability_zone': 'az1'} vol2 = {'name': name, 'availability_zone': 'az2'} vol3 = {'name': 'Volume2', 'availability_zone': 'az1'} - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['backups', 'detail']), - json={"backups": [vol1, vol2, vol3]})]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['backups', 'detail'] + ), + json={"backups": [vol1, vol2, vol3]}, + ) + ] + ) result = self.cloud.get_volume_backup( - name, {'availability_zone': 'az1'}) + name, {'availability_zone': 'az1'} + ) self._compare_backups(vol1, result) self.assert_calls() def test_list_volume_backups(self): - backup = {'id': '6ff16bdf-44d5-4bf9-b0f3-687549c76414', - 'status': 'available'} + backup = { + 'id': '6ff16bdf-44d5-4bf9-b0f3-687549c76414', + 'status': 'available', + } search_opts = {'status': 'available'} - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['backups', 'detail'], - qs_elements=['='.join(i) for i in search_opts.items()]), - json={"backups": [backup]})]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', + 'public', + append=['backups', 'detail'], + qs_elements=['='.join(i) for i in search_opts.items()], + ), + json={"backups": [backup]}, + ) + ] + ) result = self.cloud.list_volume_backups(True, search_opts) self.assertEqual(len(result), 1) @@ -74,55 +99,78 @@ def test_list_volume_backups(self): def test_delete_volume_backup_wait(self): backup_id = '6ff16bdf-44d5-4bf9-b0f3-687549c76414' backup = {'id': backup_id, 'status': 'available'} - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['backups', 'detail']), - json={"backups": [backup]}), - dict(method='DELETE', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['backups', backup_id])), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['backups', backup_id]), - json={"backup": backup}), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['backups', backup_id]), - status_code=404)]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['backups', 'detail'] + ), + json={"backups": [backup]}, + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'volumev3', 'public', append=['backups', backup_id] + ), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['backups', backup_id] + ), + json={"backup": backup}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['backups', backup_id] + ), + status_code=404, + ), + ] + ) self.cloud.delete_volume_backup(backup_id, False, True, 1) self.assert_calls() def test_delete_volume_backup_force(self): backup_id = '6ff16bdf-44d5-4bf9-b0f3-687549c76414' backup = {'id': backup_id, 'status': 'available'} - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['backups', 'detail']), - json={"backups": [backup]}), - dict(method='POST', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['backups', backup_id, 'action']), - json={'os-force_delete': {}}, - validate=dict(json={u'os-force_delete': {}})), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['backups', backup_id]), - json={"backup": backup}), - dict(method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['backups', backup_id]), - status_code=404) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['backups', 'detail'] + ), + json={"backups": [backup]}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'volumev3', + 'public', + append=['backups', backup_id, 'action'], + ), + json={'os-force_delete': {}}, + validate=dict(json={u'os-force_delete': {}}), + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['backups', backup_id] + ), + json={"backup": backup}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['backups', backup_id] + ), + status_code=404, + ), + ] + ) self.cloud.delete_volume_backup(backup_id, True, True, 1) self.assert_calls() @@ -133,26 +181,32 @@ def test_create_volume_backup(self): 'id': '5678', 'volume_id': volume_id, 'status': 'available', - 'name': backup_name + 'name': backup_name, } - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['backups']), - json={'backup': bak1}, - validate=dict(json={ - 'backup': { - 'name': backup_name, - 'volume_id': volume_id, - 'description': None, - 'force': False, - 'snapshot_id': None, - 'incremental': False - } - })), - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'volumev3', 'public', append=['backups'] + ), + json={'backup': bak1}, + validate=dict( + json={ + 'backup': { + 'name': backup_name, + 'volume_id': volume_id, + 'description': None, + 'force': False, + 'snapshot_id': None, + 'incremental': False, + } + } + ), + ), + ] + ) self.cloud.create_volume_backup(volume_id, name=backup_name) self.assert_calls() @@ -163,28 +217,35 @@ def test_create_incremental_volume_backup(self): 'id': '5678', 'volume_id': volume_id, 'status': 'available', - 'name': backup_name + 'name': backup_name, } - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['backups']), - json={'backup': bak1}, - validate=dict(json={ - 'backup': { - 'name': backup_name, - 'volume_id': volume_id, - 'description': None, - 'force': False, - 'snapshot_id': None, - 'incremental': True - } - })), - ]) - self.cloud.create_volume_backup(volume_id, name=backup_name, - incremental=True) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'volumev3', 'public', append=['backups'] + ), + json={'backup': bak1}, + validate=dict( + json={ + 'backup': { + 'name': backup_name, + 'volume_id': volume_id, + 'description': None, + 'force': False, + 'snapshot_id': None, + 'incremental': True, + } + } + ), + ), + ] + ) + self.cloud.create_volume_backup( + volume_id, name=backup_name, incremental=True + ) self.assert_calls() def test_create_volume_backup_from_snapshot(self): @@ -195,27 +256,33 @@ def test_create_volume_backup_from_snapshot(self): 'id': '5678', 'volume_id': volume_id, 'status': 'available', - 'name': 'bak1' + 'name': 'bak1', } - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'volumev3', 'public', - append=['backups']), - json={'backup': bak1}, - validate=dict(json={ - 'backup': { - 'name': backup_name, - 'volume_id': volume_id, - 'description': None, - 'force': False, - 'snapshot_id': snapshot_id, - 'incremental': False - } - })), - - ]) - self.cloud.create_volume_backup(volume_id, name=backup_name, - snapshot_id=snapshot_id) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'volumev3', 'public', append=['backups'] + ), + json={'backup': bak1}, + validate=dict( + json={ + 'backup': { + 'name': backup_name, + 'volume_id': volume_id, + 'description': None, + 'force': False, + 'snapshot_id': snapshot_id, + 'incremental': False, + } + } + ), + ), + ] + ) + self.cloud.create_volume_backup( + volume_id, name=backup_name, snapshot_id=snapshot_id + ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_zone.py b/openstack/tests/unit/cloud/test_zone.py index 2c3f9b1ba..81a1713c4 100644 --- a/openstack/tests/unit/cloud/test_zone.py +++ b/openstack/tests/unit/cloud/test_zone.py @@ -22,12 +22,11 @@ 'email': 'test@example.net', 'description': 'Example zone', 'ttl': 3600, - 'id': '1' + 'id': '1', } class ZoneTestWrapper: - def __init__(self, ut, attrs): self.remote_res = attrs self.ut = ut @@ -39,8 +38,7 @@ def get_get_response_json(self): return self.remote_res def __getitem__(self, key): - """Dict access to be able to access properties easily - """ + """Dict access to be able to access properties easily""" return self.remote_res[key] def cmp(self, other): @@ -54,48 +52,58 @@ def cmp(self, other): class TestZone(base.TestCase): - def setUp(self): super(TestZone, self).setUp() self.use_designate() def test_create_zone(self): fake_zone = ZoneTestWrapper(self, zone_dict) - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones']), - json=fake_zone.get_create_response_json(), - validate=dict(json={ - 'description': zone_dict['description'], - 'email': zone_dict['email'], - 'name': zone_dict['name'], - 'ttl': zone_dict['ttl'], - 'type': 'PRIMARY' - })) - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'dns', 'public', append=['v2', 'zones'] + ), + json=fake_zone.get_create_response_json(), + validate=dict( + json={ + 'description': zone_dict['description'], + 'email': zone_dict['email'], + 'name': zone_dict['name'], + 'ttl': zone_dict['ttl'], + 'type': 'PRIMARY', + } + ), + ) + ] + ) z = self.cloud.create_zone( name=zone_dict['name'], zone_type=zone_dict['type'], email=zone_dict['email'], description=zone_dict['description'], ttl=zone_dict['ttl'], - masters=None) + masters=None, + ) fake_zone.cmp(z) self.assert_calls() def test_create_zone_exception(self): - self.register_uris([ - dict(method='POST', - uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones']), - status_code=500) - ]) + self.register_uris( + [ + dict( + method='POST', + uri=self.get_mock_url( + 'dns', 'public', append=['v2', 'zones'] + ), + status_code=500, + ) + ] + ) self.assertRaises( - exceptions.SDKException, - self.cloud.create_zone, - 'example.net.' + exceptions.SDKException, self.cloud.create_zone, 'example.net.' ) self.assert_calls() @@ -105,44 +113,75 @@ def test_update_zone(self): updated_zone_dict = copy.copy(zone_dict) updated_zone_dict['ttl'] = new_ttl updated_zone = ZoneTestWrapper(self, updated_zone_dict) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones', fake_zone['id']]), - json=fake_zone.get_get_response_json()), - dict(method='PATCH', - uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones', fake_zone['id']]), - json=updated_zone.get_get_response_json(), - validate=dict(json={"ttl": new_ttl})) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones', fake_zone['id']], + ), + json=fake_zone.get_get_response_json(), + ), + dict( + method='PATCH', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones', fake_zone['id']], + ), + json=updated_zone.get_get_response_json(), + validate=dict(json={"ttl": new_ttl}), + ), + ] + ) z = self.cloud.update_zone(fake_zone['id'], ttl=new_ttl) updated_zone.cmp(z) self.assert_calls() def test_delete_zone(self): fake_zone = ZoneTestWrapper(self, zone_dict) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones', fake_zone['id']]), - json=fake_zone.get_get_response_json()), - dict(method='DELETE', - uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones', fake_zone['id']]), - status_code=202) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones', fake_zone['id']], + ), + json=fake_zone.get_get_response_json(), + ), + dict( + method='DELETE', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones', fake_zone['id']], + ), + status_code=202, + ), + ] + ) self.assertTrue(self.cloud.delete_zone(fake_zone['id'])) self.assert_calls() def test_get_zone_by_id(self): fake_zone = ZoneTestWrapper(self, zone_dict) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones', fake_zone['id']]), - json=fake_zone.get_get_response_json()) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones', fake_zone['id']], + ), + json=fake_zone.get_get_response_json(), + ) + ] + ) res = self.cloud.get_zone(fake_zone['id']) fake_zone.cmp(res) @@ -150,66 +189,103 @@ def test_get_zone_by_id(self): def test_get_zone_by_name(self): fake_zone = ZoneTestWrapper(self, zone_dict) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones', fake_zone['name']]), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones'], - qs_elements=[ - 'name={name}'.format(name=fake_zone['name'])]), - json={"zones": [fake_zone.get_get_response_json()]}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones', fake_zone['name']], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones'], + qs_elements=[ + 'name={name}'.format(name=fake_zone['name']) + ], + ), + json={"zones": [fake_zone.get_get_response_json()]}, + ), + ] + ) res = self.cloud.get_zone(fake_zone['name']) fake_zone.cmp(res) self.assert_calls() def test_get_zone_not_found_returns_false(self): - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones', 'nonexistingzone.net.']), - status_code=404), - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', append=['v2', 'zones'], - qs_elements=['name=nonexistingzone.net.']), - json={"zones": []}) - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones', 'nonexistingzone.net.'], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones'], + qs_elements=['name=nonexistingzone.net.'], + ), + json={"zones": []}, + ), + ] + ) zone = self.cloud.get_zone('nonexistingzone.net.') self.assertFalse(zone) self.assert_calls() def test_list_zones(self): fake_zone = ZoneTestWrapper(self, zone_dict) - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones']), - json={'zones': [fake_zone.get_get_response_json()], - 'links': { - 'next': self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones/'], - qs_elements=['limit=1', 'marker=asd']), - 'self': self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones/'], - qs_elements=['limit=1'])}, - 'metadata':{'total_count': 2}}), - dict(method='GET', - uri=self.get_mock_url( - 'dns', 'public', - append=['v2', 'zones/'], - qs_elements=[ - 'limit=1', 'marker=asd']), - json={'zones': [fake_zone.get_get_response_json()]}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'dns', 'public', append=['v2', 'zones'] + ), + json={ + 'zones': [fake_zone.get_get_response_json()], + 'links': { + 'next': self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones/'], + qs_elements=['limit=1', 'marker=asd'], + ), + 'self': self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones/'], + qs_elements=['limit=1'], + ), + }, + 'metadata': {'total_count': 2}, + }, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'dns', + 'public', + append=['v2', 'zones/'], + qs_elements=['limit=1', 'marker=asd'], + ), + json={'zones': [fake_zone.get_get_response_json()]}, + ), + ] + ) res = self.cloud.list_zones() # updated_rs.cmp(res) From a36f514295a4b4e6157ce69a210f653bcc4df7f2 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 5 May 2023 11:25:50 +0100 Subject: [PATCH 3257/3836] Blackify everything else Black used with the '-l 79 -S' flags. A future change will ignore this commit in git-blame history by adding a 'git-blame-ignore-revs' file. Change-Id: Ie106a5cec8831e113a2b764b62b712a205e3153b Signed-off-by: Stephen Finucane --- doc/source/conf.py | 11 +- examples/cloud/cleanup-servers.py | 10 +- examples/cloud/create-server-dict.py | 31 +- examples/cloud/create-server-name-or-id.py | 21 +- examples/cloud/debug-logging.py | 4 +- examples/cloud/find-an-image.py | 7 +- examples/cloud/http-debug-logging.py | 4 +- examples/cloud/munch-dict-object.py | 1 + examples/cloud/normalization.py | 4 +- examples/cloud/server-information.py | 8 +- .../cloud/service-conditional-overrides.py | 1 + examples/cloud/service-conditionals.py | 1 + examples/cloud/strict-mode.py | 7 +- examples/cloud/upload-large-object.py | 7 +- examples/cloud/upload-object.py | 7 +- examples/cloud/user-agent.py | 4 +- examples/clustering/cluster.py | 9 +- examples/clustering/policy.py | 4 +- examples/clustering/profile.py | 6 +- examples/clustering/receiver.py | 13 +- examples/compute/create.py | 16 +- examples/connect.py | 24 +- examples/image/import.py | 6 +- examples/key_manager/create.py | 12 +- examples/key_manager/list.py | 4 +- examples/network/create.py | 6 +- examples/network/delete.py | 3 +- examples/network/security_group_rules.py | 12 +- .../shared_file_system/share_instances.py | 6 +- openstack/__main__.py | 14 +- openstack/_log.py | 11 +- openstack/_services_mixin.py | 115 +- openstack/common/metadata.py | 20 +- openstack/common/quota_set.py | 18 +- openstack/common/tag.py | 13 +- openstack/config/__init__.py | 17 +- openstack/config/_util.py | 4 +- openstack/config/cloud_config.py | 1 - openstack/config/cloud_region.py | 351 +++-- openstack/config/defaults.py | 3 +- openstack/config/loader.py | 339 ++-- openstack/config/vendors/__init__.py | 8 +- openstack/connection.py | 86 +- openstack/exceptions.py | 57 +- openstack/fixture/connection.py | 9 +- openstack/format.py | 10 +- openstack/proxy.py | 57 +- openstack/resource.py | 48 +- openstack/service_description.py | 67 +- openstack/tests/base.py | 40 +- openstack/tests/fakes.py | 367 +++-- openstack/tests/functional/base.py | 43 +- openstack/tests/unit/base.py | 665 +++++--- .../tests/unit/cloud/test_create_server.py | 4 +- openstack/tests/unit/common/test_metadata.py | 15 +- openstack/tests/unit/common/test_quota_set.py | 58 +- openstack/tests/unit/common/test_tag.py | 6 +- openstack/tests/unit/config/base.py | 70 +- .../tests/unit/config/test_cloud_config.py | 187 ++- openstack/tests/unit/config/test_config.py | 993 +++++++----- openstack/tests/unit/config/test_environ.py | 161 +- openstack/tests/unit/config/test_from_conf.py | 292 ++-- .../tests/unit/config/test_from_session.py | 22 +- openstack/tests/unit/config/test_init.py | 13 +- openstack/tests/unit/config/test_json.py | 15 +- openstack/tests/unit/config/test_loader.py | 81 +- openstack/tests/unit/test_connection.py | 313 ++-- openstack/tests/unit/test_exceptions.py | 124 +- openstack/tests/unit/test_format.py | 1 - openstack/tests/unit/test_hacking.py | 23 +- openstack/tests/unit/test_microversions.py | 51 +- openstack/tests/unit/test_missing_version.py | 8 +- openstack/tests/unit/test_placement_rest.py | 31 +- openstack/tests/unit/test_proxy.py | 248 +-- openstack/tests/unit/test_proxy_base.py | 126 +- openstack/tests/unit/test_resource.py | 1379 ++++++++++------- openstack/tests/unit/test_stats.py | 190 ++- openstack/tests/unit/test_utils.py | 122 +- openstack/utils.py | 82 +- releasenotes/source/conf.py | 34 +- setup.py | 4 +- tools/keystone_version.py | 8 +- tools/nova_version.py | 23 +- tools/print-services.py | 13 +- 84 files changed, 4468 insertions(+), 2841 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 0669681cf..792110237 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -77,10 +77,13 @@ # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ - ('index', - 'doc-openstacksdk.tex', - 'OpenStackSDK Documentation', - 'OpenStack Foundation', 'manual'), + ( + 'index', + 'doc-openstacksdk.tex', + 'OpenStackSDK Documentation', + 'OpenStack Foundation', + 'manual', + ), ] # Allow deeper levels of nesting for \begin...\end stanzas diff --git a/examples/cloud/cleanup-servers.py b/examples/cloud/cleanup-servers.py index 2bf18cde5..076415e5e 100644 --- a/examples/cloud/cleanup-servers.py +++ b/examples/cloud/cleanup-servers.py @@ -16,11 +16,11 @@ openstack.enable_logging(debug=True) for cloud_name, region_name in [ - ('my-vexxhost', 'ca-ymq-1'), - ('my-citycloud', 'Buf1'), - ('my-internap', 'ams01')]: + ('my-vexxhost', 'ca-ymq-1'), + ('my-citycloud', 'Buf1'), + ('my-internap', 'ams01'), +]: # Initialize cloud - cloud = openstack.connect( - cloud=cloud_name, region_name=region_name) + cloud = openstack.connect(cloud=cloud_name, region_name=region_name) for server in cloud.search_servers('my-server'): cloud.delete_server(server, wait=True, delete_ips=True) diff --git a/examples/cloud/create-server-dict.py b/examples/cloud/create-server-dict.py index 30d1f72dc..479923817 100644 --- a/examples/cloud/create-server-dict.py +++ b/examples/cloud/create-server-dict.py @@ -16,20 +16,31 @@ openstack.enable_logging(debug=True) for cloud_name, region_name, image, flavor_id in [ - ('my-vexxhost', 'ca-ymq-1', 'Ubuntu 16.04.1 LTS [2017-03-03]', - '5cf64088-893b-46b5-9bb1-ee020277635d'), - ('my-citycloud', 'Buf1', 'Ubuntu 16.04 Xenial Xerus', - '0dab10b5-42a2-438e-be7b-505741a7ffcc'), - ('my-internap', 'ams01', 'Ubuntu 16.04 LTS (Xenial Xerus)', - 'A1.4')]: + ( + 'my-vexxhost', + 'ca-ymq-1', + 'Ubuntu 16.04.1 LTS [2017-03-03]', + '5cf64088-893b-46b5-9bb1-ee020277635d', + ), + ( + 'my-citycloud', + 'Buf1', + 'Ubuntu 16.04 Xenial Xerus', + '0dab10b5-42a2-438e-be7b-505741a7ffcc', + ), + ('my-internap', 'ams01', 'Ubuntu 16.04 LTS (Xenial Xerus)', 'A1.4'), +]: # Initialize cloud - cloud = openstack.connect( - cloud=cloud_name, region_name=region_name) + cloud = openstack.connect(cloud=cloud_name, region_name=region_name) # Boot a server, wait for it to boot, and then do whatever is needed # to get a public ip for it. server = cloud.create_server( - 'my-server', image=image, flavor=dict(id=flavor_id), - wait=True, auto_ip=True) + 'my-server', + image=image, + flavor=dict(id=flavor_id), + wait=True, + auto_ip=True, + ) # Delete it - this is a demo cloud.delete_server(server, wait=True, delete_ips=True) diff --git a/examples/cloud/create-server-name-or-id.py b/examples/cloud/create-server-name-or-id.py index 06b218848..2f3521d63 100644 --- a/examples/cloud/create-server-name-or-id.py +++ b/examples/cloud/create-server-name-or-id.py @@ -16,21 +16,24 @@ openstack.enable_logging(debug=True) for cloud_name, region_name, image, flavor in [ - ('my-vexxhost', 'ca-ymq-1', - 'Ubuntu 16.04.1 LTS [2017-03-03]', 'v1-standard-4'), - ('my-citycloud', 'Buf1', - 'Ubuntu 16.04 Xenial Xerus', '4C-4GB-100GB'), - ('my-internap', 'ams01', - 'Ubuntu 16.04 LTS (Xenial Xerus)', 'A1.4')]: + ( + 'my-vexxhost', + 'ca-ymq-1', + 'Ubuntu 16.04.1 LTS [2017-03-03]', + 'v1-standard-4', + ), + ('my-citycloud', 'Buf1', 'Ubuntu 16.04 Xenial Xerus', '4C-4GB-100GB'), + ('my-internap', 'ams01', 'Ubuntu 16.04 LTS (Xenial Xerus)', 'A1.4'), +]: # Initialize cloud - cloud = openstack.connect( - cloud=cloud_name, region_name=region_name) + cloud = openstack.connect(cloud=cloud_name, region_name=region_name) cloud.delete_server('my-server', wait=True, delete_ips=True) # Boot a server, wait for it to boot, and then do whatever is needed # to get a public ip for it. server = cloud.create_server( - 'my-server', image=image, flavor=flavor, wait=True, auto_ip=True) + 'my-server', image=image, flavor=flavor, wait=True, auto_ip=True + ) print(server.name) print(server['name']) cloud.pprint(server) diff --git a/examples/cloud/debug-logging.py b/examples/cloud/debug-logging.py index 76fce6797..a49061919 100644 --- a/examples/cloud/debug-logging.py +++ b/examples/cloud/debug-logging.py @@ -11,8 +11,8 @@ # under the License. from openstack import cloud as openstack + openstack.enable_logging(debug=True) -cloud = openstack.connect( - cloud='my-vexxhost', region_name='ca-ymq-1') +cloud = openstack.connect(cloud='my-vexxhost', region_name='ca-ymq-1') cloud.get_image('Ubuntu 16.04.1 LTS [2017-03-03]') diff --git a/examples/cloud/find-an-image.py b/examples/cloud/find-an-image.py index 297a29a87..11da214d0 100644 --- a/examples/cloud/find-an-image.py +++ b/examples/cloud/find-an-image.py @@ -11,9 +11,10 @@ # under the License. from openstack import cloud as openstack + openstack.enable_logging() cloud = openstack.connect(cloud='fuga', region_name='cystack') -cloud.pprint([ - image for image in cloud.list_images() - if 'ubuntu' in image.name.lower()]) +cloud.pprint( + [image for image in cloud.list_images() if 'ubuntu' in image.name.lower()] +) diff --git a/examples/cloud/http-debug-logging.py b/examples/cloud/http-debug-logging.py index b047c81f0..bbe9413cf 100644 --- a/examples/cloud/http-debug-logging.py +++ b/examples/cloud/http-debug-logging.py @@ -11,8 +11,8 @@ # under the License. from openstack import cloud as openstack + openstack.enable_logging(http_debug=True) -cloud = openstack.connect( - cloud='my-vexxhost', region_name='ca-ymq-1') +cloud = openstack.connect(cloud='my-vexxhost', region_name='ca-ymq-1') cloud.get_image('Ubuntu 16.04.1 LTS [2017-03-03]') diff --git a/examples/cloud/munch-dict-object.py b/examples/cloud/munch-dict-object.py index d9e7ff1a0..e7730e7fb 100644 --- a/examples/cloud/munch-dict-object.py +++ b/examples/cloud/munch-dict-object.py @@ -11,6 +11,7 @@ # under the License. from openstack import cloud as openstack + openstack.enable_logging(debug=True) cloud = openstack.connect(cloud='ovh', region_name='SBG1') diff --git a/examples/cloud/normalization.py b/examples/cloud/normalization.py index c7830ad8c..9c719e179 100644 --- a/examples/cloud/normalization.py +++ b/examples/cloud/normalization.py @@ -11,9 +11,11 @@ # under the License. from openstack import cloud as openstack + openstack.enable_logging() cloud = openstack.connect(cloud='fuga', region_name='cystack') image = cloud.get_image( - 'Ubuntu 16.04 LTS - Xenial Xerus - 64-bit - Fuga Cloud Based Image') + 'Ubuntu 16.04 LTS - Xenial Xerus - 64-bit - Fuga Cloud Based Image' +) cloud.pprint(image) diff --git a/examples/cloud/server-information.py b/examples/cloud/server-information.py index 26896e22e..616ed041b 100644 --- a/examples/cloud/server-information.py +++ b/examples/cloud/server-information.py @@ -11,14 +11,18 @@ # under the License. import openstack + openstack.enable_logging(debug=True) cloud = openstack.connect(cloud='my-citycloud', region_name='Buf1') try: server = cloud.create_server( - 'my-server', image='Ubuntu 16.04 Xenial Xerus', + 'my-server', + image='Ubuntu 16.04 Xenial Xerus', flavor=dict(id='0dab10b5-42a2-438e-be7b-505741a7ffcc'), - wait=True, auto_ip=True) + wait=True, + auto_ip=True, + ) print("\n\nFull Server\n\n") cloud.pprint(server) diff --git a/examples/cloud/service-conditional-overrides.py b/examples/cloud/service-conditional-overrides.py index 31e7840e6..77d540e8a 100644 --- a/examples/cloud/service-conditional-overrides.py +++ b/examples/cloud/service-conditional-overrides.py @@ -11,6 +11,7 @@ # under the License. import openstack + openstack.enable_logging(debug=True) cloud = openstack.connect(cloud='rax', region_name='DFW') diff --git a/examples/cloud/service-conditionals.py b/examples/cloud/service-conditionals.py index d17d250b6..f8ca94a22 100644 --- a/examples/cloud/service-conditionals.py +++ b/examples/cloud/service-conditionals.py @@ -11,6 +11,7 @@ # under the License. import openstack + openstack.enable_logging(debug=True) cloud = openstack.connect(cloud='kiss', region_name='region1') diff --git a/examples/cloud/strict-mode.py b/examples/cloud/strict-mode.py index 14877fd78..393af8d1b 100644 --- a/examples/cloud/strict-mode.py +++ b/examples/cloud/strict-mode.py @@ -11,10 +11,11 @@ # under the License. import openstack + openstack.enable_logging() -cloud = openstack.connect( - cloud='fuga', region_name='cystack', strict=True) +cloud = openstack.connect(cloud='fuga', region_name='cystack', strict=True) image = cloud.get_image( - 'Ubuntu 16.04 LTS - Xenial Xerus - 64-bit - Fuga Cloud Based Image') + 'Ubuntu 16.04 LTS - Xenial Xerus - 64-bit - Fuga Cloud Based Image' +) cloud.pprint(image) diff --git a/examples/cloud/upload-large-object.py b/examples/cloud/upload-large-object.py index c88c21c11..2ac3b8438 100644 --- a/examples/cloud/upload-large-object.py +++ b/examples/cloud/upload-large-object.py @@ -11,12 +11,15 @@ # under the License. import openstack + openstack.enable_logging(debug=True) cloud = openstack.connect(cloud='ovh', region_name='SBG1') cloud.create_object( - container='my-container', name='my-object', + container='my-container', + name='my-object', filename='/home/mordred/briarcliff.sh3d', - segment_size=1000000) + segment_size=1000000, +) cloud.delete_object('my-container', 'my-object') cloud.delete_container('my-container') diff --git a/examples/cloud/upload-object.py b/examples/cloud/upload-object.py index c88c21c11..2ac3b8438 100644 --- a/examples/cloud/upload-object.py +++ b/examples/cloud/upload-object.py @@ -11,12 +11,15 @@ # under the License. import openstack + openstack.enable_logging(debug=True) cloud = openstack.connect(cloud='ovh', region_name='SBG1') cloud.create_object( - container='my-container', name='my-object', + container='my-container', + name='my-object', filename='/home/mordred/briarcliff.sh3d', - segment_size=1000000) + segment_size=1000000, +) cloud.delete_object('my-container', 'my-object') cloud.delete_container('my-container') diff --git a/examples/cloud/user-agent.py b/examples/cloud/user-agent.py index 52ddd4750..b616dcb4c 100644 --- a/examples/cloud/user-agent.py +++ b/examples/cloud/user-agent.py @@ -11,8 +11,10 @@ # under the License. import openstack + openstack.enable_logging(http_debug=True) cloud = openstack.connect( - cloud='datacentred', app_name='AmazingApp', app_version='1.0') + cloud='datacentred', app_name='AmazingApp', app_version='1.0' +) cloud.list_networks() diff --git a/examples/clustering/cluster.py b/examples/clustering/cluster.py index 60f06ae19..ddeb5e99f 100644 --- a/examples/clustering/cluster.py +++ b/examples/clustering/cluster.py @@ -107,9 +107,7 @@ def replace_nodes_in_cluster(conn): old_node = NODE_ID new_node = "cd803d4a-015d-4223-b15f-db29bad3146c" - spec = { - old_node: new_node - } + spec = {old_node: new_node} res = conn.clustering.replace_nodes_in_cluster(CLUSTER_ID, **spec) print(res) @@ -135,7 +133,7 @@ def resize_cluster(conn): 'min_size': 1, 'max_size': 6, 'adjustment_type': 'EXACT_CAPACITY', - 'number': 2 + 'number': 2, } res = conn.clustering.resize_cluster(CLUSTER_ID, **spec) print(res) @@ -146,7 +144,8 @@ def attach_policy_to_cluster(conn): spec = {'enabled': True} res = conn.clustering.attach_policy_to_cluster( - CLUSTER_ID, POLICY_ID, **spec) + CLUSTER_ID, POLICY_ID, **spec + ) print(res) diff --git a/examples/clustering/policy.py b/examples/clustering/policy.py index 328196fd6..da56e6343 100644 --- a/examples/clustering/policy.py +++ b/examples/clustering/policy.py @@ -38,8 +38,8 @@ def create_policy(conn): 'properties': { 'criteria': 'oldest_first', 'destroy_after_deletion': True, - } - } + }, + }, } policy = conn.clustering.create_policy(attrs) diff --git a/examples/clustering/profile.py b/examples/clustering/profile.py index d69fe1996..9fc2c9fcb 100644 --- a/examples/clustering/profile.py +++ b/examples/clustering/profile.py @@ -44,10 +44,8 @@ def create_profile(conn): 'name': SERVER_NAME, 'flavor': FLAVOR_NAME, 'image': IMAGE_NAME, - 'networks': { - 'network': NETWORK_NAME - } - } + 'networks': {'network': NETWORK_NAME}, + }, } profile = conn.clustering.create_profile(spec) diff --git a/examples/clustering/receiver.py b/examples/clustering/receiver.py index 84c3febe3..2c9a9c4b3 100644 --- a/examples/clustering/receiver.py +++ b/examples/clustering/receiver.py @@ -39,10 +39,8 @@ def create_receiver(conn): "action": "CLUSTER_SCALE_OUT", "cluster_id": CLUSTER_ID, "name": FAKE_NAME, - "params": { - "count": "1" - }, - "type": "webhook" + "params": {"count": "1"}, + "type": "webhook", } receiver = conn.clustering.create_receiver(**spec) @@ -66,12 +64,7 @@ def find_receiver(conn): def update_receiver(conn): print("Update Receiver:") - spec = { - "name": "test_receiver2", - "params": { - "count": "2" - } - } + spec = {"name": "test_receiver2", "params": {"count": "2"}} receiver = conn.clustering.update_receiver(FAKE_NAME, **spec) print(receiver.to_dict()) diff --git a/examples/compute/create.py b/examples/compute/create.py index 7a3aa0850..d4250efec 100644 --- a/examples/compute/create.py +++ b/examples/compute/create.py @@ -62,11 +62,17 @@ def create_server(conn): keypair = create_keypair(conn) server = conn.compute.create_server( - name=SERVER_NAME, image_id=image.id, flavor_id=flavor.id, - networks=[{"uuid": network.id}], key_name=keypair.name) + name=SERVER_NAME, + image_id=image.id, + flavor_id=flavor.id, + networks=[{"uuid": network.id}], + key_name=keypair.name, + ) server = conn.compute.wait_for_server(server) - print("ssh -i {key} root@{ip}".format( - key=PRIVATE_KEYPAIR_FILE, - ip=server.access_ipv4)) + print( + "ssh -i {key} root@{ip}".format( + key=PRIVATE_KEYPAIR_FILE, ip=server.access_ipv4 + ) + ) diff --git a/examples/connect.py b/examples/connect.py index 12bbfc334..50de0fde2 100644 --- a/examples/connect.py +++ b/examples/connect.py @@ -45,8 +45,9 @@ def __init__(self, cloud_name='devstack-admin', debug=False): def _get_resource_value(resource_key, default): - return config.get_extra_config( - EXAMPLE_CONFIG_KEY).get(resource_key, default) + return config.get_extra_config(EXAMPLE_CONFIG_KEY).get( + resource_key, default + ) SERVER_NAME = 'openstacksdk-example' @@ -55,10 +56,12 @@ def _get_resource_value(resource_key, default): NETWORK_NAME = _get_resource_value('network_name', 'private') KEYPAIR_NAME = _get_resource_value('keypair_name', 'openstacksdk-example') SSH_DIR = _get_resource_value( - 'ssh_dir', '{home}/.ssh'.format(home=os.path.expanduser("~"))) + 'ssh_dir', '{home}/.ssh'.format(home=os.path.expanduser("~")) +) PRIVATE_KEYPAIR_FILE = _get_resource_value( - 'private_keypair_file', '{ssh_dir}/id_rsa.{key}'.format( - ssh_dir=SSH_DIR, key=KEYPAIR_NAME)) + 'private_keypair_file', + '{ssh_dir}/id_rsa.{key}'.format(ssh_dir=SSH_DIR, key=KEYPAIR_NAME), +) EXAMPLE_IMAGE_NAME = 'openstacksdk-example-public-image' @@ -72,8 +75,15 @@ def create_connection_from_args(): return openstack.connect(options=parser) -def create_connection(auth_url, region, project_name, username, password, - user_domain, project_domain): +def create_connection( + auth_url, + region, + project_name, + username, + password, + user_domain, + project_domain, +): return openstack.connect( auth_url=auth_url, project_name=project_name, diff --git a/examples/image/import.py b/examples/image/import.py index 277bb6349..44be08213 100644 --- a/examples/image/import.py +++ b/examples/image/import.py @@ -24,8 +24,10 @@ def import_image(conn): print("Import Image:") # Url where glance can download the image - uri = 'https://download.cirros-cloud.net/0.4.0/' \ - 'cirros-0.4.0-x86_64-disk.img' + uri = ( + 'https://download.cirros-cloud.net/0.4.0/' + 'cirros-0.4.0-x86_64-disk.img' + ) # Build the image attributes and import the image. image_attrs = { diff --git a/examples/key_manager/create.py b/examples/key_manager/create.py index c45e7dc2e..b01387ba9 100644 --- a/examples/key_manager/create.py +++ b/examples/key_manager/create.py @@ -18,8 +18,10 @@ def create_secret(conn): print("Create a secret:") - conn.key_manager.create_secret(name="My public key", - secret_type="public", - expiration="2020-02-28T23:59:59", - payload="ssh rsa...", - payload_content_type="text/plain") + conn.key_manager.create_secret( + name="My public key", + secret_type="public", + expiration="2020-02-28T23:59:59", + payload="ssh rsa...", + payload_content_type="text/plain", + ) diff --git a/examples/key_manager/list.py b/examples/key_manager/list.py index b74e4df7d..17f989e41 100644 --- a/examples/key_manager/list.py +++ b/examples/key_manager/list.py @@ -26,6 +26,6 @@ def list_secrets_query(conn): print("List Secrets:") for secret in conn.key_manager.secrets( - secret_type="symmetric", - expiration="gte:2020-01-01T00:00:00"): + secret_type="symmetric", expiration="gte:2020-01-01T00:00:00" + ): print(secret) diff --git a/examples/network/create.py b/examples/network/create.py index 1342d7a98..bb25a81a1 100644 --- a/examples/network/create.py +++ b/examples/network/create.py @@ -22,7 +22,8 @@ def create_network(conn): print("Create Network:") example_network = conn.network.create_network( - name='openstacksdk-example-project-network') + name='openstacksdk-example-project-network' + ) print(example_network) @@ -31,6 +32,7 @@ def create_network(conn): network_id=example_network.id, ip_version='4', cidr='10.0.2.0/24', - gateway_ip='10.0.2.1') + gateway_ip='10.0.2.1', + ) print(example_subnet) diff --git a/examples/network/delete.py b/examples/network/delete.py index 720dfb5be..b43413ff5 100644 --- a/examples/network/delete.py +++ b/examples/network/delete.py @@ -22,7 +22,8 @@ def delete_network(conn): print("Delete Network:") example_network = conn.network.find_network( - 'openstacksdk-example-project-network') + 'openstacksdk-example-project-network' + ) for example_subnet in example_network.subnet_ids: conn.network.delete_subnet(example_subnet, ignore_missing=False) diff --git a/examples/network/security_group_rules.py b/examples/network/security_group_rules.py index b6c1533b5..99363c336 100644 --- a/examples/network/security_group_rules.py +++ b/examples/network/security_group_rules.py @@ -22,7 +22,8 @@ def open_port(conn): print("Open a port:") example_sec_group = conn.network.create_security_group( - name='openstacksdk-example-security-group') + name='openstacksdk-example-security-group' + ) print(example_sec_group) @@ -33,7 +34,8 @@ def open_port(conn): protocol='HTTPS', port_range_max='443', port_range_min='443', - ethertype='IPv4') + ethertype='IPv4', + ) print(example_rule) @@ -42,7 +44,8 @@ def allow_ping(conn): print("Allow pings:") example_sec_group = conn.network.create_security_group( - name='openstacksdk-example-security-group2') + name='openstacksdk-example-security-group2' + ) print(example_sec_group) @@ -53,6 +56,7 @@ def allow_ping(conn): protocol='icmp', port_range_max=None, port_range_min=None, - ethertype='IPv4') + ethertype='IPv4', + ) print(example_rule) diff --git a/examples/shared_file_system/share_instances.py b/examples/shared_file_system/share_instances.py index 93a6cbf0d..972d2b632 100644 --- a/examples/shared_file_system/share_instances.py +++ b/examples/shared_file_system/share_instances.py @@ -31,8 +31,10 @@ def get_share_instance(conn, share_instance_id): def reset_share_instance_status(conn, share_instance_id, status): - print('Reset the status of the share instance with the given ' - 'share_instance_id to the given status') + print( + 'Reset the status of the share instance with the given ' + 'share_instance_id to the given status' + ) conn.share.reset_share_instance_status(share_instance_id, status) diff --git a/openstack/__main__.py b/openstack/__main__.py index 30d775de9..cf3fcb34f 100644 --- a/openstack/__main__.py +++ b/openstack/__main__.py @@ -19,16 +19,18 @@ def show_version(args): - print("OpenstackSDK Version %s" % - pbr.version.VersionInfo('openstacksdk').version_string_with_vcs()) + print( + "OpenstackSDK Version %s" + % pbr.version.VersionInfo('openstacksdk').version_string_with_vcs() + ) parser = argparse.ArgumentParser(description="Openstack SDK") -subparsers = parser.add_subparsers(title='commands', - dest='command') +subparsers = parser.add_subparsers(title='commands', dest='command') -cmd_version = subparsers.add_parser('version', - help='show Openstack SDK version') +cmd_version = subparsers.add_parser( + 'version', help='show Openstack SDK version' +) cmd_version.set_defaults(func=show_version) args = parser.parse_args() diff --git a/openstack/_log.py b/openstack/_log.py index 6c298414a..6a909fc34 100644 --- a/openstack/_log.py +++ b/openstack/_log.py @@ -44,7 +44,10 @@ def setup_logging(name, handlers=None, level=None): def enable_logging( - debug=False, http_debug=False, path=None, stream=None, + debug=False, + http_debug=False, + path=None, + stream=None, format_stream=False, format_template='%(asctime)s %(levelname)s: %(name)s %(message)s', handlers=None, @@ -121,9 +124,11 @@ def enable_logging( # enable_logging should not be used and instead python logging should # be configured directly. setup_logging( - 'urllib3', handlers=[logging.NullHandler()], level=logging.INFO) + 'urllib3', handlers=[logging.NullHandler()], level=logging.INFO + ) setup_logging( - 'stevedore', handlers=[logging.NullHandler()], level=logging.INFO) + 'stevedore', handlers=[logging.NullHandler()], level=logging.INFO + ) # Suppress warning about keystoneauth loggers setup_logging('keystoneauth.discovery') setup_logging('keystoneauth.identity.base') diff --git a/openstack/_services_mixin.py b/openstack/_services_mixin.py index 13c5358c7..187c2862c 100644 --- a/openstack/_services_mixin.py +++ b/openstack/_services_mixin.py @@ -6,7 +6,9 @@ from openstack.block_storage import block_storage_service from openstack.clustering import clustering_service from openstack.compute import compute_service -from openstack.container_infrastructure_management import container_infrastructure_management_service +from openstack.container_infrastructure_management import ( + container_infrastructure_management_service, +) from openstack.database import database_service from openstack.dns import dns_service from openstack.identity import identity_service @@ -31,32 +33,52 @@ class ServicesMixin: image = image_service.ImageService(service_type='image') - load_balancer = load_balancer_service.LoadBalancerService(service_type='load-balancer') + load_balancer = load_balancer_service.LoadBalancerService( + service_type='load-balancer' + ) - object_store = object_store_service.ObjectStoreService(service_type='object-store') + object_store = object_store_service.ObjectStoreService( + service_type='object-store' + ) - clustering = clustering_service.ClusteringService(service_type='clustering') + clustering = clustering_service.ClusteringService( + service_type='clustering' + ) resource_cluster = clustering cluster = clustering - data_processing = service_description.ServiceDescription(service_type='data-processing') + data_processing = service_description.ServiceDescription( + service_type='data-processing' + ) baremetal = baremetal_service.BaremetalService(service_type='baremetal') bare_metal = baremetal - baremetal_introspection = baremetal_introspection_service.BaremetalIntrospectionService(service_type='baremetal-introspection') + baremetal_introspection = ( + baremetal_introspection_service.BaremetalIntrospectionService( + service_type='baremetal-introspection' + ) + ) - key_manager = key_manager_service.KeyManagerService(service_type='key-manager') + key_manager = key_manager_service.KeyManagerService( + service_type='key-manager' + ) - resource_optimization = service_description.ServiceDescription(service_type='resource-optimization') + resource_optimization = service_description.ServiceDescription( + service_type='resource-optimization' + ) infra_optim = resource_optimization message = message_service.MessageService(service_type='message') messaging = message - application_catalog = service_description.ServiceDescription(service_type='application-catalog') + application_catalog = service_description.ServiceDescription( + service_type='application-catalog' + ) - container_infrastructure_management = container_infrastructure_management_service.ContainerInfrastructureManagementService(service_type='container-infrastructure-management') + container_infrastructure_management = container_infrastructure_management_service.ContainerInfrastructureManagementService( + service_type='container-infrastructure-management' + ) container_infra = container_infrastructure_management container_infrastructure = container_infrastructure_management @@ -68,17 +90,27 @@ class ServicesMixin: rating = service_description.ServiceDescription(service_type='rating') - operator_policy = service_description.ServiceDescription(service_type='operator-policy') + operator_policy = service_description.ServiceDescription( + service_type='operator-policy' + ) policy = operator_policy - shared_file_system = shared_file_system_service.SharedFilesystemService(service_type='shared-file-system') + shared_file_system = shared_file_system_service.SharedFilesystemService( + service_type='shared-file-system' + ) share = shared_file_system - data_protection_orchestration = service_description.ServiceDescription(service_type='data-protection-orchestration') + data_protection_orchestration = service_description.ServiceDescription( + service_type='data-protection-orchestration' + ) - orchestration = orchestration_service.OrchestrationService(service_type='orchestration') + orchestration = orchestration_service.OrchestrationService( + service_type='orchestration' + ) - block_storage = block_storage_service.BlockStorageService(service_type='block-storage') + block_storage = block_storage_service.BlockStorageService( + service_type='block-storage' + ) block_store = block_storage volume = block_storage @@ -92,44 +124,69 @@ class ServicesMixin: event = service_description.ServiceDescription(service_type='event') events = event - application_deployment = service_description.ServiceDescription(service_type='application-deployment') + application_deployment = service_description.ServiceDescription( + service_type='application-deployment' + ) application_deployment = application_deployment - multi_region_network_automation = service_description.ServiceDescription(service_type='multi-region-network-automation') + multi_region_network_automation = service_description.ServiceDescription( + service_type='multi-region-network-automation' + ) tricircle = multi_region_network_automation database = database_service.DatabaseService(service_type='database') - application_container = service_description.ServiceDescription(service_type='application-container') + application_container = service_description.ServiceDescription( + service_type='application-container' + ) container = application_container - root_cause_analysis = service_description.ServiceDescription(service_type='root-cause-analysis') + root_cause_analysis = service_description.ServiceDescription( + service_type='root-cause-analysis' + ) rca = root_cause_analysis - nfv_orchestration = service_description.ServiceDescription(service_type='nfv-orchestration') + nfv_orchestration = service_description.ServiceDescription( + service_type='nfv-orchestration' + ) network = network_service.NetworkService(service_type='network') backup = service_description.ServiceDescription(service_type='backup') - monitoring_logging = service_description.ServiceDescription(service_type='monitoring-logging') + monitoring_logging = service_description.ServiceDescription( + service_type='monitoring-logging' + ) monitoring_log_api = monitoring_logging - monitoring = service_description.ServiceDescription(service_type='monitoring') + monitoring = service_description.ServiceDescription( + service_type='monitoring' + ) - monitoring_events = service_description.ServiceDescription(service_type='monitoring-events') + monitoring_events = service_description.ServiceDescription( + service_type='monitoring-events' + ) placement = placement_service.PlacementService(service_type='placement') - instance_ha = instance_ha_service.InstanceHaService(service_type='instance-ha') + instance_ha = instance_ha_service.InstanceHaService( + service_type='instance-ha' + ) ha = instance_ha - reservation = service_description.ServiceDescription(service_type='reservation') + reservation = service_description.ServiceDescription( + service_type='reservation' + ) - function_engine = service_description.ServiceDescription(service_type='function-engine') + function_engine = service_description.ServiceDescription( + service_type='function-engine' + ) - accelerator = accelerator_service.AcceleratorService(service_type='accelerator') + accelerator = accelerator_service.AcceleratorService( + service_type='accelerator' + ) - admin_logic = service_description.ServiceDescription(service_type='admin-logic') + admin_logic = service_description.ServiceDescription( + service_type='admin-logic' + ) registration = admin_logic - diff --git a/openstack/common/metadata.py b/openstack/common/metadata.py index 9f129141e..531663ea1 100644 --- a/openstack/common/metadata.py +++ b/openstack/common/metadata.py @@ -82,14 +82,13 @@ def get_metadata_item(self, session, key): url = utils.urljoin(self.base_path, self.id, 'metadata', key) response = session.get(url) exceptions.raise_from_response( - response, error_message='Metadata item does not exist') + response, error_message='Metadata item does not exist' + ) meta = response.json().get('meta', {}) # Here we need to potentially init metadata metadata = self.metadata or {} metadata[key] = meta.get(key) - self._body.attributes.update({ - 'metadata': metadata - }) + self._body.attributes.update({'metadata': metadata}) return self @@ -101,17 +100,12 @@ def set_metadata_item(self, session, key, value): :param str value: The value. """ url = utils.urljoin(self.base_path, self.id, 'metadata', key) - response = session.put( - url, - json={'meta': {key: value}} - ) + response = session.put(url, json={'meta': {key: value}}) exceptions.raise_from_response(response) # we do not want to update tags directly metadata = self.metadata metadata[key] = value - self._body.attributes.update({ - 'metadata': metadata - }) + self._body.attributes.update({'metadata': metadata}) return self def delete_metadata_item(self, session, key): @@ -132,7 +126,5 @@ def delete_metadata_item(self, session, key): metadata = {} except ValueError: pass # do nothing! - self._body.attributes.update({ - 'metadata': metadata - }) + self._body.attributes.update({'metadata': metadata}) return self diff --git a/openstack/common/quota_set.py b/openstack/common/quota_set.py index 34730118b..c298416e6 100644 --- a/openstack/common/quota_set.py +++ b/openstack/common/quota_set.py @@ -26,8 +26,7 @@ class QuotaSet(resource.Resource): allow_delete = True allow_commit = True - _query_mapping = resource.QueryParameters( - "usage") + _query_mapping = resource.QueryParameters("usage") # NOTE(gtema) Sadly this attribute is useless in all the methods, but keep # it here extra as a reminder @@ -47,8 +46,14 @@ class QuotaSet(resource.Resource): project_id = resource.URI('project_id') - def fetch(self, session, requires_id=False, - base_path=None, error_message=None, **params): + def fetch( + self, + session, + requires_id=False, + base_path=None, + error_message=None, + **params + ): return super(QuotaSet, self).fetch( session, requires_id=False, @@ -93,8 +98,9 @@ def _translate_response(self, response, has_body=None, error_message=None): if 'in_use' in val: normalized_attrs['usage'][key] = val['in_use'] if 'reserved' in val: - normalized_attrs['reservation'][key] = \ - val['reserved'] + normalized_attrs['reservation'][key] = val[ + 'reserved' + ] if 'limit' in val: normalized_attrs[key] = val['limit'] else: diff --git a/openstack/common/tag.py b/openstack/common/tag.py index 885f1da60..6f07a36f5 100644 --- a/openstack/common/tag.py +++ b/openstack/common/tag.py @@ -81,8 +81,9 @@ def check_tag(self, session, tag): url = utils.urljoin(self.base_path, self.id, 'tags', tag) session = self._get_session(session) response = session.get(url) - exceptions.raise_from_response(response, - error_message='Tag does not exist') + exceptions.raise_from_response( + response, error_message='Tag does not exist' + ) return self def add_tag(self, session, tag): @@ -98,9 +99,7 @@ def add_tag(self, session, tag): # we do not want to update tags directly tags = self.tags tags.append(tag) - self._body.attributes.update({ - 'tags': tags - }) + self._body.attributes.update({'tags': tags}) return self def remove_tag(self, session, tag): @@ -121,7 +120,5 @@ def remove_tag(self, session, tag): tags.remove(tag) except ValueError: pass # do nothing! - self._body.attributes.update({ - 'tags': tags - }) + self._body.attributes.update({'tags': tags}) return self diff --git a/openstack/config/__init__.py b/openstack/config/__init__.py index 4919b171b..c7950dd0c 100644 --- a/openstack/config/__init__.py +++ b/openstack/config/__init__.py @@ -18,15 +18,20 @@ def get_cloud_region( - service_key=None, options=None, - app_name=None, app_version=None, - load_yaml_config=True, - load_envvars=True, - **kwargs): + service_key=None, + options=None, + app_name=None, + app_version=None, + load_yaml_config=True, + load_envvars=True, + **kwargs +): config = OpenStackConfig( load_yaml_config=load_yaml_config, load_envvars=load_envvars, - app_name=app_name, app_version=app_version) + app_name=app_name, + app_version=app_version, + ) if options: config.register_argparse_arguments(options, sys.argv, service_key) parsed_options = options.parse_known_args(sys.argv) diff --git a/openstack/config/_util.py b/openstack/config/_util.py index c77aaf235..d52c6e893 100644 --- a/openstack/config/_util.py +++ b/openstack/config/_util.py @@ -22,7 +22,9 @@ def normalize_keys(config): elif isinstance(value, bool): new_config[key] = value elif isinstance(value, int) and key not in ( - 'verbose_level', 'api_timeout'): + 'verbose_level', + 'api_timeout', + ): new_config[key] = str(value) elif isinstance(value, float): new_config[key] = str(value) diff --git a/openstack/config/cloud_config.py b/openstack/config/cloud_config.py index 168c43edf..7ce2bff3d 100644 --- a/openstack/config/cloud_config.py +++ b/openstack/config/cloud_config.py @@ -18,7 +18,6 @@ class CloudConfig(cloud_region.CloudRegion): - def __init__(self, name, region, config, **kwargs): super(CloudConfig, self).__init__(name, region, config, **kwargs) self.region = region diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 55a1bd98a..9ab67e387 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -28,6 +28,7 @@ from keystoneauth1 import session as ks_session import os_service_types import requestsexceptions + try: import statsd except ImportError: @@ -52,9 +53,11 @@ _logger = _log.setup_logging('openstack') SCOPE_KEYS = { - 'domain_id', 'domain_name', - 'project_id', 'project_name', - 'system_scope' + 'domain_id', + 'domain_name', + 'project_id', + 'project_name', + 'system_scope', } # Sentinel for nonexistence @@ -90,9 +93,15 @@ def _get_implied_microversion(version): return version -def from_session(session, name=None, region_name=None, - force_ipv4=False, - app_name=None, app_version=None, **kwargs): +def from_session( + session, + name=None, + region_name=None, + force_ipv4=False, + app_name=None, + app_version=None, + **kwargs +): """Construct a CloudRegion from an existing `keystoneauth1.session.Session` When a Session already exists, we don't actually even need to go through @@ -118,9 +127,14 @@ def from_session(session, name=None, region_name=None, config_dict = config_defaults.get_defaults() config_dict.update(**kwargs) return CloudRegion( - name=name, session=session, config=config_dict, - region_name=region_name, force_ipv4=force_ipv4, - app_name=app_name, app_version=app_version) + name=name, + session=session, + config=config_dict, + region_name=region_name, + force_ipv4=force_ipv4, + app_name=app_name, + app_version=app_version, + ) def from_conf(conf, session=None, service_types=None, **kwargs): @@ -160,8 +174,10 @@ def from_conf(conf, session=None, service_types=None, **kwargs): for st in stm.all_types_by_service_type: if service_types is not None and st not in service_types: _disable_service( - config_dict, st, - reason="Not in the list of requested service_types.") + config_dict, + st, + reason="Not in the list of requested service_types.", + ) continue project_name = stm.get_project_name(st) if project_name not in conf: @@ -170,10 +186,13 @@ def from_conf(conf, session=None, service_types=None, **kwargs): if project_name not in conf: _disable_service( - config_dict, st, + config_dict, + st, reason="No section for project '{project}' (service type " - "'{service_type}') was present in the config." - .format(project=project_name, service_type=st)) + "'{service_type}') was present in the config.".format( + project=project_name, service_type=st + ), + ) continue opt_dict = {} # Populate opt_dict with (appropriately processed) Adapter conf opts @@ -189,20 +208,24 @@ def from_conf(conf, session=None, service_types=None, **kwargs): # option of) blowing up right away for (2) rather than letting them # get all the way to the point of trying the service and having # *that* blow up. - reason = ("Encountered an exception attempting to process config " - "for project '{project}' (service type " - "'{service_type}'): {exception}".format( - project=project_name, service_type=st, exception=e)) - _logger.warning("Disabling service '{service_type}': " - "{reason}".format(service_type=st, reason=reason)) + reason = ( + "Encountered an exception attempting to process config " + "for project '{project}' (service type " + "'{service_type}'): {exception}".format( + project=project_name, service_type=st, exception=e + ) + ) + _logger.warning( + "Disabling service '{service_type}': " + "{reason}".format(service_type=st, reason=reason) + ) _disable_service(config_dict, st, reason=reason) continue # Load them into config_dict under keys prefixed by ${service_type}_ for raw_name, opt_val in opt_dict.items(): config_name = _make_key(raw_name, st) config_dict[config_name] = opt_val - return CloudRegion( - session=session, config=config_dict, **kwargs) + return CloudRegion(session=session, config=config_dict, **kwargs) class CloudRegion: @@ -232,18 +255,34 @@ class CloudRegion: 'interface': 'public' """ - def __init__(self, name=None, region_name=None, config=None, - force_ipv4=False, auth_plugin=None, - openstack_config=None, session_constructor=None, - app_name=None, app_version=None, session=None, - discovery_cache=None, extra_config=None, - cache_expiration_time=0, cache_expirations=None, - cache_path=None, cache_class='dogpile.cache.null', - cache_arguments=None, password_callback=None, - statsd_host=None, statsd_port=None, statsd_prefix=None, - influxdb_config=None, - collector_registry=None, - cache_auth=False): + + def __init__( + self, + name=None, + region_name=None, + config=None, + force_ipv4=False, + auth_plugin=None, + openstack_config=None, + session_constructor=None, + app_name=None, + app_version=None, + session=None, + discovery_cache=None, + extra_config=None, + cache_expiration_time=0, + cache_expirations=None, + cache_path=None, + cache_class='dogpile.cache.null', + cache_arguments=None, + password_callback=None, + statsd_host=None, + statsd_port=None, + statsd_prefix=None, + influxdb_config=None, + collector_registry=None, + cache_auth=False, + ): self._name = name self.config = _util.normalize_keys(config) # NOTE(efried): For backward compatibility: a) continue to accept the @@ -294,9 +333,7 @@ def __iter__(self): return self.config.__iter__() def __eq__(self, other): - return ( - self.name == other.name - and self.config == other.config) + return self.name == other.name and self.config == other.config def __ne__(self, other): return not self == other @@ -306,7 +343,8 @@ def name(self): if self._name is None: try: self._name = urllib.parse.urlparse( - self.get_session().auth.auth_url).hostname + self.get_session().auth.auth_url + ).hostname except Exception: self._name = self._app_name or '' return self._name @@ -352,7 +390,9 @@ def get_requests_verify_args(self): "You are specifying a cacert for the cloud {full_name}" " but also to ignore the host verification. The host SSL" " cert will not be verified.".format( - full_name=self.full_name)) + full_name=self.full_name + ) + ) cert = self.config.get('cert') if cert: @@ -365,19 +405,23 @@ def get_services(self): """Return a list of service types we know something about.""" services = [] for key, val in self.config.items(): - if (key.endswith('api_version') - or key.endswith('service_type') - or key.endswith('service_name')): + if ( + key.endswith('api_version') + or key.endswith('service_type') + or key.endswith('service_name') + ): services.append("_".join(key.split('_')[:-2])) return list(set(services)) def get_enabled_services(self): services = set() - all_services = [k['service_type'] for k in - self._service_type_manager.services] - all_services.extend(k[4:] for k in - self.config.keys() if k.startswith('has_')) + all_services = [ + k['service_type'] for k in self._service_type_manager.services + ] + all_services.extend( + k[4:] for k in self.config.keys() if k.startswith('has_') + ) for srv in all_services: ep = self.get_endpoint_from_catalog(srv) @@ -390,10 +434,13 @@ def get_auth_args(self): return self.config.get('auth', {}) def _get_config( - self, key, service_type, - default=None, - fallback_to_unprefixed=False, - converter=None): + self, + key, + service_type, + default=None, + fallback_to_unprefixed=False, + converter=None, + ): '''Get a config value for a service_type. Finds the config value for a key, looking first for it prefixed by @@ -442,11 +489,13 @@ def get_region_name(self, service_type=None): # If a region_name for the specific service_type is configured, use it; # else use the one configured for the CloudRegion as a whole. return self._get_config( - 'region_name', service_type, fallback_to_unprefixed=True) + 'region_name', service_type, fallback_to_unprefixed=True + ) def get_interface(self, service_type=None): return self._get_config( - 'interface', service_type, fallback_to_unprefixed=True) + 'interface', service_type, fallback_to_unprefixed=True + ) def get_api_version(self, service_type): version = self._get_config('api_version', service_type) @@ -458,7 +507,8 @@ def get_api_version(self, service_type): warnings.warn( "You have a configured API_VERSION with 'latest' in" " it. In the context of openstacksdk this doesn't make" - " any sense.") + " any sense." + ) return None return version @@ -475,9 +525,11 @@ def get_service_type(self, service_type): # type will get us things in the right order. if self._service_type_manager.is_known(service_type): service_type = self._service_type_manager.get_service_type( - service_type) + service_type + ) return self._get_config( - 'service_type', service_type, default=service_type) + 'service_type', service_type, default=service_type + ) def get_service_name(self, service_type): return self._get_config('service_name', service_type) @@ -492,8 +544,11 @@ def get_endpoint(self, service_type): # then the endpoint value is the endpoint_override for every # service. value = auth.get('endpoint') - if (not value and service_type == 'identity' - and SCOPE_KEYS.isdisjoint(set(auth.keys()))): + if ( + not value + and service_type == 'identity' + and SCOPE_KEYS.isdisjoint(set(auth.keys())) + ): # There are a small number of unscoped identity operations. # Specifically, looking up a list of projects/domains/system to # scope to. @@ -503,7 +558,8 @@ def get_endpoint(self, service_type): # only v1 is in the catalog but the service actually does support # v2. But the endpoint needs the project_id. service_type = self._service_type_manager.get_service_type( - service_type) + service_type + ) if ( value and self.config.get('profile') == 'rackspace' @@ -513,7 +569,8 @@ def get_endpoint(self, service_type): return value def get_endpoint_from_catalog( - self, service_type, interface=None, region_name=None): + self, service_type, interface=None, region_name=None + ): """Return the endpoint for a given service as found in the catalog. For values respecting endpoint overrides, see @@ -537,19 +594,26 @@ def get_endpoint_from_catalog( return catalog.url_for( service_type=service_type, interface=interface, - region_name=region_name) + region_name=region_name, + ) except keystoneauth1.exceptions.catalog.EndpointNotFound: return None def get_connect_retries(self, service_type): - return self._get_config('connect_retries', service_type, - fallback_to_unprefixed=True, - converter=int) + return self._get_config( + 'connect_retries', + service_type, + fallback_to_unprefixed=True, + converter=int, + ) def get_status_code_retries(self, service_type): - return self._get_config('status_code_retries', service_type, - fallback_to_unprefixed=True, - converter=int) + return self._get_config( + 'status_code_retries', + service_type, + fallback_to_unprefixed=True, + converter=int, + ) @property def prefer_ipv6(self): @@ -612,14 +676,16 @@ def insert_user_agent(self): desirable. """ self._keystone_session.additional_user_agent.append( - ('openstacksdk', openstack_version.__version__)) + ('openstacksdk', openstack_version.__version__) + ) def get_session(self): """Return a keystoneauth session based on the auth credentials.""" if self._keystone_session is None: if not self._auth: raise exceptions.ConfigException( - "Problem with auth parameters") + "Problem with auth parameters" + ) (verify, cert) = self.get_requests_verify_args() # Turn off urllib3 warnings about insecure certs if we have # explicitly configured requests to tell it we do not want @@ -627,7 +693,8 @@ def get_session(self): if not verify: self.log.debug( "Turning off SSL warnings for {full_name}" - " since verify=False".format(full_name=self.full_name)) + " since verify=False".format(full_name=self.full_name) + ) requestsexceptions.squelch_warnings(insecure_requests=not verify) self._keystone_session = self._session_constructor( auth=self._auth, @@ -635,7 +702,8 @@ def get_session(self): cert=cert, timeout=self.config.get('api_timeout'), collect_timing=self.config.get('timing'), - discovery_cache=self._discovery_cache) + discovery_cache=self._discovery_cache, + ) self.insert_user_agent() # Using old keystoneauth with new os-client-config fails if # we pass in app_name and app_version. Those are not essential, @@ -683,15 +751,20 @@ def _get_version_request(self, service_type, version): default_microversion = self.get_default_microversion(service_type) implied_microversion = _get_implied_microversion(version) - if (implied_microversion and default_microversion - and implied_microversion != default_microversion): + if ( + implied_microversion + and default_microversion + and implied_microversion != default_microversion + ): raise exceptions.ConfigException( "default_microversion of {default_microversion} was given" " for {service_type}, but api_version looks like a" " microversion as well. Please set api_version to just the" " desired major version, or omit default_microversion".format( default_microversion=default_microversion, - service_type=service_type)) + service_type=service_type, + ) + ) if implied_microversion: default_microversion = implied_microversion # If we're inferring a microversion, don't pass the whole @@ -715,7 +788,8 @@ def get_all_version_data(self, service_type): ) region_versions = versions.get(region_name, {}) interface_versions = region_versions.get( - self.get_interface(service_type), {}) + self.get_interface(service_type), {} + ) return interface_versions.get(service_type, []) def _get_endpoint_from_catalog(self, service_type, constructor): @@ -729,8 +803,7 @@ def _get_endpoint_from_catalog(self, service_type, constructor): return adapter.get_endpoint() def _get_hardcoded_endpoint(self, service_type, constructor): - endpoint = self._get_endpoint_from_catalog( - service_type, constructor) + endpoint = self._get_endpoint_from_catalog(service_type, constructor) if not endpoint.rstrip().rsplit('/')[-1] == 'v2.0': if not endpoint.endswith('/'): endpoint += '/' @@ -738,9 +811,8 @@ def _get_hardcoded_endpoint(self, service_type, constructor): return endpoint def get_session_client( - self, service_type, version=None, - constructor=proxy.Proxy, - **kwargs): + self, service_type, version=None, constructor=proxy.Proxy, **kwargs + ): """Return a prepped keystoneauth Adapter for a given service. This is useful for making direct requests calls against a @@ -757,23 +829,28 @@ def get_session_client( version_request = self._get_version_request(service_type, version) kwargs.setdefault('region_name', self.get_region_name(service_type)) - kwargs.setdefault('connect_retries', - self.get_connect_retries(service_type)) - kwargs.setdefault('status_code_retries', - self.get_status_code_retries(service_type)) + kwargs.setdefault( + 'connect_retries', self.get_connect_retries(service_type) + ) + kwargs.setdefault( + 'status_code_retries', self.get_status_code_retries(service_type) + ) kwargs.setdefault('statsd_prefix', self.get_statsd_prefix()) kwargs.setdefault('statsd_client', self.get_statsd_client()) kwargs.setdefault('prometheus_counter', self.get_prometheus_counter()) kwargs.setdefault( - 'prometheus_histogram', self.get_prometheus_histogram()) + 'prometheus_histogram', self.get_prometheus_histogram() + ) kwargs.setdefault('influxdb_config', self._influxdb_config) kwargs.setdefault('influxdb_client', self.get_influxdb_client()) endpoint_override = self.get_endpoint(service_type) version = version_request.version min_api_version = ( - kwargs.pop('min_version', None) or version_request.min_api_version) + kwargs.pop('min_version', None) or version_request.min_api_version + ) max_api_version = ( - kwargs.pop('max_version', None) or version_request.max_api_version) + kwargs.pop('max_version', None) or version_request.max_api_version + ) # Older neutron has inaccessible discovery document. Nobody noticed # because neutronclient hard-codes an append of v2.0. YAY! @@ -784,7 +861,8 @@ def get_session_client( max_api_version = None if endpoint_override is None: endpoint_override = self._get_hardcoded_endpoint( - service_type, constructor) + service_type, constructor + ) client = constructor( session=self.get_session(), @@ -798,14 +876,15 @@ def get_session_client( default_microversion=version_request.default_microversion, rate_limit=self.get_rate_limit(service_type), concurrency=self.get_concurrency(service_type), - **kwargs) + **kwargs + ) if version_request.default_microversion: default_microversion = version_request.default_microversion info = client.get_endpoint_data() if not discover.version_between( - info.min_microversion, - info.max_microversion, - default_microversion + info.min_microversion, + info.max_microversion, + default_microversion, ): if self.get_default_microversion(service_type): raise exceptions.ConfigException( @@ -816,9 +895,13 @@ def get_session_client( service_type=service_type, default_microversion=default_microversion, min_microversion=discover.version_to_string( - info.min_microversion), + info.min_microversion + ), max_microversion=discover.version_to_string( - info.max_microversion))) + info.max_microversion + ), + ) + ) else: raise exceptions.ConfigException( "A default microversion for service {service_type} of" @@ -836,13 +919,18 @@ def get_session_client( api_version=self.get_api_version(service_type), default_microversion=default_microversion, min_microversion=discover.version_to_string( - info.min_microversion), + info.min_microversion + ), max_microversion=discover.version_to_string( - info.max_microversion))) + info.max_microversion + ), + ) + ) return client def get_session_endpoint( - self, service_type, min_version=None, max_version=None): + self, service_type, min_version=None, max_version=None + ): """Return the endpoint from config or the catalog. If a configuration lists an explicit endpoint for a service, @@ -934,38 +1022,50 @@ def requires_floating_ip(self): def get_external_networks(self): """Get list of network names for external networks.""" return [ - net['name'] for net in self.config.get('networks', []) - if net['routes_externally']] + net['name'] + for net in self.config.get('networks', []) + if net['routes_externally'] + ] def get_external_ipv4_networks(self): """Get list of network names for external IPv4 networks.""" return [ - net['name'] for net in self.config.get('networks', []) - if net['routes_ipv4_externally']] + net['name'] + for net in self.config.get('networks', []) + if net['routes_ipv4_externally'] + ] def get_external_ipv6_networks(self): """Get list of network names for external IPv6 networks.""" return [ - net['name'] for net in self.config.get('networks', []) - if net['routes_ipv6_externally']] + net['name'] + for net in self.config.get('networks', []) + if net['routes_ipv6_externally'] + ] def get_internal_networks(self): """Get list of network names for internal networks.""" return [ - net['name'] for net in self.config.get('networks', []) - if not net['routes_externally']] + net['name'] + for net in self.config.get('networks', []) + if not net['routes_externally'] + ] def get_internal_ipv4_networks(self): """Get list of network names for internal IPv4 networks.""" return [ - net['name'] for net in self.config.get('networks', []) - if not net['routes_ipv4_externally']] + net['name'] + for net in self.config.get('networks', []) + if not net['routes_ipv4_externally'] + ] def get_internal_ipv6_networks(self): """Get list of network names for internal IPv6 networks.""" return [ - net['name'] for net in self.config.get('networks', []) - if not net['routes_ipv6_externally']] + net['name'] + for net in self.config.get('networks', []) + if not net['routes_ipv6_externally'] + ] def get_default_network(self): """Get network used for default interactions.""" @@ -999,8 +1099,8 @@ def _get_extra_config(self, key, defaults=None): if not key: return defaults return _util.merge_clouds( - defaults, - _util.normalize_keys(self._extra_config.get(key, {}))) + defaults, _util.normalize_keys(self._extra_config.get(key, {})) + ) def get_client_config(self, name=None, defaults=None): """Get config settings for a named client. @@ -1020,25 +1120,29 @@ def get_client_config(self, name=None, defaults=None): client section and the defaults. """ return self._get_extra_config( - name, self._get_extra_config('client', defaults)) + name, self._get_extra_config('client', defaults) + ) def get_password_callback(self): return self._password_callback def get_rate_limit(self, service_type=None): return self._get_service_config( - 'rate_limit', service_type=service_type) + 'rate_limit', service_type=service_type + ) def get_concurrency(self, service_type=None): return self._get_service_config( - 'concurrency', service_type=service_type) + 'concurrency', service_type=service_type + ) def get_statsd_client(self): if not statsd: if self._statsd_host: self.log.warning( 'StatsD python library is not available. ' - 'Reporting disabled') + 'Reporting disabled' + ) return None statsd_args = {} if self._statsd_host: @@ -1075,7 +1179,10 @@ def get_prometheus_histogram(self): 'openstack_http_response_time', 'Time taken for an http response to an OpenStack service', labelnames=[ - 'method', 'endpoint', 'service_type', 'status_code' + 'method', + 'endpoint', + 'service_type', + 'status_code', ], registry=registry, ) @@ -1092,7 +1199,10 @@ def get_prometheus_counter(self): 'openstack_http_requests', 'Number of HTTP requests made to an OpenStack service', labelnames=[ - 'method', 'endpoint', 'service_type', 'status_code' + 'method', + 'endpoint', + 'service_type', + 'status_code', ], registry=registry, ) @@ -1103,7 +1213,8 @@ def has_service(self, service_type): service_type = service_type.lower().replace('-', '_') key = 'has_{service_type}'.format(service_type=service_type) return self.config.get( - key, self._service_type_manager.is_official(service_type)) + key, self._service_type_manager.is_official(service_type) + ) def disable_service(self, service_type, reason=None): _disable_service(self.config, service_type, reason=reason) @@ -1140,6 +1251,8 @@ def get_influxdb_client(self): except Exception: self.log.warning('Cannot establish connection to InfluxDB') else: - self.log.warning('InfluxDB configuration is present, ' - 'but no client library is found.') + self.log.warning( + 'InfluxDB configuration is present, ' + 'but no client library is found.' + ) return None diff --git a/openstack/config/defaults.py b/openstack/config/defaults.py index 1e7c7f4a0..4fa5637e4 100644 --- a/openstack/config/defaults.py +++ b/openstack/config/defaults.py @@ -17,7 +17,8 @@ import threading _json_path = os.path.join( - os.path.dirname(os.path.realpath(__file__)), 'defaults.json') + os.path.dirname(os.path.realpath(__file__)), 'defaults.json' +) _defaults = None _defaults_lock = threading.Lock() diff --git a/openstack/config/loader.py b/openstack/config/loader.py index dfddce046..c4f37c33a 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -46,19 +46,23 @@ # see https://snapcraft.io/docs/environment-variables SNAP_REAL_HOME = os.getenv('SNAP_REAL_HOME') if SNAP_REAL_HOME: - UNIX_CONFIG_HOME = os.path.join(os.path.join(SNAP_REAL_HOME, '.config'), - 'openstack') + UNIX_CONFIG_HOME = os.path.join( + os.path.join(SNAP_REAL_HOME, '.config'), 'openstack' + ) else: UNIX_CONFIG_HOME = os.path.join( - os.path.expanduser(os.path.join('~', '.config')), 'openstack') + os.path.expanduser(os.path.join('~', '.config')), 'openstack' + ) UNIX_SITE_CONFIG_HOME = '/etc/openstack' SITE_CONFIG_HOME = APPDIRS.site_config_dir CONFIG_SEARCH_PATH = [ os.getcwd(), - CONFIG_HOME, UNIX_CONFIG_HOME, - SITE_CONFIG_HOME, UNIX_SITE_CONFIG_HOME + CONFIG_HOME, + UNIX_CONFIG_HOME, + SITE_CONFIG_HOME, + UNIX_SITE_CONFIG_HOME, ] YAML_SUFFIXES = ('.yaml', '.yml') JSON_SUFFIXES = ('.json',) @@ -134,8 +138,8 @@ def _fix_argv(argv): "The following options were given: '{options}' which contain" " duplicates except that one has _ and one has -. There is" " no sane way for us to know what you're doing. Remove the" - " duplicate option and try again".format( - options=','.join(overlap))) + " duplicate option and try again".format(options=','.join(overlap)) + ) class OpenStackConfig: @@ -146,14 +150,25 @@ class OpenStackConfig: _cloud_region_class = cloud_region.CloudRegion _defaults_module = defaults - def __init__(self, config_files=None, vendor_files=None, - override_defaults=None, force_ipv4=None, - envvar_prefix=None, secure_files=None, - pw_func=None, session_constructor=None, - app_name=None, app_version=None, - load_yaml_config=True, load_envvars=True, - statsd_host=None, statsd_port=None, - statsd_prefix=None, influxdb_config=None): + def __init__( + self, + config_files=None, + vendor_files=None, + override_defaults=None, + force_ipv4=None, + envvar_prefix=None, + secure_files=None, + pw_func=None, + session_constructor=None, + app_name=None, + app_version=None, + load_yaml_config=True, + load_envvars=True, + statsd_host=None, + statsd_port=None, + statsd_prefix=None, + influxdb_config=None, + ): self.log = _log.setup_logging('openstack.config') self._session_constructor = session_constructor self._app_name = app_name @@ -196,7 +211,8 @@ def __init__(self, config_files=None, vendor_files=None, _, secure_config = self._load_secure_file() if secure_config: self.cloud_config = _util.merge_clouds( - self.cloud_config, secure_config) + self.cloud_config, secure_config + ) if not self.cloud_config: self.cloud_config = {'clouds': {}} @@ -217,14 +233,20 @@ def __init__(self, config_files=None, vendor_files=None, # Get the backwards compat value prefer_ipv6 = get_boolean( self._get_envvar( - 'OS_PREFER_IPV6', client_config.get( - 'prefer_ipv6', client_config.get( - 'prefer-ipv6', True)))) + 'OS_PREFER_IPV6', + client_config.get( + 'prefer_ipv6', client_config.get('prefer-ipv6', True) + ), + ) + ) force_ipv4 = get_boolean( self._get_envvar( - 'OS_FORCE_IPV4', client_config.get( - 'force_ipv4', client_config.get( - 'broken-ipv6', False)))) + 'OS_FORCE_IPV4', + client_config.get( + 'force_ipv4', client_config.get('broken-ipv6', False) + ), + ) + ) self.force_ipv4 = force_ipv4 if not prefer_ipv6: @@ -239,8 +261,10 @@ def __init__(self, config_files=None, vendor_files=None, '"{0}" defines a cloud named "{1}", but' ' OS_CLOUD_NAME is also set to "{1}". Please rename' ' either your environment based cloud, or one of your' - ' file-based clouds.'.format(self.config_filename, - self.envvar_key)) + ' file-based clouds.'.format( + self.config_filename, self.envvar_key + ) + ) self.default_cloud = self._get_envvar('OS_CLOUD') @@ -259,15 +283,15 @@ def __init__(self, config_files=None, vendor_files=None, # clouds.yaml. # The next/iter thing is for python3 compat where dict.keys # returns an iterator but in python2 it's a list. - self.default_cloud = next(iter( - self.cloud_config['clouds'].keys())) + self.default_cloud = next( + iter(self.cloud_config['clouds'].keys()) + ) # Finally, fall through and make a cloud that starts with defaults # because we need somewhere to put arguments, and there are neither # config files or env vars if not self.cloud_config['clouds']: - self.cloud_config = dict( - clouds=dict(defaults=dict(self.defaults))) + self.cloud_config = dict(clouds=dict(defaults=dict(self.defaults))) self.default_cloud = 'defaults' self._cache_auth = False @@ -281,13 +305,15 @@ def __init__(self, config_files=None, vendor_files=None, cache_settings = _util.normalize_keys(self.cloud_config['cache']) self._cache_auth = get_boolean( - cache_settings.get('auth', self._cache_auth)) + cache_settings.get('auth', self._cache_auth) + ) # expiration_time used to be 'max_age' but the dogpile setting # is expiration_time. Support max_age for backwards compat. self._cache_expiration_time = cache_settings.get( - 'expiration_time', cache_settings.get( - 'max_age', self._cache_expiration_time)) + 'expiration_time', + cache_settings.get('max_age', self._cache_expiration_time), + ) # If cache class is given, use that. If not, but if cache time # is given, default to memory. Otherwise, default to nothing. @@ -295,14 +321,18 @@ def __init__(self, config_files=None, vendor_files=None, if self._cache_expiration_time: self._cache_class = 'dogpile.cache.memory' self._cache_class = self.cloud_config['cache'].get( - 'class', self._cache_class) + 'class', self._cache_class + ) self._cache_path = os.path.expanduser( - cache_settings.get('path', self._cache_path)) + cache_settings.get('path', self._cache_path) + ) self._cache_arguments = cache_settings.get( - 'arguments', self._cache_arguments) + 'arguments', self._cache_arguments + ) self._cache_expirations = cache_settings.get( - 'expiration', self._cache_expirations) + 'expiration', self._cache_expirations + ) if load_yaml_config: metrics_config = self.cloud_config.get('metrics', {}) @@ -326,12 +356,21 @@ def __init__(self, config_files=None, vendor_files=None, use_udp = use_udp.lower() in ('true', 'yes', '1') elif not isinstance(use_udp, bool): use_udp = False - self.log.warning('InfluxDB.use_udp value type is not ' - 'supported. Use one of ' - '[true|false|yes|no|1|0]') + self.log.warning( + 'InfluxDB.use_udp value type is not ' + 'supported. Use one of ' + '[true|false|yes|no|1|0]' + ) config['use_udp'] = use_udp - for key in ['host', 'port', 'username', 'password', 'database', - 'measurement', 'timeout']: + for key in [ + 'host', + 'port', + 'username', + 'password', + 'database', + 'measurement', + 'timeout', + ]: if key in influxdb_config: config[key] = influxdb_config[key] self._influxdb_config = config @@ -357,20 +396,28 @@ def _get_os_environ(self, envvar_prefix=None): if not envvar_prefix: # This makes the or below be OS_ or OS_ which is a no-op envvar_prefix = 'OS_' - environkeys = [k for k in os.environ.keys() - if (k.startswith('OS_') or k.startswith(envvar_prefix)) - and not k.startswith('OS_TEST') # infra CI var - and not k.startswith('OS_STD') # oslotest var - and not k.startswith('OS_LOG') # oslotest var - ] + environkeys = [ + k + for k in os.environ.keys() + if (k.startswith('OS_') or k.startswith(envvar_prefix)) + and not k.startswith('OS_TEST') # infra CI var + and not k.startswith('OS_STD') # oslotest var + and not k.startswith('OS_LOG') # oslotest var + ] for k in environkeys: newkey = k.split('_', 1)[-1].lower() ret[newkey] = os.environ[k] # If the only environ keys are selectors or behavior modification, # don't return anything - selectors = set([ - 'OS_CLOUD', 'OS_REGION_NAME', - 'OS_CLIENT_CONFIG_FILE', 'OS_CLIENT_SECURE_FILE', 'OS_CLOUD_NAME']) + selectors = set( + [ + 'OS_CLOUD', + 'OS_REGION_NAME', + 'OS_CLIENT_CONFIG_FILE', + 'OS_CLIENT_SECURE_FILE', + 'OS_CLOUD_NAME', + ] + ) if set(environkeys) - selectors: return ret return None @@ -391,8 +438,8 @@ def get_extra_config(self, key, defaults=None): if not key: return defaults return _util.merge_clouds( - defaults, - _util.normalize_keys(self.cloud_config.get(key, {}))) + defaults, _util.normalize_keys(self.cloud_config.get(key, {})) + ) def _load_config_file(self): return self._load_yaml_json_file(self._config_files) @@ -427,10 +474,12 @@ def _expand_regions(self, regions): for region in regions: if isinstance(region, dict): # i.e. must have name key, and only name,values keys - if 'name' not in region or \ - not {'name', 'values'} >= set(region): + if 'name' not in region or not {'name', 'values'} >= set( + region + ): raise exceptions.ConfigException( - 'Invalid region entry at: %s' % region) + 'Invalid region entry at: %s' % region + ) if 'values' not in region: region['values'] = {} ret.append(copy.deepcopy(region)) @@ -460,7 +509,8 @@ def _get_known_regions(self, cloud): warnings.warn( "Comma separated lists in region_name are deprecated." " Please use a yaml list in the regions" - " parameter in {0} instead.".format(self.config_filename)) + " parameter in {0} instead.".format(self.config_filename) + ) return self._expand_regions(regions) else: # crappit. we don't have a region defined. @@ -495,7 +545,9 @@ def _get_region(self, cloud=None, region_name=''): ' region names are case sensitive.'.format( region_name=region_name, region_list=','.join([r['name'] for r in regions]), - cloud=cloud)) + cloud=cloud, + ) + ) def get_cloud_names(self): return self.cloud_config['clouds'].keys() @@ -506,8 +558,8 @@ def _get_base_cloud_config(self, name, profile=None): # Only validate cloud name if one was given if name and name not in self.cloud_config['clouds']: raise exceptions.ConfigException( - "Cloud {name} was not found.".format( - name=name)) + "Cloud {name} was not found.".format(name=name) + ) our_cloud = self.cloud_config['clouds'].get(name, dict()) if profile: @@ -536,11 +588,15 @@ def _expand_vendor_profile(self, name, cloud, our_cloud): warnings.warn( "{0} uses the keyword 'cloud' to reference a known " "vendor profile. This has been deprecated in favor of the " - "'profile' keyword.".format(self.config_filename)) + "'profile' keyword.".format(self.config_filename) + ) vendor_filename, vendor_file = self._load_vendor_file() - if (vendor_file and 'public-clouds' in vendor_file - and profile_name in vendor_file['public-clouds']): + if ( + vendor_file + and 'public-clouds' in vendor_file + and profile_name in vendor_file['public-clouds'] + ): _auth_update(cloud, vendor_file['public-clouds'][profile_name]) else: profile_data = vendors.get_profile(profile_name) @@ -555,23 +611,31 @@ def _expand_vendor_profile(self, name, cloud, our_cloud): if status == 'deprecated': warnings.warn( "{profile_name} is deprecated: {message}".format( - profile_name=profile_name, message=message)) + profile_name=profile_name, message=message + ) + ) elif status == 'shutdown': raise exceptions.ConfigException( "{profile_name} references a cloud that no longer" " exists: {message}".format( - profile_name=profile_name, message=message)) + profile_name=profile_name, message=message + ) + ) _auth_update(cloud, profile_data) else: # Can't find the requested vendor config, go about business - warnings.warn("Couldn't find the vendor profile '{0}', for" - " the cloud '{1}'".format(profile_name, - name)) + warnings.warn( + "Couldn't find the vendor profile '{0}', for" + " the cloud '{1}'".format(profile_name, name) + ) def _project_scoped(self, cloud): - return ('project_id' in cloud or 'project_name' in cloud - or 'project_id' in cloud['auth'] - or 'project_name' in cloud['auth']) + return ( + 'project_id' in cloud + or 'project_name' in cloud + or 'project_id' in cloud['auth'] + or 'project_name' in cloud['auth'] + ) def _validate_networks(self, networks, key): value = None @@ -580,9 +644,9 @@ def _validate_networks(self, networks, key): raise exceptions.ConfigException( "Duplicate network entries for {key}: {net1} and {net2}." " Only one network can be flagged with {key}".format( - key=key, - net1=value['name'], - net2=net['name'])) + key=key, net1=value['name'], net2=net['name'] + ) + ) if not value and net[key]: value = net @@ -595,7 +659,8 @@ def _fix_backwards_networks(self, cloud): name = net.get('name') if not name: raise exceptions.ConfigException( - 'Entry in network list is missing required field "name".') + 'Entry in network list is missing required field "name".' + ) network = dict( name=name, routes_externally=get_boolean(net.get('routes_externally')), @@ -605,12 +670,12 @@ def _fix_backwards_networks(self, cloud): ) # routes_ipv4_externally defaults to the value of routes_externally network['routes_ipv4_externally'] = get_boolean( - net.get( - 'routes_ipv4_externally', network['routes_externally'])) + net.get('routes_ipv4_externally', network['routes_externally']) + ) # routes_ipv6_externally defaults to the value of routes_externally network['routes_ipv6_externally'] = get_boolean( - net.get( - 'routes_ipv6_externally', network['routes_externally'])) + net.get('routes_ipv6_externally', network['routes_externally']) + ) networks.append(network) for key in ('external_network', 'internal_network'): @@ -619,18 +684,24 @@ def _fix_backwards_networks(self, cloud): raise exceptions.ConfigException( "Both {key} and networks were specified in the config." " Please remove {key} from the config and use the network" - " list to configure network behavior.".format(key=key)) + " list to configure network behavior.".format(key=key) + ) if key in cloud: warnings.warn( "{key} is deprecated. Please replace with an entry in" " a dict inside of the networks list with name: {name}" " and routes_externally: {external}".format( - key=key, name=cloud[key], external=external)) - networks.append(dict( - name=cloud[key], - routes_externally=external, - nat_destination=not external, - default_interface=external)) + key=key, name=cloud[key], external=external + ) + ) + networks.append( + dict( + name=cloud[key], + routes_externally=external, + nat_destination=not external, + default_interface=external, + ) + ) # Validate that we don't have duplicates self._validate_networks(networks, 'nat_destination') @@ -668,7 +739,9 @@ def _fix_backwards_project(self, cloud): 'user_domain_name': ('user_domain_name', 'user-domain-name'), 'project_domain_id': ('project_domain_id', 'project-domain-id'), 'project_domain_name': ( - 'project_domain_name', 'project-domain-name'), + 'project_domain_name', + 'project-domain-name', + ), 'token': ('auth-token', 'auth_token', 'token'), } if cloud.get('auth_type', None) == 'v2password': @@ -676,14 +749,30 @@ def _fix_backwards_project(self, cloud): # clouds. That's fine - we need to map settings in the opposite # direction mappings['tenant_id'] = ( - 'project_id', 'project-id', 'tenant_id', 'tenant-id') + 'project_id', + 'project-id', + 'tenant_id', + 'tenant-id', + ) mappings['tenant_name'] = ( - 'project_name', 'project-name', 'tenant_name', 'tenant-name') + 'project_name', + 'project-name', + 'tenant_name', + 'tenant-name', + ) else: mappings['project_id'] = ( - 'tenant_id', 'tenant-id', 'project_id', 'project-id') + 'tenant_id', + 'tenant-id', + 'project_id', + 'project-id', + ) mappings['project_name'] = ( - 'tenant_name', 'tenant-name', 'project_name', 'project-name') + 'tenant_name', + 'tenant-name', + 'project_name', + 'project-name', + ) for target_key, possible_values in mappings.items(): target = None for key in possible_values: @@ -747,7 +836,8 @@ def register_argparse_arguments(self, parser, argv, service_keys=None): '--os-cloud', metavar='', default=self._get_envvar('OS_CLOUD', None), - help='Named cloud to connect to') + help='Named cloud to connect to', + ) # we need to peek to see if timeout was actually passed, since # the keystoneauth declaration of it has a default, which means @@ -782,7 +872,8 @@ def register_argparse_arguments(self, parser, argv, service_keys=None): try: loading.register_auth_argparse_arguments( - parser, argv, default=default_auth_type) + parser, argv, default=default_auth_type + ) except Exception: # Hidiing the keystoneauth exception because we're not actually # loading the auth plugin at this point, so the error message @@ -793,7 +884,9 @@ def register_argparse_arguments(self, parser, argv, service_keys=None): "An invalid auth-type was specified: {auth_type}." " Valid choices are: {plugin_names}.".format( auth_type=options.os_auth_type, - plugin_names=",".join(plugin_names))) + plugin_names=",".join(plugin_names), + ) + ) if service_keys: primary_service = service_keys[0] @@ -801,15 +894,19 @@ def register_argparse_arguments(self, parser, argv, service_keys=None): primary_service = None loading.register_session_argparse_arguments(parser) adapter.register_adapter_argparse_arguments( - parser, service_type=primary_service) + parser, service_type=primary_service + ) for service_key in service_keys: # legacy clients have un-prefixed api-version options parser.add_argument( '--{service_key}-api-version'.format( - service_key=service_key.replace('_', '-')), - help=argparse_mod.SUPPRESS) + service_key=service_key.replace('_', '-') + ), + help=argparse_mod.SUPPRESS, + ) adapter.register_service_adapter_argparse_arguments( - parser, service_type=service_key) + parser, service_type=service_key + ) # Backwards compat options for legacy clients parser.add_argument('--http-timeout', help=argparse_mod.SUPPRESS) @@ -837,7 +934,8 @@ def _fix_backwards_api_timeout(self, cloud): service_timeout = None for key in cloud.keys(): if key.endswith('timeout') and not ( - key == 'timeout' or key == 'api_timeout'): + key == 'timeout' or key == 'api_timeout' + ): service_timeout = cloud[key] else: new_cloud[key] = cloud[key] @@ -857,9 +955,11 @@ def get_all(self): for cloud in self.get_cloud_names(): for region in self._get_regions(cloud): if region: - clouds.append(self.get_one( - cloud, region_name=region['name'])) + clouds.append( + self.get_one(cloud, region_name=region['name']) + ) return clouds + # TODO(mordred) Backwards compat for OSC transition get_all_clouds = get_all @@ -904,8 +1004,9 @@ def _find_winning_auth_value(self, opt, config): if opt_name in config: return config[opt_name] else: - deprecated = getattr(opt, 'deprecated', getattr( - opt, 'deprecated_opts', [])) + deprecated = getattr( + opt, 'deprecated', getattr(opt, 'deprecated_opts', []) + ) for d_opt in deprecated: d_opt_name = d_opt.name.replace('-', '_') if d_opt_name in config: @@ -1027,9 +1128,9 @@ def _validate_auth_correctly(self, config, loader): def option_prompt(self, config, p_opt): """Prompt user for option that requires a value""" if ( - getattr(p_opt, 'prompt', None) is not None - and p_opt.dest not in config['auth'] - and self._pw_callback is not None + getattr(p_opt, 'prompt', None) is not None + and p_opt.dest not in config['auth'] + and self._pw_callback is not None ): config['auth'][p_opt.dest] = self._pw_callback(p_opt.prompt) return config @@ -1046,8 +1147,7 @@ def _clean_up_after_ourselves(self, config, p_opt, 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) + config['auth'][p_opt.name.replace('-', '_')] = winning_value else: config['auth'][p_opt.dest] = winning_value return config @@ -1056,9 +1156,11 @@ def magic_fixes(self, config): """Perform the set of magic argument fixups""" # Infer token plugin if a token was given - if (('auth' in config and 'token' in config['auth']) - or ('auth_token' in config and config['auth_token']) - or ('token' in config and config['token'])): + if ( + ('auth' in config and 'token' in config['auth']) + or ('auth_token' in config and config['auth_token']) + or ('token' in config and config['token']) + ): config.setdefault('token', config.pop('auth_token', None)) # Infer passcode if it was given separately @@ -1094,12 +1196,12 @@ def magic_fixes(self, config): # more generalized if 'auth' in config and 'auth_url' in config['auth']: config['auth']['auth_url'] = config['auth']['auth_url'].format( - **config) + **config + ) return config - def get_one( - self, cloud=None, validate=True, argparse=None, **kwargs): + def get_one(self, cloud=None, validate=True, argparse=None, **kwargs): """Retrieve a single CloudRegion and merge additional options :param string cloud: @@ -1217,15 +1319,12 @@ def get_one( statsd_prefix=statsd_prefix, influxdb_config=influxdb_config, ) + # TODO(mordred) Backwards compat for OSC transition get_one_cloud = get_one def get_one_cloud_osc( - self, - cloud=None, - validate=True, - argparse=None, - **kwargs + self, cloud=None, validate=True, argparse=None, **kwargs ): """Retrieve a single CloudRegion and merge additional options @@ -1359,10 +1458,10 @@ def set_one_cloud(config_file, cloud, set_config=None): if len(sys.argv) == 1: print_cloud = True elif len(sys.argv) == 3 and ( - sys.argv[1] == cloud.name and sys.argv[2] == cloud.region): + sys.argv[1] == cloud.name and sys.argv[2] == cloud.region + ): print_cloud = True - elif len(sys.argv) == 2 and ( - sys.argv[1] == cloud.name): + elif len(sys.argv) == 2 and (sys.argv[1] == cloud.name): print_cloud = True if print_cloud: diff --git a/openstack/config/vendors/__init__.py b/openstack/config/vendors/__init__.py index 0c0bbf80a..95c5ccb30 100644 --- a/openstack/config/vendors/__init__.py +++ b/openstack/config/vendors/__init__.py @@ -61,7 +61,9 @@ def get_profile(profile_name): " {status_code} {reason}".format( profile_name=profile_name, status_code=response.status_code, - reason=response.reason)) + reason=response.reason, + ) + ) vendor_defaults[profile_name] = None return vendor_data = response.json() @@ -69,8 +71,8 @@ def get_profile(profile_name): # Merge named and url cloud config, but make named config override the # config from the cloud so that we can supply local overrides if needed. profile = _util.merge_clouds( - vendor_data['profile'], - vendor_defaults.get(name, {})) + vendor_data['profile'], vendor_defaults.get(name, {}) + ) # If there is (or was) a profile listed in a named config profile, it # might still be here. We just merged in content from a URL though, so # pop the key to prevent doing it again in the future. diff --git a/openstack/connection.py b/openstack/connection.py index 588f7ff89..3f945a132 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -220,7 +220,8 @@ if requestsexceptions.SubjectAltNameWarning: warnings.filterwarnings( - 'ignore', category=requestsexceptions.SubjectAltNameWarning) + 'ignore', category=requestsexceptions.SubjectAltNameWarning + ) _logger = _log.setup_logging('openstack') @@ -249,7 +250,8 @@ def from_config(cloud=None, config=None, options=None, **kwargs): config = kwargs.pop('cloud_config', config) if config is None: config = _config.OpenStackConfig().get_one( - cloud=cloud, argparse=options, **kwargs) + cloud=cloud, argparse=options, **kwargs + ) return Connection(config=config) @@ -274,20 +276,25 @@ class Connection( _security_group.SecurityGroupCloudMixin, _shared_file_system.SharedFileSystemCloudMixin, ): - - def __init__(self, cloud=None, config=None, session=None, - app_name=None, app_version=None, - extra_services=None, - strict=False, - use_direct_get=False, - task_manager=None, - rate_limit=None, - oslo_conf=None, - service_types=None, - global_request_id=None, - strict_proxies=False, - pool_executor=None, - **kwargs): + def __init__( + self, + cloud=None, + config=None, + session=None, + app_name=None, + app_version=None, + extra_services=None, + strict=False, + use_direct_get=False, + task_manager=None, + rate_limit=None, + oslo_conf=None, + service_types=None, + global_request_id=None, + strict_proxies=False, + pool_executor=None, + **kwargs + ): """Create a connection to a cloud. A connection needs information about how to connect, how to @@ -373,24 +380,32 @@ def __init__(self, cloud=None, config=None, session=None, if not self.config: if oslo_conf: self.config = cloud_region.from_conf( - oslo_conf, session=session, app_name=app_name, - app_version=app_version, service_types=service_types) + oslo_conf, + session=session, + app_name=app_name, + app_version=app_version, + service_types=service_types, + ) elif session: self.config = cloud_region.from_session( session=session, - app_name=app_name, app_version=app_version, + app_name=app_name, + app_version=app_version, load_yaml_config=False, load_envvars=False, rate_limit=rate_limit, - **kwargs) + **kwargs + ) else: self.config = _config.get_cloud_region( cloud=cloud, - app_name=app_name, app_version=app_version, + app_name=app_name, + app_version=app_version, load_yaml_config=cloud is not None, load_envvars=cloud is not None, rate_limit=rate_limit, - **kwargs) + **kwargs + ) self._session = None self._proxies = {} @@ -440,19 +455,25 @@ def __init__(self, cloud=None, config=None, session=None, hook = ep.load() hook(self) except ValueError: - self.log.warning('Hook should be in the entrypoint ' - 'module:attribute format') + self.log.warning( + 'Hook should be in the entrypoint ' + 'module:attribute format' + ) except (ImportError, TypeError, AttributeError) as e: - self.log.warning('Configured hook %s cannot be executed: %s', - vendor_hook, e) + self.log.warning( + 'Configured hook %s cannot be executed: %s', vendor_hook, e + ) # Add additional metrics into the configuration according to the # selected connection. We don't want to deal with overall config in the # proxy, just pass required part. - if (self.config._influxdb_config - and 'additional_metric_tags' in self.config.config): - self.config._influxdb_config['additional_metric_tags'] = \ - self.config.config['additional_metric_tags'] + if ( + self.config._influxdb_config + and 'additional_metric_tags' in self.config.config + ): + self.config._influxdb_config[ + 'additional_metric_tags' + ] = self.config.config['additional_metric_tags'] def __del__(self): # try to force release of resources and save authorization @@ -500,7 +521,7 @@ def getter(self): setattr( self.__class__, attr_name.replace('-', '_'), - property(fget=getter) + property(fget=getter), ) self.config.enable_service(service.service_type) @@ -527,7 +548,8 @@ def authorize(self): def _pool_executor(self): if not self.__pool_executor: self.__pool_executor = concurrent.futures.ThreadPoolExecutor( - max_workers=5) + max_workers=5 + ) return self.__pool_executor def close(self): diff --git a/openstack/exceptions.py b/openstack/exceptions.py index ec13dde09..a8f12ef97 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -24,6 +24,7 @@ class SDKException(Exception): """The base exception class for all exceptions this library raises.""" + def __init__(self, message=None, extra_data=None): self.message = self.__class__.__name__ if message is None else message self.extra_data = extra_data @@ -35,6 +36,7 @@ def __init__(self, message=None, extra_data=None): class EndpointNotFound(SDKException): """A mismatch occurred between what the client and server expect.""" + def __init__(self, message=None): super(EndpointNotFound, self).__init__(message) @@ -55,20 +57,25 @@ def __init__(self, message=None): class HttpException(SDKException, _rex.HTTPError): - - def __init__(self, message='Error', response=None, - http_status=None, - details=None, request_id=None): + def __init__( + self, + message='Error', + response=None, + http_status=None, + details=None, + request_id=None, + ): # TODO(shade) Remove http_status parameter and the ability for response # to be None once we're not mocking Session everywhere. if not message: if response is not None: message = "{name}: {code}".format( - name=self.__class__.__name__, - code=response.status_code) + name=self.__class__.__name__, code=response.status_code + ) else: message = "{name}: Unknown error".format( - name=self.__class__.__name__) + name=self.__class__.__name__ + ) # Call directly rather than via super to control parameters SDKException.__init__(self, message=message) @@ -96,7 +103,8 @@ def __str__(self): return self.message if self.url: remote_error = "{source} Error for url: {url}".format( - source=self.source, url=self.url) + source=self.source, url=self.url + ) if self.details: remote_error += ', ' if self.details: @@ -104,31 +112,37 @@ def __str__(self): return "{message}: {remote_error}".format( message=super(HttpException, self).__str__(), - remote_error=remote_error) + remote_error=remote_error, + ) class BadRequestException(HttpException): """HTTP 400 Bad Request.""" + pass class ForbiddenException(HttpException): """HTTP 403 Forbidden Request.""" + pass class ConflictException(HttpException): """HTTP 409 Conflict.""" + pass class PreconditionFailedException(HttpException): """HTTP 412 Precondition Failed.""" + pass class MethodNotSupported(SDKException): """The resource does not support this operation type.""" + def __init__(self, resource, method): # This needs to work with both classes and instances. try: @@ -136,18 +150,23 @@ def __init__(self, resource, method): except AttributeError: name = resource.__class__.__name__ - message = ('The %s method is not supported for %s.%s' % - (method, resource.__module__, name)) + message = 'The %s method is not supported for %s.%s' % ( + method, + resource.__module__, + name, + ) super(MethodNotSupported, self).__init__(message=message) class DuplicateResource(SDKException): """More than one resource exists with that name.""" + pass class ResourceNotFound(HttpException): """No resource exists with that name or id.""" + pass @@ -156,16 +175,19 @@ class ResourceNotFound(HttpException): class ResourceTimeout(SDKException): """Timeout waiting for resource.""" + pass class ResourceFailure(SDKException): """General resource failure.""" + pass class InvalidResourceQuery(SDKException): """Invalid query params for resource.""" + pass @@ -225,8 +247,9 @@ def raise_from_response(response, error_message=None): details = response.text elif response.content and 'text/html' in content_type: # Split the lines, strip whitespace and inline HTML from the response. - details = [re.sub(r'<.+?>', '', i.strip()) - for i in response.text.splitlines()] + details = [ + re.sub(r'<.+?>', '', i.strip()) for i in response.text.splitlines() + ] details = list(set([msg for msg in details if msg])) # Return joined string separated by colons. details = ': '.join(details) @@ -238,8 +261,11 @@ def raise_from_response(response, error_message=None): request_id = response.headers.get('x-openstack-request-id') raise cls( - message=error_message, response=response, details=details, - http_status=http_status, request_id=request_id + message=error_message, + response=response, + details=details, + http_status=http_status, + request_id=request_id, ) @@ -249,6 +275,7 @@ class UnsupportedServiceVersion(Warning): class ArgumentDeprecationWarning(Warning): """A deprecated argument has been provided.""" + pass diff --git a/openstack/fixture/connection.py b/openstack/fixture/connection.py index 52ee4b12e..c3aee3041 100644 --- a/openstack/fixture/connection.py +++ b/openstack/fixture/connection.py @@ -61,7 +61,8 @@ def _get_endpoint_templates(self, service_type, alias=None, v2=False): templates = {} for k, v in self._endpoint_templates.items(): suffix = self._suffixes.get( - alias, self._suffixes.get(service_type, '')) + alias, self._suffixes.get(service_type, '') + ) # For a keystone v2 catalog, we want to list the # versioned endpoint in the catalog, because that's # more likely how those were deployed. @@ -88,10 +89,8 @@ def build_tokens(self): continue service_name = service['project'] ets = self._get_endpoint_templates(service_type) - v3_svc = self.v3_token.add_service( - service_type, name=service_name) - v2_svc = self.v2_token.add_service( - service_type, name=service_name) + v3_svc = self.v3_token.add_service(service_type, name=service_name) + v2_svc = self.v2_token.add_service(service_type, name=service_name) v3_svc.add_standard_endpoints(region='RegionOne', **ets) if service_type == 'identity': ets = self._get_endpoint_templates(service_type, v2=True) diff --git a/openstack/format.py b/openstack/format.py index 7e586709d..3a750faad 100644 --- a/openstack/format.py +++ b/openstack/format.py @@ -12,7 +12,6 @@ class Formatter: - @classmethod def serialize(cls, value): """Return a string representing the formatted value""" @@ -25,7 +24,6 @@ def deserialize(cls, value): class BoolStr(Formatter): - @classmethod def deserialize(cls, value): """Convert a boolean string to a boolean""" @@ -35,8 +33,9 @@ def deserialize(cls, value): elif "false" == expr: return False else: - raise ValueError("Unable to deserialize boolean string: %s" - % value) + raise ValueError( + "Unable to deserialize boolean string: %s" % value + ) @classmethod def serialize(cls, value): @@ -47,5 +46,4 @@ def serialize(cls, value): else: return "false" else: - raise ValueError("Unable to serialize boolean string: %s" - % value) + raise ValueError("Unable to serialize boolean string: %s" % value) diff --git a/openstack/proxy.py b/openstack/proxy.py index 45d94767b..99b0f5e76 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -437,8 +437,7 @@ def _get_connection(self): self, '_connection', getattr(self.session, '_sdk_connection', None) ) - def _get_resource(self, resource_type: Type[T], value, - **attrs) -> T: + def _get_resource(self, resource_type: Type[T], value, **attrs) -> T: """Get a resource object to work on :param resource_type: The type of resource to operate on. This should @@ -484,8 +483,9 @@ def _get_uri_attribute(self, child, parent, name): value = resource.Resource._get_id(parent) return value - def _find(self, resource_type: Type[T], name_or_id, ignore_missing=True, - **attrs) -> Optional[T]: + def _find( + self, resource_type: Type[T], name_or_id, ignore_missing=True, **attrs + ) -> Optional[T]: """Find a resource :param name_or_id: The name or ID of a resource to find. @@ -505,8 +505,9 @@ def _find(self, resource_type: Type[T], name_or_id, ignore_missing=True, ) @_check_resource(strict=False) - def _delete(self, resource_type: Type[T], value, ignore_missing=True, - **attrs): + def _delete( + self, resource_type: Type[T], value, ignore_missing=True, **attrs + ): """Delete a resource :param resource_type: The type of resource to delete. This should @@ -542,8 +543,9 @@ def _delete(self, resource_type: Type[T], value, ignore_missing=True, return rv @_check_resource(strict=False) - def _update(self, resource_type: Type[T], value, base_path=None, - **attrs) -> T: + def _update( + self, resource_type: Type[T], value, base_path=None, **attrs + ) -> T: """Update a resource :param resource_type: The type of resource to update. @@ -591,8 +593,9 @@ def _create(self, resource_type: Type[T], base_path=None, **attrs): res = resource_type.new(connection=conn, **attrs) return res.create(self, base_path=base_path) - def _bulk_create(self, resource_type: Type[T], data, base_path=None - ) -> Generator[T, None, None]: + def _bulk_create( + self, resource_type: Type[T], data, base_path=None + ) -> Generator[T, None, None]: """Create a resource from attributes :param resource_type: The type of resource to create. @@ -614,13 +617,13 @@ def _bulk_create(self, resource_type: Type[T], data, base_path=None @_check_resource(strict=False) def _get( - self, - resource_type: Type[T], - value=None, - requires_id=True, - base_path=None, - skip_cache=False, - **attrs + self, + resource_type: Type[T], + value=None, + requires_id=True, + base_path=None, + skip_cache=False, + **attrs ): """Fetch a resource @@ -657,12 +660,12 @@ def _get( ) def _list( - self, - resource_type: Type[T], - paginated=True, - base_path=None, - jmespath_filters=None, - **attrs + self, + resource_type: Type[T], + paginated=True, + base_path=None, + jmespath_filters=None, + **attrs ) -> Generator[T, None, None]: """List a resource @@ -690,8 +693,7 @@ def _list( the ``resource_type``. """ data = resource_type.list( - self, paginated=paginated, base_path=base_path, - **attrs + self, paginated=paginated, base_path=base_path, **attrs ) if jmespath_filters and isinstance(jmespath_filters, str): @@ -699,8 +701,9 @@ def _list( return data - def _head(self, resource_type: Type[T], value=None, base_path=None, - **attrs): + def _head( + self, resource_type: Type[T], value=None, base_path=None, **attrs + ): """Retrieve a resource's header :param resource_type: The type of resource to retrieve. diff --git a/openstack/resource.py b/openstack/resource.py index fd85daca8..644eaa3bd 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -207,15 +207,14 @@ def __get__(self, instance, owner): def warn_if_deprecated_property(self, value): deprecated = object.__getattribute__(self, 'deprecated') deprecation_reason = object.__getattribute__( - self, 'deprecation_reason', + self, + 'deprecation_reason', ) if value and deprecated: warnings.warn( - "The field %r has been deprecated. %s" % ( - self.name, - deprecation_reason or "Avoid usage." - ), + "The field %r has been deprecated. %s" + % (self.name, deprecation_reason or "Avoid usage."), os_warnings.RemovedFieldWarning, ) return value @@ -1027,9 +1026,7 @@ def _attr_to_dict(self, attr, to_munch): converted = [] for raw in value: if isinstance(raw, Resource): - converted.append( - raw.to_dict(_to_munch=to_munch) - ) + converted.append(raw.to_dict(_to_munch=to_munch)) elif isinstance(raw, dict) and to_munch: converted.append(utils.Munch(raw)) else: @@ -1223,10 +1220,7 @@ def _prepare_request( requires_id = self.requires_id # Conditionally construct arguments for _prepare_request_body - request_kwargs = { - "patch": patch, - "prepend_key": prepend_key - } + request_kwargs = {"patch": patch, "prepend_key": prepend_key} if resource_request_key is not None: request_kwargs['resource_request_key'] = resource_request_key body = self._prepare_request_body(**request_kwargs) @@ -1443,7 +1437,7 @@ def create( resource_request_key=None, resource_response_key=None, microversion=None, - **params + **params, ): """Create a remote resource based on this instance. @@ -1532,8 +1526,7 @@ def create( # fetch the body if it's required but not returned by create fetch_kwargs = {} if resource_response_key is not None: - fetch_kwargs = \ - {'resource_response_key': resource_response_key} + fetch_kwargs = {'resource_response_key': resource_response_key} return self.fetch(session, **fetch_kwargs) return self @@ -1681,7 +1674,8 @@ def fetch( raise exceptions.MethodNotSupported(self, 'fetch') request = self._prepare_request( - requires_id=requires_id, base_path=base_path, + requires_id=requires_id, + base_path=base_path, ) session = self._get_session(session) if microversion is None: @@ -1931,8 +1925,9 @@ def patch( retry_on_conflict=retry_on_conflict, ) - def delete(self, session, error_message=None, *, microversion=None, - **kwargs): + def delete( + self, session, error_message=None, *, microversion=None, **kwargs + ): """Delete the remote resource based on this instance. :param session: The session to use for making this request. @@ -1948,8 +1943,9 @@ def delete(self, session, error_message=None, *, microversion=None, the resource was not found. """ - response = self._raw_delete(session, microversion=microversion, - **kwargs) + response = self._raw_delete( + session, microversion=microversion, **kwargs + ) kwargs = {} if error_message: kwargs['error_message'] = error_message @@ -2116,7 +2112,8 @@ def _dict_filter(f, d): for key in client_filters.keys(): if isinstance(client_filters[key], dict): if not _dict_filter( - client_filters[key], value.get(key, None)): + client_filters[key], value.get(key, None) + ): filters_matched = False break elif value.get(key, None) != client_filters[key]: @@ -2176,7 +2173,7 @@ def _get_next_link(cls, uri, response, data, marker, limit, total_yielded): # Glance has a next field in the main body next_link = next_link or data.get('next') if next_link and next_link.startswith('/v'): - next_link = next_link[next_link.find('/', 1):] + next_link = next_link[next_link.find('/', 1) :] if not next_link and 'next' in response.links: # RFC5988 specifies Link headers and requests parses them if they @@ -2281,8 +2278,11 @@ def find( **params, ) return match.fetch(session, microversion=microversion, **params) - except (exceptions.NotFoundException, exceptions.BadRequestException, - exceptions.ForbiddenException): + except ( + exceptions.NotFoundException, + exceptions.BadRequestException, + exceptions.ForbiddenException, + ): # NOTE(gtema): There are few places around openstack that return # 400 if we try to GET resource and it doesn't exist. pass diff --git a/openstack/service_description.py b/openstack/service_description.py index 223a166d3..49cc81eab 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -36,7 +36,9 @@ def __getattr__(self, item): raise exceptions.ServiceDisabledException( "Service '{service_type}' is disabled because its configuration " "could not be loaded. {reason}".format( - service_type=self.service_type, reason=self.reason or '')) + service_type=self.service_type, reason=self.reason or '' + ) + ) class ServiceDescription: @@ -73,9 +75,8 @@ def __init__(self, service_type, supported_versions=None, aliases=None): """ self.service_type = service_type or self.service_type self.supported_versions = ( - supported_versions - or self.supported_versions - or {}) + supported_versions or self.supported_versions or {} + ) self.aliases = aliases or self.aliases self.all_types = [service_type] + self.aliases @@ -135,7 +136,9 @@ def _validate_proxy(self, proxy, endpoint): "Failed to create a working proxy for service {service_type}: " "{message}".format( service_type=self.service_type, - message=exc or "No valid endpoint was discoverable.")) + message=exc or "No valid endpoint was discoverable.", + ) + ) def _make_proxy(self, instance): """Create a Proxy for the service in question. @@ -148,7 +151,8 @@ def _make_proxy(self, instance): if not config.has_service(self.service_type): return _ServiceDisabledProxyShim( self.service_type, - config.get_disabled_reason(self.service_type)) + config.get_disabled_reason(self.service_type), + ) # We don't know anything about this service, so the user is # explicitly just using us for a passthrough REST adapter. @@ -186,13 +190,12 @@ def _make_proxy(self, instance): " {service_type} is not known or supported by" " openstacksdk. The resulting Proxy object will only" " have direct passthrough REST capabilities.".format( - version=version_string, - service_type=self.service_type), - category=exceptions.UnsupportedServiceVersion) + version=version_string, service_type=self.service_type + ), + category=exceptions.UnsupportedServiceVersion, + ) elif endpoint_override: - temp_adapter = config.get_session_client( - self.service_type - ) + temp_adapter = config.get_session_client(self.service_type) api_version = temp_adapter.get_endpoint_data().api_version proxy_class = self.supported_versions.get(str(api_version[0])) if proxy_class: @@ -207,9 +210,10 @@ def _make_proxy(self, instance): " is not supported by openstacksdk. The resulting Proxy" " object will only have direct passthrough REST" " capabilities.".format( - version=api_version, - service_type=self.service_type), - category=exceptions.UnsupportedServiceVersion) + version=api_version, service_type=self.service_type + ), + category=exceptions.UnsupportedServiceVersion, + ) if proxy_obj: @@ -225,7 +229,9 @@ def _make_proxy(self, instance): raise exceptions.ServiceDiscoveryException( "Failed to create a working proxy for service " "{service_type}: No endpoint data found.".format( - service_type=self.service_type)) + service_type=self.service_type + ) + ) # If we've gotten here with a proxy object it means we have # an endpoint_override in place. If the catalog_url and @@ -235,7 +241,8 @@ def _make_proxy(self, instance): # so that subsequent discovery calls don't get made incorrectly. if data.catalog_url != data.service_url: ep_key = '{service_type}_endpoint_override'.format( - service_type=self.service_type.replace('-', '_')) + service_type=self.service_type.replace('-', '_') + ) config.config[ep_key] = data.service_url proxy_obj = config.get_session_client( self.service_type, @@ -248,16 +255,16 @@ def _make_proxy(self, instance): if version_string: version_kwargs['version'] = version_string else: - supported_versions = sorted([ - int(f) for f in self.supported_versions]) + supported_versions = sorted( + [int(f) for f in self.supported_versions] + ) version_kwargs['min_version'] = str(supported_versions[0]) version_kwargs['max_version'] = '{version}.latest'.format( - version=str(supported_versions[-1])) + version=str(supported_versions[-1]) + ) temp_adapter = config.get_session_client( - self.service_type, - allow_version_hack=True, - **version_kwargs + self.service_type, allow_version_hack=True, **version_kwargs ) found_version = temp_adapter.get_api_major_version() if found_version is None: @@ -268,14 +275,18 @@ def _make_proxy(self, instance): " exists but does not have any supported versions.".format( service_type=self.service_type, cloud=instance.name, - region_name=region_name)) + region_name=region_name, + ) + ) else: raise exceptions.NotSupported( "The {service_type} service for {cloud}:{region_name}" " exists but no version was discoverable.".format( service_type=self.service_type, cloud=instance.name, - region_name=region_name)) + region_name=region_name, + ) + ) proxy_class = self.supported_versions.get(str(found_version[0])) if proxy_class: return config.get_session_client( @@ -294,8 +305,10 @@ def _make_proxy(self, instance): "Service {service_type} has no discoverable version." " The resulting Proxy object will only have direct" " passthrough REST capabilities.".format( - service_type=self.service_type), - category=exceptions.UnsupportedServiceVersion) + service_type=self.service_type + ), + category=exceptions.UnsupportedServiceVersion, + ) return temp_adapter def __set__(self, instance, value): diff --git a/openstack/tests/base.py b/openstack/tests/base.py index e74851dfd..a1239f95f 100644 --- a/openstack/tests/base.py +++ b/openstack/tests/base.py @@ -47,7 +47,9 @@ def setUp(self): test_timeout = int(test_timeout * self.TIMEOUT_SCALING_FACTOR) self.useFixture( fixtures.EnvironmentVariable( - 'OS_TEST_TIMEOUT', str(test_timeout))) + 'OS_TEST_TIMEOUT', str(test_timeout) + ) + ) except ValueError: # Let oslotest do its thing pass @@ -90,7 +92,8 @@ def assertEqual(self, first, second, *args, **kwargs): if isinstance(second, utils.Munch): second = second.toDict() return super(TestCase, self).assertEqual( - first, second, *args, **kwargs) + first, second, *args, **kwargs + ) def printLogs(self, *args): self._log_stream.seek(0) @@ -104,16 +107,18 @@ def reader(): if not x: break yield x.encode('utf8') + content = testtools.content.content_from_reader( - reader, - testtools.content_type.UTF8_TEXT, - False) + reader, testtools.content_type.UTF8_TEXT, False + ) self.addDetail('logging', content) def add_info_on_exception(self, name, text): def add_content(unused): - self.addDetail(name, testtools.content.text_content( - pprint.pformat(text))) + self.addDetail( + name, testtools.content.text_content(pprint.pformat(text)) + ) + self.addOnException(add_content) def assertSubdict(self, part, whole): @@ -124,11 +129,18 @@ def assertSubdict(self, part, whole): if not whole[key] and part[key]: missing_keys.append(key) if missing_keys: - self.fail("Keys %s are in %s but not in %s" % - (missing_keys, part, whole)) - wrong_values = [(key, part[key], whole[key]) - for key in part if part[key] != whole[key]] + self.fail( + "Keys %s are in %s but not in %s" % (missing_keys, part, whole) + ) + wrong_values = [ + (key, part[key], whole[key]) + for key in part + if part[key] != whole[key] + ] if wrong_values: - self.fail("Mismatched values: %s" % - ", ".join("for %s got %s and %s" % tpl - for tpl in wrong_values)) + self.fail( + "Mismatched values: %s" + % ", ".join( + "for %s got %s and %s" % tpl for tpl in wrong_values + ) + ) diff --git a/openstack/tests/fakes.py b/openstack/tests/fakes.py index e686cf2e5..a4a7ece8e 100644 --- a/openstack/tests/fakes.py +++ b/openstack/tests/fakes.py @@ -32,7 +32,8 @@ STRAWBERRY_FLAVOR_ID = u'0c1d9008-f546-4608-9e8f-f8bdaec8dddf' COMPUTE_ENDPOINT = 'https://compute.example.com/v2.1' ORCHESTRATION_ENDPOINT = 'https://orchestration.example.com/v1/{p}'.format( - p=PROJECT_ID) + p=PROJECT_ID +) NO_MD5 = '93b885adfe0da089cdf634904fd59f71' NO_SHA256 = '6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d' FAKE_PUBLIC_KEY = ( @@ -41,7 +42,8 @@ "sZacm0cZNuL69EObEGHdprfGJQajrpz22NQoCD8TFB8Wv+8om9NH9Le6s+WPe98WC77KLw8qg" "fQsbIey+JawPWl4O67ZdL5xrypuRjfIPWjgy/VH85IXg/Z/GONZ2nxHgSShMkwqSFECAC5L3P" "HB+0+/12M/iikdatFSVGjpuHvkLOs3oe7m6HlOfluSJ85BzLWBbvva93qkGmLg4ZAc8rPh2O+" - "YIsBUHNLLMM/oQp Generated-by-Nova\n") + "YIsBUHNLLMM/oQp Generated-by-Nova\n" +) def make_fake_flavor(flavor_id, name, ram=100, disk=1600, vcpus=24): @@ -50,29 +52,36 @@ def make_fake_flavor(flavor_id, name, ram=100, disk=1600, vcpus=24): u'OS-FLV-EXT-DATA:ephemeral': 0, u'disk': disk, u'id': flavor_id, - u'links': [{ - u'href': u'{endpoint}/flavors/{id}'.format( - endpoint=COMPUTE_ENDPOINT, id=flavor_id), - u'rel': u'self' - }, { - u'href': u'{endpoint}/flavors/{id}'.format( - endpoint=COMPUTE_ENDPOINT, id=flavor_id), - u'rel': u'bookmark' - }], + u'links': [ + { + u'href': u'{endpoint}/flavors/{id}'.format( + endpoint=COMPUTE_ENDPOINT, id=flavor_id + ), + u'rel': u'self', + }, + { + u'href': u'{endpoint}/flavors/{id}'.format( + endpoint=COMPUTE_ENDPOINT, id=flavor_id + ), + u'rel': u'bookmark', + }, + ], u'name': name, u'os-flavor-access:is_public': True, u'ram': ram, u'rxtx_factor': 1.0, u'swap': u'', - u'vcpus': vcpus + u'vcpus': vcpus, } FAKE_FLAVOR = make_fake_flavor(FLAVOR_ID, 'vanilla') FAKE_CHOCOLATE_FLAVOR = make_fake_flavor( - CHOCOLATE_FLAVOR_ID, 'chocolate', ram=200) + CHOCOLATE_FLAVOR_ID, 'chocolate', ram=200 +) FAKE_STRAWBERRY_FLAVOR = make_fake_flavor( - STRAWBERRY_FLAVOR_ID, 'strawberry', ram=300) + STRAWBERRY_FLAVOR_ID, 'strawberry', ram=300 +) FAKE_FLAVOR_LIST = [FAKE_FLAVOR, FAKE_CHOCOLATE_FLAVOR, FAKE_STRAWBERRY_FLAVOR] FAKE_TEMPLATE = '''heat_template_version: 2014-10-16 @@ -95,8 +104,14 @@ def make_fake_flavor(flavor_id, name, ram=100, disk=1600, vcpus=24): def make_fake_server( - server_id, name, status='ACTIVE', admin_pass=None, - addresses=None, image=None, flavor=None): + server_id, + name, + status='ACTIVE', + admin_pass=None, + addresses=None, + image=None, + flavor=None, +): if addresses is None: if status == 'ACTIVE': addresses = { @@ -105,25 +120,28 @@ def make_fake_server( "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:df:b0:8d", "version": 6, "addr": "fddb:b018:307:0:f816:3eff:fedf:b08d", - "OS-EXT-IPS:type": "fixed"}, + "OS-EXT-IPS:type": "fixed", + }, { "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:df:b0:8d", "version": 4, "addr": "10.1.0.9", - "OS-EXT-IPS:type": "fixed"}, + "OS-EXT-IPS:type": "fixed", + }, { "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:df:b0:8d", "version": 4, "addr": "172.24.5.5", - "OS-EXT-IPS:type": "floating"}]} + "OS-EXT-IPS:type": "floating", + }, + ] + } else: addresses = {} if image is None: - image = {"id": "217f3ab1-03e0-4450-bf27-63d52b421e9e", - "links": []} + image = {"id": "217f3ab1-03e0-4450-bf27-63d52b421e9e", "links": []} if flavor is None: - flavor = {"id": "64", - "links": []} + flavor = {"id": "64", "links": []} server = { "OS-EXT-STS:task_state": None, @@ -152,7 +170,8 @@ def make_fake_server( "created": "2017-03-23T23:57:12Z", "tenant_id": PROJECT_ID, "os-extended-volumes:volumes_attached": [], - "config_drive": "True"} + "config_drive": "True", + } if admin_pass: server['adminPass'] = admin_pass return json.loads(json.dumps(server)) @@ -188,7 +207,8 @@ def make_fake_stack(id, name, description=None, status='CREATE_COMPLETE'): def make_fake_stack_event( - id, name, status='CREATE_COMPLETED', resource_name='id'): + id, name, status='CREATE_COMPLETED', resource_name='id' +): event_id = uuid.uuid4().hex self_url = "{endpoint}/stacks/{name}/{id}/resources/{name}/events/{event}" resource_url = "{endpoint}/stacks/{name}/{id}/resources/{name}" @@ -199,19 +219,25 @@ def make_fake_stack_event( { "href": self_url.format( endpoint=ORCHESTRATION_ENDPOINT, - name=name, id=id, event=event_id), - "rel": "self" - }, { + name=name, + id=id, + event=event_id, + ), + "rel": "self", + }, + { "href": resource_url.format( - endpoint=ORCHESTRATION_ENDPOINT, - name=name, id=id), - "rel": "resource" - }, { + endpoint=ORCHESTRATION_ENDPOINT, name=name, id=id + ), + "rel": "resource", + }, + { "href": "{endpoint}/stacks/{name}/{id}".format( - endpoint=ORCHESTRATION_ENDPOINT, - name=name, id=id), - "rel": "stack" - }], + endpoint=ORCHESTRATION_ENDPOINT, name=name, id=id + ), + "rel": "stack", + }, + ], "logical_resource_id": name, "resource_status": status, "resource_status_reason": "", @@ -221,10 +247,14 @@ def make_fake_stack_event( def make_fake_image( - image_id=None, md5=NO_MD5, sha256=NO_SHA256, status='active', - image_name=u'fake_image', - data=None, - checksum=u'ee36e35a297980dee1b514de9803ec6d'): + image_id=None, + md5=NO_MD5, + sha256=NO_SHA256, + status='active', + image_name=u'fake_image', + data=None, + checksum=u'ee36e35a297980dee1b514de9803ec6d', +): if data: md5 = utils.md5(usedforsecurity=False) sha256 = hashlib.sha256() @@ -249,9 +279,9 @@ def make_fake_image( u'status': status, u'tags': [], u'visibility': u'private', - u'locations': [{ - u'url': u'http://127.0.0.1/images/' + image_id, - u'metadata': {}}], + u'locations': [ + {u'url': u'http://127.0.0.1/images/' + image_id, u'metadata': {}} + ], u'min_disk': 40, u'virtual_size': None, u'name': image_name, @@ -260,16 +290,16 @@ def make_fake_image( u'owner_specified.openstack.md5': md5 or NO_MD5, u'owner_specified.openstack.sha256': sha256 or NO_SHA256, u'owner_specified.openstack.object': 'images/{name}'.format( - name=image_name), - u'protected': False} + name=image_name + ), + u'protected': False, + } def make_fake_machine(machine_name, machine_id=None): if not machine_id: machine_id = uuid.uuid4().hex - return meta.obj_to_munch(FakeMachine( - id=machine_id, - name=machine_name)) + return meta.obj_to_munch(FakeMachine(id=machine_id, name=machine_name)) def make_fake_port(address, node_id=None, port_id=None): @@ -277,10 +307,9 @@ def make_fake_port(address, node_id=None, port_id=None): node_id = uuid.uuid4().hex if not port_id: port_id = uuid.uuid4().hex - return meta.obj_to_munch(FakeMachinePort( - id=port_id, - address=address, - node_id=node_id)) + return meta.obj_to_munch( + FakeMachinePort(id=port_id, address=address, node_id=node_id) + ) class FakeFloatingIP: @@ -293,63 +322,58 @@ def __init__(self, id, pool, ip, fixed_ip, instance_id): def make_fake_server_group(id, name, policies): - return json.loads(json.dumps({ - 'id': id, - 'name': name, - 'policies': policies, - 'members': [], - 'metadata': {}, - })) + return json.loads( + json.dumps( + { + 'id': id, + 'name': name, + 'policies': policies, + 'members': [], + 'metadata': {}, + } + ) + ) def make_fake_hypervisor(id, name): - return json.loads(json.dumps({ - 'id': id, - 'hypervisor_hostname': name, - 'state': 'up', - 'status': 'enabled', - "cpu_info": { - "arch": "x86_64", - "model": "Nehalem", - "vendor": "Intel", - "features": [ - "pge", - "clflush" - ], - "topology": { - "cores": 1, - "threads": 1, - "sockets": 4 + return json.loads( + json.dumps( + { + 'id': id, + 'hypervisor_hostname': name, + 'state': 'up', + 'status': 'enabled', + "cpu_info": { + "arch": "x86_64", + "model": "Nehalem", + "vendor": "Intel", + "features": ["pge", "clflush"], + "topology": {"cores": 1, "threads": 1, "sockets": 4}, + }, + "current_workload": 0, + "status": "enabled", + "state": "up", + "disk_available_least": 0, + "host_ip": "1.1.1.1", + "free_disk_gb": 1028, + "free_ram_mb": 7680, + "hypervisor_type": "fake", + "hypervisor_version": 1000, + "local_gb": 1028, + "local_gb_used": 0, + "memory_mb": 8192, + "memory_mb_used": 512, + "running_vms": 0, + "service": {"host": "host1", "id": 7, "disabled_reason": None}, + "vcpus": 1, + "vcpus_used": 0, } - }, - "current_workload": 0, - "status": "enabled", - "state": "up", - "disk_available_least": 0, - "host_ip": "1.1.1.1", - "free_disk_gb": 1028, - "free_ram_mb": 7680, - "hypervisor_type": "fake", - "hypervisor_version": 1000, - "local_gb": 1028, - "local_gb_used": 0, - "memory_mb": 8192, - "memory_mb_used": 512, - "running_vms": 0, - "service": { - "host": "host1", - "id": 7, - "disabled_reason": None - }, - "vcpus": 1, - "vcpus_used": 0 - })) + ) + ) class FakeVolume: - def __init__( - self, id, status, name, attachments=[], - size=75): + def __init__(self, id, status, name, attachments=[], size=75): self.id = id self.status = status self.name = name @@ -366,8 +390,7 @@ def __init__( class FakeVolumeSnapshot: - def __init__( - self, id, status, name, description, size=75): + def __init__(self, id, status, name, description, size=75): self.id = id self.status = status self.name = name @@ -380,10 +403,20 @@ def __init__( class FakeMachine: - def __init__(self, id, name=None, driver=None, driver_info=None, - chassis_uuid=None, instance_info=None, instance_uuid=None, - properties=None, reservation=None, last_error=None, - provision_state='available'): + def __init__( + self, + id, + name=None, + driver=None, + driver_info=None, + chassis_uuid=None, + instance_info=None, + instance_uuid=None, + properties=None, + reservation=None, + last_error=None, + provision_state='available', + ): self.uuid = id self.name = name self.driver = driver @@ -405,50 +438,69 @@ def __init__(self, id, address, node_id): def make_fake_neutron_security_group( - id, name, description, rules, stateful=True, project_id=None): + id, name, description, rules, stateful=True, project_id=None +): if not rules: rules = [] if not project_id: project_id = PROJECT_ID - return json.loads(json.dumps({ - 'id': id, - 'name': name, - 'description': description, - 'stateful': stateful, - 'project_id': project_id, - 'tenant_id': project_id, - 'security_group_rules': rules, - })) + return json.loads( + json.dumps( + { + 'id': id, + 'name': name, + 'description': description, + 'stateful': stateful, + 'project_id': project_id, + 'tenant_id': project_id, + 'security_group_rules': rules, + } + ) + ) def make_fake_nova_security_group_rule( - id, from_port, to_port, ip_protocol, cidr): - return json.loads(json.dumps({ - 'id': id, - 'from_port': int(from_port), - 'to_port': int(to_port), - 'ip_protcol': 'tcp', - 'ip_range': { - 'cidr': cidr - } - })) + id, from_port, to_port, ip_protocol, cidr +): + return json.loads( + json.dumps( + { + 'id': id, + 'from_port': int(from_port), + 'to_port': int(to_port), + 'ip_protcol': 'tcp', + 'ip_range': {'cidr': cidr}, + } + ) + ) def make_fake_nova_security_group(id, name, description, rules): if not rules: rules = [] - return json.loads(json.dumps({ - 'id': id, - 'name': name, - 'description': description, - 'tenant_id': PROJECT_ID, - 'rules': rules, - })) + return json.loads( + json.dumps( + { + 'id': id, + 'name': name, + 'description': description, + 'tenant_id': PROJECT_ID, + 'rules': rules, + } + ) + ) class FakeNovaSecgroupRule: - def __init__(self, id, from_port=None, to_port=None, ip_protocol=None, - cidr=None, parent_group_id=None): + def __init__( + self, + id, + from_port=None, + to_port=None, + ip_protocol=None, + cidr=None, + parent_group_id=None, + ): self.id = id self.from_port = from_port self.to_port = to_port @@ -465,8 +517,7 @@ def __init__(self, id, hostname): class FakeZone: - def __init__(self, id, name, type_, email, description, - ttl, masters): + def __init__(self, id, name, type_, email, description, ttl, masters): self.id = id self.name = name self.type_ = type_ @@ -477,8 +528,7 @@ def __init__(self, id, name, type_, email, description, class FakeRecordset: - def __init__(self, zone, id, name, type_, description, - ttl, records): + def __init__(self, zone, id, name, type_, description, ttl, records): self.zone = zone self.id = id self.name = name @@ -488,22 +538,27 @@ def __init__(self, zone, id, name, type_, description, self.records = records -def make_fake_aggregate(id, name, availability_zone='nova', - metadata=None, hosts=None): +def make_fake_aggregate( + id, name, availability_zone='nova', metadata=None, hosts=None +): if not metadata: metadata = {} if not hosts: hosts = [] - return json.loads(json.dumps({ - "availability_zone": availability_zone, - "created_at": datetime.datetime.now().isoformat(), - "deleted": False, - "deleted_at": None, - "hosts": hosts, - "id": int(id), - "metadata": { - "availability_zone": availability_zone, - }, - "name": name, - "updated_at": None, - })) + return json.loads( + json.dumps( + { + "availability_zone": availability_zone, + "created_at": datetime.datetime.now().isoformat(), + "deleted": False, + "deleted_at": None, + "hosts": hosts, + "id": int(id), + "metadata": { + "availability_zone": availability_zone, + }, + "name": name, + "updated_at": None, + } + ) + ) diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index 42cf40b99..3376ed122 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -51,10 +51,12 @@ def setUp(self): self._demo_name = os.environ.get('OPENSTACKSDK_DEMO_CLOUD', 'devstack') self._demo_name_alt = os.environ.get( - 'OPENSTACKSDK_DEMO_CLOUD_ALT', 'devstack-alt', + 'OPENSTACKSDK_DEMO_CLOUD_ALT', + 'devstack-alt', ) self._op_name = os.environ.get( - 'OPENSTACKSDK_OPERATOR_CLOUD', 'devstack-admin', + 'OPENSTACKSDK_OPERATOR_CLOUD', + 'devstack-admin', ) self.config = openstack.config.OpenStackConfig() @@ -64,8 +66,9 @@ def setUp(self): else: self.operator_cloud = None - self.identity_version = \ - self.user_cloud.config.get_api_version('identity') + self.identity_version = self.user_cloud.config.get_api_version( + 'identity' + ) self.flavor = self._pick_flavor() self.image = self._pick_image() @@ -73,8 +76,11 @@ def setUp(self): # Defines default timeout for wait_for methods used # in the functional tests self._wait_for_timeout = int( - os.getenv(self._wait_for_timeout_key, os.getenv( - 'OPENSTACKSDK_FUNC_TEST_TIMEOUT', 300))) + os.getenv( + self._wait_for_timeout_key, + os.getenv('OPENSTACKSDK_FUNC_TEST_TIMEOUT', 300), + ) + ) def _set_user_cloud(self, **kwargs): user_config = self.config.get_one(cloud=self._demo_name, **kwargs) @@ -85,7 +91,8 @@ def _set_user_cloud(self, **kwargs): # it if self._demo_name_alt: user_config_alt = self.config.get_one( - cloud=self._demo_name_alt, **kwargs) + cloud=self._demo_name_alt, **kwargs + ) self.user_cloud_alt = connection.Connection(config=user_config_alt) _disable_keep_alive(self.user_cloud_alt) else: @@ -119,7 +126,8 @@ def _pick_flavor(self): return flavor raise self.failureException( - "Cloud does not have flavor '%s'", flavor_name, + "Cloud does not have flavor '%s'", + flavor_name, ) # Enable running functional tests against RAX, which requires @@ -159,7 +167,8 @@ def _pick_image(self): return image raise self.failureException( - "Cloud does not have image '%s'", image_name, + "Cloud does not have image '%s'", + image_name, ) for image in images: @@ -186,6 +195,7 @@ def addEmptyCleanup(self, func, *args, **kwargs): def cleanup(): result = func(*args, **kwargs) self.assertIsNone(result) + self.addCleanup(cleanup) def require_service(self, service_type, min_microversion=None, **kwargs): @@ -201,14 +211,18 @@ def setUp(self): :returns: True if the service exists, otherwise False. """ if not self.conn.has_service(service_type): - self.skipTest('Service {service_type} not found in cloud'.format( - service_type=service_type)) + self.skipTest( + 'Service {service_type} not found in cloud'.format( + service_type=service_type + ) + ) if not min_microversion: return data = self.conn.session.get_endpoint_data( - service_type=service_type, **kwargs) + service_type=service_type, **kwargs + ) if not ( data.min_microversion @@ -230,12 +244,11 @@ def getUniqueString(self, prefix=None): # unix_t is also used to easier determine orphans when running real # functional tests on a real cloud return (prefix if prefix else '') + "{time}-{uuid}".format( - time=int(time.time()), - uuid=uuid.uuid4().hex) + time=int(time.time()), uuid=uuid.uuid4().hex + ) class KeystoneBaseFunctionalTest(BaseFunctionalTest): - def setUp(self): super(KeystoneBaseFunctionalTest, self).setUp() diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 2b0b28217..45cd73503 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -37,44 +37,49 @@ _ProjectData = collections.namedtuple( 'ProjectData', 'project_id, project_name, enabled, domain_id, description, ' - 'parent_id, json_response, json_request') + 'parent_id, json_response, json_request', +) _UserData = collections.namedtuple( 'UserData', 'user_id, password, name, email, description, domain_id, enabled, ' - 'json_response, json_request') + 'json_response, json_request', +) _GroupData = collections.namedtuple( 'GroupData', 'group_id, group_name, domain_id, description, json_response, ' - 'json_request') + 'json_request', +) _DomainData = collections.namedtuple( 'DomainData', - 'domain_id, domain_name, description, json_response, ' - 'json_request') + 'domain_id, domain_name, description, json_response, ' 'json_request', +) _ServiceData = collections.namedtuple( 'Servicedata', 'service_id, service_name, service_type, description, enabled, ' - 'json_response_v3, json_response_v2, json_request') + 'json_response_v3, json_response_v2, json_request', +) _EndpointDataV3 = collections.namedtuple( 'EndpointData', 'endpoint_id, service_id, interface, region_id, url, enabled, ' - 'json_response, json_request') + 'json_response, json_request', +) # NOTE(notmorgan): Shade does not support domain-specific roles # This should eventually be fixed if it becomes a main-stream feature. _RoleData = collections.namedtuple( - 'RoleData', - 'role_id, role_name, json_response, json_request') + 'RoleData', 'role_id, role_name, json_response, json_request' +) class TestCase(base.TestCase): @@ -92,17 +97,20 @@ def setUp(self, cloud_config_fixture='clouds.yaml'): def _nosleep(seconds): return realsleep(seconds * 0.0001) - self.sleep_fixture = self.useFixture(fixtures.MonkeyPatch( - 'time.sleep', - _nosleep)) + self.sleep_fixture = self.useFixture( + fixtures.MonkeyPatch('time.sleep', _nosleep) + ) self.fixtures_directory = 'openstack/tests/unit/fixtures' self.os_fixture = self.useFixture( - os_fixture.ConnectionFixture(project_id=fakes.PROJECT_ID)) + os_fixture.ConnectionFixture(project_id=fakes.PROJECT_ID) + ) # Isolate openstack.config from test environment config = tempfile.NamedTemporaryFile(delete=False) - cloud_path = '%s/clouds/%s' % (self.fixtures_directory, - cloud_config_fixture) + cloud_path = '%s/clouds/%s' % ( + self.fixtures_directory, + cloud_config_fixture, + ) with open(cloud_path, 'rb') as f: content = f.read() config.write(content) @@ -115,7 +123,8 @@ def _nosleep(seconds): self.config = occ.OpenStackConfig( config_files=[config.name], vendor_files=[vendor.name], - secure_files=['non-existant']) + secure_files=['non-existant'], + ) self.oslo_config_dict = { # All defaults for nova @@ -126,7 +135,7 @@ def _nosleep(seconds): 'heat': { 'region_name': 'SpecialRegion', 'interface': 'internal', - 'endpoint_override': 'https://example.org:8888/heat/v2' + 'endpoint_override': 'https://example.org:8888/heat/v2', }, # test a service with dashes 'ironic_inspector': { @@ -151,7 +160,8 @@ def _nosleep(seconds): # request in the correct orders. self._uri_registry = collections.OrderedDict() self.discovery_json = os.path.join( - self.fixtures_directory, 'discovery.json') + self.fixtures_directory, 'discovery.json' + ) self.use_keystone_v3() self.__register_uris_called = False @@ -166,11 +176,18 @@ def _load_ks_cfg_opts(self): return conf # TODO(shade) Update this to handle service type aliases - def get_mock_url(self, service_type, interface='public', resource=None, - append=None, base_url_append=None, - qs_elements=None): + def get_mock_url( + self, + service_type, + interface='public', + resource=None, + append=None, + base_url_append=None, + qs_elements=None, + ): endpoint_url = self.cloud.endpoint_for( - service_type=service_type, interface=interface) + service_type=service_type, interface=interface + ) # Strip trailing slashes, so as not to produce double-slashes below if endpoint_url.endswith('/'): endpoint_url = endpoint_url[:-1] @@ -184,13 +201,17 @@ def get_mock_url(self, service_type, interface='public', resource=None, to_join.extend([urllib.parse.quote(i) for i in append]) if qs_elements is not None: qs = '?%s' % '&'.join(qs_elements) - return '%(uri)s%(qs)s' % { - 'uri': '/'.join(to_join), - 'qs': qs} - - def mock_for_keystone_projects(self, project=None, v3=True, - list_get=False, id_get=False, - project_list=None, project_count=None): + return '%(uri)s%(qs)s' % {'uri': '/'.join(to_join), 'qs': qs} + + def mock_for_keystone_projects( + self, + project=None, + v3=True, + list_get=False, + id_get=False, + project_list=None, + project_count=None, + ): if project: assert not (project_list or project_count) elif project_list: @@ -198,8 +219,9 @@ def mock_for_keystone_projects(self, project=None, v3=True, elif project_count: assert not (project or project_list) else: - raise Exception('Must specify a project, project_list, ' - 'or project_count') + raise Exception( + 'Must specify a project, project_list, ' 'or project_count' + ) assert list_get or id_get base_url_append = 'v3' if v3 else None @@ -207,40 +229,57 @@ def mock_for_keystone_projects(self, project=None, v3=True, project_list = [project] elif project_count: # Generate multiple projects - project_list = [self._get_project_data(v3=v3) - for c in range(0, project_count)] + project_list = [ + self._get_project_data(v3=v3) for c in range(0, project_count) + ] uri_mock_list = [] if list_get: uri_mock_list.append( - dict(method='GET', - uri=self.get_mock_url( - service_type='identity', - interface='admin', - resource='projects', - base_url_append=base_url_append), - status_code=200, - json={'projects': [p.json_response['project'] - for p in project_list]}) + dict( + method='GET', + uri=self.get_mock_url( + service_type='identity', + interface='admin', + resource='projects', + base_url_append=base_url_append, + ), + status_code=200, + json={ + 'projects': [ + p.json_response['project'] for p in project_list + ] + }, + ) ) if id_get: for p in project_list: uri_mock_list.append( - dict(method='GET', - uri=self.get_mock_url( - service_type='identity', - interface='admin', - resource='projects', - append=[p.project_id], - base_url_append=base_url_append), - status_code=200, - json=p.json_response) + dict( + method='GET', + uri=self.get_mock_url( + service_type='identity', + interface='admin', + resource='projects', + append=[p.project_id], + base_url_append=base_url_append, + ), + status_code=200, + json=p.json_response, + ) ) self.__do_register_uris(uri_mock_list) return project_list - def _get_project_data(self, project_name=None, enabled=None, - domain_id=None, description=None, v3=True, - project_id=None, parent_id=None): + def _get_project_data( + self, + project_name=None, + enabled=None, + domain_id=None, + description=None, + v3=True, + project_id=None, + parent_id=None, + ): project_name = project_name or self.getUniqueString('projectName') project_id = uuid.UUID(project_id or uuid.uuid4().hex).hex if parent_id: @@ -264,9 +303,16 @@ def _get_project_data(self, project_name=None, enabled=None, response['description'] = description request['description'] = description request.setdefault('description', None) - return _ProjectData(project_id, project_name, enabled, domain_id, - description, parent_id, - {'project': response}, {'project': request}) + return _ProjectData( + project_id, + project_name, + enabled, + domain_id, + description, + parent_id, + {'project': response}, + {'project': request}, + ) def _get_group_data(self, name=None, domain_id=None, description=None): group_id = uuid.uuid4().hex @@ -278,8 +324,14 @@ def _get_group_data(self, name=None, domain_id=None, description=None): response['description'] = description request['description'] = description - return _GroupData(group_id, name, domain_id, description, - {'group': response}, {'group': request}) + return _GroupData( + group_id, + name, + domain_id, + description, + {'group': response}, + {'group': request}, + ) def _get_user_data(self, name=None, password=None, **kwargs): @@ -305,16 +357,27 @@ def _get_user_data(self, name=None, password=None, **kwargs): if response['description']: request['description'] = response['description'] - self.assertIs(0, len(kwargs), message='extra key-word args received ' - 'on _get_user_data') + self.assertIs( + 0, + len(kwargs), + message='extra key-word args received ' 'on _get_user_data', + ) - return _UserData(user_id, password, name, response['email'], - response['description'], response.get('domain_id'), - response.get('enabled'), {'user': response}, - {'user': request}) + return _UserData( + user_id, + password, + name, + response['email'], + response['description'], + response.get('domain_id'), + response.get('enabled'), + {'user': response}, + {'user': request}, + ) - def _get_domain_data(self, domain_name=None, description=None, - enabled=None): + def _get_domain_data( + self, domain_name=None, description=None, enabled=None + ): domain_id = uuid.uuid4().hex domain_name = domain_name or self.getUniqueString('domainName') response = {'id': domain_id, 'name': domain_name} @@ -326,41 +389,76 @@ def _get_domain_data(self, domain_name=None, description=None, response['description'] = description request['description'] = description response.setdefault('enabled', True) - return _DomainData(domain_id, domain_name, description, - {'domain': response}, {'domain': request}) + return _DomainData( + domain_id, + domain_name, + description, + {'domain': response}, + {'domain': request}, + ) - def _get_service_data(self, type=None, name=None, description=None, - enabled=True): + def _get_service_data( + self, type=None, name=None, description=None, enabled=True + ): service_id = uuid.uuid4().hex name = name or uuid.uuid4().hex type = type or uuid.uuid4().hex - response = {'id': service_id, 'name': name, 'type': type, - 'enabled': enabled} + response = { + 'id': service_id, + 'name': name, + 'type': type, + 'enabled': enabled, + } if description is not None: response['description'] = description request = response.copy() request.pop('id') - return _ServiceData(service_id, name, type, description, enabled, - {'service': response}, - {'OS-KSADM:service': response}, request) + return _ServiceData( + service_id, + name, + type, + description, + enabled, + {'service': response}, + {'OS-KSADM:service': response}, + request, + ) - def _get_endpoint_v3_data(self, service_id=None, region=None, - url=None, interface=None, enabled=True): + def _get_endpoint_v3_data( + self, + service_id=None, + region=None, + url=None, + interface=None, + enabled=True, + ): endpoint_id = uuid.uuid4().hex service_id = service_id or uuid.uuid4().hex region = region or uuid.uuid4().hex url = url or 'https://example.com/' interface = interface or uuid.uuid4().hex - response = {'id': endpoint_id, 'service_id': service_id, - 'region_id': region, 'interface': interface, - 'url': url, 'enabled': enabled} + response = { + 'id': endpoint_id, + 'service_id': service_id, + 'region_id': region, + 'interface': interface, + 'url': url, + 'enabled': enabled, + } request = response.copy() request.pop('id') - return _EndpointDataV3(endpoint_id, service_id, interface, region, - url, enabled, {'endpoint': response}, - {'endpoint': request}) + return _EndpointDataV3( + endpoint_id, + service_id, + interface, + region, + url, + enabled, + {'endpoint': response}, + {'endpoint': request}, + ) def _get_role_data(self, role_name=None): role_id = uuid.uuid4().hex @@ -368,20 +466,28 @@ def _get_role_data(self, role_name=None): request = {'name': role_name} response = request.copy() response['id'] = role_id - return _RoleData(role_id, role_name, {'role': response}, - {'role': request}) + return _RoleData( + role_id, role_name, {'role': response}, {'role': request} + ) def use_broken_keystone(self): self.adapter = self.useFixture(rm_fixture.Fixture()) self.calls = [] self._uri_registry.clear() - self.__do_register_uris([ - dict(method='GET', uri='https://identity.example.com/', - text=open(self.discovery_json, 'r').read()), - dict(method='POST', - uri='https://identity.example.com/v3/auth/tokens', - status_code=400), - ]) + self.__do_register_uris( + [ + dict( + method='GET', + uri='https://identity.example.com/', + text=open(self.discovery_json, 'r').read(), + ), + dict( + method='POST', + uri='https://identity.example.com/v3/auth/tokens', + status_code=400, + ), + ] + ) self._make_test_cloud(identity_api_version='3') def use_nothing(self): @@ -389,40 +495,38 @@ def use_nothing(self): self._uri_registry.clear() def get_keystone_v3_token( - self, - project_name='admin', + self, + project_name='admin', ): return dict( method='POST', uri='https://identity.example.com/v3/auth/tokens', - headers={ - 'X-Subject-Token': self.getUniqueString('KeystoneToken') - }, + headers={'X-Subject-Token': self.getUniqueString('KeystoneToken')}, json=self.os_fixture.v3_token, - validate=dict(json={ - 'auth': { - 'identity': { - 'methods': ['password'], - 'password': { - 'user': { - 'domain': { - 'name': 'default', - }, - 'name': 'admin', - 'password': 'password' - } - } - }, - 'scope': { - 'project': { - 'domain': { - 'name': 'default' + validate=dict( + json={ + 'auth': { + 'identity': { + 'methods': ['password'], + 'password': { + 'user': { + 'domain': { + 'name': 'default', + }, + 'name': 'admin', + 'password': 'password', + } }, - 'name': project_name - } + }, + 'scope': { + 'project': { + 'domain': {'name': 'default'}, + 'name': project_name, + } + }, } } - }), + ), ) def get_keystone_discovery(self): @@ -437,10 +541,12 @@ def use_keystone_v3(self): self.adapter = self.useFixture(rm_fixture.Fixture()) self.calls = [] self._uri_registry.clear() - self.__do_register_uris([ - self.get_keystone_discovery(), - self.get_keystone_v3_token(), - ]) + self.__do_register_uris( + [ + self.get_keystone_discovery(), + self.get_keystone_v3_token(), + ] + ) self._make_test_cloud(identity_api_version='3') def use_keystone_v2(self): @@ -448,119 +554,171 @@ def use_keystone_v2(self): self.calls = [] self._uri_registry.clear() - self.__do_register_uris([ - self.get_keystone_discovery(), - dict(method='POST', - uri='https://identity.example.com/v2.0/tokens', - json=self.os_fixture.v2_token, - ), - ]) + self.__do_register_uris( + [ + self.get_keystone_discovery(), + dict( + method='POST', + uri='https://identity.example.com/v2.0/tokens', + json=self.os_fixture.v2_token, + ), + ] + ) - self._make_test_cloud(cloud_name='_test_cloud_v2_', - identity_api_version='2.0') + self._make_test_cloud( + cloud_name='_test_cloud_v2_', identity_api_version='2.0' + ) def _make_test_cloud(self, cloud_name='_test_cloud_', **kwargs): test_cloud = os.environ.get('OPENSTACKSDK_OS_CLOUD', cloud_name) self.cloud_config = self.config.get_one( - cloud=test_cloud, validate=True, **kwargs) + cloud=test_cloud, validate=True, **kwargs + ) self.cloud = openstack.connection.Connection( - config=self.cloud_config, strict=self.strict_cloud) + config=self.cloud_config, strict=self.strict_cloud + ) def get_cinder_discovery_mock_dict( - self, - block_storage_version_json='block-storage-version.json', - block_storage_discovery_url='https://block-storage.example.com/'): + self, + block_storage_version_json='block-storage-version.json', + block_storage_discovery_url='https://block-storage.example.com/', + ): discovery_fixture = os.path.join( - self.fixtures_directory, block_storage_version_json) - return dict(method='GET', uri=block_storage_discovery_url, - text=open(discovery_fixture, 'r').read()) + self.fixtures_directory, block_storage_version_json + ) + return dict( + method='GET', + uri=block_storage_discovery_url, + text=open(discovery_fixture, 'r').read(), + ) def get_glance_discovery_mock_dict( - self, - image_version_json='image-version.json', - image_discovery_url='https://image.example.com/'): + self, + image_version_json='image-version.json', + image_discovery_url='https://image.example.com/', + ): discovery_fixture = os.path.join( - self.fixtures_directory, image_version_json) - return dict(method='GET', uri=image_discovery_url, - status_code=300, - text=open(discovery_fixture, 'r').read()) + self.fixtures_directory, image_version_json + ) + return dict( + method='GET', + uri=image_discovery_url, + status_code=300, + text=open(discovery_fixture, 'r').read(), + ) def get_nova_discovery_mock_dict( - self, - compute_version_json='compute-version.json', - compute_discovery_url='https://compute.example.com/v2.1/'): + self, + compute_version_json='compute-version.json', + compute_discovery_url='https://compute.example.com/v2.1/', + ): discovery_fixture = os.path.join( - self.fixtures_directory, compute_version_json) + self.fixtures_directory, compute_version_json + ) return dict( method='GET', uri=compute_discovery_url, - text=open(discovery_fixture, 'r').read()) + text=open(discovery_fixture, 'r').read(), + ) def get_placement_discovery_mock_dict( - self, discovery_fixture='placement.json'): + self, discovery_fixture='placement.json' + ): discovery_fixture = os.path.join( - self.fixtures_directory, discovery_fixture) - return dict(method='GET', uri="https://placement.example.com/", - text=open(discovery_fixture, 'r').read()) + self.fixtures_directory, discovery_fixture + ) + return dict( + method='GET', + uri="https://placement.example.com/", + text=open(discovery_fixture, 'r').read(), + ) def get_designate_discovery_mock_dict(self): - discovery_fixture = os.path.join( - self.fixtures_directory, "dns.json") - return dict(method='GET', uri="https://dns.example.com/", - text=open(discovery_fixture, 'r').read()) + discovery_fixture = os.path.join(self.fixtures_directory, "dns.json") + return dict( + method='GET', + uri="https://dns.example.com/", + text=open(discovery_fixture, 'r').read(), + ) def get_ironic_discovery_mock_dict(self): discovery_fixture = os.path.join( - self.fixtures_directory, "baremetal.json") - return dict(method='GET', uri="https://baremetal.example.com/", - text=open(discovery_fixture, 'r').read()) + self.fixtures_directory, "baremetal.json" + ) + return dict( + method='GET', + uri="https://baremetal.example.com/", + text=open(discovery_fixture, 'r').read(), + ) def get_senlin_discovery_mock_dict(self): discovery_fixture = os.path.join( - self.fixtures_directory, "clustering.json") - return dict(method='GET', uri="https://clustering.example.com/", - text=open(discovery_fixture, 'r').read()) + self.fixtures_directory, "clustering.json" + ) + return dict( + method='GET', + uri="https://clustering.example.com/", + text=open(discovery_fixture, 'r').read(), + ) def use_compute_discovery( - self, compute_version_json='compute-version.json', - compute_discovery_url='https://compute.example.com/v2.1/'): - self.__do_register_uris([ - self.get_nova_discovery_mock_dict( - compute_version_json, compute_discovery_url), - ]) + self, + compute_version_json='compute-version.json', + compute_discovery_url='https://compute.example.com/v2.1/', + ): + self.__do_register_uris( + [ + self.get_nova_discovery_mock_dict( + compute_version_json, compute_discovery_url + ), + ] + ) def get_cyborg_discovery_mock_dict(self): discovery_fixture = os.path.join( - self.fixtures_directory, "accelerator.json") - return dict(method='GET', uri="https://accelerator.example.com/", - text=open(discovery_fixture, 'r').read()) + self.fixtures_directory, "accelerator.json" + ) + return dict( + method='GET', + uri="https://accelerator.example.com/", + text=open(discovery_fixture, 'r').read(), + ) def get_manila_discovery_mock_dict(self): discovery_fixture = os.path.join( - self.fixtures_directory, "shared-file-system.json") - return dict(method='GET', - uri="https://shared-file-system.example.com/", - text=open(discovery_fixture, 'r').read()) + self.fixtures_directory, "shared-file-system.json" + ) + return dict( + method='GET', + uri="https://shared-file-system.example.com/", + text=open(discovery_fixture, 'r').read(), + ) def use_glance( - self, image_version_json='image-version.json', - image_discovery_url='https://image.example.com/'): + self, + image_version_json='image-version.json', + image_discovery_url='https://image.example.com/', + ): # NOTE(notmorgan): This method is only meant to be used in "setUp" # where the ordering of the url being registered is tightly controlled # if the functionality of .use_glance is meant to be used during an # actual test case, use .get_glance_discovery_mock and apply to the # right location in the mock_uris when calling .register_uris - self.__do_register_uris([ - self.get_glance_discovery_mock_dict( - image_version_json, image_discovery_url)]) + self.__do_register_uris( + [ + self.get_glance_discovery_mock_dict( + image_version_json, image_discovery_url + ) + ] + ) def use_cinder(self): - self.__do_register_uris([ - self.get_cinder_discovery_mock_dict()]) + self.__do_register_uris([self.get_cinder_discovery_mock_dict()]) def use_placement(self, **kwargs): - self.__do_register_uris([ - self.get_placement_discovery_mock_dict(**kwargs)]) + self.__do_register_uris( + [self.get_placement_discovery_mock_dict(**kwargs)] + ) def use_designate(self): # NOTE(slaweq): This method is only meant to be used in "setUp" @@ -568,8 +726,7 @@ def use_designate(self): # if the functionality of .use_designate is meant to be used during an # actual test case, use .get_designate_discovery_mock and apply to the # right location in the mock_uris when calling .register_uris - self.__do_register_uris([ - self.get_designate_discovery_mock_dict()]) + self.__do_register_uris([self.get_designate_discovery_mock_dict()]) def use_ironic(self): # NOTE(TheJulia): This method is only meant to be used in "setUp" @@ -577,8 +734,7 @@ def use_ironic(self): # if the functionality of .use_ironic is meant to be used during an # actual test case, use .get_ironic_discovery_mock and apply to the # right location in the mock_uris when calling .register_uris - self.__do_register_uris([ - self.get_ironic_discovery_mock_dict()]) + self.__do_register_uris([self.get_ironic_discovery_mock_dict()]) def use_senlin(self): # NOTE(elachance): This method is only meant to be used in "setUp" @@ -586,8 +742,7 @@ def use_senlin(self): # if the functionality of .use_senlin is meant to be used during an # actual test case, use .get_senlin_discovery_mock and apply to the # right location in the mock_uris when calling .register_uris - self.__do_register_uris([ - self.get_senlin_discovery_mock_dict()]) + self.__do_register_uris([self.get_senlin_discovery_mock_dict()]) def use_cyborg(self): # NOTE(s_shogo): This method is only meant to be used in "setUp" @@ -595,8 +750,7 @@ def use_cyborg(self): # if the functionality of .use_cyborg is meant to be used during an # actual test case, use .get_cyborg_discovery_mock and apply to the # right location in the mock_uris when calling .register_uris - self.__do_register_uris([ - self.get_cyborg_discovery_mock_dict()]) + self.__do_register_uris([self.get_cyborg_discovery_mock_dict()]) def use_manila(self): # NOTE(gouthamr): This method is only meant to be used in "setUp" @@ -604,8 +758,7 @@ def use_manila(self): # if the functionality of .use_manila is meant to be used during an # actual test case, use .get_manila_discovery_mock and apply to the # right location in the mock_uris when calling .register_uris - self.__do_register_uris([ - self.get_manila_discovery_mock_dict()]) + self.__do_register_uris([self.get_manila_discovery_mock_dict()]) def register_uris(self, uri_mock_list=None): """Mock a list of URIs and responses via requests mock. @@ -645,10 +798,11 @@ def register_uris(self, uri_mock_list=None): def __do_register_uris(self, uri_mock_list=None): for to_mock in uri_mock_list: - kw_params = {k: to_mock.pop(k) - for k in ('request_headers', 'complete_qs', - '_real_http') - if k in to_mock} + kw_params = { + k: to_mock.pop(k) + for k in ('request_headers', 'complete_qs', '_real_http') + if k in to_mock + } method = to_mock.pop('method') uri = to_mock.pop('uri') @@ -656,44 +810,51 @@ def __do_register_uris(self, uri_mock_list=None): # case "|" is used so that the split can be a bit easier on # maintainers of this code. key = '{method}|{uri}|{params}'.format( - method=method, uri=uri, params=kw_params) + method=method, uri=uri, params=kw_params + ) validate = to_mock.pop('validate', {}) valid_keys = set(['json', 'headers', 'params', 'data']) invalid_keys = set(validate.keys()) - valid_keys if invalid_keys: raise TypeError( "Invalid values passed to validate: {keys}".format( - keys=invalid_keys)) - headers = structures.CaseInsensitiveDict(to_mock.pop('headers', - {})) + keys=invalid_keys + ) + ) + headers = structures.CaseInsensitiveDict( + to_mock.pop('headers', {}) + ) if 'content-type' not in headers: headers[u'content-type'] = 'application/json' if 'exc' not in to_mock: to_mock['headers'] = headers - self.calls += [ - dict( - method=method, - url=uri, **validate) - ] + self.calls += [dict(method=method, url=uri, **validate)] self._uri_registry.setdefault( - key, {'response_list': [], 'kw_params': kw_params}) + key, {'response_list': [], 'kw_params': kw_params} + ) if self._uri_registry[key]['kw_params'] != kw_params: raise AssertionError( 'PROGRAMMING ERROR: key-word-params ' 'should be part of the uri_key and cannot change, ' 'it will affect the matcher in requests_mock. ' - '%(old)r != %(new)r' % - {'old': self._uri_registry[key]['kw_params'], - 'new': kw_params}) + '%(old)r != %(new)r' + % { + 'old': self._uri_registry[key]['kw_params'], + 'new': kw_params, + } + ) self._uri_registry[key]['response_list'].append(to_mock) for mocked, params in self._uri_registry.items(): mock_method, mock_uri, _ignored = mocked.split('|', 2) self.adapter.register_uri( - mock_method, mock_uri, params['response_list'], - **params['kw_params']) + mock_method, + mock_uri, + params['response_list'], + **params['kw_params'] + ) def assert_no_calls(self): # TODO(mordred) For now, creating the adapter for self.conn is @@ -704,46 +865,65 @@ def assert_no_calls(self): def assert_calls(self, stop_after=None, do_count=True): for (x, (call, history)) in enumerate( - zip(self.calls, self.adapter.request_history)): + zip(self.calls, self.adapter.request_history) + ): if stop_after and x > stop_after: break call_uri_parts = urllib.parse.urlparse(call['url']) history_uri_parts = urllib.parse.urlparse(history.url) self.assertEqual( - (call['method'], call_uri_parts.scheme, call_uri_parts.netloc, - call_uri_parts.path, call_uri_parts.params, - urllib.parse.parse_qs(call_uri_parts.query)), - (history.method, history_uri_parts.scheme, - history_uri_parts.netloc, history_uri_parts.path, - history_uri_parts.params, - urllib.parse.parse_qs(history_uri_parts.query)), - ('REST mismatch on call %(index)d. Expected %(call)r. ' - 'Got %(history)r). ' - 'NOTE: query string order differences wont cause mismatch' % - { - 'index': x, - 'call': '{method} {url}'.format(method=call['method'], - url=call['url']), - 'history': '{method} {url}'.format( - method=history.method, - url=history.url)}) + ( + call['method'], + call_uri_parts.scheme, + call_uri_parts.netloc, + call_uri_parts.path, + call_uri_parts.params, + urllib.parse.parse_qs(call_uri_parts.query), + ), + ( + history.method, + history_uri_parts.scheme, + history_uri_parts.netloc, + history_uri_parts.path, + history_uri_parts.params, + urllib.parse.parse_qs(history_uri_parts.query), + ), + ( + 'REST mismatch on call %(index)d. Expected %(call)r. ' + 'Got %(history)r). ' + 'NOTE: query string order differences wont cause mismatch' + % { + 'index': x, + 'call': '{method} {url}'.format( + method=call['method'], url=call['url'] + ), + 'history': '{method} {url}'.format( + method=history.method, url=history.url + ), + } + ), ) if 'json' in call: self.assertEqual( - call['json'], history.json(), - 'json content mismatch in call {index}'.format(index=x)) + call['json'], + history.json(), + 'json content mismatch in call {index}'.format(index=x), + ) # headers in a call isn't exhaustive - it's checking to make sure # a specific header or headers are there, not that they are the # only headers if 'headers' in call: for key, value in call['headers'].items(): self.assertEqual( - value, history.headers[key], - 'header mismatch in call {index}'.format(index=x)) + value, + history.headers[key], + 'header mismatch in call {index}'.format(index=x), + ) if do_count: self.assertEqual( - len(self.calls), len(self.adapter.request_history)) + len(self.calls), len(self.adapter.request_history) + ) def assertResourceEqual(self, actual, expected, resource_type): """Helper for the assertEqual which compares Resource object against @@ -756,7 +936,7 @@ def assertResourceEqual(self, actual, expected, resource_type): """ return self.assertEqual( resource_type(**expected).to_dict(computed=False), - actual.to_dict(computed=False) + actual.to_dict(computed=False), ) def assertResourceListEqual(self, actual, expected, resource_type): @@ -771,12 +951,11 @@ def assertResourceListEqual(self, actual, expected, resource_type): """ self.assertEqual( [resource_type(**f).to_dict(computed=False) for f in expected], - [f.to_dict(computed=False) for f in actual] + [f.to_dict(computed=False) for f in actual], ) class IronicTestCase(TestCase): - def setUp(self): super(IronicTestCase, self).setUp() self.use_ironic() diff --git a/openstack/tests/unit/cloud/test_create_server.py b/openstack/tests/unit/cloud/test_create_server.py index 4b3c1e543..deaf19cb8 100644 --- a/openstack/tests/unit/cloud/test_create_server.py +++ b/openstack/tests/unit/cloud/test_create_server.py @@ -706,7 +706,9 @@ def test_create_server_wait(self, mock_wait): ] ) self.cloud.create_server( - 'server-name', dict(id='image-id'), dict(id='flavor-id'), + 'server-name', + dict(id='image-id'), + dict(id='flavor-id'), wait=True, ), diff --git a/openstack/tests/unit/common/test_metadata.py b/openstack/tests/unit/common/test_metadata.py index d1aac4d6d..bc1909675 100644 --- a/openstack/tests/unit/common/test_metadata.py +++ b/openstack/tests/unit/common/test_metadata.py @@ -23,7 +23,6 @@ class TestMetadata(base.TestCase): - def setUp(self): super(TestMetadata, self).setUp() @@ -95,8 +94,7 @@ def test_set_metadata(self): self.assertEqual(res, result) url = self.base_path + '/' + res.id + '/metadata' self.session.post.assert_called_once_with( - url, - json={'metadata': {'foo': 'bar'}} + url, json={'metadata': {'foo': 'bar'}} ) def test_replace_metadata(self): @@ -109,8 +107,7 @@ def test_replace_metadata(self): self.assertEqual(res, result) url = self.base_path + '/' + res.id + '/metadata' self.session.put.assert_called_once_with( - url, - json={'metadata': {'foo': 'bar'}} + url, json={'metadata': {'foo': 'bar'}} ) def test_delete_all_metadata(self): @@ -125,9 +122,7 @@ def test_delete_all_metadata(self): # Check passed resource is returned self.assertEqual(res, result) url = self.base_path + '/' + res.id + '/metadata' - self.session.put.assert_called_once_with( - url, - json={'metadata': {}}) + self.session.put.assert_called_once_with(url, json={'metadata': {}}) def test_get_metadata_item(self): res = self.sot @@ -198,5 +193,5 @@ def test_set_metadata_item(self): self.assertEqual(res, result) url = self.base_path + '/' + res.id + '/metadata/foo' self.session.put.assert_called_once_with( - url, - json={'meta': {'foo': 'black'}}) + url, json={'meta': {'foo': 'black'}} + ) diff --git a/openstack/tests/unit/common/test_quota_set.py b/openstack/tests/unit/common/test_quota_set.py index fe7b49d75..8fdc9a3d6 100644 --- a/openstack/tests/unit/common/test_quota_set.py +++ b/openstack/tests/unit/common/test_quota_set.py @@ -25,26 +25,13 @@ } USAGE_EXAMPLE = { - "backup_gigabytes": { - "in_use": 0, - "limit": 1000, - "reserved": 0 - }, - "backups": { - "in_use": 0, - "limit": 10, - "reserved": 0 - }, - "gigabytes___DEFAULT__": { - "in_use": 0, - "limit": -1, - "reserved": 0 - } + "backup_gigabytes": {"in_use": 0, "limit": 1000, "reserved": 0}, + "backups": {"in_use": 0, "limit": 10, "reserved": 0}, + "gigabytes___DEFAULT__": {"in_use": 0, "limit": -1, "reserved": 0}, } class TestQuotaSet(base.TestCase): - def setUp(self): super(TestQuotaSet, self).setUp() self.sess = mock.Mock(spec=adapter.Adapter) @@ -64,10 +51,9 @@ def test_basic(self): self.assertTrue(sot.allow_commit) self.assertDictEqual( - {"usage": "usage", - "limit": "limit", - "marker": "marker"}, - sot._query_mapping._mapping) + {"usage": "usage", "limit": "limit", "marker": "marker"}, + sot._query_mapping._mapping, + ) def test_make_basic(self): sot = _qs.QuotaSet(**BASIC_EXAMPLE) @@ -87,10 +73,8 @@ def test_get(self): sot.fetch(self.sess) self.sess.get.assert_called_with( - '/os-quota-sets/proj', - microversion=1, - params={}, - skip_cache=False) + '/os-quota-sets/proj', microversion=1, params={}, skip_cache=False + ) self.assertEqual(BASIC_EXAMPLE['backups'], sot.backups) self.assertEqual({}, sot.reservation) @@ -112,11 +96,10 @@ def test_get_usage(self): '/os-quota-sets/proj', microversion=1, params={'usage': True}, - skip_cache=False) + skip_cache=False, + ) - self.assertEqual( - USAGE_EXAMPLE['backups']['limit'], - sot.backups) + self.assertEqual(USAGE_EXAMPLE['backups']['limit'], sot.backups) def test_update_quota(self): # Use QuotaSet as if it was returned by get(usage=True) @@ -124,7 +107,8 @@ def test_update_quota(self): project_id='proj', reservation={'a': 'b'}, usage={'c': 'd'}, - foo='bar') + foo='bar', + ) resp = mock.Mock() resp.body = {'quota_set': copy.deepcopy(BASIC_EXAMPLE)} @@ -133,10 +117,7 @@ def test_update_quota(self): resp.headers = {} self.sess.put = mock.Mock(return_value=resp) - sot._update( - reservation={'b': 'd'}, - backups=15, - something_else=20) + sot._update(reservation={'b': 'd'}, backups=15, something_else=20) sot.commit(self.sess) @@ -144,12 +125,8 @@ def test_update_quota(self): '/os-quota-sets/proj', microversion=1, headers={}, - json={ - 'quota_set': { - 'backups': 15, - 'something_else': 20 - } - }) + json={'quota_set': {'backups': 15, 'something_else': 20}}, + ) def test_delete_quota(self): # Use QuotaSet as if it was returned by get(usage=True) @@ -157,7 +134,8 @@ def test_delete_quota(self): project_id='proj', reservation={'a': 'b'}, usage={'c': 'd'}, - foo='bar') + foo='bar', + ) resp = mock.Mock() resp.body = None diff --git a/openstack/tests/unit/common/test_tag.py b/openstack/tests/unit/common/test_tag.py index d27ffdca4..e4483e047 100644 --- a/openstack/tests/unit/common/test_tag.py +++ b/openstack/tests/unit/common/test_tag.py @@ -21,7 +21,6 @@ class TestTagMixin(base.TestCase): - def setUp(self): super(TestTagMixin, self).setUp() @@ -94,10 +93,7 @@ def test_set_tags(self): # Check the passed resource is returned self.assertEqual(res, result) url = self.base_path + '/' + res.id + '/tags' - sess.put.assert_called_once_with( - url, - json={'tags': ['blue', 'green']} - ) + sess.put.assert_called_once_with(url, json={'tags': ['blue', 'green']}) def test_remove_all_tags(self): res = self.sot diff --git a/openstack/tests/unit/config/base.py b/openstack/tests/unit/config/base.py index 5d353588a..2ed642d5f 100644 --- a/openstack/tests/unit/config/base.py +++ b/openstack/tests/unit/config/base.py @@ -48,10 +48,7 @@ 'force_ipv4': True, }, 'metrics': { - 'statsd': { - 'host': '127.0.0.1', - 'port': '1234' - }, + 'statsd': {'host': '127.0.0.1', 'port': '1234'}, 'influxdb': { 'host': '127.0.0.1', 'port': '1234', @@ -61,7 +58,7 @@ 'database': 'database', 'measurement': 'measurement.name', 'timeout': 10, - } + }, }, 'clouds': { '_test-cloud_': { @@ -112,30 +109,37 @@ 'domain_id': '6789', 'project_domain_id': '123456789', }, - 'networks': [{ - 'name': 'a-public', - 'routes_externally': True, - 'nat_source': True, - }, { - 'name': 'another-public', - 'routes_externally': True, - 'default_interface': True, - }, { - 'name': 'a-private', - 'routes_externally': False, - }, { - 'name': 'another-private', - 'routes_externally': False, - 'nat_destination': True, - }, { - 'name': 'split-default', - 'routes_externally': True, - 'routes_ipv4_externally': False, - }, { - 'name': 'split-no-default', - 'routes_ipv6_externally': False, - 'routes_ipv4_externally': True, - }], + 'networks': [ + { + 'name': 'a-public', + 'routes_externally': True, + 'nat_source': True, + }, + { + 'name': 'another-public', + 'routes_externally': True, + 'default_interface': True, + }, + { + 'name': 'a-private', + 'routes_externally': False, + }, + { + 'name': 'another-private', + 'routes_externally': False, + 'nat_destination': True, + }, + { + 'name': 'split-default', + 'routes_externally': True, + 'routes_ipv4_externally': False, + }, + { + 'name': 'split-no-default', + 'routes_ipv6_externally': False, + 'routes_ipv4_externally': True, + }, + ], 'region_name': 'test-region', }, '_test_cloud_regions': { @@ -150,13 +154,13 @@ 'name': 'region1', 'values': { 'external_network': 'region1-network', - } + }, }, { 'name': 'region2', 'values': { 'external_network': 'my-network', - } + }, }, { 'name': 'region-no-value', @@ -198,13 +202,13 @@ 'statsd': { 'host': '127.0.0.1', 'port': 4321, - 'prefix': 'statsd.override.prefix' + 'prefix': 'statsd.override.prefix', }, 'influxdb': { 'username': 'override-username', 'password': 'override-password', 'database': 'override-database', - } + }, }, }, }, diff --git a/openstack/tests/unit/config/test_cloud_config.py b/openstack/tests/unit/config/test_cloud_config.py index 098cee0cb..16dbc4501 100644 --- a/openstack/tests/unit/config/test_cloud_config.py +++ b/openstack/tests/unit/config/test_cloud_config.py @@ -40,7 +40,6 @@ class TestCloudRegion(base.TestCase): - def test_arbitrary_attributes(self): cc = cloud_region.CloudRegion("test1", "region-al", fake_config_dict) self.assertEqual("test1", cc.name) @@ -89,12 +88,10 @@ def test_get_config(self): self.assertIsNone(cc._get_config('nothing', None)) # This is what is happening behind the scenes in get_default_interface. self.assertEqual( - fake_services_dict['interface'], - cc._get_config('interface', None)) + fake_services_dict['interface'], cc._get_config('interface', None) + ) # The same call as above, but from one step up the stack - self.assertEqual( - fake_services_dict['interface'], - cc.get_interface()) + self.assertEqual(fake_services_dict['interface'], cc.get_interface()) # Which finally is what is called to populate the below self.assertEqual('public', self.cloud.default_interface) @@ -150,16 +147,21 @@ def test_cert_with_key(self): def test_ipv6(self): cc = cloud_region.CloudRegion( - "test1", "region-al", fake_config_dict, force_ipv4=True) + "test1", "region-al", fake_config_dict, force_ipv4=True + ) self.assertTrue(cc.force_ipv4) def test_getters(self): cc = cloud_region.CloudRegion("test1", "region-al", fake_services_dict) - self.assertEqual(['compute', 'identity', 'image', 'volume'], - sorted(cc.get_services())) - self.assertEqual({'password': 'hunter2', 'username': 'AzureDiamond'}, - cc.get_auth_args()) + self.assertEqual( + ['compute', 'identity', 'image', 'volume'], + sorted(cc.get_services()), + ) + self.assertEqual( + {'password': 'hunter2', 'username': 'AzureDiamond'}, + cc.get_auth_args(), + ) self.assertEqual('public', cc.get_interface()) self.assertEqual('public', cc.get_interface('compute')) self.assertEqual('admin', cc.get_interface('identity')) @@ -170,8 +172,9 @@ def test_getters(self): self.assertEqual('compute', cc.get_service_type('compute')) self.assertEqual('1', cc.get_api_version('volume')) self.assertEqual('block-storage', cc.get_service_type('volume')) - self.assertEqual('http://compute.example.com', - cc.get_endpoint('compute')) + self.assertEqual( + 'http://compute.example.com', cc.get_endpoint('compute') + ) self.assertIsNone(cc.get_endpoint('image')) self.assertIsNone(cc.get_service_name('compute')) self.assertEqual('locks', cc.get_service_name('identity')) @@ -184,38 +187,45 @@ def test_rackspace_workaround(self): # We're skipping loader here, so we have to expand relevant # parts from the rackspace profile. The thing we're testing # is that the project_id logic works. - cc = cloud_region.CloudRegion("test1", "DFW", { - 'profile': 'rackspace', - 'region_name': 'DFW', - 'auth': {'project_id': '123456'}, - 'block_storage_endpoint_override': 'https://example.com/v2/', - }) + cc = cloud_region.CloudRegion( + "test1", + "DFW", + { + 'profile': 'rackspace', + 'region_name': 'DFW', + 'auth': {'project_id': '123456'}, + 'block_storage_endpoint_override': 'https://example.com/v2/', + }, + ) self.assertEqual( - 'https://example.com/v2/123456', - cc.get_endpoint('block-storage') + 'https://example.com/v2/123456', cc.get_endpoint('block-storage') ) def test_rackspace_workaround_only_rax(self): - cc = cloud_region.CloudRegion("test1", "DFW", { - 'region_name': 'DFW', - 'auth': {'project_id': '123456'}, - 'block_storage_endpoint_override': 'https://example.com/v2/', - }) + cc = cloud_region.CloudRegion( + "test1", + "DFW", + { + 'region_name': 'DFW', + 'auth': {'project_id': '123456'}, + 'block_storage_endpoint_override': 'https://example.com/v2/', + }, + ) self.assertEqual( - 'https://example.com/v2/', - cc.get_endpoint('block-storage') + 'https://example.com/v2/', cc.get_endpoint('block-storage') ) def test_get_region_name(self): - def assert_region_name(default, compute): self.assertEqual(default, cc.region_name) self.assertEqual(default, cc.get_region_name()) self.assertEqual(default, cc.get_region_name(service_type=None)) self.assertEqual( - compute, cc.get_region_name(service_type='compute')) + compute, cc.get_region_name(service_type='compute') + ) self.assertEqual( - default, cc.get_region_name(service_type='placement')) + default, cc.get_region_name(service_type='placement') + ) # No region_name kwarg, no regions specified in services dict # (including the default). @@ -224,14 +234,17 @@ def assert_region_name(default, compute): # Only region_name kwarg; it's returned for everything cc = cloud_region.CloudRegion( - region_name='foo', config=fake_services_dict) + region_name='foo', config=fake_services_dict + ) assert_region_name('foo', 'foo') # No region_name kwarg; values (including default) show through from # config dict services_dict = dict( fake_services_dict, - region_name='the-default', compute_region_name='compute-region') + region_name='the-default', + compute_region_name='compute-region', + ) cc = cloud_region.CloudRegion(config=services_dict) assert_region_name('the-default', 'compute-region') @@ -239,9 +252,12 @@ def assert_region_name(default, compute): # compatibility), but service-specific region_name takes precedence. services_dict = dict( fake_services_dict, - region_name='dict', compute_region_name='compute-region') + region_name='dict', + compute_region_name='compute-region', + ) cc = cloud_region.CloudRegion( - region_name='kwarg', config=services_dict) + region_name='kwarg', config=services_dict + ) assert_region_name('kwarg', 'compute-region') def test_aliases(self): @@ -265,9 +281,7 @@ def test_get_session_no_auth(self): config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) cc = cloud_region.CloudRegion("test1", "region-al", config_dict) - self.assertRaises( - exceptions.ConfigException, - cc.get_session) + self.assertRaises(exceptions.ConfigException, cc.get_session) @mock.patch.object(ksa_session, 'Session') def test_get_session(self, mock_session): @@ -277,15 +291,21 @@ def test_get_session(self, mock_session): fake_session.additional_user_agent = [] mock_session.return_value = fake_session cc = cloud_region.CloudRegion( - "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + "test1", "region-al", config_dict, auth_plugin=mock.Mock() + ) cc.get_session() mock_session.assert_called_with( auth=mock.ANY, - verify=True, cert=None, timeout=None, collect_timing=None, - discovery_cache=None) + verify=True, + cert=None, + timeout=None, + collect_timing=None, + discovery_cache=None, + ) self.assertEqual( fake_session.additional_user_agent, - [('openstacksdk', openstack_version.__version__)]) + [('openstacksdk', openstack_version.__version__)], + ) @mock.patch.object(ksa_session, 'Session') def test_get_session_with_app_name(self, mock_session): @@ -297,18 +317,28 @@ def test_get_session_with_app_name(self, mock_session): fake_session.app_version = None mock_session.return_value = fake_session cc = cloud_region.CloudRegion( - "test1", "region-al", config_dict, auth_plugin=mock.Mock(), - app_name="test_app", app_version="test_version") + "test1", + "region-al", + config_dict, + auth_plugin=mock.Mock(), + app_name="test_app", + app_version="test_version", + ) cc.get_session() mock_session.assert_called_with( auth=mock.ANY, - verify=True, cert=None, timeout=None, collect_timing=None, - discovery_cache=None) + verify=True, + cert=None, + timeout=None, + collect_timing=None, + discovery_cache=None, + ) self.assertEqual(fake_session.app_name, "test_app") self.assertEqual(fake_session.app_version, "test_version") self.assertEqual( fake_session.additional_user_agent, - [('openstacksdk', openstack_version.__version__)]) + [('openstacksdk', openstack_version.__version__)], + ) @mock.patch.object(ksa_session, 'Session') def test_get_session_with_timeout(self, mock_session): @@ -319,15 +349,21 @@ def test_get_session_with_timeout(self, mock_session): config_dict.update(fake_services_dict) config_dict['api_timeout'] = 9 cc = cloud_region.CloudRegion( - "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + "test1", "region-al", config_dict, auth_plugin=mock.Mock() + ) cc.get_session() mock_session.assert_called_with( auth=mock.ANY, - verify=True, cert=None, timeout=9, - collect_timing=None, discovery_cache=None) + verify=True, + cert=None, + timeout=9, + collect_timing=None, + discovery_cache=None, + ) self.assertEqual( fake_session.additional_user_agent, - [('openstacksdk', openstack_version.__version__)]) + [('openstacksdk', openstack_version.__version__)], + ) @mock.patch.object(ksa_session, 'Session') def test_get_session_with_timing(self, mock_session): @@ -338,35 +374,45 @@ def test_get_session_with_timing(self, mock_session): config_dict.update(fake_services_dict) config_dict['timing'] = True cc = cloud_region.CloudRegion( - "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + "test1", "region-al", config_dict, auth_plugin=mock.Mock() + ) cc.get_session() mock_session.assert_called_with( auth=mock.ANY, - verify=True, cert=None, timeout=None, - collect_timing=True, discovery_cache=None) + verify=True, + cert=None, + timeout=None, + collect_timing=True, + discovery_cache=None, + ) self.assertEqual( fake_session.additional_user_agent, - [('openstacksdk', openstack_version.__version__)]) + [('openstacksdk', openstack_version.__version__)], + ) @mock.patch.object(ksa_session, 'Session') def test_override_session_endpoint_override(self, mock_session): config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) cc = cloud_region.CloudRegion( - "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + "test1", "region-al", config_dict, auth_plugin=mock.Mock() + ) self.assertEqual( cc.get_session_endpoint('compute'), - fake_services_dict['compute_endpoint_override']) + fake_services_dict['compute_endpoint_override'], + ) @mock.patch.object(ksa_session, 'Session') def test_override_session_endpoint(self, mock_session): config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) cc = cloud_region.CloudRegion( - "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + "test1", "region-al", config_dict, auth_plugin=mock.Mock() + ) self.assertEqual( cc.get_session_endpoint('telemetry'), - fake_services_dict['telemetry_endpoint']) + fake_services_dict['telemetry_endpoint'], + ) @mock.patch.object(cloud_region.CloudRegion, 'get_session') def test_session_endpoint(self, mock_get_session): @@ -375,20 +421,23 @@ def test_session_endpoint(self, mock_get_session): config_dict = defaults.get_defaults() config_dict.update(fake_services_dict) cc = cloud_region.CloudRegion( - "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + "test1", "region-al", config_dict, auth_plugin=mock.Mock() + ) cc.get_session_endpoint('orchestration') mock_session.get_endpoint.assert_called_with( interface='public', service_name=None, region_name='region-al', - service_type='orchestration') + service_type='orchestration', + ) @mock.patch.object(cloud_region.CloudRegion, 'get_session') def test_session_endpoint_not_found(self, mock_get_session): exc_to_raise = ksa_exceptions.catalog.EndpointNotFound mock_get_session.return_value.get_endpoint.side_effect = exc_to_raise cc = cloud_region.CloudRegion( - "test1", "region-al", {}, auth_plugin=mock.Mock()) + "test1", "region-al", {}, auth_plugin=mock.Mock() + ) self.assertIsNone(cc.get_session_endpoint('notfound')) def test_get_endpoint_from_catalog(self): @@ -396,14 +445,20 @@ def test_get_endpoint_from_catalog(self): self.cloud.config.config['dns_endpoint_override'] = dns_override self.assertEqual( 'https://compute.example.com/v2.1/', - self.cloud.config.get_endpoint_from_catalog('compute')) + self.cloud.config.get_endpoint_from_catalog('compute'), + ) self.assertEqual( 'https://internal.compute.example.com/v2.1/', self.cloud.config.get_endpoint_from_catalog( - 'compute', interface='internal')) + 'compute', interface='internal' + ), + ) self.assertIsNone( self.cloud.config.get_endpoint_from_catalog( - 'compute', region_name='unknown-region')) + 'compute', region_name='unknown-region' + ) + ) self.assertEqual( 'https://dns.example.com', - self.cloud.config.get_endpoint_from_catalog('dns')) + self.cloud.config.get_endpoint_from_catalog('dns'), + ) diff --git a/openstack/tests/unit/config/test_config.py b/openstack/tests/unit/config/test_config.py index bc31cc2ef..a9a7693b9 100644 --- a/openstack/tests/unit/config/test_config.py +++ b/openstack/tests/unit/config/test_config.py @@ -34,95 +34,112 @@ def prompt_for_password(prompt=None): class TestConfig(base.TestCase): - def test_get_all(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml], - secure_files=[self.no_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + secure_files=[self.no_yaml], + ) clouds = c.get_all() # We add two by hand because the regions cloud is going to exist # thrice since it has three regions in it - user_clouds = [ - cloud for cloud in base.USER_CONF['clouds'].keys() - ] + ['_test_cloud_regions', '_test_cloud_regions'] + user_clouds = [cloud for cloud in base.USER_CONF['clouds'].keys()] + [ + '_test_cloud_regions', + '_test_cloud_regions', + ] configured_clouds = [cloud.name for cloud in clouds] self.assertCountEqual(user_clouds, configured_clouds) def test_get_all_clouds(self): # Ensure the alias is in place - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml], - secure_files=[self.no_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + secure_files=[self.no_yaml], + ) clouds = c.get_all_clouds() # We add two by hand because the regions cloud is going to exist # thrice since it has three regions in it - user_clouds = [ - cloud for cloud in base.USER_CONF['clouds'].keys() - ] + ['_test_cloud_regions', '_test_cloud_regions'] + user_clouds = [cloud for cloud in base.USER_CONF['clouds'].keys()] + [ + '_test_cloud_regions', + '_test_cloud_regions', + ] configured_clouds = [cloud.name for cloud in clouds] self.assertCountEqual(user_clouds, configured_clouds) def test_get_one(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) cloud = c.get_one(validate=False) self.assertIsInstance(cloud, cloud_region.CloudRegion) self.assertEqual(cloud.name, '') def test_get_one_cloud(self): # Ensure the alias is in place - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) cloud = c.get_one_cloud(validate=False) self.assertIsInstance(cloud, cloud_region.CloudRegion) self.assertEqual(cloud.name, '') def test_get_one_default_cloud_from_file(self): - single_conf = base._write_yaml({ - 'clouds': { - 'single': { - 'auth': { - 'auth_url': 'http://example.com/v2', - 'username': 'testuser', - 'password': 'testpass', - 'project_name': 'testproject', - }, - 'region_name': 'test-region', + single_conf = base._write_yaml( + { + 'clouds': { + 'single': { + 'auth': { + 'auth_url': 'http://example.com/v2', + 'username': 'testuser', + 'password': 'testpass', + 'project_name': 'testproject', + }, + 'region_name': 'test-region', + } } } - }) - c = config.OpenStackConfig(config_files=[single_conf], - secure_files=[], - vendor_files=[self.vendor_yaml]) + ) + c = config.OpenStackConfig( + config_files=[single_conf], + secure_files=[], + vendor_files=[self.vendor_yaml], + ) cc = c.get_one() self.assertEqual(cc.name, 'single') def test_remote_profile(self): - single_conf = base._write_yaml({ - 'clouds': { - 'remote': { - 'profile': 'https://example.com', - 'auth': { - 'username': 'testuser', - 'password': 'testpass', - 'project_name': 'testproject', - }, - 'region_name': 'test-region', + single_conf = base._write_yaml( + { + 'clouds': { + 'remote': { + 'profile': 'https://example.com', + 'auth': { + 'username': 'testuser', + 'password': 'testpass', + 'project_name': 'testproject', + }, + 'region_name': 'test-region', + } } } - }) - self.register_uris([ - dict(method='GET', - uri='https://example.com/.well-known/openstack/api', - json={ - "name": "example", - "profile": { - "auth": { - "auth_url": "https://auth.example.com/v3", - } - } - }), - ]) + ) + self.register_uris( + [ + dict( + method='GET', + uri='https://example.com/.well-known/openstack/api', + json={ + "name": "example", + "profile": { + "auth": { + "auth_url": "https://auth.example.com/v3", + } + }, + }, + ), + ] + ) c = config.OpenStackConfig(config_files=[single_conf]) cc = c.get_one(cloud='remote') @@ -140,9 +157,11 @@ def test_get_one_auth_defaults(self): ) def test_get_one_with_config_files(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml], - secure_files=[self.secure_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + secure_files=[self.secure_yaml], + ) self.assertIsInstance(c.cloud_config, dict) self.assertIn('cache', c.cloud_config) self.assertIsInstance(c.cloud_config['cache'], dict) @@ -154,14 +173,16 @@ def test_get_one_with_config_files(self): self._assert_cloud_details(cc) def test_get_one_with_int_project_id(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) cc = c.get_one('_test-cloud-int-project_') self.assertEqual('12345', cc.auth['project_id']) def test_get_one_with_domain_id(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) cc = c.get_one('_test-cloud-domain-id_') self.assertEqual('6789', cc.auth['user_domain_id']) self.assertEqual('123456789', cc.auth['project_domain_id']) @@ -170,26 +191,31 @@ def test_get_one_with_domain_id(self): self.assertNotIn('domain_id', cc) def test_get_one_unscoped_identity(self): - single_conf = base._write_yaml({ - 'clouds': { - 'unscoped': { - 'auth': { - 'auth_url': 'http://example.com/v2', - 'username': 'testuser', - 'password': 'testpass', - }, + single_conf = base._write_yaml( + { + 'clouds': { + 'unscoped': { + 'auth': { + 'auth_url': 'http://example.com/v2', + 'username': 'testuser', + 'password': 'testpass', + }, + } } } - }) - c = config.OpenStackConfig(config_files=[single_conf], - secure_files=[], - vendor_files=[self.vendor_yaml]) + ) + c = config.OpenStackConfig( + config_files=[single_conf], + secure_files=[], + vendor_files=[self.vendor_yaml], + ) cc = c.get_one() self.assertEqual('http://example.com/v2', cc.get_endpoint('identity')) def test_get_one_domain_scoped(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) cc = c.get_one('_test-cloud-domain-scoped_') self.assertEqual('12345', cc.auth['domain_id']) self.assertNotIn('user_domain_id', cc.auth) @@ -197,8 +223,9 @@ def test_get_one_domain_scoped(self): self.assertIsNone(cc.get_endpoint('identity')) def test_get_one_infer_user_domain(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) cc = c.get_one('_test-cloud-int-project_') self.assertEqual('awesome-domain', cc.auth['user_domain_id']) self.assertEqual('awesome-domain', cc.auth['project_domain_id']) @@ -206,37 +233,41 @@ def test_get_one_infer_user_domain(self): self.assertNotIn('domain_id', cc) def test_get_one_infer_passcode(self): - single_conf = base._write_yaml({ - 'clouds': { - 'mfa': { - 'auth_type': 'v3multifactor', - 'auth_methods': ['v3password', 'v3totp'], - 'auth': { - 'auth_url': 'fake_url', - 'username': 'testuser', - 'password': 'testpass', - 'project_name': 'testproject', - 'project_domain_name': 'projectdomain', - 'user_domain_name': 'udn' - }, - 'region_name': 'test-region', + single_conf = base._write_yaml( + { + 'clouds': { + 'mfa': { + 'auth_type': 'v3multifactor', + 'auth_methods': ['v3password', 'v3totp'], + 'auth': { + 'auth_url': 'fake_url', + 'username': 'testuser', + 'password': 'testpass', + 'project_name': 'testproject', + 'project_domain_name': 'projectdomain', + 'user_domain_name': 'udn', + }, + 'region_name': 'test-region', + } } } - }) + ) c = config.OpenStackConfig(config_files=[single_conf]) cc = c.get_one(cloud='mfa', passcode='123') self.assertEqual('123', cc.auth['passcode']) def test_get_one_with_hyphenated_project_id(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) cc = c.get_one('_test_cloud_hyphenated') self.assertEqual('12345', cc.auth['project_id']) def test_get_one_with_hyphenated_kwargs(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) args = { 'auth': { 'username': 'testuser', @@ -250,43 +281,51 @@ def test_get_one_with_hyphenated_kwargs(self): self.assertEqual('http://example.com/v2', cc.auth['auth_url']) def test_no_environ(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) - self.assertRaises( - exceptions.ConfigException, c.get_one, 'envvars') + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) + self.assertRaises(exceptions.ConfigException, c.get_one, 'envvars') def test_fallthrough(self): - c = config.OpenStackConfig(config_files=[self.no_yaml], - vendor_files=[self.no_yaml], - secure_files=[self.no_yaml]) + c = config.OpenStackConfig( + config_files=[self.no_yaml], + vendor_files=[self.no_yaml], + secure_files=[self.no_yaml], + ) for k in os.environ.keys(): if k.startswith('OS_'): self.useFixture(fixtures.EnvironmentVariable(k)) c.get_one(cloud='defaults', validate=False) def test_prefer_ipv6_true(self): - c = config.OpenStackConfig(config_files=[self.no_yaml], - vendor_files=[self.no_yaml], - secure_files=[self.no_yaml]) + c = config.OpenStackConfig( + config_files=[self.no_yaml], + vendor_files=[self.no_yaml], + secure_files=[self.no_yaml], + ) cc = c.get_one(cloud='defaults', validate=False) self.assertTrue(cc.prefer_ipv6) def test_prefer_ipv6_false(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) cc = c.get_one(cloud='_test-cloud_') self.assertFalse(cc.prefer_ipv6) def test_force_ipv4_true(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) cc = c.get_one(cloud='_test-cloud_') self.assertTrue(cc.force_ipv4) def test_force_ipv4_false(self): - c = config.OpenStackConfig(config_files=[self.no_yaml], - vendor_files=[self.no_yaml], - secure_files=[self.no_yaml]) + c = config.OpenStackConfig( + config_files=[self.no_yaml], + vendor_files=[self.no_yaml], + secure_files=[self.no_yaml], + ) cc = c.get_one(cloud='defaults', validate=False) self.assertFalse(cc.force_ipv4) @@ -297,28 +336,34 @@ def test_get_one_auth_merge(self): self.assertEqual('testpass', cc.auth['password']) def test_get_one_networks(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) cc = c.get_one('_test-cloud-networks_') self.assertEqual( ['a-public', 'another-public', 'split-default'], - cc.get_external_networks()) + cc.get_external_networks(), + ) self.assertEqual( ['a-private', 'another-private', 'split-no-default'], - cc.get_internal_networks()) + cc.get_internal_networks(), + ) self.assertEqual('a-public', cc.get_nat_source()) self.assertEqual('another-private', cc.get_nat_destination()) self.assertEqual('another-public', cc.get_default_network()) self.assertEqual( ['a-public', 'another-public', 'split-no-default'], - cc.get_external_ipv4_networks()) + cc.get_external_ipv4_networks(), + ) self.assertEqual( ['a-public', 'another-public', 'split-default'], - cc.get_external_ipv6_networks()) + cc.get_external_ipv6_networks(), + ) def test_get_one_no_networks(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) cc = c.get_one('_test-cloud-domain-scoped_') self.assertEqual([], cc.get_external_networks()) self.assertEqual([], cc.get_internal_networks()) @@ -327,31 +372,38 @@ def test_get_one_no_networks(self): self.assertIsNone(cc.get_default_network()) def test_only_secure_yaml(self): - c = config.OpenStackConfig(config_files=['nonexistent'], - vendor_files=['nonexistent'], - secure_files=[self.secure_yaml]) + c = config.OpenStackConfig( + config_files=['nonexistent'], + vendor_files=['nonexistent'], + secure_files=[self.secure_yaml], + ) cc = c.get_one(cloud='_test_cloud_no_vendor', validate=False) self.assertEqual('testpass', cc.auth['password']) def test_get_cloud_names(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - secure_files=[self.no_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], secure_files=[self.no_yaml] + ) self.assertCountEqual( - ['_test-cloud-domain-id_', - '_test-cloud-domain-scoped_', - '_test-cloud-int-project_', - '_test-cloud-networks_', - '_test-cloud_', - '_test-cloud_no_region', - '_test_cloud_hyphenated', - '_test_cloud_no_vendor', - '_test_cloud_regions', - '_test-cloud-override-metrics', - ], - c.get_cloud_names()) - c = config.OpenStackConfig(config_files=[self.no_yaml], - vendor_files=[self.no_yaml], - secure_files=[self.no_yaml]) + [ + '_test-cloud-domain-id_', + '_test-cloud-domain-scoped_', + '_test-cloud-int-project_', + '_test-cloud-networks_', + '_test-cloud_', + '_test-cloud_no_region', + '_test_cloud_hyphenated', + '_test_cloud_no_vendor', + '_test_cloud_regions', + '_test-cloud-override-metrics', + ], + c.get_cloud_names(), + ) + c = config.OpenStackConfig( + config_files=[self.no_yaml], + vendor_files=[self.no_yaml], + secure_files=[self.no_yaml], + ) for k in os.environ.keys(): if k.startswith('OS_'): self.useFixture(fixtures.EnvironmentVariable(k)) @@ -365,16 +417,12 @@ def test_set_one_cloud_creates_file(self): config.OpenStackConfig.set_one_cloud(config_path, '_test_cloud_') self.assertTrue(os.path.isfile(config_path)) with open(config_path) as fh: - self.assertEqual({'clouds': {'_test_cloud_': {}}}, - yaml.safe_load(fh)) + self.assertEqual( + {'clouds': {'_test_cloud_': {}}}, yaml.safe_load(fh) + ) def test_set_one_cloud_updates_cloud(self): - new_config = { - 'cloud': 'new_cloud', - 'auth': { - 'password': 'newpass' - } - } + new_config = {'cloud': 'new_cloud', 'auth': {'password': 'newpass'}} resulting_cloud_config = { 'auth': { @@ -384,12 +432,13 @@ def test_set_one_cloud_updates_cloud(self): }, 'cloud': 'new_cloud', 'profile': '_test_cloud_in_our_cloud', - 'region_name': 'test-region' + 'region_name': 'test-region', } resulting_config = copy.deepcopy(base.USER_CONF) resulting_config['clouds']['_test-cloud_'] = resulting_cloud_config - config.OpenStackConfig.set_one_cloud(self.cloud_yaml, '_test-cloud_', - new_config) + config.OpenStackConfig.set_one_cloud( + self.cloud_yaml, '_test-cloud_', new_config + ) with open(self.cloud_yaml) as fh: written_config = yaml.safe_load(fh) # We write a cache config for testing @@ -397,120 +446,157 @@ def test_set_one_cloud_updates_cloud(self): self.assertEqual(written_config, resulting_config) def test_get_region_no_region_default(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml], - secure_files=[self.no_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + secure_files=[self.no_yaml], + ) region = c._get_region(cloud='_test-cloud_no_region') self.assertEqual(region, {'name': '', 'values': {}}) def test_get_region_no_region(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml], - secure_files=[self.no_yaml]) - region = c._get_region(cloud='_test-cloud_no_region', - region_name='override-region') + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + secure_files=[self.no_yaml], + ) + region = c._get_region( + cloud='_test-cloud_no_region', region_name='override-region' + ) self.assertEqual(region, {'name': 'override-region', 'values': {}}) def test_get_region_region_is_none(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml], - secure_files=[self.no_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + secure_files=[self.no_yaml], + ) region = c._get_region(cloud='_test-cloud_no_region', region_name=None) self.assertEqual(region, {'name': '', 'values': {}}) def test_get_region_region_set(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml], - secure_files=[self.no_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + secure_files=[self.no_yaml], + ) region = c._get_region(cloud='_test-cloud_', region_name='test-region') self.assertEqual(region, {'name': 'test-region', 'values': {}}) def test_get_region_many_regions_default(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml], - secure_files=[self.no_yaml]) - region = c._get_region(cloud='_test_cloud_regions', - region_name='') - self.assertEqual(region, {'name': 'region1', 'values': - {'external_network': 'region1-network'}}) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + secure_files=[self.no_yaml], + ) + region = c._get_region(cloud='_test_cloud_regions', region_name='') + self.assertEqual( + region, + { + 'name': 'region1', + 'values': {'external_network': 'region1-network'}, + }, + ) def test_get_region_many_regions(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml], - secure_files=[self.no_yaml]) - region = c._get_region(cloud='_test_cloud_regions', - region_name='region2') - self.assertEqual(region, {'name': 'region2', 'values': - {'external_network': 'my-network'}}) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + secure_files=[self.no_yaml], + ) + region = c._get_region( + cloud='_test_cloud_regions', region_name='region2' + ) + self.assertEqual( + region, + {'name': 'region2', 'values': {'external_network': 'my-network'}}, + ) def test_get_region_by_name_no_value(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) - region = c._get_region(cloud='_test_cloud_regions', - region_name='region-no-value') + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) + region = c._get_region( + cloud='_test_cloud_regions', region_name='region-no-value' + ) self.assertEqual(region, {'name': 'region-no-value', 'values': {}}) def test_get_region_invalid_region(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml], - secure_files=[self.no_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + secure_files=[self.no_yaml], + ) self.assertRaises( - exceptions.ConfigException, c._get_region, - cloud='_test_cloud_regions', region_name='invalid-region') + exceptions.ConfigException, + c._get_region, + cloud='_test_cloud_regions', + region_name='invalid-region', + ) def test_get_region_no_cloud(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml], - secure_files=[self.no_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + secure_files=[self.no_yaml], + ) region = c._get_region(region_name='no-cloud-region') self.assertEqual(region, {'name': 'no-cloud-region', 'values': {}}) def test_get_region_invalid_keys(self): - invalid_conf = base._write_yaml({ - 'clouds': { - '_test_cloud': { - 'profile': '_test_cloud_in_our_cloud', - 'auth': { - 'auth_url': 'http://example.com/v2', - 'username': 'testuser', - 'password': 'testpass', - }, - 'regions': [ - { - 'name': 'region1', - 'foo': 'bar' + invalid_conf = base._write_yaml( + { + 'clouds': { + '_test_cloud': { + 'profile': '_test_cloud_in_our_cloud', + 'auth': { + 'auth_url': 'http://example.com/v2', + 'username': 'testuser', + 'password': 'testpass', }, - ] + 'regions': [ + {'name': 'region1', 'foo': 'bar'}, + ], + } } } - }) - c = config.OpenStackConfig(config_files=[invalid_conf], - vendor_files=[self.vendor_yaml]) + ) + c = config.OpenStackConfig( + config_files=[invalid_conf], vendor_files=[self.vendor_yaml] + ) self.assertRaises( - exceptions.ConfigException, c._get_region, - cloud='_test_cloud', region_name='region1') + exceptions.ConfigException, + c._get_region, + cloud='_test_cloud', + region_name='region1', + ) @mock.patch('openstack.config.cloud_region.keyring') @mock.patch( - 'keystoneauth1.identity.base.BaseIdentityPlugin.set_auth_state') + 'keystoneauth1.identity.base.BaseIdentityPlugin.set_auth_state' + ) def test_load_auth_cache_not_found(self, ks_mock, kr_mock): c = config.OpenStackConfig( - config_files=[self.cloud_yaml], secure_files=[]) + config_files=[self.cloud_yaml], secure_files=[] + ) c._cache_auth = True kr_mock.get_password = mock.Mock(side_effect=[RuntimeError]) region = c.get_one('_test-cloud_') kr_mock.get_password.assert_called_with( - 'openstacksdk', region._auth.get_cache_id()) + 'openstacksdk', region._auth.get_cache_id() + ) ks_mock.assert_not_called() @mock.patch('openstack.config.cloud_region.keyring') @mock.patch( - 'keystoneauth1.identity.base.BaseIdentityPlugin.set_auth_state') + 'keystoneauth1.identity.base.BaseIdentityPlugin.set_auth_state' + ) def test_load_auth_cache_found(self, ks_mock, kr_mock): c = config.OpenStackConfig( - config_files=[self.cloud_yaml], secure_files=[]) + config_files=[self.cloud_yaml], secure_files=[] + ) c._cache_auth = True fake_auth = {'a': 'b'} @@ -518,13 +604,15 @@ def test_load_auth_cache_found(self, ks_mock, kr_mock): region = c.get_one('_test-cloud_') kr_mock.get_password.assert_called_with( - 'openstacksdk', region._auth.get_cache_id()) + 'openstacksdk', region._auth.get_cache_id() + ) ks_mock.assert_called_with(fake_auth) @mock.patch('openstack.config.cloud_region.keyring') def test_set_auth_cache(self, kr_mock): c = config.OpenStackConfig( - config_files=[self.cloud_yaml], secure_files=[]) + config_files=[self.cloud_yaml], secure_files=[] + ) c._cache_auth = True kr_mock.get_password = mock.Mock(side_effect=[RuntimeError]) @@ -534,13 +622,17 @@ def test_set_auth_cache(self, kr_mock): region.set_auth_cache() kr_mock.set_password.assert_called_with( - 'openstacksdk', region._auth.get_cache_id(), - region._auth.get_auth_state()) + 'openstacksdk', + region._auth.get_cache_id(), + region._auth.get_auth_state(), + ) def test_metrics_global(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml], - secure_files=[self.secure_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + secure_files=[self.secure_yaml], + ) self.assertIsInstance(c.cloud_config, dict) cc = c.get_one('_test-cloud_') statsd = { @@ -561,20 +653,22 @@ def test_metrics_global(self): 'password': 'password', 'database': 'database', 'measurement': 'measurement.name', - 'timeout': 10 + 'timeout': 10, } self.assertEqual(influxdb, cc._influxdb_config) def test_metrics_override(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml], - secure_files=[self.secure_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + secure_files=[self.secure_yaml], + ) self.assertIsInstance(c.cloud_config, dict) cc = c.get_one('_test-cloud-override-metrics') statsd = { 'host': '127.0.0.1', 'port': '4321', - 'prefix': 'statsd.override.prefix' + 'prefix': 'statsd.override.prefix', } self.assertEqual(statsd['host'], cc._statsd_host) self.assertEqual(statsd['port'], cc._statsd_port) @@ -587,7 +681,7 @@ def test_metrics_override(self): 'password': 'override-password', 'database': 'override-database', 'measurement': 'measurement.name', - 'timeout': 10 + 'timeout': 10, } self.assertEqual(influxdb, cc._influxdb_config) @@ -621,39 +715,44 @@ def setUp(self): self.options = argparse.Namespace(**self.args) def test_get_one_cloud_password_brace(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) - password = 'foo{' # Would raise ValueError, single brace + password = 'foo{' # Would raise ValueError, single brace self.options.password = password cc = c.get_one_cloud( - cloud='_test_cloud_regions', argparse=self.options, validate=False) + cloud='_test_cloud_regions', argparse=self.options, validate=False + ) self.assertEqual(cc.password, password) - password = 'foo{bar}' # Would raise KeyError, 'bar' not found + password = 'foo{bar}' # Would raise KeyError, 'bar' not found self.options.password = password cc = c.get_one_cloud( - cloud='_test_cloud_regions', argparse=self.options, validate=False) + cloud='_test_cloud_regions', argparse=self.options, validate=False + ) self.assertEqual(cc.password, password) def test_get_one_cloud_osc_password_brace(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) - password = 'foo{' # Would raise ValueError, single brace + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) + password = 'foo{' # Would raise ValueError, single brace self.options.password = password cc = c.get_one_cloud_osc( - cloud='_test_cloud_regions', argparse=self.options, validate=False) + cloud='_test_cloud_regions', argparse=self.options, validate=False + ) self.assertEqual(cc.password, password) - password = 'foo{bar}' # Would raise KeyError, 'bar' not found + password = 'foo{bar}' # Would raise KeyError, 'bar' not found self.options.password = password cc = c.get_one_cloud_osc( - cloud='_test_cloud_regions', argparse=self.options, validate=False) + cloud='_test_cloud_regions', argparse=self.options, validate=False + ) self.assertEqual(cc.password, password) class TestConfigArgparse(base.TestCase): - def setUp(self): super(TestConfigArgparse, self).setUp() @@ -670,25 +769,32 @@ def setUp(self): self.options = argparse.Namespace(**self.args) def test_get_one_bad_region_argparse(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) self.assertRaises( - exceptions.ConfigException, c.get_one, - cloud='_test-cloud_', argparse=self.options) + exceptions.ConfigException, + c.get_one, + cloud='_test-cloud_', + argparse=self.options, + ) def test_get_one_argparse(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) cc = c.get_one( - cloud='_test_cloud_regions', argparse=self.options, validate=False) + cloud='_test_cloud_regions', argparse=self.options, validate=False + ) self.assertEqual(cc.region_name, 'region2') self.assertEqual(cc.snack_type, 'cookie') def test_get_one_precedence(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) kwargs = { 'auth': { @@ -712,8 +818,7 @@ def test_get_one_precedence(self): ) options = argparse.Namespace(**args) - cc = c.get_one( - argparse=options, **kwargs) + cc = c.get_one(argparse=options, **kwargs) self.assertEqual(cc.region_name, 'region2') self.assertEqual(cc.auth['password'], 'authpass') self.assertEqual(cc.snack_type, 'cookie') @@ -746,17 +851,15 @@ def test_get_one_cloud_precedence_osc(self): ) options = argparse.Namespace(**args) - cc = c.get_one_cloud_osc( - argparse=options, - **kwargs - ) + cc = c.get_one_cloud_osc(argparse=options, **kwargs) self.assertEqual(cc.region_name, 'region2') self.assertEqual(cc.auth['password'], 'argpass') self.assertEqual(cc.snack_type, 'cookie') def test_get_one_precedence_no_argparse(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) kwargs = { 'auth': { @@ -776,8 +879,9 @@ def test_get_one_precedence_no_argparse(self): self.assertIsNone(cc.password) def test_get_one_just_argparse(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) cc = c.get_one(argparse=self.options, validate=False) self.assertIsNone(cc.cloud) @@ -785,8 +889,9 @@ def test_get_one_just_argparse(self): self.assertEqual(cc.snack_type, 'cookie') def test_get_one_just_kwargs(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) cc = c.get_one(validate=False, **self.args) self.assertIsNone(cc.cloud) @@ -794,8 +899,9 @@ def test_get_one_just_kwargs(self): self.assertEqual(cc.snack_type, 'cookie') def test_get_one_dash_kwargs(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) args = { 'auth-url': 'http://example.com/v2', @@ -811,8 +917,9 @@ def test_get_one_dash_kwargs(self): self.assertEqual(cc.snack_type, 'cookie') def test_get_one_no_argparse(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) cc = c.get_one(cloud='_test-cloud_', argparse=None) self._assert_cloud_details(cc) @@ -820,8 +927,9 @@ def test_get_one_no_argparse(self): self.assertIsNone(cc.snack_type) def test_get_one_no_argparse_regions(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) cc = c.get_one(cloud='_test_cloud_regions', argparse=None) self._assert_cloud_details(cc) @@ -829,48 +937,60 @@ def test_get_one_no_argparse_regions(self): self.assertIsNone(cc.snack_type) def test_get_one_bad_region(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) self.assertRaises( exceptions.ConfigException, c.get_one, - cloud='_test_cloud_regions', region_name='bad') + cloud='_test_cloud_regions', + region_name='bad', + ) def test_get_one_bad_region_no_regions(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) self.assertRaises( exceptions.ConfigException, c.get_one, - cloud='_test-cloud_', region_name='bad_region') + cloud='_test-cloud_', + region_name='bad_region', + ) def test_get_one_no_argparse_region2(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) cc = c.get_one( - cloud='_test_cloud_regions', region_name='region2', argparse=None) + cloud='_test_cloud_regions', region_name='region2', argparse=None + ) self._assert_cloud_details(cc) self.assertEqual(cc.region_name, 'region2') self.assertIsNone(cc.snack_type) def test_get_one_network(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) cc = c.get_one( - cloud='_test_cloud_regions', region_name='region1', argparse=None) + cloud='_test_cloud_regions', region_name='region1', argparse=None + ) self._assert_cloud_details(cc) self.assertEqual(cc.region_name, 'region1') self.assertEqual('region1-network', cc.config['external_network']) def test_get_one_per_region_network(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) cc = c.get_one( - cloud='_test_cloud_regions', region_name='region2', argparse=None) + cloud='_test_cloud_regions', region_name='region2', argparse=None + ) self._assert_cloud_details(cc) self.assertEqual(cc.region_name, 'region2') self.assertEqual('my-network', cc.config['external_network']) @@ -881,14 +1001,19 @@ def test_get_one_no_yaml_no_cloud(self): self.assertRaises( exceptions.ConfigException, c.get_one, - cloud='_test_cloud_regions', region_name='region2', argparse=None) + cloud='_test_cloud_regions', + region_name='region2', + argparse=None, + ) def test_get_one_no_yaml(self): c = config.OpenStackConfig(load_yaml_config=False) cc = c.get_one( - region_name='region2', argparse=None, - **base.USER_CONF['clouds']['_test_cloud_regions']) + region_name='region2', + argparse=None, + **base.USER_CONF['clouds']['_test_cloud_regions'] + ) # Not using assert_cloud_details because of cache settings which # are not present without the file self.assertIsInstance(cc, cloud_region.CloudRegion) @@ -907,8 +1032,9 @@ def test_get_one_no_yaml(self): self.assertEqual(cc.region_name, 'region2') def test_fix_env_args(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) env_args = {'os-compute-api-version': 1} fixed_args = c._fix_args(env_args) @@ -916,8 +1042,9 @@ def test_fix_env_args(self): self.assertDictEqual({'compute_api_version': 1}, fixed_args) def test_extra_config(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) defaults = {'use_hostnames': False, 'other-value': 'something'} ansible_options = c.get_extra_config('ansible', defaults) @@ -934,14 +1061,15 @@ def test_extra_config(self): 'use_hostnames': True, 'other_value': 'something', }, - ansible_options) + ansible_options, + ) def test_get_client_config(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) - cc = c.get_one( - cloud='_test_cloud_regions') + cc = c.get_one(cloud='_test_cloud_regions') defaults = { 'use_hostnames': False, @@ -964,64 +1092,77 @@ def test_get_client_config(self): 'other_value': 'something', 'force_ipv4': True, }, - ansible_options) + ansible_options, + ) def test_register_argparse_cloud(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) parser = argparse.ArgumentParser() c.register_argparse_arguments(parser, []) opts, _remain = parser.parse_known_args(['--os-cloud', 'foo']) self.assertEqual(opts.os_cloud, 'foo') def test_env_argparse_precedence(self): - self.useFixture(fixtures.EnvironmentVariable( - 'OS_TENANT_NAME', 'tenants-are-bad')) - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + self.useFixture( + fixtures.EnvironmentVariable('OS_TENANT_NAME', 'tenants-are-bad') + ) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) - cc = c.get_one( - cloud='envvars', argparse=self.options, validate=False) + cc = c.get_one(cloud='envvars', argparse=self.options, validate=False) self.assertEqual(cc.auth['project_name'], 'project') def test_argparse_default_no_token(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) parser = argparse.ArgumentParser() c.register_argparse_arguments(parser, []) # novaclient will add this parser.add_argument('--os-auth-token') opts, _remain = parser.parse_known_args() - cc = c.get_one( - cloud='_test_cloud_regions', argparse=opts) + cc = c.get_one(cloud='_test_cloud_regions', argparse=opts) self.assertEqual(cc.config['auth_type'], 'password') self.assertNotIn('token', cc.config['auth']) def test_argparse_token(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) parser = argparse.ArgumentParser() c.register_argparse_arguments(parser, []) # novaclient will add this parser.add_argument('--os-auth-token') opts, _remain = parser.parse_known_args( - ['--os-auth-token', 'very-bad-things', - '--os-auth-type', 'token']) + ['--os-auth-token', 'very-bad-things', '--os-auth-type', 'token'] + ) cc = c.get_one(argparse=opts, validate=False) self.assertEqual(cc.config['auth_type'], 'token') self.assertEqual(cc.config['auth']['token'], 'very-bad-things') def test_argparse_underscores(self): - c = config.OpenStackConfig(config_files=[self.no_yaml], - vendor_files=[self.no_yaml], - secure_files=[self.no_yaml]) + c = config.OpenStackConfig( + config_files=[self.no_yaml], + vendor_files=[self.no_yaml], + secure_files=[self.no_yaml], + ) parser = argparse.ArgumentParser() parser.add_argument('--os_username') argv = [ - '--os_username', 'user', '--os_password', 'pass', - '--os-auth-url', 'auth-url', '--os-project-name', 'project'] + '--os_username', + 'user', + '--os_password', + 'pass', + '--os-auth-url', + 'auth-url', + '--os-project-name', + 'project', + ] c.register_argparse_arguments(parser, argv=argv) opts, _remain = parser.parse_known_args(argv) cc = c.get_one(argparse=opts) @@ -1030,9 +1171,11 @@ def test_argparse_underscores(self): self.assertEqual(cc.config['auth']['auth_url'], 'auth-url') def test_argparse_action_append_no_underscore(self): - c = config.OpenStackConfig(config_files=[self.no_yaml], - vendor_files=[self.no_yaml], - secure_files=[self.no_yaml]) + c = config.OpenStackConfig( + config_files=[self.no_yaml], + vendor_files=[self.no_yaml], + secure_files=[self.no_yaml], + ) parser = argparse.ArgumentParser() parser.add_argument('--foo', action='append') argv = ['--foo', '1', '--foo', '2'] @@ -1041,47 +1184,69 @@ def test_argparse_action_append_no_underscore(self): self.assertEqual(opts.foo, ['1', '2']) def test_argparse_underscores_duplicate(self): - c = config.OpenStackConfig(config_files=[self.no_yaml], - vendor_files=[self.no_yaml], - secure_files=[self.no_yaml]) + c = config.OpenStackConfig( + config_files=[self.no_yaml], + vendor_files=[self.no_yaml], + secure_files=[self.no_yaml], + ) parser = argparse.ArgumentParser() parser.add_argument('--os_username') argv = [ - '--os_username', 'user', '--os_password', 'pass', - '--os-username', 'user1', '--os-password', 'pass1', - '--os-auth-url', 'auth-url', '--os-project-name', 'project'] + '--os_username', + 'user', + '--os_password', + 'pass', + '--os-username', + 'user1', + '--os-password', + 'pass1', + '--os-auth-url', + 'auth-url', + '--os-project-name', + 'project', + ] self.assertRaises( exceptions.ConfigException, c.register_argparse_arguments, - parser=parser, argv=argv) + parser=parser, + argv=argv, + ) def test_register_argparse_bad_plugin(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) parser = argparse.ArgumentParser() self.assertRaises( exceptions.ConfigException, c.register_argparse_arguments, - parser, ['--os-auth-type', 'foo']) + parser, + ['--os-auth-type', 'foo'], + ) def test_register_argparse_not_password(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) parser = argparse.ArgumentParser() args = [ - '--os-auth-type', 'v3token', - '--os-token', 'some-secret', + '--os-auth-type', + 'v3token', + '--os-token', + 'some-secret', ] c.register_argparse_arguments(parser, args) opts, _remain = parser.parse_known_args(args) self.assertEqual(opts.os_token, 'some-secret') def test_register_argparse_password(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) parser = argparse.ArgumentParser() args = [ - '--os-password', 'some-secret', + '--os-password', + 'some-secret', ] c.register_argparse_arguments(parser, args) opts, _remain = parser.parse_known_args(args) @@ -1090,13 +1255,17 @@ def test_register_argparse_password(self): opts.os_token def test_register_argparse_service_type(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) parser = argparse.ArgumentParser() args = [ - '--os-service-type', 'network', - '--os-endpoint-type', 'admin', - '--http-timeout', '20', + '--os-service-type', + 'network', + '--os-endpoint-type', + 'admin', + '--http-timeout', + '20', ] c.register_argparse_arguments(parser, args) opts, _remain = parser.parse_known_args(args) @@ -1112,12 +1281,15 @@ def test_register_argparse_service_type(self): self.assertNotIn('http_timeout', cloud.config) def test_register_argparse_network_service_type(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) parser = argparse.ArgumentParser() args = [ - '--os-endpoint-type', 'admin', - '--network-api-version', '4', + '--os-endpoint-type', + 'admin', + '--network-api-version', + '4', ] c.register_argparse_arguments(parser, args, ['network']) opts, _remain = parser.parse_known_args(args) @@ -1133,17 +1305,23 @@ def test_register_argparse_network_service_type(self): self.assertNotIn('http_timeout', cloud.config) def test_register_argparse_network_service_types(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) parser = argparse.ArgumentParser() args = [ - '--os-compute-service-name', 'cloudServers', - '--os-network-service-type', 'badtype', - '--os-endpoint-type', 'admin', - '--network-api-version', '4', + '--os-compute-service-name', + 'cloudServers', + '--os-network-service-type', + 'badtype', + '--os-endpoint-type', + 'admin', + '--network-api-version', + '4', ] c.register_argparse_arguments( - parser, args, ['compute', 'network', 'volume']) + parser, args, ['compute', 'network', 'volume'] + ) opts, _remain = parser.parse_known_args(args) self.assertEqual(opts.os_network_service_type, 'badtype') self.assertIsNone(opts.os_compute_service_type) @@ -1163,7 +1341,6 @@ def test_register_argparse_network_service_types(self): class TestConfigPrompt(base.TestCase): - def setUp(self): super(TestConfigPrompt, self).setUp() @@ -1195,7 +1372,6 @@ def test_get_one_prompt(self): class TestConfigDefault(base.TestCase): - def setUp(self): super(TestConfigDefault, self).setUp() @@ -1207,18 +1383,19 @@ def _reset_defaults(self): defaults._defaults = None def test_set_no_default(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) cc = c.get_one(cloud='_test-cloud_', argparse=None) self._assert_cloud_details(cc) self.assertEqual('password', cc.auth_type) class TestBackwardsCompatibility(base.TestCase): - def test_set_no_default(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) cloud = { 'identity_endpoint_type': 'admin', 'compute_endpoint_type': 'private', @@ -1235,61 +1412,65 @@ def test_set_no_default(self): self.assertDictEqual(expected, result) def test_project_v2password(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) cloud = { 'auth_type': 'v2password', 'auth': { 'project-name': 'my_project_name', - 'project-id': 'my_project_id' - } + 'project-id': 'my_project_id', + }, } result = c._fix_backwards_project(cloud) expected = { 'auth_type': 'v2password', 'auth': { 'tenant_name': 'my_project_name', - 'tenant_id': 'my_project_id' - } + 'tenant_id': 'my_project_id', + }, } self.assertEqual(expected, result) def test_project_password(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) cloud = { 'auth_type': 'password', 'auth': { 'project-name': 'my_project_name', - 'project-id': 'my_project_id' - } + 'project-id': 'my_project_id', + }, } result = c._fix_backwards_project(cloud) expected = { 'auth_type': 'password', 'auth': { 'project_name': 'my_project_name', - 'project_id': 'my_project_id' - } + 'project_id': 'my_project_id', + }, } self.assertEqual(expected, result) def test_backwards_network_fail(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) cloud = { 'external_network': 'public', 'networks': [ {'name': 'private', 'routes_externally': False}, - ] + ], } self.assertRaises( - exceptions.ConfigException, - c._fix_backwards_networks, cloud) + exceptions.ConfigException, c._fix_backwards_networks, cloud + ) def test_backwards_network(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) cloud = { 'external_network': 'public', 'internal_network': 'private', @@ -1299,37 +1480,47 @@ def test_backwards_network(self): 'external_network': 'public', 'internal_network': 'private', 'networks': [ - {'name': 'public', 'routes_externally': True, - 'nat_destination': False, 'default_interface': True}, - {'name': 'private', 'routes_externally': False, - 'nat_destination': True, 'default_interface': False}, - ] + { + 'name': 'public', + 'routes_externally': True, + 'nat_destination': False, + 'default_interface': True, + }, + { + 'name': 'private', + 'routes_externally': False, + 'nat_destination': True, + 'default_interface': False, + }, + ], } self.assertEqual(expected, result) def test_normalize_network(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) - cloud = { - 'networks': [ - {'name': 'private'} - ] - } + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) + cloud = {'networks': [{'name': 'private'}]} result = c._fix_backwards_networks(cloud) expected = { 'networks': [ - {'name': 'private', 'routes_externally': False, - 'nat_destination': False, 'default_interface': False, - 'nat_source': False, - 'routes_ipv4_externally': False, - 'routes_ipv6_externally': False}, + { + 'name': 'private', + 'routes_externally': False, + 'nat_destination': False, + 'default_interface': False, + 'nat_source': False, + 'routes_ipv4_externally': False, + 'routes_ipv6_externally': False, + }, ] } self.assertEqual(expected, result) def test_single_default_interface(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) cloud = { 'networks': [ {'name': 'blue', 'default_interface': True}, @@ -1337,5 +1528,5 @@ def test_single_default_interface(self): ] } self.assertRaises( - exceptions.ConfigException, - c._fix_backwards_networks, cloud) + exceptions.ConfigException, c._fix_backwards_networks, cloud + ) diff --git a/openstack/tests/unit/config/test_environ.py b/openstack/tests/unit/config/test_environ.py index da02bb562..82ed13eea 100644 --- a/openstack/tests/unit/config/test_environ.py +++ b/openstack/tests/unit/config/test_environ.py @@ -21,52 +21,64 @@ class TestEnviron(base.TestCase): - def setUp(self): super(TestEnviron, self).setUp() self.useFixture( - fixtures.EnvironmentVariable('OS_AUTH_URL', 'https://example.com')) + fixtures.EnvironmentVariable('OS_AUTH_URL', 'https://example.com') + ) self.useFixture( - fixtures.EnvironmentVariable('OS_USERNAME', 'testuser')) + fixtures.EnvironmentVariable('OS_USERNAME', 'testuser') + ) self.useFixture( - fixtures.EnvironmentVariable('OS_PASSWORD', 'testpass')) + fixtures.EnvironmentVariable('OS_PASSWORD', 'testpass') + ) self.useFixture( - fixtures.EnvironmentVariable('OS_PROJECT_NAME', 'testproject')) + fixtures.EnvironmentVariable('OS_PROJECT_NAME', 'testproject') + ) self.useFixture( - fixtures.EnvironmentVariable('NOVA_PROJECT_ID', 'testnova')) + fixtures.EnvironmentVariable('NOVA_PROJECT_ID', 'testnova') + ) def test_get_one(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) self.assertIsInstance(c.get_one(), cloud_region.CloudRegion) def test_no_fallthrough(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) - self.assertRaises( - exceptions.ConfigException, c.get_one, 'openstack') + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) + self.assertRaises(exceptions.ConfigException, c.get_one, 'openstack') def test_envvar_name_override(self): self.useFixture( - fixtures.EnvironmentVariable('OS_CLOUD_NAME', 'override')) - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + fixtures.EnvironmentVariable('OS_CLOUD_NAME', 'override') + ) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) cc = c.get_one('override') self._assert_cloud_details(cc) def test_envvar_prefer_ipv6_override(self): self.useFixture( - fixtures.EnvironmentVariable('OS_PREFER_IPV6', 'false')) - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml], - secure_files=[self.secure_yaml]) + fixtures.EnvironmentVariable('OS_PREFER_IPV6', 'false') + ) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + secure_files=[self.secure_yaml], + ) cc = c.get_one('_test-cloud_') self.assertFalse(cc.prefer_ipv6) def test_environ_exists(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml], - secure_files=[self.secure_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + secure_files=[self.secure_yaml], + ) cc = c.get_one('envvars') self._assert_cloud_details(cc) self.assertNotIn('auth_url', cc.config) @@ -79,10 +91,12 @@ def test_environ_exists(self): self._assert_cloud_details(cc) def test_environ_prefix(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml], - envvar_prefix='NOVA_', - secure_files=[self.secure_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + envvar_prefix='NOVA_', + secure_files=[self.secure_yaml], + ) cc = c.get_one('envvars') self._assert_cloud_details(cc) self.assertNotIn('auth_url', cc.config) @@ -95,9 +109,11 @@ def test_environ_prefix(self): self._assert_cloud_details(cc) def test_get_one_with_config_files(self): - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml], - secure_files=[self.secure_yaml]) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + secure_files=[self.secure_yaml], + ) self.assertIsInstance(c.cloud_config, dict) self.assertIn('cache', c.cloud_config) self.assertIsInstance(c.cloud_config['cache'], dict) @@ -111,40 +127,40 @@ def test_get_one_with_config_files(self): def test_config_file_override(self): self.useFixture( fixtures.EnvironmentVariable( - 'OS_CLIENT_CONFIG_FILE', self.cloud_yaml)) - c = config.OpenStackConfig(config_files=[], - vendor_files=[self.vendor_yaml]) + 'OS_CLIENT_CONFIG_FILE', self.cloud_yaml + ) + ) + c = config.OpenStackConfig( + config_files=[], vendor_files=[self.vendor_yaml] + ) cc = c.get_one('_test-cloud_') self._assert_cloud_details(cc) class TestEnvvars(base.TestCase): - def test_no_envvars(self): - self.useFixture( - fixtures.EnvironmentVariable('NOVA_USERNAME', 'nova')) - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) - self.assertRaises( - exceptions.ConfigException, c.get_one, 'envvars') + self.useFixture(fixtures.EnvironmentVariable('NOVA_USERNAME', 'nova')) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) + self.assertRaises(exceptions.ConfigException, c.get_one, 'envvars') def test_test_envvars(self): + self.useFixture(fixtures.EnvironmentVariable('NOVA_USERNAME', 'nova')) self.useFixture( - fixtures.EnvironmentVariable('NOVA_USERNAME', 'nova')) - self.useFixture( - fixtures.EnvironmentVariable('OS_STDERR_CAPTURE', 'True')) - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) - self.assertRaises( - exceptions.ConfigException, c.get_one, 'envvars') + fixtures.EnvironmentVariable('OS_STDERR_CAPTURE', 'True') + ) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) + self.assertRaises(exceptions.ConfigException, c.get_one, 'envvars') def test_incomplete_envvars(self): - self.useFixture( - fixtures.EnvironmentVariable('NOVA_USERNAME', 'nova')) - self.useFixture( - fixtures.EnvironmentVariable('OS_USERNAME', 'user')) - config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + self.useFixture(fixtures.EnvironmentVariable('NOVA_USERNAME', 'nova')) + self.useFixture(fixtures.EnvironmentVariable('OS_USERNAME', 'user')) + config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) # This is broken due to an issue that's fixed in a subsequent patch # commenting it out in this patch to keep the patch size reasonable # self.assertRaises( @@ -152,33 +168,38 @@ def test_incomplete_envvars(self): # c.get_one, 'envvars') def test_have_envvars(self): + self.useFixture(fixtures.EnvironmentVariable('NOVA_USERNAME', 'nova')) self.useFixture( - fixtures.EnvironmentVariable('NOVA_USERNAME', 'nova')) - self.useFixture( - fixtures.EnvironmentVariable('OS_AUTH_URL', 'http://example.com')) - self.useFixture( - fixtures.EnvironmentVariable('OS_USERNAME', 'user')) + fixtures.EnvironmentVariable('OS_AUTH_URL', 'http://example.com') + ) + self.useFixture(fixtures.EnvironmentVariable('OS_USERNAME', 'user')) self.useFixture( - fixtures.EnvironmentVariable('OS_PASSWORD', 'password')) + fixtures.EnvironmentVariable('OS_PASSWORD', 'password') + ) self.useFixture( - fixtures.EnvironmentVariable('OS_PROJECT_NAME', 'project')) - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml]) + fixtures.EnvironmentVariable('OS_PROJECT_NAME', 'project') + ) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) cc = c.get_one('envvars') self.assertEqual(cc.config['auth']['username'], 'user') def test_old_envvars(self): + self.useFixture(fixtures.EnvironmentVariable('NOVA_USERNAME', 'nova')) self.useFixture( - fixtures.EnvironmentVariable('NOVA_USERNAME', 'nova')) - self.useFixture( - fixtures.EnvironmentVariable( - 'NOVA_AUTH_URL', 'http://example.com')) + fixtures.EnvironmentVariable('NOVA_AUTH_URL', 'http://example.com') + ) self.useFixture( - fixtures.EnvironmentVariable('NOVA_PASSWORD', 'password')) + fixtures.EnvironmentVariable('NOVA_PASSWORD', 'password') + ) self.useFixture( - fixtures.EnvironmentVariable('NOVA_PROJECT_NAME', 'project')) - c = config.OpenStackConfig(config_files=[self.cloud_yaml], - vendor_files=[self.vendor_yaml], - envvar_prefix='NOVA_') + fixtures.EnvironmentVariable('NOVA_PROJECT_NAME', 'project') + ) + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], + vendor_files=[self.vendor_yaml], + envvar_prefix='NOVA_', + ) cc = c.get_one('envvars') self.assertEqual(cc.config['auth']['username'], 'nova') diff --git a/openstack/tests/unit/config/test_from_conf.py b/openstack/tests/unit/config/test_from_conf.py index 31a75b46d..3f89b2832 100644 --- a/openstack/tests/unit/config/test_from_conf.py +++ b/openstack/tests/unit/config/test_from_conf.py @@ -23,13 +23,15 @@ class TestFromConf(base.TestCase): - def _get_conn(self, **from_conf_kwargs): oslocfg = self._load_ks_cfg_opts() # Throw name in here to prove **kwargs is working config = cloud_region.from_conf( - oslocfg, session=self.cloud.session, name='from_conf.example.com', - **from_conf_kwargs) + oslocfg, + session=self.cloud.session, + name='from_conf.example.com', + **from_conf_kwargs + ) self.assertEqual('from_conf.example.com', config.name) return connection.Connection(config=config, strict_proxies=True) @@ -41,33 +43,48 @@ def test_adapter_opts_set(self): discovery = { "versions": { "values": [ - {"status": "stable", - "updated": "2019-06-01T00:00:00Z", - "media-types": [{ - "base": "application/json", - "type": "application/vnd.openstack.heat-v2+json"}], - "id": "v2.0", - "links": [{ - "href": "https://example.org:8888/heat/v2", - "rel": "self"}] - }] + { + "status": "stable", + "updated": "2019-06-01T00:00:00Z", + "media-types": [ + { + "base": "application/json", + "type": "application/vnd.openstack.heat-v2+json", # noqa: E501 + } + ], + "id": "v2.0", + "links": [ + { + "href": "https://example.org:8888/heat/v2", + "rel": "self", + } + ], + } + ] } } - self.register_uris([ - dict(method='GET', - uri='https://example.org:8888/heat/v2', - json=discovery), - dict(method='GET', - uri='https://example.org:8888/heat/v2/foo', - json={'foo': {}}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://example.org:8888/heat/v2', + json=discovery, + ), + dict( + method='GET', + uri='https://example.org:8888/heat/v2/foo', + json={'foo': {}}, + ), + ] + ) adap = conn.orchestration self.assertEqual('SpecialRegion', adap.region_name) self.assertEqual('orchestration', adap.service_type) self.assertEqual('internal', adap.interface) - self.assertEqual('https://example.org:8888/heat/v2', - adap.endpoint_override) + self.assertEqual( + 'https://example.org:8888/heat/v2', adap.endpoint_override + ) adap.get('/foo') self.assert_calls() @@ -80,13 +97,18 @@ def test_default_adapter_opts(self): server_name = self.getUniqueString('name') fake_server = fakes.make_fake_server(server_id, server_name) - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': [fake_server]}), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + json={'servers': [fake_server]}, + ), + ] + ) # Nova has empty adapter config, so these default adap = conn.compute @@ -108,20 +130,27 @@ def test_service_not_ready_catalog(self): server_name = self.getUniqueString('name') fake_server = fakes.make_fake_server(server_id, server_name) - self.register_uris([ - dict(method='GET', - uri='https://compute.example.com/v2.1/', - exc=requests.exceptions.ConnectionError), - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': [fake_server]}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://compute.example.com/v2.1/', + exc=requests.exceptions.ConnectionError, + ), + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + json={'servers': [fake_server]}, + ), + ] + ) self.assertRaises( - exceptions.ServiceDiscoveryException, - getattr, conn, 'compute') + exceptions.ServiceDiscoveryException, getattr, conn, 'compute' + ) # Nova has empty adapter config, so these default adap = conn.compute @@ -141,31 +170,41 @@ def test_name_with_dashes(self): discovery = { "versions": { "values": [ - {"status": "stable", - "id": "v1", - "links": [{ - "href": "https://example.org:5050/v1", - "rel": "self"}] - }] + { + "status": "stable", + "id": "v1", + "links": [ + { + "href": "https://example.org:5050/v1", + "rel": "self", + } + ], + } + ] } } - status = { - 'finished': True, - 'error': None - } - self.register_uris([ - dict(method='GET', - uri='https://example.org:5050', - json=discovery), - # strict-proxies means we're going to fetch the discovery - # doc from the versioned endpoint to verify it works. - dict(method='GET', - uri='https://example.org:5050/v1', - json=discovery), - dict(method='GET', - uri='https://example.org:5050/v1/introspection/abcd', - json=status), - ]) + status = {'finished': True, 'error': None} + self.register_uris( + [ + dict( + method='GET', + uri='https://example.org:5050', + json=discovery, + ), + # strict-proxies means we're going to fetch the discovery + # doc from the versioned endpoint to verify it works. + dict( + method='GET', + uri='https://example.org:5050/v1', + json=discovery, + ), + dict( + method='GET', + uri='https://example.org:5050/v1/introspection/abcd', + json=status, + ), + ] + ) adap = conn.baremetal_introspection self.assertEqual('baremetal-introspection', adap.service_type) @@ -180,38 +219,53 @@ def test_service_not_ready_endpoint_override(self): discovery = { "versions": { "values": [ - {"status": "stable", - "id": "v1", - "links": [{ - "href": "https://example.org:5050/v1", - "rel": "self"}] - }] + { + "status": "stable", + "id": "v1", + "links": [ + { + "href": "https://example.org:5050/v1", + "rel": "self", + } + ], + } + ] } } - status = { - 'finished': True, - 'error': None - } - self.register_uris([ - dict(method='GET', - uri='https://example.org:5050', - exc=requests.exceptions.ConnectTimeout), - dict(method='GET', - uri='https://example.org:5050', - json=discovery), - # strict-proxies means we're going to fetch the discovery - # doc from the versioned endpoint to verify it works. - dict(method='GET', - uri='https://example.org:5050/v1', - json=discovery), - dict(method='GET', - uri='https://example.org:5050/v1/introspection/abcd', - json=status), - ]) + status = {'finished': True, 'error': None} + self.register_uris( + [ + dict( + method='GET', + uri='https://example.org:5050', + exc=requests.exceptions.ConnectTimeout, + ), + dict( + method='GET', + uri='https://example.org:5050', + json=discovery, + ), + # strict-proxies means we're going to fetch the discovery + # doc from the versioned endpoint to verify it works. + dict( + method='GET', + uri='https://example.org:5050/v1', + json=discovery, + ), + dict( + method='GET', + uri='https://example.org:5050/v1/introspection/abcd', + json=status, + ), + ] + ) self.assertRaises( exceptions.ServiceDiscoveryException, - getattr, conn, 'baremetal_introspection') + getattr, + conn, + 'baremetal_introspection', + ) adap = conn.baremetal_introspection self.assertEqual('baremetal-introspection', adap.service_type) @@ -220,16 +274,21 @@ def test_service_not_ready_endpoint_override(self): self.assertTrue(adap.get_introspection('abcd').is_finished) - def assert_service_disabled(self, service_type, expected_reason, - **from_conf_kwargs): + def assert_service_disabled( + self, service_type, expected_reason, **from_conf_kwargs + ): conn = self._get_conn(**from_conf_kwargs) # The _ServiceDisabledProxyShim loads up okay... adap = getattr(conn, service_type) # ...but freaks out if you try to use it. ex = self.assertRaises( - exceptions.ServiceDisabledException, getattr, adap, 'get') - self.assertIn("Service '%s' is disabled because its configuration " - "could not be loaded." % service_type, ex.message) + exceptions.ServiceDisabledException, getattr, adap, 'get' + ) + self.assertIn( + "Service '%s' is disabled because its configuration " + "could not be loaded." % service_type, + ex.message, + ) self.assertIn(expected_reason, ex.message) def test_no_such_conf_section(self): @@ -238,15 +297,18 @@ def test_no_such_conf_section(self): self.assert_service_disabled( 'orchestration', "No section for project 'heat' (service type 'orchestration') was " - "present in the config.") + "present in the config.", + ) def test_no_such_conf_section_ignore_service_type(self): """Ignore absent conf section if service type not requested.""" del self.oslo_config_dict['heat'] self.assert_service_disabled( - 'orchestration', "Not in the list of requested service_types.", + 'orchestration', + "Not in the list of requested service_types.", # 'orchestration' absent from this list - service_types=['compute']) + service_types=['compute'], + ) def test_no_adapter_opts(self): """Conf section present, but opts for service type not registered.""" @@ -254,15 +316,18 @@ def test_no_adapter_opts(self): self.assert_service_disabled( 'orchestration', "Encountered an exception attempting to process config for " - "project 'heat' (service type 'orchestration'): no such option") + "project 'heat' (service type 'orchestration'): no such option", + ) def test_no_adapter_opts_ignore_service_type(self): """Ignore unregistered conf section if service type not requested.""" self.oslo_config_dict['heat'] = None self.assert_service_disabled( - 'orchestration', "Not in the list of requested service_types.", + 'orchestration', + "Not in the list of requested service_types.", # 'orchestration' absent from this list - service_types=['compute']) + service_types=['compute'], + ) def test_invalid_adapter_opts(self): """Adapter opts are bogus, in exception-raising ways.""" @@ -274,24 +339,31 @@ def test_invalid_adapter_opts(self): 'orchestration', "Encountered an exception attempting to process config for " "project 'heat' (service type 'orchestration'): interface and " - "valid_interfaces are mutually exclusive.") + "valid_interfaces are mutually exclusive.", + ) def test_no_session(self): # TODO(efried): Currently calling without a Session is not implemented. - self.assertRaises(exceptions.ConfigException, - cloud_region.from_conf, self._load_ks_cfg_opts()) + self.assertRaises( + exceptions.ConfigException, + cloud_region.from_conf, + self._load_ks_cfg_opts(), + ) def test_no_endpoint(self): """Conf contains adapter opts, but service type not in catalog.""" self.os_fixture.v3_token.remove_service('monitoring') conn = self._get_conn() # Monasca is not in the service catalog - self.assertRaises(ks_exc.catalog.EndpointNotFound, - getattr, conn, 'monitoring') + self.assertRaises( + ks_exc.catalog.EndpointNotFound, getattr, conn, 'monitoring' + ) def test_no_endpoint_ignore_service_type(self): """Bogus service type disabled if not in requested service_types.""" self.assert_service_disabled( - 'monitoring', "Not in the list of requested service_types.", + 'monitoring', + "Not in the list of requested service_types.", # 'monitoring' absent from this list - service_types={'compute', 'orchestration', 'bogus'}) + service_types={'compute', 'orchestration', 'bogus'}, + ) diff --git a/openstack/tests/unit/config/test_from_session.py b/openstack/tests/unit/config/test_from_session.py index 0873e296c..4d28e07e6 100644 --- a/openstack/tests/unit/config/test_from_session.py +++ b/openstack/tests/unit/config/test_from_session.py @@ -30,7 +30,8 @@ class TestFromSession(base.TestCase): def test_from_session(self): config = cloud_region.from_session( - self.cloud.session, region_name=self.test_region) + self.cloud.session, region_name=self.test_region + ) self.assertEqual(config.name, 'identity.example.com') if not self.test_region: self.assertIsNone(config.region_name) @@ -40,13 +41,18 @@ def test_from_session(self): server_id = str(uuid.uuid4()) server_name = self.getUniqueString('name') fake_server = fakes.make_fake_server(server_id, server_name) - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - json={'servers': [fake_server]}), - ]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + json={'servers': [fake_server]}, + ), + ] + ) conn = connection.Connection(config=config) s = next(conn.compute.servers()) diff --git a/openstack/tests/unit/config/test_init.py b/openstack/tests/unit/config/test_init.py index 62fb7ea45..c1813feb0 100644 --- a/openstack/tests/unit/config/test_init.py +++ b/openstack/tests/unit/config/test_init.py @@ -19,17 +19,16 @@ class TestInit(base.TestCase): def test_get_cloud_region_without_arg_parser(self): cloud_region = openstack.config.get_cloud_region( - options=None, validate=False) + options=None, validate=False + ) self.assertIsInstance( - cloud_region, - openstack.config.cloud_region.CloudRegion + cloud_region, openstack.config.cloud_region.CloudRegion ) def test_get_cloud_region_with_arg_parser(self): cloud_region = openstack.config.get_cloud_region( - options=argparse.ArgumentParser(), - validate=False) + options=argparse.ArgumentParser(), validate=False + ) self.assertIsInstance( - cloud_region, - openstack.config.cloud_region.CloudRegion + cloud_region, openstack.config.cloud_region.CloudRegion ) diff --git a/openstack/tests/unit/config/test_json.py b/openstack/tests/unit/config/test_json.py index 7a43341e1..ef9ffd5c0 100644 --- a/openstack/tests/unit/config/test_json.py +++ b/openstack/tests/unit/config/test_json.py @@ -24,7 +24,6 @@ class TestConfig(base.TestCase): - def json_diagnostics(self, exc_info): self.addDetail('filename', content.text_content(self.filename)) for error in sorted(self.validator.iter_errors(self.json_data)): @@ -32,8 +31,8 @@ def json_diagnostics(self, exc_info): def test_defaults_valid_json(self): _schema_path = os.path.join( - os.path.dirname(os.path.realpath(defaults.__file__)), - 'schema.json') + os.path.dirname(os.path.realpath(defaults.__file__)), 'schema.json' + ) with open(_schema_path, 'r') as f: schema = json.load(f) self.validator = jsonschema.Draft4Validator(schema) @@ -41,7 +40,8 @@ def test_defaults_valid_json(self): self.filename = os.path.join( os.path.dirname(os.path.realpath(defaults.__file__)), - 'defaults.json') + 'defaults.json', + ) with open(self.filename, 'r') as f: self.json_data = json.load(f) @@ -50,7 +50,8 @@ def test_defaults_valid_json(self): def test_vendors_valid_json(self): _schema_path = os.path.join( os.path.dirname(os.path.realpath(defaults.__file__)), - 'vendor-schema.json') + 'vendor-schema.json', + ) with open(_schema_path, 'r') as f: schema = json.load(f) self.validator = jsonschema.Draft4Validator(schema) @@ -58,8 +59,8 @@ def test_vendors_valid_json(self): self.addOnException(self.json_diagnostics) _vendors_path = os.path.join( - os.path.dirname(os.path.realpath(defaults.__file__)), - 'vendors') + os.path.dirname(os.path.realpath(defaults.__file__)), 'vendors' + ) for self.filename in glob.glob(os.path.join(_vendors_path, '*.json')): with open(self.filename, 'r') as f: self.json_data = json.load(f) diff --git a/openstack/tests/unit/config/test_loader.py b/openstack/tests/unit/config/test_loader.py index 9e54fbcfa..a21f3b286 100644 --- a/openstack/tests/unit/config/test_loader.py +++ b/openstack/tests/unit/config/test_loader.py @@ -21,14 +21,17 @@ from openstack.tests.unit.config import base FILES = { - 'yaml': textwrap.dedent(''' + 'yaml': textwrap.dedent( + ''' foo: bar baz: - 1 - 2 - 3 - '''), - 'json': textwrap.dedent(''' + ''' + ), + 'json': textwrap.dedent( + ''' { "foo": "bar", "baz": [ @@ -37,18 +40,20 @@ 3 ] } - '''), - 'txt': textwrap.dedent(''' + ''' + ), + 'txt': textwrap.dedent( + ''' foo bar baz test one two - '''), + ''' + ), } class TestLoader(base.TestCase): - def test_base_load_yaml_json_file(self): with tempfile.TemporaryDirectory() as tmpdir: tested_files = [] @@ -59,7 +64,8 @@ def test_base_load_yaml_json_file(self): tested_files.append(fn) path, result = loader.OpenStackConfig()._load_yaml_json_file( - tested_files) + tested_files + ) # NOTE(hberaud): Prefer to test path rather than file because # our FILES var is a dict so results are appened # without keeping the initial order (python 3.5) @@ -77,7 +83,8 @@ def test__load_yaml_json_file_without_json(self): tested_files.append(fn) path, result = loader.OpenStackConfig()._load_yaml_json_file( - tested_files) + tested_files + ) # NOTE(hberaud): Prefer to test path rather than file because # our FILES var is a dict so results are appened # without keeping the initial order (python 3.5) @@ -92,7 +99,8 @@ def test__load_yaml_json_file_without_json_yaml(self): tested_files.append(fn) path, result = loader.OpenStackConfig()._load_yaml_json_file( - tested_files) + tested_files + ) self.assertEqual(fn, path) def test__load_yaml_json_file_without_perm(self): @@ -105,7 +113,8 @@ def test__load_yaml_json_file_without_perm(self): tested_files.append(fn) path, result = loader.OpenStackConfig()._load_yaml_json_file( - tested_files) + tested_files + ) self.assertEqual(None, path) def test__load_yaml_json_file_nonexisting(self): @@ -114,28 +123,56 @@ def test__load_yaml_json_file_nonexisting(self): tested_files.append(fn) path, result = loader.OpenStackConfig()._load_yaml_json_file( - tested_files) + tested_files + ) self.assertEqual(None, path) class TestFixArgv(base.TestCase): def test_no_changes(self): - argv = ['-a', '-b', '--long-arg', '--multi-value', 'key1=value1', - '--multi-value', 'key2=value2'] + argv = [ + '-a', + '-b', + '--long-arg', + '--multi-value', + 'key1=value1', + '--multi-value', + 'key2=value2', + ] expected = argv[:] loader._fix_argv(argv) self.assertEqual(expected, argv) def test_replace(self): - argv = ['-a', '-b', '--long-arg', '--multi_value', 'key1=value1', - '--multi_value', 'key2=value2'] - expected = ['-a', '-b', '--long-arg', '--multi-value', 'key1=value1', - '--multi-value', 'key2=value2'] + argv = [ + '-a', + '-b', + '--long-arg', + '--multi_value', + 'key1=value1', + '--multi_value', + 'key2=value2', + ] + expected = [ + '-a', + '-b', + '--long-arg', + '--multi-value', + 'key1=value1', + '--multi-value', + 'key2=value2', + ] loader._fix_argv(argv) self.assertEqual(expected, argv) def test_mix(self): - argv = ['-a', '-b', '--long-arg', '--multi_value', 'key1=value1', - '--multi-value', 'key2=value2'] - self.assertRaises(exceptions.ConfigException, - loader._fix_argv, argv) + argv = [ + '-a', + '-b', + '--long-arg', + '--multi_value', + 'key1=value1', + '--multi-value', + 'key2=value2', + ] + self.assertRaises(exceptions.ConfigException, loader._fix_argv, argv) diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index abdc4ff69..4dfc25748 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -70,9 +70,13 @@ password: {password} project_name: {project} cacert: {cacert} -""".format(auth_url=CONFIG_AUTH_URL, username=CONFIG_USERNAME, - password=CONFIG_PASSWORD, project=CONFIG_PROJECT, - cacert=CONFIG_CACERT) +""".format( + auth_url=CONFIG_AUTH_URL, + username=CONFIG_USERNAME, + password=CONFIG_PASSWORD, + project=CONFIG_PROJECT, + cacert=CONFIG_CACERT, +) VENDOR_CONFIG = """ {{ @@ -84,7 +88,9 @@ "vendor_hook": "openstack.tests.unit.test_connection:vendor_hook" }} }} -""".format(auth_url=CONFIG_AUTH_URL) +""".format( + auth_url=CONFIG_AUTH_URL +) PUBLIC_CLOUDS_YAML = """ public-clouds: @@ -92,11 +98,12 @@ auth: auth_url: {auth_url} vendor_hook: openstack.tests.unit.test_connection:vendor_hook -""".format(auth_url=CONFIG_AUTH_URL) +""".format( + auth_url=CONFIG_AUTH_URL +) class _TestConnectionBase(base.TestCase): - def setUp(self): super(_TestConnectionBase, self).setUp() # Create a temporary directory where our test config will live @@ -107,8 +114,9 @@ def setUp(self): with open(config_path, "w") as conf: conf.write(CLOUD_CONFIG) - self.useFixture(fixtures.EnvironmentVariable( - "OS_CLIENT_CONFIG_FILE", config_path)) + self.useFixture( + fixtures.EnvironmentVariable("OS_CLIENT_CONFIG_FILE", config_path) + ) self.use_keystone_v2() @@ -152,104 +160,127 @@ def test_create_session(self): # conn.workflow.__class__.__module__) def test_create_unknown_proxy(self): - self.register_uris([ - self.get_placement_discovery_mock_dict(), - ]) + self.register_uris( + [ + self.get_placement_discovery_mock_dict(), + ] + ) def closure(): return self.cloud.placement - self.assertThat( - closure, - matchers.Warnings(matchers.HasLength(0))) + self.assertThat(closure, matchers.Warnings(matchers.HasLength(0))) - self.assertIsInstance( - self.cloud.placement, - proxy.Proxy) + self.assertIsInstance(self.cloud.placement, proxy.Proxy) self.assert_calls() def test_create_connection_version_param_default(self): c1 = connection.Connection(cloud='sample-cloud') conn = connection.Connection(session=c1.session) - self.assertEqual('openstack.identity.v3._proxy', - conn.identity.__class__.__module__) + self.assertEqual( + 'openstack.identity.v3._proxy', conn.identity.__class__.__module__ + ) def test_create_connection_version_param_string(self): c1 = connection.Connection(cloud='sample-cloud') conn = connection.Connection( - session=c1.session, identity_api_version='2') - self.assertEqual('openstack.identity.v2._proxy', - conn.identity.__class__.__module__) + session=c1.session, identity_api_version='2' + ) + self.assertEqual( + 'openstack.identity.v2._proxy', conn.identity.__class__.__module__ + ) def test_create_connection_version_param_int(self): c1 = connection.Connection(cloud='sample-cloud') conn = connection.Connection( - session=c1.session, identity_api_version=3) - self.assertEqual('openstack.identity.v3._proxy', - conn.identity.__class__.__module__) + session=c1.session, identity_api_version=3 + ) + self.assertEqual( + 'openstack.identity.v3._proxy', conn.identity.__class__.__module__ + ) def test_create_connection_version_param_bogus(self): c1 = connection.Connection(cloud='sample-cloud') conn = connection.Connection( - session=c1.session, identity_api_version='red') + session=c1.session, identity_api_version='red' + ) # TODO(mordred) This is obviously silly behavior - self.assertEqual('openstack.identity.v3._proxy', - conn.identity.__class__.__module__) + self.assertEqual( + 'openstack.identity.v3._proxy', conn.identity.__class__.__module__ + ) def test_from_config_given_config(self): - cloud_region = (openstack.config.OpenStackConfig(). - get_one("sample-cloud")) + cloud_region = openstack.config.OpenStackConfig().get_one( + "sample-cloud" + ) sot = connection.from_config(config=cloud_region) - self.assertEqual(CONFIG_USERNAME, - sot.config.config['auth']['username']) - self.assertEqual(CONFIG_PASSWORD, - sot.config.config['auth']['password']) - self.assertEqual(CONFIG_AUTH_URL, - sot.config.config['auth']['auth_url']) - self.assertEqual(CONFIG_PROJECT, - sot.config.config['auth']['project_name']) + self.assertEqual( + CONFIG_USERNAME, sot.config.config['auth']['username'] + ) + self.assertEqual( + CONFIG_PASSWORD, sot.config.config['auth']['password'] + ) + self.assertEqual( + CONFIG_AUTH_URL, sot.config.config['auth']['auth_url'] + ) + self.assertEqual( + CONFIG_PROJECT, sot.config.config['auth']['project_name'] + ) def test_from_config_given_cloud(self): sot = connection.from_config(cloud="sample-cloud") - self.assertEqual(CONFIG_USERNAME, - sot.config.config['auth']['username']) - self.assertEqual(CONFIG_PASSWORD, - sot.config.config['auth']['password']) - self.assertEqual(CONFIG_AUTH_URL, - sot.config.config['auth']['auth_url']) - self.assertEqual(CONFIG_PROJECT, - sot.config.config['auth']['project_name']) + self.assertEqual( + CONFIG_USERNAME, sot.config.config['auth']['username'] + ) + self.assertEqual( + CONFIG_PASSWORD, sot.config.config['auth']['password'] + ) + self.assertEqual( + CONFIG_AUTH_URL, sot.config.config['auth']['auth_url'] + ) + self.assertEqual( + CONFIG_PROJECT, sot.config.config['auth']['project_name'] + ) def test_from_config_given_cloud_config(self): - cloud_region = (openstack.config.OpenStackConfig(). - get_one("sample-cloud")) + cloud_region = openstack.config.OpenStackConfig().get_one( + "sample-cloud" + ) sot = connection.from_config(cloud_config=cloud_region) - self.assertEqual(CONFIG_USERNAME, - sot.config.config['auth']['username']) - self.assertEqual(CONFIG_PASSWORD, - sot.config.config['auth']['password']) - self.assertEqual(CONFIG_AUTH_URL, - sot.config.config['auth']['auth_url']) - self.assertEqual(CONFIG_PROJECT, - sot.config.config['auth']['project_name']) + self.assertEqual( + CONFIG_USERNAME, sot.config.config['auth']['username'] + ) + self.assertEqual( + CONFIG_PASSWORD, sot.config.config['auth']['password'] + ) + self.assertEqual( + CONFIG_AUTH_URL, sot.config.config['auth']['auth_url'] + ) + self.assertEqual( + CONFIG_PROJECT, sot.config.config['auth']['project_name'] + ) def test_from_config_given_cloud_name(self): sot = connection.from_config(cloud_name="sample-cloud") - self.assertEqual(CONFIG_USERNAME, - sot.config.config['auth']['username']) - self.assertEqual(CONFIG_PASSWORD, - sot.config.config['auth']['password']) - self.assertEqual(CONFIG_AUTH_URL, - sot.config.config['auth']['auth_url']) - self.assertEqual(CONFIG_PROJECT, - sot.config.config['auth']['project_name']) + self.assertEqual( + CONFIG_USERNAME, sot.config.config['auth']['username'] + ) + self.assertEqual( + CONFIG_PASSWORD, sot.config.config['auth']['password'] + ) + self.assertEqual( + CONFIG_AUTH_URL, sot.config.config['auth']['auth_url'] + ) + self.assertEqual( + CONFIG_PROJECT, sot.config.config['auth']['project_name'] + ) def test_from_config_verify(self): sot = connection.from_config(cloud="insecure-cloud") @@ -268,25 +299,32 @@ class TestOsloConfig(_TestConnectionBase): def test_from_conf(self): c1 = connection.Connection(cloud='sample-cloud') conn = connection.Connection( - session=c1.session, oslo_conf=self._load_ks_cfg_opts()) + session=c1.session, oslo_conf=self._load_ks_cfg_opts() + ) # There was no config for keystone self.assertIsInstance( - conn.identity, service_description._ServiceDisabledProxyShim) + conn.identity, service_description._ServiceDisabledProxyShim + ) # But nova was in there - self.assertEqual('openstack.compute.v2._proxy', - conn.compute.__class__.__module__) + self.assertEqual( + 'openstack.compute.v2._proxy', conn.compute.__class__.__module__ + ) def test_from_conf_filter_service_types(self): c1 = connection.Connection(cloud='sample-cloud') conn = connection.Connection( - session=c1.session, oslo_conf=self._load_ks_cfg_opts(), - service_types={'orchestration', 'i-am-ignored'}) + session=c1.session, + oslo_conf=self._load_ks_cfg_opts(), + service_types={'orchestration', 'i-am-ignored'}, + ) # There was no config for keystone self.assertIsInstance( - conn.identity, service_description._ServiceDisabledProxyShim) + conn.identity, service_description._ServiceDisabledProxyShim + ) # Nova was in there, but disabled because not requested self.assertIsInstance( - conn.compute, service_description._ServiceDisabledProxyShim) + conn.compute, service_description._ServiceDisabledProxyShim + ) class TestNetworkConnection(base.TestCase): @@ -298,15 +336,18 @@ def test_network_proxy(self): svc.add_endpoint( interface='public', url='https://network.example.com/v2.0', - region='RegionOne') + region='RegionOne', + ) self.use_keystone_v3() self.assertEqual( 'openstack.network.v2._proxy', - self.cloud.network.__class__.__module__) + self.cloud.network.__class__.__module__, + ) self.assert_calls() self.assertEqual( "https://network.example.com/v2.0", - self.cloud.network.get_endpoint()) + self.cloud.network.get_endpoint(), + ) class TestNetworkConnectionSuffix(base.TestCase): @@ -316,15 +357,16 @@ class TestNetworkConnectionSuffix(base.TestCase): def test_network_proxy(self): self.assertEqual( 'openstack.network.v2._proxy', - self.cloud.network.__class__.__module__) + self.cloud.network.__class__.__module__, + ) self.assert_calls() self.assertEqual( "https://network.example.com/v2.0", - self.cloud.network.get_endpoint()) + self.cloud.network.get_endpoint(), + ) class TestAuthorize(base.TestCase): - def test_authorize_works(self): res = self.cloud.authorize() self.assertEqual('KeystoneToken-1', res) @@ -332,12 +374,12 @@ def test_authorize_works(self): def test_authorize_failure(self): self.use_broken_keystone() - self.assertRaises(openstack.exceptions.SDKException, - self.cloud.authorize) + self.assertRaises( + openstack.exceptions.SDKException, self.cloud.authorize + ) class TestNewService(base.TestCase): - def test_add_service_v1(self): svc = self.os_fixture.v3_token.add_service('fake') svc.add_endpoint( @@ -355,21 +397,30 @@ def test_add_service_v1(self): # Ensure no discovery calls made self.assertEqual(0, len(self.adapter.request_history)) - self.register_uris([ - dict(method='GET', - uri='https://fake.example.com', - status_code=404), - dict(method='GET', - uri='https://fake.example.com/v1/', - status_code=404), - dict(method='GET', - uri=self.get_mock_url('fake'), - status_code=404), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://fake.example.com', + status_code=404, + ), + dict( + method='GET', + uri='https://fake.example.com/v1/', + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url('fake'), + status_code=404, + ), + ] + ) self.assertEqual( 'openstack.tests.unit.fake.v1._proxy', - conn.fake.__class__.__module__) + conn.fake.__class__.__module__, + ) self.assertTrue(conn.fake.dummy()) def test_add_service_v2(self): @@ -382,17 +433,25 @@ def test_add_service_v2(self): self.use_keystone_v3() conn = self.cloud - self.register_uris([ - dict(method='GET', - uri='https://fake.example.com', - status_code=404), - dict(method='GET', - uri='https://fake.example.com/v2/', - status_code=404), - dict(method='GET', - uri=self.get_mock_url('fake'), - status_code=404), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://fake.example.com', + status_code=404, + ), + dict( + method='GET', + uri='https://fake.example.com/v2/', + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url('fake'), + status_code=404, + ), + ] + ) service = fake_service.FakeService('fake') @@ -400,7 +459,8 @@ def test_add_service_v2(self): self.assertEqual( 'openstack.tests.unit.fake.v2._proxy', - conn.fake.__class__.__module__) + conn.fake.__class__.__module__, + ) self.assertFalse(conn.fake.dummy()) def test_replace_system_service(self): @@ -416,17 +476,25 @@ def test_replace_system_service(self): # delete native dns service delattr(conn, 'dns') - self.register_uris([ - dict(method='GET', - uri='https://fake.example.com', - status_code=404), - dict(method='GET', - uri='https://fake.example.com/v2/', - status_code=404), - dict(method='GET', - uri=self.get_mock_url('fake'), - status_code=404), - ]) + self.register_uris( + [ + dict( + method='GET', + uri='https://fake.example.com', + status_code=404, + ), + dict( + method='GET', + uri='https://fake.example.com/v2/', + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url('fake'), + status_code=404, + ), + ] + ) # add fake service with alias 'DNS' service = fake_service.FakeService('fake', aliases=['dns']) @@ -441,7 +509,6 @@ def vendor_hook(conn): class TestVendorProfile(base.TestCase): - def setUp(self): super(TestVendorProfile, self).setUp() # Create a temporary directory where our test config will live @@ -456,12 +523,14 @@ def setUp(self): with open(public_clouds, "w") as conf: conf.write(PUBLIC_CLOUDS_YAML) - self.useFixture(fixtures.EnvironmentVariable( - "OS_CLIENT_CONFIG_FILE", config_path)) + self.useFixture( + fixtures.EnvironmentVariable("OS_CLIENT_CONFIG_FILE", config_path) + ) self.use_keystone_v2() self.config = openstack.config.loader.OpenStackConfig( - vendor_files=[public_clouds]) + vendor_files=[public_clouds] + ) def test_conn_from_profile(self): @@ -483,7 +552,7 @@ def test_hook_from_connection_param(self): conn = connection.Connection( cloud='sample-cloud', - vendor_hook='openstack.tests.unit.test_connection:vendor_hook' + vendor_hook='openstack.tests.unit.test_connection:vendor_hook', ) self.assertEqual('test_val', conn.test) @@ -492,7 +561,7 @@ def test_hook_from_connection_ignore_missing(self): conn = connection.Connection( cloud='sample-cloud', - vendor_hook='openstack.tests.unit.test_connection:missing' + vendor_hook='openstack.tests.unit.test_connection:missing', ) self.assertIsNotNone(conn) diff --git a/openstack/tests/unit/test_exceptions.py b/openstack/tests/unit/test_exceptions.py index a4b5f2bf7..b591150ed 100644 --- a/openstack/tests/unit/test_exceptions.py +++ b/openstack/tests/unit/test_exceptions.py @@ -23,13 +23,14 @@ class Test_Exception(base.TestCase): def test_method_not_supported(self): exc = exceptions.MethodNotSupported(self.__class__, 'list') - expected = ('The list method is not supported for ' - + 'openstack.tests.unit.test_exceptions.Test_Exception') + expected = ( + 'The list method is not supported for ' + + 'openstack.tests.unit.test_exceptions.Test_Exception' + ) self.assertEqual(expected, str(exc)) class Test_HttpException(base.TestCase): - def setUp(self): super(Test_HttpException, self).setUp() self.message = "mayday" @@ -38,32 +39,38 @@ def _do_raise(self, *args, **kwargs): raise exceptions.HttpException(*args, **kwargs) def test_message(self): - exc = self.assertRaises(exceptions.HttpException, - self._do_raise, self.message) + exc = self.assertRaises( + exceptions.HttpException, self._do_raise, self.message + ) self.assertEqual(self.message, exc.message) def test_details(self): details = "some details" - exc = self.assertRaises(exceptions.HttpException, - self._do_raise, self.message, - details=details) + exc = self.assertRaises( + exceptions.HttpException, + self._do_raise, + self.message, + details=details, + ) self.assertEqual(self.message, exc.message) self.assertEqual(details, exc.details) def test_http_status(self): http_status = 123 - exc = self.assertRaises(exceptions.HttpException, - self._do_raise, self.message, - http_status=http_status) + exc = self.assertRaises( + exceptions.HttpException, + self._do_raise, + self.message, + http_status=http_status, + ) self.assertEqual(self.message, exc.message) self.assertEqual(http_status, exc.status_code) class TestRaiseFromResponse(base.TestCase): - def setUp(self): super(TestRaiseFromResponse, self).setUp() self.message = "Where is my kitty?" @@ -83,14 +90,16 @@ def test_raise_not_found_exception(self): 'content-type': 'application/json', 'x-openstack-request-id': uuid.uuid4().hex, } - exc = self.assertRaises(exceptions.NotFoundException, - self._do_raise, response, - error_message=self.message) + exc = self.assertRaises( + exceptions.NotFoundException, + self._do_raise, + response, + error_message=self.message, + ) self.assertEqual(self.message, exc.message) self.assertEqual(response.status_code, exc.status_code) self.assertEqual( - response.headers.get('x-openstack-request-id'), - exc.request_id + response.headers.get('x-openstack-request-id'), exc.request_id ) def test_raise_bad_request_exception(self): @@ -100,14 +109,16 @@ def test_raise_bad_request_exception(self): 'content-type': 'application/json', 'x-openstack-request-id': uuid.uuid4().hex, } - exc = self.assertRaises(exceptions.BadRequestException, - self._do_raise, response, - error_message=self.message) + exc = self.assertRaises( + exceptions.BadRequestException, + self._do_raise, + response, + error_message=self.message, + ) self.assertEqual(self.message, exc.message) self.assertEqual(response.status_code, exc.status_code) self.assertEqual( - response.headers.get('x-openstack-request-id'), - exc.request_id + response.headers.get('x-openstack-request-id'), exc.request_id ) def test_raise_http_exception(self): @@ -117,14 +128,16 @@ def test_raise_http_exception(self): 'content-type': 'application/json', 'x-openstack-request-id': uuid.uuid4().hex, } - exc = self.assertRaises(exceptions.HttpException, - self._do_raise, response, - error_message=self.message) + exc = self.assertRaises( + exceptions.HttpException, + self._do_raise, + response, + error_message=self.message, + ) self.assertEqual(self.message, exc.message) self.assertEqual(response.status_code, exc.status_code) self.assertEqual( - response.headers.get('x-openstack-request-id'), - exc.request_id + response.headers.get('x-openstack-request-id'), exc.request_id ) def test_raise_compute_format(self): @@ -139,9 +152,12 @@ def test_raise_compute_format(self): 'code': 404, } } - exc = self.assertRaises(exceptions.NotFoundException, - self._do_raise, response, - error_message=self.message) + exc = self.assertRaises( + exceptions.NotFoundException, + self._do_raise, + response, + error_message=self.message, + ) self.assertEqual(response.status_code, exc.status_code) self.assertEqual(self.message, exc.details) self.assertIn(self.message, str(exc)) @@ -159,9 +175,12 @@ def test_raise_network_format(self): 'detail': '', } } - exc = self.assertRaises(exceptions.NotFoundException, - self._do_raise, response, - error_message=self.message) + exc = self.assertRaises( + exceptions.NotFoundException, + self._do_raise, + response, + error_message=self.message, + ) self.assertEqual(response.status_code, exc.status_code) self.assertEqual(self.message, exc.details) self.assertIn(self.message, str(exc)) @@ -173,15 +192,20 @@ def test_raise_baremetal_old_format(self): 'content-type': 'application/json', } response.json.return_value = { - 'error_message': json.dumps({ - 'faultstring': self.message, - 'faultcode': 'Client', - 'debuginfo': None, - }) + 'error_message': json.dumps( + { + 'faultstring': self.message, + 'faultcode': 'Client', + 'debuginfo': None, + } + ) } - exc = self.assertRaises(exceptions.NotFoundException, - self._do_raise, response, - error_message=self.message) + exc = self.assertRaises( + exceptions.NotFoundException, + self._do_raise, + response, + error_message=self.message, + ) self.assertEqual(response.status_code, exc.status_code) self.assertEqual(self.message, exc.details) self.assertIn(self.message, str(exc)) @@ -199,9 +223,12 @@ def test_raise_baremetal_corrected_format(self): 'debuginfo': None, } } - exc = self.assertRaises(exceptions.NotFoundException, - self._do_raise, response, - error_message=self.message) + exc = self.assertRaises( + exceptions.NotFoundException, + self._do_raise, + response, + error_message=self.message, + ) self.assertEqual(response.status_code, exc.status_code) self.assertEqual(self.message, exc.details) self.assertIn(self.message, str(exc)) @@ -217,9 +244,12 @@ def test_raise_wsme_format(self): 'faultcode': 'Client', 'debuginfo': None, } - exc = self.assertRaises(exceptions.NotFoundException, - self._do_raise, response, - error_message=self.message) + exc = self.assertRaises( + exceptions.NotFoundException, + self._do_raise, + response, + error_message=self.message, + ) self.assertEqual(response.status_code, exc.status_code) self.assertEqual(self.message, exc.details) self.assertIn(self.message, str(exc)) diff --git a/openstack/tests/unit/test_format.py b/openstack/tests/unit/test_format.py index 18724232d..50b5b04da 100644 --- a/openstack/tests/unit/test_format.py +++ b/openstack/tests/unit/test_format.py @@ -15,7 +15,6 @@ class TestBoolStrFormatter(base.TestCase): - def test_deserialize(self): self.assertTrue(format.BoolStr.deserialize(True)) self.assertTrue(format.BoolStr.deserialize('True')) diff --git a/openstack/tests/unit/test_hacking.py b/openstack/tests/unit/test_hacking.py index e6da4f58d..bf719d11d 100644 --- a/openstack/tests/unit/test_hacking.py +++ b/openstack/tests/unit/test_hacking.py @@ -49,12 +49,23 @@ class HackingTestCase(base.TestCase): just assertTrue if the check is expected to fail and assertFalse if it should pass. """ + def test_assert_no_setupclass(self): - self.assertEqual(len(list(_hacking.assert_no_setupclass( - "def setUpClass(cls)"))), 1) + self.assertEqual( + len(list(_hacking.assert_no_setupclass("def setUpClass(cls)"))), 1 + ) - self.assertEqual(len(list(_hacking.assert_no_setupclass( - "# setUpClass is evil"))), 0) + self.assertEqual( + len(list(_hacking.assert_no_setupclass("# setUpClass is evil"))), 0 + ) - self.assertEqual(len(list(_hacking.assert_no_setupclass( - "def setUpClassyDrinkingLocation(cls)"))), 0) + self.assertEqual( + len( + list( + _hacking.assert_no_setupclass( + "def setUpClassyDrinkingLocation(cls)" + ) + ) + ), + 0, + ) diff --git a/openstack/tests/unit/test_microversions.py b/openstack/tests/unit/test_microversions.py index 67dc79d1a..cbe73a6ad 100644 --- a/openstack/tests/unit/test_microversions.py +++ b/openstack/tests/unit/test_microversions.py @@ -16,7 +16,6 @@ class TestMicroversions(base.TestCase): - def setUp(self): super(TestMicroversions, self).setUp() self.use_compute_discovery() @@ -27,7 +26,8 @@ def test_get_bad_inferred_max_microversion(self): self.assertRaises( exceptions.ConfigException, - self.cloud.get_server, 'doesNotExist', + self.cloud.get_server, + 'doesNotExist', ) self.assert_calls() @@ -38,7 +38,8 @@ def test_get_bad_default_max_microversion(self): self.assertRaises( exceptions.ConfigException, - self.cloud.get_server, 'doesNotExist', + self.cloud.get_server, + 'doesNotExist', ) self.assert_calls() @@ -49,7 +50,8 @@ def test_get_bad_inferred_min_microversion(self): self.assertRaises( exceptions.ConfigException, - self.cloud.get_server, 'doesNotExist', + self.cloud.get_server, + 'doesNotExist', ) self.assert_calls() @@ -60,7 +62,8 @@ def test_get_bad_default_min_microversion(self): self.assertRaises( exceptions.ConfigException, - self.cloud.get_server, 'doesNotExist', + self.cloud.get_server, + 'doesNotExist', ) self.assert_calls() @@ -72,13 +75,18 @@ def test_inferred_default_microversion(self): server1 = fakes.make_fake_server('123', 'mickey') server2 = fakes.make_fake_server('345', 'mouse') - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - request_headers={'OpenStack-API-Version': 'compute 2.42'}, - json={'servers': [server1, server2]}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + request_headers={'OpenStack-API-Version': 'compute 2.42'}, + json={'servers': [server1, server2]}, + ), + ] + ) r = self.cloud.get_server('mickey', bare=True) self.assertIsNotNone(r) @@ -93,13 +101,18 @@ def test_default_microversion(self): server1 = fakes.make_fake_server('123', 'mickey') server2 = fakes.make_fake_server('345', 'mouse') - self.register_uris([ - dict(method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail']), - request_headers={'OpenStack-API-Version': 'compute 2.42'}, - json={'servers': [server1, server2]}), - ]) + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + request_headers={'OpenStack-API-Version': 'compute 2.42'}, + json={'servers': [server1, server2]}, + ), + ] + ) r = self.cloud.get_server('mickey', bare=True) self.assertIsNotNone(r) diff --git a/openstack/tests/unit/test_missing_version.py b/openstack/tests/unit/test_missing_version.py index c1a948f71..b2a18f9c8 100644 --- a/openstack/tests/unit/test_missing_version.py +++ b/openstack/tests/unit/test_missing_version.py @@ -18,18 +18,20 @@ class TestMissingVersion(base.TestCase): - def setUp(self): super(TestMissingVersion, self).setUp() self.os_fixture.clear_tokens() svc = self.os_fixture.v3_token.add_service('image') svc.add_endpoint( url='https://example.com/image/', - region='RegionOne', interface='public') + region='RegionOne', + interface='public', + ) self.use_keystone_v3() self.use_glance( image_version_json='bad-glance-version.json', - image_discovery_url='https://example.com/image/') + image_discovery_url='https://example.com/image/', + ) def test_unsupported_version(self): diff --git a/openstack/tests/unit/test_placement_rest.py b/openstack/tests/unit/test_placement_rest.py index 6ac80e28a..713db3dc0 100644 --- a/openstack/tests/unit/test_placement_rest.py +++ b/openstack/tests/unit/test_placement_rest.py @@ -20,7 +20,6 @@ @ddt.ddt class TestPlacementRest(base.TestCase): - def setUp(self): super(TestPlacementRest, self).setUp() self.use_placement() @@ -29,8 +28,10 @@ def _register_uris(self, status_code=None): uri = dict( method='GET', uri=self.get_mock_url( - 'placement', 'public', append=['allocation_candidates']), - json={}) + 'placement', 'public', append=['allocation_candidates'] + ), + json={}, + ) if status_code is not None: uri['status_code'] = status_code self.register_uris([uri]) @@ -38,8 +39,8 @@ def _register_uris(self, status_code=None): def _validate_resp(self, resp, status_code): self.assertEqual(status_code, resp.status_code) self.assertEqual( - 'https://placement.example.com/allocation_candidates', - resp.url) + 'https://placement.example.com/allocation_candidates', resp.url + ) self.assert_calls() @ddt.data({}, {'raise_exc': False}, {'raise_exc': True}) @@ -61,18 +62,20 @@ def test_discovery_exc(self): # raise_exc=True raises a ksa exception appropriate to the status code ex = self.assertRaises( exceptions.InternalServerError, - self.cloud.placement.get, '/allocation_candidates', raise_exc=True) + self.cloud.placement.get, + '/allocation_candidates', + raise_exc=True, + ) self._validate_resp(ex.response, 500) def test_microversion_discovery(self): self.assertEqual( - (1, 17), - self.cloud.placement.get_endpoint_data().max_microversion) + (1, 17), self.cloud.placement.get_endpoint_data().max_microversion + ) self.assert_calls() class TestBadPlacementRest(base.TestCase): - def setUp(self): self.skipTest('Need to re-add support for broken placement versions') super(TestBadPlacementRest, self).setUp() @@ -85,8 +88,10 @@ def _register_uris(self, status_code=None): uri = dict( method='GET', uri=self.get_mock_url( - 'placement', 'public', append=['allocation_candidates']), - json={}) + 'placement', 'public', append=['allocation_candidates'] + ), + json={}, + ) if status_code is not None: uri['status_code'] = status_code self.register_uris([uri]) @@ -94,8 +99,8 @@ def _register_uris(self, status_code=None): def _validate_resp(self, resp, status_code): self.assertEqual(status_code, resp.status_code) self.assertEqual( - 'https://placement.example.com/allocation_candidates', - resp.url) + 'https://placement.example.com/allocation_candidates', resp.url + ) self.assert_calls() def test_discovery(self): diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index cd1b09b58..3dddb9cad 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -58,7 +58,6 @@ class HeadableResource(resource.Resource): class TestProxyPrivate(base.TestCase): - def setUp(self): super(TestProxyPrivate, self).setUp() @@ -88,9 +87,14 @@ def test__check_resource_notstrict_id(self): def test__check_resource_strict_id(self): decorated = proxy._check_resource(strict=True)(self.sot.method) - self.assertRaisesRegex(ValueError, "A Resource must be passed", - decorated, self.sot, resource.Resource, - "this-is-not-a-resource") + self.assertRaisesRegex( + ValueError, + "A Resource must be passed", + decorated, + self.sot, + resource.Resource, + "this-is-not-a-resource", + ) def test__check_resource_incorrect_resource(self): class OneType(resource.Resource): @@ -101,9 +105,14 @@ class AnotherType(resource.Resource): value = AnotherType() decorated = proxy._check_resource(strict=False)(self.sot.method) - self.assertRaisesRegex(ValueError, - "Expected OneType but received AnotherType", - decorated, self.sot, OneType, value) + self.assertRaisesRegex( + ValueError, + "Expected OneType but received AnotherType", + decorated, + self.sot, + OneType, + value, + ) def test__get_uri_attribute_no_parent(self): class Child(resource.Resource): @@ -161,7 +170,8 @@ def new(cls, **kwargs): result = self.fake_proxy._get_resource(Fake, id, **attrs) self.assertDictEqual( - dict(id=id, connection=mock.ANY, **attrs), Fake.call) + dict(id=id, connection=mock.ANY, **attrs), Fake.call + ) self.assertEqual(value, result) def test__get_resource_from_resource(self): @@ -170,8 +180,7 @@ def test__get_resource_from_resource(self): attrs = {"first": "Brian", "last": "Curtin"} - result = self.fake_proxy._get_resource(resource.Resource, - res, **attrs) + result = self.fake_proxy._get_resource(resource.Resource, res, **attrs) res._update.assert_called_once_with(**attrs) self.assertEqual(result, res) @@ -193,7 +202,6 @@ def test__get_resource_from_munch(self): class TestProxyDelete(base.TestCase): - def setUp(self): super(TestProxyDelete, self).setUp() @@ -215,7 +223,8 @@ def test_delete(self): self.sot._delete(DeleteableResource, self.fake_id) DeleteableResource.new.assert_called_with( - connection=self.cloud, id=self.fake_id) + connection=self.cloud, id=self.fake_id + ) self.res.delete.assert_called_with(self.sot) # Delete generally doesn't return anything, so we will normally @@ -227,32 +236,42 @@ def test_delete(self): def test_delete_ignore_missing(self): self.res.delete.side_effect = exceptions.ResourceNotFound( - message="test", http_status=404) + message="test", http_status=404 + ) rv = self.sot._delete(DeleteableResource, self.fake_id) self.assertIsNone(rv) def test_delete_NotFound(self): self.res.delete.side_effect = exceptions.ResourceNotFound( - message="test", http_status=404) + message="test", http_status=404 + ) self.assertRaisesRegex( exceptions.ResourceNotFound, # TODO(shade) The mocks here are hiding the thing we want to test. "test", - self.sot._delete, DeleteableResource, self.res, - ignore_missing=False) + self.sot._delete, + DeleteableResource, + self.res, + ignore_missing=False, + ) def test_delete_HttpException(self): self.res.delete.side_effect = exceptions.HttpException( - message="test", http_status=500) + message="test", http_status=500 + ) - self.assertRaises(exceptions.HttpException, self.sot._delete, - DeleteableResource, self.res, ignore_missing=False) + self.assertRaises( + exceptions.HttpException, + self.sot._delete, + DeleteableResource, + self.res, + ignore_missing=False, + ) class TestProxyUpdate(base.TestCase): - def setUp(self): super(TestProxyUpdate, self).setUp() @@ -280,8 +299,9 @@ def test_update_resource(self): def test_update_resource_override_base_path(self): base_path = 'dummy' - rv = self.sot._update(UpdateableResource, self.res, - base_path=base_path, **self.attrs) + rv = self.sot._update( + UpdateableResource, self.res, base_path=base_path, **self.attrs + ) self.assertEqual(rv, self.fake_result) self.res._update.assert_called_once_with(**self.attrs) @@ -295,7 +315,6 @@ def test_update_id(self): class TestProxyCreate(base.TestCase): - def setUp(self): super(TestProxyCreate, self).setUp() @@ -317,7 +336,8 @@ def test_create_attributes(self): self.assertEqual(rv, self.fake_result) CreateableResource.new.assert_called_once_with( - connection=self.cloud, **attrs) + connection=self.cloud, **attrs + ) self.res.create.assert_called_once_with(self.sot, base_path=None) def test_create_attributes_override_base_path(self): @@ -329,12 +349,12 @@ def test_create_attributes_override_base_path(self): self.assertEqual(rv, self.fake_result) CreateableResource.new.assert_called_once_with( - connection=self.cloud, **attrs) + connection=self.cloud, **attrs + ) self.res.create.assert_called_once_with(self.sot, base_path=base_path) class TestProxyBulkCreate(base.TestCase): - def setUp(self): super(TestProxyBulkCreate, self).setUp() @@ -353,8 +373,9 @@ def test_bulk_create_attributes(self): rv = self.sot._bulk_create(self.cls, self.data) self.assertEqual(rv, self.result) - self.cls.bulk_create.assert_called_once_with(self.sot, self.data, - base_path=None) + self.cls.bulk_create.assert_called_once_with( + self.sot, self.data, base_path=None + ) def test_bulk_create_attributes_override_base_path(self): base_path = 'dummy' @@ -362,12 +383,12 @@ def test_bulk_create_attributes_override_base_path(self): rv = self.sot._bulk_create(self.cls, self.data, base_path=base_path) self.assertEqual(rv, self.result) - self.cls.bulk_create.assert_called_once_with(self.sot, self.data, - base_path=base_path) + self.cls.bulk_create.assert_called_once_with( + self.sot, self.data, base_path=base_path + ) class TestProxyGet(base.TestCase): - def setUp(self): super(TestProxyGet, self).setUp() @@ -389,10 +410,12 @@ def test_get_resource(self): rv = self.sot._get(RetrieveableResource, self.res) self.res.fetch.assert_called_with( - self.sot, requires_id=True, + self.sot, + requires_id=True, base_path=None, skip_cache=mock.ANY, - error_message=mock.ANY) + error_message=mock.ANY, + ) self.assertEqual(rv, self.fake_result) def test_get_resource_with_args(self): @@ -401,46 +424,62 @@ def test_get_resource_with_args(self): self.res._update.assert_called_once_with(**args) self.res.fetch.assert_called_with( - self.sot, requires_id=True, base_path=None, + self.sot, + requires_id=True, + base_path=None, skip_cache=mock.ANY, - error_message=mock.ANY) + error_message=mock.ANY, + ) self.assertEqual(rv, self.fake_result) def test_get_id(self): rv = self.sot._get(RetrieveableResource, self.fake_id) RetrieveableResource.new.assert_called_with( - connection=self.cloud, id=self.fake_id) + connection=self.cloud, id=self.fake_id + ) self.res.fetch.assert_called_with( - self.sot, requires_id=True, base_path=None, + self.sot, + requires_id=True, + base_path=None, skip_cache=mock.ANY, - error_message=mock.ANY) + error_message=mock.ANY, + ) self.assertEqual(rv, self.fake_result) def test_get_base_path(self): base_path = 'dummy' - rv = self.sot._get(RetrieveableResource, self.fake_id, - base_path=base_path) + rv = self.sot._get( + RetrieveableResource, self.fake_id, base_path=base_path + ) RetrieveableResource.new.assert_called_with( - connection=self.cloud, id=self.fake_id) + connection=self.cloud, id=self.fake_id + ) self.res.fetch.assert_called_with( - self.sot, requires_id=True, base_path=base_path, + self.sot, + requires_id=True, + base_path=base_path, skip_cache=mock.ANY, - error_message=mock.ANY) + error_message=mock.ANY, + ) self.assertEqual(rv, self.fake_result) def test_get_not_found(self): self.res.fetch.side_effect = exceptions.ResourceNotFound( - message="test", http_status=404) + message="test", http_status=404 + ) self.assertRaisesRegex( exceptions.ResourceNotFound, - "test", self.sot._get, RetrieveableResource, self.res) + "test", + self.sot._get, + RetrieveableResource, + self.res, + ) class TestProxyList(base.TestCase): - def setUp(self): super(TestProxyList, self).setUp() @@ -455,12 +494,17 @@ def setUp(self): ListableResource.list.return_value = self.fake_response def _test_list(self, paginated, base_path=None): - rv = self.sot._list(ListableResource, paginated=paginated, - base_path=base_path, **self.args) + rv = self.sot._list( + ListableResource, + paginated=paginated, + base_path=base_path, + **self.args, + ) self.assertEqual(self.fake_response, rv) ListableResource.list.assert_called_once_with( - self.sot, paginated=paginated, base_path=base_path, **self.args) + self.sot, paginated=paginated, base_path=base_path, **self.args + ) def test_list_paginated(self): self._test_list(True) @@ -481,21 +525,24 @@ def test_list_filters_jmespath(self): FilterableResource.list.return_value = fake_response rv = self.sot._list( - FilterableResource, paginated=False, - base_path=None, jmespath_filters="[?c=='c']" + FilterableResource, + paginated=False, + base_path=None, + jmespath_filters="[?c=='c']", ) self.assertEqual(3, len(rv)) # Test filtering based on unknown attribute rv = self.sot._list( - FilterableResource, paginated=False, - base_path=None, jmespath_filters="[?d=='c']" + FilterableResource, + paginated=False, + base_path=None, + jmespath_filters="[?d=='c']", ) self.assertEqual(0, len(rv)) class TestProxyHead(base.TestCase): - def setUp(self): super(TestProxyHead, self).setUp() @@ -530,7 +577,8 @@ def test_head_id(self): rv = self.sot._head(HeadableResource, self.fake_id) HeadableResource.new.assert_called_with( - connection=self.cloud, id=self.fake_id) + connection=self.cloud, id=self.fake_id + ) self.res.head.assert_called_with(self.sot, base_path=None) self.assertEqual(rv, self.fake_result) @@ -546,10 +594,14 @@ class TestExtractName(base.TestCase): ('networks_arg', dict(url='/v2.0/networks/1', parts=['network'])), ('tokens', dict(url='/v3/tokens', parts=['tokens'])), ('discovery', dict(url='/', parts=['discovery'])), - ('secgroups', dict( - url='/servers/1/os-security-groups', - parts=['server', 'os-security-groups'])), - ('bm_chassis', dict(url='/v1/chassis/id', parts=['chassis'])) + ( + 'secgroups', + dict( + url='/servers/1/os-security-groups', + parts=['server', 'os-security-groups'], + ), + ), + ('bm_chassis', dict(url='/v1/chassis/id', parts=['chassis'])), ] def test_extract_name(self): @@ -559,7 +611,6 @@ def test_extract_name(self): class TestProxyCache(base.TestCase): - class Res(resource.Resource): base_path = 'fake' @@ -570,7 +621,8 @@ class Res(resource.Resource): def setUp(self): super(TestProxyCache, self).setUp( - cloud_config_fixture='clouds_cache.yaml') + cloud_config_fixture='clouds_cache.yaml' + ) self.session = mock.Mock() self.session._sdk_connection = self.cloud @@ -581,19 +633,15 @@ def setUp(self): self.response.history = [] self.response.headers = {} self.response.body = {} - self.response.json = mock.Mock( - return_value=self.response.body) - self.session.request = mock.Mock( - return_value=self.response) + self.response.json = mock.Mock(return_value=self.response.body) + self.session.request = mock.Mock(return_value=self.response) self.sot = proxy.Proxy(self.session) self.sot._connection = self.cloud self.sot.service_type = 'srv' def _get_key(self, id): - return ( - f"srv.fake.fake/{id}." - "{'microversion': None, 'params': {}}") + return f"srv.fake.fake/{id}." "{'microversion': None, 'params': {}}" def test_get_not_in_cache(self): self.cloud._cache_expirations['srv.fake'] = 5 @@ -602,15 +650,15 @@ def test_get_not_in_cache(self): self.session.request.assert_called_with( 'fake/1', 'GET', - connect_retries=mock.ANY, raise_exc=mock.ANY, + connect_retries=mock.ANY, + raise_exc=mock.ANY, global_request_id=mock.ANY, endpoint_filter=mock.ANY, headers=mock.ANY, - microversion=mock.ANY, params=mock.ANY + microversion=mock.ANY, + params=mock.ANY, ) - self.assertIn( - self._get_key(1), - self.cloud._api_cache_keys) + self.assertIn(self._get_key(1), self.cloud._api_cache_keys) def test_get_from_cache(self): key = self._get_key(2) @@ -639,9 +687,7 @@ def test_modify(self): self.session.request.assert_called() self.assertIsNotNone(self.cloud._cache.get(key)) - self.assertEqual( - 'NoValue', - type(self.cloud._cache.get(key)).__name__) + self.assertEqual('NoValue', type(self.cloud._cache.get(key)).__name__) self.assertNotIn(key, self.cloud._api_cache_keys) # next get call again triggers API @@ -663,13 +709,10 @@ def test_get_bypass_cache(self): # validate we got empty body as expected, and not what is in cache self.assertEqual(dict(), self.response.body) self.assertNotIn(key, self.cloud._api_cache_keys) - self.assertEqual( - 'NoValue', - type(self.cloud._cache.get(key)).__name__) + self.assertEqual('NoValue', type(self.cloud._cache.get(key)).__name__) class TestProxyCleanup(base.TestCase): - def setUp(self): super(TestProxyCleanup, self).setUp() @@ -693,40 +736,28 @@ def setUp(self): def test_filters_evaluation_created_at(self): self.assertTrue( self.sot._service_cleanup_resource_filters_evaluation( - self.res, - filters={ - 'created_at': '2020-02-03T00:00:00' - } + self.res, filters={'created_at': '2020-02-03T00:00:00'} ) ) def test_filters_evaluation_created_at_not(self): self.assertFalse( self.sot._service_cleanup_resource_filters_evaluation( - self.res, - filters={ - 'created_at': '2020-01-01T00:00:00' - } + self.res, filters={'created_at': '2020-01-01T00:00:00'} ) ) def test_filters_evaluation_updated_at(self): self.assertTrue( self.sot._service_cleanup_resource_filters_evaluation( - self.res, - filters={ - 'updated_at': '2020-02-03T00:00:00' - } + self.res, filters={'updated_at': '2020-02-03T00:00:00'} ) ) def test_filters_evaluation_updated_at_not(self): self.assertFalse( self.sot._service_cleanup_resource_filters_evaluation( - self.res, - filters={ - 'updated_at': '2020-01-01T00:00:00' - } + self.res, filters={'updated_at': '2020-01-01T00:00:00'} ) ) @@ -734,9 +765,7 @@ def test_filters_evaluation_updated_at_missing(self): self.assertFalse( self.sot._service_cleanup_resource_filters_evaluation( self.res_no_updated, - filters={ - 'updated_at': '2020-01-01T00:00:00' - } + filters={'updated_at': '2020-01-01T00:00:00'}, ) ) @@ -750,19 +779,14 @@ def test_filters_empty(self): def test_service_cleanup_dry_run(self): self.assertTrue( self.sot._service_cleanup_del_res( - self.delete_mock, - self.res, - dry_run=True + self.delete_mock, self.res, dry_run=True ) ) self.delete_mock.assert_not_called() def test_service_cleanup_dry_run_default(self): self.assertTrue( - self.sot._service_cleanup_del_res( - self.delete_mock, - self.res - ) + self.sot._service_cleanup_del_res(self.delete_mock, self.res) ) self.delete_mock.assert_not_called() @@ -783,7 +807,7 @@ def test_service_cleanup_real_run_identified_resources(self): self.delete_mock, self.res, dry_run=False, - identified_resources=rd + identified_resources=rd, ) ) self.delete_mock.assert_called_with(self.res) @@ -795,7 +819,7 @@ def test_service_cleanup_resource_evaluation_false(self): self.delete_mock, self.res, dry_run=False, - resource_evaluation_fn=lambda x, y, z: False + resource_evaluation_fn=lambda x, y, z: False, ) ) self.delete_mock.assert_not_called() @@ -806,7 +830,7 @@ def test_service_cleanup_resource_evaluation_true(self): self.delete_mock, self.res, dry_run=False, - resource_evaluation_fn=lambda x, y, z: True + resource_evaluation_fn=lambda x, y, z: True, ) ) self.delete_mock.assert_called() @@ -818,7 +842,7 @@ def test_service_cleanup_resource_evaluation_override_filters(self): self.res, dry_run=False, resource_evaluation_fn=lambda x, y, z: False, - filters={'created_at': '2200-01-01'} + filters={'created_at': '2200-01-01'}, ) ) @@ -828,7 +852,7 @@ def test_service_cleanup_filters(self): self.delete_mock, self.res, dry_run=False, - filters={'created_at': '2200-01-01'} + filters={'created_at': '2200-01-01'}, ) ) self.delete_mock.assert_called() @@ -841,7 +865,7 @@ def test_service_cleanup_queue(self): self.res, dry_run=False, client_status_queue=q, - filters={'created_at': '2200-01-01'} + filters={'created_at': '2200-01-01'}, ) ) self.assertEqual(self.res, q.get_nowait()) diff --git a/openstack/tests/unit/test_proxy_base.py b/openstack/tests/unit/test_proxy_base.py index a6c62065e..b901c37f5 100644 --- a/openstack/tests/unit/test_proxy_base.py +++ b/openstack/tests/unit/test_proxy_base.py @@ -21,18 +21,27 @@ def setUp(self): self.session = mock.Mock() def _verify( - self, mock_method, test_method, *, - method_args=None, method_kwargs=None, method_result=None, - expected_args=None, expected_kwargs=None, expected_result=None, + self, + mock_method, + test_method, + *, + method_args=None, + method_kwargs=None, + method_result=None, + expected_args=None, + expected_kwargs=None, + expected_result=None, ): with mock.patch(mock_method) as mocked: mocked.return_value = expected_result - if any([ - method_args, - method_kwargs, - expected_args, - expected_kwargs, - ]): + if any( + [ + method_args, + method_kwargs, + expected_args, + expected_kwargs, + ] + ): method_args = method_args or () method_kwargs = method_kwargs or {} expected_args = expected_args or () @@ -77,9 +86,16 @@ def _verify( mocked.assert_called_with(test_method.__self__) def verify_create( - self, test_method, resource_type, base_path=None, *, - method_args=None, method_kwargs=None, - expected_args=None, expected_kwargs=None, expected_result="result", + self, + test_method, + resource_type, + base_path=None, + *, + method_args=None, + method_kwargs=None, + expected_args=None, + expected_kwargs=None, + expected_result="result", mock_method="openstack.proxy.Proxy._create", ): if method_args is None: @@ -103,9 +119,15 @@ def verify_create( ) def verify_delete( - self, test_method, resource_type, ignore_missing=True, *, - method_args=None, method_kwargs=None, - expected_args=None, expected_kwargs=None, + self, + test_method, + resource_type, + ignore_missing=True, + *, + method_args=None, + method_kwargs=None, + expected_args=None, + expected_kwargs=None, mock_method="openstack.proxy.Proxy._delete", ): if method_args is None: @@ -128,9 +150,16 @@ def verify_delete( ) def verify_get( - self, test_method, resource_type, requires_id=False, base_path=None, *, - method_args=None, method_kwargs=None, - expected_args=None, expected_kwargs=None, + self, + test_method, + resource_type, + requires_id=False, + base_path=None, + *, + method_args=None, + method_kwargs=None, + expected_args=None, + expected_kwargs=None, mock_method="openstack.proxy.Proxy._get", ): if method_args is None: @@ -156,15 +185,23 @@ def verify_get_overrided(self, proxy, resource_type, patch_target): proxy._get_resource = mock.Mock(return_value=res) proxy._get(resource_type) res.fetch.assert_called_once_with( - proxy, requires_id=True, + proxy, + requires_id=True, base_path=None, error_message=mock.ANY, - skip_cache=False) + skip_cache=False, + ) def verify_head( - self, test_method, resource_type, base_path=None, *, - method_args=None, method_kwargs=None, - expected_args=None, expected_kwargs=None, + self, + test_method, + resource_type, + base_path=None, + *, + method_args=None, + method_kwargs=None, + expected_args=None, + expected_kwargs=None, mock_method="openstack.proxy.Proxy._head", ): if method_args is None: @@ -184,10 +221,16 @@ def verify_head( ) def verify_find( - self, test_method, resource_type, name_or_id='resource_name', - ignore_missing=True, *, - method_args=None, method_kwargs=None, - expected_args=None, expected_kwargs=None, + self, + test_method, + resource_type, + name_or_id='resource_name', + ignore_missing=True, + *, + method_args=None, + method_kwargs=None, + expected_args=None, + expected_kwargs=None, mock_method="openstack.proxy.Proxy._find", ): method_args = [name_or_id] + (method_args or []) @@ -206,9 +249,16 @@ def verify_find( ) def verify_list( - self, test_method, resource_type, paginated=None, base_path=None, *, - method_args=None, method_kwargs=None, - expected_args=None, expected_kwargs=None, + self, + test_method, + resource_type, + paginated=None, + base_path=None, + *, + method_args=None, + method_kwargs=None, + expected_args=None, + expected_kwargs=None, mock_method="openstack.proxy.Proxy._list", ): if method_args is None: @@ -234,9 +284,16 @@ def verify_list( ) def verify_update( - self, test_method, resource_type, base_path=None, *, - method_args=None, method_kwargs=None, - expected_args=None, expected_kwargs=None, expected_result="result", + self, + test_method, + resource_type, + base_path=None, + *, + method_args=None, + method_kwargs=None, + expected_args=None, + expected_kwargs=None, + expected_result="result", mock_method="openstack.proxy.Proxy._update", ): if method_args is None: @@ -259,7 +316,8 @@ def verify_update( ) def verify_wait_for_status( - self, test_method, + self, + test_method, mock_method="openstack.resource.wait_for_status", **kwargs, ): diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 259eff672..3dc01637a 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -36,7 +36,6 @@ def json(self): class TestComponent(base.TestCase): - class ExampleComponent(resource._BaseComponent): key = "_example" @@ -54,7 +53,8 @@ def test_implementations(self): def test_creation(self): sot = resource._BaseComponent( - "name", type=int, default=1, alternate_id=True, aka="alias") + "name", type=int, default=1, alternate_id=True, aka="alias" + ) self.assertEqual("name", sot.name) self.assertEqual(int, sot.type) @@ -93,8 +93,9 @@ class Parent: instance = Parent() # NOTE: type=dict but the default value is an int. If we didn't # short-circuit the typing part of __get__ it would fail. - sot = TestComponent.ExampleComponent("name", type=dict, - default=expected_result) + sot = TestComponent.ExampleComponent( + "name", type=dict, default=expected_result + ) # Test that we directly return any default value. result = sot.__get__(instance, None) @@ -248,7 +249,6 @@ class Parent: class TestComponentManager(base.TestCase): - def test_create_basic(self): sot = resource._ComponentManager() self.assertEqual(dict(), sot.attributes) @@ -344,7 +344,6 @@ def test_clean(self): class Test_Request(base.TestCase): - def test_create(self): uri = 1 body = 2 @@ -358,22 +357,27 @@ def test_create(self): class TestQueryParameters(base.TestCase): - def test_create(self): location = "location" - mapping = {"first_name": "first-name", - "second_name": {"name": "second-name"}, - "third_name": {"name": "third", "type": int}} + mapping = { + "first_name": "first-name", + "second_name": {"name": "second-name"}, + "third_name": {"name": "third", "type": int}, + } sot = resource.QueryParameters(location, **mapping) - self.assertEqual({"location": "location", - "first_name": "first-name", - "second_name": {"name": "second-name"}, - "third_name": {"name": "third", "type": int}, - "limit": "limit", - "marker": "marker"}, - sot._mapping) + self.assertEqual( + { + "location": "location", + "first_name": "first-name", + "second_name": {"name": "second-name"}, + "third_name": {"name": "third", "type": int}, + "limit": "limit", + "marker": "marker", + }, + sot._mapping, + ) def test_transpose_unmapped(self): def _type(value, rtype): @@ -381,59 +385,76 @@ def _type(value, rtype): return value * 10 location = "location" - mapping = {"first_name": "first-name", - "pet_name": {"name": "pet"}, - "answer": {"name": "answer", "type": int}, - "complex": {"type": _type}} + mapping = { + "first_name": "first-name", + "pet_name": {"name": "pet"}, + "answer": {"name": "answer", "type": int}, + "complex": {"type": _type}, + } sot = resource.QueryParameters(location, **mapping) - result = sot._transpose({"location": "Brooklyn", - "first_name": "Brian", - "pet_name": "Meow", - "answer": "42", - "last_name": "Curtin", - "complex": 1}, - mock.sentinel.resource_type) + result = sot._transpose( + { + "location": "Brooklyn", + "first_name": "Brian", + "pet_name": "Meow", + "answer": "42", + "last_name": "Curtin", + "complex": 1, + }, + mock.sentinel.resource_type, + ) # last_name isn't mapped and shouldn't be included - self.assertEqual({"location": "Brooklyn", "first-name": "Brian", - "pet": "Meow", "answer": 42, "complex": 10}, - result) + self.assertEqual( + { + "location": "Brooklyn", + "first-name": "Brian", + "pet": "Meow", + "answer": 42, + "complex": 10, + }, + result, + ) def test_transpose_not_in_query(self): location = "location" - mapping = {"first_name": "first-name", - "pet_name": {"name": "pet"}, - "answer": {"name": "answer", "type": int}} + mapping = { + "first_name": "first-name", + "pet_name": {"name": "pet"}, + "answer": {"name": "answer", "type": int}, + } sot = resource.QueryParameters(location, **mapping) - result = sot._transpose({"location": "Brooklyn"}, - mock.sentinel.resource_type) + result = sot._transpose( + {"location": "Brooklyn"}, mock.sentinel.resource_type + ) # first_name not being in the query shouldn't affect results - self.assertEqual({"location": "Brooklyn"}, - result) + self.assertEqual({"location": "Brooklyn"}, result) class TestResource(base.TestCase): - def test_initialize_basic(self): body = {"body": 1} header = {"header": 2, "Location": "somewhere"} uri = {"uri": 3} computed = {"computed": 4} - everything = dict(itertools.chain( - body.items(), - header.items(), - uri.items(), - computed.items(), - )) + everything = dict( + itertools.chain( + body.items(), + header.items(), + uri.items(), + computed.items(), + ) + ) mock_collect = mock.Mock() mock_collect.return_value = body, header, uri, computed - with mock.patch.object(resource.Resource, - "_collect_attrs", mock_collect): + with mock.patch.object( + resource.Resource, "_collect_attrs", mock_collect + ): sot = resource.Resource(_synchronized=False, **everything) mock_collect.assert_called_once_with(everything) self.assertIsNone(sot.location) @@ -463,20 +484,20 @@ def test_repr(self): class Test(resource.Resource): def __init__(self): self._body = mock.Mock() - self._body.attributes.items = mock.Mock( - return_value=a.items()) + self._body.attributes.items = mock.Mock(return_value=a.items()) self._header = mock.Mock() self._header.attributes.items = mock.Mock( - return_value=b.items()) + return_value=b.items() + ) self._uri = mock.Mock() - self._uri.attributes.items = mock.Mock( - return_value=c.items()) + self._uri.attributes.items = mock.Mock(return_value=c.items()) self._computed = mock.Mock() self._computed.attributes.items = mock.Mock( - return_value=d.items()) + return_value=d.items() + ) the_repr = repr(Test()) @@ -511,7 +532,8 @@ def test__update(self): computed = "computed" sot._collect_attrs = mock.Mock( - return_value=(body, header, uri, computed)) + return_value=(body, header, uri, computed) + ) sot._body.update = mock.Mock() sot._header.update = mock.Mock() sot._uri.update = mock.Mock() @@ -533,14 +555,18 @@ def test__consume_attrs(self): clientside_key2 = "some_key2" value1 = "value1" value2 = "value2" - mapping = {serverside_key1: clientside_key1, - serverside_key2: clientside_key2} + mapping = { + serverside_key1: clientside_key1, + serverside_key2: clientside_key2, + } other_key = "otherKey" other_value = "other" - attrs = {clientside_key1: value1, - serverside_key2: value2, - other_key: other_value} + attrs = { + clientside_key1: value1, + serverside_key2: value2, + other_key: other_value, + } sot = resource.Resource() @@ -553,8 +579,9 @@ def test__consume_attrs(self): # Make sure that after we've popped our relevant client-side # key off that we are returning it keyed off of its server-side # name. - self.assertDictEqual({serverside_key1: value1, - serverside_key2: value2}, result) + self.assertDictEqual( + {serverside_key1: value1, serverside_key2: value2}, result + ) def test__mapping_defaults(self): # Check that even on an empty class, we get the expected @@ -702,31 +729,36 @@ class Test(resource.Resource): sot = Test() self.assertEqual( - sorted(['foo', 'bar', '_bar', 'bar_local', - 'id', 'name', 'location']), - sorted(sot._attributes()) + sorted( + ['foo', 'bar', '_bar', 'bar_local', 'id', 'name', 'location'] + ), + sorted(sot._attributes()), ) self.assertEqual( sorted(['foo', 'bar', 'bar_local', 'id', 'name', 'location']), - sorted(sot._attributes(include_aliases=False)) + sorted(sot._attributes(include_aliases=False)), ) self.assertEqual( - sorted(['foo', 'bar', '_bar', 'bar_remote', - 'id', 'name', 'location']), - sorted(sot._attributes(remote_names=True)) + sorted( + ['foo', 'bar', '_bar', 'bar_remote', 'id', 'name', 'location'] + ), + sorted(sot._attributes(remote_names=True)), ) self.assertEqual( sorted(['bar', '_bar', 'bar_local', 'id', 'name', 'location']), - sorted(sot._attributes( - components=tuple([resource.Body, resource.Computed]))) + sorted( + sot._attributes( + components=tuple([resource.Body, resource.Computed]) + ) + ), ) self.assertEqual( ('foo',), - tuple(sot._attributes(components=tuple([resource.Header]))) + tuple(sot._attributes(components=tuple([resource.Header]))), ) def test__attributes_iterator(self): @@ -750,13 +782,13 @@ class Child(Parent): # Check we iterate only over headers for attr, component in sot._attributes_iterator( - components=tuple([resource.Header])): + components=tuple([resource.Header]) + ): if attr in expected: expected.remove(attr) self.assertEqual([], expected) def test_to_dict(self): - class Test(resource.Resource): foo = resource.Header('foo') bar = resource.Body('bar', aka='_bar') @@ -769,12 +801,11 @@ class Test(resource.Resource): 'location': None, 'foo': None, 'bar': None, - '_bar': None + '_bar': None, } self.assertEqual(expected, res.to_dict()) def test_to_dict_nested(self): - class Test(resource.Resource): foo = resource.Header('foo') bar = resource.Body('bar') @@ -785,10 +816,7 @@ class Sub(resource.Resource): sub = Sub(id='ANOTHER_ID', foo='bar') - res = Test( - id='FAKE_ID', - bar=sub, - a_list=[sub]) + res = Test(id='FAKE_ID', bar=sub, a_list=[sub]) expected = { 'id': 'FAKE_ID', @@ -801,12 +829,14 @@ class Sub(resource.Resource): 'sub': 'bar', 'location': None, }, - 'a_list': [{ - 'id': 'ANOTHER_ID', - 'name': None, - 'sub': 'bar', - 'location': None, - }], + 'a_list': [ + { + 'id': 'ANOTHER_ID', + 'name': None, + 'sub': 'bar', + 'location': None, + } + ], } self.assertEqual(expected, res.to_dict()) a_munch = res.to_dict(_to_munch=True) @@ -816,7 +846,6 @@ class Sub(resource.Resource): self.assertEqual(a_munch.a_list[0].sub, 'bar') def test_to_dict_no_body(self): - class Test(resource.Resource): foo = resource.Header('foo') bar = resource.Body('bar') @@ -830,7 +859,6 @@ class Test(resource.Resource): self.assertEqual(expected, res.to_dict(body=False)) def test_to_dict_no_header(self): - class Test(resource.Resource): foo = resource.Header('foo') bar = resource.Body('bar') @@ -846,7 +874,6 @@ class Test(resource.Resource): self.assertEqual(expected, res.to_dict(headers=False)) def test_to_dict_ignore_none(self): - class Test(resource.Resource): foo = resource.Header('foo') bar = resource.Body('bar') @@ -860,7 +887,6 @@ class Test(resource.Resource): self.assertEqual(expected, res.to_dict(ignore_none=True)) def test_to_dict_with_mro(self): - class Parent(resource.Resource): foo = resource.Header('foo') bar = resource.Body('bar', aka='_bar') @@ -879,7 +905,7 @@ class Child(Parent): 'bar_new': None, 'id': 'FAKE_ID', 'location': None, - 'name': None + 'name': None, } self.assertEqual(expected, res.to_dict()) @@ -910,12 +936,12 @@ class Test(resource.Resource): actual = json.dumps(res, sort_keys=True) self.assertEqual(expected, actual) - response = FakeResponse({ - 'foo': 'new_bar'}) + response = FakeResponse({'foo': 'new_bar'}) res._translate_response(response) - expected = ('{"foo": "new_bar", "id": null, ' - '"location": null, "name": null}') + expected = ( + '{"foo": "new_bar", "id": null, ' '"location": null, "name": null}' + ) actual = json.dumps(res, sort_keys=True) self.assertEqual(expected, actual) @@ -925,11 +951,7 @@ class Test(resource.Resource): bar = resource.Body('bar') foot = resource.Body('foot') - data = { - 'foo': 'bar', - 'bar': 'foo\n', - 'foot': 'a:b:c:d' - } + data = {'foo': 'bar', 'bar': 'foo\n', 'foot': 'a:b:c:d'} res = Test(**data) for k, v in res.items(): @@ -947,13 +969,15 @@ class Test(resource.Resource): self.assertEqual('bar', res.foo_alias) self.assertTrue('foo' in res.keys()) self.assertTrue('foo_alias' in res.keys()) - expected = utils.Munch({ - 'id': None, - 'name': 'test', - 'location': None, - 'foo': 'bar', - 'foo_alias': 'bar' - }) + expected = utils.Munch( + { + 'id': None, + 'name': 'test', + 'location': None, + 'foo': 'bar', + 'foo_alias': 'bar', + } + ) actual = utils.Munch(res) self.assertEqual(expected, actual) self.assertEqual(expected, res.toDict()) @@ -962,7 +986,6 @@ class Test(resource.Resource): self.assertDictEqual(expected, dict(res)) def test_access_by_resource_name(self): - class Test(resource.Resource): blah = resource.Body("blah_resource") @@ -972,7 +995,6 @@ class Test(resource.Resource): self.assertEqual(result, sot.blah) def test_to_dict_value_error(self): - class Test(resource.Resource): foo = resource.Header('foo') bar = resource.Body('bar') @@ -980,15 +1002,14 @@ class Test(resource.Resource): res = Test(id='FAKE_ID') err = self.assertRaises( - ValueError, - res.to_dict, - body=False, headers=False, computed=False) + ValueError, res.to_dict, body=False, headers=False, computed=False + ) self.assertEqual( 'At least one of `body`, `headers` or `computed` must be True', - str(err)) + str(err), + ) def test_to_dict_with_mro_no_override(self): - class Parent(resource.Resource): header = resource.Header('HEADER') body = resource.Body('BODY') @@ -1006,7 +1027,7 @@ class Child(Parent): 'header': 'HEADER_VALUE', 'id': 'FAKE_ID', 'location': None, - 'name': None + 'name': None, } self.assertEqual(expected, res.to_dict()) @@ -1061,8 +1082,12 @@ class Test(resource.Resource): the_id = "id" body_value = "body" header_value = "header" - sot = Test(id=the_id, body_attr=body_value, header_attr=header_value, - _synchronized=False) + sot = Test( + id=the_id, + body_attr=body_value, + header_attr=header_value, + _synchronized=False, + ) result = sot._prepare_request(requires_id=True) @@ -1079,8 +1104,12 @@ class Test(resource.Resource): the_id = "id" body_value = "body" header_value = "header" - sot = Test(id=the_id, body_attr=body_value, header_attr=header_value, - _synchronized=False) + sot = Test( + id=the_id, + body_attr=body_value, + header_attr=header_value, + _synchronized=False, + ) sot._body._dirty.discard("id") result = sot._prepare_request(requires_id=True) @@ -1092,8 +1121,9 @@ class Test(resource.Resource): def test__prepare_request_missing_id(self): sot = resource.Resource(id=None) - self.assertRaises(exceptions.InvalidRequest, - sot._prepare_request, requires_id=True) + self.assertRaises( + exceptions.InvalidRequest, sot._prepare_request, requires_id=True + ) def test__prepare_request_with_resource_key(self): key = "key" @@ -1106,8 +1136,9 @@ class Test(resource.Resource): body_value = "body" header_value = "header" - sot = Test(body_attr=body_value, header_attr=header_value, - _synchronized=False) + sot = Test( + body_attr=body_value, header_attr=header_value, _synchronized=False + ) result = sot._prepare_request(requires_id=False, prepend_key=True) @@ -1127,13 +1158,15 @@ class Test(resource.Resource): body_value = "body" header_value = "header" - sot = Test(body_attr=body_value, header_attr=header_value, - _synchronized=False) + sot = Test( + body_attr=body_value, header_attr=header_value, _synchronized=False + ) result = sot._prepare_request( requires_id=False, prepend_key=True, - resource_request_key=override_key) + resource_request_key=override_key, + ) self.assertEqual("/something", result.url) self.assertEqual({override_key: {"x": body_value}}, result.body) @@ -1153,8 +1186,9 @@ class Test(resource.Resource): result = sot._prepare_request(requires_id=True, patch=True) self.assertEqual("something/id", result.url) - self.assertEqual([{'op': 'replace', 'path': '/x', 'value': 3}], - result.body) + self.assertEqual( + [{'op': 'replace', 'path': '/x', 'value': 3}], result.body + ) def test__prepare_request_with_patch_not_synchronized(self): class Test(resource.Resource): @@ -1169,8 +1203,9 @@ class Test(resource.Resource): result = sot._prepare_request(requires_id=True, patch=True) self.assertEqual("something/id", result.url) - self.assertEqual([{'op': 'add', 'path': '/x', 'value': 1}], - result.body) + self.assertEqual( + [{'op': 'add', 'path': '/x', 'value': 1}], result.body + ) def test__prepare_request_with_patch_params(self): class Test(resource.Resource): @@ -1183,15 +1218,16 @@ class Test(resource.Resource): sot = Test.existing(id=the_id, x=1, y=2) sot.x = 3 - params = [('foo', 'bar'), - ('life', 42)] + params = [('foo', 'bar'), ('life', 42)] - result = sot._prepare_request(requires_id=True, patch=True, - params=params) + result = sot._prepare_request( + requires_id=True, patch=True, params=params + ) self.assertEqual("something/id?foo=bar&life=42", result.url) - self.assertEqual([{'op': 'replace', 'path': '/x', 'value': 3}], - result.body) + self.assertEqual( + [{'op': 'replace', 'path': '/x', 'value': 3}], result.body + ) def test__translate_response_no_body(self): class Test(resource.Resource): @@ -1275,29 +1311,23 @@ class Test(resource.Resource): properties = resource.Body("properties") _store_unknown_attrs_as_properties = True - sot = Test.new(**{ - 'dummy': 'value', - }) - self.assertDictEqual({'dummy': 'value'}, sot.properties) - self.assertDictEqual( - {'dummy': 'value'}, sot.to_dict()['properties'] - ) - self.assertDictEqual( - {'dummy': 'value'}, sot['properties'] + sot = Test.new( + **{ + 'dummy': 'value', + } ) + self.assertDictEqual({'dummy': 'value'}, sot.properties) + self.assertDictEqual({'dummy': 'value'}, sot.to_dict()['properties']) + self.assertDictEqual({'dummy': 'value'}, sot['properties']) self.assertEqual('value', sot['properties']['dummy']) - sot = Test.new(**{ - 'dummy': 'value', - 'properties': 'a,b,c' - }) + sot = Test.new(**{'dummy': 'value', 'properties': 'a,b,c'}) self.assertDictEqual( - {'dummy': 'value', 'properties': 'a,b,c'}, - sot.properties + {'dummy': 'value', 'properties': 'a,b,c'}, sot.properties ) self.assertDictEqual( {'dummy': 'value', 'properties': 'a,b,c'}, - sot.to_dict()['properties'] + sot.to_dict()['properties'], ) sot = Test.new(**{'properties': None}) @@ -1308,18 +1338,22 @@ def test_unknown_attrs_not_stored(self): class Test(resource.Resource): properties = resource.Body("properties") - sot = Test.new(**{ - 'dummy': 'value', - }) + sot = Test.new( + **{ + 'dummy': 'value', + } + ) self.assertIsNone(sot.properties) def test_unknown_attrs_not_stored1(self): class Test(resource.Resource): _store_unknown_attrs_as_properties = True - sot = Test.new(**{ - 'dummy': 'value', - }) + sot = Test.new( + **{ + 'dummy': 'value', + } + ) self.assertRaises(KeyError, sot.__getitem__, 'properties') def test_unknown_attrs_under_props_set(self): @@ -1327,9 +1361,11 @@ class Test(resource.Resource): properties = resource.Body("properties") _store_unknown_attrs_as_properties = True - sot = Test.new(**{ - 'dummy': 'value', - }) + sot = Test.new( + **{ + 'dummy': 'value', + } + ) sot['properties'] = {'dummy': 'new_value'} self.assertEqual('new_value', sot['properties']['dummy']) @@ -1342,22 +1378,16 @@ class Test(resource.Resource): _store_unknown_attrs_as_properties = True # Unknown attribute given as root attribute - sot = Test.new(**{ - 'dummy': 'value', - 'properties': 'a,b,c' - }) + sot = Test.new(**{'dummy': 'value', 'properties': 'a,b,c'}) request_body = sot._prepare_request(requires_id=False).body self.assertEqual('value', request_body['dummy']) self.assertEqual('a,b,c', request_body['properties']) # properties are already a dict - sot = Test.new(**{ - 'properties': { - 'properties': 'a,b,c', - 'dummy': 'value' - } - }) + sot = Test.new( + **{'properties': {'properties': 'a,b,c', 'dummy': 'value'}} + ) request_body = sot._prepare_request(requires_id=False).body self.assertEqual('value', request_body['dummy']) @@ -1367,17 +1397,16 @@ def test_unknown_attrs_prepare_request_no_unpack_dict(self): # if props type is not None - ensure no unpacking is done class Test(resource.Resource): properties = resource.Body("properties", type=dict) - sot = Test.new(**{ - 'properties': { - 'properties': 'a,b,c', - 'dummy': 'value' - } - }) + + sot = Test.new( + **{'properties': {'properties': 'a,b,c', 'dummy': 'value'}} + ) request_body = sot._prepare_request(requires_id=False).body self.assertDictEqual( {'dummy': 'value', 'properties': 'a,b,c'}, - request_body['properties']) + request_body['properties'], + ) def test_unknown_attrs_prepare_request_patch_unpacked(self): class Test(resource.Resource): @@ -1385,21 +1414,15 @@ class Test(resource.Resource): _store_unknown_attrs_as_properties = True commit_jsonpatch = True - sot = Test.existing(**{ - 'dummy': 'value', - 'properties': 'a,b,c' - }) + sot = Test.existing(**{'dummy': 'value', 'properties': 'a,b,c'}) sot._update(**{'properties': {'dummy': 'new_value'}}) request_body = sot._prepare_request(requires_id=False, patch=True).body self.assertDictEqual( - { - u'path': u'/dummy', - u'value': u'new_value', - u'op': u'replace' - }, - request_body[0]) + {u'path': u'/dummy', u'value': u'new_value', u'op': u'replace'}, + request_body[0], + ) def test_unknown_attrs_under_props_translate_response(self): class Test(resource.Resource): @@ -1414,8 +1437,7 @@ class Test(resource.Resource): sot._translate_response(response, has_body=True) self.assertDictEqual( - {'dummy': 'value', 'properties': 'a,b,c'}, - sot.properties + {'dummy': 'value', 'properties': 'a,b,c'}, sot.properties ) def test_unknown_attrs_in_body_create(self): @@ -1423,10 +1445,7 @@ class Test(resource.Resource): known_param = resource.Body("known_param") _allow_unknown_attrs_in_body = True - sot = Test.new(**{ - 'known_param': 'v1', - 'unknown_param': 'v2' - }) + sot = Test.new(**{'known_param': 'v1', 'unknown_param': 'v2'}) self.assertEqual('v1', sot.known_param) self.assertEqual('v2', sot.unknown_param) @@ -1435,10 +1454,7 @@ class Test(resource.Resource): known_param = resource.Body("known_param") properties = resource.Body("properties") - sot = Test.new(**{ - 'known_param': 'v1', - 'unknown_param': 'v2' - }) + sot = Test.new(**{'known_param': 'v1', 'unknown_param': 'v2'}) self.assertEqual('v1', sot.known_param) self.assertNotIn('unknown_param', sot) @@ -1447,9 +1463,11 @@ class Test(resource.Resource): known_param = resource.Body("known_param") _allow_unknown_attrs_in_body = True - sot = Test.new(**{ - 'known_param': 'v1', - }) + sot = Test.new( + **{ + 'known_param': 'v1', + } + ) sot['unknown_param'] = 'v2' self.assertEqual('v1', sot.known_param) @@ -1460,17 +1478,21 @@ class Test(resource.Resource): known_param = resource.Body("known_param") _allow_unknown_attrs_in_body = False - sot = Test.new(**{ - 'known_param': 'v1', - }) + sot = Test.new( + **{ + 'known_param': 'v1', + } + ) try: sot['unknown_param'] = 'v2' except KeyError: self.assertEqual('v1', sot.known_param) self.assertNotIn('unknown_param', sot) return - self.fail("Parameter 'unknown_param' unexpectedly set through the " - "dict interface") + self.fail( + "Parameter 'unknown_param' unexpectedly set through the " + "dict interface" + ) def test_unknown_attrs_in_body_translate_response(self): class Test(resource.Resource): @@ -1502,7 +1524,6 @@ class Test(resource.Resource): class TestResourceActions(base.TestCase): - def setUp(self): super(TestResourceActions, self).setUp() @@ -1546,14 +1567,24 @@ class Test(resource.Resource): self.session.default_microversion = None self.session.retriable_status_codes = None - self.endpoint_data = mock.Mock(max_microversion='1.99', - min_microversion=None) + self.endpoint_data = mock.Mock( + max_microversion='1.99', min_microversion=None + ) self.session.get_endpoint_data.return_value = self.endpoint_data - def _test_create(self, cls, requires_id=False, prepend_key=False, - microversion=None, base_path=None, params=None, - id_marked_dirty=True, explicit_microversion=None, - resource_request_key=None, resource_response_key=None): + def _test_create( + self, + cls, + requires_id=False, + prepend_key=False, + microversion=None, + base_path=None, + params=None, + id_marked_dirty=True, + explicit_microversion=None, + resource_request_key=None, + resource_response_key=None, + ): id = "id" if requires_id else None sot = cls(id=id) sot._prepare_request = mock.Mock(return_value=self.request) @@ -1564,40 +1595,51 @@ def _test_create(self, cls, requires_id=False, prepend_key=False, if explicit_microversion is not None: kwargs['microversion'] = explicit_microversion microversion = explicit_microversion - result = sot.create(self.session, prepend_key=prepend_key, - base_path=base_path, - resource_request_key=resource_request_key, - resource_response_key=resource_response_key, - **kwargs) + result = sot.create( + self.session, + prepend_key=prepend_key, + base_path=base_path, + resource_request_key=resource_request_key, + resource_response_key=resource_response_key, + **kwargs + ) - id_is_dirty = ('id' in sot._body._dirty) + id_is_dirty = 'id' in sot._body._dirty self.assertEqual(id_marked_dirty, id_is_dirty) prepare_kwargs = {} if resource_request_key is not None: prepare_kwargs['resource_request_key'] = resource_request_key sot._prepare_request.assert_called_once_with( - requires_id=requires_id, prepend_key=prepend_key, - base_path=base_path, **prepare_kwargs) + requires_id=requires_id, + prepend_key=prepend_key, + base_path=base_path, + **prepare_kwargs + ) if requires_id: self.session.put.assert_called_once_with( self.request.url, - json=self.request.body, headers=self.request.headers, - microversion=microversion, params=params) + json=self.request.body, + headers=self.request.headers, + microversion=microversion, + params=params, + ) else: self.session.post.assert_called_once_with( self.request.url, - json=self.request.body, headers=self.request.headers, - microversion=microversion, params=params) + json=self.request.body, + headers=self.request.headers, + microversion=microversion, + params=params, + ) self.assertEqual(sot.microversion, microversion) res_kwargs = {} if resource_response_key is not None: res_kwargs['resource_response_key'] = resource_response_key sot._translate_response.assert_called_once_with( - self.response, - has_body=sot.has_body, - **res_kwargs) + self.response, has_body=sot.has_body, **res_kwargs + ) self.assertEqual(result, sot) def test_put_create(self): @@ -1617,8 +1659,9 @@ class Test(resource.Resource): create_method = 'PUT' create_exclude_id_from_body = True - self._test_create(Test, requires_id=True, prepend_key=True, - id_marked_dirty=False) + self._test_create( + Test, requires_id=True, prepend_key=True, id_marked_dirty=False + ) def test_put_create_with_microversion(self): class Test(resource.Resource): @@ -1628,8 +1671,9 @@ class Test(resource.Resource): create_method = 'PUT' _max_microversion = '1.42' - self._test_create(Test, requires_id=True, prepend_key=True, - microversion='1.42') + self._test_create( + Test, requires_id=True, prepend_key=True, microversion='1.42' + ) def test_put_create_with_explicit_microversion(self): class Test(resource.Resource): @@ -1639,8 +1683,12 @@ class Test(resource.Resource): create_method = 'PUT' _max_microversion = '1.99' - self._test_create(Test, requires_id=True, prepend_key=True, - explicit_microversion='1.42') + self._test_create( + Test, + requires_id=True, + prepend_key=True, + explicit_microversion='1.42', + ) def test_put_create_with_params(self): class Test(resource.Resource): @@ -1649,8 +1697,9 @@ class Test(resource.Resource): allow_create = True create_method = 'PUT' - self._test_create(Test, requires_id=True, prepend_key=True, - params={'answer': 42}) + self._test_create( + Test, requires_id=True, prepend_key=True, params={'answer': 42} + ) def test_post_create(self): class Test(resource.Resource): @@ -1673,7 +1722,8 @@ class Test(resource.Resource): Test, requires_id=False, prepend_key=True, - resource_request_key="OtherKey") + resource_request_key="OtherKey", + ) def test_post_create_override_response_key(self): class Test(resource.Resource): @@ -1687,7 +1737,8 @@ class Test(resource.Resource): Test, requires_id=False, prepend_key=True, - resource_response_key="OtherKey") + resource_response_key="OtherKey", + ) def test_post_create_override_key_both(self): class Test(resource.Resource): @@ -1702,7 +1753,8 @@ class Test(resource.Resource): requires_id=False, prepend_key=True, resource_request_key="OtherKey", - resource_response_key="SomeOtherKey") + resource_response_key="SomeOtherKey", + ) def test_post_create_base_path(self): class Test(resource.Resource): @@ -1711,8 +1763,9 @@ class Test(resource.Resource): allow_create = True create_method = 'POST' - self._test_create(Test, requires_id=False, prepend_key=True, - base_path='dummy') + self._test_create( + Test, requires_id=False, prepend_key=True, base_path='dummy' + ) def test_post_create_with_params(self): class Test(resource.Resource): @@ -1721,46 +1774,52 @@ class Test(resource.Resource): allow_create = True create_method = 'POST' - self._test_create(Test, requires_id=False, prepend_key=True, - params={'answer': 42}) + self._test_create( + Test, requires_id=False, prepend_key=True, params={'answer': 42} + ) def test_fetch(self): result = self.sot.fetch(self.session) self.sot._prepare_request.assert_called_once_with( - requires_id=True, base_path=None) + requires_id=True, base_path=None + ) self.session.get.assert_called_once_with( - self.request.url, microversion=None, params={}, - skip_cache=False) + self.request.url, microversion=None, params={}, skip_cache=False + ) self.assertIsNone(self.sot.microversion) self.sot._translate_response.assert_called_once_with(self.response) self.assertEqual(result, self.sot) def test_fetch_with_override_key(self): - result = self.sot.fetch( - self.session, resource_response_key="SomeKey") + result = self.sot.fetch(self.session, resource_response_key="SomeKey") self.sot._prepare_request.assert_called_once_with( - requires_id=True, base_path=None) + requires_id=True, base_path=None + ) self.session.get.assert_called_once_with( - self.request.url, microversion=None, params={}, - skip_cache=False) + self.request.url, microversion=None, params={}, skip_cache=False + ) self.assertIsNone(self.sot.microversion) self.sot._translate_response.assert_called_once_with( - self.response, - resource_response_key="SomeKey") + self.response, resource_response_key="SomeKey" + ) self.assertEqual(result, self.sot) def test_fetch_with_params(self): result = self.sot.fetch(self.session, fields='a,b') self.sot._prepare_request.assert_called_once_with( - requires_id=True, base_path=None) + requires_id=True, base_path=None + ) self.session.get.assert_called_once_with( - self.request.url, microversion=None, params={'fields': 'a,b'}, - skip_cache=False) + self.request.url, + microversion=None, + params={'fields': 'a,b'}, + skip_cache=False, + ) self.assertIsNone(self.sot.microversion) self.sot._translate_response.assert_called_once_with(self.response) @@ -1780,10 +1839,11 @@ class Test(resource.Resource): result = sot.fetch(self.session) sot._prepare_request.assert_called_once_with( - requires_id=True, base_path=None) + requires_id=True, base_path=None + ) self.session.get.assert_called_once_with( - self.request.url, microversion='1.42', params={}, - skip_cache=False) + self.request.url, microversion='1.42', params={}, skip_cache=False + ) self.assertEqual(sot.microversion, '1.42') sot._translate_response.assert_called_once_with(self.response) @@ -1803,10 +1863,11 @@ class Test(resource.Resource): result = sot.fetch(self.session, microversion='1.42') sot._prepare_request.assert_called_once_with( - requires_id=True, base_path=None) + requires_id=True, base_path=None + ) self.session.get.assert_called_once_with( - self.request.url, microversion='1.42', params={}, - skip_cache=False) + self.request.url, microversion='1.42', params={}, skip_cache=False + ) self.assertEqual(sot.microversion, '1.42') sot._translate_response.assert_called_once_with(self.response) @@ -1816,10 +1877,11 @@ def test_fetch_not_requires_id(self): result = self.sot.fetch(self.session, False) self.sot._prepare_request.assert_called_once_with( - requires_id=False, base_path=None) + requires_id=False, base_path=None + ) self.session.get.assert_called_once_with( - self.request.url, microversion=None, params={}, - skip_cache=False) + self.request.url, microversion=None, params={}, skip_cache=False + ) self.sot._translate_response.assert_called_once_with(self.response) self.assertEqual(result, self.sot) @@ -1828,11 +1890,11 @@ def test_fetch_base_path(self): result = self.sot.fetch(self.session, False, base_path='dummy') self.sot._prepare_request.assert_called_once_with( - requires_id=False, - base_path='dummy') + requires_id=False, base_path='dummy' + ) self.session.get.assert_called_once_with( - self.request.url, microversion=None, params={}, - skip_cache=False) + self.request.url, microversion=None, params={}, skip_cache=False + ) self.sot._translate_response.assert_called_once_with(self.response) self.assertEqual(result, self.sot) @@ -1842,12 +1904,13 @@ def test_head(self): self.sot._prepare_request.assert_called_once_with(base_path=None) self.session.head.assert_called_once_with( - self.request.url, - microversion=None) + self.request.url, microversion=None + ) self.assertIsNone(self.sot.microversion) self.sot._translate_response.assert_called_once_with( - self.response, has_body=False) + self.response, has_body=False + ) self.assertEqual(result, self.sot) def test_head_base_path(self): @@ -1855,12 +1918,13 @@ def test_head_base_path(self): self.sot._prepare_request.assert_called_once_with(base_path='dummy') self.session.head.assert_called_once_with( - self.request.url, - microversion=None) + self.request.url, microversion=None + ) self.assertIsNone(self.sot.microversion) self.sot._translate_response.assert_called_once_with( - self.response, has_body=False) + self.response, has_body=False + ) self.assertEqual(result, self.sot) def test_head_with_microversion(self): @@ -1878,18 +1942,26 @@ class Test(resource.Resource): sot._prepare_request.assert_called_once_with(base_path=None) self.session.head.assert_called_once_with( - self.request.url, - microversion='1.42') + self.request.url, microversion='1.42' + ) self.assertEqual(sot.microversion, '1.42') sot._translate_response.assert_called_once_with( - self.response, has_body=False) + self.response, has_body=False + ) self.assertEqual(result, sot) - def _test_commit(self, commit_method='PUT', prepend_key=True, - has_body=True, microversion=None, - commit_args=None, expected_args=None, base_path=None, - explicit_microversion=None): + def _test_commit( + self, + commit_method='PUT', + prepend_key=True, + has_body=True, + microversion=None, + commit_args=None, + expected_args=None, + base_path=None, + explicit_microversion=None, + ): self.sot.commit_method = commit_method # Need to make sot look dirty so we can attempt an update @@ -1900,73 +1972,101 @@ def _test_commit(self, commit_method='PUT', prepend_key=True, if explicit_microversion is not None: commit_args['microversion'] = explicit_microversion microversion = explicit_microversion - self.sot.commit(self.session, prepend_key=prepend_key, - has_body=has_body, base_path=base_path, - **commit_args) + self.sot.commit( + self.session, + prepend_key=prepend_key, + has_body=has_body, + base_path=base_path, + **commit_args + ) self.sot._prepare_request.assert_called_once_with( - prepend_key=prepend_key, base_path=base_path) + prepend_key=prepend_key, base_path=base_path + ) if commit_method == 'PATCH': self.session.patch.assert_called_once_with( self.request.url, - json=self.request.body, headers=self.request.headers, - microversion=microversion, **(expected_args or {})) + json=self.request.body, + headers=self.request.headers, + microversion=microversion, + **(expected_args or {}) + ) elif commit_method == 'POST': self.session.post.assert_called_once_with( self.request.url, - json=self.request.body, headers=self.request.headers, - microversion=microversion, **(expected_args or {})) + json=self.request.body, + headers=self.request.headers, + microversion=microversion, + **(expected_args or {}) + ) elif commit_method == 'PUT': self.session.put.assert_called_once_with( self.request.url, - json=self.request.body, headers=self.request.headers, - microversion=microversion, **(expected_args or {})) + json=self.request.body, + headers=self.request.headers, + microversion=microversion, + **(expected_args or {}) + ) self.assertEqual(self.sot.microversion, microversion) self.sot._translate_response.assert_called_once_with( - self.response, has_body=has_body) + self.response, has_body=has_body + ) def test_commit_put(self): self._test_commit(commit_method='PUT', prepend_key=True, has_body=True) def test_commit_patch(self): self._test_commit( - commit_method='PATCH', prepend_key=False, has_body=False) + commit_method='PATCH', prepend_key=False, has_body=False + ) def test_commit_base_path(self): - self._test_commit(commit_method='PUT', prepend_key=True, has_body=True, - base_path='dummy') + self._test_commit( + commit_method='PUT', + prepend_key=True, + has_body=True, + base_path='dummy', + ) def test_commit_patch_retry_on_conflict(self): self._test_commit( commit_method='PATCH', commit_args={'retry_on_conflict': True}, - expected_args={'retriable_status_codes': {409}}) + expected_args={'retriable_status_codes': {409}}, + ) def test_commit_put_retry_on_conflict(self): self._test_commit( commit_method='PUT', commit_args={'retry_on_conflict': True}, - expected_args={'retriable_status_codes': {409}}) + expected_args={'retriable_status_codes': {409}}, + ) def test_commit_patch_no_retry_on_conflict(self): self.session.retriable_status_codes = {409, 503} self._test_commit( commit_method='PATCH', commit_args={'retry_on_conflict': False}, - expected_args={'retriable_status_codes': {503}}) + expected_args={'retriable_status_codes': {503}}, + ) def test_commit_put_no_retry_on_conflict(self): self.session.retriable_status_codes = {409, 503} self._test_commit( commit_method='PATCH', commit_args={'retry_on_conflict': False}, - expected_args={'retriable_status_codes': {503}}) + expected_args={'retriable_status_codes': {503}}, + ) def test_commit_put_explicit_microversion(self): - self._test_commit(commit_method='PUT', prepend_key=True, has_body=True, - explicit_microversion='1.42') + self._test_commit( + commit_method='PUT', + prepend_key=True, + has_body=True, + explicit_microversion='1.42', + ) def test_commit_not_dirty(self): self.sot._body = mock.Mock() @@ -1987,16 +2087,21 @@ class Test(resource.Resource): nested = resource.Body('renamed') other = resource.Body('other') - test_patch = [{'path': '/attr', 'op': 'replace', 'value': 'new'}, - {'path': '/nested/dog', 'op': 'remove'}, - {'path': '/nested/cat', 'op': 'add', 'value': 'meow'}] - expected = [{'path': '/attr', 'op': 'replace', 'value': 'new'}, - {'path': '/renamed/dog', 'op': 'remove'}, - {'path': '/renamed/cat', 'op': 'add', 'value': 'meow'}] + test_patch = [ + {'path': '/attr', 'op': 'replace', 'value': 'new'}, + {'path': '/nested/dog', 'op': 'remove'}, + {'path': '/nested/cat', 'op': 'add', 'value': 'meow'}, + ] + expected = [ + {'path': '/attr', 'op': 'replace', 'value': 'new'}, + {'path': '/renamed/dog', 'op': 'remove'}, + {'path': '/renamed/cat', 'op': 'add', 'value': 'meow'}, + ] sot = Test.existing(id=1, attr=42, nested={'dog': 'bark'}) sot.patch(self.session, test_patch) self.session.patch.assert_called_once_with( - '/1', json=expected, headers=mock.ANY, microversion=None) + '/1', json=expected, headers=mock.ANY, microversion=None + ) def test_patch_with_server_names(self): class Test(resource.Resource): @@ -2007,13 +2112,16 @@ class Test(resource.Resource): nested = resource.Body('renamed') other = resource.Body('other') - test_patch = [{'path': '/attr', 'op': 'replace', 'value': 'new'}, - {'path': '/renamed/dog', 'op': 'remove'}, - {'path': '/renamed/cat', 'op': 'add', 'value': 'meow'}] + test_patch = [ + {'path': '/attr', 'op': 'replace', 'value': 'new'}, + {'path': '/renamed/dog', 'op': 'remove'}, + {'path': '/renamed/cat', 'op': 'add', 'value': 'meow'}, + ] sot = Test.existing(id=1, attr=42, nested={'dog': 'bark'}) sot.patch(self.session, test_patch) self.session.patch.assert_called_once_with( - '/1', json=test_patch, headers=mock.ANY, microversion=None) + '/1', json=test_patch, headers=mock.ANY, microversion=None + ) def test_patch_with_changed_fields(self): class Test(resource.Resource): @@ -2027,22 +2135,25 @@ class Test(resource.Resource): sot.attr = 'new' sot.patch(self.session, {'path': '/renamed/dog', 'op': 'remove'}) - expected = [{'path': '/attr', 'op': 'replace', 'value': 'new'}, - {'path': '/renamed/dog', 'op': 'remove'}] + expected = [ + {'path': '/attr', 'op': 'replace', 'value': 'new'}, + {'path': '/renamed/dog', 'op': 'remove'}, + ] self.session.patch.assert_called_once_with( - '/1', json=expected, headers=mock.ANY, microversion=None) + '/1', json=expected, headers=mock.ANY, microversion=None + ) def test_delete(self): result = self.sot.delete(self.session) self.sot._prepare_request.assert_called_once_with() self.session.delete.assert_called_once_with( - self.request.url, - headers='headers', - microversion=None) + self.request.url, headers='headers', microversion=None + ) self.sot._translate_response.assert_called_once_with( - self.response, has_body=False) + self.response, has_body=False + ) self.assertEqual(result, self.sot) def test_delete_with_microversion(self): @@ -2060,12 +2171,12 @@ class Test(resource.Resource): sot._prepare_request.assert_called_once_with() self.session.delete.assert_called_once_with( - self.request.url, - headers='headers', - microversion='1.42') + self.request.url, headers='headers', microversion='1.42' + ) sot._translate_response.assert_called_once_with( - self.response, has_body=False) + self.response, has_body=False + ) self.assertEqual(result, sot) def test_delete_with_explicit_microversion(self): @@ -2083,12 +2194,12 @@ class Test(resource.Resource): sot._prepare_request.assert_called_once_with() self.session.delete.assert_called_once_with( - self.request.url, - headers='headers', - microversion='1.42') + self.request.url, headers='headers', microversion='1.42' + ) sot._translate_response.assert_called_once_with( - self.response, has_body=False) + self.response, has_body=False + ) self.assertEqual(result, sot) # NOTE: As list returns a generator, testing it requires consuming @@ -2107,7 +2218,8 @@ def test_list_empty_response(self): self.base_path, headers={"Accept": "application/json"}, params={}, - microversion=None) + microversion=None, + ) self.assertEqual([], result) @@ -2144,7 +2256,8 @@ def test_list_one_page_response_not_paginated(self): self.base_path, headers={"Accept": "application/json"}, params={}, - microversion=None) + microversion=None, + ) self.assertEqual(1, len(results)) self.assertEqual(id_value, results[0].id) @@ -2172,7 +2285,8 @@ class Test(self.test_class): self.base_path, headers={"Accept": "application/json"}, params={}, - microversion=None) + microversion=None, + ) self.assertEqual(1, len(results)) self.assertEqual(id_value, results[0].id) @@ -2185,10 +2299,12 @@ def test_list_response_paginated_without_links(self): mock_response.links = {} mock_response.json.return_value = { "resources": [{"id": ids[0]}], - "resources_links": [{ - "href": "https://example.com/next-url", - "rel": "next", - }] + "resources_links": [ + { + "href": "https://example.com/next-url", + "rel": "next", + } + ], } mock_response2 = mock.Mock() mock_response2.status_code = 200 @@ -2205,15 +2321,23 @@ def test_list_response_paginated_without_links(self): self.assertEqual(ids[0], results[0].id) self.assertEqual(ids[1], results[1].id) self.assertEqual( - mock.call('base_path', - headers={'Accept': 'application/json'}, params={}, - microversion=None), - self.session.get.mock_calls[0]) + mock.call( + 'base_path', + headers={'Accept': 'application/json'}, + params={}, + microversion=None, + ), + self.session.get.mock_calls[0], + ) self.assertEqual( - mock.call('https://example.com/next-url', - headers={'Accept': 'application/json'}, params={}, - microversion=None), - self.session.get.mock_calls[1]) + mock.call( + 'https://example.com/next-url', + headers={'Accept': 'application/json'}, + params={}, + microversion=None, + ), + self.session.get.mock_calls[1], + ) self.assertEqual(2, len(self.session.get.call_args_list)) self.assertIsInstance(results[0], self.test_class) @@ -2225,13 +2349,17 @@ def test_list_response_paginated_with_links(self): mock_response.json.side_effect = [ { "resources": [{"id": ids[0]}], - "resources_links": [{ - "href": "https://example.com/next-url", - "rel": "next", - }] - }, { + "resources_links": [ + { + "href": "https://example.com/next-url", + "rel": "next", + } + ], + }, + { "resources": [{"id": ids[1]}], - }] + }, + ] self.session.get.return_value = mock_response @@ -2241,15 +2369,23 @@ def test_list_response_paginated_with_links(self): self.assertEqual(ids[0], results[0].id) self.assertEqual(ids[1], results[1].id) self.assertEqual( - mock.call('base_path', - headers={'Accept': 'application/json'}, params={}, - microversion=None), - self.session.get.mock_calls[0]) + mock.call( + 'base_path', + headers={'Accept': 'application/json'}, + params={}, + microversion=None, + ), + self.session.get.mock_calls[0], + ) self.assertEqual( - mock.call('https://example.com/next-url', - headers={'Accept': 'application/json'}, params={}, - microversion=None), - self.session.get.mock_calls[2]) + mock.call( + 'https://example.com/next-url', + headers={'Accept': 'application/json'}, + params={}, + microversion=None, + ), + self.session.get.mock_calls[2], + ) self.assertEqual(2, len(self.session.get.call_args_list)) self.assertIsInstance(results[0], self.test_class) @@ -2262,15 +2398,21 @@ def test_list_response_paginated_with_links_and_query(self): mock_response.json.side_effect = [ { "resources": [{"id": ids[0]}], - "resources_links": [{ - "href": "https://example.com/next-url?limit=%d" % q_limit, - "rel": "next", - }] - }, { + "resources_links": [ + { + "href": "https://example.com/next-url?limit=%d" + % q_limit, + "rel": "next", + } + ], + }, + { "resources": [{"id": ids[1]}], - }, { + }, + { "resources": [], - }] + }, + ] self.session.get.return_value = mock_response @@ -2283,19 +2425,27 @@ class Test(self.test_class): self.assertEqual(ids[0], results[0].id) self.assertEqual(ids[1], results[1].id) self.assertEqual( - mock.call('base_path', - headers={'Accept': 'application/json'}, params={ - 'limit': q_limit, - }, - microversion=None), - self.session.get.mock_calls[0]) + mock.call( + 'base_path', + headers={'Accept': 'application/json'}, + params={ + 'limit': q_limit, + }, + microversion=None, + ), + self.session.get.mock_calls[0], + ) self.assertEqual( - mock.call('https://example.com/next-url', - headers={'Accept': 'application/json'}, params={ - 'limit': [str(q_limit)], - }, - microversion=None), - self.session.get.mock_calls[2]) + mock.call( + 'https://example.com/next-url', + headers={'Accept': 'application/json'}, + params={ + 'limit': [str(q_limit)], + }, + microversion=None, + ), + self.session.get.mock_calls[2], + ) self.assertEqual(3, len(self.session.get.call_args_list)) self.assertIsInstance(results[0], self.test_class) @@ -2307,6 +2457,7 @@ def test_list_response_paginated_with_next_field(self): returns a 'first' field and, if there are more pages, a 'next' field in the response body. Ensure we correctly parse these. """ + class Test(resource.Resource): service = self.service_name base_path = '/foos/bars' @@ -2344,7 +2495,7 @@ class Test(resource.Resource): params={'wow': 'cool'}, microversion=None, ), - self.session.get.mock_calls[0] + self.session.get.mock_calls[0], ) self.assertEqual( mock.call( @@ -2373,10 +2524,12 @@ class Test(resource.Resource): mock_response.links = {} mock_response.json.return_value = { "resources": [{"id": ids[0]}], - "resources_links": [{ - "href": "https://example.com/next-url", - "rel": "next", - }] + "resources_links": [ + { + "href": "https://example.com/next-url", + "rel": "next", + } + ], } mock_response2 = mock.Mock() mock_response2.status_code = 200 @@ -2393,15 +2546,23 @@ class Test(resource.Resource): self.assertEqual(ids[0], results[0].id) self.assertEqual(ids[1], results[1].id) self.assertEqual( - mock.call('base_path', - headers={'Accept': 'application/json'}, params={}, - microversion='1.42'), - self.session.get.mock_calls[0]) + mock.call( + 'base_path', + headers={'Accept': 'application/json'}, + params={}, + microversion='1.42', + ), + self.session.get.mock_calls[0], + ) self.assertEqual( - mock.call('https://example.com/next-url', - headers={'Accept': 'application/json'}, params={}, - microversion='1.42'), - self.session.get.mock_calls[1]) + mock.call( + 'https://example.com/next-url', + headers={'Accept': 'application/json'}, + params={}, + microversion='1.42', + ), + self.session.get.mock_calls[1], + ) self.assertEqual(2, len(self.session.get.call_args_list)) self.assertIsInstance(results[0], Test) self.assertEqual('1.42', results[0].microversion) @@ -2431,9 +2592,11 @@ def test_list_paginated_infinite_loop(self): mock_response.json.side_effect = [ { "resources": [{"id": 1}], - }, { + }, + { "resources": [{"id": 1}], - }] + }, + ] self.session.get.return_value = mock_response @@ -2442,11 +2605,7 @@ class Test(self.test_class): res = Test.list(self.session, paginated=True, limit=q_limit) - self.assertRaises( - exceptions.SDKException, - list, - res - ) + self.assertRaises(exceptions.SDKException, list, res) def test_list_query_params(self): id = 1 @@ -2471,8 +2630,14 @@ class Test(self.test_class): base_path = "/%(something)s/blah" something = resource.URI("something") - results = list(Test.list(self.session, paginated=True, - query_param=qp, something=uri_param)) + results = list( + Test.list( + self.session, + paginated=True, + query_param=qp, + something=uri_param, + ) + ) self.assertEqual(1, len(results)) # Verify URI attribute is set on the resource @@ -2481,11 +2646,13 @@ class Test(self.test_class): # Look at the `params` argument to each of the get calls that # were made. self.assertEqual( - self.session.get.call_args_list[0][1]["params"], - {qp_name: qp}) + self.session.get.call_args_list[0][1]["params"], {qp_name: qp} + ) - self.assertEqual(self.session.get.call_args_list[0][0][0], - Test.base_path % {"something": uri_param}) + self.assertEqual( + self.session.get.call_args_list[0][0][0], + Test.base_path % {"something": uri_param}, + ) def test_allow_invalid_list_params(self): qp = "query param!" @@ -2504,14 +2671,21 @@ class Test(self.test_class): base_path = "/%(something)s/blah" something = resource.URI("something") - list(Test.list(self.session, paginated=True, query_param=qp, - allow_unknown_params=True, something=uri_param, - something_wrong=True)) + list( + Test.list( + self.session, + paginated=True, + query_param=qp, + allow_unknown_params=True, + something=uri_param, + something_wrong=True, + ) + ) self.session.get.assert_called_once_with( "/{something}/blah".format(something=uri_param), headers={'Accept': 'application/json'}, microversion=None, - params={qp_name: qp} + params={qp_name: qp}, ) def test_list_client_filters(self): @@ -2521,10 +2695,12 @@ def test_list_client_filters(self): mock_empty = mock.Mock() mock_empty.status_code = 200 mock_empty.links = {} - mock_empty.json.return_value = {"resources": [ - {"a": "1", "b": "1"}, - {"a": "1", "b": "2"}, - ]} + mock_empty.json.return_value = { + "resources": [ + {"a": "1", "b": "1"}, + {"a": "1", "b": "2"}, + ] + } self.session.get.side_effect = [mock_empty] @@ -2535,15 +2711,22 @@ class Test(self.test_class): a = resource.Body("a") b = resource.Body("b") - res = list(Test.list( - self.session, paginated=True, query_param=qp, - allow_unknown_params=True, something=uri_param, - a='1', b='2')) + res = list( + Test.list( + self.session, + paginated=True, + query_param=qp, + allow_unknown_params=True, + something=uri_param, + a='1', + b='2', + ) + ) self.session.get.assert_called_once_with( "/{something}/blah".format(something=uri_param), headers={'Accept': 'application/json'}, microversion=None, - params={'a': '1'} + params={'a': '1'}, ) self.assertEqual(1, len(res)) self.assertEqual("2", res[0].b) @@ -2571,19 +2754,27 @@ class Test(self.test_class): base_path = "/%(something)s/blah" something = resource.URI("something") - results = list(Test.list(self.session, paginated=True, - something=uri_param, **{qp_name: qp})) + results = list( + Test.list( + self.session, + paginated=True, + something=uri_param, + **{qp_name: qp} + ) + ) self.assertEqual(1, len(results)) # Look at the `params` argument to each of the get calls that # were made. self.assertEqual( - self.session.get.call_args_list[0][1]["params"], - {qp_name: qp}) + self.session.get.call_args_list[0][1]["params"], {qp_name: qp} + ) - self.assertEqual(self.session.get.call_args_list[0][0][0], - Test.base_path % {"something": uri_param}) + self.assertEqual( + self.session.get.call_args_list[0][0][0], + Test.base_path % {"something": uri_param}, + ) def test_values_as_list_params_precedence(self): id = 1 @@ -2609,19 +2800,28 @@ class Test(self.test_class): base_path = "/%(something)s/blah" something = resource.URI("something") - results = list(Test.list(self.session, paginated=True, query_param=qp2, - something=uri_param, **{qp_name: qp})) + results = list( + Test.list( + self.session, + paginated=True, + query_param=qp2, + something=uri_param, + **{qp_name: qp} + ) + ) self.assertEqual(1, len(results)) # Look at the `params` argument to each of the get calls that # were made. self.assertEqual( - self.session.get.call_args_list[0][1]["params"], - {qp_name: qp2}) + self.session.get.call_args_list[0][1]["params"], {qp_name: qp2} + ) - self.assertEqual(self.session.get.call_args_list[0][0][0], - Test.base_path % {"something": uri_param}) + self.assertEqual( + self.session.get.call_args_list[0][0][0], + Test.base_path % {"something": uri_param}, + ) def test_list_multi_page_response_paginated(self): ids = [1, 2] @@ -2630,27 +2830,29 @@ def test_list_multi_page_response_paginated(self): resp1.links = {} resp1.json.return_value = { "resources": [{"id": ids[0]}], - "resources_links": [{ - "href": "https://example.com/next-url", - "rel": "next", - }], + "resources_links": [ + { + "href": "https://example.com/next-url", + "rel": "next", + } + ], } resp2 = mock.Mock() resp2.status_code = 200 resp2.links = {} resp2.json.return_value = { "resources": [{"id": ids[1]}], - "resources_links": [{ - "href": "https://example.com/next-url", - "rel": "next", - }], + "resources_links": [ + { + "href": "https://example.com/next-url", + "rel": "next", + } + ], } resp3 = mock.Mock() resp3.status_code = 200 resp3.links = {} - resp3.json.return_value = { - "resources": [] - } + resp3.json.return_value = {"resources": []} self.session.get.side_effect = [resp1, resp2, resp3] @@ -2662,7 +2864,8 @@ def test_list_multi_page_response_paginated(self): self.base_path, headers={"Accept": "application/json"}, params={}, - microversion=None) + microversion=None, + ) result1 = next(results) self.assertEqual(result1.id, ids[1]) @@ -2670,14 +2873,16 @@ def test_list_multi_page_response_paginated(self): 'https://example.com/next-url', headers={"Accept": "application/json"}, params={}, - microversion=None) + microversion=None, + ) self.assertRaises(StopIteration, next, results) self.session.get.assert_called_with( 'https://example.com/next-url', headers={"Accept": "application/json"}, params={}, - microversion=None) + microversion=None, + ) def test_list_multi_page_no_early_termination(self): # This tests verifies that multipages are not early terminated. @@ -2720,7 +2925,8 @@ def test_list_multi_page_no_early_termination(self): self.base_path, headers={"Accept": "application/json"}, params={"limit": 3}, - microversion=None) + microversion=None, + ) # Second page contains another two items result2 = next(results) @@ -2731,7 +2937,8 @@ def test_list_multi_page_no_early_termination(self): self.base_path, headers={"Accept": "application/json"}, params={"limit": 3, "marker": 2}, - microversion=None) + microversion=None, + ) # Ensure we're done after those four items self.assertRaises(StopIteration, next, results) @@ -2741,7 +2948,8 @@ def test_list_multi_page_no_early_termination(self): self.base_path, headers={"Accept": "application/json"}, params={"limit": 3, "marker": 4}, - microversion=None) + microversion=None, + ) # Ensure we made three calls to get this done self.assertEqual(3, len(self.session.get.call_args_list)) @@ -2779,7 +2987,8 @@ def test_list_multi_page_inferred_additional(self): self.base_path, headers={"Accept": "application/json"}, params={"limit": 2}, - microversion=None) + microversion=None, + ) result2 = next(results) self.assertEqual(result2.id, ids[2]) @@ -2787,7 +2996,8 @@ def test_list_multi_page_inferred_additional(self): self.base_path, headers={"Accept": "application/json"}, params={'limit': 2, 'marker': 2}, - microversion=None) + microversion=None, + ) # Ensure we're done after those three items # In python3.7, PEP 479 is enabled for all code, and StopIteration @@ -2802,6 +3012,7 @@ def test_list_multi_page_header_count(self): class Test(self.test_class): resources_key = None pagination_key = 'X-Container-Object-Count' + self.sot = Test() # Swift returns a total number of objects in a header and we compare @@ -2832,7 +3043,8 @@ class Test(self.test_class): self.base_path, headers={"Accept": "application/json"}, params={}, - microversion=None) + microversion=None, + ) result2 = next(results) self.assertEqual(result2.id, ids[2]) @@ -2840,7 +3052,8 @@ class Test(self.test_class): self.base_path, headers={"Accept": "application/json"}, params={'marker': 2}, - microversion=None) + microversion=None, + ) # Ensure we're done after those three items self.assertRaises(StopIteration, next, results) @@ -2856,7 +3069,8 @@ def test_list_multi_page_link_header(self): resp1 = mock.Mock() resp1.status_code = 200 resp1.links = { - 'next': {'uri': 'https://example.com/next-url', 'rel': 'next'}} + 'next': {'uri': 'https://example.com/next-url', 'rel': 'next'} + } resp1.headers = {} resp1.json.return_value = { "resources": [{"id": ids[0]}, {"id": ids[1]}], @@ -2880,7 +3094,8 @@ def test_list_multi_page_link_header(self): self.base_path, headers={"Accept": "application/json"}, params={}, - microversion=None) + microversion=None, + ) result2 = next(results) self.assertEqual(result2.id, ids[2]) @@ -2888,7 +3103,8 @@ def test_list_multi_page_link_header(self): 'https://example.com/next-url', headers={"Accept": "application/json"}, params={}, - microversion=None) + microversion=None, + ) # Ensure we're done after those three items self.assertRaises(StopIteration, next, results) @@ -2911,8 +3127,9 @@ class Test(resource.Resource): self.assertRaises(ValueError, Test.bulk_create, self.session, "hi!") self.assertRaises(ValueError, Test.bulk_create, self.session, ["hi!"]) - def _test_bulk_create(self, cls, http_method, microversion=None, - base_path=None, **params): + def _test_bulk_create( + self, cls, http_method, microversion=None, base_path=None, **params + ): req1 = mock.Mock() req2 = mock.Mock() req1.body = {'name': 'resource1'} @@ -2922,8 +3139,12 @@ def _test_bulk_create(self, cls, http_method, microversion=None, req1.headers = 'headers' req2.headers = 'headers' - request_body = {"tests": [{'name': 'resource1', 'id': 'id1'}, - {'name': 'resource2', 'id': 'id2'}]} + request_body = { + "tests": [ + {'name': 'resource1', 'id': 'id1'}, + {'name': 'resource2', 'id': 'id2'}, + ] + } cls._prepare_request = mock.Mock(side_effect=[req1, req2]) mock_response = mock.Mock() @@ -2932,19 +3153,25 @@ def _test_bulk_create(self, cls, http_method, microversion=None, mock_response.json.return_value = request_body http_method.return_value = mock_response - res = list(cls.bulk_create(self.session, [{'name': 'resource1'}, - {'name': 'resource2'}], - base_path=base_path, **params)) + res = list( + cls.bulk_create( + self.session, + [{'name': 'resource1'}, {'name': 'resource2'}], + base_path=base_path, + **params + ) + ) self.assertEqual(len(res), 2) self.assertEqual(res[0].id, 'id1') self.assertEqual(res[1].id, 'id2') - http_method.assert_called_once_with(self.request.url, - json={'tests': [req1.body, - req2.body]}, - headers=self.request.headers, - microversion=microversion, - params=params) + http_method.assert_called_once_with( + self.request.url, + json={'tests': [req1.body, req2.body]}, + headers=self.request.headers, + microversion=microversion, + params=params, + ) def test_bulk_create_post(self): class Test(resource.Resource): @@ -3005,8 +3232,12 @@ class Test(resource.Resource): allow_create = False resources_key = 'tests' - self.assertRaises(exceptions.MethodNotSupported, Test.bulk_create, - self.session, [{'name': 'name'}]) + self.assertRaises( + exceptions.MethodNotSupported, + Test.bulk_create, + self.session, + [{'name': 'name'}], + ) def test_bulk_create_fail_on_request(self): class Test(resource.Resource): @@ -3017,14 +3248,20 @@ class Test(resource.Resource): resources_key = 'tests' response = FakeResponse({}, status_code=409) - response.content = ('{"TestError": {"message": "Failed to parse ' - 'request. Required attribute \'foo\' not ' - 'specified", "type": "HTTPBadRequest", ' - '"detail": ""}}') + response.content = ( + '{"TestError": {"message": "Failed to parse ' + 'request. Required attribute \'foo\' not ' + 'specified", "type": "HTTPBadRequest", ' + '"detail": ""}}' + ) response.reason = 'Bad Request' self.session.post.return_value = response - self.assertRaises(exceptions.ConflictException, Test.bulk_create, - self.session, [{'name': 'name'}]) + self.assertRaises( + exceptions.ConflictException, + Test.bulk_create, + self.session, + [{'name': 'name'}], + ) class TestResourceFind(base.TestCase): @@ -3032,26 +3269,22 @@ class TestResourceFind(base.TestCase): result = 1 class Base(resource.Resource): - @classmethod def existing(cls, **kwargs): response = mock.Mock() response.status_code = 404 - raise exceptions.ResourceNotFound( - 'Not Found', response=response) + raise exceptions.ResourceNotFound('Not Found', response=response) @classmethod def list(cls, session, **params): return [] class OneResult(Base): - @classmethod def _get_one_match(cls, *args): return TestResourceFind.result class NoResults(Base): - @classmethod def _get_one_match(cls, *args): return None @@ -3070,7 +3303,6 @@ def test_find_short_circuit(self): value = 1 class Test(resource.Resource): - @classmethod def existing(cls, **kwargs): mock_match = mock.Mock() @@ -3082,32 +3314,40 @@ def existing(cls, **kwargs): self.assertEqual(result, value) def test_no_match_raise(self): - self.assertRaises(exceptions.ResourceNotFound, self.no_results.find, - self.cloud.compute, "name", ignore_missing=False) + self.assertRaises( + exceptions.ResourceNotFound, + self.no_results.find, + self.cloud.compute, + "name", + ignore_missing=False, + ) def test_no_match_return(self): self.assertIsNone( self.no_results.find( - self.cloud.compute, "name", ignore_missing=True)) + self.cloud.compute, "name", ignore_missing=True + ) + ) def test_find_result_name_not_in_query_parameters(self): - with mock.patch.object(self.one_result, 'existing', - side_effect=self.OneResult.existing) \ - as mock_existing, \ - mock.patch.object(self.one_result, 'list', - side_effect=self.OneResult.list) \ - as mock_list: + with mock.patch.object( + self.one_result, 'existing', side_effect=self.OneResult.existing + ) as mock_existing, mock.patch.object( + self.one_result, 'list', side_effect=self.OneResult.list + ) as mock_list: self.assertEqual( - self.result, - self.one_result.find(self.cloud.compute, "name")) - mock_existing.assert_called_once_with(id='name', - connection=mock.ANY) + self.result, self.one_result.find(self.cloud.compute, "name") + ) + mock_existing.assert_called_once_with( + id='name', connection=mock.ANY + ) mock_list.assert_called_once_with(mock.ANY) def test_find_result_name_in_query_parameters(self): self.assertEqual( self.result, - self.one_result_with_qparams.find(self.cloud.compute, "name")) + self.one_result_with_qparams.find(self.cloud.compute, "name"), + ) def test_match_empty_results(self): self.assertIsNone(resource.Resource._get_one_match("name", [])) @@ -3161,7 +3401,10 @@ def test_multiple_matches(self): self.assertRaises( exceptions.DuplicateResource, - resource.Resource._get_one_match, the_id, [match, match]) + resource.Resource._get_one_match, + the_id, + [match, match], + ) def test_list_no_base_path(self): @@ -3174,21 +3417,23 @@ def test_list_base_path(self): with mock.patch.object(self.Base, "list") as list_mock: self.Base.find( - self.cloud.compute, "name", list_base_path='/dummy/list') + self.cloud.compute, "name", list_base_path='/dummy/list' + ) list_mock.assert_called_with( - self.cloud.compute, base_path='/dummy/list') + self.cloud.compute, base_path='/dummy/list' + ) class TestWaitForStatus(base.TestCase): - def test_immediate_status(self): status = "loling" res = mock.Mock(spec=['id', 'status']) res.status = status result = resource.wait_for_status( - self.cloud.compute, res, status, "failures", "interval", "wait") + self.cloud.compute, res, status, "failures", "interval", "wait" + ) self.assertEqual(res, result) @@ -3198,7 +3443,8 @@ def test_immediate_status_case(self): res.status = status result = resource.wait_for_status( - self.cloud.compute, res, 'lOling', "failures", "interval", "wait") + self.cloud.compute, res, 'lOling', "failures", "interval", "wait" + ) self.assertEqual(res, result) @@ -3208,8 +3454,14 @@ def test_immediate_status_different_attribute(self): res.mood = status result = resource.wait_for_status( - self.cloud.compute, res, status, "failures", "interval", "wait", - attribute='mood') + self.cloud.compute, + res, + status, + "failures", + "interval", + "wait", + attribute='mood', + ) self.assertEqual(res, result) @@ -3231,10 +3483,12 @@ def test_status_match(self): # other gets past the first check, two anothers gets through # the sleep loop, and the third matches resources = self._resources_from_statuses( - "first", "other", "another", "another", status) + "first", "other", "another", "another", status + ) result = resource.wait_for_status( - mock.Mock(), resources[0], status, None, 1, 5) + mock.Mock(), resources[0], status, None, 1, 5 + ) self.assertEqual(result, resources[-1]) @@ -3243,10 +3497,12 @@ def test_status_match_with_none(self): # apparently, None is a correct state in some cases resources = self._resources_from_statuses( - None, "other", None, "another", status) + None, "other", None, "another", status + ) result = resource.wait_for_status( - mock.Mock(), resources[0], status, None, 1, 5) + mock.Mock(), resources[0], status, None, 1, 5 + ) self.assertEqual(result, resources[-1]) @@ -3255,10 +3511,12 @@ def test_status_match_none(self): # apparently, None can be expected status in some cases resources = self._resources_from_statuses( - "first", "other", "another", "another", status) + "first", "other", "another", "another", status + ) result = resource.wait_for_status( - mock.Mock(), resources[0], status, None, 1, 5) + mock.Mock(), resources[0], status, None, 1, 5 + ) self.assertEqual(result, resources[-1]) @@ -3266,12 +3524,12 @@ def test_status_match_different_attribute(self): status = "loling" resources = self._resources_from_statuses( - "first", "other", "another", "another", status, - attribute='mood') + "first", "other", "another", "another", status, attribute='mood' + ) result = resource.wait_for_status( - mock.Mock(), resources[0], status, None, 1, 5, - attribute='mood') + mock.Mock(), resources[0], status, None, 1, 5, attribute='mood' + ) self.assertEqual(result, resources[-1]) @@ -3283,19 +3541,32 @@ def test_status_fails(self): self.assertRaises( exceptions.ResourceFailure, resource.wait_for_status, - mock.Mock(), resources[0], "loling", [failure], 1, 5) + mock.Mock(), + resources[0], + "loling", + [failure], + 1, + 5, + ) def test_status_fails_different_attribute(self): failure = "crying" - resources = self._resources_from_statuses("success", "other", failure, - attribute='mood') + resources = self._resources_from_statuses( + "success", "other", failure, attribute='mood' + ) self.assertRaises( exceptions.ResourceFailure, resource.wait_for_status, - mock.Mock(), resources[0], "loling", [failure.upper()], 1, 5, - attribute='mood') + mock.Mock(), + resources[0], + "loling", + [failure.upper()], + 1, + 5, + attribute='mood', + ) def test_timeout(self): status = "loling" @@ -3308,30 +3579,45 @@ def test_timeout(self): statuses = ["other"] * 7 type(res).status = mock.PropertyMock(side_effect=statuses) - self.assertRaises(exceptions.ResourceTimeout, - resource.wait_for_status, - self.cloud.compute, res, status, None, 0.01, 0.1) + self.assertRaises( + exceptions.ResourceTimeout, + resource.wait_for_status, + self.cloud.compute, + res, + status, + None, + 0.01, + 0.1, + ) def test_no_sleep(self): res = mock.Mock() statuses = ["other"] type(res).status = mock.PropertyMock(side_effect=statuses) - self.assertRaises(exceptions.ResourceTimeout, - resource.wait_for_status, - self.cloud.compute, res, "status", None, 0, -1) + self.assertRaises( + exceptions.ResourceTimeout, + resource.wait_for_status, + self.cloud.compute, + res, + "status", + None, + 0, + -1, + ) class TestWaitForDelete(base.TestCase): - def test_success(self): response = mock.Mock() response.headers = {} response.status_code = 404 res = mock.Mock() res.fetch.side_effect = [ - None, None, - exceptions.ResourceNotFound('Not Found', response)] + None, + None, + exceptions.ResourceNotFound('Not Found', response), + ] result = resource.wait_for_delete(self.cloud.compute, res, 1, 3) @@ -3345,7 +3631,11 @@ def test_timeout(self): self.assertRaises( exceptions.ResourceTimeout, resource.wait_for_delete, - self.cloud.compute, res, 0.1, 0.3) + self.cloud.compute, + res, + 0.1, + 0.3, + ) @mock.patch.object(resource.Resource, '_get_microversion', autospec=True) @@ -3358,33 +3648,46 @@ def test_compatible(self, mock_get_ver): self.assertEqual( '1.42', - self.res._assert_microversion_for(self.session, 'fetch', '1.6')) + self.res._assert_microversion_for(self.session, 'fetch', '1.6'), + ) mock_get_ver.assert_called_once_with(self.session, action='fetch') def test_incompatible(self, mock_get_ver): mock_get_ver.return_value = '1.1' - self.assertRaisesRegex(exceptions.NotSupported, - '1.6 is required, but 1.1 will be used', - self.res._assert_microversion_for, - self.session, 'fetch', '1.6') + self.assertRaisesRegex( + exceptions.NotSupported, + '1.6 is required, but 1.1 will be used', + self.res._assert_microversion_for, + self.session, + 'fetch', + '1.6', + ) mock_get_ver.assert_called_once_with(self.session, action='fetch') def test_custom_message(self, mock_get_ver): mock_get_ver.return_value = '1.1' - self.assertRaisesRegex(exceptions.NotSupported, - 'boom.*1.6 is required, but 1.1 will be used', - self.res._assert_microversion_for, - self.session, 'fetch', '1.6', - error_message='boom') + self.assertRaisesRegex( + exceptions.NotSupported, + 'boom.*1.6 is required, but 1.1 will be used', + self.res._assert_microversion_for, + self.session, + 'fetch', + '1.6', + error_message='boom', + ) mock_get_ver.assert_called_once_with(self.session, action='fetch') def test_none(self, mock_get_ver): mock_get_ver.return_value = None - self.assertRaisesRegex(exceptions.NotSupported, - '1.6 is required, but the default version', - self.res._assert_microversion_for, - self.session, 'fetch', '1.6') + self.assertRaisesRegex( + exceptions.NotSupported, + '1.6 is required, but the default version', + self.res._assert_microversion_for, + self.session, + 'fetch', + '1.6', + ) mock_get_ver.assert_called_once_with(self.session, action='fetch') diff --git a/openstack/tests/unit/test_stats.py b/openstack/tests/unit/test_stats.py index 288621b9a..25d8c11b0 100644 --- a/openstack/tests/unit/test_stats.py +++ b/openstack/tests/unit/test_stats.py @@ -66,16 +66,17 @@ def _cleanup(self): class TestStats(base.TestCase): - def setUp(self): self.statsd = StatsdFixture() self.useFixture(self.statsd) # note, use 127.0.0.1 rather than localhost to avoid getting ipv6 # see: https://github.com/jsocol/pystatsd/issues/61 self.useFixture( - fixtures.EnvironmentVariable('STATSD_HOST', '127.0.0.1')) + fixtures.EnvironmentVariable('STATSD_HOST', '127.0.0.1') + ) self.useFixture( - fixtures.EnvironmentVariable('STATSD_PORT', str(self.statsd.port))) + fixtures.EnvironmentVariable('STATSD_PORT', str(self.statsd.port)) + ) self.add_info_on_exception('statsd_content', self.statsd.stats) # Set up the above things before the super setup so that we have the @@ -93,7 +94,8 @@ def _add_prometheus_samples(self, exc_info): samples.append(s) self.addDetail( 'prometheus_samples', - testtools.content.text_content(pprint.pformat(samples))) + testtools.content.text_content(pprint.pformat(samples)), + ) def assert_reported_stat(self, key, value=None, kind=None): """Check statsd output @@ -127,7 +129,8 @@ def assert_reported_stat(self, key, value=None, kind=None): # newlines; thus we first flatten the stats out into # single entries. stats = itertools.chain.from_iterable( - [s.decode('utf-8').split('\n') for s in self.statsd.stats]) + [s.decode('utf-8').split('\n') for s in self.statsd.stats] + ) for stat in stats: k, v = stat.split(':') if key == k: @@ -166,127 +169,184 @@ def assert_prometheus_stat(self, name, value, labels=None): def test_list_projects(self): mock_uri = self.get_mock_url( - service_type='identity', resource='projects', - base_url_append='v3') - - self.register_uris([ - dict(method='GET', uri=mock_uri, status_code=200, - json={'projects': []})]) + service_type='identity', resource='projects', base_url_append='v3' + ) + + self.register_uris( + [ + dict( + method='GET', + uri=mock_uri, + status_code=200, + json={'projects': []}, + ) + ] + ) self.cloud.list_projects() self.assert_calls() self.assert_reported_stat( - 'openstack.api.identity.GET.projects.200', value='1', kind='c') + 'openstack.api.identity.GET.projects.200', value='1', kind='c' + ) self.assert_prometheus_stat( - 'openstack_http_requests_total', 1, dict( + 'openstack_http_requests_total', + 1, + dict( service_type='identity', endpoint=mock_uri, method='GET', - status_code='200')) + status_code='200', + ), + ) def test_projects(self): mock_uri = self.get_mock_url( - service_type='identity', resource='projects', - base_url_append='v3') - - self.register_uris([ - dict(method='GET', uri=mock_uri, status_code=200, - json={'projects': []})]) + service_type='identity', resource='projects', base_url_append='v3' + ) + + self.register_uris( + [ + dict( + method='GET', + uri=mock_uri, + status_code=200, + json={'projects': []}, + ) + ] + ) list(self.cloud.identity.projects()) self.assert_calls() self.assert_reported_stat( - 'openstack.api.identity.GET.projects.200', value='1', kind='c') + 'openstack.api.identity.GET.projects.200', value='1', kind='c' + ) self.assert_prometheus_stat( - 'openstack_http_requests_total', 1, dict( + 'openstack_http_requests_total', + 1, + dict( service_type='identity', endpoint=mock_uri, method='GET', - status_code='200')) + status_code='200', + ), + ) def test_servers(self): mock_uri = 'https://compute.example.com/v2.1/servers/detail' - self.register_uris([ - self.get_nova_discovery_mock_dict(), - dict(method='GET', uri=mock_uri, status_code=200, - json={'servers': []})]) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=mock_uri, + status_code=200, + json={'servers': []}, + ), + ] + ) list(self.cloud.compute.servers()) self.assert_calls() self.assert_reported_stat( - 'openstack.api.compute.GET.servers_detail.200', - value='1', kind='c') + 'openstack.api.compute.GET.servers_detail.200', value='1', kind='c' + ) self.assert_reported_stat( 'openstack.api.compute.GET.servers_detail.200', - value='0', kind='ms') + value='0', + kind='ms', + ) self.assert_prometheus_stat( - 'openstack_http_requests_total', 1, dict( + 'openstack_http_requests_total', + 1, + dict( service_type='compute', endpoint=mock_uri, method='GET', - status_code='200')) + status_code='200', + ), + ) def test_servers_no_detail(self): mock_uri = 'https://compute.example.com/v2.1/servers' - self.register_uris([ - dict(method='GET', uri=mock_uri, status_code=200, - json={'servers': []})]) + self.register_uris( + [ + dict( + method='GET', + uri=mock_uri, + status_code=200, + json={'servers': []}, + ) + ] + ) self.cloud.compute.get('/servers') self.assert_calls() self.assert_reported_stat( - 'openstack.api.compute.GET.servers.200', value='1', kind='c') + 'openstack.api.compute.GET.servers.200', value='1', kind='c' + ) self.assert_reported_stat( - 'openstack.api.compute.GET.servers.200', value='0', kind='ms') + 'openstack.api.compute.GET.servers.200', value='0', kind='ms' + ) self.assert_reported_stat( - 'openstack.api.compute.GET.servers.attempted', value='1', kind='c') + 'openstack.api.compute.GET.servers.attempted', value='1', kind='c' + ) self.assert_prometheus_stat( - 'openstack_http_requests_total', 1, dict( + 'openstack_http_requests_total', + 1, + dict( service_type='compute', endpoint=mock_uri, method='GET', - status_code='200')) + status_code='200', + ), + ) def test_servers_error(self): mock_uri = 'https://compute.example.com/v2.1/servers' - self.register_uris([ - dict(method='GET', uri=mock_uri, status_code=500, - json={})]) + self.register_uris( + [dict(method='GET', uri=mock_uri, status_code=500, json={})] + ) self.cloud.compute.get('/servers') self.assert_calls() self.assert_reported_stat( - 'openstack.api.compute.GET.servers.500', value='1', kind='c') + 'openstack.api.compute.GET.servers.500', value='1', kind='c' + ) self.assert_reported_stat( - 'openstack.api.compute.GET.servers.500', value='0', kind='ms') + 'openstack.api.compute.GET.servers.500', value='0', kind='ms' + ) self.assert_reported_stat( - 'openstack.api.compute.GET.servers.attempted', value='1', kind='c') + 'openstack.api.compute.GET.servers.attempted', value='1', kind='c' + ) self.assert_prometheus_stat( - 'openstack_http_requests_total', 1, dict( + 'openstack_http_requests_total', + 1, + dict( service_type='compute', endpoint=mock_uri, method='GET', - status_code='500')) + status_code='500', + ), + ) def test_timeout(self): mock_uri = 'https://compute.example.com/v2.1/servers' - self.register_uris([ - dict(method='GET', uri=mock_uri, - exc=rexceptions.ConnectTimeout) - ]) + self.register_uris( + [dict(method='GET', uri=mock_uri, exc=rexceptions.ConnectTimeout)] + ) try: self.cloud.compute.get('/servers') @@ -294,13 +354,14 @@ def test_timeout(self): pass self.assert_reported_stat( - 'openstack.api.compute.GET.servers.failed', value='1', kind='c') + 'openstack.api.compute.GET.servers.failed', value='1', kind='c' + ) self.assert_reported_stat( - 'openstack.api.compute.GET.servers.attempted', value='1', kind='c') + 'openstack.api.compute.GET.servers.attempted', value='1', kind='c' + ) class TestNoStats(base.TestCase): - def setUp(self): super(TestNoStats, self).setUp() self.statsd = StatsdFixture() @@ -309,12 +370,19 @@ def setUp(self): def test_no_stats(self): mock_uri = self.get_mock_url( - service_type='identity', resource='projects', - base_url_append='v3') - - self.register_uris([ - dict(method='GET', uri=mock_uri, status_code=200, - json={'projects': []})]) + service_type='identity', resource='projects', base_url_append='v3' + ) + + self.register_uris( + [ + dict( + method='GET', + uri=mock_uri, + status_code=200, + json={'projects': []}, + ) + ] + ) self.cloud.identity._statsd_client = None list(self.cloud.identity.projects()) diff --git a/openstack/tests/unit/test_utils.py b/openstack/tests/unit/test_utils.py index 142478b4f..12593e79a 100644 --- a/openstack/tests/unit/test_utils.py +++ b/openstack/tests/unit/test_utils.py @@ -29,7 +29,6 @@ class Test_enable_logging(base.TestCase): - def setUp(self): super(Test_enable_logging, self).setUp() self.openstack_logger = mock.Mock() @@ -54,10 +53,11 @@ def setUp(self): self.stevedore_logger, self.ksa_logger_1, self.ksa_logger_2, - self.ksa_logger_3 + self.ksa_logger_3, ] self.useFixture( - fixtures.MonkeyPatch('logging.getLogger', self.fake_get_logger)) + fixtures.MonkeyPatch('logging.getLogger', self.fake_get_logger) + ) def _console_tests(self, level, debug, stream): @@ -69,7 +69,8 @@ def _console_tests(self, level, debug, stream): def _file_tests(self, level, debug): file_handler = mock.Mock() self.useFixture( - fixtures.MonkeyPatch('logging.FileHandler', file_handler)) + fixtures.MonkeyPatch('logging.FileHandler', file_handler) + ) fake_path = "fake/path.log" openstack.enable_logging(debug=debug, path=fake_path) @@ -85,7 +86,8 @@ def test_none(self): self.assertEqual(self.openstack_logger.addHandler.call_count, 1) self.assertIsInstance( self.openstack_logger.addHandler.call_args_list[0][0][0], - logging.StreamHandler) + logging.StreamHandler, + ) def test_debug_console_stderr(self): self._console_tests(logging.DEBUG, True, sys.stderr) @@ -107,7 +109,6 @@ def test_warning_file(self): class Test_urljoin(base.TestCase): - def test_strings(self): root = "http://www.example.com" leaves = "foo", "bar" @@ -138,21 +139,20 @@ class TestSupportsMicroversion(base.TestCase): def setUp(self): super(TestSupportsMicroversion, self).setUp() self.adapter = mock.Mock(spec=['get_endpoint_data']) - self.endpoint_data = mock.Mock(spec=['min_microversion', - 'max_microversion'], - min_microversion='1.1', - max_microversion='1.99') + self.endpoint_data = mock.Mock( + spec=['min_microversion', 'max_microversion'], + min_microversion='1.1', + max_microversion='1.99', + ) self.adapter.get_endpoint_data.return_value = self.endpoint_data def test_requested_supported_no_default(self): self.adapter.default_microversion = None - self.assertTrue( - utils.supports_microversion(self.adapter, '1.2')) + self.assertTrue(utils.supports_microversion(self.adapter, '1.2')) def test_requested_not_supported_no_default(self): self.adapter.default_microversion = None - self.assertFalse( - utils.supports_microversion(self.adapter, '2.2')) + self.assertFalse(utils.supports_microversion(self.adapter, '2.2')) def test_requested_not_supported_no_default_exception(self): self.adapter.default_microversion = None @@ -161,22 +161,20 @@ def test_requested_not_supported_no_default_exception(self): utils.supports_microversion, self.adapter, '2.2', - True) + True, + ) def test_requested_supported_higher_default(self): self.adapter.default_microversion = '1.8' - self.assertTrue( - utils.supports_microversion(self.adapter, '1.6')) + self.assertTrue(utils.supports_microversion(self.adapter, '1.6')) def test_requested_supported_equal_default(self): self.adapter.default_microversion = '1.8' - self.assertTrue( - utils.supports_microversion(self.adapter, '1.8')) + self.assertTrue(utils.supports_microversion(self.adapter, '1.8')) def test_requested_supported_lower_default(self): self.adapter.default_microversion = '1.2' - self.assertFalse( - utils.supports_microversion(self.adapter, '1.8')) + self.assertFalse(utils.supports_microversion(self.adapter, '1.8')) def test_requested_supported_lower_default_exception(self): self.adapter.default_microversion = '1.2' @@ -185,54 +183,58 @@ def test_requested_supported_lower_default_exception(self): utils.supports_microversion, self.adapter, '1.8', - True) + True, + ) @mock.patch('openstack.utils.supports_microversion') def test_require_microversion(self, sm_mock): utils.require_microversion(self.adapter, '1.2') - sm_mock.assert_called_with(self.adapter, - '1.2', - raise_exception=True) + sm_mock.assert_called_with(self.adapter, '1.2', raise_exception=True) class TestMaximumSupportedMicroversion(base.TestCase): def setUp(self): super(TestMaximumSupportedMicroversion, self).setUp() self.adapter = mock.Mock(spec=['get_endpoint_data']) - self.endpoint_data = mock.Mock(spec=['min_microversion', - 'max_microversion'], - min_microversion=None, - max_microversion='1.99') + self.endpoint_data = mock.Mock( + spec=['min_microversion', 'max_microversion'], + min_microversion=None, + max_microversion='1.99', + ) self.adapter.get_endpoint_data.return_value = self.endpoint_data def test_with_none(self): - self.assertIsNone(utils.maximum_supported_microversion(self.adapter, - None)) + self.assertIsNone( + utils.maximum_supported_microversion(self.adapter, None) + ) def test_with_value(self): - self.assertEqual('1.42', - utils.maximum_supported_microversion(self.adapter, - '1.42')) + self.assertEqual( + '1.42', utils.maximum_supported_microversion(self.adapter, '1.42') + ) def test_value_more_than_max(self): - self.assertEqual('1.99', - utils.maximum_supported_microversion(self.adapter, - '1.100')) + self.assertEqual( + '1.99', utils.maximum_supported_microversion(self.adapter, '1.100') + ) def test_value_less_than_min(self): self.endpoint_data.min_microversion = '1.42' - self.assertIsNone(utils.maximum_supported_microversion(self.adapter, - '1.2')) + self.assertIsNone( + utils.maximum_supported_microversion(self.adapter, '1.2') + ) class TestOsServiceTypesVersion(base.TestCase): def test_ost_version(self): ost_version = '2019-05-01T19:53:21.498745' self.assertEqual( - ost_version, os_service_types.ServiceTypes().version, + ost_version, + os_service_types.ServiceTypes().version, "This project must be pinned to the latest version of " "os-service-types. Please bump requirements.txt and " - "lower-constraints.txt accordingly.") + "lower-constraints.txt accordingly.", + ) class TestTinyDAG(base.TestCase): @@ -243,7 +245,7 @@ class TestTinyDAG(base.TestCase): 'd': ['e'], 'e': [], 'f': ['e'], - 'g': ['e'] + 'g': ['e'], } def _verify_order(self, test_graph, test_list): @@ -306,13 +308,13 @@ def test_walker_fn(graph, node, lst): class Test_md5(base.TestCase): - def setUp(self): super(Test_md5, self).setUp() self.md5_test_data = "Openstack forever".encode('utf-8') try: self.md5_digest = hashlib.md5( # nosec - self.md5_test_data).hexdigest() + self.md5_test_data + ).hexdigest() self.fips_enabled = False except ValueError: self.md5_digest = '0d6dc3c588ae71a04ce9a6beebbbba06' @@ -327,15 +329,17 @@ def test_md5_with_data(self): # [digital envelope routines: EVP_DigestInit_ex] disabled for FIPS self.assertRaises(ValueError, utils.md5, self.md5_test_data) if not self.fips_enabled: - digest = utils.md5(self.md5_test_data, - usedforsecurity=True).hexdigest() + digest = utils.md5( + self.md5_test_data, usedforsecurity=True + ).hexdigest() self.assertEqual(digest, self.md5_digest) else: self.assertRaises( - ValueError, utils.md5, self.md5_test_data, - usedforsecurity=True) - digest = utils.md5(self.md5_test_data, - usedforsecurity=False).hexdigest() + ValueError, utils.md5, self.md5_test_data, usedforsecurity=True + ) + digest = utils.md5( + self.md5_test_data, usedforsecurity=False + ).hexdigest() self.assertEqual(digest, self.md5_digest) def test_md5_without_data(self): @@ -363,25 +367,25 @@ def test_string_data_raises_type_error(self): self.assertRaises(TypeError, hashlib.md5, u'foo') self.assertRaises(TypeError, utils.md5, u'foo') self.assertRaises( - TypeError, utils.md5, u'foo', usedforsecurity=True) + TypeError, utils.md5, u'foo', usedforsecurity=True + ) else: self.assertRaises(ValueError, hashlib.md5, u'foo') self.assertRaises(ValueError, utils.md5, u'foo') self.assertRaises( - ValueError, utils.md5, u'foo', usedforsecurity=True) - self.assertRaises( - TypeError, utils.md5, u'foo', usedforsecurity=False) + ValueError, utils.md5, u'foo', usedforsecurity=True + ) + self.assertRaises(TypeError, utils.md5, u'foo', usedforsecurity=False) def test_none_data_raises_type_error(self): if not self.fips_enabled: self.assertRaises(TypeError, hashlib.md5, None) self.assertRaises(TypeError, utils.md5, None) - self.assertRaises( - TypeError, utils.md5, None, usedforsecurity=True) + self.assertRaises(TypeError, utils.md5, None, usedforsecurity=True) else: self.assertRaises(ValueError, hashlib.md5, None) self.assertRaises(ValueError, utils.md5, None) self.assertRaises( - ValueError, utils.md5, None, usedforsecurity=True) - self.assertRaises( - TypeError, utils.md5, None, usedforsecurity=False) + ValueError, utils.md5, None, usedforsecurity=True + ) + self.assertRaises(TypeError, utils.md5, None, usedforsecurity=False) diff --git a/openstack/utils.py b/openstack/utils.py index c058f77b0..1444cfc71 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -57,7 +57,8 @@ def iterate_timeout(timeout, message, wait=2): except ValueError: raise exceptions.SDKException( "Wait value must be an int or float value. {wait} given" - " instead".format(wait=wait)) + " instead".format(wait=wait) + ) start = time.time() count = 0 @@ -76,6 +77,7 @@ def get_string_format_keys(fmt_string, old_style=True): use the old style string formatting. """ if old_style: + class AccessSaver: def __init__(self): self.keys = [] @@ -115,25 +117,29 @@ def supports_microversion(adapter, microversion, raise_exception=False): """ endpoint_data = adapter.get_endpoint_data() - if (endpoint_data.min_microversion - and endpoint_data.max_microversion - and discover.version_between( - endpoint_data.min_microversion, - endpoint_data.max_microversion, - microversion)): + if ( + endpoint_data.min_microversion + and endpoint_data.max_microversion + and discover.version_between( + endpoint_data.min_microversion, + endpoint_data.max_microversion, + microversion, + ) + ): if adapter.default_microversion is not None: # If default_microversion is set - evaluate # whether it match the expectation candidate = discover.normalize_version_number( - adapter.default_microversion) + adapter.default_microversion + ) required = discover.normalize_version_number(microversion) supports = discover.version_match(required, candidate) if raise_exception and not supports: raise exceptions.SDKException( 'Required microversion {ver} is higher than currently ' 'selected {curr}'.format( - ver=microversion, - curr=adapter.default_microversion) + ver=microversion, curr=adapter.default_microversion + ) ) return supports return True @@ -175,19 +181,24 @@ def pick_microversion(session, required): if session.default_microversion is not None: default = discover.normalize_version_number( - session.default_microversion) + session.default_microversion + ) if required is None: required = default else: - required = (default if discover.version_match(required, default) - else required) + required = ( + default + if discover.version_match(required, default) + else required + ) if required is not None: if not supports_microversion(session, required): raise exceptions.SDKException( 'Requested microversion is not supported by the server side ' - 'or the default microversion is too low') + 'or the default microversion is too low' + ) return discover.version_to_string(required) @@ -212,8 +223,10 @@ def maximum_supported_microversion(adapter, client_maximum): if endpoint_data is None: log = _log.setup_logging('openstack') - log.warning('Cannot determine endpoint data for service %s', - adapter.service_type or adapter.service_name) + log.warning( + 'Cannot determine endpoint data for service %s', + adapter.service_type or adapter.service_name, + ) return None if not endpoint_data.max_microversion: @@ -221,11 +234,13 @@ def maximum_supported_microversion(adapter, client_maximum): client_max = discover.normalize_version_number(client_maximum) server_max = discover.normalize_version_number( - endpoint_data.max_microversion) + endpoint_data.max_microversion + ) if endpoint_data.min_microversion: server_min = discover.normalize_version_number( - endpoint_data.min_microversion) + endpoint_data.min_microversion + ) if client_max < server_min: # NOTE(dtantsur): we may want to raise in this case, but this keeps # the current behavior intact. @@ -265,6 +280,7 @@ def _hashes_up_to_date(md5, sha256, md5_key, sha256_key): # See https://docs.python.org/3.9/library/hashlib.html md5 = hashlib.md5 except TypeError: + def md5(string=b'', usedforsecurity=True): """Return an md5 hashlib object without usedforsecurity parameter For python distributions that do not yet support this keyword @@ -314,8 +330,7 @@ def _reset(self): @property def graph(self): - """Get graph as adjacency dict - """ + """Get graph as adjacency dict""" return self._graph def add_node(self, node): @@ -332,8 +347,7 @@ def from_dict(self, data): self.add_edge(k, dep) def walk(self, timeout=None): - """Start the walking from the beginning. - """ + """Start the walking from the beginning.""" if timeout: self._wait_timeout = timeout return self @@ -345,17 +359,16 @@ def __iter__(self): def __next__(self): # Start waiting if it is expected to get something # (counting down from graph length to 0). - if (self._it_cnt > 0): + if self._it_cnt > 0: self._it_cnt -= 1 try: - res = self._queue.get( - block=True, - timeout=self._wait_timeout) + res = self._queue.get(block=True, timeout=self._wait_timeout) return res except queue.Empty: - raise exceptions.SDKException('Timeout waiting for ' - 'cleanup task to complete') + raise exceptions.SDKException( + 'Timeout waiting for ' 'cleanup task to complete' + ) else: raise StopIteration @@ -410,13 +423,13 @@ def is_complete(self): # it we can have a reduced version. class Munch(dict): """A slightly stripped version of munch.Munch class""" + def __init__(self, *args, **kwargs): self.update(*args, **kwargs) # only called if k not found in normal places def __getattr__(self, k): - """Gets key if it exists, otherwise throws AttributeError. - """ + """Gets key if it exists, otherwise throws AttributeError.""" try: return object.__getattribute__(self, k) except AttributeError: @@ -427,8 +440,8 @@ def __getattr__(self, k): def __setattr__(self, k, v): """Sets attribute k if it exists, otherwise sets key k. A KeyError - raised by set-item (only likely if you subclass Munch) will - propagate as an AttributeError instead. + raised by set-item (only likely if you subclass Munch) will + propagate as an AttributeError instead. """ try: # Throws exception if not in prototype chain @@ -459,8 +472,7 @@ def __delattr__(self, k): object.__delattr__(self, k) def toDict(self): - """Recursively converts a munch back into a dictionary. - """ + """Recursively converts a munch back into a dictionary.""" return unmunchify(self) @property @@ -468,7 +480,7 @@ def __dict__(self): return self.toDict() def __repr__(self): - """Invertible* string-form of a Munch. """ + """Invertible* string-form of a Munch.""" return f'{self.__class__.__name__}({dict.__repr__(self)})' def __dir__(self): diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py index ead946fab..c434441fd 100644 --- a/releasenotes/source/conf.py +++ b/releasenotes/source/conf.py @@ -197,9 +197,13 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - ('index', 'shadeReleaseNotes.tex', - 'Shade Release Notes Documentation', - 'Shade Developers', 'manual'), + ( + 'index', + 'shadeReleaseNotes.tex', + 'Shade Release Notes Documentation', + 'Shade Developers', + 'manual', + ), ] # The name of an image file (relative to this directory) to place at the top of @@ -228,9 +232,13 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'shadereleasenotes', - 'shade Release Notes Documentation', - ['shade Developers'], 1) + ( + 'index', + 'shadereleasenotes', + 'shade Release Notes Documentation', + ['shade Developers'], + 1, + ) ] # If true, show URL addresses after external links. @@ -243,11 +251,15 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'shadeReleaseNotes', - 'shade Release Notes Documentation', - 'shade Developers', 'shadeReleaseNotes', - 'A client library for interacting with OpenStack clouds', - 'Miscellaneous'), + ( + 'index', + 'shadeReleaseNotes', + 'shade Release Notes Documentation', + 'shade Developers', + 'shadeReleaseNotes', + 'A client library for interacting with OpenStack clouds', + 'Miscellaneous', + ), ] # Documents to append as an appendix to all manuals. diff --git a/setup.py b/setup.py index f63cc23c5..83c92e22c 100644 --- a/setup.py +++ b/setup.py @@ -16,6 +16,4 @@ # THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT import setuptools -setuptools.setup( - setup_requires=['pbr>=2.0.0'], - pbr=True) +setuptools.setup(setup_requires=['pbr>=2.0.0'], pbr=True) diff --git a/tools/keystone_version.py b/tools/keystone_version.py index df8fadcd2..8ca504eb1 100644 --- a/tools/keystone_version.py +++ b/tools/keystone_version.py @@ -36,8 +36,9 @@ def print_version(version): if version['status'] in ('CURRENT', 'stable'): print( "\tVersion ID: {id} updated {updated}".format( - id=version.get('id'), - updated=version.get('updated'))) + id=version.get('id'), updated=version.get('updated') + ) + ) verbose = '-v' in sys.argv @@ -71,7 +72,8 @@ def print_version(version): if port: stripped = '{stripped}:{port}'.format(stripped=stripped, port=port) endpoint = urlparse.urlunsplit( - (url.scheme, url.netloc, stripped, url.params, url.query)) + (url.scheme, url.netloc, stripped, url.params, url.query) + ) print(" also {endpoint}".format(endpoint=endpoint)) try: r = c.get(endpoint).json() diff --git a/tools/nova_version.py b/tools/nova_version.py index 955812251..ad8ea0cbc 100644 --- a/tools/nova_version.py +++ b/tools/nova_version.py @@ -35,22 +35,27 @@ have_current = True print( "\tVersion ID: {id} updated {updated}".format( - id=version.get('id'), - updated=version.get('updated'))) + id=version.get('id'), updated=version.get('updated') + ) + ) + print("\tVersion Max: {max}".format(max=version.get('version'))) print( - "\tVersion Max: {max}".format(max=version.get('version'))) - print( - "\tVersion Min: {min}".format(min=version.get('min_version'))) + "\tVersion Min: {min}".format(min=version.get('min_version')) + ) if not have_current: for version in r['versions']: if version['status'] == 'SUPPORTED': have_current = True print( "\tVersion ID: {id} updated {updated}".format( - id=version.get('id'), - updated=version.get('updated'))) + id=version.get('id'), updated=version.get('updated') + ) + ) print( - "\tVersion Max: {max}".format(max=version.get('version'))) + "\tVersion Max: {max}".format(max=version.get('version')) + ) print( "\tVersion Min: {min}".format( - min=version.get('min_version'))) + min=version.get('min_version') + ) + ) diff --git a/tools/print-services.py b/tools/print-services.py index f30d29106..6b9c95b8e 100644 --- a/tools/print-services.py +++ b/tools/print-services.py @@ -42,16 +42,16 @@ def make_names(): if desc_class.__module__ != 'openstack.service_description': base_mod, dm = desc_class.__module__.rsplit('.', 1) imports.append( - 'from {base_mod} import {dm}'.format( - base_mod=base_mod, - dm=dm)) + 'from {base_mod} import {dm}'.format(base_mod=base_mod, dm=dm) + ) else: dm = 'service_description' dc = desc_class.__name__ services.append( "{st} = {dm}.{dc}(service_type='{service_type}')".format( - st=st, dm=dm, dc=dc, service_type=service_type), + st=st, dm=dm, dc=dc, service_type=service_type + ), ) # Register the descriptor class with every known alias. Don't @@ -63,9 +63,8 @@ def make_names(): if alias_name[-1].isdigit(): continue services.append( - '{alias_name} = {st}'.format( - alias_name=alias_name, - st=st)) + '{alias_name} = {st}'.format(alias_name=alias_name, st=st) + ) services.append('') print("# Generated file, to change, run tools/print-services.py") for imp in sorted(imports): From 48ff44f046f1f2643fce1da9f9325a1cbc4a2039 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 5 May 2023 11:27:59 +0100 Subject: [PATCH 3258/3836] Ignore blackify changes Add a .git-blame-ignore-revs file to ignore the blackify changes. File was generated with the following command: git log --pretty=format:"%H # %s" --no-merges --grep='Blackify' Change-Id: Iddd92a3c1d1b62bc55dabbf040f7e4680d2715e3 Signed-off-by: Stephen Finucane --- .git-blame-ignore-revs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..494eff046 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,26 @@ +# You can configure git to automatically use this file with the following config: +# git config --global blame.ignoreRevsFile .git-blame-ignore-revs + +a36f514295a4b4e6157ce69a210f653bcc4df7f2 # Blackify everything else +004c7352d0a4fb467a319ae9743eb6ca5ee9ce7f # Blackify openstack.cloud +c2ff7336cecabc665e7bf04cbe87ef8d0c2e6f9f # Blackify openstack.clustering +073abda5a94b12a319c79d6a9b8594036f95fc65 # Blackify openstack.container_infrastructure_management +570b81f0ec3b3876aefbb223c78093f2a957bb01 # Blackify openstack.accelerator +33bed575013f11e4d408593e53c6c99ca66d6110 # Blackify openstack.instance_ha +10018dbf5be5e19c87543a5931f6809006eba4c5 # Blackify openstack.dns +19ec9ba383d14f4af6a1bb78dbbeaa6638ee8a4f # Blackify openstack.database +0e2b5d263fdf12e0c8a67503712afab2816ef2d0 # Blackify openstack.message +9d3d986241ce110e8f6bdf3ecb19609dc417a10a # Blackify openstack.workflow +874ea74103a0c833df7668a45b96b7145a8158a2 # Blackify openstack.orchestration +409f648ce506d7e768305f75025c4b01c5fa3008 # Blackify openstack.placement +93d8f41713ec2128210bf0a8479a5f3872ce0382 # Blackify openstack.key_manager +3d2511f98025d2d2826e13cea8be7545e90990f7 # Blackify openstack.shared_file_system +82c2a534024cff7690620876723422a98e8f371a # Blackify openstack.load_balancer +f8e42017e756e383367145c4caf39de796babcba # Blackify openstack.baremetal, openstack.baremetal_introspection +4589e293e829950d2fd4c705cce2f7ce30ca9e29 # Blackify openstack.object_store +34da09f3125ccd0408f2e0019c85d95188fef573 # Blackify openstack.block_storage +542ddaa1ad5cfc9b9876de3de0759941c9a9ea83 # Blackify openstack.identity +f526b990f31de03a1b6181a4724976e1b86a654a # Blackify openstack.network +bcf99f3433ceecf9a210d0aa0580a67645ccf7ee # Blackify openstack.image +69735d3bd8fd874a9817c26b5b009921110fb416 # Blackify openstack.compute (tests) +395a77298ecd79623b1af75ad0dc7653f5e4eb61 # Blackify openstack.compute From c946294bddd0ad37b707c7f993cdbc4706c00d00 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 5 May 2023 11:35:18 +0100 Subject: [PATCH 3259/3836] pre-commit: Enable black Since our pep8 tox env now runs pre-commit, we can ensure that black styling will be maintained. Change-Id: Ie3d684cecf882498914f7726363e443d2d3e99a3 Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 09c805ab4..f593bdfd4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,6 +21,11 @@ repos: rev: v1.1.1 hooks: - id: doc8 + - repo: https://github.com/psf/black + rev: 22.8.0 + hooks: + - id: black + args: ['-S', '-l', '79'] - repo: local hooks: - id: flake8 From c7010a2f929de9fad4e1a7c7f5a17cb8e210432a Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 5 May 2023 12:44:02 +0100 Subject: [PATCH 3260/3836] Bump black to 23.3.0 I was using the distro-provided version of black, 22.8.0, during the blackify series. There's a newer version out however, 23.3.0. Bump this, applying the various changes it introduces in the process. Change-Id: I3442a7ddaf0cb35f37b84bdbe06625a28e982dba Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 2 +- examples/image/download.py | 1 - openstack/_services_mixin.py | 1 - openstack/baremetal/v1/_proxy.py | 1 - openstack/baremetal/v1/allocation.py | 1 - openstack/baremetal/v1/chassis.py | 1 - openstack/baremetal/v1/conductor.py | 1 - openstack/baremetal/v1/deploy_templates.py | 1 - openstack/baremetal/v1/driver.py | 1 - openstack/baremetal/v1/node.py | 2 -- openstack/baremetal/v1/port.py | 1 - openstack/baremetal/v1/port_group.py | 1 - openstack/baremetal/v1/volume_connector.py | 1 - openstack/baremetal/v1/volume_target.py | 1 - .../baremetal_introspection/v1/introspection.py | 1 - openstack/block_storage/v2/_proxy.py | 1 - openstack/block_storage/v2/quota_set.py | 1 - openstack/block_storage/v3/quota_set.py | 1 - openstack/cloud/_block_storage.py | 1 - openstack/cloud/_compute.py | 4 +--- openstack/cloud/_floating_ip.py | 3 --- openstack/cloud/_identity.py | 1 - openstack/cloud/_network_common.py | 1 - openstack/cloud/_security_group.py | 2 -- openstack/cloud/inventory.py | 1 - openstack/cloud/meta.py | 2 +- openstack/cloud/openstackcloud.py | 1 - openstack/common/metadata.py | 1 - openstack/common/tag.py | 1 - openstack/compute/v2/limits.py | 2 -- openstack/compute/v2/server_action.py | 1 - openstack/compute/v2/server_ip.py | 1 - openstack/config/_util.py | 2 +- openstack/config/loader.py | 15 ++++++--------- .../v1/_proxy.py | 1 - .../v1/cluster.py | 1 - .../v1/cluster_certificate.py | 1 - .../v1/cluster_template.py | 1 - .../v1/service.py | 1 - openstack/fixture/connection.py | 1 - openstack/identity/v2/extension.py | 1 - openstack/identity/version.py | 1 - openstack/image/v2/_proxy.py | 1 - openstack/object_store/v1/_base.py | 1 - openstack/object_store/v1/_proxy.py | 3 +-- openstack/object_store/v1/info.py | 1 - openstack/orchestration/util/template_utils.py | 2 -- openstack/orchestration/v1/stack.py | 1 - openstack/orchestration/v1/stack_environment.py | 1 - openstack/orchestration/v1/stack_files.py | 1 - openstack/orchestration/v1/stack_template.py | 1 - openstack/orchestration/v1/template.py | 1 - openstack/resource.py | 3 +-- openstack/service_description.py | 2 -- openstack/shared_file_system/v2/storage_pool.py | 1 - openstack/tests/functional/baremetal/base.py | 1 - .../baremetal/test_baremetal_allocation.py | 2 -- .../baremetal/test_baremetal_chassis.py | 1 - .../baremetal/test_baremetal_conductor.py | 1 - .../test_baremetal_deploy_templates.py | 1 - .../baremetal/test_baremetal_driver.py | 1 - .../functional/baremetal/test_baremetal_node.py | 4 ---- .../functional/baremetal/test_baremetal_port.py | 1 - .../baremetal/test_baremetal_port_group.py | 1 - .../test_baremetal_volume_connector.py | 1 - .../baremetal/test_baremetal_volume_target.py | 1 - openstack/tests/functional/base.py | 1 - .../tests/functional/block_storage/v2/base.py | 1 - .../tests/functional/block_storage/v3/base.py | 1 - .../tests/functional/cloud/test_devstack.py | 1 - .../tests/functional/cloud/test_endpoints.py | 1 - .../tests/functional/cloud/test_floating_ip.py | 1 - .../functional/cloud/test_project_cleanup.py | 1 - .../tests/functional/cloud/test_services.py | 1 - openstack/tests/functional/cloud/test_stack.py | 1 - openstack/tests/functional/cloud/test_volume.py | 1 - .../tests/functional/clustering/test_cluster.py | 1 - openstack/tests/functional/compute/base.py | 1 - openstack/tests/functional/image/v2/base.py | 1 - .../image/v2/test_metadef_namespace.py | 1 - .../load_balancer/v2/test_load_balancer.py | 1 - .../functional/network/v2/test_address_group.py | 1 - .../functional/network/v2/test_address_scope.py | 1 - .../tests/functional/network/v2/test_agent.py | 1 - .../network/v2/test_agent_add_remove_network.py | 1 - .../network/v2/test_agent_add_remove_router.py | 1 - .../network/v2/test_auto_allocated_topology.py | 1 - .../network/v2/test_availability_zone.py | 1 - .../functional/network/v2/test_dvr_router.py | 1 - .../network/v2/test_firewall_group.py | 1 - .../network/v2/test_firewall_policy.py | 1 - .../functional/network/v2/test_firewall_rule.py | 1 - .../test_firewall_rule_insert_remove_policy.py | 1 - .../tests/functional/network/v2/test_flavor.py | 1 - .../functional/network/v2/test_floating_ip.py | 1 - .../network/v2/test_l3_conntrack_helper.py | 1 - .../functional/network/v2/test_local_ip.py | 1 - .../functional/network/v2/test_ndp_proxy.py | 1 - .../tests/functional/network/v2/test_network.py | 1 - .../network/v2/test_network_ip_availability.py | 1 - .../network/v2/test_network_segment_range.py | 1 - .../tests/functional/network/v2/test_port.py | 1 - .../network/v2/test_port_forwarding.py | 1 - .../network/v2/test_qos_bandwidth_limit_rule.py | 1 - .../network/v2/test_qos_dscp_marking_rule.py | 1 - .../v2/test_qos_minimum_bandwidth_rule.py | 1 - .../v2/test_qos_minimum_packet_rate_rule.py | 1 - .../functional/network/v2/test_qos_policy.py | 1 - .../functional/network/v2/test_qos_rule_type.py | 1 - .../functional/network/v2/test_rbac_policy.py | 1 - .../tests/functional/network/v2/test_router.py | 1 - .../v2/test_router_add_remove_interface.py | 1 - .../network/v2/test_security_group.py | 1 - .../network/v2/test_security_group_rule.py | 1 - .../tests/functional/network/v2/test_segment.py | 1 - .../network/v2/test_service_profile.py | 1 - .../tests/functional/network/v2/test_subnet.py | 1 - .../network/v2/test_subnet_from_subnet_pool.py | 1 - .../functional/network/v2/test_subnet_pool.py | 1 - .../tests/functional/network/v2/test_trunk.py | 1 - .../tests/functional/network/v2/test_vpnaas.py | 1 - .../functional/object_store/v1/test_obj.py | 1 - .../functional/orchestration/v1/test_stack.py | 1 - .../tests/functional/shared_file_system/base.py | 1 - .../test_availability_zone.py | 1 - .../shared_file_system/test_export_locations.py | 1 - .../shared_file_system/test_share_instance.py | 1 - openstack/tests/unit/base.py | 4 +--- openstack/tests/unit/cloud/test_caching.py | 5 ----- .../tests/unit/cloud/test_cluster_templates.py | 2 -- openstack/tests/unit/cloud/test_flavors.py | 1 - .../unit/cloud/test_floating_ip_neutron.py | 1 - .../tests/unit/cloud/test_floating_ip_nova.py | 2 -- .../tests/unit/cloud/test_floating_ip_pool.py | 2 -- openstack/tests/unit/cloud/test_image.py | 2 -- openstack/tests/unit/cloud/test_meta.py | 1 - openstack/tests/unit/cloud/test_network.py | 1 - openstack/tests/unit/cloud/test_object.py | 17 ----------------- .../unit/cloud/test_qos_bandwidth_limit_rule.py | 1 - .../unit/cloud/test_qos_dscp_marking_rule.py | 1 - .../cloud/test_qos_minimum_bandwidth_rule.py | 1 - openstack/tests/unit/cloud/test_qos_policy.py | 1 - .../tests/unit/cloud/test_qos_rule_type.py | 1 - openstack/tests/unit/cloud/test_router.py | 1 - .../tests/unit/cloud/test_security_groups.py | 4 ---- .../tests/unit/cloud/test_server_console.py | 2 -- openstack/tests/unit/cloud/test_server_group.py | 1 - openstack/tests/unit/cloud/test_services.py | 1 - openstack/tests/unit/cloud/test_stack.py | 1 - openstack/tests/unit/cloud/test_subnet.py | 1 - openstack/tests/unit/compute/v2/test_proxy.py | 1 - .../tests/unit/config/test_from_session.py | 1 - openstack/tests/unit/fake/v1/_proxy.py | 1 - openstack/tests/unit/fake/v2/_proxy.py | 1 - .../tests/unit/load_balancer/v2/test_proxy.py | 1 - .../tests/unit/object_store/v1/test_proxy.py | 9 +-------- .../tests/unit/orchestration/v1/test_proxy.py | 2 -- .../tests/unit/orchestration/v1/test_stack.py | 1 - openstack/tests/unit/test_connection.py | 5 ----- openstack/tests/unit/test_microversions.py | 7 ------- openstack/tests/unit/test_missing_version.py | 1 - openstack/tests/unit/test_proxy.py | 2 -- openstack/tests/unit/test_resource.py | 4 ---- openstack/tests/unit/test_stats.py | 8 +------- openstack/tests/unit/test_utils.py | 1 - openstack/utils.py | 4 ++-- 166 files changed, 17 insertions(+), 248 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f593bdfd4..c8c9f25d9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,7 @@ repos: hooks: - id: doc8 - repo: https://github.com/psf/black - rev: 22.8.0 + rev: 23.3.0 hooks: - id: black args: ['-S', '-l', '79'] diff --git a/examples/image/download.py b/examples/image/download.py index 85d92b214..d5513dff7 100644 --- a/examples/image/download.py +++ b/examples/image/download.py @@ -38,7 +38,6 @@ def download_image_stream(conn): # Read only 1024 bytes of memory at a time until # all of the image data has been consumed. for chunk in response.iter_content(chunk_size=1024): - # With each chunk, add it to the hash to be computed. md5.update(chunk) diff --git a/openstack/_services_mixin.py b/openstack/_services_mixin.py index 187c2862c..9e2f4ace6 100644 --- a/openstack/_services_mixin.py +++ b/openstack/_services_mixin.py @@ -26,7 +26,6 @@ class ServicesMixin: - identity = identity_service.IdentityService(service_type='identity') compute = compute_service.ComputeService(service_type='compute') diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 0fd64aea2..90fd14e5d 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -27,7 +27,6 @@ class Proxy(proxy.Proxy): - retriable_status_codes = _common.RETRIABLE_STATUS_CODES _resource_registry = { diff --git a/openstack/baremetal/v1/allocation.py b/openstack/baremetal/v1/allocation.py index c8887b4fa..3179f15ba 100644 --- a/openstack/baremetal/v1/allocation.py +++ b/openstack/baremetal/v1/allocation.py @@ -17,7 +17,6 @@ class Allocation(_common.ListMixin, resource.Resource): - resources_key = 'allocations' base_path = '/allocations' diff --git a/openstack/baremetal/v1/chassis.py b/openstack/baremetal/v1/chassis.py index 635177e31..0640bb785 100644 --- a/openstack/baremetal/v1/chassis.py +++ b/openstack/baremetal/v1/chassis.py @@ -15,7 +15,6 @@ class Chassis(_common.ListMixin, resource.Resource): - resources_key = 'chassis' base_path = '/chassis' diff --git a/openstack/baremetal/v1/conductor.py b/openstack/baremetal/v1/conductor.py index 05ecf043e..15c3b76b7 100644 --- a/openstack/baremetal/v1/conductor.py +++ b/openstack/baremetal/v1/conductor.py @@ -15,7 +15,6 @@ class Conductor(_common.ListMixin, resource.Resource): - resources_key = 'conductors' base_path = '/conductors' diff --git a/openstack/baremetal/v1/deploy_templates.py b/openstack/baremetal/v1/deploy_templates.py index 3c9e40420..17f59ea79 100644 --- a/openstack/baremetal/v1/deploy_templates.py +++ b/openstack/baremetal/v1/deploy_templates.py @@ -15,7 +15,6 @@ class DeployTemplate(_common.ListMixin, resource.Resource): - resources_key = 'deploy_templates' base_path = '/deploy_templates' diff --git a/openstack/baremetal/v1/driver.py b/openstack/baremetal/v1/driver.py index d8bfe4d5f..9018816d0 100644 --- a/openstack/baremetal/v1/driver.py +++ b/openstack/baremetal/v1/driver.py @@ -17,7 +17,6 @@ class Driver(resource.Resource): - resources_key = 'drivers' base_path = '/drivers' diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 44b0ec407..fe1081371 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -71,7 +71,6 @@ class WaitResult( class Node(_common.ListMixin, resource.Resource): - resources_key = 'nodes' base_path = '/nodes' @@ -1323,7 +1322,6 @@ def patch( base_path=None, reset_interfaces=None, ): - if reset_interfaces is not None: # The id cannot be dirty for an commit self._body._dirty.discard("id") diff --git a/openstack/baremetal/v1/port.py b/openstack/baremetal/v1/port.py index 79d3c6369..1435d7c13 100644 --- a/openstack/baremetal/v1/port.py +++ b/openstack/baremetal/v1/port.py @@ -15,7 +15,6 @@ class Port(_common.ListMixin, resource.Resource): - resources_key = 'ports' base_path = '/ports' diff --git a/openstack/baremetal/v1/port_group.py b/openstack/baremetal/v1/port_group.py index 123a563e6..5460058ae 100644 --- a/openstack/baremetal/v1/port_group.py +++ b/openstack/baremetal/v1/port_group.py @@ -15,7 +15,6 @@ class PortGroup(_common.ListMixin, resource.Resource): - resources_key = 'portgroups' base_path = '/portgroups' diff --git a/openstack/baremetal/v1/volume_connector.py b/openstack/baremetal/v1/volume_connector.py index 395e90cbb..70e009f02 100644 --- a/openstack/baremetal/v1/volume_connector.py +++ b/openstack/baremetal/v1/volume_connector.py @@ -15,7 +15,6 @@ class VolumeConnector(_common.ListMixin, resource.Resource): - resources_key = 'connectors' base_path = '/volume/connectors' diff --git a/openstack/baremetal/v1/volume_target.py b/openstack/baremetal/v1/volume_target.py index 52d6bd382..f2b933294 100644 --- a/openstack/baremetal/v1/volume_target.py +++ b/openstack/baremetal/v1/volume_target.py @@ -15,7 +15,6 @@ class VolumeTarget(_common.ListMixin, resource.Resource): - resources_key = 'targets' base_path = '/volume/targets' diff --git a/openstack/baremetal_introspection/v1/introspection.py b/openstack/baremetal_introspection/v1/introspection.py index 7a0c2a67f..906b257b2 100644 --- a/openstack/baremetal_introspection/v1/introspection.py +++ b/openstack/baremetal_introspection/v1/introspection.py @@ -21,7 +21,6 @@ class Introspection(resource.Resource): - resources_key = 'introspection' base_path = '/introspection' diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 19bcf4583..ee31bbf1d 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -22,7 +22,6 @@ class Proxy(_base_proxy.BaseBlockStorageProxy): - # ====== SNAPSHOTS ====== def get_snapshot(self, snapshot): """Get a single snapshot diff --git a/openstack/block_storage/v2/quota_set.py b/openstack/block_storage/v2/quota_set.py index 0ef3b51f9..568e58c40 100644 --- a/openstack/block_storage/v2/quota_set.py +++ b/openstack/block_storage/v2/quota_set.py @@ -14,7 +14,6 @@ class QuotaSet(quota_set.QuotaSet): - #: Properties #: The size (GB) of backups that are allowed for each project. backup_gigabytes = resource.Body('backup_gigabytes', type=int) diff --git a/openstack/block_storage/v3/quota_set.py b/openstack/block_storage/v3/quota_set.py index 0ef3b51f9..568e58c40 100644 --- a/openstack/block_storage/v3/quota_set.py +++ b/openstack/block_storage/v3/quota_set.py @@ -14,7 +14,6 @@ class QuotaSet(quota_set.QuotaSet): - #: Properties #: The size (GB) of backups that are allowed for each project. backup_gigabytes = resource.Body('backup_gigabytes', type=int) diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index c63ab716b..e20bee648 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -294,7 +294,6 @@ def get_volume_limits(self, name_or_id=None): project_id = None error_msg = "Failed to get limits" if name_or_id: - proj = self.get_project(name_or_id) if not proj: raise exc.OpenStackCloudException("project does not exist") diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index b2ab77850..81be3f31c 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -432,7 +432,6 @@ def get_compute_limits(self, name_or_id=None): params = {} project_id = None if name_or_id: - proj = self.get_project(name_or_id) if not proj: raise exc.OpenStackCloudException("project does not exist") @@ -874,7 +873,7 @@ def create_server( user_data = kwargs.pop('userdata') if user_data: kwargs['user_data'] = self._encode_server_userdata(user_data) - for (desired, given) in ( + for desired, given in ( ('OS-DCF:diskConfig', 'disk_config'), ('config_drive', 'config_drive'), ('key_name', 'key_name'), @@ -1092,7 +1091,6 @@ def _get_boot_from_volume_kwargs( kwargs['block_device_mapping_v2'].append(block_mapping) kwargs['imageRef'] = '' elif boot_from_volume: - if isinstance(image, dict): image_obj = image else: diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index 951173140..30b1f32e7 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -495,7 +495,6 @@ def _neutron_create_floating_ip( timeout=60, network_id=None, ): - if not network_id: if network_name_or_id: try: @@ -796,7 +795,6 @@ def _attach_ip_to_server( def _neutron_attach_ip_to_server( self, server, floating_ip, fixed_address=None, nat_destination=None ): - # Find an available port (port, fixed_address) = self._nat_destination_port( server, @@ -884,7 +882,6 @@ def _neutron_detach_ip_from_server(self, server_id, floating_ip_id): return True def _nova_detach_ip_from_server(self, server_id, floating_ip_id): - f_ip = self.get_floating_ip(id=floating_ip_id) if f_ip is None: raise exc.OpenStackCloudException( diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index a6cc6e29d..0468f919b 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -454,7 +454,6 @@ def create_service(self, name, enabled=True, **kwargs): 'name', 'enabled', 'type', 'service_type', 'description' ) def update_service(self, name_or_id, **kwargs): - # NOTE(SamYaple): Keystone v3 only accepts 'type' but shade accepts # both 'type' and 'service_type' with a preference # towards 'type' diff --git a/openstack/cloud/_network_common.py b/openstack/cloud/_network_common.py index aa774345e..b466a8edc 100644 --- a/openstack/cloud/_network_common.py +++ b/openstack/cloud/_network_common.py @@ -90,7 +90,6 @@ def _set_interesting_networks(self): return for network in all_networks: - # External IPv4 networks if ( network['name'] in self._external_ipv4_names diff --git a/openstack/cloud/_security_group.py b/openstack/cloud/_security_group.py index 08d56e90c..5f9e2254b 100644 --- a/openstack/cloud/_security_group.py +++ b/openstack/cloud/_security_group.py @@ -465,7 +465,6 @@ def _normalize_secgroups(self, groups): # TODO(stephenfin): Remove this once we get rid of support for nova # secgroups def _normalize_secgroup(self, group): - ret = utils.Munch() # Copy incoming group because of shared dicts in unittests group = group.copy() @@ -518,7 +517,6 @@ def _normalize_secgroup_rules(self, rules): # TODO(stephenfin): Remove this once we get rid of support for nova # secgroups def _normalize_secgroup_rule(self, rule): - ret = utils.Munch() # Copy incoming rule because of shared dicts in unittests rule = rule.copy() diff --git a/openstack/cloud/inventory.py b/openstack/cloud/inventory.py index 42b9402dc..7b723ff94 100644 --- a/openstack/cloud/inventory.py +++ b/openstack/cloud/inventory.py @@ -23,7 +23,6 @@ class OpenStackInventory: - # Put this here so the capability can be detected with hasattr on the class extra_config = None diff --git a/openstack/cloud/meta.py b/openstack/cloud/meta.py index c1e6f692f..0f050cce0 100644 --- a/openstack/cloud/meta.py +++ b/openstack/cloud/meta.py @@ -27,7 +27,7 @@ def find_nova_interfaces( addresses, ext_tag=None, key_name=None, version=4, mac_addr=None ): ret = [] - for (k, v) in iter(addresses.items()): + for k, v in iter(addresses.items()): if key_name is not None and k != key_name: # key_name is specified and it doesn't match the current network. # Continue with the next one diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 76f73b500..c68bdff72 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -75,7 +75,6 @@ class _OpenStackCloudMixin: _SHADE_OBJECT_AUTOCREATE_KEY = 'x-object-meta-x-shade-autocreated' def __init__(self): - super(_OpenStackCloudMixin, self).__init__() self.log = _log.setup_logging('openstack') diff --git a/openstack/common/metadata.py b/openstack/common/metadata.py index 531663ea1..81a6a98ef 100644 --- a/openstack/common/metadata.py +++ b/openstack/common/metadata.py @@ -15,7 +15,6 @@ class MetadataMixin: - #: *Type: list of tag strings* metadata = resource.Body('metadata', type=dict) diff --git a/openstack/common/tag.py b/openstack/common/tag.py index 6f07a36f5..0d25693ff 100644 --- a/openstack/common/tag.py +++ b/openstack/common/tag.py @@ -15,7 +15,6 @@ class TagMixin: - _tag_query_parameters = { 'tags': 'tags', 'any_tags': 'tags-any', diff --git a/openstack/compute/v2/limits.py b/openstack/compute/v2/limits.py index 2e2e68432..5c17a2976 100644 --- a/openstack/compute/v2/limits.py +++ b/openstack/compute/v2/limits.py @@ -14,7 +14,6 @@ class AbsoluteLimits(resource.Resource): - _max_microversion = '2.57' #: The number of key-value pairs that can be set as image metadata. @@ -74,7 +73,6 @@ class AbsoluteLimits(resource.Resource): class RateLimit(resource.Resource): - # TODO(mordred) Make a resource type for the contents of limit and add # it to list_type here. #: A list of the specific limits that apply to the ``regex`` and ``uri``. diff --git a/openstack/compute/v2/server_action.py b/openstack/compute/v2/server_action.py index 06e4a12e3..07e45d4d7 100644 --- a/openstack/compute/v2/server_action.py +++ b/openstack/compute/v2/server_action.py @@ -14,7 +14,6 @@ class ServerActionEvent(resource.Resource): - # Added the 'details' field in 2.84 _max_microversion = '2.84' diff --git a/openstack/compute/v2/server_ip.py b/openstack/compute/v2/server_ip.py index e76e71f83..df85e0687 100644 --- a/openstack/compute/v2/server_ip.py +++ b/openstack/compute/v2/server_ip.py @@ -41,7 +41,6 @@ def list( base_path=None, **params ): - if base_path is None: base_path = cls.base_path diff --git a/openstack/config/_util.py b/openstack/config/_util.py index d52c6e893..d886179c9 100644 --- a/openstack/config/_util.py +++ b/openstack/config/_util.py @@ -36,7 +36,7 @@ def normalize_keys(config): def merge_clouds(old_dict, new_dict): """Like dict.update, except handling nested dicts.""" ret = old_dict.copy() - for (k, v) in new_dict.items(): + for k, v in new_dict.items(): if isinstance(v, dict): if k in ret: ret[k] = merge_clouds(ret[k], v) diff --git a/openstack/config/loader.py b/openstack/config/loader.py index c4f37c33a..63d8c71bf 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -101,7 +101,7 @@ def get_boolean(value): def _auth_update(old_dict, new_dict_source): """Like dict.update, except handling the nested dict called auth.""" new_dict = copy.deepcopy(new_dict_source) - for (k, v) in new_dict.items(): + for k, v in new_dict.items(): if k == 'auth': if k in old_dict: old_dict[k].update(v) @@ -143,7 +143,6 @@ def _fix_argv(argv): class OpenStackConfig: - # These two attribute are to allow os-client-config to plumb in its # local versions for backwards compat. # They should not be used by anyone else. @@ -949,7 +948,6 @@ def _fix_backwards_api_timeout(self, cloud): return new_cloud def get_all(self): - clouds = [] for cloud in self.get_cloud_names(): @@ -985,7 +983,7 @@ def _fix_args(self, args=None, argparse=None): os_args = dict() new_args = dict() - for (key, val) in iter(args.items()): + for key, val in iter(args.items()): if type(args[key]) == dict: # dive into the auth dict new_args[key] = self._fix_args(args[key]) @@ -1136,7 +1134,6 @@ def option_prompt(self, config, p_opt): return config def _clean_up_after_ourselves(self, config, p_opt, winning_value): - # Clean up after ourselves for opt in [p_opt.name] + [o.name for o in p_opt.deprecated]: opt = opt.replace('-', '_') @@ -1245,7 +1242,7 @@ def get_one(self, cloud=None, validate=True, argparse=None, **kwargs): # Can't just do update, because None values take over for arg_list in region_args, args: - for (key, val) in iter(arg_list.items()): + for key, val in iter(arg_list.items()): if val is not None: if key == 'auth' and config[key] is not None: config[key] = _auth_update(config[key], val) @@ -1268,7 +1265,7 @@ def get_one(self, cloud=None, validate=True, argparse=None, **kwargs): auth_plugin = None # If any of the defaults reference other values, we need to expand - for (key, value) in config.items(): + for key, value in config.items(): if hasattr(value, 'format') and key not in FORMAT_EXCLUSIONS: config[key] = value.format(**config) @@ -1367,7 +1364,7 @@ def get_one_cloud_osc( # Can't just do update, because None values take over for arg_list in region_args, args: - for (key, val) in iter(arg_list.items()): + for key, val in iter(arg_list.items()): if val is not None: if key == 'auth' and config[key] is not None: config[key] = _auth_update(config[key], val) @@ -1389,7 +1386,7 @@ def get_one_cloud_osc( auth_plugin = None # If any of the defaults reference other values, we need to expand - for (key, value) in config.items(): + for key, value in config.items(): if hasattr(value, 'format') and key not in FORMAT_EXCLUSIONS: config[key] = value.format(**config) diff --git a/openstack/container_infrastructure_management/v1/_proxy.py b/openstack/container_infrastructure_management/v1/_proxy.py index c0ee5204c..1e95b428d 100644 --- a/openstack/container_infrastructure_management/v1/_proxy.py +++ b/openstack/container_infrastructure_management/v1/_proxy.py @@ -26,7 +26,6 @@ class Proxy(proxy.Proxy): - _resource_registry = { "cluster": _cluster.Cluster, "cluster_template": _cluster_template.ClusterTemplate, diff --git a/openstack/container_infrastructure_management/v1/cluster.py b/openstack/container_infrastructure_management/v1/cluster.py index bd7618555..776cdeee2 100644 --- a/openstack/container_infrastructure_management/v1/cluster.py +++ b/openstack/container_infrastructure_management/v1/cluster.py @@ -16,7 +16,6 @@ class Cluster(resource.Resource): - resources_key = 'clusters' base_path = '/clusters' diff --git a/openstack/container_infrastructure_management/v1/cluster_certificate.py b/openstack/container_infrastructure_management/v1/cluster_certificate.py index 5bf9a3656..0bd8d6fd0 100644 --- a/openstack/container_infrastructure_management/v1/cluster_certificate.py +++ b/openstack/container_infrastructure_management/v1/cluster_certificate.py @@ -14,7 +14,6 @@ class ClusterCertificate(resource.Resource): - base_path = '/certificates' # capabilities diff --git a/openstack/container_infrastructure_management/v1/cluster_template.py b/openstack/container_infrastructure_management/v1/cluster_template.py index 31acf376f..0f743ca03 100644 --- a/openstack/container_infrastructure_management/v1/cluster_template.py +++ b/openstack/container_infrastructure_management/v1/cluster_template.py @@ -14,7 +14,6 @@ class ClusterTemplate(resource.Resource): - resources_key = 'clustertemplates' base_path = '/clustertemplates' diff --git a/openstack/container_infrastructure_management/v1/service.py b/openstack/container_infrastructure_management/v1/service.py index 277fa54b7..5937f2045 100644 --- a/openstack/container_infrastructure_management/v1/service.py +++ b/openstack/container_infrastructure_management/v1/service.py @@ -14,7 +14,6 @@ class Service(resource.Resource): - resources_key = 'mservices' base_path = '/mservices' diff --git a/openstack/fixture/connection.py b/openstack/fixture/connection.py index c3aee3041..80195602d 100644 --- a/openstack/fixture/connection.py +++ b/openstack/fixture/connection.py @@ -34,7 +34,6 @@ class ConnectionFixture(fixtures.Fixture): - _suffixes = { 'baremetal': '/', 'block-storage': '/{project_id}', diff --git a/openstack/identity/v2/extension.py b/openstack/identity/v2/extension.py index 1e0d26428..e5343b2b4 100644 --- a/openstack/identity/v2/extension.py +++ b/openstack/identity/v2/extension.py @@ -44,7 +44,6 @@ class Extension(resource.Resource): @classmethod def list(cls, session, paginated=False, base_path=None, **params): - if base_path is None: base_path = cls.base_path diff --git a/openstack/identity/version.py b/openstack/identity/version.py index ee329edc6..2742e7a22 100644 --- a/openstack/identity/version.py +++ b/openstack/identity/version.py @@ -28,7 +28,6 @@ class Version(resource.Resource): @classmethod def list(cls, session, paginated=False, base_path=None, **params): - if base_path is None: base_path = cls.base_path diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 54927a50e..6c6b50249 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -49,7 +49,6 @@ def _get_name_and_filename(name, image_format): class Proxy(proxy.Proxy): - _resource_registry = { "cache": _cache.Cache, "image": _image.Image, diff --git a/openstack/object_store/v1/_base.py b/openstack/object_store/v1/_base.py index 19f4c4cdc..a39286f5e 100644 --- a/openstack/object_store/v1/_base.py +++ b/openstack/object_store/v1/_base.py @@ -16,7 +16,6 @@ class BaseResource(resource.Resource): - commit_method = 'POST' create_method = 'PUT' diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index 3bd4edf2a..4e45851f3 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -447,7 +447,6 @@ def create_object( file_size = os.path.getsize(filename) if self.is_object_stale(container_name, name, filename, md5, sha256): - self._connection.log.debug( "swift uploading %(filename)s to %(endpoint)s", {'filename': filename, 'endpoint': endpoint}, @@ -744,7 +743,7 @@ def _upload_object(self, endpoint, filename, headers): def _get_file_segments(self, endpoint, filename, file_size, segment_size): # Use an ordered dict here so that testing can replicate things segments = collections.OrderedDict() - for (index, offset) in enumerate(range(0, file_size, segment_size)): + for index, offset in enumerate(range(0, file_size, segment_size)): remaining = file_size - (index * segment_size) segment = _utils.FileSegment( filename, diff --git a/openstack/object_store/v1/info.py b/openstack/object_store/v1/info.py index a4f048eab..f3999ad0a 100644 --- a/openstack/object_store/v1/info.py +++ b/openstack/object_store/v1/info.py @@ -18,7 +18,6 @@ class Info(resource.Resource): - base_path = "/info" allow_fetch = True diff --git a/openstack/orchestration/util/template_utils.py b/openstack/orchestration/util/template_utils.py index f61ca7b92..43bc657be 100644 --- a/openstack/orchestration/util/template_utils.py +++ b/openstack/orchestration/util/template_utils.py @@ -31,7 +31,6 @@ def get_template_contents( files=None, existing=False, ): - is_object = False tpl = None @@ -123,7 +122,6 @@ def get_file_contents( is_object=False, object_request=None, ): - if recurse_if and recurse_if(from_data): if isinstance(from_data, dict): recurse_data = from_data.values() diff --git a/openstack/orchestration/v1/stack.py b/openstack/orchestration/v1/stack.py index ef27225aa..4ca9c9dbb 100644 --- a/openstack/orchestration/v1/stack.py +++ b/openstack/orchestration/v1/stack.py @@ -187,7 +187,6 @@ def fetch( skip_cache=False, resolve_outputs=True, ): - if not self.allow_fetch: raise exceptions.MethodNotSupported(self, "fetch") diff --git a/openstack/orchestration/v1/stack_environment.py b/openstack/orchestration/v1/stack_environment.py index 24b05e750..e61bea9da 100644 --- a/openstack/orchestration/v1/stack_environment.py +++ b/openstack/orchestration/v1/stack_environment.py @@ -14,7 +14,6 @@ class StackEnvironment(resource.Resource): - base_path = "/stacks/%(stack_name)s/%(stack_id)s/environment" # capabilities diff --git a/openstack/orchestration/v1/stack_files.py b/openstack/orchestration/v1/stack_files.py index 98b28aa37..71a735623 100644 --- a/openstack/orchestration/v1/stack_files.py +++ b/openstack/orchestration/v1/stack_files.py @@ -14,7 +14,6 @@ class StackFiles(resource.Resource): - base_path = "/stacks/%(stack_name)s/%(stack_id)s/files" # capabilities diff --git a/openstack/orchestration/v1/stack_template.py b/openstack/orchestration/v1/stack_template.py index 7ba46e767..d8bb7a967 100644 --- a/openstack/orchestration/v1/stack_template.py +++ b/openstack/orchestration/v1/stack_template.py @@ -14,7 +14,6 @@ class StackTemplate(resource.Resource): - base_path = "/stacks/%(stack_name)s/%(stack_id)s/template" # capabilities diff --git a/openstack/orchestration/v1/template.py b/openstack/orchestration/v1/template.py index 64ec8aa27..1ae9ebfe7 100644 --- a/openstack/orchestration/v1/template.py +++ b/openstack/orchestration/v1/template.py @@ -16,7 +16,6 @@ class Template(resource.Resource): - # capabilities allow_create = False allow_list = False diff --git a/openstack/resource.py b/openstack/resource.py index 644eaa3bd..3443f584e 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -87,7 +87,6 @@ def _convert_type(value, data_type, list_type=None): class _BaseComponent: - # The name this component is being tracked as in the Resource key = None # The class to be used for mappings @@ -2031,7 +2030,7 @@ def list( ) client_filters = dict() # Gather query parameters which are not supported by the server - for (k, v) in params.items(): + for k, v in params.items(): if ( # Known attr hasattr(cls, k) diff --git a/openstack/service_description.py b/openstack/service_description.py index 49cc81eab..0cf7ec844 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -42,7 +42,6 @@ def __getattr__(self, item): class ServiceDescription: - #: Dictionary of supported versions and proxy classes for that version supported_versions = None #: main service_type to use to find this service in the catalog @@ -216,7 +215,6 @@ def _make_proxy(self, instance): ) if proxy_obj: - if getattr(proxy_obj, 'skip_discovery', False): # Some services, like swift, don't have discovery. While # keystoneauth will behave correctly and handle such diff --git a/openstack/shared_file_system/v2/storage_pool.py b/openstack/shared_file_system/v2/storage_pool.py index 0f271e802..49ebe399d 100644 --- a/openstack/shared_file_system/v2/storage_pool.py +++ b/openstack/shared_file_system/v2/storage_pool.py @@ -14,7 +14,6 @@ class StoragePool(resource.Resource): - resources_key = "pools" base_path = "/scheduler-stats/pools" diff --git a/openstack/tests/functional/baremetal/base.py b/openstack/tests/functional/baremetal/base.py index b0e03696f..cb22f73e1 100644 --- a/openstack/tests/functional/baremetal/base.py +++ b/openstack/tests/functional/baremetal/base.py @@ -14,7 +14,6 @@ class BaseBaremetalTest(base.BaseFunctionalTest): - min_microversion = None node_id = None diff --git a/openstack/tests/functional/baremetal/test_baremetal_allocation.py b/openstack/tests/functional/baremetal/test_baremetal_allocation.py index c6a3fa2d4..aea6d381f 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_allocation.py +++ b/openstack/tests/functional/baremetal/test_baremetal_allocation.py @@ -39,7 +39,6 @@ def _create_available_node(self): class TestBareMetalAllocation(Base): - min_microversion = '1.52' def test_allocation_create_get_delete(self): @@ -135,7 +134,6 @@ def test_allocation_fields(self): class TestBareMetalAllocationUpdate(Base): - min_microversion = '1.57' def test_allocation_update(self): diff --git a/openstack/tests/functional/baremetal/test_baremetal_chassis.py b/openstack/tests/functional/baremetal/test_baremetal_chassis.py index 53ba687f5..142e1fe93 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_chassis.py +++ b/openstack/tests/functional/baremetal/test_baremetal_chassis.py @@ -72,7 +72,6 @@ def test_chassis_negative_non_existing(self): class TestBareMetalChassisFields(base.BaseBaremetalTest): - min_microversion = '1.8' def test_chassis_fields(self): diff --git a/openstack/tests/functional/baremetal/test_baremetal_conductor.py b/openstack/tests/functional/baremetal/test_baremetal_conductor.py index 310304dee..bd7f64da6 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_conductor.py +++ b/openstack/tests/functional/baremetal/test_baremetal_conductor.py @@ -15,7 +15,6 @@ class TestBareMetalConductor(base.BaseBaremetalTest): - min_microversion = '1.49' def test_list_get_conductor(self): diff --git a/openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py b/openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py index eec646477..565a10bb2 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py +++ b/openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py @@ -15,7 +15,6 @@ class TestBareMetalDeployTemplate(base.BaseBaremetalTest): - min_microversion = '1.55' def setUp(self): diff --git a/openstack/tests/functional/baremetal/test_baremetal_driver.py b/openstack/tests/functional/baremetal/test_baremetal_driver.py index 252ca2073..c182192a3 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_driver.py +++ b/openstack/tests/functional/baremetal/test_baremetal_driver.py @@ -34,7 +34,6 @@ def test_driver_negative_non_existing(self): class TestBareMetalDriverDetails(base.BaseBaremetalTest): - min_microversion = '1.30' def test_fake_hardware_get(self): diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index 03a909064..e6d2269ef 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -327,7 +327,6 @@ def test_maintenance_via_update(self): class TestNodeRetired(base.BaseBaremetalTest): - min_microversion = '1.61' def test_retired(self): @@ -390,7 +389,6 @@ def test_retired_in_available(self): class TestBareMetalNodeFields(base.BaseBaremetalTest): - min_microversion = '1.8' def test_node_fields(self): @@ -404,7 +402,6 @@ def test_node_fields(self): class TestBareMetalVif(base.BaseBaremetalTest): - min_microversion = '1.28' def setUp(self): @@ -445,7 +442,6 @@ def test_node_vif_negative(self): class TestTraits(base.BaseBaremetalTest): - min_microversion = '1.37' def setUp(self): diff --git a/openstack/tests/functional/baremetal/test_baremetal_port.py b/openstack/tests/functional/baremetal/test_baremetal_port.py index 38692d084..cf7831378 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_port.py +++ b/openstack/tests/functional/baremetal/test_baremetal_port.py @@ -132,7 +132,6 @@ def test_port_negative_non_existing(self): class TestBareMetalPortFields(base.BaseBaremetalTest): - min_microversion = '1.8' def test_port_fields(self): diff --git a/openstack/tests/functional/baremetal/test_baremetal_port_group.py b/openstack/tests/functional/baremetal/test_baremetal_port_group.py index b6001156c..b3a60b98e 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_port_group.py +++ b/openstack/tests/functional/baremetal/test_baremetal_port_group.py @@ -16,7 +16,6 @@ class TestBareMetalPortGroup(base.BaseBaremetalTest): - min_microversion = '1.23' def setUp(self): diff --git a/openstack/tests/functional/baremetal/test_baremetal_volume_connector.py b/openstack/tests/functional/baremetal/test_baremetal_volume_connector.py index 4459a0fc8..f8cd86597 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_volume_connector.py +++ b/openstack/tests/functional/baremetal/test_baremetal_volume_connector.py @@ -16,7 +16,6 @@ class TestBareMetalVolumeconnector(base.BaseBaremetalTest): - min_microversion = '1.32' def setUp(self): diff --git a/openstack/tests/functional/baremetal/test_baremetal_volume_target.py b/openstack/tests/functional/baremetal/test_baremetal_volume_target.py index c4395435c..77dc24ee7 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_volume_target.py +++ b/openstack/tests/functional/baremetal/test_baremetal_volume_target.py @@ -16,7 +16,6 @@ class TestBareMetalVolumetarget(base.BaseBaremetalTest): - min_microversion = '1.32' def setUp(self): diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index 3376ed122..3bc4b41aa 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -41,7 +41,6 @@ def _disable_keep_alive(conn): class BaseFunctionalTest(base.TestCase): - _wait_for_timeout_key = '' def setUp(self): diff --git a/openstack/tests/functional/block_storage/v2/base.py b/openstack/tests/functional/block_storage/v2/base.py index 6da35eb40..e69bd4d62 100644 --- a/openstack/tests/functional/block_storage/v2/base.py +++ b/openstack/tests/functional/block_storage/v2/base.py @@ -14,7 +14,6 @@ class BaseBlockStorageTest(base.BaseFunctionalTest): - _wait_for_timeout_key = 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_BLOCK_STORAGE' def setUp(self): diff --git a/openstack/tests/functional/block_storage/v3/base.py b/openstack/tests/functional/block_storage/v3/base.py index d9e60e818..39543c247 100644 --- a/openstack/tests/functional/block_storage/v3/base.py +++ b/openstack/tests/functional/block_storage/v3/base.py @@ -14,7 +14,6 @@ class BaseBlockStorageTest(base.BaseFunctionalTest): - _wait_for_timeout_key = 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_BLOCK_STORAGE' def setUp(self): diff --git a/openstack/tests/functional/cloud/test_devstack.py b/openstack/tests/functional/cloud/test_devstack.py index a6754009a..e5251e942 100644 --- a/openstack/tests/functional/cloud/test_devstack.py +++ b/openstack/tests/functional/cloud/test_devstack.py @@ -26,7 +26,6 @@ class TestDevstack(base.BaseFunctionalTest): - scenarios = [ ('designate', dict(env='DESIGNATE', service='dns')), ('heat', dict(env='HEAT', service='orchestration')), diff --git a/openstack/tests/functional/cloud/test_endpoints.py b/openstack/tests/functional/cloud/test_endpoints.py index d27dd9dda..c7e9e882c 100644 --- a/openstack/tests/functional/cloud/test_endpoints.py +++ b/openstack/tests/functional/cloud/test_endpoints.py @@ -28,7 +28,6 @@ class TestEndpoints(base.KeystoneBaseFunctionalTest): - endpoint_attributes = [ 'id', 'region', diff --git a/openstack/tests/functional/cloud/test_floating_ip.py b/openstack/tests/functional/cloud/test_floating_ip.py index 34e631059..fdcda44ed 100644 --- a/openstack/tests/functional/cloud/test_floating_ip.py +++ b/openstack/tests/functional/cloud/test_floating_ip.py @@ -119,7 +119,6 @@ def _cleanup_servers(self): raise OpenStackCloudException('\n'.join(exception_list)) def _cleanup_ips(self, server): - exception_list = list() fixed_ip = meta.get_server_private_ip(server) diff --git a/openstack/tests/functional/cloud/test_project_cleanup.py b/openstack/tests/functional/cloud/test_project_cleanup.py index 8d661d0ce..b5a9a8c28 100644 --- a/openstack/tests/functional/cloud/test_project_cleanup.py +++ b/openstack/tests/functional/cloud/test_project_cleanup.py @@ -22,7 +22,6 @@ class TestProjectCleanup(base.BaseFunctionalTest): - _wait_for_timeout_key = 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_CLEANUP' def setUp(self): diff --git a/openstack/tests/functional/cloud/test_services.py b/openstack/tests/functional/cloud/test_services.py index 30ba4f5a4..e6c1902de 100644 --- a/openstack/tests/functional/cloud/test_services.py +++ b/openstack/tests/functional/cloud/test_services.py @@ -28,7 +28,6 @@ class TestServices(base.KeystoneBaseFunctionalTest): - service_attributes = ['id', 'name', 'type', 'description'] def setUp(self): diff --git a/openstack/tests/functional/cloud/test_stack.py b/openstack/tests/functional/cloud/test_stack.py index ee2ae9775..62cb13717 100644 --- a/openstack/tests/functional/cloud/test_stack.py +++ b/openstack/tests/functional/cloud/test_stack.py @@ -145,7 +145,6 @@ def test_stack_simple(self): self.assertEqual(12, len(new_rand)) def test_stack_nested(self): - test_template = tempfile.NamedTemporaryFile( suffix='.yaml', delete=False ) diff --git a/openstack/tests/functional/cloud/test_volume.py b/openstack/tests/functional/cloud/test_volume.py index 722f59967..04c2467e9 100644 --- a/openstack/tests/functional/cloud/test_volume.py +++ b/openstack/tests/functional/cloud/test_volume.py @@ -26,7 +26,6 @@ class TestVolume(base.BaseFunctionalTest): - # Creating and deleting volumes is slow TIMEOUT_SCALING_FACTOR = 1.5 diff --git a/openstack/tests/functional/clustering/test_cluster.py b/openstack/tests/functional/clustering/test_cluster.py index 8ce1a17c0..38d2ed144 100644 --- a/openstack/tests/functional/clustering/test_cluster.py +++ b/openstack/tests/functional/clustering/test_cluster.py @@ -18,7 +18,6 @@ class TestCluster(base.BaseFunctionalTest): - _wait_for_timeout_key = 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_CLUSTER' def setUp(self): diff --git a/openstack/tests/functional/compute/base.py b/openstack/tests/functional/compute/base.py index bfeabd521..844c30a1e 100644 --- a/openstack/tests/functional/compute/base.py +++ b/openstack/tests/functional/compute/base.py @@ -14,5 +14,4 @@ class BaseComputeTest(base.BaseFunctionalTest): - _wait_for_timeout_key = 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_COMPUTE' diff --git a/openstack/tests/functional/image/v2/base.py b/openstack/tests/functional/image/v2/base.py index f9762d3e4..290ee0a46 100644 --- a/openstack/tests/functional/image/v2/base.py +++ b/openstack/tests/functional/image/v2/base.py @@ -14,7 +14,6 @@ class BaseImageTest(base.BaseFunctionalTest): - _wait_for_timeout_key = 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_IMAGE' def setUp(self): diff --git a/openstack/tests/functional/image/v2/test_metadef_namespace.py b/openstack/tests/functional/image/v2/test_metadef_namespace.py index 90c2d7b20..8fb9a83d4 100644 --- a/openstack/tests/functional/image/v2/test_metadef_namespace.py +++ b/openstack/tests/functional/image/v2/test_metadef_namespace.py @@ -15,7 +15,6 @@ class TestMetadefNamespace(base.BaseImageTest): - # TODO(stephenfin): We should use setUpClass here for MOAR SPEED!!! def setUp(self): super().setUp() diff --git a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py index f9f819a98..efa87058a 100644 --- a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py +++ b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py @@ -26,7 +26,6 @@ class TestLoadBalancer(base.BaseFunctionalTest): - HM_ID = None L7POLICY_ID = None LB_ID = None diff --git a/openstack/tests/functional/network/v2/test_address_group.py b/openstack/tests/functional/network/v2/test_address_group.py index 933e86834..5b50ec250 100644 --- a/openstack/tests/functional/network/v2/test_address_group.py +++ b/openstack/tests/functional/network/v2/test_address_group.py @@ -16,7 +16,6 @@ class TestAddressGroup(base.BaseFunctionalTest): - ADDRESS_GROUP_ID = None ADDRESSES = ["10.0.0.1/32", "2001:db8::/32"] diff --git a/openstack/tests/functional/network/v2/test_address_scope.py b/openstack/tests/functional/network/v2/test_address_scope.py index 76ac60611..9b9340382 100644 --- a/openstack/tests/functional/network/v2/test_address_scope.py +++ b/openstack/tests/functional/network/v2/test_address_scope.py @@ -16,7 +16,6 @@ class TestAddressScope(base.BaseFunctionalTest): - ADDRESS_SCOPE_ID = None IS_SHARED = False IP_VERSION = 4 diff --git a/openstack/tests/functional/network/v2/test_agent.py b/openstack/tests/functional/network/v2/test_agent.py index 3ef3c89ad..dcb8523ec 100644 --- a/openstack/tests/functional/network/v2/test_agent.py +++ b/openstack/tests/functional/network/v2/test_agent.py @@ -17,7 +17,6 @@ class TestAgent(base.BaseFunctionalTest): - AGENT = None DESC = "test description" diff --git a/openstack/tests/functional/network/v2/test_agent_add_remove_network.py b/openstack/tests/functional/network/v2/test_agent_add_remove_network.py index 723e227f2..8cd6d901a 100644 --- a/openstack/tests/functional/network/v2/test_agent_add_remove_network.py +++ b/openstack/tests/functional/network/v2/test_agent_add_remove_network.py @@ -16,7 +16,6 @@ class TestAgentNetworks(base.BaseFunctionalTest): - NETWORK_ID = None AGENT = None AGENT_ID = None diff --git a/openstack/tests/functional/network/v2/test_agent_add_remove_router.py b/openstack/tests/functional/network/v2/test_agent_add_remove_router.py index 20313a67a..7a3b305b4 100644 --- a/openstack/tests/functional/network/v2/test_agent_add_remove_router.py +++ b/openstack/tests/functional/network/v2/test_agent_add_remove_router.py @@ -16,7 +16,6 @@ class TestAgentRouters(base.BaseFunctionalTest): - ROUTER = None AGENT = None diff --git a/openstack/tests/functional/network/v2/test_auto_allocated_topology.py b/openstack/tests/functional/network/v2/test_auto_allocated_topology.py index 73c070bfb..6e20b44bd 100644 --- a/openstack/tests/functional/network/v2/test_auto_allocated_topology.py +++ b/openstack/tests/functional/network/v2/test_auto_allocated_topology.py @@ -14,7 +14,6 @@ class TestAutoAllocatedTopology(base.BaseFunctionalTest): - NETWORK_NAME = "auto_allocated_network" NETWORK_ID = None PROJECT_ID = None diff --git a/openstack/tests/functional/network/v2/test_availability_zone.py b/openstack/tests/functional/network/v2/test_availability_zone.py index 255b38990..6828c7c0d 100644 --- a/openstack/tests/functional/network/v2/test_availability_zone.py +++ b/openstack/tests/functional/network/v2/test_availability_zone.py @@ -18,7 +18,6 @@ class TestAvailabilityZone(base.BaseFunctionalTest): def test_list(self): availability_zones = list(self.user_cloud.network.availability_zones()) if len(availability_zones) > 0: - for az in availability_zones: self.assertIsInstance(az.name, str) self.assertIsInstance(az.resource, str) diff --git a/openstack/tests/functional/network/v2/test_dvr_router.py b/openstack/tests/functional/network/v2/test_dvr_router.py index db595ab89..81a1a3585 100644 --- a/openstack/tests/functional/network/v2/test_dvr_router.py +++ b/openstack/tests/functional/network/v2/test_dvr_router.py @@ -16,7 +16,6 @@ class TestDVRRouter(base.BaseFunctionalTest): - ID = None def setUp(self): diff --git a/openstack/tests/functional/network/v2/test_firewall_group.py b/openstack/tests/functional/network/v2/test_firewall_group.py index 17daa0e71..83f314999 100644 --- a/openstack/tests/functional/network/v2/test_firewall_group.py +++ b/openstack/tests/functional/network/v2/test_firewall_group.py @@ -19,7 +19,6 @@ class TestFirewallGroup(base.BaseFunctionalTest): - ID = None def setUp(self): diff --git a/openstack/tests/functional/network/v2/test_firewall_policy.py b/openstack/tests/functional/network/v2/test_firewall_policy.py index 6bb134db2..efb6670c7 100644 --- a/openstack/tests/functional/network/v2/test_firewall_policy.py +++ b/openstack/tests/functional/network/v2/test_firewall_policy.py @@ -19,7 +19,6 @@ class TestFirewallPolicy(base.BaseFunctionalTest): - ID = None def setUp(self): diff --git a/openstack/tests/functional/network/v2/test_firewall_rule.py b/openstack/tests/functional/network/v2/test_firewall_rule.py index 185fc6cdf..477233d1b 100644 --- a/openstack/tests/functional/network/v2/test_firewall_rule.py +++ b/openstack/tests/functional/network/v2/test_firewall_rule.py @@ -19,7 +19,6 @@ class TestFirewallRule(base.BaseFunctionalTest): - ACTION = "allow" DEST_IP = "10.0.0.0/24" DEST_PORT = "80" diff --git a/openstack/tests/functional/network/v2/test_firewall_rule_insert_remove_policy.py b/openstack/tests/functional/network/v2/test_firewall_rule_insert_remove_policy.py index 8be7e3508..22b8bfdd7 100644 --- a/openstack/tests/functional/network/v2/test_firewall_rule_insert_remove_policy.py +++ b/openstack/tests/functional/network/v2/test_firewall_rule_insert_remove_policy.py @@ -21,7 +21,6 @@ class TestFirewallPolicyRuleAssociations(base.BaseFunctionalTest): - POLICY_NAME = uuid.uuid4().hex RULE1_NAME = uuid.uuid4().hex RULE2_NAME = uuid.uuid4().hex diff --git a/openstack/tests/functional/network/v2/test_flavor.py b/openstack/tests/functional/network/v2/test_flavor.py index c0106eb59..f67f082a4 100644 --- a/openstack/tests/functional/network/v2/test_flavor.py +++ b/openstack/tests/functional/network/v2/test_flavor.py @@ -15,7 +15,6 @@ class TestFlavor(base.BaseFunctionalTest): - UPDATE_NAME = "UPDATED-NAME" SERVICE_TYPE = "FLAVORS" ID = None diff --git a/openstack/tests/functional/network/v2/test_floating_ip.py b/openstack/tests/functional/network/v2/test_floating_ip.py index df73aa02c..e090e7c0f 100644 --- a/openstack/tests/functional/network/v2/test_floating_ip.py +++ b/openstack/tests/functional/network/v2/test_floating_ip.py @@ -20,7 +20,6 @@ class TestFloatingIP(base.BaseFunctionalTest): - IPV4 = 4 EXT_CIDR = "10.100.0.0/24" INT_CIDR = "10.101.0.0/24" diff --git a/openstack/tests/functional/network/v2/test_l3_conntrack_helper.py b/openstack/tests/functional/network/v2/test_l3_conntrack_helper.py index a9e0315d3..66bc90217 100644 --- a/openstack/tests/functional/network/v2/test_l3_conntrack_helper.py +++ b/openstack/tests/functional/network/v2/test_l3_conntrack_helper.py @@ -17,7 +17,6 @@ class TestL3ConntrackHelper(base.BaseFunctionalTest): - PROTOCOL = "udp" HELPER = "tftp" PORT = 69 diff --git a/openstack/tests/functional/network/v2/test_local_ip.py b/openstack/tests/functional/network/v2/test_local_ip.py index 0a21c9a7d..085bff320 100644 --- a/openstack/tests/functional/network/v2/test_local_ip.py +++ b/openstack/tests/functional/network/v2/test_local_ip.py @@ -18,7 +18,6 @@ class TestLocalIP(base.BaseFunctionalTest): - LOCAL_IP_ID = None def setUp(self): diff --git a/openstack/tests/functional/network/v2/test_ndp_proxy.py b/openstack/tests/functional/network/v2/test_ndp_proxy.py index 300e31c58..35e78d968 100644 --- a/openstack/tests/functional/network/v2/test_ndp_proxy.py +++ b/openstack/tests/functional/network/v2/test_ndp_proxy.py @@ -19,7 +19,6 @@ class TestNDPProxy(base.BaseFunctionalTest): - IPV6 = 6 EXT_CIDR = "2002::1:0/112" INT_CIDR = "2002::2:0/112" diff --git a/openstack/tests/functional/network/v2/test_network.py b/openstack/tests/functional/network/v2/test_network.py index 520eb761c..d857fa33d 100644 --- a/openstack/tests/functional/network/v2/test_network.py +++ b/openstack/tests/functional/network/v2/test_network.py @@ -36,7 +36,6 @@ def delete_network(conn, network, subnet): class TestNetwork(base.BaseFunctionalTest): - ID = None def setUp(self): diff --git a/openstack/tests/functional/network/v2/test_network_ip_availability.py b/openstack/tests/functional/network/v2/test_network_ip_availability.py index e066be774..3f8bc21d6 100644 --- a/openstack/tests/functional/network/v2/test_network_ip_availability.py +++ b/openstack/tests/functional/network/v2/test_network_ip_availability.py @@ -18,7 +18,6 @@ class TestNetworkIPAvailability(base.BaseFunctionalTest): - IPV4 = 4 CIDR = "10.100.0.0/24" NET_ID = None diff --git a/openstack/tests/functional/network/v2/test_network_segment_range.py b/openstack/tests/functional/network/v2/test_network_segment_range.py index 314aa6f0f..69212797a 100644 --- a/openstack/tests/functional/network/v2/test_network_segment_range.py +++ b/openstack/tests/functional/network/v2/test_network_segment_range.py @@ -18,7 +18,6 @@ class TestNetworkSegmentRange(base.BaseFunctionalTest): - NETWORK_SEGMENT_RANGE_ID = None NAME = "test_name" DEFAULT = False diff --git a/openstack/tests/functional/network/v2/test_port.py b/openstack/tests/functional/network/v2/test_port.py index 12a281c26..a0028ae25 100644 --- a/openstack/tests/functional/network/v2/test_port.py +++ b/openstack/tests/functional/network/v2/test_port.py @@ -18,7 +18,6 @@ class TestPort(base.BaseFunctionalTest): - IPV4 = 4 CIDR = "10.100.0.0/24" NET_ID = None diff --git a/openstack/tests/functional/network/v2/test_port_forwarding.py b/openstack/tests/functional/network/v2/test_port_forwarding.py index 3afe3f9b5..dbeaaeafe 100644 --- a/openstack/tests/functional/network/v2/test_port_forwarding.py +++ b/openstack/tests/functional/network/v2/test_port_forwarding.py @@ -21,7 +21,6 @@ class TestPortForwarding(base.BaseFunctionalTest): - IPV4 = 4 FIP_ID = None EXT_CIDR = "10.100.0.0/24" diff --git a/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py b/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py index 3f0e43c1c..ce7c8150b 100644 --- a/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py @@ -18,7 +18,6 @@ class TestQoSBandwidthLimitRule(base.BaseFunctionalTest): - QOS_POLICY_ID = None QOS_IS_SHARED = False QOS_POLICY_DESCRIPTION = "QoS policy description" diff --git a/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py b/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py index ac4849774..f9f21a2d4 100644 --- a/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py +++ b/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py @@ -18,7 +18,6 @@ class TestQoSDSCPMarkingRule(base.BaseFunctionalTest): - QOS_POLICY_ID = None QOS_IS_SHARED = False QOS_POLICY_DESCRIPTION = "QoS policy description" diff --git a/openstack/tests/functional/network/v2/test_qos_minimum_bandwidth_rule.py b/openstack/tests/functional/network/v2/test_qos_minimum_bandwidth_rule.py index d293e9f2b..4e3278292 100644 --- a/openstack/tests/functional/network/v2/test_qos_minimum_bandwidth_rule.py +++ b/openstack/tests/functional/network/v2/test_qos_minimum_bandwidth_rule.py @@ -18,7 +18,6 @@ class TestQoSMinimumBandwidthRule(base.BaseFunctionalTest): - QOS_POLICY_ID = None QOS_IS_SHARED = False QOS_POLICY_DESCRIPTION = "QoS policy description" diff --git a/openstack/tests/functional/network/v2/test_qos_minimum_packet_rate_rule.py b/openstack/tests/functional/network/v2/test_qos_minimum_packet_rate_rule.py index 0e35f2681..40088152b 100644 --- a/openstack/tests/functional/network/v2/test_qos_minimum_packet_rate_rule.py +++ b/openstack/tests/functional/network/v2/test_qos_minimum_packet_rate_rule.py @@ -18,7 +18,6 @@ class TestQoSMinimumPacketRateRule(base.BaseFunctionalTest): - QOS_POLICY_ID = None QOS_IS_SHARED = False QOS_POLICY_DESCRIPTION = "QoS policy description" diff --git a/openstack/tests/functional/network/v2/test_qos_policy.py b/openstack/tests/functional/network/v2/test_qos_policy.py index 734130cbf..acd9c4cf1 100644 --- a/openstack/tests/functional/network/v2/test_qos_policy.py +++ b/openstack/tests/functional/network/v2/test_qos_policy.py @@ -16,7 +16,6 @@ class TestQoSPolicy(base.BaseFunctionalTest): - QOS_POLICY_ID = None IS_SHARED = False IS_DEFAULT = False diff --git a/openstack/tests/functional/network/v2/test_qos_rule_type.py b/openstack/tests/functional/network/v2/test_qos_rule_type.py index 3f44c9e7a..162b0e728 100644 --- a/openstack/tests/functional/network/v2/test_qos_rule_type.py +++ b/openstack/tests/functional/network/v2/test_qos_rule_type.py @@ -15,7 +15,6 @@ class TestQoSRuleType(base.BaseFunctionalTest): - QOS_RULE_TYPE = "bandwidth_limit" def setUp(self): diff --git a/openstack/tests/functional/network/v2/test_rbac_policy.py b/openstack/tests/functional/network/v2/test_rbac_policy.py index 70db50ae9..b96ca7b30 100644 --- a/openstack/tests/functional/network/v2/test_rbac_policy.py +++ b/openstack/tests/functional/network/v2/test_rbac_policy.py @@ -17,7 +17,6 @@ class TestRBACPolicy(base.BaseFunctionalTest): - ACTION = "access_as_shared" OBJ_TYPE = "network" TARGET_TENANT_ID = "*" diff --git a/openstack/tests/functional/network/v2/test_router.py b/openstack/tests/functional/network/v2/test_router.py index bb5e71496..5657edddd 100644 --- a/openstack/tests/functional/network/v2/test_router.py +++ b/openstack/tests/functional/network/v2/test_router.py @@ -16,7 +16,6 @@ class TestRouter(base.BaseFunctionalTest): - ID = None def setUp(self): diff --git a/openstack/tests/functional/network/v2/test_router_add_remove_interface.py b/openstack/tests/functional/network/v2/test_router_add_remove_interface.py index 9cd10553f..c59cc0e8d 100644 --- a/openstack/tests/functional/network/v2/test_router_add_remove_interface.py +++ b/openstack/tests/functional/network/v2/test_router_add_remove_interface.py @@ -18,7 +18,6 @@ class TestRouterInterface(base.BaseFunctionalTest): - CIDR = "10.100.0.0/16" IPV4 = 4 ROUTER_ID = None diff --git a/openstack/tests/functional/network/v2/test_security_group.py b/openstack/tests/functional/network/v2/test_security_group.py index 93f997e31..ddc1d08e0 100644 --- a/openstack/tests/functional/network/v2/test_security_group.py +++ b/openstack/tests/functional/network/v2/test_security_group.py @@ -16,7 +16,6 @@ class TestSecurityGroup(base.BaseFunctionalTest): - ID = None def setUp(self): diff --git a/openstack/tests/functional/network/v2/test_security_group_rule.py b/openstack/tests/functional/network/v2/test_security_group_rule.py index e2a7229a1..e5bd4a953 100644 --- a/openstack/tests/functional/network/v2/test_security_group_rule.py +++ b/openstack/tests/functional/network/v2/test_security_group_rule.py @@ -17,7 +17,6 @@ class TestSecurityGroupRule(base.BaseFunctionalTest): - IPV4 = "IPv4" PROTO = "tcp" PORT = 22 diff --git a/openstack/tests/functional/network/v2/test_segment.py b/openstack/tests/functional/network/v2/test_segment.py index 723a9c90b..07d9f6edc 100644 --- a/openstack/tests/functional/network/v2/test_segment.py +++ b/openstack/tests/functional/network/v2/test_segment.py @@ -17,7 +17,6 @@ class TestSegment(base.BaseFunctionalTest): - NETWORK_TYPE = None PHYSICAL_NETWORK = None SEGMENTATION_ID = None diff --git a/openstack/tests/functional/network/v2/test_service_profile.py b/openstack/tests/functional/network/v2/test_service_profile.py index 86c1e8fb3..d8c50c124 100644 --- a/openstack/tests/functional/network/v2/test_service_profile.py +++ b/openstack/tests/functional/network/v2/test_service_profile.py @@ -15,7 +15,6 @@ class TestServiceProfile(base.BaseFunctionalTest): - SERVICE_PROFILE_DESCRIPTION = "DESCRIPTION" UPDATE_DESCRIPTION = "UPDATED-DESCRIPTION" METAINFO = "FlAVOR_PROFILE_METAINFO" diff --git a/openstack/tests/functional/network/v2/test_subnet.py b/openstack/tests/functional/network/v2/test_subnet.py index 2bafd40d2..1fe35a5ba 100644 --- a/openstack/tests/functional/network/v2/test_subnet.py +++ b/openstack/tests/functional/network/v2/test_subnet.py @@ -17,7 +17,6 @@ class TestSubnet(base.BaseFunctionalTest): - IPV4 = 4 CIDR = "10.100.0.0/24" DNS_SERVERS = ["8.8.4.4", "8.8.8.8"] diff --git a/openstack/tests/functional/network/v2/test_subnet_from_subnet_pool.py b/openstack/tests/functional/network/v2/test_subnet_from_subnet_pool.py index 00f23597b..c31dfdbcf 100644 --- a/openstack/tests/functional/network/v2/test_subnet_from_subnet_pool.py +++ b/openstack/tests/functional/network/v2/test_subnet_from_subnet_pool.py @@ -18,7 +18,6 @@ class TestSubnetFromSubnetPool(base.BaseFunctionalTest): - IPV4 = 4 CIDR = "10.100.0.0/28" MINIMUM_PREFIX_LENGTH = 8 diff --git a/openstack/tests/functional/network/v2/test_subnet_pool.py b/openstack/tests/functional/network/v2/test_subnet_pool.py index 15848eaa9..8daa8db9b 100644 --- a/openstack/tests/functional/network/v2/test_subnet_pool.py +++ b/openstack/tests/functional/network/v2/test_subnet_pool.py @@ -16,7 +16,6 @@ class TestSubnetPool(base.BaseFunctionalTest): - SUBNET_POOL_ID = None MINIMUM_PREFIX_LENGTH = 8 DEFAULT_PREFIX_LENGTH = 24 diff --git a/openstack/tests/functional/network/v2/test_trunk.py b/openstack/tests/functional/network/v2/test_trunk.py index b0d29348b..7107e1ddf 100644 --- a/openstack/tests/functional/network/v2/test_trunk.py +++ b/openstack/tests/functional/network/v2/test_trunk.py @@ -18,7 +18,6 @@ class TestTrunk(base.BaseFunctionalTest): - TIMEOUT_SCALING_FACTOR = 2.0 def setUp(self): diff --git a/openstack/tests/functional/network/v2/test_vpnaas.py b/openstack/tests/functional/network/v2/test_vpnaas.py index bfd89169c..53be0bbfc 100644 --- a/openstack/tests/functional/network/v2/test_vpnaas.py +++ b/openstack/tests/functional/network/v2/test_vpnaas.py @@ -15,7 +15,6 @@ class TestVpnIkePolicy(base.BaseFunctionalTest): - ID = None def setUp(self): diff --git a/openstack/tests/functional/object_store/v1/test_obj.py b/openstack/tests/functional/object_store/v1/test_obj.py index 7012325b5..a8d48c356 100644 --- a/openstack/tests/functional/object_store/v1/test_obj.py +++ b/openstack/tests/functional/object_store/v1/test_obj.py @@ -14,7 +14,6 @@ class TestObject(base.BaseFunctionalTest): - DATA = b'abc' def setUp(self): diff --git a/openstack/tests/functional/orchestration/v1/test_stack.py b/openstack/tests/functional/orchestration/v1/test_stack.py index 9b5b14f04..4abb3a556 100644 --- a/openstack/tests/functional/orchestration/v1/test_stack.py +++ b/openstack/tests/functional/orchestration/v1/test_stack.py @@ -19,7 +19,6 @@ class TestStack(base.BaseFunctionalTest): - NAME = 'test_stack' stack = None network = None diff --git a/openstack/tests/functional/shared_file_system/base.py b/openstack/tests/functional/shared_file_system/base.py index c9ab00a2d..f319c7368 100644 --- a/openstack/tests/functional/shared_file_system/base.py +++ b/openstack/tests/functional/shared_file_system/base.py @@ -15,7 +15,6 @@ class BaseSharedFileSystemTest(base.BaseFunctionalTest): - min_microversion = None def setUp(self): diff --git a/openstack/tests/functional/shared_file_system/test_availability_zone.py b/openstack/tests/functional/shared_file_system/test_availability_zone.py index 045e5bd31..07f694d47 100644 --- a/openstack/tests/functional/shared_file_system/test_availability_zone.py +++ b/openstack/tests/functional/shared_file_system/test_availability_zone.py @@ -14,7 +14,6 @@ class AvailabilityZoneTest(base.BaseSharedFileSystemTest): - min_microversion = '2.7' def test_availability_zones(self): diff --git a/openstack/tests/functional/shared_file_system/test_export_locations.py b/openstack/tests/functional/shared_file_system/test_export_locations.py index 9db3c003e..ee801f1e0 100644 --- a/openstack/tests/functional/shared_file_system/test_export_locations.py +++ b/openstack/tests/functional/shared_file_system/test_export_locations.py @@ -14,7 +14,6 @@ class TestExportLocation(base.BaseSharedFileSystemTest): - min_microversion = '2.9' def setUp(self): diff --git a/openstack/tests/functional/shared_file_system/test_share_instance.py b/openstack/tests/functional/shared_file_system/test_share_instance.py index 01ada1eec..7ecf37cd0 100644 --- a/openstack/tests/functional/shared_file_system/test_share_instance.py +++ b/openstack/tests/functional/shared_file_system/test_share_instance.py @@ -17,7 +17,6 @@ class ShareInstanceTest(base.BaseSharedFileSystemTest): - min_microversion = '2.7' def setUp(self): diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 45cd73503..36f067c10 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -83,7 +83,6 @@ class TestCase(base.TestCase): - strict_cloud = False def setUp(self, cloud_config_fixture='clouds.yaml'): @@ -334,7 +333,6 @@ def _get_group_data(self, name=None, domain_id=None, description=None): ) def _get_user_data(self, name=None, password=None, **kwargs): - name = name or self.getUniqueString('username') password = password or self.getUniqueString('user_password') user_id = uuid.uuid4().hex @@ -864,7 +862,7 @@ def assert_no_calls(self): self.assertEqual(2, len(self.adapter.request_history)) def assert_calls(self, stop_after=None, do_count=True): - for (x, (call, history)) in enumerate( + for x, (call, history) in enumerate( zip(self.calls, self.adapter.request_history) ): if stop_after and x > stop_after: diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py index a1fd697f8..d137d15ab 100644 --- a/openstack/tests/unit/cloud/test_caching.py +++ b/openstack/tests/unit/cloud/test_caching.py @@ -574,7 +574,6 @@ def test_list_flavors(self): self.assert_calls() def test_list_images(self): - self.use_glance() fake_image = fakes.make_fake_image(image_id='42') @@ -715,7 +714,6 @@ def test_list_ports_filtered(self): class TestCacheIgnoresQueuedStatus(base.TestCase): - scenarios = [ ('queued', dict(status='queued')), ('saving', dict(status='saving')), @@ -745,7 +743,6 @@ def _compare_images(self, exp, real): ) def test_list_images_ignores_pending_status(self): - self.register_uris( [ dict( @@ -777,7 +774,6 @@ def test_list_images_ignores_pending_status(self): class TestCacheSteadyStatus(base.TestCase): - scenarios = [ ('active', dict(status='active')), ('killed', dict(status='killed')), @@ -801,7 +797,6 @@ def _compare_images(self, exp, real): ) def test_list_images_caches_steady_status(self): - self.register_uris( [ dict( diff --git a/openstack/tests/unit/cloud/test_cluster_templates.py b/openstack/tests/unit/cloud/test_cluster_templates.py index bddf07fc8..a1c5a4c63 100644 --- a/openstack/tests/unit/cloud/test_cluster_templates.py +++ b/openstack/tests/unit/cloud/test_cluster_templates.py @@ -70,7 +70,6 @@ def get_mock_url( ) def test_list_cluster_templates_without_detail(self): - self.register_uris( [ dict( @@ -124,7 +123,6 @@ def test_search_cluster_templates_by_name(self): self.assert_calls() def test_search_cluster_templates_not_found(self): - self.register_uris( [ dict( diff --git a/openstack/tests/unit/cloud/test_flavors.py b/openstack/tests/unit/cloud/test_flavors.py index 98e5c4efd..b6026110a 100644 --- a/openstack/tests/unit/cloud/test_flavors.py +++ b/openstack/tests/unit/cloud/test_flavors.py @@ -22,7 +22,6 @@ def setUp(self): # self.use_compute_discovery() def test_create_flavor(self): - self.use_compute_discovery() self.register_uris( [ diff --git a/openstack/tests/unit/cloud/test_floating_ip_neutron.py b/openstack/tests/unit/cloud/test_floating_ip_neutron.py index 9f85c9fab..3d3ae2479 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_neutron.py +++ b/openstack/tests/unit/cloud/test_floating_ip_neutron.py @@ -170,7 +170,6 @@ def test_list_floating_ips(self): self.assert_calls() def test_list_floating_ips_with_filters(self): - self.register_uris( [ dict( diff --git a/openstack/tests/unit/cloud/test_floating_ip_nova.py b/openstack/tests/unit/cloud/test_floating_ip_nova.py index 406d504f6..2c34c3d97 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_nova.py +++ b/openstack/tests/unit/cloud/test_floating_ip_nova.py @@ -88,7 +88,6 @@ def setUp(self): self.cloud.has_service = get_fake_has_service(self.cloud.has_service) def test_list_floating_ips(self): - self.register_uris( [ dict( @@ -278,7 +277,6 @@ def test_available_floating_ip_new(self): self.assert_calls() def test_delete_floating_ip_existing(self): - self.register_uris( [ dict( diff --git a/openstack/tests/unit/cloud/test_floating_ip_pool.py b/openstack/tests/unit/cloud/test_floating_ip_pool.py index 6c4d87b0e..d2dd2485a 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_pool.py +++ b/openstack/tests/unit/cloud/test_floating_ip_pool.py @@ -28,7 +28,6 @@ class TestFloatingIPPool(base.TestCase): pools = [{'name': 'public'}] def test_list_floating_ip_pools(self): - self.register_uris( [ dict( @@ -66,7 +65,6 @@ def test_list_floating_ip_pools(self): self.assert_calls() def test_list_floating_ip_pools_exception(self): - self.register_uris( [ dict( diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 8e8b4b733..eff949fd1 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -1930,7 +1930,6 @@ def setUp(self): self.use_glance(image_version_json='image-version-v1.json') def test_config_v1(self): - self.cloud.config.config['image_api_version'] = '1' # We override the scheme of the endpoint with the scheme of the service # because glance has a bug where it doesn't return https properly. @@ -2032,7 +2031,6 @@ def test_create_image_volume(self): self.assert_calls() def test_create_image_volume_duplicate(self): - self.register_uris( [ self.get_cinder_discovery_mock_dict(), diff --git a/openstack/tests/unit/cloud/test_meta.py b/openstack/tests/unit/cloud/test_meta.py index 56d95648d..7a03c3a4c 100644 --- a/openstack/tests/unit/cloud/test_meta.py +++ b/openstack/tests/unit/cloud/test_meta.py @@ -451,7 +451,6 @@ def test_get_server_private_ip_devstack( mock_get_volumes, mock_has_service, ): - mock_get_image_name.return_value = 'cirros-0.3.4-x86_64-uec' mock_get_flavor_name.return_value = 'm1.tiny' mock_get_volumes.return_value = [] diff --git a/openstack/tests/unit/cloud/test_network.py b/openstack/tests/unit/cloud/test_network.py index 860efc3a1..717697f09 100644 --- a/openstack/tests/unit/cloud/test_network.py +++ b/openstack/tests/unit/cloud/test_network.py @@ -21,7 +21,6 @@ class TestNetwork(base.TestCase): - mock_new_network_rep = { 'provider:physical_network': None, 'ipv6_address_scope': None, diff --git a/openstack/tests/unit/cloud/test_object.py b/openstack/tests/unit/cloud/test_object.py index 53461dc3c..054108752 100644 --- a/openstack/tests/unit/cloud/test_object.py +++ b/openstack/tests/unit/cloud/test_object.py @@ -375,7 +375,6 @@ def test_list_containers_exception(self): @mock.patch('time.time', autospec=True) def test_generate_form_signature_container_key(self, mock_time): - mock_time.return_value = 12345 self.register_uris( @@ -414,7 +413,6 @@ def test_generate_form_signature_container_key(self, mock_time): @mock.patch('time.time', autospec=True) def test_generate_form_signature_account_key(self, mock_time): - mock_time.return_value = 12345 self.register_uris( @@ -459,7 +457,6 @@ def test_generate_form_signature_account_key(self, mock_time): @mock.patch('time.time') def test_generate_form_signature_key_argument(self, mock_time): - mock_time.return_value = 12345 self.assertEqual( @@ -477,7 +474,6 @@ def test_generate_form_signature_key_argument(self, mock_time): self.assert_calls() def test_generate_form_signature_no_key(self): - self.register_uris( [ dict( @@ -512,7 +508,6 @@ def test_generate_form_signature_no_key(self): self.assert_calls() def test_set_account_temp_url_key(self): - key = 'super-secure-key' self.register_uris( @@ -536,7 +531,6 @@ def test_set_account_temp_url_key(self): self.assert_calls() def test_set_account_temp_url_key_secondary(self): - key = 'super-secure-key' self.register_uris( @@ -560,7 +554,6 @@ def test_set_account_temp_url_key_secondary(self): self.assert_calls() def test_set_container_temp_url_key(self): - key = 'super-secure-key' self.register_uris( @@ -584,7 +577,6 @@ def test_set_container_temp_url_key(self): self.assert_calls() def test_set_container_temp_url_key_secondary(self): - key = 'super-secure-key' self.register_uris( @@ -907,7 +899,6 @@ def setUp(self): self.endpoint = self.cloud._object_store_client.get_endpoint() def test_create_object(self): - self.register_uris( [ dict( @@ -954,7 +945,6 @@ def test_create_object(self): self.assert_calls() def test_create_object_index_rax(self): - self.register_uris( [ dict( @@ -986,7 +976,6 @@ def test_create_object_index_rax(self): self.assert_calls() def test_create_directory_marker_object(self): - self.register_uris( [ dict( @@ -1013,7 +1002,6 @@ def test_create_directory_marker_object(self): self.assert_calls() def test_create_dynamic_large_object(self): - max_file_size = 2 min_file_size = 1 @@ -1094,7 +1082,6 @@ def test_create_dynamic_large_object(self): ) def test_create_static_large_object(self): - max_file_size = 25 min_file_size = 1 @@ -1542,7 +1529,6 @@ def test_slo_manifest_fail(self): self.assert_calls(stop_after=3) def test_object_segment_retry_failure(self): - max_file_size = 25 min_file_size = 1 @@ -1626,7 +1612,6 @@ def test_object_segment_retry_failure(self): self.assert_calls(stop_after=3) def test_object_segment_retries(self): - max_file_size = 25 min_file_size = 1 @@ -1773,7 +1758,6 @@ def test_object_segment_retries(self): ) def test_create_object_skip_checksum(self): - self.register_uris( [ dict( @@ -1816,7 +1800,6 @@ def test_create_object_skip_checksum(self): self.assert_calls() def test_create_object_data(self): - self.register_uris( [ dict( diff --git a/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py b/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py index 468f4abf7..01b0ca719 100644 --- a/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py @@ -21,7 +21,6 @@ class TestQosBandwidthLimitRule(base.TestCase): - policy_name = 'qos test policy' policy_id = '881d1bb7-a663-44c0-8f9f-ee2765b74486' project_id = 'c88fc89f-5121-4a4c-87fd-496b5af864e9' diff --git a/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py b/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py index 1b864b1a6..828356822 100644 --- a/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py +++ b/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py @@ -21,7 +21,6 @@ class TestQosDscpMarkingRule(base.TestCase): - policy_name = 'qos test policy' policy_id = '881d1bb7-a663-44c0-8f9f-ee2765b74486' project_id = 'c88fc89f-5121-4a4c-87fd-496b5af864e9' diff --git a/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py b/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py index 411d6d18f..a08ae90b4 100644 --- a/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py +++ b/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py @@ -21,7 +21,6 @@ class TestQosMinimumBandwidthRule(base.TestCase): - policy_name = 'qos test policy' policy_id = '881d1bb7-a663-44c0-8f9f-ee2765b74486' project_id = 'c88fc89f-5121-4a4c-87fd-496b5af864e9' diff --git a/openstack/tests/unit/cloud/test_qos_policy.py b/openstack/tests/unit/cloud/test_qos_policy.py index 7bfd77334..fe0ed44ba 100644 --- a/openstack/tests/unit/cloud/test_qos_policy.py +++ b/openstack/tests/unit/cloud/test_qos_policy.py @@ -21,7 +21,6 @@ class TestQosPolicy(base.TestCase): - policy_name = 'qos test policy' policy_id = '881d1bb7-a663-44c0-8f9f-ee2765b74486' project_id = 'c88fc89f-5121-4a4c-87fd-496b5af864e9' diff --git a/openstack/tests/unit/cloud/test_qos_rule_type.py b/openstack/tests/unit/cloud/test_qos_rule_type.py index 878e162f1..0c2e1d241 100644 --- a/openstack/tests/unit/cloud/test_qos_rule_type.py +++ b/openstack/tests/unit/cloud/test_qos_rule_type.py @@ -19,7 +19,6 @@ class TestQosRuleType(base.TestCase): - rule_type_name = "bandwidth_limit" qos_extension = { diff --git a/openstack/tests/unit/cloud/test_router.py b/openstack/tests/unit/cloud/test_router.py index a23ec6acc..8ab57ac97 100644 --- a/openstack/tests/unit/cloud/test_router.py +++ b/openstack/tests/unit/cloud/test_router.py @@ -24,7 +24,6 @@ class TestRouter(base.TestCase): - router_name = 'goofy' router_id = '57076620-dcfb-42ed-8ad6-79ccb4a79ed2' subnet_id = '1f1696eb-7f47-47f6-835c-4889bff88604' diff --git a/openstack/tests/unit/cloud/test_security_groups.py b/openstack/tests/unit/cloud/test_security_groups.py index c76ca430b..ff010bf39 100644 --- a/openstack/tests/unit/cloud/test_security_groups.py +++ b/openstack/tests/unit/cloud/test_security_groups.py @@ -101,7 +101,6 @@ def test_list_security_groups_nova(self): self.assert_calls() def test_list_security_groups_none(self): - self.cloud.secgroup_source = None self.has_neutron = False self.assertRaises( @@ -625,7 +624,6 @@ def test_create_security_group_rule_nova(self): self.assert_calls() def test_create_security_group_rule_nova_no_ports(self): - self.has_neutron = False self.cloud.secgroup_source = 'nova' @@ -836,7 +834,6 @@ def test_list_server_security_groups_bad_source(self): self.assertEqual([], ret) def test_add_security_group_to_server_nova(self): - self.has_neutron = False self.cloud.secgroup_source = 'nova' @@ -916,7 +913,6 @@ def test_add_security_group_to_server_neutron(self): self.assert_calls() def test_remove_security_group_from_server_nova(self): - self.has_neutron = False self.cloud.secgroup_source = 'nova' diff --git a/openstack/tests/unit/cloud/test_server_console.py b/openstack/tests/unit/cloud/test_server_console.py index 1364422ba..abfb52e6a 100644 --- a/openstack/tests/unit/cloud/test_server_console.py +++ b/openstack/tests/unit/cloud/test_server_console.py @@ -48,7 +48,6 @@ def test_get_server_console_dict(self): self.assert_calls() def test_get_server_console_name_or_id(self): - self.register_uris( [ self.get_nova_discovery_mock_dict(), @@ -77,7 +76,6 @@ def test_get_server_console_name_or_id(self): self.assert_calls() def test_get_server_console_no_console(self): - self.register_uris( [ self.get_nova_discovery_mock_dict(), diff --git a/openstack/tests/unit/cloud/test_server_group.py b/openstack/tests/unit/cloud/test_server_group.py index 5e6d987c3..fc1443221 100644 --- a/openstack/tests/unit/cloud/test_server_group.py +++ b/openstack/tests/unit/cloud/test_server_group.py @@ -28,7 +28,6 @@ def setUp(self): ) def test_create_server_group(self): - self.register_uris( [ self.get_nova_discovery_mock_dict(), diff --git a/openstack/tests/unit/cloud/test_services.py b/openstack/tests/unit/cloud/test_services.py index c89695e3a..ee37389b2 100644 --- a/openstack/tests/unit/cloud/test_services.py +++ b/openstack/tests/unit/cloud/test_services.py @@ -37,7 +37,6 @@ def get_mock_url( append=None, base_url_append='v3', ): - return super(CloudServices, self).get_mock_url( service_type, interface, resource, append, base_url_append ) diff --git a/openstack/tests/unit/cloud/test_stack.py b/openstack/tests/unit/cloud/test_stack.py index 85297d0a7..0e2717bdb 100644 --- a/openstack/tests/unit/cloud/test_stack.py +++ b/openstack/tests/unit/cloud/test_stack.py @@ -616,7 +616,6 @@ def test_create_stack(self): self.assert_calls() def test_create_stack_wait(self): - test_template = tempfile.NamedTemporaryFile(delete=False) test_template.write(fakes.FAKE_TEMPLATE.encode('utf-8')) test_template.close() diff --git a/openstack/tests/unit/cloud/test_subnet.py b/openstack/tests/unit/cloud/test_subnet.py index fe95f4912..13b6447aa 100644 --- a/openstack/tests/unit/cloud/test_subnet.py +++ b/openstack/tests/unit/cloud/test_subnet.py @@ -23,7 +23,6 @@ class TestSubnet(base.TestCase): - network_name = 'network_name' subnet_name = 'subnet_name' subnet_id = '1f1696eb-7f47-47f6-835c-4889bff88604' diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 2d488acd1..b0afd8dd9 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -1310,7 +1310,6 @@ def test_create_image(self): with mock.patch( 'openstack.compute.v2.server.Server.create_image' ) as ci_mock: - ci_mock.return_value = 'image_id' connection_mock = mock.Mock() connection_mock.get_image = mock.Mock(return_value='image') diff --git a/openstack/tests/unit/config/test_from_session.py b/openstack/tests/unit/config/test_from_session.py index 4d28e07e6..b96692c0b 100644 --- a/openstack/tests/unit/config/test_from_session.py +++ b/openstack/tests/unit/config/test_from_session.py @@ -22,7 +22,6 @@ class TestFromSession(base.TestCase): - scenarios = [ ('no_region', dict(test_region=None)), ('with_region', dict(test_region='RegionOne')), diff --git a/openstack/tests/unit/fake/v1/_proxy.py b/openstack/tests/unit/fake/v1/_proxy.py index 98a05119e..8b84491fa 100644 --- a/openstack/tests/unit/fake/v1/_proxy.py +++ b/openstack/tests/unit/fake/v1/_proxy.py @@ -13,7 +13,6 @@ class Proxy(proxy.Proxy): - skip_discovery = True def dummy(self): diff --git a/openstack/tests/unit/fake/v2/_proxy.py b/openstack/tests/unit/fake/v2/_proxy.py index 003c72d29..66955be6b 100644 --- a/openstack/tests/unit/fake/v2/_proxy.py +++ b/openstack/tests/unit/fake/v2/_proxy.py @@ -13,7 +13,6 @@ class Proxy(proxy.Proxy): - skip_discovery = True def dummy(self): diff --git a/openstack/tests/unit/load_balancer/v2/test_proxy.py b/openstack/tests/unit/load_balancer/v2/test_proxy.py index ee663969a..6fd813f2c 100644 --- a/openstack/tests/unit/load_balancer/v2/test_proxy.py +++ b/openstack/tests/unit/load_balancer/v2/test_proxy.py @@ -33,7 +33,6 @@ class TestLoadBalancerProxy(test_proxy_base.TestProxyBase): - LB_ID = uuid.uuid4() LISTENER_ID = uuid.uuid4() POOL_ID = uuid.uuid4() diff --git a/openstack/tests/unit/object_store/v1/test_proxy.py b/openstack/tests/unit/object_store/v1/test_proxy.py index 72ba171a2..cd4d0274a 100644 --- a/openstack/tests/unit/object_store/v1/test_proxy.py +++ b/openstack/tests/unit/object_store/v1/test_proxy.py @@ -38,7 +38,6 @@ def json(self): class TestObjectStoreProxy(test_proxy_base.TestProxyBase): - kwargs_to_path_args = False def setUp(self): @@ -156,7 +155,6 @@ def test_object_get_remember_content(self): self.assertEqual(res.data, "data") def test_set_temp_url_key(self): - key = 'super-secure-key' self.register_uris( @@ -180,7 +178,6 @@ def test_set_temp_url_key(self): self.assert_calls() def test_set_account_temp_url_key_second(self): - key = 'super-secure-key' self.register_uris( @@ -204,7 +201,6 @@ def test_set_account_temp_url_key_second(self): self.assert_calls() def test_set_container_temp_url_key(self): - key = 'super-secure-key' self.register_uris( @@ -228,7 +224,6 @@ def test_set_container_temp_url_key(self): self.assert_calls() def test_set_container_temp_url_key_second(self): - key = 'super-secure-key' self.register_uris( @@ -274,7 +269,7 @@ def test_file_segment(self): ) self.assertEqual(len(segments), 5) segment_content = b'' - for (index, (name, segment)) in enumerate(segments.items()): + for index, (name, segment) in enumerate(segments.items()): self.assertEqual( 'test_container/test_image/{index:0>6}'.format(index=index), name, @@ -333,7 +328,6 @@ def test_stream(self): class TestExtractName(TestObjectStoreProxy): - scenarios = [ ('discovery', dict(url='/', parts=['account'])), ('endpoints', dict(url='/endpoints', parts=['endpoints'])), @@ -351,7 +345,6 @@ class TestExtractName(TestObjectStoreProxy): ] def test_extract_name(self): - results = self.proxy._extract_name(self.url, project_id='123') self.assertEqual(self.parts, results) diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index bd1b212ec..013ee9007 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -403,7 +403,6 @@ def test_validate_template_invalid_request(self): class TestExtractName(TestOrchestrationProxy): - scenarios = [ ('stacks', dict(url='/stacks', parts=['stacks'])), ('name_id', dict(url='/stacks/name/id', parts=['stack'])), @@ -452,6 +451,5 @@ class TestExtractName(TestOrchestrationProxy): ] def test_extract_name(self): - results = self.proxy._extract_name(self.url) self.assertEqual(self.parts, results) diff --git a/openstack/tests/unit/orchestration/v1/test_stack.py b/openstack/tests/unit/orchestration/v1/test_stack.py index 3503ff00b..ecb485c9a 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack.py +++ b/openstack/tests/unit/orchestration/v1/test_stack.py @@ -213,7 +213,6 @@ def test_check(self): sot._action.assert_called_with(sess, body) def test_fetch(self): - sess = mock.Mock() sess.default_microversion = None sot = stack.Stack(**FAKE) diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index 4dfc25748..4149be766 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -328,7 +328,6 @@ def test_from_conf_filter_service_types(self): class TestNetworkConnection(base.TestCase): - # Verify that if the catalog has the suffix we don't mess things up. def test_network_proxy(self): self.os_fixture.v3_token.remove_service('network') @@ -533,7 +532,6 @@ def setUp(self): ) def test_conn_from_profile(self): - self.cloud = self.config.get_one(cloud='profiled-cloud') conn = connection.Connection(config=self.cloud) @@ -541,7 +539,6 @@ def test_conn_from_profile(self): self.assertIsNotNone(conn) def test_hook_from_profile(self): - self.cloud = self.config.get_one(cloud='profiled-cloud') conn = connection.Connection(config=self.cloud) @@ -549,7 +546,6 @@ def test_hook_from_profile(self): self.assertEqual('test_val', conn.test) def test_hook_from_connection_param(self): - conn = connection.Connection( cloud='sample-cloud', vendor_hook='openstack.tests.unit.test_connection:vendor_hook', @@ -558,7 +554,6 @@ def test_hook_from_connection_param(self): self.assertEqual('test_val', conn.test) def test_hook_from_connection_ignore_missing(self): - conn = connection.Connection( cloud='sample-cloud', vendor_hook='openstack.tests.unit.test_connection:missing', diff --git a/openstack/tests/unit/test_microversions.py b/openstack/tests/unit/test_microversions.py index cbe73a6ad..531f89bcf 100644 --- a/openstack/tests/unit/test_microversions.py +++ b/openstack/tests/unit/test_microversions.py @@ -21,7 +21,6 @@ def setUp(self): self.use_compute_discovery() def test_get_bad_inferred_max_microversion(self): - self.cloud.config.config['compute_api_version'] = '2.61' self.assertRaises( @@ -33,7 +32,6 @@ def test_get_bad_inferred_max_microversion(self): self.assert_calls() def test_get_bad_default_max_microversion(self): - self.cloud.config.config['compute_default_microversion'] = '2.61' self.assertRaises( @@ -45,7 +43,6 @@ def test_get_bad_default_max_microversion(self): self.assert_calls() def test_get_bad_inferred_min_microversion(self): - self.cloud.config.config['compute_api_version'] = '2.7' self.assertRaises( @@ -57,7 +54,6 @@ def test_get_bad_inferred_min_microversion(self): self.assert_calls() def test_get_bad_default_min_microversion(self): - self.cloud.config.config['compute_default_microversion'] = '2.7' self.assertRaises( @@ -69,7 +65,6 @@ def test_get_bad_default_min_microversion(self): self.assert_calls() def test_inferred_default_microversion(self): - self.cloud.config.config['compute_api_version'] = '2.42' server1 = fakes.make_fake_server('123', 'mickey') @@ -95,7 +90,6 @@ def test_inferred_default_microversion(self): self.assert_calls() def test_default_microversion(self): - self.cloud.config.config['compute_default_microversion'] = '2.42' server1 = fakes.make_fake_server('123', 'mickey') @@ -121,7 +115,6 @@ def test_default_microversion(self): self.assert_calls() def test_conflicting_implied_and_direct(self): - self.cloud.config.config['compute_default_microversion'] = '2.7' self.cloud.config.config['compute_api_version'] = '2.13' diff --git a/openstack/tests/unit/test_missing_version.py b/openstack/tests/unit/test_missing_version.py index b2a18f9c8..473a9594e 100644 --- a/openstack/tests/unit/test_missing_version.py +++ b/openstack/tests/unit/test_missing_version.py @@ -34,7 +34,6 @@ def setUp(self): ) def test_unsupported_version(self): - with testtools.ExpectedException(exceptions.NotSupported): self.cloud.image.get('/') diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index 3dddb9cad..45cff353f 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -584,7 +584,6 @@ def test_head_id(self): class TestExtractName(base.TestCase): - scenarios = [ ('slash_servers_bare', dict(url='/servers', parts=['servers'])), ('slash_servers_arg', dict(url='/servers/1', parts=['server'])), @@ -605,7 +604,6 @@ class TestExtractName(base.TestCase): ] def test_extract_name(self): - results = proxy.Proxy(mock.Mock())._extract_name(self.url) self.assertEqual(self.parts, results) diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 3dc01637a..67a4fcad6 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -3265,7 +3265,6 @@ class Test(resource.Resource): class TestResourceFind(base.TestCase): - result = 1 class Base(resource.Resource): @@ -3290,7 +3289,6 @@ def _get_one_match(cls, *args): return None class OneResultWithQueryParams(OneResult): - _query_mapping = resource.QueryParameters('name') def setUp(self): @@ -3407,14 +3405,12 @@ def test_multiple_matches(self): ) def test_list_no_base_path(self): - with mock.patch.object(self.Base, "list") as list_mock: self.Base.find(self.cloud.compute, "name") list_mock.assert_called_with(self.cloud.compute) def test_list_base_path(self): - with mock.patch.object(self.Base, "list") as list_mock: self.Base.find( self.cloud.compute, "name", list_base_path='/dummy/list' diff --git a/openstack/tests/unit/test_stats.py b/openstack/tests/unit/test_stats.py index 25d8c11b0..f97b4d6f9 100644 --- a/openstack/tests/unit/test_stats.py +++ b/openstack/tests/unit/test_stats.py @@ -50,7 +50,7 @@ def run(self): poll.register(self.sock, select.POLLIN) poll.register(self.wake_read, select.POLLIN) ret = poll.poll() - for (fd, event) in ret: + for fd, event in ret: if fd == self.sock.fileno(): data = self.sock.recvfrom(1024) if not data: @@ -167,7 +167,6 @@ def assert_prometheus_stat(self, name, value, labels=None): self.assertEqual(sample_value, value) def test_list_projects(self): - mock_uri = self.get_mock_url( service_type='identity', resource='projects', base_url_append='v3' ) @@ -234,7 +233,6 @@ def test_projects(self): ) def test_servers(self): - mock_uri = 'https://compute.example.com/v2.1/servers/detail' self.register_uris( @@ -272,7 +270,6 @@ def test_servers(self): ) def test_servers_no_detail(self): - mock_uri = 'https://compute.example.com/v2.1/servers' self.register_uris( @@ -310,7 +307,6 @@ def test_servers_no_detail(self): ) def test_servers_error(self): - mock_uri = 'https://compute.example.com/v2.1/servers' self.register_uris( @@ -341,7 +337,6 @@ def test_servers_error(self): ) def test_timeout(self): - mock_uri = 'https://compute.example.com/v2.1/servers' self.register_uris( @@ -368,7 +363,6 @@ def setUp(self): self.useFixture(self.statsd) def test_no_stats(self): - mock_uri = self.get_mock_url( service_type='identity', resource='projects', base_url_append='v3' ) diff --git a/openstack/tests/unit/test_utils.py b/openstack/tests/unit/test_utils.py index 12593e79a..f5b3413fe 100644 --- a/openstack/tests/unit/test_utils.py +++ b/openstack/tests/unit/test_utils.py @@ -60,7 +60,6 @@ def setUp(self): ) def _console_tests(self, level, debug, stream): - openstack.enable_logging(debug=debug, stream=stream) self.assertEqual(self.openstack_logger.addHandler.call_count, 1) diff --git a/openstack/utils.py b/openstack/utils.py index 1444cfc71..78d9a22e9 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -563,7 +563,7 @@ def post_munchify(partial, obj): elif isinstance(obj, list): partial.extend(munchify_cycles(item) for item in obj) elif isinstance(obj, tuple): - for (item_partial, item) in zip(partial, obj): + for item_partial, item in zip(partial, obj): post_munchify(item_partial, item) return partial @@ -603,7 +603,7 @@ def post_unmunchify(partial, obj): elif isinstance(obj, list): partial.extend(unmunchify_cycles(v) for v in obj) elif isinstance(obj, tuple): - for (value_partial, value) in zip(partial, obj): + for value_partial, value in zip(partial, obj): post_unmunchify(value_partial, value) return partial From aba2b4179c15baae0ab1f9f26fb1460fae7cd0eb Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 5 May 2023 12:48:49 +0100 Subject: [PATCH 3261/3836] Ignore black version bump Add the latest black version bump commit to .git-blame-ignore-revs Change-Id: Ica8ed638c11eaa4fbcd0104e70c88e1044f70661 Signed-off-by: Stephen Finucane --- .git-blame-ignore-revs | 1 + 1 file changed, 1 insertion(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 494eff046..ef89dbb03 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,6 +1,7 @@ # You can configure git to automatically use this file with the following config: # git config --global blame.ignoreRevsFile .git-blame-ignore-revs +c7010a2f929de9fad4e1a7c7f5a17cb8e210432a # Bump black to 23.3.0 a36f514295a4b4e6157ce69a210f653bcc4df7f2 # Blackify everything else 004c7352d0a4fb467a319ae9743eb6ca5ee9ce7f # Blackify openstack.cloud c2ff7336cecabc665e7bf04cbe87ef8d0c2e6f9f # Blackify openstack.clustering From 165b50d9f0c3b6d3c1c9a9e6fc3be26263ce146a Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 4 May 2023 14:07:06 +0200 Subject: [PATCH 3262/3836] add extended neutron job We are currently not testing fwaas at all and not seeing it is not working at all as seen by an attempt to add ansible module. Same is true for vpnaas. Hopefully fix flaking auto_allocated_topology which tried to idenfity projects by available networks. Change-Id: Iaf62662ca3dc4415737369f676109cccf7090e5a --- .../v2/test_auto_allocated_topology.py | 4 +- zuul.d/functional-jobs.yaml | 73 +++++++++++++++++++ zuul.d/project.yaml | 2 + 3 files changed, 76 insertions(+), 3 deletions(-) diff --git a/openstack/tests/functional/network/v2/test_auto_allocated_topology.py b/openstack/tests/functional/network/v2/test_auto_allocated_topology.py index 73c070bfb..800bfcac2 100644 --- a/openstack/tests/functional/network/v2/test_auto_allocated_topology.py +++ b/openstack/tests/functional/network/v2/test_auto_allocated_topology.py @@ -31,9 +31,7 @@ def setUp(self): "required for this test" ) - projects = [ - o.project_id for o in self.operator_cloud.network.networks() - ] + projects = [o.id for o in self.operator_cloud.identity.projects()] self.PROJECT_ID = projects[0] def tearDown(self): diff --git a/zuul.d/functional-jobs.yaml b/zuul.d/functional-jobs.yaml index 794711ccf..b0933a8f9 100644 --- a/zuul.d/functional-jobs.yaml +++ b/zuul.d/functional-jobs.yaml @@ -143,6 +143,79 @@ OPENSTACKSDK_HAS_SWIFT: 0 OPENSTACKSDK_HAS_HEAT: 0 +- job: + name: openstacksdk-functional-devstack-networking-ext + parent: openstacksdk-functional-devstack-networking + description: | + Run openstacksdk functional tests against a devstack with super advanced + networking services enabled (VPNaas, FWaas) which still require ovs. + required-projects: + - openstack/neutron-fwaas + - openstack/neutron-vpnaas + vars: + INSTALL_OVN: False + configure_swap_size: 4096 + devstack_local_conf: + post-config: + $OCTAVIA_CONF: + DEFAULT: + debug: true + controller_worker: + amphora_driver: amphora_noop_driver + compute_driver: compute_noop_driver + network_driver: network_noop_driver + certificates: + cert_manager: local_cert_manager + $NEUTRON_CONF: + DEFAULT: + router_distributed: True + l3_ha: True + "/$NEUTRON_CORE_PLUGIN_CONF": + ovs: + tunnel_bridge: br-tun + bridge_mappings: public:br-ex + $NEUTRON_L3_CONF: + DEFAULT: + agent_mode: dvr_snat + agent: + availability_zone: nova + debug_iptables_rules: True + $NEUTRON_DHCP_CONF: + agent: + availability_zone: nova + devstack_localrc: + Q_SERVICE_PLUGIN_CLASSES: qos,trunk + NETWORK_API_EXTENSIONS: "agent,binding,dhcp_agent_scheduler,external-net,ext-gw-mode,extra_dhcp_opts,quotas,router,security-group,subnet_allocation,network-ip-availability,auto-allocated-topology,timestamp_core,tag,service-type,rbac-policies,standard-attr-description,pagination,sorting,project-id,fwaas_v2,vpnaas" + Q_AGENT: openvswitch + Q_ML2_TENANT_NETWORK_TYPE: vxlan + Q_ML2_PLUGIN_MECHANISM_DRIVERS: openvswitch + IPSEC_PACKAGE: libreswan + devstack_plugins: + designate: https://opendev.org/openstack/designate + octavia: https://opendev.org/openstack/octavia + neutron-fwaas: https://opendev.org/openstack/neutron-fwaas.git + neutron-vpnaas: https://opendev.org/openstack/neutron-vpnaas.git + devstack_services: + designate: true + octavia: true + o-api: true + o-cw: true + o-hm: true + o-hk: true + neutron-dns: true + s-account: false + s-container: false + s-object: false + s-proxy: false + h-eng: false + h-api: false + h-api-cfn: false + q-fwaas-v2: true + tox_environment: + OPENSTACKSDK_HAS_DESIGNATE: 1 + OPENSTACKSDK_HAS_SWIFT: 0 + OPENSTACKSDK_HAS_HEAT: 0 + - job: name: openstacksdk-functional-devstack-tips parent: openstacksdk-functional-devstack diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index 6f9cb948b..284d200a2 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -21,6 +21,7 @@ voting: false - openstacksdk-functional-devstack - openstacksdk-functional-devstack-networking + - openstacksdk-functional-devstack-networking-ext - openstacksdk-functional-devstack-senlin - openstacksdk-functional-devstack-magnum: voting: false @@ -43,4 +44,5 @@ voting: false - openstacksdk-functional-devstack - openstacksdk-functional-devstack-networking + - openstacksdk-functional-devstack-networking-ext - openstacksdk-functional-devstack-senlin From 692b7f39e3765b10f3092ea6a2886ed03cbc37b6 Mon Sep 17 00:00:00 2001 From: Reynaldo Bontje Date: Wed, 3 May 2023 23:55:33 +0000 Subject: [PATCH 3263/3836] Added neutron fields to share network resource. Adds fields to share network resource, enabling the creation of share networks that use neutron networks and neutron subnets. Change-Id: Ie6cace724bcb2084b71942be35bac79f085215d7 --- .../shared_file_system/v2/share_network.py | 13 +++++++- .../shared_file_system/test_share_network.py | 30 ++++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/openstack/shared_file_system/v2/share_network.py b/openstack/shared_file_system/v2/share_network.py index 72f088ff0..6a4fdd3f3 100644 --- a/openstack/shared_file_system/v2/share_network.py +++ b/openstack/shared_file_system/v2/share_network.py @@ -11,6 +11,7 @@ # under the License. from openstack import resource +from openstack.shared_file_system.v2 import share_network_subnet class ShareNetwork(resource.Resource): @@ -48,7 +49,17 @@ class ShareNetwork(resource.Resource): project_id = resource.Body("project_id", type=str) #: A list of share network subnets that pertain to the related share #: network. - # share_network_subnets = resource.Body("share_network_subnets", type=list) + share_network_subnets = resource.Body( + "share_network_subnets", + type=list, + list_type=share_network_subnet.ShareNetworkSubnet, + ) + #: The UUID of a neutron network when setting up or + #: updating a share network subnet with neutron. + neutron_net_id = resource.Body("neutron_net_id", type=str) + #: The UUID of the neutron subnet when setting up or updating + #: a share network subnet with neutron. + neutron_subnet_id = resource.Body("neutron_subnet_id", type=str) #: The date and time stamp when the resource was last updated within #: the service’s database. updated_at = resource.Body("updated_at", type=str) diff --git a/openstack/tests/functional/shared_file_system/test_share_network.py b/openstack/tests/functional/shared_file_system/test_share_network.py index affc2143e..1f365d6f4 100644 --- a/openstack/tests/functional/shared_file_system/test_share_network.py +++ b/openstack/tests/functional/shared_file_system/test_share_network.py @@ -18,10 +18,28 @@ class ShareNetworkTest(base.BaseSharedFileSystemTest): def setUp(self): super(ShareNetworkTest, self).setUp() + self.NETWORK_NAME = self.getUniqueString() + net = self.user_cloud.network.create_network(name=self.NETWORK_NAME) + self.assertIsNotNone(net) + self.assertIsNotNone(net.id) + self.NETWORK_ID = net.id + + self.SUBNET_NAME = self.getUniqueString() + subnet = self.user_cloud.network.create_subnet( + name=self.SUBNET_NAME, + network_id=self.NETWORK_ID, + ip_version=4, + cidr='10.0.0.0/24', + ) + self.SUBNET_ID = subnet.id + self.SHARE_NETWORK_NAME = self.getUniqueString() snt = self.user_cloud.shared_file_system.create_share_network( - name=self.SHARE_NETWORK_NAME + name=self.SHARE_NETWORK_NAME, + neutron_net_id=self.NETWORK_ID, + neutron_subnet_id=self.SUBNET_ID, ) + self.assertIsNotNone(snt) self.assertIsNotNone(snt.id) self.SHARE_NETWORK_ID = snt.id @@ -31,6 +49,7 @@ def tearDown(self): self.SHARE_NETWORK_ID, ignore_missing=True ) self.assertIsNone(sot) + self.user_cloud.network.delete_network(self.NETWORK_ID) super(ShareNetworkTest, self).tearDown() def test_get(self): @@ -39,6 +58,15 @@ def test_get(self): ) assert isinstance(sot, _share_network.ShareNetwork) self.assertEqual(self.SHARE_NETWORK_ID, sot.id) + self.assertIsNotNone(sot.share_network_subnets) + self.assertEqual( + self.NETWORK_ID, + sot.share_network_subnets[0]['neutron_net_id'], + ) + self.assertEqual( + self.SUBNET_ID, + sot.share_network_subnets[0]['neutron_subnet_id'], + ) def test_list_share_network(self): share_nets = self.user_cloud.shared_file_system.share_networks( From 7fec4e0bb1b69cf86951365d1511650017b2b36c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 10 May 2023 10:53:44 +0100 Subject: [PATCH 3264/3836] tox: Bump min_version to 4.3.0 tox 4 is significantly faster when it comes to creating virtualenvs and is far better and detecting e.g. changes to requirements. tox 4.3.0 includes many of the fixes for bugs introduced by tox 4.0 and is a good default candidate. Change-Id: I7d29a4a1bc6ee9273e70adc9f382c1d1610f0286 Signed-off-by: Stephen Finucane --- tox.ini | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tox.ini b/tox.ini index 13a0c3a3b..db9bc379b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,17 +1,13 @@ [tox] -minversion = 3.18.0 +minversion = 4.3.0 envlist = pep8,py3 -ignore_basepython_conflict = True [testenv] -usedevelop = True -install_command = pip install {opts} {packages} +usedevelop = true passenv = OS_* OPENSTACKSDK_* -basepython = python3 setenv = - VIRTUAL_ENV={envdir} LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=C From ec1a035529bdb7be01dec318090bfbf6832d0019 Mon Sep 17 00:00:00 2001 From: whoami-rajat Date: Thu, 11 May 2023 12:52:09 +0530 Subject: [PATCH 3265/3836] nit: Correct name of variable The variable name is resources_key and not resource_keys. https://github.com/openstack/openstacksdk/blob/692b7f39e3765b10f3092ea6a2886ed03cbc37b6/openstack/compute/v2/server.py#L33 Change-Id: I58d1973aa5b1d37673d629fb39630280cb1b31ad --- doc/source/contributor/layout.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/contributor/layout.rst b/doc/source/contributor/layout.rst index 0656ee1a2..f96bb5632 100644 --- a/doc/source/contributor/layout.rst +++ b/doc/source/contributor/layout.rst @@ -44,7 +44,7 @@ string replacement is used, e.g., ``base_path = "/servers/%(server_id)s/ips"``. requires a key to obtain the response value. For example, the ``Server`` class sets ``resource_key = "server"`` as an individual ``Server`` is stored in a dictionary keyed with the singular noun, -and ``resource_keys = "servers"`` as multiple ``Server``\s are stored in +and ``resources_key = "servers"`` as multiple ``Server``\s are stored in a dictionary keyed with the plural noun in the response. Proxy From 8357f00424107902f1678171fabcf155f4784e4d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 16 May 2023 10:54:28 +0100 Subject: [PATCH 3266/3836] ironic: Add support for Introspection Rules https://docs.openstack.org/api-ref/baremetal-introspection/#introspection-rules Change-Id: I3136eefe7733edb0a05c2e5ee6b90c74781d5bc2 Story: 2008193 Task: 40958 --- .../baremetal_introspection/index.rst | 1 + .../v1/introspection_rule.rst | 13 +++ .../baremetal_introspection/v1/_proxy.py | 82 ++++++++++++++++++- .../v1/introspection_rule.py | 44 ++++++++++ .../v1/test_introspection_rule.py | 74 +++++++++++++++++ .../baremetal_introspection/v1/test_proxy.py | 39 +++++++++ ...ection_rules_support-18b0488a76800122.yaml | 3 + 7 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 doc/source/user/resources/baremetal_introspection/v1/introspection_rule.rst create mode 100644 openstack/baremetal_introspection/v1/introspection_rule.py create mode 100644 openstack/tests/unit/baremetal_introspection/v1/test_introspection_rule.py create mode 100644 releasenotes/notes/ironic-introspection_rules_support-18b0488a76800122.yaml diff --git a/doc/source/user/resources/baremetal_introspection/index.rst b/doc/source/user/resources/baremetal_introspection/index.rst index 4f1b371c7..b1da15638 100644 --- a/doc/source/user/resources/baremetal_introspection/index.rst +++ b/doc/source/user/resources/baremetal_introspection/index.rst @@ -5,3 +5,4 @@ Baremetal Introspection Resources :maxdepth: 1 v1/introspection + v1/introspection_rule diff --git a/doc/source/user/resources/baremetal_introspection/v1/introspection_rule.rst b/doc/source/user/resources/baremetal_introspection/v1/introspection_rule.rst new file mode 100644 index 000000000..ba8bc2856 --- /dev/null +++ b/doc/source/user/resources/baremetal_introspection/v1/introspection_rule.rst @@ -0,0 +1,13 @@ +openstack.baremetal_introspection.v1.introspection_rule +======================================================== + +.. automodule:: openstack.baremetal_introspection.v1.introspection_rule + +The IntrospectionRule Class +---------------------------- + +The ``IntrospectionRule`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.baremetal_introspection.v1.introspection_rule.IntrospectionRule + :members: diff --git a/openstack/baremetal_introspection/v1/_proxy.py b/openstack/baremetal_introspection/v1/_proxy.py index e75d9ac14..b82cee8fe 100644 --- a/openstack/baremetal_introspection/v1/_proxy.py +++ b/openstack/baremetal_introspection/v1/_proxy.py @@ -13,6 +13,9 @@ from openstack import _log from openstack.baremetal.v1 import node as _node from openstack.baremetal_introspection.v1 import introspection as _introspect +from openstack.baremetal_introspection.v1 import ( + introspection_rule as _introspection_rule, +) from openstack import exceptions from openstack import proxy @@ -23,6 +26,7 @@ class Proxy(proxy.Proxy): _resource_registry = { "introspection": _introspect.Introspection, + "introspection_rule": _introspection_rule.IntrospectionRule, } def introspections(self, **query): @@ -128,7 +132,10 @@ def abort_introspection(self, introspection, ignore_missing=True): raise def wait_for_introspection( - self, introspection, timeout=None, ignore_error=False + self, + introspection, + timeout=None, + ignore_error=False, ): """Wait for the introspection to finish. @@ -147,3 +154,76 @@ def wait_for_introspection( """ res = self._get_resource(_introspect.Introspection, introspection) return res.wait(self, timeout=timeout, ignore_error=ignore_error) + + def create_introspection_rule(self, **attrs): + """Create a new introspection rules from attributes. + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~.introspection_rule.IntrospectionRule`, + comprised of the properties on the IntrospectionRule class. + + :returns: :class:`~.introspection_rule.IntrospectionRule` instance. + """ + return self._create(_introspection_rule.IntrospectionRule, **attrs) + + def delete_introspection_rule( + self, + introspection_rule, + ignore_missing=True, + ): + """Delete an introspection rule. + + :param introspection_rule: The value can be either the ID of an + introspection rule or a + :class:`~.introspection_rule.IntrospectionRule` instance. + :param bool ignore_missing: When set to ``False``, an + exception:class:`~openstack.exceptions.ResourceNotFound` will be + raised when the introspection rule could not be found. When set to + ``True``, no exception will be raised when attempting to delete a + non-existent introspection rule. + + :returns: ``None`` + """ + self._delete( + _introspection_rule.IntrospectionRule, + introspection_rule, + ignore_missing=ignore_missing, + ) + + def get_introspection_rule(self, introspection_rule): + """Get a specific introspection rule. + + :param introspection_rule: The value can be the name or ID of an + introspection rule or a + :class:`~.introspection_rule.IntrospectionRule` instance. + + :returns: :class:`~.introspection_rule.IntrospectionRule` instance. + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + introspection rule matching the name or ID could be found. + """ + return self._get( + _introspection_rule.IntrospectionRule, + introspection_rule, + ) + + def introspection_rules(self, **query): + """Retrieve a generator of introspection rules. + + :param dict query: Optional query parameters to be sent to restrict + the records to be returned. Available parameters include: + + * ``uuid``: The UUID of the Ironic Inspector rule. + * ``limit``: List of a logic statementd or operations in rules, + that can be evaluated as True or False. + * ``actions``: List of operations that will be performed + if conditions of this rule are fulfilled. + * ``description``: Rule human-readable description. + * ``scope``: Scope of an introspection rule. If set, the rule + is only applied to nodes that have + matching inspection_scope property. + + :returns: A generator of + :class:`~.introspection_rule.IntrospectionRule` + objects + """ + return self._list(_introspection_rule.IntrospectionRule, **query) diff --git a/openstack/baremetal_introspection/v1/introspection_rule.py b/openstack/baremetal_introspection/v1/introspection_rule.py new file mode 100644 index 000000000..1d37d4dea --- /dev/null +++ b/openstack/baremetal_introspection/v1/introspection_rule.py @@ -0,0 +1,44 @@ +# 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.baremetal.v1 import _common +from openstack import resource + + +class IntrospectionRule(_common.ListMixin, resource.Resource): + resources_key = 'rules' + base_path = '/rules' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = False + allow_delete = True + allow_list = True + + # created via POST with ID + create_method = 'POST' + create_requires_id = True + + #: The UUID of the resource. + id = resource.Body('uuid', alternate_id=True) + #: List of a logic statementd or operations in rules + conditions = resource.Body('conditions', type=list) + #: List of operations that will be performed if conditions of this rule + #: are fulfilled. + actions = resource.Body('actions', type=list) + #: Rule human-readable description + description = resource.Body('description') + #: Scope of an introspection rule + scope = resource.Body('scope') + #: A list of relative links, including the self and bookmark links. + links = resource.Body('links', type=list) diff --git a/openstack/tests/unit/baremetal_introspection/v1/test_introspection_rule.py b/openstack/tests/unit/baremetal_introspection/v1/test_introspection_rule.py new file mode 100644 index 000000000..5e059f80a --- /dev/null +++ b/openstack/tests/unit/baremetal_introspection/v1/test_introspection_rule.py @@ -0,0 +1,74 @@ +# 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.baremetal_introspection.v1 import introspection_rule +from openstack.tests.unit import base + +FAKE = { + "actions": [ + { + "action": "set-attribute", + "path": "driver_info/deploy_kernel", + "value": "8fd65-c97b-4d00-aa8b-7ed166a60971", + }, + { + "action": "set-attribute", + "path": "driver_info/deploy_ramdisk", + "value": "09e5420c-6932-4199-996e-9485c56b3394", + }, + ], + "conditions": [ + { + "field": "node://driver_info.deploy_ramdisk", + "invert": False, + "multiple": "any", + "op": "is-empty", + }, + { + "field": "node://driver_info.deploy_kernel", + "invert": False, + "multiple": "any", + "op": "is-empty", + }, + ], + "description": "Set deploy info if not already set on node", + "links": [ + { + "href": "/v1/rules/7459bf7c-9ff9-43a8-ba9f-48542ecda66c", + "rel": "self", + } + ], + "uuid": "7459bf7c-9ff9-43a8-ba9f-48542ecda66c", + "scope": "", +} + + +class TestIntrospectionRule(base.TestCase): + def test_basic(self): + sot = introspection_rule.IntrospectionRule() + self.assertIsNone(sot.resource_key) + self.assertEqual('rules', sot.resources_key) + self.assertEqual('/rules', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertEqual('POST', sot.create_method) + + def test_instantiate(self): + sot = introspection_rule.IntrospectionRule(**FAKE) + self.assertEqual(FAKE['conditions'], sot.conditions) + self.assertEqual(FAKE['actions'], sot.actions) + self.assertEqual(FAKE['description'], sot.description) + self.assertEqual(FAKE['uuid'], sot.id) + self.assertEqual(FAKE['scope'], sot.scope) diff --git a/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py b/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py index 6192028d1..19975aca6 100644 --- a/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py @@ -17,6 +17,7 @@ from openstack.baremetal.v1 import node as _node from openstack.baremetal_introspection.v1 import _proxy from openstack.baremetal_introspection.v1 import introspection +from openstack.baremetal_introspection.v1 import introspection_rule from openstack import exceptions from openstack.tests.unit import base from openstack.tests.unit import test_proxy_base @@ -188,3 +189,41 @@ def test_get_unprocessed_data(self, mock_request): microversion='1.17', ) self.assertIs(data, mock_request.return_value.json.return_value) + + +class TestIntrospectionRule(test_proxy_base.TestProxyBase): + def setUp(self): + super().setUp() + self.proxy = _proxy.Proxy(self.session) + + def test_introspection_rule_create(self): + self.verify_create( + self.proxy.create_introspection_rule, + introspection_rule.IntrospectionRule, + ) + + def test_introspection_rule_delete(self): + self.verify_delete( + self.proxy.delete_introspection_rule, + introspection_rule.IntrospectionRule, + False, + ) + + def test_introspection_rule_delete_ignore(self): + self.verify_delete( + self.proxy.delete_introspection_rule, + introspection_rule.IntrospectionRule, + True, + ) + + def test_introspection_rule_get(self): + self.verify_get( + self.proxy.get_introspection_rule, + introspection_rule.IntrospectionRule, + ) + + def test_introspection_rules(self): + self.verify_list( + self.proxy.introspection_rules, + introspection_rule.IntrospectionRule, + ) diff --git a/releasenotes/notes/ironic-introspection_rules_support-18b0488a76800122.yaml b/releasenotes/notes/ironic-introspection_rules_support-18b0488a76800122.yaml new file mode 100644 index 000000000..7aede0678 --- /dev/null +++ b/releasenotes/notes/ironic-introspection_rules_support-18b0488a76800122.yaml @@ -0,0 +1,3 @@ +features: + - | + Add support for Ironic Inspector Introspection Rules API. From 4faf511f4ffb2222cde2d011d8f0a4d6e8407c6d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 16 May 2023 11:03:58 +0100 Subject: [PATCH 3267/3836] identity: Add access rule CRUD support This patch adds the client support of access rule in application credentials [1]. [1] https://docs.openstack.org/api-ref/identity/v3/index.html#list-access-rules Change-Id: I9e1eab6eb3ff6e152b408af2fe6ddc57c8c168b2 --- openstack/identity/v3/_proxy.py | 55 +++++++++++++ openstack/identity/v3/access_rule.py | 39 +++++++++ .../identity/v3/application_credential.py | 2 + .../identity/v3/test_access_rule.py | 81 +++++++++++++++++++ .../unit/identity/v3/test_access_rule.py | 42 ++++++++++ .../v3/test_application_credential.py | 4 + .../tests/unit/identity/v3/test_proxy.py | 43 ++++++++++ .../add_access_rules-06eb8a1f9fcd9367.yaml | 5 ++ 8 files changed, 271 insertions(+) create mode 100644 openstack/identity/v3/access_rule.py create mode 100644 openstack/tests/functional/identity/v3/test_access_rule.py create mode 100644 openstack/tests/unit/identity/v3/test_access_rule.py create mode 100644 releasenotes/notes/add_access_rules-06eb8a1f9fcd9367.yaml diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index d772f75c0..aac0e8d9a 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -14,6 +14,7 @@ from openstack.identity.v3 import ( application_credential as _application_credential, ) +from openstack.identity.v3 import access_rule as _access_rule from openstack.identity.v3 import credential as _credential from openstack.identity.v3 import domain as _domain from openstack.identity.v3 import endpoint as _endpoint @@ -57,6 +58,7 @@ class Proxy(proxy.Proxy): _resource_registry = { "application_credential": _application_credential.ApplicationCredential, # noqa: E501 + "access_rule": _access_rule.AccessRule, "credential": _credential.Credential, "domain": _domain.Domain, "endpoint": _endpoint.Endpoint, @@ -2011,3 +2013,56 @@ def update_identity_provider(self, identity_provider, **attrs): return self._update( _identity_provider.IdentityProvider, identity_provider, **attrs ) + + # ========== Access rules ========== + + def access_rules(self, user, **query): + """Retrieve a generator of access rules + + :param user: Either the ID of a user or a :class:`~.user.User` + instance. + :param kwargs query: Optional query parameters to be sent to + limit the resources being returned. + + :returns: A generator of access rules instances. + :rtype: :class:`~openstack.identity.v3.access_rule.AccessRule` + """ + user = self._get_resource(_user.User, user) + return self._list(_access_rule.AccessRule, user_id=user.id, **query) + + def get_access_rule(self, user, access_rule): + """Get a single access rule + + :param user: Either the ID of a user or a :class:`~.user.User` + instance. + :param access rule: The value can be the ID of an access rule or a + :class:`~.access_rule.AccessRule` instance. + + :returns: One :class:`~.access_rule.AccessRule` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource can be found. + """ + user = self._get_resource(_user.User, user) + return self._get(_access_rule.AccessRule, access_rule, user_id=user.id) + + def delete_access_rule(self, user, access_rule, ignore_missing=True): + """Delete an access rule + + :param user: Either the ID of a user or a :class:`~.user.User` + instance. + :param access rule: The value can be either the ID of an + access rule or a :class:`~.access_rule.AccessRule` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the access rule does not exist. When set to ``True``, no exception + will be thrown when attempting to delete a nonexistent access rule. + + :returns: ``None`` + """ + user = self._get_resource(_user.User, user) + self._delete( + _access_rule.AccessRule, + access_rule, + user_id=user.id, + ignore_missing=ignore_missing, + ) diff --git a/openstack/identity/v3/access_rule.py b/openstack/identity/v3/access_rule.py new file mode 100644 index 000000000..ddd5d1658 --- /dev/null +++ b/openstack/identity/v3/access_rule.py @@ -0,0 +1,39 @@ +# 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 resource + + +class AccessRule(resource.Resource): + resource_key = 'access_rule' + resources_key = 'access_rules' + base_path = '/users/%(user_id)s/access_rules' + + # capabilities + allow_fetch = True + allow_delete = True + allow_list = True + + # Properties + #: The links for the access rule resource. + links = resource.Body('links') + #: Method that application credential is permitted to use. + # *Type: string* + method = resource.Body('method') + #: Path that the application credential is permitted to access. + # *Type: string* + path = resource.Body('path') + #: Service type identifier that application credential had access. + # *Type: string* + service = resource.Body('service') + #: User ID using access rule. *Type: string* + user_id = resource.URI('user_id') diff --git a/openstack/identity/v3/application_credential.py b/openstack/identity/v3/application_credential.py index a3876502f..8548aeedc 100644 --- a/openstack/identity/v3/application_credential.py +++ b/openstack/identity/v3/application_credential.py @@ -47,3 +47,5 @@ class ApplicationCredential(resource.Resource): unrestricted = resource.Body('unrestricted', type=bool) #: ID of project. *Type: string* project_id = resource.Body('project_id') + #: access rules for application credential. *Type: list* + access_rules = resource.Body('access_rules') diff --git a/openstack/tests/functional/identity/v3/test_access_rule.py b/openstack/tests/functional/identity/v3/test_access_rule.py new file mode 100644 index 000000000..cdcec1a36 --- /dev/null +++ b/openstack/tests/functional/identity/v3/test_access_rule.py @@ -0,0 +1,81 @@ +# 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.tests.functional import base + + +class TestAccessRule(base.BaseFunctionalTest): + def setUp(self): + super().setUp() + self.user_id = self.operator_cloud.current_user_id + + def _create_application_credential_with_access_rule(self): + """create application credential with access_rule.""" + + app_cred = self.conn.identity.create_application_credential( + user=self.user_id, + name='app_cred', + access_rules=[ + { + "path": "/v2.0/metrics", + "service": "monitoring", + "method": "GET", + } + ], + ) + self.addCleanup( + self.conn.identity.delete_application_credential, + self.user_id, + app_cred['id'], + ) + return app_cred + + def test_get_access_rule(self): + app_cred = self._create_application_credential_with_access_rule() + access_rule_id = app_cred['access_rules'][0]['id'] + access_rule = self.conn.identity.get_access_rule( + user=self.user_id, access_rule=access_rule_id + ) + self.assertEqual(access_rule['id'], access_rule_id) + self.assertEqual(access_rule['user_id'], self.user_id) + + def test_list_access_rules(self): + app_cred = self._create_application_credential_with_access_rule() + access_rule_id = app_cred['access_rules'][0]['id'] + access_rules = self.conn.identity.access_rules(user=self.user_id) + self.assertEqual(1, len(list(access_rules))) + for access_rule in access_rules: + self.assertEqual(app_cred['user_id'], self.user_id) + self.assertEqual(access_rule_id, access_rule['id']) + + def test_delete_access_rule(self): + app_cred = self._create_application_credential_with_access_rule() + access_rule_id = app_cred['access_rules'][0]['id'] + + # This is expected to raise an exception since access_rule is still + # in use for app_cred. + self.assertRaises( + exceptions.HttpException, + self.conn.identity.delete_access_rule, + user=self.user_id, + access_rule=access_rule_id, + ) + + # delete application credential first to delete access rule + self.conn.identity.delete_application_credential( + user=self.user_id, application_credential=app_cred['id'] + ) + # delete orphaned access rules + self.conn.identity.delete_access_rule( + user=self.user_id, access_rule=access_rule_id + ) diff --git a/openstack/tests/unit/identity/v3/test_access_rule.py b/openstack/tests/unit/identity/v3/test_access_rule.py new file mode 100644 index 000000000..95aa6275b --- /dev/null +++ b/openstack/tests/unit/identity/v3/test_access_rule.py @@ -0,0 +1,42 @@ +# 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.identity.v3 import access_rule +from openstack.tests.unit import base + +EXAMPLE = { + "links": { + "self": "https://example.com/identity/v3/access_rules" + "/07d719df00f349ef8de77d542edf010c" + }, + "path": "/v2.1/servers/{server_id}/ips", + "method": "GET", + "service": "compute", +} + + +class TestAccessRule(base.TestCase): + def test_basic(self): + sot = access_rule.AccessRule() + self.assertEqual('access_rule', sot.resource_key) + self.assertEqual('access_rules', sot.resources_key) + self.assertEqual('/users/%(user_id)s/access_rules', sot.base_path) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = access_rule.AccessRule(**EXAMPLE) + self.assertEqual(EXAMPLE['path'], sot.path) + self.assertEqual(EXAMPLE['method'], sot.method) + self.assertEqual(EXAMPLE['service'], sot.service) + self.assertEqual(EXAMPLE['links'], sot.links) diff --git a/openstack/tests/unit/identity/v3/test_application_credential.py b/openstack/tests/unit/identity/v3/test_application_credential.py index 231759331..c4a3ba729 100644 --- a/openstack/tests/unit/identity/v3/test_application_credential.py +++ b/openstack/tests/unit/identity/v3/test_application_credential.py @@ -19,6 +19,9 @@ "name": 'monitoring', "secret": 'rEaqvJka48mpv', "roles": [{"name": "Reader"}], + "access_rules": [ + {"path": "/v2.0/metrics", "service": "monitoring", "method": "GET"}, + ], "expires_at": '2018-02-27T18:30:59Z', "description": "Application credential for monitoring", "unrestricted": "False", @@ -51,3 +54,4 @@ def test_make_it(self): self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['roles'], sot.roles) self.assertEqual(EXAMPLE['links'], sot.links) + self.assertEqual(EXAMPLE['access_rules'], sot.access_rules) diff --git a/openstack/tests/unit/identity/v3/test_proxy.py b/openstack/tests/unit/identity/v3/test_proxy.py index 4c40dff37..cee998dfe 100644 --- a/openstack/tests/unit/identity/v3/test_proxy.py +++ b/openstack/tests/unit/identity/v3/test_proxy.py @@ -13,6 +13,7 @@ import uuid from openstack.identity.v3 import _proxy +from openstack.identity.v3 import access_rule from openstack.identity.v3 import credential from openstack.identity.v3 import domain from openstack.identity.v3 import endpoint @@ -568,3 +569,45 @@ def test_validate_group_has_system_role(self): self.proxy._get_resource(role.Role, 'rid'), ], ) + + +class TestAccessRule(TestIdentityProxyBase): + def test_access_rule_delete(self): + self.verify_delete( + self.proxy.delete_access_rule, + access_rule.AccessRule, + False, + method_args=[], + method_kwargs={'user': USER_ID, 'access_rule': 'access_rule'}, + expected_args=['access_rule'], + expected_kwargs={'user_id': USER_ID}, + ) + + def test_access_rule_delete_ignore(self): + self.verify_delete( + self.proxy.delete_access_rule, + access_rule.AccessRule, + True, + method_args=[], + method_kwargs={'user': USER_ID, 'access_rule': 'access_rule'}, + expected_args=['access_rule'], + expected_kwargs={'user_id': USER_ID}, + ) + + def test_access_rule_get(self): + self.verify_get( + self.proxy.get_access_rule, + access_rule.AccessRule, + method_args=[], + method_kwargs={'user': USER_ID, 'access_rule': 'access_rule'}, + expected_args=['access_rule'], + expected_kwargs={'user_id': USER_ID}, + ) + + def test_access_rules(self): + self.verify_list( + self.proxy.access_rules, + access_rule.AccessRule, + method_kwargs={'user': USER_ID}, + expected_kwargs={'user_id': USER_ID}, + ) diff --git a/releasenotes/notes/add_access_rules-06eb8a1f9fcd9367.yaml b/releasenotes/notes/add_access_rules-06eb8a1f9fcd9367.yaml new file mode 100644 index 000000000..60ebf1c6b --- /dev/null +++ b/releasenotes/notes/add_access_rules-06eb8a1f9fcd9367.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added support for `access_rules + `_. From 6d04b0c3ced433f57a6a44158d975b143c03afc1 Mon Sep 17 00:00:00 2001 From: Rafael Castillo Date: Thu, 15 Sep 2022 11:25:52 -0700 Subject: [PATCH 3268/3836] compute: Adds shelve-offload support Change-Id: I74979ded4ded26382629611b5990229c9936e247 --- openstack/compute/v2/_proxy.py | 19 +++++++++++++++++++ openstack/compute/v2/server.py | 4 ++++ openstack/tests/unit/compute/v2/test_proxy.py | 8 ++++++++ .../tests/unit/compute/v2/test_server.py | 15 +++++++++++++++ .../add-shelve_offload-427f6550fc55e622.yaml | 4 ++++ 5 files changed, 50 insertions(+) create mode 100644 releasenotes/notes/add-shelve_offload-427f6550fc55e622.yaml diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 7d4eb1095..8eb7c5ec3 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -1135,6 +1135,25 @@ def shelve_server(self, server): server = self._get_resource(_server.Server, server) server.shelve(self) + def shelve_offload_server(self, server): + """Shelve-offloads, or removes, a server + + Data and resource associations are deleted. + + Policy defaults enable only users with administrative role or the owner + of the server to perform this operation. Cloud provides could change + this permission though. + + Note that in some clouds, shelved servers are automatically offloaded, + sometimes after a certain time period. + + :param server: Either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance. + :returns: None + """ + server = self._get_resource(_server.Server, server) + server.shelve_offload(self) + def unshelve_server(self, server): """Unshelves or restores a shelved server. diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index ddbc163e5..fb501e8eb 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -537,6 +537,10 @@ def shelve(self, session): body = {"shelve": None} self._action(session, body) + def shelve_offload(self, session): + body = {"shelveOffload": None} + self._action(session, body) + def unshelve(self, session, availability_zone=_sentinel, host=None): """ Unshelve -- Unshelve the server. diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index b0afd8dd9..7bbb11cf3 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -1224,6 +1224,14 @@ def test_server_shelve(self): expected_args=[self.proxy], ) + def test_server_shelve_offload(self): + self._verify( + "openstack.compute.v2.server.Server.shelve_offload", + self.proxy.shelve_offload_server, + method_args=["value"], + expected_args=[self.proxy], + ) + def test_server_unshelve(self): self._verify( "openstack.compute.v2.server.Server.unshelve", diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 33ca69d96..88a545579 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -461,6 +461,21 @@ def test_revert_resize(self): microversion=self.sess.default_microversion, ) + def test_shelve_offload(self): + sot = server.Server(**EXAMPLE) + + self.assertIsNone(sot.shelve_offload(self.sess)) + + url = 'servers/IDENTIFIER/action' + body = {"shelveOffload": None} + headers = {'Accept': ''} + self.sess.post.assert_called_with( + url, + json=body, + headers=headers, + microversion=self.sess.default_microversion, + ) + def test_create_image_header(self): sot = server.Server(**EXAMPLE) name = 'noo' diff --git a/releasenotes/notes/add-shelve_offload-427f6550fc55e622.yaml b/releasenotes/notes/add-shelve_offload-427f6550fc55e622.yaml new file mode 100644 index 000000000..0162c0e55 --- /dev/null +++ b/releasenotes/notes/add-shelve_offload-427f6550fc55e622.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Adds shelve_offload_server method to the compute proxy From a72d46a9554ba50a36539dd046a6fdbf73de2808 Mon Sep 17 00:00:00 2001 From: Nobuto Murata Date: Thu, 18 May 2023 11:06:59 +0900 Subject: [PATCH 3269/3836] Bump the chunk_size to use CPU more efficiently The chunk_size used for downloading images was 1KiB for some time. That is okay for relatively small images but the client side of CPU can be a bottleneck especially for large images. Bump the default chunk_size from 1KiB to 1MiB so we can use the client side CPU more efficiently. [1KiB chunk_size - current] $ time openstack image save IMAGE_689MB --file /dev/null real 0m16.633s user 0m12.633s sys 0m1.365s -> ~331 Mbps [1MiB chunk_size - patched] $ time openstack image save IMAGE_689MB --file /dev/null real 0m4.896s user 0m3.361s sys 0m0.724s -> ~1,125 Mbps Story: 2010759 Task: 48044 Change-Id: Ib877d292f8adbf2fa0c51065f2917b3f1e263483 --- examples/image/download.py | 4 ++-- openstack/cloud/_image.py | 4 ++-- openstack/image/_download.py | 4 +++- openstack/image/v1/_proxy.py | 4 ++-- openstack/image/v2/_proxy.py | 4 ++-- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/examples/image/download.py b/examples/image/download.py index d5513dff7..791a65235 100644 --- a/examples/image/download.py +++ b/examples/image/download.py @@ -35,9 +35,9 @@ def download_image_stream(conn): with open("myimage.qcow2", "wb") as local_image: response = conn.image.download_image(image, stream=True) - # Read only 1024 bytes of memory at a time until + # Read only 1 MiB of memory at a time until # all of the image data has been consumed. - for chunk in response.iter_content(chunk_size=1024): + for chunk in response.iter_content(chunk_size=1024 * 1024): # With each chunk, add it to the hash to be computed. md5.update(chunk) diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py index 1bd240fcb..8ddb5b750 100644 --- a/openstack/cloud/_image.py +++ b/openstack/cloud/_image.py @@ -121,7 +121,7 @@ def download_image( name_or_id, output_path=None, output_file=None, - chunk_size=1024, + chunk_size=1024 * 1024, ): """Download an image by name or ID @@ -132,7 +132,7 @@ def download_image( image data to. Only write() will be called on this object. Either this or output_path must be specified :param int chunk_size: size in bytes to read from the wire and buffer - at one time. Defaults to 1024 + at one time. Defaults to 1024 * 1024 = 1 MiB :returns: When output_path and output_file are not given - the bytes comprising the given Image when stream is False, otherwise a :class:`requests.Response` instance. When output_path or diff --git a/openstack/image/_download.py b/openstack/image/_download.py index cf0e687f0..efcecf73c 100644 --- a/openstack/image/_download.py +++ b/openstack/image/_download.py @@ -25,7 +25,9 @@ def _verify_checksum(md5, checksum): class DownloadMixin: - def download(self, session, stream=False, output=None, chunk_size=1024): + def download( + self, session, stream=False, output=None, chunk_size=1024 * 1024 + ): """Download the data contained in an image""" # TODO(briancurtin): This method should probably offload the get # operation into another thread or something of that nature. diff --git a/openstack/image/v1/_proxy.py b/openstack/image/v1/_proxy.py index 522d99a37..3f894b68e 100644 --- a/openstack/image/v1/_proxy.py +++ b/openstack/image/v1/_proxy.py @@ -386,7 +386,7 @@ def download_image( image, stream=False, output=None, - chunk_size=1024, + chunk_size=1024 * 1024, ): """Download an image @@ -411,7 +411,7 @@ def download_image( When ``False``, return the entire contents of the response. :param output: Either a file object or a path to store data into. :param int chunk_size: size in bytes to read from the wire and buffer - at one time. Defaults to 1024 + at one time. Defaults to 1024 * 1024 = 1 MiB :returns: When output is not given - the bytes comprising the given Image when stream is False, otherwise a :class:`requests.Response` diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 6c6b50249..bf7e0b81f 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -776,7 +776,7 @@ def download_image( *, stream=False, output=None, - chunk_size=1024, + chunk_size=1024 * 1024, ): """Download an image @@ -800,7 +800,7 @@ def download_image( When ``False``, return the entire contents of the response. :param output: Either a file object or a path to store data into. :param int chunk_size: size in bytes to read from the wire and buffer - at one time. Defaults to 1024 + at one time. Defaults to 1024 * 1024 = 1 MiB :returns: When output is not given - the bytes comprising the given Image when stream is False, otherwise a :class:`requests.Response` From 1aa942500c0957e9dc49affa04c70a874dac55ba Mon Sep 17 00:00:00 2001 From: Polina-Gubina Date: Thu, 11 May 2023 18:08:55 +0200 Subject: [PATCH 3270/3836] Add find_share() for shared file system share resource Change-Id: I5e6af38d47735eeb983d06c43ad2f9c9be9a6c15 --- .../user/proxies/shared_file_system.rst | 2 +- openstack/shared_file_system/v2/_proxy.py | 20 +++++++++++++++++++ openstack/shared_file_system/v2/share.py | 2 +- .../shared_file_system/test_share.py | 5 +++++ .../unit/shared_file_system/v2/test_proxy.py | 3 +++ 5 files changed, 30 insertions(+), 2 deletions(-) diff --git a/doc/source/user/proxies/shared_file_system.rst b/doc/source/user/proxies/shared_file_system.rst index a3cd6d128..1887f2668 100644 --- a/doc/source/user/proxies/shared_file_system.rst +++ b/doc/source/user/proxies/shared_file_system.rst @@ -33,7 +33,7 @@ service. .. autoclass:: openstack.shared_file_system.v2._proxy.Proxy :noindex: :members: shares, get_share, delete_share, update_share, create_share, - revert_share_to_snapshot, resize_share + revert_share_to_snapshot, resize_share, find_share Shared File System Storage Pools diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index 8402d49dd..52495e4ec 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -109,6 +109,26 @@ def shares(self, details=True, **query): base_path = '/shares/detail' if details else None return self._list(_share.Share, base_path=base_path, **query) + def find_share(self, name_or_id, ignore_missing=True, **query): + """Find a single share + + :param name_or_id: The name or ID of a share. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict query: Any additional parameters to be passed into + underlying methods. such as query filters. + + :returns: One :class:`~openstack.shared_file_system.v2.share.Share` + or None + """ + + return self._find( + _share.Share, name_or_id, ignore_missing=ignore_missing, **query + ) + def get_share(self, share_id): """Lists details of a single share diff --git a/openstack/shared_file_system/v2/share.py b/openstack/shared_file_system/v2/share.py index cb13b07b0..79f9c3fa4 100644 --- a/openstack/shared_file_system/v2/share.py +++ b/openstack/shared_file_system/v2/share.py @@ -24,9 +24,9 @@ class Share(resource.Resource): allow_create = True allow_fetch = True allow_commit = True - allow_delete = True allow_list = True allow_head = False + allow_delete = True #: Properties #: The share instance access rules status. A valid value is active, diff --git a/openstack/tests/functional/shared_file_system/test_share.py b/openstack/tests/functional/shared_file_system/test_share.py index f51ac17b5..8112b5ea7 100644 --- a/openstack/tests/functional/shared_file_system/test_share.py +++ b/openstack/tests/functional/shared_file_system/test_share.py @@ -36,6 +36,11 @@ def test_get(self): assert isinstance(sot, _share.Share) self.assertEqual(self.SHARE_ID, sot.id) + def test_find(self): + sot = self.user_cloud.share.find_share(name_or_id=self.SHARE_NAME) + assert isinstance(sot, _share.Share) + self.assertEqual(self.SHARE_ID, sot.id) + def test_list_share(self): shares = self.user_cloud.share.shares(details=False) self.assertGreater(len(list(shares)), 0) diff --git a/openstack/tests/unit/shared_file_system/v2/test_proxy.py b/openstack/tests/unit/shared_file_system/v2/test_proxy.py index b3516073e..18ed60a50 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_proxy.py +++ b/openstack/tests/unit/shared_file_system/v2/test_proxy.py @@ -55,6 +55,9 @@ def test_shares_not_detailed(self): def test_share_get(self): self.verify_get(self.proxy.get_share, share.Share) + def test_share_find(self): + self.verify_find(self.proxy.find_share, share.Share) + def test_share_delete(self): self.verify_delete(self.proxy.delete_share, share.Share, False) From a30f9562dfe146260611720ac961481be56fb2a6 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 19 Dec 2022 17:52:15 +0000 Subject: [PATCH 3271/3836] Add 'callback' to 'wait_for_delete', 'wait_for_status' This is helpful for OSC. We also use the opportunity to clean up the tests for these two functions since they were fairly janky. Change-Id: I559e6341b15041cb40fe208439da44c66b7cc6ca Signed-off-by: Stephen Finucane --- openstack/block_storage/v2/_proxy.py | 32 ++- openstack/block_storage/v3/_proxy.py | 25 +- openstack/compute/v2/_proxy.py | 13 +- openstack/resource.py | 18 +- .../tests/unit/block_storage/v2/test_proxy.py | 1 + .../tests/unit/block_storage/v3/test_proxy.py | 1 + openstack/tests/unit/compute/v2/test_proxy.py | 1 + openstack/tests/unit/test_resource.py | 264 ++++++++++++++---- ...elete_callback_param-68d30161e23340bb.yaml | 8 + tox.ini | 2 +- 10 files changed, 295 insertions(+), 70 deletions(-) create mode 100644 releasenotes/notes/wait_for_status_delete_callback_param-68d30161e23340bb.yaml diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index ee31bbf1d..1957b4cf3 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -610,7 +610,13 @@ def reset_backup(self, backup, status): backup.reset(self, status) def wait_for_status( - self, res, status='available', failures=None, interval=2, wait=120 + self, + res, + status='available', + failures=None, + interval=2, + wait=120, + callback=None, ): """Wait for a resource to be in a particular status. @@ -624,6 +630,9 @@ def wait_for_status( checks. Default to 2. :param wait: Maximum number of seconds to wait before the change. Default to 120. + :param callback: A callback function. This will be called with a single + value, progress. + :returns: The resource is returned on success. :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition to the desired status failed to occur in specified seconds. @@ -634,10 +643,16 @@ def wait_for_status( """ failures = ['error'] if failures is None else failures return resource.wait_for_status( - self, res, status, failures, interval, wait + self, + res, + status, + failures, + interval, + wait, + callback=callback, ) - def wait_for_delete(self, res, interval=2, wait=120): + def wait_for_delete(self, res, interval=2, wait=120, callback=None): """Wait for a resource to be deleted. :param res: The resource to wait on to be deleted. @@ -646,11 +661,20 @@ def wait_for_delete(self, res, interval=2, wait=120): checks. Default to 2. :param wait: Maximum number of seconds to wait before the change. Default to 120. + :param callback: A callback function. This will be called with a single + value, progress. + :returns: The resource is returned on success. :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition to delete failed to occur in the specified seconds. """ - return resource.wait_for_delete(self, res, interval, wait) + return resource.wait_for_delete( + self, + res, + interval, + wait, + callback=callback, + ) def get_quota_set(self, project, usage=False, **query): """Show QuotaSet information for the project diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index ec2147a0d..642e74049 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -1584,6 +1584,7 @@ def wait_for_status( failures=None, interval=2, wait=120, + callback=None, ): """Wait for a resource to be in a particular status. @@ -1596,6 +1597,9 @@ def wait_for_status( checks. Default to 2. :param wait: Maximum number of seconds to wait before the change. Default to 120. + :param callback: A callback function. This will be called with a single + value, progress. + :returns: The resource is returned on success. :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition to the desired status failed to occur in specified seconds. @@ -1606,10 +1610,16 @@ def wait_for_status( """ failures = ['error'] if failures is None else failures return resource.wait_for_status( - self, res, status, failures, interval, wait + self, + res, + status, + failures, + interval, + wait, + callback=callback, ) - def wait_for_delete(self, res, interval=2, wait=120): + def wait_for_delete(self, res, interval=2, wait=120, callback=None): """Wait for a resource to be deleted. :param res: The resource to wait on to be deleted. @@ -1618,11 +1628,20 @@ def wait_for_delete(self, res, interval=2, wait=120): checks. Default to 2. :param int wait: Maximum number of seconds to wait before the change. Default to 120. + :param callback: A callback function. This will be called with a single + value, progress. + :returns: The resource is returned on success. :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition to delete failed to occur in the specified seconds. """ - return resource.wait_for_delete(self, res, interval, wait) + return resource.wait_for_delete( + self, + res, + interval, + wait, + callback=callback, + ) def _get_cleanup_dependencies(self): return {'block_storage': {'before': []}} diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 8eb7c5ec3..a22e68776 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -2468,6 +2468,7 @@ def wait_for_server( failures=None, interval=2, wait=120, + callback=None, ): """Wait for a server to be in a particular status. @@ -2484,6 +2485,10 @@ def wait_for_server( :param wait: Maximum number of seconds to wait before the change. Default to 120. :type wait: int + :param callback: A callback function. This will be called with a single + value, progress, which is a percentage value from 0-100. + :type callback: callable + :returns: The resource is returned on success. :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition to the desired status failed to occur in specified seconds. @@ -2500,9 +2505,10 @@ def wait_for_server( failures, interval, wait, + callback=callback, ) - def wait_for_delete(self, res, interval=2, wait=120): + def wait_for_delete(self, res, interval=2, wait=120, callback=None): """Wait for a resource to be deleted. :param res: The resource to wait on to be deleted. @@ -2511,11 +2517,14 @@ def wait_for_delete(self, res, interval=2, wait=120): checks. Default to 2. :param wait: Maximum number of seconds to wait before the change. Default to 120. + :param callback: A callback function. This will be called with a single + value, progress, which is a percentage value from 0-100. + :returns: The resource is returned on success. :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition to delete failed to occur in the specified seconds. """ - return resource.wait_for_delete(self, res, interval, wait) + return resource.wait_for_delete(self, res, interval, wait, callback) def _get_cleanup_dependencies(self): return { diff --git a/openstack/resource.py b/openstack/resource.py index 3443f584e..bfa39fa59 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -2329,6 +2329,7 @@ def wait_for_status( interval=None, wait=None, attribute='status', + callback=None, ): """Wait for the resource to be in a particular status. @@ -2345,6 +2346,9 @@ def wait_for_status( :param wait: Maximum number of seconds to wait for transition. Set to ``None`` to wait forever. :param attribute: Name of the resource attribute that contains the status. + :param callback: A callback function. This will be called with a single + value, progress. This is API specific but is generally a percentage + value from 0-100. :return: The updated resource. :raises: :class:`~openstack.exceptions.ResourceTimeout` transition @@ -2372,7 +2376,6 @@ def wait_for_status( timeout=wait, message=msg, wait=interval ): resource = resource.fetch(session, skip_cache=True) - if not resource: raise exceptions.ResourceFailure( "{name} went away while waiting for {status}".format( @@ -2399,8 +2402,12 @@ def wait_for_status( new_status, ) + if callback: + progress = getattr(resource, 'progress', None) or 0 + callback(progress) + -def wait_for_delete(session, resource, interval, wait): +def wait_for_delete(session, resource, interval, wait, callback=None): """Wait for the resource to be deleted. :param session: The session to use for making this request. @@ -2409,6 +2416,9 @@ def wait_for_delete(session, resource, interval, wait): :type resource: :class:`~openstack.resource.Resource` :param interval: Number of seconds to wait between checks. :param wait: Maximum number of seconds to wait for the delete. + :param callback: A callback function. This will be called with a single + value, progress. This is API specific but is generally a percentage + value from 0-100. :return: Method returns self on success. :raises: :class:`~openstack.exceptions.ResourceTimeout` transition @@ -2430,3 +2440,7 @@ def wait_for_delete(session, resource, interval, wait): return resource except exceptions.NotFoundException: return orig_resource + + if callback: + progress = getattr(resource, 'progress', None) or 0 + callback(progress) diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index 0dad6787c..4bfe0c46e 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -121,6 +121,7 @@ def test_volume_wait_for(self): self.proxy.wait_for_status, method_args=[value], expected_args=[self.proxy, value, 'available', ['error'], 2, 120], + expected_kwargs={'callback': None}, ) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 38638205b..2efe2fbfc 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -123,6 +123,7 @@ def test_volume_wait_for(self): self.proxy.wait_for_status, method_args=[value], expected_args=[self.proxy, value, 'available', ['error'], 2, 120], + expected_kwargs={'callback': None}, ) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 7bbb11cf3..ecebf919c 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -963,6 +963,7 @@ def test_server_wait_for(self): self.proxy.wait_for_server, method_args=[value], expected_args=[self.proxy, value, 'ACTIVE', ['ERROR'], 2, 120], + expected_kwargs={'callback': None}, ) def test_server_resize(self): diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 67a4fcad6..e23f7fc1d 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -12,6 +12,7 @@ import itertools import json +import logging from unittest import mock from keystoneauth1 import adapter @@ -3421,14 +3422,57 @@ def test_list_base_path(self): ) -class TestWaitForStatus(base.TestCase): +class TestWait(base.TestCase): + def setUp(self): + super().setUp() + + handler = logging.StreamHandler(self._log_stream) + formatter = logging.Formatter('%(asctime)s %(name)-32s %(message)s') + handler.setFormatter(formatter) + + logger = logging.getLogger('openstack.iterate_timeout') + logger.setLevel(logging.DEBUG) + logger.addHandler(handler) + + @staticmethod + def _fake_resource(statuses=None, progresses=None, *, attribute='status'): + if statuses is None: + statuses = ['building', 'building', 'building', 'active'] + + def fetch(*args, **kwargs): + # when we get to the last status, keep returning that + if statuses: + setattr(fake_resource, attribute, statuses.pop(0)) + + if progresses: + fake_resource.progress = progresses.pop(0) + + return fake_resource + + spec = ['id', attribute, 'fetch'] + if progresses: + spec.append('progress') + + fake_resource = mock.Mock(spec=spec) + setattr(fake_resource, attribute, statuses.pop(0)) + fake_resource.fetch.side_effect = fetch + + return fake_resource + + +class TestWaitForStatus(TestWait): def test_immediate_status(self): status = "loling" res = mock.Mock(spec=['id', 'status']) res.status = status result = resource.wait_for_status( - self.cloud.compute, res, status, "failures", "interval", "wait" + self.cloud.compute, + res, + status, + None, + interval=1, + wait=1, ) self.assertEqual(res, result) @@ -3439,7 +3483,12 @@ def test_immediate_status_case(self): res.status = status result = resource.wait_for_status( - self.cloud.compute, res, 'lOling', "failures", "interval", "wait" + self.cloud.compute, + res, + 'lOling', + None, + interval=1, + wait=1, ) self.assertEqual(res, result) @@ -3453,127 +3502,131 @@ def test_immediate_status_different_attribute(self): self.cloud.compute, res, status, - "failures", - "interval", - "wait", + None, + interval=1, + wait=1, attribute='mood', ) self.assertEqual(res, result) - def _resources_from_statuses(self, *statuses, **kwargs): - attribute = kwargs.pop('attribute', 'status') - assert not kwargs, 'Unexpected keyword arguments: %s' % kwargs - resources = [] - for status in statuses: - res = mock.Mock(spec=['id', 'fetch', attribute]) - setattr(res, attribute, status) - resources.append(res) - for index, res in enumerate(resources[:-1]): - res.fetch.return_value = resources[index + 1] - return resources - def test_status_match(self): status = "loling" # other gets past the first check, two anothers gets through # the sleep loop, and the third matches - resources = self._resources_from_statuses( - "first", "other", "another", "another", status - ) + statuses = ["first", "other", "another", "another", status] + res = self._fake_resource(statuses) result = resource.wait_for_status( - mock.Mock(), resources[0], status, None, 1, 5 + mock.Mock(), + res, + status, + None, + interval=1, + wait=5, ) - self.assertEqual(result, resources[-1]) + self.assertEqual(result, res) def test_status_match_with_none(self): status = "loling" # apparently, None is a correct state in some cases - resources = self._resources_from_statuses( - None, "other", None, "another", status - ) + statuses = [None, "other", None, "another", status] + res = self._fake_resource(statuses) result = resource.wait_for_status( - mock.Mock(), resources[0], status, None, 1, 5 + mock.Mock(), + res, + status, + None, + interval=1, + wait=5, ) - self.assertEqual(result, resources[-1]) + self.assertEqual(result, res) def test_status_match_none(self): status = None # apparently, None can be expected status in some cases - resources = self._resources_from_statuses( - "first", "other", "another", "another", status - ) + statuses = ["first", "other", "another", "another", status] + res = self._fake_resource(statuses) result = resource.wait_for_status( - mock.Mock(), resources[0], status, None, 1, 5 + mock.Mock(), + res, + status, + None, + interval=1, + wait=5, ) - self.assertEqual(result, resources[-1]) + self.assertEqual(result, res) def test_status_match_different_attribute(self): status = "loling" - resources = self._resources_from_statuses( - "first", "other", "another", "another", status, attribute='mood' - ) + statuses = ["first", "other", "another", "another", status] + res = self._fake_resource(statuses, attribute='mood') result = resource.wait_for_status( - mock.Mock(), resources[0], status, None, 1, 5, attribute='mood' + mock.Mock(), + res, + status, + None, + interval=1, + wait=5, + attribute='mood', ) - self.assertEqual(result, resources[-1]) + self.assertEqual(result, res) def test_status_fails(self): failure = "crying" - resources = self._resources_from_statuses("success", "other", failure) + statuses = ["success", "other", failure] + res = self._fake_resource(statuses) self.assertRaises( exceptions.ResourceFailure, resource.wait_for_status, mock.Mock(), - resources[0], + res, "loling", [failure], - 1, - 5, + interval=1, + wait=5, ) def test_status_fails_different_attribute(self): failure = "crying" - resources = self._resources_from_statuses( - "success", "other", failure, attribute='mood' - ) + statuses = ["success", "other", failure] + res = self._fake_resource(statuses, attribute='mood') self.assertRaises( exceptions.ResourceFailure, resource.wait_for_status, mock.Mock(), - resources[0], + res, "loling", [failure.upper()], - 1, - 5, + interval=1, + wait=5, attribute='mood', ) def test_timeout(self): status = "loling" - res = mock.Mock() # The first "other" gets past the first check, and then three # pairs of "other" statuses run through the sleep counter loop, # after which time should be up. This is because we have a # one second interval and three second waiting period. statuses = ["other"] * 7 - type(res).status = mock.PropertyMock(side_effect=statuses) + res = self._fake_resource(statuses) self.assertRaises( exceptions.ResourceTimeout, @@ -3587,9 +3640,8 @@ def test_timeout(self): ) def test_no_sleep(self): - res = mock.Mock() statuses = ["other"] - type(res).status = mock.PropertyMock(side_effect=statuses) + res = self._fake_resource(statuses) self.assertRaises( exceptions.ResourceTimeout, @@ -3598,20 +3650,63 @@ def test_no_sleep(self): res, "status", None, - 0, - -1, + interval=0, + wait=-1, ) + def test_callback(self): + """Callback is called with 'progress' attribute.""" + statuses = ['building', 'building', 'building', 'building', 'active'] + progresses = [0, 25, 50, 100] + res = self._fake_resource(statuses=statuses, progresses=progresses) + + callback = mock.Mock() + + result = resource.wait_for_status( + mock.Mock(), + res, + 'active', + None, + interval=0.1, + wait=1, + callback=callback, + ) + + self.assertEqual(result, res) + callback.assert_has_calls([mock.call(x) for x in progresses]) + + def test_callback_without_progress(self): + """Callback is called with 0 if 'progress' attribute is missing.""" + statuses = ['building', 'building', 'building', 'building', 'active'] + res = self._fake_resource(statuses=statuses) + + callback = mock.Mock() -class TestWaitForDelete(base.TestCase): - def test_success(self): + result = resource.wait_for_status( + mock.Mock(), + res, + 'active', + None, + interval=0.1, + wait=1, + callback=callback, + ) + + self.assertEqual(result, res) + # there are 5 statuses but only 3 callback calls since the initial + # status and final status don't result in calls + callback.assert_has_calls([mock.call(0)] * 3) + + +class TestWaitForDelete(TestWait): + def test_success_not_found(self): response = mock.Mock() response.headers = {} response.status_code = 404 res = mock.Mock() res.fetch.side_effect = [ - None, - None, + res, + res, exceptions.ResourceNotFound('Not Found', response), ] @@ -3619,6 +3714,59 @@ def test_success(self): self.assertEqual(result, res) + def test_status(self): + """Successful deletion indicated by status.""" + statuses = ['active', 'deleting', 'deleting', 'deleting', 'deleted'] + res = self._fake_resource(statuses=statuses) + + result = resource.wait_for_delete( + mock.Mock(), + res, + interval=0.1, + wait=1, + ) + + self.assertEqual(result, res) + + def test_callback(self): + """Callback is called with 'progress' attribute.""" + statuses = ['active', 'deleting', 'deleting', 'deleting', 'deleted'] + progresses = [0, 25, 50, 100] + res = self._fake_resource(statuses=statuses, progresses=progresses) + + callback = mock.Mock() + + result = resource.wait_for_delete( + mock.Mock(), + res, + interval=1, + wait=5, + callback=callback, + ) + + self.assertEqual(result, res) + callback.assert_has_calls([mock.call(x) for x in progresses]) + + def test_callback_without_progress(self): + """Callback is called with 0 if 'progress' attribute is missing.""" + statuses = ['active', 'deleting', 'deleting', 'deleting', 'deleted'] + res = self._fake_resource(statuses=statuses) + + callback = mock.Mock() + + result = resource.wait_for_delete( + mock.Mock(), + res, + interval=1, + wait=5, + callback=callback, + ) + + self.assertEqual(result, res) + # there are 5 statuses but only 3 callback calls since the initial + # status and final status don't result in calls + callback.assert_has_calls([mock.call(0)] * 3) + def test_timeout(self): res = mock.Mock() res.status = 'ACTIVE' diff --git a/releasenotes/notes/wait_for_status_delete_callback_param-68d30161e23340bb.yaml b/releasenotes/notes/wait_for_status_delete_callback_param-68d30161e23340bb.yaml new file mode 100644 index 000000000..1e088ef05 --- /dev/null +++ b/releasenotes/notes/wait_for_status_delete_callback_param-68d30161e23340bb.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + The ``Resource.wait_for_status``, ``Resource.wait_for_delete``, and related + proxy wrappers now accept a ``callback`` argument that can be used to pass + a callback function. When provided, the wait function will attempt to + retrieve a ``progress`` value from the resource in question and pass it to + the callback function each time it iterates. diff --git a/tox.ini b/tox.ini index 13a0c3a3b..54936d374 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,7 @@ setenv = LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=C - OS_LOG_CAPTURE={env:OS_LOG_CAPTURE:false} + OS_LOG_CAPTURE={env:OS_LOG_CAPTURE:true} OS_STDOUT_CAPTURE={env:OS_STDOUT_CAPTURE:true} OS_STDERR_CAPTURE={env:OS_STDERR_CAPTURE:true} deps = From ad1a00e8389e1a99b100200123d63c3e558a376b Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 18 May 2023 12:51:34 +0100 Subject: [PATCH 3272/3836] tests: Silence warning Clean test output ftw. Change-Id: I4686e23a73e88f4aa4579944413c532216b3039d Signed-off-by: Stephen Finucane --- openstack/tests/unit/test_missing_version.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openstack/tests/unit/test_missing_version.py b/openstack/tests/unit/test_missing_version.py index 473a9594e..0f96c4f48 100644 --- a/openstack/tests/unit/test_missing_version.py +++ b/openstack/tests/unit/test_missing_version.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import fixtures import testtools from openstack import exceptions @@ -41,5 +42,8 @@ def test_unsupported_version(self): def test_unsupported_version_override(self): self.cloud.config.config['image_api_version'] = '7' - self.assertIsInstance(self.cloud.image, proxy.Proxy) + w = fixtures.WarningsCapture() + with w: + self.assertIsInstance(self.cloud.image, proxy.Proxy) + self.assertEqual(1, len(w.captures)) self.assert_calls() From b7b3f418e06b5b30b6ee97ec82f84cb6f7f715df Mon Sep 17 00:00:00 2001 From: elajkat Date: Mon, 22 May 2023 17:18:50 +0200 Subject: [PATCH 3273/3836] FWAAS: add Computed summary field to FirewallRule Needed-by: https://review.opendev.org/c/openstack/python-neutronclient/+/880629 Related-Bug: #1999774 Change-Id: I3cd12e2b3380b528c9be2266d5bfa28eb79feaeb --- openstack/network/v2/firewall_rule.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openstack/network/v2/firewall_rule.py b/openstack/network/v2/firewall_rule.py index da2df0d95..04a84eadb 100644 --- a/openstack/network/v2/firewall_rule.py +++ b/openstack/network/v2/firewall_rule.py @@ -73,6 +73,10 @@ class FirewallRule(resource.Resource): source_ip_address = resource.Body('source_ip_address') #: The source port or port range for the firewall rule. source_port = resource.Body('source_port') + #: Summary field of a FirewallRule, composed of the protocol, + #: source_ip_address:source_port, + #: destination_ip_address:destination_port and action. + summary = resource.Computed('summary', default='') #: The ID of the firewall policy. firewall_policy_id = resource.Body('firewall_policy_id') #: The ID of the firewall rule. From 8567c858d5b909db81f85a74ecda6fc60b4cce3f Mon Sep 17 00:00:00 2001 From: Lars Kellogg-Stedman Date: Wed, 24 May 2023 23:52:36 -0400 Subject: [PATCH 3274/3836] Allow tags to be passed through to compute.create_server Setting tags on servers is useful for managing sets of related resources (e.g., we're spawning JupyterHub instances in OpenStack and we want to tag all servers created by our spawner to make it easier to clean things up). This commit configures the create_server method to accept the tags keyword paramter and pass it through to compute.create_server. Change-Id: I92a449e584f9912f2bff86939e3565c6b3001bb9 --- openstack/cloud/_compute.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 81be3f31c..9cfaedcca 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -750,6 +750,7 @@ def get_server_meta(self, server): 'config_drive', 'admin_pass', 'disk_config', + 'tags', ) def create_server( self, From 68ecfaccaeadaeb7bd1b2df27d3f71b76e0799e6 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 25 May 2023 11:53:51 +0100 Subject: [PATCH 3275/3836] tests: Isolate tests from OS_CLOUD Currently, if you run unit tests when the OS_CLOUD envvar is set, a number of tests will fail because the cloud doesn't exist in the clouds.yaml file that the tests are creating and using. Resolve this by unsetting the variable. Change-Id: Ide9b61ef2b66fe278e74e6404caf93a00362df33 Signed-off-by: Stephen Finucane --- openstack/tests/unit/base.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 36f067c10..36091f4fc 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -105,9 +105,13 @@ def _nosleep(seconds): ) # Isolate openstack.config from test environment + self.os_cloud_fixture = self.useFixture( + fixtures.EnvironmentVariable('OS_CLOUD'), + ) config = tempfile.NamedTemporaryFile(delete=False) - cloud_path = '%s/clouds/%s' % ( + cloud_path = os.path.join( self.fixtures_directory, + 'clouds', cloud_config_fixture, ) with open(cloud_path, 'rb') as f: From 71c25d2626427495fc34d52d9c0d65ee9f19720d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 25 May 2023 11:40:08 +0100 Subject: [PATCH 3276/3836] exceptions: Remove unused exception Change I0316174e9c9fdf1f418e3728b5a96d46158edb97, way back in 2019, removed the only user of this exception. Change-Id: Ib31adef67cf32d91d7cb8c5ab689c8c9cac8b1cc Signed-off-by: Stephen Finucane --- openstack/exceptions.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openstack/exceptions.py b/openstack/exceptions.py index a8f12ef97..e178b07a1 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -291,10 +291,6 @@ class ValidationException(SDKException): """Validation failed for resource.""" -class TaskManagerStopped(SDKException): - """Operations were attempted on a stopped TaskManager.""" - - class ServiceDisabledException(ConfigException): """This service is disabled for reasons.""" From dc52fd4978424ae9f55b3a602572dce6a291407b Mon Sep 17 00:00:00 2001 From: Maurice Escher Date: Thu, 2 Mar 2023 18:34:35 +0100 Subject: [PATCH 3277/3836] cloud: Filter FIPs by valid filters 'network' was the attribute name normalized for nova: we should be using 'floating_network_id'. Ditto for 'port' and 'port_id'. Change-Id: I4cfdccd4f2e5a1df33c7eca5c97ba2ed55f9ee0f Story: 2010627 Task: 47572 --- openstack/cloud/_floating_ip.py | 6 +++--- .../tests/functional/cloud/test_floating_ip.py | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index 30b1f32e7..d42843483 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -284,9 +284,9 @@ def _neutron_available_floating_ips( floating_network_id = self._get_floating_network_id() filters = { - 'port': None, - 'network': floating_network_id, - 'location': {'project': {'id': project_id}}, + 'port_id': None, + 'floating_network_id': floating_network_id, + 'project_id': project_id, } floating_ips = self._list_floating_ips() diff --git a/openstack/tests/functional/cloud/test_floating_ip.py b/openstack/tests/functional/cloud/test_floating_ip.py index fdcda44ed..96df485c5 100644 --- a/openstack/tests/functional/cloud/test_floating_ip.py +++ b/openstack/tests/functional/cloud/test_floating_ip.py @@ -325,3 +325,18 @@ def test_get_floating_ip_by_id(self): ret_fip = self.user_cloud.get_floating_ip_by_id(fip_user.id) self.assertEqual(fip_user, ret_fip) + + def test_available_floating_ip(self): + fips_user = self.user_cloud.list_floating_ips() + self.assertEqual(fips_user, []) + + new_fip = self.user_cloud.available_floating_ip() + self.assertIsNotNone(new_fip) + self.assertIn('id', new_fip) + self.addCleanup(self.user_cloud.delete_floating_ip, new_fip.id) + + new_fips_user = self.user_cloud.list_floating_ips() + self.assertEqual(new_fips_user, [new_fip]) + + reuse_fip = self.user_cloud.available_floating_ip() + self.assertEqual(reuse_fip.id, new_fip.id) From 2091ec2a149958902eef0d85c8e4d94ad3ce49b0 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 18 May 2023 20:37:56 +0100 Subject: [PATCH 3278/3836] Migrate warnings to openstack.warnings There are two, one of which isn't used. Migrate the one that is. Change-Id: I6e3856834e178abc5146ecac6b1c9c5525fc1643 Signed-off-by: Stephen Finucane --- openstack/exceptions.py | 10 --------- openstack/service_description.py | 37 +++++++++++++++----------------- openstack/warnings.py | 4 ++++ 3 files changed, 21 insertions(+), 30 deletions(-) diff --git a/openstack/exceptions.py b/openstack/exceptions.py index a8f12ef97..343250513 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -269,16 +269,6 @@ def raise_from_response(response, error_message=None): ) -class UnsupportedServiceVersion(Warning): - """The user has configured a major version that SDK doesn't know.""" - - -class ArgumentDeprecationWarning(Warning): - """A deprecated argument has been provided.""" - - pass - - class ConfigException(SDKException): """Something went wrong with parsing your OpenStack Config.""" diff --git a/openstack/service_description.py b/openstack/service_description.py index 0cf7ec844..dab5d0598 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -18,6 +18,7 @@ from openstack import _log from openstack import exceptions from openstack import proxy as proxy_mod +from openstack import warnings as os_warnings __all__ = [ 'ServiceDescription', @@ -185,13 +186,11 @@ def _make_proxy(self, instance): ) else: warnings.warn( - "The configured version, {version} for service" - " {service_type} is not known or supported by" - " openstacksdk. The resulting Proxy object will only" - " have direct passthrough REST capabilities.".format( - version=version_string, service_type=self.service_type - ), - category=exceptions.UnsupportedServiceVersion, + f"The configured version, {version_string} for service " + f"{self.service_type} is not known or supported by " + f"openstacksdk. The resulting Proxy object will only " + f"have direct passthrough REST capabilities.", + category=os_warnings.UnsupportedServiceVersion, ) elif endpoint_override: temp_adapter = config.get_session_client(self.service_type) @@ -204,14 +203,12 @@ def _make_proxy(self, instance): ) else: warnings.warn( - "Service {service_type} has an endpoint override set" - " but the version discovered at that endpoint, {version}" - " is not supported by openstacksdk. The resulting Proxy" - " object will only have direct passthrough REST" - " capabilities.".format( - version=api_version, service_type=self.service_type - ), - category=exceptions.UnsupportedServiceVersion, + f"Service {self.service_type} has an endpoint override " + f"set but the version discovered at that endpoint, " + f"{api_version}, is not supported by openstacksdk. " + f"The resulting Proxy object will only have direct " + f"passthrough REST capabilities.", + category=os_warnings.UnsupportedServiceVersion, ) if proxy_obj: @@ -291,7 +288,7 @@ def _make_proxy(self, instance): self.service_type, allow_version_hack=True, constructor=proxy_class, - **version_kwargs + **version_kwargs, ) # No proxy_class @@ -300,12 +297,12 @@ def _make_proxy(self, instance): # service catalog that also doesn't have any useful # version discovery? warnings.warn( - "Service {service_type} has no discoverable version." - " The resulting Proxy object will only have direct" - " passthrough REST capabilities.".format( + "Service {service_type} has no discoverable version. " + "The resulting Proxy object will only have direct " + "passthrough REST capabilities.".format( service_type=self.service_type ), - category=exceptions.UnsupportedServiceVersion, + category=os_warnings.UnsupportedServiceVersion, ) return temp_adapter diff --git a/openstack/warnings.py b/openstack/warnings.py index b4be45549..29c3a6d55 100644 --- a/openstack/warnings.py +++ b/openstack/warnings.py @@ -29,3 +29,7 @@ class RemovedFieldWarning(OpenStackDeprecationWarning): class LegacyAPIWarning(OpenStackDeprecationWarning): """Indicates an API that is in 'legacy' status, a long term deprecation.""" + + +class UnsupportedServiceVersion(Warning): + """Indicates a major version that SDK doesn't understand.""" From 661efb7cedf702545dc6e2b99395dfcc1e5873ad Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 25 May 2023 11:14:20 +0100 Subject: [PATCH 3279/3836] cloud: Ignore invalid filters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'openstack.cloud._utils._filter_list' function accepts a list of filters. These filters are key-value pairs where the key indicates the attribute of the resources that we wish to filter on, and the value is either a simple value or another key-value pair to allow us to filter on nested attributes (turtles all the way down 🐢🌍). We were using '.get(key, None)' to retrieve the attribute from the resource, meaning we inadvertently allowed for filters on attributes that did not exist. If the attribute was not found, we treated this as a mismatch. This is certainly not what was intended and it's not necessary: because the cloud layer is now using the proxy layer rather than raw REST requests, and the proxy layer in-turn uses resources, we can ensure that a valid attribute will always be present even if it unset. As such, the current logic is incorrect and has allowed at least one bug to slip into our codebase. Correct this issue by checking for the presence of a resource attribute matching the key. If an attribute doesn't match, we now raise a warning and exception, as one would have expected. Change-Id: Id8756f679ec5130779d61d454455c9c25afe8baa Signed-off-by: Stephen Finucane Story: 2010627 Task: 48095 --- openstack/cloud/_utils.py | 24 +++++++++++++++++-- .../tests/functional/cloud/test_users.py | 2 +- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index 77f0600f7..dea5a3eaa 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -80,6 +80,7 @@ def _filter_list(data, name_or_id, filters): OR A string containing a jmespath expression for further filtering. + Invalid filters will be ignored. """ # The logger is openstack.cloud.fmmatch to allow a user/operator to # configure logging not to communicate about fnmatch misses @@ -131,6 +132,17 @@ def _dict_filter(f, d): if not d: return False for key in f.keys(): + if key not in d: + log.warning( + "Invalid filter: %s is not an attribute of %s.%s", + key, + e.__class__.__module__, + e.__class__.__qualname__, + ) + # we intentionally skip this since the user was trying to + # filter on _something_, but we don't know what that + # _something_ was + raise AttributeError(key) if isinstance(f[key], dict): if not _dict_filter(f[key], d.get(key, None)): return False @@ -142,11 +154,19 @@ def _dict_filter(f, d): for e in data: filtered.append(e) for key in filters.keys(): + if key not in e: + log.warning( + "Invalid filter: %s is not an attribute of %s.%s", + key, + e.__class__.__module__, + e.__class__.__qualname__, + ) + raise AttributeError(key) if isinstance(filters[key], dict): - if not _dict_filter(filters[key], e.get(key, None)): + if not _dict_filter(filters[key], e[key]): filtered.pop() break - elif e.get(key, None) != filters[key]: + elif e[key] != filters[key]: filtered.pop() break return filtered diff --git a/openstack/tests/functional/cloud/test_users.py b/openstack/tests/functional/cloud/test_users.py index 32e58b231..adefb6043 100644 --- a/openstack/tests/functional/cloud/test_users.py +++ b/openstack/tests/functional/cloud/test_users.py @@ -64,7 +64,7 @@ def test_get_user(self): self.assertEqual('admin', user['name']) def test_search_users(self): - users = self.operator_cloud.search_users(filters={'enabled': True}) + users = self.operator_cloud.search_users(filters={'is_enabled': True}) self.assertIsNotNone(users) def test_search_users_jmespath(self): From d77ec568d3e138348e828ec8c5099b7c14ed50c4 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 25 May 2023 11:29:52 +0100 Subject: [PATCH 3280/3836] cloud: Reduce duplication We were calling '_dict_filter' if we encountered a dictionary filter; however, this function is perfectly able to handle non-dictionary values. Use it. Change-Id: Ia7be0d18cd4c0fd19c57bf539162d439d85c83ed Signed-off-by: Stephen Finucane --- openstack/cloud/_utils.py | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index dea5a3eaa..b6b6bf686 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -152,23 +152,8 @@ def _dict_filter(f, d): filtered = [] for e in data: - filtered.append(e) - for key in filters.keys(): - if key not in e: - log.warning( - "Invalid filter: %s is not an attribute of %s.%s", - key, - e.__class__.__module__, - e.__class__.__qualname__, - ) - raise AttributeError(key) - if isinstance(filters[key], dict): - if not _dict_filter(filters[key], e[key]): - filtered.pop() - break - elif e[key] != filters[key]: - filtered.pop() - break + if _dict_filter(filters, e): + filtered.append(e) return filtered From ca770c5bcd00348c4dda1e7d9872814ddcc7712b Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 8 Jun 2022 14:37:03 +0200 Subject: [PATCH 3281/3836] cloud: Remove '_ShadeAdapter' Very few things require the JSON conversion functionality this provides nowadays (few TODO items outstanding). We can finally remove it. Change-Id: Iaff2a67913c47d6f807fe8d332ad98d0581823b0 Signed-off-by: Stephen Finucane --- doc/source/contributor/coding.rst | 11 ++++------- openstack/cloud/openstackcloud.py | 6 +++--- openstack/orchestration/util/event_utils.py | 9 +++++++-- openstack/proxy.py | 16 +++++----------- 4 files changed, 19 insertions(+), 23 deletions(-) diff --git a/doc/source/contributor/coding.rst b/doc/source/contributor/coding.rst index 67e9ea3eb..9572d53d4 100644 --- a/doc/source/contributor/coding.rst +++ b/doc/source/contributor/coding.rst @@ -62,15 +62,12 @@ The `openstack.cloud` layer has some specific rules: Returned Resources ------------------ -Complex objects returned to the caller must be a `munch.Munch` type. The -`openstack.proxy._ShadeAdapter` class makes resources into `munch.Munch`. +Complex objects returned to the caller must be a `openstack.resource.Resource` +type. -All objects should be normalized. It is shade's purpose in life to make +All objects should be normalized. It is openstacksdk's purpose in life to make OpenStack consistent for end users, and this means not trusting the clouds -to return consistent objects. There should be a normalize function in -`openstack/cloud/_normalize.py` that is applied to objects before returning -them to the user. See :doc:`../user/model` for further details on object model -requirements. +to return consistent objects. The `Resource` object should do this for us. Fields should not be in the normalization contract if we cannot commit to providing them to all users. diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index c68bdff72..5fd38f514 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -396,7 +396,7 @@ def _get_versioned_client( request_max_version = '{version}.latest'.format( version=config_major ) - adapter = proxy._ShadeAdapter( + adapter = proxy.Proxy( session=self.session, service_type=self.config.get_service_type(service_type), service_name=self.config.get_service_name(service_type), @@ -414,7 +414,7 @@ def _get_versioned_client( if adapter.get_endpoint(): return adapter - adapter = proxy._ShadeAdapter( + adapter = proxy.Proxy( session=self.session, service_type=self.config.get_service_type(service_type), service_name=self.config.get_service_name(service_type), @@ -457,7 +457,7 @@ def _get_versioned_client( def _get_raw_client( self, service_type, api_version=None, endpoint_override=None ): - return proxy._ShadeAdapter( + return proxy.Proxy( session=self.session, service_type=self.config.get_service_type(service_type), service_name=self.config.get_service_name(service_type), diff --git a/openstack/orchestration/util/event_utils.py b/openstack/orchestration/util/event_utils.py index 763924cb0..a06119481 100644 --- a/openstack/orchestration/util/event_utils.py +++ b/openstack/orchestration/util/event_utils.py @@ -16,8 +16,10 @@ import time from openstack.cloud import meta +from openstack import proxy +# TODO(stephenfin): Convert to use proxy def get_events(cloud, stack_id, event_args, marker=None, limit=None): # TODO(mordred) FIX THIS ONCE assert_calls CAN HANDLE QUERY STRINGS params = collections.OrderedDict() @@ -29,8 +31,11 @@ def get_events(cloud, stack_id, event_args, marker=None, limit=None): if limit: event_args['limit'] = limit - data = cloud._orchestration_client.get( - '/stacks/{id}/events'.format(id=stack_id), params=params + data = proxy._json_response( + cloud._orchestration_client.get( + '/stacks/{id}/events'.format(id=stack_id), + params=params, + ) ) events = meta.get_and_munchify('events', data) diff --git a/openstack/proxy.py b/openstack/proxy.py index 99b0f5e76..3a87fbcb1 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -107,7 +107,7 @@ def __init__( kwargs.setdefault( 'retriable_status_codes', self.retriable_status_codes ) - super(Proxy, self).__init__(session=session, *args, **kwargs) + super().__init__(session=session, *args, **kwargs) self._statsd_client = statsd_client self._statsd_prefix = statsd_prefix self._prometheus_counter = prometheus_counter @@ -173,7 +173,7 @@ def request( # Get from cache or execute and cache response = conn._cache.get_or_create( key=key, - creator=super(Proxy, self).request, + creator=super().request, creator_args=( [url, method], dict( @@ -190,7 +190,7 @@ def request( # asked for cache bypass self._invalidate_cache(conn, key_prefix) # Pass through the API request bypassing cache - response = super(Proxy, self).request( + response = super().request( url, method, connect_retries=connect_retries, @@ -815,6 +815,8 @@ def _service_cleanup_resource_filters_evaluation(self, obj, filters=None): return False +# TODO(stephenfin): Remove this and all users. Use of this generally indicates +# a missing Resource type. def _json_response(response, result_key=None, error_message=None): """Temporary method to use to bridge from ShadeAdapter to SDK calls.""" exceptions.raise_from_response(response, error_message=error_message) @@ -832,11 +834,3 @@ def _json_response(response, result_key=None, error_message=None): except JSONDecodeError: return response return result_json - - -class _ShadeAdapter(Proxy): - """Wrapper for shade methods that expect json unpacking.""" - - def request(self, url, method, error_message=None, **kwargs): - response = super(_ShadeAdapter, self).request(url, method, **kwargs) - return _json_response(response, error_message=error_message) From d2127f848a0dc3e7815326d9fc6ae69645c52f8f Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 25 May 2023 18:06:56 +0100 Subject: [PATCH 3282/3836] cloud: Rename shade-specific method Change-Id: I33a98b8e6f3f2aeeeb44c194d72c1ac1f8688b17 Signed-off-by: Stephen Finucane --- openstack/cloud/_floating_ip.py | 4 ++-- openstack/cloud/_utils.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index 30b1f32e7..1a43c6b40 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -318,7 +318,7 @@ def _nova_available_floating_ips(self, pool=None): is not specified and cannot be found. """ - with _utils.shade_exceptions( + with _utils.openstacksdk_exceptions( "Unable to create floating IP in pool {pool}".format(pool=pool) ): if pool is None: @@ -575,7 +575,7 @@ def _neutron_create_floating_ip( return fip def _nova_create_floating_ip(self, pool=None): - with _utils.shade_exceptions( + with _utils.openstacksdk_exceptions( "Unable to create floating IP in pool {pool}".format(pool=pool) ): if pool is None: diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index 77f0600f7..14c5f1a82 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -168,7 +168,7 @@ def _get_entity(cloud, resource, name_or_id, filters, **kwargs): Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" """ - # Sometimes in the control flow of shade, we already have an object + # Sometimes in the control flow of openstacksdk, we already have an object # fetched. Rather than then needing to pull the name or id out of that # object, pass it in here and rely on caching to prevent us from making # an additional call, it's simple enough to test to see if we got an @@ -284,13 +284,13 @@ def invalidate(obj, *args, **kwargs): @contextlib.contextmanager -def shade_exceptions(error_message=None): - """Context manager for dealing with shade exceptions. +def openstacksdk_exceptions(error_message=None): + """Context manager for dealing with openstack exceptions. :param string error_message: String to use for the exception message content on non-OpenStackCloudExceptions. - Useful for avoiding wrapping shade OpenStackCloudException exceptions + Useful for avoiding wrapping OpenStackCloudException exceptions within themselves. Code called from within the context may throw such exceptions without having to catch and reraise them. From 0b137cf1e9b41bccd42a5ebfe2b3498384bfcbe0 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 26 May 2023 09:47:13 +0100 Subject: [PATCH 3283/3836] cloud: Remove ClusteringCloudMixin This is all commented out and isn't currently doing anything of use. Remove it. Change-Id: Iee56f6841355bebe1bc21a05bd5ce594f2d7b4cc Signed-off-by: Stephen Finucane --- openstack/cloud/_clustering.py | 512 --------------------------------- openstack/connection.py | 3 - 2 files changed, 515 deletions(-) delete mode 100644 openstack/cloud/_clustering.py diff --git a/openstack/cloud/_clustering.py b/openstack/cloud/_clustering.py deleted file mode 100644 index 73851f5e4..000000000 --- a/openstack/cloud/_clustering.py +++ /dev/null @@ -1,512 +0,0 @@ -# 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 types so that we can reference ListType in sphinx param declarations. -# We can't just use list, because sphinx gets confused by -# openstack.resource.Resource.list and openstack.resource2.Resource.list -import types # noqa - - -class ClusteringCloudMixin: - pass - - @property - def _clustering_client(self): - if 'clustering' not in self._raw_clients: - clustering_client = self._get_versioned_client( - 'clustering', min_version=1, max_version='1.latest' - ) - self._raw_clients['clustering'] = clustering_client - return self._raw_clients['clustering'] - - -# NOTE(gtema): work on getting rid of direct API calls showed that this -# implementation never worked properly and tests in reality verifying wrong -# things. Unless someone is really interested in this piece of code this will -# be commented out and apparently dropped completely. This has no impact on the -# proxy layer. - -# def create_cluster(self, name, profile, config=None, desired_capacity=0, -# max_size=None, metadata=None, min_size=None, -# timeout=None): -# profile = self.get_cluster_profile(profile) -# profile_id = profile['id'] -# body = { -# 'desired_capacity': desired_capacity, -# 'name': name, -# 'profile_id': profile_id -# } -# -# if config is not None: -# body['config'] = config -# -# if max_size is not None: -# body['max_size'] = max_size -# -# if metadata is not None: -# body['metadata'] = metadata -# -# if min_size is not None: -# body['min_size'] = min_size -# -# if timeout is not None: -# body['timeout'] = timeout -# -# return self.clustering.create_cluster(**body) -# -# def set_cluster_metadata(self, name_or_id, metadata): -# cluster = self.get_cluster(name_or_id) -# if not cluster: -# raise exc.OpenStackCloudException( -# 'Invalid Cluster {cluster}'.format(cluster=name_or_id)) -# self.clustering.set_cluster_metadata(cluster, **metadata) -# -# def get_cluster_by_id(self, cluster_id): -# try: -# return self.get_cluster(cluster_id) -# except Exception: -# return None -# -# def get_cluster(self, name_or_id, filters=None): -# return _utils._get_entity( -# cloud=self, resource='cluster', -# name_or_id=name_or_id, filters=filters) -# -# def update_cluster(self, name_or_id, new_name=None, -# profile_name_or_id=None, config=None, metadata=None, -# timeout=None, profile_only=False): -# old_cluster = self.get_cluster(name_or_id) -# if old_cluster is None: -# raise exc.OpenStackCloudException( -# 'Invalid Cluster {cluster}'.format(cluster=name_or_id)) -# cluster = { -# 'profile_only': profile_only -# } -# -# if config is not None: -# cluster['config'] = config -# -# if metadata is not None: -# cluster['metadata'] = metadata -# -# if profile_name_or_id is not None: -# profile = self.get_cluster_profile(profile_name_or_id) -# if profile is None: -# raise exc.OpenStackCloudException( -# 'Invalid Cluster Profile {profile}'.format( -# profile=profile_name_or_id)) -# cluster['profile_id'] = profile.id -# -# if timeout is not None: -# cluster['timeout'] = timeout -# -# if new_name is not None: -# cluster['name'] = new_name -# -# return self.update_cluster(old_cluster, cluster) -# -# def delete_cluster(self, name_or_id): -# cluster = self.get_cluster(name_or_id) -# if cluster is None: -# self.log.debug("Cluster %s not found for deleting", name_or_id) -# return False -# -# for policy in self.list_policies_on_cluster(name_or_id): -# detach_policy = self.get_cluster_policy_by_id( -# policy['policy_id']) -# self.detach_policy_from_cluster(cluster, detach_policy) -# -# for receiver in self.list_cluster_receivers(): -# if cluster["id"] == receiver["cluster_id"]: -# self.delete_cluster_receiver(receiver["id"], wait=True) -# -# self.clustering.delete_cluster(cluster) -# -# return True -# -# def search_clusters(self, name_or_id=None, filters=None): -# clusters = self.list_clusters() -# return _utils._filter_list(clusters, name_or_id, filters) -# -# def list_clusters(self): -# return list(self.clustering.clusters()) -# -# def attach_policy_to_cluster( -# self, name_or_id, policy_name_or_id, is_enabled -# ): -# cluster = self.get_cluster(name_or_id) -# policy = self.get_cluster_policy(policy_name_or_id) -# if cluster is None: -# raise exc.OpenStackCloudException( -# 'Cluster {cluster} not found for attaching'.format( -# cluster=name_or_id)) -# -# if policy is None: -# raise exc.OpenStackCloudException( -# 'Policy {policy} not found for attaching'.format( -# policy=policy_name_or_id)) -# -# self.clustering.attach_policy_to_cluster(cluster, policy) -# -# return True -# -# def detach_policy_from_cluster( -# self, name_or_id, policy_name_or_id, wait=False, timeout=3600): -# cluster = self.get_cluster(name_or_id) -# policy = self.get_cluster_policy(policy_name_or_id) -# if cluster is None: -# raise exc.OpenStackCloudException( -# 'Cluster {cluster} not found for detaching'.format( -# cluster=name_or_id)) -# -# if policy is None: -# raise exc.OpenStackCloudException( -# 'Policy {policy} not found for detaching'.format( -# policy=policy_name_or_id)) -# -# self.clustering.detach_policy_from_cluster(cluster, policy) -# -# if not wait: -# return True -# -# for count in utils.iterate_timeout( -# timeout, "Timeout waiting for cluster policy to detach"): -# -# policies = self.get_cluster_by_id(cluster['id']).policies -# -# # NOTE(gtema): we iterate over all current policies and "continue" -# # if selected policy is still there -# is_present = False -# for active_policy in policies: -# if active_policy.id == policy.id: -# is_present = True -# if not is_present: -# break -# return True -# -# def update_policy_on_cluster(self, name_or_id, policy_name_or_id, -# is_enabled): -# cluster = self.get_cluster(name_or_id) -# policy = self.get_cluster_policy(policy_name_or_id) -# if cluster is None: -# raise exc.OpenStackCloudException( -# 'Cluster {cluster} not found for updating'.format( -# cluster=name_or_id)) -# -# if policy is None: -# raise exc.OpenStackCloudException( -# 'Policy {policy} not found for updating'.format( -# policy=policy_name_or_id)) -# -# self.clustering.update_cluster_policy( -# cluster, policy, is_enabled=is_enabled) -# -# return True -# -# def get_policy_on_cluster(self, name_or_id, policy_name_or_id): -# cluster = self.get_cluster(name_or_id) -# policy = self.get_policy(policy_name_or_id) -# if policy and cluster: -# return self.clustering.get_cluster_policy( -# policy, cluster) -# else: -# return False -# -# def list_policies_on_cluster(self, name_or_id): -# cluster = self.get_cluster(name_or_id) -# return list(self.clustering.cluster_policies(cluster)) -# -# def create_cluster_profile(self, name, spec, metadata=None): -# profile = { -# 'name': name, -# 'spec': spec -# } -# -# if metadata is not None: -# profile['metadata'] = metadata -# -# data = self._clustering_client.post( -# '/profiles', json={'profile': profile}, -# error_message="Error creating profile {name}".format(name=name)) -# -# return self._get_and_munchify('profile', data) -# -# def set_cluster_profile_metadata(self, name_or_id, metadata): -# profile = self.get_cluster_profile(name_or_id) -# if not profile: -# raise exc.OpenStackCloudException( -# 'Invalid Profile {profile}'.format(profile=name_or_id)) -# -# self._clustering_client.post( -# '/profiles/{profile_id}/metadata'.format(profile_id=profile['id']), -# json={'metadata': metadata}, -# error_message='Error updating profile metadata') -# -# def search_cluster_profiles(self, name_or_id=None, filters=None): -# cluster_profiles = self.list_cluster_profiles() -# return _utils._filter_list(cluster_profiles, name_or_id, filters) -# -# def list_cluster_profiles(self): -# try: -# data = self._clustering_client.get( -# '/profiles', -# error_message="Error fetching profiles") -# except exc.OpenStackCloudURINotFound as e: -# self.log.debug(str(e), exc_info=True) -# return [] -# return self._get_and_munchify('profiles', data) -# -# def get_cluster_profile_by_id(self, profile_id): -# try: -# data = self._clustering_client.get( -# "/profiles/{profile_id}".format(profile_id=profile_id), -# error_message="Error fetching profile {name}".format( -# name=profile_id)) -# return self._get_and_munchify('profile', data) -# except exc.OpenStackCloudURINotFound as e: -# self.log.debug(str(e), exc_info=True) -# return None -# -# def get_cluster_profile(self, name_or_id, filters=None): -# return _utils._get_entity(self, 'cluster_profile', name_or_id, -# filters) -# -# def delete_cluster_profile(self, name_or_id): -# profile = self.get_cluster_profile(name_or_id) -# if profile is None: -# self.log.debug("Profile %s not found for deleting", name_or_id) -# return False -# -# for cluster in self.list_clusters(): -# if (name_or_id, profile.id) in cluster.items(): -# self.log.debug( -# "Profile %s is being used by cluster %s, won't delete", -# name_or_id, cluster.name) -# return False -# -# self._clustering_client.delete( -# "/profiles/{profile_id}".format(profile_id=profile['id']), -# error_message="Error deleting profile " -# "{name}".format(name=name_or_id)) -# -# return True -# -# def update_cluster_profile( -# self, name_or_id, metadata=None, new_name=None -# ): -# old_profile = self.get_cluster_profile(name_or_id) -# if not old_profile: -# raise exc.OpenStackCloudException( -# 'Invalid Profile {profile}'.format(profile=name_or_id)) -# -# profile = {} -# -# if metadata is not None: -# profile['metadata'] = metadata -# -# if new_name is not None: -# profile['name'] = new_name -# -# data = self._clustering_client.patch( -# "/profiles/{profile_id}".format(profile_id=old_profile.id), -# json={'profile': profile}, -# error_message="Error updating profile {name}".format( -# name=name_or_id)) -# -# return self._get_and_munchify(key=None, data=data) -# -# def create_cluster_policy(self, name, spec): -# policy = { -# 'name': name, -# 'spec': spec -# } -# -# data = self._clustering_client.post( -# '/policies', json={'policy': policy}, -# error_message="Error creating policy {name}".format( -# name=policy['name'])) -# return self._get_and_munchify('policy', data) -# -# def search_cluster_policies(self, name_or_id=None, filters=None): -# cluster_policies = self.list_cluster_policies() -# return _utils._filter_list(cluster_policies, name_or_id, filters) -# -# def list_cluster_policies(self): -# endpoint = "/policies" -# try: -# data = self._clustering_client.get( -# endpoint, -# error_message="Error fetching cluster policies") -# except exc.OpenStackCloudURINotFound as e: -# self.log.debug(str(e), exc_info=True) -# return [] -# return self._get_and_munchify('policies', data) -# -# def get_cluster_policy_by_id(self, policy_id): -# try: -# data = self._clustering_client.get( -# "/policies/{policy_id}".format(policy_id=policy_id), -# error_message="Error fetching policy {name}".format( -# name=policy_id)) -# return self._get_and_munchify('policy', data) -# except exc.OpenStackCloudURINotFound as e: -# self.log.debug(str(e), exc_info=True) -# return None -# -# def get_cluster_policy(self, name_or_id, filters=None): -# return _utils._get_entity( -# self, 'cluster_policie', name_or_id, filters) -# -# def delete_cluster_policy(self, name_or_id): -# policy = self.get_cluster_policy_by_id(name_or_id) -# if policy is None: -# self.log.debug("Policy %s not found for deleting", name_or_id) -# return False -# -# for cluster in self.list_clusters(): -# if (name_or_id, policy.id) in cluster.items(): -# self.log.debug( -# "Policy %s is being used by cluster %s, won't delete", -# name_or_id, cluster.name) -# return False -# -# self._clustering_client.delete( -# "/policies/{policy_id}".format(policy_id=name_or_id), -# error_message="Error deleting policy " -# "{name}".format(name=name_or_id)) -# -# return True -# -# def update_cluster_policy(self, name_or_id, new_name): -# old_policy = self.get_cluster_policy(name_or_id) -# if not old_policy: -# raise exc.OpenStackCloudException( -# 'Invalid Policy {policy}'.format(policy=name_or_id)) -# policy = {'name': new_name} -# -# data = self._clustering_client.patch( -# "/policies/{policy_id}".format(policy_id=old_policy.id), -# json={'policy': policy}, -# error_message="Error updating policy " -# "{name}".format(name=name_or_id)) -# return self._get_and_munchify(key=None, data=data) -# -# def create_cluster_receiver(self, name, receiver_type, -# cluster_name_or_id=None, action=None, -# actor=None, params=None): -# cluster = self.get_cluster(cluster_name_or_id) -# if cluster is None: -# raise exc.OpenStackCloudException( -# 'Invalid cluster {cluster}'.format( -# cluster=cluster_name_or_id)) -# -# receiver = { -# 'name': name, -# 'type': receiver_type -# } -# -# if cluster_name_or_id is not None: -# receiver['cluster_id'] = cluster.id -# -# if action is not None: -# receiver['action'] = action -# -# if actor is not None: -# receiver['actor'] = actor -# -# if params is not None: -# receiver['params'] = params -# -# data = self._clustering_client.post( -# '/receivers', json={'receiver': receiver}, -# error_message="Error creating receiver {name}".format(name=name)) -# return self._get_and_munchify('receiver', data) -# -# def search_cluster_receivers(self, name_or_id=None, filters=None): -# cluster_receivers = self.list_cluster_receivers() -# return _utils._filter_list(cluster_receivers, name_or_id, filters) -# -# def list_cluster_receivers(self): -# try: -# data = self._clustering_client.get( -# '/receivers', -# error_message="Error fetching receivers") -# except exc.OpenStackCloudURINotFound as e: -# self.log.debug(str(e), exc_info=True) -# return [] -# return self._get_and_munchify('receivers', data) -# -# def get_cluster_receiver_by_id(self, receiver_id): -# try: -# data = self._clustering_client.get( -# "/receivers/{receiver_id}".format(receiver_id=receiver_id), -# error_message="Error fetching receiver {name}".format( -# name=receiver_id)) -# return self._get_and_munchify('receiver', data) -# except exc.OpenStackCloudURINotFound as e: -# self.log.debug(str(e), exc_info=True) -# return None -# -# def get_cluster_receiver(self, name_or_id, filters=None): -# return _utils._get_entity( -# self, 'cluster_receiver', name_or_id, filters) -# -# def delete_cluster_receiver(self, name_or_id, wait=False, timeout=3600): -# receiver = self.get_cluster_receiver(name_or_id) -# if receiver is None: -# self.log.debug("Receiver %s not found for deleting", name_or_id) -# return False -# -# receiver_id = receiver['id'] -# -# self._clustering_client.delete( -# "/receivers/{receiver_id}".format(receiver_id=receiver_id), -# error_message="Error deleting receiver {name}".format( -# name=name_or_id)) -# -# if not wait: -# return True -# -# for count in utils.iterate_timeout( -# timeout, "Timeout waiting for cluster receiver to delete"): -# -# receiver = self.get_cluster_receiver_by_id(receiver_id) -# -# if not receiver: -# break -# -# return True -# -# def update_cluster_receiver(self, name_or_id, new_name=None, action=None, -# params=None): -# old_receiver = self.get_cluster_receiver(name_or_id) -# if old_receiver is None: -# raise exc.OpenStackCloudException( -# 'Invalid receiver {receiver}'.format(receiver=name_or_id)) -# -# receiver = {} -# -# if new_name is not None: -# receiver['name'] = new_name -# -# if action is not None: -# receiver['action'] = action -# -# if params is not None: -# receiver['params'] = params -# -# data = self._clustering_client.patch( -# "/receivers/{receiver_id}".format(receiver_id=old_receiver.id), -# json={'receiver': receiver}, -# error_message="Error updating receiver {name}".format( -# name=name_or_id)) -# return self._get_and_munchify(key=None, data=data) diff --git a/openstack/connection.py b/openstack/connection.py index 3f945a132..819e16bd3 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -194,7 +194,6 @@ from openstack.cloud import _accelerator from openstack.cloud import _baremetal from openstack.cloud import _block_storage -from openstack.cloud import _clustering from openstack.cloud import _coe from openstack.cloud import _compute from openstack.cloud import _dns @@ -263,7 +262,6 @@ class Connection( _baremetal.BaremetalCloudMixin, _block_storage.BlockStorageCloudMixin, _compute.ComputeCloudMixin, - _clustering.ClusteringCloudMixin, _coe.CoeCloudMixin, _dns.DnsCloudMixin, _floating_ip.FloatingIPCloudMixin, @@ -419,7 +417,6 @@ def __init__( _accelerator.AcceleratorCloudMixin.__init__(self) _baremetal.BaremetalCloudMixin.__init__(self) _block_storage.BlockStorageCloudMixin.__init__(self) - _clustering.ClusteringCloudMixin.__init__(self) _coe.CoeCloudMixin.__init__(self) _compute.ComputeCloudMixin.__init__(self) _dns.DnsCloudMixin.__init__(self) From 3cb5bc98573e8eb7d3487121b7b59ebd535035c5 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 26 May 2023 11:05:49 +0100 Subject: [PATCH 3284/3836] utils: Add 'supports_version' Not all services use microversion (looking at you, Glance), meaning the 'supports_microversion' helper is of no use to them. Add a 'support_version' helper for these services. We will use this shortly in the cloud layer. Change-Id: I02fa4037b8119681379bb3c4b7bb667efff506ee Signed-off-by: Stephen Finucane --- openstack/utils.py | 55 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/openstack/utils.py b/openstack/utils.py index 78d9a22e9..29d917547 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -9,6 +9,9 @@ # 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 __future__ import annotations + from collections.abc import Mapping import hashlib import queue @@ -17,6 +20,7 @@ import time import keystoneauth1 +from keystoneauth1 import adapter as ks_adapter from keystoneauth1 import discover from openstack import _log @@ -97,25 +101,56 @@ def __getitem__(self, key): return keys +def supports_version( + adapter: ks_adapter.Adapter, + version: str, + raise_exception: bool = False, +) -> bool: + """Determine if the given adapter supports the given version. + + Checks the version asserted by the service and ensures this matches the + provided version. ``version`` can be a major version of a major-minor + version + + :param adapter: :class:`~keystoneauth1.adapter.Adapter` instance. + :param version: String containing the desired version. + :param raise_exception: Raise exception when requested version + is not supported by the server. + :returns: ``True`` if the service supports the version, else ``False``. + :raises: :class:`~openstack.exceptions.SDKException` when + ``raise_exception`` is ``True`` and requested version is not supported. + """ + required = discover.normalize_version_number(version) + + if discover.version_match(required, adapter.get_api_major_version()): + return True + + if raise_exception: + raise exceptions.SDKException( + f'Required version {version} is not supported by the server' + ) + + return False + + def supports_microversion(adapter, microversion, raise_exception=False): """Determine if the given adapter supports the given microversion. - Checks the min and max microversion asserted by the service and checks to - make sure that ``min <= microversion <= max``. Current default microversion - is taken into consideration if set and verifies that ``microversion <= - default``. + Checks the min and max microversion asserted by the service and ensures + ``min <= microversion <= max``. If set, the current default microversion is + taken into consideration to ensure ``microversion <= default``. :param adapter: :class:`~keystoneauth1.adapter.Adapter` instance. :param str microversion: String containing the desired microversion. :param bool raise_exception: Raise exception when requested microversion - is not supported be the server side or is higher than the current - default microversion. - :returns: True if the service supports the microversion. + is not supported by the server or is higher than the current default + microversion. + :returns: True if the service supports the microversion, else False. :rtype: bool - :raises: :class:`~openstack.exceptions.SDKException` when requested - microversion is not supported. + :raises: :class:`~openstack.exceptions.SDKException` when + ``raise_exception`` is ``True`` and requested microversion is not + supported. """ - endpoint_data = adapter.get_endpoint_data() if ( endpoint_data.min_microversion From f4c3a7734c39ed701b7fc227e33855de89ad07ef Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 26 May 2023 11:20:00 +0100 Subject: [PATCH 3285/3836] cloud: Move identity-related helpers These belong in the 'openstack.cloud._identity'. Make it, so resolving a little duplication in the process. Change-Id: I0671859307b3490afd98bbecc87b25cdefaad08e Signed-off-by: Stephen Finucane --- openstack/cloud/_identity.py | 64 ++++++++++++++++++++----------- openstack/cloud/openstackcloud.py | 42 +------------------- openstack/tests/unit/base.py | 7 +++- 3 files changed, 48 insertions(+), 65 deletions(-) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index 0468f919b..ad9f2e93d 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -32,6 +32,47 @@ def _identity_client(self): ) return self._raw_clients['identity'] + def _get_project_id_param_dict(self, name_or_id): + if name_or_id: + project = self.get_project(name_or_id) + if not project: + return {} + if self._is_client_version('identity', 3): + return {'default_project_id': project['id']} + else: + return {'tenant_id': project['id']} + else: + return {} + + def _get_domain_id_param_dict(self, domain_id): + """Get a useable domain.""" + + # Keystone v3 requires domains for user and project creation. v2 does + # not. However, keystone v2 does not allow user creation by non-admin + # users, so we can throw an error to the user that does not need to + # mention api versions + if self._is_client_version('identity', 3): + if not domain_id: + raise exc.OpenStackCloudException( + "User or project creation requires an explicit domain_id " + "argument." + ) + else: + return {'domain_id': domain_id} + else: + return {} + + def _get_identity_params(self, domain_id=None, project=None): + """Get the domain and project/tenant parameters if needed. + + keystone v2 and v3 are divergent enough that we need to pass or not + pass project or tenant_id or domain or nothing in a sane manner. + """ + ret = {} + ret.update(self._get_domain_id_param_dict(domain_id)) + ret.update(self._get_project_id_param_dict(project)) + return ret + @_utils.cache_on_arguments() def list_projects(self, domain_id=None, name_or_id=None, filters=None): """List projects. @@ -1473,26 +1514,3 @@ def revoke_role( group, role, system ) return True - - def _get_identity_params(self, domain_id=None, project=None): - """Get the domain and project/tenant parameters if needed. - - keystone v2 and v3 are divergent enough that we need to pass or not - pass project or tenant_id or domain or nothing in a sane manner. - """ - ret = {} - if not domain_id: - raise exc.OpenStackCloudException( - "User or project creation requires an explicit" - " domain_id argument." - ) - else: - ret.update({'domain_id': domain_id}) - - ret.update(self._get_project_id_param_dict(project)) - if project: - project_obj = self.get_project(project) - if project_obj: - ret.update({'default_project_id': project['id']}) - - return ret diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 5fd38f514..230a04cfd 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -617,6 +617,7 @@ def _get_current_location(self, project_id=None, zone=None): project=self._get_project_info(project_id), ) + # TODO(stephenfin): This looks unused? Can we delete it? def _get_identity_location(self): '''Identity resources do not exist inside of projects.''' return utils.Munch( @@ -628,47 +629,6 @@ def _get_identity_location(self): ), ) - def _get_project_id_param_dict(self, name_or_id): - if name_or_id: - project = self.get_project(name_or_id) - if not project: - return {} - if self._is_client_version('identity', 3): - return {'default_project_id': project['id']} - else: - return {'tenant_id': project['id']} - else: - return {} - - def _get_domain_id_param_dict(self, domain_id): - """Get a useable domain.""" - - # Keystone v3 requires domains for user and project creation. v2 does - # not. However, keystone v2 does not allow user creation by non-admin - # users, so we can throw an error to the user that does not need to - # mention api versions - if self._is_client_version('identity', 3): - if not domain_id: - raise exc.OpenStackCloudException( - "User or project creation requires an explicit" - " domain_id argument." - ) - else: - return {'domain_id': domain_id} - else: - return {} - - def _get_identity_params(self, domain_id=None, project=None): - """Get the domain and project/tenant parameters if needed. - - keystone v2 and v3 are divergent enough that we need to pass or not - pass project or tenant_id or domain or nothing in a sane manner. - """ - ret = {} - ret.update(self._get_domain_id_param_dict(domain_id)) - ret.update(self._get_project_id_param_dict(project)) - return ret - def range_search(self, data, filters): """Perform integer range searches across a list of dictionaries. diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 36091f4fc..79838fe32 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -924,7 +924,12 @@ def assert_calls(self, stop_after=None, do_count=True): ) if do_count: self.assertEqual( - len(self.calls), len(self.adapter.request_history) + len(self.calls), + len(self.adapter.request_history), + "Expected:\n{}'\nGot:\n{}".format( + '\n'.join([c['url'] for c in self.calls]), + '\n'.join([h.url for h in self.adapter.request_history]), + ), ) def assertResourceEqual(self, actual, expected, resource_type): From 3f9685ffada1f3191d9ed8aea74f110fac430948 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 25 May 2023 13:22:14 +0100 Subject: [PATCH 3286/3836] Use custom warnings everywhere Build on change I3846e8fcffdb5de2afe64365952d90b5ecb0f74a by switching all callers of 'warning.warn' to use our custom warnings. This allows users (like OSC) to filter these out as needed. Since we have new types of warning, the docs are updated accordingly. Change-Id: I5039fd8585e3352798a6d2fae7d5623fbc1adb6a Signed-off-by: Stephen Finucane --- doc/source/user/warnings.rst | 13 +++---- openstack/cloud/_baremetal.py | 14 ++++---- openstack/cloud/_block_storage.py | 5 +-- openstack/cloud/_floating_ip.py | 5 +-- openstack/cloud/openstackcloud.py | 30 ++++++++-------- openstack/compute/v2/_proxy.py | 24 +++++++++---- openstack/compute/v2/hypervisor.py | 4 ++- openstack/config/cloud_region.py | 25 ++++++------- openstack/config/loader.py | 35 ++++++++++--------- openstack/image/v1/_proxy.py | 6 +++- openstack/image/v2/_proxy.py | 6 +++- openstack/tests/unit/compute/v2/test_proxy.py | 6 +++- openstack/warnings.py | 10 +++++- 13 files changed, 109 insertions(+), 74 deletions(-) diff --git a/doc/source/user/warnings.rst b/doc/source/user/warnings.rst index a0053a3a3..e1c0afb31 100644 --- a/doc/source/user/warnings.rst +++ b/doc/source/user/warnings.rst @@ -3,12 +3,13 @@ Warnings openstacksdk uses the `warnings`__ infrastructure to warn users about deprecated resources and resource fields, as well as deprecated behavior in -openstacksdk itself. Currently, these warnings are all derived from -``DeprecationWarning``. In Python, deprecation warnings are silenced by -default. You must turn them on using the ``-Wa`` Python command line option or -the ``PYTHONWARNINGS`` environment variable. If you are writing an application -that uses openstacksdk, you may wish to enable some of these warnings during -test runs to ensure you migrate away from deprecated behavior. +openstacksdk itself. These warnings are derived from ``Warning`` or +``DeprecationWarning``. In Python, warnings are emitted by default while +deprecation warnings are silenced by default and must be turned on using the +``-Wa`` Python command line option or the ``PYTHONWARNINGS`` environment +variable. If you are writing an application that uses openstacksdk, you may +wish to enable some of these warnings during test runs to ensure you migrate +away from deprecated behavior. Available warnings ------------------ diff --git a/openstack/cloud/_baremetal.py b/openstack/cloud/_baremetal.py index e856496b9..02f57fd57 100644 --- a/openstack/cloud/_baremetal.py +++ b/openstack/cloud/_baremetal.py @@ -23,6 +23,7 @@ from openstack.baremetal.v1._proxy import Proxy from openstack.cloud import exc +from openstack import warnings as os_warnings def _normalize_port_list(nics): @@ -317,7 +318,7 @@ def unregister_machine(self, nics, uuid, wait=None, timeout=600): if wait is not None: warnings.warn( "wait argument is deprecated and has no effect", - DeprecationWarning, + os_warnings.OpenStackDeprecationWarning, ) machine = self.get_machine(uuid) @@ -480,9 +481,8 @@ def validate_machine(self, name_or_id, for_deploy=True): def validate_node(self, uuid): warnings.warn( - 'validate_node is deprecated, please use ' - 'validate_machine instead', - DeprecationWarning, + 'validate_node is deprecated, please use validate_machine instead', + os_warnings.OpenStackDeprecationWarning, ) self.baremetal.validate_node(uuid) @@ -623,7 +623,7 @@ def set_node_instance_info(self, uuid, patch): warnings.warn( "The set_node_instance_info call is deprecated, " "use patch_machine or update_machine instead", - DeprecationWarning, + os_warnings.OpenStackDeprecationWarning, ) return self.patch_machine(uuid, patch) @@ -631,7 +631,7 @@ def purge_node_instance_info(self, uuid): warnings.warn( "The purge_node_instance_info call is deprecated, " "use patch_machine or update_machine instead", - DeprecationWarning, + os_warnings.OpenStackDeprecationWarning, ) return self.patch_machine( uuid, dict(path='/instance_info', op='remove') @@ -649,6 +649,6 @@ def wait_for_baremetal_node_lock(self, node, timeout=30): "The wait_for_baremetal_node_lock call is deprecated " "in favor of wait_for_node_reservation on the baremetal " "proxy", - DeprecationWarning, + os_warnings.OpenStackDeprecationWarning, ) self.baremetal.wait_for_node_reservation(node, timeout) diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index e20bee648..d55221fa7 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -22,6 +22,7 @@ from openstack.cloud import exc from openstack import exceptions from openstack import proxy +from openstack import warnings as os_warnings def _no_pending_volumes(volumes): @@ -46,7 +47,7 @@ def list_volumes(self, cache=True): warnings.warn( "the 'cache' argument is deprecated and no longer does anything; " "consider removing it from calls", - DeprecationWarning, + os_warnings.OpenStackDeprecationWarning, ) return list(self.block_storage.volumes()) @@ -63,7 +64,7 @@ def list_volume_types(self, get_extra=None): warnings.warn( "the 'get_extra' argument is deprecated and no longer does " "anything; consider removing it from calls", - DeprecationWarning, + os_warnings.OpenStackDeprecationWarning, ) return list(self.block_storage.types()) diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index 30b1f32e7..272dcf9b4 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -26,6 +26,7 @@ from openstack.network.v2._proxy import Proxy from openstack import proxy from openstack import utils +from openstack import warnings as os_warnings _CONFIG_DOC_URL = ( "https://docs.openstack.org/openstacksdk/latest/" @@ -66,8 +67,8 @@ def search_floating_ips(self, id=None, filters=None): # `filters` could be a jmespath expression which Neutron server doesn't # understand, obviously. warnings.warn( - "search_floating_ips is deprecated. " - "Use search_resource instead." + "search_floating_ips is deprecated. Use search_resource instead.", + os_warnings.OpenStackDeprecationWarning, ) if self._use_neutron_floating() and isinstance(filters, dict): return list(self.network.ips(**filters)) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index c68bdff72..07db1f26f 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -36,7 +36,7 @@ from openstack import exceptions from openstack import proxy from openstack import utils - +from openstack import warnings as os_warnings DEFAULT_SERVER_AGE = 5 DEFAULT_PORT_AGE = 5 @@ -303,7 +303,7 @@ def global_request(self, global_request_id): app_name=self.config._app_name, app_version=self.config._app_version, discovery_cache=self.session._discovery_cache, - **params + **params, ) # Override the cloud name so that logging/location work right @@ -433,23 +433,21 @@ def _get_versioned_client( api_major = self._get_major_version_id(api_version) # If we detect a different version that was configured, warn the user. - # shade still knows what to do - but if the user gave us an explicit - # version and we couldn't find it, they may want to investigate. + # openstacksdk still knows what to do - but if the user gave us an + # explicit version and we couldn't find it, they may want to + # investigate if api_version and config_version and (api_major != config_major): + api_version_str = '.'.join([str(f) for f in api_version]) warning_msg = ( - '{service_type} is configured for {config_version}' - ' but only {api_version} is available. shade is happy' - ' with this version, but if you were trying to force an' - ' override, that did not happen. You may want to check' - ' your cloud, or remove the version specification from' - ' your config.'.format( - service_type=service_type, - config_version=config_version, - api_version='.'.join([str(f) for f in api_version]), - ) + f'{service_type} is configured for {config_version} but only ' + f'{api_version_str} is available. openstacksdk is happy ' + f'with this version, but if you were trying to force an ' + f'override, that did not happen. You may want to check ' + f'your cloud, or remove the version specification from ' + f'your config.' ) self.log.debug(warning_msg) - warnings.warn(warning_msg) + warnings.warn(warning_msg, os_warnings.OpenStackDeprecationWarning) return adapter # TODO(shade) This should be replaced with using openstack Connection @@ -790,7 +788,7 @@ def search_resources( get_kwargs=None, list_args=None, list_kwargs=None, - **filters + **filters, ): """Search resources diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index a22e68776..574a38b98 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -40,6 +40,7 @@ from openstack import proxy from openstack import resource from openstack import utils +from openstack import warnings as os_warnings class Proxy(proxy.Proxy): @@ -483,7 +484,7 @@ def delete_image(self, image, ignore_missing=True): warnings.warn( 'This API is a proxy to the image service and has been ' 'deprecated; use the image service proxy API instead', - DeprecationWarning, + os_warnings.OpenStackDeprecationWarning, ) self._delete(_image.Image, image, ignore_missing=ignore_missing) @@ -508,7 +509,7 @@ def find_image(self, name_or_id, ignore_missing=True): warnings.warn( 'This API is a proxy to the image service and has been ' 'deprecated; use the image service proxy API instead', - DeprecationWarning, + os_warnings.OpenStackDeprecationWarning, ) return self._find( _image.Image, @@ -529,7 +530,7 @@ def get_image(self, image): warnings.warn( 'This API is a proxy to the image service and has been ' 'deprecated; use the image service proxy API instead', - DeprecationWarning, + os_warnings.OpenStackDeprecationWarning, ) return self._get(_image.Image, image) @@ -548,7 +549,7 @@ def images(self, details=True, **query): warnings.warn( 'This API is a proxy to the image service and has been ' 'deprecated; use the image service proxy API instead', - DeprecationWarning, + os_warnings.OpenStackDeprecationWarning, ) base_path = '/images/detail' if details else None return self._list(_image.Image, base_path=base_path, **query) @@ -1859,7 +1860,10 @@ def create_volume_attachment(self, server, volume=None, **attrs): 'argument. This is legacy behavior that will be removed in ' 'a future version. Update callers to use a volume argument.' ) - warnings.warn(deprecation_msg, DeprecationWarning) + warnings.warn( + deprecation_msg, + os_warnings.OpenStackDeprecationWarning, + ) else: volume_id = resource.Resource._get_id(volume) @@ -1932,7 +1936,10 @@ def _verify_server_volume_args(self, server, volume): if isinstance(server, _volume.Volume) or isinstance( volume, _server.Server ): - warnings.warn(deprecation_msg, DeprecationWarning) + warnings.warn( + deprecation_msg, + os_warnings.OpenStackDeprecationWarning, + ) return volume, server # without type info we have to try a find the server corresponding to @@ -1940,7 +1947,10 @@ def _verify_server_volume_args(self, server, volume): if self.find_server(server, ignore_missing=True) is not None: return server, volume else: - warnings.warn(deprecation_msg, DeprecationWarning) + warnings.warn( + deprecation_msg, + os_warnings.OpenStackDeprecationWarning, + ) return volume, server def delete_volume_attachment(self, server, volume, ignore_missing=True): diff --git a/openstack/compute/v2/hypervisor.py b/openstack/compute/v2/hypervisor.py index 8f10cd204..fdcb1afee 100644 --- a/openstack/compute/v2/hypervisor.py +++ b/openstack/compute/v2/hypervisor.py @@ -15,6 +15,7 @@ from openstack import exceptions from openstack import resource from openstack import utils +from openstack import warnings as os_warnings class Hypervisor(resource.Resource): @@ -86,7 +87,8 @@ def get_uptime(self, session): Updates uptime attribute of the hypervisor object """ warnings.warn( - "This call is deprecated and is only available until Nova 2.88" + "This call is deprecated and is only available until Nova 2.88", + os_warnings.LegacyAPIWarning, ) if utils.supports_microversion(session, '2.88'): raise exceptions.SDKException( diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 9ab67e387..421e2438c 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -48,6 +48,7 @@ from openstack import exceptions from openstack import proxy from openstack import version as openstack_version +from openstack import warnings as os_warnings _logger = _log.setup_logging('openstack') @@ -100,7 +101,7 @@ def from_session( force_ipv4=False, app_name=None, app_version=None, - **kwargs + **kwargs, ): """Construct a CloudRegion from an existing `keystoneauth1.session.Session` @@ -387,11 +388,10 @@ def get_requests_verify_args(self): else: if cacert: warnings.warn( - "You are specifying a cacert for the cloud {full_name}" - " but also to ignore the host verification. The host SSL" - " cert will not be verified.".format( - full_name=self.full_name - ) + f"You are specifying a cacert for the cloud " + f"{self.full_name} but also to ignore the host " + f"verification. The host SSL cert will not be verified.", + os_warnings.ConfigurationWarning, ) cert = self.config.get('cert') @@ -505,9 +505,10 @@ def get_api_version(self, service_type): except ValueError: if 'latest' in version: warnings.warn( - "You have a configured API_VERSION with 'latest' in" - " it. In the context of openstacksdk this doesn't make" - " any sense." + "You have a configured API_VERSION with 'latest' in " + "it. In the context of openstacksdk this doesn't make " + "any sense.", + os_warnings.ConfigurationWarning, ) return None return version @@ -686,7 +687,7 @@ def get_session(self): raise exceptions.ConfigException( "Problem with auth parameters" ) - (verify, cert) = self.get_requests_verify_args() + verify, cert = self.get_requests_verify_args() # Turn off urllib3 warnings about insecure certs if we have # explicitly configured requests to tell it we do not want # cert verification @@ -876,7 +877,7 @@ def get_session_client( default_microversion=version_request.default_microversion, rate_limit=self.get_rate_limit(service_type), concurrency=self.get_concurrency(service_type), - **kwargs + **kwargs, ) if version_request.default_microversion: default_microversion = version_request.default_microversion @@ -962,7 +963,7 @@ def get_session_endpoint( region_name=region_name, interface=interface, service_name=service_name, - **version_kwargs + **version_kwargs, ) except keystoneauth1.exceptions.catalog.EndpointNotFound: endpoint = None diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 63d8c71bf..8df3422be 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -12,7 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. - # alias because we already had an option named argparse import argparse as argparse_mod import collections @@ -35,6 +34,7 @@ from openstack.config import defaults from openstack.config import vendors from openstack import exceptions +from openstack import warnings as os_warnings APPDIRS = appdirs.AppDirs('openstack', 'OpenStack', multipath='/etc') CONFIG_HOME = APPDIRS.user_config_dir @@ -506,9 +506,10 @@ def _get_known_regions(self, cloud): regions = config['region_name'].split(',') if len(regions) > 1: warnings.warn( - "Comma separated lists in region_name are deprecated." - " Please use a yaml list in the regions" - " parameter in {0} instead.".format(self.config_filename) + f"Comma separated lists in region_name are deprecated. " + f"Please use a yaml list in the regions " + f"parameter in {self.config_filename} instead.", + os_warnings.OpenStackDeprecationWarning, ) return self._expand_regions(regions) else: @@ -585,9 +586,10 @@ def _expand_vendor_profile(self, name, cloud, our_cloud): return if 'cloud' in our_cloud: warnings.warn( - "{0} uses the keyword 'cloud' to reference a known " - "vendor profile. This has been deprecated in favor of the " - "'profile' keyword.".format(self.config_filename) + f"{self.config_filename} uses the keyword 'cloud' to " + f"reference a known vendor profile. This has been deprecated " + f"in favor of the 'profile' keyword.", + os_warnings.OpenStackDeprecationWarning, ) vendor_filename, vendor_file = self._load_vendor_file() @@ -609,9 +611,8 @@ def _expand_vendor_profile(self, name, cloud, our_cloud): message = profile_data.pop('message', '') if status == 'deprecated': warnings.warn( - "{profile_name} is deprecated: {message}".format( - profile_name=profile_name, message=message - ) + f"{profile_name} is deprecated: {message}", + os_warnings.OpenStackDeprecationWarning, ) elif status == 'shutdown': raise exceptions.ConfigException( @@ -624,8 +625,9 @@ def _expand_vendor_profile(self, name, cloud, our_cloud): else: # Can't find the requested vendor config, go about business warnings.warn( - "Couldn't find the vendor profile '{0}', for" - " the cloud '{1}'".format(profile_name, name) + f"Couldn't find the vendor profile {profile_name} for" + f"the cloud {name}", + os_warnings.ConfigurationWarning, ) def _project_scoped(self, cloud): @@ -687,11 +689,10 @@ def _fix_backwards_networks(self, cloud): ) if key in cloud: warnings.warn( - "{key} is deprecated. Please replace with an entry in" - " a dict inside of the networks list with name: {name}" - " and routes_externally: {external}".format( - key=key, name=cloud[key], external=external - ) + f"{key} is deprecated. Please replace with an entry in " + f"a dict inside of the networks list with name: " + f"{cloud[key]} and routes_externally: {external}", + os_warnings.OpenStackDeprecationWarning, ) networks.append( dict( diff --git a/openstack/image/v1/_proxy.py b/openstack/image/v1/_proxy.py index 522d99a37..c2d7a4260 100644 --- a/openstack/image/v1/_proxy.py +++ b/openstack/image/v1/_proxy.py @@ -18,6 +18,7 @@ from openstack.image.v1 import image as _image from openstack import proxy from openstack import utils +from openstack import warnings as os_warnings def _get_name_and_filename(name, image_format): @@ -258,7 +259,10 @@ def upload_image(self, **attrs): :returns: The results of image creation :rtype: :class:`~openstack.image.v1.image.Image` """ - warnings.warn("upload_image is deprecated. Use create_image instead.") + warnings.warn( + "upload_image is deprecated. Use create_image instead.", + os_warnings.OpenStackDeprecationWarning, + ) return self._create(_image.Image, **attrs) def _upload_image( diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 448778311..84bfc185c 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -27,6 +27,7 @@ from openstack import proxy from openstack import resource from openstack import utils +from openstack import warnings as os_warnings # Rackspace returns this for intermittent import errors _IMAGE_ERROR_396 = "Image cannot be imported. Error code: '396'" @@ -500,7 +501,10 @@ def upload_image( :returns: The results of image creation :rtype: :class:`~openstack.image.v2.image.Image` """ - warnings.warn("upload_image is deprecated. Use create_image instead.") + warnings.warn( + "upload_image is deprecated. Use create_image instead.", + os_warnings.OpenStackDeprecationWarning, + ) # container_format and disk_format are required to be set # on the image by the time upload_image is called, but they're not # required by the _create call. Enforce them here so that we don't diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index ecebf919c..02d0626fa 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -39,6 +39,7 @@ from openstack.compute.v2 import volume_attachment from openstack import resource from openstack.tests.unit import test_proxy_base +from openstack import warnings as os_warnings class TestComputeProxy(test_proxy_base.TestProxyBase): @@ -523,7 +524,10 @@ def test_volume_attachment_create__legacy_parameters(self): ) self.assertEqual(1, len(w)) - self.assertEqual(w[-1].category, DeprecationWarning) + self.assertEqual( + os_warnings.OpenStackDeprecationWarning, + w[-1].category, + ) self.assertIn( 'This method was called with a volume_id or volumeId argument', str(w[-1]), diff --git a/openstack/warnings.py b/openstack/warnings.py index 29c3a6d55..acbb81e4e 100644 --- a/openstack/warnings.py +++ b/openstack/warnings.py @@ -31,5 +31,13 @@ class LegacyAPIWarning(OpenStackDeprecationWarning): """Indicates an API that is in 'legacy' status, a long term deprecation.""" -class UnsupportedServiceVersion(Warning): +class OpenStackWarning(Warning): + """Base class for general warnings in openstacksdk.""" + + +class ConfigurationWarning(OpenStackWarning): + """Indicates an issue with configuration.""" + + +class UnsupportedServiceVersion(OpenStackWarning): """Indicates a major version that SDK doesn't understand.""" From dcd89705d661748cc4acaaa3e37633960a1dbee7 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 25 May 2023 16:38:43 +0100 Subject: [PATCH 3287/3836] tests: Ignore our own deprecation warnings They're obvious noise. Change-Id: I3bc9b1a6ab54c52797f4794548245c34a7a12ecb Signed-off-by: Stephen Finucane --- openstack/tests/base.py | 5 +- openstack/tests/fakes.py | 4 +- openstack/tests/fixtures.py | 55 ++++++++++++++++++++ openstack/tests/unit/test_missing_version.py | 13 +++-- 4 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 openstack/tests/fixtures.py diff --git a/openstack/tests/base.py b/openstack/tests/base.py index a1239f95f..142d80b50 100644 --- a/openstack/tests/base.py +++ b/openstack/tests/base.py @@ -23,6 +23,7 @@ from oslotest import base import testtools.content +from openstack.tests import fixtures as os_fixtures from openstack import utils _TRUE_VALUES = ('true', '1', 'yes') @@ -54,7 +55,9 @@ def setUp(self): # Let oslotest do its thing pass - super(TestCase, self).setUp() + super().setUp() + + self.warnings = self.useFixture(os_fixtures.WarningsFixture()) if os.environ.get('OS_LOG_CAPTURE') in _TRUE_VALUES: self._log_stream = StringIO() diff --git a/openstack/tests/fakes.py b/openstack/tests/fakes.py index a4a7ece8e..3804a0599 100644 --- a/openstack/tests/fakes.py +++ b/openstack/tests/fakes.py @@ -8,11 +8,11 @@ # 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.V +# under the License. """ fakes ----------------------------------- +----- Fakes used for testing """ diff --git a/openstack/tests/fixtures.py b/openstack/tests/fixtures.py new file mode 100644 index 000000000..7b69cd7bb --- /dev/null +++ b/openstack/tests/fixtures.py @@ -0,0 +1,55 @@ +# 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. + +""" +fixtures +-------- + +Fixtures used for testing +""" + +import warnings + +import fixtures + +from openstack import warnings as os_warnings + + +# TODO(stephenfin): Replace this with WarningsFilter from fixtures when it's +# released https://github.com/testing-cabal/fixtures/pull/50 +class WarningsFixture(fixtures.Fixture): + """Filters out warnings during test runs.""" + + def setUp(self): + super().setUp() + + self._original_warning_filters = warnings.filters[:] + + # enable deprecation warnings in general... + warnings.simplefilter("once", DeprecationWarning) + + # ...but ignore our own deprecation warnings + warnings.filterwarnings( + "ignore", + category=os_warnings.OpenStackDeprecationWarning, + ) + + # also ignore our own general warnings + warnings.filterwarnings( + "ignore", + category=os_warnings.OpenStackWarning, + ) + + self.addCleanup(self._reset_warning_filters) + + def _reset_warning_filters(self): + warnings.filters[:] = self._original_warning_filters diff --git a/openstack/tests/unit/test_missing_version.py b/openstack/tests/unit/test_missing_version.py index 0f96c4f48..c17569def 100644 --- a/openstack/tests/unit/test_missing_version.py +++ b/openstack/tests/unit/test_missing_version.py @@ -10,7 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -import fixtures +import warnings + import testtools from openstack import exceptions @@ -42,8 +43,12 @@ def test_unsupported_version(self): def test_unsupported_version_override(self): self.cloud.config.config['image_api_version'] = '7' - w = fixtures.WarningsCapture() - with w: + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") self.assertIsInstance(self.cloud.image, proxy.Proxy) - self.assertEqual(1, len(w.captures)) + self.assertEqual(1, len(w)) + self.assertIn( + "Service image has no discoverable version.", + str(w[-1].message), + ) self.assert_calls() From de6b6bce0268f3d85a61583cf6d8640d3722d75b Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 25 May 2023 16:55:02 +0100 Subject: [PATCH 3288/3836] tests: Enable UserWarning by default These nearly always indicate a more serious issue that we need to address. Make them an error, correcting one issue this highlights. Change-Id: I9452a4f8ac6bcd35c803e591a1022294041d26a0 Signed-off-by: Stephen Finucane --- openstack/tests/fixtures.py | 3 +++ openstack/tests/unit/test_proxy.py | 8 +++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/openstack/tests/fixtures.py b/openstack/tests/fixtures.py index 7b69cd7bb..463734dd2 100644 --- a/openstack/tests/fixtures.py +++ b/openstack/tests/fixtures.py @@ -34,6 +34,9 @@ def setUp(self): self._original_warning_filters = warnings.filters[:] + # enable user warnings as many libraries use this (it's the default) + warnings.simplefilter("error", UserWarning) + # enable deprecation warnings in general... warnings.simplefilter("once", DeprecationWarning) diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index 45cff353f..1d6007945 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -14,6 +14,7 @@ import queue from unittest import mock +from keystoneauth1 import session from testscenarios import load_tests_apply_scenarios as load_tests # noqa from openstack import exceptions @@ -622,7 +623,7 @@ def setUp(self): cloud_config_fixture='clouds_cache.yaml' ) - self.session = mock.Mock() + self.session = mock.Mock(spec=session.Session) self.session._sdk_connection = self.cloud self.session.get_project_id = mock.Mock(return_value='fake_prj') @@ -651,10 +652,11 @@ def test_get_not_in_cache(self): connect_retries=mock.ANY, raise_exc=mock.ANY, global_request_id=mock.ANY, - endpoint_filter=mock.ANY, - headers=mock.ANY, microversion=mock.ANY, params=mock.ANY, + endpoint_filter=mock.ANY, + headers=mock.ANY, + rate_semaphore=mock.ANY, ) self.assertIn(self._get_key(1), self.cloud._api_cache_keys) From 97f28e410b68bc78d382623797ebf2166522c73c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 26 May 2023 16:09:51 +0100 Subject: [PATCH 3289/3836] tests: Use uuid, not randint randint isn't random enough, especially on CI machines with low entropy. As a result, we are seeing test failures. Use a UUID instead. Change-Id: I4a42342dd4a248cce9b630348252482ff33851a7 Signed-off-by: Stephen Finucane --- .../functional/dns/v2/test_zone_share.py | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/openstack/tests/functional/dns/v2/test_zone_share.py b/openstack/tests/functional/dns/v2/test_zone_share.py index 51980970c..42be9abcf 100644 --- a/openstack/tests/functional/dns/v2/test_zone_share.py +++ b/openstack/tests/functional/dns/v2/test_zone_share.py @@ -9,7 +9,8 @@ # 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 random + +import uuid from openstack import exceptions from openstack.tests.functional import base @@ -26,7 +27,7 @@ def setUp(self): # chose a new zone name for a test # getUniqueString is not guaranteed to return unique string between # different tests of the same class. - self.ZONE_NAME = 'example-{0}.org.'.format(random.randint(1, 10000)) + self.ZONE_NAME = 'example-{0}.org.'.format(uuid.uuid4().hex) self.zone = self.operator_cloud.dns.create_zone( name=self.ZONE_NAME, @@ -36,7 +37,9 @@ def setUp(self): description='example zone for sdk zone share tests', ) self.addCleanup( - self.operator_cloud.dns.delete_zone, self.zone, delete_shares=True + self.operator_cloud.dns.delete_zone, + self.zone, + delete_shares=True, ) self.project_id = self.operator_cloud.session.get_project_id() @@ -47,7 +50,9 @@ def test_create_delete_zone_share(self): self.zone, target_project_id=self.demo_project_id ) self.addCleanup( - self.operator_cloud.dns.delete_zone_share, self.zone, zone_share + self.operator_cloud.dns.delete_zone_share, + self.zone, + zone_share, ) self.assertEqual(self.zone.id, zone_share.zone_id) @@ -59,7 +64,8 @@ def test_create_delete_zone_share(self): def test_get_zone_share(self): orig_zone_share = self.operator_cloud.dns.create_zone_share( - self.zone, target_project_id=self.demo_project_id + self.zone, + target_project_id=self.demo_project_id, ) self.addCleanup( self.operator_cloud.dns.delete_zone_share, @@ -68,7 +74,8 @@ def test_get_zone_share(self): ) zone_share = self.operator_cloud.dns.get_zone_share( - self.zone, orig_zone_share + self.zone, + orig_zone_share, ) self.assertEqual(self.zone.id, zone_share.zone_id) @@ -89,7 +96,8 @@ def test_find_zone_share(self): ) zone_share = self.operator_cloud.dns.find_zone_share( - self.zone, orig_zone_share.id + self.zone, + orig_zone_share.id, ) self.assertEqual(self.zone.id, zone_share.zone_id) @@ -101,7 +109,8 @@ def test_find_zone_share(self): def test_find_zone_share_ignore_missing(self): zone_share = self.operator_cloud.dns.find_zone_share( - self.zone, 'bogus_id' + self.zone, + 'bogus_id', ) self.assertIsNone(zone_share) @@ -116,10 +125,13 @@ def test_find_zone_share_ignore_missing_false(self): def test_list_zone_shares(self): zone_share = self.operator_cloud.dns.create_zone_share( - self.zone, target_project_id=self.demo_project_id + self.zone, + target_project_id=self.demo_project_id, ) self.addCleanup( - self.operator_cloud.dns.delete_zone_share, self.zone, zone_share + self.operator_cloud.dns.delete_zone_share, + self.zone, + zone_share, ) target_ids = [ @@ -130,10 +142,13 @@ def test_list_zone_shares(self): def test_list_zone_shares_with_target_id(self): zone_share = self.operator_cloud.dns.create_zone_share( - self.zone, target_project_id=self.demo_project_id + self.zone, + target_project_id=self.demo_project_id, ) self.addCleanup( - self.operator_cloud.dns.delete_zone_share, self.zone, zone_share + self.operator_cloud.dns.delete_zone_share, + self.zone, + zone_share, ) target_ids = [ From 2e186521d8f7ee81b159434092b04c2a3601f7e7 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 26 May 2023 17:36:20 +0200 Subject: [PATCH 3290/3836] Add image metadef_resource_type into the registry Every resource should be present in the service registry to allow use of generalized functions (search_resources) Change-Id: Ie4946407b6e591c98163a701ea2f126d13375788 --- openstack/image/v2/_proxy.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 448778311..d3af44a06 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -54,6 +54,8 @@ class Proxy(proxy.Proxy): "image": _image.Image, "image_member": _member.Member, "metadef_namespace": _metadef_namespace.MetadefNamespace, + "metadef_resource_type": _metadef_resource_type.MetadefResourceType, + "metadef_resource_type_association": _metadef_resource_type.MetadefResourceTypeAssociation, # noqa "schema": _schema.Schema, "info_import": _si.Import, "info_store": _si.Store, From c1c6a0e9168762eed604c0ba7ec58b2c77247077 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 27 Mar 2023 11:28:00 +0100 Subject: [PATCH 3291/3836] docs: Replace/remove shade-specific docs Lots of minor tweaks, none of them worthy of their own change. Change-Id: Iba8649b8e12f367760849fd185cf76b94b703706 Signed-off-by: Stephen Finucane --- doc/source/contributor/coding.rst | 65 ++- doc/source/contributor/history.rst | 69 ++-- doc/source/contributor/index.rst | 1 - doc/source/contributor/layout.rst | 11 +- doc/source/contributor/setup.rst | 26 +- doc/source/index.rst | 1 - doc/source/install/index.rst | 1 - doc/source/releasenotes.rst | 1 - doc/source/user/index.rst | 1 - doc/source/user/microversions.rst | 1 - doc/source/user/model.rst | 568 +++------------------------ doc/source/user/multi-cloud-demo.rst | 216 +++++----- doc/source/user/resource.rst | 4 - 13 files changed, 240 insertions(+), 725 deletions(-) diff --git a/doc/source/contributor/coding.rst b/doc/source/contributor/coding.rst index 9572d53d4..e1640e46d 100644 --- a/doc/source/contributor/coding.rst +++ b/doc/source/contributor/coding.rst @@ -1,24 +1,25 @@ -======================================== OpenStack SDK Developer Coding Standards ======================================== In the beginning, there were no guidelines. And it was good. But that didn't last long. As more and more people added more and more code, we realized that we needed a set of coding standards to make sure that -the openstacksdk API at least *attempted* to display some form of consistency. +the *openstacksdk* API at least *attempted* to display some form of +consistency. Thus, these coding standards/guidelines were developed. Note that not -all of openstacksdk adheres to these standards just yet. Some older code has +all of *openstacksdk* adheres to these standards just yet. Some older code has not been updated because we need to maintain backward compatibility. Some of it just hasn't been changed yet. But be clear, all new code *must* adhere to these guidelines. -Below are the patterns that we expect openstacksdk developers to follow. +Below are the patterns that we expect *openstacksdk* developers to follow. + Release Notes -============= +------------- -openstacksdk uses `reno `_ for +*openstacksdk* uses `reno `_ for managing its release notes. A new release note should be added to your contribution anytime you add new API calls, fix significant bugs, add new functionality or parameters to existing API calls, or make any @@ -29,56 +30,47 @@ It is *not* necessary to add release notes for minor fixes, such as correction of documentation typos, minor code cleanup or reorganization, or any other change that a user would not notice through normal usage. + Exceptions -========== +---------- Exceptions should NEVER be wrapped and re-raised inside of a new exception. This removes important debug information from the user. All of the exceptions should be raised correctly the first time. + openstack.cloud API Methods -=========================== +--------------------------- -The `openstack.cloud` layer has some specific rules: +The ``openstack.cloud`` layer has some specific rules: - When an API call acts on a resource that has both a unique ID and a name, that API call should accept either identifier with a name_or_id parameter. - All resources should adhere to the get/list/search interface that - control retrieval of those resources. E.g., `get_image()`, `list_images()`, - `search_images()`. + control retrieval of those resources. E.g., ``get_image()``, + ``list_images()``, ``search_images()``. -- Resources should have `create_RESOURCE()`, `delete_RESOURCE()`, - `update_RESOURCE()` API methods (as it makes sense). +- Resources should have ``create_RESOURCE()``, ``delete_RESOURCE()``, + ``update_RESOURCE()`` API methods (as it makes sense). - For those methods that should behave differently for omitted or None-valued - parameters, use the `_utils.valid_kwargs` decorator. Notably: all Neutron - `update_*` functions. + parameters, use the ``_utils.valid_kwargs`` decorator. This includes all + Neutron ``update_*`` functions. - Deleting a resource should return True if the delete succeeded, or False if the resource was not found. Returned Resources ------------------- +~~~~~~~~~~~~~~~~~~ -Complex objects returned to the caller must be a `openstack.resource.Resource` -type. - -All objects should be normalized. It is openstacksdk's purpose in life to make -OpenStack consistent for end users, and this means not trusting the clouds -to return consistent objects. The `Resource` object should do this for us. - -Fields should not be in the normalization contract if we cannot commit to -providing them to all users. - -Fields should be renamed in normalization to be consistent with -the rest of `openstack.cloud`. For instance, nothing in `openstack.cloud` -exposes the legacy OpenStack concept of "tenant" to a user, but instead uses -"project" even if the cloud in question uses tenant. +The ``openstack.cloud`` layer should rely on the proxy layer for the given +service. This will ensure complex objects returned to the caller are of +``openstack.resource.Resource`` type. Nova vs. Neutron ----------------- +~~~~~~~~~~~~~~~~ - Recognize that not all cloud providers support Neutron, so never assume it will be present. If a task can be handled by either @@ -86,16 +78,17 @@ Nova vs. Neutron - For methods that accept either a Nova pool or Neutron network, the parameter should just refer to the network, but documentation of it - should explain about the pool. See: `create_floating_ip()` and - `available_floating_ip()` methods. + should explain about the pool. See: ``create_floating_ip()`` and + ``available_floating_ip()`` methods. + Tests -===== +----- - New API methods *must* have unit tests! -- New unit tests should only mock at the REST layer using `requests_mock`. - Any mocking of openstacksdk itself should be considered legacy and to be +- New unit tests should only mock at the REST layer using ``requests_mock``. + Any mocking of *openstacksdk* itself should be considered legacy and to be avoided. Exceptions to this rule can be made when attempting to test the internals of a logical shim where the inputs and output of the method aren't actually impacted by remote content. diff --git a/doc/source/contributor/history.rst b/doc/source/contributor/history.rst index 02a43f754..b2901ff3f 100644 --- a/doc/source/contributor/history.rst +++ b/doc/source/contributor/history.rst @@ -1,45 +1,46 @@ A Brief History =============== -openstacksdk started its life as three different libraries: shade, -os-client-config and python-openstacksdk. - -``shade`` started its life as some code inside of OpenStack Infra's `nodepool`_ -project, and as some code inside of the `Ansible OpenStack Modules`_. -Ansible had a bunch of different OpenStack related modules, and there was a -ton of duplicated code. Eventually, between refactoring that duplication into -an internal library, and adding the logic and features that the OpenStack Infra -team had developed to run client applications at scale, it turned out that we'd -written nine-tenths of what we'd need to have a standalone library. - -Because of its background from nodepool, shade contained abstractions to -work around deployment differences and is resource oriented rather than service -oriented. This allows a user to think about Security Groups without having to -know whether Security Groups are provided by Nova or Neutron on a given cloud. -On the other hand, as an interface that provides an abstraction, it deviates -from the published OpenStack REST API and adds its own opinions, which may not -get in the way of more advanced users with specific needs. - -``os-client-config`` was a library for collecting client configuration for -using an OpenStack cloud in a consistent and comprehensive manner, which -introduced the ``clouds.yaml`` file for expressing named cloud configurations. - -``python-openstacksdk`` was a library that exposed the OpenStack APIs to -developers in a consistent and predictable manner. +*openstacksdk* started its life as three different libraries: *shade*, +*os-client-config* and *python-openstacksdk*. + +*shade* + *shade* started its life as some code inside of OpenStack Infra's `nodepool`_ + project, and as some code inside of the `Ansible OpenStack Modules`_. + Ansible had a bunch of different OpenStack related modules, and there was a + ton of duplicated code. Eventually, between refactoring that duplication into + an internal library, and adding the logic and features that the OpenStack + Infra team had developed to run client applications at scale, it turned out + that we'd written nine-tenths of what we'd need to have a standalone library. + + Because of its background from nodepool, *shade* contained abstractions to + work around deployment differences and is resource oriented rather than + service oriented. This allows a user to think about Security Groups without + having to know whether Security Groups are provided by Nova or Neutron on a + given cloud. On the other hand, as an interface that provides an abstraction, + it deviates from the published OpenStack REST API and adds its own opinions, + which may not get in the way of more advanced users with specific needs. + +*os-client-config* + *os-client-config* was a library for collecting client configuration for + using an OpenStack cloud in a consistent and comprehensive manner, which + introduced the ``clouds.yaml`` file for expressing named cloud + configurations. + +*python-openstacksdk* + *python-openstacksdk* was a library that exposed the OpenStack APIs to + developers in a consistent and predictable manner. After a while it became clear that there was value in both the high-level layer that contains additional business logic and the lower-level SDK that exposes services and their resources faithfully and consistently as Python -objects. +objects. Even with both of those layers, it is still beneficial at times to be +able to make direct REST calls and to do so with the same properly configured +`Session`_ from `python-requests`_. This led to the merge of the three +projects. -Even with both of those layers, it is still beneficial at times to be able to -make direct REST calls and to do so with the same properly configured -`Session`_ from `python-requests`_. - -This led to the merge of the three projects. - -The original contents of the shade library have been moved into -``openstack.cloud`` and os-client-config has been moved in to +The original contents of the *shade* library have been moved into +``openstack.cloud`` and *os-client-config* has been moved in to ``openstack.config``. .. _nodepool: https://docs.openstack.org/infra/nodepool/ diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst index 7d9be8eed..c651c76ec 100644 --- a/doc/source/contributor/index.rst +++ b/doc/source/contributor/index.rst @@ -1,4 +1,3 @@ -================================= Contributing to the OpenStack SDK ================================= diff --git a/doc/source/contributor/layout.rst b/doc/source/contributor/layout.rst index f96bb5632..b91d35985 100644 --- a/doc/source/contributor/layout.rst +++ b/doc/source/contributor/layout.rst @@ -80,9 +80,12 @@ method. For the time being, it simply passes on the ``Adapter`` maintained by the ``Proxy``, and returns what the underlying ``Resource.list`` method does. -The implementations and method signatures of ``Proxy`` methods are currently -under construction, as we figure out the best way to implement them in a -way which will apply nicely across all of the services. +Cloud +----- + +.. todo + +TODO. Connection ---------- @@ -93,7 +96,7 @@ higher level interface constructed of ``Proxy`` objects from each of the services. The ``Connection`` class' primary purpose is to act as a high-level interface -to this SDK, managing the lower level connecton bits and exposing the +to this SDK, managing the lower level connection bits and exposing the ``Resource`` objects through their corresponding `Proxy`_ object. If you've built proper ``Resource`` objects and implemented methods on the diff --git a/doc/source/contributor/setup.rst b/doc/source/contributor/setup.rst index 66109ac4b..36c5aa13b 100644 --- a/doc/source/contributor/setup.rst +++ b/doc/source/contributor/setup.rst @@ -5,20 +5,18 @@ Required Tools -------------- Python -****** +~~~~~~ As the OpenStack SDK is developed in Python, you will need at least one -version of Python installed. It is strongly preferred that you have at least -one of version 2 and one of version 3 so that your tests are run against both. -Our continuous integration system runs against several versions, so ultimately -we will have the proper test coverage, but having multiple versions locally -results in less time spent in code review when changes unexpectedly break -other versions. +version of Python installed. Our continuous integration system runs against +several versions, so ultimately we will have the proper test coverage, but +having multiple versions locally results in less time spent in code review when +changes unexpectedly break other versions. Python can be downloaded from https://www.python.org/downloads. virtualenv -********** +~~~~~~~~~~ In order to isolate our development environment from the system-based Python installation, we use `virtualenv `_. @@ -28,9 +26,9 @@ Virtualenv must be installed on your system in order to use it, and it can be had from PyPI, via pip, as follows. Note that you may need to run this as an administrator in some situations.:: - $ apt-get install python-virtualenv # Debian based platforms - $ yum install python-virtualenv # Red Hat based platforms - $ pip install virtualenv # Mac OS X and other platforms + $ apt-get install python3-virtualenv # Debian based platforms + $ dnf install python3-virtualenv # Red Hat based platforms + $ pip install virtualenv # Mac OS X and other platforms You can create a virtualenv in any location. A common usage is to store all of your virtualenvs in the same place, such as under your home directory. @@ -40,7 +38,7 @@ To create a virtualenv for the default Python, run the following:: To create an environment for a different version, run the following:: - $ virtualenv -p python3.8 $HOME/envs/sdk3 + $ virtualenv -p python3 $HOME/envs/sdk3 When you want to enable your environment so that you can develop inside of it, you *activate* it. To activate an environment, run the /bin/activate @@ -54,7 +52,7 @@ command prompt. In order to exit that environment, run the ``deactivate`` command. tox -*** +~~~ We use `tox `_ as our test runner, which allows us to run the same test commands against multiple versions @@ -64,7 +62,7 @@ run the following to install ``tox`` into it.:: (sdk3)$ pip install tox Git -*** +~~~ The source of the OpenStack SDK is stored in Git. In order to work with our source repository, you must have Git installed on your system. If your diff --git a/doc/source/index.rst b/doc/source/index.rst index c5a8c5d3a..d935708cd 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -1,4 +1,3 @@ -============ openstacksdk ============ diff --git a/doc/source/install/index.rst b/doc/source/install/index.rst index 49cf87d5d..33255ae9a 100644 --- a/doc/source/install/index.rst +++ b/doc/source/install/index.rst @@ -1,4 +1,3 @@ -================== Installation guide ================== diff --git a/doc/source/releasenotes.rst b/doc/source/releasenotes.rst index 17b9814e2..b4eb5e488 100644 --- a/doc/source/releasenotes.rst +++ b/doc/source/releasenotes.rst @@ -1,4 +1,3 @@ -============= Release Notes ============= diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index a78322772..0066080d0 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -1,4 +1,3 @@ -======================= Using the OpenStack SDK ======================= diff --git a/doc/source/user/microversions.rst b/doc/source/user/microversions.rst index a4960876e..fcef09656 100644 --- a/doc/source/user/microversions.rst +++ b/doc/source/user/microversions.rst @@ -1,4 +1,3 @@ -============= Microversions ============= diff --git a/doc/source/user/model.rst b/doc/source/user/model.rst index f6e7b4fbb..1c270e364 100644 --- a/doc/source/user/model.rst +++ b/doc/source/user/model.rst @@ -1,46 +1,47 @@ -========== Data Model ========== -shade has a very strict policy on not breaking backwards compatability ever. -However, with the data structures returned from OpenStack, there are places -where the resource structures from OpenStack are returned to the user somewhat -directly, leaving a shade user open to changes/differences in result content. - -To combat that, shade 'normalizes' the return structure from OpenStack in many -places, and the results of that normalization are listed below. Where shade -performs normalization, a user can count on any fields declared in the docs -as being completely safe to use - they are as much a part of shade's API -contract as any other Python method. - -Some OpenStack objects allow for arbitrary attributes at -the root of the object. shade will pass those through so as not to break anyone -who may be counting on them, but as they are arbitrary shade can make no -guarantees as to their existence. As part of normalization, shade will put any -attribute from an OpenStack resource that is not in its data model contract -into an attribute called 'properties'. The contents of properties are -defined to be an arbitrary collection of key value pairs with no promises as -to any particular key ever existing. - -If a user passes `strict=True` to the shade constructor, shade will not pass -through arbitrary objects to the root of the resource, and will instead only -put them in the properties dict. If a user is worried about accidentally -writing code that depends on an attribute that is not part of the API contract, -this can be a useful tool. Keep in mind all data can still be accessed via -the properties dict, but any code touching anything in the properties dict -should be aware that the keys found there are highly user/cloud specific. -Any key that is transformed as part of the shade data model contract will -not wind up with an entry in properties - only keys that are unknown. - -Location --------- +*openstacksdk* has a very strict policy on not breaking backwards compatibility +ever. However, with the data structures returned from OpenStack, there are +places where the resource structures from OpenStack are returned to the user +somewhat directly, leaving an openstacksdk user open to changes/differences in +result content. + +To combat that, openstacksdk 'normalizes' the return structure from OpenStack +in many places, and the results of that normalization are listed below. Where +openstacksdk performs normalization, a user can count on any fields declared in +the docs as being completely safe to use - they are as much a part of +openstacksdk's API contract as any other Python method. + +Some OpenStack objects allow for arbitrary attributes at the root of the +object. openstacksdk will pass those through so as not to break anyone who may +be counting on them, but as they are arbitrary openstacksdk can make no +guarantees as to their existence. As part of normalization, openstacksdk will +put any attribute from an OpenStack resource that is not in its data model +contract into an attribute called 'properties'. The contents of properties are +defined to be an arbitrary collection of key value pairs with no promises as to +any particular key ever existing. + +If a user passes ``strict=True`` to the openstacksdk constructor, openstacksdk +will not pass through arbitrary objects to the root of the resource, and will +instead only put them in the properties dict. If a user is worried about +accidentally writing code that depends on an attribute that is not part of the +API contract, this can be a useful tool. Keep in mind all data can still be +accessed via the properties dict, but any code touching anything in the +properties dict should be aware that the keys found there are highly user/cloud +specific. Any key that is transformed as part of the openstacksdk data model +contract will not wind up with an entry in properties - only keys that are +unknown. + +The ``location`` field +---------------------- A Location defines where a resource lives. It includes a cloud name and a region name, an availability zone as well as information about the project that owns the resource. -The project information may contain a project id, or a combination of one or -more of a project name with a domain name or id. If a project id is present, +The project information may contain a project ID, or a combination of one or +more of a project name with a domain name or ID. If a project ID is present, it should be considered correct. Some resources do not carry ownership information with them. For those, the @@ -50,491 +51,16 @@ has a token for. Some resources do not have information about availability zones, or may exist region wide. Those resources will have None as their availability zone. -If all of the project information is None, then - -.. code-block:: python - - Location = dict( - cloud=str(), - region_name=str(), - zone=str() or None, - project=dict( - id=str() or None, - name=str() or None, - domain_id=str() or None, - domain_name=str() or None)) - - -Resources -========= - -Flavor ------- - -A flavor for a Nova Server. - -.. code-block:: python - - Flavor = dict( - location=Location(), - id=str(), - name=str(), - is_public=bool(), - is_disabled=bool(), - ram=int(), - vcpus=int(), - disk=int(), - ephemeral=int(), - swap=int(), - rxtx_factor=float(), - extra_specs=dict(), - properties=dict()) - - -Flavor Access -------------- - -An access entry for a Nova Flavor. - -.. code-block:: python - - FlavorAccess = dict( - flavor_id=str(), - project_id=str()) - - -Image ------ - -A Glance Image. - -.. code-block:: python - - Image = dict( - location=Location(), - id=str(), - name=str(), - min_ram=int(), - min_disk=int(), - size=int(), - virtual_size=int(), - container_format=str(), - disk_format=str(), - checksum=str(), - created_at=str(), - updated_at=str(), - owner=str(), - is_public=bool(), - is_protected=bool(), - visibility=str(), - status=str(), - locations=list(), - direct_url=str() or None, - tags=list(), - properties=dict()) - - -Keypair -------- - -A keypair for a Nova Server. - -.. code-block:: python - - Keypair = dict( - location=Location(), - name=str(), - id=str(), - public_key=str(), - fingerprint=str(), - type=str(), - user_id=str(), - private_key=str() or None - properties=dict()) - - -Security Group --------------- - -A Security Group from either Nova or Neutron - -.. code-block:: python - - SecurityGroup = dict( - location=Location(), - id=str(), - name=str(), - description=str(), - security_group_rules=list(), - properties=dict()) - -Security Group Rule -------------------- - -A Security Group Rule from either Nova or Neutron - -.. code-block:: python - - SecurityGroupRule = dict( - location=Location(), - id=str(), - direction=str(), # oneof('ingress', 'egress') - ethertype=str(), - port_range_min=int() or None, - port_range_max=int() or None, - protocol=str() or None, - remote_ip_prefix=str() or None, - security_group_id=str() or None, - remote_group_id=str() or None - properties=dict()) - -Server ------- - -A Server from Nova - -.. code-block:: python - - Server = dict( - location=Location(), - id=str(), - name=str(), - image=dict() or str(), - flavor=dict(), - volumes=list(), # Volume - interface_ip=str(), - has_config_drive=bool(), - accessIPv4=str(), - accessIPv6=str(), - addresses=dict(), # string, list(Address) - created=str(), - created_at=str(), - key_name=str(), - metadata=dict(), # string, string - private_v4=str(), - progress=int(), - public_v4=str(), - public_v6=str(), - security_groups=list(), # SecurityGroup - status=str(), - updated=str(), - user_id=str(), - host_id=str() or None, - power_state=str() or None, - task_state=str() or None, - vm_state=str() or None, - launched_at=str() or None, - terminated_at=str() or None, - task_state=str() or None, - block_device_mapping=dict() or None, - instance_name=str() or None, - hypervisor_name=str() or None, - tags=list(), - personality=str() or None, - scheduler_hints=str() or None, - user_data=str() or None, - properties=dict()) - -ComputeLimits -------------- - -Limits and current usage for a project in Nova - -.. code-block:: python - - ComputeLimits = dict( - location=Location(), - max_personality=int(), - max_personality_size=int(), - max_server_group_members=int(), - max_server_groups=int(), - max_server_meta=int(), - max_total_cores=int(), - max_total_instances=int(), - max_total_keypairs=int(), - max_total_ram_size=int(), - total_cores_used=int(), - total_instances_used=int(), - total_ram_used=int(), - total_server_groups_used=int(), - properties=dict()) - -ComputeUsage ------------- - -Current usage for a project in Nova - -.. code-block:: python - - ComputeUsage = dict( - location=Location(), - started_at=str(), - stopped_at=str(), - server_usages=list(), - max_personality=int(), - max_personality_size=int(), - max_server_group_members=int(), - max_server_groups=int(), - max_server_meta=int(), - max_total_cores=int(), - max_total_instances=int(), - max_total_keypairs=int(), - max_total_ram_size=int(), - total_cores_used=int(), - total_hours=int(), - total_instances_used=int(), - total_local_gb_usage=int(), - total_memory_mb_usage=int(), - total_ram_used=int(), - total_server_groups_used=int(), - total_vcpus_usage=int(), - properties=dict()) - -ServerUsage ------------ - -Current usage for a server in Nova - -.. code-block:: python - - ComputeUsage = dict( - started_at=str(), - ended_at=str(), - flavor=str(), - hours=int(), - instance_id=str(), - local_gb=int(), - memory_mb=int(), - name=str(), - state=str(), - uptime=int(), - vcpus=int(), - properties=dict()) - -Floating IP ------------ - -A Floating IP from Neutron or Nova - - -.. code-block:: python - - FloatingIP = dict( - location=Location(), - id=str(), - description=str(), - attached=bool(), - fixed_ip_address=str() or None, - floating_ip_address=str() or None, - network=str() or None, - port=str() or None, - router=str(), - status=str(), - created_at=str() or None, - updated_at=str() or None, - revision_number=int() or None, - properties=dict()) - -Volume ------- - -A volume from cinder. - -.. code-block:: python - - Volume = dict( - location=Location(), - id=str(), - name=str(), - description=str(), - size=int(), - attachments=list(), - status=str(), - migration_status=str() or None, - host=str() or None, - replication_driver=str() or None, - replication_status=str() or None, - replication_extended_status=str() or None, - snapshot_id=str() or None, - created_at=str(), - updated_at=str() or None, - source_volume_id=str() or None, - consistencygroup_id=str() or None, - volume_type=str() or None, - metadata=dict(), - is_bootable=bool(), - is_encrypted=bool(), - can_multiattach=bool(), - properties=dict()) - - -VolumeType ----------- - -A volume type from cinder. - -.. code-block:: python - - VolumeType = dict( - location=Location(), - id=str(), - name=str(), - description=str() or None, - is_public=bool(), - qos_specs_id=str() or None, - extra_specs=dict(), - properties=dict()) - - -VolumeTypeAccess ----------------- - -A volume type access from cinder. - -.. code-block:: python - - VolumeTypeAccess = dict( - location=Location(), - volume_type_id=str(), - project_id=str(), - properties=dict()) - - -ClusterTemplate ---------------- - -A Cluster Template from magnum. - -.. code-block:: python - - ClusterTemplate = dict( - location=Location(), - apiserver_port=int(), - cluster_distro=str(), - coe=str(), - created_at=str(), - dns_nameserver=str(), - docker_volume_size=int(), - external_network_id=str(), - fixed_network=str() or None, - flavor_id=str(), - http_proxy=str() or None, - https_proxy=str() or None, - id=str(), - image_id=str(), - insecure_registry=str(), - is_public=bool(), - is_registry_enabled=bool(), - is_tls_disabled=bool(), - keypair_id=str(), - labels=dict(), - master_flavor_id=str() or None, - name=str(), - network_driver=str(), - no_proxy=str() or None, - server_type=str(), - updated_at=str() or None, - volume_driver=str(), - properties=dict()) - -MagnumService -------------- - -A Magnum Service from magnum - -.. code-block:: python - - MagnumService = dict( - location=Location(), - binary=str(), - created_at=str(), - disabled_reason=str() or None, - host=str(), - id=str(), - report_count=int(), - state=str(), - properties=dict()) - -Stack ------ - -A Stack from Heat - -.. code-block:: python - - Stack = dict( - location=Location(), - id=str(), - name=str(), - created_at=str(), - deleted_at=str(), - updated_at=str(), - description=str(), - action=str(), - identifier=str(), - is_rollback_enabled=bool(), - notification_topics=list(), - outputs=list(), - owner=str(), - parameters=dict(), - parent=str(), - stack_user_project_id=str(), - status=str(), - status_reason=str(), - tags=dict(), - tempate_description=str(), - timeout_mins=int(), - properties=dict()) - -Identity Resources -================== - -Identity Resources are slightly different. - -They are global to a cloud, so location.availability_zone and -location.region_name and will always be None. If a deployer happens to deploy -OpenStack in such a way that users and projects are not shared amongst regions, -that necessitates treating each of those regions as separate clouds from -shade's POV. - -The Identity Resources that are not Project do not exist within a Project, -so all of the values in ``location.project`` will be None. - -Project -------- - -A Project from Keystone (or a tenant if Keystone v2) - -Location information for Project has some additional specific semantics. -If the project has a parent project, that will be in ``location.project.id``, -and if it doesn't that should be ``None``. - -If the Project is associated with a domain that will be in -``location.project.domain_id`` in addition to the normal ``domain_id`` -regardless of the current user's token scope. - -.. code-block:: python - - Project = dict( - location=Location(), - id=str(), - name=str(), - description=str(), - is_enabled=bool(), - is_domain=bool(), - domain_id=str(), - properties=dict()) - -Role ----- - -A Role from Keystone - .. code-block:: python - Project = dict( - location=Location(), - id=str(), - name=str(), - domain_id=str(), - properties=dict()) + Location = dict( + cloud=str(), + region_name=str(), + zone=str() or None, + project=dict( + id=str() or None, + name=str() or None, + domain_id=str() or None, + domain_name=str() or None, + ) + ) diff --git a/doc/source/user/multi-cloud-demo.rst b/doc/source/user/multi-cloud-demo.rst index 710055df4..7aace3f3c 100644 --- a/doc/source/user/multi-cloud-demo.rst +++ b/doc/source/user/multi-cloud-demo.rst @@ -1,4 +1,3 @@ -================ Multi-Cloud Demo ================ @@ -10,15 +9,12 @@ walk through it like a presentation, install `presentty` and run: presentty doc/source/user/multi-cloud-demo.rst The content is hopefully helpful even if it's not being narrated, so it's being -included in the `shade` docs. +included in the openstacksdk docs. .. _presentty: https://pypi.org/project/presentty -Using Multiple OpenStack Clouds Easily with Shade -================================================= - Who am I? -========= +--------- Monty Taylor @@ -27,7 +23,7 @@ Monty Taylor * twitter: @e_monty What are we going to talk about? -================================ +-------------------------------- `OpenStackSDK` @@ -43,22 +39,22 @@ What are we going to talk about? * Librified to re-use in Ansible OpenStackSDK is Free Software -============================= +----------------------------- * https://opendev.org/openstack/openstacksdk * openstack-discuss@lists.openstack.org * #openstack-sdks on oftc This talk is Free Software, too -=============================== +------------------------------- * Written for presentty (https://pypi.org/project/presentty) -* doc/source/multi-cloud-demo.rst -* examples in doc/source/examples -* Paths subject to change- this is the first presentation in tree! +* doc/source/user/multi-cloud-demo.rst +* examples in examples/cloud +* Paths subject to change - this is the first presentation in tree! Complete Example -================ +---------------- .. code:: python @@ -68,15 +64,19 @@ Complete Example openstack.enable_logging(debug=True) for cloud_name, region_name in [ - ('my-vexxhost', 'ca-ymq-1'), - ('my-citycloud', 'Buf1'), - ('my-internap', 'ams01')]: + ('my-vexxhost', 'ca-ymq-1'), + ('my-citycloud', 'Buf1'), + ('my-internap', 'ams01'), + ]: # Initialize cloud cloud = openstack.connect(cloud=cloud_name, region_name=region_name) # Upload an image to the cloud image = cloud.create_image( - 'devuan-jessie', filename='devuan-jessie.qcow2', wait=True) + 'devuan-jessie', + filename='devuan-jessie.qcow2', + wait=True, + ) # Find a flavor with at least 512M of RAM flavor = cloud.get_flavor_by_ram(512) @@ -84,19 +84,24 @@ Complete Example # Boot a server, wait for it to boot, and then do whatever is needed # to get a public ip for it. cloud.create_server( - 'my-server', image=image, flavor=flavor, wait=True, auto_ip=True) + 'my-server', + image=image, + flavor=flavor, + wait=True, + auto_ip=True, + ) Let's Take a Few Steps Back -=========================== +--------------------------- Multi-cloud is easy, but you need to know a few things. * Terminology * Config -* Shade API +* OpenStackSDK API Cloud Terminology -================= +----------------- Let's define a few terms, so that we can use them with ease: @@ -108,7 +113,7 @@ Let's define a few terms, so that we can use them with ease: * `domain` - collection of users and projects Cloud Terminology Relationships -=============================== +------------------------------- * A `cloud` has one or more `regions` * A `patron` has one or more `users` @@ -121,14 +126,14 @@ Cloud Terminology Relationships * A `user` has one or more `roles` on one or more `projects` HTTP Sessions -============= +------------- * HTTP interactions are authenticated via keystone * Authenticating returns a `token` * An authenticated HTTP Session is shared across a `region` Cloud Regions -============= +------------- A `cloud region` is the basic unit of REST interaction. @@ -138,7 +143,7 @@ A `cloud region` is the basic unit of REST interaction. * A `region` is completely autonomous Users, Projects and Domains -=========================== +--------------------------- In clouds with multiple domains, project and user names are only unique within a region. @@ -151,12 +156,12 @@ only unique within a region. * `user_id` does not Confused Yet? -============= +------------- Don't worry - you don't have to deal with most of that. Auth per cloud, select per region -================================= +--------------------------------- In general, the thing you need to know is: @@ -164,7 +169,7 @@ In general, the thing you need to know is: * Select config to use by `cloud` and `region` clouds.yaml -=========== +----------- Information about the clouds you want to connect to is stored in a file called `clouds.yaml`. @@ -178,7 +183,7 @@ Full docs on `clouds.yaml` are at https://docs.openstack.org/os-client-config/latest/ What about Mac and Windows? -=========================== +--------------------------- `USER_CONFIG_DIR` is different on Linux, OSX and Windows. @@ -193,7 +198,7 @@ What about Mac and Windows? * Windows: `C:\\ProgramData\\OpenStack\\openstack` Config Terminology -================== +------------------ For multi-cloud, think of two types: @@ -203,7 +208,7 @@ For multi-cloud, think of two types: Apologies for the use of `cloud` twice. Environment Variables and Simple Usage -====================================== +-------------------------------------- * Environment variables starting with `OS_` go into a cloud called `envvars` * If you only have one cloud, you don't have to specify it @@ -211,10 +216,10 @@ Environment Variables and Simple Usage `cloud` and `region_name` TOO MUCH TALKING - NOT ENOUGH CODE -================================== +---------------------------------- basic clouds.yaml for the example code -====================================== +-------------------------------------- Simple example of a clouds.yaml @@ -239,14 +244,14 @@ Simple example of a clouds.yaml Where's the password? secure.yaml -=========== +----------- * Optional additional file just like `clouds.yaml` * Values overlaid on `clouds.yaml` * Useful if you want to protect secrets more stringently Example secure.yaml -=================== +------------------- * No, my password isn't XXXXXXXX * `cloud` name should match `clouds.yaml` @@ -260,7 +265,7 @@ Example secure.yaml password: XXXXXXXX more clouds.yaml -================ +---------------- More information can be provided. @@ -281,7 +286,7 @@ More information can be provided. username: 0b8c435b-cc4d-4e05-8a47-a2ada0539af1 Much more complex clouds.yaml example -===================================== +------------------------------------- * Not using a profile - all settings included * In the `ams01` `region` there are two networks with undiscoverable qualities @@ -310,7 +315,7 @@ Much more complex clouds.yaml example routes_externally: false Complete Example Again -====================== +---------------------- .. code:: python @@ -339,17 +344,17 @@ Complete Example Again 'my-server', image=image, flavor=flavor, wait=True, auto_ip=True) Step By Step -============ +------------ Import the library -================== +------------------ .. code:: python from openstack import cloud as openstack Logging -======= +------- * `openstacksdk` uses standard python logging * ``openstack.enable_logging`` does easy defaults @@ -357,7 +362,7 @@ Logging * `debug` - * Logs shade loggers at debug level + * Logs openstacksdk loggers at debug level * `http_debug` Implies `debug`, turns on HTTP tracing @@ -367,23 +372,22 @@ Logging openstack.enable_logging(debug=True) Example with Debug Logging -========================== +-------------------------- -* doc/source/examples/debug-logging.py +* examples/cloud/debug-logging.py .. code:: python from openstack import cloud as openstack openstack.enable_logging(debug=True) - cloud = openstack.connect( - cloud='my-vexxhost', region_name='ca-ymq-1') + cloud = openstack.connect(cloud='my-vexxhost', region_name='ca-ymq-1') cloud.get_image('Ubuntu 16.04.1 LTS [2017-03-03]') Example with HTTP Debug Logging -=============================== +------------------------------- -* doc/source/examples/http-debug-logging.py +* examples/cloud/http-debug-logging.py .. code:: python @@ -395,7 +399,7 @@ Example with HTTP Debug Logging cloud.get_image('Ubuntu 16.04.1 LTS [2017-03-03]') Cloud Regions -============= +------------- * `cloud` constructor needs `cloud` and `region_name` * `openstack.connect` is a helper factory function @@ -403,14 +407,15 @@ Cloud Regions .. code:: python for cloud_name, region_name in [ - ('my-vexxhost', 'ca-ymq-1'), - ('my-citycloud', 'Buf1'), - ('my-internap', 'ams01')]: + ('my-vexxhost', 'ca-ymq-1'), + ('my-citycloud', 'Buf1'), + ('my-internap', 'ams01') + ]: # Initialize cloud cloud = openstack.connect(cloud=cloud_name, region_name=region_name) Upload an Image -=============== +--------------- * Picks the correct upload mechanism * **SUGGESTION** Always upload your own base images @@ -419,10 +424,13 @@ Upload an Image # Upload an image to the cloud image = cloud.create_image( - 'devuan-jessie', filename='devuan-jessie.qcow2', wait=True) + 'devuan-jessie', + filename='devuan-jessie.qcow2', + wait=True, + ) Always Upload an Image -====================== +---------------------- Ok. You don't have to. But, for multi-cloud... @@ -432,7 +440,7 @@ Ok. You don't have to. But, for multi-cloud... * Download from OS vendor or build with `diskimage-builder` Find a flavor -============= +------------- * Flavors are all named differently on clouds * Flavors can be found via RAM @@ -444,7 +452,7 @@ Find a flavor flavor = cloud.get_flavor_by_ram(512) Create a server -=============== +--------------- * my-vexxhost @@ -472,15 +480,15 @@ Create a server 'my-server', image=image, flavor=flavor, wait=True, auto_ip=True) Wow. We didn't even deploy Wordpress! -===================================== +------------------------------------- Image and Flavor by Name or ID -============================== +------------------------------ * Pass string to image/flavor * Image/Flavor will be found by name or ID * Common pattern -* doc/source/examples/create-server-name-or-id.py +* examples/cloud/create-server-name-or-id.py .. code:: python @@ -509,11 +517,8 @@ Image and Flavor by Name or ID # Delete it - this is a demo cloud.delete_server(server, wait=True, delete_ips=True) -cloud.pprint method was just added this morning -=============================================== - Delete Servers -============== +-------------- * `delete_ips` Delete any `floating_ips` the server may have @@ -522,12 +527,12 @@ Delete Servers cloud.delete_server('my-server', wait=True, delete_ips=True) Image and Flavor by Dict -======================== +------------------------ * Pass dict to image/flavor * If you know if the value is Name or ID * Common pattern -* doc/source/examples/create-server-dict.py +* examples/cloud/create-server-dict.py .. code:: python @@ -555,10 +560,10 @@ Image and Flavor by Dict cloud.delete_server(server, wait=True, delete_ips=True) Munch Objects -============= +------------- * Behave like a dict and an object -* doc/source/examples/munch-dict-object.py +* examples/cloud/munch-dict-object.py .. code:: python @@ -571,7 +576,7 @@ Munch Objects print(image['name']) API Organized by Logical Resource -================================= +--------------------------------- * list_servers * search_servers @@ -587,10 +592,10 @@ For other things, it's still {verb}_{noun} * add_auto_ip Cleanup Script -============== +-------------- * Sometimes my examples had bugs -* doc/source/examples/cleanup-servers.py +* examples/cloud/cleanup-servers.py .. code:: python @@ -609,10 +614,9 @@ Cleanup Script cloud.delete_server(server, wait=True, delete_ips=True) Normalization -============= +------------- -* https://docs.openstack.org/shade/latest/user/model.html#image -* doc/source/examples/normalization.py +* examples/cloud/normalization.py .. code:: python @@ -625,10 +629,10 @@ Normalization cloud.pprint(image) Strict Normalized Results -========================= +------------------------- * Return only the declared model -* doc/source/examples/strict-mode.py +* examples/cloud/strict-mode.py .. code:: python @@ -642,10 +646,10 @@ Strict Normalized Results cloud.pprint(image) How Did I Find the Image Name for the Last Example? -=================================================== +--------------------------------------------------- * I often make stupid little utility scripts -* doc/source/examples/find-an-image.py +* examples/cloud/find-an-image.py .. code:: python @@ -658,7 +662,7 @@ How Did I Find the Image Name for the Last Example? if 'ubuntu' in image.name.lower()]) Added / Modified Information -============================ +---------------------------- * Servers need more extra help * Fetch addresses dict from neutron @@ -666,7 +670,7 @@ Added / Modified Information * `detailed` - defaults to True, add everything * `bare` - no extra calls - don't even fix broken things * `bare` is still normalized -* doc/source/examples/server-information.py +* examples/cloud/server-information.py .. code:: python @@ -694,9 +698,9 @@ Added / Modified Information cloud.delete_server(server, wait=True, delete_ips=True) Exceptions -========== +---------- -* All shade exceptions are subclasses of `OpenStackCloudException` +* All openstacksdk exceptions are subclasses of `OpenStackCloudException` * Direct REST calls throw `OpenStackCloudHTTPError` * `OpenStackCloudHTTPError` subclasses `OpenStackCloudException` and `requests.exceptions.HTTPError` @@ -704,11 +708,11 @@ Exceptions * `OpenStackCloudBadRequest` for 400 User Agent Info -=============== +--------------- * Set `app_name` and `app_version` for User Agents -* (sssh ... `region_name` is optional if the cloud has one region) -* doc/source/examples/user-agent.py +* (ssh ... `region_name` is optional if the cloud has one region) +* examples/cloud/user-agent.py .. code:: python @@ -716,17 +720,20 @@ User Agent Info openstack.enable_logging(http_debug=True) cloud = openstack.connect( - cloud='datacentred', app_name='AmazingApp', app_version='1.0') + cloud='datacentred', + app_name='AmazingApp', + app_version='1.0', + ) cloud.list_networks() Uploading Large Objects -======================= +----------------------- * swift has a maximum object size * Large Objects are uploaded specially -* shade figures this out and does it +* openstacksdk figures this out and does it * multi-threaded -* doc/source/examples/upload-object.py +* examples/cloud/upload-object.py .. code:: python @@ -735,19 +742,21 @@ Uploading Large Objects cloud = openstack.connect(cloud='ovh', region_name='SBG1') cloud.create_object( - container='my-container', name='my-object', - filename='/home/mordred/briarcliff.sh3d') + container='my-container', + name='my-object', + filename='/home/mordred/briarcliff.sh3d', + ) cloud.delete_object('my-container', 'my-object') cloud.delete_container('my-container') Uploading Large Objects -======================= +----------------------- * Default max_file_size is 5G * This is a conference demo * Let's force a segment_size * One MILLION bytes -* doc/source/examples/upload-object.py +* examples/cloud/upload-object.py .. code:: python @@ -756,14 +765,16 @@ Uploading Large Objects cloud = openstack.connect(cloud='ovh', region_name='SBG1') cloud.create_object( - container='my-container', name='my-object', + container='my-container', + name='my-object', filename='/home/mordred/briarcliff.sh3d', - segment_size=1000000) + segment_size=1000000, + ) cloud.delete_object('my-container', 'my-object') cloud.delete_container('my-container') Service Conditionals -==================== +-------------------- .. code:: python @@ -775,7 +786,7 @@ Service Conditionals print(cloud.has_service('container-orchestration')) Service Conditional Overrides -============================= +----------------------------- * Sometimes clouds are weird and figuring that out won't work @@ -798,12 +809,5 @@ Service Conditional Overrides # This is already in profile: rackspace has_network: false -Coming Soon -=========== - -* Completion of RESTification -* Full version discovery support -* Multi-cloud facade layer -* Microversion support (talk tomorrow) -* Completion of caching tier (talk tomorrow) -* All of you helping hacking on shade!!! (we're friendly) +FIN +--- diff --git a/doc/source/user/resource.rst b/doc/source/user/resource.rst index 8453265f5..c7b5ae2d9 100644 --- a/doc/source/user/resource.rst +++ b/doc/source/user/resource.rst @@ -1,7 +1,3 @@ -**Note: This class is in the process of being applied as the new base class -for resources around the OpenStack SDK. Once that has been completed, -this module will be drop the 2 suffix and be the only resource module.** - Resource ======== .. automodule:: openstack.resource From 7c5ce14588f616d47bca35c3e10ee87063cb4c95 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 31 May 2023 17:38:10 +0100 Subject: [PATCH 3292/3836] tox: Disable E501 This is more trouble than it's worth now. Black takes care of this for us. I wish I'd decided this before the blackify series :( Change-Id: Idae7a151fa4f24b932a130669fce563e7150278b Signed-off-by: Stephen Finucane --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 54936d374..60da4bcaa 100644 --- a/tox.ini +++ b/tox.ini @@ -128,13 +128,14 @@ application-import-names = openstack # However, if you feel strongly about it, patches will be accepted to fix them # if they fix ALL of the occurances of one and only one of them. # E203 Black will put spaces after colons in list comprehensions +# E501 Black takes care of line length for us # H238 New Style Classes are the default in Python3 # H301 Black will put commas after imports that can't fit on one line # H4 Are about docstrings and there's just a huge pile of pre-existing issues. # W503 Is supposed to be off by default but in the latest pycodestyle isn't. # Also, both openstacksdk and Donald Knuth disagree with the rule. Line # breaks should occur before the binary operator for readability. -ignore = E203, H301, H238, H4, W503 +ignore = E203, E501, H301, H238, H4, W503 import-order-style = pep8 show-source = True exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build,openstack/_services_mixin.py From 0adf8f5601001b3b768d62d787b730f8fd531202 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 2 Jun 2023 13:57:27 +0100 Subject: [PATCH 3293/3836] volume: Add missing attributes to Extension We also rename 'updated' to 'updated_at' to match the 'Extension' objects provided by other services. Change-Id: I69522bf820e5cf4546ccf99da2c7373218785d9c --- openstack/block_storage/v3/extension.py | 8 +++++--- .../tests/functional/block_storage/v3/test_extension.py | 2 +- openstack/tests/unit/block_storage/v3/test_extension.py | 4 +++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/openstack/block_storage/v3/extension.py b/openstack/block_storage/v3/extension.py index a9de9df49..7f95a5aa4 100644 --- a/openstack/block_storage/v3/extension.py +++ b/openstack/block_storage/v3/extension.py @@ -14,8 +14,6 @@ class Extension(resource.Resource): - """Extension""" - resources_key = "extensions" base_path = "/extensions" @@ -27,6 +25,10 @@ class Extension(resource.Resource): alias = resource.Body('alias', type=str) #: The extension description. description = resource.Body('description', type=str) + #: Links pertaining to this extension. + links = resource.Body('links', type=list) + #: The name of this extension. + name = resource.Body('name') #: The date and time when the resource was updated. #: The date and time stamp format is ISO 8601. - updated = resource.Body('updated', type=str) + updated_at = resource.Body('updated', type=str) diff --git a/openstack/tests/functional/block_storage/v3/test_extension.py b/openstack/tests/functional/block_storage/v3/test_extension.py index 48ec27118..2313f9024 100644 --- a/openstack/tests/functional/block_storage/v3/test_extension.py +++ b/openstack/tests/functional/block_storage/v3/test_extension.py @@ -20,4 +20,4 @@ def test_get(self): for extension in extensions: self.assertIsInstance(extension.alias, str) self.assertIsInstance(extension.description, str) - self.assertIsInstance(extension.updated, str) + self.assertIsInstance(extension.updated_at, str) diff --git a/openstack/tests/unit/block_storage/v3/test_extension.py b/openstack/tests/unit/block_storage/v3/test_extension.py index ca98da237..5c092b70e 100644 --- a/openstack/tests/unit/block_storage/v3/test_extension.py +++ b/openstack/tests/unit/block_storage/v3/test_extension.py @@ -38,4 +38,6 @@ def test_make_extension(self): self.assertEqual( EXTENSION['description'], extension_resource.description ) - self.assertEqual(EXTENSION['updated'], extension_resource.updated) + self.assertEqual(EXTENSION['links'], extension_resource.links) + self.assertEqual(EXTENSION['name'], extension_resource.name) + self.assertEqual(EXTENSION['updated'], extension_resource.updated_at) From e94830ddeaff4acc2223e72de19b4cd778b2259d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 2 Jun 2023 17:51:38 +0100 Subject: [PATCH 3294/3836] Remove unnecessary quotes This was a side-effect of the blacken process. Change-Id: Id07185012b6f98f4027ee35f97560d469c78ddc4 Signed-off-by: Stephen Finucane --- openstack/baremetal/v1/node.py | 2 +- openstack/block_storage/v2/backup.py | 2 +- openstack/block_storage/v3/backup.py | 2 +- openstack/block_storage/v3/type.py | 2 +- openstack/cloud/_floating_ip.py | 2 +- openstack/cloud/_security_group.py | 6 ++---- openstack/cloud/openstackcloud.py | 2 +- openstack/compute/v2/server_remote_console.py | 2 +- openstack/network/v2/_proxy.py | 12 ++++++------ openstack/object_store/v1/_proxy.py | 2 +- openstack/tests/functional/cloud/test_qos_policy.py | 2 +- .../unit/accelerator/v2/test_accelerator_request.py | 2 +- openstack/tests/unit/base.py | 6 +++--- openstack/tests/unit/cloud/test_accelerator.py | 2 +- openstack/tests/unit/cloud/test_caching.py | 2 +- openstack/tests/unit/network/v2/test_proxy.py | 2 +- openstack/tests/unit/object_store/v1/test_proxy.py | 4 ++-- openstack/tests/unit/orchestration/v1/test_proxy.py | 2 +- openstack/tests/unit/orchestration/v1/test_stack.py | 10 +++++----- openstack/tests/unit/test_proxy.py | 2 +- openstack/tests/unit/test_resource.py | 2 +- openstack/utils.py | 2 +- 22 files changed, 35 insertions(+), 37 deletions(-) diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index fe1081371..0ec30198f 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -453,7 +453,7 @@ def set_provision_state( if clean_steps is not None: if target != 'clean': raise ValueError( - 'Clean steps can only be provided with ' '"clean" target' + 'Clean steps can only be provided with "clean" target' ) body['clean_steps'] = clean_steps diff --git a/openstack/block_storage/v2/backup.py b/openstack/block_storage/v2/backup.py index 3355424ef..a19490abd 100644 --- a/openstack/block_storage/v2/backup.py +++ b/openstack/block_storage/v2/backup.py @@ -180,7 +180,7 @@ def restore(self, session, volume_id=None, name=None): body['restore']['name'] = name if not (volume_id or name): raise exceptions.SDKException( - 'Either of `name` or `volume_id`' ' must be specified.' + 'Either of `name` or `volume_id` must be specified.' ) response = session.post(url, json=body) self._translate_response(response, has_body=False) diff --git a/openstack/block_storage/v3/backup.py b/openstack/block_storage/v3/backup.py index 74374e490..a5c5bae56 100644 --- a/openstack/block_storage/v3/backup.py +++ b/openstack/block_storage/v3/backup.py @@ -194,7 +194,7 @@ def restore(self, session, volume_id=None, name=None): body['restore']['name'] = name if not (volume_id or name): raise exceptions.SDKException( - 'Either of `name` or `volume_id`' ' must be specified.' + 'Either of `name` or `volume_id` must be specified.' ) response = session.post(url, json=body) self._translate_response(response, has_body=False) diff --git a/openstack/block_storage/v3/type.py b/openstack/block_storage/v3/type.py index 59931373e..77ed0bd68 100644 --- a/openstack/block_storage/v3/type.py +++ b/openstack/block_storage/v3/type.py @@ -42,7 +42,7 @@ def _extra_specs(self, method, key=None, delete=False, extra_specs=None): for k, v in extra_specs.items(): if not isinstance(v, str): raise ValueError( - "The value for %s (%s) must be " "a text string" % (k, v) + "The value for %s (%s) must be a text string" % (k, v) ) if key is not None: diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index 30b1f32e7..d1edbed24 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -535,7 +535,7 @@ def _neutron_create_floating_ip( try: for count in utils.iterate_timeout( timeout, - "Timeout waiting for the floating IP" " to be ACTIVE", + "Timeout waiting for the floating IP to be ACTIVE", wait=self._FLOAT_AGE, ): fip = self.get_floating_ip(fip_id) diff --git a/openstack/cloud/_security_group.py b/openstack/cloud/_security_group.py index 5f9e2254b..44a1bfdd8 100644 --- a/openstack/cloud/_security_group.py +++ b/openstack/cloud/_security_group.py @@ -107,14 +107,12 @@ def get_security_group_by_id(self, id): raise exc.OpenStackCloudUnavailableFeature( "Unavailable feature: security groups" ) - error_message = "Error getting security group with" " ID {id}".format( - id=id - ) + error_message = f"Error getting security group with ID {id}" if self._use_neutron_secgroups(): return self.network.get_security_group(id) else: data = proxy._json_response( - self.compute.get('/os-security-groups/{id}'.format(id=id)), + self.compute.get(f'/os-security-groups/{id}'), error_message=error_message, ) return self._normalize_secgroup( diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index c68bdff72..f1dbc2af9 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -763,7 +763,7 @@ def has_service(self, service_key, version=None): and self._disable_warnings[service_key] ): self.log.debug( - "Disabling %(service_key)s entry in catalog" " per config", + "Disabling %(service_key)s entry in catalog per config", {'service_key': service_key}, ) self._disable_warnings[service_key] = True diff --git a/openstack/compute/v2/server_remote_console.py b/openstack/compute/v2/server_remote_console.py index bc66feb92..cfa24a1a2 100644 --- a/openstack/compute/v2/server_remote_console.py +++ b/openstack/compute/v2/server_remote_console.py @@ -53,7 +53,7 @@ def create(self, session, prepend_key=True, base_path=None, **params): and self.type == 'webmks' ): raise ValueError( - 'Console type webmks is not supported on ' 'server side' + 'Console type webmks is not supported on server side' ) return super(ServerRemoteConsole, self).create( session, prepend_key=prepend_key, base_path=base_path, **params diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index c7a3a60bc..d891cf7d8 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -628,7 +628,7 @@ def delete_bgp_peer(self, peer, ignore_missing=True): self._delete(_bgp_peer.BgpPeer, peer, ignore_missing=ignore_missing) def find_bgp_peer(self, name_or_id, ignore_missing=True, **query): - """ "Find a single BGP Peer""" + """Find a single BGP Peer""" return self._find( _bgp_peer.BgpPeer, name_or_id, @@ -659,7 +659,7 @@ def delete_bgp_speaker(self, speaker, ignore_missing=True): ) def find_bgp_speaker(self, name_or_id, ignore_missing=True, **query): - """ "Find a single BGP Peer""" + """Find a single BGP Peer""" return self._find( _bgp_speaker.BgpSpeaker, name_or_id, @@ -756,7 +756,7 @@ def delete_bgpvpn(self, bgpvpn, ignore_missing=True): self._delete(_bgpvpn.BgpVpn, bgpvpn, ignore_missing=ignore_missing) def find_bgpvpn(self, name_or_id, ignore_missing=True, **query): - """ "Find a single BGPVPN + """Find a single BGPVPN :param name_or_id: The name or ID of a BGPVPN. :param bool ignore_missing: When set to ``False`` @@ -949,7 +949,7 @@ def delete_bgpvpn_port_association( def find_bgpvpn_port_association( self, name_or_id, bgpvpn_id, ignore_missing=True, **query ): - """ "Find a single BGPVPN Port Association + """Find a single BGPVPN Port Association :param name_or_id: The name or ID of a BgpVpnNetworkAssociation. :param bgpvpn_id: The value can be the ID of a BGPVPN. @@ -6151,7 +6151,7 @@ def delete_tap_flow(self, tap_flow, ignore_missing=True): ) def find_tap_flow(self, name_or_id, ignore_missing=True, **query): - """ "Find a single Tap Service""" + """Find a single Tap Service""" return self._find( _tap_flow.TapFlow, name_or_id, @@ -6182,7 +6182,7 @@ def delete_tap_service(self, tap_service, ignore_missing=True): ) def find_tap_service(self, name_or_id, ignore_missing=True, **query): - """ "Find a single Tap Service""" + """Find a single Tap Service""" return self._find( _tap_service.TapService, name_or_id, diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index 4e45851f3..ea873728f 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -1030,7 +1030,7 @@ def generate_temp_url( raise ValueError('path must at least contain /v1/a/c/') else: raise ValueError( - 'path must be full path to an object' ' e.g. /v1/a/c/o' + 'path must be full path to an object e.g. /v1/a/c/o' ) standard_methods = ['GET', 'PUT', 'HEAD', 'POST', 'DELETE'] diff --git a/openstack/tests/functional/cloud/test_qos_policy.py b/openstack/tests/functional/cloud/test_qos_policy.py index b55b17d2f..a341f9a12 100644 --- a/openstack/tests/functional/cloud/test_qos_policy.py +++ b/openstack/tests/functional/cloud/test_qos_policy.py @@ -66,7 +66,7 @@ def test_create_qos_policy_shared(self): def test_create_qos_policy_default(self): if not self.operator_cloud._has_neutron_extension('qos-default'): self.skipTest( - "'qos-default' network extension not supported " "by cloud" + "'qos-default' network extension not supported by cloud" ) policy = self.operator_cloud.create_qos_policy( name=self.policy_name, default=True diff --git a/openstack/tests/unit/accelerator/v2/test_accelerator_request.py b/openstack/tests/unit/accelerator/v2/test_accelerator_request.py index 641d60b7e..87ed69fd8 100644 --- a/openstack/tests/unit/accelerator/v2/test_accelerator_request.py +++ b/openstack/tests/unit/accelerator/v2/test_accelerator_request.py @@ -18,7 +18,7 @@ FAKE_RP_UUID = 'f4b7fe6c-8ab4-4914-a113-547af022935b' FAKE_INSTANCE_UUID = '1ce4a597-9836-4e02-bea1-a3a6cbe7b9f9' FAKE_ATTACH_INFO_STR = ( - '{"bus": "5e", ' '"device": "00", ' '"domain": "0000", ' '"function": "1"}' + '{"bus": "5e", "device": "00", "domain": "0000", "function": "1"}' ) FAKE = { diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 36091f4fc..96e5effd7 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -57,7 +57,7 @@ _DomainData = collections.namedtuple( 'DomainData', - 'domain_id, domain_name, description, json_response, ' 'json_request', + 'domain_id, domain_name, description, json_response, json_request', ) @@ -223,7 +223,7 @@ def mock_for_keystone_projects( assert not (project or project_list) else: raise Exception( - 'Must specify a project, project_list, ' 'or project_count' + 'Must specify a project, project_list, or project_count' ) assert list_get or id_get @@ -362,7 +362,7 @@ def _get_user_data(self, name=None, password=None, **kwargs): self.assertIs( 0, len(kwargs), - message='extra key-word args received ' 'on _get_user_data', + message='extra key-word args received on _get_user_data', ) return _UserData( diff --git a/openstack/tests/unit/cloud/test_accelerator.py b/openstack/tests/unit/cloud/test_accelerator.py index e42757073..5afe6f3f6 100644 --- a/openstack/tests/unit/cloud/test_accelerator.py +++ b/openstack/tests/unit/cloud/test_accelerator.py @@ -64,7 +64,7 @@ ARQ_DEV_RP_UUID = uuid.uuid4().hex ARQ_INSTANCE_UUID = uuid.uuid4().hex ARQ_ATTACH_INFO_STR = ( - '{"bus": "5e", ' '"device": "00", ' '"domain": "0000", ' '"function": "1"}' + '{"bus": "5e", "device": "00", "domain": "0000", "function": "1"}' ) ARQ_DICT = { 'uuid': ARQ_UUID, diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py index d137d15ab..579ae1b94 100644 --- a/openstack/tests/unit/cloud/test_caching.py +++ b/openstack/tests/unit/cloud/test_caching.py @@ -78,7 +78,7 @@ def _(msg): }, "expires_at": { "description": _( - "Datetime when this resource would be" " subject to removal" + "Datetime when this resource would be subject to removal" ), "type": ["null", "string"], }, diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 3b5132846..9747fb0d1 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -173,7 +173,7 @@ def test_address_group_update(self): ) @mock.patch( - 'openstack.network.v2._proxy.Proxy.' 'add_addresses_to_address_group' + 'openstack.network.v2._proxy.Proxy.add_addresses_to_address_group' ) def test_add_addresses_to_address_group(self, add_addresses): data = mock.sentinel diff --git a/openstack/tests/unit/object_store/v1/test_proxy.py b/openstack/tests/unit/object_store/v1/test_proxy.py index cd4d0274a..3005dc17b 100644 --- a/openstack/tests/unit/object_store/v1/test_proxy.py +++ b/openstack/tests/unit/object_store/v1/test_proxy.py @@ -353,7 +353,7 @@ class TestTempURL(TestObjectStoreProxy): expires_iso8601_format = '%Y-%m-%dT%H:%M:%SZ' short_expires_iso8601_format = '%Y-%m-%d' time_errmsg = ( - 'time must either be a whole number or in specific ' 'ISO 8601 format.' + 'time must either be a whole number or in specific ISO 8601 format.' ) path_errmsg = 'path must be full path to an object e.g. /v1/a/c/o' url = '/v1/AUTH_account/c/o' @@ -361,7 +361,7 @@ class TestTempURL(TestObjectStoreProxy): key = 'correcthorsebatterystaple' method = 'GET' expected_url = url + ( - '?temp_url_sig=temp_url_signature' '&temp_url_expires=1400003600' + '?temp_url_sig=temp_url_signature&temp_url_expires=1400003600' ) expected_body = '\n'.join( [ diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index 013ee9007..fe37c70e0 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -397,7 +397,7 @@ def test_validate_template_invalid_request(self): template_url=None, ) self.assertEqual( - "'template_url' must be specified when template is " "None", + "'template_url' must be specified when template is None", str(err), ) diff --git a/openstack/tests/unit/orchestration/v1/test_stack.py b/openstack/tests/unit/orchestration/v1/test_stack.py index ecb485c9a..7eebade8a 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack.py +++ b/openstack/tests/unit/orchestration/v1/test_stack.py @@ -57,7 +57,7 @@ { 'updated_time': 'datetime', 'resource_name': '', - 'physical_resource_id': '{resource id or ' '}', + 'physical_resource_id': '{resource id or }', 'resource_action': 'CREATE', 'resource_status': 'COMPLETE', 'resource_status_reason': '', @@ -70,7 +70,7 @@ { 'updated_time': 'datetime', 'resource_name': '', - 'physical_resource_id': '{resource id or ' '}', + 'physical_resource_id': '{resource id or }', 'resource_action': 'CREATE', 'resource_status': 'COMPLETE', 'resource_status_reason': '', @@ -83,7 +83,7 @@ { 'updated_time': 'datetime', 'resource_name': '', - 'physical_resource_id': '{resource id or ' '}', + 'physical_resource_id': '{resource id or }', 'resource_action': 'CREATE', 'resource_status': 'COMPLETE', 'resource_status_reason': '', @@ -96,7 +96,7 @@ { 'updated_time': 'datetime', 'resource_name': '', - 'physical_resource_id': '{resource id or ' '}', + 'physical_resource_id': '{resource id or }', 'resource_action': 'CREATE', 'resource_status': 'COMPLETE', 'resource_status_reason': '', @@ -109,7 +109,7 @@ { 'updated_time': 'datetime', 'resource_name': '', - 'physical_resource_id': '{resource id or ' '}', + 'physical_resource_id': '{resource id or }', 'resource_action': 'CREATE', 'resource_status': 'COMPLETE', 'resource_status_reason': '', diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index 45cff353f..c6526a6c6 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -639,7 +639,7 @@ def setUp(self): self.sot.service_type = 'srv' def _get_key(self, id): - return f"srv.fake.fake/{id}." "{'microversion': None, 'params': {}}" + return "srv.fake.fake/%s.{'microversion': None, 'params': {}}" % id def test_get_not_in_cache(self): self.cloud._cache_expirations['srv.fake'] = 5 diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index e23f7fc1d..c05b6c4d4 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -941,7 +941,7 @@ class Test(resource.Resource): res._translate_response(response) expected = ( - '{"foo": "new_bar", "id": null, ' '"location": null, "name": null}' + '{"foo": "new_bar", "id": null, "location": null, "name": null}' ) actual = json.dumps(res, sort_keys=True) self.assertEqual(expected, actual) diff --git a/openstack/utils.py b/openstack/utils.py index 78d9a22e9..a79d051bd 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -367,7 +367,7 @@ def __next__(self): except queue.Empty: raise exceptions.SDKException( - 'Timeout waiting for ' 'cleanup task to complete' + 'Timeout waiting for cleanup task to complete' ) else: raise StopIteration From 1c3157e1ed24d9b76c64297f9ec4803a7a68f2cf Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 9 Jun 2023 18:19:33 +0100 Subject: [PATCH 3295/3836] Fix typo Change-Id: I4beae6dc14f28b9962ff23dd3a623b0aa59ac957 Signed-off-by: Stephen Finucane --- openstack/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/utils.py b/openstack/utils.py index 5935e0fbe..e9864dc89 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -109,7 +109,7 @@ def supports_version( """Determine if the given adapter supports the given version. Checks the version asserted by the service and ensures this matches the - provided version. ``version`` can be a major version of a major-minor + provided version. ``version`` can be a major version or a major-minor version :param adapter: :class:`~keystoneauth1.adapter.Adapter` instance. From 200cd5c4f8f06d69ae886880fdcf9f2000463c20 Mon Sep 17 00:00:00 2001 From: whoami-rajat Date: Thu, 25 May 2023 18:57:04 +0530 Subject: [PATCH 3296/3836] Add block storage summary support Added support to provide block storage summary. Change-Id: I1e665a45c8c2c3658099aa6aa070facb3a6c9e60 --- doc/source/user/proxies/block_storage_v3.rst | 8 +++ .../user/resources/block_storage/index.rst | 1 + .../v3/block_storage_summary.rst | 14 ++++ openstack/block_storage/v3/_proxy.py | 22 ++++++ .../block_storage/v3/block_storage_summary.py | 30 ++++++++ .../v3/test_block_storage_summary.py | 21 ++++++ .../v3/test_block_storage_summary.py | 69 +++++++++++++++++++ ...rage-summary-support-dd00d424c4e6a3b1.yaml | 5 ++ 8 files changed, 170 insertions(+) create mode 100644 doc/source/user/resources/block_storage/v3/block_storage_summary.rst create mode 100644 openstack/block_storage/v3/block_storage_summary.py create mode 100644 openstack/tests/functional/block_storage/v3/test_block_storage_summary.py create mode 100644 openstack/tests/unit/block_storage/v3/test_block_storage_summary.py create mode 100644 releasenotes/notes/add-block-storage-summary-support-dd00d424c4e6a3b1.yaml diff --git a/doc/source/user/proxies/block_storage_v3.rst b/doc/source/user/proxies/block_storage_v3.rst index d91c5ea5e..df619305e 100644 --- a/doc/source/user/proxies/block_storage_v3.rst +++ b/doc/source/user/proxies/block_storage_v3.rst @@ -133,3 +133,11 @@ Helpers .. autoclass:: openstack.block_storage.v3._proxy.Proxy :noindex: :members: wait_for_status, wait_for_delete + +BlockStorageSummary Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v3._proxy.Proxy + :noindex: + :members: summary + diff --git a/doc/source/user/resources/block_storage/index.rst b/doc/source/user/resources/block_storage/index.rst index 7f9e16d21..564597d89 100644 --- a/doc/source/user/resources/block_storage/index.rst +++ b/doc/source/user/resources/block_storage/index.rst @@ -15,3 +15,4 @@ Block Storage Resources v3/snapshot v3/type v3/volume + v3/block_storage_summary diff --git a/doc/source/user/resources/block_storage/v3/block_storage_summary.rst b/doc/source/user/resources/block_storage/v3/block_storage_summary.rst new file mode 100644 index 000000000..2215db8f0 --- /dev/null +++ b/doc/source/user/resources/block_storage/v3/block_storage_summary.rst @@ -0,0 +1,14 @@ +openstack.block_storage.v3.block_storage_summary +================================================ + +.. automodule:: openstack.block_storage.v3.block_storage_summary + +The Block Storage Summary Class +------------------------------- + +The ``Block Storage Summary`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v3.block_storage_summary.BlockStorageSummary + :members: + diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 642e74049..34f03aea9 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -13,6 +13,7 @@ from openstack.block_storage import _base_proxy from openstack.block_storage.v3 import availability_zone from openstack.block_storage.v3 import backup as _backup +from openstack.block_storage.v3 import block_storage_summary as _summary from openstack.block_storage.v3 import capabilities as _capabilities from openstack.block_storage.v3 import extension as _extension from openstack.block_storage.v3 import group as _group @@ -44,6 +45,7 @@ class Proxy(_base_proxy.BaseBlockStorageProxy): "resource_filter": _resource_filter.ResourceFilter, "snapshot": _snapshot.Snapshot, "stats_pools": _stats.Pools, + "summary": _summary.BlockStorageSummary, "type": _type.Type, "volume": _volume.Volume, } @@ -662,6 +664,26 @@ def delete_volume_metadata(self, volume, keys=None): else: volume.delete_metadata(self) + def summary(self, all_projects): + """Get Volumes Summary + + This method returns the volumes summary in the deployment. + + :param all_projects: Whether to return the summary of all projects + or not. + + :returns: One :class: + `~openstack.block_storage.v3.block_storage_summary.Summary` + instance. + """ + res = self._get(_summary.BlockStorageSummary, requires_id=False) + return res.fetch( + self, + requires_id=False, + resource_response_key='volume-summary', + all_projects=all_projects, + ) + # ====== VOLUME ACTIONS ====== def extend_volume(self, volume, size): """Extend a volume diff --git a/openstack/block_storage/v3/block_storage_summary.py b/openstack/block_storage/v3/block_storage_summary.py new file mode 100644 index 000000000..410424fe4 --- /dev/null +++ b/openstack/block_storage/v3/block_storage_summary.py @@ -0,0 +1,30 @@ +# 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 resource + + +class BlockStorageSummary(resource.Resource): + base_path = "/volumes/summary" + + # capabilities + allow_fetch = True + + # Properties + #: Total size of all the volumes + total_size = resource.Body("total_size") + #: Total count of all the volumes + total_count = resource.Body("total_count") + #: Metadata of all the volumes + metadata = resource.Body("metadata") + + _max_microversion = "3.36" diff --git a/openstack/tests/functional/block_storage/v3/test_block_storage_summary.py b/openstack/tests/functional/block_storage/v3/test_block_storage_summary.py new file mode 100644 index 000000000..1efff92a3 --- /dev/null +++ b/openstack/tests/functional/block_storage/v3/test_block_storage_summary.py @@ -0,0 +1,21 @@ +# 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.tests.functional.block_storage.v3 import base + + +class TestBlockStorageSummary(base.BaseBlockStorageTest): + def test_get(self): + sot = self.conn.block_storage.summary(all_projects=True) + self.assertIn('total_size', sot) + self.assertIn('total_count', sot) + self.assertIn('metadata', sot) diff --git a/openstack/tests/unit/block_storage/v3/test_block_storage_summary.py b/openstack/tests/unit/block_storage/v3/test_block_storage_summary.py new file mode 100644 index 000000000..32b00689f --- /dev/null +++ b/openstack/tests/unit/block_storage/v3/test_block_storage_summary.py @@ -0,0 +1,69 @@ +# 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 + +from openstack.block_storage.v3 import block_storage_summary as summary +from openstack.tests.unit import base + + +BLOCK_STORAGE_SUMMARY_312 = { + "total_size": "4", + "total_count": "2", + "metadata": {"key1": "value1"}, +} + + +BLOCK_STORAGE_SUMMARY_326 = copy.deepcopy(BLOCK_STORAGE_SUMMARY_312) +BLOCK_STORAGE_SUMMARY_326['metadata'] = {"key1": "value1"} + + +class TestBlockStorageSummary(base.TestCase): + def test_basic(self): + summary_resource = summary.BlockStorageSummary() + self.assertEqual(None, summary_resource.resource_key) + self.assertEqual(None, summary_resource.resources_key) + self.assertEqual("/volumes/summary", summary_resource.base_path) + self.assertTrue(summary_resource.allow_fetch) + self.assertFalse(summary_resource.allow_create) + self.assertFalse(summary_resource.allow_commit) + self.assertFalse(summary_resource.allow_delete) + self.assertFalse(summary_resource.allow_list) + + def test_get_summary_312(self): + summary_resource = summary.BlockStorageSummary( + **BLOCK_STORAGE_SUMMARY_312 + ) + self.assertEqual( + BLOCK_STORAGE_SUMMARY_312["total_size"], + summary_resource.total_size, + ) + self.assertEqual( + BLOCK_STORAGE_SUMMARY_312["total_count"], + summary_resource.total_count, + ) + + def test_get_summary_326(self): + summary_resource = summary.BlockStorageSummary( + **BLOCK_STORAGE_SUMMARY_326 + ) + self.assertEqual( + BLOCK_STORAGE_SUMMARY_326["total_size"], + summary_resource.total_size, + ) + self.assertEqual( + BLOCK_STORAGE_SUMMARY_326["total_count"], + summary_resource.total_count, + ) + self.assertEqual( + BLOCK_STORAGE_SUMMARY_326["metadata"], summary_resource.metadata + ) diff --git a/releasenotes/notes/add-block-storage-summary-support-dd00d424c4e6a3b1.yaml b/releasenotes/notes/add-block-storage-summary-support-dd00d424c4e6a3b1.yaml new file mode 100644 index 000000000..4620d2e3b --- /dev/null +++ b/releasenotes/notes/add-block-storage-summary-support-dd00d424c4e6a3b1.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added support for block storage summary. + From a087bc0b4bacb288884312ac9c24a0804498f9d9 Mon Sep 17 00:00:00 2001 From: Mahnoor Asghar Date: Tue, 13 Jun 2023 12:22:11 -0400 Subject: [PATCH 3297/3836] Adds support for node hardware inventory Adds support for querying a node's hardware inventory as per functionality introduced in API 1.81. Change-Id: I218f3458d701ee9d0ff163884ee6f09992e84363 --- openstack/baremetal/v1/_proxy.py | 13 ++++++ openstack/baremetal/v1/node.py | 23 +++++++++- .../tests/unit/baremetal/v1/test_node.py | 43 +++++++++++++++++++ .../add-node-inventory-52f54e16777814e7.yaml | 5 +++ 4 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/add-node-inventory-52f54e16777814e7.yaml diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 90fd14e5d..be72c3d1e 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -332,6 +332,19 @@ def get_node(self, node, fields=None): """ return self._get_with_fields(_node.Node, node, fields=fields) + def get_node_inventory(self, node): + """Get a specific node's hardware inventory. + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + + :returns: The node inventory + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + inventory could be found. + """ + res = self._get_resource(_node.Node, node) + return res.get_node_inventory(self, node) + def update_node(self, node, retry_on_conflict=True, **attrs): """Update a node. diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 0ec30198f..18737d8bb 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -96,8 +96,8 @@ class Node(_common.ListMixin, resource.Resource): is_maintenance='maintenance', ) - # Ability to change boot_mode and secure_boot, introduced in 1.76 (Xena). - _max_microversion = '1.76' + # Ability to get node inventory, introduced in 1.81 (Antelope). + _max_microversion = '1.81' # Properties #: The UUID of the allocation associated with this node. Added in API @@ -1312,6 +1312,25 @@ def set_console_mode(self, session, enabled): ) exceptions.raise_from_response(response, error_message=msg) + def get_node_inventory(self, session, node_id): + session = self._get_session(session) + version = self._get_microversion(session, action='fetch') + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'inventory') + + response = session.get( + request.url, + headers=request.headers, + microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) + + msg = "Failed to get inventory for node {node}".format( + node=node_id, + ) + exceptions.raise_from_response(response, error_message=msg) + return response.json() + def patch( self, session, diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index a3d0ecafd..672d4fa7e 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -1165,3 +1165,46 @@ def test_set_console_mode_invalid_enabled(self): self.session, 'true', # not a bool ) + + +@mock.patch.object(node.Node, 'fetch', lambda self, session: self) +@mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) +class TestNodeInventory(base.TestCase): + def setUp(self): + super().setUp() + self.node = node.Node(**FAKE) + self.session = mock.Mock( + spec=adapter.Adapter, + default_microversion='1.81', + ) + + def test_get_inventory(self): + node_inventory = { + 'inventory': { + 'memory': {'physical_mb': 3072}, + 'cpu': { + 'count': 1, + 'model_name': 'qemu64', + 'architecture': 'x86_64', + }, + 'disks': [{'name': 'testvm1.qcow2', 'size': 11811160064}], + 'interfaces': [{'mac_address': '52:54:00:c7:02:45'}], + 'system_vendor': { + 'product_name': 'testvm1', + 'manufacturer': 'Sushy Emulator', + }, + 'boot': {'current_boot_mode': 'uefi'}, + }, + 'plugin_data': {'fake_plugin_data'}, + } + self.session.get.return_value.json.return_value = node_inventory + + res = self.node.get_node_inventory(self.session, self.node.id) + self.assertEqual(node_inventory, res) + + self.session.get.assert_called_once_with( + 'nodes/%s/inventory' % self.node.id, + headers=mock.ANY, + microversion='1.81', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) diff --git a/releasenotes/notes/add-node-inventory-52f54e16777814e7.yaml b/releasenotes/notes/add-node-inventory-52f54e16777814e7.yaml new file mode 100644 index 000000000..5e4b84128 --- /dev/null +++ b/releasenotes/notes/add-node-inventory-52f54e16777814e7.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Adds support for querying a node's hardware inventory as per functionality + introduced in API 1.81. \ No newline at end of file From 8995d38bfc947a3605009c47653e7babd12ccdb8 Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Wed, 14 Jun 2023 11:02:57 -0500 Subject: [PATCH 3298/3836] update rackspace profile to specify identity version Rackspace currently only support identity API v2.0 Change-Id: I1e0aecf1abf80ae2e23674a3894b7e3e55f9f29b --- openstack/config/vendors/rackspace.json | 1 + 1 file changed, 1 insertion(+) diff --git a/openstack/config/vendors/rackspace.json b/openstack/config/vendors/rackspace.json index 53db96284..1884980e2 100644 --- a/openstack/config/vendors/rackspace.json +++ b/openstack/config/vendors/rackspace.json @@ -4,6 +4,7 @@ "auth": { "auth_url": "https://identity.api.rackspacecloud.com/v2.0/" }, + "identity_api_version": "2.0", "regions": [ "DFW", "HKG", From d2a166a98eaaec40cdd994dfe3fddeefa6c34532 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 9 May 2023 13:25:45 +0200 Subject: [PATCH 3299/3836] Add fake resources generator In a variety of places we need to have an easy way to have resources populated with fake data. It costs quite a lot of lines of code and can be easily automated. With this change it can be as easy as `_fake = sdk_fakes.generate_fake_resource(Project)`. The code is implemented not to require estabilshed connection to ease use in tests. Change-Id: I47312f4036a0b389cd3689466ab220ba558aa39a --- openstack/test/__init__.py | 0 openstack/test/fakes.py | 122 ++++++++++++++++++ openstack/tests/unit/test_fakes.py | 73 +++++++++++ .../add-fakes-generator-72c53d34c995fcb2.yaml | 5 + 4 files changed, 200 insertions(+) create mode 100644 openstack/test/__init__.py create mode 100644 openstack/test/fakes.py create mode 100644 openstack/tests/unit/test_fakes.py create mode 100644 releasenotes/notes/add-fakes-generator-72c53d34c995fcb2.yaml diff --git a/openstack/test/__init__.py b/openstack/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/test/fakes.py b/openstack/test/fakes.py new file mode 100644 index 000000000..a4f7b809a --- /dev/null +++ b/openstack/test/fakes.py @@ -0,0 +1,122 @@ +# 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 inspect +from random import choice +from random import randint +from random import random +import uuid + +from openstack import format as _format +from openstack import resource + + +def generate_fake_resource(resource_type, **attrs): + """Generate fake resource + + :param type resource_type: Object class + :param dict attrs: Optional attributes to be set on resource + + :return: Instance of `resource_type` class populated with fake + values of expected types. + """ + base_attrs = dict() + for name, value in inspect.getmembers( + resource_type, + predicate=lambda x: isinstance(x, (resource.Body, resource.URI)), + ): + if isinstance(value, resource.Body): + target_type = value.type + if target_type is None: + if ( + name == "properties" + and hasattr( + resource_type, "_store_unknown_attrs_as_properties" + ) + and resource_type._store_unknown_attrs_as_properties + ): + # virtual "properties" attr which hosts all unknown attrs + # (i.e. Image) + base_attrs[name] = dict() + else: + # Type not defined - string + base_attrs[name] = uuid.uuid4().hex + elif issubclass(target_type, resource.Resource): + # Attribute is of another Resource type + base_attrs[name] = generate_fake_resource(target_type) + elif issubclass(target_type, list) and value.list_type is not None: + # List of ... + item_type = value.list_type + if issubclass(item_type, resource.Resource): + # item is of Resource type + base_attrs[name] = generate_fake_resource(item_type) + elif issubclass(item_type, dict): + base_attrs[name] = dict() + elif issubclass(item_type, str): + base_attrs[name] = [uuid.uuid4().hex] + else: + # Everything else + msg = "Fake value for %s.%s can not be generated" % ( + resource_type.__name__, + name, + ) + raise NotImplementedError(msg) + elif issubclass(target_type, list) and value.list_type is None: + # List of str + base_attrs[name] = [uuid.uuid4().hex] + elif issubclass(target_type, str): + # definitely string + base_attrs[name] = uuid.uuid4().hex + elif issubclass(target_type, int): + # int + base_attrs[name] = randint(1, 100) + elif issubclass(target_type, float): + # float + base_attrs[name] = random() + elif issubclass(target_type, bool) or issubclass( + target_type, _format.BoolStr + ): + # bool + base_attrs[name] = choice([True, False]) + elif issubclass(target_type, dict): + # some dict - without further details leave it empty + base_attrs[name] = dict() + else: + # Everything else + msg = "Fake value for %s.%s can not be generated" % ( + resource_type.__name__, + name, + ) + raise NotImplementedError(msg) + if isinstance(value, resource.URI): + # For URI we just generate something + base_attrs[name] = uuid.uuid4().hex + + base_attrs.update(**attrs) + fake = resource_type(**base_attrs) + return fake + + +def generate_fake_resources(resource_type, count=1, attrs=None): + """Generate given number of fake resource entities + + :param type resource_type: Object class + :param int count: Number of objects to return + :param dict attrs: Attribute values to set into each instance + + :return: Array of `resource_type` class instances populated with fake + values of expected types. + """ + if not attrs: + attrs = {} + for _ in range(count): + yield generate_fake_resource(resource_type, **attrs) diff --git a/openstack/tests/unit/test_fakes.py b/openstack/tests/unit/test_fakes.py new file mode 100644 index 000000000..e0e2fa803 --- /dev/null +++ b/openstack/tests/unit/test_fakes.py @@ -0,0 +1,73 @@ +# 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 format as _format +from openstack import resource +from openstack.test import fakes +from openstack.tests.unit import base + + +class TestGetFake(base.TestCase): + def test_generate_fake_resource_one(self): + res = fakes.generate_fake_resource(resource.Resource) + self.assertIsInstance(res, resource.Resource) + + def test_generate_fake_resource_list(self): + res = list(fakes.generate_fake_resources(resource.Resource, 2)) + self.assertEqual(2, len(res)) + self.assertIsInstance(res[0], resource.Resource) + + def test_generate_fake_resource_types(self): + class Fake(resource.Resource): + a = resource.Body("a", type=str) + b = resource.Body("b", type=int) + c = resource.Body("c", type=bool) + d = resource.Body("d", type=_format.BoolStr) + e = resource.Body("e", type=dict) + f = resource.URI("path") + + res = fakes.generate_fake_resource(Fake) + self.assertIsInstance(res.a, str) + self.assertIsInstance(res.b, int) + self.assertIsInstance(res.c, bool) + self.assertIsInstance(res.d, bool) + self.assertIsInstance(res.e, dict) + self.assertIsInstance(res.f, str) + + def test_generate_fake_resource_attrs(self): + class Fake(resource.Resource): + a = resource.Body("a", type=str) + b = resource.Body("b", type=str) + + res = fakes.generate_fake_resource(Fake, b="bar") + self.assertIsInstance(res.a, str) + self.assertIsInstance(res.b, str) + self.assertEqual("bar", res.b) + + def test_generate_fake_resource_types_inherit(self): + class Fake(resource.Resource): + a = resource.Body("a", type=str) + + class FakeInherit(resource.Resource): + a = resource.Body("a", type=Fake) + + res = fakes.generate_fake_resource(FakeInherit) + self.assertIsInstance(res.a, Fake) + self.assertIsInstance(res.a.a, str) + + def test_unknown_attrs_as_props(self): + class Fake(resource.Resource): + properties = resource.Body("properties") + _store_unknown_attrs_as_properties = True + + res = fakes.generate_fake_resource(Fake) + self.assertIsInstance(res.properties, dict) diff --git a/releasenotes/notes/add-fakes-generator-72c53d34c995fcb2.yaml b/releasenotes/notes/add-fakes-generator-72c53d34c995fcb2.yaml new file mode 100644 index 000000000..06fa9c039 --- /dev/null +++ b/releasenotes/notes/add-fakes-generator-72c53d34c995fcb2.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add fake resource generator to ease unit testing in packages that depend on + openstacksdk. From 068cc997d570b1bcc28075517639c4c76bf5787a Mon Sep 17 00:00:00 2001 From: Jan Hartkopf Date: Mon, 27 Mar 2023 15:46:36 +0200 Subject: [PATCH 3300/3836] Allow resources to be skipped on project cleanup Story: 2010370 Task: 46596 Change-Id: Id6d68e40656c92910f491fe3b66e40c69b44e352 Signed-off-by: Jan Hartkopf --- openstack/block_storage/v3/_proxy.py | 154 +++++------ openstack/cloud/openstackcloud.py | 4 + openstack/compute/v2/_proxy.py | 4 + openstack/dns/v2/_proxy.py | 52 ++-- openstack/network/v2/_proxy.py | 250 ++++++++++-------- openstack/object_store/v1/_proxy.py | 6 + openstack/orchestration/v1/_proxy.py | 4 + openstack/proxy.py | 27 +- openstack/tests/unit/test_proxy.py | 10 + ...eanup-exclude-option-65cba962eaa5b61a.yaml | 6 + 10 files changed, 301 insertions(+), 216 deletions(-) create mode 100644 releasenotes/notes/project-cleanup-exclude-option-65cba962eaa5b61a.yaml diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 34f03aea9..bb24bd6bc 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -1675,6 +1675,7 @@ def _service_cleanup( identified_resources=None, filters=None, resource_evaluation_fn=None, + skip_resources=None, ): # It is not possible to delete backup if there are dependent backups. # In order to be able to do cleanup those is required to have multiple @@ -1684,11 +1685,85 @@ def _service_cleanup( # required to limit amount of iterations we do (currently pick 10). In # dry_run all those iterations are doing not what we want, therefore # only iterate in a real cleanup mode. - if dry_run: - # Just iterate and evaluate backups in dry_run mode - for obj in self.backups(details=False): + if not self.should_skip_resource_cleanup("backup", skip_resources): + if dry_run: + # Just iterate and evaluate backups in dry_run mode + for obj in self.backups(details=False): + need_delete = self._service_cleanup_del_res( + self.delete_backup, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn, + ) + else: + # Set initial iterations conditions + need_backup_iteration = True + max_iterations = 10 + while need_backup_iteration and max_iterations > 0: + # Reset iteration controls + need_backup_iteration = False + max_iterations -= 1 + backups = [] + # To increase success chance sort backups by age, dependent + # backups are logically younger. + for obj in self.backups( + details=True, sort_key='created_at', sort_dir='desc' + ): + if not obj.has_dependent_backups: + # If no dependent backups - go with it + need_delete = self._service_cleanup_del_res( + self.delete_backup, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn, + ) + if not dry_run and need_delete: + backups.append(obj) + else: + # Otherwise we need another iteration + need_backup_iteration = True + + # Before proceeding need to wait for backups to be deleted + for obj in backups: + try: + self.wait_for_delete(obj) + except exceptions.SDKException: + # Well, did our best, still try further + pass + + if not self.should_skip_resource_cleanup("snapshot", skip_resources): + snapshots = [] + for obj in self.snapshots(details=False): need_delete = self._service_cleanup_del_res( - self.delete_backup, + self.delete_snapshot, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn, + ) + if not dry_run and need_delete: + snapshots.append(obj) + + # Before deleting volumes need to wait for snapshots to be deleted + for obj in snapshots: + try: + self.wait_for_delete(obj) + except exceptions.SDKException: + # Well, did our best, still try further + pass + + if not self.should_skip_resource_cleanup("volume", skip_resources): + for obj in self.volumes(details=True): + self._service_cleanup_del_res( + self.delete_volume, obj, dry_run=dry_run, client_status_queue=client_status_queue, @@ -1696,74 +1771,3 @@ def _service_cleanup( filters=filters, resource_evaluation_fn=resource_evaluation_fn, ) - else: - # Set initial iterations conditions - need_backup_iteration = True - max_iterations = 10 - while need_backup_iteration and max_iterations > 0: - # Reset iteration controls - need_backup_iteration = False - max_iterations -= 1 - backups = [] - # To increase success chance sort backups by age, dependent - # backups are logically younger. - for obj in self.backups( - details=True, sort_key='created_at', sort_dir='desc' - ): - if not obj.has_dependent_backups: - # If no dependent backups - go with it - need_delete = self._service_cleanup_del_res( - self.delete_backup, - obj, - dry_run=dry_run, - client_status_queue=client_status_queue, - identified_resources=identified_resources, - filters=filters, - resource_evaluation_fn=resource_evaluation_fn, - ) - if not dry_run and need_delete: - backups.append(obj) - else: - # Otherwise we need another iteration - need_backup_iteration = True - - # Before proceeding need to wait for backups to be deleted - for obj in backups: - try: - self.wait_for_delete(obj) - except exceptions.SDKException: - # Well, did our best, still try further - pass - - snapshots = [] - for obj in self.snapshots(details=False): - need_delete = self._service_cleanup_del_res( - self.delete_snapshot, - obj, - dry_run=dry_run, - client_status_queue=client_status_queue, - identified_resources=identified_resources, - filters=filters, - resource_evaluation_fn=resource_evaluation_fn, - ) - if not dry_run and need_delete: - snapshots.append(obj) - - # Before deleting volumes need to wait for snapshots to be deleted - for obj in snapshots: - try: - self.wait_for_delete(obj) - except exceptions.SDKException: - # Well, did our best, still try further - pass - - for obj in self.volumes(details=True): - self._service_cleanup_del_res( - self.delete_volume, - obj, - dry_run=dry_run, - client_status_queue=client_status_queue, - identified_resources=identified_resources, - filters=filters, - resource_evaluation_fn=resource_evaluation_fn, - ) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index b0fc5eca3..28a2f5777 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -813,6 +813,7 @@ def project_cleanup( status_queue=None, filters=None, resource_evaluation_fn=None, + skip_resources=None, ): """Cleanup the project resources. @@ -829,6 +830,8 @@ def project_cleanup( :param resource_evaluation_fn: A callback function, which will be invoked for each resurce and must return True/False depending on whether resource need to be deleted or not. + :param skip_resources: List of specific resources whose cleanup should + be skipped. """ dependencies = {} get_dep_fn_name = '_get_cleanup_dependencies' @@ -879,6 +882,7 @@ def project_cleanup( identified_resources=cleanup_resources, filters=filters, resource_evaluation_fn=resource_evaluation_fn, + skip_resources=skip_resources, ) except exceptions.ServiceDisabledException: # same reason as above diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 574a38b98..e9917784b 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -2548,7 +2548,11 @@ def _service_cleanup( identified_resources=None, filters=None, resource_evaluation_fn=None, + skip_resources=None, ): + if self.should_skip_resource_cleanup("server", skip_resources): + return + servers = [] for obj in self.servers(): need_delete = self._service_cleanup_del_res( diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py index 174093e99..de2ee5742 100644 --- a/openstack/dns/v2/_proxy.py +++ b/openstack/dns/v2/_proxy.py @@ -667,27 +667,33 @@ def _service_cleanup( identified_resources=None, filters=None, resource_evaluation_fn=None, + skip_resources=None, ): - # Delete all zones - for obj in self.zones(): - self._service_cleanup_del_res( - self.delete_zone, - obj, - dry_run=dry_run, - client_status_queue=client_status_queue, - identified_resources=identified_resources, - filters=filters, - resource_evaluation_fn=resource_evaluation_fn, - ) - # Unset all floatingIPs - # NOTE: FloatingIPs are not cleaned when filters are set - for obj in self.floating_ips(): - self._service_cleanup_del_res( - self.unset_floating_ip, - obj, - dry_run=dry_run, - client_status_queue=client_status_queue, - identified_resources=identified_resources, - filters=filters, - resource_evaluation_fn=resource_evaluation_fn, - ) + if not self.should_skip_resource_cleanup("zone", skip_resources): + # Delete all zones + for obj in self.zones(): + self._service_cleanup_del_res( + self.delete_zone, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn, + ) + + if not self.should_skip_resource_cleanup( + "floating_ip", skip_resources + ): + # Unset all floatingIPs + # NOTE: FloatingIPs are not cleaned when filters are set + for obj in self.floating_ips(): + self._service_cleanup_del_res( + self.unset_floating_ip, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn, + ) diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index d891cf7d8..42e98ad4f 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -6212,106 +6212,137 @@ def _service_cleanup( identified_resources=None, filters=None, resource_evaluation_fn=None, + skip_resources=None, ): project_id = self.get_project_id() - # Delete floating_ips in the project if no filters defined OR all - # filters are matching and port_id is empty - for obj in self.ips(project_id=project_id): - self._service_cleanup_del_res( - self.delete_ip, - obj, - dry_run=dry_run, - client_status_queue=client_status_queue, - identified_resources=identified_resources, - filters=filters, - resource_evaluation_fn=fip_cleanup_evaluation, - ) - # Delete (try to delete) all security groups in the project - # Let's hope we can't drop SG in use - for obj in self.security_groups(project_id=project_id): - if obj.name != 'default': + if not self.should_skip_resource_cleanup( + "floating_ip", skip_resources + ): + # Delete floating_ips in the project if no filters defined OR all + # filters are matching and port_id is empty + for obj in self.ips(project_id=project_id): self._service_cleanup_del_res( - self.delete_security_group, + self.delete_ip, obj, dry_run=dry_run, client_status_queue=client_status_queue, identified_resources=identified_resources, filters=filters, - resource_evaluation_fn=resource_evaluation_fn, + resource_evaluation_fn=fip_cleanup_evaluation, ) - # Networks are crazy, try to delete router+net+subnet - # if there are no "other" ports allocated on the net - for net in self.networks(project_id=project_id): - network_has_ports_allocated = False - router_if = list() - for port in self.ports(project_id=project_id, network_id=net.id): - self.log.debug('Looking at port %s' % port) - if port.device_owner in [ - 'network:router_interface', - 'network:router_interface_distributed', - 'network:ha_router_replicated_interface', - ]: - router_if.append(port) - elif port.device_owner == 'network:dhcp': - # we don't treat DHCP as a real port + if not self.should_skip_resource_cleanup( + "security_group", skip_resources + ): + # Delete (try to delete) all security groups in the project + # Let's hope we can't drop SG in use + for obj in self.security_groups(project_id=project_id): + if obj.name != 'default': + self._service_cleanup_del_res( + self.delete_security_group, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn, + ) + + if ( + not self.should_skip_resource_cleanup("network", skip_resources) + and not self.should_skip_resource_cleanup("port", skip_resources) + and not self.should_skip_resource_cleanup("subnet", skip_resources) + ): + # Networks are crazy, try to delete router+net+subnet + # if there are no "other" ports allocated on the net + for net in self.networks(project_id=project_id): + network_has_ports_allocated = False + router_if = list() + for port in self.ports( + project_id=project_id, network_id=net.id + ): + self.log.debug('Looking at port %s' % port) + if port.device_owner in [ + 'network:router_interface', + 'network:router_interface_distributed', + 'network:ha_router_replicated_interface', + ]: + router_if.append(port) + elif port.device_owner == 'network:dhcp': + # we don't treat DHCP as a real port + continue + elif port.device_owner is None or port.device_owner == '': + # Nobody owns the port - go with it + continue + elif ( + identified_resources + and port.device_id not in identified_resources + ): + # It seems some no other service identified this resource + # to be deleted. We can assume it doesn't count + network_has_ports_allocated = True + if network_has_ports_allocated: + # If some ports are on net - we cannot delete it continue - elif port.device_owner is None or port.device_owner == '': - # Nobody owns the port - go with it + self.log.debug('Network %s should be deleted' % net) + # __Check__ if we need to drop network according to filters + network_must_be_deleted = self._service_cleanup_del_res( + self.delete_network, + net, + dry_run=True, + client_status_queue=None, + identified_resources=None, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn, + ) + if not network_must_be_deleted: + # If not - check another net continue - elif ( - identified_resources - and port.device_id not in identified_resources + # otherwise disconnect router, drop net, subnet, router + # Disconnect + for port in router_if: + if client_status_queue: + client_status_queue.put(port) + if not dry_run: + try: + self.remove_interface_from_router( + router=port.device_id, port_id=port.id + ) + except exceptions.SDKException: + self.log.error('Cannot delete object %s' % obj) + # router disconnected, drop it + self._service_cleanup_del_res( + self.delete_router, + self.get_router(port.device_id), + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=None, + resource_evaluation_fn=None, + ) + # Drop ports not belonging to anybody + for port in self.ports( + project_id=project_id, network_id=net.id ): - # It seems some no other service identified this resource - # to be deleted. We can assume it doesn't count - network_has_ports_allocated = True - if network_has_ports_allocated: - # If some ports are on net - we cannot delete it - continue - self.log.debug('Network %s should be deleted' % net) - # __Check__ if we need to drop network according to filters - network_must_be_deleted = self._service_cleanup_del_res( - self.delete_network, - net, - dry_run=True, - client_status_queue=None, - identified_resources=None, - filters=filters, - resource_evaluation_fn=resource_evaluation_fn, - ) - if not network_must_be_deleted: - # If not - check another net - continue - # otherwise disconnect router, drop net, subnet, router - # Disconnect - for port in router_if: - if client_status_queue: - client_status_queue.put(port) - if not dry_run: - try: - self.remove_interface_from_router( - router=port.device_id, port_id=port.id + if port.device_owner is None or port.device_owner == '': + self._service_cleanup_del_res( + self.delete_port, + port, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=None, + resource_evaluation_fn=None, ) - except exceptions.SDKException: - self.log.error('Cannot delete object %s' % obj) - # router disconnected, drop it - self._service_cleanup_del_res( - self.delete_router, - self.get_router(port.device_id), - dry_run=dry_run, - client_status_queue=client_status_queue, - identified_resources=identified_resources, - filters=None, - resource_evaluation_fn=None, - ) - # Drop ports not belonging to anybody - for port in self.ports(project_id=project_id, network_id=net.id): - if port.device_owner is None or port.device_owner == '': + + # Drop all subnets in the net (no further conditions) + for obj in self.subnets( + project_id=project_id, network_id=net.id + ): self._service_cleanup_del_res( - self.delete_port, - port, + self.delete_subnet, + obj, dry_run=dry_run, client_status_queue=client_status_queue, identified_resources=identified_resources, @@ -6319,43 +6350,38 @@ def _service_cleanup( resource_evaluation_fn=None, ) - # Drop all subnets in the net (no further conditions) - for obj in self.subnets(project_id=project_id, network_id=net.id): + # And now the network itself (we are here definitely only if we + # need that) self._service_cleanup_del_res( - self.delete_subnet, - obj, + self.delete_network, + net, dry_run=dry_run, client_status_queue=client_status_queue, identified_resources=identified_resources, filters=None, resource_evaluation_fn=None, ) - - # And now the network itself (we are here definitely only if we - # need that) - self._service_cleanup_del_res( - self.delete_network, - net, - dry_run=dry_run, - client_status_queue=client_status_queue, - identified_resources=identified_resources, - filters=None, - resource_evaluation_fn=None, + else: + self.log.debug( + "Skipping cleanup of networks, ports and subnets " + "as those resources require none of them to be " + "excluded, but at least one should be kept" ) - # It might happen, that we have routers not attached to anything - for obj in self.routers(): - ports = list(self.ports(device_id=obj.id)) - if len(ports) == 0: - self._service_cleanup_del_res( - self.delete_router, - obj, - dry_run=dry_run, - client_status_queue=client_status_queue, - identified_resources=identified_resources, - filters=None, - resource_evaluation_fn=None, - ) + if not self.should_skip_resource_cleanup("router", skip_resources): + # It might happen, that we have routers not attached to anything + for obj in self.routers(): + ports = list(self.ports(device_id=obj.id)) + if len(ports) == 0: + self._service_cleanup_del_res( + self.delete_router, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=None, + resource_evaluation_fn=None, + ) def fip_cleanup_evaluation(obj, identified_resources=None, filters=None): diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index ea873728f..fae81678d 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -1130,7 +1130,13 @@ def _service_cleanup( identified_resources=None, filters=None, resource_evaluation_fn=None, + skip_resources=None, ): + if self.should_skip_resource_cleanup( + "container", skip_resources + ) or self.should_skip_resource_cleanup("object", skip_resources): + return + is_bulk_delete_supported = False bulk_delete_max_per_request = None try: diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index 2ac55c97f..27200f2e5 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -549,7 +549,11 @@ def _service_cleanup( identified_resources=None, filters=None, resource_evaluation_fn=None, + skip_resources=None, ): + if self.should_skip_resource_cleanup("stack", skip_resources): + return + stacks = [] for obj in self.stacks(): need_delete = self._service_cleanup_del_res( diff --git a/openstack/proxy.py b/openstack/proxy.py index 3a87fbcb1..ce4010721 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -100,7 +100,7 @@ def __init__( influxdb_config=None, influxdb_client=None, *args, - **kwargs + **kwargs, ): # NOTE(dtantsur): keystoneauth defaults retriable_status_codes to None, # override it with a class-level value. @@ -144,7 +144,7 @@ def request( connect_retries=1, global_request_id=None, *args, - **kwargs + **kwargs, ): conn = self._get_connection() if not global_request_id: @@ -180,7 +180,7 @@ def request( connect_retries=connect_retries, raise_exc=raise_exc, global_request_id=global_request_id, - **kwargs + **kwargs, ), ), expiration_time=expiration_time, @@ -196,7 +196,7 @@ def request( connect_retries=connect_retries, raise_exc=raise_exc, global_request_id=global_request_id, - **kwargs + **kwargs, ) for h in response.history: @@ -623,7 +623,7 @@ def _get( requires_id=True, base_path=None, skip_cache=False, - **attrs + **attrs, ): """Fetch a resource @@ -665,7 +665,7 @@ def _list( paginated=True, base_path=None, jmespath_filters=None, - **attrs + **attrs, ) -> Generator[T, None, None]: """List a resource @@ -736,6 +736,7 @@ def _service_cleanup( identified_resources=None, filters=None, resource_evaluation_fn=None, + skip_resources=None, ): return None @@ -814,6 +815,20 @@ def _service_cleanup_resource_filters_evaluation(self, obj, filters=None): else: return False + def should_skip_resource_cleanup(self, resource=None, skip_resources=None): + if resource is None or skip_resources is None: + return False + + resource_name = f"{self.service_type.replace('-', '_')}.{resource}" + + if resource_name in skip_resources: + self.log.debug( + f"Skipping resource {resource_name} " "in project cleanup" + ) + return True + + return False + # TODO(stephenfin): Remove this and all users. Use of this generally indicates # a missing Resource type. diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index c6526a6c6..6c338f5f3 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -728,6 +728,7 @@ def setUp(self): self.res_no_updated.created_at = '2020-01-02T03:04:05' self.sot = proxy.Proxy(self.session) + self.sot.service_type = "block-storage" self.delete_mock = mock.Mock() @@ -867,3 +868,12 @@ def test_service_cleanup_queue(self): ) ) self.assertEqual(self.res, q.get_nowait()) + + def test_should_skip_resource_cleanup(self): + excluded = ["block_storage.backup"] + self.assertTrue( + self.sot.should_skip_resource_cleanup("backup", excluded) + ) + self.assertFalse( + self.sot.should_skip_resource_cleanup("volume", excluded) + ) diff --git a/releasenotes/notes/project-cleanup-exclude-option-65cba962eaa5b61a.yaml b/releasenotes/notes/project-cleanup-exclude-option-65cba962eaa5b61a.yaml new file mode 100644 index 000000000..17516d552 --- /dev/null +++ b/releasenotes/notes/project-cleanup-exclude-option-65cba962eaa5b61a.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Project cleanup now supports skipping specific resources, + which will be kept as-is. Resource names are based on the + resource registry names, e. g. "block_storage.volume". From 2b4aeff6d3972761da9c4a6a8b696103664a758b Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Sun, 18 Jun 2023 16:48:34 +0200 Subject: [PATCH 3301/3836] fix connection.Connection finalizer finalizers execution order is not guaranteed in python3. This leads to absence of builtins like "open" in the finalizers. Depending on the keyring backend used inside of the set_auth_cache access to the file system may be required. Replace use of destructor to another builtin library "atexit" to implement a more reliable exit handler. Change-Id: I2d6882d64b57b65dbef086a08a3b3dc6b5faa2be --- openstack/config/cloud_region.py | 6 +++++- openstack/connection.py | 8 ++++---- openstack/tests/unit/config/test_config.py | 18 ++++++++++++++++++ 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 421e2438c..ddd91bc8e 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -658,7 +658,11 @@ def set_auth_cache(self): state = self._auth.get_auth_state() try: - keyring.set_password('openstacksdk', cache_id, state) + if state: + # NOTE: under some conditions the method may be invoked when auth + # is empty. This may lead to exception in the keyring lib, thus do + # nothing. + keyring.set_password('openstacksdk', cache_id, state) except RuntimeError: # the fail backend raises this self.log.debug('Failed to set auth into keyring') diff --git a/openstack/connection.py b/openstack/connection.py index 819e16bd3..5722a01e2 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -176,6 +176,7 @@ Additional information about the services can be found in the :ref:`service-proxies` documentation. """ +import atexit import concurrent.futures import warnings import weakref @@ -472,9 +473,8 @@ def __init__( 'additional_metric_tags' ] = self.config.config['additional_metric_tags'] - def __del__(self): - # try to force release of resources and save authorization - self.close() + # Register cleanup steps + atexit.register(self.close) @property def session(self): @@ -551,9 +551,9 @@ def _pool_executor(self): def close(self): """Release any resources held open.""" + self.config.set_auth_cache() if self.__pool_executor: self.__pool_executor.shutdown() - self.config.set_auth_cache() def set_global_request_id(self, global_request_id): self._global_request_id = global_request_id diff --git a/openstack/tests/unit/config/test_config.py b/openstack/tests/unit/config/test_config.py index a9a7693b9..da5658940 100644 --- a/openstack/tests/unit/config/test_config.py +++ b/openstack/tests/unit/config/test_config.py @@ -608,6 +608,21 @@ def test_load_auth_cache_found(self, ks_mock, kr_mock): ) ks_mock.assert_called_with(fake_auth) + @mock.patch('openstack.config.cloud_region.keyring') + def test_set_auth_cache_empty_auth(self, kr_mock): + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], secure_files=[] + ) + c._cache_auth = True + + kr_mock.get_password = mock.Mock(side_effect=[RuntimeError]) + kr_mock.set_password = mock.Mock() + + region = c.get_one('_test-cloud_') + + region.set_auth_cache() + kr_mock.set_password.assert_not_called() + @mock.patch('openstack.config.cloud_region.keyring') def test_set_auth_cache(self, kr_mock): c = config.OpenStackConfig( @@ -619,6 +634,9 @@ def test_set_auth_cache(self, kr_mock): kr_mock.set_password = mock.Mock() region = c.get_one('_test-cloud_') + region._auth.set_auth_state( + '{"auth_token":"foo", "body":{"token":"bar"}}' + ) region.set_auth_cache() kr_mock.set_password.assert_called_with( From fd2c41c69715ba39588c18224aef4e8734ed3ce1 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 7 Jun 2023 16:35:31 +0200 Subject: [PATCH 3302/3836] fix flavor.swap attribute type swap attribute of flavor is expected to be int. It is already checked as int in compute unittests, but not once returned as a fake. Change-Id: I4da810cd9828374cebb2a120cce6a8d55f182ea9 --- openstack/compute/v2/flavor.py | 2 +- openstack/resource.py | 9 ++++++++- openstack/tests/fakes.py | 2 +- openstack/tests/unit/cloud/test_caching.py | 6 ++++-- openstack/tests/unit/compute/v2/test_flavor.py | 6 ++++++ 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/openstack/compute/v2/flavor.py b/openstack/compute/v2/flavor.py index 10975e3f9..e9671df4f 100644 --- a/openstack/compute/v2/flavor.py +++ b/openstack/compute/v2/flavor.py @@ -57,7 +57,7 @@ class Flavor(resource.Resource): #: The number of virtual CPUs this flavor offers. *Type: int* vcpus = resource.Body('vcpus', type=int, default=0) #: Size of the swap partitions. - swap = resource.Body('swap', default=0) + swap = resource.Body('swap', type=int, default=0) #: Size of the ephemeral data disk attached to this server. *Type: int* ephemeral = resource.Body('OS-FLV-EXT-DATA:ephemeral', type=int, default=0) #: ``True`` if this flavor is disabled, ``False`` if not. *Type: bool* diff --git a/openstack/resource.py b/openstack/resource.py index 3443f584e..deff1f009 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -83,7 +83,14 @@ def _convert_type(value, data_type, list_type=None): # and the AbsoluteLimits type for an example. if isinstance(value, dict): return data_type(**value) - return data_type(value) + try: + return data_type(value) + except ValueError: + # If we can not convert data to the expected type return empty + # instance of the expected type. + # This is necessary to handle issues like with flavor.swap where + # empty string means "0". + return data_type() class _BaseComponent: diff --git a/openstack/tests/fakes.py b/openstack/tests/fakes.py index a4a7ece8e..9a208462b 100644 --- a/openstack/tests/fakes.py +++ b/openstack/tests/fakes.py @@ -70,7 +70,7 @@ def make_fake_flavor(flavor_id, name, ram=100, disk=1600, vcpus=24): u'os-flavor-access:is_public': True, u'ram': ram, u'rxtx_factor': 1.0, - u'swap': u'', + u'swap': 0, u'vcpus': vcpus, } diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py index d137d15ab..92ae2b15d 100644 --- a/openstack/tests/unit/cloud/test_caching.py +++ b/openstack/tests/unit/cloud/test_caching.py @@ -26,6 +26,7 @@ from openstack.identity.v3 import user as _user from openstack.image.v2 import image as _image from openstack.network.v2 import port as _port +from openstack.test import fakes as _fakes from openstack.tests import fakes from openstack.tests.unit import base from openstack.tests.unit.cloud import test_port @@ -539,6 +540,7 @@ def test_list_flavors(self): mock_uri = '{endpoint}/flavors/detail?is_public=None'.format( endpoint=fakes.COMPUTE_ENDPOINT ) + flavors = list(_fakes.generate_fake_resources(_flavor.Flavor, count=2)) uris_to_mock = [ dict( @@ -555,7 +557,7 @@ def test_list_flavors(self): validate=dict( headers={'OpenStack-API-Version': 'compute 2.53'} ), - json={'flavors': fakes.FAKE_FLAVOR_LIST}, + json={'flavors': flavors}, ), ] self.use_compute_discovery() @@ -568,7 +570,7 @@ def test_list_flavors(self): self.cloud.list_flavors.invalidate(self.cloud) self.assertResourceListEqual( - self.cloud.list_flavors(), fakes.FAKE_FLAVOR_LIST, _flavor.Flavor + self.cloud.list_flavors(), flavors, _flavor.Flavor ) self.assert_calls() diff --git a/openstack/tests/unit/compute/v2/test_flavor.py b/openstack/tests/unit/compute/v2/test_flavor.py index d3bc42350..35dcdff4b 100644 --- a/openstack/tests/unit/compute/v2/test_flavor.py +++ b/openstack/tests/unit/compute/v2/test_flavor.py @@ -91,6 +91,12 @@ def test_make_basic(self): ) self.assertEqual(BASIC_EXAMPLE['rxtx_factor'], sot.rxtx_factor) + def test_make_basic_swap(self): + sot = flavor.Flavor(id=IDENTIFIER, swap="") + self.assertEqual(0, sot.swap) + sot1 = flavor.Flavor(id=IDENTIFIER, swap=0) + self.assertEqual(0, sot1.swap) + def test_make_defaults(self): sot = flavor.Flavor(**DEFAULTS_EXAMPLE) self.assertEqual(DEFAULTS_EXAMPLE['original_name'], sot.name) From 9fc4b9f26aeee0ed157d21c47aaad600199b4dd4 Mon Sep 17 00:00:00 2001 From: Rodion Gyrbu Date: Wed, 21 Jun 2023 16:29:45 +0300 Subject: [PATCH 3303/3836] Add missing `force` parameter Change-Id: I6c413cdb9001373d2f73ad9df56bb86dd51566ef --- openstack/cloud/_block_storage.py | 2 +- openstack/tests/fakes.py | 1 + .../tests/functional/cloud/test_compute.py | 18 ++++++++++++++++++ .../unit/cloud/test_create_volume_snapshot.py | 18 +++++++++++++++--- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index d55221fa7..1568db019 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -461,7 +461,7 @@ def create_volume_snapshot( :raises: OpenStackCloudException on operation error. """ kwargs = self._get_volume_kwargs(kwargs) - payload = {'volume_id': volume_id} + payload = {'volume_id': volume_id, 'force': force} payload.update(kwargs) snapshot = self.block_storage.create_snapshot(**payload) if wait: diff --git a/openstack/tests/fakes.py b/openstack/tests/fakes.py index 3804a0599..533aed706 100644 --- a/openstack/tests/fakes.py +++ b/openstack/tests/fakes.py @@ -400,6 +400,7 @@ def __init__(self, id, status, name, description, size=75): self.updated_at = None self.volume_id = '12345' self.metadata = {} + self.is_forced = False class FakeMachine: diff --git a/openstack/tests/functional/cloud/test_compute.py b/openstack/tests/functional/cloud/test_compute.py index becb67cce..0a5f1c934 100644 --- a/openstack/tests/functional/cloud/test_compute.py +++ b/openstack/tests/functional/cloud/test_compute.py @@ -115,6 +115,24 @@ def test_attach_detach_volume(self): self.assertTrue(vol_attachment[key]) # assert string is not empty self.assertIsNone(self.user_cloud.detach_volume(server, volume)) + def test_attach_volume_create_snapshot(self): + self.skipTest('Volume functional tests temporarily disabled') + server_name = self.getUniqueString() + self.addCleanup(self._cleanup_servers_and_volumes, server_name) + server = self.user_cloud.create_server( + name=server_name, image=self.image, flavor=self.flavor, wait=True + ) + volume = self.user_cloud.create_volume(1) + vol_attachment = self.user_cloud.attach_volume(server, volume) + for key in ('device', 'serverId', 'volumeId'): + self.assertIn(key, vol_attachment) + self.assertTrue(vol_attachment[key]) # assert string is not empty + snapshot = self.user_cloud.create_volume_snapshot( + volume_id=volume.id, force=True, wait=True + ) + self.addCleanup(self.user_cloud.delete_volume_snapshot, snapshot['id']) + self.assertIsNotNone(snapshot) + def test_create_and_delete_server_with_config_drive(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) server = self.user_cloud.create_server( diff --git a/openstack/tests/unit/cloud/test_create_volume_snapshot.py b/openstack/tests/unit/cloud/test_create_volume_snapshot.py index ed8421475..b1e0e2fb5 100644 --- a/openstack/tests/unit/cloud/test_create_volume_snapshot.py +++ b/openstack/tests/unit/cloud/test_create_volume_snapshot.py @@ -59,7 +59,11 @@ def test_create_volume_snapshot_wait(self): 'volumev3', 'public', append=['snapshots'] ), json={'snapshot': build_snapshot_dict}, - validate=dict(json={'snapshot': {'volume_id': '1234'}}), + validate=dict( + json={ + 'snapshot': {'volume_id': '1234', 'force': False} + } + ), ), dict( method='GET', @@ -104,7 +108,11 @@ def test_create_volume_snapshot_with_timeout(self): 'volumev3', 'public', append=['snapshots'] ), json={'snapshot': build_snapshot_dict}, - validate=dict(json={'snapshot': {'volume_id': '1234'}}), + validate=dict( + json={ + 'snapshot': {'volume_id': '1234', 'force': False} + } + ), ), dict( method='GET', @@ -149,7 +157,11 @@ def test_create_volume_snapshot_with_error(self): 'volumev3', 'public', append=['snapshots'] ), json={'snapshot': build_snapshot_dict}, - validate=dict(json={'snapshot': {'volume_id': '1234'}}), + validate=dict( + json={ + 'snapshot': {'volume_id': '1234', 'force': False} + } + ), ), dict( method='GET', From 2da00af16014e16d447e28940742e381a0c9c1a6 Mon Sep 17 00:00:00 2001 From: ricolin Date: Wed, 28 Jun 2023 14:17:14 +0800 Subject: [PATCH 3304/3836] Fix broken python3.6 support This propose to remove import for annotations. Change-Id: I1c3f819a8f96d8de6f52a797fb46accf9e25d479 --- openstack/utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openstack/utils.py b/openstack/utils.py index e9864dc89..012fdbab8 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from __future__ import annotations - from collections.abc import Mapping import hashlib import queue From 8a5f2b0d5e0361a15aa71a3d3eb4456b1db678e4 Mon Sep 17 00:00:00 2001 From: Danila Balagansky Date: Wed, 28 Jun 2023 21:37:54 +0300 Subject: [PATCH 3305/3836] Add missing `return` in `upload_volume_to_image` Change-Id: I85cb9175682fb0aecbd894c4275d8d661c1d106c --- openstack/block_storage/v3/_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 34f03aea9..7dc96ea40 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -883,7 +883,7 @@ def upload_volume_to_image( :returns: dictionary describing the image. """ volume = self._get_resource(_volume.Volume, volume) - volume.upload_to_image( + return volume.upload_to_image( self, image_name, force=force, From 0017ffb050e6df0df28716d32e2b6b03e145a4b6 Mon Sep 17 00:00:00 2001 From: Kafilat Adeleke Date: Wed, 23 Jun 2021 08:33:31 +0000 Subject: [PATCH 3306/3836] Adds share group resource to shared file system Introduce Share groups class with basic methods including list, create, delete, get and update to shared file system service. Change-Id: I7d7e4e7addc1c1b040276e10f8152bc0adb8eeb2 --- .../user/proxies/shared_file_system.rst | 12 +++ .../resources/shared_file_system/index.rst | 1 + .../shared_file_system/v2/share_group.rst | 13 +++ openstack/shared_file_system/v2/_proxy.py | 102 ++++++++++++++++++ .../shared_file_system/v2/share_group.py | 56 ++++++++++ .../functional/shared_file_system/base.py | 10 ++ .../shared_file_system/test_share_group.py | 66 ++++++++++++ .../unit/shared_file_system/v2/test_proxy.py | 44 ++++++++ .../shared_file_system/v2/test_share_group.py | 81 ++++++++++++++ ...group-to-shared-file-8cee20d8aa2afbb7.yaml | 5 + 10 files changed, 390 insertions(+) create mode 100644 doc/source/user/resources/shared_file_system/v2/share_group.rst create mode 100644 openstack/shared_file_system/v2/share_group.py create mode 100644 openstack/tests/functional/shared_file_system/test_share_group.py create mode 100644 openstack/tests/unit/shared_file_system/v2/test_share_group.py create mode 100644 releasenotes/notes/add-share_group-to-shared-file-8cee20d8aa2afbb7.yaml diff --git a/doc/source/user/proxies/shared_file_system.rst b/doc/source/user/proxies/shared_file_system.rst index 1887f2668..f954b1847 100644 --- a/doc/source/user/proxies/shared_file_system.rst +++ b/doc/source/user/proxies/shared_file_system.rst @@ -138,3 +138,15 @@ Shared File Systems service. :noindex: :members: access_rules, get_access_rule, create_access_rule, delete_access_rule + + +Shared File System Share Groups +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Interact with Share groups supported by the Shared File Systems +service. + +.. autoclass:: openstack.shared_file_system.v2._proxy.Proxy + :noindex: + :members: share_groups, get_share_group, delete_share_group, + update_share_group, create_share_group, find_share_group diff --git a/doc/source/user/resources/shared_file_system/index.rst b/doc/source/user/resources/shared_file_system/index.rst index 84dd66ed8..c0b6fa4d3 100644 --- a/doc/source/user/resources/shared_file_system/index.rst +++ b/doc/source/user/resources/shared_file_system/index.rst @@ -14,4 +14,5 @@ Shared File System service resources v2/share_snapshot_instance v2/share_network v2/user_message + v2/share_group v2/share_access_rule diff --git a/doc/source/user/resources/shared_file_system/v2/share_group.rst b/doc/source/user/resources/shared_file_system/v2/share_group.rst new file mode 100644 index 000000000..232202b56 --- /dev/null +++ b/doc/source/user/resources/shared_file_system/v2/share_group.rst @@ -0,0 +1,13 @@ +openstack.shared_file_system.v2.share_group +=========================================== + +.. automodule:: openstack.shared_file_system.v2.share_group + +The ShareGroup Class +-------------------- + +The ``ShareGroup`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.shared_file_system.v2.share_group.ShareGroup + :members: diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index 52495e4ec..a682cdd9b 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -17,6 +17,7 @@ ) from openstack.shared_file_system.v2 import limit as _limit from openstack.shared_file_system.v2 import share as _share +from openstack.shared_file_system.v2 import share_group as _share_group from openstack.shared_file_system.v2 import ( share_access_rule as _share_access_rule, ) @@ -50,6 +51,7 @@ class Proxy(proxy.Proxy): "share_instance": _share_instance.ShareInstance, "share_export_locations": _share_export_locations.ShareExportLocation, "share_access_rule": _share_access_rule.ShareAccessRule, + "share_group": _share_group.ShareGroup, } def availability_zones(self): @@ -211,6 +213,106 @@ def resize_share( elif new_size < res.size and no_shrink is not True: res.shrink_share(self, new_size) + def share_groups(self, **query): + """Lists all share groups. + + :param kwargs query: Optional query parameters to be sent to limit + the share groups being returned. Available parameters include: + + * status: Filters by a share group status. + * name: The user defined name of the resource to filter resources + by. + * description: The user defined description text that can be used + to filter resources. + * project_id: The project ID of the user or service. + * share_server_id: The UUID of the share server. + * snapshot_id: The UUID of the share’s base snapshot to filter + the request based on. + * host: The host name for the back end. + * share_network_id: The UUID of the share network to filter + resources by. + * share_group_type_id: The share group type ID to filter + share groups. + * share_group_snapshot_id: The source share group snapshot ID to + list the share group. + * share_types: A list of one or more share type IDs. Allows + filtering share groups. + * limit: The maximum number of share groups members to return. + * offset: The offset to define start point of share or share + group listing. + * sort_key: The key to sort a list of shares. + * sort_dir: The direction to sort a list of shares + * name~: The name pattern that can be used to filter shares, + share snapshots, share networks or share groups. + * description~: The description pattern that can be used to + filter shares, share snapshots, share networks or share groups. + + :returns: A generator of manila share group resources + :rtype: :class:`~openstack.shared_file_system.v2. + share_group.ShareGroup` + """ + return self._list(_share_group.ShareGroup, **query) + + def get_share_group(self, share_group_id): + """Lists details for a share group. + + :param share: The ID of the share group to get + :returns: Details of the identified share group + :rtype: :class:`~openstack.shared_file_system.v2. + share_group.ShareGroup` + """ + return self._get(_share_group.ShareGroup, share_group_id) + + def find_share_group(self, name_or_id, ignore_missing=True): + """Finds a single share group + + :param name_or_id: The name or ID of a share group. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :returns: One :class:`~openstack.shared_file_system.v2. + share_group.ShareGroup` + or None + """ + return self._find( + _share_group.ShareGroup, name_or_id, ignore_missing=ignore_missing + ) + + def create_share_group(self, **attrs): + """Creates a share group from attributes + + :returns: Details of the new share group + :rtype: :class:`~openstack.shared_file_system.v2. + share_group.ShareGroup` + """ + return self._create(_share_group.ShareGroup, **attrs) + + def update_share_group(self, share_group_id, **kwargs): + """Updates details of a single share group + + :param share: The ID of the share group + :returns: Updated details of the identified share group + :rtype: :class:`~openstack.shared_file_system.v2. + share_group.ShareGroup` + """ + return self._update(_share_group.ShareGroup, share_group_id, **kwargs) + + def delete_share_group(self, share_group_id, ignore_missing=True): + """Deletes a single share group + + :param share: The ID of the share group + :returns: Result of the "delete" on share group + :rtype: :class:`~openstack.shared_file_system.v2. + share_group.ShareGroup` + """ + return self._delete( + _share_group.ShareGroup, + share_group_id, + ignore_missing=ignore_missing, + ) + def wait_for_status( self, res, status='active', failures=None, interval=2, wait=120 ): diff --git a/openstack/shared_file_system/v2/share_group.py b/openstack/shared_file_system/v2/share_group.py new file mode 100644 index 000000000..1dadbd9e4 --- /dev/null +++ b/openstack/shared_file_system/v2/share_group.py @@ -0,0 +1,56 @@ +# 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 resource + + +class ShareGroup(resource.Resource): + resource_key = "share_group" + resources_key = "share_groups" + base_path = "/share-groups" + + _query_mapping = resource.QueryParameters("share_group_id") + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + allow_head = False + + #: Properties + #: The availability zone ID that the share group exists within. + availability_zone = resource.Body("availability_zone", type=str) + #: The consistency snapshot support. + consistent_snapshot_support = resource.Body( + "consistent_snapshot_support", type=str + ) + #: The date and time stamp when the resource was created within the + #: service’s database. + created_at = resource.Body("created_at", type=str) + #: The user defined description of the resource. + description = resource.Body("description", type=str) + #: The ID of the project that owns the resource. + project_id = resource.Body("project_id", type=str) + #: The share group snapshot ID. + share_group_snapshot_id = resource.Body( + "share_group_snapshot_id", type=str + ) + #: The share group type ID. + share_group_type_id = resource.Body("share_group_type_id", type=str) + #: The share network ID where the resource is exported to. + share_network_id = resource.Body("share_network_id", type=str) + #: A list of share type IDs. + share_types = resource.Body("share_types", type=list) + #: The share status + status = resource.Body("status", type=str) diff --git a/openstack/tests/functional/shared_file_system/base.py b/openstack/tests/functional/shared_file_system/base.py index f319c7368..72926e901 100644 --- a/openstack/tests/functional/shared_file_system/base.py +++ b/openstack/tests/functional/shared_file_system/base.py @@ -65,3 +65,13 @@ def create_share_snapshot(self, share_id, **kwargs): ) self.assertIsNotNone(share_snapshot.id) return share_snapshot + + def create_share_group(self, **kwargs): + share_group = self.user_cloud.share.create_share_group(**kwargs) + self.addCleanup( + self.conn.share.delete_share_group, + share_group.id, + ignore_missing=True, + ) + self.assertIsNotNone(share_group.id) + return share_group diff --git a/openstack/tests/functional/shared_file_system/test_share_group.py b/openstack/tests/functional/shared_file_system/test_share_group.py new file mode 100644 index 000000000..cfd0bd9bb --- /dev/null +++ b/openstack/tests/functional/shared_file_system/test_share_group.py @@ -0,0 +1,66 @@ +# 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.shared_file_system.v2 import share_group as _share_group +from openstack.tests.functional.shared_file_system import base + + +class ShareGroupTest(base.BaseSharedFileSystemTest): + def setUp(self): + super(ShareGroupTest, self).setUp() + + self.SHARE_GROUP_NAME = self.getUniqueString() + share_grp = self.user_cloud.shared_file_system.create_share_group( + name=self.SHARE_GROUP_NAME + ) + self.user_cloud.shared_file_system.wait_for_status( + share_grp, + status='available', + failures=['error'], + interval=5, + wait=self._wait_for_timeout, + ) + self.assertIsNotNone(share_grp) + self.assertIsNotNone(share_grp.id) + self.SHARE_GROUP_ID = share_grp.id + + def test_get(self): + sot = self.user_cloud.shared_file_system.get_share_group( + self.SHARE_GROUP_ID + ) + assert isinstance(sot, _share_group.ShareGroup) + self.assertEqual(self.SHARE_GROUP_ID, sot.id) + + def test_find(self): + sot = self.user_cloud.shared_file_system.find_share_group( + self.SHARE_GROUP_NAME + ) + assert isinstance(sot, _share_group.ShareGroup) + self.assertEqual(self.SHARE_GROUP_NAME, sot.name) + self.assertEqual(self.SHARE_GROUP_ID, sot.id) + + def test_list_delete_share_group(self): + s_grps = self.user_cloud.shared_file_system.share_groups() + self.assertGreater(len(list(s_grps)), 0) + for s_grp in s_grps: + for attribute in ('id', 'name', 'created_at'): + self.assertTrue(hasattr(s_grp, attribute)) + + sot = self.conn.shared_file_system.delete_share_group(s_grp) + self.assertIsNone(sot) + + def test_update(self): + u_gp = self.user_cloud.shared_file_system.update_share_group( + self.SHARE_GROUP_ID, description='updated share group' + ) + get_u_gp = self.user_cloud.shared_file_system.get_share_group(u_gp.id) + self.assertEqual('updated share group', get_u_gp.description) diff --git a/openstack/tests/unit/shared_file_system/v2/test_proxy.py b/openstack/tests/unit/shared_file_system/v2/test_proxy.py index 18ed60a50..6fc6928d5 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_proxy.py +++ b/openstack/tests/unit/shared_file_system/v2/test_proxy.py @@ -16,6 +16,7 @@ from openstack.shared_file_system.v2 import limit from openstack.shared_file_system.v2 import share from openstack.shared_file_system.v2 import share_access_rule +from openstack.shared_file_system.v2 import share_group from openstack.shared_file_system.v2 import share_instance from openstack.shared_file_system.v2 import share_network from openstack.shared_file_system.v2 import share_network_subnet @@ -417,3 +418,46 @@ def test_access_rules_delete(self): method_args=['access_id', 'share_id', 'ignore_missing'], expected_args=[self.proxy, 'share_id'], ) + + +class TestShareGroupResource(test_proxy_base.TestProxyBase): + def setUp(self): + super(TestShareGroupResource, self).setUp() + self.proxy = _proxy.Proxy(self.session) + + def test_share_groups(self): + self.verify_list(self.proxy.share_groups, share_group.ShareGroup) + + def test_share_groups_query(self): + self.verify_list( + self.proxy.share_groups, + share_group.ShareGroup, + method_kwargs={"query": 1}, + expected_kwargs={"query": 1}, + ) + + def test_share_group_get(self): + self.verify_get(self.proxy.get_share_group, share_group.ShareGroup) + + def test_share_group_find(self): + self.verify_find(self.proxy.find_share_group, share_group.ShareGroup) + + def test_share_group_delete(self): + self.verify_delete( + self.proxy.delete_share_group, share_group.ShareGroup, False + ) + + def test_share_group_delete_ignore(self): + self.verify_delete( + self.proxy.delete_share_group, share_group.ShareGroup, True + ) + + def test_share_group_create(self): + self.verify_create( + self.proxy.create_share_group, share_group.ShareGroup + ) + + def test_share_group_update(self): + self.verify_update( + self.proxy.update_share_group, share_group.ShareGroup + ) diff --git a/openstack/tests/unit/shared_file_system/v2/test_share_group.py b/openstack/tests/unit/shared_file_system/v2/test_share_group.py new file mode 100644 index 000000000..f75976ff8 --- /dev/null +++ b/openstack/tests/unit/shared_file_system/v2/test_share_group.py @@ -0,0 +1,81 @@ +# 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.shared_file_system.v2 import share_group +from openstack.tests.unit import base + + +EXAMPLE = { + "status": "creating", + "description": None, + "links": "[]", + "availability_zone": None, + "source_share_group_snapshot_id": None, + "share_network_id": None, + "share_server_id": None, + "host": None, + "share_group_type_id": "89861c2a-10bf-4013-bdd4-3d020466aee4", + "consistent_snapshot_support": None, + "id": "f9c1f80c-2392-4e34-bd90-fc89cdc5bf93", + "name": None, + "created_at": "2021-06-03T19:20:33.974421", + "project_id": "e23850eeb91d4fa3866af634223e454c", + "share_types": ["ecd11f4c-d811-4471-b656-c755c77e02ba"], +} + + +class TestShareGroups(base.TestCase): + def test_basic(self): + share_groups = share_group.ShareGroup() + self.assertEqual('share_groups', share_groups.resources_key) + self.assertEqual('/share-groups', share_groups.base_path) + self.assertTrue(share_groups.allow_list) + self.assertTrue(share_groups.allow_fetch) + self.assertTrue(share_groups.allow_create) + self.assertTrue(share_groups.allow_commit) + self.assertTrue(share_groups.allow_delete) + self.assertFalse(share_groups.allow_head) + + self.assertDictEqual( + { + "limit": "limit", + "marker": "marker", + "share_group_id": "share_group_id", + }, + share_groups._query_mapping._mapping, + ) + + def test_make_share_groups(self): + share_group_res = share_group.ShareGroup(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], share_group_res.id) + self.assertEqual(EXAMPLE['status'], share_group_res.status) + self.assertEqual( + EXAMPLE['availability_zone'], share_group_res.availability_zone + ) + self.assertEqual(EXAMPLE['description'], share_group_res.description) + self.assertEqual( + EXAMPLE['source_share_group_snapshot_id'], + share_group_res.share_group_snapshot_id, + ) + self.assertEqual( + EXAMPLE['share_network_id'], share_group_res.share_network_id + ) + self.assertEqual( + EXAMPLE['share_group_type_id'], share_group_res.share_group_type_id + ) + self.assertEqual( + EXAMPLE['consistent_snapshot_support'], + share_group_res.consistent_snapshot_support, + ) + self.assertEqual(EXAMPLE['created_at'], share_group_res.created_at) + self.assertEqual(EXAMPLE['project_id'], share_group_res.project_id) + self.assertEqual(EXAMPLE['share_types'], share_group_res.share_types) diff --git a/releasenotes/notes/add-share_group-to-shared-file-8cee20d8aa2afbb7.yaml b/releasenotes/notes/add-share_group-to-shared-file-8cee20d8aa2afbb7.yaml new file mode 100644 index 000000000..ae76ef055 --- /dev/null +++ b/releasenotes/notes/add-share_group-to-shared-file-8cee20d8aa2afbb7.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added support to create, update, list, get, and delete share + groups on the shared file system service. From 4838e5e7292e0fdd851e0c4758ff30006b7d5914 Mon Sep 17 00:00:00 2001 From: Ashley Rodriguez Date: Thu, 12 May 2022 20:07:42 +0000 Subject: [PATCH 3307/3836] Add share group snapshots to shared file systems. Introduce Share Group Snapshot class with basic methods including list, create, delete, get and update to shared file system storage service. Change-Id: Ice8750dcf07ff436ba8d9e9cf5e8cb183b5b3825 --- doc/source/user/guides/shared_file_system.rst | 64 +++++++++ .../user/proxies/shared_file_system.rst | 13 ++ .../resources/shared_file_system/index.rst | 1 + .../v2/share_group_snapshot.rst | 13 ++ .../share_group_snapshots.py | 66 ++++++++++ openstack/shared_file_system/v2/_proxy.py | 123 ++++++++++++++++++ .../v2/share_group_snapshot.py | 86 ++++++++++++ .../test_share_group_snapshot.py | 105 +++++++++++++++ .../unit/shared_file_system/v2/test_proxy.py | 33 +++++ .../v2/test_share_group_snapshot.py | 104 +++++++++++++++ ...share-group-snapshot-c5099e6c8accf077.yaml | 5 + 11 files changed, 613 insertions(+) create mode 100644 doc/source/user/resources/shared_file_system/v2/share_group_snapshot.rst create mode 100644 examples/shared_file_system/share_group_snapshots.py create mode 100644 openstack/shared_file_system/v2/share_group_snapshot.py create mode 100644 openstack/tests/functional/shared_file_system/test_share_group_snapshot.py create mode 100644 openstack/tests/unit/shared_file_system/v2/test_share_group_snapshot.py create mode 100644 releasenotes/notes/add-shared-file-system-share-group-snapshot-c5099e6c8accf077.yaml diff --git a/doc/source/user/guides/shared_file_system.rst b/doc/source/user/guides/shared_file_system.rst index 86c25a6b0..938e75fc5 100644 --- a/doc/source/user/guides/shared_file_system.rst +++ b/doc/source/user/guides/shared_file_system.rst @@ -70,3 +70,67 @@ size. For details on resizing shares, refer to the :pyobject: resize_share .. literalinclude:: ../examples/shared_file_system/shares.py :pyobject: resize_shares_without_shrink + + +List Share Group Snapshots +-------------------------- + +A share group snapshot is a point-in-time, read-only copy of the data that is +contained in a share group. You can list all share group snapshots + +.. literalinclude:: ../examples/shared_file_system/share_group_snapshots.py + :pyobject: list_share_group_snapshots + + +Get Share Group Snapshot +------------------------ + +Show share group snapshot details + +.. literalinclude:: ../examples/shared_file_system/share_group_snapshots.py + :pyobject: get_share_group_snapshot + + +List Share Group Snapshot Members +--------------------------------- + +Lists all share group snapshots members. + +.. literalinclude:: ../examples/shared_file_system/share_group_snapshots.py + :pyobject: share_group_snapshot_members + + +Create Share Group Snapshot +--------------------------- + +Creates a snapshot from a share group. + +.. literalinclude:: ../examples/shared_file_system/share_group_snapshots.py + :pyobject: create_share_group_snapshot + + +Reset Share Group Snapshot +--------------------------- + +Reset share group snapshot state. + +.. literalinclude:: ../examples/shared_file_system/share_group_snapshots.py + :pyobject: reset_share_group_snapshot_status + + +Update Share Group Snapshot +--------------------------- + +Updates a share group snapshot. + +.. literalinclude:: ../examples/shared_file_system/share_group_snapshots.py + :pyobject: update_share_group_snapshot + + +Delete Share Group Snapshot +--------------------------- + +Deletes a share group snapshot. + +.. literalinclude:: ../examples/shared_file_system/share_group_snapshots.py + :pyobject: delete_share_group_snapshot diff --git a/doc/source/user/proxies/shared_file_system.rst b/doc/source/user/proxies/shared_file_system.rst index f954b1847..28a4ca50a 100644 --- a/doc/source/user/proxies/shared_file_system.rst +++ b/doc/source/user/proxies/shared_file_system.rst @@ -150,3 +150,16 @@ service. :noindex: :members: share_groups, get_share_group, delete_share_group, update_share_group, create_share_group, find_share_group + + +Shared File System Share Group Snapshots +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Interact with Share Group Snapshots by the Shared File Systems +service. + +.. autoclass:: openstack.shared_file_system.v2._proxy.Proxy + :noindex: + :members: share_group_snapshots, get_share_group_snapshot, create_share_group_snapshot, + reset_share_group_snapshot_status, update_share_group_snapshot, + delete_share_group_snapshot diff --git a/doc/source/user/resources/shared_file_system/index.rst b/doc/source/user/resources/shared_file_system/index.rst index c0b6fa4d3..e2bd0488a 100644 --- a/doc/source/user/resources/shared_file_system/index.rst +++ b/doc/source/user/resources/shared_file_system/index.rst @@ -16,3 +16,4 @@ Shared File System service resources v2/user_message v2/share_group v2/share_access_rule + v2/share_group_snapshot diff --git a/doc/source/user/resources/shared_file_system/v2/share_group_snapshot.rst b/doc/source/user/resources/shared_file_system/v2/share_group_snapshot.rst new file mode 100644 index 000000000..54fb704e9 --- /dev/null +++ b/doc/source/user/resources/shared_file_system/v2/share_group_snapshot.rst @@ -0,0 +1,13 @@ +openstack.shared_file_system.v2.share_group_snapshot +==================================================== + +.. automodule: : openstack.shared_file_system.v2.share_group_snapshot + +The ShareGroupSnapshot Class +---------------------------- + +The ``ShareGroupSnapshot`` class inherits from +: class: `~openstack.resource.Resource`. + +.. autoclass: : openstack.shared_file_system.v2.ShareGroupSnapshot + :members: diff --git a/examples/shared_file_system/share_group_snapshots.py b/examples/shared_file_system/share_group_snapshots.py new file mode 100644 index 000000000..33ea2e7c5 --- /dev/null +++ b/examples/shared_file_system/share_group_snapshots.py @@ -0,0 +1,66 @@ +# 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. + +""" +List resources from the Shared File System service. + +For a full guide see +https://docs.openstack.org/openstacksdk/latest/user/guides/shared_file_system.html +""" + + +def list_share_group_snapshots(conn, **query): + print("List all share group snapshots:") + share_group_snapshots = conn.share.share_group_snapshots(**query) + for share_group_snapshot in share_group_snapshots: + print(share_group_snapshot) + + +def get_share_group_snapshot(conn, group_snapshot_id): + print("Show share group snapshot with given Id:") + share_group_snapshot = conn.share.get_share_group_snapshots( + group_snapshot_id + ) + print(share_group_snapshot) + + +def share_group_snapshot_members(conn, group_snapshot_id): + print("Show share group snapshot members with given Id:") + members = conn.share.share_group_snapshot_members(group_snapshot_id) + for member in members: + print(member) + + +def create_share_group_snapshot(conn, share_group_id, **attrs): + print("Creating a share group snapshot from given attributes:") + share_group_snapshot = conn.share.create_share_group_snapshot( + share_group_id, **attrs + ) + print(share_group_snapshot) + + +def reset_share_group_snapshot_status(conn, group_snapshot_id, status): + print("Reseting the share group snapshot status:") + conn.share.reset_share_group_snapshot_status(group_snapshot_id, status) + + +def update_share_group_snapshot(conn, group_snapshot_id, **attrs): + print("Updating a share group snapshot with given Id:") + share_group_snapshot = conn.share.update_share_group_snapshot( + group_snapshot_id, **attrs + ) + print(share_group_snapshot) + + +def delete_share_group_snapshot(conn, group_snapshot_id): + print("Deleting a share group snapshot with given Id:") + conn.share.delete_share_group_snapshot(group_snapshot_id) diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index a682cdd9b..157b1b7a5 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -18,6 +18,9 @@ from openstack.shared_file_system.v2 import limit as _limit from openstack.shared_file_system.v2 import share as _share from openstack.shared_file_system.v2 import share_group as _share_group +from openstack.shared_file_system.v2 import ( + share_group_snapshot as _share_group_snapshot, +) from openstack.shared_file_system.v2 import ( share_access_rule as _share_access_rule, ) @@ -52,6 +55,7 @@ class Proxy(proxy.Proxy): "share_export_locations": _share_export_locations.ShareExportLocation, "share_access_rule": _share_access_rule.ShareAccessRule, "share_group": _share_group.ShareGroup, + "share_group_snapshot": _share_group_snapshot.ShareGroupSnapshot, } def availability_zones(self): @@ -816,3 +820,122 @@ def delete_access_rule(self, access_id, share_id, ignore_missing=True): """ res = self._get_resource(_share_access_rule.ShareAccessRule, access_id) res.delete(self, share_id, ignore_missing=ignore_missing) + + def share_group_snapshots(self, details=True, **query): + """Lists all share group snapshots. + + :param kwargs query: Optional query parameters to be sent + to limit the share group snapshots being returned. + Available parameters include: + + * project_id: The ID of the project that owns the resource. + * name: The user defined name of the resource to filter resources. + * description: The user defined description text that can be used + to filter resources. + * status: Filters by a share status + * share_group_id: The UUID of a share group to filter resource. + * limit: The maximum number of share group snapshot members + to return. + * offset: The offset to define start point of share or + share group listing. + * sort_key: The key to sort a list of shares. + * sort_dir: The direction to sort a list of shares. A valid + value is asc, or desc. + + :returns: Details of share group snapshots resources + :rtype: :class:`~openstack.shared_file_system.v2. + share_group_snapshot.ShareGroupSnapshot` + """ + base_path = '/share-group-snapshots/detail' if details else None + return self._list( + _share_group_snapshot.ShareGroupSnapshot, + base_path=base_path, + **query + ) + + def share_group_snapshot_members(self, group_snapshot_id): + """Lists all share group snapshots members. + + :param group_snapshot_id: The ID of the group snapshot to get + :returns: List of the share group snapshot members, which are + share snapshots. + :rtype: :dict: Attributes of the share snapshots. + """ + res = self._get( + _share_group_snapshot.ShareGroupSnapshot, group_snapshot_id + ) + response = res.members(self) + return response + + def get_share_group_snapshot(self, group_snapshot_id): + """Show share group snapshot details + + :param group_snapshot_id: The ID of the group snapshot to get + :returns: Details of the group snapshot + :rtype: :class:`~openstack.shared_file_system.v2. + share_group_snapshot.ShareGroupSnapshot` + """ + return self._get( + _share_group_snapshot.ShareGroupSnapshot, group_snapshot_id + ) + + def create_share_group_snapshot(self, share_group_id, **attrs): + """Creates a point-in-time snapshot copy of a share group. + + :returns: Details of the new snapshot + :param dict attrs: Attributes which will be used to create + a :class:`~openstack.shared_file_system.v2. + share_group_snapshots.ShareGroupSnapshots`, + :param 'share_group_id': ID of the share group to have the snapshot + taken. + :rtype: :class:`~openstack.shared_file_system.v2. + share_group_snapshot.ShareGroupSnapshot` + """ + return self._create( + _share_group_snapshot.ShareGroupSnapshot, + share_group_id=share_group_id, + **attrs + ) + + def reset_share_group_snapshot_status(self, group_snapshot_id, status): + """Reset share group snapshot state. + + :param group_snapshot_id: The ID of the share group snapshot to reset + :param status: The state of the share group snapshot to be set, A + valid value is "creating", "error", "available", "deleting", + "error_deleting". + :rtype: ``None`` + """ + res = self._get( + _share_group_snapshot.ShareGroupSnapshot, group_snapshot_id + ) + res.reset_status(self, status) + + def update_share_group_snapshot(self, group_snapshot_id, **attrs): + """Updates a share group snapshot. + + :param group_snapshot_id: The ID of the share group snapshot to update + :param dict attrs: The attributes to update on the share group snapshot + :returns: the updated share group snapshot + :rtype: :class:`~openstack.shared_file_system.v2. + share_group_snapshot.ShareGroupSnapshot` + """ + return self._update( + _share_group_snapshot.ShareGroupSnapshot, + group_snapshot_id, + **attrs + ) + + def delete_share_group_snapshot( + self, group_snapshot_id, ignore_missing=True + ): + """Deletes a share group snapshot. + + :param group_snapshot_id: The ID of the share group snapshot to delete + :rtype: ``None`` + """ + self._delete( + _share_group_snapshot.ShareGroupSnapshot, + group_snapshot_id, + ignore_missing=ignore_missing, + ) diff --git a/openstack/shared_file_system/v2/share_group_snapshot.py b/openstack/shared_file_system/v2/share_group_snapshot.py new file mode 100644 index 000000000..366a52275 --- /dev/null +++ b/openstack/shared_file_system/v2/share_group_snapshot.py @@ -0,0 +1,86 @@ +# 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 resource +from openstack import utils + + +class ShareGroupSnapshot(resource.Resource): + resource_key = "share_group_snapshot" + resources_key = "share_group_snapshots" + base_path = "/share-group-snapshots" + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + allow_head = False + + _query_mapping = resource.QueryParameters( + 'project_id', + 'all_tenants', + 'name', + 'description', + 'status', + 'share_group_id', + 'limit', + 'offset', + 'sort_key', + 'sort_dir', + ) + + #: Properties + #: The ID of the project that owns the resource. + project_id = resource.Body("project_id", type=str) + #: Filters by a share group snapshot status. A valid value is creating, + #: error, available, deleting, error_deleting. + status = resource.Body("status", type=str) + #: The UUID of the share group. + share_group_id = resource.Body("share_group_id", type=str) + #: The user defined description of the resource. + description = resource.Body("description", type=str) + #: The date and time stamp when the resource was created. + created_at = resource.Body("created_at", type=str) + #: The share group snapshot members. + members = resource.Body("members", type=str) + #: The snapshot size, in GiBs. + size = resource.Body("size", type=int) + #: NFS, CIFS, GlusterFS, HDFS, CephFS or MAPRFS. + share_protocol = resource.Body("share_proto", type=str) + + def _action(self, session, body, action='patch', microversion=None): + """Perform ShareGroupSnapshot actions given the message body.""" + # NOTE: This is using ShareGroupSnapshot.base_path instead of + # self.base_path as ShareGroupSnapshot instances can be acted on, + # but the URL used is sans any additional /detail/ part. + url = utils.urljoin(self.base_path, self.id, 'action') + headers = {'Accept': ''} + microversion = microversion or self._get_microversion( + session, action=action + ) + extra_attrs = {'microversion': microversion} + session.post(url, json=body, headers=headers, **extra_attrs) + + def reset_status(self, session, status): + body = {"reset_status": {"status": status}} + self._action(session, body) + + def members(self, session, microversion=None): + url = utils.urljoin(self.base_path, self.id, 'members') + microversion = microversion or self._get_microversion( + session, action='list' + ) + headers = {'Accept': ''} + response = session.get(url, headers=headers, microversion=microversion) + return response.json() diff --git a/openstack/tests/functional/shared_file_system/test_share_group_snapshot.py b/openstack/tests/functional/shared_file_system/test_share_group_snapshot.py new file mode 100644 index 000000000..662173cfb --- /dev/null +++ b/openstack/tests/functional/shared_file_system/test_share_group_snapshot.py @@ -0,0 +1,105 @@ +# 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 resource +from openstack.shared_file_system.v2 import ( + share_group_snapshot as _share_group_snapshot, +) +from openstack.tests.functional.shared_file_system import base + + +class ShareGroupSnapshotTest(base.BaseSharedFileSystemTest): + min_microversion = '2.55' + + def setUp(self): + super(ShareGroupSnapshotTest, self).setUp() + + self.SHARE_GROUP_NAME = self.getUniqueString() + share_grp = self.user_cloud.shared_file_system.create_share_group( + name=self.SHARE_GROUP_NAME + ) + self.user_cloud.shared_file_system.wait_for_status( + share_grp, + status='available', + failures=['error'], + interval=5, + wait=self._wait_for_timeout, + ) + self.assertIsNotNone(share_grp) + self.assertIsNotNone(share_grp.id) + self.SHARE_GROUP_ID = share_grp.id + + self.SHARE_GROUP_SNAPSHOT_NAME = self.getUniqueString() + grp_ss = ( + self.user_cloud.shared_file_system.create_share_group_snapshot( + self.SHARE_GROUP_ID, name=self.SHARE_GROUP_SNAPSHOT_NAME + ) + ) + self.user_cloud.shared_file_system.wait_for_status( + grp_ss, + status='available', + failures=['error'], + interval=5, + wait=self._wait_for_timeout, + ) + self.assertIsNotNone(grp_ss) + self.assertIsNotNone(grp_ss.id) + self.SHARE_GROUP_SNAPSHOT_ID = grp_ss.id + + def tearDown(self): + sot = self.user_cloud.shared_file_system.get_share_group_snapshot( + self.SHARE_GROUP_SNAPSHOT_ID + ) + self.user_cloud.shared_file_system.delete_share_group_snapshot( + self.SHARE_GROUP_SNAPSHOT_ID, ignore_missing=False + ) + resource.wait_for_delete( + self.user_cloud.share, sot, wait=self._wait_for_timeout, interval=2 + ) + self.user_cloud.shared_file_system.delete_share_group( + self.SHARE_GROUP_ID, ignore_missing=False + ) + super(ShareGroupSnapshotTest, self).tearDown() + + def test_get(self): + sot = self.user_cloud.shared_file_system.get_share_group_snapshot( + self.SHARE_GROUP_SNAPSHOT_ID + ) + assert isinstance(sot, _share_group_snapshot.ShareGroupSnapshot) + self.assertEqual(self.SHARE_GROUP_SNAPSHOT_ID, sot.id) + + def test_list(self): + snapshots = self.user_cloud.shared_file_system.share_group_snapshots() + self.assertGreater(len(list(snapshots)), 0) + for snapshot in snapshots: + for attribute in ('id', 'name', 'created_at'): + self.assertTrue(hasattr(snapshot, attribute)) + + def test_update(self): + u_ss = self.user_cloud.shared_file_system.update_share_group_snapshot( + self.SHARE_GROUP_SNAPSHOT_ID, + description='updated share group snapshot', + ) + get_u_ss = self.user_cloud.shared_file_system.get_share_group_snapshot( + u_ss.id + ) + self.assertEqual('updated share group snapshot', get_u_ss.description) + + def test_reset(self): + res = self.operator_cloud.shared_file_system.reset_share_group_snapshot_status( + self.SHARE_GROUP_SNAPSHOT_ID, 'error' + ) + self.assertIsNone(res) + sot = self.user_cloud.shared_file_system.get_share_group_snapshot( + self.SHARE_GROUP_SNAPSHOT_ID + ) + self.assertEqual('error', sot.status) diff --git a/openstack/tests/unit/shared_file_system/v2/test_proxy.py b/openstack/tests/unit/shared_file_system/v2/test_proxy.py index 6fc6928d5..795b35b44 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_proxy.py +++ b/openstack/tests/unit/shared_file_system/v2/test_proxy.py @@ -17,6 +17,7 @@ from openstack.shared_file_system.v2 import share from openstack.shared_file_system.v2 import share_access_rule from openstack.shared_file_system.v2 import share_group +from openstack.shared_file_system.v2 import share_group_snapshot from openstack.shared_file_system.v2 import share_instance from openstack.shared_file_system.v2 import share_network from openstack.shared_file_system.v2 import share_network_subnet @@ -461,3 +462,35 @@ def test_share_group_update(self): self.verify_update( self.proxy.update_share_group, share_group.ShareGroup ) + + def test_share_group_snapshots(self): + self.verify_list( + self.proxy.share_group_snapshots, + share_group_snapshot.ShareGroupSnapshot, + ) + + def test_share_group_snapshot_get(self): + self.verify_get( + self.proxy.get_share_group_snapshot, + share_group_snapshot.ShareGroupSnapshot, + ) + + def test_share_group_snapshot_update(self): + self.verify_update( + self.proxy.update_share_group_snapshot, + share_group_snapshot.ShareGroupSnapshot, + ) + + def test_share_group_snapshot_delete(self): + self.verify_delete( + self.proxy.delete_share_group_snapshot, + share_group_snapshot.ShareGroupSnapshot, + False, + ) + + def test_share_group_snapshot_delete_ignore(self): + self.verify_delete( + self.proxy.delete_share_group_snapshot, + share_group_snapshot.ShareGroupSnapshot, + True, + ) diff --git a/openstack/tests/unit/shared_file_system/v2/test_share_group_snapshot.py b/openstack/tests/unit/shared_file_system/v2/test_share_group_snapshot.py new file mode 100644 index 000000000..65be62157 --- /dev/null +++ b/openstack/tests/unit/shared_file_system/v2/test_share_group_snapshot.py @@ -0,0 +1,104 @@ +# 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 unittest import mock + +from keystoneauth1 import adapter + +from openstack.shared_file_system.v2 import share_group_snapshot +from openstack.tests.unit import base + +IDENTIFIER = '38152b6d-e1b5-465f-91bc-20bca4676a2a' +EXAMPLE = { + "id": IDENTIFIER, + "name": "snapshot_1", + "created_at": "2021-10-24T19:36:49.555325", + "status": "available", + "description": "first snapshot of sg-1", + "project_id": "7343d2f7770b4eb6a7bc33f44dcee1e0", + "share_group_id": "fb41512f-7c49-4304-afb1-66573c7feb14", +} + + +class TestShareGroupSnapshot(base.TestCase): + def test_basic(self): + share_group_snapshots = share_group_snapshot.ShareGroupSnapshot() + self.assertEqual( + 'share_group_snapshot', share_group_snapshots.resource_key + ) + self.assertEqual( + 'share_group_snapshots', share_group_snapshots.resources_key + ) + self.assertEqual( + '/share-group-snapshots', share_group_snapshots.base_path + ) + self.assertTrue(share_group_snapshots.allow_create) + self.assertTrue(share_group_snapshots.allow_fetch) + self.assertTrue(share_group_snapshots.allow_commit) + self.assertTrue(share_group_snapshots.allow_delete) + self.assertTrue(share_group_snapshots.allow_list) + self.assertFalse(share_group_snapshots.allow_head) + + def test_make_share_groups(self): + share_group_snapshots = share_group_snapshot.ShareGroupSnapshot( + **EXAMPLE + ) + self.assertEqual(EXAMPLE['id'], share_group_snapshots.id) + self.assertEqual(EXAMPLE['name'], share_group_snapshots.name) + self.assertEqual( + EXAMPLE['created_at'], share_group_snapshots.created_at + ) + self.assertEqual(EXAMPLE['status'], share_group_snapshots.status) + self.assertEqual( + EXAMPLE['description'], share_group_snapshots.description + ) + self.assertEqual( + EXAMPLE['project_id'], share_group_snapshots.project_id + ) + self.assertEqual( + EXAMPLE['share_group_id'], share_group_snapshots.share_group_id + ) + + +class TestShareGroupSnapshotActions(TestShareGroupSnapshot): + def setUp(self): + super(TestShareGroupSnapshot, self).setUp() + self.resp = mock.Mock() + self.resp.body = None + self.resp.status_code = 200 + self.resp.json = mock.Mock(return_value=self.resp.body) + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.default_microversion = '3.0' + self.sess.post = mock.Mock(return_value=self.resp) + self.sess._get_connection = mock.Mock(return_value=self.cloud) + + def test_reset_status(self): + sot = share_group_snapshot.ShareGroupSnapshot(**EXAMPLE) + self.assertIsNone(sot.reset_status(self.sess, 'available')) + url = f'share-group-snapshots/{IDENTIFIER}/action' + body = {"reset_status": {"status": 'available'}} + headers = {'Accept': ''} + self.sess.post.assert_called_with( + url, + json=body, + headers=headers, + microversion=self.sess.default_microversion, + ) + + def test_get_members(self): + sot = share_group_snapshot.ShareGroupSnapshot(**EXAMPLE) + sot.members(self.sess) + url = f'share-group-snapshots/{IDENTIFIER}/members' + headers = {'Accept': ''} + self.sess.get.assert_called_with( + url, headers=headers, microversion=self.sess.default_microversion + ) diff --git a/releasenotes/notes/add-shared-file-system-share-group-snapshot-c5099e6c8accf077.yaml b/releasenotes/notes/add-shared-file-system-share-group-snapshot-c5099e6c8accf077.yaml new file mode 100644 index 000000000..9c6b09686 --- /dev/null +++ b/releasenotes/notes/add-shared-file-system-share-group-snapshot-c5099e6c8accf077.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added support for list, show, update, delete, reset and create + Share Group Snapshots for Shared File Systems service. From 144c370b3541b31ac0cf9e9b77d7284cbf39d549 Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Thu, 6 Jul 2023 15:07:48 -0700 Subject: [PATCH 3308/3836] Treat server as a dict in add_server_interfaces SDK is generally favoring treating objects as dictionaries internally. In addition to promoting consistency and facilitating any future changes around the Server proxy objects, updating this method to use exclusively dictionary access will allow callers to submit a simple dictionary as returned by nova in place of a Server proxy object. Change-Id: I68cbb0408ec008f2234d0bed77fd25d4e5b43169 --- openstack/cloud/meta.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openstack/cloud/meta.py b/openstack/cloud/meta.py index 0f050cce0..8e57d8964 100644 --- a/openstack/cloud/meta.py +++ b/openstack/cloud/meta.py @@ -487,10 +487,10 @@ def add_server_interfaces(cloud, server): # where they were set previous, we use the values, so this will not break # clouds that provide the information if cloud.private and server.private_v4: - server.access_ipv4 = server.private_v4 + server['access_ipv4'] = server['private_v4'] else: - server.access_ipv4 = server.public_v4 - server.access_ipv6 = server.public_v6 + server['access_ipv4'] = server['public_v4'] + server['access_ipv6'] = server['public_v6'] return server From 69fea357b56815646a9df936562ae4013ec8cd9f Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 12 Jul 2023 11:20:00 +0100 Subject: [PATCH 3309/3836] tests: Update functional tests for resource providers Tweak this to use the new pattern of creating the resource in setUp, deleting them in tearDown, and doing everything else in one function. Take the opportunity to also add a test of the update mechanism while we're here. Change-Id: Iecb3437a7676250b52797bffe382bf8f2db0a647 Signed-off-by: Stephen Finucane --- .../placement/v1/test_resource_provider.py | 87 ++++++++++++++----- 1 file changed, 67 insertions(+), 20 deletions(-) diff --git a/openstack/tests/functional/placement/v1/test_resource_provider.py b/openstack/tests/functional/placement/v1/test_resource_provider.py index 307c82f69..e5db948a8 100644 --- a/openstack/tests/functional/placement/v1/test_resource_provider.py +++ b/openstack/tests/functional/placement/v1/test_resource_provider.py @@ -10,39 +10,86 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.placement.v1 import resource_provider +from openstack.placement.v1 import resource_provider as _resource_provider from openstack.tests.functional import base class TestResourceProvider(base.BaseFunctionalTest): def setUp(self): super().setUp() - self._set_operator_cloud(interface='admin') - self.NAME = self.getUniqueString() + if not self.operator_cloud.has_service('placement'): + self.skipTest('placement service not supported by cloud') - sot = self.conn.placement.create_resource_provider(name=self.NAME) - assert isinstance(sot, resource_provider.ResourceProvider) - self.assertEqual(self.NAME, sot.name) - self._resource_provider = sot + self.resource_provider_name = self.getUniqueString() + + resource_provider = ( + self.operator_cloud.placement.create_resource_provider( + name=self.resource_provider_name, + ) + ) + self.assertIsInstance( + resource_provider, _resource_provider.ResourceProvider + ) + self.assertEqual(self.resource_provider_name, resource_provider.name) + self.resource_provider = resource_provider def tearDown(self): - sot = self.conn.placement.delete_resource_provider( - self._resource_provider + result = self.conn.placement.delete_resource_provider( + self.resource_provider, ) - self.assertIsNone(sot) + self.assertIsNone(result) super().tearDown() - def test_find(self): - sot = self.conn.placement.find_resource_provider(self.NAME) - self.assertEqual(self.NAME, sot.name) + def test_resource_provider(self): + # list all resource providers + + resource_providers = list( + self.operator_cloud.placement.resource_providers() + ) + self.assertIsInstance( + resource_providers[0], + _resource_provider.ResourceProvider, + ) + self.assertIn( + self.resource_provider_name, + {x.name for x in resource_providers}, + ) + + # retrieve details of the resource provider by name - def test_get(self): - sot = self.conn.placement.get_resource_provider( - self._resource_provider.id + resource_provider = ( + self.operator_cloud.placement.find_resource_provider( + self.resource_provider.name, + ) ) - self.assertEqual(self.NAME, sot.name) + self.assertEqual(self.resource_provider_name, resource_provider.name) - def test_list(self): - names = [o.name for o in self.conn.placement.resource_providers()] - self.assertIn(self.NAME, names) + # retrieve details of the resource provider by ID + + resource_provider = ( + self.operator_cloud.placement.get_resource_provider( + self.resource_provider.id, + ) + ) + self.assertEqual(self.resource_provider_name, resource_provider.name) + + # update the resource provider + + new_resource_provider_name = self.getUniqueString() + + resource_provider = ( + self.operator_cloud.placement.update_resource_provider( + self.resource_provider, + name=new_resource_provider_name, + generation=self.resource_provider.generation, + ) + ) + self.assertIsInstance( + resource_provider, + _resource_provider.ResourceProvider, + ) + self.assertEqual( + new_resource_provider_name, + resource_provider.name, + ) From 15207b2070dacfbbb08f470d56450d3426f0695d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 22 Apr 2021 17:29:44 +0100 Subject: [PATCH 3310/3836] placement: Add support for resource provider inventories Unlike resource classes, this one is not easy. The list operation is particularly tricky: unlike most OpenStack APIs that returns a list of objects, the resource provider inventory list operation returns an object with resource class as the keys and the inventory details as the values. Put another way, we see: { "MEMORY_MB": { ... }, "VCPU": { ... }, ... } instead of the more typical: [ { "resource_class": "MEMORY_MB", ... }, { "resource_class": "VCPU", ... }, ... ] This need special handling code, namely a reimplementation of the 'list' class method. In addition, updating inventory requires providing 'resource_provider_generation'. This should match the current server value of this field, but by setting this SDK assumes the field is unchanged and doesn't include it in the list of "dirty" attributes. This requires manually adding the 'resource_provider_generation' value to the list of 'dirty' fields before the 'commit' operation. Change-Id: I5ea5d0a477147e1a4e30b428f17b44807253deaa Signed-off-by: Stephen Finucane --- doc/source/user/proxies/placement.rst | 11 + doc/source/user/resources/placement/index.rst | 1 + .../v1/resource_provider_inventory.rst | 13 ++ openstack/placement/v1/_proxy.py | 161 +++++++++++++++ .../v1/resource_provider_inventory.py | 193 ++++++++++++++++++ openstack/resource.py | 6 +- .../v1/test_resource_provider_inventory.py | 163 +++++++++++++++ .../tests/unit/placement/v1/test_proxy.py | 64 +++++- .../v1/test_resource_provider_inventory.py | 51 +++++ ...e-provider-inventory-8714cafefae74810.yaml | 4 + 10 files changed, 661 insertions(+), 6 deletions(-) create mode 100644 doc/source/user/resources/placement/v1/resource_provider_inventory.rst create mode 100644 openstack/placement/v1/resource_provider_inventory.py create mode 100644 openstack/tests/functional/placement/v1/test_resource_provider_inventory.py create mode 100644 openstack/tests/unit/placement/v1/test_resource_provider_inventory.py create mode 100644 releasenotes/notes/add-placement-resource-provider-inventory-8714cafefae74810.yaml diff --git a/doc/source/user/proxies/placement.rst b/doc/source/user/proxies/placement.rst index b9978e7d1..fe15e73a2 100644 --- a/doc/source/user/proxies/placement.rst +++ b/doc/source/user/proxies/placement.rst @@ -27,3 +27,14 @@ Resource Providers :members: create_resource_provider, update_resource_provider, delete_resource_provider, get_resource_provider, find_resource_provider, resource_providers + +Resource Provider Inventories +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.placement.v1._proxy.Proxy + :noindex: + :members: create_resource_provider_inventory, + update_resource_provider_inventory, + delete_resource_provider_inventory, + get_resource_provider_inventory, + resource_provider_inventories diff --git a/doc/source/user/resources/placement/index.rst b/doc/source/user/resources/placement/index.rst index 5d8180cd9..f4fe8dd8f 100644 --- a/doc/source/user/resources/placement/index.rst +++ b/doc/source/user/resources/placement/index.rst @@ -6,3 +6,4 @@ Placement v1 Resources v1/resource_class v1/resource_provider + v1/resource_provider_inventory diff --git a/doc/source/user/resources/placement/v1/resource_provider_inventory.rst b/doc/source/user/resources/placement/v1/resource_provider_inventory.rst new file mode 100644 index 000000000..1f3491a19 --- /dev/null +++ b/doc/source/user/resources/placement/v1/resource_provider_inventory.rst @@ -0,0 +1,13 @@ +openstack.placement.v1.resource_provider_inventory +================================================== + +.. automodule:: openstack.placement.v1.resource_provider_inventory + +The ResourceProviderInventory Class +----------------------------------- + +The ``ResourceProviderInventory`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.placement.v1.resource_provider_inventory.ResourceProviderInventory + :members: diff --git a/openstack/placement/v1/_proxy.py b/openstack/placement/v1/_proxy.py index fc6c92a44..1781b6867 100644 --- a/openstack/placement/v1/_proxy.py +++ b/openstack/placement/v1/_proxy.py @@ -12,7 +12,11 @@ from openstack.placement.v1 import resource_class as _resource_class from openstack.placement.v1 import resource_provider as _resource_provider +from openstack.placement.v1 import ( + resource_provider_inventory as _resource_provider_inventory, +) from openstack import proxy +from openstack import resource class Proxy(proxy.Proxy): @@ -204,3 +208,160 @@ def resource_providers(self, **query): :returns: A generator of resource provider instances. """ return self._list(_resource_provider.ResourceProvider, **query) + + # resource provider inventories + + def create_resource_provider_inventory( + self, + resource_provider, + resource_class, + *, + total, + **attrs, + ): + """Create a new resource provider inventory from attributes + + :param resource_provider: Either the ID of a resource provider or a + :class:`~openstack.placement.v1.resource_provider.ResourceProvider` + instance. + :param total: The actual amount of the resource that the provider can + accommodate. + :param attrs: Keyword arguments which will be used to create a + :class:`~openstack.placement.v1.resource_provider_inventory.ResourceProviderInventory`, + comprised of the properties on the ResourceProviderInventory class. + + :returns: The results of resource provider inventory creation + :rtype: :class:`~openstack.placement.v1.resource_provider_inventory.ResourceProviderInventory` + """ # noqa: E501 + resource_provider_id = resource.Resource._get_id(resource_provider) + resource_class_name = resource.Resource._get_id(resource_class) + return self._create( + _resource_provider_inventory.ResourceProviderInventory, + resource_provider_id=resource_provider_id, + resource_class=resource_class_name, + total=total, + **attrs, + ) + + def delete_resource_provider_inventory( + self, + resource_provider_inventory, + resource_provider=None, + ignore_missing=True, + ): + """Delete a resource provider inventory + + :param resource_provider_inventory: The value can be either the ID of a + resource provider or an + :class:`~openstack.placement.v1.resource_provider_inventory.ResourceProviderInventory`, + instance. + :param resource_provider: Either the ID of a resource provider or a + :class:`~openstack.placement.v1.resource_provider.ResourceProvider` + instance. This value must be specified when + ``resource_provider_inventory`` is an ID. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the resource provider inventory does not exist. When set to + ``True``, no exception will be set when attempting to delete a + nonexistent resource provider inventory. + + :returns: ``None`` + """ + resource_provider_id = self._get_uri_attribute( + resource_provider_inventory, + resource_provider, + 'resource_provider_id', + ) + self._delete( + _resource_provider_inventory.ResourceProviderInventory, + resource_provider_inventory, + resource_provider_id=resource_provider_id, + ignore_missing=ignore_missing, + ) + + def update_resource_provider_inventory( + self, + resource_provider_inventory, + resource_provider=None, + *, + resource_provider_generation=None, + **attrs, + ): + """Update a resource provider's inventory + + :param resource_provider_inventory: The value can be either the ID of a resource + provider inventory or an + :class:`~openstack.placement.v1.resource_provider_inventory.ResourceProviderInventory`, + instance. + :param resource_provider: Either the ID of a resource provider or a + :class:`~openstack.placement.v1.resource_provider.ResourceProvider` + instance. This value must be specified when + ``resource_provider_inventory`` is an ID. + :attrs kwargs: The attributes to update on the resource provider inventory + represented by ``resource_provider_inventory``. + + :returns: The updated resource provider inventory + :rtype: :class:`~openstack.placement.v1.resource_provider_inventory.ResourceProviderInventory` + """ # noqa: E501 + resource_provider_id = self._get_uri_attribute( + resource_provider_inventory, + resource_provider, + 'resource_provider_id', + ) + return self._update( + _resource_provider_inventory.ResourceProviderInventory, + resource_provider_inventory, + resource_provider_id=resource_provider_id, + resource_provider_generation=resource_provider_generation, + **attrs, + ) + + def get_resource_provider_inventory( + self, + resource_provider_inventory, + resource_provider=None, + ): + """Get a single resource_provider_inventory + + :param resource_provider_inventory: The value can be either the ID of a + resource provider inventory or an + :class:`~openstack.placement.v1.resource_provider_inventory.ResourceProviderInventory`, + instance. + :param resource_provider: Either the ID of a resource provider or a + :class:`~openstack.placement.v1.resource_provider.ResourceProvider` + instance. This value must be specified when + ``resource_provider_inventory`` is an ID. + + :returns: An instance of + :class:`~openstack.placement.v1.resource_provider_inventory.ResourceProviderInventory` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource provider inventory matching the criteria could be found. + """ + resource_provider_id = self._get_uri_attribute( + resource_provider_inventory, + resource_provider, + 'resource_provider_id', + ) + return self._get( + _resource_provider_inventory.ResourceProviderInventory, + resource_provider_inventory, + resource_provider_id=resource_provider_id, + ) + + def resource_provider_inventories(self, resource_provider, **query): + """Retrieve a generator of resource provider inventories + + :param resource_provider: Either the ID of a resource provider or a + :class:`~openstack.placement.v1.resource_provider.ResourceProvider` + instance. + :param query: Optional query parameters to be sent to limit + the resources being returned. + + :returns: A generator of resource provider inventory instances. + """ + resource_provider_id = resource.Resource._get_id(resource_provider) + return self._list( + _resource_provider_inventory.ResourceProviderInventory, + resource_provider_id=resource_provider_id, + **query, + ) diff --git a/openstack/placement/v1/resource_provider_inventory.py b/openstack/placement/v1/resource_provider_inventory.py new file mode 100644 index 000000000..016bb49f3 --- /dev/null +++ b/openstack/placement/v1/resource_provider_inventory.py @@ -0,0 +1,193 @@ +# 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 import resource + + +class ResourceProviderInventory(resource.Resource): + resource_key = None + resources_key = None + base_path = '/resource_providers/%(resource_provider_id)s/inventories' + + _query_mapping = {} + + # Capabilities + + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + # Properties + + #: The UUID of a resource provider. + resource_provider_id = resource.URI('resource_provider_id') + #: The name of the resource class. + resource_class = resource.Body('resource_class', alternate_id=True) + #: A consistent view marker that assists with the management of concurrent + #: resource provider updates. + resource_provider_generation = resource.Body( + 'resource_provider_generation', + type=int, + ) + + #: It is used in determining whether consumption of the resource of the + #: provider can exceed physical constraints. + allocation_ratio = resource.Body('allocation_ratio', type=float) + #: A maximum amount any single allocation against an inventory can have. + max_unit = resource.Body('max_unit', type=int) + #: A minimum amount any single allocation against an inventory can have. + min_unit = resource.Body('min_unit', type=int) + #: The amount of the resource a provider has reserved for its own use. + reserved = resource.Body('reserved', type=int) + #: A representation of the divisible amount of the resource that may be + #: requested. For example, step_size = 5 means that only values divisible + #: by 5 (5, 10, 15, etc.) can be requested. + step_size = resource.Body('step_size', type=int) + #: The actual amount of the resource that the provider can accommodate. + total = resource.Body('total', type=int) + + def commit( + self, + session, + prepend_key=True, + has_body=True, + retry_on_conflict=None, + base_path=None, + *, + microversion=None, + **kwargs, + ): + # resource_provider_generation must always be provided on update, but + # it will appear to be identical (by design) so we strip it. Prevent + # tihs happening. + self._body._dirty.add('resource_provider_generation') + return super().commit( + session, + prepend_key=prepend_key, + has_body=has_body, + retry_on_conflict=retry_on_conflict, + base_path=base_path, + microversion=microversion, + **kwargs, + ) + + # TODO(stephenfin): It would be nicer if we could do this in Resource + # itself since the logic is also found elsewhere (e.g. + # openstack.identity.v2.extension.Extension) but that code is a bit of a + # rat's nest right now and needs a spring clean + @classmethod + def list( + cls, + session, + paginated=True, + base_path=None, + allow_unknown_params=False, + *, + microversion=None, + **params, + ): + """This method is a generator which yields resource objects. + + A re-implementation of :meth:`~openstack.resource.Resource.list` that + handles placement's single, unpaginated list implementation. + + Refer to :meth:`~openstack.resource.Resource.list` for full + documentation including parameter, exception and return type + documentation. + """ + session = cls._get_session(session) + + if microversion is None: + microversion = cls._get_microversion(session, action='list') + + if base_path is None: + base_path = cls.base_path + + # There is no server-side filtering, only client-side + client_filters = {} + # Gather query parameters which are not supported by the server + for k, v in params.items(): + if ( + # Known attr + hasattr(cls, k) + # Is real attr property + and isinstance(getattr(cls, k), resource.Body) + # not included in the query_params + and k not in cls._query_mapping._mapping.keys() + ): + client_filters[k] = v + + uri = base_path % params + uri_params = {} + + for k, v in params.items(): + # We need to gather URI parts to set them on the resource later + if hasattr(cls, k) and isinstance(getattr(cls, k), resource.URI): + uri_params[k] = v + + def _dict_filter(f, d): + """Dict param based filtering""" + if not d: + return False + for key in f.keys(): + if isinstance(f[key], dict): + if not _dict_filter(f[key], d.get(key, None)): + return False + elif d.get(key, None) != f[key]: + return False + return True + + response = session.get( + uri, + headers={"Accept": "application/json"}, + params={}, + microversion=microversion, + ) + exceptions.raise_from_response(response) + data = response.json() + + for resource_class, resource_data in data['inventories'].items(): + resource_inventory = { + 'resource_class': resource_class, + 'resource_provider_generation': data[ + 'resource_provider_generation' + ], # noqa: E501 + **resource_data, + **uri_params, + } + value = cls.existing( + microversion=microversion, + connection=session._get_connection(), + **resource_inventory, + ) + + filters_matched = True + # Iterate over client filters and return only if matching + for key in client_filters.keys(): + if isinstance(client_filters[key], dict): + if not _dict_filter( + client_filters[key], + value.get(key, None), + ): + filters_matched = False + break + elif value.get(key, None) != client_filters[key]: + filters_matched = False + break + + if filters_matched: + yield value + + return None diff --git a/openstack/resource.py b/openstack/resource.py index bfa39fa59..104fcb36f 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -350,7 +350,7 @@ def _validate(self, query, base_path=None, allow_unknown_params=False): client-side parameter name or server side name. :param base_path: Formatted python string of the base url path for the resource. - : param allow_unknown_params: Exclude query params not known by the + :param allow_unknown_params: Exclude query params not known by the resource. :returns: Filtered collection of the supported QueryParameters @@ -2018,17 +2018,19 @@ def list( raise exceptions.MethodNotSupported(cls, 'list') session = cls._get_session(session) + if microversion is None: microversion = cls._get_microversion(session, action='list') if base_path is None: base_path = cls.base_path + api_filters = cls._query_mapping._validate( params, base_path=base_path, allow_unknown_params=True, ) - client_filters = dict() + client_filters = {} # Gather query parameters which are not supported by the server for k, v in params.items(): if ( diff --git a/openstack/tests/functional/placement/v1/test_resource_provider_inventory.py b/openstack/tests/functional/placement/v1/test_resource_provider_inventory.py new file mode 100644 index 000000000..16d4bc5fa --- /dev/null +++ b/openstack/tests/functional/placement/v1/test_resource_provider_inventory.py @@ -0,0 +1,163 @@ +# 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 uuid + +from openstack.placement.v1 import resource_class as _resource_class +from openstack.placement.v1 import resource_provider as _resource_provider +from openstack.placement.v1 import ( + resource_provider_inventory as _resource_provider_inventory, +) +from openstack.tests.functional import base + + +class TestResourceProviderInventory(base.BaseFunctionalTest): + def setUp(self): + super().setUp() + + if not self.operator_cloud.has_service('placement'): + self.skipTest('placement service not supported by cloud') + + self.resource_provider_name = self.getUniqueString() + self.resource_class_name = f'CUSTOM_{uuid.uuid4().hex.upper()}' + + resource_class = self.operator_cloud.placement.create_resource_class( + name=self.resource_class_name, + ) + self.assertIsInstance(resource_class, _resource_class.ResourceClass) + self.assertEqual(self.resource_class_name, resource_class.name) + + resource_provider = ( + self.operator_cloud.placement.create_resource_provider( + name=self.resource_provider_name, + ) + ) + self.assertIsInstance( + resource_provider, + _resource_provider.ResourceProvider, + ) + self.assertEqual(self.resource_provider_name, resource_provider.name) + + self.resource_provider = resource_provider + self.resource_class = resource_class + + def tearDown(self): + self.operator_cloud.placement.delete_resource_provider( + self.resource_provider, + ) + self.operator_cloud.placement.delete_resource_class( + self.resource_class, + ) + super().tearDown() + + def test_resource_provider_inventory(self): + # create the resource provider inventory + + resource_provider_inventory = ( + self.operator_cloud.placement.create_resource_provider_inventory( + self.resource_provider, + resource_class=self.resource_class, + total=10, + step_size=1, + ) + ) + self.assertIsInstance( + resource_provider_inventory, + _resource_provider_inventory.ResourceProviderInventory, + ) + self.assertEqual( + self.resource_class.name, + resource_provider_inventory.resource_class, + ) + self.assertEqual(10, resource_provider_inventory.total) + + # list all resource provider inventories (there should only be one) + + resource_provider_inventories = list( + self.operator_cloud.placement.resource_provider_inventories( + self.resource_provider + ) + ) + self.assertIsInstance( + resource_provider_inventories[0], + _resource_provider_inventory.ResourceProviderInventory, + ) + self.assertIn( + self.resource_class.name, + {rpi.id for rpi in resource_provider_inventories}, + ) + + # update the resource provider inventory + + resource_provider_inventory = self.operator_cloud.placement.update_resource_provider_inventory( + resource_provider_inventory, + total=20, + resource_provider_generation=resource_provider_inventory.resource_provider_generation, + ) + self.assertIsInstance( + resource_provider_inventory, + _resource_provider_inventory.ResourceProviderInventory, + ) + self.assertEqual( + self.resource_class.name, + resource_provider_inventory.id, + ) + self.assertEqual(20, resource_provider_inventory.total) + + # retrieve details of the (updated) resource provider inventory + + resource_provider_inventory = ( + self.operator_cloud.placement.get_resource_provider_inventory( + resource_provider_inventory, + ) + ) + self.assertIsInstance( + resource_provider_inventory, + _resource_provider_inventory.ResourceProviderInventory, + ) + self.assertEqual( + self.resource_class.name, + resource_provider_inventory.id, + ) + self.assertEqual(20, resource_provider_inventory.total) + + # retrieve details of the resource provider inventory using IDs + # (requires us to provide the resource provider also) + + resource_provider_inventory = ( + self.operator_cloud.placement.get_resource_provider_inventory( + resource_provider_inventory.id, + self.resource_provider, + ) + ) + self.assertIsInstance( + resource_provider_inventory, + _resource_provider_inventory.ResourceProviderInventory, + ) + self.assertEqual( + self.resource_class.name, + resource_provider_inventory.id, + ) + self.assertEqual(20, resource_provider_inventory.total) + + # (no find_resource_provider_inventory method) + + # delete the resource provider inventory + + result = ( + self.operator_cloud.placement.delete_resource_provider_inventory( + resource_provider_inventory, + self.resource_provider, + ignore_missing=False, + ) + ) + self.assertIsNone(result) diff --git a/openstack/tests/unit/placement/v1/test_proxy.py b/openstack/tests/unit/placement/v1/test_proxy.py index 2c9cdec9c..0687551ea 100644 --- a/openstack/tests/unit/placement/v1/test_proxy.py +++ b/openstack/tests/unit/placement/v1/test_proxy.py @@ -13,6 +13,7 @@ from openstack.placement.v1 import _proxy from openstack.placement.v1 import resource_class from openstack.placement.v1 import resource_provider +from openstack.placement.v1 import resource_provider_inventory from openstack.tests.unit import test_proxy_base as test_proxy_base @@ -22,8 +23,7 @@ def setUp(self): self.proxy = _proxy.Proxy(self.session) -# resource classes -class TestPlacementResourceClass: +class TestPlacementResourceClass(TestPlacementProxy): def test_resource_class_create(self): self.verify_create( self.proxy.create_resource_class, @@ -57,8 +57,7 @@ def test_resource_classes(self): ) -# resource providers -class TestPlacementResourceProvider: +class TestPlacementResourceProvider(TestPlacementProxy): def test_resource_provider_create(self): self.verify_create( self.proxy.create_resource_provider, @@ -90,3 +89,60 @@ def test_resource_providers(self): self.proxy.resource_providers, resource_provider.ResourceProvider, ) + + +class TestPlacementResourceProviderInventory(TestPlacementProxy): + def test_resource_provider_inventory_create(self): + self.verify_create( + self.proxy.create_resource_provider_inventory, + resource_provider_inventory.ResourceProviderInventory, + method_kwargs={ + 'resource_provider': 'test_id', + 'resource_class': 'CUSTOM_FOO', + 'total': 20, + }, + expected_kwargs={ + 'resource_provider_id': 'test_id', + 'resource_class': 'CUSTOM_FOO', + 'total': 20, + }, + ) + + def test_resource_provider_inventory_delete(self): + self.verify_delete( + self.proxy.delete_resource_provider_inventory, + resource_provider_inventory.ResourceProviderInventory, + ignore_missing=False, + method_kwargs={'resource_provider': 'test_id'}, + expected_kwargs={'resource_provider_id': 'test_id'}, + ) + + def test_resource_provider_inventory_update(self): + self.verify_update( + self.proxy.update_resource_provider_inventory, + resource_provider_inventory.ResourceProviderInventory, + method_kwargs={ + 'resource_provider': 'test_id', + 'resource_provider_generation': 1, + }, + expected_kwargs={ + 'resource_provider_id': 'test_id', + 'resource_provider_generation': 1, + }, + ) + + def test_resource_provider_inventory_get(self): + self.verify_get( + self.proxy.get_resource_provider_inventory, + resource_provider_inventory.ResourceProviderInventory, + method_kwargs={'resource_provider': 'test_id'}, + expected_kwargs={'resource_provider_id': 'test_id'}, + ) + + def test_resource_provider_inventories(self): + self.verify_list( + self.proxy.resource_provider_inventories, + resource_provider_inventory.ResourceProviderInventory, + method_kwargs={'resource_provider': 'test_id'}, + expected_kwargs={'resource_provider_id': 'test_id'}, + ) diff --git a/openstack/tests/unit/placement/v1/test_resource_provider_inventory.py b/openstack/tests/unit/placement/v1/test_resource_provider_inventory.py new file mode 100644 index 000000000..6f01f8e46 --- /dev/null +++ b/openstack/tests/unit/placement/v1/test_resource_provider_inventory.py @@ -0,0 +1,51 @@ +# 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.placement.v1 import resource_provider_inventory +from openstack.tests.unit import base + +FAKE = { + 'allocation_ratio': 1.0, + 'max_unit': 35, + 'min_unit': 1, + 'reserved': 0, + 'step_size': 1, + 'total': 35, +} + + +class TestResourceProviderInventory(base.TestCase): + def test_basic(self): + sot = resource_provider_inventory.ResourceProviderInventory() + self.assertIsNone(sot.resource_key) + self.assertIsNone(sot.resources_key) + self.assertEqual( + '/resource_providers/%(resource_provider_id)s/inventories', + sot.base_path, + ) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertFalse(sot.allow_patch) + + self.assertDictEqual({}, sot._query_mapping) + + def test_make_it(self): + sot = resource_provider_inventory.ResourceProviderInventory(**FAKE) + self.assertEqual(FAKE['allocation_ratio'], sot.allocation_ratio) + self.assertEqual(FAKE['max_unit'], sot.max_unit) + self.assertEqual(FAKE['min_unit'], sot.min_unit) + self.assertEqual(FAKE['reserved'], sot.reserved) + self.assertEqual(FAKE['step_size'], sot.step_size) + self.assertEqual(FAKE['total'], sot.total) diff --git a/releasenotes/notes/add-placement-resource-provider-inventory-8714cafefae74810.yaml b/releasenotes/notes/add-placement-resource-provider-inventory-8714cafefae74810.yaml new file mode 100644 index 000000000..33cabac4e --- /dev/null +++ b/releasenotes/notes/add-placement-resource-provider-inventory-8714cafefae74810.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Added support for the ``ResourceProviderInventory`` Placement resource. From ebd4d75418b35727071780060ec3d19e81a7dee2 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 11 Jul 2023 16:22:50 +0100 Subject: [PATCH 3311/3836] placement: Add support for resource provider aggregates This is exposed as methods on the 'ResourceProvider' resource since these don't make a whole lot of sense as separate resources. Change-Id: Ief0cad2900573b4a6591ab4042a55714baad085a Signed-off-by: Stephen Finucane --- doc/source/user/proxies/placement.rst | 18 ++++--- openstack/placement/v1/_proxy.py | 44 +++++++++++++++ openstack/placement/v1/resource_provider.py | 53 +++++++++++++++++++ .../placement/v1/test_resource_provider.py | 24 +++++++++ .../tests/unit/placement/v1/test_proxy.py | 17 ++++++ ...-provider-aggregates-1310c0be6a4097d3.yaml | 4 ++ 6 files changed, 152 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/add-placement-resource-provider-aggregates-1310c0be6a4097d3.yaml diff --git a/doc/source/user/proxies/placement.rst b/doc/source/user/proxies/placement.rst index fe15e73a2..b7bd15625 100644 --- a/doc/source/user/proxies/placement.rst +++ b/doc/source/user/proxies/placement.rst @@ -14,19 +14,21 @@ Resource Classes ^^^^^^^^^^^^^^^^ .. autoclass:: openstack.placement.v1._proxy.Proxy - :noindex: - :members: create_resource_class, update_resource_class, - delete_resource_class, get_resource_class, - resource_classes + :noindex: + :members: create_resource_class, update_resource_class, + delete_resource_class, get_resource_class, + resource_classes Resource Providers ^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.placement.v1._proxy.Proxy - :noindex: - :members: create_resource_provider, update_resource_provider, - delete_resource_provider, get_resource_provider, - find_resource_provider, resource_providers + :noindex: + :members: create_resource_provider, update_resource_provider, + delete_resource_provider, get_resource_provider, + find_resource_provider, resource_providers, + get_resource_provider_aggregates, + set_resource_provider_aggregates Resource Provider Inventories ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/openstack/placement/v1/_proxy.py b/openstack/placement/v1/_proxy.py index 1781b6867..d3a0b79fc 100644 --- a/openstack/placement/v1/_proxy.py +++ b/openstack/placement/v1/_proxy.py @@ -209,6 +209,50 @@ def resource_providers(self, **query): """ return self._list(_resource_provider.ResourceProvider, **query) + # resource provider aggregates + + def get_resource_provider_aggregates(self, resource_provider): + """Get a list of aggregates for a resource provider. + + :param resource_provider: The value can be either the ID of a resource + provider or an + :class:`~openstack.placement.v1.resource_provider.ResourceProvider`, + instance. + + :returns: An instance of + :class:`~openstack.placement.v1.resource_provider.ResourceProvider` + with the ``aggregates`` attribute populated. + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource provider matching the criteria could be found. + """ + res = self._get_resource( + _resource_provider.ResourceProvider, + resource_provider, + ) + return res.fetch_aggregates(self) + + def set_resource_provider_aggregates(self, resource_provider, *aggregates): + """Update aggregates for a resource provider. + + :param resource_provider: The value can be either the ID of a resource + provider or an + :class:`~openstack.placement.v1.resource_provider.ResourceProvider`, + instance. + :param aggregates: A list of aggregates. These aggregates will replace + all aggregates currently present. + + :returns: An instance of + :class:`~openstack.placement.v1.resource_provider.ResourceProvider` + with the ``aggregates`` attribute populated with the updated value. + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource provider matching the criteria could be found. + """ + res = self._get_resource( + _resource_provider.ResourceProvider, + resource_provider, + ) + return res.set_aggregates(self, aggregates=aggregates) + # resource provider inventories def create_resource_provider_inventory( diff --git a/openstack/placement/v1/resource_provider.py b/openstack/placement/v1/resource_provider.py index 1d6ba5d83..4da2d3bcc 100644 --- a/openstack/placement/v1/resource_provider.py +++ b/openstack/placement/v1/resource_provider.py @@ -10,7 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import exceptions from openstack import resource +from openstack import utils class ResourceProvider(resource.Resource): @@ -45,6 +47,8 @@ class ResourceProvider(resource.Resource): # Properties + #: Aggregates + aggregates = resource.Body('aggregates', type=list, list_type=str) #: The UUID of a resource provider. id = resource.Body('uuid', alternate_id=True) #: A consistent view marker that assists with the management of concurrent @@ -59,3 +63,52 @@ class ResourceProvider(resource.Resource): parent_provider_id = resource.Body('parent_provider_uuid') #: Read-only UUID of the top-most provider in this provider tree. root_provider_id = resource.Body('root_provider_uuid') + + def fetch_aggregates(self, session): + """List aggregates set on the resource provider + + :param session: The session to use for making this request + :return: The resource provider with aggregates populated + """ + url = utils.urljoin(self.base_path, self.id, 'aggregates') + microversion = self._get_microversion(session, action='fetch') + + response = session.get(url, microversion=microversion) + exceptions.raise_from_response(response) + data = response.json() + + updates = {'aggregates': data['aggregates']} + if utils.supports_microversion(session, '1.19'): + updates['generation'] = data['resource_provider_generation'] + self._body.attributes.update(updates) + + return self + + def set_aggregates(self, session, aggregates=None): + """Replaces aggregates on the resource provider + + :param session: The session to use for making this request + :param list aggregates: List of aggregates + :return: The resource provider with updated aggregates populated + """ + url = utils.urljoin(self.base_path, self.id, 'aggregates') + microversion = self._get_microversion(session, action='commit') + + body = { + 'aggregates': aggregates or [], + } + if utils.supports_microversion(session, '1.19'): + body['resource_provider_generation'] = self.generation + + response = session.put(url, json=body, microversion=microversion) + exceptions.raise_from_response(response) + data = response.json() + + updates = {'aggregates': data['aggregates']} + if 'resource_provider_generation' in data: + updates['resource_provider_generation'] = data[ + 'resource_provider_generation' + ] + self._body.attributes.update(updates) + + return self diff --git a/openstack/tests/functional/placement/v1/test_resource_provider.py b/openstack/tests/functional/placement/v1/test_resource_provider.py index e5db948a8..516182242 100644 --- a/openstack/tests/functional/placement/v1/test_resource_provider.py +++ b/openstack/tests/functional/placement/v1/test_resource_provider.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import uuid + from openstack.placement.v1 import resource_provider as _resource_provider from openstack.tests.functional import base @@ -93,3 +95,25 @@ def test_resource_provider(self): new_resource_provider_name, resource_provider.name, ) + + def test_resource_provider_aggregates(self): + aggregates = [uuid.uuid4().hex, uuid.uuid4().hex] + + # update the resource provider aggregates + + resource_provider = ( + self.operator_cloud.placement.set_resource_provider_aggregates( + self.resource_provider, + *aggregates, + ) + ) + self.assertCountEqual(aggregates, resource_provider.aggregates) + + # retrieve details of resource provider aggregates + + resource_provider = ( + self.operator_cloud.placement.get_resource_provider_aggregates( + self.resource_provider, + ) + ) + self.assertCountEqual(aggregates, resource_provider.aggregates) diff --git a/openstack/tests/unit/placement/v1/test_proxy.py b/openstack/tests/unit/placement/v1/test_proxy.py index 0687551ea..5dcdec11d 100644 --- a/openstack/tests/unit/placement/v1/test_proxy.py +++ b/openstack/tests/unit/placement/v1/test_proxy.py @@ -90,6 +90,23 @@ def test_resource_providers(self): resource_provider.ResourceProvider, ) + def test_resource_provider_set_aggregates(self): + self._verify( + 'openstack.placement.v1.resource_provider.ResourceProvider.set_aggregates', + self.proxy.set_resource_provider_aggregates, + method_args=['value', 'a', 'b'], + expected_args=[self.proxy], + expected_kwargs={'aggregates': ('a', 'b')}, + ) + + def test_resource_provider_get_aggregates(self): + self._verify( + 'openstack.placement.v1.resource_provider.ResourceProvider.fetch_aggregates', + self.proxy.get_resource_provider_aggregates, + method_args=['value'], + expected_args=[self.proxy], + ) + class TestPlacementResourceProviderInventory(TestPlacementProxy): def test_resource_provider_inventory_create(self): diff --git a/releasenotes/notes/add-placement-resource-provider-aggregates-1310c0be6a4097d3.yaml b/releasenotes/notes/add-placement-resource-provider-aggregates-1310c0be6a4097d3.yaml new file mode 100644 index 000000000..e8385edad --- /dev/null +++ b/releasenotes/notes/add-placement-resource-provider-aggregates-1310c0be6a4097d3.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add support for aggregates to the ``ResourceProvider`` Placement resource. From d54f4e30afda4c005af450f108ab72254894c5db Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 12 Jul 2023 12:26:54 +0100 Subject: [PATCH 3312/3836] placement: Add support for traits Another weird one. There are two oddities here. Firstly, creation of a trait is a PUT request and doesn't return a body, only a 'Location'. It also doesn't error out if the trait already exists but rather returns a HTTP 204. This is mostly handled by setting the 'create_method' attribute. Secondly, the list response returns a list of strings rather than a list of objects. This is less easily worked around and once again requires a custom implementation of the 'list' class method. We extend the 'QueryParameter' class to accept a new kwarg argument, 'include_pagination_defaults', which allows us to disable adding the default 'limit' and 'marker' query string parameters: Placement doesn't use these. Change-Id: Idafa6c5c356d215224711b73c56a87ed7a690b94 Signed-off-by: Stephen Finucane --- doc/source/user/proxies/placement.rst | 7 + doc/source/user/resources/placement/index.rst | 1 + .../user/resources/placement/v1/trait.rst | 12 ++ openstack/placement/v1/_proxy.py | 51 +++++++ openstack/placement/v1/trait.py | 142 ++++++++++++++++++ openstack/resource.py | 35 +++-- .../functional/placement/v1/test_trait.py | 61 ++++++++ .../tests/unit/placement/v1/test_trait.py | 42 ++++++ .../add-placement-trait-29957d2c03edbfb9.yaml | 4 + 9 files changed, 341 insertions(+), 14 deletions(-) create mode 100644 doc/source/user/resources/placement/v1/trait.rst create mode 100644 openstack/placement/v1/trait.py create mode 100644 openstack/tests/functional/placement/v1/test_trait.py create mode 100644 openstack/tests/unit/placement/v1/test_trait.py create mode 100644 releasenotes/notes/add-placement-trait-29957d2c03edbfb9.yaml diff --git a/doc/source/user/proxies/placement.rst b/doc/source/user/proxies/placement.rst index b7bd15625..864b41204 100644 --- a/doc/source/user/proxies/placement.rst +++ b/doc/source/user/proxies/placement.rst @@ -40,3 +40,10 @@ Resource Provider Inventories delete_resource_provider_inventory, get_resource_provider_inventory, resource_provider_inventories + +Traits +^^^^^^ + +.. autoclass:: openstack.placement.v1._proxy.Proxy + :noindex: + :members: create_trait, delete_trait, get_trait, traits diff --git a/doc/source/user/resources/placement/index.rst b/doc/source/user/resources/placement/index.rst index f4fe8dd8f..b57b79740 100644 --- a/doc/source/user/resources/placement/index.rst +++ b/doc/source/user/resources/placement/index.rst @@ -7,3 +7,4 @@ Placement v1 Resources v1/resource_class v1/resource_provider v1/resource_provider_inventory + v1/trait diff --git a/doc/source/user/resources/placement/v1/trait.rst b/doc/source/user/resources/placement/v1/trait.rst new file mode 100644 index 000000000..e9c70a887 --- /dev/null +++ b/doc/source/user/resources/placement/v1/trait.rst @@ -0,0 +1,12 @@ +openstack.placement.v1.trait +============================ + +.. automodule:: openstack.placement.v1.trait + +The Trait Class +--------------- + +The ``Trait`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.placement.v1.trait.Trait + :members: diff --git a/openstack/placement/v1/_proxy.py b/openstack/placement/v1/_proxy.py index d3a0b79fc..60768b5a5 100644 --- a/openstack/placement/v1/_proxy.py +++ b/openstack/placement/v1/_proxy.py @@ -15,6 +15,7 @@ from openstack.placement.v1 import ( resource_provider_inventory as _resource_provider_inventory, ) +from openstack.placement.v1 import trait as _trait from openstack import proxy from openstack import resource @@ -409,3 +410,53 @@ def resource_provider_inventories(self, resource_provider, **query): resource_provider_id=resource_provider_id, **query, ) + + # ========== Traits ========== + + def create_trait(self, name): + """Create a new trait + + :param name: The name of the new trait + + :returns: The results of trait creation + :rtype: :class:`~openstack.placement.v1.trait.Trait` + """ + return self._create(_trait.Trait, name=name) + + def delete_trait(self, trait, ignore_missing=True): + """Delete a trait + + :param trait: The value can be either the ID of a trait or an + :class:`~openstack.placement.v1.trait.Trait`, instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the resource provider inventory does not exist. When set to + ``True``, no exception will be set when attempting to delete a + nonexistent resource provider inventory. + + :returns: ``None`` + """ + self._delete(_trait.Trait, trait, ignore_missing=ignore_missing) + + def get_trait(self, trait): + """Get a single trait + + :param trait: The value can be either the ID of a trait or an + :class:`~openstack.placement.v1.trait.Trait`, instance. + + :returns: An instance of + :class:`~openstack.placement.v1.resource_provider_inventory.ResourceProviderInventory` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + trait matching the criteria could be found. + """ + return self._get(_trait.Trait, trait) + + def traits(self, **query): + """Retrieve a generator of traits + + :param query: Optional query parameters to be sent to limit + the resources being returned. + + :returns: A generator of trait objects + """ + return self._list(_trait.Trait, **query) diff --git a/openstack/placement/v1/trait.py b/openstack/placement/v1/trait.py new file mode 100644 index 000000000..d87c60856 --- /dev/null +++ b/openstack/placement/v1/trait.py @@ -0,0 +1,142 @@ +# 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 import resource + + +class Trait(resource.Resource): + resource_key = None + resources_key = None + base_path = '/traits' + + # Capabilities + + allow_create = True + allow_fetch = True + allow_delete = True + allow_list = True + + create_method = 'PUT' + + # Added in 1.6 + _max_microversion = '1.6' + + _query_mapping = resource.QueryParameters( + 'name', + 'associated', + include_pagination_defaults=False, + ) + + name = resource.Body('name', alternate_id=True) + + @classmethod + def list( + cls, + session, + paginated=True, + base_path=None, + allow_unknown_params=False, + *, + microversion=None, + **params, + ): + """This method is a generator which yields resource objects. + + A re-implementation of :meth:`~openstack.resource.Resource.list` that + handles the list of strings (as opposed to a list of objects) that this + call returns. + + Refer to :meth:`~openstack.resource.Resource.list` for full + documentation including parameter, exception and return type + documentation. + """ + session = cls._get_session(session) + + if microversion is None: + microversion = cls._get_microversion(session, action='list') + + if base_path is None: + base_path = cls.base_path + + # There is no server-side filtering, only client-side + client_filters = {} + # Gather query parameters which are not supported by the server + for k, v in params.items(): + if ( + # Known attr + hasattr(cls, k) + # Is real attr property + and isinstance(getattr(cls, k), resource.Body) + # not included in the query_params + and k not in cls._query_mapping._mapping.keys() + ): + client_filters[k] = v + + uri = base_path % params + uri_params = {} + + for k, v in params.items(): + # We need to gather URI parts to set them on the resource later + if hasattr(cls, k) and isinstance(getattr(cls, k), resource.URI): + uri_params[k] = v + + def _dict_filter(f, d): + """Dict param based filtering""" + if not d: + return False + for key in f.keys(): + if isinstance(f[key], dict): + if not _dict_filter(f[key], d.get(key, None)): + return False + elif d.get(key, None) != f[key]: + return False + return True + + response = session.get( + uri, + headers={"Accept": "application/json"}, + params={}, + microversion=microversion, + ) + exceptions.raise_from_response(response) + data = response.json() + + for trait_name in data['traits']: + trait = { + 'name': trait_name, + **uri_params, + } + value = cls.existing( + microversion=microversion, + connection=session._get_connection(), + **trait, + ) + + filters_matched = True + # Iterate over client filters and return only if matching + for key in client_filters.keys(): + if isinstance(client_filters[key], dict): + if not _dict_filter( + client_filters[key], + value.get(key, None), + ): + filters_matched = False + break + elif value.get(key, None) != client_filters[key]: + filters_matched = False + break + + if filters_matched: + yield value + + return None diff --git a/openstack/resource.py b/openstack/resource.py index 104fcb36f..2551dc88a 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -321,25 +321,32 @@ def __init__(self, url, body, headers): class QueryParameters: - def __init__(self, *names, **mappings): + def __init__( + self, + *names, + include_pagination_defaults=True, + **mappings, + ): """Create a dict of accepted query parameters :param names: List of strings containing client-side query parameter - names. Each name in the list maps directly to the name - expected by the server. - + names. Each name in the list maps directly to the name + expected by the server. :param mappings: Key-value pairs where the key is the client-side - name we'll accept here and the value is the name - the server expects, e.g, changes_since=changes-since. - Additionally, a value can be a dict with optional keys - name - server-side name, - type - callable to convert from client to server - representation. - - By default, both limit and marker are included in the initial mapping - as they're the most common query parameters used for listing resources. + name we'll accept here and the value is the name + the server expects, e.g, ``changes_since=changes-since``. + Additionally, a value can be a dict with optional keys: + + - ``name`` - server-side name, + - ``type`` - callable to convert from client to server + representation + :param include_pagination_defaults: If true, include default pagination + parameters, ``limit`` and ``marker``. These are the most common + query parameters used for listing resources in OpenStack APIs. """ - self._mapping = {"limit": "limit", "marker": "marker"} + self._mapping = {} + if include_pagination_defaults: + self._mapping.update({"limit": "limit", "marker": "marker"}) self._mapping.update({name: name for name in names}) self._mapping.update(mappings) diff --git a/openstack/tests/functional/placement/v1/test_trait.py b/openstack/tests/functional/placement/v1/test_trait.py new file mode 100644 index 000000000..923f88c1e --- /dev/null +++ b/openstack/tests/functional/placement/v1/test_trait.py @@ -0,0 +1,61 @@ +# 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 uuid + +from openstack.placement.v1 import trait as _trait +from openstack.tests.functional import base + + +class TestTrait(base.BaseFunctionalTest): + def setUp(self): + super().setUp() + + if not self.operator_cloud.has_service('placement'): + self.skipTest('placement service not supported by cloud') + + self.trait_name = f'CUSTOM_{uuid.uuid4().hex.upper()}' + + trait = self.operator_cloud.placement.create_trait( + name=self.trait_name, + ) + self.assertIsInstance(trait, _trait.Trait) + self.assertEqual(self.trait_name, trait.name) + + self.trait = trait + + def tearDown(self): + self.operator_cloud.placement.delete_trait(self.trait) + super().tearDown() + + def test_resource_provider_inventory(self): + # list all traits + + traits = list(self.operator_cloud.placement.traits()) + self.assertIsInstance(traits[0], _trait.Trait) + self.assertIn(self.trait.name, {x.id for x in traits}) + + # (no update_trait method) + + # retrieve details of the trait + + trait = self.operator_cloud.placement.get_trait(self.trait) + self.assertIsInstance(trait, _trait.Trait) + self.assertEqual(self.trait_name, trait.id) + + # retrieve details of the trait using IDs + + trait = self.operator_cloud.placement.get_trait(self.trait_name) + self.assertIsInstance(trait, _trait.Trait) + self.assertEqual(self.trait_name, trait.id) + + # (no find_trait method) diff --git a/openstack/tests/unit/placement/v1/test_trait.py b/openstack/tests/unit/placement/v1/test_trait.py new file mode 100644 index 000000000..777a6d7da --- /dev/null +++ b/openstack/tests/unit/placement/v1/test_trait.py @@ -0,0 +1,42 @@ +# 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.placement.v1 import trait as _trait +from openstack.tests.unit import base + +FAKE = { + 'name': 'CUSTOM_FOO', +} + + +class TestResourceClass(base.TestCase): + def test_basic(self): + sot = _trait.Trait() + self.assertEqual(None, sot.resource_key) + self.assertEqual(None, sot.resources_key) + self.assertEqual('/traits', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertFalse(sot.allow_patch) + + self.assertDictEqual( + {'name': 'name', 'associated': 'associated'}, + sot._query_mapping._mapping, + ) + + def test_make_it(self): + sot = _trait.Trait(**FAKE) + self.assertEqual(FAKE['name'], sot.id) + self.assertEqual(FAKE['name'], sot.name) diff --git a/releasenotes/notes/add-placement-trait-29957d2c03edbfb9.yaml b/releasenotes/notes/add-placement-trait-29957d2c03edbfb9.yaml new file mode 100644 index 000000000..773d77fd1 --- /dev/null +++ b/releasenotes/notes/add-placement-trait-29957d2c03edbfb9.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add support for the ``Trait`` Placement resource. From c9c212c81a430b46b7447ea9f918fedfd0d6b154 Mon Sep 17 00:00:00 2001 From: Kirill Tyugaev Date: Tue, 18 Jul 2023 12:31:06 +0300 Subject: [PATCH 3313/3836] wokflow: add update_workflow proxy method Change-Id: I939530af74174f6b0db706295a34612bb643ca38 --- doc/source/user/proxies/workflow.rst | 4 ++-- .../tests/unit/workflow/test_workflow.py | 1 + .../tests/unit/workflow/v2/test_proxy.py | 3 +++ openstack/workflow/v2/_proxy.py | 15 +++++++++++++ openstack/workflow/v2/workflow.py | 21 ++++++++++++++++--- .../update_workflow-ecdef6056ef2687b.yaml | 3 +++ 6 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/update_workflow-ecdef6056ef2687b.yaml diff --git a/doc/source/user/proxies/workflow.rst b/doc/source/user/proxies/workflow.rst index cd866e82f..f47baa55d 100644 --- a/doc/source/user/proxies/workflow.rst +++ b/doc/source/user/proxies/workflow.rst @@ -15,8 +15,8 @@ Workflow Operations .. autoclass:: openstack.workflow.v2._proxy.Proxy :noindex: - :members: create_workflow, delete_workflow, get_workflow, - find_workflow, workflows + :members: create_workflow, update_workflow, delete_workflow, + get_workflow, find_workflow, workflows Execution Operations ^^^^^^^^^^^^^^^^^^^^ diff --git a/openstack/tests/unit/workflow/test_workflow.py b/openstack/tests/unit/workflow/test_workflow.py index efab05db4..0e2f7e983 100644 --- a/openstack/tests/unit/workflow/test_workflow.py +++ b/openstack/tests/unit/workflow/test_workflow.py @@ -33,6 +33,7 @@ def test_basic(self): self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_list) self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_commit) self.assertTrue(sot.allow_delete) def test_instantiate(self): diff --git a/openstack/tests/unit/workflow/v2/test_proxy.py b/openstack/tests/unit/workflow/v2/test_proxy.py index 551b57a0c..3f8b56352 100644 --- a/openstack/tests/unit/workflow/v2/test_proxy.py +++ b/openstack/tests/unit/workflow/v2/test_proxy.py @@ -37,6 +37,9 @@ def test_execution_get(self): def test_workflow_create(self): self.verify_create(self.proxy.create_workflow, workflow.Workflow) + def test_workflow_update(self): + self.verify_update(self.proxy.update_workflow, workflow.Workflow) + def test_execution_create(self): self.verify_create(self.proxy.create_execution, execution.Execution) diff --git a/openstack/workflow/v2/_proxy.py b/openstack/workflow/v2/_proxy.py index a12552ab8..98c67b739 100644 --- a/openstack/workflow/v2/_proxy.py +++ b/openstack/workflow/v2/_proxy.py @@ -34,6 +34,21 @@ def create_workflow(self, **attrs): """ return self._create(_workflow.Workflow, **attrs) + def update_workflow(self, workflow, **attrs): + """Update workflow from attributes + + :param workflow: The value can be either the name of a workflow or a + :class:`~openstack.workflow.v2.workflow.Workflow` + instance. + :param dict attrs: Keyword arguments which will be used to update + a :class:`~openstack.workflow.v2.workflow.Workflow`, + comprised of the properties on the Workflow class. + + :returns: The results of workflow update + :rtype: :class:`~openstack.workflow.v2.workflow.Workflow` + """ + return self._update(_workflow.Workflow, workflow, **attrs) + def get_workflow(self, *attrs): """Get a workflow diff --git a/openstack/workflow/v2/workflow.py b/openstack/workflow/v2/workflow.py index ef3e6b785..e4f96f4eb 100644 --- a/openstack/workflow/v2/workflow.py +++ b/openstack/workflow/v2/workflow.py @@ -20,6 +20,7 @@ class Workflow(resource.Resource): # capabilities allow_create = True + allow_commit = True allow_list = True allow_fetch = True allow_delete = True @@ -47,7 +48,7 @@ class Workflow(resource.Resource): #: The time at which the workflow was created updated_at = resource.Body("updated_at") - def create(self, session, prepend_key=True, base_path=None): + def _request_kwargs(self, prepend_key=True, base_path=None): request = self._prepare_request( requires_id=False, prepend_key=prepend_key, base_path=base_path ) @@ -61,9 +62,23 @@ def create(self, session, prepend_key=True, base_path=None): uri = request.url + scope request.headers.update(headers) - response = session.post( - uri, json=None, headers=request.headers, **kwargs + return dict(url=uri, json=None, headers=request.headers, **kwargs) + + def create(self, session, prepend_key=True, base_path=None): + kwargs = self._request_kwargs( + prepend_key=prepend_key, base_path=base_path ) + response = session.post(**kwargs) + self._translate_response(response, has_body=False) + return self + def update(self, session, prepend_key=True, base_path=None): + kwargs = self._request_kwargs( + prepend_key=prepend_key, base_path=base_path + ) + response = session.put(**kwargs) self._translate_response(response, has_body=False) return self + + def commit(self, *args, **kwargs): + return self.update(*args, **kwargs) diff --git a/releasenotes/notes/update_workflow-ecdef6056ef2687b.yaml b/releasenotes/notes/update_workflow-ecdef6056ef2687b.yaml new file mode 100644 index 000000000..516004ff5 --- /dev/null +++ b/releasenotes/notes/update_workflow-ecdef6056ef2687b.yaml @@ -0,0 +1,3 @@ +features: + - | + Added ``update_workflow`` to the workflow proxy. From 1d43b6b13a529c435502769985b7d1f7df4f9cba Mon Sep 17 00:00:00 2001 From: Tom Weininger Date: Wed, 26 Apr 2023 14:29:54 +0200 Subject: [PATCH 3314/3836] Add Octavia support for HSTS HTTP Strict Transport Security (HSTS) support has been added to Octavia. Depends-On: https://review.opendev.org/c/openstack/octavia/+/880806 Depends-On: https://review.opendev.org/c/openstack/octavia-lib/+/880821 Partial-Bug: #2017972 Change-Id: I0c73d01360931acbb2c18822b312312c87cf4b15 --- openstack/load_balancer/v2/listener.py | 13 +++++++++++++ openstack/tests/unit/load_balancer/test_listener.py | 9 +++++++++ ...tavia-listener-hsts-fields-50c621b71e56dc13.yaml | 5 +++++ 3 files changed, 27 insertions(+) create mode 100644 releasenotes/notes/add-octavia-listener-hsts-fields-50c621b71e56dc13.yaml diff --git a/openstack/load_balancer/v2/listener.py b/openstack/load_balancer/v2/listener.py index 1b71b85b4..0d29a1528 100644 --- a/openstack/load_balancer/v2/listener.py +++ b/openstack/load_balancer/v2/listener.py @@ -49,6 +49,9 @@ class Listener(resource.Resource, tag.TagMixin): 'tls_ciphers', 'tls_versions', 'alpn_protocols', + 'hsts_max_age', + is_hsts_include_subdomains='hsts_include_subdomains', + is_hsts_preload='hsts_preload', is_admin_state_up='admin_state_up', **tag.TagMixin._tag_query_parameters ) @@ -71,6 +74,16 @@ class Listener(resource.Resource, tag.TagMixin): default_tls_container_ref = resource.Body('default_tls_container_ref') #: Description for the listener. description = resource.Body('description') + #: Defines whether the `include_subdomains` directive is used for HSTS or + #: not + is_hsts_include_subdomains = resource.Body( + 'hsts_include_subdomains', type=bool + ) + #: Enables HTTP Strict Transport Security (HSTS) and sets the `max_age` + #: directive to given value + hsts_max_age = resource.Body('hsts_max_age', type=int) + #: Defines whether the `hsts_preload` directive is used for HSTS or not + is_hsts_preload = resource.Body('hsts_preload', type=bool) #: Dictionary of additional headers insertion into HTTP header. insert_headers = resource.Body('insert_headers', type=dict) #: The administrative state of the listener, which is up diff --git a/openstack/tests/unit/load_balancer/test_listener.py b/openstack/tests/unit/load_balancer/test_listener.py index ca5da5250..284056437 100644 --- a/openstack/tests/unit/load_balancer/test_listener.py +++ b/openstack/tests/unit/load_balancer/test_listener.py @@ -40,6 +40,9 @@ 'updated_at': '2017-07-17T12:16:57.233772', 'operating_status': 'ONLINE', 'provisioning_status': 'ACTIVE', + 'hsts_include_subdomains': True, + 'hsts_max_age': 30_000_000, + 'hsts_preload': False, 'timeout_client_data': 50000, 'timeout_member_connect': 5000, 'timeout_member_data': 50000, @@ -102,6 +105,9 @@ def test_make_it(self): ) self.assertEqual(EXAMPLE['created_at'], test_listener.created_at) self.assertEqual(EXAMPLE['updated_at'], test_listener.updated_at) + self.assertTrue(test_listener.is_hsts_include_subdomains) + self.assertEqual(EXAMPLE['hsts_max_age'], test_listener.hsts_max_age) + self.assertFalse(test_listener.is_hsts_preload) self.assertEqual( EXAMPLE['provisioning_status'], test_listener.provisioning_status ) @@ -143,6 +149,9 @@ def test_make_it(self): 'operating_status': 'operating_status', 'provisioning_status': 'provisioning_status', 'is_admin_state_up': 'admin_state_up', + 'is_hsts_include_subdomains': 'hsts_include_subdomains', + 'hsts_max_age': 'hsts_max_age', + 'is_hsts_preload': 'hsts_preload', 'allowed_cidrs': 'allowed_cidrs', 'connection_limit': 'connection_limit', 'default_pool_id': 'default_pool_id', diff --git a/releasenotes/notes/add-octavia-listener-hsts-fields-50c621b71e56dc13.yaml b/releasenotes/notes/add-octavia-listener-hsts-fields-50c621b71e56dc13.yaml new file mode 100644 index 000000000..b97968a86 --- /dev/null +++ b/releasenotes/notes/add-octavia-listener-hsts-fields-50c621b71e56dc13.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added new fields to loadbalancer create/update listener API in order to + support new HTTP Strict Transport Security support. From 3d45785cb079141b6a974842f2db3183c4bff2a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Ribaud?= Date: Wed, 19 Jul 2023 09:56:18 +0200 Subject: [PATCH 3315/3836] Define version for export_locations Not specifying a microversion when using the Python command line interpreter with the SDK results in it not functioning properly. The SDK raises an error stating that the URL cannot be found. However, when a microversion is explicitly defined, the SDK functions correctly without any additional configuration. Change-Id: I249b293b11a0e57ddf685de284681f060a334c1c --- openstack/shared_file_system/v2/share_export_locations.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openstack/shared_file_system/v2/share_export_locations.py b/openstack/shared_file_system/v2/share_export_locations.py index f18bf49f0..ef8165a00 100644 --- a/openstack/shared_file_system/v2/share_export_locations.py +++ b/openstack/shared_file_system/v2/share_export_locations.py @@ -27,6 +27,9 @@ class ShareExportLocation(resource.Resource): allow_delete = False allow_head = False + _max_microversion = '2.47' + _min_microversion = '2.9' + #: Properties # The share ID, part of the URI for export locations share_id = resource.URI("share_id", type='str') From b02c96974edc3bdf91b4b11afc689569562b6c78 Mon Sep 17 00:00:00 2001 From: Slawek Kaplonski Date: Thu, 29 Jun 2023 12:02:24 +0200 Subject: [PATCH 3316/3836] Add support for default security group rules in SDK Neutron introduced new API for "default security group rules" which are kind of template of the SG rules which are next added to each newly created SG. This patch adds support for that new API in SDK. Depends-On: https://review.opendev.org/c/openstack/neutron/+/884474 Related-bug: #1983053 Change-Id: I82f905590bc4341c79d6aed6db0064763fd7cb0a --- doc/source/user/proxies/network.rst | 9 ++ openstack/network/v2/_proxy.py | 114 ++++++++++++++++++ .../network/v2/default_security_group_rule.py | 89 ++++++++++++++ .../v2/test_default_security_group_rule.py | 83 +++++++++++++ .../v2/test_default_security_group_rule.py | 85 +++++++++++++ 5 files changed, 380 insertions(+) create mode 100644 openstack/network/v2/default_security_group_rule.py create mode 100644 openstack/tests/functional/network/v2/test_default_security_group_rule.py create mode 100644 openstack/tests/unit/network/v2/test_default_security_group_rule.py diff --git a/doc/source/user/proxies/network.rst b/doc/source/user/proxies/network.rst index 4ce20b92b..7eabad4d0 100644 --- a/doc/source/user/proxies/network.rst +++ b/doc/source/user/proxies/network.rst @@ -71,6 +71,15 @@ Auto Allocated Topology Operations :members: delete_auto_allocated_topology, get_auto_allocated_topology, validate_auto_allocated_topology +Default Security Group Rules Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + :noindex: + :members: create_default_security_group_rule, + find_default_security_group_rule, get_default_security_group_rule, + delete_default_security_group_rule, default_security_group_rules + Security Group Operations ^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index d891cf7d8..496c00cdb 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -34,6 +34,9 @@ from openstack.network.v2 import ( bgpvpn_router_association as _bgpvpn_router_association, ) +from openstack.network.v2 import ( + default_security_group_rule as _default_security_group_rule, +) from openstack.network.v2 import extension from openstack.network.v2 import firewall_group as _firewall_group from openstack.network.v2 import firewall_policy as _firewall_policy @@ -118,6 +121,9 @@ class Proxy(proxy.Proxy, Generic[T]): "bgpvpn_router_association": ( _bgpvpn_router_association.BgpVpnRouterAssociation ), + "default_security_group_rule": ( + _default_security_group_rule.DefaultSecurityGroupRule + ), "extension": extension.Extension, "firewall_group": _firewall_group.FirewallGroup, "firewall_policy": _firewall_policy.FirewallPolicy, @@ -4834,6 +4840,114 @@ def security_group_rules(self, **query): """ return self._list(_security_group_rule.SecurityGroupRule, **query) + def create_default_security_group_rule(self, **attrs): + """Create a new default security group rule from attributes + + :param attrs: Keyword arguments which will be used to create a + :class:`~openstack.network.v2.default_security_group_rule. + DefaultSecurityGroupRule`, + comprised of the properties on the DefaultSecurityGroupRule class. + + :returns: The results of default security group rule creation + :rtype: + :class:`~openstack.network.v2.default_security_group_rule. + DefaultSecurityGroupRule` + """ + return self._create( + _default_security_group_rule.DefaultSecurityGroupRule, **attrs + ) + + def delete_default_security_group_rule( + self, + default_security_group_rule, + ignore_missing=True, + ): + """Delete a default security group rule + + :param default_security_group_rule: + The value can be either the ID of a default security group rule + or a + :class:`~openstack.network.v2.default_security_group_rule. + DefaultSecurityGroupRule` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the defaul security group rule does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent default security group rule. + + :returns: ``None`` + """ + self._delete( + _default_security_group_rule.DefaultSecurityGroupRule, + default_security_group_rule, + ignore_missing=ignore_missing, + ) + + def find_default_security_group_rule( + self, name_or_id, ignore_missing=True, **query + ): + """Find a single default security group rule + + :param str name_or_id: The ID of a default security group rule. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict query: Any additional parameters to be passed into + underlying methods. such as query filters. + :returns: One + :class:`~openstack.network.v2.default_security_group_rule. + DefaultSecurityGroupRule` or None + """ + return self._find( + _default_security_group_rule.DefaultSecurityGroupRule, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) + + def get_default_security_group_rule(self, default_security_group_rule): + """Get a single default security group rule + + :param default_security_group_rule: + The value can be the ID of a default security group rule or a + :class:`~openstack.network.v2.default_security_group_rule. + DefaultSecurityGroupRule` instance. + + :returns: + :class:`~openstack.network.v2.default_security_group_rule. + DefaultSecurityGroupRule` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get( + _default_security_group_rule.DefaultSecurityGroupRule, + default_security_group_rule, + ) + + def default_security_group_rules(self, **query): + """Return a generator of default security group rules + + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. Available parameters include: + + * ``description``: The default security group rule description + * ``direction``: Default security group rule direction + * ``ether_type``: Must be IPv4 or IPv6, and addresses represented + in CIDR must match the ingress or egress rule. + * ``protocol``: Default security group rule protocol + * ``remote_group_id``: ID of a remote security group + + :returns: A generator of default security group rule objects + :rtype: + :class:`~openstack.network.v2.default_security_group_rule. + DefaultSecurityGroupRule` + """ + return self._list( + _default_security_group_rule.DefaultSecurityGroupRule, **query + ) + def create_segment(self, **attrs): """Create a new segment from attributes diff --git a/openstack/network/v2/default_security_group_rule.py b/openstack/network/v2/default_security_group_rule.py new file mode 100644 index 000000000..0d40649ac --- /dev/null +++ b/openstack/network/v2/default_security_group_rule.py @@ -0,0 +1,89 @@ +# 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.network.v2 import _base +from openstack import resource + + +class DefaultSecurityGroupRule(_base.NetworkResource): + resource_key = 'default_security_group_rule' + resources_key = 'default_security_group_rules' + base_path = '/default-security-group-rules' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = False + allow_delete = True + allow_list = True + + _query_mapping = resource.QueryParameters( + 'id', + 'description', + 'remote_group_id', + 'remote_address_group_id', + 'direction', + 'protocol', + 'port_range_min', + 'port_range_max', + 'remote_ip_prefix', + 'used_in_default_sg', + 'used_in_non_default_sg', + 'sort_dir', + 'sort_key', + ether_type='ethertype', + ) + + # Properties + #: The default security group rule description. + description = resource.Body('description') + #: The remote security group ID to be associated with this security + #: group rule created from this template. + #: You can specify either ``remote_group_id`` or #: + #: ``remote_address_group_id`` or ``remote_ip_prefix``. + remote_group_id = resource.Body('remote_group_id') + #: The remote address group ID to be associated with this security + #: group rule created from that template. + #: You can specify either ``remote_group_id`` or + #: ``remote_address_group_id`` or ``remote_ip_prefix``. + remote_address_group_id = resource.Body('remote_address_group_id') + #: ``ingress`` or ``egress``: The direction in which the security group #: + #: rule will be applied. See 'direction' field in the security group rule + #: API. + direction = resource.Body('direction') + #: The protocol that is matched by the security group rule. + #: Valid values are ``null``, ``tcp``, ``udp``, and ``icmp``. + protocol = resource.Body('protocol') + #: The minimum port number in the range that is matched by the + #: security group rule. If the protocol is TCP or UDP, this value + #: must be less than or equal to the value of the port_range_max + #: attribute. If the protocol is ICMP, this value must be an ICMP type. + port_range_min = resource.Body('port_range_min', type=int) + #: The maximum port number in the range that is matched by the + #: security group rule. The port_range_min attribute constrains + #: the port_range_max attribute. If the protocol is ICMP, this + #: value must be an ICMP type. + port_range_max = resource.Body('port_range_max', type=int) + #: The remote IP prefix to be associated with this security group rule. + #: You can specify either ``remote_group_id`` or + #: ``remote_address_group_id`` or ``remote_ip_prefix``. + #: This attribute matches the specified IP prefix as the source or + #: destination IP address of the IP packet depending on direction. + remote_ip_prefix = resource.Body('remote_ip_prefix') + #: Must be IPv4 or IPv6, and addresses represented in CIDR must match + #: the ingress or egress rules. + ether_type = resource.Body('ethertype') + #: Indicate if this template be used to create security group rules in the + #: default security group created automatically for each project. + used_in_default_sg = resource.Body('used_in_default_sg', type=bool) + #: Indicate if this template be used to create security group rules in the + #: custom security groups created in the project by users. + used_in_non_default_sg = resource.Body('used_in_non_default_sg', type=bool) diff --git a/openstack/tests/functional/network/v2/test_default_security_group_rule.py b/openstack/tests/functional/network/v2/test_default_security_group_rule.py new file mode 100644 index 000000000..11bf5cf50 --- /dev/null +++ b/openstack/tests/functional/network/v2/test_default_security_group_rule.py @@ -0,0 +1,83 @@ +# 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 random + +from openstack.network.v2 import default_security_group_rule +from openstack.tests.functional import base + + +class TestDefaultSecurityGroupRule(base.BaseFunctionalTest): + def setUp(self): + super().setUp() + if not self.user_cloud._has_neutron_extension( + "security-groups-default-rules" + ): + self.skipTest( + "Neutron security-groups-default-rules extension " + "is required for this test" + ) + + self.IPV4 = random.choice(["IPv4", "IPv6"]) + self.PROTO = random.choice(["tcp", "udp"]) + self.PORT = random.randint(1, 65535) + self.DIR = random.choice(["ingress", "egress"]) + self.USED_IN_DEFAULT_SG = random.choice([True, False]) + self.USED_IN_NON_DEFAULT_SG = random.choice([True, False]) + + rul = self.operator_cloud.network.create_default_security_group_rule( + direction=self.DIR, + ethertype=self.IPV4, + port_range_max=self.PORT, + port_range_min=self.PORT, + protocol=self.PROTO, + used_in_default_sg=self.USED_IN_DEFAULT_SG, + used_in_non_default_sg=self.USED_IN_NON_DEFAULT_SG, + ) + assert isinstance( + rul, default_security_group_rule.DefaultSecurityGroupRule + ) + self.RULE_ID = rul.id + + def tearDown(self): + sot = self.operator_cloud.network.delete_default_security_group_rule( + self.RULE_ID, ignore_missing=False + ) + self.assertIsNone(sot) + super(TestDefaultSecurityGroupRule, self).tearDown() + + def test_find(self): + sot = self.operator_cloud.network.find_default_security_group_rule( + self.RULE_ID + ) + self.assertEqual(self.RULE_ID, sot.id) + + def test_get(self): + sot = self.operator_cloud.network.get_default_security_group_rule( + self.RULE_ID + ) + self.assertEqual(self.RULE_ID, sot.id) + self.assertEqual(self.DIR, sot.direction) + self.assertEqual(self.PROTO, sot.protocol) + self.assertEqual(self.PORT, sot.port_range_min) + self.assertEqual(self.PORT, sot.port_range_max) + self.assertEqual(self.USED_IN_DEFAULT_SG, sot.used_in_default_sg) + self.assertEqual( + self.USED_IN_NON_DEFAULT_SG, sot.used_in_non_default_sg + ) + + def test_list(self): + ids = [ + o.id + for o in self.operator_cloud.network.default_security_group_rules() + ] + self.assertIn(self.RULE_ID, ids) diff --git a/openstack/tests/unit/network/v2/test_default_security_group_rule.py b/openstack/tests/unit/network/v2/test_default_security_group_rule.py new file mode 100644 index 000000000..f4b0e27d7 --- /dev/null +++ b/openstack/tests/unit/network/v2/test_default_security_group_rule.py @@ -0,0 +1,85 @@ +# 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.network.v2 import default_security_group_rule +from openstack.tests.unit import base + + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'description': '1', + 'direction': '2', + 'ethertype': '3', + 'id': IDENTIFIER, + 'port_range_max': 4, + 'port_range_min': 5, + 'protocol': '6', + 'remote_group_id': '7', + 'remote_ip_prefix': '8', + 'remote_address_group_id': '13', + 'used_in_default_sg': True, + 'used_in_non_default_sg': True, +} + + +class TestDefaultSecurityGroupRule(base.TestCase): + def test_basic(self): + sot = default_security_group_rule.DefaultSecurityGroupRule() + self.assertEqual('default_security_group_rule', sot.resource_key) + self.assertEqual('default_security_group_rules', sot.resources_key) + self.assertEqual('/default-security-group-rules', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + self.assertDictEqual( + { + 'description': 'description', + 'direction': 'direction', + 'id': 'id', + 'ether_type': 'ethertype', + 'limit': 'limit', + 'marker': 'marker', + 'port_range_max': 'port_range_max', + 'port_range_min': 'port_range_min', + 'protocol': 'protocol', + 'remote_group_id': 'remote_group_id', + 'remote_address_group_id': 'remote_address_group_id', + 'remote_ip_prefix': 'remote_ip_prefix', + 'sort_dir': 'sort_dir', + 'sort_key': 'sort_key', + 'used_in_default_sg': 'used_in_default_sg', + 'used_in_non_default_sg': 'used_in_non_default_sg', + }, + sot._query_mapping._mapping, + ) + + def test_make_it(self): + sot = default_security_group_rule.DefaultSecurityGroupRule(**EXAMPLE) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['direction'], sot.direction) + self.assertEqual(EXAMPLE['ethertype'], sot.ether_type) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['port_range_max'], sot.port_range_max) + self.assertEqual(EXAMPLE['port_range_min'], sot.port_range_min) + self.assertEqual(EXAMPLE['protocol'], sot.protocol) + self.assertEqual(EXAMPLE['remote_group_id'], sot.remote_group_id) + self.assertEqual( + EXAMPLE['remote_address_group_id'], sot.remote_address_group_id + ) + self.assertEqual(EXAMPLE['remote_ip_prefix'], sot.remote_ip_prefix) + self.assertEqual(EXAMPLE['used_in_default_sg'], sot.used_in_default_sg) + self.assertEqual( + EXAMPLE['used_in_non_default_sg'], sot.used_in_non_default_sg + ) From 423d8ceb036d797f8bd74cb3b2ba9998d81622c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Ribaud?= Date: Fri, 21 Jul 2023 17:10:24 +0200 Subject: [PATCH 3317/3836] Define version for share_access_rules This is a similar issue as I249b293b11a0e57ddf685de284681f060a334c1c but with share_access_rules. Not specifying a microversion when using the Python command line interpreter with the SDK results in it not functioning properly. The SDK raises an error stating that the URL cannot be found. However, when a microversion is explicitly defined, the SDK functions correctly without any additional configuration. Change-Id: I6bbad8a74dba781115487882e7a72adf7f394f2f --- openstack/shared_file_system/v2/share_access_rule.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openstack/shared_file_system/v2/share_access_rule.py b/openstack/shared_file_system/v2/share_access_rule.py index 48138c373..75459806b 100644 --- a/openstack/shared_file_system/v2/share_access_rule.py +++ b/openstack/shared_file_system/v2/share_access_rule.py @@ -30,6 +30,8 @@ class ShareAccessRule(resource.Resource): _query_mapping = resource.QueryParameters("share_id") + _max_microversion = '2.45' + #: Properties #: The access credential of the entity granted share access. access_key = resource.Body("access_key", type=str) From d63ce704847bca526210c8c3324a8df557236358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Ribaud?= Date: Tue, 25 Jul 2023 11:11:34 +0200 Subject: [PATCH 3318/3836] Remove _min_microversion for consistency This is a minor update for change I249b293b11a0e57ddf685de284681f060a334c1c in order to keep consistency. Change-Id: I3bfe3efb8260c08f5f81cd0487afefe0dd439c94 --- openstack/shared_file_system/v2/share_export_locations.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openstack/shared_file_system/v2/share_export_locations.py b/openstack/shared_file_system/v2/share_export_locations.py index ef8165a00..9e83b7cc7 100644 --- a/openstack/shared_file_system/v2/share_export_locations.py +++ b/openstack/shared_file_system/v2/share_export_locations.py @@ -28,7 +28,6 @@ class ShareExportLocation(resource.Resource): allow_head = False _max_microversion = '2.47' - _min_microversion = '2.9' #: Properties # The share ID, part of the URI for export locations From 7bedf9fc4f1f614ad7e4f14277fc4afd92ee9c49 Mon Sep 17 00:00:00 2001 From: elajkat Date: Fri, 23 Jun 2023 13:12:00 +0200 Subject: [PATCH 3319/3836] VPN: add missing fields to VpnIpsecPolicy encapsulation_mode and transform_protocol are missing from the VpnIpsecPolicy class. Change-Id: I94b33dc387cd939156cccff4a9ef20aa0760fac8 Related-Bug: #1999774 --- openstack/network/v2/vpn_ipsec_policy.py | 6 ++++++ openstack/tests/unit/network/v2/test_vpn_ipsecpolicy.py | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/openstack/network/v2/vpn_ipsec_policy.py b/openstack/network/v2/vpn_ipsec_policy.py index 9c9e09ec9..bc171e267 100644 --- a/openstack/network/v2/vpn_ipsec_policy.py +++ b/openstack/network/v2/vpn_ipsec_policy.py @@ -28,11 +28,13 @@ class VpnIpsecPolicy(resource.Resource): _query_mapping = resource.QueryParameters( 'auth_algorithm', 'description', + 'encapsulation_mode', 'encryption_algorithm', 'name', 'pfs', 'project_id', 'phase1_negotiation_mode', + 'transform_protocol', ) # Properties @@ -42,6 +44,8 @@ class VpnIpsecPolicy(resource.Resource): #: A human-readable description for the resource. # Default is an empty string. description = resource.Body('description') + #: The encapsulation mode. A valid value is tunnel or transport + encapsulation_mode = resource.Body('encapsulation_mode') #: The encryption algorithm. A valid value is 3des, aes-128, # aes-192, aes-256, and so on. Default is aes-128. encryption_algorithm = resource.Body('encryption_algorithm') @@ -59,6 +63,8 @@ class VpnIpsecPolicy(resource.Resource): project_id = resource.Body('project_id') #: The IKE mode. A valid value is main, which is the default. phase1_negotiation_mode = resource.Body('phase1_negotiation_mode') + #: The transform protocol. A valid value is ESP, AH, or AH- ESP. + transform_protocol = resource.Body('transform_protocol') #: The units for the lifetime of the security association. # The lifetime consists of a unit and integer value. # You can omit either the unit or value portion of the lifetime. diff --git a/openstack/tests/unit/network/v2/test_vpn_ipsecpolicy.py b/openstack/tests/unit/network/v2/test_vpn_ipsecpolicy.py index 0d945d085..dc17cb3b9 100644 --- a/openstack/tests/unit/network/v2/test_vpn_ipsecpolicy.py +++ b/openstack/tests/unit/network/v2/test_vpn_ipsecpolicy.py @@ -17,11 +17,13 @@ EXAMPLE = { "auth_algorithm": "1", "description": "2", + "encapsulation_mode": "tunnel", "encryption_algorithm": "3", "lifetime": {'a': 5}, "name": "5", "pfs": "6", "project_id": "7", + "transform_protocol": "ESP", "units": "9", "value": 10, } @@ -43,6 +45,7 @@ def test_make_it(self): sot = vpn_ipsec_policy.VpnIpsecPolicy(**EXAMPLE) self.assertEqual(EXAMPLE['auth_algorithm'], sot.auth_algorithm) self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['encapsulation_mode'], sot.encapsulation_mode) self.assertEqual( EXAMPLE['encryption_algorithm'], sot.encryption_algorithm ) @@ -50,6 +53,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['pfs'], sot.pfs) self.assertEqual(EXAMPLE['project_id'], sot.project_id) + self.assertEqual(EXAMPLE['transform_protocol'], sot.transform_protocol) self.assertEqual(EXAMPLE['units'], sot.units) self.assertEqual(EXAMPLE['value'], sot.value) @@ -59,11 +63,13 @@ def test_make_it(self): "marker": "marker", 'auth_algorithm': 'auth_algorithm', 'description': 'description', + 'encapsulation_mode': 'encapsulation_mode', 'encryption_algorithm': 'encryption_algorithm', 'name': 'name', 'pfs': 'pfs', 'project_id': 'project_id', 'phase1_negotiation_mode': 'phase1_negotiation_mode', + 'transform_protocol': 'transform_protocol', }, sot._query_mapping._mapping, ) From f5af5f5e35359feda3f7a3057dd890c2d4e220c8 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 2 Jun 2023 13:58:10 +0100 Subject: [PATCH 3320/3836] Encode type of 'links' for Extension objects Change-Id: Iefe15202cc4f386284c07b7500cf0713e71542f1 Signed-off-by: Stephen Finucane --- openstack/compute/v2/extension.py | 3 +-- openstack/identity/v2/extension.py | 2 +- openstack/network/v2/extension.py | 2 +- openstack/tests/unit/compute/v2/test_extension.py | 2 +- openstack/tests/unit/identity/v2/test_extension.py | 2 +- openstack/tests/unit/network/v2/test_extension.py | 2 +- 6 files changed, 6 insertions(+), 7 deletions(-) diff --git a/openstack/compute/v2/extension.py b/openstack/compute/v2/extension.py index 4b19b0934..c4da9681b 100644 --- a/openstack/compute/v2/extension.py +++ b/openstack/compute/v2/extension.py @@ -17,7 +17,6 @@ class Extension(resource.Resource): resource_key = 'extension' resources_key = 'extensions' base_path = '/extensions' - id_attribute = "alias" # capabilities allow_fetch = True @@ -30,7 +29,7 @@ class Extension(resource.Resource): description = resource.Body('description') #: Links pertaining to this extension. This is a list of dictionaries, #: each including keys ``href`` and ``rel``. - links = resource.Body('links') + links = resource.Body('links', type=list, list_type=dict) #: The name of the extension. name = resource.Body('name') #: A URL pointing to the namespace for this extension. diff --git a/openstack/identity/v2/extension.py b/openstack/identity/v2/extension.py index e5343b2b4..7e47bf610 100644 --- a/openstack/identity/v2/extension.py +++ b/openstack/identity/v2/extension.py @@ -31,7 +31,7 @@ class Extension(resource.Resource): #: A description of the extension. *Type: string* description = resource.Body('description') #: Links to the documentation in various format. *Type: string* - links = resource.Body('links') + links = resource.Body('links', type=list, list_type=dict) #: The name of the extension. *Type: string* name = resource.Body('name') #: The second unique identifier of the extension after the alias. diff --git a/openstack/network/v2/extension.py b/openstack/network/v2/extension.py index 6bb86fa16..2e6736e5f 100644 --- a/openstack/network/v2/extension.py +++ b/openstack/network/v2/extension.py @@ -32,7 +32,7 @@ class Extension(resource.Resource): #: Text describing what the extension does. description = resource.Body('description') #: Links pertaining to this extension. - links = resource.Body('links') + links = resource.Body('links', type=list, list_type=dict) #: The name of this extension. name = resource.Body('name') #: Timestamp when the extension was last updated. diff --git a/openstack/tests/unit/compute/v2/test_extension.py b/openstack/tests/unit/compute/v2/test_extension.py index ddf54bd9e..421cbd3af 100644 --- a/openstack/tests/unit/compute/v2/test_extension.py +++ b/openstack/tests/unit/compute/v2/test_extension.py @@ -18,7 +18,7 @@ EXAMPLE = { 'alias': '1', 'description': '2', - 'links': '3', + 'links': [], 'name': '4', 'namespace': '5', 'updated': '2015-03-09T12:14:57.233772', diff --git a/openstack/tests/unit/identity/v2/test_extension.py b/openstack/tests/unit/identity/v2/test_extension.py index 912e79b59..b6df19f9e 100644 --- a/openstack/tests/unit/identity/v2/test_extension.py +++ b/openstack/tests/unit/identity/v2/test_extension.py @@ -19,7 +19,7 @@ EXAMPLE = { 'alias': '1', 'description': '2', - 'links': '3', + 'links': [], 'name': '4', 'namespace': '5', 'updated': '2015-03-09T12:14:57.233772', diff --git a/openstack/tests/unit/network/v2/test_extension.py b/openstack/tests/unit/network/v2/test_extension.py index 6a6e6e32e..1f0d1468f 100644 --- a/openstack/tests/unit/network/v2/test_extension.py +++ b/openstack/tests/unit/network/v2/test_extension.py @@ -18,7 +18,7 @@ EXAMPLE = { 'alias': '1', 'description': '2', - 'links': '3', + 'links': [], 'name': '4', 'updated': '2016-03-09T12:14:57.233772', } From dceca6bb43dd69e8e87a32ad3b126504dd657711 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 2 Jun 2023 14:08:55 +0100 Subject: [PATCH 3321/3836] volume: Add Extension to volume v2 API Makes our lives easier for migrating OSC, since volume v2 is still a thing is some areas. We reorder some methods in the v2 proxy, while we're here. Change-Id: I1e4528f5a5d8e2caaaf204792ddcee7267e4c2e9 Signed-off-by: Stephen Finucane --- openstack/block_storage/v2/_proxy.py | 149 ++++++++++-------- openstack/block_storage/v2/extension.py | 36 +++++ .../unit/block_storage/v2/test_extension.py | 45 ++++++ 3 files changed, 162 insertions(+), 68 deletions(-) create mode 100644 openstack/block_storage/v2/extension.py create mode 100644 openstack/tests/unit/block_storage/v2/test_extension.py diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 1957b4cf3..fcf45d820 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -12,6 +12,7 @@ from openstack.block_storage import _base_proxy from openstack.block_storage.v2 import backup as _backup +from openstack.block_storage.v2 import extension as _extension from openstack.block_storage.v2 import quota_set as _quota_set from openstack.block_storage.v2 import snapshot as _snapshot from openstack.block_storage.v2 import stats as _stats @@ -476,7 +477,6 @@ def complete_volume_migration(self, volume, new_volume, error=False): volume.complete_migration(self, new_volume, error) # ====== BACKEND POOLS ====== - def backend_pools(self, **query): """Returns a generator of cinder Back-end storage pools @@ -609,73 +609,7 @@ def reset_backup(self, backup, status): backup = self._get_resource(_backup.Backup, backup) backup.reset(self, status) - def wait_for_status( - self, - res, - status='available', - failures=None, - interval=2, - wait=120, - callback=None, - ): - """Wait for a resource to be in a particular status. - - :param res: The resource to wait on to reach the specified status. - The resource must have a ``status`` attribute. - :type resource: A :class:`~openstack.resource.Resource` object. - :param status: Desired status. - :param failures: Statuses that would be interpreted as failures. - :type failures: :py:class:`list` - :param interval: Number of seconds to wait before to consecutive - checks. Default to 2. - :param wait: Maximum number of seconds to wait before the change. - Default to 120. - :param callback: A callback function. This will be called with a single - value, progress. - - :returns: The resource is returned on success. - :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition - to the desired status failed to occur in specified seconds. - :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource - has transited to one of the failure statuses. - :raises: :class:`~AttributeError` if the resource does not have a - ``status`` attribute. - """ - failures = ['error'] if failures is None else failures - return resource.wait_for_status( - self, - res, - status, - failures, - interval, - wait, - callback=callback, - ) - - def wait_for_delete(self, res, interval=2, wait=120, callback=None): - """Wait for a resource to be deleted. - - :param res: The resource to wait on to be deleted. - :type resource: A :class:`~openstack.resource.Resource` object. - :param interval: Number of seconds to wait before to consecutive - checks. Default to 2. - :param wait: Maximum number of seconds to wait before the change. - Default to 120. - :param callback: A callback function. This will be called with a single - value, progress. - - :returns: The resource is returned on success. - :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition - to delete failed to occur in the specified seconds. - """ - return resource.wait_for_delete( - self, - res, - interval, - wait, - callback=callback, - ) - + # ====== QUOTA SETS ====== def get_quota_set(self, project, usage=False, **query): """Show QuotaSet information for the project @@ -749,6 +683,7 @@ def update_quota_set(self, quota_set, query=None, **attrs): query = {} return res.commit(self, **query) + # ====== VOLUME METADATA ====== def get_volume_metadata(self, volume): """Return a dictionary of metadata for a volume @@ -795,6 +730,7 @@ def delete_volume_metadata(self, volume, keys=None): else: volume.delete_metadata(self) + # ====== SNAPSHOT METADATA ====== def get_snapshot_metadata(self, snapshot): """Return a dictionary of metadata for a snapshot @@ -842,3 +778,80 @@ def delete_snapshot_metadata(self, snapshot, keys=None): snapshot.delete_metadata_item(self, key) else: snapshot.delete_metadata(self) + + # ====== EXTENSIONS ====== + def extensions(self): + """Return a generator of extensions + + :returns: A generator of extension + :rtype: :class:`~openstack.block_storage.v3.extension.Extension` + """ + return self._list(_extension.Extension) + + # ====== UTILS ====== + def wait_for_status( + self, + res, + status='available', + failures=None, + interval=2, + wait=120, + callback=None, + ): + """Wait for a resource to be in a particular status. + + :param res: The resource to wait on to reach the specified status. + The resource must have a ``status`` attribute. + :type resource: A :class:`~openstack.resource.Resource` object. + :param status: Desired status. + :param failures: Statuses that would be interpreted as failures. + :type failures: :py:class:`list` + :param interval: Number of seconds to wait before to consecutive + checks. Default to 2. + :param wait: Maximum number of seconds to wait before the change. + Default to 120. + :param callback: A callback function. This will be called with a single + value, progress. + + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to the desired status failed to occur in specified seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + has transited to one of the failure statuses. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute. + """ + failures = ['error'] if failures is None else failures + return resource.wait_for_status( + self, + res, + status, + failures, + interval, + wait, + callback=callback, + ) + + def wait_for_delete(self, res, interval=2, wait=120, callback=None): + """Wait for a resource to be deleted. + + :param res: The resource to wait on to be deleted. + :type resource: A :class:`~openstack.resource.Resource` object. + :param interval: Number of seconds to wait before to consecutive + checks. Default to 2. + :param wait: Maximum number of seconds to wait before the change. + Default to 120. + :param callback: A callback function. This will be called with a single + value, progress. + + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete( + self, + res, + interval, + wait, + callback=callback, + ) diff --git a/openstack/block_storage/v2/extension.py b/openstack/block_storage/v2/extension.py new file mode 100644 index 000000000..98a3093fc --- /dev/null +++ b/openstack/block_storage/v2/extension.py @@ -0,0 +1,36 @@ +# 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 resource + + +class Extension(resource.Resource): + resources_key = "extensions" + base_path = "/extensions" + + # Capabilities + allow_list = True + + #: Properties + #: The alias for the extension. + alias = resource.Body('alias', type=str) + #: The extension description. + description = resource.Body('description', type=str) + #: Links pertaining to this extension. + links = resource.Body('links', type=list) + #: The name of this extension. + name = resource.Body('name') + #: A URL pointing to the namespace for this extension. + namespace = resource.Body('namespace') + #: The date and time when the resource was updated. + #: The date and time stamp format is ISO 8601. + updated_at = resource.Body('updated', type=str) diff --git a/openstack/tests/unit/block_storage/v2/test_extension.py b/openstack/tests/unit/block_storage/v2/test_extension.py new file mode 100644 index 000000000..b2ec046ad --- /dev/null +++ b/openstack/tests/unit/block_storage/v2/test_extension.py @@ -0,0 +1,45 @@ +# 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.block_storage.v2 import extension +from openstack.tests.unit import base + +EXTENSION = { + "alias": "os-hosts", + "description": "Admin-only host administration.", + "links": [], + "name": "Hosts", + "namespace": "https://docs.openstack.org/volume/ext/hosts/api/v1.1", + "updated": "2011-06-29T00:00:00+00:00", +} + + +class TestExtension(base.TestCase): + def test_basic(self): + extension_resource = extension.Extension() + self.assertEqual('extensions', extension_resource.resources_key) + self.assertEqual('/extensions', extension_resource.base_path) + self.assertFalse(extension_resource.allow_create) + self.assertFalse(extension_resource.allow_fetch) + self.assertFalse(extension_resource.allow_commit) + self.assertFalse(extension_resource.allow_delete) + self.assertTrue(extension_resource.allow_list) + + def test_make_extension(self): + extension_resource = extension.Extension(**EXTENSION) + self.assertEqual(EXTENSION['alias'], extension_resource.alias) + self.assertEqual( + EXTENSION['description'], extension_resource.description + ) + self.assertEqual(EXTENSION['links'], extension_resource.links) + self.assertEqual(EXTENSION['name'], extension_resource.name) + self.assertEqual(EXTENSION['namespace'], extension_resource.namespace) + self.assertEqual(EXTENSION['updated'], extension_resource.updated_at) From 738d5f18028269502a8f26b14988ac8b12749ac3 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 26 Jul 2023 13:19:10 +0100 Subject: [PATCH 3322/3836] docs: Add documentation for openstack.test.fakes This is quite a nifty utility so let users know about it. Note that some imports had to be changed to avoid them appearing in the documentation. Change-Id: Ifbdfe24126651fd35296a239eb559f5272b316d8 Signed-off-by: Stephen Finucane --- doc/source/user/index.rst | 10 ++++++ doc/source/user/testing/fakes.rst | 5 +++ doc/source/user/testing/index.rst | 8 +++++ openstack/test/fakes.py | 53 +++++++++++++++++++++++-------- 4 files changed, 63 insertions(+), 13 deletions(-) create mode 100644 doc/source/user/testing/fakes.rst create mode 100644 doc/source/user/testing/index.rst diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index 0066080d0..f9e49fcf6 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -42,6 +42,16 @@ approach, this is where you'll want to begin. Orchestration Shared File System +Testing +------- + +The SDK provides a number of utilities to help you test your applications. + +.. toctree:: + :maxdepth: 1 + + testing/index + API Documentation ----------------- diff --git a/doc/source/user/testing/fakes.rst b/doc/source/user/testing/fakes.rst new file mode 100644 index 000000000..ec238ef22 --- /dev/null +++ b/doc/source/user/testing/fakes.rst @@ -0,0 +1,5 @@ +Fakes +===== + +.. automodule:: openstack.test.fakes + :members: diff --git a/doc/source/user/testing/index.rst b/doc/source/user/testing/index.rst new file mode 100644 index 000000000..ef602a9a6 --- /dev/null +++ b/doc/source/user/testing/index.rst @@ -0,0 +1,8 @@ +======================================== +Testing applications using OpenStack SDK +======================================== + +.. toctree:: + :maxdepth: 1 + + fakes diff --git a/openstack/test/fakes.py b/openstack/test/fakes.py index a4f7b809a..173f963cc 100644 --- a/openstack/test/fakes.py +++ b/openstack/test/fakes.py @@ -10,10 +10,18 @@ # License for the specific language governing permissions and limitations # under the License. +""" +The :mod:`~openstack.test.fakes` module exists to help application developers +using the OpenStack SDK to unit test their applications. It provides a number +of helper utilities to generate fake :class:`~openstack.resource.Resource` and +:class:`~openstack.proxy.Proxy` instances. These fakes do not require an +established connection and allow you to validate that your application using +valid attributes and methods for both :class:`~openstack.resource.Resource` and +:class:`~openstack.proxy.Proxy` instances. +""" + import inspect -from random import choice -from random import randint -from random import random +import random import uuid from openstack import format as _format @@ -21,13 +29,23 @@ def generate_fake_resource(resource_type, **attrs): - """Generate fake resource + """Generate a fake resource + + Example usage: + + .. code-block:: python + + >>> from openstack.compute.v2 import server + >>> from openstack.test import fakes + >>> fakes.generate_fake_resource(server.Server) + openstack.compute.v2.server.Server(...) :param type resource_type: Object class :param dict attrs: Optional attributes to be set on resource - - :return: Instance of `resource_type` class populated with fake - values of expected types. + :return: Instance of ``resource_type`` class populated with fake + values of expected types + :raises NotImplementedError: If a resource attribute specifies a ``type`` + or ``list_type`` that cannot be automatically generated """ base_attrs = dict() for name, value in inspect.getmembers( @@ -78,15 +96,15 @@ def generate_fake_resource(resource_type, **attrs): base_attrs[name] = uuid.uuid4().hex elif issubclass(target_type, int): # int - base_attrs[name] = randint(1, 100) + base_attrs[name] = random.randint(1, 100) elif issubclass(target_type, float): # float - base_attrs[name] = random() + base_attrs[name] = random.random() elif issubclass(target_type, bool) or issubclass( target_type, _format.BoolStr ): # bool - base_attrs[name] = choice([True, False]) + base_attrs[name] = random.choice([True, False]) elif issubclass(target_type, dict): # some dict - without further details leave it empty base_attrs[name] = dict() @@ -97,6 +115,7 @@ def generate_fake_resource(resource_type, **attrs): name, ) raise NotImplementedError(msg) + if isinstance(value, resource.URI): # For URI we just generate something base_attrs[name] = uuid.uuid4().hex @@ -107,13 +126,21 @@ def generate_fake_resource(resource_type, **attrs): def generate_fake_resources(resource_type, count=1, attrs=None): - """Generate given number of fake resource entities + """Generate a given number of fake resource entities + + Example usage: + + .. code-block:: python + + >>> from openstack.compute.v2 import server + >>> from openstack.test import fakes + >>> fakes.generate_fake_resources(server.Server, count=3) + :param type resource_type: Object class :param int count: Number of objects to return :param dict attrs: Attribute values to set into each instance - - :return: Array of `resource_type` class instances populated with fake + :return: Generator of ``resource_type`` class instances populated with fake values of expected types. """ if not attrs: From b806adc1ace12d3c1c149c7e0b1efa003076df2b Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 26 Jul 2023 12:30:52 +0100 Subject: [PATCH 3323/3836] Add fake proxy generator As I47312f4036a0b389cd3689466ab220ba558aa39a already did for Resource, start supporting fake Proxy class generation. This requires the user provide a ServiceDescription implementation and will autospec the corresponding proxy class for the API version provided (or the only API version, if only one is supported). Change-Id: Ibc5698ea36d30960d2a6fddef7d9c33187a5b3ca Signed-off-by: Stephen Finucane --- openstack/test/fakes.py | 85 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/openstack/test/fakes.py b/openstack/test/fakes.py index 173f963cc..b0c0abce1 100644 --- a/openstack/test/fakes.py +++ b/openstack/test/fakes.py @@ -22,10 +22,17 @@ import inspect import random +from typing import ( + Optional, + Type, +) +from unittest import mock import uuid from openstack import format as _format +from openstack import proxy from openstack import resource +from openstack import service_description def generate_fake_resource(resource_type, **attrs): @@ -147,3 +154,81 @@ def generate_fake_resources(resource_type, count=1, attrs=None): attrs = {} for _ in range(count): yield generate_fake_resource(resource_type, **attrs) + + +# TODO(stephenfin): It would be helpful to generate fake resources for the +# various proxy methods also, but doing so requires deep code introspection or +# (better) type annotations +def generate_fake_proxy( + service: Type[service_description.ServiceDescription], + api_version: Optional[int] = None, +) -> proxy.Proxy: + """Generate a fake proxy for the given service type + + Example usage: + + .. code-block:: python + + >>> import functools + >>> from openstack.compute import compute_service + >>> from openstack.compute.v2 import server + >>> from openstack.test import fakes + >>> # create the fake proxy + >>> fake_compute_proxy = fakes.generate_fake_proxy( + ... compute_service.ComputeService, + ... ) + >>> # configure return values for various proxy APIs + >>> # note that this will generate new fake resources on each invocation + >>> fake_compute_proxy.get_server.side_effect = functools.partial( + ... fakes.generate_fake_resource, + ... server.Server, + ... ) + >>> fake_compute_proxy.servers.side_effect = functools.partial( + ... fakes.generate_fake_resources, + ... server.Server, + ... ) + >>> fake_compute_proxy.servers() + + >>> fake_compute_proxy.serverssss() + Traceback (most recent call last): + File "", line 1, in + File "/usr/lib64/python3.11/unittest/mock.py", line 653, in __getattr__ + raise AttributeError("Mock object has no attribute %r" % name) + AttributeError: Mock object has no attribute 'serverssss'. Did you mean: 'server_ips'? + + :param service: The service to generate the fake proxy for. + :type service: :class:`~openstack.service_description.ServiceDescription` + :param api_version: The API version to generate the fake proxy for. + This should be a major version must be supported by openstacksdk, as + specified in the ``supported_versions`` attribute of the provided + ``service``. This is only required if openstacksdk supports multiple + API versions for the given service. + :type api_version: int or None + :raises ValueError: if the ``service`` is not a valid + :class:`~openstack.service_description.ServiceDescription` or if + ``api_version`` is not supported + :returns: An autospecced mock of the :class:`~openstack.proxy.Proxy` + implementation for the specified service type and API version + """ + if not issubclass(service, service_description.ServiceDescription): + raise ValueError( + f"Service {service.__name__} is not a valid ServiceDescription" + ) + + supported_versions = service.supported_versions + + if api_version is None: + if len(supported_versions) > 1: + raise ValueError( + f"api_version was not provided but service {service.__name__} " + f"provides multiple API versions" + ) + else: + api_version = list(supported_versions)[0] + elif str(api_version) not in supported_versions: + raise ValueError( + f"API version {api_version} is not supported by openstacksdk. " + f"Supported API versions are: {', '.join(supported_versions)}" + ) + + return mock.create_autospec(supported_versions[str(api_version)]) From bdb32979dc79c1ee7e337769770af5018339f8f9 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 26 Jul 2023 13:05:21 +0100 Subject: [PATCH 3324/3836] fakes: Add type hints Useful for third parties using this. Change-Id: I91659d6c08439b22a8fb8c881decd54634497846 Signed-off-by: Stephen Finucane --- openstack/test/fakes.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/openstack/test/fakes.py b/openstack/test/fakes.py index b0c0abce1..a405e4642 100644 --- a/openstack/test/fakes.py +++ b/openstack/test/fakes.py @@ -23,8 +23,12 @@ import inspect import random from typing import ( + Any, + Dict, + Generator, Optional, Type, + TypeVar, ) from unittest import mock import uuid @@ -35,9 +39,18 @@ from openstack import service_description -def generate_fake_resource(resource_type, **attrs): +Resource = TypeVar('Resource', bound=resource.Resource) + + +def generate_fake_resource( + resource_type: Type[Resource], + **attrs: Dict[str, Any], +) -> Resource: """Generate a fake resource + :param type resource_type: Object class + :param dict attrs: Optional attributes to be set on resource + Example usage: .. code-block:: python @@ -132,9 +145,17 @@ def generate_fake_resource(resource_type, **attrs): return fake -def generate_fake_resources(resource_type, count=1, attrs=None): +def generate_fake_resources( + resource_type: Type[Resource], + count: int = 1, + attrs: Optional[Dict[str, Any]] = None, +) -> Generator[Resource, None, None]: """Generate a given number of fake resource entities + :param type resource_type: Object class + :param int count: Number of objects to return + :param dict attrs: Attribute values to set into each instance + Example usage: .. code-block:: python From c0758f146273d915aebe632e4953c6d0f54d3cf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Ribaud?= Date: Mon, 24 Jul 2023 17:35:52 +0200 Subject: [PATCH 3325/3836] Return HTTP response for delete_access_rule. In the original code, when ignore_missing parameter is set to False, no information is reported when either the share or the access is not found. However, after applying this patch, the HTTP response is returned to users, allowing them to verify whether the request completed successfully or not. If there are serious issues with the request clients, an exception will be raised to handle those situations appropriately. Change-Id: I925d943a00b4de198013336f5043753552255247 --- openstack/shared_file_system/v2/_proxy.py | 5 +++-- openstack/shared_file_system/v2/share_access_rule.py | 8 +++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index 157b1b7a5..4392c798f 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -816,10 +816,11 @@ def delete_access_rule(self, access_id, share_id, ignore_missing=True): :param access_id: The id of the access rule to get :param share_id: The ID of the share - :rtype: ``None`` + :rtype: ``requests.models.Response`` HTTP response from internal + requests client """ res = self._get_resource(_share_access_rule.ShareAccessRule, access_id) - res.delete(self, share_id, ignore_missing=ignore_missing) + return res.delete(self, share_id, ignore_missing=ignore_missing) def share_group_snapshots(self, details=True, **query): """Lists all share group snapshots. diff --git a/openstack/shared_file_system/v2/share_access_rule.py b/openstack/shared_file_system/v2/share_access_rule.py index 48138c373..1983ad6dd 100644 --- a/openstack/shared_file_system/v2/share_access_rule.py +++ b/openstack/shared_file_system/v2/share_access_rule.py @@ -61,7 +61,7 @@ def _action(self, session, body, url, action='patch', microversion=None): if microversion is None: microversion = self._get_microversion(session, action=action) - session.post( + return session.post( url, json=body, headers=headers, microversion=microversion ) @@ -74,10 +74,12 @@ def create(self, session, **kwargs): ) def delete(self, session, share_id, ignore_missing=True): - body = {'deny_access': {'access_id': self.id}} - url = utils.urljoin('/shares', share_id, 'action') + body = {"deny_access": {"access_id": self.id}} + url = utils.urljoin("/shares", share_id, "action") + response = self._action(session, body, url) try: response = self._action(session, body, url) + self._translate_response(response) except exceptions.ResourceNotFound: if not ignore_missing: raise From 6ef2af39d5cec38db3dca075aa874c5e6f40d6a8 Mon Sep 17 00:00:00 2001 From: Anvi Joshi Date: Fri, 23 Jun 2023 20:02:44 +0000 Subject: [PATCH 3326/3836] Implemented methods for share metadata Change-Id: Ib3c27ec3d0eb102b30a80994d6cc2af4ee78f3dd --- doc/source/user/guides/shared_file_system.rst | 46 +++++++ .../user/proxies/shared_file_system.rst | 13 ++ examples/shared_file_system/share_metadata.py | 61 +++++++++ openstack/shared_file_system/v2/_proxy.py | 92 +++++++++++++- openstack/shared_file_system/v2/share.py | 3 +- .../shared_file_system/test_share_metadata.py | 120 ++++++++++++++++++ .../unit/shared_file_system/v2/test_proxy.py | 57 +++++++++ ...ystem-share-metadata-e0415bb71d8a0a48.yaml | 6 + 8 files changed, 395 insertions(+), 3 deletions(-) create mode 100644 examples/shared_file_system/share_metadata.py create mode 100644 openstack/tests/functional/shared_file_system/test_share_metadata.py create mode 100644 releasenotes/notes/add-shared-file-system-share-metadata-e0415bb71d8a0a48.yaml diff --git a/doc/source/user/guides/shared_file_system.rst b/doc/source/user/guides/shared_file_system.rst index 938e75fc5..a2e300869 100644 --- a/doc/source/user/guides/shared_file_system.rst +++ b/doc/source/user/guides/shared_file_system.rst @@ -134,3 +134,49 @@ Deletes a share group snapshot. .. literalinclude:: ../examples/shared_file_system/share_group_snapshots.py :pyobject: delete_share_group_snapshot + + +List Share Metadata +-------------------- + +Lists all metadata for a given share. + +.. literalinclude:: ../examples/shared_file_system/share_metadata.py + :pyobject: list_share_metadata + + +Get Share Metadata Item +----------------------- + +Retrieves a specific metadata item from a shares metadata by its key. + +.. literalinclude:: ../examples/shared_file_system/share_metadata.py + :pyobject: get_share_metadata_item + + +Create Share Metadata +---------------------- + +Creates share metadata. + +.. literalinclude:: ../examples/shared_file_system/share_metadata.py + :pyobject: create_share_metadata + + +Update Share Metadata +---------------------- + +Updates metadata of a given share. + +.. literalinclude:: ../examples/shared_file_system/share_metadata.py + :pyobject: update_share_metadata + + +Delete Share Metadata +---------------------- + +Deletes a specific metadata item from a shares metadata by its key. Can +specify multiple keys to be deleted. + +.. literalinclude:: ../examples/shared_file_system/share_metadata.py + :pyobject: delete_share_metadata diff --git a/doc/source/user/proxies/shared_file_system.rst b/doc/source/user/proxies/shared_file_system.rst index 28a4ca50a..5cdaab1af 100644 --- a/doc/source/user/proxies/shared_file_system.rst +++ b/doc/source/user/proxies/shared_file_system.rst @@ -163,3 +163,16 @@ service. :members: share_group_snapshots, get_share_group_snapshot, create_share_group_snapshot, reset_share_group_snapshot_status, update_share_group_snapshot, delete_share_group_snapshot + + +Shared File System Share Metadata +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +List, Get, Create, Update, and Delete metadata for shares from the +Shared File Systems service. + +.. autoclass:: openstack.shared_file_system.v2._proxy.Proxy + :noindex: + :members: get_share_metadata, get_share_metadata_item, + create_share_metadata, update_share_metadata, + delete_share_metadata diff --git a/examples/shared_file_system/share_metadata.py b/examples/shared_file_system/share_metadata.py new file mode 100644 index 000000000..b1ca00c62 --- /dev/null +++ b/examples/shared_file_system/share_metadata.py @@ -0,0 +1,61 @@ +# 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. + + +def list_share_metadata(conn, share_id): + # Method returns the entire share with the metadata inside it. + returned_share = conn.get_share_metadata(share_id) + + # Access metadata of share + metadata = returned_share['metadata'] + + print("List All Share Metadata:") + for meta_key in metadata: + print(f"{meta_key}={metadata[meta_key]}") + + +def get_share_metadata_item(conn, share_id, key): + # Method returns the entire share with the metadata inside it. + returned_share = conn.get_share_metadata_item(share_id, key) + + # Access metadata of share + metadata = returned_share['metadata'] + + print("Get share metadata item given item key and share id:") + print(metadata[key]) + + +def create_share_metadata(conn, share_id, metadata): + # Method returns the entire share with the metadata inside it. + created_share = conn.create_share_metadata(share_id, metadata) + + # Access metadata of share + metadata = created_share['metadata'] + + print("Metadata created for given share:") + print(metadata) + + +def update_share_metadata(conn, share_id, metadata): + # Method returns the entire share with the metadata inside it. + updated_share = conn.update_share_metadata(share_id, metadata, True) + + # Access metadata of share + metadata = updated_share['metadata'] + + print("Updated metadata for given share:") + print(metadata) + + +def delete_share_metadata(conn, share_id, keys): + # Method doesn't return anything. + conn.delete_share_metadata(share_id, keys) diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index 4392c798f..bbe756349 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -9,7 +9,7 @@ # 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 import proxy from openstack import resource from openstack.shared_file_system.v2 import ( @@ -820,7 +820,7 @@ def delete_access_rule(self, access_id, share_id, ignore_missing=True): requests client """ res = self._get_resource(_share_access_rule.ShareAccessRule, access_id) - return res.delete(self, share_id, ignore_missing=ignore_missing) + res.delete(self, share_id, ignore_missing=ignore_missing) def share_group_snapshots(self, details=True, **query): """Lists all share group snapshots. @@ -940,3 +940,91 @@ def delete_share_group_snapshot( group_snapshot_id, ignore_missing=ignore_missing, ) + + # ========= Share Metadata ========== + def get_share_metadata(self, share_id): + """Lists all metadata for a share. + + :param share_id: The ID of the share + + :returns: A :class:`~openstack.shared_file_system.v2.share.Share` + with the share's metadata. + :rtype: + :class:`~openstack.shared_file_system.v2.share.Share` + """ + share = self._get_resource(_share.Share, share_id) + return share.fetch_metadata(self) + + def get_share_metadata_item(self, share_id, key): + """Retrieves a specific metadata item from a share by its key. + + :param share_id: The ID of the share + :param key: The key of the share metadata + + :returns: A :class:`~openstack.shared_file_system.v2.share.Share` + with the share's metadata. + :rtype: + :class:`~openstack.shared_file_system.v2.share.Share` + """ + share = self._get_resource(_share.Share, share_id) + return share.get_metadata_item(self, key) + + def create_share_metadata(self, share_id, **metadata): + """Creates share metadata as key-value pairs. + + :param share_id: The ID of the share + :param metadata: The metadata to be created + + :returns: A :class:`~openstack.shared_file_system.v2.share.Share` + with the share's metadata. + :rtype: + :class:`~openstack.shared_file_system.v2.share.Share` + """ + share = self._get_resource(_share.Share, share_id) + return share.set_metadata(self, metadata=metadata) + + def update_share_metadata(self, share_id, metadata, replace=False): + """Updates metadata of given share. + + :param share_id: The ID of the share + :param metadata: The metadata to be created + :param replace: Boolean for whether the preexisting metadata + should be replaced + + :returns: A :class:`~openstack.shared_file_system.v2.share.Share` + with the share's updated metadata. + :rtype: + :class:`~openstack.shared_file_system.v2.share.Share` + """ + share = self._get_resource(_share.Share, share_id) + return share.set_metadata(self, metadata=metadata, replace=replace) + + def delete_share_metadata(self, share_id, keys, ignore_missing=True): + """Deletes a single metadata item on a share, idetified by its key. + + :param share_id: The ID of the share + :param keys: The list of share metadata keys to be deleted + :param ignore_missing: Boolean indicating if missing keys should be ignored. + + :returns: None + :rtype: None + """ + share = self._get_resource(_share.Share, share_id) + keys_failed_to_delete = [] + for key in keys: + try: + share.delete_metadata_item(self, key) + except exceptions.NotFoundException: + if not ignore_missing: + self._connection.log.info("Key %s not found.", key) + keys_failed_to_delete.append(key) + except exceptions.ForbiddenException: + self._connection.log.info("Key %s cannot be deleted.", key) + keys_failed_to_delete.append(key) + except exceptions.SDKException: + self._connection.log.info("Failed to delete key %s.", key) + keys_failed_to_delete.append(key) + if keys_failed_to_delete: + raise exceptions.SDKException( + "Some keys failed to be deleted %s" % keys_failed_to_delete + ) diff --git a/openstack/shared_file_system/v2/share.py b/openstack/shared_file_system/v2/share.py index 79f9c3fa4..ebe3adfb4 100644 --- a/openstack/shared_file_system/v2/share.py +++ b/openstack/shared_file_system/v2/share.py @@ -10,12 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.common import metadata from openstack import exceptions from openstack import resource from openstack import utils -class Share(resource.Resource): +class Share(resource.Resource, metadata.MetadataMixin): resource_key = "share" resources_key = "shares" base_path = "/shares" diff --git a/openstack/tests/functional/shared_file_system/test_share_metadata.py b/openstack/tests/functional/shared_file_system/test_share_metadata.py new file mode 100644 index 000000000..1afa56b9c --- /dev/null +++ b/openstack/tests/functional/shared_file_system/test_share_metadata.py @@ -0,0 +1,120 @@ +# 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.shared_file_system.v2 import share as _share +from openstack.tests.functional.shared_file_system import base + + +class ShareMetadataTest(base.BaseSharedFileSystemTest): + def setUp(self): + super().setUp() + + self.SHARE_NAME = self.getUniqueString() + my_share = self.create_share( + name=self.SHARE_NAME, + size=2, + share_type="dhss_false", + share_protocol='NFS', + description=None, + ) + self.SHARE_ID = my_share.id + self.assertIsNotNone(my_share) + self.assertIsNotNone(my_share.id) + + def test_create(self): + meta = {"foo": "bar"} + created_share = ( + self.user_cloud.shared_file_system.create_share_metadata( + self.SHARE_ID, **meta + ) + ) + assert isinstance(created_share, _share.Share) + self.assertEqual(created_share['metadata'], meta) + + def test_get_item(self): + meta = {"foo": "bar"} + created_share = ( + self.user_cloud.shared_file_system.create_share_metadata( + self.SHARE_ID, **meta + ) + ) + returned_share = ( + self.user_cloud.shared_file_system.get_share_metadata_item( + self.SHARE_ID, "foo" + ) + ) + self.assertEqual( + created_share['metadata']['foo'], returned_share['metadata']['foo'] + ) + + def test_get(self): + meta = {"foo": "bar"} + created_share = ( + self.user_cloud.shared_file_system.create_share_metadata( + self.SHARE_ID, **meta + ) + ) + returned_share = self.user_cloud.shared_file_system.get_share_metadata( + self.SHARE_ID + ) + self.assertEqual( + created_share['metadata']['foo'], returned_share['metadata']['foo'] + ) + + def test_update(self): + meta = {"foo": "bar"} + created_share = ( + self.user_cloud.shared_file_system.create_share_metadata( + self.SHARE_ID, **meta + ) + ) + + new_meta = {"newFoo": "newBar"} + full_meta = {"foo": "bar", "newFoo": "newBar"} + empty_meta = {} + + updated_share = ( + self.user_cloud.shared_file_system.update_share_metadata( + created_share, new_meta + ) + ) + self.assertEqual(updated_share['metadata'], new_meta) + + full_metadata = self.user_cloud.shared_file_system.get_share_metadata( + created_share + )['metadata'] + self.assertEqual(full_metadata, full_meta) + + share_with_deleted_metadata = ( + self.user_cloud.shared_file_system.update_share_metadata( + updated_share, empty_meta + ) + ) + self.assertEqual(share_with_deleted_metadata['metadata'], empty_meta) + + def test_delete(self): + meta = {"foo": "bar", "newFoo": "newBar"} + created_share = ( + self.user_cloud.shared_file_system.create_share_metadata( + self.SHARE_ID, **meta + ) + ) + + self.user_cloud.shared_file_system.delete_share_metadata( + created_share, ["foo", "invalidKey"] + ) + + deleted_share = self.user_cloud.shared_file_system.get_share_metadata( + self.SHARE_ID + ) + + self.assertEqual(deleted_share['metadata'], {"newFoo": "newBar"}) diff --git a/openstack/tests/unit/shared_file_system/v2/test_proxy.py b/openstack/tests/unit/shared_file_system/v2/test_proxy.py index 795b35b44..921318f64 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_proxy.py +++ b/openstack/tests/unit/shared_file_system/v2/test_proxy.py @@ -155,6 +155,63 @@ def test_storage_pool_not_detailed(self): ) +class TestSharedFileSystemShareMetadata(TestSharedFileSystemProxy): + def test_get_share_metadata(self): + self._verify( + "openstack.shared_file_system.v2.share.Share.fetch_metadata", + self.proxy.get_share_metadata, + method_args=["share_id"], + expected_args=[self.proxy], + expected_result=share.Share( + id="share_id", metadata={"key": "value"} + ), + ) + + def test_get_share_metadata_item(self): + self._verify( + "openstack.shared_file_system.v2.share.Share.get_metadata_item", + self.proxy.get_share_metadata_item, + method_args=["share_id", "key"], + expected_args=[self.proxy, "key"], + expected_result=share.Share( + id="share_id", metadata={"key": "value"} + ), + ) + + def test_create_share_metadata(self): + metadata = {"foo": "bar", "newFoo": "newBar"} + self._verify( + "openstack.shared_file_system.v2.share.Share.set_metadata", + self.proxy.create_share_metadata, + method_args=["share_id"], + method_kwargs=metadata, + expected_args=[self.proxy], + expected_kwargs={"metadata": metadata}, + expected_result=share.Share(id="share_id", metadata=metadata), + ) + + def test_update_share_metadata(self): + metadata = {"foo": "bar", "newFoo": "newBar"} + replace = True + self._verify( + "openstack.shared_file_system.v2.share.Share.set_metadata", + self.proxy.update_share_metadata, + method_args=["share_id", metadata, replace], + expected_args=[self.proxy], + expected_kwargs={"metadata": metadata, "replace": replace}, + expected_result=share.Share(id="share_id", metadata=metadata), + ) + + def test_delete_share_metadata(self): + self._verify( + "openstack.shared_file_system.v2.share.Share.delete_metadata_item", + self.proxy.delete_share_metadata, + expected_result=None, + method_args=["share_id", ["key"]], + expected_args=[self.proxy, "key"], + ) + + class TestUserMessageProxy(test_proxy_base.TestProxyBase): def setUp(self): super(TestUserMessageProxy, self).setUp() diff --git a/releasenotes/notes/add-shared-file-system-share-metadata-e0415bb71d8a0a48.yaml b/releasenotes/notes/add-shared-file-system-share-metadata-e0415bb71d8a0a48.yaml new file mode 100644 index 000000000..6461ec7b7 --- /dev/null +++ b/releasenotes/notes/add-shared-file-system-share-metadata-e0415bb71d8a0a48.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Added support to list, get, create, + update, and delete share metadata + from shared file system service. \ No newline at end of file From 0f7aeda99fc061145a51a986cd519b83aaa493fc Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 26 May 2023 11:10:28 +0100 Subject: [PATCH 3327/3836] cloud: Remove '_is_client_version' This was relying on legacy clients. We don't need it anymore. Remove it and associated helpers. Change-Id: I11a9e17ff6b9acafb0faba675c1ddb729024abab Signed-off-by: Stephen Finucane --- openstack/cloud/_identity.py | 10 +++------- openstack/cloud/_image.py | 2 +- openstack/cloud/openstackcloud.py | 7 ------- openstack/proxy.py | 6 ------ openstack/tests/unit/cloud/test_image.py | 16 ++++++++++++---- 5 files changed, 16 insertions(+), 25 deletions(-) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index ad9f2e93d..7a8cd3daf 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -10,15 +10,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -# import types so that we can reference ListType in sphinx param declarations. -# We can't just use list, because sphinx gets confused by -# openstack.resource.Resource.list and openstack.resource2.Resource.list -import types # noqa - from openstack.cloud import _utils from openstack.cloud import exc from openstack import exceptions from openstack.identity.v3._proxy import Proxy +from openstack import utils class IdentityCloudMixin: @@ -37,7 +33,7 @@ def _get_project_id_param_dict(self, name_or_id): project = self.get_project(name_or_id) if not project: return {} - if self._is_client_version('identity', 3): + if utils.supports_version(self.identity, '3'): return {'default_project_id': project['id']} else: return {'tenant_id': project['id']} @@ -51,7 +47,7 @@ def _get_domain_id_param_dict(self, domain_id): # not. However, keystone v2 does not allow user creation by non-admin # users, so we can throw an error to the user that does not need to # mention api versions - if self._is_client_version('identity', 3): + if utils.supports_version(self.identity, '3'): if not domain_id: raise exc.OpenStackCloudException( "User or project creation requires an explicit domain_id " diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py index 8ddb5b750..aafefc9ef 100644 --- a/openstack/cloud/_image.py +++ b/openstack/cloud/_image.py @@ -71,7 +71,7 @@ def list_images(self, filter_deleted=True, show_all=False): images = [] params = {} image_list = [] - if self._is_client_version('image', 2): + if utils.supports_version(self.image, '2'): if show_all: params['member_status'] = 'all' image_list = list(self.image.images(**params)) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 28a2f5777..42bcbd102 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -465,13 +465,6 @@ def _get_raw_client( region_name=self.config.get_region_name(service_type), ) - def _is_client_version(self, client, version): - client_name = '_{client}_client'.format( - client=client.replace('-', '_') - ) - client = getattr(self, client_name) - return client._version_matches(version) - @property def _application_catalog_client(self): if 'application-catalog' not in self._raw_clients: diff --git a/openstack/proxy.py b/openstack/proxy.py index ce4010721..ff2dd75dc 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -420,12 +420,6 @@ def _report_stats_influxdb( except Exception: self.log.exception('Error writing statistics to InfluxDB') - def _version_matches(self, version): - api_version = self.get_api_major_version() - if api_version: - return api_version[0] == version - return False - def _get_connection(self): """Get the Connection object associated with this Proxy. diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index eff949fd1..158a41442 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -1937,7 +1937,9 @@ def test_config_v1(self): 'https://image.example.com/v1/', self.cloud._image_client.get_endpoint(), ) - self.assertTrue(self.cloud._is_client_version('image', 1)) + self.assertEqual( + self.cloud._image_client.get_api_major_version()[0], 1 + ) def test_config_v2(self): self.cloud.config.config['image_api_version'] = '2' @@ -1947,7 +1949,9 @@ def test_config_v2(self): 'https://image.example.com/v1/', self.cloud._image_client.get_endpoint(), ) - self.assertFalse(self.cloud._is_client_version('image', 2)) + self.assertNotEqual( + self.cloud._image_client.get_api_major_version()[0], 2 + ) class TestImageV2Only(base.TestCase): @@ -1963,7 +1967,9 @@ def test_config_v1(self): 'https://image.example.com/v2/', self.cloud._image_client.get_endpoint(), ) - self.assertTrue(self.cloud._is_client_version('image', 2)) + self.assertEqual( + self.cloud._image_client.get_api_major_version()[0], 2 + ) def test_config_v2(self): self.cloud.config.config['image_api_version'] = '2' @@ -1973,7 +1979,9 @@ def test_config_v2(self): 'https://image.example.com/v2/', self.cloud._image_client.get_endpoint(), ) - self.assertTrue(self.cloud._is_client_version('image', 2)) + self.assertEqual( + self.cloud._image_client.get_api_major_version()[0], 2 + ) class TestImageVolume(BaseTestImage): From 66d201915d716101ce7423183034a5aa981d3158 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 27 Jul 2023 15:42:13 +0100 Subject: [PATCH 3328/3836] block storage: Add support for services Change-Id: I6f6097bf5c8a9f81ec4100f60358c63e50b1289d Signed-off-by: Stephen Finucane --- doc/source/user/proxies/block_storage_v3.rst | 9 +- .../user/resources/block_storage/index.rst | 1 + .../resources/block_storage/v3/service.rst | 12 ++ openstack/block_storage/v3/_proxy.py | 147 +++++++++++++ openstack/block_storage/v3/service.py | 158 ++++++++++++++ .../block_storage/v3/test_service.py | 38 ++++ .../tests/unit/block_storage/v3/test_proxy.py | 48 +++++ .../unit/block_storage/v3/test_service.py | 195 ++++++++++++++++++ ...rage-service-support-ce03092ce2d7e7b9.yaml | 4 + 9 files changed, 611 insertions(+), 1 deletion(-) create mode 100644 doc/source/user/resources/block_storage/v3/service.rst create mode 100644 openstack/block_storage/v3/service.py create mode 100644 openstack/tests/functional/block_storage/v3/test_service.py create mode 100644 openstack/tests/unit/block_storage/v3/test_service.py create mode 100644 releasenotes/notes/add-block-storage-service-support-ce03092ce2d7e7b9.yaml diff --git a/doc/source/user/proxies/block_storage_v3.rst b/doc/source/user/proxies/block_storage_v3.rst index df619305e..abe3f0e73 100644 --- a/doc/source/user/proxies/block_storage_v3.rst +++ b/doc/source/user/proxies/block_storage_v3.rst @@ -91,6 +91,14 @@ Group Type Operations update_group_type_group_specs_property, delete_group_type_group_specs_property +Service Operations +^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v3._proxy.Proxy + :noindex: + :members: find_service, services, enable_service, disable_service, + thaw_service, freeze_service, failover_service + Type Operations ^^^^^^^^^^^^^^^ @@ -140,4 +148,3 @@ BlockStorageSummary Operations .. autoclass:: openstack.block_storage.v3._proxy.Proxy :noindex: :members: summary - diff --git a/doc/source/user/resources/block_storage/index.rst b/doc/source/user/resources/block_storage/index.rst index 564597d89..92a22ae22 100644 --- a/doc/source/user/resources/block_storage/index.rst +++ b/doc/source/user/resources/block_storage/index.rst @@ -15,4 +15,5 @@ Block Storage Resources v3/snapshot v3/type v3/volume + v3/service v3/block_storage_summary diff --git a/doc/source/user/resources/block_storage/v3/service.rst b/doc/source/user/resources/block_storage/v3/service.rst new file mode 100644 index 000000000..433880a86 --- /dev/null +++ b/doc/source/user/resources/block_storage/v3/service.rst @@ -0,0 +1,12 @@ +openstack.block_storage.v3.service +================================== + +.. automodule:: openstack.block_storage.v3.service + +The Service Class +----------------- + +The ``Service`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v3.service.Service + :members: diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index b729f9720..ca41f39f5 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + from openstack.block_storage import _base_proxy from openstack.block_storage.v3 import availability_zone from openstack.block_storage.v3 import backup as _backup @@ -22,6 +24,7 @@ from openstack.block_storage.v3 import limits as _limits from openstack.block_storage.v3 import quota_set as _quota_set from openstack.block_storage.v3 import resource_filter as _resource_filter +from openstack.block_storage.v3 import service as _service from openstack.block_storage.v3 import snapshot as _snapshot from openstack.block_storage.v3 import stats as _stats from openstack.block_storage.v3 import type as _type @@ -1581,6 +1584,150 @@ def update_quota_set(self, quota_set, query=None, **attrs): query = {} return res.commit(self, **query) + # ====== SERVICES ====== + @ty.overload + def find_service( + self, + name_or_id: str, + ignore_missing: ty.Literal[True] = True, + **query, + ) -> ty.Optional[_service.Service]: + ... + + @ty.overload + def find_service( + self, + name_or_id: str, + ignore_missing: ty.Literal[False] = True, + **query, + ) -> _service.Service: + ... + + def find_service( + self, + name_or_id: str, + ignore_missing: bool = True, + **query, + ) -> ty.Optional[_service.Service]: + """Find a single service + + :param name_or_id: The name or ID of a service + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the resource does not exist. + When set to ``True``, None will be returned when attempting to find + a nonexistent resource. + :param dict query: Additional attributes like 'host' + + :returns: One: class:`~openstack.block_storage.v3.service.Service` or None + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple + resources are found. + """ + return self._find( + _service.Service, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) + + def services( + self, + **query: ty.Any, + ) -> ty.Generator[_service.Service, None, None]: + """Return a generator of service + + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. + :returns: A generator of Service objects + :rtype: class: `~openstack.block_storage.v3.service.Service` + """ + return self._list(_service.Service, **query) + + def enable_service( + self, + service: ty.Union[str, _service.Service], + ) -> _service.Service: + """Enable a service + + :param service: Either the ID of a service or a + :class:`~openstack.block_storage.v3.service.Service` instance. + + :returns: Updated service instance + :rtype: class: `~openstack.block_storage.v3.service.Service` + """ + service = self._get_resource(_service.Service, service) + return service.enable(self) + + def disable_service( + self, + service: ty.Union[str, _service.Service], + *, + reason: ty.Optional[str] = None, + ) -> _service.Service: + """Disable a service + + :param service: Either the ID of a service or a + :class:`~openstack.block_storage.v3.service.Service` instance + :param str reason: The reason to disable a service + + :returns: Updated service instance + :rtype: class: `~openstack.block_storage.v3.service.Service` + """ + service = self._get_resource(_service.Service, service) + return service.disable(self, reason=reason) + + def thaw_service( + self, + service: ty.Union[str, _service.Service], + ) -> _service.Service: + """Thaw a service + + :param service: Either the ID of a service or a + :class:`~openstack.block_storage.v3.service.Service` instance + + :returns: Updated service instance + :rtype: class: `~openstack.block_storage.v3.service.Service` + """ + service = self._get_resource(_service.Service, service) + return service.thaw(self) + + def freeze_service( + self, + service: ty.Union[str, _service.Service], + ) -> _service.Service: + """Freeze a service + + :param service: Either the ID of a service or a + :class:`~openstack.block_storage.v3.service.Service` instance + + :returns: Updated service instance + :rtype: class: `~openstack.block_storage.v3.service.Service` + """ + service = self._get_resource(_service.Service, service) + return service.freeze(self) + + def failover_service( + self, + service: ty.Union[str, _service.Service], + *, + cluster: ty.Optional[str] = None, + backend_id: ty.Optional[str] = None, + ) -> _service.Service: + """Failover a service + + Only applies to replicating cinder-volume services. + + :param service: Either the ID of a service or a + :class:`~openstack.block_storage.v3.service.Service` instance + + :returns: Updated service instance + :rtype: class: `~openstack.block_storage.v3.service.Service` + """ + service = self._get_resource(_service.Service, service) + return service.failover(self, cluster=cluster, backend_id=backend_id) + # ====== RESOURCE FILTERS ====== def resource_filters(self, **query): """Retrieve a generator of resource filters diff --git a/openstack/block_storage/v3/service.py b/openstack/block_storage/v3/service.py new file mode 100644 index 000000000..fe78f1f72 --- /dev/null +++ b/openstack/block_storage/v3/service.py @@ -0,0 +1,158 @@ +# 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 import resource +from openstack import utils + + +class Service(resource.Resource): + resources_key = 'services' + base_path = '/os-services' + + # capabilities + allow_list = True + + _query_mapping = resource.QueryParameters( + 'binary', + 'host', + ) + + # Properties + #: The ID of active storage backend (cinder-volume services only) + active_backend_id = resource.Body('active_backend_id') + #: The availability zone of service + availability_zone = resource.Body('zone') + #: The state of storage backend (cinder-volume services only) + backend_state = resource.Body('backend_state') + #: Binary name of service + binary = resource.Body('binary') + #: The cluster name (since 3.7) + cluster = resource.Body('cluster') + #: Disabled reason of service + disabled_reason = resource.Body('disabled_reason') + #: The name of the host where service runs + host = resource.Body('host') + # Whether the host is frozen or not (cinder-volume services only) + is_frozen = resource.Body('frozen') + #: Service name + name = resource.Body('name', alias='binary') + #: The volume service replication status (cinder-volume services only) + replication_status = resource.Body('replication_status') + #: State of service + state = resource.Body('state') + #: Status of service + status = resource.Body('status') + #: The date and time when the resource was updated + updated_at = resource.Body('updated_at') + + # 3.7 introduced the 'cluster' field + _max_microversion = '3.7' + + @classmethod + def find(cls, session, name_or_id, ignore_missing=True, **params): + # No direct request possible, thus go directly to list + data = cls.list(session, **params) + + result = None + for maybe_result in data: + # Since ID might be both int and str force cast + id_value = str(cls._get_id(maybe_result)) + name_value = maybe_result.name + + if str(name_or_id) in (id_value, name_value): + if 'host' in params and maybe_result['host'] != params['host']: + continue + # Only allow one resource to be found. If we already + # found a match, raise an exception to show it. + if result is None: + result = maybe_result + else: + msg = "More than one %s exists with the name '%s'." + msg = msg % (cls.__name__, name_or_id) + raise exceptions.DuplicateResource(msg) + + if result is not None: + return result + + if ignore_missing: + return None + raise exceptions.ResourceNotFound( + "No %s found for %s" % (cls.__name__, name_or_id) + ) + + def commit(self, session, prepend_key=False, **kwargs): + # we need to set prepend_key to false + return super().commit( + session, + prepend_key=prepend_key, + **kwargs, + ) + + def _action(self, session, action, body, microversion=None): + if not microversion: + microversion = session.default_microversion + url = utils.urljoin(Service.base_path, action) + response = session.put(url, json=body, microversion=microversion) + self._translate_response(response) + return self + + # TODO(stephenfin): Add support for log levels once we have the resource + # modelled (it can be done on a deployment wide basis) + + def enable(self, session): + """Enable service.""" + body = {'binary': self.binary, 'host': self.host} + return self._action(session, 'enable', body) + + def disable(self, session, *, reason=None): + """Disable service.""" + body = {'binary': self.binary, 'host': self.host} + + if not reason: + action = 'disable' + else: + action = 'disable-log-reason' + body['disabled_reason'] = reason + + return self._action(session, action, body) + + def thaw(self, session): + body = {'host': self.host} + return self._action(session, 'thaw', body) + + def freeze(self, session): + body = {'host': self.host} + return self._action(session, 'freeze', body) + + def failover( + self, + session, + *, + cluster=None, + backend_id=None, + ): + """Failover a service + + Only applies to replicating cinder-volume services. + """ + body = {'host': self.host} + if cluster: + body['cluster'] = cluster + if backend_id: + body['backend_id'] = backend_id + + action = 'failover_host' + if utils.supports_microversion(self, '3.26'): + action = 'failover' + + return self._action(session, action, body) diff --git a/openstack/tests/functional/block_storage/v3/test_service.py b/openstack/tests/functional/block_storage/v3/test_service.py new file mode 100644 index 000000000..d97b915ad --- /dev/null +++ b/openstack/tests/functional/block_storage/v3/test_service.py @@ -0,0 +1,38 @@ +# 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.tests.functional import base + + +class TestService(base.BaseFunctionalTest): + # listing services is slooowwww + TIMEOUT_SCALING_FACTOR = 2.0 + + def test_list(self): + sot = list(self.operator_cloud.block_storage.services()) + self.assertIsNotNone(sot) + + def test_disable_enable(self): + for srv in self.operator_cloud.block_storage.services(): + # only nova-block_storage can be updated + if srv.name == 'nova-block_storage': + self.operator_cloud.block_storage.disable_service(srv) + self.operator_cloud.block_storage.enable_service(srv) + break + + def test_find(self): + for srv in self.operator_cloud.block_storage.services(): + self.operator_cloud.block_storage.find_service( + srv.name, + host=srv.host, + ignore_missing=False, + ) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 2efe2fbfc..bb517b7e2 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -22,6 +22,7 @@ from openstack.block_storage.v3 import limits from openstack.block_storage.v3 import quota_set from openstack.block_storage.v3 import resource_filter +from openstack.block_storage.v3 import service from openstack.block_storage.v3 import snapshot from openstack.block_storage.v3 import stats from openstack.block_storage.v3 import type @@ -316,6 +317,53 @@ def test_group_type_delete_group_specs_prop(self): ) +class TestService(TestVolumeProxy): + def test_services(self): + self.verify_list(self.proxy.services, service.Service) + + def test_enable_service(self): + self._verify( + 'openstack.block_storage.v3.service.Service.enable', + self.proxy.enable_service, + method_args=["value"], + expected_args=[self.proxy], + ) + + def test_disable_service(self): + self._verify( + 'openstack.block_storage.v3.service.Service.disable', + self.proxy.disable_service, + method_args=["value"], + expected_kwargs={"reason": None}, + expected_args=[self.proxy], + ) + + def test_thaw_service(self): + self._verify( + 'openstack.block_storage.v3.service.Service.thaw', + self.proxy.thaw_service, + method_args=["value"], + expected_args=[self.proxy], + ) + + def test_freeze_service(self): + self._verify( + 'openstack.block_storage.v3.service.Service.freeze', + self.proxy.freeze_service, + method_args=["value"], + expected_args=[self.proxy], + ) + + def test_failover_service(self): + self._verify( + 'openstack.block_storage.v3.service.Service.failover', + self.proxy.failover_service, + method_args=["value"], + expected_args=[self.proxy], + expected_kwargs={"backend_id": None, "cluster": None}, + ) + + class TestExtension(TestVolumeProxy): def test_extensions(self): self.verify_list(self.proxy.extensions, extension.Extension) diff --git a/openstack/tests/unit/block_storage/v3/test_service.py b/openstack/tests/unit/block_storage/v3/test_service.py new file mode 100644 index 000000000..7f66a17b0 --- /dev/null +++ b/openstack/tests/unit/block_storage/v3/test_service.py @@ -0,0 +1,195 @@ +# 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 unittest import mock + +from openstack.block_storage.v3 import service +from openstack.tests.unit import base + +EXAMPLE = { + "binary": "cinder-scheduler", + "disabled_reason": None, + "host": "devstack", + "state": "up", + "status": "enabled", + "updated_at": "2017-06-29T05:50:35.000000", + "zone": "nova", +} + + +class TestService(base.TestCase): + def setUp(self): + super().setUp() + self.resp = mock.Mock() + self.resp.body = {'service': {}} + self.resp.json = mock.Mock(return_value=self.resp.body) + self.resp.status_code = 200 + self.resp.headers = {} + self.sess = mock.Mock() + self.sess.put = mock.Mock(return_value=self.resp) + self.sess.default_microversion = '3.0' + + def test_basic(self): + sot = service.Service() + self.assertIsNone(sot.resource_key) + self.assertEqual('services', sot.resources_key) + self.assertEqual('/os-services', sot.base_path) + self.assertFalse(sot.allow_commit) + self.assertTrue(sot.allow_list) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_delete) + + self.assertDictEqual( + { + 'binary': 'binary', + 'host': 'host', + 'limit': 'limit', + 'marker': 'marker', + }, + sot._query_mapping._mapping, + ) + + def test_make_it(self): + sot = service.Service(**EXAMPLE) + self.assertEqual(EXAMPLE['binary'], sot.binary) + self.assertEqual(EXAMPLE['binary'], sot.name) + self.assertEqual(EXAMPLE['disabled_reason'], sot.disabled_reason) + self.assertEqual(EXAMPLE['host'], sot.host) + self.assertEqual(EXAMPLE['state'], sot.state) + self.assertEqual(EXAMPLE['status'], sot.status) + self.assertEqual(EXAMPLE['zone'], sot.availability_zone) + + def test_enable(self): + sot = service.Service(**EXAMPLE) + + res = sot.enable(self.sess) + self.assertIsNotNone(res) + + url = 'os-services/enable' + body = { + 'binary': 'cinder-scheduler', + 'host': 'devstack', + } + self.sess.put.assert_called_with( + url, + json=body, + microversion=self.sess.default_microversion, + ) + + def test_disable(self): + sot = service.Service(**EXAMPLE) + + res = sot.disable(self.sess) + self.assertIsNotNone(res) + + url = 'os-services/disable' + body = { + 'binary': 'cinder-scheduler', + 'host': 'devstack', + } + self.sess.put.assert_called_with( + url, + json=body, + microversion=self.sess.default_microversion, + ) + + def test_disable__with_reason(self): + sot = service.Service(**EXAMPLE) + reason = 'fencing' + + res = sot.disable(self.sess, reason=reason) + + self.assertIsNotNone(res) + + url = 'os-services/disable-log-reason' + body = { + 'binary': 'cinder-scheduler', + 'host': 'devstack', + 'disabled_reason': reason, + } + self.sess.put.assert_called_with( + url, + json=body, + microversion=self.sess.default_microversion, + ) + + def test_thaw(self): + sot = service.Service(**EXAMPLE) + + res = sot.thaw(self.sess) + self.assertIsNotNone(res) + + url = 'os-services/thaw' + body = {'host': 'devstack'} + self.sess.put.assert_called_with( + url, + json=body, + microversion=self.sess.default_microversion, + ) + + def test_freeze(self): + sot = service.Service(**EXAMPLE) + + res = sot.freeze(self.sess) + self.assertIsNotNone(res) + + url = 'os-services/freeze' + body = {'host': 'devstack'} + self.sess.put.assert_called_with( + url, + json=body, + microversion=self.sess.default_microversion, + ) + + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=False, + ) + def test_failover(self, mock_supports): + sot = service.Service(**EXAMPLE) + + res = sot.failover(self.sess) + self.assertIsNotNone(res) + + url = 'os-services/failover_host' + body = {'host': 'devstack'} + self.sess.put.assert_called_with( + url, + json=body, + microversion=self.sess.default_microversion, + ) + + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=True, + ) + def test_failover__with_cluster(self, mock_supports): + self.sess.default_microversion = '3.26' + + sot = service.Service(**EXAMPLE) + + res = sot.failover(self.sess, cluster='foo', backend_id='bar') + self.assertIsNotNone(res) + + url = 'os-services/failover' + body = { + 'host': 'devstack', + 'cluster': 'foo', + 'backend_id': 'bar', + } + self.sess.put.assert_called_with( + url, + json=body, + microversion='3.26', + ) diff --git a/releasenotes/notes/add-block-storage-service-support-ce03092ce2d7e7b9.yaml b/releasenotes/notes/add-block-storage-service-support-ce03092ce2d7e7b9.yaml new file mode 100644 index 000000000..95fadb580 --- /dev/null +++ b/releasenotes/notes/add-block-storage-service-support-ce03092ce2d7e7b9.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Added support for block storage services. From 6370f546f75f3cce3d00ea66ca04ceea1a3a619a Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 27 Jul 2023 17:41:38 +0100 Subject: [PATCH 3329/3836] tests: Migrate tests from os-hosts to os-services The former is deprecated. Change-Id: I542fc9774905c7585fdf642fb1801ee2dd57b531 Signed-off-by: Stephen Finucane --- .../block_storage/v3/test_capabilities.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/openstack/tests/functional/block_storage/v3/test_capabilities.py b/openstack/tests/functional/block_storage/v3/test_capabilities.py index 3d419080d..541b4d7c8 100644 --- a/openstack/tests/functional/block_storage/v3/test_capabilities.py +++ b/openstack/tests/functional/block_storage/v3/test_capabilities.py @@ -10,16 +10,20 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import proxy from openstack.tests.functional.block_storage.v3 import base class TestCapabilities(base.BaseBlockStorageTest): + # getting capabilities can be slow + TIMEOUT_SCALING_FACTOR = 1.5 + def test_get(self): - response = proxy._json_response( - self.conn.block_storage.get('/os-hosts') - ) - host = response['hosts'][0]['host_name'] + services = list(self.operator_cloud.block_storage.services()) + host = [ + service + for service in services + if service.binary == 'cinder-volume' + ][0].host sot = self.conn.block_storage.get_capabilities(host) self.assertIn('description', sot) From 7852d95707c87efacd1cf1c346463f3d8eb7ebcf Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 26 May 2023 09:44:58 +0100 Subject: [PATCH 3330/3836] cloud: Remove '_{service}_client' properties These are mostly unused and have been unused since we migrated the cloud layer to use the proxy layer. Remove them. We also remove a number of '_{service}_raw_client' properties but can't remove all of these are there are some callers that still rely on them. Change-Id: I026f5ee286bea6236710d767d5590d52a1e291a5 Signed-off-by: Stephen Finucane --- openstack/cloud/_identity.py | 8 -- openstack/cloud/_image.py | 15 --- openstack/cloud/_object_store.py | 1 + openstack/cloud/_orchestration.py | 1 + openstack/cloud/openstackcloud.py | 125 ----------------------- openstack/tests/unit/cloud/test_image.py | 109 -------------------- 6 files changed, 2 insertions(+), 257 deletions(-) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index 7a8cd3daf..7f66aa2cb 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -20,14 +20,6 @@ class IdentityCloudMixin: identity: Proxy - @property - def _identity_client(self): - if 'identity' not in self._raw_clients: - self._raw_clients['identity'] = self._get_versioned_client( - 'identity', min_version=2, max_version='3.latest' - ) - return self._raw_clients['identity'] - def _get_project_id_param_dict(self, name_or_id): if name_or_id: project = self.get_project(name_or_id) diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py index aafefc9ef..822b97a2b 100644 --- a/openstack/cloud/_image.py +++ b/openstack/cloud/_image.py @@ -35,21 +35,6 @@ class ImageCloudMixin: def __init__(self): self.image_api_use_tasks = self.config.config['image_api_use_tasks'] - @property - def _raw_image_client(self): - if 'raw-image' not in self._raw_clients: - image_client = self._get_raw_client('image') - self._raw_clients['raw-image'] = image_client - return self._raw_clients['raw-image'] - - @property - def _image_client(self): - if 'image' not in self._raw_clients: - self._raw_clients['image'] = self._get_versioned_client( - 'image', min_version=1, max_version='2.latest' - ) - return self._raw_clients['image'] - def search_images(self, name_or_id=None, filters=None): images = self.list_images() return _utils._filter_list(images, name_or_id, filters) diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index 945d4e00f..c7f734334 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -38,6 +38,7 @@ class ObjectStoreCloudMixin: object_store: Proxy + # TODO(stephenfin): Remove final user of this @property def _object_store_client(self): if 'object-store' not in self._raw_clients: diff --git a/openstack/cloud/_orchestration.py b/openstack/cloud/_orchestration.py index 451f1ab77..e994f286f 100644 --- a/openstack/cloud/_orchestration.py +++ b/openstack/cloud/_orchestration.py @@ -33,6 +33,7 @@ def _no_pending_stacks(stacks): class OrchestrationCloudMixin: orchestration: Proxy + # TODO(stephenfin): Remove final user of this @property def _orchestration_client(self): if 'orchestration' not in self._raw_clients: diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 42bcbd102..7c751cb12 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -36,7 +36,6 @@ from openstack import exceptions from openstack import proxy from openstack import utils -from openstack import warnings as os_warnings DEFAULT_SERVER_AGE = 5 DEFAULT_PORT_AGE = 5 @@ -347,109 +346,6 @@ def _get_cache(self, resource_name): else: return self._cache - def _get_major_version_id(self, version): - if isinstance(version, int): - return version - elif isinstance(version, (str, tuple)): - return int(version[0]) - return version - - def _get_versioned_client( - self, service_type, min_version=None, max_version=None - ): - config_version = self.config.get_api_version(service_type) - config_major = self._get_major_version_id(config_version) - max_major = self._get_major_version_id(max_version) - min_major = self._get_major_version_id(min_version) - # TODO(shade) This should be replaced with use of Connection. However, - # we need to find a sane way to deal with this additional - # logic - or we need to give up on it. If we give up on it, - # we need to make sure we can still support it in the shade - # compat layer. - # NOTE(mordred) This logic for versions is slightly different - # than the ksa Adapter constructor logic. openstack.cloud knows the - # versions it knows, and uses them when it detects them. However, if - # a user requests a version, and it's not found, and a different one - # openstack.cloud does know about is found, that's a warning in - # openstack.cloud. - if config_version: - if min_major and config_major < min_major: - raise exc.OpenStackCloudException( - "Version {config_version} requested for {service_type}" - " but shade understands a minimum of {min_version}".format( - config_version=config_version, - service_type=service_type, - min_version=min_version, - ) - ) - elif max_major and config_major > max_major: - raise exc.OpenStackCloudException( - "Version {config_version} requested for {service_type}" - " but openstack.cloud understands a maximum of" - " {max_version}".format( - config_version=config_version, - service_type=service_type, - max_version=max_version, - ) - ) - request_min_version = config_version - request_max_version = '{version}.latest'.format( - version=config_major - ) - adapter = proxy.Proxy( - session=self.session, - service_type=self.config.get_service_type(service_type), - service_name=self.config.get_service_name(service_type), - interface=self.config.get_interface(service_type), - endpoint_override=self.config.get_endpoint(service_type), - region_name=self.config.get_region_name(service_type), - statsd_prefix=self.config.get_statsd_prefix(), - statsd_client=self.config.get_statsd_client(), - prometheus_counter=self.config.get_prometheus_counter(), - prometheus_histogram=self.config.get_prometheus_histogram(), - influxdb_client=self.config.get_influxdb_client(), - min_version=request_min_version, - max_version=request_max_version, - ) - if adapter.get_endpoint(): - return adapter - - adapter = proxy.Proxy( - session=self.session, - service_type=self.config.get_service_type(service_type), - service_name=self.config.get_service_name(service_type), - interface=self.config.get_interface(service_type), - endpoint_override=self.config.get_endpoint(service_type), - region_name=self.config.get_region_name(service_type), - min_version=min_version, - max_version=max_version, - ) - - # data.api_version can be None if no version was detected, such - # as with neutron - api_version = adapter.get_api_major_version( - endpoint_override=self.config.get_endpoint(service_type) - ) - api_major = self._get_major_version_id(api_version) - - # If we detect a different version that was configured, warn the user. - # openstacksdk still knows what to do - but if the user gave us an - # explicit version and we couldn't find it, they may want to - # investigate - if api_version and config_version and (api_major != config_major): - api_version_str = '.'.join([str(f) for f in api_version]) - warning_msg = ( - f'{service_type} is configured for {config_version} but only ' - f'{api_version_str} is available. openstacksdk is happy ' - f'with this version, but if you were trying to force an ' - f'override, that did not happen. You may want to check ' - f'your cloud, or remove the version specification from ' - f'your config.' - ) - self.log.debug(warning_msg) - warnings.warn(warning_msg, os_warnings.OpenStackDeprecationWarning) - return adapter - # TODO(shade) This should be replaced with using openstack Connection # object. def _get_raw_client( @@ -465,27 +361,6 @@ def _get_raw_client( region_name=self.config.get_region_name(service_type), ) - @property - def _application_catalog_client(self): - if 'application-catalog' not in self._raw_clients: - self._raw_clients['application-catalog'] = self._get_raw_client( - 'application-catalog' - ) - return self._raw_clients['application-catalog'] - - @property - def _database_client(self): - if 'database' not in self._raw_clients: - self._raw_clients['database'] = self._get_raw_client('database') - return self._raw_clients['database'] - - @property - def _raw_image_client(self): - if 'raw-image' not in self._raw_clients: - image_client = self._get_raw_client('image') - self._raw_clients['raw-image'] = image_client - return self._raw_clients['raw-image'] - def pprint(self, resource): """Wrapper around pprint that groks munch objects""" # import late since this is a utility function diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 158a41442..122f0b8af 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -66,26 +66,6 @@ def setUp(self): super(TestImage, self).setUp() self.use_glance() - def test_config_v1(self): - self.cloud.config.config['image_api_version'] = '1' - # We override the scheme of the endpoint with the scheme of the service - # because glance has a bug where it doesn't return https properly. - self.assertEqual( - 'https://image.example.com/v1/', - self.cloud._image_client.get_endpoint(), - ) - self.assertEqual('1', self.cloud_config.get_api_version('image')) - - def test_config_v2(self): - self.cloud.config.config['image_api_version'] = '2' - # We override the scheme of the endpoint with the scheme of the service - # because glance has a bug where it doesn't return https properly. - self.assertEqual( - 'https://image.example.com/v2/', - self.cloud._image_client.get_endpoint(), - ) - self.assertEqual('2', self.cloud_config.get_api_version('image')) - def test_download_image_no_output(self): self.assertRaises( exc.OpenStackCloudException, @@ -1924,66 +1904,6 @@ def test_list_images_paginated(self): self.assert_calls() -class TestImageV1Only(base.TestCase): - def setUp(self): - super(TestImageV1Only, self).setUp() - self.use_glance(image_version_json='image-version-v1.json') - - def test_config_v1(self): - self.cloud.config.config['image_api_version'] = '1' - # We override the scheme of the endpoint with the scheme of the service - # because glance has a bug where it doesn't return https properly. - self.assertEqual( - 'https://image.example.com/v1/', - self.cloud._image_client.get_endpoint(), - ) - self.assertEqual( - self.cloud._image_client.get_api_major_version()[0], 1 - ) - - def test_config_v2(self): - self.cloud.config.config['image_api_version'] = '2' - # We override the scheme of the endpoint with the scheme of the service - # because glance has a bug where it doesn't return https properly. - self.assertEqual( - 'https://image.example.com/v1/', - self.cloud._image_client.get_endpoint(), - ) - self.assertNotEqual( - self.cloud._image_client.get_api_major_version()[0], 2 - ) - - -class TestImageV2Only(base.TestCase): - def setUp(self): - super(TestImageV2Only, self).setUp() - self.use_glance(image_version_json='image-version-v2.json') - - def test_config_v1(self): - self.cloud.config.config['image_api_version'] = '1' - # We override the scheme of the endpoint with the scheme of the service - # because glance has a bug where it doesn't return https properly. - self.assertEqual( - 'https://image.example.com/v2/', - self.cloud._image_client.get_endpoint(), - ) - self.assertEqual( - self.cloud._image_client.get_api_major_version()[0], 2 - ) - - def test_config_v2(self): - self.cloud.config.config['image_api_version'] = '2' - # We override the scheme of the endpoint with the scheme of the service - # because glance has a bug where it doesn't return https properly. - self.assertEqual( - 'https://image.example.com/v2/', - self.cloud._image_client.get_endpoint(), - ) - self.assertEqual( - self.cloud._image_client.get_api_major_version()[0], 2 - ) - - class TestImageVolume(BaseTestImage): def setUp(self): super(TestImageVolume, self).setUp() @@ -2087,32 +2007,3 @@ def test_create_image_volume_duplicate(self): ) self.assert_calls() - - -class TestImageBrokenDiscovery(base.TestCase): - def setUp(self): - super(TestImageBrokenDiscovery, self).setUp() - self.use_glance(image_version_json='image-version-broken.json') - - def test_url_fix(self): - # image-version-broken.json has both http urls and localhost as the - # host. This is testing that what is discovered is https, because - # that's what's in the catalog, and image.example.com for the same - # reason. - self.register_uris( - [ - dict( - method='GET', - uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2' - ), - json={'images': []}, - ) - ] - ) - self.assertEqual([], self.cloud.list_images()) - self.assertEqual( - self.cloud._image_client.get_endpoint(), - 'https://image.example.com/v2/', - ) - self.assert_calls() From cc682b914c03cc30fa1f91610ca7bc51d49ddeac Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 27 Jul 2023 19:00:34 +0100 Subject: [PATCH 3331/3836] docs: Improve docs for Connection with CONF object This came up recently and I found the documentation a little thin on the ground. Fix it. Change-Id: I897325032ee7b0f559906e82be7f3a7695768c52 Signed-off-by: Stephen Finucane --- openstack/connection.py | 55 +++++++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/openstack/connection.py b/openstack/connection.py index 5722a01e2..5b6f12759 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -30,8 +30,11 @@ might exist inside of an OpenStack service operational context. * Using an existing :class:`~openstack.config.cloud_region.CloudRegion`. +Creating the Connection +----------------------- + Using config settings ---------------------- +~~~~~~~~~~~~~~~~~~~~~ For users who want to create a :class:`~openstack.connection.Connection` making use of named clouds in ``clouds.yaml`` files, ``OS_`` environment variables @@ -57,8 +60,8 @@ options = argparse.ArgumentParser(description='Awesome OpenStack App') conn = openstack.connect(options=options) -Using Only Keyword Arguments ----------------------------- +Using only keyword arguments +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If the application wants to avoid loading any settings from ``clouds.yaml`` or environment variables, use the :class:`~openstack.connection.Connection` @@ -79,14 +82,16 @@ conn = connection.Connection( region_name='example-region', - auth=dict( - auth_url='https://auth.example.com', - username='amazing-user', - password='super-secret-password', - project_id='33aa1afc-03fe-43b8-8201-4e0d3b4b8ab5', - user_domain_id='054abd68-9ad9-418b-96d3-3437bb376703'), + auth={ + 'auth_url': 'https://auth.example.com', + 'username': 'amazing-user', + 'password': 'super-secret-password', + 'project_id': '33aa1afc-03fe-43b8-8201-4e0d3b4b8ab5', + 'user_domain_id': '054abd68-9ad9-418b-96d3-3437bb376703' + }, compute_api_version='2', - identity_interface='internal') + identity_interface='internal', + ) Per-service settings as needed by `keystoneauth1.adapter.Adapter` such as ``api_version``, ``service_name``, and ``interface`` can be set, as seen @@ -96,7 +101,7 @@ service. From existing authenticated Session ------------------------------------ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ For applications that already have an authenticated Session, simply passing it to the :class:`~openstack.connection.Connection` constructor is all that @@ -110,10 +115,11 @@ session=session, region_name='example-region', compute_api_version='2', - identity_interface='internal') + identity_interface='internal', + ) From oslo.conf CONF object --------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~ For applications that have an oslo.config ``CONF`` object that has been populated with ``keystoneauth1.loading.register_adapter_conf_options`` in @@ -127,14 +133,29 @@ .. code-block:: python + from keystoneauth1 import loading as ks_loading + from oslo_config import cfg from openstack import connection + CONF = cfg.CONF + + group = cfg.OptGroup('neutron') + ks_loading.register_session_conf_options(CONF, group) + ks_loading.register_auth_conf_options(CONF, group) + ks_loading.register_adapter_conf_options(CONF, group) + + CONF() + + auth = ks_loading.load_auth_from_conf_options(CONF, 'neutron') + sess = ks_loading.load_session_from_conf_options(CONF, 'neutron', auth=auth) + conn = connection.Connection( session=session, - oslo_conf=CONF) + oslo_conf=CONF, + ) From existing CloudRegion -------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~ If you already have an :class:`~openstack.config.cloud_region.CloudRegion` you can pass it in instead: @@ -145,7 +166,9 @@ import openstack.config config = openstack.config.get_cloud_region( - cloud='example', region_name='earth') + cloud='example', + region_name='earth', + ) conn = connection.Connection(config=config) Using the Connection From bbc378ec18d4fd359f78c1aa21ba0534d932ffb5 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 28 Jul 2023 11:10:09 +0100 Subject: [PATCH 3332/3836] Rename share group snapshot 'members' method This conflicts with a similarly named attribute on the resource. Some doc issues are also addressed. Change-Id: I63db4f45d6e088e660833093a4a586cbcbdb94ff Signed-off-by: Stephen Finucane --- .../shared_file_system/v2/share_group_snapshot.rst | 6 +++--- openstack/shared_file_system/v2/_proxy.py | 7 ++++--- openstack/shared_file_system/v2/share_group_snapshot.py | 2 +- .../shared_file_system/v2/test_share_group_snapshot.py | 6 ++++-- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/doc/source/user/resources/shared_file_system/v2/share_group_snapshot.rst b/doc/source/user/resources/shared_file_system/v2/share_group_snapshot.rst index 54fb704e9..40972ad93 100644 --- a/doc/source/user/resources/shared_file_system/v2/share_group_snapshot.rst +++ b/doc/source/user/resources/shared_file_system/v2/share_group_snapshot.rst @@ -1,13 +1,13 @@ openstack.shared_file_system.v2.share_group_snapshot ==================================================== -.. automodule: : openstack.shared_file_system.v2.share_group_snapshot +.. automodule:: openstack.shared_file_system.v2.share_group_snapshot The ShareGroupSnapshot Class ---------------------------- The ``ShareGroupSnapshot`` class inherits from -: class: `~openstack.resource.Resource`. +:class:`~openstack.resource.Resource`. -.. autoclass: : openstack.shared_file_system.v2.ShareGroupSnapshot +.. autoclass:: openstack.shared_file_system.v2.share_group_snapshot.ShareGroupSnapshot :members: diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index 4392c798f..4f8f93abd 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -860,12 +860,13 @@ def share_group_snapshot_members(self, group_snapshot_id): :param group_snapshot_id: The ID of the group snapshot to get :returns: List of the share group snapshot members, which are share snapshots. - :rtype: :dict: Attributes of the share snapshots. + :rtype: dict containing attributes of the share snapshot members. """ res = self._get( - _share_group_snapshot.ShareGroupSnapshot, group_snapshot_id + _share_group_snapshot.ShareGroupSnapshot, + group_snapshot_id, ) - response = res.members(self) + response = res.get_members(self) return response def get_share_group_snapshot(self, group_snapshot_id): diff --git a/openstack/shared_file_system/v2/share_group_snapshot.py b/openstack/shared_file_system/v2/share_group_snapshot.py index 366a52275..85b86ab95 100644 --- a/openstack/shared_file_system/v2/share_group_snapshot.py +++ b/openstack/shared_file_system/v2/share_group_snapshot.py @@ -76,7 +76,7 @@ def reset_status(self, session, status): body = {"reset_status": {"status": status}} self._action(session, body) - def members(self, session, microversion=None): + def get_members(self, session, microversion=None): url = utils.urljoin(self.base_path, self.id, 'members') microversion = microversion or self._get_microversion( session, action='list' diff --git a/openstack/tests/unit/shared_file_system/v2/test_share_group_snapshot.py b/openstack/tests/unit/shared_file_system/v2/test_share_group_snapshot.py index 65be62157..92880f57c 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_share_group_snapshot.py +++ b/openstack/tests/unit/shared_file_system/v2/test_share_group_snapshot.py @@ -96,9 +96,11 @@ def test_reset_status(self): def test_get_members(self): sot = share_group_snapshot.ShareGroupSnapshot(**EXAMPLE) - sot.members(self.sess) + sot.get_members(self.sess) url = f'share-group-snapshots/{IDENTIFIER}/members' headers = {'Accept': ''} self.sess.get.assert_called_with( - url, headers=headers, microversion=self.sess.default_microversion + url, + headers=headers, + microversion=self.sess.default_microversion, ) From 82ee4444fba9e3b096bc1fe15754d1f2879ad194 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 26 May 2023 12:33:42 +0100 Subject: [PATCH 3333/3836] cloud: Remove '_object_store_client' Replace the use of a raw client with a REST request using our proxy clients. These things aren't identical since the use of the proxy clients necessitates a whole load of version negotiation but it's close enough and let's us dump the whole raw client idea. Note that we need to do a little manipulation of some tests. We had tests that were manipulating 'time.time()'. This was conflicting with the rate limiting semaphore provided in keystoneauth. By mocking a wrapper function instead, we can make our mock a little more targeted. Change-Id: I39d171e4456ea491577cb46216c321da2c9d2d19 Signed-off-by: Stephen Finucane --- openstack/cloud/_object_store.py | 10 +--------- openstack/object_store/v1/_proxy.py | 8 ++++++-- openstack/tests/unit/cloud/test_image.py | 6 +++--- openstack/tests/unit/cloud/test_object.py | 24 +++++++++-------------- 4 files changed, 19 insertions(+), 29 deletions(-) diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index c7f734334..1c45b0f28 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -38,14 +38,6 @@ class ObjectStoreCloudMixin: object_store: Proxy - # TODO(stephenfin): Remove final user of this - @property - def _object_store_client(self): - if 'object-store' not in self._raw_clients: - raw_client = self._get_raw_client('object-store') - self._raw_clients['object-store'] = raw_client - return self._raw_clients['object-store'] - # TODO(stephenfin): Remove 'full_listing' as it's a noop def list_containers(self, full_listing=True, prefix=None): """List containers. @@ -421,7 +413,7 @@ def get_object_raw(self, container, obj, query_string=None, stream=False): :raises: OpenStackCloudException on operation error. """ endpoint = self._get_object_endpoint(container, obj, query_string) - return self._object_store_client.get(endpoint, stream=stream) + return self.object_store.get(endpoint, stream=stream) def _get_object_endpoint(self, container, obj=None, query_string=None): endpoint = urllib.parse.quote(container) diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index fae81678d..1c6b0f79c 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -35,6 +35,10 @@ SHORT_EXPIRES_ISO8601_FORMAT = '%Y-%m-%d' +def _get_expiration(expiration): + return int(time.time() + expiration) + + class Proxy(proxy.Proxy): _resource_registry = { "account": _account.Account, @@ -913,7 +917,7 @@ def generate_form_signature( raise exceptions.SDKException( 'Please use a positive value.' ) - expires = int(time.time() + int(timeout)) + expires = _get_expiration(timeout) temp_url_key = self._check_temp_url_key( container=container, temp_url_key=temp_url_key @@ -1042,7 +1046,7 @@ def generate_temp_url( ) if not absolute: - expiration = int(time.time() + timestamp) + expiration = _get_expiration(timestamp) else: expiration = timestamp diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 122f0b8af..889a28d32 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -755,7 +755,7 @@ def test_create_image_use_import(self): def test_create_image_task(self): self.cloud.image_api_use_tasks = True - endpoint = self.cloud._object_store_client.get_endpoint() + endpoint = self.cloud.object_store.get_endpoint() task_id = str(uuid.uuid4()) args = dict( @@ -1034,7 +1034,7 @@ def test_delete_autocreated_no_tasks(self): def test_delete_image_task(self): self.cloud.image_api_use_tasks = True - endpoint = self.cloud._object_store_client.get_endpoint() + endpoint = self.cloud.object_store.get_endpoint() object_path = self.fake_image_dict['owner_specified.openstack.object'] @@ -1096,7 +1096,7 @@ def test_delete_image_task(self): def test_delete_autocreated_image_objects(self): self.use_keystone_v3() self.cloud.image_api_use_tasks = True - endpoint = self.cloud._object_store_client.get_endpoint() + endpoint = self.cloud.object_store.get_endpoint() other_image = self.getUniqueString('no-delete') self.register_uris( diff --git a/openstack/tests/unit/cloud/test_object.py b/openstack/tests/unit/cloud/test_object.py index 054108752..643b1346a 100644 --- a/openstack/tests/unit/cloud/test_object.py +++ b/openstack/tests/unit/cloud/test_object.py @@ -34,7 +34,7 @@ def setUp(self): self.container = self.getUniqueString() self.object = self.getUniqueString() - self.endpoint = self.cloud._object_store_client.get_endpoint() + self.endpoint = self.cloud.object_store.get_endpoint() self.container_endpoint = '{endpoint}/{container}'.format( endpoint=self.endpoint, container=self.container ) @@ -373,10 +373,8 @@ def test_list_containers_exception(self): ) self.assert_calls() - @mock.patch('time.time', autospec=True) - def test_generate_form_signature_container_key(self, mock_time): - mock_time.return_value = 12345 - + @mock.patch.object(_proxy, '_get_expiration', return_value=13345) + def test_generate_form_signature_container_key(self, mock_expiration): self.register_uris( [ dict( @@ -411,10 +409,8 @@ def test_generate_form_signature_container_key(self, mock_time): ) self.assert_calls() - @mock.patch('time.time', autospec=True) - def test_generate_form_signature_account_key(self, mock_time): - mock_time.return_value = 12345 - + @mock.patch.object(_proxy, '_get_expiration', return_value=13345) + def test_generate_form_signature_account_key(self, mock_expiration): self.register_uris( [ dict( @@ -455,10 +451,8 @@ def test_generate_form_signature_account_key(self, mock_time): ) self.assert_calls() - @mock.patch('time.time') - def test_generate_form_signature_key_argument(self, mock_time): - mock_time.return_value = 12345 - + @mock.patch.object(_proxy, '_get_expiration', return_value=13345) + def test_generate_form_signature_key_argument(self, mock_expiration): self.assertEqual( (13345, '1c283a05c6628274b732212d9a885265e6f67b63'), self.cloud.object_store.generate_form_signature( @@ -895,8 +889,8 @@ def setUp(self): self.object_file = tempfile.NamedTemporaryFile(delete=False) self.object_file.write(self.content) self.object_file.close() - (self.md5, self.sha256) = utils._get_file_hashes(self.object_file.name) - self.endpoint = self.cloud._object_store_client.get_endpoint() + self.md5, self.sha256 = utils._get_file_hashes(self.object_file.name) + self.endpoint = self.cloud.object_store.get_endpoint() def test_create_object(self): self.register_uris( From 84f28505826cfedf2419eb1bcae4a59b39c08ac9 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 25 Jul 2023 13:17:39 +0100 Subject: [PATCH 3334/3836] tox: Add environment documentation This should be helpful for new contributors. Change-Id: I91c14bbaeb61bdf7026015a3eb3f21edae8ec2a9 Signed-off-by: Stephen Finucane --- tox.ini | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 57cca3461..36fd20eb3 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,8 @@ minversion = 4.3.0 envlist = pep8,py3 [testenv] -usedevelop = true +description = + Run unit tests. passenv = OS_* OPENSTACKSDK_* @@ -23,6 +24,8 @@ commands = stestr slowest [testenv:functional{,-py36,-py37,-py38,-py39}] +description = + Run functional tests. # Some jobs (especially heat) takes longer, therefore increase default timeout # This timeout should not be smaller, than the longest individual timeout setenv = @@ -37,6 +40,8 @@ commands = # Acceptance tests are the ones running on real clouds [testenv:acceptance-regular-user] +description = + Run acceptance tests. # This env intends to test functions of a regular user without admin privileges # Some jobs (especially heat) takes longer, therefore increase default timeout # This timeout should not be smaller, than the longest individual timeout @@ -53,12 +58,16 @@ commands = stestr slowest [testenv:pep8] +description = + Run style checks. deps = pre-commit commands = pre-commit run --all-files --show-diff-on-failure [testenv:venv] +description = + Run specified command in a virtual environment with all dependencies installed. deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/test-requirements.txt @@ -67,6 +76,8 @@ deps = commands = {posargs} [testenv:debug] +description = + Run specified tests through oslo_debug_helper, which allows use of pdb. # allow 1 year, or 31536000 seconds, to debug a test before it times out setenv = OS_TEST_TIMEOUT=31536000 @@ -76,6 +87,8 @@ commands = oslo_debug_helper -t openstack/tests {posargs} [testenv:cover] +description = + Run unit tests and generate coverage report. setenv = {[testenv]setenv} PYTHON=coverage run --source openstack --parallel-mode @@ -86,6 +99,8 @@ commands = coverage xml -o cover/coverage.xml [testenv:ansible] +description = + Run ansible tests. # Need to pass some env vars for the Ansible playbooks passenv = HOME @@ -97,6 +112,8 @@ deps = commands = {toxinidir}/extras/run-ansible-tests.sh -e {envdir} {posargs} [testenv:docs] +description = + Build documentation in HTML format. deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/doc/requirements.txt @@ -104,6 +121,8 @@ commands = sphinx-build -W --keep-going -b html -j auto doc/source/ doc/build/html [testenv:pdf-docs] +description = + Build documentation in PDF format. deps = {[testenv:docs]deps} allowlist_externals = make @@ -112,6 +131,8 @@ commands = make -C doc/build/pdf [testenv:releasenotes] +description = + Build release note documentation in HTML format. deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/doc/requirements.txt From d7cb495b23f5252610aaa72424129ba0798185cf Mon Sep 17 00:00:00 2001 From: Goutham Pacha Ravi Date: Fri, 28 Jul 2023 17:09:46 -0700 Subject: [PATCH 3335/3836] Fix manila access rules functional tests We recently [1] changed the behavior of the share access rule deletion method to return the response code to end users. The functional tests were asserting prior behavior and hence failing. [1] https://review.opendev.org/c/openstack/openstacksdk/+/889328 Change-Id: I03efeecc5f6384bd57655bc86146f10dd500d6b1 --- .../functional/shared_file_system/test_share_access_rule.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openstack/tests/functional/shared_file_system/test_share_access_rule.py b/openstack/tests/functional/shared_file_system/test_share_access_rule.py index a45ebf5db..d8dc4d85b 100644 --- a/openstack/tests/functional/shared_file_system/test_share_access_rule.py +++ b/openstack/tests/functional/shared_file_system/test_share_access_rule.py @@ -46,11 +46,9 @@ def setUp(self): self.RESOURCE_KEY = access_rule.resource_key def tearDown(self): - acr = self.user_cloud.share.delete_access_rule( + self.user_cloud.share.delete_access_rule( self.ACCESS_ID, self.SHARE_ID, ignore_missing=True ) - - self.assertIsNone(acr) super(ShareAccessRuleTest, self).tearDown() def test_get_access_rule(self): From 90640d630dbe4e98d2667138a9574f4e913309ea Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 3 Aug 2023 10:35:33 +0200 Subject: [PATCH 3336/3836] fix block storage resource registry in the resource registry grouptype entry accidentially points to the module and not to the exact class. Change-Id: Ib19139c4c29236778ef4a857595c27444f738240 --- openstack/block_storage/v3/_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index b729f9720..6c0591fb2 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -39,7 +39,7 @@ class Proxy(_base_proxy.BaseBlockStorageProxy): "extension": _extension.Extension, "group": _group.Group, "group_snapshot": _group_snapshot.GroupSnapshot, - "group_type": _group_type, + "group_type": _group_type.GroupType, "limits": _limits.Limit, "quota_set": _quota_set.QuotaSet, "resource_filter": _resource_filter.ResourceFilter, From 460cee1f5000f908ba0dea990a903685013cf86a Mon Sep 17 00:00:00 2001 From: Christian Rohmann Date: Thu, 3 Aug 2023 14:13:24 +0200 Subject: [PATCH 3337/3836] Cleanup logic to either dry-run, bulk_delete or do single object deletes By moving the dry-run switch for project cleanup object-storage deletion to its own condition there are no more side effects with dry-running also always switching to bulk_delete. Story: 2010862 Task: 48538 Change-Id: I2990f5c5366df17888f3dbada1143972dd2c00a7 --- openstack/object_store/v1/_proxy.py | 31 +++++++++++++++-------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index 1c6b0f79c..61bec6831 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -1169,18 +1169,20 @@ def _service_cleanup( resource_evaluation_fn=resource_evaluation_fn, ) if need_delete: - if not is_bulk_delete_supported and not dry_run: - self.delete_object(obj, cont) - else: + if dry_run: + continue + elif is_bulk_delete_supported: elements.append(f"{cont.name}/{obj.name}") if len(elements) >= bulk_delete_max_per_request: - self._bulk_delete(elements, dry_run=dry_run) + self._bulk_delete(elements) elements.clear() + else: + self.delete_object(obj, cont) else: objects_remaining = True if len(elements) > 0: - self._bulk_delete(elements, dry_run=dry_run) + self._bulk_delete(elements) elements.clear() # Eventually delete container itself @@ -1195,14 +1197,13 @@ def _service_cleanup( resource_evaluation_fn=resource_evaluation_fn, ) - def _bulk_delete(self, elements, dry_run=False): + def _bulk_delete(self, elements): data = "\n".join([parse.quote(x) for x in elements]) - if not dry_run: - self.delete( - "?bulk-delete", - data=data, - headers={ - 'Content-Type': 'text/plain', - 'Accept': 'application/json', - }, - ) + self.delete( + "?bulk-delete", + data=data, + headers={ + 'Content-Type': 'text/plain', + 'Accept': 'application/json', + }, + ) From 8ee8f57a434ee42c5daa12b556066244dcf2af73 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 3 Aug 2023 17:36:47 +0100 Subject: [PATCH 3338/3836] Stop randomly sorting error lines When we receive a HTML formatted error page e.g. from Apache, we convert the HTML to plain text that we can output. This conversion effectively amounts to stripping all the HTML and newline characters from the body of the page and then removing duplicated lines. This last step is proving problematic. Way back when, we did this the "dumb" way, by having a list to store lines and only adding lines to this list if they were not already present. This changed in change I7b46e263a76d84573bdfbbece57b1048764ed939 when we switched to calling set() on the generated list. However, sets are unordered which means we end up with confusing, nonsensical error message in this case. For example, given the following error page: 502 Bad Gateway

Bad Gateway

The proxy server received an invalid response from an upstream server.

Additionally, a 201 Created error was encountered while trying to use an ErrorDocument to handle the request.


Apache/2.4.52 (Ubuntu) Server at 10.0.110.85 Port 80
We would expect to see the following: 502 Bad Gateway: Bad Gateway: The proxy server received an invalid: response from an upstream server.: Additionally, a 201 Created: error was encountered while trying to use an ErrorDocument to handle the request.: Apache/2.4.52 (Ubuntu) Server at 10.0.110.85 Port 80 Instead, we're getting: Apache/2.4.52 (Ubuntu) Server at 10.0.110.85 Port 80: error was encountered while trying to use an ErrorDocument to handle the request.: Additionally, a 201 Created: The proxy server received an invalid: response from an upstream server.: 502 Bad Gateway: Bad Gateway Which is total nonsense. Fix this by iterating as we used to, rather than relying on unsorted sets. PS: We also change variable names to keep mypy happy, since we'd like to integrate that soon. Change-Id: I52c1321e00aff1b2dfeaad2adfd4c02455b6eda7 Signed-off-by: Stephen Finucane --- openstack/exceptions.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openstack/exceptions.py b/openstack/exceptions.py index 0608746ef..93a4b9d06 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -246,13 +246,13 @@ def raise_from_response(response, error_message=None): except Exception: details = response.text elif response.content and 'text/html' in content_type: - # Split the lines, strip whitespace and inline HTML from the response. - details = [ - re.sub(r'<.+?>', '', i.strip()) for i in response.text.splitlines() - ] - details = list(set([msg for msg in details if msg])) + messages = [] + for line in response.text.splitlines(): + message = re.sub(r'<.+?>', '', line.strip()) + if message not in messages: + messages.append(message) # Return joined string separated by colons. - details = ': '.join(details) + details = ': '.join(messages) if not details: details = response.reason if response.reason else response.text From bb4f5f1e77bd5dcdaf9ddb1d280f8f52f4c701f5 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 3 Aug 2023 18:12:22 +0100 Subject: [PATCH 3339/3836] tests: Skip intermittently failing placement traits test I have nothing to show for my debugging efforts so far. Let's skip this since we're hitting it relatively frequently at the moment. Change-Id: Iaed95378d628a0d9d94662addd47804e47364b13 Signed-off-by: Stephen Finucane --- openstack/tests/functional/placement/v1/test_trait.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openstack/tests/functional/placement/v1/test_trait.py b/openstack/tests/functional/placement/v1/test_trait.py index 923f88c1e..b06586d0f 100644 --- a/openstack/tests/functional/placement/v1/test_trait.py +++ b/openstack/tests/functional/placement/v1/test_trait.py @@ -20,6 +20,12 @@ class TestTrait(base.BaseFunctionalTest): def setUp(self): super().setUp() + self.skipTest( + "This test intermittently fails on DevStack deployments. " + "See https://bugs.launchpad.net/placement/+bug/2029520 for more " + "information." + ) + if not self.operator_cloud.has_service('placement'): self.skipTest('placement service not supported by cloud') From 8f9aa3b7e2abf60e427bd73ceb44b2322154874f Mon Sep 17 00:00:00 2001 From: Felix Huettner Date: Tue, 8 Aug 2023 12:04:36 +0200 Subject: [PATCH 3340/3836] fix memory leak of Connections during `__init__` of a `Connection` we register an `atexit` handler to call the `close` method when the python interpreter exits. This should ensure that connections are closed reliably. However when a connection is closed earlier (e.g. by using the context manager of `Connection`) `close` is already called. However the `atexit` handler is not unregistered. This prevents python garbage collector from actually collecting the `Connection` since there is still an active reference to it. To fix this we unregister the `atexit` handler if `close` is called. Change-Id: Id02a495353e1bfef3785248e48dec7344ef8f5af --- openstack/connection.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openstack/connection.py b/openstack/connection.py index 5b6f12759..430666c16 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -577,6 +577,7 @@ def close(self): self.config.set_auth_cache() if self.__pool_executor: self.__pool_executor.shutdown() + atexit.unregister(self.close) def set_global_request_id(self, global_request_id): self._global_request_id = global_request_id From 7b5356f32de2a0cd9bde411c008ec612df5c0f99 Mon Sep 17 00:00:00 2001 From: Anvi Joshi Date: Wed, 12 Jul 2023 21:38:51 +0000 Subject: [PATCH 3341/3836] Support manage/unmanage shares with manila Change-Id: I95b72434a8a44f737f5ee9a44b6ecbf8f4162a7a --- doc/source/user/guides/shared_file_system.rst | 18 ++++++ .../user/proxies/shared_file_system.rst | 3 +- examples/shared_file_system/shares.py | 23 ++++++++ openstack/shared_file_system/v2/_proxy.py | 36 ++++++++++++ openstack/shared_file_system/v2/share.py | 51 +++++++++++++++++ .../shared_file_system/test_share.py | 57 +++++++++++++++++++ .../unit/shared_file_system/v2/test_share.py | 47 +++++++++++++++ ...anage-unmanage-share-830e313f96e5fd2b.yaml | 5 ++ 8 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-shared-file-system-manage-unmanage-share-830e313f96e5fd2b.yaml diff --git a/doc/source/user/guides/shared_file_system.rst b/doc/source/user/guides/shared_file_system.rst index a2e300869..dc6b5d021 100644 --- a/doc/source/user/guides/shared_file_system.rst +++ b/doc/source/user/guides/shared_file_system.rst @@ -180,3 +180,21 @@ specify multiple keys to be deleted. .. literalinclude:: ../examples/shared_file_system/share_metadata.py :pyobject: delete_share_metadata + + +Manage Share +------------ + +Manage a share with Manila. + +.. literalinclude:: ../examples/shared_file_system/shares.py + :pyobject: manage_share + + +Unmanage Share +-------------- + +Unmanage a share from Manila. + +.. literalinclude:: ../examples/shared_file_system/shares.py + :pyobject: unmanage_share diff --git a/doc/source/user/proxies/shared_file_system.rst b/doc/source/user/proxies/shared_file_system.rst index 5cdaab1af..facbe03b2 100644 --- a/doc/source/user/proxies/shared_file_system.rst +++ b/doc/source/user/proxies/shared_file_system.rst @@ -33,7 +33,8 @@ service. .. autoclass:: openstack.shared_file_system.v2._proxy.Proxy :noindex: :members: shares, get_share, delete_share, update_share, create_share, - revert_share_to_snapshot, resize_share, find_share + revert_share_to_snapshot, resize_share, find_share, manage_share, + unmanage_share Shared File System Storage Pools diff --git a/examples/shared_file_system/shares.py b/examples/shared_file_system/shares.py index 9328b5048..5f274ed7d 100644 --- a/examples/shared_file_system/shares.py +++ b/examples/shared_file_system/shares.py @@ -31,3 +31,26 @@ def resize_shares_without_shrink(conn, min_size): # Extend shares smaller than min_size to min_size, # but don't shrink shares larger than min_size. conn.share.resize_share(share.id, min_size, no_shrink=True) + + +def manage_share(conn, protocol, export_path, service_host, **params): + # Manage a share with the given protocol, export path, service host, and + # optional additional parameters + managed_share = conn.share.manage_share( + protocol, export_path, service_host, **params + ) + + # Can get the ID of the share, which is now being managed with Manila + managed_share_id = managed_share.id + print("The ID of the share which was managed: %s", managed_share_id) + + +def unmanage_share(conn, share_id): + # Unmanage the share with the given share ID + conn.share.unmanage_share(share_id) + + try: + # Getting the share will raise an exception as it has been unmanaged + conn.share.get_share(share_id) + except Exception: + pass diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index af6f1e663..76eae7557 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -187,6 +187,42 @@ def revert_share_to_snapshot(self, share_id, snapshot_id): res = self._get(_share.Share, share_id) res.revert_to_snapshot(self, snapshot_id) + def manage_share(self, protocol, export_path, service_host, **params): + """Manage a share. + + :param str protocol: The shared file systems protocol of this share. + :param str export_path: The export path formatted according to the + protocol. + :param str service_host: The manage-share service host. + :param kwargs params: Optional parameters to be sent. Available + parameters include: + * name: The user defined name of the resource. + * share_type: The name or ID of the share type to be used to create + the resource. + * driver_options: A set of one or more key and value pairs, as a + dictionary of strings, that describe driver options. + * is_public: The level of visibility for the share. + * description: The user defiend description of the resource. + * share_server_id: The UUID of the share server. + + :returns: The share that was managed. + """ + + share = _share.Share() + return share.manage( + self, protocol, export_path, service_host, **params + ) + + def unmanage_share(self, share_id): + """Unmanage the share with the given share ID. + + :param share_id: The ID of the share to unmanage. + :returns: ``None`` + """ + + share_to_unmanage = self._get(_share.Share, share_id) + share_to_unmanage.unmanage(self) + def resize_share( self, share_id, new_size, no_shrink=False, no_extend=False, force=False ): diff --git a/openstack/shared_file_system/v2/share.py b/openstack/shared_file_system/v2/share.py index ebe3adfb4..621ea729d 100644 --- a/openstack/shared_file_system/v2/share.py +++ b/openstack/shared_file_system/v2/share.py @@ -160,3 +160,54 @@ def revert_to_snapshot(self, session, snapshot_id): """ body = {"revert": {"snapshot_id": snapshot_id}} self._action(session, body) + + def manage(self, session, protocol, export_path, service_host, **params): + """Manage a share. + + :param session: A session object used for sending request. + :param str protocol: The shared file systems protocol of this share. + :param str export_path: The export path formatted according to the + protocol. + :param str service_host: The manage-share service host. + :param kwargs params: Optional parameters to be sent. Available + parameters include: + + * name: The user defined name of the resource. + * share_type: The name or ID of the share type to be used to create + the resource. + * driver_options: A set of one or more key and value pairs, as a + dictionary of strings, that describe driver options. + * is_public: The level of visibility for the share. + * description: The user defiend description of the resource. + * share_server_id: The UUID of the share server. + + :returns: The share that was managed. + """ + + path = 'manage' + attrs = { + 'share': { + 'protocol': protocol, + 'export_path': export_path, + 'service_host': service_host, + } + } + + attrs['share'].update(params) + + url = utils.urljoin(self.base_path, path) + resp = session.post(url, json=attrs) + + self._translate_response(resp) + return self + + def unmanage(self, session): + """Unmanage a share. + + :param session: A session object used for sending request. + :returns: ``None`` + """ + + body = {'unmanage': None} + + self._action(session, body) diff --git a/openstack/tests/functional/shared_file_system/test_share.py b/openstack/tests/functional/shared_file_system/test_share.py index 8112b5ea7..c79c6978f 100644 --- a/openstack/tests/functional/shared_file_system/test_share.py +++ b/openstack/tests/functional/shared_file_system/test_share.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import exceptions from openstack.shared_file_system.v2 import share as _share from openstack.tests.functional.shared_file_system import base @@ -158,3 +159,59 @@ def test_resize_share_with_force(self): wait=self._wait_for_timeout, ) self.assertEqual(larger_size, get_resized_share.size) + + +class ManageUnmanageShareTest(base.BaseSharedFileSystemTest): + def setUp(self): + super(ManageUnmanageShareTest, self).setUp() + + self.NEW_SHARE = self.create_share( + share_proto="NFS", + name="accounting_p8787", + size=2, + ) + self.SHARE_ID = self.NEW_SHARE.id + + self.export_locations = self.operator_cloud.share.export_locations( + self.SHARE_ID + ) + export_paths = [export['path'] for export in self.export_locations] + self.export_path = export_paths[0] + + self.share_host = self.operator_cloud.share.get_share(self.SHARE_ID)[ + 'host' + ] + + def test_manage_and_unmanage_share(self): + self.operator_cloud.share.unmanage_share(self.SHARE_ID) + + self.operator_cloud.shared_file_system.wait_for_delete( + self.NEW_SHARE, interval=2, wait=self._wait_for_timeout + ) + + try: + self.operator_cloud.share.get_share(self.SHARE_ID) + except exceptions.ResourceNotFound: + pass + + managed_share = self.operator_cloud.share.manage_share( + self.NEW_SHARE.share_protocol, self.export_path, self.share_host + ) + + self.operator_cloud.share.wait_for_status( + managed_share, + status='available', + failures=['error'], + interval=5, + wait=self._wait_for_timeout, + ) + + self.assertEqual( + self.NEW_SHARE.share_protocol, managed_share.share_protocol + ) + + managed_host = self.operator_cloud.share.get_share(managed_share.id)[ + 'host' + ] + + self.assertEqual(self.share_host, managed_host) diff --git a/openstack/tests/unit/shared_file_system/v2/test_share.py b/openstack/tests/unit/shared_file_system/v2/test_share.py index 3ddea2061..f45bc9c6c 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_share.py +++ b/openstack/tests/unit/shared_file_system/v2/test_share.py @@ -183,3 +183,50 @@ def test_revert_to_snapshot(self): self.sess.post.assert_called_with( url, json=body, headers=headers, microversion=microversion ) + + def test_manage_share(self): + sot = share.Share() + + self.resp.headers = {} + self.resp.json = mock.Mock( + return_value={"share": {"name": "test_share", "size": 1}} + ) + + export_path = ( + "10.254.0 .5:/shares/share-42033c24-0261-424f-abda-4fef2f6dbfd5." + ) + params = {"name": "test_share"} + res = sot.manage( + self.sess, + sot["share_protocol"], + export_path, + sot["host"], + **params, + ) + + self.assertEqual(res.name, "test_share") + self.assertEqual(res.size, 1) + + jsonDict = { + "share": { + "protocol": sot["share_protocol"], + "export_path": export_path, + "service_host": sot["host"], + "name": "test_share", + } + } + + self.sess.post.assert_called_once_with("shares/manage", json=jsonDict) + + def test_unmanage_share(self): + sot = share.Share(**EXAMPLE) + microversion = sot._get_microversion(self.sess, action='patch') + + self.assertIsNone(sot.unmanage(self.sess)) + + url = 'shares/%s/action' % IDENTIFIER + body = {'unmanage': None} + + self.sess.post.assert_called_with( + url, json=body, headers={'Accept': ''}, microversion=microversion + ) diff --git a/releasenotes/notes/add-shared-file-system-manage-unmanage-share-830e313f96e5fd2b.yaml b/releasenotes/notes/add-shared-file-system-manage-unmanage-share-830e313f96e5fd2b.yaml new file mode 100644 index 000000000..d349d6c90 --- /dev/null +++ b/releasenotes/notes/add-shared-file-system-manage-unmanage-share-830e313f96e5fd2b.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added support to manage and unmanage shares + from the shared file system service. \ No newline at end of file From c8557f88479f3c901efd4a99d92e79a4f4a82387 Mon Sep 17 00:00:00 2001 From: Omer Date: Wed, 9 Aug 2023 15:40:19 +0200 Subject: [PATCH 3342/3836] Add 2 tls container params for Octavia Pools This patch add 2 new pool options, which are 'ca_tls_container_ref' and 'crl_container_ref'. 'ca_tls_container_ref' will store the ca certificate used by backend servers. 'crl_container_ref' will store the revocation list file. Change-Id: I86277ff370e9ca04a22400348dbca90c63af55bc --- openstack/load_balancer/v2/pool.py | 6 ++++++ openstack/tests/unit/load_balancer/test_pool.py | 16 ++++++++++++++++ ...arams-for-octavia-pools-76f295cd2daa7f53.yaml | 6 ++++++ 3 files changed, 28 insertions(+) create mode 100644 releasenotes/notes/add-tls-container-refs-params-for-octavia-pools-76f295cd2daa7f53.yaml diff --git a/openstack/load_balancer/v2/pool.py b/openstack/load_balancer/v2/pool.py index ac1422189..d736548f2 100644 --- a/openstack/load_balancer/v2/pool.py +++ b/openstack/load_balancer/v2/pool.py @@ -42,6 +42,8 @@ class Pool(resource.Resource, tag.TagMixin): 'tls_ciphers', 'tls_versions', 'alpn_protocols', + 'ca_tls_container_ref', + 'crl_container_ref', is_admin_state_up='admin_state_up', **tag.TagMixin._tag_query_parameters ) @@ -89,3 +91,7 @@ class Pool(resource.Resource, tag.TagMixin): updated_at = resource.Body('updated_at') #: Use TLS for connections to backend member servers *Type: bool* tls_enabled = resource.Body('tls_enabled', type=bool) + #: Stores the ca certificate used by backend servers + ca_tls_container_ref = resource.Body('ca_tls_container_ref') + #: Stores the revocation list file + crl_container_ref = resource.Body('crl_container_ref') diff --git a/openstack/tests/unit/load_balancer/test_pool.py b/openstack/tests/unit/load_balancer/test_pool.py index 1d1dfac79..ea7c325ea 100644 --- a/openstack/tests/unit/load_balancer/test_pool.py +++ b/openstack/tests/unit/load_balancer/test_pool.py @@ -40,6 +40,14 @@ 'tls_ciphers': 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256', 'tls_versions': ['TLSv1.1', 'TLSv1.2'], 'alpn_protocols': ['h2', 'http/1.1', 'http/1.0'], + 'ca_tls_container_ref': ( + 'http://198.51.100.10:9311/v1/containers/' + 'a570068c-d295-4780-91d4-3046a325db52' + ), + 'crl_container_ref': ( + 'http://198.51.100.10:9311/v1/containers/' + 'a570068c-d295-4780-91d4-3046a325db53' + ), } @@ -88,6 +96,12 @@ def test_make_it(self): self.assertEqual(EXAMPLE['tls_ciphers'], test_pool.tls_ciphers) self.assertEqual(EXAMPLE['tls_versions'], test_pool.tls_versions) self.assertEqual(EXAMPLE['alpn_protocols'], test_pool.alpn_protocols) + self.assertEqual( + EXAMPLE['ca_tls_container_ref'], test_pool.ca_tls_container_ref + ) + self.assertEqual( + EXAMPLE['crl_container_ref'], test_pool.crl_container_ref + ) self.assertDictEqual( { @@ -114,6 +128,8 @@ def test_make_it(self): 'tls_ciphers': 'tls_ciphers', 'tls_versions': 'tls_versions', 'alpn_protocols': 'alpn_protocols', + 'ca_tls_container_ref': 'ca_tls_container_ref', + 'crl_container_ref': 'crl_container_ref', }, test_pool._query_mapping._mapping, ) diff --git a/releasenotes/notes/add-tls-container-refs-params-for-octavia-pools-76f295cd2daa7f53.yaml b/releasenotes/notes/add-tls-container-refs-params-for-octavia-pools-76f295cd2daa7f53.yaml new file mode 100644 index 000000000..e7a87adbc --- /dev/null +++ b/releasenotes/notes/add-tls-container-refs-params-for-octavia-pools-76f295cd2daa7f53.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add both ``ca_tls_container_ref`` and ``crl_container_ref`` + parameters for Octavia pools, which can be used to store the ca + certificate used by backend servers and the revocation list file. From c6aaae56ee705bd6a6e61520b4efe74d56376387 Mon Sep 17 00:00:00 2001 From: Taehyun Park Date: Mon, 19 Sep 2022 21:21:31 +0900 Subject: [PATCH 3343/3836] Add image metadef object operations The Image Metadata Object operations are not support currently. In this patch, I added an operations of image metadata object. Co-authored-by: Hael Yoon Co-authored-by: JaeSeong Shin Co-authored-by: LEE SUN JAE Co-authored-by: Mridula Joshi Change-Id: Ie76804204878d5302beda4dfa93b898ec3f6f46a --- doc/source/user/proxies/image_v2.rst | 8 ++ doc/source/user/resources/image/index.rst | 1 + .../resources/image/v2/metadef_object.rst | 13 ++ openstack/image/v2/_proxy.py | 114 +++++++++++++++++- openstack/image/v2/metadef_object.py | 40 ++++++ .../image/v2/test_metadef_object.py | 103 ++++++++++++++++ .../unit/image/v2/test_metadef_object.py | 77 ++++++++++++ openstack/tests/unit/image/v2/test_proxy.py | 48 ++++++++ .../add-metadef-object-5eec168baf039e80.yaml | 4 + 9 files changed, 407 insertions(+), 1 deletion(-) create mode 100644 doc/source/user/resources/image/v2/metadef_object.rst create mode 100644 openstack/image/v2/metadef_object.py create mode 100644 openstack/tests/functional/image/v2/test_metadef_object.py create mode 100644 openstack/tests/unit/image/v2/test_metadef_object.py create mode 100644 releasenotes/notes/add-metadef-object-5eec168baf039e80.yaml diff --git a/doc/source/user/proxies/image_v2.rst b/doc/source/user/proxies/image_v2.rst index 83c933269..b280e8198 100644 --- a/doc/source/user/proxies/image_v2.rst +++ b/doc/source/user/proxies/image_v2.rst @@ -67,6 +67,14 @@ Metadef Namespace Operations get_metadef_namespace, metadef_namespaces, update_metadef_namespace +Metadef Object Operations +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.image.v2._proxy.Proxy + :noindex: + :members: create_metadef_object, delete_metadef_object, + get_metadef_object, metadef_objects, update_metadef_object + Metadef Resource Type Operations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/image/index.rst b/doc/source/user/resources/image/index.rst index 2f291f2c6..6cfe1ae65 100644 --- a/doc/source/user/resources/image/index.rst +++ b/doc/source/user/resources/image/index.rst @@ -18,6 +18,7 @@ Image v2 Resources v2/image v2/member v2/metadef_namespace + v2/metadef_object v2/metadef_resource_type v2/metadef_schema v2/task diff --git a/doc/source/user/resources/image/v2/metadef_object.rst b/doc/source/user/resources/image/v2/metadef_object.rst new file mode 100644 index 000000000..2f9548a0c --- /dev/null +++ b/doc/source/user/resources/image/v2/metadef_object.rst @@ -0,0 +1,13 @@ +openstack.image.v2.metadef_object +================================== + +.. automodule:: openstack.image.v2.metadef_object + +The MetadefObject Class +------------------------ + +The ``MetadefObject`` class inherits +from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.image.v2.metadef_object.MetadefObject + :members: diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index f3eb12f28..f75ed0715 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -19,6 +19,7 @@ from openstack.image.v2 import image as _image from openstack.image.v2 import member as _member from openstack.image.v2 import metadef_namespace as _metadef_namespace +from openstack.image.v2 import metadef_object as _metadef_object from openstack.image.v2 import metadef_resource_type as _metadef_resource_type from openstack.image.v2 import metadef_schema as _metadef_schema from openstack.image.v2 import schema as _schema @@ -1205,6 +1206,117 @@ def update_metadef_namespace(self, metadef_namespace, **attrs): **attrs, ) + # ====== METADEF OBJECT ====== + def create_metadef_object(self, namespace, **attrs): + """Create a new object from namespace + + :param namespace: The value can be either the name of a metadef + namespace or a + :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` + instance. + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.image.v2.metadef_object.MetadefObject`, + comprised of the properties on the Metadef object class. + + :returns: A metadef namespace + :rtype: :class:`~openstack.image.v2.metadef_object.MetadefObject` + """ + namespace_name = resource.Resource._get_id(namespace) + return self._create( + _metadef_object.MetadefObject, + namespace_name=namespace_name, + **attrs, + ) + + def get_metadef_object(self, metadef_object, namespace): + """Get a single metadef object + + :param metadef_object: The value can be the ID of a metadef_object + or a + :class:`~openstack.image.v2.metadef_object.MetadefObject` + instance. + :param namespace: The value can be either the name of a metadef + namespace or a + :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` + instance. + :returns: One :class:`~openstack.image.v2.metadef_object.MetadefObject` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource can be found. + """ + object_name = resource.Resource._get_id(metadef_object) + namespace_name = resource.Resource._get_id(namespace) + return self._get( + _metadef_object.MetadefObject, + namespace_name=namespace_name, + name=object_name, + ) + + def metadef_objects(self, namespace): + """Get metadef object list of the namespace + + :param namespace: The value can be either the name of a metadef + namespace or a + :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` + instance. + + :returns: One :class:`~openstack.image.v2.metadef_object.MetadefObject` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource can be found. + """ + namespace_name = resource.Resource._get_id(namespace) + return self._list( + _metadef_object.MetadefObject, + namespace_name=namespace_name, + ) + + def update_metadef_object(self, metadef_object, namespace, **attrs): + """Update a single metadef object + + :param metadef_object: The value can be the ID of a metadef_object or a + :class:`~openstack.image.v2.metadef_object.MetadefObject` instance. + :param namespace: The value can be either the name of a metadef + namespace or a + :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` + instance. + :param dict attrs: Keyword arguments which will be used to update + a :class:`~openstack.image.v2.metadef_object.MetadefObject` + + :returns: One :class:`~openstack.image.v2.metadef_object.MetadefObject` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource can be found. + """ + namespace_name = resource.Resource._get_id(namespace) + return self._update( + _metadef_object.MetadefObject, + metadef_object, + namespace_name=namespace_name, + **attrs, + ) + + def delete_metadef_object(self, metadef_object, namespace, **attrs): + """Removes a single metadef object + + :param metadef_object: The value can be the ID of a metadef_object or a + :class:`~openstack.image.v2.metadef_object.MetadefObject` instance. + :param namespace: The value can be either the name of a metadef + namespace or a + :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` + instance. + :param dict attrs: Keyword arguments which will be used to update + a :class:`~openstack.image.v2.metadef_object.MetadefObject` + + :returns: ``None`` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource can be found. + """ + namespace_name = resource.Resource._get_id(namespace) + return self._delete( + _metadef_object.MetadefObject, + metadef_object, + namespace_name=namespace_name, + **attrs, + ) + # ====== METADEF RESOURCE TYPES ====== def metadef_resource_types(self, **query): """Return a generator of metadef resource types @@ -1213,7 +1325,7 @@ def metadef_resource_types(self, **query): :rtype: :class:`~openstack.image.v2.metadef_resource_type.MetadefResourceType` :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. + when no resource can be found. """ return self._list(_metadef_resource_type.MetadefResourceType, **query) diff --git a/openstack/image/v2/metadef_object.py b/openstack/image/v2/metadef_object.py new file mode 100644 index 000000000..fede230ee --- /dev/null +++ b/openstack/image/v2/metadef_object.py @@ -0,0 +1,40 @@ +# 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 resource + + +class MetadefObject(resource.Resource): + resources_key = 'objects' + base_path = '/metadefs/namespaces/%(namespace_name)s/objects' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + _query_mapping = resource.QueryParameters( + "visibility", + "resource_types", + "sort_key", + "sort_dir", + ) + + created_at = resource.Body('created_at') + description = resource.Body('description') + name = resource.Body('name', alternate_id=True) + namespace_name = resource.URI('namespace_name') + properties = resource.Body('properties') + required = resource.Body('required') + updated_at = resource.Body('updated_at') diff --git a/openstack/tests/functional/image/v2/test_metadef_object.py b/openstack/tests/functional/image/v2/test_metadef_object.py new file mode 100644 index 000000000..2edd2e364 --- /dev/null +++ b/openstack/tests/functional/image/v2/test_metadef_object.py @@ -0,0 +1,103 @@ +# 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.image.v2 import metadef_namespace as _metadef_namespace +from openstack.image.v2 import metadef_object as _metadef_object +from openstack.tests.functional.image.v2 import base + + +class TestMetadefObject(base.BaseImageTest): + def setUp(self): + super().setUp() + + # create namespace for object + namespace = self.getUniqueString().split('.')[-1] + self.metadef_namespace = self.conn.image.create_metadef_namespace( + namespace=namespace, + ) + self.assertIsInstance( + self.metadef_namespace, + _metadef_namespace.MetadefNamespace, + ) + self.assertEqual(namespace, self.metadef_namespace.namespace) + + # create object + object = self.getUniqueString().split('.')[-1] + self.metadef_object = self.conn.image.create_metadef_object( + name=object, + namespace=self.metadef_namespace, + ) + self.assertIsInstance( + self.metadef_object, + _metadef_object.MetadefObject, + ) + self.assertEqual(object, self.metadef_object.name) + + def tearDown(self): + self.conn.image.delete_metadef_object( + self.metadef_object, + self.metadef_object.namespace_name, + ) + self.conn.image.wait_for_delete(self.metadef_object) + + self.conn.image.delete_metadef_namespace(self.metadef_namespace) + self.conn.image.wait_for_delete(self.metadef_namespace) + + super().tearDown() + + def test_metadef_objects(self): + # get + metadef_object = self.conn.image.get_metadef_object( + self.metadef_object.name, + self.metadef_namespace, + ) + self.assertEqual( + self.metadef_object.namespace_name, + metadef_object.namespace_name, + ) + self.assertEqual( + self.metadef_object.name, + metadef_object.name, + ) + + # list + metadef_objects = list( + self.conn.image.metadef_objects(self.metadef_object.namespace_name) + ) + # there are a load of default metadef objects so we don't assert + # that this is the *only* metadef objects present + self.assertIn( + self.metadef_object.name, + {o.name for o in metadef_objects}, + ) + + # update + metadef_object_new_name = 'New object name' + metadef_object_new_description = 'New object description' + metadef_object = self.conn.image.update_metadef_object( + self.metadef_object.name, + namespace=self.metadef_object.namespace_name, + name=metadef_object_new_name, + description=metadef_object_new_description, + ) + self.assertIsInstance( + metadef_object, + _metadef_object.MetadefObject, + ) + self.assertEqual( + metadef_object_new_name, + metadef_object.name, + ) + self.assertEqual( + metadef_object_new_description, + metadef_object.description, + ) diff --git a/openstack/tests/unit/image/v2/test_metadef_object.py b/openstack/tests/unit/image/v2/test_metadef_object.py new file mode 100644 index 000000000..10d36c2c9 --- /dev/null +++ b/openstack/tests/unit/image/v2/test_metadef_object.py @@ -0,0 +1,77 @@ +# 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.image.v2 import metadef_object +from openstack.tests.unit import base + + +EXAMPLE = { + 'created_at': '2014-09-19T18:20:56Z', + 'description': 'The CPU limits with control parameters.', + 'name': 'CPU Limits', + 'properties': { + 'quota:cpu_period': { + 'description': 'The enforcement interval', + 'maximum': 1000000, + 'minimum': 1000, + 'title': 'Quota: CPU Period', + 'type': 'integer', + }, + 'quota:cpu_quota': { + 'description': 'The maximum allowed bandwidth', + 'title': 'Quota: CPU Quota', + 'type': 'integer', + }, + 'quota:cpu_shares': { + 'description': 'The proportional weighted', + 'title': 'Quota: CPU Shares', + 'type': 'integer', + }, + }, + 'required': [], + 'schema': '/v2/schemas/metadefs/object', + 'updated_at': '2014-09-19T18:20:56Z', +} + + +class TestMetadefObject(base.TestCase): + def test_basic(self): + sot = metadef_object.MetadefObject() + self.assertIsNone(sot.resource_key) + self.assertEqual('objects', sot.resources_key) + test_base_path = '/metadefs/namespaces/%(namespace_name)s/objects' + self.assertEqual(test_base_path, sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = metadef_object.MetadefObject(**EXAMPLE) + self.assertEqual(EXAMPLE['created_at'], sot.created_at) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['properties'], sot.properties) + self.assertEqual(EXAMPLE['required'], sot.required) + self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) + self.assertDictEqual( + { + "limit": "limit", + "marker": "marker", + "visibility": "visibility", + "resource_types": "resource_types", + "sort_key": "sort_key", + "sort_dir": "sort_dir", + }, + sot._query_mapping._mapping, + ) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 23816cc1f..d74777cc9 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -21,6 +21,7 @@ from openstack.image.v2 import image as _image from openstack.image.v2 import member as _member from openstack.image.v2 import metadef_namespace as _metadef_namespace +from openstack.image.v2 import metadef_object as _metadef_object from openstack.image.v2 import metadef_resource_type as _metadef_resource_type from openstack.image.v2 import metadef_schema as _metadef_schema from openstack.image.v2 import schema as _schema @@ -569,6 +570,53 @@ def test_metadef_namespace_update(self): ) +class TestMetadefObject(TestImageProxy): + def test_create_metadef_object(self): + self.verify_create( + self.proxy.create_metadef_object, + _metadef_object.MetadefObject, + method_kwargs={"namespace": "test_namespace_name"}, + expected_kwargs={"namespace_name": "test_namespace_name"}, + ) + + def test_get_metadef_object(self): + self.verify_get( + self.proxy.get_metadef_object, + _metadef_object.MetadefObject, + method_kwargs={"namespace": "test_namespace_name"}, + expected_kwargs={ + "namespace_name": "test_namespace_name", + 'name': 'resource_id', + }, + expected_args=[], + ) + + def test_metadef_objects(self): + self.verify_list( + self.proxy.metadef_objects, + _metadef_object.MetadefObject, + method_kwargs={"namespace": "test_namespace_name"}, + expected_kwargs={"namespace_name": "test_namespace_name"}, + ) + + def test_update_metadef_object(self): + self.verify_update( + self.proxy.update_metadef_object, + _metadef_object.MetadefObject, + method_kwargs={"namespace": "test_namespace_name"}, + expected_kwargs={"namespace_name": "test_namespace_name"}, + ) + + def test_delete_metadef_object(self): + self.verify_delete( + self.proxy.delete_metadef_object, + _metadef_object.MetadefObject, + False, + method_kwargs={"namespace": "test_namespace_name"}, + expected_kwargs={"namespace_name": "test_namespace_name"}, + ) + + class TestMetadefResourceType(TestImageProxy): def test_metadef_resource_types(self): self.verify_list( diff --git a/releasenotes/notes/add-metadef-object-5eec168baf039e80.yaml b/releasenotes/notes/add-metadef-object-5eec168baf039e80.yaml new file mode 100644 index 000000000..a1e037f78 --- /dev/null +++ b/releasenotes/notes/add-metadef-object-5eec168baf039e80.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add support for Image Metadef objects. From 97d034968615ed7a1d1120a8d35527edf597d5e8 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 25 May 2023 11:55:08 +0100 Subject: [PATCH 3344/3836] tests: Rename file, remove unused file shade is dead. Rename one file and remain another stub that is never going to be populated at this point. Change-Id: I333ccb91df3803d341316180c65bd59f691a1995 Signed-off-by: Stephen Finucane --- .../cloud/{test_shade.py => test_cloud.py} | 4 ++-- .../tests/unit/cloud/test_shade_operator.py | 20 ------------------- 2 files changed, 2 insertions(+), 22 deletions(-) rename openstack/tests/unit/cloud/{test_shade.py => test_cloud.py} (99%) delete mode 100644 openstack/tests/unit/cloud/test_shade_operator.py diff --git a/openstack/tests/unit/cloud/test_shade.py b/openstack/tests/unit/cloud/test_cloud.py similarity index 99% rename from openstack/tests/unit/cloud/test_shade.py rename to openstack/tests/unit/cloud/test_cloud.py index 9bc80f332..147c8656d 100644 --- a/openstack/tests/unit/cloud/test_shade.py +++ b/openstack/tests/unit/cloud/test_cloud.py @@ -33,7 +33,7 @@ ] -class TestShade(base.TestCase): +class TestCloud(base.TestCase): def setUp(self): # This set of tests are not testing neutron, they're testing # rebuilding servers, but we do several network calls in service @@ -43,7 +43,7 @@ def setUp(self): # and then turn it back on in the few tests that specifically do. # Maybe we should reorg these into two classes - one with neutron # mocked out - and one with it not mocked out - super(TestShade, self).setUp() + super().setUp() self.has_neutron = False def fake_has_service(*args, **kwargs): diff --git a/openstack/tests/unit/cloud/test_shade_operator.py b/openstack/tests/unit/cloud/test_shade_operator.py deleted file mode 100644 index 1939d2daa..000000000 --- a/openstack/tests/unit/cloud/test_shade_operator.py +++ /dev/null @@ -1,20 +0,0 @@ -# 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. - -# TODO(shade) Port this content back in from shade repo as tests don't have -# references to ironic_client. - -from openstack.tests.unit import base - - -class TestShadeOperator(base.TestCase): - pass From d67006ea0f59ee6a7ab9bc5fd7c8d615167198c8 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 25 May 2023 12:00:29 +0100 Subject: [PATCH 3345/3836] tests: Move cloud-layer service tests to their own file This mirrors how we do unit testing elsewhere, with the test module's path mirroring the module-under-test's path. Change-Id: I0382c5bb0d86431f6f7520b16881360477eb2658 Signed-off-by: Stephen Finucane --- openstack/tests/unit/cloud/test_cloud.py | 615 --------------------- openstack/tests/unit/cloud/test_compute.py | 500 +++++++++++++++++ openstack/tests/unit/cloud/test_image.py | 16 + openstack/tests/unit/cloud/test_network.py | 124 ++++- 4 files changed, 639 insertions(+), 616 deletions(-) create mode 100644 openstack/tests/unit/cloud/test_compute.py diff --git a/openstack/tests/unit/cloud/test_cloud.py b/openstack/tests/unit/cloud/test_cloud.py index 147c8656d..cbed0c86a 100644 --- a/openstack/tests/unit/cloud/test_cloud.py +++ b/openstack/tests/unit/cloud/test_cloud.py @@ -17,8 +17,6 @@ from openstack.cloud import exc from openstack import connection -from openstack import exceptions -from openstack.tests import fakes from openstack.tests.unit import base from openstack import utils @@ -34,23 +32,6 @@ class TestCloud(base.TestCase): - def setUp(self): - # This set of tests are not testing neutron, they're testing - # rebuilding servers, but we do several network calls in service - # of a NORMAL rebuild to find the default_network. Putting - # in all of the neutron mocks for that will make the tests harder - # to read. SO - we're going mock neutron into the off position - # and then turn it back on in the few tests that specifically do. - # Maybe we should reorg these into two classes - one with neutron - # mocked out - and one with it not mocked out - super().setUp() - self.has_neutron = False - - def fake_has_service(*args, **kwargs): - return self.has_neutron - - self.cloud.has_service = fake_has_service - def test_openstack_cloud(self): self.assertIsInstance(self.cloud, connection.Connection) @@ -116,20 +97,6 @@ def test_connect_as_context(self): self.assertEqual(c2.list_servers(), []) self.assert_calls() - @mock.patch.object(connection.Connection, 'search_images') - def test_get_images(self, mock_search): - image1 = dict(id='123', name='mickey') - mock_search.return_value = [image1] - r = self.cloud.get_image('mickey') - self.assertIsNotNone(r) - self.assertDictEqual(image1, r) - - @mock.patch.object(connection.Connection, 'search_images') - def test_get_image_not_found(self, mock_search): - mock_search.return_value = [] - r = self.cloud.get_image('doesNotExist') - self.assertIsNone(r) - def test_global_request_id(self): request_id = uuid.uuid4().hex self.register_uris( @@ -176,355 +143,6 @@ def test_global_request_id_context(self): self.assert_calls() - def test_get_server(self): - server1 = fakes.make_fake_server('123', 'mickey') - server2 = fakes.make_fake_server('345', 'mouse') - - self.register_uris( - [ - self.get_nova_discovery_mock_dict(), - dict( - method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'] - ), - json={'servers': [server1, server2]}, - ), - ] - ) - - r = self.cloud.get_server('mickey') - self.assertIsNotNone(r) - self.assertEqual(server1['name'], r['name']) - - self.assert_calls() - - def test_get_server_not_found(self): - self.register_uris( - [ - self.get_nova_discovery_mock_dict(), - dict( - method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'] - ), - json={'servers': []}, - ), - ] - ) - - r = self.cloud.get_server('doesNotExist') - self.assertIsNone(r) - - self.assert_calls() - - def test_list_servers_exception(self): - self.register_uris( - [ - self.get_nova_discovery_mock_dict(), - dict( - method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'] - ), - status_code=400, - ), - ] - ) - - self.assertRaises(exc.OpenStackCloudException, self.cloud.list_servers) - - self.assert_calls() - - def test_neutron_not_found(self): - self.use_nothing() - self.cloud.has_service = mock.Mock(return_value=False) - self.assertEqual([], self.cloud.list_networks()) - self.assert_calls() - - def test_list_servers(self): - server_id = str(uuid.uuid4()) - server_name = self.getUniqueString('name') - fake_server = fakes.make_fake_server(server_id, server_name) - self.register_uris( - [ - self.get_nova_discovery_mock_dict(), - dict( - method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'] - ), - json={'servers': [fake_server]}, - ), - ] - ) - - r = self.cloud.list_servers() - - self.assertEqual(1, len(r)) - self.assertEqual(server_name, r[0]['name']) - - self.assert_calls() - - def test_list_server_private_ip(self): - self.has_neutron = True - fake_server = { - "OS-EXT-STS:task_state": None, - "addresses": { - "private": [ - { - "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:b4:a3:07", - "version": 4, - "addr": "10.4.0.13", - "OS-EXT-IPS:type": "fixed", - }, - { - "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:b4:a3:07", - "version": 4, - "addr": "89.40.216.229", - "OS-EXT-IPS:type": "floating", - }, - ] - }, - "links": [ - {"href": "http://example.com/images/95e4c4", "rel": "self"}, - { - "href": "http://example.com/images/95e4c4", - "rel": "bookmark", - }, - ], - "image": { - "id": "95e4c449-8abf-486e-97d9-dc3f82417d2d", - "links": [ - { - "href": "http://example.com/images/95e4c4", - "rel": "bookmark", - } - ], - }, - "OS-EXT-STS:vm_state": "active", - "OS-SRV-USG:launched_at": "2018-03-01T02:44:50.000000", - "flavor": { - "id": "3bd99062-2fe8-4eac-93f0-9200cc0f97ae", - "links": [ - { - "href": "http://example.com/flavors/95e4c4", - "rel": "bookmark", - } - ], - }, - "id": "97fe35e9-756a-41a2-960a-1d057d2c9ee4", - "security_groups": [{"name": "default"}], - "user_id": "c17534835f8f42bf98fc367e0bf35e09", - "OS-DCF:diskConfig": "MANUAL", - "accessIPv4": "", - "accessIPv6": "", - "progress": 0, - "OS-EXT-STS:power_state": 1, - "OS-EXT-AZ:availability_zone": "nova", - "metadata": {}, - "status": "ACTIVE", - "updated": "2018-03-01T02:44:51Z", - "hostId": "", - "OS-SRV-USG:terminated_at": None, - "key_name": None, - "name": "mttest", - "created": "2018-03-01T02:44:46Z", - "tenant_id": "65222a4d09ea4c68934fa1028c77f394", - "os-extended-volumes:volumes_attached": [], - "config_drive": "", - } - fake_networks = { - "networks": [ - { - "status": "ACTIVE", - "router:external": True, - "availability_zone_hints": [], - "availability_zones": ["nova"], - "description": None, - "subnets": [ - "df3e17fa-a4b2-47ae-9015-bc93eb076ba2", - "6b0c3dc9-b0b8-4d87-976a-7f2ebf13e7ec", - "fc541f48-fc7f-48c0-a063-18de6ee7bdd7", - ], - "shared": False, - "tenant_id": "a564613210ee43708b8a7fc6274ebd63", - "tags": [], - "ipv6_address_scope": "9f03124f-89af-483a-b6fd-10f08079db4d", # noqa: E501 - "mtu": 1550, - "is_default": False, - "admin_state_up": True, - "revision_number": 0, - "ipv4_address_scope": None, - "port_security_enabled": True, - "project_id": "a564613210ee43708b8a7fc6274ebd63", - "id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", - "name": "ext-net", - }, - { - "status": "ACTIVE", - "router:external": False, - "availability_zone_hints": [], - "availability_zones": ["nova"], - "description": "", - "subnets": ["f0ad1df5-53ee-473f-b86b-3604ea5591e9"], - "shared": False, - "tenant_id": "65222a4d09ea4c68934fa1028c77f394", - "created_at": "2016-10-22T13:46:26Z", - "tags": [], - "ipv6_address_scope": None, - "updated_at": "2016-10-22T13:46:26Z", - "admin_state_up": True, - "mtu": 1500, - "revision_number": 0, - "ipv4_address_scope": None, - "port_security_enabled": True, - "project_id": "65222a4d09ea4c68934fa1028c77f394", - "id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", - "name": "private", - }, - ] - } - fake_subnets = { - "subnets": [ - { - "service_types": [], - "description": "", - "enable_dhcp": True, - "tags": [], - "network_id": "827c6bb6-492f-4168-9577-f3a131eb29e8", - "tenant_id": "65222a4d09ea4c68934fa1028c77f394", - "created_at": "2017-06-12T13:23:57Z", - "dns_nameservers": [], - "updated_at": "2017-06-12T13:23:57Z", - "gateway_ip": "10.24.4.1", - "ipv6_ra_mode": None, - "allocation_pools": [ - {"start": "10.24.4.2", "end": "10.24.4.254"} - ], - "host_routes": [], - "revision_number": 0, - "ip_version": 4, - "ipv6_address_mode": None, - "cidr": "10.24.4.0/24", - "project_id": "65222a4d09ea4c68934fa1028c77f394", - "id": "3f0642d9-4644-4dff-af25-bcf64f739698", - "subnetpool_id": None, - "name": "foo_subnet", - }, - { - "service_types": [], - "description": "", - "enable_dhcp": True, - "tags": [], - "network_id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", - "tenant_id": "65222a4d09ea4c68934fa1028c77f394", - "created_at": "2016-10-22T13:46:26Z", - "dns_nameservers": ["89.36.90.101", "89.36.90.102"], - "updated_at": "2016-10-22T13:46:26Z", - "gateway_ip": "10.4.0.1", - "ipv6_ra_mode": None, - "allocation_pools": [ - {"start": "10.4.0.2", "end": "10.4.0.200"} - ], - "host_routes": [], - "revision_number": 0, - "ip_version": 4, - "ipv6_address_mode": None, - "cidr": "10.4.0.0/24", - "project_id": "65222a4d09ea4c68934fa1028c77f394", - "id": "f0ad1df5-53ee-473f-b86b-3604ea5591e9", - "subnetpool_id": None, - "name": "private-subnet-ipv4", - }, - ] - } - self.register_uris( - [ - self.get_nova_discovery_mock_dict(), - dict( - method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'] - ), - json={'servers': [fake_server]}, - ), - dict( - method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'networks'] - ), - json=fake_networks, - ), - dict( - method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'subnets'] - ), - json=fake_subnets, - ), - ] - ) - - r = self.cloud.get_server('97fe35e9-756a-41a2-960a-1d057d2c9ee4') - - self.assertEqual('10.4.0.13', r['private_v4']) - - self.assert_calls() - - def test_list_servers_all_projects(self): - '''This test verifies that when list_servers is called with - `all_projects=True` that it passes `all_tenants=True` to nova.''' - self.register_uris( - [ - self.get_nova_discovery_mock_dict(), - dict( - method='GET', - uri=self.get_mock_url( - 'compute', - 'public', - append=['servers', 'detail'], - qs_elements=['all_tenants=True'], - ), - complete_qs=True, - json={'servers': []}, - ), - ] - ) - - self.cloud.list_servers(all_projects=True) - - self.assert_calls() - - def test_list_servers_filters(self): - '''This test verifies that when list_servers is called with - `filters` dict that it passes it to nova.''' - self.register_uris( - [ - self.get_nova_discovery_mock_dict(), - dict( - method='GET', - uri=self.get_mock_url( - 'compute', - 'public', - append=['servers', 'detail'], - qs_elements=[ - 'deleted=True', - 'changes-since=2014-12-03T00:00:00Z', - ], - ), - complete_qs=True, - json={'servers': []}, - ), - ] - ) - - self.cloud.list_servers( - filters={'deleted': True, 'changes-since': '2014-12-03T00:00:00Z'} - ) - - self.assert_calls() - def test_iterate_timeout_bad_wait(self): with testtools.ExpectedException( exc.OpenStackCloudException, @@ -561,239 +179,6 @@ def test_iterate_timeout_timeout(self, mock_sleep): pass mock_sleep.assert_called_with(1.0) - def test__nova_extensions(self): - body = [ - { - "updated": "2014-12-03T00:00:00Z", - "name": "Multinic", - "links": [], - "namespace": "http://openstack.org/compute/ext/fake_xml", - "alias": "NMN", - "description": "Multiple network support.", - }, - { - "updated": "2014-12-03T00:00:00Z", - "name": "DiskConfig", - "links": [], - "namespace": "http://openstack.org/compute/ext/fake_xml", - "alias": "OS-DCF", - "description": "Disk Management Extension.", - }, - ] - self.register_uris( - [ - dict( - method='GET', - uri='{endpoint}/extensions'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), - json=dict(extensions=body), - ) - ] - ) - extensions = self.cloud._nova_extensions() - self.assertEqual(set(['NMN', 'OS-DCF']), extensions) - - self.assert_calls() - - def test__nova_extensions_fails(self): - self.register_uris( - [ - dict( - method='GET', - uri='{endpoint}/extensions'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), - status_code=404, - ), - ] - ) - self.assertRaises( - exceptions.ResourceNotFound, self.cloud._nova_extensions - ) - - self.assert_calls() - - def test__has_nova_extension(self): - body = [ - { - "updated": "2014-12-03T00:00:00Z", - "name": "Multinic", - "links": [], - "namespace": "http://openstack.org/compute/ext/fake_xml", - "alias": "NMN", - "description": "Multiple network support.", - }, - { - "updated": "2014-12-03T00:00:00Z", - "name": "DiskConfig", - "links": [], - "namespace": "http://openstack.org/compute/ext/fake_xml", - "alias": "OS-DCF", - "description": "Disk Management Extension.", - }, - ] - self.register_uris( - [ - dict( - method='GET', - uri='{endpoint}/extensions'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), - json=dict(extensions=body), - ) - ] - ) - self.assertTrue(self.cloud._has_nova_extension('NMN')) - - self.assert_calls() - - def test__has_nova_extension_missing(self): - body = [ - { - "updated": "2014-12-03T00:00:00Z", - "name": "Multinic", - "links": [], - "namespace": "http://openstack.org/compute/ext/fake_xml", - "alias": "NMN", - "description": "Multiple network support.", - }, - { - "updated": "2014-12-03T00:00:00Z", - "name": "DiskConfig", - "links": [], - "namespace": "http://openstack.org/compute/ext/fake_xml", - "alias": "OS-DCF", - "description": "Disk Management Extension.", - }, - ] - self.register_uris( - [ - dict( - method='GET', - uri='{endpoint}/extensions'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), - json=dict(extensions=body), - ) - ] - ) - self.assertFalse(self.cloud._has_nova_extension('invalid')) - - self.assert_calls() - - def test__neutron_extensions(self): - body = [ - { - "updated": "2014-06-1T10:00:00-00:00", - "name": "Distributed Virtual Router", - "links": [], - "alias": "dvr", - "description": "Enables configuration of Distributed Virtual Routers.", # noqa: E501 - }, - { - "updated": "2013-07-23T10:00:00-00:00", - "name": "Allowed Address Pairs", - "links": [], - "alias": "allowed-address-pairs", - "description": "Provides allowed address pairs", - }, - ] - self.register_uris( - [ - dict( - method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions'] - ), - json=dict(extensions=body), - ) - ] - ) - extensions = self.cloud._neutron_extensions() - self.assertEqual(set(['dvr', 'allowed-address-pairs']), extensions) - - self.assert_calls() - - def test__neutron_extensions_fails(self): - self.register_uris( - [ - dict( - method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions'] - ), - status_code=404, - ) - ] - ) - with testtools.ExpectedException(exceptions.ResourceNotFound): - self.cloud._neutron_extensions() - - self.assert_calls() - - def test__has_neutron_extension(self): - body = [ - { - "updated": "2014-06-1T10:00:00-00:00", - "name": "Distributed Virtual Router", - "links": [], - "alias": "dvr", - "description": "Enables configuration of Distributed Virtual Routers.", # noqa: E501 - }, - { - "updated": "2013-07-23T10:00:00-00:00", - "name": "Allowed Address Pairs", - "links": [], - "alias": "allowed-address-pairs", - "description": "Provides allowed address pairs", - }, - ] - self.register_uris( - [ - dict( - method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions'] - ), - json=dict(extensions=body), - ) - ] - ) - self.assertTrue(self.cloud._has_neutron_extension('dvr')) - self.assert_calls() - - def test__has_neutron_extension_missing(self): - body = [ - { - "updated": "2014-06-1T10:00:00-00:00", - "name": "Distributed Virtual Router", - "links": [], - "alias": "dvr", - "description": "Enables configuration of Distributed Virtual Routers.", # noqa: E501 - }, - { - "updated": "2013-07-23T10:00:00-00:00", - "name": "Allowed Address Pairs", - "links": [], - "alias": "allowed-address-pairs", - "description": "Provides allowed address pairs", - }, - ] - self.register_uris( - [ - dict( - method='GET', - uri=self.get_mock_url( - 'network', 'public', append=['v2.0', 'extensions'] - ), - json=dict(extensions=body), - ) - ] - ) - self.assertFalse(self.cloud._has_neutron_extension('invalid')) - self.assert_calls() - def test_range_search(self): filters = {"key1": "min", "key2": "20"} retval = self.cloud.range_search(RANGE_DATA, filters) diff --git a/openstack/tests/unit/cloud/test_compute.py b/openstack/tests/unit/cloud/test_compute.py new file mode 100644 index 000000000..04622ee63 --- /dev/null +++ b/openstack/tests/unit/cloud/test_compute.py @@ -0,0 +1,500 @@ +# 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 uuid + +from openstack.cloud import exc +from openstack import exceptions +from openstack.tests import fakes +from openstack.tests.unit import base + + +class TestNovaExtensions(base.TestCase): + def test__nova_extensions(self): + body = [ + { + "updated": "2014-12-03T00:00:00Z", + "name": "Multinic", + "links": [], + "namespace": "http://openstack.org/compute/ext/fake_xml", + "alias": "NMN", + "description": "Multiple network support.", + }, + { + "updated": "2014-12-03T00:00:00Z", + "name": "DiskConfig", + "links": [], + "namespace": "http://openstack.org/compute/ext/fake_xml", + "alias": "OS-DCF", + "description": "Disk Management Extension.", + }, + ] + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/extensions'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json=dict(extensions=body), + ) + ] + ) + extensions = self.cloud._nova_extensions() + self.assertEqual(set(['NMN', 'OS-DCF']), extensions) + + self.assert_calls() + + def test__nova_extensions_fails(self): + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/extensions'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + status_code=404, + ), + ] + ) + self.assertRaises( + exceptions.ResourceNotFound, self.cloud._nova_extensions + ) + + self.assert_calls() + + def test__has_nova_extension(self): + body = [ + { + "updated": "2014-12-03T00:00:00Z", + "name": "Multinic", + "links": [], + "namespace": "http://openstack.org/compute/ext/fake_xml", + "alias": "NMN", + "description": "Multiple network support.", + }, + { + "updated": "2014-12-03T00:00:00Z", + "name": "DiskConfig", + "links": [], + "namespace": "http://openstack.org/compute/ext/fake_xml", + "alias": "OS-DCF", + "description": "Disk Management Extension.", + }, + ] + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/extensions'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json=dict(extensions=body), + ) + ] + ) + self.assertTrue(self.cloud._has_nova_extension('NMN')) + + self.assert_calls() + + def test__has_nova_extension_missing(self): + body = [ + { + "updated": "2014-12-03T00:00:00Z", + "name": "Multinic", + "links": [], + "namespace": "http://openstack.org/compute/ext/fake_xml", + "alias": "NMN", + "description": "Multiple network support.", + }, + { + "updated": "2014-12-03T00:00:00Z", + "name": "DiskConfig", + "links": [], + "namespace": "http://openstack.org/compute/ext/fake_xml", + "alias": "OS-DCF", + "description": "Disk Management Extension.", + }, + ] + self.register_uris( + [ + dict( + method='GET', + uri='{endpoint}/extensions'.format( + endpoint=fakes.COMPUTE_ENDPOINT + ), + json=dict(extensions=body), + ) + ] + ) + self.assertFalse(self.cloud._has_nova_extension('invalid')) + + self.assert_calls() + + +class TestServers(base.TestCase): + def test_get_server(self): + server1 = fakes.make_fake_server('123', 'mickey') + server2 = fakes.make_fake_server('345', 'mouse') + + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + json={'servers': [server1, server2]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={"networks": []}, + ), + ] + ) + + r = self.cloud.get_server('mickey') + self.assertIsNotNone(r) + self.assertEqual(server1['name'], r['name']) + + self.assert_calls() + + def test_get_server_not_found(self): + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + json={'servers': []}, + ), + ] + ) + + r = self.cloud.get_server('doesNotExist') + self.assertIsNone(r) + + self.assert_calls() + + def test_list_servers(self): + server_id = str(uuid.uuid4()) + server_name = self.getUniqueString('name') + fake_server = fakes.make_fake_server(server_id, server_name) + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + json={'servers': [fake_server]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json={"networks": []}, + ), + ] + ) + + r = self.cloud.list_servers() + + self.assertEqual(1, len(r)) + self.assertEqual(server_name, r[0]['name']) + + self.assert_calls() + + def test_list_server_private_ip(self): + self.has_neutron = True + fake_server = { + "OS-EXT-STS:task_state": None, + "addresses": { + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:b4:a3:07", + "version": 4, + "addr": "10.4.0.13", + "OS-EXT-IPS:type": "fixed", + }, + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:b4:a3:07", + "version": 4, + "addr": "89.40.216.229", + "OS-EXT-IPS:type": "floating", + }, + ] + }, + "links": [ + {"href": "http://example.com/images/95e4c4", "rel": "self"}, + { + "href": "http://example.com/images/95e4c4", + "rel": "bookmark", + }, + ], + "image": { + "id": "95e4c449-8abf-486e-97d9-dc3f82417d2d", + "links": [ + { + "href": "http://example.com/images/95e4c4", + "rel": "bookmark", + } + ], + }, + "OS-EXT-STS:vm_state": "active", + "OS-SRV-USG:launched_at": "2018-03-01T02:44:50.000000", + "flavor": { + "id": "3bd99062-2fe8-4eac-93f0-9200cc0f97ae", + "links": [ + { + "href": "http://example.com/flavors/95e4c4", + "rel": "bookmark", + } + ], + }, + "id": "97fe35e9-756a-41a2-960a-1d057d2c9ee4", + "security_groups": [{"name": "default"}], + "user_id": "c17534835f8f42bf98fc367e0bf35e09", + "OS-DCF:diskConfig": "MANUAL", + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "OS-EXT-AZ:availability_zone": "nova", + "metadata": {}, + "status": "ACTIVE", + "updated": "2018-03-01T02:44:51Z", + "hostId": "", + "OS-SRV-USG:terminated_at": None, + "key_name": None, + "name": "mttest", + "created": "2018-03-01T02:44:46Z", + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", + "os-extended-volumes:volumes_attached": [], + "config_drive": "", + } + fake_networks = { + "networks": [ + { + "status": "ACTIVE", + "router:external": True, + "availability_zone_hints": [], + "availability_zones": ["nova"], + "description": None, + "subnets": [ + "df3e17fa-a4b2-47ae-9015-bc93eb076ba2", + "6b0c3dc9-b0b8-4d87-976a-7f2ebf13e7ec", + "fc541f48-fc7f-48c0-a063-18de6ee7bdd7", + ], + "shared": False, + "tenant_id": "a564613210ee43708b8a7fc6274ebd63", + "tags": [], + "ipv6_address_scope": "9f03124f-89af-483a-b6fd-10f08079db4d", # noqa: E501 + "mtu": 1550, + "is_default": False, + "admin_state_up": True, + "revision_number": 0, + "ipv4_address_scope": None, + "port_security_enabled": True, + "project_id": "a564613210ee43708b8a7fc6274ebd63", + "id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", + "name": "ext-net", + }, + { + "status": "ACTIVE", + "router:external": False, + "availability_zone_hints": [], + "availability_zones": ["nova"], + "description": "", + "subnets": ["f0ad1df5-53ee-473f-b86b-3604ea5591e9"], + "shared": False, + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", + "created_at": "2016-10-22T13:46:26Z", + "tags": [], + "ipv6_address_scope": None, + "updated_at": "2016-10-22T13:46:26Z", + "admin_state_up": True, + "mtu": 1500, + "revision_number": 0, + "ipv4_address_scope": None, + "port_security_enabled": True, + "project_id": "65222a4d09ea4c68934fa1028c77f394", + "id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", + "name": "private", + }, + ] + } + fake_subnets = { + "subnets": [ + { + "service_types": [], + "description": "", + "enable_dhcp": True, + "tags": [], + "network_id": "827c6bb6-492f-4168-9577-f3a131eb29e8", + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", + "created_at": "2017-06-12T13:23:57Z", + "dns_nameservers": [], + "updated_at": "2017-06-12T13:23:57Z", + "gateway_ip": "10.24.4.1", + "ipv6_ra_mode": None, + "allocation_pools": [ + {"start": "10.24.4.2", "end": "10.24.4.254"} + ], + "host_routes": [], + "revision_number": 0, + "ip_version": 4, + "ipv6_address_mode": None, + "cidr": "10.24.4.0/24", + "project_id": "65222a4d09ea4c68934fa1028c77f394", + "id": "3f0642d9-4644-4dff-af25-bcf64f739698", + "subnetpool_id": None, + "name": "foo_subnet", + }, + { + "service_types": [], + "description": "", + "enable_dhcp": True, + "tags": [], + "network_id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", + "created_at": "2016-10-22T13:46:26Z", + "dns_nameservers": ["89.36.90.101", "89.36.90.102"], + "updated_at": "2016-10-22T13:46:26Z", + "gateway_ip": "10.4.0.1", + "ipv6_ra_mode": None, + "allocation_pools": [ + {"start": "10.4.0.2", "end": "10.4.0.200"} + ], + "host_routes": [], + "revision_number": 0, + "ip_version": 4, + "ipv6_address_mode": None, + "cidr": "10.4.0.0/24", + "project_id": "65222a4d09ea4c68934fa1028c77f394", + "id": "f0ad1df5-53ee-473f-b86b-3604ea5591e9", + "subnetpool_id": None, + "name": "private-subnet-ipv4", + }, + ] + } + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + json={'servers': [fake_server]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'networks'] + ), + json=fake_networks, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets'] + ), + json=fake_subnets, + ), + ] + ) + + r = self.cloud.get_server('97fe35e9-756a-41a2-960a-1d057d2c9ee4') + + self.assertEqual('10.4.0.13', r['private_v4']) + + self.assert_calls() + + def test_list_servers_all_projects(self): + """This test verifies that when list_servers is called with + `all_projects=True` that it passes `all_tenants=True` to nova.""" + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', 'detail'], + qs_elements=['all_tenants=True'], + ), + complete_qs=True, + json={'servers': []}, + ), + ] + ) + + self.cloud.list_servers(all_projects=True) + + self.assert_calls() + + def test_list_servers_filters(self): + """This test verifies that when list_servers is called with + `filters` dict that it passes it to nova.""" + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', 'detail'], + qs_elements=[ + 'deleted=True', + 'changes-since=2014-12-03T00:00:00Z', + ], + ), + complete_qs=True, + json={'servers': []}, + ), + ] + ) + + self.cloud.list_servers( + filters={'deleted': True, 'changes-since': '2014-12-03T00:00:00Z'} + ) + + self.assert_calls() + + def test_list_servers_exception(self): + self.register_uris( + [ + self.get_nova_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', 'public', append=['servers', 'detail'] + ), + status_code=400, + ), + ] + ) + + self.assertRaises(exc.OpenStackCloudException, self.cloud.list_servers) + + self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 889a28d32..731ab7ee3 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -15,10 +15,12 @@ import io import operator import tempfile +from unittest import mock import uuid from openstack.cloud import exc from openstack.cloud import meta +from openstack import connection from openstack import exceptions from openstack.image.v1 import image as image_v1 from openstack.image.v2 import image @@ -164,6 +166,20 @@ def test_download_image_with_path(self): self.assertEqual(output_file.read(), self.output) self.assert_calls() + @mock.patch.object(connection.Connection, 'search_images') + def test_get_images(self, mock_search): + image1 = dict(id='123', name='mickey') + mock_search.return_value = [image1] + r = self.cloud.get_image('mickey') + self.assertIsNotNone(r) + self.assertDictEqual(image1, r) + + @mock.patch.object(connection.Connection, 'search_images') + def test_get_image_not_found(self, mock_search): + mock_search.return_value = [] + r = self.cloud.get_image('doesNotExist') + self.assertIsNone(r) + def test_get_image_name(self, cloud=None): cloud = cloud or self.cloud self.register_uris( diff --git a/openstack/tests/unit/cloud/test_network.py b/openstack/tests/unit/cloud/test_network.py index 717697f09..04880b9e1 100644 --- a/openstack/tests/unit/cloud/test_network.py +++ b/openstack/tests/unit/cloud/test_network.py @@ -11,16 +11,132 @@ # limitations under the License. import copy +from unittest import mock import testtools import openstack import openstack.cloud +from openstack import exceptions from openstack.network.v2 import network as _network from openstack.tests.unit import base -class TestNetwork(base.TestCase): +class TestNeutronExtensions(base.TestCase): + def test__neutron_extensions(self): + body = [ + { + "updated": "2014-06-1T10:00:00-00:00", + "name": "Distributed Virtual Router", + "links": [], + "alias": "dvr", + "description": "Enables configuration of Distributed Virtual Routers.", # noqa: E501 + }, + { + "updated": "2013-07-23T10:00:00-00:00", + "name": "Allowed Address Pairs", + "links": [], + "alias": "allowed-address-pairs", + "description": "Provides allowed address pairs", + }, + ] + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json=dict(extensions=body), + ) + ] + ) + extensions = self.cloud._neutron_extensions() + self.assertEqual(set(['dvr', 'allowed-address-pairs']), extensions) + + self.assert_calls() + + def test__neutron_extensions_fails(self): + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + status_code=404, + ) + ] + ) + with testtools.ExpectedException(exceptions.ResourceNotFound): + self.cloud._neutron_extensions() + + self.assert_calls() + + def test__has_neutron_extension(self): + body = [ + { + "updated": "2014-06-1T10:00:00-00:00", + "name": "Distributed Virtual Router", + "links": [], + "alias": "dvr", + "description": "Enables configuration of Distributed Virtual Routers.", # noqa: E501 + }, + { + "updated": "2013-07-23T10:00:00-00:00", + "name": "Allowed Address Pairs", + "links": [], + "alias": "allowed-address-pairs", + "description": "Provides allowed address pairs", + }, + ] + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json=dict(extensions=body), + ) + ] + ) + self.assertTrue(self.cloud._has_neutron_extension('dvr')) + self.assert_calls() + + def test__has_neutron_extension_missing(self): + body = [ + { + "updated": "2014-06-1T10:00:00-00:00", + "name": "Distributed Virtual Router", + "links": [], + "alias": "dvr", + "description": "Enables configuration of Distributed Virtual Routers.", # noqa: E501 + }, + { + "updated": "2013-07-23T10:00:00-00:00", + "name": "Allowed Address Pairs", + "links": [], + "alias": "allowed-address-pairs", + "description": "Provides allowed address pairs", + }, + ] + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'extensions'] + ), + json=dict(extensions=body), + ) + ] + ) + self.assertFalse(self.cloud._has_neutron_extension('invalid')) + self.assert_calls() + + +class TestNetworks(base.TestCase): mock_new_network_rep = { 'provider:physical_network': None, 'ipv6_address_scope': None, @@ -109,6 +225,12 @@ def test_list_networks_filtered(self): self.cloud.list_networks(filters={'name': 'test'}) self.assert_calls() + def test_list_networks_neutron_not_found(self): + self.use_nothing() + self.cloud.has_service = mock.Mock(return_value=False) + self.assertEqual([], self.cloud.list_networks()) + self.assert_calls() + def test_create_network(self): self.register_uris( [ From c05e17a0e9f9273c2e0107263eeb67b3ac3aecdb Mon Sep 17 00:00:00 2001 From: Mridula Joshi Date: Thu, 18 May 2023 11:36:50 +0000 Subject: [PATCH 3346/3836] image: Fixed URL for stores_info Updated the ``stores()`` by updating base-path for stores-info detail. It should be ``info/stores/detail Change-Id: If436e58d410e2b2670415ef962b5a7758103970b --- openstack/image/v2/_proxy.py | 2 +- openstack/image/v2/service_info.py | 2 ++ openstack/tests/unit/image/v2/test_service_info.py | 6 ++++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 448778311..8dfbf21a9 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -1615,7 +1615,7 @@ def stores(self, details=False, **query): :rtype: :class:`~openstack.image.v2.service_info.Store` """ if details: - query['base_path'] = utils.urljoin(_si.Store, 'details') + query['base_path'] = utils.urljoin(_si.Store.base_path, 'detail') return self._list(_si.Store, **query) # ====== IMPORTS ====== diff --git a/openstack/image/v2/service_info.py b/openstack/image/v2/service_info.py index 15023b028..1b27b5143 100644 --- a/openstack/image/v2/service_info.py +++ b/openstack/image/v2/service_info.py @@ -36,6 +36,8 @@ class Store(resource.Resource): description = resource.Body('description') #: default is_default = resource.Body('default', type=bool) + #: properties + properties = resource.Body('properties', type=dict) def delete_image(self, session, image, *, ignore_missing=False): """Delete image from store diff --git a/openstack/tests/unit/image/v2/test_service_info.py b/openstack/tests/unit/image/v2/test_service_info.py index 92c53c3f3..b9d354218 100644 --- a/openstack/tests/unit/image/v2/test_service_info.py +++ b/openstack/tests/unit/image/v2/test_service_info.py @@ -29,6 +29,11 @@ 'id': IDENTIFIER, 'description': 'Fast access to rbd store', 'default': True, + 'properties': { + "pool": "pool1", + "chunk_size": 65536, + "thin_provisioning": False, + }, } @@ -49,6 +54,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE_STORE['id'], sot.id) self.assertEqual(EXAMPLE_STORE['description'], sot.description) self.assertEqual(EXAMPLE_STORE['default'], sot.is_default) + self.assertEqual(EXAMPLE_STORE['properties'], sot.properties) @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) def test_delete_image(self): From e24747d27aa786ddbaad4d89c1b7718401b8881a Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 27 Jul 2023 10:27:17 +0100 Subject: [PATCH 3347/3836] cloud: Remove unnecessary '_make_unicode' helper Everything is unicode in Python 3. Change-Id: I8273945e579246a1c35128bc2f23b91f7f821a2f Signed-off-by: Stephen Finucane --- openstack/cloud/_utils.py | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index 573ce9540..758febdd2 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -31,24 +31,6 @@ _decorated_methods = [] -def _make_unicode(input): - """Turn an input into unicode unconditionally - - :param input: A unicode, string or other object - """ - try: - if isinstance(input, unicode): - return input - if isinstance(input, str): - return input.decode('utf-8') - else: - # int, for example - return unicode(input) - except NameError: - # python3! - return str(input) - - def _dictify_resource(resource): if isinstance(resource, list): return [_dictify_resource(r) for r in resource] @@ -88,7 +70,7 @@ def _filter_list(data, name_or_id, filters): log = _log.setup_logging('openstack.fnmatch') if name_or_id: # name_or_id might already be unicode - name_or_id = _make_unicode(name_or_id) + name_or_id = str(name_or_id) identifier_matches = [] bad_pattern = False try: @@ -100,8 +82,8 @@ def _filter_list(data, name_or_id, filters): # search fn_reg = None for e in data: - e_id = _make_unicode(e.get('id', None)) - e_name = _make_unicode(e.get('name', None)) + e_id = str(e.get('id', None)) + e_name = str(e.get('name', None)) if (e_id and e_id == name_or_id) or ( e_name and e_name == name_or_id From 41d19a5591eb11148fafb58cfbfc549324524f56 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 27 Jul 2023 10:54:11 +0100 Subject: [PATCH 3348/3836] cloud: Remove unnecessary types import The 'openstack.resource2' module hasn't been a thing for a long time. Change-Id: I2cad44a0fb11f7d20af22da48f355432d213a7a0 Signed-off-by: Stephen Finucane --- openstack/cloud/_accelerator.py | 3 --- openstack/cloud/_baremetal.py | 5 ----- openstack/cloud/_block_storage.py | 4 ---- openstack/cloud/_coe.py | 4 ---- openstack/cloud/_compute.py | 3 --- openstack/cloud/_dns.py | 5 ----- openstack/cloud/_floating_ip.py | 4 ---- openstack/cloud/_image.py | 5 ----- openstack/cloud/_network.py | 4 ---- openstack/cloud/_network_common.py | 4 ---- openstack/cloud/_object_store.py | 4 ---- openstack/cloud/_orchestration.py | 5 ----- openstack/cloud/_security_group.py | 6 ------ openstack/cloud/openstackcloud.py | 6 +----- 14 files changed, 1 insertion(+), 61 deletions(-) diff --git a/openstack/cloud/_accelerator.py b/openstack/cloud/_accelerator.py index 5cd7b124f..c54ff8c72 100644 --- a/openstack/cloud/_accelerator.py +++ b/openstack/cloud/_accelerator.py @@ -10,9 +10,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -# import types so that we can reference ListType in sphinx param declarations. -# We can't just use list, because sphinx gets confused by -# openstack.resource.Resource.list and openstack.resource2.Resource.list from openstack.accelerator.v2._proxy import Proxy diff --git a/openstack/cloud/_baremetal.py b/openstack/cloud/_baremetal.py index 02f57fd57..aae84eb12 100644 --- a/openstack/cloud/_baremetal.py +++ b/openstack/cloud/_baremetal.py @@ -10,13 +10,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# import types so that we can reference ListType in sphinx param declarations. -# We can't just use list, because sphinx gets confused by -# openstack.resource.Resource.list and openstack.resource2.Resource.list - import contextlib import sys -import types # noqa import warnings import jsonpatch diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index 1568db019..c85e3fd27 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -10,10 +10,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -# import types so that we can reference ListType in sphinx param declarations. -# We can't just use list, because sphinx gets confused by -# openstack.resource.Resource.list and openstack.resource2.Resource.list -import types # noqa import warnings from openstack.block_storage.v3._proxy import Proxy diff --git a/openstack/cloud/_coe.py b/openstack/cloud/_coe.py index cbbe63187..c10553467 100644 --- a/openstack/cloud/_coe.py +++ b/openstack/cloud/_coe.py @@ -10,10 +10,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -# import types so that we can reference ListType in sphinx param declarations. -# We can't just use list, because sphinx gets confused by -# openstack.resource.Resource.list and openstack.resource2.Resource.list - from openstack.cloud import _utils from openstack.cloud import exc diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 81be3f31c..19b145692 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -10,9 +10,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -# import types so that we can reference ListType in sphinx param declarations. -# We can't just use list, because sphinx gets confused by -# openstack.resource.Resource.list and openstack.resource2.Resource.list import base64 import functools import operator diff --git a/openstack/cloud/_dns.py b/openstack/cloud/_dns.py index 4001aa6bd..a896f90b6 100644 --- a/openstack/cloud/_dns.py +++ b/openstack/cloud/_dns.py @@ -10,11 +10,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -# import types so that we can reference ListType in sphinx param declarations. -# We can't just use list, because sphinx gets confused by -# openstack.resource.Resource.list and openstack.resource2.Resource.list -import types # noqa - from openstack.cloud import _utils from openstack.cloud import exc from openstack.dns.v2._proxy import Proxy diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index 452e1c0f8..31037b2fe 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -10,13 +10,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -# import types so that we can reference ListType in sphinx param declarations. -# We can't just use list, because sphinx gets confused by -# openstack.resource.Resource.list and openstack.resource2.Resource.list import ipaddress import threading import time -import types # noqa import warnings from openstack.cloud import _utils diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py index 822b97a2b..54cb09b08 100644 --- a/openstack/cloud/_image.py +++ b/openstack/cloud/_image.py @@ -10,11 +10,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -# import types so that we can reference ListType in sphinx param declarations. -# We can't just use list, because sphinx gets confused by -# openstack.resource.Resource.list and openstack.resource2.Resource.list -import types # noqa - from openstack.cloud import _utils from openstack.cloud import exc from openstack.image.v2._proxy import Proxy diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index ae7d96f94..7fdb63e30 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -10,12 +10,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# import types so that we can reference ListType in sphinx param declarations. -# We can't just use list, because sphinx gets confused by -# openstack.resource.Resource.list and openstack.resource2.Resource.list import threading import time -import types # noqa from openstack.cloud import _utils from openstack.cloud import exc diff --git a/openstack/cloud/_network_common.py b/openstack/cloud/_network_common.py index b466a8edc..4a9f31c65 100644 --- a/openstack/cloud/_network_common.py +++ b/openstack/cloud/_network_common.py @@ -10,11 +10,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# import types so that we can reference ListType in sphinx param declarations. -# We can't just use list, because sphinx gets confused by -# openstack.resource.Resource.list and openstack.resource2.Resource.list import threading -import types # noqa from openstack.cloud import exc diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index 1c45b0f28..3fe735bd1 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -10,11 +10,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# import types so that we can reference ListType in sphinx param declarations. -# We can't just use list, because sphinx gets confused by -# openstack.resource.Resource.list and openstack.resource2.Resource.list import concurrent.futures -import types # noqa import urllib.parse import keystoneauth1.exceptions diff --git a/openstack/cloud/_orchestration.py b/openstack/cloud/_orchestration.py index e994f286f..dac0f3309 100644 --- a/openstack/cloud/_orchestration.py +++ b/openstack/cloud/_orchestration.py @@ -10,11 +10,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -# import types so that we can reference ListType in sphinx param declarations. -# We can't just use list, because sphinx gets confused by -# openstack.resource.Resource.list and openstack.resource2.Resource.list -import types # noqa - from openstack.cloud import _utils from openstack.cloud import exc from openstack.orchestration.util import event_utils diff --git a/openstack/cloud/_security_group.py b/openstack/cloud/_security_group.py index 44a1bfdd8..ecbdb4246 100644 --- a/openstack/cloud/_security_group.py +++ b/openstack/cloud/_security_group.py @@ -10,12 +10,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -# import types so that we can reference ListType in sphinx param declarations. -# We can't just use list, because sphinx gets confused by -# openstack.resource.Resource.list and openstack.resource2.Resource.list -# import jsonpatch -import types # noqa - from openstack.cloud import _utils from openstack.cloud import exc from openstack import exceptions diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 7c751cb12..555bd7ce1 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -9,14 +9,10 @@ # 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 functools import queue - -# import types so that we can reference ListType in sphinx param declarations. -# We can't just use list, because sphinx gets confused by -# openstack.resource.Resource.list and openstack.resource2.Resource.list -import types # noqa import warnings import dogpile.cache From f9bea72ac5c6271d6bc23e27057a7c1dc33a2c62 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 25 Jul 2023 13:12:35 +0100 Subject: [PATCH 3349/3836] resource: Remove unused 'Resource.service' attribute This has been unused since we switched to discovery way back in change I11c16d37d3ab3d77bed3a0bcbd98f1fa33b9555f. Change-Id: I3a60ab7b5a9fcdee9765e3c53497d1c1c49c511f Signed-off-by: Stephen Finucane --- openstack/resource.py | 6 ------ openstack/tests/unit/compute/v2/test_limits.py | 1 - 2 files changed, 7 deletions(-) diff --git a/openstack/resource.py b/openstack/resource.py index e8c1830c2..35e9c908f 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -471,9 +471,6 @@ class Resource(dict): #: The base part of the URI for this resource. base_path = "" - #: The service associated with this resource to find the service URL. - service = None - #: Allow create operation for this resource. allow_create = False #: Allow get operation for this resource. @@ -1339,9 +1336,6 @@ def _get_session(cls, session): # Resource class that is calling session.$something to be complete. if isinstance(session, adapter.Adapter): return session - if hasattr(session, '_sdk_connection'): - service_type = cls.service['service_type'] - return getattr(session._sdk_connection, service_type) raise ValueError( "The session argument to Resource methods requires either an" " instance of an openstack.proxy.Proxy object or at the very least" diff --git a/openstack/tests/unit/compute/v2/test_limits.py b/openstack/tests/unit/compute/v2/test_limits.py index 90da84d9d..4971dad1d 100644 --- a/openstack/tests/unit/compute/v2/test_limits.py +++ b/openstack/tests/unit/compute/v2/test_limits.py @@ -113,7 +113,6 @@ def test_basic(self): self.assertIsNone(sot.resource_key) self.assertIsNone(sot.resources_key) self.assertEqual("", sot.base_path) - self.assertIsNone(sot.service) self.assertFalse(sot.allow_create) self.assertFalse(sot.allow_fetch) self.assertFalse(sot.allow_commit) From 3163c7597df8334023742e0c0981b26f465a7181 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 25 Jul 2023 14:32:57 +0100 Subject: [PATCH 3350/3836] openstack.format: Remove 'serialize' classmethod This is not used anywhere. Drop it. A release note is included although it likely isn't necessary since this will have no effect as again we weren't using this. Change-Id: I3878216a12a387c9c30fcbadad4da0019612fc5a Signed-off-by: Stephen Finucane --- openstack/format.py | 16 ---------------- openstack/key_manager/v1/_format.py | 8 -------- openstack/tests/unit/test_format.py | 7 ------- openstack/tests/unit/test_resource.py | 4 ---- ...p-formatter-deserialize-30b19956fb79bb8d.yaml | 5 +++++ 5 files changed, 5 insertions(+), 35 deletions(-) create mode 100644 releasenotes/notes/drop-formatter-deserialize-30b19956fb79bb8d.yaml diff --git a/openstack/format.py b/openstack/format.py index 3a750faad..b72c34f97 100644 --- a/openstack/format.py +++ b/openstack/format.py @@ -12,11 +12,6 @@ class Formatter: - @classmethod - def serialize(cls, value): - """Return a string representing the formatted value""" - raise NotImplementedError - @classmethod def deserialize(cls, value): """Return a formatted object representing the value""" @@ -36,14 +31,3 @@ def deserialize(cls, value): raise ValueError( "Unable to deserialize boolean string: %s" % value ) - - @classmethod - def serialize(cls, value): - """Convert a boolean to a boolean string""" - if isinstance(value, bool): - if value: - return "true" - else: - return "false" - else: - raise ValueError("Unable to serialize boolean string: %s" % value) diff --git a/openstack/key_manager/v1/_format.py b/openstack/key_manager/v1/_format.py index 8da3515ac..58313c0a0 100644 --- a/openstack/key_manager/v1/_format.py +++ b/openstack/key_manager/v1/_format.py @@ -28,11 +28,3 @@ def deserialize(cls, value): # The UUID will be the last portion of the URI. return parts.path.split("/")[-1] - - @classmethod - def serialize(cls, value): - # NOTE(briancurtin): If we had access to the session to get - # the endpoint we could do something smart here like take an ID - # and give back an HREF, but this will just have to be something - # that works different because Barbican does what it does... - return value diff --git a/openstack/tests/unit/test_format.py b/openstack/tests/unit/test_format.py index 50b5b04da..532133502 100644 --- a/openstack/tests/unit/test_format.py +++ b/openstack/tests/unit/test_format.py @@ -27,10 +27,3 @@ def test_deserialize(self): self.assertRaises(ValueError, format.BoolStr.deserialize, None) self.assertRaises(ValueError, format.BoolStr.deserialize, '') self.assertRaises(ValueError, format.BoolStr.deserialize, 'INVALID') - - def test_serialize(self): - self.assertEqual('true', format.BoolStr.serialize(True)) - self.assertEqual('false', format.BoolStr.serialize(False)) - self.assertRaises(ValueError, format.BoolStr.serialize, None) - self.assertRaises(ValueError, format.BoolStr.serialize, '') - self.assertRaises(ValueError, format.BoolStr.serialize, 'True') diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index c05b6c4d4..7d09eda4b 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -204,10 +204,6 @@ class Parent: class FakeFormatter(format.Formatter): calls = [] - @classmethod - def serialize(cls, arg): - FakeFormatter.calls.append(arg) - @classmethod def deserialize(cls, arg): FakeFormatter.calls.append(arg) diff --git a/releasenotes/notes/drop-formatter-deserialize-30b19956fb79bb8d.yaml b/releasenotes/notes/drop-formatter-deserialize-30b19956fb79bb8d.yaml new file mode 100644 index 000000000..d8415bf62 --- /dev/null +++ b/releasenotes/notes/drop-formatter-deserialize-30b19956fb79bb8d.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + The ``openstack.format.Formatter`` class no longer defines a ``serialize`` + method to override. This was unused and unneccessary complexity. From 2a8627d4f1411c26a2b3efbb18df7e3d9ca453d7 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 25 Jul 2023 13:15:11 +0100 Subject: [PATCH 3351/3836] mypy: Address issues with top-level files Address issues in all files in the 'openstack' directory as well as the 'openstack/common', 'openstack/config' and 'openstack/test' directories. With this done, we can start introducing mypy iteratively. Note that we disable type hints in Sphinx. This is necessary because Sphinx apparently can't tell the difference between 'Type' from 'typing' and 'Type' from 'openstack.block_storage.v[23].Type', which causes a build warning. This is okay since typing makes docs too noisy anyway. Change-Id: Ia91c5da779b5b68c408dfc934a21d77e9ca2f550 Signed-off-by: Stephen Finucane --- doc/source/conf.py | 14 +++--- openstack/common/metadata.py | 5 ++ openstack/common/quota_set.py | 5 +- openstack/common/tag.py | 9 ++++ openstack/config/cloud_region.py | 3 +- openstack/config/loader.py | 11 +++-- openstack/config/vendors/__init__.py | 3 +- openstack/connection.py | 2 +- openstack/exceptions.py | 3 ++ openstack/network/v2/_proxy.py | 19 +++----- openstack/network/v2/port.py | 5 +- openstack/proxy.py | 73 ++++++++++++++++++---------- openstack/resource.py | 50 +++++++++++-------- openstack/service_description.py | 7 +-- openstack/test/fakes.py | 8 +-- openstack/tests/base.py | 7 ++- openstack/utils.py | 10 ++-- 17 files changed, 146 insertions(+), 88 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 792110237..ec4dab832 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -60,7 +60,13 @@ # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'native' -autodoc_member_order = "bysource" +autodoc_member_order = 'bysource' + +# Include both the class and __init__ docstrings when describing the class +autoclass_content = 'both' + +# Don't document type hints as they're too noisy +autodoc_typehints = 'none' # Locations to exclude when looking for source files. exclude_patterns = [] @@ -70,8 +76,7 @@ # Don't let openstackdocstheme insert TOCs automatically. theme_include_auto_toc = False -# Output file base name for HTML help builder. -htmlhelp_basename = 'openstacksdkdoc' +# -- Options for LaTeX output --------------------------------------------- # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass @@ -91,6 +96,3 @@ # Disable usage of xindy https://bugzilla.redhat.com/show_bug.cgi?id=1643664 latex_use_xindy = False - -# Include both the class and __init__ docstrings when describing the class -autoclass_content = "both" diff --git a/openstack/common/metadata.py b/openstack/common/metadata.py index 81a6a98ef..36c30b1d0 100644 --- a/openstack/common/metadata.py +++ b/openstack/common/metadata.py @@ -9,12 +9,17 @@ # 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 import resource from openstack import utils class MetadataMixin: + id: resource.Body + base_path: str + _body: resource._ComponentManager + #: *Type: list of tag strings* metadata = resource.Body('metadata', type=dict) diff --git a/openstack/common/quota_set.py b/openstack/common/quota_set.py index c298416e6..f5afd7c04 100644 --- a/openstack/common/quota_set.py +++ b/openstack/common/quota_set.py @@ -9,6 +9,9 @@ # 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 typing as ty + from openstack import exceptions from openstack import resource @@ -88,7 +91,7 @@ def _translate_response(self, response, has_body=None, error_message=None): body.pop("self", None) # Process body_attrs to strip usage and reservation out - normalized_attrs = dict( + normalized_attrs: ty.Dict[str, ty.Any] = dict( reservation={}, usage={}, ) diff --git a/openstack/common/tag.py b/openstack/common/tag.py index 0d25693ff..e49a38268 100644 --- a/openstack/common/tag.py +++ b/openstack/common/tag.py @@ -9,12 +9,21 @@ # 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 import resource from openstack import utils class TagMixin: + id: resource.Body + base_path: str + _body: resource._ComponentManager + + @classmethod + def _get_session(cls, session): + ... + _tag_query_parameters = { 'tags': 'tags', 'any_tags': 'tags-any', diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index ddd91bc8e..ee16dd612 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -14,6 +14,7 @@ import copy import os.path +import typing as ty import urllib import warnings @@ -195,7 +196,7 @@ def from_conf(conf, session=None, service_types=None, **kwargs): ), ) continue - opt_dict = {} + opt_dict: ty.Dict[str, str] = {} # Populate opt_dict with (appropriately processed) Adapter conf opts try: ks_load_adap.process_conf_options(conf[project_name], opt_dict) diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 8df3422be..1f330e8f4 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -21,6 +21,7 @@ import os import re import sys +import typing as ty import warnings import appdirs @@ -129,7 +130,7 @@ def _fix_argv(argv): argv[index] = "=".join(split_args) # Save both for later so we can throw an error about dupes processed[new].add(orig) - overlap = [] + overlap: ty.List[str] = [] for new, old in processed.items(): if len(old) > 1: overlap.extend(old) @@ -297,8 +298,8 @@ def __init__( self._cache_expiration_time = 0 self._cache_path = CACHE_PATH self._cache_class = 'dogpile.cache.null' - self._cache_arguments = {} - self._cache_expirations = {} + self._cache_arguments: ty.Dict[str, ty.Any] = {} + self._cache_expirations: ty.Dict[str, int] = {} self._influxdb_config = {} if 'cache' in self.cloud_config: cache_settings = _util.normalize_keys(self.cloud_config['cache']) @@ -514,8 +515,8 @@ def _get_known_regions(self, cloud): return self._expand_regions(regions) else: # crappit. we don't have a region defined. - new_cloud = dict() - our_cloud = self.cloud_config['clouds'].get(cloud, dict()) + new_cloud: ty.Dict[str, ty.Any] = {} + our_cloud = self.cloud_config['clouds'].get(cloud, {}) self._expand_vendor_profile(cloud, new_cloud, our_cloud) if 'regions' in new_cloud and new_cloud['regions']: return self._expand_regions(new_cloud['regions']) diff --git a/openstack/config/vendors/__init__.py b/openstack/config/vendors/__init__.py index 95c5ccb30..189d3744a 100644 --- a/openstack/config/vendors/__init__.py +++ b/openstack/config/vendors/__init__.py @@ -15,6 +15,7 @@ import glob import json import os +import typing as ty import urllib import requests @@ -24,7 +25,7 @@ from openstack import exceptions _VENDORS_PATH = os.path.dirname(os.path.realpath(__file__)) -_VENDOR_DEFAULTS = {} +_VENDOR_DEFAULTS: ty.Dict[str, ty.Dict] = {} _WELL_KNOWN_PATH = "{scheme}://{netloc}/.well-known/openstack/api" diff --git a/openstack/connection.py b/openstack/connection.py index 430666c16..b0f317395 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -209,7 +209,7 @@ import importlib.metadata as importlib_metadata except ImportError: # For everyone else - import importlib_metadata + import importlib_metadata # type: ignore import keystoneauth1.exceptions import requestsexceptions diff --git a/openstack/exceptions.py b/openstack/exceptions.py index 93a4b9d06..439e85123 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -18,6 +18,7 @@ import json import re +import typing as ty from requests import exceptions as _rex @@ -214,6 +215,7 @@ def raise_from_response(response, error_message=None): if response.status_code < 400: return + cls: ty.Type[SDKException] if response.status_code == 400: cls = BadRequestException elif response.status_code == 403: @@ -251,6 +253,7 @@ def raise_from_response(response, error_message=None): message = re.sub(r'<.+?>', '', line.strip()) if message not in messages: messages.append(message) + # Return joined string separated by colons. details = ': '.join(messages) diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 42e98ad4f..66b6b4619 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -9,10 +9,8 @@ # 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 typing import Generic -from typing import Optional -from typing import Type -from typing import TypeVar + +import typing as ty from openstack import exceptions from openstack.network.v2 import address_group as _address_group @@ -93,11 +91,10 @@ ) from openstack.network.v2 import vpn_service as _vpn_service from openstack import proxy - -T = TypeVar('T') +from openstack import resource -class Proxy(proxy.Proxy, Generic[T]): +class Proxy(proxy.Proxy): _resource_registry = { "address_group": _address_group.AddressGroup, "address_scope": _address_scope.AddressScope, @@ -179,24 +176,24 @@ class Proxy(proxy.Proxy, Generic[T]): @proxy._check_resource(strict=False) def _update( self, - resource_type: Type[T], + resource_type: ty.Type[resource.Resource], value, base_path=None, if_revision=None, **attrs, - ) -> T: + ) -> resource.Resource: res = self._get_resource(resource_type, value, **attrs) return res.commit(self, base_path=base_path, if_revision=if_revision) @proxy._check_resource(strict=False) def _delete( self, - resource_type: Type[T], + resource_type: ty.Type[resource.Resource], value, ignore_missing=True, if_revision=None, **attrs, - ) -> Optional[T]: + ) -> ty.Optional[resource.Resource]: res = self._get_resource(resource_type, value, **attrs) try: diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index c0fba5d2d..33ea63e69 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -9,7 +9,8 @@ # 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 typing import List + +import typing as ty from openstack.common import tag from openstack.network.v2 import _base @@ -58,7 +59,7 @@ class Port(_base.NetworkResource, tag.TagMixin): # Properties #: Allowed address pairs list. Dictionary key ``ip_address`` is required #: and key ``mac_address`` is optional. - allowed_address_pairs: List[dict] = resource.Body( + allowed_address_pairs: ty.List[dict] = resource.Body( 'allowed_address_pairs', type=list ) #: The ID of the host where the port is allocated. In some cases, diff --git a/openstack/proxy.py b/openstack/proxy.py index ff2dd75dc..e96e2f6d8 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -11,11 +11,7 @@ # under the License. import functools -from typing import Generator -from typing import Generic -from typing import Optional -from typing import Type -from typing import TypeVar +import typing as ty import urllib from urllib.parse import urlparse @@ -24,7 +20,7 @@ JSONDecodeError = simplejson.scanner.JSONDecodeError except ImportError: - JSONDecodeError = ValueError + JSONDecodeError = ValueError # type: ignore import iso8601 import jmespath from keystoneauth1 import adapter @@ -33,7 +29,8 @@ from openstack import exceptions from openstack import resource -T = TypeVar('T') + +ResourceType = ty.TypeVar('ResourceType', bound=resource.Resource) # The _check_resource decorator is used on Proxy methods to ensure that @@ -74,7 +71,7 @@ def normalize_metric_name(name): return name -class Proxy(adapter.Adapter, Generic[T]): +class Proxy(adapter.Adapter): """Represents a service.""" retriable_status_codes = None @@ -84,7 +81,7 @@ class Proxy(adapter.Adapter, Generic[T]): ``_status_code_retries``. """ - _resource_registry = dict() + _resource_registry: ty.Dict[str, ty.Type[resource.Resource]] = {} """Registry of the supported resourses. Dictionary of resource names (key) types (value). @@ -431,7 +428,9 @@ def _get_connection(self): self, '_connection', getattr(self.session, '_sdk_connection', None) ) - def _get_resource(self, resource_type: Type[T], value, **attrs) -> T: + def _get_resource( + self, resource_type: ty.Type[ResourceType], value, **attrs + ) -> ResourceType: """Get a resource object to work on :param resource_type: The type of resource to operate on. This should @@ -478,8 +477,12 @@ def _get_uri_attribute(self, child, parent, name): return value def _find( - self, resource_type: Type[T], name_or_id, ignore_missing=True, **attrs - ) -> Optional[T]: + self, + resource_type: ty.Type[ResourceType], + name_or_id, + ignore_missing=True, + **attrs, + ) -> ty.Optional[ResourceType]: """Find a resource :param name_or_id: The name or ID of a resource to find. @@ -500,8 +503,12 @@ def _find( @_check_resource(strict=False) def _delete( - self, resource_type: Type[T], value, ignore_missing=True, **attrs - ): + self, + resource_type: ty.Type[ResourceType], + value, + ignore_missing=True, + **attrs, + ) -> ty.Optional[ResourceType]: """Delete a resource :param resource_type: The type of resource to delete. This should @@ -538,8 +545,12 @@ def _delete( @_check_resource(strict=False) def _update( - self, resource_type: Type[T], value, base_path=None, **attrs - ) -> T: + self, + resource_type: ty.Type[ResourceType], + value, + base_path=None, + **attrs, + ) -> ResourceType: """Update a resource :param resource_type: The type of resource to update. @@ -563,7 +574,12 @@ def _update( res = self._get_resource(resource_type, value, **attrs) return res.commit(self, base_path=base_path) - def _create(self, resource_type: Type[T], base_path=None, **attrs): + def _create( + self, + resource_type: ty.Type[ResourceType], + base_path=None, + **attrs, + ) -> ResourceType: """Create a resource from attributes :param resource_type: The type of resource to create. @@ -588,8 +604,11 @@ def _create(self, resource_type: Type[T], base_path=None, **attrs): return res.create(self, base_path=base_path) def _bulk_create( - self, resource_type: Type[T], data, base_path=None - ) -> Generator[T, None, None]: + self, + resource_type: ty.Type[ResourceType], + data, + base_path=None, + ) -> ty.Generator[ResourceType, None, None]: """Create a resource from attributes :param resource_type: The type of resource to create. @@ -612,13 +631,13 @@ def _bulk_create( @_check_resource(strict=False) def _get( self, - resource_type: Type[T], + resource_type: ty.Type[ResourceType], value=None, requires_id=True, base_path=None, skip_cache=False, **attrs, - ): + ) -> ResourceType: """Fetch a resource :param resource_type: The type of resource to get. @@ -655,12 +674,12 @@ def _get( def _list( self, - resource_type: Type[T], + resource_type: ty.Type[ResourceType], paginated=True, base_path=None, jmespath_filters=None, **attrs, - ) -> Generator[T, None, None]: + ) -> ty.Generator[ResourceType, None, None]: """List a resource :param resource_type: The type of resource to list. This should @@ -696,8 +715,12 @@ def _list( return data def _head( - self, resource_type: Type[T], value=None, base_path=None, **attrs - ): + self, + resource_type: ty.Type[ResourceType], + value=None, + base_path=None, + **attrs, + ) -> ResourceType: """Retrieve a resource's header :param resource_type: The type of resource to retrieve. diff --git a/openstack/resource.py b/openstack/resource.py index 35e9c908f..50387a579 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -32,10 +32,12 @@ class that represent a remote resource. The attributes that and then returned to the caller. """ +import abc import collections import inspect import itertools import operator +import typing as ty import urllib.parse import warnings @@ -93,11 +95,11 @@ def _convert_type(value, data_type, list_type=None): return data_type() -class _BaseComponent: +class _BaseComponent(abc.ABC): # The name this component is being tracked as in the Resource - key = None + key: str # The class to be used for mappings - _map_cls = dict + _map_cls: ty.Type[ty.Mapping] = dict #: Marks the property as deprecated. deprecated = False @@ -270,6 +272,8 @@ class Computed(_BaseComponent): class _ComponentManager(collections.abc.MutableMapping): """Storage of a component type""" + attributes: ty.Dict[str, ty.Any] + def __init__(self, attributes=None, synchronized=False): self.attributes = dict() if attributes is None else attributes.copy() self._dirty = set() if synchronized else set(self.attributes.keys()) @@ -452,14 +456,15 @@ class Resource(dict): # will work properly. #: Singular form of key for resource. - resource_key = None + resource_key: ty.Optional[str] = None #: Plural form of key for resource. - resources_key = None + resources_key: ty.Optional[str] = None #: Key used for pagination links pagination_key = None #: The ID of this resource. id = Body("id") + #: The name of this resource. name = Body("name") #: The OpenStack location of this resource. @@ -469,7 +474,7 @@ class Resource(dict): _query_mapping = QueryParameters() #: The base part of the URI for this resource. - base_path = "" + base_path: str = "" #: Allow create operation for this resource. allow_create = False @@ -508,22 +513,22 @@ class Resource(dict): create_returns_body = None #: Maximum microversion to use for getting/creating/updating the Resource - _max_microversion = None + _max_microversion: ty.Optional[str] = None #: API microversion (string or None) this Resource was loaded with microversion = None _connection = None - _body = None - _header = None - _uri = None - _computed = None - _original_body = None + _body: _ComponentManager + _header: _ComponentManager + _uri: _ComponentManager + _computed: _ComponentManager + _original_body: ty.Dict[str, ty.Any] = {} _store_unknown_attrs_as_properties = False _allow_unknown_attrs_in_body = False - _unknown_attrs_in_body = None + _unknown_attrs_in_body: ty.Dict[str, ty.Any] = {} # Placeholder for aliases as dict of {__alias__:__original} - _attr_aliases = {} + _attr_aliases: ty.Dict[str, str] = {} def __init__(self, _synchronized=False, connection=None, **attrs): """The base resource @@ -1072,12 +1077,13 @@ def to_dict( :return: A dictionary of key/value pairs where keys are named as they exist as attributes of this class. """ + mapping: ty.Union[utils.Munch, ty.Dict] if _to_munch: mapping = utils.Munch() else: mapping = {} - components = [] + components: ty.List[ty.Type[_BaseComponent]] = [] if body: components.append(Body) if headers: @@ -1089,9 +1095,6 @@ def to_dict( "At least one of `body`, `headers` or `computed` must be True" ) - # isinstance stricly requires this to be a tuple - components = tuple(components) - if body and self._allow_unknown_attrs_in_body: for key in self._unknown_attrs_in_body: converted = self._attr_to_dict( @@ -1105,7 +1108,8 @@ def to_dict( # but is slightly different in that we're looking at an instance # and we're mapping names on this class to their actual stored # values. - for attr, component in self._attributes_iterator(components): + # NOTE: isinstance stricly requires components to be a tuple + for attr, component in self._attributes_iterator(tuple(components)): if original_names: key = component.name else: @@ -1167,6 +1171,7 @@ def _prepare_request_body( *, resource_request_key=None, ): + body: ty.Union[ty.Dict[str, ty.Any], ty.List[ty.Any]] if patch: if not self._store_unknown_attrs_as_properties: # Default case @@ -1592,7 +1597,7 @@ def bulk_create( "Invalid create method: %s" % cls.create_method ) - body = [] + _body: ty.List[ty.Any] = [] resources = [] for attrs in data: # NOTE(gryf): we need to create resource objects, since @@ -1605,9 +1610,12 @@ def bulk_create( request = resource._prepare_request( requires_id=requires_id, base_path=base_path ) - body.append(request.body) + _body.append(request.body) + + body: ty.Union[ty.Dict[str, ty.Any], ty.List[ty.Any]] = _body if prepend_key: + assert cls.resources_key body = {cls.resources_key: body} response = method( diff --git a/openstack/service_description.py b/openstack/service_description.py index dab5d0598..eab27a88f 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -11,6 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty import warnings import os_service_types @@ -44,11 +45,11 @@ def __getattr__(self, item): class ServiceDescription: #: Dictionary of supported versions and proxy classes for that version - supported_versions = None + supported_versions: ty.Dict[str, ty.Type[proxy_mod.Proxy]] = {} #: main service_type to use to find this service in the catalog - service_type = None + service_type: str #: list of aliases this service might be registered as - aliases = [] + aliases: ty.List[str] = [] def __init__(self, service_type, supported_versions=None, aliases=None): """Class describing how to interact with a REST service. diff --git a/openstack/test/fakes.py b/openstack/test/fakes.py index a405e4642..857b56a56 100644 --- a/openstack/test/fakes.py +++ b/openstack/test/fakes.py @@ -67,7 +67,7 @@ def generate_fake_resource( :raises NotImplementedError: If a resource attribute specifies a ``type`` or ``list_type`` that cannot be automatically generated """ - base_attrs = dict() + base_attrs: Dict[str, Any] = {} for name, value in inspect.getmembers( resource_type, predicate=lambda x: isinstance(x, (resource.Body, resource.URI)), @@ -182,7 +182,7 @@ def generate_fake_resources( # (better) type annotations def generate_fake_proxy( service: Type[service_description.ServiceDescription], - api_version: Optional[int] = None, + api_version: Optional[str] = None, ) -> proxy.Proxy: """Generate a fake proxy for the given service type @@ -246,10 +246,10 @@ def generate_fake_proxy( ) else: api_version = list(supported_versions)[0] - elif str(api_version) not in supported_versions: + elif api_version not in supported_versions: raise ValueError( f"API version {api_version} is not supported by openstacksdk. " f"Supported API versions are: {', '.join(supported_versions)}" ) - return mock.create_autospec(supported_versions[str(api_version)]) + return mock.create_autospec(supported_versions[api_version]) diff --git a/openstack/tests/base.py b/openstack/tests/base.py index 142d80b50..88cdebcbb 100644 --- a/openstack/tests/base.py +++ b/openstack/tests/base.py @@ -13,11 +13,12 @@ # License for the specific language governing permissions and limitations # under the License. -from io import StringIO +import io import logging import os import pprint import sys +import typing as ty import fixtures from oslotest import base @@ -59,8 +60,10 @@ def setUp(self): self.warnings = self.useFixture(os_fixtures.WarningsFixture()) + self._log_stream: ty.TextIO + if os.environ.get('OS_LOG_CAPTURE') in _TRUE_VALUES: - self._log_stream = StringIO() + self._log_stream = io.StringIO() if os.environ.get('OS_ALWAYS_LOG') in _TRUE_VALUES: self.addCleanup(self.printLogs) else: diff --git a/openstack/utils.py b/openstack/utils.py index 012fdbab8..24c409c58 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -16,6 +16,7 @@ import string import threading import time +import typing as ty import keystoneauth1 from keystoneauth1 import adapter as ks_adapter @@ -417,7 +418,7 @@ def node_done(self, node): def _start_traverse(self): """Initialize graph traversing""" self._run_in_degree = self._get_in_degree() - self._queue = queue.Queue() + self._queue: queue.Queue[str] = queue.Queue() self._done = set() self._it_cnt = len(self._graph) @@ -427,8 +428,7 @@ def _start_traverse(self): def _get_in_degree(self): """Calculate the in_degree (count incoming) for nodes""" - _in_degree = dict() - _in_degree = {u: 0 for u in self._graph.keys()} + _in_degree: ty.Dict[str, int] = {u: 0 for u in self._graph.keys()} for u in self._graph: for v in self._graph[u]: _in_degree[v] += 1 @@ -568,7 +568,7 @@ def setdefault(self, k, d=None): def munchify(x, factory=Munch): """Recursively transforms a dictionary into a Munch via copy.""" # Munchify x, using `seen` to track object cycles - seen = dict() + seen: ty.Dict[int, ty.Any] = dict() def munchify_cycles(obj): try: @@ -608,7 +608,7 @@ def unmunchify(x): """Recursively converts a Munch into a dictionary.""" # Munchify x, using `seen` to track object cycles - seen = dict() + seen: ty.Dict[int, ty.Any] = dict() def unmunchify_cycles(obj): try: From c9a877e2de25fdd82c0444fd794c0fd48b645b59 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 25 Jul 2023 12:34:40 +0100 Subject: [PATCH 3352/3836] Run mypy as pre-commit Note that mypy is the last item in the list of checks since flake8 is going to catch e.g. syntax issues which should be addressed first. Change-Id: Id58fb9e8ea454c10e502c96ad4c36788e7533318 Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 43 +++++++++++++++++++++++++++++++++++++++ setup.cfg | 45 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c8c9f25d9..e3f7d939d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,3 +37,46 @@ repos: entry: flake8 files: '^.*\.py$' exclude: '^(doc|releasenotes|tools)/.*$' + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.4.1 + hooks: + - id: mypy + additional_dependencies: + - types-decorator + - types-PyYAML + - types-requests + - types-simplejson + # keep this in-sync with '[mypy] exclude' in 'setup.cfg' + exclude: | + (?x)( + openstack/tests/ansible/.* + | openstack/tests/functional/.* + | openstack/tests/unit/.* + | openstack/tests/fixtures.py + | openstack/accelerator/.* + | openstack/baremetal/.* + | openstack/baremetal_introspection/.* + | openstack/block_storage/.* + | openstack/cloud/.* + | openstack/clustering/.* + | openstack/compute/.* + | openstack/container_infrastructure_management/.* + | openstack/database/.* + | openstack/dns/.* + | openstack/fixture/.* + | openstack/identity/.* + | openstack/image/.* + | openstack/instance_ha/.* + | openstack/key_manager/.* + | openstack/load_balancer/.* + | openstack/message/.* + | openstack/network/.* + | openstack/object_store/.* + | openstack/orchestration/.* + | openstack/placement/.* + | openstack/shared_file_system/.* + | openstack/workflow/.* + | doc/.* + | examples/.* + | releasenotes/.* + ) diff --git a/setup.cfg b/setup.cfg index 2fd433c2e..50074a951 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,3 +28,48 @@ packages = [entry_points] console_scripts = openstack-inventory = openstack.cloud.cmd.inventory:main + +[mypy] +show_column_numbers = true +show_error_context = true +ignore_missing_imports = true +# follow_imports = normal +follow_imports = skip +incremental = true +check_untyped_defs = true +warn_unused_ignores = true +# keep this in-sync with 'mypy.exclude' in '.pre-commit-config.yaml' +# TODO(stephenfin) Eventually we should remove everything here except the +# unit tests module +exclude = (?x)( + openstack/tests/ansible + | openstack/tests/functional + | openstack/tests/unit + | openstack/tests/fixtures.py + | openstack/accelerator + | openstack/baremetal + | openstack/baremetal_introspection + | openstack/block_storage + | openstack/cloud + | openstack/clustering + | openstack/compute + | openstack/container_infrastructure_management + | openstack/database + | openstack/dns + | openstack/fixture + | openstack/identity + | openstack/image + | openstack/instance_ha + | openstack/key_manager + | openstack/load_balancer + | openstack/message + | openstack/network + | openstack/object_store + | openstack/orchestration + | openstack/placement + | openstack/shared_file_system + | openstack/workflow + | doc + | examples + | releasenotes + ) From 16c114dd66043ff45b21f1fd6f6d99a9e9d0d534 Mon Sep 17 00:00:00 2001 From: elajkat Date: Wed, 28 Jun 2023 17:29:05 +0200 Subject: [PATCH 3353/3836] Add SFC to SDK Included resources are: * PortChain * PortPairGroup * PortPair * FlowClassifier * ServiceGraph api-ref (under review): https://review.opendev.org/c/openstack/neutron-lib/+/887193 Related-Bug: #1999774 Change-Id: I254fadb780798fec65e41e8d95719f38cc1e0601 --- doc/source/user/proxies/network.rst | 17 + doc/source/user/resources/network/index.rst | 5 + .../network/v2/sfc_flow_classifier.rst | 13 + .../resources/network/v2/sfc_port_chain.rst | 12 + .../resources/network/v2/sfc_port_pair.rst | 12 + .../network/v2/sfc_port_pair_group.rst | 13 + .../network/v2/sfc_service_graph.rst | 13 + openstack/network/v2/_proxy.py | 527 ++++++++++++++++++ openstack/network/v2/sfc_flow_classifier.py | 88 +++ openstack/network/v2/sfc_port_chain.py | 49 ++ openstack/network/v2/sfc_port_pair.py | 53 ++ openstack/network/v2/sfc_port_pair_group.py | 57 ++ openstack/network/v2/sfc_service_graph.py | 45 ++ .../tests/functional/network/v2/test_sfc.py | 136 +++++ .../network/v2/test_sfc_flow_classifier.py | 102 ++++ .../unit/network/v2/test_sfc_port_chain.py | 62 +++ .../unit/network/v2/test_sfc_port_pair.py | 67 +++ .../network/v2/test_sfc_port_pair_group.py | 63 +++ .../unit/network/v2/test_sfc_service_graph.py | 65 +++ ...rk_add_sfc_resources-8a52c0c8c1f8e932.yaml | 6 + 20 files changed, 1405 insertions(+) create mode 100644 doc/source/user/resources/network/v2/sfc_flow_classifier.rst create mode 100644 doc/source/user/resources/network/v2/sfc_port_chain.rst create mode 100644 doc/source/user/resources/network/v2/sfc_port_pair.rst create mode 100644 doc/source/user/resources/network/v2/sfc_port_pair_group.rst create mode 100644 doc/source/user/resources/network/v2/sfc_service_graph.rst create mode 100644 openstack/network/v2/sfc_flow_classifier.py create mode 100644 openstack/network/v2/sfc_port_chain.py create mode 100644 openstack/network/v2/sfc_port_pair.py create mode 100644 openstack/network/v2/sfc_port_pair_group.py create mode 100644 openstack/network/v2/sfc_service_graph.py create mode 100644 openstack/tests/functional/network/v2/test_sfc.py create mode 100644 openstack/tests/unit/network/v2/test_sfc_flow_classifier.py create mode 100644 openstack/tests/unit/network/v2/test_sfc_port_chain.py create mode 100644 openstack/tests/unit/network/v2/test_sfc_port_pair.py create mode 100644 openstack/tests/unit/network/v2/test_sfc_port_pair_group.py create mode 100644 openstack/tests/unit/network/v2/test_sfc_service_graph.py create mode 100644 releasenotes/notes/network_add_sfc_resources-8a52c0c8c1f8e932.yaml diff --git a/doc/source/user/proxies/network.rst b/doc/source/user/proxies/network.rst index 4ce20b92b..a292eef84 100644 --- a/doc/source/user/proxies/network.rst +++ b/doc/source/user/proxies/network.rst @@ -329,3 +329,20 @@ BGPVPN operations bgpvpn_port_associations, create_bgpvpn_router_association, delete_bgpvpn_router_association, get_bgpvpn_router_association, update_bgpvpn_router_association, bgpvpn_router_associations + +SFC operations +^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + :noindex: + :members: create_sfc_flow_classifier, delete_sfc_flow_classifier, + find_sfc_flow_classifier, get_sfc_flow_classifier, + update_sfc_flow_classifier, create_sfc_port_chain, + delete_sfc_port_chain, find_sfc_port_chain, get_sfc_port_chain, + update_sfc_port_chain, create_sfc_port_pair, delete_sfc_port_pair, + find_sfc_port_pair, get_sfc_port_pair, update_sfc_port_pair, + create_sfc_port_pair_group, delete_sfc_port_pair_group, + find_sfc_port_pair_group, get_sfc_port_pair_group, + update_sfc_port_pair_group, create_sfc_service_graph, + delete_sfc_service_graph, find_sfc_service_graph, + get_sfc_service_graph, update_sfc_service_graph diff --git a/doc/source/user/resources/network/index.rst b/doc/source/user/resources/network/index.rst index 167c47746..24215f2fd 100644 --- a/doc/source/user/resources/network/index.rst +++ b/doc/source/user/resources/network/index.rst @@ -46,6 +46,11 @@ Network Resources v2/segment v2/service_profile v2/service_provider + v2/sfc_flow_classifier + v2/sfc_port_chain + v2/sfc_port_pair_group + v2/sfc_port_pair + v2/sfc_service_graph v2/subnet v2/subnet_pool v2/tap_flow diff --git a/doc/source/user/resources/network/v2/sfc_flow_classifier.rst b/doc/source/user/resources/network/v2/sfc_flow_classifier.rst new file mode 100644 index 000000000..da94f2756 --- /dev/null +++ b/doc/source/user/resources/network/v2/sfc_flow_classifier.rst @@ -0,0 +1,13 @@ +openstack.network.v2.sfc_flow_classifier +======================================== + +.. automodule:: openstack.network.v2.sfc_flow_classifier + +The SfcFlowClassifier Class +--------------------------- + +The ``SfcFlowClassifier`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.sfc_flow_classifier.SfcFlowClassifier + :members: diff --git a/doc/source/user/resources/network/v2/sfc_port_chain.rst b/doc/source/user/resources/network/v2/sfc_port_chain.rst new file mode 100644 index 000000000..3842f5709 --- /dev/null +++ b/doc/source/user/resources/network/v2/sfc_port_chain.rst @@ -0,0 +1,12 @@ +openstack.network.v2.sfc_port_chain +=================================== + +.. automodule:: openstack.network.v2.sfc_port_chain + +The SfcPortChain Class +---------------------- + +The ``SfcPortChain`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.sfc_port_chain.SfcPortChain + :members: diff --git a/doc/source/user/resources/network/v2/sfc_port_pair.rst b/doc/source/user/resources/network/v2/sfc_port_pair.rst new file mode 100644 index 000000000..84e65000c --- /dev/null +++ b/doc/source/user/resources/network/v2/sfc_port_pair.rst @@ -0,0 +1,12 @@ +openstack.network.v2.sfc_port_pair +================================== + +.. automodule:: openstack.network.v2.sfc_port_pair + +The SfcPortPair Class +--------------------- + +The ``SfcPortPair`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.sfc_port_pair.SfcPortPair + :members: diff --git a/doc/source/user/resources/network/v2/sfc_port_pair_group.rst b/doc/source/user/resources/network/v2/sfc_port_pair_group.rst new file mode 100644 index 000000000..3333062bc --- /dev/null +++ b/doc/source/user/resources/network/v2/sfc_port_pair_group.rst @@ -0,0 +1,13 @@ +openstack.network.v2.sfc_port_pair_group +======================================== + +.. automodule:: openstack.network.v2.sfc_port_pair_group + +The SfcPortPairGroup Class +-------------------------- + +The ``SfcPortPairGroup`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.sfc_port_pair_group.SfcPortPairGroup + :members: diff --git a/doc/source/user/resources/network/v2/sfc_service_graph.rst b/doc/source/user/resources/network/v2/sfc_service_graph.rst new file mode 100644 index 000000000..718b7201e --- /dev/null +++ b/doc/source/user/resources/network/v2/sfc_service_graph.rst @@ -0,0 +1,13 @@ +openstack.network.v2.sfc_service_graph +====================================== + +.. automodule:: openstack.network.v2.sfc_service_graph + +The SfcServiceGraph Class +------------------------- + +The ``SfcServiceGraph`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.sfc_service_graph.SfcServiceGraph + :members: diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index d891cf7d8..924afb738 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -80,6 +80,11 @@ from openstack.network.v2 import segment as _segment from openstack.network.v2 import service_profile as _service_profile from openstack.network.v2 import service_provider as _service_provider +from openstack.network.v2 import sfc_flow_classifier as _sfc_flow_classifier +from openstack.network.v2 import sfc_port_chain as _sfc_port_chain +from openstack.network.v2 import sfc_port_pair as _sfc_port_pair +from openstack.network.v2 import sfc_port_pair_group as _sfc_port_pair_group +from openstack.network.v2 import sfc_service_graph as _sfc_sservice_graph from openstack.network.v2 import subnet as _subnet from openstack.network.v2 import subnet_pool as _subnet_pool from openstack.network.v2 import tap_flow as _tap_flow @@ -162,6 +167,11 @@ class Proxy(proxy.Proxy, Generic[T]): "segment": _segment.Segment, "service_profile": _service_profile.ServiceProfile, "service_provider": _service_provider.ServiceProvider, + "sfc_flow_classifier": _sfc_flow_classifier.SfcFlowClassifier, + "sfc_port_chain": _sfc_port_chain.SfcPortChain, + "sfc_port_pair": _sfc_port_pair.SfcPortPair, + "sfc_port_pair_group": _sfc_port_pair_group.SfcPortPairGroup, + "sfc_service_graph": _sfc_sservice_graph.SfcServiceGraph, "subnet": _subnet.Subnet, "subnet_pool": _subnet_pool.SubnetPool, "tap_flow": _tap_flow.TapFlow, @@ -6202,6 +6212,523 @@ def tap_services(self, **query): """Return a generator of Tap Services""" return self._list(_tap_service.TapService, **query) + def create_sfc_flow_classifier(self, **attrs): + """Create a new Flow Classifier from attributes + + :param attrs: Keyword arguments which will be used to create a + :class:`~openstack.network.v2.sfc_flow_classifier.SfcFlowClassifier`, + comprised of the properties on the SfcFlowClassifier class. + + :returns: The results of SFC Flow Classifier creation + :rtype: + :class:`~openstack.network.v2.sfc_flow_classifier.SfcFlowClassifier` + """ + + return self._create(_sfc_flow_classifier.SfcFlowClassifier, **attrs) + + def delete_sfc_flow_classifier(self, flow_classifier, ignore_missing=True): + """Delete a Flow Classifier + + :param flow_classifier: + The value can be either the ID of a flow classifier or a + :class:`~openstack.network.v2.sfc_flow_classifier.SfcFlowClassifier` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the flow classifier does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent flow classifier. + + :returns: ``None`` + """ + self._delete( + _sfc_flow_classifier.SfcFlowClassifier, + flow_classifier, + ignore_missing=ignore_missing, + ) + + def find_sfc_flow_classifier( + self, name_or_id, ignore_missing=True, **query + ): + """Find a single Flow Classifier + + :param str name_or_id: The name or ID of an SFC flow classifier. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict query: Any additional parameters to be passed into + underlying methods. such as query filters. + :returns: One + :class:`~openstack.network.v2.sfc_flow_classifier. + SfcFlowClassifier` or None + """ + return self._find( + _sfc_flow_classifier.SfcFlowClassifier, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) + + def get_sfc_flow_classifier(self, flow_classifier): + """Get a single Flow Classifier + + :param flow_classifier: + The value can be the ID of an SFC flow classifier or a + :class:`~openstack.network.v2.sfc_flow_classifier.SfcFlowClassifier` instance. + + :returns: + :class:`~openstack.network.v2.sfc_flow_classifier.SfcFlowClassifier` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get( + _sfc_flow_classifier.SfcFlowClassifier, flow_classifier + ) + + def update_sfc_flow_classifier(self, flow_classifier, **attrs): + """Update a Flow Classifier + + :param flow_classifier: The value can be the ID of a Flow Classifier + :class:`~openstack.network.v2.sfc_flow_classifier.SfcFlowClassifier`, + instance. + :param attrs: The attributes to update on the Flow Classifier + + :returns: The updated Flow Classifier. + :rtype: + :class:`~openstack.network.v2.sfc_flow_classifier.SfcFlowClassifier` + """ + return self._update( + _sfc_flow_classifier.SfcFlowClassifier, flow_classifier, **attrs + ) + + def sfc_flow_classifiers(self, **query): + """Return a generator of Flow Classifiers + + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. Available parameters include: + + * ``name``: The name of the flow classifier. + * ``description``: The flow classifier description + * ``ethertype``: Must be IPv4 or IPv6. + * ``protocol``: Flow classifier protocol + + :returns: A generator of SFC Flow classifier objects + :rtype: + :class:`~openstack.network.v2.sfc_flow_classifier. + SfcFlowClassifier` + """ + return self._list(_sfc_flow_classifier.SfcFlowClassifier, **query) + + def create_sfc_port_chain(self, **attrs): + """Create a new Port Chain from attributes + + :param attrs: Keyword arguments which will be used to create a + :class:`~openstack.network.v2.sfc_port_chain.SfcPortChain`, + comprised of the properties on the SfcPortchain class. + + :returns: The results of SFC Port Chain creation + :rtype: + :class:`~openstack.network.v2.sfc_port_chain.SfcPortChain` + """ + return self._create(_sfc_port_chain.SfcPortChain, **attrs) + + def delete_sfc_port_chain(self, port_chain, ignore_missing=True): + """Delete a Port Chain + + :param port_chain: + The value can be either the ID of a port chain or a + :class:`~openstack.network.v2.sfc_port_chain.SfcPortChain` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the port chain does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent port chain. + + :returns: ``None`` + """ + self._delete( + _sfc_port_chain.SfcPortChain, + port_chain, + ignore_missing=ignore_missing, + ) + + def find_sfc_port_chain(self, name_or_id, ignore_missing=True, **query): + """Find a single Port Chain + + :param str name_or_id: The name or ID of an SFC port chain. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict query: Any additional parameters to be passed into + underlying methods. such as query filters. + :returns: One + :class:`~openstack.network.v2.sfc_port_chain. + SfcPortChain` or None + """ + return self._find( + _sfc_port_chain.SfcPortChain, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) + + def get_sfc_port_chain(self, port_chain): + """Get a signle Port Chain + + :param port_chain: + The value can be the ID of an SFC port chain or a + :class:`~openstack.network.v2.sfc_port_chain.SfcPortChain` + instance. + + :returns: + :class:`~openstack.network.v2.sfc_port_chain.SfcPortchain` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_sfc_port_chain.SfcPortChain, port_chain) + + def update_sfc_port_chain(self, port_chain, **attrs): + """Update a Port Chain + + :param flow_classifier: The value can be the ID of a Flow Classifier + :class:`~openstack.network.v2.sfc_flow_classifier.SfcFlowClassifier`, + instance. + :param attrs: The attributes to update on the Flow Classifier + + :returns: The updated Flow Classifier. + :rtype: + :class:`~openstack.network.v2.sfc_flow_classifier.SfcFlowClassifier` + """ + return self._update(_sfc_port_chain.SfcPortChain, port_chain, **attrs) + + def sfc_port_chains(self, **query): + """Return a generator of Port Chains + + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. Available parameters include: + + * ``name``: The name of the port chain + * ``description``: The port chain description + + :returns: A generator of SFC port chain objects + :rtype: + :class:`~openstack.network.v2.sfc_port_chain.SfcPortChain` + """ + return self._list(_sfc_port_chain.SfcPortChain, **query) + + def create_sfc_port_pair(self, **attrs): + """Create a new Port Pair from attributes + + :param attrs: Keyword arguments which will be used to create a + :class:`~openstack.network.v2.sfc_port_pair.SfcPortPair`, + comprised of the properties on the SfcPortPair class. + + :returns: The results of SFC Port Pair creation + :rtype: + :class:`~openstack.network.v2.sfc_port_pair.SfPortPair` + """ + return self._create(_sfc_port_pair.SfcPortPair, **attrs) + + def delete_sfc_port_pair(self, port_pair, ignore_missing=True): + """Delete a Port Pair + + :param port_pair: + The value can be either the ID of a port pair or a + :class:`~openstack.network.v2.sfc_port_pair.SfcPortPair` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the port pair does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent port pair. + + :returns: ``None`` + """ + self._delete( + _sfc_port_pair.SfcPortPair, + port_pair, + ignore_missing=ignore_missing, + ) + + def find_sfc_port_pair(self, name_or_id, ignore_missing=True, **query): + """Find a single Port Pair + + :param str name_or_id: The name or ID of an SFC port pair. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict query: Any additional parameters to be passed into + underlying methods. such as query filters. + :returns: One + :class:`~openstack.network.v2.sfc_port_pair.SfcPortPair` or None + """ + return self._find( + _sfc_port_pair.SfcPortPair, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) + + def get_sfc_port_pair(self, port_pair): + """Get a signle Port Pair + + :param port_pair: + The value can be the ID of an SFC port pair or a + :class:`~openstack.network.v2.sfc_port_pair.SfcPortPair` + instance. + + :returns: + :class:`~openstack.network.v2.sfc_port_pair.SfcPortPair` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_sfc_port_pair.SfcPortPair, port_pair) + + def update_sfc_port_pair(self, port_pair, **attrs): + """Update a Port Pair + + :param port_pair: The value can be the ID of a Port Pair + :class:`~openstack.network.v2.sfc_port_pair.SfcPortPair`, + instance. + :param attrs: The attributes to update on the Port Pair + + :returns: The updated Port Pair. + :rtype: + :class:`~openstack.network.v2.sfc_port_pair.SfcPortPair` + """ + return self._update(_sfc_port_pair.SfcPortPair, port_pair, **attrs) + + def sfc_port_pairs(self, **query): + """Return a generator of Port Pairs + + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. Available parameters include: + + * ``name``: The name of the port pair. + * ``description``: The port pair description. + + :returns: A generator of SFC port pair objects + :rtype: + :class:`~openstack.network.v2.sfc_port_pair.SfcPortPair` + """ + return self._list(_sfc_port_pair.SfcPortPair, **query) + + def create_sfc_port_pair_group(self, **attrs): + """Create a new Port Pair Group from attributes + + :param attrs: Keyword arguments which will be used to create a + :class:`~openstack.network.v2.sfc_port_pair_group.SfcPortPairGroup`, + comprised of the properties on the SfcPortPairGroup class. + + :returns: The results of SFC Port Pair Group creation + :rtype: + :class:`~openstack.network.v2.sfc_port_pair_group.SfcPortPairGroup` + """ + return self._create(_sfc_port_pair_group.SfcPortPairGroup, **attrs) + + def delete_sfc_port_pair_group(self, port_pair_group, ignore_missing=True): + """Delete a Port Pair Group + + :param port_pair_group: + The value can be either the ID of a port pair group or a + :class:`~openstack.network.v2.sfc_port_pair_group. + SfcPortPairGroup` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the port pair group does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent port pair group. + + :returns: ``None`` + """ + self._delete( + _sfc_port_pair_group.SfcPortPairGroup, + port_pair_group, + ignore_missing=ignore_missing, + ) + + def find_sfc_port_pair_group( + self, name_or_id, ignore_missing=True, **query + ): + """Find a single Port Pair Group + + :param str name_or_id: The name or ID of an SFC port pair group. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict query: Any additional parameters to be passed into + underlying methods. such as query filters. + :returns: One + :class:`~openstack.network.v2.sfc_port_pair_group. + SfcPortPairGroup` or None + """ + return self._find( + _sfc_port_pair_group.SfcPortPairGroup, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) + + def get_sfc_port_pair_group(self, port_pair_group): + """Get a signle Port Pair Group + + :param port_pair_group: + The value can be the ID of an SFC port pair group or a + :class:`~openstack.network.v2.sfc_port_pair_group.SfcPortPairGroup` + instance. + + :returns: + :class:`~openstack.network.v2.sfc_port_pair_group.SfcPortPairGroup` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get( + _sfc_port_pair_group.SfcPortPairGroup, port_pair_group + ) + + def update_sfc_port_pair_group(self, port_pair_group, **attrs): + """Update a Port Pair Group + + :param port_pair_group: The value can be the ID of a Port Pair Group + :class:`~openstack.network.v2.sfc_port_pair.SfcPortPairGroup`, + instance. + :param attrs: The attributes to update on the Port Pair Group + + :returns: The updated Port Pair Group. + :rtype: + :class:`~openstack.network.v2.sfc_port_pair_group.SfcPortPairGroup` + """ + return self._update( + _sfc_port_pair_group.SfcPortPairGroup, port_pair_group, **attrs + ) + + def sfc_port_pair_groups(self, **query): + """Return a generator of Port Pair Groups + + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. Available parameters include: + + * ``name``: The name of the port pair. + * ``description``: The port pair description. + + :returns: A generator of SFC port pair group objects + :rtype: + :class:`~openstack.network.v2.sfc_port_pair_group. + SfcPortPairGroup` + """ + return self._list(_sfc_port_pair_group.SfcPortPairGroup, **query) + + def create_sfc_service_graph(self, **attrs): + """Create a new Service Graph from attributes + + :param attrs: Keyword arguments which will be used to create a + :class:`~openstack.network.v2.sfc_service_graph.SfcServiceGraph`, + comprised of the properties on the SfcServiceGraph class. + + :returns: The results of SFC Service Graph creation + :rtype: + :class:`~openstack.network.v2.sfc_service_graph.SfcServiceGraph` + """ + return self._create(_sfc_sservice_graph.SfcServiceGraph, **attrs) + + def delete_sfc_service_graph(self, service_graph, ignore_missing=True): + """Delete a Service Graph + + :param service_graph: + The value can be either the ID of a service graph or a + :class:`~openstack.network.v2.sfc_service_graph.SfcServiceGraph` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the service graph does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent service graph. + + :returns: ``None`` + """ + self._delete( + _sfc_sservice_graph.SfcServiceGraph, + service_graph, + ignore_missing=ignore_missing, + ) + + def find_sfc_service_graph(self, name_or_id, ignore_missing=True, **query): + """Find a single Service Graph + + :param str name_or_id: The name or ID of an SFC service graph. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict query: Any additional parameters to be passed into + underlying methods. such as query filters. + :returns: One + :class:`~openstack.network.v2.sfc_service_graph. + SfcServiceGraph` or None + """ + return self._find( + _sfc_sservice_graph.SfcServiceGraph, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) + + def get_sfc_service_graph(self, service_graph): + """Get a signle Service Graph + + :param service_graph: + The value can be the ID of an SFC service graph or a + :class:`~openstack.network.v2.sfc_service_graph.SfcServiceGraph` + instance. + + :returns: + :class:`~openstack.network.v2.sfc_service_graph.SfcServiceGraph` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_sfc_sservice_graph.SfcServiceGraph, service_graph) + + def update_sfc_service_graph(self, service_graph, **attrs): + """Update a Service Graph + + :param service_graph: The value can be the ID of a Service Graph + :class:`~openstack.network.v2.sfc_service_graph.SfcServiceGraph`, + instance. + :param attrs: The attributes to update on the Service Graph + + :returns: The updated Service Graph. + :rtype: + :class:`~openstack.network.v2.sfc_service_graph.SfcServiceGraph` + """ + return self._update( + _sfc_sservice_graph.SfcServiceGraph, service_graph, **attrs + ) + + def sfc_service_graphs(self, **query): + """Return a generator of Service Graphs + + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. Available parameters include: + + * ``name``: The name of the port pair. + * ``description``: The port pair description. + + :returns: A generator of SFC service graph objects + :rtype: + :class:`~openstack.network.v2.sfc_service_graph.SfcServiceGraph` + """ + return self._list(_sfc_sservice_graph.SfcServiceGraph, **query) + def _get_cleanup_dependencies(self): return {'network': {'before': ['identity']}} diff --git a/openstack/network/v2/sfc_flow_classifier.py b/openstack/network/v2/sfc_flow_classifier.py new file mode 100644 index 000000000..527a37220 --- /dev/null +++ b/openstack/network/v2/sfc_flow_classifier.py @@ -0,0 +1,88 @@ +# 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 resource + + +class SfcFlowClassifier(resource.Resource): + resource_key = 'flow_classifier' + resources_key = 'flow_classifiers' + base_path = '/sfc/flow_classifiers' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + _query_mapping = resource.QueryParameters( + 'description', + 'name', + 'project_id', + 'tenant_id', + 'ethertype', + 'protocol', + 'source_port_range_min', + 'source_port_range_max', + 'destination_port_range_min', + 'destination_port_range_max', + 'logical_source_port', + 'logical_destination_port', + ) + + # Properties + #: Human-readable description for the resource. + description = resource.Body('description') + #: Human-readable name of the resource. Default is an empty string. + name = resource.Body('name') + #: Must be IPv4 or IPv6, and addresses represented in CIDR must match + # the ingress or egress rules. + ethertype = resource.Body('ethertype') + #: The IP protocol can be represented by a string, an integer, or null. + #: Valid values: any (0), ah (51), dccp (33), egp (8), esp (50), gre (47), + #: icmp (1), icmpv6 (58), igmp (2), ipip (4), ipv6-encap (41), + #: ipv6-frag (44), ipv6-icmp (58), ipv6-nonxt (59), ipv6-opts (60), + #: ipv6-route (43), ospf (89), pgm (113), rsvp (46), sctp (132), tcp (6), + #: udp (17), udplite (136), vrrp (112). + protocol = resource.Body('protocol') + #: Minimum source protocol port. + source_port_range_min = resource.Body('source_port_range_min', type=int) + #: Maximum source protocol port. + source_port_range_max = resource.Body('source_port_range_max', type=int) + #: Minimum destination protocol port. + destination_port_range_min = resource.Body( + 'destination_port_range_min', type=int + ) + #: Maximum destination protocol port. + destination_port_range_max = resource.Body( + 'destination_port_range_max', type=int + ) + #: The source IP prefix. + source_ip_prefix = resource.Body('source_ip_prefix') + #: The destination IP prefix. + destination_ip_prefix = resource.Body('destination_ip_prefix') + #: The UUID of the source logical port. + logical_source_port = resource.Body('logical_source_port') + #: The UUID of the destination logical port. + logical_destination_port = resource.Body('logical_destination_port') + #: A dictionary of L7 parameters, in the form of + #: logical_source_network: uuid, logical_destination_network: uuid. + l7_parameters = resource.Body('l7_parameters', type=dict) + #: Summary field of a Flow Classifier, composed of the + #: protocol, source protcol port, destination ptocolo port, + #: logical_source_port, logical_destination_port and + #: l7_parameters + summary = resource.Computed('summary', default='') + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) diff --git a/openstack/network/v2/sfc_port_chain.py b/openstack/network/v2/sfc_port_chain.py new file mode 100644 index 000000000..bf89148d2 --- /dev/null +++ b/openstack/network/v2/sfc_port_chain.py @@ -0,0 +1,49 @@ +# 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 resource + + +class SfcPortChain(resource.Resource): + resource_key = 'port_chain' + resources_key = 'port_chains' + base_path = '/sfc/port_chains' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + _query_mapping = resource.QueryParameters( + 'description', + 'name', + 'project_id', + 'tenant_id', + ) + + # Properties + #: Human-readable description for the resource. + description = resource.Body('description') + #: Human-readable name of the resource. Default is an empty string. + name = resource.Body('name') + #: List of port-pair-group UUIDs. + port_pair_groups = resource.Body('port_pair_groups', type=list) + #: List of flow-classifier UUIDs. + flow_classifiers = resource.Body('flow_classifiers', type=list) + #: A dictionary of chain parameters, correlation values can be + #: mpls and nsh, symmetric can be True or False. + chain_parameters = resource.Body('chain_parameters', type=dict) + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) diff --git a/openstack/network/v2/sfc_port_pair.py b/openstack/network/v2/sfc_port_pair.py new file mode 100644 index 000000000..bbe4accee --- /dev/null +++ b/openstack/network/v2/sfc_port_pair.py @@ -0,0 +1,53 @@ +# 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 resource + + +class SfcPortPair(resource.Resource): + resource_key = 'port_pair' + resources_key = 'port_pairs' + base_path = '/sfc/port_pairs' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + _query_mapping = resource.QueryParameters( + 'description', + 'name', + 'egress', + 'ingress', + 'project_id', + 'tenant_id', + ) + + # Properties + #: Human-readable description for the resource. + description = resource.Body('description') + #: Human-readable name of the resource. Default is an empty string. + name = resource.Body('name') + #: The UUID of the ingress Neutron port. + ingress = resource.Body('ingress') + #: The UUID of the egress Neutron port. + egress = resource.Body('egress') + #: A dictionary of service function parameters, correlation values can be + #: mpls and nsh, weight which can be an int. + service_function_parameters = resource.Body( + 'service_function_parameters', type=dict + ) + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) diff --git a/openstack/network/v2/sfc_port_pair_group.py b/openstack/network/v2/sfc_port_pair_group.py new file mode 100644 index 000000000..f53c2afbc --- /dev/null +++ b/openstack/network/v2/sfc_port_pair_group.py @@ -0,0 +1,57 @@ +# 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 resource + + +class SfcPortPairGroup(resource.Resource): + resource_key = 'port_pair_group' + resources_key = 'port_pair_groups' + base_path = '/sfc/port_pair_groups' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + _query_mapping = resource.QueryParameters( + 'description', + 'name', + 'project_id', + 'tenant_id', + ) + + # Properties + #: Human-readable description for the resource. + description = resource.Body('description') + #: Human-readable name of the resource. Default is an empty string. + name = resource.Body('name') + #: List of port-pair UUIDs. + port_pairs = resource.Body('port_pairs', type=list) + #: Dictionary of port pair group parameters, in the form of + #: lb_fields: list of regex (eth|ip|tcp|udp)_(src|dst)), + #: ppg_n_tuple_mapping: ingress_n_tuple or egress_n_tuple. + #: The ingress or egress tuple is a dict with the following keys: + #: source_ip_prefix, destination_ip_prefix, source_port_range_min, + #: source_port_range_max, destination_port_range_min, + #: destination_port_range_max. + port_pair_group_parameters = resource.Body( + 'port_pair_group_parameters', type=dict + ) + #: True if passive Tap service functions support is enabled, + #: default is False. + is_tap_enabled = resource.Body('tap_enabled', type=bool) + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) diff --git a/openstack/network/v2/sfc_service_graph.py b/openstack/network/v2/sfc_service_graph.py new file mode 100644 index 000000000..fe76db5bb --- /dev/null +++ b/openstack/network/v2/sfc_service_graph.py @@ -0,0 +1,45 @@ +# 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 resource + + +class SfcServiceGraph(resource.Resource): + resource_key = 'service_graph' + resources_key = 'service_graphs' + base_path = '/sfc/service_graphs' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + _query_mapping = resource.QueryParameters( + 'description', + 'name', + 'project_id', + 'tenant_id', + ) + + # Properties + #: Human-readable description for the resource. + description = resource.Body('description') + #: Human-readable name of the resource. Default is an empty string. + name = resource.Body('name') + #: A dictionary where the key is the source port chain and the + #: value is a list of destination port chains. + port_chains = resource.Body('port_chains') + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) diff --git a/openstack/tests/functional/network/v2/test_sfc.py b/openstack/tests/functional/network/v2/test_sfc.py new file mode 100644 index 000000000..c52ecc57f --- /dev/null +++ b/openstack/tests/functional/network/v2/test_sfc.py @@ -0,0 +1,136 @@ +# 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.network.v2 import network as _network +from openstack.network.v2 import port as _port +from openstack.network.v2 import sfc_flow_classifier as _flow_classifier +from openstack.network.v2 import subnet as _subnet +from openstack.tests.functional import base + + +class TestSFCFlowClassifier(base.BaseFunctionalTest): + FC_ID = None + + def setUp(self): + super().setUp() + + if not self.user_cloud.network.find_extension("sfc"): + self.skipTest("Neutron SFC Extension disabled") + + self.FLOW_CLASSIFIER_NAME = 'my_classifier' + self.getUniqueString() + self.UPDATE_NAME = 'updated' + self.getUniqueString() + self.NET_NAME = 'network1' + self.getUniqueString() + self.SUBNET_NAME = 'subnet1' + self.getUniqueString() + self.PORT1_NAME = 'port1' + self.getUniqueString() + self.PORT2_NAME = 'port2' + self.getUniqueString() + self.ETHERTYPE = 'IPv4' + self.PROTOCOL = 'tcp' + self.S_PORT_RANGE_MIN = 80 + self.S_PORT_RANGE_MAX = 80 + self.D_PORT_RANGE_MIN = 180 + self.D_PORT_RANGE_MAX = 180 + self.CIDR = "10.101.0.0/24" + self.SOURCE_IP = '10.101.1.12/32' + self.DESTINATION_IP = '10.102.2.12/32' + + self.PORT_CHAIN_NAME = 'my_chain' + self.getUniqueString() + self.PORT_PAIR_NAME = 'my_port_pair' + self.getUniqueString() + self.PORT_PAIR_GROUP_NAME = ( + 'my_port_pair_group' + self.getUniqueString() + ) + self.SERVICE_GRAPH_NAME = 'my_service_graph' + self.getUniqueString() + self.op_net_client = self.operator_cloud.network + + net = self.op_net_client.create_network(name=self.NET_NAME) + self.assertIsInstance(net, _network.Network) + self.NETWORK = net + subnet = self.operator_cloud.network.create_subnet( + name=self.SUBNET_NAME, + ip_version=4, + network_id=self.NETWORK.id, + cidr=self.CIDR, + ) + self.assertIsInstance(subnet, _subnet.Subnet) + self.SUBNET = subnet + + self.PORT1 = self._create_port( + network=self.NETWORK, port_name=self.PORT1_NAME + ) + self.PORT2 = self._create_port( + network=self.NETWORK, port_name=self.PORT2_NAME + ) + + flow_cls = self.op_net_client.create_sfc_flow_classifier( + name=self.FLOW_CLASSIFIER_NAME, + ethertype=self.ETHERTYPE, + protocol=self.PROTOCOL, + source_port_range_min=self.S_PORT_RANGE_MIN, + source_port_range_max=self.S_PORT_RANGE_MAX, + destination_port_range_min=self.D_PORT_RANGE_MIN, + destination_port_range_max=self.D_PORT_RANGE_MAX, + source_ip_prefix=self.SOURCE_IP, + destination_ip_prefix=self.DESTINATION_IP, + logical_source_port=self.PORT1.id, + logical_destination_port=self.PORT2.id, + ) + self.assertIsInstance(flow_cls, _flow_classifier.SfcFlowClassifier) + self.FLOW_CLASSIFIER = flow_cls + self.FC_ID = flow_cls.id + + def _create_port(self, network, port_name): + port = self.op_net_client.create_port( + name=port_name, + network_id=network.id, + ) + self.assertIsInstance(port, _port.Port) + return port + + def tearDown(self): + sot = self.operator_cloud.network.delete_sfc_flow_classifier( + self.FLOW_CLASSIFIER.id, ignore_missing=True + ) + self.assertIsNone(sot) + sot = self.operator_cloud.network.delete_port(self.PORT1.id) + self.assertIsNone(sot) + sot = self.operator_cloud.network.delete_port(self.PORT2.id) + self.assertIsNone(sot) + + sot = self.operator_cloud.network.delete_subnet(self.SUBNET.id) + self.assertIsNone(sot) + sot = self.operator_cloud.network.delete_network(self.NETWORK.id) + self.assertIsNone(sot) + super().tearDown() + + def test_sfc_flow_classifier(self): + sot = self.operator_cloud.network.find_sfc_flow_classifier( + self.FLOW_CLASSIFIER.name + ) + self.assertEqual(self.ETHERTYPE, sot.ethertype) + self.assertEqual(self.SOURCE_IP, sot.source_ip_prefix) + self.assertEqual(self.PROTOCOL, sot.protocol) + + classifiers = [ + fc.name + for fc in self.operator_cloud.network.sfc_flow_classifiers() + ] + self.assertIn(self.FLOW_CLASSIFIER_NAME, classifiers) + + classifier = self.operator_cloud.network.get_sfc_flow_classifier( + self.FC_ID + ) + self.assertEqual(self.FLOW_CLASSIFIER_NAME, classifier.name) + self.assertEqual(self.FC_ID, classifier.id) + + classifier = self.operator_cloud.network.update_sfc_flow_classifier( + self.FC_ID, name=self.UPDATE_NAME + ) + self.assertEqual(self.UPDATE_NAME, classifier.name) diff --git a/openstack/tests/unit/network/v2/test_sfc_flow_classifier.py b/openstack/tests/unit/network/v2/test_sfc_flow_classifier.py new file mode 100644 index 000000000..31789b5ba --- /dev/null +++ b/openstack/tests/unit/network/v2/test_sfc_flow_classifier.py @@ -0,0 +1,102 @@ +# 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.network.v2 import sfc_flow_classifier +from openstack.tests.unit import base + + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + "description": "", + "project_id": "4ad57e7ce0b24fca8f12b9834d91079d", + "tenant_id": "4ad57e7ce0b24fca8f12b9834d91079d", + "ethertype": "IPv4", + "protocol": 6, + "source_port_range_min": 22, + "source_port_range_max": 2000, + "destination_port_range_min": 80, + "destination_port_range_max": 80, + "source_ip_prefix": None, + "destination_ip_prefix": "22.12.34.45", + "logical_source_port": "uuid1", + "logical_destination_port": "uuid2", + "l7_parameters": None, + "id": "6ecd9cf3-ca64-46c7-863f-f2eb1b9e838a", + "name": "flow_classifier", +} + + +class TestFlowClassifier(base.TestCase): + def test_basic(self): + sot = sfc_flow_classifier.SfcFlowClassifier() + self.assertEqual('flow_classifier', sot.resource_key) + self.assertEqual('flow_classifiers', sot.resources_key) + self.assertEqual('/sfc/flow_classifiers', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = sfc_flow_classifier.SfcFlowClassifier(**EXAMPLE) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['protocol'], sot.protocol) + self.assertEqual(EXAMPLE['ethertype'], sot.ethertype) + self.assertEqual( + EXAMPLE['source_port_range_min'], sot.source_port_range_min + ) + self.assertEqual( + EXAMPLE['source_port_range_max'], sot.source_port_range_max + ) + self.assertEqual( + EXAMPLE['destination_port_range_min'], + sot.destination_port_range_min, + ) + self.assertEqual( + EXAMPLE['destination_port_range_max'], + sot.destination_port_range_max, + ) + self.assertEqual(EXAMPLE['source_ip_prefix'], sot.source_ip_prefix) + self.assertEqual( + EXAMPLE['destination_ip_prefix'], sot.destination_ip_prefix + ) + self.assertEqual( + EXAMPLE['logical_source_port'], sot.logical_source_port + ) + self.assertEqual( + EXAMPLE['logical_destination_port'], sot.logical_destination_port + ) + self.assertEqual(EXAMPLE['l7_parameters'], sot.l7_parameters) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + + self.assertDictEqual( + { + "limit": "limit", + "marker": "marker", + 'description': 'description', + 'name': 'name', + 'project_id': 'project_id', + 'tenant_id': 'tenant_id', + 'ethertype': 'ethertype', + 'protocol': 'protocol', + 'source_port_range_min': 'source_port_range_min', + 'source_port_range_max': 'source_port_range_max', + 'destination_port_range_min': 'destination_port_range_min', + 'destination_port_range_max': 'destination_port_range_max', + 'logical_source_port': 'logical_source_port', + 'logical_destination_port': 'logical_destination_port', + }, + sot._query_mapping._mapping, + ) diff --git a/openstack/tests/unit/network/v2/test_sfc_port_chain.py b/openstack/tests/unit/network/v2/test_sfc_port_chain.py new file mode 100644 index 000000000..fd3ec865a --- /dev/null +++ b/openstack/tests/unit/network/v2/test_sfc_port_chain.py @@ -0,0 +1,62 @@ +# 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.network.v2 import sfc_port_chain +from openstack.tests.unit import base + + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + "description": "", + "project_id": "4ad57e7ce0b24fca8f12b9834d91079d", + "tenant_id": "4ad57e7ce0b24fca8f12b9834d91079d", + "port_pair_groups": ["p_group1", "p_group2"], + "flow_classifiers": ["f_classifier1", "f_classifier_2"], + "chain_parameters": {"correlation": "mpls", "symmetric": True}, + "id": "6ecd9cf3-ca64-46c7-863f-f2eb1b9e838a", + "name": "peers", +} + + +class TestPortChain(base.TestCase): + def test_basic(self): + sot = sfc_port_chain.SfcPortChain() + self.assertEqual('port_chain', sot.resource_key) + self.assertEqual('port_chains', sot.resources_key) + self.assertEqual('/sfc/port_chains', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = sfc_port_chain.SfcPortChain(**EXAMPLE) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['port_pair_groups'], sot.port_pair_groups) + self.assertEqual(EXAMPLE['flow_classifiers'], sot.flow_classifiers) + self.assertEqual(EXAMPLE['chain_parameters'], sot.chain_parameters) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + + self.assertDictEqual( + { + "limit": "limit", + "marker": "marker", + 'description': 'description', + 'name': 'name', + 'project_id': 'project_id', + 'tenant_id': 'tenant_id', + }, + sot._query_mapping._mapping, + ) diff --git a/openstack/tests/unit/network/v2/test_sfc_port_pair.py b/openstack/tests/unit/network/v2/test_sfc_port_pair.py new file mode 100644 index 000000000..ea8257f64 --- /dev/null +++ b/openstack/tests/unit/network/v2/test_sfc_port_pair.py @@ -0,0 +1,67 @@ +# 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.network.v2 import sfc_port_pair +from openstack.tests.unit import base + + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + "description": "", + "project_id": "4ad57e7ce0b24fca8f12b9834d91079d", + "tenant_id": "4ad57e7ce0b24fca8f12b9834d91079d", + "egress": "d294f042-1736-11ee-821a-7f8301c71f83", + "ingress": "d9908eba-1736-11ee-b77f-1fcc4c520068", + "service_function_parameters": {"correlation": "mpls", "weigjt": 101}, + "id": "6ecd9cf3-ca64-46c7-863f-f2eb1b9e838a", + "name": "port_pair_1", +} + + +class TestSfcPortPair(base.TestCase): + def test_basic(self): + sot = sfc_port_pair.SfcPortPair() + self.assertEqual('port_pair', sot.resource_key) + self.assertEqual('port_pairs', sot.resources_key) + self.assertEqual('/sfc/port_pairs', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = sfc_port_pair.SfcPortPair(**EXAMPLE) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['egress'], sot.egress) + self.assertEqual(EXAMPLE['ingress'], sot.ingress) + self.assertEqual( + EXAMPLE['service_function_parameters'], + sot.service_function_parameters, + ) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + + self.assertDictEqual( + { + "limit": "limit", + "marker": "marker", + 'description': 'description', + 'name': 'name', + 'project_id': 'project_id', + 'tenant_id': 'tenant_id', + 'ingress': 'ingress', + 'egress': 'egress', + }, + sot._query_mapping._mapping, + ) diff --git a/openstack/tests/unit/network/v2/test_sfc_port_pair_group.py b/openstack/tests/unit/network/v2/test_sfc_port_pair_group.py new file mode 100644 index 000000000..2dc1df6c5 --- /dev/null +++ b/openstack/tests/unit/network/v2/test_sfc_port_pair_group.py @@ -0,0 +1,63 @@ +# 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.network.v2 import sfc_port_pair_group +from openstack.tests.unit import base + + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + "description": "", + "project_id": "4ad57e7ce0b24fca8f12b9834d91079d", + "tenant_id": "4ad57e7ce0b24fca8f12b9834d91079d", + "port_pairs": ["8d57819a-174d-11ee-97b0-2f370d29c014"], + "port_pair_group_parameters": {}, + "id": "6ecd9cf3-ca64-46c7-863f-f2eb1b9e838a", + "name": "port_pair_gr", +} + + +class TestSfcPortPairGroup(base.TestCase): + def test_basic(self): + sot = sfc_port_pair_group.SfcPortPairGroup() + self.assertEqual('port_pair_group', sot.resource_key) + self.assertEqual('port_pair_groups', sot.resources_key) + self.assertEqual('/sfc/port_pair_groups', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = sfc_port_pair_group.SfcPortPairGroup(**EXAMPLE) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['port_pairs'], sot.port_pairs) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual( + EXAMPLE['port_pair_group_parameters'], + sot.port_pair_group_parameters, + ) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + + self.assertDictEqual( + { + "limit": "limit", + "marker": "marker", + 'description': 'description', + 'name': 'name', + 'project_id': 'project_id', + 'tenant_id': 'tenant_id', + }, + sot._query_mapping._mapping, + ) diff --git a/openstack/tests/unit/network/v2/test_sfc_service_graph.py b/openstack/tests/unit/network/v2/test_sfc_service_graph.py new file mode 100644 index 000000000..3eefb54ec --- /dev/null +++ b/openstack/tests/unit/network/v2/test_sfc_service_graph.py @@ -0,0 +1,65 @@ +# 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.network.v2 import sfc_service_graph +from openstack.tests.unit import base + + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + "description": "", + "project_id": "4ad57e7ce0b24fca8f12b9834d91079d", + "tenant_id": "4ad57e7ce0b24fca8f12b9834d91079d", + "port_chains": { + "0e6b9678-19aa-11ee-97ae-a3cec2c2ac72": [ + "1e19c266-19aa-11ee-8e02-6fa0c9a9832d" + ], + "2a394dc8-19aa-11ee-b87e-7f24d71926f1": [ + "3299fcf6-19aa-11ee-9398-3f8c68c11209" + ], + }, + "id": "6ecd9cf3-ca64-46c7-863f-f2eb1b9e838a", + "name": "service_graph", +} + + +class TestSfcServiceGraph(base.TestCase): + def test_basic(self): + sot = sfc_service_graph.SfcServiceGraph() + self.assertEqual('service_graph', sot.resource_key) + self.assertEqual('service_graphs', sot.resources_key) + self.assertEqual('/sfc/service_graphs', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = sfc_service_graph.SfcServiceGraph(**EXAMPLE) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['port_chains'], sot.port_chains) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + + self.assertDictEqual( + { + "limit": "limit", + "marker": "marker", + 'description': 'description', + 'name': 'name', + 'project_id': 'project_id', + 'tenant_id': 'tenant_id', + }, + sot._query_mapping._mapping, + ) diff --git a/releasenotes/notes/network_add_sfc_resources-8a52c0c8c1f8e932.yaml b/releasenotes/notes/network_add_sfc_resources-8a52c0c8c1f8e932.yaml new file mode 100644 index 000000000..382257b8a --- /dev/null +++ b/releasenotes/notes/network_add_sfc_resources-8a52c0c8c1f8e932.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add SFC resources: FlowClassifier, PortChain, PortPair, PortPairGroup + and ServiceGraph resources and introduce support for CRUD operations + for these. From 9950a3b4c056180e7e9e869a5c5a8141eb5335bc Mon Sep 17 00:00:00 2001 From: elajkat Date: Thu, 24 Aug 2023 13:26:33 +0200 Subject: [PATCH 3354/3836] docs: Use asterix for Network resources doc Change-Id: Ie873dd06ce58d52cd87f3bb7d78a22b9ed6a2849 --- doc/source/user/resources/network/index.rst | 53 +------------------ .../user/resources/network/v2/vpn/index.rst | 7 +-- 2 files changed, 4 insertions(+), 56 deletions(-) diff --git a/doc/source/user/resources/network/index.rst b/doc/source/user/resources/network/index.rst index 24215f2fd..ff063779c 100644 --- a/doc/source/user/resources/network/index.rst +++ b/doc/source/user/resources/network/index.rst @@ -3,56 +3,7 @@ Network Resources .. toctree:: :maxdepth: 1 + :glob: - v2/address_group - v2/address_scope - v2/agent - v2/auto_allocated_topology - v2/availability_zone - v2/bgp_peer - v2/bgp_speaker - v2/bgpvpn - v2/bgpvpn_network_association - v2/bgpvpn_port_association - v2/bgpvpn_router_association - v2/extension - v2/flavor - v2/floating_ip - v2/health_monitor - v2/listener - v2/load_balancer - v2/local_ip - v2/local_ip_association - v2/metering_label - v2/metering_label_rule - v2/ndp_proxy - v2/network - v2/network_ip_availability - v2/network_segment_range - v2/pool - v2/pool_member - v2/port - v2/qos_bandwidth_limit_rule - v2/qos_dscp_marking_rule - v2/qos_minimum_bandwidth_rule - v2/qos_minimum_packet_rate_rule - v2/qos_policy - v2/qos_rule_type - v2/quota - v2/rbac_policy - v2/router - v2/security_group - v2/security_group_rule - v2/segment - v2/service_profile - v2/service_provider - v2/sfc_flow_classifier - v2/sfc_port_chain - v2/sfc_port_pair_group - v2/sfc_port_pair - v2/sfc_service_graph - v2/subnet - v2/subnet_pool - v2/tap_flow - v2/tap_service + v2/* v2/vpn/index diff --git a/doc/source/user/resources/network/v2/vpn/index.rst b/doc/source/user/resources/network/v2/vpn/index.rst index 5180f8f2a..fee6fe931 100644 --- a/doc/source/user/resources/network/v2/vpn/index.rst +++ b/doc/source/user/resources/network/v2/vpn/index.rst @@ -3,9 +3,6 @@ VPNaaS Resources .. toctree:: :maxdepth: 1 + :glob: - endpoint_group - ipsec_site_connection - ike_policy - ipsec_policy - service + * From 3d7bbc481a7c84cf1596559838eae167ce615ede Mon Sep 17 00:00:00 2001 From: Seungju Baek Date: Fri, 11 Aug 2023 02:58:16 +0900 Subject: [PATCH 3355/3836] Fix heat stack _action function to handle exception The openstack orchestration `stack actions API` sends 400, 401, 404 as client error reponse. Currently, stack._action() function cannot handle there error responses. So I fixed it as below. - Changed stack._action() to handle exception from post request - Added test cases for BadRequsetException and NotFoundException in "test_check" unit test Change-Id: I464b19258d999eee9919332a519f870ada4e3017 --- openstack/orchestration/v1/stack.py | 5 +++-- openstack/tests/unit/orchestration/v1/test_stack.py | 9 ++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/openstack/orchestration/v1/stack.py b/openstack/orchestration/v1/stack.py index 4ca9c9dbb..387bd7a33 100644 --- a/openstack/orchestration/v1/stack.py +++ b/openstack/orchestration/v1/stack.py @@ -165,8 +165,9 @@ def update(self, session, preview=False): def _action(self, session, body): """Perform stack actions""" url = utils.urljoin(self.base_path, self._get_id(self), 'actions') - resp = session.post(url, json=body) - return resp.json() + resp = session.post(url, json=body, microversion=self.microversion) + exceptions.raise_from_response(resp) + return resp def check(self, session): return self._action(session, {'check': ''}) diff --git a/openstack/tests/unit/orchestration/v1/test_stack.py b/openstack/tests/unit/orchestration/v1/test_stack.py index 7eebade8a..c1a35c661 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack.py +++ b/openstack/tests/unit/orchestration/v1/test_stack.py @@ -206,12 +206,19 @@ def test_check(self): sess = mock.Mock() sot = stack.Stack(**FAKE) sot._action = mock.Mock() + sot._action.side_effect = [ + test_resource.FakeResponse(None, 200, None), + exceptions.BadRequestException(message='oops'), + exceptions.NotFoundException(message='oops'), + ] body = {'check': ''} sot.check(sess) - sot._action.assert_called_with(sess, body) + self.assertRaises(exceptions.BadRequestException, sot.check, sess) + self.assertRaises(exceptions.NotFoundException, sot.check, sess) + def test_fetch(self): sess = mock.Mock() sess.default_microversion = None From 891443e78d489856b679e173a82182a1be93a156 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 21 Aug 2023 13:07:51 +0100 Subject: [PATCH 3356/3836] cloud: Remove dead code This util method should have been removed in change I63ea929540f22c2b73faf4a1f767e30ecc1dd5dd. Change-Id: I4aadd480fb3a4c0ec9092595f854b5f33b7a6320 Signed-off-by: Stephen Finucane --- openstack/cloud/_utils.py | 53 --------------------------------------- 1 file changed, 53 deletions(-) diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index 573ce9540..803413ccc 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -17,7 +17,6 @@ import functools import inspect import re -import time import uuid from decorator import decorator @@ -379,58 +378,6 @@ def safe_dict_max(key, data): return max_value -def _call_client_and_retry( - client, url, retry_on=None, call_retries=3, retry_wait=2, **kwargs -): - """Method to provide retry operations. - - Some APIs utilize HTTP errors on certain operations to indicate that - the resource is presently locked, and as such this mechanism provides - the ability to retry upon known error codes. - - :param object client: The client method, such as: - ``self.baremetal_client.post`` - :param string url: The URL to perform the operation upon. - :param integer retry_on: A list of error codes that can be retried on. - The method also supports a single integer to be - defined. - :param integer call_retries: The number of times to retry the call upon - the error code defined by the 'retry_on' parameter. Default: 3 - :param integer retry_wait: The time in seconds to wait between retry - attempts. Default: 2 - - :returns: The object returned by the client call. - """ - - # NOTE(TheJulia): This method, as of this note, does not have direct - # unit tests, although is fairly well tested by the tests checking - # retry logic in test_baremetal_node.py. - log = _log.setup_logging('shade.http') - - if isinstance(retry_on, int): - retry_on = [retry_on] - - count = 0 - while count < call_retries: - count += 1 - try: - ret_val = client(url, **kwargs) - except exc.OpenStackCloudHTTPError as e: - if retry_on is not None and e.response.status_code in retry_on: - log.debug( - 'Received retryable error %(err)s, waiting ' - '%(wait)s seconds to retry', - {'err': e.response.status_code, 'wait': retry_wait}, - ) - time.sleep(retry_wait) - continue - else: - raise - # Break out of the loop, since the loop should only continue - # when we encounter a known connection error. - return ret_val - - def parse_range(value): """Parse a numerical range string. From 60be29a59649c221fde1789dc491e071d8999619 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 11 Aug 2023 17:48:26 +0100 Subject: [PATCH 3357/3836] identity: Add support for domain config https://docs.openstack.org/api-ref/identity/v3/index.html#create-domain-configuration Change-Id: I25e5e4266bfa1bf470fa2dcd38e9002950ecebef --- doc/source/user/proxies/identity_v3.rst | 8 ++ doc/source/user/resources/identity/index.rst | 17 +-- .../resources/identity/v3/domain_config.rst | 12 ++ openstack/identity/v3/_proxy.py | 117 ++++++++++++++++++ openstack/identity/v3/domain_config.py | 47 +++++++ .../identity/v3/test_domain_config.py | 83 +++++++++++++ .../unit/identity/v3/test_domain_config.py | 47 +++++++ .../tests/unit/identity/v3/test_proxy.py | 70 +++++++++++ ...domain-configuration-2e8bcaa20736b379.yaml | 5 + 9 files changed, 393 insertions(+), 13 deletions(-) create mode 100644 doc/source/user/resources/identity/v3/domain_config.rst create mode 100644 openstack/identity/v3/domain_config.py create mode 100644 openstack/tests/functional/identity/v3/test_domain_config.py create mode 100644 openstack/tests/unit/identity/v3/test_domain_config.py create mode 100644 releasenotes/notes/add-identity-domain-configuration-2e8bcaa20736b379.yaml diff --git a/doc/source/user/proxies/identity_v3.rst b/doc/source/user/proxies/identity_v3.rst index 9498f47dc..1afe30ed0 100644 --- a/doc/source/user/proxies/identity_v3.rst +++ b/doc/source/user/proxies/identity_v3.rst @@ -28,6 +28,14 @@ Domain Operations :members: create_domain, update_domain, delete_domain, get_domain, find_domain, domains +Domain Config Operations +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.identity.v3._proxy.Proxy + :noindex: + :members: create_domain_config, delete_domain_config, get_domain_config, + update_domain_config + Endpoint Operations ^^^^^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/identity/index.rst b/doc/source/user/resources/identity/index.rst index b52221494..7c35d6ebb 100644 --- a/doc/source/user/resources/identity/index.rst +++ b/doc/source/user/resources/identity/index.rst @@ -5,24 +5,15 @@ Identity v2 Resources --------------------- .. toctree:: :maxdepth: 1 + :glob: - v2/extension - v2/role - v2/tenant - v2/user + v2/* Identity v3 Resources --------------------- .. toctree:: :maxdepth: 1 + :glob: - v3/credential - v3/domain - v3/endpoint - v3/group - v3/policy - v3/project - v3/service - v3/trust - v3/user + v3/* diff --git a/doc/source/user/resources/identity/v3/domain_config.rst b/doc/source/user/resources/identity/v3/domain_config.rst new file mode 100644 index 000000000..28defa237 --- /dev/null +++ b/doc/source/user/resources/identity/v3/domain_config.rst @@ -0,0 +1,12 @@ +openstack.identity.v3.domain_config +=================================== + +.. automodule:: openstack.identity.v3.domain_config + +The Domain Class +---------------- + +The ``DomainConfig`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.identity.v3.domain_config.DomainConfig + :members: diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index d772f75c0..902e1b3e5 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -16,6 +16,7 @@ ) from openstack.identity.v3 import credential as _credential from openstack.identity.v3 import domain as _domain +from openstack.identity.v3 import domain_config as _domain_config from openstack.identity.v3 import endpoint as _endpoint from openstack.identity.v3 import federation_protocol as _federation_protocol from openstack.identity.v3 import group as _group @@ -51,6 +52,7 @@ from openstack.identity.v3 import trust as _trust from openstack.identity.v3 import user as _user from openstack import proxy +from openstack import resource from openstack import utils @@ -83,6 +85,8 @@ class Proxy(proxy.Proxy): "user": _user.User, } + # ========== Credentials ========== + def create_credential(self, **attrs): """Create a new credential from attributes @@ -165,6 +169,8 @@ def update_credential(self, credential, **attrs): """ return self._update(_credential.Credential, credential, **attrs) + # ========== Domains ========== + def create_domain(self, **attrs): """Create a new domain from attributes @@ -244,6 +250,85 @@ def update_domain(self, domain, **attrs): """ return self._update(_domain.Domain, domain, **attrs) + # ========== Domain configs ========== + + def create_domain_config(self, domain, **attrs): + """Create a new config for a domain from attributes. + + :param domain: The value can be the ID of a domain or + a :class:`~openstack.identity.v3.domain.Domain` instance. + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.identity.v3.domain_config.DomainConfig` + comprised of the properties on the DomainConfig class. + + :returns: The results of domain config creation + :rtype: :class:`~openstack.identity.v3.domain_config.DomainConfig` + """ + domain_id = resource.Resource._get_id(domain) + return self._create( + _domain_config.DomainConfig, + domain_id=domain_id, + **attrs, + ) + + def delete_domain_config(self, domain, ignore_missing=True): + """Delete a config for a domain + + :param domain: The value can be the ID of a domain or a + a :class:`~openstack.identity.v3.domain.Domain` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the identity provider does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent config for a domain. + + :returns: ``None`` + """ + domain_id = resource.Resource._get_id(domain) + self._delete( + _domain_config.DomainConfig, + domain_id=domain_id, + ignore_missing=ignore_missing, + ) + + def get_domain_config(self, domain): + """Get a single config for a domain + + :param domain_id: The value can be the ID of a domain or a + :class:`~openstack.identity.v3.domain.Domain` instance. + + :returns: One + :class:`~openstack.identity.v3.domain_config.DomainConfig` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource can be found. + """ + domain_id = resource.Resource._get_id(domain) + return self._get( + _domain_config.DomainConfig, + domain_id=domain_id, + requires_id=False, + ) + + def update_domain_config(self, domain, **attrs): + """Update a config for a domain + + :param domain_id: The value can be the ID of a domain or a + :class:`~openstack.identity.v3.domain.Domain` instance. + :attrs kwargs: The attributes to update on the config for a domain + represented by ``domain_id``. + + :returns: The updated config for a domain + :rtype: :class:`~openstack.identity.v3.domain_config.DomainConfig` + """ + domain_id = resource.Resource._get_id(domain) + return self._update( + _domain_config.DomainConfig, + domain_id=domain_id, + **attrs, + ) + + # ========== Endpoints ========== + def create_endpoint(self, **attrs): """Create a new endpoint from attributes @@ -326,6 +411,8 @@ def update_endpoint(self, endpoint, **attrs): """ return self._update(_endpoint.Endpoint, endpoint, **attrs) + # ========== Groups ========== + def create_group(self, **attrs): """Create a new group from attributes @@ -462,6 +549,8 @@ def group_users(self, group, **attrs): users = self._list(_user.User, base_path=base_path, **attrs) return users + # ========== Policies ========== + def create_policy(self, **attrs): """Create a new policy from attributes @@ -541,6 +630,8 @@ def update_policy(self, policy, **attrs): """ return self._update(_policy.Policy, policy, **attrs) + # ========== Project ========== + def create_project(self, **attrs): """Create a new project from attributes @@ -638,6 +729,8 @@ def update_project(self, project, **attrs): """ return self._update(_project.Project, project, **attrs) + # ========== Services ========== + def create_service(self, **attrs): """Create a new service from attributes @@ -717,6 +810,8 @@ def update_service(self, service, **attrs): """ return self._update(_service.Service, service, **attrs) + # ========== Users ========== + def create_user(self, **attrs): """Create a new user from attributes @@ -799,6 +894,8 @@ def update_user(self, user, **attrs): """ return self._update(_user.User, user, **attrs) + # ========== Trusts ========== + def create_trust(self, **attrs): """Create a new trust from attributes @@ -865,6 +962,8 @@ def trusts(self, **query): # TODO(briancurtin): This is paginated but requires base list changes. return self._list(_trust.Trust, **query) + # ========== Regions ========== + def create_region(self, **attrs): """Create a new region from attributes @@ -944,6 +1043,8 @@ def update_region(self, region, **attrs): """ return self._update(_region.Region, region, **attrs) + # ========== Roles ========== + def create_role(self, **attrs): """Create a new role from attributes @@ -1025,6 +1126,8 @@ def update_role(self, role, **attrs): """ return self._update(_role.Role, role, **attrs) + # ========== Role assignments ========== + def role_assignments_filter( self, domain=None, project=None, system=None, group=None, user=None ): @@ -1127,6 +1230,8 @@ def role_assignments(self, **query): """ return self._list(_role_assignment.RoleAssignment, **query) + # ========== Registered limits ========== + def registered_limits(self, **query): """Retrieve a generator of registered_limits @@ -1204,6 +1309,8 @@ def delete_registered_limit(self, registered_limit, ignore_missing=True): ignore_missing=ignore_missing, ) + # ========== Limits ========== + def limits(self, **query): """Retrieve a generator of limits @@ -1267,6 +1374,8 @@ def delete_limit(self, limit, ignore_missing=True): """ self._delete(limit.Limit, limit, ignore_missing=ignore_missing) + # ========== Roles ========== + def assign_domain_role_to_user(self, domain, user, role): """Assign role to user on a domain @@ -1555,6 +1664,8 @@ def validate_group_has_system_role(self, group, role, system): system = self._get_resource(_system.System, system) return system.validate_group_has_role(self, group, role) + # ========== Application credentials ========== + def application_credentials(self, user, **query): """Retrieve a generator of application credentials @@ -1681,6 +1792,8 @@ def delete_application_credential( ignore_missing=ignore_missing, ) + # ========== Federation protocols ========== + def create_federation_protocol(self, idp_id, **attrs): """Create a new federation protocol from attributes @@ -1834,6 +1947,8 @@ def update_federation_protocol(self, idp_id, protocol, **attrs): idp_id = idp_id.id return self._update(cls, protocol, idp_id=idp_id, **attrs) + # ========== Mappings ========== + def create_mapping(self, **attrs): """Create a new mapping from attributes @@ -1914,6 +2029,8 @@ def update_mapping(self, mapping, **attrs): """ return self._update(_mapping.Mapping, mapping, **attrs) + # ========== Identity providers ========== + def create_identity_provider(self, **attrs): """Create a new identity provider from attributes diff --git a/openstack/identity/v3/domain_config.py b/openstack/identity/v3/domain_config.py new file mode 100644 index 000000000..ba95185d3 --- /dev/null +++ b/openstack/identity/v3/domain_config.py @@ -0,0 +1,47 @@ +# 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 resource + + +class DomainConfigLDAP(resource.Resource): + #: The base distinguished name (DN) of LDAP. + user_tree_dn = resource.Body('user_tree_dn') + #: The LDAP URL. + url = resource.Body('url') + + +class DomainConfigDriver(resource.Resource): + #: The Identity backend driver. + driver = resource.Body('driver') + + +class DomainConfig(resource.Resource): + resource_key = 'config' + base_path = '/domains/%(domain_id)s/config' + requires_id = False + create_requires_id = False + commit_method = 'PATCH' + create_method = 'PUT' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + + #: The domain ID. + domain_id = resource.URI('domain_id') + #: An identity object. + identity = resource.Body('identity', type=DomainConfigDriver) + #: The config object. + ldap = resource.Body('ldap', type=DomainConfigLDAP) diff --git a/openstack/tests/functional/identity/v3/test_domain_config.py b/openstack/tests/functional/identity/v3/test_domain_config.py new file mode 100644 index 000000000..e294498f4 --- /dev/null +++ b/openstack/tests/functional/identity/v3/test_domain_config.py @@ -0,0 +1,83 @@ +# 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 uuid + +from openstack.identity.v3 import domain as _domain +from openstack.identity.v3 import domain_config as _domain_config +from openstack.tests.functional import base + + +class TestDomainConfig(base.BaseFunctionalTest): + def setUp(self): + super().setUp() + + self.domain_name = self.getUniqueString() + + # create the domain and domain config + + self.domain = self.operator_cloud.create_domain( + name=self.domain_name, + ) + self.assertIsInstance(self.domain, _domain.Domain) + self.addCleanup(self._delete_domain) + + def _delete_domain(self): + self.operator_cloud.identity.update_domain( + self.domain, + enabled=False, + ) + self.operator_cloud.identity.delete_domain(self.domain) + + def test_domain_config(self): + # create the domain config + + domain_config = self.operator_cloud.identity.create_domain_config( + self.domain, + identity={'driver': uuid.uuid4().hex}, + ldap={'url': uuid.uuid4().hex}, + ) + self.assertIsInstance( + domain_config, + _domain_config.DomainConfig, + ) + + # update the domain config + + ldap_url = uuid.uuid4().hex + domain_config = self.operator_cloud.identity.update_domain_config( + self.domain, + ldap={'url': ldap_url}, + ) + self.assertIsInstance( + domain_config, + _domain_config.DomainConfig, + ) + + # retrieve details of the (updated) domain config + + domain_config = self.operator_cloud.identity.get_domain_config( + self.domain, + ) + self.assertIsInstance( + domain_config, + _domain_config.DomainConfig, + ) + self.assertEqual(ldap_url, domain_config.ldap.url) + + # delete the domain config + + result = self.operator_cloud.identity.delete_domain_config( + self.domain, + ignore_missing=False, + ) + self.assertIsNone(result) diff --git a/openstack/tests/unit/identity/v3/test_domain_config.py b/openstack/tests/unit/identity/v3/test_domain_config.py new file mode 100644 index 000000000..26d774d9a --- /dev/null +++ b/openstack/tests/unit/identity/v3/test_domain_config.py @@ -0,0 +1,47 @@ +# 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.identity.v3 import domain_config +from openstack.tests.unit import base + + +EXAMPLE = { + 'identity': { + 'driver': 'ldap', + }, + 'ldap': { + 'url': 'ldap://myldap.com:389/', + 'user_tree_dn': 'ou=Users,dc=my_new_root,dc=org', + }, +} + + +class TestDomainConfig(base.TestCase): + def test_basic(self): + sot = domain_config.DomainConfig() + self.assertEqual('config', sot.resource_key) + self.assertEqual('/domains/%(domain_id)s/config', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + + def test_make_it(self): + sot = domain_config.DomainConfig(**EXAMPLE) + self.assertIsInstance(sot.identity, domain_config.DomainConfigDriver) + self.assertEqual(EXAMPLE['identity']['driver'], sot.identity.driver) + self.assertIsInstance(sot.ldap, domain_config.DomainConfigLDAP) + self.assertEqual(EXAMPLE['ldap']['url'], sot.ldap.url) + self.assertEqual( + EXAMPLE['ldap']['user_tree_dn'], + sot.ldap.user_tree_dn, + ) diff --git a/openstack/tests/unit/identity/v3/test_proxy.py b/openstack/tests/unit/identity/v3/test_proxy.py index 4c40dff37..a0b7fcfad 100644 --- a/openstack/tests/unit/identity/v3/test_proxy.py +++ b/openstack/tests/unit/identity/v3/test_proxy.py @@ -15,6 +15,7 @@ from openstack.identity.v3 import _proxy from openstack.identity.v3 import credential from openstack.identity.v3 import domain +from openstack.identity.v3 import domain_config from openstack.identity.v3 import endpoint from openstack.identity.v3 import group from openstack.identity.v3 import policy @@ -85,6 +86,75 @@ def test_domain_update(self): self.verify_update(self.proxy.update_domain, domain.Domain) +class TestIdentityProxyDomainConfig(TestIdentityProxyBase): + def test_domain_config_create_attrs(self): + self.verify_create( + self.proxy.create_domain_config, + domain_config.DomainConfig, + method_args=['domain_id'], + method_kwargs={}, + expected_args=[], + expected_kwargs={ + 'domain_id': 'domain_id', + }, + ) + + def test_domain_config_delete(self): + self.verify_delete( + self.proxy.delete_domain_config, + domain_config.DomainConfig, + ignore_missing=False, + method_args=['domain_id'], + method_kwargs={}, + expected_args=[], + expected_kwargs={ + 'domain_id': 'domain_id', + }, + ) + + def test_domain_config_delete_ignore(self): + self.verify_delete( + self.proxy.delete_domain_config, + domain_config.DomainConfig, + ignore_missing=True, + method_args=['domain_id'], + method_kwargs={}, + expected_args=[], + expected_kwargs={ + 'domain_id': 'domain_id', + }, + ) + + # no find_domain_config + + def test_domain_config_get(self): + self.verify_get( + self.proxy.get_domain_config, + domain_config.DomainConfig, + method_args=['domain_id'], + method_kwargs={}, + expected_args=[], + expected_kwargs={ + 'domain_id': 'domain_id', + 'requires_id': False, + }, + ) + + # no domain_configs + + def test_domain_config_update(self): + self.verify_update( + self.proxy.update_domain_config, + domain_config.DomainConfig, + method_args=['domain_id'], + method_kwargs={}, + expected_args=[], + expected_kwargs={ + 'domain_id': 'domain_id', + }, + ) + + class TestIdentityProxyEndpoint(TestIdentityProxyBase): def test_endpoint_create_attrs(self): self.verify_create(self.proxy.create_endpoint, endpoint.Endpoint) diff --git a/releasenotes/notes/add-identity-domain-configuration-2e8bcaa20736b379.yaml b/releasenotes/notes/add-identity-domain-configuration-2e8bcaa20736b379.yaml new file mode 100644 index 000000000..6929c8be6 --- /dev/null +++ b/releasenotes/notes/add-identity-domain-configuration-2e8bcaa20736b379.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add support for creating, updating and deleting domain configurations for + the identity service. From e66cee12c88ff9957b263a834ab47e31a3e86b6f Mon Sep 17 00:00:00 2001 From: rladntjr4 Date: Sat, 26 Aug 2023 18:41:39 +0900 Subject: [PATCH 3358/3836] Unused param in stack unit test This FAKE parameter only used in class Resource __init__ function by '_syncronized' positional argument. it could be misunderstood so i delete it. Change-Id: I8e3bac6730de618a035eccc485d445e083a90079 --- openstack/tests/unit/orchestration/v1/test_stack.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openstack/tests/unit/orchestration/v1/test_stack.py b/openstack/tests/unit/orchestration/v1/test_stack.py index c1a35c661..7ac4fef96 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack.py +++ b/openstack/tests/unit/orchestration/v1/test_stack.py @@ -181,7 +181,7 @@ def test_make_it(self): @mock.patch.object(resource.Resource, 'create') def test_create(self, mock_create): sess = mock.Mock() - sot = stack.Stack(FAKE) + sot = stack.Stack() res = sot.create(sess) @@ -193,7 +193,7 @@ def test_create(self, mock_create): @mock.patch.object(resource.Resource, 'commit') def test_commit(self, mock_commit): sess = mock.Mock() - sot = stack.Stack(FAKE) + sot = stack.Stack() res = sot.commit(sess) From b21ac8981f059d1ae35e28622ca25ef5671f21eb Mon Sep 17 00:00:00 2001 From: suzhengwei Date: Mon, 30 Jan 2023 08:38:46 +0800 Subject: [PATCH 3359/3836] support notification vmoves for masakari Masakari provides a new 'VMove' api, which could help users to gain insigh into the process or result of the host recovery workflow. Change-Id: I3da3024a8f34563bf0bff3924ce6ad8ac18a3018 --- openstack/instance_ha/v1/_proxy.py | 42 ++++++++++ openstack/instance_ha/v1/vmove.py | 63 +++++++++++++++ .../tests/unit/instance_ha/v1/test_proxy.py | 23 ++++++ .../tests/unit/instance_ha/v1/test_vmove.py | 79 +++++++++++++++++++ .../add-masakari-vmoves-873ad67830c92254.yaml | 4 + 5 files changed, 211 insertions(+) create mode 100644 openstack/instance_ha/v1/vmove.py create mode 100644 openstack/tests/unit/instance_ha/v1/test_vmove.py create mode 100644 releasenotes/notes/add-masakari-vmoves-873ad67830c92254.yaml diff --git a/openstack/instance_ha/v1/_proxy.py b/openstack/instance_ha/v1/_proxy.py index 13471d182..11081e5ea 100644 --- a/openstack/instance_ha/v1/_proxy.py +++ b/openstack/instance_ha/v1/_proxy.py @@ -16,6 +16,7 @@ from openstack.instance_ha.v1 import host as _host from openstack.instance_ha.v1 import notification as _notification from openstack.instance_ha.v1 import segment as _segment +from openstack.instance_ha.v1 import vmove as _vmove from openstack import proxy from openstack import resource @@ -30,6 +31,7 @@ class Proxy(proxy.Proxy): "host": _host.Host, "notification": _notification.Notification, "segment": _segment.Segment, + "vmove": _vmove.VMove, } def notifications(self, **query): @@ -216,3 +218,43 @@ def delete_host(self, host, segment_id=None, ignore_missing=True): segment_id=segment_id, ignore_missing=ignore_missing, ) + + def vmoves(self, notification, **query): + """Return a generator of vmoves. + + :param notification: The value can be the UUID of a notification or + a :class: `~masakariclient.sdk.ha.v1.notification.Notification` + instance. + :param kwargs query: Optional query parameters to be sent to + limit the vmoves being returned. + + :returns: A generator of vmoves + """ + notification_id = resource.Resource._get_id(notification) + return self._list( + _vmove.VMove, + notification_id=notification_id, + **query, + ) + + def get_vmove(self, vmove, notification): + """Get a single vmove. + + :param vmove: The value can be the UUID of one vmove or + a :class: `~masakariclient.sdk.ha.v1.vmove.VMove` instance. + :param notification: The value can be the UUID of a notification or + a :class: `~masakariclient.sdk.ha.v1.notification.Notification` + instance. + :returns: one 'VMove' resource class. + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + :raises: :class:`~openstack.exceptions.InvalidRequest` + when notification_id is None. + """ + notification_id = resource.Resource._get_id(notification) + vmove_id = resource.Resource._get_id(vmove) + return self._get( + _vmove.VMove, + vmove_id, + notification_id=notification_id, + ) diff --git a/openstack/instance_ha/v1/vmove.py b/openstack/instance_ha/v1/vmove.py new file mode 100644 index 000000000..e9cf9e360 --- /dev/null +++ b/openstack/instance_ha/v1/vmove.py @@ -0,0 +1,63 @@ +# Copyright(c) 2022 Inspur +# +# 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 resource + + +class VMove(resource.Resource): + resource_key = "vmove" + resources_key = "vmoves" + base_path = "/notifications/%(notification_id)s/vmoves" + + # capabilities + # 1] GET /v1/notifications/{notification_uuid}/vmoves + # 2] GET /v1/notifications/{notification_uuid}/vmoves/{vmove_uuid} + allow_list = True + allow_fetch = True + + _query_mapping = resource.QueryParameters( + "sort_key", + "sort_dir", + "type", + "status", + ) + + #: A ID of representing this vmove + id = resource.Body("id") + #: A UUID of representing this vmove + uuid = resource.Body("uuid") + #: The notification UUID this vmove belongs to(in URI) + notification_id = resource.URI("notification_id") + #: A created time of this vmove + created_at = resource.Body("created_at") + #: A latest updated time of this vmove + updated_at = resource.Body("updated_at") + #: The instance uuid of this vmove + server_id = resource.Body("instance_uuid") + #: The instance name of this vmove + server_name = resource.Body("instance_name") + #: The source host of this vmove + source_host = resource.Body("source_host") + #: The dest host of this vmove + dest_host = resource.Body("dest_host") + #: A start time of this vmove + start_time = resource.Body("start_time") + #: A end time of this vmove + end_time = resource.Body("end_time") + #: The status of this vmove + status = resource.Body("status") + #: The type of this vmove + type = resource.Body("type") + #: The message of this vmove + message = resource.Body("message") diff --git a/openstack/tests/unit/instance_ha/v1/test_proxy.py b/openstack/tests/unit/instance_ha/v1/test_proxy.py index 2e54410c3..4fa93520d 100644 --- a/openstack/tests/unit/instance_ha/v1/test_proxy.py +++ b/openstack/tests/unit/instance_ha/v1/test_proxy.py @@ -16,10 +16,13 @@ from openstack.instance_ha.v1 import host from openstack.instance_ha.v1 import notification from openstack.instance_ha.v1 import segment +from openstack.instance_ha.v1 import vmove from openstack.tests.unit import test_proxy_base SEGMENT_ID = "c50b96eb-2a66-40f8-bca8-c5fa90d595c0" HOST_ID = "52d05e43-d08e-42b8-ae33-e47c8ea2ad47" +NOTIFICATION_ID = "a0e70d3a-b3a2-4616-b65d-a7c03a2c85fc" +VMOVE_ID = "16a7c91f-8342-49a7-c731-3a632293f845" class TestInstanceHaProxy(test_proxy_base.TestProxyBase): @@ -102,3 +105,23 @@ def test_segment_update(self): def test_segment_delete(self): self.verify_delete(self.proxy.delete_segment, segment.Segment, True) + + +class TestInstanceHaVMoves(TestInstanceHaProxy): + def test_vmoves(self): + self.verify_list( + self.proxy.vmoves, + vmove.VMove, + method_args=[NOTIFICATION_ID], + expected_args=[], + expected_kwargs={"notification_id": NOTIFICATION_ID}, + ) + + def test_vmove_get(self): + self.verify_get( + self.proxy.get_vmove, + vmove.VMove, + method_args=[VMOVE_ID, NOTIFICATION_ID], + expected_args=[VMOVE_ID], + expected_kwargs={"notification_id": NOTIFICATION_ID}, + ) diff --git a/openstack/tests/unit/instance_ha/v1/test_vmove.py b/openstack/tests/unit/instance_ha/v1/test_vmove.py new file mode 100644 index 000000000..c5c708d96 --- /dev/null +++ b/openstack/tests/unit/instance_ha/v1/test_vmove.py @@ -0,0 +1,79 @@ +# Copyright(c) 2022 Inspur +# +# 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.instance_ha.v1 import vmove +from openstack.tests.unit import base + +FAKE_ID = "1" +FAKE_UUID = "16a7c91f-8342-49a7-c731-3a632293f845" +FAKE_NOTIFICATION_ID = "a0e70d3a-b3a2-4616-b65d-a7c03a2c85fc" +FAKE_SERVER_ID = "1c2f1795-ce78-4d4c-afd0-ce141fdb3952" + +VMOVE = { + 'id': FAKE_ID, + 'uuid': FAKE_UUID, + 'notification_id': FAKE_NOTIFICATION_ID, + 'created_at': "2023-01-28T14:55:26.000000", + 'updated_at': "2023-01-28T14:55:31.000000", + 'server_id': FAKE_SERVER_ID, + 'server_name': 'vm1', + 'source_host': 'host1', + 'dest_host': 'host2', + 'start_time': "2023-01-28T14:55:27.000000", + 'end_time': "2023-01-28T14:55:31.000000", + 'status': 'succeeded', + 'type': 'evacuation', + 'message': None, +} + + +class TestVMove(base.TestCase): + def test_basic(self): + sot = vmove.VMove(VMOVE) + self.assertEqual("vmove", sot.resource_key) + self.assertEqual("vmoves", sot.resources_key) + self.assertEqual( + "/notifications/%(notification_id)s/vmoves", sot.base_path + ) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_fetch) + + self.assertDictEqual( + { + "status": "status", + "type": "type", + "limit": "limit", + "marker": "marker", + "sort_dir": "sort_dir", + "sort_key": "sort_key", + }, + sot._query_mapping._mapping, + ) + + def test_create(self): + sot = vmove.VMove(**VMOVE) + self.assertEqual(VMOVE["id"], sot.id) + self.assertEqual(VMOVE["uuid"], sot.uuid) + self.assertEqual(VMOVE["notification_id"], sot.notification_id) + self.assertEqual(VMOVE["created_at"], sot.created_at) + self.assertEqual(VMOVE["updated_at"], sot.updated_at) + self.assertEqual(VMOVE["server_id"], sot.server_id) + self.assertEqual(VMOVE["server_name"], sot.server_name) + self.assertEqual(VMOVE["source_host"], sot.source_host) + self.assertEqual(VMOVE["dest_host"], sot.dest_host) + self.assertEqual(VMOVE["start_time"], sot.start_time) + self.assertEqual(VMOVE["end_time"], sot.end_time) + self.assertEqual(VMOVE["status"], sot.status) + self.assertEqual(VMOVE["type"], sot.type) + self.assertEqual(VMOVE["message"], sot.message) diff --git a/releasenotes/notes/add-masakari-vmoves-873ad67830c92254.yaml b/releasenotes/notes/add-masakari-vmoves-873ad67830c92254.yaml new file mode 100644 index 000000000..ffe6d382c --- /dev/null +++ b/releasenotes/notes/add-masakari-vmoves-873ad67830c92254.yaml @@ -0,0 +1,4 @@ +--- +features: + - Add support for the new ``VMove`` resource for the instance + HA service (Masakari). From 61b94cdc5f75c16c307bb59d42700c4b87af6ccf Mon Sep 17 00:00:00 2001 From: rladntjr4 Date: Sat, 12 Aug 2023 18:25:29 +0900 Subject: [PATCH 3360/3836] Implement stack export in openstacksdk 'stack export' for get stack data in JSON format in this commit just get response by /export URL Need json_dump when print out in CLI story: 2010881 task: 48604 Change-Id: Iccb5d7a5f86a810fec5424ae1e44a792d69b4a65 --- doc/source/user/proxies/orchestration.rst | 3 +- openstack/orchestration/v1/_proxy.py | 18 ++++++++++++ openstack/orchestration/v1/stack.py | 13 +++++++++ .../tests/unit/orchestration/v1/test_proxy.py | 29 +++++++++++++++++++ .../tests/unit/orchestration/v1/test_stack.py | 17 +++++++++++ .../add-stack-export-3ace746a8c80d766.yaml | 4 +++ 6 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-stack-export-3ace746a8c80d766.yaml diff --git a/doc/source/user/proxies/orchestration.rst b/doc/source/user/proxies/orchestration.rst index 93ee46fc3..86662b7b1 100644 --- a/doc/source/user/proxies/orchestration.rst +++ b/doc/source/user/proxies/orchestration.rst @@ -20,7 +20,8 @@ Stack Operations :noindex: :members: create_stack, check_stack, update_stack, delete_stack, find_stack, get_stack, get_stack_environment, get_stack_files, - get_stack_template, stacks, validate_template, resources + get_stack_template, stacks, validate_template, resources, + export_stack Software Configuration Operations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index 27200f2e5..9ada733e7 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -24,6 +24,9 @@ from openstack import resource +# TODO(rladntjr4): Some of these methods support lookup by ID, while +# others support lookup by ID or name. We should choose one and use +# it consistently. class Proxy(proxy.Proxy): _resource_registry = { "resource": _resource.Resource, @@ -222,6 +225,21 @@ def abandon_stack(self, stack): res = self._get_resource(_stack.Stack, stack) return res.abandon(self) + def export_stack(self, stack): + """Get the stack data in JSON format + + :param stack: The value can be the ID or a name or + an instance of :class:`~openstack.orchestration.v1.stack.Stack` + :returns: A dictionary containing the stack data. + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + if isinstance(stack, _stack.Stack): + obj = stack + else: + obj = self._find(_stack.Stack, stack, ignore_missing=False) + return obj.export(self) + def get_stack_template(self, stack): """Get template used by a stack diff --git a/openstack/orchestration/v1/stack.py b/openstack/orchestration/v1/stack.py index 387bd7a33..a62e0017e 100644 --- a/openstack/orchestration/v1/stack.py +++ b/openstack/orchestration/v1/stack.py @@ -179,6 +179,19 @@ def abandon(self, session): resp = session.delete(url) return resp.json() + def export(self, session): + """Export a stack data + + :param session: The session to use for making this request. + :return: A dictionary containing the stack data. + """ + url = utils.urljoin( + self.base_path, self.name, self._get_id(self), 'export' + ) + resp = session.get(url) + exceptions.raise_from_response(resp) + return resp.json() + def fetch( self, session, diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index fe37c70e0..0e3af53df 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -121,6 +121,35 @@ def test_abandon_stack(self): expected_args=[self.proxy], ) + @mock.patch.object(stack.Stack, 'find') + def test_export_stack_with_identity(self, mock_find): + stack_id = '1234' + stack_name = 'test_stack' + stk = stack.Stack(id=stack_id, name=stack_name) + mock_find.return_value = stk + + self._verify( + 'openstack.orchestration.v1.stack.Stack.export', + self.proxy.export_stack, + method_args=['IDENTITY'], + expected_args=[self.proxy], + ) + mock_find.assert_called_once_with( + mock.ANY, 'IDENTITY', ignore_missing=False + ) + + def test_export_stack_with_object(self): + stack_id = '1234' + stack_name = 'test_stack' + stk = stack.Stack(id=stack_id, name=stack_name) + + self._verify( + 'openstack.orchestration.v1.stack.Stack.export', + self.proxy.export_stack, + method_args=[stk], + expected_args=[self.proxy], + ) + def test_delete_stack(self): self.verify_delete(self.proxy.delete_stack, stack.Stack, False) diff --git a/openstack/tests/unit/orchestration/v1/test_stack.py b/openstack/tests/unit/orchestration/v1/test_stack.py index c1a35c661..e8ab9205d 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack.py +++ b/openstack/tests/unit/orchestration/v1/test_stack.py @@ -272,6 +272,23 @@ def test_abandon(self): 'stacks/%s/%s/abandon' % (FAKE_NAME, FAKE_ID), ) + def test_export(self): + sess = mock.Mock() + sess.default_microversion = None + + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.headers = {} + mock_response.json.return_value = {} + sess.get = mock.Mock(return_value=mock_response) + sot = stack.Stack(**FAKE) + + sot.export(sess) + + sess.get.assert_called_with( + 'stacks/%s/%s/export' % (FAKE_NAME, FAKE_ID), + ) + def test_update(self): sess = mock.Mock() sess.default_microversion = None diff --git a/releasenotes/notes/add-stack-export-3ace746a8c80d766.yaml b/releasenotes/notes/add-stack-export-3ace746a8c80d766.yaml new file mode 100644 index 000000000..00d680b0f --- /dev/null +++ b/releasenotes/notes/add-stack-export-3ace746a8c80d766.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add ``export_stack`` to print stack infomation in a json format From 19ba0adc3398eb0ed31a373cbccfbae7c785b8e0 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 11 Aug 2023 18:07:43 +0100 Subject: [PATCH 3361/3836] docs: Add missing docs for identity resources Change-Id: I895f47336471d64bad3a0dc1672c3b79e147a6e5 Signed-off-by: Stephen Finucane --- doc/source/user/resources/identity/index.rst | 8 ++++++++ .../identity/v3/application_credential.rst | 13 +++++++++++++ .../resources/identity/v3/federation_protocol.rst | 13 +++++++++++++ .../resources/identity/v3/identity_provider.rst | 13 +++++++++++++ doc/source/user/resources/identity/v3/limit.rst | 12 ++++++++++++ doc/source/user/resources/identity/v3/mapping.rst | 12 ++++++++++++ doc/source/user/resources/identity/v3/region.rst | 12 ++++++++++++ .../user/resources/identity/v3/registered_limit.rst | 13 +++++++++++++ doc/source/user/resources/identity/v3/role.rst | 12 ++++++++++++ .../user/resources/identity/v3/role_assignment.rst | 13 +++++++++++++ .../identity/v3/role_domain_group_assignment.rst | 13 +++++++++++++ .../identity/v3/role_domain_user_assignment.rst | 13 +++++++++++++ .../identity/v3/role_project_group_assignment.rst | 13 +++++++++++++ .../identity/v3/role_project_user_assignment.rst | 13 +++++++++++++ .../identity/v3/role_system_group_assignment.rst | 13 +++++++++++++ .../identity/v3/role_system_user_assignment.rst | 13 +++++++++++++ doc/source/user/resources/identity/v3/system.rst | 12 ++++++++++++ doc/source/user/resources/identity/version.rst | 12 ++++++++++++ 18 files changed, 223 insertions(+) create mode 100644 doc/source/user/resources/identity/v3/application_credential.rst create mode 100644 doc/source/user/resources/identity/v3/federation_protocol.rst create mode 100644 doc/source/user/resources/identity/v3/identity_provider.rst create mode 100644 doc/source/user/resources/identity/v3/limit.rst create mode 100644 doc/source/user/resources/identity/v3/mapping.rst create mode 100644 doc/source/user/resources/identity/v3/region.rst create mode 100644 doc/source/user/resources/identity/v3/registered_limit.rst create mode 100644 doc/source/user/resources/identity/v3/role.rst create mode 100644 doc/source/user/resources/identity/v3/role_assignment.rst create mode 100644 doc/source/user/resources/identity/v3/role_domain_group_assignment.rst create mode 100644 doc/source/user/resources/identity/v3/role_domain_user_assignment.rst create mode 100644 doc/source/user/resources/identity/v3/role_project_group_assignment.rst create mode 100644 doc/source/user/resources/identity/v3/role_project_user_assignment.rst create mode 100644 doc/source/user/resources/identity/v3/role_system_group_assignment.rst create mode 100644 doc/source/user/resources/identity/v3/role_system_user_assignment.rst create mode 100644 doc/source/user/resources/identity/v3/system.rst create mode 100644 doc/source/user/resources/identity/version.rst diff --git a/doc/source/user/resources/identity/index.rst b/doc/source/user/resources/identity/index.rst index 7c35d6ebb..82d7df631 100644 --- a/doc/source/user/resources/identity/index.rst +++ b/doc/source/user/resources/identity/index.rst @@ -17,3 +17,11 @@ Identity v3 Resources :glob: v3/* + +Other Resources +--------------- + +.. toctree:: + :maxdepth: 1 + + version diff --git a/doc/source/user/resources/identity/v3/application_credential.rst b/doc/source/user/resources/identity/v3/application_credential.rst new file mode 100644 index 000000000..ee7b1dc14 --- /dev/null +++ b/doc/source/user/resources/identity/v3/application_credential.rst @@ -0,0 +1,13 @@ +openstack.identity.v3.application_credential +============================================ + +.. automodule:: openstack.identity.v3.application_credential + +The ApplicationCredential Class +------------------------------- + +The ``ApplicationCredential`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.identity.v3.application_credential.ApplicationCredential + :members: diff --git a/doc/source/user/resources/identity/v3/federation_protocol.rst b/doc/source/user/resources/identity/v3/federation_protocol.rst new file mode 100644 index 000000000..4cd8a9712 --- /dev/null +++ b/doc/source/user/resources/identity/v3/federation_protocol.rst @@ -0,0 +1,13 @@ +openstack.identity.v3.federation_protocol +========================================= + +.. automodule:: openstack.identity.v3.federation_protocol + +The FederationProtocol Class +---------------------------- + +The ``FederationProtocol`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.identity.v3.federation_protocol.FederationProtocol + :members: diff --git a/doc/source/user/resources/identity/v3/identity_provider.rst b/doc/source/user/resources/identity/v3/identity_provider.rst new file mode 100644 index 000000000..1b1bc7642 --- /dev/null +++ b/doc/source/user/resources/identity/v3/identity_provider.rst @@ -0,0 +1,13 @@ +openstack.identity.v3.identity_provider +======================================= + +.. automodule:: openstack.identity.v3.identity_provider + +The IdentityProvider Class +-------------------------- + +The ``IdentityProvider`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.identity.v3.identity_provider.IdentityProvider + :members: diff --git a/doc/source/user/resources/identity/v3/limit.rst b/doc/source/user/resources/identity/v3/limit.rst new file mode 100644 index 000000000..f8a8174e4 --- /dev/null +++ b/doc/source/user/resources/identity/v3/limit.rst @@ -0,0 +1,12 @@ +openstack.identity.v3.limit +=========================== + +.. automodule:: openstack.identity.v3.limit + +The Limit Class +--------------- + +The ``Limit`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.identity.v3.limit.Limit + :members: diff --git a/doc/source/user/resources/identity/v3/mapping.rst b/doc/source/user/resources/identity/v3/mapping.rst new file mode 100644 index 000000000..dd242f958 --- /dev/null +++ b/doc/source/user/resources/identity/v3/mapping.rst @@ -0,0 +1,12 @@ +openstack.identity.v3.mapping +============================= + +.. automodule:: openstack.identity.v3.mapping + +The Mapping Class +----------------- + +The ``Mapping`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.identity.v3.mapping.Mapping + :members: diff --git a/doc/source/user/resources/identity/v3/region.rst b/doc/source/user/resources/identity/v3/region.rst new file mode 100644 index 000000000..ba334a76a --- /dev/null +++ b/doc/source/user/resources/identity/v3/region.rst @@ -0,0 +1,12 @@ +openstack.identity.v3.region +============================ + +.. automodule:: openstack.identity.v3.region + +The Region Class +---------------- + +The ``Region`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.identity.v3.region.Region + :members: diff --git a/doc/source/user/resources/identity/v3/registered_limit.rst b/doc/source/user/resources/identity/v3/registered_limit.rst new file mode 100644 index 000000000..8ecdfe3f5 --- /dev/null +++ b/doc/source/user/resources/identity/v3/registered_limit.rst @@ -0,0 +1,13 @@ +openstack.identity.v3.registered_limit +====================================== + +.. automodule:: openstack.identity.v3.registered_limit + +The RegisteredLimit Class +------------------------- + +The ``RegisteredLimit`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.identity.v3.registered_limit.RegisteredLimit + :members: diff --git a/doc/source/user/resources/identity/v3/role.rst b/doc/source/user/resources/identity/v3/role.rst new file mode 100644 index 000000000..baa85e87d --- /dev/null +++ b/doc/source/user/resources/identity/v3/role.rst @@ -0,0 +1,12 @@ +openstack.identity.v3.role +========================== + +.. automodule:: openstack.identity.v3.role + +The Role Class +-------------- + +The ``Role`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.identity.v3.role.Role + :members: diff --git a/doc/source/user/resources/identity/v3/role_assignment.rst b/doc/source/user/resources/identity/v3/role_assignment.rst new file mode 100644 index 000000000..abdfcdc9e --- /dev/null +++ b/doc/source/user/resources/identity/v3/role_assignment.rst @@ -0,0 +1,13 @@ +openstack.identity.v3.role_assignment +===================================== + +.. automodule:: openstack.identity.v3.role_assignment + +The RoleAssignment Class +------------------------ + +The ``RoleAssignment`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.identity.v3.role_assignment.RoleAssignment + :members: diff --git a/doc/source/user/resources/identity/v3/role_domain_group_assignment.rst b/doc/source/user/resources/identity/v3/role_domain_group_assignment.rst new file mode 100644 index 000000000..8ef6ef990 --- /dev/null +++ b/doc/source/user/resources/identity/v3/role_domain_group_assignment.rst @@ -0,0 +1,13 @@ +openstack.identity.v3.role_domain_group_assignment +================================================== + +.. automodule:: openstack.identity.v3.role_domain_group_assignment + +The RoleDomainGroupAssignment Class +----------------------------------- + +The ``RoleDomainGroupAssignment`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.identity.v3.role_domain_group_assignment.RoleDomainGroupAssignment + :members: diff --git a/doc/source/user/resources/identity/v3/role_domain_user_assignment.rst b/doc/source/user/resources/identity/v3/role_domain_user_assignment.rst new file mode 100644 index 000000000..063a3d7ec --- /dev/null +++ b/doc/source/user/resources/identity/v3/role_domain_user_assignment.rst @@ -0,0 +1,13 @@ +openstack.identity.v3.role_domain_user_assignment +================================================= + +.. automodule:: openstack.identity.v3.role_domain_user_assignment + +The RoleDomainUserAssignment Class +---------------------------------- + +The ``RoleDomainUserAssignment`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.identity.v3.role_domain_user_assignment.RoleDomainUserAssignment + :members: diff --git a/doc/source/user/resources/identity/v3/role_project_group_assignment.rst b/doc/source/user/resources/identity/v3/role_project_group_assignment.rst new file mode 100644 index 000000000..2824e0c5a --- /dev/null +++ b/doc/source/user/resources/identity/v3/role_project_group_assignment.rst @@ -0,0 +1,13 @@ +openstack.identity.v3.role_project_group_assignment +=================================================== + +.. automodule:: openstack.identity.v3.role_project_group_assignment + +The RoleProjectGroupAssignment Class +------------------------------------ + +The ``RoleProjectGroupAssignment`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.identity.v3.role_project_group_assignment.RoleProjectGroupAssignment + :members: diff --git a/doc/source/user/resources/identity/v3/role_project_user_assignment.rst b/doc/source/user/resources/identity/v3/role_project_user_assignment.rst new file mode 100644 index 000000000..d9f44c963 --- /dev/null +++ b/doc/source/user/resources/identity/v3/role_project_user_assignment.rst @@ -0,0 +1,13 @@ +openstack.identity.v3.role_project_user_assignment +================================================== + +.. automodule:: openstack.identity.v3.role_project_user_assignment + +The RoleProjectUserAssignment Class +----------------------------------- + +The ``RoleProjectUserAssignment`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.identity.v3.role_project_user_assignment.RoleProjectUserAssignment + :members: diff --git a/doc/source/user/resources/identity/v3/role_system_group_assignment.rst b/doc/source/user/resources/identity/v3/role_system_group_assignment.rst new file mode 100644 index 000000000..5e9771c93 --- /dev/null +++ b/doc/source/user/resources/identity/v3/role_system_group_assignment.rst @@ -0,0 +1,13 @@ +openstack.identity.v3.role_system_group_assignment +================================================== + +.. automodule:: openstack.identity.v3.role_system_group_assignment + +The RoleSystemGroupAssignment Class +----------------------------------- + +The ``RoleSystemGroupAssignment`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.identity.v3.role_system_group_assignment.RoleSystemGroupAssignment + :members: diff --git a/doc/source/user/resources/identity/v3/role_system_user_assignment.rst b/doc/source/user/resources/identity/v3/role_system_user_assignment.rst new file mode 100644 index 000000000..7346d0058 --- /dev/null +++ b/doc/source/user/resources/identity/v3/role_system_user_assignment.rst @@ -0,0 +1,13 @@ +openstack.identity.v3.role_system_user_assignment +================================================= + +.. automodule:: openstack.identity.v3.role_system_user_assignment + +The RoleSystemUserAssignment Class +---------------------------------- + +The ``RoleSystemUserAssignment`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.identity.v3.role_system_user_assignment.RoleSystemUserAssignment + :members: diff --git a/doc/source/user/resources/identity/v3/system.rst b/doc/source/user/resources/identity/v3/system.rst new file mode 100644 index 000000000..dcdda5be6 --- /dev/null +++ b/doc/source/user/resources/identity/v3/system.rst @@ -0,0 +1,12 @@ +openstack.identity.v3.system +============================ + +.. automodule:: openstack.identity.v3.system + +The System Class +---------------- + +The ``System`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.identity.v3.system.System + :members: diff --git a/doc/source/user/resources/identity/version.rst b/doc/source/user/resources/identity/version.rst new file mode 100644 index 000000000..1fe3bfb74 --- /dev/null +++ b/doc/source/user/resources/identity/version.rst @@ -0,0 +1,12 @@ +openstack.identity.version +========================== + +.. automodule:: openstack.identity.version + +The Version Class +----------------- + +The ``Version`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.identity.version.Version + :members: From d06ac50e7d2a744b5b1c5f1771cae9e13189e04f Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 16 Aug 2023 18:41:23 +0100 Subject: [PATCH 3362/3836] docs: Add missing docs for compute resources Change-Id: I34dd2cd97451b04da7f4db7d6868572aafdeec70 Signed-off-by: Stephen Finucane --- doc/source/user/resources/compute/index.rst | 15 +++---------- .../user/resources/compute/v2/aggregate.rst | 12 ++++++++++ .../compute/v2/availability_zone.rst | 13 +++++++++++ .../resources/compute/v2/server_action.rst | 22 +++++++++++++++++++ .../compute/v2/server_diagnostics.rst | 13 +++++++++++ .../resources/compute/v2/server_group.rst | 13 +++++++++++ .../compute/v2/server_remote_console.rst | 13 +++++++++++ .../user/resources/compute/v2/service.rst | 12 ++++++++++ .../user/resources/compute/v2/usage.rst | 20 +++++++++++++++++ .../compute/v2/volume_attachment.rst | 13 +++++++++++ doc/source/user/resources/compute/version.rst | 12 ++++++++++ 11 files changed, 146 insertions(+), 12 deletions(-) create mode 100644 doc/source/user/resources/compute/v2/aggregate.rst create mode 100644 doc/source/user/resources/compute/v2/availability_zone.rst create mode 100644 doc/source/user/resources/compute/v2/server_action.rst create mode 100644 doc/source/user/resources/compute/v2/server_diagnostics.rst create mode 100644 doc/source/user/resources/compute/v2/server_group.rst create mode 100644 doc/source/user/resources/compute/v2/server_remote_console.rst create mode 100644 doc/source/user/resources/compute/v2/service.rst create mode 100644 doc/source/user/resources/compute/v2/usage.rst create mode 100644 doc/source/user/resources/compute/v2/volume_attachment.rst create mode 100644 doc/source/user/resources/compute/version.rst diff --git a/doc/source/user/resources/compute/index.rst b/doc/source/user/resources/compute/index.rst index 993cff52d..b982b0e67 100644 --- a/doc/source/user/resources/compute/index.rst +++ b/doc/source/user/resources/compute/index.rst @@ -3,16 +3,7 @@ Compute Resources .. toctree:: :maxdepth: 1 + :glob: - v2/extension - v2/flavor - v2/image - v2/keypair - v2/limits - v2/migration - v2/server - v2/server_interface - v2/server_migration - v2/server_ip - v2/hypervisor - v2/quota_set + v2/* + version diff --git a/doc/source/user/resources/compute/v2/aggregate.rst b/doc/source/user/resources/compute/v2/aggregate.rst new file mode 100644 index 000000000..0d786887f --- /dev/null +++ b/doc/source/user/resources/compute/v2/aggregate.rst @@ -0,0 +1,12 @@ +openstack.compute.v2.aggregate +============================== + +.. automodule:: openstack.compute.v2.aggregate + +The Aggregate Class +------------------- + +The ``Aggregate`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.compute.v2.aggregate.Aggregate + :members: diff --git a/doc/source/user/resources/compute/v2/availability_zone.rst b/doc/source/user/resources/compute/v2/availability_zone.rst new file mode 100644 index 000000000..8d58bb732 --- /dev/null +++ b/doc/source/user/resources/compute/v2/availability_zone.rst @@ -0,0 +1,13 @@ +openstack.compute.v2.availability_zone +====================================== + +.. automodule:: openstack.compute.v2.availability_zone + +The AvailabilityZone Class +-------------------------- + +The ``AvailabilityZone`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.compute.v2.availability_zone.AvailabilityZone + :members: diff --git a/doc/source/user/resources/compute/v2/server_action.rst b/doc/source/user/resources/compute/v2/server_action.rst new file mode 100644 index 000000000..a3afefe89 --- /dev/null +++ b/doc/source/user/resources/compute/v2/server_action.rst @@ -0,0 +1,22 @@ +openstack.compute.v2.server_action +================================== + +.. automodule:: openstack.compute.v2.server_action + +The ServerAction Class +---------------------- + +The ``ServerAction`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.compute.v2.server_action.ServerAction + :members: + +The ServerActionEvent Class +--------------------------- + +The ``ServerActionEvent`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.compute.v2.server_action.ServerActionEvent + :members: diff --git a/doc/source/user/resources/compute/v2/server_diagnostics.rst b/doc/source/user/resources/compute/v2/server_diagnostics.rst new file mode 100644 index 000000000..7b00e93bf --- /dev/null +++ b/doc/source/user/resources/compute/v2/server_diagnostics.rst @@ -0,0 +1,13 @@ +openstack.compute.v2.server_diagnostics +======================================= + +.. automodule:: openstack.compute.v2.server_diagnostics + +The ServerDiagnostics Class +--------------------------- + +The ``ServerDiagnostics`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.compute.v2.server_diagnostics.ServerDiagnostics + :members: diff --git a/doc/source/user/resources/compute/v2/server_group.rst b/doc/source/user/resources/compute/v2/server_group.rst new file mode 100644 index 000000000..be84fe6da --- /dev/null +++ b/doc/source/user/resources/compute/v2/server_group.rst @@ -0,0 +1,13 @@ +openstack.compute.v2.server_group +================================= + +.. automodule:: openstack.compute.v2.server_group + +The ServerGroup Class +--------------------- + +The ``ServerGroup`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.compute.v2.server_group.ServerGroup + :members: diff --git a/doc/source/user/resources/compute/v2/server_remote_console.rst b/doc/source/user/resources/compute/v2/server_remote_console.rst new file mode 100644 index 000000000..9ce7c0851 --- /dev/null +++ b/doc/source/user/resources/compute/v2/server_remote_console.rst @@ -0,0 +1,13 @@ +openstack.compute.v2.server_remote_console +========================================== + +.. automodule:: openstack.compute.v2.server_remote_console + +The ServerRemoteConsole Class +----------------------------- + +The ``ServerRemoteConsole`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.compute.v2.server_remote_console.ServerRemoteConsole + :members: diff --git a/doc/source/user/resources/compute/v2/service.rst b/doc/source/user/resources/compute/v2/service.rst new file mode 100644 index 000000000..ac0fdef66 --- /dev/null +++ b/doc/source/user/resources/compute/v2/service.rst @@ -0,0 +1,12 @@ +openstack.compute.v2.service +============================ + +.. automodule:: openstack.compute.v2.service + +The Service Class +----------------- + +The ``Service`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.compute.v2.service.Service + :members: diff --git a/doc/source/user/resources/compute/v2/usage.rst b/doc/source/user/resources/compute/v2/usage.rst new file mode 100644 index 000000000..1be8f6be8 --- /dev/null +++ b/doc/source/user/resources/compute/v2/usage.rst @@ -0,0 +1,20 @@ +openstack.compute.v2.usage +========================== + +.. automodule:: openstack.compute.v2.usage + +The Usage Class +--------------- + +The ``Usage`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.compute.v2.usage.Usage + :members: + +The ServerUsage Class +--------------------- + +The ``ServerUsage`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.compute.v2.usage.ServerUsage + :members: diff --git a/doc/source/user/resources/compute/v2/volume_attachment.rst b/doc/source/user/resources/compute/v2/volume_attachment.rst new file mode 100644 index 000000000..74d4be3f5 --- /dev/null +++ b/doc/source/user/resources/compute/v2/volume_attachment.rst @@ -0,0 +1,13 @@ +openstack.compute.v2.volume_attachment +====================================== + +.. automodule:: openstack.compute.v2.volume_attachment + +The VolumeAttachment Class +-------------------------- + +The ``VolumeAttachment`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.compute.v2.volume_attachment.VolumeAttachment + :members: diff --git a/doc/source/user/resources/compute/version.rst b/doc/source/user/resources/compute/version.rst new file mode 100644 index 000000000..0191df12b --- /dev/null +++ b/doc/source/user/resources/compute/version.rst @@ -0,0 +1,12 @@ +openstack.compute.version +========================= + +.. automodule:: openstack.compute.version + +The Version Class +----------------- + +The ``Version`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.compute.version.Version + :members: From c0f2aee724a54a598a88dc2c00bbf85889755faa Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 17 Aug 2023 10:42:08 +0100 Subject: [PATCH 3363/3836] docs: Add missing docs for block storage resources Change-Id: I6f6c2b1cec5d4273d27fa0a109dbd49579686251 Signed-off-by: Stephen Finucane --- .../user/resources/block_storage/index.rst | 25 +++++++------ .../user/resources/block_storage/v2/stats.rst | 12 ++++++ .../block_storage/v3/availability_zone.rst | 13 +++++++ .../block_storage/v3/capabilities.rst | 12 ++++++ .../resources/block_storage/v3/extension.rst | 12 ++++++ .../user/resources/block_storage/v3/group.rst | 12 ++++++ .../block_storage/v3/group_snapshot.rst | 13 +++++++ .../resources/block_storage/v3/group_type.rst | 12 ++++++ .../resources/block_storage/v3/limits.rst | 37 +++++++++++++++++++ .../block_storage/v3/resource_filter.rst | 13 +++++++ .../user/resources/block_storage/v3/stats.rst | 12 ++++++ .../user/resources/block_storage/v3/type.rst | 8 ++++ 12 files changed, 169 insertions(+), 12 deletions(-) create mode 100644 doc/source/user/resources/block_storage/v2/stats.rst create mode 100644 doc/source/user/resources/block_storage/v3/availability_zone.rst create mode 100644 doc/source/user/resources/block_storage/v3/capabilities.rst create mode 100644 doc/source/user/resources/block_storage/v3/extension.rst create mode 100644 doc/source/user/resources/block_storage/v3/group.rst create mode 100644 doc/source/user/resources/block_storage/v3/group_snapshot.rst create mode 100644 doc/source/user/resources/block_storage/v3/group_type.rst create mode 100644 doc/source/user/resources/block_storage/v3/limits.rst create mode 100644 doc/source/user/resources/block_storage/v3/resource_filter.rst create mode 100644 doc/source/user/resources/block_storage/v3/stats.rst diff --git a/doc/source/user/resources/block_storage/index.rst b/doc/source/user/resources/block_storage/index.rst index 92a22ae22..18c2c98c7 100644 --- a/doc/source/user/resources/block_storage/index.rst +++ b/doc/source/user/resources/block_storage/index.rst @@ -1,19 +1,20 @@ Block Storage Resources ======================= +Block Storage v2 Resources +-------------------------- + .. toctree:: :maxdepth: 1 + :glob: + + v2/* + +Block Storage v3 Resources +-------------------------- - v2/backup - v2/quota_set - v2/snapshot - v2/type - v2/volume +.. toctree:: + :maxdepth: 1 + :glob: - v3/backup - v3/quota_set - v3/snapshot - v3/type - v3/volume - v3/service - v3/block_storage_summary + v3/* diff --git a/doc/source/user/resources/block_storage/v2/stats.rst b/doc/source/user/resources/block_storage/v2/stats.rst new file mode 100644 index 000000000..78717e202 --- /dev/null +++ b/doc/source/user/resources/block_storage/v2/stats.rst @@ -0,0 +1,12 @@ +openstack.block_storage.v2.stats +================================ + +.. automodule:: openstack.block_storage.v2.stats + +The Pools Class +--------------- + +The ``Pools`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v2.stats.Pools + :members: diff --git a/doc/source/user/resources/block_storage/v3/availability_zone.rst b/doc/source/user/resources/block_storage/v3/availability_zone.rst new file mode 100644 index 000000000..83e8f0b2e --- /dev/null +++ b/doc/source/user/resources/block_storage/v3/availability_zone.rst @@ -0,0 +1,13 @@ +openstack.block_storage.v3.availability_zone +============================================ + +.. automodule:: openstack.block_storage.v3.availability_zone + +The AvailabilityZone Class +-------------------------- + +The ``AvailabilityZone`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v3.availability_zone.AvailabilityZone + :members: diff --git a/doc/source/user/resources/block_storage/v3/capabilities.rst b/doc/source/user/resources/block_storage/v3/capabilities.rst new file mode 100644 index 000000000..9f4c79f14 --- /dev/null +++ b/doc/source/user/resources/block_storage/v3/capabilities.rst @@ -0,0 +1,12 @@ +openstack.block_storage.v3.capabilities +======================================= + +.. automodule:: openstack.block_storage.v3.capabilities + +The Capabilities Class +---------------------- + +The ``Capabilities`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v3.capabilities.Capabilities + :members: diff --git a/doc/source/user/resources/block_storage/v3/extension.rst b/doc/source/user/resources/block_storage/v3/extension.rst new file mode 100644 index 000000000..c72ec408a --- /dev/null +++ b/doc/source/user/resources/block_storage/v3/extension.rst @@ -0,0 +1,12 @@ +openstack.block_storage.v3.extension +==================================== + +.. automodule:: openstack.block_storage.v3.extension + +The Extension Class +------------------- + +The ``Extension`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v3.extension.Extension + :members: diff --git a/doc/source/user/resources/block_storage/v3/group.rst b/doc/source/user/resources/block_storage/v3/group.rst new file mode 100644 index 000000000..3d1d6abc0 --- /dev/null +++ b/doc/source/user/resources/block_storage/v3/group.rst @@ -0,0 +1,12 @@ +openstack.block_storage.v3.group +================================ + +.. automodule:: openstack.block_storage.v3.group + +The Group Class +--------------- + +The ``Group`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v3.group.Group + :members: diff --git a/doc/source/user/resources/block_storage/v3/group_snapshot.rst b/doc/source/user/resources/block_storage/v3/group_snapshot.rst new file mode 100644 index 000000000..f8135e3e1 --- /dev/null +++ b/doc/source/user/resources/block_storage/v3/group_snapshot.rst @@ -0,0 +1,13 @@ +openstack.block_storage.v3.group_snapshot +========================================= + +.. automodule:: openstack.block_storage.v3.group_snapshot + +The GroupSnapshot Class +----------------------- + +The ``GroupSnapshot`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v3.group_snapshot.GroupSnapshot + :members: diff --git a/doc/source/user/resources/block_storage/v3/group_type.rst b/doc/source/user/resources/block_storage/v3/group_type.rst new file mode 100644 index 000000000..064b9d247 --- /dev/null +++ b/doc/source/user/resources/block_storage/v3/group_type.rst @@ -0,0 +1,12 @@ +openstack.block_storage.v3.group_type +===================================== + +.. automodule:: openstack.block_storage.v3.group_type + +The GroupType Class +------------------- + +The ``GroupType`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v3.group_type.GroupType + :members: diff --git a/doc/source/user/resources/block_storage/v3/limits.rst b/doc/source/user/resources/block_storage/v3/limits.rst new file mode 100644 index 000000000..69a7d7a31 --- /dev/null +++ b/doc/source/user/resources/block_storage/v3/limits.rst @@ -0,0 +1,37 @@ +openstack.block_storage.v3.limits +================================= + +.. automodule:: openstack.block_storage.v3.limits + +The AbsoluteLimit Class +----------------------- + +The ``AbsoluteLimit`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v3.limits.AbsoluteLimit + :members: + +The Limit Class +--------------- + +The ``Limit`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v3.limits.Limit + :members: + +The RateLimit Class +------------------- + +The ``RateLimit`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v3.limits.RateLimit + :members: + +The RateLimits Class +-------------------- + +The ``RateLimits`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v3.limits.RateLimits + :members: diff --git a/doc/source/user/resources/block_storage/v3/resource_filter.rst b/doc/source/user/resources/block_storage/v3/resource_filter.rst new file mode 100644 index 000000000..eaef7b3b4 --- /dev/null +++ b/doc/source/user/resources/block_storage/v3/resource_filter.rst @@ -0,0 +1,13 @@ +openstack.block_storage.v3.resource_filter +========================================== + +.. automodule:: openstack.block_storage.v3.resource_filter + +The ResourceFilter Class +------------------------ + +The ``ResourceFilter`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v3.resource_filter.ResourceFilter + :members: diff --git a/doc/source/user/resources/block_storage/v3/stats.rst b/doc/source/user/resources/block_storage/v3/stats.rst new file mode 100644 index 000000000..b8e802e62 --- /dev/null +++ b/doc/source/user/resources/block_storage/v3/stats.rst @@ -0,0 +1,12 @@ +openstack.block_storage.v3.stats +================================ + +.. automodule:: openstack.block_storage.v3.stats + +The Pools Class +--------------- + +The ``Pools`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v3.stats.Pools + :members: diff --git a/doc/source/user/resources/block_storage/v3/type.rst b/doc/source/user/resources/block_storage/v3/type.rst index 8cb7650c2..32ce79968 100644 --- a/doc/source/user/resources/block_storage/v3/type.rst +++ b/doc/source/user/resources/block_storage/v3/type.rst @@ -11,3 +11,11 @@ The ``Type`` class inherits from :class:`~openstack.resource.Resource`. .. autoclass:: openstack.block_storage.v3.type.Type :members: +The TypeEncryption Class +------------------------ + +The ``TypeEncryption`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v3.type.TypeEncryption + :members: From ac35b6ba644b04b7ffc8e07c701e408b37581ddd Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 16 Aug 2023 18:16:00 +0100 Subject: [PATCH 3364/3836] compute: Add missing docstrings Change-Id: I16e40b7ee481662f1e3eb26a55c07b602b7c77e1 Signed-off-by: Stephen Finucane --- openstack/compute/v2/flavor.py | 56 +++++-- openstack/compute/v2/server.py | 276 +++++++++++++++++++++++++++++++-- 2 files changed, 307 insertions(+), 25 deletions(-) diff --git a/openstack/compute/v2/flavor.py b/openstack/compute/v2/flavor.py index e9671df4f..dd0c39300 100644 --- a/openstack/compute/v2/flavor.py +++ b/openstack/compute/v2/flavor.py @@ -120,12 +120,22 @@ def _action(self, session, body, microversion=None): return response def add_tenant_access(self, session, tenant): - """Adds flavor access to a tenant and flavor.""" + """Adds flavor access to a tenant and flavor. + + :param session: The session to use for making this request. + :param tenant: + :returns: None + """ body = {'addTenantAccess': {'tenant': tenant}} self._action(session, body) def remove_tenant_access(self, session, tenant): - """Removes flavor access to a tenant and flavor.""" + """Removes flavor access to a tenant and flavor. + + :param session: The session to use for making this request. + :param tenant: + :returns: None + """ body = {'removeTenantAccess': {'tenant': tenant}} self._action(session, body) @@ -133,9 +143,10 @@ def get_access(self, session): """Lists tenants who have access to a private flavor By default, only administrators can manage private flavor access. A - private flavor has is_public set to false while a public flavor has - is_public set to true. + private flavor has ``is_public`` set to false while a public flavor has + ``is_public`` set to true. + :param session: The session to use for making this request. :return: List of dicts with flavor_id and tenant_id attributes """ url = utils.urljoin(Flavor.base_path, self.id, 'os-flavor-access') @@ -144,10 +155,13 @@ def get_access(self, session): return response.json().get('flavor_access', []) def fetch_extra_specs(self, session): - """Fetch extra_specs of the flavor + """Fetch extra specs of the flavor - Starting with 2.61 extra_specs are returned with the flavor details, + Starting with 2.61 extra specs are returned with the flavor details, before that a separate call is required. + + :param session: The session to use for making this request. + :returns: The updated flavor. """ url = utils.urljoin(Flavor.base_path, self.id, 'os-extra_specs') microversion = self._get_microversion(session, action='fetch') @@ -158,7 +172,12 @@ def fetch_extra_specs(self, session): return self def create_extra_specs(self, session, specs): - """Creates extra specs for a flavor""" + """Creates extra specs for a flavor. + + :param session: The session to use for making this request. + :param specs: + :returns: The updated flavor. + """ url = utils.urljoin(Flavor.base_path, self.id, 'os-extra_specs') microversion = self._get_microversion(session, action='create') response = session.post( @@ -170,7 +189,12 @@ def create_extra_specs(self, session, specs): return self def get_extra_specs_property(self, session, prop): - """Get individual extra_spec property""" + """Get an individual extra spec property. + + :param session: The session to use for making this request. + :param prop: The property to fetch. + :returns: The value of the property if it exists, else ``None``. + """ url = utils.urljoin(Flavor.base_path, self.id, 'os-extra_specs', prop) microversion = self._get_microversion(session, action='fetch') response = session.get(url, microversion=microversion) @@ -179,7 +203,13 @@ def get_extra_specs_property(self, session, prop): return val def update_extra_specs_property(self, session, prop, val): - """Update An Extra Spec For A Flavor""" + """Update an extra spec for a flavor. + + :param session: The session to use for making this request. + :param prop: The property to update. + :param val: The value to update with. + :returns: The updated value of the property. + """ url = utils.urljoin(Flavor.base_path, self.id, 'os-extra_specs', prop) microversion = self._get_microversion(session, action='commit') response = session.put( @@ -190,11 +220,17 @@ def update_extra_specs_property(self, session, prop, val): return val def delete_extra_specs_property(self, session, prop): - """Delete An Extra Spec For A Flavor""" + """Delete an extra spec for a flavor. + + :param session: The session to use for making this request. + :param prop: The property to delete. + :returns: None + """ url = utils.urljoin(Flavor.base_path, self.id, 'os-extra_specs', prop) microversion = self._get_microversion(session, action='delete') response = session.delete(url, microversion=microversion) exceptions.raise_from_response(response) +# TODO(stephenfin): Deprecate this for removal in 2.0 FlavorDetail = Flavor diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index fb501e8eb..6ea37c8a6 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -314,13 +314,22 @@ def _action(self, session, body, microversion=None): exceptions.raise_from_response(response) return response - def change_password(self, session, new_password): - """Change the administrator password to the given password.""" - body = {'changePassword': {'adminPass': new_password}} + def change_password(self, session, password): + """Change the administrator password to the given password. + + :param session: The session to use for making this request. + :param password: The new password. + :returns: None + """ + body = {'changePassword': {'adminPass': password}} self._action(session, body) def get_password(self, session): - """Get the encrypted administrator password.""" + """Get the encrypted administrator password. + + :param session: The session to use for making this request. + :returns: The encrypted administrator password. + """ url = utils.urljoin(Server.base_path, self.id, 'os-server-password') response = session.get(url) @@ -330,12 +339,21 @@ def get_password(self, session): return data.get('password') def reboot(self, session, reboot_type): - """Reboot server where reboot_type might be 'SOFT' or 'HARD'.""" + """Reboot server where reboot_type might be 'SOFT' or 'HARD'. + + :param session: The session to use for making this request. + :param reboot_type: The type of reboot. One of: ``SOFT``, ``HARD``. + :returns: None + """ body = {'reboot': {'type': reboot_type}} self._action(session, body) def force_delete(self, session): - """Force delete a server.""" + """Force delete the server. + + :param session: The session to use for making this request. + :returns: None + """ body = {'forceDelete': None} self._action(session, body) @@ -352,7 +370,25 @@ def rebuild( user_data=None, key_name=None, ): - """Rebuild the server with the given arguments.""" + """Rebuild the server with the given arguments. + + :param session: The session to use for making this request. + :param image: The image to rebuild to. Either an ID or a + :class:`~openstack.image.v1.image.Image` instance. + :param name: A name to set on the rebuilt server. (Optional) + :param admin_password: An admin password to set on the rebuilt server. + (Optional) + :param preserve_ephemeral: Whether to preserve the ephemeral drive + during the rebuild. (Optional) + :param access_ipv4: An IPv4 address that will be used to access the + rebuilt server. (Optional) + :param access_ipv6: An IPv6 address that will be used to access the + rebuilt server. (Optional) + :param metadata: Metadata to set on the updated server. (Optional) + :param user_data: User data to set on the updated server. (Optional) + :param key_name: A key name to set on the updated server. (Optional) + :returns: The updated server. + """ action = {'imageRef': resource.Resource._get_id(image)} if preserve_ephemeral is not None: action['preserve_ephemeral'] = preserve_ephemeral @@ -377,22 +413,41 @@ def rebuild( return self def resize(self, session, flavor): - """Resize server to flavor reference.""" + """Resize server to flavor reference. + + :param session: The session to use for making this request. + :param flavor: The server to resize to. + :returns: None + """ body = {'resize': {'flavorRef': flavor}} self._action(session, body) def confirm_resize(self, session): - """Confirm the resize of the server.""" + """Confirm the resize of the server. + + :param session: The session to use for making this request. + :returns: None + """ body = {'confirmResize': None} self._action(session, body) def revert_resize(self, session): - """Revert the resize of the server.""" + """Revert the resize of the server. + + :param session: The session to use for making this request. + :returns: None + """ body = {'revertResize': None} self._action(session, body) def create_image(self, session, name, metadata=None): - """Create image from server.""" + """Create image from server. + + :param session: The session to use for making this request. + :param name: The name to use for the created image. + :param metadata: Metadata to set on the created image. (Optional) + :returns: None + """ action = {'name': name} if metadata is not None: action['metadata'] = metadata @@ -432,36 +487,100 @@ def create_image(self, session, name, metadata=None): return image_id def add_security_group(self, session, security_group_name): + """Add a security group to the server. + + :param session: The session to use for making this request. + :param security_group_name: The security group to add to the server. + :returns: None + """ body = {"addSecurityGroup": {"name": security_group_name}} self._action(session, body) def remove_security_group(self, session, security_group_name): + """Remove a security group from the server. + + :param session: The session to use for making this request. + :param security_group_name: The security group to remove from the + server. + :returns: None + """ body = {"removeSecurityGroup": {"name": security_group_name}} self._action(session, body) def reset_state(self, session, state): + """Reset server state. + + This is admin-only by default. + + :param session: The session to use for making this request. + :param state: The state to set on the server. + :returns: None + """ body = {"os-resetState": {"state": state}} self._action(session, body) def add_fixed_ip(self, session, network_id): + """Add a fixed IP to the server. + + This is effectively an alias for adding a network. + + :param session: The session to use for making this request. + :param network_id: The network to connect the server to. + :returns: None + """ body = {"addFixedIp": {"networkId": network_id}} self._action(session, body) def remove_fixed_ip(self, session, address): + """Remove a fixed IP from the server. + + This is effectively an alias from removing a port from the server. + + :param session: The session to use for making this request. + :param network_id: The address to remove from the server. + :returns: None + """ body = {"removeFixedIp": {"address": address}} self._action(session, body) def add_floating_ip(self, session, address, fixed_address=None): + """Add a floating IP to the server. + + :param session: The session to use for making this request. + :param address: The floating IP address to associate with the server. + :param fixed_address: A fixed IP address with which to associated the + floating IP. (Optional) + :returns: None + """ body = {"addFloatingIp": {"address": address}} if fixed_address is not None: body['addFloatingIp']['fixed_address'] = fixed_address self._action(session, body) def remove_floating_ip(self, session, address): + """Remove a floating IP from the server. + + :param session: The session to use for making this request. + :param address: The floating IP address to disassociate from the + server. + :returns: None + """ body = {"removeFloatingIp": {"address": address}} self._action(session, body) def backup(self, session, name, backup_type, rotation): + """Create a backup of the server. + + :param session: The session to use for making this request. + :param name: The name to use for the backup image. + :param backup_type: The type of backup. The value and meaning of this + atribute is user-defined and can be used to separate backups of + different types. For example, this could be used to distinguish + between ``daily`` and ``weekly`` backups. + :param rotation: The number of backups to retain. All images older than + the rotation'th image will be deleted. + :returns: None + """ body = { "createBackup": { "name": name, @@ -472,22 +591,48 @@ def backup(self, session, name, backup_type, rotation): self._action(session, body) def pause(self, session): + """Pause the server. + + :param session: The session to use for making this request. + :returns: None + """ body = {"pause": None} self._action(session, body) def unpause(self, session): + """Unpause the server. + + :param session: The session to use for making this request. + :returns: None + """ body = {"unpause": None} self._action(session, body) def suspend(self, session): + """Suspend the server. + + :param session: The session to use for making this request. + :returns: None + """ body = {"suspend": None} self._action(session, body) def resume(self, session): + """Resume the server. + + :param session: The session to use for making this request. + :returns: None + """ body = {"resume": None} self._action(session, body) def lock(self, session, locked_reason=None): + """Lock the server. + + :param session: The session to use for making this request. + :param locked_reason: The reason for locking the server. + :returns: None + """ body = {"lock": None} if locked_reason is not None: body["lock"] = { @@ -496,10 +641,26 @@ def lock(self, session, locked_reason=None): self._action(session, body) def unlock(self, session): + """Unlock the server. + + :param session: The session to use for making this request. + :returns: None + """ body = {"unlock": None} self._action(session, body) def rescue(self, session, admin_pass=None, image_ref=None): + """Rescue the server. + + This is admin-only by default. + + :param session: The session to use for making this request. + :param admin_pass: A new admin password to set on the rescued server. + (Optional) + :param image_ref: The image to use when rescuing the server. If not + provided, the server will use the existing image. (Optional) + :returns: None + """ body = {"rescue": {}} if admin_pass is not None: body["rescue"]["adminPass"] = admin_pass @@ -508,10 +669,26 @@ def rescue(self, session, admin_pass=None, image_ref=None): self._action(session, body) def unrescue(self, session): + """Unrescue the server. + + This is admin-only by default. + + :param session: The session to use for making this request. + :returns: None + """ body = {"unrescue": None} self._action(session, body) def evacuate(self, session, host=None, admin_pass=None, force=None): + """Evacuate the server. + + :param session: The session to use for making this request. + :param host: The host to evacuate the instance to. (Optional) + :param admin_pass: The admin password to set on the evacuated instance. + (Optional) + :param force: Whether to force evacuation. + :returns: None + """ body = {"evacuate": {}} if host is not None: body["evacuate"]["host"] = host @@ -522,29 +699,57 @@ def evacuate(self, session, host=None, admin_pass=None, force=None): self._action(session, body) def start(self, session): + """Start the server. + + :param session: The session to use for making this request. + :returns: None + """ body = {"os-start": None} self._action(session, body) def stop(self, session): + """Stop the server. + + :param session: The session to use for making this request. + :returns: None + """ body = {"os-stop": None} self._action(session, body) def restore(self, session): + """Restore the server. + + This is only supported if the server is soft-deleted. This is + cloud-specific. + + :param session: The session to use for making this request. + :returns: None + """ body = {"restore": None} self._action(session, body) def shelve(self, session): + """Shelve the server. + + :param session: The session to use for making this request. + :returns: None + """ body = {"shelve": None} self._action(session, body) def shelve_offload(self, session): + """Shelve-offload the server. + + :param session: The session to use for making this request. + :returns: None + """ body = {"shelveOffload": None} self._action(session, body) def unshelve(self, session, availability_zone=_sentinel, host=None): - """ - Unshelve -- Unshelve the server. + """Unshelve the server. + :param session: The session to use for making this request. :param availability_zone: If specified the instance will be unshelved to the availability_zone. If None is passed the instance defined availability_zone is unpin @@ -564,14 +769,31 @@ def unshelve(self, session, availability_zone=_sentinel, host=None): self._action(session, body) def migrate(self, session): + """Migrate the server. + + :param session: The session to use for making this request. + :returns: None + """ body = {"migrate": None} self._action(session, body) def trigger_crash_dump(self, session): + """Trigger a crash dump for the server. + + :param session: The session to use for making this request. + :returns: None + """ body = {"trigger_crash_dump": None} self._action(session, body) def get_console_output(self, session, length=None): + """Get console output for the server. + + :param session: The session to use for making this request. + :param length: The max length of the console output to return. + (Optional) + :returns: None + """ body = {"os-getConsoleOutput": {}} if length is not None: body["os-getConsoleOutput"]["length"] = length @@ -579,6 +801,14 @@ def get_console_output(self, session, length=None): return resp.json() def get_console_url(self, session, console_type): + """Get the console URL for the server. + + :param session: The session to use for making this request. + :param console_type: The type of console to return. This is + cloud-specific. One of: ``novnc``, ``xvpvnc``, ``spice-html5``, + ``rdp-html5``, ``serial``. + :returns: None + """ action = CONSOLE_TYPE_ACTION_MAPPING.get(console_type) if not action: raise ValueError("Unsupported console type %s" % console_type) @@ -594,6 +824,17 @@ def live_migrate( block_migration, disk_over_commit=False, ): + """Live migrate the server. + + :param session: The session to use for making this request. + :param host: The host to live migrate the server to. (Optional) + :param force: Whether to force the migration. (Optional) + :param block_migration: Whether to do block migration. One of: + ``True``, ``False``, ``'auto'``. (Optional) + :param disk_over_commit: Whether to allow disk over-commit on the + destination host. (Optional) + :returns: None + """ if utils.supports_microversion(session, '2.30'): return self._live_migrate_30( session, @@ -693,6 +934,11 @@ def _live_migrate( ) def fetch_topology(self, session): + """Fetch the topology information for the server. + + :param session: The session to use for making this request. + :returns: None + """ utils.require_microversion(session, 2.78) url = utils.urljoin(Server.base_path, self.id, 'topology') @@ -707,10 +953,10 @@ def fetch_topology(self, session): pass def fetch_security_groups(self, session): - """Fetch security groups of a server. + """Fetch security groups of the server. + :param session: The session to use for making this request. :returns: Updated Server instance. - """ url = utils.urljoin(Server.base_path, self.id, 'os-security-groups') From dca643988656ae059ffc58785459f1e5b746f84d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 16 Aug 2023 15:08:10 +0100 Subject: [PATCH 3365/3836] block storage: Add missing docstrings Change-Id: I7cc660d6146b83d70241e7aad8851d1bb7324444 Signed-off-by: Stephen Finucane --- openstack/block_storage/v2/type.py | 15 ++++++++++++++ openstack/block_storage/v3/type.py | 33 ++++++++++++++++++++++-------- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/openstack/block_storage/v2/type.py b/openstack/block_storage/v2/type.py index 9a4772e08..4b55d7fe7 100644 --- a/openstack/block_storage/v2/type.py +++ b/openstack/block_storage/v2/type.py @@ -35,6 +35,11 @@ class Type(resource.Resource): is_public = resource.Body('os-volume-type-access:is_public', type=bool) def get_private_access(self, session): + """List projects with private access to the volume type. + + :param session: The session to use for making this request. + :returns: The volume type access response. + """ url = utils.urljoin(self.base_path, self.id, "os-volume-type-access") resp = session.get(url) @@ -43,6 +48,11 @@ def get_private_access(self, session): return resp.json().get("volume_type_access", []) def add_private_access(self, session, project_id): + """Add project access from the volume type. + + :param session: The session to use for making this request. + :param project_id: The project to add access for. + """ url = utils.urljoin(self.base_path, self.id, "action") body = {"addProjectAccess": {"project": project_id}} @@ -51,6 +61,11 @@ def add_private_access(self, session, project_id): exceptions.raise_from_response(resp) def remove_private_access(self, session, project_id): + """Remove project access from the volume type. + + :param session: The session to use for making this request. + :param project_id: The project to remove access for. + """ url = utils.urljoin(self.base_path, self.id, "action") body = {"removeProjectAccess": {"project": project_id}} diff --git a/openstack/block_storage/v3/type.py b/openstack/block_storage/v3/type.py index 77ed0bd68..c804f4fe9 100644 --- a/openstack/block_storage/v3/type.py +++ b/openstack/block_storage/v3/type.py @@ -62,14 +62,15 @@ def _extra_specs(self, method, key=None, delete=False, extra_specs=None): return response.json() if not delete else None def set_extra_specs(self, session, **extra_specs): - """Update extra_specs + """Update extra specs. This call will replace only the extra_specs with the same keys given here. Other keys will not be modified. - :param session: The session to use for this request. - :param kwargs extra_specs: key/value extra_specs pairs to be update on - this volume type. All keys and values + :param session: The session to use for making this request. + :param kwargs extra_specs: Key/value extra_specs pairs to be update on + this volume type. All keys and values. + :returns: The updated extra specs. """ if not extra_specs: return dict() @@ -78,19 +79,25 @@ def set_extra_specs(self, session, **extra_specs): return result["extra_specs"] def delete_extra_specs(self, session, keys): - """Delete extra_specs + """Delete extra specs. + + .. note:: - Note: This method will do a HTTP DELETE request for every key in keys. + This method will do a HTTP DELETE request for every key in keys. :param session: The session to use for this request. :param list keys: The keys to delete. - - :rtype: ``None`` + :returns: ``None`` """ for key in keys: self._extra_specs(session.delete, key=key, delete=True) def get_private_access(self, session): + """List projects with private access to the volume type. + + :param session: The session to use for making this request. + :returns: The volume type access response. + """ url = utils.urljoin(self.base_path, self.id, "os-volume-type-access") resp = session.get(url) @@ -99,6 +106,11 @@ def get_private_access(self, session): return resp.json().get("volume_type_access", []) def add_private_access(self, session, project_id): + """Add project access from the volume type. + + :param session: The session to use for making this request. + :param project_id: The project to add access for. + """ url = utils.urljoin(self.base_path, self.id, "action") body = {"addProjectAccess": {"project": project_id}} @@ -107,6 +119,11 @@ def add_private_access(self, session, project_id): exceptions.raise_from_response(resp) def remove_private_access(self, session, project_id): + """Remove project access from the volume type. + + :param session: The session to use for making this request. + :param project_id: The project to remove access for. + """ url = utils.urljoin(self.base_path, self.id, "action") body = {"removeProjectAccess": {"project": project_id}} From a5a5e244292ec42f7dc9a69c319df008cd09b7de Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 17 Aug 2023 11:15:48 +0100 Subject: [PATCH 3366/3836] baremetal: Add missing docstrings Change-Id: I5902cb7f2a772a82f727bcbf8be04dee590f769b Signed-off-by: Stephen Finucane --- openstack/baremetal/v1/node.py | 44 ++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 18737d8bb..69bc2a767 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -886,7 +886,7 @@ def list_vifs(self, session): return [vif['id'] for vif in response.json()['vifs']] def validate(self, session, required=('boot', 'deploy', 'power')): - """Validate required information on a node. + """Validate required information on the node. :param session: The session to use for making this request. :type session: :class:`~keystoneauth1.adapter.Adapter` @@ -970,6 +970,11 @@ def _do_maintenance_action(self, session, verb, body=None): exceptions.raise_from_response(response, error_message=msg) def get_boot_device(self, session): + """Get node boot device. + + :param session: The session to use for making this request. + :returns: The HTTP response. + """ session = self._get_session(session) version = self._get_microversion(session, action='fetch') request = self._prepare_request(requires_id=True) @@ -996,6 +1001,7 @@ def set_boot_device(self, session, boot_device, persistent=False): :param boot_device: Boot device to assign to the node. :param persistent: If the boot device change is maintained after node reboot + :returns: ``None`` """ session = self._get_session(session) version = self._get_microversion(session, action='commit') @@ -1016,6 +1022,11 @@ def set_boot_device(self, session, boot_device, persistent=False): exceptions.raise_from_response(response, error_message=msg) def get_supported_boot_devices(self, session): + """Get supported boot devices for the node. + + :param session: The session to use for making this request. + :returns: The HTTP response. + """ session = self._get_session(session) version = self._get_microversion(session, action='fetch') request = self._prepare_request(requires_id=True) @@ -1048,6 +1059,7 @@ def set_boot_mode(self, session, target): :param session: The session to use for making this request. :param target: Boot mode to set for node, one of either 'uefi'/'bios'. + :returns: ``None`` :raises: ValueError if ``target`` is not one of 'uefi or 'bios'. """ session = self._get_session(session) @@ -1083,6 +1095,7 @@ def set_secure_boot(self, session, target): :param session: The session to use for making this request. :param bool target: Boolean indicating secure boot state to set. True/False corresponding to 'on'/'off' respectively. + :returns: ``None`` :raises: ValueError if ``target`` is not boolean. """ session = self._get_session(session) @@ -1112,10 +1125,11 @@ def set_secure_boot(self, session, target): exceptions.raise_from_response(response, error_message=msg) def add_trait(self, session, trait): - """Add a trait to a node. + """Add a trait to the node. :param session: The session to use for making this request. :param trait: The trait to add to the node. + :returns: ``None`` """ session = self._get_session(session) version = utils.pick_microversion(session, '1.37') @@ -1137,7 +1151,7 @@ def add_trait(self, session, trait): self.traits = list(set(self.traits or ()) | {trait}) def remove_trait(self, session, trait, ignore_missing=True): - """Remove a trait from a node. + """Remove a trait from the node. :param session: The session to use for making this request. :param trait: The trait to remove from the node. @@ -1179,13 +1193,14 @@ def remove_trait(self, session, trait, ignore_missing=True): return True def set_traits(self, session, traits): - """Set traits for a node. + """Set traits for the node. Removes any existing traits and adds the traits passed in to this method. :param session: The session to use for making this request. :param traits: list of traits to add to the node. + :returns: ``None`` """ session = self._get_session(session) version = utils.pick_microversion(session, '1.37') @@ -1243,7 +1258,7 @@ def call_vendor_passthru(self, session, verb, method, body=None): return response def list_vendor_passthru(self, session): - """List vendor passthru methods. + """List vendor passthru methods for the node. :param session: The session to use for making this request. :returns: The HTTP response. @@ -1268,6 +1283,11 @@ def list_vendor_passthru(self, session): return response.json() def get_console(self, session): + """Get the node console. + + :param session: The session to use for making this request. + :returns: The HTTP response. + """ session = self._get_session(session) version = self._get_microversion(session, action='fetch') request = self._prepare_request(requires_id=True) @@ -1288,6 +1308,12 @@ def get_console(self, session): return response.json() def set_console_mode(self, session, enabled): + """Set the node console mode. + + :param session: The session to use for making this request. + :param enabled: Whether the console should be enabled or not. + :return: ``None`` + """ session = self._get_session(session) version = self._get_microversion(session, action='commit') request = self._prepare_request(requires_id=True) @@ -1312,7 +1338,15 @@ def set_console_mode(self, session, enabled): ) exceptions.raise_from_response(response, error_message=msg) + # TODO(stephenfin): Drop 'node_id' and use 'self.id' instead or convert to + # a classmethod def get_node_inventory(self, session, node_id): + """Get a node's inventory. + + :param session: The session to use for making this request. + :param node_id: The ID of the node. + :returns: The HTTP response. + """ session = self._get_session(session) version = self._get_microversion(session, action='fetch') request = self._prepare_request(requires_id=True) From 7349594757577284cdf01079560b24a8abe1dd1b Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 17 Aug 2023 11:00:32 +0100 Subject: [PATCH 3367/3836] docs: Document various warnings Add a ':members:' option to ensure we actually list the available warnings, as was the original intent. Change-Id: Ia7cf763e0a4f2aa345b0f41b6a096a05512dbcaf Signed-off-by: Stephen Finucane --- doc/source/user/warnings.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/user/warnings.rst b/doc/source/user/warnings.rst index e1c0afb31..34d1893fe 100644 --- a/doc/source/user/warnings.rst +++ b/doc/source/user/warnings.rst @@ -15,5 +15,6 @@ Available warnings ------------------ .. automodule:: openstack.warnings + :members: .. __: https://docs.python.org/3/library/warnings.html From d84495279d1bc88d7221e22902513fe48625bdbc Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 17 Aug 2023 11:01:13 +0100 Subject: [PATCH 3368/3836] docs: Add exception documentation Provide a list of available exceptions and stress that callers must handle these themselves. Change-Id: If3f56524a4642e0e147305758a8c5a4cc88c94bd Signed-off-by: Stephen Finucane --- doc/source/user/exceptions.rst | 15 +++++++++++++++ doc/source/user/index.rst | 13 +++++++++++++ openstack/exceptions.py | 22 +++------------------- 3 files changed, 31 insertions(+), 19 deletions(-) create mode 100644 doc/source/user/exceptions.rst diff --git a/doc/source/user/exceptions.rst b/doc/source/user/exceptions.rst new file mode 100644 index 000000000..3e85f6f98 --- /dev/null +++ b/doc/source/user/exceptions.rst @@ -0,0 +1,15 @@ +Exceptions +========== + +openstacksdk provides a number of `exceptions`__ for commonly encountered +issues, such as missing API endpoints, various HTTP error codes, timeouts and +so forth. It is the responsibility of the calling application to handle these +exceptions appropriately. + +Available exceptions +-------------------- + +.. automodule:: openstack.exceptions + :members: + +.. __: https://docs.python.org/3/library/exceptions.html diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index f9e49fcf6..7522e7116 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -171,6 +171,19 @@ can be customized. resource service_description utils + +Errors and warnings +~~~~~~~~~~~~~~~~~~~ + +The SDK attempts to provide detailed errors and warnings for things like failed +requests, deprecated APIs, and invalid configurations. Application developers +are responsible for handling these errors and can opt into warnings to ensure +their applications stay up-to-date. + +.. toctree:: + :maxdepth: 1 + + exceptions warnings Presentations diff --git a/openstack/exceptions.py b/openstack/exceptions.py index 93a4b9d06..8c14f9d0b 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -57,6 +57,8 @@ def __init__(self, message=None): class HttpException(SDKException, _rex.HTTPError): + """The base exception for all HTTP error responses.""" + def __init__( self, message='Error', @@ -119,26 +121,18 @@ def __str__(self): class BadRequestException(HttpException): """HTTP 400 Bad Request.""" - pass - class ForbiddenException(HttpException): """HTTP 403 Forbidden Request.""" - pass - class ConflictException(HttpException): """HTTP 409 Conflict.""" - pass - class PreconditionFailedException(HttpException): """HTTP 412 Precondition Failed.""" - pass - class MethodNotSupported(SDKException): """The resource does not support this operation type.""" @@ -161,13 +155,9 @@ def __init__(self, resource, method): class DuplicateResource(SDKException): """More than one resource exists with that name.""" - pass - class ResourceNotFound(HttpException): - """No resource exists with that name or id.""" - - pass + """No resource exists with that name or ID.""" NotFoundException = ResourceNotFound @@ -176,20 +166,14 @@ class ResourceNotFound(HttpException): class ResourceTimeout(SDKException): """Timeout waiting for resource.""" - pass - class ResourceFailure(SDKException): """General resource failure.""" - pass - class InvalidResourceQuery(SDKException): """Invalid query params for resource.""" - pass - def _extract_message(obj): if isinstance(obj, dict): From 9958574631291ca97c13bbcd2a3d9f17b6755f60 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 28 Aug 2023 16:15:25 +0100 Subject: [PATCH 3369/3836] block storage: Add query string params for many APIs There are a lot more to be added and the Cinder API docs are extremely lacking here, but this gets the basics down. Change-Id: I30083dec46235f1c4f233235bb30ecfea0129ee3 Signed-off-by: Stephen Finucane --- openstack/block_storage/v3/backup.py | 20 ++++++++++--------- openstack/block_storage/v3/group.py | 10 ++++++++++ openstack/block_storage/v3/group_snapshot.py | 10 ++++++++++ openstack/block_storage/v3/resource_filter.py | 5 ++++- openstack/block_storage/v3/snapshot.py | 12 ++++++++++- openstack/block_storage/v3/type.py | 11 +++++++++- .../unit/block_storage/v3/test_backup.py | 10 ++++++---- .../block_storage/v3/test_resource_filter.py | 4 +++- .../unit/block_storage/v3/test_snapshot.py | 8 ++++++-- 9 files changed, 71 insertions(+), 19 deletions(-) diff --git a/openstack/block_storage/v3/backup.py b/openstack/block_storage/v3/backup.py index a5c5bae56..a74688c6a 100644 --- a/openstack/block_storage/v3/backup.py +++ b/openstack/block_storage/v3/backup.py @@ -25,15 +25,17 @@ class Backup(resource.Resource): # search (name~, status~, volume_id~). But this is not documented # officially and seem to require microversion be set _query_mapping = resource.QueryParameters( - 'all_tenants', - 'limit', - 'marker', - 'project_id', - 'name', - 'status', - 'volume_id', - 'sort_key', - 'sort_dir', + "limit", + "marker", + "offset", + "project_id", + "name", + "status", + "volume_id", + "sort_key", + "sort_dir", + "sort", + all_projects="all_tenants", ) # capabilities diff --git a/openstack/block_storage/v3/group.py b/openstack/block_storage/v3/group.py index ab7c5c536..82af284ae 100644 --- a/openstack/block_storage/v3/group.py +++ b/openstack/block_storage/v3/group.py @@ -27,6 +27,16 @@ class Group(resource.Resource): allow_commit = True allow_list = True + _query_mapping = resource.QueryParameters( + "limit", + "marker", + "offset", + "sort_dir", + "sort_key", + "sort", + all_projects="all_tenants", + ) + availability_zone = resource.Body("availability_zone") created_at = resource.Body("created_at") description = resource.Body("description") diff --git a/openstack/block_storage/v3/group_snapshot.py b/openstack/block_storage/v3/group_snapshot.py index 8341890be..c820aa7b4 100644 --- a/openstack/block_storage/v3/group_snapshot.py +++ b/openstack/block_storage/v3/group_snapshot.py @@ -27,6 +27,16 @@ class GroupSnapshot(resource.Resource): allow_commit = False allow_list = True + _query_mapping = resource.QueryParameters( + "limit", + "marker", + "offset", + "sort_dir", + "sort_key", + "sort", + all_projects="all_tenants", + ) + #: Properties #: The date and time when the resource was created. created_at = resource.Body("created_at") diff --git a/openstack/block_storage/v3/resource_filter.py b/openstack/block_storage/v3/resource_filter.py index c5b10ab74..06342ba02 100644 --- a/openstack/block_storage/v3/resource_filter.py +++ b/openstack/block_storage/v3/resource_filter.py @@ -19,7 +19,10 @@ class ResourceFilter(resource.Resource): resources_key = "resource_filters" base_path = "/resource_filters" - _query_mapping = resource.QueryParameters('resource') + _query_mapping = resource.QueryParameters( + 'resource', + include_pagination_defaults=False, + ) # Capabilities allow_list = True diff --git a/openstack/block_storage/v3/snapshot.py b/openstack/block_storage/v3/snapshot.py index 9028862b5..d551db259 100644 --- a/openstack/block_storage/v3/snapshot.py +++ b/openstack/block_storage/v3/snapshot.py @@ -23,7 +23,17 @@ class Snapshot(resource.Resource, metadata.MetadataMixin): base_path = "/snapshots" _query_mapping = resource.QueryParameters( - 'name', 'status', 'volume_id', 'project_id', all_projects='all_tenants' + "name", + "status", + "volume_id", + "project_id", + "limit", + "marker", + "offset", + "sort_dir", + "sort_key", + "sort", + all_projects="all_tenants", ) # capabilities diff --git a/openstack/block_storage/v3/type.py b/openstack/block_storage/v3/type.py index 77ed0bd68..828ac7449 100644 --- a/openstack/block_storage/v3/type.py +++ b/openstack/block_storage/v3/type.py @@ -27,7 +27,16 @@ class Type(resource.Resource): allow_list = True allow_commit = True - _query_mapping = resource.QueryParameters("is_public") + _query_mapping = resource.QueryParameters( + "is_public", + "limit", + "marker", + "offset", + "sort_dir", + "sort_key", + "sort", + all_projects="all_tenants", + ) # Properties #: Description of the type. diff --git a/openstack/tests/unit/block_storage/v3/test_backup.py b/openstack/tests/unit/block_storage/v3/test_backup.py index 7ba33e6e2..27772ba78 100644 --- a/openstack/tests/unit/block_storage/v3/test_backup.py +++ b/openstack/tests/unit/block_storage/v3/test_backup.py @@ -71,15 +71,17 @@ def test_basic(self): self.assertDictEqual( { - "all_tenants": "all_tenants", "limit": "limit", "marker": "marker", - "name": "name", + "offset": "offset", "project_id": "project_id", - "sort_dir": "sort_dir", - "sort_key": "sort_key", + "name": "name", "status": "status", "volume_id": "volume_id", + "sort_dir": "sort_dir", + "sort_key": "sort_key", + "sort": "sort", + "all_projects": "all_tenants", }, sot._query_mapping._mapping, ) diff --git a/openstack/tests/unit/block_storage/v3/test_resource_filter.py b/openstack/tests/unit/block_storage/v3/test_resource_filter.py index f5efa4a66..e8c5c4798 100644 --- a/openstack/tests/unit/block_storage/v3/test_resource_filter.py +++ b/openstack/tests/unit/block_storage/v3/test_resource_filter.py @@ -36,7 +36,9 @@ def test_basic(self): self.assertTrue(resource.allow_list) self.assertDictEqual( - {"resource": "resource", "limit": "limit", "marker": "marker"}, + { + "resource": "resource", + }, resource._query_mapping._mapping, ) diff --git a/openstack/tests/unit/block_storage/v3/test_snapshot.py b/openstack/tests/unit/block_storage/v3/test_snapshot.py index 7361e1e35..bd5d7d517 100644 --- a/openstack/tests/unit/block_storage/v3/test_snapshot.py +++ b/openstack/tests/unit/block_storage/v3/test_snapshot.py @@ -51,11 +51,15 @@ def test_basic(self): { "name": "name", "status": "status", - "all_projects": "all_tenants", - "project_id": "project_id", "volume_id": "volume_id", + "project_id": "project_id", "limit": "limit", + "offset": "offset", "marker": "marker", + "sort_dir": "sort_dir", + "sort_key": "sort_key", + "sort": "sort", + "all_projects": "all_tenants", }, sot._query_mapping._mapping, ) From 2f5c12dab8d7c50a0d4a76e1708589b956c503f8 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 26 May 2023 13:49:57 +0100 Subject: [PATCH 3370/3836] cloud: Remove dead method Change Ie358fb1e79c7047453807b0b969077996bfc3a23 should have removed this also. Whoops. Change-Id: I1f9b72cc41339eb25f055c2bab252a6040025500 Signed-off-by: Stephen Finucane --- openstack/cloud/openstackcloud.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 555bd7ce1..898476114 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -479,18 +479,6 @@ def _get_current_location(self, project_id=None, zone=None): project=self._get_project_info(project_id), ) - # TODO(stephenfin): This looks unused? Can we delete it? - def _get_identity_location(self): - '''Identity resources do not exist inside of projects.''' - return utils.Munch( - cloud=self.name, - region_name=None, - zone=None, - project=utils.Munch( - id=None, name=None, domain_id=None, domain_name=None - ), - ) - def range_search(self, data, filters): """Perform integer range searches across a list of dictionaries. From fc9ca79242647f80ed6390b99fdfb75b46a960a5 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 26 May 2023 10:00:02 +0100 Subject: [PATCH 3371/3836] cloud: Remove '_orchestration_client' Replace the use of raw clients with a REST request using our normal clients. Change-Id: I320ea0f5b1a8147c8e059844fdb9ec3b8f9f2628 Signed-off-by: Stephen Finucane --- openstack/cloud/_orchestration.py | 8 -------- openstack/orchestration/util/event_utils.py | 15 +++++++-------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/openstack/cloud/_orchestration.py b/openstack/cloud/_orchestration.py index dac0f3309..c35c40475 100644 --- a/openstack/cloud/_orchestration.py +++ b/openstack/cloud/_orchestration.py @@ -28,14 +28,6 @@ def _no_pending_stacks(stacks): class OrchestrationCloudMixin: orchestration: Proxy - # TODO(stephenfin): Remove final user of this - @property - def _orchestration_client(self): - if 'orchestration' not in self._raw_clients: - raw_client = self._get_raw_client('orchestration') - self._raw_clients['orchestration'] = raw_client - return self._raw_clients['orchestration'] - def get_template_contents( self, template_file=None, diff --git a/openstack/orchestration/util/event_utils.py b/openstack/orchestration/util/event_utils.py index a06119481..99d61deda 100644 --- a/openstack/orchestration/util/event_utils.py +++ b/openstack/orchestration/util/event_utils.py @@ -16,10 +16,10 @@ import time from openstack.cloud import meta -from openstack import proxy +from openstack import exceptions -# TODO(stephenfin): Convert to use proxy +# TODO(stephenfin): Convert to use real resources def get_events(cloud, stack_id, event_args, marker=None, limit=None): # TODO(mordred) FIX THIS ONCE assert_calls CAN HANDLE QUERY STRINGS params = collections.OrderedDict() @@ -31,15 +31,14 @@ def get_events(cloud, stack_id, event_args, marker=None, limit=None): if limit: event_args['limit'] = limit - data = proxy._json_response( - cloud._orchestration_client.get( - '/stacks/{id}/events'.format(id=stack_id), - params=params, - ) + response = cloud.orchestration.get( + f'/stacks/{stack_id}/events', + params=params, ) - events = meta.get_and_munchify('events', data) + exceptions.raise_from_response(response) # Show which stack the event comes from (for nested events) + events = meta.get_and_munchify('events', response.json()) for e in events: e['stack_name'] = stack_id.split("/")[0] return events From e8bece4aec3135b39aebf8f1fdca87d19f1bc117 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 28 Jul 2023 11:29:46 +0100 Subject: [PATCH 3372/3836] cloud: Remove '_get_raw_client' We have removed the final users of this method. Change-Id: If289e6b83d23c30e57cc81d127b288e3605bd832 Signed-off-by: Stephen Finucane --- openstack/cloud/openstackcloud.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 898476114..513f985d6 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -342,21 +342,6 @@ def _get_cache(self, resource_name): else: return self._cache - # TODO(shade) This should be replaced with using openstack Connection - # object. - def _get_raw_client( - self, service_type, api_version=None, endpoint_override=None - ): - return proxy.Proxy( - session=self.session, - service_type=self.config.get_service_type(service_type), - service_name=self.config.get_service_name(service_type), - interface=self.config.get_interface(service_type), - endpoint_override=self.config.get_endpoint(service_type) - or endpoint_override, - region_name=self.config.get_region_name(service_type), - ) - def pprint(self, resource): """Wrapper around pprint that groks munch objects""" # import late since this is a utility function From d474eb84c605c429bb9cccb166cabbdd1654d73c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 27 Jul 2023 15:09:35 +0100 Subject: [PATCH 3373/3836] cloud: Convert 'get_volume_limits' to use proxy layer We missed this in 1.0. Change-Id: I01955f42ca54d23c2a89f4e0636312e25f95b41c Signed-off-by: Stephen Finucane --- openstack/block_storage/v3/_proxy.py | 10 ++++++++-- openstack/cloud/_block_storage.py | 21 ++++----------------- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index ca41f39f5..48cf3da77 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -1094,15 +1094,21 @@ def reset_backup(self, backup, status): backup.reset(self, status) # ====== LIMITS ====== - def get_limits(self): + def get_limits(self, project=None): """Retrieves limits + :param project: A project to get limits for. The value can be either + the ID of a project or an + :class:`~openstack.identity.v3.project.Project` instance. :returns: A Limit object, including both :class:`~openstack.block_storage.v3.limits.AbsoluteLimit` and :class:`~openstack.block_storage.v3.limits.RateLimit` :rtype: :class:`~openstack.block_storage.v3.limits.Limit` """ - return self._get(_limits.Limit, requires_id=False) + params = {} + if project: + params['project_id'] = resource.Resource._get_id(project) + return self._get(_limits.Limit, requires_id=False, **params) def get_capabilities(self, host): """Get a backend's capabilites diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index c85e3fd27..73293c344 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -17,7 +17,6 @@ from openstack.cloud import _utils from openstack.cloud import exc from openstack import exceptions -from openstack import proxy from openstack import warnings as os_warnings @@ -279,7 +278,6 @@ def get_volumes(self, server, cache=True): volumes.append(volume) return volumes - # TODO(stephenfin): Convert to use proxy def get_volume_limits(self, name_or_id=None): """Get volume limits for the current project @@ -288,23 +286,12 @@ def get_volume_limits(self, name_or_id=None): :returns: The volume ``Limit`` object if found, else None. """ params = {} - project_id = None - error_msg = "Failed to get limits" if name_or_id: - proj = self.get_project(name_or_id) - if not proj: + project = self.get_project(name_or_id) + if not project: raise exc.OpenStackCloudException("project does not exist") - project_id = proj.id - params['tenant_id'] = project_id - error_msg = "{msg} for the project: {project} ".format( - msg=error_msg, project=name_or_id - ) - - data = proxy._json_response( - self.block_storage.get('/limits', params=params) - ) - limits = self._get_and_munchify('limits', data) - return limits + params['project'] = project + return self.block_storage.get_limits(**params) def get_volume_id(self, name_or_id): """Get ID of a volume. From 10217f5bc2ed58d1a6d48f5ffd8c705bcb08b58a Mon Sep 17 00:00:00 2001 From: Felix Huettner Date: Mon, 19 Dec 2022 16:51:38 +0100 Subject: [PATCH 3374/3836] Support passing a subnetpool for create_subnet previously create_subnet would wrongly report that we need to set a cidr if subnetpool_id is set. Change-Id: Icf1d6364d281a0b8b3156ae6b24d7ebca80d632b --- openstack/cloud/_network.py | 51 +++++++++-- openstack/tests/unit/cloud/test_subnet.py | 87 +++++++++++++++++++ ...subnet-by-subnetpool-eba1129c67ed4d96.yaml | 5 ++ 3 files changed, 135 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/create-subnet-by-subnetpool-eba1129c67ed4d96.yaml diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index 7fdb63e30..8ac148c35 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -457,6 +457,17 @@ def get_port_by_id(self, id): """ return self.network.get_port(id) + def get_subnetpool(self, name_or_id): + """Get a subnetpool by name or ID. + + :param name_or_id: Name or ID of the subnetpool. + + :returns: A network ``Subnetpool`` object if found, else None. + """ + return self.network.find_subnet_pool( + name_or_id=name_or_id, ignore_missing=True + ) + def create_network( self, name, @@ -2308,13 +2319,16 @@ def create_subnet( ipv6_address_mode=None, prefixlen=None, use_default_subnetpool=False, + subnetpool_name_or_id=None, **kwargs, ): """Create a subnet on a specified network. :param string network_name_or_id: The unique name or ID of the attached network. If a non-unique name is supplied, an exception is raised. - :param string cidr: The CIDR. + :param string cidr: The CIDR. Only one of ``cidr``, + ``use_default_subnetpool`` and ``subnetpool_name_or_id`` may be + specified at the same time. :param int ip_version: The IP version, which is 4 or 6. :param bool enable_dhcp: Set to ``True`` if DHCP is enabled and ``False`` if disabled. Default is ``False``. @@ -2362,10 +2376,15 @@ def create_subnet( :param string ipv6_address_mode: IPv6 address mode. Valid values are: 'dhcpv6-stateful', 'dhcpv6-stateless', or 'slaac'. :param string prefixlen: The prefix length to use for subnet allocation - from a subnet pool. + from a subnetpool. :param bool use_default_subnetpool: Use the default subnetpool for - ``ip_version`` to obtain a CIDR. It is required to pass ``None`` to - the ``cidr`` argument when enabling this option. + ``ip_version`` to obtain a CIDR. Only one of ``cidr``, + ``use_default_subnetpool`` and ``subnetpool_name_or_id`` may be + specified at the same time. + :param string subnetpool_name_or_id: The unique name or id of the + subnetpool to obtain a CIDR from. Only one of ``cidr``, + ``use_default_subnetpool`` and ``subnetpool_name_or_id`` may be + specified at the same time. :param kwargs: Key value pairs to be passed to the Neutron API. :returns: The created network ``Subnet`` object. :raises: OpenStackCloudException on operation error. @@ -2387,17 +2406,31 @@ def create_subnet( 'arg:disable_gateway_ip is not allowed with arg:gateway_ip' ) - if not cidr and not use_default_subnetpool: + uses_subnetpool = use_default_subnetpool or subnetpool_name_or_id + if not cidr and not uses_subnetpool: raise exc.OpenStackCloudException( 'arg:cidr is required when a subnetpool is not used' ) - if cidr and use_default_subnetpool: + if cidr and uses_subnetpool: raise exc.OpenStackCloudException( - 'arg:cidr must be set to None when use_default_subnetpool == ' - 'True' + 'arg:cidr and subnetpool may not be used at the same time' ) + if use_default_subnetpool and subnetpool_name_or_id: + raise exc.OpenStackCloudException( + 'arg:use_default_subnetpool and arg:subnetpool_id may not be ' + 'used at the same time' + ) + + subnetpool = None + if subnetpool_name_or_id: + subnetpool = self.get_subnetpool(subnetpool_name_or_id) + if not subnetpool: + raise exc.OpenStackCloudException( + "Subnetpool %s not found." % subnetpool_name_or_id + ) + # Be friendly on ip_version and allow strings if isinstance(ip_version, str): try: @@ -2443,6 +2476,8 @@ def create_subnet( subnet['prefixlen'] = prefixlen if use_default_subnetpool: subnet['use_default_subnetpool'] = True + if subnetpool: + subnet['subnetpool_id'] = subnetpool["id"] return self.network.create_subnet(**subnet) diff --git a/openstack/tests/unit/cloud/test_subnet.py b/openstack/tests/unit/cloud/test_subnet.py index 13b6447aa..02925d686 100644 --- a/openstack/tests/unit/cloud/test_subnet.py +++ b/openstack/tests/unit/cloud/test_subnet.py @@ -554,6 +554,93 @@ def test_create_subnet_from_subnetpool_with_prefixlen(self): self._compare_subnets(mock_subnet_rep, subnet) self.assert_calls() + def test_create_subnet_from_specific_subnetpool(self): + pool = [{'start': '172.16.0.2', 'end': '172.16.0.15'}] + id = '143296eb-7f47-4755-835c-488123475604' + gateway = '172.16.0.1' + dns = ['8.8.8.8'] + routes = [{"destination": "0.0.0.0/0", "nexthop": "123.456.78.9"}] + mock_subnet_rep = copy.copy(self.mock_subnet_rep) + mock_subnet_rep['allocation_pools'] = pool + mock_subnet_rep['dns_nameservers'] = dns + mock_subnet_rep['host_routes'] = routes + mock_subnet_rep['gateway_ip'] = gateway + mock_subnet_rep['subnetpool_id'] = self.mock_subnetpool_rep['id'] + mock_subnet_rep['cidr'] = self.subnetpool_cidr + mock_subnet_rep['id'] = id + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks', self.network_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=['v2.0', 'networks'], + qs_elements=['name=%s' % self.network_name], + ), + json={'networks': [self.mock_network_rep]}, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'network', + 'public', + append=[ + 'v2.0', + 'subnetpools', + self.mock_subnetpool_rep['id'], + ], + ), + json={"subnetpool": self.mock_subnetpool_rep}, + ), + dict( + method='POST', + uri=self.get_mock_url( + 'network', 'public', append=['v2.0', 'subnets'] + ), + json={'subnet': mock_subnet_rep}, + validate=dict( + json={ + 'subnet': { + 'enable_dhcp': False, + 'ip_version': 4, + 'network_id': self.mock_network_rep['id'], + 'allocation_pools': pool, + 'dns_nameservers': dns, + 'subnetpool_id': self.mock_subnetpool_rep[ + 'id' + ], + 'prefixlen': self.prefix_length, + 'host_routes': routes, + } + } + ), + ), + ] + ) + subnet = self.cloud.create_subnet( + self.network_name, + allocation_pools=pool, + dns_nameservers=dns, + subnetpool_name_or_id=self.mock_subnetpool_rep['id'], + prefixlen=self.prefix_length, + host_routes=routes, + ) + mock_subnet_rep.update( + {'prefixlen': self.prefix_length, 'use_default_subnetpool': None} + ) + self._compare_subnets(mock_subnet_rep, subnet) + self.assert_calls() + def test_delete_subnet(self): self.register_uris( [ diff --git a/releasenotes/notes/create-subnet-by-subnetpool-eba1129c67ed4d96.yaml b/releasenotes/notes/create-subnet-by-subnetpool-eba1129c67ed4d96.yaml new file mode 100644 index 000000000..bb7127ac6 --- /dev/null +++ b/releasenotes/notes/create-subnet-by-subnetpool-eba1129c67ed4d96.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added support for specifying the subnetpool to use when creating subnets + (``subnetpool_name_or_id``) From 58476dec0e62d9621916279f7a95fbbcd3d5dd7f Mon Sep 17 00:00:00 2001 From: Christian Rohmann Date: Tue, 29 Aug 2023 14:36:45 +0200 Subject: [PATCH 3375/3836] Fix Swift endpoint conversion to determine info/caps url This adds some robustness and flexibility converting the Swift endpoint URL from the registry to the one exposing the info / capabilities. Additionally some unit tests were added to ensure different use-cases and URL patterns are covered. Story: 2010898 Task: 48689 Related-Bug: #1712358 Change-Id: I321ae9375d16e21771ee9d9bce1c61f85627c879 --- openstack/object_store/v1/info.py | 31 +++++++++---- .../tests/unit/object_store/v1/test_info.py | 44 +++++++++++++++++++ .../notes/bug-2010898-430da335e4df0efe.yaml | 5 +++ 3 files changed, 71 insertions(+), 9 deletions(-) create mode 100644 openstack/tests/unit/object_store/v1/test_info.py create mode 100644 releasenotes/notes/bug-2010898-430da335e4df0efe.yaml diff --git a/openstack/object_store/v1/info.py b/openstack/object_store/v1/info.py index f3999ad0a..ac83d8265 100644 --- a/openstack/object_store/v1/info.py +++ b/openstack/object_store/v1/info.py @@ -11,10 +11,12 @@ # License for the specific language governing permissions and limitations # under the License. +import re import urllib from openstack import exceptions from openstack import resource +from openstack import utils class Info(resource.Resource): @@ -32,6 +34,24 @@ class Info(resource.Resource): staticweb = resource.Body("staticweb", type=dict) tempurl = resource.Body("tempurl", type=dict) + # The endpoint in the catalog has version and project-id in it + # To get capabilities, we have to disassemble and reassemble the URL + # to append 'info' + # This logic is taken from swiftclient + def _get_info_url(self, url): + URI_PATTERN_VERSION = re.compile(r'\/v\d+\.?\d*(\/.*)?') + scheme, netloc, path, params, query, fragment = urllib.parse.urlparse( + url + ) + if URI_PATTERN_VERSION.search(path): + path = URI_PATTERN_VERSION.sub('/info', path) + else: + path = utils.urljoin(path, 'info') + + return urllib.parse.urlunparse( + (scheme, netloc, path, params, query, fragment) + ) + def fetch( self, session, @@ -60,18 +80,11 @@ def fetch( if not self.allow_fetch: raise exceptions.MethodNotSupported(self, "fetch") - # The endpoint in the catalog has version and project-id in it - # To get capabilities, we have to disassemble and reassemble the URL - # This logic is taken from swiftclient - session = self._get_session(session) - endpoint = urllib.parse.urlparse(session.get_endpoint()) - url = "{scheme}://{netloc}/info".format( - scheme=endpoint.scheme, netloc=endpoint.netloc - ) + info_url = self._get_info_url(session.get_endpoint()) microversion = self._get_microversion(session, action='fetch') - response = session.get(url, microversion=microversion) + response = session.get(info_url, microversion=microversion) kwargs = {} if error_message: kwargs['error_message'] = error_message diff --git a/openstack/tests/unit/object_store/v1/test_info.py b/openstack/tests/unit/object_store/v1/test_info.py new file mode 100644 index 000000000..36f5e7e9f --- /dev/null +++ b/openstack/tests/unit/object_store/v1/test_info.py @@ -0,0 +1,44 @@ +# 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.object_store.v1 import info +from openstack.tests.unit import base + + +class TestInfo(base.TestCase): + def setUp(self): + super(TestInfo, self).setUp() + + def test_get_info_url(self): + sot = info.Info() + test_urls = { + 'http://object.cloud.example.com': 'http://object.cloud.example.com/info', + 'http://object.cloud.example.com/': 'http://object.cloud.example.com/info', + 'http://object.cloud.example.com/v1': 'http://object.cloud.example.com/info', + 'http://object.cloud.example.com/v1/': 'http://object.cloud.example.com/info', + 'http://object.cloud.example.com/swift': 'http://object.cloud.example.com/swift/info', + 'http://object.cloud.example.com/swift/': 'http://object.cloud.example.com/swift/info', + 'http://object.cloud.example.com/v1.0': 'http://object.cloud.example.com/info', + 'http://object.cloud.example.com/swift/v1.0': 'http://object.cloud.example.com/swift/info', + 'http://object.cloud.example.com/v111': 'http://object.cloud.example.com/info', + 'http://object.cloud.example.com/v111/test': 'http://object.cloud.example.com/info', + 'http://object.cloud.example.com/v1/test': 'http://object.cloud.example.com/info', + 'http://object.cloud.example.com/swift/v1.0/test': 'http://object.cloud.example.com/swift/info', + 'http://object.cloud.example.com/v1.0/test': 'http://object.cloud.example.com/info', + 'https://object.cloud.example.com/swift/v1/AUTH_%(tenant_id)s': 'https://object.cloud.example.com/swift/info', + 'https://object.cloud.example.com/swift/v1/AUTH_%(project_id)s': 'https://object.cloud.example.com/swift/info', + 'https://object.cloud.example.com/services/swift/v1/AUTH_%(project_id)s': 'https://object.cloud.example.com/services/swift/info', + 'https://object.cloud.example.com/services/swift/v1/AUTH_%(project_id)s/': 'https://object.cloud.example.com/services/swift/info', + 'https://object.cloud.example.com/info/v1/AUTH_%(project_id)s/': 'https://object.cloud.example.com/info/info', + } + for uri_k, uri_v in test_urls.items(): + self.assertEqual(sot._get_info_url(uri_k), uri_v) diff --git a/releasenotes/notes/bug-2010898-430da335e4df0efe.yaml b/releasenotes/notes/bug-2010898-430da335e4df0efe.yaml new file mode 100644 index 000000000..2d09fc799 --- /dev/null +++ b/releasenotes/notes/bug-2010898-430da335e4df0efe.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + [`bug 2010898 `_] + Fix Swift endpoint url handling to determine info/caps url From bb9b54cebf227ff1bf13f300df68f0f779565bdf Mon Sep 17 00:00:00 2001 From: Christian Rohmann Date: Tue, 29 Aug 2023 16:48:36 +0200 Subject: [PATCH 3376/3836] Fix bulk_delete support determination 1) The indication if Swift supports bulk_delete is actually presented via key bulk_delete at the root of the /info JSON response. 2) Using a default of {} caused is_bulk_delete_supported to always be True 3) Default to 10k max_deletes_per_request like python-swiftclient does Story: 2010899 Task: 48690 Change-Id: Id2a57ee6095b1ca25cd329c61cd5415d3c38e175 --- openstack/object_store/v1/_proxy.py | 11 ++++++----- openstack/object_store/v1/info.py | 1 + 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index 61bec6831..f9d74e7e4 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -1148,11 +1148,12 @@ def _service_cleanup( except exceptions.SDKException: pass else: - bulk_delete = caps.swift.get("bulk_delete", {}) - is_bulk_delete_supported = bulk_delete is not None - bulk_delete_max_per_request = bulk_delete.get( - "max_deletes_per_request", 100 - ) + bulk_delete = caps.get("bulk_delete") + if bulk_delete is not None: + is_bulk_delete_supported = True + bulk_delete_max_per_request = bulk_delete.get( + "max_deletes_per_request", 10000 + ) elements = [] for cont in self.containers(): diff --git a/openstack/object_store/v1/info.py b/openstack/object_store/v1/info.py index f3999ad0a..c82ba6470 100644 --- a/openstack/object_store/v1/info.py +++ b/openstack/object_store/v1/info.py @@ -27,6 +27,7 @@ class Info(resource.Resource): ) # Properties + bulk_delete = resource.Body("bulk_delete", type=dict) swift = resource.Body("swift", type=dict) slo = resource.Body("slo", type=dict) staticweb = resource.Body("staticweb", type=dict) From ac3dabb08662149d9440417d54e9e7cbea39cf6b Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 31 Aug 2023 17:37:19 +0100 Subject: [PATCH 3377/3836] docs: Rewrite caching docs Change-Id: I395f89073cc33db2f7db5c40540b7fd5ee21d96e Signed-off-by: Stephen Finucane --- doc/source/user/config/configuration.rst | 174 +++++++++++++++-------- 1 file changed, 115 insertions(+), 59 deletions(-) diff --git a/doc/source/user/config/configuration.rst b/doc/source/user/config/configuration.rst index c3c657d4e..41e8dfc75 100644 --- a/doc/source/user/config/configuration.rst +++ b/doc/source/user/config/configuration.rst @@ -207,48 +207,110 @@ boolean is also recognised (with the opposite semantics to `verify`; i.e. `True` ignores certificate failures). This should be considered deprecated for `verify`. + Cache Settings -------------- -Accessing a cloud is often expensive, so it's quite common to want to do some -client-side caching of those operations. To facilitate that, `openstacksdk` -understands passing through cache settings to dogpile.cache, with the following -behaviors: - -* Listing no config settings means you get a null cache. -* `cache.expiration_time` and nothing else gets you memory cache. -* Otherwise, `cache.class` and `cache.arguments` are passed in - -Different cloud behaviors are also differently expensive to deal with. If you -want to get really crazy and tweak stuff, you can specify different expiration -times on a per-resource basis by passing values, in seconds to an expiration -mapping keyed on the singular name of the resource. A value of `-1` indicates -that the resource should never expire. Not specifying a value (same as -specifying `0`) indicates that no caching for this resource should be done. -`openstacksdk` only caches `GET` request responses for the queries which have -non-zero expiration time defined. Caching key contains url and request -parameters, therefore no collisions are expected. - -The expiration time key is constructed (joined with `.`) in the same way as the -metrics are emmited: - -* service type -* meaningful resource url segments (i.e. `/servers` results in `servers`, - `/servers/ID` results in `server`, `/servers/ID/metadata/KEY` results in - `server.metadata` - -Non `GET` requests cause cache invalidation based on the caching key prefix so -that i.e. `PUT` request to `/images/ID` will invalidate all images cache (list -and all individual entries). Moreover it is possible to explicitly pass -`_sdk_skip_cache` parameter to the `proxy._get` function to bypass cache and -invalidate what is already there. This is happening automatically in the -`wait_for_status` methods where it is expected that resource is going to change -some of the attributes over the time. Forcing complete cache invalidation can -be achieved calling `conn._cache.invalidate`. - -`openstacksdk` does not actually cache anything itself, but it collects and -presents the cache information so that your various applications that are -connecting to OpenStack can share a cache should you desire. +.. versionchanged:: 1.0.0 + + Previously, caching was managed exclusively in the cloud layer. Starting in + openstacksdk 1.0.0, caching is moved to the proxy layer. As the cloud layer + depends on the proxy layer in 1.0.0, this means both layers can benefit from + the cache. + +Authenticating and accessing resources on a cloud is often expensive. It is +therefore quite common that applications will wish to do some client-side +caching of both credentials and cloud resources. To facilitate this, +*openstacksdk* supports caching credentials and resources using the system +keyring and *dogpile.cache*, respectively. + +.. tip:: + + It is important to emphasise that *openstacksdk* does not actually cache + anything itself. Rather, it collects and presents the cache information + so that your various applications that are connecting to OpenStack can share + a cache should you desire. It is important that your cache backend is + correctly configured according to the needs of your application. + +Caching in enabled or disabled globally, rather than on a cloud-by-cloud basis. +This is done by setting configuring the``cache`` top-level key. Caching of +authentication tokens can be configured using the following settings: + +``cache.auth`` + A boolean indicating whether tokens should be cached in the keyring. + When enabled, this allows the consequent connections to the same cloud to + skip fetching new token. When the token expires or is invalidated, + `openstacksdk` will automatically establish a new connection. + Defaults to ``false``. + +For example, to configure caching of authentication tokens. + +.. code-block:: yaml + + cache: + auth: true + +Caching of resources can be configured using the following settings: + +``cache.expiration_time`` + The expiration time in seconds for a cache entry. + This should be an integer. + Defaults to ``0``. + +``cache.class`` + The cache backend to use, which can include any backend supported by + *dogpile.cache* natively as well as backend provided by third-part packages. + This should be a string. + Defaults to ``dogpile.cache.memory``. + +``cache.arguments`` + A mapping of arbitrary arguments to pass into the cache backend. These are + backend specific. Keys should correspond to a configuration option for the + configured cache backend. + Defaults to ``{}``. + +``cache.expirations`` + A mapping of resource types to expiration times. The keys should be specified + in the same way as the metrics are emitted, by joining meaningful resource + URL segments with ``.``. For example, both ``/servers`` and ``/servers/ID`` + should be specified as ``servers``, while ``/servers/ID/metadata/KEY`` should + be specified as `server.metadata`. Values should be an expiration time in + seconds. A value of ``-1`` indicates that the cache should never expire, + while a value of ``0`` disables caching for the resource. + Defaults to ``{}`` + +For example, to configure caching with the ``dogpile.cache.memory`` backend +with a 1 hour expiration. + +.. code-block:: yaml + + cache: + expiration_time: 3600 + +To configure caching with the ``dogpile.cache.memory`` backend with a 1 hour +expiration but only for requests to the OpenStack Compute service's +``/servers`` API: + +.. code-block:: yaml + + cache: + expirations: + servers: 3600 + +To configure caching with the ``dogpile.cache.pylibmc`` backend with a 1 hour +expiration time and a memcached server running on your localhost. + +.. code-block:: yaml + + cache: + expiration_time: 3600 + arguments: + url: + - 127.0.0.1 + +To configure caching with the ``dogpile.cache.pylibmc`` backend with a 1 hour +expiration time, a memcached server running on your localhost, and multiple +per-resource cache expiration times. .. code-block:: yaml @@ -264,34 +326,28 @@ connecting to OpenStack can share a cache should you desire. compute.servers: 5 compute.flavors: -1 image.images: 5 - clouds: - mtvexx: - profile: vexxhost - auth: - username: mordred@inaugust.com - password: XXXXXXXXX - project_name: mordred@inaugust.com - region_name: ca-ymq-1 - dns_api_version: 1 -`openstacksdk` can also cache authorization state (token) in the keyring. -That allow the consequent connections to the same cloud to skip fetching new -token. When the token gets expired or gets invalid `openstacksdk` will -establish new connection. +Finally, if the ``cache`` key is undefined, a null cache is enabled meaning +caching is effectively disabled. +.. note:: -.. code-block:: yaml - - cache: - auth: true - + Non ``GET`` requests cause cache invalidation based on the caching key + prefix. This means that, for example, a ``PUT`` request to ``/images/ID`` + will invalidate all images cache (list and all individual entries). Moreover + it is possible to explicitly pass the ``skip_cache`` parameter to the + ``proxy._get`` function to bypass cache and invalidate what is already + there. This is happening automatically in the ``wait_for_status`` methods + where it is expected that resource will change some of the attributes over + the time. Forcing complete cache invalidation can be achieved calling + ``conn._cache.invalidate`` MFA Support ----------- MFA support requires a specially prepared configuration file. In this case a -combination of 2 different authorization plugins is used with their individual -requirements to the specified parameteres. +combination of two different authorization plugins is used with their +individual requirements to the specified parameters. .. code-block:: yaml From 1322913dc6f2ac65f64b6ae002f1c8d8bcacf814 Mon Sep 17 00:00:00 2001 From: whoami-rajat Date: Wed, 24 May 2023 16:01:50 +0530 Subject: [PATCH 3378/3836] Add volume attachment support Add support for the following operations: * Attachment Create * Attachment Get * Attachment List * Attachment Delete * Attachment Update * Attachment Complete Change-Id: Id32c1607d22a88aacce815b93e23bd03eeafcded --- doc/source/user/proxies/block_storage_v3.rst | 8 + .../resources/block_storage/v3/attachment.rst | 13 ++ openstack/block_storage/v3/_proxy.py | 109 ++++++++++ openstack/block_storage/v3/attachment.py | 103 ++++++++++ .../block_storage/v3/test_attachment.py | 90 +++++++++ .../unit/block_storage/v3/test_attachment.py | 188 ++++++++++++++++++ ...e-attachment-support-b5f9a9e78ba88355.yaml | 11 + 7 files changed, 522 insertions(+) create mode 100644 doc/source/user/resources/block_storage/v3/attachment.rst create mode 100644 openstack/block_storage/v3/attachment.py create mode 100644 openstack/tests/functional/block_storage/v3/test_attachment.py create mode 100644 openstack/tests/unit/block_storage/v3/test_attachment.py create mode 100644 releasenotes/notes/add-volume-attachment-support-b5f9a9e78ba88355.yaml diff --git a/doc/source/user/proxies/block_storage_v3.rst b/doc/source/user/proxies/block_storage_v3.rst index abe3f0e73..c7cc4b085 100644 --- a/doc/source/user/proxies/block_storage_v3.rst +++ b/doc/source/user/proxies/block_storage_v3.rst @@ -148,3 +148,11 @@ BlockStorageSummary Operations .. autoclass:: openstack.block_storage.v3._proxy.Proxy :noindex: :members: summary + +Attachments +^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v3._proxy.Proxy + :noindex: + :members: create_attachment, get_attachment, attachments, + delete_attachment, update_attachment, complete_attachment diff --git a/doc/source/user/resources/block_storage/v3/attachment.rst b/doc/source/user/resources/block_storage/v3/attachment.rst new file mode 100644 index 000000000..113ecd32e --- /dev/null +++ b/doc/source/user/resources/block_storage/v3/attachment.rst @@ -0,0 +1,13 @@ +openstack.block_storage.v3.attachment +===================================== + +.. automodule:: openstack.block_storage.v3.attachment + +The Volume Attachment Class +--------------------------- + +The ``Volume Attachment`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v3.attachment.Attachment + :members: create, update, complete diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 48cf3da77..c17cb5a3e 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -13,6 +13,7 @@ import typing as ty from openstack.block_storage import _base_proxy +from openstack.block_storage.v3 import attachment as _attachment from openstack.block_storage.v3 import availability_zone from openstack.block_storage.v3 import backup as _backup from openstack.block_storage.v3 import block_storage_summary as _summary @@ -37,6 +38,7 @@ class Proxy(_base_proxy.BaseBlockStorageProxy): _resource_registry = { "availability_zone": availability_zone.AvailabilityZone, + "attachment": _attachment.Attachment, "backup": _backup.Backup, "capabilities": _capabilities.Capabilities, "extension": _extension.Extension, @@ -959,6 +961,113 @@ def terminate_volume_attachment(self, volume, connector): volume = self._get_resource(_volume.Volume, volume) volume.terminate_attachment(self, connector) + # ====== ATTACHMENTS ====== + + def create_attachment(self, volume, **attrs): + """Create a new attachment + + This is an internal API and should only be called by services + consuming volume attachments like nova, glance, ironic etc. + + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.block_storage.v3.volume.Volume` instance. + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.block_storage.v3.attachment.Attachment` + comprised of the properties on the Attachment class like + connector, instance_id, mode etc. + :returns: The results of attachment creation + :rtype: :class:`~openstack.block_storage.v3.attachment.Attachment` + """ + volume_id = resource.Resource._get_id(volume) + return self._create( + _attachment.Attachment, volume_id=volume_id, **attrs + ) + + def get_attachment(self, attachment): + """Get a single volume + + This is an internal API and should only be called by services + consuming volume attachments like nova, glance, ironic etc. + + :param attachment: The value can be the ID of an attachment or a + :class:`~attachment.Attachment` instance. + + :returns: One :class:`~attachment.Attachment` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_attachment.Attachment, attachment) + + def attachments(self, **query): + """Returns a generator of attachments. + + This is an internal API and should only be called by services + consuming volume attachments like nova, glance, ironic etc. + + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. + + :returns: A generator of attachment objects. + """ + return self._list(_attachment.Attachment, **query) + + def delete_attachment(self, attachment, ignore_missing=True): + """Delete an attachment + + This is an internal API and should only be called by services + consuming volume attachments like nova, glance, ironic etc. + + :param type: The value can be either the ID of a attachment or a + :class:`~openstack.block_storage.v3.attachment.Attachment` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the attachment does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent attachment. + + :returns: ``None`` + """ + self._delete( + _attachment.Attachment, + attachment, + ignore_missing=ignore_missing, + ) + + def update_attachment(self, attachment, **attrs): + """Update an attachment + + This is an internal API and should only be called by services + consuming volume attachments like nova, glance, ironic etc. + + :param attachment: The value can be the ID of an attachment or a + :class:`~openstack.block_storage.v3.attachment.Attachment` + instance. + :param dict attrs: Keyword arguments which will be used to update + a :class:`~openstack.block_storage.v3.attachment.Attachment` + comprised of the properties on the Attachment class + + :returns: The updated attachment + :rtype: :class:`~openstack.volume.v3.attachment.Attachment` + """ + return self._update(_attachment.Attachment, attachment, **attrs) + + def complete_attachment(self, attachment): + """Complete an attachment + + This is an internal API and should only be called by services + consuming volume attachments like nova, glance, ironic etc. + + :param attachment: The value can be the ID of an attachment or a + :class:`~openstack.block_storage.v3.attachment.Attachment` + instance. + + :returns: ``None`` + :rtype: :class:`~openstack.volume.v3.attachment.Attachment` + """ + attachment_obj = self._get_resource(_attachment.Attachment, attachment) + return attachment_obj.complete(self) + # ====== BACKEND POOLS ====== def backend_pools(self, **query): """Returns a generator of cinder Back-end storage pools diff --git a/openstack/block_storage/v3/attachment.py b/openstack/block_storage/v3/attachment.py new file mode 100644 index 000000000..ba6e845a0 --- /dev/null +++ b/openstack/block_storage/v3/attachment.py @@ -0,0 +1,103 @@ +# 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 + +from openstack import exceptions +from openstack import resource +from openstack import utils + + +class Attachment(resource.Resource): + resource_key = "attachment" + resources_key = "attachments" + base_path = "/attachments" + + # capabilities + allow_create = True + allow_delete = True + allow_commit = True + allow_list = True + allow_get = True + allow_fetch = True + + _max_microversion = "3.54" + + # Properties + #: The ID of the attachment. + id = resource.Body("id") + #: The status of the attachment. + status = resource.Body("status") + #: The UUID of the attaching instance. + instance = resource.Body("instance") + #: The UUID of the volume which the attachment belongs to. + volume_id = resource.Body("volume_id") + #: The time when attachment is attached. + attached_at = resource.Body("attach_time") + #: The time when attachment is detached. + detached_at = resource.Body("detach_time") + #: The attach mode of attachment, read-only ('ro') or read-and-write + # ('rw'), default is 'rw'. + attach_mode = resource.Body("mode") + #: The connection info used for server to connect the volume. + connection_info = resource.Body("connection_info") + #: The connector object. + connector = resource.Body("connector") + + def create( + self, + session, + prepend_key=True, + base_path=None, + *, + resource_request_key=None, + resource_response_key=None, + microversion=None, + **params, + ): + if utils.supports_microversion(session, '3.54'): + if not self.attach_mode: + self._body.clean(only={'mode'}) + return super().create( + session, + prepend_key=prepend_key, + base_path=base_path, + resource_request_key=resource_request_key, + resource_response_key=resource_response_key, + microversion=microversion, + **params, + ) + + def complete(self, session, *, microversion=None): + """Mark the attachment as completed.""" + body = {'os-complete': self.id} + if not microversion: + microversion = self._get_microversion(session, action='commit') + url = os.path.join(Attachment.base_path, self.id, 'action') + response = session.post(url, json=body, microversion=microversion) + exceptions.raise_from_response(response) + + def _prepare_request_body( + self, + patch, + prepend_key, + *, + resource_request_key=None, + ): + body = self._body.dirty + if body.get('volume_id'): + body['volume_uuid'] = body.pop('volume_id') + if body.get('instance'): + body['instance_uuid'] = body.pop('instance') + if prepend_key and self.resource_key is not None: + body = {self.resource_key: body} + return body diff --git a/openstack/tests/functional/block_storage/v3/test_attachment.py b/openstack/tests/functional/block_storage/v3/test_attachment.py new file mode 100644 index 000000000..b7b85752e --- /dev/null +++ b/openstack/tests/functional/block_storage/v3/test_attachment.py @@ -0,0 +1,90 @@ +# 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.block_storage.v3 import volume as _volume +from openstack.tests.functional.block_storage.v3 import base + + +class TestAttachment(base.BaseBlockStorageTest): + """Test class for volume attachment operations. + + We have implemented a test that performs attachment create + and attachment delete operations. Attachment create requires + the instance ID and the volume ID for which we have created a + volume resource and an instance resource. + We haven't implemented attachment update test since it requires + the host connector information which is not readily available to + us and hard to retrieve. Without passing this information, the + attachment update operation will fail. + Similarly, we haven't implement attachment complete test since it + depends on attachment update and can only be performed when the volume + status is 'attaching' which is done by attachment update operation. + """ + + def setUp(self): + super().setUp() + + # Create Volume + self.volume_name = self.getUniqueString() + + volume = self.user_cloud.block_storage.create_volume( + name=self.volume_name, size=1 + ) + self.user_cloud.block_storage.wait_for_status( + volume, + status='available', + failures=['error'], + interval=2, + wait=self._wait_for_timeout, + ) + self.assertIsInstance(volume, _volume.Volume) + self.VOLUME_ID = volume.id + + # Create Server + self.server_name = self.getUniqueString() + self.server = self.operator_cloud.compute.create_server( + name=self.server_name, + flavor_id=self.flavor.id, + image_id=self.image.id, + networks='none', + ) + self.operator_cloud.compute.wait_for_server( + self.server, wait=self._wait_for_timeout + ) + + def tearDown(self): + # Since delete_on_termination flag is set to True, we + # don't need to cleanup the volume manually + result = self.conn.compute.delete_server(self.server.id) + self.conn.compute.wait_for_delete( + self.server, wait=self._wait_for_timeout + ) + self.assertIsNone(result) + super().tearDown() + + def test_attachment(self): + attachment = self.conn.block_storage.create_attachment( + self.VOLUME_ID, + connector={}, + instance_id=self.server.id, + ) + self.assertIn('id', attachment) + self.assertIn('status', attachment) + self.assertIn('instance', attachment) + self.assertIn('volume_id', attachment) + self.assertIn('attached_at', attachment) + self.assertIn('detached_at', attachment) + self.assertIn('attach_mode', attachment) + self.assertIn('connection_info', attachment) + attachment = self.user_cloud.block_storage.delete_attachment( + attachment.id, ignore_missing=False + ) diff --git a/openstack/tests/unit/block_storage/v3/test_attachment.py b/openstack/tests/unit/block_storage/v3/test_attachment.py new file mode 100644 index 000000000..af05a64ea --- /dev/null +++ b/openstack/tests/unit/block_storage/v3/test_attachment.py @@ -0,0 +1,188 @@ +# 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 unittest import mock + +from keystoneauth1 import adapter + +from openstack.block_storage.v3 import attachment +from openstack import resource +from openstack.tests.unit import base + + +FAKE_ID = "92dc3671-d0ab-4370-8058-c88a71661ec5" +FAKE_VOL_ID = "138e4a2e-85ef-4f96-a0d0-9f3ef9f32987" +FAKE_INSTANCE_UUID = "ee9ae89e-d4fc-4c95-93ad-d9e80f240cae" + +CONNECTION_INFO = { + "access_mode": "rw", + "attachment_id": "92dc3671-d0ab-4370-8058-c88a71661ec5", + "auth_enabled": True, + "auth_username": "cinder", + "cacheable": False, + "cluster_name": "ceph", + "discard": True, + "driver_volume_type": "rbd", + "encrypted": False, + "hosts": ["127.0.0.1"], + "name": "volumes/volume-138e4a2e-85ef-4f96-a0d0-9f3ef9f32987", + "ports": ["6789"], + "secret_type": "ceph", + "secret_uuid": "e5d27872-64ab-4d8c-8c25-4dbdc522fbbf", + "volume_id": "138e4a2e-85ef-4f96-a0d0-9f3ef9f32987", +} + +CONNECTOR = { + "do_local_attach": False, + "host": "devstack-VirtualBox", + "initiator": "iqn.2005-03.org.open-iscsi:1f6474a01f9a", + "ip": "127.0.0.1", + "multipath": False, + "nqn": "nqn.2014-08.org.nvmexpress:uuid:4dfe457e-6206-4a61-b547-5a9d0e2fa557", + "nvme_native_multipath": False, + "os_type": "linux", + "platform": "x86_64", + "system_uuid": "2f4d1bf2-8a9e-864f-80ec-d265222bf145", + "uuid": "87c73a20-e7f9-4370-ad85-5829b54675d7", +} + +ATTACHMENT = { + "id": FAKE_ID, + "status": "attached", + "instance": FAKE_INSTANCE_UUID, + "volume_id": FAKE_VOL_ID, + "attached_at": "2023-07-07T10:30:40.000000", + "detached_at": None, + "attach_mode": "rw", + "connection_info": CONNECTION_INFO, +} + + +class TestAttachment(base.TestCase): + def setUp(self): + super(TestAttachment, self).setUp() + self.resp = mock.Mock() + self.resp.body = None + self.resp.json = mock.Mock(return_value=self.resp.body) + self.resp.headers = {} + self.resp.status_code = 202 + + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.get = mock.Mock() + self.sess.post = mock.Mock(return_value=self.resp) + self.sess.put = mock.Mock(return_value=self.resp) + self.sess.default_microversion = "3.54" + + def test_basic(self): + sot = attachment.Attachment(ATTACHMENT) + self.assertEqual("attachment", sot.resource_key) + self.assertEqual("attachments", sot.resources_key) + self.assertEqual("/attachments", sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_commit) + self.assertIsNotNone(sot._max_microversion) + + self.assertDictEqual( + { + "limit": "limit", + "marker": "marker", + }, + sot._query_mapping._mapping, + ) + + def test_create_resource(self): + sot = attachment.Attachment(**ATTACHMENT) + self.assertEqual(ATTACHMENT["id"], sot.id) + self.assertEqual(ATTACHMENT["status"], sot.status) + self.assertEqual(ATTACHMENT["instance"], sot.instance) + self.assertEqual(ATTACHMENT["volume_id"], sot.volume_id) + self.assertEqual(ATTACHMENT["attached_at"], sot.attached_at) + self.assertEqual(ATTACHMENT["detached_at"], sot.detached_at) + self.assertEqual(ATTACHMENT["attach_mode"], sot.attach_mode) + self.assertEqual(ATTACHMENT["connection_info"], sot.connection_info) + + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=True, + ) + @mock.patch.object(resource.Resource, '_translate_response') + def test_create_no_mode_no_instance_id(self, mock_translate, mock_mv): + self.sess.default_microversion = "3.27" + mock_mv.return_value = False + sot = attachment.Attachment() + FAKE_MODE = "rw" + sot.create( + self.sess, + volume_id=FAKE_VOL_ID, + connector=CONNECTOR, + instance=None, + mode=FAKE_MODE, + ) + self.sess.post.assert_called_with( + '/attachments', + json={'attachment': {}}, + headers={}, + microversion="3.27", + params={ + 'volume_id': FAKE_VOL_ID, + 'connector': CONNECTOR, + 'instance': None, + 'mode': 'rw', + }, + ) + self.sess.default_microversion = "3.54" + + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=True, + ) + @mock.patch.object(resource.Resource, '_translate_response') + def test_create_with_mode_with_instance_id(self, mock_translate, mock_mv): + sot = attachment.Attachment() + FAKE_MODE = "rw" + sot.create( + self.sess, + volume_id=FAKE_VOL_ID, + connector=CONNECTOR, + instance=FAKE_INSTANCE_UUID, + mode=FAKE_MODE, + ) + self.sess.post.assert_called_with( + '/attachments', + json={'attachment': {}}, + headers={}, + microversion="3.54", + params={ + 'volume_id': FAKE_VOL_ID, + 'connector': CONNECTOR, + 'instance': FAKE_INSTANCE_UUID, + 'mode': FAKE_MODE, + }, + ) + + @mock.patch.object(resource.Resource, '_translate_response') + def test_complete(self, mock_translate): + sot = attachment.Attachment() + sot.id = FAKE_ID + sot.complete(self.sess) + self.sess.post.assert_called_with( + '/attachments/%s/action' % FAKE_ID, + json={ + 'os-complete': '92dc3671-d0ab-4370-8058-c88a71661ec5', + }, + microversion="3.54", + ) diff --git a/releasenotes/notes/add-volume-attachment-support-b5f9a9e78ba88355.yaml b/releasenotes/notes/add-volume-attachment-support-b5f9a9e78ba88355.yaml new file mode 100644 index 000000000..d540fd058 --- /dev/null +++ b/releasenotes/notes/add-volume-attachment-support-b5f9a9e78ba88355.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + Added support for: + + * Create Attachment + * Update Attachment + * List Attachment + * Get Attachment + * Delete Attachment + * Complete Attachment From cf349e8b8270c25c4df67cc28b5863c60de82b00 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 1 Sep 2023 09:48:16 +0100 Subject: [PATCH 3379/3836] cloud: Remove remnants of port caching This functionality was effectively removed when we reworked the caching feature in change I2c8ae2c59d15c750ea8ebd3031ffdd2ced2421ed, moving it from the cloud layer to the proxy layer. Change-Id: I06b51ab62dd927c2b9defb94cb97faadb9430aaa Signed-off-by: Stephen Finucane --- openstack/cloud/_floating_ip.py | 21 +++------------ openstack/cloud/_network.py | 45 +++++-------------------------- openstack/cloud/openstackcloud.py | 3 --- 3 files changed, 9 insertions(+), 60 deletions(-) diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index 31037b2fe..b4e2cf46c 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -1222,26 +1222,11 @@ def _nat_destination_port( :param fixed_address: Fixed ip address of the port :param nat_destination: Name or ID of the network of the port. """ - # If we are caching port lists, we may not find the port for - # our server if the list is old. Try for at least 2 cache - # periods if that is the case. - if self._PORT_AGE: - timeout = self._PORT_AGE * 2 - else: - timeout = None - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for port to show up in list", - wait=self._PORT_AGE, - ): - try: - port_filter = {'device_id': server['id']} - ports = self.search_ports(filters=port_filter) - break - except exc.OpenStackCloudTimeout: - ports = None + port_filter = {'device_id': server['id']} + ports = self.search_ports(filters=port_filter) if not ports: return (None, None) + port = None if not fixed_address: if len(ports) > 1: diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index 7fdb63e30..f6d78526b 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -10,9 +10,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import threading -import time - from openstack.cloud import _utils from openstack.cloud import exc from openstack import exceptions @@ -22,11 +19,6 @@ class NetworkCloudMixin: network: Proxy - def __init__(self): - self._ports = None - self._ports_time = 0 - self._ports_lock = threading.Lock() - @_utils.cache_on_arguments() def _neutron_extensions(self): extensions = set() @@ -106,10 +98,10 @@ def search_ports(self, name_or_id=None, filters=None): :raises: ``OpenStackCloudException`` if something goes wrong during the OpenStack API call. """ - # If port caching is enabled, do not push the filter down to - # neutron; get all the ports (potentially from the cache) and - # filter locally. - if self._PORT_AGE or isinstance(filters, str): + # If the filter is a string, do not push the filter down to neutron; + # get all the ports and filter locally. + # TODO(stephenfin): '_filter_list' can handle a dict - pass it down + if isinstance(filters, str): pushdown_filters = None else: pushdown_filters = filters @@ -167,39 +159,14 @@ def list_ports(self, filters=None): :param filters: (optional) A dict of filter conditions to push down :returns: A list of network ``Port`` objects. """ - # If pushdown filters are specified and we do not have batched caching - # enabled, bypass local caching and push down the filters. - if filters and self._PORT_AGE == 0: - return self._list_ports(filters) - - if (time.time() - self._ports_time) >= self._PORT_AGE: - # Since we're using cached data anyway, we don't need to - # have more than one thread actually submit the list - # ports task. Let the first one submit it while holding - # a lock, and the non-blocking acquire method will cause - # subsequent threads to just skip this and use the old - # data until it succeeds. - # Initially when we never got data, block to retrieve some data. - first_run = self._ports is None - if self._ports_lock.acquire(first_run): - try: - if not (first_run and self._ports is not None): - self._ports = self._list_ports({}) - self._ports_time = time.time() - finally: - self._ports_lock.release() - # Wrap the return with filter_list so that if filters were passed - # but we were batching/caching and thus always fetching the whole - # list from the cloud, we still return a filtered list. - return _utils._filter_list(self._ports, None, filters or {}) - - def _list_ports(self, filters): # If the cloud is running nova-network, just return an empty list. if not self.has_service('network'): return [] + # Translate None from search interface to empty {} for kwargs below if not filters: filters = {} + return list(self.network.ports(**filters)) # TODO(stephenfin): Deprecate 'filters'; users should use 'list' for this diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 513f985d6..2afd9b821 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -34,7 +34,6 @@ from openstack import utils DEFAULT_SERVER_AGE = 5 -DEFAULT_PORT_AGE = 5 DEFAULT_FLOAT_AGE = 5 _CONFIG_DOC_URL = _floating_ip._CONFIG_DOC_URL DEFAULT_OBJECT_SEGMENT_SIZE = _object_store.DEFAULT_OBJECT_SEGMENT_SIZE @@ -119,7 +118,6 @@ def invalidate(self): # Replace this with a more specific cache configuration # soon. self._SERVER_AGE = 0 - self._PORT_AGE = 0 self._FLOAT_AGE = 0 self._cache = _FakeCache() # Undecorate cache decorated methods. Otherwise the call stacks @@ -145,7 +143,6 @@ def invalidate(self): # TODO(gtema): delete in next change self._SERVER_AGE = 0 - self._PORT_AGE = 0 self._FLOAT_AGE = 0 self._api_cache_keys = set() From 404ef826377f5476077bd22a75c9bb9d9901fdb1 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 1 Sep 2023 10:56:44 +0100 Subject: [PATCH 3380/3836] cloud: Remove remnants of floating IP caching Yet another cleanup post I2c8ae2c59d15c750ea8ebd3031ffdd2ced2421ed. This is defunct code now that caching is done at the proxy layer instead. Change-Id: Ibc0ff82a90eb5b7f84918e552b2cfda82b2a1420 Signed-off-by: Stephen Finucane --- openstack/cloud/_floating_ip.py | 96 +++++++++---------------------- openstack/cloud/openstackcloud.py | 5 -- 2 files changed, 26 insertions(+), 75 deletions(-) diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index b4e2cf46c..92efe0df9 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -24,11 +24,6 @@ from openstack import utils from openstack import warnings as os_warnings -_CONFIG_DOC_URL = ( - "https://docs.openstack.org/openstacksdk/latest/" - "user/config/configuration.html" -) - class FloatingIPCloudMixin: network: Proxy @@ -43,10 +38,6 @@ def __init__(self): else: self._floating_ip_source = self._floating_ip_source.lower() - self._floating_ips = None - self._floating_ips_time = 0 - self._floating_ips_lock = threading.Lock() - self._floating_network_by_router = None self._floating_network_by_router_run = False self._floating_network_by_router_lock = threading.Lock() @@ -110,39 +101,45 @@ def get_floating_ip(self, id, filters=None): """ return _utils._get_entity(self, 'floating_ip', id, filters) - def _list_floating_ips(self, filters=None): + def list_floating_ips(self, filters=None): + """List all available floating IPs. + + :param filters: (optional) dict of filter conditions to push down + :returns: A list of floating IP + ``openstack.network.v2.floating_ip.FloatingIP``. + """ + if not filters: + filters = {} + if self._use_neutron_floating(): try: return self._neutron_list_floating_ips(filters) except exc.OpenStackCloudURINotFound as e: # Nova-network don't support server-side floating ips - # filtering, so it's safer to return and empty list than + # filtering, so it's safer to return an empty list than # to fallback to Nova which may return more results that # expected. if filters: self.log.error( - "Neutron returned NotFound for floating IPs, which" - " means this cloud doesn't have neutron floating ips." - " shade can't fallback to trying Nova since nova" - " doesn't support server-side filtering when listing" - " floating ips and filters were given. If you do not" - " think shade should be attempting to list floating" - " ips on neutron, it is possible to control the" - " behavior by setting floating_ip_source to 'nova' or" - " None for cloud: %(cloud)s. If you are not already" - " using clouds.yaml to configure settings for your" - " cloud(s), and you want to configure this setting," - " you will need a clouds.yaml file. For more" - " information, please see %(doc_url)s", + "Neutron returned NotFound for floating IPs, which " + "means this cloud doesn't have neutron floating ips. " + "openstacksdk can't fallback to trying Nova since " + "nova doesn't support server-side filtering when " + "listing floating ips and filters were given. " + "If you do not think openstacksdk should be " + "attempting to list floating IPs on neutron, it is " + "possible to control the behavior by setting " + "floating_ip_source to 'nova' or None for cloud " + "%(cloud)r in 'clouds.yaml'.", { 'cloud': self.name, - 'doc_url': _CONFIG_DOC_URL, }, ) # We can't fallback to nova because we push-down filters. # We got a 404 which means neutron doesn't exist. If the # user return [] + self.log.debug( "Something went wrong talking to neutron API: " "'%(msg)s'. Trying with Nova.", @@ -153,7 +150,7 @@ def _list_floating_ips(self, filters=None): if filters: raise ValueError( "Nova-network don't support server-side floating ips " - "filtering. Use the search_floatting_ips method instead" + "filtering. Use the search_floating_ips method instead" ) floating_ips = self._nova_list_floating_ips() @@ -167,9 +164,7 @@ def list_floating_ip_pools(self): neutron. `get_external_ipv4_floating_networks` is what you should almost certainly be using. - :returns: A list of floating IP pool - ``openstack.network.v2.floating_ip.FloatingIP``. - + :returns: A list of floating IP pools """ if not self._has_nova_extension('os-floating-ip-pools'): raise exc.OpenStackCloudUnavailableExtension( @@ -183,40 +178,6 @@ def list_floating_ip_pools(self): pools = self._get_and_munchify('floating_ip_pools', data) return [{'name': p['name']} for p in pools] - def list_floating_ips(self, filters=None): - """List all available floating IPs. - - :param filters: (optional) dict of filter conditions to push down - :returns: A list of floating IP - ``openstack.network.v2.floating_ip.FloatingIP``. - - """ - # If pushdown filters are specified and we do not have batched caching - # enabled, bypass local caching and push down the filters. - if filters and self._FLOAT_AGE == 0: - return self._list_floating_ips(filters) - - if (time.time() - self._floating_ips_time) >= self._FLOAT_AGE: - # Since we're using cached data anyway, we don't need to - # have more than one thread actually submit the list - # floating ips task. Let the first one submit it while holding - # a lock, and the non-blocking acquire method will cause - # subsequent threads to just skip this and use the old - # data until it succeeds. - # Initially when we never got data, block to retrieve some data. - first_run = self._floating_ips is None - if self._floating_ips_lock.acquire(first_run): - try: - if not (first_run and self._floating_ips is not None): - self._floating_ips = self._list_floating_ips() - self._floating_ips_time = time.time() - finally: - self._floating_ips_lock.release() - # Wrap the return with filter_list so that if filters were passed - # but we were batching/caching and thus always fetching the whole - # list from the cloud, we still return a filtered list. - return _utils._filter_list(self._floating_ips, None, filters) - def get_floating_ip_by_id(self, id): """Get a floating ip by ID @@ -250,7 +211,6 @@ def _neutron_available_floating_ips( :param server: (server) Server the Floating IP is for :returns: a list of floating IP addresses. - :raises: ``OpenStackCloudResourceNotFound``, if an external network that meets the specified criteria cannot be found. """ @@ -286,7 +246,7 @@ def _neutron_available_floating_ips( 'project_id': project_id, } - floating_ips = self._list_floating_ips() + floating_ips = self.list_floating_ips() available_ips = _utils._filter_list( floating_ips, name_or_id=None, filters=filters ) @@ -533,7 +493,7 @@ def _neutron_create_floating_ip( for count in utils.iterate_timeout( timeout, "Timeout waiting for the floating IP to be ACTIVE", - wait=self._FLOAT_AGE, + wait=min(5, timeout), ): fip = self.get_floating_ip(fip_id) if fip and fip['status'] == 'ACTIVE': @@ -615,10 +575,6 @@ def delete_floating_ip(self, floating_ip_id, retry=1): if (retry == 0) or not result: return result - # Wait for the cached floating ip list to be regenerated - if self._FLOAT_AGE: - time.sleep(self._FLOAT_AGE) - # neutron sometimes returns success when deleting a floating # ip. That's awesome. SO - verify that the delete actually # worked. Some clouds will set the status to DOWN rather than diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 2afd9b821..03d2aaeb7 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -22,7 +22,6 @@ import requestsexceptions from openstack import _log -from openstack.cloud import _floating_ip from openstack.cloud import _object_store from openstack.cloud import _utils from openstack.cloud import exc @@ -34,8 +33,6 @@ from openstack import utils DEFAULT_SERVER_AGE = 5 -DEFAULT_FLOAT_AGE = 5 -_CONFIG_DOC_URL = _floating_ip._CONFIG_DOC_URL DEFAULT_OBJECT_SEGMENT_SIZE = _object_store.DEFAULT_OBJECT_SEGMENT_SIZE # This halves the current default for Swift DEFAULT_MAX_FILE_SIZE = _object_store.DEFAULT_MAX_FILE_SIZE @@ -118,7 +115,6 @@ def invalidate(self): # Replace this with a more specific cache configuration # soon. self._SERVER_AGE = 0 - self._FLOAT_AGE = 0 self._cache = _FakeCache() # Undecorate cache decorated methods. Otherwise the call stacks # wind up being stupidly long and hard to debug @@ -143,7 +139,6 @@ def invalidate(self): # TODO(gtema): delete in next change self._SERVER_AGE = 0 - self._FLOAT_AGE = 0 self._api_cache_keys = set() self._container_cache = dict() From 8d462696a7bd334827bfc1b1ef855353188853d9 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 1 Sep 2023 11:02:01 +0100 Subject: [PATCH 3381/3836] cloud: Remove remnants of server caching The final cleanup post I2c8ae2c59d15c750ea8ebd3031ffdd2ced2421ed. Once again, this is all dead code now that can be removed in favour of proxy layer caching. Note that the removed test can be removed since this is a test for this cloud-layer caching, not the proxy-layer caching (which would require configuration of dogpile.cache). Change-Id: Ifb80965a8c4faa53cbdbcb1e1efd8a5b90ddb0e7 Signed-off-by: Stephen Finucane --- openstack/cloud/_compute.py | 70 ++----------------- openstack/cloud/_floating_ip.py | 5 +- openstack/cloud/openstackcloud.py | 5 -- openstack/tests/unit/cloud/test_caching.py | 27 ------- .../tests/unit/cloud/test_create_server.py | 1 - 5 files changed, 6 insertions(+), 102 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 19b145692..46b980612 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -13,7 +13,6 @@ import base64 import functools import operator -import threading import time import iso8601 @@ -73,11 +72,6 @@ def _pop_or_get(resource, key, default, strict): class ComputeCloudMixin: compute: Proxy - def __init__(self): - self._servers = None - self._servers_time = 0 - self._servers_lock = threading.Lock() - @property def _compute_region(self): # This is only used in exception messages. Can we get rid of it? @@ -364,49 +358,14 @@ def list_servers( :param filters: Additional query parameters passed to the API server. :returns: A list of compute ``Server`` objects. """ - # If pushdown filters are specified and we do not have batched caching - # enabled, bypass local caching and push down the filters. - if filters and self._SERVER_AGE == 0: - return self._list_servers( - detailed=detailed, - all_projects=all_projects, - bare=bare, - filters=filters, - ) + if not filters: + filters = {} - if (time.time() - self._servers_time) >= self._SERVER_AGE: - # Since we're using cached data anyway, we don't need to - # have more than one thread actually submit the list - # servers task. Let the first one submit it while holding - # a lock, and the non-blocking acquire method will cause - # subsequent threads to just skip this and use the old - # data until it succeeds. - # Initially when we never got data, block to retrieve some data. - first_run = self._servers is None - if self._servers_lock.acquire(first_run): - try: - if not (first_run and self._servers is not None): - self._servers = self._list_servers( - detailed=detailed, - all_projects=all_projects, - bare=bare, - ) - self._servers_time = time.time() - finally: - self._servers_lock.release() - # Wrap the return with filter_list so that if filters were passed - # but we were batching/caching and thus always fetching the whole - # list from the cloud, we still return a filtered list. - return _utils._filter_list(self._servers, None, filters) - - def _list_servers( - self, detailed=False, all_projects=False, bare=False, filters=None - ): - filters = filters or {} return [ self._expand_server(server, detailed, bare) for server in self.compute.servers( - all_projects=all_projects, **filters + all_projects=all_projects, + **filters, ) ] @@ -1014,14 +973,6 @@ def create_server( # to add unit tests for this too. admin_pass = server.admin_password or kwargs.get('admin_pass') if not wait: - # This is a direct get call to skip the list_servers - # cache which has absolutely no chance of containing the - # new server. - # Only do this if we're not going to wait for the server - # to complete booting, because the only reason we do it - # is to get a server record that is the return value from - # get/list rather than the return value of create. If we're - # going to do the wait loop below, this is a waste of a call server = self.compute.get_server(server.id) if server.status == 'ERROR': raise exc.OpenStackCloudCreateException( @@ -1159,24 +1110,16 @@ def wait_for_server( """ Wait for a server to reach ACTIVE status. """ - # server = self.compute.wait_for_server( - # server=server, interval=self._SERVER_AGE or 2, wait=timeout - # ) server_id = server['id'] timeout_message = "Timeout waiting for the server to come up." start_time = time.time() - # There is no point in iterating faster than the list_servers cache for count in utils.iterate_timeout( timeout, timeout_message, - # if _SERVER_AGE is 0 we still want to wait a bit - # to be friendly with the server. - wait=self._SERVER_AGE or 2, + wait=min(5, timeout), ): try: - # Use the get_server call so that the list_servers - # cache can be leveraged server = self.get_server(server_id) except Exception: continue @@ -1458,9 +1401,6 @@ def _delete_server( if reset_volume_cache: self.list_volumes.invalidate(self) - # Reset the list servers cache time so that the next list server - # call gets a new list - self._servers_time = self._servers_time - self._SERVER_AGE return True @_utils.valid_kwargs('name', 'description') diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index 92efe0df9..7d7e362e7 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -735,7 +735,7 @@ def _attach_ip_to_server( for _ in utils.iterate_timeout( timeout, "Timeout waiting for the floating IP to be attached.", - wait=self._SERVER_AGE, + wait=min(5, timeout), ): server = self.get_server_by_id(server_id) ext_ip = meta.get_server_ip( @@ -898,9 +898,6 @@ def _add_ip_from_pool( timeout=timeout, ) timeout = timeout - (time.time() - start_time) - # Wait for cache invalidation time so that we don't try - # to attach the FIP a second time below - time.sleep(self._SERVER_AGE) server = self.get_server(server.id) # We run attach as a second call rather than in the create call diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 03d2aaeb7..13009d7c9 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -32,7 +32,6 @@ from openstack import proxy from openstack import utils -DEFAULT_SERVER_AGE = 5 DEFAULT_OBJECT_SEGMENT_SIZE = _object_store.DEFAULT_OBJECT_SEGMENT_SIZE # This halves the current default for Swift DEFAULT_MAX_FILE_SIZE = _object_store.DEFAULT_MAX_FILE_SIZE @@ -114,7 +113,6 @@ def invalidate(self): # Don't cache list_servers if we're not caching things. # Replace this with a more specific cache configuration # soon. - self._SERVER_AGE = 0 self._cache = _FakeCache() # Undecorate cache decorated methods. Otherwise the call stacks # wind up being stupidly long and hard to debug @@ -137,9 +135,6 @@ def invalidate(self): for expire_key in expirations.keys(): self._cache_expirations[expire_key] = expirations[expire_key] - # TODO(gtema): delete in next change - self._SERVER_AGE = 0 - self._api_cache_keys = set() self._container_cache = dict() self._file_hash_cache = dict() diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py index 58f6489f5..ea95b2707 100644 --- a/openstack/tests/unit/cloud/test_caching.py +++ b/openstack/tests/unit/cloud/test_caching.py @@ -10,9 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import concurrent -import time - import testtools from testscenarios import load_tests_apply_scenarios as load_tests # noqa @@ -180,30 +177,6 @@ def test_list_projects_v3(self): self.assert_calls() - def test_list_servers_no_herd(self): - self.cloud._SERVER_AGE = 2 - fake_server = fakes.make_fake_server('1234', 'name') - self.register_uris( - [ - self.get_nova_discovery_mock_dict(), - dict( - method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'] - ), - json={'servers': [fake_server]}, - ), - ] - ) - with concurrent.futures.ThreadPoolExecutor(16) as pool: - for i in range(16): - pool.submit(lambda: self.cloud.list_servers(bare=True)) - # It's possible to race-condition 16 threads all in the - # single initial lock without a tiny sleep - time.sleep(0.001) - - self.assert_calls() - def test_list_volumes(self): fake_volume = fakes.FakeVolume( 'volume1', 'available', 'Volume 1 Display Name' diff --git a/openstack/tests/unit/cloud/test_create_server.py b/openstack/tests/unit/cloud/test_create_server.py index deaf19cb8..02b4fa6b5 100644 --- a/openstack/tests/unit/cloud/test_create_server.py +++ b/openstack/tests/unit/cloud/test_create_server.py @@ -813,7 +813,6 @@ def test_create_server_no_addresses(self, mock_add_ips_to_server): ] ) mock_add_ips_to_server.return_value = fake_server - self.cloud._SERVER_AGE = 0 self.assertRaises( exc.OpenStackCloudException, From dd4e457f71ce5290c7a0cf3a1586d6befd9c5336 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 1 Sep 2023 11:11:45 +0100 Subject: [PATCH 3382/3836] cloud: Remove floating network cache This can be done enabling caching at the proxy layer. No need for a special variation on this. Change-Id: Ife5bda82eeb10c093b6cb55b0027ec16c89734d7 Signed-off-by: Stephen Finucane --- openstack/cloud/_floating_ip.py | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index 7d7e362e7..e63dd23fb 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -11,7 +11,6 @@ # limitations under the License. import ipaddress -import threading import time import warnings @@ -38,10 +37,6 @@ def __init__(self): else: self._floating_ip_source = self._floating_ip_source.lower() - self._floating_network_by_router = None - self._floating_network_by_router_run = False - self._floating_network_by_router_lock = threading.Lock() - def search_floating_ip_pools(self, name=None, filters=None): pools = self.list_floating_ip_pools() return _utils._filter_list(pools, name, filters) @@ -303,25 +298,13 @@ def _nova_available_floating_ips(self, pool=None): def _find_floating_network_by_router(self): """Find the network providing floating ips by looking at routers.""" - - if self._floating_network_by_router_lock.acquire( - not self._floating_network_by_router_run - ): - if self._floating_network_by_router_run: - self._floating_network_by_router_lock.release() - return self._floating_network_by_router - try: - for router in self.list_routers(): - if router['admin_state_up']: - network_id = router.get( - 'external_gateway_info', {} - ).get('network_id') - if network_id: - self._floating_network_by_router = network_id - finally: - self._floating_network_by_router_run = True - self._floating_network_by_router_lock.release() - return self._floating_network_by_router + for router in self.list_routers(): + if router['admin_state_up']: + network_id = router.get('external_gateway_info', {}).get( + 'network_id' + ) + if network_id: + return network_id def available_floating_ip(self, network=None, server=None): """Get a floating IP from a network or a pool. From a068ec30614e691a72b53809817cc03f5175a611 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 1 Sep 2023 09:54:28 +0100 Subject: [PATCH 3383/3836] cloud: Don't use dangerous argument defaults Weird that flake8 (or, rather, pycodestyle) doesn't have a check for this. Change-Id: Id67675bdb0e2c70f145d931a5e42cae399bb290a Signed-off-by: Stephen Finucane --- openstack/cloud/_compute.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 46b980612..8204e72da 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -1573,12 +1573,14 @@ def list_flavor_access(self, flavor_id): """ return self.compute.get_flavor_access(flavor_id) - def list_hypervisors(self, filters={}): + def list_hypervisors(self, filters=None): """List all hypervisors :param filters: :returns: A list of compute ``Hypervisor`` objects. """ + if not filters: + filters = {} return list(self.compute.hypervisors(details=True, **filters)) @@ -1595,11 +1597,14 @@ def search_aggregates(self, name_or_id=None, filters=None): aggregates = self.list_aggregates() return _utils._filter_list(aggregates, name_or_id, filters) - def list_aggregates(self, filters={}): + def list_aggregates(self, filters=None): """List all available host aggregates. :returns: A list of compute ``Aggregate`` objects. """ + if not filters: + filters = {} + return self.compute.aggregates(**filters) def get_aggregate(self, name_or_id, filters=None): From ec35409212463c8532d0d204197381c53ebf75a1 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 27 Jul 2023 10:50:27 +0100 Subject: [PATCH 3384/3836] cloud: Remove old cloud-layer caching functionality This is no longer necessary since we migrated to proxy-layer caching in change I2c8ae2c59d15c750ea8ebd3031ffdd2ced2421ed. Remove the final traces of it as well as the tests. Change-Id: I7f4ce810a917093d88edd92c092f6ddc73c67d36 Signed-off-by: Stephen Finucane --- openstack/cloud/_block_storage.py | 17 - openstack/cloud/_coe.py | 5 - openstack/cloud/_compute.py | 20 - openstack/cloud/_identity.py | 14 - openstack/cloud/_image.py | 14 +- openstack/cloud/_network.py | 1 - openstack/cloud/_object_store.py | 1 - openstack/cloud/_orchestration.py | 10 - openstack/cloud/_utils.py | 43 - openstack/cloud/openstackcloud.py | 32 - openstack/compute/v2/_proxy.py | 1 - openstack/image/v1/_proxy.py | 3 - openstack/image/v2/_proxy.py | 6 - openstack/proxy.py | 1 + openstack/tests/unit/cloud/test_caching.py | 807 ------------------ ...-cloud-caching-layer-2b0384870a45e8a3.yaml | 7 + 16 files changed, 9 insertions(+), 973 deletions(-) delete mode 100644 openstack/tests/unit/cloud/test_caching.py create mode 100644 releasenotes/notes/remove-cloud-caching-layer-2b0384870a45e8a3.yaml diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index 73293c344..a03c4dd81 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -20,19 +20,10 @@ from openstack import warnings as os_warnings -def _no_pending_volumes(volumes): - """If there are any volumes not in a steady state, don't cache""" - for volume in volumes: - if volume['status'] not in ('available', 'error', 'in-use'): - return False - return True - - class BlockStorageCloudMixin: block_storage: Proxy # TODO(stephenfin): Remove 'cache' in a future major version - @_utils.cache_on_arguments(should_cache_fn=_no_pending_volumes) def list_volumes(self, cache=True): """List all available volumes. @@ -47,7 +38,6 @@ def list_volumes(self, cache=True): return list(self.block_storage.volumes()) # TODO(stephenfin): Remove 'get_extra' in a future major version - @_utils.cache_on_arguments() def list_volume_types(self, get_extra=None): """List all available volume types. @@ -166,8 +156,6 @@ def create_volume( volume = self.block_storage.create_volume(**kwargs) - self.list_volumes.invalidate(self) - if volume['status'] == 'error': raise exc.OpenStackCloudException("Error in creating volume") @@ -195,8 +183,6 @@ def update_volume(self, name_or_id, **kwargs): volume = self.block_storage.update_volume(volume, **kwargs) - self.list_volumes.invalidate(self) - return volume def set_volume_bootable(self, name_or_id, bootable=True): @@ -240,8 +226,6 @@ def delete_volume( :raises: OpenStackCloudTimeout if wait time exceeded. :raises: OpenStackCloudException on operation error. """ - - self.list_volumes.invalidate(self) volume = self.block_storage.find_volume(name_or_id) if not volume: @@ -257,7 +241,6 @@ def delete_volume( self.log.exception("error in deleting volume") raise - self.list_volumes.invalidate(self) if wait: self.block_storage.wait_for_delete(volume, wait=timeout) diff --git a/openstack/cloud/_coe.py b/openstack/cloud/_coe.py index c10553467..f568c6ceb 100644 --- a/openstack/cloud/_coe.py +++ b/openstack/cloud/_coe.py @@ -15,7 +15,6 @@ class CoeCloudMixin: - @_utils.cache_on_arguments() def list_coe_clusters(self): """List COE (Container Orchestration Engine) cluster. @@ -90,7 +89,6 @@ def create_coe_cluster( **kwargs, ) - self.list_coe_clusters.invalidate(self) return cluster def delete_coe_cluster(self, name_or_id): @@ -114,7 +112,6 @@ def delete_coe_cluster(self, name_or_id): return False self.container_infrastructure_management.delete_cluster(cluster) - self.list_coe_clusters.invalidate(self) return True def update_coe_cluster(self, name_or_id, **kwargs): @@ -127,7 +124,6 @@ def update_coe_cluster(self, name_or_id, **kwargs): :raises: OpenStackCloudException on operation error. """ - self.list_coe_clusters.invalidate(self) cluster = self.get_coe_cluster(name_or_id) if not cluster: raise exc.OpenStackCloudException( @@ -169,7 +165,6 @@ def sign_coe_cluster_certificate(self, cluster_id, csr): cluster_uuid=cluster_id, csr=csr ) - @_utils.cache_on_arguments() def list_cluster_templates(self, detail=False): """List cluster templates. diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 8204e72da..971cbd0e4 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -115,7 +115,6 @@ def get_flavor_by_ram(self, ram, include=None, get_extra=True): ) ) - @_utils.cache_on_arguments() def _nova_extensions(self): extensions = set([e.alias for e in self.compute.extensions()]) return extensions @@ -194,7 +193,6 @@ def list_keypairs(self, filters=None): filters = {} return list(self.compute.keypairs(**filters)) - @_utils.cache_on_arguments() def list_availability_zone_names(self, unavailable=False): """List names of availability zones. @@ -216,7 +214,6 @@ def list_availability_zone_names(self, unavailable=False): ) return [] - @_utils.cache_on_arguments() def list_flavors(self, get_extra=False): """List all available flavors. @@ -1093,8 +1090,6 @@ def _get_boot_from_volume_kwargs( 'source_type': 'volume', } kwargs['block_device_mapping_v2'].append(block_mapping) - if boot_volume or boot_from_volume or volumes: - self.list_volumes.invalidate(self) return kwargs def wait_for_server( @@ -1379,18 +1374,6 @@ def _delete_server( if not wait: return True - # If the server has volume attachments, or if it has booted - # from volume, deleting it will change volume state so we will - # need to invalidate the cache. Avoid the extra API call if - # caching is not enabled. - reset_volume_cache = False - if ( - self.cache_enabled - and self.has_service('volume') - and self.get_volumes(server) - ): - reset_volume_cache = True - if not isinstance(server, _server.Server): # We might come here with Munch object (at the moment). # If this is the case - convert it into real server to be able to @@ -1398,9 +1381,6 @@ def _delete_server( server = _server.Server(id=server['id']) self.compute.wait_for_delete(server, wait=timeout) - if reset_volume_cache: - self.list_volumes.invalidate(self) - return True @_utils.valid_kwargs('name', 'description') diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index 7f66aa2cb..62daed4ab 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -61,7 +61,6 @@ def _get_identity_params(self, domain_id=None, project=None): ret.update(self._get_project_id_param_dict(project)) return ret - @_utils.cache_on_arguments() def list_projects(self, domain_id=None, name_or_id=None, filters=None): """List projects. @@ -186,7 +185,6 @@ def update_project( if enabled is not None: kwargs.update({'enabled': enabled}) project = self.identity.update_project(project, **kwargs) - self.list_projects.invalidate(self) return project def create_project( @@ -242,7 +240,6 @@ def delete_project(self, name_or_id, domain_id=None): return False @_utils.valid_kwargs('domain_id', 'name') - @_utils.cache_on_arguments() def list_users(self, **kwargs): """List users. @@ -340,7 +337,6 @@ def get_user_by_id(self, user_id, normalize=True): 'default_project', ) def update_user(self, name_or_id, **kwargs): - self.list_users.invalidate(self) user_kwargs = {} if 'domain_id' in kwargs and kwargs['domain_id']: user_kwargs['domain_id'] = kwargs['domain_id'] @@ -355,7 +351,6 @@ def update_user(self, name_or_id, **kwargs): del kwargs['domain_id'] user = self.identity.update_user(user, **kwargs) - self.list_users.invalidate(self) return user def create_user( @@ -378,13 +373,10 @@ def create_user( user = self.identity.create_user(**params) - self.list_users.invalidate(self) return user @_utils.valid_kwargs('domain_id') def delete_user(self, name_or_id, **kwargs): - # TODO(mordred) Why are we invalidating at the TOP? - self.list_users.invalidate(self) try: user = self.get_user(name_or_id, **kwargs) if not user: @@ -394,7 +386,6 @@ def delete_user(self, name_or_id, **kwargs): return False self.identity.delete_user(user) - self.list_users.invalidate(self) return True except exceptions.SDKException: @@ -891,7 +882,6 @@ def get_domain(self, domain_id=None, name_or_id=None, filters=None): return self.identity.get_domain(domain_id) @_utils.valid_kwargs('domain_id') - @_utils.cache_on_arguments() def list_groups(self, **kwargs): """List Keystone groups. @@ -969,7 +959,6 @@ def create_group(self, name, description, domain=None): group = self.identity.create_group(**group_ref) - self.list_groups.invalidate(self) return group def update_group( @@ -988,7 +977,6 @@ def update_group( :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ - self.list_groups.invalidate(self) group = self.identity.find_group(name_or_id, **kwargs) if group is None: raise exc.OpenStackCloudException( @@ -1003,7 +991,6 @@ def update_group( group = self.identity.update_group(group, **group_ref) - self.list_groups.invalidate(self) return group def delete_group(self, name_or_id): @@ -1022,7 +1009,6 @@ def delete_group(self, name_or_id): self.identity.delete_group(group) - self.list_groups.invalidate(self) return True except exceptions.SDKException: diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py index 54cb09b08..97807faa8 100644 --- a/openstack/cloud/_image.py +++ b/openstack/cloud/_image.py @@ -16,14 +16,6 @@ from openstack import utils -def _no_pending_images(images): - """If there are any images not in a steady state, don't cache""" - for image in images: - if image.status not in ('active', 'deleted', 'killed'): - return False - return True - - class ImageCloudMixin: image: Proxy @@ -34,7 +26,6 @@ def search_images(self, name_or_id=None, filters=None): images = self.list_images() return _utils._filter_list(images, name_or_id, filters) - @_utils.cache_on_arguments(should_cache_fn=_no_pending_images) def list_images(self, filter_deleted=True, show_all=False): """Get available images. @@ -170,7 +161,6 @@ def wait_for_image(self, image, timeout=3600): for count in utils.iterate_timeout( timeout, "Timeout waiting for image to snapshot" ): - self.list_images.invalidate(self) image = self.get_image(image_id) if not image: continue @@ -203,7 +193,6 @@ def delete_image( if not image: return False self.image.delete_image(image) - self.list_images.invalidate(self) # Task API means an image was uploaded to swift # TODO(gtema) does it make sense to move this into proxy? @@ -221,7 +210,6 @@ def delete_image( for count in utils.iterate_timeout( timeout, "Timeout waiting for the image to be deleted." ): - self._get_cache(None).invalidate() if self.get_image(image.id) is None: break return True @@ -321,9 +309,9 @@ def create_image( **kwargs, ) - self._get_cache(None).invalidate() if not wait: return image + try: for count in utils.iterate_timeout( timeout, "Timeout waiting for the image to finish." diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index f6d78526b..34af1ae3e 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -19,7 +19,6 @@ class NetworkCloudMixin: network: Proxy - @_utils.cache_on_arguments() def _neutron_extensions(self): extensions = set() for extension in self.network.extensions(): diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index 3fe735bd1..a80df2bbf 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -171,7 +171,6 @@ def get_container_access(self, name): "Could not determine container access for ACL: %s." % acl ) - @_utils.cache_on_arguments() def get_object_capabilities(self): """Get infomation about the object-storage service diff --git a/openstack/cloud/_orchestration.py b/openstack/cloud/_orchestration.py index c35c40475..fa79872b2 100644 --- a/openstack/cloud/_orchestration.py +++ b/openstack/cloud/_orchestration.py @@ -16,15 +16,6 @@ from openstack.orchestration.v1._proxy import Proxy -def _no_pending_stacks(stacks): - """If there are any stacks not in a steady state, don't cache""" - for stack in stacks: - status = stack['stack_status'] - if '_COMPLETE' not in status and '_FAILED' not in status: - return False - return True - - class OrchestrationCloudMixin: orchestration: Proxy @@ -226,7 +217,6 @@ def search_stacks(self, name_or_id=None, filters=None): stacks = self.list_stacks() return _utils._filter_list(stacks, name_or_id, filters) - @_utils.cache_on_arguments(should_cache_fn=_no_pending_stacks) def list_stacks(self, **query): """List all stacks. diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index 642f1874d..6da820f5e 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -14,7 +14,6 @@ import contextlib import fnmatch -import functools import inspect import re import uuid @@ -27,9 +26,6 @@ from openstack.cloud import exc -_decorated_methods = [] - - def _dictify_resource(resource): if isinstance(resource, list): return [_dictify_resource(r) for r in resource] @@ -230,45 +226,6 @@ def func_wrapper(func, *args, **kwargs): return func_wrapper -def _func_wrap(f): - # NOTE(morgan): This extra wrapper is intended to eliminate ever - # passing a bound method to dogpile.cache's cache_on_arguments. In - # 0.7.0 and later it is impossible to pass bound methods to the - # decorator. This was introduced when utilizing the decorate module in - # lieu of a direct wrap implementation. - @functools.wraps(f) - def inner(*args, **kwargs): - return f(*args, **kwargs) - - return inner - - -def cache_on_arguments(*cache_on_args, **cache_on_kwargs): - _cache_name = cache_on_kwargs.pop('resource', None) - - def _inner_cache_on_arguments(func): - def _cache_decorator(obj, *args, **kwargs): - the_method = obj._get_cache(_cache_name).cache_on_arguments( - *cache_on_args, **cache_on_kwargs - )(_func_wrap(func.__get__(obj, type(obj)))) - return the_method(*args, **kwargs) - - def invalidate(obj, *args, **kwargs): - return ( - obj._get_cache(_cache_name) - .cache_on_arguments()(func) - .invalidate(*args, **kwargs) - ) - - _cache_decorator.invalidate = invalidate - _cache_decorator.func = func - _decorated_methods.append(func.__name__) - - return _cache_decorator - - return _inner_cache_on_arguments - - @contextlib.contextmanager def openstacksdk_exceptions(error_message=None): """Context manager for dealing with openstack exceptions. diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 13009d7c9..7d847dade 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -101,32 +101,6 @@ def __init__(self): else: self.cache_enabled = False - # TODO(gtema): delete it with the standalone cloud layer caching - - def _fake_invalidate(unused): - pass - - class _FakeCache: - def invalidate(self): - pass - - # Don't cache list_servers if we're not caching things. - # Replace this with a more specific cache configuration - # soon. - self._cache = _FakeCache() - # Undecorate cache decorated methods. Otherwise the call stacks - # wind up being stupidly long and hard to debug - for method in _utils._decorated_methods: - meth_obj = getattr(self, method, None) - if not meth_obj: - continue - if hasattr(meth_obj, 'invalidate') and hasattr( - meth_obj, 'func' - ): - new_func = functools.partial(meth_obj.func, self) - new_func.invalidate = _fake_invalidate - setattr(self, method, new_func) - # Uncoditionally create cache even with a "null" backend self._cache = self._make_cache( cache_class, cache_expiration_time, cache_arguments @@ -323,12 +297,6 @@ def generate_key(*args, **kwargs): return generate_key - def _get_cache(self, resource_name): - if resource_name and resource_name in self._resource_caches: - return self._resource_caches[resource_name] - else: - return self._cache - def pprint(self, resource): """Wrapper around pprint that groks munch objects""" # import late since this is a utility function diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index e9917784b..53ea68a4d 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -962,7 +962,6 @@ def create_server_image( server = self._get_resource(_server.Server, server) image_id = server.create_image(self, name, metadata) - self._connection.list_images.invalidate(self) image = self._connection.get_image(image_id) if not wait: diff --git a/openstack/image/v1/_proxy.py b/openstack/image/v1/_proxy.py index 7923467c3..21cf626eb 100644 --- a/openstack/image/v1/_proxy.py +++ b/openstack/image/v1/_proxy.py @@ -240,8 +240,6 @@ def create_image( else: image = self._create(_image.Image, name=name, **kwargs) - self._connection._get_cache(None).invalidate() - return image def upload_image(self, **attrs): @@ -441,7 +439,6 @@ def _update_image_properties(self, image, meta, properties): if not img_props: return False self.put('/images/{id}'.format(id=image.id), headers=img_props) - self._connection.list_images.invalidate(self._connection) return True def update_image_properties( diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 3a1e04876..fd2e6741f 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -341,8 +341,6 @@ def create_image( image_kwargs['name'] = name image = self._create(_image.Image, **image_kwargs) - self._connection._get_cache(None).invalidate() - return image def import_image( @@ -734,7 +732,6 @@ def _upload_image_task( } glance_task = self.create_task(**task_args) - self._connection.list_images.invalidate(self) if wait: start = time.time() @@ -771,7 +768,6 @@ def _upload_image_task( # Clean up after ourselves. The object we created is not # needed after the import is done. self._connection.delete_object(container, name) - self._connection.list_images.invalidate(self) return image else: return glance_task @@ -964,8 +960,6 @@ def update_image_properties( self.update_image(image, **img_props) - self._connection.list_images.invalidate(self._connection) - return True def add_tag(self, image, tag): diff --git a/openstack/proxy.py b/openstack/proxy.py index e96e2f6d8..06574cafe 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -208,6 +208,7 @@ def request( self._report_stats(None, url, method, e) raise + # TODO(stephenfin): service_type is unused and should be dropped @functools.lru_cache(maxsize=256) def _extract_name(self, url, service_type=None, project_id=None): """Produce a key name to use in logging/metrics from the URL path. diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py deleted file mode 100644 index ea95b2707..000000000 --- a/openstack/tests/unit/cloud/test_caching.py +++ /dev/null @@ -1,807 +0,0 @@ -# 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 testtools -from testscenarios import load_tests_apply_scenarios as load_tests # noqa - -import openstack -from openstack.block_storage.v3 import volume as _volume -import openstack.cloud -from openstack.cloud import meta -from openstack.compute.v2 import flavor as _flavor -from openstack import exceptions -from openstack.identity.v3 import project as _project -from openstack.identity.v3 import user as _user -from openstack.image.v2 import image as _image -from openstack.network.v2 import port as _port -from openstack.test import fakes as _fakes -from openstack.tests import fakes -from openstack.tests.unit import base -from openstack.tests.unit.cloud import test_port - - -# Mock out the gettext function so that the task schema can be copypasta -def _(msg): - return msg - - -_TASK_PROPERTIES = { - "id": { - "description": _("An identifier for the task"), - "pattern": _( - '^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}' - '-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$' - ), - "type": "string", - }, - "type": { - "description": _("The type of task represented by this content"), - "enum": [ - "import", - ], - "type": "string", - }, - "status": { - "description": _("The current status of this task"), - "enum": ["pending", "processing", "success", "failure"], - "type": "string", - }, - "input": { - "description": _("The parameters required by task, JSON blob"), - "type": ["null", "object"], - }, - "result": { - "description": _("The result of current task, JSON blob"), - "type": ["null", "object"], - }, - "owner": { - "description": _("An identifier for the owner of this task"), - "type": "string", - }, - "message": { - "description": _( - "Human-readable informative message only included" - " when appropriate (usually on failure)" - ), - "type": "string", - }, - "expires_at": { - "description": _( - "Datetime when this resource would be subject to removal" - ), - "type": ["null", "string"], - }, - "created_at": { - "description": _("Datetime when this resource was created"), - "type": "string", - }, - "updated_at": { - "description": _("Datetime when this resource was updated"), - "type": "string", - }, - 'self': {'type': 'string'}, - 'schema': {'type': 'string'}, -} -_TASK_SCHEMA = dict( - name='Task', - properties=_TASK_PROPERTIES, - additionalProperties=False, -) - - -class TestMemoryCache(base.TestCase): - def setUp(self): - super(TestMemoryCache, self).setUp( - cloud_config_fixture='clouds_cache.yaml' - ) - - def _compare_images(self, exp, real): - self.assertDictEqual( - _image.Image(**exp).to_dict(computed=False), - real.to_dict(computed=False), - ) - - def _compare_volumes(self, exp, real): - self.assertDictEqual( - _volume.Volume(**exp).to_dict(computed=False), - real.to_dict(computed=False), - ) - - def test_openstack_cloud(self): - self.assertIsInstance(self.cloud, openstack.connection.Connection) - - def _compare_projects(self, exp, real): - self.assertDictEqual( - _project.Project(**exp).to_dict(computed=False), - real.to_dict(computed=False), - ) - - def _compare_users(self, exp, real): - self.assertDictEqual( - _user.User(**exp).to_dict(computed=False), - real.to_dict(computed=False), - ) - - def test_list_projects_v3(self): - project_one = self._get_project_data() - project_two = self._get_project_data() - project_list = [project_one, project_two] - - first_response = {'projects': [project_one.json_response['project']]} - second_response = { - 'projects': [p.json_response['project'] for p in project_list] - } - - mock_uri = self.get_mock_url( - service_type='identity', resource='projects', base_url_append='v3' - ) - - self.register_uris( - [ - dict( - method='GET', - uri=mock_uri, - status_code=200, - json=first_response, - ), - dict( - method='GET', - uri=mock_uri, - status_code=200, - json=second_response, - ), - ] - ) - - for a, b in zip( - first_response['projects'], self.cloud.list_projects() - ): - self._compare_projects(a, b) - - # invalidate the list_projects cache - self.cloud.list_projects.invalidate(self.cloud) - - for a, b in zip( - second_response['projects'], self.cloud.list_projects() - ): - self._compare_projects(a, b) - - self.assert_calls() - - def test_list_volumes(self): - fake_volume = fakes.FakeVolume( - 'volume1', 'available', 'Volume 1 Display Name' - ) - fake_volume_dict = meta.obj_to_munch(fake_volume) - fake_volume2 = fakes.FakeVolume( - 'volume2', 'available', 'Volume 2 Display Name' - ) - fake_volume2_dict = meta.obj_to_munch(fake_volume2) - self.register_uris( - [ - self.get_cinder_discovery_mock_dict(), - dict( - method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', 'detail'] - ), - json={'volumes': [fake_volume_dict]}, - ), - dict( - method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', 'detail'] - ), - json={'volumes': [fake_volume_dict, fake_volume2_dict]}, - ), - ] - ) - - for a, b in zip([fake_volume_dict], self.cloud.list_volumes()): - self._compare_volumes(a, b) - # this call should hit the cache - for a, b in zip([fake_volume_dict], self.cloud.list_volumes()): - self._compare_volumes(a, b) - self.cloud.list_volumes.invalidate(self.cloud) - for a, b in zip( - [fake_volume_dict, fake_volume2_dict], self.cloud.list_volumes() - ): - self._compare_volumes(a, b) - self.assert_calls() - - def test_list_volumes_creating_invalidates(self): - fake_volume = fakes.FakeVolume( - 'volume1', 'creating', 'Volume 1 Display Name' - ) - fake_volume_dict = meta.obj_to_munch(fake_volume) - fake_volume2 = fakes.FakeVolume( - 'volume2', 'available', 'Volume 2 Display Name' - ) - fake_volume2_dict = meta.obj_to_munch(fake_volume2) - self.register_uris( - [ - self.get_cinder_discovery_mock_dict(), - dict( - method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', 'detail'] - ), - json={'volumes': [fake_volume_dict]}, - ), - dict( - method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', 'detail'] - ), - json={'volumes': [fake_volume_dict, fake_volume2_dict]}, - ), - ] - ) - for a, b in zip([fake_volume_dict], self.cloud.list_volumes()): - self._compare_volumes(a, b) - for a, b in zip( - [fake_volume_dict, fake_volume2_dict], self.cloud.list_volumes() - ): - self._compare_volumes(a, b) - self.assert_calls() - - def test_create_volume_invalidates(self): - fake_volb4 = meta.obj_to_munch( - fakes.FakeVolume('volume1', 'available', '') - ) - _id = '12345' - fake_vol_creating = meta.obj_to_munch( - fakes.FakeVolume(_id, 'creating', '') - ) - fake_vol_avail = meta.obj_to_munch( - fakes.FakeVolume(_id, 'available', '') - ) - - def now_deleting(request, context): - fake_vol_avail['status'] = 'deleting' - - self.register_uris( - [ - self.get_cinder_discovery_mock_dict(), - dict( - method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', 'detail'] - ), - json={'volumes': [fake_volb4]}, - ), - dict( - method='POST', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes'] - ), - json={'volume': fake_vol_creating}, - ), - dict( - method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', _id] - ), - json={'volume': fake_vol_creating}, - ), - dict( - method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', _id] - ), - json={'volume': fake_vol_avail}, - ), - dict( - method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', 'detail'] - ), - json={'volumes': [fake_volb4, fake_vol_avail]}, - ), - dict( - method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', _id] - ), - json={'volume': fake_vol_avail}, - ), - dict( - method='DELETE', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', _id] - ), - json=now_deleting, - ), - dict( - method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', _id] - ), - status_code=404, - ), - dict( - method='GET', - uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', 'detail'] - ), - json={'volumes': [fake_volb4, fake_vol_avail]}, - ), - ] - ) - - for a, b in zip([fake_volb4], self.cloud.list_volumes()): - self._compare_volumes(a, b) - volume = dict( - display_name='junk_vol', - size=1, - display_description='test junk volume', - ) - self.cloud.create_volume(wait=True, timeout=2, **volume) - # If cache was not invalidated, we would not see our own volume here - # because the first volume was available and thus would already be - # cached. - for a, b in zip( - [fake_volb4, fake_vol_avail], self.cloud.list_volumes() - ): - self._compare_volumes(a, b) - self.cloud.delete_volume(_id) - # And now delete and check same thing since list is cached as all - # available - for a, b in zip([fake_volb4], self.cloud.list_volumes()): - self._compare_volumes(a, b) - self.assert_calls() - - def test_list_users(self): - user_data = self._get_user_data(email='test@example.com') - self.register_uris( - [ - dict( - method='GET', - uri=self.get_mock_url( - service_type='identity', - resource='users', - base_url_append='v3', - ), - status_code=200, - json={'users': [user_data.json_response['user']]}, - ) - ] - ) - users = self.cloud.list_users() - self.assertEqual(1, len(users)) - self.assertEqual(user_data.user_id, users[0]['id']) - self.assertEqual(user_data.name, users[0]['name']) - self.assertEqual(user_data.email, users[0]['email']) - self.assert_calls() - - def test_modify_user_invalidates_cache(self): - self.use_keystone_v2() - - user_data = self._get_user_data(email='test@example.com') - new_resp = {'user': user_data.json_response['user'].copy()} - new_resp['user']['email'] = 'Nope@Nope.Nope' - new_req = {'user': {'email': new_resp['user']['email']}} - - mock_users_url = self.get_mock_url( - service_type='identity', interface='admin', resource='users' - ) - mock_user_resource_url = self.get_mock_url( - service_type='identity', - interface='admin', - resource='users', - append=[user_data.user_id], - ) - - empty_user_list_resp = {'users': []} - users_list_resp = {'users': [user_data.json_response['user']]} - updated_users_list_resp = {'users': [new_resp['user']]} - - # Password is None in the original create below - del user_data.json_request['user']['password'] - - uris_to_mock = [ - # Inital User List is Empty - dict( - method='GET', - uri=mock_users_url, - status_code=200, - json=empty_user_list_resp, - ), - # POST to create the user - # GET to get the user data after POST - dict( - method='POST', - uri=mock_users_url, - status_code=200, - json=user_data.json_response, - validate=dict(json=user_data.json_request), - ), - # List Users Call - dict( - method='GET', - uri=mock_users_url, - status_code=200, - json=users_list_resp, - ), - # List users to get ID for update - # Get user using user_id from list - # Update user - # Get updated user - dict( - method='GET', - uri=mock_users_url, - status_code=200, - json=users_list_resp, - ), - dict( - method='PUT', - uri=mock_user_resource_url, - status_code=200, - json=new_resp, - validate=dict(json=new_req), - ), - # List Users Call - dict( - method='GET', - uri=mock_users_url, - status_code=200, - json=updated_users_list_resp, - ), - # List User to get ID for delete - # delete user - dict( - method='GET', - uri=mock_users_url, - status_code=200, - json=updated_users_list_resp, - ), - dict(method='DELETE', uri=mock_user_resource_url, status_code=204), - # List Users Call (empty post delete) - dict( - method='GET', - uri=mock_users_url, - status_code=200, - json=empty_user_list_resp, - ), - ] - - self.register_uris(uris_to_mock) - - # first cache an empty list - self.assertEqual([], self.cloud.list_users()) - - # now add one - created = self.cloud.create_user( - name=user_data.name, email=user_data.email - ) - self.assertEqual(user_data.user_id, created['id']) - self.assertEqual(user_data.name, created['name']) - self.assertEqual(user_data.email, created['email']) - # Cache should have been invalidated - users = self.cloud.list_users() - self.assertEqual(user_data.user_id, users[0]['id']) - self.assertEqual(user_data.name, users[0]['name']) - self.assertEqual(user_data.email, users[0]['email']) - - # Update and check to see if it is updated - updated = self.cloud.update_user( - user_data.user_id, email=new_resp['user']['email'] - ) - self.assertEqual(user_data.user_id, updated.id) - self.assertEqual(user_data.name, updated.name) - self.assertEqual(new_resp['user']['email'], updated.email) - users = self.cloud.list_users() - self.assertEqual(1, len(users)) - self.assertEqual(user_data.user_id, users[0]['id']) - self.assertEqual(user_data.name, users[0]['name']) - self.assertEqual(new_resp['user']['email'], users[0]['email']) - # Now delete and ensure it disappears - self.cloud.delete_user(user_data.user_id) - self.assertEqual([], self.cloud.list_users()) - self.assert_calls() - - def test_list_flavors(self): - mock_uri = '{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ) - flavors = list(_fakes.generate_fake_resources(_flavor.Flavor, count=2)) - - uris_to_mock = [ - dict( - method='GET', - uri=mock_uri, - validate=dict( - headers={'OpenStack-API-Version': 'compute 2.53'} - ), - json={'flavors': []}, - ), - dict( - method='GET', - uri=mock_uri, - validate=dict( - headers={'OpenStack-API-Version': 'compute 2.53'} - ), - json={'flavors': flavors}, - ), - ] - self.use_compute_discovery() - - self.register_uris(uris_to_mock) - - self.assertEqual([], self.cloud.list_flavors()) - - self.assertEqual([], self.cloud.list_flavors()) - - self.cloud.list_flavors.invalidate(self.cloud) - self.assertResourceListEqual( - self.cloud.list_flavors(), flavors, _flavor.Flavor - ) - - self.assert_calls() - - def test_list_images(self): - self.use_glance() - fake_image = fakes.make_fake_image(image_id='42') - - self.register_uris( - [ - dict( - method='GET', - uri=self.get_mock_url( - 'image', 'public', append=['v2', 'images'] - ), - json={'images': []}, - ), - dict( - method='GET', - uri=self.get_mock_url( - 'image', 'public', append=['v2', 'images'] - ), - json={'images': [fake_image]}, - ), - ] - ) - - self.assertEqual([], self.cloud.list_images()) - self.assertEqual([], self.cloud.list_images()) - self.cloud.list_images.invalidate(self.cloud) - [ - self._compare_images(a, b) - for a, b in zip([fake_image], self.cloud.list_images()) - ] - - self.assert_calls() - - def test_list_images_caches_deleted_status(self): - self.use_glance() - - deleted_image_id = self.getUniqueString() - deleted_image = fakes.make_fake_image( - image_id=deleted_image_id, status='deleted' - ) - active_image_id = self.getUniqueString() - active_image = fakes.make_fake_image(image_id=active_image_id) - list_return = {'images': [active_image, deleted_image]} - self.register_uris( - [ - dict( - method='GET', - uri='https://image.example.com/v2/images', - json=list_return, - ), - ] - ) - - [ - self._compare_images(a, b) - for a, b in zip([active_image], self.cloud.list_images()) - ] - - [ - self._compare_images(a, b) - for a, b in zip([active_image], self.cloud.list_images()) - ] - - # We should only have one call - self.assert_calls() - - def test_cache_no_cloud_name(self): - self.use_glance() - - self.cloud.name = None - fi = fakes.make_fake_image(image_id=self.getUniqueString()) - fi2 = fakes.make_fake_image(image_id=self.getUniqueString()) - - self.register_uris( - [ - dict( - method='GET', - uri='https://image.example.com/v2/images', - json={'images': [fi]}, - ), - dict( - method='GET', - uri='https://image.example.com/v2/images', - json={'images': [fi, fi2]}, - ), - ] - ) - - [ - self._compare_images(a, b) - for a, b in zip([fi], self.cloud.list_images()) - ] - - # Now test that the list was cached - [ - self._compare_images(a, b) - for a, b in zip([fi], self.cloud.list_images()) - ] - - # Invalidation too - self.cloud.list_images.invalidate(self.cloud) - [ - self._compare_images(a, b) - for a, b in zip([fi, fi2], self.cloud.list_images()) - ] - - def test_list_ports_filtered(self): - down_port = test_port.TestPort.mock_neutron_port_create_rep['port'] - active_port = down_port.copy() - active_port['status'] = 'ACTIVE' - # We're testing to make sure a query string is passed when we're - # caching (cache by url), and that the results are still filtered. - self.register_uris( - [ - dict( - method='GET', - uri=self.get_mock_url( - 'network', - 'public', - append=['v2.0', 'ports'], - qs_elements=['status=DOWN'], - ), - json={ - 'ports': [ - down_port, - active_port, - ] - }, - ), - ] - ) - ports = self.cloud.list_ports(filters={'status': 'DOWN'}) - for a, b in zip([down_port], ports): - self.assertDictEqual( - _port.Port(**a).to_dict(computed=False), - b.to_dict(computed=False), - ) - self.assert_calls() - - -class TestCacheIgnoresQueuedStatus(base.TestCase): - scenarios = [ - ('queued', dict(status='queued')), - ('saving', dict(status='saving')), - ('pending_delete', dict(status='pending_delete')), - ] - - def setUp(self): - super(TestCacheIgnoresQueuedStatus, self).setUp( - cloud_config_fixture='clouds_cache.yaml' - ) - self.use_glance() - active_image_id = self.getUniqueString() - self.active_image = fakes.make_fake_image( - image_id=active_image_id, status=self.status - ) - self.active_list_return = {'images': [self.active_image]} - steady_image_id = self.getUniqueString() - self.steady_image = fakes.make_fake_image(image_id=steady_image_id) - self.steady_list_return = { - 'images': [self.active_image, self.steady_image] - } - - def _compare_images(self, exp, real): - self.assertDictEqual( - _image.Image(**exp).to_dict(computed=False), - real.to_dict(computed=False), - ) - - def test_list_images_ignores_pending_status(self): - self.register_uris( - [ - dict( - method='GET', - uri='https://image.example.com/v2/images', - json=self.active_list_return, - ), - dict( - method='GET', - uri='https://image.example.com/v2/images', - json=self.steady_list_return, - ), - ] - ) - - [ - self._compare_images(a, b) - for a, b in zip([self.active_image], self.cloud.list_images()) - ] - - # Should expect steady_image to appear if active wasn't cached - [ - self._compare_images(a, b) - for a, b in zip( - [self.active_image, self.steady_image], - self.cloud.list_images(), - ) - ] - - -class TestCacheSteadyStatus(base.TestCase): - scenarios = [ - ('active', dict(status='active')), - ('killed', dict(status='killed')), - ] - - def setUp(self): - super(TestCacheSteadyStatus, self).setUp( - cloud_config_fixture='clouds_cache.yaml' - ) - self.use_glance() - active_image_id = self.getUniqueString() - self.active_image = fakes.make_fake_image( - image_id=active_image_id, status=self.status - ) - self.active_list_return = {'images': [self.active_image]} - - def _compare_images(self, exp, real): - self.assertDictEqual( - _image.Image(**exp).to_dict(computed=False), - real.to_dict(computed=False), - ) - - def test_list_images_caches_steady_status(self): - self.register_uris( - [ - dict( - method='GET', - uri='https://image.example.com/v2/images', - json=self.active_list_return, - ), - ] - ) - - [ - self._compare_images(a, b) - for a, b in zip([self.active_image], self.cloud.list_images()) - ] - - [ - self._compare_images(a, b) - for a, b in zip([self.active_image], self.cloud.list_images()) - ] - - # We should only have one call - self.assert_calls() - - -class TestBogusAuth(base.TestCase): - def setUp(self): - super(TestBogusAuth, self).setUp( - cloud_config_fixture='clouds_cache.yaml' - ) - - def test_get_auth_bogus(self): - with testtools.ExpectedException(exceptions.ConfigException): - openstack.connect(cloud='_bogus_test_', config=self.config) diff --git a/releasenotes/notes/remove-cloud-caching-layer-2b0384870a45e8a3.yaml b/releasenotes/notes/remove-cloud-caching-layer-2b0384870a45e8a3.yaml new file mode 100644 index 000000000..e8f60b032 --- /dev/null +++ b/releasenotes/notes/remove-cloud-caching-layer-2b0384870a45e8a3.yaml @@ -0,0 +1,7 @@ +--- +upgrade: + - | + The cloud-layer caching functionality has been removed in favour of the + proxy-layer caching functionality first introduced in openstacksdk 1.0.0. + This migration to proxy-layer caching was designed to be transparent to + end-users and there should be no user-facing impact from this removal. From 21af0cee98deb0809d8103038f8eb5bda8beb122 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 31 Aug 2023 15:24:46 +0100 Subject: [PATCH 3385/3836] tests: Remove references to shade Change-Id: I66afa38437a23f9346463f2a5544a91d5267e6d0 Signed-off-by: Stephen Finucane --- openstack/tests/functional/cloud/test_aggregate.py | 2 +- openstack/tests/functional/cloud/test_clustering.py | 2 +- openstack/tests/functional/cloud/test_coe_clusters.py | 2 +- openstack/tests/functional/cloud/test_compute.py | 2 +- openstack/tests/functional/cloud/test_domain.py | 2 +- openstack/tests/functional/cloud/test_endpoints.py | 2 +- openstack/tests/functional/cloud/test_flavor.py | 2 +- openstack/tests/functional/cloud/test_groups.py | 2 +- openstack/tests/functional/cloud/test_identity.py | 2 +- openstack/tests/functional/cloud/test_image.py | 2 +- openstack/tests/functional/cloud/test_inventory.py | 2 +- openstack/tests/functional/cloud/test_keypairs.py | 2 +- openstack/tests/functional/cloud/test_limits.py | 2 +- openstack/tests/functional/cloud/test_magnum_services.py | 2 +- openstack/tests/functional/cloud/test_network.py | 2 +- openstack/tests/functional/cloud/test_object.py | 2 +- openstack/tests/functional/cloud/test_port.py | 2 +- openstack/tests/functional/cloud/test_project.py | 2 +- .../tests/functional/cloud/test_qos_bandwidth_limit_rule.py | 2 +- openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py | 2 +- .../tests/functional/cloud/test_qos_minimum_bandwidth_rule.py | 2 +- openstack/tests/functional/cloud/test_qos_policy.py | 2 +- openstack/tests/functional/cloud/test_quotas.py | 2 +- openstack/tests/functional/cloud/test_recordset.py | 2 +- openstack/tests/functional/cloud/test_router.py | 2 +- openstack/tests/functional/cloud/test_security_groups.py | 2 +- openstack/tests/functional/cloud/test_server_group.py | 2 +- openstack/tests/functional/cloud/test_services.py | 2 +- openstack/tests/functional/cloud/test_stack.py | 2 +- openstack/tests/functional/cloud/test_users.py | 2 +- openstack/tests/functional/cloud/test_volume.py | 2 +- openstack/tests/functional/cloud/test_volume_type.py | 2 +- openstack/tests/functional/cloud/test_zone.py | 2 +- 33 files changed, 33 insertions(+), 33 deletions(-) diff --git a/openstack/tests/functional/cloud/test_aggregate.py b/openstack/tests/functional/cloud/test_aggregate.py index abd920e62..9ffe953b4 100644 --- a/openstack/tests/functional/cloud/test_aggregate.py +++ b/openstack/tests/functional/cloud/test_aggregate.py @@ -14,7 +14,7 @@ test_aggregate ---------------------------------- -Functional tests for `shade` aggregate resource. +Functional tests for aggregate resource. """ from openstack.tests.functional import base diff --git a/openstack/tests/functional/cloud/test_clustering.py b/openstack/tests/functional/cloud/test_clustering.py index 96bb3302a..dcb273535 100644 --- a/openstack/tests/functional/cloud/test_clustering.py +++ b/openstack/tests/functional/cloud/test_clustering.py @@ -14,7 +14,7 @@ test_clustering ---------------------------------- -Functional tests for `shade` clustering methods. +Functional tests for clustering methods. """ import time diff --git a/openstack/tests/functional/cloud/test_coe_clusters.py b/openstack/tests/functional/cloud/test_coe_clusters.py index 317d5f03a..be0b1aab2 100644 --- a/openstack/tests/functional/cloud/test_coe_clusters.py +++ b/openstack/tests/functional/cloud/test_coe_clusters.py @@ -14,7 +14,7 @@ test_coe_clusters ---------------------------------- -Functional tests for `shade` COE clusters methods. +Functional tests for COE clusters methods. """ from openstack.tests.functional import base diff --git a/openstack/tests/functional/cloud/test_compute.py b/openstack/tests/functional/cloud/test_compute.py index 0a5f1c934..34a08b5ea 100644 --- a/openstack/tests/functional/cloud/test_compute.py +++ b/openstack/tests/functional/cloud/test_compute.py @@ -14,7 +14,7 @@ test_compute ---------------------------------- -Functional tests for `shade` compute methods. +Functional tests for compute methods. """ import datetime diff --git a/openstack/tests/functional/cloud/test_domain.py b/openstack/tests/functional/cloud/test_domain.py index f5f2ea210..7053b9f03 100644 --- a/openstack/tests/functional/cloud/test_domain.py +++ b/openstack/tests/functional/cloud/test_domain.py @@ -14,7 +14,7 @@ test_domain ---------------------------------- -Functional tests for `shade` keystone domain resource. +Functional tests for keystone domain resource. """ import openstack.cloud diff --git a/openstack/tests/functional/cloud/test_endpoints.py b/openstack/tests/functional/cloud/test_endpoints.py index c7e9e882c..9b2713c0d 100644 --- a/openstack/tests/functional/cloud/test_endpoints.py +++ b/openstack/tests/functional/cloud/test_endpoints.py @@ -16,7 +16,7 @@ test_endpoint ---------------------------------- -Functional tests for `shade` endpoint resource. +Functional tests for endpoint resource. """ import random diff --git a/openstack/tests/functional/cloud/test_flavor.py b/openstack/tests/functional/cloud/test_flavor.py index 5d3e94241..c1ffc136f 100644 --- a/openstack/tests/functional/cloud/test_flavor.py +++ b/openstack/tests/functional/cloud/test_flavor.py @@ -16,7 +16,7 @@ test_flavor ---------------------------------- -Functional tests for `shade` flavor resource. +Functional tests for flavor resource. """ from openstack.cloud.exc import OpenStackCloudException diff --git a/openstack/tests/functional/cloud/test_groups.py b/openstack/tests/functional/cloud/test_groups.py index 9415b337f..729b4a1d9 100644 --- a/openstack/tests/functional/cloud/test_groups.py +++ b/openstack/tests/functional/cloud/test_groups.py @@ -14,7 +14,7 @@ test_groups ---------------------------------- -Functional tests for `shade` keystone group resource. +Functional tests for keystone group resource. """ import openstack.cloud diff --git a/openstack/tests/functional/cloud/test_identity.py b/openstack/tests/functional/cloud/test_identity.py index 5fc004628..ab5a0a630 100644 --- a/openstack/tests/functional/cloud/test_identity.py +++ b/openstack/tests/functional/cloud/test_identity.py @@ -14,7 +14,7 @@ test_identity ---------------------------------- -Functional tests for `shade` identity methods. +Functional tests for identity methods. """ import random diff --git a/openstack/tests/functional/cloud/test_image.py b/openstack/tests/functional/cloud/test_image.py index 489f2c1a2..a13b31cbf 100644 --- a/openstack/tests/functional/cloud/test_image.py +++ b/openstack/tests/functional/cloud/test_image.py @@ -14,7 +14,7 @@ test_compute ---------------------------------- -Functional tests for `shade` image methods. +Functional tests for image methods. """ import filecmp diff --git a/openstack/tests/functional/cloud/test_inventory.py b/openstack/tests/functional/cloud/test_inventory.py index c5235fc0b..532080b7e 100644 --- a/openstack/tests/functional/cloud/test_inventory.py +++ b/openstack/tests/functional/cloud/test_inventory.py @@ -16,7 +16,7 @@ test_inventory ---------------------------------- -Functional tests for `shade` inventory methods. +Functional tests for inventory methods. """ from openstack.cloud import inventory diff --git a/openstack/tests/functional/cloud/test_keypairs.py b/openstack/tests/functional/cloud/test_keypairs.py index 64b58d186..3fd920486 100644 --- a/openstack/tests/functional/cloud/test_keypairs.py +++ b/openstack/tests/functional/cloud/test_keypairs.py @@ -14,7 +14,7 @@ test_keypairs ---------------------------------- -Functional tests for `shade` keypairs methods +Functional tests for keypairs methods """ from openstack.tests import fakes from openstack.tests.functional import base diff --git a/openstack/tests/functional/cloud/test_limits.py b/openstack/tests/functional/cloud/test_limits.py index 5e9f7bc83..13838599c 100644 --- a/openstack/tests/functional/cloud/test_limits.py +++ b/openstack/tests/functional/cloud/test_limits.py @@ -14,7 +14,7 @@ test_limits ---------------------------------- -Functional tests for `shade` limits method +Functional tests for limits method """ from openstack.compute.v2 import limits as _limits from openstack.tests.functional import base diff --git a/openstack/tests/functional/cloud/test_magnum_services.py b/openstack/tests/functional/cloud/test_magnum_services.py index 1690b5d23..c3a526f72 100644 --- a/openstack/tests/functional/cloud/test_magnum_services.py +++ b/openstack/tests/functional/cloud/test_magnum_services.py @@ -14,7 +14,7 @@ test_magnum_services -------------------- -Functional tests for `shade` services method. +Functional tests for services method. """ from openstack.tests.functional import base diff --git a/openstack/tests/functional/cloud/test_network.py b/openstack/tests/functional/cloud/test_network.py index 60c3a0b74..ee6508cd4 100644 --- a/openstack/tests/functional/cloud/test_network.py +++ b/openstack/tests/functional/cloud/test_network.py @@ -14,7 +14,7 @@ test_network ---------------------------------- -Functional tests for `shade` network methods. +Functional tests for network methods. """ from openstack.cloud.exc import OpenStackCloudException diff --git a/openstack/tests/functional/cloud/test_object.py b/openstack/tests/functional/cloud/test_object.py index 0b3e274d7..6212d783d 100644 --- a/openstack/tests/functional/cloud/test_object.py +++ b/openstack/tests/functional/cloud/test_object.py @@ -14,7 +14,7 @@ test_object ---------------------------------- -Functional tests for `shade` object methods. +Functional tests for object methods. """ import random diff --git a/openstack/tests/functional/cloud/test_port.py b/openstack/tests/functional/cloud/test_port.py index c36923334..51afd6596 100644 --- a/openstack/tests/functional/cloud/test_port.py +++ b/openstack/tests/functional/cloud/test_port.py @@ -16,7 +16,7 @@ test_port ---------------------------------- -Functional tests for `shade` port resource. +Functional tests for port resource. """ import random diff --git a/openstack/tests/functional/cloud/test_project.py b/openstack/tests/functional/cloud/test_project.py index d4a4647c6..bb7d5f816 100644 --- a/openstack/tests/functional/cloud/test_project.py +++ b/openstack/tests/functional/cloud/test_project.py @@ -16,7 +16,7 @@ test_project ---------------------------------- -Functional tests for `shade` project resource. +Functional tests for project resource. """ import pprint diff --git a/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py b/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py index 3f618698c..0fb323f7d 100644 --- a/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py @@ -15,7 +15,7 @@ test_qos_bandwidth_limit_rule ---------------------------------- -Functional tests for `shade`QoS bandwidth limit methods. +Functional tests for QoS bandwidth limit methods. """ from openstack.cloud.exc import OpenStackCloudException diff --git a/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py b/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py index b5f100010..69822c838 100644 --- a/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py +++ b/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py @@ -15,7 +15,7 @@ test_qos_dscp_marking_rule ---------------------------------- -Functional tests for `shade`QoS DSCP marking rule methods. +Functional tests for QoS DSCP marking rule methods. """ from openstack.cloud.exc import OpenStackCloudException diff --git a/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py b/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py index 43d3c5b9c..d70387a63 100644 --- a/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py +++ b/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py @@ -15,7 +15,7 @@ test_qos_minumum_bandwidth_rule ---------------------------------- -Functional tests for `shade`QoS minimum bandwidth methods. +Functional tests for QoS minimum bandwidth methods. """ from openstack.cloud.exc import OpenStackCloudException diff --git a/openstack/tests/functional/cloud/test_qos_policy.py b/openstack/tests/functional/cloud/test_qos_policy.py index a341f9a12..ff366e4d2 100644 --- a/openstack/tests/functional/cloud/test_qos_policy.py +++ b/openstack/tests/functional/cloud/test_qos_policy.py @@ -15,7 +15,7 @@ test_qos_policy ---------------------------------- -Functional tests for `shade`QoS policies methods. +Functional tests for QoS policies methods. """ from openstack.cloud.exc import OpenStackCloudException diff --git a/openstack/tests/functional/cloud/test_quotas.py b/openstack/tests/functional/cloud/test_quotas.py index 15525f5d2..87e96b5fd 100644 --- a/openstack/tests/functional/cloud/test_quotas.py +++ b/openstack/tests/functional/cloud/test_quotas.py @@ -14,7 +14,7 @@ test_quotas ---------------------------------- -Functional tests for `shade` quotas methods. +Functional tests for quotas methods. """ from openstack.tests.functional import base diff --git a/openstack/tests/functional/cloud/test_recordset.py b/openstack/tests/functional/cloud/test_recordset.py index 709547a47..15574238e 100644 --- a/openstack/tests/functional/cloud/test_recordset.py +++ b/openstack/tests/functional/cloud/test_recordset.py @@ -14,7 +14,7 @@ test_recordset ---------------------------------- -Functional tests for `shade` recordset methods. +Functional tests for recordset methods. """ import random import string diff --git a/openstack/tests/functional/cloud/test_router.py b/openstack/tests/functional/cloud/test_router.py index 75dcf8d9c..1b0db5364 100644 --- a/openstack/tests/functional/cloud/test_router.py +++ b/openstack/tests/functional/cloud/test_router.py @@ -14,7 +14,7 @@ test_router ---------------------------------- -Functional tests for `shade` router methods. +Functional tests for router methods. """ import ipaddress diff --git a/openstack/tests/functional/cloud/test_security_groups.py b/openstack/tests/functional/cloud/test_security_groups.py index 3ad785c9a..e452ec62d 100644 --- a/openstack/tests/functional/cloud/test_security_groups.py +++ b/openstack/tests/functional/cloud/test_security_groups.py @@ -14,7 +14,7 @@ test_security_groups ---------------------------------- -Functional tests for `shade` security_groups resource. +Functional tests for security_groups resource. """ from openstack.tests.functional import base diff --git a/openstack/tests/functional/cloud/test_server_group.py b/openstack/tests/functional/cloud/test_server_group.py index 77d31d38d..9ec0f360e 100644 --- a/openstack/tests/functional/cloud/test_server_group.py +++ b/openstack/tests/functional/cloud/test_server_group.py @@ -14,7 +14,7 @@ test_server_group ---------------------------------- -Functional tests for `shade` server_group resource. +Functional tests for server_group resource. """ from openstack.tests.functional import base diff --git a/openstack/tests/functional/cloud/test_services.py b/openstack/tests/functional/cloud/test_services.py index e6c1902de..92aa5cc6c 100644 --- a/openstack/tests/functional/cloud/test_services.py +++ b/openstack/tests/functional/cloud/test_services.py @@ -16,7 +16,7 @@ test_services ---------------------------------- -Functional tests for `shade` service resource. +Functional tests for service resource. """ import random diff --git a/openstack/tests/functional/cloud/test_stack.py b/openstack/tests/functional/cloud/test_stack.py index 62cb13717..c3399e29a 100644 --- a/openstack/tests/functional/cloud/test_stack.py +++ b/openstack/tests/functional/cloud/test_stack.py @@ -14,7 +14,7 @@ test_stack ---------------------------------- -Functional tests for `shade` stack methods. +Functional tests for stack methods. """ import tempfile diff --git a/openstack/tests/functional/cloud/test_users.py b/openstack/tests/functional/cloud/test_users.py index adefb6043..45572a2f9 100644 --- a/openstack/tests/functional/cloud/test_users.py +++ b/openstack/tests/functional/cloud/test_users.py @@ -14,7 +14,7 @@ test_users ---------------------------------- -Functional tests for `shade` user methods. +Functional tests for user methods. """ from openstack.cloud.exc import OpenStackCloudException diff --git a/openstack/tests/functional/cloud/test_volume.py b/openstack/tests/functional/cloud/test_volume.py index 04c2467e9..551f75b9c 100644 --- a/openstack/tests/functional/cloud/test_volume.py +++ b/openstack/tests/functional/cloud/test_volume.py @@ -14,7 +14,7 @@ test_volume ---------------------------------- -Functional tests for `shade` block storage methods. +Functional tests for block storage methods. """ from fixtures import TimeoutException diff --git a/openstack/tests/functional/cloud/test_volume_type.py b/openstack/tests/functional/cloud/test_volume_type.py index ab8b0e5a6..51494ecbe 100644 --- a/openstack/tests/functional/cloud/test_volume_type.py +++ b/openstack/tests/functional/cloud/test_volume_type.py @@ -16,7 +16,7 @@ test_volume ---------------------------------- -Functional tests for `shade` block storage methods. +Functional tests for block storage methods. """ import testtools diff --git a/openstack/tests/functional/cloud/test_zone.py b/openstack/tests/functional/cloud/test_zone.py index c9625fe0f..a0b971b48 100644 --- a/openstack/tests/functional/cloud/test_zone.py +++ b/openstack/tests/functional/cloud/test_zone.py @@ -14,7 +14,7 @@ test_zone ---------------------------------- -Functional tests for `shade` zone methods. +Functional tests for zone methods. """ from testtools import content From eac81a65a94c761046ec4516754152a41bb2025b Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 1 Sep 2023 12:47:10 +0100 Subject: [PATCH 3386/3836] connection: Fix typo Change-Id: Ic69370182e44a327c192e0a77f68de38ba929a22 Signed-off-by: Stephen Finucane --- openstack/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/connection.py b/openstack/connection.py index b0f317395..9055077a2 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -150,7 +150,7 @@ sess = ks_loading.load_session_from_conf_options(CONF, 'neutron', auth=auth) conn = connection.Connection( - session=session, + session=sess, oslo_conf=CONF, ) From bf81c293433e35caa195d7c8a0721e3f1619de1f Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 5 Sep 2023 15:23:01 +0100 Subject: [PATCH 3387/3836] tests: Remove unused hook Change-Id: I9d8ee6dffadd1834b1e3c5c8e2f5bb391a2ffe86 Signed-off-by: Stephen Finucane --- .../functional/cloud/hooks/post_test_hook.sh | 54 ------------------- 1 file changed, 54 deletions(-) delete mode 100755 openstack/tests/functional/cloud/hooks/post_test_hook.sh diff --git a/openstack/tests/functional/cloud/hooks/post_test_hook.sh b/openstack/tests/functional/cloud/hooks/post_test_hook.sh deleted file mode 100755 index 8092a6114..000000000 --- a/openstack/tests/functional/cloud/hooks/post_test_hook.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash -x - -# 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. - -# TODO(shade) Rework for zuul v3 - -export OPENSTACKSDK_DIR="$BASE/new/shade" - -cd $OPENSTACKSDK_DIR -sudo chown -R jenkins:stack $OPENSTACKSDK_DIR - -CLOUDS_YAML=/etc/openstack/clouds.yaml - -if [ ! -e ${CLOUDS_YAML} ] -then - # stable/liberty had clouds.yaml in the home/base directory - sudo mkdir -p /etc/openstack - sudo cp $BASE/new/.config/openstack/clouds.yaml ${CLOUDS_YAML} - sudo chown -R jenkins:stack /etc/openstack -fi - -# Devstack runs both keystone v2 and v3. An environment variable is set -# within the shade keystone v2 job that tells us which version we should -# test against. -if [ ${OPENSTACKSDK_USE_KEYSTONE_V2:-0} -eq 1 ] -then - sudo sed -ie "s/identity_api_version: '3'/identity_api_version: '2.0'/g" $CLOUDS_YAML - sudo sed -ie '/^.*domain_id.*$/d' $CLOUDS_YAML -fi - -if [ "x$1" = "xtips" ] ; then - tox_env=functional-tips -else - tox_env=functional -fi -echo "Running shade functional test suite" -set +e -sudo -E -H -u jenkins tox -e$tox_env -EXIT_CODE=$? -sudo stestr last --subunit > $WORKSPACE/tempest.subunit -.tox/$tox_env/bin/pbr freeze -set -e - -exit $EXIT_CODE From a869170a09e847757e54ad3edfa2798473f9988d Mon Sep 17 00:00:00 2001 From: whoami-rajat Date: Sun, 6 Aug 2023 13:54:18 +0530 Subject: [PATCH 3388/3836] Fix: volume request body for scheduler hints Currently we pass 'OS-SCH-HNT:scheduler_hints' in the volume request body in the volume JSON object. However, the scheduler hints shouldn't be passed inside the volume JSON object and passed separately in the body. We need to do the same in SDK as done in cinderclient[1]. [1] https://opendev.org/openstack/python-cinderclient/src/commit/f64df99134c95bb5f9e7d24deeb7869cd828aa4b/cinderclient/v3/volumes.py#L126-L127 Story: #2010868 Task: #48557 Change-Id: Ie09ae37dbdaeaa03f7ccef92d466fa6f2a58ad5d --- openstack/block_storage/v3/volume.py | 15 ++++++++ .../unit/block_storage/v3/test_volume.py | 36 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index 5fa1337a6..eb412c40e 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -298,5 +298,20 @@ def terminate_attachment(self, session, connector): self._action(session, body) + def _prepare_request_body(self, patch, prepend_key): + body = self._body.dirty + # Scheduler hints is external to the standard volume request + # so pass it separately and not under the volume JSON object. + scheduler_hints = None + if 'OS-SCH-HNT:scheduler_hints' in body.keys(): + scheduler_hints = body.pop('OS-SCH-HNT:scheduler_hints') + if prepend_key and self.resource_key is not None: + body = {self.resource_key: body} + # If scheduler hints was passed in the request but the value is + # None, it doesn't make a difference to include it. + if scheduler_hints: + body['OS-SCH-HNT:scheduler_hints'] = scheduler_hints + return body + VolumeDetail = Volume diff --git a/openstack/tests/unit/block_storage/v3/test_volume.py b/openstack/tests/unit/block_storage/v3/test_volume.py index b8326ea98..7e162252c 100644 --- a/openstack/tests/unit/block_storage/v3/test_volume.py +++ b/openstack/tests/unit/block_storage/v3/test_volume.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import copy from unittest import mock from keystoneauth1 import adapter @@ -562,3 +563,38 @@ def test_terminate_attachment(self): self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion ) + + def test__prepare_request_body(self): + sot = volume.Volume(**VOLUME) + body = sot._prepare_request_body(patch=False, prepend_key=True) + original_body = copy.deepcopy(sot._body.dirty) + # Verify that scheduler hints aren't modified after preparing request + # but also not part of 'volume' JSON object + self.assertEqual( + original_body['OS-SCH-HNT:scheduler_hints'], + body['OS-SCH-HNT:scheduler_hints'], + ) + # Pop scheduler hints to verify other parameters in body + original_body.pop('OS-SCH-HNT:scheduler_hints') + # Verify that other request parameters are same but in 'volume' JSON + self.assertEqual(original_body, body['volume']) + + def test_create_scheduler_hints(self): + sot = volume.Volume(**VOLUME) + sot._translate_response = mock.Mock() + sot.create(self.sess) + + url = '/volumes' + volume_body = copy.deepcopy(VOLUME) + scheduler_hints = volume_body.pop('OS-SCH-HNT:scheduler_hints') + body = { + "volume": volume_body, + 'OS-SCH-HNT:scheduler_hints': scheduler_hints, + } + self.sess.post.assert_called_with( + url, + json=body, + microversion='3.0', + headers={}, + params={}, + ) From 02a7b9cb6bba553cac6e0403334d46453baf924b Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Thu, 7 Sep 2023 09:49:22 +0000 Subject: [PATCH 3389/3836] Update master for stable/2023.2 Add file to the reno documentation build to show release notes for stable/2023.2. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/2023.2. Sem-Ver: feature Change-Id: I89a9b4e4ca84be0e723e531ba203ef501c385572 --- releasenotes/source/2023.2.rst | 6 ++++++ releasenotes/source/index.rst | 1 + 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/2023.2.rst diff --git a/releasenotes/source/2023.2.rst b/releasenotes/source/2023.2.rst new file mode 100644 index 000000000..a4838d7d0 --- /dev/null +++ b/releasenotes/source/2023.2.rst @@ -0,0 +1,6 @@ +=========================== +2023.2 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2023.2 diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 317361138..cf39fb80b 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ :maxdepth: 1 unreleased + 2023.2 2023.1 zed yoga From df46d13f3cb16019c379f45768be869105dae612 Mon Sep 17 00:00:00 2001 From: Jay Faulkner Date: Wed, 13 Sep 2023 08:54:45 -0700 Subject: [PATCH 3390/3836] [baremetal] Add support for querying for shards Required for nova support for shard querying. Related-bug: 2035286 Change-Id: I3aabd411537dfbfa2be2c8a2ac2b3ff1bb0e3689 --- openstack/baremetal/v1/node.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 69bc2a767..e3f9c1785 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -91,13 +91,14 @@ class Node(_common.ListMixin, resource.Resource): 'fault', 'provision_state', 'resource_class', + 'shard', fields={'type': _common.fields_type}, instance_id='instance_uuid', is_maintenance='maintenance', ) - # Ability to get node inventory, introduced in 1.81 (Antelope). - _max_microversion = '1.81' + # Ability to query nodes by shard, introduced in 1.82 (Antelope). + _max_microversion = '1.82' # Properties #: The UUID of the allocation associated with this node. Added in API From cc1a6c3e20e0a2e5da154b82eaf4bc41b31b5d98 Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Wed, 23 Aug 2023 12:44:58 +0000 Subject: [PATCH 3391/3836] Add "hardware_offload_type" attribute to "port" Depends-On: https://review.opendev.org/c/openstack/python-openstackclient/+/894510 Related-Bug: #2013228 Change-Id: I48f09efe3e47b1d20c3756ef4159ba908adde38b --- openstack/network/v2/port.py | 3 +++ openstack/tests/unit/network/v2/test_port.py | 1 + .../add-port-hardware-offload-type-1232c5ae3f62d7df.yaml | 7 +++++++ 3 files changed, 11 insertions(+) create mode 100644 releasenotes/notes/add-port-hardware-offload-type-1232c5ae3f62d7df.yaml diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index 33ea63e69..7be8cea21 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -112,6 +112,9 @@ class Port(_base.NetworkResource, tag.TagMixin): extra_dhcp_opts = resource.Body('extra_dhcp_opts', type=list) #: IP addresses for the port. Includes the IP address and subnet ID. fixed_ips = resource.Body('fixed_ips', type=list) + #: The type of hardware offload this port will request when attached to the + # network backend. + hardware_offload_type = resource.Body('hardware_offload_type') #: Read-only. The ip_allocation indicates when ports use deferred, # immediate or no IP allocation. ip_allocation = resource.Body('ip_allocation') diff --git a/openstack/tests/unit/network/v2/test_port.py b/openstack/tests/unit/network/v2/test_port.py index 8e06a9f35..3547485f3 100644 --- a/openstack/tests/unit/network/v2/test_port.py +++ b/openstack/tests/unit/network/v2/test_port.py @@ -34,6 +34,7 @@ 'dns_name': '12', 'extra_dhcp_opts': [{'13': 13}], 'fixed_ips': [{'14': '14'}], + 'hardware_offload_type': None, 'id': IDENTIFIER, 'ip_allocation': 'immediate', 'mac_address': '16', diff --git a/releasenotes/notes/add-port-hardware-offload-type-1232c5ae3f62d7df.yaml b/releasenotes/notes/add-port-hardware-offload-type-1232c5ae3f62d7df.yaml new file mode 100644 index 000000000..b53e6765a --- /dev/null +++ b/releasenotes/notes/add-port-hardware-offload-type-1232c5ae3f62d7df.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Add ``hardware_offload_type`` attribute to ``port`` resource. Users + can set this attribute to a valid value defined in + ``neutron_lib.constants.VALID_HWOL_TYPES``, set "None" or leave it + undefined. From 1211a5118d5d4ab360c4d35164008d25bd8fdbfc Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Thu, 14 Sep 2023 12:03:50 -0700 Subject: [PATCH 3392/3836] [baremetal] Ensure baremetal shard parameter gets passed Add a test to validate that we do, indeed, pass parameters like we expect with the wrapped list method in baremetal/v1/_common.py. Change-Id: Id46c6f86b6a0f6827acf844107bdca9bb8614eb8 --- openstack/baremetal/v1/_proxy.py | 1 + .../tests/unit/baremetal/v1/test_node.py | 36 ++++++++++++++++--- .../tests/unit/baremetal/v1/test_proxy.py | 13 +++++++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index be72c3d1e..b3fb6550d 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -272,6 +272,7 @@ def nodes(self, details=False, **query): ``provision_state``. * ``resource_class``: Only return those with the specified ``resource_class``. + * ``shard``: Only return nodes matching the supplied shard key. * ``sort_dir``: Sorts the response by the requested sort direction. A valid value is ``asc`` (ascending) or ``desc`` (descending). Default is ``asc``. You can specify multiple diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 672d4fa7e..75e063373 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -93,6 +93,10 @@ } +def _fake_assert(self, session, action, expected, error_message=None): + return expected + + class TestNode(base.TestCase): def test_basic(self): sot = node.Node() @@ -154,6 +158,34 @@ def test_normalize_provision_state(self): sot = node.Node(**attrs) self.assertEqual('available', sot.provision_state) + @mock.patch.object(node.Node, '_assert_microversion_for', _fake_assert) + @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) + def test_list(self): + self.node = node.Node() + self.session = mock.Mock( + spec=adapter.Adapter, default_microversion=None + ) + # Set a default, so we don't try and figure out the microversions + # with additional requests. + self.session.default_microversion = float(self.node._max_microversion) + self.session.get.return_value.json.return_value = {'nodes': []} + + result = list( + self.node.list( + self.session, + details=False, + shard='meow', + allow_unknown_params=True, + ) + ) + self.assertEqual(0, len(result)) + self.session.get.assert_called_once_with( + '/nodes', + headers={'Accept': 'application/json'}, + params={'shard': 'meow'}, + microversion=float(self.node._max_microversion), + ) + @mock.patch('time.sleep', lambda _t: None) @mock.patch.object(node.Node, 'fetch', autospec=True) @@ -245,10 +277,6 @@ def _get_side_effect(_self, session): ) -def _fake_assert(self, session, action, expected, error_message=None): - return expected - - @mock.patch.object(node.Node, '_assert_microversion_for', _fake_assert) @mock.patch.object(node.Node, 'fetch', lambda self, session: self) @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) diff --git a/openstack/tests/unit/baremetal/v1/test_proxy.py b/openstack/tests/unit/baremetal/v1/test_proxy.py index 0b88bf0cb..834b29a77 100644 --- a/openstack/tests/unit/baremetal/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal/v1/test_proxy.py @@ -93,6 +93,19 @@ def test_nodes_not_detailed(self, mock_list): self.assertIs(result, mock_list.return_value) mock_list.assert_called_once_with(self.proxy, details=False, query=1) + @mock.patch.object(node.Node, 'list') + def test_nodes_sharded(self, mock_list): + kwargs = {"shard": 'meow', "query": 1} + result = self.proxy.nodes(fields=("uuid", "instance_uuid"), **kwargs) + self.assertIs(result, mock_list.return_value) + mock_list.assert_called_once_with( + self.proxy, + details=False, + fields=('uuid', 'instance_uuid'), + shard='meow', + query=1, + ) + def test_create_node(self): self.verify_create(self.proxy.create_node, node.Node) From 1ceb9972eb460090fc8cc8524d9f036f562cb552 Mon Sep 17 00:00:00 2001 From: Andrew Bogott Date: Fri, 1 Sep 2023 18:14:43 -0500 Subject: [PATCH 3393/3836] config loader: Prefer cli/env over clouds.yaml args for some args If a user specifies a project or domain on the cli, that should take precedence over the value set in clouds.yaml. This fixes enables workflows that rely on domain- or cloud-wide credentials in clouds.yaml. The cli args that are reprioritized in this patch are: domain-id domain-name user-domain-id user-domain-name project-domain-id project-domain-name auth-token project-id tenant-id project-name tenant-name Story: 2010784 Change-Id: I45e7cff6579e6686d790bd3bb3e3ab9955885a64 --- doc/source/user/config/configuration.rst | 47 +++++++++++++++++++ openstack/config/loader.py | 9 ++-- openstack/tests/unit/config/test_config.py | 43 +++++++++++++++++ .../notes/story-2010784-21d23043155497f5.yaml | 33 +++++++++++++ 4 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/story-2010784-21d23043155497f5.yaml diff --git a/doc/source/user/config/configuration.rst b/doc/source/user/config/configuration.rst index 41e8dfc75..72cfaa510 100644 --- a/doc/source/user/config/configuration.rst +++ b/doc/source/user/config/configuration.rst @@ -450,3 +450,50 @@ region. - name: inap-17037-WAN1654 routes_externally: true - name: inap-17037-LAN6745 + + +Setting Precedence +------------------ + +Some settings are redundant, e.g. ``project-name`` and ``project-id`` both +specify the project. In a conflict between redundant settings, the +``_name`` ``clouds.yaml`` option (or equivalent ``-name`` CLI option and ``_NAME`` environment variable) will be used. + +Some environment variables or commandline flags can override the settings from +clouds.yaml. These are: + +- ``--domain-id`` (``OS_DOMAIN_ID``) +- ``--domain-name`` (``OS_DOMAIN_NAME``) +- ``--user-domain-id`` (``OS_USER_DOMAIN_ID``) +- ``--user-domain-name`` (``OS_USER_DOMAIN_NAME``) +- ``--project-domain-id`` (``OS_PROJECT_DOMAIN_ID``) +- ``--project-domain-name`` (``OS_PROJECT_DOMAIN_NAME``) +- ``--auth-token`` (``OS_AUTH_TOKEN``) +- ``--project-id`` (``OS_PROJECT_ID``) +- ``--project-name`` (``OS_PROJECT_NAME``) +- ``--tenant-id`` (``OS_TENANT_ID``) (deprecated for ``--project-id``) +- ``--tenant-name`` (``OS_TENANT_NAME``) (deprecated for ``--project-name``) + +Similarly, if one of the above settings is specified in ``clouds.yaml`` as +part of the ``auth`` section as well as the main section, the ``auth`` settings +will be overridden. For example in this config section, note that project is +specified multiple times: + +.. code-block:: yaml + + clouds: + mtvexx: + profile: https://vexxhost.com + auth: + username: mordred@inaugust.com + password: XXXXXXXXX + project_name: mylessfavoriteproject + project_id: 0bedab75-898c-4521-a038-0b4b71c41bed + region_name: ca-ymq-1 + project_name: myfavoriteproject + project_id: 2acf9403-25e8-479e-a3c6-d67540c424a4 + +In the above example, the ``project_id`` configuration values will be ignored +in favor of the ``project_name`` configuration values, and the higher-level +project will be chosen over the auth-specified project. So the actual project +used will be ```myfavoriteproject```. diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 1f330e8f4..2954e160c 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -777,12 +777,15 @@ def _fix_backwards_project(self, cloud): for target_key, possible_values in mappings.items(): target = None for key in possible_values: - if key in cloud: - target = str(cloud[key]) - del cloud[key] + # Prefer values from the 'auth' section + # as they may contain cli or environment overrides. + # See story 2010784 for context. if key in cloud['auth']: target = str(cloud['auth'][key]) del cloud['auth'][key] + if key in cloud: + target = str(cloud[key]) + del cloud[key] if target: cloud['auth'][target_key] = target return cloud diff --git a/openstack/tests/unit/config/test_config.py b/openstack/tests/unit/config/test_config.py index da5658940..0d865e910 100644 --- a/openstack/tests/unit/config/test_config.py +++ b/openstack/tests/unit/config/test_config.py @@ -1471,6 +1471,49 @@ def test_project_password(self): } self.assertEqual(expected, result) + def test_project_conflict_priority(self): + """The order of priority should be + 1: env or cli settings + 2: setting from 'auth' section of clouds.yaml + + The ordering of #1 is important so that operators can use domain-wide + inherited credentials in clouds.yaml. + """ + + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) + cloud = { + 'auth_type': 'password', + 'auth': { + 'project_id': 'my_project_id', + }, + } + result = c._fix_backwards_project(cloud) + expected = { + 'auth_type': 'password', + 'auth': { + 'project_id': 'my_project_id', + }, + } + self.assertEqual(expected, result) + + cloud = { + 'auth_type': 'password', + 'auth': { + 'project_id': 'my_project_id', + }, + 'project_id': 'different_project_id', + } + result = c._fix_backwards_project(cloud) + expected = { + 'auth_type': 'password', + 'auth': { + 'project_id': 'different_project_id', + }, + } + self.assertEqual(expected, result) + def test_backwards_network_fail(self): c = config.OpenStackConfig( config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] diff --git a/releasenotes/notes/story-2010784-21d23043155497f5.yaml b/releasenotes/notes/story-2010784-21d23043155497f5.yaml new file mode 100644 index 000000000..557eef5f3 --- /dev/null +++ b/releasenotes/notes/story-2010784-21d23043155497f5.yaml @@ -0,0 +1,33 @@ +--- +upgrade: + - | + Many cloud administrators use universal cloud-wide credentials. This is + supported in keystone via 'inherited' roles that can be applied cloud- + or domain-wide. + + In previous releases, these credentials could not be usefully defined + within ```clouds.yaml``` because ```clouds.yaml``` supports only + specifying a single domain and project for auth purposes. This project + or domain could not be overridden on the commandline. +fixes: + - | + When some config settings are specified multiple times, the order of + precendence has been changed to prefer command-line or env settings over + those found in ```clouds.yaml```. The same reordering has been done when + a setting is specified multiple times within ```clouds.yaml```; now a + higher-level setting will take precedence over that specified within + the auth section. + + Affected settings are: + + - ``domain_id`` + - ``domain_name`` + - ``user_domain_id`` + - ``user_domain_name`` + - ``project_domain_id`` + - ``project_domain_name`` + - ``auth-token`` + - ``project_id`` + - ``tenant_id`` + - ``project_name`` + - ``tenant_name`` From c2600c35b7d4f9878a4d41d40ab9f60f361fb355 Mon Sep 17 00:00:00 2001 From: jihyun huh Date: Thu, 15 Sep 2022 00:30:56 +0900 Subject: [PATCH 3394/3836] image: Add support for metadef property operations Like placement's resource provider inventory API and keystone's old extensions API, this is yet another example of an API that returns an object with property names as keys and all other attributes as the values, e.g. we see: { "os_admin_user": { ... }, ... } rather than: [ { "name": "os_admin_user", ... }, ,,, ] Change-Id: I8e2ae8545cfaf32ced6d086a0921732f16282216 Co-authored-by: KIM SOJUNG Co-authored-by: GA EUM KIM Co-authored-by: EunYoung Kim Co-authored-by: hyemin Choi Co-authored-by: Antonia Gaete Co-authored-by: YeJun, Jung --- doc/source/user/proxies/image_v2.rst | 10 + doc/source/user/resources/image/index.rst | 1 + .../resources/image/v2/metadef_property.rst | 13 ++ openstack/image/v2/_proxy.py | 127 +++++++++++++ openstack/image/v2/metadef_property.py | 179 ++++++++++++++++++ .../image/v2/test_metadef_property.py | 129 +++++++++++++ .../unit/image/v2/test_metadef_property.py | 82 ++++++++ ...age-metadef-property-fb87e5a7090e73ac.yaml | 4 + 8 files changed, 545 insertions(+) create mode 100644 doc/source/user/resources/image/v2/metadef_property.rst create mode 100644 openstack/image/v2/metadef_property.py create mode 100644 openstack/tests/functional/image/v2/test_metadef_property.py create mode 100644 openstack/tests/unit/image/v2/test_metadef_property.py create mode 100644 releasenotes/notes/add-image-metadef-property-fb87e5a7090e73ac.yaml diff --git a/doc/source/user/proxies/image_v2.rst b/doc/source/user/proxies/image_v2.rst index b280e8198..e0b5caeec 100644 --- a/doc/source/user/proxies/image_v2.rst +++ b/doc/source/user/proxies/image_v2.rst @@ -84,6 +84,16 @@ Metadef Resource Type Operations create_metadef_resource_type_association, delete_metadef_resource_type_association + +Metadef Property Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.image.v2._proxy.Proxy + :noindex: + :members: create_metadef_property, update_metadef_property, + delete_metadef_property, get_metadef_property + + Helpers ^^^^^^^ diff --git a/doc/source/user/resources/image/index.rst b/doc/source/user/resources/image/index.rst index 6cfe1ae65..c6ef3d78f 100644 --- a/doc/source/user/resources/image/index.rst +++ b/doc/source/user/resources/image/index.rst @@ -20,6 +20,7 @@ Image v2 Resources v2/metadef_namespace v2/metadef_object v2/metadef_resource_type + v2/metadef_property v2/metadef_schema v2/task v2/service_info diff --git a/doc/source/user/resources/image/v2/metadef_property.rst b/doc/source/user/resources/image/v2/metadef_property.rst new file mode 100644 index 000000000..e70fff6eb --- /dev/null +++ b/doc/source/user/resources/image/v2/metadef_property.rst @@ -0,0 +1,13 @@ +openstack.image.v2.metadef_property +=================================== + +.. automodule:: openstack.image.v2.metadef_property + +The MetadefProperty Class +------------------------- + +The ``MetadefProperty`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.image.v2.metadef_property.MetadefProperty + :members: diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index fd2e6741f..5e1ccdbd3 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -20,6 +20,7 @@ from openstack.image.v2 import member as _member from openstack.image.v2 import metadef_namespace as _metadef_namespace from openstack.image.v2 import metadef_object as _metadef_object +from openstack.image.v2 import metadef_property as _metadef_property from openstack.image.v2 import metadef_resource_type as _metadef_resource_type from openstack.image.v2 import metadef_schema as _metadef_schema from openstack.image.v2 import schema as _schema @@ -1397,6 +1398,132 @@ def metadef_resource_type_associations(self, metadef_namespace, **query): **query, ) + # ====== METADEF PROPERTY ====== + def create_metadef_property(self, metadef_namespace, **attrs): + """Create a metadef property + + :param metadef_namespace: The value can be either the name of metadef + namespace or an + :class:`~openstack.image.v2.metadef_property.MetadefNamespace` + instance + :param attrs: The attributes to create on the metadef property + represented by ``metadef_property``. + + :returns: The created metadef property + :rtype: :class:`~openstack.image.v2.metadef_property.MetadefProperty` + """ + namespace_name = resource.Resource._get_id(metadef_namespace) + return self._create( + _metadef_property.MetadefProperty, + namespace_name=namespace_name, + **attrs, + ) + + def update_metadef_property( + self, metadef_property, metadef_namespace, **attrs + ): + """Update a metadef property + + :param metadef_property: The value can be either the name of metadef + property or an + :class:`~openstack.image.v2.metadef_property.MetadefProperty` + instance. + :param metadef_namespace: The value can be either the name of metadef + namespace or an + :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` + instance + :param attrs: The attributes to update on the metadef property + represented by ``metadef_property``. + + :returns: The updated metadef property + :rtype: :class:`~openstack.image.v2.metadef_property.MetadefProperty` + """ + namespace_name = resource.Resource._get_id(metadef_namespace) + metadef_property = resource.Resource._get_id(metadef_property) + return self._update( + _metadef_property.MetadefProperty, + metadef_property, + namespace_name=namespace_name, + **attrs, + ) + + def delete_metadef_property( + self, metadef_property, metadef_namespace, ignore_missing=True + ): + """Delete a metadef property + + :param metadef_property: The value can be either the name of metadef + property or an + :class:`~openstack.image.v2.metadef_property.MetadefProperty` + instance + :param metadef_namespace: The value can be either the name of metadef + namespace or an + :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` + instance + :param bool ignore_missing: When set to + ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the instance does not exist. When set to ``True``, + no exception will be set when attempting to delete a nonexistent + instance. + + :returns: ``None`` + """ + namespace_name = resource.Resource._get_id(metadef_namespace) + metadef_property = resource.Resource._get_id(metadef_property) + return self._delete( + _metadef_property.MetadefProperty, + metadef_property, + namespace_name=namespace_name, + ignore_missing=ignore_missing, + ) + + def metadef_properties(self, metadef_namespace, **query): + """Return a generator of metadef properties + + :param metadef_namespace: The value can be either the name of metadef + namespace or an + :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` + instance + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. + + :returns: A generator of property objects + """ + namespace_name = resource.Resource._get_id(metadef_namespace) + return self._list( + _metadef_property.MetadefProperty, + requires_id=False, + namespace_name=namespace_name, + **query, + ) + + def get_metadef_property( + self, metadef_property, metadef_namespace, **query + ): + """Get a single metadef property + + :param metadef_property: The value can be either the name of metadef + property or an + :class:`~openstack.image.v2.metadef_property.MetadefProperty` + instance. + :param metadef_namespace: The value can be either the name of metadef + namespace or an + :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` + instance + + :returns: One + :class:`~~openstack.image.v2.metadef_property.MetadefProperty` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource can be found. + """ + namespace_name = resource.Resource._get_id(metadef_namespace) + return self._get( + _metadef_property.MetadefProperty, + metadef_property, + namespace_name=namespace_name, + **query, + ) + # ====== SCHEMAS ====== def get_images_schema(self): """Get images schema diff --git a/openstack/image/v2/metadef_property.py b/openstack/image/v2/metadef_property.py new file mode 100644 index 000000000..ae40ebd4b --- /dev/null +++ b/openstack/image/v2/metadef_property.py @@ -0,0 +1,179 @@ +# 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 import resource + + +class MetadefProperty(resource.Resource): + base_path = '/metadefs/namespaces/%(namespace_name)s/properties' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + #: An identifier (a name) for the namespace. + namespace_name = resource.URI('namespace_name') + #: The name of the property + name = resource.Body('name', alternate_id=True) + #: The property type. + type = resource.Body('type') + #: The title of the property. + title = resource.Body('title') + #: Detailed description of the property. + description = resource.Body('description') + #: A list of operator + operators = resource.Body('operators', type=list) + #: Default property description. + default = resource.Body('default') + #: Indicates whether this is a read-only property. + is_readonly = resource.Body('readonly', type=bool) + #: Minimum allowed numerical value. + minimum = resource.Body('minimum', type=int) + #: Maximum allowed numerical value. + maximum = resource.Body('maximum', type=int) + #: Enumerated list of property values. + enum = resource.Body('enum', type=list) + #: A regular expression + #: (`ECMA 262 `_) + #: that a string value must match. + pattern = resource.Body('pattern') + #: Minimum allowed string length. + min_length = resource.Body('minLength', type=int, minimum=0, default=0) + #: Maximum allowed string length. + max_length = resource.Body('maxLength', type=int, minimum=0) + #: Schema for the items in an array. + items = resource.Body('items', type=dict) + #: Indicates whether all values in the array must be distinct. + require_unique_items = resource.Body( + 'uniqueItems', type=bool, default=False + ) + #: Minimum length of an array. + min_items = resource.Body('minItems', type=int, minimum=0, default=0) + #: Maximum length of an array. + max_items = resource.Body('maxItems', type=int, minimum=0) + #: Describes extra items, if you use tuple typing. If the value of + #: ``items`` is an array (tuple typing) and the instance is longer than + #: the list of schemas in ``items``, the additional items are described by + #: the schema in this property. If this value is ``false``, the instance + #: cannot be longer than the list of schemas in ``items``. If this value + #: is ``true``, that is equivalent to the empty schema (anything goes). + allow_additional_items = resource.Body('additionalItems', type=bool) + + # TODO(stephenfin): It would be nicer if we could do this in Resource + # itself since the logic is also found elsewhere (e.g. + # openstack.identity.v2.extension.Extension) but that code is a bit of a + # rat's nest right now and needs a spring clean + @classmethod + def list( + cls, + session, + paginated=True, + base_path=None, + allow_unknown_params=False, + *, + microversion=None, + **params, + ): + """This method is a generator which yields resource objects. + + A re-implementation of :meth:`~openstack.resource.Resource.list` that + handles glance's single, unpaginated list implementation. + + Refer to :meth:`~openstack.resource.Resource.list` for full + documentation including parameter, exception and return type + documentation. + """ + session = cls._get_session(session) + + if microversion is None: + microversion = cls._get_microversion(session, action='list') + + if base_path is None: + base_path = cls.base_path + + # There is no server-side filtering, only client-side + client_filters = {} + # Gather query parameters which are not supported by the server + for k, v in params.items(): + if ( + # Known attr + hasattr(cls, k) + # Is real attr property + and isinstance(getattr(cls, k), resource.Body) + # not included in the query_params + and k not in cls._query_mapping._mapping.keys() + ): + client_filters[k] = v + + uri = base_path % params + uri_params = {} + + for k, v in params.items(): + # We need to gather URI parts to set them on the resource later + if hasattr(cls, k) and isinstance(getattr(cls, k), resource.URI): + uri_params[k] = v + + def _dict_filter(f, d): + """Dict param based filtering""" + if not d: + return False + for key in f.keys(): + if isinstance(f[key], dict): + if not _dict_filter(f[key], d.get(key, None)): + return False + elif d.get(key, None) != f[key]: + return False + return True + + response = session.get( + uri, + headers={"Accept": "application/json"}, + params={}, + microversion=microversion, + ) + exceptions.raise_from_response(response) + data = response.json() + + for name, property_data in data['properties'].items(): + property = { + 'name': name, + **property_data, + **uri_params, + } + value = cls.existing( + microversion=microversion, + connection=session._get_connection(), + **property, + ) + + filters_matched = True + # Iterate over client filters and return only if matching + for key in client_filters.keys(): + if isinstance(client_filters[key], dict): + if not _dict_filter( + client_filters[key], + value.get(key, None), + ): + filters_matched = False + break + elif value.get(key, None) != client_filters[key]: + filters_matched = False + break + + if filters_matched: + yield value + + return None diff --git a/openstack/tests/functional/image/v2/test_metadef_property.py b/openstack/tests/functional/image/v2/test_metadef_property.py new file mode 100644 index 000000000..c58edf7a4 --- /dev/null +++ b/openstack/tests/functional/image/v2/test_metadef_property.py @@ -0,0 +1,129 @@ +# 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 random +import string + +from openstack.image.v2 import metadef_namespace as _metadef_namespace +from openstack.image.v2 import metadef_property as _metadef_property +from openstack.tests.functional.image.v2 import base + + +class TestMetadefProperty(base.BaseImageTest): + def setUp(self): + super().setUp() + + # there's a limit on namespace length + namespace = 'test_' + ''.join( + random.choice(string.ascii_lowercase) for _ in range(75) + ) + self.metadef_namespace = self.conn.image.create_metadef_namespace( + namespace=namespace, + ) + self.assertIsInstance( + self.metadef_namespace, + _metadef_namespace.MetadefNamespace, + ) + self.assertEqual(namespace, self.metadef_namespace.namespace) + + # there's a limit on property length + property_name = 'test_' + ''.join( + random.choice(string.ascii_lowercase) for _ in range(75) + ) + self.attrs = { + 'name': property_name, + 'title': property_name, + 'type': 'string', + 'description': 'Web Server port', + 'enum': ["80", "443"], + } + self.metadef_property = self.conn.image.create_metadef_property( + self.metadef_namespace.namespace, **self.attrs + ) + self.assertIsInstance( + self.metadef_property, _metadef_property.MetadefProperty + ) + self.assertEqual(self.attrs['name'], self.metadef_property.name) + self.assertEqual(self.attrs['title'], self.metadef_property.title) + self.assertEqual(self.attrs['type'], self.metadef_property.type) + self.assertEqual( + self.attrs['description'], self.metadef_property.description + ) + self.assertEqual(self.attrs['enum'], self.metadef_property.enum) + + def tearDown(self): + # we do this in tearDown rather than via 'addCleanup' since we want to + # wait for the deletion of the resource to ensure it completes + self.conn.image.delete_metadef_property( + self.metadef_property, self.metadef_namespace + ) + self.conn.image.delete_metadef_namespace(self.metadef_namespace) + self.conn.image.wait_for_delete(self.metadef_namespace) + + super().tearDown() + + def test_metadef_property(self): + # get metadef property + metadef_property = self.conn.image.get_metadef_property( + self.metadef_property, self.metadef_namespace + ) + self.assertIsNotNone(metadef_property) + self.assertIsInstance( + metadef_property, _metadef_property.MetadefProperty + ) + self.assertEqual(self.attrs['name'], metadef_property.name) + self.assertEqual(self.attrs['title'], metadef_property.title) + self.assertEqual(self.attrs['type'], metadef_property.type) + self.assertEqual( + self.attrs['description'], metadef_property.description + ) + self.assertEqual(self.attrs['enum'], metadef_property.enum) + + # (no find_metadef_property method) + + # list + metadef_properties = list( + self.conn.image.metadef_properties(self.metadef_namespace) + ) + self.assertIsNotNone(metadef_properties) + self.assertIsInstance( + metadef_properties[0], _metadef_property.MetadefProperty + ) + + # update + self.attrs['title'] = ''.join( + random.choice(string.ascii_lowercase) for _ in range(10) + ) + self.attrs['description'] = ''.join( + random.choice(string.ascii_lowercase) for _ in range(10) + ) + metadef_property = self.conn.image.update_metadef_property( + self.metadef_property, + self.metadef_namespace.namespace, + **self.attrs + ) + self.assertIsNotNone(metadef_property) + self.assertIsInstance( + metadef_property, + _metadef_property.MetadefProperty, + ) + metadef_property = self.conn.image.get_metadef_property( + self.metadef_property.name, self.metadef_namespace + ) + self.assertEqual( + self.attrs['title'], + metadef_property.title, + ) + self.assertEqual( + self.attrs['description'], + metadef_property.description, + ) diff --git a/openstack/tests/unit/image/v2/test_metadef_property.py b/openstack/tests/unit/image/v2/test_metadef_property.py new file mode 100644 index 000000000..cfd7768a8 --- /dev/null +++ b/openstack/tests/unit/image/v2/test_metadef_property.py @@ -0,0 +1,82 @@ +# 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.image.v2 import metadef_property +from openstack.tests.unit import base + +EXAMPLE = { + 'namespace_name': 'CIM::StorageAllocationSettingData', + 'name': 'Access', + 'type': 'string', + 'title': 'Access', + 'description': ( + 'Access describes whether the allocated storage extent is ' + '1 (readable), 2 (writeable), or 3 (both).' + ), + 'operators': [''], + 'default': None, + 'readonly': None, + 'minimum': None, + 'maximum': None, + 'enum': [ + 'Unknown', + 'Readable', + 'Writeable', + 'Read/Write Supported', + 'DMTF Reserved', + ], + 'pattern': None, + 'min_length': 0, + 'max_length': None, + 'items': None, + 'unique_items': False, + 'min_items': 0, + 'max_items': None, + 'additional_items': None, +} + + +class TestMetadefProperty(base.TestCase): + def test_basic(self): + sot = metadef_property.MetadefProperty() + self.assertEqual( + '/metadefs/namespaces/%(namespace_name)s/properties', sot.base_path + ) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = metadef_property.MetadefProperty(**EXAMPLE) + self.assertEqual(EXAMPLE['namespace_name'], sot.namespace_name) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['type'], sot.type) + self.assertEqual(EXAMPLE['title'], sot.title) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertListEqual(EXAMPLE['operators'], sot.operators) + self.assertEqual(EXAMPLE['default'], sot.default) + self.assertEqual(EXAMPLE['readonly'], sot.is_readonly) + self.assertEqual(EXAMPLE['minimum'], sot.minimum) + self.assertEqual(EXAMPLE['maximum'], sot.maximum) + self.assertListEqual(EXAMPLE['enum'], sot.enum) + self.assertEqual(EXAMPLE['pattern'], sot.pattern) + self.assertEqual(EXAMPLE['min_length'], sot.min_length) + self.assertEqual(EXAMPLE['max_length'], sot.max_length) + self.assertEqual(EXAMPLE['items'], sot.items) + self.assertEqual(EXAMPLE['unique_items'], sot.require_unique_items) + self.assertEqual(EXAMPLE['min_items'], sot.min_items) + self.assertEqual(EXAMPLE['max_items'], sot.max_items) + self.assertEqual( + EXAMPLE['additional_items'], sot.allow_additional_items + ) diff --git a/releasenotes/notes/add-image-metadef-property-fb87e5a7090e73ac.yaml b/releasenotes/notes/add-image-metadef-property-fb87e5a7090e73ac.yaml new file mode 100644 index 000000000..042d4c71a --- /dev/null +++ b/releasenotes/notes/add-image-metadef-property-fb87e5a7090e73ac.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Added support for the ``MetadefProperty`` Image resource. From a704286747bc7688c13d88596a17f5db3d3256f0 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 6 Oct 2023 10:50:16 +0100 Subject: [PATCH 3395/3836] fakes: Generate correct type for list type components Change-Id: I413f0a595c8857381e814944b51f9a9e9c520791 Signed-off-by: Stephen Finucane --- openstack/test/fakes.py | 4 ++-- openstack/tests/unit/test_fakes.py | 38 +++++++++++++++++++++++------- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/openstack/test/fakes.py b/openstack/test/fakes.py index 857b56a56..7ce55f372 100644 --- a/openstack/test/fakes.py +++ b/openstack/test/fakes.py @@ -96,9 +96,9 @@ def generate_fake_resource( item_type = value.list_type if issubclass(item_type, resource.Resource): # item is of Resource type - base_attrs[name] = generate_fake_resource(item_type) + base_attrs[name] = [generate_fake_resource(item_type)] elif issubclass(item_type, dict): - base_attrs[name] = dict() + base_attrs[name] = [{}] elif issubclass(item_type, str): base_attrs[name] = [uuid.uuid4().hex] else: diff --git a/openstack/tests/unit/test_fakes.py b/openstack/tests/unit/test_fakes.py index e0e2fa803..8013d8068 100644 --- a/openstack/tests/unit/test_fakes.py +++ b/openstack/tests/unit/test_fakes.py @@ -27,7 +27,7 @@ def test_generate_fake_resource_list(self): self.assertIsInstance(res[0], resource.Resource) def test_generate_fake_resource_types(self): - class Fake(resource.Resource): + class Foo(resource.Resource): a = resource.Body("a", type=str) b = resource.Body("b", type=int) c = resource.Body("c", type=bool) @@ -35,13 +35,35 @@ class Fake(resource.Resource): e = resource.Body("e", type=dict) f = resource.URI("path") - res = fakes.generate_fake_resource(Fake) - self.assertIsInstance(res.a, str) - self.assertIsInstance(res.b, int) - self.assertIsInstance(res.c, bool) - self.assertIsInstance(res.d, bool) - self.assertIsInstance(res.e, dict) - self.assertIsInstance(res.f, str) + class Bar(resource.Resource): + a = resource.Body("a", type=list, list_type=str) + b = resource.Body("b", type=list, list_type=dict) + c = resource.Body("c", type=list, list_type=Foo) + + foo = fakes.generate_fake_resource(Foo) + self.assertIsInstance(foo.a, str) + self.assertIsInstance(foo.b, int) + self.assertIsInstance(foo.c, bool) + self.assertIsInstance(foo.d, bool) + self.assertIsInstance(foo.e, dict) + self.assertIsInstance(foo.f, str) + + bar = fakes.generate_fake_resource(Bar) + self.assertIsInstance(bar.a, list) + self.assertEqual(1, len(bar.a)) + self.assertIsInstance(bar.a[0], str) + self.assertIsInstance(bar.b, list) + self.assertEqual(1, len(bar.b)) + self.assertIsInstance(bar.b[0], dict) + self.assertIsInstance(bar.c, list) + self.assertEqual(1, len(bar.c)) + self.assertIsInstance(bar.c[0], Foo) + self.assertIsInstance(bar.c[0].a, str) + self.assertIsInstance(bar.c[0].b, int) + self.assertIsInstance(bar.c[0].c, bool) + self.assertIsInstance(bar.c[0].d, bool) + self.assertIsInstance(bar.c[0].e, dict) + self.assertIsInstance(bar.c[0].f, str) def test_generate_fake_resource_attrs(self): class Fake(resource.Resource): From b34de32a5414bc4a724a13148d92b53a1b0091b0 Mon Sep 17 00:00:00 2001 From: Jeremy Stanley Date: Wed, 21 Jun 2023 15:02:02 +0000 Subject: [PATCH 3396/3836] Replace appdirs usage with platformdirs The appdirs library has been abandoned for several years now, and a fork named platformdirs is the recommended alternative. It's almost a drop-in replacement, except the module and convenience class name have changed. Adjust the internal variable name we're using for the expansion to match as well, for all those consistent hobgoblins. Add it with a >=3 lower bound because that introduces a breaking change of the default configuration location for macOS users. Include a release note about that too. Depends-On: https://review.opendev.org/886642 Change-Id: I4c2b60ca0da29da1f605b4c0b5e77f8e1071a19c --- openstack/config/loader.py | 12 +++++++----- ...e-appdirs-with-platformdirs-d3f5bcbe726b7829.yaml | 8 ++++++++ requirements.txt | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/replace-appdirs-with-platformdirs-d3f5bcbe726b7829.yaml diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 8df3422be..b9dc82402 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -23,9 +23,9 @@ import sys import warnings -import appdirs from keystoneauth1 import adapter from keystoneauth1 import loading +import platformdirs import yaml from openstack import _log @@ -36,9 +36,11 @@ from openstack import exceptions from openstack import warnings as os_warnings -APPDIRS = appdirs.AppDirs('openstack', 'OpenStack', multipath='/etc') -CONFIG_HOME = APPDIRS.user_config_dir -CACHE_PATH = APPDIRS.user_cache_dir +PLATFORMDIRS = platformdirs.PlatformDirs( + 'openstack', 'OpenStack', multipath='/etc' +) +CONFIG_HOME = PLATFORMDIRS.user_config_dir +CACHE_PATH = PLATFORMDIRS.user_cache_dir # snaps do set $HOME to something like # /home/$USER/snap/openstackclients/$SNAP_VERSION @@ -55,7 +57,7 @@ ) UNIX_SITE_CONFIG_HOME = '/etc/openstack' -SITE_CONFIG_HOME = APPDIRS.site_config_dir +SITE_CONFIG_HOME = PLATFORMDIRS.site_config_dir CONFIG_SEARCH_PATH = [ os.getcwd(), diff --git a/releasenotes/notes/replace-appdirs-with-platformdirs-d3f5bcbe726b7829.yaml b/releasenotes/notes/replace-appdirs-with-platformdirs-d3f5bcbe726b7829.yaml new file mode 100644 index 000000000..01a0815a2 --- /dev/null +++ b/releasenotes/notes/replace-appdirs-with-platformdirs-d3f5bcbe726b7829.yaml @@ -0,0 +1,8 @@ +--- +upgrade: + - | + The ``appdirs`` dependency is replaced by a requirement for + ``platformdirs`` 3.0.0 or later. Users on macOS may need to move + configuration files to ``*/Library/Application Support``. See its release + notes for further details: + https://platformdirs.readthedocs.io/en/latest/changelog.html#platformdirs-3-0-0-2023-02-06 diff --git a/requirements.txt b/requirements.txt index 6dc4d905c..48188d4e2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ # process, which may cause wedges in the gate later. pbr!=2.1.0,>=2.0.0 # Apache-2.0 PyYAML>=3.13 # MIT -appdirs>=1.3.0 # MIT License +platformdirs>=3 # MIT License requestsexceptions>=1.2.0 # Apache-2.0 jsonpatch!=1.20,>=1.16 # BSD os-service-types>=1.7.0 # Apache-2.0 From be544e6f757a2162e00eee1494d03566e24d783e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 6 Oct 2023 16:38:09 +0100 Subject: [PATCH 3397/3836] volume: Add Capability to volume v2 API As with Extension in change I1e4528f5a5d8e2caaaf204792ddcee7267e4c2e9, this makes our lives easier for migrating OSC since volume v2 is still a thing in some areas. Note that the API is unchanged between v2 and v3 so this can be a straightforward copy-paste job. This was determined by inspecting the cinder code before the v2 API was removed (e.g. commit 'e05b261af~', file 'cinder/api/views/capabilities.py'). Change-Id: I98252ddd0dadba2bfa1aaf97b59932a19c396cd4 Signed-off-by: Stephen Finucane --- doc/source/user/proxies/block_storage_v2.rst | 7 ++ .../block_storage/v2/capabilities.rst | 12 +++ openstack/block_storage/v2/_proxy.py | 18 +++- openstack/block_storage/v2/capabilities.py | 45 ++++++++ openstack/block_storage/v3/_proxy.py | 1 + .../block_storage/v2/test_capabilities.py | 102 ++++++++++++++++++ .../tests/unit/block_storage/v2/test_proxy.py | 6 ++ 7 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 doc/source/user/resources/block_storage/v2/capabilities.rst create mode 100644 openstack/block_storage/v2/capabilities.py create mode 100644 openstack/tests/unit/block_storage/v2/test_capabilities.py diff --git a/doc/source/user/proxies/block_storage_v2.rst b/doc/source/user/proxies/block_storage_v2.rst index cd8260535..b6e0faa31 100644 --- a/doc/source/user/proxies/block_storage_v2.rst +++ b/doc/source/user/proxies/block_storage_v2.rst @@ -26,6 +26,13 @@ Backup Operations :noindex: :members: create_backup, delete_backup, get_backup, backups, restore_backup +Capabilities Operations +^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v2._proxy.Proxy + :noindex: + :members: get_capabilities + Type Operations ^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/block_storage/v2/capabilities.rst b/doc/source/user/resources/block_storage/v2/capabilities.rst new file mode 100644 index 000000000..5835928ac --- /dev/null +++ b/doc/source/user/resources/block_storage/v2/capabilities.rst @@ -0,0 +1,12 @@ +openstack.block_storage.v2.capabilities +======================================= + +.. automodule:: openstack.block_storage.v2.capabilities + +The Capabilities Class +---------------------- + +The ``Capabilities`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v2.capabilities.Capabilities + :members: diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index fcf45d820..0312fbdfb 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -12,6 +12,7 @@ from openstack.block_storage import _base_proxy from openstack.block_storage.v2 import backup as _backup +from openstack.block_storage.v2 import capabilities as _capabilities from openstack.block_storage.v2 import extension as _extension from openstack.block_storage.v2 import quota_set as _quota_set from openstack.block_storage.v2 import snapshot as _snapshot @@ -601,7 +602,7 @@ def reset_backup(self, backup, status): """Reset status of the backup :param backup: The value can be either the ID of a backup or a - :class:`~openstack.block_storage.v3.backup.Backup` instance. + :class:`~openstack.block_storage.v2.backup.Backup` instance. :param str status: New backup status :returns: None @@ -609,6 +610,19 @@ def reset_backup(self, backup, status): backup = self._get_resource(_backup.Backup, backup) backup.reset(self, status) + # ====== CAPABILITIES ====== + def get_capabilities(self, host): + """Get a backend's capabilites + + :param host: Specified backend to obtain volume stats and properties. + + :returns: One :class: + `~openstack.block_storage.v2.capabilites.Capabilities` instance. + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource can be found. + """ + return self._get(_capabilities.Capabilities, host) + # ====== QUOTA SETS ====== def get_quota_set(self, project, usage=False, **query): """Show QuotaSet information for the project @@ -784,7 +798,7 @@ def extensions(self): """Return a generator of extensions :returns: A generator of extension - :rtype: :class:`~openstack.block_storage.v3.extension.Extension` + :rtype: :class:`~openstack.block_storage.v2.extension.Extension` """ return self._list(_extension.Extension) diff --git a/openstack/block_storage/v2/capabilities.py b/openstack/block_storage/v2/capabilities.py new file mode 100644 index 000000000..03d958d8b --- /dev/null +++ b/openstack/block_storage/v2/capabilities.py @@ -0,0 +1,45 @@ +# 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 resource + + +class Capabilities(resource.Resource): + base_path = "/capabilities" + + # Capabilities + allow_fetch = True + + #: Properties + #: The capabilities description + description = resource.Body("description") + #: The name of volume backend capabilities. + display_name = resource.Body("display_name") + #: The driver version. + driver_version = resource.Body("driver_version") + #: The storage namespace, such as OS::Storage::Capabilities::foo. + namespace = resource.Body("namespace") + #: The name of the storage pool. + pool_name = resource.Body("pool_name") + #: The backend volume capabilites list, which consists of cinder + #: standard capabilities and vendor unique properties. + properties = resource.Body("properties", type=dict) + #: A list of volume backends used to replicate volumes on this backend. + replication_targets = resource.Body("replication_targets", type=list) + #: The storage backend for the backend volume. + storage_protocol = resource.Body("storage_protocol") + #: The name of the vendor. + vendor_name = resource.Body("vendor_name") + #: The volume type access. + visibility = resource.Body("visibility") + #: The name of the back-end volume. + volume_backend_name = resource.Body("volume_backend_name") diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 90d0008eb..1b5f061df 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -1219,6 +1219,7 @@ def get_limits(self, project=None): params['project_id'] = resource.Resource._get_id(project) return self._get(_limits.Limit, requires_id=False, **params) + # ====== CAPABILITIES ====== def get_capabilities(self, host): """Get a backend's capabilites diff --git a/openstack/tests/unit/block_storage/v2/test_capabilities.py b/openstack/tests/unit/block_storage/v2/test_capabilities.py new file mode 100644 index 000000000..907a43661 --- /dev/null +++ b/openstack/tests/unit/block_storage/v2/test_capabilities.py @@ -0,0 +1,102 @@ +# 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.block_storage.v2 import capabilities +from openstack.tests.unit import base + +CAPABILITIES = { + "namespace": "OS::Storage::Capabilities::fake", + "vendor_name": "OpenStack", + "volume_backend_name": "lvmdriver-1", + "pool_name": "pool", + "driver_version": "2.0.0", + "storage_protocol": "iSCSI", + "display_name": "Capabilities of Cinder LVM driver", + "description": "These are volume type options", + "visibility": "public", + "replication_targets": [], + "properties": { + "compression": { + "title": "Compression", + "description": "Enables compression.", + "type": "boolean", + }, + "qos": { + "title": "QoS", + "description": "Enables QoS.", + "type": "boolean", + }, + "replication": { + "title": "Replication", + "description": "Enables replication.", + "type": "boolean", + }, + "thin_provisioning": { + "title": "Thin Provisioning", + "description": "Sets thin provisioning.", + "type": "boolean", + }, + }, +} + + +class TestCapabilites(base.TestCase): + def test_basic(self): + capabilities_resource = capabilities.Capabilities() + self.assertEqual(None, capabilities_resource.resource_key) + self.assertEqual(None, capabilities_resource.resources_key) + self.assertEqual("/capabilities", capabilities_resource.base_path) + self.assertTrue(capabilities_resource.allow_fetch) + self.assertFalse(capabilities_resource.allow_create) + self.assertFalse(capabilities_resource.allow_commit) + self.assertFalse(capabilities_resource.allow_delete) + self.assertFalse(capabilities_resource.allow_list) + + def test_make_capabilities(self): + capabilities_resource = capabilities.Capabilities(**CAPABILITIES) + self.assertEqual( + CAPABILITIES["description"], capabilities_resource.description + ) + self.assertEqual( + CAPABILITIES["display_name"], capabilities_resource.display_name + ) + self.assertEqual( + CAPABILITIES["driver_version"], + capabilities_resource.driver_version, + ) + self.assertEqual( + CAPABILITIES["namespace"], capabilities_resource.namespace + ) + self.assertEqual( + CAPABILITIES["pool_name"], capabilities_resource.pool_name + ) + self.assertEqual( + CAPABILITIES["properties"], capabilities_resource.properties + ) + self.assertEqual( + CAPABILITIES["replication_targets"], + capabilities_resource.replication_targets, + ) + self.assertEqual( + CAPABILITIES["storage_protocol"], + capabilities_resource.storage_protocol, + ) + self.assertEqual( + CAPABILITIES["vendor_name"], capabilities_resource.vendor_name + ) + self.assertEqual( + CAPABILITIES["visibility"], capabilities_resource.visibility + ) + self.assertEqual( + CAPABILITIES["volume_backend_name"], + capabilities_resource.volume_backend_name, + ) diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index 4bfe0c46e..bf3a3b66f 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -13,6 +13,7 @@ from openstack.block_storage.v2 import _proxy from openstack.block_storage.v2 import backup +from openstack.block_storage.v2 import capabilities from openstack.block_storage.v2 import quota_set from openstack.block_storage.v2 import snapshot from openstack.block_storage.v2 import stats @@ -289,6 +290,11 @@ def test_backup_reset(self): ) +class TestCapabilities(TestVolumeProxy): + def test_capabilites_get(self): + self.verify_get(self.proxy.get_capabilities, capabilities.Capabilities) + + class TestSnapshot(TestVolumeProxy): def test_snapshot_get(self): self.verify_get(self.proxy.get_snapshot, snapshot.Snapshot) From b3ddf29738c8a1828b709203e35b1397470e0585 Mon Sep 17 00:00:00 2001 From: Yujia Zheng Date: Fri, 6 Oct 2023 18:37:55 +0000 Subject: [PATCH 3398/3836] Bump API version in Manila functional tests In API version 2.64 and beyond, Manila only permits administrators to use the "force" parameter when extending shares. Change-Id: If13f73fcf537bef61c9eaa0bccf670de6ade0604 --- openstack/tests/functional/shared_file_system/base.py | 4 ++-- openstack/tests/functional/shared_file_system/test_share.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/openstack/tests/functional/shared_file_system/base.py b/openstack/tests/functional/shared_file_system/base.py index 72926e901..f12a48c6b 100644 --- a/openstack/tests/functional/shared_file_system/base.py +++ b/openstack/tests/functional/shared_file_system/base.py @@ -22,8 +22,8 @@ def setUp(self): self.require_service( 'shared-file-system', min_microversion=self.min_microversion ) - self._set_operator_cloud(shared_file_system_api_version='2.63') - self._set_user_cloud(shared_file_system_api_version='2.63') + self._set_operator_cloud(shared_file_system_api_version='2.78') + self._set_user_cloud(shared_file_system_api_version='2.78') def create_share(self, **kwargs): share = self.user_cloud.share.create_share(**kwargs) diff --git a/openstack/tests/functional/shared_file_system/test_share.py b/openstack/tests/functional/shared_file_system/test_share.py index c79c6978f..1ff645de6 100644 --- a/openstack/tests/functional/shared_file_system/test_share.py +++ b/openstack/tests/functional/shared_file_system/test_share.py @@ -143,9 +143,10 @@ def test_resize_share_smaller_no_shrink(self): self.assertEqual(self.SHARE_SIZE, get_resized_share.size) def test_resize_share_with_force(self): + """Test that extend with force works as expected.""" # Resize to 3 GiB larger_size = 3 - self.user_cloud.share.resize_share( + self.operator_cloud.share.resize_share( self.SHARE_ID, larger_size, force=True ) From 1d3da1c86b32db6a1c3467d9aaa5dcc456725d9b Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Thu, 14 Sep 2023 13:35:50 -0700 Subject: [PATCH 3399/3836] [baremetal] Add support for parent_nodes Ironic introduced the concept of parent_node in the bobcat development cycle where a nested relationship exists for the purposes of management of nodes and related interactions. This change adds the field on the SDK and allows the field to be queried utilizing the additional features added to the API for listing child nodes and matching nodes with a parent node. Change-Id: I415cf2049e31821a183eed3b17e3701eaa5dd23f --- openstack/baremetal/v1/node.py | 9 +++++++-- openstack/tests/unit/baremetal/v1/test_node.py | 2 ++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index e3f9c1785..61e0cb56d 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -89,6 +89,8 @@ class Node(_common.ListMixin, resource.Resource): 'conductor_group', 'driver', 'fault', + 'include_children', + 'parent_node', 'provision_state', 'resource_class', 'shard', @@ -97,8 +99,8 @@ class Node(_common.ListMixin, resource.Resource): is_maintenance='maintenance', ) - # Ability to query nodes by shard, introduced in 1.82 (Antelope). - _max_microversion = '1.82' + # Ability to query for parent_node, and view the field. + _max_microversion = '1.83' # Properties #: The UUID of the allocation associated with this node. Added in API @@ -170,6 +172,9 @@ class Node(_common.ListMixin, resource.Resource): #: Human readable identifier for the node. May be undefined. Certain words #: are reserved. Added in API microversion 1.5 name = resource.Body("name") + #: The node which serves as the parent_node for this node. + #: Added in API version 1.83 + parent_node = resource.Body("parent_node") #: Links to the collection of ports on this node. ports = resource.Body("ports", type=list) #: Links to the collection of portgroups on this node. Available since diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 75e063373..c210349cc 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -46,6 +46,7 @@ "name": "test_node", "network_interface": "flat", "owner": "4b7ed919-e4a6-4017-a081-43205c5b0b73", + "parent_node": None, "portgroups": [ { "href": "http://127.0.0.1:6385/v1/nodes//portgroups", @@ -136,6 +137,7 @@ def test_instantiate(self): self.assertEqual(FAKE['name'], sot.name) self.assertEqual(FAKE['network_interface'], sot.network_interface) self.assertEqual(FAKE['owner'], sot.owner) + self.assertEqual(FAKE['parent_node'], sot.parent_node) self.assertEqual(FAKE['ports'], sot.ports) self.assertEqual(FAKE['portgroups'], sot.port_groups) self.assertEqual(FAKE['power_state'], sot.power_state) From ae992fc3843adcaa73dc4a9770201b71eb2e8dc6 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Thu, 14 Sep 2023 13:49:53 -0700 Subject: [PATCH 3400/3836] [baremetal] Add unhold provision state verb support Adds support for passing the "unhold" provision state verb to Ironic. The underlying feature added allows step executions for cleaning/provisioning (and later service), to pause operations pending external intervention. This "unhold" provision state verb is that intervention which unpauses the underlying operation allowing it to complete normally. Change-Id: Ic833b1e2f50e3540c2455c161eed08931f023d59 --- openstack/baremetal/v1/_common.py | 1 + openstack/baremetal/v1/node.py | 2 +- openstack/tests/unit/baremetal/v1/test_node.py | 12 ++++++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/openstack/baremetal/v1/_common.py b/openstack/baremetal/v1/_common.py index 626605e04..f6ad15b1c 100644 --- a/openstack/baremetal/v1/_common.py +++ b/openstack/baremetal/v1/_common.py @@ -31,6 +31,7 @@ 'provide': 4, 'rescue': 38, 'unrescue': 38, + 'unhold': 85, } """API microversions introducing provisioning verbs.""" diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 61e0cb56d..3c7cbf3d3 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -100,7 +100,7 @@ class Node(_common.ListMixin, resource.Resource): ) # Ability to query for parent_node, and view the field. - _max_microversion = '1.83' + _max_microversion = '1.85' # Properties #: The UUID of the allocation associated with this node. Added in API diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index c210349cc..01cc80142 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -383,6 +383,18 @@ def test_rebuild_with_deploy_steps(self): retriable_status_codes=_common.RETRIABLE_STATUS_CODES, ) + def test_set_provision_state_unhold(self): + result = self.node.set_provision_state(self.session, 'unhold') + + self.assertIs(result, self.node) + self.session.put.assert_called_once_with( + 'nodes/%s/states/provision' % self.node.id, + json={'target': 'unhold'}, + headers=mock.ANY, + microversion='1.85', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) + @mock.patch.object(node.Node, '_translate_response', mock.Mock()) @mock.patch.object(node.Node, '_get_session', lambda self, x: x) From 3742f2ce3526509581571fcbfe27ce5ba95dd351 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Thu, 14 Sep 2023 14:06:14 -0700 Subject: [PATCH 3401/3836] [baremetal] Add firmware_interface support Adds support for new top level node field firmware_interface which was added in API microversion 1.86 to enable abstraction of operations in relation to firmware. Change-Id: I1b51f460d30d14c7a4afffb8a08a6c05505dfc9b --- openstack/baremetal/v1/node.py | 7 +++++-- openstack/tests/unit/baremetal/v1/test_node.py | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 3c7cbf3d3..877275650 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -99,8 +99,8 @@ class Node(_common.ListMixin, resource.Resource): is_maintenance='maintenance', ) - # Ability to query for parent_node, and view the field. - _max_microversion = '1.85' + # Ability to have a firmware_interface on a node. + _max_microversion = '1.86' # Properties #: The UUID of the allocation associated with this node. Added in API @@ -234,6 +234,9 @@ class Node(_common.ListMixin, resource.Resource): #: Deploy interface to use when deploying the node. #: Introduced in API microversion 1.31. deploy_interface = resource.Body("deploy_interface") + #: Firmware interface to be used when managing the node. + #: Introduced in API microversion 1.86 + firmware_interface = resource.Body("firmware_interface") #: Inspect interface to use when inspecting the node. #: Introduced in API microversion 1.31. inspect_interface = resource.Body("inspect_interface") diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 01cc80142..0c82da05f 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -32,6 +32,7 @@ "driver_info": {"ipmi_password": "******", "ipmi_username": "ADMIN"}, "driver_internal_info": {}, "extra": {}, + "firmware_interface": None, "inspection_finished_at": None, "inspection_started_at": None, "instance_info": {}, @@ -127,6 +128,7 @@ def test_instantiate(self): FAKE['driver_internal_info'], sot.driver_internal_info ) self.assertEqual(FAKE['extra'], sot.extra) + self.assertEqual(FAKE['firmware_interface'], sot.firmware_interface) self.assertEqual(FAKE['instance_info'], sot.instance_info) self.assertEqual(FAKE['instance_uuid'], sot.instance_id) self.assertEqual(FAKE['console_enabled'], sot.is_console_enabled) From 00a610ebac49ede5860228ffe5227e355c99b532 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Thu, 14 Sep 2023 14:33:42 -0700 Subject: [PATCH 3402/3836] [baremetal] Add support for service steps Adds support for baremetal nodes to receive a service command which is used to modify a node in an active state. Also adds service_step to the node object to allow API consumers to understand where the service step operation is at. Change-Id: I0c1e46a209d83d8c641a0f39302330f3d61d8cdf --- openstack/baremetal/v1/_common.py | 1 + openstack/baremetal/v1/node.py | 16 +++++++++++++++- openstack/tests/unit/baremetal/v1/test_node.py | 17 +++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/openstack/baremetal/v1/_common.py b/openstack/baremetal/v1/_common.py index f6ad15b1c..787e9b23e 100644 --- a/openstack/baremetal/v1/_common.py +++ b/openstack/baremetal/v1/_common.py @@ -32,6 +32,7 @@ 'rescue': 38, 'unrescue': 38, 'unhold': 85, + 'service': 87, } """API microversions introducing provisioning verbs.""" diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 877275650..c8d03add6 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -100,7 +100,7 @@ class Node(_common.ListMixin, resource.Resource): ) # Ability to have a firmware_interface on a node. - _max_microversion = '1.86' + _max_microversion = '1.87' # Properties #: The UUID of the allocation associated with this node. Added in API @@ -201,6 +201,9 @@ class Node(_common.ListMixin, resource.Resource): #: A string to be used by external schedulers to identify this node as a #: unit of a specific type of resource. Added in API microversion 1.21. resource_class = resource.Body("resource_class") + #: A string represents the current service step being executed upon. + #: Added in API microversion 1.87. + service_step = resource.Body("service_step") #: A string indicating the shard this node belongs to. Added in API #: microversion 1,82. shard = resource.Body("shard") @@ -399,6 +402,7 @@ def set_provision_state( wait=False, timeout=None, deploy_steps=None, + service_steps=None, ): """Run an action modifying this node's provision state. @@ -421,6 +425,8 @@ def set_provision_state( reached. If ``None``, wait without timeout. :param deploy_steps: Deploy steps to execute, only valid for ``active`` and ``rebuild`` target. + :param service_steps: Service steps to execute, only valid for + ``service`` target. :return: This :class:`Node` instance. :raises: ValueError if ``config_drive``, ``clean_steps``, @@ -474,6 +480,14 @@ def set_provision_state( ) body['deploy_steps'] = deploy_steps + if service_steps is not None: + if target != 'service': + raise ValueError( + 'Service steps can only be provided with ' + '"service" target' + ) + body['service_steps'] = service_steps + if rescue_password is not None: if target != 'rescue': raise ValueError( diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 0c82da05f..a47eec1dc 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -75,6 +75,7 @@ "raid_config": {}, "reservation": None, "resource_class": None, + "service_step": {}, "secure_boot": True, "shard": "TestShard", "states": [ @@ -148,6 +149,7 @@ def test_instantiate(self): self.assertEqual(FAKE['raid_config'], sot.raid_config) self.assertEqual(FAKE['reservation'], sot.reservation) self.assertEqual(FAKE['resource_class'], sot.resource_class) + self.assertEqual(FAKE['service_step'], sot.service_step) self.assertEqual(FAKE['secure_boot'], sot.is_secure_boot) self.assertEqual(FAKE['states'], sot.states) self.assertEqual( @@ -397,6 +399,21 @@ def test_set_provision_state_unhold(self): retriable_status_codes=_common.RETRIABLE_STATUS_CODES, ) + def test_set_provision_state_service(self): + service_steps = [{'interface': 'deploy', 'step': 'hold'}] + result = self.node.set_provision_state( + self.session, 'service', service_steps=service_steps + ) + + self.assertIs(result, self.node) + self.session.put.assert_called_once_with( + 'nodes/%s/states/provision' % self.node.id, + json={'target': 'service', 'service_steps': service_steps}, + headers=mock.ANY, + microversion='1.87', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) + @mock.patch.object(node.Node, '_translate_response', mock.Mock()) @mock.patch.object(node.Node, '_get_session', lambda self, x: x) From 4afd21e13c2abf644677882a5abbfb1809138a24 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 24 Mar 2023 11:40:06 +0000 Subject: [PATCH 3403/3836] compute: Add 'locked_reason' to Server resource Change-Id: Idcdd81c8a32e583d43a386bb4854b55528d9b641 Signed-off-by: Stephen Finucane --- openstack/compute/v2/server.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 6ea37c8a6..2622e7c3b 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -165,7 +165,6 @@ class Server(resource.Resource, metadata.MetadataMixin, tag.TagMixin): #: routable ipv6 addresses, and to private_ipv4 if the Connection was #: created with private=True. Otherwise it will be set to public_ipv4. interface_ip = resource.Computed('interface_ip', default='') - # The locked status of the server is_locked = resource.Body('locked', type=bool) #: The UUID of the kernel image when using an AMI. Will be null if not. @@ -179,6 +178,8 @@ class Server(resource.Resource, metadata.MetadataMixin, tag.TagMixin): launch_index = resource.Body('OS-EXT-SRV-ATTR:launch_index', type=int) #: The timestamp when the server was launched. launched_at = resource.Body('OS-SRV-USG:launched_at') + #: The reason the server was locked, if any. + locked_reason = resource.Body('locked_reason') #: The maximum number of servers to create. max_count = resource.Body('max_count') #: The minimum number of servers to create. From 111951424ab84b683333233b9ab440376a1c4dbf Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Wed, 11 Oct 2023 17:28:50 +0000 Subject: [PATCH 3404/3836] Check Designate API version for shared zones tests This patch adds an API version check in the Designate shared zones fucntional tests. The shared zones tests will skip if the Designate API does not support shared zones. Depends-On: https://review.opendev.org/c/openstack/neutron-vpnaas/+/898153 Closes-Bug: #2039066 Change-Id: Id91a9296ddf5caab339d5fbec3882143a9c25f1a --- openstack/tests/functional/dns/v2/test_zone.py | 7 +++++++ openstack/tests/functional/dns/v2/test_zone_share.py | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/openstack/tests/functional/dns/v2/test_zone.py b/openstack/tests/functional/dns/v2/test_zone.py index 7cea55aa0..f57fe48d8 100644 --- a/openstack/tests/functional/dns/v2/test_zone.py +++ b/openstack/tests/functional/dns/v2/test_zone.py @@ -14,6 +14,7 @@ from openstack import connection from openstack import exceptions from openstack.tests.functional import base +from openstack import utils class TestZone(base.BaseFunctionalTest): @@ -72,6 +73,12 @@ def test_create_rs(self): ) def test_delete_zone_with_shares(self): + # Make sure the API under test has shared zones support + if not utils.supports_version(self.conn.dns, '2.1'): + self.skipTest( + 'Designate API version does not support shared zones.' + ) + zone_name = 'example-{0}.org.'.format(random.randint(1, 10000)) zone = self.conn.dns.create_zone( name=zone_name, diff --git a/openstack/tests/functional/dns/v2/test_zone_share.py b/openstack/tests/functional/dns/v2/test_zone_share.py index 42be9abcf..01870988e 100644 --- a/openstack/tests/functional/dns/v2/test_zone_share.py +++ b/openstack/tests/functional/dns/v2/test_zone_share.py @@ -14,6 +14,7 @@ from openstack import exceptions from openstack.tests.functional import base +from openstack import utils class TestZoneShare(base.BaseFunctionalTest): @@ -29,6 +30,12 @@ def setUp(self): # different tests of the same class. self.ZONE_NAME = 'example-{0}.org.'.format(uuid.uuid4().hex) + # Make sure the API under test has shared zones support + if not utils.supports_version(self.conn.dns, '2.1'): + self.skipTest( + 'Designate API version does not support shared zones.' + ) + self.zone = self.operator_cloud.dns.create_zone( name=self.ZONE_NAME, email='joe@example.org', From 3ec78b7e4cc446f5538bdf3bc849a7bf54c16381 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Thu, 14 Sep 2023 14:59:07 -0700 Subject: [PATCH 3405/3836] [baremetal] Add some missing fields to node object Adds the lessee, and description fields to the node object, and adds basic testing to the conductor_group object to just ensure it is populated when available. Also, adds a test entry for is_automated_clean_enabled, as the value's population was previously not set. Change-Id: Ia1d9232400b679b82f1dfbeef55db3f9a4116862 --- openstack/baremetal/v1/node.py | 7 ++++++- openstack/tests/unit/baremetal/v1/test_node.py | 11 ++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index c8d03add6..65d5f0d40 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -115,7 +115,7 @@ class Node(_common.ListMixin, resource.Resource): chassis_id = resource.Body("chassis_uuid") #: The current clean step. clean_step = resource.Body("clean_step") - #: Hostname of the conductor currently handling this ndoe. Added in API + #: Hostname of the conductor currently handling this node. Added in API # microversion 1.49. conductor = resource.Body("conductor") #: Conductor group this node is managed by. Added in API microversion 1.46. @@ -124,6 +124,8 @@ class Node(_common.ListMixin, resource.Resource): created_at = resource.Body("created_at") #: The current deploy step. Added in API microversion 1.44. deploy_step = resource.Body("deploy_step") + #: The description of the node. Added in API microversion 1.51. + description = resource.Body("description") #: The name of the driver. driver = resource.Body("driver") #: All the metadata required by the driver to manage this node. List of @@ -164,6 +166,9 @@ class Node(_common.ListMixin, resource.Resource): #: Any error from the most recent transaction that started but failed to #: finish. last_error = resource.Body("last_error") + #: Field indicating if the node is leased to a specific project. + #: Added in API version 1.65 + lessee = resource.Body("lessee") #: A list of relative links, including self and bookmark links. links = resource.Body("links", type=list) #: user settable description of the reason why the node was placed into diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index a47eec1dc..430763edb 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -23,11 +23,14 @@ # NOTE: Sample data from api-ref doc FAKE = { + "automated_clean": False, "boot_mode": "uefi", "chassis_uuid": "1", # NOTE: missed in api-ref sample "clean_step": {}, + "conductor_group": None, "console_enabled": False, "created_at": "2016-08-18T22:28:48.643434+00:00", + "description": "A node.", "driver": "agent_ipmitool", "driver_info": {"ipmi_password": "******", "ipmi_username": "ADMIN"}, "driver_internal_info": {}, @@ -38,6 +41,7 @@ "instance_info": {}, "instance_uuid": None, "last_error": None, + "lessee": None, "links": [ {"href": "http://127.0.0.1:6385/v1/nodes/", "rel": "self"}, {"href": "http://127.0.0.1:6385/nodes/", "rel": "bookmark"}, @@ -118,11 +122,15 @@ def test_instantiate(self): self.assertEqual(FAKE['uuid'], sot.id) self.assertEqual(FAKE['name'], sot.name) - + self.assertEqual( + FAKE['automated_clean'], sot.is_automated_clean_enabled + ) self.assertEqual(FAKE['boot_mode'], sot.boot_mode) self.assertEqual(FAKE['chassis_uuid'], sot.chassis_id) self.assertEqual(FAKE['clean_step'], sot.clean_step) + self.assertEqual(FAKE['conductor_group'], sot.conductor_group) self.assertEqual(FAKE['created_at'], sot.created_at) + self.assertEqual(FAKE['description'], sot.description) self.assertEqual(FAKE['driver'], sot.driver) self.assertEqual(FAKE['driver_info'], sot.driver_info) self.assertEqual( @@ -135,6 +143,7 @@ def test_instantiate(self): self.assertEqual(FAKE['console_enabled'], sot.is_console_enabled) self.assertEqual(FAKE['maintenance'], sot.is_maintenance) self.assertEqual(FAKE['last_error'], sot.last_error) + self.assertEqual(FAKE['lessee'], sot.lessee) self.assertEqual(FAKE['links'], sot.links) self.assertEqual(FAKE['maintenance_reason'], sot.maintenance_reason) self.assertEqual(FAKE['name'], sot.name) From be0ed5f92eb4b0e0373a44bb1ba9a3328df9b7fd Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Fri, 20 Oct 2023 10:36:30 +1300 Subject: [PATCH 3406/3836] [baremetal] Add missing owner attribute to allocation Change-Id: I9f4529764fc69dd398c800b5f25185e3c5b3202c --- openstack/baremetal/v1/allocation.py | 6 +++++- openstack/tests/unit/baremetal/v1/test_allocation.py | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/openstack/baremetal/v1/allocation.py b/openstack/baremetal/v1/allocation.py index 3179f15ba..dadb5e34d 100644 --- a/openstack/baremetal/v1/allocation.py +++ b/openstack/baremetal/v1/allocation.py @@ -34,12 +34,14 @@ class Allocation(_common.ListMixin, resource.Resource): 'node', 'resource_class', 'state', + 'owner', fields={'type': _common.fields_type}, ) # Allocation update is available since 1.57 # Backfilling allocations is available since 1.58 - _max_microversion = '1.58' + # owner attribute is available since 1.60 + _max_microversion = '1.60' #: The candidate nodes for this allocation. candidate_nodes = resource.Body('candidate_nodes', type=list) @@ -60,6 +62,8 @@ class Allocation(_common.ListMixin, resource.Resource): node = resource.Body('node') #: UUID of the node this allocation belongs to. node_id = resource.Body('node_uuid') + #: The tenant who owns the object + owner = resource.Body('owner') #: The requested resource class. resource_class = resource.Body('resource_class') #: The state of the allocation. diff --git a/openstack/tests/unit/baremetal/v1/test_allocation.py b/openstack/tests/unit/baremetal/v1/test_allocation.py index 4c532d935..5d6bd3087 100644 --- a/openstack/tests/unit/baremetal/v1/test_allocation.py +++ b/openstack/tests/unit/baremetal/v1/test_allocation.py @@ -35,6 +35,7 @@ ], "name": "test_allocation", "node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d", + "owner": "demo", "resource_class": "baremetal", "state": "active", "traits": [], @@ -64,6 +65,7 @@ def test_instantiate(self): self.assertEqual(FAKE['links'], sot.links) self.assertEqual(FAKE['name'], sot.name) self.assertEqual(FAKE['node_uuid'], sot.node_id) + self.assertEqual(FAKE['owner'], sot.owner) self.assertEqual(FAKE['resource_class'], sot.resource_class) self.assertEqual(FAKE['state'], sot.state) self.assertEqual(FAKE['traits'], sot.traits) From cb3a426ab0b0b77944202d894ccc09bb820f3d1c Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Fri, 20 Oct 2023 10:49:48 +1300 Subject: [PATCH 3407/3836] [baremetal] driver add missing firmware interface Change-Id: Icd6861b2edb32078a9377dd538644fe666a6e60d --- openstack/baremetal/v1/driver.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openstack/baremetal/v1/driver.py b/openstack/baremetal/v1/driver.py index 9018816d0..c4bb374ed 100644 --- a/openstack/baremetal/v1/driver.py +++ b/openstack/baremetal/v1/driver.py @@ -30,7 +30,8 @@ class Driver(resource.Resource): _query_mapping = resource.QueryParameters(details='detail') # The BIOS interface fields introduced in 1.40 (Rocky). - _max_microversion = '1.40' + # The firmware interface fields introduced in 1.86. + _max_microversion = '1.86' #: A list of active hosts that support this driver. hosts = resource.Body('hosts', type=list) @@ -56,6 +57,9 @@ class Driver(resource.Resource): #: Default deploy interface implementation. #: Introduced in API microversion 1.30. default_deploy_interface = resource.Body("default_deploy_interface") + #: Default firmware interface implementation. + #: Introduced in API microversion 1.86. + default_firmware_interface = resource.Body("default_firmware_interface") #: Default inspect interface implementation. #: Introduced in API microversion 1.30. default_inspect_interface = resource.Body("default_inspect_interface") @@ -95,6 +99,9 @@ class Driver(resource.Resource): #: Enabled deploy interface implementations. #: Introduced in API microversion 1.30. enabled_deploy_interfaces = resource.Body("enabled_deploy_interfaces") + #: Enabled firmware interface implementations. + #: Introduced in API microversion 1.86. + enabled_firmware_interfaces = resource.Body("enabled_firmware_interfaces") #: Enabled inspect interface implementations. #: Introduced in API microversion 1.30. enabled_inspect_interfaces = resource.Body("enabled_inspect_interfaces") From 1326d1bf75046740a542d1313ef2180ceb5e0903 Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Fri, 20 Oct 2023 13:40:51 +1300 Subject: [PATCH 3408/3836] [baremetal] port add shard, is_smartnic, name Change-Id: Iead7d129479a850c60f08b366f0f9e4f02a53b83 --- openstack/baremetal/v1/port.py | 10 +++++++++- openstack/tests/unit/baremetal/v1/test_port.py | 4 ++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/openstack/baremetal/v1/port.py b/openstack/baremetal/v1/port.py index 1435d7c13..fa2511865 100644 --- a/openstack/baremetal/v1/port.py +++ b/openstack/baremetal/v1/port.py @@ -32,12 +32,16 @@ class Port(_common.ListMixin, resource.Resource): 'address', 'node', 'portgroup', + 'shard', fields={'type': _common.fields_type}, node_id='node_uuid', ) # The physical_network field introduced in 1.34 - _max_microversion = '1.34' + # The is_smartnic field added in 1.53 + # Query filter by shard added in 1.82 + # The name field added in 1.88 + _max_microversion = '1.88' #: The physical hardware address of the network port, typically the #: hardware MAC address. @@ -53,6 +57,8 @@ class Port(_common.ListMixin, resource.Resource): internal_info = resource.Body('internal_info') #: Whether PXE is enabled on the port. Added in API microversion 1.19. is_pxe_enabled = resource.Body('pxe_enabled', type=bool) + #: Whether the port is a Smart NIC port. Added in API microversion 1.53. + is_smartnic = resource.Body('is_smartnic', type=bool) #: A list of relative links, including the self and bookmark links. links = resource.Body('links', type=list) #: The port bindig profile. If specified, must contain ``switch_id`` and @@ -60,6 +66,8 @@ class Port(_common.ListMixin, resource.Resource): #: to be used to store vendor specific information. Added in API #: microversion 1.19. local_link_connection = resource.Body('local_link_connection') + #: The name of the port + name = resource.Body('name') #: The UUID of node this port belongs to node_id = resource.Body('node_uuid') #: The name of physical network this port is attached to. diff --git a/openstack/tests/unit/baremetal/v1/test_port.py b/openstack/tests/unit/baremetal/v1/test_port.py index ed98f090e..d688d7fbe 100644 --- a/openstack/tests/unit/baremetal/v1/test_port.py +++ b/openstack/tests/unit/baremetal/v1/test_port.py @@ -19,6 +19,7 @@ "created_at": "2016-08-18T22:28:49.946416+00:00", "extra": {}, "internal_info": {}, + "is_smartnic": True, "links": [ {"href": "http://127.0.0.1:6385/v1/ports/", "rel": "self"}, {"href": "http://127.0.0.1:6385/ports/", "rel": "bookmark"}, @@ -28,6 +29,7 @@ "switch_id": "0a:1b:2c:3d:4e:5f", "switch_info": "switch1", }, + "name": "port_name", "node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d", "portgroup_uuid": "e43c722c-248e-4c6e-8ce8-0d8ff129387a", "pxe_enabled": True, @@ -56,10 +58,12 @@ def test_instantiate(self): self.assertEqual(FAKE['created_at'], sot.created_at) self.assertEqual(FAKE['extra'], sot.extra) self.assertEqual(FAKE['internal_info'], sot.internal_info) + self.assertEqual(FAKE['is_smartnic'], sot.is_smartnic) self.assertEqual(FAKE['links'], sot.links) self.assertEqual( FAKE['local_link_connection'], sot.local_link_connection ) + self.assertEqual(FAKE['name'], sot.name) self.assertEqual(FAKE['node_uuid'], sot.node_id) self.assertEqual(FAKE['portgroup_uuid'], sot.port_group_id) self.assertEqual(FAKE['pxe_enabled'], sot.is_pxe_enabled) From 64e197d1ef7361cab31d779467fed8827b1b901d Mon Sep 17 00:00:00 2001 From: Rajat Dhasmana Date: Tue, 12 Sep 2023 02:32:04 +0530 Subject: [PATCH 3409/3836] Add snapshot manage unmanage support Add support for volume snapshot manage and volume snapshot unmanage operations. Change-Id: I25710af7b7a85a07e133aa3df94d2d751e253e84 --- doc/source/user/proxies/block_storage_v3.rst | 2 +- openstack/block_storage/v3/_proxy.py | 24 ++++++ openstack/block_storage/v3/snapshot.py | 36 ++++++++ .../tests/unit/block_storage/v3/test_proxy.py | 18 ++++ .../unit/block_storage/v3/test_snapshot.py | 83 ++++++++++++++++++- ...age-unmanage-support-fc0be2a3fb4427d1.yaml | 5 ++ 6 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/add-volume-snapshot-manage-unmanage-support-fc0be2a3fb4427d1.yaml diff --git a/doc/source/user/proxies/block_storage_v3.rst b/doc/source/user/proxies/block_storage_v3.rst index abe3f0e73..3c3ee8e9e 100644 --- a/doc/source/user/proxies/block_storage_v3.rst +++ b/doc/source/user/proxies/block_storage_v3.rst @@ -118,7 +118,7 @@ Snapshot Operations :members: create_snapshot, delete_snapshot, update_snapshot, get_snapshot, find_snapshot, snapshots, get_snapshot_metadata, set_snapshot_metadata, delete_snapshot_metadata, reset_snapshot, - set_snapshot_status + set_snapshot_status, manage_snapshot, unmanage_snapshot Stats Operations ^^^^^^^^^^^^^^^^ diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 4975103b2..20a607c36 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -253,6 +253,30 @@ def set_snapshot_status(self, snapshot, status, progress=None): snapshot = self._get_resource(_snapshot.Snapshot, snapshot) snapshot.set_status(self, status, progress) + def manage_snapshot(self, **attrs): + """Creates a snapshot by using existing storage rather than + allocating new storage. + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.block_storage.v3.snapshot.Snapshot`, + comprised of the properties on the Snapshot class. + + :returns: The results of snapshot creation + :rtype: :class:`~openstack.block_storage.v3.snapshot.Snapshot` + """ + return _snapshot.Snapshot.manage(self, **attrs) + + def unmanage_snapshot(self, snapshot): + """Unmanage a snapshot from block storage provisioning. + + :param snapshot: Either the ID of a snapshot or a + :class:`~openstack.block_storage.v3.snapshot.Snapshot`. + + :returns: None + """ + snapshot_obj = self._get_resource(_snapshot.Snapshot, snapshot) + snapshot_obj.unmanage(self) + # ====== TYPES ====== def get_type(self, type): """Get a single type diff --git a/openstack/block_storage/v3/snapshot.py b/openstack/block_storage/v3/snapshot.py index d551db259..418e1de4b 100644 --- a/openstack/block_storage/v3/snapshot.py +++ b/openstack/block_storage/v3/snapshot.py @@ -65,6 +65,8 @@ class Snapshot(resource.Resource, metadata.MetadataMixin): #: The ID of the volume this snapshot was taken of. volume_id = resource.Body("volume_id") + _max_microversion = '3.8' + def _action(self, session, body, microversion=None): """Preform backup actions given the message body.""" url = utils.urljoin(self.base_path, self.id, 'action') @@ -91,5 +93,39 @@ def set_status(self, session, status, progress=None): body['os-update_snapshot_status']['progress'] = progress self._action(session, body) + @classmethod + def manage( + cls, + session, + volume_id, + ref, + name=None, + description=None, + metadata=None, + ): + """Manage a snapshot under block storage provisioning.""" + url = '/manageable_snapshots' + if not utils.supports_microversion(session, '3.8'): + url = '/os-snapshot-manage' + body = { + 'snapshot': { + 'volume_id': volume_id, + 'ref': ref, + 'name': name, + 'description': description, + 'metadata': metadata, + } + } + resp = session.post(url, json=body, microversion=cls._max_microversion) + exceptions.raise_from_response(resp) + snapshot = Snapshot() + snapshot._translate_response(resp) + return snapshot + + def unmanage(self, session): + """Unmanage a snapshot from block storage provisioning.""" + body = {'os-unmanage': None} + self._action(session, body) + SnapshotDetail = Snapshot diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index bb517b7e2..a67ddf353 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -785,6 +785,24 @@ def test_delete_snapshot_metadata(self): expected_args=[self.proxy, "key"], ) + def test_manage_snapshot(self): + kwargs = { + "volume_id": "fake_id", + "remote_source": "fake_volume", + "snapshot_name": "fake_snap", + "description": "test_snap", + "property": {"k": "v"}, + } + self._verify( + "openstack.block_storage.v3.snapshot.Snapshot.manage", + self.proxy.manage_snapshot, + method_kwargs=kwargs, + method_result=snapshot.Snapshot(id="fake_id"), + expected_args=[self.proxy], + expected_kwargs=kwargs, + expected_result=snapshot.Snapshot(id="fake_id"), + ) + class TestType(TestVolumeProxy): def test_type_get(self): diff --git a/openstack/tests/unit/block_storage/v3/test_snapshot.py b/openstack/tests/unit/block_storage/v3/test_snapshot.py index bd5d7d517..128af5421 100644 --- a/openstack/tests/unit/block_storage/v3/test_snapshot.py +++ b/openstack/tests/unit/block_storage/v3/test_snapshot.py @@ -9,6 +9,7 @@ # 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 from unittest import mock from keystoneauth1 import adapter @@ -18,6 +19,7 @@ FAKE_ID = "ffa9bc5e-1172-4021-acaf-cdcd78a9584d" +FAKE_VOLUME_ID = "5aa119a8-d25b-45a7-8d1b-88e127885635" SNAPSHOT = { "status": "creating", @@ -25,7 +27,7 @@ "created_at": "2015-03-09T12:14:57.233772", "updated_at": None, "metadata": {}, - "volume_id": "5aa119a8-d25b-45a7-8d1b-88e127885635", + "volume_id": FAKE_VOLUME_ID, "size": 1, "id": FAKE_ID, "name": "snap-001", @@ -130,3 +132,82 @@ def test_set_status(self): self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion ) + + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=True, + ) + def test_manage(self, mock_mv): + resp = mock.Mock() + resp.body = {'snapshot': copy.deepcopy(SNAPSHOT)} + resp.json = mock.Mock(return_value=resp.body) + resp.headers = {} + resp.status_code = 202 + + self.sess.post = mock.Mock(return_value=resp) + + sot = snapshot.Snapshot.manage( + self.sess, volume_id=FAKE_VOLUME_ID, ref=FAKE_ID + ) + + self.assertIsNotNone(sot) + + url = '/manageable_snapshots' + body = { + 'snapshot': { + 'volume_id': FAKE_VOLUME_ID, + 'ref': FAKE_ID, + 'name': None, + 'description': None, + 'metadata': None, + } + } + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion + ) + + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=False, + ) + def test_manage_pre_38(self, mock_mv): + resp = mock.Mock() + resp.body = {'snapshot': copy.deepcopy(SNAPSHOT)} + resp.json = mock.Mock(return_value=resp.body) + resp.headers = {} + resp.status_code = 202 + + self.sess.post = mock.Mock(return_value=resp) + + sot = snapshot.Snapshot.manage( + self.sess, volume_id=FAKE_VOLUME_ID, ref=FAKE_ID + ) + + self.assertIsNotNone(sot) + + url = '/os-snapshot-manage' + body = { + 'snapshot': { + 'volume_id': FAKE_VOLUME_ID, + 'ref': FAKE_ID, + 'name': None, + 'description': None, + 'metadata': None, + } + } + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion + ) + + def test_unmanage(self): + sot = snapshot.Snapshot(**SNAPSHOT) + + self.assertIsNone(sot.unmanage(self.sess)) + + url = 'snapshots/%s/action' % FAKE_ID + body = {'os-unmanage': None} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion + ) diff --git a/releasenotes/notes/add-volume-snapshot-manage-unmanage-support-fc0be2a3fb4427d1.yaml b/releasenotes/notes/add-volume-snapshot-manage-unmanage-support-fc0be2a3fb4427d1.yaml new file mode 100644 index 000000000..b54379ad4 --- /dev/null +++ b/releasenotes/notes/add-volume-snapshot-manage-unmanage-support-fc0be2a3fb4427d1.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added support for volume snapshot manage and volume + snapshot unmanage. From 4303afc9aea34c04a31e58349760d1014d22b3ab Mon Sep 17 00:00:00 2001 From: Rajat Dhasmana Date: Tue, 29 Aug 2023 11:36:36 +0530 Subject: [PATCH 3410/3836] Fix: Update/Delete type encryption operation When requesting to update/delete the volume type encryption, we do a GET encryption type call and fail with KeyError: 'volume_type_id'. This is because the request URL for GET encryption type is created as '/types/%(volume_type_id)s/encryption' and we are passing 'volume_type' instead of 'volume_type_id'. This patch fixes the issue by passing 'volume_type_id'. Story: #2010896 Task: #48686 Change-Id: Ic3d528e0e7b8e07a72f83cbe84442dc17ae32c27 --- openstack/block_storage/v3/_proxy.py | 4 ++-- .../tests/unit/block_storage/v3/test_proxy.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index b729f9720..f1bf09bf4 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -461,7 +461,7 @@ def delete_type_encryption( volume_type = self._get_resource(_type.Type, volume_type) encryption = self._get( _type.TypeEncryption, - volume_type=volume_type.id, + volume_type_id=volume_type.id, requires_id=False, ) @@ -494,7 +494,7 @@ def update_type_encryption( volume_type = self._get_resource(_type.Type, volume_type) encryption = self._get( _type.TypeEncryption, - volume_type=volume_type.id, + volume_type_id=volume_type.id, requires_id=False, ) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 2efe2fbfc..6f4dea4a6 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -825,11 +825,27 @@ def test_type_encryption_create(self): ) def test_type_encryption_update(self): + # Verify that the get call was made with correct kwargs + self.verify_get( + self.proxy.get_type_encryption, + type.TypeEncryption, + method_args=['value'], + expected_args=[], + expected_kwargs={'volume_type_id': 'value', 'requires_id': False}, + ) self.verify_update( self.proxy.update_type_encryption, type.TypeEncryption ) def test_type_encryption_delete(self): + # Verify that the get call was made with correct kwargs + self.verify_get( + self.proxy.get_type_encryption, + type.TypeEncryption, + method_args=['value'], + expected_args=[], + expected_kwargs={'volume_type_id': 'value', 'requires_id': False}, + ) self.verify_delete( self.proxy.delete_type_encryption, type.TypeEncryption, False ) From ea8c5d5a5f720a59539c2db2234128dd897ddb78 Mon Sep 17 00:00:00 2001 From: Dmitrii Shcherbakov Date: Fri, 7 Jul 2023 15:34:44 +0300 Subject: [PATCH 3411/3836] Support the API for managing external gateways Relevant Neutron core change: https://review.opendev.org/c/openstack/neutron/+/873593 Co-Authored-by: Frode Nordahl Partial-Bug: #2002687 Change-Id: Idedce3c0ae6f4239ff3c1df35dc1b9af0add0a58 --- doc/source/user/proxies/network.rst | 2 + openstack/network/v2/_proxy.py | 36 +++++++++++++++ openstack/network/v2/router.py | 46 +++++++++++++++++++ openstack/tests/unit/network/v2/test_proxy.py | 42 +++++++++++++++++ 4 files changed, 126 insertions(+) diff --git a/doc/source/user/proxies/network.rst b/doc/source/user/proxies/network.rst index 81ad0b79f..a742f9996 100644 --- a/doc/source/user/proxies/network.rst +++ b/doc/source/user/proxies/network.rst @@ -39,6 +39,8 @@ Router Operations :members: create_router, update_router, delete_router, get_router, find_router, routers, add_gateway_to_router, remove_gateway_from_router, + add_external_gateways, update_external_gateways, + remove_external_gateways, add_interface_to_router, remove_interface_from_router, add_extra_routes_to_router, remove_extra_routes_from_router, create_conntrack_helper, update_conntrack_helper, diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index ab311c351..b98bcd5dd 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -4099,6 +4099,42 @@ def remove_gateway_from_router(self, router, **body): router = self._get_resource(_router.Router, router) return router.remove_gateway(self, **body) + def add_external_gateways(self, router, body): + """Add router external gateways + + :param router: Either the router ID or an instance of + :class:`~openstack.network.v2.router.Router` + :param body: Body containing the external_gateways parameter. + :returns: Router with added gateways + :rtype: :class:`~openstack.network.v2.router.Router` + """ + router = self._get_resource(_router.Router, router) + return router.add_external_gateways(self, body) + + def update_external_gateways(self, router, body): + """Update router external gateways + + :param router: Either the router ID or an instance of + :class:`~openstack.network.v2.router.Router` + :param body: Body containing the external_gateways parameter. + :returns: Router with updated gateways + :rtype: :class:`~openstack.network.v2.router.Router` + """ + router = self._get_resource(_router.Router, router) + return router.update_external_gateways(self, body) + + def remove_external_gateways(self, router, body): + """Remove router external gateways + + :param router: Either the router ID or an instance of + :class:`~openstack.network.v2.router.Router` + :param body: Body containing the external_gateways parameter. + :returns: Router without the removed gateways + :rtype: :class:`~openstack.network.v2.router.Router` + """ + router = self._get_resource(_router.Router, router) + return router.remove_external_gateways(self, body) + def routers_hosting_l3_agents(self, router, **query): """Return a generator of L3 agent hosting a router diff --git a/openstack/network/v2/router.py b/openstack/network/v2/router.py index 30e10ca98..a803c1a49 100644 --- a/openstack/network/v2/router.py +++ b/openstack/network/v2/router.py @@ -177,6 +177,52 @@ def remove_gateway(self, session, **body): resp = session.put(url, json=body) return resp.json() + def add_external_gateways(self, session, body): + """Add external gateways to a router. + + :param session: The session to communicate through. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param dict body: The body requested to be updated on the router + + :returns: The body of the response as a dictionary. + """ + url = utils.urljoin(self.base_path, self.id, 'add_external_gateways') + resp = session.put(url, json=body) + self._translate_response(resp) + return self + + def update_external_gateways(self, session, body): + """Update external gateways of a router. + + :param session: The session to communicate through. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param dict body: The body requested to be updated on the router + + :returns: The body of the response as a dictionary. + """ + url = utils.urljoin( + self.base_path, self.id, 'update_external_gateways' + ) + resp = session.put(url, json=body) + self._translate_response(resp) + return self + + def remove_external_gateways(self, session, body): + """Remove external gateways from a router. + + :param session: The session to communicate through. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param dict body: The body requested to be updated on the router + + :returns: The body of the response as a dictionary. + """ + url = utils.urljoin( + self.base_path, self.id, 'remove_external_gateways' + ) + resp = session.put(url, json=body) + self._translate_response(resp) + return self + class L3AgentRouter(Router): resource_key = 'router' diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 9747fb0d1..844e844a4 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -1511,6 +1511,48 @@ def test_remove_gateway_from_router(self, mock_remove, mock_get): ) mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") + @mock.patch.object(proxy_base.Proxy, '_get_resource') + @mock.patch.object(router.Router, 'add_external_gateways') + def test_add_external_gateways(self, mock_add, mock_get): + x_router = router.Router.new(id="ROUTER_ID") + mock_get.return_value = x_router + + self._verify( + "openstack.network.v2.router.Router.add_external_gateways", + self.proxy.add_external_gateways, + method_args=["FAKE_ROUTER", "bar"], + expected_args=[self.proxy, "bar"], + ) + mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") + + @mock.patch.object(proxy_base.Proxy, '_get_resource') + @mock.patch.object(router.Router, 'update_external_gateways') + def test_update_external_gateways(self, mock_remove, mock_get): + x_router = router.Router.new(id="ROUTER_ID") + mock_get.return_value = x_router + + self._verify( + "openstack.network.v2.router.Router.update_external_gateways", + self.proxy.update_external_gateways, + method_args=["FAKE_ROUTER", "bar"], + expected_args=[self.proxy, "bar"], + ) + mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") + + @mock.patch.object(proxy_base.Proxy, '_get_resource') + @mock.patch.object(router.Router, 'remove_external_gateways') + def test_remove_external_gateways(self, mock_remove, mock_get): + x_router = router.Router.new(id="ROUTER_ID") + mock_get.return_value = x_router + + self._verify( + "openstack.network.v2.router.Router.remove_external_gateways", + self.proxy.remove_external_gateways, + method_args=["FAKE_ROUTER", "bar"], + expected_args=[self.proxy, "bar"], + ) + mock_get.assert_called_once_with(router.Router, "FAKE_ROUTER") + def test_router_hosting_l3_agents_list(self): self.verify_list( self.proxy.routers_hosting_l3_agents, From 9979a3c8f22218806299a3828fce262072f2627e Mon Sep 17 00:00:00 2001 From: Jan Hartkopf Date: Thu, 26 Oct 2023 14:06:55 +0200 Subject: [PATCH 3412/3836] Fix project cleanup for routers with static routes If a router has static routes configured, its interfaces within the static route's subnet cannot be deleted. This causes all other dependent resources' (ports, subnets, ...) deletion to also fail. Fix this by ensuring a router has no static routes, or remove them first. Story: 2010910 Task: 48754 Change-Id: I16711a9a136b0b7b21cdc850c64b777fcf5cf601 Signed-off-by: Jan Hartkopf --- openstack/network/v2/_proxy.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index ab311c351..444c776df 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -6887,10 +6887,11 @@ def _service_cleanup( resource_evaluation_fn=resource_evaluation_fn, ) - if ( - not self.should_skip_resource_cleanup("network", skip_resources) - and not self.should_skip_resource_cleanup("port", skip_resources) - and not self.should_skip_resource_cleanup("subnet", skip_resources) + if not ( + self.should_skip_resource_cleanup("network", skip_resources) + or self.should_skip_resource_cleanup("router", skip_resources) + or self.should_skip_resource_cleanup("port", skip_resources) + or self.should_skip_resource_cleanup("subnet", skip_resources) ): # Networks are crazy, try to delete router+net+subnet # if there are no "other" ports allocated on the net @@ -6942,7 +6943,22 @@ def _service_cleanup( for port in router_if: if client_status_queue: client_status_queue.put(port) + + router = self.get_router(port.device_id) if not dry_run: + # Router interfaces cannot be deleted when the router has + # static routes, so remove those first + if len(router.routes) > 0: + try: + self.remove_extra_routes_from_router( + router, + {"router": {"routes": router.routes}}, + ) + except exceptions.SDKException: + self.log.error( + f"Cannot delete routes {router.routes} from router {router}" + ) + try: self.remove_interface_from_router( router=port.device_id, port_id=port.id @@ -6952,7 +6968,7 @@ def _service_cleanup( # router disconnected, drop it self._service_cleanup_del_res( self.delete_router, - self.get_router(port.device_id), + router, dry_run=dry_run, client_status_queue=client_status_queue, identified_resources=identified_resources, @@ -7001,9 +7017,9 @@ def _service_cleanup( ) else: self.log.debug( - "Skipping cleanup of networks, ports and subnets " - "as those resources require none of them to be " - "excluded, but at least one should be kept" + "Skipping cleanup of networks, routers, ports and subnets " + "as those resources require all of them to be cleaned up" + "together, but at least one should be kept" ) if not self.should_skip_resource_cleanup("router", skip_resources): From 0b71363dd6822a7cc72aca78588a208fefc7270a Mon Sep 17 00:00:00 2001 From: whoami-rajat Date: Tue, 27 Jun 2023 17:42:49 +0530 Subject: [PATCH 3413/3836] Add volume transfer support [1/2] This patch adds support for volume transfer create, find and delete. Change-Id: I759a583d94d2de508efe76ed38ae4d2ba0e85a48 --- doc/source/user/proxies/block_storage_v3.rst | 7 + .../resources/block_storage/v3/transfer.rst | 13 ++ openstack/block_storage/v3/_proxy.py | 56 ++++++ openstack/block_storage/v3/transfer.py | 173 ++++++++++++++++++ .../block_storage/v3/test_transfer.py | 58 ++++++ .../unit/block_storage/v3/test_transfer.py | 99 ++++++++++ ...ume-transfer-support-28bf34a243d96e1b.yaml | 5 + 7 files changed, 411 insertions(+) create mode 100644 doc/source/user/resources/block_storage/v3/transfer.rst create mode 100644 openstack/block_storage/v3/transfer.py create mode 100644 openstack/tests/functional/block_storage/v3/test_transfer.py create mode 100644 openstack/tests/unit/block_storage/v3/test_transfer.py create mode 100644 releasenotes/notes/add-volume-transfer-support-28bf34a243d96e1b.yaml diff --git a/doc/source/user/proxies/block_storage_v3.rst b/doc/source/user/proxies/block_storage_v3.rst index c7cc4b085..2c830ce90 100644 --- a/doc/source/user/proxies/block_storage_v3.rst +++ b/doc/source/user/proxies/block_storage_v3.rst @@ -156,3 +156,10 @@ Attachments :noindex: :members: create_attachment, get_attachment, attachments, delete_attachment, update_attachment, complete_attachment + +Transfer Operations +^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v3._proxy.Proxy + :noindex: + :members: create_transfer, delete_transfer, find_transfer diff --git a/doc/source/user/resources/block_storage/v3/transfer.rst b/doc/source/user/resources/block_storage/v3/transfer.rst new file mode 100644 index 000000000..e738c7529 --- /dev/null +++ b/doc/source/user/resources/block_storage/v3/transfer.rst @@ -0,0 +1,13 @@ +openstack.block_storage.v3.transfer +=================================== + +.. automodule:: openstack.block_storage.v3.transfer + +The Volume Transfer Class +------------------------- + +The ``Volume Transfer`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v3.transfer.Transfer + :members: diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 1b5f061df..9524cc8cc 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -28,6 +28,7 @@ from openstack.block_storage.v3 import service as _service from openstack.block_storage.v3 import snapshot as _snapshot from openstack.block_storage.v3 import stats as _stats +from openstack.block_storage.v3 import transfer as _transfer from openstack.block_storage.v3 import type as _type from openstack.block_storage.v3 import volume as _volume from openstack import exceptions @@ -51,6 +52,7 @@ class Proxy(_base_proxy.BaseBlockStorageProxy): "snapshot": _snapshot.Snapshot, "stats_pools": _stats.Pools, "summary": _summary.BlockStorageSummary, + "transfer": _transfer.Transfer, "type": _type.Type, "volume": _volume.Volume, } @@ -1861,6 +1863,60 @@ def extensions(self): """ return self._list(_extension.Extension) + # ===== TRANFERS ===== + + def create_transfer(self, **attrs): + """Create a new Transfer record + + :param volume_id: The value is ID of the volume. + :param name: The value is name of the transfer + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.block_storage.v3.transfer.Transfer` + comprised of the properties on the Transfer class. + :returns: The results of Transfer creation + :rtype: :class:`~openstack.block_storage.v3.transfer.Transfer` + """ + return self._create(_transfer.Transfer, **attrs) + + def delete_transfer(self, transfer, ignore_missing=True): + """Delete a volume transfer + + :param transfer: The value can be either the ID of a transfer or a + :class:`~openstack.block_storage.v3.transfer.Transfer`` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the transfer does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent transfer. + + :returns: ``None`` + """ + self._delete( + _transfer.Transfer, + transfer, + ignore_missing=ignore_missing, + ) + + def find_transfer(self, name_or_id, ignore_missing=True): + """Find a single transfer + + :param name_or_id: The name or ID a transfer + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the volume transfer does not exist. + + :returns: One :class:`~openstack.block_storage.v3.transfer.Transfer` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple + resources are found. + """ + return self._find( + _transfer.Transfer, + name_or_id, + ignore_missing=ignore_missing, + ) + # ====== UTILS ====== def wait_for_status( self, diff --git a/openstack/block_storage/v3/transfer.py b/openstack/block_storage/v3/transfer.py new file mode 100644 index 000000000..27d9fd9fc --- /dev/null +++ b/openstack/block_storage/v3/transfer.py @@ -0,0 +1,173 @@ +# 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 resource +from openstack import utils + + +class Transfer(resource.Resource): + resource_key = "transfer" + resources_key = "transfers" + base_path = "/volume-transfers" + + # capabilities + allow_create = True + allow_delete = True + allow_fetch = True + + # Properties + #: UUID of the transfer. + id = resource.Body("id") + #: The date and time when the resource was created. + created_at = resource.Body("created_at") + #: Name of the volume to transfer. + name = resource.Body("name") + #: ID of the volume to transfer. + volume_id = resource.Body("volume_id") + #: Auth key for the transfer. + auth_key = resource.Body("auth_key") + #: A list of links associated with this volume. *Type: list* + links = resource.Body("links") + #: Whether to transfer snapshots or not + no_snapshots = resource.Body("no_snapshots") + + _max_microversion = "3.55" + + def create( + self, + session, + prepend_key=True, + base_path=None, + *, + resource_request_key=None, + resource_response_key=None, + microversion=None, + **params, + ): + """Create a volume transfer. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param prepend_key: A boolean indicating whether the resource_key + should be prepended in a resource creation request. Default to + True. + :param str base_path: Base part of the URI for creating resources, if + different from :data:`~openstack.resource.Resource.base_path`. + :param str resource_request_key: Overrides the usage of + self.resource_key when prepending a key to the request body. + Ignored if `prepend_key` is false. + :param str resource_response_key: Overrides the usage of + self.resource_key when processing response bodies. + Ignored if `prepend_key` is false. + :param str microversion: API version to override the negotiated one. + :param dict params: Additional params to pass. + :return: This :class:`Resource` instance. + :raises: :exc:`~openstack.exceptions.MethodNotSupported` if + :data:`Resource.allow_create` is not set to ``True``. + """ + + # With MV 3.55 we introduced new API for volume transfer + # (/volume-transfers). Prior to that (MV < 3.55), we use + # the old API (/os-volume-transfer) + if not utils.supports_microversion(session, '3.55'): + base_path = '/os-volume-transfer' + # With MV 3.55, we also introduce the ability to transfer + # snapshot along with the volume. If MV < 3.55, we should + # not send 'no_snapshots' parameter in the request. + if 'no_snapshots' in params: + params.pop('no_snapshots') + + return super().create( + session, + prepend_key=prepend_key, + base_path=base_path, + resource_request_key=resource_request_key, + resource_response_key=resource_response_key, + microversion=microversion, + **params, + ) + + def fetch( + self, + session, + requires_id=True, + base_path=None, + error_message=None, + skip_cache=False, + *, + resource_response_key=None, + microversion=None, + **params, + ): + """Get volume transfer. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param boolean requires_id: A boolean indicating whether resource ID + should be part of the requested URI. + :param str base_path: Base part of the URI for fetching resources, if + different from :data:`~openstack.resource.Resource.base_path`. + :param str error_message: An Error message to be returned if + requested object does not exist. + :param bool skip_cache: A boolean indicating whether optional API + cache should be skipped for this invocation. + :param str resource_response_key: Overrides the usage of + self.resource_key when processing the response body. + :param str microversion: API version to override the negotiated one. + :param dict params: Additional parameters that can be consumed. + :return: This :class:`Resource` instance. + :raises: :exc:`~openstack.exceptions.MethodNotSupported` if + :data:`Resource.allow_fetch` is not set to ``True``. + :raises: :exc:`~openstack.exceptions.ResourceNotFound` if + the resource was not found. + """ + + if not utils.supports_microversion(session, '3.55'): + base_path = '/os-volume-transfer' + + return super().fetch( + session, + requires_id=requires_id, + base_path=base_path, + error_message=error_message, + skip_cache=skip_cache, + resource_response_key=resource_response_key, + microversion=microversion, + **params, + ) + + def delete( + self, session, error_message=None, *, microversion=None, **kwargs + ): + """Delete a volume transfer. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param str microversion: API version to override the negotiated one. + :param dict kwargs: Parameters that will be passed to + _prepare_request() + + :return: This :class:`Resource` instance. + :raises: :exc:`~openstack.exceptions.MethodNotSupported` if + :data:`Resource.allow_commit` is not set to ``True``. + :raises: :exc:`~openstack.exceptions.ResourceNotFound` if + the resource was not found. + """ + + if not utils.supports_microversion(session, '3.55'): + kwargs['base_path'] = '/os-volume-transfer' + return super().delete( + session, + error_message=error_message, + microversion=microversion, + **kwargs, + ) diff --git a/openstack/tests/functional/block_storage/v3/test_transfer.py b/openstack/tests/functional/block_storage/v3/test_transfer.py new file mode 100644 index 000000000..18504deec --- /dev/null +++ b/openstack/tests/functional/block_storage/v3/test_transfer.py @@ -0,0 +1,58 @@ +# 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.tests.functional.block_storage.v3 import base +from openstack import utils + + +class TestTransfer(base.BaseBlockStorageTest): + def setUp(self): + super().setUp() + + self.VOLUME_NAME = self.getUniqueString() + + self.volume = self.user_cloud.block_storage.create_volume( + name=self.VOLUME_NAME, + size=1, + ) + self.user_cloud.block_storage.wait_for_status( + self.volume, + status='available', + failures=['error'], + interval=2, + wait=self._wait_for_timeout, + ) + self.VOLUME_ID = self.volume.id + + def tearDown(self): + sot = self.user_cloud.block_storage.delete_volume( + self.VOLUME_ID, ignore_missing=False + ) + self.assertIsNone(sot) + super().tearDown() + + def test_transfer(self): + if not utils.supports_microversion(self.conn.block_storage, "3.55"): + self.skipTest("Cannot test new transfer API if MV < 3.55") + sot = self.conn.block_storage.create_transfer( + volume_id=self.VOLUME_ID, + name=self.VOLUME_NAME, + ) + self.assertIn('auth_key', sot) + self.assertIn('created_at', sot) + self.assertIn('id', sot) + self.assertIn('name', sot) + self.assertIn('volume_id', sot) + + sot = self.user_cloud.block_storage.delete_transfer( + sot.id, ignore_missing=False + ) diff --git a/openstack/tests/unit/block_storage/v3/test_transfer.py b/openstack/tests/unit/block_storage/v3/test_transfer.py new file mode 100644 index 000000000..9a88d6428 --- /dev/null +++ b/openstack/tests/unit/block_storage/v3/test_transfer.py @@ -0,0 +1,99 @@ +# 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 unittest import mock + +from keystoneauth1 import adapter + +from openstack.block_storage.v3 import transfer +from openstack import resource +from openstack.tests.unit import base + + +FAKE_ID = "09d18b36-9e8d-4438-a4da-3f5eff5e1130" +FAKE_VOL_ID = "390de1bc-19d1-41e7-ba67-c492bb36cae5" +FAKE_VOL_NAME = "test-volume" + +TRANSFER = { + "auth_key": "95bc670c0068821d", + "created_at": "2023-06-27T08:47:23.035010", + "id": FAKE_ID, + "name": FAKE_VOL_NAME, + "volume_id": FAKE_VOL_ID, +} + + +class TestTransfer(base.TestCase): + def setUp(self): + super().setUp() + self.resp = mock.Mock() + self.resp.body = None + self.resp.json = mock.Mock(return_value=self.resp.body) + self.resp.headers = {} + self.resp.status_code = 202 + + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.post = mock.Mock(return_value=self.resp) + self.sess.default_microversion = "3.55" + + def test_basic(self): + tr = transfer.Transfer(TRANSFER) + self.assertEqual("transfer", tr.resource_key) + self.assertEqual("transfers", tr.resources_key) + self.assertEqual("/volume-transfers", tr.base_path) + self.assertTrue(tr.allow_create) + self.assertIsNotNone(tr._max_microversion) + + self.assertDictEqual( + { + "limit": "limit", + "marker": "marker", + }, + tr._query_mapping._mapping, + ) + + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=True, + ) + @mock.patch.object(resource.Resource, '_translate_response') + def test_create(self, mock_mv, mock_translate): + sot = transfer.Transfer() + + sot.create(self.sess, volume_id=FAKE_VOL_ID, name=FAKE_VOL_NAME) + self.sess.post.assert_called_with( + '/volume-transfers', + json={'transfer': {}}, + microversion="3.55", + headers={}, + params={'volume_id': FAKE_VOL_ID, 'name': FAKE_VOL_NAME}, + ) + + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=False, + ) + @mock.patch.object(resource.Resource, '_translate_response') + def test_create_pre_v355(self, mock_mv, mock_translate): + self.sess.default_microversion = "3.0" + sot = transfer.Transfer() + + sot.create(self.sess, volume_id=FAKE_VOL_ID, name=FAKE_VOL_NAME) + self.sess.post.assert_called_with( + '/os-volume-transfer', + json={'transfer': {}}, + microversion="3.0", + headers={}, + params={'volume_id': FAKE_VOL_ID, 'name': FAKE_VOL_NAME}, + ) diff --git a/releasenotes/notes/add-volume-transfer-support-28bf34a243d96e1b.yaml b/releasenotes/notes/add-volume-transfer-support-28bf34a243d96e1b.yaml new file mode 100644 index 000000000..ff0ed5db5 --- /dev/null +++ b/releasenotes/notes/add-volume-transfer-support-28bf34a243d96e1b.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added support for volume transfer create, find + and delete. From 7df00eef612bacd4e0db448029616595bea0d74c Mon Sep 17 00:00:00 2001 From: gtema Date: Tue, 31 Oct 2023 11:29:42 +0100 Subject: [PATCH 3414/3836] Switch back to LaunchPad for issue tracking We move away from StoryBoard back to LaunchPad. Change-Id: Ic18c514f2fb9e96e68d09b973448541a4ce65432 --- CONTRIBUTING.rst | 4 ++-- README.rst | 4 ++-- doc/source/conf.py | 2 +- releasenotes/source/conf.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index be71810e2..790776c3e 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -33,10 +33,10 @@ Project Documentation https://docs.openstack.org/openstacksdk/latest/ Bug tracker - https://storyboard.openstack.org/#!/project/openstack/openstacksdk + https://bugs.launchpad.net/openstacksdk Mailing list (prefix subjects with ``[sdk]`` for faster responses) - http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-discuss + https://lists.openstack.org/mailman3/lists/openstack-discuss.lists.openstack.org/ Code Hosting https://opendev.org/openstack/openstacksdk diff --git a/README.rst b/README.rst index c3d8a3a67..a3a062569 100644 --- a/README.rst +++ b/README.rst @@ -305,9 +305,9 @@ OpenStack service can be found in the `Project Navigator`__. Links ----- -* `Issue Tracker `_ +* `Issue Tracker `_ * `Code Review `_ * `Documentation `_ * `PyPI `_ -* `Mailing list `_ +* `Mailing list `_ * `Release Notes `_ diff --git a/doc/source/conf.py b/doc/source/conf.py index ec4dab832..b41ba20d9 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -31,7 +31,7 @@ # openstackdocstheme options openstackdocs_repo_name = 'openstack/openstacksdk' openstackdocs_pdf_link = True -openstackdocs_use_storyboard = True +openstackdocs_use_storyboard = False html_theme = 'openstackdocs' # autodoc generation is a bit aggressive and a nuisance when doing heavy diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py index c434441fd..ac4be8e19 100644 --- a/releasenotes/source/conf.py +++ b/releasenotes/source/conf.py @@ -44,7 +44,7 @@ # openstackdocstheme options openstackdocs_repo_name = 'openstack/openstacksdk' -openstackdocs_use_storyboard = True +openstackdocs_use_storyboard = False # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] From 47f5005464d0b5323878c1d7f19fc02951f31707 Mon Sep 17 00:00:00 2001 From: whoami-rajat Date: Tue, 4 Jul 2023 21:38:45 +0530 Subject: [PATCH 3415/3836] Add volume transfer support [2/2] This patch adds support for volume transfer get, list and accept. Change-Id: I09cd55bf0ce30a7a1d445c09cd8d2a3c852296ee --- doc/source/user/proxies/block_storage_v3.rst | 3 +- openstack/block_storage/v3/_proxy.py | 50 +++++++++++++++++++ openstack/block_storage/v3/transfer.py | 30 +++++++++++ .../unit/block_storage/v3/test_transfer.py | 47 ++++++++++++++++- ...ume-transfer-support-28bf34a243d96e1b.yaml | 4 +- 5 files changed, 130 insertions(+), 4 deletions(-) diff --git a/doc/source/user/proxies/block_storage_v3.rst b/doc/source/user/proxies/block_storage_v3.rst index 2c830ce90..5b6eaae17 100644 --- a/doc/source/user/proxies/block_storage_v3.rst +++ b/doc/source/user/proxies/block_storage_v3.rst @@ -162,4 +162,5 @@ Transfer Operations .. autoclass:: openstack.block_storage.v3._proxy.Proxy :noindex: - :members: create_transfer, delete_transfer, find_transfer + :members: create_transfer, delete_transfer, find_transfer, + get_transfer, transfers, accept_transfer diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 9524cc8cc..f076b7b8c 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -34,6 +34,7 @@ from openstack import exceptions from openstack.identity.v3 import project as _project from openstack import resource +from openstack import utils class Proxy(_base_proxy.BaseBlockStorageProxy): @@ -1917,6 +1918,55 @@ def find_transfer(self, name_or_id, ignore_missing=True): ignore_missing=ignore_missing, ) + def get_transfer(self, transfer): + """Get a single transfer + + :param transfer: The value can be the ID of a transfer or a + :class:`~openstack.block_storage.v3.transfer.Transfer` + instance. + + :returns: One :class:`~openstack.block_storage.v3.transfer.Transfer` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_transfer.Transfer, transfer) + + def transfers(self, *, details=True, all_projects=False, **query): + """Retrieve a generator of transfers + + :param bool details: When set to ``False`` no extended attributes + will be returned. The default, ``True``, will cause objects with + additional attributes to be returned. + :param bool all_projects: When set to ``True``, list transfers from + all projects. Admin-only by default. + :param kwargs query: Optional query parameters to be sent to limit + the transfers being returned. + + :returns: A generator of transfer objects. + """ + if all_projects: + query['all_projects'] = True + base_path = '/volume-transfers' + if not utils.supports_microversion(self, '3.55'): + base_path = '/os-volume-transfer' + if details: + base_path = utils.urljoin(base_path, 'detail') + return self._list(_transfer.Transfer, base_path=base_path, **query) + + def accept_transfer(self, transfer_id, auth_key): + """Accept a Transfer + + :param transfer_id: The value can be the ID of a transfer or a + :class:`~openstack.block_storage.v3.transfer.Transfer` + instance. + :param auth_key: The key to authenticate volume transfer. + + :returns: The results of Transfer creation + :rtype: :class:`~openstack.block_storage.v3.transfer.Transfer` + """ + transfer = self._get_resource(_transfer.Transfer, transfer_id) + return transfer.accept(self, auth_key=auth_key) + # ====== UTILS ====== def wait_for_status( self, diff --git a/openstack/block_storage/v3/transfer.py b/openstack/block_storage/v3/transfer.py index 27d9fd9fc..e05cb3f98 100644 --- a/openstack/block_storage/v3/transfer.py +++ b/openstack/block_storage/v3/transfer.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import exceptions from openstack import resource from openstack import utils @@ -23,6 +24,8 @@ class Transfer(resource.Resource): allow_create = True allow_delete = True allow_fetch = True + allow_list = True + allow_get = True # Properties #: UUID of the transfer. @@ -171,3 +174,30 @@ def delete( microversion=microversion, **kwargs, ) + + def accept(self, session, *, auth_key=None): + """Accept a volume transfer. + + :param session: The session to use for making this request. + :param auth_key: The authentication key for the volume transfer. + + :return: This :class:`Transfer` instance. + """ + body = {'accept': {'auth_key': auth_key}} + + path = self.base_path + if not utils.supports_microversion(session, '3.55'): + path = '/os-volume-transfer' + + url = utils.urljoin(path, self.id, 'accept') + microversion = self._get_microversion(session, action='commit') + resp = session.post( + url, + json=body, + microversion=microversion, + ) + exceptions.raise_from_response(resp) + + transfer = Transfer() + transfer._translate_response(response=resp) + return transfer diff --git a/openstack/tests/unit/block_storage/v3/test_transfer.py b/openstack/tests/unit/block_storage/v3/test_transfer.py index 9a88d6428..4114ab30b 100644 --- a/openstack/tests/unit/block_storage/v3/test_transfer.py +++ b/openstack/tests/unit/block_storage/v3/test_transfer.py @@ -22,9 +22,11 @@ FAKE_ID = "09d18b36-9e8d-4438-a4da-3f5eff5e1130" FAKE_VOL_ID = "390de1bc-19d1-41e7-ba67-c492bb36cae5" FAKE_VOL_NAME = "test-volume" +FAKE_TRANSFER = "7d048960-7c3f-4bf0-952f-4312fdea1dec" +FAKE_AUTH_KEY = "95bc670c0068821d" TRANSFER = { - "auth_key": "95bc670c0068821d", + "auth_key": FAKE_AUTH_KEY, "created_at": "2023-06-27T08:47:23.035010", "id": FAKE_ID, "name": FAKE_VOL_NAME, @@ -97,3 +99,46 @@ def test_create_pre_v355(self, mock_mv, mock_translate): headers={}, params={'volume_id': FAKE_VOL_ID, 'name': FAKE_VOL_NAME}, ) + + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=True, + ) + @mock.patch.object(resource.Resource, '_translate_response') + def test_accept(self, mock_mv, mock_translate): + sot = transfer.Transfer() + sot.id = FAKE_TRANSFER + + sot.accept(self.sess, auth_key=FAKE_AUTH_KEY) + self.sess.post.assert_called_with( + 'volume-transfers/%s/accept' % FAKE_TRANSFER, + json={ + 'accept': { + 'auth_key': FAKE_AUTH_KEY, + } + }, + microversion="3.55", + ) + + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=False, + ) + @mock.patch.object(resource.Resource, '_translate_response') + def test_accept_pre_v355(self, mock_mv, mock_translate): + self.sess.default_microversion = "3.0" + sot = transfer.Transfer() + sot.id = FAKE_TRANSFER + + sot.accept(self.sess, auth_key=FAKE_AUTH_KEY) + self.sess.post.assert_called_with( + 'os-volume-transfer/%s/accept' % FAKE_TRANSFER, + json={ + 'accept': { + 'auth_key': FAKE_AUTH_KEY, + } + }, + microversion="3.0", + ) diff --git a/releasenotes/notes/add-volume-transfer-support-28bf34a243d96e1b.yaml b/releasenotes/notes/add-volume-transfer-support-28bf34a243d96e1b.yaml index ff0ed5db5..40976dcab 100644 --- a/releasenotes/notes/add-volume-transfer-support-28bf34a243d96e1b.yaml +++ b/releasenotes/notes/add-volume-transfer-support-28bf34a243d96e1b.yaml @@ -1,5 +1,5 @@ --- features: - | - Added support for volume transfer create, find - and delete. + Added support for volume transfer create, find, + delete, get, list and accept. From 5dd7d64769c5b91282ffa8222a2e85b9c0ad0432 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 31 Oct 2023 16:46:22 +0000 Subject: [PATCH 3416/3836] volume: Add Limit to volume v2 API Another case of adding a resource from v3 to the legacy v2 API. We tackled Extension in change I1e4528f5a5d8e2caaaf204792ddcee7267e4c2e9 and Capability in change I98252ddd0dadba2bfa1aaf97b59932a19c396cd4. Now it's time for Limit. Note that the API is unchanged between v2 and v3 so this can be a straightforward copy-paste job. This was determined by inspecting the cinder code before the v2 API was removed (e.g. commit 'e05b261af~', file 'cinder/api/views/limits.py'). Change-Id: I5a375d8dee7e68368f2e454851812a3638acf9ee Signed-off-by: Stephen Finucane --- doc/source/user/proxies/block_storage_v2.rst | 7 + .../resources/block_storage/v2/limits.rst | 37 ++++ openstack/block_storage/v2/_proxy.py | 18 ++ openstack/block_storage/v2/limits.py | 82 +++++++ .../unit/block_storage/v2/test_limits.py | 206 ++++++++++++++++++ .../tests/unit/block_storage/v2/test_proxy.py | 11 + 6 files changed, 361 insertions(+) create mode 100644 doc/source/user/resources/block_storage/v2/limits.rst create mode 100644 openstack/block_storage/v2/limits.py create mode 100644 openstack/tests/unit/block_storage/v2/test_limits.py diff --git a/doc/source/user/proxies/block_storage_v2.rst b/doc/source/user/proxies/block_storage_v2.rst index b6e0faa31..c5bca1866 100644 --- a/doc/source/user/proxies/block_storage_v2.rst +++ b/doc/source/user/proxies/block_storage_v2.rst @@ -33,6 +33,13 @@ Capabilities Operations :noindex: :members: get_capabilities +Limits Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v2._proxy.Proxy + :noindex: + :members: get_limits + Type Operations ^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/block_storage/v2/limits.rst b/doc/source/user/resources/block_storage/v2/limits.rst new file mode 100644 index 000000000..ec6e8fd0a --- /dev/null +++ b/doc/source/user/resources/block_storage/v2/limits.rst @@ -0,0 +1,37 @@ +openstack.block_storage.v2.limits +================================= + +.. automodule:: openstack.block_storage.v2.limits + +The AbsoluteLimit Class +----------------------- + +The ``AbsoluteLimit`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v2.limits.AbsoluteLimit + :members: + +The Limit Class +--------------- + +The ``Limit`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v2.limits.Limit + :members: + +The RateLimit Class +------------------- + +The ``RateLimit`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v2.limits.RateLimit + :members: + +The RateLimits Class +-------------------- + +The ``RateLimits`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v2.limits.RateLimits + :members: diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 0312fbdfb..fa38a0638 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -14,6 +14,7 @@ from openstack.block_storage.v2 import backup as _backup from openstack.block_storage.v2 import capabilities as _capabilities from openstack.block_storage.v2 import extension as _extension +from openstack.block_storage.v2 import limits as _limits from openstack.block_storage.v2 import quota_set as _quota_set from openstack.block_storage.v2 import snapshot as _snapshot from openstack.block_storage.v2 import stats as _stats @@ -610,6 +611,23 @@ def reset_backup(self, backup, status): backup = self._get_resource(_backup.Backup, backup) backup.reset(self, status) + # ====== LIMITS ====== + def get_limits(self, project=None): + """Retrieves limits + + :param project: A project to get limits for. The value can be either + the ID of a project or an + :class:`~openstack.identity.v2.project.Project` instance. + :returns: A Limit object, including both + :class:`~openstack.block_storage.v2.limits.AbsoluteLimit` and + :class:`~openstack.block_storage.v2.limits.RateLimit` + :rtype: :class:`~openstack.block_storage.v2.limits.Limit` + """ + params = {} + if project: + params['project_id'] = resource.Resource._get_id(project) + return self._get(_limits.Limit, requires_id=False, **params) + # ====== CAPABILITIES ====== def get_capabilities(self, host): """Get a backend's capabilites diff --git a/openstack/block_storage/v2/limits.py b/openstack/block_storage/v2/limits.py new file mode 100644 index 000000000..490e4e4b8 --- /dev/null +++ b/openstack/block_storage/v2/limits.py @@ -0,0 +1,82 @@ +# 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 resource + + +class AbsoluteLimit(resource.Resource): + #: Properties + #: The maximum total amount of backups, in gibibytes (GiB). + max_total_backup_gigabytes = resource.Body( + "maxTotalBackupGigabytes", type=int + ) + #: The maximum number of backups. + max_total_backups = resource.Body("maxTotalBackups", type=int) + #: The maximum number of snapshots. + max_total_snapshots = resource.Body("maxTotalSnapshots", type=int) + #: The maximum total amount of volumes, in gibibytes (GiB). + max_total_volume_gigabytes = resource.Body( + "maxTotalVolumeGigabytes", type=int + ) + #: The maximum number of volumes. + max_total_volumes = resource.Body("maxTotalVolumes", type=int) + #: The total number of backups gibibytes (GiB) used. + total_backup_gigabytes_used = resource.Body( + "totalBackupGigabytesUsed", type=int + ) + #: The total number of backups used. + total_backups_used = resource.Body("totalBackupsUsed", type=int) + #: The total number of gibibytes (GiB) used. + total_gigabytes_used = resource.Body("totalGigabytesUsed", type=int) + #: The total number of snapshots used. + total_snapshots_used = resource.Body("totalSnapshotsUsed", type=int) + #: The total number of volumes used. + total_volumes_used = resource.Body("totalVolumesUsed", type=int) + + +class RateLimit(resource.Resource): + #: Properties + #: Rate limits next availabe time. + next_available = resource.Body("next-available") + #: Integer for rate limits remaining. + remaining = resource.Body("remaining", type=int) + #: Unit of measurement for the value parameter. + unit = resource.Body("unit") + #: Integer number of requests which can be made. + value = resource.Body("value", type=int) + #: An HTTP verb (POST, PUT, etc.). + verb = resource.Body("verb") + + +class RateLimits(resource.Resource): + #: Properties + #: A list of the specific limits that apply to the ``regex`` and ``uri``. + limits = resource.Body("limit", type=list, list_type=RateLimit) + #: A regex representing which routes this rate limit applies to. + regex = resource.Body("regex") + #: A URI representing which routes this rate limit applies to. + uri = resource.Body("uri") + + +class Limit(resource.Resource): + resource_key = "limits" + base_path = "/limits" + + # capabilities + allow_fetch = True + + #: Properties + #: An absolute limits object. + absolute = resource.Body("absolute", type=AbsoluteLimit) + #: Rate-limit volume copy bandwidth, used to mitigate + #: slow down of data access from the instances. + rate = resource.Body("rate", type=list, list_type=RateLimits) diff --git a/openstack/tests/unit/block_storage/v2/test_limits.py b/openstack/tests/unit/block_storage/v2/test_limits.py new file mode 100644 index 000000000..ec74c3fc9 --- /dev/null +++ b/openstack/tests/unit/block_storage/v2/test_limits.py @@ -0,0 +1,206 @@ +# 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.block_storage.v2 import limits +from openstack.tests.unit import base + +ABSOLUTE_LIMIT = { + "totalSnapshotsUsed": 1, + "maxTotalBackups": 10, + "maxTotalVolumeGigabytes": 1000, + "maxTotalSnapshots": 10, + "maxTotalBackupGigabytes": 1000, + "totalBackupGigabytesUsed": 1, + "maxTotalVolumes": 10, + "totalVolumesUsed": 2, + "totalBackupsUsed": 3, + "totalGigabytesUsed": 2, +} + +RATE_LIMIT = { + "verb": "POST", + "value": 80, + "remaining": 80, + "unit": "MINUTE", + "next-available": "2021-02-23T22:08:00Z", +} + +RATE_LIMITS = {"regex": ".*", "uri": "*", "limit": [RATE_LIMIT]} + +LIMIT = {"rate": [RATE_LIMITS], "absolute": ABSOLUTE_LIMIT} + + +class TestAbsoluteLimit(base.TestCase): + def test_basic(self): + limit_resource = limits.AbsoluteLimit() + self.assertIsNone(limit_resource.resource_key) + self.assertIsNone(limit_resource.resources_key) + self.assertEqual('', limit_resource.base_path) + self.assertFalse(limit_resource.allow_create) + self.assertFalse(limit_resource.allow_fetch) + self.assertFalse(limit_resource.allow_delete) + self.assertFalse(limit_resource.allow_commit) + self.assertFalse(limit_resource.allow_list) + + def test_make_absolute_limit(self): + limit_resource = limits.AbsoluteLimit(**ABSOLUTE_LIMIT) + self.assertEqual( + ABSOLUTE_LIMIT['totalSnapshotsUsed'], + limit_resource.total_snapshots_used, + ) + self.assertEqual( + ABSOLUTE_LIMIT['maxTotalBackups'], limit_resource.max_total_backups + ) + self.assertEqual( + ABSOLUTE_LIMIT['maxTotalVolumeGigabytes'], + limit_resource.max_total_volume_gigabytes, + ) + self.assertEqual( + ABSOLUTE_LIMIT['maxTotalSnapshots'], + limit_resource.max_total_snapshots, + ) + self.assertEqual( + ABSOLUTE_LIMIT['maxTotalBackupGigabytes'], + limit_resource.max_total_backup_gigabytes, + ) + self.assertEqual( + ABSOLUTE_LIMIT['totalBackupGigabytesUsed'], + limit_resource.total_backup_gigabytes_used, + ) + self.assertEqual( + ABSOLUTE_LIMIT['maxTotalVolumes'], limit_resource.max_total_volumes + ) + self.assertEqual( + ABSOLUTE_LIMIT['totalVolumesUsed'], + limit_resource.total_volumes_used, + ) + self.assertEqual( + ABSOLUTE_LIMIT['totalBackupsUsed'], + limit_resource.total_backups_used, + ) + self.assertEqual( + ABSOLUTE_LIMIT['totalGigabytesUsed'], + limit_resource.total_gigabytes_used, + ) + + +class TestRateLimit(base.TestCase): + def test_basic(self): + limit_resource = limits.RateLimit() + self.assertIsNone(limit_resource.resource_key) + self.assertIsNone(limit_resource.resources_key) + self.assertEqual('', limit_resource.base_path) + self.assertFalse(limit_resource.allow_create) + self.assertFalse(limit_resource.allow_fetch) + self.assertFalse(limit_resource.allow_delete) + self.assertFalse(limit_resource.allow_commit) + self.assertFalse(limit_resource.allow_list) + + def test_make_rate_limit(self): + limit_resource = limits.RateLimit(**RATE_LIMIT) + self.assertEqual(RATE_LIMIT['verb'], limit_resource.verb) + self.assertEqual(RATE_LIMIT['value'], limit_resource.value) + self.assertEqual(RATE_LIMIT['remaining'], limit_resource.remaining) + self.assertEqual(RATE_LIMIT['unit'], limit_resource.unit) + self.assertEqual( + RATE_LIMIT['next-available'], limit_resource.next_available + ) + + +class TestRateLimits(base.TestCase): + def test_basic(self): + limit_resource = limits.RateLimits() + self.assertIsNone(limit_resource.resource_key) + self.assertIsNone(limit_resource.resources_key) + self.assertEqual('', limit_resource.base_path) + self.assertFalse(limit_resource.allow_create) + self.assertFalse(limit_resource.allow_fetch) + self.assertFalse(limit_resource.allow_delete) + self.assertFalse(limit_resource.allow_commit) + self.assertFalse(limit_resource.allow_list) + + def _test_rate_limit(self, expected, actual): + self.assertEqual(expected[0]['verb'], actual[0].verb) + self.assertEqual(expected[0]['value'], actual[0].value) + self.assertEqual(expected[0]['remaining'], actual[0].remaining) + self.assertEqual(expected[0]['unit'], actual[0].unit) + self.assertEqual( + expected[0]['next-available'], actual[0].next_available + ) + + def test_make_rate_limits(self): + limit_resource = limits.RateLimits(**RATE_LIMITS) + self.assertEqual(RATE_LIMITS['regex'], limit_resource.regex) + self.assertEqual(RATE_LIMITS['uri'], limit_resource.uri) + self._test_rate_limit(RATE_LIMITS['limit'], limit_resource.limits) + + +class TestLimit(base.TestCase): + def test_basic(self): + limit_resource = limits.Limit() + self.assertEqual('limits', limit_resource.resource_key) + self.assertEqual('/limits', limit_resource.base_path) + self.assertTrue(limit_resource.allow_fetch) + self.assertFalse(limit_resource.allow_create) + self.assertFalse(limit_resource.allow_commit) + self.assertFalse(limit_resource.allow_delete) + self.assertFalse(limit_resource.allow_list) + + def _test_absolute_limit(self, expected, actual): + self.assertEqual( + expected['totalSnapshotsUsed'], actual.total_snapshots_used + ) + self.assertEqual(expected['maxTotalBackups'], actual.max_total_backups) + self.assertEqual( + expected['maxTotalVolumeGigabytes'], + actual.max_total_volume_gigabytes, + ) + self.assertEqual( + expected['maxTotalSnapshots'], actual.max_total_snapshots + ) + self.assertEqual( + expected['maxTotalBackupGigabytes'], + actual.max_total_backup_gigabytes, + ) + self.assertEqual( + expected['totalBackupGigabytesUsed'], + actual.total_backup_gigabytes_used, + ) + self.assertEqual(expected['maxTotalVolumes'], actual.max_total_volumes) + self.assertEqual( + expected['totalVolumesUsed'], actual.total_volumes_used + ) + self.assertEqual( + expected['totalBackupsUsed'], actual.total_backups_used + ) + self.assertEqual( + expected['totalGigabytesUsed'], actual.total_gigabytes_used + ) + + def _test_rate_limit(self, expected, actual): + self.assertEqual(expected[0]['verb'], actual[0].verb) + self.assertEqual(expected[0]['value'], actual[0].value) + self.assertEqual(expected[0]['remaining'], actual[0].remaining) + self.assertEqual(expected[0]['unit'], actual[0].unit) + self.assertEqual( + expected[0]['next-available'], actual[0].next_available + ) + + def _test_rate_limits(self, expected, actual): + self.assertEqual(expected[0]['regex'], actual[0].regex) + self.assertEqual(expected[0]['uri'], actual[0].uri) + self._test_rate_limit(expected[0]['limit'], actual[0].limits) + + def test_make_limit(self): + limit_resource = limits.Limit(**LIMIT) + self._test_rate_limits(LIMIT['rate'], limit_resource.rate) + self._test_absolute_limit(LIMIT['absolute'], limit_resource.absolute) diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index bf3a3b66f..3455f6abf 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -14,6 +14,7 @@ from openstack.block_storage.v2 import _proxy from openstack.block_storage.v2 import backup from openstack.block_storage.v2 import capabilities +from openstack.block_storage.v2 import limits from openstack.block_storage.v2 import quota_set from openstack.block_storage.v2 import snapshot from openstack.block_storage.v2 import stats @@ -290,6 +291,16 @@ def test_backup_reset(self): ) +class TestLimit(TestVolumeProxy): + def test_limits_get(self): + self.verify_get( + self.proxy.get_limits, + limits.Limit, + method_args=[], + expected_kwargs={'requires_id': False}, + ) + + class TestCapabilities(TestVolumeProxy): def test_capabilites_get(self): self.verify_get(self.proxy.get_capabilities, capabilities.Capabilities) From 01b9a49c95021988a08c4b48b72d1a9d0486d907 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 26 Oct 2023 17:02:47 +0100 Subject: [PATCH 3417/3836] requirements: Sort alphabetically Order no longer matters starting with pip 20.3. Change-Id: I743674eaf684be9d8b6255e90edc82de0cd80734 Signed-off-by: Stephen Finucane --- doc/requirements.txt | 5 +---- requirements.txt | 28 +++++++++++----------------- test-requirements.txt | 8 ++------ 3 files changed, 14 insertions(+), 27 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 070806f54..925ca4dc8 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,8 +1,5 @@ -# 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. -sphinx>=2.0.0,!=2.1.0 # BSD docutils>=0.11 # OSI-Approved Open Source, Public Domain openstackdocstheme>=2.2.1 # Apache-2.0 reno>=3.1.0 # Apache-2.0 +sphinx>=2.0.0,!=2.1.0 # BSD sphinxcontrib-svg2pdfconverter>=0.1.0 # BSD diff --git a/requirements.txt b/requirements.txt index 48188d4e2..25fb9a198 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,20 +1,14 @@ -# 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 -PyYAML>=3.13 # MIT -platformdirs>=3 # MIT License -requestsexceptions>=1.2.0 # Apache-2.0 -jsonpatch!=1.20,>=1.16 # BSD -os-service-types>=1.7.0 # Apache-2.0 -keystoneauth1>=3.18.0 # Apache-2.0 - +cryptography>=2.7 # BSD/Apache-2.0 decorator>=4.4.1 # BSD -jmespath>=0.9.0 # MIT -iso8601>=0.1.11 # MIT -netifaces>=0.10.4 # MIT - dogpile.cache>=0.6.5 # BSD -cryptography>=2.7 # BSD/Apache-2.0 - importlib_metadata>=1.7.0;python_version<'3.8' # Apache-2.0 +iso8601>=0.1.11 # MIT +jmespath>=0.9.0 # MIT +jsonpatch!=1.20,>=1.16 # BSD +keystoneauth1>=3.18.0 # Apache-2.0 +netifaces>=0.10.4 # MIT +os-service-types>=1.7.0 # Apache-2.0 +pbr!=2.1.0,>=2.0.0 # Apache-2.0 +platformdirs>=3 # MIT License +PyYAML>=3.13 # MIT +requestsexceptions>=1.2.0 # Apache-2.0 diff --git a/test-requirements.txt b/test-requirements.txt index 3f22c9d11..b771e0ca3 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,15 +1,11 @@ -# 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>=3.1.0,<4.0.0 # Apache-2.0 - coverage!=4.4,>=4.0 # Apache-2.0 ddt>=1.0.1 # MIT fixtures>=3.0.0 # Apache-2.0/BSD +hacking>=3.1.0,<4.0.0 # Apache-2.0 jsonschema>=3.2.0 # MIT -prometheus-client>=0.4.2 # Apache-2.0 oslo.config>=6.1.0 # Apache-2.0 oslotest>=3.2.0 # Apache-2.0 +prometheus-client>=0.4.2 # Apache-2.0 requests-mock>=1.2.0 # Apache-2.0 statsd>=3.3.0 stestr>=1.0.0 # Apache-2.0 From f44004747562fdb5365fcb1ea96314b77f797a8d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 6 Nov 2023 10:46:58 +0000 Subject: [PATCH 3418/3836] Drop support for Python 3.6 This was in-effect already dropped with the introduction of the dependency on platformdirs > 3.0. This just makes it official. Change-Id: I18806e78fe1491948948d224edb27d6ac4a8f45f Signed-off-by: Stephen Finucane --- setup.cfg | 5 +++-- tox.ini | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 50074a951..ad5039547 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,11 +14,12 @@ classifier = Operating System :: POSIX :: Linux Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 -python_requires = >=3.6 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 +python_requires = >=3.7 [files] packages = diff --git a/tox.ini b/tox.ini index 36fd20eb3..bdc0a3737 100644 --- a/tox.ini +++ b/tox.ini @@ -23,7 +23,7 @@ commands = stestr run {posargs} stestr slowest -[testenv:functional{,-py36,-py37,-py38,-py39}] +[testenv:functional{-py37,-py38,-py39}] description = Run functional tests. # Some jobs (especially heat) takes longer, therefore increase default timeout From 621b561c6c73409913095e98969057f62a4cccef Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 31 Aug 2023 14:27:00 +0100 Subject: [PATCH 3419/3836] cloud: Replace use of aliased exceptions Change-Id: I273e7554af766b15deb5ac8f38a6793b119a3bb9 Signed-off-by: Stephen Finucane --- openstack/cloud/_baremetal.py | 71 +++-- openstack/cloud/_block_storage.py | 113 +++++--- openstack/cloud/_coe.py | 60 ++-- openstack/cloud/_compute.py | 194 ++++++++----- openstack/cloud/_dns.py | 42 ++- openstack/cloud/_floating_ip.py | 91 +++--- openstack/cloud/_identity.py | 212 ++++++++------ openstack/cloud/_image.py | 29 +- openstack/cloud/_network.py | 260 ++++++++++-------- openstack/cloud/_network_common.py | 26 +- openstack/cloud/_object_store.py | 53 ++-- openstack/cloud/_orchestration.py | 40 ++- openstack/cloud/_security_group.py | 30 +- openstack/cloud/_utils.py | 23 +- openstack/cloud/cmd/inventory.py | 3 +- openstack/cloud/exc.py | 2 +- openstack/cloud/inventory.py | 2 +- openstack/cloud/meta.py | 8 +- openstack/cloud/openstackcloud.py | 10 +- openstack/exceptions.py | 7 +- openstack/image/v1/_proxy.py | 13 +- openstack/object_store/v1/_proxy.py | 3 +- .../tests/functional/cloud/test_compute.py | 10 +- .../tests/functional/cloud/test_domain.py | 6 +- .../tests/functional/cloud/test_endpoints.py | 6 +- .../tests/functional/cloud/test_flavor.py | 4 +- .../functional/cloud/test_floating_ip.py | 6 +- .../tests/functional/cloud/test_groups.py | 6 +- .../tests/functional/cloud/test_identity.py | 8 +- .../tests/functional/cloud/test_network.py | 4 +- .../tests/functional/cloud/test_object.py | 6 +- openstack/tests/functional/cloud/test_port.py | 4 +- .../tests/functional/cloud/test_project.py | 4 +- .../cloud/test_qos_bandwidth_limit_rule.py | 4 +- .../cloud/test_qos_dscp_marking_rule.py | 4 +- .../cloud/test_qos_minimum_bandwidth_rule.py | 4 +- .../tests/functional/cloud/test_qos_policy.py | 4 +- .../functional/cloud/test_range_search.py | 4 +- .../tests/functional/cloud/test_router.py | 8 +- .../tests/functional/cloud/test_services.py | 8 +- .../tests/functional/cloud/test_stack.py | 4 +- .../tests/functional/cloud/test_users.py | 4 +- .../tests/functional/cloud/test_volume.py | 4 +- .../functional/cloud/test_volume_type.py | 10 +- openstack/tests/unit/cloud/test__utils.py | 10 +- .../tests/unit/cloud/test_baremetal_node.py | 29 +- .../tests/unit/cloud/test_baremetal_ports.py | 6 +- openstack/tests/unit/cloud/test_cloud.py | 6 +- .../unit/cloud/test_cluster_templates.py | 4 +- openstack/tests/unit/cloud/test_compute.py | 3 +- .../tests/unit/cloud/test_create_server.py | 14 +- .../unit/cloud/test_create_volume_snapshot.py | 6 +- .../tests/unit/cloud/test_delete_server.py | 4 +- .../unit/cloud/test_delete_volume_snapshot.py | 8 +- .../tests/unit/cloud/test_domain_params.py | 4 +- openstack/tests/unit/cloud/test_domains.py | 14 +- openstack/tests/unit/cloud/test_flavors.py | 7 +- .../unit/cloud/test_floating_ip_neutron.py | 10 +- .../tests/unit/cloud/test_floating_ip_pool.py | 5 +- openstack/tests/unit/cloud/test_image.py | 13 +- .../tests/unit/cloud/test_image_snapshot.py | 4 +- openstack/tests/unit/cloud/test_keypair.py | 8 +- openstack/tests/unit/cloud/test_network.py | 12 +- openstack/tests/unit/cloud/test_object.py | 24 +- openstack/tests/unit/cloud/test_operator.py | 4 +- openstack/tests/unit/cloud/test_port.py | 10 +- openstack/tests/unit/cloud/test_project.py | 5 +- .../cloud/test_qos_bandwidth_limit_rule.py | 12 +- .../unit/cloud/test_qos_dscp_marking_rule.py | 12 +- .../cloud/test_qos_minimum_bandwidth_rule.py | 12 +- openstack/tests/unit/cloud/test_qos_policy.py | 12 +- .../tests/unit/cloud/test_qos_rule_type.py | 8 +- openstack/tests/unit/cloud/test_quotas.py | 4 +- .../tests/unit/cloud/test_rebuild_server.py | 8 +- .../tests/unit/cloud/test_role_assignment.py | 26 +- openstack/tests/unit/cloud/test_router.py | 6 +- .../tests/unit/cloud/test_security_groups.py | 3 +- .../unit/cloud/test_server_delete_metadata.py | 4 +- .../unit/cloud/test_server_set_metadata.py | 4 +- openstack/tests/unit/cloud/test_services.py | 4 +- openstack/tests/unit/cloud/test_stack.py | 18 +- openstack/tests/unit/cloud/test_subnet.py | 14 +- .../tests/unit/cloud/test_update_server.py | 4 +- openstack/tests/unit/cloud/test_users.py | 4 +- openstack/tests/unit/cloud/test_volume.py | 14 +- .../tests/unit/cloud/test_volume_access.py | 5 +- 86 files changed, 960 insertions(+), 838 deletions(-) diff --git a/openstack/cloud/_baremetal.py b/openstack/cloud/_baremetal.py index aae84eb12..81cde7561 100644 --- a/openstack/cloud/_baremetal.py +++ b/openstack/cloud/_baremetal.py @@ -17,7 +17,7 @@ import jsonpatch from openstack.baremetal.v1._proxy import Proxy -from openstack.cloud import exc +from openstack import exceptions from openstack import warnings as os_warnings @@ -86,7 +86,7 @@ def get_machine(self, name_or_id): """ try: return self.baremetal.find_node(name_or_id, ignore_missing=False) - except exc.OpenStackCloudResourceNotFound: + except exceptions.NotFoundException: return None def get_machine_by_mac(self, mac): @@ -130,7 +130,7 @@ def inspect_machine(self, name_or_id, wait=False, timeout=3600): # we need to move the machine back to manageable first. if node.provision_state == 'available': if node.instance_id: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Refusing to inspect available machine %(node)s " "which is associated with an instance " "(instance_uuid %(inst)s)" @@ -146,7 +146,7 @@ def inspect_machine(self, name_or_id, wait=False, timeout=3600): ) if node.provision_state not in ('manageable', 'inspect failed'): - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Machine %(node)s must be in 'manageable', 'inspect failed' " "or 'available' provision state to start inspection, the " "current state is %(state)s" @@ -215,29 +215,24 @@ def register_machine( ] Alternatively, you can provide an array of MAC addresses. - :param wait: Boolean value, defaulting to false, to wait for the node to reach the available state where the node can be provisioned. It must be noted, when set to false, the method will still wait for locks to clear before sending the next required command. - :param timeout: Integer value, defautling to 3600 seconds, for the wait state to reach completion. - :param lock_timeout: Integer value, defaulting to 600 seconds, for locks to clear. - :param provision_state: The expected provision state, one of "enroll" "manageable" or "available". Using "available" results in automated cleaning. - :param kwargs: Key value pairs to be passed to the Ironic API, including uuid, name, chassis_uuid, driver_info, properties. - :raises: OpenStackCloudException on operation error. - - :rtype: :class:`~openstack.baremetal.v1.node.Node`. :returns: Current state of the node. + :rtype: :class:`~openstack.baremetal.v1.node.Node`. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ if provision_state not in ('enroll', 'manageable', 'available'): raise ValueError( @@ -301,14 +296,13 @@ def unregister_machine(self, nics, uuid, wait=None, timeout=600): :param nics: An array of strings that consist of MAC addresses to be removed. :param string uuid: The UUID of the node to be deleted. - :param wait: DEPRECATED, do not use. - :param timeout: Integer value, representing seconds with a default value of 600, which controls the maximum amount of time to block until a lock is released on machine. - :raises: OpenStackCloudException on operation failure. + :raises: :class:`~openstack.exceptions.SDKException` on operation + failure. """ if wait is not None: warnings.warn( @@ -319,7 +313,7 @@ def unregister_machine(self, nics, uuid, wait=None, timeout=600): machine = self.get_machine(uuid) invalid_states = ['active', 'cleaning', 'clean wait', 'clean failed'] if machine['provision_state'] in invalid_states: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Error unregistering node '%s' due to current provision " "state '%s'" % (uuid, machine['provision_state']) ) @@ -330,8 +324,8 @@ def unregister_machine(self, nics, uuid, wait=None, timeout=600): # failure, and resubitted the request in python-ironicclient. try: self.baremetal.wait_for_node_reservation(machine, timeout) - except exc.OpenStackCloudException as e: - raise exc.OpenStackCloudException( + except exceptions.SDKException as e: + raise exceptions.SDKException( "Error unregistering node '%s': Exception occured while" " waiting to be able to proceed: %s" % (machine['uuid'], e) ) @@ -375,10 +369,10 @@ def patch_machine(self, name_or_id, patch): 'value': 'administrator' }) - :raises: OpenStackCloudException on operation error. - :returns: Current state of the node. :rtype: :class:`~openstack.baremetal.v1.node.Node`. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ return self.baremetal.patch_node(name_or_id, patch) @@ -391,16 +385,16 @@ def update_machine(self, name_or_id, **attrs): :param string name_or_id: A machine name or UUID to be updated. :param attrs: Attributes to updated on the machine. - :raises: OpenStackCloudException on operation error. - :returns: Dictionary containing a machine sub-dictonary consisting of the updated data returned from the API update operation, and a list named changes which contains all of the API paths that received updates. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ machine = self.get_machine(name_or_id) if not machine: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Machine update failed to find Machine: %s. " % name_or_id ) @@ -411,7 +405,7 @@ def update_machine(self, name_or_id, **attrs): machine._to_munch(), new_config ) except Exception as e: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Machine update failed - Error generating JSON patch object " "for submission to the API. Machine: %s Error: %s" % (name_or_id, e) @@ -504,10 +498,10 @@ def node_set_provision_state( representing the amount of time to wait for the desire end state to be reached. - :raises: OpenStackCloudException on operation error. - :returns: Current state of the machine upon exit of the method. :rtype: :class:`~openstack.baremetal.v1.node.Node`. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ node = self.baremetal.set_node_provision_state( name_or_id, @@ -534,9 +528,9 @@ def set_machine_maintenance_state( the baremetal API to allow for notation as to why the node is in maintenance state. - :raises: OpenStackCloudException on operation error. - :returns: None + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ if state: self.baremetal.set_node_maintenance(name_or_id, reason) @@ -554,9 +548,9 @@ def remove_machine_from_maintenance(self, name_or_id): :param string name_or_id: The Name or UUID value representing the baremetal node. - :raises: OpenStackCloudException on operation error. - :returns: None + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ self.baremetal.unset_node_maintenance(name_or_id) @@ -568,9 +562,9 @@ def set_machine_power_on(self, name_or_id): :params string name_or_id: A string representing the baremetal node to have power turned to an "on" state. - :raises: OpenStackCloudException on operation error. - :returns: None + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ self.baremetal.set_node_power_state(name_or_id, 'power on') @@ -582,9 +576,9 @@ def set_machine_power_off(self, name_or_id): :params string name_or_id: A string representing the baremetal node to have power turned to an "off" state. - :raises: OpenStackCloudException on operation error. - - :returns: + :returns: None + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ self.baremetal.set_node_power_state(name_or_id, 'power off') @@ -598,9 +592,9 @@ def set_machine_power_reboot(self, name_or_id): :params string name_or_id: A string representing the baremetal node to have power turned to an "off" state. - :raises: OpenStackCloudException on operation error. - :returns: None + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ self.baremetal.set_node_power_state(name_or_id, 'rebooting') @@ -637,7 +631,8 @@ def wait_for_baremetal_node_lock(self, node, timeout=30): DEPRECATED, use ``wait_for_node_reservation`` on the `baremetal` proxy. - :raises: OpenStackCloudException upon client failure. + :raises: :class:`~openstack.exceptions.SDKException` upon client + failure. :returns: None """ warnings.warn( diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index a03c4dd81..501f51851 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -15,7 +15,6 @@ from openstack.block_storage.v3._proxy import Proxy from openstack.block_storage.v3 import quota_set as _qs from openstack.cloud import _utils -from openstack.cloud import exc from openstack import exceptions from openstack import warnings as os_warnings @@ -134,9 +133,12 @@ def create_volume( :param bootable: (optional) Make this volume bootable. If set, wait will also be set to true. :param kwargs: Keyword arguments as expected for cinder client. + :returns: The created volume ``Volume`` object. - :raises: OpenStackCloudTimeout if wait time exceeded. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if wait time + exceeded. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ if bootable is not None: wait = True @@ -144,7 +146,7 @@ def create_volume( if image: image_obj = self.get_image(image) if not image_obj: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Image {image} was requested as the basis for a new" " volume, but was not found on the cloud".format( image=image @@ -157,7 +159,7 @@ def create_volume( volume = self.block_storage.create_volume(**kwargs) if volume['status'] == 'error': - raise exc.OpenStackCloudException("Error in creating volume") + raise exceptions.SDKException("Error in creating volume") if wait: self.block_storage.wait_for_status(volume, wait=timeout) @@ -177,9 +179,7 @@ def update_volume(self, name_or_id, **kwargs): volume = self.get_volume(name_or_id) if not volume: - raise exc.OpenStackCloudException( - "Volume %s not found." % name_or_id - ) + raise exceptions.SDKException("Volume %s not found." % name_or_id) volume = self.block_storage.update_volume(volume, **kwargs) @@ -192,15 +192,17 @@ def set_volume_bootable(self, name_or_id, bootable=True): :param bool bootable: Whether the volume should be bootable. (Defaults to True) - :raises: OpenStackCloudTimeout if wait time exceeded. - :raises: OpenStackCloudException on operation error. :returns: None + :raises: :class:`~openstack.exceptions.ResourceTimeout` if wait time + exceeded. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ volume = self.get_volume(name_or_id) if not volume: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Volume {name_or_id} does not exist".format( name_or_id=name_or_id ) @@ -222,9 +224,12 @@ def delete_volume( :param timeout: Seconds to wait for volume deletion. None is forever. :param force: Force delete volume even if the volume is in deleting or error_deleting state. + :returns: True if deletion was successful, else False. - :raises: OpenStackCloudTimeout if wait time exceeded. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if wait time + exceeded. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ volume = self.block_storage.find_volume(name_or_id) @@ -272,7 +277,7 @@ def get_volume_limits(self, name_or_id=None): if name_or_id: project = self.get_project(name_or_id) if not project: - raise exc.OpenStackCloudException("project does not exist") + raise exceptions.SDKException("project does not exist") params['project'] = project return self.block_storage.get_limits(**params) @@ -317,9 +322,12 @@ def detach_volume(self, server, volume, wait=True, timeout=None): :param volume: The volume dict to detach. :param wait: If true, waits for volume to be detached. :param timeout: Seconds to wait for volume detachment. None is forever. + :returns: None - :raises: OpenStackCloudTimeout if wait time exceeded. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if wait time + exceeded. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ self.compute.delete_volume_attachment( server=server['id'], @@ -354,19 +362,22 @@ def attach_volume( :param device: The device name where the volume will attach. :param wait: If true, waits for volume to be attached. :param timeout: Seconds to wait for volume attachment. None is forever. + :returns: a volume attachment object. - :raises: OpenStackCloudTimeout if wait time exceeded. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if wait time + exceeded. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ dev = self.get_volume_attach_device(volume, server['id']) if dev: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Volume %s already attached to server %s on device %s" % (volume['id'], server['id'], dev) ) if volume['status'] != 'available': - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Volume %s is not available. Status is '%s'" % (volume['id'], volume['status']) ) @@ -422,9 +433,12 @@ def create_volume_snapshot( :param wait: If true, waits for volume snapshot to be created. :param timeout: Seconds to wait for volume snapshot creation. None is forever. + :returns: The created volume ``Snapshot`` object. - :raises: OpenStackCloudTimeout if wait time exceeded. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if wait time + exceeded. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ kwargs = self._get_volume_kwargs(kwargs) payload = {'volume_id': volume_id, 'force': force} @@ -500,9 +514,12 @@ def create_volume_backup( forever. :param incremental: If set to true, the backup will be incremental. :param snapshot_id: The UUID of the source snapshot to back up. + :returns: The created volume ``Backup`` object. - :raises: OpenStackCloudTimeout if wait time exceeded. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if wait time + exceeded. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ payload = { 'name': name, @@ -596,12 +613,14 @@ def delete_volume_backup( :param force: Allow delete in state other than error or available. :param wait: If true, waits for volume backup to be deleted. :param timeout: Seconds to wait for volume backup deletion. None is - forever. + forever. + :returns: True if deletion was successful, else False. - :raises: OpenStackCloudTimeout if wait time exceeded. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if wait time + exceeded. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ - volume_backup = self.get_volume_backup(name_or_id) if not volume_backup: @@ -627,9 +646,12 @@ def delete_volume_snapshot( :param wait: If true, waits for volume snapshot to be deleted. :param timeout: Seconds to wait for volume snapshot deletion. None is forever. + :returns: True if deletion was successful, else False. - :raises: OpenStackCloudTimeout if wait time exceeded. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if wait time + exceeded. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ volumesnapshot = self.get_volume_snapshot(name_or_id) @@ -764,11 +786,12 @@ def get_volume_type_access(self, name_or_id): :param name_or_id: Name or unique ID of the volume type. :returns: A volume ``Type`` object if found, else None. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ volume_type = self.get_volume_type(name_or_id) if not volume_type: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "VolumeType not found: %s" % name_or_id ) @@ -781,12 +804,14 @@ def add_volume_type_access(self, name_or_id, project_id): :param name_or_id: ID or name of a volume_type :param project_id: A project id + :returns: None - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ volume_type = self.get_volume_type(name_or_id) if not volume_type: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "VolumeType not found: %s" % name_or_id ) @@ -797,12 +822,14 @@ def remove_volume_type_access(self, name_or_id, project_id): :param name_or_id: ID or name of a volume_type :param project_id: A project id + :returns: None - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ volume_type = self.get_volume_type(name_or_id) if not volume_type: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "VolumeType not found: %s" % name_or_id ) self.block_storage.remove_type_access(volume_type, project_id) @@ -812,9 +839,10 @@ def set_volume_quotas(self, name_or_id, **kwargs): :param name_or_id: project name or id :param kwargs: key/value pairs of quota name and quota value + :returns: None - :raises: OpenStackCloudException if the resource to set the - quota does not exist. + :raises: :class:`~openstack.exceptions.SDKException` if the resource to + set the quota does not exist. """ proj = self.identity.find_project(name_or_id, ignore_missing=False) @@ -827,8 +855,10 @@ def get_volume_quotas(self, name_or_id): """Get volume quotas for a project :param name_or_id: project name or id + :returns: A volume ``QuotaSet`` object with the quotas - :raises: OpenStackCloudException if it's not a valid project + :raises: :class:`~openstack.exceptions.SDKException` if it's not a + valid project """ proj = self.identity.find_project(name_or_id, ignore_missing=False) @@ -838,9 +868,10 @@ def delete_volume_quotas(self, name_or_id): """Delete volume quotas for a project :param name_or_id: project name or id + :returns: The deleted volume ``QuotaSet`` object. - :raises: OpenStackCloudException if it's not a valid project or the - call failed + :raises: :class:`~openstack.exceptions.SDKException` if it's not a + valid project or the call failed """ proj = self.identity.find_project(name_or_id, ignore_missing=False) diff --git a/openstack/cloud/_coe.py b/openstack/cloud/_coe.py index f568c6ceb..6bf0ba059 100644 --- a/openstack/cloud/_coe.py +++ b/openstack/cloud/_coe.py @@ -11,7 +11,7 @@ # limitations under the License. from openstack.cloud import _utils -from openstack.cloud import exc +from openstack import exceptions class CoeCloudMixin: @@ -20,8 +20,8 @@ def list_coe_clusters(self): :returns: A list of container infrastructure management ``Cluster`` objects. - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ return list(self.container_infrastructure_management.clusters()) @@ -35,8 +35,8 @@ def search_coe_clusters(self, name_or_id=None, filters=None): :returns: A list of container infrastructure management ``Cluster`` objects. - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ coe_clusters = self.list_coe_clusters() return _utils._filter_list(coe_clusters, name_or_id, filters) @@ -77,11 +77,10 @@ def create_coe_cluster( :param string cluster_template_id: ID of the cluster template to use. :param dict kwargs: Any other arguments to pass in. - :returns: a dict containing the cluster description :returns: The created container infrastructure management ``Cluster`` object. - :raises: ``OpenStackCloudException`` if something goes wrong during - the OpenStack API call + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call """ cluster = self.container_infrastructure_management.create_cluster( name=name, @@ -95,10 +94,11 @@ def delete_coe_cluster(self, name_or_id): """Delete a COE cluster. :param name_or_id: Name or unique ID of the cluster. + :returns: True if the delete succeeded, False if the cluster was not found. - - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ cluster = self.get_coe_cluster(name_or_id) @@ -122,11 +122,12 @@ def update_coe_cluster(self, name_or_id, **kwargs): :returns: The updated cluster ``Cluster`` object. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ cluster = self.get_coe_cluster(name_or_id) if not cluster: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "COE cluster %s not found." % name_or_id ) @@ -158,8 +159,8 @@ def sign_coe_cluster_certificate(self, cluster_id, csr): certificate that client will use to communicate with the cluster. :returns: a dict representing the signed certs. - - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ return self.container_infrastructure_management.create_cluster_certificate( # noqa: E501 cluster_uuid=cluster_id, csr=csr @@ -172,9 +173,8 @@ def list_cluster_templates(self, detail=False): ClusterTemplates are always returned with full details. :returns: a list of dicts containing the cluster template details. - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ return list( self.container_infrastructure_management.cluster_templates() @@ -191,9 +191,8 @@ def search_cluster_templates( detailed output. :returns: a list of dict containing the cluster templates - - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException`: if something goes + wrong during the OpenStack API call. """ cluster_templates = self.list_cluster_templates(detail=detail) return _utils._filter_list(cluster_templates, name_or_id, filters) @@ -240,9 +239,8 @@ def create_cluster_template( Other arguments will be passed in kwargs. :returns: a dict containing the cluster template description - - :raises: ``OpenStackCloudException`` if something goes wrong during - the OpenStack API call + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call """ cluster_template = ( self.container_infrastructure_management.create_cluster_template( @@ -260,10 +258,11 @@ def delete_cluster_template(self, name_or_id): """Delete a cluster template. :param name_or_id: Name or unique ID of the cluster template. + :returns: True if the delete succeeded, False if the cluster template was not found. - - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ cluster_template = self.get_cluster_template(name_or_id) @@ -287,12 +286,12 @@ def update_cluster_template(self, name_or_id, **kwargs): :param name_or_id: Name or ID of the cluster template being updated. :returns: an update cluster template. - - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ cluster_template = self.get_cluster_template(name_or_id) if not cluster_template: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Cluster template %s not found." % name_or_id ) @@ -306,8 +305,9 @@ def update_cluster_template(self, name_or_id, **kwargs): def list_magnum_services(self): """List all Magnum services. - :returns: a list of dicts containing the service details. - :raises: OpenStackCloudException on operation error. + :returns: a list of dicts containing the service details. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ return list(self.container_infrastructure_management.services()) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 971cbd0e4..2a2df8bdb 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -99,8 +99,9 @@ def get_flavor_by_ram(self, ram, include=None, get_extra=True): :param string include: If given, will return a flavor whose name contains this string as a substring. :param get_extra: + :returns: A compute ``Flavor`` object. - :raises: :class:`~openstack.exceptions.OpenStackCloudException` if no + :raises: :class:`~openstack.exceptions.SDKException` if no matching flavour could be found. """ flavors = self.list_flavors(get_extra=get_extra) @@ -109,7 +110,7 @@ def get_flavor_by_ram(self, ram, include=None, get_extra=True): not include or include in flavor['name'] ): return flavor - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Could not find a flavor with {ram} and '{include}'".format( ram=ram, include=include ) @@ -175,10 +176,11 @@ def search_server_groups(self, name_or_id=None, filters=None): :param name_or_id: Name or unique ID of the server group(s). :param filters: A dict containing additional filters to use. + :returns: A list of compute ``ServerGroup`` objects matching the search criteria. - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ server_groups = self.list_server_groups() return _utils._filter_list(server_groups, name_or_id, filters) @@ -283,7 +285,8 @@ def add_server_security_groups(self, server, security_groups): :returns: False if server or security groups are undefined, True otherwise. - :raises: ``OpenStackCloudException``, on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ server, security_groups = self._get_server_security_groups( server, security_groups @@ -306,7 +309,8 @@ def remove_server_security_groups(self, server, security_groups): :returns: False if server or security groups are undefined, True otherwise. - :raises: ``OpenStackCloudException``, on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ server, security_groups = self._get_server_security_groups( server, security_groups @@ -378,16 +382,18 @@ def get_compute_limits(self, name_or_id=None): :param name_or_id: (optional) project name or ID to get limits for if different from the current project - :raises: OpenStackCloudException if it's not a valid project + :returns: A compute :class:`~openstack.compute.v2.limits.Limits.AbsoluteLimits` object. + :raises: :class:`~openstack.exceptions.SDKException` if it's not a + valid project """ params = {} project_id = None if name_or_id: proj = self.get_project(name_or_id) if not proj: - raise exc.OpenStackCloudException("project does not exist") + raise exceptions.SDKException("project does not exist") project_id = proj.id params['tenant_id'] = project_id return self.compute.get_limits(**params).absolute @@ -468,21 +474,21 @@ def get_server_console(self, server, length=None): :returns: A string containing the text of the console log or an empty string if the cloud does not support console logs. - :raises: OpenStackCloudException if an invalid server argument is given - or if something else unforseen happens + :raises: :class:`~openstack.exceptions.SDKException` if an invalid + server argument is given or if something else unforseen happens """ if not isinstance(server, dict): server = self.get_server(server, bare=True) if not server: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Console log requested for invalid server" ) try: return self._get_server_console_output(server['id'], length) - except exc.OpenStackCloudBadRequest: + except exceptions.BadRequestException: return "" def _get_server_console_output(self, server_id, length=None): @@ -582,8 +588,10 @@ def create_keypair(self, name, public_key=None): :param name: Name of the keypair being created. :param public_key: Public key for the new keypair. + :returns: The created compute ``Keypair`` object. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ keypair = { 'name': name, @@ -598,8 +606,8 @@ def delete_keypair(self, name): :param name: Name of the keypair to delete. :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ try: self.compute.delete_keypair(name, ignore_missing=False) @@ -630,13 +638,15 @@ def create_image_snapshot( :param wait: If true, waits for image to be created. :param timeout: Seconds to wait for image creation. None is forever. :param metadata: Metadata to give newly-created image entity + :returns: The created image ``Image`` object. - :raises: OpenStackCloudException if there are problems uploading + :raises: :class:`~openstack.exceptions.SDKException` if there are + problems uploading """ if not isinstance(server, dict): server_obj = self.get_server(server, bare=True) if not server_obj: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Server {server} could not be found and therefore" " could not be snapshotted.".format(server=server) ) @@ -800,8 +810,10 @@ def create_server( :param group: ServerGroup dict, name or id to boot the server in. If a group is provided in both scheduler_hints and in the group param, the group param will win. (Optional, defaults to None) + :returns: The created compute ``Server`` object. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ # TODO(shade) Image is optional but flavor is not - yet flavor comes # after image in the argument list. Doh. @@ -840,7 +852,7 @@ def create_server( if group: group_obj = self.get_server_group(group) if not group_obj: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Server Group {group} was requested but was not found" " on the cloud".format(group=group) ) @@ -856,7 +868,7 @@ def create_server( # Be nice and help the user out kwargs['nics'] = [kwargs['nics']] else: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'nics parameter to create_server takes a list of dicts.' ' Got: {nics}'.format(nics=kwargs['nics']) ) @@ -871,7 +883,7 @@ def create_server( else: network_obj = self.get_network(name_or_id=net_name) if not network_obj: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'Network {network} is not a valid network in' ' {cloud}:{region}'.format( network=network, @@ -899,7 +911,7 @@ def create_server( net_name = nic.pop('net-name') nic_net = self.get_network(net_name) if not nic_net: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Requested network {net} could not be found.".format( net=net_name ) @@ -908,7 +920,7 @@ def create_server( for ip_key in ('v4-fixed-ip', 'v6-fixed-ip', 'fixed_ip'): fixed_ip = nic.pop(ip_key, None) if fixed_ip and net.get('fixed_ip'): - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Only one of v4-fixed-ip, v6-fixed-ip or fixed_ip" " may be given" ) @@ -923,7 +935,7 @@ def create_server( utils.require_microversion(self.compute, '2.42') net['tag'] = nic.pop('tag') if nic: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Additional unsupported keys given for server network" " creation: {keys}".format(keys=nic.keys()) ) @@ -1018,7 +1030,7 @@ def _get_boot_from_volume_kwargs( if boot_volume: volume = self.get_volume(boot_volume) if not volume: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'Volume {boot_volume} is not a valid volume' ' in {cloud}:{region}'.format( boot_volume=boot_volume, @@ -1041,7 +1053,7 @@ def _get_boot_from_volume_kwargs( else: image_obj = self.get_image(image) if not image_obj: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'Image {image} is not a valid image in' ' {cloud}:{region}'.format( image=image, @@ -1074,7 +1086,7 @@ def _get_boot_from_volume_kwargs( for volume in volumes: volume_obj = self.get_volume(volume) if not volume_obj: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'Volume {volume} is not a valid volume' ' in {cloud}:{region}'.format( volume=volume, @@ -1126,7 +1138,7 @@ def wait_for_server( # and pass it down into the IP stack. remaining_timeout = timeout - int(time.time() - start_time) if remaining_timeout <= 0: - raise exc.OpenStackCloudTimeout(timeout_message) + raise exceptions.ResourceTimeout(timeout_message) server = self.get_active_server( server=server, @@ -1159,7 +1171,7 @@ def get_active_server( and server['fault'] is not None and 'message' in server['fault'] ): - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Error in creating the server." " Compute service reports fault: {reason}".format( reason=server['fault']['message'] @@ -1167,7 +1179,7 @@ def get_active_server( extra_data=dict(server=server), ) - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Error in creating the server" " (no further information available)", extra_data=dict(server=server), @@ -1195,13 +1207,13 @@ def get_active_server( try: self._delete_server(server=server, wait=wait, timeout=timeout) except Exception as e: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'Server reached ACTIVE state without being' ' allocated an IP address AND then could not' ' be deleted: {0}'.format(e), extra_data=dict(server=server), ) - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'Server reached ACTIVE state without being' ' allocated an IP address.', extra_data=dict(server=server), @@ -1253,12 +1265,14 @@ def set_server_metadata(self, name_or_id, metadata): :param dict metadata: A dictionary with the key=value pairs to set in the server instance. It only updates the key=value pairs provided. Existing ones will remain untouched. + :returns: None - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ server = self.get_server(name_or_id, bare=True) if not server: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'Invalid Server {server}'.format(server=name_or_id) ) @@ -1271,12 +1285,14 @@ def delete_server_metadata(self, name_or_id, metadata_keys): to update. :param metadata_keys: A list with the keys to be deleted from the server instance. + :returns: None - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ server = self.get_server(name_or_id, bare=True) if not server: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'Invalid Server {server}'.format(server=name_or_id) ) @@ -1301,9 +1317,11 @@ def delete_server( associated with the instance. :param int delete_ip_retry: Number of times to retry deleting any floating ips, should the first try be unsuccessful. + :returns: True if delete succeeded, False otherwise if the server does not exist. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ # If delete_ips is True, we need the server to not be bare. server = self.compute.find_server(name_or_id, ignore_missing=True) @@ -1332,7 +1350,7 @@ def _delete_server_floating_ips(self, server, delete_ip_retry): ip = self.get_floating_ip( id=None, filters={'floating_ip_address': fip['addr']} ) - except exc.OpenStackCloudURINotFound: + except exceptions.NotFoundException: # We're deleting. If it doesn't exist - awesome # NOTE(mordred) If the cloud is a nova FIP cloud but # floating_ip_source is set to neutron, this @@ -1342,7 +1360,7 @@ def _delete_server_floating_ips(self, server, delete_ip_retry): continue deleted = self.delete_floating_ip(ip['id'], retry=delete_ip_retry) if not deleted: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Tried to delete floating ip {floating_ip}" " associated with server {id} but there was" " an error deleting it. Not deleting server.".format( @@ -1396,8 +1414,10 @@ def update_server(self, name_or_id, detailed=False, bare=False, **kwargs): detailed = False. :param name: New name for the server :param description: New description for the server + :returns: The updated compute ``Server`` object. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ server = self.compute.find_server(name_or_id, ignore_missing=False) @@ -1410,8 +1430,10 @@ def create_server_group(self, name, policies=None, policy=None): :param name: Name of the server group being created :param policies: List of policies for the server group. + :returns: The created compute ``ServerGroup`` object. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ sg_attrs = {'name': name} if policies: @@ -1424,8 +1446,10 @@ def delete_server_group(self, name_or_id): """Delete a server group. :param name_or_id: Name or ID of the server group to delete + :returns: True if delete succeeded, False otherwise - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ server_group = self.get_server_group(name_or_id) if not server_group: @@ -1462,8 +1486,10 @@ def create_flavor( :param swap: Swap space in MB :param rxtx_factor: RX/TX factor :param is_public: Make flavor accessible to the public + :returns: The created compute ``Flavor`` object. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ attrs = { 'disk': disk, @@ -1486,8 +1512,10 @@ def delete_flavor(self, name_or_id): """Delete a flavor :param name_or_id: ID or name of the flavor to delete. + :returns: True if delete succeeded, False otherwise. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ try: flavor = self.compute.find_flavor(name_or_id) @@ -1497,7 +1525,7 @@ def delete_flavor(self, name_or_id): self.compute.delete_flavor(flavor) return True except exceptions.SDKException: - raise exceptions.OpenStackCloudException( + raise exceptions.SDKException( "Unable to delete flavor {name}".format(name=name_or_id) ) @@ -1507,8 +1535,10 @@ def set_flavor_specs(self, flavor_id, extra_specs): :param string flavor_id: ID of the flavor to update. :param dict extra_specs: Dictionary of key-value pairs. - :raises: OpenStackCloudException on operation error. - :raises: OpenStackCloudResourceNotFound if flavor ID is not found. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. + :raises: :class:`~openstack.exceptions.BadRequestException` if flavor + ID is not found. """ self.compute.create_flavor_extra_specs(flavor_id, extra_specs) @@ -1518,8 +1548,10 @@ def unset_flavor_specs(self, flavor_id, keys): :param string flavor_id: ID of the flavor to update. :param keys: List of spec keys to delete. - :raises: OpenStackCloudException on operation error. - :raises: OpenStackCloudResourceNotFound if flavor ID is not found. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. + :raises: :class:`~openstack.exceptions.BadRequestException` if flavor + ID is not found. """ for key in keys: self.compute.delete_flavor_extra_specs_property(flavor_id, key) @@ -1530,7 +1562,8 @@ def add_flavor_access(self, flavor_id, project_id): :param string flavor_id: ID of the private flavor. :param string project_id: ID of the project/tenant. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ self.compute.flavor_add_tenant_access(flavor_id, project_id) @@ -1540,7 +1573,8 @@ def remove_flavor_access(self, flavor_id, project_id): :param string flavor_id: ID of the private flavor. :param string project_id: ID of the project/tenant. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ self.compute.flavor_remove_tenant_access(flavor_id, project_id) @@ -1548,8 +1582,10 @@ def list_flavor_access(self, flavor_id): """List access from a private flavor for a project/tenant. :param string flavor_id: ID of the private flavor. + :returns: List of dicts with flavor_id and tenant_id attributes. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ return self.compute.get_flavor_access(flavor_id) @@ -1569,10 +1605,11 @@ def search_aggregates(self, name_or_id=None, filters=None): :param name: aggregate name or id. :param filters: a dict containing additional filters to use. + :returns: A list of compute ``Aggregate`` objects matching the search criteria. - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ aggregates = self.list_aggregates() return _utils._filter_list(aggregates, name_or_id, filters) @@ -1612,8 +1649,10 @@ def create_aggregate(self, name, availability_zone=None): :param name: Name of the host aggregate being created :param availability_zone: Availability zone to assign hosts + :returns: The created compute ``Aggregate`` object. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ return self.compute.create_aggregate( name=name, availability_zone=availability_zone @@ -1626,8 +1665,10 @@ def update_aggregate(self, name_or_id, **kwargs): :param name_or_id: Name or ID of the aggregate being updated. :param name: New aggregate name :param availability_zone: Availability zone to assign to hosts + :returns: The updated compute ``Aggregate`` object. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ aggregate = self.get_aggregate(name_or_id) return self.compute.update_aggregate(aggregate, **kwargs) @@ -1636,8 +1677,10 @@ def delete_aggregate(self, name_or_id): """Delete a host aggregate. :param name_or_id: Name or ID of the host aggregate to delete. + :returns: True if delete succeeded, False otherwise. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ if isinstance(name_or_id, (str, bytes)) and not name_or_id.isdigit(): aggregate = self.get_aggregate(name_or_id) @@ -1662,12 +1705,12 @@ def set_aggregate_metadata(self, name_or_id, metadata): {'key': None} to remove a key) :returns: a dict representing the new host aggregate. - - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ aggregate = self.get_aggregate(name_or_id) if not aggregate: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Host aggregate %s not found." % name_or_id ) @@ -1679,11 +1722,12 @@ def add_host_to_aggregate(self, name_or_id, host_name): :param name_or_id: Name or ID of the host aggregate. :param host_name: Host to add. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ aggregate = self.get_aggregate(name_or_id) if not aggregate: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Host aggregate %s not found." % name_or_id ) @@ -1695,11 +1739,12 @@ def remove_host_from_aggregate(self, name_or_id, host_name): :param name_or_id: Name or ID of the host aggregate. :param host_name: Host to remove. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ aggregate = self.get_aggregate(name_or_id) if not aggregate: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Host aggregate %s not found." % name_or_id ) @@ -1711,8 +1756,8 @@ def set_compute_quotas(self, name_or_id, **kwargs): :param name_or_id: project name or id :param kwargs: key/value pairs of quota name and quota value - :raises: OpenStackCloudException if the resource to set the - quota does not exist. + :raises: :class:`~openstack.exceptions.SDKException` if the resource to + set the quota does not exist. """ proj = self.identity.find_project(name_or_id, ignore_missing=False) kwargs['force'] = True @@ -1724,8 +1769,10 @@ def get_compute_quotas(self, name_or_id): """Get quota for a project :param name_or_id: project name or id + :returns: A compute ``QuotaSet`` object if found, else None. - :raises: OpenStackCloudException if it's not a valid project + :raises: :class:`~openstack.exceptions.SDKException` if it's not a + valid project """ proj = self.identity.find_project(name_or_id, ignore_missing=False) return self.compute.get_quota_set(proj) @@ -1734,9 +1781,9 @@ def delete_compute_quotas(self, name_or_id): """Delete quota for a project :param name_or_id: project name or id - :raises: OpenStackCloudException if it's not a valid project or the - nova client call failed - :returns: None + + :raises: :class:`~openstack.exceptions.SDKException` if it's not a + valid project or the nova client call failed """ proj = self.identity.find_project(name_or_id, ignore_missing=False) self.compute.revert_quota_set(proj) @@ -1750,9 +1797,10 @@ def get_compute_usage(self, name_or_id, start=None, end=None): was started) :param end: :class:`datetime.datetime` or string. End date in UTC. Defaults to now - :raises: OpenStackCloudException if it's not a valid project :returns: A :class:`~openstack.compute.v2.usage.Usage` object + :raises: :class:`~openstack.exceptions.SDKException` if it's not a + valid project """ def parse_date(date): @@ -1762,7 +1810,7 @@ def parse_date(date): # Yes. This is an exception mask. However,iso8601 is an # implementation detail - and the error message is actually # less informative. - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Date given, {date}, is invalid. Please pass in a date" " string in ISO 8601 format -" " YYYY-MM-DDTHH:MM:SS".format(date=date) @@ -1775,7 +1823,7 @@ def parse_date(date): proj = self.get_project(name_or_id) if not proj: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "project does not exist: {name}".format(name=proj.id) ) diff --git a/openstack/cloud/_dns.py b/openstack/cloud/_dns.py index a896f90b6..5c5ca80b3 100644 --- a/openstack/cloud/_dns.py +++ b/openstack/cloud/_dns.py @@ -11,7 +11,6 @@ # limitations under the License. from openstack.cloud import _utils -from openstack.cloud import exc from openstack.dns.v2._proxy import Proxy from openstack import exceptions from openstack import resource @@ -74,8 +73,8 @@ def create_zone( if zone_type is secondary) :returns: a dict representing the created zone. - - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ # We capitalize in case the user passes time in lowercase, as @@ -83,7 +82,7 @@ def create_zone( if zone_type is not None: zone_type = zone_type.upper() if zone_type not in ('PRIMARY', 'SECONDARY'): - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Invalid type %s, valid choices are PRIMARY or SECONDARY" % zone_type ) @@ -105,7 +104,7 @@ def create_zone( try: return self.dns.create_zone(**zone) except exceptions.SDKException: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Unable to create zone {name}".format(name=name) ) @@ -122,14 +121,12 @@ def update_zone(self, name_or_id, **kwargs): if zone_type is secondary) :returns: a dict representing the updated zone. - - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ zone = self.get_zone(name_or_id) if not zone: - raise exc.OpenStackCloudException( - "Zone %s not found." % name_or_id - ) + raise exceptions.SDKException("Zone %s not found." % name_or_id) return self.dns.update_zone(zone['id'], **kwargs) @@ -139,8 +136,8 @@ def delete_zone(self, name_or_id): :param name_or_id: Name or ID of the zone being deleted. :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ zone = self.dns.find_zone(name_or_id) @@ -166,7 +163,7 @@ def list_recordsets(self, zone): else: zone_obj = self.get_zone(zone) if zone_obj is None: - raise exc.OpenStackCloudException("Zone %s not found." % zone) + raise exceptions.SDKException("Zone %s not found." % zone) return list(self.dns.recordsets(zone_obj)) def get_recordset(self, zone, name_or_id): @@ -185,7 +182,7 @@ def get_recordset(self, zone, name_or_id): else: zone_obj = self.get_zone(zone) if not zone_obj: - raise exc.OpenStackCloudException("Zone %s not found." % zone) + raise exceptions.SDKException("Zone %s not found." % zone) try: return self.dns.find_recordset( zone=zone_obj, name_or_id=name_or_id, ignore_missing=False @@ -211,16 +208,15 @@ def create_recordset( :param ttl: TTL value of the recordset :returns: a dict representing the created recordset. - - :raises: OpenStackCloudException on operation error. - + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ if isinstance(zone, resource.Resource): zone_obj = zone else: zone_obj = self.get_zone(zone) if not zone_obj: - raise exc.OpenStackCloudException("Zone %s not found." % zone) + raise exceptions.SDKException("Zone %s not found." % zone) # We capitalize the type in case the user sends in lowercase recordset_type = recordset_type.upper() @@ -247,13 +243,13 @@ def update_recordset(self, zone, name_or_id, **kwargs): :param ttl: TTL (Time to live) value in seconds of the recordset :returns: a dict representing the updated recordset. - - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ rs = self.get_recordset(zone, name_or_id) if not rs: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Recordset %s not found." % name_or_id ) @@ -269,8 +265,8 @@ def delete_recordset(self, zone, name_or_id): :param name_or_id: Name or ID of the recordset being deleted. :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ recordset = self.get_recordset(zone, name_or_id) diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index e63dd23fb..9d0ca4648 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -67,7 +67,7 @@ def _neutron_list_floating_ips(self, filters=None): def _nova_list_floating_ips(self): try: data = proxy._json_response(self.compute.get('/os-floating-ips')) - except exc.OpenStackCloudURINotFound: + except exceptions.NotFoundException: return [] return self._get_and_munchify('floating_ips', data) @@ -109,7 +109,7 @@ def list_floating_ips(self, filters=None): if self._use_neutron_floating(): try: return self._neutron_list_floating_ips(filters) - except exc.OpenStackCloudURINotFound as e: + except exceptions.NotFoundException as e: # Nova-network don't support server-side floating ips # filtering, so it's safer to return an empty list than # to fallback to Nova which may return more results that @@ -206,8 +206,8 @@ def _neutron_available_floating_ips( :param server: (server) Server the Floating IP is for :returns: a list of floating IP addresses. - :raises: ``OpenStackCloudResourceNotFound``, if an external network - that meets the specified criteria cannot be found. + :raises: :class:`~openstack.exceptions.BadRequestException` if an + external network that meets the specified criteria cannot be found. """ if project_id is None: # Make sure we are only listing floatingIPs allocated the current @@ -229,7 +229,7 @@ def _neutron_available_floating_ips( break if floating_network_id is None: - raise exc.OpenStackCloudResourceNotFound( + raise exceptions.NotFoundException( "unable to find external network {net}".format(net=network) ) else: @@ -265,9 +265,8 @@ def _nova_available_floating_ips(self, pool=None): :param pool: Nova floating IP pool name. :returns: a list of floating IP addresses. - - :raises: ``OpenStackCloudResourceNotFound``, if a floating IP pool - is not specified and cannot be found. + :raises: :class:`~openstack.exceptions.BadRequestException` if a + floating IP pool is not specified and cannot be found. """ with _utils.openstacksdk_exceptions( @@ -276,7 +275,7 @@ def _nova_available_floating_ips(self, pool=None): if pool is None: pools = self.list_floating_ip_pools() if not pools: - raise exc.OpenStackCloudResourceNotFound( + raise exceptions.NotFoundException( "unable to find a floating ip pool" ) pool = pools[0]['name'] @@ -323,7 +322,7 @@ def available_floating_ip(self, network=None, server=None): network=network, server=server ) return f_ips[0] - except exc.OpenStackCloudURINotFound as e: + except exceptions.NotFoundException as e: self.log.debug( "Something went wrong talking to neutron API: " "'%(msg)s'. Trying with Nova.", @@ -346,7 +345,7 @@ def _get_floating_network_id(self): if floating_network: floating_network_id = floating_network else: - raise exc.OpenStackCloudResourceNotFound( + raise exceptions.NotFoundException( "unable to find an external network" ) return floating_network_id @@ -384,8 +383,8 @@ def create_floating_ip( provided. :returns: a floating IP address - - :raises: ``OpenStackCloudException``, on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ if self._use_neutron_floating(): try: @@ -398,7 +397,7 @@ def create_floating_ip( wait=wait, timeout=timeout, ) - except exc.OpenStackCloudURINotFound as e: + except exceptions.NotFoundException as e: self.log.debug( "Something went wrong talking to neutron API: " "'%(msg)s'. Trying with Nova.", @@ -407,7 +406,7 @@ def create_floating_ip( # Fall-through, trying with Nova if port: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "This cloud uses nova-network which does not support" " arbitrary floating-ip/port mappings. Please nudge" " your cloud provider to upgrade the networking stack" @@ -440,7 +439,7 @@ def _neutron_create_floating_ip( try: network = self.network.find_network(network_name_or_id) except exceptions.ResourceNotFound: - raise exc.OpenStackCloudResourceNotFound( + raise exceptions.NotFoundException( "unable to find network for floating ips with ID " "{0}".format(network_name_or_id) ) @@ -481,7 +480,7 @@ def _neutron_create_floating_ip( fip = self.get_floating_ip(fip_id) if fip and fip['status'] == 'ACTIVE': break - except exc.OpenStackCloudTimeout: + except exceptions.ResourceTimeout: self.log.error( "Timed out on floating ip %(fip)s becoming active." " Deleting", @@ -499,7 +498,7 @@ def _neutron_create_floating_ip( raise if fip['port_id'] != port: if server: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Attempted to create FIP on port {port} for server" " {server} but FIP has port {port_id}".format( port=port, @@ -508,7 +507,7 @@ def _neutron_create_floating_ip( ) ) else: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Attempted to create FIP on port {port}" " but something went wrong".format(port=port) ) @@ -521,7 +520,7 @@ def _nova_create_floating_ip(self, pool=None): if pool is None: pools = self.list_floating_ip_pools() if not pools: - raise exc.OpenStackCloudResourceNotFound( + raise exceptions.NotFoundException( "unable to find a floating ip pool" ) pool = pools[0]['name'] @@ -548,9 +547,9 @@ def delete_floating_ip(self, floating_ip_id, retry=1): occur. :returns: True if the IP address has been deleted, False if the IP - address was not found. - - :raises: ``OpenStackCloudException``, on operation error. + address was not found. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ for count in range(0, max(0, retry) + 1): result = self._delete_floating_ip(floating_ip_id) @@ -566,7 +565,7 @@ def delete_floating_ip(self, floating_ip_id, retry=1): if not f_ip or f_ip['status'] == 'DOWN': return True - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Attempted to delete Floating IP {ip} with ID {id} a total of" " {retry} times. Although the cloud did not indicate any errors" " the floating ip is still in existence. Aborting further" @@ -581,7 +580,7 @@ def _delete_floating_ip(self, floating_ip_id): if self._use_neutron_floating(): try: return self._neutron_delete_floating_ip(floating_ip_id) - except exc.OpenStackCloudURINotFound as e: + except exceptions.NotFoundException as e: self.log.debug( "Something went wrong talking to neutron API: " "'%(msg)s'. Trying with Nova.", @@ -606,7 +605,7 @@ def _nova_delete_floating_ip(self, floating_ip_id): fip_id=floating_ip_id ), ) - except exc.OpenStackCloudURINotFound: + except exceptions.NotFoundException: return False return True @@ -629,8 +628,8 @@ def delete_unattached_floating_ips(self, retry=1): occur. :returns: Number of Floating IPs deleted, False if none - - :raises: ``OpenStackCloudException``, on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ processed = [] if self._use_neutron_floating(): @@ -669,8 +668,8 @@ def _attach_ip_to_server( FIP to attach to will come from. :returns: The server ``openstack.compute.v2.server.Server`` - - :raises: OpenStackCloudException, on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ # Short circuit if we're asking to attach an IP that's already # attached @@ -697,7 +696,7 @@ def _attach_ip_to_server( fixed_address=fixed_address, nat_destination=nat_destination, ) - except exc.OpenStackCloudURINotFound as e: + except exceptions.NotFoundException as e: self.log.debug( "Something went wrong talking to neutron API: " "'%(msg)s'. Trying with Nova.", @@ -738,7 +737,7 @@ def _neutron_attach_ip_to_server( nat_destination=nat_destination, ) if not port: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "unable to find a port for server {0}".format(server['id']) ) @@ -753,7 +752,7 @@ def _nova_attach_ip_to_server( ): f_ip = self.get_floating_ip(id=floating_ip_id) if f_ip is None: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "unable to find floating IP {0}".format(floating_ip_id) ) error_message = "Error attaching IP {ip} to instance {id}".format( @@ -777,16 +776,16 @@ def detach_ip_from_server(self, server_id, floating_ip_id): :param floating_ip_id: Id of the floating IP to detach. :returns: True if the IP has been detached, or False if the IP wasn't - attached to any server. - - :raises: ``OpenStackCloudException``, on operation error. + attached to any server. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ if self._use_neutron_floating(): try: return self._neutron_detach_ip_from_server( server_id=server_id, floating_ip_id=floating_ip_id ) - except exc.OpenStackCloudURINotFound as e: + except exceptions.NotFoundException as e: self.log.debug( "Something went wrong talking to neutron API: " "'%(msg)s'. Trying with Nova.", @@ -820,7 +819,7 @@ def _neutron_detach_ip_from_server(self, server_id, floating_ip_id): def _nova_detach_ip_from_server(self, server_id, floating_ip_id): f_ip = self.get_floating_ip(id=floating_ip_id) if f_ip is None: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "unable to find floating IP {0}".format(floating_ip_id) ) error_message = "Error detaching IP {ip} from instance {id}".format( @@ -921,8 +920,8 @@ def add_ip_list( floating IP should be on :returns: The updated server ``openstack.compute.v2.server.Server`` - - :raises: ``OpenStackCloudException``, on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ if type(ips) != list: @@ -999,7 +998,7 @@ def _add_auto_ip(self, server, wait=False, timeout=60, reuse=True): timeout=timeout, skip_attach=skip_attach, ) - except exc.OpenStackCloudTimeout: + except exceptions.ResourceTimeout: if self._use_neutron_floating() and created: # We are here because we created an IP on the port # It failed. Delete so as not to leak an unmanaged @@ -1131,7 +1130,7 @@ def _needs_floating_ip(self, server, nat_destination): # No floating ip network - no FIPs try: self._get_floating_network_id() - except exc.OpenStackCloudException: + except exceptions.SDKException: return False (port_obj, fixed_ip_address) = self._nat_destination_port( @@ -1169,7 +1168,7 @@ def _nat_destination_port( if nat_destination: nat_network = self.get_network(nat_destination) if not nat_network: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'NAT Destination {nat_destination} was configured' ' but not found on the cloud. Please check your' ' config and your cloud and try again.'.format( @@ -1180,7 +1179,7 @@ def _nat_destination_port( nat_network = self.get_nat_destination() if not nat_network: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'Multiple ports were found for server {server}' ' but none of the networks are a valid NAT' ' destination, so it is impossible to add a' @@ -1198,7 +1197,7 @@ def _nat_destination_port( if maybe_port['network_id'] == nat_network['id']: maybe_ports.append(maybe_port) if not maybe_ports: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'No port on server {server} was found matching' ' your NAT destination network {dest}. Please ' ' check your config'.format( @@ -1224,7 +1223,7 @@ def _nat_destination_port( if ip.version == 4: fixed_address = address['ip_address'] return port, fixed_address - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "unable to find a free fixed IPv4 address for server " "{0}".format(server['id']) ) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index 62daed4ab..5ee0e6aca 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -11,7 +11,6 @@ # limitations under the License. from openstack.cloud import _utils -from openstack.cloud import exc from openstack import exceptions from openstack.identity.v3._proxy import Proxy from openstack import utils @@ -41,7 +40,7 @@ def _get_domain_id_param_dict(self, domain_id): # mention api versions if utils.supports_version(self.identity, '3'): if not domain_id: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "User or project creation requires an explicit domain_id " "argument." ) @@ -87,8 +86,8 @@ def list_projects(self, domain_id=None, name_or_id=None, filters=None): "[?last_name==`Smith`] | [?other.gender]==`Female`]" :returns: A list of identity ``Project`` objects. - :raises: ``OpenStackCloudException`` if something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ if not filters: filters = {} @@ -154,9 +153,10 @@ def get_project(self, name_or_id, filters=None, domain_id=None): "[?last_name==`Smith`] | [?other.gender]==`Female`]" :param domain_id: Domain ID to scope the retrieved project. + :returns: An identity ``Project`` object. - :raises: ``OpenStackCloudException`` if something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ return _utils._get_entity( self, 'project', name_or_id, filters, domain_id=domain_id @@ -218,9 +218,10 @@ def delete_project(self, name_or_id, domain_id=None): :param name_or_id: Name or unique ID of the project. :param domain_id: Domain ID to scope the retrieved project. + :returns: True if delete succeeded, False if the project was not found. - :raises: ``OpenStackCloudException`` if something goes wrong during - the OpenStack API call + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call """ try: project = self.identity.find_project( @@ -245,9 +246,10 @@ def list_users(self, **kwargs): :param name: :param domain_id: Domain ID to scope the retrieved users. + :returns: A list of identity ``User`` objects. - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ return list(self.identity.users(**kwargs)) @@ -273,9 +275,10 @@ def search_users(self, name_or_id=None, filters=None, domain_id=None): Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :returns: A list of identity ``User`` objects - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ kwargs = {} # NOTE(jdwidari) if name_or_id isn't UUID like then make use of server- @@ -312,9 +315,10 @@ def get_user(self, name_or_id, filters=None, **kwargs): Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :returns: an identity ``User`` object - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ return _utils._get_entity(self, 'user', name_or_id, filters, **kwargs) @@ -397,13 +401,13 @@ def delete_user(self, name_or_id, **kwargs): def _get_user_and_group(self, user_name_or_id, group_name_or_id): user = self.get_user(user_name_or_id) if not user: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'User {user} not found'.format(user=user_name_or_id) ) group = self.get_group(group_name_or_id) if not group: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'Group {user} not found'.format(user=group_name_or_id) ) @@ -414,8 +418,9 @@ def add_user_to_group(self, name_or_id, group_name_or_id): :param name_or_id: Name or unique ID of the user. :param group_name_or_id: Group name or ID - :raises: ``OpenStackCloudException`` if something goes wrong during - the OpenStack API call + + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call """ user, group = self._get_user_and_group(name_or_id, group_name_or_id) @@ -426,9 +431,10 @@ def is_user_in_group(self, name_or_id, group_name_or_id): :param name_or_id: Name or unique ID of the user. :param group_name_or_id: Group name or ID + :returns: True if user is in the group, False otherwise - :raises: ``OpenStackCloudException`` if something goes wrong during - the OpenStack API call + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call """ user, group = self._get_user_and_group(name_or_id, group_name_or_id) @@ -439,8 +445,9 @@ def remove_user_from_group(self, name_or_id, group_name_or_id): :param name_or_id: Name or unique ID of the user. :param group_name_or_id: Group name or ID - :raises: ``OpenStackCloudException`` if something goes wrong during - the OpenStack API call + + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call """ user, group = self._get_user_and_group(name_or_id, group_name_or_id) @@ -455,9 +462,10 @@ def create_service(self, name, enabled=True, **kwargs): :param service_type: Service type. (type or service_type required.) :param description: Service description (optional). :param enabled: Whether the service is enabled (v3 only) + :returns: an identity ``Service`` object - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ type_ = kwargs.pop('type', None) service_type = kwargs.pop('service_type', None) @@ -489,8 +497,8 @@ def list_services(self): """List all Keystone services. :returns: A list of identity ``Service`` object - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ return list(self.identity.services()) @@ -515,9 +523,10 @@ def search_services(self, name_or_id=None, filters=None): Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :returns: a list of identity ``Service`` objects - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ services = self.list_services() return _utils._filter_list(services, name_or_id, filters) @@ -527,9 +536,11 @@ def get_service(self, name_or_id, filters=None): """Get exactly one Keystone service. :param name_or_id: Name or unique ID of the service. + :returns: an identity ``Service`` object - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call or if multiple matches are found. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call or if multiple matches are + found. """ return _utils._get_entity(self, 'service', name_or_id, filters) @@ -537,9 +548,10 @@ def delete_service(self, name_or_id): """Delete a Keystone service. :param name_or_id: Name or unique ID of the service. + :returns: True if delete succeeded, False otherwise. - :raises: ``OpenStackCloudException`` if something goes wrong during - the OpenStack API call + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call """ service = self.get_service(name_or_id=name_or_id) if service is None: @@ -575,23 +587,25 @@ def create_endpoint( :param admin_url: Endpoint admin URL. :param region: Endpoint region. :param enabled: Whether the endpoint is enabled + :returns: A list of identity ``Endpoint`` objects - :raises: OpenStackCloudException if the service cannot be found or if - something goes wrong during the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if the service + cannot be found or if something goes wrong during the OpenStack API + call. """ public_url = kwargs.pop('public_url', None) internal_url = kwargs.pop('internal_url', None) admin_url = kwargs.pop('admin_url', None) if (url or interface) and (public_url or internal_url or admin_url): - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "create_endpoint takes either url and interface OR " "public_url, internal_url, admin_url" ) service = self.get_service(name_or_id=service_name_or_id) if service is None: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "service {service} not found".format( service=service_name_or_id ) @@ -652,8 +666,8 @@ def list_endpoints(self): """List Keystone endpoints. :returns: A list of identity ``Endpoint`` objects - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ return list(self.identity.endpoints()) @@ -678,9 +692,10 @@ def search_endpoints(self, id=None, filters=None): Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :returns: A list of identity ``Endpoint`` objects - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ # NOTE(SamYaple): With keystone v3 we can filter directly via the # the keystone api, but since the return of all the endpoints even in @@ -702,9 +717,10 @@ def delete_endpoint(self, id): """Delete a Keystone endpoint. :param id: ID of the endpoint to delete. + :returns: True if delete succeeded, False otherwise. - :raises: ``OpenStackCloudException`` if something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ endpoint = self.get_endpoint(id=id) if endpoint is None: @@ -725,7 +741,8 @@ def create_domain(self, name, description=None, enabled=True): :param description: A description of the domain. :param enabled: Is the domain enabled or not (default True). :returns: The created identity ``Endpoint`` object. - :raise OpenStackCloudException: if the domain cannot be created. + :raises: :class:`~openstack.exceptions.SDKException` if the domain + cannot be created. """ domain_ref = {'name': name, 'enabled': enabled} if description is not None: @@ -750,16 +767,17 @@ def update_domain( :param enabled: :param name_or_id: Name or unique ID of the domain. :returns: The updated identity ``Domain`` object. - :raise OpenStackCloudException: if the domain cannot be updated + :raises: :class:`~openstack.exceptions.SDKException` if the domain + cannot be updated """ if domain_id is None: if name_or_id is None: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "You must pass either domain_id or name_or_id value" ) dom = self.get_domain(None, name_or_id) if dom is None: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Domain {0} not found for updating".format(name_or_id) ) domain_id = dom['id'] @@ -777,14 +795,15 @@ def delete_domain(self, domain_id=None, name_or_id=None): :param domain_id: ID of the domain to delete. :param name_or_id: Name or unique ID of the domain. + :returns: True if delete succeeded, False otherwise. - :raises: ``OpenStackCloudException`` if something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ try: if domain_id is None: if name_or_id is None: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "You must pass either domain_id or name_or_id value" ) dom = self.get_domain(name_or_id=name_or_id) @@ -808,8 +827,8 @@ def list_domains(self, **filters): """List Keystone domains. :returns: A list of identity ``Domain`` objects. - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ return list(self.identity.domains(**filters)) @@ -835,9 +854,10 @@ def search_domains(self, filters=None, name_or_id=None): Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :returns: a list of identity ``Domain`` objects - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ if filters is None: filters = {} @@ -872,9 +892,10 @@ def get_domain(self, domain_id=None, name_or_id=None, filters=None): Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :returns: an identity ``Domain`` object - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ if domain_id is None: return self.identity.find_domain(name_or_id) @@ -886,9 +907,10 @@ def list_groups(self, **kwargs): """List Keystone groups. :param domain_id: Domain ID. + :returns: A list of identity ``Group`` objects - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ return list(self.identity.groups(**kwargs)) @@ -915,9 +937,10 @@ def search_groups(self, name_or_id=None, filters=None, **kwargs): "[?last_name==`Smith`] | [?other.gender]==`Female`]" :param domain_id: domain id. + :returns: A list of identity ``Group`` objects - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ groups = self.list_groups(**kwargs) return _utils._filter_list(groups, name_or_id, filters) @@ -929,9 +952,10 @@ def get_group(self, name_or_id, filters=None, **kwargs): """Get exactly one Keystone group. :param name_or_id: Name or unique ID of the group(s). + :returns: An identity ``Group`` object - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ return _utils._get_entity(self, 'group', name_or_id, filters, **kwargs) @@ -941,9 +965,10 @@ def create_group(self, name, description, domain=None): :param string name: Group name. :param string description: Group description. :param string domain: Domain name or ID for the group. + :returns: An identity ``Group`` object - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ group_ref = {'name': name} if description: @@ -951,7 +976,7 @@ def create_group(self, name, description, domain=None): if domain: dom = self.get_domain(domain) if not dom: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Creating group {group} failed: Invalid domain " "{domain}".format(group=name, domain=domain) ) @@ -973,13 +998,14 @@ def update_group( :param name_or_id: Name or unique ID of the group. :param name: New group name. :param description: New group description. + :returns: The updated identity ``Group`` object. - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ group = self.identity.find_group(name_or_id, **kwargs) if group is None: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Group {0} not found for updating".format(name_or_id) ) @@ -997,9 +1023,10 @@ def delete_group(self, name_or_id): """Delete a group :param name_or_id: Name or unique ID of the group. + :returns: True if delete succeeded, False otherwise. - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ try: group = self.identity.find_group(name_or_id) @@ -1021,8 +1048,8 @@ def list_roles(self, **kwargs): """List Keystone roles. :returns: A list of identity ``Role`` objects - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ return list(self.identity.roles(**kwargs)) @@ -1047,9 +1074,10 @@ def search_roles(self, name_or_id=None, filters=None): Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :returns: a list of identity ``Role`` objects - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ roles = self.list_roles() return _utils._filter_list(roles, name_or_id, filters) @@ -1061,9 +1089,10 @@ def get_role(self, name_or_id, filters=None, **kwargs): """Get a Keystone role. :param name_or_id: Name or unique ID of the role. + :returns: An identity ``Role`` object if found, else None. - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ return _utils._get_entity(self, 'role', name_or_id, filters, **kwargs) @@ -1120,8 +1149,8 @@ def list_role_assignments(self, filters=None): :returns: A list of indentity :class:`openstack.identity.v3.role_assignment.RoleAssignment` objects - :raises: ``OpenStackCloudException``: if something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ # NOTE(samueldmq): although 'include_names' is a valid query parameter # in the keystone v3 list role assignments API, it would have NO effect @@ -1162,7 +1191,8 @@ def create_role(self, name, **kwargs): :param string name: The name of the role. :param domain_id: domain id (v3) :returns: an identity ``Role`` object - :raise OpenStackCloudException: if the role cannot be created + :raises: :class:`~openstack.exceptions.SDKException` if the role cannot + be created """ kwargs['name'] = name return self.identity.create_role(**kwargs) @@ -1175,7 +1205,8 @@ def update_role(self, name_or_id, name, **kwargs): :param string name: The new role name :param domain_id: domain id :returns: an identity ``Role`` object - :raise OpenStackCloudException: if the role cannot be created + :raises: :class:`~openstack.exceptions.SDKException` if the role cannot + be created """ role = self.get_role(name_or_id, **kwargs) if role is None: @@ -1190,9 +1221,10 @@ def delete_role(self, name_or_id, **kwargs): :param name_or_id: Name or unique ID of the role. :param domain_id: domain id (v3) + :returns: True if delete succeeded, False otherwise. - :raises: ``OpenStackCloudException`` if something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ role = self.get_role(name_or_id, **kwargs) if role is None: @@ -1229,9 +1261,7 @@ def _get_grant_revoke_params( data['role'] = self.identity.find_role(name_or_id=role) if not data['role']: - raise exc.OpenStackCloudException( - 'Role {0} not found.'.format(role) - ) + raise exceptions.SDKException('Role {0} not found.'.format(role)) if user: # use cloud.get_user to save us from bad searching by name @@ -1242,15 +1272,15 @@ def _get_grant_revoke_params( ) if data.get('user') and data.get('group'): - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'Specify either a group or a user, not both' ) if data.get('user') is None and data.get('group') is None: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'Must specify either a user or a group' ) if project is None and domain is None and system is None: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'Must specify either a domain, project or system' ) @@ -1293,8 +1323,8 @@ def grant_role( NOTE: precedence is given first to project, then domain, then system :returns: True if the role is assigned, otherwise False - - :raise OpenStackCloudException: if the role cannot be granted + :raises: :class:`~openstack.exceptions.SDKException` if the role cannot + be granted """ data = self._get_grant_revoke_params( name_or_id, @@ -1401,8 +1431,8 @@ def revoke_role( NOTE: precedence is given first to project, then domain, then system :returns: True if the role is revoke, otherwise False - - :raise OpenStackCloudException: if the role cannot be removed + :raises: :class:`~openstack.exceptions.SDKException` if the role cannot + be removed """ data = self._get_grant_revoke_params( name_or_id, diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py index 97807faa8..73f342b37 100644 --- a/openstack/cloud/_image.py +++ b/openstack/cloud/_image.py @@ -11,7 +11,7 @@ # limitations under the License. from openstack.cloud import _utils -from openstack.cloud import exc +from openstack import exceptions from openstack.image.v2._proxy import Proxy from openstack import utils @@ -104,30 +104,32 @@ def download_image( this or output_path must be specified :param int chunk_size: size in bytes to read from the wire and buffer at one time. Defaults to 1024 * 1024 = 1 MiB + :returns: When output_path and output_file are not given - the bytes comprising the given Image when stream is False, otherwise a :class:`requests.Response` instance. When output_path or output_file are given - an image :class:`~openstack.image.v2.image.Image` instance. - :raises: OpenStackCloudException in the event download_image is called - without exactly one of either output_path or output_file - :raises: OpenStackCloudResourceNotFound if no images are found matching - the name or ID provided + :raises: :class:`~openstack.exceptions.SDKException` in the event + download_image is called without exactly one of either output_path + or output_file + :raises: :class:`~openstack.exceptions.BadRequestException` if no + images are found matching the name or ID provided """ if output_path is None and output_file is None: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'No output specified, an output path or file object' ' is necessary to write the image data to' ) elif output_path is not None and output_file is not None: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'Both an output path and file object were provided,' ' however only one can be used at once' ) image = self.image.find_image(name_or_id) if not image: - raise exc.OpenStackCloudResourceNotFound( + raise exceptions.NotFoundException( "No images with name or ID %s were found" % name_or_id, None ) @@ -167,7 +169,7 @@ def wait_for_image(self, image, timeout=3600): if image['status'] == 'active': return image elif image['status'] == 'error': - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'Image {image} hit error state'.format(image=image_id) ) @@ -186,8 +188,8 @@ def delete_image( :param delete_objects: If True, also deletes uploaded swift objects. :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException if there are problems deleting. + :raises: :class:`~openstack.exceptions.SDKException` if there are + problems deleting. """ image = self.get_image(name_or_id) if not image: @@ -279,7 +281,8 @@ def create_image( If a value is in meta and kwargs, meta wins. :returns: An image :class:`openstack.image.v2.image.Image` object. - :raises: OpenStackCloudException if there are problems uploading + :raises: :class:`~openstack.exceptions.SDKException` if there are + problems uploading """ if volume: image = self.block_storage.create_image( @@ -319,7 +322,7 @@ def create_image( image_obj = self.get_image(image.id) if image_obj and image_obj.status not in ('queued', 'saving'): return image_obj - except exc.OpenStackCloudTimeout: + except exceptions.ResourceTimeout: self.log.debug( "Timeout waiting for image to become ready. Deleting." ) diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index 654dd2dfa..f6718748d 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -35,10 +35,11 @@ def search_networks(self, name_or_id=None, filters=None): :param name_or_id: Name or ID of the desired network. :param filters: A dict containing additional filters to use. e.g. {'router:external': True} + :returns: A list of network ``Network`` objects matching the search criteria. - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ query = {} if name_or_id: @@ -54,10 +55,11 @@ def search_routers(self, name_or_id=None, filters=None): :param name_or_id: Name or ID of the desired router. :param filters: A dict containing additional filters to use. e.g. {'admin_state_up': True} + :returns: A list of network ``Router`` objects matching the search criteria. - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ query = {} if name_or_id: @@ -73,10 +75,11 @@ def search_subnets(self, name_or_id=None, filters=None): :param name_or_id: Name or ID of the desired subnet. :param filters: A dict containing additional filters to use. e.g. {'enable_dhcp': True} + :returns: A list of network ``Subnet`` objects matching the search criteria. - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ query = {} if name_or_id: @@ -92,10 +95,11 @@ def search_ports(self, name_or_id=None, filters=None): :param name_or_id: Name or ID of the desired port. :param filters: A dict containing additional filters to use. e.g. {'device_id': '2711c67a-b4a7-43dd-ace7-6187b791c3f0'} + :returns: A list of network ``Port`` objects matching the search criteria. - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ # If the filter is a string, do not push the filter down to neutron; # get all the ports and filter locally. @@ -208,10 +212,11 @@ def search_qos_policies(self, name_or_id=None, filters=None): :param name_or_id: Name or ID of the desired policy. :param filters: a dict containing additional filters to use. e.g. {'shared': True} + :returns: A list of network ``QosPolicy`` objects matching the search criteria. - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( @@ -467,7 +472,8 @@ def create_network( :param string dns_domain: Specify the DNS domain associated with this network. :returns: The created network ``Network`` object. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ network = { 'name': name, @@ -482,7 +488,7 @@ def create_network( if availability_zone_hints is not None: if not isinstance(availability_zone_hints, list): - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Parameter 'availability_zone_hints' must be a list" ) if not self._has_neutron_extension('network_availability_zone'): @@ -494,7 +500,7 @@ def create_network( if provider: if not isinstance(provider, dict): - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Parameter 'provider' must be a dict" ) # Only pass what we know @@ -515,18 +521,18 @@ def create_network( if port_security_enabled is not None: if not isinstance(port_security_enabled, bool): - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Parameter 'port_security_enabled' must be a bool" ) network['port_security_enabled'] = port_security_enabled if mtu_size: if not isinstance(mtu_size, int): - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Parameter 'mtu_size' must be an integer." ) if not mtu_size >= 68: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Parameter 'mtu_size' must be greater than 67." ) @@ -568,13 +574,15 @@ def update_network(self, name_or_id, **kwargs): :param bool port_security_enabled: Enable or disable port security. :param string dns_domain: Specify the DNS domain associated with this network. + :returns: The updated network ``Network`` object. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ provider = kwargs.pop('provider', None) if provider: if not isinstance(provider, dict): - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Parameter 'provider' must be a dict" ) for key in ('physical_network', 'network_type', 'segmentation_id'): @@ -586,26 +594,24 @@ def update_network(self, name_or_id, **kwargs): if 'port_security_enabled' in kwargs: if not isinstance(kwargs['port_security_enabled'], bool): - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Parameter 'port_security_enabled' must be a bool" ) if 'mtu_size' in kwargs: if not isinstance(kwargs['mtu_size'], int): - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Parameter 'mtu_size' must be an integer." ) if kwargs['mtu_size'] < 68: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Parameter 'mtu_size' must be greater than 67." ) kwargs['mtu'] = kwargs.pop('mtu_size') network = self.get_network(name_or_id) if not network: - raise exc.OpenStackCloudException( - "Network %s not found." % name_or_id - ) + raise exceptions.SDKException("Network %s not found." % name_or_id) network = self.network.update_network(network, **kwargs) @@ -619,8 +625,8 @@ def delete_network(self, name_or_id): :param name_or_id: Name or ID of the network being deleted. :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ network = self.get_network(name_or_id) if not network: @@ -640,13 +646,13 @@ def set_network_quotas(self, name_or_id, **kwargs): :param name_or_id: project name or id :param kwargs: key/value pairs of quota name and quota value - :raises: OpenStackCloudException if the resource to set the - quota does not exist. + :raises: :class:`~openstack.exceptions.SDKException` if the resource to + set the quota does not exist. """ proj = self.get_project(name_or_id) if not proj: - raise exc.OpenStackCloudException("project does not exist") + raise exceptions.SDKException("project does not exist") self.network.update_quota(proj.id, **kwargs) @@ -656,8 +662,10 @@ def get_network_quotas(self, name_or_id, details=False): :param name_or_id: project name or id :param details: if set to True it will return details about usage of quotas by given project - :raises: OpenStackCloudException if it's not a valid project + :returns: A network ``Quota`` object if found, else None. + :raises: :class:`~openstack.exceptions.SDKException` if it's not a + valid project """ proj = self.identity.find_project(name_or_id, ignore_missing=False) return self.network.get_quota(proj.id, details) @@ -673,14 +681,14 @@ def delete_network_quotas(self, name_or_id): """Delete network quotas for a project :param name_or_id: project name or id - :raises: OpenStackCloudException if it's not a valid project or the - network client call failed :returns: dict with the quotas + :raises: :class:`~openstack.exceptions.SDKException` if it's not a + valid project or the network client call failed """ proj = self.get_project(name_or_id) if not proj: - raise exc.OpenStackCloudException("project does not exist") + raise exceptions.SDKException("project does not exist") self.network.delete_quota(proj.id) @_utils.valid_kwargs( @@ -1348,8 +1356,10 @@ def create_qos_policy(self, **kwargs): :param bool default: Set the QoS policy as default for project. :param string project_id: Specify the project ID this QoS policy will be created on (admin-only). + :returns: The created network ``QosPolicy`` object. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( @@ -1380,8 +1390,10 @@ def update_qos_policy(self, name_or_id, **kwargs): :param bool shared: If True, the QoS policy will be set as shared. :param bool default: If True, the QoS policy will be set as default for project. + :returns: The updated network ``QosPolicyRule`` object. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( @@ -1404,7 +1416,7 @@ def update_qos_policy(self, name_or_id, **kwargs): curr_policy = self.network.find_qos_policy(name_or_id) if not curr_policy: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "QoS policy %s not found." % name_or_id ) @@ -1416,8 +1428,8 @@ def delete_qos_policy(self, name_or_id): :param name_or_id: Name or ID of the policy being deleted. :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( @@ -1446,10 +1458,11 @@ def search_qos_bandwidth_limit_rules( :param string rule_id: ID of searched rule. :param filters: a dict containing additional filters to use. e.g. {'max_kbps': 1000} + :returns: A list of network ``QoSBandwidthLimitRule`` objects matching the search criteria. - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ rules = self.list_qos_bandwidth_limit_rules(policy_name_or_id, filters) return _utils._filter_list(rules, rule_id, filters) @@ -1461,8 +1474,8 @@ def list_qos_bandwidth_limit_rules(self, policy_name_or_id, filters=None): from rules should be listed. :param filters: (optional) A dict of filter conditions to push down :returns: A list of network ``QoSBandwidthLimitRule`` objects. - :raises: ``OpenStackCloudResourceNotFound`` if QoS policy will not be - found. + :raises: ``:class:`~openstack.exceptions.BadRequestException``` if QoS + policy will not be found. """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( @@ -1471,7 +1484,7 @@ def list_qos_bandwidth_limit_rules(self, policy_name_or_id, filters=None): policy = self.network.find_qos_policy(policy_name_or_id) if not policy: - raise exc.OpenStackCloudResourceNotFound( + raise exceptions.NotFoundException( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id ) @@ -1503,7 +1516,7 @@ def get_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): policy = self.network.find_qos_policy(policy_name_or_id) if not policy: - raise exc.OpenStackCloudResourceNotFound( + raise exceptions.NotFoundException( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id ) @@ -1527,8 +1540,10 @@ def create_qos_bandwidth_limit_rule( :param int max_burst_kbps: Maximum burst value (in kilobits). :param string direction: Ingress or egress. The direction in which the traffic will be limited. + :returns: The created network ``QoSBandwidthLimitRule`` object. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( @@ -1537,7 +1552,7 @@ def create_qos_bandwidth_limit_rule( policy = self.network.find_qos_policy(policy_name_or_id) if not policy: - raise exc.OpenStackCloudResourceNotFound( + raise exceptions.NotFoundException( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id ) @@ -1569,8 +1584,10 @@ def update_qos_bandwidth_limit_rule( :param int max_burst_kbps: Maximum burst value (in kilobits). :param string direction: Ingress or egress. The direction in which the traffic will be limited. + :returns: The updated network ``QoSBandwidthLimitRule`` object. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( @@ -1581,7 +1598,7 @@ def update_qos_bandwidth_limit_rule( policy_name_or_id, ignore_missing=True ) if not policy: - raise exc.OpenStackCloudResourceNotFound( + raise exceptions.NotFoundException( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id ) @@ -1603,7 +1620,7 @@ def update_qos_bandwidth_limit_rule( qos_rule=rule_id, qos_policy=policy ) if not curr_rule: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "QoS bandwidth_limit_rule {rule_id} not found in policy " "{policy_id}".format(rule_id=rule_id, policy_id=policy['id']) ) @@ -1619,7 +1636,8 @@ def delete_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): rule is associated. :param string rule_id: ID of rule to update. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( @@ -1628,7 +1646,7 @@ def delete_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): policy = self.network.find_qos_policy(policy_name_or_id) if not policy: - raise exc.OpenStackCloudResourceNotFound( + raise exceptions.NotFoundException( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id ) @@ -1663,10 +1681,11 @@ def search_qos_dscp_marking_rules( :param string rule_id: ID of searched rule. :param filters: a dict containing additional filters to use. e.g. {'dscp_mark': 32} + :returns: A list of network ``QoSDSCPMarkingRule`` objects matching the search criteria. - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ rules = self.list_qos_dscp_marking_rules(policy_name_or_id, filters) return _utils._filter_list(rules, rule_id, filters) @@ -1678,8 +1697,8 @@ def list_qos_dscp_marking_rules(self, policy_name_or_id, filters=None): from rules should be listed. :param filters: (optional) A dict of filter conditions to push down :returns: A list of network ``QoSDSCPMarkingRule`` objects. - :raises: ``OpenStackCloudResourceNotFound`` if QoS policy will not be - found. + :raises: ``:class:`~openstack.exceptions.BadRequestException``` if QoS + policy will not be found. """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( @@ -1690,7 +1709,7 @@ def list_qos_dscp_marking_rules(self, policy_name_or_id, filters=None): policy_name_or_id, ignore_missing=True ) if not policy: - raise exc.OpenStackCloudResourceNotFound( + raise exceptions.NotFoundException( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id ) @@ -1717,7 +1736,7 @@ def get_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): policy = self.network.find_qos_policy(policy_name_or_id) if not policy: - raise exc.OpenStackCloudResourceNotFound( + raise exceptions.NotFoundException( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id ) @@ -1735,8 +1754,10 @@ def create_qos_dscp_marking_rule( :param string policy_name_or_id: Name or ID of the QoS policy to which rule should be associated. :param int dscp_mark: DSCP mark value + :returns: The created network ``QoSDSCPMarkingRule`` object. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( @@ -1745,7 +1766,7 @@ def create_qos_dscp_marking_rule( policy = self.network.find_qos_policy(policy_name_or_id) if not policy: - raise exc.OpenStackCloudResourceNotFound( + raise exceptions.NotFoundException( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id ) @@ -1765,8 +1786,10 @@ def update_qos_dscp_marking_rule( rule is associated. :param string rule_id: ID of rule to update. :param int dscp_mark: DSCP mark value + :returns: The updated network ``QoSDSCPMarkingRule`` object. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( @@ -1775,7 +1798,7 @@ def update_qos_dscp_marking_rule( policy = self.network.find_qos_policy(policy_name_or_id) if not policy: - raise exc.OpenStackCloudResourceNotFound( + raise exceptions.NotFoundException( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id ) @@ -1787,7 +1810,7 @@ def update_qos_dscp_marking_rule( curr_rule = self.network.get_qos_dscp_marking_rule(rule_id, policy) if not curr_rule: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "QoS dscp_marking_rule {rule_id} not found in policy " "{policy_id}".format(rule_id=rule_id, policy_id=policy['id']) ) @@ -1803,7 +1826,8 @@ def delete_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): rule is associated. :param string rule_id: ID of rule to update. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( @@ -1812,7 +1836,7 @@ def delete_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): policy = self.network.find_qos_policy(policy_name_or_id) if not policy: - raise exc.OpenStackCloudResourceNotFound( + raise exceptions.NotFoundException( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id ) @@ -1847,10 +1871,11 @@ def search_qos_minimum_bandwidth_rules( :param string rule_id: ID of searched rule. :param filters: a dict containing additional filters to use. e.g. {'min_kbps': 1000} + :returns: A list of network ``QoSMinimumBandwidthRule`` objects matching the search criteria. - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ rules = self.list_qos_minimum_bandwidth_rules( policy_name_or_id, filters @@ -1866,8 +1891,8 @@ def list_qos_minimum_bandwidth_rules( from rules should be listed. :param filters: (optional) A dict of filter conditions to push down :returns: A list of network ``QoSMinimumBandwidthRule`` objects. - :raises: ``OpenStackCloudResourceNotFound`` if QoS policy will not be - found. + :raises: ``:class:`~openstack.exceptions.BadRequestException``` if QoS + policy will not be found. """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( @@ -1876,7 +1901,7 @@ def list_qos_minimum_bandwidth_rules( policy = self.network.find_qos_policy(policy_name_or_id) if not policy: - raise exc.OpenStackCloudResourceNotFound( + raise exceptions.NotFoundException( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id ) @@ -1906,7 +1931,7 @@ def get_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): policy = self.network.find_qos_policy(policy_name_or_id) if not policy: - raise exc.OpenStackCloudResourceNotFound( + raise exceptions.NotFoundException( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id ) @@ -1928,8 +1953,10 @@ def create_qos_minimum_bandwidth_rule( :param int min_kbps: Minimum bandwidth value (in kilobits per second). :param string direction: Ingress or egress. The direction in which the traffic will be available. + :returns: The created network ``QoSMinimumBandwidthRule`` object. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( @@ -1938,7 +1965,7 @@ def create_qos_minimum_bandwidth_rule( policy = self.network.find_qos_policy(policy_name_or_id) if not policy: - raise exc.OpenStackCloudResourceNotFound( + raise exceptions.NotFoundException( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id ) @@ -1960,8 +1987,10 @@ def update_qos_minimum_bandwidth_rule( :param int min_kbps: Minimum bandwidth value (in kilobits per second). :param string direction: Ingress or egress. The direction in which the traffic will be available. + :returns: The updated network ``QoSMinimumBandwidthRule`` object. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( @@ -1970,7 +1999,7 @@ def update_qos_minimum_bandwidth_rule( policy = self.network.find_qos_policy(policy_name_or_id) if not policy: - raise exc.OpenStackCloudResourceNotFound( + raise exceptions.NotFoundException( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id ) @@ -1984,7 +2013,7 @@ def update_qos_minimum_bandwidth_rule( rule_id, policy ) if not curr_rule: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "QoS minimum_bandwidth_rule {rule_id} not found in policy " "{policy_id}".format(rule_id=rule_id, policy_id=policy['id']) ) @@ -2000,7 +2029,8 @@ def delete_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): rule is associated. :param string rule_id: ID of rule to delete. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ if not self._has_neutron_extension('qos'): raise exc.OpenStackCloudUnavailableExtension( @@ -2009,7 +2039,7 @@ def delete_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): policy = self.network.find_qos_policy(policy_name_or_id) if not policy: - raise exc.OpenStackCloudResourceNotFound( + raise exceptions.NotFoundException( "QoS policy {name_or_id} not Found.".format( name_or_id=policy_name_or_id ) @@ -2039,8 +2069,10 @@ def add_router_interface(self, router, subnet_id=None, port_id=None): :param dict router: The dict object of the router being changed :param string subnet_id: The ID of the subnet to use for the interface :param string port_id: The ID of the port to use for the interface + :returns: The raw response body from the request. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ return self.network.add_interface_to_router( router=router, subnet_id=subnet_id, port_id=port_id @@ -2060,8 +2092,8 @@ def remove_router_interface(self, router, subnet_id=None, port_id=None): :param string port_id: The ID of the port to use for the interface :returns: None on success - - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ if not subnet_id and not port_id: raise ValueError( @@ -2141,10 +2173,12 @@ def create_router( ] :param string project_id: Project ID for the router. - :param types.ListType availability_zone_hints: - A list of availability zone hints. + :param types.ListType availability_zone_hints: A list of availability + zone hints. + :returns: The created network ``Router`` object. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ router = {'admin_state_up': admin_state_up} if project_id is not None: @@ -2158,7 +2192,7 @@ def create_router( router['external_gateway_info'] = ext_gw_info if availability_zone_hints is not None: if not isinstance(availability_zone_hints, list): - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Parameter 'availability_zone_hints' must be a list" ) if not self._has_neutron_extension('router_availability_zone'): @@ -2213,7 +2247,8 @@ def update_router( ] :returns: The updated network ``Router`` object. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ router = {} if name: @@ -2240,9 +2275,7 @@ def update_router( curr_router = self.get_router(name_or_id) if not curr_router: - raise exc.OpenStackCloudException( - "Router %s not found." % name_or_id - ) + raise exceptions.SDKException("Router %s not found." % name_or_id) return self.network.update_router(curr_router, **router) @@ -2256,8 +2289,8 @@ def delete_router(self, name_or_id): :param name_or_id: Name or ID of the router being deleted. :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ router = self.network.find_router(name_or_id, ignore_missing=True) if not router: @@ -2352,8 +2385,10 @@ def create_subnet( ``use_default_subnetpool`` and ``subnetpool_name_or_id`` may be specified at the same time. :param kwargs: Key value pairs to be passed to the Neutron API. + :returns: The created network ``Subnet`` object. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ if tenant_id is not None: @@ -2363,28 +2398,28 @@ def create_subnet( network = self.get_network(network_name_or_id, filters) if not network: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Network %s not found." % network_name_or_id ) if disable_gateway_ip and gateway_ip: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'arg:disable_gateway_ip is not allowed with arg:gateway_ip' ) uses_subnetpool = use_default_subnetpool or subnetpool_name_or_id if not cidr and not uses_subnetpool: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'arg:cidr is required when a subnetpool is not used' ) if cidr and uses_subnetpool: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'arg:cidr and subnetpool may not be used at the same time' ) if use_default_subnetpool and subnetpool_name_or_id: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'arg:use_default_subnetpool and arg:subnetpool_id may not be ' 'used at the same time' ) @@ -2393,7 +2428,7 @@ def create_subnet( if subnetpool_name_or_id: subnetpool = self.get_subnetpool(subnetpool_name_or_id) if not subnetpool: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Subnetpool %s not found." % subnetpool_name_or_id ) @@ -2402,9 +2437,7 @@ def create_subnet( try: ip_version = int(ip_version) except ValueError: - raise exc.OpenStackCloudException( - 'ip_version must be an integer' - ) + raise exceptions.SDKException('ip_version must be an integer') # The body of the neutron message for the subnet we wish to create. # This includes attributes that are required or have defaults. @@ -2457,8 +2490,8 @@ def delete_subnet(self, name_or_id): :param name_or_id: Name or ID of the subnet being deleted. :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ subnet = self.network.find_subnet(name_or_id, ignore_missing=True) if not subnet: @@ -2522,7 +2555,8 @@ def update_subnet( ] :returns: The updated network ``Subnet`` object. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ subnet = {} if subnet_name: @@ -2545,15 +2579,13 @@ def update_subnet( return if disable_gateway_ip and gateway_ip: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'arg:disable_gateway_ip is not allowed with arg:gateway_ip' ) curr_subnet = self.get_subnet(name_or_id) if not curr_subnet: - raise exc.OpenStackCloudException( - "Subnet %s not found." % name_or_id - ) + raise exceptions.SDKException("Subnet %s not found." % name_or_id) return self.network.update_subnet(curr_subnet, **subnet) @@ -2647,8 +2679,10 @@ def create_port(self, network_id, **kwargs): be propagated. (Optional) :param mac_learning_enabled: If mac learning should be enabled on the port. (Optional) + :returns: The created network ``Port`` object. - :raises: ``OpenStackCloudException`` on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ kwargs['network_id'] = network_id @@ -2721,12 +2755,14 @@ def update_port(self, name_or_id, **kwargs): :param port_security_enabled: The security port state created on the network. (Optional) :param qos_policy_id: The ID of the QoS policy to apply for port. + :returns: The updated network ``Port`` object. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ port = self.get_port(name_or_id=name_or_id) if port is None: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "failed to find port '{port}'".format(port=name_or_id) ) @@ -2738,8 +2774,8 @@ def delete_port(self, name_or_id): :param name_or_id: ID or name of the port to delete. :returns: True if delete succeeded, False otherwise. - - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ port = self.network.find_port(name_or_id) diff --git a/openstack/cloud/_network_common.py b/openstack/cloud/_network_common.py index 4a9f31c65..bf405e9d2 100644 --- a/openstack/cloud/_network_common.py +++ b/openstack/cloud/_network_common.py @@ -12,7 +12,7 @@ import threading -from openstack.cloud import exc +from openstack import exceptions class NetworkCommonCloudMixin: @@ -81,7 +81,7 @@ def _set_interesting_networks(self): # though, that's fine, clearly the neutron introspection is # not going to work. all_networks = self.list_networks() - except exc.OpenStackCloudException: + except exceptions.SDKException: self._network_list_stamp = True return @@ -145,7 +145,7 @@ def _set_interesting_networks(self): # External Floating IPv4 networks if self._nat_source in (network['name'], network['id']): if nat_source: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'Multiple networks were found matching' ' {nat_net} which is the network configured' ' to be the NAT source. Please check your' @@ -163,7 +163,7 @@ def _set_interesting_networks(self): # NAT Destination if self._nat_destination in (network['name'], network['id']): if nat_destination: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'Multiple networks were found matching' ' {nat_net} which is the network configured' ' to be the NAT destination. Please check your' @@ -180,7 +180,7 @@ def _set_interesting_networks(self): if all_subnets is None: try: all_subnets = self.list_subnets() - except exc.OpenStackCloudException: + except exceptions.SDKException: # Thanks Rackspace broken neutron all_subnets = [] @@ -198,7 +198,7 @@ def _set_interesting_networks(self): # Default network if self._default_network in (network['name'], network['id']): if default_network: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'Multiple networks were found matching' ' {default_net} which is the network' ' configured to be the default interface' @@ -212,7 +212,7 @@ def _set_interesting_networks(self): # Validate config vs. reality for net_name in self._external_ipv4_names: if net_name not in [net['name'] for net in external_ipv4_networks]: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Networks: {network} was provided for external IPv4" " access and those networks could not be found".format( network=net_name @@ -221,7 +221,7 @@ def _set_interesting_networks(self): for net_name in self._internal_ipv4_names: if net_name not in [net['name'] for net in internal_ipv4_networks]: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Networks: {network} was provided for internal IPv4" " access and those networks could not be found".format( network=net_name @@ -230,7 +230,7 @@ def _set_interesting_networks(self): for net_name in self._external_ipv6_names: if net_name not in [net['name'] for net in external_ipv6_networks]: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Networks: {network} was provided for external IPv6" " access and those networks could not be found".format( network=net_name @@ -239,7 +239,7 @@ def _set_interesting_networks(self): for net_name in self._internal_ipv6_names: if net_name not in [net['name'] for net in internal_ipv6_networks]: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Networks: {network} was provided for internal IPv6" " access and those networks could not be found".format( network=net_name @@ -247,21 +247,21 @@ def _set_interesting_networks(self): ) if self._nat_destination and not nat_destination: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'Network {network} was configured to be the' ' destination for inbound NAT but it could not be' ' found'.format(network=self._nat_destination) ) if self._nat_source and not nat_source: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'Network {network} was configured to be the' ' source for inbound NAT but it could not be' ' found'.format(network=self._nat_source) ) if self._default_network and not default_network: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'Network {network} was configured to be the' ' default network interface but it could not be' ' found'.format(network=self._default_network) diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index a80df2bbf..c704c0fc1 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -16,7 +16,6 @@ import keystoneauth1.exceptions from openstack.cloud import _utils -from openstack.cloud import exc from openstack import exceptions from openstack.object_store.v1._proxy import Proxy @@ -42,7 +41,8 @@ def list_containers(self, full_listing=True, prefix=None): :param prefix: Only objects with this prefix will be returned. (optional) :returns: A list of object store ``Container`` objects. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ return list(self.object_store.containers(prefix=prefix)) @@ -57,8 +57,8 @@ def search_containers(self, name=None, filters=None): :returns: A list of object store ``Container`` objects matching the search criteria. - :raises: ``OpenStackCloudException``: If something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException`: If something goes + wrong during the OpenStack API call. """ containers = self.list_containers() return _utils._filter_list(containers, name, filters) @@ -112,7 +112,7 @@ def delete_container(self, name): except exceptions.NotFoundException: return False except exceptions.ConflictException: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( 'Attempt to delete container {container} failed. The' ' container is not empty. Please delete the objects' ' inside it before deleting the container'.format( @@ -140,7 +140,7 @@ def set_container_access(self, name, access, refresh=False): :param refresh: Flag to trigger refresh of the container properties """ if access not in OBJECT_CONTAINER_ACLS: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Invalid container access specified: %s. Must be one of %s" % (access, list(OBJECT_CONTAINER_ACLS.keys())) ) @@ -153,13 +153,12 @@ def get_container_access(self, name): :param str name: Name of the container. :returns: The contol list for the container. - :raises: :class:`~openstack.exceptions.OpenStackCloudException` if the - container was not found or container access could not be - determined. + :raises: :class:`~openstack.exceptions.SDKException` if the container + was not found or container access could not be determined. """ container = self.get_container(name, skip_cache=True) if not container: - raise exc.OpenStackCloudException("Container not found: %s" % name) + raise exceptions.SDKException("Container not found: %s" % name) acl = container.read_ACL or '' for key, value in OBJECT_CONTAINER_ACLS.items(): # Convert to string for the comparison because swiftclient @@ -167,7 +166,7 @@ def get_container_access(self, name): # on bytes doesn't work like you'd think if str(acl) == str(value): return key - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Could not determine container access for ACL: %s." % acl ) @@ -281,8 +280,10 @@ def create_object( uploads of identical data. (optional, defaults to True) :param metadata: This dict will get changed into headers that set metadata of the object + :returns: The created object store ``Object`` object. - :raises: ``OpenStackCloudException`` on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ return self.object_store.create_object( container, @@ -306,8 +307,10 @@ def update_object(self, container, name, metadata=None, **headers): metadata of the object :param headers: These will be passed through to the object update API as HTTP Headers. + :returns: None - :raises: ``OpenStackCloudException`` on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ meta = metadata.copy() or {} meta.update(**headers) @@ -320,8 +323,10 @@ def list_objects(self, container, full_listing=True, prefix=None): :param full_listing: Ignored. Present for backwards compat :param prefix: Only objects with this prefix will be returned. (optional) + :returns: A list of object store ``Object`` objects. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ return list( self.object_store.objects(container=container, prefix=prefix) @@ -338,8 +343,8 @@ def search_objects(self, container, name=None, filters=None): :returns: A list of object store ``Object`` objects matching the search criteria. - :raises: ``OpenStackCloudException``: If something goes wrong during - the OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException`: If something goes + wrong during the OpenStack API call. """ objects = self.list_objects(container) return _utils._filter_list(objects, name, filters) @@ -351,8 +356,10 @@ def delete_object(self, container, name, meta=None): :param string name: Name of the object to delete. :param dict meta: Metadata for the object in question. (optional, will be fetched if not provided) + :returns: True if delete succeeded, False if the object was not found. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ try: self.object_store.delete_object( @@ -404,8 +411,10 @@ def get_object_raw(self, container, obj, query_string=None, stream=False): :param string query_string: Query args for uri. (delimiter, prefix, etc.) :param bool stream: Whether to stream the response or not. + :returns: A `requests.Response` - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ endpoint = self._get_object_endpoint(container, obj, query_string) return self.object_store.get(endpoint, stream=stream) @@ -437,9 +446,11 @@ def stream_object( etc.) :param int resp_chunk_size: Chunk size of data to read. Only used if the results are + :returns: An iterator over the content or None if the object is not found. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ try: for ret in self.object_store.stream_object( @@ -471,9 +482,11 @@ def get_object( contents. If this option is given, body in the return tuple will be None. outfile can either be a file path given as a string, or a File like object. + :returns: Tuple (headers, body) of the object, or None if the object is not found (404). - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ try: obj = self.object_store.get_object( diff --git a/openstack/cloud/_orchestration.py b/openstack/cloud/_orchestration.py index fa79872b2..0d9e7d7f4 100644 --- a/openstack/cloud/_orchestration.py +++ b/openstack/cloud/_orchestration.py @@ -11,7 +11,7 @@ # limitations under the License. from openstack.cloud import _utils -from openstack.cloud import exc +from openstack import exceptions from openstack.orchestration.util import event_utils from openstack.orchestration.v1._proxy import Proxy @@ -67,9 +67,8 @@ def create_stack( specified. :returns: a dict containing the stack description - - :raises: ``OpenStackCloudException`` if something goes wrong during - the OpenStack API call + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call """ params = dict( tags=tags, @@ -124,9 +123,8 @@ def update_stack( specified. :returns: a dict containing the stack description - - :raises: ``OpenStackCloudException`` if something goes wrong during - the OpenStack API calls + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API calls """ params = dict( tags=tags, @@ -166,9 +164,8 @@ def delete_stack(self, name_or_id, wait=False): :param boolean wait: Whether to wait for the delete to finish :returns: True if delete succeeded, False if the stack was not found. - - :raises: ``OpenStackCloudException`` if something goes wrong during - the OpenStack API call + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call """ stack = self.get_stack(name_or_id, resolve_outputs=False) if stack is None: @@ -189,11 +186,11 @@ def delete_stack(self, name_or_id, wait=False): event_utils.poll_for_events( self, stack_name=name_or_id, action='DELETE', marker=marker ) - except exc.OpenStackCloudHTTPError: + except exceptions.HttpException: pass stack = self.get_stack(name_or_id, resolve_outputs=False) if stack and stack['stack_status'] == 'DELETE_FAILED': - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Failed to delete stack {id}: {reason}".format( id=name_or_id, reason=stack['stack_status_reason'] ) @@ -210,9 +207,8 @@ def search_stacks(self, name_or_id=None, filters=None): :returns: a list of ``openstack.orchestration.v1.stack.Stack`` containing the stack description. - - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ stacks = self.list_stacks() return _utils._filter_list(stacks, name_or_id, filters) @@ -221,11 +217,11 @@ def list_stacks(self, **query): """List all stacks. :param dict query: Query parameters to limit stacks. + :returns: a list of :class:`openstack.orchestration.v1.stack.Stack` objects containing the stack description. - - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call. """ return list(self.orchestration.stacks(**query)) @@ -240,9 +236,9 @@ def get_stack(self, name_or_id, filters=None, resolve_outputs=True): :returns: a :class:`openstack.orchestration.v1.stack.Stack` containing the stack description - - :raises: ``OpenStackCloudException`` if something goes wrong during the - OpenStack API call or if multiple matches are found. + :raises: :class:`~openstack.exceptions.SDKException` if something goes + wrong during the OpenStack API call or if multiple matches are + found. """ def _search_one_stack(name_or_id=None, filters=None): @@ -256,7 +252,7 @@ def _search_one_stack(name_or_id=None, filters=None): ) if stack.status == 'DELETE_COMPLETE': return [] - except exc.OpenStackCloudURINotFound: + except exceptions.NotFoundException: return [] return _utils._filter_list([stack], name_or_id, filters) diff --git a/openstack/cloud/_security_group.py b/openstack/cloud/_security_group.py index ecbdb4246..d6d70a34f 100644 --- a/openstack/cloud/_security_group.py +++ b/openstack/cloud/_security_group.py @@ -128,7 +128,8 @@ def create_security_group( :returns: A ``openstack.network.v2.security_group.SecurityGroup`` representing the new security group. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. :raises: OpenStackCloudUnavailableFeature if security groups are not supported on this cloud. """ @@ -165,7 +166,8 @@ def delete_security_group(self, name_or_id): :returns: True if delete succeeded, False otherwise. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. :raises: OpenStackCloudUnavailableFeature if security groups are not supported on this cloud. """ @@ -208,8 +210,8 @@ def update_security_group(self, name_or_id, **kwargs): :returns: A ``openstack.network.v2.security_group.SecurityGroup`` describing the updated security group. - - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ # Security groups not supported if not self._has_secgroups(): @@ -220,7 +222,7 @@ def update_security_group(self, name_or_id, **kwargs): group = self.get_security_group(name_or_id) if group is None: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Security group %s not found." % name_or_id ) @@ -298,10 +300,11 @@ def create_security_group_rule( on (admin-only). :param string description: Description of the rule, max 255 characters. + :returns: A ``openstack.network.v2.security_group.SecurityGroup`` representing the new security group rule. - - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. """ # Security groups not supported if not self._has_secgroups(): @@ -311,7 +314,7 @@ def create_security_group_rule( secgroup = self.get_security_group(secgroup_name_or_id) if not secgroup: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Security group %s not found." % secgroup_name_or_id ) @@ -341,15 +344,13 @@ def create_security_group_rule( else: # NOTE: Neutron accepts None for protocol. Nova does not. if protocol is None: - raise exc.OpenStackCloudException('Protocol must be specified') + raise exceptions.SDKException('Protocol must be specified') if direction == 'egress': self.log.debug( 'Rule creation failed: Nova does not support egress rules' ) - raise exc.OpenStackCloudException( - 'No support for egress rules' - ) + raise exceptions.SDKException('No support for egress rules') # NOTE: Neutron accepts None for ports, but Nova requires -1 # as the equivalent value for ICMP. @@ -399,7 +400,8 @@ def delete_security_group_rule(self, rule_id): :returns: True if delete succeeded, False otherwise. - :raises: OpenStackCloudException on operation error. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. :raises: OpenStackCloudUnavailableFeature if security groups are not supported on this cloud. """ @@ -422,7 +424,7 @@ def delete_security_group_rule(self, rule_id): '/os-security-group-rules/{id}'.format(id=rule_id) ) ) - except exc.OpenStackCloudResourceNotFound: + except exceptions.NotFoundException: return False return True diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index 6da820f5e..232a12bfe 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -23,7 +23,7 @@ import netifaces from openstack import _log -from openstack.cloud import exc +from openstack import exceptions def _dictify_resource(resource): @@ -177,7 +177,7 @@ def _get_entity(cloud, resource, name_or_id, filters, **kwargs): entities = search(name_or_id, filters, **kwargs) if entities: if len(entities) > 1: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Multiple matches found for %s" % name_or_id ) return entities[0] @@ -231,24 +231,24 @@ def openstacksdk_exceptions(error_message=None): """Context manager for dealing with openstack exceptions. :param string error_message: String to use for the exception message - content on non-OpenStackCloudExceptions. + content on non-SDKException exception. - Useful for avoiding wrapping OpenStackCloudException exceptions + Useful for avoiding wrapping SDKException exceptions within themselves. Code called from within the context may throw such exceptions without having to catch and reraise them. - Non-OpenStackCloudException exceptions thrown within the context will + Non-SDKException exceptions thrown within the context will be wrapped and the exception message will be appended to the given error message. """ try: yield - except exc.OpenStackCloudException: + except exceptions.SDKException: raise except Exception as e: if error_message is None: error_message = str(e) - raise exc.OpenStackCloudException(error_message) + raise exceptions.SDKException(error_message) def safe_dict_min(key, data): @@ -273,7 +273,7 @@ def safe_dict_min(key, data): try: val = int(d[key]) except ValueError: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Search for minimum value failed. " "Value for {key} is not an integer: {value}".format( key=key, value=d[key] @@ -306,7 +306,7 @@ def safe_dict_max(key, data): try: val = int(d[key]) except ValueError: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Search for maximum value failed. " "Value for {key} is not an integer: {value}".format( key=key, value=d[key] @@ -360,7 +360,8 @@ def range_filter(data, key, range_exp): :param string range_exp: The expression describing the range of values. :returns: A list subset of the original data set. - :raises: OpenStackCloudException on invalid range expressions. + :raises: :class:`~openstack.exceptions.SDKException` on invalid range + expressions. """ filtered = [] range_exp = str(range_exp).upper() @@ -388,7 +389,7 @@ def range_filter(data, key, range_exp): # If parsing the range fails, it must be a bad value. if val_range is None: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Invalid range value: {value}".format(value=range_exp) ) diff --git a/openstack/cloud/cmd/inventory.py b/openstack/cloud/cmd/inventory.py index ce6cbeb97..06e027cba 100644 --- a/openstack/cloud/cmd/inventory.py +++ b/openstack/cloud/cmd/inventory.py @@ -21,6 +21,7 @@ import openstack.cloud import openstack.cloud.inventory +from openstack import exceptions def output_format_dict(data, use_yaml): @@ -76,7 +77,7 @@ def main(): elif args.host: output = inventory.get_host(args.host) print(output_format_dict(output, args.yaml)) - except openstack.cloud.OpenStackCloudException as e: + except exceptions.SDKException as e: sys.stderr.write(e.message + '\n') sys.exit(1) sys.exit(0) diff --git a/openstack/cloud/exc.py b/openstack/cloud/exc.py index 3d75d4878..cc831f402 100644 --- a/openstack/cloud/exc.py +++ b/openstack/cloud/exc.py @@ -15,7 +15,6 @@ from openstack import exceptions OpenStackCloudException = exceptions.SDKException -OpenStackCloudTimeout = exceptions.ResourceTimeout class OpenStackCloudCreateException(OpenStackCloudException): @@ -39,6 +38,7 @@ class OpenStackCloudUnavailableFeature(OpenStackCloudException): # Backwards compat +OpenStackCloudTimeout = exceptions.ResourceTimeout OpenStackCloudHTTPError = exceptions.HttpException OpenStackCloudBadRequest = exceptions.BadRequestException OpenStackCloudURINotFound = exceptions.NotFoundException diff --git a/openstack/cloud/inventory.py b/openstack/cloud/inventory.py index 7b723ff94..2c4909a6b 100644 --- a/openstack/cloud/inventory.py +++ b/openstack/cloud/inventory.py @@ -74,7 +74,7 @@ def list_hosts( detailed=expand, all_projects=all_projects ): hostvars.append(server) - except exceptions.OpenStackCloudException: + except exceptions.SDKException: # Don't fail on one particular cloud as others may work if fail_on_cloud_config: raise diff --git a/openstack/cloud/meta.py b/openstack/cloud/meta.py index 8e57d8964..184c1b984 100644 --- a/openstack/cloud/meta.py +++ b/openstack/cloud/meta.py @@ -16,7 +16,7 @@ import socket from openstack import _log -from openstack.cloud import exc +from openstack import exceptions from openstack import utils @@ -452,7 +452,7 @@ def _get_supplemental_addresses(cloud, server): server['addresses'][fixed_net].append( _make_address_dict(fip, port) ) - except exc.OpenStackCloudException: + except exceptions.SDKException: # If something goes wrong with a cloud call, that's cool - this is # an attempt to provide additional data and should not block forward # progress @@ -498,7 +498,7 @@ def add_server_interfaces(cloud, server): def expand_server_security_groups(cloud, server): try: groups = cloud.list_server_security_groups(server) - except exc.OpenStackCloudException: + except exceptions.SDKException: groups = [] server['security_groups'] = groups or [] @@ -550,7 +550,7 @@ def get_hostvars_from_server(cloud, server, mounts=None): # Make things easier to consume elsewhere volume['device'] = volume['attachments'][0]['device'] volumes.append(volume) - except exc.OpenStackCloudException: + except exceptions.SDKException: pass server_vars['volumes'] = volumes if mounts: diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 7d847dade..b0cccf45e 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -24,7 +24,6 @@ from openstack import _log from openstack.cloud import _object_store from openstack.cloud import _utils -from openstack.cloud import exc from openstack.cloud import meta import openstack.config from openstack.config import cloud_region as cloud_region_mod @@ -445,7 +444,8 @@ def range_search(self, data, filters): {"vcpus": "<=5", "ram": "<=2048", "disk": "1"} :returns: A list subset of the original data set. - :raises: OpenStackCloudException on invalid range expressions. + :raises: :class:`~openstack.exceptions.SDKException` on invalid range + expressions. """ filtered = [] @@ -491,10 +491,10 @@ def get_session_endpoint(self, service_key, **kwargs): "Endpoint not found in %s cloud: %s", self.name, str(e) ) endpoint = None - except exc.OpenStackCloudException: + except exceptions.SDKException: raise except Exception as e: - raise exc.OpenStackCloudException( + raise exceptions.SDKException( "Error getting {service} endpoint on {cloud}:{region}:" " {error}".format( service=service_key, @@ -525,7 +525,7 @@ def has_service(self, service_key, version=None): kwargs['min_version'] = version kwargs['max_version'] = version endpoint = self.get_session_endpoint(service_key, **kwargs) - except exc.OpenStackCloudException: + except exceptions.SDKException: return False if endpoint: return True diff --git a/openstack/exceptions.py b/openstack/exceptions.py index 747827ffa..53dc0a6a3 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -32,9 +32,6 @@ def __init__(self, message=None, extra_data=None): super(SDKException, self).__init__(self.message) -OpenStackCloudException = SDKException - - class EndpointNotFound(SDKException): """A mismatch occurred between what the client and server expect.""" @@ -274,3 +271,7 @@ class ServiceDisabledException(ConfigException): class ServiceDiscoveryException(SDKException): """The service cannot be discovered.""" + + +# Backwards compatibility +OpenStackCloudException = SDKException diff --git a/openstack/image/v1/_proxy.py b/openstack/image/v1/_proxy.py index 21cf626eb..b32e7a2fd 100644 --- a/openstack/image/v1/_proxy.py +++ b/openstack/image/v1/_proxy.py @@ -13,8 +13,7 @@ import os import warnings -from openstack.cloud import exc -from openstack import exceptions +from openstack import exceptions as exc from openstack.image.v1 import image as _image from openstack import proxy from openstack import utils @@ -128,7 +127,7 @@ def create_image( or 'all_stores' in kwargs or 'all_stores_must_succeed' in kwargs ): - raise exceptions.InvalidRequest( + raise exc.InvalidRequest( "Glance v1 does not support stores or image import" ) @@ -151,7 +150,7 @@ def create_image( container_format = 'bare' if data and filename: - raise exceptions.SDKException( + raise exc.SDKException( 'Passing filename and data simultaneously is not supported' ) @@ -163,7 +162,7 @@ def create_image( ) if validate_checksum and data and not isinstance(data, bytes): - raise exceptions.SDKException( + raise exc.SDKException( 'Validating checksum is not possible when data is not a ' 'direct binary object' ) @@ -301,11 +300,11 @@ def _upload_image( data=image_data, ), ) - except exc.OpenStackCloudHTTPError: + except exc.HttpException: self.log.debug("Deleting failed upload of image %s", name) try: self.delete('/images/{id}'.format(id=image.id)) - except exc.OpenStackCloudHTTPError: + except exc.HttpException: # We're just trying to clean up - if it doesn't work - shrug self.log.warning( "Failed deleting image after we failed uploading it.", diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index f9d74e7e4..f66032f99 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -394,7 +394,8 @@ def create_object( :param metadata: This dict will get changed into headers that set metadata of the object - :raises: ``OpenStackCloudException`` on operation error. + :raises: ``:class:`~openstack.exceptions.SDKException``` on operation + error. """ if data is not None and filename: raise ValueError( diff --git a/openstack/tests/functional/cloud/test_compute.py b/openstack/tests/functional/cloud/test_compute.py index 34a08b5ea..48930528e 100644 --- a/openstack/tests/functional/cloud/test_compute.py +++ b/openstack/tests/functional/cloud/test_compute.py @@ -21,7 +21,7 @@ from fixtures import TimeoutException -from openstack.cloud import exc +from openstack import exceptions from openstack.tests.functional import base from openstack import utils @@ -52,7 +52,7 @@ def _cleanup_servers_and_volumes(self, server_name): for volume in volumes: if volume.status != 'deleting': self.user_cloud.delete_volume(volume.id, wait=True) - except (exc.OpenStackCloudTimeout, TimeoutException): + except (exceptions.ResourceTimeout, TimeoutException): # Ups, some timeout occured during process of deletion server # or volumes, so now we will try to call delete each of them # once again and we will try to live with it @@ -197,7 +197,7 @@ def test_list_all_servers(self): def test_list_all_servers_bad_permissions(self): # Normal users are not allowed to pass all_projects=True self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.user_cloud.list_servers, all_projects=True, ) @@ -252,7 +252,7 @@ def test_list_availability_zone_names(self): def test_get_server_console_bad_server(self): self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.user_cloud.get_server_console, server=self.server_name, ) @@ -525,7 +525,7 @@ def test_set_and_delete_metadata(self): self.assertEqual(set(updated_server.metadata.items()), set([])) self.assertRaises( - exc.OpenStackCloudURINotFound, + exceptions.NotFoundException, self.user_cloud.delete_server_metadata, self.server_name, ['key1'], diff --git a/openstack/tests/functional/cloud/test_domain.py b/openstack/tests/functional/cloud/test_domain.py index 7053b9f03..b56943202 100644 --- a/openstack/tests/functional/cloud/test_domain.py +++ b/openstack/tests/functional/cloud/test_domain.py @@ -17,7 +17,7 @@ Functional tests for keystone domain resource. """ -import openstack.cloud +from openstack import exceptions from openstack.tests.functional import base @@ -45,9 +45,7 @@ def _cleanup_domains(self): if exception_list: # Raise an error: we must make users aware that something went # wrong - raise openstack.cloud.OpenStackCloudException( - '\n'.join(exception_list) - ) + raise exceptions.SDKException('\n'.join(exception_list)) def test_search_domains(self): domain_name = self.domain_prefix + '_search' diff --git a/openstack/tests/functional/cloud/test_endpoints.py b/openstack/tests/functional/cloud/test_endpoints.py index 9b2713c0d..e53fc7df7 100644 --- a/openstack/tests/functional/cloud/test_endpoints.py +++ b/openstack/tests/functional/cloud/test_endpoints.py @@ -22,8 +22,8 @@ import random import string -from openstack.cloud.exc import OpenStackCloudException from openstack.cloud.exc import OpenStackCloudUnavailableFeature +from openstack import exceptions from openstack.tests.functional import base @@ -65,7 +65,7 @@ def _cleanup_endpoints(self): if exception_list: # Raise an error: we must make users aware that something went # wrong - raise OpenStackCloudException('\n'.join(exception_list)) + raise exceptions.SDKException('\n'.join(exception_list)) def _cleanup_services(self): exception_list = list() @@ -82,7 +82,7 @@ def _cleanup_services(self): if exception_list: # Raise an error: we must make users aware that something went # wrong - raise OpenStackCloudException('\n'.join(exception_list)) + raise exceptions.SDKException('\n'.join(exception_list)) def test_create_endpoint(self): service_name = self.new_item_name + '_create' diff --git a/openstack/tests/functional/cloud/test_flavor.py b/openstack/tests/functional/cloud/test_flavor.py index c1ffc136f..29d2fd555 100644 --- a/openstack/tests/functional/cloud/test_flavor.py +++ b/openstack/tests/functional/cloud/test_flavor.py @@ -19,7 +19,7 @@ Functional tests for flavor resource. """ -from openstack.cloud.exc import OpenStackCloudException +from openstack import exceptions from openstack.tests.functional import base @@ -47,7 +47,7 @@ def _cleanup_flavors(self): if exception_list: # Raise an error: we must make users aware that something went # wrong - raise OpenStackCloudException('\n'.join(exception_list)) + raise exceptions.SDKException('\n'.join(exception_list)) def test_create_flavor(self): if not self.operator_cloud: diff --git a/openstack/tests/functional/cloud/test_floating_ip.py b/openstack/tests/functional/cloud/test_floating_ip.py index 96df485c5..b28019991 100644 --- a/openstack/tests/functional/cloud/test_floating_ip.py +++ b/openstack/tests/functional/cloud/test_floating_ip.py @@ -24,8 +24,8 @@ from testtools import content -from openstack.cloud.exc import OpenStackCloudException from openstack.cloud import meta +from openstack import exceptions from openstack import proxy from openstack.tests.functional import base from openstack import utils @@ -116,7 +116,7 @@ def _cleanup_servers(self): if exception_list: # Raise an error: we must make users aware that something went # wrong - raise OpenStackCloudException('\n'.join(exception_list)) + raise exceptions.SDKException('\n'.join(exception_list)) def _cleanup_ips(self, server): exception_list = list() @@ -137,7 +137,7 @@ def _cleanup_ips(self, server): if exception_list: # Raise an error: we must make users aware that something went # wrong - raise OpenStackCloudException('\n'.join(exception_list)) + raise exceptions.SDKException('\n'.join(exception_list)) def _setup_networks(self): if self.user_cloud.has_service('network'): diff --git a/openstack/tests/functional/cloud/test_groups.py b/openstack/tests/functional/cloud/test_groups.py index 729b4a1d9..1e4219d49 100644 --- a/openstack/tests/functional/cloud/test_groups.py +++ b/openstack/tests/functional/cloud/test_groups.py @@ -17,7 +17,7 @@ Functional tests for keystone group resource. """ -import openstack.cloud +from openstack import exceptions from openstack.tests.functional import base @@ -46,9 +46,7 @@ def _cleanup_groups(self): if exception_list: # Raise an error: we must make users aware that something went # wrong - raise openstack.cloud.OpenStackCloudException( - '\n'.join(exception_list) - ) + raise exceptions.SDKException('\n'.join(exception_list)) def test_create_group(self): group_name = self.group_prefix + '_create' diff --git a/openstack/tests/functional/cloud/test_identity.py b/openstack/tests/functional/cloud/test_identity.py index ab5a0a630..b8a8df251 100644 --- a/openstack/tests/functional/cloud/test_identity.py +++ b/openstack/tests/functional/cloud/test_identity.py @@ -20,7 +20,7 @@ import random import string -from openstack.cloud.exc import OpenStackCloudException +from openstack import exceptions from openstack.tests.functional import base @@ -53,7 +53,7 @@ def _cleanup_groups(self): if exception_list: # Raise an error: we must make users aware that something went # wrong - raise OpenStackCloudException('\n'.join(exception_list)) + raise exceptions.SDKException('\n'.join(exception_list)) def _cleanup_users(self): exception_list = list() @@ -66,7 +66,7 @@ def _cleanup_users(self): continue if exception_list: - raise OpenStackCloudException('\n'.join(exception_list)) + raise exceptions.SDKException('\n'.join(exception_list)) def _cleanup_roles(self): exception_list = list() @@ -79,7 +79,7 @@ def _cleanup_roles(self): continue if exception_list: - raise OpenStackCloudException('\n'.join(exception_list)) + raise exceptions.SDKException('\n'.join(exception_list)) def _create_user(self, **kwargs): domain_id = None diff --git a/openstack/tests/functional/cloud/test_network.py b/openstack/tests/functional/cloud/test_network.py index ee6508cd4..9e170afe5 100644 --- a/openstack/tests/functional/cloud/test_network.py +++ b/openstack/tests/functional/cloud/test_network.py @@ -17,7 +17,7 @@ Functional tests for network methods. """ -from openstack.cloud.exc import OpenStackCloudException +from openstack import exceptions from openstack.tests.functional import base @@ -43,7 +43,7 @@ def _cleanup_networks(self): continue if exception_list: - raise OpenStackCloudException('\n'.join(exception_list)) + raise exceptions.SDKException('\n'.join(exception_list)) def test_create_network_basic(self): net1 = self.operator_cloud.create_network(name=self.network_name) diff --git a/openstack/tests/functional/cloud/test_object.py b/openstack/tests/functional/cloud/test_object.py index 6212d783d..88e373d4c 100644 --- a/openstack/tests/functional/cloud/test_object.py +++ b/openstack/tests/functional/cloud/test_object.py @@ -23,7 +23,7 @@ from testtools import content -from openstack.cloud import exc +from openstack import exceptions from openstack.tests.functional import base @@ -97,7 +97,7 @@ def test_create_object(self): self.assertIsNotNone( self.user_cloud.get_object(container_name, name) ) - except exc.OpenStackCloudException as e: + except exceptions.SDKException as e: self.addDetail( 'failed_response', content.text_content(str(e.response.headers)), @@ -186,7 +186,7 @@ def test_download_object_to_file(self): ) downloaded_content = open(fake_file.name, 'rb').read() self.assertEqual(fake_content, downloaded_content) - except exc.OpenStackCloudException as e: + except exceptions.SDKException as e: self.addDetail( 'failed_response', content.text_content(str(e.response.headers)), diff --git a/openstack/tests/functional/cloud/test_port.py b/openstack/tests/functional/cloud/test_port.py index 51afd6596..ea407d453 100644 --- a/openstack/tests/functional/cloud/test_port.py +++ b/openstack/tests/functional/cloud/test_port.py @@ -22,7 +22,7 @@ import random import string -from openstack.cloud.exc import OpenStackCloudException +from openstack import exceptions from openstack.tests.functional import base @@ -59,7 +59,7 @@ def _cleanup_ports(self): if exception_list: # Raise an error: we must make users aware that something went # wrong - raise OpenStackCloudException('\n'.join(exception_list)) + raise exceptions.SDKException('\n'.join(exception_list)) def test_create_port(self): port_name = self.new_port_name + '_create' diff --git a/openstack/tests/functional/cloud/test_project.py b/openstack/tests/functional/cloud/test_project.py index bb7d5f816..421c5a7d5 100644 --- a/openstack/tests/functional/cloud/test_project.py +++ b/openstack/tests/functional/cloud/test_project.py @@ -20,7 +20,7 @@ """ import pprint -from openstack.cloud.exc import OpenStackCloudException +from openstack import exceptions from openstack.tests.functional import base @@ -43,7 +43,7 @@ def _cleanup_projects(self): exception_list.append(str(e)) continue if exception_list: - raise OpenStackCloudException('\n'.join(exception_list)) + raise exceptions.SDKException('\n'.join(exception_list)) def test_create_project(self): project_name = self.new_project_name + '_create' diff --git a/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py b/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py index 0fb323f7d..2d729e7b5 100644 --- a/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py @@ -18,7 +18,7 @@ Functional tests for QoS bandwidth limit methods. """ -from openstack.cloud.exc import OpenStackCloudException +from openstack import exceptions from openstack.tests.functional import base @@ -41,7 +41,7 @@ def _cleanup_qos_policy(self): try: self.operator_cloud.delete_qos_policy(self.policy['id']) except Exception as e: - raise OpenStackCloudException(e) + raise exceptions.SDKException(e) def test_qos_bandwidth_limit_rule_lifecycle(self): max_kbps = 1500 diff --git a/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py b/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py index 69822c838..0ce43e896 100644 --- a/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py +++ b/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py @@ -18,7 +18,7 @@ Functional tests for QoS DSCP marking rule methods. """ -from openstack.cloud.exc import OpenStackCloudException +from openstack import exceptions from openstack.tests.functional import base @@ -41,7 +41,7 @@ def _cleanup_qos_policy(self): try: self.operator_cloud.delete_qos_policy(self.policy['id']) except Exception as e: - raise OpenStackCloudException(e) + raise exceptions.SDKException(e) def test_qos_dscp_marking_rule_lifecycle(self): dscp_mark = 16 diff --git a/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py b/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py index d70387a63..2810f67aa 100644 --- a/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py +++ b/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py @@ -18,7 +18,7 @@ Functional tests for QoS minimum bandwidth methods. """ -from openstack.cloud.exc import OpenStackCloudException +from openstack import exceptions from openstack.tests.functional import base @@ -41,7 +41,7 @@ def _cleanup_qos_policy(self): try: self.operator_cloud.delete_qos_policy(self.policy['id']) except Exception as e: - raise OpenStackCloudException(e) + raise exceptions.SDKException(e) def test_qos_minimum_bandwidth_rule_lifecycle(self): min_kbps = 1500 diff --git a/openstack/tests/functional/cloud/test_qos_policy.py b/openstack/tests/functional/cloud/test_qos_policy.py index ff366e4d2..002f49757 100644 --- a/openstack/tests/functional/cloud/test_qos_policy.py +++ b/openstack/tests/functional/cloud/test_qos_policy.py @@ -18,7 +18,7 @@ Functional tests for QoS policies methods. """ -from openstack.cloud.exc import OpenStackCloudException +from openstack import exceptions from openstack.tests.functional import base @@ -45,7 +45,7 @@ def _cleanup_policies(self): continue if exception_list: - raise OpenStackCloudException('\n'.join(exception_list)) + raise exceptions.SDKException('\n'.join(exception_list)) def test_create_qos_policy_basic(self): policy = self.operator_cloud.create_qos_policy(name=self.policy_name) diff --git a/openstack/tests/functional/cloud/test_range_search.py b/openstack/tests/functional/cloud/test_range_search.py index c0a186c0a..d6662511e 100644 --- a/openstack/tests/functional/cloud/test_range_search.py +++ b/openstack/tests/functional/cloud/test_range_search.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from openstack.cloud import exc +from openstack import exceptions from openstack.tests.functional import base @@ -28,7 +28,7 @@ def _filter_m1_flavors(self, results): def test_range_search_bad_range(self): flavors = self.user_cloud.list_flavors(get_extra=False) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.user_cloud.range_search, flavors, {"ram": "<1a0"}, diff --git a/openstack/tests/functional/cloud/test_router.py b/openstack/tests/functional/cloud/test_router.py index 1b0db5364..1968492a3 100644 --- a/openstack/tests/functional/cloud/test_router.py +++ b/openstack/tests/functional/cloud/test_router.py @@ -19,7 +19,7 @@ import ipaddress -from openstack.cloud.exc import OpenStackCloudException +from openstack import exceptions from openstack.tests.functional import base @@ -64,7 +64,7 @@ def _cleanup_routers(self): continue if exception_list: - raise OpenStackCloudException('\n'.join(exception_list)) + raise exceptions.SDKException('\n'.join(exception_list)) def _cleanup_networks(self): exception_list = list() @@ -77,7 +77,7 @@ def _cleanup_networks(self): continue if exception_list: - raise OpenStackCloudException('\n'.join(exception_list)) + raise exceptions.SDKException('\n'.join(exception_list)) def _cleanup_subnets(self): exception_list = list() @@ -90,7 +90,7 @@ def _cleanup_subnets(self): continue if exception_list: - raise OpenStackCloudException('\n'.join(exception_list)) + raise exceptions.SDKException('\n'.join(exception_list)) def test_create_router_basic(self): net1_name = self.network_prefix + '_net1' diff --git a/openstack/tests/functional/cloud/test_services.py b/openstack/tests/functional/cloud/test_services.py index 92aa5cc6c..c7639af2a 100644 --- a/openstack/tests/functional/cloud/test_services.py +++ b/openstack/tests/functional/cloud/test_services.py @@ -22,8 +22,8 @@ import random import string -from openstack.cloud.exc import OpenStackCloudException -from openstack.cloud.exc import OpenStackCloudUnavailableFeature +from openstack.cloud import exc +from openstack import exceptions from openstack.tests.functional import base @@ -57,7 +57,7 @@ def _cleanup_services(self): if exception_list: # Raise an error: we must make users aware that something went # wrong - raise OpenStackCloudException('\n'.join(exception_list)) + raise exceptions.SDKException('\n'.join(exception_list)) def test_create_service(self): service = self.operator_cloud.create_service( @@ -72,7 +72,7 @@ def test_update_service(self): if ver.startswith('2'): # NOTE(SamYaple): Update service only works with v3 api self.assertRaises( - OpenStackCloudUnavailableFeature, + exc.OpenStackCloudUnavailableFeature, self.operator_cloud.update_service, 'service_id', name='new name', diff --git a/openstack/tests/functional/cloud/test_stack.py b/openstack/tests/functional/cloud/test_stack.py index c3399e29a..81190a384 100644 --- a/openstack/tests/functional/cloud/test_stack.py +++ b/openstack/tests/functional/cloud/test_stack.py @@ -19,7 +19,7 @@ import tempfile -from openstack.cloud import exc +from openstack import exceptions from openstack.tests import fakes from openstack.tests.functional import base @@ -88,7 +88,7 @@ def test_stack_validation(self): test_template.close() stack_name = self.getUniqueString('validate_template') self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.user_cloud.create_stack, name=stack_name, template_file=test_template.name, diff --git a/openstack/tests/functional/cloud/test_users.py b/openstack/tests/functional/cloud/test_users.py index 45572a2f9..08c02ee7d 100644 --- a/openstack/tests/functional/cloud/test_users.py +++ b/openstack/tests/functional/cloud/test_users.py @@ -17,7 +17,7 @@ Functional tests for user methods. """ -from openstack.cloud.exc import OpenStackCloudException +from openstack import exceptions from openstack.tests.functional import base @@ -41,7 +41,7 @@ def _cleanup_users(self): continue if exception_list: - raise OpenStackCloudException('\n'.join(exception_list)) + raise exceptions.SDKException('\n'.join(exception_list)) def _create_user(self, **kwargs): domain_id = None diff --git a/openstack/tests/functional/cloud/test_volume.py b/openstack/tests/functional/cloud/test_volume.py index 551f75b9c..d8c7df283 100644 --- a/openstack/tests/functional/cloud/test_volume.py +++ b/openstack/tests/functional/cloud/test_volume.py @@ -20,7 +20,7 @@ from fixtures import TimeoutException from testtools import content -from openstack.cloud import exc +from openstack import exceptions from openstack.tests.functional import base from openstack import utils @@ -123,7 +123,7 @@ def cleanup(self, volume, snapshot_name=None, image_name=None): break if not found: break - except (exc.OpenStackCloudTimeout, TimeoutException): + except (exceptions.ResourceTimeout, TimeoutException): # NOTE(slaweq): ups, some volumes are still not removed # so we should try to force delete it once again and move # forward diff --git a/openstack/tests/functional/cloud/test_volume_type.py b/openstack/tests/functional/cloud/test_volume_type.py index 51494ecbe..1e527cba0 100644 --- a/openstack/tests/functional/cloud/test_volume_type.py +++ b/openstack/tests/functional/cloud/test_volume_type.py @@ -20,7 +20,7 @@ """ import testtools -from openstack.cloud import exc +from openstack import exceptions from openstack.tests.functional import base @@ -94,7 +94,7 @@ def test_add_volume_type_access_missing_project(self): def test_add_volume_type_access_missing_volume(self): with testtools.ExpectedException( - exc.OpenStackCloudException, "VolumeType not found.*" + exceptions.SDKException, "VolumeType not found.*" ): self.operator_cloud.add_volume_type_access( 'MISSING_VOLUME_TYPE', self.operator_cloud.current_project_id @@ -102,7 +102,7 @@ def test_add_volume_type_access_missing_volume(self): def test_remove_volume_type_access_missing_volume(self): with testtools.ExpectedException( - exc.OpenStackCloudException, "VolumeType not found.*" + exceptions.SDKException, "VolumeType not found.*" ): self.operator_cloud.remove_volume_type_access( 'MISSING_VOLUME_TYPE', self.operator_cloud.current_project_id @@ -110,7 +110,7 @@ def test_remove_volume_type_access_missing_volume(self): def test_add_volume_type_access_bad_project(self): with testtools.ExpectedException( - exc.OpenStackCloudBadRequest, "Unable to authorize.*" + exceptions.BadRequestException, "Unable to authorize.*" ): self.operator_cloud.add_volume_type_access( 'test-volume-type', 'BAD_PROJECT_ID' @@ -118,7 +118,7 @@ def test_add_volume_type_access_bad_project(self): def test_remove_volume_type_access_missing_project(self): with testtools.ExpectedException( - exc.OpenStackCloudURINotFound, "Unable to revoke.*" + exceptions.NotFoundException, "Unable to revoke.*" ): self.operator_cloud.remove_volume_type_access( 'test-volume-type', '00000000000000000000000000000000' diff --git a/openstack/tests/unit/cloud/test__utils.py b/openstack/tests/unit/cloud/test__utils.py index de5a37cde..273a3f6e2 100644 --- a/openstack/tests/unit/cloud/test__utils.py +++ b/openstack/tests/unit/cloud/test__utils.py @@ -18,7 +18,7 @@ import testtools from openstack.cloud import _utils -from openstack.cloud import exc +from openstack import exceptions from openstack.tests.unit import base RANGE_DATA = [ @@ -200,7 +200,7 @@ def test_safe_dict_min_not_int(self): """Test non-integer key value raises OSCE""" data = [{'f1': 3}, {'f1': "aaa"}, {'f1': 1}] with testtools.ExpectedException( - exc.OpenStackCloudException, + exceptions.SDKException, "Search for minimum value failed. " "Value for f1 is not an integer: aaa", ): @@ -240,7 +240,7 @@ def test_safe_dict_max_not_int(self): """Test non-integer key value raises OSCE""" data = [{'f1': 3}, {'f1': "aaa"}, {'f1': 1}] with testtools.ExpectedException( - exc.OpenStackCloudException, + exceptions.SDKException, "Search for maximum value failed. " "Value for f1 is not an integer: aaa", ): @@ -308,13 +308,13 @@ def test_range_filter_exact(self): def test_range_filter_invalid_int(self): with testtools.ExpectedException( - exc.OpenStackCloudException, "Invalid range value: <1A0" + exceptions.SDKException, "Invalid range value: <1A0" ): _utils.range_filter(RANGE_DATA, "key1", "<1A0") def test_range_filter_invalid_op(self): with testtools.ExpectedException( - exc.OpenStackCloudException, "Invalid range value: <>100" + exceptions.SDKException, "Invalid range value: <>100" ): _utils.range_filter(RANGE_DATA, "key1", "<>100") diff --git a/openstack/tests/unit/cloud/test_baremetal_node.py b/openstack/tests/unit/cloud/test_baremetal_node.py index 71d9a9027..c768d2378 100644 --- a/openstack/tests/unit/cloud/test_baremetal_node.py +++ b/openstack/tests/unit/cloud/test_baremetal_node.py @@ -21,7 +21,6 @@ from testscenarios import load_tests_apply_scenarios as load_tests # noqa -from openstack.cloud import exc from openstack import exceptions from openstack.network.v2 import port as _port from openstack.tests import fakes @@ -319,7 +318,7 @@ def test_inspect_machine_fail_active(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.inspect_machine, self.fake_baremetal_node['uuid'], wait=True, @@ -344,7 +343,7 @@ def test_inspect_machine_fail_associated(self): ] ) self.assertRaisesRegex( - exc.OpenStackCloudException, + exceptions.SDKException, 'associated with an instance', self.cloud.inspect_machine, self.fake_baremetal_node['uuid'], @@ -744,7 +743,7 @@ def test_inspect_machine_inspect_failed(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.inspect_machine, self.fake_baremetal_node['uuid'], wait=True, @@ -985,7 +984,7 @@ def test_set_machine_power_reboot_failure(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.set_machine_power_reboot, self.fake_baremetal_node['uuid'], ) @@ -1195,7 +1194,7 @@ def test_node_set_provision_state_wait_timeout_fails(self): ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.node_set_provision_state, self.fake_baremetal_node['uuid'], 'active', @@ -1267,7 +1266,7 @@ def test_node_set_provision_state_wait_failure_cases(self): ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.node_set_provision_state, self.fake_baremetal_node['uuid'], 'active', @@ -1378,7 +1377,7 @@ def test_wait_for_baremetal_node_lock_timeout(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.wait_for_baremetal_node_lock, self.fake_baremetal_node, timeout=0.001, @@ -1742,7 +1741,7 @@ def test_register_machine_enroll_failure(self): ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.register_machine, nics, **node_to_post @@ -1807,7 +1806,7 @@ def test_register_machine_enroll_timeout(self): # state to the API is essentially a busy state that we # want to block on until it has cleared. self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.register_machine, nics, timeout=0.001, @@ -1897,7 +1896,7 @@ def test_register_machine_enroll_timeout_wait(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.register_machine, nics, wait=True, @@ -1946,7 +1945,7 @@ def test_register_machine_port_create_failed(self): ] ) self.assertRaisesRegex( - exc.OpenStackCloudException, + exceptions.SDKException, 'no ports for you', self.cloud.register_machine, nics, @@ -2012,7 +2011,7 @@ def test_register_machine_several_ports_create_failed(self): ] ) self.assertRaisesRegex( - exc.OpenStackCloudException, + exceptions.SDKException, 'no ports for you', self.cloud.register_machine, nics, @@ -2101,7 +2100,7 @@ def test_unregister_machine_locked_timeout(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.unregister_machine, nics, self.fake_baremetal_node['uuid'], @@ -2211,7 +2210,7 @@ def test_unregister_machine_unavailable(self): for state in invalid_states: self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.unregister_machine, nics, self.fake_baremetal_node['uuid'], diff --git a/openstack/tests/unit/cloud/test_baremetal_ports.py b/openstack/tests/unit/cloud/test_baremetal_ports.py index cd2a2152b..19c5cff54 100644 --- a/openstack/tests/unit/cloud/test_baremetal_ports.py +++ b/openstack/tests/unit/cloud/test_baremetal_ports.py @@ -19,7 +19,7 @@ from testscenarios import load_tests_apply_scenarios as load_tests # noqa -from openstack.cloud import exc +from openstack import exceptions from openstack.tests import fakes from openstack.tests.unit import base @@ -71,7 +71,7 @@ def test_list_nics_failure(self): ) ] ) - self.assertRaises(exc.OpenStackCloudException, self.cloud.list_nics) + self.assertRaises(exceptions.SDKException, self.cloud.list_nics) self.assert_calls() def test_list_nics_for_machine(self): @@ -121,7 +121,7 @@ def test_list_nics_for_machine_failure(self): ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.list_nics_for_machine, self.fake_baremetal_node['uuid'], ) diff --git a/openstack/tests/unit/cloud/test_cloud.py b/openstack/tests/unit/cloud/test_cloud.py index cbed0c86a..8ae554f46 100644 --- a/openstack/tests/unit/cloud/test_cloud.py +++ b/openstack/tests/unit/cloud/test_cloud.py @@ -15,8 +15,8 @@ import testtools -from openstack.cloud import exc from openstack import connection +from openstack import exceptions from openstack.tests.unit import base from openstack import utils @@ -145,7 +145,7 @@ def test_global_request_id_context(self): def test_iterate_timeout_bad_wait(self): with testtools.ExpectedException( - exc.OpenStackCloudException, + exceptions.SDKException, "Wait value must be an int or float value.", ): for count in utils.iterate_timeout( @@ -174,7 +174,7 @@ def test_iterate_timeout_int_wait(self, mock_sleep): @mock.patch('time.sleep') def test_iterate_timeout_timeout(self, mock_sleep): message = "timeout test" - with testtools.ExpectedException(exc.OpenStackCloudTimeout, message): + with testtools.ExpectedException(exceptions.ResourceTimeout, message): for count in utils.iterate_timeout(0.1, message, wait=1): pass mock_sleep.assert_called_with(1.0) diff --git a/openstack/tests/unit/cloud/test_cluster_templates.py b/openstack/tests/unit/cloud/test_cluster_templates.py index a1c5a4c63..75dd40b49 100644 --- a/openstack/tests/unit/cloud/test_cluster_templates.py +++ b/openstack/tests/unit/cloud/test_cluster_templates.py @@ -210,9 +210,9 @@ def test_create_cluster_template_exception(self): # for matching the old error message text. Investigate plumbing # an error message in to the adapter call so that we can give a # more informative error. Also, the test was originally catching - # OpenStackCloudException - but for some reason testtools will not + # SDKException - but for some reason testtools will not # match the more specific HTTPError, even though it's a subclass - # of OpenStackCloudException. + # of SDKException. with testtools.ExpectedException(exceptions.ForbiddenException): self.cloud.create_cluster_template('fake-cluster-template') self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_compute.py b/openstack/tests/unit/cloud/test_compute.py index 04622ee63..90f984505 100644 --- a/openstack/tests/unit/cloud/test_compute.py +++ b/openstack/tests/unit/cloud/test_compute.py @@ -12,7 +12,6 @@ import uuid -from openstack.cloud import exc from openstack import exceptions from openstack.tests import fakes from openstack.tests.unit import base @@ -495,6 +494,6 @@ def test_list_servers_exception(self): ] ) - self.assertRaises(exc.OpenStackCloudException, self.cloud.list_servers) + self.assertRaises(exceptions.SDKException, self.cloud.list_servers) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_create_server.py b/openstack/tests/unit/cloud/test_create_server.py index 02b4fa6b5..1acedb2c3 100644 --- a/openstack/tests/unit/cloud/test_create_server.py +++ b/openstack/tests/unit/cloud/test_create_server.py @@ -21,10 +21,10 @@ from unittest import mock import uuid -from openstack.cloud import exc from openstack.cloud import meta from openstack.compute.v2 import server from openstack import connection +from openstack import exceptions from openstack.tests import fakes from openstack.tests.unit import base @@ -81,7 +81,7 @@ def test_create_server_with_get_exception(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.create_server, 'server-name', {'id': 'image-id'}, @@ -135,7 +135,7 @@ def test_create_server_with_server_error(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.create_server, 'server-name', {'id': 'image-id'}, @@ -196,7 +196,7 @@ def test_create_server_wait_server_error(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.create_server, 'server-name', dict(id='image-id'), @@ -251,7 +251,7 @@ def test_create_server_with_timeout(self): ] ) self.assertRaises( - exc.OpenStackCloudTimeout, + exceptions.ResourceTimeout, self.cloud.create_server, 'server-name', dict(id='image-id'), @@ -815,7 +815,7 @@ def test_create_server_no_addresses(self, mock_add_ips_to_server): mock_add_ips_to_server.return_value = fake_server self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.create_server, 'server-name', {'id': 'image-id'}, @@ -1179,7 +1179,7 @@ def test_create_server_network_fixed_ip_conflicts(self): self.use_nothing() fixed_ip = '10.0.0.1' self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.create_server, 'server-name', dict(id='image-id'), diff --git a/openstack/tests/unit/cloud/test_create_volume_snapshot.py b/openstack/tests/unit/cloud/test_create_volume_snapshot.py index b1e0e2fb5..12ca3aeaf 100644 --- a/openstack/tests/unit/cloud/test_create_volume_snapshot.py +++ b/openstack/tests/unit/cloud/test_create_volume_snapshot.py @@ -18,8 +18,8 @@ """ from openstack.block_storage.v3 import snapshot -from openstack.cloud import exc from openstack.cloud import meta +from openstack import exceptions from openstack.tests import fakes from openstack.tests.unit import base @@ -125,7 +125,7 @@ def test_create_volume_snapshot_with_timeout(self): ) self.assertRaises( - exc.OpenStackCloudTimeout, + exceptions.ResourceTimeout, self.cloud.create_volume_snapshot, volume_id=volume_id, wait=True, @@ -181,7 +181,7 @@ def test_create_volume_snapshot_with_error(self): ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.create_volume_snapshot, volume_id=volume_id, wait=True, diff --git a/openstack/tests/unit/cloud/test_delete_server.py b/openstack/tests/unit/cloud/test_delete_server.py index 9a508cfab..3e62f71a0 100644 --- a/openstack/tests/unit/cloud/test_delete_server.py +++ b/openstack/tests/unit/cloud/test_delete_server.py @@ -18,7 +18,7 @@ """ import uuid -from openstack.cloud import exc as shade_exc +from openstack import exceptions from openstack.tests import fakes from openstack.tests.unit import base @@ -204,7 +204,7 @@ def test_delete_server_fails(self): ) self.assertRaises( - shade_exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.delete_server, 'speedy', wait=False, diff --git a/openstack/tests/unit/cloud/test_delete_volume_snapshot.py b/openstack/tests/unit/cloud/test_delete_volume_snapshot.py index c70f938ee..77314b42d 100644 --- a/openstack/tests/unit/cloud/test_delete_volume_snapshot.py +++ b/openstack/tests/unit/cloud/test_delete_volume_snapshot.py @@ -17,8 +17,8 @@ Tests for the `delete_volume_snapshot` command. """ -from openstack.cloud import exc from openstack.cloud import meta +from openstack import exceptions from openstack.tests import fakes from openstack.tests.unit import base @@ -66,7 +66,7 @@ def test_delete_volume_snapshot(self): def test_delete_volume_snapshot_with_error(self): """ Test that a exception while deleting a volume snapshot will cause an - OpenStackCloudException. + SDKException. """ fake_snapshot = fakes.FakeVolumeSnapshot( '1234', 'available', 'foo', 'derpysnapshot' @@ -94,7 +94,7 @@ def test_delete_volume_snapshot_with_error(self): ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.delete_volume_snapshot, name_or_id='1234', ) @@ -138,7 +138,7 @@ def test_delete_volume_snapshot_with_timeout(self): ) self.assertRaises( - exc.OpenStackCloudTimeout, + exceptions.ResourceTimeout, self.cloud.delete_volume_snapshot, name_or_id='1234', wait=True, diff --git a/openstack/tests/unit/cloud/test_domain_params.py b/openstack/tests/unit/cloud/test_domain_params.py index fd52e89b7..bbf7f4c08 100644 --- a/openstack/tests/unit/cloud/test_domain_params.py +++ b/openstack/tests/unit/cloud/test_domain_params.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.cloud import exc +from openstack import exceptions from openstack.tests.unit import base @@ -43,7 +43,7 @@ def test_identity_params_v3_no_domain(self): project_data = self._get_project_data(v3=True) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud._get_identity_params, domain_id=None, project=project_data.project_name, diff --git a/openstack/tests/unit/cloud/test_domains.py b/openstack/tests/unit/cloud/test_domains.py index 824377975..761e7ced5 100644 --- a/openstack/tests/unit/cloud/test_domains.py +++ b/openstack/tests/unit/cloud/test_domains.py @@ -18,7 +18,7 @@ import testtools from testtools import matchers -import openstack.cloud +from openstack import exceptions from openstack.tests.unit import base @@ -145,9 +145,7 @@ def test_create_domain_exception(self): domain_data = self._get_domain_data( domain_name='domain_name', enabled=True ) - with testtools.ExpectedException( - openstack.cloud.OpenStackCloudBadRequest - ): + with testtools.ExpectedException(exceptions.BadRequestException): self.register_uris( [ dict( @@ -237,9 +235,7 @@ def test_delete_domain_exception(self): ), ] ) - with testtools.ExpectedException( - openstack.exceptions.ResourceNotFound - ): + with testtools.ExpectedException(exceptions.ResourceNotFound): self.cloud.delete_domain(domain_data.domain_id) self.assert_calls() @@ -320,8 +316,6 @@ def test_update_domain_exception(self): ) ] ) - with testtools.ExpectedException( - openstack.exceptions.ConflictException - ): + with testtools.ExpectedException(exceptions.ConflictException): self.cloud.delete_domain(domain_data.domain_id) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_flavors.py b/openstack/tests/unit/cloud/test_flavors.py index b6026110a..3f9230f59 100644 --- a/openstack/tests/unit/cloud/test_flavors.py +++ b/openstack/tests/unit/cloud/test_flavors.py @@ -10,8 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. - -import openstack.cloud +from openstack import exceptions from openstack.tests import fakes from openstack.tests.unit import base @@ -136,7 +135,7 @@ def test_delete_flavor_exception(self): ) self.assertRaises( - openstack.cloud.OpenStackCloudException, + exceptions.SDKException, self.cloud.delete_flavor, 'vanilla', ) @@ -278,7 +277,7 @@ def test_get_flavor_by_ram_not_found(self): ] ) self.assertRaises( - openstack.cloud.OpenStackCloudException, + exceptions.SDKException, self.cloud.get_flavor_by_ram, ram=100, ) diff --git a/openstack/tests/unit/cloud/test_floating_ip_neutron.py b/openstack/tests/unit/cloud/test_floating_ip_neutron.py index 3d3ae2479..b3d668866 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_neutron.py +++ b/openstack/tests/unit/cloud/test_floating_ip_neutron.py @@ -22,7 +22,7 @@ import copy import datetime -from openstack.cloud import exc +from openstack import exceptions from openstack.tests import fakes from openstack.tests.unit import base from openstack import utils @@ -341,7 +341,7 @@ def test_create_floating_ip_port_bad_response(self): # Fails because we requested a port and the returned FIP has no port self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.create_floating_ip, network='my-network', port='ce705c24-c1ef-408a-bda3-7bbd946164ab', @@ -492,7 +492,7 @@ def test_neutron_available_floating_ips_invalid_network(self): ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud._neutron_available_floating_ips, network='INVALID', ) @@ -993,7 +993,7 @@ def test_delete_floating_ip_existing_no_delete(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.delete_floating_ip, floating_ip_id=fip_id, retry=2, @@ -1313,7 +1313,7 @@ def test_create_floating_ip_no_port(self): ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud._neutron_create_floating_ip, server=dict(id='some-server'), ) diff --git a/openstack/tests/unit/cloud/test_floating_ip_pool.py b/openstack/tests/unit/cloud/test_floating_ip_pool.py index d2dd2485a..bfa5645aa 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_pool.py +++ b/openstack/tests/unit/cloud/test_floating_ip_pool.py @@ -19,7 +19,7 @@ Test floating IP pool resource (managed by nova) """ -from openstack.cloud.exc import OpenStackCloudException +from openstack import exceptions from openstack.tests import fakes from openstack.tests.unit import base @@ -96,7 +96,8 @@ def test_list_floating_ip_pools_exception(self): ) self.assertRaises( - OpenStackCloudException, self.cloud.list_floating_ip_pools + exceptions.SDKException, + self.cloud.list_floating_ip_pools, ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 731ab7ee3..8204895cf 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -18,7 +18,6 @@ from unittest import mock import uuid -from openstack.cloud import exc from openstack.cloud import meta from openstack import connection from openstack import exceptions @@ -70,7 +69,7 @@ def setUp(self): def test_download_image_no_output(self): self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.download_image, self.image_name, ) @@ -78,7 +77,7 @@ def test_download_image_no_output(self): def test_download_image_two_outputs(self): fake_fd = io.BytesIO() self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.download_image, self.image_name, output_path='fake_path', @@ -110,7 +109,7 @@ def test_download_image_no_images_found(self): ] ) self.assertRaises( - exc.OpenStackCloudResourceNotFound, + exceptions.NotFoundException, self.cloud.download_image, self.image_name, output_path='fake_path', @@ -1354,7 +1353,7 @@ def test_create_image_put_v1_bad_delete(self): ) self.assertRaises( - exc.OpenStackCloudHTTPError, + exceptions.HttpException, self._call_create_image, self.image_name, ) @@ -1470,7 +1469,7 @@ def test_create_image_put_v2_bad_delete(self): ) self.assertRaises( - exc.OpenStackCloudHTTPError, + exceptions.HttpException, self._call_create_image, self.image_name, ) @@ -1556,7 +1555,7 @@ def test_create_image_put_bad_int(self): self.cloud.image_api_use_tasks = False self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self._call_create_image, self.image_name, allow_duplicates=True, diff --git a/openstack/tests/unit/cloud/test_image_snapshot.py b/openstack/tests/unit/cloud/test_image_snapshot.py index 2e33be1a4..71006b611 100644 --- a/openstack/tests/unit/cloud/test_image_snapshot.py +++ b/openstack/tests/unit/cloud/test_image_snapshot.py @@ -14,7 +14,7 @@ import uuid -from openstack.cloud import exc +from openstack import exceptions from openstack.tests import fakes from openstack.tests.unit import base @@ -66,7 +66,7 @@ def test_create_image_snapshot_wait_until_active_never_active(self): ) self.assertRaises( - exc.OpenStackCloudTimeout, + exceptions.ResourceTimeout, self.cloud.create_image_snapshot, snapshot_name, dict(id=self.server_id), diff --git a/openstack/tests/unit/cloud/test_keypair.py b/openstack/tests/unit/cloud/test_keypair.py index 20410609f..433cf13c3 100644 --- a/openstack/tests/unit/cloud/test_keypair.py +++ b/openstack/tests/unit/cloud/test_keypair.py @@ -13,7 +13,7 @@ import fixtures -from openstack.cloud import exc +from openstack import exceptions from openstack.tests import fakes from openstack.tests.unit import base @@ -83,7 +83,7 @@ def test_create_keypair_exception(self): ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.create_keypair, self.keyname, self.key['public_key'], @@ -195,7 +195,5 @@ def test_list_keypairs_exception(self): ), ] ) - self.assertRaises( - exc.OpenStackCloudException, self.cloud.list_keypairs - ) + self.assertRaises(exceptions.SDKException, self.cloud.list_keypairs) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_network.py b/openstack/tests/unit/cloud/test_network.py index 04880b9e1..1490799c0 100644 --- a/openstack/tests/unit/cloud/test_network.py +++ b/openstack/tests/unit/cloud/test_network.py @@ -15,8 +15,6 @@ import testtools -import openstack -import openstack.cloud from openstack import exceptions from openstack.network.v2 import network as _network from openstack.tests.unit import base @@ -468,7 +466,7 @@ def test_create_network_provider_ignored_value(self): def test_create_network_wrong_availability_zone_hints_type(self): azh_opts = "invalid" with testtools.ExpectedException( - openstack.cloud.OpenStackCloudException, + exceptions.SDKException, "Parameter 'availability_zone_hints' must be a list", ): self.cloud.create_network( @@ -478,7 +476,7 @@ def test_create_network_wrong_availability_zone_hints_type(self): def test_create_network_provider_wrong_type(self): provider_opts = "invalid" with testtools.ExpectedException( - openstack.cloud.OpenStackCloudException, + exceptions.SDKException, "Parameter 'provider' must be a dict", ): self.cloud.create_network("netname", provider=provider_opts) @@ -543,14 +541,14 @@ def test_create_network_with_mtu(self): def test_create_network_with_wrong_mtu_size(self): with testtools.ExpectedException( - openstack.cloud.OpenStackCloudException, + exceptions.SDKException, "Parameter 'mtu_size' must be greater than 67.", ): self.cloud.create_network("netname", mtu_size=42) def test_create_network_with_wrong_mtu_type(self): with testtools.ExpectedException( - openstack.cloud.OpenStackCloudException, + exceptions.SDKException, "Parameter 'mtu_size' must be an integer.", ): self.cloud.create_network("netname", mtu_size="fourty_two") @@ -658,7 +656,7 @@ def test_delete_network_exception(self): ] ) self.assertRaises( - openstack.cloud.OpenStackCloudException, + exceptions.SDKException, self.cloud.delete_network, network_name, ) diff --git a/openstack/tests/unit/cloud/test_object.py b/openstack/tests/unit/cloud/test_object.py index 643b1346a..643b41d6e 100644 --- a/openstack/tests/unit/cloud/test_object.py +++ b/openstack/tests/unit/cloud/test_object.py @@ -17,8 +17,6 @@ import testtools -import openstack.cloud -from openstack.cloud import exc import openstack.cloud.openstackcloud as oc_oc from openstack import exceptions from openstack.object_store.v1 import _proxy @@ -198,7 +196,7 @@ def test_delete_container_error(self): ] ) self.assertRaises( - openstack.cloud.OpenStackCloudException, + exceptions.SDKException, self.cloud.delete_container, self.container, ) @@ -231,7 +229,7 @@ def test_update_container_error(self): [dict(method='POST', uri=self.container_endpoint, status_code=409)] ) self.assertRaises( - openstack.cloud.OpenStackCloudException, + exceptions.SDKException, self.cloud.update_container, self.container, dict(foo='bar'), @@ -284,7 +282,7 @@ def test_set_container_access_private(self): def test_set_container_access_invalid(self): self.assertRaises( - openstack.cloud.OpenStackCloudException, + exceptions.SDKException, self.cloud.set_container_access, self.container, 'invalid', @@ -319,7 +317,7 @@ def test_get_container_invalid(self): ) with testtools.ExpectedException( - exc.OpenStackCloudException, + exceptions.SDKException, "Could not determine container access for ACL: invalid", ): self.cloud.get_container_access(self.container) @@ -329,7 +327,7 @@ def test_get_container_access_not_found(self): [dict(method='HEAD', uri=self.container_endpoint, status_code=404)] ) with testtools.ExpectedException( - exc.OpenStackCloudException, + exceptions.SDKException, "Container not found: %s" % self.container, ): self.cloud.get_container_access(self.container) @@ -368,9 +366,7 @@ def test_list_containers_exception(self): ] ) - self.assertRaises( - exc.OpenStackCloudException, self.cloud.list_containers - ) + self.assertRaises(exceptions.SDKException, self.cloud.list_containers) self.assert_calls() @mock.patch.object(_proxy, '_get_expiration', return_value=13345) @@ -660,7 +656,7 @@ def test_list_objects_exception(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.list_objects, self.container, ) @@ -798,7 +794,7 @@ def test_get_object_exception(self): ) self.assertRaises( - openstack.cloud.OpenStackCloudException, + exceptions.SDKException, self.cloud.get_object, self.container, self.object, @@ -1511,7 +1507,7 @@ def test_slo_manifest_fail(self): self.cloud.image_api_use_tasks = True self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.create_object, container=self.container, name=self.object, @@ -1594,7 +1590,7 @@ def test_object_segment_retry_failure(self): ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.create_object, container=self.container, name=self.object, diff --git a/openstack/tests/unit/cloud/test_operator.py b/openstack/tests/unit/cloud/test_operator.py index 32a2da484..1487d3208 100644 --- a/openstack/tests/unit/cloud/test_operator.py +++ b/openstack/tests/unit/cloud/test_operator.py @@ -15,8 +15,8 @@ import testtools -from openstack.cloud import exc from openstack.config import cloud_region +from openstack import exceptions from openstack.tests import fakes from openstack.tests.unit import base @@ -90,7 +90,7 @@ def side_effect(*args, **kwargs): self.cloud.name = 'testcloud' self.cloud.config.config['region_name'] = 'testregion' with testtools.ExpectedException( - exc.OpenStackCloudException, + exceptions.SDKException, "Error getting image endpoint on testcloud:testregion:" " No service", ): diff --git a/openstack/tests/unit/cloud/test_port.py b/openstack/tests/unit/cloud/test_port.py index 0a21aba6f..d184ca440 100644 --- a/openstack/tests/unit/cloud/test_port.py +++ b/openstack/tests/unit/cloud/test_port.py @@ -19,7 +19,7 @@ Test port resource (managed by neutron) """ -from openstack.cloud.exc import OpenStackCloudException +from openstack import exceptions from openstack.network.v2 import port as _port from openstack.tests.unit import base @@ -203,7 +203,7 @@ def test_create_port_exception(self): ] ) self.assertRaises( - OpenStackCloudException, + exceptions.SDKException, self.cloud.create_port, network_id='test-net-id', name='test-port-name', @@ -312,7 +312,7 @@ def test_update_port_exception(self): ] ) self.assertRaises( - OpenStackCloudException, + exceptions.SDKException, self.cloud.update_port, name_or_id='d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b', name='test-port-name-updated', @@ -368,7 +368,7 @@ def test_list_ports_exception(self): ) ] ) - self.assertRaises(OpenStackCloudException, self.cloud.list_ports) + self.assertRaises(exceptions.SDKException, self.cloud.list_ports) def test_search_ports_by_id(self): port_id = 'f71a6703-d6de-4be1-a91a-a570ede1d159' @@ -514,7 +514,7 @@ def test_delete_subnet_multiple_found(self): ] ) self.assertRaises( - OpenStackCloudException, self.cloud.delete_port, port_name + exceptions.SDKException, self.cloud.delete_port, port_name ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_project.py b/openstack/tests/unit/cloud/test_project.py index dde72440d..244c9d115 100644 --- a/openstack/tests/unit/cloud/test_project.py +++ b/openstack/tests/unit/cloud/test_project.py @@ -15,8 +15,7 @@ import testtools from testtools import matchers -import openstack.cloud -import openstack.cloud._utils +from openstack import exceptions from openstack.tests.unit import base @@ -129,7 +128,7 @@ def test_update_project_not_found(self): # shade will raise an attribute error instead of the proper # project not found exception. with testtools.ExpectedException( - openstack.cloud.OpenStackCloudException, + exceptions.SDKException, "Project %s not found." % project_data.project_id, ): self.cloud.update_project(project_data.project_id) diff --git a/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py b/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py index 01b0ca719..1a88497d2 100644 --- a/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py @@ -15,7 +15,7 @@ import copy -from openstack.cloud import exc +from openstack import exceptions from openstack.network.v2 import qos_bandwidth_limit_rule from openstack.tests.unit import base @@ -164,7 +164,7 @@ def test_get_qos_bandwidth_limit_rule_no_qos_policy_found(self): ] ) self.assertRaises( - exc.OpenStackCloudResourceNotFound, + exceptions.NotFoundException, self.cloud.get_qos_bandwidth_limit_rule, self.policy_name, self.rule_id, @@ -184,7 +184,7 @@ def test_get_qos_bandwidth_limit_rule_no_qos_extension(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.get_qos_bandwidth_limit_rule, self.policy_name, self.rule_id, @@ -256,7 +256,7 @@ def test_create_qos_bandwidth_limit_rule_no_qos_extension(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.create_qos_bandwidth_limit_rule, self.policy_name, max_kbps=100, @@ -403,7 +403,7 @@ def test_update_qos_bandwidth_limit_rule_no_qos_extension(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.update_qos_bandwidth_limit_rule, self.policy_id, self.rule_id, @@ -558,7 +558,7 @@ def test_delete_qos_bandwidth_limit_rule_no_qos_extension(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.delete_qos_bandwidth_limit_rule, self.policy_name, self.rule_id, diff --git a/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py b/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py index 828356822..30af9b7cc 100644 --- a/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py +++ b/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py @@ -15,7 +15,7 @@ import copy -from openstack.cloud import exc +from openstack import exceptions from openstack.network.v2 import qos_dscp_marking_rule from openstack.tests.unit import base @@ -147,7 +147,7 @@ def test_get_qos_dscp_marking_rule_no_qos_policy_found(self): ] ) self.assertRaises( - exc.OpenStackCloudResourceNotFound, + exceptions.NotFoundException, self.cloud.get_qos_dscp_marking_rule, self.policy_name, self.rule_id, @@ -167,7 +167,7 @@ def test_get_qos_dscp_marking_rule_no_qos_extension(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.get_qos_dscp_marking_rule, self.policy_name, self.rule_id, @@ -239,7 +239,7 @@ def test_create_qos_dscp_marking_rule_no_qos_extension(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.create_qos_dscp_marking_rule, self.policy_name, dscp_mark=16, @@ -328,7 +328,7 @@ def test_update_qos_dscp_marking_rule_no_qos_extension(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.update_qos_dscp_marking_rule, self.policy_id, self.rule_id, @@ -403,7 +403,7 @@ def test_delete_qos_dscp_marking_rule_no_qos_extension(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.delete_qos_dscp_marking_rule, self.policy_name, self.rule_id, diff --git a/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py b/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py index a08ae90b4..ac04d8d5b 100644 --- a/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py +++ b/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py @@ -15,7 +15,7 @@ import copy -from openstack.cloud import exc +from openstack import exceptions from openstack.network.v2 import qos_minimum_bandwidth_rule from openstack.tests.unit import base @@ -148,7 +148,7 @@ def test_get_qos_minimum_bandwidth_rule_no_qos_policy_found(self): ] ) self.assertRaises( - exc.OpenStackCloudResourceNotFound, + exceptions.NotFoundException, self.cloud.get_qos_minimum_bandwidth_rule, self.policy_name, self.rule_id, @@ -168,7 +168,7 @@ def test_get_qos_minimum_bandwidth_rule_no_qos_extension(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.get_qos_minimum_bandwidth_rule, self.policy_name, self.rule_id, @@ -240,7 +240,7 @@ def test_create_qos_minimum_bandwidth_rule_no_qos_extension(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.create_qos_minimum_bandwidth_rule, self.policy_name, min_kbps=100, @@ -328,7 +328,7 @@ def test_update_qos_minimum_bandwidth_rule_no_qos_extension(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.update_qos_minimum_bandwidth_rule, self.policy_id, self.rule_id, @@ -403,7 +403,7 @@ def test_delete_qos_minimum_bandwidth_rule_no_qos_extension(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.delete_qos_minimum_bandwidth_rule, self.policy_name, self.rule_id, diff --git a/openstack/tests/unit/cloud/test_qos_policy.py b/openstack/tests/unit/cloud/test_qos_policy.py index fe0ed44ba..bec3e3bee 100644 --- a/openstack/tests/unit/cloud/test_qos_policy.py +++ b/openstack/tests/unit/cloud/test_qos_policy.py @@ -15,7 +15,7 @@ import copy -from openstack.cloud import exc +from openstack import exceptions from openstack.network.v2 import qos_policy as _policy from openstack.tests.unit import base @@ -110,7 +110,7 @@ def test_get_qos_policy_no_qos_extension(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.get_qos_policy, self.policy_name, ) @@ -154,7 +154,7 @@ def test_create_qos_policy_no_qos_extension(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.create_qos_policy, name=self.policy_name, ) @@ -256,7 +256,7 @@ def test_delete_qos_policy_no_qos_extension(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.delete_qos_policy, self.policy_name, ) @@ -330,7 +330,7 @@ def test_delete_qos_policy_multiple_found(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.delete_qos_policy, self.policy_name, ) @@ -420,7 +420,7 @@ def test_update_qos_policy_no_qos_extension(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.update_qos_policy, self.policy_id, name="goofy", diff --git a/openstack/tests/unit/cloud/test_qos_rule_type.py b/openstack/tests/unit/cloud/test_qos_rule_type.py index 0c2e1d241..374ad831d 100644 --- a/openstack/tests/unit/cloud/test_qos_rule_type.py +++ b/openstack/tests/unit/cloud/test_qos_rule_type.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from openstack.cloud import exc +from openstack import exceptions from openstack.network.v2 import qos_rule_type from openstack.tests.unit import base @@ -117,7 +117,7 @@ def test_list_qos_rule_types_no_qos_extension(self): ] ) self.assertRaises( - exc.OpenStackCloudException, self.cloud.list_qos_rule_types + exceptions.SDKException, self.cloud.list_qos_rule_types ) self.assert_calls() @@ -184,7 +184,7 @@ def test_get_qos_rule_type_details_no_qos_extension(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.get_qos_rule_type_details, self.rule_type_name, ) @@ -210,7 +210,7 @@ def test_get_qos_rule_type_details_no_qos_details_extension(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.get_qos_rule_type_details, self.rule_type_name, ) diff --git a/openstack/tests/unit/cloud/test_quotas.py b/openstack/tests/unit/cloud/test_quotas.py index 3ea399919..53b32d2bb 100644 --- a/openstack/tests/unit/cloud/test_quotas.py +++ b/openstack/tests/unit/cloud/test_quotas.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.cloud import exc +from openstack import exceptions from openstack.network.v2 import quota as _quota from openstack.tests.unit import base @@ -100,7 +100,7 @@ def test_update_quotas_bad_request(self): ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.set_compute_quotas, project.project_id, ) diff --git a/openstack/tests/unit/cloud/test_rebuild_server.py b/openstack/tests/unit/cloud/test_rebuild_server.py index cba4101db..cc96d91b7 100644 --- a/openstack/tests/unit/cloud/test_rebuild_server.py +++ b/openstack/tests/unit/cloud/test_rebuild_server.py @@ -21,7 +21,7 @@ import uuid -from openstack.cloud import exc +from openstack import exceptions from openstack.tests import fakes from openstack.tests.unit import base @@ -65,7 +65,7 @@ def test_rebuild_server_rebuild_exception(self): ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.rebuild_server, self.fake_server['id'], "a", @@ -102,7 +102,7 @@ def test_rebuild_server_server_error(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.rebuild_server, self.fake_server['id'], "a", @@ -139,7 +139,7 @@ def test_rebuild_server_timeout(self): ] ) self.assertRaises( - exc.OpenStackCloudTimeout, + exceptions.ResourceTimeout, self.cloud.rebuild_server, self.fake_server['id'], "a", diff --git a/openstack/tests/unit/cloud/test_role_assignment.py b/openstack/tests/unit/cloud/test_role_assignment.py index 5f2d465af..23b5e3d51 100644 --- a/openstack/tests/unit/cloud/test_role_assignment.py +++ b/openstack/tests/unit/cloud/test_role_assignment.py @@ -14,7 +14,7 @@ import testtools from testtools import matchers -from openstack.cloud import exc +from openstack import exceptions from openstack.tests.unit import base @@ -1739,7 +1739,7 @@ def test_grant_no_role(self): self.register_uris(uris) with testtools.ExpectedException( - exc.OpenStackCloudException, + exceptions.SDKException, 'Role {0} not found'.format(self.role_data.role_name), ): self.cloud.grant_role( @@ -1779,7 +1779,7 @@ def test_revoke_no_role(self): self.register_uris(uris) with testtools.ExpectedException( - exc.OpenStackCloudException, + exceptions.SDKException, 'Role {0} not found'.format(self.role_data.role_name), ): self.cloud.revoke_role( @@ -1795,7 +1795,7 @@ def test_grant_no_user_or_group_specified(self): ) self.register_uris(uris) with testtools.ExpectedException( - exc.OpenStackCloudException, + exceptions.SDKException, 'Must specify either a user or a group', ): self.cloud.grant_role(self.role_data.role_name) @@ -1807,7 +1807,7 @@ def test_revoke_no_user_or_group_specified(self): ) self.register_uris(uris) with testtools.ExpectedException( - exc.OpenStackCloudException, + exceptions.SDKException, 'Must specify either a user or a group', ): self.cloud.revoke_role(self.role_data.role_name) @@ -1823,7 +1823,7 @@ def test_grant_no_user_or_group(self): self.register_uris(uris) with testtools.ExpectedException( - exc.OpenStackCloudException, + exceptions.SDKException, 'Must specify either a user or a group', ): self.cloud.grant_role( @@ -1841,7 +1841,7 @@ def test_revoke_no_user_or_group(self): self.register_uris(uris) with testtools.ExpectedException( - exc.OpenStackCloudException, + exceptions.SDKException, 'Must specify either a user or a group', ): self.cloud.revoke_role( @@ -1862,7 +1862,7 @@ def test_grant_both_user_and_group(self): self.register_uris(uris) with testtools.ExpectedException( - exc.OpenStackCloudException, + exceptions.SDKException, 'Specify either a group or a user, not both', ): self.cloud.grant_role( @@ -1885,7 +1885,7 @@ def test_revoke_both_user_and_group(self): self.register_uris(uris) with testtools.ExpectedException( - exc.OpenStackCloudException, + exceptions.SDKException, 'Specify either a group or a user, not both', ): self.cloud.revoke_role( @@ -2017,7 +2017,7 @@ def test_grant_no_project_or_domain(self): self.register_uris(uris) with testtools.ExpectedException( - exc.OpenStackCloudException, + exceptions.SDKException, 'Must specify either a domain, project or system', ): self.cloud.grant_role( @@ -2036,7 +2036,7 @@ def test_revoke_no_project_or_domain_or_system(self): self.register_uris(uris) with testtools.ExpectedException( - exc.OpenStackCloudException, + exceptions.SDKException, 'Must specify either a domain, project or system', ): self.cloud.revoke_role( @@ -2063,7 +2063,7 @@ def test_grant_bad_domain_exception(self): ), ] ) - with testtools.ExpectedException(exc.OpenStackCloudURINotFound): + with testtools.ExpectedException(exceptions.NotFoundException): self.cloud.grant_role( self.role_data.role_name, user=self.user_data.name, @@ -2090,7 +2090,7 @@ def test_revoke_bad_domain_exception(self): ), ] ) - with testtools.ExpectedException(exc.OpenStackCloudURINotFound): + with testtools.ExpectedException(exceptions.NotFoundException): self.cloud.revoke_role( self.role_data.role_name, user=self.user_data.name, diff --git a/openstack/tests/unit/cloud/test_router.py b/openstack/tests/unit/cloud/test_router.py index 8ab57ac97..2d3dd6955 100644 --- a/openstack/tests/unit/cloud/test_router.py +++ b/openstack/tests/unit/cloud/test_router.py @@ -17,7 +17,7 @@ import testtools -from openstack.cloud import exc +from openstack import exceptions from openstack.network.v2 import port as _port from openstack.network.v2 import router as _router from openstack.tests.unit import base @@ -315,7 +315,7 @@ def test_create_router_with_enable_snat_False(self): def test_create_router_wrong_availability_zone_hints_type(self): azh_opts = "invalid" with testtools.ExpectedException( - exc.OpenStackCloudException, + exceptions.SDKException, "Parameter 'availability_zone_hints' must be a list", ): self.cloud.create_router( @@ -522,7 +522,7 @@ def test_delete_router_multiple_found(self): ] ) self.assertRaises( - exc.OpenStackCloudException, self.cloud.delete_router, 'mickey' + exceptions.SDKException, self.cloud.delete_router, 'mickey' ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_security_groups.py b/openstack/tests/unit/cloud/test_security_groups.py index ff010bf39..cee8b52a1 100644 --- a/openstack/tests/unit/cloud/test_security_groups.py +++ b/openstack/tests/unit/cloud/test_security_groups.py @@ -14,6 +14,7 @@ import copy import openstack.cloud +from openstack import exceptions from openstack.tests import fakes from openstack.tests.unit import base @@ -786,7 +787,7 @@ def test_nova_egress_security_group_rule(self): ] ) self.assertRaises( - openstack.cloud.OpenStackCloudException, + exceptions.SDKException, self.cloud.create_security_group_rule, secgroup_name_or_id='nova-sec-group', direction='egress', diff --git a/openstack/tests/unit/cloud/test_server_delete_metadata.py b/openstack/tests/unit/cloud/test_server_delete_metadata.py index 0183ae182..3e98bfdde 100644 --- a/openstack/tests/unit/cloud/test_server_delete_metadata.py +++ b/openstack/tests/unit/cloud/test_server_delete_metadata.py @@ -19,7 +19,7 @@ import uuid -from openstack.cloud.exc import OpenStackCloudURINotFound +from openstack import exceptions from openstack.tests import fakes from openstack.tests.unit import base @@ -65,7 +65,7 @@ def test_server_delete_metadata_with_exception(self): ) self.assertRaises( - OpenStackCloudURINotFound, + exceptions.NotFoundException, self.cloud.delete_server_metadata, self.server_name, ['key'], diff --git a/openstack/tests/unit/cloud/test_server_set_metadata.py b/openstack/tests/unit/cloud/test_server_set_metadata.py index 75bc0facd..974509949 100644 --- a/openstack/tests/unit/cloud/test_server_set_metadata.py +++ b/openstack/tests/unit/cloud/test_server_set_metadata.py @@ -19,7 +19,7 @@ import uuid -from openstack.cloud.exc import OpenStackCloudBadRequest +from openstack import exceptions from openstack.tests import fakes from openstack.tests.unit import base @@ -59,7 +59,7 @@ def test_server_set_metadata_with_exception(self): ) self.assertRaises( - OpenStackCloudBadRequest, + exceptions.BadRequestException, self.cloud.set_server_metadata, self.server_name, {'meta': 'data'}, diff --git a/openstack/tests/unit/cloud/test_services.py b/openstack/tests/unit/cloud/test_services.py index ee37389b2..d9d2e94e8 100644 --- a/openstack/tests/unit/cloud/test_services.py +++ b/openstack/tests/unit/cloud/test_services.py @@ -21,7 +21,7 @@ from testtools import matchers -from openstack.cloud.exc import OpenStackCloudException +from openstack import exceptions from openstack.tests.unit import base @@ -203,7 +203,7 @@ def test_get_service(self): # Multiple matches # test we are getting an Exception self.assertRaises( - OpenStackCloudException, + exceptions.SDKException, self.cloud.get_service, name_or_id=None, filters={'type': 'type2'}, diff --git a/openstack/tests/unit/cloud/test_stack.py b/openstack/tests/unit/cloud/test_stack.py index 0e2717bdb..0246124f1 100644 --- a/openstack/tests/unit/cloud/test_stack.py +++ b/openstack/tests/unit/cloud/test_stack.py @@ -14,7 +14,7 @@ import testtools -import openstack.cloud +from openstack import exceptions from openstack.orchestration.v1 import stack from openstack.tests import fakes from openstack.tests.unit import base @@ -95,9 +95,7 @@ def test_list_stacks_exception(self): ) ] ) - with testtools.ExpectedException( - openstack.cloud.OpenStackCloudURINotFound - ): + with testtools.ExpectedException(exceptions.NotFoundException): self.cloud.list_stacks() self.assert_calls() @@ -160,9 +158,7 @@ def test_search_stacks_exception(self): ) ] ) - with testtools.ExpectedException( - openstack.cloud.OpenStackCloudURINotFound - ): + with testtools.ExpectedException(exceptions.NotFoundException): self.cloud.search_stacks() def test_delete_stack(self): @@ -264,9 +260,7 @@ def test_delete_stack_exception(self): ), ] ) - with testtools.ExpectedException( - openstack.cloud.OpenStackCloudBadRequest - ): + with testtools.ExpectedException(exceptions.BadRequestException): self.cloud.delete_stack(self.stack_id) self.assert_calls() @@ -550,9 +544,7 @@ def test_delete_stack_wait_failed(self): ] ) - with testtools.ExpectedException( - openstack.cloud.OpenStackCloudException - ): + with testtools.ExpectedException(exceptions.SDKException): self.cloud.delete_stack(self.stack_id, wait=True) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_subnet.py b/openstack/tests/unit/cloud/test_subnet.py index 02925d686..0005a1969 100644 --- a/openstack/tests/unit/cloud/test_subnet.py +++ b/openstack/tests/unit/cloud/test_subnet.py @@ -17,7 +17,7 @@ import testtools -from openstack.cloud import exc +from openstack import exceptions from openstack.network.v2 import subnet as _subnet from openstack.tests.unit import base @@ -253,7 +253,7 @@ def test_create_subnet_bad_ip_version(self): ] ) with testtools.ExpectedException( - exc.OpenStackCloudException, "ip_version must be an integer" + exceptions.SDKException, "ip_version must be an integer" ): self.cloud.create_subnet( self.network_name, self.subnet_cidr, ip_version='4x' @@ -407,7 +407,7 @@ def test_create_subnet_conflict_gw_ops(self): ) gateway = '192.168.200.3' self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.create_subnet, 'kooky', self.subnet_cidr, @@ -441,7 +441,7 @@ def test_create_subnet_bad_network(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.create_subnet, 'duck', self.subnet_cidr, @@ -475,7 +475,7 @@ def test_create_subnet_non_unique_network(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.create_subnet, self.network_name, self.subnet_cidr, @@ -731,7 +731,7 @@ def test_delete_subnet_multiple_found(self): ] ) self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.delete_subnet, self.subnet_name, ) @@ -859,7 +859,7 @@ def test_update_subnet_disable_gateway_ip(self): def test_update_subnet_conflict_gw_ops(self): self.assertRaises( - exc.OpenStackCloudException, + exceptions.SDKException, self.cloud.update_subnet, self.subnet_id, gateway_ip="192.168.199.3", diff --git a/openstack/tests/unit/cloud/test_update_server.py b/openstack/tests/unit/cloud/test_update_server.py index 88c61b32b..7f800b4ab 100644 --- a/openstack/tests/unit/cloud/test_update_server.py +++ b/openstack/tests/unit/cloud/test_update_server.py @@ -19,7 +19,7 @@ import uuid -from openstack.cloud.exc import OpenStackCloudException +from openstack import exceptions from openstack.tests import fakes from openstack.tests.unit import base @@ -74,7 +74,7 @@ def test_update_server_with_update_exception(self): ] ) self.assertRaises( - OpenStackCloudException, + exceptions.SDKException, self.cloud.update_server, self.server_name, name=self.updated_server_name, diff --git a/openstack/tests/unit/cloud/test_users.py b/openstack/tests/unit/cloud/test_users.py index 29620591d..de5a12b27 100644 --- a/openstack/tests/unit/cloud/test_users.py +++ b/openstack/tests/unit/cloud/test_users.py @@ -14,7 +14,7 @@ import testtools -import openstack.cloud +from openstack import exceptions from openstack.tests.unit import base @@ -83,7 +83,7 @@ def test_create_user_v3_no_domain(self): domain_id=uuid.uuid4().hex, email='test@example.com' ) with testtools.ExpectedException( - openstack.cloud.OpenStackCloudException, + exceptions.SDKException, "User or project creation requires an explicit" " domain_id argument.", ): diff --git a/openstack/tests/unit/cloud/test_volume.py b/openstack/tests/unit/cloud/test_volume.py index 2cd42dd2e..aab503b54 100644 --- a/openstack/tests/unit/cloud/test_volume.py +++ b/openstack/tests/unit/cloud/test_volume.py @@ -14,9 +14,9 @@ import testtools from openstack.block_storage.v3 import volume -import openstack.cloud from openstack.cloud import meta from openstack.compute.v2 import volume_attachment +from openstack import exceptions from openstack.tests import fakes from openstack.tests.unit import base @@ -105,7 +105,7 @@ def test_attach_volume_exception(self): ] ) with testtools.ExpectedException( - openstack.cloud.OpenStackCloudURINotFound + exceptions.NotFoundException, ): self.cloud.attach_volume(server, volume, wait=False) self.assert_calls() @@ -225,7 +225,7 @@ def test_attach_volume_wait_error(self): ] ) - with testtools.ExpectedException(openstack.exceptions.ResourceFailure): + with testtools.ExpectedException(exceptions.ResourceFailure): self.cloud.attach_volume(server, volume) self.assert_calls() @@ -234,7 +234,7 @@ def test_attach_volume_not_available(self): volume = dict(id='volume001', status='error', attachments=[]) with testtools.ExpectedException( - openstack.cloud.OpenStackCloudException, + exceptions.SDKException, "Volume %s is not available. Status is '%s'" % (volume['id'], volume['status']), ): @@ -250,7 +250,7 @@ def test_attach_volume_already_attached(self): ) with testtools.ExpectedException( - openstack.cloud.OpenStackCloudException, + exceptions.SDKException, "Volume %s already attached to server %s on device %s" % (volume['id'], server['id'], device_id), ): @@ -324,7 +324,7 @@ def test_detach_volume_exception(self): ] ) with testtools.ExpectedException( - openstack.cloud.OpenStackCloudURINotFound + exceptions.NotFoundException, ): self.cloud.detach_volume(server, volume, wait=False) self.assert_calls() @@ -433,7 +433,7 @@ def test_detach_volume_wait_error(self): ), ] ) - with testtools.ExpectedException(openstack.exceptions.ResourceFailure): + with testtools.ExpectedException(exceptions.ResourceFailure): self.cloud.detach_volume(server, volume) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_volume_access.py b/openstack/tests/unit/cloud/test_volume_access.py index 43cbb1df1..6c33842e3 100644 --- a/openstack/tests/unit/cloud/test_volume_access.py +++ b/openstack/tests/unit/cloud/test_volume_access.py @@ -12,10 +12,9 @@ # License for the specific language governing permissions and limitations # under the License. - import testtools -import openstack.cloud +from openstack import exceptions from openstack.tests.unit import base @@ -297,7 +296,7 @@ def test_add_volume_type_access_missing(self): ] ) with testtools.ExpectedException( - openstack.cloud.OpenStackCloudException, + exceptions.SDKException, "VolumeType not found: MISSING", ): self.cloud.add_volume_type_access( From 7fa802ca7fd32c9385d476f0713191f6f2bc4153 Mon Sep 17 00:00:00 2001 From: gtema Date: Tue, 7 Nov 2023 11:06:33 +0100 Subject: [PATCH 3420/3836] Enforce endpoint override for services without discovery Services without version discovery implemented are tricky. When user is not using endpoint_override for those there are sporadic and often attempts to perform version discovery on every request sent [1]. It could be prevented as much as possible on SDK side during proxy initialization, but when a request is sent to the proxy this is still happening inside the keystonauth library. Forcing endpoint_override for those help avoiding that. Touching cloud_region triggers mypy complain, so change urllib import to keep it happy. [1] https://lists.openstack.org/archives/list/openstack-discuss@lists.openstack.org/thread/GBBHV2LN5S3HEUQHUQGZZ3UFEVRHQBOS/ Change-Id: I1956dfaac9ba8e3f9234cc98a36b20a330dc88f6 --- openstack/config/cloud_region.py | 8 ++++---- openstack/service_description.py | 30 +++++++++++++++++++++++++++--- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index ee16dd612..4ba3daa57 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -15,7 +15,7 @@ import copy import os.path import typing as ty -import urllib +from urllib import parse import warnings try: @@ -344,7 +344,7 @@ def __ne__(self, other): def name(self): if self._name is None: try: - self._name = urllib.parse.urlparse( + self._name = parse.urlparse( self.get_session().auth.auth_url ).hostname except Exception: @@ -598,7 +598,7 @@ def get_endpoint_from_catalog( interface=interface, region_name=region_name, ) - except keystoneauth1.exceptions.catalog.EndpointNotFound: + except (keystoneauth1.exceptions.catalog.EndpointNotFound, ValueError): return None def get_connect_retries(self, service_type): @@ -813,7 +813,7 @@ def _get_hardcoded_endpoint(self, service_type, constructor): if not endpoint.rstrip().rsplit('/')[-1] == 'v2.0': if not endpoint.endswith('/'): endpoint += '/' - endpoint = urllib.parse.urljoin(endpoint, 'v2.0') + endpoint = parse.urljoin(endpoint, 'v2.0') return endpoint def get_session_client( diff --git a/openstack/service_description.py b/openstack/service_description.py index eab27a88f..efe1e783b 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -248,12 +248,36 @@ def _make_proxy(self, instance): # Make an adapter to let discovery take over version_kwargs = {} + supported_versions = sorted([int(f) for f in self.supported_versions]) if version_string: version_kwargs['version'] = version_string + if getattr( + self.supported_versions[str(supported_versions[0])], + 'skip_discovery', + False, + ): + # Requested service does not support version discovery + # In this case it is more efficient to set the + # endpoint_override to the current catalog endpoint value, + # otherwise next request will try to perform discovery. + + temp_adapter = config.get_session_client(self.service_type) + ep_override = temp_adapter.get_endpoint(skip_discovery=True) + + ep_key = '{service_type}_endpoint_override'.format( + service_type=self.service_type.replace('-', '_') + ) + config.config[ep_key] = ep_override + + return config.get_session_client( + self.service_type, + allow_version_hack=True, + constructor=self.supported_versions[ + str(supported_versions[0]) + ], + version=version_string, + ) else: - supported_versions = sorted( - [int(f) for f in self.supported_versions] - ) version_kwargs['min_version'] = str(supported_versions[0]) version_kwargs['max_version'] = '{version}.latest'.format( version=str(supported_versions[-1]) From a03396d3479a4341d0143f3e72729a06eb45ea53 Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Wed, 8 Nov 2023 08:29:45 +1300 Subject: [PATCH 3421/3836] Return the results of init attachment request Methods ``openstack.block_storage.v3.volume.Volume.init_attachment`` and ``block_storage.init_volume_attachment`` now return the results of the POST request instead of None. This replicates the behaviour of cinderclient; the returned data is used by nova and ironic for managing volume attachments. Change-Id: I55ad94c872e807668b85125b32f142c3a8cf40bf --- openstack/block_storage/v3/_proxy.py | 4 ++-- openstack/block_storage/v3/volume.py | 3 ++- openstack/tests/unit/block_storage/v3/test_volume.py | 9 ++++++++- .../block-storage-init-return-95b465b4755f03ca.yaml | 7 +++++++ 4 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/block-storage-init-return-95b465b4755f03ca.yaml diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index f076b7b8c..0b7ff890b 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -948,9 +948,9 @@ def init_volume_attachment(self, volume, connector): :class:`~openstack.block_storage.v3.volume.Volume` instance. :param dict connector: The connector object. - :returns: None""" + :returns: Dictionary containing the modified connector object""" volume = self._get_resource(_volume.Volume, volume) - volume.init_attachment(self, connector) + return volume.init_attachment(self, connector) def terminate_volume_attachment(self, volume, connector): """Update volume status to 'in-use'. diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index eb412c40e..dd14b07d9 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -290,7 +290,8 @@ def init_attachment(self, session, connector): """Initialize volume attachment""" body = {'os-initialize_connection': {'connector': connector}} - self._action(session, body) + resp = self._action(session, body).json() + return resp['connection_info'] def terminate_attachment(self, session, connector): """Terminate volume attachment""" diff --git a/openstack/tests/unit/block_storage/v3/test_volume.py b/openstack/tests/unit/block_storage/v3/test_volume.py index 7e162252c..0a2edbd67 100644 --- a/openstack/tests/unit/block_storage/v3/test_volume.py +++ b/openstack/tests/unit/block_storage/v3/test_volume.py @@ -545,7 +545,14 @@ def test_abort_detaching(self): def test_init_attachment(self): sot = volume.Volume(**VOLUME) - self.assertIsNone(sot.init_attachment(self.sess, {'a': 'b'})) + self.resp = mock.Mock() + self.resp.body = {'connection_info': {'c': 'd'}} + self.resp.status_code = 200 + self.resp.json = mock.Mock(return_value=self.resp.body) + self.sess.post = mock.Mock(return_value=self.resp) + self.assertEqual( + {'c': 'd'}, sot.init_attachment(self.sess, {'a': 'b'}) + ) url = 'volumes/%s/action' % FAKE_ID body = {'os-initialize_connection': {'connector': {'a': 'b'}}} diff --git a/releasenotes/notes/block-storage-init-return-95b465b4755f03ca.yaml b/releasenotes/notes/block-storage-init-return-95b465b4755f03ca.yaml new file mode 100644 index 000000000..6650ac89a --- /dev/null +++ b/releasenotes/notes/block-storage-init-return-95b465b4755f03ca.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Methods ``openstack.block_storage.v3.volume.Volume.init_attachment`` and + ``block_storage.init_volume_attachment`` now return the results of the POST + request instead of None. This replicates the behaviour of cinderclient; the + returned data is used by nova and ironic for managing volume attachments. \ No newline at end of file From 478f5b9c46b959dee8cdfe86e60ff60d3e4bfc42 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 26 Jul 2023 14:32:15 +0100 Subject: [PATCH 3422/3836] mypy: Address issues with openstack.compute Change-Id: Iabc2f7dafb1254395a6249cdfcda6fd444941812 Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 1 - openstack/compute/v2/server.py | 24 +++++++++++++----------- setup.cfg | 1 - 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e3f7d939d..7d293eadf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -59,7 +59,6 @@ repos: | openstack/block_storage/.* | openstack/cloud/.* | openstack/clustering/.* - | openstack/compute/.* | openstack/container_infrastructure_management/.* | openstack/database/.* | openstack/dns/.* diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 2622e7c3b..817924618 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -9,6 +9,9 @@ # 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 typing as ty + from openstack.common import metadata from openstack.common import tag from openstack.compute.v2 import flavor @@ -474,14 +477,13 @@ def create_image(self, session, name, metadata=None): microversion = '2.45' response = self._action(session, body, microversion) - body = None try: - # There might be body, might be not - body = response.json() + # There might be a body, there might not be + response_body = response.json() except Exception: - pass - if body and 'image_id' in body: - image_id = body['image_id'] + response_body = None + if response_body and 'image_id' in response_body: + image_id = response_body['image_id'] else: image_id = response.headers['Location'].rsplit('/', 1)[1] @@ -634,7 +636,7 @@ def lock(self, session, locked_reason=None): :param locked_reason: The reason for locking the server. :returns: None """ - body = {"lock": None} + body: ty.Dict[str, ty.Any] = {"lock": None} if locked_reason is not None: body["lock"] = { "locked_reason": locked_reason, @@ -662,7 +664,7 @@ def rescue(self, session, admin_pass=None, image_ref=None): provided, the server will use the existing image. (Optional) :returns: None """ - body = {"rescue": {}} + body: ty.Dict[str, ty.Any] = {"rescue": {}} if admin_pass is not None: body["rescue"]["adminPass"] = admin_pass if image_ref is not None: @@ -690,7 +692,7 @@ def evacuate(self, session, host=None, admin_pass=None, force=None): :param force: Whether to force evacuation. :returns: None """ - body = {"evacuate": {}} + body: ty.Dict[str, ty.Any] = {"evacuate": {}} if host is not None: body["evacuate"]["host"] = host if admin_pass is not None: @@ -795,7 +797,7 @@ def get_console_output(self, session, length=None): (Optional) :returns: None """ - body = {"os-getConsoleOutput": {}} + body: ty.Dict[str, ty.Any] = {"os-getConsoleOutput": {}} if length is not None: body["os-getConsoleOutput"]["length"] = length resp = self._action(session, body) @@ -907,7 +909,7 @@ def _live_migrate( disk_over_commit, ): microversion = None - body = { + body: ty.Dict[str, ty.Any] = { 'host': None, } if block_migration == 'auto': diff --git a/setup.cfg b/setup.cfg index 50074a951..99d518f09 100644 --- a/setup.cfg +++ b/setup.cfg @@ -52,7 +52,6 @@ exclude = (?x)( | openstack/block_storage | openstack/cloud | openstack/clustering - | openstack/compute | openstack/container_infrastructure_management | openstack/database | openstack/dns From c212f3f6afc69b25ff12de55846bb4da8472bb1e Mon Sep 17 00:00:00 2001 From: Seungju Baek Date: Tue, 22 Aug 2023 00:39:41 +0900 Subject: [PATCH 3423/3836] Implemented heat 'stack suspend' and 'stack resume' function Added suspend_stack function to support stack resources suspend. Added resume_stack function to support stack resources resume. Also implemented functional test and unit test code. Change-Id: Idf4befef149e945a419a3434886a8ba5c76481c6 --- doc/source/user/proxies/orchestration.rst | 2 +- openstack/orchestration/v1/_proxy.py | 20 +++++++++++ openstack/orchestration/v1/stack.py | 18 ++++++++++ .../functional/orchestration/v1/test_stack.py | 23 ++++++++++++ .../tests/unit/orchestration/v1/test_proxy.py | 16 +++++++++ .../tests/unit/orchestration/v1/test_stack.py | 36 +++++++++++++++++++ ...k-suspend-and-resume-26d4fc5904291d5d.yaml | 4 +++ 7 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-stack-suspend-and-resume-26d4fc5904291d5d.yaml diff --git a/doc/source/user/proxies/orchestration.rst b/doc/source/user/proxies/orchestration.rst index 86662b7b1..2e8721b31 100644 --- a/doc/source/user/proxies/orchestration.rst +++ b/doc/source/user/proxies/orchestration.rst @@ -18,7 +18,7 @@ Stack Operations .. autoclass:: openstack.orchestration.v1._proxy.Proxy :noindex: - :members: create_stack, check_stack, update_stack, delete_stack, find_stack, + :members: create_stack, check_stack, update_stack, delete_stack, find_stack, suspend_stack, resume_stack, get_stack, get_stack_environment, get_stack_files, get_stack_template, stacks, validate_template, resources, export_stack diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index 9ada733e7..908c73fa6 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -240,6 +240,26 @@ def export_stack(self, stack): obj = self._find(_stack.Stack, stack, ignore_missing=False) return obj.export(self) + def suspend_stack(self, stack): + """Suspend a stack status + + :param stack: The value can be either the ID of a stack or an instance + of :class:`~openstack.orchestration.v1.stack.Stack`. + :returns: ``None`` + """ + res = self._get_resource(_stack.Stack, stack) + res.suspend(self) + + def resume_stack(self, stack): + """Resume a stack status + + :param stack: The value can be either the ID of a stack or an instance + of :class:`~openstack.orchestration.v1.stack.Stack`. + :returns: ``None`` + """ + res = self._get_resource(_stack.Stack, stack) + res.resume(self) + def get_stack_template(self, stack): """Get template used by a stack diff --git a/openstack/orchestration/v1/stack.py b/openstack/orchestration/v1/stack.py index a62e0017e..d7871e53c 100644 --- a/openstack/orchestration/v1/stack.py +++ b/openstack/orchestration/v1/stack.py @@ -192,6 +192,24 @@ def export(self, session): exceptions.raise_from_response(resp) return resp.json() + def suspend(self, session): + """Suspend a stack + + :param session: The session to use for making this request + :returns: None + """ + body = {"suspend": None} + self._action(session, body) + + def resume(self, session): + """Resume a stack + + :param session: The session to use for making this request + :returns: None + """ + body = {"resume": None} + self._action(session, body) + def fetch( self, session, diff --git a/openstack/tests/functional/orchestration/v1/test_stack.py b/openstack/tests/functional/orchestration/v1/test_stack.py index 4abb3a556..bc561da35 100644 --- a/openstack/tests/functional/orchestration/v1/test_stack.py +++ b/openstack/tests/functional/orchestration/v1/test_stack.py @@ -80,3 +80,26 @@ def tearDown(self): def test_list(self): names = [o.name for o in self.conn.orchestration.stacks()] self.assertIn(self.NAME, names) + + def test_suspend_resume(self): + # given + suspend_status = "SUSPEND_COMPLETE" + resume_status = "RESUME_COMPLETE" + + # when + self.conn.orchestration.suspend_stack(self.stack) + sot = self.conn.orchestration.wait_for_status( + self.stack, suspend_status, wait=self._wait_for_timeout + ) + + # then + self.assertEqual(suspend_status, sot.status) + + # when + self.conn.orchestration.resume_stack(self.stack) + sot = self.conn.orchestration.wait_for_status( + self.stack, resume_status, wait=self._wait_for_timeout + ) + + # then + self.assertEqual(resume_status, sot.status) diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index 0e3af53df..583d64b65 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -150,6 +150,22 @@ def test_export_stack_with_object(self): expected_args=[self.proxy], ) + def test_suspend_stack(self): + self._verify( + 'openstack.orchestration.v1.stack.Stack.suspend', + self.proxy.suspend_stack, + method_args=['stack'], + expected_args=[self.proxy], + ) + + def test_resume_stack(self): + self._verify( + 'openstack.orchestration.v1.stack.Stack.resume', + self.proxy.resume_stack, + method_args=['stack'], + expected_args=[self.proxy], + ) + def test_delete_stack(self): self.verify_delete(self.proxy.delete_stack, stack.Stack, False) diff --git a/openstack/tests/unit/orchestration/v1/test_stack.py b/openstack/tests/unit/orchestration/v1/test_stack.py index 1a0bd8f96..a6b7bcb61 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack.py +++ b/openstack/tests/unit/orchestration/v1/test_stack.py @@ -340,3 +340,39 @@ def test_update_preview(self): FAKE_UPDATE_PREVIEW_RESPONSE['unchanged'], ret.unchanged ) self.assertEqual(FAKE_UPDATE_PREVIEW_RESPONSE['updated'], ret.updated) + + def test_suspend(self): + sess = mock.Mock() + + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.headers = {} + mock_response.json.return_value = {} + sess.post = mock.Mock(return_value=mock_response) + url = "stacks/%s/actions" % FAKE_ID + body = {"suspend": None} + sot = stack.Stack(**FAKE) + + res = sot.suspend(sess) + + self.assertIsNone(res) + sess.post.assert_called_with(url, json=body, microversion=None) + + def test_resume(self): + sess = mock.Mock() + + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.headers = {} + mock_response.json.return_value = {} + sess.post = mock.Mock(return_value=mock_response) + url = "stacks/%s/actions" % FAKE_ID + + body = {"resume": None} + + sot = stack.Stack(**FAKE) + + res = sot.resume(sess) + + self.assertIsNone(res) + sess.post.assert_called_with(url, json=body, microversion=None) diff --git a/releasenotes/notes/add-stack-suspend-and-resume-26d4fc5904291d5d.yaml b/releasenotes/notes/add-stack-suspend-and-resume-26d4fc5904291d5d.yaml new file mode 100644 index 000000000..fa3eecd46 --- /dev/null +++ b/releasenotes/notes/add-stack-suspend-and-resume-26d4fc5904291d5d.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Adds ``suspend_stack`` and ``resume_stack`` to support stack non-lifecycle operations. \ No newline at end of file From 1a07c9d1730d8f004b809cae54d067bfaf79e7c3 Mon Sep 17 00:00:00 2001 From: suheoon Date: Sun, 27 Aug 2023 01:26:14 +0900 Subject: [PATCH 3424/3836] Implement heat 'stack event list' Add stack_events function to support retrieving stack events Add StackEvents Class to support retrieving stack events Also implement unit test code Change-Id: Ie55fd6ed85f9871c5ddb06e90e8efb0dcbc90ef9 --- doc/source/user/proxies/orchestration.rst | 7 +++ .../user/resources/orchestration/index.rst | 1 + .../orchestration/v1/stack_event.rst | 12 ++++ openstack/orchestration/v1/_proxy.py | 36 ++++++++++++ openstack/orchestration/v1/stack_event.py | 53 +++++++++++++++++ .../tests/unit/orchestration/v1/test_proxy.py | 58 +++++++++++++++++++ .../unit/orchestration/v1/test_stack_event.py | 53 +++++++++++++++++ .../add-stack-events-b8674d7bb657e789.yaml | 5 ++ 8 files changed, 225 insertions(+) create mode 100644 doc/source/user/resources/orchestration/v1/stack_event.rst create mode 100644 openstack/orchestration/v1/stack_event.py create mode 100644 openstack/tests/unit/orchestration/v1/test_stack_event.py create mode 100644 releasenotes/notes/add-stack-events-b8674d7bb657e789.yaml diff --git a/doc/source/user/proxies/orchestration.rst b/doc/source/user/proxies/orchestration.rst index 2e8721b31..c9f3fa41f 100644 --- a/doc/source/user/proxies/orchestration.rst +++ b/doc/source/user/proxies/orchestration.rst @@ -23,6 +23,13 @@ Stack Operations get_stack_template, stacks, validate_template, resources, export_stack +Stack Event Operations +^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.orchestration.v1._proxy.Proxy + :noindex: + :members: stack_events + Software Configuration Operations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/orchestration/index.rst b/doc/source/user/resources/orchestration/index.rst index 0af426b4a..d0c53f6b8 100644 --- a/doc/source/user/resources/orchestration/index.rst +++ b/doc/source/user/resources/orchestration/index.rst @@ -6,3 +6,4 @@ Orchestration Resources v1/stack v1/resource + v1/stack_event diff --git a/doc/source/user/resources/orchestration/v1/stack_event.rst b/doc/source/user/resources/orchestration/v1/stack_event.rst new file mode 100644 index 000000000..8838f44f4 --- /dev/null +++ b/doc/source/user/resources/orchestration/v1/stack_event.rst @@ -0,0 +1,12 @@ +openstack.orchestration.v1.stack_event +====================================== + +.. automodule:: openstack.orchestration.v1.stack_event + +The StackEvent Class +-------------------- + +The ``StackEvent`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.orchestration.v1.stack_event.StackEvent + :members: diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index 908c73fa6..d68cee8e1 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -17,6 +17,7 @@ from openstack.orchestration.v1 import software_deployment as _sd from openstack.orchestration.v1 import stack as _stack from openstack.orchestration.v1 import stack_environment as _stack_environment +from openstack.orchestration.v1 import stack_event as _stack_event from openstack.orchestration.v1 import stack_files as _stack_files from openstack.orchestration.v1 import stack_template as _stack_template from openstack.orchestration.v1 import template as _template @@ -608,3 +609,38 @@ def _service_cleanup( for stack in stacks: self.wait_for_delete(stack) + + def stack_events(self, stack, resource_name=None, **attr): + """Get a stack events + + :param stack: The value can be the ID of a stack or an instance of + :class:`~openstack.orchestration.v1.stack.Stack` + :param resource_name: The name of resource. If the resource_name is not None, + the base_path changes. + + :returns: A generator of stack_events objects + :rtype: :class:`~openstack.orchestration.v1.stack_event.StackEvent` + """ + + if isinstance(stack, _stack.Stack): + obj = stack + else: + obj = self._get(_stack.Stack, stack) + + if resource_name: + base_path = '/stacks/%(stack_name)s/%(stack_id)s/resources/%(resource_name)s/events' + return self._list( + _stack_event.StackEvent, + stack_name=obj.name, + stack_id=obj.id, + resource_name=resource_name, + base_path=base_path, + **attr + ) + + return self._list( + _stack_event.StackEvent, + stack_name=obj.name, + stack_id=obj.id, + **attr + ) diff --git a/openstack/orchestration/v1/stack_event.py b/openstack/orchestration/v1/stack_event.py new file mode 100644 index 000000000..640659e6a --- /dev/null +++ b/openstack/orchestration/v1/stack_event.py @@ -0,0 +1,53 @@ +# 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 resource + + +class StackEvent(resource.Resource): + base_path = '/stacks/%(stack_name)s/%(stack_id)s/events' + resources_key = 'events' + + # capabilities + allow_create = False + allow_list = True + allow_fetch = True + allow_delete = False + allow_commit = False + + _query_mapping = resource.QueryParameters( + "resource_action", + "resource_status", + "resource_name", + "resource_type", + "nested_depth", + "sort_key", + "sort_dir", + ) + + # Properties + #: The date and time when the event was created + event_time = resource.Body('event_time') + #: The ID of the event object + id = resource.Body('id') + #: A list of dictionaries containing links relevant to the stack. + links = resource.Body('links') + #: The ID of the logical stack resource. + logical_resource_id = resource.Body('logical_resource_id') + #: The ID of the stack physical resource. + physical_resource_id = resource.Body('physical_resource_id') + #: The name of the resource. + resource_name = resource.Body('resource_name') + #: The status of the resource. + resource_status = resource.Body('resource_status') + #: The reason for the current stack resource state. + resource_status_reason = resource.Body('resource_status_reason') diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index 583d64b65..0e8206085 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -21,9 +21,11 @@ from openstack.orchestration.v1 import software_deployment as sd from openstack.orchestration.v1 import stack from openstack.orchestration.v1 import stack_environment +from openstack.orchestration.v1 import stack_event from openstack.orchestration.v1 import stack_files from openstack.orchestration.v1 import stack_template from openstack.orchestration.v1 import template +from openstack import proxy from openstack.tests.unit import test_proxy_base @@ -498,3 +500,59 @@ class TestExtractName(TestOrchestrationProxy): def test_extract_name(self): results = self.proxy._extract_name(self.url) self.assertEqual(self.parts, results) + + +class TestOrchestrationStackEvents(TestOrchestrationProxy): + def test_stack_events_with_stack_object(self): + stack_id = '1234' + stack_name = 'test_stack' + stk = stack.Stack(id=stack_id, name=stack_name) + + self._verify( + 'openstack.proxy.Proxy._list', + self.proxy.stack_events, + method_args=[stk], + expected_args=[stack_event.StackEvent], + expected_kwargs={ + 'stack_name': stack_name, + 'stack_id': stack_id, + }, + ) + + @mock.patch.object(proxy.Proxy, '_get') + def test_stack_events_with_stack_id(self, mock_get): + stack_id = '1234' + stack_name = 'test_stack' + stk = stack.Stack(id=stack_id, name=stack_name) + mock_get.return_value = stk + + self._verify( + 'openstack.proxy.Proxy._list', + self.proxy.stack_events, + method_args=[stk], + expected_args=[stack_event.StackEvent], + expected_kwargs={ + 'stack_name': stack_name, + 'stack_id': stack_id, + }, + ) + + def test_stack_events_with_resource_name(self): + stack_id = '1234' + stack_name = 'test_stack' + resource_name = 'id' + base_path = '/stacks/%(stack_name)s/%(stack_id)s/resources/%(resource_name)s/events' + stk = stack.Stack(id=stack_id, name=stack_name) + + self._verify( + 'openstack.proxy.Proxy._list', + self.proxy.stack_events, + method_args=[stk, resource_name], + expected_args=[stack_event.StackEvent], + expected_kwargs={ + 'stack_name': stack_name, + 'stack_id': stack_id, + 'resource_name': resource_name, + 'base_path': base_path, + }, + ) diff --git a/openstack/tests/unit/orchestration/v1/test_stack_event.py b/openstack/tests/unit/orchestration/v1/test_stack_event.py new file mode 100644 index 000000000..5c5057287 --- /dev/null +++ b/openstack/tests/unit/orchestration/v1/test_stack_event.py @@ -0,0 +1,53 @@ +# 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.orchestration.v1 import stack_event +from openstack.tests.unit import base + + +FAKE_ID = 'ce8ae86c-9810-4cb1-8888-7fb53bc523bf' +FAKE_NAME = 'test_stack' +FAKE = { + 'event_time': '2015-03-09T12:15:57.233772', + 'id': FAKE_ID, + 'links': [{'href': 'stacks/%s/%s' % (FAKE_NAME, FAKE_ID), 'rel': 'self'}], + 'logical_resource_id': 'my_test_group', + 'physical_resource_id': 'my_test_group', + 'resource_name': 'my_test_resource', + 'resource_status': 'CREATE_IN_PROGRESS', + 'resource_status_reason': 'state changed', +} + + +class TestStackEvent(base.TestCase): + def test_basic(self): + sot = stack_event.StackEvent() + self.assertFalse(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = stack_event.StackEvent(**FAKE) + self.assertEqual(FAKE['event_time'], sot.event_time) + self.assertEqual(FAKE['id'], sot.id) + self.assertEqual(FAKE['links'], sot.links) + self.assertEqual(FAKE['logical_resource_id'], sot.logical_resource_id) + self.assertEqual( + FAKE['physical_resource_id'], sot.physical_resource_id + ) + self.assertEqual(FAKE['resource_name'], sot.resource_name) + self.assertEqual(FAKE['resource_status'], sot.resource_status) + self.assertEqual( + FAKE['resource_status_reason'], sot.resource_status_reason + ) diff --git a/releasenotes/notes/add-stack-events-b8674d7bb657e789.yaml b/releasenotes/notes/add-stack-events-b8674d7bb657e789.yaml new file mode 100644 index 000000000..cdc9367f0 --- /dev/null +++ b/releasenotes/notes/add-stack-events-b8674d7bb657e789.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + The ``stack_events`` method and ``StackEvent`` Class have been + added to retrieve stack events \ No newline at end of file From 2e843536600118579254eb4832b0552c4b016e1d Mon Sep 17 00:00:00 2001 From: likui Date: Mon, 9 Oct 2023 10:41:41 +0800 Subject: [PATCH 3425/3836] Remove importlib-metadata from requirements We don't need it anymore as we don't support python < 3.8 Also it was removed from global requirements so it breaks the requirements check. Change-Id: I9a053e4a2fd11b3cfcf2d9de6ba6441c7d5b0dc2 --- openstack/connection.py | 7 +------ requirements.txt | 1 - 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/openstack/connection.py b/openstack/connection.py index 9055077a2..c993dbdd7 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -201,15 +201,10 @@ """ import atexit import concurrent.futures +import importlib.metadata as importlib_metadata import warnings import weakref -try: - # For python 3.8 and later - import importlib.metadata as importlib_metadata -except ImportError: - # For everyone else - import importlib_metadata # type: ignore import keystoneauth1.exceptions import requestsexceptions diff --git a/requirements.txt b/requirements.txt index 25fb9a198..ad1c7e95a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ cryptography>=2.7 # BSD/Apache-2.0 decorator>=4.4.1 # BSD dogpile.cache>=0.6.5 # BSD -importlib_metadata>=1.7.0;python_version<'3.8' # Apache-2.0 iso8601>=0.1.11 # MIT jmespath>=0.9.0 # MIT jsonpatch!=1.20,>=1.16 # BSD From de87212558504bca0f0410546dd4a83901384a9e Mon Sep 17 00:00:00 2001 From: Tobias Urdin Date: Mon, 18 Dec 2023 13:14:14 +0100 Subject: [PATCH 3426/3836] Remove resource_key for dns floating IP The resource_key should not be set otherwise a proxy call to update_floating_ip() will fail in Designate API with a 400 bad request. Closes-Bug: 2046891 Change-Id: Ic056bc6f800d80a3c4a4821660912b1c88e73efd --- openstack/dns/v2/floating_ip.py | 1 - openstack/tests/unit/dns/v2/test_floating_ip.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/openstack/dns/v2/floating_ip.py b/openstack/dns/v2/floating_ip.py index a79f88d71..63e5790b4 100644 --- a/openstack/dns/v2/floating_ip.py +++ b/openstack/dns/v2/floating_ip.py @@ -17,7 +17,6 @@ class FloatingIP(_base.Resource): """DNS Floating IP Resource""" - resource_key = '' resources_key = 'floatingips' base_path = '/reverse/floatingips' diff --git a/openstack/tests/unit/dns/v2/test_floating_ip.py b/openstack/tests/unit/dns/v2/test_floating_ip.py index c87a15235..18312dbdd 100644 --- a/openstack/tests/unit/dns/v2/test_floating_ip.py +++ b/openstack/tests/unit/dns/v2/test_floating_ip.py @@ -30,7 +30,7 @@ class TestFloatingIP(base.TestCase): def test_basic(self): sot = fip.FloatingIP() - self.assertEqual('', sot.resource_key) + self.assertEqual(None, sot.resource_key) self.assertEqual('floatingips', sot.resources_key) self.assertEqual('/reverse/floatingips', sot.base_path) self.assertTrue(sot.allow_list) From f59019da2c7d3c55a7b08bc9201c7e5736d973b1 Mon Sep 17 00:00:00 2001 From: Pham Le Gia Dai Date: Tue, 2 Jan 2024 18:19:48 +0700 Subject: [PATCH 3427/3836] Remove unnecessary `keys` calls Python >= 3.5 alternative: unpack into a list literal Unpacking with * works with any object that is iterable and, since dictionaries return their keys when iterated through, you can easily create a list by using it within a list literal. Adding .keys() i.e [*newdict.keys()] might help in making your intent a bit more explicit though it will cost you a function look-up and invocation. (which, in all honesty, isn't something you should really be worried about). Refer [PEP 448](https://peps.python.org/pep-0448/) Change-Id: Iecaf73c9fd89257e32478c622407b0a3883b9e0d --- openstack/resource.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openstack/resource.py b/openstack/resource.py index 50387a579..0acf70f3f 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -373,7 +373,7 @@ def _validate(self, query, base_path=None, allow_unknown_params=False): :returns: Filtered collection of the supported QueryParameters """ - expected_params = list(self._mapping.keys()) + expected_params = list(self._mapping) expected_params.extend( value.get('name', key) if isinstance(value, dict) else value for key, value in self._mapping.items() @@ -382,7 +382,7 @@ def _validate(self, query, base_path=None, allow_unknown_params=False): if base_path: expected_params += utils.get_string_format_keys(base_path) - invalid_keys = set(query.keys()) - set(expected_params) + invalid_keys = set(query) - set(expected_params) if not invalid_keys: return query else: @@ -393,9 +393,7 @@ def _validate(self, query, base_path=None, allow_unknown_params=False): extra_data=invalid_keys, ) else: - known_keys = set(query.keys()).intersection( - set(expected_params) - ) + known_keys = set(query).intersection(set(expected_params)) return {k: query[k] for k in known_keys} def _transpose(self, query, resource_type): From ed4117ffe10e43002022dbafb011fad943d576d0 Mon Sep 17 00:00:00 2001 From: Pierre Riteau Date: Fri, 5 Jan 2024 18:01:22 +0100 Subject: [PATCH 3428/3836] Fix markup syntax in docstring Change-Id: I06ea52265d88771d16f3f321b53cf5fd7f1c302c --- openstack/config/cloud_region.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 4ba3daa57..e50bf08a8 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -239,7 +239,7 @@ class CloudRegion: :param str region_name: The default region name for all services in this CloudRegion. If - both ``region_name`` and ``config['region_name'] are specified, the + both ``region_name`` and ``config['region_name']`` are specified, the kwarg takes precedence. May be overridden for a given ${service} via a ${service}_region_name key in the ``config`` dict. :param dict config: From 24f76117f8677b1e46e25e4f6bfb26e5374011fd Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 14 Dec 2023 10:46:44 +0000 Subject: [PATCH 3429/3836] config: Add missing space to warning Change-Id: I8070bac2118612628effda601b309d3402947660 Signed-off-by: Stephen Finucane --- openstack/config/loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/config/loader.py b/openstack/config/loader.py index e6e288f41..da70616d6 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -628,7 +628,7 @@ def _expand_vendor_profile(self, name, cloud, our_cloud): else: # Can't find the requested vendor config, go about business warnings.warn( - f"Couldn't find the vendor profile {profile_name} for" + f"Couldn't find the vendor profile {profile_name} for " f"the cloud {name}", os_warnings.ConfigurationWarning, ) From b999a1e28951d5bfd1659439a166be25eb99af62 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 8 Jan 2024 18:20:30 +0000 Subject: [PATCH 3430/3836] zuul: Switch bifrost job to jammy This has a newer Python version. Change-Id: I09823e88d1548ade9527ad267f2657801ea3ec11 Signed-off-by: Stephen Finucane --- zuul.d/metal-jobs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zuul.d/metal-jobs.yaml b/zuul.d/metal-jobs.yaml index 6d9291575..a9da32274 100644 --- a/zuul.d/metal-jobs.yaml +++ b/zuul.d/metal-jobs.yaml @@ -9,7 +9,7 @@ - job: name: bifrost-integration-openstacksdk-src - parent: bifrost-integration-tinyipa-ubuntu-focal + parent: bifrost-integration-tinyipa-ubuntu-jammy required-projects: - openstack/ansible-collections-openstack - openstack/openstacksdk From bbe8518c8c00ef28b3cdc80d3a2c6276dd55ac36 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 26 Jul 2023 14:46:40 +0100 Subject: [PATCH 3431/3836] mypy: Address issues with openstack.image Change-Id: I361ab5c4ecb5b22bf794025d0a0039068783d788 Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 1 - openstack/image/_download.py | 25 ++++++++++++++++++++++++- openstack/image/image_signer.py | 5 ++++- openstack/image/v2/_proxy.py | 3 ++- openstack/image/v2/image.py | 10 ++++++---- openstack/image/v2/metadef_property.py | 4 +++- openstack/proxy.py | 2 +- openstack/resource.py | 2 +- setup.cfg | 3 +-- 9 files changed, 42 insertions(+), 13 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7d293eadf..5808d8bbd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -64,7 +64,6 @@ repos: | openstack/dns/.* | openstack/fixture/.* | openstack/identity/.* - | openstack/image/.* | openstack/instance_ha/.* | openstack/key_manager/.* | openstack/load_balancer/.* diff --git a/openstack/image/_download.py b/openstack/image/_download.py index efcecf73c..e15b8f2ad 100644 --- a/openstack/image/_download.py +++ b/openstack/image/_download.py @@ -9,9 +9,11 @@ # 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 io from openstack import exceptions +from openstack import resource from openstack import utils @@ -25,8 +27,29 @@ def _verify_checksum(md5, checksum): class DownloadMixin: + id: resource.Body + base_path: str + + def fetch( + self, + session, + requires_id=True, + base_path=None, + error_message=None, + skip_cache=False, + *, + resource_response_key=None, + microversion=None, + **params, + ): + ... + def download( - self, session, stream=False, output=None, chunk_size=1024 * 1024 + self, + session, + stream=False, + output=None, + chunk_size=1024 * 1024, ): """Download the data contained in an image""" # TODO(briancurtin): This method should probably offload the get diff --git a/openstack/image/image_signer.py b/openstack/image/image_signer.py index 4c88e4c3a..13f2765c3 100644 --- a/openstack/image/image_signer.py +++ b/openstack/image/image_signer.py @@ -9,7 +9,6 @@ # 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 cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import padding @@ -17,6 +16,7 @@ from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import serialization +from openstack import exceptions from openstack.image.iterable_chunked_file import IterableChunkedFile HASH_METHODS = { @@ -56,6 +56,9 @@ def load_private_key(self, file_path, password=None): ) def generate_signature(self, file_obj): + if not self.private_key: + raise exceptions.SDKException("private_key not set") + file_obj.seek(0) chunked_file = IterableChunkedFile(file_obj) for chunk in chunked_file: diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 5e1ccdbd3..776386631 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -12,6 +12,7 @@ import os import time +import typing as ty import warnings from openstack import exceptions @@ -602,7 +603,7 @@ def _upload_image( ) def _make_v2_image_params(self, meta, properties): - ret = {} + ret: ty.Dict = {} for k, v in iter(properties.items()): if k in _INT_PROPERTIES: ret[k] = int(v) diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index d4ed035ec..b42d05314 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -9,6 +9,9 @@ # 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 typing as ty + from openstack.common import tag from openstack import exceptions from openstack.image import _download @@ -335,7 +338,7 @@ def import_image( stores = stores or [] url = utils.urljoin(self.base_path, self.id, 'import') - data = {'method': {'name': method}} + data: ty.Dict[str, ty.Any] = {'method': {'name': method}} if uri: if method != 'web-download': @@ -356,9 +359,8 @@ def import_image( data['all_stores'] = all_stores if all_stores_must_succeed is not None: data['all_stores_must_succeed'] = all_stores_must_succeed - for s in stores: - data.setdefault('stores', []) - data['stores'].append(s.id) + if stores: + data['stores'] = [s.id for s in stores] headers = {} # Backward compat diff --git a/openstack/image/v2/metadef_property.py b/openstack/image/v2/metadef_property.py index ae40ebd4b..0f31b0239 100644 --- a/openstack/image/v2/metadef_property.py +++ b/openstack/image/v2/metadef_property.py @@ -54,8 +54,10 @@ class MetadefProperty(resource.Resource): min_length = resource.Body('minLength', type=int, minimum=0, default=0) #: Maximum allowed string length. max_length = resource.Body('maxLength', type=int, minimum=0) + # FIXME(stephenfin): This is causing conflicts due to the 'dict.items' + # method. Perhaps we need to rename it? #: Schema for the items in an array. - items = resource.Body('items', type=dict) + items = resource.Body('items', type=dict) # type: ignore #: Indicates whether all values in the array must be distinct. require_unique_items = resource.Body( 'uniqueItems', type=bool, default=False diff --git a/openstack/proxy.py b/openstack/proxy.py index 06574cafe..552becba4 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -74,7 +74,7 @@ def normalize_metric_name(name): class Proxy(adapter.Adapter): """Represents a service.""" - retriable_status_codes = None + retriable_status_codes: ty.Optional[ty.List[int]] = None """HTTP status codes that should be retried by default. The number of retries is defined by the configuration in parameters called diff --git a/openstack/resource.py b/openstack/resource.py index 50387a579..3b462bee2 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -468,7 +468,7 @@ class Resource(dict): #: The name of this resource. name = Body("name") #: The OpenStack location of this resource. - location = Computed('location') + location: ty.Union[Computed, Body] = Computed('location') #: Mapping of accepted query parameter names. _query_mapping = QueryParameters() diff --git a/setup.cfg b/setup.cfg index a66f14e0f..865487151 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,7 +38,7 @@ ignore_missing_imports = true follow_imports = skip incremental = true check_untyped_defs = true -warn_unused_ignores = true +warn_unused_ignores = false # keep this in-sync with 'mypy.exclude' in '.pre-commit-config.yaml' # TODO(stephenfin) Eventually we should remove everything here except the # unit tests module @@ -58,7 +58,6 @@ exclude = (?x)( | openstack/dns | openstack/fixture | openstack/identity - | openstack/image | openstack/instance_ha | openstack/key_manager | openstack/load_balancer From 4cf4eea21b34ca51d85bb580aa7c02fb4b1dc744 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 26 Jul 2023 14:59:50 +0100 Subject: [PATCH 3432/3836] mypy: Address issues with openstack.block_storage Our first significant bug! We were registering the 'group_type' module rather than the 'GroupType' resource in our block storage v3 proxy's resource registry. This is corrected. The rest of the changes are simply to work around mypy (obviously) not being able to detect the key and value types for empty dicts. Cinder doesn't care about the values of the dict for most WSGI actions so this doesn't matter. Change-Id: I4d9a4d4fcf08e8aa13b47314b24b2bb7226be3eb Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 1 - openstack/block_storage/v2/backup.py | 7 +++++-- openstack/block_storage/v2/volume.py | 4 ++-- openstack/block_storage/v3/_proxy.py | 13 ++++++++++++- openstack/block_storage/v3/backup.py | 7 +++++-- openstack/block_storage/v3/snapshot.py | 2 +- openstack/block_storage/v3/volume.py | 12 ++++++------ .../tests/unit/block_storage/v2/test_backup.py | 2 +- .../tests/unit/block_storage/v2/test_volume.py | 4 ++-- .../tests/unit/block_storage/v3/test_backup.py | 2 +- .../tests/unit/block_storage/v3/test_snapshot.py | 2 +- .../tests/unit/block_storage/v3/test_volume.py | 12 ++++++------ openstack/tests/unit/cloud/test_volume.py | 2 +- openstack/tests/unit/cloud/test_volume_backups.py | 4 ++-- setup.cfg | 1 - 15 files changed, 45 insertions(+), 30 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5808d8bbd..1dd92c32b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -56,7 +56,6 @@ repos: | openstack/accelerator/.* | openstack/baremetal/.* | openstack/baremetal_introspection/.* - | openstack/block_storage/.* | openstack/cloud/.* | openstack/clustering/.* | openstack/container_infrastructure_management/.* diff --git a/openstack/block_storage/v2/backup.py b/openstack/block_storage/v2/backup.py index a19490abd..12c78f1cb 100644 --- a/openstack/block_storage/v2/backup.py +++ b/openstack/block_storage/v2/backup.py @@ -9,6 +9,9 @@ # 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 typing as ty + from openstack import exceptions from openstack import resource from openstack import utils @@ -173,7 +176,7 @@ def restore(self, session, volume_id=None, name=None): :return: Updated backup instance """ url = utils.urljoin(self.base_path, self.id, "restore") - body = {'restore': {}} + body: ty.Dict[str, ty.Dict] = {'restore': {}} if volume_id: body['restore']['volume_id'] = volume_id if name: @@ -188,7 +191,7 @@ def restore(self, session, volume_id=None, name=None): def force_delete(self, session): """Force backup deletion""" - body = {'os-force_delete': {}} + body = {'os-force_delete': None} self._action(session, body) def reset(self, session, status): diff --git a/openstack/block_storage/v2/volume.py b/openstack/block_storage/v2/volume.py index 7c7843ddc..7f17d22ee 100644 --- a/openstack/block_storage/v2/volume.py +++ b/openstack/block_storage/v2/volume.py @@ -144,7 +144,7 @@ def detach(self, session, attachment, force=False): def unmanage(self, session): """Unmanage volume""" - body = {'os-unmanage': {}} + body = {'os-unmanage': None} self._action(session, body) @@ -184,7 +184,7 @@ def complete_migration(self, session, new_volume_id, error=False): def force_delete(self, session): """Force volume deletion""" - body = {'os-force_delete': {}} + body = {'os-force_delete': None} self._action(session, body) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index f076b7b8c..040d80c54 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -1717,11 +1717,22 @@ def find_service( def find_service( self, name_or_id: str, - ignore_missing: ty.Literal[False] = True, + ignore_missing: ty.Literal[False], **query, ) -> _service.Service: ... + # excuse the duplication here: it's mypy's fault + # https://github.com/python/mypy/issues/14764 + @ty.overload + def find_service( + self, + name_or_id: str, + ignore_missing: bool, + **query, + ) -> ty.Optional[_service.Service]: + ... + def find_service( self, name_or_id: str, diff --git a/openstack/block_storage/v3/backup.py b/openstack/block_storage/v3/backup.py index a74688c6a..b942fe98d 100644 --- a/openstack/block_storage/v3/backup.py +++ b/openstack/block_storage/v3/backup.py @@ -9,6 +9,9 @@ # 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 typing as ty + from openstack import exceptions from openstack import resource from openstack import utils @@ -189,7 +192,7 @@ def restore(self, session, volume_id=None, name=None): :return: Updated backup instance """ url = utils.urljoin(self.base_path, self.id, "restore") - body = {'restore': {}} + body: ty.Dict[str, ty.Dict] = {'restore': {}} if volume_id: body['restore']['volume_id'] = volume_id if name: @@ -204,7 +207,7 @@ def restore(self, session, volume_id=None, name=None): def force_delete(self, session): """Force backup deletion""" - body = {'os-force_delete': {}} + body = {'os-force_delete': None} self._action(session, body) def reset(self, session, status): diff --git a/openstack/block_storage/v3/snapshot.py b/openstack/block_storage/v3/snapshot.py index d551db259..9ce791452 100644 --- a/openstack/block_storage/v3/snapshot.py +++ b/openstack/block_storage/v3/snapshot.py @@ -76,7 +76,7 @@ def _action(self, session, body, microversion=None): def force_delete(self, session): """Force snapshot deletion.""" - body = {'os-force_delete': {}} + body = {'os-force_delete': None} self._action(session, body) def reset(self, session, status): diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index eb412c40e..660a2f722 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -181,7 +181,7 @@ def detach(self, session, attachment, force=False, connector=None): def unmanage(self, session): """Unmanage volume""" - body = {'os-unmanage': {}} + body = {'os-unmanage': None} self._action(session, body) @@ -229,7 +229,7 @@ def complete_migration(self, session, new_volume_id, error=False): def force_delete(self, session): """Force volume deletion""" - body = {'os-force_delete': {}} + body = {'os-force_delete': None} self._action(session, body) @@ -264,25 +264,25 @@ def upload_to_image( def reserve(self, session): """Reserve volume""" - body = {'os-reserve': {}} + body = {'os-reserve': None} self._action(session, body) def unreserve(self, session): """Unreserve volume""" - body = {'os-unreserve': {}} + body = {'os-unreserve': None} self._action(session, body) def begin_detaching(self, session): """Update volume status to 'detaching'""" - body = {'os-begin_detaching': {}} + body = {'os-begin_detaching': None} self._action(session, body) def abort_detaching(self, session): """Roll back volume status to 'in-use'""" - body = {'os-roll_detaching': {}} + body = {'os-roll_detaching': None} self._action(session, body) diff --git a/openstack/tests/unit/block_storage/v2/test_backup.py b/openstack/tests/unit/block_storage/v2/test_backup.py index b877a32d3..7de7902a5 100644 --- a/openstack/tests/unit/block_storage/v2/test_backup.py +++ b/openstack/tests/unit/block_storage/v2/test_backup.py @@ -172,7 +172,7 @@ def test_force_delete(self): self.assertIsNone(sot.force_delete(self.sess)) url = 'backups/%s/action' % FAKE_ID - body = {'os-force_delete': {}} + body = {'os-force_delete': None} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion ) diff --git a/openstack/tests/unit/block_storage/v2/test_volume.py b/openstack/tests/unit/block_storage/v2/test_volume.py index 41e59f143..1fdd8e312 100644 --- a/openstack/tests/unit/block_storage/v2/test_volume.py +++ b/openstack/tests/unit/block_storage/v2/test_volume.py @@ -237,7 +237,7 @@ def test_unmanage(self): self.assertIsNone(sot.unmanage(self.sess)) url = 'volumes/%s/action' % FAKE_ID - body = {'os-unmanage': {}} + body = {'os-unmanage': None} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion ) @@ -330,7 +330,7 @@ def test_force_delete(self): self.assertIsNone(sot.force_delete(self.sess)) url = 'volumes/%s/action' % FAKE_ID - body = {'os-force_delete': {}} + body = {'os-force_delete': None} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion ) diff --git a/openstack/tests/unit/block_storage/v3/test_backup.py b/openstack/tests/unit/block_storage/v3/test_backup.py index 27772ba78..5e73d61c9 100644 --- a/openstack/tests/unit/block_storage/v3/test_backup.py +++ b/openstack/tests/unit/block_storage/v3/test_backup.py @@ -185,7 +185,7 @@ def test_force_delete(self): self.assertIsNone(sot.force_delete(self.sess)) url = 'backups/%s/action' % FAKE_ID - body = {'os-force_delete': {}} + body = {'os-force_delete': None} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion ) diff --git a/openstack/tests/unit/block_storage/v3/test_snapshot.py b/openstack/tests/unit/block_storage/v3/test_snapshot.py index bd5d7d517..4796df95e 100644 --- a/openstack/tests/unit/block_storage/v3/test_snapshot.py +++ b/openstack/tests/unit/block_storage/v3/test_snapshot.py @@ -104,7 +104,7 @@ def test_force_delete(self): self.assertIsNone(sot.force_delete(self.sess)) url = 'snapshots/%s/action' % FAKE_ID - body = {'os-force_delete': {}} + body = {'os-force_delete': None} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion ) diff --git a/openstack/tests/unit/block_storage/v3/test_volume.py b/openstack/tests/unit/block_storage/v3/test_volume.py index 7e162252c..3c16d4005 100644 --- a/openstack/tests/unit/block_storage/v3/test_volume.py +++ b/openstack/tests/unit/block_storage/v3/test_volume.py @@ -314,7 +314,7 @@ def test_unmanage(self): self.assertIsNone(sot.unmanage(self.sess)) url = 'volumes/%s/action' % FAKE_ID - body = {'os-unmanage': {}} + body = {'os-unmanage': None} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion ) @@ -434,7 +434,7 @@ def test_force_delete(self): self.assertIsNone(sot.force_delete(self.sess)) url = 'volumes/%s/action' % FAKE_ID - body = {'os-force_delete': {}} + body = {'os-force_delete': None} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion ) @@ -504,7 +504,7 @@ def test_reserve(self): self.assertIsNone(sot.reserve(self.sess)) url = 'volumes/%s/action' % FAKE_ID - body = {'os-reserve': {}} + body = {'os-reserve': None} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion ) @@ -515,7 +515,7 @@ def test_unreserve(self): self.assertIsNone(sot.unreserve(self.sess)) url = 'volumes/%s/action' % FAKE_ID - body = {'os-unreserve': {}} + body = {'os-unreserve': None} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion ) @@ -526,7 +526,7 @@ def test_begin_detaching(self): self.assertIsNone(sot.begin_detaching(self.sess)) url = 'volumes/%s/action' % FAKE_ID - body = {'os-begin_detaching': {}} + body = {'os-begin_detaching': None} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion ) @@ -537,7 +537,7 @@ def test_abort_detaching(self): self.assertIsNone(sot.abort_detaching(self.sess)) url = 'volumes/%s/action' % FAKE_ID - body = {'os-roll_detaching': {}} + body = {'os-roll_detaching': None} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion ) diff --git a/openstack/tests/unit/cloud/test_volume.py b/openstack/tests/unit/cloud/test_volume.py index aab503b54..8c41006b8 100644 --- a/openstack/tests/unit/cloud/test_volume.py +++ b/openstack/tests/unit/cloud/test_volume.py @@ -535,7 +535,7 @@ def test_delete_volume_force(self): 'public', append=['volumes', volume.id, 'action'], ), - validate=dict(json={'os-force_delete': {}}), + validate=dict(json={'os-force_delete': None}), ), dict( method='GET', diff --git a/openstack/tests/unit/cloud/test_volume_backups.py b/openstack/tests/unit/cloud/test_volume_backups.py index 41a779241..7e2e53479 100644 --- a/openstack/tests/unit/cloud/test_volume_backups.py +++ b/openstack/tests/unit/cloud/test_volume_backups.py @@ -152,8 +152,8 @@ def test_delete_volume_backup_force(self): 'public', append=['backups', backup_id, 'action'], ), - json={'os-force_delete': {}}, - validate=dict(json={u'os-force_delete': {}}), + json={'os-force_delete': None}, + validate=dict(json={'os-force_delete': None}), ), dict( method='GET', diff --git a/setup.cfg b/setup.cfg index 865487151..ef60b26fa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -50,7 +50,6 @@ exclude = (?x)( | openstack/accelerator | openstack/baremetal | openstack/baremetal_introspection - | openstack/block_storage | openstack/cloud | openstack/clustering | openstack/container_infrastructure_management From 6c8f2307ae33ee3be5b96193e33205bdd57bc226 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 26 Jul 2023 17:13:37 +0100 Subject: [PATCH 3433/3836] mypy: Address issues with openstack.identity Another bug highlighted. A test will come later so as not to confuse this patch. Change-Id: I38f1a3911781470b77644cd42512408f8c03dc4d Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 1 - openstack/identity/v3/_proxy.py | 2 +- openstack/resource.py | 2 +- setup.cfg | 1 - 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1dd92c32b..6d1d4a034 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -62,7 +62,6 @@ repos: | openstack/database/.* | openstack/dns/.* | openstack/fixture/.* - | openstack/identity/.* | openstack/instance_ha/.* | openstack/key_manager/.* | openstack/load_balancer/.* diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 902e1b3e5..1dabd0f00 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -1201,7 +1201,7 @@ def role_assignments_filter( user_id=user.id, ) else: - system = self._get_resource(_project.System, system) + system = self._get_resource(_system.System, system) if group: group = self._get_resource(_group.Group, group) return self._list( diff --git a/openstack/resource.py b/openstack/resource.py index 3b462bee2..4841036ee 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -504,7 +504,7 @@ class Resource(dict): #: Do calls for this resource require an id requires_id = True #: Whether create requires an ID (determined from method if None). - create_requires_id = None + create_requires_id: ty.Optional[bool] = None #: Whether create should exclude ID in the body of the request. create_exclude_id_from_body = False #: Do responses for this resource have bodies diff --git a/setup.cfg b/setup.cfg index ef60b26fa..d0ff24920 100644 --- a/setup.cfg +++ b/setup.cfg @@ -56,7 +56,6 @@ exclude = (?x)( | openstack/database | openstack/dns | openstack/fixture - | openstack/identity | openstack/instance_ha | openstack/key_manager | openstack/load_balancer From f8a0ec9a637a452e4918539551992e3f77b32e3d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 26 Jul 2023 17:29:17 +0100 Subject: [PATCH 3434/3836] identity: Add test for 'role_assignments_filter' proxy method To prevent regressions. We also switch to retrieving only the ID of provided resources, which should be faster. Change-Id: Id61340ae026a41c77ce9d9ca031f488f1bf82c61 Signed-off-by: Stephen Finucane --- openstack/identity/v3/_proxy.py | 42 +++++------ .../tests/unit/identity/v3/test_proxy.py | 72 +++++++++++++++++++ 2 files changed, 93 insertions(+), 21 deletions(-) diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 1dabd0f00..316dfe266 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -1169,52 +1169,52 @@ def role_assignments_filter( ) if domain: - domain = self._get_resource(_domain.Domain, domain) + domain_id = resource.Resource._get_id(domain) if group: - group = self._get_resource(_group.Group, group) + group_id = resource.Resource._get_id(group) return self._list( _role_domain_group_assignment.RoleDomainGroupAssignment, - domain_id=domain.id, - group_id=group.id, + domain_id=domain_id, + group_id=group_id, ) else: - user = self._get_resource(_user.User, user) + user_id = resource.Resource._get_id(user) return self._list( _role_domain_user_assignment.RoleDomainUserAssignment, - domain_id=domain.id, - user_id=user.id, + domain_id=domain_id, + user_id=user_id, ) elif project: - project = self._get_resource(_project.Project, project) + project_id = resource.Resource._get_id(project) if group: - group = self._get_resource(_group.Group, group) + group_id = resource.Resource._get_id(group) return self._list( _role_project_group_assignment.RoleProjectGroupAssignment, - project_id=project.id, - group_id=group.id, + project_id=project_id, + group_id=group_id, ) else: - user = self._get_resource(_user.User, user) + user_id = resource.Resource._get_id(user) return self._list( _role_project_user_assignment.RoleProjectUserAssignment, - project_id=project.id, - user_id=user.id, + project_id=project_id, + user_id=user_id, ) else: - system = self._get_resource(_system.System, system) + system_id = resource.Resource._get_id(system) if group: - group = self._get_resource(_group.Group, group) + group_id = resource.Resource._get_id(group) return self._list( _role_system_group_assignment.RoleSystemGroupAssignment, - system_id=system.id, - group_id=group.id, + system_id=system_id, + group_id=group_id, ) else: - user = self._get_resource(_user.User, user) + user_id = resource.Resource._get_id(user) return self._list( _role_system_user_assignment.RoleSystemUserAssignment, - system_id=system.id, - user_id=user.id, + system_id=system_id, + user_id=user_id, ) def role_assignments(self, **query): diff --git a/openstack/tests/unit/identity/v3/test_proxy.py b/openstack/tests/unit/identity/v3/test_proxy.py index a0b7fcfad..3b6b9a3e1 100644 --- a/openstack/tests/unit/identity/v3/test_proxy.py +++ b/openstack/tests/unit/identity/v3/test_proxy.py @@ -22,6 +22,12 @@ from openstack.identity.v3 import project from openstack.identity.v3 import region from openstack.identity.v3 import role +from openstack.identity.v3 import role_domain_group_assignment +from openstack.identity.v3 import role_domain_user_assignment +from openstack.identity.v3 import role_project_group_assignment +from openstack.identity.v3 import role_project_user_assignment +from openstack.identity.v3 import role_system_group_assignment +from openstack.identity.v3 import role_system_user_assignment from openstack.identity.v3 import service from openstack.identity.v3 import trust from openstack.identity.v3 import user @@ -411,6 +417,72 @@ def test_role_update(self): class TestIdentityProxyRoleAssignments(TestIdentityProxyBase): + def test_role_assignments_filter__domain_user(self): + self.verify_list( + self.proxy.role_assignments_filter, + role_domain_user_assignment.RoleDomainUserAssignment, + method_kwargs={'domain': 'domain', 'user': 'user'}, + expected_kwargs={ + 'domain_id': 'domain', + 'user_id': 'user', + }, + ) + + def test_role_assignments_filter__domain_group(self): + self.verify_list( + self.proxy.role_assignments_filter, + role_domain_group_assignment.RoleDomainGroupAssignment, + method_kwargs={'domain': 'domain', 'group': 'group'}, + expected_kwargs={ + 'domain_id': 'domain', + 'group_id': 'group', + }, + ) + + def test_role_assignments_filter__project_user(self): + self.verify_list( + self.proxy.role_assignments_filter, + role_project_user_assignment.RoleProjectUserAssignment, + method_kwargs={'project': 'project', 'user': 'user'}, + expected_kwargs={ + 'project_id': 'project', + 'user_id': 'user', + }, + ) + + def test_role_assignments_filter__project_group(self): + self.verify_list( + self.proxy.role_assignments_filter, + role_project_group_assignment.RoleProjectGroupAssignment, + method_kwargs={'project': 'project', 'group': 'group'}, + expected_kwargs={ + 'project_id': 'project', + 'group_id': 'group', + }, + ) + + def test_role_assignments_filter__system_user(self): + self.verify_list( + self.proxy.role_assignments_filter, + role_system_user_assignment.RoleSystemUserAssignment, + method_kwargs={'system': 'system', 'user': 'user'}, + expected_kwargs={ + 'system_id': 'system', + 'user_id': 'user', + }, + ) + + def test_role_assignments_filter__system_group(self): + self.verify_list( + self.proxy.role_assignments_filter, + role_system_group_assignment.RoleSystemGroupAssignment, + method_kwargs={'system': 'system', 'group': 'group'}, + expected_kwargs={ + 'system_id': 'system', + 'group_id': 'group', + }, + ) + def test_assign_domain_role_to_user(self): self._verify( "openstack.identity.v3.domain.Domain.assign_role_to_user", From 4f8d4102f5e73963d2decc3da12e5505a8e08f5b Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 26 Jul 2023 17:32:03 +0100 Subject: [PATCH 3435/3836] mypy: Address issues with openstack.network Just an incorrect type annotation to correct here. We need to figure out a way to properly type the 'Resource' class with all its magic. Change-Id: Id238b870de85a4663032710d1c15f6e200d2d543 Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 1 - openstack/network/v2/port.py | 6 +----- setup.cfg | 1 - 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6d1d4a034..5d6864407 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,7 +66,6 @@ repos: | openstack/key_manager/.* | openstack/load_balancer/.* | openstack/message/.* - | openstack/network/.* | openstack/object_store/.* | openstack/orchestration/.* | openstack/placement/.* diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index 7be8cea21..bd2f35d2f 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import typing as ty - from openstack.common import tag from openstack.network.v2 import _base from openstack import resource @@ -59,9 +57,7 @@ class Port(_base.NetworkResource, tag.TagMixin): # Properties #: Allowed address pairs list. Dictionary key ``ip_address`` is required #: and key ``mac_address`` is optional. - allowed_address_pairs: ty.List[dict] = resource.Body( - 'allowed_address_pairs', type=list - ) + allowed_address_pairs = resource.Body('allowed_address_pairs', type=list) #: The ID of the host where the port is allocated. In some cases, #: different implementations can run on different hosts. binding_host_id = resource.Body('binding:host_id') diff --git a/setup.cfg b/setup.cfg index d0ff24920..a9a8f5777 100644 --- a/setup.cfg +++ b/setup.cfg @@ -60,7 +60,6 @@ exclude = (?x)( | openstack/key_manager | openstack/load_balancer | openstack/message - | openstack/network | openstack/object_store | openstack/orchestration | openstack/placement From 5d47d65d0056f2a4711c6d5a55da176daa79c681 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 26 Jul 2023 17:46:57 +0100 Subject: [PATCH 3436/3836] mypy: Address issues with openstack.object_store Another small bug to be corrected here: SDKException does not have a 'response' attribute. Change-Id: I084336ba41147f824b92dc07235e5f19b7ac4a9c Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 1 - openstack/object_store/v1/_base.py | 10 +++--- openstack/object_store/v1/_proxy.py | 53 ++++++++++++++++------------- openstack/resource.py | 2 +- setup.cfg | 1 - 5 files changed, 36 insertions(+), 31 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5d6864407..094368644 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,7 +66,6 @@ repos: | openstack/key_manager/.* | openstack/load_balancer/.* | openstack/message/.* - | openstack/object_store/.* | openstack/orchestration/.* | openstack/placement/.* | openstack/shared_file_system/.* diff --git a/openstack/object_store/v1/_base.py b/openstack/object_store/v1/_base.py index a39286f5e..0b1c4dfed 100644 --- a/openstack/object_store/v1/_base.py +++ b/openstack/object_store/v1/_base.py @@ -11,6 +11,8 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + from openstack import exceptions from openstack import resource @@ -20,11 +22,11 @@ class BaseResource(resource.Resource): create_method = 'PUT' #: Metadata stored for this resource. *Type: dict* - metadata = dict() + metadata: ty.Dict[str, ty.Any] = {} - _custom_metadata_prefix = None - _system_metadata = dict() - _last_headers = dict() + _custom_metadata_prefix: str + _system_metadata: ty.Dict[str, ty.Any] = {} + _last_headers: ty.Dict[str, ty.Any] = {} def __init__(self, metadata=None, **attrs): """Process and save metadata known at creation stage""" diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index f66032f99..a63d9dc2c 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -769,15 +769,17 @@ def get_object_segment_size(self, segment_size): try: # caps = self.get_object_capabilities() caps = self.get_info() - except exceptions.SDKException as e: - if e.response.status_code in (404, 412): - server_max_file_size = DEFAULT_MAX_FILE_SIZE - self._connection.log.info( - "Swift capabilities not supported. " - "Using default max file size." - ) - else: - raise + except ( + exceptions.NotFoundException, + exceptions.PreconditionFailedException, + ): + server_max_file_size = DEFAULT_MAX_FILE_SIZE + self._connection.log.info( + "Swift capabilities not supported. " + "Using default max file size." + ) + except exceptions.SDKException: + raise else: server_max_file_size = caps.swift.get('max_file_size', 0) min_segment_size = caps.slo.get('min_segment_size', 0) @@ -935,8 +937,7 @@ def generate_form_signature( max_upload_count, expires, ) - data = data.encode('utf8') - sig = hmac.new(temp_url_key, data, sha1).hexdigest() + sig = hmac.new(temp_url_key, data.encode(), sha1).hexdigest() return (expires, sig) @@ -992,18 +993,17 @@ def generate_temp_url( try: t = time.strptime(seconds, f) except ValueError: - t = None - else: - if f == EXPIRES_ISO8601_FORMAT: - timestamp = timegm(t) - else: - # Use local time if UTC designator is missing. - timestamp = int(time.mktime(t)) + continue - absolute = True - break + if f == EXPIRES_ISO8601_FORMAT: + timestamp = timegm(t) + else: + # Use local time if UTC designator is missing. + timestamp = int(time.mktime(t)) - if t is None: + absolute = True + break + else: raise ValueError() else: if not timestamp.is_integer(): @@ -1046,6 +1046,7 @@ def generate_temp_url( method.upper(), ) + expiration: float | int if not absolute: expiration = _get_expiration(timestamp) else: @@ -1074,12 +1075,16 @@ def generate_temp_url( ).hexdigest() if iso8601: - expiration = time.strftime( + exp = time.strftime( EXPIRES_ISO8601_FORMAT, time.gmtime(expiration) ) + else: + exp = str(expiration) temp_url = u'{path}?temp_url_sig={sig}&temp_url_expires={exp}'.format( - path=path_for_body, sig=sig, exp=expiration + path=path_for_body, + sig=sig, + exp=exp, ) if ip_range: @@ -1143,7 +1148,7 @@ def _service_cleanup( return is_bulk_delete_supported = False - bulk_delete_max_per_request = None + bulk_delete_max_per_request = 1 try: caps = self.get_info() except exceptions.SDKException: diff --git a/openstack/resource.py b/openstack/resource.py index 4841036ee..9cc47c8b3 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -460,7 +460,7 @@ class Resource(dict): #: Plural form of key for resource. resources_key: ty.Optional[str] = None #: Key used for pagination links - pagination_key = None + pagination_key: ty.Optional[str] = None #: The ID of this resource. id = Body("id") diff --git a/setup.cfg b/setup.cfg index a9a8f5777..2b002bfda 100644 --- a/setup.cfg +++ b/setup.cfg @@ -60,7 +60,6 @@ exclude = (?x)( | openstack/key_manager | openstack/load_balancer | openstack/message - | openstack/object_store | openstack/orchestration | openstack/placement | openstack/shared_file_system From b6cc1d817dcc78f5de301491c8965654d631329d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 26 Jul 2023 18:01:57 +0100 Subject: [PATCH 3437/3836] mypy: Address issues with openstack.baremetal, baremetal_introspection Yet another bug here: we weren't passing 'session' arguments. Change-Id: Id9c5bafe8bc71024ff6d453870553dd45b6576d1 Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 2 -- openstack/baremetal/configdrive.py | 15 +++++++-------- openstack/baremetal/v1/_common.py | 6 ++++-- openstack/baremetal/v1/_proxy.py | 4 ++-- openstack/baremetal/v1/allocation.py | 2 +- openstack/baremetal/v1/chassis.py | 2 +- openstack/baremetal/v1/conductor.py | 2 +- openstack/baremetal/v1/deploy_templates.py | 2 +- openstack/baremetal/v1/driver.py | 4 +++- openstack/baremetal/v1/node.py | 8 +++----- openstack/baremetal/v1/port.py | 2 +- openstack/baremetal/v1/port_group.py | 2 +- openstack/baremetal/v1/volume_connector.py | 2 +- openstack/baremetal/v1/volume_target.py | 2 +- .../v1/introspection_rule.py | 2 +- openstack/resource.py | 2 +- .../tests/unit/baremetal/test_configdrive.py | 4 ++-- openstack/tests/unit/baremetal/v1/test_node.py | 2 +- setup.cfg | 2 -- 19 files changed, 32 insertions(+), 35 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 094368644..bf45105eb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -54,8 +54,6 @@ repos: | openstack/tests/unit/.* | openstack/tests/fixtures.py | openstack/accelerator/.* - | openstack/baremetal/.* - | openstack/baremetal_introspection/.* | openstack/cloud/.* | openstack/clustering/.* | openstack/container_infrastructure_management/.* diff --git a/openstack/baremetal/configdrive.py b/openstack/baremetal/configdrive.py index bcf2fb689..24bedba82 100644 --- a/openstack/baremetal/configdrive.py +++ b/openstack/baremetal/configdrive.py @@ -20,6 +20,7 @@ import shutil import subprocess import tempfile +import typing as ty @contextlib.contextmanager @@ -100,7 +101,7 @@ def build( return pack(path) -def pack(path): +def pack(path: str) -> str: """Pack a directory with files into a Bare Metal service configdrive. Creates an ISO image with the files and label "config-2". @@ -112,6 +113,7 @@ def pack(path): # NOTE(toabctl): Luckily, genisoimage, mkisofs and xorrisofs understand # the same parameters which are currently used. cmds = ['genisoimage', 'mkisofs', 'xorrisofs'] + error: ty.Optional[Exception] for c in cmds: try: p = subprocess.Popen( @@ -153,7 +155,7 @@ def pack(path): raise RuntimeError( 'Error generating the configdrive.' 'Stdout: "%(stdout)s". Stderr: "%(stderr)s"' - % {'stdout': stdout, 'stderr': stderr} + % {'stdout': stdout.decode(), 'stderr': stderr.decode()} ) tmpfile.seek(0) @@ -163,11 +165,8 @@ def pack(path): shutil.copyfileobj(tmpfile, gz_file) tmpzipfile.seek(0) - cd = base64.b64encode(tmpzipfile.read()) - - # NOTE(dtantsur): Ironic expects configdrive to be a string, but base64 - # returns bytes on Python 3. - if not isinstance(cd, str): - cd = cd.decode('utf-8') + # NOTE(dtantsur): Ironic expects configdrive to be a string, but + # base64 returns bytes on Python 3. + cd = base64.b64encode(tmpzipfile.read()).decode() return cd diff --git a/openstack/baremetal/v1/_common.py b/openstack/baremetal/v1/_common.py index 787e9b23e..83dbc1a92 100644 --- a/openstack/baremetal/v1/_common.py +++ b/openstack/baremetal/v1/_common.py @@ -89,7 +89,9 @@ """API version in which boot_mode and secure_boot states can be changed""" -class ListMixin: +class Resource(resource.Resource): + base_path: str + @classmethod def list(cls, session, details=False, **params): """This method is a generator which yields resource objects. @@ -113,7 +115,7 @@ def list(cls, session, details=False, **params): base_path = cls.base_path if details: base_path += '/detail' - return super(ListMixin, cls).list( + return super().list( session, paginated=True, base_path=base_path, **params ) diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index b3fb6550d..18115e711 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -456,7 +456,7 @@ def get_node_boot_device(self, node): :return: The node boot device """ res = self._get_resource(_node.Node, node) - return res.get_boot_device() + return res.get_boot_device(self) def set_node_boot_device(self, node, boot_device, persistent=False): """Set node boot device @@ -479,7 +479,7 @@ def get_node_supported_boot_devices(self, node): :return: The node boot device """ res = self._get_resource(_node.Node, node) - return res.get_supported_boot_devices() + return res.get_supported_boot_devices(self) def set_node_boot_mode(self, node, target): """Make a request to change node's boot mode diff --git a/openstack/baremetal/v1/allocation.py b/openstack/baremetal/v1/allocation.py index dadb5e34d..d4f78163e 100644 --- a/openstack/baremetal/v1/allocation.py +++ b/openstack/baremetal/v1/allocation.py @@ -16,7 +16,7 @@ from openstack import utils -class Allocation(_common.ListMixin, resource.Resource): +class Allocation(_common.Resource): resources_key = 'allocations' base_path = '/allocations' diff --git a/openstack/baremetal/v1/chassis.py b/openstack/baremetal/v1/chassis.py index 0640bb785..daa03d563 100644 --- a/openstack/baremetal/v1/chassis.py +++ b/openstack/baremetal/v1/chassis.py @@ -14,7 +14,7 @@ from openstack import resource -class Chassis(_common.ListMixin, resource.Resource): +class Chassis(_common.Resource): resources_key = 'chassis' base_path = '/chassis' diff --git a/openstack/baremetal/v1/conductor.py b/openstack/baremetal/v1/conductor.py index 15c3b76b7..ff1f00346 100644 --- a/openstack/baremetal/v1/conductor.py +++ b/openstack/baremetal/v1/conductor.py @@ -14,7 +14,7 @@ from openstack import resource -class Conductor(_common.ListMixin, resource.Resource): +class Conductor(_common.Resource): resources_key = 'conductors' base_path = '/conductors' diff --git a/openstack/baremetal/v1/deploy_templates.py b/openstack/baremetal/v1/deploy_templates.py index 17f59ea79..edb8a8fa0 100644 --- a/openstack/baremetal/v1/deploy_templates.py +++ b/openstack/baremetal/v1/deploy_templates.py @@ -14,7 +14,7 @@ from openstack import resource -class DeployTemplate(_common.ListMixin, resource.Resource): +class DeployTemplate(_common.Resource): resources_key = 'deploy_templates' base_path = '/deploy_templates' diff --git a/openstack/baremetal/v1/driver.py b/openstack/baremetal/v1/driver.py index c4bb374ed..4f457f9e7 100644 --- a/openstack/baremetal/v1/driver.py +++ b/openstack/baremetal/v1/driver.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + from openstack.baremetal.v1 import _common from openstack import exceptions from openstack import resource @@ -153,7 +155,7 @@ def list_vendor_passthru(self, session): return response.json() def call_vendor_passthru( - self, session, verb: str, method: str, body: dict = None + self, session, verb: str, method: str, body: ty.Optional[dict] = None ): """Call a vendor specific passthru method diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index c8d03add6..5c409edba 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -70,7 +70,7 @@ class WaitResult( __slots__ = () -class Node(_common.ListMixin, resource.Resource): +class Node(_common.Resource): resources_key = 'nodes' base_path = '/nodes' @@ -711,11 +711,9 @@ def inject_nmi(self, session): request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'management', 'inject_nmi') - body = {} - response = session.put( request.url, - json=body, + json={}, headers=request.headers, microversion=version, retriable_status_codes=_common.RETRIABLE_STATUS_CODES, @@ -813,7 +811,7 @@ def attach_vif(self, session, vif_id, retry_on_conflict=True): body = {'id': vif_id} retriable_status_codes = _common.RETRIABLE_STATUS_CODES if not retry_on_conflict: - retriable_status_codes = set(retriable_status_codes) - {409} + retriable_status_codes = list(set(retriable_status_codes) - {409}) response = session.post( request.url, json=body, diff --git a/openstack/baremetal/v1/port.py b/openstack/baremetal/v1/port.py index 1435d7c13..be27d9baf 100644 --- a/openstack/baremetal/v1/port.py +++ b/openstack/baremetal/v1/port.py @@ -14,7 +14,7 @@ from openstack import resource -class Port(_common.ListMixin, resource.Resource): +class Port(_common.Resource): resources_key = 'ports' base_path = '/ports' diff --git a/openstack/baremetal/v1/port_group.py b/openstack/baremetal/v1/port_group.py index 5460058ae..eba7a077b 100644 --- a/openstack/baremetal/v1/port_group.py +++ b/openstack/baremetal/v1/port_group.py @@ -14,7 +14,7 @@ from openstack import resource -class PortGroup(_common.ListMixin, resource.Resource): +class PortGroup(_common.Resource): resources_key = 'portgroups' base_path = '/portgroups' diff --git a/openstack/baremetal/v1/volume_connector.py b/openstack/baremetal/v1/volume_connector.py index 70e009f02..06495e876 100644 --- a/openstack/baremetal/v1/volume_connector.py +++ b/openstack/baremetal/v1/volume_connector.py @@ -14,7 +14,7 @@ from openstack import resource -class VolumeConnector(_common.ListMixin, resource.Resource): +class VolumeConnector(_common.Resource): resources_key = 'connectors' base_path = '/volume/connectors' diff --git a/openstack/baremetal/v1/volume_target.py b/openstack/baremetal/v1/volume_target.py index f2b933294..12d9da5e5 100644 --- a/openstack/baremetal/v1/volume_target.py +++ b/openstack/baremetal/v1/volume_target.py @@ -14,7 +14,7 @@ from openstack import resource -class VolumeTarget(_common.ListMixin, resource.Resource): +class VolumeTarget(_common.Resource): resources_key = 'targets' base_path = '/volume/targets' diff --git a/openstack/baremetal_introspection/v1/introspection_rule.py b/openstack/baremetal_introspection/v1/introspection_rule.py index 1d37d4dea..27bf61d79 100644 --- a/openstack/baremetal_introspection/v1/introspection_rule.py +++ b/openstack/baremetal_introspection/v1/introspection_rule.py @@ -14,7 +14,7 @@ from openstack import resource -class IntrospectionRule(_common.ListMixin, resource.Resource): +class IntrospectionRule(_common.Resource): resources_key = 'rules' base_path = '/rules' diff --git a/openstack/resource.py b/openstack/resource.py index 9cc47c8b3..c3af1e557 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -510,7 +510,7 @@ class Resource(dict): #: Do responses for this resource have bodies has_body = True #: Does create returns a body (if False requires ID), defaults to has_body - create_returns_body = None + create_returns_body: ty.Optional[bool] = None #: Maximum microversion to use for getting/creating/updating the Resource _max_microversion: ty.Optional[str] = None diff --git a/openstack/tests/unit/baremetal/test_configdrive.py b/openstack/tests/unit/baremetal/test_configdrive.py index 6b694fa3f..a2ba7d9a8 100644 --- a/openstack/tests/unit/baremetal/test_configdrive.py +++ b/openstack/tests/unit/baremetal/test_configdrive.py @@ -97,12 +97,12 @@ def test_no_genisoimage(self, mock_popen): ) def test_genisoimage_fails(self, mock_popen): - mock_popen.return_value.communicate.return_value = "", "BOOM" + mock_popen.return_value.communicate.return_value = b"", b"BOOM" mock_popen.return_value.returncode = 1 self.assertRaisesRegex(RuntimeError, "BOOM", configdrive.pack, "/fake") def test_success(self, mock_popen): - mock_popen.return_value.communicate.return_value = "", "" + mock_popen.return_value.communicate.return_value = b"", b"" mock_popen.return_value.returncode = 0 result = configdrive.pack("/fake") # Make sure the result is string on all python versions diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index a47eec1dc..907b93edc 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -562,7 +562,7 @@ def test_attach_vif_no_retries(self): json={'id': self.vif_id}, headers=mock.ANY, microversion='1.28', - retriable_status_codes={503}, + retriable_status_codes=[503], ) def test_detach_vif_existing(self): diff --git a/setup.cfg b/setup.cfg index 2b002bfda..5a398d8df 100644 --- a/setup.cfg +++ b/setup.cfg @@ -48,8 +48,6 @@ exclude = (?x)( | openstack/tests/unit | openstack/tests/fixtures.py | openstack/accelerator - | openstack/baremetal - | openstack/baremetal_introspection | openstack/cloud | openstack/clustering | openstack/container_infrastructure_management From 4c1ced6ae29ccf2092c5250bdc9864709a92824b Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 26 Jul 2023 18:18:25 +0100 Subject: [PATCH 3438/3836] mypy: Address issues with openstack.clustering This one is cool. We use overload to allow the value of 'Resource.find' and 'Proxy._find' to vary depending on whether 'ignore_missing' is True or False. Change-Id: I386e10774dfb6ec9db80cbda9757446a2b5e4e57 Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 1 - openstack/proxy.py | 36 ++++++++++++++++++++++++-- openstack/resource.py | 57 +++++++++++++++++++++++++++++++++++++---- setup.cfg | 1 - 4 files changed, 86 insertions(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bf45105eb..8349fbbd4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -55,7 +55,6 @@ repos: | openstack/tests/fixtures.py | openstack/accelerator/.* | openstack/cloud/.* - | openstack/clustering/.* | openstack/container_infrastructure_management/.* | openstack/database/.* | openstack/dns/.* diff --git a/openstack/proxy.py b/openstack/proxy.py index 552becba4..90e06b877 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -477,11 +477,43 @@ def _get_uri_attribute(self, child, parent, name): value = resource.Resource._get_id(parent) return value + @ty.overload def _find( self, resource_type: ty.Type[ResourceType], - name_or_id, - ignore_missing=True, + name_or_id: str, + ignore_missing: ty.Literal[True] = True, + **attrs, + ) -> ty.Optional[ResourceType]: + ... + + @ty.overload + def _find( + self, + resource_type: ty.Type[ResourceType], + name_or_id: str, + ignore_missing: ty.Literal[False], + **attrs, + ) -> ResourceType: + ... + + # excuse the duplication here: it's mypy's fault + # https://github.com/python/mypy/issues/14764 + @ty.overload + def _find( + self, + resource_type: ty.Type[ResourceType], + name_or_id: str, + ignore_missing: bool, + **attrs, + ) -> ty.Optional[ResourceType]: + ... + + def _find( + self, + resource_type: ty.Type[ResourceType], + name_or_id: str, + ignore_missing: bool = True, **attrs, ) -> ty.Optional[ResourceType]: """Find a resource diff --git a/openstack/resource.py b/openstack/resource.py index c3af1e557..13645963e 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -2248,16 +2248,63 @@ def _get_one_match(cls, name_or_id, results): return the_result + @ty.overload @classmethod def find( cls, session, - name_or_id, - ignore_missing=True, - list_base_path=None, + name_or_id: str, + ignore_missing: ty.Literal[True] = True, + list_base_path: ty.Optional[str] = None, *, - microversion=None, - all_projects=None, + microversion: ty.Optional[str] = None, + all_projects: ty.Optional[bool] = None, + **params, + ) -> ty.Optional['Resource']: + ... + + @ty.overload + @classmethod + def find( + cls, + session, + name_or_id: str, + ignore_missing: ty.Literal[False], + list_base_path: ty.Optional[str] = None, + *, + microversion: ty.Optional[str] = None, + all_projects: ty.Optional[bool] = None, + **params, + ) -> 'Resource': + ... + + # excuse the duplication here: it's mypy's fault + # https://github.com/python/mypy/issues/14764 + @ty.overload + @classmethod + def find( + cls, + session, + name_or_id: str, + ignore_missing: bool, + list_base_path: ty.Optional[str] = None, + *, + microversion: ty.Optional[str] = None, + all_projects: ty.Optional[bool] = None, + **params, + ): + ... + + @classmethod + def find( + cls, + session, + name_or_id: str, + ignore_missing: bool = True, + list_base_path: ty.Optional[str] = None, + *, + microversion: ty.Optional[str] = None, + all_projects: ty.Optional[bool] = None, **params, ): """Find a resource by its name or id. diff --git a/setup.cfg b/setup.cfg index 5a398d8df..b325cd3d8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -49,7 +49,6 @@ exclude = (?x)( | openstack/tests/fixtures.py | openstack/accelerator | openstack/cloud - | openstack/clustering | openstack/container_infrastructure_management | openstack/database | openstack/dns From a7cfc5342fec6d410efefb92f94c5dba2840ccbe Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 27 Jul 2023 10:14:49 +0100 Subject: [PATCH 3439/3836] mypy: Address issues with openstack.orchestration Change-Id: I181b1ddee04b2514283b4d742e74b9bd54790414 Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 1 - openstack/orchestration/util/template_format.py | 2 +- openstack/orchestration/util/template_utils.py | 9 +++++---- openstack/orchestration/v1/stack_environment.py | 4 ++-- openstack/orchestration/v1/stack_files.py | 4 ++-- openstack/resource.py | 2 +- setup.cfg | 1 - 7 files changed, 11 insertions(+), 12 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8349fbbd4..ef635507e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -63,7 +63,6 @@ repos: | openstack/key_manager/.* | openstack/load_balancer/.* | openstack/message/.* - | openstack/orchestration/.* | openstack/placement/.* | openstack/shared_file_system/.* | openstack/workflow/.* diff --git a/openstack/orchestration/util/template_format.py b/openstack/orchestration/util/template_format.py index 618490811..426a22c66 100644 --- a/openstack/orchestration/util/template_format.py +++ b/openstack/orchestration/util/template_format.py @@ -17,7 +17,7 @@ if hasattr(yaml, 'CSafeLoader'): yaml_loader = yaml.CSafeLoader else: - yaml_loader = yaml.SafeLoader + yaml_loader = yaml.SafeLoader # type: ignore class HeatYamlLoader(yaml_loader): diff --git a/openstack/orchestration/util/template_utils.py b/openstack/orchestration/util/template_utils.py index 43bc657be..48797f24d 100644 --- a/openstack/orchestration/util/template_utils.py +++ b/openstack/orchestration/util/template_utils.py @@ -14,6 +14,7 @@ import collections.abc import json +import typing as ty from urllib import parse from urllib import request @@ -221,8 +222,8 @@ def process_multiple_environments_and_files( :return: tuple of files dict and a dict of the consolidated environment :rtype: tuple """ - merged_files = {} - merged_env = {} + merged_files: ty.Dict[str, str] = {} + merged_env: ty.Dict[str, ty.Dict] = {} # If we're keeping a list of environment files separately, include the # contents of the files in the files dict @@ -275,8 +276,8 @@ def process_environment_and_files( :return: tuple of files dict and the loaded environment as a dict :rtype: (dict, dict) """ - files = {} - env = {} + files: ty.Dict[str, str] = {} + env: ty.Dict[str, ty.Dict] = {} is_object = env_path_is_object and env_path_is_object(env_path) diff --git a/openstack/orchestration/v1/stack_environment.py b/openstack/orchestration/v1/stack_environment.py index e61bea9da..475efdb22 100644 --- a/openstack/orchestration/v1/stack_environment.py +++ b/openstack/orchestration/v1/stack_environment.py @@ -29,9 +29,9 @@ class StackEnvironment(resource.Resource): # Backwards compat stack_name = name #: ID of the stack where the template is referenced. - id = resource.URI('stack_id') + id = resource.URI('stack_id') # type: ignore # Backwards compat - stack_id = id + stack_id = id # type: ignore #: A list of parameter names whose values are encrypted encrypted_param_names = resource.Body('encrypted_param_names') #: A list of event sinks diff --git a/openstack/orchestration/v1/stack_files.py b/openstack/orchestration/v1/stack_files.py index 71a735623..0b72c6b68 100644 --- a/openstack/orchestration/v1/stack_files.py +++ b/openstack/orchestration/v1/stack_files.py @@ -29,9 +29,9 @@ class StackFiles(resource.Resource): # Backwards compat stack_name = name #: ID of the stack where the template is referenced. - id = resource.URI('stack_id') + id = resource.URI('stack_id') # type: ignore # Backwards compat - stack_id = id + stack_id = id # type: ignore def fetch(self, session, base_path=None): # The stack files response contains a map of filenames and file diff --git a/openstack/resource.py b/openstack/resource.py index 13645963e..54ca6bab1 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -466,7 +466,7 @@ class Resource(dict): id = Body("id") #: The name of this resource. - name = Body("name") + name: ty.Union[Body, URI] = Body("name") #: The OpenStack location of this resource. location: ty.Union[Computed, Body] = Computed('location') diff --git a/setup.cfg b/setup.cfg index b325cd3d8..e5536e14b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -57,7 +57,6 @@ exclude = (?x)( | openstack/key_manager | openstack/load_balancer | openstack/message - | openstack/orchestration | openstack/placement | openstack/shared_file_system | openstack/workflow From 554dc6284c15c7a5c8bca52ed148f88f04344564 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 27 Jul 2023 10:18:59 +0100 Subject: [PATCH 3440/3836] mypy: Address issues with remaining service modules The changes here are all small enough to be bundled into one. Change-Id: Ia585244e314a9bd18a7cd2388a2936517e25dbf2 Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 11 ----------- openstack/database/v1/instance.py | 2 +- openstack/dns/v2/_base.py | 4 +++- openstack/dns/v2/zone_share.py | 9 ++++----- openstack/instance_ha/v1/host.py | 2 -- openstack/message/v2/message.py | 5 +++++ openstack/placement/v1/resource_provider_inventory.py | 4 +++- openstack/resource.py | 2 +- openstack/tests/unit/database/v1/test_instance.py | 2 +- .../placement/v1/test_resource_provider_inventory.py | 2 +- setup.cfg | 11 ----------- 11 files changed, 19 insertions(+), 35 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ef635507e..48e6782a2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -53,19 +53,8 @@ repos: | openstack/tests/functional/.* | openstack/tests/unit/.* | openstack/tests/fixtures.py - | openstack/accelerator/.* | openstack/cloud/.* - | openstack/container_infrastructure_management/.* - | openstack/database/.* - | openstack/dns/.* | openstack/fixture/.* - | openstack/instance_ha/.* - | openstack/key_manager/.* - | openstack/load_balancer/.* - | openstack/message/.* - | openstack/placement/.* - | openstack/shared_file_system/.* - | openstack/workflow/.* | doc/.* | examples/.* | releasenotes/.* diff --git a/openstack/database/v1/instance.py b/openstack/database/v1/instance.py index c24abaa4a..31f181862 100644 --- a/openstack/database/v1/instance.py +++ b/openstack/database/v1/instance.py @@ -89,7 +89,7 @@ def restart(self, session): :returns: ``None`` """ - body = {'restart': {}} + body = {'restart': None} url = utils.urljoin(self.base_path, self.id, 'action') session.post(url, json=body) diff --git a/openstack/dns/v2/_base.py b/openstack/dns/v2/_base.py index 791d9d537..0cac1eac5 100644 --- a/openstack/dns/v2/_base.py +++ b/openstack/dns/v2/_base.py @@ -9,6 +9,8 @@ # 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 typing as ty import urllib.parse from openstack import exceptions @@ -73,7 +75,7 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): @classmethod def _get_next_link(cls, uri, response, data, marker, limit, total_yielded): next_link = None - params = {} + params: ty.Dict[str, ty.Union[ty.List[str], str]] = {} if isinstance(data, dict): links = data.get('links') if links: diff --git a/openstack/dns/v2/zone_share.py b/openstack/dns/v2/zone_share.py index b778b1620..af889d355 100644 --- a/openstack/dns/v2/zone_share.py +++ b/openstack/dns/v2/zone_share.py @@ -29,17 +29,16 @@ class ZoneShare(_base.Resource): _query_mapping = resource.QueryParameters('target_project_id') # Properties + #: The ID of the zone being shared. + zone_id = resource.URI('zone_id') #: Timestamp when the share was created. created_at = resource.Body('created_at') #: Timestamp when the member was last updated. updated_at = resource.Body('updated_at') + # FIXME(stephenfin): This conflicts since there is a zone ID in the URI #: The zone ID of the zone being shared. - zone_id = resource.Body('zone_id') + # zone_id = resource.Body('zone_id') #: The project ID that owns the share. project_id = resource.Body('project_id') #: The target project ID that the zone is shared with. target_project_id = resource.Body('target_project_id') - - # URI Properties - #: The ID of the zone being shared. - zone_id = resource.URI('zone_id') diff --git a/openstack/instance_ha/v1/host.py b/openstack/instance_ha/v1/host.py index 29cc94f9a..ff356797a 100644 --- a/openstack/instance_ha/v1/host.py +++ b/openstack/instance_ha/v1/host.py @@ -32,8 +32,6 @@ class Host(resource.Resource): allow_commit = True allow_delete = True - #: A ID of representing this host - id = resource.URI("id") #: A Uuid of representing this host uuid = resource.Body("uuid") #: A failover segment ID of this host(in URI) diff --git a/openstack/message/v2/message.py b/openstack/message/v2/message.py index 40db3a5e9..da36c58b0 100644 --- a/openstack/message/v2/message.py +++ b/openstack/message/v2/message.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty import uuid from openstack import resource @@ -53,6 +54,10 @@ class Message(resource.Resource): #: in case keystone auth is not enabled in Zaqar service. project_id = resource.Header("X-PROJECT-ID") + # FIXME(stephenfin): This is actually a query arg but we need it for + # deletions and resource.delete doesn't respect these currently + claim_id: ty.Optional[str] = None + def post(self, session, messages): request = self._prepare_request(requires_id=False, prepend_key=True) headers = { diff --git a/openstack/placement/v1/resource_provider_inventory.py b/openstack/placement/v1/resource_provider_inventory.py index 016bb49f3..4261e6ba1 100644 --- a/openstack/placement/v1/resource_provider_inventory.py +++ b/openstack/placement/v1/resource_provider_inventory.py @@ -19,7 +19,9 @@ class ResourceProviderInventory(resource.Resource): resources_key = None base_path = '/resource_providers/%(resource_provider_id)s/inventories' - _query_mapping = {} + _query_mapping = resource.QueryParameters( + include_pagination_defaults=False + ) # Capabilities diff --git a/openstack/resource.py b/openstack/resource.py index 54ca6bab1..7fa930ae2 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -468,7 +468,7 @@ class Resource(dict): #: The name of this resource. name: ty.Union[Body, URI] = Body("name") #: The OpenStack location of this resource. - location: ty.Union[Computed, Body] = Computed('location') + location: ty.Union[Computed, Body, Header] = Computed('location') #: Mapping of accepted query parameter names. _query_mapping = QueryParameters() diff --git a/openstack/tests/unit/database/v1/test_instance.py b/openstack/tests/unit/database/v1/test_instance.py index 0a329d379..bb2df451d 100644 --- a/openstack/tests/unit/database/v1/test_instance.py +++ b/openstack/tests/unit/database/v1/test_instance.py @@ -97,7 +97,7 @@ def test_action_restart(self): self.assertIsNone(sot.restart(sess)) url = "instances/%s/action" % IDENTIFIER - body = {'restart': {}} + body = {'restart': None} sess.post.assert_called_with(url, json=body) def test_action_resize(self): diff --git a/openstack/tests/unit/placement/v1/test_resource_provider_inventory.py b/openstack/tests/unit/placement/v1/test_resource_provider_inventory.py index 6f01f8e46..1194ca81a 100644 --- a/openstack/tests/unit/placement/v1/test_resource_provider_inventory.py +++ b/openstack/tests/unit/placement/v1/test_resource_provider_inventory.py @@ -39,7 +39,7 @@ def test_basic(self): self.assertTrue(sot.allow_list) self.assertFalse(sot.allow_patch) - self.assertDictEqual({}, sot._query_mapping) + self.assertDictEqual({}, sot._query_mapping._mapping) def test_make_it(self): sot = resource_provider_inventory.ResourceProviderInventory(**FAKE) diff --git a/setup.cfg b/setup.cfg index e5536e14b..f1a352187 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,19 +47,8 @@ exclude = (?x)( | openstack/tests/functional | openstack/tests/unit | openstack/tests/fixtures.py - | openstack/accelerator | openstack/cloud - | openstack/container_infrastructure_management - | openstack/database - | openstack/dns | openstack/fixture - | openstack/instance_ha - | openstack/key_manager - | openstack/load_balancer - | openstack/message - | openstack/placement - | openstack/shared_file_system - | openstack/workflow | doc | examples | releasenotes From d506b9ff49e2f5d2826722117bd0481e4859483e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 27 Jul 2023 11:40:14 +0100 Subject: [PATCH 3441/3836] mypy: Add typing to openstack._log A nice easy one to start. Change-Id: I9a08341002cbf90ba9dccaacaa5f2d3e7d7560a2 Signed-off-by: Stephen Finucane --- openstack/_log.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/openstack/_log.py b/openstack/_log.py index 6a909fc34..62f2c7d36 100644 --- a/openstack/_log.py +++ b/openstack/_log.py @@ -14,9 +14,14 @@ import logging import sys +import typing as ty -def setup_logging(name, handlers=None, level=None): +def setup_logging( + name: str, + handlers: ty.Optional[ty.List[logging.Handler]] = None, + level: ty.Optional[int] = None, +) -> logging.Logger: """Set up logging for a named logger. Gets and initializes a named logger, ensuring it at least has a @@ -34,8 +39,7 @@ def setup_logging(name, handlers=None, level=None): handlers = handlers or [] log = logging.getLogger(name) if len(log.handlers) == 0 and not handlers: - h = logging.NullHandler() - log.addHandler(h) + log.addHandler(logging.NullHandler()) for h in handlers: log.addHandler(h) if level: @@ -44,14 +48,14 @@ def setup_logging(name, handlers=None, level=None): def enable_logging( - debug=False, - http_debug=False, - path=None, - stream=None, - format_stream=False, - format_template='%(asctime)s %(levelname)s: %(name)s %(message)s', - handlers=None, -): + debug: bool = False, + http_debug: bool = False, + path: ty.Optional[str] = None, + stream: ty.Optional[ty.TextIO] = None, + format_stream: bool = False, + format_template: str = '%(asctime)s %(levelname)s: %(name)s %(message)s', + handlers: ty.Optional[ty.List[logging.Handler]] = None, +) -> None: """Enable logging output. Helper function to enable logging. This function is available for From 92868d3e8b6fc8e809e390505ba1527996c0140b Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 9 Jan 2024 11:44:02 +0000 Subject: [PATCH 3442/3836] pre-commit: Bump linter versions Fix the issues this uncovers. We also migrate to the native hacking pre-commit hook. Change-Id: I44385c25fcb010f3e62b4098fd34ae3290292630 Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 18 +++++++----------- openstack/cloud/_floating_ip.py | 2 +- openstack/config/loader.py | 2 +- openstack/resource.py | 2 +- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7d293eadf..5eb3938ca 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ default_language_version: python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: trailing-whitespace - id: mixed-line-ending @@ -22,23 +22,19 @@ repos: hooks: - id: doc8 - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 23.12.1 hooks: - id: black args: ['-S', '-l', '79'] - - repo: local + - repo: https://opendev.org/openstack/hacking + rev: 6.1.0 hooks: - - id: flake8 - name: flake8 + - id: hacking additional_dependencies: - - hacking>=6.0.1,<6.1.0 - - flake8-import-order>=0.18.2,<0.19.0 - language: python - entry: flake8 - files: '^.*\.py$' + - flake8-import-order~=0.18.2 exclude: '^(doc|releasenotes|tools)/.*$' - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.4.1 + rev: v1.8.0 hooks: - id: mypy additional_dependencies: diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index 9d0ca4648..970d337f7 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -924,7 +924,7 @@ def add_ip_list( error. """ - if type(ips) != list: + if type(ips) is list: ips = [ips] for ip in ips: diff --git a/openstack/config/loader.py b/openstack/config/loader.py index da70616d6..2d9cf19e8 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -991,7 +991,7 @@ def _fix_args(self, args=None, argparse=None): os_args = dict() new_args = dict() for key, val in iter(args.items()): - if type(args[key]) == dict: + if type(args[key]) is dict: # dive into the auth dict new_args[key] = self._fix_args(args[key]) continue diff --git a/openstack/resource.py b/openstack/resource.py index 50387a579..3405dd962 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -355,7 +355,7 @@ def __init__( parameters, ``limit`` and ``marker``. These are the most common query parameters used for listing resources in OpenStack APIs. """ - self._mapping = {} + self._mapping: ty.Dict[str, ty.Union[str, ty.Dict]] = {} if include_pagination_defaults: self._mapping.update({"limit": "limit", "marker": "marker"}) self._mapping.update({name: name for name in names}) From a4feb32ac4e06b75bb7151e21d9663a198791065 Mon Sep 17 00:00:00 2001 From: Rajat Dhasmana Date: Fri, 15 Sep 2023 01:34:07 +0530 Subject: [PATCH 3443/3836] Add volume manage support Add support for volume manage operation. Change-Id: Ic31e92a501721b5ecbf56d44188d9dad95cd1ac3 --- doc/source/user/proxies/block_storage_v3.rst | 1 + openstack/block_storage/v3/_proxy.py | 15 ++++- openstack/block_storage/v3/volume.py | 38 +++++++++++ .../unit/block_storage/v3/test_volume.py | 63 +++++++++++++++++++ ...anage-volume-support-a4fd90e3ff2fa0d0.yaml | 4 ++ 5 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-manage-volume-support-a4fd90e3ff2fa0d0.yaml diff --git a/doc/source/user/proxies/block_storage_v3.rst b/doc/source/user/proxies/block_storage_v3.rst index c7cc4b085..d8c101703 100644 --- a/doc/source/user/proxies/block_storage_v3.rst +++ b/doc/source/user/proxies/block_storage_v3.rst @@ -26,6 +26,7 @@ Volume Operations upload_volume_to_image, reserve_volume, unreserve_volume, begin_volume_detaching, abort_volume_detaching, init_volume_attachment, terminate_volume_attachment, + manage_volume, Backend Pools Operations ^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 90d0008eb..15bf11080 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -806,6 +806,18 @@ def detach_volume(self, volume, attachment, force=False, connector=None): volume = self._get_resource(_volume.Volume, volume) volume.detach(self, attachment, force, connector) + def manage_volume(self, **attrs): + """Creates a volume by using existing storage rather than + allocating new storage. + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.block_storage.v3.volume.Volume`, + comprised of the properties on the Volume class. + :returns: The results of volume creation + :rtype: :class:`~openstack.block_storage.v3.volume.Volume` + """ + return _volume.Volume.manage(self, **attrs) + def unmanage_volume(self, volume): """Removes a volume from Block Storage management without removing the back-end storage object that is associated with it. @@ -813,7 +825,8 @@ def unmanage_volume(self, volume): :param volume: The value can be either the ID of a volume or a :class:`~openstack.block_storage.v3.volume.Volume` instance. - :returns: None""" + :returns: None + """ volume = self._get_resource(_volume.Volume, volume) volume.unmanage(self) diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index eb412c40e..0b6a69b48 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -179,6 +179,44 @@ def detach(self, session, attachment, force=False, connector=None): self._action(session, body) + @classmethod + def manage( + cls, + session, + host, + ref, + name=None, + description=None, + volume_type=None, + availability_zone=None, + metadata=None, + bootable=False, + cluster=None, + ): + """Manage an existing volume.""" + url = '/manageable_volumes' + if not utils.supports_microversion(session, '3.8'): + url = '/os-volume-manage' + body = { + 'volume': { + 'host': host, + 'ref': ref, + 'name': name, + 'description': description, + 'volume_type': volume_type, + 'availability_zone': availability_zone, + 'metadata': metadata, + 'bootable': bootable, + } + } + if cluster is not None: + body['volume']['cluster'] = cluster + resp = session.post(url, json=body, microversion=cls._max_microversion) + exceptions.raise_from_response(resp) + volume = Volume() + volume._translate_response(resp) + return volume + def unmanage(self, session): """Unmanage volume""" body = {'os-unmanage': {}} diff --git a/openstack/tests/unit/block_storage/v3/test_volume.py b/openstack/tests/unit/block_storage/v3/test_volume.py index 7e162252c..27a9d6d99 100644 --- a/openstack/tests/unit/block_storage/v3/test_volume.py +++ b/openstack/tests/unit/block_storage/v3/test_volume.py @@ -31,6 +31,7 @@ u'size': '13167616', } +FAKE_HOST = "fake_host@fake_backend#fake_pool" VOLUME = { "status": "creating", "name": "my_volume", @@ -598,3 +599,65 @@ def test_create_scheduler_hints(self): headers={}, params={}, ) + + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=True, + ) + def test_manage(self, mock_mv): + resp = mock.Mock() + resp.body = {'volume': copy.deepcopy(VOLUME)} + resp.json = mock.Mock(return_value=resp.body) + resp.headers = {} + resp.status_code = 202 + self.sess.post = mock.Mock(return_value=resp) + sot = volume.Volume.manage(self.sess, host=FAKE_HOST, ref=FAKE_ID) + self.assertIsNotNone(sot) + url = '/manageable_volumes' + body = { + 'volume': { + 'host': FAKE_HOST, + 'ref': FAKE_ID, + 'name': None, + 'description': None, + 'volume_type': None, + 'availability_zone': None, + 'metadata': None, + 'bootable': False, + } + } + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion + ) + + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=False, + ) + def test_manage_pre_38(self, mock_mv): + resp = mock.Mock() + resp.body = {'volume': copy.deepcopy(VOLUME)} + resp.json = mock.Mock(return_value=resp.body) + resp.headers = {} + resp.status_code = 202 + self.sess.post = mock.Mock(return_value=resp) + sot = volume.Volume.manage(self.sess, host=FAKE_HOST, ref=FAKE_ID) + self.assertIsNotNone(sot) + url = '/os-volume-manage' + body = { + 'volume': { + 'host': FAKE_HOST, + 'ref': FAKE_ID, + 'name': None, + 'description': None, + 'volume_type': None, + 'availability_zone': None, + 'metadata': None, + 'bootable': False, + } + } + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion + ) diff --git a/releasenotes/notes/add-manage-volume-support-a4fd90e3ff2fa0d0.yaml b/releasenotes/notes/add-manage-volume-support-a4fd90e3ff2fa0d0.yaml new file mode 100644 index 000000000..6d0a0aedf --- /dev/null +++ b/releasenotes/notes/add-manage-volume-support-a4fd90e3ff2fa0d0.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Added support for manage volume operation. From dd329d0a7243b572de389cae866be0a034846b2b Mon Sep 17 00:00:00 2001 From: Luis Morales Date: Tue, 23 Jan 2024 20:40:43 -0600 Subject: [PATCH 3444/3836] Add support for provider_id for volume objects provider_id - MV 3.21 Change-Id: I99be623a2a669d527a9fcb8e26655c5efe8b6d57 --- openstack/block_storage/v3/volume.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index 660a2f722..d179fadca 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -81,6 +81,8 @@ class Volume(resource.Resource, metadata.MetadataMixin): replication_driver_data = resource.Body( "os-volume-replication:driver_data" ) + #: The provider ID for the volume. + provider_id = resource.Body("provider_id") #: Status of replication on this volume. replication_status = resource.Body("replication_status") #: Scheduler hints for the volume From 6077e6c2ba3a461b79c3d90b16f8d3b39b8ee8c5 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Mon, 5 Feb 2024 16:13:41 +0000 Subject: [PATCH 3445/3836] reno: Update master for unmaintained/yoga Update the yoga release notes configuration to build from unmaintained/yoga. Change-Id: Ifa66170e346564db6782ce6109c35044c919f861 --- releasenotes/source/yoga.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/source/yoga.rst b/releasenotes/source/yoga.rst index 7cd5e908a..43cafdea8 100644 --- a/releasenotes/source/yoga.rst +++ b/releasenotes/source/yoga.rst @@ -3,4 +3,4 @@ Yoga Series Release Notes ========================= .. release-notes:: - :branch: stable/yoga + :branch: unmaintained/yoga From a0cec19e1d4a53b827408fe798312b1c26870d74 Mon Sep 17 00:00:00 2001 From: Rajat Dhasmana Date: Wed, 13 Sep 2023 00:18:23 +0530 Subject: [PATCH 3446/3836] Add missing snapshot parameters This patch adds some of the missing snapshot parameters like: consumes_quota - MV 3.65 group_snapshot_id - MV 3.14 user_id - MV 3.41 Change-Id: I7ece6168d0ac27712992778ecda87464897b00ea --- openstack/block_storage/v3/snapshot.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/openstack/block_storage/v3/snapshot.py b/openstack/block_storage/v3/snapshot.py index 9b5a64e37..d98d8e459 100644 --- a/openstack/block_storage/v3/snapshot.py +++ b/openstack/block_storage/v3/snapshot.py @@ -44,10 +44,18 @@ class Snapshot(resource.Resource, metadata.MetadataMixin): allow_list = True # Properties + #: Whether this resource consumes quota or not. Resources that not + #: counted for quota usage are usually temporary internal resources + #: created to perform an operation. + #: This is included from microversion 3.65 + consumes_quota = resource.Body("consumes_quota") #: The timestamp of this snapshot creation. created_at = resource.Body("created_at") #: Description of snapshot. Default is None. description = resource.Body("description") + #: The ID of the group snapshot. + #: This is included from microversion 3.14 + group_snapshot_id = resource.Body("group_snapshot_id") #: Indicate whether to create snapshot, even if the volume is attached. #: Default is ``False``. *Type: bool* is_forced = resource.Body("force", type=format.BoolStr) @@ -62,10 +70,13 @@ class Snapshot(resource.Resource, metadata.MetadataMixin): status = resource.Body("status") #: The date and time when the resource was updated. updated_at = resource.Body("updated_at") + #: The UUID of the user. + #: This is included from microversion 3.41 + user_id = resource.Body("user_id") #: The ID of the volume this snapshot was taken of. volume_id = resource.Body("volume_id") - _max_microversion = '3.8' + _max_microversion = '3.65' def _action(self, session, body, microversion=None): """Preform backup actions given the message body.""" From 434adf775c9c2a62876b5bd1d33b9a36164b4f17 Mon Sep 17 00:00:00 2001 From: Mridula Joshi Date: Tue, 5 Dec 2023 07:57:12 +0000 Subject: [PATCH 3447/3836] Adding SDK support for ``glance md-namespace-properties-delete`` In this patch, we have added support for deleting all properties from the namespace. Change-Id: I6ec24881a09a96ac7c99f169835ce8b57d0f692a --- openstack/image/v2/_proxy.py | 17 +++++++++++++++++ openstack/image/v2/metadef_namespace.py | 17 +++++++++++++++++ .../unit/image/v2/test_metadef_namespace.py | 16 ++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 5e1ccdbd3..6bfd603fa 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -1524,6 +1524,23 @@ def get_metadef_property( **query, ) + def delete_all_metadef_properties(self, metadef_namespace): + """Delete all metadata definitions property inside a specific namespace. + + :param metadef_namespace: The value can be either the name of a metadef + namespace or a + :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` + instance. + + :returns: ``None`` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource can be found. + """ + namespace = self._get_resource( + _metadef_namespace.MetadefNamespace, metadef_namespace + ) + return namespace.delete_all_properties(self) + # ====== SCHEMAS ====== def get_images_schema(self): """Get images schema diff --git a/openstack/image/v2/metadef_namespace.py b/openstack/image/v2/metadef_namespace.py index 24dd90899..133a929b1 100644 --- a/openstack/image/v2/metadef_namespace.py +++ b/openstack/image/v2/metadef_namespace.py @@ -10,7 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import exceptions from openstack import resource +from openstack import utils class MetadefNamespace(resource.Resource): @@ -71,3 +73,18 @@ def _commit( has_body=True, retry_on_conflict=None, ) + + def _delete_all(self, session, url): + response = session.delete(url) + exceptions.raise_from_response(response) + self._translate_response(response, has_body=False) + return self + + def delete_all_properties(self, session): + """Delete all properties in a namespace. + + :param session: The session to use for making this request + :returns: The server response + """ + url = utils.urljoin(self.base_path, self.id, 'properties') + return self._delete_all(session, url) diff --git a/openstack/tests/unit/image/v2/test_metadef_namespace.py b/openstack/tests/unit/image/v2/test_metadef_namespace.py index b55e99c34..f22a1e6ce 100644 --- a/openstack/tests/unit/image/v2/test_metadef_namespace.py +++ b/openstack/tests/unit/image/v2/test_metadef_namespace.py @@ -10,6 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. + +from unittest import mock + +from keystoneauth1 import adapter + +from openstack import exceptions from openstack.image.v2 import metadef_namespace from openstack.tests.unit import base @@ -71,3 +77,13 @@ def test_make_it(self): }, sot._query_mapping._mapping, ) + + @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) + def test_delete_all_properties(self): + sot = metadef_namespace.MetadefNamespace(**EXAMPLE) + session = mock.Mock(spec=adapter.Adapter) + sot._translate_response = mock.Mock() + sot.delete_all_properties(session) + session.delete.assert_called_with( + 'metadefs/namespaces/OS::Cinder::Volumetype/properties' + ) From 4baf2eb2c391261ed44161329d8b0727b2ce8e33 Mon Sep 17 00:00:00 2001 From: Radoslaw Smigielski Date: Sat, 10 Feb 2024 18:12:17 +0100 Subject: [PATCH 3448/3836] Incorrect protocol type in create_security_group_rule() Example conn.network.create_security_group_rule() in examples/network/security_group_rules.py uses wrong protocol type protocol='HTTPS'. HTTPS is not allowed value, allowed values are: icmp, udp, tcp. It should be "protocol='tcp'" instead. Closes-Bug: #2052860 Change-Id: I0fb0532ca562bcf93672a1ca3f1ce2e3ad864c6d --- examples/network/security_group_rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/network/security_group_rules.py b/examples/network/security_group_rules.py index 99363c336..b9eb8b8a0 100644 --- a/examples/network/security_group_rules.py +++ b/examples/network/security_group_rules.py @@ -31,7 +31,7 @@ def open_port(conn): security_group_id=example_sec_group.id, direction='ingress', remote_ip_prefix='0.0.0.0/0', - protocol='HTTPS', + protocol='tcp', port_range_max='443', port_range_min='443', ethertype='IPv4', From 6ec46610050e36f46ab71097991c485caac48ba3 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 1 Sep 2023 13:14:07 +0100 Subject: [PATCH 3449/3836] docs: Add docs on configuration of a service user Build upon I897325032ee7b0f559906e82be7f3a7695768c52 to give an additional example using a service user. Change-Id: Iac2b85ac19d100c68a9039583b55437aa8b4494e Signed-off-by: Stephen Finucane --- openstack/connection.py | 71 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/openstack/connection.py b/openstack/connection.py index c993dbdd7..499cfd88e 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -154,6 +154,77 @@ oslo_conf=CONF, ) +This can then be used with an appropriate configuration file. + +.. code-block:: ini + + [neutron] + region_name = RegionOne + auth_strategy = keystone + project_domain_name = Default + project_name = service + user_domain_name = Default + password = password + username = neutron + auth_url = http://10.0.110.85/identity + auth_type = password + service_metadata_proxy = True + default_floating_pool = public + +You may also wish to configure a service user. As discussed in the `Keystone +documentation`__, service users are users with specific roles that identify the +user as a service. The use of service users can avoid issues caused by the +expiration of the original user's token during long running operations, as a +fresh token issued for the service user will always accompany the user's token, +which may have expired. + +.. code-block:: python + + from keystoneauth1 import loading as ks_loading + from keystoneauth1 import service_token + from oslo_config import cfg + import openstack + from openstack import connection + + CONF = cfg.CONF + + neutron_group = cfg.OptGroup('neutron') + ks_loading.register_session_conf_options(CONF, neutron_group) + ks_loading.register_auth_conf_options(CONF, neutron_group) + ks_loading.register_adapter_conf_options(CONF, neutron_group) + + service_group = cfg.OptGroup('service_user') + ks_loading.register_session_conf_options(CONF, service_group) + ks_loading.register_auth_conf_options(CONF, service_group) + + CONF() + user_auth = ks_loading.load_auth_from_conf_options(CONF, 'neutron') + service_auth = ks_loading.load_auth_from_conf_options(CONF, 'service_user') + auth = service_token.ServiceTokenAuthWrapper(user_auth, service_auth) + + sess = ks_loading.load_session_from_conf_options(CONF, 'neutron', auth=auth) + + conn = connection.Connection( + session=sess, + oslo_conf=CONF, + ) + +This will necessitate an additional section in the configuration file used. + +.. code-block:: ini + + [service_user] + auth_strategy = keystone + project_domain_name = Default + project_name = service + user_domain_name = Default + password = password + username = nova + auth_url = http://10.0.110.85/identity + auth_type = password + +.. __: https://docs.openstack.org/keystone/latest/admin/manage-services.html + From existing CloudRegion ~~~~~~~~~~~~~~~~~~~~~~~~~ From d1ff60d125fad059ffd8b45fea70b76e61f72acc Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 15 Feb 2024 12:16:58 +0000 Subject: [PATCH 3450/3836] Fix typo Change-Id: I160d95eebfe4de5bbea77820e40632f2a1dfd67a Signed-off-by: Stephen Finucane --- openstack/compute/v2/_proxy.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 53ea68a4d..5929e0e61 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -330,9 +330,8 @@ def aggregates(self, **query): def get_aggregate(self, aggregate): """Get a single host aggregate - :param image: The value can be the ID of an aggregate or a - :class:`~openstack.compute.v2.aggregate.Aggregate` - instance. + :param aggregate: The value can be the ID of an aggregate or a + :class:`~openstack.compute.v2.aggregate.Aggregate` instance. :returns: One :class:`~openstack.compute.v2.aggregate.Aggregate` :raises: :class:`~openstack.exceptions.ResourceNotFound` From 568921ce5b50275061c5dd0ba7180842f3c157a7 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 15 Feb 2024 14:19:10 +0000 Subject: [PATCH 3451/3836] tox: Correct functional test factors We are running functional tests in zuul without a 'pyNN' factor (e.g. 'tox -e functional'). For this to work, we need to allow an empty factor, i.e. we want: [testenv:functional{,-py310}] rather than: [testenv:functional{-py310}] (note the missing comma) Unfortunately we missed this as tox 4 has a currently unaddressed regression [1] that results in it running the base testenv in the case there is only a partial factor match. That needs to be fixed for avoid this biting us again the future, but we can at least fix it for now. [1] https://github.com/tox-dev/tox/issues/3219 Change-Id: Ib9f65a4523222f1224d51534c5061f90501b59d3 Signed-off-by: Stephen Finucane --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index bdc0a3737..693535a1c 100644 --- a/tox.ini +++ b/tox.ini @@ -23,7 +23,7 @@ commands = stestr run {posargs} stestr slowest -[testenv:functional{-py37,-py38,-py39}] +[testenv:functional{,-py37,-py38,-py39,-py310,-py311,-py312}] description = Run functional tests. # Some jobs (especially heat) takes longer, therefore increase default timeout From 8c988b7e627ef802c019979c26fd5cac24871473 Mon Sep 17 00:00:00 2001 From: Pavlo Shchelokovskyy Date: Wed, 5 Jul 2023 13:27:46 +0300 Subject: [PATCH 3452/3836] Fix list of server migrations the _list method goes straight through to formatting base_path with attrs, and for server migrations the attr in the base path is server_uuid, not server_id. This patch fixes the base_path of the ServerMigration resource to use server_id stanza. Change-Id: I44335a22846f1a11ba60e8bb758b10c39e728897 Story: 2010633 Task: 47591 --- openstack/compute/v2/server_migration.py | 15 ++++++--------- .../unit/compute/v2/test_server_migration.py | 5 +++-- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/openstack/compute/v2/server_migration.py b/openstack/compute/v2/server_migration.py index eb89de1c8..42585ab32 100644 --- a/openstack/compute/v2/server_migration.py +++ b/openstack/compute/v2/server_migration.py @@ -18,15 +18,15 @@ class ServerMigration(resource.Resource): resource_key = 'migration' resources_key = 'migrations' - base_path = '/servers/%(server_uuid)s/migrations' + base_path = '/servers/%(server_id)s/migrations' # capabilities allow_fetch = True allow_list = True allow_delete = True - #: The ID for the server. - server_id = resource.URI('server_uuid') + #: The ID for the server from the URI of the resource + server_id = resource.URI('server_id') #: The date and time when the resource was created. created_at = resource.Body('created_at') @@ -53,11 +53,8 @@ class ServerMigration(resource.Resource): #: The ID of the project that initiated the server migration (since #: microversion 2.80) project_id = resource.Body('project_id') - # FIXME(stephenfin): This conflicts since there is a server ID in the URI - # *and* in the body. We need a field that handles both or we need to use - # different names. - # #: The UUID of the server - # server_id = resource.Body('server_uuid') + #: The UUID of the server from the response body + server_uuid = resource.Body('server_uuid') #: The source compute of the migration. source_compute = resource.Body('source_compute') #: The source node of the migration. @@ -80,7 +77,7 @@ def _action(self, session, body): microversion = self._get_microversion(session, action='list') url = utils.urljoin( - self.base_path % {'server_uuid': self.server_id}, + self.base_path % {'server_id': self.server_id}, self.id, 'action', ) diff --git a/openstack/tests/unit/compute/v2/test_server_migration.py b/openstack/tests/unit/compute/v2/test_server_migration.py index cc309f683..936a6ca9b 100644 --- a/openstack/tests/unit/compute/v2/test_server_migration.py +++ b/openstack/tests/unit/compute/v2/test_server_migration.py @@ -17,6 +17,7 @@ EXAMPLE = { 'id': 4, + 'server_id': '4cfba335-03d8-49b2-8c52-e69043d1e8fe', 'server_uuid': '4cfba335-03d8-49b2-8c52-e69043d1e8fe', 'user_id': '8dbaa0f0-ab95-4ffe-8cb4-9c89d2ac9d24', 'project_id': '5f705771-3aa9-4f4c-8660-0d9522ffdbea', @@ -51,7 +52,7 @@ def test_basic(self): sot = server_migration.ServerMigration() self.assertEqual('migration', sot.resource_key) self.assertEqual('migrations', sot.resources_key) - self.assertEqual('/servers/%(server_uuid)s/migrations', sot.base_path) + self.assertEqual('/servers/%(server_id)s/migrations', sot.base_path) self.assertFalse(sot.allow_create) self.assertTrue(sot.allow_fetch) self.assertTrue(sot.allow_list) @@ -105,7 +106,7 @@ def test_force_complete(self): self.assertIsNone(sot.force_complete(self.sess)) url = 'servers/%s/migrations/%s/action' % ( - EXAMPLE['server_uuid'], + EXAMPLE['server_id'], EXAMPLE['id'], ) body = {'force_complete': None} From 4e0d6938164fed823e09f54f191fb537c90fdaa1 Mon Sep 17 00:00:00 2001 From: silvacarloss Date: Tue, 25 Jul 2023 12:31:06 -0300 Subject: [PATCH 3453/3836] Resource locks and access rules restrictions Implement resource locks and access rules restrictions feature in the openstacksdk. Depends-On: Ib9f65a4523222f1224d51534c5061f90501b59d3 Change-Id: I45f9b06b1b41756d34f39604c82e28fd4eb102de --- .../user/proxies/shared_file_system.rst | 17 ++- .../resources/shared_file_system/index.rst | 1 + .../shared_file_system/v2/resource_locks.rst | 13 ++ openstack/proxy.py | 7 + openstack/shared_file_system/v2/_proxy.py | 141 +++++++++++++++++- .../shared_file_system/v2/resource_locks.py | 73 +++++++++ .../v2/share_access_rule.py | 17 ++- .../functional/shared_file_system/base.py | 14 +- .../shared_file_system/test_resource_lock.py | 96 ++++++++++++ .../test_share_access_rule.py | 13 ++ .../unit/shared_file_system/v2/test_proxy.py | 46 +++++- ...system-locks-support-4859ca93f93a1056.yaml | 8 + 12 files changed, 434 insertions(+), 12 deletions(-) create mode 100644 doc/source/user/resources/shared_file_system/v2/resource_locks.rst create mode 100644 openstack/shared_file_system/v2/resource_locks.py create mode 100644 openstack/tests/functional/shared_file_system/test_resource_lock.py create mode 100644 releasenotes/notes/add-shared-file-system-locks-support-4859ca93f93a1056.yaml diff --git a/doc/source/user/proxies/shared_file_system.rst b/doc/source/user/proxies/shared_file_system.rst index facbe03b2..2a3a756a0 100644 --- a/doc/source/user/proxies/shared_file_system.rst +++ b/doc/source/user/proxies/shared_file_system.rst @@ -133,7 +133,9 @@ Shared File System Share Access Rules ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Create, View, and Delete access rules for shares from the -Shared File Systems service. +Shared File Systems service. Access rules can also have their deletion +and visibility restricted during creation. A lock reason can also be +specified. The deletion restriction can be removed during the access removal. .. autoclass:: openstack.shared_file_system.v2._proxy.Proxy :noindex: @@ -177,3 +179,16 @@ Shared File Systems service. :members: get_share_metadata, get_share_metadata_item, create_share_metadata, update_share_metadata, delete_share_metadata + + +Shared File System Resource Locks +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Create, list, update and delete locks for resources. When a resource is +locked, it means that it can be deleted only by services, admins or +the user that created the lock. + +.. autoclass:: openstack.shared_file_system.v2._proxy.Proxy + :noindex: + :members: resource_locks, get_resource_lock, update_resource_lock, + delete_resource_lock, create_resource_lock diff --git a/doc/source/user/resources/shared_file_system/index.rst b/doc/source/user/resources/shared_file_system/index.rst index e2bd0488a..1b45f4f17 100644 --- a/doc/source/user/resources/shared_file_system/index.rst +++ b/doc/source/user/resources/shared_file_system/index.rst @@ -17,3 +17,4 @@ Shared File System service resources v2/share_group v2/share_access_rule v2/share_group_snapshot + v2/resource_locks diff --git a/doc/source/user/resources/shared_file_system/v2/resource_locks.rst b/doc/source/user/resources/shared_file_system/v2/resource_locks.rst new file mode 100644 index 000000000..6040bfa5a --- /dev/null +++ b/doc/source/user/resources/shared_file_system/v2/resource_locks.rst @@ -0,0 +1,13 @@ +openstack.shared_file_system.v2.resource_locks +============================================== + +.. automodule:: openstack.shared_file_system.v2.resource_locks + +The Resource Locks Class +------------------------ + +The ``ResourceLock`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.shared_file_system.v2.resource_locks.ResourceLock + :members: diff --git a/openstack/proxy.py b/openstack/proxy.py index 90e06b877..c058964a9 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -632,6 +632,13 @@ def _create( :returns: The result of the ``create`` :rtype: :class:`~openstack.resource.Resource` """ + # Check for attributes whose names conflict with the parameters + # specified in the method. + conflicting_attrs = attrs.get('__conflicting_attrs', {}) + if conflicting_attrs: + for k, v in conflicting_attrs.items(): + attrs[k] = v + attrs.pop('__conflicting_attrs') conn = self._get_connection() res = resource_type.new(connection=conn, **attrs) return res.create(self, base_path=base_path) diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index 76eae7557..56c5dfb74 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -16,6 +16,7 @@ availability_zone as _availability_zone, ) from openstack.shared_file_system.v2 import limit as _limit +from openstack.shared_file_system.v2 import resource_locks as _resource_locks from openstack.shared_file_system.v2 import share as _share from openstack.shared_file_system.v2 import share_group as _share_group from openstack.shared_file_system.v2 import ( @@ -56,6 +57,7 @@ class Proxy(proxy.Proxy): "share_access_rule": _share_access_rule.ShareAccessRule, "share_group": _share_group.ShareGroup, "share_group_snapshot": _share_group_snapshot.ShareGroupSnapshot, + "resource_locks": _resource_locks.ResourceLock, } def availability_zones(self): @@ -354,7 +356,13 @@ def delete_share_group(self, share_group_id, ignore_missing=True): ) def wait_for_status( - self, res, status='active', failures=None, interval=2, wait=120 + self, + res, + status='active', + failures=None, + interval=2, + wait=120, + status_attr_name='status', ): """Wait for a resource to be in a particular status. :param res: The resource to wait on to reach the specified status. @@ -367,6 +375,8 @@ def wait_for_status( checks. Default to 2. :param wait: Maximum number of seconds to wait before the change. Default to 120. + :param status_attr_name: name of the attribute to reach the desired + status. :returns: The resource is returned on success. :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition to the desired status failed to occur in specified seconds. @@ -377,7 +387,13 @@ def wait_for_status( """ failures = [] if failures is None else failures return resource.wait_for_status( - self, res, status, failures, interval, wait + self, + res, + status, + failures, + interval, + wait, + attribute=status_attr_name, ) def storage_pools(self, details=True, **query): @@ -846,17 +862,25 @@ def create_access_rule(self, share_id, **attrs): _share_access_rule.ShareAccessRule, base_path=base_path, **attrs ) - def delete_access_rule(self, access_id, share_id, ignore_missing=True): + def delete_access_rule( + self, access_id, share_id, ignore_missing=True, *, unrestrict=False + ): """Deletes an access rule :param access_id: The id of the access rule to get :param share_id: The ID of the share + :param unrestrict: If Manila must attempt removing locks while deleting :rtype: ``requests.models.Response`` HTTP response from internal requests client """ res = self._get_resource(_share_access_rule.ShareAccessRule, access_id) - res.delete(self, share_id, ignore_missing=ignore_missing) + return res.delete( + self, + share_id, + ignore_missing=ignore_missing, + unrestrict=unrestrict, + ) def share_group_snapshots(self, details=True, **query): """Lists all share group snapshots. @@ -1065,3 +1089,112 @@ def delete_share_metadata(self, share_id, keys, ignore_missing=True): raise exceptions.SDKException( "Some keys failed to be deleted %s" % keys_failed_to_delete ) + + def resource_locks(self, **query): + """Lists all resource locks. + + :param kwargs query: Optional query parameters to be sent to limit + the resource locks being returned. Available parameters include: + + * project_id: The project ID of the user that the lock is + created for. + * user_id: The ID of a user to filter resource locks by. + * all_projects: list locks from all projects (Admin Only) + * resource_id: The ID of the resource that the locks pertain to + filter resource locks by. + * resource_action: The action prevented by the filtered resource + locks. + * resource_type: The type of the resource that the locks pertain + to filter resource locks by. + * lock_context: The lock creator’s context to filter locks by. + * lock_reason: The lock reason that can be used to filter resource + locks. (Inexact search is also available with lock_reason~) + * created_since: Search for the list of resources that were created + after the specified date. The date is in ‘yyyy-mm-dd’ format. + * created_before: Search for the list of resources that were + created prior to the specified date. The date is in + ‘yyyy-mm-dd’ format. + * limit: The maximum number of resource locks to return. + * offset: The offset to define start point of resource lock + listing. + * sort_key: The key to sort a list of shares. + * sort_dir: The direction to sort a list of shares + * with_count: Whether to show count in API response or not, + default is False. This query parameter is useful with + pagination. + + :returns: A generator of manila resource locks + :rtype: :class:`~openstack.shared_file_system.v2. + resource_locks.ResourceLock` + """ + return self._list(_resource_locks.ResourceLock, **query) + + def get_resource_lock(self, resource_lock): + """Show details of a resource lock. + + :param resource_lock: The ID of a resource lock or a + :class:`~openstack.shared_file_system.v2. + resource_locks.ResourceLock` instance. + :returns: Details of the identified resource lock. + :rtype: :class:`~openstack.shared_file_system.v2. + resource_locks.ResourceLock` + """ + return self._get(_resource_locks.ResourceLock, resource_lock) + + def update_resource_lock(self, resource_lock, **attrs): + """Updates details of a single resource lock. + + :param resource_lock: The ID of a resource lock or a + :class:`~openstack.shared_file_system.v2. + resource_locks.ResourceLock` instance. + :param dict attrs: The attributes to update on the resource lock + :returns: the updated resource lock + :rtype: :class:`~openstack.shared_file_system.v2. + resource_locks.ResourceLock` + """ + return self._update( + _resource_locks.ResourceLock, resource_lock, **attrs + ) + + def delete_resource_lock(self, resource_lock, ignore_missing=True): + """Deletes a single resource lock + + :param resource_lock: The ID of a resource lock or a + :class:`~openstack.shared_file_system.v2. + resource_locks.ResourceLock` instance. + :returns: Result of the ``delete`` + :rtype: ``None`` + """ + return self._delete( + _resource_locks.ResourceLock, + resource_lock, + ignore_missing=ignore_missing, + ) + + def create_resource_lock(self, **attrs): + """Locks a resource. + + :param dict attrs: Attributes which will be used to create + a :class:`~openstack.shared_file_system.v2. + resource_locks.ResourceLock`, comprised of the properties + on the ResourceLock class. Available parameters include: + + * ``resource_id``: ID of the resource to be locked. + * ``resource_type``: type of the resource (share, access_rule). + * ``resource_action``: action to be locked (delete, show). + * ``lock_reason``: reason why you're locking the resource + (Optional). + :returns: Details of the lock + :rtype: :class:`~openstack.shared_file_system.v2. + resource_locks.ResourceLock` + """ + + if attrs.get('resource_type'): + # The _create method has a parameter named resource_type, which + # refers to the type of resource to be created, so we need to avoid + # a conflict of parameters we are sending to the method. + attrs['__conflicting_attrs'] = { + 'resource_type': attrs.get('resource_type') + } + attrs.pop('resource_type') + return self._create(_resource_locks.ResourceLock, **attrs) diff --git a/openstack/shared_file_system/v2/resource_locks.py b/openstack/shared_file_system/v2/resource_locks.py new file mode 100644 index 000000000..2d5731e13 --- /dev/null +++ b/openstack/shared_file_system/v2/resource_locks.py @@ -0,0 +1,73 @@ +# 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 resource + + +class ResourceLock(resource.Resource): + resource_key = "resource_lock" + resources_key = "resource_locks" + base_path = "/resource-locks" + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + allow_head = False + + _query_mapping = resource.QueryParameters( + "project_id", + "created_since", + "created_before", + "limit", + "offset", + "id", + "resource_id", + "resource_type", + "resource_action", + "user_id", + "lock_context", + "lock_reason", + "lock_reason~", + "sort_key", + "sort_dir", + "with_count", + "all_projects", + ) + # The resource was introduced in this microversion, so it is the minimum + # version to use it. Openstacksdk currently doesn't allow to set + # minimum microversions. + _max_microversion = '2.81' + + #: Properties + #: The date and time stamp when the resource was created within the + #: service’s database. + created_at = resource.Body("created_at", type=str) + #: The date and time stamp when the resource was last modified within the + #: service’s database. + updated_at = resource.Body("updated_at", type=str) + #: The ID of the user that owns the lock + user_id = resource.Body("user_id", type=str) + #: The ID of the project that owns the lock. + project_id = resource.Body("project_id", type=str) + #: The type of the resource that is locked, i.e.: share, access rule. + resource_type = resource.Body("resource_type", type=str) + #: The UUID of the resource that is locked. + resource_id = resource.Body("resource_id", type=str) + #: What action is currently locked, i.e.: deletion, visibility of fields. + resource_action = resource.Body("resource_action", type=str) + #: The reason specified while the lock was being placed. + lock_reason = resource.Body("lock_reason", type=str) + #: The context that placed the lock (user, admin or service). + lock_context = resource.Body("lock_context", type=str) diff --git a/openstack/shared_file_system/v2/share_access_rule.py b/openstack/shared_file_system/v2/share_access_rule.py index 519c679b9..66be84223 100644 --- a/openstack/shared_file_system/v2/share_access_rule.py +++ b/openstack/shared_file_system/v2/share_access_rule.py @@ -16,7 +16,7 @@ class ShareAccessRule(resource.Resource): - resource_key = "share_access_rule" + resource_key = "access" resources_key = "access_list" base_path = "/share-access-rules" @@ -30,7 +30,8 @@ class ShareAccessRule(resource.Resource): _query_mapping = resource.QueryParameters("share_id") - _max_microversion = '2.45' + # Restricted access rules became available in 2.82 + _max_microversion = '2.82' #: Properties #: The access credential of the entity granted share access. @@ -56,6 +57,12 @@ class ShareAccessRule(resource.Resource): #: The date and time stamp when the resource was last updated within #: the service’s database. updated_at = resource.Body("updated_at", type=str) + #: Whether the visibility of some sensitive fields is restricted or not + lock_visibility = resource.Body("lock_visibility", type=bool) + #: Whether the deletion of the access rule should be restricted or not + lock_deletion = resource.Body("lock_deletion", type=bool) + #: Reason for placing the loc + lock_reason = resource.Body("lock_reason", type=bool) def _action(self, session, body, url, action='patch', microversion=None): headers = {'Accept': ''} @@ -75,8 +82,12 @@ def create(self, session, **kwargs): **kwargs ) - def delete(self, session, share_id, ignore_missing=True): + def delete( + self, session, share_id, ignore_missing=True, *, unrestrict=False + ): body = {"deny_access": {"access_id": self.id}} + if unrestrict: + body['deny_access']['unrestrict'] = True url = utils.urljoin("/shares", share_id, "action") response = self._action(session, body, url) try: diff --git a/openstack/tests/functional/shared_file_system/base.py b/openstack/tests/functional/shared_file_system/base.py index f12a48c6b..4a1d5a295 100644 --- a/openstack/tests/functional/shared_file_system/base.py +++ b/openstack/tests/functional/shared_file_system/base.py @@ -22,8 +22,8 @@ def setUp(self): self.require_service( 'shared-file-system', min_microversion=self.min_microversion ) - self._set_operator_cloud(shared_file_system_api_version='2.78') - self._set_user_cloud(shared_file_system_api_version='2.78') + self._set_operator_cloud(shared_file_system_api_version='2.82') + self._set_user_cloud(shared_file_system_api_version='2.82') def create_share(self, **kwargs): share = self.user_cloud.share.create_share(**kwargs) @@ -75,3 +75,13 @@ def create_share_group(self, **kwargs): ) self.assertIsNotNone(share_group.id) return share_group + + def create_resource_lock(self, **kwargs): + resource_lock = self.user_cloud.share.create_resource_lock(**kwargs) + self.addCleanup( + self.user_cloud.share.delete_resource_lock, + resource_lock.id, + ignore_missing=True, + ) + self.assertIsNotNone(resource_lock.id) + return resource_lock diff --git a/openstack/tests/functional/shared_file_system/test_resource_lock.py b/openstack/tests/functional/shared_file_system/test_resource_lock.py new file mode 100644 index 000000000..e4f2b0351 --- /dev/null +++ b/openstack/tests/functional/shared_file_system/test_resource_lock.py @@ -0,0 +1,96 @@ +# 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.shared_file_system.v2 import resource_locks as _resource_locks +from openstack.tests.functional.shared_file_system import base + + +class ResourceLocksTest(base.BaseSharedFileSystemTest): + def setUp(self): + super(ResourceLocksTest, self).setUp() + + self.SHARE_NAME = self.getUniqueString() + share = self.user_cloud.shared_file_system.create_share( + name=self.SHARE_NAME, + size=2, + share_type="dhss_false", + share_protocol='NFS', + description=None, + ) + self.SHARE_ID = share.id + self.user_cloud.shared_file_system.wait_for_status( + share, + status='available', + failures=['error'], + interval=5, + wait=self._wait_for_timeout, + ) + access_rule = self.user_cloud.share.create_access_rule( + self.SHARE_ID, + access_level="rw", + access_type="ip", + access_to="0.0.0.0/0", + ) + self.user_cloud.shared_file_system.wait_for_status( + access_rule, + status='active', + failures=['error'], + interval=5, + wait=self._wait_for_timeout, + status_attr_name='state', + ) + self.assertIsNotNone(share) + self.assertIsNotNone(share.id) + self.ACCESS_ID = access_rule.id + share_lock = self.create_resource_lock( + resource_action='delete', + resource_type='share', + resource_id=self.SHARE_ID, + lock_reason='openstacksdk testing', + ) + access_lock = self.create_resource_lock( + resource_action='show', + resource_type='access_rule', + resource_id=self.ACCESS_ID, + lock_reason='openstacksdk testing', + ) + self.SHARE_LOCK_ID = share_lock.id + self.ACCESS_LOCK_ID = access_lock.id + + def test_get(self): + share_lock = self.user_cloud.shared_file_system.get_resource_lock( + self.SHARE_LOCK_ID + ) + access_lock = self.user_cloud.shared_file_system.get_resource_lock( + self.ACCESS_LOCK_ID + ) + assert isinstance(share_lock, _resource_locks.ResourceLock) + assert isinstance(access_lock, _resource_locks.ResourceLock) + self.assertEqual(self.SHARE_LOCK_ID, share_lock.id) + self.assertEqual(self.ACCESS_LOCK_ID, access_lock.id) + self.assertEqual('show', access_lock.resource_action) + + def test_list(self): + resource_locks = self.user_cloud.share.resource_locks() + self.assertGreater(len(list(resource_locks)), 0) + lock_attrs = ( + 'id', + 'lock_reason', + 'resource_type', + 'resource_action', + 'lock_context', + 'created_at', + 'updated_at', + ) + for lock in resource_locks: + for attribute in lock_attrs: + self.assertTrue(hasattr(lock, attribute)) diff --git a/openstack/tests/functional/shared_file_system/test_share_access_rule.py b/openstack/tests/functional/shared_file_system/test_share_access_rule.py index d8dc4d85b..7fc7817a5 100644 --- a/openstack/tests/functional/shared_file_system/test_share_access_rule.py +++ b/openstack/tests/functional/shared_file_system/test_share_access_rule.py @@ -75,3 +75,16 @@ def test_list_access_rules(self): 'metadata', ): self.assertTrue(hasattr(rule, attribute)) + + def test_create_delete_access_rule_with_locks(self): + access_rule = self.user_cloud.share.create_access_rule( + self.SHARE_ID, + access_level="rw", + access_type="ip", + access_to="203.0.113.10", + lock_deletion=True, + lock_visibility=True, + ) + self.user_cloud.share.delete_access_rule( + access_rule['id'], self.SHARE_ID, unrestrict=True + ) diff --git a/openstack/tests/unit/shared_file_system/v2/test_proxy.py b/openstack/tests/unit/shared_file_system/v2/test_proxy.py index 921318f64..a562a450b 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_proxy.py +++ b/openstack/tests/unit/shared_file_system/v2/test_proxy.py @@ -14,6 +14,7 @@ from openstack.shared_file_system.v2 import _proxy from openstack.shared_file_system.v2 import limit +from openstack.shared_file_system.v2 import resource_locks from openstack.shared_file_system.v2 import share from openstack.shared_file_system.v2 import share_access_rule from openstack.shared_file_system.v2 import share_group @@ -130,7 +131,7 @@ def test_wait_for(self, mock_wait): self.proxy.wait_for_status(mock_resource, 'ACTIVE') mock_wait.assert_called_once_with( - self.proxy, mock_resource, 'ACTIVE', [], 2, 120 + self.proxy, mock_resource, 'ACTIVE', [], 2, 120, attribute='status' ) @@ -473,8 +474,49 @@ def test_access_rules_delete(self): "openstack.shared_file_system.v2.share_access_rule." + "ShareAccessRule.delete", self.proxy.delete_access_rule, - method_args=['access_id', 'share_id', 'ignore_missing'], + method_args=[ + 'access_id', + 'share_id', + 'ignore_missing', + ], expected_args=[self.proxy, 'share_id'], + expected_kwargs={'unrestrict': False}, + ) + + +class TestResourceLocksProxy(test_proxy_base.TestProxyBase): + def setUp(self): + super(TestResourceLocksProxy, self).setUp() + self.proxy = _proxy.Proxy(self.session) + + def test_list_resource_locks(self): + self.verify_list( + self.proxy.resource_locks, resource_locks.ResourceLock + ) + + def test_resource_lock_get(self): + self.verify_get( + self.proxy.get_resource_lock, resource_locks.ResourceLock + ) + + def test_resource_lock_delete(self): + self.verify_delete( + self.proxy.delete_resource_lock, resource_locks.ResourceLock, False + ) + + def test_resource_lock_delete_ignore(self): + self.verify_delete( + self.proxy.delete_resource_lock, resource_locks.ResourceLock, True + ) + + def test_resource_lock_create(self): + self.verify_create( + self.proxy.create_resource_lock, resource_locks.ResourceLock + ) + + def test_resource_lock_update(self): + self.verify_update( + self.proxy.update_resource_lock, resource_locks.ResourceLock ) diff --git a/releasenotes/notes/add-shared-file-system-locks-support-4859ca93f93a1056.yaml b/releasenotes/notes/add-shared-file-system-locks-support-4859ca93f93a1056.yaml new file mode 100644 index 000000000..c6dca5601 --- /dev/null +++ b/releasenotes/notes/add-shared-file-system-locks-support-4859ca93f93a1056.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Added support to manipulate resource locks from the shared file system + service. + - | + Added support to restrict the visibility and deletion of the shared file + system share access rules. From 31fe67b835a9ebf2f9b90e47f274033139808782 Mon Sep 17 00:00:00 2001 From: Clark Boylan Date: Mon, 26 Feb 2024 09:18:05 -0800 Subject: [PATCH 3454/3836] Fix the mailing list domain in documentation link The docs had the openstack-discuss mailing list living at the @openstack.org domain. In reality this list lives at the @lists.openstack.org domain. Correct this. Change-Id: Ifee33c8e5757b231d6cc6c3341d7d23072fce94d --- doc/source/contributor/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst index c651c76ec..d25021d00 100644 --- a/doc/source/contributor/index.rst +++ b/doc/source/contributor/index.rst @@ -46,7 +46,7 @@ The `openstack-discuss`__ mailing list fields questions of all types on OpenStack. Using the ``[sdk]`` filter to begin your email subject will ensure that the message gets to SDK developers. -.. __: mailto:openstack-discuss@openstack.org?subject=[sdk]%20Question%20about%20openstacksdk +.. __: mailto:openstack-discuss@lists.openstack.org?subject=[sdk]%20Question%20about%20openstacksdk Coding Standards ---------------- From 4f46ab3f8bf200b227065ea80f34fb2736e3ba18 Mon Sep 17 00:00:00 2001 From: Rajesh Tailor Date: Tue, 2 Jan 2024 17:50:58 +0530 Subject: [PATCH 3455/3836] Add support for showing requested az in output This change adds support for showing the availability zone requested during instance create server show and server list --long output. Also bump the _max_microversion to 2.96 to use the newly added microversion. Depends-On: https://review.opendev.org/c/openstack/nova/+/904568 Change-Id: Iafedc1b7eba682dedaf0bcb0a5af79e85501679a --- openstack/compute/v2/server.py | 7 ++++++- openstack/tests/unit/compute/v2/test_server.py | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 817924618..3c0376b4c 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -76,6 +76,7 @@ class Server(resource.Resource, metadata.MetadataMixin, tag.TagMixin): "vm_state", "sort_key", "sort_dir", + "pinned_availability_zone", access_ipv4="access_ip_v4", access_ipv6="access_ip_v6", has_config_drive="config_drive", @@ -91,7 +92,7 @@ class Server(resource.Resource, metadata.MetadataMixin, tag.TagMixin): **tag.TagMixin._tag_query_parameters, ) - _max_microversion = '2.91' + _max_microversion = '2.96' #: A list of dictionaries holding links relevant to this server. links = resource.Body('links') @@ -192,6 +193,10 @@ class Server(resource.Resource, metadata.MetadataMixin, tag.TagMixin): #: networks parameter, the server attaches to the only network #: created for the current tenant. networks = resource.Body('networks') + #: The availability zone requested during server creation OR pinned + #: availability zone, which is configured using default_schedule_zone + #: config option. + pinned_availability_zone = resource.Body('pinned_availability_zone') #: The power state of this server. power_state = resource.Body('OS-EXT-STS:power_state') #: While the server is building, this value represents the percentage diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 88a545579..4eb29271a 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -166,6 +166,7 @@ def test_basic(self): "progress": "progress", "project_id": "project_id", "ramdisk_id": "ramdisk_id", + "pinned_availability_zone": "pinned_availability_zone", "reservation_id": "reservation_id", "root_device_name": "root_device_name", "sort_dir": "sort_dir", From e7bdb02f90de6d7839fadd2dff8e6019a5d8b74e Mon Sep 17 00:00:00 2001 From: Jan Hartkopf Date: Wed, 29 Nov 2023 17:36:46 +0100 Subject: [PATCH 3456/3836] Allow project switching for Designate API Contrary to other OpenStack APIs, the Designate v2 API implements project switching based on HTTP headers, especially x-auth-sudo-project-id and x-auth-all-projects. This commit adds feature parity with other OpenStack APIs, i. e. direct usage of parameters "project_id" and "all_projects" for DNS resource list operations. Story: 2010909 Task: 48748 Change-Id: I1cb1efbf1f243cca0c5bb6e1058d25b2ad863355 Signed-off-by: Jan Hartkopf --- openstack/dns/v2/_base.py | 20 +++++++++++ openstack/dns/v2/_proxy.py | 2 +- openstack/proxy.py | 2 +- openstack/resource.py | 9 ++++- openstack/tests/unit/test_resource.py | 33 +++++++++++++++++++ ...urce-list-by-project-8b5479a045ef7373.yaml | 6 ++++ 6 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/add-dns-resource-list-by-project-8b5479a045ef7373.yaml diff --git a/openstack/dns/v2/_base.py b/openstack/dns/v2/_base.py index 0cac1eac5..a72513a7a 100644 --- a/openstack/dns/v2/_base.py +++ b/openstack/dns/v2/_base.py @@ -72,6 +72,26 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): "No %s found for %s" % (cls.__name__, name_or_id) ) + @classmethod + def list( + cls, + session, + project_id=None, + all_projects=None, + **params, + ): + headers: ty.Union[ty.Dict[str, str] | None] = ( + {} if project_id or all_projects else None + ) + + if headers is not None: + if project_id: + headers["x-auth-sudo-project-id"] = str(project_id) + if all_projects: + headers["x-auth-all-projects"] = str(all_projects) + + return super().list(session=session, headers=headers, **params) + @classmethod def _get_next_link(cls, uri, response, data, marker, limit, total_yielded): next_link = None diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py index de2ee5742..9c71b2f5f 100644 --- a/openstack/dns/v2/_proxy.py +++ b/openstack/dns/v2/_proxy.py @@ -561,7 +561,7 @@ def create_zone_transfer_accept(self, **attrs): # ======== Zone Shares ======== def zone_shares(self, zone, **query): - """Retrieve a generator of zone sharess + """Retrieve a generator of zone shares :param zone: The zone ID or a :class:`~openstack.dns.v2.zone.Zone` instance diff --git a/openstack/proxy.py b/openstack/proxy.py index c058964a9..41144159b 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -880,7 +880,7 @@ def should_skip_resource_cleanup(self, resource=None, skip_resources=None): if resource_name in skip_resources: self.log.debug( - f"Skipping resource {resource_name} " "in project cleanup" + f"Skipping resource {resource_name} in project cleanup" ) return True diff --git a/openstack/resource.py b/openstack/resource.py index 53b786176..ca4196c09 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1990,6 +1990,7 @@ def list( allow_unknown_params=False, *, microversion=None, + headers=None, **params, ): """This method is a generator which yields resource objects. @@ -2010,6 +2011,8 @@ def list( passing everything known to the server. ``False`` will result in validation exception when unknown query parameters are passed. :param str microversion: API version to override the negotiated one. + :param dict headers: Additional headers to inject into the HTTP + request. :param dict params: These keyword arguments are passed through the :meth:`~openstack.resource.QueryParamter._transpose` method to find if any of them match expected query parameters to be sent @@ -2079,6 +2082,10 @@ def _dict_filter(f, d): return False return True + headers_final = {"Accept": "application/json"} + if headers: + headers_final = {**headers_final, **headers} + # Track the total number of resources yielded so we can paginate # swift objects total_yielded = 0 @@ -2086,7 +2093,7 @@ def _dict_filter(f, d): # Copy query_params due to weird mock unittest interactions response = session.get( uri, - headers={"Accept": "application/json"}, + headers=headers_final, params=query_params.copy(), microversion=microversion, ) diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 7d09eda4b..253db463f 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -18,6 +18,7 @@ from keystoneauth1 import adapter import requests +from openstack import dns from openstack import exceptions from openstack import format from openstack import resource @@ -2651,6 +2652,38 @@ class Test(self.test_class): Test.base_path % {"something": uri_param}, ) + def test_list_with_injected_headers(self): + mock_empty = mock.Mock() + mock_empty.status_code = 200 + mock_empty.json.return_value = {"resources": []} + + self.session.get.side_effect = [mock_empty] + + _ = list( + self.test_class.list(self.session, headers={'X-Test': 'value'}) + ) + + expected = {'Accept': 'application/json', 'X-Test': 'value'} + self.assertEqual( + expected, self.session.get.call_args.kwargs['headers'] + ) + + @mock.patch.object(resource.Resource, 'list') + def test_list_dns_with_headers(self, mock_resource_list): + dns.v2._base.Resource.list( + self.session, + project_id='1234', + all_projects=True, + ) + + expected = { + 'x-auth-sudo-project-id': '1234', + 'x-auth-all-projects': 'True', + } + self.assertEqual( + expected, mock_resource_list.call_args.kwargs['headers'] + ) + def test_allow_invalid_list_params(self): qp = "query param!" qp_name = "query-param" diff --git a/releasenotes/notes/add-dns-resource-list-by-project-8b5479a045ef7373.yaml b/releasenotes/notes/add-dns-resource-list-by-project-8b5479a045ef7373.yaml new file mode 100644 index 000000000..f79ed22c0 --- /dev/null +++ b/releasenotes/notes/add-dns-resource-list-by-project-8b5479a045ef7373.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add functionality to list DNS resources for a certain project only, + or for all projects, using the new `project_id` and `all_projects` + parameters. From 16ddc1ca79b7c6d742088af1fa24a925b8daa35a Mon Sep 17 00:00:00 2001 From: Mridula Joshi Date: Tue, 13 Feb 2024 18:12:13 +0000 Subject: [PATCH 3457/3836] Fixed update_metadef_object Missed to retrieve the id of the metadef object. Added the statement to fetch the id of the metadef_object to be updated Change-Id: I99f334fcd3a5a3886cdf734747a018e8bca61099 --- openstack/image/v2/_proxy.py | 1 + openstack/tests/unit/image/v2/test_proxy.py | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 776386631..eef44837b 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -1282,6 +1282,7 @@ def update_metadef_object(self, metadef_object, namespace, **attrs): resource can be found. """ namespace_name = resource.Resource._get_id(namespace) + metadef_object = resource.Resource._get_id(metadef_object) return self._update( _metadef_object.MetadefObject, metadef_object, diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index d74777cc9..4e515ff37 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -600,11 +600,19 @@ def test_metadef_objects(self): ) def test_update_metadef_object(self): - self.verify_update( + self._verify( + "openstack.proxy.Proxy._update", self.proxy.update_metadef_object, - _metadef_object.MetadefObject, - method_kwargs={"namespace": "test_namespace_name"}, - expected_kwargs={"namespace_name": "test_namespace_name"}, + method_args=["test_metadef_object", "test_namespace_name"], + method_kwargs={"name": "new_object"}, + expected_args=[ + _metadef_object.MetadefObject, + 'test_metadef_object', + ], + expected_kwargs={ + "name": "new_object", + "namespace_name": "test_namespace_name", + }, ) def test_delete_metadef_object(self): From af2c520b91f9a29173a4c2f51ab2e949ad8c47ec Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Thu, 7 Mar 2024 08:43:47 +0000 Subject: [PATCH 3458/3836] reno: Update master for unmaintained/victoria Update the victoria release notes configuration to build from unmaintained/victoria. Change-Id: Iafc5829b229ccb384249b531340cbeb88ad327b7 --- releasenotes/source/victoria.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/source/victoria.rst b/releasenotes/source/victoria.rst index 4efc7b6f3..8ce933419 100644 --- a/releasenotes/source/victoria.rst +++ b/releasenotes/source/victoria.rst @@ -3,4 +3,4 @@ Victoria Series Release Notes ============================= .. release-notes:: - :branch: stable/victoria + :branch: unmaintained/victoria From aabe1e04ba61d912e6f2c6da3fc62bc04af8ed1a Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Thu, 7 Mar 2024 08:44:45 +0000 Subject: [PATCH 3459/3836] reno: Update master for unmaintained/wallaby Update the wallaby release notes configuration to build from unmaintained/wallaby. Change-Id: I1a52b619d8b7646bf606494188971870bf133b50 --- releasenotes/source/wallaby.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/source/wallaby.rst b/releasenotes/source/wallaby.rst index d77b56599..bcf35c5f8 100644 --- a/releasenotes/source/wallaby.rst +++ b/releasenotes/source/wallaby.rst @@ -3,4 +3,4 @@ Wallaby Series Release Notes ============================ .. release-notes:: - :branch: stable/wallaby + :branch: unmaintained/wallaby From c689bbaf60b1e4b6b3082d7ce48d664d51baf0a0 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Thu, 7 Mar 2024 08:45:52 +0000 Subject: [PATCH 3460/3836] reno: Update master for unmaintained/xena Update the xena release notes configuration to build from unmaintained/xena. Change-Id: I4f760e1f19e74dc252e1af5fa21f1c7cd039f42c --- releasenotes/source/xena.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/source/xena.rst b/releasenotes/source/xena.rst index 1be85be3e..d19eda488 100644 --- a/releasenotes/source/xena.rst +++ b/releasenotes/source/xena.rst @@ -3,4 +3,4 @@ Xena Series Release Notes ========================= .. release-notes:: - :branch: stable/xena + :branch: unmaintained/xena From 4b1e75aad618a0644157dc6f407dc682ad9c4afe Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 8 Mar 2024 13:57:08 +0000 Subject: [PATCH 3461/3836] Update master for stable/2024.1 Add file to the reno documentation build to show release notes for stable/2024.1. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/2024.1. Sem-Ver: feature Change-Id: Ic938d8d02cab2e7aa74008d33d2bedef60de43af --- releasenotes/source/2024.1.rst | 6 ++++++ releasenotes/source/index.rst | 1 + 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/2024.1.rst diff --git a/releasenotes/source/2024.1.rst b/releasenotes/source/2024.1.rst new file mode 100644 index 000000000..4977a4f1a --- /dev/null +++ b/releasenotes/source/2024.1.rst @@ -0,0 +1,6 @@ +=========================== +2024.1 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2024.1 diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index cf39fb80b..161acda0e 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ :maxdepth: 1 unreleased + 2024.1 2023.2 2023.1 zed From 8b84bf0e59db7cf9b20d6a54f6a3ec76d622d081 Mon Sep 17 00:00:00 2001 From: Cyril Roelandt Date: Tue, 19 Mar 2024 04:12:47 +0100 Subject: [PATCH 3462/3836] image: make sure the target for "clear_cache" is valid This commit: - makes "both" the default target for clear_cache, as described by the documentation; - makes sure an InvalidRequest exception is raised if another target is passed. Change-Id: I61fccad78fc1b280395e0c590caaa2ee73586d93 --- openstack/image/v2/_proxy.py | 2 +- openstack/image/v2/cache.py | 6 +++++- openstack/tests/unit/image/v2/test_cache.py | 17 +++++++++++++++++ openstack/tests/unit/image/v2/test_proxy.py | 9 +++++++++ 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 776386631..accdf0685 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -101,7 +101,7 @@ def queue_image(self, image_id): cache = self._get_resource(_cache.Cache, None) return cache.queue(self, image_id) - def clear_cache(self, target): + def clear_cache(self, target='both'): """Clear all images from cache, queue or both :param target: Specify which target you want to clear diff --git a/openstack/image/v2/cache.py b/openstack/image/v2/cache.py index 54cc0d92e..1b240afd3 100644 --- a/openstack/image/v2/cache.py +++ b/openstack/image/v2/cache.py @@ -62,8 +62,12 @@ def clear(self, session, target='both'): :returns: The server response """ headers = {} - if target != "both": + if target in ('cache', 'queue'): headers = {'x-image-cache-clear-target': target} + elif target != "both": + raise exceptions.InvalidRequest( + 'Target must be "cache", "queue" or "both".' + ) response = session.delete(self.base_path, headers=headers) exceptions.raise_from_response(response) return response diff --git a/openstack/tests/unit/image/v2/test_cache.py b/openstack/tests/unit/image/v2/test_cache.py index 81415c948..458f361df 100644 --- a/openstack/tests/unit/image/v2/test_cache.py +++ b/openstack/tests/unit/image/v2/test_cache.py @@ -69,5 +69,22 @@ def test_clear(self): session = mock.Mock() session.delete = mock.Mock() + sot.clear(session) + session.delete.assert_called_with('/cache', headers={}) + sot.clear(session, 'both') session.delete.assert_called_with('/cache', headers={}) + + sot.clear(session, 'cache') + session.delete.assert_called_with( + '/cache', headers={'x-image-cache-clear-target': 'cache'} + ) + + sot.clear(session, 'queue') + session.delete.assert_called_with( + '/cache', headers={'x-image-cache-clear-target': 'queue'} + ) + + self.assertRaises( + exceptions.InvalidRequest, sot.clear, session, 'invalid' + ) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index d74777cc9..2cd7aaa05 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -974,3 +974,12 @@ def test_image_clear_cache(self, mock_get_resource): expected_args=[self.proxy, 'both'], ) mock_get_resource.assert_called_once_with(_cache.Cache, None) + + mock_get_resource.reset_mock() + self._verify( + "openstack.image.v2.cache.Cache.clear", + self.proxy.clear_cache, + method_args=[], + expected_args=[self.proxy, 'both'], + ) + mock_get_resource.assert_called_once_with(_cache.Cache, None) From afdf1549d45d92a1f50402794d8f82cde66976e7 Mon Sep 17 00:00:00 2001 From: elajkat Date: Wed, 21 Feb 2024 13:59:39 +0100 Subject: [PATCH 3463/3836] Add sort_key and sort_dir to core Neutron resources Change-Id: Ia53396c05a99dbbdba668c6966e9c601965d001f Related-Bug: #1999774 --- openstack/network/v2/address_group.py | 4 ++-- openstack/network/v2/address_scope.py | 2 ++ openstack/network/v2/flavor.py | 2 ++ openstack/network/v2/floating_ip.py | 2 ++ openstack/network/v2/local_ip.py | 4 ++-- openstack/network/v2/local_ip_association.py | 2 ++ openstack/network/v2/metering_label.py | 2 ++ openstack/network/v2/metering_label_rule.py | 2 ++ openstack/network/v2/network.py | 2 ++ openstack/network/v2/network_ip_availability.py | 2 ++ openstack/network/v2/network_segment_range.py | 2 ++ openstack/network/v2/port.py | 2 ++ openstack/network/v2/port_forwarding.py | 2 ++ openstack/network/v2/qos_policy.py | 2 ++ openstack/network/v2/router.py | 2 ++ openstack/network/v2/segment.py | 2 ++ openstack/network/v2/subnet.py | 2 ++ openstack/network/v2/subnet_pool.py | 2 ++ openstack/tests/unit/network/v2/test_floating_ip.py | 2 ++ openstack/tests/unit/network/v2/test_local_ip_association.py | 2 ++ openstack/tests/unit/network/v2/test_network.py | 2 ++ openstack/tests/unit/network/v2/test_port.py | 2 ++ openstack/tests/unit/network/v2/test_port_forwarding.py | 2 ++ 23 files changed, 46 insertions(+), 4 deletions(-) diff --git a/openstack/network/v2/address_group.py b/openstack/network/v2/address_group.py index 73bbc6344..9aebf49e6 100644 --- a/openstack/network/v2/address_group.py +++ b/openstack/network/v2/address_group.py @@ -32,8 +32,8 @@ class AddressGroup(resource.Resource): _allow_unknown_attrs_in_body = True _query_mapping = resource.QueryParameters( - "sort_key", - "sort_dir", + 'sort_key', + 'sort_dir', 'name', 'description', 'project_id', diff --git a/openstack/network/v2/address_scope.py b/openstack/network/v2/address_scope.py index 3b1aac26e..ca1798826 100644 --- a/openstack/network/v2/address_scope.py +++ b/openstack/network/v2/address_scope.py @@ -33,6 +33,8 @@ class AddressScope(resource.Resource): 'name', 'ip_version', 'project_id', + 'sort_key', + 'sort_dir', is_shared='shared', ) diff --git a/openstack/network/v2/flavor.py b/openstack/network/v2/flavor.py index 97f35efb4..364a4248e 100644 --- a/openstack/network/v2/flavor.py +++ b/openstack/network/v2/flavor.py @@ -32,6 +32,8 @@ class Flavor(resource.Resource): 'description', 'name', 'service_type', + 'sort_key', + 'sort_dir', is_enabled='enabled', ) diff --git a/openstack/network/v2/floating_ip.py b/openstack/network/v2/floating_ip.py index 17db58ea7..e2a2c3df3 100644 --- a/openstack/network/v2/floating_ip.py +++ b/openstack/network/v2/floating_ip.py @@ -40,6 +40,8 @@ class FloatingIP(_base.NetworkResource, tag.TagMixin): 'subnet_id', 'project_id', 'tenant_id', + 'sort_key', + 'sort_dir', tenant_id='project_id', **tag.TagMixin._tag_query_parameters ) diff --git a/openstack/network/v2/local_ip.py b/openstack/network/v2/local_ip.py index 74beb123c..35b61c536 100644 --- a/openstack/network/v2/local_ip.py +++ b/openstack/network/v2/local_ip.py @@ -34,8 +34,8 @@ class LocalIP(resource.Resource): _allow_unknown_attrs_in_body = True _query_mapping = resource.QueryParameters( - "sort_key", - "sort_dir", + 'sort_key', + 'sort_dir', 'name', 'description', 'project_id', diff --git a/openstack/network/v2/local_ip_association.py b/openstack/network/v2/local_ip_association.py index c8ecfad09..17d09b513 100644 --- a/openstack/network/v2/local_ip_association.py +++ b/openstack/network/v2/local_ip_association.py @@ -36,6 +36,8 @@ class LocalIPAssociation(resource.Resource): 'fixed_port_id', 'fixed_ip', 'host', + 'sort_key', + 'sort_dir', ) # Properties diff --git a/openstack/network/v2/metering_label.py b/openstack/network/v2/metering_label.py index 0bef6b11c..0ae1caaf3 100644 --- a/openstack/network/v2/metering_label.py +++ b/openstack/network/v2/metering_label.py @@ -31,6 +31,8 @@ class MeteringLabel(resource.Resource): 'description', 'name', 'project_id', + 'sort_key', + 'sort_dir', is_shared='shared', ) diff --git a/openstack/network/v2/metering_label_rule.py b/openstack/network/v2/metering_label_rule.py index 85c7e6ae4..3347ec2e0 100644 --- a/openstack/network/v2/metering_label_rule.py +++ b/openstack/network/v2/metering_label_rule.py @@ -34,6 +34,8 @@ class MeteringLabelRule(resource.Resource): 'source_ip_prefix', 'destination_ip_prefix', 'project_id', + 'sort_key', + 'sort_dir', ) # Properties diff --git a/openstack/network/v2/network.py b/openstack/network/v2/network.py index 9f9f94f0a..8382e0e5f 100644 --- a/openstack/network/v2/network.py +++ b/openstack/network/v2/network.py @@ -32,6 +32,8 @@ class Network(_base.NetworkResource, tag.TagMixin): 'name', 'status', 'project_id', + 'sort_key', + 'sort_dir', ipv4_address_scope_id='ipv4_address_scope', ipv6_address_scope_id='ipv6_address_scope', is_admin_state_up='admin_state_up', diff --git a/openstack/network/v2/network_ip_availability.py b/openstack/network/v2/network_ip_availability.py index 8410ece5a..18e305422 100644 --- a/openstack/network/v2/network_ip_availability.py +++ b/openstack/network/v2/network_ip_availability.py @@ -33,6 +33,8 @@ class NetworkIPAvailability(resource.Resource): 'network_id', 'network_name', 'project_id', + 'sort_key', + 'sort_dir', ) # Properties diff --git a/openstack/network/v2/network_segment_range.py b/openstack/network/v2/network_segment_range.py index 67b844537..55d08de39 100644 --- a/openstack/network/v2/network_segment_range.py +++ b/openstack/network/v2/network_segment_range.py @@ -41,6 +41,8 @@ class NetworkSegmentRange(resource.Resource): 'maximum', 'used', 'available', + 'sort_key', + 'sort_dir', ) # Properties diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index bd2f35d2f..d85427576 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -48,6 +48,8 @@ class Port(_base.NetworkResource, tag.TagMixin): 'subnet_id', 'project_id', 'security_groups', + 'sort_key', + 'sort_dir', is_admin_state_up='admin_state_up', is_port_security_enabled='port_security_enabled', security_group_ids='security_groups', diff --git a/openstack/network/v2/port_forwarding.py b/openstack/network/v2/port_forwarding.py index 77b92928b..1e651f76e 100644 --- a/openstack/network/v2/port_forwarding.py +++ b/openstack/network/v2/port_forwarding.py @@ -33,6 +33,8 @@ class PortForwarding(resource.Resource): 'internal_port_id', 'external_port', 'protocol', + 'sort_key', + 'sort_dir', ) # Properties diff --git a/openstack/network/v2/qos_policy.py b/openstack/network/v2/qos_policy.py index 6967735e4..3442be891 100644 --- a/openstack/network/v2/qos_policy.py +++ b/openstack/network/v2/qos_policy.py @@ -33,6 +33,8 @@ class QoSPolicy(resource.Resource, tag.TagMixin): 'description', 'is_default', 'project_id', + 'sort_key', + 'sort_dir', is_shared='shared', **tag.TagMixin._tag_query_parameters ) diff --git a/openstack/network/v2/router.py b/openstack/network/v2/router.py index a803c1a49..1ea630541 100644 --- a/openstack/network/v2/router.py +++ b/openstack/network/v2/router.py @@ -35,6 +35,8 @@ class Router(_base.NetworkResource, tag.TagMixin): 'name', 'status', 'project_id', + 'sort_key', + 'sort_dir', is_admin_state_up='admin_state_up', is_distributed='distributed', is_ha='ha', diff --git a/openstack/network/v2/segment.py b/openstack/network/v2/segment.py index 62cd325b3..f0c1f09d4 100644 --- a/openstack/network/v2/segment.py +++ b/openstack/network/v2/segment.py @@ -34,6 +34,8 @@ class Segment(resource.Resource): 'network_type', 'physical_network', 'segmentation_id', + 'sort_key', + 'sort_dir', ) # Properties diff --git a/openstack/network/v2/subnet.py b/openstack/network/v2/subnet.py index 8d83feb27..21f2afab2 100644 --- a/openstack/network/v2/subnet.py +++ b/openstack/network/v2/subnet.py @@ -39,6 +39,8 @@ class Subnet(_base.NetworkResource, tag.TagMixin): 'segment_id', 'dns_publish_fixed_ip', 'project_id', + 'sort_key', + 'sort_dir', is_dhcp_enabled='enable_dhcp', subnet_pool_id='subnetpool_id', use_default_subnet_pool='use_default_subnetpool', diff --git a/openstack/network/v2/subnet_pool.py b/openstack/network/v2/subnet_pool.py index 22e3e05e0..a517bcc61 100644 --- a/openstack/network/v2/subnet_pool.py +++ b/openstack/network/v2/subnet_pool.py @@ -34,6 +34,8 @@ class SubnetPool(resource.Resource, tag.TagMixin): 'is_default', 'name', 'project_id', + 'sort_key', + 'sort_dir', is_shared='shared', **tag.TagMixin._tag_query_parameters ) diff --git a/openstack/tests/unit/network/v2/test_floating_ip.py b/openstack/tests/unit/network/v2/test_floating_ip.py index 53b8cff9d..d2e20e429 100644 --- a/openstack/tests/unit/network/v2/test_floating_ip.py +++ b/openstack/tests/unit/network/v2/test_floating_ip.py @@ -91,6 +91,8 @@ def test_make_it(self): 'any_tags': 'tags-any', 'not_tags': 'not-tags', 'not_any_tags': 'not-tags-any', + 'sort_dir': 'sort_dir', + 'sort_key': 'sort_key', }, sot._query_mapping._mapping, ) diff --git a/openstack/tests/unit/network/v2/test_local_ip_association.py b/openstack/tests/unit/network/v2/test_local_ip_association.py index b94da0abe..973105b7a 100644 --- a/openstack/tests/unit/network/v2/test_local_ip_association.py +++ b/openstack/tests/unit/network/v2/test_local_ip_association.py @@ -46,6 +46,8 @@ def test_basic(self): 'host': 'host', 'limit': 'limit', 'marker': 'marker', + 'sort_dir': 'sort_dir', + 'sort_key': 'sort_key', }, sot._query_mapping._mapping, ) diff --git a/openstack/tests/unit/network/v2/test_network.py b/openstack/tests/unit/network/v2/test_network.py index 17ea946e5..ad909d172 100644 --- a/openstack/tests/unit/network/v2/test_network.py +++ b/openstack/tests/unit/network/v2/test_network.py @@ -119,6 +119,8 @@ def test_make_it(self): 'any_tags': 'tags-any', 'not_tags': 'not-tags', 'not_any_tags': 'not-tags-any', + 'sort_dir': 'sort_dir', + 'sort_key': 'sort_key', }, sot._query_mapping._mapping, ) diff --git a/openstack/tests/unit/network/v2/test_port.py b/openstack/tests/unit/network/v2/test_port.py index 3547485f3..4fe890ee4 100644 --- a/openstack/tests/unit/network/v2/test_port.py +++ b/openstack/tests/unit/network/v2/test_port.py @@ -113,6 +113,8 @@ def test_basic(self): "not_any_tags": "not-tags-any", "not_tags": "not-tags", "tags": "tags", + 'sort_dir': 'sort_dir', + 'sort_key': 'sort_key', }, sot._query_mapping._mapping, ) diff --git a/openstack/tests/unit/network/v2/test_port_forwarding.py b/openstack/tests/unit/network/v2/test_port_forwarding.py index a95ea9fba..15bb9fcb6 100644 --- a/openstack/tests/unit/network/v2/test_port_forwarding.py +++ b/openstack/tests/unit/network/v2/test_port_forwarding.py @@ -47,6 +47,8 @@ def test_basic(self): 'limit': 'limit', 'marker': 'marker', 'protocol': 'protocol', + 'sort_dir': 'sort_dir', + 'sort_key': 'sort_key', }, sot._query_mapping._mapping, ) From be6699639cdf8a07818cf62bf7fa4c4ba1803ed1 Mon Sep 17 00:00:00 2001 From: elajkat Date: Wed, 14 Jun 2023 11:15:08 +0200 Subject: [PATCH 3464/3836] Add Tap Mirrors to SDK Depends-On: https://review.opendev.org/c/893086 Change-Id: If8151ebe82c3991c9cd2fed57ecb7723ab3db97c Related-Bug: #2015471 --- doc/source/user/proxies/network.rst | 8 ++ .../user/resources/network/v2/tap_mirror.rst | 12 +++ openstack/network/v2/_proxy.py | 33 ++++++++ openstack/network/v2/tap_mirror.py | 54 ++++++++++++ .../functional/network/v2/test_tap_mirror.py | 83 +++++++++++++++++++ openstack/tests/unit/network/v2/test_proxy.py | 28 +++++++ .../tests/unit/network/v2/test_tap_mirror.py | 62 ++++++++++++++ ...twork-add-tap-mirror-46376bd98ee69c81.yaml | 5 ++ zuul.d/functional-jobs.yaml | 9 +- 9 files changed, 292 insertions(+), 2 deletions(-) create mode 100644 doc/source/user/resources/network/v2/tap_mirror.rst create mode 100644 openstack/network/v2/tap_mirror.py create mode 100644 openstack/tests/functional/network/v2/test_tap_mirror.py create mode 100644 openstack/tests/unit/network/v2/test_tap_mirror.py create mode 100644 releasenotes/notes/network-add-tap-mirror-46376bd98ee69c81.yaml diff --git a/doc/source/user/proxies/network.rst b/doc/source/user/proxies/network.rst index a742f9996..4e5ef4dfb 100644 --- a/doc/source/user/proxies/network.rst +++ b/doc/source/user/proxies/network.rst @@ -357,3 +357,11 @@ SFC operations update_sfc_port_pair_group, create_sfc_service_graph, delete_sfc_service_graph, find_sfc_service_graph, get_sfc_service_graph, update_sfc_service_graph + +Tap Mirror operations +^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + :noindex: + :members: create_tap_mirror, delete_tap_mirror, find_tap_mirror, + get_tap_mirror, tap_mirrors, update_tap_mirror diff --git a/doc/source/user/resources/network/v2/tap_mirror.rst b/doc/source/user/resources/network/v2/tap_mirror.rst new file mode 100644 index 000000000..8697b1973 --- /dev/null +++ b/doc/source/user/resources/network/v2/tap_mirror.rst @@ -0,0 +1,12 @@ +openstack.network.v2.tap_mirror +=============================== + +.. automodule:: openstack.network.v2.tap_mirror + +The TapMirror Class +------------------- + +The ``TapMirror`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.tap_mirror.TapMirror + :members: diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 70ed80193..be363bfce 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -89,6 +89,7 @@ from openstack.network.v2 import subnet as _subnet from openstack.network.v2 import subnet_pool as _subnet_pool from openstack.network.v2 import tap_flow as _tap_flow +from openstack.network.v2 import tap_mirror as _tap_mirror from openstack.network.v2 import tap_service as _tap_service from openstack.network.v2 import trunk as _trunk from openstack.network.v2 import vpn_endpoint_group as _vpn_endpoint_group @@ -178,6 +179,7 @@ class Proxy(proxy.Proxy): "subnet": _subnet.Subnet, "subnet_pool": _subnet_pool.SubnetPool, "tap_flow": _tap_flow.TapFlow, + "tap_mirror": _tap_mirror.TapMirror, "tap_service": _tap_service.TapService, "trunk": _trunk.Trunk, "vpn_endpoint_group": _vpn_endpoint_group.VpnEndpointGroup, @@ -6328,6 +6330,37 @@ def tap_flows(self, **query): """Return a generator of Tap Flows""" return self._list(_tap_flow.TapFlow, **query) + def create_tap_mirror(self, **attrs): + """Create a new Tap Mirror from attributes""" + return self._create(_tap_mirror.TapMirror, **attrs) + + def delete_tap_mirror(self, tap_mirror, ignore_missing=True): + """Delete a Tap Mirror""" + self._delete( + _tap_mirror.TapMirror, tap_mirror, ignore_missing=ignore_missing + ) + + def find_tap_mirror(self, name_or_id, ignore_missing=True, **query): + """Find a single Tap Mirror""" + return self._find( + _tap_mirror.TapMirror, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) + + def get_tap_mirror(self, tap_mirror): + """Get a signle Tap Mirror""" + return self._get(_tap_mirror.TapMirror, tap_mirror) + + def update_tap_mirror(self, tap_mirror, **attrs): + """Update a Tap Mirror""" + return self._update(_tap_mirror.TapMirror, tap_mirror, **attrs) + + def tap_mirrors(self, **query): + """Return a generator of Tap Mirrors""" + return self._list(_tap_mirror.TapMirror, **query) + def create_tap_service(self, **attrs): """Create a new Tap Service from attributes""" return self._create(_tap_service.TapService, **attrs) diff --git a/openstack/network/v2/tap_mirror.py b/openstack/network/v2/tap_mirror.py new file mode 100644 index 000000000..562c91039 --- /dev/null +++ b/openstack/network/v2/tap_mirror.py @@ -0,0 +1,54 @@ +# 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 resource + + +class TapMirror(resource.Resource): + """Tap Mirror""" + + resource_key = 'tap_mirror' + resources_key = 'tap_mirrors' + base_path = '/taas/tap_mirrors' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + _allow_unknown_attrs_in_body = True + + _query_mapping = resource.QueryParameters( + "sort_key", "sort_dir", 'name', 'project_id' + ) + + # Properties + #: The ID of the Tap Mirror. + id = resource.Body('id') + #: The Tap Mirror name. + name = resource.Body('name') + #: The Tap Mirror description. + description = resource.Body('description') + #: The ID of the project that owns the Tap Mirror. + project_id = resource.Body('project_id', alias='tenant_id') + #: Tenant_id (deprecated attribute). + tenant_id = resource.Body('tenant_id', deprecated=True) + #: The id of the port the Tap Mirror is associated with + port_id = resource.Body('port_id') + #: The status for the tap service. + directions = resource.Body('directions') + #: The destination IP address of the Tap Mirror + remote_ip = resource.Body('remote_ip') + #: The type of the Tap Mirror, it can be gre or erspanv1 + mirror_type = resource.Body('mirror_type') diff --git a/openstack/tests/functional/network/v2/test_tap_mirror.py b/openstack/tests/functional/network/v2/test_tap_mirror.py new file mode 100644 index 000000000..aa781441f --- /dev/null +++ b/openstack/tests/functional/network/v2/test_tap_mirror.py @@ -0,0 +1,83 @@ +# 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.network.v2 import network as _network +from openstack.network.v2 import port as _port +from openstack.network.v2 import tap_mirror as _tap_mirror +from openstack.tests.functional import base + + +class TestTapMirror(base.BaseFunctionalTest): + def setUp(self): + super().setUp() + + if not self.user_cloud.network.find_extension("tap-mirror"): + self.skipTest("Neutron Tap Mirror Extension disabled") + + self.TAP_M_NAME = 'my_tap_mirror' + self.getUniqueString() + net = self.user_cloud.network.create_network() + assert isinstance(net, _network.Network) + self.MIRROR_NET_ID = net.id + + port = self.user_cloud.network.create_port( + network_id=self.MIRROR_NET_ID + ) + assert isinstance(port, _port.Port) + self.MIRROR_PORT_ID = port.id + + self.REMOTE_IP = '193.10.10.2' + self.MIRROR_TYPE = 'erspanv1' + + tap_mirror = self.user_cloud.network.create_tap_mirror( + name=self.TAP_M_NAME, + port_id=self.MIRROR_PORT_ID, + remote_ip=self.REMOTE_IP, + mirror_type=self.MIRROR_TYPE, + directions={'IN': 99}, + ) + assert isinstance(tap_mirror, _tap_mirror.TapMirror) + self.TAP_MIRROR = tap_mirror + + def tearDown(self): + sot = self.user_cloud.network.delete_tap_mirror( + self.TAP_MIRROR.id, ignore_missing=False + ) + self.assertIsNone(sot) + sot = self.user_cloud.network.delete_port(self.MIRROR_PORT_ID) + self.assertIsNone(sot) + sot = self.user_cloud.network.delete_network(self.MIRROR_NET_ID) + self.assertIsNone(sot) + + super().tearDown() + + def test_find_tap_mirror(self): + sot = self.user_cloud.network.find_tap_mirror(self.TAP_MIRROR.name) + self.assertEqual(self.MIRROR_PORT_ID, sot.port_id) + self.assertEqual(self.TAP_M_NAME, sot.name) + + def test_get_tap_mirror(self): + sot = self.user_cloud.network.get_tap_mirror(self.TAP_MIRROR.id) + self.assertEqual(self.MIRROR_PORT_ID, sot.port_id) + self.assertEqual(self.TAP_M_NAME, sot.name) + + def test_list_tap_mirrors(self): + tap_mirror_ids = [ + tm.id for tm in self.user_cloud.network.tap_mirrors() + ] + self.assertIn(self.TAP_MIRROR.id, tap_mirror_ids) + + def test_update_tap_mirror(self): + description = 'My Tap Mirror' + sot = self.user_cloud.network.update_tap_mirror( + self.TAP_MIRROR.id, description=description + ) + self.assertEqual(description, sot.description) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 844e844a4..45560a3eb 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -64,6 +64,7 @@ from openstack.network.v2 import service_provider from openstack.network.v2 import subnet from openstack.network.v2 import subnet_pool +from openstack.network.v2 import tap_mirror from openstack.network.v2 import vpn_endpoint_group from openstack.network.v2 import vpn_ike_policy from openstack.network.v2 import vpn_ipsec_policy @@ -2638,3 +2639,30 @@ def test_bgpvpn_router_association_update(self): expected_args=[self.ROUTER_ASSOCIATION], expected_kwargs={'bgpvpn_id': BGPVPN_ID}, ) + + +class TestNetworkTapMirror(TestNetworkProxy): + def test_create_tap_mirror(self): + self.verify_create(self.proxy.create_tap_mirror, tap_mirror.TapMirror) + + def test_delete_tap_mirror(self): + self.verify_delete( + self.proxy.delete_tap_mirror, tap_mirror.TapMirror, False + ) + + def test_delete_tap_mirror_ignore(self): + self.verify_delete( + self.proxy.delete_tap_mirror, tap_mirror.TapMirror, True + ) + + def test_find_tap_mirror(self): + self.verify_find(self.proxy.find_tap_mirror, tap_mirror.TapMirror) + + def test_get_tap_mirror(self): + self.verify_get(self.proxy.get_tap_mirror, tap_mirror.TapMirror) + + def test_tap_mirrors(self): + self.verify_list(self.proxy.tap_mirrors, tap_mirror.TapMirror) + + def test_update_tap_mirror(self): + self.verify_update(self.proxy.update_tap_mirror, tap_mirror.TapMirror) diff --git a/openstack/tests/unit/network/v2/test_tap_mirror.py b/openstack/tests/unit/network/v2/test_tap_mirror.py new file mode 100644 index 000000000..f7bd92edc --- /dev/null +++ b/openstack/tests/unit/network/v2/test_tap_mirror.py @@ -0,0 +1,62 @@ +# 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.network.v2 import tap_mirror +from openstack.tests.unit import base + + +IDENTIFIER = 'IDENTIFIER' +PORT_ID = 'PORT_ID' +EXAMPLE = { + 'name': 'my_tap_mirror', + 'port_id': PORT_ID, + 'directions': {'IN': 99}, + 'remote_ip': '193.10.10.1', + 'mirror_type': 'erspanv1', + 'id': IDENTIFIER, + 'project_id': '42', +} + + +class TestTapMirror(base.TestCase): + def test_basic(self): + sot = tap_mirror.TapMirror() + self.assertEqual('tap_mirror', sot.resource_key) + self.assertEqual('tap_mirrors', sot.resources_key) + self.assertEqual('/taas/tap_mirrors', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = tap_mirror.TapMirror(**EXAMPLE) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['port_id'], sot.port_id) + self.assertEqual(EXAMPLE['directions'], sot.directions) + self.assertEqual(EXAMPLE['remote_ip'], sot.remote_ip) + self.assertEqual(EXAMPLE['mirror_type'], sot.mirror_type) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + + self.assertDictEqual( + { + 'limit': 'limit', + 'marker': 'marker', + 'name': 'name', + 'project_id': 'project_id', + 'sort_key': 'sort_key', + 'sort_dir': 'sort_dir', + }, + sot._query_mapping._mapping, + ) diff --git a/releasenotes/notes/network-add-tap-mirror-46376bd98ee69c81.yaml b/releasenotes/notes/network-add-tap-mirror-46376bd98ee69c81.yaml new file mode 100644 index 000000000..d0253fbdf --- /dev/null +++ b/releasenotes/notes/network-add-tap-mirror-46376bd98ee69c81.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``Tap Mirror`` and introduce the support for creating, reading, + updating and deleting ``tap_mirrors``. diff --git a/zuul.d/functional-jobs.yaml b/zuul.d/functional-jobs.yaml index b0933a8f9..89cea4d5b 100644 --- a/zuul.d/functional-jobs.yaml +++ b/zuul.d/functional-jobs.yaml @@ -152,6 +152,7 @@ required-projects: - openstack/neutron-fwaas - openstack/neutron-vpnaas + - openstack/tap-as-a-service vars: INSTALL_OVN: False configure_swap_size: 4096 @@ -184,17 +185,19 @@ agent: availability_zone: nova devstack_localrc: - Q_SERVICE_PLUGIN_CLASSES: qos,trunk - NETWORK_API_EXTENSIONS: "agent,binding,dhcp_agent_scheduler,external-net,ext-gw-mode,extra_dhcp_opts,quotas,router,security-group,subnet_allocation,network-ip-availability,auto-allocated-topology,timestamp_core,tag,service-type,rbac-policies,standard-attr-description,pagination,sorting,project-id,fwaas_v2,vpnaas" + Q_SERVICE_PLUGIN_CLASSES: qos,trunk,taas + NETWORK_API_EXTENSIONS: "agent,binding,dhcp_agent_scheduler,external-net,ext-gw-mode,extra_dhcp_opts,quotas,router,security-group,subnet_allocation,network-ip-availability,auto-allocated-topology,timestamp_core,tag,service-type,rbac-policies,standard-attr-description,pagination,sorting,project-id,fwaas_v2,vpnaas,taas,tap_mirror" Q_AGENT: openvswitch Q_ML2_TENANT_NETWORK_TYPE: vxlan Q_ML2_PLUGIN_MECHANISM_DRIVERS: openvswitch IPSEC_PACKAGE: libreswan + TAAS_SERVICE_DRIVER: TAAS:TAAS:neutron_taas.services.taas.service_drivers.taas_rpc.TaasRpcDriver:default devstack_plugins: designate: https://opendev.org/openstack/designate octavia: https://opendev.org/openstack/octavia neutron-fwaas: https://opendev.org/openstack/neutron-fwaas.git neutron-vpnaas: https://opendev.org/openstack/neutron-vpnaas.git + tap-as-a-service: https://opendev.org/openstack/tap-as-a-service.git devstack_services: designate: true octavia: true @@ -211,6 +214,8 @@ h-api: false h-api-cfn: false q-fwaas-v2: true + taas: true + tap_mirror: true tox_environment: OPENSTACKSDK_HAS_DESIGNATE: 1 OPENSTACKSDK_HAS_SWIFT: 0 From 52c5d74518ddf49ac0e4fc430ffe11b329d65271 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 8 Apr 2024 12:30:06 +0100 Subject: [PATCH 3465/3836] volume: Allow setting volume statuses individually Some of these are admin-only. We don't want to force setting them all at once. Change-Id: I3b1694ee5e4dfd96315cc48b44b3d28c01aa3bfa Signed-off-by: Stephen Finucane --- openstack/block_storage/v2/_proxy.py | 2 +- openstack/block_storage/v2/volume.py | 21 ++++++++++++------- openstack/block_storage/v3/_proxy.py | 2 +- openstack/block_storage/v3/volume.py | 20 +++++++++++------- .../unit/block_storage/v2/test_volume.py | 15 +++++++++++++ .../unit/block_storage/v3/test_volume.py | 15 +++++++++++++ 6 files changed, 57 insertions(+), 18 deletions(-) diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index fa38a0638..0708af0b0 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -385,7 +385,7 @@ def set_volume_bootable_status(self, volume, bootable): volume.set_bootable_status(self, bootable) def reset_volume_status( - self, volume, status, attach_status, migration_status + self, volume, status=None, attach_status=None, migration_status=None ): """Reset volume statuses. diff --git a/openstack/block_storage/v2/volume.py b/openstack/block_storage/v2/volume.py index 7f17d22ee..4d63586b3 100644 --- a/openstack/block_storage/v2/volume.py +++ b/openstack/block_storage/v2/volume.py @@ -9,6 +9,9 @@ # 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 typing as ty + from openstack.common import metadata from openstack import format from openstack import resource @@ -114,15 +117,17 @@ def set_bootable_status(self, session, bootable=True): body = {'os-set_bootable': {'bootable': bootable}} self._action(session, body) - def reset_status(self, session, status, attach_status, migration_status): + def reset_status( + self, session, status=None, attach_status=None, migration_status=None + ): """Reset volume statuses (admin operation)""" - body = { - 'os-reset_status': { - 'status': status, - 'attach_status': attach_status, - 'migration_status': migration_status, - } - } + body: ty.Dict[str, ty.Dict[str, str]] = {'os-reset_status': {}} + if status: + body['os-reset_status']['status'] = status + if attach_status: + body['os-reset_status']['attach_status'] = attach_status + if migration_status: + body['os-reset_status']['migration_status'] = migration_status self._action(session, body) def attach(self, session, mountpoint, instance): diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index ea5b23d52..62eca1f81 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -771,7 +771,7 @@ def set_volume_bootable_status(self, volume, bootable): volume.set_bootable_status(self, bootable) def reset_volume_status( - self, volume, status, attach_status, migration_status + self, volume, status=None, attach_status=None, migration_status=None ): """Reset volume statuses. diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index 81d483a77..f5e6dc6e3 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + from openstack.common import metadata from openstack import exceptions from openstack import format @@ -138,15 +140,17 @@ def set_readonly(self, session, readonly): body = {'os-update_readonly_flag': {'readonly': readonly}} self._action(session, body) - def reset_status(self, session, status, attach_status, migration_status): + def reset_status( + self, session, status=None, attach_status=None, migration_status=None + ): """Reset volume statuses (admin operation)""" - body = { - 'os-reset_status': { - 'status': status, - 'attach_status': attach_status, - 'migration_status': migration_status, - } - } + body: ty.Dict[str, ty.Dict[str, str]] = {'os-reset_status': {}} + if status: + body['os-reset_status']['status'] = status + if attach_status: + body['os-reset_status']['attach_status'] = attach_status + if migration_status: + body['os-reset_status']['migration_status'] = migration_status self._action(session, body) def revert_to_snapshot(self, session, snapshot_id): diff --git a/openstack/tests/unit/block_storage/v2/test_volume.py b/openstack/tests/unit/block_storage/v2/test_volume.py index 1fdd8e312..ab9c49308 100644 --- a/openstack/tests/unit/block_storage/v2/test_volume.py +++ b/openstack/tests/unit/block_storage/v2/test_volume.py @@ -198,6 +198,21 @@ def test_reset_status(self): url, json=body, microversion=sot._max_microversion ) + def test_reset_status__single_option(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.reset_status(self.sess, status='1')) + + url = 'volumes/%s/action' % FAKE_ID + body = { + 'os-reset_status': { + 'status': '1', + } + } + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion + ) + def test_attach_instance(self): sot = volume.Volume(**VOLUME) diff --git a/openstack/tests/unit/block_storage/v3/test_volume.py b/openstack/tests/unit/block_storage/v3/test_volume.py index 8ec8705a2..ffdb11290 100644 --- a/openstack/tests/unit/block_storage/v3/test_volume.py +++ b/openstack/tests/unit/block_storage/v3/test_volume.py @@ -227,6 +227,21 @@ def test_reset_status(self): url, json=body, microversion=sot._max_microversion ) + def test_reset_status__single_option(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.reset_status(self.sess, status='1')) + + url = 'volumes/%s/action' % FAKE_ID + body = { + 'os-reset_status': { + 'status': '1', + } + } + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion + ) + @mock.patch( 'openstack.utils.require_microversion', autospec=True, From 644087b27090773afc89836947d1b17dbaa9eb71 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 8 Apr 2024 12:21:13 +0100 Subject: [PATCH 3466/3836] volume: Add ability to set, unset image metadata Another action down. Change-Id: I4d59939bb9d6aa7e66d613e16f7ee89f610f9d5f Signed-off-by: Stephen Finucane --- doc/source/user/proxies/block_storage_v2.rst | 8 +++- doc/source/user/proxies/block_storage_v3.rst | 1 + openstack/block_storage/v2/_proxy.py | 30 ++++++++++++ openstack/block_storage/v2/volume.py | 16 +++++++ openstack/block_storage/v3/_proxy.py | 30 ++++++++++++ openstack/block_storage/v3/volume.py | 16 +++++++ .../tests/unit/block_storage/v2/test_proxy.py | 26 ++++++++++ .../unit/block_storage/v2/test_volume.py | 47 +++++++++++++++++++ .../tests/unit/block_storage/v3/test_proxy.py | 26 ++++++++++ .../unit/block_storage/v3/test_volume.py | 46 ++++++++++++++++++ ...age-metadata-support-c61bcb918fdff529.yaml | 4 ++ 11 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-volume-image-metadata-support-c61bcb918fdff529.yaml diff --git a/doc/source/user/proxies/block_storage_v2.rst b/doc/source/user/proxies/block_storage_v2.rst index c5bca1866..d15c33b15 100644 --- a/doc/source/user/proxies/block_storage_v2.rst +++ b/doc/source/user/proxies/block_storage_v2.rst @@ -17,7 +17,13 @@ Volume Operations .. autoclass:: openstack.block_storage.v2._proxy.Proxy :noindex: - :members: create_volume, delete_volume, get_volume, volumes + :members: create_volume, delete_volume, get_volume, + find_volume, volumes, get_volume_metadata, set_volume_metadata, + delete_volume_metadata, extend_volume, + retype_volume, set_volume_bootable_status, reset_volume_status, + set_volume_image_metadata, delete_volume_image_metadata, + attach_volume, detach_volume, + unmanage_volume, migrate_volume, complete_volume_migration Backup Operations ^^^^^^^^^^^^^^^^^ diff --git a/doc/source/user/proxies/block_storage_v3.rst b/doc/source/user/proxies/block_storage_v3.rst index 6799ba283..73a98623d 100644 --- a/doc/source/user/proxies/block_storage_v3.rst +++ b/doc/source/user/proxies/block_storage_v3.rst @@ -21,6 +21,7 @@ Volume Operations find_volume, volumes, get_volume_metadata, set_volume_metadata, delete_volume_metadata, extend_volume, set_volume_readonly, retype_volume, set_volume_bootable_status, reset_volume_status, + set_volume_image_metadata, delete_volume_image_metadata, revert_volume_to_snapshot, attach_volume, detach_volume, unmanage_volume, migrate_volume, complete_volume_migration, upload_volume_to_image, reserve_volume, unreserve_volume, diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 0708af0b0..4557df44f 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -384,6 +384,36 @@ def set_volume_bootable_status(self, volume, bootable): volume = self._get_resource(_volume.Volume, volume) volume.set_bootable_status(self, bootable) + def set_volume_image_metadata(self, volume, **metadata): + """Update image metadata for a volume + + :param volume: Either the ID of a volume or a + :class:`~openstack.block_storage.v2.volume.Volume`. + :param kwargs metadata: Key/value pairs to be updated in the volume's + image metadata. No other metadata is modified by this call. + + :returns: None + """ + volume = self._get_resource(_volume.Volume, volume) + return volume.set_image_metadata(self, metadata=metadata) + + def delete_volume_image_metadata(self, volume, keys=None): + """Delete metadata for a volume + + :param volume: Either the ID of a volume or a + :class:`~openstack.block_storage.v2.volume.Volume`. + :param list keys: The keys to delete. If left empty complete + metadata will be removed. + + :returns: None + """ + volume = self._get_resource(_volume.Volume, volume) + if keys is not None: + for key in keys: + volume.delete_image_metadata_item(self, key) + else: + volume.delete_image_metadata(self) + def reset_volume_status( self, volume, status=None, attach_status=None, migration_status=None ): diff --git a/openstack/block_storage/v2/volume.py b/openstack/block_storage/v2/volume.py index 4d63586b3..1fd0533d8 100644 --- a/openstack/block_storage/v2/volume.py +++ b/openstack/block_storage/v2/volume.py @@ -117,6 +117,22 @@ def set_bootable_status(self, session, bootable=True): body = {'os-set_bootable': {'bootable': bootable}} self._action(session, body) + def set_image_metadata(self, session, metadata): + """Sets image metadata key-value pairs on the volume""" + body = {'os-set_image_metadata': metadata} + self._action(session, body) + + def delete_image_metadata(self, session): + """Remove all image metadata from the volume""" + for key in self.metadata: + body = {'os-unset_image_metadata': key} + self._action(session, body) + + def delete_image_metadata_item(self, session, key): + """Remove a single image metadata from the volume""" + body = {'os-unset_image_metadata': key} + self._action(session, body) + def reset_status( self, session, status=None, attach_status=None, migration_status=None ): diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 62eca1f81..d127651bb 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -770,6 +770,36 @@ def set_volume_bootable_status(self, volume, bootable): volume = self._get_resource(_volume.Volume, volume) volume.set_bootable_status(self, bootable) + def set_volume_image_metadata(self, volume, **metadata): + """Update image metadata for a volume + + :param volume: Either the ID of a volume or a + :class:`~openstack.block_storage.v3.volume.Volume`. + :param kwargs metadata: Key/value pairs to be updated in the volume's + image metadata. No other metadata is modified by this call. + + :returns: None + """ + volume = self._get_resource(_volume.Volume, volume) + return volume.set_image_metadata(self, metadata=metadata) + + def delete_volume_image_metadata(self, volume, keys=None): + """Delete metadata for a volume + + :param volume: Either the ID of a volume or a + :class:`~openstack.block_storage.v3.volume.Volume`. + :param list keys: The keys to delete. If left empty complete + metadata will be removed. + + :returns: None + """ + volume = self._get_resource(_volume.Volume, volume) + if keys is not None: + for key in keys: + volume.delete_image_metadata_item(self, key) + else: + volume.delete_image_metadata(self) + def reset_volume_status( self, volume, status=None, attach_status=None, migration_status=None ): diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index f5e6dc6e3..eb927ec40 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -140,6 +140,22 @@ def set_readonly(self, session, readonly): body = {'os-update_readonly_flag': {'readonly': readonly}} self._action(session, body) + def set_image_metadata(self, session, metadata): + """Sets image metadata key-value pairs on the volume""" + body = {'os-set_image_metadata': metadata} + self._action(session, body) + + def delete_image_metadata(self, session): + """Remove all image metadata from the volume""" + for key in self.metadata: + body = {'os-unset_image_metadata': key} + self._action(session, body) + + def delete_image_metadata_item(self, session, key): + """Remove a single image metadata from the volume""" + body = {'os-unset_image_metadata': key} + self._action(session, body) + def reset_status( self, session, status=None, attach_status=None, migration_status=None ): diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index 3455f6abf..63ac98588 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -152,6 +152,32 @@ def test_volume_reset_volume_status(self): expected_args=[self.proxy, '1', '2', '3'], ) + def test_set_volume_image_metadata(self): + self._verify( + "openstack.block_storage.v2.volume.Volume.set_image_metadata", + self.proxy.set_volume_image_metadata, + method_args=["value"], + method_kwargs={'foo': 'bar'}, + expected_args=[self.proxy], + expected_kwargs={'metadata': {'foo': 'bar'}}, + ) + + def test_delete_volume_image_metadata(self): + self._verify( + "openstack.block_storage.v2.volume.Volume.delete_image_metadata", + self.proxy.delete_volume_image_metadata, + method_args=["value"], + expected_args=[self.proxy], + ) + + def test_delete_volume_image_metadata__with_keys(self): + self._verify( + "openstack.block_storage.v2.volume.Volume.delete_image_metadata_item", + self.proxy.delete_volume_image_metadata, + method_args=["value", ['foo']], + expected_args=[self.proxy, 'foo'], + ) + def test_attach_instance(self): self._verify( "openstack.block_storage.v2.volume.Volume.attach", diff --git a/openstack/tests/unit/block_storage/v2/test_volume.py b/openstack/tests/unit/block_storage/v2/test_volume.py index ab9c49308..a8120f493 100644 --- a/openstack/tests/unit/block_storage/v2/test_volume.py +++ b/openstack/tests/unit/block_storage/v2/test_volume.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import copy from unittest import mock from keystoneauth1 import adapter @@ -181,6 +182,52 @@ def test_set_volume_bootable_false(self): url, json=body, microversion=sot._max_microversion ) + def test_set_image_metadata(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.set_image_metadata(self.sess, {'foo': 'bar'})) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-set_image_metadata': {'foo': 'bar'}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion + ) + + def test_delete_image_metadata(self): + _volume = copy.deepcopy(VOLUME) + _volume['metadata'] = { + 'foo': 'bar', + 'baz': 'wow', + } + sot = volume.Volume(**_volume) + + self.assertIsNone(sot.delete_image_metadata(self.sess)) + + url = 'volumes/%s/action' % FAKE_ID + body_a = {'os-unset_image_metadata': 'foo'} + body_b = {'os-unset_image_metadata': 'baz'} + self.sess.post.assert_has_calls( + [ + mock.call( + url, json=body_a, microversion=sot._max_microversion + ), + mock.call( + url, json=body_b, microversion=sot._max_microversion + ), + ] + ) + + def test_delete_image_metadata_item(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.delete_image_metadata_item(self.sess, 'foo')) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-unset_image_metadata': 'foo'} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion + ) + def test_reset_status(self): sot = volume.Volume(**VOLUME) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 1686004f2..5545118a9 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -410,6 +410,32 @@ def test_volume_reset_volume_status(self): expected_args=[self.proxy, '1', '2', '3'], ) + def test_set_volume_image_metadata(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.set_image_metadata", + self.proxy.set_volume_image_metadata, + method_args=["value"], + method_kwargs={'foo': 'bar'}, + expected_args=[self.proxy], + expected_kwargs={'metadata': {'foo': 'bar'}}, + ) + + def test_delete_volume_image_metadata(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.delete_image_metadata", + self.proxy.delete_volume_image_metadata, + method_args=["value"], + expected_args=[self.proxy], + ) + + def test_delete_volume_image_metadata__with_keys(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.delete_image_metadata_item", + self.proxy.delete_volume_image_metadata, + method_args=["value", ['foo']], + expected_args=[self.proxy, 'foo'], + ) + def test_volume_revert_to_snapshot(self): self._verify( "openstack.block_storage.v3.volume.Volume.revert_to_snapshot", diff --git a/openstack/tests/unit/block_storage/v3/test_volume.py b/openstack/tests/unit/block_storage/v3/test_volume.py index ffdb11290..df2e234d8 100644 --- a/openstack/tests/unit/block_storage/v3/test_volume.py +++ b/openstack/tests/unit/block_storage/v3/test_volume.py @@ -210,6 +210,52 @@ def test_set_volume_bootable_false(self): url, json=body, microversion=sot._max_microversion ) + def test_set_image_metadata(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.set_image_metadata(self.sess, {'foo': 'bar'})) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-set_image_metadata': {'foo': 'bar'}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion + ) + + def test_delete_image_metadata(self): + _volume = copy.deepcopy(VOLUME) + _volume['metadata'] = { + 'foo': 'bar', + 'baz': 'wow', + } + sot = volume.Volume(**_volume) + + self.assertIsNone(sot.delete_image_metadata(self.sess)) + + url = 'volumes/%s/action' % FAKE_ID + body_a = {'os-unset_image_metadata': 'foo'} + body_b = {'os-unset_image_metadata': 'baz'} + self.sess.post.assert_has_calls( + [ + mock.call( + url, json=body_a, microversion=sot._max_microversion + ), + mock.call( + url, json=body_b, microversion=sot._max_microversion + ), + ] + ) + + def test_delete_image_metadata_item(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.delete_image_metadata_item(self.sess, 'foo')) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-unset_image_metadata': 'foo'} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion + ) + def test_reset_status(self): sot = volume.Volume(**VOLUME) diff --git a/releasenotes/notes/add-volume-image-metadata-support-c61bcb918fdff529.yaml b/releasenotes/notes/add-volume-image-metadata-support-c61bcb918fdff529.yaml new file mode 100644 index 000000000..6bd02c360 --- /dev/null +++ b/releasenotes/notes/add-volume-image-metadata-support-c61bcb918fdff529.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Added support for managing volume image metadata. From 52b86348a1dd76fbd6db051f2fcdde73acdfdf49 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 8 Apr 2024 12:36:25 +0100 Subject: [PATCH 3467/3836] volume: Allow passing a volume type object to retype_volume Small quality of life improvement. Change-Id: Ief3256cee7110a1e1920cd5e033586c1166f280e Signed-off-by: Stephen Finucane --- openstack/block_storage/v2/_proxy.py | 7 +++++-- openstack/block_storage/v3/_proxy.py | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 4557df44f..6ac642480 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -361,7 +361,9 @@ def retype_volume(self, volume, new_type, migration_policy="never"): :param volume: The value can be either the ID of a volume or a :class:`~openstack.block_storage.v2.volume.Volume` instance. - :param str new_type: The new volume type that volume is changed with. + :param new_type: The new volume type that volume is changed with. + The value can be either the ID of the volume type or a + :class:`~openstack.block_storage.v2.type.Type` instance. :param str migration_policy: Specify if the volume should be migrated when it is re-typed. Possible values are on-demand or never. Default: never. @@ -369,7 +371,8 @@ def retype_volume(self, volume, new_type, migration_policy="never"): :returns: None """ volume = self._get_resource(_volume.Volume, volume) - volume.retype(self, new_type, migration_policy) + type_id = resource.Resource._get_id(new_type) + volume.retype(self, type_id, migration_policy) def set_volume_bootable_status(self, volume, bootable): """Set bootable status of the volume. diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index d127651bb..cf17a8f60 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -747,7 +747,9 @@ def retype_volume(self, volume, new_type, migration_policy="never"): :param volume: The value can be either the ID of a volume or a :class:`~openstack.block_storage.v3.volume.Volume` instance. - :param str new_type: The new volume type that volume is changed with. + :param new_type: The new volume type that volume is changed with. + The value can be either the ID of the volume type or a + :class:`~openstack.block_storage.v3.type.Type` instance. :param str migration_policy: Specify if the volume should be migrated when it is re-typed. Possible values are on-demand or never. Default: never. @@ -755,7 +757,8 @@ def retype_volume(self, volume, new_type, migration_policy="never"): :returns: None """ volume = self._get_resource(_volume.Volume, volume) - volume.retype(self, new_type, migration_policy) + type_id = resource.Resource._get_id(new_type) + volume.retype(self, type_id, migration_policy) def set_volume_bootable_status(self, volume, bootable): """Set bootable status of the volume. From 111d72d513de383ac9c6fdbbd8a535186d971963 Mon Sep 17 00:00:00 2001 From: Tobias Urdin Date: Tue, 16 Apr 2024 14:36:03 +0200 Subject: [PATCH 3468/3836] Add Binero public cloud to vendor support This adds the Binero public cloud vendor to the vendor support document. Change-Id: I5465f75859b6d72328cf7e4a1a5d33f99e2aac79 --- doc/source/user/config/vendor-support.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/doc/source/user/config/vendor-support.rst b/doc/source/user/config/vendor-support.rst index 5e1ea395c..6a8bb0add 100644 --- a/doc/source/user/config/vendor-support.rst +++ b/doc/source/user/config/vendor-support.rst @@ -56,6 +56,22 @@ betacloud-1 Karlsruhe, Germany * Public IPv4 is provided via NAT with Neutron Floating IP * Volume API Version is 3 +Binero +------ + +https://auth.binero.cloud:5000/v3 + +============== ================== +Region Name Location +============== ================== +europe-se-1 Stockholm, SE +============== ================== + +* Identity API Version is 3 +* Volume API Version is 3 +* Public IPv4 is directly routable via DHCP from Neutron +* Public IPv4 is provided via NAT with Neutron Floating IP + Catalyst -------- From 67c1fb82c0026e5a036291bdcb02d98d7ddc8404 Mon Sep 17 00:00:00 2001 From: Mridula Joshi Date: Wed, 22 Nov 2023 13:24:48 +0000 Subject: [PATCH 3469/3836] Adding SDK support for ``glance md-namespace-objects-delete`` In this patch, we have added support for deleting all objects from the namespace. Change-Id: Ieab58d35b11da4da57aab18c5de83c92f3e41bdc --- openstack/image/v2/_proxy.py | 16 ++++++++++++++++ openstack/image/v2/metadef_namespace.py | 10 ++++++++++ .../unit/image/v2/test_metadef_namespace.py | 10 ++++++++++ openstack/tests/unit/image/v2/test_proxy.py | 14 ++++++++++++++ ...space-object-delete-all-6cea62cb038012df.yaml | 5 +++++ 5 files changed, 55 insertions(+) create mode 100644 releasenotes/notes/add-namespace-object-delete-all-6cea62cb038012df.yaml diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 94d99dc28..39e940f6a 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -1313,6 +1313,22 @@ def delete_metadef_object(self, metadef_object, namespace, **attrs): **attrs, ) + def delete_all_metadef_objects(self, namespace): + """Delete all objects + + :param namespace: The value can be either the name of a metadef + namespace or a + :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` + instance. + :returns: ``None`` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource can be found. + """ + namespace = self._get_resource( + _metadef_namespace.MetadefNamespace, namespace + ) + return namespace.delete_all_objects(self) + # ====== METADEF RESOURCE TYPES ====== def metadef_resource_types(self, **query): """Return a generator of metadef resource types diff --git a/openstack/image/v2/metadef_namespace.py b/openstack/image/v2/metadef_namespace.py index 133a929b1..88e028f8e 100644 --- a/openstack/image/v2/metadef_namespace.py +++ b/openstack/image/v2/metadef_namespace.py @@ -86,5 +86,15 @@ def delete_all_properties(self, session): :param session: The session to use for making this request :returns: The server response """ + url = utils.urljoin(self.base_path, self.id, 'properties') return self._delete_all(session, url) + + def delete_all_objects(self, session): + """Delete all objects in a namespace. + + :param session: The session to use for making this request + :returns: The server response + """ + url = utils.urljoin(self.base_path, self.id, 'objects') + return self._delete_all(session, url) diff --git a/openstack/tests/unit/image/v2/test_metadef_namespace.py b/openstack/tests/unit/image/v2/test_metadef_namespace.py index f22a1e6ce..cf614f2a8 100644 --- a/openstack/tests/unit/image/v2/test_metadef_namespace.py +++ b/openstack/tests/unit/image/v2/test_metadef_namespace.py @@ -87,3 +87,13 @@ def test_delete_all_properties(self): session.delete.assert_called_with( 'metadefs/namespaces/OS::Cinder::Volumetype/properties' ) + + @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) + def test_delete_all_objects(self): + sot = metadef_namespace.MetadefNamespace(**EXAMPLE) + session = mock.Mock(spec=adapter.Adapter) + sot._translate_response = mock.Mock() + sot.delete_all_objects(session) + session.delete.assert_called_with( + 'metadefs/namespaces/OS::Cinder::Volumetype/objects' + ) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index d74777cc9..bc5fd234d 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -616,6 +616,20 @@ def test_delete_metadef_object(self): expected_kwargs={"namespace_name": "test_namespace_name"}, ) + @mock.patch.object(proxy_base.Proxy, '_get_resource') + def test_delete_all_metadef_objects(self, mock_get_resource): + fake_object = _metadef_namespace.MetadefNamespace() + mock_get_resource.return_value = fake_object + self._verify( + "openstack.image.v2.metadef_namespace.MetadefNamespace.delete_all_objects", + self.proxy.delete_all_metadef_objects, + method_args=['test_namespace'], + expected_args=[self.proxy], + ) + mock_get_resource.assert_called_once_with( + _metadef_namespace.MetadefNamespace, 'test_namespace' + ) + class TestMetadefResourceType(TestImageProxy): def test_metadef_resource_types(self): diff --git a/releasenotes/notes/add-namespace-object-delete-all-6cea62cb038012df.yaml b/releasenotes/notes/add-namespace-object-delete-all-6cea62cb038012df.yaml new file mode 100644 index 000000000..b8863d816 --- /dev/null +++ b/releasenotes/notes/add-namespace-object-delete-all-6cea62cb038012df.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add support for deleting all objects inside + a namespace. From 7131781adb04c00d8977bc27e993d1f7315b4c8a Mon Sep 17 00:00:00 2001 From: Michael Still Date: Sun, 7 Apr 2024 09:09:57 +1000 Subject: [PATCH 3470/3836] Remove mypy union line which breaks older pythons. Change I084336ba41147f824b92dc07235e5f19b7ac4a9c introduced mypy syntax which breaks Python releases before 3.10. Unfortunately, for 2024.1 we commit to supporting Python back to 3.8. Specifically, you receive this error message if you run pep8: pep8 mypy.....................................................................Failed pep8 - hook id: mypy pep8 - exit code: 1 pep8 pep8 openstack/object_store/v1/_proxy.py: note: In member "generate_temp_url" of class "Proxy": pep8 openstack/object_store/v1/_proxy.py:1049:21: error: X | Y syntax for unions requires Python 3.10 [syntax] pep8 Found 1 error in 1 file (checked 410 source files) I asked some buddies, and we're fairly sure that this line would crash the runtime on Python 3.8, because its a syntax error. So instead, let's use typing syntax compatible with other pythons. Change-Id: I0a5f57346c7ff469ffe1b93051e470141117ada9 --- openstack/object_store/v1/_proxy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index a63d9dc2c..e28f0b0b7 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -17,6 +17,7 @@ import json import os import time +import typing as ty from urllib import parse from openstack import _log @@ -1046,7 +1047,7 @@ def generate_temp_url( method.upper(), ) - expiration: float | int + expiration: ty.Union[float, int] if not absolute: expiration = _get_expiration(timestamp) else: From 1610e2da76701d711f3f8b5114c93b511721ec42 Mon Sep 17 00:00:00 2001 From: Tobias Urdin Date: Mon, 4 Mar 2024 15:44:48 +0100 Subject: [PATCH 3471/3836] Fix multiple image_id query mappings The loadbalancer v2 proxy has the image_id query mapping written out twice, instead that should be compute_flavor [1]. [1] https://docs.openstack.org/api-ref/load-balancer/v2/#list-amphora Change-Id: I886bdc222904d6e42d8ccad254ed5ae58238d089 --- openstack/load_balancer/v2/amphora.py | 2 +- openstack/tests/unit/load_balancer/test_amphora.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/openstack/load_balancer/v2/amphora.py b/openstack/load_balancer/v2/amphora.py index ac90c7c80..955404877 100644 --- a/openstack/load_balancer/v2/amphora.py +++ b/openstack/load_balancer/v2/amphora.py @@ -47,7 +47,7 @@ class Amphora(resource.Resource): 'created_at', 'updated_at', 'image_id', - 'image_id', + 'compute_flavor', ) # Properties diff --git a/openstack/tests/unit/load_balancer/test_amphora.py b/openstack/tests/unit/load_balancer/test_amphora.py index 04b93ca87..21324fa3f 100644 --- a/openstack/tests/unit/load_balancer/test_amphora.py +++ b/openstack/tests/unit/load_balancer/test_amphora.py @@ -97,6 +97,7 @@ def test_make_it(self): 'marker': 'marker', 'id': 'id', 'loadbalancer_id': 'loadbalancer_id', + 'compute_flavor': 'compute_flavor', 'compute_id': 'compute_id', 'lb_network_ip': 'lb_network_ip', 'vrrp_ip': 'vrrp_ip', From fab08babab0ec0f0dd87df71cbc36c548233f3fc Mon Sep 17 00:00:00 2001 From: Tobias Urdin Date: Mon, 22 Apr 2024 21:23:11 +0200 Subject: [PATCH 3472/3836] Add Binero public cloud to vendor The [1] patch missed the actual vendor JSON file. [1] https://review.opendev.org/c/openstack/openstacksdk/+/915984 Change-Id: I22683505b4a7cf9c10314966309089b6e0ccf199 --- openstack/config/vendors/binero.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 openstack/config/vendors/binero.json diff --git a/openstack/config/vendors/binero.json b/openstack/config/vendors/binero.json new file mode 100644 index 000000000..5482d7196 --- /dev/null +++ b/openstack/config/vendors/binero.json @@ -0,0 +1,13 @@ +{ + "name": "binero", + "profile": { + "auth": { + "auth_url": "https://auth.binero.cloud:5000/v3" + }, + "identity_api_version": "3", + "block_storage_api_version": "3", + "regions": [ + "europe-se-1" + ] + } +} From f73335df5a112604ddcdda0ad34d25d3392240d3 Mon Sep 17 00:00:00 2001 From: Manuel Osorio Date: Mon, 26 Feb 2024 11:35:05 -0500 Subject: [PATCH 3473/3836] Add quota class set to shared file system Add Quota class set Get and Update api implementation to file share system as a service. Change-Id: Id1c5591e7954d1d1f97b58ab409b4bd985e63125 --- .../resources/shared_file_system/index.rst | 1 + .../shared_file_system/v2/quota_class_set.rst | 13 +++ openstack/shared_file_system/v2/_proxy.py | 25 +++++ .../shared_file_system/v2/quota_class_set.py | 57 +++++++++++ .../test_quota_class_set.py | 43 ++++++++ .../v2/test_quota_class_set.py | 99 +++++++++++++++++++ ...-shared-file-systems-43da33e6a3ed65e3.yaml | 4 + 7 files changed, 242 insertions(+) create mode 100644 doc/source/user/resources/shared_file_system/v2/quota_class_set.rst create mode 100644 openstack/shared_file_system/v2/quota_class_set.py create mode 100644 openstack/tests/functional/shared_file_system/test_quota_class_set.py create mode 100644 openstack/tests/unit/shared_file_system/v2/test_quota_class_set.py create mode 100644 releasenotes/notes/add-quota-class-set-to-shared-file-systems-43da33e6a3ed65e3.yaml diff --git a/doc/source/user/resources/shared_file_system/index.rst b/doc/source/user/resources/shared_file_system/index.rst index 1b45f4f17..d34b4a0dd 100644 --- a/doc/source/user/resources/shared_file_system/index.rst +++ b/doc/source/user/resources/shared_file_system/index.rst @@ -18,3 +18,4 @@ Shared File System service resources v2/share_access_rule v2/share_group_snapshot v2/resource_locks + v2/quota_class_set diff --git a/doc/source/user/resources/shared_file_system/v2/quota_class_set.rst b/doc/source/user/resources/shared_file_system/v2/quota_class_set.rst new file mode 100644 index 000000000..4ad5311f8 --- /dev/null +++ b/doc/source/user/resources/shared_file_system/v2/quota_class_set.rst @@ -0,0 +1,13 @@ +openstack.shared_file_system.v2.quota_class_set +=============================================== + +.. automodule:: openstack.shared_file_system.v2.quota_class_set + +The QuotaClassSet Class +----------------------- + +The ``QuotaClassSet`` class inherits from +:class:`~openstack.resource.Resource` and can be used to query quota class + +.. autoclass:: openstack.shared_file_system.v2.quota_class_set.QuotaClassSet + :members: diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index 56c5dfb74..0293a405f 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -16,6 +16,7 @@ availability_zone as _availability_zone, ) from openstack.shared_file_system.v2 import limit as _limit +from openstack.shared_file_system.v2 import quota_class_set as _quota_class_set from openstack.shared_file_system.v2 import resource_locks as _resource_locks from openstack.shared_file_system.v2 import share as _share from openstack.shared_file_system.v2 import share_group as _share_group @@ -58,6 +59,7 @@ class Proxy(proxy.Proxy): "share_group": _share_group.ShareGroup, "share_group_snapshot": _share_group_snapshot.ShareGroupSnapshot, "resource_locks": _resource_locks.ResourceLock, + "quota_class_set": _quota_class_set.QuotaClassSet, } def availability_zones(self): @@ -1198,3 +1200,26 @@ def create_resource_lock(self, **attrs): } attrs.pop('resource_type') return self._create(_resource_locks.ResourceLock, **attrs) + + def get_quota_class_set(self, quota_class_name): + """Get quota class set. + + :param quota_class_name: The name of the quota class + :returns: A :class:`~openstack.shared_file_system.v2 + .quota_class_set.QuotaClassSet` + """ + return self._get(_quota_class_set.QuotaClassSet, quota_class_name) + + def update_quota_class_set(self, quota_class_name, **attrs): + """Update quota class set. + + :param quota_class_name: The name of the quota class + :param attrs: The attributes to update on the quota class set + :returns: the updated quota class set + :rtype: :class:`~openstack.shared_file_system.v2 + .quota_class_set.QuotaClassSet` + """ + + return self._update( + _quota_class_set.QuotaClassSet, quota_class_name, **attrs + ) diff --git a/openstack/shared_file_system/v2/quota_class_set.py b/openstack/shared_file_system/v2/quota_class_set.py new file mode 100644 index 000000000..cf2090de5 --- /dev/null +++ b/openstack/shared_file_system/v2/quota_class_set.py @@ -0,0 +1,57 @@ +# 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 resource + + +class QuotaClassSet(resource.Resource): + base_path = '/quota-class-sets' + resource_key = 'quota_class_set' + + allow_create = False + allow_fetch = True + allow_commit = True + allow_delete = False + allow_list = False + allow_head = False + + _query_mapping = resource.QueryParameters("quota_class_name", "project_id") + #: Properties + #: A quota_class_set id. + id = resource.Body("id", type=str) + #: The maximum number of share groups. + share_groups = resource.Body("share_groups", type=int) + #: The maximum number of share group snapshots. + share_group_snapshots = resource.Body("share_group_snapshots", type=int) + #: The total maximum number of shares that are allowed in a project. + snapshots = resource.Body("snapshots", type=int) + #: The maximum number of snapshot gigabytes that are allowed in a project. + snapshot_gigabytes = resource.Body("snapshot_gigabytes", type=int) + #: The total maximum number of snapshot gigabytes that are allowed in a project. + shares = resource.Body("shares", type=int) + #: The maximum number of share-networks that are allowed in a project. + share_networks = resource.Body("share_networks", type=int) + #: The maximum number of share replicas that is allowed. + share_replicas = resource.Body("share_replicas", type=int) + #: The total maximum number of share gigabytes that are allowed in a project. + #: You cannot request a share that exceeds the allowed gigabytes quota. + gigabytes = resource.Body("gigabytes", type=int) + #: The maximum number of replica gigabytes that are allowed in a project. + #: You cannot create a share, share replica, manage a share or extend a share + #: if it is going to exceed the allowed replica gigabytes quota. + replica_gigabytes = resource.Body("replica_gigabytes", type=int) + #: The number of gigabytes per share allowed in a project. + per_share_gigabytes = resource.Body("per_share_gigabytes", type=int) + #: The total maximum number of share backups that are allowed in a project. + backups = resource.Body("backups", type=int) + #: The total maximum number of backup gigabytes that are allowed in a project. + backup_gigabytes = resource.Body("backup_gigabytes", type=int) diff --git a/openstack/tests/functional/shared_file_system/test_quota_class_set.py b/openstack/tests/functional/shared_file_system/test_quota_class_set.py new file mode 100644 index 000000000..cec5554ee --- /dev/null +++ b/openstack/tests/functional/shared_file_system/test_quota_class_set.py @@ -0,0 +1,43 @@ +# 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.tests.functional.shared_file_system import base + + +class QuotaClassSetTest(base.BaseSharedFileSystemTest): + def test_quota_class_set(self): + project_id = self.operator_cloud.current_project_id + + initial_quota_class_set = ( + self.operator_cloud.share.get_quota_class_set(project_id) + ) + self.assertIn('shares', initial_quota_class_set) + + initial_backups_value = initial_quota_class_set['backups'] + + updated_quota_class_set = ( + self.operator_cloud.share.update_quota_class_set( + project_id, + **{ + "backups": initial_backups_value + 1, + } + ) + ) + self.assertEqual( + updated_quota_class_set['backups'], initial_backups_value + 1 + ) + + reverted = self.operator_cloud.share.update_quota_class_set( + project_id, **{"backups": initial_backups_value} + ) + + self.assertEqual(initial_quota_class_set, reverted) diff --git a/openstack/tests/unit/shared_file_system/v2/test_quota_class_set.py b/openstack/tests/unit/shared_file_system/v2/test_quota_class_set.py new file mode 100644 index 000000000..daf2bb12f --- /dev/null +++ b/openstack/tests/unit/shared_file_system/v2/test_quota_class_set.py @@ -0,0 +1,99 @@ +# 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.shared_file_system.v2 import quota_class_set +from openstack.tests.unit import base + +EXAMPLE = { + "share_groups": 50, + "gigabytes": 1000, + "share_group_snapshots": 50, + "snapshots": 50, + "snapshot_gigabytes": 1000, + "shares": 50, + "id": "default", + "share_networks": 10, + "share_replicas": 100, + "replica_gigabytes": 1000, + "per_share_gigabytes": -1, + "backups": 50, + "backup_gigabytes": 1000, +} + + +class TestQuotaClassSet(base.TestCase): + def test_basic(self): + _quota_class_set = quota_class_set.QuotaClassSet() + + self.assertEqual('/quota-class-sets', _quota_class_set.base_path) + self.assertTrue(_quota_class_set.allow_fetch) + self.assertTrue(_quota_class_set.allow_commit) + self.assertFalse(_quota_class_set.allow_create) + self.assertFalse(_quota_class_set.allow_delete) + self.assertFalse(_quota_class_set.allow_list) + self.assertFalse(_quota_class_set.allow_head) + + def test_get_quota_class_set(self): + _quota_class_set = quota_class_set.QuotaClassSet(**EXAMPLE) + self.assertEqual( + EXAMPLE['share_groups'], _quota_class_set.share_groups + ) + self.assertEqual(EXAMPLE['gigabytes'], _quota_class_set.gigabytes) + self.assertEqual( + EXAMPLE['share_group_snapshots'], + _quota_class_set.share_group_snapshots, + ) + self.assertEqual(EXAMPLE['snapshots'], _quota_class_set.snapshots) + self.assertEqual( + EXAMPLE['snapshot_gigabytes'], _quota_class_set.snapshot_gigabytes + ) + self.assertEqual(EXAMPLE['shares'], _quota_class_set.shares) + self.assertEqual(EXAMPLE['id'], _quota_class_set.id) + self.assertEqual( + EXAMPLE['share_networks'], _quota_class_set.share_networks + ) + self.assertEqual( + EXAMPLE['share_replicas'], _quota_class_set.share_replicas + ) + self.assertEqual( + EXAMPLE['replica_gigabytes'], _quota_class_set.replica_gigabytes + ) + self.assertEqual( + EXAMPLE['per_share_gigabytes'], + _quota_class_set.per_share_gigabytes, + ) + self.assertEqual(EXAMPLE['backups'], _quota_class_set.backups) + self.assertEqual( + EXAMPLE['backup_gigabytes'], _quota_class_set.backup_gigabytes + ) + + def test_update_quota_class_set(self): + _quota_class_set = quota_class_set.QuotaClassSet(**EXAMPLE) + updated_attributes = { + "share_groups": 100, + "gigabytes": 2000, + "share_group_snapshots": 100, + } + _quota_class_set._update(**updated_attributes) + + self.assertEqual( + updated_attributes['share_groups'], _quota_class_set.share_groups + ) + self.assertEqual( + updated_attributes['gigabytes'], _quota_class_set.gigabytes + ) + self.assertEqual( + updated_attributes['share_group_snapshots'], + _quota_class_set.share_group_snapshots, + ) + self.assertEqual(EXAMPLE['snapshots'], _quota_class_set.snapshots) diff --git a/releasenotes/notes/add-quota-class-set-to-shared-file-systems-43da33e6a3ed65e3.yaml b/releasenotes/notes/add-quota-class-set-to-shared-file-systems-43da33e6a3ed65e3.yaml new file mode 100644 index 000000000..e102feb3d --- /dev/null +++ b/releasenotes/notes/add-quota-class-set-to-shared-file-systems-43da33e6a3ed65e3.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Added get and update to Quota Class Set to file system as a service. From e409254ed530844e4c600a2657a28d15bc4f491a Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 23 Apr 2024 10:30:41 +0100 Subject: [PATCH 3474/3836] image: Pass correct stores argument Change I3b897abccfecf9353be07abc8f8325d91f3eb9d4 introduced support for multiple image stores. Unfortunately a typo was included that has been carried forward since. Correct the mistake now and add a test to prevent regressions. Change-Id: I4d2c81d19d295aae8a77007a1c7de7083d061f24 Signed-off-by: Stephen Finucane --- openstack/image/v2/_proxy.py | 4 +- openstack/tests/unit/image/v2/test_proxy.py | 73 +++++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 94d99dc28..aefcc6b7c 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -333,8 +333,8 @@ def create_image( validate_checksum=validate_checksum, use_import=use_import, stores=stores, - all_stores=stores, - all_stores_must_succeed=stores, + all_stores=all_stores, + all_stores_must_succeed=all_stores_must_succeed, **image_kwargs, ) else: diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index d74777cc9..4d55de76b 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -278,6 +278,79 @@ def test_image_create_protected(self): args, kwargs = self.proxy._create.call_args self.assertEqual(kwargs["is_protected"], True) + def test_image_create_with_stores(self): + self.proxy.find_image = mock.Mock() + self.proxy._upload_image = mock.Mock() + + self.proxy.create_image( + name='fake', + data=b'fake', + container='bare', + disk_format='raw', + use_import=True, + stores=['cinder', 'swift'], + ) + + self.proxy.find_image.assert_called_with('fake') + + self.proxy._upload_image.assert_called_with( + 'fake', + container_format='bare', + disk_format='raw', + filename=None, + data=b'fake', + meta={}, + properties={ + self.proxy._IMAGE_MD5_KEY: '', + self.proxy._IMAGE_SHA256_KEY: '', # noqa: E501 + self.proxy._IMAGE_OBJECT_KEY: 'bare/fake', + }, + timeout=3600, + validate_checksum=False, + use_import=True, + stores=['cinder', 'swift'], + all_stores=None, + all_stores_must_succeed=None, + wait=False, + ) + + def test_image_create_with_all_stores(self): + self.proxy.find_image = mock.Mock() + self.proxy._upload_image = mock.Mock() + + self.proxy.create_image( + name='fake', + data=b'fake', + container='bare', + disk_format='raw', + use_import=True, + all_stores=True, + all_stores_must_succeed=True, + ) + + self.proxy.find_image.assert_called_with('fake') + + self.proxy._upload_image.assert_called_with( + 'fake', + container_format='bare', + disk_format='raw', + filename=None, + data=b'fake', + meta={}, + properties={ + self.proxy._IMAGE_MD5_KEY: '', + self.proxy._IMAGE_SHA256_KEY: '', # noqa: E501 + self.proxy._IMAGE_OBJECT_KEY: 'bare/fake', + }, + timeout=3600, + validate_checksum=False, + use_import=True, + stores=None, + all_stores=True, + all_stores_must_succeed=True, + wait=False, + ) + def test_image_upload_no_args(self): # container_format and disk_format are required args self.assertRaises(exceptions.InvalidRequest, self.proxy.upload_image) From 4b9a3e91365dac020ed6f2521876622153475e23 Mon Sep 17 00:00:00 2001 From: Simon Hensel Date: Tue, 9 Apr 2024 14:48:13 +0200 Subject: [PATCH 3475/3836] Extend project cleanup Now the project cleanup routine also deletes server groups, images and VPNaaS resources. Change-Id: I56ad0ce633365cfa3a0b5a38841c2d817147dfd8 --- openstack/compute/v2/_proxy.py | 19 ++- openstack/image/v2/_proxy.py | 29 ++++ openstack/network/v2/_proxy.py | 73 +++++++++ .../functional/cloud/test_project_cleanup.py | 151 ++++++++++++++++++ 4 files changed, 271 insertions(+), 1 deletion(-) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 53ea68a4d..aaf4c47f1 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -2537,7 +2537,9 @@ def wait_for_delete(self, res, interval=2, wait=120, callback=None): def _get_cleanup_dependencies(self): return { - 'compute': {'before': ['block_storage', 'network', 'identity']} + 'compute': { + 'before': ['block_storage', 'network', 'identity', 'image'] + } } def _service_cleanup( @@ -2575,3 +2577,18 @@ def _service_cleanup( # might be still holding ports on the subnet for server in servers: self.wait_for_delete(server) + + for obj in self.server_groups(): + # Do not delete server groups that still have members + if obj.member_ids: + continue + + self._service_cleanup_del_res( + self.delete_server_group, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn, + ) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 776386631..731b7bd2f 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -1883,3 +1883,32 @@ def wait_for_delete(self, res, interval=2, wait=120): to delete failed to occur in the specified seconds. """ return resource.wait_for_delete(self, res, interval, wait) + + def _get_cleanup_dependencies(self): + return {'image': {'before': ['identity']}} + + def _service_cleanup( + self, + dry_run=True, + client_status_queue=None, + identified_resources=None, + filters=None, + resource_evaluation_fn=None, + skip_resources=None, + ): + if self.should_skip_resource_cleanup("image", skip_resources): + return + + project_id = self.get_project_id() + + # Note that images cannot be deleted when they are still being used + for obj in self.images(owner=project_id): + self._service_cleanup_del_res( + self.delete_image, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn, + ) diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 70ed80193..e942fd32c 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -6890,6 +6890,79 @@ def _service_cleanup( ): project_id = self.get_project_id() + # check if the VPN service plugin is configured + vpn_plugin = list(self.service_providers(service_type="VPN")) + if vpn_plugin: + if not self.should_skip_resource_cleanup( + "vpn_ipsec_site_connection", skip_resources + ): + for obj in self.vpn_ipsec_site_connections(): + self._service_cleanup_del_res( + self.delete_vpn_ipsec_site_connection, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn, + ) + + if not self.should_skip_resource_cleanup( + "vpn_service", skip_resources + ): + for obj in self.vpn_services(): + self._service_cleanup_del_res( + self.delete_vpn_service, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn, + ) + + if not self.should_skip_resource_cleanup( + "vpn_endpoint_group", skip_resources + ): + for obj in self.vpn_endpoint_groups(): + self._service_cleanup_del_res( + self.delete_vpn_endpoint_group, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn, + ) + + if not self.should_skip_resource_cleanup( + "vpn_ike_policy", skip_resources + ): + for obj in self.vpn_ike_policies(): + self._service_cleanup_del_res( + self.delete_vpn_ike_policy, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn, + ) + + if not self.should_skip_resource_cleanup( + "vpn_ipsec_policy", skip_resources + ): + for obj in self.vpn_ipsec_policies(): + self._service_cleanup_del_res( + self.delete_vpn_ipsec_policy, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn, + ) + if not self.should_skip_resource_cleanup( "floating_ip", skip_resources ): diff --git a/openstack/tests/functional/cloud/test_project_cleanup.py b/openstack/tests/functional/cloud/test_project_cleanup.py index b5a9a8c28..7af15dc20 100644 --- a/openstack/tests/functional/cloud/test_project_cleanup.py +++ b/openstack/tests/functional/cloud/test_project_cleanup.py @@ -256,3 +256,154 @@ def test_cleanup_swift(self): while not status_queue.empty(): objects.append(status_queue.get()) self.assertIsNone(self.conn.get_container('test_container')) + + def test_cleanup_vpnaas(self): + if not list(self.conn.network.service_providers(service_type="VPN")): + self.skipTest("VPNaaS plugin is requred, but not available") + + status_queue = queue.Queue() + + # Find available external networks and use one + external_network = None + for network in self.conn.network.networks(): + if network.is_router_external: + external_network = network + break + if not external_network: + self.skipTest("External network is required, but not available") + + # Create left network resources + network_left = self.conn.network.create_network(name="network_left") + subnet_left = self.conn.network.create_subnet( + name="subnet_left", + network_id=network_left.id, + cidr="192.168.1.0/24", + ip_version=4, + ) + router_left = self.conn.network.create_router(name="router_left") + self.conn.network.add_interface_to_router( + router=router_left.id, subnet_id=subnet_left.id + ) + router_left = self.conn.network.update_router( + router_left, + external_gateway_info={"network_id": external_network.id}, + ) + + # Create right network resources + network_right = self.conn.network.create_network(name="network_right") + subnet_right = self.conn.network.create_subnet( + name="subnet_right", + network_id=network_right.id, + cidr="192.168.2.0/24", + ip_version=4, + ) + router_right = self.conn.network.create_router(name="router_right") + self.conn.network.add_interface_to_router( + router=router_right.id, subnet_id=subnet_right.id + ) + router_right = self.conn.network.update_router( + router_right, + external_gateway_info={"network_id": external_network.id}, + ) + + # Create VPNaaS resources + ike_policy = self.conn.network.create_vpn_ike_policy(name="ike_policy") + ipsec_policy = self.conn.network.create_vpn_ipsec_policy( + name="ipsec_policy" + ) + + vpn_service = self.conn.network.create_vpn_service( + name="vpn_service", router_id=router_left.id + ) + + ep_group_local = self.conn.network.create_vpn_endpoint_group( + name="endpoint_group_local", + type="subnet", + endpoints=[subnet_left.id], + ) + ep_group_peer = self.conn.network.create_vpn_endpoint_group( + name="endpoint_group_peer", + type="cidr", + endpoints=[subnet_right.cidr], + ) + + router_right_ip = router_right.external_gateway_info[ + 'external_fixed_ips' + ][0]['ip_address'] + ipsec_site_conn = self.conn.network.create_vpn_ipsec_site_connection( + name="ipsec_site_connection", + vpnservice_id=vpn_service.id, + ikepolicy_id=ike_policy.id, + ipsecpolicy_id=ipsec_policy.id, + local_ep_group_id=ep_group_local.id, + peer_ep_group_id=ep_group_peer.id, + psk="test", + peer_address=router_right_ip, + peer_id=router_right_ip, + ) + + # First round - check no resources are old enough + self.conn.project_cleanup( + dry_run=True, + wait_timeout=120, + status_queue=status_queue, + filters={'created_at': '2000-01-01'}, + ) + self.assertTrue(status_queue.empty()) + + # Second round - resource evaluation function return false, ensure + # nothing identified + self.conn.project_cleanup( + dry_run=True, + wait_timeout=120, + status_queue=status_queue, + filters={'created_at': '2200-01-01'}, + resource_evaluation_fn=lambda x, y, z: False, + ) + self.assertTrue(status_queue.empty()) + + # Third round - filters set too low + self.conn.project_cleanup( + dry_run=True, + wait_timeout=120, + status_queue=status_queue, + filters={'created_at': '2200-01-01'}, + ) + objects = [] + while not status_queue.empty(): + objects.append(status_queue.get()) + + # VPN resources do not have a created_at property + # Check for the network instead + resource_ids = list(obj.id for obj in objects) + self.assertIn(network_left.id, resource_ids) + + # Fourth round - dry run with no filters, ensure everything identified + self.conn.project_cleanup( + dry_run=True, wait_timeout=120, status_queue=status_queue + ) + objects = [] + while not status_queue.empty(): + objects.append(status_queue.get()) + + resource_ids = list(obj.id for obj in objects) + self.assertIn(ipsec_site_conn.id, resource_ids) + + # Ensure vpn resources still exist + site_conn_check = self.conn.network.get_vpn_ipsec_site_connection( + ipsec_site_conn.id + ) + self.assertEqual(site_conn_check.name, ipsec_site_conn.name) + + # Last round - do a real cleanup + self.conn.project_cleanup( + dry_run=False, wait_timeout=600, status_queue=status_queue + ) + # Ensure no VPN resources remain + self.assertEqual(0, len(list(self.conn.network.vpn_ike_policies()))) + self.assertEqual(0, len(list(self.conn.network.vpn_ipsec_policies()))) + self.assertEqual(0, len(list(self.conn.network.vpn_services()))) + self.assertEqual(0, len(list(self.conn.network.vpn_endpoint_groups()))) + self.assertEqual( + 0, len(list(self.conn.network.vpn_ipsec_site_connections())) + ) From 761d558e17cfd07c17f8457d5b4d66aea0ddc09a Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Thu, 18 Apr 2024 12:27:37 +0200 Subject: [PATCH 3476/3836] Allow filtering BGPVPNs BGPVPNs can be filtered server-side when being listed. The most interesting filters here are for routers, networks and ports, as these allow to filter for a specific association the BGPVPN has, but the other filter options might come in handy as well. route|import|export-target support only exact matches in the BGPVPN API, meaning all entries need to be given in the right order as a comma separated string. As this is quiet counter-intuitive for a user (that might expect they could provide only one or unsorted entries) these attributes have been left out. Change-Id: I355b6584e115cf70a7fd040a24d30f9686555b85 --- openstack/network/v2/bgpvpn.py | 13 +++++++++++++ openstack/tests/unit/network/v2/test_bgpvpn.py | 8 ++++++++ .../notes/bgpvpn-list-filters-e76183a7008c0631.yaml | 7 +++++++ 3 files changed, 28 insertions(+) create mode 100644 releasenotes/notes/bgpvpn-list-filters-e76183a7008c0631.yaml diff --git a/openstack/network/v2/bgpvpn.py b/openstack/network/v2/bgpvpn.py index 98de33208..03478c8e5 100644 --- a/openstack/network/v2/bgpvpn.py +++ b/openstack/network/v2/bgpvpn.py @@ -27,6 +27,19 @@ class BgpVpn(resource.Resource): allow_delete = True allow_list = True + _query_mapping = resource.QueryParameters( + 'name', + 'project_id', + 'local_pref', + 'vni', + 'type', + 'networks', + 'routers', + 'ports', + # NOTE(seba): (route|import|export) targets only support exact matches + # and have therefore been left out + ) + # Properties #: The Id of the BGPVPN id = resource.Body('id') diff --git a/openstack/tests/unit/network/v2/test_bgpvpn.py b/openstack/tests/unit/network/v2/test_bgpvpn.py index c6e4671e0..0aba1fefb 100644 --- a/openstack/tests/unit/network/v2/test_bgpvpn.py +++ b/openstack/tests/unit/network/v2/test_bgpvpn.py @@ -62,6 +62,14 @@ def test_make_it(self): { 'limit': 'limit', 'marker': 'marker', + 'local_pref': 'local_pref', + 'name': 'name', + 'networks': 'networks', + 'routers': 'routers', + 'ports': 'ports', + 'project_id': 'project_id', + 'type': 'type', + 'vni': 'vni', }, sot._query_mapping._mapping, ) diff --git a/releasenotes/notes/bgpvpn-list-filters-e76183a7008c0631.yaml b/releasenotes/notes/bgpvpn-list-filters-e76183a7008c0631.yaml new file mode 100644 index 000000000..4eb359e9b --- /dev/null +++ b/releasenotes/notes/bgpvpn-list-filters-e76183a7008c0631.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + ``openstack.network.v2.bgpvpn.BgpVpn`` can now be filtered by its + associations to `networks`, `routers` and `ports. Additionally, + filtering for the attributes `name`, `project_id`, `local_pref`, `vni` + and `type` is now done on server-side. From e67a7b14b98e4890fa334ab7b200505bf357f287 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 23 Apr 2024 12:13:19 +0100 Subject: [PATCH 3477/3836] pre-commit: Bump versions We apply fold in the new black changes also. Change-Id: I632728aa81e6c55423c0275a0a9b35e0753980a6 Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 6 +++--- openstack/block_storage/v3/_proxy.py | 9 +++------ openstack/cloud/_security_group.py | 12 ++++++------ openstack/common/tag.py | 3 +-- openstack/connection.py | 6 +++--- openstack/image/_download.py | 3 +-- openstack/proxy.py | 9 +++------ openstack/resource.py | 9 +++------ openstack/tests/base.py | 1 - .../tests/unit/cloud/test_role_assignment.py | 16 ++++++++++------ tox.ini | 3 ++- 11 files changed, 35 insertions(+), 42 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5677151b0..f5b3fa8fe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ default_language_version: python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: trailing-whitespace - id: mixed-line-ending @@ -22,7 +22,7 @@ repos: hooks: - id: doc8 - repo: https://github.com/psf/black - rev: 23.12.1 + rev: 24.4.0 hooks: - id: black args: ['-S', '-l', '79'] @@ -34,7 +34,7 @@ repos: - flake8-import-order~=0.18.2 exclude: '^(doc|releasenotes|tools)/.*$' - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.8.0 + rev: v1.9.0 hooks: - id: mypy additional_dependencies: diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index cf17a8f60..f3bfafaed 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -1780,8 +1780,7 @@ def find_service( name_or_id: str, ignore_missing: ty.Literal[True] = True, **query, - ) -> ty.Optional[_service.Service]: - ... + ) -> ty.Optional[_service.Service]: ... @ty.overload def find_service( @@ -1789,8 +1788,7 @@ def find_service( name_or_id: str, ignore_missing: ty.Literal[False], **query, - ) -> _service.Service: - ... + ) -> _service.Service: ... # excuse the duplication here: it's mypy's fault # https://github.com/python/mypy/issues/14764 @@ -1800,8 +1798,7 @@ def find_service( name_or_id: str, ignore_missing: bool, **query, - ) -> ty.Optional[_service.Service]: - ... + ) -> ty.Optional[_service.Service]: ... def find_service( self, diff --git a/openstack/cloud/_security_group.py b/openstack/cloud/_security_group.py index d6d70a34f..a78bd578a 100644 --- a/openstack/cloud/_security_group.py +++ b/openstack/cloud/_security_group.py @@ -323,12 +323,12 @@ def create_security_group_rule( # as the equivalent value. rule_def = { 'security_group_id': secgroup['id'], - 'port_range_min': None - if port_range_min == -1 - else port_range_min, - 'port_range_max': None - if port_range_max == -1 - else port_range_max, + 'port_range_min': ( + None if port_range_min == -1 else port_range_min + ), + 'port_range_max': ( + None if port_range_max == -1 else port_range_max + ), 'protocol': protocol, 'remote_ip_prefix': remote_ip_prefix, 'remote_group_id': remote_group_id, diff --git a/openstack/common/tag.py b/openstack/common/tag.py index e49a38268..0a2d16ec3 100644 --- a/openstack/common/tag.py +++ b/openstack/common/tag.py @@ -21,8 +21,7 @@ class TagMixin: _body: resource._ComponentManager @classmethod - def _get_session(cls, session): - ... + def _get_session(cls, session): ... _tag_query_parameters = { 'tags': 'tags', diff --git a/openstack/connection.py b/openstack/connection.py index 499cfd88e..a9d0d9392 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -558,9 +558,9 @@ def __init__( self.config._influxdb_config and 'additional_metric_tags' in self.config.config ): - self.config._influxdb_config[ - 'additional_metric_tags' - ] = self.config.config['additional_metric_tags'] + self.config._influxdb_config['additional_metric_tags'] = ( + self.config.config['additional_metric_tags'] + ) # Register cleanup steps atexit.register(self.close) diff --git a/openstack/image/_download.py b/openstack/image/_download.py index e15b8f2ad..fc4145cbf 100644 --- a/openstack/image/_download.py +++ b/openstack/image/_download.py @@ -41,8 +41,7 @@ def fetch( resource_response_key=None, microversion=None, **params, - ): - ... + ): ... def download( self, diff --git a/openstack/proxy.py b/openstack/proxy.py index c058964a9..5026cf2c9 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -484,8 +484,7 @@ def _find( name_or_id: str, ignore_missing: ty.Literal[True] = True, **attrs, - ) -> ty.Optional[ResourceType]: - ... + ) -> ty.Optional[ResourceType]: ... @ty.overload def _find( @@ -494,8 +493,7 @@ def _find( name_or_id: str, ignore_missing: ty.Literal[False], **attrs, - ) -> ResourceType: - ... + ) -> ResourceType: ... # excuse the duplication here: it's mypy's fault # https://github.com/python/mypy/issues/14764 @@ -506,8 +504,7 @@ def _find( name_or_id: str, ignore_missing: bool, **attrs, - ) -> ty.Optional[ResourceType]: - ... + ) -> ty.Optional[ResourceType]: ... def _find( self, diff --git a/openstack/resource.py b/openstack/resource.py index 53b786176..76293f216 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -2258,8 +2258,7 @@ def find( microversion: ty.Optional[str] = None, all_projects: ty.Optional[bool] = None, **params, - ) -> ty.Optional['Resource']: - ... + ) -> ty.Optional['Resource']: ... @ty.overload @classmethod @@ -2273,8 +2272,7 @@ def find( microversion: ty.Optional[str] = None, all_projects: ty.Optional[bool] = None, **params, - ) -> 'Resource': - ... + ) -> 'Resource': ... # excuse the duplication here: it's mypy's fault # https://github.com/python/mypy/issues/14764 @@ -2290,8 +2288,7 @@ def find( microversion: ty.Optional[str] = None, all_projects: ty.Optional[bool] = None, **params, - ): - ... + ): ... @classmethod def find( diff --git a/openstack/tests/base.py b/openstack/tests/base.py index 88cdebcbb..0882899b1 100644 --- a/openstack/tests/base.py +++ b/openstack/tests/base.py @@ -31,7 +31,6 @@ class TestCase(base.BaseTestCase): - """Test case base class for all tests.""" # A way to adjust slow test classes diff --git a/openstack/tests/unit/cloud/test_role_assignment.py b/openstack/tests/unit/cloud/test_role_assignment.py index 23b5e3d51..45d213c7f 100644 --- a/openstack/tests/unit/cloud/test_role_assignment.py +++ b/openstack/tests/unit/cloud/test_role_assignment.py @@ -199,9 +199,11 @@ def __user_mocks(self, user_data, use_name, is_found=True): uri=self.get_mock_url(resource='users'), status_code=200, json={ - 'users': [user_data.json_response['user']] - if is_found - else [] + 'users': ( + [user_data.json_response['user']] + if is_found + else [] + ) }, ) ) @@ -215,9 +217,11 @@ def __user_mocks(self, user_data, use_name, is_found=True): ), status_code=200, json={ - 'users': [user_data.json_response['user']] - if is_found - else [] + 'users': ( + [user_data.json_response['user']] + if is_found + else [] + ) }, ) ) diff --git a/tox.ini b/tox.ini index 693535a1c..59493d19d 100644 --- a/tox.ini +++ b/tox.ini @@ -146,13 +146,14 @@ application-import-names = openstack # if they fix ALL of the occurances of one and only one of them. # E203 Black will put spaces after colons in list comprehensions # E501 Black takes care of line length for us +# E704 Black will occasionally put multiple statements on one line # H238 New Style Classes are the default in Python3 # H301 Black will put commas after imports that can't fit on one line # H4 Are about docstrings and there's just a huge pile of pre-existing issues. # W503 Is supposed to be off by default but in the latest pycodestyle isn't. # Also, both openstacksdk and Donald Knuth disagree with the rule. Line # breaks should occur before the binary operator for readability. -ignore = E203, E501, H301, H238, H4, W503 +ignore = E203, E501, E704, H301, H238, H4, W503 import-order-style = pep8 show-source = True exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build,openstack/_services_mixin.py From 81d60c7874d02d36ba835e09a977509f8723fce9 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 24 Apr 2024 11:11:56 +0100 Subject: [PATCH 3478/3836] README: Add guide on raw HTTP layer Highlight that one can do e.g. `conn.compute.get('/servers')`. You almost certainly *don't want* to do this, but it can be helpful (see: our support for Nova's os-hypervisors API in OSC). The flow of the README is modified slightly so we go sequentially from high-level layers to low-level layers. Rubrics (header-like elements that don't produce anchors or appear in tables of contents) are also added to produce improve information hierarchy. Change-Id: Ifd4a5a2c753f6698fa4384a197e81cc5383ef312 Signed-off-by: Stephen Finucane --- README.rst | 113 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 95 insertions(+), 18 deletions(-) diff --git a/README.rst b/README.rst index a3a062569..51b862be7 100644 --- a/README.rst +++ b/README.rst @@ -20,6 +20,8 @@ https://docs.openstack.org/openstacksdk/latest/contributor/history.html Getting started --------------- +.. rubric:: Authentication and connection management + openstacksdk aims to talk to any OpenStack cloud. To do this, it requires a configuration file. openstacksdk favours ``clouds.yaml`` files, but can also use environment variables. The ``clouds.yaml`` file should be provided by your @@ -38,29 +40,43 @@ cloud provider or deployment tooling. An example: openstacksdk will look for ``clouds.yaml`` files in the following locations: +* If set, the path indicated by the ``OS_CLIENT_CONFIG_FILE`` environment + variable * ``.`` (the current directory) * ``$HOME/.config/openstack`` * ``/etc/openstack`` -openstacksdk consists of three layers. Most users will make use of the *proxy* -layer. Using the above ``clouds.yaml``, consider listing servers: +You can create a connection using the ``openstack.connect`` function. The cloud +name can be either passed directly to this function or specified using the +``OS_CLOUD`` environment variable. If you don't have a ``clouds.yaml`` file and +instead use environment variables for configuration then you can use the +special ``envvars`` cloud name to load configuration from the environment. For +example: .. code-block:: python import openstack - # Initialize and turn on debug logging - openstack.enable_logging(debug=True) + # Initialize connection from a clouds.yaml by passing a cloud name + conn_from_cloud_name = openstack.connect(cloud='mordred') - # Initialize connection - conn = openstack.connect(cloud='mordred') + # Initialize connection from a clouds.yaml using the OS_CLOUD envvar + conn_from_os_cloud = openstack.connect() - # List the servers - for server in conn.compute.servers(): - print(server.to_dict()) + # Initialize connection from environment variables + conn_from_env_vars = openstack.connect(cloud='envvars') + +.. note:: -openstacksdk also contains a higher-level *cloud* layer based on logical -operations: + How this is all achieved is described in more detail `below + `__. + +.. rubric:: The cloud layer + +openstacksdk consists of four layers which all build on top of each other. The +highest level layer is the *cloud* layer. Cloud layer methods are available via +the top level ``Connection`` object returned by ``openstack.connect``. For +example: .. code-block:: python @@ -76,8 +92,10 @@ operations: for server in conn.list_servers(): print(server.to_dict()) -The benefit of this layer is mostly seen in more complicated operations that -take multiple steps and where the steps vary across providers. For example: +The cloud layer is based on logical operations that can potentially touch +multiple services. The benefit of this layer is mostly seen in more complicated +operations that take multiple steps and where the steps vary across providers. +For example: .. code-block:: python @@ -101,9 +119,38 @@ take multiple steps and where the steps vary across providers. For example: conn.create_server( 'my-server', image=image, flavor=flavor, wait=True, auto_ip=True) -Finally, there is the low-level *resource* layer. This provides support for the -basic CRUD operations supported by REST APIs and is the base building block for -the other layers. You typically will not need to use this directly: +.. rubric:: The proxy layer + +The next layer is the *proxy* layer. Most users will make use of this layer. +The proxy layer is service-specific, so methods will be available under +service-specific connection attributes of the ``Connection`` object such as +``compute``, ``block_storage``, ``image`` etc. For example: + +.. code-block:: python + + import openstack + + # Initialize and turn on debug logging + openstack.enable_logging(debug=True) + + # Initialize connection + conn = openstack.connect(cloud='mordred') + + # List the servers + for server in conn.compute.servers(): + print(server.to_dict()) + +.. note:: + + A list of supported services is given `below `__. + +.. rubric:: The resource layer + +Below this there is the *resource* layer. This provides support for the basic +CRUD operations supported by REST APIs and is the base building block for the +other layers. You typically will not need to use this directly but it can be +helpful for operations where you already have a ``Resource`` object to hand. +For example: .. code-block:: python @@ -121,6 +168,34 @@ the other layers. You typically will not need to use this directly: for server in openstack.compute.v2.server.Server.list(session=conn.compute): print(server.to_dict()) +.. rubric:: The raw HTTP layer + +Finally, there is the *raw HTTP* layer. This exposes raw HTTP semantics and +is effectively a wrapper around the `requests`__ API with added smarts to +handle stuff like authentication and version management. As such, you can use +the ``requests`` API methods you know and love, like ``get``, ``post`` and +``put``, and expect to receive a ``requests.Response`` object in response +(unlike the other layers, which mostly all return objects that subclass +``openstack.resource.Resource``). Like the *resource* layer, you will typically +not need to use this directly but it can be helpful to interact with APIs that +have not or will not be supported by openstacksdk. For example: + +.. code-block:: python + + import openstack + + # Initialize and turn on debug logging + openstack.enable_logging(debug=True) + + # Initialize connection + conn = openstack.connect(cloud='mordred') + + # List servers + for server in openstack.compute.get('/servers').json(): + print(server) + +.. __: https://requests.readthedocs.io/en/latest/ + .. _openstack.config: Configuration @@ -145,14 +220,14 @@ environment by running ``openstack.config.loader``. For example: More information at https://docs.openstack.org/openstacksdk/latest/user/config/configuration.html +.. _supported-services: + Supported services ------------------ The following services are currently supported. A full list of all available OpenStack service can be found in the `Project Navigator`__. -.. __: https://www.openstack.org/software/project-navigator/openstack-components#openstack-services - .. note:: Support here does not guarantee full-support for all APIs. It simply means @@ -302,6 +377,8 @@ OpenStack service can be found in the `Project Navigator`__. - ✔ - ✔ (``openstack.instance_ha``) +.. __: https://www.openstack.org/software/project-navigator/openstack-components#openstack-services + Links ----- From 5de45a329db004b1106468c66b82a298219084f5 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 2 Feb 2024 10:37:43 +0000 Subject: [PATCH 3479/3836] hacking: Add check for deprecated exception types We also move everything to a new module with no external imports so flake8 can import it without needing to install all the dependencies of openstacksdk. Change-Id: I8e610bc196f530223b27a6fbb8e8ca11b6420b82 Signed-off-by: Stephen Finucane --- openstack/{_hacking.py => _hacking/checks.py} | 23 ++++++++++++- openstack/cloud/exc.py | 2 +- openstack/tests/unit/test_hacking.py | 33 ++++++++++++++++--- tox.ini | 5 +-- 4 files changed, 55 insertions(+), 8 deletions(-) rename openstack/{_hacking.py => _hacking/checks.py} (66%) diff --git a/openstack/_hacking.py b/openstack/_hacking/checks.py similarity index 66% rename from openstack/_hacking.py rename to openstack/_hacking/checks.py index 01b05adfc..0325be2dd 100644 --- a/openstack/_hacking.py +++ b/openstack/_hacking/checks.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. +import os import re from hacking import core @@ -26,7 +27,7 @@ - Keep the test method code in the source file ordered based on the O3xx value. - List the new rule in the top level HACKING.rst file - - Add test cases for each new rule to nova/tests/unit/test_hacking.py + - Add test cases for each new rule to openstack/tests/unit/test_hacking.py """ @@ -41,3 +42,23 @@ def assert_no_setupclass(logical_line): """ if SETUPCLASS_RE.match(logical_line): yield (0, "O300: setUpClass not allowed") + + +@core.flake8ext +def assert_no_deprecated_exceptions(logical_line, filename): + """Check for use of deprecated cloud-layer exceptions + + 0310 + """ + if filename.endswith(os.path.join('openstack', 'cloud', 'exc.py')): + return + + for exception in ( + 'OpenStackCloudTimeout', + 'OpenStackCloudHTTPError', + 'OpenStackCloudBadRequest', + 'OpenStackCloudURINotFound', + 'OpenStackCloudResourceNotFound', + ): + if re.search(fr'\b{exception}\b', logical_line): + yield (0, 'O310: Use of deprecated Exception class') diff --git a/openstack/cloud/exc.py b/openstack/cloud/exc.py index cc831f402..68ffb35fc 100644 --- a/openstack/cloud/exc.py +++ b/openstack/cloud/exc.py @@ -37,7 +37,7 @@ class OpenStackCloudUnavailableFeature(OpenStackCloudException): pass -# Backwards compat +# Backwards compat. These are deprecated and should not be used in new code. OpenStackCloudTimeout = exceptions.ResourceTimeout OpenStackCloudHTTPError = exceptions.HttpException OpenStackCloudBadRequest = exceptions.BadRequestException diff --git a/openstack/tests/unit/test_hacking.py b/openstack/tests/unit/test_hacking.py index bf719d11d..df4ce222f 100644 --- a/openstack/tests/unit/test_hacking.py +++ b/openstack/tests/unit/test_hacking.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import _hacking +from openstack._hacking import checks from openstack.tests.unit import base @@ -52,20 +52,45 @@ class HackingTestCase(base.TestCase): def test_assert_no_setupclass(self): self.assertEqual( - len(list(_hacking.assert_no_setupclass("def setUpClass(cls)"))), 1 + len(list(checks.assert_no_setupclass("def setUpClass(cls)"))), 1 ) self.assertEqual( - len(list(_hacking.assert_no_setupclass("# setUpClass is evil"))), 0 + len(list(checks.assert_no_setupclass("# setUpClass is evil"))), 0 ) self.assertEqual( len( list( - _hacking.assert_no_setupclass( + checks.assert_no_setupclass( "def setUpClassyDrinkingLocation(cls)" ) ) ), 0, ) + + def test_assert_no_deprecated_exceptions(self): + self.assertEqual( + len( + list( + checks.assert_no_deprecated_exceptions( + "raise exc.OpenStackCloudTimeout", + "openstack/cloud/compute.py", + ) + ) + ), + 1, + ) + + self.assertEqual( + len( + list( + checks.assert_no_deprecated_exceptions( + "raise exc.OpenStackCloudTimeout", + "openstack/cloud/exc.py", + ) + ) + ), + 0, + ) diff --git a/tox.ini b/tox.ini index 693535a1c..3d287e1d2 100644 --- a/tox.ini +++ b/tox.ini @@ -159,8 +159,9 @@ exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build,openstack/_services_m [flake8:local-plugins] extension = - O300 = _hacking:assert_no_setupclass -paths = ./openstack + O300 = checks:assert_no_setupclass + O310 = checks:assert_no_deprecated_exceptions +paths = ./openstack/_hacking [doc8] extensions = .rst, .yaml From f90fd9a3078ccf5c62b9b742511884986f6abb23 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 2 Feb 2024 11:14:26 +0000 Subject: [PATCH 3480/3836] cloud: Remove use of OpenStackCloudCreateException We'd like to get rid of the openstack.cloud.exc module. This exception has one user so it is an easy cull. Change-Id: I9b6a96d3b4d66ed69d87ba2a1d27bee9bab8c98d Signed-off-by: Stephen Finucane --- openstack/_hacking/checks.py | 1 + openstack/cloud/_compute.py | 30 +++++++++++++++++++++++------- openstack/cloud/exc.py | 18 +++++++++--------- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/openstack/_hacking/checks.py b/openstack/_hacking/checks.py index 0325be2dd..df0e7d4ea 100644 --- a/openstack/_hacking/checks.py +++ b/openstack/_hacking/checks.py @@ -54,6 +54,7 @@ def assert_no_deprecated_exceptions(logical_line, filename): return for exception in ( + 'OpenStackCloudCreateException', 'OpenStackCloudTimeout', 'OpenStackCloudHTTPError', 'OpenStackCloudBadRequest', diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 2a2df8bdb..7c604b85c 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -983,10 +983,26 @@ def create_server( admin_pass = server.admin_password or kwargs.get('admin_pass') if not wait: server = self.compute.get_server(server.id) - if server.status == 'ERROR': - raise exc.OpenStackCloudCreateException( - resource='server', resource_id=server.id + if server['status'] == 'ERROR': + if ( + 'fault' in server + and server['fault'] is not None + and 'message' in server['fault'] + ): + raise exceptions.SDKException( + "Error in creating the server. " + "Compute service reports fault: {reason}".format( + reason=server['fault']['message'] + ), + extra_data=dict(server=server), + ) + + raise exceptions.SDKException( + "Error in creating the server " + "(no further information available)", + extra_data=dict(server=server), ) + server = meta.add_server_interfaces(self, server) else: @@ -1172,16 +1188,16 @@ def get_active_server( and 'message' in server['fault'] ): raise exceptions.SDKException( - "Error in creating the server." - " Compute service reports fault: {reason}".format( + "Error in creating the server. " + "Compute service reports fault: {reason}".format( reason=server['fault']['message'] ), extra_data=dict(server=server), ) raise exceptions.SDKException( - "Error in creating the server" - " (no further information available)", + "Error in creating the server " + "(no further information available)", extra_data=dict(server=server), ) diff --git a/openstack/cloud/exc.py b/openstack/cloud/exc.py index 68ffb35fc..b104653a0 100644 --- a/openstack/cloud/exc.py +++ b/openstack/cloud/exc.py @@ -17,6 +17,15 @@ OpenStackCloudException = exceptions.SDKException +class OpenStackCloudUnavailableExtension(OpenStackCloudException): + pass + + +class OpenStackCloudUnavailableFeature(OpenStackCloudException): + pass + + +# Backwards compat. These are deprecated and should not be used in new code. class OpenStackCloudCreateException(OpenStackCloudException): def __init__(self, resource, resource_id, extra_data=None, **kwargs): super(OpenStackCloudCreateException, self).__init__( @@ -29,15 +38,6 @@ def __init__(self, resource, resource_id, extra_data=None, **kwargs): self.resource_id = resource_id -class OpenStackCloudUnavailableExtension(OpenStackCloudException): - pass - - -class OpenStackCloudUnavailableFeature(OpenStackCloudException): - pass - - -# Backwards compat. These are deprecated and should not be used in new code. OpenStackCloudTimeout = exceptions.ResourceTimeout OpenStackCloudHTTPError = exceptions.HttpException OpenStackCloudBadRequest = exceptions.BadRequestException From 763e09a4b194576b394f7b814b2ef10c94ffea9c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 21 Aug 2023 10:53:33 +0100 Subject: [PATCH 3481/3836] cloud: Trivial fixes Before we combine the various nova-net/neutron common network functions address a few nits around return statement indentation and docstring wrapping. Change-Id: I992673d30225c6639012fd4add91e86ac5e72966 Signed-off-by: Stephen Finucane --- openstack/cloud/_block_storage.py | 6 +- openstack/cloud/_compute.py | 54 +++++------ openstack/cloud/_floating_ip.py | 89 +++++++++---------- openstack/cloud/_network.py | 11 ++- openstack/cloud/_network_common.py | 72 +++++++-------- openstack/cloud/_security_group.py | 31 ++++--- .../tests/unit/cloud/test_floating_ip_nova.py | 3 +- 7 files changed, 128 insertions(+), 138 deletions(-) diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index 501f51851..3351360cd 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -147,10 +147,8 @@ def create_volume( image_obj = self.get_image(image) if not image_obj: raise exceptions.SDKException( - "Image {image} was requested as the basis for a new" - " volume, but was not found on the cloud".format( - image=image - ) + f"Image {image} was requested as the basis for a new " + f"volume but was not found on the cloud" ) kwargs['imageRef'] = image_obj['id'] kwargs = self._get_volume_kwargs(kwargs) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 2a2df8bdb..bfff6196e 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -389,13 +389,14 @@ def get_compute_limits(self, name_or_id=None): valid project """ params = {} - project_id = None if name_or_id: - proj = self.get_project(name_or_id) - if not proj: - raise exceptions.SDKException("project does not exist") - project_id = proj.id - params['tenant_id'] = project_id + project = self.get_project(name_or_id) + if not project: + raise exceptions.SDKException( + f"Project {name_or_id} was requested but was not found " + f"on the cloud" + ) + params['tenant_id'] = project.id return self.compute.get_limits(**params).absolute def get_keypair(self, name_or_id, filters=None): @@ -1031,12 +1032,8 @@ def _get_boot_from_volume_kwargs( volume = self.get_volume(boot_volume) if not volume: raise exceptions.SDKException( - 'Volume {boot_volume} is not a valid volume' - ' in {cloud}:{region}'.format( - boot_volume=boot_volume, - cloud=self.name, - region=self._compute_region, - ) + f"Volume {volume} was requested but was not found " + f"on the cloud" ) block_mapping = { 'boot_index': '0', @@ -1052,15 +1049,11 @@ def _get_boot_from_volume_kwargs( image_obj = image else: image_obj = self.get_image(image) - if not image_obj: - raise exceptions.SDKException( - 'Image {image} is not a valid image in' - ' {cloud}:{region}'.format( - image=image, - cloud=self.name, - region=self._compute_region, + if not image_obj: + raise exceptions.SDKException( + f"Image {image} was requested but was not found " + f"on the cloud" ) - ) block_mapping = { 'boot_index': '0', @@ -1076,23 +1069,19 @@ def _get_boot_from_volume_kwargs( # If we're attaching volumes on boot but booting from an image, # we need to specify that in the BDM. block_mapping = { - u'boot_index': 0, - u'delete_on_termination': True, - u'destination_type': u'local', - u'source_type': u'image', - u'uuid': kwargs['imageRef'], + 'boot_index': 0, + 'delete_on_termination': True, + 'destination_type': 'local', + 'source_type': 'image', + 'uuid': kwargs['imageRef'], } kwargs['block_device_mapping_v2'].append(block_mapping) for volume in volumes: volume_obj = self.get_volume(volume) if not volume_obj: raise exceptions.SDKException( - 'Volume {volume} is not a valid volume' - ' in {cloud}:{region}'.format( - volume=volume, - cloud=self.name, - region=self._compute_region, - ) + f"Volume {volume} was requested but was not found " + f"on the cloud" ) block_mapping = { 'boot_index': '-1', @@ -1824,7 +1813,8 @@ def parse_date(date): proj = self.get_project(name_or_id) if not proj: raise exceptions.SDKException( - "project does not exist: {name}".format(name=proj.id) + f"Project {name_or_id} was requested but was not found " + f"on the cloud" ) return self.compute.get_usage(proj, start, end) diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index 970d337f7..d3e020a9e 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -144,8 +144,8 @@ def list_floating_ips(self, filters=None): else: if filters: raise ValueError( - "Nova-network don't support server-side floating ips " - "filtering. Use the search_floating_ips method instead" + "nova-network doesn't support server-side floating IPs " + "filtering. Use the 'search_floating_ips' method instead" ) floating_ips = self._nova_list_floating_ips() @@ -159,7 +159,7 @@ def list_floating_ip_pools(self): neutron. `get_external_ipv4_floating_networks` is what you should almost certainly be using. - :returns: A list of floating IP pools + :returns: A list of floating IP pool objects """ if not self._has_nova_extension('os-floating-ip-pools'): raise exc.OpenStackCloudUnavailableExtension( @@ -407,11 +407,11 @@ def create_floating_ip( if port: raise exceptions.SDKException( - "This cloud uses nova-network which does not support" - " arbitrary floating-ip/port mappings. Please nudge" - " your cloud provider to upgrade the networking stack" - " to neutron, or alternately provide the server," - " fixed_address and nat_destination arguments as appropriate" + "This cloud uses nova-network which does not support " + "arbitrary floating-ip/port mappings. Please nudge " + "your cloud provider to upgrade the networking stack " + "to neutron, or alternately provide the server, " + "fixed_address and nat_destination arguments as appropriate" ) # Else, we are using Nova network f_ips = self._normalize_floating_ips( @@ -482,8 +482,8 @@ def _neutron_create_floating_ip( break except exceptions.ResourceTimeout: self.log.error( - "Timed out on floating ip %(fip)s becoming active." - " Deleting", + "Timed out on floating ip %(fip)s becoming active. " + "Deleting", {'fip': fip_id}, ) try: @@ -499,8 +499,8 @@ def _neutron_create_floating_ip( if fip['port_id'] != port: if server: raise exceptions.SDKException( - "Attempted to create FIP on port {port} for server" - " {server} but FIP has port {port_id}".format( + "Attempted to create FIP on port {port} for server " + "{server} but FIP has port {port_id}".format( port=port, port_id=fip['port_id'], server=server['id'], @@ -508,8 +508,8 @@ def _neutron_create_floating_ip( ) else: raise exceptions.SDKException( - "Attempted to create FIP on port {port}" - " but something went wrong".format(port=port) + "Attempted to create FIP on port {port} " + "but something went wrong".format(port=port) ) return fip @@ -542,9 +542,9 @@ def delete_floating_ip(self, floating_ip_id, retry=1): :param floating_ip_id: a floating IP address ID. :param retry: number of times to retry. Optional, defaults to 1, - which is in addition to the initial delete call. - A value of 0 will also cause no checking of results to - occur. + which is in addition to the initial delete call. + A value of 0 will also cause no checking of results to + occur. :returns: True if the IP address has been deleted, False if the IP address was not found. @@ -566,10 +566,10 @@ def delete_floating_ip(self, floating_ip_id, retry=1): return True raise exceptions.SDKException( - "Attempted to delete Floating IP {ip} with ID {id} a total of" - " {retry} times. Although the cloud did not indicate any errors" - " the floating ip is still in existence. Aborting further" - " operations.".format( + "Attempted to delete Floating IP {ip} with ID {id} a total of " + "{retry} times. Although the cloud did not indicate any errors " + "the floating IP is still in existence. Aborting further " + "operations.".format( id=floating_ip_id, ip=f_ip['floating_ip_address'], retry=retry + 1, @@ -623,9 +623,8 @@ def delete_unattached_floating_ips(self, retry=1): IPs by passing in a server to the create_floating_ip call. :param retry: number of times to retry. Optional, defaults to 1, - which is in addition to the initial delete call. - A value of 0 will also cause no checking of results to - occur. + which is in addition to the initial delete call. + A value of 0 will also cause no checking of results to occur. :returns: Number of Floating IPs deleted, False if none :raises: :class:`~openstack.exceptions.SDKException` on operation @@ -657,15 +656,15 @@ def _attach_ip_to_server( :param server: Server dict :param floating_ip: Floating IP dict to attach :param fixed_address: (optional) fixed address to which attach the - floating IP to. + floating IP to. :param wait: (optional) Wait for the address to appear as assigned - to the server. Defaults to False. + to the server. Defaults to False. :param timeout: (optional) Seconds to wait, defaults to 60. - See the ``wait`` parameter. + See the ``wait`` parameter. :param skip_attach: (optional) Skip the actual attach and just do - the wait. Defaults to False. + the wait. Defaults to False. :param nat_destination: The fixed network the server's port for the - FIP to attach to will come from. + FIP to attach to will come from. :returns: The server ``openstack.compute.v2.server.Server`` :raises: :class:`~openstack.exceptions.SDKException` on operation @@ -859,11 +858,11 @@ def _add_ip_from_pool( :param fixed_address: a fixed address :param reuse: Try to reuse existing ips. Defaults to True. :param wait: (optional) Wait for the address to appear as assigned - to the server. Defaults to False. + to the server. Defaults to False. :param timeout: (optional) Seconds to wait, defaults to 60. - See the ``wait`` parameter. + See the ``wait`` parameter. :param nat_destination: (optional) the name of the network of the - port to associate with the floating ip. + port to associate with the floating ip. :returns: the updated server ``openstack.compute.v2.server.Server`` """ @@ -910,14 +909,13 @@ def add_ip_list( :param server: a server object :param ips: list of floating IP addresses or a single address :param wait: (optional) Wait for the address to appear as assigned - to the server. Defaults to False. + to the server. Defaults to False. :param timeout: (optional) Seconds to wait, defaults to 60. - See the ``wait`` parameter. + See the ``wait`` parameter. :param fixed_address: (optional) Fixed address of the server to - attach the IP to + attach the IP to :param nat_destination: (optional) Name or ID of the network that - the fixed IP to attach the - floating IP should be on + the fixed IP to attach the floating IP should be on :returns: The updated server ``openstack.compute.v2.server.Server`` :raises: :class:`~openstack.exceptions.SDKException` on operation @@ -953,15 +951,14 @@ def add_auto_ip(self, server, wait=False, timeout=60, reuse=True): :param server: a server dictionary. :param reuse: Whether or not to attempt to reuse IPs, defaults - to True. + to True. :param wait: (optional) Wait for the address to appear as assigned - to the server. Defaults to False. + to the server. Defaults to False. :param timeout: (optional) Seconds to wait, defaults to 60. - See the ``wait`` parameter. + See the ``wait`` parameter. :param reuse: Try to reuse existing ips. Defaults to True. :returns: Floating IP address attached to server. - """ server = self._add_auto_ip( server, wait=wait, timeout=timeout, reuse=reuse @@ -1004,10 +1001,10 @@ def _add_auto_ip(self, server, wait=False, timeout=60, reuse=True): # It failed. Delete so as not to leak an unmanaged # resource self.log.error( - "Timeout waiting for floating IP to become" - " active. Floating IP %(ip)s:%(id)s was created for" - " server %(server)s but is being deleted due to" - " activation failure.", + "Timeout waiting for floating IP to become " + "active. Floating IP %(ip)s:%(id)s was created for " + "server %(server)s but is being deleted due to " + "activation failure.", { 'ip': f_ip['floating_ip_address'], 'id': f_ip['id'], @@ -1265,7 +1262,7 @@ def _normalize_floating_ips(self, ips): Neutron. This function extract attributes that are common to Nova and Neutron floating IP resource. - If the whole structure is needed inside shade, shade provides private + If the whole structure is needed inside openstacksdk there are private methods that returns "original" objects (e.g. _neutron_allocate_floating_ip) diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index f6718748d..3cd63b30e 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -652,8 +652,10 @@ def set_network_quotas(self, name_or_id, **kwargs): proj = self.get_project(name_or_id) if not proj: - raise exceptions.SDKException("project does not exist") - + raise exceptions.SDKException( + f"Project {name_or_id} was requested by was not found " + f"on the cloud" + ) self.network.update_quota(proj.id, **kwargs) def get_network_quotas(self, name_or_id, details=False): @@ -688,7 +690,10 @@ def delete_network_quotas(self, name_or_id): """ proj = self.get_project(name_or_id) if not proj: - raise exceptions.SDKException("project does not exist") + raise exceptions.SDKException( + f"Project {name_or_id} was requested by was not found " + f"on the cloud" + ) self.network.delete_quota(proj.id) @_utils.valid_kwargs( diff --git a/openstack/cloud/_network_common.py b/openstack/cloud/_network_common.py index bf405e9d2..a33056e84 100644 --- a/openstack/cloud/_network_common.py +++ b/openstack/cloud/_network_common.py @@ -146,12 +146,12 @@ def _set_interesting_networks(self): if self._nat_source in (network['name'], network['id']): if nat_source: raise exceptions.SDKException( - 'Multiple networks were found matching' - ' {nat_net} which is the network configured' - ' to be the NAT source. Please check your' - ' cloud resources. It is probably a good idea' - ' to configure this network by ID rather than' - ' by name.'.format(nat_net=self._nat_source) + 'Multiple networks were found matching ' + '{nat_net} which is the network configured ' + 'to be the NAT source. Please check your ' + 'cloud resources. It is probably a good idea ' + 'to configure this network by ID rather than ' + 'by name.'.format(nat_net=self._nat_source) ) external_ipv4_floating_networks.append(network) nat_source = network @@ -164,12 +164,12 @@ def _set_interesting_networks(self): if self._nat_destination in (network['name'], network['id']): if nat_destination: raise exceptions.SDKException( - 'Multiple networks were found matching' - ' {nat_net} which is the network configured' - ' to be the NAT destination. Please check your' - ' cloud resources. It is probably a good idea' - ' to configure this network by ID rather than' - ' by name.'.format(nat_net=self._nat_destination) + 'Multiple networks were found matching ' + '{nat_net} which is the network configured ' + 'to be the NAT destination. Please check your ' + 'cloud resources. It is probably a good idea ' + 'to configure this network by ID rather than ' + 'by name.'.format(nat_net=self._nat_destination) ) nat_destination = network elif self._nat_destination is None: @@ -199,13 +199,13 @@ def _set_interesting_networks(self): if self._default_network in (network['name'], network['id']): if default_network: raise exceptions.SDKException( - 'Multiple networks were found matching' - ' {default_net} which is the network' - ' configured to be the default interface' - ' network. Please check your cloud resources.' - ' It is probably a good idea' - ' to configure this network by ID rather than' - ' by name.'.format(default_net=self._default_network) + 'Multiple networks were found matching ' + '{default_net} which is the network ' + 'configured to be the default interface ' + 'network. Please check your cloud resources. ' + 'It is probably a good idea ' + 'to configure this network by ID rather than ' + 'by name.'.format(default_net=self._default_network) ) default_network = network @@ -213,8 +213,8 @@ def _set_interesting_networks(self): for net_name in self._external_ipv4_names: if net_name not in [net['name'] for net in external_ipv4_networks]: raise exceptions.SDKException( - "Networks: {network} was provided for external IPv4" - " access and those networks could not be found".format( + "Networks: {network} was provided for external IPv4 " + "access and those networks could not be found".format( network=net_name ) ) @@ -222,8 +222,8 @@ def _set_interesting_networks(self): for net_name in self._internal_ipv4_names: if net_name not in [net['name'] for net in internal_ipv4_networks]: raise exceptions.SDKException( - "Networks: {network} was provided for internal IPv4" - " access and those networks could not be found".format( + "Networks: {network} was provided for internal IPv4 " + "access and those networks could not be found".format( network=net_name ) ) @@ -231,8 +231,8 @@ def _set_interesting_networks(self): for net_name in self._external_ipv6_names: if net_name not in [net['name'] for net in external_ipv6_networks]: raise exceptions.SDKException( - "Networks: {network} was provided for external IPv6" - " access and those networks could not be found".format( + "Networks: {network} was provided for external IPv6 " + "access and those networks could not be found".format( network=net_name ) ) @@ -240,31 +240,31 @@ def _set_interesting_networks(self): for net_name in self._internal_ipv6_names: if net_name not in [net['name'] for net in internal_ipv6_networks]: raise exceptions.SDKException( - "Networks: {network} was provided for internal IPv6" - " access and those networks could not be found".format( + "Networks: {network} was provided for internal IPv6 " + "access and those networks could not be found".format( network=net_name ) ) if self._nat_destination and not nat_destination: raise exceptions.SDKException( - 'Network {network} was configured to be the' - ' destination for inbound NAT but it could not be' - ' found'.format(network=self._nat_destination) + 'Network {network} was configured to be the ' + 'destination for inbound NAT but it could not be ' + 'found'.format(network=self._nat_destination) ) if self._nat_source and not nat_source: raise exceptions.SDKException( - 'Network {network} was configured to be the' - ' source for inbound NAT but it could not be' - ' found'.format(network=self._nat_source) + 'Network {network} was configured to be the ' + 'source for inbound NAT but it could not be ' + 'found'.format(network=self._nat_source) ) if self._default_network and not default_network: raise exceptions.SDKException( - 'Network {network} was configured to be the' - ' default network interface but it could not be' - ' found'.format(network=self._default_network) + 'Network {network} was configured to be the ' + 'default network interface but it could not be ' + 'found'.format(network=self._default_network) ) self._external_ipv4_networks = external_ipv4_networks diff --git a/openstack/cloud/_security_group.py b/openstack/cloud/_security_group.py index d6d70a34f..d84c7245c 100644 --- a/openstack/cloud/_security_group.py +++ b/openstack/cloud/_security_group.py @@ -54,15 +54,14 @@ def list_security_groups(self, filters=None): # pass filters dict to the list to filter as much as possible on # the server side return list(self.network.security_groups(**filters)) - # Handle nova security groups else: data = proxy._json_response( self.compute.get('/os-security-groups', params=filters) ) - return self._normalize_secgroups( - self._get_and_munchify('security_groups', data) - ) + return self._normalize_secgroups( + self._get_and_munchify('security_groups', data) + ) def get_security_group(self, name_or_id, filters=None): """Get a security group by name or ID. @@ -109,9 +108,9 @@ def get_security_group_by_id(self, id): self.compute.get(f'/os-security-groups/{id}'), error_message=error_message, ) - return self._normalize_secgroup( - self._get_and_munchify('security_group', data) - ) + return self._normalize_secgroup( + self._get_and_munchify('security_group', data) + ) def create_security_group( self, name, description, project_id=None, stateful=None @@ -155,9 +154,9 @@ def create_security_group( json={'security_group': security_group_json}, ) ) - return self._normalize_secgroup( - self._get_and_munchify('security_group', data) - ) + return self._normalize_secgroup( + self._get_and_munchify('security_group', data) + ) def delete_security_group(self, name_or_id): """Delete a security group @@ -237,9 +236,9 @@ def update_security_group(self, name_or_id, **kwargs): json={'security_group': kwargs}, ) ) - return self._normalize_secgroup( - self._get_and_munchify('security_group', data) - ) + return self._normalize_secgroup( + self._get_and_munchify('security_group', data) + ) def create_security_group_rule( self, @@ -389,9 +388,9 @@ def create_security_group_rule( '/os-security-group-rules', json=security_group_rule_dict ) ) - return self._normalize_secgroup_rule( - self._get_and_munchify('security_group_rule', data) - ) + return self._normalize_secgroup_rule( + self._get_and_munchify('security_group_rule', data) + ) def delete_security_group_rule(self, rule_id): """Delete a security group rule diff --git a/openstack/tests/unit/cloud/test_floating_ip_nova.py b/openstack/tests/unit/cloud/test_floating_ip_nova.py index 2c34c3d97..9bd9ade49 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_nova.py +++ b/openstack/tests/unit/cloud/test_floating_ip_nova.py @@ -110,7 +110,8 @@ def test_list_floating_ips(self): def test_list_floating_ips_with_filters(self): self.assertRaisesRegex( ValueError, - "Nova-network don't support server-side", + "nova-network doesn't support server-side floating IPs filtering. " + "Use the 'search_floating_ips' method instead", self.cloud.list_floating_ips, filters={'Foo': 42}, ) From a8adbadf0c4cdf1539019177fb1be08e04d98e82 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 17 Aug 2023 12:26:33 +0100 Subject: [PATCH 3482/3836] cloud: Do not reference other cloud mixin's methods We are using a mixin-like pattern to construct the cloud layer of our API. However, this is not a true mixin pattern since each of the "mixins" have dependencies on each other and to be honest, it's really only there to allow us to split up the otherwise giant 'Connection' object. Unfortunately this pattern doesn't really work with type checkers or language servers since there's no obvious way to identify where the additional resources are coming from until they're combined in the 'Connection' class. Fixing this will require multiple steps, but the first it to remove all dependencies between the various service mixins. As such, rather than depending on e.g. the 'get_volume' cloud method in various compute-related cloud methods, we use the 'find_volume' proxy method. A later change can then add these various proxy attributes, exposing them. Change-Id: I10d3782899ac519f715d771d83303990a8289f04 Signed-off-by: Stephen Finucane --- openstack/cloud/_baremetal.py | 6 +- openstack/cloud/_block_storage.py | 4 +- openstack/cloud/_compute.py | 31 ++- openstack/cloud/_floating_ip.py | 16 +- openstack/cloud/_image.py | 7 +- openstack/cloud/_network.py | 12 +- openstack/cloud/_network_common.py | 10 +- .../tests/unit/cloud/test_create_server.py | 54 +++-- .../unit/cloud/test_floating_ip_neutron.py | 185 +++++------------- openstack/tests/unit/cloud/test_limits.py | 2 +- openstack/tests/unit/cloud/test_quotas.py | 4 +- openstack/tests/unit/cloud/test_usage.py | 2 +- 12 files changed, 143 insertions(+), 190 deletions(-) diff --git a/openstack/cloud/_baremetal.py b/openstack/cloud/_baremetal.py index 81cde7561..f2a94dc79 100644 --- a/openstack/cloud/_baremetal.py +++ b/openstack/cloud/_baremetal.py @@ -427,7 +427,7 @@ def attach_port_to_machine(self, name_or_id, port_name_or_id): :return: Nothing. """ machine = self.get_machine(name_or_id) - port = self.get_port(port_name_or_id) + port = self.network.find_port(port_name_or_id) self.baremetal.attach_vif_to_node(machine, port['id']) def detach_port_from_machine(self, name_or_id, port_name_or_id): @@ -439,7 +439,7 @@ def detach_port_from_machine(self, name_or_id, port_name_or_id): :return: Nothing. """ machine = self.get_machine(name_or_id) - port = self.get_port(port_name_or_id) + port = self.network.find_port(port_name_or_id) self.baremetal.detach_vif_from_node(machine, port['id']) def list_ports_attached_to_machine(self, name_or_id): @@ -451,7 +451,7 @@ def list_ports_attached_to_machine(self, name_or_id): """ machine = self.get_machine(name_or_id) vif_ids = self.baremetal.list_node_vifs(machine) - return [self.get_port(vif) for vif in vif_ids] + return [self.network.find_port(vif) for vif in vif_ids] def validate_machine(self, name_or_id, for_deploy=True): """Validate parameters of the machine. diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index 3351360cd..23acfcf63 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -144,7 +144,7 @@ def create_volume( wait = True if image: - image_obj = self.get_image(image) + image_obj = self.image.find_image(image) if not image_obj: raise exceptions.SDKException( f"Image {image} was requested as the basis for a new " @@ -273,7 +273,7 @@ def get_volume_limits(self, name_or_id=None): """ params = {} if name_or_id: - project = self.get_project(name_or_id) + project = self.identity.find_project(name_or_id) if not project: raise exceptions.SDKException("project does not exist") params['project'] = project diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index bfff6196e..e1733e585 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -390,7 +390,7 @@ def get_compute_limits(self, name_or_id=None): """ params = {} if name_or_id: - project = self.get_project(name_or_id) + project = self.identity.find_project(name_or_id) if not project: raise exceptions.SDKException( f"Project {name_or_id} was requested but was not found " @@ -851,7 +851,7 @@ def create_server( kwargs[desired] = value if group: - group_obj = self.get_server_group(group) + group_obj = self.compute.find_server_group(group) if not group_obj: raise exceptions.SDKException( "Server Group {group} was requested but was not found" @@ -882,7 +882,7 @@ def create_server( if isinstance(net_name, dict) and 'id' in net_name: network_obj = net_name else: - network_obj = self.get_network(name_or_id=net_name) + network_obj = self.network.find_network(net_name) if not network_obj: raise exceptions.SDKException( 'Network {network} is not a valid network in' @@ -910,7 +910,7 @@ def create_server( nic.pop('net-name', None) elif 'net-name' in nic: net_name = nic.pop('net-name') - nic_net = self.get_network(net_name) + nic_net = self.network.find_network(net_name) if not nic_net: raise exceptions.SDKException( "Requested network {net} could not be found.".format( @@ -950,10 +950,21 @@ def create_server( kwargs['networks'] = 'auto' if image: + # TODO(stephenfin): Drop support for dicts: we should only accept + # strings or Image objects if isinstance(image, dict): kwargs['imageRef'] = image['id'] else: - kwargs['imageRef'] = self.get_image(image).id + image_obj = self.image.find_image(image) + if not image_obj: + raise exc.OpenStackCloudException( + f"Image {image} was requested but was not found " + f"on the cloud" + ) + kwargs['imageRef'] = image_obj.id + + # TODO(stephenfin): Drop support for dicts: we should only accept + # strings or Image objects if isinstance(flavor, dict): kwargs['flavorRef'] = flavor['id'] else: @@ -1029,7 +1040,7 @@ def _get_boot_from_volume_kwargs( # If we have boot_from_volume but no root volume, then we're # booting an image from volume if boot_volume: - volume = self.get_volume(boot_volume) + volume = self.block_storage.find_volume(boot_volume) if not volume: raise exceptions.SDKException( f"Volume {volume} was requested but was not found " @@ -1045,10 +1056,12 @@ def _get_boot_from_volume_kwargs( kwargs['block_device_mapping_v2'].append(block_mapping) kwargs['imageRef'] = '' elif boot_from_volume: + # TODO(stephenfin): Drop support for dicts: we should only accept + # strings or Image objects if isinstance(image, dict): image_obj = image else: - image_obj = self.get_image(image) + image_obj = self.image.find_image(image) if not image_obj: raise exceptions.SDKException( f"Image {image} was requested but was not found " @@ -1077,7 +1090,7 @@ def _get_boot_from_volume_kwargs( } kwargs['block_device_mapping_v2'].append(block_mapping) for volume in volumes: - volume_obj = self.get_volume(volume) + volume_obj = self.block_storage.find_volume(volume) if not volume_obj: raise exceptions.SDKException( f"Volume {volume} was requested but was not found " @@ -1810,7 +1823,7 @@ def parse_date(date): if isinstance(end, str): end = parse_date(end) - proj = self.get_project(name_or_id) + proj = self.identity.find_project(name_or_id) if not proj: raise exceptions.SDKException( f"Project {name_or_id} was requested but was not found " diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index d3e020a9e..06eb81ff0 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -297,13 +297,14 @@ def _nova_available_floating_ips(self, pool=None): def _find_floating_network_by_router(self): """Find the network providing floating ips by looking at routers.""" - for router in self.list_routers(): + for router in self.network.routers(): if router['admin_state_up']: network_id = router.get('external_gateway_info', {}).get( 'network_id' ) if network_id: - return network_id + self._floating_network_by_router = network_id + return self._floating_network_by_router def available_floating_ip(self, network=None, server=None): """Get a floating IP from a network or a pool. @@ -679,7 +680,7 @@ def _attach_ip_to_server( # old to check whether it belongs to us now, thus refresh # the server data and try again. There are some clouds, which # explicitely forbids FIP assign call if it is already assigned. - server = self.get_server_by_id(server['id']) + server = self.compute.get_server(server['id']) ext_ip = meta.get_server_ip( server, ext_tag='floating', public=True ) @@ -718,7 +719,7 @@ def _attach_ip_to_server( "Timeout waiting for the floating IP to be attached.", wait=min(5, timeout), ): - server = self.get_server_by_id(server_id) + server = self.compute.get_server(server_id) ext_ip = meta.get_server_ip( server, ext_tag='floating', public=True ) @@ -879,7 +880,7 @@ def _add_ip_from_pool( timeout=timeout, ) timeout = timeout - (time.time() - start_time) - server = self.get_server(server.id) + server = self.compute.get_server(server.id) # We run attach as a second call rather than in the create call # because there are code flows where we will not have an attached @@ -1154,8 +1155,7 @@ def _nat_destination_port( :param fixed_address: Fixed ip address of the port :param nat_destination: Name or ID of the network of the port. """ - port_filter = {'device_id': server['id']} - ports = self.search_ports(filters=port_filter) + ports = list(self.network.ports(device_id=server['id'])) if not ports: return (None, None) @@ -1163,7 +1163,7 @@ def _nat_destination_port( if not fixed_address: if len(ports) > 1: if nat_destination: - nat_network = self.get_network(nat_destination) + nat_network = self.network.find_network(nat_destination) if not nat_network: raise exceptions.SDKException( 'NAT Destination {nat_destination} was configured' diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py index 73f342b37..960f985be 100644 --- a/openstack/cloud/_image.py +++ b/openstack/cloud/_image.py @@ -202,11 +202,14 @@ def delete_image( self.image._IMAGE_OBJECT_KEY in image.properties or self.image._SHADE_IMAGE_OBJECT_KEY in image.properties ): - (container, objname) = image.properties.get( + container, objname = image.properties.get( self.image._IMAGE_OBJECT_KEY, image.properties.get(self.image._SHADE_IMAGE_OBJECT_KEY), ).split('/', 1) - self.delete_object(container=container, name=objname) + self.object_store.delete_object( + objname, + container=container, + ) if wait: for count in utils.iterate_timeout( diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index 3cd63b30e..ce35669b5 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -649,8 +649,7 @@ def set_network_quotas(self, name_or_id, **kwargs): :raises: :class:`~openstack.exceptions.SDKException` if the resource to set the quota does not exist. """ - - proj = self.get_project(name_or_id) + proj = self.identity.find_project(name_or_id) if not proj: raise exceptions.SDKException( f"Project {name_or_id} was requested by was not found " @@ -669,7 +668,12 @@ def get_network_quotas(self, name_or_id, details=False): :raises: :class:`~openstack.exceptions.SDKException` if it's not a valid project """ - proj = self.identity.find_project(name_or_id, ignore_missing=False) + proj = self.identity.find_project(name_or_id) + if not proj: + raise exc.OpenStackCloudException( + f"Project {name_or_id} was requested by was not found " + f"on the cloud" + ) return self.network.get_quota(proj.id, details) def get_network_extensions(self): @@ -688,7 +692,7 @@ def delete_network_quotas(self, name_or_id): :raises: :class:`~openstack.exceptions.SDKException` if it's not a valid project or the network client call failed """ - proj = self.get_project(name_or_id) + proj = self.identity.find_project(name_or_id) if not proj: raise exceptions.SDKException( f"Project {name_or_id} was requested by was not found " diff --git a/openstack/cloud/_network_common.py b/openstack/cloud/_network_common.py index a33056e84..b3d3a074b 100644 --- a/openstack/cloud/_network_common.py +++ b/openstack/cloud/_network_common.py @@ -80,7 +80,10 @@ def _set_interesting_networks(self): # this search_networks can just totally fail. If it does # though, that's fine, clearly the neutron introspection is # not going to work. - all_networks = self.list_networks() + if self.has_service('network'): + all_networks = list(self.network.networks()) + else: + all_networks = [] except exceptions.SDKException: self._network_list_stamp = True return @@ -179,7 +182,10 @@ def _set_interesting_networks(self): # it out. if all_subnets is None: try: - all_subnets = self.list_subnets() + if self.has_service('network'): + all_subnets = list(self.network.subnets()) + else: + all_subnets = [] except exceptions.SDKException: # Thanks Rackspace broken neutron all_subnets = [] diff --git a/openstack/tests/unit/cloud/test_create_server.py b/openstack/tests/unit/cloud/test_create_server.py index 1acedb2c3..e7712ffb3 100644 --- a/openstack/tests/unit/cloud/test_create_server.py +++ b/openstack/tests/unit/cloud/test_create_server.py @@ -21,7 +21,6 @@ from unittest import mock import uuid -from openstack.cloud import meta from openstack.compute.v2 import server from openstack import connection from openstack import exceptions @@ -721,7 +720,7 @@ def test_create_server_wait(self, mock_wait): networks='auto', imageRef='image-id', flavorRef='flavor-id', - **fake_server + **fake_server, ) mock_wait.assert_called_once_with( srv, @@ -1201,7 +1200,7 @@ def test_create_server_get_flavor_image(self): [ dict( method='GET', - uri='https://image.example.com/v2/images', + uri=f'https://image.example.com/v2/images/{image_id}', json=fake_image_search_return, ), self.get_nova_discovery_mock_dict(), @@ -1320,14 +1319,7 @@ def test_create_server_nics_port_id(self): def test_create_boot_attach_volume(self): build_server = fakes.make_fake_server('1234', '', 'BUILD') active_server = fakes.make_fake_server('1234', '', 'BUILD') - - vol = { - 'id': 'volume001', - 'status': 'available', - 'name': '', - 'attachments': [], - } - volume = meta.obj_to_munch(fakes.FakeVolume(**vol)) + volume_id = '20e82d93-14fa-475b-bfcc-f5e6246dd194' self.register_uris( [ @@ -1339,6 +1331,24 @@ def test_create_boot_attach_volume(self): json={'networks': []}, ), self.get_nova_discovery_mock_dict(), + self.get_cinder_discovery_mock_dict(), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['volumes', volume_id] + ), + json={ + 'volume': { + 'id': volume_id, + 'status': 'available', + 'size': 1, + 'availability_zone': 'cinder', + 'name': '', + 'description': None, + 'volume_type': 'lvmdriver-1', + } + }, + ), dict( method='POST', uri=self.get_mock_url( @@ -1365,7 +1375,7 @@ def test_create_boot_attach_volume(self): 'delete_on_termination': False, 'destination_type': 'volume', 'source_type': 'volume', - 'uuid': 'volume001', + 'uuid': volume_id, }, ], 'name': 'server-name', @@ -1389,7 +1399,7 @@ def test_create_boot_attach_volume(self): image=dict(id='image-id'), flavor=dict(id='flavor-id'), boot_from_volume=False, - volumes=[volume], + volumes=[volume_id], wait=False, ) @@ -1552,7 +1562,9 @@ def test_create_server_scheduler_hints_group_merge(self): dict( method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['os-server-groups'] + 'compute', + 'public', + append=['os-server-groups', group_id], ), json={'server_groups': [fake_group]}, ), @@ -1598,7 +1610,7 @@ def test_create_server_scheduler_hints_group_merge(self): image=dict(id='image-id'), flavor=dict(id='flavor-id'), scheduler_hints=dict(scheduler_hints), - group=group_name, + group=group_id, wait=False, ) @@ -1610,11 +1622,11 @@ def test_create_server_scheduler_hints_group_override(self): param """ group_id_scheduler_hints = uuid.uuid4().hex - group_id_param = uuid.uuid4().hex + group_id = uuid.uuid4().hex group_name = self.getUniqueString('server-group') policies = ['affinity'] fake_group = fakes.make_fake_server_group( - group_id_param, group_name, policies + group_id, group_name, policies ) # The scheduler hints we pass in that are expected to be ignored in @@ -1625,7 +1637,7 @@ def test_create_server_scheduler_hints_group_override(self): # The scheduler hints we expect to be in POST request group_scheduler_hints = { - 'group': group_id_param, + 'group': group_id, } fake_server = fakes.make_fake_server('1234', '', 'BUILD') @@ -1637,7 +1649,9 @@ def test_create_server_scheduler_hints_group_override(self): dict( method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['os-server-groups'] + 'compute', + 'public', + append=['os-server-groups', group_id], ), json={'server_groups': [fake_group]}, ), @@ -1683,7 +1697,7 @@ def test_create_server_scheduler_hints_group_override(self): image=dict(id='image-id'), flavor=dict(id='flavor-id'), scheduler_hints=dict(scheduler_hints), - group=group_name, + group=group_id, wait=False, ) diff --git a/openstack/tests/unit/cloud/test_floating_ip_neutron.py b/openstack/tests/unit/cloud/test_floating_ip_neutron.py index b3d668866..e23337581 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_neutron.py +++ b/openstack/tests/unit/cloud/test_floating_ip_neutron.py @@ -500,6 +500,7 @@ def test_neutron_available_floating_ips_invalid_network(self): self.assert_calls() def test_auto_ip_pool_no_reuse(self): + server_id = 'f80e3ad0-e13e-41d4-8e9c-be79bccdb8f7' # payloads taken from citycloud self.register_uris( [ @@ -540,8 +541,7 @@ def test_auto_ip_pool_no_reuse(self): ), dict( method='GET', - uri='https://network.example.com/v2.0/ports' - '?device_id=f80e3ad0-e13e-41d4-8e9c-be79bccdb8f7', + uri=f'https://network.example.com/v2.0/ports?device_id={server_id}', json={ "ports": [ { @@ -569,7 +569,7 @@ def test_auto_ip_pool_no_reuse(self): "security_groups": [ "9fb5ba44-5c46-4357-8e60-8b55526cab54" ], - "device_id": "f80e3ad0-e13e-41d4-8e9c-be79bccdb8f7", # noqa: E501 + "device_id": server_id, # noqa: E501 } ] }, @@ -605,141 +605,54 @@ def test_auto_ip_pool_no_reuse(self): self.get_nova_discovery_mock_dict(), dict( method='GET', - uri='{endpoint}/servers/detail'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), - json={ - "servers": [ - { - "status": "ACTIVE", - "updated": "2017-02-06T20:59:49Z", - "addresses": { - "private": [ - { - "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:e8:7f:03", # noqa: E501 - "version": 4, - "addr": "10.4.0.16", - "OS-EXT-IPS:type": "fixed", - }, - { - "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:e8:7f:03", # noqa: E501 - "version": 4, - "addr": "89.40.216.153", - "OS-EXT-IPS:type": "floating", - }, - ] - }, - "key_name": None, - "image": { - "id": "95e4c449-8abf-486e-97d9-dc3f82417d2d" # noqa: E501 - }, - "OS-EXT-STS:task_state": None, - "OS-EXT-STS:vm_state": "active", - "OS-SRV-USG:launched_at": "2017-02-06T20:59:48.000000", # noqa: E501 - "flavor": { - "id": "2186bd79-a05e-4953-9dde-ddefb63c88d4" # noqa: E501 - }, - "id": "f80e3ad0-e13e-41d4-8e9c-be79bccdb8f7", - "security_groups": [{"name": "default"}], - "OS-SRV-USG:terminated_at": None, - "OS-EXT-AZ:availability_zone": "nova", - "user_id": "c17534835f8f42bf98fc367e0bf35e09", - "name": "testmt", - "created": "2017-02-06T20:59:44Z", - "tenant_id": "65222a4d09ea4c68934fa1028c77f394", # noqa: E501 - "OS-DCF:diskConfig": "MANUAL", - "os-extended-volumes:volumes_attached": [], - "accessIPv4": "", - "accessIPv6": "", - "progress": 0, - "OS-EXT-STS:power_state": 1, - "config_drive": "", - "metadata": {}, - } - ] - }, - ), - dict( - method='GET', - uri='https://network.example.com/v2.0/networks', + uri=f'https://compute.example.com/v2.1/servers/{server_id}', json={ - "networks": [ - { - "status": "ACTIVE", - "subnets": [ - "df3e17fa-a4b2-47ae-9015-bc93eb076ba2", - "6b0c3dc9-b0b8-4d87-976a-7f2ebf13e7ec", - "fc541f48-fc7f-48c0-a063-18de6ee7bdd7", - ], - "availability_zone_hints": [], - "availability_zones": ["nova"], - "name": "ext-net", - "admin_state_up": True, - "tenant_id": "a564613210ee43708b8a7fc6274ebd63", # noqa: E501 - "tags": [], - "ipv6_address_scope": "9f03124f-89af-483a-b6fd-10f08079db4d", # noqa: E501 - "mtu": 0, - "is_default": False, - "router:external": True, - "ipv4_address_scope": None, - "shared": False, - "id": "0232c17f-2096-49bc-b205-d3dcd9a30ebf", - "description": None, + "server": { + "status": "ACTIVE", + "updated": "2017-02-06T20:59:49Z", + "addresses": { + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:e8:7f:03", # noqa: E501 + "version": 4, + "addr": "10.4.0.16", + "OS-EXT-IPS:type": "fixed", + }, + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:e8:7f:03", # noqa: E501 + "version": 4, + "addr": "89.40.216.153", + "OS-EXT-IPS:type": "floating", + }, + ] }, - { - "status": "ACTIVE", - "subnets": [ - "f0ad1df5-53ee-473f-b86b-3604ea5591e9" - ], - "availability_zone_hints": [], - "availability_zones": ["nova"], - "name": "private", - "admin_state_up": True, - "tenant_id": "65222a4d09ea4c68934fa1028c77f394", # noqa: E501 - "created_at": "2016-10-22T13:46:26", - "tags": [], - "updated_at": "2016-10-22T13:46:26", - "ipv6_address_scope": None, - "router:external": False, - "ipv4_address_scope": None, - "shared": False, - "mtu": 1450, - "id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", - "description": "", + "key_name": None, + "image": { + "id": "95e4c449-8abf-486e-97d9-dc3f82417d2d" # noqa: E501 }, - ] - }, - ), - dict( - method='GET', - uri='https://network.example.com/v2.0/subnets', - json={ - "subnets": [ - { - "description": "", - "enable_dhcp": True, - "network_id": "2c9adcb5-c123-4c5a-a2ba-1ad4c4e1481f", # noqa: E501 - "tenant_id": "65222a4d09ea4c68934fa1028c77f394", # noqa: E501 - "created_at": "2016-10-22T13:46:26", - "dns_nameservers": [ - "89.36.90.101", - "89.36.90.102", - ], - "updated_at": "2016-10-22T13:46:26", - "gateway_ip": "10.4.0.1", - "ipv6_ra_mode": None, - "allocation_pools": [ - {"start": "10.4.0.2", "end": "10.4.0.200"} - ], - "host_routes": [], - "ip_version": 4, - "ipv6_address_mode": None, - "cidr": "10.4.0.0/24", - "id": "f0ad1df5-53ee-473f-b86b-3604ea5591e9", - "subnetpool_id": None, - "name": "private-subnet-ipv4", - } - ] + "OS-EXT-STS:task_state": None, + "OS-EXT-STS:vm_state": "active", + "OS-SRV-USG:launched_at": "2017-02-06T20:59:48.000000", # noqa: E501 + "flavor": { + "id": "2186bd79-a05e-4953-9dde-ddefb63c88d4" # noqa: E501 + }, + "id": server_id, + "security_groups": [{"name": "default"}], + "OS-SRV-USG:terminated_at": None, + "OS-EXT-AZ:availability_zone": "nova", + "user_id": "c17534835f8f42bf98fc367e0bf35e09", + "name": "testmt", + "created": "2017-02-06T20:59:44Z", + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", # noqa: E501 + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": {}, + } }, ), ] @@ -747,7 +660,7 @@ def test_auto_ip_pool_no_reuse(self): self.cloud.add_ips_to_server( utils.Munch( - id='f80e3ad0-e13e-41d4-8e9c-be79bccdb8f7', + id=server_id, addresses={ "private": [ { diff --git a/openstack/tests/unit/cloud/test_limits.py b/openstack/tests/unit/cloud/test_limits.py index 8481ca50d..ed67290fd 100644 --- a/openstack/tests/unit/cloud/test_limits.py +++ b/openstack/tests/unit/cloud/test_limits.py @@ -58,7 +58,7 @@ def test_get_compute_limits(self): def test_other_get_compute_limits(self): project = self.mock_for_keystone_projects( - project_count=1, list_get=True + project_count=1, id_get=True )[0] self.register_uris( [ diff --git a/openstack/tests/unit/cloud/test_quotas.py b/openstack/tests/unit/cloud/test_quotas.py index 53b32d2bb..35d61fb18 100644 --- a/openstack/tests/unit/cloud/test_quotas.py +++ b/openstack/tests/unit/cloud/test_quotas.py @@ -258,7 +258,7 @@ def test_cinder_delete_quotas(self): def test_neutron_update_quotas(self): project = self.mock_for_keystone_projects( - project_count=1, list_get=True + project_count=1, id_get=True )[0] self.register_uris( [ @@ -362,7 +362,7 @@ def test_neutron_get_quotas_details(self): def test_neutron_delete_quotas(self): project = self.mock_for_keystone_projects( - project_count=1, list_get=True + project_count=1, id_get=True )[0] self.register_uris( [ diff --git a/openstack/tests/unit/cloud/test_usage.py b/openstack/tests/unit/cloud/test_usage.py index 1c13f7ad8..e5c34e5d7 100644 --- a/openstack/tests/unit/cloud/test_usage.py +++ b/openstack/tests/unit/cloud/test_usage.py @@ -20,7 +20,7 @@ class TestUsage(base.TestCase): def test_get_usage(self): project = self.mock_for_keystone_projects( - project_count=1, list_get=True + project_count=1, id_get=True )[0] start = end = datetime.datetime.now() From 113e41250c2e115e178e5d5179ec5d86ce8cf749 Mon Sep 17 00:00:00 2001 From: Emilien Lefrancois Date: Thu, 8 Feb 2024 11:01:17 +0100 Subject: [PATCH 3483/3836] Fix AttributeError in delete_limit method This commit resolves an issue in the delete_limit method of the Identity V3 Proxy class. The method was incorrectly trying to access a 'Limit' attribute of a 'Limit' object, causing an AttributeError. The fix involves correctly passing the 'Limit' class from the '_limit' module as the first argument to the '_delete' method. Closes-Bug: #2052650 Change-Id: I435f52f8085c899fe1be57fcd5a2ce6f04618427 --- openstack/identity/v3/_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 316dfe266..50cfe6b0f 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -1372,7 +1372,7 @@ def delete_limit(self, limit, ignore_missing=True): :returns: ``None`` """ - self._delete(limit.Limit, limit, ignore_missing=ignore_missing) + self._delete(_limit.Limit, limit, ignore_missing=ignore_missing) # ========== Roles ========== From 93e2976c3239d284499658a8cd70842720fd4d1d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 26 Apr 2024 13:11:31 +0100 Subject: [PATCH 3484/3836] tox: Don't install package in pep8 It's a waste of time: pre-commit doesn't need it. (I tested and our own hacking hooks continue to work) Change-Id: I4fe6bedc9ddf1f83bc9ddf037a9eae76dfde6f83 Signed-off-by: Stephen Finucane --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 693535a1c..445f894d6 100644 --- a/tox.ini +++ b/tox.ini @@ -60,6 +60,7 @@ commands = [testenv:pep8] description = Run style checks. +skip_install = true deps = pre-commit commands = From 8b02b04572258a88e49aef70a54689da2cd1df66 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 23 Apr 2024 12:18:06 +0100 Subject: [PATCH 3485/3836] trivial: Prepare for pyupgrade pre-commit hook This is kept separate from addition of the actual hook so that we can ignore the commit later. Change-Id: I3af752894490d619b3ef755aca5e717edafe104c Signed-off-by: Stephen Finucane --- examples/connect.py | 2 +- .../accelerator/v2/accelerator_request.py | 8 +- openstack/accelerator/v2/device_profile.py | 8 +- openstack/baremetal/v1/driver.py | 2 +- openstack/baremetal/v1/node.py | 24 +- .../v1/introspection.py | 2 +- openstack/block_storage/_base_proxy.py | 2 +- openstack/block_storage/v3/service.py | 2 +- openstack/block_storage/v3/type.py | 2 +- openstack/cloud/_compute.py | 14 +- openstack/cloud/_dns.py | 4 +- openstack/cloud/_floating_ip.py | 36 +-- openstack/cloud/_identity.py | 30 +- openstack/cloud/_image.py | 2 +- openstack/cloud/_network.py | 4 +- openstack/cloud/_object_store.py | 5 +- openstack/cloud/_security_group.py | 4 +- openstack/cloud/_utils.py | 4 +- openstack/cloud/exc.py | 2 +- openstack/cloud/meta.py | 12 +- openstack/cloud/openstackcloud.py | 12 +- openstack/common/quota_set.py | 2 +- openstack/compute/v2/flavor.py | 2 +- openstack/compute/v2/keypair.py | 2 +- openstack/compute/v2/limits.py | 2 +- openstack/compute/v2/server.py | 2 +- openstack/compute/v2/server_remote_console.py | 2 +- openstack/compute/v2/service.py | 4 +- openstack/config/cloud_config.py | 2 +- openstack/config/cloud_region.py | 6 +- openstack/config/defaults.py | 2 +- openstack/config/loader.py | 26 +- openstack/config/vendors/__init__.py | 4 +- openstack/dns/v2/_base.py | 2 +- openstack/exceptions.py | 14 +- openstack/fixture/connection.py | 2 +- openstack/image/_download.py | 2 +- openstack/image/v1/_proxy.py | 10 +- openstack/image/v1/image.py | 2 +- openstack/image/v2/_proxy.py | 8 +- openstack/image/v2/image.py | 6 +- openstack/load_balancer/v2/amphora.py | 8 +- openstack/load_balancer/v2/load_balancer.py | 4 +- openstack/load_balancer/v2/quota.py | 2 +- openstack/message/v2/claim.py | 2 +- openstack/network/v2/_base.py | 2 +- openstack/network/v2/quota.py | 4 +- openstack/network/v2/security_group_rule.py | 4 +- openstack/object_store/v1/_base.py | 2 +- openstack/object_store/v1/_proxy.py | 14 +- openstack/orchestration/util/event_utils.py | 6 +- .../orchestration/util/template_format.py | 4 +- openstack/orchestration/v1/_proxy.py | 2 +- openstack/orchestration/v1/software_config.py | 4 +- .../orchestration/v1/software_deployment.py | 8 +- openstack/orchestration/v1/stack.py | 24 +- openstack/proxy.py | 10 +- openstack/resource.py | 20 +- openstack/shared_file_system/v2/_proxy.py | 12 +- openstack/test/fakes.py | 4 +- openstack/tests/base.py | 8 +- openstack/tests/fakes.py | 90 +++--- openstack/tests/functional/baremetal/base.py | 2 +- .../baremetal/test_baremetal_allocation.py | 2 +- .../test_baremetal_deploy_templates.py | 2 +- .../baremetal/test_baremetal_node.py | 4 +- .../baremetal/test_baremetal_port.py | 2 +- .../baremetal/test_baremetal_port_group.py | 2 +- .../test_baremetal_volume_connector.py | 2 +- .../baremetal/test_baremetal_volume_target.py | 2 +- openstack/tests/functional/base.py | 4 +- .../tests/functional/block_storage/v2/base.py | 2 +- .../block_storage/v2/test_backup.py | 4 +- .../block_storage/v2/test_snapshot.py | 4 +- .../functional/block_storage/v2/test_stats.py | 2 +- .../functional/block_storage/v2/test_type.py | 4 +- .../block_storage/v2/test_volume.py | 4 +- .../tests/functional/block_storage/v3/base.py | 2 +- .../block_storage/v3/test_backup.py | 4 +- .../block_storage/v3/test_snapshot.py | 4 +- .../functional/block_storage/v3/test_type.py | 4 +- .../cloud/test_cluster_templates.py | 2 +- .../tests/functional/cloud/test_clustering.py | 2 +- .../tests/functional/cloud/test_compute.py | 4 +- .../tests/functional/cloud/test_devstack.py | 5 +- .../tests/functional/cloud/test_domain.py | 2 +- .../tests/functional/cloud/test_endpoints.py | 2 +- .../tests/functional/cloud/test_flavor.py | 2 +- .../functional/cloud/test_floating_ip_pool.py | 2 +- .../tests/functional/cloud/test_groups.py | 2 +- .../tests/functional/cloud/test_identity.py | 2 +- .../functional/cloud/test_magnum_services.py | 2 +- .../tests/functional/cloud/test_network.py | 2 +- .../tests/functional/cloud/test_object.py | 2 +- openstack/tests/functional/cloud/test_port.py | 10 +- .../tests/functional/cloud/test_project.py | 2 +- .../functional/cloud/test_project_cleanup.py | 2 +- .../cloud/test_qos_bandwidth_limit_rule.py | 2 +- .../cloud/test_qos_dscp_marking_rule.py | 2 +- .../cloud/test_qos_minimum_bandwidth_rule.py | 2 +- .../tests/functional/cloud/test_qos_policy.py | 2 +- .../tests/functional/cloud/test_quotas.py | 2 +- .../tests/functional/cloud/test_recordset.py | 2 +- .../tests/functional/cloud/test_router.py | 16 +- .../tests/functional/cloud/test_services.py | 2 +- .../tests/functional/cloud/test_stack.py | 2 +- .../tests/functional/cloud/test_users.py | 2 +- .../tests/functional/cloud/test_volume.py | 2 +- .../functional/cloud/test_volume_backup.py | 2 +- .../functional/cloud/test_volume_type.py | 10 +- openstack/tests/functional/cloud/test_zone.py | 2 +- .../functional/clustering/test_cluster.py | 4 +- .../functional/compute/v2/test_flavor.py | 2 +- .../functional/compute/v2/test_hypervisor.py | 2 +- .../functional/compute/v2/test_keypair.py | 8 +- .../functional/compute/v2/test_server.py | 8 +- .../functional/compute/v2/test_service.py | 2 +- .../tests/functional/dns/v2/test_zone.py | 8 +- .../functional/dns/v2/test_zone_share.py | 4 +- .../tests/functional/examples/test_compute.py | 2 +- .../functional/examples/test_identity.py | 2 +- .../tests/functional/examples/test_image.py | 2 +- .../tests/functional/examples/test_network.py | 2 +- .../v3/test_application_credential.py | 2 +- .../tests/functional/image/v2/test_image.py | 2 +- .../tests/functional/instance_ha/test_host.py | 2 +- .../functional/instance_ha/test_segment.py | 2 +- .../load_balancer/v2/test_load_balancer.py | 4 +- .../network/v2/test_address_group.py | 4 +- .../network/v2/test_address_scope.py | 4 +- .../tests/functional/network/v2/test_agent.py | 2 +- .../v2/test_agent_add_remove_network.py | 2 +- .../v2/test_agent_add_remove_router.py | 2 +- .../v2/test_auto_allocated_topology.py | 4 +- .../v2/test_default_security_group_rule.py | 2 +- .../functional/network/v2/test_dvr_router.py | 4 +- .../network/v2/test_firewall_group.py | 4 +- .../network/v2/test_firewall_policy.py | 4 +- .../network/v2/test_firewall_rule.py | 4 +- ...test_firewall_rule_insert_remove_policy.py | 4 +- .../functional/network/v2/test_flavor.py | 4 +- .../functional/network/v2/test_floating_ip.py | 4 +- .../network/v2/test_l3_conntrack_helper.py | 4 +- .../functional/network/v2/test_local_ip.py | 4 +- .../network/v2/test_local_ip_association.py | 4 +- .../functional/network/v2/test_ndp_proxy.py | 4 +- .../functional/network/v2/test_network.py | 4 +- .../v2/test_network_ip_availability.py | 4 +- .../network/v2/test_network_segment_range.py | 4 +- .../tests/functional/network/v2/test_port.py | 4 +- .../network/v2/test_port_forwarding.py | 4 +- .../v2/test_qos_bandwidth_limit_rule.py | 4 +- .../network/v2/test_qos_dscp_marking_rule.py | 4 +- .../v2/test_qos_minimum_bandwidth_rule.py | 4 +- .../v2/test_qos_minimum_packet_rate_rule.py | 4 +- .../functional/network/v2/test_qos_policy.py | 4 +- .../network/v2/test_qos_rule_type.py | 2 +- .../tests/functional/network/v2/test_quota.py | 2 +- .../functional/network/v2/test_rbac_policy.py | 4 +- .../functional/network/v2/test_router.py | 4 +- .../v2/test_router_add_remove_interface.py | 4 +- .../network/v2/test_security_group.py | 4 +- .../network/v2/test_security_group_rule.py | 4 +- .../functional/network/v2/test_segment.py | 4 +- .../network/v2/test_service_profile.py | 4 +- .../functional/network/v2/test_subnet.py | 4 +- .../v2/test_subnet_from_subnet_pool.py | 4 +- .../functional/network/v2/test_subnet_pool.py | 4 +- .../tests/functional/network/v2/test_trunk.py | 4 +- .../functional/network/v2/test_vpnaas.py | 4 +- .../object_store/v1/test_account.py | 4 +- .../object_store/v1/test_container.py | 2 +- .../functional/object_store/v1/test_obj.py | 2 +- .../functional/orchestration/v1/test_stack.py | 4 +- .../functional/shared_file_system/base.py | 2 +- .../shared_file_system/test_resource_lock.py | 2 +- .../shared_file_system/test_share.py | 4 +- .../test_share_access_rule.py | 4 +- .../shared_file_system/test_share_group.py | 2 +- .../test_share_group_snapshot.py | 4 +- .../shared_file_system/test_share_instance.py | 2 +- .../shared_file_system/test_share_network.py | 4 +- .../shared_file_system/test_share_snapshot.py | 4 +- .../test_share_snapshot_instance.py | 2 +- .../accelerator/v2/test_device_profile.py | 4 +- .../tests/unit/accelerator/v2/test_proxy.py | 2 +- .../tests/unit/baremetal/test_configdrive.py | 2 +- .../unit/baremetal/v1/test_allocation.py | 2 +- .../tests/unit/baremetal/v1/test_node.py | 40 +-- .../tests/unit/baremetal/v1/test_proxy.py | 4 +- .../baremetal_introspection/v1/test_proxy.py | 10 +- openstack/tests/unit/base.py | 40 +-- .../unit/block_storage/v2/test_backup.py | 2 +- .../unit/block_storage/v2/test_snapshot.py | 2 +- .../tests/unit/block_storage/v2/test_stats.py | 2 +- .../tests/unit/block_storage/v2/test_type.py | 2 +- .../unit/block_storage/v2/test_volume.py | 6 +- .../unit/block_storage/v3/test_attachment.py | 2 +- .../unit/block_storage/v3/test_backup.py | 2 +- .../tests/unit/block_storage/v3/test_proxy.py | 2 +- .../unit/block_storage/v3/test_snapshot.py | 2 +- .../tests/unit/block_storage/v3/test_type.py | 2 +- .../unit/block_storage/v3/test_volume.py | 6 +- openstack/tests/unit/cloud/test__utils.py | 8 +- .../tests/unit/cloud/test_accelerator.py | 2 +- openstack/tests/unit/cloud/test_aggregate.py | 2 +- .../tests/unit/cloud/test_baremetal_node.py | 4 +- .../tests/unit/cloud/test_baremetal_ports.py | 2 +- .../unit/cloud/test_cluster_templates.py | 2 +- openstack/tests/unit/cloud/test_clustering.py | 2 +- .../tests/unit/cloud/test_coe_clusters.py | 2 +- .../cloud/test_coe_clusters_certificate.py | 2 +- openstack/tests/unit/cloud/test_compute.py | 2 +- .../unit/cloud/test_create_volume_snapshot.py | 2 +- .../unit/cloud/test_delete_volume_snapshot.py | 2 +- openstack/tests/unit/cloud/test_domains.py | 2 +- openstack/tests/unit/cloud/test_endpoints.py | 2 +- openstack/tests/unit/cloud/test_flavors.py | 2 +- .../unit/cloud/test_floating_ip_common.py | 4 +- .../unit/cloud/test_floating_ip_neutron.py | 32 +- .../tests/unit/cloud/test_floating_ip_nova.py | 12 +- openstack/tests/unit/cloud/test_fwaas.py | 6 +- openstack/tests/unit/cloud/test_groups.py | 6 +- .../tests/unit/cloud/test_identity_roles.py | 4 +- .../tests/unit/cloud/test_identity_users.py | 2 +- openstack/tests/unit/cloud/test_image.py | 18 +- .../tests/unit/cloud/test_image_snapshot.py | 2 +- openstack/tests/unit/cloud/test_inventory.py | 2 +- openstack/tests/unit/cloud/test_keypair.py | 2 +- openstack/tests/unit/cloud/test_limits.py | 4 +- openstack/tests/unit/cloud/test_meta.py | 276 +++++++++--------- openstack/tests/unit/cloud/test_network.py | 2 +- openstack/tests/unit/cloud/test_object.py | 36 +-- .../tests/unit/cloud/test_openstackcloud.py | 2 +- .../tests/unit/cloud/test_operator_noauth.py | 4 +- openstack/tests/unit/cloud/test_project.py | 2 +- .../tests/unit/cloud/test_qos_rule_type.py | 4 +- openstack/tests/unit/cloud/test_quotas.py | 4 +- .../tests/unit/cloud/test_rebuild_server.py | 4 +- openstack/tests/unit/cloud/test_recordset.py | 2 +- .../tests/unit/cloud/test_role_assignment.py | 8 +- openstack/tests/unit/cloud/test_router.py | 8 +- .../tests/unit/cloud/test_security_groups.py | 2 +- .../tests/unit/cloud/test_server_console.py | 2 +- .../unit/cloud/test_server_delete_metadata.py | 2 +- .../tests/unit/cloud/test_server_group.py | 2 +- .../unit/cloud/test_server_set_metadata.py | 2 +- openstack/tests/unit/cloud/test_services.py | 4 +- .../unit/cloud/test_shared_file_system.py | 2 +- openstack/tests/unit/cloud/test_stack.py | 2 +- .../tests/unit/cloud/test_update_server.py | 2 +- openstack/tests/unit/cloud/test_usage.py | 6 +- .../tests/unit/cloud/test_volume_access.py | 4 +- .../tests/unit/cloud/test_volume_backups.py | 2 +- openstack/tests/unit/cloud/test_zone.py | 2 +- .../tests/unit/clustering/v1/test_action.py | 2 +- .../unit/clustering/v1/test_build_info.py | 2 +- .../tests/unit/clustering/v1/test_cluster.py | 2 +- .../unit/clustering/v1/test_cluster_attr.py | 2 +- .../unit/clustering/v1/test_cluster_policy.py | 2 +- .../tests/unit/clustering/v1/test_event.py | 2 +- .../tests/unit/clustering/v1/test_policy.py | 4 +- .../tests/unit/clustering/v1/test_profile.py | 4 +- .../tests/unit/clustering/v1/test_proxy.py | 2 +- .../tests/unit/clustering/v1/test_receiver.py | 2 +- .../tests/unit/clustering/v1/test_service.py | 2 +- openstack/tests/unit/common/test_metadata.py | 2 +- openstack/tests/unit/common/test_quota_set.py | 2 +- openstack/tests/unit/common/test_tag.py | 2 +- .../tests/unit/compute/v2/test_aggregate.py | 2 +- .../tests/unit/compute/v2/test_flavor.py | 2 +- .../tests/unit/compute/v2/test_hypervisor.py | 4 +- openstack/tests/unit/compute/v2/test_proxy.py | 2 +- .../tests/unit/compute/v2/test_server.py | 2 +- .../unit/compute/v2/test_server_migration.py | 2 +- .../compute/v2/test_server_remote_console.py | 2 +- .../tests/unit/compute/v2/test_service.py | 2 +- openstack/tests/unit/config/base.py | 4 +- openstack/tests/unit/config/test_config.py | 8 +- openstack/tests/unit/config/test_environ.py | 2 +- openstack/tests/unit/config/test_json.py | 8 +- openstack/tests/unit/config/test_loader.py | 4 +- .../tests/unit/database/v1/test_proxy.py | 2 +- openstack/tests/unit/dns/v2/test_proxy.py | 2 +- openstack/tests/unit/dns/v2/test_zone.py | 2 +- .../tests/unit/dns/v2/test_zone_share.py | 2 +- openstack/tests/unit/fakes.py | 4 +- .../tests/unit/identity/v2/test_proxy.py | 2 +- .../tests/unit/identity/v3/test_domain.py | 2 +- .../tests/unit/identity/v3/test_group.py | 2 +- .../tests/unit/identity/v3/test_project.py | 2 +- .../tests/unit/identity/v3/test_proxy.py | 2 +- openstack/tests/unit/image/v1/test_proxy.py | 2 +- openstack/tests/unit/image/v2/test_image.py | 2 +- openstack/tests/unit/image/v2/test_proxy.py | 2 +- .../tests/unit/instance_ha/v1/test_proxy.py | 2 +- .../tests/unit/key_manager/v1/test_proxy.py | 2 +- .../unit/load_balancer/test_load_balancer.py | 4 +- .../tests/unit/load_balancer/v2/test_proxy.py | 2 +- openstack/tests/unit/message/v2/test_claim.py | 54 ++-- .../tests/unit/message/v2/test_message.py | 46 +-- openstack/tests/unit/message/v2/test_proxy.py | 2 +- .../unit/message/v2/test_subscription.py | 44 +-- .../tests/unit/network/v2/test_bgp_speaker.py | 2 +- openstack/tests/unit/network/v2/test_proxy.py | 2 +- .../unit/object_store/v1/test_account.py | 2 +- .../unit/object_store/v1/test_container.py | 2 +- .../tests/unit/object_store/v1/test_info.py | 2 +- .../tests/unit/object_store/v1/test_obj.py | 2 +- .../tests/unit/object_store/v1/test_proxy.py | 40 +-- .../tests/unit/orchestration/v1/test_proxy.py | 2 +- .../tests/unit/orchestration/v1/test_stack.py | 18 +- .../unit/orchestration/v1/test_stack_event.py | 2 +- .../unit/orchestration/v1/test_stack_files.py | 8 +- .../unit/shared_file_system/v2/test_proxy.py | 18 +- .../v2/test_share_instance.py | 2 +- openstack/tests/unit/test_connection.py | 10 +- openstack/tests/unit/test_exceptions.py | 6 +- openstack/tests/unit/test_microversions.py | 2 +- openstack/tests/unit/test_missing_version.py | 2 +- openstack/tests/unit/test_placement_rest.py | 4 +- openstack/tests/unit/test_proxy.py | 22 +- openstack/tests/unit/test_proxy_base.py | 2 +- openstack/tests/unit/test_resource.py | 28 +- openstack/tests/unit/test_stats.py | 4 +- openstack/tests/unit/test_utils.py | 30 +- .../tests/unit/workflow/test_execution.py | 2 +- .../tests/unit/workflow/test_workflow.py | 2 +- .../tests/unit/workflow/v2/test_proxy.py | 2 +- openstack/workflow/v2/cron_trigger.py | 4 +- releasenotes/source/conf.py | 1 - tools/keystone_version.py | 8 +- tools/print-services.py | 10 +- 333 files changed, 990 insertions(+), 1087 deletions(-) diff --git a/examples/connect.py b/examples/connect.py index 50de0fde2..4446fe729 100644 --- a/examples/connect.py +++ b/examples/connect.py @@ -60,7 +60,7 @@ def _get_resource_value(resource_key, default): ) PRIVATE_KEYPAIR_FILE = _get_resource_value( 'private_keypair_file', - '{ssh_dir}/id_rsa.{key}'.format(ssh_dir=SSH_DIR, key=KEYPAIR_NAME), + f'{SSH_DIR}/id_rsa.{KEYPAIR_NAME}', ) EXAMPLE_IMAGE_NAME = 'openstacksdk-example-public-image' diff --git a/openstack/accelerator/v2/accelerator_request.py b/openstack/accelerator/v2/accelerator_request.py index 543f7234a..71f6b099b 100644 --- a/openstack/accelerator/v2/accelerator_request.py +++ b/openstack/accelerator/v2/accelerator_request.py @@ -52,7 +52,7 @@ def _convert_patch(self, patch): # and its value is an ordinary JSON patch. spec: # https://specs.openstack.org/openstack/cyborg-specs/specs/train/implemented/cyborg-api - converted = super(AcceleratorRequest, self)._convert_patch(patch) + converted = super()._convert_patch(patch) converted = {self.id: converted} return converted @@ -102,11 +102,9 @@ def _consume_attrs(self, mapping, attrs): if isinstance(self, AcceleratorRequest): if self.resources_key in attrs: attrs = attrs[self.resources_key][0] - return super(AcceleratorRequest, self)._consume_attrs(mapping, attrs) + return super()._consume_attrs(mapping, attrs) def create(self, session, base_path=None): # This overrides the default behavior of resource creation because # cyborg doesn't accept resource_key in its request. - return super(AcceleratorRequest, self).create( - session, prepend_key=False, base_path=base_path - ) + return super().create(session, prepend_key=False, base_path=base_path) diff --git a/openstack/accelerator/v2/device_profile.py b/openstack/accelerator/v2/device_profile.py index 15034d395..67d289564 100644 --- a/openstack/accelerator/v2/device_profile.py +++ b/openstack/accelerator/v2/device_profile.py @@ -39,14 +39,10 @@ class DeviceProfile(resource.Resource): # TODO(s_shogo): This implementation only treat [ DeviceProfile ], and # cannot treat multiple DeviceProfiles in list. def _prepare_request_body(self, patch, prepend_key): - body = super(DeviceProfile, self)._prepare_request_body( - patch, prepend_key - ) + body = super()._prepare_request_body(patch, prepend_key) return [body] def create(self, session, base_path=None): # This overrides the default behavior of resource creation because # cyborg doesn't accept resource_key in its request. - return super(DeviceProfile, self).create( - session, prepend_key=False, base_path=base_path - ) + return super().create(session, prepend_key=False, base_path=base_path) diff --git a/openstack/baremetal/v1/driver.py b/openstack/baremetal/v1/driver.py index 4f457f9e7..cea8f73de 100644 --- a/openstack/baremetal/v1/driver.py +++ b/openstack/baremetal/v1/driver.py @@ -173,7 +173,7 @@ def call_vendor_passthru( :returns: response of method call. """ if verb.upper() not in ['GET', 'PUT', 'POST', 'DELETE']: - raise ValueError('Invalid verb: {}'.format(verb)) + raise ValueError(f'Invalid verb: {verb}') session = self._get_session(session) request = self._prepare_request() diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 3f7bf9914..052358400 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -275,7 +275,7 @@ def _consume_body_attrs(self, attrs): # API version 1.1 uses None instead of "available". Make it # consistent. attrs['provision_state'] = 'available' - return super(Node, self)._consume_body_attrs(attrs) + return super()._consume_body_attrs(attrs) def create(self, session, *args, **kwargs): """Create a remote resource based on this instance. @@ -346,9 +346,7 @@ def create(self, session, *args, **kwargs): # Ironic cannot set provision_state itself, so marking it as unchanged self._clean_body_attrs({'provision_state'}) - super(Node, self).create( - session, *args, microversion=microversion, **kwargs - ) + super().create(session, *args, microversion=microversion, **kwargs) if ( expected_provision_state == 'manageable' @@ -395,7 +393,7 @@ def commit(self, session, *args, **kwargs): # the new status. return self.fetch(session) - return super(Node, self).commit(session, *args, **kwargs) + return super().commit(session, *args, **kwargs) def set_provision_state( self, @@ -724,7 +722,7 @@ def inject_nmi(self, session): retriable_status_codes=_common.RETRIABLE_STATUS_CODES, ) - msg = "Failed to inject NMI to node {node}".format(node=self.id) + msg = f"Failed to inject NMI to node {self.id}" exceptions.raise_from_response(response, error_message=msg) def set_power_state(self, session, target, wait=False, timeout=None): @@ -934,13 +932,13 @@ def validate(self, session, required=('boot', 'deploy', 'power')): request.url, headers=request.headers, microversion=version ) - msg = "Failed to validate node {node}".format(node=self.id) + msg = f"Failed to validate node {self.id}" exceptions.raise_from_response(response, error_message=msg) result = response.json() if required: failed = [ - '%s (%s)' % (key, value.get('reason', 'no reason')) + '{} ({})'.format(key, value.get('reason', 'no reason')) for key, value in result.items() if key in required and not value.get('result') ] @@ -1044,7 +1042,7 @@ def set_boot_device(self, session, boot_device, persistent=False): retriable_status_codes=_common.RETRIABLE_STATUS_CODES, ) - msg = "Failed to set boot device for node {node}".format(node=self.id) + msg = f"Failed to set boot device for node {self.id}" exceptions.raise_from_response(response, error_message=msg) def get_supported_boot_devices(self, session): @@ -1109,7 +1107,7 @@ def set_boot_mode(self, session, target): retriable_status_codes=_common.RETRIABLE_STATUS_CODES, ) - msg = "Failed to change boot mode for node {node}".format(node=self.id) + msg = f"Failed to change boot mode for node {self.id}" exceptions.raise_from_response(response, error_message=msg) def set_secure_boot(self, session, target): @@ -1243,7 +1241,7 @@ def set_traits(self, session, traits): retriable_status_codes=_common.RETRIABLE_STATUS_CODES, ) - msg = "Failed to set traits for node {node}".format(node=self.id) + msg = f"Failed to set traits for node {self.id}" exceptions.raise_from_response(response, error_message=msg) self.traits = traits @@ -1261,7 +1259,7 @@ def call_vendor_passthru(self, session, verb, method, body=None): version = self._get_microversion(session, action='commit') request = self._prepare_request(requires_id=True) request.url = utils.urljoin( - request.url, 'vendor_passthru?method={}'.format(method) + request.url, f'vendor_passthru?method={method}' ) call = getattr(session, verb.lower()) @@ -1439,7 +1437,7 @@ def patch( ) else: - return super(Node, self).patch( + return super().patch( session, patch=patch, retry_on_conflict=retry_on_conflict ) diff --git a/openstack/baremetal_introspection/v1/introspection.py b/openstack/baremetal_introspection/v1/introspection.py index 906b257b2..db9eda0c5 100644 --- a/openstack/baremetal_introspection/v1/introspection.py +++ b/openstack/baremetal_introspection/v1/introspection.py @@ -71,7 +71,7 @@ def abort(self, session): microversion=version, retriable_status_codes=_common.RETRIABLE_STATUS_CODES, ) - msg = "Failed to abort introspection for node {id}".format(id=self.id) + msg = f"Failed to abort introspection for node {self.id}" exceptions.raise_from_response(response, error_message=msg) def get_data(self, session, processed=True): diff --git a/openstack/block_storage/_base_proxy.py b/openstack/block_storage/_base_proxy.py index 43df38161..cba07da93 100644 --- a/openstack/block_storage/_base_proxy.py +++ b/openstack/block_storage/_base_proxy.py @@ -43,7 +43,7 @@ def create_image( ) volume_id = volume_obj['id'] data = self.post( - '/volumes/{id}/action'.format(id=volume_id), + f'/volumes/{volume_id}/action', json={ 'os-volume_upload_image': { 'force': allow_duplicates, diff --git a/openstack/block_storage/v3/service.py b/openstack/block_storage/v3/service.py index fe78f1f72..a0c229da7 100644 --- a/openstack/block_storage/v3/service.py +++ b/openstack/block_storage/v3/service.py @@ -87,7 +87,7 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): if ignore_missing: return None raise exceptions.ResourceNotFound( - "No %s found for %s" % (cls.__name__, name_or_id) + f"No {cls.__name__} found for {name_or_id}" ) def commit(self, session, prepend_key=False, **kwargs): diff --git a/openstack/block_storage/v3/type.py b/openstack/block_storage/v3/type.py index 565e555b1..488fd4a97 100644 --- a/openstack/block_storage/v3/type.py +++ b/openstack/block_storage/v3/type.py @@ -51,7 +51,7 @@ def _extra_specs(self, method, key=None, delete=False, extra_specs=None): for k, v in extra_specs.items(): if not isinstance(v, str): raise ValueError( - "The value for %s (%s) must be a text string" % (k, v) + f"The value for {k} ({v}) must be a text string" ) if key is not None: diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 1e0906b10..7848e46ab 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -117,7 +117,7 @@ def get_flavor_by_ram(self, ram, include=None, get_extra=True): ) def _nova_extensions(self): - extensions = set([e.alias for e in self.compute.extensions()]) + extensions = {e.alias for e in self.compute.extensions()} return extensions def _has_nova_extension(self, extension_name): @@ -1229,7 +1229,7 @@ def get_active_server( raise exceptions.SDKException( 'Server reached ACTIVE state without being' ' allocated an IP address AND then could not' - ' be deleted: {0}'.format(e), + ' be deleted: {}'.format(e), extra_data=dict(server=server), ) raise exceptions.SDKException( @@ -1291,9 +1291,7 @@ def set_server_metadata(self, name_or_id, metadata): """ server = self.get_server(name_or_id, bare=True) if not server: - raise exceptions.SDKException( - 'Invalid Server {server}'.format(server=name_or_id) - ) + raise exceptions.SDKException(f'Invalid Server {name_or_id}') self.compute.set_server_metadata(server=server.id, **metadata) @@ -1311,9 +1309,7 @@ def delete_server_metadata(self, name_or_id, metadata_keys): """ server = self.get_server(name_or_id, bare=True) if not server: - raise exceptions.SDKException( - 'Invalid Server {server}'.format(server=name_or_id) - ) + raise exceptions.SDKException(f'Invalid Server {name_or_id}') self.compute.delete_server_metadata( server=server.id, keys=metadata_keys @@ -1545,7 +1541,7 @@ def delete_flavor(self, name_or_id): return True except exceptions.SDKException: raise exceptions.SDKException( - "Unable to delete flavor {name}".format(name=name_or_id) + f"Unable to delete flavor {name_or_id}" ) def set_flavor_specs(self, flavor_id, extra_specs): diff --git a/openstack/cloud/_dns.py b/openstack/cloud/_dns.py index 5c5ca80b3..c0bca9102 100644 --- a/openstack/cloud/_dns.py +++ b/openstack/cloud/_dns.py @@ -104,9 +104,7 @@ def create_zone( try: return self.dns.create_zone(**zone) except exceptions.SDKException: - raise exceptions.SDKException( - "Unable to create zone {name}".format(name=name) - ) + raise exceptions.SDKException(f"Unable to create zone {name}") @_utils.valid_kwargs('email', 'description', 'ttl', 'masters') def update_zone(self, name_or_id, **kwargs): diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index 06eb81ff0..60ed7c9e1 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -180,14 +180,14 @@ def get_floating_ip_by_id(self, id): :returns: A floating ip `:class:`~openstack.network.v2.floating_ip.FloatingIP`. """ - error_message = "Error getting floating ip with ID {id}".format(id=id) + error_message = f"Error getting floating ip with ID {id}" if self._use_neutron_floating(): fip = self.network.get_ip(id) return fip else: data = proxy._json_response( - self.compute.get('/os-floating-ips/{id}'.format(id=id)), + self.compute.get(f'/os-floating-ips/{id}'), error_message=error_message, ) return self._normalize_floating_ip( @@ -230,7 +230,7 @@ def _neutron_available_floating_ips( if floating_network_id is None: raise exceptions.NotFoundException( - "unable to find external network {net}".format(net=network) + f"unable to find external network {network}" ) else: floating_network_id = self._get_floating_network_id() @@ -270,7 +270,7 @@ def _nova_available_floating_ips(self, pool=None): """ with _utils.openstacksdk_exceptions( - "Unable to create floating IP in pool {pool}".format(pool=pool) + f"Unable to create floating IP in pool {pool}" ): if pool is None: pools = self.list_floating_ip_pools() @@ -442,7 +442,7 @@ def _neutron_create_floating_ip( except exceptions.ResourceNotFound: raise exceptions.NotFoundException( "unable to find network for floating ips with ID " - "{0}".format(network_name_or_id) + "{}".format(network_name_or_id) ) network_id = network['id'] else: @@ -516,7 +516,7 @@ def _neutron_create_floating_ip( def _nova_create_floating_ip(self, pool=None): with _utils.openstacksdk_exceptions( - "Unable to create floating IP in pool {pool}".format(pool=pool) + f"Unable to create floating IP in pool {pool}" ): if pool is None: pools = self.list_floating_ip_pools() @@ -599,9 +599,7 @@ def _neutron_delete_floating_ip(self, floating_ip_id): def _nova_delete_floating_ip(self, floating_ip_id): try: proxy._json_response( - self.compute.delete( - '/os-floating-ips/{id}'.format(id=floating_ip_id) - ), + self.compute.delete(f'/os-floating-ips/{floating_ip_id}'), error_message='Unable to delete floating IP {fip_id}'.format( fip_id=floating_ip_id ), @@ -738,7 +736,7 @@ def _neutron_attach_ip_to_server( ) if not port: raise exceptions.SDKException( - "unable to find a port for server {0}".format(server['id']) + "unable to find a port for server {}".format(server['id']) ) floating_ip_args = {'port_id': port['id']} @@ -753,7 +751,7 @@ def _nova_attach_ip_to_server( f_ip = self.get_floating_ip(id=floating_ip_id) if f_ip is None: raise exceptions.SDKException( - "unable to find floating IP {0}".format(floating_ip_id) + f"unable to find floating IP {floating_ip_id}" ) error_message = "Error attaching IP {ip} to instance {id}".format( ip=floating_ip_id, id=server_id @@ -763,7 +761,7 @@ def _nova_attach_ip_to_server( body['fixed_address'] = fixed_address return proxy._json_response( self.compute.post( - '/servers/{server_id}/action'.format(server_id=server_id), + f'/servers/{server_id}/action', json=dict(addFloatingIp=body), ), error_message=error_message, @@ -806,11 +804,9 @@ def _neutron_detach_ip_from_server(self, server_id, floating_ip_id): self.network.update_ip(floating_ip_id, port_id=None) except exceptions.SDKException: raise exceptions.SDKException( - ( - "Error detaching IP {ip} from " - "server {server_id}".format( - ip=floating_ip_id, server_id=server_id - ) + "Error detaching IP {ip} from " + "server {server_id}".format( + ip=floating_ip_id, server_id=server_id ) ) @@ -820,14 +816,14 @@ def _nova_detach_ip_from_server(self, server_id, floating_ip_id): f_ip = self.get_floating_ip(id=floating_ip_id) if f_ip is None: raise exceptions.SDKException( - "unable to find floating IP {0}".format(floating_ip_id) + f"unable to find floating IP {floating_ip_id}" ) error_message = "Error detaching IP {ip} from instance {id}".format( ip=floating_ip_id, id=server_id ) return proxy._json_response( self.compute.post( - '/servers/{server_id}/action'.format(server_id=server_id), + f'/servers/{server_id}/action', json=dict( removeFloatingIp=dict(address=f_ip['floating_ip_address']) ), @@ -1222,7 +1218,7 @@ def _nat_destination_port( return port, fixed_address raise exceptions.SDKException( "unable to find a free fixed IPv4 address for server " - "{0}".format(server['id']) + "{}".format(server['id']) ) # unfortunately a port can have more than one fixed IP: # we can't use the search_ports filtering for fixed_address as diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index 5ee0e6aca..31089a917 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -384,31 +384,25 @@ def delete_user(self, name_or_id, **kwargs): try: user = self.get_user(name_or_id, **kwargs) if not user: - self.log.debug( - "User {0} not found for deleting".format(name_or_id) - ) + self.log.debug(f"User {name_or_id} not found for deleting") return False self.identity.delete_user(user) return True except exceptions.SDKException: - self.log.exception( - "Error in deleting user {user}".format(user=name_or_id) - ) + self.log.exception(f"Error in deleting user {name_or_id}") return False def _get_user_and_group(self, user_name_or_id, group_name_or_id): user = self.get_user(user_name_or_id) if not user: - raise exceptions.SDKException( - 'User {user} not found'.format(user=user_name_or_id) - ) + raise exceptions.SDKException(f'User {user_name_or_id} not found') group = self.get_group(group_name_or_id) if not group: raise exceptions.SDKException( - 'Group {user} not found'.format(user=group_name_or_id) + f'Group {group_name_or_id} not found' ) return (user, group) @@ -731,7 +725,7 @@ def delete_endpoint(self, id): self.identity.delete_endpoint(id) return True except exceptions.SDKException: - self.log.exception("Failed to delete endpoint {id}".format(id=id)) + self.log.exception(f"Failed to delete endpoint {id}") return False def create_domain(self, name, description=None, enabled=True): @@ -778,7 +772,7 @@ def update_domain( dom = self.get_domain(None, name_or_id) if dom is None: raise exceptions.SDKException( - "Domain {0} not found for updating".format(name_or_id) + f"Domain {name_or_id} not found for updating" ) domain_id = dom['id'] @@ -1006,7 +1000,7 @@ def update_group( group = self.identity.find_group(name_or_id, **kwargs) if group is None: raise exceptions.SDKException( - "Group {0} not found for updating".format(name_or_id) + f"Group {name_or_id} not found for updating" ) group_ref = {} @@ -1039,9 +1033,7 @@ def delete_group(self, name_or_id): return True except exceptions.SDKException: - self.log.exception( - "Unable to delete group {name}".format(name=name_or_id) - ) + self.log.exception(f"Unable to delete group {name_or_id}") return False def list_roles(self, **kwargs): @@ -1235,9 +1227,7 @@ def delete_role(self, name_or_id, **kwargs): self.identity.delete_role(role) return True except exceptions.SDKExceptions: - self.log.exception( - "Unable to delete role {name}".format(name=name_or_id) - ) + self.log.exception(f"Unable to delete role {name_or_id}") raise def _get_grant_revoke_params( @@ -1261,7 +1251,7 @@ def _get_grant_revoke_params( data['role'] = self.identity.find_role(name_or_id=role) if not data['role']: - raise exceptions.SDKException('Role {0} not found.'.format(role)) + raise exceptions.SDKException(f'Role {role} not found.') if user: # use cloud.get_user to save us from bad searching by name diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py index 960f985be..685dce2f4 100644 --- a/openstack/cloud/_image.py +++ b/openstack/cloud/_image.py @@ -170,7 +170,7 @@ def wait_for_image(self, image, timeout=3600): return image elif image['status'] == 'error': raise exceptions.SDKException( - 'Image {image} hit error state'.format(image=image_id) + f'Image {image_id} hit error state' ) def delete_image( diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index ce35669b5..b88bec504 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -2772,7 +2772,7 @@ def update_port(self, name_or_id, **kwargs): port = self.get_port(name_or_id=name_or_id) if port is None: raise exceptions.SDKException( - "failed to find port '{port}'".format(port=name_or_id) + f"failed to find port '{name_or_id}'" ) return self.network.update_port(port, **kwargs) @@ -2813,7 +2813,7 @@ def _get_port_ids(self, name_or_id_list, filters=None): port = self.get_port(name_or_id, filters) if not port: raise exceptions.ResourceNotFound( - 'Port {id} not found'.format(id=name_or_id) + f'Port {name_or_id} not found' ) ids_list.append(port['id']) return ids_list diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index c704c0fc1..f918d3235 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -453,10 +453,9 @@ def stream_object( error. """ try: - for ret in self.object_store.stream_object( + yield from self.object_store.stream_object( obj, container, chunk_size=resp_chunk_size - ): - yield ret + ) except exceptions.ResourceNotFound: return diff --git a/openstack/cloud/_security_group.py b/openstack/cloud/_security_group.py index c7b641ebe..d1af47a54 100644 --- a/openstack/cloud/_security_group.py +++ b/openstack/cloud/_security_group.py @@ -419,9 +419,7 @@ def delete_security_group_rule(self, rule_id): else: try: exceptions.raise_from_response( - self.compute.delete( - '/os-security-group-rules/{id}'.format(id=rule_id) - ) + self.compute.delete(f'/os-security-group-rules/{rule_id}') ) except exceptions.NotFoundException: return False diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index 232a12bfe..57830f771 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -389,9 +389,7 @@ def range_filter(data, key, range_exp): # If parsing the range fails, it must be a bad value. if val_range is None: - raise exceptions.SDKException( - "Invalid range value: {value}".format(value=range_exp) - ) + raise exceptions.SDKException(f"Invalid range value: {range_exp}") op = val_range[0] if op: diff --git a/openstack/cloud/exc.py b/openstack/cloud/exc.py index b104653a0..2c9e6be87 100644 --- a/openstack/cloud/exc.py +++ b/openstack/cloud/exc.py @@ -28,7 +28,7 @@ class OpenStackCloudUnavailableFeature(OpenStackCloudException): # Backwards compat. These are deprecated and should not be used in new code. class OpenStackCloudCreateException(OpenStackCloudException): def __init__(self, resource, resource_id, extra_data=None, **kwargs): - super(OpenStackCloudCreateException, self).__init__( + super().__init__( message="Error creating {resource}: {resource_id}".format( resource=resource, resource_id=resource_id ), diff --git a/openstack/cloud/meta.py b/openstack/cloud/meta.py index 184c1b984..5b1571ab0 100644 --- a/openstack/cloud/meta.py +++ b/openstack/cloud/meta.py @@ -265,7 +265,7 @@ def find_best_address(addresses, public=False, cloud_public=True): connect_socket.settimeout(1) connect_socket.connect(sa) return address - except socket.error: + except OSError: # Sometimes a "no route to address" type error # will fail fast, but can often come alive # when retried. @@ -370,7 +370,7 @@ def get_groups_from_server(cloud, server, server_vars): groups.append(region) # And one by cloud_region - groups.append("%s_%s" % (cloud_name, region)) + groups.append(f"{cloud_name}_{region}") # Check if group metadata key in servers' metadata group = server['metadata'].get('group') @@ -385,17 +385,17 @@ def get_groups_from_server(cloud, server, server_vars): for key in ('flavor', 'image'): if 'name' in server_vars[key]: - groups.append('%s-%s' % (key, server_vars[key]['name'])) + groups.append('{}-{}'.format(key, server_vars[key]['name'])) for key, value in iter(server['metadata'].items()): - groups.append('meta-%s_%s' % (key, value)) + groups.append(f'meta-{key}_{value}') az = server_vars.get('az', None) if az: # Make groups for az, region_az and cloud_region_az groups.append(az) - groups.append('%s_%s' % (region, az)) - groups.append('%s_%s_%s' % (cloud.name, region, az)) + groups.append(f'{region}_{az}') + groups.append(f'{cloud.name}_{region}_{az}') return groups diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index b0cccf45e..81abdb436 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -64,7 +64,7 @@ class _OpenStackCloudMixin: _SHADE_OBJECT_AUTOCREATE_KEY = 'x-object-meta-x-shade-autocreated' def __init__(self): - super(_OpenStackCloudMixin, self).__init__() + super().__init__() self.log = _log.setup_logging('openstack') @@ -172,10 +172,10 @@ def pop_keys(params, auth, name_key, id_key): name_key = 'username' else: name_key = 'project_name' - id_key = '{prefix}_id'.format(prefix=prefix) + id_key = f'{prefix}_id' pop_keys(params, kwargs, name_key, id_key) - id_key = '{prefix}_domain_id'.format(prefix=prefix) - name_key = '{prefix}_domain_name'.format(prefix=prefix) + id_key = f'{prefix}_domain_id' + name_key = f'{prefix}_domain_name' pop_keys(params, kwargs, name_key, id_key) for key, value in kwargs.items(): @@ -282,14 +282,14 @@ def _make_cache_key(self, namespace, fn): if namespace is None: name_key = self.name else: - name_key = '%s:%s' % (self.name, namespace) + name_key = f'{self.name}:{namespace}' def generate_key(*args, **kwargs): # TODO(frickler): make handling arg keys actually work arg_key = '' kw_keys = sorted(kwargs.keys()) kwargs_key = ','.join( - ['%s:%s' % (k, kwargs[k]) for k in kw_keys if k != 'cache'] + [f'{k}:{kwargs[k]}' for k in kw_keys if k != 'cache'] ) ans = "_".join([str(name_key), fname, arg_key, kwargs_key]) return ans diff --git a/openstack/common/quota_set.py b/openstack/common/quota_set.py index f5afd7c04..1c7a147af 100644 --- a/openstack/common/quota_set.py +++ b/openstack/common/quota_set.py @@ -57,7 +57,7 @@ def fetch( error_message=None, **params ): - return super(QuotaSet, self).fetch( + return super().fetch( session, requires_id=False, base_path=base_path, diff --git a/openstack/compute/v2/flavor.py b/openstack/compute/v2/flavor.py index dd0c39300..e6d1390b3 100644 --- a/openstack/compute/v2/flavor.py +++ b/openstack/compute/v2/flavor.py @@ -103,7 +103,7 @@ def list( # is_public is ternary - None means give all flavors. # Force it to string to avoid requests skipping it. params['is_public'] = 'None' - return super(Flavor, cls).list( + return super().list( session, paginated=paginated, base_path=base_path, **params ) diff --git a/openstack/compute/v2/keypair.py b/openstack/compute/v2/keypair.py index 17567b52f..3a7bb958e 100644 --- a/openstack/compute/v2/keypair.py +++ b/openstack/compute/v2/keypair.py @@ -60,7 +60,7 @@ def _consume_attrs(self, mapping, attrs): # it **SOMETIMES** keypair picks up id and not name. This is a hammer. if 'id' in attrs: attrs.setdefault('name', attrs.pop('id')) - return super(Keypair, self)._consume_attrs(mapping, attrs) + return super()._consume_attrs(mapping, attrs) @classmethod def existing(cls, connection=None, **kwargs): diff --git a/openstack/compute/v2/limits.py b/openstack/compute/v2/limits.py index 5c17a2976..897e86403 100644 --- a/openstack/compute/v2/limits.py +++ b/openstack/compute/v2/limits.py @@ -113,7 +113,7 @@ def fetch( """ # TODO(mordred) We shouldn't have to subclass just to declare # requires_id = False. - return super(Limits, self).fetch( + return super().fetch( session=session, requires_id=requires_id, error_message=error_message, diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 3c0376b4c..078d75d8b 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -268,7 +268,7 @@ def _prepare_request( base_path=None, **kwargs, ): - request = super(Server, self)._prepare_request( + request = super()._prepare_request( requires_id=requires_id, prepend_key=prepend_key, base_path=base_path, diff --git a/openstack/compute/v2/server_remote_console.py b/openstack/compute/v2/server_remote_console.py index cfa24a1a2..7a845b6ab 100644 --- a/openstack/compute/v2/server_remote_console.py +++ b/openstack/compute/v2/server_remote_console.py @@ -55,6 +55,6 @@ def create(self, session, prepend_key=True, base_path=None, **params): raise ValueError( 'Console type webmks is not supported on server side' ) - return super(ServerRemoteConsole, self).create( + return super().create( session, prepend_key=prepend_key, base_path=base_path, **params ) diff --git a/openstack/compute/v2/service.py b/openstack/compute/v2/service.py index e35526582..e4dce2ee6 100644 --- a/openstack/compute/v2/service.py +++ b/openstack/compute/v2/service.py @@ -84,12 +84,12 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): if ignore_missing: return None raise exceptions.ResourceNotFound( - "No %s found for %s" % (cls.__name__, name_or_id) + f"No {cls.__name__} found for {name_or_id}" ) def commit(self, session, prepend_key=False, **kwargs): # we need to set prepend_key to false - return super(Service, self).commit( + return super().commit( session, prepend_key=prepend_key, **kwargs, diff --git a/openstack/config/cloud_config.py b/openstack/config/cloud_config.py index 7ce2bff3d..fdb4925b8 100644 --- a/openstack/config/cloud_config.py +++ b/openstack/config/cloud_config.py @@ -19,5 +19,5 @@ class CloudConfig(cloud_region.CloudRegion): def __init__(self, name, region, config, **kwargs): - super(CloudConfig, self).__init__(name, region, config, **kwargs) + super().__init__(name, region, config, **kwargs) self.region = region diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index e50bf08a8..83f7b4eb4 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -76,7 +76,7 @@ def _make_key(key, service_type): def _disable_service(config, service_type, reason=None): service_type = service_type.lower().replace('-', '_') - key = 'has_{service_type}'.format(service_type=service_type) + key = f'has_{service_type}' config[key] = False if reason: d_key = _make_key('disabled_reason', service_type) @@ -1217,7 +1217,7 @@ def get_prometheus_counter(self): def has_service(self, service_type): service_type = service_type.lower().replace('-', '_') - key = 'has_{service_type}'.format(service_type=service_type) + key = f'has_{service_type}' return self.config.get( key, self._service_type_manager.is_official(service_type) ) @@ -1227,7 +1227,7 @@ def disable_service(self, service_type, reason=None): def enable_service(self, service_type): service_type = service_type.lower().replace('-', '_') - key = 'has_{service_type}'.format(service_type=service_type) + key = f'has_{service_type}' self.config[key] = True def get_disabled_reason(self, service_type): diff --git a/openstack/config/defaults.py b/openstack/config/defaults.py index 4fa5637e4..a4fc6f05d 100644 --- a/openstack/config/defaults.py +++ b/openstack/config/defaults.py @@ -46,7 +46,7 @@ def get_defaults(json_path=_json_path): cert=None, key=None, ) - with open(json_path, 'r') as json_file: + with open(json_path) as json_file: updates = json.load(json_file) if updates is not None: tmp_defaults.update(updates) diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 2d9cf19e8..ad4055fa4 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -411,15 +411,13 @@ def _get_os_environ(self, envvar_prefix=None): ret[newkey] = os.environ[k] # If the only environ keys are selectors or behavior modification, # don't return anything - selectors = set( - [ - 'OS_CLOUD', - 'OS_REGION_NAME', - 'OS_CLIENT_CONFIG_FILE', - 'OS_CLIENT_SECURE_FILE', - 'OS_CLOUD_NAME', - ] - ) + selectors = { + 'OS_CLOUD', + 'OS_REGION_NAME', + 'OS_CLIENT_CONFIG_FILE', + 'OS_CLIENT_SECURE_FILE', + 'OS_CLOUD_NAME', + } if set(environkeys) - selectors: return ret return None @@ -456,12 +454,12 @@ def _load_yaml_json_file(self, filelist): for path in filelist: if os.path.exists(path): try: - with open(path, 'r') as f: + with open(path) as f: if path.endswith('json'): return path, json.load(f) else: return path, yaml.safe_load(f) - except IOError as e: + except OSError as e: if e.errno == errno.EACCES: # Can't access file so let's continue to the next # file @@ -560,9 +558,7 @@ def _get_base_cloud_config(self, name, profile=None): # Only validate cloud name if one was given if name and name not in self.cloud_config['clouds']: - raise exceptions.ConfigException( - "Cloud {name} was not found.".format(name=name) - ) + raise exceptions.ConfigException(f"Cloud {name} was not found.") our_cloud = self.cloud_config['clouds'].get(name, dict()) if profile: @@ -1440,7 +1436,7 @@ def set_one_cloud(config_file, cloud, set_config=None): try: with open(config_file) as fh: cur_config = yaml.safe_load(fh) - except IOError as e: + except OSError as e: # Not no such file if e.errno != 2: raise diff --git a/openstack/config/vendors/__init__.py b/openstack/config/vendors/__init__.py index 189d3744a..68d68b74e 100644 --- a/openstack/config/vendors/__init__.py +++ b/openstack/config/vendors/__init__.py @@ -33,11 +33,11 @@ def _get_vendor_defaults(): global _VENDOR_DEFAULTS if not _VENDOR_DEFAULTS: for vendor in glob.glob(os.path.join(_VENDORS_PATH, '*.yaml')): - with open(vendor, 'r') as f: + with open(vendor) as f: vendor_data = yaml.safe_load(f) _VENDOR_DEFAULTS[vendor_data['name']] = vendor_data['profile'] for vendor in glob.glob(os.path.join(_VENDORS_PATH, '*.json')): - with open(vendor, 'r') as f: + with open(vendor) as f: vendor_data = json.load(f) _VENDOR_DEFAULTS[vendor_data['name']] = vendor_data['profile'] return _VENDOR_DEFAULTS diff --git a/openstack/dns/v2/_base.py b/openstack/dns/v2/_base.py index 0cac1eac5..7efdbfed0 100644 --- a/openstack/dns/v2/_base.py +++ b/openstack/dns/v2/_base.py @@ -69,7 +69,7 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): if ignore_missing: return None raise exceptions.ResourceNotFound( - "No %s found for %s" % (cls.__name__, name_or_id) + f"No {cls.__name__} found for {name_or_id}" ) @classmethod diff --git a/openstack/exceptions.py b/openstack/exceptions.py index 53dc0a6a3..aa2f92296 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -29,21 +29,21 @@ class SDKException(Exception): def __init__(self, message=None, extra_data=None): self.message = self.__class__.__name__ if message is None else message self.extra_data = extra_data - super(SDKException, self).__init__(self.message) + super().__init__(self.message) class EndpointNotFound(SDKException): """A mismatch occurred between what the client and server expect.""" def __init__(self, message=None): - super(EndpointNotFound, self).__init__(message) + super().__init__(message) class InvalidResponse(SDKException): """The response from the server is not valid for this request.""" def __init__(self, response): - super(InvalidResponse, self).__init__() + super().__init__() self.response = response @@ -51,7 +51,7 @@ class InvalidRequest(SDKException): """The request to the server is not valid.""" def __init__(self, message=None): - super(InvalidRequest, self).__init__(message) + super().__init__(message) class HttpException(SDKException, _rex.HTTPError): @@ -111,7 +111,7 @@ def __str__(self): remote_error += str(self.details) return "{message}: {remote_error}".format( - message=super(HttpException, self).__str__(), + message=super().__str__(), remote_error=remote_error, ) @@ -142,12 +142,12 @@ def __init__(self, resource, method): except AttributeError: name = resource.__class__.__name__ - message = 'The %s method is not supported for %s.%s' % ( + message = 'The {} method is not supported for {}.{}'.format( method, resource.__module__, name, ) - super(MethodNotSupported, self).__init__(message=message) + super().__init__(message=message) class DuplicateResource(SDKException): diff --git a/openstack/fixture/connection.py b/openstack/fixture/connection.py index 80195602d..d9a68614c 100644 --- a/openstack/fixture/connection.py +++ b/openstack/fixture/connection.py @@ -46,7 +46,7 @@ class ConnectionFixture(fixtures.Fixture): } def __init__(self, suburl=False, project_id=None, *args, **kwargs): - super(ConnectionFixture, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self._endpoint_templates = _ENDPOINT_TEMPLATES if suburl: self.use_suburl() diff --git a/openstack/image/_download.py b/openstack/image/_download.py index fc4145cbf..1203b5579 100644 --- a/openstack/image/_download.py +++ b/openstack/image/_download.py @@ -22,7 +22,7 @@ def _verify_checksum(md5, checksum): digest = md5.hexdigest() if digest != checksum: raise exceptions.InvalidResponse( - "checksum mismatch: %s != %s" % (checksum, digest) + f"checksum mismatch: {checksum} != {digest}" ) diff --git a/openstack/image/v1/_proxy.py b/openstack/image/v1/_proxy.py index b32e7a2fd..bdcf35cf7 100644 --- a/openstack/image/v1/_proxy.py +++ b/openstack/image/v1/_proxy.py @@ -295,7 +295,7 @@ def _upload_image( image = self._connection._get_and_munchify( 'image', self.put( - '/images/{id}'.format(id=image.id), + f'/images/{image.id}', headers=headers, data=image_data, ), @@ -303,7 +303,7 @@ def _upload_image( except exc.HttpException: self.log.debug("Deleting failed upload of image %s", name) try: - self.delete('/images/{id}'.format(id=image.id)) + self.delete(f'/images/{image.id}') except exc.HttpException: # We're just trying to clean up - if it doesn't work - shrug self.log.warning( @@ -434,10 +434,10 @@ def _update_image_properties(self, image, meta, properties): img_props = {} for k, v in iter(properties.items()): if image.properties.get(k, None) != v: - img_props['x-image-meta-{key}'.format(key=k)] = v + img_props[f'x-image-meta-{k}'] = v if not img_props: return False - self.put('/images/{id}'.format(id=image.id), headers=img_props) + self.put(f'/images/{image.id}', headers=img_props) return True def update_image_properties( @@ -469,7 +469,7 @@ def update_image_properties( for k, v in iter(kwargs.items()): if v and k in ['ramdisk', 'kernel']: v = self._connection.get_image_id(v) - k = '{0}_id'.format(k) + k = f'{k}_id' img_props[k] = v return self._update_image_properties(image, meta, img_props) diff --git a/openstack/image/v1/image.py b/openstack/image/v1/image.py index c53941224..7e1283b1d 100644 --- a/openstack/image/v1/image.py +++ b/openstack/image/v1/image.py @@ -135,5 +135,5 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): if ignore_missing: return None raise exceptions.ResourceNotFound( - "No %s found for %s" % (cls.__name__, name_or_id) + f"No {cls.__name__} found for {name_or_id}" ) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index ca004d50e..e704d46ae 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -598,9 +598,7 @@ def _upload_image( self.log.debug("Image creation failed", exc_info=True) raise except Exception as e: - raise exceptions.SDKException( - "Image creation failed: {message}".format(message=str(e)) - ) + raise exceptions.SDKException(f"Image creation failed: {str(e)}") def _make_v2_image_params(self, meta, properties): ret: ty.Dict = {} @@ -949,7 +947,7 @@ def update_image_properties( for k, v in iter(kwargs.items()): if v and k in ['ramdisk', 'kernel']: v = self._connection.get_image_id(v) - k = '{0}_id'.format(k) + k = f'{k}_id' properties[k] = v img_props = image.properties.copy() @@ -1840,7 +1838,7 @@ def wait_for_task( if task.status.lower() == status.lower(): return task - name = "{res}:{id}".format(res=task.__class__.__name__, id=task.id) + name = f"{task.__class__.__name__}:{task.id}" msg = "Timeout waiting for {name} to transition to {status}".format( name=name, status=status ) diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index b42d05314..ec1da5e5f 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -385,7 +385,7 @@ def _prepare_request( base_path=None, **kwargs, ): - request = super(Image, self)._prepare_request( + request = super()._prepare_request( requires_id=requires_id, prepend_key=prepend_key, patch=patch, @@ -403,7 +403,7 @@ def _prepare_request( @classmethod def find(cls, session, name_or_id, ignore_missing=True, **params): # Do a regular search first (ignoring missing) - result = super(Image, cls).find(session, name_or_id, True, **params) + result = super().find(session, name_or_id, True, **params) if result: return result @@ -419,5 +419,5 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): if ignore_missing: return None raise exceptions.ResourceNotFound( - "No %s found for %s" % (cls.__name__, name_or_id) + f"No {cls.__name__} found for {name_or_id}" ) diff --git a/openstack/load_balancer/v2/amphora.py b/openstack/load_balancer/v2/amphora.py index 955404877..b33a4a603 100644 --- a/openstack/load_balancer/v2/amphora.py +++ b/openstack/load_balancer/v2/amphora.py @@ -114,9 +114,7 @@ class AmphoraConfig(resource.Resource): # The default _update code path also has no # way to pass has_body into this function, so overriding the method here. def commit(self, session, base_path=None): - return super(AmphoraConfig, self).commit( - session, base_path=base_path, has_body=False - ) + return super().commit(session, base_path=base_path, has_body=False) class AmphoraFailover(resource.Resource): @@ -139,6 +137,4 @@ class AmphoraFailover(resource.Resource): # The default _update code path also has no # way to pass has_body into this function, so overriding the method here. def commit(self, session, base_path=None): - return super(AmphoraFailover, self).commit( - session, base_path=base_path, has_body=False - ) + return super().commit(session, base_path=base_path, has_body=False) diff --git a/openstack/load_balancer/v2/load_balancer.py b/openstack/load_balancer/v2/load_balancer.py index 34cc39be8..0fddea5fa 100644 --- a/openstack/load_balancer/v2/load_balancer.py +++ b/openstack/load_balancer/v2/load_balancer.py @@ -146,6 +146,4 @@ class LoadBalancerFailover(resource.Resource): # The default _update code path also has no # way to pass has_body into this function, so overriding the method here. def commit(self, session, base_path=None): - return super(LoadBalancerFailover, self).commit( - session, base_path=base_path, has_body=False - ) + return super().commit(session, base_path=base_path, has_body=False) diff --git a/openstack/load_balancer/v2/quota.py b/openstack/load_balancer/v2/quota.py index 8b46c5475..ddab05746 100644 --- a/openstack/load_balancer/v2/quota.py +++ b/openstack/load_balancer/v2/quota.py @@ -44,7 +44,7 @@ class Quota(resource.Resource): def _prepare_request( self, requires_id=True, base_path=None, prepend_key=False, **kwargs ): - _request = super(Quota, self)._prepare_request( + _request = super()._prepare_request( requires_id, prepend_key, base_path=base_path ) if self.resource_key in _request.body: diff --git a/openstack/message/v2/claim.py b/openstack/message/v2/claim.py index 1ce0544ce..d4fb9bb30 100644 --- a/openstack/message/v2/claim.py +++ b/openstack/message/v2/claim.py @@ -56,7 +56,7 @@ class Claim(resource.Resource): project_id = resource.Header("X-PROJECT-ID") def _translate_response(self, response, has_body=True): - super(Claim, self)._translate_response(response, has_body=has_body) + super()._translate_response(response, has_body=has_body) if has_body and self.location: # Extract claim ID from location self.id = self.location.split("claims/")[1] diff --git a/openstack/network/v2/_base.py b/openstack/network/v2/_base.py index e151e8144..f0ef10e46 100644 --- a/openstack/network/v2/_base.py +++ b/openstack/network/v2/_base.py @@ -28,7 +28,7 @@ def _prepare_request( if_revision=None, **kwargs ): - req = super(NetworkResource, self)._prepare_request( + req = super()._prepare_request( requires_id=requires_id, prepend_key=prepend_key, patch=patch, diff --git a/openstack/network/v2/quota.py b/openstack/network/v2/quota.py index f3b79743a..8c5bc7eb8 100644 --- a/openstack/network/v2/quota.py +++ b/openstack/network/v2/quota.py @@ -63,9 +63,7 @@ class Quota(resource.Resource): def _prepare_request( self, requires_id=True, prepend_key=False, base_path=None, **kwargs ): - _request = super(Quota, self)._prepare_request( - requires_id, prepend_key - ) + _request = super()._prepare_request(requires_id, prepend_key) if self.resource_key in _request.body: _body = _request.body[self.resource_key] else: diff --git a/openstack/network/v2/security_group_rule.py b/openstack/network/v2/security_group_rule.py index 369a70de2..d4e7b0d6d 100644 --- a/openstack/network/v2/security_group_rule.py +++ b/openstack/network/v2/security_group_rule.py @@ -96,9 +96,7 @@ class SecurityGroupRule(_base.NetworkResource, tag.TagMixin): updated_at = resource.Body('updated_at') def _prepare_request(self, *args, **kwargs): - _request = super(SecurityGroupRule, self)._prepare_request( - *args, **kwargs - ) + _request = super()._prepare_request(*args, **kwargs) # Old versions of Neutron do not handle being passed a # remote_address_group_id and raise and error. Remove it from # the body if it is blank. diff --git a/openstack/object_store/v1/_base.py b/openstack/object_store/v1/_base.py index 0b1c4dfed..0f910d0cf 100644 --- a/openstack/object_store/v1/_base.py +++ b/openstack/object_store/v1/_base.py @@ -97,7 +97,7 @@ def _translate_response(self, response, has_body=None, error_message=None): # This must happen before invoking parent _translate_response, cause it # pops known headers. self._last_headers = response.headers.copy() - super(BaseResource, self)._translate_response( + super()._translate_response( response, has_body=has_body, error_message=error_message ) self._set_metadata(response.headers) diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index e28f0b0b7..708c9d5e5 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -652,7 +652,7 @@ def _upload_large_object( # While Object Storage usually expects the name to be # urlencoded in most requests, the SLO manifest requires # plain object names instead. - path='/{name}'.format(name=parse.unquote(name)), + path=f'/{parse.unquote(name)}', size_bytes=segment.length, ) ) @@ -808,7 +808,7 @@ def _add_etag_to_manifest(self, segment_results, manifest): continue name = self._object_name_from_url(result.url) for entry in manifest: - if entry['path'] == '/{name}'.format(name=parse.unquote(name)): + if entry['path'] == f'/{parse.unquote(name)}': entry['etag'] = result.headers['Etag'] def get_info(self): @@ -931,7 +931,7 @@ def generate_form_signature( endpoint = parse.urlparse(self.get_endpoint()) path = '/'.join([endpoint.path, res.name, object_prefix]) - data = '%s\n%s\n%s\n%s\n%s' % ( + data = '{}\n{}\n{}\n{}\n{}'.format( path, redirect_url, max_file_size, @@ -1067,7 +1067,7 @@ def generate_temp_url( raise ValueError('ip_range must be representable as UTF-8') hmac_parts.insert(0, "ip=%s" % ip_range) - hmac_body = u'\n'.join(hmac_parts) + hmac_body = '\n'.join(hmac_parts) temp_url_key = self._check_temp_url_key(temp_url_key=temp_url_key) @@ -1082,17 +1082,17 @@ def generate_temp_url( else: exp = str(expiration) - temp_url = u'{path}?temp_url_sig={sig}&temp_url_expires={exp}'.format( + temp_url = '{path}?temp_url_sig={sig}&temp_url_expires={exp}'.format( path=path_for_body, sig=sig, exp=exp, ) if ip_range: - temp_url += u'&temp_url_ip_range={}'.format(ip_range) + temp_url += f'&temp_url_ip_range={ip_range}' if prefix: - temp_url += u'&temp_url_prefix={}'.format(parts[4]) + temp_url += f'&temp_url_prefix={parts[4]}' # Have return type match path from caller if isinstance(path, bytes): return temp_url.encode('utf-8') diff --git a/openstack/orchestration/util/event_utils.py b/openstack/orchestration/util/event_utils.py index 99d61deda..c84a550c3 100644 --- a/openstack/orchestration/util/event_utils.py +++ b/openstack/orchestration/util/event_utils.py @@ -72,10 +72,10 @@ def is_stack_event(event): return False phys_id = event.get('physical_resource_id', '') - links = dict( - (link.get('rel'), link.get('href')) + links = { + link.get('rel'): link.get('href') for link in event.get('links', []) - ) + } stack_id = links.get('stack', phys_id).rsplit('/', 1)[-1] return stack_id == phys_id diff --git a/openstack/orchestration/util/template_format.py b/openstack/orchestration/util/template_format.py index 426a22c66..ab1844444 100644 --- a/openstack/orchestration/util/template_format.py +++ b/openstack/orchestration/util/template_format.py @@ -30,13 +30,13 @@ def _construct_yaml_str(self, node): return self.construct_scalar(node) -HeatYamlLoader.add_constructor(u'tag:yaml.org,2002:str', _construct_yaml_str) +HeatYamlLoader.add_constructor('tag:yaml.org,2002:str', _construct_yaml_str) # Unquoted dates like 2013-05-23 in yaml files get loaded as objects of type # datetime.data which causes problems in API layer when being processed by # openstack.common.jsonutils. Therefore, make unicode string out of timestamps # until jsonutils can handle dates. HeatYamlLoader.add_constructor( - u'tag:yaml.org,2002:timestamp', _construct_yaml_str + 'tag:yaml.org,2002:timestamp', _construct_yaml_str ) diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index d68cee8e1..685f59ba2 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -58,7 +58,7 @@ def _extract_name_consume_url_parts(self, url_parts): # (/stacks/name/id/everything_else), so if on third position we # have not a known part - discard it, not to brake further logic del url_parts[2] - return super(Proxy, self)._extract_name_consume_url_parts(url_parts) + return super()._extract_name_consume_url_parts(url_parts) def read_env_and_templates( self, diff --git a/openstack/orchestration/v1/software_config.py b/openstack/orchestration/v1/software_config.py index 21a9c9750..cbd053fba 100644 --- a/openstack/orchestration/v1/software_config.py +++ b/openstack/orchestration/v1/software_config.py @@ -48,6 +48,4 @@ class SoftwareConfig(resource.Resource): def create(self, session, base_path=None): # This overrides the default behavior of resource creation because # heat doesn't accept resource_key in its request. - return super(SoftwareConfig, self).create( - session, prepend_key=False, base_path=base_path - ) + return super().create(session, prepend_key=False, base_path=base_path) diff --git a/openstack/orchestration/v1/software_deployment.py b/openstack/orchestration/v1/software_deployment.py index 9a2cbf6d8..12178fc06 100644 --- a/openstack/orchestration/v1/software_deployment.py +++ b/openstack/orchestration/v1/software_deployment.py @@ -52,13 +52,9 @@ class SoftwareDeployment(resource.Resource): def create(self, session, base_path=None): # This overrides the default behavior of resource creation because # heat doesn't accept resource_key in its request. - return super(SoftwareDeployment, self).create( - session, prepend_key=False, base_path=base_path - ) + return super().create(session, prepend_key=False, base_path=base_path) def commit(self, session, base_path=None): # This overrides the default behavior of resource creation because # heat doesn't accept resource_key in its request. - return super(SoftwareDeployment, self).commit( - session, prepend_key=False, base_path=base_path - ) + return super().commit(session, prepend_key=False, base_path=base_path) diff --git a/openstack/orchestration/v1/stack.py b/openstack/orchestration/v1/stack.py index d7871e53c..ef7230a17 100644 --- a/openstack/orchestration/v1/stack.py +++ b/openstack/orchestration/v1/stack.py @@ -36,7 +36,7 @@ class Stack(resource.Resource): 'owner_id', 'username', project_id='tenant_id', - **tag.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters, ) # Properties @@ -115,14 +115,12 @@ class Stack(resource.Resource): def create(self, session, base_path=None): # This overrides the default behavior of resource creation because # heat doesn't accept resource_key in its request. - return super(Stack, self).create( - session, prepend_key=False, base_path=base_path - ) + return super().create(session, prepend_key=False, base_path=base_path) def commit(self, session, base_path=None): # This overrides the default behavior of resource creation because # heat doesn't accept resource_key in its request. - return super(Stack, self).commit( + return super().commit( session, prepend_key=False, has_body=False, base_path=None ) @@ -131,16 +129,16 @@ def update(self, session, preview=False): # we need to use other endpoint for update preview. base_path = None if self.name and self.id: - base_path = '/stacks/%(stack_name)s/%(stack_id)s' % { - 'stack_name': self.name, - 'stack_id': self.id, - } + base_path = '/stacks/{stack_name}/{stack_id}'.format( + stack_name=self.name, + stack_id=self.id, + ) elif self.name or self.id: # We have only one of name/id. Do not try to build a stacks/NAME/ID # path - base_path = '/stacks/%(stack_identity)s' % { - 'stack_identity': self.name or self.id - } + base_path = '/stacks/{stack_identity}'.format( + stack_identity=self.name or self.id + ) request = self._prepare_request( prepend_key=False, requires_id=False, base_path=base_path ) @@ -290,7 +288,7 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): if ignore_missing: return None raise exceptions.ResourceNotFound( - "No %s found for %s" % (cls.__name__, name_or_id) + f"No {cls.__name__} found for {name_or_id}" ) diff --git a/openstack/proxy.py b/openstack/proxy.py index 5026cf2c9..2971824e0 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -112,7 +112,7 @@ def __init__( self._influxdb_client = influxdb_client self._influxdb_config = influxdb_config if self.service_type: - log_name = 'openstack.{0}'.format(self.service_type) + log_name = f'openstack.{self.service_type}' else: log_name = 'openstack' self.log = _log.setup_logging(log_name) @@ -333,7 +333,9 @@ def _report_stats_statsd(self, response, url=None, method=None, exc=None): with self._statsd_client.pipeline() as pipe: if response is not None: duration = int(response.elapsed.total_seconds() * 1000) - metric_name = '%s.%s' % (key, str(response.status_code)) + metric_name = '{}.{}'.format( + key, str(response.status_code) + ) pipe.timing(metric_name, duration) pipe.incr(metric_name) if duration > 1000: @@ -396,7 +398,7 @@ def _report_stats_influxdb( tags['status_code'] = str(response.status_code) # Note(gtema): emit also status_code as a value (counter) fields[str(response.status_code)] = 1 - fields['%s.%s' % (method, response.status_code)] = 1 + fields[f'{method}.{response.status_code}'] = 1 # Note(gtema): status_code field itself is also very helpful on the # graphs to show what was the code, instead of counting its # occurences @@ -411,7 +413,7 @@ def _report_stats_influxdb( else 'openstack_api' ) # Note(gtema) append service name into the measurement name - measurement = '%s.%s' % (measurement, self.service_type) + measurement = f'{measurement}.{self.service_type}' data = [dict(measurement=measurement, tags=tags, fields=fields)] try: self._influxdb_client.write_points(data) diff --git a/openstack/resource.py b/openstack/resource.py index 76293f216..a8c7a9970 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -306,9 +306,7 @@ def __len__(self): @property def dirty(self): """Return a dict of modified attributes""" - return dict( - (key, self.attributes.get(key, None)) for key in self._dirty - ) + return {key: self.attributes.get(key, None) for key in self._dirty} def clean(self, only=None): """Signal that the resource no longer has modified attributes. @@ -610,7 +608,7 @@ def _attributes_iterator(cls, components=tuple([Body, Header])): def __repr__(self): pairs = [ - "%s=%s" % (k, v if v is not None else 'None') + "{}={}".format(k, v if v is not None else 'None') for k, v in dict( itertools.chain( self._body.attributes.items(), @@ -622,7 +620,9 @@ def __repr__(self): ] args = ", ".join(pairs) - return "%s.%s(%s)" % (self.__module__, self.__class__.__name__, args) + return "{}.{}({})".format( + self.__module__, self.__class__.__name__, args + ) def __eq__(self, comparand): """Return True if another resource has the same contents""" @@ -1406,7 +1406,7 @@ def _assert_microversion_for( def _raise(message): if error_message: error_message.rstrip('.') - message = '%s. %s' % (error_message, message) + message = f'{error_message}. {message}' raise exceptions.NotSupported(message) @@ -1868,7 +1868,7 @@ def _convert_patch(self, patch): server_field = component.name if len(parts) > 1: - new_path = '/%s/%s' % (server_field, parts[1]) + new_path = f'/{server_field}/{parts[1]}' else: new_path = '/%s' % server_field converted.append(dict(item, path=new_path)) @@ -2172,7 +2172,7 @@ def _get_next_link(cls, uri, response, data, marker, limit, total_yielded): if not pagination_key and cls.resources_key: # Nova has a {key}_links dict in the main body - pagination_key = '{key}_links'.format(key=cls.resources_key) + pagination_key = f'{cls.resources_key}_links' if pagination_key: links = data.get(pagination_key, {}) @@ -2371,7 +2371,7 @@ def find( return None raise exceptions.ResourceNotFound( - "No %s found for %s" % (cls.__name__, name_or_id) + f"No {cls.__name__} found for {name_or_id}" ) @@ -2427,7 +2427,7 @@ def wait_for_status( failures = ['ERROR'] failures = [f.lower() for f in failures] - name = "{res}:{id}".format(res=resource.__class__.__name__, id=resource.id) + name = f"{resource.__class__.__name__}:{resource.id}" msg = "Timeout waiting for {name} to transition to {status}".format( name=name, status=status ) diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index 56c5dfb74..453ddb56a 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -594,7 +594,7 @@ def create_share_network_subnet(self, share_network_id, **attrs): return self._create( _share_network_subnet.ShareNetworkSubnet, **attrs, - share_network_id=share_network_id + share_network_id=share_network_id, ) def delete_share_network_subnet( @@ -654,7 +654,7 @@ def share_snapshot_instances(self, details=True, **query): return self._list( _share_snapshot_instance.ShareSnapshotInstance, base_path=base_path, - **query + **query, ) def get_share_snapshot_instance(self, snapshot_instance_id): @@ -857,7 +857,7 @@ def create_access_rule(self, share_id, **attrs): :rtype: :class:`~openstack.shared_file_system.v2. share_access_rules.ShareAccessRules` """ - base_path = "/shares/%s/action" % (share_id,) + base_path = f"/shares/{share_id}/action" return self._create( _share_access_rule.ShareAccessRule, base_path=base_path, **attrs ) @@ -911,7 +911,7 @@ def share_group_snapshots(self, details=True, **query): return self._list( _share_group_snapshot.ShareGroupSnapshot, base_path=base_path, - **query + **query, ) def share_group_snapshot_members(self, group_snapshot_id): @@ -956,7 +956,7 @@ def create_share_group_snapshot(self, share_group_id, **attrs): return self._create( _share_group_snapshot.ShareGroupSnapshot, share_group_id=share_group_id, - **attrs + **attrs, ) def reset_share_group_snapshot_status(self, group_snapshot_id, status): @@ -985,7 +985,7 @@ def update_share_group_snapshot(self, group_snapshot_id, **attrs): return self._update( _share_group_snapshot.ShareGroupSnapshot, group_snapshot_id, - **attrs + **attrs, ) def delete_share_group_snapshot( diff --git a/openstack/test/fakes.py b/openstack/test/fakes.py index 7ce55f372..63315b1b7 100644 --- a/openstack/test/fakes.py +++ b/openstack/test/fakes.py @@ -103,7 +103,7 @@ def generate_fake_resource( base_attrs[name] = [uuid.uuid4().hex] else: # Everything else - msg = "Fake value for %s.%s can not be generated" % ( + msg = "Fake value for {}.{} can not be generated".format( resource_type.__name__, name, ) @@ -130,7 +130,7 @@ def generate_fake_resource( base_attrs[name] = dict() else: # Everything else - msg = "Fake value for %s.%s can not be generated" % ( + msg = "Fake value for {}.{} can not be generated".format( resource_type.__name__, name, ) diff --git a/openstack/tests/base.py b/openstack/tests/base.py index 0882899b1..be5bbb06d 100644 --- a/openstack/tests/base.py +++ b/openstack/tests/base.py @@ -96,9 +96,7 @@ def assertEqual(self, first, second, *args, **kwargs): first = first.toDict() if isinstance(second, utils.Munch): second = second.toDict() - return super(TestCase, self).assertEqual( - first, second, *args, **kwargs - ) + return super().assertEqual(first, second, *args, **kwargs) def printLogs(self, *args): self._log_stream.seek(0) @@ -135,7 +133,9 @@ def assertSubdict(self, part, whole): missing_keys.append(key) if missing_keys: self.fail( - "Keys %s are in %s but not in %s" % (missing_keys, part, whole) + "Keys {} are in {} but not in {}".format( + missing_keys, part, whole + ) ) wrong_values = [ (key, part[key], whole[key]) diff --git a/openstack/tests/fakes.py b/openstack/tests/fakes.py index 1bf0bd705..e3f48c453 100644 --- a/openstack/tests/fakes.py +++ b/openstack/tests/fakes.py @@ -27,9 +27,9 @@ from openstack import utils PROJECT_ID = '1c36b64c840a42cd9e9b931a369337f0' -FLAVOR_ID = u'0c1d9008-f546-4608-9e8f-f8bdaec8dddd' -CHOCOLATE_FLAVOR_ID = u'0c1d9008-f546-4608-9e8f-f8bdaec8ddde' -STRAWBERRY_FLAVOR_ID = u'0c1d9008-f546-4608-9e8f-f8bdaec8dddf' +FLAVOR_ID = '0c1d9008-f546-4608-9e8f-f8bdaec8dddd' +CHOCOLATE_FLAVOR_ID = '0c1d9008-f546-4608-9e8f-f8bdaec8ddde' +STRAWBERRY_FLAVOR_ID = '0c1d9008-f546-4608-9e8f-f8bdaec8dddf' COMPUTE_ENDPOINT = 'https://compute.example.com/v2.1' ORCHESTRATION_ENDPOINT = 'https://orchestration.example.com/v1/{p}'.format( p=PROJECT_ID @@ -48,30 +48,30 @@ def make_fake_flavor(flavor_id, name, ram=100, disk=1600, vcpus=24): return { - u'OS-FLV-DISABLED:disabled': False, - u'OS-FLV-EXT-DATA:ephemeral': 0, - u'disk': disk, - u'id': flavor_id, - u'links': [ + 'OS-FLV-DISABLED:disabled': False, + 'OS-FLV-EXT-DATA:ephemeral': 0, + 'disk': disk, + 'id': flavor_id, + 'links': [ { - u'href': u'{endpoint}/flavors/{id}'.format( + 'href': '{endpoint}/flavors/{id}'.format( endpoint=COMPUTE_ENDPOINT, id=flavor_id ), - u'rel': u'self', + 'rel': 'self', }, { - u'href': u'{endpoint}/flavors/{id}'.format( + 'href': '{endpoint}/flavors/{id}'.format( endpoint=COMPUTE_ENDPOINT, id=flavor_id ), - u'rel': u'bookmark', + 'rel': 'bookmark', }, ], - u'name': name, - u'os-flavor-access:is_public': True, - u'ram': ram, - u'rxtx_factor': 1.0, - u'swap': 0, - u'vcpus': vcpus, + 'name': name, + 'os-flavor-access:is_public': True, + 'ram': ram, + 'rxtx_factor': 1.0, + 'swap': 0, + 'vcpus': vcpus, } @@ -251,9 +251,9 @@ def make_fake_image( md5=NO_MD5, sha256=NO_SHA256, status='active', - image_name=u'fake_image', + image_name='fake_image', data=None, - checksum=u'ee36e35a297980dee1b514de9803ec6d', + checksum='ee36e35a297980dee1b514de9803ec6d', ): if data: md5 = utils.md5(usedforsecurity=False) @@ -265,34 +265,34 @@ def make_fake_image( md5 = md5.hexdigest() sha256 = sha256.hexdigest() return { - u'image_state': u'available', - u'container_format': u'bare', - u'min_ram': 0, - u'ramdisk_id': 'fake_ramdisk_id', - u'updated_at': u'2016-02-10T05:05:02Z', - u'file': '/v2/images/' + image_id + '/file', - u'size': 3402170368, - u'image_type': u'snapshot', - u'disk_format': u'qcow2', - u'id': image_id, - u'schema': u'/v2/schemas/image', - u'status': status, - u'tags': [], - u'visibility': u'private', - u'locations': [ - {u'url': u'http://127.0.0.1/images/' + image_id, u'metadata': {}} + 'image_state': 'available', + 'container_format': 'bare', + 'min_ram': 0, + 'ramdisk_id': 'fake_ramdisk_id', + 'updated_at': '2016-02-10T05:05:02Z', + 'file': '/v2/images/' + image_id + '/file', + 'size': 3402170368, + 'image_type': 'snapshot', + 'disk_format': 'qcow2', + 'id': image_id, + 'schema': '/v2/schemas/image', + 'status': status, + 'tags': [], + 'visibility': 'private', + 'locations': [ + {'url': 'http://127.0.0.1/images/' + image_id, 'metadata': {}} ], - u'min_disk': 40, - u'virtual_size': None, - u'name': image_name, - u'checksum': md5 or checksum, - u'created_at': u'2016-02-10T05:03:11Z', - u'owner_specified.openstack.md5': md5 or NO_MD5, - u'owner_specified.openstack.sha256': sha256 or NO_SHA256, - u'owner_specified.openstack.object': 'images/{name}'.format( + 'min_disk': 40, + 'virtual_size': None, + 'name': image_name, + 'checksum': md5 or checksum, + 'created_at': '2016-02-10T05:03:11Z', + 'owner_specified.openstack.md5': md5 or NO_MD5, + 'owner_specified.openstack.sha256': sha256 or NO_SHA256, + 'owner_specified.openstack.object': 'images/{name}'.format( name=image_name ), - u'protected': False, + 'protected': False, } diff --git a/openstack/tests/functional/baremetal/base.py b/openstack/tests/functional/baremetal/base.py index cb22f73e1..2f08576ef 100644 --- a/openstack/tests/functional/baremetal/base.py +++ b/openstack/tests/functional/baremetal/base.py @@ -18,7 +18,7 @@ class BaseBaremetalTest(base.BaseFunctionalTest): node_id = None def setUp(self): - super(BaseBaremetalTest, self).setUp() + super().setUp() self.require_service( 'baremetal', min_microversion=self.min_microversion ) diff --git a/openstack/tests/functional/baremetal/test_baremetal_allocation.py b/openstack/tests/functional/baremetal/test_baremetal_allocation.py index aea6d381f..a74db925c 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_allocation.py +++ b/openstack/tests/functional/baremetal/test_baremetal_allocation.py @@ -18,7 +18,7 @@ class Base(base.BaseBaremetalTest): def setUp(self): - super(Base, self).setUp() + super().setUp() # NOTE(dtantsur): generate a unique resource class to prevent parallel # tests from clashing. self.resource_class = 'baremetal-%d' % random.randrange(1024) diff --git a/openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py b/openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py index 565a10bb2..bf40c4b4e 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py +++ b/openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py @@ -18,7 +18,7 @@ class TestBareMetalDeployTemplate(base.BaseBaremetalTest): min_microversion = '1.55' def setUp(self): - super(TestBareMetalDeployTemplate, self).setUp() + super().setUp() def test_baremetal_deploy_create_get_delete(self): steps = [ diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index e6d2269ef..78d06e2ce 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -405,7 +405,7 @@ class TestBareMetalVif(base.BaseBaremetalTest): min_microversion = '1.28' def setUp(self): - super(TestBareMetalVif, self).setUp() + super().setUp() self.node = self.create_node(network_interface='noop') self.vif_id = "200712fc-fdfb-47da-89a6-2d19f76c7618" @@ -445,7 +445,7 @@ class TestTraits(base.BaseBaremetalTest): min_microversion = '1.37' def setUp(self): - super(TestTraits, self).setUp() + super().setUp() self.node = self.create_node() def test_add_remove_node_trait(self): diff --git a/openstack/tests/functional/baremetal/test_baremetal_port.py b/openstack/tests/functional/baremetal/test_baremetal_port.py index cf7831378..e704146c7 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_port.py +++ b/openstack/tests/functional/baremetal/test_baremetal_port.py @@ -17,7 +17,7 @@ class TestBareMetalPort(base.BaseBaremetalTest): def setUp(self): - super(TestBareMetalPort, self).setUp() + super().setUp() self.node = self.create_node() def test_port_create_get_delete(self): diff --git a/openstack/tests/functional/baremetal/test_baremetal_port_group.py b/openstack/tests/functional/baremetal/test_baremetal_port_group.py index b3a60b98e..752f27202 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_port_group.py +++ b/openstack/tests/functional/baremetal/test_baremetal_port_group.py @@ -19,7 +19,7 @@ class TestBareMetalPortGroup(base.BaseBaremetalTest): min_microversion = '1.23' def setUp(self): - super(TestBareMetalPortGroup, self).setUp() + super().setUp() self.node = self.create_node() def test_port_group_create_get_delete(self): diff --git a/openstack/tests/functional/baremetal/test_baremetal_volume_connector.py b/openstack/tests/functional/baremetal/test_baremetal_volume_connector.py index f8cd86597..bf3e70bfb 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_volume_connector.py +++ b/openstack/tests/functional/baremetal/test_baremetal_volume_connector.py @@ -19,7 +19,7 @@ class TestBareMetalVolumeconnector(base.BaseBaremetalTest): min_microversion = '1.32' def setUp(self): - super(TestBareMetalVolumeconnector, self).setUp() + super().setUp() self.node = self.create_node(provision_state='enroll') def test_volume_connector_create_get_delete(self): diff --git a/openstack/tests/functional/baremetal/test_baremetal_volume_target.py b/openstack/tests/functional/baremetal/test_baremetal_volume_target.py index 77dc24ee7..d6e9d8f0f 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_volume_target.py +++ b/openstack/tests/functional/baremetal/test_baremetal_volume_target.py @@ -19,7 +19,7 @@ class TestBareMetalVolumetarget(base.BaseBaremetalTest): min_microversion = '1.32' def setUp(self): - super(TestBareMetalVolumetarget, self).setUp() + super().setUp() self.node = self.create_node(provision_state='enroll') def test_volume_target_create_get_delete(self): diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index 3bc4b41aa..6d6f80bac 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -44,7 +44,7 @@ class BaseFunctionalTest(base.TestCase): _wait_for_timeout_key = '' def setUp(self): - super(BaseFunctionalTest, self).setUp() + super().setUp() self.conn = connection.Connection(config=TEST_CLOUD_REGION) _disable_keep_alive(self.conn) @@ -249,7 +249,7 @@ def getUniqueString(self, prefix=None): class KeystoneBaseFunctionalTest(BaseFunctionalTest): def setUp(self): - super(KeystoneBaseFunctionalTest, self).setUp() + super().setUp() use_keystone_v2 = os.environ.get('OPENSTACKSDK_USE_KEYSTONE_V2', False) if use_keystone_v2: diff --git a/openstack/tests/functional/block_storage/v2/base.py b/openstack/tests/functional/block_storage/v2/base.py index e69bd4d62..4bdcd2454 100644 --- a/openstack/tests/functional/block_storage/v2/base.py +++ b/openstack/tests/functional/block_storage/v2/base.py @@ -17,7 +17,7 @@ class BaseBlockStorageTest(base.BaseFunctionalTest): _wait_for_timeout_key = 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_BLOCK_STORAGE' def setUp(self): - super(BaseBlockStorageTest, self).setUp() + super().setUp() self._set_user_cloud(block_storage_api_version='2') self._set_operator_cloud(block_storage_api_version='2') diff --git a/openstack/tests/functional/block_storage/v2/test_backup.py b/openstack/tests/functional/block_storage/v2/test_backup.py index 6eb52c767..9b2fe7944 100644 --- a/openstack/tests/functional/block_storage/v2/test_backup.py +++ b/openstack/tests/functional/block_storage/v2/test_backup.py @@ -17,7 +17,7 @@ class TestBackup(base.BaseBlockStorageTest): def setUp(self): - super(TestBackup, self).setUp() + super().setUp() if not self.user_cloud.has_service('object-store'): self.skipTest('Object service is requred, but not available') @@ -62,7 +62,7 @@ def tearDown(self): self.VOLUME_ID, ignore_missing=False ) self.assertIsNone(sot) - super(TestBackup, self).tearDown() + super().tearDown() def test_get(self): sot = self.user_cloud.block_storage.get_backup(self.BACKUP_ID) diff --git a/openstack/tests/functional/block_storage/v2/test_snapshot.py b/openstack/tests/functional/block_storage/v2/test_snapshot.py index 600fe773d..42c4bcc42 100644 --- a/openstack/tests/functional/block_storage/v2/test_snapshot.py +++ b/openstack/tests/functional/block_storage/v2/test_snapshot.py @@ -18,7 +18,7 @@ class TestSnapshot(base.BaseBlockStorageTest): def setUp(self): - super(TestSnapshot, self).setUp() + super().setUp() self.SNAPSHOT_NAME = self.getUniqueString() self.SNAPSHOT_ID = None @@ -65,7 +65,7 @@ def tearDown(self): self.VOLUME_ID, ignore_missing=False ) self.assertIsNone(sot) - super(TestSnapshot, self).tearDown() + super().tearDown() def test_get(self): sot = self.user_cloud.block_storage.get_snapshot(self.SNAPSHOT_ID) diff --git a/openstack/tests/functional/block_storage/v2/test_stats.py b/openstack/tests/functional/block_storage/v2/test_stats.py index 5c8abf3b7..a76db0416 100644 --- a/openstack/tests/functional/block_storage/v2/test_stats.py +++ b/openstack/tests/functional/block_storage/v2/test_stats.py @@ -17,7 +17,7 @@ class TestStats(base.BaseBlockStorageTest): def setUp(self): - super(TestStats, self).setUp() + super().setUp() sot = self.operator_cloud.block_storage.backend_pools() for pool in sot: diff --git a/openstack/tests/functional/block_storage/v2/test_type.py b/openstack/tests/functional/block_storage/v2/test_type.py index a15cede3a..f1e54ebf3 100644 --- a/openstack/tests/functional/block_storage/v2/test_type.py +++ b/openstack/tests/functional/block_storage/v2/test_type.py @@ -17,7 +17,7 @@ class TestType(base.BaseBlockStorageTest): def setUp(self): - super(TestType, self).setUp() + super().setUp() self.TYPE_NAME = self.getUniqueString() self.TYPE_ID = None @@ -34,7 +34,7 @@ def tearDown(self): self.TYPE_ID, ignore_missing=False ) self.assertIsNone(sot) - super(TestType, self).tearDown() + super().tearDown() def test_get(self): sot = self.operator_cloud.block_storage.get_type(self.TYPE_ID) diff --git a/openstack/tests/functional/block_storage/v2/test_volume.py b/openstack/tests/functional/block_storage/v2/test_volume.py index 13f76592a..9cb3e13d6 100644 --- a/openstack/tests/functional/block_storage/v2/test_volume.py +++ b/openstack/tests/functional/block_storage/v2/test_volume.py @@ -16,7 +16,7 @@ class TestVolume(base.BaseBlockStorageTest): def setUp(self): - super(TestVolume, self).setUp() + super().setUp() if not self.user_cloud.has_service('block-storage'): self.skipTest('block-storage service not supported by cloud') @@ -43,7 +43,7 @@ def tearDown(self): self.VOLUME_ID, ignore_missing=False ) self.assertIsNone(sot) - super(TestVolume, self).tearDown() + super().tearDown() def test_get(self): sot = self.user_cloud.block_storage.get_volume(self.VOLUME_ID) diff --git a/openstack/tests/functional/block_storage/v3/base.py b/openstack/tests/functional/block_storage/v3/base.py index 39543c247..d53a68792 100644 --- a/openstack/tests/functional/block_storage/v3/base.py +++ b/openstack/tests/functional/block_storage/v3/base.py @@ -17,7 +17,7 @@ class BaseBlockStorageTest(base.BaseFunctionalTest): _wait_for_timeout_key = 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_BLOCK_STORAGE' def setUp(self): - super(BaseBlockStorageTest, self).setUp() + super().setUp() self._set_user_cloud(block_storage_api_version='3') if not self.user_cloud.has_service('block-storage', '3'): self.skipTest('block-storage service not supported by cloud') diff --git a/openstack/tests/functional/block_storage/v3/test_backup.py b/openstack/tests/functional/block_storage/v3/test_backup.py index d791bfb42..6dcde68cc 100644 --- a/openstack/tests/functional/block_storage/v3/test_backup.py +++ b/openstack/tests/functional/block_storage/v3/test_backup.py @@ -17,7 +17,7 @@ class TestBackup(base.BaseBlockStorageTest): def setUp(self): - super(TestBackup, self).setUp() + super().setUp() if not self.user_cloud.has_service('object-store'): self.skipTest('Object service is requred, but not available') @@ -62,7 +62,7 @@ def tearDown(self): self.VOLUME_ID, ignore_missing=False ) self.assertIsNone(sot) - super(TestBackup, self).tearDown() + super().tearDown() def test_get(self): sot = self.user_cloud.block_storage.get_backup(self.BACKUP_ID) diff --git a/openstack/tests/functional/block_storage/v3/test_snapshot.py b/openstack/tests/functional/block_storage/v3/test_snapshot.py index 1c96ea26e..a20816d77 100644 --- a/openstack/tests/functional/block_storage/v3/test_snapshot.py +++ b/openstack/tests/functional/block_storage/v3/test_snapshot.py @@ -18,7 +18,7 @@ class TestSnapshot(base.BaseBlockStorageTest): def setUp(self): - super(TestSnapshot, self).setUp() + super().setUp() self.SNAPSHOT_NAME = self.getUniqueString() self.SNAPSHOT_ID = None @@ -65,7 +65,7 @@ def tearDown(self): self.VOLUME_ID, ignore_missing=False ) self.assertIsNone(sot) - super(TestSnapshot, self).tearDown() + super().tearDown() def test_get(self): sot = self.user_cloud.block_storage.get_snapshot(self.SNAPSHOT_ID) diff --git a/openstack/tests/functional/block_storage/v3/test_type.py b/openstack/tests/functional/block_storage/v3/test_type.py index 97156f3c1..1bce5096e 100644 --- a/openstack/tests/functional/block_storage/v3/test_type.py +++ b/openstack/tests/functional/block_storage/v3/test_type.py @@ -17,7 +17,7 @@ class TestType(base.BaseBlockStorageTest): def setUp(self): - super(TestType, self).setUp() + super().setUp() self.TYPE_NAME = self.getUniqueString() self.TYPE_ID = None @@ -36,7 +36,7 @@ def tearDown(self): self.TYPE_ID, ignore_missing=False ) self.assertIsNone(sot) - super(TestType, self).tearDown() + super().tearDown() def test_get(self): sot = self.operator_cloud.block_storage.get_type(self.TYPE_ID) diff --git a/openstack/tests/functional/cloud/test_cluster_templates.py b/openstack/tests/functional/cloud/test_cluster_templates.py index 897950a22..a52f3283a 100644 --- a/openstack/tests/functional/cloud/test_cluster_templates.py +++ b/openstack/tests/functional/cloud/test_cluster_templates.py @@ -27,7 +27,7 @@ class TestClusterTemplate(base.BaseFunctionalTest): def setUp(self): - super(TestClusterTemplate, self).setUp() + super().setUp() if not self.user_cloud.has_service( 'container-infrastructure-management' ): diff --git a/openstack/tests/functional/cloud/test_clustering.py b/openstack/tests/functional/cloud/test_clustering.py index dcb273535..c609fa7ac 100644 --- a/openstack/tests/functional/cloud/test_clustering.py +++ b/openstack/tests/functional/cloud/test_clustering.py @@ -108,7 +108,7 @@ def wait_for_delete(client, client_args, check_interval=1, timeout=60): class TestClustering(base.BaseFunctionalTest): def setUp(self): - super(TestClustering, self).setUp() + super().setUp() self.skipTest('clustering service not supported by cloud') def test_create_profile(self): diff --git a/openstack/tests/functional/cloud/test_compute.py b/openstack/tests/functional/cloud/test_compute.py index 48930528e..c7e270f4a 100644 --- a/openstack/tests/functional/cloud/test_compute.py +++ b/openstack/tests/functional/cloud/test_compute.py @@ -32,7 +32,7 @@ def setUp(self): # but on a bad day, test_attach_detach_volume can take more time. self.TIMEOUT_SCALING_FACTOR = 1.5 - super(TestCompute, self).setUp() + super().setUp() self.server_name = self.getUniqueString() def _cleanup_servers_and_volumes(self, server_name): @@ -522,7 +522,7 @@ def test_set_and_delete_metadata(self): self.user_cloud.delete_server_metadata(self.server_name, ['key1']) updated_server = self.user_cloud.get_server(self.server_name) - self.assertEqual(set(updated_server.metadata.items()), set([])) + self.assertEqual(set(updated_server.metadata.items()), set()) self.assertRaises( exceptions.NotFoundException, diff --git a/openstack/tests/functional/cloud/test_devstack.py b/openstack/tests/functional/cloud/test_devstack.py index e5251e942..7170adfe1 100644 --- a/openstack/tests/functional/cloud/test_devstack.py +++ b/openstack/tests/functional/cloud/test_devstack.py @@ -39,8 +39,5 @@ class TestDevstack(base.BaseFunctionalTest): ] def test_has_service(self): - if ( - os.environ.get('OPENSTACKSDK_HAS_{env}'.format(env=self.env), '0') - == '1' - ): + if os.environ.get(f'OPENSTACKSDK_HAS_{self.env}', '0') == '1': self.assertTrue(self.user_cloud.has_service(self.service)) diff --git a/openstack/tests/functional/cloud/test_domain.py b/openstack/tests/functional/cloud/test_domain.py index b56943202..7cbd4b828 100644 --- a/openstack/tests/functional/cloud/test_domain.py +++ b/openstack/tests/functional/cloud/test_domain.py @@ -23,7 +23,7 @@ class TestDomain(base.BaseFunctionalTest): def setUp(self): - super(TestDomain, self).setUp() + super().setUp() if not self.operator_cloud: self.skipTest("Operator cloud is required for this test") i_ver = self.operator_cloud.config.get_api_version('identity') diff --git a/openstack/tests/functional/cloud/test_endpoints.py b/openstack/tests/functional/cloud/test_endpoints.py index e53fc7df7..b21bd8d27 100644 --- a/openstack/tests/functional/cloud/test_endpoints.py +++ b/openstack/tests/functional/cloud/test_endpoints.py @@ -38,7 +38,7 @@ class TestEndpoints(base.KeystoneBaseFunctionalTest): ] def setUp(self): - super(TestEndpoints, self).setUp() + super().setUp() if not self.operator_cloud: self.skipTest("Operator cloud is required for this test") diff --git a/openstack/tests/functional/cloud/test_flavor.py b/openstack/tests/functional/cloud/test_flavor.py index 29d2fd555..6181c9727 100644 --- a/openstack/tests/functional/cloud/test_flavor.py +++ b/openstack/tests/functional/cloud/test_flavor.py @@ -25,7 +25,7 @@ class TestFlavor(base.BaseFunctionalTest): def setUp(self): - super(TestFlavor, self).setUp() + super().setUp() # Generate a random name for flavors in this test self.new_item_name = self.getUniqueString('flavor') diff --git a/openstack/tests/functional/cloud/test_floating_ip_pool.py b/openstack/tests/functional/cloud/test_floating_ip_pool.py index 30e84fc3a..c963aa7b0 100644 --- a/openstack/tests/functional/cloud/test_floating_ip_pool.py +++ b/openstack/tests/functional/cloud/test_floating_ip_pool.py @@ -33,7 +33,7 @@ class TestFloatingIPPool(base.BaseFunctionalTest): def setUp(self): - super(TestFloatingIPPool, self).setUp() + super().setUp() if not self.user_cloud._has_nova_extension('os-floating-ip-pools'): # Skipping this test is floating-ip-pool extension is not diff --git a/openstack/tests/functional/cloud/test_groups.py b/openstack/tests/functional/cloud/test_groups.py index 1e4219d49..6a9dd0ab4 100644 --- a/openstack/tests/functional/cloud/test_groups.py +++ b/openstack/tests/functional/cloud/test_groups.py @@ -23,7 +23,7 @@ class TestGroup(base.BaseFunctionalTest): def setUp(self): - super(TestGroup, self).setUp() + super().setUp() if not self.operator_cloud: self.skipTest("Operator cloud is required for this test") diff --git a/openstack/tests/functional/cloud/test_identity.py b/openstack/tests/functional/cloud/test_identity.py index b8a8df251..ae8b8ce3e 100644 --- a/openstack/tests/functional/cloud/test_identity.py +++ b/openstack/tests/functional/cloud/test_identity.py @@ -26,7 +26,7 @@ class TestIdentity(base.KeystoneBaseFunctionalTest): def setUp(self): - super(TestIdentity, self).setUp() + super().setUp() if not self.operator_cloud: self.skipTest("Operator cloud is required for this test") self.role_prefix = 'test_role' + ''.join( diff --git a/openstack/tests/functional/cloud/test_magnum_services.py b/openstack/tests/functional/cloud/test_magnum_services.py index c3a526f72..63ef86bde 100644 --- a/openstack/tests/functional/cloud/test_magnum_services.py +++ b/openstack/tests/functional/cloud/test_magnum_services.py @@ -22,7 +22,7 @@ class TestMagnumServices(base.BaseFunctionalTest): def setUp(self): - super(TestMagnumServices, self).setUp() + super().setUp() if not self.user_cloud.has_service( 'container-infrastructure-management' ): diff --git a/openstack/tests/functional/cloud/test_network.py b/openstack/tests/functional/cloud/test_network.py index 9e170afe5..1c596d769 100644 --- a/openstack/tests/functional/cloud/test_network.py +++ b/openstack/tests/functional/cloud/test_network.py @@ -23,7 +23,7 @@ class TestNetwork(base.BaseFunctionalTest): def setUp(self): - super(TestNetwork, self).setUp() + super().setUp() if not self.operator_cloud: self.skipTest("Operator cloud is required for this test") diff --git a/openstack/tests/functional/cloud/test_object.py b/openstack/tests/functional/cloud/test_object.py index 88e373d4c..8edd4e965 100644 --- a/openstack/tests/functional/cloud/test_object.py +++ b/openstack/tests/functional/cloud/test_object.py @@ -29,7 +29,7 @@ class TestObject(base.BaseFunctionalTest): def setUp(self): - super(TestObject, self).setUp() + super().setUp() if not self.user_cloud.has_service('object-store'): self.skipTest('Object service not supported by cloud') diff --git a/openstack/tests/functional/cloud/test_port.py b/openstack/tests/functional/cloud/test_port.py index ea407d453..3d46e8d31 100644 --- a/openstack/tests/functional/cloud/test_port.py +++ b/openstack/tests/functional/cloud/test_port.py @@ -28,7 +28,7 @@ class TestPort(base.BaseFunctionalTest): def setUp(self): - super(TestPort, self).setUp() + super().setUp() # Skip Neutron tests if neutron is not present if not self.user_cloud.has_service('network'): self.skipTest('Network service not supported by cloud') @@ -118,13 +118,13 @@ def test_update_port(self): updated_port = self.user_cloud.get_port(name_or_id=port['id']) self.assertEqual(port.get('name'), new_port_name) port.pop('revision_number', None) - port.pop(u'revision_number', None) + port.pop('revision_number', None) + port.pop('updated_at', None) port.pop('updated_at', None) - port.pop(u'updated_at', None) updated_port.pop('revision_number', None) - updated_port.pop(u'revision_number', None) + updated_port.pop('revision_number', None) + updated_port.pop('updated_at', None) updated_port.pop('updated_at', None) - updated_port.pop(u'updated_at', None) self.assertEqual(port, updated_port) diff --git a/openstack/tests/functional/cloud/test_project.py b/openstack/tests/functional/cloud/test_project.py index 421c5a7d5..d92abdb1d 100644 --- a/openstack/tests/functional/cloud/test_project.py +++ b/openstack/tests/functional/cloud/test_project.py @@ -26,7 +26,7 @@ class TestProject(base.KeystoneBaseFunctionalTest): def setUp(self): - super(TestProject, self).setUp() + super().setUp() if not self.operator_cloud: self.skipTest("Operator cloud is required for this test") diff --git a/openstack/tests/functional/cloud/test_project_cleanup.py b/openstack/tests/functional/cloud/test_project_cleanup.py index b5a9a8c28..13d9a38ef 100644 --- a/openstack/tests/functional/cloud/test_project_cleanup.py +++ b/openstack/tests/functional/cloud/test_project_cleanup.py @@ -25,7 +25,7 @@ class TestProjectCleanup(base.BaseFunctionalTest): _wait_for_timeout_key = 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_CLEANUP' def setUp(self): - super(TestProjectCleanup, self).setUp() + super().setUp() if not self.user_cloud_alt: self.skipTest("Alternate demo cloud is required for this test") diff --git a/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py b/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py index 2d729e7b5..09334afad 100644 --- a/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py @@ -24,7 +24,7 @@ class TestQosBandwidthLimitRule(base.BaseFunctionalTest): def setUp(self): - super(TestQosBandwidthLimitRule, self).setUp() + super().setUp() if not self.operator_cloud: self.skipTest("Operator cloud is required for this test") if not self.operator_cloud.has_service('network'): diff --git a/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py b/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py index 0ce43e896..ee6fba631 100644 --- a/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py +++ b/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py @@ -24,7 +24,7 @@ class TestQosDscpMarkingRule(base.BaseFunctionalTest): def setUp(self): - super(TestQosDscpMarkingRule, self).setUp() + super().setUp() if not self.operator_cloud: self.skipTest("Operator cloud is required for this test") if not self.operator_cloud.has_service('network'): diff --git a/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py b/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py index 2810f67aa..8da1ff651 100644 --- a/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py +++ b/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py @@ -24,7 +24,7 @@ class TestQosMinimumBandwidthRule(base.BaseFunctionalTest): def setUp(self): - super(TestQosMinimumBandwidthRule, self).setUp() + super().setUp() if not self.operator_cloud: self.skipTest("Operator cloud is required for this test") if not self.operator_cloud.has_service('network'): diff --git a/openstack/tests/functional/cloud/test_qos_policy.py b/openstack/tests/functional/cloud/test_qos_policy.py index 002f49757..03a2f3c3c 100644 --- a/openstack/tests/functional/cloud/test_qos_policy.py +++ b/openstack/tests/functional/cloud/test_qos_policy.py @@ -24,7 +24,7 @@ class TestQosPolicy(base.BaseFunctionalTest): def setUp(self): - super(TestQosPolicy, self).setUp() + super().setUp() if not self.operator_cloud: self.skipTest("Operator cloud is required for this test") if not self.operator_cloud.has_service('network'): diff --git a/openstack/tests/functional/cloud/test_quotas.py b/openstack/tests/functional/cloud/test_quotas.py index 87e96b5fd..0db5878bd 100644 --- a/openstack/tests/functional/cloud/test_quotas.py +++ b/openstack/tests/functional/cloud/test_quotas.py @@ -44,7 +44,7 @@ def test_set_quotas(self): class TestVolumeQuotas(base.BaseFunctionalTest): def setUp(self): - super(TestVolumeQuotas, self).setUp() + super().setUp() if not self.user_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') diff --git a/openstack/tests/functional/cloud/test_recordset.py b/openstack/tests/functional/cloud/test_recordset.py index 15574238e..98f8d6564 100644 --- a/openstack/tests/functional/cloud/test_recordset.py +++ b/openstack/tests/functional/cloud/test_recordset.py @@ -26,7 +26,7 @@ class TestRecordset(base.BaseFunctionalTest): def setUp(self): - super(TestRecordset, self).setUp() + super().setUp() if not self.user_cloud.has_service('dns'): self.skipTest('dns service not supported by cloud') diff --git a/openstack/tests/functional/cloud/test_router.py b/openstack/tests/functional/cloud/test_router.py index 1968492a3..a805df895 100644 --- a/openstack/tests/functional/cloud/test_router.py +++ b/openstack/tests/functional/cloud/test_router.py @@ -38,7 +38,7 @@ class TestRouter(base.BaseFunctionalTest): def setUp(self): - super(TestRouter, self).setUp() + super().setUp() if not self.operator_cloud: self.skipTest("Operator cloud required for this test") if not self.operator_cloud.has_service('network'): @@ -200,11 +200,11 @@ def _create_and_verify_advanced_router( return router def test_create_router_advanced(self): - self._create_and_verify_advanced_router(external_cidr=u'10.2.2.0/24') + self._create_and_verify_advanced_router(external_cidr='10.2.2.0/24') def test_add_remove_router_interface(self): router = self._create_and_verify_advanced_router( - external_cidr=u'10.3.3.0/24' + external_cidr='10.3.3.0/24' ) net_name = self.network_prefix + '_intnet1' sub_name = self.subnet_prefix + '_intsub1' @@ -235,7 +235,7 @@ def test_add_remove_router_interface(self): def test_list_router_interfaces(self): router = self._create_and_verify_advanced_router( - external_cidr=u'10.5.5.0/24' + external_cidr='10.5.5.0/24' ) net_name = self.network_prefix + '_intnet1' sub_name = self.subnet_prefix + '_intsub1' @@ -279,7 +279,7 @@ def test_list_router_interfaces(self): def test_update_router_name(self): router = self._create_and_verify_advanced_router( - external_cidr=u'10.7.7.0/24' + external_cidr='10.7.7.0/24' ) new_name = self.router_prefix + '_update_name' @@ -303,7 +303,7 @@ def test_update_router_name(self): def test_update_router_routes(self): router = self._create_and_verify_advanced_router( - external_cidr=u'10.7.7.0/24' + external_cidr='10.7.7.0/24' ) routes = [{"destination": "10.7.7.0/24", "nexthop": "10.7.7.99"}] @@ -328,7 +328,7 @@ def test_update_router_routes(self): def test_update_router_admin_state(self): router = self._create_and_verify_advanced_router( - external_cidr=u'10.8.8.0/24' + external_cidr='10.8.8.0/24' ) updated = self.operator_cloud.update_router( @@ -354,7 +354,7 @@ def test_update_router_admin_state(self): def test_update_router_ext_gw_info(self): router = self._create_and_verify_advanced_router( - external_cidr=u'10.9.9.0/24' + external_cidr='10.9.9.0/24' ) # create a new subnet diff --git a/openstack/tests/functional/cloud/test_services.py b/openstack/tests/functional/cloud/test_services.py index c7639af2a..df311d3dc 100644 --- a/openstack/tests/functional/cloud/test_services.py +++ b/openstack/tests/functional/cloud/test_services.py @@ -31,7 +31,7 @@ class TestServices(base.KeystoneBaseFunctionalTest): service_attributes = ['id', 'name', 'type', 'description'] def setUp(self): - super(TestServices, self).setUp() + super().setUp() if not self.operator_cloud: self.skipTest("Operator cloud is required for this test") diff --git a/openstack/tests/functional/cloud/test_stack.py b/openstack/tests/functional/cloud/test_stack.py index 81190a384..1a3a17bdc 100644 --- a/openstack/tests/functional/cloud/test_stack.py +++ b/openstack/tests/functional/cloud/test_stack.py @@ -74,7 +74,7 @@ class TestStack(base.BaseFunctionalTest): def setUp(self): - super(TestStack, self).setUp() + super().setUp() if not self.user_cloud.has_service('orchestration'): self.skipTest('Orchestration service not supported by cloud') diff --git a/openstack/tests/functional/cloud/test_users.py b/openstack/tests/functional/cloud/test_users.py index 08c02ee7d..cdce80bdb 100644 --- a/openstack/tests/functional/cloud/test_users.py +++ b/openstack/tests/functional/cloud/test_users.py @@ -23,7 +23,7 @@ class TestUsers(base.KeystoneBaseFunctionalTest): def setUp(self): - super(TestUsers, self).setUp() + super().setUp() if not self.operator_cloud: self.skipTest("Operator cloud is required for this test") diff --git a/openstack/tests/functional/cloud/test_volume.py b/openstack/tests/functional/cloud/test_volume.py index d8c7df283..9e6326b6d 100644 --- a/openstack/tests/functional/cloud/test_volume.py +++ b/openstack/tests/functional/cloud/test_volume.py @@ -30,7 +30,7 @@ class TestVolume(base.BaseFunctionalTest): TIMEOUT_SCALING_FACTOR = 1.5 def setUp(self): - super(TestVolume, self).setUp() + super().setUp() self.skipTest('Volume functional tests temporarily disabled') if not self.user_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') diff --git a/openstack/tests/functional/cloud/test_volume_backup.py b/openstack/tests/functional/cloud/test_volume_backup.py index ce3c89e1c..d4bd03252 100644 --- a/openstack/tests/functional/cloud/test_volume_backup.py +++ b/openstack/tests/functional/cloud/test_volume_backup.py @@ -17,7 +17,7 @@ class TestVolume(base.BaseFunctionalTest): TIMEOUT_SCALING_FACTOR = 1.5 def setUp(self): - super(TestVolume, self).setUp() + super().setUp() self.skipTest('Volume functional tests temporarily disabled') if not self.user_cloud.has_service('volume'): self.skipTest('volume service not supported by cloud') diff --git a/openstack/tests/functional/cloud/test_volume_type.py b/openstack/tests/functional/cloud/test_volume_type.py index 1e527cba0..52f4fe29d 100644 --- a/openstack/tests/functional/cloud/test_volume_type.py +++ b/openstack/tests/functional/cloud/test_volume_type.py @@ -1,5 +1,3 @@ -# -*- 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 @@ -31,7 +29,7 @@ def _assert_project(self, volume_name_or_id, project_id, allowed=True): self.assertEqual(allowed, project_id in allowed_projects) def setUp(self): - super(TestVolumeType, self).setUp() + super().setUp() if not self.operator_cloud: self.skipTest("Operator cloud is required for this test") if not self.user_cloud.has_service('volume'): @@ -48,10 +46,8 @@ def setUp(self): def tearDown(self): ret = self.operator_cloud.get_volume_type('test-volume-type') if ret.get('id'): - self.operator_cloud.block_storage.delete( - '/types/{volume_type_id}'.format(volume_type_id=ret.id) - ) - super(TestVolumeType, self).tearDown() + self.operator_cloud.block_storage.delete(f'/types/{ret.id}') + super().tearDown() def test_list_volume_types(self): volume_types = self.operator_cloud.list_volume_types() diff --git a/openstack/tests/functional/cloud/test_zone.py b/openstack/tests/functional/cloud/test_zone.py index a0b971b48..0542da694 100644 --- a/openstack/tests/functional/cloud/test_zone.py +++ b/openstack/tests/functional/cloud/test_zone.py @@ -24,7 +24,7 @@ class TestZone(base.BaseFunctionalTest): def setUp(self): - super(TestZone, self).setUp() + super().setUp() if not self.user_cloud.has_service('dns'): self.skipTest('dns service not supported by cloud') diff --git a/openstack/tests/functional/clustering/test_cluster.py b/openstack/tests/functional/clustering/test_cluster.py index 38d2ed144..47739c6cd 100644 --- a/openstack/tests/functional/clustering/test_cluster.py +++ b/openstack/tests/functional/clustering/test_cluster.py @@ -21,7 +21,7 @@ class TestCluster(base.BaseFunctionalTest): _wait_for_timeout_key = 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_CLUSTER' def setUp(self): - super(TestCluster, self).setUp() + super().setUp() self.require_service('clustering') self.cidr = '10.99.99.0/16' @@ -74,7 +74,7 @@ def tearDown(self): self.conn.clustering.delete_profile(self.profile) - super(TestCluster, self).tearDown() + super().tearDown() def test_find(self): sot = self.conn.clustering.find_cluster(self.cluster.id) diff --git a/openstack/tests/functional/compute/v2/test_flavor.py b/openstack/tests/functional/compute/v2/test_flavor.py index 8b72657f8..ca680ffec 100644 --- a/openstack/tests/functional/compute/v2/test_flavor.py +++ b/openstack/tests/functional/compute/v2/test_flavor.py @@ -17,7 +17,7 @@ class TestFlavor(base.BaseFunctionalTest): def setUp(self): - super(TestFlavor, self).setUp() + super().setUp() self.new_item_name = self.getUniqueString('flavor') self.one_flavor = list(self.conn.compute.flavors())[0] diff --git a/openstack/tests/functional/compute/v2/test_hypervisor.py b/openstack/tests/functional/compute/v2/test_hypervisor.py index 2edeffc20..150ee83a5 100644 --- a/openstack/tests/functional/compute/v2/test_hypervisor.py +++ b/openstack/tests/functional/compute/v2/test_hypervisor.py @@ -15,7 +15,7 @@ class TestHypervisor(base.BaseFunctionalTest): def setUp(self): - super(TestHypervisor, self).setUp() + super().setUp() def test_list_hypervisors(self): rslt = list(self.conn.compute.hypervisors()) diff --git a/openstack/tests/functional/compute/v2/test_keypair.py b/openstack/tests/functional/compute/v2/test_keypair.py index d642ca74d..5fba46cc0 100644 --- a/openstack/tests/functional/compute/v2/test_keypair.py +++ b/openstack/tests/functional/compute/v2/test_keypair.py @@ -17,7 +17,7 @@ class TestKeypair(base.BaseFunctionalTest): def setUp(self): - super(TestKeypair, self).setUp() + super().setUp() # Keypairs can't have .'s in the name. Because why? self.NAME = self.getUniqueString().split('.')[-1] @@ -30,7 +30,7 @@ def setUp(self): def tearDown(self): sot = self.conn.compute.delete_keypair(self._keypair) self.assertIsNone(sot) - super(TestKeypair, self).tearDown() + super().tearDown() def test_find(self): sot = self.conn.compute.find_keypair(self.NAME) @@ -50,7 +50,7 @@ def test_list(self): class TestKeypairAdmin(base.BaseFunctionalTest): def setUp(self): - super(TestKeypairAdmin, self).setUp() + super().setUp() self._set_operator_cloud(interface='admin') self.NAME = self.getUniqueString().split('.')[-1] @@ -67,7 +67,7 @@ def setUp(self): def tearDown(self): sot = self.conn.compute.delete_keypair(self._keypair) self.assertIsNone(sot) - super(TestKeypairAdmin, self).tearDown() + super().tearDown() def test_get(self): sot = self.conn.compute.get_keypair(self.NAME) diff --git a/openstack/tests/functional/compute/v2/test_server.py b/openstack/tests/functional/compute/v2/test_server.py index c19566edb..34174a2ed 100644 --- a/openstack/tests/functional/compute/v2/test_server.py +++ b/openstack/tests/functional/compute/v2/test_server.py @@ -17,7 +17,7 @@ class TestServerAdmin(ft_base.BaseComputeTest): def setUp(self): - super(TestServerAdmin, self).setUp() + super().setUp() self._set_operator_cloud(interface='admin') self.NAME = 'needstobeshortandlowercase' self.USERDATA = 'SSdtIGFjdHVhbGx5IGEgZ29hdC4=' @@ -50,7 +50,7 @@ def tearDown(self): self.server, wait=self._wait_for_timeout ) self.assertIsNone(sot) - super(TestServerAdmin, self).tearDown() + super().tearDown() def test_get(self): sot = self.conn.compute.get_server(self.server.id) @@ -66,7 +66,7 @@ def test_get(self): class TestServer(ft_base.BaseComputeTest): def setUp(self): - super(TestServer, self).setUp() + super().setUp() self.NAME = self.getUniqueString() self.server = None self.network = None @@ -97,7 +97,7 @@ def tearDown(self): self.server, wait=self._wait_for_timeout ) test_network.delete_network(self.conn, self.network, self.subnet) - super(TestServer, self).tearDown() + super().tearDown() def test_find(self): sot = self.conn.compute.find_server(self.NAME) diff --git a/openstack/tests/functional/compute/v2/test_service.py b/openstack/tests/functional/compute/v2/test_service.py index 2403aaf0e..271d21cf4 100644 --- a/openstack/tests/functional/compute/v2/test_service.py +++ b/openstack/tests/functional/compute/v2/test_service.py @@ -15,7 +15,7 @@ class TestService(base.BaseFunctionalTest): def setUp(self): - super(TestService, self).setUp() + super().setUp() self._set_operator_cloud(interface='admin') def test_list(self): diff --git a/openstack/tests/functional/dns/v2/test_zone.py b/openstack/tests/functional/dns/v2/test_zone.py index f57fe48d8..417897516 100644 --- a/openstack/tests/functional/dns/v2/test_zone.py +++ b/openstack/tests/functional/dns/v2/test_zone.py @@ -19,7 +19,7 @@ class TestZone(base.BaseFunctionalTest): def setUp(self): - super(TestZone, self).setUp() + super().setUp() self.require_service('dns') self.conn = connection.from_config(cloud_name=base.TEST_CLOUD_NAME) @@ -28,7 +28,7 @@ def setUp(self): # chose a new zone name for a test # getUniqueString is not guaranteed to return unique string between # different tests of the same class. - self.ZONE_NAME = 'example-{0}.org.'.format(random.randint(1, 10000)) + self.ZONE_NAME = f'example-{random.randint(1, 10000)}.org.' self.zone = self.conn.dns.create_zone( name=self.ZONE_NAME, @@ -64,7 +64,7 @@ def test_create_rs(self): self.assertIsNotNone( self.conn.dns.create_recordset( zone=zone, - name='www.{zone}'.format(zone=zone.name), + name=f'www.{zone.name}', type='A', description='Example zone rec', ttl=3600, @@ -79,7 +79,7 @@ def test_delete_zone_with_shares(self): 'Designate API version does not support shared zones.' ) - zone_name = 'example-{0}.org.'.format(random.randint(1, 10000)) + zone_name = f'example-{random.randint(1, 10000)}.org.' zone = self.conn.dns.create_zone( name=zone_name, email='joe@example.org', diff --git a/openstack/tests/functional/dns/v2/test_zone_share.py b/openstack/tests/functional/dns/v2/test_zone_share.py index 01870988e..53a9c766f 100644 --- a/openstack/tests/functional/dns/v2/test_zone_share.py +++ b/openstack/tests/functional/dns/v2/test_zone_share.py @@ -19,7 +19,7 @@ class TestZoneShare(base.BaseFunctionalTest): def setUp(self): - super(TestZoneShare, self).setUp() + super().setUp() self.require_service('dns') if not self.user_cloud: self.skipTest("The demo cloud is required for this test") @@ -28,7 +28,7 @@ def setUp(self): # chose a new zone name for a test # getUniqueString is not guaranteed to return unique string between # different tests of the same class. - self.ZONE_NAME = 'example-{0}.org.'.format(uuid.uuid4().hex) + self.ZONE_NAME = f'example-{uuid.uuid4().hex}.org.' # Make sure the API under test has shared zones support if not utils.supports_version(self.conn.dns, '2.1'): diff --git a/openstack/tests/functional/examples/test_compute.py b/openstack/tests/functional/examples/test_compute.py index 888348ef9..2b0ba6b7b 100644 --- a/openstack/tests/functional/examples/test_compute.py +++ b/openstack/tests/functional/examples/test_compute.py @@ -29,7 +29,7 @@ class TestCompute(base.BaseFunctionalTest): """ def setUp(self): - super(TestCompute, self).setUp() + super().setUp() self.conn = connect.create_connection_from_config() def test_compute(self): diff --git a/openstack/tests/functional/examples/test_identity.py b/openstack/tests/functional/examples/test_identity.py index 5ee731771..a779eb912 100644 --- a/openstack/tests/functional/examples/test_identity.py +++ b/openstack/tests/functional/examples/test_identity.py @@ -24,7 +24,7 @@ class TestIdentity(base.BaseFunctionalTest): """ def setUp(self): - super(TestIdentity, self).setUp() + super().setUp() self.conn = connect.create_connection_from_config() def test_identity(self): diff --git a/openstack/tests/functional/examples/test_image.py b/openstack/tests/functional/examples/test_image.py index c10054b41..5dfd6c7bb 100644 --- a/openstack/tests/functional/examples/test_image.py +++ b/openstack/tests/functional/examples/test_image.py @@ -26,7 +26,7 @@ class TestImage(base.BaseFunctionalTest): """ def setUp(self): - super(TestImage, self).setUp() + super().setUp() self.conn = connect.create_connection_from_config() def test_image(self): diff --git a/openstack/tests/functional/examples/test_network.py b/openstack/tests/functional/examples/test_network.py index 7143c447c..e6064b919 100644 --- a/openstack/tests/functional/examples/test_network.py +++ b/openstack/tests/functional/examples/test_network.py @@ -27,7 +27,7 @@ class TestNetwork(base.BaseFunctionalTest): """ def setUp(self): - super(TestNetwork, self).setUp() + super().setUp() self.conn = connect.create_connection_from_config() def test_network(self): diff --git a/openstack/tests/functional/identity/v3/test_application_credential.py b/openstack/tests/functional/identity/v3/test_application_credential.py index cce6670bf..f05db4b4e 100644 --- a/openstack/tests/functional/identity/v3/test_application_credential.py +++ b/openstack/tests/functional/identity/v3/test_application_credential.py @@ -16,7 +16,7 @@ class TestApplicationCredentials(base.BaseFunctionalTest): def setUp(self): - super(TestApplicationCredentials, self).setUp() + super().setUp() self.user_id = self.operator_cloud.current_user_id def _create_application_credentials(self): diff --git a/openstack/tests/functional/image/v2/test_image.py b/openstack/tests/functional/image/v2/test_image.py index 7b7829c8d..6590bddd1 100644 --- a/openstack/tests/functional/image/v2/test_image.py +++ b/openstack/tests/functional/image/v2/test_image.py @@ -30,7 +30,7 @@ def setUp(self): properties={ 'description': 'This is not an image', }, - data=open('CONTRIBUTING.rst', 'r'), + data=open('CONTRIBUTING.rst'), ) self.assertIsInstance(self.image, _image.Image) self.assertEqual(TEST_IMAGE_NAME, self.image.name) diff --git a/openstack/tests/functional/instance_ha/test_host.py b/openstack/tests/functional/instance_ha/test_host.py index bd6f0f4b2..f307c74cf 100644 --- a/openstack/tests/functional/instance_ha/test_host.py +++ b/openstack/tests/functional/instance_ha/test_host.py @@ -32,7 +32,7 @@ def hypervisors(): class TestHost(base.BaseFunctionalTest): def setUp(self): - super(TestHost, self).setUp() + super().setUp() self.require_service('instance-ha') self.NAME = self.getUniqueString() diff --git a/openstack/tests/functional/instance_ha/test_segment.py b/openstack/tests/functional/instance_ha/test_segment.py index a99d616e2..1f8611562 100644 --- a/openstack/tests/functional/instance_ha/test_segment.py +++ b/openstack/tests/functional/instance_ha/test_segment.py @@ -18,7 +18,7 @@ class TestSegment(base.BaseFunctionalTest): def setUp(self): - super(TestSegment, self).setUp() + super().setUp() self.require_service('instance-ha') self.NAME = self.getUniqueString() diff --git a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py index efa87058a..f158e6008 100644 --- a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py +++ b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py @@ -64,7 +64,7 @@ class TestLoadBalancer(base.BaseFunctionalTest): # use setUpClass, but that's a whole other pile of bad, so # we may need to engineer something pleasing here. def setUp(self): - super(TestLoadBalancer, self).setUp() + super().setUp() self.require_service('load-balancer') self.HM_NAME = self.getUniqueString() @@ -290,7 +290,7 @@ def tearDown(self): self.conn.load_balancer.delete_load_balancer( self.LB_ID, ignore_missing=False ) - super(TestLoadBalancer, self).tearDown() + super().tearDown() self.conn.load_balancer.delete_flavor( self.FLAVOR_ID, ignore_missing=False diff --git a/openstack/tests/functional/network/v2/test_address_group.py b/openstack/tests/functional/network/v2/test_address_group.py index 5b50ec250..7938930d2 100644 --- a/openstack/tests/functional/network/v2/test_address_group.py +++ b/openstack/tests/functional/network/v2/test_address_group.py @@ -20,7 +20,7 @@ class TestAddressGroup(base.BaseFunctionalTest): ADDRESSES = ["10.0.0.1/32", "2001:db8::/32"] def setUp(self): - super(TestAddressGroup, self).setUp() + super().setUp() # Skip the tests if address group extension is not enabled. if not self.user_cloud.network.find_extension("address-group"): @@ -48,7 +48,7 @@ def tearDown(self): self.ADDRESS_GROUP_ID ) self.assertIsNone(sot) - super(TestAddressGroup, self).tearDown() + super().tearDown() def test_find(self): sot = self.user_cloud.network.find_address_group( diff --git a/openstack/tests/functional/network/v2/test_address_scope.py b/openstack/tests/functional/network/v2/test_address_scope.py index 9b9340382..766a5fa97 100644 --- a/openstack/tests/functional/network/v2/test_address_scope.py +++ b/openstack/tests/functional/network/v2/test_address_scope.py @@ -21,7 +21,7 @@ class TestAddressScope(base.BaseFunctionalTest): IP_VERSION = 4 def setUp(self): - super(TestAddressScope, self).setUp() + super().setUp() self.ADDRESS_SCOPE_NAME = self.getUniqueString() self.ADDRESS_SCOPE_NAME_UPDATED = self.getUniqueString() address_scope = self.user_cloud.network.create_address_scope( @@ -38,7 +38,7 @@ def tearDown(self): self.ADDRESS_SCOPE_ID ) self.assertIsNone(sot) - super(TestAddressScope, self).tearDown() + super().tearDown() def test_find(self): sot = self.user_cloud.network.find_address_scope( diff --git a/openstack/tests/functional/network/v2/test_agent.py b/openstack/tests/functional/network/v2/test_agent.py index dcb8523ec..e23198c74 100644 --- a/openstack/tests/functional/network/v2/test_agent.py +++ b/openstack/tests/functional/network/v2/test_agent.py @@ -28,7 +28,7 @@ def validate_uuid(self, s): return True def setUp(self): - super(TestAgent, self).setUp() + super().setUp() if not self.user_cloud._has_neutron_extension("agent"): self.skipTest("Neutron agent extension is required for this test") diff --git a/openstack/tests/functional/network/v2/test_agent_add_remove_network.py b/openstack/tests/functional/network/v2/test_agent_add_remove_network.py index 8cd6d901a..221518116 100644 --- a/openstack/tests/functional/network/v2/test_agent_add_remove_network.py +++ b/openstack/tests/functional/network/v2/test_agent_add_remove_network.py @@ -21,7 +21,7 @@ class TestAgentNetworks(base.BaseFunctionalTest): AGENT_ID = None def setUp(self): - super(TestAgentNetworks, self).setUp() + super().setUp() if not self.user_cloud._has_neutron_extension("agent"): self.skipTest("Neutron agent extension is required for this test") diff --git a/openstack/tests/functional/network/v2/test_agent_add_remove_router.py b/openstack/tests/functional/network/v2/test_agent_add_remove_router.py index 7a3b305b4..b583f7f01 100644 --- a/openstack/tests/functional/network/v2/test_agent_add_remove_router.py +++ b/openstack/tests/functional/network/v2/test_agent_add_remove_router.py @@ -20,7 +20,7 @@ class TestAgentRouters(base.BaseFunctionalTest): AGENT = None def setUp(self): - super(TestAgentRouters, self).setUp() + super().setUp() if not self.user_cloud._has_neutron_extension("agent"): self.skipTest("Neutron agent extension is required for this test") diff --git a/openstack/tests/functional/network/v2/test_auto_allocated_topology.py b/openstack/tests/functional/network/v2/test_auto_allocated_topology.py index cb386ef1c..5941374f5 100644 --- a/openstack/tests/functional/network/v2/test_auto_allocated_topology.py +++ b/openstack/tests/functional/network/v2/test_auto_allocated_topology.py @@ -19,7 +19,7 @@ class TestAutoAllocatedTopology(base.BaseFunctionalTest): PROJECT_ID = None def setUp(self): - super(TestAutoAllocatedTopology, self).setUp() + super().setUp() if not self.operator_cloud: self.skipTest("Operator cloud is required for this test") if not self.operator_cloud._has_neutron_extension( @@ -38,7 +38,7 @@ def tearDown(self): self.PROJECT_ID ) self.assertIsNone(res) - super(TestAutoAllocatedTopology, self).tearDown() + super().tearDown() def test_dry_run_option_pass(self): # Dry run will only pass if there is a public network diff --git a/openstack/tests/functional/network/v2/test_default_security_group_rule.py b/openstack/tests/functional/network/v2/test_default_security_group_rule.py index 11bf5cf50..3391fa77a 100644 --- a/openstack/tests/functional/network/v2/test_default_security_group_rule.py +++ b/openstack/tests/functional/network/v2/test_default_security_group_rule.py @@ -53,7 +53,7 @@ def tearDown(self): self.RULE_ID, ignore_missing=False ) self.assertIsNone(sot) - super(TestDefaultSecurityGroupRule, self).tearDown() + super().tearDown() def test_find(self): sot = self.operator_cloud.network.find_default_security_group_rule( diff --git a/openstack/tests/functional/network/v2/test_dvr_router.py b/openstack/tests/functional/network/v2/test_dvr_router.py index 81a1a3585..899b93967 100644 --- a/openstack/tests/functional/network/v2/test_dvr_router.py +++ b/openstack/tests/functional/network/v2/test_dvr_router.py @@ -19,7 +19,7 @@ class TestDVRRouter(base.BaseFunctionalTest): ID = None def setUp(self): - super(TestDVRRouter, self).setUp() + super().setUp() if not self.operator_cloud: # Current policies forbid regular user use it self.skipTest("Operator cloud is required for this test") @@ -41,7 +41,7 @@ def tearDown(self): self.ID, ignore_missing=False ) self.assertIsNone(sot) - super(TestDVRRouter, self).tearDown() + super().tearDown() def test_find(self): sot = self.operator_cloud.network.find_router(self.NAME) diff --git a/openstack/tests/functional/network/v2/test_firewall_group.py b/openstack/tests/functional/network/v2/test_firewall_group.py index 83f314999..8592149d1 100644 --- a/openstack/tests/functional/network/v2/test_firewall_group.py +++ b/openstack/tests/functional/network/v2/test_firewall_group.py @@ -22,7 +22,7 @@ class TestFirewallGroup(base.BaseFunctionalTest): ID = None def setUp(self): - super(TestFirewallGroup, self).setUp() + super().setUp() if not self.user_cloud._has_neutron_extension("fwaas_v2"): self.skipTest("fwaas_v2 service not supported by cloud") self.NAME = self.getUniqueString() @@ -36,7 +36,7 @@ def tearDown(self): self.ID, ignore_missing=False ) self.assertIs(None, sot) - super(TestFirewallGroup, self).tearDown() + super().tearDown() def test_find(self): sot = self.user_cloud.network.find_firewall_group(self.NAME) diff --git a/openstack/tests/functional/network/v2/test_firewall_policy.py b/openstack/tests/functional/network/v2/test_firewall_policy.py index efb6670c7..c1520d162 100644 --- a/openstack/tests/functional/network/v2/test_firewall_policy.py +++ b/openstack/tests/functional/network/v2/test_firewall_policy.py @@ -22,7 +22,7 @@ class TestFirewallPolicy(base.BaseFunctionalTest): ID = None def setUp(self): - super(TestFirewallPolicy, self).setUp() + super().setUp() if not self.user_cloud._has_neutron_extension("fwaas_v2"): self.skipTest("fwaas_v2 service not supported by cloud") self.NAME = self.getUniqueString() @@ -36,7 +36,7 @@ def tearDown(self): self.ID, ignore_missing=False ) self.assertIs(None, sot) - super(TestFirewallPolicy, self).tearDown() + super().tearDown() def test_find(self): sot = self.user_cloud.network.find_firewall_policy(self.NAME) diff --git a/openstack/tests/functional/network/v2/test_firewall_rule.py b/openstack/tests/functional/network/v2/test_firewall_rule.py index 477233d1b..8c06d1726 100644 --- a/openstack/tests/functional/network/v2/test_firewall_rule.py +++ b/openstack/tests/functional/network/v2/test_firewall_rule.py @@ -29,7 +29,7 @@ class TestFirewallRule(base.BaseFunctionalTest): ID = None def setUp(self): - super(TestFirewallRule, self).setUp() + super().setUp() if not self.user_cloud._has_neutron_extension("fwaas_v2"): self.skipTest("fwaas_v2 service not supported by cloud") self.NAME = self.getUniqueString() @@ -52,7 +52,7 @@ def tearDown(self): self.ID, ignore_missing=False ) self.assertIs(None, sot) - super(TestFirewallRule, self).tearDown() + super().tearDown() def test_find(self): sot = self.user_cloud.network.find_firewall_rule(self.NAME) diff --git a/openstack/tests/functional/network/v2/test_firewall_rule_insert_remove_policy.py b/openstack/tests/functional/network/v2/test_firewall_rule_insert_remove_policy.py index 22b8bfdd7..72bef3b3c 100644 --- a/openstack/tests/functional/network/v2/test_firewall_rule_insert_remove_policy.py +++ b/openstack/tests/functional/network/v2/test_firewall_rule_insert_remove_policy.py @@ -29,7 +29,7 @@ class TestFirewallPolicyRuleAssociations(base.BaseFunctionalTest): RULE2_ID = None def setUp(self): - super(TestFirewallPolicyRuleAssociations, self).setUp() + super().setUp() if not self.user_cloud._has_neutron_extension("fwaas_v2"): self.skipTest("fwaas_v2 service not supported by cloud") rul1 = self.user_cloud.network.create_firewall_rule( @@ -64,7 +64,7 @@ def tearDown(self): self.RULE2_ID, ignore_missing=False ) self.assertIs(None, sot) - super(TestFirewallPolicyRuleAssociations, self).tearDown() + super().tearDown() def test_insert_rule_into_policy(self): policy = self.user_cloud.network.insert_rule_into_policy( diff --git a/openstack/tests/functional/network/v2/test_flavor.py b/openstack/tests/functional/network/v2/test_flavor.py index f67f082a4..f366c85db 100644 --- a/openstack/tests/functional/network/v2/test_flavor.py +++ b/openstack/tests/functional/network/v2/test_flavor.py @@ -23,7 +23,7 @@ class TestFlavor(base.BaseFunctionalTest): METAINFO = "FlAVOR_PROFILE_METAINFO" def setUp(self): - super(TestFlavor, self).setUp() + super().setUp() if not self.user_cloud._has_neutron_extension("flavors"): self.skipTest("Neutron flavor extension is required for this test") @@ -56,7 +56,7 @@ def tearDown(self): self.ID, ignore_missing=True ) self.assertIsNone(service_profiles) - super(TestFlavor, self).tearDown() + super().tearDown() def test_find(self): if self.ID: diff --git a/openstack/tests/functional/network/v2/test_floating_ip.py b/openstack/tests/functional/network/v2/test_floating_ip.py index e090e7c0f..eb78f432a 100644 --- a/openstack/tests/functional/network/v2/test_floating_ip.py +++ b/openstack/tests/functional/network/v2/test_floating_ip.py @@ -34,7 +34,7 @@ class TestFloatingIP(base.BaseFunctionalTest): DNS_NAME = "fip1" def setUp(self): - super(TestFloatingIP, self).setUp() + super().setUp() if not self.user_cloud._has_neutron_extension("external-net"): self.skipTest( "Neutron external-net extension is required for this test" @@ -130,7 +130,7 @@ def tearDown(self): self.INT_NET_ID, ignore_missing=False ) self.assertIsNone(sot) - super(TestFloatingIP, self).tearDown() + super().tearDown() def _create_network(self, name, **args): self.name = name diff --git a/openstack/tests/functional/network/v2/test_l3_conntrack_helper.py b/openstack/tests/functional/network/v2/test_l3_conntrack_helper.py index 66bc90217..bf8022c04 100644 --- a/openstack/tests/functional/network/v2/test_l3_conntrack_helper.py +++ b/openstack/tests/functional/network/v2/test_l3_conntrack_helper.py @@ -24,7 +24,7 @@ class TestL3ConntrackHelper(base.BaseFunctionalTest): ROT_ID = None def setUp(self): - super(TestL3ConntrackHelper, self).setUp() + super().setUp() if not self.user_cloud.network.find_extension("l3-conntrack-helper"): self.skipTest("L3 conntrack helper extension disabled") @@ -52,7 +52,7 @@ def tearDown(self): self.ROT_ID, ignore_missing=False ) self.assertIsNone(sot) - super(TestL3ConntrackHelper, self).tearDown() + super().tearDown() def test_get(self): sot = self.user_cloud.network.get_conntrack_helper( diff --git a/openstack/tests/functional/network/v2/test_local_ip.py b/openstack/tests/functional/network/v2/test_local_ip.py index 085bff320..47ac357ab 100644 --- a/openstack/tests/functional/network/v2/test_local_ip.py +++ b/openstack/tests/functional/network/v2/test_local_ip.py @@ -21,7 +21,7 @@ class TestLocalIP(base.BaseFunctionalTest): LOCAL_IP_ID = None def setUp(self): - super(TestLocalIP, self).setUp() + super().setUp() if not self.user_cloud.network.find_extension("local_ip"): self.skipTest("Local IP extension disabled") @@ -42,7 +42,7 @@ def setUp(self): def tearDown(self): sot = self.user_cloud.network.delete_local_ip(self.LOCAL_IP_ID) self.assertIsNone(sot) - super(TestLocalIP, self).tearDown() + super().tearDown() def test_find(self): sot = self.user_cloud.network.find_local_ip(self.LOCAL_IP_NAME) diff --git a/openstack/tests/functional/network/v2/test_local_ip_association.py b/openstack/tests/functional/network/v2/test_local_ip_association.py index 4976fc43c..9911ee767 100644 --- a/openstack/tests/functional/network/v2/test_local_ip_association.py +++ b/openstack/tests/functional/network/v2/test_local_ip_association.py @@ -23,7 +23,7 @@ class TestLocalIPAssociation(base.BaseFunctionalTest): FIXED_IP = None def setUp(self): - super(TestLocalIPAssociation, self).setUp() + super().setUp() if not self.user_cloud.network.find_extension("local_ip"): self.skipTest("Local IP extension disabled") @@ -52,7 +52,7 @@ def tearDown(self): self.LOCAL_IP_ID, self.FIXED_PORT_ID ) self.assertIsNone(sot) - super(TestLocalIPAssociation, self).tearDown() + super().tearDown() def test_find(self): sot = self.user_cloud.network.find_local_ip_association( diff --git a/openstack/tests/functional/network/v2/test_ndp_proxy.py b/openstack/tests/functional/network/v2/test_ndp_proxy.py index 35e78d968..ccfd93037 100644 --- a/openstack/tests/functional/network/v2/test_ndp_proxy.py +++ b/openstack/tests/functional/network/v2/test_ndp_proxy.py @@ -30,7 +30,7 @@ class TestNDPProxy(base.BaseFunctionalTest): INTERNAL_PORT_ID = None def setUp(self): - super(TestNDPProxy, self).setUp() + super().setUp() if not self.user_cloud.network.find_extension("l3-ndp-proxy"): self.skipTest("L3 ndp proxy extension disabled") @@ -112,7 +112,7 @@ def tearDown(self): self.INT_NET_ID, ignore_missing=False ) self.assertIsNone(sot) - super(TestNDPProxy, self).tearDown() + super().tearDown() def _create_network(self, name, **args): self.name = name diff --git a/openstack/tests/functional/network/v2/test_network.py b/openstack/tests/functional/network/v2/test_network.py index d857fa33d..f48cf16d2 100644 --- a/openstack/tests/functional/network/v2/test_network.py +++ b/openstack/tests/functional/network/v2/test_network.py @@ -39,7 +39,7 @@ class TestNetwork(base.BaseFunctionalTest): ID = None def setUp(self): - super(TestNetwork, self).setUp() + super().setUp() self.NAME = self.getUniqueString() sot = self.user_cloud.network.create_network(name=self.NAME) assert isinstance(sot, network.Network) @@ -51,7 +51,7 @@ def tearDown(self): self.ID, ignore_missing=False ) self.assertIsNone(sot) - super(TestNetwork, self).tearDown() + super().tearDown() def test_find(self): sot = self.user_cloud.network.find_network(self.NAME) diff --git a/openstack/tests/functional/network/v2/test_network_ip_availability.py b/openstack/tests/functional/network/v2/test_network_ip_availability.py index 3f8bc21d6..7ae4b0226 100644 --- a/openstack/tests/functional/network/v2/test_network_ip_availability.py +++ b/openstack/tests/functional/network/v2/test_network_ip_availability.py @@ -25,7 +25,7 @@ class TestNetworkIPAvailability(base.BaseFunctionalTest): PORT_ID = None def setUp(self): - super(TestNetworkIPAvailability, self).setUp() + super().setUp() if not self.operator_cloud: self.skipTest("Operator cloud required for this test") if not self.operator_cloud._has_neutron_extension( @@ -67,7 +67,7 @@ def tearDown(self): self.assertIsNone(sot) sot = self.operator_cloud.network.delete_network(self.NET_ID) self.assertIsNone(sot) - super(TestNetworkIPAvailability, self).tearDown() + super().tearDown() def test_find(self): sot = self.operator_cloud.network.find_network_ip_availability( diff --git a/openstack/tests/functional/network/v2/test_network_segment_range.py b/openstack/tests/functional/network/v2/test_network_segment_range.py index 69212797a..c7c4791bf 100644 --- a/openstack/tests/functional/network/v2/test_network_segment_range.py +++ b/openstack/tests/functional/network/v2/test_network_segment_range.py @@ -29,7 +29,7 @@ class TestNetworkSegmentRange(base.BaseFunctionalTest): MAXIMUM = 200 def setUp(self): - super(TestNetworkSegmentRange, self).setUp() + super().setUp() if not self.operator_cloud: self.skipTest("Operator cloud required for this test") @@ -69,7 +69,7 @@ def setUp(self): self.assertEqual(self.MAXIMUM, test_seg_range.maximum) def tearDown(self): - super(TestNetworkSegmentRange, self).tearDown() + super().tearDown() def test_create_delete(self): del_test_seg_range = ( diff --git a/openstack/tests/functional/network/v2/test_port.py b/openstack/tests/functional/network/v2/test_port.py index a0028ae25..5a57686e0 100644 --- a/openstack/tests/functional/network/v2/test_port.py +++ b/openstack/tests/functional/network/v2/test_port.py @@ -25,7 +25,7 @@ class TestPort(base.BaseFunctionalTest): PORT_ID = None def setUp(self): - super(TestPort, self).setUp() + super().setUp() self.NET_NAME = self.getUniqueString() self.SUB_NAME = self.getUniqueString() self.PORT_NAME = self.getUniqueString() @@ -63,7 +63,7 @@ def tearDown(self): self.NET_ID, ignore_missing=False ) self.assertIsNone(sot) - super(TestPort, self).tearDown() + super().tearDown() def test_find(self): sot = self.user_cloud.network.find_port(self.PORT_NAME) diff --git a/openstack/tests/functional/network/v2/test_port_forwarding.py b/openstack/tests/functional/network/v2/test_port_forwarding.py index dbeaaeafe..362338231 100644 --- a/openstack/tests/functional/network/v2/test_port_forwarding.py +++ b/openstack/tests/functional/network/v2/test_port_forwarding.py @@ -39,7 +39,7 @@ class TestPortForwarding(base.BaseFunctionalTest): DESCRIPTION = "description" def setUp(self): - super(TestPortForwarding, self).setUp() + super().setUp() if not self.user_cloud._has_neutron_extension("external-net"): self.skipTest( @@ -147,7 +147,7 @@ def tearDown(self): self.INT_NET_ID, ignore_missing=False ) self.assertIsNone(sot) - super(TestPortForwarding, self).tearDown() + super().tearDown() def _create_network(self, name, **args): self.name = name diff --git a/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py b/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py index ce7c8150b..47037062c 100644 --- a/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py @@ -29,7 +29,7 @@ class TestQoSBandwidthLimitRule(base.BaseFunctionalTest): RULE_DIRECTION_NEW = "ingress" def setUp(self): - super(TestQoSBandwidthLimitRule, self).setUp() + super().setUp() if not self.operator_cloud: self.skipTest("Operator cloud required for this test") @@ -71,7 +71,7 @@ def tearDown(self): ) self.assertIsNone(rule) self.assertIsNone(qos_policy) - super(TestQoSBandwidthLimitRule, self).tearDown() + super().tearDown() def test_find(self): sot = self.operator_cloud.network.find_qos_bandwidth_limit_rule( diff --git a/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py b/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py index f9f21a2d4..7849c3020 100644 --- a/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py +++ b/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py @@ -25,7 +25,7 @@ class TestQoSDSCPMarkingRule(base.BaseFunctionalTest): RULE_DSCP_MARK_NEW = 40 def setUp(self): - super(TestQoSDSCPMarkingRule, self).setUp() + super().setUp() if not self.operator_cloud: self.skipTest("Operator cloud is required for this test") @@ -57,7 +57,7 @@ def tearDown(self): qos_policy = self.conn.network.delete_qos_policy(self.QOS_POLICY_ID) self.assertIsNone(rule) self.assertIsNone(qos_policy) - super(TestQoSDSCPMarkingRule, self).tearDown() + super().tearDown() def test_find(self): sot = self.conn.network.find_qos_dscp_marking_rule( diff --git a/openstack/tests/functional/network/v2/test_qos_minimum_bandwidth_rule.py b/openstack/tests/functional/network/v2/test_qos_minimum_bandwidth_rule.py index 4e3278292..eb0898cb9 100644 --- a/openstack/tests/functional/network/v2/test_qos_minimum_bandwidth_rule.py +++ b/openstack/tests/functional/network/v2/test_qos_minimum_bandwidth_rule.py @@ -27,7 +27,7 @@ class TestQoSMinimumBandwidthRule(base.BaseFunctionalTest): RULE_DIRECTION = "egress" def setUp(self): - super(TestQoSMinimumBandwidthRule, self).setUp() + super().setUp() if not self.operator_cloud: self.skipTest("Operator cloud is required for this test") @@ -69,7 +69,7 @@ def tearDown(self): ) self.assertIsNone(rule) self.assertIsNone(qos_policy) - super(TestQoSMinimumBandwidthRule, self).tearDown() + super().tearDown() def test_find(self): sot = self.operator_cloud.network.find_qos_minimum_bandwidth_rule( diff --git a/openstack/tests/functional/network/v2/test_qos_minimum_packet_rate_rule.py b/openstack/tests/functional/network/v2/test_qos_minimum_packet_rate_rule.py index 40088152b..2329c77bc 100644 --- a/openstack/tests/functional/network/v2/test_qos_minimum_packet_rate_rule.py +++ b/openstack/tests/functional/network/v2/test_qos_minimum_packet_rate_rule.py @@ -28,7 +28,7 @@ class TestQoSMinimumPacketRateRule(base.BaseFunctionalTest): RULE_DIRECTION_NEW = "ingress" def setUp(self): - super(TestQoSMinimumPacketRateRule, self).setUp() + super().setUp() if not self.operator_cloud: self.skipTest("Operator cloud is required for this test") @@ -68,7 +68,7 @@ def tearDown(self): ) self.assertIsNone(rule) self.assertIsNone(qos_policy) - super(TestQoSMinimumPacketRateRule, self).tearDown() + super().tearDown() def test_find(self): sot = self.operator_cloud.network.find_qos_minimum_packet_rate_rule( diff --git a/openstack/tests/functional/network/v2/test_qos_policy.py b/openstack/tests/functional/network/v2/test_qos_policy.py index acd9c4cf1..8395566fc 100644 --- a/openstack/tests/functional/network/v2/test_qos_policy.py +++ b/openstack/tests/functional/network/v2/test_qos_policy.py @@ -23,7 +23,7 @@ class TestQoSPolicy(base.BaseFunctionalTest): QOS_POLICY_DESCRIPTION = "QoS policy description" def setUp(self): - super(TestQoSPolicy, self).setUp() + super().setUp() if not self.operator_cloud: self.skipTest("Operator cloud is required for this test") @@ -47,7 +47,7 @@ def setUp(self): def tearDown(self): sot = self.operator_cloud.network.delete_qos_policy(self.QOS_POLICY_ID) self.assertIsNone(sot) - super(TestQoSPolicy, self).tearDown() + super().tearDown() def test_find(self): sot = self.operator_cloud.network.find_qos_policy(self.QOS_POLICY_NAME) diff --git a/openstack/tests/functional/network/v2/test_qos_rule_type.py b/openstack/tests/functional/network/v2/test_qos_rule_type.py index 162b0e728..6491bc19e 100644 --- a/openstack/tests/functional/network/v2/test_qos_rule_type.py +++ b/openstack/tests/functional/network/v2/test_qos_rule_type.py @@ -18,7 +18,7 @@ class TestQoSRuleType(base.BaseFunctionalTest): QOS_RULE_TYPE = "bandwidth_limit" def setUp(self): - super(TestQoSRuleType, self).setUp() + super().setUp() if not self.operator_cloud: self.skipTest("Operator cloud is required for this test") diff --git a/openstack/tests/functional/network/v2/test_quota.py b/openstack/tests/functional/network/v2/test_quota.py index ae582488b..fdab400ab 100644 --- a/openstack/tests/functional/network/v2/test_quota.py +++ b/openstack/tests/functional/network/v2/test_quota.py @@ -15,7 +15,7 @@ class TestQuota(base.BaseFunctionalTest): def setUp(self): - super(TestQuota, self).setUp() + super().setUp() if not self.operator_cloud: self.skipTest("Operator cloud required for this test") diff --git a/openstack/tests/functional/network/v2/test_rbac_policy.py b/openstack/tests/functional/network/v2/test_rbac_policy.py index b96ca7b30..ba4c2fb80 100644 --- a/openstack/tests/functional/network/v2/test_rbac_policy.py +++ b/openstack/tests/functional/network/v2/test_rbac_policy.py @@ -24,7 +24,7 @@ class TestRBACPolicy(base.BaseFunctionalTest): ID = None def setUp(self): - super(TestRBACPolicy, self).setUp() + super().setUp() if not self.user_cloud._has_neutron_extension("rbac-policies"): self.skipTest( "Neutron rbac-policies extension is required for this test" @@ -68,7 +68,7 @@ def tearDown(self): self.NET_ID, ignore_missing=False ) self.assertIsNone(sot) - super(TestRBACPolicy, self).tearDown() + super().tearDown() def test_find(self): if self.operator_cloud: diff --git a/openstack/tests/functional/network/v2/test_router.py b/openstack/tests/functional/network/v2/test_router.py index 5657edddd..cd55c8417 100644 --- a/openstack/tests/functional/network/v2/test_router.py +++ b/openstack/tests/functional/network/v2/test_router.py @@ -19,7 +19,7 @@ class TestRouter(base.BaseFunctionalTest): ID = None def setUp(self): - super(TestRouter, self).setUp() + super().setUp() self.NAME = self.getUniqueString() self.UPDATE_NAME = self.getUniqueString() sot = self.user_cloud.network.create_router(name=self.NAME) @@ -32,7 +32,7 @@ def tearDown(self): self.ID, ignore_missing=False ) self.assertIsNone(sot) - super(TestRouter, self).tearDown() + super().tearDown() def test_find(self): sot = self.user_cloud.network.find_router(self.NAME) diff --git a/openstack/tests/functional/network/v2/test_router_add_remove_interface.py b/openstack/tests/functional/network/v2/test_router_add_remove_interface.py index c59cc0e8d..6557b1ac7 100644 --- a/openstack/tests/functional/network/v2/test_router_add_remove_interface.py +++ b/openstack/tests/functional/network/v2/test_router_add_remove_interface.py @@ -26,7 +26,7 @@ class TestRouterInterface(base.BaseFunctionalTest): ROT = None def setUp(self): - super(TestRouterInterface, self).setUp() + super().setUp() self.ROUTER_NAME = self.getUniqueString() self.NET_NAME = self.getUniqueString() self.SUB_NAME = self.getUniqueString() @@ -62,7 +62,7 @@ def tearDown(self): self.NET_ID, ignore_missing=False ) self.assertIsNone(sot) - super(TestRouterInterface, self).tearDown() + super().tearDown() def test_router_add_remove_interface(self): iface = self.ROT.add_interface( diff --git a/openstack/tests/functional/network/v2/test_security_group.py b/openstack/tests/functional/network/v2/test_security_group.py index ddc1d08e0..023accfe9 100644 --- a/openstack/tests/functional/network/v2/test_security_group.py +++ b/openstack/tests/functional/network/v2/test_security_group.py @@ -19,7 +19,7 @@ class TestSecurityGroup(base.BaseFunctionalTest): ID = None def setUp(self): - super(TestSecurityGroup, self).setUp() + super().setUp() self.NAME = self.getUniqueString() sot = self.user_cloud.network.create_security_group(name=self.NAME) assert isinstance(sot, security_group.SecurityGroup) @@ -31,7 +31,7 @@ def tearDown(self): self.ID, ignore_missing=False ) self.assertIsNone(sot) - super(TestSecurityGroup, self).tearDown() + super().tearDown() def test_find(self): sot = self.user_cloud.network.find_security_group(self.NAME) diff --git a/openstack/tests/functional/network/v2/test_security_group_rule.py b/openstack/tests/functional/network/v2/test_security_group_rule.py index e5bd4a953..27d91cf84 100644 --- a/openstack/tests/functional/network/v2/test_security_group_rule.py +++ b/openstack/tests/functional/network/v2/test_security_group_rule.py @@ -25,7 +25,7 @@ class TestSecurityGroupRule(base.BaseFunctionalTest): RULE_ID = None def setUp(self): - super(TestSecurityGroupRule, self).setUp() + super().setUp() self.NAME = self.getUniqueString() sot = self.user_cloud.network.create_security_group(name=self.NAME) assert isinstance(sot, security_group.SecurityGroup) @@ -52,7 +52,7 @@ def tearDown(self): self.ID, ignore_missing=False ) self.assertIsNone(sot) - super(TestSecurityGroupRule, self).tearDown() + super().tearDown() def test_find(self): sot = self.user_cloud.network.find_security_group_rule(self.RULE_ID) diff --git a/openstack/tests/functional/network/v2/test_segment.py b/openstack/tests/functional/network/v2/test_segment.py index 07d9f6edc..43ba82008 100644 --- a/openstack/tests/functional/network/v2/test_segment.py +++ b/openstack/tests/functional/network/v2/test_segment.py @@ -25,7 +25,7 @@ class TestSegment(base.BaseFunctionalTest): SEGMENT_EXTENSION = None def setUp(self): - super(TestSegment, self).setUp() + super().setUp() self.NETWORK_NAME = self.getUniqueString() if not self.operator_cloud: @@ -60,7 +60,7 @@ def tearDown(self): self.NETWORK_ID, ignore_missing=False ) self.assertIsNone(sot) - super(TestSegment, self).tearDown() + super().tearDown() def test_create_delete(self): sot = self.operator_cloud.network.create_segment( diff --git a/openstack/tests/functional/network/v2/test_service_profile.py b/openstack/tests/functional/network/v2/test_service_profile.py index d8c50c124..6bee7f47d 100644 --- a/openstack/tests/functional/network/v2/test_service_profile.py +++ b/openstack/tests/functional/network/v2/test_service_profile.py @@ -21,7 +21,7 @@ class TestServiceProfile(base.BaseFunctionalTest): ID = None def setUp(self): - super(TestServiceProfile, self).setUp() + super().setUp() if not self.user_cloud._has_neutron_extension("flavors"): self.skipTest("Neutron flavor extension is required for this test") @@ -50,7 +50,7 @@ def tearDown(self): ) ) self.assertIsNone(service_profiles) - super(TestServiceProfile, self).tearDown() + super().tearDown() def test_find(self): self.user_cloud.network.find_service_profile( diff --git a/openstack/tests/functional/network/v2/test_subnet.py b/openstack/tests/functional/network/v2/test_subnet.py index 1fe35a5ba..23c8744de 100644 --- a/openstack/tests/functional/network/v2/test_subnet.py +++ b/openstack/tests/functional/network/v2/test_subnet.py @@ -26,7 +26,7 @@ class TestSubnet(base.BaseFunctionalTest): SUB_ID = None def setUp(self): - super(TestSubnet, self).setUp() + super().setUp() self.NET_NAME = self.getUniqueString() self.SUB_NAME = self.getUniqueString() self.UPDATE_NAME = self.getUniqueString() @@ -54,7 +54,7 @@ def tearDown(self): self.NET_ID, ignore_missing=False ) self.assertIsNone(sot) - super(TestSubnet, self).tearDown() + super().tearDown() def test_find(self): sot = self.user_cloud.network.find_subnet(self.SUB_NAME) diff --git a/openstack/tests/functional/network/v2/test_subnet_from_subnet_pool.py b/openstack/tests/functional/network/v2/test_subnet_from_subnet_pool.py index c31dfdbcf..175fe2740 100644 --- a/openstack/tests/functional/network/v2/test_subnet_from_subnet_pool.py +++ b/openstack/tests/functional/network/v2/test_subnet_from_subnet_pool.py @@ -31,7 +31,7 @@ class TestSubnetFromSubnetPool(base.BaseFunctionalTest): SUB_POOL_ID = None def setUp(self): - super(TestSubnetFromSubnetPool, self).setUp() + super().setUp() self.NET_NAME = self.getUniqueString() self.SUB_NAME = self.getUniqueString() self.SUB_POOL_NAME = self.getUniqueString() @@ -70,7 +70,7 @@ def tearDown(self): self.assertIsNone(sot) sot = self.user_cloud.network.delete_subnet_pool(self.SUB_POOL_ID) self.assertIsNone(sot) - super(TestSubnetFromSubnetPool, self).tearDown() + super().tearDown() def test_get(self): sot = self.user_cloud.network.get_subnet(self.SUB_ID) diff --git a/openstack/tests/functional/network/v2/test_subnet_pool.py b/openstack/tests/functional/network/v2/test_subnet_pool.py index 8daa8db9b..a453c6095 100644 --- a/openstack/tests/functional/network/v2/test_subnet_pool.py +++ b/openstack/tests/functional/network/v2/test_subnet_pool.py @@ -26,7 +26,7 @@ class TestSubnetPool(base.BaseFunctionalTest): PREFIXES = ["10.100.0.0/24", "10.101.0.0/24"] def setUp(self): - super(TestSubnetPool, self).setUp() + super().setUp() self.SUBNET_POOL_NAME = self.getUniqueString() self.SUBNET_POOL_NAME_UPDATED = self.getUniqueString() subnet_pool = self.user_cloud.network.create_subnet_pool( @@ -45,7 +45,7 @@ def setUp(self): def tearDown(self): sot = self.user_cloud.network.delete_subnet_pool(self.SUBNET_POOL_ID) self.assertIsNone(sot) - super(TestSubnetPool, self).tearDown() + super().tearDown() def test_find(self): sot = self.user_cloud.network.find_subnet_pool(self.SUBNET_POOL_NAME) diff --git a/openstack/tests/functional/network/v2/test_trunk.py b/openstack/tests/functional/network/v2/test_trunk.py index 7107e1ddf..4f60d3e22 100644 --- a/openstack/tests/functional/network/v2/test_trunk.py +++ b/openstack/tests/functional/network/v2/test_trunk.py @@ -21,7 +21,7 @@ class TestTrunk(base.BaseFunctionalTest): TIMEOUT_SCALING_FACTOR = 2.0 def setUp(self): - super(TestTrunk, self).setUp() + super().setUp() # Skip the tests if trunk extension is not enabled. if not self.user_cloud.network.find_extension("trunk"): @@ -51,7 +51,7 @@ def tearDown(self): self.user_cloud.network.delete_network( self.NET_ID, ignore_missing=False ) - super(TestTrunk, self).tearDown() + super().tearDown() def test_find(self): sot = self.user_cloud.network.find_trunk(self.TRUNK_NAME) diff --git a/openstack/tests/functional/network/v2/test_vpnaas.py b/openstack/tests/functional/network/v2/test_vpnaas.py index 53be0bbfc..0fdd2b86c 100644 --- a/openstack/tests/functional/network/v2/test_vpnaas.py +++ b/openstack/tests/functional/network/v2/test_vpnaas.py @@ -18,7 +18,7 @@ class TestVpnIkePolicy(base.BaseFunctionalTest): ID = None def setUp(self): - super(TestVpnIkePolicy, self).setUp() + super().setUp() if not self.user_cloud._has_neutron_extension("vpnaas"): self.skipTest("vpnaas service not supported by cloud") self.IKEPOLICY_NAME = self.getUniqueString("ikepolicy") @@ -35,7 +35,7 @@ def tearDown(self): self.ID, ignore_missing=True ) self.assertIsNone(ikepolicy) - super(TestVpnIkePolicy, self).tearDown() + super().tearDown() def test_list(self): policies = [f.name for f in self.user_cloud.network.vpn_ike_policies()] diff --git a/openstack/tests/functional/object_store/v1/test_account.py b/openstack/tests/functional/object_store/v1/test_account.py index abdf821e7..ac518bc2c 100644 --- a/openstack/tests/functional/object_store/v1/test_account.py +++ b/openstack/tests/functional/object_store/v1/test_account.py @@ -15,13 +15,13 @@ class TestAccount(base.BaseFunctionalTest): def setUp(self): - super(TestAccount, self).setUp() + super().setUp() self.require_service('object-store') def tearDown(self): account = self.conn.object_store.get_account_metadata() self.conn.object_store.delete_account_metadata(account.metadata.keys()) - super(TestAccount, self).tearDown() + super().tearDown() def test_system_metadata(self): account = self.conn.object_store.get_account_metadata() diff --git a/openstack/tests/functional/object_store/v1/test_container.py b/openstack/tests/functional/object_store/v1/test_container.py index d0211497b..00b81bb1d 100644 --- a/openstack/tests/functional/object_store/v1/test_container.py +++ b/openstack/tests/functional/object_store/v1/test_container.py @@ -16,7 +16,7 @@ class TestContainer(base.BaseFunctionalTest): def setUp(self): - super(TestContainer, self).setUp() + super().setUp() self.require_service('object-store') self.NAME = self.getUniqueString() diff --git a/openstack/tests/functional/object_store/v1/test_obj.py b/openstack/tests/functional/object_store/v1/test_obj.py index a8d48c356..b74f45154 100644 --- a/openstack/tests/functional/object_store/v1/test_obj.py +++ b/openstack/tests/functional/object_store/v1/test_obj.py @@ -17,7 +17,7 @@ class TestObject(base.BaseFunctionalTest): DATA = b'abc' def setUp(self): - super(TestObject, self).setUp() + super().setUp() self.require_service('object-store') self.FOLDER = self.getUniqueString() diff --git a/openstack/tests/functional/orchestration/v1/test_stack.py b/openstack/tests/functional/orchestration/v1/test_stack.py index bc561da35..33492925c 100644 --- a/openstack/tests/functional/orchestration/v1/test_stack.py +++ b/openstack/tests/functional/orchestration/v1/test_stack.py @@ -28,7 +28,7 @@ class TestStack(base.BaseFunctionalTest): _wait_for_timeout_key = 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_ORCHESTRATION' def setUp(self): - super(TestStack, self).setUp() + super().setUp() self.require_service('orchestration') if self.conn.compute.find_keypair(self.NAME) is None: @@ -75,7 +75,7 @@ def tearDown(self): except exceptions.ResourceNotFound: pass test_network.delete_network(self.conn, self.network, self.subnet) - super(TestStack, self).tearDown() + super().tearDown() def test_list(self): names = [o.name for o in self.conn.orchestration.stacks()] diff --git a/openstack/tests/functional/shared_file_system/base.py b/openstack/tests/functional/shared_file_system/base.py index 4a1d5a295..b57ce9a66 100644 --- a/openstack/tests/functional/shared_file_system/base.py +++ b/openstack/tests/functional/shared_file_system/base.py @@ -18,7 +18,7 @@ class BaseSharedFileSystemTest(base.BaseFunctionalTest): min_microversion = None def setUp(self): - super(BaseSharedFileSystemTest, self).setUp() + super().setUp() self.require_service( 'shared-file-system', min_microversion=self.min_microversion ) diff --git a/openstack/tests/functional/shared_file_system/test_resource_lock.py b/openstack/tests/functional/shared_file_system/test_resource_lock.py index e4f2b0351..42198fb54 100644 --- a/openstack/tests/functional/shared_file_system/test_resource_lock.py +++ b/openstack/tests/functional/shared_file_system/test_resource_lock.py @@ -16,7 +16,7 @@ class ResourceLocksTest(base.BaseSharedFileSystemTest): def setUp(self): - super(ResourceLocksTest, self).setUp() + super().setUp() self.SHARE_NAME = self.getUniqueString() share = self.user_cloud.shared_file_system.create_share( diff --git a/openstack/tests/functional/shared_file_system/test_share.py b/openstack/tests/functional/shared_file_system/test_share.py index 1ff645de6..86b91b98a 100644 --- a/openstack/tests/functional/shared_file_system/test_share.py +++ b/openstack/tests/functional/shared_file_system/test_share.py @@ -17,7 +17,7 @@ class ShareTest(base.BaseSharedFileSystemTest): def setUp(self): - super(ShareTest, self).setUp() + super().setUp() self.SHARE_NAME = self.getUniqueString() my_share = self.create_share( @@ -164,7 +164,7 @@ def test_resize_share_with_force(self): class ManageUnmanageShareTest(base.BaseSharedFileSystemTest): def setUp(self): - super(ManageUnmanageShareTest, self).setUp() + super().setUp() self.NEW_SHARE = self.create_share( share_proto="NFS", diff --git a/openstack/tests/functional/shared_file_system/test_share_access_rule.py b/openstack/tests/functional/shared_file_system/test_share_access_rule.py index 7fc7817a5..b242cc52f 100644 --- a/openstack/tests/functional/shared_file_system/test_share_access_rule.py +++ b/openstack/tests/functional/shared_file_system/test_share_access_rule.py @@ -15,7 +15,7 @@ class ShareAccessRuleTest(base.BaseSharedFileSystemTest): def setUp(self): - super(ShareAccessRuleTest, self).setUp() + super().setUp() self.SHARE_NAME = self.getUniqueString() mys = self.create_share( @@ -49,7 +49,7 @@ def tearDown(self): self.user_cloud.share.delete_access_rule( self.ACCESS_ID, self.SHARE_ID, ignore_missing=True ) - super(ShareAccessRuleTest, self).tearDown() + super().tearDown() def test_get_access_rule(self): sot = self.user_cloud.shared_file_system.get_access_rule( diff --git a/openstack/tests/functional/shared_file_system/test_share_group.py b/openstack/tests/functional/shared_file_system/test_share_group.py index cfd0bd9bb..376ff5ee0 100644 --- a/openstack/tests/functional/shared_file_system/test_share_group.py +++ b/openstack/tests/functional/shared_file_system/test_share_group.py @@ -16,7 +16,7 @@ class ShareGroupTest(base.BaseSharedFileSystemTest): def setUp(self): - super(ShareGroupTest, self).setUp() + super().setUp() self.SHARE_GROUP_NAME = self.getUniqueString() share_grp = self.user_cloud.shared_file_system.create_share_group( diff --git a/openstack/tests/functional/shared_file_system/test_share_group_snapshot.py b/openstack/tests/functional/shared_file_system/test_share_group_snapshot.py index 662173cfb..0ba40a263 100644 --- a/openstack/tests/functional/shared_file_system/test_share_group_snapshot.py +++ b/openstack/tests/functional/shared_file_system/test_share_group_snapshot.py @@ -21,7 +21,7 @@ class ShareGroupSnapshotTest(base.BaseSharedFileSystemTest): min_microversion = '2.55' def setUp(self): - super(ShareGroupSnapshotTest, self).setUp() + super().setUp() self.SHARE_GROUP_NAME = self.getUniqueString() share_grp = self.user_cloud.shared_file_system.create_share_group( @@ -68,7 +68,7 @@ def tearDown(self): self.user_cloud.shared_file_system.delete_share_group( self.SHARE_GROUP_ID, ignore_missing=False ) - super(ShareGroupSnapshotTest, self).tearDown() + super().tearDown() def test_get(self): sot = self.user_cloud.shared_file_system.get_share_group_snapshot( diff --git a/openstack/tests/functional/shared_file_system/test_share_instance.py b/openstack/tests/functional/shared_file_system/test_share_instance.py index 7ecf37cd0..ece9db913 100644 --- a/openstack/tests/functional/shared_file_system/test_share_instance.py +++ b/openstack/tests/functional/shared_file_system/test_share_instance.py @@ -20,7 +20,7 @@ class ShareInstanceTest(base.BaseSharedFileSystemTest): min_microversion = '2.7' def setUp(self): - super(ShareInstanceTest, self).setUp() + super().setUp() self.SHARE_NAME = self.getUniqueString() my_share = self.create_share( diff --git a/openstack/tests/functional/shared_file_system/test_share_network.py b/openstack/tests/functional/shared_file_system/test_share_network.py index 1f365d6f4..5497ac1f9 100644 --- a/openstack/tests/functional/shared_file_system/test_share_network.py +++ b/openstack/tests/functional/shared_file_system/test_share_network.py @@ -16,7 +16,7 @@ class ShareNetworkTest(base.BaseSharedFileSystemTest): def setUp(self): - super(ShareNetworkTest, self).setUp() + super().setUp() self.NETWORK_NAME = self.getUniqueString() net = self.user_cloud.network.create_network(name=self.NETWORK_NAME) @@ -50,7 +50,7 @@ def tearDown(self): ) self.assertIsNone(sot) self.user_cloud.network.delete_network(self.NETWORK_ID) - super(ShareNetworkTest, self).tearDown() + super().tearDown() def test_get(self): sot = self.user_cloud.shared_file_system.get_share_network( diff --git a/openstack/tests/functional/shared_file_system/test_share_snapshot.py b/openstack/tests/functional/shared_file_system/test_share_snapshot.py index 8134ea9ac..aed32fa5f 100644 --- a/openstack/tests/functional/shared_file_system/test_share_snapshot.py +++ b/openstack/tests/functional/shared_file_system/test_share_snapshot.py @@ -15,7 +15,7 @@ class ShareSnapshotTest(base.BaseSharedFileSystemTest): def setUp(self): - super(ShareSnapshotTest, self).setUp() + super().setUp() self.SHARE_NAME = self.getUniqueString() self.SNAPSHOT_NAME = self.getUniqueString() @@ -65,7 +65,7 @@ def tearDown(self): self.SHARE_ID, ignore_missing=False ) self.assertIsNone(sot) - super(ShareSnapshotTest, self).tearDown() + super().tearDown() def test_get(self): sot = self.operator_cloud.shared_file_system.get_share_snapshot( diff --git a/openstack/tests/functional/shared_file_system/test_share_snapshot_instance.py b/openstack/tests/functional/shared_file_system/test_share_snapshot_instance.py index 8eae2ee1b..f426895ff 100644 --- a/openstack/tests/functional/shared_file_system/test_share_snapshot_instance.py +++ b/openstack/tests/functional/shared_file_system/test_share_snapshot_instance.py @@ -15,7 +15,7 @@ class ShareSnapshotInstanceTest(base.BaseSharedFileSystemTest): def setUp(self): - super(ShareSnapshotInstanceTest, self).setUp() + super().setUp() self.SHARE_NAME = self.getUniqueString() my_share = self.create_share( diff --git a/openstack/tests/unit/accelerator/v2/test_device_profile.py b/openstack/tests/unit/accelerator/v2/test_device_profile.py index f686acc20..ce50241d7 100644 --- a/openstack/tests/unit/accelerator/v2/test_device_profile.py +++ b/openstack/tests/unit/accelerator/v2/test_device_profile.py @@ -16,8 +16,8 @@ FAKE = { "id": 1, - "uuid": u"a95e10ae-b3e3-4eab-a513-1afae6f17c51", - "name": u'afaas_example_1', + "uuid": "a95e10ae-b3e3-4eab-a513-1afae6f17c51", + "name": 'afaas_example_1', "groups": [ { "resources:ACCELERATOR_FPGA": "1", diff --git a/openstack/tests/unit/accelerator/v2/test_proxy.py b/openstack/tests/unit/accelerator/v2/test_proxy.py index 848e75cff..bd2bbdd43 100644 --- a/openstack/tests/unit/accelerator/v2/test_proxy.py +++ b/openstack/tests/unit/accelerator/v2/test_proxy.py @@ -19,7 +19,7 @@ class TestAcceleratorProxy(test_proxy_base.TestProxyBase): def setUp(self): - super(TestAcceleratorProxy, self).setUp() + super().setUp() self.proxy = _proxy.Proxy(self.session) diff --git a/openstack/tests/unit/baremetal/test_configdrive.py b/openstack/tests/unit/baremetal/test_configdrive.py index a2ba7d9a8..e560dd2d9 100644 --- a/openstack/tests/unit/baremetal/test_configdrive.py +++ b/openstack/tests/unit/baremetal/test_configdrive.py @@ -79,7 +79,7 @@ def test_with_user_data(self): self._check({'foo': 42}, b'I am user data') def test_with_user_data_as_string(self): - self._check({'foo': 42}, u'I am user data') + self._check({'foo': 42}, 'I am user data') def test_with_network_data(self): self._check({'foo': 42}, network_data={'networks': {}}) diff --git a/openstack/tests/unit/baremetal/v1/test_allocation.py b/openstack/tests/unit/baremetal/v1/test_allocation.py index 5d6bd3087..48b782563 100644 --- a/openstack/tests/unit/baremetal/v1/test_allocation.py +++ b/openstack/tests/unit/baremetal/v1/test_allocation.py @@ -77,7 +77,7 @@ def test_instantiate(self): @mock.patch.object(allocation.Allocation, 'fetch', autospec=True) class TestWaitForAllocation(base.TestCase): def setUp(self): - super(TestWaitForAllocation, self).setUp() + super().setUp() self.session = mock.Mock(spec=adapter.Adapter) self.session.default_microversion = '1.52' self.session.log = mock.Mock() diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 51ebc2e7c..022f99466 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -206,7 +206,7 @@ def test_list(self): @mock.patch.object(node.Node, 'fetch', autospec=True) class TestNodeWaitForProvisionState(base.TestCase): def setUp(self): - super(TestNodeWaitForProvisionState, self).setUp() + super().setUp() self.node = node.Node(**FAKE) self.session = mock.Mock() @@ -297,7 +297,7 @@ def _get_side_effect(_self, session): @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) class TestNodeSetProvisionState(base.TestCase): def setUp(self): - super(TestNodeSetProvisionState, self).setUp() + super().setUp() self.node = node.Node(**FAKE) self.session = mock.Mock( spec=adapter.Adapter, default_microversion=None @@ -429,7 +429,7 @@ def test_set_provision_state_service(self): @mock.patch.object(node.Node, 'set_provision_state', autospec=True) class TestNodeCreate(base.TestCase): def setUp(self): - super(TestNodeCreate, self).setUp() + super().setUp() self.new_state = None self.session = mock.Mock(spec=adapter.Adapter) self.session.default_microversion = '1.1' @@ -541,7 +541,7 @@ def test_manageable_new_version(self, mock_prov): @mock.patch.object(node.Node, '_get_session', lambda self, x: x) class TestNodeVif(base.TestCase): def setUp(self): - super(TestNodeVif, self).setUp() + super().setUp() self.session = mock.Mock(spec=adapter.Adapter) self.session.default_microversion = '1.28' self.session.log = mock.Mock() @@ -577,7 +577,7 @@ def test_attach_vif_no_retries(self): def test_detach_vif_existing(self): self.assertTrue(self.node.detach_vif(self.session, self.vif_id)) self.session.delete.assert_called_once_with( - 'nodes/%s/vifs/%s' % (self.node.id, self.vif_id), + f'nodes/{self.node.id}/vifs/{self.vif_id}', headers=mock.ANY, microversion='1.28', retriable_status_codes=_common.RETRIABLE_STATUS_CODES, @@ -587,7 +587,7 @@ def test_detach_vif_missing(self): self.session.delete.return_value.status_code = 400 self.assertFalse(self.node.detach_vif(self.session, self.vif_id)) self.session.delete.assert_called_once_with( - 'nodes/%s/vifs/%s' % (self.node.id, self.vif_id), + f'nodes/{self.node.id}/vifs/{self.vif_id}', headers=mock.ANY, microversion='1.28', retriable_status_codes=_common.RETRIABLE_STATUS_CODES, @@ -631,7 +631,7 @@ def test_incompatible_microversion(self): @mock.patch.object(node.Node, '_get_session', lambda self, x: x) class TestNodeValidate(base.TestCase): def setUp(self): - super(TestNodeValidate, self).setUp() + super().setUp() self.session = mock.Mock(spec=adapter.Adapter) self.session.default_microversion = '1.28' self.node = node.Node(**FAKE) @@ -690,7 +690,7 @@ def test_validate_no_failure(self): @mock.patch.object(node.Node, 'fetch', autospec=True) class TestNodeWaitForReservation(base.TestCase): def setUp(self): - super(TestNodeWaitForReservation, self).setUp() + super().setUp() self.session = mock.Mock(spec=adapter.Adapter) self.session.default_microversion = '1.6' self.session.log = mock.Mock() @@ -760,7 +760,7 @@ def test_incompatible_microversion(self): @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) class TestNodeSetPowerState(base.TestCase): def setUp(self): - super(TestNodeSetPowerState, self).setUp() + super().setUp() self.node = node.Node(**FAKE) self.session = mock.Mock( spec=adapter.Adapter, default_microversion=None @@ -792,7 +792,7 @@ def test_soft_power_on(self): @mock.patch.object(node.Node, '_get_session', lambda self, x: x) class TestNodeMaintenance(base.TestCase): def setUp(self): - super(TestNodeMaintenance, self).setUp() + super().setUp() self.node = node.Node.existing(**FAKE) self.session = mock.Mock( spec=adapter.Adapter, @@ -962,7 +962,7 @@ def test_get_supported_boot_devices(self): @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) class TestNodeSetBootMode(base.TestCase): def setUp(self): - super(TestNodeSetBootMode, self).setUp() + super().setUp() self.node = node.Node(**FAKE) self.session = mock.Mock( spec=adapter.Adapter, default_microversion='1.1' @@ -989,7 +989,7 @@ def test_node_set_boot_mode_invalid_mode(self): @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) class TestNodeSetSecureBoot(base.TestCase): def setUp(self): - super(TestNodeSetSecureBoot, self).setUp() + super().setUp() self.node = node.Node(**FAKE) self.session = mock.Mock( spec=adapter.Adapter, default_microversion='1.1' @@ -1016,7 +1016,7 @@ def test_node_set_secure_boot_invalid_none(self): @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) class TestNodeTraits(base.TestCase): def setUp(self): - super(TestNodeTraits, self).setUp() + super().setUp() self.node = node.Node(**FAKE) self.session = mock.Mock( spec=adapter.Adapter, default_microversion='1.37' @@ -1026,7 +1026,7 @@ def setUp(self): def test_node_add_trait(self): self.node.add_trait(self.session, 'CUSTOM_FAKE') self.session.put.assert_called_once_with( - 'nodes/%s/traits/%s' % (self.node.id, 'CUSTOM_FAKE'), + 'nodes/{}/traits/{}'.format(self.node.id, 'CUSTOM_FAKE'), json=None, headers=mock.ANY, microversion='1.37', @@ -1036,7 +1036,7 @@ def test_node_add_trait(self): def test_remove_trait(self): self.assertTrue(self.node.remove_trait(self.session, 'CUSTOM_FAKE')) self.session.delete.assert_called_once_with( - 'nodes/%s/traits/%s' % (self.node.id, 'CUSTOM_FAKE'), + 'nodes/{}/traits/{}'.format(self.node.id, 'CUSTOM_FAKE'), headers=mock.ANY, microversion='1.37', retriable_status_codes=_common.RETRIABLE_STATUS_CODES, @@ -1048,7 +1048,7 @@ def test_remove_trait_missing(self): self.node.remove_trait(self.session, 'CUSTOM_MISSING') ) self.session.delete.assert_called_once_with( - 'nodes/%s/traits/%s' % (self.node.id, 'CUSTOM_MISSING'), + 'nodes/{}/traits/{}'.format(self.node.id, 'CUSTOM_MISSING'), headers=mock.ANY, microversion='1.37', retriable_status_codes=_common.RETRIABLE_STATUS_CODES, @@ -1070,7 +1070,7 @@ def test_set_traits(self): @mock.patch.object(resource.Resource, 'patch', autospec=True) class TestNodePatch(base.TestCase): def setUp(self): - super(TestNodePatch, self).setUp() + super().setUp() self.node = node.Node(**FAKE) self.session = mock.Mock( spec=adapter.Adapter, default_microversion=None @@ -1113,7 +1113,7 @@ def test_node_patch_reset_interfaces( @mock.patch.object(node.Node, 'fetch', autospec=True) class TestNodeWaitForPowerState(base.TestCase): def setUp(self): - super(TestNodeWaitForPowerState, self).setUp() + super().setUp() self.node = node.Node(**FAKE) self.session = mock.Mock() @@ -1143,9 +1143,9 @@ def test_timeout(self, mock_fetch): @mock.patch.object(utils, 'pick_microversion', lambda session, v: v) @mock.patch.object(node.Node, 'fetch', lambda self, session: self) @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) -class TestNodePassthru(object): +class TestNodePassthru: def setUp(self): - super(TestNodePassthru, self).setUp() + super().setUp() self.node = node.Node(**FAKE) self.session = node.Mock( spec=adapter.Adapter, default_microversion='1.37' diff --git a/openstack/tests/unit/baremetal/v1/test_proxy.py b/openstack/tests/unit/baremetal/v1/test_proxy.py index 834b29a77..5d14bb312 100644 --- a/openstack/tests/unit/baremetal/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal/v1/test_proxy.py @@ -31,7 +31,7 @@ class TestBaremetalProxy(test_proxy_base.TestProxyBase): def setUp(self): - super(TestBaremetalProxy, self).setUp() + super().setUp() self.proxy = _proxy.Proxy(self.session) @@ -349,7 +349,7 @@ def test__get_with_fields_port(self, mock_fetch): @mock.patch.object(_proxy.Proxy, 'get_node', autospec=True) class TestWaitForNodesProvisionState(base.TestCase): def setUp(self): - super(TestWaitForNodesProvisionState, self).setUp() + super().setUp() self.session = mock.Mock() self.proxy = _proxy.Proxy(self.session) diff --git a/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py b/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py index 19975aca6..3419ce6fd 100644 --- a/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal_introspection/v1/test_proxy.py @@ -26,7 +26,7 @@ @mock.patch.object(introspection.Introspection, 'create', autospec=True) class TestStartIntrospection(base.TestCase): def setUp(self): - super(TestStartIntrospection, self).setUp() + super().setUp() self.session = mock.Mock(spec=adapter.Adapter) self.proxy = _proxy.Proxy(self.session) @@ -53,7 +53,7 @@ def test_create_introspection_manage_boot(self, mock_create): class TestBaremetalIntrospectionProxy(test_proxy_base.TestProxyBase): def setUp(self): - super(TestBaremetalIntrospectionProxy, self).setUp() + super().setUp() self.proxy = _proxy.Proxy(self.session) def test_get_introspection(self): @@ -66,7 +66,7 @@ def test_get_introspection(self): @mock.patch.object(introspection.Introspection, 'fetch', autospec=True) class TestWaitForIntrospection(base.TestCase): def setUp(self): - super(TestWaitForIntrospection, self).setUp() + super().setUp() self.session = mock.Mock(spec=adapter.Adapter) self.proxy = _proxy.Proxy(self.session) self.fake = {'state': 'waiting', 'error': None, 'finished': False} @@ -136,7 +136,7 @@ def _side_effect(allocation, session): @mock.patch.object(_proxy.Proxy, 'request', autospec=True) class TestAbortIntrospection(base.TestCase): def setUp(self): - super(TestAbortIntrospection, self).setUp() + super().setUp() self.session = mock.Mock(spec=adapter.Adapter) self.proxy = _proxy.Proxy(self.session) self.fake = {'id': '1234', 'finished': False} @@ -158,7 +158,7 @@ def test_abort(self, mock_request): @mock.patch.object(_proxy.Proxy, 'request', autospec=True) class TestGetData(base.TestCase): def setUp(self): - super(TestGetData, self).setUp() + super().setUp() self.session = mock.Mock(spec=adapter.Adapter) self.proxy = _proxy.Proxy(self.session) self.fake = {'id': '1234', 'finished': False} diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index b3b40a62c..b1d609ce8 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -88,7 +88,7 @@ class TestCase(base.TestCase): def setUp(self, cloud_config_fixture='clouds.yaml'): """Run before each test method to initialize test environment.""" - super(TestCase, self).setUp() + super().setUp() # Sleeps are for real testing, but unit tests shouldn't need them realsleep = time.sleep @@ -204,7 +204,7 @@ def get_mock_url( to_join.extend([urllib.parse.quote(i) for i in append]) if qs_elements is not None: qs = '?%s' % '&'.join(qs_elements) - return '%(uri)s%(qs)s' % {'uri': '/'.join(to_join), 'qs': qs} + return '{uri}{qs}'.format(uri='/'.join(to_join), qs=qs) def mock_for_keystone_projects( self, @@ -481,7 +481,7 @@ def use_broken_keystone(self): dict( method='GET', uri='https://identity.example.com/', - text=open(self.discovery_json, 'r').read(), + text=open(self.discovery_json).read(), ), dict( method='POST', @@ -532,7 +532,7 @@ def get_keystone_v3_token( ) def get_keystone_discovery(self): - with open(self.discovery_json, 'r') as discovery_file: + with open(self.discovery_json) as discovery_file: return dict( method='GET', uri='https://identity.example.com/', @@ -591,7 +591,7 @@ def get_cinder_discovery_mock_dict( return dict( method='GET', uri=block_storage_discovery_url, - text=open(discovery_fixture, 'r').read(), + text=open(discovery_fixture).read(), ) def get_glance_discovery_mock_dict( @@ -606,7 +606,7 @@ def get_glance_discovery_mock_dict( method='GET', uri=image_discovery_url, status_code=300, - text=open(discovery_fixture, 'r').read(), + text=open(discovery_fixture).read(), ) def get_nova_discovery_mock_dict( @@ -620,7 +620,7 @@ def get_nova_discovery_mock_dict( return dict( method='GET', uri=compute_discovery_url, - text=open(discovery_fixture, 'r').read(), + text=open(discovery_fixture).read(), ) def get_placement_discovery_mock_dict( @@ -632,7 +632,7 @@ def get_placement_discovery_mock_dict( return dict( method='GET', uri="https://placement.example.com/", - text=open(discovery_fixture, 'r').read(), + text=open(discovery_fixture).read(), ) def get_designate_discovery_mock_dict(self): @@ -640,7 +640,7 @@ def get_designate_discovery_mock_dict(self): return dict( method='GET', uri="https://dns.example.com/", - text=open(discovery_fixture, 'r').read(), + text=open(discovery_fixture).read(), ) def get_ironic_discovery_mock_dict(self): @@ -650,7 +650,7 @@ def get_ironic_discovery_mock_dict(self): return dict( method='GET', uri="https://baremetal.example.com/", - text=open(discovery_fixture, 'r').read(), + text=open(discovery_fixture).read(), ) def get_senlin_discovery_mock_dict(self): @@ -660,7 +660,7 @@ def get_senlin_discovery_mock_dict(self): return dict( method='GET', uri="https://clustering.example.com/", - text=open(discovery_fixture, 'r').read(), + text=open(discovery_fixture).read(), ) def use_compute_discovery( @@ -683,7 +683,7 @@ def get_cyborg_discovery_mock_dict(self): return dict( method='GET', uri="https://accelerator.example.com/", - text=open(discovery_fixture, 'r').read(), + text=open(discovery_fixture).read(), ) def get_manila_discovery_mock_dict(self): @@ -693,7 +693,7 @@ def get_manila_discovery_mock_dict(self): return dict( method='GET', uri="https://shared-file-system.example.com/", - text=open(discovery_fixture, 'r').read(), + text=open(discovery_fixture).read(), ) def use_glance( @@ -815,7 +815,7 @@ def __do_register_uris(self, uri_mock_list=None): method=method, uri=uri, params=kw_params ) validate = to_mock.pop('validate', {}) - valid_keys = set(['json', 'headers', 'params', 'data']) + valid_keys = {'json', 'headers', 'params', 'data'} invalid_keys = set(validate.keys()) - valid_keys if invalid_keys: raise TypeError( @@ -827,7 +827,7 @@ def __do_register_uris(self, uri_mock_list=None): to_mock.pop('headers', {}) ) if 'content-type' not in headers: - headers[u'content-type'] = 'application/json' + headers['content-type'] = 'application/json' if 'exc' not in to_mock: to_mock['headers'] = headers @@ -855,7 +855,7 @@ def __do_register_uris(self, uri_mock_list=None): mock_method, mock_uri, params['response_list'], - **params['kw_params'] + **params['kw_params'], ) def assert_no_calls(self): @@ -910,7 +910,7 @@ def assert_calls(self, stop_after=None, do_count=True): self.assertEqual( call['json'], history.json(), - 'json content mismatch in call {index}'.format(index=x), + f'json content mismatch in call {x}', ) # headers in a call isn't exhaustive - it's checking to make sure # a specific header or headers are there, not that they are the @@ -920,7 +920,7 @@ def assert_calls(self, stop_after=None, do_count=True): self.assertEqual( value, history.headers[key], - 'header mismatch in call {index}'.format(index=x), + f'header mismatch in call {x}', ) if do_count: self.assertEqual( @@ -964,7 +964,7 @@ def assertResourceListEqual(self, actual, expected, resource_type): class IronicTestCase(TestCase): def setUp(self): - super(IronicTestCase, self).setUp() + super().setUp() self.use_ironic() self.uuid = str(uuid.uuid4()) self.name = self.getUniqueString('name') @@ -973,4 +973,4 @@ def get_mock_url(self, **kwargs): kwargs.setdefault('service_type', 'baremetal') kwargs.setdefault('interface', 'public') kwargs.setdefault('base_url_append', 'v1') - return super(IronicTestCase, self).get_mock_url(**kwargs) + return super().get_mock_url(**kwargs) diff --git a/openstack/tests/unit/block_storage/v2/test_backup.py b/openstack/tests/unit/block_storage/v2/test_backup.py index 7de7902a5..040063024 100644 --- a/openstack/tests/unit/block_storage/v2/test_backup.py +++ b/openstack/tests/unit/block_storage/v2/test_backup.py @@ -41,7 +41,7 @@ class TestBackup(base.TestCase): def setUp(self): - super(TestBackup, self).setUp() + super().setUp() self.resp = mock.Mock() self.resp.body = None self.resp.json = mock.Mock(return_value=self.resp.body) diff --git a/openstack/tests/unit/block_storage/v2/test_snapshot.py b/openstack/tests/unit/block_storage/v2/test_snapshot.py index f81522f4d..0c2909404 100644 --- a/openstack/tests/unit/block_storage/v2/test_snapshot.py +++ b/openstack/tests/unit/block_storage/v2/test_snapshot.py @@ -80,7 +80,7 @@ def test_create_basic(self): class TestSnapshotActions(base.TestCase): def setUp(self): - super(TestSnapshotActions, self).setUp() + super().setUp() self.resp = mock.Mock() self.resp.body = None self.resp.json = mock.Mock(return_value=self.resp.body) diff --git a/openstack/tests/unit/block_storage/v2/test_stats.py b/openstack/tests/unit/block_storage/v2/test_stats.py index 029fcaba2..0de797015 100644 --- a/openstack/tests/unit/block_storage/v2/test_stats.py +++ b/openstack/tests/unit/block_storage/v2/test_stats.py @@ -31,7 +31,7 @@ class TestBackendPools(base.TestCase): def setUp(self): - super(TestBackendPools, self).setUp() + super().setUp() def test_basic(self): sot = stats.Pools(POOLS) diff --git a/openstack/tests/unit/block_storage/v2/test_type.py b/openstack/tests/unit/block_storage/v2/test_type.py index 967b4e143..d67934d06 100644 --- a/openstack/tests/unit/block_storage/v2/test_type.py +++ b/openstack/tests/unit/block_storage/v2/test_type.py @@ -24,7 +24,7 @@ class TestType(base.TestCase): def setUp(self): - super(TestType, self).setUp() + super().setUp() self.extra_specs_result = {"extra_specs": {"go": "cubs", "boo": "sox"}} self.resp = mock.Mock() self.resp.body = None diff --git a/openstack/tests/unit/block_storage/v2/test_volume.py b/openstack/tests/unit/block_storage/v2/test_volume.py index a8120f493..4e86bf155 100644 --- a/openstack/tests/unit/block_storage/v2/test_volume.py +++ b/openstack/tests/unit/block_storage/v2/test_volume.py @@ -22,12 +22,12 @@ IMAGE_METADATA = { 'container_format': 'bare', 'min_ram': '64', - 'disk_format': u'qcow2', + 'disk_format': 'qcow2', 'image_name': 'TestVM', 'image_id': '625d4f2c-cf67-4af3-afb6-c7220f766947', 'checksum': '64d7c1cd2b6f60c92c14662941cb7913', 'min_disk': '0', - u'size': '13167616', + 'size': '13167616', } VOLUME = { @@ -139,7 +139,7 @@ def test_create(self): class TestVolumeActions(TestVolume): def setUp(self): - super(TestVolumeActions, self).setUp() + super().setUp() self.resp = mock.Mock() self.resp.body = None self.resp.status_code = 200 diff --git a/openstack/tests/unit/block_storage/v3/test_attachment.py b/openstack/tests/unit/block_storage/v3/test_attachment.py index af05a64ea..af725a0ae 100644 --- a/openstack/tests/unit/block_storage/v3/test_attachment.py +++ b/openstack/tests/unit/block_storage/v3/test_attachment.py @@ -69,7 +69,7 @@ class TestAttachment(base.TestCase): def setUp(self): - super(TestAttachment, self).setUp() + super().setUp() self.resp = mock.Mock() self.resp.body = None self.resp.json = mock.Mock(return_value=self.resp.body) diff --git a/openstack/tests/unit/block_storage/v3/test_backup.py b/openstack/tests/unit/block_storage/v3/test_backup.py index 5e73d61c9..a80b97d46 100644 --- a/openstack/tests/unit/block_storage/v3/test_backup.py +++ b/openstack/tests/unit/block_storage/v3/test_backup.py @@ -45,7 +45,7 @@ class TestBackup(base.TestCase): def setUp(self): - super(TestBackup, self).setUp() + super().setUp() self.resp = mock.Mock() self.resp.body = None self.resp.json = mock.Mock(return_value=self.resp.body) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 5545118a9..cc581bf9d 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -33,7 +33,7 @@ class TestVolumeProxy(test_proxy_base.TestProxyBase): def setUp(self): - super(TestVolumeProxy, self).setUp() + super().setUp() self.proxy = _proxy.Proxy(self.session) diff --git a/openstack/tests/unit/block_storage/v3/test_snapshot.py b/openstack/tests/unit/block_storage/v3/test_snapshot.py index 6f5498fc9..b377d5fca 100644 --- a/openstack/tests/unit/block_storage/v3/test_snapshot.py +++ b/openstack/tests/unit/block_storage/v3/test_snapshot.py @@ -88,7 +88,7 @@ def test_create_basic(self): class TestSnapshotActions(base.TestCase): def setUp(self): - super(TestSnapshotActions, self).setUp() + super().setUp() self.resp = mock.Mock() self.resp.body = None self.resp.json = mock.Mock(return_value=self.resp.body) diff --git a/openstack/tests/unit/block_storage/v3/test_type.py b/openstack/tests/unit/block_storage/v3/test_type.py index b1817c418..7386a5fd6 100644 --- a/openstack/tests/unit/block_storage/v3/test_type.py +++ b/openstack/tests/unit/block_storage/v3/test_type.py @@ -30,7 +30,7 @@ class TestType(base.TestCase): def setUp(self): - super(TestType, self).setUp() + super().setUp() self.extra_specs_result = {"extra_specs": {"go": "cubs", "boo": "sox"}} self.resp = mock.Mock() self.resp.body = None diff --git a/openstack/tests/unit/block_storage/v3/test_volume.py b/openstack/tests/unit/block_storage/v3/test_volume.py index df2e234d8..03d06351d 100644 --- a/openstack/tests/unit/block_storage/v3/test_volume.py +++ b/openstack/tests/unit/block_storage/v3/test_volume.py @@ -23,12 +23,12 @@ IMAGE_METADATA = { 'container_format': 'bare', 'min_ram': '64', - 'disk_format': u'qcow2', + 'disk_format': 'qcow2', 'image_name': 'TestVM', 'image_id': '625d4f2c-cf67-4af3-afb6-c7220f766947', 'checksum': '64d7c1cd2b6f60c92c14662941cb7913', 'min_disk': '0', - u'size': '13167616', + 'size': '13167616', } FAKE_HOST = "fake_host@fake_backend#fake_pool" @@ -145,7 +145,7 @@ def test_create(self): class TestVolumeActions(TestVolume): def setUp(self): - super(TestVolumeActions, self).setUp() + super().setUp() self.resp = mock.Mock() self.resp.body = None self.resp.status_code = 200 diff --git a/openstack/tests/unit/cloud/test__utils.py b/openstack/tests/unit/cloud/test__utils.py index 273a3f6e2..6aac3f834 100644 --- a/openstack/tests/unit/cloud/test__utils.py +++ b/openstack/tests/unit/cloud/test__utils.py @@ -1,5 +1,3 @@ -# -*- 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 @@ -86,13 +84,13 @@ def test__filter_list_name_or_id_glob_not_found(self): def test__filter_list_unicode(self): el1 = dict( id=100, - name=u'中文', + name='中文', last='duck', other=dict(category='duck', financial=dict(status='poor')), ) el2 = dict( id=200, - name=u'中文', + name='中文', last='trump', other=dict(category='human', financial=dict(status='rich')), ) @@ -104,7 +102,7 @@ def test__filter_list_unicode(self): ) data = [el1, el2, el3] ret = _utils._filter_list( - data, u'中文', {'other': {'financial': {'status': 'rich'}}} + data, '中文', {'other': {'financial': {'status': 'rich'}}} ) self.assertEqual([el2], ret) diff --git a/openstack/tests/unit/cloud/test_accelerator.py b/openstack/tests/unit/cloud/test_accelerator.py index 5afe6f3f6..2fdb4e2eb 100644 --- a/openstack/tests/unit/cloud/test_accelerator.py +++ b/openstack/tests/unit/cloud/test_accelerator.py @@ -82,7 +82,7 @@ class TestAccelerator(base.TestCase): def setUp(self): - super(TestAccelerator, self).setUp() + super().setUp() self.use_cyborg() def test_list_deployables(self): diff --git a/openstack/tests/unit/cloud/test_aggregate.py b/openstack/tests/unit/cloud/test_aggregate.py index d5c265513..d0d8f8453 100644 --- a/openstack/tests/unit/cloud/test_aggregate.py +++ b/openstack/tests/unit/cloud/test_aggregate.py @@ -16,7 +16,7 @@ class TestAggregate(base.TestCase): def setUp(self): - super(TestAggregate, self).setUp() + super().setUp() self.aggregate_name = self.getUniqueString('aggregate') self.fake_aggregate = fakes.make_fake_aggregate(1, self.aggregate_name) self.use_compute_discovery() diff --git a/openstack/tests/unit/cloud/test_baremetal_node.py b/openstack/tests/unit/cloud/test_baremetal_node.py index c768d2378..f81fdd3e0 100644 --- a/openstack/tests/unit/cloud/test_baremetal_node.py +++ b/openstack/tests/unit/cloud/test_baremetal_node.py @@ -29,7 +29,7 @@ class TestBaremetalNode(base.IronicTestCase): def setUp(self): - super(TestBaremetalNode, self).setUp() + super().setUp() self.fake_baremetal_node = fakes.make_fake_machine( self.name, self.uuid ) @@ -2367,7 +2367,7 @@ class TestUpdateMachinePatch(base.IronicTestCase): # variables that need to be tested. def setUp(self): - super(TestUpdateMachinePatch, self).setUp() + super().setUp() self.fake_baremetal_node = fakes.make_fake_machine( self.name, self.uuid ) diff --git a/openstack/tests/unit/cloud/test_baremetal_ports.py b/openstack/tests/unit/cloud/test_baremetal_ports.py index 19c5cff54..3cd4c043d 100644 --- a/openstack/tests/unit/cloud/test_baremetal_ports.py +++ b/openstack/tests/unit/cloud/test_baremetal_ports.py @@ -26,7 +26,7 @@ class TestBaremetalPort(base.IronicTestCase): def setUp(self): - super(TestBaremetalPort, self).setUp() + super().setUp() self.fake_baremetal_node = fakes.make_fake_machine( self.name, self.uuid ) diff --git a/openstack/tests/unit/cloud/test_cluster_templates.py b/openstack/tests/unit/cloud/test_cluster_templates.py index 75dd40b49..e25105e71 100644 --- a/openstack/tests/unit/cloud/test_cluster_templates.py +++ b/openstack/tests/unit/cloud/test_cluster_templates.py @@ -62,7 +62,7 @@ def get_mock_url( append=None, resource=None, ): - return super(TestClusterTemplates, self).get_mock_url( + return super().get_mock_url( service_type=service_type, resource=resource, append=append, diff --git a/openstack/tests/unit/cloud/test_clustering.py b/openstack/tests/unit/cloud/test_clustering.py index 7c0d1dc4a..7a5da7f3a 100644 --- a/openstack/tests/unit/cloud/test_clustering.py +++ b/openstack/tests/unit/cloud/test_clustering.py @@ -64,7 +64,7 @@ def _compare_clusters(self, exp, real): ) def setUp(self): - super(TestClustering, self).setUp() + super().setUp() self.use_senlin() diff --git a/openstack/tests/unit/cloud/test_coe_clusters.py b/openstack/tests/unit/cloud/test_coe_clusters.py index 833b86584..97e864e09 100644 --- a/openstack/tests/unit/cloud/test_coe_clusters.py +++ b/openstack/tests/unit/cloud/test_coe_clusters.py @@ -51,7 +51,7 @@ def get_mock_url( append=None, resource=None, ): - return super(TestCOEClusters, self).get_mock_url( + return super().get_mock_url( service_type=service_type, resource=resource, append=append, diff --git a/openstack/tests/unit/cloud/test_coe_clusters_certificate.py b/openstack/tests/unit/cloud/test_coe_clusters_certificate.py index 57aabe7f4..f1e56e8ca 100644 --- a/openstack/tests/unit/cloud/test_coe_clusters_certificate.py +++ b/openstack/tests/unit/cloud/test_coe_clusters_certificate.py @@ -51,7 +51,7 @@ def get_mock_url( append=None, resource=None, ): - return super(TestCOEClusters, self).get_mock_url( + return super().get_mock_url( service_type=service_type, resource=resource, append=append, diff --git a/openstack/tests/unit/cloud/test_compute.py b/openstack/tests/unit/cloud/test_compute.py index 90f984505..f032b1709 100644 --- a/openstack/tests/unit/cloud/test_compute.py +++ b/openstack/tests/unit/cloud/test_compute.py @@ -49,7 +49,7 @@ def test__nova_extensions(self): ] ) extensions = self.cloud._nova_extensions() - self.assertEqual(set(['NMN', 'OS-DCF']), extensions) + self.assertEqual({'NMN', 'OS-DCF'}, extensions) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_create_volume_snapshot.py b/openstack/tests/unit/cloud/test_create_volume_snapshot.py index 12ca3aeaf..f19211ba4 100644 --- a/openstack/tests/unit/cloud/test_create_volume_snapshot.py +++ b/openstack/tests/unit/cloud/test_create_volume_snapshot.py @@ -26,7 +26,7 @@ class TestCreateVolumeSnapshot(base.TestCase): def setUp(self): - super(TestCreateVolumeSnapshot, self).setUp() + super().setUp() self.use_cinder() def _compare_snapshots(self, exp, real): diff --git a/openstack/tests/unit/cloud/test_delete_volume_snapshot.py b/openstack/tests/unit/cloud/test_delete_volume_snapshot.py index 77314b42d..6e1c543a8 100644 --- a/openstack/tests/unit/cloud/test_delete_volume_snapshot.py +++ b/openstack/tests/unit/cloud/test_delete_volume_snapshot.py @@ -25,7 +25,7 @@ class TestDeleteVolumeSnapshot(base.TestCase): def setUp(self): - super(TestDeleteVolumeSnapshot, self).setUp() + super().setUp() self.use_cinder() def test_delete_volume_snapshot(self): diff --git a/openstack/tests/unit/cloud/test_domains.py b/openstack/tests/unit/cloud/test_domains.py index 761e7ced5..0150d23e2 100644 --- a/openstack/tests/unit/cloud/test_domains.py +++ b/openstack/tests/unit/cloud/test_domains.py @@ -31,7 +31,7 @@ def get_mock_url( base_url_append='v3', qs_elements=None, ): - return super(TestDomains, self).get_mock_url( + return super().get_mock_url( service_type=service_type, resource=resource, append=append, diff --git a/openstack/tests/unit/cloud/test_endpoints.py b/openstack/tests/unit/cloud/test_endpoints.py index 6077464b5..d992e1555 100644 --- a/openstack/tests/unit/cloud/test_endpoints.py +++ b/openstack/tests/unit/cloud/test_endpoints.py @@ -35,7 +35,7 @@ def get_mock_url( append=None, base_url_append='v3', ): - return super(TestCloudEndpoints, self).get_mock_url( + return super().get_mock_url( service_type, interface, resource, append, base_url_append ) diff --git a/openstack/tests/unit/cloud/test_flavors.py b/openstack/tests/unit/cloud/test_flavors.py index 3f9230f59..f65dab2cd 100644 --- a/openstack/tests/unit/cloud/test_flavors.py +++ b/openstack/tests/unit/cloud/test_flavors.py @@ -17,7 +17,7 @@ class TestFlavors(base.TestCase): def setUp(self): - super(TestFlavors, self).setUp() + super().setUp() # self.use_compute_discovery() def test_create_flavor(self): diff --git a/openstack/tests/unit/cloud/test_floating_ip_common.py b/openstack/tests/unit/cloud/test_floating_ip_common.py index 420af74de..d61a93181 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_common.py +++ b/openstack/tests/unit/cloud/test_floating_ip_common.py @@ -106,8 +106,8 @@ def test_add_ips_to_server_ipv6_only( 'private': [{'addr': "10.223.160.141", 'version': 4}], 'public': [ { - u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:ae:7d:42', - u'OS-EXT-IPS:type': u'fixed', + 'OS-EXT-IPS-MAC:mac_addr': 'fa:16:3e:ae:7d:42', + 'OS-EXT-IPS:type': 'fixed', 'addr': "2001:4800:7819:103:be76:4eff:fe05:8525", 'version': 6, } diff --git a/openstack/tests/unit/cloud/test_floating_ip_neutron.py b/openstack/tests/unit/cloud/test_floating_ip_neutron.py index e23337581..ad821cb67 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_neutron.py +++ b/openstack/tests/unit/cloud/test_floating_ip_neutron.py @@ -131,7 +131,7 @@ def assertAreInstances(self, elements, elem_type): self.assertIsInstance(e, elem_type) def setUp(self): - super(TestFloatingIP, self).setUp() + super().setUp() self.fake_server = fakes.make_fake_server( 'server-id', @@ -741,7 +741,7 @@ def test_delete_floating_ip_existing(self): uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'floatingips/{0}'.format(fip_id)], + append=['v2.0', f'floatingips/{fip_id}'], ), json={}, ), @@ -757,7 +757,7 @@ def test_delete_floating_ip_existing(self): uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'floatingips/{0}'.format(fip_id)], + append=['v2.0', f'floatingips/{fip_id}'], ), json={}, ), @@ -773,7 +773,7 @@ def test_delete_floating_ip_existing(self): uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'floatingips/{0}'.format(fip_id)], + append=['v2.0', f'floatingips/{fip_id}'], ), json={}, ), @@ -811,7 +811,7 @@ def test_delete_floating_ip_existing_down(self): uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'floatingips/{0}'.format(fip_id)], + append=['v2.0', f'floatingips/{fip_id}'], ), json={}, ), @@ -827,7 +827,7 @@ def test_delete_floating_ip_existing_down(self): uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'floatingips/{0}'.format(fip_id)], + append=['v2.0', f'floatingips/{fip_id}'], ), json={}, ), @@ -860,7 +860,7 @@ def test_delete_floating_ip_existing_no_delete(self): uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'floatingips/{0}'.format(fip_id)], + append=['v2.0', f'floatingips/{fip_id}'], ), json={}, ), @@ -876,7 +876,7 @@ def test_delete_floating_ip_existing_no_delete(self): uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'floatingips/{0}'.format(fip_id)], + append=['v2.0', f'floatingips/{fip_id}'], ), json={}, ), @@ -892,7 +892,7 @@ def test_delete_floating_ip_existing_no_delete(self): uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'floatingips/{0}'.format(fip_id)], + append=['v2.0', f'floatingips/{fip_id}'], ), json={}, ), @@ -945,7 +945,7 @@ def test_attach_ip_to_server(self): 'network', 'public', append=['v2.0', 'ports'], - qs_elements=["device_id={0}".format(device_id)], + qs_elements=[f"device_id={device_id}"], ), json={'ports': self.mock_search_ports_rep}, ), @@ -954,7 +954,7 @@ def test_attach_ip_to_server(self): uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'floatingips/{0}'.format(fip['id'])], + append=['v2.0', 'floatingips/{}'.format(fip['id'])], ), json={ 'floatingip': self.mock_floating_ip_list_rep[ @@ -999,7 +999,7 @@ def test_detach_ip_from_server(self): uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'floatingips/{0}'.format(fip['id'])], + append=['v2.0', 'floatingips/{}'.format(fip['id'])], ), json={'floatingip': fip}, validate=dict(json={'floatingip': {'port_id': None}}), @@ -1060,7 +1060,7 @@ def test_add_ip_from_pool(self): 'public', append=['v2.0', 'ports'], qs_elements=[ - "device_id={0}".format(self.fake_server['id']) + "device_id={}".format(self.fake_server['id']) ], ), json={'ports': self.mock_search_ports_rep}, @@ -1070,7 +1070,7 @@ def test_add_ip_from_pool(self): uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'floatingips/{0}'.format(fip['id'])], + append=['v2.0', 'floatingips/{}'.format(fip['id'])], ), json={'floatingip': fip}, validate=dict( @@ -1141,7 +1141,7 @@ def test_cleanup_floating_ips(self): 'public', append=[ 'v2.0', - 'floatingips/{0}'.format(floating_ips[0]['id']), + 'floatingips/{}'.format(floating_ips[0]['id']), ], ), json={}, @@ -1161,7 +1161,7 @@ def test_cleanup_floating_ips(self): 'public', append=[ 'v2.0', - 'floatingips/{0}'.format(floating_ips[1]['id']), + 'floatingips/{}'.format(floating_ips[1]['id']), ], ), json={}, diff --git a/openstack/tests/unit/cloud/test_floating_ip_nova.py b/openstack/tests/unit/cloud/test_floating_ip_nova.py index 9bd9ade49..a1a1d390a 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_nova.py +++ b/openstack/tests/unit/cloud/test_floating_ip_nova.py @@ -67,19 +67,19 @@ def assertAreInstances(self, elements, elem_type): self.assertIsInstance(e, elem_type) def setUp(self): - super(TestFloatingIP, self).setUp() + super().setUp() self.fake_server = fakes.make_fake_server( 'server-id', '', 'ACTIVE', addresses={ - u'test_pnztt_net': [ + 'test_pnztt_net': [ { - u'OS-EXT-IPS:type': u'fixed', - u'addr': '192.0.2.129', - u'version': 4, - u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:ae:7d:42', + 'OS-EXT-IPS:type': 'fixed', + 'addr': '192.0.2.129', + 'version': 4, + 'OS-EXT-IPS-MAC:mac_addr': 'fa:16:3e:ae:7d:42', } ] }, diff --git a/openstack/tests/unit/cloud/test_fwaas.py b/openstack/tests/unit/cloud/test_fwaas.py index 708e12d27..599509ce3 100644 --- a/openstack/tests/unit/cloud/test_fwaas.py +++ b/openstack/tests/unit/cloud/test_fwaas.py @@ -52,7 +52,7 @@ class TestFirewallRule(FirewallTestCase): mock_firewall_rule = None def setUp(self, cloud_config_fixture='clouds.yaml'): - super(TestFirewallRule, self).setUp() + super().setUp() self.mock_firewall_rule = FirewallRule( connection=self.cloud, **self._mock_firewall_rule_attrs ).to_dict() @@ -367,7 +367,7 @@ class TestFirewallPolicy(FirewallTestCase): mock_firewall_policy = None def setUp(self, cloud_config_fixture='clouds.yaml'): - super(TestFirewallPolicy, self).setUp() + super().setUp() self.mock_firewall_policy = FirewallPolicy( connection=self.cloud, **self._mock_firewall_policy_attrs ).to_dict() @@ -1206,7 +1206,7 @@ class TestFirewallGroup(FirewallTestCase): mock_returned_firewall_rule = None def setUp(self, cloud_config_fixture='clouds.yaml'): - super(TestFirewallGroup, self).setUp() + super().setUp() self.mock_egress_policy = FirewallPolicy( connection=self.cloud, **self._mock_egress_policy_attrs ).to_dict() diff --git a/openstack/tests/unit/cloud/test_groups.py b/openstack/tests/unit/cloud/test_groups.py index dab4bc3e6..273852dd3 100644 --- a/openstack/tests/unit/cloud/test_groups.py +++ b/openstack/tests/unit/cloud/test_groups.py @@ -16,9 +16,7 @@ class TestGroups(base.TestCase): def setUp(self, cloud_config_fixture='clouds.yaml'): - super(TestGroups, self).setUp( - cloud_config_fixture=cloud_config_fixture - ) + super().setUp(cloud_config_fixture=cloud_config_fixture) self.addCleanup(self.assert_calls) def get_mock_url( @@ -29,7 +27,7 @@ def get_mock_url( append=None, base_url_append='v3', ): - return super(TestGroups, self).get_mock_url( + return super().get_mock_url( service_type='identity', interface=interface, resource=resource, diff --git a/openstack/tests/unit/cloud/test_identity_roles.py b/openstack/tests/unit/cloud/test_identity_roles.py index c89562834..1349d7ec2 100644 --- a/openstack/tests/unit/cloud/test_identity_roles.py +++ b/openstack/tests/unit/cloud/test_identity_roles.py @@ -44,7 +44,7 @@ def get_mock_url( base_url_append='v3', qs_elements=None, ): - return super(TestIdentityRoles, self).get_mock_url( + return super().get_mock_url( service_type, interface, resource, @@ -75,7 +75,7 @@ def test_list_role_by_name(self): dict( method='GET', uri=self.get_mock_url( - qs_elements=['name={0}'.format(role_data.role_name)] + qs_elements=[f'name={role_data.role_name}'] ), status_code=200, json={'roles': [role_data.json_response['role']]}, diff --git a/openstack/tests/unit/cloud/test_identity_users.py b/openstack/tests/unit/cloud/test_identity_users.py index 93b85500a..97e6ca433 100644 --- a/openstack/tests/unit/cloud/test_identity_users.py +++ b/openstack/tests/unit/cloud/test_identity_users.py @@ -25,7 +25,7 @@ def get_mock_url( base_url_append='v3', qs_elements=None, ): - return super(TestIdentityUsers, self).get_mock_url( + return super().get_mock_url( service_type, interface, resource, diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 8204895cf..18fc33853 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -32,10 +32,10 @@ class BaseTestImage(base.TestCase): def setUp(self): - super(BaseTestImage, self).setUp() + super().setUp() self.image_id = str(uuid.uuid4()) self.image_name = self.getUniqueString('image') - self.object_name = 'images/{name}'.format(name=self.image_name) + self.object_name = f'images/{self.image_name}' self.imagefile = tempfile.NamedTemporaryFile(delete=False) data = b'\2\0' self.imagefile.write(data) @@ -64,7 +64,7 @@ def _compare_images_v1(self, exp, real): class TestImage(BaseTestImage): def setUp(self): - super(TestImage, self).setUp() + super().setUp() self.use_glance() def test_download_image_no_output(self): @@ -428,7 +428,7 @@ def test_list_images_paginated(self): 'image', append=['images'], base_url_append='v2', - qs_elements=['marker={marker}'.format(marker=marker)], + qs_elements=[f'marker={marker}'], ), json=self.fake_search_return, ), @@ -1217,7 +1217,7 @@ def _call_create_image(self, name, **kwargs): timeout=1, is_public=False, validate_checksum=True, - **kwargs + **kwargs, ) def test_create_image_put_v1(self): @@ -1385,7 +1385,7 @@ def test_update_image_no_patch(self): 'owner_specified.openstack.object': 'images/{name}'.format( name=self.image_name ) - } + }, ) self.assert_calls() @@ -1854,7 +1854,7 @@ def test_create_image_put_protected(self): class TestImageSuburl(BaseTestImage): def setUp(self): - super(TestImageSuburl, self).setUp() + super().setUp() self.os_fixture.use_suburl() self.os_fixture.build_tokens() self.use_keystone_v3() @@ -1903,7 +1903,7 @@ def test_list_images_paginated(self): 'image', append=['images'], base_url_append='v2', - qs_elements=['marker={marker}'.format(marker=marker)], + qs_elements=[f'marker={marker}'], ), json=self.fake_search_return, ), @@ -1921,7 +1921,7 @@ def test_list_images_paginated(self): class TestImageVolume(BaseTestImage): def setUp(self): - super(TestImageVolume, self).setUp() + super().setUp() self.volume_id = str(uuid.uuid4()) def test_create_image_volume(self): diff --git a/openstack/tests/unit/cloud/test_image_snapshot.py b/openstack/tests/unit/cloud/test_image_snapshot.py index 71006b611..bf497f794 100644 --- a/openstack/tests/unit/cloud/test_image_snapshot.py +++ b/openstack/tests/unit/cloud/test_image_snapshot.py @@ -21,7 +21,7 @@ class TestImageSnapshot(base.TestCase): def setUp(self): - super(TestImageSnapshot, self).setUp() + super().setUp() self.server_id = str(uuid.uuid4()) self.image_id = str(uuid.uuid4()) self.server_name = self.getUniqueString('name') diff --git a/openstack/tests/unit/cloud/test_inventory.py b/openstack/tests/unit/cloud/test_inventory.py index c6520897e..b2c65a5e2 100644 --- a/openstack/tests/unit/cloud/test_inventory.py +++ b/openstack/tests/unit/cloud/test_inventory.py @@ -20,7 +20,7 @@ class TestInventory(base.TestCase): def setUp(self): - super(TestInventory, self).setUp() + super().setUp() @mock.patch("openstack.config.loader.OpenStackConfig") @mock.patch("openstack.connection.Connection") diff --git a/openstack/tests/unit/cloud/test_keypair.py b/openstack/tests/unit/cloud/test_keypair.py index 433cf13c3..79694f404 100644 --- a/openstack/tests/unit/cloud/test_keypair.py +++ b/openstack/tests/unit/cloud/test_keypair.py @@ -20,7 +20,7 @@ class TestKeypair(base.TestCase): def setUp(self): - super(TestKeypair, self).setUp() + super().setUp() self.keyname = self.getUniqueString('key') self.key = fakes.make_fake_keypair(self.keyname) self.useFixture( diff --git a/openstack/tests/unit/cloud/test_limits.py b/openstack/tests/unit/cloud/test_limits.py index ed67290fd..6fbf8a531 100644 --- a/openstack/tests/unit/cloud/test_limits.py +++ b/openstack/tests/unit/cloud/test_limits.py @@ -68,9 +68,7 @@ def test_other_get_compute_limits(self): 'compute', 'public', append=['limits'], - qs_elements=[ - 'tenant_id={id}'.format(id=project.project_id) - ], + qs_elements=[f'tenant_id={project.project_id}'], ), json={ "limits": { diff --git a/openstack/tests/unit/cloud/test_meta.py b/openstack/tests/unit/cloud/test_meta.py index 7a03c3a4c..7b40371d5 100644 --- a/openstack/tests/unit/cloud/test_meta.py +++ b/openstack/tests/unit/cloud/test_meta.py @@ -103,136 +103,132 @@ def get_default_network(self): SUBNETS_WITH_NAT = [ { - u'name': u'', - u'enable_dhcp': True, - u'network_id': u'5ef0358f-9403-4f7b-9151-376ca112abf7', - u'tenant_id': u'29c79f394b2946f1a0f8446d715dc301', - u'dns_nameservers': [], - u'ipv6_ra_mode': None, - u'allocation_pools': [ - {u'start': u'10.10.10.2', u'end': u'10.10.10.254'} - ], - u'gateway_ip': u'10.10.10.1', - u'ipv6_address_mode': None, - u'ip_version': 4, - u'host_routes': [], - u'cidr': u'10.10.10.0/24', - u'id': u'14025a85-436e-4418-b0ee-f5b12a50f9b4', + 'name': '', + 'enable_dhcp': True, + 'network_id': '5ef0358f-9403-4f7b-9151-376ca112abf7', + 'tenant_id': '29c79f394b2946f1a0f8446d715dc301', + 'dns_nameservers': [], + 'ipv6_ra_mode': None, + 'allocation_pools': [{'start': '10.10.10.2', 'end': '10.10.10.254'}], + 'gateway_ip': '10.10.10.1', + 'ipv6_address_mode': None, + 'ip_version': 4, + 'host_routes': [], + 'cidr': '10.10.10.0/24', + 'id': '14025a85-436e-4418-b0ee-f5b12a50f9b4', }, ] OSIC_NETWORKS = [ { - u'admin_state_up': True, - u'id': u'7004a83a-13d3-4dcd-8cf5-52af1ace4cae', - u'mtu': 0, - u'name': u'GATEWAY_NET', - u'router:external': True, - u'shared': True, - u'status': u'ACTIVE', - u'subnets': [u'cf785ee0-6cc9-4712-be3d-0bf6c86cf455'], - u'tenant_id': u'7a1ca9f7cc4e4b13ac0ed2957f1e8c32', + 'admin_state_up': True, + 'id': '7004a83a-13d3-4dcd-8cf5-52af1ace4cae', + 'mtu': 0, + 'name': 'GATEWAY_NET', + 'router:external': True, + 'shared': True, + 'status': 'ACTIVE', + 'subnets': ['cf785ee0-6cc9-4712-be3d-0bf6c86cf455'], + 'tenant_id': '7a1ca9f7cc4e4b13ac0ed2957f1e8c32', }, { - u'admin_state_up': True, - u'id': u'405abfcc-77dc-49b2-a271-139619ac9b26', - u'mtu': 0, - u'name': u'openstackjenkins-network1', - u'router:external': False, - u'shared': False, - u'status': u'ACTIVE', - u'subnets': [u'a47910bc-f649-45db-98ec-e2421c413f4e'], - u'tenant_id': u'7e9c4d5842b3451d94417bd0af03a0f4', + 'admin_state_up': True, + 'id': '405abfcc-77dc-49b2-a271-139619ac9b26', + 'mtu': 0, + 'name': 'openstackjenkins-network1', + 'router:external': False, + 'shared': False, + 'status': 'ACTIVE', + 'subnets': ['a47910bc-f649-45db-98ec-e2421c413f4e'], + 'tenant_id': '7e9c4d5842b3451d94417bd0af03a0f4', }, { - u'admin_state_up': True, - u'id': u'54753d2c-0a58-4928-9b32-084c59dd20a6', - u'mtu': 0, - u'name': u'GATEWAY_NET_V6', - u'router:external': True, - u'shared': True, - u'status': u'ACTIVE', - u'subnets': [ - u'9c21d704-a8b9-409a-b56d-501cb518d380', - u'7cb0ce07-64c3-4a3d-92d3-6f11419b45b9', + 'admin_state_up': True, + 'id': '54753d2c-0a58-4928-9b32-084c59dd20a6', + 'mtu': 0, + 'name': 'GATEWAY_NET_V6', + 'router:external': True, + 'shared': True, + 'status': 'ACTIVE', + 'subnets': [ + '9c21d704-a8b9-409a-b56d-501cb518d380', + '7cb0ce07-64c3-4a3d-92d3-6f11419b45b9', ], - u'tenant_id': u'7a1ca9f7cc4e4b13ac0ed2957f1e8c32', + 'tenant_id': '7a1ca9f7cc4e4b13ac0ed2957f1e8c32', }, ] OSIC_SUBNETS = [ { - u'allocation_pools': [ - {u'end': u'172.99.106.254', u'start': u'172.99.106.5'} + 'allocation_pools': [ + {'end': '172.99.106.254', 'start': '172.99.106.5'} ], - u'cidr': u'172.99.106.0/24', - u'dns_nameservers': [u'69.20.0.164', u'69.20.0.196'], - u'enable_dhcp': True, - u'gateway_ip': u'172.99.106.1', - u'host_routes': [], - u'id': u'cf785ee0-6cc9-4712-be3d-0bf6c86cf455', - u'ip_version': 4, - u'ipv6_address_mode': None, - u'ipv6_ra_mode': None, - u'name': u'GATEWAY_NET', - u'network_id': u'7004a83a-13d3-4dcd-8cf5-52af1ace4cae', - u'subnetpool_id': None, - u'tenant_id': u'7a1ca9f7cc4e4b13ac0ed2957f1e8c32', + 'cidr': '172.99.106.0/24', + 'dns_nameservers': ['69.20.0.164', '69.20.0.196'], + 'enable_dhcp': True, + 'gateway_ip': '172.99.106.1', + 'host_routes': [], + 'id': 'cf785ee0-6cc9-4712-be3d-0bf6c86cf455', + 'ip_version': 4, + 'ipv6_address_mode': None, + 'ipv6_ra_mode': None, + 'name': 'GATEWAY_NET', + 'network_id': '7004a83a-13d3-4dcd-8cf5-52af1ace4cae', + 'subnetpool_id': None, + 'tenant_id': '7a1ca9f7cc4e4b13ac0ed2957f1e8c32', }, { - u'allocation_pools': [{u'end': u'10.0.1.254', u'start': u'10.0.1.2'}], - u'cidr': u'10.0.1.0/24', - u'dns_nameservers': [u'8.8.8.8', u'8.8.4.4'], - u'enable_dhcp': True, - u'gateway_ip': u'10.0.1.1', - u'host_routes': [], - u'id': u'a47910bc-f649-45db-98ec-e2421c413f4e', - u'ip_version': 4, - u'ipv6_address_mode': None, - u'ipv6_ra_mode': None, - u'name': u'openstackjenkins-subnet1', - u'network_id': u'405abfcc-77dc-49b2-a271-139619ac9b26', - u'subnetpool_id': None, - u'tenant_id': u'7e9c4d5842b3451d94417bd0af03a0f4', + 'allocation_pools': [{'end': '10.0.1.254', 'start': '10.0.1.2'}], + 'cidr': '10.0.1.0/24', + 'dns_nameservers': ['8.8.8.8', '8.8.4.4'], + 'enable_dhcp': True, + 'gateway_ip': '10.0.1.1', + 'host_routes': [], + 'id': 'a47910bc-f649-45db-98ec-e2421c413f4e', + 'ip_version': 4, + 'ipv6_address_mode': None, + 'ipv6_ra_mode': None, + 'name': 'openstackjenkins-subnet1', + 'network_id': '405abfcc-77dc-49b2-a271-139619ac9b26', + 'subnetpool_id': None, + 'tenant_id': '7e9c4d5842b3451d94417bd0af03a0f4', }, { - u'allocation_pools': [ - {u'end': u'10.255.255.254', u'start': u'10.0.0.2'} - ], - u'cidr': u'10.0.0.0/8', - u'dns_nameservers': [u'8.8.8.8', u'8.8.4.4'], - u'enable_dhcp': True, - u'gateway_ip': u'10.0.0.1', - u'host_routes': [], - u'id': u'9c21d704-a8b9-409a-b56d-501cb518d380', - u'ip_version': 4, - u'ipv6_address_mode': None, - u'ipv6_ra_mode': None, - u'name': u'GATEWAY_SUBNET_V6V4', - u'network_id': u'54753d2c-0a58-4928-9b32-084c59dd20a6', - u'subnetpool_id': None, - u'tenant_id': u'7a1ca9f7cc4e4b13ac0ed2957f1e8c32', + 'allocation_pools': [{'end': '10.255.255.254', 'start': '10.0.0.2'}], + 'cidr': '10.0.0.0/8', + 'dns_nameservers': ['8.8.8.8', '8.8.4.4'], + 'enable_dhcp': True, + 'gateway_ip': '10.0.0.1', + 'host_routes': [], + 'id': '9c21d704-a8b9-409a-b56d-501cb518d380', + 'ip_version': 4, + 'ipv6_address_mode': None, + 'ipv6_ra_mode': None, + 'name': 'GATEWAY_SUBNET_V6V4', + 'network_id': '54753d2c-0a58-4928-9b32-084c59dd20a6', + 'subnetpool_id': None, + 'tenant_id': '7a1ca9f7cc4e4b13ac0ed2957f1e8c32', }, { - u'allocation_pools': [ + 'allocation_pools': [ { - u'end': u'2001:4800:1ae1:18:ffff:ffff:ffff:ffff', - u'start': u'2001:4800:1ae1:18::2', + 'end': '2001:4800:1ae1:18:ffff:ffff:ffff:ffff', + 'start': '2001:4800:1ae1:18::2', } ], - u'cidr': u'2001:4800:1ae1:18::/64', - u'dns_nameservers': [u'2001:4860:4860::8888'], - u'enable_dhcp': True, - u'gateway_ip': u'2001:4800:1ae1:18::1', - u'host_routes': [], - u'id': u'7cb0ce07-64c3-4a3d-92d3-6f11419b45b9', - u'ip_version': 6, - u'ipv6_address_mode': u'dhcpv6-stateless', - u'ipv6_ra_mode': None, - u'name': u'GATEWAY_SUBNET_V6V6', - u'network_id': u'54753d2c-0a58-4928-9b32-084c59dd20a6', - u'subnetpool_id': None, - u'tenant_id': u'7a1ca9f7cc4e4b13ac0ed2957f1e8c32', + 'cidr': '2001:4800:1ae1:18::/64', + 'dns_nameservers': ['2001:4860:4860::8888'], + 'enable_dhcp': True, + 'gateway_ip': '2001:4800:1ae1:18::1', + 'host_routes': [], + 'id': '7cb0ce07-64c3-4a3d-92d3-6f11419b45b9', + 'ip_version': 6, + 'ipv6_address_mode': 'dhcpv6-stateless', + 'ipv6_ra_mode': None, + 'name': 'GATEWAY_SUBNET_V6V6', + 'network_id': '54753d2c-0a58-4928-9b32-084c59dd20a6', + 'subnetpool_id': None, + 'tenant_id': '7a1ca9f7cc4e4b13ac0ed2957f1e8c32', }, ] @@ -460,18 +456,18 @@ def test_get_server_private_ip_devstack( server_id='test-id', name='test-name', status='ACTIVE', - flavor={u'id': u'1'}, + flavor={'id': '1'}, image={ - 'name': u'cirros-0.3.4-x86_64-uec', - u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1', + 'name': 'cirros-0.3.4-x86_64-uec', + 'id': 'f93d000b-7c29-4489-b375-3641a1758fe1', }, addresses={ - u'test_pnztt_net': [ + 'test_pnztt_net': [ { - u'OS-EXT-IPS:type': u'fixed', - u'addr': PRIVATE_V4, - u'version': 4, - u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:ae:7d:42', + 'OS-EXT-IPS:type': 'fixed', + 'addr': PRIVATE_V4, + 'version': 4, + 'OS-EXT-IPS-MAC:mac_addr': 'fa:16:3e:ae:7d:42', } ] }, @@ -563,18 +559,18 @@ def test_get_server_private_ip_no_fip( server_id='test-id', name='test-name', status='ACTIVE', - flavor={u'id': u'1'}, + flavor={'id': '1'}, image={ - 'name': u'cirros-0.3.4-x86_64-uec', - u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1', + 'name': 'cirros-0.3.4-x86_64-uec', + 'id': 'f93d000b-7c29-4489-b375-3641a1758fe1', }, addresses={ - u'test_pnztt_net': [ + 'test_pnztt_net': [ { - u'OS-EXT-IPS:type': u'fixed', - u'addr': PRIVATE_V4, - u'version': 4, - u'OS-EXT-IPS-MAC:mac_addr': u'fa:16:3e:ae:7d:42', + 'OS-EXT-IPS:type': 'fixed', + 'addr': PRIVATE_V4, + 'version': 4, + 'OS-EXT-IPS-MAC:mac_addr': 'fa:16:3e:ae:7d:42', } ] }, @@ -641,16 +637,16 @@ def test_get_server_cloud_no_fips( server_id='test-id', name='test-name', status='ACTIVE', - flavor={u'id': u'1'}, + flavor={'id': '1'}, image={ - 'name': u'cirros-0.3.4-x86_64-uec', - u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1', + 'name': 'cirros-0.3.4-x86_64-uec', + 'id': 'f93d000b-7c29-4489-b375-3641a1758fe1', }, addresses={ - u'test_pnztt_net': [ + 'test_pnztt_net': [ { - u'addr': PRIVATE_V4, - u'version': 4, + 'addr': PRIVATE_V4, + 'version': 4, } ] }, @@ -722,16 +718,16 @@ def test_get_server_cloud_missing_fips( server_id='test-id', name='test-name', status='ACTIVE', - flavor={u'id': u'1'}, + flavor={'id': '1'}, image={ - 'name': u'cirros-0.3.4-x86_64-uec', - u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1', + 'name': 'cirros-0.3.4-x86_64-uec', + 'id': 'f93d000b-7c29-4489-b375-3641a1758fe1', }, addresses={ - u'test_pnztt_net': [ + 'test_pnztt_net': [ { - u'addr': PRIVATE_V4, - u'version': 4, + 'addr': PRIVATE_V4, + 'version': 4, 'OS-EXT-IPS-MAC:mac_addr': 'fa:16:3e:ae:7d:42', } ] @@ -838,10 +834,10 @@ def test_get_server_cloud_rackspace_v6( server_id='test-id', name='test-name', status='ACTIVE', - flavor={u'id': u'1'}, + flavor={'id': '1'}, image={ - 'name': u'cirros-0.3.4-x86_64-uec', - u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1', + 'name': 'cirros-0.3.4-x86_64-uec', + 'id': 'f93d000b-7c29-4489-b375-3641a1758fe1', }, addresses={ 'private': [{'addr': "10.223.160.141", 'version': 4}], @@ -910,10 +906,10 @@ def test_get_server_cloud_osic_split( server_id='test-id', name='test-name', status='ACTIVE', - flavor={u'id': u'1'}, + flavor={'id': '1'}, image={ - 'name': u'cirros-0.3.4-x86_64-uec', - u'id': u'f93d000b-7c29-4489-b375-3641a1758fe1', + 'name': 'cirros-0.3.4-x86_64-uec', + 'id': 'f93d000b-7c29-4489-b375-3641a1758fe1', }, addresses={ 'private': [{'addr': "10.223.160.141", 'version': 4}], @@ -1354,7 +1350,7 @@ def test_current_location(self): 'domain_id': None, 'domain_name': 'default', }, - 'region_name': u'RegionOne', + 'region_name': 'RegionOne', 'zone': None, }, self.cloud.current_location, diff --git a/openstack/tests/unit/cloud/test_network.py b/openstack/tests/unit/cloud/test_network.py index 1490799c0..e7ae1da97 100644 --- a/openstack/tests/unit/cloud/test_network.py +++ b/openstack/tests/unit/cloud/test_network.py @@ -50,7 +50,7 @@ def test__neutron_extensions(self): ] ) extensions = self.cloud._neutron_extensions() - self.assertEqual(set(['dvr', 'allowed-address-pairs']), extensions) + self.assertEqual({'dvr', 'allowed-address-pairs'}, extensions) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_object.py b/openstack/tests/unit/cloud/test_object.py index 643b41d6e..fc7a1f4c3 100644 --- a/openstack/tests/unit/cloud/test_object.py +++ b/openstack/tests/unit/cloud/test_object.py @@ -28,7 +28,7 @@ class BaseTestObject(base.TestCase): def setUp(self): - super(BaseTestObject, self).setUp() + super().setUp() self.container = self.getUniqueString() self.object = self.getUniqueString() @@ -333,8 +333,8 @@ def test_get_container_access_not_found(self): self.cloud.get_container_access(self.container) def test_list_containers(self): - endpoint = '{endpoint}/'.format(endpoint=self.endpoint) - containers = [{u'count': 0, u'bytes': 0, u'name': self.container}] + endpoint = f'{self.endpoint}/' + containers = [{'count': 0, 'bytes': 0, 'name': self.container}] self.register_uris( [ @@ -354,7 +354,7 @@ def test_list_containers(self): self._compare_containers(a, b) def test_list_containers_exception(self): - endpoint = '{endpoint}/'.format(endpoint=self.endpoint) + endpoint = f'{self.endpoint}/' self.register_uris( [ dict( @@ -598,11 +598,11 @@ def test_list_objects(self): objects = [ { - u'bytes': 20304400896, - u'last_modified': u'2016-12-15T13:34:13.650090', - u'hash': u'daaf9ed2106d09bba96cf193d866445e', - u'name': self.object, - u'content_type': u'application/octet-stream', + 'bytes': 20304400896, + 'last_modified': '2016-12-15T13:34:13.650090', + 'hash': 'daaf9ed2106d09bba96cf193d866445e', + 'name': self.object, + 'content_type': 'application/octet-stream', } ] @@ -623,11 +623,11 @@ def test_list_objects_with_prefix(self): objects = [ { - u'bytes': 20304400896, - u'last_modified': u'2016-12-15T13:34:13.650090', - u'hash': u'daaf9ed2106d09bba96cf193d866445e', - u'name': self.object, - u'content_type': u'application/octet-stream', + 'bytes': 20304400896, + 'last_modified': '2016-12-15T13:34:13.650090', + 'hash': 'daaf9ed2106d09bba96cf193d866445e', + 'name': self.object, + 'content_type': 'application/octet-stream', } ] @@ -879,7 +879,7 @@ def test_update_container_cors(self): class TestObjectUploads(BaseTestObject): def setUp(self): - super(TestObjectUploads, self).setUp() + super().setUp() self.content = self.getUniqueString().encode('latin-1') self.object_file = tempfile.NamedTemporaryFile(delete=False) @@ -1106,7 +1106,7 @@ def test_create_static_large_object(self): index=index, ), status_code=201, - headers=dict(Etag='etag{index}'.format(index=index)), + headers=dict(Etag=f'etag{index}'), ) for index, offset in enumerate( range(0, len(self.content), max_file_size) @@ -1228,7 +1228,7 @@ def test_slo_manifest_retry(self): index=index, ), status_code=201, - headers=dict(Etag='etag{index}'.format(index=index)), + headers=dict(Etag=f'etag{index}'), ) for index, offset in enumerate( range(0, len(self.content), max_file_size) @@ -1387,7 +1387,7 @@ def test_slo_manifest_fail(self): index=index, ), status_code=201, - headers=dict(Etag='etag{index}'.format(index=index)), + headers=dict(Etag=f'etag{index}'), ) for index, offset in enumerate( range(0, len(self.content), max_file_size) diff --git a/openstack/tests/unit/cloud/test_openstackcloud.py b/openstack/tests/unit/cloud/test_openstackcloud.py index d2a9e33b3..8f69c069f 100644 --- a/openstack/tests/unit/cloud/test_openstackcloud.py +++ b/openstack/tests/unit/cloud/test_openstackcloud.py @@ -26,7 +26,7 @@ class FakeResource(resource.Resource): foo = resource.Body("foo") def setUp(self): - super(TestSearch, self).setUp() + super().setUp() self.session = proxy.Proxy(self.cloud) self.session._sdk_connection = self.cloud diff --git a/openstack/tests/unit/cloud/test_operator_noauth.py b/openstack/tests/unit/cloud/test_operator_noauth.py index 1dacd43ac..c3b83b652 100644 --- a/openstack/tests/unit/cloud/test_operator_noauth.py +++ b/openstack/tests/unit/cloud/test_operator_noauth.py @@ -28,7 +28,7 @@ def setUp(self): Uses base.TestCase instead of IronicTestCase because we need to do completely different things with discovery. """ - super(TestOpenStackCloudOperatorNoAuth, self).setUp() + super().setUp() # By clearing the URI registry, we remove all calls to a keystone # catalog or getting a token self._uri_registry.clear() @@ -129,7 +129,7 @@ def setUp(self): Uses base.TestCase instead of IronicTestCase because we need to do completely different things with discovery. """ - super(TestOpenStackCloudOperatorNoAuthUnversioned, self).setUp() + super().setUp() # By clearing the URI registry, we remove all calls to a keystone # catalog or getting a token self._uri_registry.clear() diff --git a/openstack/tests/unit/cloud/test_project.py b/openstack/tests/unit/cloud/test_project.py index 244c9d115..dfbf4b05b 100644 --- a/openstack/tests/unit/cloud/test_project.py +++ b/openstack/tests/unit/cloud/test_project.py @@ -36,7 +36,7 @@ def get_mock_url( resource = 'tenants' if base_url_append is None and v3: base_url_append = 'v3' - return super(TestProject, self).get_mock_url( + return super().get_mock_url( service_type=service_type, interface=interface, resource=resource, diff --git a/openstack/tests/unit/cloud/test_qos_rule_type.py b/openstack/tests/unit/cloud/test_qos_rule_type.py index 374ad831d..21d85f32c 100644 --- a/openstack/tests/unit/cloud/test_qos_rule_type.py +++ b/openstack/tests/unit/cloud/test_qos_rule_type.py @@ -54,12 +54,12 @@ class TestQosRuleType(base.TestCase): { 'parameter_values': {'start': 0, 'end': 2147483647}, 'parameter_type': 'range', - 'parameter_name': u'max_kbps', + 'parameter_name': 'max_kbps', }, { 'parameter_values': ['ingress', 'egress'], 'parameter_type': 'choices', - 'parameter_name': u'direction', + 'parameter_name': 'direction', }, { 'parameter_values': {'start': 0, 'end': 2147483647}, diff --git a/openstack/tests/unit/cloud/test_quotas.py b/openstack/tests/unit/cloud/test_quotas.py index 35d61fb18..50dbe7f41 100644 --- a/openstack/tests/unit/cloud/test_quotas.py +++ b/openstack/tests/unit/cloud/test_quotas.py @@ -34,9 +34,7 @@ class TestQuotas(base.TestCase): def setUp(self, cloud_config_fixture='clouds.yaml'): - super(TestQuotas, self).setUp( - cloud_config_fixture=cloud_config_fixture - ) + super().setUp(cloud_config_fixture=cloud_config_fixture) def test_update_quotas(self): project = self._get_project_data() diff --git a/openstack/tests/unit/cloud/test_rebuild_server.py b/openstack/tests/unit/cloud/test_rebuild_server.py index cc96d91b7..b187fd037 100644 --- a/openstack/tests/unit/cloud/test_rebuild_server.py +++ b/openstack/tests/unit/cloud/test_rebuild_server.py @@ -1,5 +1,3 @@ -# -*- 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 @@ -28,7 +26,7 @@ class TestRebuildServer(base.TestCase): def setUp(self): - super(TestRebuildServer, self).setUp() + super().setUp() self.server_id = str(uuid.uuid4()) self.server_name = self.getUniqueString('name') self.fake_server = fakes.make_fake_server( diff --git a/openstack/tests/unit/cloud/test_recordset.py b/openstack/tests/unit/cloud/test_recordset.py index b5e376030..181d5b41a 100644 --- a/openstack/tests/unit/cloud/test_recordset.py +++ b/openstack/tests/unit/cloud/test_recordset.py @@ -35,7 +35,7 @@ class RecordsetTestWrapper(test_zone.ZoneTestWrapper): class TestRecordset(base.TestCase): def setUp(self): - super(TestRecordset, self).setUp() + super().setUp() self.use_designate() def test_create_recordset_zoneid(self): diff --git a/openstack/tests/unit/cloud/test_role_assignment.py b/openstack/tests/unit/cloud/test_role_assignment.py index 45d213c7f..7a0c2452e 100644 --- a/openstack/tests/unit/cloud/test_role_assignment.py +++ b/openstack/tests/unit/cloud/test_role_assignment.py @@ -48,7 +48,7 @@ def _build_role_assignment_response( ] def setUp(self, cloud_config_fixture='clouds.yaml'): - super(TestRoleAssignment, self).setUp(cloud_config_fixture) + super().setUp(cloud_config_fixture) self.role_data = self._get_role_data() self.domain_data = self._get_domain_data() self.user_data = self._get_user_data( @@ -120,7 +120,7 @@ def get_mock_url( base_url_append='v3', qs_elements=None, ): - return super(TestRoleAssignment, self).get_mock_url( + return super().get_mock_url( service_type, interface, resource, @@ -1744,7 +1744,7 @@ def test_grant_no_role(self): with testtools.ExpectedException( exceptions.SDKException, - 'Role {0} not found'.format(self.role_data.role_name), + f'Role {self.role_data.role_name} not found', ): self.cloud.grant_role( self.role_data.role_name, @@ -1784,7 +1784,7 @@ def test_revoke_no_role(self): with testtools.ExpectedException( exceptions.SDKException, - 'Role {0} not found'.format(self.role_data.role_name), + f'Role {self.role_data.role_name} not found', ): self.cloud.revoke_role( self.role_data.role_name, diff --git a/openstack/tests/unit/cloud/test_router.py b/openstack/tests/unit/cloud/test_router.py index 2d3dd6955..ab67ac385 100644 --- a/openstack/tests/unit/cloud/test_router.py +++ b/openstack/tests/unit/cloud/test_router.py @@ -32,17 +32,17 @@ class TestRouter(base.TestCase): 'admin_state_up': True, 'availability_zone_hints': [], 'availability_zones': [], - 'description': u'', + 'description': '', 'distributed': False, 'external_gateway_info': None, 'flavor_id': None, 'ha': False, 'id': router_id, 'name': router_name, - 'project_id': u'861808a93da0484ea1767967c4df8a23', + 'project_id': '861808a93da0484ea1767967c4df8a23', 'routes': [{"destination": "179.24.1.0/24", "nexthop": "172.24.3.99"}], - 'status': u'ACTIVE', - 'tenant_id': u'861808a93da0484ea1767967c4df8a23', + 'status': 'ACTIVE', + 'tenant_id': '861808a93da0484ea1767967c4df8a23', } mock_router_interface_rep = { diff --git a/openstack/tests/unit/cloud/test_security_groups.py b/openstack/tests/unit/cloud/test_security_groups.py index cee8b52a1..9ccb8a593 100644 --- a/openstack/tests/unit/cloud/test_security_groups.py +++ b/openstack/tests/unit/cloud/test_security_groups.py @@ -55,7 +55,7 @@ class TestSecurityGroups(base.TestCase): def setUp(self): - super(TestSecurityGroups, self).setUp() + super().setUp() self.has_neutron = True def fake_has_service(*args, **kwargs): diff --git a/openstack/tests/unit/cloud/test_server_console.py b/openstack/tests/unit/cloud/test_server_console.py index abfb52e6a..872fe27d8 100644 --- a/openstack/tests/unit/cloud/test_server_console.py +++ b/openstack/tests/unit/cloud/test_server_console.py @@ -18,7 +18,7 @@ class TestServerConsole(base.TestCase): def setUp(self): - super(TestServerConsole, self).setUp() + super().setUp() self.server_id = str(uuid.uuid4()) self.server_name = self.getUniqueString('name') diff --git a/openstack/tests/unit/cloud/test_server_delete_metadata.py b/openstack/tests/unit/cloud/test_server_delete_metadata.py index 3e98bfdde..94f912433 100644 --- a/openstack/tests/unit/cloud/test_server_delete_metadata.py +++ b/openstack/tests/unit/cloud/test_server_delete_metadata.py @@ -26,7 +26,7 @@ class TestServerDeleteMetadata(base.TestCase): def setUp(self): - super(TestServerDeleteMetadata, self).setUp() + super().setUp() self.server_id = str(uuid.uuid4()) self.server_name = self.getUniqueString('name') self.fake_server = fakes.make_fake_server( diff --git a/openstack/tests/unit/cloud/test_server_group.py b/openstack/tests/unit/cloud/test_server_group.py index fc1443221..14c10d23c 100644 --- a/openstack/tests/unit/cloud/test_server_group.py +++ b/openstack/tests/unit/cloud/test_server_group.py @@ -19,7 +19,7 @@ class TestServerGroup(base.TestCase): def setUp(self): - super(TestServerGroup, self).setUp() + super().setUp() self.group_id = uuid.uuid4().hex self.group_name = self.getUniqueString('server-group') self.policies = ['affinity'] diff --git a/openstack/tests/unit/cloud/test_server_set_metadata.py b/openstack/tests/unit/cloud/test_server_set_metadata.py index 974509949..d27724a27 100644 --- a/openstack/tests/unit/cloud/test_server_set_metadata.py +++ b/openstack/tests/unit/cloud/test_server_set_metadata.py @@ -26,7 +26,7 @@ class TestServerSetMetadata(base.TestCase): def setUp(self): - super(TestServerSetMetadata, self).setUp() + super().setUp() self.server_id = str(uuid.uuid4()) self.server_name = self.getUniqueString('name') self.fake_server = fakes.make_fake_server( diff --git a/openstack/tests/unit/cloud/test_services.py b/openstack/tests/unit/cloud/test_services.py index d9d2e94e8..3ea0d619b 100644 --- a/openstack/tests/unit/cloud/test_services.py +++ b/openstack/tests/unit/cloud/test_services.py @@ -27,7 +27,7 @@ class CloudServices(base.TestCase): def setUp(self, cloud_config_fixture='clouds.yaml'): - super(CloudServices, self).setUp(cloud_config_fixture) + super().setUp(cloud_config_fixture) def get_mock_url( self, @@ -37,7 +37,7 @@ def get_mock_url( append=None, base_url_append='v3', ): - return super(CloudServices, self).get_mock_url( + return super().get_mock_url( service_type, interface, resource, append, base_url_append ) diff --git a/openstack/tests/unit/cloud/test_shared_file_system.py b/openstack/tests/unit/cloud/test_shared_file_system.py index 2463b5fe8..f87fc3132 100644 --- a/openstack/tests/unit/cloud/test_shared_file_system.py +++ b/openstack/tests/unit/cloud/test_shared_file_system.py @@ -26,7 +26,7 @@ class TestSharedFileSystem(base.TestCase): def setUp(self): - super(TestSharedFileSystem, self).setUp() + super().setUp() self.use_manila() def test_list_availability_zones(self): diff --git a/openstack/tests/unit/cloud/test_stack.py b/openstack/tests/unit/cloud/test_stack.py index 0246124f1..91b7acce1 100644 --- a/openstack/tests/unit/cloud/test_stack.py +++ b/openstack/tests/unit/cloud/test_stack.py @@ -22,7 +22,7 @@ class TestStack(base.TestCase): def setUp(self): - super(TestStack, self).setUp() + super().setUp() self.stack_id = self.getUniqueString('id') self.stack_name = self.getUniqueString('name') self.stack_tag = self.getUniqueString('tag') diff --git a/openstack/tests/unit/cloud/test_update_server.py b/openstack/tests/unit/cloud/test_update_server.py index 7f800b4ab..275c0f65a 100644 --- a/openstack/tests/unit/cloud/test_update_server.py +++ b/openstack/tests/unit/cloud/test_update_server.py @@ -26,7 +26,7 @@ class TestUpdateServer(base.TestCase): def setUp(self): - super(TestUpdateServer, self).setUp() + super().setUp() self.server_id = str(uuid.uuid4()) self.server_name = self.getUniqueString('name') self.updated_server_name = self.getUniqueString('name2') diff --git a/openstack/tests/unit/cloud/test_usage.py b/openstack/tests/unit/cloud/test_usage.py index e5c34e5d7..910682bf9 100644 --- a/openstack/tests/unit/cloud/test_usage.py +++ b/openstack/tests/unit/cloud/test_usage.py @@ -1,5 +1,3 @@ -# -*- 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 @@ -34,8 +32,8 @@ def test_get_usage(self): 'public', append=['os-simple-tenant-usage', project.project_id], qs_elements=[ - 'start={now}'.format(now=start.isoformat()), - 'end={now}'.format(now=end.isoformat()), + f'start={start.isoformat()}', + f'end={end.isoformat()}', ], ), json={ diff --git a/openstack/tests/unit/cloud/test_volume_access.py b/openstack/tests/unit/cloud/test_volume_access.py index 6c33842e3..035291b7a 100644 --- a/openstack/tests/unit/cloud/test_volume_access.py +++ b/openstack/tests/unit/cloud/test_volume_access.py @@ -1,5 +1,3 @@ -# -*- 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 @@ -20,7 +18,7 @@ class TestVolumeAccess(base.TestCase): def setUp(self): - super(TestVolumeAccess, self).setUp() + super().setUp() self.use_cinder() def test_list_volume_types(self): diff --git a/openstack/tests/unit/cloud/test_volume_backups.py b/openstack/tests/unit/cloud/test_volume_backups.py index 7e2e53479..8419f0e7f 100644 --- a/openstack/tests/unit/cloud/test_volume_backups.py +++ b/openstack/tests/unit/cloud/test_volume_backups.py @@ -15,7 +15,7 @@ class TestVolumeBackups(base.TestCase): def setUp(self): - super(TestVolumeBackups, self).setUp() + super().setUp() self.use_cinder() def _compare_backups(self, exp, real): diff --git a/openstack/tests/unit/cloud/test_zone.py b/openstack/tests/unit/cloud/test_zone.py index 81a1713c4..d23a3f3ef 100644 --- a/openstack/tests/unit/cloud/test_zone.py +++ b/openstack/tests/unit/cloud/test_zone.py @@ -53,7 +53,7 @@ def cmp(self, other): class TestZone(base.TestCase): def setUp(self): - super(TestZone, self).setUp() + super().setUp() self.use_designate() def test_create_zone(self): diff --git a/openstack/tests/unit/clustering/v1/test_action.py b/openstack/tests/unit/clustering/v1/test_action.py index aeceb4317..464c3ac14 100644 --- a/openstack/tests/unit/clustering/v1/test_action.py +++ b/openstack/tests/unit/clustering/v1/test_action.py @@ -46,7 +46,7 @@ class TestAction(base.TestCase): def setUp(self): - super(TestAction, self).setUp() + super().setUp() def test_basic(self): sot = action.Action() diff --git a/openstack/tests/unit/clustering/v1/test_build_info.py b/openstack/tests/unit/clustering/v1/test_build_info.py index 490dc3dd4..a80f8608c 100644 --- a/openstack/tests/unit/clustering/v1/test_build_info.py +++ b/openstack/tests/unit/clustering/v1/test_build_info.py @@ -26,7 +26,7 @@ class TestBuildInfo(base.TestCase): def setUp(self): - super(TestBuildInfo, self).setUp() + super().setUp() def test_basic(self): sot = build_info.BuildInfo() diff --git a/openstack/tests/unit/clustering/v1/test_cluster.py b/openstack/tests/unit/clustering/v1/test_cluster.py index dc06ff43b..a359150c6 100644 --- a/openstack/tests/unit/clustering/v1/test_cluster.py +++ b/openstack/tests/unit/clustering/v1/test_cluster.py @@ -66,7 +66,7 @@ class TestCluster(base.TestCase): def setUp(self): - super(TestCluster, self).setUp() + super().setUp() def test_basic(self): sot = cluster.Cluster() diff --git a/openstack/tests/unit/clustering/v1/test_cluster_attr.py b/openstack/tests/unit/clustering/v1/test_cluster_attr.py index c0d8bd570..bdbaf56c9 100644 --- a/openstack/tests/unit/clustering/v1/test_cluster_attr.py +++ b/openstack/tests/unit/clustering/v1/test_cluster_attr.py @@ -24,7 +24,7 @@ class TestClusterAttr(base.TestCase): def setUp(self): - super(TestClusterAttr, self).setUp() + super().setUp() def test_basic(self): sot = ca.ClusterAttr() diff --git a/openstack/tests/unit/clustering/v1/test_cluster_policy.py b/openstack/tests/unit/clustering/v1/test_cluster_policy.py index c218b238b..d544664cb 100644 --- a/openstack/tests/unit/clustering/v1/test_cluster_policy.py +++ b/openstack/tests/unit/clustering/v1/test_cluster_policy.py @@ -27,7 +27,7 @@ class TestClusterPolicy(base.TestCase): def setUp(self): - super(TestClusterPolicy, self).setUp() + super().setUp() def test_basic(self): sot = cluster_policy.ClusterPolicy() diff --git a/openstack/tests/unit/clustering/v1/test_event.py b/openstack/tests/unit/clustering/v1/test_event.py index cad15ca78..598931247 100644 --- a/openstack/tests/unit/clustering/v1/test_event.py +++ b/openstack/tests/unit/clustering/v1/test_event.py @@ -35,7 +35,7 @@ class TestEvent(base.TestCase): def setUp(self): - super(TestEvent, self).setUp() + super().setUp() def test_basic(self): sot = event.Event() diff --git a/openstack/tests/unit/clustering/v1/test_policy.py b/openstack/tests/unit/clustering/v1/test_policy.py index f2fa94e41..4cbcd78c2 100644 --- a/openstack/tests/unit/clustering/v1/test_policy.py +++ b/openstack/tests/unit/clustering/v1/test_policy.py @@ -42,7 +42,7 @@ class TestPolicy(base.TestCase): def setUp(self): - super(TestPolicy, self).setUp() + super().setUp() def test_basic(self): sot = policy.Policy() @@ -70,7 +70,7 @@ def test_instantiate(self): class TestPolicyValidate(base.TestCase): def setUp(self): - super(TestPolicyValidate, self).setUp() + super().setUp() def test_basic(self): sot = policy.PolicyValidate() diff --git a/openstack/tests/unit/clustering/v1/test_profile.py b/openstack/tests/unit/clustering/v1/test_profile.py index de6d3bf7f..7a2e7ea6f 100644 --- a/openstack/tests/unit/clustering/v1/test_profile.py +++ b/openstack/tests/unit/clustering/v1/test_profile.py @@ -42,7 +42,7 @@ class TestProfile(base.TestCase): def setUp(self): - super(TestProfile, self).setUp() + super().setUp() def test_basic(self): sot = profile.Profile() @@ -72,7 +72,7 @@ def test_instantiate(self): class TestProfileValidate(base.TestCase): def setUp(self): - super(TestProfileValidate, self).setUp() + super().setUp() def test_basic(self): sot = profile.ProfileValidate() diff --git a/openstack/tests/unit/clustering/v1/test_proxy.py b/openstack/tests/unit/clustering/v1/test_proxy.py index 1b4eb4c3a..e25d90290 100644 --- a/openstack/tests/unit/clustering/v1/test_proxy.py +++ b/openstack/tests/unit/clustering/v1/test_proxy.py @@ -32,7 +32,7 @@ class TestClusterProxy(test_proxy_base.TestProxyBase): def setUp(self): - super(TestClusterProxy, self).setUp() + super().setUp() self.proxy = _proxy.Proxy(self.session) def test_build_info_get(self): diff --git a/openstack/tests/unit/clustering/v1/test_receiver.py b/openstack/tests/unit/clustering/v1/test_receiver.py index 73b33bb0d..43c61b6d9 100644 --- a/openstack/tests/unit/clustering/v1/test_receiver.py +++ b/openstack/tests/unit/clustering/v1/test_receiver.py @@ -38,7 +38,7 @@ class TestReceiver(base.TestCase): def setUp(self): - super(TestReceiver, self).setUp() + super().setUp() def test_basic(self): sot = receiver.Receiver() diff --git a/openstack/tests/unit/clustering/v1/test_service.py b/openstack/tests/unit/clustering/v1/test_service.py index 4d62202f1..303603465 100644 --- a/openstack/tests/unit/clustering/v1/test_service.py +++ b/openstack/tests/unit/clustering/v1/test_service.py @@ -28,7 +28,7 @@ class TestService(base.TestCase): def setUp(self): - super(TestService, self).setUp() + super().setUp() self.resp = mock.Mock() self.resp.body = None self.resp.json = mock.Mock(return_value=self.resp.body) diff --git a/openstack/tests/unit/common/test_metadata.py b/openstack/tests/unit/common/test_metadata.py index bc1909675..839bbd3cf 100644 --- a/openstack/tests/unit/common/test_metadata.py +++ b/openstack/tests/unit/common/test_metadata.py @@ -24,7 +24,7 @@ class TestMetadata(base.TestCase): def setUp(self): - super(TestMetadata, self).setUp() + super().setUp() self.service_name = "service" self.base_path = "base_path" diff --git a/openstack/tests/unit/common/test_quota_set.py b/openstack/tests/unit/common/test_quota_set.py index 8fdc9a3d6..c686e7bba 100644 --- a/openstack/tests/unit/common/test_quota_set.py +++ b/openstack/tests/unit/common/test_quota_set.py @@ -33,7 +33,7 @@ class TestQuotaSet(base.TestCase): def setUp(self): - super(TestQuotaSet, self).setUp() + super().setUp() self.sess = mock.Mock(spec=adapter.Adapter) self.sess.default_microversion = 1 self.sess._get_connection = mock.Mock(return_value=self.cloud) diff --git a/openstack/tests/unit/common/test_tag.py b/openstack/tests/unit/common/test_tag.py index e4483e047..390ef8999 100644 --- a/openstack/tests/unit/common/test_tag.py +++ b/openstack/tests/unit/common/test_tag.py @@ -22,7 +22,7 @@ class TestTagMixin(base.TestCase): def setUp(self): - super(TestTagMixin, self).setUp() + super().setUp() self.service_name = "service" self.base_path = "base_path" diff --git a/openstack/tests/unit/compute/v2/test_aggregate.py b/openstack/tests/unit/compute/v2/test_aggregate.py index 9bc0f6b3c..dec21ca90 100644 --- a/openstack/tests/unit/compute/v2/test_aggregate.py +++ b/openstack/tests/unit/compute/v2/test_aggregate.py @@ -35,7 +35,7 @@ class TestAggregate(base.TestCase): def setUp(self): - super(TestAggregate, self).setUp() + super().setUp() self.resp = mock.Mock() self.resp.body = EXAMPLE.copy() self.resp.json = mock.Mock(return_value=self.resp.body) diff --git a/openstack/tests/unit/compute/v2/test_flavor.py b/openstack/tests/unit/compute/v2/test_flavor.py index 35dcdff4b..8b5d2dc21 100644 --- a/openstack/tests/unit/compute/v2/test_flavor.py +++ b/openstack/tests/unit/compute/v2/test_flavor.py @@ -42,7 +42,7 @@ class TestFlavor(base.TestCase): def setUp(self): - super(TestFlavor, self).setUp() + super().setUp() self.sess = mock.Mock(spec=adapter.Adapter) self.sess.default_microversion = 1 self.sess._get_connection = mock.Mock(return_value=self.cloud) diff --git a/openstack/tests/unit/compute/v2/test_hypervisor.py b/openstack/tests/unit/compute/v2/test_hypervisor.py index 7c63f0a9a..88f4a88c7 100644 --- a/openstack/tests/unit/compute/v2/test_hypervisor.py +++ b/openstack/tests/unit/compute/v2/test_hypervisor.py @@ -71,7 +71,7 @@ class TestHypervisor(base.TestCase): def setUp(self): - super(TestHypervisor, self).setUp() + super().setUp() self.sess = mock.Mock(spec=adapter.Adapter) self.sess.default_microversion = 1 self.sess._get_connection = mock.Mock(return_value=self.cloud) @@ -147,7 +147,7 @@ def test_get_uptime(self, mv_mock): hyp = sot.get_uptime(self.sess) self.sess.get.assert_called_with( - 'os-hypervisors/{id}/uptime'.format(id=sot.id), + f'os-hypervisors/{sot.id}/uptime', microversion=self.sess.default_microversion, ) self.assertEqual(rsp['hypervisor']['uptime'], hyp.uptime) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 02d0626fa..378935f84 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -44,7 +44,7 @@ class TestComputeProxy(test_proxy_base.TestProxyBase): def setUp(self): - super(TestComputeProxy, self).setUp() + super().setUp() self.proxy = _proxy.Proxy(self.session) diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 4eb29271a..d996638ef 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -112,7 +112,7 @@ class TestServer(base.TestCase): def setUp(self): - super(TestServer, self).setUp() + super().setUp() self.resp = mock.Mock() self.resp.body = None self.resp.json = mock.Mock(return_value=self.resp.body) diff --git a/openstack/tests/unit/compute/v2/test_server_migration.py b/openstack/tests/unit/compute/v2/test_server_migration.py index 936a6ca9b..7fa486ffa 100644 --- a/openstack/tests/unit/compute/v2/test_server_migration.py +++ b/openstack/tests/unit/compute/v2/test_server_migration.py @@ -105,7 +105,7 @@ def test_force_complete(self): self.assertIsNone(sot.force_complete(self.sess)) - url = 'servers/%s/migrations/%s/action' % ( + url = 'servers/{}/migrations/{}/action'.format( EXAMPLE['server_id'], EXAMPLE['id'], ) diff --git a/openstack/tests/unit/compute/v2/test_server_remote_console.py b/openstack/tests/unit/compute/v2/test_server_remote_console.py index 96844eb3a..eac32b556 100644 --- a/openstack/tests/unit/compute/v2/test_server_remote_console.py +++ b/openstack/tests/unit/compute/v2/test_server_remote_console.py @@ -24,7 +24,7 @@ class TestServerRemoteConsole(base.TestCase): def setUp(self): - super(TestServerRemoteConsole, self).setUp() + super().setUp() self.sess = mock.Mock(spec=adapter.Adapter) self.sess.default_microversion = '2.9' self.resp = mock.Mock() diff --git a/openstack/tests/unit/compute/v2/test_service.py b/openstack/tests/unit/compute/v2/test_service.py index 65bd629cc..44665dd8f 100644 --- a/openstack/tests/unit/compute/v2/test_service.py +++ b/openstack/tests/unit/compute/v2/test_service.py @@ -29,7 +29,7 @@ class TestService(base.TestCase): def setUp(self): - super(TestService, self).setUp() + super().setUp() self.resp = mock.Mock() self.resp.body = {'service': {}} self.resp.json = mock.Mock(return_value=self.resp.body) diff --git a/openstack/tests/unit/config/base.py b/openstack/tests/unit/config/base.py index 2ed642d5f..8f0f470f5 100644 --- a/openstack/tests/unit/config/base.py +++ b/openstack/tests/unit/config/base.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Copyright 2010-2011 OpenStack Foundation # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # @@ -242,7 +240,7 @@ class TestCase(base.TestCase): """Test case base class for all unit tests.""" def setUp(self): - super(TestCase, self).setUp() + super().setUp() conf = copy.deepcopy(USER_CONF) tdir = self.useFixture(fixtures.TempDir()) diff --git a/openstack/tests/unit/config/test_config.py b/openstack/tests/unit/config/test_config.py index 0d865e910..e6e9bcff8 100644 --- a/openstack/tests/unit/config/test_config.py +++ b/openstack/tests/unit/config/test_config.py @@ -719,7 +719,7 @@ class TestExcludedFormattedConfigValue(base.TestCase): # (e.g. 'foo{bar}') which raises a KeyError. def setUp(self): - super(TestExcludedFormattedConfigValue, self).setUp() + super().setUp() self.args = dict( auth_url='http://example.com/v2', @@ -772,7 +772,7 @@ def test_get_one_cloud_osc_password_brace(self): class TestConfigArgparse(base.TestCase): def setUp(self): - super(TestConfigArgparse, self).setUp() + super().setUp() self.args = dict( auth_url='http://example.com/v2', @@ -1360,7 +1360,7 @@ def test_register_argparse_network_service_types(self): class TestConfigPrompt(base.TestCase): def setUp(self): - super(TestConfigPrompt, self).setUp() + super().setUp() self.args = dict( auth_url='http://example.com/v2', @@ -1391,7 +1391,7 @@ def test_get_one_prompt(self): class TestConfigDefault(base.TestCase): def setUp(self): - super(TestConfigDefault, self).setUp() + super().setUp() # Reset defaults after each test so that other tests are # not affected by any changes. diff --git a/openstack/tests/unit/config/test_environ.py b/openstack/tests/unit/config/test_environ.py index 82ed13eea..53e1bd7f2 100644 --- a/openstack/tests/unit/config/test_environ.py +++ b/openstack/tests/unit/config/test_environ.py @@ -22,7 +22,7 @@ class TestEnviron(base.TestCase): def setUp(self): - super(TestEnviron, self).setUp() + super().setUp() self.useFixture( fixtures.EnvironmentVariable('OS_AUTH_URL', 'https://example.com') ) diff --git a/openstack/tests/unit/config/test_json.py b/openstack/tests/unit/config/test_json.py index ef9ffd5c0..da3b9d2fb 100644 --- a/openstack/tests/unit/config/test_json.py +++ b/openstack/tests/unit/config/test_json.py @@ -33,7 +33,7 @@ def test_defaults_valid_json(self): _schema_path = os.path.join( os.path.dirname(os.path.realpath(defaults.__file__)), 'schema.json' ) - with open(_schema_path, 'r') as f: + with open(_schema_path) as f: schema = json.load(f) self.validator = jsonschema.Draft4Validator(schema) self.addOnException(self.json_diagnostics) @@ -42,7 +42,7 @@ def test_defaults_valid_json(self): os.path.dirname(os.path.realpath(defaults.__file__)), 'defaults.json', ) - with open(self.filename, 'r') as f: + with open(self.filename) as f: self.json_data = json.load(f) self.assertTrue(self.validator.is_valid(self.json_data)) @@ -52,7 +52,7 @@ def test_vendors_valid_json(self): os.path.dirname(os.path.realpath(defaults.__file__)), 'vendor-schema.json', ) - with open(_schema_path, 'r') as f: + with open(_schema_path) as f: schema = json.load(f) self.validator = jsonschema.Draft4Validator(schema) @@ -62,7 +62,7 @@ def test_vendors_valid_json(self): os.path.dirname(os.path.realpath(defaults.__file__)), 'vendors' ) for self.filename in glob.glob(os.path.join(_vendors_path, '*.json')): - with open(self.filename, 'r') as f: + with open(self.filename) as f: self.json_data = json.load(f) self.assertTrue(self.validator.is_valid(self.json_data)) diff --git a/openstack/tests/unit/config/test_loader.py b/openstack/tests/unit/config/test_loader.py index a21f3b286..30890c5b6 100644 --- a/openstack/tests/unit/config/test_loader.py +++ b/openstack/tests/unit/config/test_loader.py @@ -58,7 +58,7 @@ def test_base_load_yaml_json_file(self): with tempfile.TemporaryDirectory() as tmpdir: tested_files = [] for key, value in FILES.items(): - fn = os.path.join(tmpdir, 'file.{ext}'.format(ext=key)) + fn = os.path.join(tmpdir, f'file.{key}') with open(fn, 'w+') as fp: fp.write(value) tested_files.append(fn) @@ -77,7 +77,7 @@ def test__load_yaml_json_file_without_json(self): for key, value in FILES.items(): if key == 'json': continue - fn = os.path.join(tmpdir, 'file.{ext}'.format(ext=key)) + fn = os.path.join(tmpdir, f'file.{key}') with open(fn, 'w+') as fp: fp.write(value) tested_files.append(fn) diff --git a/openstack/tests/unit/database/v1/test_proxy.py b/openstack/tests/unit/database/v1/test_proxy.py index 98c641d5c..e624edfc7 100644 --- a/openstack/tests/unit/database/v1/test_proxy.py +++ b/openstack/tests/unit/database/v1/test_proxy.py @@ -20,7 +20,7 @@ class TestDatabaseProxy(test_proxy_base.TestProxyBase): def setUp(self): - super(TestDatabaseProxy, self).setUp() + super().setUp() self.proxy = _proxy.Proxy(self.session) def test_database_create_attrs(self): diff --git a/openstack/tests/unit/dns/v2/test_proxy.py b/openstack/tests/unit/dns/v2/test_proxy.py index c4dc9f1e1..035701ef8 100644 --- a/openstack/tests/unit/dns/v2/test_proxy.py +++ b/openstack/tests/unit/dns/v2/test_proxy.py @@ -23,7 +23,7 @@ class TestDnsProxy(test_proxy_base.TestProxyBase): def setUp(self): - super(TestDnsProxy, self).setUp() + super().setUp() self.proxy = _proxy.Proxy(self.session) diff --git a/openstack/tests/unit/dns/v2/test_zone.py b/openstack/tests/unit/dns/v2/test_zone.py index b916a3ac4..673509efe 100644 --- a/openstack/tests/unit/dns/v2/test_zone.py +++ b/openstack/tests/unit/dns/v2/test_zone.py @@ -33,7 +33,7 @@ class TestZone(base.TestCase): def setUp(self): - super(TestZone, self).setUp() + super().setUp() self.resp = mock.Mock() self.resp.body = None self.resp.json = mock.Mock(return_value=self.resp.body) diff --git a/openstack/tests/unit/dns/v2/test_zone_share.py b/openstack/tests/unit/dns/v2/test_zone_share.py index c9b76ad3b..45bc81ef6 100644 --- a/openstack/tests/unit/dns/v2/test_zone_share.py +++ b/openstack/tests/unit/dns/v2/test_zone_share.py @@ -20,7 +20,7 @@ class TestZoneShare(base.TestCase): def setUp(self): - super(TestZoneShare, self).setUp() + super().setUp() self.resp = mock.Mock() self.resp.body = None self.resp.json = mock.Mock(return_value=self.resp.body) diff --git a/openstack/tests/unit/fakes.py b/openstack/tests/unit/fakes.py index 835205035..535ef5505 100644 --- a/openstack/tests/unit/fakes.py +++ b/openstack/tests/unit/fakes.py @@ -20,7 +20,7 @@ class FakeTransport(mock.Mock): RESPONSE = mock.Mock('200 OK') def __init__(self): - super(FakeTransport, self).__init__() + super().__init__() self.request = mock.Mock() self.request.return_value = self.RESPONSE @@ -30,7 +30,7 @@ class FakeAuthenticator(mock.Mock): ENDPOINT = 'http://www.example.com/endpoint' def __init__(self): - super(FakeAuthenticator, self).__init__() + super().__init__() self.get_token = mock.Mock() self.get_token.return_value = self.TOKEN self.get_endpoint = mock.Mock() diff --git a/openstack/tests/unit/identity/v2/test_proxy.py b/openstack/tests/unit/identity/v2/test_proxy.py index 0a08fd641..57aebbe1a 100644 --- a/openstack/tests/unit/identity/v2/test_proxy.py +++ b/openstack/tests/unit/identity/v2/test_proxy.py @@ -19,7 +19,7 @@ class TestIdentityProxy(test_proxy_base.TestProxyBase): def setUp(self): - super(TestIdentityProxy, self).setUp() + super().setUp() self.proxy = _proxy.Proxy(self.session) def test_role_create_attrs(self): diff --git a/openstack/tests/unit/identity/v3/test_domain.py b/openstack/tests/unit/identity/v3/test_domain.py index bc8c5b100..baea9560c 100644 --- a/openstack/tests/unit/identity/v3/test_domain.py +++ b/openstack/tests/unit/identity/v3/test_domain.py @@ -33,7 +33,7 @@ class TestDomain(base.TestCase): def setUp(self): - super(TestDomain, self).setUp() + super().setUp() self.sess = mock.Mock(spec=adapter.Adapter) self.sess.default_microversion = 1 self.sess._get_connection = mock.Mock(return_value=self.cloud) diff --git a/openstack/tests/unit/identity/v3/test_group.py b/openstack/tests/unit/identity/v3/test_group.py index 3f8d4e344..42955e6fa 100644 --- a/openstack/tests/unit/identity/v3/test_group.py +++ b/openstack/tests/unit/identity/v3/test_group.py @@ -30,7 +30,7 @@ class TestGroup(base.TestCase): def setUp(self): - super(TestGroup, self).setUp() + super().setUp() self.sess = mock.Mock(spec=adapter.Adapter) self.sess.default_microversion = 1 self.sess._get_connection = mock.Mock(return_value=self.cloud) diff --git a/openstack/tests/unit/identity/v3/test_project.py b/openstack/tests/unit/identity/v3/test_project.py index 23577f638..965700951 100644 --- a/openstack/tests/unit/identity/v3/test_project.py +++ b/openstack/tests/unit/identity/v3/test_project.py @@ -36,7 +36,7 @@ class TestProject(base.TestCase): def setUp(self): - super(TestProject, self).setUp() + super().setUp() self.sess = mock.Mock(spec=adapter.Adapter) self.sess.default_microversion = 1 self.sess._get_connection = mock.Mock(return_value=self.cloud) diff --git a/openstack/tests/unit/identity/v3/test_proxy.py b/openstack/tests/unit/identity/v3/test_proxy.py index dd12919d9..12350b5b8 100644 --- a/openstack/tests/unit/identity/v3/test_proxy.py +++ b/openstack/tests/unit/identity/v3/test_proxy.py @@ -39,7 +39,7 @@ class TestIdentityProxyBase(test_proxy_base.TestProxyBase): def setUp(self): - super(TestIdentityProxyBase, self).setUp() + super().setUp() self.proxy = _proxy.Proxy(self.session) diff --git a/openstack/tests/unit/image/v1/test_proxy.py b/openstack/tests/unit/image/v1/test_proxy.py index c5cc5419c..155fb579a 100644 --- a/openstack/tests/unit/image/v1/test_proxy.py +++ b/openstack/tests/unit/image/v1/test_proxy.py @@ -17,7 +17,7 @@ class TestImageProxy(test_proxy_base.TestProxyBase): def setUp(self): - super(TestImageProxy, self).setUp() + super().setUp() self.proxy = _proxy.Proxy(self.session) def test_image_upload_attrs(self): diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 5f9e34066..8ff4cb2b7 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -118,7 +118,7 @@ def json(self): class TestImage(base.TestCase): def setUp(self): - super(TestImage, self).setUp() + super().setUp() self.resp = mock.Mock() self.resp.body = None self.resp.json = mock.Mock(return_value=self.resp.body) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index bd8dd043e..9bf464cfd 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -47,7 +47,7 @@ def json(self): class TestImageProxy(test_proxy_base.TestProxyBase): def setUp(self): - super(TestImageProxy, self).setUp() + super().setUp() self.proxy = _proxy.Proxy(self.session) self.proxy._connection = self.cloud diff --git a/openstack/tests/unit/instance_ha/v1/test_proxy.py b/openstack/tests/unit/instance_ha/v1/test_proxy.py index 4fa93520d..3d2727146 100644 --- a/openstack/tests/unit/instance_ha/v1/test_proxy.py +++ b/openstack/tests/unit/instance_ha/v1/test_proxy.py @@ -27,7 +27,7 @@ class TestInstanceHaProxy(test_proxy_base.TestProxyBase): def setUp(self): - super(TestInstanceHaProxy, self).setUp() + super().setUp() self.proxy = _proxy.Proxy(self.session) diff --git a/openstack/tests/unit/key_manager/v1/test_proxy.py b/openstack/tests/unit/key_manager/v1/test_proxy.py index e5ec2bb9b..f770f11d6 100644 --- a/openstack/tests/unit/key_manager/v1/test_proxy.py +++ b/openstack/tests/unit/key_manager/v1/test_proxy.py @@ -19,7 +19,7 @@ class TestKeyManagerProxy(test_proxy_base.TestProxyBase): def setUp(self): - super(TestKeyManagerProxy, self).setUp() + super().setUp() self.proxy = _proxy.Proxy(self.session) diff --git a/openstack/tests/unit/load_balancer/test_load_balancer.py b/openstack/tests/unit/load_balancer/test_load_balancer.py index db29ae883..607bc97b0 100644 --- a/openstack/tests/unit/load_balancer/test_load_balancer.py +++ b/openstack/tests/unit/load_balancer/test_load_balancer.py @@ -144,7 +144,7 @@ def test_delete_non_cascade(self): sot._translate_response = mock.Mock() sot.delete(sess) - url = 'lbaas/loadbalancers/%(lb)s' % {'lb': EXAMPLE['id']} + url = 'lbaas/loadbalancers/{lb}'.format(lb=EXAMPLE['id']) params = {} sess.delete.assert_called_with(url, params=params) sot._translate_response.assert_called_once_with( @@ -163,7 +163,7 @@ def test_delete_cascade(self): sot._translate_response = mock.Mock() sot.delete(sess) - url = 'lbaas/loadbalancers/%(lb)s' % {'lb': EXAMPLE['id']} + url = 'lbaas/loadbalancers/{lb}'.format(lb=EXAMPLE['id']) params = {'cascade': True} sess.delete.assert_called_with(url, params=params) sot._translate_response.assert_called_once_with( diff --git a/openstack/tests/unit/load_balancer/v2/test_proxy.py b/openstack/tests/unit/load_balancer/v2/test_proxy.py index 6fd813f2c..db7391218 100644 --- a/openstack/tests/unit/load_balancer/v2/test_proxy.py +++ b/openstack/tests/unit/load_balancer/v2/test_proxy.py @@ -41,7 +41,7 @@ class TestLoadBalancerProxy(test_proxy_base.TestProxyBase): AMPHORA_ID = uuid.uuid4() def setUp(self): - super(TestLoadBalancerProxy, self).setUp() + super().setUp() self.proxy = _proxy.Proxy(self.session) def test_load_balancers(self): diff --git a/openstack/tests/unit/message/v2/test_claim.py b/openstack/tests/unit/message/v2/test_claim.py index f08c5aa30..2b108dcb3 100644 --- a/openstack/tests/unit/message/v2/test_claim.py +++ b/openstack/tests/unit/message/v2/test_claim.py @@ -76,7 +76,7 @@ def test_create_204_resp(self, mock_uuid): sot = claim.Claim(**FAKE1) res = sot.create(sess) - url = "/queues/%(queue)s/claims" % {"queue": FAKE.pop("queue_name")} + url = "/queues/{queue}/claims".format(queue=FAKE.pop("queue_name")) headers = { "Client-ID": "NEW_CLIENT_ID", "X-PROJECT-ID": "NEW_PROJECT_ID", @@ -99,7 +99,7 @@ def test_create_non_204_resp(self, mock_uuid): sot._translate_response = mock.Mock() res = sot.create(sess) - url = "/queues/%(queue)s/claims" % {"queue": FAKE.pop("queue_name")} + url = "/queues/{queue}/claims".format(queue=FAKE.pop("queue_name")) headers = { "Client-ID": "NEW_CLIENT_ID", "X-PROJECT-ID": "NEW_PROJECT_ID", @@ -120,7 +120,7 @@ def test_create_client_id_project_id_exist(self): sot._translate_response = mock.Mock() res = sot.create(sess) - url = "/queues/%(queue)s/claims" % {"queue": FAKE.pop("queue_name")} + url = "/queues/{queue}/claims".format(queue=FAKE.pop("queue_name")) headers = { "Client-ID": FAKE.pop("client_id"), "X-PROJECT-ID": FAKE.pop("project_id"), @@ -140,10 +140,10 @@ def test_get(self, mock_uuid): sot._translate_response = mock.Mock() res = sot.fetch(sess) - url = "queues/%(queue)s/claims/%(claim)s" % { - "queue": FAKE1["queue_name"], - "claim": FAKE1["id"], - } + url = "queues/{queue}/claims/{claim}".format( + queue=FAKE1["queue_name"], + claim=FAKE1["id"], + ) headers = { "Client-ID": "NEW_CLIENT_ID", "X-PROJECT-ID": "NEW_PROJECT_ID", @@ -162,10 +162,10 @@ def test_get_client_id_project_id_exist(self): sot._translate_response = mock.Mock() res = sot.fetch(sess) - url = "queues/%(queue)s/claims/%(claim)s" % { - "queue": FAKE2["queue_name"], - "claim": FAKE2["id"], - } + url = "queues/{queue}/claims/{claim}".format( + queue=FAKE2["queue_name"], + claim=FAKE2["id"], + ) headers = { "Client-ID": "OLD_CLIENT_ID", "X-PROJECT-ID": "OLD_PROJECT_ID", @@ -186,10 +186,10 @@ def test_update(self, mock_uuid): sot = claim.Claim(**FAKE1) res = sot.commit(sess) - url = "queues/%(queue)s/claims/%(claim)s" % { - "queue": FAKE.pop("queue_name"), - "claim": FAKE["id"], - } + url = "queues/{queue}/claims/{claim}".format( + queue=FAKE.pop("queue_name"), + claim=FAKE["id"], + ) headers = { "Client-ID": "NEW_CLIENT_ID", "X-PROJECT-ID": "NEW_PROJECT_ID", @@ -207,10 +207,10 @@ def test_update_client_id_project_id_exist(self): sot = claim.Claim(**FAKE2) res = sot.commit(sess) - url = "queues/%(queue)s/claims/%(claim)s" % { - "queue": FAKE.pop("queue_name"), - "claim": FAKE["id"], - } + url = "queues/{queue}/claims/{claim}".format( + queue=FAKE.pop("queue_name"), + claim=FAKE["id"], + ) headers = { "Client-ID": FAKE.pop("client_id"), "X-PROJECT-ID": FAKE.pop("project_id"), @@ -230,10 +230,10 @@ def test_delete(self, mock_uuid): sot._translate_response = mock.Mock() sot.delete(sess) - url = "queues/%(queue)s/claims/%(claim)s" % { - "queue": FAKE1["queue_name"], - "claim": FAKE1["id"], - } + url = "queues/{queue}/claims/{claim}".format( + queue=FAKE1["queue_name"], + claim=FAKE1["id"], + ) headers = { "Client-ID": "NEW_CLIENT_ID", "X-PROJECT-ID": "NEW_PROJECT_ID", @@ -251,10 +251,10 @@ def test_delete_client_id_project_id_exist(self): sot._translate_response = mock.Mock() sot.delete(sess) - url = "queues/%(queue)s/claims/%(claim)s" % { - "queue": FAKE2["queue_name"], - "claim": FAKE2["id"], - } + url = "queues/{queue}/claims/{claim}".format( + queue=FAKE2["queue_name"], + claim=FAKE2["id"], + ) headers = { "Client-ID": "OLD_CLIENT_ID", "X-PROJECT-ID": "OLD_PROJECT_ID", diff --git a/openstack/tests/unit/message/v2/test_message.py b/openstack/tests/unit/message/v2/test_message.py index b42c44159..d7aa7de49 100644 --- a/openstack/tests/unit/message/v2/test_message.py +++ b/openstack/tests/unit/message/v2/test_message.py @@ -87,7 +87,7 @@ def test_post(self, mock_uuid): sot = message.Message(**FAKE1) res = sot.post(sess, messages) - url = '/queues/%(queue)s/messages' % {'queue': FAKE1['queue_name']} + url = '/queues/{queue}/messages'.format(queue=FAKE1['queue_name']) headers = { 'Client-ID': 'NEW_CLIENT_ID', 'X-PROJECT-ID': 'NEW_PROJECT_ID', @@ -116,7 +116,7 @@ def test_post_client_id_project_id_exist(self): sot = message.Message(**FAKE2) res = sot.post(sess, messages) - url = '/queues/%(queue)s/messages' % {'queue': FAKE2['queue_name']} + url = '/queues/{queue}/messages'.format(queue=FAKE2['queue_name']) headers = { 'Client-ID': 'OLD_CLIENT_ID', 'X-PROJECT-ID': 'OLD_PROJECT_ID', @@ -139,10 +139,10 @@ def test_get(self, mock_uuid): sot._translate_response = mock.Mock() res = sot.fetch(sess) - url = 'queues/%(queue)s/messages/%(message)s' % { - 'queue': FAKE1['queue_name'], - 'message': FAKE1['id'], - } + url = 'queues/{queue}/messages/{message}'.format( + queue=FAKE1['queue_name'], + message=FAKE1['id'], + ) headers = { 'Client-ID': 'NEW_CLIENT_ID', 'X-PROJECT-ID': 'NEW_PROJECT_ID', @@ -161,10 +161,10 @@ def test_get_client_id_project_id_exist(self): sot._translate_response = mock.Mock() res = sot.fetch(sess) - url = 'queues/%(queue)s/messages/%(message)s' % { - 'queue': FAKE2['queue_name'], - 'message': FAKE2['id'], - } + url = 'queues/{queue}/messages/{message}'.format( + queue=FAKE2['queue_name'], + message=FAKE2['id'], + ) sot = message.Message(**FAKE2) sot._translate_response = mock.Mock() res = sot.fetch(sess) @@ -189,10 +189,10 @@ def test_delete_unclaimed(self, mock_uuid): sot._translate_response = mock.Mock() sot.delete(sess) - url = 'queues/%(queue)s/messages/%(message)s' % { - 'queue': FAKE1['queue_name'], - 'message': FAKE1['id'], - } + url = 'queues/{queue}/messages/{message}'.format( + queue=FAKE1['queue_name'], + message=FAKE1['id'], + ) headers = { 'Client-ID': 'NEW_CLIENT_ID', 'X-PROJECT-ID': 'NEW_PROJECT_ID', @@ -214,11 +214,11 @@ def test_delete_claimed(self, mock_uuid): sot._translate_response = mock.Mock() sot.delete(sess) - url = 'queues/%(queue)s/messages/%(message)s?claim_id=%(cid)s' % { - 'queue': FAKE1['queue_name'], - 'message': FAKE1['id'], - 'cid': 'CLAIM_ID', - } + url = 'queues/{queue}/messages/{message}?claim_id={cid}'.format( + queue=FAKE1['queue_name'], + message=FAKE1['id'], + cid='CLAIM_ID', + ) headers = { 'Client-ID': 'NEW_CLIENT_ID', 'X-PROJECT-ID': 'NEW_PROJECT_ID', @@ -237,10 +237,10 @@ def test_delete_client_id_project_id_exist(self): sot._translate_response = mock.Mock() sot.delete(sess) - url = 'queues/%(queue)s/messages/%(message)s' % { - 'queue': FAKE2['queue_name'], - 'message': FAKE2['id'], - } + url = 'queues/{queue}/messages/{message}'.format( + queue=FAKE2['queue_name'], + message=FAKE2['id'], + ) headers = { 'Client-ID': 'OLD_CLIENT_ID', 'X-PROJECT-ID': 'OLD_PROJECT_ID', diff --git a/openstack/tests/unit/message/v2/test_proxy.py b/openstack/tests/unit/message/v2/test_proxy.py index 19e3181de..9569b018c 100644 --- a/openstack/tests/unit/message/v2/test_proxy.py +++ b/openstack/tests/unit/message/v2/test_proxy.py @@ -25,7 +25,7 @@ class TestMessageProxy(test_proxy_base.TestProxyBase): def setUp(self): - super(TestMessageProxy, self).setUp() + super().setUp() self.proxy = _proxy.Proxy(self.session) diff --git a/openstack/tests/unit/message/v2/test_subscription.py b/openstack/tests/unit/message/v2/test_subscription.py index 1fb78a43d..57686f8c4 100644 --- a/openstack/tests/unit/message/v2/test_subscription.py +++ b/openstack/tests/unit/message/v2/test_subscription.py @@ -80,9 +80,9 @@ def test_create(self, mock_uuid): sot._translate_response = mock.Mock() res = sot.create(sess) - url = "/queues/%(queue)s/subscriptions" % { - "queue": FAKE.pop("queue_name") - } + url = "/queues/{queue}/subscriptions".format( + queue=FAKE.pop("queue_name") + ) headers = { "Client-ID": "NEW_CLIENT_ID", "X-PROJECT-ID": "NEW_PROJECT_ID", @@ -101,9 +101,9 @@ def test_create_client_id_project_id_exist(self): sot._translate_response = mock.Mock() res = sot.create(sess) - url = "/queues/%(queue)s/subscriptions" % { - "queue": FAKE.pop("queue_name") - } + url = "/queues/{queue}/subscriptions".format( + queue=FAKE.pop("queue_name") + ) headers = { "Client-ID": FAKE.pop("client_id"), "X-PROJECT-ID": FAKE.pop("project_id"), @@ -123,10 +123,10 @@ def test_get(self, mock_uuid): sot._translate_response = mock.Mock() res = sot.fetch(sess) - url = "queues/%(queue)s/subscriptions/%(subscription)s" % { - "queue": FAKE1["queue_name"], - "subscription": FAKE1["id"], - } + url = "queues/{queue}/subscriptions/{subscription}".format( + queue=FAKE1["queue_name"], + subscription=FAKE1["id"], + ) headers = { "Client-ID": "NEW_CLIENT_ID", "X-PROJECT-ID": "NEW_PROJECT_ID", @@ -145,10 +145,10 @@ def test_get_client_id_project_id_exist(self): sot._translate_response = mock.Mock() res = sot.fetch(sess) - url = "queues/%(queue)s/subscriptions/%(subscription)s" % { - "queue": FAKE2["queue_name"], - "subscription": FAKE2["id"], - } + url = "queues/{queue}/subscriptions/{subscription}".format( + queue=FAKE2["queue_name"], + subscription=FAKE2["id"], + ) headers = { "Client-ID": "OLD_CLIENT_ID", "X-PROJECT-ID": "OLD_PROJECT_ID", @@ -169,10 +169,10 @@ def test_delete(self, mock_uuid): sot._translate_response = mock.Mock() sot.delete(sess) - url = "queues/%(queue)s/subscriptions/%(subscription)s" % { - "queue": FAKE1["queue_name"], - "subscription": FAKE1["id"], - } + url = "queues/{queue}/subscriptions/{subscription}".format( + queue=FAKE1["queue_name"], + subscription=FAKE1["id"], + ) headers = { "Client-ID": "NEW_CLIENT_ID", "X-PROJECT-ID": "NEW_PROJECT_ID", @@ -190,10 +190,10 @@ def test_delete_client_id_project_id_exist(self): sot._translate_response = mock.Mock() sot.delete(sess) - url = "queues/%(queue)s/subscriptions/%(subscription)s" % { - "queue": FAKE2["queue_name"], - "subscription": FAKE2["id"], - } + url = "queues/{queue}/subscriptions/{subscription}".format( + queue=FAKE2["queue_name"], + subscription=FAKE2["id"], + ) headers = { "Client-ID": "OLD_CLIENT_ID", "X-PROJECT-ID": "OLD_PROJECT_ID", diff --git a/openstack/tests/unit/network/v2/test_bgp_speaker.py b/openstack/tests/unit/network/v2/test_bgp_speaker.py index 839efdd51..848aa590d 100644 --- a/openstack/tests/unit/network/v2/test_bgp_speaker.py +++ b/openstack/tests/unit/network/v2/test_bgp_speaker.py @@ -181,5 +181,5 @@ def test_remove_bgp_speaker_from_dragent(self): sess.delete = mock.Mock(return_value=response) self.assertIsNone(sot.remove_bgp_speaker_from_dragent(sess, agent_id)) - url = 'agents/%s/bgp-drinstances/%s' % (agent_id, IDENTIFIER) + url = f'agents/{agent_id}/bgp-drinstances/{IDENTIFIER}' sess.delete.assert_called_with(url) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 45560a3eb..3b8265101 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -87,7 +87,7 @@ class TestNetworkProxy(test_proxy_base.TestProxyBase): def setUp(self): - super(TestNetworkProxy, self).setUp() + super().setUp() self.proxy = _proxy.Proxy(self.session) def verify_update( diff --git a/openstack/tests/unit/object_store/v1/test_account.py b/openstack/tests/unit/object_store/v1/test_account.py index d0ca018d5..f8d88f5e5 100644 --- a/openstack/tests/unit/object_store/v1/test_account.py +++ b/openstack/tests/unit/object_store/v1/test_account.py @@ -30,7 +30,7 @@ class TestAccount(base.TestCase): def setUp(self): - super(TestAccount, self).setUp() + super().setUp() self.endpoint = self.cloud.object_store.get_endpoint() + '/' def test_basic(self): diff --git a/openstack/tests/unit/object_store/v1/test_container.py b/openstack/tests/unit/object_store/v1/test_container.py index dc1f2e539..d67e72dc3 100644 --- a/openstack/tests/unit/object_store/v1/test_container.py +++ b/openstack/tests/unit/object_store/v1/test_container.py @@ -18,7 +18,7 @@ class TestContainer(base.TestCase): def setUp(self): - super(TestContainer, self).setUp() + super().setUp() self.container = self.getUniqueString() self.endpoint = self.cloud.object_store.get_endpoint() + '/' self.container_endpoint = '{endpoint}{container}'.format( diff --git a/openstack/tests/unit/object_store/v1/test_info.py b/openstack/tests/unit/object_store/v1/test_info.py index 36f5e7e9f..9a6dbc583 100644 --- a/openstack/tests/unit/object_store/v1/test_info.py +++ b/openstack/tests/unit/object_store/v1/test_info.py @@ -16,7 +16,7 @@ class TestInfo(base.TestCase): def setUp(self): - super(TestInfo, self).setUp() + super().setUp() def test_get_info_url(self): sot = info.Info() diff --git a/openstack/tests/unit/object_store/v1/test_obj.py b/openstack/tests/unit/object_store/v1/test_obj.py index f3f6cb7c1..de30ba1c5 100644 --- a/openstack/tests/unit/object_store/v1/test_obj.py +++ b/openstack/tests/unit/object_store/v1/test_obj.py @@ -28,7 +28,7 @@ class TestObject(base_test_object.BaseTestObject): def setUp(self): - super(TestObject, self).setUp() + super().setUp() self.the_data = b'test body' self.the_data_length = len(self.the_data) # TODO(mordred) Make the_data be from getUniqueString and then diff --git a/openstack/tests/unit/object_store/v1/test_proxy.py b/openstack/tests/unit/object_store/v1/test_proxy.py index 3005dc17b..b30dde267 100644 --- a/openstack/tests/unit/object_store/v1/test_proxy.py +++ b/openstack/tests/unit/object_store/v1/test_proxy.py @@ -41,7 +41,7 @@ class TestObjectStoreProxy(test_proxy_base.TestProxyBase): kwargs_to_path_args = False def setUp(self): - super(TestObjectStoreProxy, self).setUp() + super().setUp() self.proxy = self.cloud.object_store self.container = self.getUniqueString() self.endpoint = self.cloud.object_store.get_endpoint() + '/' @@ -271,7 +271,7 @@ def test_file_segment(self): segment_content = b'' for index, (name, segment) in enumerate(segments.items()): self.assertEqual( - 'test_container/test_image/{index:0>6}'.format(index=index), + f'test_container/test_image/{index:0>6}', name, ) segment_content += segment.read() @@ -280,7 +280,7 @@ def test_file_segment(self): class TestDownloadObject(base_test_object.BaseTestObject): def setUp(self): - super(TestDownloadObject, self).setUp() + super().setUp() self.the_data = b'test body' self.register_uris( [ @@ -602,7 +602,7 @@ def test_generate_absolute_expiry_temp_url(self, hmac_mock): ) else: expected_url = self.expected_url.replace( - u'1400003600', u'2146636800' + '1400003600', '2146636800' ) url = self.proxy.generate_temp_url( self.url, @@ -654,38 +654,38 @@ def test_generate_temp_url_bad_path(self): class TestTempURLUnicodePathAndKey(TestTempURL): - url = u'/v1/\u00e4/c/\u00f3' - key = u'k\u00e9y' + url = '/v1/\u00e4/c/\u00f3' + key = 'k\u00e9y' expected_url = ( - u'%s?temp_url_sig=temp_url_signature' u'&temp_url_expires=1400003600' + '%s?temp_url_sig=temp_url_signature' '&temp_url_expires=1400003600' ) % url - expected_body = u'\n'.join( + expected_body = '\n'.join( [ - u'GET', - u'1400003600', + 'GET', + '1400003600', url, ] ).encode('utf-8') class TestTempURLUnicodePathBytesKey(TestTempURL): - url = u'/v1/\u00e4/c/\u00f3' - key = u'k\u00e9y'.encode('utf-8') + url = '/v1/\u00e4/c/\u00f3' + key = 'k\u00e9y'.encode() expected_url = ( - u'%s?temp_url_sig=temp_url_signature' u'&temp_url_expires=1400003600' + '%s?temp_url_sig=temp_url_signature' '&temp_url_expires=1400003600' ) % url expected_body = '\n'.join( [ - u'GET', - u'1400003600', + 'GET', + '1400003600', url, ] ).encode('utf-8') class TestTempURLBytesPathUnicodeKey(TestTempURL): - url = u'/v1/\u00e4/c/\u00f3'.encode('utf-8') - key = u'k\u00e9y' + url = '/v1/\u00e4/c/\u00f3'.encode() + key = 'k\u00e9y' expected_url = url + ( b'?temp_url_sig=temp_url_signature' b'&temp_url_expires=1400003600' ) @@ -699,8 +699,8 @@ class TestTempURLBytesPathUnicodeKey(TestTempURL): class TestTempURLBytesPathAndKey(TestTempURL): - url = u'/v1/\u00e4/c/\u00f3'.encode('utf-8') - key = u'k\u00e9y'.encode('utf-8') + url = '/v1/\u00e4/c/\u00f3'.encode() + key = 'k\u00e9y'.encode() expected_url = url + ( b'?temp_url_sig=temp_url_signature' b'&temp_url_expires=1400003600' ) @@ -714,7 +714,7 @@ class TestTempURLBytesPathAndKey(TestTempURL): class TestTempURLBytesPathAndNonUtf8Key(TestTempURL): - url = u'/v1/\u00e4/c/\u00f3'.encode('utf-8') + url = '/v1/\u00e4/c/\u00f3'.encode() key = b'k\xffy' expected_url = url + ( b'?temp_url_sig=temp_url_signature' b'&temp_url_expires=1400003600' diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index 0e8206085..794c5221d 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -31,7 +31,7 @@ class TestOrchestrationProxy(test_proxy_base.TestProxyBase): def setUp(self): - super(TestOrchestrationProxy, self).setUp() + super().setUp() self.proxy = _proxy.Proxy(self.session) diff --git a/openstack/tests/unit/orchestration/v1/test_stack.py b/openstack/tests/unit/orchestration/v1/test_stack.py index a6b7bcb61..079c5c078 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack.py +++ b/openstack/tests/unit/orchestration/v1/test_stack.py @@ -31,7 +31,7 @@ 'files': {'file1': 'content'}, 'files_container': 'dummy_container', 'id': FAKE_ID, - 'links': [{'href': 'stacks/%s/%s' % (FAKE_NAME, FAKE_ID), 'rel': 'self'}], + 'links': [{'href': f'stacks/{FAKE_NAME}/{FAKE_ID}', 'rel': 'self'}], 'notification_topics': '7', 'outputs': '8', 'parameters': {'OS::stack_id': '9'}, @@ -47,9 +47,7 @@ FAKE_CREATE_RESPONSE = { 'stack': { 'id': FAKE_ID, - 'links': [ - {'href': 'stacks/%s/%s' % (FAKE_NAME, FAKE_ID), 'rel': 'self'} - ], + 'links': [{'href': f'stacks/{FAKE_NAME}/{FAKE_ID}', 'rel': 'self'}], } } FAKE_UPDATE_PREVIEW_RESPONSE = { @@ -240,13 +238,13 @@ def test_fetch(self): self.assertEqual(sot, sot.fetch(sess)) sess.get.assert_called_with( - 'stacks/{id}'.format(id=sot.id), + f'stacks/{sot.id}', microversion=None, skip_cache=False, ) sot.fetch(sess, resolve_outputs=False) sess.get.assert_called_with( - 'stacks/{id}?resolve_outputs=False'.format(id=sot.id), + f'stacks/{sot.id}?resolve_outputs=False', microversion=None, skip_cache=False, ) @@ -269,7 +267,7 @@ def test_abandon(self): sot.abandon(sess) sess.delete.assert_called_with( - 'stacks/%s/%s/abandon' % (FAKE_NAME, FAKE_ID), + f'stacks/{FAKE_NAME}/{FAKE_ID}/abandon', ) def test_export(self): @@ -286,7 +284,7 @@ def test_export(self): sot.export(sess) sess.get.assert_called_with( - 'stacks/%s/%s/export' % (FAKE_NAME, FAKE_ID), + f'stacks/{FAKE_NAME}/{FAKE_ID}/export', ) def test_update(self): @@ -304,7 +302,7 @@ def test_update(self): sot.update(sess) sess.put.assert_called_with( - '/stacks/%s/%s' % (FAKE_NAME, FAKE_ID), + f'/stacks/{FAKE_NAME}/{FAKE_ID}', headers={}, microversion=None, json=body, @@ -325,7 +323,7 @@ def test_update_preview(self): ret = sot.update(sess, preview=True) sess.put.assert_called_with( - 'stacks/%s/%s/preview' % (FAKE_NAME, FAKE_ID), + f'stacks/{FAKE_NAME}/{FAKE_ID}/preview', headers={}, microversion=None, json=body, diff --git a/openstack/tests/unit/orchestration/v1/test_stack_event.py b/openstack/tests/unit/orchestration/v1/test_stack_event.py index 5c5057287..913792fee 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack_event.py +++ b/openstack/tests/unit/orchestration/v1/test_stack_event.py @@ -19,7 +19,7 @@ FAKE = { 'event_time': '2015-03-09T12:15:57.233772', 'id': FAKE_ID, - 'links': [{'href': 'stacks/%s/%s' % (FAKE_NAME, FAKE_ID), 'rel': 'self'}], + 'links': [{'href': f'stacks/{FAKE_NAME}/{FAKE_ID}', 'rel': 'self'}], 'logical_resource_id': 'my_test_group', 'physical_resource_id': 'my_test_group', 'resource_name': 'my_test_resource', diff --git a/openstack/tests/unit/orchestration/v1/test_stack_files.py b/openstack/tests/unit/orchestration/v1/test_stack_files.py index 1e6e5d729..6b4527f60 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack_files.py +++ b/openstack/tests/unit/orchestration/v1/test_stack_files.py @@ -44,10 +44,10 @@ def test_get(self, mock_prepare_request): sot = sf.StackFiles(**FAKE) req = mock.MagicMock() - req.url = '/stacks/%(stack_name)s/%(stack_id)s/files' % { - 'stack_name': FAKE['stack_name'], - 'stack_id': FAKE['stack_id'], - } + req.url = '/stacks/{stack_name}/{stack_id}/files'.format( + stack_name=FAKE['stack_name'], + stack_id=FAKE['stack_id'], + ) mock_prepare_request.return_value = req files = sot.fetch(sess) diff --git a/openstack/tests/unit/shared_file_system/v2/test_proxy.py b/openstack/tests/unit/shared_file_system/v2/test_proxy.py index a562a450b..3534d8886 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_proxy.py +++ b/openstack/tests/unit/shared_file_system/v2/test_proxy.py @@ -31,7 +31,7 @@ class TestSharedFileSystemProxy(test_proxy_base.TestProxyBase): def setUp(self): - super(TestSharedFileSystemProxy, self).setUp() + super().setUp() self.proxy = _proxy.Proxy(self.session) @@ -215,7 +215,7 @@ def test_delete_share_metadata(self): class TestUserMessageProxy(test_proxy_base.TestProxyBase): def setUp(self): - super(TestUserMessageProxy, self).setUp() + super().setUp() self.proxy = _proxy.Proxy(self.session) def test_user_messages(self): @@ -248,7 +248,7 @@ def test_limit(self): class TestShareSnapshotResource(test_proxy_base.TestProxyBase): def setUp(self): - super(TestShareSnapshotResource, self).setUp() + super().setUp() self.proxy = _proxy.Proxy(self.session) def test_share_snapshots(self): @@ -313,7 +313,7 @@ def test_wait_for_delete(self, mock_wait): class TestShareSnapshotInstanceResource(test_proxy_base.TestProxyBase): def setUp(self): - super(TestShareSnapshotInstanceResource, self).setUp() + super().setUp() self.proxy = _proxy.Proxy(self.session) def test_share_snapshot_instances(self): @@ -347,7 +347,7 @@ def test_share_snapshot_instance_get(self): class TestShareNetworkResource(test_proxy_base.TestProxyBase): def setUp(self): - super(TestShareNetworkResource, self).setUp() + super().setUp() self.proxy = _proxy.Proxy(self.session) def test_share_networks(self): @@ -397,7 +397,7 @@ def test_share_network_update(self): class TestShareNetworkSubnetResource(test_proxy_base.TestProxyBase): def setUp(self): - super(TestShareNetworkSubnetResource, self).setUp() + super().setUp() self.proxy = _proxy.Proxy(self.session) def test_share_network_subnets(self): @@ -444,7 +444,7 @@ def test_share_network_subnet_delete(self): class TestAccessRuleProxy(test_proxy_base.TestProxyBase): def setUp(self): - super(TestAccessRuleProxy, self).setUp() + super().setUp() self.proxy = _proxy.Proxy(self.session) def test_access_ruless(self): @@ -486,7 +486,7 @@ def test_access_rules_delete(self): class TestResourceLocksProxy(test_proxy_base.TestProxyBase): def setUp(self): - super(TestResourceLocksProxy, self).setUp() + super().setUp() self.proxy = _proxy.Proxy(self.session) def test_list_resource_locks(self): @@ -522,7 +522,7 @@ def test_resource_lock_update(self): class TestShareGroupResource(test_proxy_base.TestProxyBase): def setUp(self): - super(TestShareGroupResource, self).setUp() + super().setUp() self.proxy = _proxy.Proxy(self.session) def test_share_groups(self): diff --git a/openstack/tests/unit/shared_file_system/v2/test_share_instance.py b/openstack/tests/unit/shared_file_system/v2/test_share_instance.py index 6dbc0a921..6d9be78ef 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_share_instance.py +++ b/openstack/tests/unit/shared_file_system/v2/test_share_instance.py @@ -84,7 +84,7 @@ def test_make_share_instances(self): class TestShareInstanceActions(TestShareInstances): def setUp(self): - super(TestShareInstanceActions, self).setUp() + super().setUp() self.resp = mock.Mock() self.resp.body = None self.resp.status_code = 200 diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index 4149be766..e2150a2a0 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -105,7 +105,7 @@ class _TestConnectionBase(base.TestCase): def setUp(self): - super(_TestConnectionBase, self).setUp() + super().setUp() # Create a temporary directory where our test config will live # and insert it into the search path via OS_CLIENT_CONFIG_FILE. config_dir = self.useFixture(fixtures.TempDir()).path @@ -384,7 +384,7 @@ def test_add_service_v1(self): svc.add_endpoint( interface='public', region='RegionOne', - url='https://fake.example.com/v1/{0}'.format(fakes.PROJECT_ID), + url=f'https://fake.example.com/v1/{fakes.PROJECT_ID}', ) self.use_keystone_v3() conn = self.cloud @@ -427,7 +427,7 @@ def test_add_service_v2(self): svc.add_endpoint( interface='public', region='RegionOne', - url='https://fake.example.com/v2/{0}'.format(fakes.PROJECT_ID), + url=f'https://fake.example.com/v2/{fakes.PROJECT_ID}', ) self.use_keystone_v3() conn = self.cloud @@ -467,7 +467,7 @@ def test_replace_system_service(self): svc.add_endpoint( interface='public', region='RegionOne', - url='https://fake.example.com/v2/{0}'.format(fakes.PROJECT_ID), + url=f'https://fake.example.com/v2/{fakes.PROJECT_ID}', ) self.use_keystone_v3() conn = self.cloud @@ -509,7 +509,7 @@ def vendor_hook(conn): class TestVendorProfile(base.TestCase): def setUp(self): - super(TestVendorProfile, self).setUp() + super().setUp() # Create a temporary directory where our test config will live # and insert it into the search path via OS_CLIENT_CONFIG_FILE. config_dir = self.useFixture(fixtures.TempDir()).path diff --git a/openstack/tests/unit/test_exceptions.py b/openstack/tests/unit/test_exceptions.py index b591150ed..86a2b62f6 100644 --- a/openstack/tests/unit/test_exceptions.py +++ b/openstack/tests/unit/test_exceptions.py @@ -1,5 +1,3 @@ -# -*- 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 @@ -32,7 +30,7 @@ def test_method_not_supported(self): class Test_HttpException(base.TestCase): def setUp(self): - super(Test_HttpException, self).setUp() + super().setUp() self.message = "mayday" def _do_raise(self, *args, **kwargs): @@ -72,7 +70,7 @@ def test_http_status(self): class TestRaiseFromResponse(base.TestCase): def setUp(self): - super(TestRaiseFromResponse, self).setUp() + super().setUp() self.message = "Where is my kitty?" def _do_raise(self, *args, **kwargs): diff --git a/openstack/tests/unit/test_microversions.py b/openstack/tests/unit/test_microversions.py index 531f89bcf..e77646b3c 100644 --- a/openstack/tests/unit/test_microversions.py +++ b/openstack/tests/unit/test_microversions.py @@ -17,7 +17,7 @@ class TestMicroversions(base.TestCase): def setUp(self): - super(TestMicroversions, self).setUp() + super().setUp() self.use_compute_discovery() def test_get_bad_inferred_max_microversion(self): diff --git a/openstack/tests/unit/test_missing_version.py b/openstack/tests/unit/test_missing_version.py index c17569def..fd8e41099 100644 --- a/openstack/tests/unit/test_missing_version.py +++ b/openstack/tests/unit/test_missing_version.py @@ -21,7 +21,7 @@ class TestMissingVersion(base.TestCase): def setUp(self): - super(TestMissingVersion, self).setUp() + super().setUp() self.os_fixture.clear_tokens() svc = self.os_fixture.v3_token.add_service('image') svc.add_endpoint( diff --git a/openstack/tests/unit/test_placement_rest.py b/openstack/tests/unit/test_placement_rest.py index 713db3dc0..32529ae15 100644 --- a/openstack/tests/unit/test_placement_rest.py +++ b/openstack/tests/unit/test_placement_rest.py @@ -21,7 +21,7 @@ @ddt.ddt class TestPlacementRest(base.TestCase): def setUp(self): - super(TestPlacementRest, self).setUp() + super().setUp() self.use_placement() def _register_uris(self, status_code=None): @@ -78,7 +78,7 @@ def test_microversion_discovery(self): class TestBadPlacementRest(base.TestCase): def setUp(self): self.skipTest('Need to re-add support for broken placement versions') - super(TestBadPlacementRest, self).setUp() + super().setUp() # The bad-placement.json is for older placement that was # missing the status field from its discovery doc. This # lets us show that we can talk to such a placement. diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index 26599035a..d4904da87 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -60,7 +60,7 @@ class HeadableResource(resource.Resource): class TestProxyPrivate(base.TestCase): def setUp(self): - super(TestProxyPrivate, self).setUp() + super().setUp() def method(self, expected_type, value): return value @@ -204,7 +204,7 @@ def test__get_resource_from_munch(self): class TestProxyDelete(base.TestCase): def setUp(self): - super(TestProxyDelete, self).setUp() + super().setUp() self.session = mock.Mock() self.session._sdk_connection = self.cloud @@ -274,7 +274,7 @@ def test_delete_HttpException(self): class TestProxyUpdate(base.TestCase): def setUp(self): - super(TestProxyUpdate, self).setUp() + super().setUp() self.session = mock.Mock() @@ -317,7 +317,7 @@ def test_update_id(self): class TestProxyCreate(base.TestCase): def setUp(self): - super(TestProxyCreate, self).setUp() + super().setUp() self.session = mock.Mock() self.session._sdk_connection = self.cloud @@ -357,7 +357,7 @@ def test_create_attributes_override_base_path(self): class TestProxyBulkCreate(base.TestCase): def setUp(self): - super(TestProxyBulkCreate, self).setUp() + super().setUp() class Res(resource.Resource): pass @@ -391,7 +391,7 @@ def test_bulk_create_attributes_override_base_path(self): class TestProxyGet(base.TestCase): def setUp(self): - super(TestProxyGet, self).setUp() + super().setUp() self.session = mock.Mock() self.session._sdk_connection = self.cloud @@ -482,7 +482,7 @@ def test_get_not_found(self): class TestProxyList(base.TestCase): def setUp(self): - super(TestProxyList, self).setUp() + super().setUp() self.session = mock.Mock() @@ -545,7 +545,7 @@ def test_list_filters_jmespath(self): class TestProxyHead(base.TestCase): def setUp(self): - super(TestProxyHead, self).setUp() + super().setUp() self.session = mock.Mock() self.session._sdk_connection = self.cloud @@ -619,9 +619,7 @@ class Res(resource.Resource): foo = resource.Body('foo') def setUp(self): - super(TestProxyCache, self).setUp( - cloud_config_fixture='clouds_cache.yaml' - ) + super().setUp(cloud_config_fixture='clouds_cache.yaml') self.session = mock.Mock(spec=session.Session) self.session._sdk_connection = self.cloud @@ -714,7 +712,7 @@ def test_get_bypass_cache(self): class TestProxyCleanup(base.TestCase): def setUp(self): - super(TestProxyCleanup, self).setUp() + super().setUp() self.session = mock.Mock() self.session._sdk_connection = self.cloud diff --git a/openstack/tests/unit/test_proxy_base.py b/openstack/tests/unit/test_proxy_base.py index b901c37f5..d1d749006 100644 --- a/openstack/tests/unit/test_proxy_base.py +++ b/openstack/tests/unit/test_proxy_base.py @@ -17,7 +17,7 @@ class TestProxyBase(base.TestCase): def setUp(self): - super(TestProxyBase, self).setUp() + super().setUp() self.session = mock.Mock() def _verify( diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 7d09eda4b..75c758a25 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1417,7 +1417,7 @@ class Test(resource.Resource): request_body = sot._prepare_request(requires_id=False, patch=True).body self.assertDictEqual( - {u'path': u'/dummy', u'value': u'new_value', u'op': u'replace'}, + {'path': '/dummy', 'value': 'new_value', 'op': 'replace'}, request_body[0], ) @@ -1522,7 +1522,7 @@ class Test(resource.Resource): class TestResourceActions(base.TestCase): def setUp(self): - super(TestResourceActions, self).setUp() + super().setUp() self.service_name = "service" self.base_path = "base_path" @@ -1598,7 +1598,7 @@ def _test_create( base_path=base_path, resource_request_key=resource_request_key, resource_response_key=resource_response_key, - **kwargs + **kwargs, ) id_is_dirty = 'id' in sot._body._dirty @@ -1611,7 +1611,7 @@ def _test_create( requires_id=requires_id, prepend_key=prepend_key, base_path=base_path, - **prepare_kwargs + **prepare_kwargs, ) if requires_id: self.session.put.assert_called_once_with( @@ -1974,7 +1974,7 @@ def _test_commit( prepend_key=prepend_key, has_body=has_body, base_path=base_path, - **commit_args + **commit_args, ) self.sot._prepare_request.assert_called_once_with( @@ -1987,7 +1987,7 @@ def _test_commit( json=self.request.body, headers=self.request.headers, microversion=microversion, - **(expected_args or {}) + **(expected_args or {}), ) elif commit_method == 'POST': self.session.post.assert_called_once_with( @@ -1995,7 +1995,7 @@ def _test_commit( json=self.request.body, headers=self.request.headers, microversion=microversion, - **(expected_args or {}) + **(expected_args or {}), ) elif commit_method == 'PUT': self.session.put.assert_called_once_with( @@ -2003,7 +2003,7 @@ def _test_commit( json=self.request.body, headers=self.request.headers, microversion=microversion, - **(expected_args or {}) + **(expected_args or {}), ) self.assertEqual(self.sot.microversion, microversion) @@ -2679,7 +2679,7 @@ class Test(self.test_class): ) ) self.session.get.assert_called_once_with( - "/{something}/blah".format(something=uri_param), + f"/{uri_param}/blah", headers={'Accept': 'application/json'}, microversion=None, params={qp_name: qp}, @@ -2720,7 +2720,7 @@ class Test(self.test_class): ) ) self.session.get.assert_called_once_with( - "/{something}/blah".format(something=uri_param), + f"/{uri_param}/blah", headers={'Accept': 'application/json'}, microversion=None, params={'a': '1'}, @@ -2756,7 +2756,7 @@ class Test(self.test_class): self.session, paginated=True, something=uri_param, - **{qp_name: qp} + **{qp_name: qp}, ) ) @@ -2803,7 +2803,7 @@ class Test(self.test_class): paginated=True, query_param=qp2, something=uri_param, - **{qp_name: qp} + **{qp_name: qp}, ) ) @@ -3155,7 +3155,7 @@ def _test_bulk_create( self.session, [{'name': 'resource1'}, {'name': 'resource2'}], base_path=base_path, - **params + **params, ) ) @@ -3289,7 +3289,7 @@ class OneResultWithQueryParams(OneResult): _query_mapping = resource.QueryParameters('name') def setUp(self): - super(TestResourceFind, self).setUp() + super().setUp() self.no_results = self.NoResults self.one_result = self.OneResult self.one_result_with_qparams = self.OneResultWithQueryParams diff --git a/openstack/tests/unit/test_stats.py b/openstack/tests/unit/test_stats.py index f97b4d6f9..6a466b1bb 100644 --- a/openstack/tests/unit/test_stats.py +++ b/openstack/tests/unit/test_stats.py @@ -81,7 +81,7 @@ def setUp(self): self.add_info_on_exception('statsd_content', self.statsd.stats) # Set up the above things before the super setup so that we have the # environment variables set when the Connection is created. - super(TestStats, self).setUp() + super().setUp() self._registry = prometheus_client.CollectorRegistry() self.cloud.config._collector_registry = self._registry @@ -358,7 +358,7 @@ def test_timeout(self): class TestNoStats(base.TestCase): def setUp(self): - super(TestNoStats, self).setUp() + super().setUp() self.statsd = StatsdFixture() self.useFixture(self.statsd) diff --git a/openstack/tests/unit/test_utils.py b/openstack/tests/unit/test_utils.py index f5b3413fe..23822ba1b 100644 --- a/openstack/tests/unit/test_utils.py +++ b/openstack/tests/unit/test_utils.py @@ -1,5 +1,3 @@ -# -*- 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 @@ -30,7 +28,7 @@ class Test_enable_logging(base.TestCase): def setUp(self): - super(Test_enable_logging, self).setUp() + super().setUp() self.openstack_logger = mock.Mock() self.openstack_logger.handlers = [] self.ksa_logger_root = mock.Mock() @@ -124,19 +122,19 @@ def test_with_none(self): def test_unicode_strings(self): root = "http://www.example.com" - leaves = u"ascii", u"extra_chars-™" + leaves = "ascii", "extra_chars-™" try: result = utils.urljoin(root, *leaves) except Exception: self.fail("urljoin failed on unicode strings") - self.assertEqual(result, u"http://www.example.com/ascii/extra_chars-™") + self.assertEqual(result, "http://www.example.com/ascii/extra_chars-™") class TestSupportsMicroversion(base.TestCase): def setUp(self): - super(TestSupportsMicroversion, self).setUp() + super().setUp() self.adapter = mock.Mock(spec=['get_endpoint_data']) self.endpoint_data = mock.Mock( spec=['min_microversion', 'max_microversion'], @@ -193,7 +191,7 @@ def test_require_microversion(self, sm_mock): class TestMaximumSupportedMicroversion(base.TestCase): def setUp(self): - super(TestMaximumSupportedMicroversion, self).setUp() + super().setUp() self.adapter = mock.Mock(spec=['get_endpoint_data']) self.endpoint_data = mock.Mock( spec=['min_microversion', 'max_microversion'], @@ -308,8 +306,8 @@ def test_walker_fn(graph, node, lst): class Test_md5(base.TestCase): def setUp(self): - super(Test_md5, self).setUp() - self.md5_test_data = "Openstack forever".encode('utf-8') + super().setUp() + self.md5_test_data = b"Openstack forever" try: self.md5_digest = hashlib.md5( # nosec self.md5_test_data @@ -363,18 +361,18 @@ def test_md5_without_data(self): def test_string_data_raises_type_error(self): if not self.fips_enabled: - self.assertRaises(TypeError, hashlib.md5, u'foo') - self.assertRaises(TypeError, utils.md5, u'foo') + self.assertRaises(TypeError, hashlib.md5, 'foo') + self.assertRaises(TypeError, utils.md5, 'foo') self.assertRaises( - TypeError, utils.md5, u'foo', usedforsecurity=True + TypeError, utils.md5, 'foo', usedforsecurity=True ) else: - self.assertRaises(ValueError, hashlib.md5, u'foo') - self.assertRaises(ValueError, utils.md5, u'foo') + self.assertRaises(ValueError, hashlib.md5, 'foo') + self.assertRaises(ValueError, utils.md5, 'foo') self.assertRaises( - ValueError, utils.md5, u'foo', usedforsecurity=True + ValueError, utils.md5, 'foo', usedforsecurity=True ) - self.assertRaises(TypeError, utils.md5, u'foo', usedforsecurity=False) + self.assertRaises(TypeError, utils.md5, 'foo', usedforsecurity=False) def test_none_data_raises_type_error(self): if not self.fips_enabled: diff --git a/openstack/tests/unit/workflow/test_execution.py b/openstack/tests/unit/workflow/test_execution.py index 0d0b7e9da..ef17b266b 100644 --- a/openstack/tests/unit/workflow/test_execution.py +++ b/openstack/tests/unit/workflow/test_execution.py @@ -29,7 +29,7 @@ class TestExecution(base.TestCase): def setUp(self): - super(TestExecution, self).setUp() + super().setUp() def test_basic(self): sot = execution.Execution() diff --git a/openstack/tests/unit/workflow/test_workflow.py b/openstack/tests/unit/workflow/test_workflow.py index 0e2f7e983..c2a8bc33a 100644 --- a/openstack/tests/unit/workflow/test_workflow.py +++ b/openstack/tests/unit/workflow/test_workflow.py @@ -23,7 +23,7 @@ class TestWorkflow(base.TestCase): def setUp(self): - super(TestWorkflow, self).setUp() + super().setUp() def test_basic(self): sot = workflow.Workflow() diff --git a/openstack/tests/unit/workflow/v2/test_proxy.py b/openstack/tests/unit/workflow/v2/test_proxy.py index 3f8b56352..39d797652 100644 --- a/openstack/tests/unit/workflow/v2/test_proxy.py +++ b/openstack/tests/unit/workflow/v2/test_proxy.py @@ -19,7 +19,7 @@ class TestWorkflowProxy(test_proxy_base.TestProxyBase): def setUp(self): - super(TestWorkflowProxy, self).setUp() + super().setUp() self.proxy = _proxy.Proxy(self.session) def test_workflows(self): diff --git a/openstack/workflow/v2/cron_trigger.py b/openstack/workflow/v2/cron_trigger.py index 2a12e61b2..15a338159 100644 --- a/openstack/workflow/v2/cron_trigger.py +++ b/openstack/workflow/v2/cron_trigger.py @@ -72,6 +72,4 @@ class CronTrigger(resource.Resource): updated_at = resource.Body("updated_at") def create(self, session, base_path=None): - return super(CronTrigger, self).create( - session, prepend_key=False, base_path=base_path - ) + return super().create(session, prepend_key=False, base_path=base_path) diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py index ac4be8e19..8507c1df9 100644 --- a/releasenotes/source/conf.py +++ b/releasenotes/source/conf.py @@ -1,4 +1,3 @@ -# -*- 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 diff --git a/tools/keystone_version.py b/tools/keystone_version.py index 8ca504eb1..5de9b63b3 100644 --- a/tools/keystone_version.py +++ b/tools/keystone_version.py @@ -57,7 +57,7 @@ def print_version(version): if verbose: pprint.pprint(r) except Exception as e: - print("Error with {cloud}: {e}".format(cloud=cloud.name, e=str(e))) + print(f"Error with {cloud.name}: {str(e)}") continue if 'version' in r: print_version(r['version']) @@ -70,11 +70,11 @@ def print_version(version): port = None stripped = path.rsplit('/', 2)[0] if port: - stripped = '{stripped}:{port}'.format(stripped=stripped, port=port) + stripped = f'{stripped}:{port}' endpoint = urlparse.urlunsplit( (url.scheme, url.netloc, stripped, url.params, url.query) ) - print(" also {endpoint}".format(endpoint=endpoint)) + print(f" also {endpoint}") try: r = c.get(endpoint).json() if verbose: @@ -87,6 +87,6 @@ def print_version(version): elif 'versions' in r: print_versions(r['versions']) else: - print("\n\nUNKNOWN\n\n{r}".format(r=r)) + print(f"\n\nUNKNOWN\n\n{r}") else: print_versions(r['versions']) diff --git a/tools/print-services.py b/tools/print-services.py index 6b9c95b8e..98602070d 100644 --- a/tools/print-services.py +++ b/tools/print-services.py @@ -41,9 +41,7 @@ def make_names(): if desc_class.__module__ != 'openstack.service_description': base_mod, dm = desc_class.__module__.rsplit('.', 1) - imports.append( - 'from {base_mod} import {dm}'.format(base_mod=base_mod, dm=dm) - ) + imports.append(f'from {base_mod} import {dm}') else: dm = 'service_description' @@ -62,9 +60,7 @@ def make_names(): for alias_name in _get_aliases(st): if alias_name[-1].isdigit(): continue - services.append( - '{alias_name} = {st}'.format(alias_name=alias_name, st=st) - ) + services.append(f'{alias_name} = {st}') services.append('') print("# Generated file, to change, run tools/print-services.py") for imp in sorted(imports): @@ -73,7 +69,7 @@ def make_names(): print("class ServicesMixin:\n") for service in services: if service: - print(" {service}".format(service=service)) + print(f" {service}") else: print() From fc539d10fa5ca4040dc635fd715c660c34f3958d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 23 Apr 2024 12:11:27 +0100 Subject: [PATCH 3486/3836] pre-commit: Add pyupgrade hook Another day, another useful hook Change-Id: I2386981aac4b6061247ec5c7acd58ba50ad4bfec Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f5b3fa8fe..f88170eb9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,6 +21,11 @@ repos: rev: v1.1.1 hooks: - id: doc8 + - repo: https://github.com/asottile/pyupgrade + rev: v3.15.2 + hooks: + - id: pyupgrade + args: ['--py38-plus'] - repo: https://github.com/psf/black rev: 24.4.0 hooks: From 30d5753bcf08b262a7879bd0831ea8c510743d9d Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Mon, 29 Apr 2024 10:47:49 +0000 Subject: [PATCH 3487/3836] reno: Update master for unmaintained/zed Update the zed release notes configuration to build from unmaintained/zed. Change-Id: I94119d23f1b956c4552238dcc668adbbda2a780f --- releasenotes/source/zed.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/source/zed.rst b/releasenotes/source/zed.rst index 9608c05e4..6cc2b1554 100644 --- a/releasenotes/source/zed.rst +++ b/releasenotes/source/zed.rst @@ -3,4 +3,4 @@ Zed Series Release Notes ======================== .. release-notes:: - :branch: stable/zed + :branch: unmaintained/zed From ba97594942bfe7532c9e13f68c4166308167727b Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 26 Apr 2024 11:27:55 +0100 Subject: [PATCH 3488/3836] docs: Add missing identity v3 proxy API docs We also reorder the proxy API to combine two groups. Change-Id: Id9cb8f7cdffa374bdb4f744cf2d12fbe2709904e Signed-off-by: Stephen Finucane --- doc/source/user/proxies/identity_v3.rst | 78 +++++- openstack/identity/v3/_proxy.py | 305 ++++++++++++------------ 2 files changed, 217 insertions(+), 166 deletions(-) diff --git a/doc/source/user/proxies/identity_v3.rst b/doc/source/user/proxies/identity_v3.rst index 1afe30ed0..ccf541b8f 100644 --- a/doc/source/user/proxies/identity_v3.rst +++ b/doc/source/user/proxies/identity_v3.rst @@ -67,7 +67,29 @@ Project Operations .. autoclass:: openstack.identity.v3._proxy.Proxy :noindex: :members: create_project, update_project, delete_project, get_project, - find_project, projects + find_project, projects, user_projects + +Service Operations +^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.identity.v3._proxy.Proxy + :noindex: + :members: create_service, update_service, delete_service, get_service, + find_service, services + +User Operations +^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.identity.v3._proxy.Proxy + :noindex: + :members: create_user, update_user, delete_user, get_user, find_user, users, + +Trust Operations +^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.identity.v3._proxy.Proxy + :noindex: + :members: create_trust, delete_trust, get_trust, find_trust, trusts Region Operations ^^^^^^^^^^^^^^^^^ @@ -100,25 +122,59 @@ Role Assignment Operations validate_user_has_system_role, assign_system_role_to_group, unassign_system_role_from_group, validate_group_has_system_role -Service Operations -^^^^^^^^^^^^^^^^^^ +Registered Limit Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.identity.v3._proxy.Proxy :noindex: - :members: create_service, update_service, delete_service, get_service, - find_service, services + :members: registered_limits, get_registered_limit, create_registered_limit, + update_registered_limit, delete_registered_limit -Trust Operations +Limit Operations ^^^^^^^^^^^^^^^^ .. autoclass:: openstack.identity.v3._proxy.Proxy :noindex: - :members: create_trust, delete_trust, get_trust, find_trust, trusts + :members: limits, get_limit, create_limit, update_limit, delete_limit -User Operations -^^^^^^^^^^^^^^^ +Application Credential Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.identity.v3._proxy.Proxy :noindex: - :members: create_user, update_user, delete_user, get_user, find_user, users, - user_projects + :members: application_credentials, get_application_credential, + create_application_credential, find_application_credential, + delete_application_credential + +Federation Protocol Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.identity.v3._proxy.Proxy + :noindex: + :members: create_federation_protocol, delete_federation_protocol, + find_federation_protocol, get_federation_protocol, + federation_protocols, update_federation_protocol + +Mapping Operations +^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.identity.v3._proxy.Proxy + :noindex: + :members: create_mapping, delete_mapping, find_mapping, get_mapping, + mappings, update_mapping + +Identity Provider Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.identity.v3._proxy.Proxy + :noindex: + :members: create_identity_provider, delete_identity_provider, + find_identity_provider, get_identity_provider, identity_providers, + update_identity_provider + +Access Rule Operations +^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.identity.v3._proxy.Proxy + :noindex: + :members: access_rules, access_rules, delete_access_rule diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 1463f1fdd..35b59faed 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -1232,152 +1232,6 @@ def role_assignments(self, **query): """ return self._list(_role_assignment.RoleAssignment, **query) - # ========== Registered limits ========== - - def registered_limits(self, **query): - """Retrieve a generator of registered_limits - - :param kwargs query: Optional query parameters to be sent to limit - the registered_limits being returned. - - :returns: A generator of registered_limits instances. - :rtype: :class: - `~openstack.identity.v3.registered_limit.RegisteredLimit` - """ - return self._list(_registered_limit.RegisteredLimit, **query) - - def get_registered_limit(self, registered_limit): - """Get a single registered_limit - - :param registered_limit: The value can be the ID of a registered_limit - or a :class: - `~openstack.identity.v3.registered_limit.RegisteredLimit` instance. - - :returns: One :class: - `~openstack.identity.v3.registered_limit.RegisteredLimit` - :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. - """ - return self._get(_registered_limit.RegisteredLimit, registered_limit) - - def create_registered_limit(self, **attrs): - """Create a new registered_limit from attributes - - :param dict attrs: Keyword arguments which will be used to create a - :class:`~openstack.identity.v3.registered_limit.RegisteredLimit`, - comprised of the properties on the RegisteredLimit class. - - :returns: The results of registered_limit creation. - :rtype: :class: - `~openstack.identity.v3.registered_limit.RegisteredLimit` - """ - return self._create(_registered_limit.RegisteredLimit, **attrs) - - def update_registered_limit(self, registered_limit, **attrs): - """Update a registered_limit - - :param registered_limit: Either the ID of a registered_limit. or a - :class:`~openstack.identity.v3.registered_limit.RegisteredLimit` - instance. - :param dict kwargs: The attributes to update on the registered_limit - represented by ``value``. - - :returns: The updated registered_limit. - :rtype: :class: - `~openstack.identity.v3.registered_limit.RegisteredLimit` - """ - return self._update( - _registered_limit.RegisteredLimit, registered_limit, **attrs - ) - - def delete_registered_limit(self, registered_limit, ignore_missing=True): - """Delete a registered_limit - - :param registered_limit: The value can be either the ID of a - registered_limit or a - :class:`~openstack.identity.v3.registered_limit.RegisteredLimit` - instance. - :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when - the registered_limit does not exist. When set to ``True``, no - exception will be thrown when attempting to delete a nonexistent - registered_limit. - - :returns: ``None`` - """ - self._delete( - _registered_limit.RegisteredLimit, - registered_limit, - ignore_missing=ignore_missing, - ) - - # ========== Limits ========== - - def limits(self, **query): - """Retrieve a generator of limits - - :param kwargs query: Optional query parameters to be sent to limit - the limits being returned. - - :returns: A generator of limits instances. - :rtype: :class:`~openstack.identity.v3.limit.Limit` - """ - return self._list(_limit.Limit, **query) - - def get_limit(self, limit): - """Get a single limit - - :param limit: The value can be the ID of a limit - or a :class:`~openstack.identity.v3.limit.Limit` instance. - - :returns: One :class: - `~openstack.identity.v3.limit.Limit` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no - resource can be found. - """ - return self._get(_limit.Limit, limit) - - def create_limit(self, **attrs): - """Create a new limit from attributes - - :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.identity.v3.limit.Limit`, comprised of the - properties on the Limit class. - - :returns: The results of limit creation. - :rtype: :class:`~openstack.identity.v3.limit.Limit` - """ - return self._create(_limit.Limit, **attrs) - - def update_limit(self, limit, **attrs): - """Update a limit - - :param limit: Either the ID of a limit. or a - :class:`~openstack.identity.v3.limit.Limit` instance. - :param dict kwargs: The attributes to update on the limit represented - by ``value``. - - :returns: The updated limit. - :rtype: :class:`~openstack.identity.v3.limit.Limit` - """ - return self._update(_limit.Limit, limit, **attrs) - - def delete_limit(self, limit, ignore_missing=True): - """Delete a limit - - :param limit: The value can be either the ID of a limit or a - :class:`~openstack.identity.v3.limit.Limit` instance. - :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when - the limit does not exist. When set to ``True``, no exception will - be thrown when attempting to delete a nonexistent limit. - - :returns: ``None`` - """ - self._delete(_limit.Limit, limit, ignore_missing=ignore_missing) - - # ========== Roles ========== - def assign_domain_role_to_user(self, domain, user, role): """Assign role to user on a domain @@ -1666,6 +1520,147 @@ def validate_group_has_system_role(self, group, role, system): system = self._get_resource(_system.System, system) return system.validate_group_has_role(self, group, role) + # ========== Registered limits ========== + + def registered_limits(self, **query): + """Retrieve a generator of registered_limits + + :param kwargs query: Optional query parameters to be sent to limit + the registered_limits being returned. + + :returns: A generator of registered_limits instances. + :rtype: :class:`~openstack.identity.v3.registered_limit.RegisteredLimit` + """ + return self._list(_registered_limit.RegisteredLimit, **query) + + def get_registered_limit(self, registered_limit): + """Get a single registered_limit + + :param registered_limit: The value can be the ID of a registered_limit + or a + :class:`~openstack.identity.v3.registered_limit.RegisteredLimit` + instance. + + :returns: One :class:`~openstack.identity.v3.registered_limit.RegisteredLimit` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_registered_limit.RegisteredLimit, registered_limit) + + def create_registered_limit(self, **attrs): + """Create a new registered_limit from attributes + + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.identity.v3.registered_limit.RegisteredLimit`, + comprised of the properties on the RegisteredLimit class. + + :returns: The results of registered_limit creation. + :rtype: :class:`~openstack.identity.v3.registered_limit.RegisteredLimit` + """ + return self._create(_registered_limit.RegisteredLimit, **attrs) + + def update_registered_limit(self, registered_limit, **attrs): + """Update a registered_limit + + :param registered_limit: Either the ID of a registered_limit. or a + :class:`~openstack.identity.v3.registered_limit.RegisteredLimit` + instance. + :param dict kwargs: The attributes to update on the registered_limit + represented by ``value``. + + :returns: The updated registered_limit. + :rtype: + :class:`~openstack.identity.v3.registered_limit.RegisteredLimit` + """ + return self._update( + _registered_limit.RegisteredLimit, registered_limit, **attrs + ) + + def delete_registered_limit(self, registered_limit, ignore_missing=True): + """Delete a registered_limit + + :param registered_limit: The value can be either the ID of a + registered_limit or a + :class:`~openstack.identity.v3.registered_limit.RegisteredLimit` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the registered_limit does not exist. When set to ``True``, no + exception will be thrown when attempting to delete a nonexistent + registered_limit. + + :returns: ``None`` + """ + self._delete( + _registered_limit.RegisteredLimit, + registered_limit, + ignore_missing=ignore_missing, + ) + + # ========== Limits ========== + + def limits(self, **query): + """Retrieve a generator of limits + + :param kwargs query: Optional query parameters to be sent to limit + the limits being returned. + + :returns: A generator of limits instances. + :rtype: :class:`~openstack.identity.v3.limit.Limit` + """ + return self._list(_limit.Limit, **query) + + def get_limit(self, limit): + """Get a single limit + + :param limit: The value can be the ID of a limit + or a :class:`~openstack.identity.v3.limit.Limit` instance. + + :returns: One :class:`~openstack.identity.v3.limit.Limit` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource can be found. + """ + return self._get(_limit.Limit, limit) + + def create_limit(self, **attrs): + """Create a new limit from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.identity.v3.limit.Limit`, comprised of the + properties on the Limit class. + + :returns: The results of limit creation. + :rtype: :class:`~openstack.identity.v3.limit.Limit` + """ + return self._create(_limit.Limit, **attrs) + + def update_limit(self, limit, **attrs): + """Update a limit + + :param limit: Either the ID of a limit. or a + :class:`~openstack.identity.v3.limit.Limit` instance. + :param dict kwargs: The attributes to update on the limit represented + by ``value``. + + :returns: The updated limit. + :rtype: :class:`~openstack.identity.v3.limit.Limit` + """ + return self._update(_limit.Limit, limit, **attrs) + + def delete_limit(self, limit, ignore_missing=True): + """Delete a limit + + :param limit: The value can be either the ID of a limit or a + :class:`~openstack.identity.v3.limit.Limit` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the limit does not exist. When set to ``True``, no exception will + be thrown when attempting to delete a nonexistent limit. + + :returns: ``None`` + """ + self._delete(_limit.Limit, limit, ignore_missing=ignore_missing) + # ========== Application credentials ========== def application_credentials(self, user, **query): @@ -1695,9 +1690,9 @@ def get_application_credential(self, user, application_credential): :class:`~openstack.identity.v3.user.User` instance. :param application_credential: The value can be the ID of a - application credential or a :class: - `~openstack.identity.v3.application_credential. - ApplicationCredential` instance. + application credential or a + :class:`~openstack.identity.v3.application_credential.ApplicationCredential` + instance. :returns: One :class:`~openstack.identity.v3.application_credential.ApplicationCredential` @@ -2136,8 +2131,8 @@ def update_identity_provider(self, identity_provider, **attrs): def access_rules(self, user, **query): """Retrieve a generator of access rules - :param user: Either the ID of a user or a :class:`~.user.User` - instance. + :param user: Either the ID of a user or a + :class:`~openstack.identity.v3.user.User` instance. :param kwargs query: Optional query parameters to be sent to limit the resources being returned. @@ -2150,8 +2145,8 @@ def access_rules(self, user, **query): def get_access_rule(self, user, access_rule): """Get a single access rule - :param user: Either the ID of a user or a :class:`~.user.User` - instance. + :param user: Either the ID of a user or a + :class:`~openstack.identity.v3.user.User` instance. :param access rule: The value can be the ID of an access rule or a :class:`~.access_rule.AccessRule` instance. @@ -2165,8 +2160,8 @@ def get_access_rule(self, user, access_rule): def delete_access_rule(self, user, access_rule, ignore_missing=True): """Delete an access rule - :param user: Either the ID of a user or a :class:`~.user.User` - instance. + :param user: Either the ID of a user or a + :class:`~openstack.identity.v3.user.User` instance. :param access rule: The value can be either the ID of an access rule or a :class:`~.access_rule.AccessRule` instance. :param bool ignore_missing: When set to ``False`` From 46af31f627ae07e4f8728b5ae4f45a8179a12d6a Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 26 Apr 2024 11:10:43 +0100 Subject: [PATCH 3489/3836] Add support for federation service providers Change-Id: I0c4c1ee77b0bbccd18af5b9b84a585a79f1a500c --- doc/source/user/proxies/identity_v3.rst | 9 ++ openstack/identity/v3/_proxy.py | 102 +++++++++++++++++- openstack/identity/v3/service_provider.py | 48 +++++++++ .../tests/unit/identity/v3/test_proxy.py | 46 ++++++++ .../unit/identity/v3/test_service_provider.py | 62 +++++++++++ ...ice-provider-support-8c97cbb157883626.yaml | 4 + 6 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 openstack/identity/v3/service_provider.py create mode 100644 openstack/tests/unit/identity/v3/test_service_provider.py create mode 100644 releasenotes/notes/add-identity-service-provider-support-8c97cbb157883626.yaml diff --git a/doc/source/user/proxies/identity_v3.rst b/doc/source/user/proxies/identity_v3.rst index ccf541b8f..64a87f5b6 100644 --- a/doc/source/user/proxies/identity_v3.rst +++ b/doc/source/user/proxies/identity_v3.rst @@ -178,3 +178,12 @@ Access Rule Operations .. autoclass:: openstack.identity.v3._proxy.Proxy :noindex: :members: access_rules, access_rules, delete_access_rule + +Service Provider Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.identity.v3._proxy.Proxy + :noindex: + :members: create_service_provider, delete_service_provider, + find_service_provider, get_service_provider, service_providers, + update_service_provider diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 35b59faed..9f8de2715 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -49,6 +49,7 @@ role_system_user_assignment as _role_system_user_assignment, ) from openstack.identity.v3 import service as _service +from openstack.identity.v3 import service_provider as _service_provider from openstack.identity.v3 import system as _system from openstack.identity.v3 import trust as _trust from openstack.identity.v3 import user as _user @@ -316,7 +317,7 @@ def update_domain_config(self, domain, **attrs): :param domain_id: The value can be the ID of a domain or a :class:`~openstack.identity.v3.domain.Domain` instance. - :attrs kwargs: The attributes to update on the config for a domain + :param attrs: The attributes to update on the config for a domain represented by ``domain_id``. :returns: The updated config for a domain @@ -2178,3 +2179,102 @@ def delete_access_rule(self, user, access_rule, ignore_missing=True): user_id=user.id, ignore_missing=ignore_missing, ) + + # ========== Service providers ========== + + def create_service_provider(self, **attrs): + """Create a new service provider from attributes + + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.identity.v3.service_provider.ServiceProvider`, + comprised of the properties on the ServiceProvider class. + + :returns: The results of service provider creation + :rtype: + :class:`~openstack.identity.v3.service_provider.ServiceProvider` + """ + return self._create(_service_provider.ServiceProvider, **attrs) + + def delete_service_provider(self, service_provider, ignore_missing=True): + """Delete a service provider + + :param service_provider: The ID of a service provider or a + :class:`~openstack.identity.v3.service_provider.ServiceProvider` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the service provider does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent service provider. + + :returns: ``None`` + """ + self._delete( + _service_provider.ServiceProvider, + service_provider, + ignore_missing=ignore_missing, + ) + + def find_service_provider(self, name_or_id, ignore_missing=True): + """Find a single service provider + + :param name_or_id: The name or ID of a service provider + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the resource does not exist. When set to ``True``, None will be + returned when attempting to find a nonexistent resource. + + :returns: The details of an service provider or None. + :rtype: + :class:`~openstack.identity.v3.service_provider.ServiceProvider` + """ + return self._find( + _service_provider.ServiceProvider, + name_or_id, + ignore_missing=ignore_missing, + ) + + def get_service_provider(self, service_provider): + """Get a single service provider + + :param service_provider: The value can be the ID of a service provider + or a + :class:`~openstack.identity.v3.server_provider.ServiceProvider` + instance. + + :returns: The details of an service provider. + :rtype: + :class:`~openstack.identity.v3.service_provider.ServiceProvider` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_service_provider.ServiceProvider, service_provider) + + def service_providers(self, **query): + """Retrieve a generator of service providers + + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. + + :returns: A generator of service provider instances. + :rtype: + :class:`~openstack.identity.v3.service_provider.ServiceProvider` + """ + return self._list(_service_provider.ServiceProvider, **query) + + def update_service_provider(self, service_provider, **attrs): + """Update a service provider + + :param service_provider: Either the ID of an service provider or a + :class:`~openstack.identity.v3.service_provider.ServiceProvider` + instance. + :param attrs: The attributes to update on the service provider + represented by ``service_provider``. + + :returns: The updated service provider. + :rtype: + :class:`~openstack.identity.v3.service_provider.ServiceProvider` + """ + return self._update( + _service_provider.ServiceProvider, service_provider, **attrs + ) diff --git a/openstack/identity/v3/service_provider.py b/openstack/identity/v3/service_provider.py new file mode 100644 index 000000000..7185a0807 --- /dev/null +++ b/openstack/identity/v3/service_provider.py @@ -0,0 +1,48 @@ +# 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 resource + + +class ServiceProvider(resource.Resource): + resource_key = 'service_provider' + resources_key = 'service_providers' + base_path = '/OS-FEDERATION/service_providers' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + create_method = 'PUT' + create_exclude_id_from_body = True + commit_method = 'PATCH' + + _query_mapping = resource.QueryParameters( + 'id', + is_enabled='enabled', + ) + + # Properties + #: The URL to authenticate against. + auth_url = resource.Body('auth_url') + #: A description of this service provider. + description = resource.Body('description') + #: If the service provider is currently enabled. + is_enabled = resource.Body('enabled', type=bool) + #: The identifier of the service provider. + name = resource.Body('id') + #: The prefix of the RelayState SAML attribute. + relay_state_prefix = resource.Body('relay_state_prefix') + #: The service provider's URL. + sp_url = resource.Body('sp_url') diff --git a/openstack/tests/unit/identity/v3/test_proxy.py b/openstack/tests/unit/identity/v3/test_proxy.py index dd12919d9..3fd3895ab 100644 --- a/openstack/tests/unit/identity/v3/test_proxy.py +++ b/openstack/tests/unit/identity/v3/test_proxy.py @@ -30,6 +30,7 @@ from openstack.identity.v3 import role_system_group_assignment from openstack.identity.v3 import role_system_user_assignment from openstack.identity.v3 import service +from openstack.identity.v3 import service_provider from openstack.identity.v3 import trust from openstack.identity.v3 import user from openstack.tests.unit import test_proxy_base @@ -753,3 +754,48 @@ def test_access_rules(self): method_kwargs={'user': USER_ID}, expected_kwargs={'user_id': USER_ID}, ) + + +class TestServiceProvider(TestIdentityProxyBase): + def test_service_provider_create(self): + self.verify_create( + self.proxy.create_service_provider, + service_provider.ServiceProvider, + ) + + def test_service_provider_delete(self): + self.verify_delete( + self.proxy.delete_service_provider, + service_provider.ServiceProvider, + False, + ) + + def test_service_provider_delete_ignore(self): + self.verify_delete( + self.proxy.delete_service_provider, + service_provider.ServiceProvider, + True, + ) + + def test_service_provider_find(self): + self.verify_find( + self.proxy.find_service_provider, service_provider.ServiceProvider + ) + + def test_service_provider_get(self): + self.verify_get( + self.proxy.get_service_provider, + service_provider.ServiceProvider, + ) + + def test_service_providers(self): + self.verify_list( + self.proxy.service_providers, + service_provider.ServiceProvider, + ) + + def test_service_provider_update(self): + self.verify_update( + self.proxy.update_service_provider, + service_provider.ServiceProvider, + ) diff --git a/openstack/tests/unit/identity/v3/test_service_provider.py b/openstack/tests/unit/identity/v3/test_service_provider.py new file mode 100644 index 000000000..068b6afd4 --- /dev/null +++ b/openstack/tests/unit/identity/v3/test_service_provider.py @@ -0,0 +1,62 @@ +# 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.identity.v3 import service_provider +from openstack.tests.unit import base + + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'id': IDENTIFIER, + 'description': 'An example description', + 'is_enabled': True, + 'auth_url': ( + "https://auth.example.com/v3/OS-FEDERATION/" + "identity_providers/idp/protocols/saml2/auth" + ), + 'sp_url': 'https://auth.example.com/Shibboleth.sso/SAML2/ECP', +} + + +class TestServiceProvider(base.TestCase): + def test_basic(self): + sot = service_provider.ServiceProvider() + self.assertEqual('service_provider', sot.resource_key) + self.assertEqual('service_providers', sot.resources_key) + self.assertEqual('/OS-FEDERATION/service_providers', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.create_exclude_id_from_body) + self.assertEqual('PATCH', sot.commit_method) + self.assertEqual('PUT', sot.create_method) + + self.assertDictEqual( + { + 'id': 'id', + 'limit': 'limit', + 'marker': 'marker', + 'is_enabled': 'enabled', + }, + sot._query_mapping._mapping, + ) + + def test_make_it(self): + sot = service_provider.ServiceProvider(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['id'], sot.name) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['is_enabled'], sot.is_enabled) + self.assertEqual(EXAMPLE['auth_url'], sot.auth_url) + self.assertEqual(EXAMPLE['sp_url'], sot.sp_url) diff --git a/releasenotes/notes/add-identity-service-provider-support-8c97cbb157883626.yaml b/releasenotes/notes/add-identity-service-provider-support-8c97cbb157883626.yaml new file mode 100644 index 000000000..1b50dfe32 --- /dev/null +++ b/releasenotes/notes/add-identity-service-provider-support-8c97cbb157883626.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add support for service providers to the identity service. From 00ed7a5eed6817eca27a99a0353784e7f92974fd Mon Sep 17 00:00:00 2001 From: ArtofBugs <74070945+ArtofBugs@users.noreply.github.com> Date: Fri, 15 Mar 2024 14:09:38 -0700 Subject: [PATCH 3490/3836] Identity: Add support for inherited_to for role_assignments Change-Id: I977fba4a49d8bd779fc14851ab4145cebb66d46c --- openstack/cloud/_identity.py | 31 ++++++++++++++++--- openstack/identity/v3/_proxy.py | 2 +- openstack/identity/v3/role_assignment.py | 3 +- .../unit/identity/v3/test_role_assignment.py | 18 +++++++++++ ...ted-role-assignments-8fe9ac9509d99f4d.yaml | 11 +++++++ 5 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/identity-cloud-mixin-inherited-role-assignments-8fe9ac9509d99f4d.yaml diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index 5ee0e6aca..d720f74d8 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -10,10 +10,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +import warnings + from openstack.cloud import _utils from openstack import exceptions from openstack.identity.v3._proxy import Proxy from openstack import utils +from openstack import warnings as os_warnings class IdentityCloudMixin: @@ -1119,7 +1122,15 @@ def _keystone_v3_role_assignments(self, **filters): # proxy filters['scope.' + k + '.id'] = filters[k] del filters[k] - if 'os_inherit_extension_inherited_to' in filters: + if 'inherited_to' in filters: + filters['scope.OS-INHERIT:inherited_to'] = filters['inherited_to'] + del filters['inherited_to'] + elif 'os_inherit_extension_inherited_to' in filters: + warnings.warn( + "os_inherit_extension_inherited_to is deprecated. Use " + "inherited_to instead.", + os_warnings.OpenStackDeprecationWarning, + ) filters['scope.OS-INHERIT:inherited_to'] = filters[ 'os_inherit_extension_inherited_to' ] @@ -1138,15 +1149,17 @@ def list_role_assignments(self, filters=None): * 'domain' (string) - Domain ID to be used as query filter. * 'system' (string) - System name to be used as query filter. * 'role' (string) - Role ID to be used as query filter. - * 'os_inherit_extension_inherited_to' (string) - Return inherited - role assignments for either 'projects' or 'domains' + * 'inherited_to' (string) - Return inherited + role assignments for either 'projects' or 'domains'. + * 'os_inherit_extension_inherited_to' (string) - Deprecated; use + 'inherited_to' instead. * 'effective' (boolean) - Return effective role assignments. * 'include_subtree' (boolean) - Include subtree 'user' and 'group' are mutually exclusive, as are 'domain' and 'project'. - :returns: A list of indentity + :returns: A list of identity :class:`openstack.identity.v3.role_assignment.RoleAssignment` objects :raises: :class:`~openstack.exceptions.SDKException` if something goes @@ -1182,6 +1195,16 @@ def list_role_assignments(self, filters=None): system_scope = filters.pop('system') filters['scope.system'] = system_scope + if 'os_inherit_extension_inherited_to' in filters: + warnings.warn( + "os_inherit_extension_inherited_to is deprecated. Use " + "inherited_to instead.", + os_warnings.OpenStackDeprecationWarning, + ) + filters['inherited_to'] = filters.pop( + 'os_inherit_extension_inherited_to' + ) + return list(self.identity.role_assignments(**filters)) @_utils.valid_kwargs('domain_id') diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 0ed5f9454..0fe4838a0 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -1225,7 +1225,7 @@ def role_assignments(self, **query): :param kwargs query: Optional query parameters to be sent to limit the resources being returned. The options are: group_id, role_id, scope_domain_id, - scope_project_id, user_id, include_names, + scope_project_id, inherited_to, user_id, include_names, include_subtree. :return: :class:`~openstack.identity.v3.role_assignment.RoleAssignment` diff --git a/openstack/identity/v3/role_assignment.py b/openstack/identity/v3/role_assignment.py index e47dec027..9ac6502f8 100644 --- a/openstack/identity/v3/role_assignment.py +++ b/openstack/identity/v3/role_assignment.py @@ -36,6 +36,7 @@ class RoleAssignment(resource.Resource): scope_project_id='scope.project.id', scope_domain_id='scope.domain.id', scope_system='scope.system', + inherited_to='scope.OS-INHERIT:inherited_to', ) # Properties @@ -43,7 +44,7 @@ class RoleAssignment(resource.Resource): links = resource.Body('links') #: The role (dictionary contains only id) *Type: dict* role = resource.Body('role', type=dict) - #: The scope (either domain or group dictionary contains id) *Type: dict* + #: The scope (either domain or project; dictionary contains only id) *Type: dict* scope = resource.Body('scope', type=dict) #: The user (dictionary contains only id) *Type: dict* user = resource.Body('user', type=dict) diff --git a/openstack/tests/unit/identity/v3/test_role_assignment.py b/openstack/tests/unit/identity/v3/test_role_assignment.py index 35c224e1a..63e8d29be 100644 --- a/openstack/tests/unit/identity/v3/test_role_assignment.py +++ b/openstack/tests/unit/identity/v3/test_role_assignment.py @@ -32,6 +32,24 @@ def test_basic(self): self.assertEqual('/role_assignments', sot.base_path) self.assertTrue(sot.allow_list) + self.assertDictEqual( + { + 'group_id': 'group.id', + 'role_id': 'role.id', + 'scope_domain_id': 'scope.domain.id', + 'scope_project_id': 'scope.project.id', + 'scope_system': 'scope.system', + 'user_id': 'user.id', + 'effective': 'effective', + 'inherited_to': 'scope.OS-INHERIT:inherited_to', + 'include_names': 'include_names', + 'include_subtree': 'include_subtree', + 'limit': 'limit', + 'marker': 'marker', + }, + sot._query_mapping._mapping, + ) + def test_make_it(self): sot = role_assignment.RoleAssignment(**EXAMPLE) self.assertEqual(EXAMPLE['id'], sot.id) diff --git a/releasenotes/notes/identity-cloud-mixin-inherited-role-assignments-8fe9ac9509d99f4d.yaml b/releasenotes/notes/identity-cloud-mixin-inherited-role-assignments-8fe9ac9509d99f4d.yaml new file mode 100644 index 000000000..ab1216793 --- /dev/null +++ b/releasenotes/notes/identity-cloud-mixin-inherited-role-assignments-8fe9ac9509d99f4d.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + Add support for ``inherited_to`` filter for listing identity role + assignments in the cloud layer. This allows filtering by whether role + grants are inheritable to sub-projects. +deprecations: + - | + Deprecate ``os-inherit-extension-inherited-to`` in favor of + ``inherited_to`` filter for listing identity role_assignments in the cloud + layer. From 2b6b32fa3dd80f0d9d05ddf9130a02464ed5bf2a Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 26 Jul 2023 11:14:14 +0100 Subject: [PATCH 3491/3836] baremetal: Decode 'config_drive' argument to 'set_provision_state' ironicclient automatically decoded the base64-encoded byte strings provided to the 'node.set_provision_state' method. We should do the same. Change-Id: Ib4997d11b460078cddf2f422223a67a216cf17df Signed-off-by: Stephen Finucane --- openstack/baremetal/v1/node.py | 8 ++++++++ openstack/tests/unit/baremetal/v1/test_node.py | 15 +++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 052358400..524930413 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -465,6 +465,14 @@ def set_provision_state( 'Config drive can only be provided with ' '"active" and "rebuild" targets' ) + if isinstance(config_drive, bytes): + try: + config_drive = config_drive.decode('utf-8') + except UnicodeError: + raise ValueError( + 'Config drive must be a dictionary or a base64 ' + 'encoded string' + ) # Not a typo - ironic accepts "configdrive" (without underscore) body['configdrive'] = config_drive diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 022f99466..9fb440143 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import base64 from unittest import mock from keystoneauth1 import adapter @@ -338,6 +339,20 @@ def test_deploy_with_configdrive(self): retriable_status_codes=_common.RETRIABLE_STATUS_CODES, ) + def test_deploy_with_configdrive_as_bytestring(self): + config_drive = base64.b64encode(b'foo') + result = self.node.set_provision_state( + self.session, 'active', config_drive=config_drive + ) + self.assertIs(result, self.node) + self.session.put.assert_called_once_with( + 'nodes/%s/states/provision' % self.node.id, + json={'target': 'active', 'configdrive': config_drive.decode()}, + headers=mock.ANY, + microversion=None, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) + def test_rebuild_with_configdrive(self): result = self.node.set_provision_state( self.session, 'rebuild', config_drive='abcd' From 8091075e3b8a9bb160eaee8cd4b24fb3c6410642 Mon Sep 17 00:00:00 2001 From: Ghanshyam Mann Date: Fri, 17 May 2024 11:58:16 -0700 Subject: [PATCH 3492/3836] Remove retired project Senlin job Senlin project has been retired[1] so removing its ref and job from sdk gate otherwise it will fail. [1] https://review.opendev.org/c/openstack/governance/+/919347 Change-Id: I7e25e30b95ebb8df7984a706ece432813554a395 --- zuul.d/functional-jobs.yaml | 19 ------------------- zuul.d/project.yaml | 2 -- 2 files changed, 21 deletions(-) diff --git a/zuul.d/functional-jobs.yaml b/zuul.d/functional-jobs.yaml index 89cea4d5b..a0a0b08aa 100644 --- a/zuul.d/functional-jobs.yaml +++ b/zuul.d/functional-jobs.yaml @@ -257,25 +257,6 @@ OPENSTACKSDK_HAS_SWIFT: 0 OPENSTACKSDK_HAS_MAGNUM: 1 -- job: - name: openstacksdk-functional-devstack-senlin - parent: openstacksdk-functional-devstack - description: | - Run openstacksdk functional tests against a master devstack with senlin - required-projects: - - openstack/senlin - vars: - devstack_plugins: - senlin: https://opendev.org/openstack/senlin - devstack_services: - s-account: false - s-container: false - s-object: false - s-proxy: false - tox_environment: - OPENSTACKSDK_HAS_SWIFT: 0 - OPENSTACKSDK_HAS_SENLIN: 1 - - job: name: openstacksdk-functional-devstack-ironic parent: openstacksdk-functional-devstack-minimum diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index 284d200a2..88fcb4c86 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -22,7 +22,6 @@ - openstacksdk-functional-devstack - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-networking-ext - - openstacksdk-functional-devstack-senlin - openstacksdk-functional-devstack-magnum: voting: false - openstacksdk-functional-devstack-manila: @@ -45,4 +44,3 @@ - openstacksdk-functional-devstack - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-networking-ext - - openstacksdk-functional-devstack-senlin From 1f21b864147ed345e243a62c851bfbcb5c5fc72b Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Mon, 20 May 2024 07:21:04 -0700 Subject: [PATCH 3493/3836] Add docs for common keystoneauth settings The `api_timeout` parameter appears undocumented. Add docs for it, as well as another sibling parameter, `collect_timing`. This is where these values are used: https://opendev.org/openstack/openstacksdk/src/commit/8091075e3b8a9bb160eaee8cd4b24fb3c6410642/openstack/config/cloud_region.py#L709-L710 Change-Id: I1fa4cf618f1f420e5614784a4922836de673b65e --- doc/source/user/config/configuration.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/doc/source/user/config/configuration.rst b/doc/source/user/config/configuration.rst index 72cfaa510..febee1486 100644 --- a/doc/source/user/config/configuration.rst +++ b/doc/source/user/config/configuration.rst @@ -139,6 +139,21 @@ as a result of a chosen plugin need to go into the auth dict. For password auth, this includes `auth_url`, `username` and `password` as well as anything related to domains, projects and trusts. +API Settings +------------ + +The following settings are passed to keystoneauth and are common to +all services. + +``api_timeout`` + A timeout for API requests. This should be a numerical value + indicating some amount (or fraction) of seconds or 0 for no + timeout. (optional, defaults to 0) + +``collect_timing`` + Whether or not to collect per-method timing information for each + API call. (optional, defaults to False) + Splitting Secrets ----------------- From 850c99cf8b7a7cb7ec5a88196b4170d1984e512d Mon Sep 17 00:00:00 2001 From: Rajat Dhasmana Date: Mon, 3 Jun 2024 15:14:44 +0530 Subject: [PATCH 3494/3836] [func test] Fix race between attachment delete and server delete Recently openstacksdk functional test, test_volume_attachment, started failing frequently. It mostly failed during the tearDown step trying to delete the volume as the volume delete was already issued by server delete (which it shouldn't be). Looking into the issue, I found out the problem to be in a race between the BDM record of instance being deleted (during volume attachment delete) and trying to delete the server. The sequence of operations that trigger this issue are: 1. Delete volume attachment 2. Wait for volume to become available 3. Delete server In step (2), nova sends a request to Cinder to delete the volume attachment[1], making the volume in available state[2], BUT the operation is still ongoing on nova side to delete the BDM record[3]. Hence we end up in a race, where nova is trying to delete the BDM record and we issue a server delete (overlapping request), which in turn consumes that BDM record and sends request to (which it shouldn't): 1. delete attachment (which is already deleted, hence returns 404) 2. delete volume Later when the functional test issue another request to delete the volume, we fail since the volume is already in the process of being deleted (by the server delete operation -- delete_on_termination is set to true). This analysis can yield a number of fixes in nova and cinder, namely: 1. Nova to prevent the race of BDM being deleted and being used at the same time. 2. Cinder to detect the volume being deleted and return success for subsequent delete requests (and not fail with 400 BadRequest). This patch focuses on fixing this on the SDK side where the flow of operations happens too fast triggering this race condition. We introduce a wait mechanism to wait for the VolumeAttachment resource to be deleted and later verify that the number of attachments for the server to be 0 before moving to the tearDown that deletes the server and the volume. there is a 1 second gap race happening which can be seen here: 1. server delete starting at 17:13:49 2024-06-05 17:13:49,892 openstack.iterate_timeout ****Timeout is 300 --- wait is 2.0 --- start time is 1717607629.892198 ---- 2024-06-05 17:13:49,892 openstack.iterate_timeout $$$$ Count is 1 --- time difference is 299.99977254867554 2024-06-05 17:13:50,133 openstack.iterate_timeout Waiting 2.0 seconds 2. BDM being deleted at 17:13:50 (already used by server delete to do attachment and volume delete calls) *************************** 2. row *************************** created_at: 2024-06-05 17:13:11 ... deleted_at: 2024-06-05 17:13:50 ... device_name: /dev/vdb volume_id: c13a3070-c5ab-4c8a-bb7e-5c7527fdf0df attachment_id: a1280ca9-4f88-49f7-9ba2-1e796688ebcc instance_uuid: 98bc13b2-50fe-4681-b263-80abf08929ac ... [1] https://opendev.org/openstack/nova/src/commit/7dc4b1ea627d864a0ee2745cc9de4336fc0ba7b5/nova/virt/block_device.py#L553 [2] https://opendev.org/openstack/cinder/src/commit/9f1292ad066c2c69066e791f9f5b914bcf1c4425/cinder/volume/api.py#L2685 [3] https://opendev.org/openstack/nova/src/commit/7dc4b1ea627d864a0ee2745cc9de4336fc0ba7b5/nova/compute/manager.py#L7658-L7659 Closes-Bug: #2067869 Change-Id: Ia59df9640d778bec4b22e608d111f82b759ac610 --- openstack/resource.py | 6 ++++-- .../compute/v2/test_volume_attachment.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/openstack/resource.py b/openstack/resource.py index 60f377ecf..27db32702 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -2503,8 +2503,10 @@ def wait_for_delete(session, resource, interval, wait, callback=None): resource = resource.fetch(session, skip_cache=True) if not resource: return orig_resource - if resource.status.lower() == 'deleted': - return resource + # Some resources like VolumeAttachment don't have status field. + if hasattr(resource, 'status'): + if resource.status.lower() == 'deleted': + return resource except exceptions.NotFoundException: return orig_resource diff --git a/openstack/tests/functional/compute/v2/test_volume_attachment.py b/openstack/tests/functional/compute/v2/test_volume_attachment.py index c65da7696..1e357c855 100644 --- a/openstack/tests/functional/compute/v2/test_volume_attachment.py +++ b/openstack/tests/functional/compute/v2/test_volume_attachment.py @@ -137,3 +137,17 @@ def test_volume_attachment(self): status='available', wait=self._wait_for_timeout, ) + + # Wait for the attachment to be deleted. + # This is done to prevent a race between the BDM + # record being deleted and we trying to delete the server. + self.user_cloud.compute.wait_for_delete( + volume_attachment, + wait=self._wait_for_timeout, + ) + + # Verify the server doesn't have any volume attachment + volume_attachments = list( + self.user_cloud.compute.volume_attachments(self.server) + ) + self.assertEqual(0, len(volume_attachments)) From 916f9af658b8c0b4e47d1d6fb77ccb7207069ee7 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 8 May 2024 11:47:57 +0100 Subject: [PATCH 3495/3836] compute, block storage: Minor fixes to limits Add a proper RateLimit class for compute (even though this is no longer a thing starting in Newton) and use consistent naming across services. Change-Id: Ib8a926900dca77cf48492839664bf2c5c13aab70 Signed-off-by: Stephen Finucane --- .../resources/block_storage/v2/limits.rst | 6 ++-- .../resources/block_storage/v3/limits.rst | 8 ++--- openstack/block_storage/v2/_proxy.py | 6 ++-- openstack/block_storage/v2/limits.py | 6 +++- openstack/block_storage/v3/_proxy.py | 8 ++--- openstack/block_storage/v3/limits.py | 14 +++++--- openstack/cloud/_block_storage.py | 2 +- openstack/compute/v2/limits.py | 33 ++++++++++++++++--- .../unit/block_storage/v2/test_limits.py | 4 +-- .../tests/unit/block_storage/v2/test_proxy.py | 2 +- .../unit/block_storage/v3/test_limits.py | 4 +-- .../tests/unit/block_storage/v3/test_proxy.py | 2 +- .../tests/unit/compute/v2/test_limits.py | 18 ++++++---- 13 files changed, 75 insertions(+), 38 deletions(-) diff --git a/doc/source/user/resources/block_storage/v2/limits.rst b/doc/source/user/resources/block_storage/v2/limits.rst index ec6e8fd0a..37a925a72 100644 --- a/doc/source/user/resources/block_storage/v2/limits.rst +++ b/doc/source/user/resources/block_storage/v2/limits.rst @@ -12,12 +12,12 @@ The ``AbsoluteLimit`` class inherits from .. autoclass:: openstack.block_storage.v2.limits.AbsoluteLimit :members: -The Limit Class ---------------- +The Limits Class +---------------- The ``Limit`` class inherits from :class:`~openstack.resource.Resource`. -.. autoclass:: openstack.block_storage.v2.limits.Limit +.. autoclass:: openstack.block_storage.v2.limits.Limits :members: The RateLimit Class diff --git a/doc/source/user/resources/block_storage/v3/limits.rst b/doc/source/user/resources/block_storage/v3/limits.rst index 69a7d7a31..d31337325 100644 --- a/doc/source/user/resources/block_storage/v3/limits.rst +++ b/doc/source/user/resources/block_storage/v3/limits.rst @@ -12,12 +12,12 @@ The ``AbsoluteLimit`` class inherits from .. autoclass:: openstack.block_storage.v3.limits.AbsoluteLimit :members: -The Limit Class ---------------- +The Limits Class +---------------- -The ``Limit`` class inherits from :class:`~openstack.resource.Resource`. +The ``Limits`` class inherits from :class:`~openstack.resource.Resource`. -.. autoclass:: openstack.block_storage.v3.limits.Limit +.. autoclass:: openstack.block_storage.v3.limits.Limits :members: The RateLimit Class diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 6ac642480..46cb472ab 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -651,15 +651,15 @@ def get_limits(self, project=None): :param project: A project to get limits for. The value can be either the ID of a project or an :class:`~openstack.identity.v2.project.Project` instance. - :returns: A Limit object, including both + :returns: A Limits object, including both :class:`~openstack.block_storage.v2.limits.AbsoluteLimit` and :class:`~openstack.block_storage.v2.limits.RateLimit` - :rtype: :class:`~openstack.block_storage.v2.limits.Limit` + :rtype: :class:`~openstack.block_storage.v2.limits.Limits` """ params = {} if project: params['project_id'] = resource.Resource._get_id(project) - return self._get(_limits.Limit, requires_id=False, **params) + return self._get(_limits.Limits, requires_id=False, **params) # ====== CAPABILITIES ====== def get_capabilities(self, host): diff --git a/openstack/block_storage/v2/limits.py b/openstack/block_storage/v2/limits.py index 490e4e4b8..486a97d0d 100644 --- a/openstack/block_storage/v2/limits.py +++ b/openstack/block_storage/v2/limits.py @@ -67,7 +67,7 @@ class RateLimits(resource.Resource): uri = resource.Body("uri") -class Limit(resource.Resource): +class Limits(resource.Resource): resource_key = "limits" base_path = "/limits" @@ -80,3 +80,7 @@ class Limit(resource.Resource): #: Rate-limit volume copy bandwidth, used to mitigate #: slow down of data access from the instances. rate = resource.Body("rate", type=list, list_type=RateLimits) + + +# legacy alias +Limit = Limits diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index f3bfafaed..aede10aed 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -47,7 +47,7 @@ class Proxy(_base_proxy.BaseBlockStorageProxy): "group": _group.Group, "group_snapshot": _group_snapshot.GroupSnapshot, "group_type": _group_type.GroupType, - "limits": _limits.Limit, + "limits": _limits.Limits, "quota_set": _quota_set.QuotaSet, "resource_filter": _resource_filter.ResourceFilter, "snapshot": _snapshot.Snapshot, @@ -1282,15 +1282,15 @@ def get_limits(self, project=None): :param project: A project to get limits for. The value can be either the ID of a project or an :class:`~openstack.identity.v3.project.Project` instance. - :returns: A Limit object, including both + :returns: A Limits object, including both :class:`~openstack.block_storage.v3.limits.AbsoluteLimit` and :class:`~openstack.block_storage.v3.limits.RateLimit` - :rtype: :class:`~openstack.block_storage.v3.limits.Limit` + :rtype: :class:`~openstack.block_storage.v3.limits.Limits` """ params = {} if project: params['project_id'] = resource.Resource._get_id(project) - return self._get(_limits.Limit, requires_id=False, **params) + return self._get(_limits.Limits, requires_id=False, **params) # ====== CAPABILITIES ====== def get_capabilities(self, host): diff --git a/openstack/block_storage/v3/limits.py b/openstack/block_storage/v3/limits.py index 490e4e4b8..fe726a6e6 100644 --- a/openstack/block_storage/v3/limits.py +++ b/openstack/block_storage/v3/limits.py @@ -14,7 +14,7 @@ class AbsoluteLimit(resource.Resource): - #: Properties + # Properties #: The maximum total amount of backups, in gibibytes (GiB). max_total_backup_gigabytes = resource.Body( "maxTotalBackupGigabytes", type=int @@ -44,7 +44,7 @@ class AbsoluteLimit(resource.Resource): class RateLimit(resource.Resource): - #: Properties + # Properties #: Rate limits next availabe time. next_available = resource.Body("next-available") #: Integer for rate limits remaining. @@ -58,7 +58,7 @@ class RateLimit(resource.Resource): class RateLimits(resource.Resource): - #: Properties + # Properties #: A list of the specific limits that apply to the ``regex`` and ``uri``. limits = resource.Body("limit", type=list, list_type=RateLimit) #: A regex representing which routes this rate limit applies to. @@ -67,16 +67,20 @@ class RateLimits(resource.Resource): uri = resource.Body("uri") -class Limit(resource.Resource): +class Limits(resource.Resource): resource_key = "limits" base_path = "/limits" # capabilities allow_fetch = True - #: Properties + # Properties #: An absolute limits object. absolute = resource.Body("absolute", type=AbsoluteLimit) #: Rate-limit volume copy bandwidth, used to mitigate #: slow down of data access from the instances. rate = resource.Body("rate", type=list, list_type=RateLimits) + + +# Legacy alias +Limit = Limits diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index 23acfcf63..2301ea186 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -269,7 +269,7 @@ def get_volume_limits(self, name_or_id=None): :param name_or_id: (optional) Project name or ID to get limits for if different from the current project - :returns: The volume ``Limit`` object if found, else None. + :returns: The volume ``Limits`` object if found, else None. """ params = {} if name_or_id: diff --git a/openstack/compute/v2/limits.py b/openstack/compute/v2/limits.py index 897e86403..8ff7838a6 100644 --- a/openstack/compute/v2/limits.py +++ b/openstack/compute/v2/limits.py @@ -16,6 +16,7 @@ class AbsoluteLimits(resource.Resource): _max_microversion = '2.57' + # Properties #: The number of key-value pairs that can be set as image metadata. image_meta = resource.Body("maxImageMeta", aka="max_image_meta") #: The maximum number of personality contents that can be supplied. @@ -73,10 +74,23 @@ class AbsoluteLimits(resource.Resource): class RateLimit(resource.Resource): - # TODO(mordred) Make a resource type for the contents of limit and add - # it to list_type here. + # Properties + #: Rate limits next availabe time. + next_available = resource.Body("next-available") + #: Integer for rate limits remaining. + remaining = resource.Body("remaining", type=int) + #: Unit of measurement for the value parameter. + unit = resource.Body("unit") + #: Integer number of requests which can be made. + value = resource.Body("value", type=int) + #: An HTTP verb (POST, PUT, etc.). + verb = resource.Body("verb") + + +class RateLimits(resource.Resource): + # Properties #: A list of the specific limits that apply to the ``regex`` and ``uri``. - limits = resource.Body("limit", type=list) + limits = resource.Body("limit", type=list, list_type=RateLimit) #: A regex representing which routes this rate limit applies to. regex = resource.Body("regex") #: A URI representing which routes this rate limit applies to. @@ -89,10 +103,19 @@ class Limits(resource.Resource): allow_fetch = True - _query_mapping = resource.QueryParameters('tenant_id') + _query_mapping = resource.QueryParameters( + 'tenant_id', + 'reserved', + project_id='tenant_id', + ) + # Properties + #: An absolute limits object. absolute = resource.Body("absolute", type=AbsoluteLimits) - rate = resource.Body("rate", type=list, list_type=RateLimit) + #: Rate-limit compute resources. This is only populated when using the + #: legacy v2 API which was removed in Nova 14.0.0 (Newton). In v2.1 it will + #: always be an empty list. + rate = resource.Body("rate", type=list, list_type=RateLimits) def fetch( self, diff --git a/openstack/tests/unit/block_storage/v2/test_limits.py b/openstack/tests/unit/block_storage/v2/test_limits.py index ec74c3fc9..bb27c970f 100644 --- a/openstack/tests/unit/block_storage/v2/test_limits.py +++ b/openstack/tests/unit/block_storage/v2/test_limits.py @@ -146,7 +146,7 @@ def test_make_rate_limits(self): class TestLimit(base.TestCase): def test_basic(self): - limit_resource = limits.Limit() + limit_resource = limits.Limits() self.assertEqual('limits', limit_resource.resource_key) self.assertEqual('/limits', limit_resource.base_path) self.assertTrue(limit_resource.allow_fetch) @@ -201,6 +201,6 @@ def _test_rate_limits(self, expected, actual): self._test_rate_limit(expected[0]['limit'], actual[0].limits) def test_make_limit(self): - limit_resource = limits.Limit(**LIMIT) + limit_resource = limits.Limits(**LIMIT) self._test_rate_limits(LIMIT['rate'], limit_resource.rate) self._test_absolute_limit(LIMIT['absolute'], limit_resource.absolute) diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index 63ac98588..9fa293079 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -321,7 +321,7 @@ class TestLimit(TestVolumeProxy): def test_limits_get(self): self.verify_get( self.proxy.get_limits, - limits.Limit, + limits.Limits, method_args=[], expected_kwargs={'requires_id': False}, ) diff --git a/openstack/tests/unit/block_storage/v3/test_limits.py b/openstack/tests/unit/block_storage/v3/test_limits.py index 2550092dd..b0638cb62 100644 --- a/openstack/tests/unit/block_storage/v3/test_limits.py +++ b/openstack/tests/unit/block_storage/v3/test_limits.py @@ -146,7 +146,7 @@ def test_make_rate_limits(self): class TestLimit(base.TestCase): def test_basic(self): - limit_resource = limits.Limit() + limit_resource = limits.Limits() self.assertEqual('limits', limit_resource.resource_key) self.assertEqual('/limits', limit_resource.base_path) self.assertTrue(limit_resource.allow_fetch) @@ -201,6 +201,6 @@ def _test_rate_limits(self, expected, actual): self._test_rate_limit(expected[0]['limit'], actual[0].limits) def test_make_limit(self): - limit_resource = limits.Limit(**LIMIT) + limit_resource = limits.Limits(**LIMIT) self._test_rate_limits(LIMIT['rate'], limit_resource.rate) self._test_absolute_limit(LIMIT['absolute'], limit_resource.absolute) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index cc581bf9d..6882a75f2 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -137,7 +137,7 @@ class TestLimit(TestVolumeProxy): def test_limits_get(self): self.verify_get( self.proxy.get_limits, - limits.Limit, + limits.Limits, method_args=[], expected_kwargs={'requires_id': False}, ) diff --git a/openstack/tests/unit/compute/v2/test_limits.py b/openstack/tests/unit/compute/v2/test_limits.py index 4971dad1d..b58af6842 100644 --- a/openstack/tests/unit/compute/v2/test_limits.py +++ b/openstack/tests/unit/compute/v2/test_limits.py @@ -107,9 +107,9 @@ def test_make_it(self): ) -class TestRateLimit(base.TestCase): +class TestRateLimits(base.TestCase): def test_basic(self): - sot = limits.RateLimit() + sot = limits.RateLimits() self.assertIsNone(sot.resource_key) self.assertIsNone(sot.resources_key) self.assertEqual("", sot.base_path) @@ -120,10 +120,10 @@ def test_basic(self): self.assertFalse(sot.allow_list) def test_make_it(self): - sot = limits.RateLimit(**RATE_LIMIT) + sot = limits.RateLimits(**RATE_LIMIT) self.assertEqual(RATE_LIMIT["regex"], sot.regex) self.assertEqual(RATE_LIMIT["uri"], sot.uri) - self.assertEqual(RATE_LIMIT["limit"], sot.limits) + self.assertIsInstance(sot.limits[0], limits.RateLimit) class TestLimits(base.TestCase): @@ -137,7 +137,13 @@ def test_basic(self): self.assertFalse(sot.allow_delete) self.assertFalse(sot.allow_list) self.assertDictEqual( - {'limit': 'limit', 'marker': 'marker', 'tenant_id': 'tenant_id'}, + { + 'limit': 'limit', + 'marker': 'marker', + 'tenant_id': 'tenant_id', + 'project_id': 'tenant_id', + 'reserved': 'reserved', + }, sot._query_mapping._mapping, ) @@ -211,7 +217,7 @@ def test_get(self): self.assertEqual(RATE_LIMIT["uri"], sot.rate[0].uri) self.assertEqual(RATE_LIMIT["regex"], sot.rate[0].regex) - self.assertEqual(RATE_LIMIT["limit"], sot.rate[0].limits) + self.assertIsInstance(sot.rate[0].limits[0], limits.RateLimit) dsot = sot.to_dict() From 67cd66688d1a657f1daf6dd26ee49c959b91beed Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 9 May 2024 12:51:00 +0100 Subject: [PATCH 3496/3836] compute: Add Server.clear_password action Change-Id: I5960605944fef2a300d6a3a9ff723a701e32cb64 Signed-off-by: Stephen Finucane --- openstack/compute/v2/_proxy.py | 11 +++++++ openstack/compute/v2/server.py | 26 ++++++++++++++--- openstack/tests/unit/compute/v2/test_proxy.py | 24 +++++++++++++++ .../tests/unit/compute/v2/test_server.py | 29 +++++++++++++++++++ openstack/tests/unit/fakes.py | 19 ++++++++++++ ...erver-clear-password-256e269223453bd7.yaml | 5 ++++ 6 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/add-server-clear-password-256e269223453bd7.yaml diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 4ec0535f4..f7c7ab7bd 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -851,6 +851,17 @@ def get_server_password(self, server): server = self._get_resource(_server.Server, server) return server.get_password(self) + def clear_server_password(self, server): + """Clear the administrator password + + :param server: Either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance. + + :returns: None + """ + server = self._get_resource(_server.Server, server) + server.clear_password(self) + def reset_server_state(self, server, state): """Reset the state of server diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 078d75d8b..6b6a1508c 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -323,7 +323,7 @@ def _action(self, session, body, microversion=None): exceptions.raise_from_response(response) return response - def change_password(self, session, password): + def change_password(self, session, password, *, microversion=None): """Change the administrator password to the given password. :param session: The session to use for making this request. @@ -331,22 +331,40 @@ def change_password(self, session, password): :returns: None """ body = {'changePassword': {'adminPass': password}} - self._action(session, body) + self._action(session, body, microversion=microversion) - def get_password(self, session): + def get_password(self, session, *, microversion=None): """Get the encrypted administrator password. :param session: The session to use for making this request. :returns: The encrypted administrator password. """ url = utils.urljoin(Server.base_path, self.id, 'os-server-password') + if microversion is None: + microversion = self._get_microversion(session, action='commit') - response = session.get(url) + response = session.get(url, microversion=microversion) exceptions.raise_from_response(response) data = response.json() return data.get('password') + def clear_password(self, session, *, microversion=None): + """Clear the administrator password. + + This removes the password from the database. It does not actually + change the server password. + + :param session: The session to use for making this request. + :returns: None + """ + url = utils.urljoin(Server.base_path, self.id, 'os-server-password') + if microversion is None: + microversion = self._get_microversion(session, action='commit') + + response = session.delete(url, microversion=microversion) + exceptions.raise_from_response(response) + def reboot(self, session, reboot_type): """Reboot server where reboot_type might be 'SOFT' or 'HARD'. diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 378935f84..a5aa9805a 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -961,6 +961,30 @@ def test_servers_not_detailed(self): def test_server_update(self): self.verify_update(self.proxy.update_server, server.Server) + def test_server_change_password(self): + self._verify( + "openstack.compute.v2.server.Server.change_password", + self.proxy.change_server_password, + method_args=["value", "password"], + expected_args=[self.proxy, "password"], + ) + + def test_server_get_password(self): + self._verify( + "openstack.compute.v2.server.Server.get_password", + self.proxy.get_server_password, + method_args=["value"], + expected_args=[self.proxy], + ) + + def test_server_clear_password(self): + self._verify( + "openstack.compute.v2.server.Server.clear_password", + self.proxy.clear_server_password, + method_args=["value"], + expected_args=[self.proxy], + ) + def test_server_wait_for(self): value = server.Server(id='1234') self.verify_wait_for_status( diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index d996638ef..e04008a77 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -10,12 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. +import http from unittest import mock from openstack.compute.v2 import flavor from openstack.compute.v2 import server from openstack.image.v2 import image from openstack.tests.unit import base +from openstack.tests.unit import fakes IDENTIFIER = 'IDENTIFIER' EXAMPLE = { @@ -317,6 +319,33 @@ def test_change_password(self): microversion=self.sess.default_microversion, ) + def test_get_password(self): + sot = server.Server(**EXAMPLE) + self.sess.get.return_value = fakes.FakeResponse( + data={'password': 'foo'} + ) + + result = sot.get_password(self.sess) + self.assertEqual('foo', result) + + url = 'servers/IDENTIFIER/os-server-password' + self.sess.get.assert_called_with( + url, microversion=self.sess.default_microversion + ) + + def test_clear_password(self): + sot = server.Server(**EXAMPLE) + self.sess.delete.return_value = fakes.FakeResponse( + status_code=http.HTTPStatus.NO_CONTENT, + ) + + self.assertIsNone(sot.clear_password(self.sess)) + + url = 'servers/IDENTIFIER/os-server-password' + self.sess.delete.assert_called_with( + url, microversion=self.sess.default_microversion + ) + def test_reboot(self): sot = server.Server(**EXAMPLE) diff --git a/openstack/tests/unit/fakes.py b/openstack/tests/unit/fakes.py index 535ef5505..50c8d4831 100644 --- a/openstack/tests/unit/fakes.py +++ b/openstack/tests/unit/fakes.py @@ -13,8 +13,11 @@ # License for the specific language governing permissions and limitations # under the License. +import json from unittest import mock +import requests + class FakeTransport(mock.Mock): RESPONSE = mock.Mock('200 OK') @@ -35,3 +38,19 @@ def __init__(self): self.get_token.return_value = self.TOKEN self.get_endpoint = mock.Mock() self.get_endpoint.return_value = self.ENDPOINT + + +class FakeResponse(requests.Response): + def __init__( + self, headers=None, status_code=200, data=None, encoding=None + ): + super().__init__() + + headers = headers or {} + + self.status_code = status_code + + self.headers.update(headers) + self._content = json.dumps(data) + if not isinstance(self._content, bytes): + self._content = self._content.encode() diff --git a/releasenotes/notes/add-server-clear-password-256e269223453bd7.yaml b/releasenotes/notes/add-server-clear-password-256e269223453bd7.yaml new file mode 100644 index 000000000..70f07cabe --- /dev/null +++ b/releasenotes/notes/add-server-clear-password-256e269223453bd7.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + The ``Server.clear_password`` and equivalent ``clear_server_password`` + proxy method have been added. From 6d98de4f331b3ebbbe735689eb077631d3881493 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 9 May 2024 12:51:20 +0100 Subject: [PATCH 3497/3836] compute: Add server tag proxy methods To add a tag, remove a tag, and remove all tags. Change-Id: I4c7533007bcc85e713c532830b979b9d3fedb612 Signed-off-by: Stephen Finucane --- doc/source/user/proxies/compute.rst | 7 ++++ openstack/compute/v2/_proxy.py | 33 +++++++++++++++++++ openstack/tests/unit/compute/v2/test_proxy.py | 24 ++++++++++++++ ...er-tag-proxy-methods-c791a36d8d4d85f6.yaml | 8 +++++ 4 files changed, 72 insertions(+) create mode 100644 releasenotes/notes/add-server-tag-proxy-methods-c791a36d8d4d85f6.yaml diff --git a/doc/source/user/proxies/compute.rst b/doc/source/user/proxies/compute.rst index b2b693bbf..681db0b10 100644 --- a/doc/source/user/proxies/compute.rst +++ b/doc/source/user/proxies/compute.rst @@ -121,6 +121,13 @@ Server Interface Operations :members: create_server_interface, delete_server_interface, get_server_interface, server_interfaces, +Server Tag Operations +^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.compute.v2._proxy.Proxy + :noindex: + :members: add_tag_to_server, remove_tag_from_server, remove_tags_from_server + Availability Zone Operations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index f7c7ab7bd..744e7cd21 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -1195,6 +1195,39 @@ def trigger_server_crash_dump(self, server): server = self._get_resource(_server.Server, server) server.trigger_crash_dump(self) + def add_tag_to_server(self, server, tag): + """Add a tag to a server. + + :param server: Either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance. + :param tag: The tag to add. + :returns: None + """ + server = self._get_resource(_server.Server, server) + server.add_tag(self, tag) + + def remove_tag_from_server(self, server, tag): + """Remove a tag from a server. + + :param server: Either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance. + :param tag: The tag to remove. + :returns: None + """ + server = self._get_resource(_server.Server, server) + server.remove_tag(self, tag) + + def remove_tags_from_server(self, server): + """Remove all tags from a server. + + :param server: Either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance. + :param tag: The tag to remove. + :returns: None + """ + server = self._get_resource(_server.Server, server) + server.remove_all_tags(self) + # ========== Server security groups ========== def fetch_server_security_groups(self, server): diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index a5aa9805a..e7a28d314 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -1277,6 +1277,30 @@ def test_server_trigger_dump(self): expected_args=[self.proxy], ) + def test_server_add_tag(self): + self._verify( + "openstack.compute.v2.server.Server.add_tag", + self.proxy.add_tag_to_server, + method_args=["value", "tag"], + expected_args=[self.proxy, "tag"], + ) + + def test_server_remove_tag(self): + self._verify( + "openstack.compute.v2.server.Server.remove_tag", + self.proxy.remove_tag_from_server, + method_args=["value", "tag"], + expected_args=[self.proxy, "tag"], + ) + + def test_server_remove_tags(self): + self._verify( + "openstack.compute.v2.server.Server.remove_all_tags", + self.proxy.remove_tags_from_server, + method_args=["value"], + expected_args=[self.proxy], + ) + def test_get_server_output(self): self._verify( "openstack.compute.v2.server.Server.get_console_output", diff --git a/releasenotes/notes/add-server-tag-proxy-methods-c791a36d8d4d85f6.yaml b/releasenotes/notes/add-server-tag-proxy-methods-c791a36d8d4d85f6.yaml new file mode 100644 index 000000000..f502d6102 --- /dev/null +++ b/releasenotes/notes/add-server-tag-proxy-methods-c791a36d8d4d85f6.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + The following new compute proxy methods have been added: + + - ``add_tag_to_server`` + - ``remove_tag_from_server`` + - ``remove_tags_from_server`` From 0bd02b3a0462fe287595f893d395b5f319f0003e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 9 May 2024 15:43:33 +0100 Subject: [PATCH 3498/3836] compute: Add support for legacy 'onSharedStorage' param This is supported by early revisions of the server evacuate action. Change-Id: I452fa9bb7077b3a1ce66552bbbf68ffd6702d1e2 Signed-off-by: Stephen Finucane --- openstack/compute/v2/_proxy.py | 20 +++++++++++++++++-- openstack/compute/v2/server.py | 13 +++++++++++- openstack/tests/unit/compute/v2/test_proxy.py | 9 ++++++++- .../tests/unit/compute/v2/test_server.py | 7 ++++++- 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 744e7cd21..fd5d8f5c8 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -1083,7 +1083,15 @@ def unrescue_server(self, server): server = self._get_resource(_server.Server, server) server.unrescue(self) - def evacuate_server(self, server, host=None, admin_pass=None, force=None): + def evacuate_server( + self, + server, + host=None, + admin_pass=None, + force=None, + *, + on_shared_storage=None, + ): """Evacuates a server from a failed host to a new host. :param server: Either the ID of a server or a @@ -1095,10 +1103,18 @@ def evacuate_server(self, server, host=None, admin_pass=None, force=None): :param force: Force an evacuation by not verifying the provided destination host by the scheduler. (New in API version 2.29). + :param on_shared_storage: Whether the host is using shared storage. + (Optional) (Only supported before API version 2.14) :returns: None """ server = self._get_resource(_server.Server, server) - server.evacuate(self, host=host, admin_pass=admin_pass, force=force) + server.evacuate( + self, + host=host, + admin_pass=admin_pass, + force=force, + on_shared_storage=on_shared_storage, + ) def start_server(self, server): """Starts a stopped server and changes its state to ``ACTIVE``. diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 34e51b026..c66432f43 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -730,7 +730,14 @@ def unrescue(self, session): body = {"unrescue": None} self._action(session, body) - def evacuate(self, session, host=None, admin_pass=None, force=None): + def evacuate( + self, + session, + host=None, + admin_pass=None, + force=None, + on_shared_storage=None, + ): """Evacuate the server. :param session: The session to use for making this request. @@ -738,6 +745,8 @@ def evacuate(self, session, host=None, admin_pass=None, force=None): :param admin_pass: The admin password to set on the evacuated instance. (Optional) :param force: Whether to force evacuation. + :param on_shared_storage: Whether the host is using shared storage. + (Optional) (Only supported before microversion 2.14) :returns: None """ body: ty.Dict[str, ty.Any] = {"evacuate": {}} @@ -747,6 +756,8 @@ def evacuate(self, session, host=None, admin_pass=None, force=None): body["evacuate"]["adminPass"] = admin_pass if force is not None: body["evacuate"]["force"] = force + if on_shared_storage is not None: + body["evacuate"]["onSharedStorage"] = on_shared_storage self._action(session, body) def start(self, session): diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index e7a28d314..5d2815953 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -1205,7 +1205,12 @@ def test_server_evacuate(self): self.proxy.evacuate_server, method_args=["value"], expected_args=[self.proxy], - expected_kwargs={"host": None, "admin_pass": None, "force": None}, + expected_kwargs={ + "host": None, + "admin_pass": None, + "force": None, + "on_shared_storage": None, + }, ) def test_server_evacuate_with_options(self): @@ -1213,11 +1218,13 @@ def test_server_evacuate_with_options(self): "openstack.compute.v2.server.Server.evacuate", self.proxy.evacuate_server, method_args=["value", 'HOST2', 'NEW_PASS', True], + method_kwargs={'on_shared_storage': False}, expected_args=[self.proxy], expected_kwargs={ "host": "HOST2", "admin_pass": 'NEW_PASS', "force": True, + "on_shared_storage": False, }, ) diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index ca5074b90..af0a02841 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -980,7 +980,11 @@ def test_evacuate_with_options(self): sot = server.Server(**EXAMPLE) res = sot.evacuate( - self.sess, host='HOST2', admin_pass='NEW_PASS', force=True + self.sess, + host='HOST2', + admin_pass='NEW_PASS', + force=True, + on_shared_storage=False, ) self.assertIsNone(res) @@ -990,6 +994,7 @@ def test_evacuate_with_options(self): 'host': 'HOST2', 'adminPass': 'NEW_PASS', 'force': True, + 'onSharedStorage': False, } } headers = {'Accept': ''} From dbf5c975ceeb349a9c5b5762bd4e5b45a0fc2bb5 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 9 May 2024 13:33:03 +0100 Subject: [PATCH 3499/3836] compute: Add additional options to Server.rebuild We also start using a sentinel value here since None is a useful value for unsetting things. Change-Id: I3bc3150877c6c00aa9ec4355104308d7755aa1d4 Signed-off-by: Stephen Finucane --- openstack/compute/v2/server.py | 57 +++++++++++++------ .../tests/unit/compute/v2/test_server.py | 56 ++++++++++++++++-- 2 files changed, 92 insertions(+), 21 deletions(-) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 6b6a1508c..34e51b026 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -22,6 +22,15 @@ from openstack import utils +# Workaround Python's lack of an undefined sentinel +# https://python-patterns.guide/python/sentinel-object/ +class Unset: + def __bool__(self) -> ty.Literal[False]: + return False + + +UNSET: Unset = Unset() + CONSOLE_TYPE_ACTION_MAPPING = { 'novnc': 'os-getVNCConsole', 'xvpvnc': 'os-getVNCConsole', @@ -388,14 +397,17 @@ def rebuild( self, session, image, - name=None, - admin_password=None, - preserve_ephemeral=None, - access_ipv4=None, - access_ipv6=None, - metadata=None, - user_data=None, - key_name=None, + name=UNSET, + admin_password=UNSET, + preserve_ephemeral=UNSET, + access_ipv4=UNSET, + access_ipv6=UNSET, + metadata=UNSET, + user_data=UNSET, + key_name=UNSET, + description=UNSET, + trusted_image_certificates=UNSET, + hostname=UNSET, ): """Rebuild the server with the given arguments. @@ -414,25 +426,38 @@ def rebuild( :param metadata: Metadata to set on the updated server. (Optional) :param user_data: User data to set on the updated server. (Optional) :param key_name: A key name to set on the updated server. (Optional) + :param description: The description to set on the updated server. + (Optional) (Requires API microversion 2.19) + :param trusted_image_certificates: The trusted image certificates to + set on the updated server. (Optional) (Requires API microversion + 2.78) + :param hostname: The hostname to set on the updated server. (Optional) + (Requires API microversion 2.90) :returns: The updated server. """ action = {'imageRef': resource.Resource._get_id(image)} - if preserve_ephemeral is not None: + if preserve_ephemeral is not UNSET: action['preserve_ephemeral'] = preserve_ephemeral - if name is not None: + if name is not UNSET: action['name'] = name - if admin_password is not None: + if admin_password is not UNSET: action['adminPass'] = admin_password - if access_ipv4 is not None: + if access_ipv4 is not UNSET: action['accessIPv4'] = access_ipv4 - if access_ipv6 is not None: + if access_ipv6 is not UNSET: action['accessIPv6'] = access_ipv6 - if metadata is not None: + if metadata is not UNSET: action['metadata'] = metadata - if user_data is not None: + if user_data is not UNSET: action['user_data'] = user_data - if key_name is not None: + if key_name is not UNSET: action['key_name'] = key_name + if description is not UNSET: + action['description'] = description + if trusted_image_certificates is not UNSET: + action['trusted_image_certificates'] = trusted_image_certificates + if hostname is not UNSET: + action['hostname'] = hostname body = {'rebuild': action} response = self._action(session, body) diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index e04008a77..ca5074b90 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -383,14 +383,18 @@ def test_rebuild(self): result = sot.rebuild( self.sess, + '123', name='noo', admin_password='seekr3t', - image='http://image/1', + preserve_ephemeral=False, access_ipv4="12.34.56.78", access_ipv6="fe80::100", metadata={"meta var": "meta val"}, user_data="ZWNobyAiaGVsbG8gd29ybGQi", - preserve_ephemeral=False, + key_name='my-ecdsa-key', + description='an updated description', + trusted_image_certificates=['foo'], + hostname='new-hostname', ) self.assertIsInstance(result, server.Server) @@ -399,13 +403,17 @@ def test_rebuild(self): body = { "rebuild": { "name": "noo", - "imageRef": "http://image/1", + "imageRef": "123", "adminPass": "seekr3t", "accessIPv4": "12.34.56.78", "accessIPv6": "fe80::100", "metadata": {"meta var": "meta val"}, "user_data": "ZWNobyAiaGVsbG8gd29ybGQi", "preserve_ephemeral": False, + "key_name": 'my-ecdsa-key', + "description": 'an updated description', + "trusted_image_certificates": ['foo'], + "hostname": "new-hostname", } } headers = {'Accept': ''} @@ -423,9 +431,9 @@ def test_rebuild_minimal(self): result = sot.rebuild( self.sess, + '123', name='nootoo', admin_password='seekr3two', - image='http://image/2', ) self.assertIsInstance(result, server.Server) @@ -434,7 +442,7 @@ def test_rebuild_minimal(self): body = { "rebuild": { "name": "nootoo", - "imageRef": "http://image/2", + "imageRef": "123", "adminPass": "seekr3two", } } @@ -446,6 +454,44 @@ def test_rebuild_minimal(self): microversion=self.sess.default_microversion, ) + def test_rebuild_none_values(self): + sot = server.Server(**EXAMPLE) + # Let the translate pass through, that portion is tested elsewhere + sot._translate_response = lambda arg: arg + + result = sot.rebuild( + self.sess, + '123', + admin_password=None, + access_ipv4=None, + access_ipv6=None, + metadata=None, + user_data=None, + description=None, + ) + + self.assertIsInstance(result, server.Server) + + url = 'servers/IDENTIFIER/action' + body = { + "rebuild": { + "imageRef": "123", + "adminPass": None, + "accessIPv4": None, + "accessIPv6": None, + "metadata": None, + "user_data": None, + "description": None, + } + } + headers = {'Accept': ''} + self.sess.post.assert_called_with( + url, + json=body, + headers=headers, + microversion=self.sess.default_microversion, + ) + def test_resize(self): sot = server.Server(**EXAMPLE) From 0f311ff3e2e57bf3659cef77e98551b6c0c7e3c9 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 9 May 2024 17:51:53 +0100 Subject: [PATCH 3500/3836] compute: Add additional server create parameters Change-Id: Icfb8cbefca8bd7e246137c7f1fae94b81148fe2e Signed-off-by: Stephen Finucane --- openstack/compute/v2/server.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index c66432f43..132ed81b1 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -156,6 +156,8 @@ class Server(resource.Resource, metadata.MetadataMixin, tag.TagMixin): #: A fault object. Only available when the server status #: is ERROR or DELETED and a fault occurred. fault = resource.Body('fault') + #: The host to boot the server on. + host = resource.Body('host') #: The host status. host_status = resource.Body('host_status') #: The hostname set on the instance when it is booted. @@ -202,6 +204,10 @@ class Server(resource.Resource, metadata.MetadataMixin, tag.TagMixin): #: networks parameter, the server attaches to the only network #: created for the current tenant. networks = resource.Body('networks') + #: Personality files. This should be a list of dicts with each dict + #: containing a file name ('name') and a base64-encoded file contents + #: ('contents') + personality = resource.Body('personality', type=list) #: The availability zone requested during server creation OR pinned #: availability zone, which is configured using default_schedule_zone #: config option. From 4275dfc53c745e584103b7d5ce1c8fb0c5cdc46f Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 16 May 2024 17:13:00 +0100 Subject: [PATCH 3501/3836] compute, block storage: Add support for quota class sets Change-Id: I2d0b9a900d60d666ba07b759eccdb9a8e1c164bb Signed-off-by: Stephen Finucane --- doc/source/user/proxies/block_storage_v2.rst | 8 +++ doc/source/user/proxies/block_storage_v3.rst | 21 ++++--- doc/source/user/proxies/compute.rst | 7 +++ openstack/block_storage/v2/_proxy.py | 38 ++++++++++++ openstack/block_storage/v2/quota_class_set.py | 39 ++++++++++++ openstack/block_storage/v3/_proxy.py | 38 ++++++++++++ openstack/block_storage/v3/quota_class_set.py | 39 ++++++++++++ openstack/compute/v2/_proxy.py | 40 ++++++++++++ openstack/compute/v2/quota_class_set.py | 61 +++++++++++++++++++ openstack/compute/v2/quota_set.py | 1 + .../tests/unit/block_storage/v2/test_proxy.py | 27 ++++++-- .../tests/unit/block_storage/v3/test_proxy.py | 27 ++++++-- openstack/tests/unit/compute/v2/test_proxy.py | 27 ++++++-- 13 files changed, 348 insertions(+), 25 deletions(-) create mode 100644 openstack/block_storage/v2/quota_class_set.py create mode 100644 openstack/block_storage/v3/quota_class_set.py create mode 100644 openstack/compute/v2/quota_class_set.py diff --git a/doc/source/user/proxies/block_storage_v2.rst b/doc/source/user/proxies/block_storage_v2.rst index d15c33b15..511dbdd41 100644 --- a/doc/source/user/proxies/block_storage_v2.rst +++ b/doc/source/user/proxies/block_storage_v2.rst @@ -67,8 +67,16 @@ Stats Operations :noindex: :members: backend_pools +QuotaClassSet Operations +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v2._proxy.Proxy + :noindex: + :members: get_quota_class_set, update_quota_class_set + QuotaSet Operations ^^^^^^^^^^^^^^^^^^^ + .. autoclass:: openstack.block_storage.v2._proxy.Proxy :noindex: :members: get_quota_set, get_quota_set_defaults, diff --git a/doc/source/user/proxies/block_storage_v3.rst b/doc/source/user/proxies/block_storage_v3.rst index 73a98623d..61657eb34 100644 --- a/doc/source/user/proxies/block_storage_v3.rst +++ b/doc/source/user/proxies/block_storage_v3.rst @@ -129,20 +129,20 @@ Stats Operations :noindex: :members: backend_pools -QuotaSet Operations -^^^^^^^^^^^^^^^^^^^ +QuotaClassSet Operations +^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.block_storage.v3._proxy.Proxy :noindex: - :members: get_quota_set, get_quota_set_defaults, - revert_quota_set, update_quota_set + :members: get_quota_class_set, update_quota_class_set -Helpers -^^^^^^^ +QuotaSet Operations +^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.block_storage.v3._proxy.Proxy :noindex: - :members: wait_for_status, wait_for_delete + :members: get_quota_set, get_quota_set_defaults, + revert_quota_set, update_quota_set BlockStorageSummary Operations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -166,3 +166,10 @@ Transfer Operations :noindex: :members: create_transfer, delete_transfer, find_transfer, get_transfer, transfers, accept_transfer + +Helpers +^^^^^^^ + +.. autoclass:: openstack.block_storage.v3._proxy.Proxy + :noindex: + :members: wait_for_status, wait_for_delete diff --git a/doc/source/user/proxies/compute.rst b/doc/source/user/proxies/compute.rst index 681db0b10..eec5c8f2a 100644 --- a/doc/source/user/proxies/compute.rst +++ b/doc/source/user/proxies/compute.rst @@ -157,6 +157,13 @@ Extension Operations :noindex: :members: find_extension, extensions +QuotaClassSet Operations +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.compute.v2._proxy.Proxy + :noindex: + :members: get_quota_class_set, update_quota_class_set + QuotaSet Operations ^^^^^^^^^^^^^^^^^^^ diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 46cb472ab..b5c57b109 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -15,6 +15,7 @@ from openstack.block_storage.v2 import capabilities as _capabilities from openstack.block_storage.v2 import extension as _extension from openstack.block_storage.v2 import limits as _limits +from openstack.block_storage.v2 import quota_class_set as _quota_class_set from openstack.block_storage.v2 import quota_set as _quota_set from openstack.block_storage.v2 import snapshot as _snapshot from openstack.block_storage.v2 import stats as _stats @@ -674,6 +675,43 @@ def get_capabilities(self, host): """ return self._get(_capabilities.Capabilities, host) + # ====== QUOTA CLASS SETS ====== + def get_quota_class_set(self, quota_class_set='default'): + """Get a single quota class set + + Only one quota class is permitted, ``default``. + + :param quota_class_set: The value can be the ID of a quota class set + (only ``default`` is supported) or a + :class:`~openstack.block_storage.v2.quota_class_set.QuotaClassSet` + instance. + + :returns: One + :class:`~openstack.block_storage.v2.quota_class_set.QuotaClassSet` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_quota_class_set.QuotaClassSet, quota_class_set) + + def update_quota_class_set(self, quota_class_set, **attrs): + """Update a QuotaClassSet. + + Only one quota class is permitted, ``default``. + + :param quota_class_set: Either the ID of a quota class set (only + ``default`` is supported) or a + :class:`~openstack.block_storage.v2.quota_class_set.QuotaClassSet` + instance. + :param attrs: The attributes to update on the QuotaClassSet represented + by ``quota_class_set``. + + :returns: The updated QuotaSet + :rtype: :class:`~openstack.block_storage.v2.quota_set.QuotaSet` + """ + return self._update( + _quota_class_set.QuotaClassSet, quota_class_set, **attrs + ) + # ====== QUOTA SETS ====== def get_quota_set(self, project, usage=False, **query): """Show QuotaSet information for the project diff --git a/openstack/block_storage/v2/quota_class_set.py b/openstack/block_storage/v2/quota_class_set.py new file mode 100644 index 000000000..d09c34ab8 --- /dev/null +++ b/openstack/block_storage/v2/quota_class_set.py @@ -0,0 +1,39 @@ +# 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 resource + + +class QuotaClassSet(resource.Resource): + resource_key = 'quota_class_set' + base_path = '/os-quota-class-sets' + + # Capabilities + allow_fetch = True + allow_commit = True + + # Properties + #: The size (GB) of backups that are allowed for each project. + backup_gigabytes = resource.Body('backup_gigabytes', type=int) + #: The number of backups that are allowed for each project. + backups = resource.Body('backups', type=int) + #: The size (GB) of volumes and snapshots that are allowed for each + #: project. + gigabytes = resource.Body('gigabytes', type=int) + #: The number of groups that are allowed for each project. + groups = resource.Body('groups', type=int) + #: The size (GB) of volumes in request that are allowed for each volume. + per_volume_gigabytes = resource.Body('per_volume_gigabytes', type=int) + #: The number of snapshots that are allowed for each project. + snapshots = resource.Body('snapshots', type=int) + #: The number of volumes that are allowed for each project. + volumes = resource.Body('volumes', type=int) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index aede10aed..ddea58037 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -23,6 +23,7 @@ from openstack.block_storage.v3 import group_snapshot as _group_snapshot from openstack.block_storage.v3 import group_type as _group_type from openstack.block_storage.v3 import limits as _limits +from openstack.block_storage.v3 import quota_class_set as _quota_class_set from openstack.block_storage.v3 import quota_set as _quota_set from openstack.block_storage.v3 import resource_filter as _resource_filter from openstack.block_storage.v3 import service as _service @@ -1701,7 +1702,44 @@ def delete_group_type_group_specs_property(self, group_type, prop): group_type = self._get_resource(_group_type.GroupType, group_type) return group_type.delete_group_specs_property(self, prop) + # ====== QUOTA CLASS SETS ====== + + def get_quota_class_set(self, quota_class_set='default'): + """Get a single quota class set + + Only one quota class is permitted, ``default``. + + :param quota_class_set: The value can be the ID of a quota class set + (only ``default`` is supported) or a + :class:`~openstack.block_storage.v3.quota_class_set.QuotaClassSet` + instance. + + :returns: One + :class:`~openstack.block_storage.v3.quota_class_set.QuotaClassSet` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_quota_class_set.QuotaClassSet, quota_class_set) + + def update_quota_class_set(self, quota_class_set, **attrs): + """Update a QuotaClassSet. + + Only one quota class is permitted, ``default``. + + :param quota_class_set: Either the ID of a quota class set (only + ``default`` is supported) or a + :param attrs: The attributes to update on the QuotaClassSet represented + by ``quota_class_set``. + + :returns: The updated QuotaSet + :rtype: :class:`~openstack.block_storage.v3.quota_set.QuotaSet` + """ + return self._update( + _quota_class_set.QuotaClassSet, quota_class_set, **attrs + ) + # ====== QUOTA SETS ====== + def get_quota_set(self, project, usage=False, **query): """Show QuotaSet information for the project diff --git a/openstack/block_storage/v3/quota_class_set.py b/openstack/block_storage/v3/quota_class_set.py new file mode 100644 index 000000000..d09c34ab8 --- /dev/null +++ b/openstack/block_storage/v3/quota_class_set.py @@ -0,0 +1,39 @@ +# 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 resource + + +class QuotaClassSet(resource.Resource): + resource_key = 'quota_class_set' + base_path = '/os-quota-class-sets' + + # Capabilities + allow_fetch = True + allow_commit = True + + # Properties + #: The size (GB) of backups that are allowed for each project. + backup_gigabytes = resource.Body('backup_gigabytes', type=int) + #: The number of backups that are allowed for each project. + backups = resource.Body('backups', type=int) + #: The size (GB) of volumes and snapshots that are allowed for each + #: project. + gigabytes = resource.Body('gigabytes', type=int) + #: The number of groups that are allowed for each project. + groups = resource.Body('groups', type=int) + #: The size (GB) of volumes in request that are allowed for each volume. + per_volume_gigabytes = resource.Body('per_volume_gigabytes', type=int) + #: The number of snapshots that are allowed for each project. + snapshots = resource.Body('snapshots', type=int) + #: The number of volumes that are allowed for each project. + volumes = resource.Body('volumes', type=int) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index fd5d8f5c8..0ef062387 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -22,6 +22,7 @@ from openstack.compute.v2 import keypair as _keypair from openstack.compute.v2 import limits from openstack.compute.v2 import migration as _migration +from openstack.compute.v2 import quota_class_set as _quota_class_set from openstack.compute.v2 import quota_set as _quota_set from openstack.compute.v2 import server as _server from openstack.compute.v2 import server_action as _server_action @@ -54,6 +55,7 @@ class Proxy(proxy.Proxy): "keypair": _keypair.Keypair, "limits": limits.Limits, "migration": _migration.Migration, + "quota_class_set": _quota_class_set.QuotaClassSet, "quota_set": _quota_set.QuotaSet, "server": _server.Server, "server_action": _server_action.ServerAction, @@ -2403,6 +2405,44 @@ def create_console(self, server, console_type, console_protocol=None): else: return server.get_console_url(self, console_type) + # ========== Quota class sets ========== + + def get_quota_class_set(self, quota_class_set='default'): + """Get a single quota class set + + Only one quota class is permitted, ``default``. + + :param quota_class_set: The value can be the ID of a quota class set + (only ``default`` is supported) or a + :class:`~openstack.compute.v2.quota_class_set.QuotaClassSet` + instance. + + :returns: One + :class:`~openstack.compute.v2.quota_class_set.QuotaClassSet` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_quota_class_set.QuotaClassSet, quota_class_set) + + def update_quota_class_set(self, quota_class_set, **attrs): + """Update a QuotaClassSet. + + Only one quota class is permitted, ``default``. + + :param quota_class_set: Either the ID of a quota class set (only + ``default`` is supported) or a + :class:`~openstack.compute.v2.quota_class_set.QuotaClassSet` + instance. + :param attrs: The attributes to update on the QuotaClassSet represented + by ``quota_class_set``. + + :returns: The updated QuotaSet + :rtype: :class:`~openstack.compute.v2.quota_set.QuotaSet` + """ + return self._update( + _quota_class_set.QuotaClassSet, quota_class_set, **attrs + ) + # ========== Quota sets ========== def get_quota_set(self, project, usage=False, **query): diff --git a/openstack/compute/v2/quota_class_set.py b/openstack/compute/v2/quota_class_set.py new file mode 100644 index 000000000..8f09a5512 --- /dev/null +++ b/openstack/compute/v2/quota_class_set.py @@ -0,0 +1,61 @@ +# 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 resource + + +class QuotaClassSet(resource.Resource): + resource_key = 'quota_class_set' + base_path = '/os-quota-class-sets' + + _max_microversion = '2.56' + + # capabilities + allow_fetch = True + allow_commit = True + + #: Properties + #: The number of allowed server cores for each tenant. + cores = resource.Body('cores', type=int) + #: The number of allowed fixed IP addresses for each tenant. Must be + #: equal to or greater than the number of allowed servers. + fixed_ips = resource.Body('fixed_ips', type=int) + #: The number of allowed floating IP addresses for each tenant. + floating_ips = resource.Body('floating_ips', type=int) + #: The number of allowed bytes of content for each injected file. + injected_file_content_bytes = resource.Body( + 'injected_file_content_bytes', type=int + ) + #: The number of allowed bytes for each injected file path. + injected_file_path_bytes = resource.Body( + 'injected_file_path_bytes', type=int + ) + #: The number of allowed injected files for each tenant. + injected_files = resource.Body('injected_files', type=int) + #: The number of allowed servers for each tenant. + instances = resource.Body('instances', type=int) + #: The number of allowed key pairs for each user. + key_pairs = resource.Body('key_pairs', type=int) + #: The number of allowed metadata items for each server. + metadata_items = resource.Body('metadata_items', type=int) + #: The number of private networks that can be created per project. + networks = resource.Body('networks', type=int) + #: The amount of allowed server RAM, in MiB, for each tenant. + ram = resource.Body('ram', type=int) + #: The number of allowed rules for each security group. + security_group_rules = resource.Body('security_group_rules', type=int) + #: The number of allowed security groups for each tenant. + security_groups = resource.Body('security_groups', type=int) + #: The number of allowed server groups for each tenant. + server_groups = resource.Body('server_groups', type=int) + #: The number of allowed members for each server group. + server_group_members = resource.Body('server_group_members', type=int) diff --git a/openstack/compute/v2/quota_set.py b/openstack/compute/v2/quota_set.py index 847c5ae0c..482cc7bf3 100644 --- a/openstack/compute/v2/quota_set.py +++ b/openstack/compute/v2/quota_set.py @@ -9,6 +9,7 @@ # 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.common import quota_set from openstack import resource diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index 9fa293079..9af158691 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -15,6 +15,7 @@ from openstack.block_storage.v2 import backup from openstack.block_storage.v2 import capabilities from openstack.block_storage.v2 import limits +from openstack.block_storage.v2 import quota_class_set from openstack.block_storage.v2 import quota_set from openstack.block_storage.v2 import snapshot from openstack.block_storage.v2 import stats @@ -457,8 +458,22 @@ def test_type_remove_private_access(self): ) -class TestQuota(TestVolumeProxy): - def test_get(self): +class TestQuotaClassSet(TestVolumeProxy): + def test_quota_class_set_get(self): + self.verify_get( + self.proxy.get_quota_class_set, quota_class_set.QuotaClassSet + ) + + def test_quota_class_set_update(self): + self.verify_update( + self.proxy.update_quota_class_set, + quota_class_set.QuotaClassSet, + False, + ) + + +class TestQuotaSet(TestVolumeProxy): + def test_quota_set_get(self): self._verify( 'openstack.resource.Resource.fetch', self.proxy.get_quota_set, @@ -473,7 +488,7 @@ def test_get(self): expected_result=quota_set.QuotaSet(), ) - def test_get_query(self): + def test_quota_set_get_query(self): self._verify( 'openstack.resource.Resource.fetch', self.proxy.get_quota_set, @@ -488,7 +503,7 @@ def test_get_query(self): }, ) - def test_get_defaults(self): + def test_quota_set_get_defaults(self): self._verify( 'openstack.resource.Resource.fetch', self.proxy.get_quota_set_defaults, @@ -501,7 +516,7 @@ def test_get_defaults(self): }, ) - def test_reset(self): + def test_quota_set_reset(self): self._verify( 'openstack.resource.Resource.delete', self.proxy.revert_quota_set, @@ -512,7 +527,7 @@ def test_reset(self): ) @mock.patch('openstack.proxy.Proxy._get_resource', autospec=True) - def test_update(self, gr_mock): + def test_quota_set_update(self, gr_mock): gr_mock.return_value = resource.Resource() gr_mock.commit = mock.Mock() self._verify( diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 6882a75f2..0a64a0dd5 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -20,6 +20,7 @@ from openstack.block_storage.v3 import group_snapshot from openstack.block_storage.v3 import group_type from openstack.block_storage.v3 import limits +from openstack.block_storage.v3 import quota_class_set from openstack.block_storage.v3 import quota_set from openstack.block_storage.v3 import resource_filter from openstack.block_storage.v3 import service @@ -948,8 +949,22 @@ def test_type_encryption_delete_ignore(self): ) -class TestQuota(TestVolumeProxy): - def test_get(self): +class TestQuotaClassSet(TestVolumeProxy): + def test_quota_class_set_get(self): + self.verify_get( + self.proxy.get_quota_class_set, quota_class_set.QuotaClassSet + ) + + def test_quota_class_set_update(self): + self.verify_update( + self.proxy.update_quota_class_set, + quota_class_set.QuotaClassSet, + False, + ) + + +class TestQuotaSet(TestVolumeProxy): + def test_quota_set_get(self): self._verify( 'openstack.resource.Resource.fetch', self.proxy.get_quota_set, @@ -964,7 +979,7 @@ def test_get(self): expected_result=quota_set.QuotaSet(), ) - def test_get_query(self): + def test_quota_set_get_query(self): self._verify( 'openstack.resource.Resource.fetch', self.proxy.get_quota_set, @@ -979,7 +994,7 @@ def test_get_query(self): }, ) - def test_get_defaults(self): + def test_quota_set_get_defaults(self): self._verify( 'openstack.resource.Resource.fetch', self.proxy.get_quota_set_defaults, @@ -992,7 +1007,7 @@ def test_get_defaults(self): }, ) - def test_reset(self): + def test_quota_set_reset(self): self._verify( 'openstack.resource.Resource.delete', self.proxy.revert_quota_set, @@ -1003,7 +1018,7 @@ def test_reset(self): ) @mock.patch('openstack.proxy.Proxy._get_resource', autospec=True) - def test_update(self, gr_mock): + def test_quota_set_update(self, gr_mock): gr_mock.return_value = resource.Resource() gr_mock.commit = mock.Mock() self._verify( diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 5d2815953..5c11d9e2a 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -26,6 +26,7 @@ from openstack.compute.v2 import image from openstack.compute.v2 import keypair from openstack.compute.v2 import migration +from openstack.compute.v2 import quota_class_set from openstack.compute.v2 import quota_set from openstack.compute.v2 import server from openstack.compute.v2 import server_action @@ -1617,8 +1618,22 @@ def test_create_console_mv_2_6(self, sgc, rcc, smv): self.assertEqual(console_fake['url'], ret['url']) -class TestQuota(TestComputeProxy): - def test_get(self): +class TestQuotaClassSet(TestComputeProxy): + def test_quota_class_set_get(self): + self.verify_get( + self.proxy.get_quota_class_set, quota_class_set.QuotaClassSet + ) + + def test_quota_class_set_update(self): + self.verify_update( + self.proxy.update_quota_class_set, + quota_class_set.QuotaClassSet, + False, + ) + + +class TestQuotaSet(TestComputeProxy): + def test_quota_set_get(self): self._verify( 'openstack.resource.Resource.fetch', self.proxy.get_quota_set, @@ -1633,7 +1648,7 @@ def test_get(self): expected_result=quota_set.QuotaSet(), ) - def test_get_query(self): + def test_quota_set_get_query(self): self._verify( 'openstack.resource.Resource.fetch', self.proxy.get_quota_set, @@ -1648,7 +1663,7 @@ def test_get_query(self): }, ) - def test_get_defaults(self): + def test_quota_set_get_defaults(self): self._verify( 'openstack.resource.Resource.fetch', self.proxy.get_quota_set_defaults, @@ -1661,7 +1676,7 @@ def test_get_defaults(self): }, ) - def test_reset(self): + def test_quota_set_reset(self): self._verify( 'openstack.resource.Resource.delete', self.proxy.revert_quota_set, @@ -1672,7 +1687,7 @@ def test_reset(self): ) @mock.patch('openstack.proxy.Proxy._get_resource', autospec=True) - def test_update(self, gr_mock): + def test_quota_set_update(self, gr_mock): gr_mock.return_value = resource.Resource() gr_mock.commit = mock.Mock() self._verify( From 14c1ebbeb8e6d7f89bd96476e6fca8ce822aebad Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 13 Jun 2024 12:39:24 +0100 Subject: [PATCH 3502/3836] exceptions: ResourceNotFound -> NotFoundException Invert the order of these such that ResourceNotFound is an alias of NotFoundException rather than the other way around. This is more consistent with the other HTTP-related exceptions. Change-Id: I3b669494cf7dc4e540a54070de42b1befe803e14 Signed-off-by: Stephen Finucane --- openstack/accelerator/v2/_proxy.py | 12 +- openstack/baremetal/v1/_proxy.py | 56 +-- openstack/baremetal/v1/node.py | 4 +- .../baremetal_introspection/v1/_proxy.py | 10 +- openstack/block_storage/v2/_proxy.py | 32 +- openstack/block_storage/v3/_proxy.py | 94 ++--- openstack/block_storage/v3/service.py | 2 +- openstack/block_storage/v3/transfer.py | 4 +- openstack/cloud/_compute.py | 10 +- openstack/cloud/_floating_ip.py | 4 +- openstack/cloud/_network.py | 30 +- openstack/cloud/_object_store.py | 4 +- openstack/cloud/openstackcloud.py | 2 +- openstack/clustering/v1/_async_resource.py | 2 +- openstack/clustering/v1/_proxy.py | 42 +-- openstack/compute/v2/_proxy.py | 90 ++--- openstack/compute/v2/service.py | 2 +- .../v1/_proxy.py | 14 +- .../v1/cluster.py | 4 +- openstack/database/v1/_proxy.py | 22 +- openstack/dns/v2/_base.py | 6 +- openstack/dns/v2/_proxy.py | 18 +- openstack/exceptions.py | 12 +- openstack/identity/v2/_proxy.py | 20 +- openstack/identity/v3/_proxy.py | 112 +++--- openstack/image/v1/_proxy.py | 6 +- openstack/image/v1/image.py | 6 +- openstack/image/v2/_proxy.py | 78 ++--- openstack/image/v2/image.py | 2 +- openstack/image/v2/service_info.py | 4 +- openstack/instance_ha/v1/_proxy.py | 16 +- openstack/key_manager/v1/_proxy.py | 18 +- openstack/load_balancer/v2/_proxy.py | 62 ++-- openstack/message/v2/_proxy.py | 16 +- openstack/network/v2/_proxy.py | 328 +++++++++--------- openstack/object_store/v1/_proxy.py | 14 +- openstack/object_store/v1/info.py | 2 +- openstack/orchestration/v1/_proxy.py | 22 +- openstack/orchestration/v1/stack.py | 8 +- openstack/placement/v1/_proxy.py | 24 +- openstack/proxy.py | 8 +- openstack/resource.py | 18 +- openstack/shared_file_system/v2/_proxy.py | 4 +- .../v2/share_access_rule.py | 2 +- .../baremetal/test_baremetal_allocation.py | 10 +- .../baremetal/test_baremetal_chassis.py | 8 +- .../test_baremetal_deploy_templates.py | 6 +- .../baremetal/test_baremetal_driver.py | 2 +- .../baremetal/test_baremetal_node.py | 18 +- .../baremetal/test_baremetal_port.py | 10 +- .../baremetal/test_baremetal_port_group.py | 8 +- .../test_baremetal_volume_connector.py | 8 +- .../baremetal/test_baremetal_volume_target.py | 8 +- .../functional/compute/v2/test_flavor.py | 2 +- .../functional/dns/v2/test_zone_share.py | 2 +- .../functional/orchestration/v1/test_stack.py | 2 +- .../shared_file_system/test_share.py | 2 +- openstack/tests/unit/cloud/test_compute.py | 2 +- openstack/tests/unit/cloud/test_domains.py | 2 +- openstack/tests/unit/cloud/test_fwaas.py | 8 +- openstack/tests/unit/cloud/test_network.py | 2 +- .../tests/unit/cloud/test_openstackcloud.py | 4 +- .../tests/unit/compute/v2/test_service.py | 2 +- openstack/tests/unit/network/v2/test_trunk.py | 4 +- .../tests/unit/orchestration/v1/test_proxy.py | 4 +- .../tests/unit/orchestration/v1/test_stack.py | 6 +- openstack/tests/unit/test_proxy.py | 10 +- openstack/tests/unit/test_resource.py | 6 +- openstack/workflow/v2/_proxy.py | 20 +- 69 files changed, 701 insertions(+), 701 deletions(-) diff --git a/openstack/accelerator/v2/_proxy.py b/openstack/accelerator/v2/_proxy.py index 7f40ccace..69f2b2cba 100644 --- a/openstack/accelerator/v2/_proxy.py +++ b/openstack/accelerator/v2/_proxy.py @@ -32,7 +32,7 @@ def get_deployable(self, uuid, fields=None): :param uuid: The value can be the UUID of a deployable. :returns: One :class:`~openstack.accelerator.v2.deployable.Deployable` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no deployable matching the criteria could be found. """ return self._get(_deployable.Deployable, uuid) @@ -76,7 +76,7 @@ def get_device(self, uuid, fields=None): :param uuid: The value can be the UUID of a device. :returns: One :class:`~openstack.accelerator.v2.device.Device` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no device matching the criteria could be found. """ return self._get(_device.Device, uuid) @@ -106,7 +106,7 @@ def delete_device_profile(self, device_profile, ignore_missing=True): :class:`~openstack.accelerator.v2.device_profile.DeviceProfile` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the device profile does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent device profile. @@ -124,7 +124,7 @@ def get_device_profile(self, uuid, fields=None): :param uuid: The value can be the UUID of a device profile. :returns: One :class: `~openstack.accelerator.v2.device_profile.DeviceProfile` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no device profile matching the criteria could be found. """ return self._get(_device_profile.DeviceProfile, uuid) @@ -158,7 +158,7 @@ def delete_accelerator_request( :class:`~openstack.accelerator.v2.device_profile.DeviceProfile` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the device profile does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent accelerator request. @@ -176,7 +176,7 @@ def get_accelerator_request(self, uuid, fields=None): :param uuid: The value can be the UUID of a accelerator request. :returns: One :class: `~openstack.accelerator.v2.accelerator_request.AcceleratorRequest` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no accelerator request matching the criteria could be found. """ return self._get(_arq.AcceleratorRequest, uuid) diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 18115e711..aa11cb0dc 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -118,7 +118,7 @@ def find_chassis(self, name_or_id, ignore_missing=True): :param str name_or_id: The ID of a chassis. :param bool ignore_missing: When set to ``False``, an exception of - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the chassis does not exist. When set to `True``, None will be returned when attempting to find a nonexistent chassis. :returns: One :class:`~openstack.baremetal.v1.chassis.Chassis` object @@ -136,7 +136,7 @@ def get_chassis(self, chassis, fields=None): :param fields: Limit the resource fields to fetch. :returns: One :class:`~openstack.baremetal.v1.chassis.Chassis` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no chassis matching the name or ID could be found. """ return self._get_with_fields(_chassis.Chassis, chassis, fields=fields) @@ -172,7 +172,7 @@ def delete_chassis(self, chassis, ignore_missing=True): :param chassis: The value can be either the ID of a chassis or a :class:`~openstack.baremetal.v1.chassis.Chassis` instance. :param bool ignore_missing: When set to ``False``, an exception - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the chassis could not be found. When set to ``True``, no exception will be raised when attempting to delete a non-existent chassis. @@ -206,7 +206,7 @@ def get_driver(self, driver): :class:`~openstack.baremetal.v1.driver.Driver` instance. :returns: One :class:`~openstack.baremetal.v1.driver.Driver` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no driver matching the name could be found. """ return self._get(_driver.Driver, driver) @@ -218,7 +218,7 @@ def list_driver_vendor_passthru(self, driver): :class:`~openstack.baremetal.v1.driver.Driver` instance. :returns: One :dict: of vendor methods with corresponding usages - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no driver matching the name could be found. """ driver = self.get_driver(driver) @@ -310,7 +310,7 @@ def find_node(self, name_or_id, ignore_missing=True): :param str name_or_id: The name or ID of a node. :param bool ignore_missing: When set to ``False``, an exception of - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the node does not exist. When set to `True``, None will be returned when attempting to find a nonexistent node. :returns: One :class:`~openstack.baremetal.v1.node.Node` object @@ -328,7 +328,7 @@ def get_node(self, node, fields=None): :param fields: Limit the resource fields to fetch. :returns: One :class:`~openstack.baremetal.v1.node.Node` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no node matching the name or ID could be found. """ return self._get_with_fields(_node.Node, node, fields=fields) @@ -340,7 +340,7 @@ def get_node_inventory(self, node): :class:`~openstack.baremetal.v1.node.Node` instance. :returns: The node inventory - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no inventory could be found. """ res = self._get_resource(_node.Node, node) @@ -702,7 +702,7 @@ def delete_node(self, node, ignore_missing=True): :param node: The value can be either the name or ID of a node or a :class:`~openstack.baremetal.v1.node.Node` instance. :param bool ignore_missing: When set to ``False``, an exception - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the node could not be found. When set to ``True``, no exception will be raised when attempting to delete a non-existent node. @@ -773,7 +773,7 @@ def find_port(self, name_or_id, ignore_missing=True): :param str name_or_id: The ID of a port. :param bool ignore_missing: When set to ``False``, an exception of - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the port does not exist. When set to `True``, None will be returned when attempting to find a nonexistent port. :returns: One :class:`~openstack.baremetal.v1.port.Port` object @@ -791,7 +791,7 @@ def get_port(self, port, fields=None): :param fields: Limit the resource fields to fetch. :returns: One :class:`~openstack.baremetal.v1.port.Port` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no port matching the name or ID could be found. """ return self._get_with_fields(_port.Port, port, fields=fields) @@ -827,7 +827,7 @@ def delete_port(self, port, ignore_missing=True): :param port: The value can be either the ID of a port or a :class:`~openstack.baremetal.v1.port.Port` instance. :param bool ignore_missing: When set to ``False``, an exception - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the port could not be found. When set to ``True``, no exception will be raised when attempting to delete a non-existent port. @@ -892,7 +892,7 @@ def find_port_group(self, name_or_id, ignore_missing=True): :param str name_or_id: The name or ID of a portgroup. :param bool ignore_missing: When set to ``False``, an exception of - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the port group does not exist. When set to `True``, None will be returned when attempting to find a nonexistent port group. :returns: One :class:`~openstack.baremetal.v1.port_group.PortGroup` @@ -910,7 +910,7 @@ def get_port_group(self, port_group, fields=None): :param fields: Limit the resource fields to fetch. :returns: One :class:`~openstack.baremetal.v1.port_group.PortGroup` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no port group matching the name or ID could be found. """ return self._get_with_fields( @@ -952,7 +952,7 @@ def delete_port_group(self, port_group, ignore_missing=True): :class:`~openstack.baremetal.v1.port_group.PortGroup` instance. :param bool ignore_missing: When set to ``False``, an exception - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the port group could not be found. When set to ``True``, no exception will be raised when attempting to delete a non-existent port group. @@ -997,7 +997,7 @@ def detach_vif_from_node(self, node, vif_id, ignore_missing=True): a :class:`~openstack.baremetal.v1.node.Node` instance. :param string vif_id: Backend-specific VIF ID. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the VIF does not exist. Otherwise, ``False`` is returned. :return: ``True`` if the VIF was detached, otherwise ``False``. @@ -1075,7 +1075,7 @@ def get_allocation(self, allocation, fields=None): :param fields: Limit the resource fields to fetch. :returns: One :class:`~openstack.baremetal.v1.allocation.Allocation` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no allocation matching the name or ID could be found. """ return self._get_with_fields( @@ -1115,7 +1115,7 @@ def delete_allocation(self, allocation, ignore_missing=True): :param allocation: The value can be the name or ID of an allocation or a :class:`~openstack.baremetal.v1.allocation.Allocation` instance. :param bool ignore_missing: When set to ``False``, an exception - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the allocation could not be found. When set to ``True``, no exception will be raised when attempting to delete a non-existent allocation. @@ -1167,7 +1167,7 @@ def remove_node_trait(self, node, trait, ignore_missing=True): :class:`~openstack.baremetal.v1.node.Node` instance. :param trait: trait to remove from the node. :param bool ignore_missing: When set to ``False``, an exception - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the trait could not be found. When set to ``True``, no exception will be raised when attempting to delete a non-existent trait. @@ -1300,7 +1300,7 @@ def find_volume_connector(self, vc_id, ignore_missing=True): :param str vc_id: The ID of a volume connector. :param bool ignore_missing: When set to ``False``, an exception of - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the volume connector does not exist. When set to `True``, None will be returned when attempting to find a nonexistent volume connector. @@ -1325,7 +1325,7 @@ def get_volume_connector(self, volume_connector, fields=None): :returns: One :class: `~openstack.baremetal.v1.volume_connector.VolumeConnector` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no volume_connector matching the name or ID could be found.` """ return self._get_with_fields( @@ -1375,7 +1375,7 @@ def delete_volume_connector(self, volume_connector, ignore_missing=True): :class:`~openstack.baremetal.v1.volume_connector.VolumeConnector` instance. :param bool ignore_missing: When set to ``False``, an exception - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the volume_connector could not be found. When set to ``True``, no exception will be raised when attempting to delete a non-existent volume_connector. @@ -1447,7 +1447,7 @@ def find_volume_target(self, vt_id, ignore_missing=True): :param str vt_id: The ID of a volume target. :param bool ignore_missing: When set to ``False``, an exception of - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the volume connector does not exist. When set to `True``, None will be returned when attempting to find a nonexistent volume target. @@ -1470,7 +1470,7 @@ def get_volume_target(self, volume_target, fields=None): :returns: One :class:`~openstack.baremetal.v1.volume_target.VolumeTarget` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no volume_target matching the name or ID could be found.` """ return self._get_with_fields( @@ -1517,7 +1517,7 @@ def delete_volume_target(self, volume_target, ignore_missing=True): :class:`~openstack.baremetal.v1.volume_target.VolumeTarget` instance. :param bool ignore_missing: When set to ``False``, an exception - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the volume_target could not be found. When set to ``True``, no exception will be raised when attempting to delete a non-existent volume_target. @@ -1585,7 +1585,7 @@ def delete_deploy_template(self, deploy_template, ignore_missing=True): instance. :param bool ignore_missing: When set to ``False``, - an exception:class:`~openstack.exceptions.ResourceNotFound` + an exception:class:`~openstack.exceptions.NotFoundException` will be raised when the deploy_template could not be found. When set to ``True``, no @@ -1616,7 +1616,7 @@ def get_deploy_template(self, deploy_template, fields=None): :returns: One :class:`~openstack.baremetal.v1.deploy_templates.DeployTemplate` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no deployment template matching the name or ID could be found. """ @@ -1663,7 +1663,7 @@ def get_conductor(self, conductor, fields=None): :returns: One :class:`~openstack.baremetal.v1.conductor.Conductor` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no conductor matching the name could be found. """ return self._get_with_fields( diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 524930413..8da9036b8 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -847,7 +847,7 @@ def detach_vif(self, session, vif_id, ignore_missing=True): :type session: :class:`~keystoneauth1.adapter.Adapter` :param string vif_id: Backend-specific VIF ID. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the VIF does not exist. Otherwise, ``False`` is returned. :return: ``True`` if the VIF was detached, otherwise ``False``. @@ -1188,7 +1188,7 @@ def remove_trait(self, session, trait, ignore_missing=True): :param session: The session to use for making this request. :param trait: The trait to remove from the node. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the trait does not exist. Otherwise, ``False`` is returned. :returns bool: True on success removing the trait. diff --git a/openstack/baremetal_introspection/v1/_proxy.py b/openstack/baremetal_introspection/v1/_proxy.py index b82cee8fe..eca563c7c 100644 --- a/openstack/baremetal_introspection/v1/_proxy.py +++ b/openstack/baremetal_introspection/v1/_proxy.py @@ -89,7 +89,7 @@ def get_introspection(self, introspection): introspection (matching bare metal node name or ID) or an :class:`~.introspection.Introspection` instance. :returns: :class:`~.introspection.Introspection` instance. - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no introspection matching the name or ID could be found. """ return self._get(_introspect.Introspection, introspection) @@ -118,7 +118,7 @@ def abort_introspection(self, introspection, ignore_missing=True): introspection (matching bare metal node name or ID) or an :class:`~.introspection.Introspection` instance. :param bool ignore_missing: When set to ``False``, an exception - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the introspection could not be found. When set to ``True``, no exception will be raised when attempting to abort a non-existent introspection. @@ -127,7 +127,7 @@ def abort_introspection(self, introspection, ignore_missing=True): res = self._get_resource(_introspect.Introspection, introspection) try: res.abort(self) - except exceptions.ResourceNotFound: + except exceptions.NotFoundException: if not ignore_missing: raise @@ -177,7 +177,7 @@ def delete_introspection_rule( introspection rule or a :class:`~.introspection_rule.IntrospectionRule` instance. :param bool ignore_missing: When set to ``False``, an - exception:class:`~openstack.exceptions.ResourceNotFound` will be + exception:class:`~openstack.exceptions.NotFoundException` will be raised when the introspection rule could not be found. When set to ``True``, no exception will be raised when attempting to delete a non-existent introspection rule. @@ -198,7 +198,7 @@ def get_introspection_rule(self, introspection_rule): :class:`~.introspection_rule.IntrospectionRule` instance. :returns: :class:`~.introspection_rule.IntrospectionRule` instance. - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no introspection rule matching the name or ID could be found. """ return self._get( diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 46cb472ab..b15d1e975 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -34,7 +34,7 @@ def get_snapshot(self, snapshot): instance. :returns: One :class:`~openstack.block_storage.v2.snapshot.Snapshot` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_snapshot.Snapshot, snapshot) @@ -51,7 +51,7 @@ def find_snapshot( :param snapshot: The name or ID a snapshot :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the snapshot does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. :param bool details: When set to ``False``, an @@ -66,7 +66,7 @@ def find_snapshot( :returns: One :class:`~openstack.block_storage.v2.snapshot.Snapshot`, one :class:`~openstack.block_storage.v2.snapshot.SnapshotDetail` object, or None. - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple resources are found. @@ -127,7 +127,7 @@ def delete_snapshot(self, snapshot, ignore_missing=True): :class:`~openstack.block_storage.v2.snapshot.Snapshot` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the snapshot does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent snapshot. @@ -159,7 +159,7 @@ def get_type(self, type): :class:`~openstack.block_storage.v2.type.Type` instance. :returns: One :class:`~openstack.block_storage.v2.type.Type` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_type.Type, type) @@ -189,7 +189,7 @@ def delete_type(self, type, ignore_missing=True): :param type: The value can be either the ID of a type or a :class:`~openstack.block_storage.v2.type.Type` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the type does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent type. @@ -244,7 +244,7 @@ def get_volume(self, volume): :class:`~openstack.block_storage.v2.volume.Volume` instance. :returns: One :class:`~openstack.block_storage.v2.volume.Volume` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_volume.Volume, volume) @@ -261,7 +261,7 @@ def find_volume( :param volume: The name or ID a volume :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the volume does not exist. :param bool details: When set to ``False`` no extended attributes will be returned. The default, ``True``, will cause an object with @@ -272,7 +272,7 @@ def find_volume( :returns: One :class:`~openstack.block_storage.v2.volume.Volume` or None. - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple resources are found. @@ -329,7 +329,7 @@ def delete_volume(self, volume, ignore_missing=True, force=False): :param volume: The value can be either the ID of a volume or a :class:`~openstack.block_storage.v2.volume.Volume` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the volume does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent volume. @@ -565,14 +565,14 @@ def find_backup(self, name_or_id, ignore_missing=True, *, details=True): :param snapshot: The name or ID a backup :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the backup does not exist. :param bool details: When set to ``False`` no additional details will be returned. The default, ``True``, will cause objects with additional attributes to be returned. :returns: One :class:`~openstack.block_storage.v2.backup.Backup` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple resources are found. @@ -603,7 +603,7 @@ def delete_backup(self, backup, ignore_missing=True, force=False): :param backup: The value can be the ID of a backup or a :class:`~openstack.block_storage.v2.backup.Backup` instance :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the zone does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent zone. @@ -669,7 +669,7 @@ def get_capabilities(self, host): :returns: One :class: `~openstack.block_storage.v2.capabilites.Capabilities` instance. - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_capabilities.Capabilities, host) @@ -686,7 +686,7 @@ def get_quota_set(self, project, usage=False, **query): :param dict query: Additional query parameters to use. :returns: One :class:`~openstack.block_storage.v2.quota_set.QuotaSet` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ project = self._get_resource(_project.Project, project) @@ -703,7 +703,7 @@ def get_quota_set_defaults(self, project): which the quota should be retrieved :returns: One :class:`~openstack.block_storage.v2.quota_set.QuotaSet` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ project = self._get_resource(_project.Project, project) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index aede10aed..bf86e461a 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -67,7 +67,7 @@ def get_snapshot(self, snapshot): instance. :returns: One :class:`~openstack.block_storage.v3.snapshot.Snapshot` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_snapshot.Snapshot, snapshot) @@ -84,7 +84,7 @@ def find_snapshot( :param snapshot: The name or ID a snapshot :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the snapshot does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. :param bool details: When set to ``False`` :class: @@ -96,7 +96,7 @@ def find_snapshot( a higher chance of duplicates. Admin-only by default. :returns: One :class:`~openstack.block_storage.v3.snapshot.Snapshot` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple resources are found. @@ -168,7 +168,7 @@ def delete_snapshot(self, snapshot, ignore_missing=True, force=False): :param snapshot: The value can be either the ID of a snapshot or a :class:`~openstack.block_storage.v3.snapshot.Snapshot` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the snapshot does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent snapshot. @@ -290,7 +290,7 @@ def get_type(self, type): :class:`~openstack.block_storage.v3.type.Type` instance. :returns: One :class:`~openstack.block_storage.v3.type.Type` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_type.Type, type) @@ -300,11 +300,11 @@ def find_type(self, name_or_id, ignore_missing=True): :param snapshot: The name or ID a volume type :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the volume type does not exist. :returns: One :class:`~openstack.block_storage.v3.type.Type` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple resources are found. @@ -340,7 +340,7 @@ def delete_type(self, type, ignore_missing=True): :param type: The value can be either the ID of a type or a :class:`~openstack.block_storage.v3.type.Type` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the type does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent type. @@ -435,7 +435,7 @@ def get_type_encryption(self, volume_type_id): instance. :returns: One :class:`~openstack.block_storage.v3.type.TypeEncryption` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ volume_type = self._get_resource(_type.Type, volume_type_id) @@ -481,7 +481,7 @@ def delete_type_encryption( instance. Required if encryption_id is None. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the type does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent type. @@ -540,7 +540,7 @@ def get_volume(self, volume): :class:`~openstack.block_storage.v3.volume.Volume` instance. :returns: One :class:`~openstack.block_storage.v3.volume.Volume` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_volume.Volume, volume) @@ -557,7 +557,7 @@ def find_volume( :param snapshot: The name or ID a volume :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the volume does not exist. :param bool details: When set to ``False`` no extended attributes will be returned. The default, ``True``, will cause objects with @@ -567,7 +567,7 @@ def find_volume( a higher chance of duplicates. Admin-only by default. :returns: One :class:`~openstack.block_storage.v3.volume.Volume` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple resources are found. @@ -624,7 +624,7 @@ def delete_volume(self, volume, ignore_missing=True, force=False): :param volume: The value can be either the ID of a volume or a :class:`~openstack.block_storage.v3.volume.Volume` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the volume does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent volume. @@ -1066,7 +1066,7 @@ def get_attachment(self, attachment): :class:`~attachment.Attachment` instance. :returns: One :class:`~attachment.Attachment` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_attachment.Attachment, attachment) @@ -1094,7 +1094,7 @@ def delete_attachment(self, attachment, ignore_missing=True): :class:`~openstack.block_storage.v3.attachment.Attachment` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the attachment does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent attachment. @@ -1196,14 +1196,14 @@ def find_backup(self, name_or_id, ignore_missing=True, *, details=True): :param snapshot: The name or ID a backup :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the backup does not exist. :param bool details: When set to ``False`` no additional details will be returned. The default, ``True``, will cause objects with additional attributes to be returned. :returns: One :class:`~openstack.block_storage.v3.backup.Backup` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple resources are found. @@ -1234,7 +1234,7 @@ def delete_backup(self, backup, ignore_missing=True, force=False): :param backup: The value can be the ID of a backup or a :class:`~openstack.block_storage.v3.backup.Backup` instance :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the zone does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent zone. @@ -1300,7 +1300,7 @@ def get_capabilities(self, host): :returns: One :class: `~openstack.block_storage.v3.capabilites.Capabilities` instance. - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_capabilities.Capabilities, host) @@ -1323,14 +1323,14 @@ def find_group(self, name_or_id, ignore_missing=True, *, details=True): :param name_or_id: The name or ID of a group. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the group snapshot does not exist. :param bool details: When set to ``False``, no additional details will be returned. The default, ``True``, will cause additional details to be returned. :returns: One :class:`~openstack.block_storage.v3.group.Group` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple resources are found. @@ -1462,14 +1462,14 @@ def find_group_snapshot( :param name_or_id: The name or ID of a group snapshot. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the group snapshot does not exist. :param bool details: When set to ``False``, no additional details will be returned. The default, ``True``, will cause additional details to be returned. :returns: One :class:`~openstack.block_storage.v3.group_snapshot` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple resources are found. @@ -1550,7 +1550,7 @@ def get_group_type(self, group_type): :returns: One :class: `~openstack.block_storage.v3.group_type.GroupType` instance. - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_group_type.GroupType, group_type) @@ -1560,12 +1560,12 @@ def find_group_type(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a group type. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the group type does not exist. :returns: One :class:`~openstack.block_storage.v3.group_type.GroupType` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple resources are found. @@ -1617,7 +1617,7 @@ def delete_group_type(self, group_type, ignore_missing=True): or a :class:`~openstack.block_storage.v3.group_type.GroupType` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the zone does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent zone. @@ -1713,7 +1713,7 @@ def get_quota_set(self, project, usage=False, **query): :param dict query: Additional query parameters to use. :returns: One :class:`~openstack.block_storage.v3.quota_set.QuotaSet` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ project = self._get_resource(_project.Project, project) @@ -1730,7 +1730,7 @@ def get_quota_set_defaults(self, project): which the quota should be retrieved :returns: One :class:`~openstack.block_storage.v3.quota_set.QuotaSet` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ project = self._get_resource(_project.Project, project) @@ -1810,14 +1810,14 @@ def find_service( :param name_or_id: The name or ID of a service :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. :param dict query: Additional attributes like 'host' :returns: One: class:`~openstack.block_storage.v3.service.Service` or None - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple resources are found. @@ -1854,8 +1854,8 @@ def enable_service( :returns: Updated service instance :rtype: class: `~openstack.block_storage.v3.service.Service` """ - service = self._get_resource(_service.Service, service) - return service.enable(self) + service_obj = self._get_resource(_service.Service, service) + return service_obj.enable(self) def disable_service( self, @@ -1872,8 +1872,8 @@ def disable_service( :returns: Updated service instance :rtype: class: `~openstack.block_storage.v3.service.Service` """ - service = self._get_resource(_service.Service, service) - return service.disable(self, reason=reason) + service_obj = self._get_resource(_service.Service, service) + return service_obj.disable(self, reason=reason) def thaw_service( self, @@ -1887,8 +1887,8 @@ def thaw_service( :returns: Updated service instance :rtype: class: `~openstack.block_storage.v3.service.Service` """ - service = self._get_resource(_service.Service, service) - return service.thaw(self) + service_obj = self._get_resource(_service.Service, service) + return service_obj.thaw(self) def freeze_service( self, @@ -1902,8 +1902,8 @@ def freeze_service( :returns: Updated service instance :rtype: class: `~openstack.block_storage.v3.service.Service` """ - service = self._get_resource(_service.Service, service) - return service.freeze(self) + service_obj = self._get_resource(_service.Service, service) + return service_obj.freeze(self) def failover_service( self, @@ -1922,8 +1922,10 @@ def failover_service( :returns: Updated service instance :rtype: class: `~openstack.block_storage.v3.service.Service` """ - service = self._get_resource(_service.Service, service) - return service.failover(self, cluster=cluster, backend_id=backend_id) + service_obj = self._get_resource(_service.Service, service) + return service_obj.failover( + self, cluster=cluster, backend_id=backend_id + ) # ====== RESOURCE FILTERS ====== def resource_filters(self, **query): @@ -1963,7 +1965,7 @@ def delete_transfer(self, transfer, ignore_missing=True): :param transfer: The value can be either the ID of a transfer or a :class:`~openstack.block_storage.v3.transfer.Transfer`` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the transfer does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent transfer. @@ -1981,11 +1983,11 @@ def find_transfer(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID a transfer :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the volume transfer does not exist. :returns: One :class:`~openstack.block_storage.v3.transfer.Transfer` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple resources are found. @@ -2004,7 +2006,7 @@ def get_transfer(self, transfer): instance. :returns: One :class:`~openstack.block_storage.v3.transfer.Transfer` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_transfer.Transfer, transfer) diff --git a/openstack/block_storage/v3/service.py b/openstack/block_storage/v3/service.py index a0c229da7..1454fc8f1 100644 --- a/openstack/block_storage/v3/service.py +++ b/openstack/block_storage/v3/service.py @@ -86,7 +86,7 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): if ignore_missing: return None - raise exceptions.ResourceNotFound( + raise exceptions.NotFoundException( f"No {cls.__name__} found for {name_or_id}" ) diff --git a/openstack/block_storage/v3/transfer.py b/openstack/block_storage/v3/transfer.py index e05cb3f98..0081d9413 100644 --- a/openstack/block_storage/v3/transfer.py +++ b/openstack/block_storage/v3/transfer.py @@ -130,7 +130,7 @@ def fetch( :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if :data:`Resource.allow_fetch` is not set to ``True``. - :raises: :exc:`~openstack.exceptions.ResourceNotFound` if + :raises: :exc:`~openstack.exceptions.NotFoundException` if the resource was not found. """ @@ -162,7 +162,7 @@ def delete( :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if :data:`Resource.allow_commit` is not set to ``True``. - :raises: :exc:`~openstack.exceptions.ResourceNotFound` if + :raises: :exc:`~openstack.exceptions.NotFoundException` if the resource was not found. """ diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 7848e46ab..09828dc72 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -325,7 +325,7 @@ def remove_server_security_groups(self, server, security_groups): try: self.compute.remove_security_group_from_server(server, sg) - except exceptions.ResourceNotFound: + except exceptions.NotFoundException: # NOTE(jamielennox): Is this ok? If we remove something that # isn't present should we just conclude job done or is that an # error? Nova returns ok if you try to add a group twice. @@ -561,7 +561,7 @@ def get_server_by_id(self, id): try: server = self.compute.get_server(id) return meta.add_server_interfaces(self, server) - except exceptions.ResourceNotFound: + except exceptions.NotFoundException: return None def get_server_group(self, name_or_id=None, filters=None): @@ -612,7 +612,7 @@ def delete_keypair(self, name): """ try: self.compute.delete_keypair(name, ignore_missing=False) - except exceptions.ResourceNotFound: + except exceptions.NotFoundException: self.log.debug("Keypair %s not found for deleting", name) return False return True @@ -1399,7 +1399,7 @@ def _delete_server( try: self.compute.delete_server(server) - except exceptions.ResourceNotFound: + except exceptions.NotFoundException: return False except Exception: raise @@ -1708,7 +1708,7 @@ def delete_aggregate(self, name_or_id): try: self.compute.delete_aggregate(name_or_id, ignore_missing=False) return True - except exceptions.ResourceNotFound: + except exceptions.NotFoundException: self.log.debug("Aggregate %s not found for deleting", name_or_id) return False diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index 60ed7c9e1..e0c698a08 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -439,7 +439,7 @@ def _neutron_create_floating_ip( if network_name_or_id: try: network = self.network.find_network(network_name_or_id) - except exceptions.ResourceNotFound: + except exceptions.NotFoundException: raise exceptions.NotFoundException( "unable to find network for floating ips with ID " "{}".format(network_name_or_id) @@ -592,7 +592,7 @@ def _delete_floating_ip(self, floating_ip_id): def _neutron_delete_floating_ip(self, floating_ip_id): try: self.network.delete_ip(floating_ip_id, ignore_missing=False) - except exceptions.ResourceNotFound: + except exceptions.NotFoundException: return False return True diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index b88bec504..3d25bbdd2 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -778,7 +778,7 @@ def delete_firewall_rule(self, name_or_id, filters=None): self.network.delete_firewall_rule( firewall_rule, ignore_missing=False ) - except exceptions.ResourceNotFound: + except exceptions.NotFoundException: self.log.debug( 'Firewall rule %s not found for deleting', name_or_id ) @@ -938,7 +938,7 @@ def create_firewall_policy(self, **kwargs): :param bool shared: Visibility to other projects. Defaults to False. :raises: BadRequestException if parameters are malformed - :raises: ResourceNotFound if a resource from firewall_list not found + :raises: NotFoundException if a resource from firewall_list not found :returns: The created network ``FirewallPolicy`` object. """ if 'firewall_rules' in kwargs: @@ -982,7 +982,7 @@ def delete_firewall_policy(self, name_or_id, filters=None): self.network.delete_firewall_policy( firewall_policy, ignore_missing=False ) - except exceptions.ResourceNotFound: + except exceptions.NotFoundException: self.log.debug( 'Firewall policy %s not found for deleting', name_or_id ) @@ -1078,7 +1078,7 @@ def update_firewall_policy(self, name_or_id, filters=None, **kwargs): :returns: The updated network ``FirewallPolicy`` object. :raises: BadRequestException if parameters are malformed :raises: DuplicateResource on multiple matches - :raises: ResourceNotFound if resource is not found + :raises: NotFoundException if resource is not found """ if not filters: filters = {} @@ -1115,7 +1115,7 @@ def insert_rule_into_policy( :param insert_before: rule name or id that should succeed added rule :param dict filters: optional filters :raises: DuplicateResource on multiple matches - :raises: ResourceNotFound if firewall policy or any of the firewall + :raises: NotFoundException if firewall policy or any of the firewall rules (inserted, after, before) is not found. :return: updated firewall policy :rtype: FirewallPolicy @@ -1166,7 +1166,7 @@ def remove_rule_from_policy( :param rule_name_or_id: firewall rule name or id :param dict filters: optional filters :raises: DuplicateResource on multiple matches - :raises: ResourceNotFound if firewall policy is not found + :raises: NotFoundException if firewall policy is not found :return: updated firewall policy :rtype: FirewallPolicy """ @@ -1228,7 +1228,7 @@ def create_firewall_group(self, **kwargs): :param shared: Visibility to other projects. Defaults to False. :raises: BadRequestException if parameters are malformed :raises: DuplicateResource on multiple matches - :raises: ResourceNotFound if (ingress-, egress-) firewall policy or + :raises: NotFoundException if (ingress-, egress-) firewall policy or a port is not found. :returns: The created network ``FirewallGroup`` object. """ @@ -1257,7 +1257,7 @@ def delete_firewall_group(self, name_or_id, filters=None): self.network.delete_firewall_group( firewall_group, ignore_missing=False ) - except exceptions.ResourceNotFound: + except exceptions.NotFoundException: self.log.debug( 'Firewall group %s not found for deleting', name_or_id ) @@ -1315,7 +1315,7 @@ def update_firewall_group(self, name_or_id, filters=None, **kwargs): :returns: The updated network ``FirewallGroup`` object. :raises: BadRequestException if parameters are malformed :raises: DuplicateResource on multiple matches - :raises: ResourceNotFound if firewall group, a firewall policy + :raises: NotFoundException if firewall group, a firewall policy (egress, ingress) or port is not found """ if not filters: @@ -1339,7 +1339,7 @@ def _lookup_ingress_egress_firewall_policy_ids(self, firewall_group): :param dict firewall_group: firewall group dict :raises: DuplicateResource on multiple matches - :raises: ResourceNotFound if a firewall policy is not found + :raises: NotFoundException if a firewall policy is not found """ for key in ('egress_firewall_policy', 'ingress_firewall_policy'): if key not in firewall_group: @@ -1665,7 +1665,7 @@ def delete_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): self.network.delete_qos_bandwidth_limit_rule( rule_id, policy, ignore_missing=False ) - except exceptions.ResourceNotFound: + except exceptions.NotFoundException: self.log.debug( "QoS bandwidth limit rule {rule_id} not found in policy " "{policy_id}. Ignoring.".format( @@ -1855,7 +1855,7 @@ def delete_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): self.network.delete_qos_dscp_marking_rule( rule_id, policy, ignore_missing=False ) - except exceptions.ResourceNotFound: + except exceptions.NotFoundException: self.log.debug( "QoS DSCP marking rule {rule_id} not found in policy " "{policy_id}. Ignoring.".format( @@ -2058,7 +2058,7 @@ def delete_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): self.network.delete_qos_minimum_bandwidth_rule( rule_id, policy, ignore_missing=False ) - except exceptions.ResourceNotFound: + except exceptions.NotFoundException: self.log.debug( "QoS minimum bandwidth rule {rule_id} not found in policy " "{policy_id}. Ignoring.".format( @@ -2804,7 +2804,7 @@ def _get_port_ids(self, name_or_id_list, filters=None): :param list[str] name_or_id_list: list of port names or ids :param dict filters: optional filters :raises: SDKException on multiple matches - :raises: ResourceNotFound if a port is not found + :raises: NotFoundException if a port is not found :return: list of port ids :rtype: list[str] """ @@ -2812,7 +2812,7 @@ def _get_port_ids(self, name_or_id_list, filters=None): for name_or_id in name_or_id_list: port = self.get_port(name_or_id, filters) if not port: - raise exceptions.ResourceNotFound( + raise exceptions.NotFoundException( f'Port {name_or_id} not found' ) ids_list.append(port['id']) diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index f918d3235..1dcba4876 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -456,7 +456,7 @@ def stream_object( yield from self.object_store.stream_object( obj, container, chunk_size=resp_chunk_size ) - except exceptions.ResourceNotFound: + except exceptions.NotFoundException: return def get_object( @@ -498,7 +498,7 @@ def get_object( headers = {k.lower(): v for k, v in obj._last_headers.items()} return (headers, obj.data) - except exceptions.ResourceNotFound: + except exceptions.NotFoundException: return None def _wait_for_futures(self, futures, raise_on_error=True): diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 81abdb436..5f4ff9ede 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -584,7 +584,7 @@ def search_resources( resource_type, name_or_id, *get_args, **get_kwargs ) return [resource_by_id] - except exceptions.ResourceNotFound: + except exceptions.NotFoundException: pass if not filters: diff --git a/openstack/clustering/v1/_async_resource.py b/openstack/clustering/v1/_async_resource.py index 060415d69..f37bb629b 100644 --- a/openstack/clustering/v1/_async_resource.py +++ b/openstack/clustering/v1/_async_resource.py @@ -27,7 +27,7 @@ def delete(self, session, error_message=None): to populate the `Action` with status information. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if :data:`Resource.allow_commit` is not set to ``True``. - :raises: :exc:`~openstack.exceptions.ResourceNotFound` if + :raises: :exc:`~openstack.exceptions.NotFoundException` if the resource was not found. """ response = self._raw_delete(session) diff --git a/openstack/clustering/v1/_proxy.py b/openstack/clustering/v1/_proxy.py index aae729c61..f63b95934 100644 --- a/openstack/clustering/v1/_proxy.py +++ b/openstack/clustering/v1/_proxy.py @@ -68,7 +68,7 @@ def get_profile_type(self, profile_type): :returns: A :class:`~openstack.clustering.v1.profile_type.ProfileType` object. - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no profile_type matching the name could be found. """ return self._get(_profile_type.ProfileType, profile_type) @@ -89,7 +89,7 @@ def get_policy_type(self, policy_type): :returns: A :class:`~openstack.clustering.v1.policy_type.PolicyType` object. - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no policy_type matching the name could be found. """ return self._get(_policy_type.PolicyType, policy_type) @@ -112,7 +112,7 @@ def delete_profile(self, profile, ignore_missing=True): :param profile: The value can be either the name or ID of a profile or a :class:`~openstack.clustering.v1.profile.Profile` instance. :param bool ignore_missing: When set to ``False``, an exception - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the profile could not be found. When set to ``True``, no exception will be raised when attempting to delete a non-existent profile. @@ -125,7 +125,7 @@ def find_profile(self, name_or_id, ignore_missing=True): :param str name_or_id: The name or ID of a profile. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -143,7 +143,7 @@ def get_profile(self, profile): :class:`~openstack.clustering.v1.profile.Profile` instance. :returns: One :class:`~openstack.clustering.v1.profile.Profile` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no profile matching the criteria could be found. """ return self._get(_profile.Profile, profile) @@ -219,7 +219,7 @@ def delete_cluster(self, cluster, ignore_missing=True, force_delete=False): :param cluster: The value can be either the name or ID of a cluster or a :class:`~openstack.cluster.v1.cluster.Cluster` instance. :param bool ignore_missing: When set to ``False``, an exception - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the cluster could not be found. When set to ``True``, no exception will be raised when attempting to delete a non-existent cluster. :param bool force_delete: When set to ``True``, the cluster deletion @@ -241,7 +241,7 @@ def find_cluster(self, name_or_id, ignore_missing=True): :param str name_or_id: The name or ID of a cluster. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -259,7 +259,7 @@ def get_cluster(self, cluster): :class:`~openstack.clustering.v1.cluster.Cluster` instance. :returns: One :class:`~openstack.clustering.v1.cluster.Cluster` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no cluster matching the criteria could be found. """ return self._get(_cluster.Cluster, cluster) @@ -560,7 +560,7 @@ def delete_node(self, node, ignore_missing=True, force_delete=False): :param node: The value can be either the name or ID of a node or a :class:`~openstack.cluster.v1.node.Node` instance. :param bool ignore_missing: When set to ``False``, an exception - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the node could not be found. When set to ``True``, no exception will be raised when attempting to delete a non-existent node. :param bool force_delete: When set to ``True``, the node deletion @@ -582,7 +582,7 @@ def find_node(self, name_or_id, ignore_missing=True): :param str name_or_id: The name or ID of a node. :param bool ignore_missing: When set to "False" - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the specified node does not exist. when set to "True", None will be returned when attempting to find a nonexistent policy @@ -602,7 +602,7 @@ def get_node(self, node, details=False): server should return more details when retrieving the node data. :returns: One :class:`~openstack.clustering.v1.node.Node` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no node matching the name or ID could be found. """ # NOTE: When retrieving node with details (using NodeDetail resource), @@ -741,7 +741,7 @@ def delete_policy(self, policy, ignore_missing=True): :param policy: The value can be either the name or ID of a policy or a :class:`~openstack.clustering.v1.policy.Policy` instance. :param bool ignore_missing: When set to ``False``, an exception - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the policy could not be found. When set to ``True``, no exception will be raised when attempting to delete a non-existent policy. @@ -754,7 +754,7 @@ def find_policy(self, name_or_id, ignore_missing=True): :param str name_or_id: The name or ID of a policy. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the specified policy does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent policy. @@ -773,7 +773,7 @@ def get_policy(self, policy): :returns: A policy object. :rtype: :class:`~openstack.clustering.v1.policy.Policy` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no policy matching the criteria could be found. """ return self._get(_policy.Policy, policy) @@ -856,7 +856,7 @@ def get_cluster_policy(self, cluster_policy, cluster): :returns: a cluster-policy binding object. :rtype: :class:`~openstack.clustering.v1.cluster_policy.CLusterPolicy` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no cluster-policy binding matching the criteria could be found. """ return self._get( @@ -893,7 +893,7 @@ def delete_receiver(self, receiver, ignore_missing=True): :param receiver: The value can be either the name or ID of a receiver or a :class:`~openstack.clustering.v1.receiver.Receiver` instance. :param bool ignore_missing: When set to ``False``, an exception - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the receiver could not be found. When set to ``True``, no exception will be raised when attempting to delete a non-existent receiver. @@ -908,7 +908,7 @@ def find_receiver(self, name_or_id, ignore_missing=True): :param str name_or_id: The name or ID of a receiver. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the specified receiver does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent receiver. @@ -927,7 +927,7 @@ def get_receiver(self, receiver): :returns: A receiver object. :rtype: :class:`~openstack.clustering.v1.receiver.Receiver` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no receiver matching the criteria could be found. """ return self._get(_receiver.Receiver, receiver) @@ -960,7 +960,7 @@ def get_action(self, action): :returns: an action object. :rtype: :class:`~openstack.clustering.v1.action.Action` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no action matching the criteria could be found. """ return self._get(_action.Action, action) @@ -1011,7 +1011,7 @@ def get_event(self, event): :returns: an event object. :rtype: :class:`~openstack.clustering.v1.event.Event` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no event matching the criteria could be found. """ return self._get(_event.Event, event) @@ -1106,7 +1106,7 @@ def list_profile_type_operations(self, profile_type): :returns: A :class:`~openstack.clustering.v1.profile_type.ProfileType` object. - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no profile_type matching the name could be found. """ obj = self._get_resource(_profile_type.ProfileType, profile_type) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index fd5d8f5c8..76ecf5af2 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -75,14 +75,14 @@ def find_extension(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of an extension. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. :returns: One :class:`~openstack.compute.v2.extension.Extension` or None - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple resources are found. @@ -116,7 +116,7 @@ def find_flavor( :param name_or_id: The name or ID of a flavor. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. :param bool get_extra_specs: When set to ``True`` and extra_specs not @@ -126,7 +126,7 @@ def find_flavor( the flavors being returned. :returns: One :class:`~openstack.compute.v2.flavor.Flavor` or None - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple resources are found. @@ -159,7 +159,7 @@ def delete_flavor(self, flavor, ignore_missing=True): :param flavor: The value can be either the ID of a flavor or a :class:`~openstack.compute.v2.flavor.Flavor` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the flavor does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent flavor. @@ -191,7 +191,7 @@ def get_flavor(self, flavor, get_extra_specs=False): extra_specs. :returns: One :class:`~openstack.compute.v2.flavor.Flavor` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ flavor = self._get(_flavor.Flavor, flavor) @@ -334,7 +334,7 @@ def get_aggregate(self, aggregate): :class:`~openstack.compute.v2.aggregate.Aggregate` instance. :returns: One :class:`~openstack.compute.v2.aggregate.Aggregate` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_aggregate.Aggregate, aggregate) @@ -344,13 +344,13 @@ def find_aggregate(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of an aggregate. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. :returns: One :class:`~openstack.compute.v2.aggregate.Aggregate` or None - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple resources are found. @@ -393,7 +393,7 @@ def delete_aggregate(self, aggregate, ignore_missing=True): :class:`~openstack.compute.v2.aggregate.Aggregate` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the aggregate does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent aggregate. @@ -473,7 +473,7 @@ def delete_image(self, image, ignore_missing=True): :param image: The value can be either the ID of an image or a :class:`~openstack.compute.v2.image.Image` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the image does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent image. @@ -494,13 +494,13 @@ def find_image(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a image. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. :returns: One :class:`~openstack.compute.v2.image.Image` or None - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple resources are found. @@ -523,7 +523,7 @@ def get_image(self, image): :class:`~openstack.compute.v2.image.Image` instance. :returns: One :class:`~openstack.compute.v2.image.Image` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ warnings.warn( @@ -633,7 +633,7 @@ def delete_keypair(self, keypair, ignore_missing=True, user_id=None): :param keypair: The value can be either the ID of a keypair or a :class:`~openstack.compute.v2.keypair.Keypair` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the keypair does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent keypair. :param str user_id: Optional user_id owning the keypair @@ -656,7 +656,7 @@ def get_keypair(self, keypair, user_id=None): :param str user_id: Optional user_id owning the keypair :returns: One :class:`~openstack.compute.v2.keypair.Keypair` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ attrs = {'user_id': user_id} if user_id else {} @@ -667,13 +667,13 @@ def find_keypair(self, name_or_id, ignore_missing=True, *, user_id=None): :param name_or_id: The name or ID of a keypair. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. :param str user_id: Optional user_id owning the keypair :returns: One :class:`~openstack.compute.v2.keypair.Keypair` or None - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple resources are found. @@ -729,7 +729,7 @@ def delete_server(self, server, ignore_missing=True, force=False): :param server: The value can be either the ID of a server or a :class:`~openstack.compute.v2.server.Server` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the server does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent server @@ -756,7 +756,7 @@ def find_server( :param name_or_id: The name or ID of a server. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. :param bool details: When set to ``False`` @@ -767,7 +767,7 @@ def find_server( higher chance of duplicates. Admin-only by default. :returns: One :class:`~openstack.compute.v2.server.Server` or None - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple resources are found. @@ -791,7 +791,7 @@ def get_server(self, server): :class:`~openstack.compute.v2.server.Server` instance. :returns: One :class:`~openstack.compute.v2.server.Server` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_server.Server, server) @@ -1380,7 +1380,7 @@ def delete_server_interface( server or a :class:`~openstack.compute.v2.server.Server` instance that the interface belongs to. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the server interface does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent server interface. @@ -1415,7 +1415,7 @@ def get_server_interface(self, server_interface, server=None): :returns: One :class:`~openstack.compute.v2.server_interface.ServerInterface` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ server_id = self._get_uri_attribute( @@ -1561,7 +1561,7 @@ def delete_server_group(self, server_group, ignore_missing=True): or a :class:`~openstack.compute.v2.server_group.ServerGroup` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the server group does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent server group. @@ -1585,7 +1585,7 @@ def find_server_group( :param name_or_id: The name or ID of a server group. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. :param bool all_projects: When set to ``True``, search for server @@ -1594,7 +1594,7 @@ def find_server_group( :returns: One :class:`~openstack.compute.v2.server_group.ServerGroup` or None - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple resources are found. @@ -1618,7 +1618,7 @@ def get_server_group(self, server_group): :returns: A :class:`~openstack.compute.v2.server_group.ServerGroup` object. - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_server_group.ServerGroup, server_group) @@ -1674,7 +1674,7 @@ def find_hypervisor( :param name_or_id: The name or ID of a hypervisor :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. :param bool details: When set to ``False`` @@ -1683,7 +1683,7 @@ def find_hypervisor( :returns: One: class:`~openstack.compute.v2.hypervisor.Hypervisor` or None - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple resources are found. @@ -1705,7 +1705,7 @@ def get_hypervisor(self, hypervisor): :returns: A :class:`~openstack.compute.v2.hypervisor.Hypervisor` object. - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_hypervisor.Hypervisor, hypervisor) @@ -1719,7 +1719,7 @@ def get_hypervisor_uptime(self, hypervisor): :returns: A :class:`~openstack.compute.v2.hypervisor.Hypervisor` object. - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ hypervisor = self._get_resource(_hypervisor.Hypervisor, hypervisor) @@ -1819,14 +1819,14 @@ def find_service(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or id of a service :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. :param dict query: Additional attributes like 'host' :returns: One: class:`~openstack.compute.v2.service.Service` or None - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple resources are found. @@ -1845,7 +1845,7 @@ def delete_service(self, service, ignore_missing=True): The value can be either the ID of a service or a :class:`~openstack.compute.v2.service.Service` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the service does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent service. @@ -2024,7 +2024,7 @@ def delete_volume_attachment(self, server, volume, ignore_missing=True): :param volume: The value can be the ID of a volume or a :class:`~openstack.block_storage.v3.volume.Volume` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the volume attachment does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent volume attachment. @@ -2058,7 +2058,7 @@ def get_volume_attachment(self, server, volume): :returns: One :class:`~openstack.compute.v2.volume_attachment.VolumeAttachment` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ server_id = resource.Resource._get_id(server) @@ -2151,7 +2151,7 @@ def abort_server_migration( server or a :class:`~openstack.compute.v2.server.Server` instance that the migration belongs to. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the server migration does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent server migration. @@ -2215,14 +2215,14 @@ def get_server_migration( :class:`~openstack.compute.v2.server.Server` instance that the migration belongs to. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the server migration does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent server migration. :returns: One :class:`~openstack.compute.v2.server_migration.ServerMigration` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ server_id = self._get_uri_attribute( @@ -2279,7 +2279,7 @@ def get_server_diagnostics(self, server): :returns: One :class:`~openstack.compute.v2.server_diagnostics.ServerDiagnostics` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ server_id = self._get_resource(_server.Server, server).id @@ -2416,7 +2416,7 @@ def get_quota_set(self, project, usage=False, **query): :param dict query: Additional query parameters to use. :returns: One :class:`~openstack.compute.v2.quota_set.QuotaSet` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ project = self._get_resource(_project.Project, project) @@ -2435,7 +2435,7 @@ def get_quota_set_defaults(self, project): which the quota should be retrieved :returns: One :class:`~openstack.compute.v2.quota_set.QuotaSet` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ project = self._get_resource(_project.Project, project) @@ -2496,13 +2496,13 @@ def get_server_action(self, server_action, server, ignore_missing=True): :class:`~openstack.compute.v2.server.Server` instance that the action is associated with. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the server action does not exist. When set to ``True``, no exception will be set when attempting to retrieve a non-existent server action. :returns: One :class:`~openstack.compute.v2.server_action.ServerAction` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ server_id = self._get_uri_attribute(server_action, server, 'server_id') diff --git a/openstack/compute/v2/service.py b/openstack/compute/v2/service.py index e4dce2ee6..ba371122f 100644 --- a/openstack/compute/v2/service.py +++ b/openstack/compute/v2/service.py @@ -83,7 +83,7 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): if ignore_missing: return None - raise exceptions.ResourceNotFound( + raise exceptions.NotFoundException( f"No {cls.__name__} found for {name_or_id}" ) diff --git a/openstack/container_infrastructure_management/v1/_proxy.py b/openstack/container_infrastructure_management/v1/_proxy.py index 1e95b428d..e11db1398 100644 --- a/openstack/container_infrastructure_management/v1/_proxy.py +++ b/openstack/container_infrastructure_management/v1/_proxy.py @@ -51,7 +51,7 @@ def delete_cluster(self, cluster, ignore_missing=True): :class:`~openstack.container_infrastructure_management.v1.cluster.Cluster` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the cluster does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent cluster. :returns: ``None`` @@ -63,7 +63,7 @@ def find_cluster(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a cluster. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -86,7 +86,7 @@ def get_cluster(self, cluster): :returns: One :class:`~openstack.container_infrastructure_management.v1.cluster.Cluster` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_cluster.Cluster, cluster) @@ -139,7 +139,7 @@ def delete_cluster_template(self, cluster_template, ignore_missing=True): :class:`~openstack.container_infrastructure_management.v1.cluster_template.ClusterTemplate` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the cluster_template does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent cluster_template. @@ -156,7 +156,7 @@ def find_cluster_template(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a cluster_template. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -180,7 +180,7 @@ def get_cluster_template(self, cluster_template): :returns: One :class:`~openstack.container_infrastructure_management.v1.cluster_template.ClusterTemplate` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_cluster_template.ClusterTemplate, cluster_template) @@ -237,7 +237,7 @@ def get_cluster_certificate(self, cluster_certificate): :returns: One :class:`~openstack.container_infrastructure_management.v1.cluster_certificate.ClusterCertificate` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_cluster_cert.ClusterCertificate, cluster_certificate) diff --git a/openstack/container_infrastructure_management/v1/cluster.py b/openstack/container_infrastructure_management/v1/cluster.py index 776cdeee2..19d4efdab 100644 --- a/openstack/container_infrastructure_management/v1/cluster.py +++ b/openstack/container_infrastructure_management/v1/cluster.py @@ -134,7 +134,7 @@ def resize(self, session, *, node_count, nodes_to_remove=None): :param nodes_to_remove: The server ID list will be removed if downsizing the cluster. :returns: The UUID of the resized cluster. - :raises: :exc:`~openstack.exceptions.ResourceNotFound` if + :raises: :exc:`~openstack.exceptions.NotFoundException` if the resource was not found. """ url = utils.urljoin(Cluster.base_path, self.id, 'actions', 'resize') @@ -154,7 +154,7 @@ def upgrade(self, session, *, cluster_template, max_batch_size=None): :param max_batch_size: The max batch size each time when doing upgrade. The default is 1 :returns: The UUID of the updated cluster. - :raises: :exc:`~openstack.exceptions.ResourceNotFound` if + :raises: :exc:`~openstack.exceptions.NotFoundException` if the resource was not found. """ url = utils.urljoin(Cluster.base_path, self.id, 'actions', 'upgrade') diff --git a/openstack/database/v1/_proxy.py b/openstack/database/v1/_proxy.py index 4e3df801f..6a4d88145 100644 --- a/openstack/database/v1/_proxy.py +++ b/openstack/database/v1/_proxy.py @@ -52,7 +52,7 @@ def delete_database(self, database, instance=None, ignore_missing=True): It can be either the ID of an instance or a :class:`~openstack.database.v1.instance.Instance` :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the database does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent database. @@ -76,7 +76,7 @@ def find_database(self, name_or_id, instance, ignore_missing=True): :param instance: This can be either the ID of an instance or a :class:`~openstack.database.v1.instance.Instance` :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -117,7 +117,7 @@ def get_database(self, database, instance=None): instance. :returns: One :class:`~openstack.database.v1.database.Database` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_database.Database, database) @@ -127,7 +127,7 @@ def find_flavor(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a flavor. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -144,7 +144,7 @@ def get_flavor(self, flavor): :class:`~openstack.database.v1.flavor.Flavor` instance. :returns: One :class:`~openstack.database.v1.flavor.Flavor` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_flavor.Flavor, flavor) @@ -178,7 +178,7 @@ def delete_instance(self, instance, ignore_missing=True): :param instance: The value can be either the ID of an instance or a :class:`~openstack.database.v1.instance.Instance` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the instance does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent instance. @@ -194,7 +194,7 @@ def find_instance(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -212,7 +212,7 @@ def get_instance(self, instance): instance. :returns: One :class:`~openstack.database.v1.instance.Instance` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_instance.Instance, instance) @@ -266,7 +266,7 @@ def delete_user(self, user, instance=None, ignore_missing=True): It can be either the ID of an instance or a :class:`~openstack.database.v1.instance.Instance` :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the user does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent user. @@ -288,7 +288,7 @@ def find_user(self, name_or_id, instance, ignore_missing=True): :param instance: This can be either the ID of an instance or a :class:`~openstack.database.v1.instance.Instance` :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -327,7 +327,7 @@ def get_user(self, user, instance=None): or a :class:`~openstack.database.v1.instance.Instance` :returns: One :class:`~openstack.database.v1.user.User` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ instance = self._get_resource(_instance.Instance, instance) diff --git a/openstack/dns/v2/_base.py b/openstack/dns/v2/_base.py index 756e42939..2c50da0a5 100644 --- a/openstack/dns/v2/_base.py +++ b/openstack/dns/v2/_base.py @@ -27,7 +27,7 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): :param name_or_id: This resource's identifier, if needed by the request. The default is ``None``. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -40,7 +40,7 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): or None if nothing matches. :raises: :class:`openstack.exceptions.DuplicateResource` if more than one resource is found for this request. - :raises: :class:`openstack.exceptions.ResourceNotFound` if nothing + :raises: :class:`openstack.exceptions.NotFoundException` if nothing is found and ignore_missing is ``False``. """ session = cls._get_session(session) @@ -68,7 +68,7 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): if ignore_missing: return None - raise exceptions.ResourceNotFound( + raise exceptions.NotFoundException( f"No {cls.__name__} found for {name_or_id}" ) diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py index 9c71b2f5f..5b1e35d85 100644 --- a/openstack/dns/v2/_proxy.py +++ b/openstack/dns/v2/_proxy.py @@ -77,7 +77,7 @@ def delete_zone(self, zone, ignore_missing=True, delete_shares=False): :param zone: The value can be the ID of a zone or a :class:`~openstack.dns.v2.zone.Zone` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the zone does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent zone. @@ -111,7 +111,7 @@ def find_zone(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a zone :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the zone does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent zone. @@ -221,7 +221,7 @@ def delete_recordset(self, recordset, zone=None, ignore_missing=True): :param zone: The value can be the ID of a zone or a :class:`~openstack.dns.v2.zone.Zone` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the zone does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent zone. @@ -242,7 +242,7 @@ def find_recordset(self, zone, name_or_id, ignore_missing=True, **query): or a :class:`~openstack.dns.v2.zone.Zone` instance. :param name_or_id: The name or ID of a zone :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the zone does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent zone. @@ -303,7 +303,7 @@ def delete_zone_import(self, zone_import, ignore_missing=True): :param zone_import: The value can be the ID of a zone import or a :class:`~openstack.dns.v2.zone_import.ZoneImport` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the zone does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent zone. @@ -380,7 +380,7 @@ def delete_zone_export(self, zone_export, ignore_missing=True): :param zone_export: The value can be the ID of a zone import or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the zone does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent zone. @@ -510,7 +510,7 @@ def delete_zone_transfer_request(self, request, ignore_missing=True): or a :class:`~openstack.dns.v2.zone_transfer.ZoneTransferRequest` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the zone does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent zone. @@ -600,7 +600,7 @@ def find_zone_share(self, zone, zone_share_id, ignore_missing=True): or a :class:`~openstack.dns.v2.zone.Zone` instance. :param zone_share_id: The zone share ID :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the zone share does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent zone share. @@ -641,7 +641,7 @@ def delete_zone_share(self, zone, zone_share, ignore_missing=True): share or a :class:`~openstack.dns.v2.zone_share.ZoneShare` instance that the zone share belongs to. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the zone share does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent zone share. diff --git a/openstack/exceptions.py b/openstack/exceptions.py index aa2f92296..528cc4c0f 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -120,6 +120,10 @@ class BadRequestException(HttpException): """HTTP 400 Bad Request.""" +class NotFoundException(HttpException): + """HTTP 404 Not Found.""" + + class ForbiddenException(HttpException): """HTTP 403 Forbidden Request.""" @@ -154,13 +158,6 @@ class DuplicateResource(SDKException): """More than one resource exists with that name.""" -class ResourceNotFound(HttpException): - """No resource exists with that name or ID.""" - - -NotFoundException = ResourceNotFound - - class ResourceTimeout(SDKException): """Timeout waiting for resource.""" @@ -275,3 +272,4 @@ class ServiceDiscoveryException(SDKException): # Backwards compatibility OpenStackCloudException = SDKException +ResourceNotFound = NotFoundException diff --git a/openstack/identity/v2/_proxy.py b/openstack/identity/v2/_proxy.py index f0a96f580..141279f00 100644 --- a/openstack/identity/v2/_proxy.py +++ b/openstack/identity/v2/_proxy.py @@ -34,7 +34,7 @@ def get_extension(self, extension): instance. :returns: One :class:`~openstack.identity.v2.extension.Extension` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no extension can be found. """ return self._get(_extension.Extension, extension) @@ -57,7 +57,7 @@ def delete_role(self, role, ignore_missing=True): :param role: The value can be either the ID of a role or a :class:`~openstack.identity.v2.role.Role` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the role does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent role. @@ -71,7 +71,7 @@ def find_role(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a role. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -88,7 +88,7 @@ def get_role(self, role): :class:`~openstack.identity.v2.role.Role` instance. :returns: One :class:`~openstack.identity.v2.role.Role` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_role.Role, role) @@ -135,7 +135,7 @@ def delete_tenant(self, tenant, ignore_missing=True): :param tenant: The value can be either the ID of a tenant or a :class:`~openstack.identity.v2.tenant.Tenant` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the tenant does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent tenant. @@ -149,7 +149,7 @@ def find_tenant(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a tenant. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -166,7 +166,7 @@ def get_tenant(self, tenant): :class:`~openstack.identity.v2.tenant.Tenant` instance. :returns: One :class:`~openstack.identity.v2.tenant.Tenant` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_tenant.Tenant, tenant) @@ -213,7 +213,7 @@ def delete_user(self, user, ignore_missing=True): :param user: The value can be either the ID of a user or a :class:`~openstack.identity.v2.user.User` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the user does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent user. @@ -227,7 +227,7 @@ def find_user(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a user. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -244,7 +244,7 @@ def get_user(self, user): :class:`~openstack.identity.v2.user.User` instance. :returns: One :class:`~openstack.identity.v2.user.User` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_user.User, user) diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 82f015294..48c061e19 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -108,7 +108,7 @@ def delete_credential(self, credential, ignore_missing=True): :param credential: The value can be either the ID of a credential or a :class:`~openstack.identity.v3.credential.Credential` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the credential does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent credential. @@ -124,7 +124,7 @@ def find_credential(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a credential. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -142,7 +142,7 @@ def get_credential(self, credential): :class:`~openstack.identity.v3.credential.Credential` instance. :returns: One :class:`~openstack.identity.v3.credential.Credential` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_credential.Credential, credential) @@ -192,7 +192,7 @@ def delete_domain(self, domain, ignore_missing=True): :param domain: The value can be either the ID of a domain or a :class:`~openstack.identity.v3.domain.Domain` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the domain does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent domain. @@ -206,7 +206,7 @@ def find_domain(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a domain. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -223,7 +223,7 @@ def get_domain(self, domain): :class:`~openstack.identity.v3.domain.Domain` instance. :returns: One :class:`~openstack.identity.v3.domain.Domain` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_domain.Domain, domain) @@ -280,7 +280,7 @@ def delete_domain_config(self, domain, ignore_missing=True): :param domain: The value can be the ID of a domain or a a :class:`~openstack.identity.v3.domain.Domain` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the identity provider does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent config for a domain. @@ -302,7 +302,7 @@ def get_domain_config(self, domain): :returns: One :class:`~openstack.identity.v3.domain_config.DomainConfig` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ domain_id = resource.Resource._get_id(domain) @@ -350,7 +350,7 @@ def delete_endpoint(self, endpoint, ignore_missing=True): :param endpoint: The value can be either the ID of an endpoint or a :class:`~openstack.identity.v3.endpoint.Endpoint` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the endpoint does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent endpoint. @@ -366,7 +366,7 @@ def find_endpoint(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a endpoint. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -384,7 +384,7 @@ def get_endpoint(self, endpoint): instance. :returns: One :class:`~openstack.identity.v3.endpoint.Endpoint` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_endpoint.Endpoint, endpoint) @@ -434,7 +434,7 @@ def delete_group(self, group, ignore_missing=True): :param group: The value can be either the ID of a group or a :class:`~openstack.identity.v3.group.Group` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the group does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent group. @@ -448,7 +448,7 @@ def find_group(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of a group. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -469,7 +469,7 @@ def get_group(self, group): instance. :returns: One :class:`~openstack.identity.v3.group.Group` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_group.Group, group) @@ -572,7 +572,7 @@ def delete_policy(self, policy, ignore_missing=True): :param policy: The value can be either the ID of a policy or a :class:`~openstack.identity.v3.policy.Policy` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the policy does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent policy. @@ -586,7 +586,7 @@ def find_policy(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a policy. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -603,7 +603,7 @@ def get_policy(self, policy): :class:`~openstack.identity.v3.policy.Policy` instance. :returns: One :class:`~openstack.identity.v3.policy.Policy` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_policy.Policy, policy) @@ -653,7 +653,7 @@ def delete_project(self, project, ignore_missing=True): :param project: The value can be either the ID of a project or a :class:`~openstack.identity.v3.project.Project` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the project does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent project. @@ -667,7 +667,7 @@ def find_project(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of a project. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -687,7 +687,7 @@ def get_project(self, project): :class:`~openstack.identity.v3.project.Project` instance. :returns: One :class:`~openstack.identity.v3.project.Project` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_project.Project, project) @@ -752,7 +752,7 @@ def delete_service(self, service, ignore_missing=True): :param service: The value can be either the ID of a service or a :class:`~openstack.identity.v3.service.Service` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the service does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent service. @@ -766,7 +766,7 @@ def find_service(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a service. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -783,7 +783,7 @@ def get_service(self, service): :class:`~openstack.identity.v3.service.Service` instance. :returns: One :class:`~openstack.identity.v3.service.Service` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_service.Service, service) @@ -833,7 +833,7 @@ def delete_user(self, user, ignore_missing=True): :param user: The value can be either the ID of a user or a :class:`~openstack.identity.v3.user.User` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the user does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent user. @@ -847,7 +847,7 @@ def find_user(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of a user. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -867,7 +867,7 @@ def get_user(self, user): :class:`~openstack.identity.v3.user.User` instance. :returns: One :class:`~openstack.identity.v3.user.User` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_user.User, user) @@ -917,7 +917,7 @@ def delete_trust(self, trust, ignore_missing=True): :param trust: The value can be either the ID of a trust or a :class:`~openstack.identity.v3.trust.Trust` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the credential does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent credential. @@ -931,7 +931,7 @@ def find_trust(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a trust. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -948,7 +948,7 @@ def get_trust(self, trust): :class:`~openstack.identity.v3.trust.Trust` instance. :returns: One :class:`~openstack.identity.v3.trust.Trust` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_trust.Trust, trust) @@ -985,7 +985,7 @@ def delete_region(self, region, ignore_missing=True): :param region: The value can be either the ID of a region or a :class:`~openstack.identity.v3.region.Region` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the region does not exist. When set to ``True``, no exception will be thrown when attempting to delete a nonexistent region. @@ -999,7 +999,7 @@ def find_region(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a region. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the region does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent region. @@ -1016,7 +1016,7 @@ def get_region(self, region): :class:`~openstack.identity.v3.region.Region` instance. :returns: One :class:`~openstack.identity.v3.region.Region` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no matching region can be found. """ return self._get(_region.Region, region) @@ -1066,7 +1066,7 @@ def delete_role(self, role, ignore_missing=True): :param role: The value can be either the ID of a role or a :class:`~openstack.identity.v3.role.Role` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the role does not exist. When set to ``True``, no exception will be thrown when attempting to delete a nonexistent role. @@ -1080,7 +1080,7 @@ def find_role(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of a role. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the role does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent role. @@ -1100,7 +1100,7 @@ def get_role(self, role): :class:`~openstack.identity.v3.role.Role` instance. :returns: One :class:`~openstack.identity.v3.role.Role` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no matching role can be found. """ return self._get(_role.Role, role) @@ -1543,7 +1543,7 @@ def get_registered_limit(self, registered_limit): instance. :returns: One :class:`~openstack.identity.v3.registered_limit.RegisteredLimit` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_registered_limit.RegisteredLimit, registered_limit) @@ -1585,7 +1585,7 @@ def delete_registered_limit(self, registered_limit, ignore_missing=True): :class:`~openstack.identity.v3.registered_limit.RegisteredLimit` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the registered_limit does not exist. When set to ``True``, no exception will be thrown when attempting to delete a nonexistent registered_limit. @@ -1618,7 +1618,7 @@ def get_limit(self, limit): or a :class:`~openstack.identity.v3.limit.Limit` instance. :returns: One :class:`~openstack.identity.v3.limit.Limit` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_limit.Limit, limit) @@ -1654,7 +1654,7 @@ def delete_limit(self, limit, ignore_missing=True): :param limit: The value can be either the ID of a limit or a :class:`~openstack.identity.v3.limit.Limit` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the limit does not exist. When set to ``True``, no exception will be thrown when attempting to delete a nonexistent limit. @@ -1697,7 +1697,7 @@ def get_application_credential(self, user, application_credential): :returns: One :class:`~openstack.identity.v3.application_credential.ApplicationCredential` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ user = self._get_resource(_user.User, user) @@ -1745,7 +1745,7 @@ def find_application_credential( :class:`~openstack.identity.v3.user.User` instance. :param name_or_id: The name or ID of an application credential. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -1775,7 +1775,7 @@ def delete_application_credential( :class:`~openstack.identity.v3.application_credential.ApplicationCredential` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the application credential does not exist. When set to ``True``, no exception will be thrown when attempting to delete a nonexistent application credential. @@ -1831,7 +1831,7 @@ def delete_federation_protocol( :class:`~openstack.identity.v3.federation_protocol.FederationProtocol` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the federation protocol does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent federation protocol. @@ -1856,7 +1856,7 @@ def find_federation_protocol(self, idp_id, protocol, ignore_missing=True): representing the identity provider the protocol is attached to. :param protocol: The name or ID of a federation protocol. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. :returns: One federation protocol or None @@ -1888,7 +1888,7 @@ def get_federation_protocol(self, idp_id, protocol): :returns: One federation protocol :rtype: :class:`~openstack.identity.v3.federation_protocol.FederationProtocol` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ cls = _federation_protocol.FederationProtocol @@ -1966,7 +1966,7 @@ def delete_mapping(self, mapping, ignore_missing=True): :class:`~openstack.identity.v3.mapping.Mapping` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the mapping does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent mapping. @@ -1980,7 +1980,7 @@ def find_mapping(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a mapping. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -1998,7 +1998,7 @@ def get_mapping(self, mapping): instance. :returns: One :class:`~openstack.identity.v3.mapping.Mapping` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_mapping.Mapping, mapping) @@ -2049,7 +2049,7 @@ def delete_identity_provider(self, identity_provider, ignore_missing=True): :class:`~openstack.identity.v3.identity_provider.IdentityProvider` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the identity provider does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent identity provider. @@ -2067,7 +2067,7 @@ def find_identity_provider(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of an identity provider :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -2091,7 +2091,7 @@ def get_identity_provider(self, identity_provider): :returns: The details of an identity provider. :rtype: :class:`~openstack.identity.v3.identity_provider.IdentityProvider` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get( @@ -2152,7 +2152,7 @@ def get_access_rule(self, user, access_rule): :class:`~.access_rule.AccessRule` instance. :returns: One :class:`~.access_rule.AccessRule` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ user = self._get_resource(_user.User, user) @@ -2166,7 +2166,7 @@ def delete_access_rule(self, user, access_rule, ignore_missing=True): :param access rule: The value can be either the ID of an access rule or a :class:`~.access_rule.AccessRule` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the access rule does not exist. When set to ``True``, no exception will be thrown when attempting to delete a nonexistent access rule. @@ -2202,7 +2202,7 @@ def delete_service_provider(self, service_provider, ignore_missing=True): :class:`~openstack.identity.v3.service_provider.ServiceProvider` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the service provider does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent service provider. @@ -2220,7 +2220,7 @@ def find_service_provider(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a service provider :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -2245,7 +2245,7 @@ def get_service_provider(self, service_provider): :returns: The details of an service provider. :rtype: :class:`~openstack.identity.v3.service_provider.ServiceProvider` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_service_provider.ServiceProvider, service_provider) diff --git a/openstack/image/v1/_proxy.py b/openstack/image/v1/_proxy.py index bdcf35cf7..73b4828f6 100644 --- a/openstack/image/v1/_proxy.py +++ b/openstack/image/v1/_proxy.py @@ -322,7 +322,7 @@ def delete_image(self, image, ignore_missing=True): :param image: The value can be either the ID of an image or a :class:`~openstack.image.v1.image.Image` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the image does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent image. @@ -336,7 +336,7 @@ def find_image(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a image. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -353,7 +353,7 @@ def get_image(self, image): :class:`~openstack.image.v1.image.Image` instance. :returns: One :class:`~openstack.image.v1.image.Image` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_image.Image, image) diff --git a/openstack/image/v1/image.py b/openstack/image/v1/image.py index 7e1283b1d..173e2544a 100644 --- a/openstack/image/v1/image.py +++ b/openstack/image/v1/image.py @@ -96,7 +96,7 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): :param name_or_id: This resource's identifier, if needed by the request. The default is ``None``. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -109,7 +109,7 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): or None if nothing matches. :raises: :class:`openstack.exceptions.DuplicateResource` if more than one resource is found for this request. - :raises: :class:`openstack.exceptions.ResourceNotFound` if nothing + :raises: :class:`openstack.exceptions.NotFoundException` if nothing is found and ignore_missing is ``False``. """ session = cls._get_session(session) @@ -134,6 +134,6 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): if ignore_missing: return None - raise exceptions.ResourceNotFound( + raise exceptions.NotFoundException( f"No {cls.__name__} found for {name_or_id}" ) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 6876ff9cf..c275a4e75 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -90,7 +90,7 @@ def cache_delete_image(self, image, ignore_missing=True): :class:`~openstack.image.v2.image.Image` instance. :param bool ignore_missing: When set to ``False``, - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the metadef namespace does not exist. :returns: ``None`` """ @@ -832,7 +832,7 @@ def delete_image(self, image, *, store=None, ignore_missing=True): image is associated with. If specified, the image will only be deleted from the specified store. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the image does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent image. @@ -850,7 +850,7 @@ def find_image(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a image. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -869,7 +869,7 @@ def get_image(self, image): :class:`~openstack.image.v2.image.Image` instance. :returns: One :class:`~openstack.image.v2.image.Image` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_image.Image, image) @@ -1018,7 +1018,7 @@ def remove_member(self, member, image=None, ignore_missing=True): :class:`~openstack.image.v2.image.Image` instance that the member is part of. This is required if ``member`` is an ID. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the member does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent member. @@ -1041,7 +1041,7 @@ def find_member(self, name_or_id, image, ignore_missing=True): the value can be the ID of a image or a :class:`~openstack.image.v2.image.Image` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -1064,7 +1064,7 @@ def get_member(self, member, image): The value can be the ID of a image or a :class:`~openstack.image.v2.image.Image` instance. :returns: One :class:`~openstack.image.v2.member.Member` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ member_id = resource.Resource._get_id(member) @@ -1136,7 +1136,7 @@ def delete_metadef_namespace(self, metadef_namespace, ignore_missing=True): :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` instance. :param bool ignore_missing: When set to ``False``, - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the metadef namespace does not exist. :returns: ``None`` """ @@ -1159,7 +1159,7 @@ def get_metadef_namespace(self, metadef_namespace): :returns: One :class:`~~openstack.image.v2.metadef_namespace.MetadefNamespace` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get( @@ -1172,7 +1172,7 @@ def metadef_namespaces(self, **query): :returns: A generator object of metadef namespaces :rtype: :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._list(_metadef_namespace.MetadefNamespace, **query) @@ -1234,7 +1234,7 @@ def get_metadef_object(self, metadef_object, namespace): :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` instance. :returns: One :class:`~openstack.image.v2.metadef_object.MetadefObject` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ object_name = resource.Resource._get_id(metadef_object) @@ -1254,7 +1254,7 @@ def metadef_objects(self, namespace): instance. :returns: One :class:`~openstack.image.v2.metadef_object.MetadefObject` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ namespace_name = resource.Resource._get_id(namespace) @@ -1276,7 +1276,7 @@ def update_metadef_object(self, metadef_object, namespace, **attrs): a :class:`~openstack.image.v2.metadef_object.MetadefObject` :returns: One :class:`~openstack.image.v2.metadef_object.MetadefObject` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ namespace_name = resource.Resource._get_id(namespace) @@ -1301,7 +1301,7 @@ def delete_metadef_object(self, metadef_object, namespace, **attrs): a :class:`~openstack.image.v2.metadef_object.MetadefObject` :returns: ``None`` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ namespace_name = resource.Resource._get_id(namespace) @@ -1320,7 +1320,7 @@ def delete_all_metadef_objects(self, namespace): :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` instance. :returns: ``None`` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ namespace = self._get_resource( @@ -1335,7 +1335,7 @@ def metadef_resource_types(self, **query): :return: A generator object of metadef resource types :rtype: :class:`~openstack.image.v2.metadef_resource_type.MetadefResourceType` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._list(_metadef_resource_type.MetadefResourceType, **query) @@ -1382,7 +1382,7 @@ def delete_metadef_resource_type_association( :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` instance :param bool ignore_missing: When set to ``False``, - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the metadef resource type association does not exist. :returns: ``None`` """ @@ -1404,7 +1404,7 @@ def metadef_resource_type_associations(self, metadef_namespace, **query): :return: A generator object of metadef resource type associations :rtype: :class:`~openstack.image.v2.metadef_resource_type.MetadefResourceTypeAssociation` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ namespace_name = resource.Resource._get_id(metadef_namespace) @@ -1477,7 +1477,7 @@ def delete_metadef_property( :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` instance :param bool ignore_missing: When set to - ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be + ``False`` :class:`~openstack.exceptions.NotFoundException` will be raised when the instance does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent instance. @@ -1529,7 +1529,7 @@ def get_metadef_property( :returns: One :class:`~~openstack.image.v2.metadef_property.MetadefProperty` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ namespace_name = resource.Resource._get_id(metadef_namespace) @@ -1549,7 +1549,7 @@ def delete_all_metadef_properties(self, metadef_namespace): instance. :returns: ``None`` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ namespace = self._get_resource( @@ -1562,7 +1562,7 @@ def get_images_schema(self): """Get images schema :returns: One :class:`~openstack.image.v2.schema.Schema` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get( @@ -1575,7 +1575,7 @@ def get_image_schema(self): """Get single image schema :returns: One :class:`~openstack.image.v2.schema.Schema` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get( @@ -1588,7 +1588,7 @@ def get_members_schema(self): """Get image members schema :returns: One :class:`~openstack.image.v2.schema.Schema` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get( @@ -1601,7 +1601,7 @@ def get_member_schema(self): """Get image member schema :returns: One :class:`~openstack.image.v2.schema.Schema` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get( @@ -1614,7 +1614,7 @@ def get_tasks_schema(self): """Get image tasks schema :returns: One :class:`~openstack.image.v2.schema.Schema` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get( @@ -1627,7 +1627,7 @@ def get_task_schema(self): """Get image task schema :returns: One :class:`~openstack.image.v2.schema.Schema` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get( @@ -1640,7 +1640,7 @@ def get_metadef_namespace_schema(self): """Get metadata definition namespace schema :returns: One :class:`~openstack.image.v2.metadef_schema.MetadefSchema` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get( @@ -1653,7 +1653,7 @@ def get_metadef_namespaces_schema(self): """Get metadata definition namespaces schema :returns: One :class:`~openstack.image.v2.metadef_schema.MetadefSchema` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get( @@ -1666,7 +1666,7 @@ def get_metadef_resource_type_schema(self): """Get metadata definition resource type association schema :returns: One :class:`~openstack.image.v2.metadef_schema.MetadefSchema` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get( @@ -1679,7 +1679,7 @@ def get_metadef_resource_types_schema(self): """Get metadata definition resource type associations schema :returns: One :class:`~openstack.image.v2.metadef_schema.MetadefSchema` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get( @@ -1692,7 +1692,7 @@ def get_metadef_object_schema(self): """Get metadata definition object schema :returns: One :class:`~openstack.image.v2.metadef_schema.MetadefSchema` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get( @@ -1705,7 +1705,7 @@ def get_metadef_objects_schema(self): """Get metadata definition objects schema :returns: One :class:`~openstack.image.v2.metadef_schema.MetadefSchema` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get( @@ -1718,7 +1718,7 @@ def get_metadef_property_schema(self): """Get metadata definition property schema :returns: One :class:`~openstack.image.v2.metadef_schema.MetadefSchema` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get( @@ -1731,7 +1731,7 @@ def get_metadef_properties_schema(self): """Get metadata definition properties schema :returns: One :class:`~openstack.image.v2.metadef_schema.MetadefSchema` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get( @@ -1744,7 +1744,7 @@ def get_metadef_tag_schema(self): """Get metadata definition tag schema :returns: One :class:`~openstack.image.v2.metadef_schema.MetadefSchema` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get( @@ -1757,7 +1757,7 @@ def get_metadef_tags_schema(self): """Get metadata definition tags schema :returns: One :class:`~openstack.image.v2.metadef_schema.MetadefSchema` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get( @@ -1785,7 +1785,7 @@ def get_task(self, task): :class:`~openstack.image.v2.task.Task` instance. :returns: One :class:`~openstack.image.v2.task.Task` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_task.Task, task) @@ -1895,7 +1895,7 @@ def get_import_info(self): """Get a info about image constraints :returns: One :class:`~openstack.image.v2.service_info.Import` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_si.Import, requires_id=False) diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index ec1da5e5f..3d1332c55 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -418,6 +418,6 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): if ignore_missing: return None - raise exceptions.ResourceNotFound( + raise exceptions.NotFoundException( f"No {cls.__name__} found for {name_or_id}" ) diff --git a/openstack/image/v2/service_info.py b/openstack/image/v2/service_info.py index 1b27b5143..24f82c777 100644 --- a/openstack/image/v2/service_info.py +++ b/openstack/image/v2/service_info.py @@ -47,7 +47,7 @@ def delete_image(self, session, image, *, ignore_missing=False): :class:`~openstack.image.v2.image.Image` instance. :returns: The result of the ``delete`` if resource found, else None. - :raises: :class:`~openstack.exceptions.ResourceNotFound` when + :raises: :class:`~openstack.exceptions.NotFoundException` when ignore_missing if ``False`` and a nonexistent resource is attempted to be deleted. """ @@ -57,7 +57,7 @@ def delete_image(self, session, image, *, ignore_missing=False): try: response = session.delete(url) exceptions.raise_from_response(response) - except exceptions.ResourceNotFound: + except exceptions.NotFoundException: if ignore_missing: return None raise diff --git a/openstack/instance_ha/v1/_proxy.py b/openstack/instance_ha/v1/_proxy.py index 11081e5ea..2a073709e 100644 --- a/openstack/instance_ha/v1/_proxy.py +++ b/openstack/instance_ha/v1/_proxy.py @@ -51,7 +51,7 @@ def get_notification(self, notification): instance. :returns: One :class:`~masakariclient.sdk.ha.v1.notification.Notification` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_notification.Notification, notification) @@ -82,7 +82,7 @@ def get_segment(self, segment): :param segment: The value can be the ID of a segment or a :class:`~masakariclient.sdk.ha.v1.segment.Segment` instance. :returns: One :class:`~masakariclient.sdk.ha.v1.segment.Segment` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_segment.Segment, segment) @@ -118,7 +118,7 @@ def delete_segment(self, segment, ignore_missing=True): The value can be either the ID of a segment or a :class:`~masakariclient.sdk.ha.v1.segment.Segment` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the segment does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent segment. @@ -159,7 +159,7 @@ def get_host(self, host, segment_id=None): `~masakariclient.sdk.ha.v1.host.Host` instance. :returns: One :class:`~masakariclient.sdk.ha.v1.host.Host` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. :raises: :class:`~openstack.exceptions.InvalidRequest` when segment_id is None. @@ -179,7 +179,7 @@ def update_host(self, host, segment_id, **attrs): :param dict attrs: The attributes to update on the host represented. :returns: The updated host - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. :raises: :class:`~openstack.exceptions.InvalidRequest` when segment_id is None. @@ -196,13 +196,13 @@ def delete_host(self, host, segment_id=None, ignore_missing=True): :param host: The value can be the ID of a host or a :class: `~masakariclient.sdk.ha.v1.host.Host` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the host does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent host. :returns: ``None`` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. :raises: :class:`~openstack.exceptions.InvalidRequest` when segment_id is None. @@ -246,7 +246,7 @@ def get_vmove(self, vmove, notification): a :class: `~masakariclient.sdk.ha.v1.notification.Notification` instance. :returns: one 'VMove' resource class. - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. :raises: :class:`~openstack.exceptions.InvalidRequest` when notification_id is None. diff --git a/openstack/key_manager/v1/_proxy.py b/openstack/key_manager/v1/_proxy.py index e3f5ee9cd..f1240be11 100644 --- a/openstack/key_manager/v1/_proxy.py +++ b/openstack/key_manager/v1/_proxy.py @@ -42,7 +42,7 @@ def delete_container(self, container, ignore_missing=True): :class:`~openstack.key_manager.v1.container.Container` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the container does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent container. @@ -58,7 +58,7 @@ def find_container(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a container. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -77,7 +77,7 @@ def get_container(self, container): instance. :returns: One :class:`~openstack.key_manager.v1.container.Container` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_container.Container, container) @@ -125,7 +125,7 @@ def delete_order(self, order, ignore_missing=True): :class:`~openstack.key_manager.v1.order.Order` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the order does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent order. @@ -139,7 +139,7 @@ def find_order(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a order. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -157,7 +157,7 @@ def get_order(self, order): instance. :returns: One :class:`~openstack.key_manager.v1.order.Order` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_order.Order, order) @@ -205,7 +205,7 @@ def delete_secret(self, secret, ignore_missing=True): :class:`~openstack.key_manager.v1.secret.Secret` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the secret does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent secret. @@ -219,7 +219,7 @@ def find_secret(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a secret. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -238,7 +238,7 @@ def get_secret(self, secret): instance. :returns: One :class:`~openstack.key_manager.v1.secret.Secret` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_secret.Secret, secret) diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index 4e95023e1..28cebdbb4 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -102,7 +102,7 @@ def delete_load_balancer( :class:`~openstack.load_balancer.v2.load_balancer.LoadBalancer` instance :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the load balancer does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent load balancer. @@ -122,7 +122,7 @@ def find_load_balancer(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a load balancer :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the load balancer does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent load balancer. @@ -216,7 +216,7 @@ def delete_listener(self, listener, ignore_missing=True): :param listener: The value can be either the ID of a listener or a :class:`~openstack.load_balancer.v2.listener.Listener` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the listner does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent listener. @@ -232,7 +232,7 @@ def find_listener(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a listener. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -252,7 +252,7 @@ def get_listener(self, listener): instance. :returns: One :class:`~openstack.load_balancer.v2.listener.Listener` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_listener.Listener, listener) @@ -266,7 +266,7 @@ def get_listener_statistics(self, listener): :returns: One :class:`~openstack.load_balancer.v2.listener.ListenerStats` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get( @@ -335,7 +335,7 @@ def delete_pool(self, pool, ignore_missing=True): :class:`~openstack.load_balancer.v2.pool.Pool` instance :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the pool does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent pool. @@ -349,7 +349,7 @@ def find_pool(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a pool :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the pool does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent pool. @@ -400,7 +400,7 @@ def delete_member(self, member, pool, ignore_missing=True): :class:`~openstack.load_balancer.v2.pool.Pool` instance that the member belongs to. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the member does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent member. @@ -423,7 +423,7 @@ def find_member(self, name_or_id, pool, ignore_missing=True): :class:`~openstack.load_balancer.v2.pool.Pool` instance that the member belongs to. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -450,7 +450,7 @@ def get_member(self, member, pool): that the member belongs to. :returns: One :class:`~openstack.load_balancer.v2.member.Member` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ poolobj = self._get_resource(_pool.Pool, pool) @@ -496,7 +496,7 @@ def find_health_monitor(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a health monitor :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the health monitor does not exist. When set to ``True``, no exception will be set when attempting to find a nonexistent health monitor. @@ -507,7 +507,7 @@ def find_health_monitor(self, name_or_id, ignore_missing=True): :raises: :class:`openstack.exceptions.DuplicateResource` if more than one resource is found for this request. - :raises: :class:`openstack.exceptions.ResourceNotFound` if nothing + :raises: :class:`openstack.exceptions.NotFoundException` if nothing is found and ignore_missing is ``False``. """ return self._find( @@ -565,7 +565,7 @@ def delete_health_monitor(self, healthmonitor, ignore_missing=True): :class:`~openstack.load_balancer.v2.healthmonitor.HealthMonitor` instance :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the healthmonitor does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent healthmonitor. @@ -610,7 +610,7 @@ def delete_l7_policy(self, l7_policy, ignore_missing=True): :param l7_policy: The value can be either the ID of a l7policy or a :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the l7policy does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent l7policy. @@ -626,7 +626,7 @@ def find_l7_policy(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a l7policy. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -646,7 +646,7 @@ def get_l7_policy(self, l7_policy): instance. :returns: One :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_l7policy.L7Policy, l7_policy) @@ -703,7 +703,7 @@ def delete_l7_rule(self, l7rule, l7_policy, ignore_missing=True): :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` instance that the l7rule belongs to. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the l7rule does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent l7rule. @@ -726,7 +726,7 @@ def find_l7_rule(self, name_or_id, l7_policy, ignore_missing=True): :class:`~openstack.load_balancer.v2.l7_policy.L7Policy` instance that the l7rule belongs to. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -753,7 +753,7 @@ def get_l7_rule(self, l7rule, l7_policy): instance that the l7rule belongs to. :returns: One :class:`~openstack.load_balancer.v2.l7_rule.L7Rule` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ l7policyobj = self._get_resource(_l7policy.L7Policy, l7_policy) @@ -815,7 +815,7 @@ def get_quota(self, quota): ID for the quota. :returns: One :class:`~openstack.load_balancer.v2.quota.Quota` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_quota.Quota, quota) @@ -850,7 +850,7 @@ def delete_quota(self, quota, ignore_missing=True): instance. The ID of a quota is the same as the project ID for the quota. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when quota does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent quota. @@ -914,7 +914,7 @@ def delete_flavor_profile(self, flavor_profile, ignore_missing=True): :class:`~openstack.load_balancer.v2.flavor_profile.FlavorProfile` instance :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the flavor profile does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent flavor profile. @@ -932,7 +932,7 @@ def find_flavor_profile(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a flavor profile :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the flavor profile does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent flavor profile. @@ -998,7 +998,7 @@ def delete_flavor(self, flavor, ignore_missing=True): :param flavor: The flavorcan be either the ID or a :class:`~openstack.load_balancer.v2.flavor.Flavor` instance :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the flavor does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent flavor. @@ -1012,7 +1012,7 @@ def find_flavor(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a flavor :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the flavor does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent flavor. @@ -1059,7 +1059,7 @@ def find_amphora(self, amphora_id, ignore_missing=True): :param amphora_id: The ID of a amphora :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the amphora does not exist. When set to ``True``, no exception will be set when attempting to find a nonexistent amphora. @@ -1138,7 +1138,7 @@ def delete_availability_zone_profile( :class:`~openstack.load_balancer.v2.availability_zone_profile.AvailabilityZoneProfile` instance :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the availability zone profile does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent availability zone profile. @@ -1156,7 +1156,7 @@ def find_availability_zone_profile(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a availability zone profile :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the availability zone profile does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent availability zone profile. @@ -1232,7 +1232,7 @@ def delete_availability_zone(self, availability_zone, ignore_missing=True): :class:`~openstack.load_balancer.v2.availability_zone.AvailabilityZone` instance :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the availability zone does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent availability zone. @@ -1250,7 +1250,7 @@ def find_availability_zone(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a availability zone :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the availability zone does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent availability zone. diff --git a/openstack/message/v2/_proxy.py b/openstack/message/v2/_proxy.py index 0bb619455..4560ace5b 100644 --- a/openstack/message/v2/_proxy.py +++ b/openstack/message/v2/_proxy.py @@ -45,7 +45,7 @@ def get_queue(self, queue): :class:`~openstack.message.v2.queue.Queue` instance. :returns: One :class:`~openstack.message.v2.queue.Queue` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no queue matching the name could be found. """ return self._get(_queue.Queue, queue) @@ -73,7 +73,7 @@ def delete_queue(self, value, ignore_missing=True): :param value: The value can be either the name of a queue or a :class:`~openstack.message.v2.queue.Queue` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the queue does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent queue. @@ -127,7 +127,7 @@ def get_message(self, queue_name, message): :class:`~openstack.message.v2.message.Message` instance. :returns: One :class:`~openstack.message.v2.message.Message` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no message matching the criteria could be found. """ message = self._get_resource( @@ -148,7 +148,7 @@ def delete_message( the claim seizing the message. If None, the message has not been claimed. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the message does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent message. @@ -206,7 +206,7 @@ def get_subscription(self, queue_name, subscription): :class:`~openstack.message.v2.subscription.Subscription` instance. :returns: One :class:`~openstack.message.v2.subscription.Subscription` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no subscription matching the criteria could be found. """ subscription = self._get_resource( @@ -223,7 +223,7 @@ def delete_subscription(self, queue_name, value, ignore_missing=True): :class:`~openstack.message.v2.subscription.Subscription` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the subscription does not exist. When set to ``True``, no exception will be thrown when attempting to delete a nonexistent subscription. @@ -260,7 +260,7 @@ def get_claim(self, queue_name, claim): :class:`~openstack.message.v2.claim.Claim` instance. :returns: One :class:`~openstack.message.v2.claim.Claim` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no claim matching the criteria could be found. """ return self._get(_claim.Claim, claim, queue_name=queue_name) @@ -289,7 +289,7 @@ def delete_claim(self, queue_name, claim, ignore_missing=True): :param claim: The value can be either the ID of a claim or a :class:`~openstack.message.v2.claim.Claim` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the claim does not exist. When set to ``True``, no exception will be thrown when attempting to delete a nonexistent claim. diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 058d3dcd8..f69c8e0b5 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -216,7 +216,7 @@ def _delete( try: rv = res.delete(self, if_revision=if_revision) - except exceptions.ResourceNotFound: + except exceptions.NotFoundException: if ignore_missing: return None raise @@ -243,7 +243,7 @@ def delete_address_group(self, address_group, ignore_missing=True): a :class:`~openstack.network.v2.address_group.AddressGroup` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will + :class:`~openstack.exceptions.NotFoundException` will be raised when the address group does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent address group. @@ -261,7 +261,7 @@ def find_address_group(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of an address group. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -284,7 +284,7 @@ def get_address_group(self, address_group): :class:`~openstack.network.v2.address_group.AddressGroup` instance. :returns: One :class:`~openstack.network.v2.address_group.AddressGroup` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_address_group.AddressGroup, address_group) @@ -365,7 +365,7 @@ def delete_address_scope(self, address_scope, ignore_missing=True): a :class:`~openstack.network.v2.address_scope.AddressScope` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the address scope does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent address scope. @@ -383,7 +383,7 @@ def find_address_scope(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of an address scope. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -406,7 +406,7 @@ def get_address_scope(self, address_scope): :class:`~openstack.network.v2.address_scope.AddressScope` instance. :returns: One :class:`~openstack.network.v2.address_scope.AddressScope` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_address_scope.AddressScope, address_scope) @@ -469,7 +469,7 @@ def delete_agent(self, agent, ignore_missing=True): :param agent: The value can be the ID of a agent or a :class:`~openstack.network.v2.agent.Agent` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the agent does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent agent. @@ -486,7 +486,7 @@ def get_agent(self, agent): :returns: One :class:`~openstack.network.v2.agent.Agent` :rtype: :class:`~openstack.network.v2.agent.Agent` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_agent.Agent, agent) @@ -582,7 +582,7 @@ def delete_auto_allocated_topology( :param project: The value is the ID or name of a project :param ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the topology does not exist. When set to ``True``, no exception will be raised when attempting to delete nonexistant topology @@ -761,7 +761,7 @@ def delete_bgpvpn(self, bgpvpn, ignore_missing=True): :param bgpvpn: The value can be either the ID of a bgpvpn or a :class:`~openstack.network.v2.bgpvpn.BgpVpn` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the BGPVPN does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent BGPVPN. @@ -775,7 +775,7 @@ def find_bgpvpn(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of a BGPVPN. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -795,7 +795,7 @@ def get_bgpvpn(self, bgpvpn): :class:`~openstack.network.v2.bgpvpn.BgpVpn` instance. :returns: One :class:`~openstack.network.v2.bgpvpn.BgpVpn` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_bgpvpn.BgpVpn, bgpvpn) @@ -857,7 +857,7 @@ def delete_bgpvpn_network_association( a :class:`~openstack.network.v2.bgpvpn_network_association. BgpVpnNetworkAssociation` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the BgpVpnNetworkAssociation does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent BgpVpnNetworkAssociation. @@ -884,7 +884,7 @@ def get_bgpvpn_network_association(self, bgpvpn, net_association): :returns: One :class:`~openstack.network.v2. bgpvpn_network_associaition.BgpVpnNetworkAssociation` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) @@ -946,7 +946,7 @@ def delete_bgpvpn_port_association( a :class:`~openstack.network.v2.bgpvpn_port_association. BgpVpnPortAssociation` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the BgpVpnPortAssociation does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent BgpVpnPortAssociation. @@ -969,7 +969,7 @@ def find_bgpvpn_port_association( :param name_or_id: The name or ID of a BgpVpnNetworkAssociation. :param bgpvpn_id: The value can be the ID of a BGPVPN. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -998,7 +998,7 @@ def get_bgpvpn_port_association(self, bgpvpn, port_association): :returns: One :class:`~openstack.network.v2. bgpvpn_port_associaition.BgpVpnPortAssociation` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) @@ -1085,7 +1085,7 @@ def delete_bgpvpn_router_association( a :class:`~openstack.network.v2.bgpvpn_router_association. BgpVpnRouterAssociation` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the BgpVpnRouterAssociation does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent BgpVpnRouterAsociation. @@ -1112,7 +1112,7 @@ def get_bgpvpn_router_association(self, bgpvpn, router_association): :returns: One :class:`~openstack.network.v2. bgpvpn_router_associaition.BgpVpnRouterAssociation` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ bgpvpn_res = self._get_resource(_bgpvpn.BgpVpn, bgpvpn) @@ -1166,7 +1166,7 @@ def find_extension(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of a extension. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -1213,7 +1213,7 @@ def delete_flavor(self, flavor, ignore_missing=True): The value can be either the ID of a flavor or a :class:`~openstack.network.v2.flavor.Flavor` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the flavor does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent flavor. @@ -1227,7 +1227,7 @@ def find_flavor(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of a flavor. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -1247,7 +1247,7 @@ def get_flavor(self, flavor): :class:`~openstack.network.v2.flavor.Flavor` instance. :returns: One :class:`~openstack.network.v2.flavor.Flavor` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_flavor.Flavor, flavor) @@ -1343,7 +1343,7 @@ def delete_local_ip(self, local_ip, ignore_missing=True, if_revision=None): :class:`~openstack.network.v2.local_ip.LocalIP` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the local ip does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent ip. @@ -1364,7 +1364,7 @@ def find_local_ip(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of an local IP. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -1388,7 +1388,7 @@ def get_local_ip(self, local_ip): instance. :returns: One :class:`~openstack.network.v2.local_ip.LocalIP` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_local_ip.LocalIP, local_ip) @@ -1465,7 +1465,7 @@ def delete_local_ip_association( `~openstack.network.v2.local_ip_association.LocalIPAssociation` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the local ip association does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent ip. @@ -1493,7 +1493,7 @@ def find_local_ip_association( :class:`~openstack.network.v2.local_ip.LocalIP` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -1525,7 +1525,7 @@ def get_local_ip_association(self, local_ip_association, local_ip): :returns: One :class:`~openstack.network.v2.local_ip_association.LocalIPAssociation` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ local_ip = self._get_resource(_local_ip.LocalIP, local_ip) @@ -1579,7 +1579,7 @@ def delete_ip(self, floating_ip, ignore_missing=True, if_revision=None): or a :class:`~openstack.network.v2.floating_ip.FloatingIP` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the floating ip does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent ip. @@ -1608,7 +1608,7 @@ def find_ip(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of an IP. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -1632,7 +1632,7 @@ def get_ip(self, floating_ip): instance. :returns: One :class:`~openstack.network.v2.floating_ip.FloatingIP` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_floating_ip.FloatingIP, floating_ip) @@ -1707,7 +1707,7 @@ def get_port_forwarding(self, port_forwarding, floating_ip): :returns: One :class:`~openstack.network.v2.port_forwarding.PortForwarding` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ floating_ip = self._get_resource(_floating_ip.FloatingIP, floating_ip) @@ -1727,7 +1727,7 @@ def find_port_forwarding( :class:`~openstack.network.v2.floating_ip.FloatingIP` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -1758,7 +1758,7 @@ def delete_port_forwarding( :class:`~openstack.network.v2.floating_ip.FloatingIP` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the floating ip does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent ip. @@ -1837,7 +1837,7 @@ def delete_health_monitor(self, health_monitor, ignore_missing=True): :class:`~openstack.network.v2.health_monitor.HealthMonitor` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the health monitor does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent health monitor. @@ -1855,7 +1855,7 @@ def find_health_monitor(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of a health monitor. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -1881,7 +1881,7 @@ def get_health_monitor(self, health_monitor): :returns: One :class:`~openstack.network.v2.health_monitor.HealthMonitor` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_health_monitor.HealthMonitor, health_monitor) @@ -1948,7 +1948,7 @@ def delete_listener(self, listener, ignore_missing=True): :param listener: The value can be either the ID of a listner or a :class:`~openstack.network.v2.listener.Listener` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the listner does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent listener. @@ -1964,7 +1964,7 @@ def find_listener(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of a listener. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -1987,7 +1987,7 @@ def get_listener(self, listener): instance. :returns: One :class:`~openstack.network.v2.listener.Listener` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_listener.Listener, listener) @@ -2048,7 +2048,7 @@ def delete_load_balancer(self, load_balancer, ignore_missing=True): :class:`~openstack.network.v2.load_balancer.LoadBalancer` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the load balancer does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent load balancer. @@ -2066,7 +2066,7 @@ def find_load_balancer(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of a load balancer. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -2090,7 +2090,7 @@ def get_load_balancer(self, load_balancer): instance. :returns: One :class:`~openstack.network.v2.load_balancer.LoadBalancer` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_load_balancer.LoadBalancer, load_balancer) @@ -2142,7 +2142,7 @@ def delete_metering_label(self, metering_label, ignore_missing=True): :class:`~openstack.network.v2.metering_label.MeteringLabel` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the metering label does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent metering label. @@ -2160,7 +2160,7 @@ def find_metering_label(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of a metering label. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -2186,7 +2186,7 @@ def get_metering_label(self, metering_label): :returns: One :class:`~openstack.network.v2.metering_label.MeteringLabel` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_metering_label.MeteringLabel, metering_label) @@ -2249,7 +2249,7 @@ def delete_metering_label_rule( :class:`~openstack.network.v2.metering_label_rule.MeteringLabelRule` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised + :class:`~openstack.exceptions.NotFoundException` will be raised when the metering label rule does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent metering label rule. @@ -2269,7 +2269,7 @@ def find_metering_label_rule( :param name_or_id: The name or ID of a metering label rule. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -2296,7 +2296,7 @@ def get_metering_label_rule(self, metering_label_rule): :returns: One :class:`~openstack.network.v2.metering_label_rule.MeteringLabelRule` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get( @@ -2363,7 +2363,7 @@ def delete_network(self, network, ignore_missing=True, if_revision=None): The value can be either the ID of a network or a :class:`~openstack.network.v2.network.Network` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the network does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent network. @@ -2384,7 +2384,7 @@ def find_network(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of a network. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -2407,7 +2407,7 @@ def get_network(self, network): :class:`~openstack.network.v2.network.Network` instance. :returns: One :class:`~openstack.network.v2.network.Network` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_network.Network, network) @@ -2464,7 +2464,7 @@ def find_network_ip_availability( :param name_or_id: The name or ID of a network. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -2490,7 +2490,7 @@ def get_network_ip_availability(self, network): :returns: One :class:`~openstack.network.v2.network_ip_availability.NetworkIPAvailability` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get( @@ -2544,7 +2544,7 @@ def delete_network_segment_range( :class:`~openstack.network.v2.network_segment_range.NetworkSegmentRange` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the network segment range does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent network segment range. @@ -2564,7 +2564,7 @@ def find_network_segment_range( :param name_or_id: The name or ID of a network segment range. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -2591,7 +2591,7 @@ def get_network_segment_range(self, network_segment_range): :returns: One :class:`~openstack.network.v2._network_segment_range.NetworkSegmentRange` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get( @@ -2667,7 +2667,7 @@ def delete_pool(self, pool, ignore_missing=True): :param pool: The value can be either the ID of a pool or a :class:`~openstack.network.v2.pool.Pool` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the pool does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent pool. @@ -2681,7 +2681,7 @@ def find_pool(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of a pool. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -2700,7 +2700,7 @@ def get_pool(self, pool): :class:`~openstack.network.v2.pool.Pool` instance. :returns: One :class:`~openstack.network.v2.pool.Pool` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_pool.Pool, pool) @@ -2772,7 +2772,7 @@ def delete_pool_member(self, pool_member, pool, ignore_missing=True): :class:`~openstack.network.v2.pool.Pool` instance that the member belongs to. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the pool member does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent pool member. @@ -2795,7 +2795,7 @@ def find_pool_member(self, name_or_id, pool, ignore_missing=True, **query): :class:`~openstack.network.v2.pool.Pool` instance that the member belongs to. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -2824,7 +2824,7 @@ def get_pool_member(self, pool_member, pool): the member belongs to. :returns: One :class:`~openstack.network.v2.pool_member.PoolMember` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ poolobj = self._get_resource(_pool.Pool, pool) @@ -2909,7 +2909,7 @@ def delete_port(self, port, ignore_missing=True, if_revision=None): :param port: The value can be either the ID of a port or a :class:`~openstack.network.v2.port.Port` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the port does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent port. @@ -2930,7 +2930,7 @@ def find_port(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of a port. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -2949,7 +2949,7 @@ def get_port(self, port): :class:`~openstack.network.v2.port.Port` instance. :returns: One :class:`~openstack.network.v2.port.Port` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_port.Port, port) @@ -3046,7 +3046,7 @@ def delete_qos_bandwidth_limit_rule( rule belongs or a :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent bandwidth limit rule. @@ -3071,7 +3071,7 @@ def find_qos_bandwidth_limit_rule( rule belongs or a :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -3102,7 +3102,7 @@ def get_qos_bandwidth_limit_rule(self, qos_rule, qos_policy): :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :returns: One :class:`~openstack.network.v2.qos_bandwidth_limit_rule.QoSBandwidthLimitRule` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) @@ -3196,7 +3196,7 @@ def delete_qos_dscp_marking_rule( rule belongs or a :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent minimum bandwidth rule. @@ -3221,7 +3221,7 @@ def find_qos_dscp_marking_rule( rule belongs or a :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -3252,7 +3252,7 @@ def get_qos_dscp_marking_rule(self, qos_rule, qos_policy): :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :returns: One :class:`~openstack.network.v2.qos_dscp_marking_rule.QoSDSCPMarkingRule` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) @@ -3341,7 +3341,7 @@ def delete_qos_minimum_bandwidth_rule( rule belongs or a :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent minimum bandwidth rule. @@ -3366,7 +3366,7 @@ def find_qos_minimum_bandwidth_rule( rule belongs or a :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -3399,7 +3399,7 @@ def get_qos_minimum_bandwidth_rule(self, qos_rule, qos_policy): :returns: One :class:`~openstack.network.v2.qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule` :raises: - :class:`~openstack.exceptions.ResourceNotFound` + :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) @@ -3487,7 +3487,7 @@ def delete_qos_minimum_packet_rate_rule( rule belongs or a :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent minimum packet rate rule. @@ -3512,7 +3512,7 @@ def find_qos_minimum_packet_rate_rule( rule belongs or a :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. :param dict query: Any additional parameters to be passed into @@ -3542,7 +3542,7 @@ def get_qos_minimum_packet_rate_rule(self, qos_rule, qos_policy): :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :returns: One :class:`~openstack.network.v2.qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) @@ -3617,7 +3617,7 @@ def delete_qos_policy(self, qos_policy, ignore_missing=True): :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the QoS policy does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent QoS policy. @@ -3633,7 +3633,7 @@ def find_qos_policy(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of a QoS policy. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -3657,7 +3657,7 @@ def get_qos_policy(self, qos_policy): instance. :returns: One :class:`~openstack.network.v2.qos_policy.QoSPolicy` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_qos_policy.QoSPolicy, qos_policy) @@ -3696,7 +3696,7 @@ def find_qos_rule_type(self, rule_type_name, ignore_missing=True): :param rule_type_name: The name of a QoS rule type. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -3718,7 +3718,7 @@ def get_qos_rule_type(self, qos_rule_type): instance. :returns: One :class:`~openstack.network.v2.qos_rule_type.QoSRuleType` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_qos_rule_type.QoSRuleType, qos_rule_type) @@ -3744,7 +3744,7 @@ def delete_quota(self, quota, ignore_missing=True): The ID of a quota is the same as the project ID for the quota. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when quota does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent quota. @@ -3764,7 +3764,7 @@ def get_quota(self, quota, details=False): be returned. :returns: One :class:`~openstack.network.v2.quota.Quota` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ if details: @@ -3785,7 +3785,7 @@ def get_quota_default(self, quota): as the project ID for the default quota. :returns: One :class:`~openstack.network.v2.quota.QuotaDefault` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ quota_obj = self._get_resource(_quota.Quota, quota) @@ -3838,7 +3838,7 @@ def delete_rbac_policy(self, rbac_policy, ignore_missing=True): :param rbac_policy: The value can be either the ID of a RBAC policy or a :class:`~openstack.network.v2.rbac_policy.RBACPolicy` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the RBAC policy does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent RBAC policy. @@ -3854,7 +3854,7 @@ def find_rbac_policy(self, rbac_policy, ignore_missing=True, **query): :param rbac_policy: The ID of a RBAC policy. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -3877,7 +3877,7 @@ def get_rbac_policy(self, rbac_policy): :class:`~openstack.network.v2.rbac_policy.RBACPolicy` instance. :returns: One :class:`~openstack.network.v2.rbac_policy.RBACPolicy` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_rbac_policy.RBACPolicy, rbac_policy) @@ -3931,7 +3931,7 @@ def delete_router(self, router, ignore_missing=True, if_revision=None): :param router: The value can be either the ID of a router or a :class:`~openstack.network.v2.router.Router` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the router does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent router. @@ -3952,7 +3952,7 @@ def find_router(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of a router. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -3971,7 +3971,7 @@ def get_router(self, router): :class:`~openstack.network.v2.router.Router` instance. :returns: One :class:`~openstack.network.v2.router.Router` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_router.Router, router) @@ -4212,7 +4212,7 @@ def get_ndp_proxy(self, ndp_proxy): :returns: One :class:`~openstack.network.v2.ndp_proxy.NDPProxy` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_ndp_proxy.NDPProxy, ndp_proxy) @@ -4222,7 +4222,7 @@ def find_ndp_proxy(self, ndp_proxy_id, ignore_missing=True, **query): :param ndp_proxy_id: The ID of a ndp proxy. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. :param dict query: Any additional parameters to be passed into @@ -4244,7 +4244,7 @@ def delete_ndp_proxy(self, ndp_proxy, ignore_missing=True): or a :class:`~openstack.network.v2.ndp_proxy.NDPProxy` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the router does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent ndp proxy. @@ -4302,7 +4302,7 @@ def delete_firewall_group(self, firewall_group, ignore_missing=True): :class:`~openstack.network.v2.firewall_group.FirewallGroup` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the firewall group does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent firewall group. @@ -4320,7 +4320,7 @@ def find_firewall_group(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of a firewall group. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -4345,7 +4345,7 @@ def get_firewall_group(self, firewall_group): :returns: One :class:`~openstack.network.v2.firewall_group.FirewallGroup` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_firewall_group.FirewallGroup, firewall_group) @@ -4410,7 +4410,7 @@ def delete_firewall_policy(self, firewall_policy, ignore_missing=True): :class:`~openstack.network.v2.firewall_policy.FirewallPolicy` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the firewall policy does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent firewall policy. @@ -4428,7 +4428,7 @@ def find_firewall_policy(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of a firewall policy. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -4455,7 +4455,7 @@ def get_firewall_policy(self, firewall_policy): :returns: One :class:`~openstack.network.v2.firewall_policy.FirewallPolicy` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_firewall_policy.FirewallPolicy, firewall_policy) @@ -4559,7 +4559,7 @@ def delete_firewall_rule(self, firewall_rule, ignore_missing=True): :class:`~openstack.network.v2.firewall_rule.FirewallRule` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the firewall rule does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent firewall rule. @@ -4577,7 +4577,7 @@ def find_firewall_rule(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of a firewall rule. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -4603,7 +4603,7 @@ def get_firewall_rule(self, firewall_rule): :returns: One :class:`~openstack.network.v2.firewall_rule.FirewallRule` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_firewall_rule.FirewallRule, firewall_rule) @@ -4676,7 +4676,7 @@ def delete_security_group( :class:`~openstack.network.v2.security_group.SecurityGroup` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the security group does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent security group. @@ -4697,7 +4697,7 @@ def find_security_group(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of a security group. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -4723,7 +4723,7 @@ def get_security_group(self, security_group): :returns: One :class:`~openstack.network.v2.security_group.SecurityGroup` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_security_group.SecurityGroup, security_group) @@ -4806,7 +4806,7 @@ def delete_security_group_rule( :class:`~openstack.network.v2.security_group_rule.SecurityGroupRule` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the security group rule does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent security group rule. @@ -4829,7 +4829,7 @@ def find_security_group_rule( :param str name_or_id: The ID of a security group rule. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -4856,7 +4856,7 @@ def get_security_group_rule(self, security_group_rule): :returns: :class:`~openstack.network.v2.security_group_rule.SecurityGroupRule` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get( @@ -4915,7 +4915,7 @@ def delete_default_security_group_rule( :class:`~openstack.network.v2.default_security_group_rule. DefaultSecurityGroupRule` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the defaul security group rule does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent default security group rule. @@ -4935,7 +4935,7 @@ def find_default_security_group_rule( :param str name_or_id: The ID of a default security group rule. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -4963,7 +4963,7 @@ def get_default_security_group_rule(self, default_security_group_rule): :returns: :class:`~openstack.network.v2.default_security_group_rule. DefaultSecurityGroupRule` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get( @@ -5012,7 +5012,7 @@ def delete_segment(self, segment, ignore_missing=True): :class:`~openstack.network.v2.segment.Segment` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the segment does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent segment. @@ -5026,7 +5026,7 @@ def find_segment(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of a segment. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -5049,7 +5049,7 @@ def get_segment(self, segment): instance. :returns: One :class:`~openstack.network.v2.segment.Segment` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_segment.Segment, segment) @@ -5118,7 +5118,7 @@ def delete_service_profile(self, service_profile, ignore_missing=True): :class:`~openstack.network.v2.service_profile.ServiceProfile` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the service profile does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent service profile. @@ -5136,7 +5136,7 @@ def find_service_profile(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of a service profile. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -5162,7 +5162,7 @@ def get_service_profile(self, service_profile): :returns: One :class:`~openstack.network.v2.service_profile.ServiceProfile` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_service_profile.ServiceProfile, service_profile) @@ -5217,7 +5217,7 @@ def delete_subnet(self, subnet, ignore_missing=True, if_revision=None): :param subnet: The value can be either the ID of a subnet or a :class:`~openstack.network.v2.subnet.Subnet` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the subnet does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent subnet. @@ -5238,7 +5238,7 @@ def find_subnet(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of a subnet. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -5257,7 +5257,7 @@ def get_subnet(self, subnet): :class:`~openstack.network.v2.subnet.Subnet` instance. :returns: One :class:`~openstack.network.v2.subnet.Subnet` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_subnet.Subnet, subnet) @@ -5321,7 +5321,7 @@ def delete_subnet_pool(self, subnet_pool, ignore_missing=True): :param subnet_pool: The value can be either the ID of a subnet pool or a :class:`~openstack.network.v2.subnet_pool.SubnetPool` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the subnet pool does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent subnet pool. @@ -5337,7 +5337,7 @@ def find_subnet_pool(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of a subnet pool. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -5360,7 +5360,7 @@ def get_subnet_pool(self, subnet_pool): :class:`~openstack.network.v2.subnet_pool.SubnetPool` instance. :returns: One :class:`~openstack.network.v2.subnet_pool.SubnetPool` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_subnet_pool.SubnetPool, subnet_pool) @@ -5449,7 +5449,7 @@ def find_trunk(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of a trunk. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -5470,7 +5470,7 @@ def get_trunk(self, trunk): :returns: One :class:`~openstack.network.v2.trunk.Trunk` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_trunk.Trunk, trunk) @@ -5565,7 +5565,7 @@ def delete_vpn_endpoint_group( :class:`~openstack.network.v2.vpn_endpoint_group.VpnEndpointGroup` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the vpn service does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent vpn service. @@ -5585,7 +5585,7 @@ def find_vpn_endpoint_group( :param name_or_id: The name or ID of a vpn service. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -5612,7 +5612,7 @@ def get_vpn_endpoint_group(self, vpn_endpoint_group): :returns: One :class:`~openstack.network.v2.vpn_endpoint_group.VpnEndpointGroup` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get( @@ -5671,7 +5671,7 @@ def find_vpn_ipsec_site_connection( :param name_or_id: The name or ID of an IPsec site connection. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -5698,7 +5698,7 @@ def get_vpn_ipsec_site_connection(self, ipsec_site_connection): :returns: One :class:`~openstack.network.v2.vpn_ipsec_site_connection.VpnIPSecSiteConnection` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get( @@ -5751,7 +5751,7 @@ def delete_vpn_ipsec_site_connection( instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the IPsec site connection does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent IPsec site connection. @@ -5782,7 +5782,7 @@ def find_vpn_ike_policy(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of an IKE policy. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -5808,7 +5808,7 @@ def get_vpn_ike_policy(self, ike_policy): :returns: One :class:`~openstack.network.v2.vpn_ike_policy.VpnIkePolicy` :rtype: :class:`~openstack.network.v2.ike_policy.VpnIkePolicy` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_ike_policy.VpnIkePolicy, ike_policy) @@ -5846,7 +5846,7 @@ def delete_vpn_ike_policy(self, ike_policy, ignore_missing=True): instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` + :class:`~openstack.exceptions.NotFoundException` will be raised when the ike policy does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent ike policy. @@ -5875,7 +5875,7 @@ def find_vpn_ipsec_policy(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of an IPsec policy. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -5902,7 +5902,7 @@ def get_vpn_ipsec_policy(self, ipsec_policy): :returns: One :class:`~openstack.network.v2.vpn_ipsec_policy.VpnIpsecPolicy` :rtype: :class:`~openstack.network.v2.ipsec_policy.VpnIpsecPolicy` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_ipsec_policy.VpnIpsecPolicy, ipsec_policy) @@ -5943,7 +5943,7 @@ def delete_vpn_ipsec_policy(self, ipsec_policy, ignore_missing=True): instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` + :class:`~openstack.exceptions.NotFoundException` will be raised when the IPsec policy does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent IPsec policy. @@ -5976,7 +5976,7 @@ def delete_vpn_service(self, vpn_service, ignore_missing=True): The value can be either the ID of a vpn service or a :class:`~openstack.network.v2.vpn_service.VpnService` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the vpn service does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent vpn service. @@ -5992,7 +5992,7 @@ def find_vpn_service(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of a vpn service. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -6017,7 +6017,7 @@ def get_vpn_service(self, vpn_service): :returns: One :class:`~openstack.network.v2.vpn_service.VpnService` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_vpn_service.VpnService, vpn_service) @@ -6079,7 +6079,7 @@ def delete_floating_ip_port_forwarding( :class:`~openstack.network.v2.port_forwarding.PortForwarding` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the floating ip does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent ip. @@ -6104,7 +6104,7 @@ def find_floating_ip_port_forwarding( :class:`~openstack.network.v2.floating_ip.FloatingIP` instance. :param port_forwarding_id: The ID of a port forwarding. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -6135,7 +6135,7 @@ def get_floating_ip_port_forwarding(self, floating_ip, port_forwarding): instance. :returns: One :class:`~openstack.network.v2.port_forwarding.PortForwarding` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ floatingip = self._get_resource(_floating_ip.FloatingIP, floating_ip) @@ -6237,7 +6237,7 @@ def get_conntrack_helper(self, conntrack_helper, router): :returns: One :class:`~openstack.network.v2.l3_conntrack_helper.ConntrackHelper` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ router = self._get_resource(_router.Router, router) @@ -6284,7 +6284,7 @@ def delete_conntrack_helper( :param router: The value can be the ID of a Router or a :class:`~openstack.network.v2.router.Router` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the floating ip does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent ip. @@ -6414,7 +6414,7 @@ def delete_sfc_flow_classifier(self, flow_classifier, ignore_missing=True): :class:`~openstack.network.v2.sfc_flow_classifier.SfcFlowClassifier` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the flow classifier does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent flow classifier. @@ -6434,7 +6434,7 @@ def find_sfc_flow_classifier( :param str name_or_id: The name or ID of an SFC flow classifier. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -6460,7 +6460,7 @@ def get_sfc_flow_classifier(self, flow_classifier): :returns: :class:`~openstack.network.v2.sfc_flow_classifier.SfcFlowClassifier` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get( @@ -6522,7 +6522,7 @@ def delete_sfc_port_chain(self, port_chain, ignore_missing=True): :class:`~openstack.network.v2.sfc_port_chain.SfcPortChain` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the port chain does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent port chain. @@ -6540,7 +6540,7 @@ def find_sfc_port_chain(self, name_or_id, ignore_missing=True, **query): :param str name_or_id: The name or ID of an SFC port chain. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -6567,7 +6567,7 @@ def get_sfc_port_chain(self, port_chain): :returns: :class:`~openstack.network.v2.sfc_port_chain.SfcPortchain` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_sfc_port_chain.SfcPortChain, port_chain) @@ -6622,7 +6622,7 @@ def delete_sfc_port_pair(self, port_pair, ignore_missing=True): :class:`~openstack.network.v2.sfc_port_pair.SfcPortPair` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the port pair does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent port pair. @@ -6640,7 +6640,7 @@ def find_sfc_port_pair(self, name_or_id, ignore_missing=True, **query): :param str name_or_id: The name or ID of an SFC port pair. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -6666,7 +6666,7 @@ def get_sfc_port_pair(self, port_pair): :returns: :class:`~openstack.network.v2.sfc_port_pair.SfcPortPair` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_sfc_port_pair.SfcPortPair, port_pair) @@ -6721,7 +6721,7 @@ def delete_sfc_port_pair_group(self, port_pair_group, ignore_missing=True): :class:`~openstack.network.v2.sfc_port_pair_group. SfcPortPairGroup` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the port pair group does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent port pair group. @@ -6741,7 +6741,7 @@ def find_sfc_port_pair_group( :param str name_or_id: The name or ID of an SFC port pair group. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -6768,7 +6768,7 @@ def get_sfc_port_pair_group(self, port_pair_group): :returns: :class:`~openstack.network.v2.sfc_port_pair_group.SfcPortPairGroup` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get( @@ -6828,7 +6828,7 @@ def delete_sfc_service_graph(self, service_graph, ignore_missing=True): :class:`~openstack.network.v2.sfc_service_graph.SfcServiceGraph` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the service graph does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent service graph. @@ -6846,7 +6846,7 @@ def find_sfc_service_graph(self, name_or_id, ignore_missing=True, **query): :param str name_or_id: The name or ID of an SFC service graph. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -6873,7 +6873,7 @@ def get_sfc_service_graph(self, service_graph): :returns: :class:`~openstack.network.v2.sfc_service_graph.SfcServiceGraph` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_sfc_sservice_graph.SfcServiceGraph, service_graph) diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index 708c9d5e5..8b661ff28 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -157,7 +157,7 @@ def delete_container(self, container, ignore_missing=True): :param container: The value can be either the name of a container or a :class:`~openstack.object_store.v1.container.Container` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the container does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent server. @@ -174,7 +174,7 @@ def get_container_metadata(self, container): :class:`~openstack.object_store.v1.container.Container` instance. :returns: One :class:`~openstack.object_store.v1.container.Container` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._head(_container.Container, container) @@ -279,7 +279,7 @@ def get_object( :returns: Instance of the :class:`~openstack.object_store.v1.obj.Object` objects. - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ container_name = self._get_container_name(obj=obj, container=container) @@ -323,7 +323,7 @@ def download_object(self, obj, container=None, **attrs): :param container: The value can be the name of a container or a :class:`~openstack.object_store.v1.container.Container` instance. - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ container_name = self._get_container_name(obj=obj, container=container) @@ -340,7 +340,7 @@ def stream_object(self, obj, container=None, chunk_size=1024, **attrs): :param container: The value can be the name of a container or a :class:`~openstack.object_store.v1.container.Container` instance. - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. :returns: An iterator that iterates over chunk_size bytes """ @@ -491,7 +491,7 @@ def delete_object(self, obj, ignore_missing=True, container=None): :param container: The value can be the ID of a container or a :class:`~openstack.object_store.v1.container.Container` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the object does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent server. @@ -515,7 +515,7 @@ def get_object_metadata(self, obj, container=None): :class:`~openstack.object_store.v1.container.Container` instance. :returns: One :class:`~openstack.object_store.v1.obj.Object` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ container_name = self._get_container_name(obj, container) diff --git a/openstack/object_store/v1/info.py b/openstack/object_store/v1/info.py index ad1e923c1..e71e25480 100644 --- a/openstack/object_store/v1/info.py +++ b/openstack/object_store/v1/info.py @@ -75,7 +75,7 @@ def fetch( :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if :data:`Resource.allow_fetch` is not set to ``True``. - :raises: :exc:`~openstack.exceptions.ResourceNotFound` if + :raises: :exc:`~openstack.exceptions.NotFoundException` if the resource was not found. """ if not self.allow_fetch: diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index 685f59ba2..696ae1ffa 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -130,7 +130,7 @@ def find_stack( :param name_or_id: The name or ID of a stack. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -162,7 +162,7 @@ def get_stack(self, stack, resolve_outputs=True): :param resolve_outputs: Whether stack should contain outputs resolved. :returns: One :class:`~openstack.orchestration.v1.stack.Stack` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_stack.Stack, stack, resolve_outputs=resolve_outputs) @@ -177,7 +177,7 @@ def update_stack(self, stack, preview=False, **attrs): :returns: The updated stack :rtype: :class:`~openstack.orchestration.v1.stack.Stack` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ res = self._get_resource(_stack.Stack, stack, **attrs) @@ -190,7 +190,7 @@ def delete_stack(self, stack, ignore_missing=True): :class:`~openstack.orchestration.v1.stack.Stack` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the stack does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent stack. @@ -232,7 +232,7 @@ def export_stack(self, stack): :param stack: The value can be the ID or a name or an instance of :class:`~openstack.orchestration.v1.stack.Stack` :returns: A dictionary containing the stack data. - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ if isinstance(stack, _stack.Stack): @@ -269,7 +269,7 @@ def get_stack_template(self, stack): :returns: One object of :class:`~openstack.orchestration.v1.stack_template.StackTemplate` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ if isinstance(stack, _stack.Stack): @@ -292,7 +292,7 @@ def get_stack_environment(self, stack): :returns: One object of :class:`~openstack.orchestration.v1.stack_environment.StackEnvironment` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ if isinstance(stack, _stack.Stack): @@ -315,7 +315,7 @@ def get_stack_files(self, stack): :returns: A dictionary containing the names and contents of all files used by the stack. - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when the stack cannot be found. """ if isinstance(stack, _stack.Stack): @@ -339,7 +339,7 @@ def resources(self, stack, **query): an exception is thrown. :rtype: A generator of :class:`~openstack.orchestration.v1.resource.Resource` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when the stack cannot be found. """ # first try treat the value as a stack object or an ID @@ -395,7 +395,7 @@ def delete_software_config(self, software_config, ignore_missing=True): config or an instance of :class:`~openstack.orchestration.v1.software_config.SoftwareConfig` :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the software config does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent software config. @@ -450,7 +450,7 @@ def delete_software_deployment( software deployment or an instance of :class:`~openstack.orchestration.v1.software_deployment.SoftwareDeployment` :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the software deployment does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent software deployment. diff --git a/openstack/orchestration/v1/stack.py b/openstack/orchestration/v1/stack.py index ef7230a17..2001ca383 100644 --- a/openstack/orchestration/v1/stack.py +++ b/openstack/orchestration/v1/stack.py @@ -242,7 +242,7 @@ def fetch( self._translate_response(response, **kwargs) if self and self.status in ['DELETE_COMPLETE', 'ADOPT_COMPLETE']: - raise exceptions.ResourceNotFound( + raise exceptions.NotFoundException( "No stack found for %s" % self.id ) return self @@ -256,7 +256,7 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): :param name_or_id: This resource's identifier, if needed by the request. The default is ``None``. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -269,7 +269,7 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): or None if nothing matches. :raises: :class:`openstack.exceptions.DuplicateResource` if more than one resource is found for this request. - :raises: :class:`openstack.exceptions.ResourceNotFound` if nothing + :raises: :class:`openstack.exceptions.NotFoundException` if nothing is found and ignore_missing is ``False``. """ session = cls._get_session(session) @@ -287,7 +287,7 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): if ignore_missing: return None - raise exceptions.ResourceNotFound( + raise exceptions.NotFoundException( f"No {cls.__name__} found for {name_or_id}" ) diff --git a/openstack/placement/v1/_proxy.py b/openstack/placement/v1/_proxy.py index 60768b5a5..80f64a510 100644 --- a/openstack/placement/v1/_proxy.py +++ b/openstack/placement/v1/_proxy.py @@ -48,7 +48,7 @@ class or an :class:`~openstack.placement.v1.resource_class.ResourceClass`, instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource class does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent resource class. @@ -90,7 +90,7 @@ class or an :returns: An instance of :class:`~openstack.placement.v1.resource_class.ResourceClass` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource class matching the criteria could be found. """ return self._get( @@ -130,7 +130,7 @@ def delete_resource_provider(self, resource_provider, ignore_missing=True): :class:`~openstack.placement.v1.resource_provider.ResourceProvider`, instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource provider does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent resource provider. @@ -172,7 +172,7 @@ def get_resource_provider(self, resource_provider): :returns: An instance of :class:`~openstack.placement.v1.resource_provider.ResourceProvider` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource provider matching the criteria could be found. """ return self._get( @@ -185,13 +185,13 @@ def find_resource_provider(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a resource provider. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. :returns: An instance of :class:`~openstack.placement.v1.resource_provider.ResourceProvider` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource provider matching the criteria could be found. """ return self._find( @@ -223,7 +223,7 @@ def get_resource_provider_aggregates(self, resource_provider): :returns: An instance of :class:`~openstack.placement.v1.resource_provider.ResourceProvider` with the ``aggregates`` attribute populated. - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource provider matching the criteria could be found. """ res = self._get_resource( @@ -245,7 +245,7 @@ def set_resource_provider_aggregates(self, resource_provider, *aggregates): :returns: An instance of :class:`~openstack.placement.v1.resource_provider.ResourceProvider` with the ``aggregates`` attribute populated with the updated value. - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource provider matching the criteria could be found. """ res = self._get_resource( @@ -305,7 +305,7 @@ def delete_resource_provider_inventory( instance. This value must be specified when ``resource_provider_inventory`` is an ID. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource provider inventory does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent resource provider inventory. @@ -379,7 +379,7 @@ def get_resource_provider_inventory( :returns: An instance of :class:`~openstack.placement.v1.resource_provider_inventory.ResourceProviderInventory` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource provider inventory matching the criteria could be found. """ resource_provider_id = self._get_uri_attribute( @@ -429,7 +429,7 @@ def delete_trait(self, trait, ignore_missing=True): :param trait: The value can be either the ID of a trait or an :class:`~openstack.placement.v1.trait.Trait`, instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource provider inventory does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent resource provider inventory. @@ -446,7 +446,7 @@ def get_trait(self, trait): :returns: An instance of :class:`~openstack.placement.v1.resource_provider_inventory.ResourceProviderInventory` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no trait matching the criteria could be found. """ return self._get(_trait.Trait, trait) diff --git a/openstack/proxy.py b/openstack/proxy.py index e11184af9..5768fc9f8 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -519,7 +519,7 @@ def _find( :param name_or_id: The name or ID of a resource to find. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -550,7 +550,7 @@ def _delete( resource or a :class:`~openstack.resource.Resource` subclass. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent resource. @@ -560,7 +560,7 @@ def _delete( :raises: ``ValueError`` if ``value`` is a :class:`~openstack.resource.Resource` that doesn't match the ``resource_type``. - :class:`~openstack.exceptions.ResourceNotFound` when + :class:`~openstack.exceptions.NotFoundException` when ignore_missing if ``False`` and a nonexistent resource is attempted to be deleted. """ @@ -568,7 +568,7 @@ def _delete( try: rv = res.delete(self) - except exceptions.ResourceNotFound: + except exceptions.NotFoundException: if ignore_missing: return None raise diff --git a/openstack/resource.py b/openstack/resource.py index 27db32702..979c1cc3f 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1680,7 +1680,7 @@ def fetch( :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if :data:`Resource.allow_fetch` is not set to ``True``. - :raises: :exc:`~openstack.exceptions.ResourceNotFound` if + :raises: :exc:`~openstack.exceptions.NotFoundException` if the resource was not found. """ if not self.allow_fetch: @@ -1721,7 +1721,7 @@ def head(self, session, base_path=None, *, microversion=None): :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if :data:`Resource.allow_head` is not set to ``True``. - :raises: :exc:`~openstack.exceptions.ResourceNotFound` if the resource + :raises: :exc:`~openstack.exceptions.NotFoundException` if the resource was not found. """ if not self.allow_head: @@ -1952,7 +1952,7 @@ def delete( :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if :data:`Resource.allow_commit` is not set to ``True``. - :raises: :exc:`~openstack.exceptions.ResourceNotFound` if + :raises: :exc:`~openstack.exceptions.NotFoundException` if the resource was not found. """ @@ -2316,9 +2316,9 @@ def find( :param name_or_id: This resource's identifier, if needed by the request. The default is ``None``. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when - the resource does not exist. When set to ``True``, None will be - returned when attempting to find a nonexistent resource. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the resource does not exist. When set to ``True``, None will + be returned when attempting to find a nonexistent resource. :param str list_base_path: base_path to be used when need listing resources. :param str microversion: API version to override the negotiated one. @@ -2331,8 +2331,8 @@ def find( or None if nothing matches. :raises: :class:`openstack.exceptions.DuplicateResource` if more than one resource is found for this request. - :raises: :class:`openstack.exceptions.ResourceNotFound` if nothing - is found and ignore_missing is ``False``. + :raises: :class:`openstack.exceptions.NotFoundException` if nothing is + found and ignore_missing is ``False``. """ session = cls._get_session(session) @@ -2377,7 +2377,7 @@ def find( if ignore_missing: return None - raise exceptions.ResourceNotFound( + raise exceptions.NotFoundException( f"No {cls.__name__} found for {name_or_id}" ) diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index aa97fd5ee..836b2d3da 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -124,7 +124,7 @@ def find_share(self, name_or_id, ignore_missing=True, **query): :param name_or_id: The name or ID of a share. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -312,7 +312,7 @@ def find_share_group(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a share group. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. diff --git a/openstack/shared_file_system/v2/share_access_rule.py b/openstack/shared_file_system/v2/share_access_rule.py index 66be84223..6edf54825 100644 --- a/openstack/shared_file_system/v2/share_access_rule.py +++ b/openstack/shared_file_system/v2/share_access_rule.py @@ -93,7 +93,7 @@ def delete( try: response = self._action(session, body, url) self._translate_response(response) - except exceptions.ResourceNotFound: + except exceptions.NotFoundException: if not ignore_missing: raise return response diff --git a/openstack/tests/functional/baremetal/test_baremetal_allocation.py b/openstack/tests/functional/baremetal/test_baremetal_allocation.py index a74db925c..5433f8b73 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_allocation.py +++ b/openstack/tests/functional/baremetal/test_baremetal_allocation.py @@ -64,7 +64,7 @@ def test_allocation_create_get_delete(self): self.conn.baremetal.delete_allocation(allocation, ignore_missing=False) self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.get_allocation, allocation.id, ) @@ -113,12 +113,12 @@ def test_allocation_negative_failure(self): def test_allocation_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.get_allocation, uuid, ) self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.delete_allocation, uuid, ignore_missing=False, @@ -158,7 +158,7 @@ def test_allocation_update(self): self.conn.baremetal.delete_allocation(allocation, ignore_missing=False) self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.get_allocation, allocation.id, ) @@ -203,7 +203,7 @@ def test_allocation_patch(self): self.conn.baremetal.delete_allocation(allocation, ignore_missing=False) self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.get_allocation, allocation.id, ) diff --git a/openstack/tests/functional/baremetal/test_baremetal_chassis.py b/openstack/tests/functional/baremetal/test_baremetal_chassis.py index 142e1fe93..2fdb2afbd 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_chassis.py +++ b/openstack/tests/functional/baremetal/test_baremetal_chassis.py @@ -24,7 +24,7 @@ def test_chassis_create_get_delete(self): self.conn.baremetal.delete_chassis(chassis, ignore_missing=False) self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.get_chassis, chassis.id, ) @@ -53,16 +53,16 @@ def test_chassis_patch(self): def test_chassis_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" self.assertRaises( - exceptions.ResourceNotFound, self.conn.baremetal.get_chassis, uuid + exceptions.NotFoundException, self.conn.baremetal.get_chassis, uuid ) self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.find_chassis, uuid, ignore_missing=False, ) self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.delete_chassis, uuid, ignore_missing=False, diff --git a/openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py b/openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py index bf40c4b4e..b09d2680c 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py +++ b/openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py @@ -40,7 +40,7 @@ def test_baremetal_deploy_create_get_delete(self): deploy_template, ignore_missing=False ) self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.get_deploy_template, deploy_template.id, ) @@ -165,12 +165,12 @@ def test_deploy_template_patch(self): def test_deploy_template_negative_non_existing(self): uuid = "bbb45f41-d4bc-4307-8d1d-32f95ce1e920" self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.get_deploy_template, uuid, ) self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.delete_deploy_template, uuid, ignore_missing=False, diff --git a/openstack/tests/functional/baremetal/test_baremetal_driver.py b/openstack/tests/functional/baremetal/test_baremetal_driver.py index c182192a3..fb775c642 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_driver.py +++ b/openstack/tests/functional/baremetal/test_baremetal_driver.py @@ -27,7 +27,7 @@ def test_fake_hardware_list(self): def test_driver_negative_non_existing(self): self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.get_driver, 'not-a-driver', ) diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index 78d06e2ce..2b0a2e36c 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -50,7 +50,7 @@ def test_node_create_get_delete(self): self.conn.baremetal.delete_node(node, ignore_missing=False) self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.get_node, self.node_id, ) @@ -63,7 +63,7 @@ def test_node_create_in_available(self): self.conn.baremetal.delete_node(node, ignore_missing=False) self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.get_node, self.node_id, ) @@ -223,22 +223,22 @@ def test_node_validate(self): def test_node_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" self.assertRaises( - exceptions.ResourceNotFound, self.conn.baremetal.get_node, uuid + exceptions.NotFoundException, self.conn.baremetal.get_node, uuid ) self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.find_node, uuid, ignore_missing=False, ) self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.delete_node, uuid, ignore_missing=False, ) self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.update_node, uuid, name='new-name', @@ -422,18 +422,18 @@ def test_node_vif_attach_detach(self): def test_node_vif_negative(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.attach_vif_to_node, uuid, self.vif_id, ) self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.list_node_vifs, uuid, ) self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.detach_vif_from_node, uuid, self.vif_id, diff --git a/openstack/tests/functional/baremetal/test_baremetal_port.py b/openstack/tests/functional/baremetal/test_baremetal_port.py index e704146c7..56e9ff9ae 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_port.py +++ b/openstack/tests/functional/baremetal/test_baremetal_port.py @@ -40,7 +40,7 @@ def test_port_create_get_delete(self): self.conn.baremetal.delete_port(port, ignore_missing=False) self.assertRaises( - exceptions.ResourceNotFound, self.conn.baremetal.get_port, port.id + exceptions.NotFoundException, self.conn.baremetal.get_port, port.id ) def test_port_list(self): @@ -107,22 +107,22 @@ def test_port_patch(self): def test_port_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" self.assertRaises( - exceptions.ResourceNotFound, self.conn.baremetal.get_port, uuid + exceptions.NotFoundException, self.conn.baremetal.get_port, uuid ) self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.find_port, uuid, ignore_missing=False, ) self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.delete_port, uuid, ignore_missing=False, ) self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.update_port, uuid, pxe_enabled=True, diff --git a/openstack/tests/functional/baremetal/test_baremetal_port_group.py b/openstack/tests/functional/baremetal/test_baremetal_port_group.py index 752f27202..4aaf76df3 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_port_group.py +++ b/openstack/tests/functional/baremetal/test_baremetal_port_group.py @@ -37,7 +37,7 @@ def test_port_group_create_get_delete(self): self.conn.baremetal.delete_port_group(port_group, ignore_missing=False) self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.get_port_group, port_group.id, ) @@ -100,18 +100,18 @@ def test_port_group_patch(self): def test_port_group_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.get_port_group, uuid, ) self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.find_port_group, uuid, ignore_missing=False, ) self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.delete_port_group, uuid, ignore_missing=False, diff --git a/openstack/tests/functional/baremetal/test_baremetal_volume_connector.py b/openstack/tests/functional/baremetal/test_baremetal_volume_connector.py index bf3e70bfb..cdcc11ce4 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_volume_connector.py +++ b/openstack/tests/functional/baremetal/test_baremetal_volume_connector.py @@ -45,7 +45,7 @@ def test_volume_connector_create_get_delete(self): volume_connector, ignore_missing=False ) self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.get_volume_connector, volume_connector.id, ) @@ -152,18 +152,18 @@ def test_volume_connector_patch(self): def test_volume_connector_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.get_volume_connector, uuid, ) self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.find_volume_connector, uuid, ignore_missing=False, ) self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.delete_volume_connector, uuid, ignore_missing=False, diff --git a/openstack/tests/functional/baremetal/test_baremetal_volume_target.py b/openstack/tests/functional/baremetal/test_baremetal_volume_target.py index d6e9d8f0f..b64d7d69b 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_volume_target.py +++ b/openstack/tests/functional/baremetal/test_baremetal_volume_target.py @@ -47,7 +47,7 @@ def test_volume_target_create_get_delete(self): volume_target, ignore_missing=False ) self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.get_volume_target, volume_target.id, ) @@ -166,18 +166,18 @@ def test_volume_target_patch(self): def test_volume_target_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.get_volume_target, uuid, ) self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.find_volume_target, uuid, ignore_missing=False, ) self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.baremetal.delete_volume_target, uuid, ignore_missing=False, diff --git a/openstack/tests/functional/compute/v2/test_flavor.py b/openstack/tests/functional/compute/v2/test_flavor.py index ca680ffec..7fc5f861a 100644 --- a/openstack/tests/functional/compute/v2/test_flavor.py +++ b/openstack/tests/functional/compute/v2/test_flavor.py @@ -48,7 +48,7 @@ def test_find_flavors_no_match_ignore_true(self): def test_find_flavors_no_match_ignore_false(self): self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.conn.compute.find_flavor, "not a flavor", ignore_missing=False, diff --git a/openstack/tests/functional/dns/v2/test_zone_share.py b/openstack/tests/functional/dns/v2/test_zone_share.py index 53a9c766f..9bed932da 100644 --- a/openstack/tests/functional/dns/v2/test_zone_share.py +++ b/openstack/tests/functional/dns/v2/test_zone_share.py @@ -123,7 +123,7 @@ def test_find_zone_share_ignore_missing(self): def test_find_zone_share_ignore_missing_false(self): self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.operator_cloud.dns.find_zone_share, self.zone, 'bogus_id', diff --git a/openstack/tests/functional/orchestration/v1/test_stack.py b/openstack/tests/functional/orchestration/v1/test_stack.py index 33492925c..91b5c489e 100644 --- a/openstack/tests/functional/orchestration/v1/test_stack.py +++ b/openstack/tests/functional/orchestration/v1/test_stack.py @@ -72,7 +72,7 @@ def tearDown(self): self.conn.orchestration.wait_for_status( self.stack, 'DELETE_COMPLETE', wait=self._wait_for_timeout ) - except exceptions.ResourceNotFound: + except exceptions.NotFoundException: pass test_network.delete_network(self.conn, self.network, self.subnet) super().tearDown() diff --git a/openstack/tests/functional/shared_file_system/test_share.py b/openstack/tests/functional/shared_file_system/test_share.py index 86b91b98a..cab1c6c07 100644 --- a/openstack/tests/functional/shared_file_system/test_share.py +++ b/openstack/tests/functional/shared_file_system/test_share.py @@ -192,7 +192,7 @@ def test_manage_and_unmanage_share(self): try: self.operator_cloud.share.get_share(self.SHARE_ID) - except exceptions.ResourceNotFound: + except exceptions.NotFoundException: pass managed_share = self.operator_cloud.share.manage_share( diff --git a/openstack/tests/unit/cloud/test_compute.py b/openstack/tests/unit/cloud/test_compute.py index f032b1709..b4c3d0340 100644 --- a/openstack/tests/unit/cloud/test_compute.py +++ b/openstack/tests/unit/cloud/test_compute.py @@ -66,7 +66,7 @@ def test__nova_extensions_fails(self): ] ) self.assertRaises( - exceptions.ResourceNotFound, self.cloud._nova_extensions + exceptions.NotFoundException, self.cloud._nova_extensions ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_domains.py b/openstack/tests/unit/cloud/test_domains.py index 0150d23e2..026424d39 100644 --- a/openstack/tests/unit/cloud/test_domains.py +++ b/openstack/tests/unit/cloud/test_domains.py @@ -235,7 +235,7 @@ def test_delete_domain_exception(self): ), ] ) - with testtools.ExpectedException(exceptions.ResourceNotFound): + with testtools.ExpectedException(exceptions.NotFoundException): self.cloud.delete_domain(domain_data.domain_id) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_fwaas.py b/openstack/tests/unit/cloud/test_fwaas.py index 599509ce3..c80b0689f 100644 --- a/openstack/tests/unit/cloud/test_fwaas.py +++ b/openstack/tests/unit/cloud/test_fwaas.py @@ -444,7 +444,7 @@ def test_create_firewall_policy_rule_not_found(self): with mock.patch.object(self.cloud.network, 'create_firewall_policy'): self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.cloud.create_firewall_policy, **posted_policy ) @@ -947,7 +947,7 @@ def test_insert_rule_into_policy_not_found(self): with mock.patch.object(self.cloud.network, 'find_firewall_rule'): self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.cloud.insert_rule_into_policy, policy_name, 'bogus_rule', @@ -979,7 +979,7 @@ def test_insert_rule_into_policy_rule_not_found(self): ] ) self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.cloud.insert_rule_into_policy, self.firewall_policy_id, rule_name, @@ -1088,7 +1088,7 @@ def test_remove_rule_from_policy_not_found(self): with mock.patch.object(self.cloud.network, 'find_firewall_rule'): self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.cloud.remove_rule_from_policy, self.firewall_policy_name, TestFirewallRule.firewall_rule_name, diff --git a/openstack/tests/unit/cloud/test_network.py b/openstack/tests/unit/cloud/test_network.py index e7ae1da97..98d0c8646 100644 --- a/openstack/tests/unit/cloud/test_network.py +++ b/openstack/tests/unit/cloud/test_network.py @@ -66,7 +66,7 @@ def test__neutron_extensions_fails(self): ) ] ) - with testtools.ExpectedException(exceptions.ResourceNotFound): + with testtools.ExpectedException(exceptions.NotFoundException): self.cloud._neutron_extensions() self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_openstackcloud.py b/openstack/tests/unit/cloud/test_openstackcloud.py index 8f69c069f..c7e3a3dc6 100644 --- a/openstack/tests/unit/cloud/test_openstackcloud.py +++ b/openstack/tests/unit/cloud/test_openstackcloud.py @@ -64,7 +64,7 @@ def test_search_resources_get_finds(self): ) def test_search_resources_list(self): - self.session._get.side_effect = exceptions.ResourceNotFound + self.session._get.side_effect = exceptions.NotFoundException self.session._list.return_value = [self.FakeResource(foo="bar")] ret = self.cloud.search_resources("mock_session.fake", "fake_name") @@ -79,7 +79,7 @@ def test_search_resources_list(self): ) def test_search_resources_args(self): - self.session._get.side_effect = exceptions.ResourceNotFound + self.session._get.side_effect = exceptions.NotFoundException self.session._list.return_value = [] self.cloud.search_resources( diff --git a/openstack/tests/unit/compute/v2/test_service.py b/openstack/tests/unit/compute/v2/test_service.py index 44665dd8f..02c62895f 100644 --- a/openstack/tests/unit/compute/v2/test_service.py +++ b/openstack/tests/unit/compute/v2/test_service.py @@ -127,7 +127,7 @@ def test_find_no_match_exception(self): list_mock.return_value = data self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, service.Service.find, self.sess, 'fake', diff --git a/openstack/tests/unit/network/v2/test_trunk.py b/openstack/tests/unit/network/v2/test_trunk.py index 3842668a3..9c83b5097 100644 --- a/openstack/tests/unit/network/v2/test_trunk.py +++ b/openstack/tests/unit/network/v2/test_trunk.py @@ -78,7 +78,7 @@ def test_add_subports_4xx(self): 'segmentation_type': 'vlan', } ] - with testtools.ExpectedException(exceptions.ResourceNotFound, msg): + with testtools.ExpectedException(exceptions.NotFoundException, msg): sot.add_subports(sess, subports) def test_delete_subports_4xx(self): @@ -100,5 +100,5 @@ def test_delete_subports_4xx(self): 'segmentation_type': 'vlan', } ] - with testtools.ExpectedException(exceptions.ResourceNotFound, msg): + with testtools.ExpectedException(exceptions.NotFoundException, msg): sot.delete_subports(sess, subports) diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index 794c5221d..72691b18e 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -348,12 +348,12 @@ def test_resources_with_stack_name(self, mock_find): @mock.patch.object(resource.Resource, 'list') def test_resources_stack_not_found(self, mock_list, mock_find): stack_name = 'test_stack' - mock_find.side_effect = exceptions.ResourceNotFound( + mock_find.side_effect = exceptions.NotFoundException( 'No stack found for test_stack' ) ex = self.assertRaises( - exceptions.ResourceNotFound, self.proxy.resources, stack_name + exceptions.NotFoundException, self.proxy.resources, stack_name ) self.assertEqual('No stack found for test_stack', str(ex)) diff --git a/openstack/tests/unit/orchestration/v1/test_stack.py b/openstack/tests/unit/orchestration/v1/test_stack.py index 079c5c078..be7dd05b3 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack.py +++ b/openstack/tests/unit/orchestration/v1/test_stack.py @@ -230,7 +230,7 @@ def test_fetch(self): test_resource.FakeResponse( {'stack': {'stack_status': 'CREATE_COMPLETE'}}, 200 ), - exceptions.ResourceNotFound(message='oops'), + exceptions.NotFoundException(message='oops'), test_resource.FakeResponse( {'stack': {'stack_status': 'DELETE_COMPLETE'}}, 200 ), @@ -248,9 +248,9 @@ def test_fetch(self): microversion=None, skip_cache=False, ) - ex = self.assertRaises(exceptions.ResourceNotFound, sot.fetch, sess) + ex = self.assertRaises(exceptions.NotFoundException, sot.fetch, sess) self.assertEqual('oops', str(ex)) - ex = self.assertRaises(exceptions.ResourceNotFound, sot.fetch, sess) + ex = self.assertRaises(exceptions.NotFoundException, sot.fetch, sess) self.assertEqual('No stack found for %s' % FAKE_ID, str(ex)) def test_abandon(self): diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index d4904da87..78470afb6 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -236,7 +236,7 @@ def test_delete(self): self.assertEqual(rv, self.fake_id) def test_delete_ignore_missing(self): - self.res.delete.side_effect = exceptions.ResourceNotFound( + self.res.delete.side_effect = exceptions.NotFoundException( message="test", http_status=404 ) @@ -244,12 +244,12 @@ def test_delete_ignore_missing(self): self.assertIsNone(rv) def test_delete_NotFound(self): - self.res.delete.side_effect = exceptions.ResourceNotFound( + self.res.delete.side_effect = exceptions.NotFoundException( message="test", http_status=404 ) self.assertRaisesRegex( - exceptions.ResourceNotFound, + exceptions.NotFoundException, # TODO(shade) The mocks here are hiding the thing we want to test. "test", self.sot._delete, @@ -467,12 +467,12 @@ def test_get_base_path(self): self.assertEqual(rv, self.fake_result) def test_get_not_found(self): - self.res.fetch.side_effect = exceptions.ResourceNotFound( + self.res.fetch.side_effect = exceptions.NotFoundException( message="test", http_status=404 ) self.assertRaisesRegex( - exceptions.ResourceNotFound, + exceptions.NotFoundException, "test", self.sot._get, RetrieveableResource, diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 42170b9ea..22cbda381 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -3302,7 +3302,7 @@ class Base(resource.Resource): def existing(cls, **kwargs): response = mock.Mock() response.status_code = 404 - raise exceptions.ResourceNotFound('Not Found', response=response) + raise exceptions.NotFoundException('Not Found', response=response) @classmethod def list(cls, session, **params): @@ -3343,7 +3343,7 @@ def existing(cls, **kwargs): def test_no_match_raise(self): self.assertRaises( - exceptions.ResourceNotFound, + exceptions.NotFoundException, self.no_results.find, self.cloud.compute, "name", @@ -3736,7 +3736,7 @@ def test_success_not_found(self): res.fetch.side_effect = [ res, res, - exceptions.ResourceNotFound('Not Found', response), + exceptions.NotFoundException('Not Found', response), ] result = resource.wait_for_delete(self.cloud.compute, res, 1, 3) diff --git a/openstack/workflow/v2/_proxy.py b/openstack/workflow/v2/_proxy.py index 98c67b739..72994bb92 100644 --- a/openstack/workflow/v2/_proxy.py +++ b/openstack/workflow/v2/_proxy.py @@ -56,7 +56,7 @@ def get_workflow(self, *attrs): :class:`~openstack.workflow.v2.workflow.Workflow` instance. :returns: One :class:`~openstack.workflow.v2.workflow.Workflow` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no workflow matching the name could be found. """ return self._get(_workflow.Workflow, *attrs) @@ -86,7 +86,7 @@ def delete_workflow(self, value, ignore_missing=True): :class:`~openstack.workflow.v2.workflow.Workflow` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will + :class:`~openstack.exceptions.NotFoundException` will be raised when the workflow does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent workflow. @@ -102,7 +102,7 @@ def find_workflow(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of an workflow. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -134,7 +134,7 @@ def get_execution(self, *attrs): :class:`~openstack.workflow.v2.execution.Execution` instance. :returns: One :class:`~openstack.workflow.v2.execution.Execution` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no execution matching the criteria could be found. """ return self._get(_execution.Execution, *attrs) @@ -164,7 +164,7 @@ def delete_execution(self, value, ignore_missing=True): :class:`~openstack.workflow.v2.execute.Execution` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the execution does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent execution. @@ -180,7 +180,7 @@ def find_execution(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of an execution. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. @@ -210,7 +210,7 @@ def get_cron_trigger(self, cron_trigger): :class:`~openstack.workflow.v2.cron_trigger.CronTrigger` instance. :returns: One :class:`~openstack.workflow.v2.cron_trigger.CronTrigger` - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no cron triggers matching the criteria could be found. """ return self._get(_cron_trigger.CronTrigger, cron_trigger) @@ -244,7 +244,7 @@ def delete_cron_trigger(self, value, ignore_missing=True): :class:`~openstack.workflow.v2.cron_trigger.CronTrigger` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be + :class:`~openstack.exceptions.NotFoundException` will be raised when the cron trigger does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent cron trigger. @@ -268,7 +268,7 @@ def find_cron_trigger( :param name_or_id: The name or ID of a cron trigger. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.ResourceNotFound` will be raised when + :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. :param bool all_projects: When set to ``True``, search for cron @@ -279,7 +279,7 @@ def find_cron_trigger( :returns: One :class:`~openstack.compute.v2.cron_trigger.CronTrigger` or None - :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple resources are found. From d84ef4f945b1013487951d11a79dba76891f6774 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 14 Jun 2024 11:52:01 +0100 Subject: [PATCH 3503/3836] tests: Rework warnings-related tests These were causing test failures due to warnings being raised by another package (keystoneauth) under Python 3.12. Rework things such that this is no longer an issue by removing one filter (which wasn't really doing anything) and reworking another to ignore DeprecationWarning-style warnings. Change-Id: I9aa73c59683dd9ee5fa701c436b860ce09740896 Signed-off-by: Stephen Finucane --- openstack/tests/unit/test_connection.py | 4 ---- openstack/tests/unit/test_missing_version.py | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index e2150a2a0..4177a63dd 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -15,7 +15,6 @@ import fixtures from keystoneauth1 import session -from testtools import matchers import openstack.config from openstack import connection @@ -169,10 +168,7 @@ def test_create_unknown_proxy(self): def closure(): return self.cloud.placement - self.assertThat(closure, matchers.Warnings(matchers.HasLength(0))) - self.assertIsInstance(self.cloud.placement, proxy.Proxy) - self.assert_calls() def test_create_connection_version_param_default(self): diff --git a/openstack/tests/unit/test_missing_version.py b/openstack/tests/unit/test_missing_version.py index fd8e41099..1efc0ac92 100644 --- a/openstack/tests/unit/test_missing_version.py +++ b/openstack/tests/unit/test_missing_version.py @@ -45,6 +45,7 @@ def test_unsupported_version_override(self): self.cloud.config.config['image_api_version'] = '7' with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") + warnings.simplefilter("ignore", DeprecationWarning) self.assertIsInstance(self.cloud.image, proxy.Proxy) self.assertEqual(1, len(w)) self.assertIn( From 9a023c503c6cf9442460f0c6b106c2baf9270c41 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 20 Jun 2024 16:36:21 +0100 Subject: [PATCH 3504/3836] image: Check path is a file before attempting to use it When you create an image and specify a name but not a path or data, SDK will look for files with the same name and use these as the source if available. This can only work if the path points to a file rather than a directory. Change-Id: I092fd729c6265826c9c69dacbe81aba042fd136d Signed-off-by: Stephen Finucane --- openstack/image/v2/_proxy.py | 2 +- openstack/tests/unit/image/v2/test_proxy.py | 60 ++++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 6876ff9cf..feb6d8592 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -40,7 +40,7 @@ def _get_name_and_filename(name, image_format): # See if name points to an existing file - if os.path.exists(name): + if os.path.exists(name) and os.path.isfile(name): # Neat. Easy enough return os.path.splitext(os.path.basename(name))[0], name diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 9bf464cfd..60c5bd881 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -11,6 +11,8 @@ # under the License. import io +import os.path +import tempfile from unittest import mock import requests @@ -119,6 +121,62 @@ def test_image_create(self): }, ) + def test_image_create_file_as_name(self): + # if we pass a filename as an image name, we should upload the file + # itself (and use the upload flow) + with tempfile.NamedTemporaryFile() as tmpfile: + name = os.path.basename(tmpfile.name) + self._verify( + 'openstack.image.v2._proxy.Proxy._upload_image', + # 'openstack.image.v2.image.Image.create', + self.proxy.create_image, + method_kwargs={ + 'name': tmpfile.name, + 'allow_duplicates': True, + }, + expected_args=[ + name, + ], + expected_kwargs={ + 'filename': tmpfile.name, + 'data': None, + 'meta': {}, + 'wait': False, + 'timeout': 3600, + 'validate_checksum': False, + 'use_import': False, + 'stores': None, + 'all_stores': None, + 'all_stores_must_succeed': None, + 'disk_format': 'qcow2', + 'container_format': 'bare', + 'properties': { + 'owner_specified.openstack.md5': '', + 'owner_specified.openstack.object': f'images/{name}', + 'owner_specified.openstack.sha256': '', + }, + }, + ) + + # but not if we use a directory... + with tempfile.TemporaryDirectory() as tmpdir: + self.verify_create( + self.proxy.create_image, + _image.Image, + method_kwargs={ + 'name': tmpdir, + 'allow_duplicates': True, + }, + expected_kwargs={ + 'container_format': 'bare', + 'disk_format': 'qcow2', + 'name': tmpdir, + 'owner_specified.openstack.md5': '', + 'owner_specified.openstack.object': f'images/{tmpdir}', + 'owner_specified.openstack.sha256': '', + }, + ) + def test_image_create_checksum_match(self): fake_image = _image.Image( id="fake", @@ -272,7 +330,7 @@ def test_image_create_protected(self): data="data", container_format="bare", disk_format="raw", - **properties + **properties, ) args, kwargs = self.proxy._create.call_args From 8ccf5132ec96c2abda87f20a2cb453b75e0e74d7 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 13 Jun 2024 14:09:27 +0100 Subject: [PATCH 3505/3836] cloud: Fix typo Change-Id: Ifd8b3a96c86b96b258193612223026dd1179eda3 Signed-off-by: Stephen Finucane --- openstack/cloud/_identity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index c228e8bdb..4ce6aaa9a 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -1249,7 +1249,7 @@ def delete_role(self, name_or_id, **kwargs): try: self.identity.delete_role(role) return True - except exceptions.SDKExceptions: + except exceptions.SDKException: self.log.exception(f"Unable to delete role {name_or_id}") raise From c6281c5bc7ceba3251ac771aa1ced9e55bccf141 Mon Sep 17 00:00:00 2001 From: Ebil Jacob Date: Wed, 12 Jun 2024 12:10:40 +0300 Subject: [PATCH 3506/3836] baremetal: Adds list node firmware support Added support to list the firmware components of a node under the baremetal service. - Created new functions `list_node_firmware` in baremetal proxy and `list_firmware` in baremetal node resource. - Added new release note to describe the new feature. - Added unit and functional tests for the new functions. Change-Id: Ib4b1584f24b0d4beb1594f97e1bbb8c81f5a84d5 --- openstack/baremetal/v1/_common.py | 3 ++ openstack/baremetal/v1/_proxy.py | 10 +++++ openstack/baremetal/v1/node.py | 31 +++++++++++++ .../baremetal/test_baremetal_node.py | 10 +++++ .../tests/unit/baremetal/v1/test_node.py | 45 +++++++++++++++++++ ...irmware-list-support-fec2f96a3a578730.yaml | 5 +++ 6 files changed, 104 insertions(+) create mode 100644 releasenotes/notes/add-node-firmware-list-support-fec2f96a3a578730.yaml diff --git a/openstack/baremetal/v1/_common.py b/openstack/baremetal/v1/_common.py index 83dbc1a92..0ee266d14 100644 --- a/openstack/baremetal/v1/_common.py +++ b/openstack/baremetal/v1/_common.py @@ -88,6 +88,9 @@ CHANGE_BOOT_MODE_VERSION = '1.76' """API version in which boot_mode and secure_boot states can be changed""" +FIRMWARE_VERSION = '1.86' +"""API version in which firmware components of a node can be accessed""" + class Resource(resource.Resource): base_path: str diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 18115e711..71699fb5d 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -1243,6 +1243,16 @@ def set_node_traits(self, node, traits): res = self._get_resource(_node.Node, node) return res.set_traits(self, traits) + def list_node_firmware(self, node): + """Lists firmware components for a node. + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :returns: A list of the node's firmware components. + """ + res = self._get_resource(_node.Node, node) + return res.list_firmware(self) + def volume_connectors(self, details=False, **query): """Retrieve a generator of volume_connector. diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 524930413..2832c93c0 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -1397,6 +1397,37 @@ def get_node_inventory(self, session, node_id): exceptions.raise_from_response(response, error_message=msg) return response.json() + def list_firmware(self, session): + """List firmware components associated with the node. + + :param session: The session to use for making this request. + :returns: The HTTP response. + """ + session = self._get_session(session) + version = self._assert_microversion_for( + session, + 'fetch', + _common.FIRMWARE_VERSION, + error_message=("Cannot use node list firmware components API"), + ) + + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'firmware') + + response = session.get( + request.url, + headers=request.headers, + microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) + + msg = "Failed to list firmware components for node {node}".format( + node=self.id + ) + exceptions.raise_from_response(response, error_message=msg) + + return response.json() + def patch( self, session, diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index 78d06e2ce..c01b6143a 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -489,3 +489,13 @@ def test_set_node_traits(self): self.assertEqual(['CUSTOM_FOOBAR'], self.node.traits) node = self.conn.baremetal.get_node(self.node) self.assertEqual(['CUSTOM_FOOBAR'], node.traits) + + +class TestBareMetalNodeListFirmware(base.BaseBaremetalTest): + min_microversion = '1.86' + + def test_list_firmware(self): + node = self.create_node(firmware_interface="no-firmware") + self.assertEqual("no-firmware", node.firmware_interface) + result = self.conn.baremetal.list_node_firmware(node) + self.assertEqual({'firmware': []}, result) diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 9fb440143..891dafd7e 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -1293,3 +1293,48 @@ def test_get_inventory(self): microversion='1.81', retriable_status_codes=_common.RETRIABLE_STATUS_CODES, ) + + +@mock.patch.object(node.Node, 'fetch', lambda self, session: self) +@mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) +class TestNodeFirmware(base.TestCase): + def setUp(self): + super().setUp() + self.node = node.Node(**FAKE) + self.session = mock.Mock( + spec=adapter.Adapter, + default_microversion='1.86', + ) + + def test_list_firmware(self): + node_firmware = { + "firmware": [ + { + "created_at": "2016-08-18T22:28:49.653974+00:00", + "updated_at": "2016-08-18T22:28:49.653974+00:00", + "component": "BMC", + "initial_version": "v1.0.0", + "current_version": "v1.2.0", + "last_version_flashed": "v1.2.0", + }, + { + "created_at": "2016-08-18T22:28:49.653974+00:00", + "updated_at": "2016-08-18T22:28:49.653974+00:00", + "component": "BIOS", + "initial_version": "v1.0.0", + "current_version": "v1.1.5", + "last_version_flashed": "v1.1.5", + }, + ] + } + self.session.get.return_value.json.return_value = node_firmware + + res = self.node.list_firmware(self.session) + self.assertEqual(node_firmware, res) + + self.session.get.assert_called_once_with( + 'nodes/%s/firmware' % self.node.id, + headers=mock.ANY, + microversion='1.86', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) diff --git a/releasenotes/notes/add-node-firmware-list-support-fec2f96a3a578730.yaml b/releasenotes/notes/add-node-firmware-list-support-fec2f96a3a578730.yaml new file mode 100644 index 000000000..1dad3c21c --- /dev/null +++ b/releasenotes/notes/add-node-firmware-list-support-fec2f96a3a578730.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Adds support for querying a bare-metal node's firmware as per functionality + introduced in API 1.86. From 4a8c12a80b883a314ff3bcbd2874fbed698b69ab Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 26 Jun 2024 13:52:25 +0100 Subject: [PATCH 3507/3836] tests: Remove errant print Change-Id: Ic3adaa90d80093b3ae6f8227a6c218ef4174a830 Signed-off-by: Stephen Finucane --- openstack/tests/unit/test_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openstack/tests/unit/test_utils.py b/openstack/tests/unit/test_utils.py index 23822ba1b..c0d779c9c 100644 --- a/openstack/tests/unit/test_utils.py +++ b/openstack/tests/unit/test_utils.py @@ -279,7 +279,6 @@ def test_walk_parallel(self): for node in sot.walk(timeout=1): executor.submit(test_walker_fn, sot, node, sorted_list) self._verify_order(sot.graph, sorted_list) - print(sorted_list) self.assertEqual(len(self.test_graph.keys()), len(sorted_list)) def test_walk_raise(self): From 3ee18eb1711c6e76188b58c0eb520443fd34750e Mon Sep 17 00:00:00 2001 From: Slawek Kaplonski Date: Wed, 3 Jul 2024 11:58:37 +0200 Subject: [PATCH 3508/3836] Run auto_allocated_topology functional tests in the own project To avoid accidental usage of the 'auto_allocate_network' which is created by Neutron automatically when 'get_auto_allocated_topology' API is called, by other tests those auto_allocated_topology tests are now run in own project which is destroyed after tests are done. Closes-Bug: #2071784 Change-Id: Ib92913204bd90c2fb56268f58b089a2d3ba45644 --- .../v2/test_auto_allocated_topology.py | 61 +++++++++++++++---- 1 file changed, 50 insertions(+), 11 deletions(-) diff --git a/openstack/tests/functional/network/v2/test_auto_allocated_topology.py b/openstack/tests/functional/network/v2/test_auto_allocated_topology.py index 5941374f5..a79070ff0 100644 --- a/openstack/tests/functional/network/v2/test_auto_allocated_topology.py +++ b/openstack/tests/functional/network/v2/test_auto_allocated_topology.py @@ -30,40 +30,79 @@ def setUp(self): "required for this test" ) - projects = [o.id for o in self.operator_cloud.identity.projects()] - self.PROJECT_ID = projects[0] + project = self._create_project() + self.PROJECT_ID = project['id'] + self.test_cloud = self.operator_cloud.connect_as_project(project) def tearDown(self): - res = self.operator_cloud.network.delete_auto_allocated_topology( + res = self.test_cloud.network.delete_auto_allocated_topology( self.PROJECT_ID ) self.assertIsNone(res) + self._destroy_project() super().tearDown() + def _create_project(self): + project_name = 'auto_allocated_topology_test_project' + project = self.operator_cloud.get_project(project_name) + if not project: + params = { + 'name': project_name, + 'description': ( + 'test project used only for the ' + 'TestAutoAllocatedTopology tests class' + ), + } + if self.identity_version == '3': + params['domain_id'] = self.operator_cloud.get_domain( + 'default' + )['id'] + + project = self.operator_cloud.create_project(**params) + + user_id = self.operator_cloud.current_user_id + # Grant the current user access to the project + role_assignment = self.operator_cloud.list_role_assignments( + {'user': user_id, 'project': project['id']} + ) + if not role_assignment: + self.operator_cloud.grant_role( + 'member', user=user_id, project=project['id'], wait=True + ) + return project + + def _destroy_project(self): + self.operator_cloud.revoke_role( + 'member', + user=self.operator_cloud.current_user_id, + project=self.PROJECT_ID, + ) + self.operator_cloud.delete_project(self.PROJECT_ID) + def test_dry_run_option_pass(self): # Dry run will only pass if there is a public network - networks = self.operator_cloud.network.networks() + networks = self.test_cloud.network.networks() self._set_network_external(networks) # Dry run option will return "dry-run=pass" in the 'id' resource - top = self.operator_cloud.network.validate_auto_allocated_topology( + top = self.test_cloud.network.validate_auto_allocated_topology( self.PROJECT_ID ) self.assertEqual(self.PROJECT_ID, top.project) self.assertEqual("dry-run=pass", top.id) def test_show_no_project_option(self): - top = self.operator_cloud.network.get_auto_allocated_topology() - project = self.conn.session.get_project_id() - network = self.operator_cloud.network.get_network(top.id) + top = self.test_cloud.network.get_auto_allocated_topology() + project = self.test_cloud.session.get_project_id() + network = self.test_cloud.network.get_network(top.id) self.assertEqual(top.project_id, project) self.assertEqual(top.id, network.id) def test_show_project_option(self): - top = self.operator_cloud.network.get_auto_allocated_topology( + top = self.test_cloud.network.get_auto_allocated_topology( self.PROJECT_ID ) - network = self.operator_cloud.network.get_network(top.id) + network = self.test_cloud.network.get_network(top.id) self.assertEqual(top.project_id, network.project_id) self.assertEqual(top.id, network.id) self.assertEqual(network.name, "auto_allocated_network") @@ -71,6 +110,6 @@ def test_show_project_option(self): def _set_network_external(self, networks): for network in networks: if network.name == "public": - self.operator_cloud.network.update_network( + self.test_cloud.network.update_network( network, is_default=True ) From 8cfb60ec1c92071ec8f20ff35d5df29f9635411c Mon Sep 17 00:00:00 2001 From: Slawek Kaplonski Date: Wed, 3 Jul 2024 16:09:13 +0200 Subject: [PATCH 3509/3836] Combine 3 auto_allocated_topology tests into one test As preparation for those tests is pretty expensive, we can save few seconds of the execution time by combining all the checks into one single test. Change-Id: I6be713773bc66938348462f226517b4eb5ed673a --- .../network/v2/test_auto_allocated_topology.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/openstack/tests/functional/network/v2/test_auto_allocated_topology.py b/openstack/tests/functional/network/v2/test_auto_allocated_topology.py index a79070ff0..3716eeca6 100644 --- a/openstack/tests/functional/network/v2/test_auto_allocated_topology.py +++ b/openstack/tests/functional/network/v2/test_auto_allocated_topology.py @@ -34,6 +34,9 @@ def setUp(self): self.PROJECT_ID = project['id'] self.test_cloud = self.operator_cloud.connect_as_project(project) + # Dry run will only pass if there is a public network + self._set_network_external() + def tearDown(self): res = self.test_cloud.network.delete_auto_allocated_topology( self.PROJECT_ID @@ -79,11 +82,8 @@ def _destroy_project(self): ) self.operator_cloud.delete_project(self.PROJECT_ID) - def test_dry_run_option_pass(self): - # Dry run will only pass if there is a public network - networks = self.test_cloud.network.networks() - self._set_network_external(networks) - + def test_auto_allocated_topology(self): + # First test validation with the 'dry-run' call # Dry run option will return "dry-run=pass" in the 'id' resource top = self.test_cloud.network.validate_auto_allocated_topology( self.PROJECT_ID @@ -91,14 +91,14 @@ def test_dry_run_option_pass(self): self.assertEqual(self.PROJECT_ID, top.project) self.assertEqual("dry-run=pass", top.id) - def test_show_no_project_option(self): + # test show auto_allocated_network without project id in the request top = self.test_cloud.network.get_auto_allocated_topology() project = self.test_cloud.session.get_project_id() network = self.test_cloud.network.get_network(top.id) self.assertEqual(top.project_id, project) self.assertEqual(top.id, network.id) - def test_show_project_option(self): + # test show auto_allocated_network with project id in the request top = self.test_cloud.network.get_auto_allocated_topology( self.PROJECT_ID ) @@ -107,7 +107,8 @@ def test_show_project_option(self): self.assertEqual(top.id, network.id) self.assertEqual(network.name, "auto_allocated_network") - def _set_network_external(self, networks): + def _set_network_external(self): + networks = self.test_cloud.network.networks() for network in networks: if network.name == "public": self.test_cloud.network.update_network( From 7040c81d1a485cc089b10cab52c9ca7b3dd2f54a Mon Sep 17 00:00:00 2001 From: Michael Still Date: Thu, 30 May 2024 17:29:40 +1000 Subject: [PATCH 3510/3836] Use mypy syntax compatible with older pythons. Change I1cb1efbf1f243cca0c5bb6e1058d25b2ad863355 introduced mypy syntax which breaks Python releases before 3.10. Unfortunately, for 2024.2 we commit to supporting Python back to 3.9. Specifically, you receive this error message if you run pep8 on Rocky 9: pep8 mypy.....................................................................Failed pep8 - hook id: mypy pep8 - exit code: 1 pep8 pep8 openstack/dns/v2/_base.py: note: In member "list" of class "Resource": pep8 openstack/dns/v2/_base.py:83:27: error: X | Y syntax for unions requires Python 3.10 [syntax] pep8 Found 1 error in 1 file (checked 414 source files) So instead, let's use typing syntax compatible with other pythons. Change-Id: Ifcf68075e572ccfc910dbef449cd58986c2a1bf5 --- openstack/dns/v2/_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/dns/v2/_base.py b/openstack/dns/v2/_base.py index 756e42939..6bba5b048 100644 --- a/openstack/dns/v2/_base.py +++ b/openstack/dns/v2/_base.py @@ -80,7 +80,7 @@ def list( all_projects=None, **params, ): - headers: ty.Union[ty.Dict[str, str] | None] = ( + headers: ty.Union[ty.Union[ty.Dict[str, str], None]] = ( {} if project_id or all_projects else None ) From 23c7a3aaa9214de9a47cc0cbea3ca1777fe9c5ab Mon Sep 17 00:00:00 2001 From: "Dr. Jens Harbott" Date: Mon, 8 Jul 2024 19:08:27 +0200 Subject: [PATCH 3511/3836] zuul: Use more stable dib job centos-8-stream is EOL and the builds are no longer passing. Since the only things that matters here is how the sdk is being used by nodepool, the actual image being built doesn't really matter. Use Ubuntu Noble. which is the most recent addition and thus will still be working for a long time, or so we hope. Also stop running non-voting jobs in the gate pipeline. Change-Id: I351ce792ffd1d24d032d7a657366be807ea29e38 --- zuul.d/project.yaml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index 88fcb4c86..202d89f67 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -17,7 +17,7 @@ - opendev-buildset-registry - nodepool-build-image-siblings: voting: false - - dib-nodepool-functional-openstack-centos-8-stream-src: + - dib-nodepool-functional-openstack-ubuntu-noble-src: voting: false - openstacksdk-functional-devstack - openstacksdk-functional-devstack-networking @@ -37,10 +37,6 @@ gate: jobs: - opendev-buildset-registry - - nodepool-build-image-siblings: - voting: false - - dib-nodepool-functional-openstack-centos-8-stream-src: - voting: false - openstacksdk-functional-devstack - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-networking-ext From d40d6a2e82b248543438fd5d24d2985a61c33a93 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 10 Jul 2024 11:39:18 +0100 Subject: [PATCH 3512/3836] compute: Correct base path for default, detailed quotas For some reason, you need to include the project ID in the path even though it's wholly ignored. Change-Id: I805cdfaa89134e92eeb9726697925e9d0657af19 Signed-off-by: Stephen Finucane --- openstack/compute/v2/_proxy.py | 7 +++++-- openstack/tests/unit/cloud/test_quotas.py | 1 - openstack/tests/unit/compute/v2/test_proxy.py | 5 ++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 0ef062387..419fb4dd9 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -2465,7 +2465,8 @@ def get_quota_set(self, project, usage=False, **query): None, project_id=project.id, ) - return res.fetch(self, usage=usage, **query) + base_path = '/os-quota-sets/%(project_id)s/detail' if usage else None + return res.fetch(self, base_path=base_path, **query) def get_quota_set_defaults(self, project): """Show QuotaSet defaults for the project @@ -2484,7 +2485,9 @@ def get_quota_set_defaults(self, project): None, project_id=project.id, ) - return res.fetch(self, base_path='/os-quota-sets/defaults') + return res.fetch( + self, base_path='/os-quota-sets/%(project_id)s/defaults' + ) def revert_quota_set(self, project, **query): """Reset Quota for the project/user. diff --git a/openstack/tests/unit/cloud/test_quotas.py b/openstack/tests/unit/cloud/test_quotas.py index 50dbe7f41..d02b2e7d8 100644 --- a/openstack/tests/unit/cloud/test_quotas.py +++ b/openstack/tests/unit/cloud/test_quotas.py @@ -125,7 +125,6 @@ def test_get_quotas(self): 'compute', 'public', append=['os-quota-sets', project.project_id], - qs_elements=['usage=False'], ), json={'quota_set': fake_quota_set}, ), diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 5c11d9e2a..38a6474ec 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -1642,7 +1642,6 @@ def test_quota_set_get(self): expected_kwargs={ 'error_message': None, 'requires_id': False, - 'usage': False, }, method_result=quota_set.QuotaSet(), expected_result=quota_set.QuotaSet(), @@ -1658,8 +1657,8 @@ def test_quota_set_get_query(self): expected_kwargs={ 'error_message': None, 'requires_id': False, - 'usage': True, 'user_id': 'uid', + 'base_path': '/os-quota-sets/%(project_id)s/detail', }, ) @@ -1672,7 +1671,7 @@ def test_quota_set_get_defaults(self): expected_kwargs={ 'error_message': None, 'requires_id': False, - 'base_path': '/os-quota-sets/defaults', + 'base_path': '/os-quota-sets/%(project_id)s/defaults', }, ) From 9145dcec6480a06622afc986731619e6d0a52691 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 10 Jul 2024 14:52:30 +0100 Subject: [PATCH 3513/3836] compute, volume: Improve 'update_quota_set' The implementations of these were rather confusing and required two separate sets of arguments. Simplify them based on the 'delete_quota_set' methods. Change-Id: I8bb0bfb039593c5b59f1f9c16523a090d899f099 Signed-off-by: Stephen Finucane --- openstack/block_storage/v2/_proxy.py | 38 +++++++++++---- openstack/block_storage/v3/_proxy.py | 33 +++++++++---- openstack/cloud/_block_storage.py | 8 +--- openstack/cloud/_compute.py | 7 +-- openstack/compute/v2/_proxy.py | 46 ++++++++++++++----- .../tests/unit/block_storage/v2/test_proxy.py | 27 ++++++----- .../tests/unit/block_storage/v3/test_proxy.py | 27 ++++++----- openstack/tests/unit/compute/v2/test_proxy.py | 29 +++++++----- openstack/warnings.py | 24 ++++++++++ 9 files changed, 158 insertions(+), 81 deletions(-) diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index b5c57b109..dea1fdf6d 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import warnings + from openstack.block_storage import _base_proxy from openstack.block_storage.v2 import backup as _backup from openstack.block_storage.v2 import capabilities as _capabilities @@ -23,6 +25,7 @@ from openstack.block_storage.v2 import volume as _volume from openstack.identity.v3 import project as _project from openstack import resource +from openstack import warnings as os_warnings class Proxy(_base_proxy.BaseBlockStorageProxy): @@ -769,22 +772,37 @@ def revert_quota_set(self, project, **query): query = {} return res.delete(self, **query) - def update_quota_set(self, quota_set, query=None, **attrs): + def update_quota_set(self, project, **attrs): """Update a QuotaSet. - :param quota_set: Either the ID of a quota_set or a - :class:`~openstack.block_storage.v2.quota_set.QuotaSet` instance. - :param dict query: Optional parameters to be used with update call. + :param project: ID or instance of + :class:`~openstack.identity.project.Project` of the project for + which the quota should be reset. :param attrs: The attributes to update on the QuotaSet represented by ``quota_set``. :returns: The updated QuotaSet - :rtype: :class:`~openstack.block_storage.v2.quota_set.QuotaSet` - """ - res = self._get_resource(_quota_set.QuotaSet, quota_set, **attrs) - if not query: - query = {} - return res.commit(self, **query) + :rtype: :class:`~openstack.block_storage.v3.quota_set.QuotaSet` + """ + if 'project_id' in attrs or isinstance(project, _quota_set.QuotaSet): + warnings.warn( + "The signature of 'update_quota_set' has changed and it " + "now expects a Project as the first argument, in line " + "with the other quota set methods.", + os_warnings.RemovedInSDK50Warning, + ) + if isinstance(project, _quota_set.QuotaSet): + attrs['project_id'] = project.project_id + + # cinder doesn't support any query parameters so we simply pop + # these + if 'query' in attrs: + attrs.pop('params') + else: + project = self._get_resource(_project.Project, project) + attrs['project_id'] = project.id + + return self._update(_quota_set.QuotaSet, None, **attrs) # ====== VOLUME METADATA ====== def get_volume_metadata(self, volume): diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index ddea58037..16638052f 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -11,6 +11,7 @@ # under the License. import typing as ty +import warnings from openstack.block_storage import _base_proxy from openstack.block_storage.v3 import attachment as _attachment @@ -36,6 +37,7 @@ from openstack.identity.v3 import project as _project from openstack import resource from openstack import utils +from openstack import warnings as os_warnings class Proxy(_base_proxy.BaseBlockStorageProxy): @@ -1794,22 +1796,37 @@ def revert_quota_set(self, project, **query): return res.delete(self, **query) - def update_quota_set(self, quota_set, query=None, **attrs): + def update_quota_set(self, project, **attrs): """Update a QuotaSet. - :param quota_set: Either the ID of a quota_set or a - :class:`~openstack.block_storage.v3.quota_set.QuotaSet` instance. - :param dict query: Optional parameters to be used with update call. + :param project: ID or instance of + :class:`~openstack.identity.project.Project` of the project for + which the quota should be reset. :param attrs: The attributes to update on the QuotaSet represented by ``quota_set``. :returns: The updated QuotaSet :rtype: :class:`~openstack.block_storage.v3.quota_set.QuotaSet` """ - res = self._get_resource(_quota_set.QuotaSet, quota_set, **attrs) - if not query: - query = {} - return res.commit(self, **query) + if 'project_id' in attrs or isinstance(project, _quota_set.QuotaSet): + warnings.warn( + "The signature of 'update_quota_set' has changed and it " + "now expects a Project as the first argument, in line " + "with the other quota set methods.", + os_warnings.RemovedInSDK50Warning, + ) + if isinstance(project, _quota_set.QuotaSet): + attrs['project_id'] = project.project_id + + # cinder doesn't support any query parameters so we simply pop + # these + if 'query' in attrs: + attrs.pop('params') + else: + project = self._get_resource(_project.Project, project) + attrs['project_id'] = project.id + + return self._update(_quota_set.QuotaSet, None, **attrs) # ====== SERVICES ====== @ty.overload diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index 2301ea186..10551d3df 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -13,7 +13,6 @@ import warnings from openstack.block_storage.v3._proxy import Proxy -from openstack.block_storage.v3 import quota_set as _qs from openstack.cloud import _utils from openstack import exceptions from openstack import warnings as os_warnings @@ -842,12 +841,9 @@ def set_volume_quotas(self, name_or_id, **kwargs): :raises: :class:`~openstack.exceptions.SDKException` if the resource to set the quota does not exist. """ + project = self.identity.find_project(name_or_id, ignore_missing=False) - proj = self.identity.find_project(name_or_id, ignore_missing=False) - - self.block_storage.update_quota_set( - _qs.QuotaSet(project_id=proj.id), **kwargs - ) + self.block_storage.update_quota_set(project=project, **kwargs) def get_volume_quotas(self, name_or_id): """Get volume quotas for a project diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 7848e46ab..4163ad6a8 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -21,7 +21,6 @@ from openstack.cloud import exc from openstack.cloud import meta from openstack.compute.v2._proxy import Proxy -from openstack.compute.v2 import quota_set as _qs from openstack.compute.v2 import server as _server from openstack import exceptions from openstack import utils @@ -1774,11 +1773,9 @@ def set_compute_quotas(self, name_or_id, **kwargs): :raises: :class:`~openstack.exceptions.SDKException` if the resource to set the quota does not exist. """ - proj = self.identity.find_project(name_or_id, ignore_missing=False) + project = self.identity.find_project(name_or_id, ignore_missing=False) kwargs['force'] = True - self.compute.update_quota_set( - _qs.QuotaSet(project_id=proj.id), **kwargs - ) + self.compute.update_quota_set(project=project, **kwargs) def get_compute_quotas(self, name_or_id): """Get quota for a project diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 419fb4dd9..c82d99d24 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -37,6 +37,7 @@ from openstack.compute.v2 import volume_attachment as _volume_attachment from openstack import exceptions from openstack.identity.v3 import project as _project +from openstack.identity.v3 import user as _user from openstack.network.v2 import security_group as _sg from openstack import proxy from openstack import resource @@ -2494,38 +2495,59 @@ def revert_quota_set(self, project, **query): :param project: ID or instance of :class:`~openstack.identity.project.Project` of the project for - which the quota should be resetted. + which the quota should be reset. :param dict query: Additional parameters to be used. :returns: ``None`` """ project = self._get_resource(_project.Project, project) res = self._get_resource( - _quota_set.QuotaSet, - None, - project_id=project.id, + _quota_set.QuotaSet, None, project_id=project.id ) if not query: query = {} return res.delete(self, **query) - def update_quota_set(self, quota_set, query=None, **attrs): + def update_quota_set(self, project, *, user=None, **attrs): """Update a QuotaSet. - :param quota_set: Either the ID of a quota_set or a - :class:`~openstack.compute.v2.quota_set.QuotaSet` instance. - :param dict query: Optional parameters to be used with update call. + :param project: ID or instance of + :class:`~openstack.identity.project.Project` of the project for + which the quota should be reset. + :param user_id: Optional ID of the user to set quotas as. :param attrs: The attributes to update on the QuotaSet represented by ``quota_set``. :returns: The updated QuotaSet :rtype: :class:`~openstack.compute.v2.quota_set.QuotaSet` """ - res = self._get_resource(_quota_set.QuotaSet, quota_set, **attrs) - if not query: - query = {} - return res.commit(self, **query) + query = {} + + if 'project_id' in attrs or isinstance(project, _quota_set.QuotaSet): + warnings.warn( + "The signature of 'update_quota_set' has changed and it " + "now expects a Project as the first argument, in line " + "with the other quota set methods.", + os_warnings.RemovedInSDK50Warning, + ) + if isinstance(project, _quota_set.QuotaSet): + attrs['project_id'] = project.project_id + + if 'query' in attrs: + query = attrs.pop('query') + else: + project = self._get_resource(_project.Project, project) + attrs['project_id'] = project.id + + if user: + user = self._get_resource(_user.User, user) + query['user_id'] = user.id + + # we don't use Proxy._update since that doesn't allow passing arbitrary + # query string parameters + quota_set = self._get_resource(_quota_set.QuotaSet, None, **attrs) + return quota_set.commit(self, **query) # ========== Server actions ========== diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index 9af158691..0590dcea4 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -21,7 +21,8 @@ from openstack.block_storage.v2 import stats from openstack.block_storage.v2 import type from openstack.block_storage.v2 import volume -from openstack import resource +from openstack.identity.v3 import project +from openstack import proxy as proxy_base from openstack.tests.unit import test_proxy_base @@ -526,19 +527,17 @@ def test_quota_set_reset(self): expected_kwargs={'user_id': 'uid'}, ) - @mock.patch('openstack.proxy.Proxy._get_resource', autospec=True) - def test_quota_set_update(self, gr_mock): - gr_mock.return_value = resource.Resource() - gr_mock.commit = mock.Mock() + @mock.patch.object(proxy_base.Proxy, '_get_resource') + def test_quota_set_update(self, mock_get): + fake_project = project.Project(id='prj') + mock_get.side_effect = [fake_project] + self._verify( - 'openstack.resource.Resource.commit', + 'openstack.proxy.Proxy._update', self.proxy.update_quota_set, - method_args=['qs'], - method_kwargs={ - 'query': {'user_id': 'uid'}, - 'a': 'b', - }, - expected_args=[self.proxy], - expected_kwargs={'user_id': 'uid'}, + method_args=['prj'], + method_kwargs={'volumes': 123}, + expected_args=[quota_set.QuotaSet, None], + expected_kwargs={'project_id': 'prj', 'volumes': 123}, ) - gr_mock.assert_called_with(self.proxy, quota_set.QuotaSet, 'qs', a='b') + mock_get.assert_called_once_with(project.Project, 'prj') diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 0a64a0dd5..986fe3696 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -28,7 +28,8 @@ from openstack.block_storage.v3 import stats from openstack.block_storage.v3 import type from openstack.block_storage.v3 import volume -from openstack import resource +from openstack.identity.v3 import project +from openstack import proxy as proxy_base from openstack.tests.unit import test_proxy_base @@ -1017,19 +1018,17 @@ def test_quota_set_reset(self): expected_kwargs={'user_id': 'uid'}, ) - @mock.patch('openstack.proxy.Proxy._get_resource', autospec=True) - def test_quota_set_update(self, gr_mock): - gr_mock.return_value = resource.Resource() - gr_mock.commit = mock.Mock() + @mock.patch.object(proxy_base.Proxy, '_get_resource') + def test_quota_set_update(self, mock_get): + fake_project = project.Project(id='prj') + mock_get.side_effect = [fake_project] + self._verify( - 'openstack.resource.Resource.commit', + 'openstack.proxy.Proxy._update', self.proxy.update_quota_set, - method_args=['qs'], - method_kwargs={ - 'query': {'user_id': 'uid'}, - 'a': 'b', - }, - expected_args=[self.proxy], - expected_kwargs={'user_id': 'uid'}, + method_args=['prj'], + method_kwargs={'volumes': 123}, + expected_args=[quota_set.QuotaSet, None], + expected_kwargs={'project_id': 'prj', 'volumes': 123}, ) - gr_mock.assert_called_with(self.proxy, quota_set.QuotaSet, 'qs', a='b') + mock_get.assert_called_once_with(project.Project, 'prj') diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 38a6474ec..45d5efe96 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -38,7 +38,8 @@ from openstack.compute.v2 import service from openstack.compute.v2 import usage from openstack.compute.v2 import volume_attachment -from openstack import resource +from openstack.identity.v3 import project +from openstack import proxy as proxy_base from openstack.tests.unit import test_proxy_base from openstack import warnings as os_warnings @@ -1685,22 +1686,26 @@ def test_quota_set_reset(self): expected_kwargs={'user_id': 'uid'}, ) - @mock.patch('openstack.proxy.Proxy._get_resource', autospec=True) - def test_quota_set_update(self, gr_mock): - gr_mock.return_value = resource.Resource() - gr_mock.commit = mock.Mock() + @mock.patch.object(proxy_base.Proxy, "_get_resource") + def test_quota_set_update(self, mock_get): + fake_project = project.Project(id='prj') + fake_quota_set = quota_set.QuotaSet(project_id='prj') + mock_get.side_effect = [fake_project, fake_quota_set] + self._verify( 'openstack.resource.Resource.commit', self.proxy.update_quota_set, - method_args=['qs'], - method_kwargs={ - 'query': {'user_id': 'uid'}, - 'a': 'b', - }, + method_args=['prj'], + method_kwargs={'ram': 123}, expected_args=[self.proxy], - expected_kwargs={'user_id': 'uid'}, + expected_kwargs={}, + ) + mock_get.assert_has_calls( + [ + mock.call(project.Project, 'prj'), + mock.call(quota_set.QuotaSet, None, project_id='prj', ram=123), + ] ) - gr_mock.assert_called_with(self.proxy, quota_set.QuotaSet, 'qs', a='b') class TestServerAction(TestComputeProxy): diff --git a/openstack/warnings.py b/openstack/warnings.py index acbb81e4e..b0f80dcc5 100644 --- a/openstack/warnings.py +++ b/openstack/warnings.py @@ -10,6 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +# API deprecation warnings +# +# These are for service-related deprecations, such as the removal of an API or +# API field due to a microversion. + class OpenStackDeprecationWarning(DeprecationWarning): """Base class for warnings about deprecated features in openstacksdk.""" @@ -31,6 +36,25 @@ class LegacyAPIWarning(OpenStackDeprecationWarning): """Indicates an API that is in 'legacy' status, a long term deprecation.""" +# Package deprecation warnings +# +# These are for SDK-specific deprecations, such as removed functions or +# function parameters. + + +class RemovedInSDK40Warning(DeprecationWarning): + """Indicates an argument that is deprecated for removal in SDK 4.0.""" + + +class RemovedInSDK50Warning(PendingDeprecationWarning): + """Indicates an argument that is deprecated for removal in SDK 5.0.""" + + +# General warnings +# +# These are usually related to misconfigurations. + + class OpenStackWarning(Warning): """Base class for general warnings in openstacksdk.""" From ecb35e0a650188f84eab9aa34bf5b07f315c1dd1 Mon Sep 17 00:00:00 2001 From: Rodrigo Barbieri Date: Fri, 12 Jul 2024 10:18:49 +0100 Subject: [PATCH 3514/3836] block_storage: Add support for project_id in Limits An admin should be able to list limits of other users. Cinder added such functionality in microversion 3.39. Closes-bug: #2071367 Change-Id: Ie2b33bef15d40a72b9aac4ec5bfe9eed93b0e864 Co-authored-by: Stephen Finucane --- openstack/block_storage/v3/_proxy.py | 6 +++++- openstack/block_storage/v3/limits.py | 6 ++++++ openstack/tests/unit/block_storage/v3/test_proxy.py | 9 +++++---- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index ddea58037..fe36b974d 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -1291,7 +1291,11 @@ def get_limits(self, project=None): params = {} if project: params['project_id'] = resource.Resource._get_id(project) - return self._get(_limits.Limits, requires_id=False, **params) + + # we don't use Proxy._get since that doesn't allow passing arbitrary + # query string parameters + res = self._get_resource(_limits.Limits, None) + return res.fetch(self, requires_id=False, **params) # ====== CAPABILITIES ====== def get_capabilities(self, host): diff --git a/openstack/block_storage/v3/limits.py b/openstack/block_storage/v3/limits.py index fe726a6e6..71717b4f3 100644 --- a/openstack/block_storage/v3/limits.py +++ b/openstack/block_storage/v3/limits.py @@ -71,6 +71,12 @@ class Limits(resource.Resource): resource_key = "limits" base_path = "/limits" + _max_microversion = "3.39" + + _query_mapping = resource.QueryParameters( + "project_id", + ) + # capabilities allow_fetch = True diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 0a64a0dd5..5254a71c1 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -19,7 +19,6 @@ from openstack.block_storage.v3 import group from openstack.block_storage.v3 import group_snapshot from openstack.block_storage.v3 import group_type -from openstack.block_storage.v3 import limits from openstack.block_storage.v3 import quota_class_set from openstack.block_storage.v3 import quota_set from openstack.block_storage.v3 import resource_filter @@ -136,11 +135,13 @@ def test_backend_pools(self): class TestLimit(TestVolumeProxy): def test_limits_get(self): - self.verify_get( + self._verify( + 'openstack.resource.Resource.fetch', self.proxy.get_limits, - limits.Limits, method_args=[], - expected_kwargs={'requires_id': False}, + method_kwargs={'project': 'foo'}, + expected_args=[self.proxy], + expected_kwargs={'requires_id': False, 'project_id': 'foo'}, ) From 346756097714b93d389c52a2c280cbe766983ed8 Mon Sep 17 00:00:00 2001 From: Salman Hajizada Date: Wed, 12 Jun 2024 22:30:24 +0300 Subject: [PATCH 3515/3836] baremetal: Enhance VIF attachment with port and portgroup UUIDs This change extends the `attach_vif` and `attach_vif_to_node` methods to accept optional parameters for VIF port UUID and VIF portgroup UUID. This enhancement allows for more flexible VIF attachment scenarios while ensuring that only one of these parameters can be set at a time. - Added parameters `vif_port_uuid` and `vif_portgroup_uuid` to the `attach_vif` method in the `Node` class. - Updated the `attach_vif_to_node` method in the `Proxy` class to pass these parameters to the `attach_vif` method. - Included a check to ensure only one of `vif_port_uuid` and `vif_portgroup_uuid` can be set at a time in both methods. - Modified the request body in the `attach_vif` method to include these parameters if provided. Change-Id: I4b8487b45ae04f387c2f02a9505916072edc96aa --- openstack/baremetal/v1/_common.py | 3 + openstack/baremetal/v1/_proxy.py | 32 ++++++-- openstack/baremetal/v1/node.py | 37 +++++++++- .../tests/unit/baremetal/v1/test_node.py | 74 +++++++++++++++++-- ...-vif-optional-params-abb755b74f076eb2.yaml | 6 ++ 5 files changed, 137 insertions(+), 15 deletions(-) create mode 100644 releasenotes/notes/add-vif-optional-params-abb755b74f076eb2.yaml diff --git a/openstack/baremetal/v1/_common.py b/openstack/baremetal/v1/_common.py index 0ee266d14..3eb3c4334 100644 --- a/openstack/baremetal/v1/_common.py +++ b/openstack/baremetal/v1/_common.py @@ -70,6 +70,9 @@ VIF_VERSION = '1.28' """API version in which the VIF operations were introduced.""" +VIF_OPTIONAL_PARAMS_VERSION = '1.67' +"""API version in which the VIF optional parameters were introduced.""" + INJECT_NMI_VERSION = '1.29' """API vresion in which support for injecting NMI was introduced.""" diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 71699fb5d..3607a9263 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + from openstack.baremetal.v1 import _common from openstack.baremetal.v1 import allocation as _allocation from openstack.baremetal.v1 import chassis as _chassis @@ -64,7 +66,7 @@ def _get_with_fields(self, resource_type, value, fields=None): error_message="No {resource_type} found for {value}".format( resource_type=resource_type.__name__, value=value ), - **kwargs + **kwargs, ) def chassis(self, details=False, **query): @@ -964,7 +966,15 @@ def delete_port_group(self, port_group, ignore_missing=True): _portgroup.PortGroup, port_group, ignore_missing=ignore_missing ) - def attach_vif_to_node(self, node, vif_id, retry_on_conflict=True): + def attach_vif_to_node( + self, + node: ty.Union[_node.Node, str], + vif_id: str, + retry_on_conflict: bool = True, + *, + port_id: ty.Optional[str] = None, + port_group_id: ty.Optional[str] = None, + ) -> None: """Attach a VIF to the node. The exact form of the VIF ID depends on the network interface used by @@ -974,17 +984,29 @@ def attach_vif_to_node(self, node, vif_id, retry_on_conflict=True): :param node: The value can be either the name or ID of a node or a :class:`~openstack.baremetal.v1.node.Node` instance. - :param string vif_id: Backend-specific VIF ID. + :param vif_id: Backend-specific VIF ID. :param retry_on_conflict: Whether to retry HTTP CONFLICT errors. This can happen when either the VIF is already used on a node or the node is locked. Since the latter happens more often, the default value is True. - :return: ``None`` + :param port_id: The UUID of the port to attach the VIF to. Only one of + port_id or port_group_id can be provided. + :param port_group_id: The UUID of the portgroup to attach to. Only one + of port_group_id or port_id can be provided. + :return: None :raises: :exc:`~openstack.exceptions.NotSupported` if the server does not support the VIF API. + :raises: :exc:`~openstack.exceptions.InvalidRequest` if both port_id + and port_group_id are provided. """ res = self._get_resource(_node.Node, node) - res.attach_vif(self, vif_id, retry_on_conflict=retry_on_conflict) + res.attach_vif( + self, + vif_id=vif_id, + retry_on_conflict=retry_on_conflict, + port_id=port_id, + port_group_id=port_group_id, + ) def detach_vif_from_node(self, node, vif_id, ignore_missing=True): """Detach a VIF from the node. diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 2832c93c0..1281f79d4 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -12,6 +12,7 @@ import collections import enum +import typing as ty from openstack.baremetal.v1 import _common from openstack import exceptions @@ -790,7 +791,15 @@ def set_power_state(self, session, target, wait=False, timeout=None): if wait: self.wait_for_power_state(session, expected, timeout=timeout) - def attach_vif(self, session, vif_id, retry_on_conflict=True): + def attach_vif( + self, + session, + vif_id: str, + retry_on_conflict: bool = True, + *, + port_id: ty.Optional[str] = None, + port_group_id: ty.Optional[str] = None, + ) -> None: """Attach a VIF to the node. The exact form of the VIF ID depends on the network interface used by @@ -800,26 +809,46 @@ def attach_vif(self, session, vif_id, retry_on_conflict=True): :param session: The session to use for making this request. :type session: :class:`~keystoneauth1.adapter.Adapter` - :param string vif_id: Backend-specific VIF ID. + :param vif_id: Backend-specific VIF ID. :param retry_on_conflict: Whether to retry HTTP CONFLICT errors. This can happen when either the VIF is already used on a node or the node is locked. Since the latter happens more often, the default value is True. - :return: ``None`` + :param port_id: The UUID of the port to attach the VIF to. Only one of + port_id or port_group_id can be provided. + :param port_group_id: The UUID of the portgroup to attach to. Only one + of port_group_id or port_id can be provided. + :return: None :raises: :exc:`~openstack.exceptions.NotSupported` if the server does not support the VIF API. + :raises: :exc:`~openstack.exceptions.InvalidRequest` if both port_id + and port_group_id are provided. """ + if port_id and port_group_id: + msg = ( + 'Only one of vif_port_id and vif_portgroup_id can be provided' + ) + raise exceptions.InvalidRequest(msg) + session = self._get_session(session) + if port_id or port_group_id: + required_version = _common.VIF_OPTIONAL_PARAMS_VERSION + else: + required_version = _common.VIF_VERSION version = self._assert_microversion_for( session, 'commit', - _common.VIF_VERSION, + required_version, error_message=("Cannot use VIF attachment API"), ) request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'vifs') body = {'id': vif_id} + if port_id: + body['port_uuid'] = port_id + elif port_group_id: + body['portgroup_uuid'] = port_group_id retriable_status_codes = _common.RETRIABLE_STATUS_CODES if not retry_on_conflict: retriable_status_codes = list(set(retriable_status_codes) - {409}) diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 891dafd7e..5e8af118e 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -558,12 +558,14 @@ class TestNodeVif(base.TestCase): def setUp(self): super().setUp() self.session = mock.Mock(spec=adapter.Adapter) - self.session.default_microversion = '1.28' + self.session.default_microversion = '1.67' self.session.log = mock.Mock() self.node = node.Node( id='c29db401-b6a7-4530-af8e-20a720dee946', driver=FAKE['driver'] ) self.vif_id = '714bdf6d-2386-4b5e-bd0d-bc036f04b1ef' + self.vif_port_uuid = 'port-uuid' + self.vif_portgroup_uuid = 'portgroup-uuid' def test_attach_vif(self): self.assertIsNone(self.node.attach_vif(self.session, self.vif_id)) @@ -571,7 +573,7 @@ def test_attach_vif(self): 'nodes/%s/vifs' % self.node.id, json={'id': self.vif_id}, headers=mock.ANY, - microversion='1.28', + microversion='1.67', retriable_status_codes=[409, 503], ) @@ -585,16 +587,59 @@ def test_attach_vif_no_retries(self): 'nodes/%s/vifs' % self.node.id, json={'id': self.vif_id}, headers=mock.ANY, - microversion='1.28', + microversion='1.67', retriable_status_codes=[503], ) + def test_attach_vif_with_port_uuid(self): + self.assertIsNone( + self.node.attach_vif( + self.session, self.vif_id, port_id=self.vif_port_uuid + ) + ) + self.session.post.assert_called_once_with( + 'nodes/%s/vifs' % self.node.id, + json={'id': self.vif_id, 'port_uuid': self.vif_port_uuid}, + headers=mock.ANY, + microversion='1.67', + retriable_status_codes=[409, 503], + ) + + def test_attach_vif_with_portgroup_uuid(self): + self.assertIsNone( + self.node.attach_vif( + self.session, + self.vif_id, + port_group_id=self.vif_portgroup_uuid, + ) + ) + self.session.post.assert_called_once_with( + 'nodes/%s/vifs' % self.node.id, + json={ + 'id': self.vif_id, + 'portgroup_uuid': self.vif_portgroup_uuid, + }, + headers=mock.ANY, + microversion='1.67', + retriable_status_codes=[409, 503], + ) + + def test_attach_vif_with_port_uuid_and_portgroup_uuid(self): + self.assertRaises( + exceptions.InvalidRequest, + self.node.attach_vif, + self.session, + self.vif_id, + port_id=self.vif_port_uuid, + port_group_id=self.vif_portgroup_uuid, + ) + def test_detach_vif_existing(self): self.assertTrue(self.node.detach_vif(self.session, self.vif_id)) self.session.delete.assert_called_once_with( f'nodes/{self.node.id}/vifs/{self.vif_id}', headers=mock.ANY, - microversion='1.28', + microversion='1.67', retriable_status_codes=_common.RETRIABLE_STATUS_CODES, ) @@ -604,7 +649,7 @@ def test_detach_vif_missing(self): self.session.delete.assert_called_once_with( f'nodes/{self.node.id}/vifs/{self.vif_id}', headers=mock.ANY, - microversion='1.28', + microversion='1.67', retriable_status_codes=_common.RETRIABLE_STATUS_CODES, ) @@ -620,7 +665,7 @@ def test_list_vifs(self): self.session.get.assert_called_once_with( 'nodes/%s/vifs' % self.node.id, headers=mock.ANY, - microversion='1.28', + microversion='1.67', ) def test_incompatible_microversion(self): @@ -641,6 +686,23 @@ def test_incompatible_microversion(self): exceptions.NotSupported, self.node.list_vifs, self.session ) + def test_incompatible_microversion_optional_params(self): + self.session.default_microversion = '1.28' + self.assertRaises( + exceptions.NotSupported, + self.node.attach_vif, + self.session, + self.vif_id, + port_id=self.vif_port_uuid, + ) + self.assertRaises( + exceptions.NotSupported, + self.node.attach_vif, + self.session, + self.vif_id, + port_group_id=self.vif_portgroup_uuid, + ) + @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) @mock.patch.object(node.Node, '_get_session', lambda self, x: x) diff --git a/releasenotes/notes/add-vif-optional-params-abb755b74f076eb2.yaml b/releasenotes/notes/add-vif-optional-params-abb755b74f076eb2.yaml new file mode 100644 index 000000000..924436dea --- /dev/null +++ b/releasenotes/notes/add-vif-optional-params-abb755b74f076eb2.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Extend the ``attach_vif`` and ``attach_vif_to_node`` methods of the + baremetal proxy to to accept optional parameters for VIF port UUID and + VIF portgroup UUID. From a56c3c91a1dfa11590cc9ffb3c15f328b9baf513 Mon Sep 17 00:00:00 2001 From: mathieu bultel Date: Tue, 16 Jul 2024 11:58:07 +0200 Subject: [PATCH 3516/3836] Allow to override _max_microversion for Volume._action function Currently the Volume actions (extend, set_bootable ...) can't set a microversion and the default _max_microversion is always used. This patch aim to use the _get_microversion method in order to use the default microversion or the one passed by the user via. Change-Id: I39539cbbf45fea67289e05672f45cc34d3a7ce24 --- openstack/block_storage/v3/volume.py | 8 ++++---- .../tests/unit/block_storage/v3/test_volume.py | 13 +++++++++++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index eb927ec40..ceb00d747 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -113,15 +113,15 @@ class Volume(resource.Resource, metadata.MetadataMixin): _max_microversion = "3.60" - def _action(self, session, body, microversion=None): + def _action(self, session, body, microversion=None, action='patch'): """Preform volume actions given the message body.""" # NOTE: This is using Volume.base_path instead of self.base_path # as both Volume and VolumeDetail instances can be acted on, but # the URL used is sans any additional /detail/ part. url = utils.urljoin(Volume.base_path, self.id, 'action') - resp = session.post( - url, json=body, microversion=self._max_microversion - ) + if microversion is None: + microversion = self._get_microversion(session, action=action) + resp = session.post(url, json=body, microversion=microversion) exceptions.raise_from_response(resp) return resp diff --git a/openstack/tests/unit/block_storage/v3/test_volume.py b/openstack/tests/unit/block_storage/v3/test_volume.py index 03d06351d..a3a6358a3 100644 --- a/openstack/tests/unit/block_storage/v3/test_volume.py +++ b/openstack/tests/unit/block_storage/v3/test_volume.py @@ -151,7 +151,7 @@ def setUp(self): self.resp.status_code = 200 self.resp.json = mock.Mock(return_value=self.resp.body) self.sess = mock.Mock(spec=adapter.Adapter) - self.sess.default_microversion = '3.0' + self.sess.default_microversion = '3.60' self.sess.post = mock.Mock(return_value=self.resp) self.sess._get_connection = mock.Mock(return_value=self.cloud) @@ -663,7 +663,7 @@ def test_create_scheduler_hints(self): self.sess.post.assert_called_with( url, json=body, - microversion='3.0', + microversion='3.60', headers={}, params={}, ) @@ -729,3 +729,12 @@ def test_manage_pre_38(self, mock_mv): self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion ) + + def test_set_microversion(self): + sot = volume.Volume(**VOLUME) + self.sess.default_microversion = '3.50' + self.assertIsNone(sot.extend(self.sess, '20')) + + url = 'volumes/%s/action' % FAKE_ID + body = {"os-extend": {"new_size": "20"}} + self.sess.post.assert_called_with(url, json=body, microversion="3.50") From 60a11dade2a16b7af9568e09aae35cd07165ad50 Mon Sep 17 00:00:00 2001 From: Jake Yip Date: Wed, 17 Jul 2024 17:47:16 +1000 Subject: [PATCH 3517/3836] Cast all header values to string Some header values like `delete_after` are int. Make sure they are casted to str before sending to requests library. Closes-Bug: #2073355 Change-Id: I5ff0d0635a7f5599e15f386bac04edcd95c4f539 --- openstack/object_store/v1/_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/object_store/v1/_base.py b/openstack/object_store/v1/_base.py index 0f910d0cf..e45e035e0 100644 --- a/openstack/object_store/v1/_base.py +++ b/openstack/object_store/v1/_base.py @@ -57,7 +57,7 @@ def _calculate_headers(self, metadata): header = key else: header = self._custom_metadata_prefix + key - headers[header] = metadata[key] + headers[header] = str(metadata[key]) return headers def set_metadata(self, session, metadata, refresh=True): From ff1ac43112d699bf2e6a8a161b28c2149f294f16 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 19 Jul 2024 13:46:20 +0100 Subject: [PATCH 3518/3836] block storage: Add 'set_readonly' action to v2 Change I335b8f11b94810f45f0c45b94fc4984f6bf5e122 added support for the 'os-update_readonly_flag' action to the v3 API but it was also supported in the v2 API. Add it now so we can use it in OSC. Change-Id: I935303a24d6c340946b578ad8e6a2834ba18dfc7 Signed-off-by: Stephen Finucane --- openstack/block_storage/v2/_proxy.py | 13 +++++++++++ openstack/block_storage/v2/volume.py | 5 +++++ .../tests/unit/block_storage/v2/test_proxy.py | 16 ++++++++++++++ .../unit/block_storage/v2/test_volume.py | 22 +++++++++++++++++++ 4 files changed, 56 insertions(+) diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index dea1fdf6d..0dc067c75 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -360,6 +360,19 @@ def extend_volume(self, volume, size): volume = self._get_resource(_volume.Volume, volume) volume.extend(self, size) + def set_volume_readonly(self, volume, readonly=True): + """Set a volume's read-only flag. + + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.block_storage.v2.volume.Volume` instance. + :param bool readonly: Whether the volume should be a read-only volume + or not. + + :returns: None + """ + volume = self._get_resource(_volume.Volume, volume) + volume.set_readonly(self, readonly) + def retype_volume(self, volume, new_type, migration_policy="never"): """Retype the volume. diff --git a/openstack/block_storage/v2/volume.py b/openstack/block_storage/v2/volume.py index 1fd0533d8..196213805 100644 --- a/openstack/block_storage/v2/volume.py +++ b/openstack/block_storage/v2/volume.py @@ -117,6 +117,11 @@ def set_bootable_status(self, session, bootable=True): body = {'os-set_bootable': {'bootable': bootable}} self._action(session, body) + def set_readonly(self, session, readonly): + """Set volume readonly flag""" + body = {'os-update_readonly_flag': {'readonly': readonly}} + self._action(session, body) + def set_image_metadata(self, session, metadata): """Sets image metadata key-value pairs on the volume""" body = {'os-set_image_metadata': metadata} diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index 0590dcea4..21accfc2c 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -138,6 +138,22 @@ def test_volume_extend(self): expected_args=[self.proxy, "new-size"], ) + def test_volume_set_readonly_no_argument(self): + self._verify( + "openstack.block_storage.v2.volume.Volume.set_readonly", + self.proxy.set_volume_readonly, + method_args=["value"], + expected_args=[self.proxy, True], + ) + + def test_volume_set_readonly_false(self): + self._verify( + "openstack.block_storage.v2.volume.Volume.set_readonly", + self.proxy.set_volume_readonly, + method_args=["value", False], + expected_args=[self.proxy, False], + ) + def test_volume_set_bootable(self): self._verify( "openstack.block_storage.v2.volume.Volume.set_bootable_status", diff --git a/openstack/tests/unit/block_storage/v2/test_volume.py b/openstack/tests/unit/block_storage/v2/test_volume.py index 4e86bf155..65b6f59a0 100644 --- a/openstack/tests/unit/block_storage/v2/test_volume.py +++ b/openstack/tests/unit/block_storage/v2/test_volume.py @@ -160,6 +160,28 @@ def test_extend(self): url, json=body, microversion=sot._max_microversion ) + def test_set_volume_readonly(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.set_readonly(self.sess, True)) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-update_readonly_flag': {'readonly': True}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion + ) + + def test_set_volume_readonly_false(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.set_readonly(self.sess, False)) + + url = 'volumes/%s/action' % FAKE_ID + body = {'os-update_readonly_flag': {'readonly': False}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion + ) + def test_set_volume_bootable(self): sot = volume.Volume(**VOLUME) From f41d60c7ef1aff0744ab62a4097370e1da5dbff9 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 19 Jul 2024 14:17:13 +0100 Subject: [PATCH 3519/3836] block storage: Add missing 'find_type' proxy API to v2 This was in v3 but not v2. Correct this. Change-Id: I56a724bb56a7e2de5ca2f0700d1bb92fa9ccdccd Signed-off-by: Stephen Finucane --- openstack/block_storage/v2/_proxy.py | 20 +++++++++++++++++++ .../tests/unit/block_storage/v2/test_proxy.py | 3 +++ 2 files changed, 23 insertions(+) diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 0dc067c75..b88dccffd 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -168,6 +168,26 @@ def get_type(self, type): """ return self._get(_type.Type, type) + def find_type(self, name_or_id, ignore_missing=True): + """Find a single volume type + + :param snapshot: The name or ID a volume type + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the volume type does not exist. + + :returns: One :class:`~openstack.block_storage.v2.type.Type` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple + resources are found. + """ + return self._find( + _type.Type, + name_or_id, + ignore_missing=ignore_missing, + ) + def types(self, **query): """Retrieve a generator of volume types diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index 21accfc2c..50cf48eb7 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -438,6 +438,9 @@ class TestType(TestVolumeProxy): def test_type_get(self): self.verify_get(self.proxy.get_type, type.Type) + def test_type_find(self): + self.verify_find(self.proxy.find_type, type.Type) + def test_types(self): self.verify_list(self.proxy.types, type.Type) From bd757116c2bf5162452f7aef2321183f6282eb89 Mon Sep 17 00:00:00 2001 From: elajkat Date: Tue, 28 May 2024 13:42:00 +0200 Subject: [PATCH 3520/3836] BGP: return list for get_dragents and get_bgp_speakers_hosted_by_dragent Closes-Bug: #2067039 Change-Id: Ia6df0d5cea4a5a26a9c634970ce85e2d3b76e689 --- openstack/network/v2/agent.py | 8 +++++++- openstack/network/v2/bgp_speaker.py | 7 +++++-- openstack/tests/unit/network/v2/test_agent.py | 18 ++++++++++++++---- .../tests/unit/network/v2/test_bgp_speaker.py | 19 +++++++++++++++---- ...ent-for-BGP-dragents-3608d8119012b11c.yaml | 5 +++++ 5 files changed, 46 insertions(+), 11 deletions(-) create mode 100644 releasenotes/notes/return-list-of-agent-for-BGP-dragents-3608d8119012b11c.yaml diff --git a/openstack/network/v2/agent.py b/openstack/network/v2/agent.py index ff5f664da..181d10dcf 100644 --- a/openstack/network/v2/agent.py +++ b/openstack/network/v2/agent.py @@ -11,6 +11,7 @@ # under the License. from openstack import exceptions +from openstack.network.v2 import bgp_speaker as _speaker from openstack import resource from openstack import utils @@ -112,12 +113,17 @@ def get_bgp_speakers_hosted_by_dragent(self, session): :param session: The session to communicate through. :type session: :class:`~keystoneauth1.adapter.Adapter` + + :returns: A list of BgpSpeakers + :rtype: :class:`~openstack.network.v2.bgp_speaker.BgpSpeaker` """ url = utils.urljoin(self.base_path, self.id, 'bgp-drinstances') resp = session.get(url) exceptions.raise_from_response(resp) self._body.attributes.update(resp.json()) - return resp.json() + speaker_ids = [sp['id'] for sp in resp.json()['bgp_speakers']] + speakers = _speaker.BgpSpeaker.list(session=session) + return [sp for sp in speakers if sp.id in speaker_ids] class NetworkHostingDHCPAgent(Agent): diff --git a/openstack/network/v2/bgp_speaker.py b/openstack/network/v2/bgp_speaker.py index 8c96764a1..1be9f0080 100644 --- a/openstack/network/v2/bgp_speaker.py +++ b/openstack/network/v2/bgp_speaker.py @@ -11,6 +11,7 @@ # under the License. from openstack import exceptions +from openstack.network.v2 import agent as _agent from openstack import resource from openstack import utils @@ -136,14 +137,16 @@ def get_bgp_dragents(self, session): :type session: :class:`~keystoneauth1.adapter.Adapter` :returns: The response as a list of dragents hosting a specific BGP Speaker. - + :rtype: :class:`~openstack.network.v2.agent.Agent` :raises: :class:`~openstack.exceptions.SDKException` on error. """ url = utils.urljoin(self.base_path, self.id, 'bgp-dragents') resp = session.get(url) exceptions.raise_from_response(resp) self._body.attributes.update(resp.json()) - return resp.json() + agent_ids = [ag['id'] for ag in resp.json()['agents']] + agents = _agent.Agent.list(session=session) + return [ag for ag in agents if ag.id in agent_ids] def add_bgp_speaker_to_dragent(self, session, bgp_agent_id): """Add BGP Speaker to a Dynamic Routing Agent diff --git a/openstack/tests/unit/network/v2/test_agent.py b/openstack/tests/unit/network/v2/test_agent.py index 80d4b8abe..16ad82bbd 100644 --- a/openstack/tests/unit/network/v2/test_agent.py +++ b/openstack/tests/unit/network/v2/test_agent.py @@ -13,6 +13,7 @@ from unittest import mock from openstack.network.v2 import agent +from openstack.network.v2 import bgp_speaker from openstack.tests.unit import base IDENTIFIER = 'IDENTIFIER' @@ -119,19 +120,28 @@ def test_remove_router_from_agent(self): 'agents/IDENTIFIER/l3-routers/', json=body ) - def test_get_bgp_speakers_hosted_by_dragent(self): + @mock.patch.object(bgp_speaker.BgpSpeaker, 'list') + def test_get_bgp_speakers_hosted_by_dragent(self, mock_list): sot = agent.Agent(**EXAMPLE) sess = mock.Mock() response = mock.Mock() - response.body = { - 'bgp_speakers': [{'name': 'bgp_speaker_1', 'ip_version': 4}] + speaker_body = { + 'bgp_speakers': [ + {'name': 'bgp_speaker_1', 'ip_version': 4, 'id': IDENTIFIER} + ] } + response.body = speaker_body + mock_list.return_value = [ + bgp_speaker.BgpSpeaker(**speaker_body['bgp_speakers'][0]) + ] response.json = mock.Mock(return_value=response.body) response.status_code = 200 sess.get = mock.Mock(return_value=response) resp = sot.get_bgp_speakers_hosted_by_dragent(sess) - self.assertEqual(resp, response.body) + self.assertEqual( + resp, [bgp_speaker.BgpSpeaker(**response.body['bgp_speakers'][0])] + ) sess.get.assert_called_with('agents/IDENTIFIER/bgp-drinstances') diff --git a/openstack/tests/unit/network/v2/test_bgp_speaker.py b/openstack/tests/unit/network/v2/test_bgp_speaker.py index 848aa590d..56c4532d2 100644 --- a/openstack/tests/unit/network/v2/test_bgp_speaker.py +++ b/openstack/tests/unit/network/v2/test_bgp_speaker.py @@ -12,6 +12,7 @@ from unittest import mock +from openstack.network.v2 import agent from openstack.network.v2 import bgp_speaker from openstack.tests.unit import base @@ -143,12 +144,21 @@ def test_get_advertised_routes(self): sess.get.assert_called_with(url) self.assertEqual(ret, response.body) - def test_get_bgp_dragents(self): + @mock.patch.object(agent.Agent, 'list') + def test_get_bgp_dragents(self, mock_list): sot = bgp_speaker.BgpSpeaker(**EXAMPLE) response = mock.Mock() - response.body = { - 'agents': [{'binary': 'neutron-bgp-dragent', 'alive': True}] + agent_body = { + 'agents': [ + { + 'binary': 'neutron-bgp-dragent', + 'alive': True, + 'id': IDENTIFIER, + } + ] } + response.body = agent_body + mock_list.return_value = [agent.Agent(**agent_body['agents'][0])] response.json = mock.Mock(return_value=response.body) response.status_code = 200 sess = mock.Mock() @@ -157,7 +167,8 @@ def test_get_bgp_dragents(self): url = 'bgp-speakers/IDENTIFIER/bgp-dragents' sess.get.assert_called_with(url) - self.assertEqual(ret, response.body) + print('BBBBB0 ', ret) + self.assertEqual(ret, [agent.Agent(**response.body['agents'][0])]) def test_add_bgp_speaker_to_dragent(self): sot = bgp_speaker.BgpSpeaker(**EXAMPLE) diff --git a/releasenotes/notes/return-list-of-agent-for-BGP-dragents-3608d8119012b11c.yaml b/releasenotes/notes/return-list-of-agent-for-BGP-dragents-3608d8119012b11c.yaml new file mode 100644 index 000000000..8c9b509db --- /dev/null +++ b/releasenotes/notes/return-list-of-agent-for-BGP-dragents-3608d8119012b11c.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + ``get_dragents`` and ``get_bgp_speakers_hosted_by_dragent`` return list + of Agents and BgpSpeakers, see https://launchpad.net/bugs/2067039 From c65b4d7c24b3ff8d347e1c0d5ece685575f1fa0d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 24 Jul 2024 13:09:09 +0100 Subject: [PATCH 3521/3836] cloud: Replace reference to removed variable We removed the floating network-specific caching in change Ife5bda82eeb10c093b6cb55b0027ec16c89734d7 but an errant rebase of change I10d3782899ac519f715d771d83303990a8289f04 reintroduced some use of the now removed private variables used for this caching. Remove them again. Change-Id: I5f867f91bb0e3a564d039d7184eadbe171e8f922 Signed-off-by: Stephen Finucane Closes-bug: #2073261 --- openstack/cloud/_floating_ip.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index e0c698a08..15725cdae 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -303,8 +303,7 @@ def _find_floating_network_by_router(self): 'network_id' ) if network_id: - self._floating_network_by_router = network_id - return self._floating_network_by_router + return network_id def available_floating_ip(self, network=None, server=None): """Get a floating IP from a network or a pool. From 1d95962881a74d3469d825173d9f8008801ed88c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 1 Jul 2024 15:16:50 +0100 Subject: [PATCH 3522/3836] pre-commit: Bump versions Change-Id: If5d9bd237432b53a9187cac1856215020dc4571f Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f88170eb9..1c0c8c0e5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,12 +22,12 @@ repos: hooks: - id: doc8 - repo: https://github.com/asottile/pyupgrade - rev: v3.15.2 + rev: v3.16.0 hooks: - id: pyupgrade args: ['--py38-plus'] - repo: https://github.com/psf/black - rev: 24.4.0 + rev: 24.4.2 hooks: - id: black args: ['-S', '-l', '79'] @@ -39,7 +39,7 @@ repos: - flake8-import-order~=0.18.2 exclude: '^(doc|releasenotes|tools)/.*$' - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.9.0 + rev: v1.10.1 hooks: - id: mypy additional_dependencies: From edd72883de8fc3efa3c49343a652312fc53a5d78 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 1 Jul 2024 11:04:35 +0100 Subject: [PATCH 3523/3836] cloud: Remove check for nova extensions All Nova extensions are enabled in API v2.1, which is the only API version we support now. There's no reason to query for these things. Change-Id: Ib12b2f4fe53182e047d6264de850178909b8fd5d Signed-off-by: Stephen Finucane --- openstack/cloud/_compute.py | 7 - openstack/cloud/_floating_ip.py | 6 - openstack/cloud/meta.py | 10 +- .../functional/cloud/test_floating_ip_pool.py | 7 - openstack/tests/unit/cloud/test_compute.py | 123 ------------------ .../tests/unit/cloud/test_floating_ip_pool.py | 57 -------- 6 files changed, 4 insertions(+), 206 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index c77d5bb73..c70ff4af7 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -115,13 +115,6 @@ def get_flavor_by_ram(self, ram, include=None, get_extra=True): ) ) - def _nova_extensions(self): - extensions = {e.alias for e in self.compute.extensions()} - return extensions - - def _has_nova_extension(self, extension_name): - return extension_name in self._nova_extensions() - def search_keypairs(self, name_or_id=None, filters=None): """Search keypairs. diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index 15725cdae..8e82f90ec 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -15,7 +15,6 @@ import warnings from openstack.cloud import _utils -from openstack.cloud import exc from openstack.cloud import meta from openstack import exceptions from openstack.network.v2._proxy import Proxy @@ -161,11 +160,6 @@ def list_floating_ip_pools(self): :returns: A list of floating IP pool objects """ - if not self._has_nova_extension('os-floating-ip-pools'): - raise exc.OpenStackCloudUnavailableExtension( - 'Floating IP pools extension is not available on target cloud' - ) - data = proxy._json_response( self.compute.get('os-floating-ip-pools'), error_message="Error fetching floating IP pool list", diff --git a/openstack/cloud/meta.py b/openstack/cloud/meta.py index 5b1571ab0..245b4e7c7 100644 --- a/openstack/cloud/meta.py +++ b/openstack/cloud/meta.py @@ -39,9 +39,8 @@ def find_nova_interfaces( # ext_tag is specified, but this interface has no tag # We could actually return right away as this means that # this cloud doesn't support OS-EXT-IPS. Nevertheless, - # it would be better to perform an explicit check. e.g.: - # cloud._has_nova_extension('OS-EXT-IPS') - # But this needs cloud to be passed to this function. + # it would be better to perform an explicit check + # but this needs cloud to be passed to this function. continue elif interface_spec['OS-EXT-IPS:type'] != ext_tag: # Type doesn't match, continue with next one @@ -52,9 +51,8 @@ def find_nova_interfaces( # mac_addr is specified, but this interface has no mac_addr # We could actually return right away as this means that # this cloud doesn't support OS-EXT-IPS-MAC. Nevertheless, - # it would be better to perform an explicit check. e.g.: - # cloud._has_nova_extension('OS-EXT-IPS-MAC') - # But this needs cloud to be passed to this function. + # it would be better to perform an explicit check + # but this needs cloud to be passed to this function. continue elif interface_spec['OS-EXT-IPS-MAC:mac_addr'] != mac_addr: # MAC doesn't match, continue with next one diff --git a/openstack/tests/functional/cloud/test_floating_ip_pool.py b/openstack/tests/functional/cloud/test_floating_ip_pool.py index c963aa7b0..2d0161c70 100644 --- a/openstack/tests/functional/cloud/test_floating_ip_pool.py +++ b/openstack/tests/functional/cloud/test_floating_ip_pool.py @@ -32,13 +32,6 @@ class TestFloatingIPPool(base.BaseFunctionalTest): - def setUp(self): - super().setUp() - - if not self.user_cloud._has_nova_extension('os-floating-ip-pools'): - # Skipping this test is floating-ip-pool extension is not - # available on the testing cloud - self.skip('Floating IP pools extension is not available') def test_list_floating_ip_pools(self): pools = self.user_cloud.list_floating_ip_pools() diff --git a/openstack/tests/unit/cloud/test_compute.py b/openstack/tests/unit/cloud/test_compute.py index b4c3d0340..2c06ecc85 100644 --- a/openstack/tests/unit/cloud/test_compute.py +++ b/openstack/tests/unit/cloud/test_compute.py @@ -17,129 +17,6 @@ from openstack.tests.unit import base -class TestNovaExtensions(base.TestCase): - def test__nova_extensions(self): - body = [ - { - "updated": "2014-12-03T00:00:00Z", - "name": "Multinic", - "links": [], - "namespace": "http://openstack.org/compute/ext/fake_xml", - "alias": "NMN", - "description": "Multiple network support.", - }, - { - "updated": "2014-12-03T00:00:00Z", - "name": "DiskConfig", - "links": [], - "namespace": "http://openstack.org/compute/ext/fake_xml", - "alias": "OS-DCF", - "description": "Disk Management Extension.", - }, - ] - self.register_uris( - [ - dict( - method='GET', - uri='{endpoint}/extensions'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), - json=dict(extensions=body), - ) - ] - ) - extensions = self.cloud._nova_extensions() - self.assertEqual({'NMN', 'OS-DCF'}, extensions) - - self.assert_calls() - - def test__nova_extensions_fails(self): - self.register_uris( - [ - dict( - method='GET', - uri='{endpoint}/extensions'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), - status_code=404, - ), - ] - ) - self.assertRaises( - exceptions.NotFoundException, self.cloud._nova_extensions - ) - - self.assert_calls() - - def test__has_nova_extension(self): - body = [ - { - "updated": "2014-12-03T00:00:00Z", - "name": "Multinic", - "links": [], - "namespace": "http://openstack.org/compute/ext/fake_xml", - "alias": "NMN", - "description": "Multiple network support.", - }, - { - "updated": "2014-12-03T00:00:00Z", - "name": "DiskConfig", - "links": [], - "namespace": "http://openstack.org/compute/ext/fake_xml", - "alias": "OS-DCF", - "description": "Disk Management Extension.", - }, - ] - self.register_uris( - [ - dict( - method='GET', - uri='{endpoint}/extensions'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), - json=dict(extensions=body), - ) - ] - ) - self.assertTrue(self.cloud._has_nova_extension('NMN')) - - self.assert_calls() - - def test__has_nova_extension_missing(self): - body = [ - { - "updated": "2014-12-03T00:00:00Z", - "name": "Multinic", - "links": [], - "namespace": "http://openstack.org/compute/ext/fake_xml", - "alias": "NMN", - "description": "Multiple network support.", - }, - { - "updated": "2014-12-03T00:00:00Z", - "name": "DiskConfig", - "links": [], - "namespace": "http://openstack.org/compute/ext/fake_xml", - "alias": "OS-DCF", - "description": "Disk Management Extension.", - }, - ] - self.register_uris( - [ - dict( - method='GET', - uri='{endpoint}/extensions'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), - json=dict(extensions=body), - ) - ] - ) - self.assertFalse(self.cloud._has_nova_extension('invalid')) - - self.assert_calls() - - class TestServers(base.TestCase): def test_get_server(self): server1 = fakes.make_fake_server('123', 'mickey') diff --git a/openstack/tests/unit/cloud/test_floating_ip_pool.py b/openstack/tests/unit/cloud/test_floating_ip_pool.py index bfa5645aa..ee3d55eae 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_pool.py +++ b/openstack/tests/unit/cloud/test_floating_ip_pool.py @@ -19,7 +19,6 @@ Test floating IP pool resource (managed by nova) """ -from openstack import exceptions from openstack.tests import fakes from openstack.tests.unit import base @@ -30,24 +29,6 @@ class TestFloatingIPPool(base.TestCase): def test_list_floating_ip_pools(self): self.register_uris( [ - dict( - method='GET', - uri='{endpoint}/extensions'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), - json={ - 'extensions': [ - { - 'alias': 'os-floating-ip-pools', - 'updated': '2014-12-03T00:00:00Z', - 'name': 'FloatingIpPools', - 'links': [], - 'namespace': 'http://docs.openstack.org/compute/ext/fake_xml', # noqa: E501 - 'description': 'Floating IPs support.', - } - ] - }, - ), dict( method='GET', uri='{endpoint}/os-floating-ip-pools'.format( @@ -63,41 +44,3 @@ def test_list_floating_ip_pools(self): self.assertCountEqual(floating_ip_pools, self.pools) self.assert_calls() - - def test_list_floating_ip_pools_exception(self): - self.register_uris( - [ - dict( - method='GET', - uri='{endpoint}/extensions'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), - json={ - 'extensions': [ - { - 'alias': 'os-floating-ip-pools', - 'updated': '2014-12-03T00:00:00Z', - 'name': 'FloatingIpPools', - 'links': [], - 'namespace': 'http://docs.openstack.org/compute/ext/fake_xml', # noqa: E501 - 'description': 'Floating IPs support.', - } - ] - }, - ), - dict( - method='GET', - uri='{endpoint}/os-floating-ip-pools'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), - status_code=404, - ), - ] - ) - - self.assertRaises( - exceptions.SDKException, - self.cloud.list_floating_ip_pools, - ) - - self.assert_calls() From 04a1cd2fb75638521f7a4cbaeb39a9e6325d7335 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 24 Jul 2024 12:39:34 +0100 Subject: [PATCH 3524/3836] cloud: Fix annotations for proxy method These align with the definitions in ServiceMixin. Change-Id: I6cd8970b707f062febd5d4b65749731a249df6b8 Signed-off-by: Stephen Finucane --- openstack/cloud/_accelerator.py | 4 ++-- openstack/cloud/_baremetal.py | 4 ++-- openstack/cloud/_block_storage.py | 4 ++-- openstack/cloud/_compute.py | 4 ++-- openstack/cloud/_dns.py | 4 ++-- openstack/cloud/_floating_ip.py | 4 ++-- openstack/cloud/_identity.py | 4 ++-- openstack/cloud/_image.py | 4 ++-- openstack/cloud/_network.py | 4 ++-- openstack/cloud/_network_common.py | 3 +++ openstack/cloud/_object_store.py | 4 ++-- openstack/cloud/_orchestration.py | 4 ++-- openstack/cloud/_security_group.py | 4 ++-- openstack/cloud/_shared_file_system.py | 5 +++-- 14 files changed, 30 insertions(+), 26 deletions(-) diff --git a/openstack/cloud/_accelerator.py b/openstack/cloud/_accelerator.py index c54ff8c72..f1e268952 100644 --- a/openstack/cloud/_accelerator.py +++ b/openstack/cloud/_accelerator.py @@ -10,11 +10,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from openstack.accelerator.v2._proxy import Proxy +from openstack.accelerator import accelerator_service class AcceleratorCloudMixin: - accelerator: Proxy + accelerator: accelerator_service.AcceleratorService def list_deployables(self, filters=None): """List all available deployables. diff --git a/openstack/cloud/_baremetal.py b/openstack/cloud/_baremetal.py index f2a94dc79..b2fe3fcfc 100644 --- a/openstack/cloud/_baremetal.py +++ b/openstack/cloud/_baremetal.py @@ -16,7 +16,7 @@ import jsonpatch -from openstack.baremetal.v1._proxy import Proxy +from openstack.baremetal import baremetal_service from openstack import exceptions from openstack import warnings as os_warnings @@ -42,7 +42,7 @@ def _normalize_port_list(nics): class BaremetalCloudMixin: - baremetal: Proxy + baremetal: baremetal_service.BaremetalService def list_nics(self): """Return a list of all bare metal ports.""" diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index 10551d3df..6b37d0af5 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -12,14 +12,14 @@ import warnings -from openstack.block_storage.v3._proxy import Proxy +from openstack.block_storage import block_storage_service from openstack.cloud import _utils from openstack import exceptions from openstack import warnings as os_warnings class BlockStorageCloudMixin: - block_storage: Proxy + block_storage: block_storage_service.BlockStorageService # TODO(stephenfin): Remove 'cache' in a future major version def list_volumes(self, cache=True): diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index c70ff4af7..430f24faa 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -20,7 +20,7 @@ from openstack.cloud import _utils from openstack.cloud import exc from openstack.cloud import meta -from openstack.compute.v2._proxy import Proxy +from openstack.compute import compute_service from openstack.compute.v2 import server as _server from openstack import exceptions from openstack import utils @@ -69,7 +69,7 @@ def _pop_or_get(resource, key, default, strict): class ComputeCloudMixin: - compute: Proxy + compute: compute_service.ComputeService @property def _compute_region(self): diff --git a/openstack/cloud/_dns.py b/openstack/cloud/_dns.py index c0bca9102..2122070ac 100644 --- a/openstack/cloud/_dns.py +++ b/openstack/cloud/_dns.py @@ -11,13 +11,13 @@ # limitations under the License. from openstack.cloud import _utils -from openstack.dns.v2._proxy import Proxy +from openstack.dns import dns_service from openstack import exceptions from openstack import resource class DnsCloudMixin: - dns: Proxy + dns: dns_service.DnsService def list_zones(self, filters=None): """List all available zones. diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index 8e82f90ec..33a33ce66 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -17,14 +17,14 @@ from openstack.cloud import _utils from openstack.cloud import meta from openstack import exceptions -from openstack.network.v2._proxy import Proxy +from openstack.network import network_service from openstack import proxy from openstack import utils from openstack import warnings as os_warnings class FloatingIPCloudMixin: - network: Proxy + network: network_service.NetworkService def __init__(self): self.private = self.config.config.get('private', False) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index 4ce6aaa9a..5509fad86 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -14,13 +14,13 @@ from openstack.cloud import _utils from openstack import exceptions -from openstack.identity.v3._proxy import Proxy +from openstack.identity import identity_service from openstack import utils from openstack import warnings as os_warnings class IdentityCloudMixin: - identity: Proxy + identity: identity_service.IdentityService def _get_project_id_param_dict(self, name_or_id): if name_or_id: diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py index 685dce2f4..c42f591e5 100644 --- a/openstack/cloud/_image.py +++ b/openstack/cloud/_image.py @@ -12,12 +12,12 @@ from openstack.cloud import _utils from openstack import exceptions -from openstack.image.v2._proxy import Proxy +from openstack.image import image_service from openstack import utils class ImageCloudMixin: - image: Proxy + image: image_service.ImageService def __init__(self): self.image_api_use_tasks = self.config.config['image_api_use_tasks'] diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index 3d25bbdd2..0ad86c1c1 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -13,11 +13,11 @@ from openstack.cloud import _utils from openstack.cloud import exc from openstack import exceptions -from openstack.network.v2._proxy import Proxy +from openstack.network import network_service class NetworkCloudMixin: - network: Proxy + network: network_service.NetworkService def _neutron_extensions(self): extensions = set() diff --git a/openstack/cloud/_network_common.py b/openstack/cloud/_network_common.py index b3d3a074b..0c233736c 100644 --- a/openstack/cloud/_network_common.py +++ b/openstack/cloud/_network_common.py @@ -13,12 +13,15 @@ import threading from openstack import exceptions +from openstack.network import network_service class NetworkCommonCloudMixin: """Shared networking functions used by FloatingIP, Network, Compute classes.""" + network: network_service.NetworkService + def __init__(self): self._external_ipv4_names = self.config.get_external_ipv4_networks() self._internal_ipv4_names = self.config.get_internal_ipv4_networks() diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index 1dcba4876..2d1b0d663 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -17,7 +17,7 @@ from openstack.cloud import _utils from openstack import exceptions -from openstack.object_store.v1._proxy import Proxy +from openstack.object_store import object_store_service DEFAULT_OBJECT_SEGMENT_SIZE = 1073741824 # 1GB # This halves the current default for Swift @@ -31,7 +31,7 @@ class ObjectStoreCloudMixin: - object_store: Proxy + object_store: object_store_service.ObjectStoreService # TODO(stephenfin): Remove 'full_listing' as it's a noop def list_containers(self, full_listing=True, prefix=None): diff --git a/openstack/cloud/_orchestration.py b/openstack/cloud/_orchestration.py index 0d9e7d7f4..796bec412 100644 --- a/openstack/cloud/_orchestration.py +++ b/openstack/cloud/_orchestration.py @@ -12,12 +12,12 @@ from openstack.cloud import _utils from openstack import exceptions +from openstack.orchestration import orchestration_service from openstack.orchestration.util import event_utils -from openstack.orchestration.v1._proxy import Proxy class OrchestrationCloudMixin: - orchestration: Proxy + orchestration: orchestration_service.OrchestrationService def get_template_contents( self, diff --git a/openstack/cloud/_security_group.py b/openstack/cloud/_security_group.py index d1af47a54..84903ed1c 100644 --- a/openstack/cloud/_security_group.py +++ b/openstack/cloud/_security_group.py @@ -13,13 +13,13 @@ from openstack.cloud import _utils from openstack.cloud import exc from openstack import exceptions -from openstack.network.v2._proxy import Proxy +from openstack.network import network_service from openstack import proxy from openstack import utils class SecurityGroupCloudMixin: - network: Proxy + network: network_service.NetworkService def __init__(self): self.secgroup_source = self.config.config['secgroup_source'] diff --git a/openstack/cloud/_shared_file_system.py b/openstack/cloud/_shared_file_system.py index 87e907371..cb5deac49 100644 --- a/openstack/cloud/_shared_file_system.py +++ b/openstack/cloud/_shared_file_system.py @@ -9,11 +9,12 @@ # 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.shared_file_system.v2._proxy import Proxy + +from openstack.shared_file_system import shared_file_system_service class SharedFileSystemCloudMixin: - share: Proxy + share: shared_file_system_service.SharedFilesystemService def list_share_availability_zones(self): """List all availability zones for the Shared File Systems service. From 200e1524acd24a6c2bddace781c78f5a6891406b Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 24 Jul 2024 12:29:52 +0100 Subject: [PATCH 3525/3836] mypy: Enable checks for openstack.tests.fixtures Change-Id: I718cc71e1b51d951a84c0f09dd16d5df6c9bd113 Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 8 +------- openstack/tests/fixtures.py | 2 +- setup.cfg | 17 ++++++++++------- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1c0c8c0e5..db9889ac7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -50,13 +50,7 @@ repos: # keep this in-sync with '[mypy] exclude' in 'setup.cfg' exclude: | (?x)( - openstack/tests/ansible/.* - | openstack/tests/functional/.* - | openstack/tests/unit/.* - | openstack/tests/fixtures.py - | openstack/cloud/.* - | openstack/fixture/.* - | doc/.* + doc/.* | examples/.* | releasenotes/.* ) diff --git a/openstack/tests/fixtures.py b/openstack/tests/fixtures.py index 463734dd2..a7c202473 100644 --- a/openstack/tests/fixtures.py +++ b/openstack/tests/fixtures.py @@ -55,4 +55,4 @@ def setUp(self): self.addCleanup(self._reset_warning_filters) def _reset_warning_filters(self): - warnings.filters[:] = self._original_warning_filters + warnings.filters[:] = self._original_warning_filters # type: ignore[index] diff --git a/setup.cfg b/setup.cfg index f1a352187..f6496594e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -43,13 +43,16 @@ warn_unused_ignores = false # TODO(stephenfin) Eventually we should remove everything here except the # unit tests module exclude = (?x)( - openstack/tests/ansible - | openstack/tests/functional - | openstack/tests/unit - | openstack/tests/fixtures.py - | openstack/cloud - | openstack/fixture - | doc + doc | examples | releasenotes ) + +[mypy-openstack.cloud.*] +ignore_errors = true + +[mypy-openstack.tests.functional.*] +ignore_errors = true + +[mypy-openstack.tests.unit.*] +ignore_errors = true From c842154091969b5dd56b500421f60d6c49857ee5 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 13 Jun 2024 13:15:39 +0100 Subject: [PATCH 3526/3836] cloud: Make _OpenStackCloudMixin subclass ServicesMixin This is the next step in normalizing the class hierarchy in 'openstack.cloud'. We also fix some docstrings while we're here. Change-Id: Ic6762a131e03f78f1d290b0065f3628d3e478f8d Signed-off-by: Stephen Finucane --- openstack/cloud/openstackcloud.py | 77 +++++++++++++++---------------- openstack/connection.py | 2 - 2 files changed, 37 insertions(+), 42 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 5f4ff9ede..54cb80b4c 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -22,6 +22,7 @@ import requestsexceptions from openstack import _log +from openstack import _services_mixin from openstack.cloud import _object_store from openstack.cloud import _utils from openstack.cloud import meta @@ -37,7 +38,7 @@ OBJECT_CONTAINER_ACLS = _object_store.OBJECT_CONTAINER_ACLS -class _OpenStackCloudMixin: +class _OpenStackCloudMixin(_services_mixin.ServicesMixin): """Represent a connection to an OpenStack Cloud. OpenStackCloud is the entry point for all cloud operations, regardless @@ -46,9 +47,6 @@ class _OpenStackCloudMixin: REST API operation oriented. For instance, one will request a Floating IP and that Floating IP will be actualized either via neutron or via nova depending on how this particular cloud has decided to arrange itself. - - :param bool strict: Only return documented attributes for each resource - as per the Data Model contract. (Default False) """ _OBJECT_MD5_KEY = 'x-sdk-md5' @@ -130,17 +128,17 @@ def connect_as(self, **kwargs): .. code-block:: python - conn = openstack.connect(cloud='example') - # Work normally - servers = conn.list_servers() - conn2 = conn.connect_as(username='different-user', password='') - # Work as different-user - servers = conn2.list_servers() + conn = openstack.connect(cloud='example') + # Work normally + servers = conn.list_servers() + conn2 = conn.connect_as(username='different-user', password='') + # Work as different-user + servers = conn2.list_servers() :param kwargs: keyword arguments can contain anything that would - normally go in an auth dict. They will override the same - settings from the parent cloud as appropriate. Entries - that do not want to be overridden can be ommitted. + normally go in an auth dict. They will override the same settings + from the parent cloud as appropriate. Entries that do not want to + be overridden can be ommitted. """ if self.config._openstack_config: @@ -202,15 +200,15 @@ def connect_as_project(self, project): .. code-block:: python - cloud = openstack.connect(cloud='example') - # Work normally - servers = cloud.list_servers() - cloud2 = cloud.connect_as_project('different-project') - # Work in different-project - servers = cloud2.list_servers() + cloud = openstack.connect(cloud='example') + # Work normally + servers = cloud.list_servers() + cloud2 = cloud.connect_as_project('different-project') + # Work in different-project + servers = cloud2.list_servers() :param project: Either a project name or a project dict as returned by - `list_projects`. + ``list_projects``. """ auth = {} if isinstance(project, dict): @@ -230,25 +228,25 @@ def global_request(self, global_request_id): .. code-block:: python - from oslo_context import context - cloud = openstack.connect(cloud='example') - # Work normally - servers = cloud.list_servers() - cloud2 = cloud.global_request(context.generate_request_id()) - # cloud2 sends all requests with global_request_id set - servers = cloud2.list_servers() + from oslo_context import context + cloud = openstack.connect(cloud='example') + # Work normally + servers = cloud.list_servers() + cloud2 = cloud.global_request(context.generate_request_id()) + # cloud2 sends all requests with global_request_id set + servers = cloud2.list_servers() Additionally, this can be used as a context manager: .. code-block:: python - from oslo_context import context - c = openstack.connect(cloud='example') - # Work normally - servers = c.list_servers() - with c.global_request(context.generate_request_id()) as c2: - # c2 sends all requests with global_request_id set - servers = c2.list_servers() + from oslo_context import context + c = openstack.connect(cloud='example') + # Work normally + servers = c.list_servers() + with c.global_request(context.generate_request_id()) as c2: + # c2 sends all requests with global_request_id set + servers = c2.list_servers() :param global_request_id: The `global_request_id` to send. """ @@ -329,12 +327,11 @@ def endpoint_for(self, service_type, interface=None, region_name=None): :meth:`~openstack.config.cloud_region.CloudRegion.get_endpoint_from_catalog` :param service_type: Service Type of the endpoint to search for. - :param interface: - Interface of the endpoint to search for. Optional, defaults to - the configured value for interface for this Connection. - :param region_name: - Region Name of the endpoint to search for. Optional, defaults to - the configured value for region_name for this Connection. + :param interface: Interface of the endpoint to search for. Optional, + defaults to the configured value for interface for this Connection. + :param region_name: Region Name of the endpoint to search for. + Optional, defaults to the configured value for region_name for this + Connection. :returns: The endpoint of the service, or None if not found. """ diff --git a/openstack/connection.py b/openstack/connection.py index a9d0d9392..806b99722 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -280,7 +280,6 @@ import requestsexceptions from openstack import _log -from openstack import _services_mixin from openstack.cloud import _accelerator from openstack.cloud import _baremetal from openstack.cloud import _block_storage @@ -346,7 +345,6 @@ def from_config(cloud=None, config=None, options=None, **kwargs): class Connection( - _services_mixin.ServicesMixin, _cloud._OpenStackCloudMixin, _accelerator.AcceleratorCloudMixin, _baremetal.BaremetalCloudMixin, From 4a7e36f31929b215c2ae78207c23bf8e786ccbe8 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 13 Jun 2024 13:24:27 +0100 Subject: [PATCH 3527/3836] cloud: Make service mixins subclass _OpenStackCloudMixin Another step towards normalization. Change-Id: I8c928f80ee6420b6eab2fa2d006783098056a649 Signed-off-by: Stephen Finucane --- openstack/cloud/_accelerator.py | 5 ++--- openstack/cloud/_baremetal.py | 5 ++--- openstack/cloud/_block_storage.py | 5 ++--- openstack/cloud/_coe.py | 4 +++- openstack/cloud/_compute.py | 5 ++--- openstack/cloud/_dns.py | 5 ++--- openstack/cloud/_floating_ip.py | 5 ++--- openstack/cloud/_identity.py | 5 ++--- openstack/cloud/_image.py | 5 ++--- openstack/cloud/_network.py | 5 ++--- openstack/cloud/_network_common.py | 3 ++- openstack/cloud/_object_store.py | 10 ++-------- openstack/cloud/_orchestration.py | 5 ++--- openstack/cloud/_security_group.py | 5 ++--- openstack/cloud/_shared_file_system.py | 5 ++--- openstack/cloud/openstackcloud.py | 6 ------ openstack/connection.py | 3 --- openstack/tests/unit/cloud/test_object.py | 14 ++++++++------ 18 files changed, 39 insertions(+), 61 deletions(-) diff --git a/openstack/cloud/_accelerator.py b/openstack/cloud/_accelerator.py index f1e268952..effd65c47 100644 --- a/openstack/cloud/_accelerator.py +++ b/openstack/cloud/_accelerator.py @@ -10,11 +10,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from openstack.accelerator import accelerator_service +from openstack.cloud import openstackcloud -class AcceleratorCloudMixin: - accelerator: accelerator_service.AcceleratorService +class AcceleratorCloudMixin(openstackcloud._OpenStackCloudMixin): def list_deployables(self, filters=None): """List all available deployables. diff --git a/openstack/cloud/_baremetal.py b/openstack/cloud/_baremetal.py index b2fe3fcfc..dd389aaa6 100644 --- a/openstack/cloud/_baremetal.py +++ b/openstack/cloud/_baremetal.py @@ -16,7 +16,7 @@ import jsonpatch -from openstack.baremetal import baremetal_service +from openstack.cloud import openstackcloud from openstack import exceptions from openstack import warnings as os_warnings @@ -41,8 +41,7 @@ def _normalize_port_list(nics): return ports -class BaremetalCloudMixin: - baremetal: baremetal_service.BaremetalService +class BaremetalCloudMixin(openstackcloud._OpenStackCloudMixin): def list_nics(self): """Return a list of all bare metal ports.""" diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index 6b37d0af5..72fe26405 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -12,14 +12,13 @@ import warnings -from openstack.block_storage import block_storage_service from openstack.cloud import _utils +from openstack.cloud import openstackcloud from openstack import exceptions from openstack import warnings as os_warnings -class BlockStorageCloudMixin: - block_storage: block_storage_service.BlockStorageService +class BlockStorageCloudMixin(openstackcloud._OpenStackCloudMixin): # TODO(stephenfin): Remove 'cache' in a future major version def list_volumes(self, cache=True): diff --git a/openstack/cloud/_coe.py b/openstack/cloud/_coe.py index 6bf0ba059..c64a96c01 100644 --- a/openstack/cloud/_coe.py +++ b/openstack/cloud/_coe.py @@ -11,10 +11,12 @@ # limitations under the License. from openstack.cloud import _utils +from openstack.cloud import openstackcloud from openstack import exceptions -class CoeCloudMixin: +class CoeCloudMixin(openstackcloud._OpenStackCloudMixin): + def list_coe_clusters(self): """List COE (Container Orchestration Engine) cluster. diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 430f24faa..d55537998 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -20,7 +20,7 @@ from openstack.cloud import _utils from openstack.cloud import exc from openstack.cloud import meta -from openstack.compute import compute_service +from openstack.cloud import openstackcloud from openstack.compute.v2 import server as _server from openstack import exceptions from openstack import utils @@ -68,8 +68,7 @@ def _pop_or_get(resource, key, default, strict): return resource.get(key, default) -class ComputeCloudMixin: - compute: compute_service.ComputeService +class ComputeCloudMixin(openstackcloud._OpenStackCloudMixin): @property def _compute_region(self): diff --git a/openstack/cloud/_dns.py b/openstack/cloud/_dns.py index 2122070ac..4864a1036 100644 --- a/openstack/cloud/_dns.py +++ b/openstack/cloud/_dns.py @@ -11,13 +11,12 @@ # limitations under the License. from openstack.cloud import _utils -from openstack.dns import dns_service +from openstack.cloud import openstackcloud from openstack import exceptions from openstack import resource -class DnsCloudMixin: - dns: dns_service.DnsService +class DnsCloudMixin(openstackcloud._OpenStackCloudMixin): def list_zones(self, filters=None): """List all available zones. diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index 33a33ce66..9a93b025a 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -16,15 +16,14 @@ from openstack.cloud import _utils from openstack.cloud import meta +from openstack.cloud import openstackcloud from openstack import exceptions -from openstack.network import network_service from openstack import proxy from openstack import utils from openstack import warnings as os_warnings -class FloatingIPCloudMixin: - network: network_service.NetworkService +class FloatingIPCloudMixin(openstackcloud._OpenStackCloudMixin): def __init__(self): self.private = self.config.config.get('private', False) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index 5509fad86..8973266f3 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -13,14 +13,13 @@ import warnings from openstack.cloud import _utils +from openstack.cloud import openstackcloud from openstack import exceptions -from openstack.identity import identity_service from openstack import utils from openstack import warnings as os_warnings -class IdentityCloudMixin: - identity: identity_service.IdentityService +class IdentityCloudMixin(openstackcloud._OpenStackCloudMixin): def _get_project_id_param_dict(self, name_or_id): if name_or_id: diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py index c42f591e5..1c757be89 100644 --- a/openstack/cloud/_image.py +++ b/openstack/cloud/_image.py @@ -11,13 +11,12 @@ # limitations under the License. from openstack.cloud import _utils +from openstack.cloud import openstackcloud from openstack import exceptions -from openstack.image import image_service from openstack import utils -class ImageCloudMixin: - image: image_service.ImageService +class ImageCloudMixin(openstackcloud._OpenStackCloudMixin): def __init__(self): self.image_api_use_tasks = self.config.config['image_api_use_tasks'] diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index 0ad86c1c1..5ada15ab5 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -12,12 +12,11 @@ from openstack.cloud import _utils from openstack.cloud import exc +from openstack.cloud import openstackcloud from openstack import exceptions -from openstack.network import network_service -class NetworkCloudMixin: - network: network_service.NetworkService +class NetworkCloudMixin(openstackcloud._OpenStackCloudMixin): def _neutron_extensions(self): extensions = set() diff --git a/openstack/cloud/_network_common.py b/openstack/cloud/_network_common.py index 0c233736c..6cec44f5e 100644 --- a/openstack/cloud/_network_common.py +++ b/openstack/cloud/_network_common.py @@ -12,11 +12,12 @@ import threading +from openstack.cloud import openstackcloud from openstack import exceptions from openstack.network import network_service -class NetworkCommonCloudMixin: +class NetworkCommonCloudMixin(openstackcloud._OpenStackCloudMixin): """Shared networking functions used by FloatingIP, Network, Compute classes.""" diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index 2d1b0d663..c16baa7b6 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -16,13 +16,8 @@ import keystoneauth1.exceptions from openstack.cloud import _utils +from openstack.cloud import openstackcloud from openstack import exceptions -from openstack.object_store import object_store_service - -DEFAULT_OBJECT_SEGMENT_SIZE = 1073741824 # 1GB -# This halves the current default for Swift -DEFAULT_MAX_FILE_SIZE = (5 * 1024 * 1024 * 1024 + 2) / 2 - OBJECT_CONTAINER_ACLS = { 'public': '.r:*,.rlistings', @@ -30,8 +25,7 @@ } -class ObjectStoreCloudMixin: - object_store: object_store_service.ObjectStoreService +class ObjectStoreCloudMixin(openstackcloud._OpenStackCloudMixin): # TODO(stephenfin): Remove 'full_listing' as it's a noop def list_containers(self, full_listing=True, prefix=None): diff --git a/openstack/cloud/_orchestration.py b/openstack/cloud/_orchestration.py index 796bec412..5be0c9918 100644 --- a/openstack/cloud/_orchestration.py +++ b/openstack/cloud/_orchestration.py @@ -11,13 +11,12 @@ # limitations under the License. from openstack.cloud import _utils +from openstack.cloud import openstackcloud from openstack import exceptions -from openstack.orchestration import orchestration_service from openstack.orchestration.util import event_utils -class OrchestrationCloudMixin: - orchestration: orchestration_service.OrchestrationService +class OrchestrationCloudMixin(openstackcloud._OpenStackCloudMixin): def get_template_contents( self, diff --git a/openstack/cloud/_security_group.py b/openstack/cloud/_security_group.py index 84903ed1c..985446045 100644 --- a/openstack/cloud/_security_group.py +++ b/openstack/cloud/_security_group.py @@ -12,14 +12,13 @@ from openstack.cloud import _utils from openstack.cloud import exc +from openstack.cloud import openstackcloud from openstack import exceptions -from openstack.network import network_service from openstack import proxy from openstack import utils -class SecurityGroupCloudMixin: - network: network_service.NetworkService +class SecurityGroupCloudMixin(openstackcloud._OpenStackCloudMixin): def __init__(self): self.secgroup_source = self.config.config['secgroup_source'] diff --git a/openstack/cloud/_shared_file_system.py b/openstack/cloud/_shared_file_system.py index cb5deac49..61affe547 100644 --- a/openstack/cloud/_shared_file_system.py +++ b/openstack/cloud/_shared_file_system.py @@ -10,11 +10,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from openstack.shared_file_system import shared_file_system_service +from openstack.cloud import openstackcloud -class SharedFileSystemCloudMixin: - share: shared_file_system_service.SharedFilesystemService +class SharedFileSystemCloudMixin(openstackcloud._OpenStackCloudMixin): def list_share_availability_zones(self): """List all availability zones for the Shared File Systems service. diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 54cb80b4c..87fc28147 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -23,7 +23,6 @@ from openstack import _log from openstack import _services_mixin -from openstack.cloud import _object_store from openstack.cloud import _utils from openstack.cloud import meta import openstack.config @@ -32,11 +31,6 @@ from openstack import proxy from openstack import utils -DEFAULT_OBJECT_SEGMENT_SIZE = _object_store.DEFAULT_OBJECT_SEGMENT_SIZE -# This halves the current default for Swift -DEFAULT_MAX_FILE_SIZE = _object_store.DEFAULT_MAX_FILE_SIZE -OBJECT_CONTAINER_ACLS = _object_store.OBJECT_CONTAINER_ACLS - class _OpenStackCloudMixin(_services_mixin.ServicesMixin): """Represent a connection to an OpenStack Cloud. diff --git a/openstack/connection.py b/openstack/connection.py index 806b99722..996586f2a 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -295,7 +295,6 @@ from openstack.cloud import _orchestration from openstack.cloud import _security_group from openstack.cloud import _shared_file_system -from openstack.cloud import openstackcloud as _cloud from openstack import config as _config from openstack.config import cloud_region from openstack import exceptions @@ -345,7 +344,6 @@ def from_config(cloud=None, config=None, options=None, **kwargs): class Connection( - _cloud._OpenStackCloudMixin, _accelerator.AcceleratorCloudMixin, _baremetal.BaremetalCloudMixin, _block_storage.BlockStorageCloudMixin, @@ -501,7 +499,6 @@ def __init__( self.strict_mode = strict # Call the _*CloudMixin constructors while we work on # integrating things better. - _cloud._OpenStackCloudMixin.__init__(self) _accelerator.AcceleratorCloudMixin.__init__(self) _baremetal.BaremetalCloudMixin.__init__(self) _block_storage.BlockStorageCloudMixin.__init__(self) diff --git a/openstack/tests/unit/cloud/test_object.py b/openstack/tests/unit/cloud/test_object.py index fc7a1f4c3..28a85fb89 100644 --- a/openstack/tests/unit/cloud/test_object.py +++ b/openstack/tests/unit/cloud/test_object.py @@ -17,7 +17,7 @@ import testtools -import openstack.cloud.openstackcloud as oc_oc +from openstack.cloud import _object_store from openstack import exceptions from openstack.object_store.v1 import _proxy from openstack.object_store.v1 import container @@ -107,7 +107,7 @@ def test_create_container_public(self): 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', 'Content-Length': '0', 'Content-Type': 'text/html; charset=UTF-8', - 'x-container-read': oc_oc.OBJECT_CONTAINER_ACLS[ + 'x-container-read': _object_store.OBJECT_CONTAINER_ACLS[ 'public' ], }, @@ -203,7 +203,9 @@ def test_delete_container_error(self): self.assert_calls() def test_update_container(self): - headers = {'x-container-read': oc_oc.OBJECT_CONTAINER_ACLS['public']} + headers = { + 'x-container-read': _object_store.OBJECT_CONTAINER_ACLS['public'] + } self.register_uris( [ dict( @@ -245,7 +247,7 @@ def test_set_container_access_public(self): status_code=204, validate=dict( headers={ - 'x-container-read': oc_oc.OBJECT_CONTAINER_ACLS[ + 'x-container-read': _object_store.OBJECT_CONTAINER_ACLS[ 'public' ] } @@ -267,7 +269,7 @@ def test_set_container_access_private(self): status_code=204, validate=dict( headers={ - 'x-container-read': oc_oc.OBJECT_CONTAINER_ACLS[ + 'x-container-read': _object_store.OBJECT_CONTAINER_ACLS[ 'private' ] } @@ -296,7 +298,7 @@ def test_get_container_access(self): uri=self.container_endpoint, headers={ 'x-container-read': str( - oc_oc.OBJECT_CONTAINER_ACLS['public'] + _object_store.OBJECT_CONTAINER_ACLS['public'] ) }, ) From 53a2ea6f6e2566adec5ae453366c3fd41fdccf90 Mon Sep 17 00:00:00 2001 From: Seungju Baek Date: Wed, 12 Jun 2024 22:29:33 +0900 Subject: [PATCH 3528/3836] Added missing stack documentation and rewrite stack proxy document - software_config - software_deployment - stack_environment - stack_files - stack_template - template Change-Id: I9675af84623831113ad72ca07ea95849eca05f9d --- doc/source/user/proxies/orchestration.rst | 28 ++++++++++++++++--- .../user/resources/orchestration/index.rst | 8 +++++- .../orchestration/v1/software_config.rst | 13 +++++++++ .../orchestration/v1/software_deployment.rst | 13 +++++++++ .../orchestration/v1/stack_environment.rst | 13 +++++++++ .../orchestration/v1/stack_files.rst | 13 +++++++++ .../orchestration/v1/stack_template.rst | 13 +++++++++ .../resources/orchestration/v1/template.rst | 13 +++++++++ 8 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 doc/source/user/resources/orchestration/v1/software_config.rst create mode 100644 doc/source/user/resources/orchestration/v1/software_deployment.rst create mode 100644 doc/source/user/resources/orchestration/v1/stack_environment.rst create mode 100644 doc/source/user/resources/orchestration/v1/stack_files.rst create mode 100644 doc/source/user/resources/orchestration/v1/stack_template.rst create mode 100644 doc/source/user/resources/orchestration/v1/template.rst diff --git a/doc/source/user/proxies/orchestration.rst b/doc/source/user/proxies/orchestration.rst index c9f3fa41f..76e555e93 100644 --- a/doc/source/user/proxies/orchestration.rst +++ b/doc/source/user/proxies/orchestration.rst @@ -18,10 +18,23 @@ Stack Operations .. autoclass:: openstack.orchestration.v1._proxy.Proxy :noindex: - :members: create_stack, check_stack, update_stack, delete_stack, find_stack, suspend_stack, resume_stack, - get_stack, get_stack_environment, get_stack_files, - get_stack_template, stacks, validate_template, resources, - export_stack + :members: create_stack, stacks,find_stack, update_stack, delete_stack, + get_stack, export_stack, + get_stack_template, get_stack_environment + +Stack Resource Operations +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.orchestration.v1._proxy.Proxy + :noindex: + :members: resources + +Stack Action Operations +^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.orchestration.v1._proxy.Proxy + :noindex: + :members: suspend_stack, resume_stack, check_stack Stack Event Operations ^^^^^^^^^^^^^^^^^^^^^^ @@ -30,6 +43,13 @@ Stack Event Operations :noindex: :members: stack_events +Stack Template Operations +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.orchestration.v1._proxy.Proxy + :noindex: + :members: validate_template + Software Configuration Operations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/orchestration/index.rst b/doc/source/user/resources/orchestration/index.rst index d0c53f6b8..4323a99ac 100644 --- a/doc/source/user/resources/orchestration/index.rst +++ b/doc/source/user/resources/orchestration/index.rst @@ -4,6 +4,12 @@ Orchestration Resources .. toctree:: :maxdepth: 1 - v1/stack v1/resource + v1/software_config + v1/software_deployment + v1/stack + v1/stack_environment v1/stack_event + v1/stack_files + v1/stack_template + v1/template diff --git a/doc/source/user/resources/orchestration/v1/software_config.rst b/doc/source/user/resources/orchestration/v1/software_config.rst new file mode 100644 index 000000000..b38adcc36 --- /dev/null +++ b/doc/source/user/resources/orchestration/v1/software_config.rst @@ -0,0 +1,13 @@ +openstack.orchestration.v1.software_config +========================================== + +.. automodule:: openstack.orchestration.v1.software_config + +The SoftwareConfig Class +------------------------ + +The ``SoftwareConfig`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.orchestration.v1.software_config.SoftwareConfig + :members: diff --git a/doc/source/user/resources/orchestration/v1/software_deployment.rst b/doc/source/user/resources/orchestration/v1/software_deployment.rst new file mode 100644 index 000000000..d39bd89a3 --- /dev/null +++ b/doc/source/user/resources/orchestration/v1/software_deployment.rst @@ -0,0 +1,13 @@ +openstack.orchestration.v1.software_deployment +============================================== + +.. automodule:: openstack.orchestration.v1.software_deployment + +The SoftwareDeployment Class +---------------------------- + +The ``SoftwareDeployment`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.orchestration.v1.software_deployment.SoftwareDeployment + :members: diff --git a/doc/source/user/resources/orchestration/v1/stack_environment.rst b/doc/source/user/resources/orchestration/v1/stack_environment.rst new file mode 100644 index 000000000..a159e0401 --- /dev/null +++ b/doc/source/user/resources/orchestration/v1/stack_environment.rst @@ -0,0 +1,13 @@ +openstack.orchestration.v1.stack_environment +============================================ + +.. automodule:: openstack.orchestration.v1.stack_environment + +The StackEnvironment Class +-------------------------- + +The ``StackEnvironment`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.orchestration.v1.stack_environment.StackEnvironment + :members: diff --git a/doc/source/user/resources/orchestration/v1/stack_files.rst b/doc/source/user/resources/orchestration/v1/stack_files.rst new file mode 100644 index 000000000..2144747e4 --- /dev/null +++ b/doc/source/user/resources/orchestration/v1/stack_files.rst @@ -0,0 +1,13 @@ +openstack.orchestration.v1.stack_files +====================================== + +.. automodule:: openstack.orchestration.v1.stack_files + +The StackFiles Class +-------------------- + +The ``StackFiles`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.orchestration.v1.stack_files.StackFiles + :members: diff --git a/doc/source/user/resources/orchestration/v1/stack_template.rst b/doc/source/user/resources/orchestration/v1/stack_template.rst new file mode 100644 index 000000000..ac2af5c9b --- /dev/null +++ b/doc/source/user/resources/orchestration/v1/stack_template.rst @@ -0,0 +1,13 @@ +openstack.orchestration.v1.stack_template +========================================= + +.. automodule:: openstack.orchestration.v1.stack_template + +The StackTemplate Class +----------------------- + +The ``StackTemplate`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.orchestration.v1.stack_template.StackTemplate + :members: diff --git a/doc/source/user/resources/orchestration/v1/template.rst b/doc/source/user/resources/orchestration/v1/template.rst new file mode 100644 index 000000000..ae9f2b453 --- /dev/null +++ b/doc/source/user/resources/orchestration/v1/template.rst @@ -0,0 +1,13 @@ +openstack.orchestration.v1.template +=================================== + +.. automodule:: openstack.orchestration.v1.template + +The Template Class +------------------ + +The ``Template`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.orchestration.v1.template.Template + :members: From d6e11e4874ed9de88874506de12262f1635eb1e3 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 27 Jun 2024 15:54:54 +0100 Subject: [PATCH 3529/3836] cloud: Reorganize _OpenStackCloudMixin, Connection (1/2) There's a lot of stuff in the latter that should really be in the former, now that it's the base class of many other things. Correct this. Change-Id: Icf2ff3ffa59e93b5cc15b2fe66668f731181c9ba Signed-off-by: Stephen Finucane --- openstack/cloud/_floating_ip.py | 4 +- openstack/cloud/_image.py | 4 +- openstack/cloud/_network_common.py | 5 +- openstack/cloud/_security_group.py | 4 +- openstack/cloud/openstackcloud.py | 187 +++++++++++++++++++++++++++-- openstack/connection.py | 122 +++---------------- 6 files changed, 211 insertions(+), 115 deletions(-) diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py index 9a93b025a..973254645 100644 --- a/openstack/cloud/_floating_ip.py +++ b/openstack/cloud/_floating_ip.py @@ -25,7 +25,9 @@ class FloatingIPCloudMixin(openstackcloud._OpenStackCloudMixin): - def __init__(self): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.private = self.config.config.get('private', False) self._floating_ip_source = self.config.config.get('floating_ip_source') diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py index 1c757be89..95f5f238d 100644 --- a/openstack/cloud/_image.py +++ b/openstack/cloud/_image.py @@ -18,7 +18,9 @@ class ImageCloudMixin(openstackcloud._OpenStackCloudMixin): - def __init__(self): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.image_api_use_tasks = self.config.config['image_api_use_tasks'] def search_images(self, name_or_id=None, filters=None): diff --git a/openstack/cloud/_network_common.py b/openstack/cloud/_network_common.py index 6cec44f5e..6d2f7cb4e 100644 --- a/openstack/cloud/_network_common.py +++ b/openstack/cloud/_network_common.py @@ -14,16 +14,15 @@ from openstack.cloud import openstackcloud from openstack import exceptions -from openstack.network import network_service class NetworkCommonCloudMixin(openstackcloud._OpenStackCloudMixin): """Shared networking functions used by FloatingIP, Network, Compute classes.""" - network: network_service.NetworkService + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) - def __init__(self): self._external_ipv4_names = self.config.get_external_ipv4_networks() self._internal_ipv4_names = self.config.get_internal_ipv4_networks() self._external_ipv6_names = self.config.get_external_ipv6_networks() diff --git a/openstack/cloud/_security_group.py b/openstack/cloud/_security_group.py index 985446045..924982839 100644 --- a/openstack/cloud/_security_group.py +++ b/openstack/cloud/_security_group.py @@ -20,7 +20,9 @@ class SecurityGroupCloudMixin(openstackcloud._OpenStackCloudMixin): - def __init__(self): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.secgroup_source = self.config.config['secgroup_source'] def search_security_groups(self, name_or_id=None, filters=None): diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 87fc28147..ed450d9eb 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -10,10 +10,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +import atexit +import concurrent.futures import copy import functools import queue import warnings +import weakref import dogpile.cache import keystoneauth1.exceptions @@ -26,7 +29,7 @@ from openstack.cloud import _utils from openstack.cloud import meta import openstack.config -from openstack.config import cloud_region as cloud_region_mod +import openstack.config.cloud_region from openstack import exceptions from openstack import proxy from openstack import utils @@ -55,9 +58,146 @@ class _OpenStackCloudMixin(_services_mixin.ServicesMixin): _SHADE_OBJECT_SHA256_KEY = 'x-object-meta-x-shade-sha256' _SHADE_OBJECT_AUTOCREATE_KEY = 'x-object-meta-x-shade-autocreated' - def __init__(self): + def __init__( + self, + cloud=None, + config=None, + session=None, + app_name=None, + app_version=None, + extra_services=None, + strict=False, + use_direct_get=False, + task_manager=None, + rate_limit=None, + oslo_conf=None, + service_types=None, + global_request_id=None, + strict_proxies=False, + pool_executor=None, + **kwargs, + ): + """Create a connection to a cloud. + + A connection needs information about how to connect, how to + authenticate and how to select the appropriate services to use. + + The recommended way to provide this information is by referencing + a named cloud config from an existing `clouds.yaml` file. The cloud + name ``envvars`` may be used to consume a cloud configured via ``OS_`` + environment variables. + + A pre-existing :class:`~openstack.config.cloud_region.CloudRegion` + object can be passed in lieu of a cloud name, for cases where the user + already has a fully formed CloudRegion and just wants to use it. + + Similarly, if for some reason the user already has a + :class:`~keystoneauth1.session.Session` and wants to use it, it may be + passed in. + + :param str cloud: Name of the cloud from config to use. + :param config: CloudRegion object representing the config for the + region of the cloud in question. + :type config: :class:`~openstack.config.cloud_region.CloudRegion` + :param session: A session object compatible with + :class:`~keystoneauth1.session.Session`. + :type session: :class:`~keystoneauth1.session.Session` + :param str app_name: Name of the application to be added to User Agent. + :param str app_version: Version of the application to be added to + User Agent. + :param extra_services: List of + :class:`~openstack.service_description.ServiceDescription` + objects describing services that openstacksdk otherwise does not + know about. + :param bool use_direct_get: + For get methods, make specific REST calls for server-side + filtering instead of making list calls and filtering client-side. + Default false. + :param task_manager: + Ignored. Exists for backwards compat during transition. Rate limit + parameters should be passed directly to the `rate_limit` parameter. + :param rate_limit: + Client-side rate limit, expressed in calls per second. The + parameter can either be a single float, or it can be a dict with + keys as service-type and values as floats expressing the calls + per second for that service. Defaults to None, which means no + rate-limiting is performed. + :param oslo_conf: An oslo.config CONF object. + :type oslo_conf: :class:`~oslo_config.cfg.ConfigOpts` + An oslo.config ``CONF`` object that has been populated with + ``keystoneauth1.loading.register_adapter_conf_options`` in + groups named by the OpenStack service's project name. + :param service_types: + A list/set of service types this Connection should support. All + other service types will be disabled (will error if used). + **Currently only supported in conjunction with the ``oslo_conf`` + kwarg.** + :param global_request_id: A Request-id to send with all interactions. + :param strict_proxies: + If True, check proxies on creation and raise + ServiceDiscoveryException if the service is unavailable. + :type strict_proxies: bool + Throw an ``openstack.exceptions.ServiceDiscoveryException`` if the + endpoint for a given service doesn't work. This is useful for + OpenStack services using sdk to talk to other OpenStack services + where it can be expected that the deployer config is correct and + errors should be reported immediately. + Default false. + :param pool_executor: + :type pool_executor: :class:`~futurist.Executor` + A futurist ``Executor`` object to be used for concurrent background + activities. Defaults to None in which case a ThreadPoolExecutor + will be created if needed. + :param kwargs: If a config is not provided, the rest of the parameters + provided are assumed to be arguments to be passed to the + CloudRegion constructor. + """ super().__init__() + self.config = config + self._extra_services = {} + self._strict_proxies = strict_proxies + if extra_services: + for service in extra_services: + self._extra_services[service.service_type] = service + + if not self.config: + if oslo_conf: + self.config = openstack.config.cloud_region.from_conf( + oslo_conf, + session=session, + app_name=app_name, + app_version=app_version, + service_types=service_types, + ) + elif session: + self.config = openstack.config.cloud_region.from_session( + session=session, + app_name=app_name, + app_version=app_version, + load_yaml_config=False, + load_envvars=False, + rate_limit=rate_limit, + **kwargs, + ) + else: + self.config = openstack.config.get_cloud_region( + cloud=cloud, + app_name=app_name, + app_version=app_version, + load_yaml_config=cloud is not None, + load_envvars=cloud is not None, + rate_limit=rate_limit, + **kwargs, + ) + + self._session = None + self._proxies = {} + self.__pool_executor = pool_executor + self._global_request_id = global_request_id + self.use_direct_get = use_direct_get + self.strict_mode = strict + self.log = _log.setup_logging('openstack') self.name = self.config.name @@ -104,14 +244,44 @@ def __init__(self): self._container_cache = dict() self._file_hash_cache = dict() - # self.__pool_executor = None - - self._raw_clients = {} - self._local_ipv6 = ( _utils.localhost_supports_ipv6() if not self.force_ipv4 else False ) + # Register cleanup steps + atexit.register(self.close) + + @property + def session(self): + if not self._session: + self._session = self.config.get_session() + # Hide a reference to the connection on the session to help with + # backwards compatibility for folks trying to just pass + # conn.session to a Resource method's session argument. + self.session._sdk_connection = weakref.proxy(self) + return self._session + + @property + def _pool_executor(self): + if not self.__pool_executor: + self.__pool_executor = concurrent.futures.ThreadPoolExecutor( + max_workers=5 + ) + return self.__pool_executor + + def close(self): + """Release any resources held open.""" + self.config.set_auth_cache() + if self.__pool_executor: + self.__pool_executor.shutdown() + atexit.unregister(self.close) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + def connect_as(self, **kwargs): """Make a new OpenStackCloud object with new auth context. @@ -214,6 +384,9 @@ def connect_as_project(self, project): auth['project_name'] = project return self.connect_as(**auth) + def set_global_request_id(self, global_request_id): + self._global_request_id = global_request_id + def global_request(self, global_request_id): """Make a new Connection object with a global request id set. @@ -245,7 +418,7 @@ def global_request(self, global_request_id): :param global_request_id: The `global_request_id` to send. """ params = copy.deepcopy(self.config.config) - cloud_region = cloud_region_mod.from_session( + cloud_region = openstack.config.cloud_region.from_session( session=self.session, app_name=self.config._app_name, app_version=self.config._app_version, diff --git a/openstack/connection.py b/openstack/connection.py index 996586f2a..e1368118d 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -270,11 +270,9 @@ Additional information about the services can be found in the :ref:`service-proxies` documentation. """ -import atexit -import concurrent.futures + import importlib.metadata as importlib_metadata import warnings -import weakref import keystoneauth1.exceptions import requestsexceptions @@ -296,7 +294,6 @@ from openstack.cloud import _security_group from openstack.cloud import _shared_file_system from openstack import config as _config -from openstack.config import cloud_region from openstack import exceptions from openstack import service_description @@ -454,66 +451,24 @@ def __init__( provided are assumed to be arguments to be passed to the CloudRegion constructor. """ - self.config = config - self._extra_services = {} - self._strict_proxies = strict_proxies - if extra_services: - for service in extra_services: - self._extra_services[service.service_type] = service - - if not self.config: - if oslo_conf: - self.config = cloud_region.from_conf( - oslo_conf, - session=session, - app_name=app_name, - app_version=app_version, - service_types=service_types, - ) - elif session: - self.config = cloud_region.from_session( - session=session, - app_name=app_name, - app_version=app_version, - load_yaml_config=False, - load_envvars=False, - rate_limit=rate_limit, - **kwargs - ) - else: - self.config = _config.get_cloud_region( - cloud=cloud, - app_name=app_name, - app_version=app_version, - load_yaml_config=cloud is not None, - load_envvars=cloud is not None, - rate_limit=rate_limit, - **kwargs - ) - - self._session = None - self._proxies = {} - self.__pool_executor = pool_executor - self._global_request_id = global_request_id - self.use_direct_get = use_direct_get - self.strict_mode = strict - # Call the _*CloudMixin constructors while we work on - # integrating things better. - _accelerator.AcceleratorCloudMixin.__init__(self) - _baremetal.BaremetalCloudMixin.__init__(self) - _block_storage.BlockStorageCloudMixin.__init__(self) - _coe.CoeCloudMixin.__init__(self) - _compute.ComputeCloudMixin.__init__(self) - _dns.DnsCloudMixin.__init__(self) - _floating_ip.FloatingIPCloudMixin.__init__(self) - _identity.IdentityCloudMixin.__init__(self) - _image.ImageCloudMixin.__init__(self) - _network_common.NetworkCommonCloudMixin.__init__(self) - _network.NetworkCloudMixin.__init__(self) - _object_store.ObjectStoreCloudMixin.__init__(self) - _orchestration.OrchestrationCloudMixin.__init__(self) - _security_group.SecurityGroupCloudMixin.__init__(self) - _shared_file_system.SharedFileSystemCloudMixin.__init__(self) + super().__init__( + cloud=cloud, + config=config, + session=session, + app_name=app_name, + app_version=app_version, + extra_services=extra_services, + strict=strict, + use_direct_get=use_direct_get, + task_manager=task_manager, + rate_limit=rate_limit, + oslo_conf=oslo_conf, + service_types=service_types, + global_request_id=global_request_id, + strict_proxies=strict_proxies, + pool_executor=pool_executor, + **kwargs + ) # Allow vendors to provide hooks. They will normally only receive a # connection object and a responsible to register additional services @@ -526,7 +481,7 @@ def __init__( # NOTE(gtema): no class name in the hook, plain module:function # Split string hook into module and function try: - (package_name, function) = vendor_hook.rsplit(':') + package_name, function = vendor_hook.rsplit(':') if package_name and function: ep = importlib_metadata.EntryPoint( @@ -557,19 +512,6 @@ def __init__( self.config.config['additional_metric_tags'] ) - # Register cleanup steps - atexit.register(self.close) - - @property - def session(self): - if not self._session: - self._session = self.config.get_session() - # Hide a reference to the connection on the session to help with - # backwards compatibility for folks trying to just pass - # conn.session to a Resource method's session argument. - self.session._sdk_connection = weakref.proxy(self) - return self._session - def add_service(self, service): """Add a service to the Connection. @@ -624,27 +566,3 @@ def authorize(self): return self.session.get_token() except keystoneauth1.exceptions.ClientException as e: raise exceptions.SDKException(e) - - @property - def _pool_executor(self): - if not self.__pool_executor: - self.__pool_executor = concurrent.futures.ThreadPoolExecutor( - max_workers=5 - ) - return self.__pool_executor - - def close(self): - """Release any resources held open.""" - self.config.set_auth_cache() - if self.__pool_executor: - self.__pool_executor.shutdown() - atexit.unregister(self.close) - - def set_global_request_id(self, global_request_id): - self._global_request_id = global_request_id - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - self.close() From 2f1829c1da6159794ed854541d51259651d5b08d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 30 Jul 2024 13:44:52 +0100 Subject: [PATCH 3530/3836] cloud: Reorganize _OpenStackCloudMixin, Connection (2/2) This is the second part, where we move stuff that is in the former but really belongs in the latter. This is anything to do with general session management or multiple services. Change-Id: Ic614b0464690b780d64b796d7fa91de853850d53 Signed-off-by: Stephen Finucane --- openstack/cloud/openstackcloud.py | 130 -------------------------- openstack/connection.py | 150 ++++++++++++++++++++++++++++-- 2 files changed, 141 insertions(+), 139 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index ed450d9eb..4d58af223 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -20,7 +20,6 @@ import dogpile.cache import keystoneauth1.exceptions -import keystoneauth1.session import requests.models import requestsexceptions @@ -282,108 +281,6 @@ def __enter__(self): def __exit__(self, exc_type, exc_value, traceback): self.close() - def connect_as(self, **kwargs): - """Make a new OpenStackCloud object with new auth context. - - Take the existing settings from the current cloud and construct a new - OpenStackCloud object with some of the auth settings overridden. This - is useful for getting an object to perform tasks with as another user, - or in the context of a different project. - - .. code-block:: python - - conn = openstack.connect(cloud='example') - # Work normally - servers = conn.list_servers() - conn2 = conn.connect_as(username='different-user', password='') - # Work as different-user - servers = conn2.list_servers() - - :param kwargs: keyword arguments can contain anything that would - normally go in an auth dict. They will override the same settings - from the parent cloud as appropriate. Entries that do not want to - be overridden can be ommitted. - """ - - if self.config._openstack_config: - config = self.config._openstack_config - else: - # TODO(mordred) Replace this with from_session - config = openstack.config.OpenStackConfig( - app_name=self.config._app_name, - app_version=self.config._app_version, - load_yaml_config=False, - ) - params = copy.deepcopy(self.config.config) - # Remove profile from current cloud so that overridding works - params.pop('profile', None) - - # Utility function to help with the stripping below. - def pop_keys(params, auth, name_key, id_key): - if name_key in auth or id_key in auth: - params['auth'].pop(name_key, None) - params['auth'].pop(id_key, None) - - # If there are user, project or domain settings in the incoming auth - # dict, strip out both id and name so that a user can say: - # cloud.connect_as(project_name='foo') - # and have that work with clouds that have a project_id set in their - # config. - for prefix in ('user', 'project'): - if prefix == 'user': - name_key = 'username' - else: - name_key = 'project_name' - id_key = f'{prefix}_id' - pop_keys(params, kwargs, name_key, id_key) - id_key = f'{prefix}_domain_id' - name_key = f'{prefix}_domain_name' - pop_keys(params, kwargs, name_key, id_key) - - for key, value in kwargs.items(): - params['auth'][key] = value - - cloud_region = config.get_one(**params) - # Attach the discovery cache from the old session so we won't - # double discover. - cloud_region._discovery_cache = self.session._discovery_cache - # Override the cloud name so that logging/location work right - cloud_region._name = self.name - cloud_region.config['profile'] = self.name - # Use self.__class__ so that we return whatever this if, like if it's - # a subclass in the case of shade wrapping sdk. - return self.__class__(config=cloud_region) - - def connect_as_project(self, project): - """Make a new OpenStackCloud object with a new project. - - Take the existing settings from the current cloud and construct a new - OpenStackCloud object with the project settings overridden. This - is useful for getting an object to perform tasks with as another user, - or in the context of a different project. - - .. code-block:: python - - cloud = openstack.connect(cloud='example') - # Work normally - servers = cloud.list_servers() - cloud2 = cloud.connect_as_project('different-project') - # Work in different-project - servers = cloud2.list_servers() - - :param project: Either a project name or a project dict as returned by - ``list_projects``. - """ - auth = {} - if isinstance(project, dict): - auth['project_id'] = project.get('id') - auth['project_name'] = project.get('name') - if project.get('domain_id'): - auth['project_domain_id'] = project['domain_id'] - else: - auth['project_name'] = project - return self.connect_as(**auth) - def set_global_request_id(self, global_request_id): self._global_request_id = global_request_id @@ -485,33 +382,6 @@ def _keystone_catalog(self): def service_catalog(self): return self._keystone_catalog.catalog - def endpoint_for(self, service_type, interface=None, region_name=None): - """Return the endpoint for a given service. - - Respects config values for Connection, including - ``*_endpoint_override``. For direct values from the catalog - regardless of overrides, see - :meth:`~openstack.config.cloud_region.CloudRegion.get_endpoint_from_catalog` - - :param service_type: Service Type of the endpoint to search for. - :param interface: Interface of the endpoint to search for. Optional, - defaults to the configured value for interface for this Connection. - :param region_name: Region Name of the endpoint to search for. - Optional, defaults to the configured value for region_name for this - Connection. - - :returns: The endpoint of the service, or None if not found. - """ - - endpoint_override = self.config.get_endpoint(service_type) - if endpoint_override: - return endpoint_override - return self.config.get_endpoint_from_catalog( - service_type=service_type, - interface=interface, - region_name=region_name, - ) - @property def auth_token(self): # Keystone's session will reuse a token if it is still valid. diff --git a/openstack/connection.py b/openstack/connection.py index e1368118d..11e4745ed 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -271,6 +271,7 @@ :ref:`service-proxies` documentation. """ +import copy import importlib.metadata as importlib_metadata import warnings @@ -294,6 +295,7 @@ from openstack.cloud import _security_group from openstack.cloud import _shared_file_system from openstack import config as _config +import openstack.config.cloud_region from openstack import exceptions from openstack import service_description @@ -374,7 +376,7 @@ def __init__( global_request_id=None, strict_proxies=False, pool_executor=None, - **kwargs + **kwargs, ): """Create a connection to a cloud. @@ -467,7 +469,7 @@ def __init__( global_request_id=global_request_id, strict_proxies=strict_proxies, pool_executor=pool_executor, - **kwargs + **kwargs, ) # Allow vendors to provide hooks. They will normally only receive a @@ -551,18 +553,148 @@ def getter(self): def authorize(self): """Authorize this Connection - .. note:: This method is optional. When an application makes a call - to any OpenStack service, this method allows you to request - a token manually before attempting to do anything else. + .. note:: - :returns: A string token. + This method is optional. When an application makes a call to any + OpenStack service, this method allows you to request a token + manually before attempting to do anything else. + :returns: A string token. :raises: :class:`~openstack.exceptions.HttpException` if the - authorization fails due to reasons like the credentials - provided are unable to be authorized or the `auth_type` - argument is missing, etc. + authorization fails due to reasons like the credentials + provided are unable to be authorized or the `auth_type` + argument is missing, etc. """ try: return self.session.get_token() except keystoneauth1.exceptions.ClientException as e: raise exceptions.SDKException(e) + + def connect_as(self, **kwargs): + """Make a new Connection object with new auth context. + + Take the existing settings from the current cloud and construct a new + Connection object with some of the auth settings overridden. This + is useful for getting an object to perform tasks with as another user, + or in the context of a different project. + + .. code-block:: python + + conn = openstack.connect(cloud='example') + # Work normally + servers = conn.list_servers() + conn2 = conn.connect_as(username='different-user', password='') + # Work as different-user + servers = conn2.list_servers() + + :param kwargs: keyword arguments can contain anything that would + normally go in an auth dict. They will override the same settings + from the parent cloud as appropriate. Entries that do not want to + be overridden can be ommitted. + """ + + if self.config._openstack_config: + config = self.config._openstack_config + else: + # TODO(mordred) Replace this with from_session + config = openstack.config.OpenStackConfig( + app_name=self.config._app_name, + app_version=self.config._app_version, + load_yaml_config=False, + ) + params = copy.deepcopy(self.config.config) + # Remove profile from current cloud so that overridding works + params.pop('profile', None) + + # Utility function to help with the stripping below. + def pop_keys(params, auth, name_key, id_key): + if name_key in auth or id_key in auth: + params['auth'].pop(name_key, None) + params['auth'].pop(id_key, None) + + # If there are user, project or domain settings in the incoming auth + # dict, strip out both id and name so that a user can say: + # cloud.connect_as(project_name='foo') + # and have that work with clouds that have a project_id set in their + # config. + for prefix in ('user', 'project'): + if prefix == 'user': + name_key = 'username' + else: + name_key = 'project_name' + id_key = f'{prefix}_id' + pop_keys(params, kwargs, name_key, id_key) + id_key = f'{prefix}_domain_id' + name_key = f'{prefix}_domain_name' + pop_keys(params, kwargs, name_key, id_key) + + for key, value in kwargs.items(): + params['auth'][key] = value + + cloud_region = config.get_one(**params) + # Attach the discovery cache from the old session so we won't + # double discover. + cloud_region._discovery_cache = self.session._discovery_cache + # Override the cloud name so that logging/location work right + cloud_region._name = self.name + cloud_region.config['profile'] = self.name + # Use self.__class__ so that we return whatever this if, like if it's + # a subclass in the case of shade wrapping sdk. + return self.__class__(config=cloud_region) + + def connect_as_project(self, project): + """Make a new Connection object with a new project. + + Take the existing settings from the current cloud and construct a new + Connection object with the project settings overridden. This + is useful for getting an object to perform tasks with as another user, + or in the context of a different project. + + .. code-block:: python + + cloud = openstack.connect(cloud='example') + # Work normally + servers = cloud.list_servers() + cloud2 = cloud.connect_as_project('different-project') + # Work in different-project + servers = cloud2.list_servers() + + :param project: Either a project name or a project dict as returned by + ``list_projects``. + """ + auth = {} + if isinstance(project, dict): + auth['project_id'] = project.get('id') + auth['project_name'] = project.get('name') + if project.get('domain_id'): + auth['project_domain_id'] = project['domain_id'] + else: + auth['project_name'] = project + return self.connect_as(**auth) + + def endpoint_for(self, service_type, interface=None, region_name=None): + """Return the endpoint for a given service. + + Respects config values for Connection, including + ``*_endpoint_override``. For direct values from the catalog regardless + of overrides, see + :meth:`~openstack.config.cloud_region.CloudRegion.get_endpoint_from_catalog` + + :param service_type: Service Type of the endpoint to search for. + :param interface: Interface of the endpoint to search for. Optional, + defaults to the configured value for interface for this Connection. + :param region_name: Region Name of the endpoint to search for. + Optional, defaults to the configured value for region_name for this + Connection. + + :returns: The endpoint of the service, or None if not found. + """ + + endpoint_override = self.config.get_endpoint(service_type) + if endpoint_override: + return endpoint_override + return self.config.get_endpoint_from_catalog( + service_type=service_type, + interface=interface, + region_name=region_name, + ) From aee822095270bd0f25b5ab65ab3d2710acdc73fb Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 27 Jun 2024 16:10:48 +0100 Subject: [PATCH 3531/3836] cloud: Misc type fixes Change-Id: I8f36ff031c2be5c66ccaaa31e9215d7bac539257 Signed-off-by: Stephen Finucane --- openstack/cloud/_baremetal.py | 4 ++-- openstack/cloud/openstackcloud.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/openstack/cloud/_baremetal.py b/openstack/cloud/_baremetal.py index dd389aaa6..277d36135 100644 --- a/openstack/cloud/_baremetal.py +++ b/openstack/cloud/_baremetal.py @@ -462,9 +462,9 @@ def validate_machine(self, name_or_id, for_deploy=True): :raises: :exc:`~openstack.exceptions.ValidationException` """ if for_deploy: - ifaces = ('boot', 'deploy', 'management', 'power') + ifaces = ['boot', 'deploy', 'management', 'power'] else: - ifaces = ('power',) + ifaces = ['power'] self.baremetal.validate_node(name_or_id, required=ifaces) def validate_node(self, uuid): diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 4d58af223..a2e0258cd 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -15,6 +15,7 @@ import copy import functools import queue +import typing as ty import warnings import weakref @@ -31,6 +32,7 @@ import openstack.config.cloud_region from openstack import exceptions from openstack import proxy +from openstack import resource from openstack import utils @@ -481,7 +483,7 @@ def range_search(self, data, filters): :raises: :class:`~openstack.exceptions.SDKException` on invalid range expressions. """ - filtered = [] + filtered: ty.List[object] = [] for key, range_value in filters.items(): # We always want to operate on the full data set so that @@ -692,7 +694,7 @@ def project_cleanup( for dep in v.get('after', []): dep_graph.add_edge(dep, k) - cleanup_resources = dict() + cleanup_resources: ty.Dict[str, resource.Resource] = {} for service in dep_graph.walk(timeout=wait_timeout): fn = None From 71b31f15ebce4ead28e0075b3f83e26b86ebc411 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Sun, 30 Jun 2024 15:10:12 +0100 Subject: [PATCH 3532/3836] cloud: Combine networking-related classes The SecurityGroupCloudMixin, FloatingIPCloudMixin and NetworkingCommonCloudMixin classes all exist to handle the difference between nova-network and neutron. Combine them and use them as a subclass for the ComputeCloudMixin and NetworkingCloudMixin classes. This makes our class hierarchy a little messier but it's the only obvious way I have to address this issue that doesn't involve something like decorator gymnastics. Change-Id: I4b101124e19636dfa9a0f704914f5100870a9fb9 Signed-off-by: Stephen Finucane --- openstack/block_storage/v2/_proxy.py | 2 +- openstack/block_storage/v3/_proxy.py | 2 +- openstack/cloud/_compute.py | 4 +- openstack/cloud/_floating_ip.py | 1342 ------------------ openstack/cloud/_network.py | 4 +- openstack/cloud/_network_common.py | 1868 +++++++++++++++++++++++++- openstack/cloud/_security_group.py | 561 -------- openstack/cloud/openstackcloud.py | 4 +- openstack/compute/v2/_proxy.py | 2 +- openstack/connection.py | 21 +- 10 files changed, 1882 insertions(+), 1928 deletions(-) delete mode 100644 openstack/cloud/_floating_ip.py delete mode 100644 openstack/cloud/_security_group.py diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index b89551a17..e9385a8fe 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -691,7 +691,7 @@ def get_quota_class_set(self, quota_class_set='default'): :returns: One :class:`~openstack.block_storage.v2.quota_class_set.QuotaClassSet` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_quota_class_set.QuotaClassSet, quota_class_set) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 6b17f9151..cf421fda5 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -1718,7 +1718,7 @@ def get_quota_class_set(self, quota_class_set='default'): :returns: One :class:`~openstack.block_storage.v3.quota_class_set.QuotaClassSet` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_quota_class_set.QuotaClassSet, quota_class_set) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index d55537998..da58282cf 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -17,10 +17,10 @@ import iso8601 +from openstack.cloud import _network_common from openstack.cloud import _utils from openstack.cloud import exc from openstack.cloud import meta -from openstack.cloud import openstackcloud from openstack.compute.v2 import server as _server from openstack import exceptions from openstack import utils @@ -68,7 +68,7 @@ def _pop_or_get(resource, key, default, strict): return resource.get(key, default) -class ComputeCloudMixin(openstackcloud._OpenStackCloudMixin): +class ComputeCloudMixin(_network_common.NetworkCommonCloudMixin): @property def _compute_region(self): diff --git a/openstack/cloud/_floating_ip.py b/openstack/cloud/_floating_ip.py deleted file mode 100644 index 973254645..000000000 --- a/openstack/cloud/_floating_ip.py +++ /dev/null @@ -1,1342 +0,0 @@ -# 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 ipaddress -import time -import warnings - -from openstack.cloud import _utils -from openstack.cloud import meta -from openstack.cloud import openstackcloud -from openstack import exceptions -from openstack import proxy -from openstack import utils -from openstack import warnings as os_warnings - - -class FloatingIPCloudMixin(openstackcloud._OpenStackCloudMixin): - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.private = self.config.config.get('private', False) - - self._floating_ip_source = self.config.config.get('floating_ip_source') - if self._floating_ip_source: - if self._floating_ip_source.lower() == 'none': - self._floating_ip_source = None - else: - self._floating_ip_source = self._floating_ip_source.lower() - - def search_floating_ip_pools(self, name=None, filters=None): - pools = self.list_floating_ip_pools() - return _utils._filter_list(pools, name, filters) - - # With Neutron, there are some cases in which full server side filtering is - # not possible (e.g. nested attributes or list of objects) so we also need - # to use the client-side filtering - # The same goes for all neutron-related search/get methods! - def search_floating_ips(self, id=None, filters=None): - # `filters` could be a jmespath expression which Neutron server doesn't - # understand, obviously. - warnings.warn( - "search_floating_ips is deprecated. Use search_resource instead.", - os_warnings.OpenStackDeprecationWarning, - ) - if self._use_neutron_floating() and isinstance(filters, dict): - return list(self.network.ips(**filters)) - else: - floating_ips = self.list_floating_ips() - return _utils._filter_list(floating_ips, id, filters) - - def _neutron_list_floating_ips(self, filters=None): - if not filters: - filters = {} - data = list(self.network.ips(**filters)) - return data - - def _nova_list_floating_ips(self): - try: - data = proxy._json_response(self.compute.get('/os-floating-ips')) - except exceptions.NotFoundException: - return [] - return self._get_and_munchify('floating_ips', data) - - def get_floating_ip(self, id, filters=None): - """Get a floating IP by ID - - :param id: ID of the floating IP. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - OR - A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - - :returns: A floating IP ``openstack.network.v2.floating_ip.FloatingIP`` - or None if no matching floating IP is found. - - """ - return _utils._get_entity(self, 'floating_ip', id, filters) - - def list_floating_ips(self, filters=None): - """List all available floating IPs. - - :param filters: (optional) dict of filter conditions to push down - :returns: A list of floating IP - ``openstack.network.v2.floating_ip.FloatingIP``. - """ - if not filters: - filters = {} - - if self._use_neutron_floating(): - try: - return self._neutron_list_floating_ips(filters) - except exceptions.NotFoundException as e: - # Nova-network don't support server-side floating ips - # filtering, so it's safer to return an empty list than - # to fallback to Nova which may return more results that - # expected. - if filters: - self.log.error( - "Neutron returned NotFound for floating IPs, which " - "means this cloud doesn't have neutron floating ips. " - "openstacksdk can't fallback to trying Nova since " - "nova doesn't support server-side filtering when " - "listing floating ips and filters were given. " - "If you do not think openstacksdk should be " - "attempting to list floating IPs on neutron, it is " - "possible to control the behavior by setting " - "floating_ip_source to 'nova' or None for cloud " - "%(cloud)r in 'clouds.yaml'.", - { - 'cloud': self.name, - }, - ) - # We can't fallback to nova because we push-down filters. - # We got a 404 which means neutron doesn't exist. If the - # user - return [] - - self.log.debug( - "Something went wrong talking to neutron API: " - "'%(msg)s'. Trying with Nova.", - {'msg': str(e)}, - ) - # Fall-through, trying with Nova - else: - if filters: - raise ValueError( - "nova-network doesn't support server-side floating IPs " - "filtering. Use the 'search_floating_ips' method instead" - ) - - floating_ips = self._nova_list_floating_ips() - return self._normalize_floating_ips(floating_ips) - - def list_floating_ip_pools(self): - """List all available floating IP pools. - - NOTE: This function supports the nova-net view of the world. nova-net - has been deprecated, so it's highly recommended to switch to using - neutron. `get_external_ipv4_floating_networks` is what you should - almost certainly be using. - - :returns: A list of floating IP pool objects - """ - data = proxy._json_response( - self.compute.get('os-floating-ip-pools'), - error_message="Error fetching floating IP pool list", - ) - pools = self._get_and_munchify('floating_ip_pools', data) - return [{'name': p['name']} for p in pools] - - def get_floating_ip_by_id(self, id): - """Get a floating ip by ID - - :param id: ID of the floating ip. - :returns: A floating ip - `:class:`~openstack.network.v2.floating_ip.FloatingIP`. - """ - error_message = f"Error getting floating ip with ID {id}" - - if self._use_neutron_floating(): - fip = self.network.get_ip(id) - return fip - else: - data = proxy._json_response( - self.compute.get(f'/os-floating-ips/{id}'), - error_message=error_message, - ) - return self._normalize_floating_ip( - self._get_and_munchify('floating_ip', data) - ) - - def _neutron_available_floating_ips( - self, network=None, project_id=None, server=None - ): - """Get a floating IP from a network. - - Return a list of available floating IPs or allocate a new one and - return it in a list of 1 element. - - :param network: A single network name or ID, or a list of them. - :param server: (server) Server the Floating IP is for - - :returns: a list of floating IP addresses. - :raises: :class:`~openstack.exceptions.BadRequestException` if an - external network that meets the specified criteria cannot be found. - """ - if project_id is None: - # Make sure we are only listing floatingIPs allocated the current - # tenant. This is the default behaviour of Nova - project_id = self.current_project_id - - if network: - if isinstance(network, str): - network = [network] - - # Use given list to get first matching external network - floating_network_id = None - for net in network: - for ext_net in self.get_external_ipv4_floating_networks(): - if net in (ext_net['name'], ext_net['id']): - floating_network_id = ext_net['id'] - break - if floating_network_id: - break - - if floating_network_id is None: - raise exceptions.NotFoundException( - f"unable to find external network {network}" - ) - else: - floating_network_id = self._get_floating_network_id() - - filters = { - 'port_id': None, - 'floating_network_id': floating_network_id, - 'project_id': project_id, - } - - floating_ips = self.list_floating_ips() - available_ips = _utils._filter_list( - floating_ips, name_or_id=None, filters=filters - ) - if available_ips: - return available_ips - - # No available IP found or we didn't try - # allocate a new Floating IP - f_ip = self._neutron_create_floating_ip( - network_id=floating_network_id, server=server - ) - - return [f_ip] - - def _nova_available_floating_ips(self, pool=None): - """Get available floating IPs from a floating IP pool. - - Return a list of available floating IPs or allocate a new one and - return it in a list of 1 element. - - :param pool: Nova floating IP pool name. - - :returns: a list of floating IP addresses. - :raises: :class:`~openstack.exceptions.BadRequestException` if a - floating IP pool is not specified and cannot be found. - """ - - with _utils.openstacksdk_exceptions( - f"Unable to create floating IP in pool {pool}" - ): - if pool is None: - pools = self.list_floating_ip_pools() - if not pools: - raise exceptions.NotFoundException( - "unable to find a floating ip pool" - ) - pool = pools[0]['name'] - - filters = {'instance_id': None, 'pool': pool} - - floating_ips = self._nova_list_floating_ips() - available_ips = _utils._filter_list( - floating_ips, name_or_id=None, filters=filters - ) - if available_ips: - return available_ips - - # No available IP found or we did not try. - # Allocate a new Floating IP - f_ip = self._nova_create_floating_ip(pool=pool) - - return [f_ip] - - def _find_floating_network_by_router(self): - """Find the network providing floating ips by looking at routers.""" - for router in self.network.routers(): - if router['admin_state_up']: - network_id = router.get('external_gateway_info', {}).get( - 'network_id' - ) - if network_id: - return network_id - - def available_floating_ip(self, network=None, server=None): - """Get a floating IP from a network or a pool. - - Return the first available floating IP or allocate a new one. - - :param network: Name or ID of the network. - :param server: Server the IP is for if known - - :returns: a (normalized) structure with a floating IP address - description. - """ - if self._use_neutron_floating(): - try: - f_ips = self._neutron_available_floating_ips( - network=network, server=server - ) - return f_ips[0] - except exceptions.NotFoundException as e: - self.log.debug( - "Something went wrong talking to neutron API: " - "'%(msg)s'. Trying with Nova.", - {'msg': str(e)}, - ) - # Fall-through, trying with Nova - - f_ips = self._normalize_floating_ips( - self._nova_available_floating_ips(pool=network) - ) - return f_ips[0] - - def _get_floating_network_id(self): - # Get first existing external IPv4 network - networks = self.get_external_ipv4_floating_networks() - if networks: - floating_network_id = networks[0]['id'] - else: - floating_network = self._find_floating_network_by_router() - if floating_network: - floating_network_id = floating_network - else: - raise exceptions.NotFoundException( - "unable to find an external network" - ) - return floating_network_id - - def create_floating_ip( - self, - network=None, - server=None, - fixed_address=None, - nat_destination=None, - port=None, - wait=False, - timeout=60, - ): - """Allocate a new floating IP from a network or a pool. - - :param network: Name or ID of the network - that the floating IP should come from. - :param server: (optional) Server dict for the server to create - the IP for and to which it should be attached. - :param fixed_address: (optional) Fixed IP to attach the floating - ip to. - :param nat_destination: (optional) Name or ID of the network - that the fixed IP to attach the floating - IP to should be on. - :param port: (optional) The port ID that the floating IP should be - attached to. Specifying a port conflicts - with specifying a server, fixed_address or - nat_destination. - :param wait: (optional) Whether to wait for the IP to be active. - Defaults to False. Only applies if a server is - provided. - :param timeout: (optional) How long to wait for the IP to be active. - Defaults to 60. Only applies if a server is - provided. - - :returns: a floating IP address - :raises: :class:`~openstack.exceptions.SDKException` on operation - error. - """ - if self._use_neutron_floating(): - try: - return self._neutron_create_floating_ip( - network_name_or_id=network, - server=server, - fixed_address=fixed_address, - nat_destination=nat_destination, - port=port, - wait=wait, - timeout=timeout, - ) - except exceptions.NotFoundException as e: - self.log.debug( - "Something went wrong talking to neutron API: " - "'%(msg)s'. Trying with Nova.", - {'msg': str(e)}, - ) - # Fall-through, trying with Nova - - if port: - raise exceptions.SDKException( - "This cloud uses nova-network which does not support " - "arbitrary floating-ip/port mappings. Please nudge " - "your cloud provider to upgrade the networking stack " - "to neutron, or alternately provide the server, " - "fixed_address and nat_destination arguments as appropriate" - ) - # Else, we are using Nova network - f_ips = self._normalize_floating_ips( - [self._nova_create_floating_ip(pool=network)] - ) - return f_ips[0] - - def _submit_create_fip(self, kwargs): - # Split into a method to aid in test mocking - return self.network.create_ip(**kwargs) - - def _neutron_create_floating_ip( - self, - network_name_or_id=None, - server=None, - fixed_address=None, - nat_destination=None, - port=None, - wait=False, - timeout=60, - network_id=None, - ): - if not network_id: - if network_name_or_id: - try: - network = self.network.find_network(network_name_or_id) - except exceptions.NotFoundException: - raise exceptions.NotFoundException( - "unable to find network for floating ips with ID " - "{}".format(network_name_or_id) - ) - network_id = network['id'] - else: - network_id = self._get_floating_network_id() - kwargs = { - 'floating_network_id': network_id, - } - if not port: - if server: - (port_obj, fixed_ip_address) = self._nat_destination_port( - server, - fixed_address=fixed_address, - nat_destination=nat_destination, - ) - if port_obj: - port = port_obj['id'] - if fixed_ip_address: - kwargs['fixed_ip_address'] = fixed_ip_address - if port: - kwargs['port_id'] = port - - fip = self._submit_create_fip(kwargs) - fip_id = fip['id'] - - if port: - # The FIP is only going to become active in this context - # when we've attached it to something, which only occurs - # if we've provided a port as a parameter - if wait: - try: - for count in utils.iterate_timeout( - timeout, - "Timeout waiting for the floating IP to be ACTIVE", - wait=min(5, timeout), - ): - fip = self.get_floating_ip(fip_id) - if fip and fip['status'] == 'ACTIVE': - break - except exceptions.ResourceTimeout: - self.log.error( - "Timed out on floating ip %(fip)s becoming active. " - "Deleting", - {'fip': fip_id}, - ) - try: - self.delete_floating_ip(fip_id) - except Exception as e: - self.log.error( - "FIP LEAK: Attempted to delete floating ip " - "%(fip)s but received %(exc)s exception: " - "%(err)s", - {'fip': fip_id, 'exc': e.__class__, 'err': str(e)}, - ) - raise - if fip['port_id'] != port: - if server: - raise exceptions.SDKException( - "Attempted to create FIP on port {port} for server " - "{server} but FIP has port {port_id}".format( - port=port, - port_id=fip['port_id'], - server=server['id'], - ) - ) - else: - raise exceptions.SDKException( - "Attempted to create FIP on port {port} " - "but something went wrong".format(port=port) - ) - return fip - - def _nova_create_floating_ip(self, pool=None): - with _utils.openstacksdk_exceptions( - f"Unable to create floating IP in pool {pool}" - ): - if pool is None: - pools = self.list_floating_ip_pools() - if not pools: - raise exceptions.NotFoundException( - "unable to find a floating ip pool" - ) - pool = pools[0]['name'] - - data = proxy._json_response( - self.compute.post('/os-floating-ips', json=dict(pool=pool)) - ) - pool_ip = self._get_and_munchify('floating_ip', data) - # TODO(mordred) Remove this - it's just for compat - data = proxy._json_response( - self.compute.get( - '/os-floating-ips/{id}'.format(id=pool_ip['id']) - ) - ) - return self._get_and_munchify('floating_ip', data) - - def delete_floating_ip(self, floating_ip_id, retry=1): - """Deallocate a floating IP from a project. - - :param floating_ip_id: a floating IP address ID. - :param retry: number of times to retry. Optional, defaults to 1, - which is in addition to the initial delete call. - A value of 0 will also cause no checking of results to - occur. - - :returns: True if the IP address has been deleted, False if the IP - address was not found. - :raises: :class:`~openstack.exceptions.SDKException` on operation - error. - """ - for count in range(0, max(0, retry) + 1): - result = self._delete_floating_ip(floating_ip_id) - - if (retry == 0) or not result: - return result - - # neutron sometimes returns success when deleting a floating - # ip. That's awesome. SO - verify that the delete actually - # worked. Some clouds will set the status to DOWN rather than - # deleting the IP immediately. This is, of course, a bit absurd. - f_ip = self.get_floating_ip(id=floating_ip_id) - if not f_ip or f_ip['status'] == 'DOWN': - return True - - raise exceptions.SDKException( - "Attempted to delete Floating IP {ip} with ID {id} a total of " - "{retry} times. Although the cloud did not indicate any errors " - "the floating IP is still in existence. Aborting further " - "operations.".format( - id=floating_ip_id, - ip=f_ip['floating_ip_address'], - retry=retry + 1, - ) - ) - - def _delete_floating_ip(self, floating_ip_id): - if self._use_neutron_floating(): - try: - return self._neutron_delete_floating_ip(floating_ip_id) - except exceptions.NotFoundException as e: - self.log.debug( - "Something went wrong talking to neutron API: " - "'%(msg)s'. Trying with Nova.", - {'msg': str(e)}, - ) - return self._nova_delete_floating_ip(floating_ip_id) - - def _neutron_delete_floating_ip(self, floating_ip_id): - try: - self.network.delete_ip(floating_ip_id, ignore_missing=False) - except exceptions.NotFoundException: - return False - return True - - def _nova_delete_floating_ip(self, floating_ip_id): - try: - proxy._json_response( - self.compute.delete(f'/os-floating-ips/{floating_ip_id}'), - error_message='Unable to delete floating IP {fip_id}'.format( - fip_id=floating_ip_id - ), - ) - except exceptions.NotFoundException: - return False - return True - - def delete_unattached_floating_ips(self, retry=1): - """Safely delete unattached floating ips. - - If the cloud can safely purge any unattached floating ips without - race conditions, do so. - - Safely here means a specific thing. It means that you are not running - this while another process that might do a two step create/attach - is running. You can safely run this method while another process - is creating servers and attaching floating IPs to them if either that - process is using add_auto_ip from shade, or is creating the floating - IPs by passing in a server to the create_floating_ip call. - - :param retry: number of times to retry. Optional, defaults to 1, - which is in addition to the initial delete call. - A value of 0 will also cause no checking of results to occur. - - :returns: Number of Floating IPs deleted, False if none - :raises: :class:`~openstack.exceptions.SDKException` on operation - error. - """ - processed = [] - if self._use_neutron_floating(): - for ip in self.list_floating_ips(): - if not bool(ip.port_id): - processed.append( - self.delete_floating_ip( - floating_ip_id=ip['id'], retry=retry - ) - ) - return len(processed) if all(processed) else False - - def _attach_ip_to_server( - self, - server, - floating_ip, - fixed_address=None, - wait=False, - timeout=60, - skip_attach=False, - nat_destination=None, - ): - """Attach a floating IP to a server. - - :param server: Server dict - :param floating_ip: Floating IP dict to attach - :param fixed_address: (optional) fixed address to which attach the - floating IP to. - :param wait: (optional) Wait for the address to appear as assigned - to the server. Defaults to False. - :param timeout: (optional) Seconds to wait, defaults to 60. - See the ``wait`` parameter. - :param skip_attach: (optional) Skip the actual attach and just do - the wait. Defaults to False. - :param nat_destination: The fixed network the server's port for the - FIP to attach to will come from. - - :returns: The server ``openstack.compute.v2.server.Server`` - :raises: :class:`~openstack.exceptions.SDKException` on operation - error. - """ - # Short circuit if we're asking to attach an IP that's already - # attached - ext_ip = meta.get_server_ip(server, ext_tag='floating', public=True) - if not ext_ip and floating_ip['port_id']: - # When we came here from reuse_fip and created FIP it might be - # already attached, but the server info might be also - # old to check whether it belongs to us now, thus refresh - # the server data and try again. There are some clouds, which - # explicitely forbids FIP assign call if it is already assigned. - server = self.compute.get_server(server['id']) - ext_ip = meta.get_server_ip( - server, ext_tag='floating', public=True - ) - if ext_ip == floating_ip['floating_ip_address']: - return server - - if self._use_neutron_floating(): - if not skip_attach: - try: - self._neutron_attach_ip_to_server( - server=server, - floating_ip=floating_ip, - fixed_address=fixed_address, - nat_destination=nat_destination, - ) - except exceptions.NotFoundException as e: - self.log.debug( - "Something went wrong talking to neutron API: " - "'%(msg)s'. Trying with Nova.", - {'msg': str(e)}, - ) - # Fall-through, trying with Nova - else: - # Nova network - self._nova_attach_ip_to_server( - server_id=server['id'], - floating_ip_id=floating_ip['id'], - fixed_address=fixed_address, - ) - - if wait: - # Wait for the address to be assigned to the server - server_id = server['id'] - for _ in utils.iterate_timeout( - timeout, - "Timeout waiting for the floating IP to be attached.", - wait=min(5, timeout), - ): - server = self.compute.get_server(server_id) - ext_ip = meta.get_server_ip( - server, ext_tag='floating', public=True - ) - if ext_ip == floating_ip['floating_ip_address']: - return server - return server - - def _neutron_attach_ip_to_server( - self, server, floating_ip, fixed_address=None, nat_destination=None - ): - # Find an available port - (port, fixed_address) = self._nat_destination_port( - server, - fixed_address=fixed_address, - nat_destination=nat_destination, - ) - if not port: - raise exceptions.SDKException( - "unable to find a port for server {}".format(server['id']) - ) - - floating_ip_args = {'port_id': port['id']} - if fixed_address is not None: - floating_ip_args['fixed_ip_address'] = fixed_address - - return self.network.update_ip(floating_ip, **floating_ip_args) - - def _nova_attach_ip_to_server( - self, server_id, floating_ip_id, fixed_address=None - ): - f_ip = self.get_floating_ip(id=floating_ip_id) - if f_ip is None: - raise exceptions.SDKException( - f"unable to find floating IP {floating_ip_id}" - ) - error_message = "Error attaching IP {ip} to instance {id}".format( - ip=floating_ip_id, id=server_id - ) - body = {'address': f_ip['floating_ip_address']} - if fixed_address: - body['fixed_address'] = fixed_address - return proxy._json_response( - self.compute.post( - f'/servers/{server_id}/action', - json=dict(addFloatingIp=body), - ), - error_message=error_message, - ) - - def detach_ip_from_server(self, server_id, floating_ip_id): - """Detach a floating IP from a server. - - :param server_id: ID of a server. - :param floating_ip_id: Id of the floating IP to detach. - - :returns: True if the IP has been detached, or False if the IP wasn't - attached to any server. - :raises: :class:`~openstack.exceptions.SDKException` on operation - error. - """ - if self._use_neutron_floating(): - try: - return self._neutron_detach_ip_from_server( - server_id=server_id, floating_ip_id=floating_ip_id - ) - except exceptions.NotFoundException as e: - self.log.debug( - "Something went wrong talking to neutron API: " - "'%(msg)s'. Trying with Nova.", - {'msg': str(e)}, - ) - # Fall-through, trying with Nova - - # Nova network - self._nova_detach_ip_from_server( - server_id=server_id, floating_ip_id=floating_ip_id - ) - - def _neutron_detach_ip_from_server(self, server_id, floating_ip_id): - f_ip = self.get_floating_ip(id=floating_ip_id) - if f_ip is None or not bool(f_ip.port_id): - return False - try: - self.network.update_ip(floating_ip_id, port_id=None) - except exceptions.SDKException: - raise exceptions.SDKException( - "Error detaching IP {ip} from " - "server {server_id}".format( - ip=floating_ip_id, server_id=server_id - ) - ) - - return True - - def _nova_detach_ip_from_server(self, server_id, floating_ip_id): - f_ip = self.get_floating_ip(id=floating_ip_id) - if f_ip is None: - raise exceptions.SDKException( - f"unable to find floating IP {floating_ip_id}" - ) - error_message = "Error detaching IP {ip} from instance {id}".format( - ip=floating_ip_id, id=server_id - ) - return proxy._json_response( - self.compute.post( - f'/servers/{server_id}/action', - json=dict( - removeFloatingIp=dict(address=f_ip['floating_ip_address']) - ), - ), - error_message=error_message, - ) - - return True - - def _add_ip_from_pool( - self, - server, - network, - fixed_address=None, - reuse=True, - wait=False, - timeout=60, - nat_destination=None, - ): - """Add a floating IP to a server from a given pool - - This method reuses available IPs, when possible, or allocate new IPs - to the current tenant. - The floating IP is attached to the given fixed address or to the - first server port/fixed address - - :param server: Server dict - :param network: Name or ID of the network. - :param fixed_address: a fixed address - :param reuse: Try to reuse existing ips. Defaults to True. - :param wait: (optional) Wait for the address to appear as assigned - to the server. Defaults to False. - :param timeout: (optional) Seconds to wait, defaults to 60. - See the ``wait`` parameter. - :param nat_destination: (optional) the name of the network of the - port to associate with the floating ip. - - :returns: the updated server ``openstack.compute.v2.server.Server`` - """ - if reuse: - f_ip = self.available_floating_ip(network=network) - else: - start_time = time.time() - f_ip = self.create_floating_ip( - server=server, - network=network, - nat_destination=nat_destination, - fixed_address=fixed_address, - wait=wait, - timeout=timeout, - ) - timeout = timeout - (time.time() - start_time) - server = self.compute.get_server(server.id) - - # We run attach as a second call rather than in the create call - # because there are code flows where we will not have an attached - # FIP yet. However, even if it was attached in the create, we run - # the attach function below to get back the server dict refreshed - # with the FIP information. - return self._attach_ip_to_server( - server=server, - floating_ip=f_ip, - fixed_address=fixed_address, - wait=wait, - timeout=timeout, - nat_destination=nat_destination, - ) - - def add_ip_list( - self, - server, - ips, - wait=False, - timeout=60, - fixed_address=None, - nat_destination=None, - ): - """Attach a list of IPs to a server. - - :param server: a server object - :param ips: list of floating IP addresses or a single address - :param wait: (optional) Wait for the address to appear as assigned - to the server. Defaults to False. - :param timeout: (optional) Seconds to wait, defaults to 60. - See the ``wait`` parameter. - :param fixed_address: (optional) Fixed address of the server to - attach the IP to - :param nat_destination: (optional) Name or ID of the network that - the fixed IP to attach the floating IP should be on - - :returns: The updated server ``openstack.compute.v2.server.Server`` - :raises: :class:`~openstack.exceptions.SDKException` on operation - error. - """ - - if type(ips) is list: - ips = [ips] - - for ip in ips: - f_ip = self.get_floating_ip( - id=None, filters={'floating_ip_address': ip} - ) - server = self._attach_ip_to_server( - server=server, - floating_ip=f_ip, - wait=wait, - timeout=timeout, - fixed_address=fixed_address, - nat_destination=nat_destination, - ) - return server - - def add_auto_ip(self, server, wait=False, timeout=60, reuse=True): - """Add a floating IP to a server. - - This method is intended for basic usage. For advanced network - architecture (e.g. multiple external networks or servers with multiple - interfaces), use other floating IP methods. - - This method can reuse available IPs, or allocate new IPs to the current - project. - - :param server: a server dictionary. - :param reuse: Whether or not to attempt to reuse IPs, defaults - to True. - :param wait: (optional) Wait for the address to appear as assigned - to the server. Defaults to False. - :param timeout: (optional) Seconds to wait, defaults to 60. - See the ``wait`` parameter. - :param reuse: Try to reuse existing ips. Defaults to True. - - :returns: Floating IP address attached to server. - """ - server = self._add_auto_ip( - server, wait=wait, timeout=timeout, reuse=reuse - ) - return server['interface_ip'] or None - - def _add_auto_ip(self, server, wait=False, timeout=60, reuse=True): - skip_attach = False - created = False - if reuse: - f_ip = self.available_floating_ip(server=server) - else: - start_time = time.time() - f_ip = self.create_floating_ip( - server=server, wait=wait, timeout=timeout - ) - timeout = timeout - (time.time() - start_time) - if server: - # This gets passed in for both nova and neutron - # but is only meaningful for the neutron logic branch - skip_attach = True - created = True - - try: - # We run attach as a second call rather than in the create call - # because there are code flows where we will not have an attached - # FIP yet. However, even if it was attached in the create, we run - # the attach function below to get back the server dict refreshed - # with the FIP information. - return self._attach_ip_to_server( - server=server, - floating_ip=f_ip, - wait=wait, - timeout=timeout, - skip_attach=skip_attach, - ) - except exceptions.ResourceTimeout: - if self._use_neutron_floating() and created: - # We are here because we created an IP on the port - # It failed. Delete so as not to leak an unmanaged - # resource - self.log.error( - "Timeout waiting for floating IP to become " - "active. Floating IP %(ip)s:%(id)s was created for " - "server %(server)s but is being deleted due to " - "activation failure.", - { - 'ip': f_ip['floating_ip_address'], - 'id': f_ip['id'], - 'server': server['id'], - }, - ) - try: - self.delete_floating_ip(f_ip['id']) - except Exception as e: - self.log.error( - "FIP LEAK: Attempted to delete floating ip " - "%(fip)s but received %(exc)s exception: %(err)s", - {'fip': f_ip['id'], 'exc': e.__class__, 'err': str(e)}, - ) - raise e - raise - - def add_ips_to_server( - self, - server, - auto_ip=True, - ips=None, - ip_pool=None, - wait=False, - timeout=60, - reuse=True, - fixed_address=None, - nat_destination=None, - ): - if ip_pool: - server = self._add_ip_from_pool( - server, - ip_pool, - reuse=reuse, - wait=wait, - timeout=timeout, - fixed_address=fixed_address, - nat_destination=nat_destination, - ) - elif ips: - server = self.add_ip_list( - server, - ips, - wait=wait, - timeout=timeout, - fixed_address=fixed_address, - nat_destination=nat_destination, - ) - elif auto_ip: - if self._needs_floating_ip(server, nat_destination): - server = self._add_auto_ip( - server, wait=wait, timeout=timeout, reuse=reuse - ) - return server - - def _needs_floating_ip(self, server, nat_destination): - """Figure out if auto_ip should add a floating ip to this server. - - If the server has a floating ip it does not need another one. - - If the server does not have a fixed ip address it does not need a - floating ip. - - If self.private then the server does not need a floating ip. - - If the cloud runs nova, and the server has a private address and not a - public address, then the server needs a floating ip. - - If the server has a fixed ip address and no floating ip address and the - cloud has a network from which floating IPs come that is connected via - a router to the network from which the fixed ip address came, - then the server needs a floating ip. - - If the server has a fixed ip address and no floating ip address and the - cloud does not have a network from which floating ips come, or it has - one but that network is not connected to the network from which - the server's fixed ip address came via a router, then the - server does not need a floating ip. - """ - if not self._has_floating_ips(): - return False - - if server['addresses'] is None: - # fetch missing server details, e.g. because - # meta.add_server_interfaces() was not called - server = self.compute.get_server(server) - - if server['public_v4'] or any( - [ - any( - [ - address['OS-EXT-IPS:type'] == 'floating' - for address in addresses - ] - ) - for addresses in (server['addresses'] or {}).values() - ] - ): - return False - - if not server['private_v4'] and not any( - [ - any( - [ - address['OS-EXT-IPS:type'] == 'fixed' - for address in addresses - ] - ) - for addresses in (server['addresses'] or {}).values() - ] - ): - return False - - if self.private: - return False - - if not self.has_service('network'): - return True - - # No floating ip network - no FIPs - try: - self._get_floating_network_id() - except exceptions.SDKException: - return False - - (port_obj, fixed_ip_address) = self._nat_destination_port( - server, nat_destination=nat_destination - ) - - if not port_obj or not fixed_ip_address: - return False - - return True - - def _nat_destination_port( - self, server, fixed_address=None, nat_destination=None - ): - """Returns server port that is on a nat_destination network - - Find a port attached to the server which is on a network which - has a subnet which can be the destination of NAT. Such a network - is referred to in shade as a "nat_destination" network. So this - then is a function which returns a port on such a network that is - associated with the given server. - - :param server: Server dict. - :param fixed_address: Fixed ip address of the port - :param nat_destination: Name or ID of the network of the port. - """ - ports = list(self.network.ports(device_id=server['id'])) - if not ports: - return (None, None) - - port = None - if not fixed_address: - if len(ports) > 1: - if nat_destination: - nat_network = self.network.find_network(nat_destination) - if not nat_network: - raise exceptions.SDKException( - 'NAT Destination {nat_destination} was configured' - ' but not found on the cloud. Please check your' - ' config and your cloud and try again.'.format( - nat_destination=nat_destination - ) - ) - else: - nat_network = self.get_nat_destination() - - if not nat_network: - raise exceptions.SDKException( - 'Multiple ports were found for server {server}' - ' but none of the networks are a valid NAT' - ' destination, so it is impossible to add a' - ' floating IP. If you have a network that is a valid' - ' destination for NAT and we could not find it,' - ' please file a bug. But also configure the' - ' nat_destination property of the networks list in' - ' your clouds.yaml file. If you do not have a' - ' clouds.yaml file, please make one - your setup' - ' is complicated.'.format(server=server['id']) - ) - - maybe_ports = [] - for maybe_port in ports: - if maybe_port['network_id'] == nat_network['id']: - maybe_ports.append(maybe_port) - if not maybe_ports: - raise exceptions.SDKException( - 'No port on server {server} was found matching' - ' your NAT destination network {dest}. Please ' - ' check your config'.format( - server=server['id'], dest=nat_network['name'] - ) - ) - ports = maybe_ports - - # Select the most recent available IPv4 address - # To do this, sort the ports in reverse order by the created_at - # field which is a string containing an ISO DateTime (which - # thankfully sort properly) This way the most recent port created, - # if there are more than one, will be the arbitrary port we - # select. - for port in sorted( - ports, key=lambda p: p.get('created_at', 0), reverse=True - ): - for address in port.get('fixed_ips', list()): - try: - ip = ipaddress.ip_address(address['ip_address']) - except Exception: - continue - if ip.version == 4: - fixed_address = address['ip_address'] - return port, fixed_address - raise exceptions.SDKException( - "unable to find a free fixed IPv4 address for server " - "{}".format(server['id']) - ) - # unfortunately a port can have more than one fixed IP: - # we can't use the search_ports filtering for fixed_address as - # they are contained in a list. e.g. - # - # "fixed_ips": [ - # { - # "subnet_id": "008ba151-0b8c-4a67-98b5-0d2b87666062", - # "ip_address": "172.24.4.2" - # } - # ] - # - # Search fixed_address - for p in ports: - for fixed_ip in p['fixed_ips']: - if fixed_address == fixed_ip['ip_address']: - return (p, fixed_address) - return (None, None) - - def _has_floating_ips(self): - if not self._floating_ip_source: - return False - else: - return self._floating_ip_source in ('nova', 'neutron') - - def _use_neutron_floating(self): - return ( - self.has_service('network') - and self._floating_ip_source == 'neutron' - ) - - def _normalize_floating_ips(self, ips): - """Normalize the structure of floating IPs - - Unfortunately, not all the Neutron floating_ip attributes are available - with Nova and not all Nova floating_ip attributes are available with - Neutron. - This function extract attributes that are common to Nova and Neutron - floating IP resource. - If the whole structure is needed inside openstacksdk there are private - methods that returns "original" objects (e.g. - _neutron_allocate_floating_ip) - - :param list ips: A list of Neutron floating IPs. - - :returns: - A list of normalized dicts with the following attributes:: - - [ - { - "id": "this-is-a-floating-ip-id", - "fixed_ip_address": "192.0.2.10", - "floating_ip_address": "198.51.100.10", - "network": "this-is-a-net-or-pool-id", - "attached": True, - "status": "ACTIVE" - }, ... - ] - - """ - return [self._normalize_floating_ip(ip) for ip in ips] - - def _normalize_floating_ip(self, ip): - # Copy incoming floating ip because of shared dicts in unittests - # Only import munch when we really need it - - location = self._get_current_location(project_id=ip.get('owner')) - # This copy is to keep things from getting epically weird in tests - ip = ip.copy() - - ret = utils.Munch(location=location) - - fixed_ip_address = ip.pop('fixed_ip_address', ip.pop('fixed_ip', None)) - floating_ip_address = ip.pop('floating_ip_address', ip.pop('ip', None)) - network_id = ip.pop( - 'floating_network_id', ip.pop('network', ip.pop('pool', None)) - ) - project_id = ip.pop('tenant_id', '') - project_id = ip.pop('project_id', project_id) - - instance_id = ip.pop('instance_id', None) - router_id = ip.pop('router_id', None) - id = ip.pop('id') - port_id = ip.pop('port_id', None) - created_at = ip.pop('created_at', None) - updated_at = ip.pop('updated_at', None) - # Note - description may not always be on the underlying cloud. - # Normalizing it here is easy - what do we do when people want to - # set a description? - description = ip.pop('description', '') - revision_number = ip.pop('revision_number', None) - - if self._use_neutron_floating(): - attached = bool(port_id) - status = ip.pop('status', 'UNKNOWN') - else: - attached = bool(instance_id) - # In neutron's terms, Nova floating IPs are always ACTIVE - status = 'ACTIVE' - - ret = utils.Munch( - attached=attached, - fixed_ip_address=fixed_ip_address, - floating_ip_address=floating_ip_address, - id=id, - location=self._get_current_location(project_id=project_id), - network=network_id, - port=port_id, - router=router_id, - status=status, - created_at=created_at, - updated_at=updated_at, - description=description, - revision_number=revision_number, - properties=ip.copy(), - ) - # Backwards compat - if not self.strict_mode: - ret['port_id'] = port_id - ret['router_id'] = router_id - ret['project_id'] = project_id - ret['tenant_id'] = project_id - ret['floating_network_id'] = network_id - for key, val in ret['properties'].items(): - ret.setdefault(key, val) - - return ret diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index 5ada15ab5..f8ddd5f54 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -10,13 +10,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +from openstack.cloud import _network_common from openstack.cloud import _utils from openstack.cloud import exc -from openstack.cloud import openstackcloud from openstack import exceptions -class NetworkCloudMixin(openstackcloud._OpenStackCloudMixin): +class NetworkCloudMixin(_network_common.NetworkCommonCloudMixin): def _neutron_extensions(self): extensions = set() diff --git a/openstack/cloud/_network_common.py b/openstack/cloud/_network_common.py index 6d2f7cb4e..43fce7a22 100644 --- a/openstack/cloud/_network_common.py +++ b/openstack/cloud/_network_common.py @@ -10,15 +10,23 @@ # See the License for the specific language governing permissions and # limitations under the License. +import ipaddress import threading +import time +import warnings +from openstack.cloud import _utils +from openstack.cloud import exc +from openstack.cloud import meta from openstack.cloud import openstackcloud from openstack import exceptions +from openstack import proxy +from openstack import utils +from openstack import warnings as os_warnings class NetworkCommonCloudMixin(openstackcloud._OpenStackCloudMixin): - """Shared networking functions used by FloatingIP, Network, Compute - classes.""" + """Shared networking functions used by Network and Compute classes.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -41,6 +49,19 @@ def __init__(self, *args, **kwargs): self._networks_lock = threading.Lock() self._reset_network_caches() + self.private = self.config.config.get('private', False) + + self._floating_ip_source = self.config.config.get('floating_ip_source') + if self._floating_ip_source: + if self._floating_ip_source.lower() == 'none': + self._floating_ip_source = None + else: + self._floating_ip_source = self._floating_ip_source.lower() + + self.secgroup_source = self.config.config['secgroup_source'] + + # networks + def use_external_network(self): return self._use_external_network @@ -392,3 +413,1846 @@ def get_internal_ipv6_networks(self): """ self._find_interesting_networks() return self._internal_ipv6_networks + + # floating IPs + + def search_floating_ip_pools(self, name=None, filters=None): + pools = self.list_floating_ip_pools() + return _utils._filter_list(pools, name, filters) + + # With Neutron, there are some cases in which full server side filtering is + # not possible (e.g. nested attributes or list of objects) so we also need + # to use the client-side filtering + # The same goes for all neutron-related search/get methods! + def search_floating_ips(self, id=None, filters=None): + # `filters` could be a jmespath expression which Neutron server doesn't + # understand, obviously. + warnings.warn( + "search_floating_ips is deprecated. Use search_resource instead.", + os_warnings.OpenStackDeprecationWarning, + ) + if self._use_neutron_floating() and isinstance(filters, dict): + return list(self.network.ips(**filters)) + else: + floating_ips = self.list_floating_ips() + return _utils._filter_list(floating_ips, id, filters) + + def _neutron_list_floating_ips(self, filters=None): + if not filters: + filters = {} + data = list(self.network.ips(**filters)) + return data + + def _nova_list_floating_ips(self): + try: + data = proxy._json_response(self.compute.get('/os-floating-ips')) + except exceptions.NotFoundException: + return [] + return self._get_and_munchify('floating_ips', data) + + def get_floating_ip(self, id, filters=None): + """Get a floating IP by ID + + :param id: ID of the floating IP. + :param filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: A floating IP ``openstack.network.v2.floating_ip.FloatingIP`` + or None if no matching floating IP is found. + + """ + return _utils._get_entity(self, 'floating_ip', id, filters) + + def list_floating_ips(self, filters=None): + """List all available floating IPs. + + :param filters: (optional) dict of filter conditions to push down + :returns: A list of floating IP + ``openstack.network.v2.floating_ip.FloatingIP``. + """ + if not filters: + filters = {} + + if self._use_neutron_floating(): + try: + return self._neutron_list_floating_ips(filters) + except exceptions.NotFoundException as e: + # Nova-network don't support server-side floating ips + # filtering, so it's safer to return an empty list than + # to fallback to Nova which may return more results that + # expected. + if filters: + self.log.error( + "Neutron returned NotFound for floating IPs, which " + "means this cloud doesn't have neutron floating ips. " + "openstacksdk can't fallback to trying Nova since " + "nova doesn't support server-side filtering when " + "listing floating ips and filters were given. " + "If you do not think openstacksdk should be " + "attempting to list floating IPs on neutron, it is " + "possible to control the behavior by setting " + "floating_ip_source to 'nova' or None for cloud " + "%(cloud)r in 'clouds.yaml'.", + { + 'cloud': self.name, + }, + ) + # We can't fallback to nova because we push-down filters. + # We got a 404 which means neutron doesn't exist. If the + # user + return [] + + self.log.debug( + "Something went wrong talking to neutron API: " + "'%(msg)s'. Trying with Nova.", + {'msg': str(e)}, + ) + # Fall-through, trying with Nova + else: + if filters: + raise ValueError( + "nova-network doesn't support server-side floating IPs " + "filtering. Use the 'search_floating_ips' method instead" + ) + + floating_ips = self._nova_list_floating_ips() + return self._normalize_floating_ips(floating_ips) + + def list_floating_ip_pools(self): + """List all available floating IP pools. + + NOTE: This function supports the nova-net view of the world. nova-net + has been deprecated, so it's highly recommended to switch to using + neutron. `get_external_ipv4_floating_networks` is what you should + almost certainly be using. + + :returns: A list of floating IP pool objects + """ + data = proxy._json_response( + self.compute.get('os-floating-ip-pools'), + error_message="Error fetching floating IP pool list", + ) + pools = self._get_and_munchify('floating_ip_pools', data) + return [{'name': p['name']} for p in pools] + + def get_floating_ip_by_id(self, id): + """Get a floating ip by ID + + :param id: ID of the floating ip. + :returns: A floating ip + `:class:`~openstack.network.v2.floating_ip.FloatingIP`. + """ + error_message = f"Error getting floating ip with ID {id}" + + if self._use_neutron_floating(): + fip = self.network.get_ip(id) + return fip + else: + data = proxy._json_response( + self.compute.get(f'/os-floating-ips/{id}'), + error_message=error_message, + ) + return self._normalize_floating_ip( + self._get_and_munchify('floating_ip', data) + ) + + def _neutron_available_floating_ips( + self, network=None, project_id=None, server=None + ): + """Get a floating IP from a network. + + Return a list of available floating IPs or allocate a new one and + return it in a list of 1 element. + + :param network: A single network name or ID, or a list of them. + :param server: (server) Server the Floating IP is for + + :returns: a list of floating IP addresses. + :raises: :class:`~openstack.exceptions.BadRequestException` if an + external network that meets the specified criteria cannot be found. + """ + if project_id is None: + # Make sure we are only listing floatingIPs allocated the current + # tenant. This is the default behaviour of Nova + project_id = self.current_project_id + + if network: + if isinstance(network, str): + network = [network] + + # Use given list to get first matching external network + floating_network_id = None + for net in network: + for ext_net in self.get_external_ipv4_floating_networks(): + if net in (ext_net['name'], ext_net['id']): + floating_network_id = ext_net['id'] + break + if floating_network_id: + break + + if floating_network_id is None: + raise exceptions.NotFoundException( + f"unable to find external network {network}" + ) + else: + floating_network_id = self._get_floating_network_id() + + filters = { + 'port_id': None, + 'floating_network_id': floating_network_id, + 'project_id': project_id, + } + + floating_ips = self.list_floating_ips() + available_ips = _utils._filter_list( + floating_ips, name_or_id=None, filters=filters + ) + if available_ips: + return available_ips + + # No available IP found or we didn't try + # allocate a new Floating IP + f_ip = self._neutron_create_floating_ip( + network_id=floating_network_id, server=server + ) + + return [f_ip] + + def _nova_available_floating_ips(self, pool=None): + """Get available floating IPs from a floating IP pool. + + Return a list of available floating IPs or allocate a new one and + return it in a list of 1 element. + + :param pool: Nova floating IP pool name. + + :returns: a list of floating IP addresses. + :raises: :class:`~openstack.exceptions.BadRequestException` if a + floating IP pool is not specified and cannot be found. + """ + + with _utils.openstacksdk_exceptions( + f"Unable to create floating IP in pool {pool}" + ): + if pool is None: + pools = self.list_floating_ip_pools() + if not pools: + raise exceptions.NotFoundException( + "unable to find a floating ip pool" + ) + pool = pools[0]['name'] + + filters = {'instance_id': None, 'pool': pool} + + floating_ips = self._nova_list_floating_ips() + available_ips = _utils._filter_list( + floating_ips, name_or_id=None, filters=filters + ) + if available_ips: + return available_ips + + # No available IP found or we did not try. + # Allocate a new Floating IP + f_ip = self._nova_create_floating_ip(pool=pool) + + return [f_ip] + + def _find_floating_network_by_router(self): + """Find the network providing floating ips by looking at routers.""" + for router in self.network.routers(): + if router['admin_state_up']: + network_id = router.get('external_gateway_info', {}).get( + 'network_id' + ) + if network_id: + return network_id + + def available_floating_ip(self, network=None, server=None): + """Get a floating IP from a network or a pool. + + Return the first available floating IP or allocate a new one. + + :param network: Name or ID of the network. + :param server: Server the IP is for if known + + :returns: a (normalized) structure with a floating IP address + description. + """ + if self._use_neutron_floating(): + try: + f_ips = self._neutron_available_floating_ips( + network=network, server=server + ) + return f_ips[0] + except exceptions.NotFoundException as e: + self.log.debug( + "Something went wrong talking to neutron API: " + "'%(msg)s'. Trying with Nova.", + {'msg': str(e)}, + ) + # Fall-through, trying with Nova + + f_ips = self._normalize_floating_ips( + self._nova_available_floating_ips(pool=network) + ) + return f_ips[0] + + def _get_floating_network_id(self): + # Get first existing external IPv4 network + networks = self.get_external_ipv4_floating_networks() + if networks: + floating_network_id = networks[0]['id'] + else: + floating_network = self._find_floating_network_by_router() + if floating_network: + floating_network_id = floating_network + else: + raise exceptions.NotFoundException( + "unable to find an external network" + ) + return floating_network_id + + def create_floating_ip( + self, + network=None, + server=None, + fixed_address=None, + nat_destination=None, + port=None, + wait=False, + timeout=60, + ): + """Allocate a new floating IP from a network or a pool. + + :param network: Name or ID of the network + that the floating IP should come from. + :param server: (optional) Server dict for the server to create + the IP for and to which it should be attached. + :param fixed_address: (optional) Fixed IP to attach the floating + ip to. + :param nat_destination: (optional) Name or ID of the network + that the fixed IP to attach the floating + IP to should be on. + :param port: (optional) The port ID that the floating IP should be + attached to. Specifying a port conflicts + with specifying a server, fixed_address or + nat_destination. + :param wait: (optional) Whether to wait for the IP to be active. + Defaults to False. Only applies if a server is + provided. + :param timeout: (optional) How long to wait for the IP to be active. + Defaults to 60. Only applies if a server is + provided. + + :returns: a floating IP address + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. + """ + if self._use_neutron_floating(): + try: + return self._neutron_create_floating_ip( + network_name_or_id=network, + server=server, + fixed_address=fixed_address, + nat_destination=nat_destination, + port=port, + wait=wait, + timeout=timeout, + ) + except exceptions.NotFoundException as e: + self.log.debug( + "Something went wrong talking to neutron API: " + "'%(msg)s'. Trying with Nova.", + {'msg': str(e)}, + ) + # Fall-through, trying with Nova + + if port: + raise exceptions.SDKException( + "This cloud uses nova-network which does not support " + "arbitrary floating-ip/port mappings. Please nudge " + "your cloud provider to upgrade the networking stack " + "to neutron, or alternately provide the server, " + "fixed_address and nat_destination arguments as appropriate" + ) + # Else, we are using Nova network + f_ips = self._normalize_floating_ips( + [self._nova_create_floating_ip(pool=network)] + ) + return f_ips[0] + + def _submit_create_fip(self, kwargs): + # Split into a method to aid in test mocking + return self.network.create_ip(**kwargs) + + def _neutron_create_floating_ip( + self, + network_name_or_id=None, + server=None, + fixed_address=None, + nat_destination=None, + port=None, + wait=False, + timeout=60, + network_id=None, + ): + if not network_id: + if network_name_or_id: + try: + network = self.network.find_network(network_name_or_id) + except exceptions.NotFoundException: + raise exceptions.NotFoundException( + "unable to find network for floating ips with ID " + "{}".format(network_name_or_id) + ) + network_id = network['id'] + else: + network_id = self._get_floating_network_id() + kwargs = { + 'floating_network_id': network_id, + } + if not port: + if server: + (port_obj, fixed_ip_address) = self._nat_destination_port( + server, + fixed_address=fixed_address, + nat_destination=nat_destination, + ) + if port_obj: + port = port_obj['id'] + if fixed_ip_address: + kwargs['fixed_ip_address'] = fixed_ip_address + if port: + kwargs['port_id'] = port + + fip = self._submit_create_fip(kwargs) + fip_id = fip['id'] + + if port: + # The FIP is only going to become active in this context + # when we've attached it to something, which only occurs + # if we've provided a port as a parameter + if wait: + try: + for count in utils.iterate_timeout( + timeout, + "Timeout waiting for the floating IP to be ACTIVE", + wait=min(5, timeout), + ): + fip = self.get_floating_ip(fip_id) + if fip and fip['status'] == 'ACTIVE': + break + except exceptions.ResourceTimeout: + self.log.error( + "Timed out on floating ip %(fip)s becoming active. " + "Deleting", + {'fip': fip_id}, + ) + try: + self.delete_floating_ip(fip_id) + except Exception as e: + self.log.error( + "FIP LEAK: Attempted to delete floating ip " + "%(fip)s but received %(exc)s exception: " + "%(err)s", + {'fip': fip_id, 'exc': e.__class__, 'err': str(e)}, + ) + raise + if fip['port_id'] != port: + if server: + raise exceptions.SDKException( + "Attempted to create FIP on port {port} for server " + "{server} but FIP has port {port_id}".format( + port=port, + port_id=fip['port_id'], + server=server['id'], + ) + ) + else: + raise exceptions.SDKException( + "Attempted to create FIP on port {port} " + "but something went wrong".format(port=port) + ) + return fip + + def _nova_create_floating_ip(self, pool=None): + with _utils.openstacksdk_exceptions( + f"Unable to create floating IP in pool {pool}" + ): + if pool is None: + pools = self.list_floating_ip_pools() + if not pools: + raise exceptions.NotFoundException( + "unable to find a floating ip pool" + ) + pool = pools[0]['name'] + + data = proxy._json_response( + self.compute.post('/os-floating-ips', json=dict(pool=pool)) + ) + pool_ip = self._get_and_munchify('floating_ip', data) + # TODO(mordred) Remove this - it's just for compat + data = proxy._json_response( + self.compute.get( + '/os-floating-ips/{id}'.format(id=pool_ip['id']) + ) + ) + return self._get_and_munchify('floating_ip', data) + + def delete_floating_ip(self, floating_ip_id, retry=1): + """Deallocate a floating IP from a project. + + :param floating_ip_id: a floating IP address ID. + :param retry: number of times to retry. Optional, defaults to 1, + which is in addition to the initial delete call. + A value of 0 will also cause no checking of results to + occur. + + :returns: True if the IP address has been deleted, False if the IP + address was not found. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. + """ + for count in range(0, max(0, retry) + 1): + result = self._delete_floating_ip(floating_ip_id) + + if (retry == 0) or not result: + return result + + # neutron sometimes returns success when deleting a floating + # ip. That's awesome. SO - verify that the delete actually + # worked. Some clouds will set the status to DOWN rather than + # deleting the IP immediately. This is, of course, a bit absurd. + f_ip = self.get_floating_ip(id=floating_ip_id) + if not f_ip or f_ip['status'] == 'DOWN': + return True + + raise exceptions.SDKException( + "Attempted to delete Floating IP {ip} with ID {id} a total of " + "{retry} times. Although the cloud did not indicate any errors " + "the floating IP is still in existence. Aborting further " + "operations.".format( + id=floating_ip_id, + ip=f_ip['floating_ip_address'], + retry=retry + 1, + ) + ) + + def _delete_floating_ip(self, floating_ip_id): + if self._use_neutron_floating(): + try: + return self._neutron_delete_floating_ip(floating_ip_id) + except exceptions.NotFoundException as e: + self.log.debug( + "Something went wrong talking to neutron API: " + "'%(msg)s'. Trying with Nova.", + {'msg': str(e)}, + ) + return self._nova_delete_floating_ip(floating_ip_id) + + def _neutron_delete_floating_ip(self, floating_ip_id): + try: + self.network.delete_ip(floating_ip_id, ignore_missing=False) + except exceptions.NotFoundException: + return False + return True + + def _nova_delete_floating_ip(self, floating_ip_id): + try: + proxy._json_response( + self.compute.delete(f'/os-floating-ips/{floating_ip_id}'), + error_message='Unable to delete floating IP {fip_id}'.format( + fip_id=floating_ip_id + ), + ) + except exceptions.NotFoundException: + return False + return True + + def delete_unattached_floating_ips(self, retry=1): + """Safely delete unattached floating ips. + + If the cloud can safely purge any unattached floating ips without + race conditions, do so. + + Safely here means a specific thing. It means that you are not running + this while another process that might do a two step create/attach + is running. You can safely run this method while another process + is creating servers and attaching floating IPs to them if either that + process is using add_auto_ip from shade, or is creating the floating + IPs by passing in a server to the create_floating_ip call. + + :param retry: number of times to retry. Optional, defaults to 1, + which is in addition to the initial delete call. + A value of 0 will also cause no checking of results to occur. + + :returns: Number of Floating IPs deleted, False if none + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. + """ + processed = [] + if self._use_neutron_floating(): + for ip in self.list_floating_ips(): + if not bool(ip.port_id): + processed.append( + self.delete_floating_ip( + floating_ip_id=ip['id'], retry=retry + ) + ) + return len(processed) if all(processed) else False + + def _attach_ip_to_server( + self, + server, + floating_ip, + fixed_address=None, + wait=False, + timeout=60, + skip_attach=False, + nat_destination=None, + ): + """Attach a floating IP to a server. + + :param server: Server dict + :param floating_ip: Floating IP dict to attach + :param fixed_address: (optional) fixed address to which attach the + floating IP to. + :param wait: (optional) Wait for the address to appear as assigned + to the server. Defaults to False. + :param timeout: (optional) Seconds to wait, defaults to 60. + See the ``wait`` parameter. + :param skip_attach: (optional) Skip the actual attach and just do + the wait. Defaults to False. + :param nat_destination: The fixed network the server's port for the + FIP to attach to will come from. + + :returns: The server ``openstack.compute.v2.server.Server`` + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. + """ + # Short circuit if we're asking to attach an IP that's already + # attached + ext_ip = meta.get_server_ip(server, ext_tag='floating', public=True) + if not ext_ip and floating_ip['port_id']: + # When we came here from reuse_fip and created FIP it might be + # already attached, but the server info might be also + # old to check whether it belongs to us now, thus refresh + # the server data and try again. There are some clouds, which + # explicitely forbids FIP assign call if it is already assigned. + server = self.compute.get_server(server['id']) + ext_ip = meta.get_server_ip( + server, ext_tag='floating', public=True + ) + if ext_ip == floating_ip['floating_ip_address']: + return server + + if self._use_neutron_floating(): + if not skip_attach: + try: + self._neutron_attach_ip_to_server( + server=server, + floating_ip=floating_ip, + fixed_address=fixed_address, + nat_destination=nat_destination, + ) + except exceptions.NotFoundException as e: + self.log.debug( + "Something went wrong talking to neutron API: " + "'%(msg)s'. Trying with Nova.", + {'msg': str(e)}, + ) + # Fall-through, trying with Nova + else: + # Nova network + self._nova_attach_ip_to_server( + server_id=server['id'], + floating_ip_id=floating_ip['id'], + fixed_address=fixed_address, + ) + + if wait: + # Wait for the address to be assigned to the server + server_id = server['id'] + for _ in utils.iterate_timeout( + timeout, + "Timeout waiting for the floating IP to be attached.", + wait=min(5, timeout), + ): + server = self.compute.get_server(server_id) + ext_ip = meta.get_server_ip( + server, ext_tag='floating', public=True + ) + if ext_ip == floating_ip['floating_ip_address']: + return server + return server + + def _neutron_attach_ip_to_server( + self, server, floating_ip, fixed_address=None, nat_destination=None + ): + # Find an available port + (port, fixed_address) = self._nat_destination_port( + server, + fixed_address=fixed_address, + nat_destination=nat_destination, + ) + if not port: + raise exceptions.SDKException( + "unable to find a port for server {}".format(server['id']) + ) + + floating_ip_args = {'port_id': port['id']} + if fixed_address is not None: + floating_ip_args['fixed_ip_address'] = fixed_address + + return self.network.update_ip(floating_ip, **floating_ip_args) + + def _nova_attach_ip_to_server( + self, server_id, floating_ip_id, fixed_address=None + ): + f_ip = self.get_floating_ip(id=floating_ip_id) + if f_ip is None: + raise exceptions.SDKException( + f"unable to find floating IP {floating_ip_id}" + ) + error_message = "Error attaching IP {ip} to instance {id}".format( + ip=floating_ip_id, id=server_id + ) + body = {'address': f_ip['floating_ip_address']} + if fixed_address: + body['fixed_address'] = fixed_address + return proxy._json_response( + self.compute.post( + f'/servers/{server_id}/action', + json=dict(addFloatingIp=body), + ), + error_message=error_message, + ) + + def detach_ip_from_server(self, server_id, floating_ip_id): + """Detach a floating IP from a server. + + :param server_id: ID of a server. + :param floating_ip_id: Id of the floating IP to detach. + + :returns: True if the IP has been detached, or False if the IP wasn't + attached to any server. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. + """ + if self._use_neutron_floating(): + try: + return self._neutron_detach_ip_from_server( + server_id=server_id, floating_ip_id=floating_ip_id + ) + except exceptions.NotFoundException as e: + self.log.debug( + "Something went wrong talking to neutron API: " + "'%(msg)s'. Trying with Nova.", + {'msg': str(e)}, + ) + # Fall-through, trying with Nova + + # Nova network + self._nova_detach_ip_from_server( + server_id=server_id, floating_ip_id=floating_ip_id + ) + + def _neutron_detach_ip_from_server(self, server_id, floating_ip_id): + f_ip = self.get_floating_ip(id=floating_ip_id) + if f_ip is None or not bool(f_ip.port_id): + return False + try: + self.network.update_ip(floating_ip_id, port_id=None) + except exceptions.SDKException: + raise exceptions.SDKException( + "Error detaching IP {ip} from " + "server {server_id}".format( + ip=floating_ip_id, server_id=server_id + ) + ) + + return True + + def _nova_detach_ip_from_server(self, server_id, floating_ip_id): + f_ip = self.get_floating_ip(id=floating_ip_id) + if f_ip is None: + raise exceptions.SDKException( + f"unable to find floating IP {floating_ip_id}" + ) + error_message = "Error detaching IP {ip} from instance {id}".format( + ip=floating_ip_id, id=server_id + ) + return proxy._json_response( + self.compute.post( + f'/servers/{server_id}/action', + json=dict( + removeFloatingIp=dict(address=f_ip['floating_ip_address']) + ), + ), + error_message=error_message, + ) + + return True + + def _add_ip_from_pool( + self, + server, + network, + fixed_address=None, + reuse=True, + wait=False, + timeout=60, + nat_destination=None, + ): + """Add a floating IP to a server from a given pool + + This method reuses available IPs, when possible, or allocate new IPs + to the current tenant. + The floating IP is attached to the given fixed address or to the + first server port/fixed address + + :param server: Server dict + :param network: Name or ID of the network. + :param fixed_address: a fixed address + :param reuse: Try to reuse existing ips. Defaults to True. + :param wait: (optional) Wait for the address to appear as assigned + to the server. Defaults to False. + :param timeout: (optional) Seconds to wait, defaults to 60. + See the ``wait`` parameter. + :param nat_destination: (optional) the name of the network of the + port to associate with the floating ip. + + :returns: the updated server ``openstack.compute.v2.server.Server`` + """ + if reuse: + f_ip = self.available_floating_ip(network=network) + else: + start_time = time.time() + f_ip = self.create_floating_ip( + server=server, + network=network, + nat_destination=nat_destination, + fixed_address=fixed_address, + wait=wait, + timeout=timeout, + ) + timeout = timeout - (time.time() - start_time) + server = self.compute.get_server(server.id) + + # We run attach as a second call rather than in the create call + # because there are code flows where we will not have an attached + # FIP yet. However, even if it was attached in the create, we run + # the attach function below to get back the server dict refreshed + # with the FIP information. + return self._attach_ip_to_server( + server=server, + floating_ip=f_ip, + fixed_address=fixed_address, + wait=wait, + timeout=timeout, + nat_destination=nat_destination, + ) + + def add_ip_list( + self, + server, + ips, + wait=False, + timeout=60, + fixed_address=None, + nat_destination=None, + ): + """Attach a list of IPs to a server. + + :param server: a server object + :param ips: list of floating IP addresses or a single address + :param wait: (optional) Wait for the address to appear as assigned + to the server. Defaults to False. + :param timeout: (optional) Seconds to wait, defaults to 60. + See the ``wait`` parameter. + :param fixed_address: (optional) Fixed address of the server to + attach the IP to + :param nat_destination: (optional) Name or ID of the network that + the fixed IP to attach the floating IP should be on + + :returns: The updated server ``openstack.compute.v2.server.Server`` + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. + """ + + if type(ips) is list: + ips = [ips] + + for ip in ips: + f_ip = self.get_floating_ip( + id=None, filters={'floating_ip_address': ip} + ) + server = self._attach_ip_to_server( + server=server, + floating_ip=f_ip, + wait=wait, + timeout=timeout, + fixed_address=fixed_address, + nat_destination=nat_destination, + ) + return server + + def add_auto_ip(self, server, wait=False, timeout=60, reuse=True): + """Add a floating IP to a server. + + This method is intended for basic usage. For advanced network + architecture (e.g. multiple external networks or servers with multiple + interfaces), use other floating IP methods. + + This method can reuse available IPs, or allocate new IPs to the current + project. + + :param server: a server dictionary. + :param reuse: Whether or not to attempt to reuse IPs, defaults + to True. + :param wait: (optional) Wait for the address to appear as assigned + to the server. Defaults to False. + :param timeout: (optional) Seconds to wait, defaults to 60. + See the ``wait`` parameter. + :param reuse: Try to reuse existing ips. Defaults to True. + + :returns: Floating IP address attached to server. + """ + server = self._add_auto_ip( + server, wait=wait, timeout=timeout, reuse=reuse + ) + return server['interface_ip'] or None + + def _add_auto_ip(self, server, wait=False, timeout=60, reuse=True): + skip_attach = False + created = False + if reuse: + f_ip = self.available_floating_ip(server=server) + else: + start_time = time.time() + f_ip = self.create_floating_ip( + server=server, wait=wait, timeout=timeout + ) + timeout = timeout - (time.time() - start_time) + if server: + # This gets passed in for both nova and neutron + # but is only meaningful for the neutron logic branch + skip_attach = True + created = True + + try: + # We run attach as a second call rather than in the create call + # because there are code flows where we will not have an attached + # FIP yet. However, even if it was attached in the create, we run + # the attach function below to get back the server dict refreshed + # with the FIP information. + return self._attach_ip_to_server( + server=server, + floating_ip=f_ip, + wait=wait, + timeout=timeout, + skip_attach=skip_attach, + ) + except exceptions.ResourceTimeout: + if self._use_neutron_floating() and created: + # We are here because we created an IP on the port + # It failed. Delete so as not to leak an unmanaged + # resource + self.log.error( + "Timeout waiting for floating IP to become " + "active. Floating IP %(ip)s:%(id)s was created for " + "server %(server)s but is being deleted due to " + "activation failure.", + { + 'ip': f_ip['floating_ip_address'], + 'id': f_ip['id'], + 'server': server['id'], + }, + ) + try: + self.delete_floating_ip(f_ip['id']) + except Exception as e: + self.log.error( + "FIP LEAK: Attempted to delete floating ip " + "%(fip)s but received %(exc)s exception: %(err)s", + {'fip': f_ip['id'], 'exc': e.__class__, 'err': str(e)}, + ) + raise e + raise + + def add_ips_to_server( + self, + server, + auto_ip=True, + ips=None, + ip_pool=None, + wait=False, + timeout=60, + reuse=True, + fixed_address=None, + nat_destination=None, + ): + if ip_pool: + server = self._add_ip_from_pool( + server, + ip_pool, + reuse=reuse, + wait=wait, + timeout=timeout, + fixed_address=fixed_address, + nat_destination=nat_destination, + ) + elif ips: + server = self.add_ip_list( + server, + ips, + wait=wait, + timeout=timeout, + fixed_address=fixed_address, + nat_destination=nat_destination, + ) + elif auto_ip: + if self._needs_floating_ip(server, nat_destination): + server = self._add_auto_ip( + server, wait=wait, timeout=timeout, reuse=reuse + ) + return server + + def _needs_floating_ip(self, server, nat_destination): + """Figure out if auto_ip should add a floating ip to this server. + + If the server has a floating ip it does not need another one. + + If the server does not have a fixed ip address it does not need a + floating ip. + + If self.private then the server does not need a floating ip. + + If the cloud runs nova, and the server has a private address and not a + public address, then the server needs a floating ip. + + If the server has a fixed ip address and no floating ip address and the + cloud has a network from which floating IPs come that is connected via + a router to the network from which the fixed ip address came, + then the server needs a floating ip. + + If the server has a fixed ip address and no floating ip address and the + cloud does not have a network from which floating ips come, or it has + one but that network is not connected to the network from which + the server's fixed ip address came via a router, then the + server does not need a floating ip. + """ + if not self._has_floating_ips(): + return False + + if server['addresses'] is None: + # fetch missing server details, e.g. because + # meta.add_server_interfaces() was not called + server = self.compute.get_server(server) + + if server['public_v4'] or any( + [ + any( + [ + address['OS-EXT-IPS:type'] == 'floating' + for address in addresses + ] + ) + for addresses in (server['addresses'] or {}).values() + ] + ): + return False + + if not server['private_v4'] and not any( + [ + any( + [ + address['OS-EXT-IPS:type'] == 'fixed' + for address in addresses + ] + ) + for addresses in (server['addresses'] or {}).values() + ] + ): + return False + + if self.private: + return False + + if not self.has_service('network'): + return True + + # No floating ip network - no FIPs + try: + self._get_floating_network_id() + except exceptions.SDKException: + return False + + (port_obj, fixed_ip_address) = self._nat_destination_port( + server, nat_destination=nat_destination + ) + + if not port_obj or not fixed_ip_address: + return False + + return True + + def _nat_destination_port( + self, server, fixed_address=None, nat_destination=None + ): + """Returns server port that is on a nat_destination network + + Find a port attached to the server which is on a network which + has a subnet which can be the destination of NAT. Such a network + is referred to in shade as a "nat_destination" network. So this + then is a function which returns a port on such a network that is + associated with the given server. + + :param server: Server dict. + :param fixed_address: Fixed ip address of the port + :param nat_destination: Name or ID of the network of the port. + """ + ports = list(self.network.ports(device_id=server['id'])) + if not ports: + return (None, None) + + port = None + if not fixed_address: + if len(ports) > 1: + if nat_destination: + nat_network = self.network.find_network(nat_destination) + if not nat_network: + raise exceptions.SDKException( + 'NAT Destination {nat_destination} was configured' + ' but not found on the cloud. Please check your' + ' config and your cloud and try again.'.format( + nat_destination=nat_destination + ) + ) + else: + nat_network = self.get_nat_destination() + + if not nat_network: + raise exceptions.SDKException( + 'Multiple ports were found for server {server}' + ' but none of the networks are a valid NAT' + ' destination, so it is impossible to add a' + ' floating IP. If you have a network that is a valid' + ' destination for NAT and we could not find it,' + ' please file a bug. But also configure the' + ' nat_destination property of the networks list in' + ' your clouds.yaml file. If you do not have a' + ' clouds.yaml file, please make one - your setup' + ' is complicated.'.format(server=server['id']) + ) + + maybe_ports = [] + for maybe_port in ports: + if maybe_port['network_id'] == nat_network['id']: + maybe_ports.append(maybe_port) + if not maybe_ports: + raise exceptions.SDKException( + 'No port on server {server} was found matching' + ' your NAT destination network {dest}. Please ' + ' check your config'.format( + server=server['id'], dest=nat_network['name'] + ) + ) + ports = maybe_ports + + # Select the most recent available IPv4 address + # To do this, sort the ports in reverse order by the created_at + # field which is a string containing an ISO DateTime (which + # thankfully sort properly) This way the most recent port created, + # if there are more than one, will be the arbitrary port we + # select. + for port in sorted( + ports, key=lambda p: p.get('created_at', 0), reverse=True + ): + for address in port.get('fixed_ips', list()): + try: + ip = ipaddress.ip_address(address['ip_address']) + except Exception: + continue + if ip.version == 4: + fixed_address = address['ip_address'] + return port, fixed_address + raise exceptions.SDKException( + "unable to find a free fixed IPv4 address for server " + "{}".format(server['id']) + ) + # unfortunately a port can have more than one fixed IP: + # we can't use the search_ports filtering for fixed_address as + # they are contained in a list. e.g. + # + # "fixed_ips": [ + # { + # "subnet_id": "008ba151-0b8c-4a67-98b5-0d2b87666062", + # "ip_address": "172.24.4.2" + # } + # ] + # + # Search fixed_address + for p in ports: + for fixed_ip in p['fixed_ips']: + if fixed_address == fixed_ip['ip_address']: + return (p, fixed_address) + return (None, None) + + def _has_floating_ips(self): + if not self._floating_ip_source: + return False + else: + return self._floating_ip_source in ('nova', 'neutron') + + def _use_neutron_floating(self): + return ( + self.has_service('network') + and self._floating_ip_source == 'neutron' + ) + + def _normalize_floating_ips(self, ips): + """Normalize the structure of floating IPs + + Unfortunately, not all the Neutron floating_ip attributes are available + with Nova and not all Nova floating_ip attributes are available with + Neutron. + This function extract attributes that are common to Nova and Neutron + floating IP resource. + If the whole structure is needed inside openstacksdk there are private + methods that returns "original" objects (e.g. + _neutron_allocate_floating_ip) + + :param list ips: A list of Neutron floating IPs. + + :returns: + A list of normalized dicts with the following attributes:: + + [ + { + "id": "this-is-a-floating-ip-id", + "fixed_ip_address": "192.0.2.10", + "floating_ip_address": "198.51.100.10", + "network": "this-is-a-net-or-pool-id", + "attached": True, + "status": "ACTIVE" + }, ... + ] + + """ + return [self._normalize_floating_ip(ip) for ip in ips] + + def _normalize_floating_ip(self, ip): + # Copy incoming floating ip because of shared dicts in unittests + # Only import munch when we really need it + + location = self._get_current_location(project_id=ip.get('owner')) + # This copy is to keep things from getting epically weird in tests + ip = ip.copy() + + ret = utils.Munch(location=location) + + fixed_ip_address = ip.pop('fixed_ip_address', ip.pop('fixed_ip', None)) + floating_ip_address = ip.pop('floating_ip_address', ip.pop('ip', None)) + network_id = ip.pop( + 'floating_network_id', ip.pop('network', ip.pop('pool', None)) + ) + project_id = ip.pop('tenant_id', '') + project_id = ip.pop('project_id', project_id) + + instance_id = ip.pop('instance_id', None) + router_id = ip.pop('router_id', None) + id = ip.pop('id') + port_id = ip.pop('port_id', None) + created_at = ip.pop('created_at', None) + updated_at = ip.pop('updated_at', None) + # Note - description may not always be on the underlying cloud. + # Normalizing it here is easy - what do we do when people want to + # set a description? + description = ip.pop('description', '') + revision_number = ip.pop('revision_number', None) + + if self._use_neutron_floating(): + attached = bool(port_id) + status = ip.pop('status', 'UNKNOWN') + else: + attached = bool(instance_id) + # In neutron's terms, Nova floating IPs are always ACTIVE + status = 'ACTIVE' + + ret = utils.Munch( + attached=attached, + fixed_ip_address=fixed_ip_address, + floating_ip_address=floating_ip_address, + id=id, + location=self._get_current_location(project_id=project_id), + network=network_id, + port=port_id, + router=router_id, + status=status, + created_at=created_at, + updated_at=updated_at, + description=description, + revision_number=revision_number, + properties=ip.copy(), + ) + # Backwards compat + if not self.strict_mode: + ret['port_id'] = port_id + ret['router_id'] = router_id + ret['project_id'] = project_id + ret['tenant_id'] = project_id + ret['floating_network_id'] = network_id + for key, val in ret['properties'].items(): + ret.setdefault(key, val) + + return ret + + # security groups + + def search_security_groups(self, name_or_id=None, filters=None): + # `filters` could be a dict or a jmespath (str) + groups = self.list_security_groups( + filters=filters if isinstance(filters, dict) else None + ) + return _utils._filter_list(groups, name_or_id, filters) + + def list_security_groups(self, filters=None): + """List all available security groups. + + :param filters: (optional) dict of filter conditions to push down + :returns: A list of security group + ``openstack.network.v2.security_group.SecurityGroup``. + + """ + # Security groups not supported + if not self._has_secgroups(): + raise exc.OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + + if not filters: + filters = {} + + data = [] + # Handle neutron security groups + if self._use_neutron_secgroups(): + # pass filters dict to the list to filter as much as possible on + # the server side + return list(self.network.security_groups(**filters)) + # Handle nova security groups + else: + data = proxy._json_response( + self.compute.get('/os-security-groups', params=filters) + ) + return self._normalize_secgroups( + self._get_and_munchify('security_groups', data) + ) + + def get_security_group(self, name_or_id, filters=None): + """Get a security group by name or ID. + + :param name_or_id: Name or ID of the security group. + :param filters: + A dictionary of meta data to use for further filtering. Elements + of this dictionary may, themselves, be dictionaries. Example:: + + { + 'last_name': 'Smith', + 'other': { + 'gender': 'Female' + } + } + + OR + A string containing a jmespath expression for further filtering. + Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + + :returns: A security group + ``openstack.network.v2.security_group.SecurityGroup`` + or None if no matching security group is found. + + """ + return _utils._get_entity(self, 'security_group', name_or_id, filters) + + def get_security_group_by_id(self, id): + """Get a security group by ID + + :param id: ID of the security group. + :returns: A security group + ``openstack.network.v2.security_group.SecurityGroup``. + """ + if not self._has_secgroups(): + raise exc.OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + error_message = f"Error getting security group with ID {id}" + if self._use_neutron_secgroups(): + return self.network.get_security_group(id) + else: + data = proxy._json_response( + self.compute.get(f'/os-security-groups/{id}'), + error_message=error_message, + ) + return self._normalize_secgroup( + self._get_and_munchify('security_group', data) + ) + + def create_security_group( + self, name, description, project_id=None, stateful=None + ): + """Create a new security group + + :param string name: A name for the security group. + :param string description: Describes the security group. + :param string project_id: + Specify the project ID this security group will be created + on (admin-only). + :param string stateful: Whether the security group is stateful or not. + + :returns: A ``openstack.network.v2.security_group.SecurityGroup`` + representing the new security group. + + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. + :raises: OpenStackCloudUnavailableFeature if security groups are + not supported on this cloud. + """ + + # Security groups not supported + if not self._has_secgroups(): + raise exc.OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + + data = [] + security_group_json = {'name': name, 'description': description} + if stateful is not None: + security_group_json['stateful'] = stateful + if project_id is not None: + security_group_json['tenant_id'] = project_id + if self._use_neutron_secgroups(): + return self.network.create_security_group(**security_group_json) + else: + data = proxy._json_response( + self.compute.post( + '/os-security-groups', + json={'security_group': security_group_json}, + ) + ) + return self._normalize_secgroup( + self._get_and_munchify('security_group', data) + ) + + def delete_security_group(self, name_or_id): + """Delete a security group + + :param string name_or_id: The name or unique ID of the security group. + + :returns: True if delete succeeded, False otherwise. + + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. + :raises: OpenStackCloudUnavailableFeature if security groups are + not supported on this cloud. + """ + # Security groups not supported + if not self._has_secgroups(): + raise exc.OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + + # TODO(mordred): Let's come back and stop doing a GET before we do + # the delete. + secgroup = self.get_security_group(name_or_id) + if secgroup is None: + self.log.debug( + 'Security group %s not found for deleting', name_or_id + ) + return False + + if self._use_neutron_secgroups(): + self.network.delete_security_group( + secgroup['id'], ignore_missing=False + ) + return True + + else: + proxy._json_response( + self.compute.delete( + '/os-security-groups/{id}'.format(id=secgroup['id']) + ) + ) + return True + + @_utils.valid_kwargs('name', 'description', 'stateful') + def update_security_group(self, name_or_id, **kwargs): + """Update a security group + + :param string name_or_id: Name or ID of the security group to update. + :param string name: New name for the security group. + :param string description: New description for the security group. + + :returns: A ``openstack.network.v2.security_group.SecurityGroup`` + describing the updated security group. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. + """ + # Security groups not supported + if not self._has_secgroups(): + raise exc.OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + + group = self.get_security_group(name_or_id) + + if group is None: + raise exceptions.SDKException( + "Security group %s not found." % name_or_id + ) + + if self._use_neutron_secgroups(): + return self.network.update_security_group(group['id'], **kwargs) + else: + for key in ('name', 'description'): + kwargs.setdefault(key, group[key]) + data = proxy._json_response( + self.compute.put( + '/os-security-groups/{id}'.format(id=group['id']), + json={'security_group': kwargs}, + ) + ) + return self._normalize_secgroup( + self._get_and_munchify('security_group', data) + ) + + def create_security_group_rule( + self, + secgroup_name_or_id, + port_range_min=None, + port_range_max=None, + protocol=None, + remote_ip_prefix=None, + remote_group_id=None, + remote_address_group_id=None, + direction='ingress', + ethertype='IPv4', + project_id=None, + description=None, + ): + """Create a new security group rule + + :param string secgroup_name_or_id: + The security group name or ID to associate with this security + group rule. If a non-unique group name is given, an exception + is raised. + :param int port_range_min: + The minimum port number in the range that is matched by the + security group rule. If the protocol is TCP or UDP, this value + must be less than or equal to the port_range_max attribute value. + If nova is used by the cloud provider for security groups, then + a value of None will be transformed to -1. + :param int port_range_max: + The maximum port number in the range that is matched by the + security group rule. The port_range_min attribute constrains the + port_range_max attribute. If nova is used by the cloud provider + for security groups, then a value of None will be transformed + to -1. + :param string protocol: + The protocol that is matched by the security group rule. Valid + values are None, tcp, udp, and icmp. + :param string remote_ip_prefix: + The remote IP prefix to be associated with this security group + rule. This attribute matches the specified IP prefix as the + source IP address of the IP packet. + :param string remote_group_id: + The remote group ID to be associated with this security group + rule. + :param string remote_address_group_id: + The remote address group ID to be associated with this security + group rule. + :param string direction: + Ingress or egress: The direction in which the security group + rule is applied. For a compute instance, an ingress security + group rule is applied to incoming (ingress) traffic for that + instance. An egress rule is applied to traffic leaving the + instance. + :param string ethertype: + Must be IPv4 or IPv6, and addresses represented in CIDR must + match the ingress or egress rules. + :param string project_id: + Specify the project ID this security group will be created + on (admin-only). + :param string description: + Description of the rule, max 255 characters. + + :returns: A ``openstack.network.v2.security_group.SecurityGroup`` + representing the new security group rule. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. + """ + # Security groups not supported + if not self._has_secgroups(): + raise exc.OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + + secgroup = self.get_security_group(secgroup_name_or_id) + if not secgroup: + raise exceptions.SDKException( + "Security group %s not found." % secgroup_name_or_id + ) + + if self._use_neutron_secgroups(): + # NOTE: Nova accepts -1 port numbers, but Neutron accepts None + # as the equivalent value. + rule_def = { + 'security_group_id': secgroup['id'], + 'port_range_min': ( + None if port_range_min == -1 else port_range_min + ), + 'port_range_max': ( + None if port_range_max == -1 else port_range_max + ), + 'protocol': protocol, + 'remote_ip_prefix': remote_ip_prefix, + 'remote_group_id': remote_group_id, + 'remote_address_group_id': remote_address_group_id, + 'direction': direction, + 'ethertype': ethertype, + } + if project_id is not None: + rule_def['tenant_id'] = project_id + if description is not None: + rule_def["description"] = description + return self.network.create_security_group_rule(**rule_def) + else: + # NOTE: Neutron accepts None for protocol. Nova does not. + if protocol is None: + raise exceptions.SDKException('Protocol must be specified') + + if direction == 'egress': + self.log.debug( + 'Rule creation failed: Nova does not support egress rules' + ) + raise exceptions.SDKException('No support for egress rules') + + # NOTE: Neutron accepts None for ports, but Nova requires -1 + # as the equivalent value for ICMP. + # + # For TCP/UDP, if both are None, Neutron allows this and Nova + # represents this as all ports (1-65535). Nova does not accept + # None values, so to hide this difference, we will automatically + # convert to the full port range. If only a single port value is + # specified, it will error as normal. + if protocol == 'icmp': + if port_range_min is None: + port_range_min = -1 + if port_range_max is None: + port_range_max = -1 + elif protocol in ['tcp', 'udp']: + if port_range_min is None and port_range_max is None: + port_range_min = 1 + port_range_max = 65535 + + security_group_rule_dict = dict( + security_group_rule=dict( + parent_group_id=secgroup['id'], + ip_protocol=protocol, + from_port=port_range_min, + to_port=port_range_max, + cidr=remote_ip_prefix, + group_id=remote_group_id, + ) + ) + if project_id is not None: + security_group_rule_dict['security_group_rule'][ + 'tenant_id' + ] = project_id + data = proxy._json_response( + self.compute.post( + '/os-security-group-rules', json=security_group_rule_dict + ) + ) + return self._normalize_secgroup_rule( + self._get_and_munchify('security_group_rule', data) + ) + + def delete_security_group_rule(self, rule_id): + """Delete a security group rule + + :param string rule_id: The unique ID of the security group rule. + + :returns: True if delete succeeded, False otherwise. + + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. + :raises: OpenStackCloudUnavailableFeature if security groups are + not supported on this cloud. + """ + # Security groups not supported + if not self._has_secgroups(): + raise exc.OpenStackCloudUnavailableFeature( + "Unavailable feature: security groups" + ) + + if self._use_neutron_secgroups(): + self.network.delete_security_group_rule( + rule_id, ignore_missing=False + ) + return True + + else: + try: + exceptions.raise_from_response( + self.compute.delete(f'/os-security-group-rules/{rule_id}') + ) + except exceptions.NotFoundException: + return False + + return True + + def _has_secgroups(self): + if not self.secgroup_source: + return False + else: + return self.secgroup_source.lower() in ('nova', 'neutron') + + def _use_neutron_secgroups(self): + return ( + self.has_service('network') and self.secgroup_source == 'neutron' + ) + + def _normalize_secgroups(self, groups): + """Normalize the structure of security groups + + This makes security group dicts, as returned from nova, look like the + security group dicts as returned from neutron. This does not make them + look exactly the same, but it's pretty close. + + :param list groups: A list of security group dicts. + + :returns: A list of normalized dicts. + """ + ret = [] + for group in groups: + ret.append(self._normalize_secgroup(group)) + return ret + + # TODO(stephenfin): Remove this once we get rid of support for nova + # secgroups + def _normalize_secgroup(self, group): + ret = utils.Munch() + # Copy incoming group because of shared dicts in unittests + group = group.copy() + + # Discard noise + self._remove_novaclient_artifacts(group) + + rules = self._normalize_secgroup_rules( + group.pop('security_group_rules', group.pop('rules', [])) + ) + project_id = group.pop('tenant_id', '') + project_id = group.pop('project_id', project_id) + + ret['location'] = self._get_current_location(project_id=project_id) + ret['id'] = group.pop('id') + ret['name'] = group.pop('name') + ret['security_group_rules'] = rules + ret['description'] = group.pop('description') + ret['properties'] = group + + if self._use_neutron_secgroups(): + ret['stateful'] = group.pop('stateful', True) + + # Backwards compat with Neutron + if not self.strict_mode: + ret['tenant_id'] = project_id + ret['project_id'] = project_id + for key, val in ret['properties'].items(): + ret.setdefault(key, val) + + return ret + + # TODO(stephenfin): Remove this once we get rid of support for nova + # secgroups + def _normalize_secgroup_rules(self, rules): + """Normalize the structure of nova security group rules + + Note that nova uses -1 for non-specific port values, but neutron + represents these with None. + + :param list rules: A list of security group rule dicts. + + :returns: A list of normalized dicts. + """ + ret = [] + for rule in rules: + ret.append(self._normalize_secgroup_rule(rule)) + return ret + + # TODO(stephenfin): Remove this once we get rid of support for nova + # secgroups + def _normalize_secgroup_rule(self, rule): + ret = utils.Munch() + # Copy incoming rule because of shared dicts in unittests + rule = rule.copy() + + ret['id'] = rule.pop('id') + ret['direction'] = rule.pop('direction', 'ingress') + ret['ethertype'] = rule.pop('ethertype', 'IPv4') + port_range_min = rule.get( + 'port_range_min', rule.pop('from_port', None) + ) + if port_range_min == -1: + port_range_min = None + if port_range_min is not None: + port_range_min = int(port_range_min) + ret['port_range_min'] = port_range_min + port_range_max = rule.pop('port_range_max', rule.pop('to_port', None)) + if port_range_max == -1: + port_range_max = None + if port_range_min is not None: + port_range_min = int(port_range_min) + ret['port_range_max'] = port_range_max + ret['protocol'] = rule.pop('protocol', rule.pop('ip_protocol', None)) + ret['remote_ip_prefix'] = rule.pop( + 'remote_ip_prefix', rule.pop('ip_range', {}).get('cidr', None) + ) + ret['security_group_id'] = rule.pop( + 'security_group_id', rule.pop('parent_group_id', None) + ) + ret['remote_group_id'] = rule.pop('remote_group_id', None) + project_id = rule.pop('tenant_id', '') + project_id = rule.pop('project_id', project_id) + ret['location'] = self._get_current_location(project_id=project_id) + ret['properties'] = rule + + # Backwards compat with Neutron + if not self.strict_mode: + ret['tenant_id'] = project_id + ret['project_id'] = project_id + for key, val in ret['properties'].items(): + ret.setdefault(key, val) + return ret + + def _remove_novaclient_artifacts(self, item): + # Remove novaclient artifacts + item.pop('links', None) + item.pop('NAME_ATTR', None) + item.pop('HUMAN_ID', None) + item.pop('human_id', None) + item.pop('request_ids', None) + item.pop('x_openstack_request_ids', None) diff --git a/openstack/cloud/_security_group.py b/openstack/cloud/_security_group.py deleted file mode 100644 index 924982839..000000000 --- a/openstack/cloud/_security_group.py +++ /dev/null @@ -1,561 +0,0 @@ -# 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.cloud import _utils -from openstack.cloud import exc -from openstack.cloud import openstackcloud -from openstack import exceptions -from openstack import proxy -from openstack import utils - - -class SecurityGroupCloudMixin(openstackcloud._OpenStackCloudMixin): - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.secgroup_source = self.config.config['secgroup_source'] - - def search_security_groups(self, name_or_id=None, filters=None): - # `filters` could be a dict or a jmespath (str) - groups = self.list_security_groups( - filters=filters if isinstance(filters, dict) else None - ) - return _utils._filter_list(groups, name_or_id, filters) - - def list_security_groups(self, filters=None): - """List all available security groups. - - :param filters: (optional) dict of filter conditions to push down - :returns: A list of security group - ``openstack.network.v2.security_group.SecurityGroup``. - - """ - # Security groups not supported - if not self._has_secgroups(): - raise exc.OpenStackCloudUnavailableFeature( - "Unavailable feature: security groups" - ) - - if not filters: - filters = {} - - data = [] - # Handle neutron security groups - if self._use_neutron_secgroups(): - # pass filters dict to the list to filter as much as possible on - # the server side - return list(self.network.security_groups(**filters)) - # Handle nova security groups - else: - data = proxy._json_response( - self.compute.get('/os-security-groups', params=filters) - ) - return self._normalize_secgroups( - self._get_and_munchify('security_groups', data) - ) - - def get_security_group(self, name_or_id, filters=None): - """Get a security group by name or ID. - - :param name_or_id: Name or ID of the security group. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: - - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } - - OR - A string containing a jmespath expression for further filtering. - Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - - :returns: A security group - ``openstack.network.v2.security_group.SecurityGroup`` - or None if no matching security group is found. - - """ - return _utils._get_entity(self, 'security_group', name_or_id, filters) - - def get_security_group_by_id(self, id): - """Get a security group by ID - - :param id: ID of the security group. - :returns: A security group - ``openstack.network.v2.security_group.SecurityGroup``. - """ - if not self._has_secgroups(): - raise exc.OpenStackCloudUnavailableFeature( - "Unavailable feature: security groups" - ) - error_message = f"Error getting security group with ID {id}" - if self._use_neutron_secgroups(): - return self.network.get_security_group(id) - else: - data = proxy._json_response( - self.compute.get(f'/os-security-groups/{id}'), - error_message=error_message, - ) - return self._normalize_secgroup( - self._get_and_munchify('security_group', data) - ) - - def create_security_group( - self, name, description, project_id=None, stateful=None - ): - """Create a new security group - - :param string name: A name for the security group. - :param string description: Describes the security group. - :param string project_id: - Specify the project ID this security group will be created - on (admin-only). - :param string stateful: Whether the security group is stateful or not. - - :returns: A ``openstack.network.v2.security_group.SecurityGroup`` - representing the new security group. - - :raises: :class:`~openstack.exceptions.SDKException` on operation - error. - :raises: OpenStackCloudUnavailableFeature if security groups are - not supported on this cloud. - """ - - # Security groups not supported - if not self._has_secgroups(): - raise exc.OpenStackCloudUnavailableFeature( - "Unavailable feature: security groups" - ) - - data = [] - security_group_json = {'name': name, 'description': description} - if stateful is not None: - security_group_json['stateful'] = stateful - if project_id is not None: - security_group_json['tenant_id'] = project_id - if self._use_neutron_secgroups(): - return self.network.create_security_group(**security_group_json) - else: - data = proxy._json_response( - self.compute.post( - '/os-security-groups', - json={'security_group': security_group_json}, - ) - ) - return self._normalize_secgroup( - self._get_and_munchify('security_group', data) - ) - - def delete_security_group(self, name_or_id): - """Delete a security group - - :param string name_or_id: The name or unique ID of the security group. - - :returns: True if delete succeeded, False otherwise. - - :raises: :class:`~openstack.exceptions.SDKException` on operation - error. - :raises: OpenStackCloudUnavailableFeature if security groups are - not supported on this cloud. - """ - # Security groups not supported - if not self._has_secgroups(): - raise exc.OpenStackCloudUnavailableFeature( - "Unavailable feature: security groups" - ) - - # TODO(mordred): Let's come back and stop doing a GET before we do - # the delete. - secgroup = self.get_security_group(name_or_id) - if secgroup is None: - self.log.debug( - 'Security group %s not found for deleting', name_or_id - ) - return False - - if self._use_neutron_secgroups(): - self.network.delete_security_group( - secgroup['id'], ignore_missing=False - ) - return True - - else: - proxy._json_response( - self.compute.delete( - '/os-security-groups/{id}'.format(id=secgroup['id']) - ) - ) - return True - - @_utils.valid_kwargs('name', 'description', 'stateful') - def update_security_group(self, name_or_id, **kwargs): - """Update a security group - - :param string name_or_id: Name or ID of the security group to update. - :param string name: New name for the security group. - :param string description: New description for the security group. - - :returns: A ``openstack.network.v2.security_group.SecurityGroup`` - describing the updated security group. - :raises: :class:`~openstack.exceptions.SDKException` on operation - error. - """ - # Security groups not supported - if not self._has_secgroups(): - raise exc.OpenStackCloudUnavailableFeature( - "Unavailable feature: security groups" - ) - - group = self.get_security_group(name_or_id) - - if group is None: - raise exceptions.SDKException( - "Security group %s not found." % name_or_id - ) - - if self._use_neutron_secgroups(): - return self.network.update_security_group(group['id'], **kwargs) - else: - for key in ('name', 'description'): - kwargs.setdefault(key, group[key]) - data = proxy._json_response( - self.compute.put( - '/os-security-groups/{id}'.format(id=group['id']), - json={'security_group': kwargs}, - ) - ) - return self._normalize_secgroup( - self._get_and_munchify('security_group', data) - ) - - def create_security_group_rule( - self, - secgroup_name_or_id, - port_range_min=None, - port_range_max=None, - protocol=None, - remote_ip_prefix=None, - remote_group_id=None, - remote_address_group_id=None, - direction='ingress', - ethertype='IPv4', - project_id=None, - description=None, - ): - """Create a new security group rule - - :param string secgroup_name_or_id: - The security group name or ID to associate with this security - group rule. If a non-unique group name is given, an exception - is raised. - :param int port_range_min: - The minimum port number in the range that is matched by the - security group rule. If the protocol is TCP or UDP, this value - must be less than or equal to the port_range_max attribute value. - If nova is used by the cloud provider for security groups, then - a value of None will be transformed to -1. - :param int port_range_max: - The maximum port number in the range that is matched by the - security group rule. The port_range_min attribute constrains the - port_range_max attribute. If nova is used by the cloud provider - for security groups, then a value of None will be transformed - to -1. - :param string protocol: - The protocol that is matched by the security group rule. Valid - values are None, tcp, udp, and icmp. - :param string remote_ip_prefix: - The remote IP prefix to be associated with this security group - rule. This attribute matches the specified IP prefix as the - source IP address of the IP packet. - :param string remote_group_id: - The remote group ID to be associated with this security group - rule. - :param string remote_address_group_id: - The remote address group ID to be associated with this security - group rule. - :param string direction: - Ingress or egress: The direction in which the security group - rule is applied. For a compute instance, an ingress security - group rule is applied to incoming (ingress) traffic for that - instance. An egress rule is applied to traffic leaving the - instance. - :param string ethertype: - Must be IPv4 or IPv6, and addresses represented in CIDR must - match the ingress or egress rules. - :param string project_id: - Specify the project ID this security group will be created - on (admin-only). - :param string description: - Description of the rule, max 255 characters. - - :returns: A ``openstack.network.v2.security_group.SecurityGroup`` - representing the new security group rule. - :raises: :class:`~openstack.exceptions.SDKException` on operation - error. - """ - # Security groups not supported - if not self._has_secgroups(): - raise exc.OpenStackCloudUnavailableFeature( - "Unavailable feature: security groups" - ) - - secgroup = self.get_security_group(secgroup_name_or_id) - if not secgroup: - raise exceptions.SDKException( - "Security group %s not found." % secgroup_name_or_id - ) - - if self._use_neutron_secgroups(): - # NOTE: Nova accepts -1 port numbers, but Neutron accepts None - # as the equivalent value. - rule_def = { - 'security_group_id': secgroup['id'], - 'port_range_min': ( - None if port_range_min == -1 else port_range_min - ), - 'port_range_max': ( - None if port_range_max == -1 else port_range_max - ), - 'protocol': protocol, - 'remote_ip_prefix': remote_ip_prefix, - 'remote_group_id': remote_group_id, - 'remote_address_group_id': remote_address_group_id, - 'direction': direction, - 'ethertype': ethertype, - } - if project_id is not None: - rule_def['tenant_id'] = project_id - if description is not None: - rule_def["description"] = description - return self.network.create_security_group_rule(**rule_def) - else: - # NOTE: Neutron accepts None for protocol. Nova does not. - if protocol is None: - raise exceptions.SDKException('Protocol must be specified') - - if direction == 'egress': - self.log.debug( - 'Rule creation failed: Nova does not support egress rules' - ) - raise exceptions.SDKException('No support for egress rules') - - # NOTE: Neutron accepts None for ports, but Nova requires -1 - # as the equivalent value for ICMP. - # - # For TCP/UDP, if both are None, Neutron allows this and Nova - # represents this as all ports (1-65535). Nova does not accept - # None values, so to hide this difference, we will automatically - # convert to the full port range. If only a single port value is - # specified, it will error as normal. - if protocol == 'icmp': - if port_range_min is None: - port_range_min = -1 - if port_range_max is None: - port_range_max = -1 - elif protocol in ['tcp', 'udp']: - if port_range_min is None and port_range_max is None: - port_range_min = 1 - port_range_max = 65535 - - security_group_rule_dict = dict( - security_group_rule=dict( - parent_group_id=secgroup['id'], - ip_protocol=protocol, - from_port=port_range_min, - to_port=port_range_max, - cidr=remote_ip_prefix, - group_id=remote_group_id, - ) - ) - if project_id is not None: - security_group_rule_dict['security_group_rule'][ - 'tenant_id' - ] = project_id - data = proxy._json_response( - self.compute.post( - '/os-security-group-rules', json=security_group_rule_dict - ) - ) - return self._normalize_secgroup_rule( - self._get_and_munchify('security_group_rule', data) - ) - - def delete_security_group_rule(self, rule_id): - """Delete a security group rule - - :param string rule_id: The unique ID of the security group rule. - - :returns: True if delete succeeded, False otherwise. - - :raises: :class:`~openstack.exceptions.SDKException` on operation - error. - :raises: OpenStackCloudUnavailableFeature if security groups are - not supported on this cloud. - """ - # Security groups not supported - if not self._has_secgroups(): - raise exc.OpenStackCloudUnavailableFeature( - "Unavailable feature: security groups" - ) - - if self._use_neutron_secgroups(): - self.network.delete_security_group_rule( - rule_id, ignore_missing=False - ) - return True - - else: - try: - exceptions.raise_from_response( - self.compute.delete(f'/os-security-group-rules/{rule_id}') - ) - except exceptions.NotFoundException: - return False - - return True - - def _has_secgroups(self): - if not self.secgroup_source: - return False - else: - return self.secgroup_source.lower() in ('nova', 'neutron') - - def _use_neutron_secgroups(self): - return ( - self.has_service('network') and self.secgroup_source == 'neutron' - ) - - def _normalize_secgroups(self, groups): - """Normalize the structure of security groups - - This makes security group dicts, as returned from nova, look like the - security group dicts as returned from neutron. This does not make them - look exactly the same, but it's pretty close. - - :param list groups: A list of security group dicts. - - :returns: A list of normalized dicts. - """ - ret = [] - for group in groups: - ret.append(self._normalize_secgroup(group)) - return ret - - # TODO(stephenfin): Remove this once we get rid of support for nova - # secgroups - def _normalize_secgroup(self, group): - ret = utils.Munch() - # Copy incoming group because of shared dicts in unittests - group = group.copy() - - # Discard noise - self._remove_novaclient_artifacts(group) - - rules = self._normalize_secgroup_rules( - group.pop('security_group_rules', group.pop('rules', [])) - ) - project_id = group.pop('tenant_id', '') - project_id = group.pop('project_id', project_id) - - ret['location'] = self._get_current_location(project_id=project_id) - ret['id'] = group.pop('id') - ret['name'] = group.pop('name') - ret['security_group_rules'] = rules - ret['description'] = group.pop('description') - ret['properties'] = group - - if self._use_neutron_secgroups(): - ret['stateful'] = group.pop('stateful', True) - - # Backwards compat with Neutron - if not self.strict_mode: - ret['tenant_id'] = project_id - ret['project_id'] = project_id - for key, val in ret['properties'].items(): - ret.setdefault(key, val) - - return ret - - # TODO(stephenfin): Remove this once we get rid of support for nova - # secgroups - def _normalize_secgroup_rules(self, rules): - """Normalize the structure of nova security group rules - - Note that nova uses -1 for non-specific port values, but neutron - represents these with None. - - :param list rules: A list of security group rule dicts. - - :returns: A list of normalized dicts. - """ - ret = [] - for rule in rules: - ret.append(self._normalize_secgroup_rule(rule)) - return ret - - # TODO(stephenfin): Remove this once we get rid of support for nova - # secgroups - def _normalize_secgroup_rule(self, rule): - ret = utils.Munch() - # Copy incoming rule because of shared dicts in unittests - rule = rule.copy() - - ret['id'] = rule.pop('id') - ret['direction'] = rule.pop('direction', 'ingress') - ret['ethertype'] = rule.pop('ethertype', 'IPv4') - port_range_min = rule.get( - 'port_range_min', rule.pop('from_port', None) - ) - if port_range_min == -1: - port_range_min = None - if port_range_min is not None: - port_range_min = int(port_range_min) - ret['port_range_min'] = port_range_min - port_range_max = rule.pop('port_range_max', rule.pop('to_port', None)) - if port_range_max == -1: - port_range_max = None - if port_range_min is not None: - port_range_min = int(port_range_min) - ret['port_range_max'] = port_range_max - ret['protocol'] = rule.pop('protocol', rule.pop('ip_protocol', None)) - ret['remote_ip_prefix'] = rule.pop( - 'remote_ip_prefix', rule.pop('ip_range', {}).get('cidr', None) - ) - ret['security_group_id'] = rule.pop( - 'security_group_id', rule.pop('parent_group_id', None) - ) - ret['remote_group_id'] = rule.pop('remote_group_id', None) - project_id = rule.pop('tenant_id', '') - project_id = rule.pop('project_id', project_id) - ret['location'] = self._get_current_location(project_id=project_id) - ret['properties'] = rule - - # Backwards compat with Neutron - if not self.strict_mode: - ret['tenant_id'] = project_id - ret['project_id'] = project_id - for key, val in ret['properties'].items(): - ret.setdefault(key, val) - return ret - - def _remove_novaclient_artifacts(self, item): - # Remove novaclient artifacts - item.pop('links', None) - item.pop('NAME_ATTR', None) - item.pop('HUMAN_ID', None) - item.pop('human_id', None) - item.pop('request_ids', None) - item.pop('x_openstack_request_ids', None) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index a2e0258cd..e54f22b3e 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -531,8 +531,8 @@ def get_session_endpoint(self, service_key, **kwargs): raise except Exception as e: raise exceptions.SDKException( - "Error getting {service} endpoint on {cloud}:{region}:" - " {error}".format( + "Error getting {service} endpoint on {cloud}:{region}: " + "{error}".format( service=service_key, cloud=self.name, region=self.config.get_region_name(service_key), diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 37af81395..063938ef9 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -2420,7 +2420,7 @@ def get_quota_class_set(self, quota_class_set='default'): :returns: One :class:`~openstack.compute.v2.quota_class_set.QuotaClassSet` - :raises: :class:`~openstack.exceptions.ResourceNotFound` + :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ return self._get(_quota_class_set.QuotaClassSet, quota_class_set) diff --git a/openstack/connection.py b/openstack/connection.py index 11e4745ed..9b4066eea 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -285,14 +285,11 @@ from openstack.cloud import _coe from openstack.cloud import _compute from openstack.cloud import _dns -from openstack.cloud import _floating_ip from openstack.cloud import _identity from openstack.cloud import _image from openstack.cloud import _network -from openstack.cloud import _network_common from openstack.cloud import _object_store from openstack.cloud import _orchestration -from openstack.cloud import _security_group from openstack.cloud import _shared_file_system from openstack import config as _config import openstack.config.cloud_region @@ -349,14 +346,11 @@ class Connection( _compute.ComputeCloudMixin, _coe.CoeCloudMixin, _dns.DnsCloudMixin, - _floating_ip.FloatingIPCloudMixin, _identity.IdentityCloudMixin, _image.ImageCloudMixin, _network.NetworkCloudMixin, - _network_common.NetworkCommonCloudMixin, _object_store.ObjectStoreCloudMixin, _orchestration.OrchestrationCloudMixin, - _security_group.SecurityGroupCloudMixin, _shared_file_system.SharedFileSystemCloudMixin, ): def __init__( @@ -423,32 +417,31 @@ def __init__( keys as service-type and values as floats expressing the calls per second for that service. Defaults to None, which means no rate-limiting is performed. - :param oslo_conf: An oslo.config CONF object. - :type oslo_conf: :class:`~oslo_config.cfg.ConfigOpts` - An oslo.config ``CONF`` object that has been populated with + :param oslo_conf: An oslo.config ``CONF`` object that has been + populated with ``keystoneauth1.loading.register_adapter_conf_options`` in groups named by the OpenStack service's project name. + :type oslo_conf: :class:`~oslo_config.cfg.ConfigOpts` :param service_types: A list/set of service types this Connection should support. All other service types will be disabled (will error if used). **Currently only supported in conjunction with the ``oslo_conf`` kwarg.** - :param global_request_id: A Request-id to send with all interactions. :param strict_proxies: - If True, check proxies on creation and raise - ServiceDiscoveryException if the service is unavailable. - :type strict_proxies: bool Throw an ``openstack.exceptions.ServiceDiscoveryException`` if the endpoint for a given service doesn't work. This is useful for OpenStack services using sdk to talk to other OpenStack services where it can be expected that the deployer config is correct and errors should be reported immediately. Default false. + :type strict_proxies: bool + :param global_request_id: A Request-id to send with all interactions. + :type global_request_id: str :param pool_executor: - :type pool_executor: :class:`~futurist.Executor` A futurist ``Executor`` object to be used for concurrent background activities. Defaults to None in which case a ThreadPoolExecutor will be created if needed. + :type pool_executor: :class:`~futurist.Executor` :param kwargs: If a config is not provided, the rest of the parameters provided are assumed to be arguments to be passed to the CloudRegion constructor. From 5704b3ef767d7be7d592c9dd62a4d4c63c9cac42 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 13 Jun 2024 14:04:30 +0100 Subject: [PATCH 3533/3836] mypy: Enable checks for openstack.cloud Change-Id: Ie2b223463cd8388ef2fe1410462a33755202cd58 Signed-off-by: Stephen Finucane --- setup.cfg | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index f6496594e..8bb9e2f16 100644 --- a/setup.cfg +++ b/setup.cfg @@ -48,9 +48,6 @@ exclude = (?x)( | releasenotes ) -[mypy-openstack.cloud.*] -ignore_errors = true - [mypy-openstack.tests.functional.*] ignore_errors = true From c4b9a44a1e7ce6c28e399e981456d6b5dab0f58a Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 1 Jul 2024 15:59:35 +0100 Subject: [PATCH 3534/3836] tests: Remove TestClustering functional tests We remove the mixin that would have added these methods recently, in change Iee56f6841355bebe1bc21a05bd5ce594f2d7b4cc, meaning these tests (which are currently skipped) cannot pass now. Change-Id: I6b74ad9cd8f32793d24c89caec5b08347d7d563f Signed-off-by: Stephen Finucane --- .../tests/functional/cloud/test_clustering.py | 1449 ----------------- 1 file changed, 1449 deletions(-) delete mode 100644 openstack/tests/functional/cloud/test_clustering.py diff --git a/openstack/tests/functional/cloud/test_clustering.py b/openstack/tests/functional/cloud/test_clustering.py deleted file mode 100644 index c609fa7ac..000000000 --- a/openstack/tests/functional/cloud/test_clustering.py +++ /dev/null @@ -1,1449 +0,0 @@ -# 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_clustering ----------------------------------- - -Functional tests for clustering methods. -""" - -import time - -from testtools import content - -from openstack.tests.functional import base - - -def wait_for_status( - client, client_args, field, value, check_interval=1, timeout=60 -): - """Wait for an OpenStack resource to enter a specified state - - :param client: An uncalled client resource to be called with resource_args - :param client_args: Arguments to be passed to client - :param field: Dictionary field to check - :param value: Dictionary value to look for - :param check_interval: Interval between checks - :param timeout: Time in seconds to wait for status to update. - :returns: True if correct status was reached - :raises: TimeoutException - """ - resource_status = client(**client_args)[field] - start = time.time() - - while resource_status != value: - time.sleep(check_interval) - resource = client(**client_args) - resource_status = resource[field] - - timed_out = time.time() - start >= timeout - - if resource_status != value and timed_out: - return False - return True - - -def wait_for_create(client, client_args, check_interval=1, timeout=60): - """Wait for an OpenStack resource to be created - - :param client: An uncalled client resource to be called with resource_args - :param client_args: Arguments to be passed to client - :param name: Name of the resource (for logging) - :param check_interval: Interval between checks - :param timeout: Time in seconds to wait for status to update. - :returns: True if openstack.exceptions.NotFoundException is caught - :raises: TimeoutException - - """ - - resource = client(**client_args) - start = time.time() - - while not resource: - time.sleep(check_interval) - resource = client(**client_args) - - timed_out = time.time() - start >= timeout - - if (not resource) and timed_out: - return False - return True - - -def wait_for_delete(client, client_args, check_interval=1, timeout=60): - """Wait for an OpenStack resource to 404/delete - - :param client: An uncalled client resource to be called with resource_args - :param client_args: Arguments to be passed to client - :param name: Name of the resource (for logging) - :param check_interval: Interval between checks - :param timeout: Time in seconds to wait for status to update. - :returns: True if openstack.exceptions.NotFoundException is caught - :raises: TimeoutException - - """ - resource = client(**client_args) - start = time.time() - - while resource: - time.sleep(check_interval) - resource = client(**client_args) - - timed_out = time.time() - start >= timeout - - if resource and timed_out: - return False - return True - - -class TestClustering(base.BaseFunctionalTest): - def setUp(self): - super().setUp() - self.skipTest('clustering service not supported by cloud') - - def test_create_profile(self): - profile_name = "test_profile" - spec = { - "properties": { - "flavor": self.flavor.name, - "image": self.image.name, - "networks": [{"network": "private"}], - "security_groups": ["default"], - }, - "type": "os.nova.server", - "version": 1.0, - } - - self.addDetail('profile', content.text_content(profile_name)) - # Test that we can create a profile and we get it returned - - profile = self.user_cloud.create_cluster_profile( - name=profile_name, spec=spec - ) - - self.addCleanup(self.cleanup_profile, profile['id']) - - self.assertEqual(profile['name'], profile_name) - self.assertEqual(profile['spec'], spec) - - def test_create_cluster(self): - profile_name = "test_profile" - spec = { - "properties": { - "flavor": self.flavor.name, - "image": self.image.name, - "networks": [{"network": "private"}], - "security_groups": ["default"], - }, - "type": "os.nova.server", - "version": 1.0, - } - - self.addDetail('profile', content.text_content(profile_name)) - # Test that we can create a profile and we get it returned - - profile = self.user_cloud.create_cluster_profile( - name=profile_name, spec=spec - ) - - self.addCleanup(self.cleanup_profile, profile['id']) - - cluster_name = 'example_cluster' - desired_capacity = 0 - - self.addDetail('cluster', content.text_content(cluster_name)) - - # Test that we can create a cluster and we get it returned - cluster = self.user_cloud.create_cluster( - name=cluster_name, - profile=profile, - desired_capacity=desired_capacity, - ) - - self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) - - self.assertEqual(cluster['cluster']['name'], cluster_name) - self.assertEqual(cluster['cluster']['profile_id'], profile['id']) - self.assertEqual( - cluster['cluster']['desired_capacity'], desired_capacity - ) - - def test_get_cluster_by_id(self): - profile_name = "test_profile" - spec = { - "properties": { - "flavor": self.flavor.name, - "image": self.image.name, - "networks": [{"network": "private"}], - "security_groups": ["default"], - }, - "type": "os.nova.server", - "version": 1.0, - } - - self.addDetail('profile', content.text_content(profile_name)) - # Test that we can create a profile and we get it returned - - profile = self.user_cloud.create_cluster_profile( - name=profile_name, spec=spec - ) - - self.addCleanup(self.cleanup_profile, profile['id']) - cluster_name = 'example_cluster' - desired_capacity = 0 - - self.addDetail('cluster', content.text_content(cluster_name)) - - # Test that we can create a cluster and we get it returned - cluster = self.user_cloud.create_cluster( - name=cluster_name, - profile=profile, - desired_capacity=desired_capacity, - ) - - self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) - - # Test that we get the same cluster with the get_cluster method - cluster_get = self.user_cloud.get_cluster_by_id( - cluster['cluster']['id'] - ) - self.assertEqual(cluster_get['id'], cluster['cluster']['id']) - - def test_update_cluster(self): - profile_name = "test_profile" - spec = { - "properties": { - "flavor": self.flavor.name, - "image": self.image.name, - "networks": [{"network": "private"}], - "security_groups": ["default"], - }, - "type": "os.nova.server", - "version": 1.0, - } - - self.addDetail('profile', content.text_content(profile_name)) - # Test that we can create a profile and we get it returned - - profile = self.user_cloud.create_cluster_profile( - name=profile_name, spec=spec - ) - - self.addCleanup(self.cleanup_profile, profile['id']) - - cluster_name = 'example_cluster' - desired_capacity = 0 - - self.addDetail('cluster', content.text_content(cluster_name)) - - # Test that we can create a cluster and we get it returned - cluster = self.user_cloud.create_cluster( - name=cluster_name, - profile=profile, - desired_capacity=desired_capacity, - ) - - self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) - - # Test that we can update a field on the cluster and only that field - # is updated - - self.user_cloud.update_cluster( - cluster['cluster']['id'], new_name='new_cluster_name' - ) - - wait = wait_for_status( - self.user_cloud.get_cluster_by_id, - {'name_or_id': cluster['cluster']['id']}, - 'status', - 'ACTIVE', - ) - - self.assertTrue(wait) - cluster_update = self.user_cloud.get_cluster_by_id( - cluster['cluster']['id'] - ) - self.assertEqual(cluster_update['id'], cluster['cluster']['id']) - self.assertEqual(cluster_update['name'], 'new_cluster_name') - self.assertEqual( - cluster_update['profile_id'], cluster['cluster']['profile_id'] - ) - self.assertEqual( - cluster_update['desired_capacity'], - cluster['cluster']['desired_capacity'], - ) - - def test_create_cluster_policy(self): - policy_name = 'example_policy' - spec = { - "properties": { - "adjustment": { - "min_step": 1, - "number": 1, - "type": "CHANGE_IN_CAPACITY", - }, - "event": "CLUSTER_SCALE_IN", - }, - "type": "senlin.policy.scaling", - "version": "1.0", - } - - self.addDetail('policy', content.text_content(policy_name)) - - # Test that we can create a policy and we get it returned - - policy = self.user_cloud.create_cluster_policy( - name=policy_name, spec=spec - ) - - self.addCleanup(self.cleanup_policy, policy['id']) - - self.assertEqual(policy['name'], policy_name) - self.assertEqual(policy['spec'], spec) - - def test_attach_policy_to_cluster(self): - profile_name = "test_profile" - spec = { - "properties": { - "flavor": self.flavor.name, - "image": self.image.name, - "networks": [{"network": "private"}], - "security_groups": ["default"], - }, - "type": "os.nova.server", - "version": 1.0, - } - - self.addDetail('profile', content.text_content(profile_name)) - # Test that we can create a profile and we get it returned - - profile = self.user_cloud.create_cluster_profile( - name=profile_name, spec=spec - ) - - self.addCleanup(self.cleanup_profile, profile['id']) - - cluster_name = 'example_cluster' - desired_capacity = 0 - - self.addDetail('cluster', content.text_content(cluster_name)) - - # Test that we can create a cluster and we get it returned - cluster = self.user_cloud.create_cluster( - name=cluster_name, - profile=profile, - desired_capacity=desired_capacity, - ) - - self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) - - policy_name = 'example_policy' - spec = { - "properties": { - "adjustment": { - "min_step": 1, - "number": 1, - "type": "CHANGE_IN_CAPACITY", - }, - "event": "CLUSTER_SCALE_IN", - }, - "type": "senlin.policy.scaling", - "version": "1.0", - } - - self.addDetail('policy', content.text_content(policy_name)) - - # Test that we can create a policy and we get it returned - - policy = self.user_cloud.create_cluster_policy( - name=policy_name, spec=spec - ) - - self.addCleanup( - self.cleanup_policy, policy['id'], cluster['cluster']['id'] - ) - - # Test that we can attach policy to cluster and get True returned - - attach_cluster = self.user_cloud.get_cluster_by_id( - cluster['cluster']['id'] - ) - attach_policy = self.user_cloud.get_cluster_policy_by_id(policy['id']) - - policy_attach = self.user_cloud.attach_policy_to_cluster( - attach_cluster, attach_policy, is_enabled=True - ) - self.assertTrue(policy_attach) - - def test_detach_policy_from_cluster(self): - profile_name = "test_profile" - spec = { - "properties": { - "flavor": self.flavor.name, - "image": self.image.name, - "networks": [{"network": "private"}], - "security_groups": ["default"], - }, - "type": "os.nova.server", - "version": 1.0, - } - - self.addDetail('profile', content.text_content(profile_name)) - # Test that we can create a profile and we get it returned - - profile = self.user_cloud.create_cluster_profile( - name=profile_name, spec=spec - ) - - self.addCleanup(self.cleanup_profile, profile['id']) - - cluster_name = 'example_cluster' - desired_capacity = 0 - - self.addDetail('cluster', content.text_content(cluster_name)) - - # Test that we can create a cluster and we get it returned - cluster = self.user_cloud.create_cluster( - name=cluster_name, - profile=profile, - desired_capacity=desired_capacity, - ) - - self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) - - policy_name = 'example_policy' - spec = { - "properties": { - "adjustment": { - "min_step": 1, - "number": 1, - "type": "CHANGE_IN_CAPACITY", - }, - "event": "CLUSTER_SCALE_IN", - }, - "type": "senlin.policy.scaling", - "version": "1.0", - } - - self.addDetail('policy', content.text_content(policy_name)) - - # Test that we can create a policy and we get it returned - - policy = self.user_cloud.create_cluster_policy( - name=policy_name, spec=spec - ) - - self.addCleanup( - self.cleanup_policy, policy['id'], cluster['cluster']['id'] - ) - - attach_cluster = self.user_cloud.get_cluster_by_id( - cluster['cluster']['id'] - ) - attach_policy = self.user_cloud.get_cluster_policy_by_id(policy['id']) - - self.user_cloud.attach_policy_to_cluster( - attach_cluster, attach_policy, is_enabled=True - ) - - wait = wait_for_status( - self.user_cloud.get_cluster_by_id, - {'name_or_id': cluster['cluster']['id']}, - 'policies', - ['{policy}'.format(policy=policy['id'])], - ) - - policy_detach = self.user_cloud.detach_policy_from_cluster( - attach_cluster, attach_policy - ) - - self.assertTrue(policy_detach) - self.assertTrue(wait) - - def test_get_policy_on_cluster_by_id(self): - profile_name = "test_profile" - spec = { - "properties": { - "flavor": self.flavor.name, - "image": self.image.name, - "networks": [{"network": "private"}], - "security_groups": ["default"], - }, - "type": "os.nova.server", - "version": 1.0, - } - - self.addDetail('profile', content.text_content(profile_name)) - # Test that we can create a profile and we get it returned - - profile = self.user_cloud.create_cluster_profile( - name=profile_name, spec=spec - ) - - self.addCleanup(self.cleanup_profile, profile['id']) - - cluster_name = 'example_cluster' - desired_capacity = 0 - - self.addDetail('cluster', content.text_content(cluster_name)) - - # Test that we can create a cluster and we get it returned - cluster = self.user_cloud.create_cluster( - name=cluster_name, - profile=profile, - desired_capacity=desired_capacity, - ) - - self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) - - policy_name = 'example_policy' - spec = { - "properties": { - "adjustment": { - "min_step": 1, - "number": 1, - "type": "CHANGE_IN_CAPACITY", - }, - "event": "CLUSTER_SCALE_IN", - }, - "type": "senlin.policy.scaling", - "version": "1.0", - } - - self.addDetail('policy', content.text_content(policy_name)) - - # Test that we can create a policy and we get it returned - - policy = self.user_cloud.create_cluster_policy( - name=policy_name, spec=spec - ) - - self.addCleanup( - self.cleanup_policy, policy['id'], cluster['cluster']['id'] - ) - - # Test that we can attach policy to cluster and get True returned - - attach_cluster = self.user_cloud.get_cluster_by_id( - cluster['cluster']['id'] - ) - attach_policy = self.user_cloud.get_cluster_policy_by_id(policy['id']) - - policy_attach = self.user_cloud.attach_policy_to_cluster( - attach_cluster, attach_policy, is_enabled=True - ) - self.assertTrue(policy_attach) - - wait = wait_for_status( - self.user_cloud.get_cluster_by_id, - {'name_or_id': cluster['cluster']['id']}, - 'policies', - ["{policy}".format(policy=policy['id'])], - ) - - # Test that we get the same policy with the get_policy_on_cluster - # method - - cluster_policy_get = self.user_cloud.get_policy_on_cluster( - cluster['cluster']["id"], policy['id'] - ) - - self.assertEqual( - cluster_policy_get['cluster_id'], cluster['cluster']["id"] - ) - self.assertEqual( - cluster_policy_get['cluster_name'], cluster['cluster']["name"] - ) - self.assertEqual(cluster_policy_get['policy_id'], policy['id']), - self.assertEqual(cluster_policy_get['policy_name'], policy['name']) - self.assertTrue(wait) - - def test_list_policies_on_cluster(self): - profile_name = "test_profile" - spec = { - "properties": { - "flavor": self.flavor.name, - "image": self.image.name, - "networks": [{"network": "private"}], - "security_groups": ["default"], - }, - "type": "os.nova.server", - "version": 1.0, - } - - self.addDetail('profile', content.text_content(profile_name)) - # Test that we can create a profile and we get it returned - - profile = self.user_cloud.create_cluster_profile( - name=profile_name, spec=spec - ) - - self.addCleanup(self.cleanup_profile, profile['id']) - - cluster_name = 'example_cluster' - desired_capacity = 0 - - self.addDetail('cluster', content.text_content(cluster_name)) - - # Test that we can create a cluster and we get it returned - cluster = self.user_cloud.create_cluster( - name=cluster_name, - profile=profile, - desired_capacity=desired_capacity, - ) - - self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) - - policy_name = 'example_policy' - spec = { - "properties": { - "adjustment": { - "min_step": 1, - "number": 1, - "type": "CHANGE_IN_CAPACITY", - }, - "event": "CLUSTER_SCALE_IN", - }, - "type": "senlin.policy.scaling", - "version": "1.0", - } - - self.addDetail('policy', content.text_content(policy_name)) - - # Test that we can create a policy and we get it returned - - policy = self.user_cloud.create_cluster_policy( - name=policy_name, spec=spec - ) - - self.addCleanup( - self.cleanup_policy, policy['id'], cluster['cluster']['id'] - ) - - attach_cluster = self.user_cloud.get_cluster_by_id( - cluster['cluster']['id'] - ) - attach_policy = self.user_cloud.get_cluster_policy_by_id(policy['id']) - - self.user_cloud.attach_policy_to_cluster( - attach_cluster, attach_policy, is_enabled=True - ) - - wait = wait_for_status( - self.user_cloud.get_cluster_by_id, - {'name_or_id': cluster['cluster']['id']}, - 'policies', - ["{policy}".format(policy=policy['id'])], - ) - - cluster_policy = self.user_cloud.get_policy_on_cluster( - name_or_id=cluster['cluster']['id'], policy_name_or_id=policy['id'] - ) - - policy_list = {"cluster_policies": [cluster_policy]} - - # Test that we can list the policies on a cluster - cluster_policies = self.user_cloud.list_policies_on_cluster( - cluster['cluster']["id"] - ) - self.assertEqual(cluster_policies, policy_list) - self.assertTrue(wait) - - def test_create_cluster_receiver(self): - profile_name = "test_profile" - spec = { - "properties": { - "flavor": self.flavor.name, - "image": self.image.name, - "networks": [{"network": "private"}], - "security_groups": ["default"], - }, - "type": "os.nova.server", - "version": 1.0, - } - - self.addDetail('profile', content.text_content(profile_name)) - # Test that we can create a profile and we get it returned - - profile = self.user_cloud.create_cluster_profile( - name=profile_name, spec=spec - ) - - self.addCleanup(self.cleanup_profile, profile['id']) - - cluster_name = 'example_cluster' - desired_capacity = 0 - - self.addDetail('cluster', content.text_content(cluster_name)) - - # Test that we can create a cluster and we get it returned - cluster = self.user_cloud.create_cluster( - name=cluster_name, - profile=profile, - desired_capacity=desired_capacity, - ) - - self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) - - receiver_name = "example_receiver" - receiver_type = "webhook" - - self.addDetail('receiver', content.text_content(receiver_name)) - - # Test that we can create a receiver and we get it returned - - receiver = self.user_cloud.create_cluster_receiver( - name=receiver_name, - receiver_type=receiver_type, - cluster_name_or_id=cluster['cluster']['id'], - action='CLUSTER_SCALE_OUT', - ) - - self.addCleanup(self.cleanup_receiver, receiver['id']) - - self.assertEqual(receiver['name'], receiver_name) - self.assertEqual(receiver['type'], receiver_type) - self.assertEqual(receiver['cluster_id'], cluster['cluster']["id"]) - - def test_list_cluster_receivers(self): - profile_name = "test_profile" - spec = { - "properties": { - "flavor": self.flavor.name, - "image": self.image.name, - "networks": [{"network": "private"}], - "security_groups": ["default"], - }, - "type": "os.nova.server", - "version": 1.0, - } - - self.addDetail('profile', content.text_content(profile_name)) - # Test that we can create a profile and we get it returned - - profile = self.user_cloud.create_cluster_profile( - name=profile_name, spec=spec - ) - - self.addCleanup(self.cleanup_profile, profile['id']) - - cluster_name = 'example_cluster' - desired_capacity = 0 - - self.addDetail('cluster', content.text_content(cluster_name)) - - # Test that we can create a cluster and we get it returned - cluster = self.user_cloud.create_cluster( - name=cluster_name, - profile=profile, - desired_capacity=desired_capacity, - ) - - self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) - - receiver_name = "example_receiver" - receiver_type = "webhook" - - self.addDetail('receiver', content.text_content(receiver_name)) - - # Test that we can create a receiver and we get it returned - - receiver = self.user_cloud.create_cluster_receiver( - name=receiver_name, - receiver_type=receiver_type, - cluster_name_or_id=cluster['cluster']['id'], - action='CLUSTER_SCALE_OUT', - ) - - self.addCleanup(self.cleanup_receiver, receiver['id']) - - get_receiver = self.user_cloud.get_cluster_receiver_by_id( - receiver['id'] - ) - receiver_list = {"receivers": [get_receiver]} - - # Test that we can list receivers - - receivers = self.user_cloud.list_cluster_receivers() - self.assertEqual(receivers, receiver_list) - - def test_delete_cluster(self): - profile_name = "test_profile" - spec = { - "properties": { - "flavor": self.flavor.name, - "image": self.image.name, - "networks": [{"network": "private"}], - "security_groups": ["default"], - }, - "type": "os.nova.server", - "version": 1.0, - } - - self.addDetail('profile', content.text_content(profile_name)) - # Test that we can create a profile and we get it returned - - profile = self.user_cloud.create_cluster_profile( - name=profile_name, spec=spec - ) - - self.addCleanup(self.cleanup_profile, profile['id']) - - cluster_name = 'example_cluster' - desired_capacity = 0 - - self.addDetail('cluster', content.text_content(cluster_name)) - - # Test that we can create a cluster and we get it returned - cluster = self.user_cloud.create_cluster( - name=cluster_name, - profile=profile, - desired_capacity=desired_capacity, - ) - - self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) - - policy_name = 'example_policy' - spec = { - "properties": { - "adjustment": { - "min_step": 1, - "number": 1, - "type": "CHANGE_IN_CAPACITY", - }, - "event": "CLUSTER_SCALE_IN", - }, - "type": "senlin.policy.scaling", - "version": "1.0", - } - - self.addDetail('policy', content.text_content(policy_name)) - - # Test that we can create a policy and we get it returned - - policy = self.user_cloud.create_cluster_policy( - name=policy_name, spec=spec - ) - - self.addCleanup(self.cleanup_policy, policy['id']) - - # Test that we can attach policy to cluster and get True returned - attach_cluster = self.user_cloud.get_cluster_by_id( - cluster['cluster']['id'] - ) - attach_policy = self.user_cloud.get_cluster_policy_by_id(policy['id']) - - self.user_cloud.attach_policy_to_cluster( - attach_cluster, attach_policy, is_enabled=True - ) - - receiver_name = "example_receiver" - receiver_type = "webhook" - - self.addDetail('receiver', content.text_content(receiver_name)) - - # Test that we can create a receiver and we get it returned - - self.user_cloud.create_cluster_receiver( - name=receiver_name, - receiver_type=receiver_type, - cluster_name_or_id=cluster['cluster']['id'], - action='CLUSTER_SCALE_OUT', - ) - - # Test that we can delete cluster and get True returned - cluster_delete = self.user_cloud.delete_cluster( - cluster['cluster']['id'] - ) - self.assertTrue(cluster_delete) - - def test_list_clusters(self): - profile_name = "test_profile" - spec = { - "properties": { - "flavor": self.flavor.name, - "image": self.image.name, - "networks": [{"network": "private"}], - "security_groups": ["default"], - }, - "type": "os.nova.server", - "version": 1.0, - } - - self.addDetail('profile', content.text_content(profile_name)) - # Test that we can create a profile and we get it returned - - profile = self.user_cloud.create_cluster_profile( - name=profile_name, spec=spec - ) - - self.addCleanup(self.cleanup_profile, profile['id']) - - cluster_name = 'example_cluster' - desired_capacity = 0 - - self.addDetail('cluster', content.text_content(cluster_name)) - - # Test that we can create a cluster and we get it returned - cluster = self.user_cloud.create_cluster( - name=cluster_name, - profile=profile, - desired_capacity=desired_capacity, - ) - - self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) - - wait = wait_for_status( - self.user_cloud.get_cluster_by_id, - {'name_or_id': cluster['cluster']['id']}, - 'status', - 'ACTIVE', - ) - - get_cluster = self.user_cloud.get_cluster_by_id( - cluster['cluster']['id'] - ) - - # Test that we can list clusters - clusters = self.user_cloud.list_clusters() - self.assertEqual(clusters, [get_cluster]) - self.assertTrue(wait) - - def test_update_policy_on_cluster(self): - profile_name = "test_profile" - spec = { - "properties": { - "flavor": self.flavor.name, - "image": self.image.name, - "networks": [{"network": "private"}], - "security_groups": ["default"], - }, - "type": "os.nova.server", - "version": 1.0, - } - - self.addDetail('profile', content.text_content(profile_name)) - # Test that we can create a profile and we get it returned - - profile = self.user_cloud.create_cluster_profile( - name=profile_name, spec=spec - ) - - self.addCleanup(self.cleanup_profile, profile['id']) - - cluster_name = 'example_cluster' - desired_capacity = 0 - - self.addDetail('cluster', content.text_content(cluster_name)) - - # Test that we can create a cluster and we get it returned - cluster = self.user_cloud.create_cluster( - name=cluster_name, - profile=profile, - desired_capacity=desired_capacity, - ) - - self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) - - policy_name = 'example_policy' - spec = { - "properties": { - "adjustment": { - "min_step": 1, - "number": 1, - "type": "CHANGE_IN_CAPACITY", - }, - "event": "CLUSTER_SCALE_IN", - }, - "type": "senlin.policy.scaling", - "version": "1.0", - } - - self.addDetail('policy', content.text_content(policy_name)) - - # Test that we can create a policy and we get it returned - - policy = self.user_cloud.create_cluster_policy( - name=policy_name, spec=spec - ) - - self.addCleanup( - self.cleanup_policy, policy['id'], cluster['cluster']['id'] - ) - - # Test that we can attach policy to cluster and get True returned - - attach_cluster = self.user_cloud.get_cluster_by_id( - cluster['cluster']['id'] - ) - attach_policy = self.user_cloud.get_cluster_policy_by_id(policy['id']) - - self.user_cloud.attach_policy_to_cluster( - attach_cluster, attach_policy, is_enabled=True - ) - - wait_attach = wait_for_status( - self.user_cloud.get_cluster_by_id, - {'name_or_id': cluster['cluster']['id']}, - 'policies', - ["{policy}".format(policy=policy['id'])], - ) - - get_old_policy = self.user_cloud.get_policy_on_cluster( - cluster['cluster']["id"], policy['id'] - ) - - # Test that we can update the policy on cluster - policy_update = self.user_cloud.update_policy_on_cluster( - attach_cluster, attach_policy, is_enabled=False - ) - - get_old_policy.update({'enabled': False}) - - wait_update = wait_for_status( - self.user_cloud.get_policy_on_cluster, - { - 'name_or_id': cluster['cluster']['id'], - 'policy_name_or_id': policy['id'], - }, - 'enabled', - False, - ) - - get_new_policy = self.user_cloud.get_policy_on_cluster( - cluster['cluster']["id"], policy['id'] - ) - - get_old_policy['last_op'] = None - get_new_policy['last_op'] = None - - self.assertTrue(policy_update) - self.assertEqual(get_new_policy, get_old_policy) - self.assertTrue(wait_attach) - self.assertTrue(wait_update) - - def test_list_cluster_profiles(self): - profile_name = "test_profile" - spec = { - "properties": { - "flavor": self.flavor.name, - "image": self.image.name, - "networks": [{"network": "private"}], - "security_groups": ["default"], - }, - "type": "os.nova.server", - "version": 1.0, - } - - self.addDetail('profile', content.text_content(profile_name)) - # Test that we can create a profile and we get it returned - - profile = self.user_cloud.create_cluster_profile( - name=profile_name, spec=spec - ) - - self.addCleanup(self.cleanup_profile, profile['id']) - - # Test that we can list profiles - - wait = wait_for_create( - self.user_cloud.get_cluster_profile_by_id, - {'name_or_id': profile['id']}, - ) - - get_profile = self.user_cloud.get_cluster_profile_by_id(profile['id']) - - profiles = self.user_cloud.list_cluster_profiles() - self.assertEqual(profiles, [get_profile]) - self.assertTrue(wait) - - def test_get_cluster_profile_by_id(self): - profile_name = "test_profile" - spec = { - "properties": { - "flavor": self.flavor.name, - "image": self.image.name, - "networks": [{"network": "private"}], - "security_groups": ["default"], - }, - "type": "os.nova.server", - "version": 1.0, - } - - self.addDetail('profile', content.text_content(profile_name)) - # Test that we can create a profile and we get it returned - - profile = self.user_cloud.create_cluster_profile( - name=profile_name, spec=spec - ) - - self.addCleanup(self.cleanup_profile, profile['id']) - - profile_get = self.user_cloud.get_cluster_profile_by_id(profile['id']) - - # Test that we get the same profile with the get_profile method - # Format of the created_at variable differs between policy create - # and policy get so if we don't ignore this variable, comparison will - # always fail - profile['created_at'] = 'ignore' - profile_get['created_at'] = 'ignore' - - self.assertEqual(profile_get, profile) - - def test_update_cluster_profile(self): - profile_name = "test_profile" - spec = { - "properties": { - "flavor": self.flavor.name, - "image": self.image.name, - "networks": [{"network": "private"}], - "security_groups": ["default"], - }, - "type": "os.nova.server", - "version": 1.0, - } - - self.addDetail('profile', content.text_content(profile_name)) - # Test that we can create a profile and we get it returned - - profile = self.user_cloud.create_cluster_profile( - name=profile_name, spec=spec - ) - - self.addCleanup(self.cleanup_profile, profile['id']) - - # Test that we can update a field on the profile and only that field - # is updated - - profile_update = self.user_cloud.update_cluster_profile( - profile['id'], new_name='new_profile_name' - ) - self.assertEqual(profile_update['profile']['id'], profile['id']) - self.assertEqual(profile_update['profile']['spec'], profile['spec']) - self.assertEqual(profile_update['profile']['name'], 'new_profile_name') - - def test_delete_cluster_profile(self): - profile_name = "test_profile" - spec = { - "properties": { - "flavor": self.flavor.name, - "image": self.image.name, - "networks": [{"network": "private"}], - "security_groups": ["default"], - }, - "type": "os.nova.server", - "version": 1.0, - } - - self.addDetail('profile', content.text_content(profile_name)) - # Test that we can create a profile and we get it returned - - profile = self.user_cloud.create_cluster_profile( - name=profile_name, spec=spec - ) - - self.addCleanup(self.cleanup_profile, profile['id']) - - # Test that we can delete a profile and get True returned - profile_delete = self.user_cloud.delete_cluster_profile(profile['id']) - self.assertTrue(profile_delete) - - def test_list_cluster_policies(self): - policy_name = 'example_policy' - spec = { - "properties": { - "adjustment": { - "min_step": 1, - "number": 1, - "type": "CHANGE_IN_CAPACITY", - }, - "event": "CLUSTER_SCALE_IN", - }, - "type": "senlin.policy.scaling", - "version": "1.0", - } - - self.addDetail('policy', content.text_content(policy_name)) - - # Test that we can create a policy and we get it returned - - policy = self.user_cloud.create_cluster_policy( - name=policy_name, spec=spec - ) - - self.addCleanup(self.cleanup_policy, policy['id']) - - policy_get = self.user_cloud.get_cluster_policy_by_id(policy['id']) - - # Test that we can list policies - - policies = self.user_cloud.list_cluster_policies() - - # Format of the created_at variable differs between policy create - # and policy get so if we don't ignore this variable, comparison will - # always fail - policies[0]['created_at'] = 'ignore' - policy_get['created_at'] = 'ignore' - - self.assertEqual(policies, [policy_get]) - - def test_get_cluster_policy_by_id(self): - policy_name = 'example_policy' - spec = { - "properties": { - "adjustment": { - "min_step": 1, - "number": 1, - "type": "CHANGE_IN_CAPACITY", - }, - "event": "CLUSTER_SCALE_IN", - }, - "type": "senlin.policy.scaling", - "version": "1.0", - } - - self.addDetail('policy', content.text_content(policy_name)) - - # Test that we can create a policy and we get it returned - - policy = self.user_cloud.create_cluster_policy( - name=policy_name, spec=spec - ) - - self.addCleanup(self.cleanup_policy, policy['id']) - - # Test that we get the same policy with the get_policy method - - policy_get = self.user_cloud.get_cluster_policy_by_id(policy['id']) - - # Format of the created_at variable differs between policy create - # and policy get so if we don't ignore this variable, comparison will - # always fail - policy['created_at'] = 'ignore' - policy_get['created_at'] = 'ignore' - - self.assertEqual(policy_get, policy) - - def test_update_cluster_policy(self): - policy_name = 'example_policy' - spec = { - "properties": { - "adjustment": { - "min_step": 1, - "number": 1, - "type": "CHANGE_IN_CAPACITY", - }, - "event": "CLUSTER_SCALE_IN", - }, - "type": "senlin.policy.scaling", - "version": "1.0", - } - - self.addDetail('policy', content.text_content(policy_name)) - - # Test that we can create a policy and we get it returned - - policy = self.user_cloud.create_cluster_policy( - name=policy_name, spec=spec - ) - - self.addCleanup(self.cleanup_policy, policy['id']) - - # Test that we can update a field on the policy and only that field - # is updated - - policy_update = self.user_cloud.update_cluster_policy( - policy['id'], new_name='new_policy_name' - ) - self.assertEqual(policy_update['policy']['id'], policy['id']) - self.assertEqual(policy_update['policy']['spec'], policy['spec']) - self.assertEqual(policy_update['policy']['name'], 'new_policy_name') - - def test_delete_cluster_policy(self): - policy_name = 'example_policy' - spec = { - "properties": { - "adjustment": { - "min_step": 1, - "number": 1, - "type": "CHANGE_IN_CAPACITY", - }, - "event": "CLUSTER_SCALE_IN", - }, - "type": "senlin.policy.scaling", - "version": "1.0", - } - - self.addDetail('policy', content.text_content(policy_name)) - - # Test that we can create a policy and we get it returned - - policy = self.user_cloud.create_cluster_policy( - name=policy_name, spec=spec - ) - - self.addCleanup(self.cleanup_policy, policy['id']) - - # Test that we can delete a policy and get True returned - policy_delete = self.user_cloud.delete_cluster_policy(policy['id']) - self.assertTrue(policy_delete) - - def test_get_cluster_receiver_by_id(self): - profile_name = "test_profile" - spec = { - "properties": { - "flavor": self.flavor.name, - "image": self.image.name, - "networks": [{"network": "private"}], - "security_groups": ["default"], - }, - "type": "os.nova.server", - "version": 1.0, - } - - self.addDetail('profile', content.text_content(profile_name)) - # Test that we can create a profile and we get it returned - - profile = self.user_cloud.create_cluster_profile( - name=profile_name, spec=spec - ) - - self.addCleanup(self.cleanup_profile, profile['id']) - - cluster_name = 'example_cluster' - desired_capacity = 0 - - self.addDetail('cluster', content.text_content(cluster_name)) - - # Test that we can create a cluster and we get it returned - cluster = self.user_cloud.create_cluster( - name=cluster_name, - profile=profile, - desired_capacity=desired_capacity, - ) - - self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) - - receiver_name = "example_receiver" - receiver_type = "webhook" - - self.addDetail('receiver', content.text_content(receiver_name)) - - # Test that we can create a receiver and we get it returned - - receiver = self.user_cloud.create_cluster_receiver( - name=receiver_name, - receiver_type=receiver_type, - cluster_name_or_id=cluster['cluster']['id'], - action='CLUSTER_SCALE_OUT', - ) - - self.addCleanup(self.cleanup_receiver, receiver['id']) - - # Test that we get the same receiver with the get_receiver method - - receiver_get = self.user_cloud.get_cluster_receiver_by_id( - receiver['id'] - ) - self.assertEqual(receiver_get['id'], receiver["id"]) - - def test_update_cluster_receiver(self): - profile_name = "test_profile" - spec = { - "properties": { - "flavor": self.flavor.name, - "image": self.image.name, - "networks": [{"network": "private"}], - "security_groups": ["default"], - }, - "type": "os.nova.server", - "version": 1.0, - } - - self.addDetail('profile', content.text_content(profile_name)) - # Test that we can create a profile and we get it returned - - profile = self.user_cloud.create_cluster_profile( - name=profile_name, spec=spec - ) - - self.addCleanup(self.cleanup_profile, profile['id']) - - cluster_name = 'example_cluster' - desired_capacity = 0 - - self.addDetail('cluster', content.text_content(cluster_name)) - - # Test that we can create a cluster and we get it returned - cluster = self.user_cloud.create_cluster( - name=cluster_name, - profile=profile, - desired_capacity=desired_capacity, - ) - - self.addCleanup(self.cleanup_cluster, cluster['cluster']['id']) - - receiver_name = "example_receiver" - receiver_type = "webhook" - - self.addDetail('receiver', content.text_content(receiver_name)) - - # Test that we can create a receiver and we get it returned - - receiver = self.user_cloud.create_cluster_receiver( - name=receiver_name, - receiver_type=receiver_type, - cluster_name_or_id=cluster['cluster']['id'], - action='CLUSTER_SCALE_OUT', - ) - - self.addCleanup(self.cleanup_receiver, receiver['id']) - - # Test that we can update a field on the receiver and only that field - # is updated - - receiver_update = self.user_cloud.update_cluster_receiver( - receiver['id'], new_name='new_receiver_name' - ) - self.assertEqual(receiver_update['receiver']['id'], receiver['id']) - self.assertEqual(receiver_update['receiver']['type'], receiver['type']) - self.assertEqual( - receiver_update['receiver']['cluster_id'], receiver['cluster_id'] - ) - self.assertEqual( - receiver_update['receiver']['name'], 'new_receiver_name' - ) - - def cleanup_profile(self, name): - time.sleep(5) - for cluster in self.user_cloud.list_clusters(): - if name == cluster["profile_id"]: - self.user_cloud.delete_cluster(cluster["id"]) - self.user_cloud.delete_cluster_profile(name) - - def cleanup_cluster(self, name): - self.user_cloud.delete_cluster(name) - - def cleanup_policy(self, name, cluster_name=None): - if cluster_name is not None: - cluster = self.user_cloud.get_cluster_by_id(cluster_name) - policy = self.user_cloud.get_cluster_policy_by_id(name) - policy_status = self.user_cloud.get_cluster_by_id(cluster['id'])[ - 'policies' - ] - if policy_status != []: - self.user_cloud.detach_policy_from_cluster(cluster, policy) - self.user_cloud.delete_cluster_policy(name) - - def cleanup_receiver(self, name): - self.user_cloud.delete_cluster_receiver(name) From fc18b74c9ebfe2dd1cbcd1f138c5ae4656605862 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 1 Jul 2024 15:58:40 +0100 Subject: [PATCH 3535/3836] mypy: Enable checks for openstack.tests.functional These don't use mocks so it is possible to run mypy against them, now that openstack.cloud is typed. Change-Id: I4f1488d260737a53354548bd51010bc15d147b79 Signed-off-by: Stephen Finucane --- openstack/tests/functional/baremetal/base.py | 6 ++- openstack/tests/functional/base.py | 38 +++++++++++------- .../functional/cloud/test_floating_ip.py | 2 +- .../tests/functional/cloud/test_object.py | 40 +++++-------------- .../functional/cloud/test_project_cleanup.py | 17 ++++---- .../tests/functional/cloud/test_volume.py | 8 ++-- .../functional/cloud/test_volume_backup.py | 2 +- .../functional/compute/v2/test_server.py | 1 - .../tests/functional/instance_ha/test_host.py | 6 ++- .../tests/functional/network/v2/test_agent.py | 2 +- .../v2/test_agent_add_remove_network.py | 8 ++-- .../v2/test_agent_add_remove_router.py | 6 +-- .../functional/network/v2/test_floating_ip.py | 27 +++++++------ .../functional/network/v2/test_ndp_proxy.py | 17 ++++---- .../network/v2/test_port_forwarding.py | 11 +++-- .../functional/network/v2/test_qos_policy.py | 3 +- .../v2/test_router_add_remove_interface.py | 8 ++-- .../functional/shared_file_system/base.py | 4 +- .../shared_file_system/test_share_metadata.py | 4 +- setup.cfg | 5 --- 20 files changed, 110 insertions(+), 105 deletions(-) diff --git a/openstack/tests/functional/baremetal/base.py b/openstack/tests/functional/baremetal/base.py index 2f08576ef..4150619ae 100644 --- a/openstack/tests/functional/baremetal/base.py +++ b/openstack/tests/functional/baremetal/base.py @@ -10,12 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + from openstack.tests.functional import base class BaseBaremetalTest(base.BaseFunctionalTest): - min_microversion = None - node_id = None + min_microversion: ty.Optional[str] = None + node_id: str def setUp(self): super().setUp() diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index 6d6f80bac..83c4cb518 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -41,6 +41,10 @@ def _disable_keep_alive(conn): class BaseFunctionalTest(base.TestCase): + user_cloud: connection.Connection + user_cloud_alt: connection.Connection + operator_cloud: connection.Connection + _wait_for_timeout_key = '' def setUp(self): @@ -49,21 +53,32 @@ def setUp(self): _disable_keep_alive(self.conn) self._demo_name = os.environ.get('OPENSTACKSDK_DEMO_CLOUD', 'devstack') + if not self._demo_name: + raise self.failureException( + "OPENSTACKSDK_OPERATOR_CLOUD must be set to a non-empty value" + ) + self._demo_name_alt = os.environ.get( 'OPENSTACKSDK_DEMO_CLOUD_ALT', 'devstack-alt', ) + if not self._demo_name_alt: + raise self.failureException( + "OPENSTACKSDK_OPERATOR_CLOUD must be set to a non-empty value" + ) + self._op_name = os.environ.get( 'OPENSTACKSDK_OPERATOR_CLOUD', 'devstack-admin', ) + if not self._op_name: + raise self.failureException( + "OPENSTACKSDK_OPERATOR_CLOUD must be set to a non-empty value" + ) self.config = openstack.config.OpenStackConfig() self._set_user_cloud() - if self._op_name: - self._set_operator_cloud() - else: - self.operator_cloud = None + self._set_operator_cloud() self.identity_version = self.user_cloud.config.get_api_version( 'identity' @@ -86,16 +101,11 @@ def _set_user_cloud(self, **kwargs): self.user_cloud = connection.Connection(config=user_config) _disable_keep_alive(self.user_cloud) - # This cloud is used by the project_cleanup test, so you can't rely on - # it - if self._demo_name_alt: - user_config_alt = self.config.get_one( - cloud=self._demo_name_alt, **kwargs - ) - self.user_cloud_alt = connection.Connection(config=user_config_alt) - _disable_keep_alive(self.user_cloud_alt) - else: - self.user_cloud_alt = None + user_config_alt = self.config.get_one( + cloud=self._demo_name_alt, **kwargs + ) + self.user_cloud_alt = connection.Connection(config=user_config_alt) + _disable_keep_alive(self.user_cloud_alt) def _set_operator_cloud(self, **kwargs): operator_config = self.config.get_one(cloud=self._op_name, **kwargs) diff --git a/openstack/tests/functional/cloud/test_floating_ip.py b/openstack/tests/functional/cloud/test_floating_ip.py index b28019991..e557fd378 100644 --- a/openstack/tests/functional/cloud/test_floating_ip.py +++ b/openstack/tests/functional/cloud/test_floating_ip.py @@ -180,7 +180,7 @@ def _setup_networks(self): else: # Find network names for nova-net data = proxy._json_response( - self.user_cloud._conn.compute.get('/os-tenant-networks') + self.user_cloud.compute.get('/os-tenant-networks') ) nets = meta.get_and_munchify('networks', data) self.addDetail( diff --git a/openstack/tests/functional/cloud/test_object.py b/openstack/tests/functional/cloud/test_object.py index 8edd4e965..9ca628f16 100644 --- a/openstack/tests/functional/cloud/test_object.py +++ b/openstack/tests/functional/cloud/test_object.py @@ -23,7 +23,6 @@ from testtools import content -from openstack import exceptions from openstack.tests.functional import base @@ -93,18 +92,9 @@ def test_create_object(self): 'testk' ], ) - try: - self.assertIsNotNone( - self.user_cloud.get_object(container_name, name) - ) - except exceptions.SDKException as e: - self.addDetail( - 'failed_response', - content.text_content(str(e.response.headers)), - ) - self.addDetail( - 'failed_response', content.text_content(e.response.text) - ) + self.assertIsNotNone( + self.user_cloud.get_object(container_name, name) + ) self.assertEqual( name, self.user_cloud.list_objects(container_name)[0]['name'] ) @@ -134,7 +124,7 @@ def test_download_object_to_file(self): (64 * 1024, 5), # 64MB, 5 segments ) for size, nseg in sizes: - fake_content = '' + fake_content = b'' segment_size = int(round(size / nseg)) with tempfile.NamedTemporaryFile() as fake_file: fake_content = ''.join( @@ -179,22 +169,14 @@ def test_download_object_to_file(self): 'testk' ], ) - try: - with tempfile.NamedTemporaryFile() as fake_file: - self.user_cloud.get_object( - container_name, name, outfile=fake_file.name - ) - downloaded_content = open(fake_file.name, 'rb').read() - self.assertEqual(fake_content, downloaded_content) - except exceptions.SDKException as e: - self.addDetail( - 'failed_response', - content.text_content(str(e.response.headers)), - ) - self.addDetail( - 'failed_response', content.text_content(e.response.text) + + with tempfile.NamedTemporaryFile() as fake_file: + self.user_cloud.get_object( + container_name, name, outfile=fake_file.name ) - raise + downloaded_content = open(fake_file.name, 'rb').read() + self.assertEqual(fake_content, downloaded_content) + self.assertEqual( name, self.user_cloud.list_objects(container_name)[0]['name'] ) diff --git a/openstack/tests/functional/cloud/test_project_cleanup.py b/openstack/tests/functional/cloud/test_project_cleanup.py index 8dc00b37f..6dfbf9cbb 100644 --- a/openstack/tests/functional/cloud/test_project_cleanup.py +++ b/openstack/tests/functional/cloud/test_project_cleanup.py @@ -16,8 +16,11 @@ Functional tests for project cleanup methods. """ + import queue +from openstack.network.v2 import network as _network +from openstack import resource from openstack.tests.functional import base @@ -52,7 +55,7 @@ def _create_network_resources(self): def test_cleanup(self): self._create_network_resources() - status_queue = queue.Queue() + status_queue: queue.Queue[resource.Resource] = queue.Queue() # First round - check no resources are old enough self.conn.project_cleanup( @@ -127,7 +130,7 @@ def test_block_storage_cleanup(self): if not self.user_cloud.has_service('object-store'): self.skipTest('Object service is requred, but not available') - status_queue = queue.Queue() + status_queue: queue.Queue[resource.Resource] = queue.Queue() vol = self.conn.block_storage.create_volume(name='vol1', size='1') self.conn.block_storage.wait_for_status(vol) @@ -211,7 +214,8 @@ def test_cleanup_swift(self): if not self.user_cloud.has_service('object-store'): self.skipTest('Object service is requred, but not available') - status_queue = queue.Queue() + status_queue: queue.Queue[resource.Resource] = queue.Queue() + self.conn.object_store.create_container('test_cleanup') for i in range(1, 10): self.conn.object_store.create_object( @@ -261,15 +265,14 @@ def test_cleanup_vpnaas(self): if not list(self.conn.network.service_providers(service_type="VPN")): self.skipTest("VPNaaS plugin is requred, but not available") - status_queue = queue.Queue() + status_queue: queue.Queue[resource.Resource] = queue.Queue() # Find available external networks and use one - external_network = None for network in self.conn.network.networks(): if network.is_router_external: - external_network = network + external_network: _network.Network = network break - if not external_network: + else: self.skipTest("External network is required, but not available") # Create left network resources diff --git a/openstack/tests/functional/cloud/test_volume.py b/openstack/tests/functional/cloud/test_volume.py index 9e6326b6d..727e38fcf 100644 --- a/openstack/tests/functional/cloud/test_volume.py +++ b/openstack/tests/functional/cloud/test_volume.py @@ -148,10 +148,10 @@ def test_list_volumes_pagination(self): volumes.append(v) self.addCleanup(self.cleanup, volumes) result = [] - for i in self.user_cloud.list_volumes(): - if i['name'] and i['name'].startswith(self.id()): - result.append(i['id']) - self.assertEqual(sorted([i['id'] for i in volumes]), sorted(result)) + for v in self.user_cloud.list_volumes(): + if v['name'] and v['name'].startswith(self.id()): + result.append(v['id']) + self.assertEqual(sorted([v['id'] for v in volumes]), sorted(result)) def test_update_volume(self): name, desc = self.getUniqueString('name'), self.getUniqueString('desc') diff --git a/openstack/tests/functional/cloud/test_volume_backup.py b/openstack/tests/functional/cloud/test_volume_backup.py index d4bd03252..acf6c7812 100644 --- a/openstack/tests/functional/cloud/test_volume_backup.py +++ b/openstack/tests/functional/cloud/test_volume_backup.py @@ -115,7 +115,7 @@ def test_list_volume_backups(self): self.assertEqual(2, len(backups)) backups = self.user_cloud.list_volume_backups( - search_opts={"name": backup_name_1} + filters={"name": backup_name_1} ) self.assertEqual(1, len(backups)) self.assertEqual(backup_name_1, backups[0]['name']) diff --git a/openstack/tests/functional/compute/v2/test_server.py b/openstack/tests/functional/compute/v2/test_server.py index 34174a2ed..215e7ccb8 100644 --- a/openstack/tests/functional/compute/v2/test_server.py +++ b/openstack/tests/functional/compute/v2/test_server.py @@ -68,7 +68,6 @@ class TestServer(ft_base.BaseComputeTest): def setUp(self): super().setUp() self.NAME = self.getUniqueString() - self.server = None self.network = None self.subnet = None self.cidr = '10.99.99.0/16' diff --git a/openstack/tests/functional/instance_ha/test_host.py b/openstack/tests/functional/instance_ha/test_host.py index f307c74cf..9447790d1 100644 --- a/openstack/tests/functional/instance_ha/test_host.py +++ b/openstack/tests/functional/instance_ha/test_host.py @@ -12,12 +12,14 @@ # 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 unittest +import typing as ty + +from openstack.compute.v2 import hypervisor from openstack import connection from openstack.tests.functional import base -HYPERVISORS = [] +HYPERVISORS: ty.List[hypervisor.Hypervisor] = [] def hypervisors(): diff --git a/openstack/tests/functional/network/v2/test_agent.py b/openstack/tests/functional/network/v2/test_agent.py index e23198c74..21763dc62 100644 --- a/openstack/tests/functional/network/v2/test_agent.py +++ b/openstack/tests/functional/network/v2/test_agent.py @@ -17,7 +17,7 @@ class TestAgent(base.BaseFunctionalTest): - AGENT = None + AGENT: agent.Agent DESC = "test description" def validate_uuid(self, s): diff --git a/openstack/tests/functional/network/v2/test_agent_add_remove_network.py b/openstack/tests/functional/network/v2/test_agent_add_remove_network.py index 221518116..a00f3fba2 100644 --- a/openstack/tests/functional/network/v2/test_agent_add_remove_network.py +++ b/openstack/tests/functional/network/v2/test_agent_add_remove_network.py @@ -10,15 +10,15 @@ # License for the specific language governing permissions and limitations # under the License. - +from openstack.network.v2 import agent from openstack.network.v2 import network from openstack.tests.functional import base class TestAgentNetworks(base.BaseFunctionalTest): - NETWORK_ID = None - AGENT = None - AGENT_ID = None + NETWORK_ID: str + AGENT: agent.Agent + AGENT_ID: str def setUp(self): super().setUp() diff --git a/openstack/tests/functional/network/v2/test_agent_add_remove_router.py b/openstack/tests/functional/network/v2/test_agent_add_remove_router.py index b583f7f01..550ddada4 100644 --- a/openstack/tests/functional/network/v2/test_agent_add_remove_router.py +++ b/openstack/tests/functional/network/v2/test_agent_add_remove_router.py @@ -10,14 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. - +from openstack.network.v2 import agent from openstack.network.v2 import router from openstack.tests.functional import base class TestAgentRouters(base.BaseFunctionalTest): - ROUTER = None - AGENT = None + ROUTER: router.Router + AGENT: agent.Agent def setUp(self): super().setUp() diff --git a/openstack/tests/functional/network/v2/test_floating_ip.py b/openstack/tests/functional/network/v2/test_floating_ip.py index eb78f432a..7e92688a2 100644 --- a/openstack/tests/functional/network/v2/test_floating_ip.py +++ b/openstack/tests/functional/network/v2/test_floating_ip.py @@ -23,13 +23,13 @@ class TestFloatingIP(base.BaseFunctionalTest): IPV4 = 4 EXT_CIDR = "10.100.0.0/24" INT_CIDR = "10.101.0.0/24" - EXT_NET_ID = None - INT_NET_ID = None - EXT_SUB_ID = None - INT_SUB_ID = None - ROT_ID = None - PORT_ID = None - FIP = None + EXT_NET_ID: str + INT_NET_ID: str + EXT_SUB_ID: str + INT_SUB_ID: str + ROT_ID: str + PORT_ID: str + FIP: floating_ip.FloatingIP DNS_DOMAIN = "example.org." DNS_NAME = "fip1" @@ -43,8 +43,6 @@ def setUp(self): self.ROT_NAME = self.getUniqueString() self.INT_NET_NAME = self.getUniqueString() self.INT_SUB_NAME = self.getUniqueString() - self.EXT_NET_ID = None - self.EXT_SUB_ID = None self.is_dns_supported = False # Find External Network @@ -58,8 +56,9 @@ def setUp(self): # credentials available # WARNING: this external net is not dropped # Create External Network - args = {"router:external": True} - net = self._create_network(self.EXT_NET_NAME, **args) + net = self._create_network( + self.EXT_NET_NAME, **{"router:external": True} + ) self.EXT_NET_ID = net.id sub = self._create_subnet( self.EXT_SUB_NAME, self.EXT_NET_ID, self.EXT_CIDR @@ -74,8 +73,10 @@ def setUp(self): ) self.INT_SUB_ID = sub.id # Create Router - args = {"external_gateway_info": {"network_id": self.EXT_NET_ID}} - sot = self.user_cloud.network.create_router(name=self.ROT_NAME, **args) + sot = self.user_cloud.network.create_router( + name=self.ROT_NAME, + **{"external_gateway_info": {"network_id": self.EXT_NET_ID}} + ) assert isinstance(sot, router.Router) self.assertEqual(self.ROT_NAME, sot.name) self.ROT_ID = sot.id diff --git a/openstack/tests/functional/network/v2/test_ndp_proxy.py b/openstack/tests/functional/network/v2/test_ndp_proxy.py index ccfd93037..ae24c0d0f 100644 --- a/openstack/tests/functional/network/v2/test_ndp_proxy.py +++ b/openstack/tests/functional/network/v2/test_ndp_proxy.py @@ -52,8 +52,9 @@ def setUp(self): # credentials available # WARNING: this external net is not dropped # Create External Network - args = {"router:external": True} - net = self._create_network(self.EXT_NET_NAME, **args) + net = self._create_network( + self.EXT_NET_NAME, **{"router:external": True} + ) self.EXT_NET_ID = net.id sub = self._create_subnet( self.EXT_SUB_NAME, self.EXT_NET_ID, self.EXT_CIDR @@ -61,11 +62,13 @@ def setUp(self): self.EXT_SUB_ID = sub.id # Create Router - args = { - "external_gateway_info": {"network_id": self.EXT_NET_ID}, - "enable_ndp_proxy": True, - } - sot = self.user_cloud.network.create_router(name=self.ROT_NAME, **args) + sot = self.user_cloud.network.create_router( + name=self.ROT_NAME, + **{ + "external_gateway_info": {"network_id": self.EXT_NET_ID}, + "enable_ndp_proxy": True, + }, + ) assert isinstance(sot, router.Router) self.assertEqual(self.ROT_NAME, sot.name) self.ROT_ID = sot.id diff --git a/openstack/tests/functional/network/v2/test_port_forwarding.py b/openstack/tests/functional/network/v2/test_port_forwarding.py index 362338231..034663e72 100644 --- a/openstack/tests/functional/network/v2/test_port_forwarding.py +++ b/openstack/tests/functional/network/v2/test_port_forwarding.py @@ -67,8 +67,9 @@ def setUp(self): # credentials available # WARNING: this external net is not dropped # Create External Network - args = {"router:external": True} - net = self._create_network(self.EXT_NET_NAME, **args) + net = self._create_network( + self.EXT_NET_NAME, **{"router:external": True} + ) self.EXT_NET_ID = net.id sub = self._create_subnet( self.EXT_SUB_NAME, self.EXT_NET_ID, self.EXT_CIDR @@ -83,8 +84,10 @@ def setUp(self): ) self.INT_SUB_ID = sub.id # Create Router - args = {"external_gateway_info": {"network_id": self.EXT_NET_ID}} - sot = self.user_cloud.network.create_router(name=self.ROT_NAME, **args) + sot = self.user_cloud.network.create_router( + name=self.ROT_NAME, + **{"external_gateway_info": {"network_id": self.EXT_NET_ID}} + ) assert isinstance(sot, router.Router) self.assertEqual(self.ROT_NAME, sot.name) self.ROT_ID = sot.id diff --git a/openstack/tests/functional/network/v2/test_qos_policy.py b/openstack/tests/functional/network/v2/test_qos_policy.py index 8395566fc..d889d79db 100644 --- a/openstack/tests/functional/network/v2/test_qos_policy.py +++ b/openstack/tests/functional/network/v2/test_qos_policy.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty from openstack.network.v2 import qos_policy as _qos_policy from openstack.tests.functional import base @@ -19,7 +20,7 @@ class TestQoSPolicy(base.BaseFunctionalTest): QOS_POLICY_ID = None IS_SHARED = False IS_DEFAULT = False - RULES = [] + RULES: ty.List[str] = [] QOS_POLICY_DESCRIPTION = "QoS policy description" def setUp(self): diff --git a/openstack/tests/functional/network/v2/test_router_add_remove_interface.py b/openstack/tests/functional/network/v2/test_router_add_remove_interface.py index 6557b1ac7..0a1c1ff36 100644 --- a/openstack/tests/functional/network/v2/test_router_add_remove_interface.py +++ b/openstack/tests/functional/network/v2/test_router_add_remove_interface.py @@ -20,10 +20,10 @@ class TestRouterInterface(base.BaseFunctionalTest): CIDR = "10.100.0.0/16" IPV4 = 4 - ROUTER_ID = None - NET_ID = None - SUB_ID = None - ROT = None + ROUTER_ID: str + NET_ID: str + SUB_ID: str + ROT: router.Router def setUp(self): super().setUp() diff --git a/openstack/tests/functional/shared_file_system/base.py b/openstack/tests/functional/shared_file_system/base.py index b57ce9a66..3a5e2d9bf 100644 --- a/openstack/tests/functional/shared_file_system/base.py +++ b/openstack/tests/functional/shared_file_system/base.py @@ -10,12 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + from openstack import resource from openstack.tests.functional import base class BaseSharedFileSystemTest(base.BaseFunctionalTest): - min_microversion = None + min_microversion: ty.Optional[str] = None def setUp(self): super().setUp() diff --git a/openstack/tests/functional/shared_file_system/test_share_metadata.py b/openstack/tests/functional/shared_file_system/test_share_metadata.py index 1afa56b9c..e4b042f39 100644 --- a/openstack/tests/functional/shared_file_system/test_share_metadata.py +++ b/openstack/tests/functional/shared_file_system/test_share_metadata.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + from openstack.shared_file_system.v2 import share as _share from openstack.tests.functional.shared_file_system import base @@ -80,7 +82,7 @@ def test_update(self): new_meta = {"newFoo": "newBar"} full_meta = {"foo": "bar", "newFoo": "newBar"} - empty_meta = {} + empty_meta: ty.Dict[str, str] = {} updated_share = ( self.user_cloud.shared_file_system.update_share_metadata( diff --git a/setup.cfg b/setup.cfg index 8bb9e2f16..7e881f77c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,16 +40,11 @@ incremental = true check_untyped_defs = true warn_unused_ignores = false # keep this in-sync with 'mypy.exclude' in '.pre-commit-config.yaml' -# TODO(stephenfin) Eventually we should remove everything here except the -# unit tests module exclude = (?x)( doc | examples | releasenotes ) -[mypy-openstack.tests.functional.*] -ignore_errors = true - [mypy-openstack.tests.unit.*] ignore_errors = true From ef052e48807e0811859d8a5fc720846ea93d0f2c Mon Sep 17 00:00:00 2001 From: Balazs Gibizer Date: Fri, 2 Aug 2024 17:19:31 +0200 Subject: [PATCH 3536/3836] Fix parameter name clashing in resource lock query TODO: * add test coverage * look at other methods taking resource_type as kwargs Closes-Bug: #2075347 Change-Id: I2a48a3793ebd2db9dd9ca02b75b599c266ad1c24 --- openstack/proxy.py | 8 ++++++++ openstack/shared_file_system/v2/_proxy.py | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/openstack/proxy.py b/openstack/proxy.py index 5768fc9f8..41bb93fa9 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -744,6 +744,14 @@ def _list( :class:`~openstack.resource.Resource` that doesn't match the ``resource_type``. """ + # Check for attributes whose names conflict with the parameters + # specified in the method. + conflicting_attrs = attrs.get('__conflicting_attrs', {}) + if conflicting_attrs: + for k, v in conflicting_attrs.items(): + attrs[k] = v + attrs.pop('__conflicting_attrs') + data = resource_type.list( self, paginated=paginated, base_path=base_path, **attrs ) diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index 836b2d3da..b64ad87ea 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -1129,6 +1129,15 @@ def resource_locks(self, **query): :rtype: :class:`~openstack.shared_file_system.v2. resource_locks.ResourceLock` """ + + if query.get('resource_type'): + # The _create method has a parameter named resource_type, which + # refers to the type of resource to be created, so we need to avoid + # a conflict of parameters we are sending to the method. + query['__conflicting_attrs'] = { + 'resource_type': query.get('resource_type') + } + query.pop('resource_type') return self._list(_resource_locks.ResourceLock, **query) def get_resource_lock(self, resource_lock): From 12ed1a34331147926d286ad2f12b389a1edd9957 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 7 Aug 2024 13:33:42 +0100 Subject: [PATCH 3537/3836] image: Check path is a file before attempting to use it (redux) We missed one. Change-Id: I51fd19054fed25ed0b07147240cd497918403817 Signed-off-by: Stephen Finucane --- openstack/image/v2/_proxy.py | 2 +- openstack/tests/unit/image/v2/test_proxy.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 2ba82afbe..63eb7a17a 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -46,7 +46,7 @@ def _get_name_and_filename(name, image_format): # Try appending the disk format name_with_ext = '.'.join((name, image_format)) - if os.path.exists(name_with_ext): + if os.path.exists(name_with_ext) and os.path.isfile(name): return os.path.basename(name), name_with_ext return name, None diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 60c5bd881..0885e0b7b 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -128,7 +128,6 @@ def test_image_create_file_as_name(self): name = os.path.basename(tmpfile.name) self._verify( 'openstack.image.v2._proxy.Proxy._upload_image', - # 'openstack.image.v2.image.Image.create', self.proxy.create_image, method_kwargs={ 'name': tmpfile.name, From b44df1479378944dfc20ef12a8a20334cadd05b2 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 15 Jul 2024 10:30:53 +0100 Subject: [PATCH 3538/3836] Add Python 3.12 classifier Change-Id: I88165b0a9aacda2b6f482b1a2b1f0046247b1e2f Signed-off-by: Stephen Finucane --- setup.cfg | 3 ++- zuul.d/project.yaml | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 7e881f77c..51ce508c9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,6 +6,7 @@ description_file = author = OpenStack author_email = openstack-discuss@lists.openstack.org home_page = https://docs.openstack.org/openstacksdk/ +python_requires = >=3.7 classifier = Environment :: OpenStack Intended Audience :: Information Technology @@ -19,7 +20,7 @@ classifier = Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 -python_requires = >=3.7 + Programming Language :: Python :: 3.12 [files] packages = diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index 202d89f67..084449319 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -14,6 +14,7 @@ - release-notes-jobs-python3 check: jobs: + - openstack-tox-py312 - opendev-buildset-registry - nodepool-build-image-siblings: voting: false From bbc130ed66669973d161df36bfb9baa21c26e060 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 7 Aug 2024 16:37:33 +0100 Subject: [PATCH 3539/3836] Drop support for Python 3.7 We haven't tested this in the gate for some time and it's been known broken. Change-Id: Ied4f04189447330ec664d2c2dbc21a6ac6ef4f6c Signed-off-by: Stephen Finucane --- setup.cfg | 3 +-- tox.ini | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 51ce508c9..2030b35a9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,7 +6,7 @@ description_file = author = OpenStack author_email = openstack-discuss@lists.openstack.org home_page = https://docs.openstack.org/openstacksdk/ -python_requires = >=3.7 +python_requires = >=3.8 classifier = Environment :: OpenStack Intended Audience :: Information Technology @@ -15,7 +15,6 @@ classifier = Operating System :: POSIX :: Linux Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 diff --git a/tox.ini b/tox.ini index 1a63a2b0d..36491ed8d 100644 --- a/tox.ini +++ b/tox.ini @@ -23,7 +23,7 @@ commands = stestr run {posargs} stestr slowest -[testenv:functional{,-py37,-py38,-py39,-py310,-py311,-py312}] +[testenv:functional{,-py38,-py39,-py310,-py311,-py312}] description = Run functional tests. # Some jobs (especially heat) takes longer, therefore increase default timeout From 48f96397444d5dad2fad4da74a4461f61c173f2e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 8 Jul 2024 17:00:24 +0100 Subject: [PATCH 3540/3836] Drop support for Python 3.8 We haven't tested this in the gate for some time. While we appear to still work under 3.8, it goes end-of-life in October 2024 and is holding us back from adding more, better typing. It's time to let it go. Change-Id: I0e1cef837febe3baa0dd145640ebc96192c0c915 Signed-off-by: Stephen Finucane Depends-on: https://review.opendev.org/c/openstack/python-openstackclient/+/924493 --- releasenotes/notes/drop-python-37-38-2a6336af44050fec.yaml | 6 ++++++ setup.cfg | 3 +-- tox.ini | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/drop-python-37-38-2a6336af44050fec.yaml diff --git a/releasenotes/notes/drop-python-37-38-2a6336af44050fec.yaml b/releasenotes/notes/drop-python-37-38-2a6336af44050fec.yaml new file mode 100644 index 000000000..ab605dfad --- /dev/null +++ b/releasenotes/notes/drop-python-37-38-2a6336af44050fec.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + Support for Python 3.7 and 3.8 has been dropped. Python 3.7 support was + untested and known to be broken for multiple releases, while Python 3.8 + is going EOL in October 2024. diff --git a/setup.cfg b/setup.cfg index 2030b35a9..2f375b29a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,7 +6,7 @@ description_file = author = OpenStack author_email = openstack-discuss@lists.openstack.org home_page = https://docs.openstack.org/openstacksdk/ -python_requires = >=3.8 +python_requires = >=3.9 classifier = Environment :: OpenStack Intended Audience :: Information Technology @@ -15,7 +15,6 @@ classifier = Operating System :: POSIX :: Linux Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 diff --git a/tox.ini b/tox.ini index 36491ed8d..68fcacfcd 100644 --- a/tox.ini +++ b/tox.ini @@ -23,7 +23,7 @@ commands = stestr run {posargs} stestr slowest -[testenv:functional{,-py38,-py39,-py310,-py311,-py312}] +[testenv:functional{-py39,-py310,-py311,-py312}] description = Run functional tests. # Some jobs (especially heat) takes longer, therefore increase default timeout From 7db657a8c4e1f2f291c6aa9fb185e33ce4835085 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 8 Jul 2024 17:11:37 +0100 Subject: [PATCH 3541/3836] mypy: Run under minimum Python version To avoid repeat of Ifcf68075e572ccfc910dbef449cd58986c2a1bf5. Change-Id: I05037fe4c806415ea3c7ea07ae6cb470073211c8 Signed-off-by: Stephen Finucane --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 2f375b29a..51a4a853c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,7 @@ console_scripts = openstack-inventory = openstack.cloud.cmd.inventory:main [mypy] +python_version = 3.9 show_column_numbers = true show_error_context = true ignore_missing_imports = true From 6b068e13e8b216e3e73f4295eb9d718509b145be Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 8 Jul 2024 17:14:31 +0100 Subject: [PATCH 3542/3836] Remove redundant code Change-Id: I5e4d34f59ffaa8b7268d87ad42fd3d8023e36a91 Signed-off-by: Stephen Finucane --- openstack/image/_download.py | 5 +- openstack/tests/fakes.py | 3 +- openstack/tests/unit/image/v2/test_image.py | 4 +- openstack/tests/unit/test_utils.py | 85 --------------------- openstack/utils.py | 27 +------ 5 files changed, 9 insertions(+), 115 deletions(-) diff --git a/openstack/image/_download.py b/openstack/image/_download.py index 1203b5579..ce9ad5285 100644 --- a/openstack/image/_download.py +++ b/openstack/image/_download.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import hashlib import io from openstack import exceptions @@ -68,7 +69,7 @@ def download( details = self.fetch(session) checksum = details.checksum - md5 = utils.md5(usedforsecurity=False) + md5 = hashlib.md5(usedforsecurity=False) if output: try: if isinstance(output, io.IOBase): @@ -97,7 +98,7 @@ def download( if checksum is not None: _verify_checksum( - utils.md5(resp.content, usedforsecurity=False), checksum + hashlib.md5(resp.content, usedforsecurity=False), checksum ) else: session.log.warning( diff --git a/openstack/tests/fakes.py b/openstack/tests/fakes.py index e3f48c453..eebf200ac 100644 --- a/openstack/tests/fakes.py +++ b/openstack/tests/fakes.py @@ -24,7 +24,6 @@ from openstack.cloud import meta from openstack.orchestration.util import template_format -from openstack import utils PROJECT_ID = '1c36b64c840a42cd9e9b931a369337f0' FLAVOR_ID = '0c1d9008-f546-4608-9e8f-f8bdaec8dddd' @@ -256,7 +255,7 @@ def make_fake_image( checksum='ee36e35a297980dee1b514de9803ec6d', ): if data: - md5 = utils.md5(usedforsecurity=False) + md5 = hashlib.md5(usedforsecurity=False) sha256 = hashlib.sha256() with open(data, 'rb') as file_obj: for chunk in iter(lambda: file_obj.read(8192), b''): diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 8ff4cb2b7..f402c7215 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import hashlib import io import operator import tempfile @@ -22,7 +23,6 @@ from openstack import exceptions from openstack.image.v2 import image from openstack.tests.unit import base -from openstack import utils IDENTIFIER = 'IDENTIFIER' EXAMPLE = { @@ -93,7 +93,7 @@ def calculate_md5_checksum(data): - checksum = utils.md5(usedforsecurity=False) + checksum = hashlib.md5(usedforsecurity=False) for chunk in data: checksum.update(chunk) return checksum.hexdigest() diff --git a/openstack/tests/unit/test_utils.py b/openstack/tests/unit/test_utils.py index c0d779c9c..347ee50f0 100644 --- a/openstack/tests/unit/test_utils.py +++ b/openstack/tests/unit/test_utils.py @@ -11,7 +11,6 @@ # under the License. import concurrent.futures -import hashlib import logging import sys from unittest import mock @@ -301,87 +300,3 @@ def test_add_node_after_edge(self): def test_walker_fn(graph, node, lst): lst.append(node) graph.node_done(node) - - -class Test_md5(base.TestCase): - def setUp(self): - super().setUp() - self.md5_test_data = b"Openstack forever" - try: - self.md5_digest = hashlib.md5( # nosec - self.md5_test_data - ).hexdigest() - self.fips_enabled = False - except ValueError: - self.md5_digest = '0d6dc3c588ae71a04ce9a6beebbbba06' - self.fips_enabled = True - - def test_md5_with_data(self): - if not self.fips_enabled: - digest = utils.md5(self.md5_test_data).hexdigest() - self.assertEqual(digest, self.md5_digest) - else: - # on a FIPS enabled system, this throws a ValueError: - # [digital envelope routines: EVP_DigestInit_ex] disabled for FIPS - self.assertRaises(ValueError, utils.md5, self.md5_test_data) - if not self.fips_enabled: - digest = utils.md5( - self.md5_test_data, usedforsecurity=True - ).hexdigest() - self.assertEqual(digest, self.md5_digest) - else: - self.assertRaises( - ValueError, utils.md5, self.md5_test_data, usedforsecurity=True - ) - digest = utils.md5( - self.md5_test_data, usedforsecurity=False - ).hexdigest() - self.assertEqual(digest, self.md5_digest) - - def test_md5_without_data(self): - if not self.fips_enabled: - test_md5 = utils.md5() - test_md5.update(self.md5_test_data) - digest = test_md5.hexdigest() - self.assertEqual(digest, self.md5_digest) - else: - self.assertRaises(ValueError, utils.md5) - if not self.fips_enabled: - test_md5 = utils.md5(usedforsecurity=True) - test_md5.update(self.md5_test_data) - digest = test_md5.hexdigest() - self.assertEqual(digest, self.md5_digest) - else: - self.assertRaises(ValueError, utils.md5, usedforsecurity=True) - test_md5 = utils.md5(usedforsecurity=False) - test_md5.update(self.md5_test_data) - digest = test_md5.hexdigest() - self.assertEqual(digest, self.md5_digest) - - def test_string_data_raises_type_error(self): - if not self.fips_enabled: - self.assertRaises(TypeError, hashlib.md5, 'foo') - self.assertRaises(TypeError, utils.md5, 'foo') - self.assertRaises( - TypeError, utils.md5, 'foo', usedforsecurity=True - ) - else: - self.assertRaises(ValueError, hashlib.md5, 'foo') - self.assertRaises(ValueError, utils.md5, 'foo') - self.assertRaises( - ValueError, utils.md5, 'foo', usedforsecurity=True - ) - self.assertRaises(TypeError, utils.md5, 'foo', usedforsecurity=False) - - def test_none_data_raises_type_error(self): - if not self.fips_enabled: - self.assertRaises(TypeError, hashlib.md5, None) - self.assertRaises(TypeError, utils.md5, None) - self.assertRaises(TypeError, utils.md5, None, usedforsecurity=True) - else: - self.assertRaises(ValueError, hashlib.md5, None) - self.assertRaises(ValueError, utils.md5, None) - self.assertRaises( - ValueError, utils.md5, None, usedforsecurity=True - ) - self.assertRaises(TypeError, utils.md5, None, usedforsecurity=False) diff --git a/openstack/utils.py b/openstack/utils.py index 24c409c58..bb53c7a1c 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -285,11 +285,11 @@ def maximum_supported_microversion(adapter, client_maximum): def _hashes_up_to_date(md5, sha256, md5_key, sha256_key): - '''Compare md5 and sha256 hashes for being up to date + """Compare md5 and sha256 hashes for being up to date md5 and sha256 are the current values. md5_key and sha256_key are the previous values. - ''' + """ up_to_date = False if md5 and md5_key == md5: up_to_date = True @@ -302,29 +302,8 @@ def _hashes_up_to_date(md5, sha256, md5_key, sha256_key): return up_to_date -try: - _test_md5 = hashlib.md5(usedforsecurity=False) # nosec - - # Python distributions that support a hashlib.md5 with the usedforsecurity - # keyword can just use that md5 definition as-is - # See https://bugs.python.org/issue9216 - # - # TODO(alee) Remove this wrapper when the minimum python version is bumped - # to 3.9 (which is the first upstream version to support this keyword) - # See https://docs.python.org/3.9/library/hashlib.html - md5 = hashlib.md5 -except TypeError: - - def md5(string=b'', usedforsecurity=True): - """Return an md5 hashlib object without usedforsecurity parameter - For python distributions that do not yet support this keyword - parameter, we drop the parameter - """ - return hashlib.md5(string) # nosec - - def _calculate_data_hashes(data): - _md5 = md5(usedforsecurity=False) + _md5 = hashlib.md5(usedforsecurity=False) _sha256 = hashlib.sha256() if hasattr(data, 'read'): From 05c442ddf151516fdfd0467ab96d97a98cbdd1dd Mon Sep 17 00:00:00 2001 From: Balazs Gibizer Date: Mon, 5 Aug 2024 12:10:13 +0200 Subject: [PATCH 3543/3836] Support server unshelve to specific host Closes-Bug: #2075972 Change-Id: I9b20bb8c32ed95bb4d3dad3f8f0e139380d45c27 --- openstack/compute/v2/_proxy.py | 6 ++++-- openstack/tests/unit/compute/v2/test_proxy.py | 15 +++++++++++++++ ...-server-unshelve-to-host-cb02eee8a20ba478.yaml | 5 +++++ 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/fix-server-unshelve-to-host-cb02eee8a20ba478.yaml diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 063938ef9..33086fd25 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -1183,7 +1183,7 @@ def shelve_offload_server(self, server): server = self._get_resource(_server.Server, server) server.shelve_offload(self) - def unshelve_server(self, server): + def unshelve_server(self, server, *, host=None): """Unshelves or restores a shelved server. Policy defaults enable only users with administrative role or the @@ -1192,10 +1192,12 @@ def unshelve_server(self, server): :param server: Either the ID of a server or a :class:`~openstack.compute.v2.server.Server` instance. + :param host: An optional parameter specifying the name the compute + host to unshelve to. (New in API version 2.91). :returns: None """ server = self._get_resource(_server.Server, server) - server.unshelve(self) + server.unshelve(self, host=host) def trigger_server_crash_dump(self, server): """Trigger a crash dump in a server. diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 45d5efe96..996ccb88b 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -1276,6 +1276,21 @@ def test_server_unshelve(self): self.proxy.unshelve_server, method_args=["value"], expected_args=[self.proxy], + expected_kwargs={ + "host": None, + }, + ) + + def test_server_unshelve_with_options(self): + self._verify( + "openstack.compute.v2.server.Server.unshelve", + self.proxy.unshelve_server, + method_args=["value"], + method_kwargs={"host": "HOST2"}, + expected_args=[self.proxy], + expected_kwargs={ + "host": "HOST2", + }, ) def test_server_trigger_dump(self): diff --git a/releasenotes/notes/fix-server-unshelve-to-host-cb02eee8a20ba478.yaml b/releasenotes/notes/fix-server-unshelve-to-host-cb02eee8a20ba478.yaml new file mode 100644 index 000000000..449d34ab8 --- /dev/null +++ b/releasenotes/notes/fix-server-unshelve-to-host-cb02eee8a20ba478.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixed the issue that unshelving a server to a specific host was failed + due to unhandled host option. From e11bb7c3c0f3439ccae95ce59603e83447b3431d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 8 Aug 2024 17:53:17 +0100 Subject: [PATCH 3544/3836] Deprecate 'use_direct_get' parameter This is additional complexity that we don't want to support and won't be able to support once everything in the cloud layer is switched over to use the proxy layer. If users want control over this, they can use the 'get_xxx' methods in the proxy layer instead of the cloud layer. Change-Id: I76e9e2873ad57db673516c2b66f9280f6b0280be Signed-off-by: Stephen Finucane --- openstack/cloud/openstackcloud.py | 11 +++++++++-- openstack/connection.py | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index e54f22b3e..33ef47c17 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -34,6 +34,7 @@ from openstack import proxy from openstack import resource from openstack import utils +from openstack import warnings as os_warnings class _OpenStackCloudMixin(_services_mixin.ServicesMixin): @@ -68,7 +69,7 @@ def __init__( app_version=None, extra_services=None, strict=False, - use_direct_get=False, + use_direct_get=None, task_manager=None, rate_limit=None, oslo_conf=None, @@ -155,6 +156,12 @@ def __init__( """ super().__init__() + if use_direct_get is not None: + warnings.warn( + "The 'use_direct_get' argument is deprecated for removal", + os_warnings.RemovedInSDK50Warning, + ) + self.config = config self._extra_services = {} self._strict_proxies = strict_proxies @@ -196,7 +203,7 @@ def __init__( self._proxies = {} self.__pool_executor = pool_executor self._global_request_id = global_request_id - self.use_direct_get = use_direct_get + self.use_direct_get = use_direct_get or False self.strict_mode = strict self.log = _log.setup_logging('openstack') diff --git a/openstack/connection.py b/openstack/connection.py index 9b4066eea..0b202bf8a 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -362,7 +362,7 @@ def __init__( app_version=None, extra_services=None, strict=False, - use_direct_get=False, + use_direct_get=None, task_manager=None, rate_limit=None, oslo_conf=None, From ec1bb9eef1ef493f5f8e20e65b44934a3b1f51dc Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 19 Aug 2024 14:17:20 +0100 Subject: [PATCH 3545/3836] config: Minimal validation of config files So if you fat-finger a clouds.yaml file and, say, type 'cloud' instead of 'clouds', you'll have breadcrumb explaining *why* cloud 'foo' might not be found. Not that anyone would ever end up in such a situation... Change-Id: I62e78497d44ad7e5c53a0191bf383cb31a6d6d8c Signed-off-by: Stephen Finucane --- openstack/config/loader.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/openstack/config/loader.py b/openstack/config/loader.py index ad4055fa4..bee1a5f16 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -210,15 +210,19 @@ def __init__( # First, use a config file if it exists where expected self.config_filename, self.cloud_config = self._load_config_file() - _, secure_config = self._load_secure_file() + if self.config_filename: + self._validate_config_file(self.config_filename, self.cloud_config) + + secure_config_filename, secure_config = self._load_secure_file() if secure_config: + self._validate_config_file(secure_config_filename, secure_config) self.cloud_config = _util.merge_clouds( self.cloud_config, secure_config ) if not self.cloud_config: self.cloud_config = {'clouds': {}} - if 'clouds' not in self.cloud_config: + elif 'clouds' not in self.cloud_config: self.cloud_config['clouds'] = {} # Save the other config @@ -466,6 +470,25 @@ def _load_yaml_json_file(self, filelist): continue return (None, {}) + def _validate_config_file(self, path: str, data: ty.Any) -> bool: + """Validate config file contains a clouds entry. + + All config files should have a 'clouds' key at a minimum. + """ + if not isinstance(data, dict): + raise exceptions.ConfigException( + 'Configuration file {path} is empty or not a valid mapping' + ) + + if 'clouds' not in data: + # TODO(stephenfin): This should probably be an error at some point + self.log.warning( + "Configuration file %s does not contain a 'clouds' key", path + ) + return False + + return True + def _expand_region_name(self, region_name): return {'name': region_name, 'values': {}} From 604d29bc9b71fd4c78fad325b4ff1a5f0b2a690b Mon Sep 17 00:00:00 2001 From: cid Date: Thu, 15 Aug 2024 15:11:29 +0100 Subject: [PATCH 3546/3836] Add support for the runbooks feature Adds support for managing runbooks within the OpenStack SDK. Related-Change: #922142 Change-Id: Ia590918c2e4bd629724c2e50146a904099858477 --- openstack/baremetal/v1/_common.py | 4 + openstack/baremetal/v1/node.py | 37 +++++++++- openstack/baremetal/v1/runbooks.py | 54 ++++++++++++++ .../tests/unit/baremetal/v1/test_node.py | 32 ++++++++ .../tests/unit/baremetal/v1/test_runbooks.py | 73 +++++++++++++++++++ ...service-via-runbooks-66ca5f6fda681228.yaml | 6 ++ 6 files changed, 203 insertions(+), 3 deletions(-) create mode 100644 openstack/baremetal/v1/runbooks.py create mode 100644 openstack/tests/unit/baremetal/v1/test_runbooks.py create mode 100644 releasenotes/notes/self-service-via-runbooks-66ca5f6fda681228.yaml diff --git a/openstack/baremetal/v1/_common.py b/openstack/baremetal/v1/_common.py index 3eb3c4334..dcfc18e58 100644 --- a/openstack/baremetal/v1/_common.py +++ b/openstack/baremetal/v1/_common.py @@ -94,6 +94,10 @@ FIRMWARE_VERSION = '1.86' """API version in which firmware components of a node can be accessed""" +RUNBOOKS_VERSION = '1.92' +"""API version in which a runbook can be used in place of arbitrary steps +for provisioning""" + class Resource(resource.Resource): base_path: str diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 47c0b48b6..c3e0684d2 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -100,8 +100,8 @@ class Node(_common.Resource): is_maintenance='maintenance', ) - # Ability to have a firmware_interface on a node. - _max_microversion = '1.87' + # Ability to run predefined sets of steps on a node using runbooks. + _max_microversion = '1.92' # Properties #: The UUID of the allocation associated with this node. Added in API @@ -207,9 +207,13 @@ class Node(_common.Resource): #: A string to be used by external schedulers to identify this node as a #: unit of a specific type of resource. Added in API microversion 1.21. resource_class = resource.Body("resource_class") - #: A string represents the current service step being executed upon. + #: A string representing the current service step being executed upon. #: Added in API microversion 1.87. service_step = resource.Body("service_step") + #: A string representing the uuid or logical name of a runbook as an + #: alternative to providing ``clean_steps`` or ``service_steps``. + #: Added in API microversion 1.92. + runbook = resource.Body("runbook") #: A string indicating the shard this node belongs to. Added in API #: microversion 1,82. shard = resource.Body("shard") @@ -407,6 +411,7 @@ def set_provision_state( timeout=None, deploy_steps=None, service_steps=None, + runbook=None, ): """Run an action modifying this node's provision state. @@ -431,6 +436,7 @@ def set_provision_state( and ``rebuild`` target. :param service_steps: Service steps to execute, only valid for ``service`` target. + :param ``runbook``: UUID or logical name of a runbook. :return: This :class:`Node` instance. :raises: ValueError if ``config_drive``, ``clean_steps``, @@ -460,6 +466,31 @@ def set_provision_state( version = self._assert_microversion_for(session, 'commit', version) body = {'target': target} + if runbook: + version = self._assert_microversion_for( + session, 'commit', _common.RUNBOOKS_VERSION + ) + + if clean_steps is not None: + raise ValueError( + 'Please provide either clean steps or a ' + 'runbook, but not both.' + ) + if service_steps is not None: + raise ValueError( + 'Please provide either service steps or a ' + 'runbook, but not both.' + ) + + if target != 'clean' and target != 'service': + msg = ( + 'A runbook can only be provided when setting target ' + 'provision state to any of "[clean, service]"' + ) + raise ValueError(msg) + + body['runbook'] = runbook + if config_drive: if target not in ('active', 'rebuild'): raise ValueError( diff --git a/openstack/baremetal/v1/runbooks.py b/openstack/baremetal/v1/runbooks.py new file mode 100644 index 000000000..e2ae0dc02 --- /dev/null +++ b/openstack/baremetal/v1/runbooks.py @@ -0,0 +1,54 @@ +# 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.baremetal.v1 import _common +from openstack import resource + + +class Runbook(_common.Resource): + resources_key = 'runbooks' + base_path = '/runbooks' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + allow_patch = True + commit_method = 'PATCH' + commit_jsonpatch = True + + _query_mapping = resource.QueryParameters( + 'detail', + fields={'type': _common.fields_type}, + ) + + # Runbooks is available since 1.92 + _max_microversion = '1.92' + name = resource.Body('name') + #: Timestamp at which the runbook was created. + created_at = resource.Body('created_at') + #: A set of one or more arbitrary metadata key and value pairs. + extra = resource.Body('extra') + #: A list of relative links. Includes the self and bookmark links. + links = resource.Body('links', type=list) + #: A set of physical information of the runbook. + steps = resource.Body('steps', type=list) + #: Indicates whether the runbook is publicly accessible. + public = resource.Body('public', type=bool) + #: The name or ID of the project that owns the runbook. + owner = resource.Body('owner', type=str) + #: Timestamp at which the runbook was last updated. + updated_at = resource.Body('updated_at') + #: The UUID of the resource. + id = resource.Body('uuid', alternate_id=True) diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 5e8af118e..f86f33c4f 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -83,6 +83,7 @@ "service_step": {}, "secure_boot": True, "shard": "TestShard", + "runbook": None, "states": [ { "href": "http://127.0.0.1:6385/v1/nodes//states", @@ -161,6 +162,7 @@ def test_instantiate(self): self.assertEqual(FAKE['resource_class'], sot.resource_class) self.assertEqual(FAKE['service_step'], sot.service_step) self.assertEqual(FAKE['secure_boot'], sot.is_secure_boot) + self.assertEqual(FAKE['runbook'], sot.runbook) self.assertEqual(FAKE['states'], sot.states) self.assertEqual( FAKE['target_provision_state'], sot.target_provision_state @@ -438,6 +440,36 @@ def test_set_provision_state_service(self): retriable_status_codes=_common.RETRIABLE_STATUS_CODES, ) + def test_set_provision_state_clean_runbook(self): + runbook = 'CUSTOM_AWESOME' + result = self.node.set_provision_state( + self.session, 'clean', runbook=runbook + ) + + self.assertIs(result, self.node) + self.session.put.assert_called_once_with( + 'nodes/%s/states/provision' % self.node.id, + json={'target': 'clean', 'runbook': runbook}, + headers=mock.ANY, + microversion='1.92', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) + + def test_set_provision_state_service_runbook(self): + runbook = 'CUSTOM_AWESOME' + result = self.node.set_provision_state( + self.session, 'service', runbook=runbook + ) + + self.assertIs(result, self.node) + self.session.put.assert_called_once_with( + 'nodes/%s/states/provision' % self.node.id, + json={'target': 'service', 'runbook': runbook}, + headers=mock.ANY, + microversion='1.92', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) + @mock.patch.object(node.Node, '_translate_response', mock.Mock()) @mock.patch.object(node.Node, '_get_session', lambda self, x: x) diff --git a/openstack/tests/unit/baremetal/v1/test_runbooks.py b/openstack/tests/unit/baremetal/v1/test_runbooks.py new file mode 100644 index 000000000..eed9e73be --- /dev/null +++ b/openstack/tests/unit/baremetal/v1/test_runbooks.py @@ -0,0 +1,73 @@ +# 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.baremetal.v1 import runbooks +from openstack.tests.unit import base + + +FAKE = { + "created_at": "2024-08-18T22:28:48.643434+11:11", + "extra": {}, + "links": [ + { + "href": """http://10.60.253.180:6385/v1/runbooks + /bbb45f41-d4bc-4307-8d1d-32f95ce1e920""", + "rel": "self", + }, + { + "href": """http://10.60.253.180:6385/runbooks + /bbb45f41-d4bc-4307-8d1d-32f95ce1e920""", + "rel": "bookmark", + }, + ], + "name": "CUSTOM_AWESOME", + "public": False, + "owner": "blah", + "steps": [ + { + "args": { + "settings": [{"name": "LogicalProc", "value": "Enabled"}] + }, + "interface": "bios", + "order": 1, + "step": "apply_configuration", + } + ], + "updated_at": None, + "uuid": "32f95ce1-4307-d4bc-8d1d-e920bbb45f41", +} + + +class Runbooks(base.TestCase): + def test_basic(self): + sot = runbooks.Runbook() + self.assertIsNone(sot.resource_key) + self.assertEqual('runbooks', sot.resources_key) + self.assertEqual('/runbooks', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertEqual('PATCH', sot.commit_method) + + def test_instantiate(self): + sot = runbooks.Runbook(**FAKE) + self.assertEqual(FAKE['steps'], sot.steps) + self.assertEqual(FAKE['created_at'], sot.created_at) + self.assertEqual(FAKE['extra'], sot.extra) + self.assertEqual(FAKE['links'], sot.links) + self.assertEqual(FAKE['name'], sot.name) + self.assertEqual(FAKE['public'], sot.public) + self.assertEqual(FAKE['owner'], sot.owner) + self.assertEqual(FAKE['updated_at'], sot.updated_at) + self.assertEqual(FAKE['uuid'], sot.id) diff --git a/releasenotes/notes/self-service-via-runbooks-66ca5f6fda681228.yaml b/releasenotes/notes/self-service-via-runbooks-66ca5f6fda681228.yaml new file mode 100644 index 000000000..87ed2c6c5 --- /dev/null +++ b/releasenotes/notes/self-service-via-runbooks-66ca5f6fda681228.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Adds support for runbooks; an API feature that enables project members + to self-serve maintenance tasks via predefined step lists in lieu of + an arbitrary list of clean/service steps. From 904e361c1ea819ec94f1885ecb7bbb378946b690 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 29 Aug 2024 11:01:54 +0100 Subject: [PATCH 3547/3836] pre-commit: Bump versions ...except for mypy, which has introduced quite a few (66, to be precise) new issues that we need to resolve. We also drop the default language setting: everything is Python 3 nowadays. Change-Id: I7f2a77b6aed22a4977e777979f2f60c2929f2613 Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index db9889ac7..042b41753 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,4 @@ --- -default_language_version: - # force all unspecified python hooks to run python3 - python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 @@ -22,17 +19,17 @@ repos: hooks: - id: doc8 - repo: https://github.com/asottile/pyupgrade - rev: v3.16.0 + rev: v3.17.0 hooks: - id: pyupgrade args: ['--py38-plus'] - repo: https://github.com/psf/black - rev: 24.4.2 + rev: 24.8.0 hooks: - id: black args: ['-S', '-l', '79'] - repo: https://opendev.org/openstack/hacking - rev: 6.1.0 + rev: 7.0.0 hooks: - id: hacking additional_dependencies: From da0667e20157401522a9c5919978f37289be95f1 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 29 Aug 2024 11:24:17 +0100 Subject: [PATCH 3548/3836] pre-commit: Migrate from flake8 to ruff Well, mostly. We still keep our own flake8 hooks and the hacking hooks enabled. Everything else can be handled by ruff. Doing this enables a couple of hacking checks that were previously unaddressed. It also highlights a few cases that flake8 missed. Both are addressed. Change-Id: I642458eacf5bf78fb041f3d8711486fe55441f47 Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 5 +++++ doc/source/conf.py | 1 - openstack/orchestration/v1/_proxy.py | 2 +- openstack/tests/fakes.py | 2 -- .../unit/block_storage/v2/test_volume.py | 1 - .../unit/block_storage/v3/test_volume.py | 1 - .../tests/unit/load_balancer/test_amphora.py | 1 - .../unit/network/v2/test_security_group.py | 1 - .../network/v2/test_security_group_rule.py | 1 - .../v2/test_share_network.py | 1 - tox.ini | 19 +++++-------------- 11 files changed, 11 insertions(+), 24 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 042b41753..54fa3c1a9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,6 +23,11 @@ repos: hooks: - id: pyupgrade args: ['--py38-plus'] + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.6.2 + hooks: + - id: ruff + args: ['--fix'] - repo: https://github.com/psf/black rev: 24.8.0 hooks: diff --git a/doc/source/conf.py b/doc/source/conf.py index b41ba20d9..e73900777 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -13,7 +13,6 @@ import os import sys -import warnings sys.path.insert(0, os.path.abspath('../..')) sys.path.insert(0, os.path.abspath('.')) diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index 696ae1ffa..b4e6a4be6 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -52,7 +52,7 @@ def _extract_name_consume_url_parts(self, url_parts): if ( url_parts[0] == 'stacks' and len(url_parts) > 2 - and not url_parts[2] in ['preview', 'resources'] + and url_parts[2] not in ['preview', 'resources'] ): # orchestrate introduce having stack name and id part of the URL # (/stacks/name/id/everything_else), so if on third position we diff --git a/openstack/tests/fakes.py b/openstack/tests/fakes.py index e3f48c453..ec7fb79f3 100644 --- a/openstack/tests/fakes.py +++ b/openstack/tests/fakes.py @@ -351,8 +351,6 @@ def make_fake_hypervisor(id, name): "topology": {"cores": 1, "threads": 1, "sockets": 4}, }, "current_workload": 0, - "status": "enabled", - "state": "up", "disk_available_least": 0, "host_ip": "1.1.1.1", "free_disk_gb": 1028, diff --git a/openstack/tests/unit/block_storage/v2/test_volume.py b/openstack/tests/unit/block_storage/v2/test_volume.py index 65b6f59a0..3f13ed80a 100644 --- a/openstack/tests/unit/block_storage/v2/test_volume.py +++ b/openstack/tests/unit/block_storage/v2/test_volume.py @@ -55,7 +55,6 @@ "os-volume-replication:extended_status": "really nah", "consistencygroup_id": "123asf-asdf123", "os-volume-replication:driver_data": "ahasadfasdfasdfasdfsdf", - "snapshot_id": "93c2e2aa-7744-4fd6-a31a-80c4726b08d7", "encrypted": "false", "OS-SCH-HNT:scheduler_hints": { "same_host": [ diff --git a/openstack/tests/unit/block_storage/v3/test_volume.py b/openstack/tests/unit/block_storage/v3/test_volume.py index 03d06351d..ebf9e2845 100644 --- a/openstack/tests/unit/block_storage/v3/test_volume.py +++ b/openstack/tests/unit/block_storage/v3/test_volume.py @@ -58,7 +58,6 @@ "os-volume-replication:extended_status": "really nah", "consistencygroup_id": "123asf-asdf123", "os-volume-replication:driver_data": "ahasadfasdfasdfasdfsdf", - "snapshot_id": "93c2e2aa-7744-4fd6-a31a-80c4726b08d7", "encrypted": "false", "OS-SCH-HNT:scheduler_hints": { "same_host": [ diff --git a/openstack/tests/unit/load_balancer/test_amphora.py b/openstack/tests/unit/load_balancer/test_amphora.py index 21324fa3f..323337a3c 100644 --- a/openstack/tests/unit/load_balancer/test_amphora.py +++ b/openstack/tests/unit/load_balancer/test_amphora.py @@ -115,7 +115,6 @@ def test_make_it(self): 'created_at': 'created_at', 'updated_at': 'updated_at', 'image_id': 'image_id', - 'image_id': 'image_id', }, test_amphora._query_mapping._mapping, ) diff --git a/openstack/tests/unit/network/v2/test_security_group.py b/openstack/tests/unit/network/v2/test_security_group.py index 3512b9a06..1597dc9e5 100644 --- a/openstack/tests/unit/network/v2/test_security_group.py +++ b/openstack/tests/unit/network/v2/test_security_group.py @@ -57,7 +57,6 @@ 'revision_number': 3, 'security_group_rules': RULES, 'project_id': '4', - 'project_id': '4', 'updated_at': '2016-10-14T12:16:57.233772', 'tags': ['5'], } diff --git a/openstack/tests/unit/network/v2/test_security_group_rule.py b/openstack/tests/unit/network/v2/test_security_group_rule.py index c9e82c80a..7fc6fbdcb 100644 --- a/openstack/tests/unit/network/v2/test_security_group_rule.py +++ b/openstack/tests/unit/network/v2/test_security_group_rule.py @@ -29,7 +29,6 @@ 'revision_number': 9, 'security_group_id': '10', 'project_id': '11', - 'project_id': '11', 'updated_at': '12', 'remote_address_group_id': '13', } diff --git a/openstack/tests/unit/shared_file_system/v2/test_share_network.py b/openstack/tests/unit/shared_file_system/v2/test_share_network.py index 6287aff0a..4bd76bd4a 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_share_network.py +++ b/openstack/tests/unit/shared_file_system/v2/test_share_network.py @@ -46,7 +46,6 @@ def test_basic(self): "created_before": "created_before", "offset": "offset", "security_service_id": "security_service_id", - "project_id": "project_id", "all_projects": "all_tenants", "name": "name", "description": "description", diff --git a/tox.ini b/tox.ini index 36491ed8d..6ad250499 100644 --- a/tox.ini +++ b/tox.ini @@ -141,21 +141,12 @@ commands = sphinx-build -W --keep-going -b html -j auto releasenotes/source releasenotes/build/html [flake8] -application-import-names = openstack -# The following are ignored on purpose. It's not super worth it to fix them. -# However, if you feel strongly about it, patches will be accepted to fix them -# if they fix ALL of the occurances of one and only one of them. -# E203 Black will put spaces after colons in list comprehensions -# E501 Black takes care of line length for us -# E704 Black will occasionally put multiple statements on one line -# H238 New Style Classes are the default in Python3 +# We only enable the hacking (H) and openstacksdk (O) checks +select = H,O # H301 Black will put commas after imports that can't fit on one line -# H4 Are about docstrings and there's just a huge pile of pre-existing issues. -# W503 Is supposed to be off by default but in the latest pycodestyle isn't. -# Also, both openstacksdk and Donald Knuth disagree with the rule. Line -# breaks should occur before the binary operator for readability. -ignore = E203, E501, E704, H301, H238, H4, W503 -import-order-style = pep8 +# H404 Docstrings don't always start with a newline +# H405 Multiline docstrings are okay +ignore = H301,H403,H404,H405 show-source = True exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build,openstack/_services_mixin.py From f83b657fc32189f238e4d3640a180284b28e1aaa Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 29 Aug 2024 11:32:48 +0100 Subject: [PATCH 3549/3836] trivial: Remove unnecessary trailing comma Change-Id: Idab313f503c15d2aeec2f82db624e52ddea9e4bf Signed-off-by: Stephen Finucane --- openstack/tests/unit/cloud/test_create_server.py | 2 +- openstack/tests/unit/load_balancer/test_pool.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openstack/tests/unit/cloud/test_create_server.py b/openstack/tests/unit/cloud/test_create_server.py index e7712ffb3..59f0c4a08 100644 --- a/openstack/tests/unit/cloud/test_create_server.py +++ b/openstack/tests/unit/cloud/test_create_server.py @@ -709,7 +709,7 @@ def test_create_server_wait(self, mock_wait): dict(id='image-id'), dict(id='flavor-id'), wait=True, - ), + ) # This is a pretty dirty hack to ensure we in principle use object with # expected properties diff --git a/openstack/tests/unit/load_balancer/test_pool.py b/openstack/tests/unit/load_balancer/test_pool.py index ea7c325ea..53bc70dc6 100644 --- a/openstack/tests/unit/load_balancer/test_pool.py +++ b/openstack/tests/unit/load_balancer/test_pool.py @@ -65,7 +65,7 @@ def test_basic(self): def test_make_it(self): test_pool = pool.Pool(**EXAMPLE) - self.assertEqual(EXAMPLE['name'], test_pool.name), + self.assertEqual(EXAMPLE['name'], test_pool.name) self.assertEqual(EXAMPLE['description'], test_pool.description) self.assertEqual( EXAMPLE['admin_state_up'], test_pool.is_admin_state_up From 187e88a5593e4f475379738811043ee4528a7df3 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 29 Aug 2024 11:29:24 +0100 Subject: [PATCH 3550/3836] pre-commit: Migrate from black to ruff format This is mostly a case of adding trailing commas and rewrapping things to fit on one line. Change-Id: Iaabb38970e8077c3cfd19041c39e3d40d2d93ffa Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 6 +- openstack/_hacking/checks.py | 2 +- openstack/accelerator/v2/deployable.py | 2 +- openstack/cloud/_accelerator.py | 1 - openstack/cloud/_baremetal.py | 33 ++- openstack/cloud/_block_storage.py | 61 +----- openstack/cloud/_coe.py | 15 +- openstack/cloud/_compute.py | 26 +-- openstack/cloud/_dns.py | 1 - openstack/cloud/_identity.py | 78 +------ openstack/cloud/_image.py | 8 +- openstack/cloud/_network.py | 190 ++++-------------- openstack/cloud/_network_common.py | 22 +- openstack/cloud/_object_store.py | 1 - openstack/cloud/_orchestration.py | 5 +- openstack/cloud/_shared_file_system.py | 1 - openstack/cloud/_utils.py | 7 +- openstack/cloud/exc.py | 2 +- openstack/cloud/openstackcloud.py | 2 + openstack/common/quota_set.py | 4 +- openstack/compute/v2/limits.py | 4 +- openstack/compute/v2/server_ip.py | 2 +- openstack/config/__init__.py | 2 +- openstack/config/cloud_region.py | 5 +- openstack/connection.py | 2 +- openstack/identity/v3/project.py | 2 +- openstack/image/v2/image.py | 6 +- openstack/load_balancer/v2/_proxy.py | 2 +- openstack/load_balancer/v2/health_monitor.py | 2 +- openstack/load_balancer/v2/l7_policy.py | 2 +- openstack/load_balancer/v2/l7_rule.py | 2 +- openstack/load_balancer/v2/listener.py | 2 +- openstack/load_balancer/v2/load_balancer.py | 2 +- openstack/load_balancer/v2/member.py | 2 +- openstack/load_balancer/v2/pool.py | 2 +- openstack/network/v2/_base.py | 2 +- openstack/network/v2/floating_ip.py | 2 +- openstack/network/v2/network.py | 2 +- openstack/network/v2/port.py | 2 +- openstack/network/v2/qos_policy.py | 2 +- openstack/network/v2/router.py | 2 +- openstack/network/v2/security_group.py | 2 +- openstack/network/v2/security_group_rule.py | 2 +- openstack/network/v2/subnet.py | 2 +- openstack/network/v2/subnet_pool.py | 2 +- openstack/network/v2/trunk.py | 2 +- openstack/orchestration/v1/_proxy.py | 4 +- .../v2/share_access_rule.py | 2 +- .../tests/functional/cloud/test_devstack.py | 1 + .../functional/cloud/test_floating_ip_pool.py | 1 - .../tests/functional/cloud/test_keypairs.py | 1 + .../tests/functional/cloud/test_limits.py | 1 + .../tests/functional/cloud/test_project.py | 1 + .../tests/functional/cloud/test_recordset.py | 1 + .../functional/cloud/test_volume_type.py | 1 + .../image/v2/test_metadef_property.py | 2 +- .../load_balancer/v2/test_load_balancer.py | 2 +- .../functional/network/v2/test_floating_ip.py | 2 +- .../network/v2/test_port_forwarding.py | 2 +- .../test_quota_class_set.py | 2 +- .../tests/unit/block_storage/v3/test_type.py | 2 +- .../tests/unit/cloud/test_baremetal_node.py | 10 +- .../tests/unit/cloud/test_delete_server.py | 1 + openstack/tests/unit/cloud/test_fwaas.py | 6 +- openstack/tests/unit/config/test_config.py | 2 +- openstack/tests/unit/config/test_from_conf.py | 2 +- .../tests/unit/object_store/v1/test_proxy.py | 4 +- openstack/tests/unit/test_connection.py | 8 +- pyproject.toml | 6 + 69 files changed, 161 insertions(+), 431 deletions(-) create mode 100644 pyproject.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 54fa3c1a9..581e10a54 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,11 +28,7 @@ repos: hooks: - id: ruff args: ['--fix'] - - repo: https://github.com/psf/black - rev: 24.8.0 - hooks: - - id: black - args: ['-S', '-l', '79'] + - id: ruff-format - repo: https://opendev.org/openstack/hacking rev: 7.0.0 hooks: diff --git a/openstack/_hacking/checks.py b/openstack/_hacking/checks.py index df0e7d4ea..9bae7e45c 100644 --- a/openstack/_hacking/checks.py +++ b/openstack/_hacking/checks.py @@ -61,5 +61,5 @@ def assert_no_deprecated_exceptions(logical_line, filename): 'OpenStackCloudURINotFound', 'OpenStackCloudResourceNotFound', ): - if re.search(fr'\b{exception}\b', logical_line): + if re.search(rf'\b{exception}\b', logical_line): yield (0, 'O310: Use of deprecated Exception class') diff --git a/openstack/accelerator/v2/deployable.py b/openstack/accelerator/v2/deployable.py index 889853e40..27873937e 100644 --- a/openstack/accelerator/v2/deployable.py +++ b/openstack/accelerator/v2/deployable.py @@ -73,7 +73,7 @@ def _commit( json=request.body, headers=request.headers, microversion=microversion, - **kwargs + **kwargs, ) self.microversion = microversion self._translate_response(response, has_body=has_body) diff --git a/openstack/cloud/_accelerator.py b/openstack/cloud/_accelerator.py index effd65c47..9944147d2 100644 --- a/openstack/cloud/_accelerator.py +++ b/openstack/cloud/_accelerator.py @@ -14,7 +14,6 @@ class AcceleratorCloudMixin(openstackcloud._OpenStackCloudMixin): - def list_deployables(self, filters=None): """List all available deployables. diff --git a/openstack/cloud/_baremetal.py b/openstack/cloud/_baremetal.py index 277d36135..e9eee152b 100644 --- a/openstack/cloud/_baremetal.py +++ b/openstack/cloud/_baremetal.py @@ -42,7 +42,6 @@ def _normalize_port_list(nics): class BaremetalCloudMixin(openstackcloud._OpenStackCloudMixin): - def list_nics(self): """Return a list of all bare metal ports.""" return list(self.baremetal.ports(details=True)) @@ -187,7 +186,7 @@ def register_machine( timeout=3600, lock_timeout=600, provision_state='available', - **kwargs + **kwargs, ): """Register Baremetal with Ironic @@ -208,10 +207,7 @@ def register_machine( Example:: - [ - {'address': 'aa:bb:cc:dd:ee:01'}, - {'address': 'aa:bb:cc:dd:ee:02'} - ] + [{'address': 'aa:bb:cc:dd:ee:01'}, {'address': 'aa:bb:cc:dd:ee:02'}] Alternatively, you can provide an array of MAC addresses. :param wait: Boolean value, defaulting to false, to wait for the node @@ -352,21 +348,16 @@ def patch_machine(self, name_or_id, patch): Example patch construction:: - patch=[] - patch.append({ - 'op': 'remove', - 'path': '/instance_info' - }) - patch.append({ - 'op': 'replace', - 'path': '/name', - 'value': 'newname' - }) - patch.append({ - 'op': 'add', - 'path': '/driver_info/username', - 'value': 'administrator' - }) + patch = [] + patch.append({'op': 'remove', 'path': '/instance_info'}) + patch.append({'op': 'replace', 'path': '/name', 'value': 'newname'}) + patch.append( + { + 'op': 'add', + 'path': '/driver_info/username', + 'value': 'administrator', + } + ) :returns: Current state of the node. :rtype: :class:`~openstack.baremetal.v1.node.Node`. diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index 72fe26405..df959b722 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -19,7 +19,6 @@ class BlockStorageCloudMixin(openstackcloud._OpenStackCloudMixin): - # TODO(stephenfin): Remove 'cache' in a future major version def list_volumes(self, cache=True): """List all available volumes. @@ -59,12 +58,7 @@ def get_volume(self, name_or_id, filters=None): further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR @@ -94,12 +88,7 @@ def get_volume_type(self, name_or_id, filters=None): further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR @@ -467,12 +456,7 @@ def get_volume_snapshot(self, name_or_id, filters=None): further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR @@ -542,12 +526,7 @@ def get_volume_backup(self, name_or_id, filters=None): further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR @@ -570,7 +549,7 @@ def list_volume_snapshots(self, detailed=True, filters=None): { 'name': 'my-volume-snapshot', 'volume_id': 'e126044c-7b4c-43be-a32a-c9cbbc9ddb56', - 'all_tenants': 1 + 'all_tenants': 1, } :returns: A list of volume ``Snapshot`` objects. @@ -590,7 +569,7 @@ def list_volume_backups(self, detailed=True, filters=None): 'name': 'my-volume-backup', 'status': 'available', 'volume_id': 'e126044c-7b4c-43be-a32a-c9cbbc9ddb56', - 'all_tenants': 1 + 'all_tenants': 1, } :returns: A list of volume ``Backup`` objects. @@ -671,12 +650,7 @@ def search_volumes(self, name_or_id=None, filters=None): further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR @@ -698,12 +672,7 @@ def search_volume_snapshots(self, name_or_id=None, filters=None): further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR @@ -725,12 +694,7 @@ def search_volume_backups(self, name_or_id=None, filters=None): further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR @@ -758,12 +722,7 @@ def search_volume_types( further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR diff --git a/openstack/cloud/_coe.py b/openstack/cloud/_coe.py index c64a96c01..01fa92f8b 100644 --- a/openstack/cloud/_coe.py +++ b/openstack/cloud/_coe.py @@ -16,7 +16,6 @@ class CoeCloudMixin(openstackcloud._OpenStackCloudMixin): - def list_coe_clusters(self): """List COE (Container Orchestration Engine) cluster. @@ -51,12 +50,7 @@ def get_coe_cluster(self, name_or_id, filters=None): A dictionary of meta data to use for further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR A string containing a jmespath expression for further filtering. @@ -207,12 +201,7 @@ def get_cluster_template(self, name_or_id, filters=None, detail=False): A dictionary of meta data to use for further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR A string containing a jmespath expression for further filtering. diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index da58282cf..0b9267def 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -69,7 +69,6 @@ def _pop_or_get(resource, key, default, strict): class ComputeCloudMixin(_network_common.NetworkCommonCloudMixin): - @property def _compute_region(self): # This is only used in exception messages. Can we get rid of it? @@ -398,12 +397,7 @@ def get_keypair(self, name_or_id, filters=None): Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR A string containing a jmespath expression for further filtering. @@ -421,12 +415,7 @@ def get_flavor(self, name_or_id, filters=None, get_extra=True): Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR A string containing a jmespath expression for further filtering. @@ -505,12 +494,7 @@ def get_server( A dictionary of meta data to use for further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR A string containing a jmespath expression for further filtering. @@ -1640,9 +1624,7 @@ def get_aggregate(self, name_or_id, filters=None): { 'availability_zone': 'nova', - 'metadata': { - 'cpu_allocation_ratio': '1.0' - } + 'metadata': {'cpu_allocation_ratio': '1.0'}, } :returns: An aggregate dict or None if no matching aggregate is diff --git a/openstack/cloud/_dns.py b/openstack/cloud/_dns.py index 4864a1036..d495b8689 100644 --- a/openstack/cloud/_dns.py +++ b/openstack/cloud/_dns.py @@ -17,7 +17,6 @@ class DnsCloudMixin(openstackcloud._OpenStackCloudMixin): - def list_zones(self, filters=None): """List all available zones. diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index 8973266f3..eb78bb2ed 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -20,7 +20,6 @@ class IdentityCloudMixin(openstackcloud._OpenStackCloudMixin): - def _get_project_id_param_dict(self, name_or_id): if name_or_id: project = self.get_project(name_or_id) @@ -73,12 +72,7 @@ def list_projects(self, domain_id=None, name_or_id=None, filters=None): Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR @@ -114,12 +108,7 @@ def search_projects(self, name_or_id=None, filters=None, domain_id=None): Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR @@ -141,12 +130,7 @@ def get_project(self, name_or_id, filters=None, domain_id=None): further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR @@ -264,12 +248,7 @@ def search_users(self, name_or_id=None, filters=None, domain_id=None): Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR @@ -304,12 +283,7 @@ def get_user(self, name_or_id, filters=None, **kwargs): further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR @@ -506,12 +480,7 @@ def search_services(self, name_or_id=None, filters=None): Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR @@ -675,12 +644,7 @@ def search_endpoints(self, id=None, filters=None): Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR @@ -837,12 +801,7 @@ def search_domains(self, filters=None, name_or_id=None): Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR @@ -875,12 +834,7 @@ def get_domain(self, domain_id=None, name_or_id=None, filters=None): further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR @@ -919,12 +873,7 @@ def search_groups(self, name_or_id=None, filters=None, **kwargs): Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR @@ -1055,12 +1004,7 @@ def search_roles(self, name_or_id=None, filters=None): Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py index 95f5f238d..00b232321 100644 --- a/openstack/cloud/_image.py +++ b/openstack/cloud/_image.py @@ -17,7 +17,6 @@ class ImageCloudMixin(openstackcloud._OpenStackCloudMixin): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -65,12 +64,7 @@ def get_image(self, name_or_id, filters=None): Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR A string containing a jmespath expression for further filtering. diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index f8ddd5f54..1abd9a436 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -17,7 +17,6 @@ class NetworkCloudMixin(_network_common.NetworkCommonCloudMixin): - def _neutron_extensions(self): extensions = set() for extension in self.network.extensions(): @@ -180,12 +179,7 @@ def get_qos_policy(self, name_or_id, filters=None): Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR A string containing a jmespath expression for further filtering. @@ -254,12 +248,7 @@ def get_qos_rule_type_details(self, rule_type, filters=None): Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR A string containing a jmespath expression for further filtering. @@ -304,12 +293,7 @@ def get_network(self, name_or_id, filters=None): Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR A string containing a jmespath expression for further filtering. @@ -340,12 +324,7 @@ def get_router(self, name_or_id, filters=None): Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR A string containing a jmespath expression for further filtering. @@ -368,12 +347,7 @@ def get_subnet(self, name_or_id, filters=None): Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} :returns: A network ``Subnet`` object if found, else None. """ @@ -400,12 +374,7 @@ def get_port(self, name_or_id, filters=None): Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR A string containing a jmespath expression for further filtering. @@ -459,7 +428,7 @@ def create_network( :param bool external: Whether this network is externally accessible. :param dict provider: A dict of network provider options. Example:: - { 'network_type': 'vlan', 'segmentation_id': 'vlan1' } + {'network_type': 'vlan', 'segmentation_id': 'vlan1'} :param string project_id: Specify the project ID this network will be created on (admin-only). @@ -566,7 +535,7 @@ def update_network(self, name_or_id, **kwargs): :param bool external: Whether this network is externally accessible. :param dict provider: A dict of network provider options. Example:: - { 'network_type': 'vlan', 'segmentation_id': 'vlan1' } + {'network_type': 'vlan', 'segmentation_id': 'vlan1'} :param int mtu_size: New maximum transmission unit value to address fragmentation. Minimum value is 68 for IPv4, and 1280 for IPv6. @@ -753,12 +722,7 @@ def delete_firewall_rule(self, name_or_id, filters=None): Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR A string containing a jmespath expression for further filtering. @@ -794,12 +758,7 @@ def get_firewall_rule(self, name_or_id, filters=None): Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR A string containing a jmespath expression for further filtering. @@ -822,12 +781,7 @@ def list_firewall_rules(self, filters=None): Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR A string containing a jmespath expression for further filtering. @@ -865,12 +819,7 @@ def update_firewall_rule(self, name_or_id, filters=None, **kwargs): Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR A string containing a jmespath expression for further filtering. @@ -957,12 +906,7 @@ def delete_firewall_policy(self, name_or_id, filters=None): Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR A string containing a jmespath expression for further filtering. @@ -998,12 +942,7 @@ def get_firewall_policy(self, name_or_id, filters=None): Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR A string containing a jmespath expression for further filtering. @@ -1026,12 +965,7 @@ def list_firewall_policies(self, filters=None): Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR A string containing a jmespath expression for further filtering. @@ -1061,12 +995,7 @@ def update_firewall_policy(self, name_or_id, filters=None, **kwargs): Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR A string containing a jmespath expression for further filtering. @@ -2176,7 +2105,7 @@ def create_router( [ { "subnet_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b", - "ip_address": "192.168.10.2" + "ip_address": "192.168.10.2", } ] @@ -2237,7 +2166,7 @@ def update_router( [ { "subnet_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b", - "ip_address": "192.168.10.2" + "ip_address": "192.168.10.2", } ] @@ -2247,12 +2176,7 @@ def update_router( Example:: - [ - { - "destination": "179.24.1.0/24", - "nexthop": "172.24.3.99" - } - ] + [{"destination": "179.24.1.0/24", "nexthop": "172.24.3.99"}] :returns: The updated network ``Router`` object. :raises: :class:`~openstack.exceptions.SDKException` on operation @@ -2346,12 +2270,7 @@ def create_subnet( :param allocation_pools: A list of dictionaries of the start and end addresses for the allocation pools. For example:: - [ - { - "start": "192.168.199.2", - "end": "192.168.199.254" - } - ] + [{"start": "192.168.199.2", "end": "192.168.199.254"}] :param string gateway_ip: The gateway IP address. When you specify both allocation_pools and gateway_ip, you must ensure that the gateway @@ -2362,20 +2281,14 @@ def create_subnet( :param dns_nameservers: A list of DNS name servers for the subnet. For example:: - [ "8.8.8.7", "8.8.8.8" ] + ["8.8.8.7", "8.8.8.8"] :param host_routes: A list of host route dictionaries for the subnet. For example:: [ - { - "destination": "0.0.0.0/0", - "nexthop": "123.456.78.9" - }, - { - "destination": "192.168.0.0/24", - "nexthop": "192.168.0.1" - } + {"destination": "0.0.0.0/0", "nexthop": "123.456.78.9"}, + {"destination": "192.168.0.0/24", "nexthop": "192.168.0.1"}, ] :param string ipv6_ra_mode: IPv6 Router Advertisement mode. Valid @@ -2536,30 +2449,19 @@ def update_subnet( :param allocation_pools: A list of dictionaries of the start and end addresses for the allocation pools. For example:: - [ - { - "start": "192.168.199.2", - "end": "192.168.199.254" - } - ] + [{"start": "192.168.199.2", "end": "192.168.199.254"}] :param dns_nameservers: A list of DNS name servers for the subnet. For example:: - [ "8.8.8.7", "8.8.8.8" ] + ["8.8.8.7", "8.8.8.8"] :param host_routes: A list of host route dictionaries for the subnet. For example:: [ - { - "destination": "0.0.0.0/0", - "nexthop": "123.456.78.9" - }, - { - "destination": "192.168.0.0/24", - "nexthop": "192.168.0.1" - } + {"destination": "0.0.0.0/0", "nexthop": "123.456.78.9"}, + {"destination": "192.168.0.0/24", "nexthop": "192.168.0.1"}, ] :returns: The updated network ``Subnet`` object. @@ -2636,8 +2538,9 @@ def create_port(self, network_id, **kwargs): [ { "ip_address": "10.29.29.13", - "subnet_id": "a78484c4-c380-4b47-85aa-21c51a2d8cbd" - }, ... + "subnet_id": "a78484c4-c380-4b47-85aa-21c51a2d8cbd", + }, + ..., ] :param subnet_id: If you specify only a subnet ID, OpenStack Networking @@ -2651,22 +2554,12 @@ def create_port(self, network_id, **kwargs): :param allowed_address_pairs: Allowed address pairs list (Optional) For example:: - [ - { - "ip_address": "23.23.23.1", - "mac_address": "fa:16:3e:c4:cd:3f" - }, ... - ] + [{"ip_address": "23.23.23.1", "mac_address": "fa:16:3e:c4:cd:3f"}, ...] :param extra_dhcp_opts: Extra DHCP options. (Optional). For example:: - [ - { - "opt_name": "opt name1", - "opt_value": "value1" - }, ... - ] + [{"opt_name": "opt name1", "opt_value": "value1"}, ...] :param device_owner: The ID of the entity that uses this port. For example, a DHCP agent. (Optional) @@ -2731,30 +2624,21 @@ def update_port(self, name_or_id, **kwargs): [ { "ip_address": "10.29.29.13", - "subnet_id": "a78484c4-c380-4b47-85aa-21c51a2d8cbd" - }, ... + "subnet_id": "a78484c4-c380-4b47-85aa-21c51a2d8cbd", + }, + ..., ] :param security_groups: List of security group UUIDs. (Optional) :param allowed_address_pairs: Allowed address pairs list (Optional) For example:: - [ - { - "ip_address": "23.23.23.1", - "mac_address": "fa:16:3e:c4:cd:3f" - }, ... - ] + [{"ip_address": "23.23.23.1", "mac_address": "fa:16:3e:c4:cd:3f"}, ...] :param extra_dhcp_opts: Extra DHCP options. (Optional). For example:: - [ - { - "opt_name": "opt name1", - "opt_value": "value1" - }, ... - ] + [{"opt_name": "opt name1", "opt_value": "value1"}, ...] :param device_owner: The ID of the entity that uses this port. For example, a DHCP agent. (Optional) diff --git a/openstack/cloud/_network_common.py b/openstack/cloud/_network_common.py index 43fce7a22..43633d7cd 100644 --- a/openstack/cloud/_network_common.py +++ b/openstack/cloud/_network_common.py @@ -458,12 +458,7 @@ def get_floating_ip(self, id, filters=None): A dictionary of meta data to use for further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR A string containing a jmespath expression for further filtering. @@ -1177,8 +1172,7 @@ def _neutron_detach_ip_from_server(self, server_id, floating_ip_id): self.network.update_ip(floating_ip_id, port_id=None) except exceptions.SDKException: raise exceptions.SDKException( - "Error detaching IP {ip} from " - "server {server_id}".format( + "Error detaching IP {ip} from server {server_id}".format( ip=floating_ip_id, server_id=server_id ) ) @@ -1647,8 +1641,9 @@ def _normalize_floating_ips(self, ips): "floating_ip_address": "198.51.100.10", "network": "this-is-a-net-or-pool-id", "attached": True, - "status": "ACTIVE" - }, ... + "status": "ACTIVE", + }, + ..., ] """ @@ -1769,12 +1764,7 @@ def get_security_group(self, name_or_id, filters=None): A dictionary of meta data to use for further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR A string containing a jmespath expression for further filtering. diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index c16baa7b6..3d95d799e 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -26,7 +26,6 @@ class ObjectStoreCloudMixin(openstackcloud._OpenStackCloudMixin): - # TODO(stephenfin): Remove 'full_listing' as it's a noop def list_containers(self, full_listing=True, prefix=None): """List containers. diff --git a/openstack/cloud/_orchestration.py b/openstack/cloud/_orchestration.py index 5be0c9918..ead46cc93 100644 --- a/openstack/cloud/_orchestration.py +++ b/openstack/cloud/_orchestration.py @@ -17,7 +17,6 @@ class OrchestrationCloudMixin(openstackcloud._OpenStackCloudMixin): - def get_template_contents( self, template_file=None, @@ -44,7 +43,7 @@ def create_stack( wait=False, timeout=3600, environment_files=None, - **parameters + **parameters, ): """Create a stack. @@ -101,7 +100,7 @@ def update_stack( wait=False, timeout=3600, environment_files=None, - **parameters + **parameters, ): """Update a stack. diff --git a/openstack/cloud/_shared_file_system.py b/openstack/cloud/_shared_file_system.py index 61affe547..7f552d46d 100644 --- a/openstack/cloud/_shared_file_system.py +++ b/openstack/cloud/_shared_file_system.py @@ -14,7 +14,6 @@ class SharedFileSystemCloudMixin(openstackcloud._OpenStackCloudMixin): - def list_share_availability_zones(self): """List all availability zones for the Shared File Systems service. diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index 57830f771..a4f5ebc1f 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -47,12 +47,7 @@ def _filter_list(data, name_or_id, filters): :param filters: A dictionary of meta data to use for further filtering. Elements of this dictionary may, themselves, be dictionaries. Example:: - { - 'last_name': 'Smith', - 'other': { - 'gender': 'Female' - } - } + {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR diff --git a/openstack/cloud/exc.py b/openstack/cloud/exc.py index 2c9e6be87..9c5ddfe66 100644 --- a/openstack/cloud/exc.py +++ b/openstack/cloud/exc.py @@ -33,7 +33,7 @@ def __init__(self, resource, resource_id, extra_data=None, **kwargs): resource=resource, resource_id=resource_id ), extra_data=extra_data, - **kwargs + **kwargs, ) self.resource_id = resource_id diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 33ef47c17..a8c8e22c1 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -302,6 +302,7 @@ def global_request(self, global_request_id): .. code-block:: python from oslo_context import context + cloud = openstack.connect(cloud='example') # Work normally servers = cloud.list_servers() @@ -314,6 +315,7 @@ def global_request(self, global_request_id): .. code-block:: python from oslo_context import context + c = openstack.connect(cloud='example') # Work normally servers = c.list_servers() diff --git a/openstack/common/quota_set.py b/openstack/common/quota_set.py index 1c7a147af..a7bd86b5c 100644 --- a/openstack/common/quota_set.py +++ b/openstack/common/quota_set.py @@ -55,14 +55,14 @@ def fetch( requires_id=False, base_path=None, error_message=None, - **params + **params, ): return super().fetch( session, requires_id=False, base_path=base_path, error_message=error_message, - **params + **params, ) def _translate_response(self, response, has_body=None, error_message=None): diff --git a/openstack/compute/v2/limits.py b/openstack/compute/v2/limits.py index 8ff7838a6..86946bf3a 100644 --- a/openstack/compute/v2/limits.py +++ b/openstack/compute/v2/limits.py @@ -124,7 +124,7 @@ def fetch( error_message=None, base_path=None, skip_cache=False, - **params + **params, ): """Get the Limits resource. @@ -142,5 +142,5 @@ def fetch( error_message=error_message, base_path=base_path, skip_cache=skip_cache, - **params + **params, ) diff --git a/openstack/compute/v2/server_ip.py b/openstack/compute/v2/server_ip.py index df85e0687..8bac8d10b 100644 --- a/openstack/compute/v2/server_ip.py +++ b/openstack/compute/v2/server_ip.py @@ -39,7 +39,7 @@ def list( server_id=None, network_label=None, base_path=None, - **params + **params, ): if base_path is None: base_path = cls.base_path diff --git a/openstack/config/__init__.py b/openstack/config/__init__.py index c7950dd0c..3e118b719 100644 --- a/openstack/config/__init__.py +++ b/openstack/config/__init__.py @@ -24,7 +24,7 @@ def get_cloud_region( app_version=None, load_yaml_config=True, load_envvars=True, - **kwargs + **kwargs, ): config = OpenStackConfig( load_yaml_config=load_yaml_config, diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 83f7b4eb4..3b7cac69d 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -218,8 +218,9 @@ def from_conf(conf, session=None, service_types=None, **kwargs): ) ) _logger.warning( - "Disabling service '{service_type}': " - "{reason}".format(service_type=st, reason=reason) + "Disabling service '{service_type}': {reason}".format( + service_type=st, reason=reason + ) ) _disable_service(config_dict, st, reason=reason) continue diff --git a/openstack/connection.py b/openstack/connection.py index 0b202bf8a..a0eecac6e 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -87,7 +87,7 @@ 'username': 'amazing-user', 'password': 'super-secret-password', 'project_id': '33aa1afc-03fe-43b8-8201-4e0d3b4b8ab5', - 'user_domain_id': '054abd68-9ad9-418b-96d3-3437bb376703' + 'user_domain_id': '054abd68-9ad9-418b-96d3-3437bb376703', }, compute_api_version='2', identity_interface='internal', diff --git a/openstack/identity/v3/project.py b/openstack/identity/v3/project.py index 7b08e3e53..677ce35d7 100644 --- a/openstack/identity/v3/project.py +++ b/openstack/identity/v3/project.py @@ -35,7 +35,7 @@ class Project(resource.Resource, tag.TagMixin): 'name', 'parent_id', is_enabled='enabled', - **tag.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters, ) # Properties diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index 3d1332c55..d84eaef01 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -349,9 +349,9 @@ def import_image( if remote_region and remote_image_id: if remote_service_interface: - data['method'][ - 'glance_service_interface' - ] = remote_service_interface + data['method']['glance_service_interface'] = ( + remote_service_interface + ) data['method']['glance_region'] = remote_region data['method']['glance_image_id'] = remote_image_id diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index 28cebdbb4..ff5947819 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -1188,7 +1188,7 @@ def update_availability_zone_profile( return self._update( _availability_zone_profile.AvailabilityZoneProfile, availability_zone_profile, - **attrs + **attrs, ) def create_availability_zone(self, **attrs): diff --git a/openstack/load_balancer/v2/health_monitor.py b/openstack/load_balancer/v2/health_monitor.py index 6622c775f..9741b285b 100644 --- a/openstack/load_balancer/v2/health_monitor.py +++ b/openstack/load_balancer/v2/health_monitor.py @@ -42,7 +42,7 @@ class HealthMonitor(resource.Resource, tag.TagMixin): 'type', 'url_path', is_admin_state_up='admin_state_up', - **tag.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters, ) #: Properties diff --git a/openstack/load_balancer/v2/l7_policy.py b/openstack/load_balancer/v2/l7_policy.py index 35d2917dc..130ada839 100644 --- a/openstack/load_balancer/v2/l7_policy.py +++ b/openstack/load_balancer/v2/l7_policy.py @@ -38,7 +38,7 @@ class L7Policy(resource.Resource, tag.TagMixin): 'redirect_prefix', 'project_id', is_admin_state_up='admin_state_up', - **tag.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters, ) #: Properties diff --git a/openstack/load_balancer/v2/l7_rule.py b/openstack/load_balancer/v2/l7_rule.py index 188f878ef..458c8000e 100644 --- a/openstack/load_balancer/v2/l7_rule.py +++ b/openstack/load_balancer/v2/l7_rule.py @@ -38,7 +38,7 @@ class L7Rule(resource.Resource, tag.TagMixin): 'operating_status', is_admin_state_up='admin_state_up', l7_policy_id='l7policy_id', - **tag.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters, ) #: Properties diff --git a/openstack/load_balancer/v2/listener.py b/openstack/load_balancer/v2/listener.py index 0d29a1528..29e50f80e 100644 --- a/openstack/load_balancer/v2/listener.py +++ b/openstack/load_balancer/v2/listener.py @@ -53,7 +53,7 @@ class Listener(resource.Resource, tag.TagMixin): is_hsts_include_subdomains='hsts_include_subdomains', is_hsts_preload='hsts_preload', is_admin_state_up='admin_state_up', - **tag.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters, ) # Properties diff --git a/openstack/load_balancer/v2/load_balancer.py b/openstack/load_balancer/v2/load_balancer.py index 0fddea5fa..a23708f17 100644 --- a/openstack/load_balancer/v2/load_balancer.py +++ b/openstack/load_balancer/v2/load_balancer.py @@ -40,7 +40,7 @@ class LoadBalancer(resource.Resource, tag.TagMixin): 'operating_status', 'availability_zone', is_admin_state_up='admin_state_up', - **tag.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters, ) # Properties diff --git a/openstack/load_balancer/v2/member.py b/openstack/load_balancer/v2/member.py index 4e1d9423e..a17c93161 100644 --- a/openstack/load_balancer/v2/member.py +++ b/openstack/load_balancer/v2/member.py @@ -40,7 +40,7 @@ class Member(resource.Resource, tag.TagMixin): 'monitor_port', 'backup', is_admin_state_up='admin_state_up', - **tag.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters, ) # Properties diff --git a/openstack/load_balancer/v2/pool.py b/openstack/load_balancer/v2/pool.py index d736548f2..09becab86 100644 --- a/openstack/load_balancer/v2/pool.py +++ b/openstack/load_balancer/v2/pool.py @@ -45,7 +45,7 @@ class Pool(resource.Resource, tag.TagMixin): 'ca_tls_container_ref', 'crl_container_ref', is_admin_state_up='admin_state_up', - **tag.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters, ) #: Properties diff --git a/openstack/network/v2/_base.py b/openstack/network/v2/_base.py index f0ef10e46..9ca01747e 100644 --- a/openstack/network/v2/_base.py +++ b/openstack/network/v2/_base.py @@ -26,7 +26,7 @@ def _prepare_request( base_path=None, params=None, if_revision=None, - **kwargs + **kwargs, ): req = super()._prepare_request( requires_id=requires_id, diff --git a/openstack/network/v2/floating_ip.py b/openstack/network/v2/floating_ip.py index e2a2c3df3..cb75e7a47 100644 --- a/openstack/network/v2/floating_ip.py +++ b/openstack/network/v2/floating_ip.py @@ -43,7 +43,7 @@ class FloatingIP(_base.NetworkResource, tag.TagMixin): 'sort_key', 'sort_dir', tenant_id='project_id', - **tag.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters, ) # Properties diff --git a/openstack/network/v2/network.py b/openstack/network/v2/network.py index 8382e0e5f..a894668b0 100644 --- a/openstack/network/v2/network.py +++ b/openstack/network/v2/network.py @@ -43,7 +43,7 @@ class Network(_base.NetworkResource, tag.TagMixin): provider_network_type='provider:network_type', provider_physical_network='provider:physical_network', provider_segmentation_id='provider:segmentation_id', - **tag.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters, ) # Properties diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index d85427576..aa01260a9 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -53,7 +53,7 @@ class Port(_base.NetworkResource, tag.TagMixin): is_admin_state_up='admin_state_up', is_port_security_enabled='port_security_enabled', security_group_ids='security_groups', - **tag.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters, ) # Properties diff --git a/openstack/network/v2/qos_policy.py b/openstack/network/v2/qos_policy.py index 3442be891..5f75f1990 100644 --- a/openstack/network/v2/qos_policy.py +++ b/openstack/network/v2/qos_policy.py @@ -36,7 +36,7 @@ class QoSPolicy(resource.Resource, tag.TagMixin): 'sort_key', 'sort_dir', is_shared='shared', - **tag.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters, ) # Properties diff --git a/openstack/network/v2/router.py b/openstack/network/v2/router.py index 1ea630541..41aba3018 100644 --- a/openstack/network/v2/router.py +++ b/openstack/network/v2/router.py @@ -40,7 +40,7 @@ class Router(_base.NetworkResource, tag.TagMixin): is_admin_state_up='admin_state_up', is_distributed='distributed', is_ha='ha', - **tag.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters, ) # Properties diff --git a/openstack/network/v2/security_group.py b/openstack/network/v2/security_group.py index 1cc9aaabf..9e1797104 100644 --- a/openstack/network/v2/security_group.py +++ b/openstack/network/v2/security_group.py @@ -37,7 +37,7 @@ class SecurityGroup(_base.NetworkResource, tag.TagMixin): 'revision_number', 'sort_dir', 'sort_key', - **tag.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters, ) # Properties diff --git a/openstack/network/v2/security_group_rule.py b/openstack/network/v2/security_group_rule.py index d4e7b0d6d..ef9504d14 100644 --- a/openstack/network/v2/security_group_rule.py +++ b/openstack/network/v2/security_group_rule.py @@ -43,7 +43,7 @@ class SecurityGroupRule(_base.NetworkResource, tag.TagMixin): 'sort_dir', 'sort_key', ether_type='ethertype', - **tag.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters, ) # Properties diff --git a/openstack/network/v2/subnet.py b/openstack/network/v2/subnet.py index 21f2afab2..5acc22744 100644 --- a/openstack/network/v2/subnet.py +++ b/openstack/network/v2/subnet.py @@ -44,7 +44,7 @@ class Subnet(_base.NetworkResource, tag.TagMixin): is_dhcp_enabled='enable_dhcp', subnet_pool_id='subnetpool_id', use_default_subnet_pool='use_default_subnetpool', - **tag.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters, ) # Properties diff --git a/openstack/network/v2/subnet_pool.py b/openstack/network/v2/subnet_pool.py index a517bcc61..5da9a4c94 100644 --- a/openstack/network/v2/subnet_pool.py +++ b/openstack/network/v2/subnet_pool.py @@ -37,7 +37,7 @@ class SubnetPool(resource.Resource, tag.TagMixin): 'sort_key', 'sort_dir', is_shared='shared', - **tag.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters, ) # Properties diff --git a/openstack/network/v2/trunk.py b/openstack/network/v2/trunk.py index 2ec34c681..c67e5686f 100644 --- a/openstack/network/v2/trunk.py +++ b/openstack/network/v2/trunk.py @@ -37,7 +37,7 @@ class Trunk(resource.Resource, tag.TagMixin): 'sub_ports', 'project_id', is_admin_state_up='admin_state_up', - **tag.TagMixin._tag_query_parameters + **tag.TagMixin._tag_query_parameters, ) # Properties diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index b4e6a4be6..bcc461e76 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -635,12 +635,12 @@ def stack_events(self, stack, resource_name=None, **attr): stack_id=obj.id, resource_name=resource_name, base_path=base_path, - **attr + **attr, ) return self._list( _stack_event.StackEvent, stack_name=obj.name, stack_id=obj.id, - **attr + **attr, ) diff --git a/openstack/shared_file_system/v2/share_access_rule.py b/openstack/shared_file_system/v2/share_access_rule.py index 6edf54825..590de0c87 100644 --- a/openstack/shared_file_system/v2/share_access_rule.py +++ b/openstack/shared_file_system/v2/share_access_rule.py @@ -79,7 +79,7 @@ def create(self, session, **kwargs): session, resource_request_key='allow_access', resource_response_key='access', - **kwargs + **kwargs, ) def delete( diff --git a/openstack/tests/functional/cloud/test_devstack.py b/openstack/tests/functional/cloud/test_devstack.py index 7170adfe1..ddeeafdea 100644 --- a/openstack/tests/functional/cloud/test_devstack.py +++ b/openstack/tests/functional/cloud/test_devstack.py @@ -18,6 +18,7 @@ Throw errors if we do not actually detect the services we're supposed to. """ + import os from testscenarios import load_tests_apply_scenarios as load_tests # noqa diff --git a/openstack/tests/functional/cloud/test_floating_ip_pool.py b/openstack/tests/functional/cloud/test_floating_ip_pool.py index 2d0161c70..3fe94f59c 100644 --- a/openstack/tests/functional/cloud/test_floating_ip_pool.py +++ b/openstack/tests/functional/cloud/test_floating_ip_pool.py @@ -32,7 +32,6 @@ class TestFloatingIPPool(base.BaseFunctionalTest): - def test_list_floating_ip_pools(self): pools = self.user_cloud.list_floating_ip_pools() if not pools: diff --git a/openstack/tests/functional/cloud/test_keypairs.py b/openstack/tests/functional/cloud/test_keypairs.py index 3fd920486..9a4635b7f 100644 --- a/openstack/tests/functional/cloud/test_keypairs.py +++ b/openstack/tests/functional/cloud/test_keypairs.py @@ -16,6 +16,7 @@ Functional tests for keypairs methods """ + from openstack.tests import fakes from openstack.tests.functional import base diff --git a/openstack/tests/functional/cloud/test_limits.py b/openstack/tests/functional/cloud/test_limits.py index 13838599c..5d6da9bf7 100644 --- a/openstack/tests/functional/cloud/test_limits.py +++ b/openstack/tests/functional/cloud/test_limits.py @@ -16,6 +16,7 @@ Functional tests for limits method """ + from openstack.compute.v2 import limits as _limits from openstack.tests.functional import base diff --git a/openstack/tests/functional/cloud/test_project.py b/openstack/tests/functional/cloud/test_project.py index d92abdb1d..7924c9096 100644 --- a/openstack/tests/functional/cloud/test_project.py +++ b/openstack/tests/functional/cloud/test_project.py @@ -18,6 +18,7 @@ Functional tests for project resource. """ + import pprint from openstack import exceptions diff --git a/openstack/tests/functional/cloud/test_recordset.py b/openstack/tests/functional/cloud/test_recordset.py index 98f8d6564..fff2b1b60 100644 --- a/openstack/tests/functional/cloud/test_recordset.py +++ b/openstack/tests/functional/cloud/test_recordset.py @@ -16,6 +16,7 @@ Functional tests for recordset methods. """ + import random import string diff --git a/openstack/tests/functional/cloud/test_volume_type.py b/openstack/tests/functional/cloud/test_volume_type.py index 52f4fe29d..bfdf011c5 100644 --- a/openstack/tests/functional/cloud/test_volume_type.py +++ b/openstack/tests/functional/cloud/test_volume_type.py @@ -16,6 +16,7 @@ Functional tests for block storage methods. """ + import testtools from openstack import exceptions diff --git a/openstack/tests/functional/image/v2/test_metadef_property.py b/openstack/tests/functional/image/v2/test_metadef_property.py index c58edf7a4..f490faab4 100644 --- a/openstack/tests/functional/image/v2/test_metadef_property.py +++ b/openstack/tests/functional/image/v2/test_metadef_property.py @@ -109,7 +109,7 @@ def test_metadef_property(self): metadef_property = self.conn.image.update_metadef_property( self.metadef_property, self.metadef_namespace.namespace, - **self.attrs + **self.attrs, ) self.assertIsNotNone(metadef_property) self.assertIsInstance( diff --git a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py index f158e6008..c43ba6ff4 100644 --- a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py +++ b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py @@ -90,7 +90,7 @@ def setUp(self): 'listener': 100, 'health_monitor': 100, 'member': 100, - } + }, ) assert isinstance(test_quota, quota.Quota) self.assertEqual(self.PROJECT_ID, test_quota.id) diff --git a/openstack/tests/functional/network/v2/test_floating_ip.py b/openstack/tests/functional/network/v2/test_floating_ip.py index 7e92688a2..3ea1ea9c4 100644 --- a/openstack/tests/functional/network/v2/test_floating_ip.py +++ b/openstack/tests/functional/network/v2/test_floating_ip.py @@ -75,7 +75,7 @@ def setUp(self): # Create Router sot = self.user_cloud.network.create_router( name=self.ROT_NAME, - **{"external_gateway_info": {"network_id": self.EXT_NET_ID}} + **{"external_gateway_info": {"network_id": self.EXT_NET_ID}}, ) assert isinstance(sot, router.Router) self.assertEqual(self.ROT_NAME, sot.name) diff --git a/openstack/tests/functional/network/v2/test_port_forwarding.py b/openstack/tests/functional/network/v2/test_port_forwarding.py index 034663e72..0d28a8efc 100644 --- a/openstack/tests/functional/network/v2/test_port_forwarding.py +++ b/openstack/tests/functional/network/v2/test_port_forwarding.py @@ -86,7 +86,7 @@ def setUp(self): # Create Router sot = self.user_cloud.network.create_router( name=self.ROT_NAME, - **{"external_gateway_info": {"network_id": self.EXT_NET_ID}} + **{"external_gateway_info": {"network_id": self.EXT_NET_ID}}, ) assert isinstance(sot, router.Router) self.assertEqual(self.ROT_NAME, sot.name) diff --git a/openstack/tests/functional/shared_file_system/test_quota_class_set.py b/openstack/tests/functional/shared_file_system/test_quota_class_set.py index cec5554ee..a9815e704 100644 --- a/openstack/tests/functional/shared_file_system/test_quota_class_set.py +++ b/openstack/tests/functional/shared_file_system/test_quota_class_set.py @@ -29,7 +29,7 @@ def test_quota_class_set(self): project_id, **{ "backups": initial_backups_value + 1, - } + }, ) ) self.assertEqual( diff --git a/openstack/tests/unit/block_storage/v3/test_type.py b/openstack/tests/unit/block_storage/v3/test_type.py index 7386a5fd6..19f10cd14 100644 --- a/openstack/tests/unit/block_storage/v3/test_type.py +++ b/openstack/tests/unit/block_storage/v3/test_type.py @@ -98,7 +98,7 @@ def test_set_extra_specs_error(self): exceptions.BadRequestException, sot.set_extra_specs, sess, - **set_specs + **set_specs, ) def test_delete_extra_specs(self): diff --git a/openstack/tests/unit/cloud/test_baremetal_node.py b/openstack/tests/unit/cloud/test_baremetal_node.py index f81fdd3e0..616fbc332 100644 --- a/openstack/tests/unit/cloud/test_baremetal_node.py +++ b/openstack/tests/unit/cloud/test_baremetal_node.py @@ -1744,7 +1744,7 @@ def test_register_machine_enroll_failure(self): exceptions.SDKException, self.cloud.register_machine, nics, - **node_to_post + **node_to_post, ) self.assert_calls() @@ -1811,7 +1811,7 @@ def test_register_machine_enroll_timeout(self): nics, timeout=0.001, lock_timeout=0.001, - **node_to_post + **node_to_post, ) self.assert_calls() @@ -1901,7 +1901,7 @@ def test_register_machine_enroll_timeout_wait(self): nics, wait=True, timeout=0.001, - **node_to_post + **node_to_post, ) self.assert_calls() @@ -1949,7 +1949,7 @@ def test_register_machine_port_create_failed(self): 'no ports for you', self.cloud.register_machine, nics, - **node_to_post + **node_to_post, ) self.assert_calls() @@ -2015,7 +2015,7 @@ def test_register_machine_several_ports_create_failed(self): 'no ports for you', self.cloud.register_machine, nics, - **node_to_post + **node_to_post, ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_delete_server.py b/openstack/tests/unit/cloud/test_delete_server.py index 3e62f71a0..3b987eecb 100644 --- a/openstack/tests/unit/cloud/test_delete_server.py +++ b/openstack/tests/unit/cloud/test_delete_server.py @@ -16,6 +16,7 @@ Tests for the `delete_server` command. """ + import uuid from openstack import exceptions diff --git a/openstack/tests/unit/cloud/test_fwaas.py b/openstack/tests/unit/cloud/test_fwaas.py index c80b0689f..935e08dbf 100644 --- a/openstack/tests/unit/cloud/test_fwaas.py +++ b/openstack/tests/unit/cloud/test_fwaas.py @@ -94,7 +94,7 @@ def test_create_firewall_rule_bad_protocol(self): self.assertRaises( exceptions.BadRequestException, self.cloud.create_firewall_rule, - **bad_rule + **bad_rule, ) self.assert_calls() @@ -146,7 +146,7 @@ def test_delete_firewall_rule_filters(self): uri=self._make_mock_url( 'firewall_rules', name=self.firewall_rule_name, - **filters + **filters, ), json={'firewall_rules': [self.mock_firewall_rule]}, ), @@ -446,7 +446,7 @@ def test_create_firewall_policy_rule_not_found(self): self.assertRaises( exceptions.NotFoundException, self.cloud.create_firewall_policy, - **posted_policy + **posted_policy, ) self.cloud.network.create_firewall_policy.assert_not_called() self.assert_calls() diff --git a/openstack/tests/unit/config/test_config.py b/openstack/tests/unit/config/test_config.py index e6e9bcff8..fccee9e06 100644 --- a/openstack/tests/unit/config/test_config.py +++ b/openstack/tests/unit/config/test_config.py @@ -1030,7 +1030,7 @@ def test_get_one_no_yaml(self): cc = c.get_one( region_name='region2', argparse=None, - **base.USER_CONF['clouds']['_test_cloud_regions'] + **base.USER_CONF['clouds']['_test_cloud_regions'], ) # Not using assert_cloud_details because of cache settings which # are not present without the file diff --git a/openstack/tests/unit/config/test_from_conf.py b/openstack/tests/unit/config/test_from_conf.py index 3f89b2832..a6e82c12a 100644 --- a/openstack/tests/unit/config/test_from_conf.py +++ b/openstack/tests/unit/config/test_from_conf.py @@ -30,7 +30,7 @@ def _get_conn(self, **from_conf_kwargs): oslocfg, session=self.cloud.session, name='from_conf.example.com', - **from_conf_kwargs + **from_conf_kwargs, ) self.assertEqual('from_conf.example.com', config.name) diff --git a/openstack/tests/unit/object_store/v1/test_proxy.py b/openstack/tests/unit/object_store/v1/test_proxy.py index b30dde267..14f6535d5 100644 --- a/openstack/tests/unit/object_store/v1/test_proxy.py +++ b/openstack/tests/unit/object_store/v1/test_proxy.py @@ -657,7 +657,7 @@ class TestTempURLUnicodePathAndKey(TestTempURL): url = '/v1/\u00e4/c/\u00f3' key = 'k\u00e9y' expected_url = ( - '%s?temp_url_sig=temp_url_signature' '&temp_url_expires=1400003600' + '%s?temp_url_sig=temp_url_signature&temp_url_expires=1400003600' ) % url expected_body = '\n'.join( [ @@ -672,7 +672,7 @@ class TestTempURLUnicodePathBytesKey(TestTempURL): url = '/v1/\u00e4/c/\u00f3' key = 'k\u00e9y'.encode() expected_url = ( - '%s?temp_url_sig=temp_url_signature' '&temp_url_expires=1400003600' + '%s?temp_url_sig=temp_url_signature&temp_url_expires=1400003600' ) % url expected_body = '\n'.join( [ diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index 4177a63dd..d1ac99c7a 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -87,9 +87,7 @@ "vendor_hook": "openstack.tests.unit.test_connection:vendor_hook" }} }} -""".format( - auth_url=CONFIG_AUTH_URL -) +""".format(auth_url=CONFIG_AUTH_URL) PUBLIC_CLOUDS_YAML = """ public-clouds: @@ -97,9 +95,7 @@ auth: auth_url: {auth_url} vendor_hook: openstack.tests.unit.test_connection:vendor_hook -""".format( - auth_url=CONFIG_AUTH_URL -) +""".format(auth_url=CONFIG_AUTH_URL) class _TestConnectionBase(base.TestCase): diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..3ab847ac0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[tool.ruff] +line-length = 79 + +[tool.ruff.format] +quote-style = "preserve" +docstring-code-format = true From c96d57f7683ca993f9b800df4539cb8ee05046ac Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 29 Aug 2024 14:46:30 +0100 Subject: [PATCH 3551/3836] Fix override of ShareAccessRule.delete This is using non-standard arguments that conflicts with the parent definition. Remove them. Change-Id: I2c930fcea0b67afda64086796e09d94a57ecda3d Signed-off-by: Stephen Finucane --- openstack/shared_file_system/v2/_proxy.py | 17 ++++++++++------ .../v2/share_access_rule.py | 20 +++++++++---------- .../unit/shared_file_system/v2/test_proxy.py | 6 ++---- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index 836b2d3da..ecb9c4c21 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -876,13 +876,18 @@ def delete_access_rule( :rtype: ``requests.models.Response`` HTTP response from internal requests client """ - res = self._get_resource(_share_access_rule.ShareAccessRule, access_id) - return res.delete( - self, - share_id, - ignore_missing=ignore_missing, - unrestrict=unrestrict, + res = self._get_resource( + _share_access_rule.ShareAccessRule, access_id, share_id=share_id ) + try: + return res.delete( + self, + unrestrict=unrestrict, + ) + except exceptions.NotFoundException: + if ignore_missing: + return None + raise def share_group_snapshots(self, details=True, **query): """Lists all share group snapshots. diff --git a/openstack/shared_file_system/v2/share_access_rule.py b/openstack/shared_file_system/v2/share_access_rule.py index 590de0c87..930f35dc1 100644 --- a/openstack/shared_file_system/v2/share_access_rule.py +++ b/openstack/shared_file_system/v2/share_access_rule.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import exceptions from openstack import resource from openstack import utils @@ -83,17 +82,18 @@ def create(self, session, **kwargs): ) def delete( - self, session, share_id, ignore_missing=True, *, unrestrict=False + self, + session, + error_message=None, + *, + microversion=None, + unrestrict=False, + **kwargs, ): - body = {"deny_access": {"access_id": self.id}} + body = {'deny_access': {'access_id': self.id}} if unrestrict: body['deny_access']['unrestrict'] = True - url = utils.urljoin("/shares", share_id, "action") + url = utils.urljoin("/shares", self.share_id, "action") response = self._action(session, body, url) - try: - response = self._action(session, body, url) - self._translate_response(response) - except exceptions.NotFoundException: - if not ignore_missing: - raise + self._translate_response(response) return response diff --git a/openstack/tests/unit/shared_file_system/v2/test_proxy.py b/openstack/tests/unit/shared_file_system/v2/test_proxy.py index 3534d8886..a412c3217 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_proxy.py +++ b/openstack/tests/unit/shared_file_system/v2/test_proxy.py @@ -471,15 +471,13 @@ def test_access_rules_create(self): def test_access_rules_delete(self): self._verify( - "openstack.shared_file_system.v2.share_access_rule." - + "ShareAccessRule.delete", + "openstack.shared_file_system.v2.share_access_rule.ShareAccessRule.delete", self.proxy.delete_access_rule, method_args=[ 'access_id', 'share_id', - 'ignore_missing', ], - expected_args=[self.proxy, 'share_id'], + expected_args=[self.proxy], expected_kwargs={'unrestrict': False}, ) From e614f8ea76ffc872df2b246990db343db66c71cb Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 29 Aug 2024 14:38:48 +0100 Subject: [PATCH 3552/3836] Remove unexpected Resource.update overrides These should actually be Resource.commit. Change-Id: I59374b7cdd0dbba8de0dab28e46f9b08f0f1aa59 Signed-off-by: Stephen Finucane --- openstack/orchestration/v1/_proxy.py | 4 ++-- openstack/orchestration/v1/stack.py | 20 +++++++++++-------- .../tests/unit/orchestration/v1/test_proxy.py | 4 ++-- .../tests/unit/orchestration/v1/test_stack.py | 20 ++++--------------- openstack/workflow/v2/workflow.py | 15 ++++++++++---- 5 files changed, 31 insertions(+), 32 deletions(-) diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index bcc461e76..49904115f 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -167,7 +167,7 @@ def get_stack(self, stack, resolve_outputs=True): """ return self._get(_stack.Stack, stack, resolve_outputs=resolve_outputs) - def update_stack(self, stack, preview=False, **attrs): + def update_stack(self, stack, *, preview=False, **attrs): """Update a stack :param stack: The value can be the ID of a stack or a @@ -181,7 +181,7 @@ def update_stack(self, stack, preview=False, **attrs): when no resource can be found. """ res = self._get_resource(_stack.Stack, stack, **attrs) - return res.update(self, preview) + return res.commit(self, preview) def delete_stack(self, stack, ignore_missing=True): """Delete a stack diff --git a/openstack/orchestration/v1/stack.py b/openstack/orchestration/v1/stack.py index 2001ca383..62a816f72 100644 --- a/openstack/orchestration/v1/stack.py +++ b/openstack/orchestration/v1/stack.py @@ -117,14 +117,18 @@ def create(self, session, base_path=None): # heat doesn't accept resource_key in its request. return super().create(session, prepend_key=False, base_path=base_path) - def commit(self, session, base_path=None): - # This overrides the default behavior of resource creation because - # heat doesn't accept resource_key in its request. - return super().commit( - session, prepend_key=False, has_body=False, base_path=None - ) - - def update(self, session, preview=False): + def commit( + self, + session, + prepend_key=True, + has_body=True, + retry_on_conflict=None, + base_path=None, + *, + microversion=None, + preview=False, + **kwargs, + ): # This overrides the default behavior of resource update because # we need to use other endpoint for update preview. base_path = None diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index 72691b18e..846d44068 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -96,7 +96,7 @@ def test_get_stack(self): def test_update_stack(self): self._verify( - 'openstack.orchestration.v1.stack.Stack.update', + 'openstack.orchestration.v1.stack.Stack.commit', self.proxy.update_stack, expected_result='result', method_args=['stack'], @@ -106,7 +106,7 @@ def test_update_stack(self): def test_update_stack_preview(self): self._verify( - 'openstack.orchestration.v1.stack.Stack.update', + 'openstack.orchestration.v1.stack.Stack.commit', self.proxy.update_stack, expected_result='result', method_args=['stack'], diff --git a/openstack/tests/unit/orchestration/v1/test_stack.py b/openstack/tests/unit/orchestration/v1/test_stack.py index be7dd05b3..e9614ec08 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack.py +++ b/openstack/tests/unit/orchestration/v1/test_stack.py @@ -188,18 +188,6 @@ def test_create(self, mock_create): ) self.assertEqual(mock_create.return_value, res) - @mock.patch.object(resource.Resource, 'commit') - def test_commit(self, mock_commit): - sess = mock.Mock() - sot = stack.Stack() - - res = sot.commit(sess) - - mock_commit.assert_called_once_with( - sess, prepend_key=False, has_body=False, base_path=None - ) - self.assertEqual(mock_commit.return_value, res) - def test_check(self): sess = mock.Mock() sot = stack.Stack(**FAKE) @@ -287,7 +275,7 @@ def test_export(self): f'stacks/{FAKE_NAME}/{FAKE_ID}/export', ) - def test_update(self): + def test_commit(self): sess = mock.Mock() sess.default_microversion = None @@ -299,7 +287,7 @@ def test_update(self): sot = stack.Stack(**FAKE) body = sot._body.dirty.copy() - sot.update(sess) + sot.commit(sess) sess.put.assert_called_with( f'/stacks/{FAKE_NAME}/{FAKE_ID}', @@ -308,7 +296,7 @@ def test_update(self): json=body, ) - def test_update_preview(self): + def test_commit_preview(self): sess = mock.Mock() sess.default_microversion = None @@ -320,7 +308,7 @@ def test_update_preview(self): sot = stack.Stack(**FAKE) body = sot._body.dirty.copy() - ret = sot.update(sess, preview=True) + ret = sot.commit(sess, preview=True) sess.put.assert_called_with( f'stacks/{FAKE_NAME}/{FAKE_ID}/preview', diff --git a/openstack/workflow/v2/workflow.py b/openstack/workflow/v2/workflow.py index e4f96f4eb..47e1afe9d 100644 --- a/openstack/workflow/v2/workflow.py +++ b/openstack/workflow/v2/workflow.py @@ -72,13 +72,20 @@ def create(self, session, prepend_key=True, base_path=None): self._translate_response(response, has_body=False) return self - def update(self, session, prepend_key=True, base_path=None): + def commit( + self, + session, + prepend_key=True, + has_body=True, + retry_on_conflict=None, + base_path=None, + *, + microversion=None, + **kwargs, + ): kwargs = self._request_kwargs( prepend_key=prepend_key, base_path=base_path ) response = session.put(**kwargs) self._translate_response(response, has_body=False) return self - - def commit(self, *args, **kwargs): - return self.update(*args, **kwargs) From 034a68b5acd79bda5b73edd3b99518f998c73836 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 29 Aug 2024 15:06:15 +0100 Subject: [PATCH 3553/3836] pre-commit: Bump mypy There are quite a few issues to resolve here but the vast majority are down to signature differences. Change-Id: Ifdcfb4c772e7485ed36c6cce69852a23f31679c2 Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 2 +- .../accelerator/v2/accelerator_request.py | 6 ++- openstack/accelerator/v2/device_profile.py | 16 ++++++-- openstack/baremetal/v1/node.py | 2 + openstack/block_storage/v3/group.py | 2 +- openstack/block_storage/v3/service.py | 5 ++- openstack/block_storage/v3/volume.py | 4 +- openstack/clustering/v1/_async_resource.py | 2 +- openstack/common/quota_set.py | 30 ++++++++++++--- openstack/common/tag.py | 2 +- openstack/compute/v2/limits.py | 12 +++--- openstack/compute/v2/server.py | 5 +++ openstack/compute/v2/service.py | 8 +--- openstack/database/v1/user.py | 8 +++- openstack/dns/v2/zone_export.py | 2 +- openstack/dns/v2/zone_import.py | 2 +- openstack/identity/v3/limit.py | 8 +++- openstack/identity/v3/registered_limit.py | 8 +++- openstack/image/v2/cache.py | 4 +- openstack/image/v2/image.py | 3 ++ openstack/key_manager/v1/secret.py | 1 + openstack/load_balancer/v2/amphora.py | 20 ++++++---- openstack/load_balancer/v2/load_balancer.py | 8 ++-- openstack/load_balancer/v2/quota.py | 8 +++- openstack/message/v2/claim.py | 29 +++++++++++--- openstack/message/v2/message.py | 5 ++- openstack/message/v2/queue.py | 7 +++- openstack/message/v2/subscription.py | 7 +++- openstack/network/v2/_base.py | 1 + openstack/network/v2/quota.py | 8 +++- openstack/object_store/v1/_base.py | 13 +++++-- openstack/object_store/v1/_proxy.py | 2 + openstack/object_store/v1/container.py | 2 +- openstack/object_store/v1/info.py | 5 ++- openstack/object_store/v1/obj.py | 14 ++++--- openstack/orchestration/v1/software_config.py | 4 +- .../orchestration/v1/software_deployment.py | 8 ++-- openstack/orchestration/v1/stack.py | 6 ++- openstack/orchestration/v1/stack_files.py | 4 +- .../v2/share_access_rule.py | 3 +- .../v2/share_network_subnet.py | 10 ++++- .../tests/unit/block_storage/v2/test_proxy.py | 37 +++++++++++++----- .../tests/unit/block_storage/v3/test_proxy.py | 37 +++++++++++++----- openstack/tests/unit/compute/v2/test_proxy.py | 38 +++++++++++++------ .../tests/unit/orchestration/v1/test_stack.py | 4 +- openstack/workflow/v2/cron_trigger.py | 10 ++++- openstack/workflow/v2/execution.py | 8 +++- openstack/workflow/v2/workflow.py | 8 +++- 48 files changed, 317 insertions(+), 121 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 581e10a54..4f4516377 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: - flake8-import-order~=0.18.2 exclude: '^(doc|releasenotes|tools)/.*$' - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.10.1 + rev: v1.11.2 hooks: - id: mypy additional_dependencies: diff --git a/openstack/accelerator/v2/accelerator_request.py b/openstack/accelerator/v2/accelerator_request.py index 71f6b099b..cf1c6f372 100644 --- a/openstack/accelerator/v2/accelerator_request.py +++ b/openstack/accelerator/v2/accelerator_request.py @@ -64,6 +64,8 @@ def patch( has_body=True, retry_on_conflict=None, base_path=None, + *, + microversion=None, ): # This overrides the default behavior of patch because # the PATCH method consumes a dict rather than a list. spec: @@ -104,7 +106,7 @@ def _consume_attrs(self, mapping, attrs): attrs = attrs[self.resources_key][0] return super()._consume_attrs(mapping, attrs) - def create(self, session, base_path=None): + def create(self, session, prepend_key=False, *args, **kwargs): # This overrides the default behavior of resource creation because # cyborg doesn't accept resource_key in its request. - return super().create(session, prepend_key=False, base_path=base_path) + return super().create(session, prepend_key, *args, **kwargs) diff --git a/openstack/accelerator/v2/device_profile.py b/openstack/accelerator/v2/device_profile.py index 67d289564..dfa8a85ec 100644 --- a/openstack/accelerator/v2/device_profile.py +++ b/openstack/accelerator/v2/device_profile.py @@ -38,11 +38,19 @@ class DeviceProfile(resource.Resource): # TODO(s_shogo): This implementation only treat [ DeviceProfile ], and # cannot treat multiple DeviceProfiles in list. - def _prepare_request_body(self, patch, prepend_key): - body = super()._prepare_request_body(patch, prepend_key) + def _prepare_request_body( + self, + patch, + prepend_key, + *, + resource_request_key=None, + ): + body = super()._prepare_request_body( + patch, prepend_key, resource_request_key=resource_request_key + ) return [body] - def create(self, session, base_path=None): + def create(self, session, prepend_key=False, *args, **kwargs): # This overrides the default behavior of resource creation because # cyborg doesn't accept resource_key in its request. - return super().create(session, prepend_key=False, base_path=base_path) + return super().create(session, prepend_key, *args, **kwargs) diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 47c0b48b6..8605e9e91 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -1465,6 +1465,8 @@ def patch( has_body=True, retry_on_conflict=None, base_path=None, + *, + microversion=None, reset_interfaces=None, ): if reset_interfaces is not None: diff --git a/openstack/block_storage/v3/group.py b/openstack/block_storage/v3/group.py index 82af284ae..13c9871c9 100644 --- a/openstack/block_storage/v3/group.py +++ b/openstack/block_storage/v3/group.py @@ -60,7 +60,7 @@ def _action(self, session, body): exceptions.raise_from_response(response) return response - def delete(self, session, *, delete_volumes=False): + def delete(self, session, *args, delete_volumes=False, **kwargs): """Delete a group.""" body = {'delete': {'delete-volumes': delete_volumes}} self._action(session, body) diff --git a/openstack/block_storage/v3/service.py b/openstack/block_storage/v3/service.py index 1454fc8f1..438e1f717 100644 --- a/openstack/block_storage/v3/service.py +++ b/openstack/block_storage/v3/service.py @@ -90,11 +90,12 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): f"No {cls.__name__} found for {name_or_id}" ) - def commit(self, session, prepend_key=False, **kwargs): + def commit(self, session, prepend_key=False, *args, **kwargs): # we need to set prepend_key to false return super().commit( session, - prepend_key=prepend_key, + prepend_key, + *args, **kwargs, ) diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index eb927ec40..e566f5ac7 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -359,7 +359,9 @@ def terminate_attachment(self, session, connector): self._action(session, body) - def _prepare_request_body(self, patch, prepend_key): + def _prepare_request_body( + self, patch, prepend_key, *, resource_request_key=None + ): body = self._body.dirty # Scheduler hints is external to the standard volume request # so pass it separately and not under the volume JSON object. diff --git a/openstack/clustering/v1/_async_resource.py b/openstack/clustering/v1/_async_resource.py index f37bb629b..01d59af9e 100644 --- a/openstack/clustering/v1/_async_resource.py +++ b/openstack/clustering/v1/_async_resource.py @@ -16,7 +16,7 @@ class AsyncResource(resource.Resource): - def delete(self, session, error_message=None): + def delete(self, session, error_message=None, **kwargs): """Delete the remote resource based on this instance. :param session: The session to use for making this request. diff --git a/openstack/common/quota_set.py b/openstack/common/quota_set.py index a7bd86b5c..35d70fa1d 100644 --- a/openstack/common/quota_set.py +++ b/openstack/common/quota_set.py @@ -55,17 +55,31 @@ def fetch( requires_id=False, base_path=None, error_message=None, + skip_cache=False, + *, + resource_response_key=None, + microversion=None, **params, ): return super().fetch( session, - requires_id=False, - base_path=base_path, - error_message=error_message, + requires_id, + base_path, + error_message, + skip_cache, + resource_response_key=resource_response_key, + microversion=microversion, **params, ) - def _translate_response(self, response, has_body=None, error_message=None): + def _translate_response( + self, + response, + has_body=None, + error_message=None, + *, + resource_response_key=None, + ): """Given a KSA response, inflate this instance with its data DELETE operations don't return a body, so only try to work @@ -127,7 +141,13 @@ def _translate_response(self, response, has_body=None, error_message=None): self._update_location() dict.update(self, self.to_dict()) - def _prepare_request_body(self, patch, prepend_key): + def _prepare_request_body( + self, + patch, + prepend_key, + *, + resource_request_key=None, + ): body = self._body.dirty # Ensure we never try to send meta props reservation and usage body.pop('reservation', None) diff --git a/openstack/common/tag.py b/openstack/common/tag.py index 0a2d16ec3..09e2323ba 100644 --- a/openstack/common/tag.py +++ b/openstack/common/tag.py @@ -52,7 +52,7 @@ def fetch_tags(self, session): self._body.attributes.update({'tags': json['tags']}) return self - def set_tags(self, session, tags=[]): + def set_tags(self, session, tags): """Sets/Replaces all tags on the resource. :param session: The session to use for making this request. diff --git a/openstack/compute/v2/limits.py b/openstack/compute/v2/limits.py index 86946bf3a..ef223e06d 100644 --- a/openstack/compute/v2/limits.py +++ b/openstack/compute/v2/limits.py @@ -121,8 +121,8 @@ def fetch( self, session, requires_id=False, - error_message=None, base_path=None, + error_message=None, skip_cache=False, **params, ): @@ -137,10 +137,10 @@ def fetch( # TODO(mordred) We shouldn't have to subclass just to declare # requires_id = False. return super().fetch( - session=session, - requires_id=requires_id, - error_message=error_message, - base_path=base_path, - skip_cache=skip_cache, + session, + requires_id, + error_message, + base_path, + skip_cache, **params, ) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 132ed81b1..2099e554f 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -280,13 +280,18 @@ def _prepare_request( self, requires_id=True, prepend_key=True, + patch=False, base_path=None, + params=None, **kwargs, ): request = super()._prepare_request( requires_id=requires_id, prepend_key=prepend_key, + patch=patch, base_path=base_path, + params=params, + **kwargs, ) server_body = request.body[self.resource_key] diff --git a/openstack/compute/v2/service.py b/openstack/compute/v2/service.py index ba371122f..0623fa0a7 100644 --- a/openstack/compute/v2/service.py +++ b/openstack/compute/v2/service.py @@ -87,13 +87,9 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): f"No {cls.__name__} found for {name_or_id}" ) - def commit(self, session, prepend_key=False, **kwargs): + def commit(self, session, prepend_key=False, *args, **kwargs): # we need to set prepend_key to false - return super().commit( - session, - prepend_key=prepend_key, - **kwargs, - ) + return super().commit(session, prepend_key, *args, **kwargs) def _action(self, session, action, body, microversion=None): if not microversion: diff --git a/openstack/database/v1/user.py b/openstack/database/v1/user.py index 3e7d9a688..d1c29cd68 100644 --- a/openstack/database/v1/user.py +++ b/openstack/database/v1/user.py @@ -35,7 +35,13 @@ class User(resource.Resource): password = resource.Body('password') def _prepare_request( - self, requires_id=True, prepend_key=True, base_path=None, **kwargs + self, + requires_id=True, + prepend_key=True, + patch=False, + base_path=None, + *args, + **kwargs, ): """Prepare a request for the database service's create call diff --git a/openstack/dns/v2/zone_export.py b/openstack/dns/v2/zone_export.py index 18cb4b1ac..34e0012e2 100644 --- a/openstack/dns/v2/zone_export.py +++ b/openstack/dns/v2/zone_export.py @@ -51,7 +51,7 @@ class ZoneExport(_base.Resource): #: ID for the zone that was created by this export zone_id = resource.Body('zone_id') - def create(self, session, prepend_key=True, base_path=None): + def create(self, session, prepend_key=True, base_path=None, **kwargs): """Create a remote resource based on this instance. :param session: The session to use for making this request. diff --git a/openstack/dns/v2/zone_import.py b/openstack/dns/v2/zone_import.py index 1ed3153bc..8f875f7b1 100644 --- a/openstack/dns/v2/zone_import.py +++ b/openstack/dns/v2/zone_import.py @@ -51,7 +51,7 @@ class ZoneImport(_base.Resource): #: ID for the zone that was created by this import zone_id = resource.Body('zone_id') - def create(self, session, prepend_key=True, base_path=None): + def create(self, session, prepend_key=True, base_path=None, **kwargs): """Create a remote resource based on this instance. :param session: The session to use for making this request. diff --git a/openstack/identity/v3/limit.py b/openstack/identity/v3/limit.py index 7918024a7..54190932b 100644 --- a/openstack/identity/v3/limit.py +++ b/openstack/identity/v3/limit.py @@ -47,7 +47,13 @@ class Limit(resource.Resource): #: ID of project. *Type: string* project_id = resource.Body('project_id') - def _prepare_request_body(self, patch, prepend_key): + def _prepare_request_body( + self, + patch, + prepend_key, + *, + resource_request_key=None, + ): body = self._body.dirty if prepend_key and self.resource_key is not None: if patch: diff --git a/openstack/identity/v3/registered_limit.py b/openstack/identity/v3/registered_limit.py index 7e8b67d66..6cdf07a20 100644 --- a/openstack/identity/v3/registered_limit.py +++ b/openstack/identity/v3/registered_limit.py @@ -45,7 +45,13 @@ class RegisteredLimit(resource.Resource): #: The default limit value. *Type: int* default_limit = resource.Body('default_limit') - def _prepare_request_body(self, patch, prepend_key): + def _prepare_request_body( + self, + patch, + prepend_key, + *, + resource_request_key=None, + ): body = self._body.dirty if prepend_key and self.resource_key is not None: if patch: diff --git a/openstack/image/v2/cache.py b/openstack/image/v2/cache.py index 1b240afd3..f181cf59b 100644 --- a/openstack/image/v2/cache.py +++ b/openstack/image/v2/cache.py @@ -54,7 +54,9 @@ def queue(self, session, image, *, microversion=None): exceptions.raise_from_response(response) return response - def clear(self, session, target='both'): + # FIXME(stephenfin): This needs to be renamed as it conflicts with + # dict.clear + def clear(self, session, target='both'): # type: ignore[override] """Clears the cache. :param session: The session to use for making this request :param target: Specify which target you want to clear diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index d84eaef01..28e7c19b0 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -383,6 +383,7 @@ def _prepare_request( prepend_key=False, patch=False, base_path=None, + params=None, **kwargs, ): request = super()._prepare_request( @@ -390,6 +391,8 @@ def _prepare_request( prepend_key=prepend_key, patch=patch, base_path=base_path, + params=params, + **kwargs, ) if patch: headers = { diff --git a/openstack/key_manager/v1/secret.py b/openstack/key_manager/v1/secret.py index aba4bca9f..ed092693a 100644 --- a/openstack/key_manager/v1/secret.py +++ b/openstack/key_manager/v1/secret.py @@ -91,6 +91,7 @@ def fetch( base_path=None, error_message=None, skip_cache=False, + **kwargs, ): request = self._prepare_request( requires_id=requires_id, base_path=base_path diff --git a/openstack/load_balancer/v2/amphora.py b/openstack/load_balancer/v2/amphora.py index b33a4a603..31f22db25 100644 --- a/openstack/load_balancer/v2/amphora.py +++ b/openstack/load_balancer/v2/amphora.py @@ -111,10 +111,12 @@ class AmphoraConfig(resource.Resource): #: The ID of the amphora. amphora_id = resource.URI('amphora_id') - # The default _update code path also has no - # way to pass has_body into this function, so overriding the method here. - def commit(self, session, base_path=None): - return super().commit(session, base_path=base_path, has_body=False) + # The default _update code path also has no way to pass has_body into this + # function, so overriding the method here. + def commit( + self, session, prepend_key=True, has_body=False, *args, **kwargs + ): + return super().commit(session, prepend_key, has_body, *args, *kwargs) class AmphoraFailover(resource.Resource): @@ -134,7 +136,9 @@ class AmphoraFailover(resource.Resource): #: The ID of the amphora. amphora_id = resource.URI('amphora_id') - # The default _update code path also has no - # way to pass has_body into this function, so overriding the method here. - def commit(self, session, base_path=None): - return super().commit(session, base_path=base_path, has_body=False) + # The default _update code path also has no way to pass has_body into this + # function, so overriding the method here. + def commit( + self, session, prepend_key=True, has_body=False, *args, **kwargs + ): + return super().commit(session, prepend_key, has_body, *args, *kwargs) diff --git a/openstack/load_balancer/v2/load_balancer.py b/openstack/load_balancer/v2/load_balancer.py index a23708f17..d435c2a70 100644 --- a/openstack/load_balancer/v2/load_balancer.py +++ b/openstack/load_balancer/v2/load_balancer.py @@ -83,7 +83,7 @@ class LoadBalancer(resource.Resource, tag.TagMixin): #: Additional VIPs additional_vips = resource.Body('additional_vips', type=list) - def delete(self, session, error_message=None): + def delete(self, session, error_message=None, **kwargs): request = self._prepare_request() params = {} if ( @@ -145,5 +145,7 @@ class LoadBalancerFailover(resource.Resource): # The default _update code path also has no # way to pass has_body into this function, so overriding the method here. - def commit(self, session, base_path=None): - return super().commit(session, base_path=base_path, has_body=False) + def commit( + self, session, prepend_key=True, has_body=False, *args, **kwargs + ): + return super().commit(session, prepend_key, has_body, *args, **kwargs) diff --git a/openstack/load_balancer/v2/quota.py b/openstack/load_balancer/v2/quota.py index ddab05746..2151418a4 100644 --- a/openstack/load_balancer/v2/quota.py +++ b/openstack/load_balancer/v2/quota.py @@ -42,7 +42,13 @@ class Quota(resource.Resource): project_id = resource.Body('project_id', alternate_id=True) def _prepare_request( - self, requires_id=True, base_path=None, prepend_key=False, **kwargs + self, + requires_id=True, + prepend_key=False, + patch=False, + base_path=None, + *args, + **kwargs, ): _request = super()._prepare_request( requires_id, prepend_key, base_path=base_path diff --git a/openstack/message/v2/claim.py b/openstack/message/v2/claim.py index d4fb9bb30..2aedf57ef 100644 --- a/openstack/message/v2/claim.py +++ b/openstack/message/v2/claim.py @@ -55,13 +55,25 @@ class Claim(resource.Resource): #: authentication is not enabled in Zaqar service. project_id = resource.Header("X-PROJECT-ID") - def _translate_response(self, response, has_body=True): - super()._translate_response(response, has_body=has_body) + def _translate_response( + self, + response, + has_body=None, + error_message=None, + *, + resource_response_key=None, + ): + super()._translate_response( + response, + has_body, + error_message, + resource_response_key=resource_response_key, + ) if has_body and self.location: # Extract claim ID from location self.id = self.location.split("claims/")[1] - def create(self, session, prepend_key=False, base_path=None): + def create(self, session, prepend_key=False, base_path=None, **kwargs): request = self._prepare_request( requires_id=False, prepend_key=prepend_key, base_path=base_path ) @@ -89,6 +101,7 @@ def fetch( base_path=None, error_message=None, skip_cache=False, + **kwargs, ): request = self._prepare_request( requires_id=requires_id, base_path=base_path @@ -107,7 +120,13 @@ def fetch( return self def commit( - self, session, prepend_key=False, has_body=False, base_path=None + self, + session, + prepend_key=True, + has_body=True, + retry_on_conflict=None, + base_path=None, + **kwargs, ): request = self._prepare_request( prepend_key=prepend_key, base_path=base_path @@ -122,7 +141,7 @@ def commit( return self - def delete(self, session): + def delete(self, session, *args, **kwargs): request = self._prepare_request() headers = { "Client-ID": self.client_id or str(uuid.uuid4()), diff --git a/openstack/message/v2/message.py b/openstack/message/v2/message.py index da36c58b0..0b73b0605 100644 --- a/openstack/message/v2/message.py +++ b/openstack/message/v2/message.py @@ -123,6 +123,7 @@ def fetch( base_path=None, error_message=None, skip_cache=False, + **kwargs, ): request = self._prepare_request( requires_id=requires_id, base_path=base_path @@ -140,7 +141,9 @@ def fetch( return self - def delete(self, session): + def delete( + self, session, error_message=None, *, microversion=None, **kwargs + ): request = self._prepare_request() headers = { "Client-ID": self.client_id or str(uuid.uuid4()), diff --git a/openstack/message/v2/queue.py b/openstack/message/v2/queue.py index a256dee57..42b0b00c5 100644 --- a/openstack/message/v2/queue.py +++ b/openstack/message/v2/queue.py @@ -50,7 +50,7 @@ class Queue(resource.Resource): #: in case keystone auth is not enabled in Zaqar service. project_id = resource.Header("X-PROJECT-ID") - def create(self, session, prepend_key=True, base_path=None): + def create(self, session, prepend_key=False, base_path=None, **kwargs): request = self._prepare_request( requires_id=True, prepend_key=prepend_key, base_path=None ) @@ -117,6 +117,7 @@ def fetch( base_path=None, error_message=None, skip_cache=False, + **kwargs, ): request = self._prepare_request( requires_id=requires_id, base_path=base_path @@ -133,7 +134,9 @@ def fetch( return self - def delete(self, session): + def delete( + self, session, error_message=None, *, microversion=None, **kwargs + ): request = self._prepare_request() headers = { "Client-ID": self.client_id or str(uuid.uuid4()), diff --git a/openstack/message/v2/subscription.py b/openstack/message/v2/subscription.py index d8d36e753..884c1ea83 100644 --- a/openstack/message/v2/subscription.py +++ b/openstack/message/v2/subscription.py @@ -58,7 +58,7 @@ class Subscription(resource.Resource): #: authentication is not enabled in Zaqar service. project_id = resource.Header("X-PROJECT-ID") - def create(self, session, prepend_key=True, base_path=None): + def create(self, session, prepend_key=False, base_path=None, **kwargs): request = self._prepare_request( requires_id=False, prepend_key=prepend_key, base_path=base_path ) @@ -125,6 +125,7 @@ def fetch( base_path=None, error_message=None, skip_cache=False, + **kwargs, ): request = self._prepare_request( requires_id=requires_id, base_path=base_path @@ -142,7 +143,9 @@ def fetch( return self - def delete(self, session): + def delete( + self, session, error_message=None, *, microversion=None, **kwargs + ): request = self._prepare_request() headers = { "Client-ID": self.client_id or str(uuid.uuid4()), diff --git a/openstack/network/v2/_base.py b/openstack/network/v2/_base.py index 9ca01747e..e55b900ec 100644 --- a/openstack/network/v2/_base.py +++ b/openstack/network/v2/_base.py @@ -25,6 +25,7 @@ def _prepare_request( patch=False, base_path=None, params=None, + *, if_revision=None, **kwargs, ): diff --git a/openstack/network/v2/quota.py b/openstack/network/v2/quota.py index 8c5bc7eb8..e4a4c55cb 100644 --- a/openstack/network/v2/quota.py +++ b/openstack/network/v2/quota.py @@ -61,7 +61,13 @@ class Quota(resource.Resource): security_groups = resource.Body('security_group', type=int) def _prepare_request( - self, requires_id=True, prepend_key=False, base_path=None, **kwargs + self, + requires_id=True, + prepend_key=False, + patch=False, + base_path=None, + *args, + **kwargs, ): _request = super()._prepare_request(requires_id, prepend_key) if self.resource_key in _request.body: diff --git a/openstack/object_store/v1/_base.py b/openstack/object_store/v1/_base.py index 0f910d0cf..b7e19f612 100644 --- a/openstack/object_store/v1/_base.py +++ b/openstack/object_store/v1/_base.py @@ -40,8 +40,8 @@ def __init__(self, metadata=None, **attrs): else: self.metadata[k] = v - def _prepare_request(self, **kwargs): - request = super()._prepare_request(**kwargs) + def _prepare_request(self, *args, **kwargs): + request = super()._prepare_request(*args, **kwargs) request.headers.update(self._calculate_headers(self.metadata)) return request @@ -91,7 +91,14 @@ def _set_metadata(self, headers): key = header[len(self._custom_metadata_prefix) :].lower() self.metadata[key] = headers[header] - def _translate_response(self, response, has_body=None, error_message=None): + def _translate_response( + self, + response, + has_body=None, + error_message=None, + *, + resource_response_key=None, + ): # Save headers of the last operation for potential use (get_object of # cloud layer). # This must happen before invoking parent _translate_response, cause it diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index 8b661ff28..9bb6de618 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -12,6 +12,7 @@ from calendar import timegm import collections +import functools from hashlib import sha1 import hmac import json @@ -56,6 +57,7 @@ class Proxy(proxy.Proxy): log = _log.setup_logging('openstack') + @functools.lru_cache(maxsize=256) def _extract_name(self, url, service_type=None, project_id=None): url_path = parse.urlparse(url).path.strip() # Remove / from the beginning to keep the list indexes of interesting diff --git a/openstack/object_store/v1/container.py b/openstack/object_store/v1/container.py index 5c225e2e0..e709f3034 100644 --- a/openstack/object_store/v1/container.py +++ b/openstack/object_store/v1/container.py @@ -123,7 +123,7 @@ def new(cls, **kwargs): kwargs.setdefault('name', name) return cls(_synchronized=False, **kwargs) - def create(self, session, prepend_key=True, base_path=None): + def create(self, session, prepend_key=True, base_path=None, **kwargs): """Create a remote resource based on this instance. :param session: The session to use for making this request. diff --git a/openstack/object_store/v1/info.py b/openstack/object_store/v1/info.py index e71e25480..572c1bb66 100644 --- a/openstack/object_store/v1/info.py +++ b/openstack/object_store/v1/info.py @@ -12,7 +12,7 @@ # under the License. import re -import urllib +import urllib.parse from openstack import exceptions from openstack import resource @@ -58,8 +58,9 @@ def fetch( session, requires_id=False, base_path=None, - skip_cache=False, error_message=None, + skip_cache=False, + **kwargs, ): """Get a remote resource based on this instance. diff --git a/openstack/object_store/v1/obj.py b/openstack/object_store/v1/obj.py index af95f2ca3..3b036e55a 100644 --- a/openstack/object_store/v1/obj.py +++ b/openstack/object_store/v1/obj.py @@ -213,7 +213,7 @@ def __init__(self, data=None, **attrs): # The Object Store treats the metadata for its resources inconsistently so # Object.set_metadata must override the BaseResource.set_metadata to # account for it. - def set_metadata(self, session, metadata): + def set_metadata(self, session, metadata, refresh=True): # Filter out items with empty values so the create metadata behaviour # is the same as account and container filtered_metadata = { @@ -321,7 +321,7 @@ def stream(self, session, error_message=None, chunk_size=1024): ) return response.iter_content(chunk_size, decode_unicode=False) - def create(self, session, base_path=None, **params): + def create(self, session, prepend_key=True, base_path=None, **kwargs): request = self._prepare_request(base_path=base_path) response = session.put( @@ -330,11 +330,11 @@ def create(self, session, base_path=None, **params): self._translate_response(response, has_body=False) return self - def _raw_delete(self, session, microversion=None): + def _raw_delete(self, session, microversion=None, **kwargs): if not self.allow_delete: - raise exceptions.MethodNotSupported(self, "delete") + raise exceptions.MethodNotSupported(self, 'delete') - request = self._prepare_request() + request = self._prepare_request(**kwargs) session = self._get_session(session) if microversion is None: microversion = self._get_microversion(session, action='delete') @@ -349,5 +349,7 @@ def _raw_delete(self, session, microversion=None): headers['multipart-manifest'] = 'delete' return session.delete( - request.url, headers=headers, microversion=microversion + request.url, + headers=headers, + microversion=microversion, ) diff --git a/openstack/orchestration/v1/software_config.py b/openstack/orchestration/v1/software_config.py index cbd053fba..f0b67b3cd 100644 --- a/openstack/orchestration/v1/software_config.py +++ b/openstack/orchestration/v1/software_config.py @@ -45,7 +45,7 @@ class SoftwareConfig(resource.Resource): #: produces. outputs = resource.Body('outputs') - def create(self, session, base_path=None): + def create(self, session, prepend_key=False, *args, **kwargs): # This overrides the default behavior of resource creation because # heat doesn't accept resource_key in its request. - return super().create(session, prepend_key=False, base_path=base_path) + return super().create(session, prepend_key, *args, **kwargs) diff --git a/openstack/orchestration/v1/software_deployment.py b/openstack/orchestration/v1/software_deployment.py index 12178fc06..24d6952d2 100644 --- a/openstack/orchestration/v1/software_deployment.py +++ b/openstack/orchestration/v1/software_deployment.py @@ -49,12 +49,12 @@ class SoftwareDeployment(resource.Resource): #: The date and time when the software deployment resource was created. updated_at = resource.Body('updated_time') - def create(self, session, base_path=None): + def create(self, session, prepend_key=False, *args, **kwargs): # This overrides the default behavior of resource creation because # heat doesn't accept resource_key in its request. - return super().create(session, prepend_key=False, base_path=base_path) + return super().create(session, prepend_key, *args, **kwargs) - def commit(self, session, base_path=None): + def commit(self, session, prepend_key=False, *args, **kwargs): # This overrides the default behavior of resource creation because # heat doesn't accept resource_key in its request. - return super().commit(session, prepend_key=False, base_path=base_path) + return super().commit(session, prepend_key, *args, **kwargs) diff --git a/openstack/orchestration/v1/stack.py b/openstack/orchestration/v1/stack.py index 62a816f72..5247a7f60 100644 --- a/openstack/orchestration/v1/stack.py +++ b/openstack/orchestration/v1/stack.py @@ -112,10 +112,10 @@ class Stack(resource.Resource): #: The ID of the user project created for this stack. user_project_id = resource.Body('stack_user_project_id') - def create(self, session, base_path=None): + def create(self, session, prepend_key=False, *args, **kwargs): # This overrides the default behavior of resource creation because # heat doesn't accept resource_key in its request. - return super().create(session, prepend_key=False, base_path=base_path) + return super().create(session, prepend_key, *args, **kwargs) def commit( self, @@ -219,7 +219,9 @@ def fetch( base_path=None, error_message=None, skip_cache=False, + *, resolve_outputs=True, + **params, ): if not self.allow_fetch: raise exceptions.MethodNotSupported(self, "fetch") diff --git a/openstack/orchestration/v1/stack_files.py b/openstack/orchestration/v1/stack_files.py index 0b72c6b68..e38907c12 100644 --- a/openstack/orchestration/v1/stack_files.py +++ b/openstack/orchestration/v1/stack_files.py @@ -33,7 +33,9 @@ class StackFiles(resource.Resource): # Backwards compat stack_id = id # type: ignore - def fetch(self, session, base_path=None): + def fetch( + self, session, requires_id=False, base_path=None, *args, **kwargs + ): # The stack files response contains a map of filenames and file # contents. request = self._prepare_request(requires_id=False, base_path=base_path) diff --git a/openstack/shared_file_system/v2/share_access_rule.py b/openstack/shared_file_system/v2/share_access_rule.py index 930f35dc1..792cc8100 100644 --- a/openstack/shared_file_system/v2/share_access_rule.py +++ b/openstack/shared_file_system/v2/share_access_rule.py @@ -73,9 +73,10 @@ def _action(self, session, body, url, action='patch', microversion=None): url, json=body, headers=headers, microversion=microversion ) - def create(self, session, **kwargs): + def create(self, session, *args, **kwargs): return super().create( session, + *args, resource_request_key='allow_access', resource_response_key='access', **kwargs, diff --git a/openstack/shared_file_system/v2/share_network_subnet.py b/openstack/shared_file_system/v2/share_network_subnet.py index 5d248b183..06e843a77 100644 --- a/openstack/shared_file_system/v2/share_network_subnet.py +++ b/openstack/shared_file_system/v2/share_network_subnet.py @@ -55,7 +55,13 @@ class ShareNetworkSubnet(resource.Resource): #: Date and time the share network subnet was last updated at. updated_at = resource.Body("updated_at", type=str) - def create(self, session, **kwargs): + def create( + self, + session, + *args, + resource_request_key='share-network-subnet', + **kwargs, + ): return super().create( - session, resource_request_key='share-network-subnet', **kwargs + session, resource_request_key=resource_request_key, *args, **kwargs ) diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index 50cf48eb7..2831d06a5 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -498,10 +498,16 @@ def test_quota_set_get(self): 'openstack.resource.Resource.fetch', self.proxy.get_quota_set, method_args=['prj'], - expected_args=[self.proxy], + expected_args=[ + self.proxy, + False, + None, + None, + False, + ], expected_kwargs={ - 'error_message': None, - 'requires_id': False, + 'microversion': None, + 'resource_response_key': None, 'usage': False, }, method_result=quota_set.QuotaSet(), @@ -514,10 +520,16 @@ def test_quota_set_get_query(self): self.proxy.get_quota_set, method_args=['prj'], method_kwargs={'usage': True, 'user_id': 'uid'}, - expected_args=[self.proxy], + expected_args=[ + self.proxy, + False, + None, + None, + False, + ], expected_kwargs={ - 'error_message': None, - 'requires_id': False, + 'microversion': None, + 'resource_response_key': None, 'usage': True, 'user_id': 'uid', }, @@ -528,11 +540,16 @@ def test_quota_set_get_defaults(self): 'openstack.resource.Resource.fetch', self.proxy.get_quota_set_defaults, method_args=['prj'], - expected_args=[self.proxy], + expected_args=[ + self.proxy, + False, + '/os-quota-sets/defaults', + None, + False, + ], expected_kwargs={ - 'error_message': None, - 'requires_id': False, - 'base_path': '/os-quota-sets/defaults', + 'microversion': None, + 'resource_response_key': None, }, ) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index c8b82f76f..27a9ee0e5 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -971,10 +971,16 @@ def test_quota_set_get(self): 'openstack.resource.Resource.fetch', self.proxy.get_quota_set, method_args=['prj'], - expected_args=[self.proxy], + expected_args=[ + self.proxy, + False, + None, + None, + False, + ], expected_kwargs={ - 'error_message': None, - 'requires_id': False, + 'microversion': None, + 'resource_response_key': None, 'usage': False, }, method_result=quota_set.QuotaSet(), @@ -987,10 +993,16 @@ def test_quota_set_get_query(self): self.proxy.get_quota_set, method_args=['prj'], method_kwargs={'usage': True, 'user_id': 'uid'}, - expected_args=[self.proxy], + expected_args=[ + self.proxy, + False, + None, + None, + False, + ], expected_kwargs={ - 'error_message': None, - 'requires_id': False, + 'microversion': None, + 'resource_response_key': None, 'usage': True, 'user_id': 'uid', }, @@ -1001,11 +1013,16 @@ def test_quota_set_get_defaults(self): 'openstack.resource.Resource.fetch', self.proxy.get_quota_set_defaults, method_args=['prj'], - expected_args=[self.proxy], + expected_args=[ + self.proxy, + False, + '/os-quota-sets/defaults', + None, + False, + ], expected_kwargs={ - 'error_message': None, - 'requires_id': False, - 'base_path': '/os-quota-sets/defaults', + 'microversion': None, + 'resource_response_key': None, }, ) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 996ccb88b..05ac01a2f 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -1654,10 +1654,16 @@ def test_quota_set_get(self): 'openstack.resource.Resource.fetch', self.proxy.get_quota_set, method_args=['prj'], - expected_args=[self.proxy], + expected_args=[ + self.proxy, + False, + None, + None, + False, + ], expected_kwargs={ - 'error_message': None, - 'requires_id': False, + 'microversion': None, + 'resource_response_key': None, }, method_result=quota_set.QuotaSet(), expected_result=quota_set.QuotaSet(), @@ -1669,12 +1675,17 @@ def test_quota_set_get_query(self): self.proxy.get_quota_set, method_args=['prj'], method_kwargs={'usage': True, 'user_id': 'uid'}, - expected_args=[self.proxy], + expected_args=[ + self.proxy, + False, + '/os-quota-sets/%(project_id)s/detail', + None, + False, + ], expected_kwargs={ - 'error_message': None, - 'requires_id': False, + 'microversion': None, + 'resource_response_key': None, 'user_id': 'uid', - 'base_path': '/os-quota-sets/%(project_id)s/detail', }, ) @@ -1683,11 +1694,16 @@ def test_quota_set_get_defaults(self): 'openstack.resource.Resource.fetch', self.proxy.get_quota_set_defaults, method_args=['prj'], - expected_args=[self.proxy], + expected_args=[ + self.proxy, + False, + '/os-quota-sets/%(project_id)s/defaults', + None, + False, + ], expected_kwargs={ - 'error_message': None, - 'requires_id': False, - 'base_path': '/os-quota-sets/%(project_id)s/defaults', + 'microversion': None, + 'resource_response_key': None, }, ) diff --git a/openstack/tests/unit/orchestration/v1/test_stack.py b/openstack/tests/unit/orchestration/v1/test_stack.py index e9614ec08..9019887dc 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack.py +++ b/openstack/tests/unit/orchestration/v1/test_stack.py @@ -183,9 +183,7 @@ def test_create(self, mock_create): res = sot.create(sess) - mock_create.assert_called_once_with( - sess, prepend_key=False, base_path=None - ) + mock_create.assert_called_once_with(sess, False) self.assertEqual(mock_create.return_value, res) def test_check(self): diff --git a/openstack/workflow/v2/cron_trigger.py b/openstack/workflow/v2/cron_trigger.py index 15a338159..139672d2c 100644 --- a/openstack/workflow/v2/cron_trigger.py +++ b/openstack/workflow/v2/cron_trigger.py @@ -71,5 +71,11 @@ class CronTrigger(resource.Resource): #: The time at which the cron trigger was created updated_at = resource.Body("updated_at") - def create(self, session, base_path=None): - return super().create(session, prepend_key=False, base_path=base_path) + def create( + self, + session, + prepend_key=False, + *args, + **kwargs, + ): + return super().create(session, prepend_key, *args, **kwargs) diff --git a/openstack/workflow/v2/execution.py b/openstack/workflow/v2/execution.py index 0f9633c76..2eb2730e8 100644 --- a/openstack/workflow/v2/execution.py +++ b/openstack/workflow/v2/execution.py @@ -58,7 +58,13 @@ class Execution(resource.Resource): #: The time at which the Execution was updated updated_at = resource.Body("updated_at") - def create(self, session, prepend_key=True, base_path=None): + def create( + self, + session, + prepend_key=True, + base_path=None, + **kwargs, + ): request = self._prepare_request( requires_id=False, prepend_key=prepend_key, base_path=base_path ) diff --git a/openstack/workflow/v2/workflow.py b/openstack/workflow/v2/workflow.py index 47e1afe9d..fc21f0322 100644 --- a/openstack/workflow/v2/workflow.py +++ b/openstack/workflow/v2/workflow.py @@ -64,7 +64,13 @@ def _request_kwargs(self, prepend_key=True, base_path=None): request.headers.update(headers) return dict(url=uri, json=None, headers=request.headers, **kwargs) - def create(self, session, prepend_key=True, base_path=None): + def create( + self, + session, + prepend_key=True, + base_path=None, + **kwargs, + ): kwargs = self._request_kwargs( prepend_key=prepend_key, base_path=base_path ) From 2ea501c069c23b534d9f8fa5f5782def784e4db1 Mon Sep 17 00:00:00 2001 From: Artom Lifshitz Date: Fri, 30 Aug 2024 09:56:16 -0400 Subject: [PATCH 3554/3836] stack event loops: handle get None result We've seen at least one case in CI of `stack` being None when polling for stack events: File "openstack/cloud/_orchestration.py", line 185, in delete_stack event_utils.poll_for_events( File "openstack/orchestration/util/event_utils.py", line 109, in poll_for_events stack_status = stack['stack_status'] TypeError: 'NoneType' object is not subscriptable Handle this case with a simple if. Change-Id: Icce6e7b600125cf98e56d98e1c3df4177414398b --- openstack/orchestration/util/event_utils.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/openstack/orchestration/util/event_utils.py b/openstack/orchestration/util/event_utils.py index c84a550c3..4e079f4e0 100644 --- a/openstack/orchestration/util/event_utils.py +++ b/openstack/orchestration/util/event_utils.py @@ -106,11 +106,12 @@ def is_stack_event(event): if no_event_polls >= 2: # after 2 polls with no events, fall back to a stack get stack = cloud.get_stack(stack_name, resolve_outputs=False) - stack_status = stack['stack_status'] - msg = msg_template % dict(name=stack_name, status=stack_status) - if stop_check(stack_status): - return stack_status, msg - # go back to event polling again - no_event_polls = 0 + if stack: + stack_status = stack['stack_status'] + msg = msg_template % dict(name=stack_name, status=stack_status) + if stop_check(stack_status): + return stack_status, msg + # go back to event polling again + no_event_polls = 0 time.sleep(poll_period) From ed96765af5c2641c810de851716d7760cce155f1 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Thu, 5 Sep 2024 15:21:17 +0000 Subject: [PATCH 3555/3836] Update master for stable/2024.2 Add file to the reno documentation build to show release notes for stable/2024.2. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/2024.2. Sem-Ver: feature Change-Id: I664039e780aba09c4b9391f959bba198439f826f --- releasenotes/source/2024.2.rst | 6 ++++++ releasenotes/source/index.rst | 1 + 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/2024.2.rst diff --git a/releasenotes/source/2024.2.rst b/releasenotes/source/2024.2.rst new file mode 100644 index 000000000..aaebcbc8c --- /dev/null +++ b/releasenotes/source/2024.2.rst @@ -0,0 +1,6 @@ +=========================== +2024.2 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2024.2 diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 161acda0e..efdec8e85 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ :maxdepth: 1 unreleased + 2024.2 2024.1 2023.2 2023.1 From b7e8e869fa6641be3e4d41a3b53e93e0c9d005d8 Mon Sep 17 00:00:00 2001 From: Tobias Urdin Date: Tue, 10 Sep 2024 22:29:03 +0200 Subject: [PATCH 3556/3836] Fix volume summary passing non-existent all_projects The parameter all_projects is invalid and the correct parameter to use is all_tenants. We also fix so that more parameter can be passed down to the GET request for filtering. This was found when fixing so that this API call actually passes the parameters down. [1] [1] https://review.opendev.org/c/openstack/cinder/+/928781/1 Change-Id: Iea983a60212d1e950ce78b1d89edcaa822d9458e --- openstack/block_storage/v3/_proxy.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index d279ded4c..a648eb9c3 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -699,7 +699,7 @@ def delete_volume_metadata(self, volume, keys=None): else: volume.delete_metadata(self) - def summary(self, all_projects): + def summary(self, all_projects, **kwargs): """Get Volumes Summary This method returns the volumes summary in the deployment. @@ -716,7 +716,8 @@ def summary(self, all_projects): self, requires_id=False, resource_response_key='volume-summary', - all_projects=all_projects, + all_tenants=all_projects, + **kwargs, ) # ====== VOLUME ACTIONS ====== From 4916542af96792ad9d4d76c30d9c0c640ef21d4c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 12 Sep 2024 12:53:57 +0100 Subject: [PATCH 3557/3836] cloud: Re-add support for passing objects A follow-up patch will resolve the many TODOs we are adding here. Change-Id: Id47a6664ed55b45e4888483d60f565ea9d1204ab Signed-off-by: Stephen Finucane Closes-Bug: #2080513 --- openstack/cloud/_block_storage.py | 19 ++++--- openstack/cloud/_compute.py | 91 +++++++++++++++++++------------ 2 files changed, 69 insertions(+), 41 deletions(-) diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index df959b722..71e0b1b83 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -131,13 +131,18 @@ def create_volume( wait = True if image: - image_obj = self.image.find_image(image) - if not image_obj: - raise exceptions.SDKException( - f"Image {image} was requested as the basis for a new " - f"volume but was not found on the cloud" - ) - kwargs['imageRef'] = image_obj['id'] + # TODO(stephenfin): Drop support for dicts: we should only accept + # strings or Image objects + if isinstance(image, dict): + kwargs['imageRef'] = image['id'] + else: # object + image_obj = self.image.find_image(image) + if not image_obj: + raise exceptions.SDKException( + f"Image {image} was requested as the basis for a new " + f"volume but was not found on the cloud" + ) + kwargs['imageRef'] = image_obj['id'] kwargs = self._get_volume_kwargs(kwargs) kwargs['size'] = size diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 0b9267def..e0a54db40 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -827,15 +827,21 @@ def create_server( kwargs[desired] = value if group: - group_obj = self.compute.find_server_group(group) - if not group_obj: - raise exceptions.SDKException( - "Server Group {group} was requested but was not found" - " on the cloud".format(group=group) - ) + if isinstance(group, dict): + # TODO(stephenfin): Drop support for dicts: we should only + # accept strings or Image objects + group_id = group['id'] + else: # object + group_obj = self.compute.find_server_group(group) + if not group_obj: + raise exceptions.SDKException( + "Server Group {group} was requested but was not found" + " on the cloud".format(group=group) + ) + group_id = group_obj['id'] if 'scheduler_hints' not in kwargs: kwargs['scheduler_hints'] = {} - kwargs['scheduler_hints']['group'] = group_obj['id'] + kwargs['scheduler_hints']['group'] = group_id kwargs.setdefault('max_count', kwargs.get('max_count', 1)) kwargs.setdefault('min_count', kwargs.get('min_count', 1)) @@ -854,21 +860,24 @@ def create_server( nics = [] if not isinstance(network, list): network = [network] - for net_name in network: - if isinstance(net_name, dict) and 'id' in net_name: - network_obj = net_name + for net in network: + if isinstance(net, dict): + # TODO(stephenfin): Drop support for dicts: we should only + # accept strings or Network objects + network_id = net['id'] else: - network_obj = self.network.find_network(net_name) - if not network_obj: - raise exceptions.SDKException( - 'Network {network} is not a valid network in' - ' {cloud}:{region}'.format( - network=network, - cloud=self.name, - region=self._compute_region, + network_obj = self.network.find_network(net) + if not network_obj: + raise exceptions.SDKException( + 'Network {network} is not a valid network in' + ' {cloud}:{region}'.format( + network=network, + cloud=self.name, + region=self._compute_region, + ) ) - ) - nics.append({'net-id': network_obj['id']}) + network_id = network_obj['id'] + nics.append({'net-id': network_id}) kwargs['nics'] = nics if not network and ('nics' not in kwargs or not kwargs['nics']): @@ -1032,17 +1041,23 @@ def _get_boot_from_volume_kwargs( # If we have boot_from_volume but no root volume, then we're # booting an image from volume if boot_volume: - volume = self.block_storage.find_volume(boot_volume) - if not volume: - raise exceptions.SDKException( - f"Volume {volume} was requested but was not found " - f"on the cloud" - ) + # TODO(stephenfin): Drop support for dicts: we should only accept + # strings or Volume objects + if isinstance(boot_volume, dict): + volume_id = boot_volume['id'] + else: + volume = self.block_storage.find_volume(boot_volume) + if not volume: + raise exceptions.SDKException( + f"Volume {volume} was requested but was not found " + f"on the cloud" + ) + volume_id = volume['id'] block_mapping = { 'boot_index': '0', 'delete_on_termination': terminate_volume, 'destination_type': 'volume', - 'uuid': volume['id'], + 'uuid': volume_id, 'source_type': 'volume', } kwargs['block_device_mapping_v2'].append(block_mapping) @@ -1070,6 +1085,7 @@ def _get_boot_from_volume_kwargs( } kwargs['imageRef'] = '' kwargs['block_device_mapping_v2'].append(block_mapping) + if volumes and kwargs['imageRef']: # If we're attaching volumes on boot but booting from an image, # we need to specify that in the BDM. @@ -1081,18 +1097,25 @@ def _get_boot_from_volume_kwargs( 'uuid': kwargs['imageRef'], } kwargs['block_device_mapping_v2'].append(block_mapping) + for volume in volumes: - volume_obj = self.block_storage.find_volume(volume) - if not volume_obj: - raise exceptions.SDKException( - f"Volume {volume} was requested but was not found " - f"on the cloud" - ) + # TODO(stephenfin): Drop support for dicts: we should only accept + # strings or Image objects + if isinstance(volume, dict): + volume_id = volume['id'] + else: + volume_obj = self.block_storage.find_volume(volume) + if not volume_obj: + raise exceptions.SDKException( + f"Volume {volume} was requested but was not found " + f"on the cloud" + ) + volume_id = volume_obj['id'] block_mapping = { 'boot_index': '-1', 'delete_on_termination': False, 'destination_type': 'volume', - 'uuid': volume_obj['id'], + 'uuid': volume_id, 'source_type': 'volume', } kwargs['block_device_mapping_v2'].append(block_mapping) From c07e99525bce474f013b98d56acd55c449478c67 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 12 Sep 2024 13:08:42 +0100 Subject: [PATCH 3558/3836] cloud: Deprecate passing dicts We want users to use strings or Resource objects instead. To do this, we add a new warning for stuff we will remove in 6.0, and remove the now unused warning for stuff from 4.0. Change-Id: I7c8fb708d18c7e3b3360676dff9fb236581e5f44 Signed-off-by: Stephen Finucane --- openstack/cloud/_block_storage.py | 10 ++++- openstack/cloud/_compute.py | 66 ++++++++++++++++++++++++------- openstack/warnings.py | 8 ++-- 3 files changed, 64 insertions(+), 20 deletions(-) diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index 71e0b1b83..f6e8d4bf3 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -15,6 +15,7 @@ from openstack.cloud import _utils from openstack.cloud import openstackcloud from openstack import exceptions +from openstack import resource from openstack import warnings as os_warnings @@ -131,9 +132,14 @@ def create_volume( wait = True if image: - # TODO(stephenfin): Drop support for dicts: we should only accept - # strings or Image objects if isinstance(image, dict): + if not isinstance(image, resource.Resource): + warnings.warn( + "Support for passing image as a raw dict has " + "been deprecated for removal. Consider passing a " + "string name or ID or an Image object instead.", + os_warnings.RemovedInSDK60Warning, + ) kwargs['imageRef'] = image['id'] else: # object image_obj = self.image.find_image(image) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index e0a54db40..017df3883 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -14,6 +14,7 @@ import functools import operator import time +import warnings import iso8601 @@ -23,7 +24,9 @@ from openstack.cloud import meta from openstack.compute.v2 import server as _server from openstack import exceptions +from openstack import resource from openstack import utils +from openstack import warnings as os_warnings _SERVER_FIELDS = ( @@ -828,8 +831,13 @@ def create_server( if group: if isinstance(group, dict): - # TODO(stephenfin): Drop support for dicts: we should only - # accept strings or Image objects + if not isinstance(group, resource.Resource): + warnings.warn( + "Support for passing server group as a raw dict has " + "been deprecated for removal. Consider passing a " + "string name or ID or a ServerGroup object instead.", + os_warnings.RemovedInSDK60Warning, + ) group_id = group['id'] else: # object group_obj = self.compute.find_server_group(group) @@ -862,8 +870,13 @@ def create_server( network = [network] for net in network: if isinstance(net, dict): - # TODO(stephenfin): Drop support for dicts: we should only - # accept strings or Network objects + if not isinstance(net, resource.Resource): + warnings.warn( + "Support for passing network as a raw dict has " + "been deprecated for removal. Consider passing a " + "string name or ID or a Network object instead.", + os_warnings.RemovedInSDK60Warning, + ) network_id = net['id'] else: network_obj = self.network.find_network(net) @@ -935,9 +948,14 @@ def create_server( kwargs['networks'] = 'auto' if image: - # TODO(stephenfin): Drop support for dicts: we should only accept - # strings or Image objects if isinstance(image, dict): + if not isinstance(image, resource.Resource): + warnings.warn( + "Support for passing image as a raw dict has " + "been deprecated for removal. Consider passing a " + "string name or ID or an Image object instead.", + os_warnings.RemovedInSDK60Warning, + ) kwargs['imageRef'] = image['id'] else: image_obj = self.image.find_image(image) @@ -948,9 +966,14 @@ def create_server( ) kwargs['imageRef'] = image_obj.id - # TODO(stephenfin): Drop support for dicts: we should only accept - # strings or Image objects if isinstance(flavor, dict): + if not isinstance(flavor, resource.Resource): + warnings.warn( + "Support for passing flavor as a raw dict has " + "been deprecated for removal. Consider passing a " + "string name or ID or a Flavor object instead.", + os_warnings.RemovedInSDK60Warning, + ) kwargs['flavorRef'] = flavor['id'] else: kwargs['flavorRef'] = self.get_flavor(flavor, get_extra=False).id @@ -1041,9 +1064,14 @@ def _get_boot_from_volume_kwargs( # If we have boot_from_volume but no root volume, then we're # booting an image from volume if boot_volume: - # TODO(stephenfin): Drop support for dicts: we should only accept - # strings or Volume objects if isinstance(boot_volume, dict): + if not isinstance(boot_volume, resource.Resource): + warnings.warn( + "Support for passing boot_volume as a raw dict has " + "been deprecated for removal. Consider passing a " + "string name or ID or a Volume object instead.", + os_warnings.RemovedInSDK60Warning, + ) volume_id = boot_volume['id'] else: volume = self.block_storage.find_volume(boot_volume) @@ -1063,9 +1091,14 @@ def _get_boot_from_volume_kwargs( kwargs['block_device_mapping_v2'].append(block_mapping) kwargs['imageRef'] = '' elif boot_from_volume: - # TODO(stephenfin): Drop support for dicts: we should only accept - # strings or Image objects if isinstance(image, dict): + if not isinstance(image, resource.Resource): + warnings.warn( + "Support for passing image as a raw dict has " + "been deprecated for removal. Consider passing a " + "string name or ID or an Image object instead.", + os_warnings.RemovedInSDK60Warning, + ) image_obj = image else: image_obj = self.image.find_image(image) @@ -1099,9 +1132,14 @@ def _get_boot_from_volume_kwargs( kwargs['block_device_mapping_v2'].append(block_mapping) for volume in volumes: - # TODO(stephenfin): Drop support for dicts: we should only accept - # strings or Image objects if isinstance(volume, dict): + if not isinstance(volume, resource.Resource): + warnings.warn( + "Support for passing volumes as a list of raw dicts " + "been deprecated for removal. Consider passing a list " + "of string name or ID or ServerGroup objects instead.", + os_warnings.RemovedInSDK60Warning, + ) volume_id = volume['id'] else: volume_obj = self.block_storage.find_volume(volume) diff --git a/openstack/warnings.py b/openstack/warnings.py index b0f80dcc5..e12d85d13 100644 --- a/openstack/warnings.py +++ b/openstack/warnings.py @@ -42,14 +42,14 @@ class LegacyAPIWarning(OpenStackDeprecationWarning): # function parameters. -class RemovedInSDK40Warning(DeprecationWarning): - """Indicates an argument that is deprecated for removal in SDK 4.0.""" - - class RemovedInSDK50Warning(PendingDeprecationWarning): """Indicates an argument that is deprecated for removal in SDK 5.0.""" +class RemovedInSDK60Warning(PendingDeprecationWarning): + """Indicates an argument that is deprecated for removal in SDK 6.0.""" + + # General warnings # # These are usually related to misconfigurations. From cf4cec6e0e27e3574b02ee1e0b747682c4cb9ebd Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Mon, 27 May 2024 15:25:02 +0200 Subject: [PATCH 3559/3836] Do not create a class in runtime on a potentially hot path Creating classes is not cheap in Python, and Resource._validate can be called pretty often. Furthermore, it does not seem like AccessSaver copies get deallocated correctly: I have an environment where tracemalloc shows a steady increase of memory in this spot. Change-Id: I149c5d880692f21732346a71c031081bef015b27 --- openstack/utils.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/openstack/utils.py b/openstack/utils.py index 24c409c58..4ed7738b2 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -73,6 +73,16 @@ def iterate_timeout(timeout, message, wait=2): raise exceptions.ResourceTimeout(message) +class _AccessSaver: + __slots__ = ('keys',) + + def __init__(self): + self.keys = [] + + def __getitem__(self, key): + self.keys.append(key) + + def get_string_format_keys(fmt_string, old_style=True): """Gets a list of required keys from a format string @@ -80,15 +90,7 @@ def get_string_format_keys(fmt_string, old_style=True): use the old style string formatting. """ if old_style: - - class AccessSaver: - def __init__(self): - self.keys = [] - - def __getitem__(self, key): - self.keys.append(key) - - a = AccessSaver() + a = _AccessSaver() fmt_string % a return a.keys From 6a434023f2375e087aeeb5595d6b6222fa21e325 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 16 Jul 2024 12:40:06 +0100 Subject: [PATCH 3560/3836] Add timeline for removal of deprecated features Not all deprecated features are deprecated for removal, but those that are need a hint for when they will start being removed. Start making use of the new class of version-specific deprecation warnings added in change I8bb0bfb039593c5b59f1f9c16523a090d899f099 to set timers for the various deprecated-for-removal features we have. Change-Id: I17d6703e82d49fca9743571f2bf1ba9c1fb3d190 Signed-off-by: Stephen Finucane --- openstack/cloud/_baremetal.py | 10 +++++----- openstack/cloud/_block_storage.py | 4 ++-- openstack/cloud/_identity.py | 4 ++-- openstack/cloud/_network_common.py | 2 +- openstack/compute/v2/_proxy.py | 6 +++--- openstack/config/cloud_region.py | 11 ++++++----- openstack/image/v1/_proxy.py | 2 +- openstack/image/v2/_proxy.py | 2 +- openstack/tests/fixtures.py | 4 ++++ openstack/tests/unit/compute/v2/test_proxy.py | 2 +- 10 files changed, 26 insertions(+), 21 deletions(-) diff --git a/openstack/cloud/_baremetal.py b/openstack/cloud/_baremetal.py index e9eee152b..fae1a46f3 100644 --- a/openstack/cloud/_baremetal.py +++ b/openstack/cloud/_baremetal.py @@ -302,7 +302,7 @@ def unregister_machine(self, nics, uuid, wait=None, timeout=600): if wait is not None: warnings.warn( "wait argument is deprecated and has no effect", - os_warnings.OpenStackDeprecationWarning, + os_warnings.RemovedInSDK50Warning, ) machine = self.get_machine(uuid) @@ -461,7 +461,7 @@ def validate_machine(self, name_or_id, for_deploy=True): def validate_node(self, uuid): warnings.warn( 'validate_node is deprecated, please use validate_machine instead', - os_warnings.OpenStackDeprecationWarning, + os_warnings.RemovedInSDK50Warning, ) self.baremetal.validate_node(uuid) @@ -602,7 +602,7 @@ def set_node_instance_info(self, uuid, patch): warnings.warn( "The set_node_instance_info call is deprecated, " "use patch_machine or update_machine instead", - os_warnings.OpenStackDeprecationWarning, + os_warnings.RemovedInSDK50Warning, ) return self.patch_machine(uuid, patch) @@ -610,7 +610,7 @@ def purge_node_instance_info(self, uuid): warnings.warn( "The purge_node_instance_info call is deprecated, " "use patch_machine or update_machine instead", - os_warnings.OpenStackDeprecationWarning, + os_warnings.RemovedInSDK50Warning, ) return self.patch_machine( uuid, dict(path='/instance_info', op='remove') @@ -629,6 +629,6 @@ def wait_for_baremetal_node_lock(self, node, timeout=30): "The wait_for_baremetal_node_lock call is deprecated " "in favor of wait_for_node_reservation on the baremetal " "proxy", - os_warnings.OpenStackDeprecationWarning, + os_warnings.RemovedInSDK50Warning, ) self.baremetal.wait_for_node_reservation(node, timeout) diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index df959b722..0d34358ac 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -29,7 +29,7 @@ def list_volumes(self, cache=True): warnings.warn( "the 'cache' argument is deprecated and no longer does anything; " "consider removing it from calls", - os_warnings.OpenStackDeprecationWarning, + os_warnings.RemovedInSDK50Warning, ) return list(self.block_storage.volumes()) @@ -45,7 +45,7 @@ def list_volume_types(self, get_extra=None): warnings.warn( "the 'get_extra' argument is deprecated and no longer does " "anything; consider removing it from calls", - os_warnings.OpenStackDeprecationWarning, + os_warnings.RemovedInSDK50Warning, ) return list(self.block_storage.types()) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index eb78bb2ed..09c08df01 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -1064,7 +1064,7 @@ def _keystone_v3_role_assignments(self, **filters): warnings.warn( "os_inherit_extension_inherited_to is deprecated. Use " "inherited_to instead.", - os_warnings.OpenStackDeprecationWarning, + os_warnings.RemovedInSDK50Warning, ) filters['scope.OS-INHERIT:inherited_to'] = filters[ 'os_inherit_extension_inherited_to' @@ -1134,7 +1134,7 @@ def list_role_assignments(self, filters=None): warnings.warn( "os_inherit_extension_inherited_to is deprecated. Use " "inherited_to instead.", - os_warnings.OpenStackDeprecationWarning, + os_warnings.RemovedInSDK50Warning, ) filters['inherited_to'] = filters.pop( 'os_inherit_extension_inherited_to' diff --git a/openstack/cloud/_network_common.py b/openstack/cloud/_network_common.py index 43633d7cd..8d5a3e535 100644 --- a/openstack/cloud/_network_common.py +++ b/openstack/cloud/_network_common.py @@ -429,7 +429,7 @@ def search_floating_ips(self, id=None, filters=None): # understand, obviously. warnings.warn( "search_floating_ips is deprecated. Use search_resource instead.", - os_warnings.OpenStackDeprecationWarning, + os_warnings.RemovedInSDK50Warning, ) if self._use_neutron_floating() and isinstance(filters, dict): return list(self.network.ips(**filters)) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 33086fd25..f2a2bf2ba 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -1925,7 +1925,7 @@ def create_volume_attachment(self, server, volume=None, **attrs): ) warnings.warn( deprecation_msg, - os_warnings.OpenStackDeprecationWarning, + os_warnings.RemovedInSDK50Warning, ) else: volume_id = resource.Resource._get_id(volume) @@ -2001,7 +2001,7 @@ def _verify_server_volume_args(self, server, volume): ): warnings.warn( deprecation_msg, - os_warnings.OpenStackDeprecationWarning, + os_warnings.RemovedInSDK50Warning, ) return volume, server @@ -2012,7 +2012,7 @@ def _verify_server_volume_args(self, server, volume): else: warnings.warn( deprecation_msg, - os_warnings.OpenStackDeprecationWarning, + os_warnings.RemovedInSDK50Warning, ) return volume, server diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 3b7cac69d..a7ad99201 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -661,9 +661,9 @@ def set_auth_cache(self): try: if state: - # NOTE: under some conditions the method may be invoked when auth - # is empty. This may lead to exception in the keyring lib, thus do - # nothing. + # NOTE: under some conditions the method may be invoked when + # auth is empty. This may lead to exception in the keyring lib, + # thus do nothing. keyring.set_password('openstacksdk', cache_id, state) except RuntimeError: # the fail backend raises this self.log.debug('Failed to set auth into keyring') @@ -699,8 +699,9 @@ def get_session(self): # cert verification if not verify: self.log.debug( - "Turning off SSL warnings for {full_name}" - " since verify=False".format(full_name=self.full_name) + 'Turning off SSL warnings for %(full_name)s since ' + 'verify=False', + {'full_name': self.full_name}, ) requestsexceptions.squelch_warnings(insecure_requests=not verify) self._keystone_session = self._session_constructor( diff --git a/openstack/image/v1/_proxy.py b/openstack/image/v1/_proxy.py index 73b4828f6..5419f8c11 100644 --- a/openstack/image/v1/_proxy.py +++ b/openstack/image/v1/_proxy.py @@ -258,7 +258,7 @@ def upload_image(self, **attrs): """ warnings.warn( "upload_image is deprecated. Use create_image instead.", - os_warnings.OpenStackDeprecationWarning, + os_warnings.RemovedInSDK50Warning, ) return self._create(_image.Image, **attrs) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 63eb7a17a..5d54dbff5 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -506,7 +506,7 @@ def upload_image( """ warnings.warn( "upload_image is deprecated. Use create_image instead.", - os_warnings.OpenStackDeprecationWarning, + os_warnings.RemovedInSDK50Warning, ) # container_format and disk_format are required to be set # on the image by the time upload_image is called, but they're not diff --git a/openstack/tests/fixtures.py b/openstack/tests/fixtures.py index a7c202473..3ee41849c 100644 --- a/openstack/tests/fixtures.py +++ b/openstack/tests/fixtures.py @@ -45,6 +45,10 @@ def setUp(self): "ignore", category=os_warnings.OpenStackDeprecationWarning, ) + warnings.filterwarnings( + "ignore", + category=os_warnings.RemovedInSDK50Warning, + ) # also ignore our own general warnings warnings.filterwarnings( diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 996ccb88b..6bcedaf23 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -527,7 +527,7 @@ def test_volume_attachment_create__legacy_parameters(self): self.assertEqual(1, len(w)) self.assertEqual( - os_warnings.OpenStackDeprecationWarning, + os_warnings.RemovedInSDK50Warning, w[-1].category, ) self.assertIn( From 269229077679367a03bfe7552a9ea94dd573710c Mon Sep 17 00:00:00 2001 From: Pavlo Shchelokovskyy Date: Thu, 5 Sep 2024 18:21:52 +0300 Subject: [PATCH 3561/3836] Return payload as text only for text/plain secrets currently SDK always returns Response.text for secret payload, which is problematic for pure binary secrets, as the value returned is decoded as whatever encoding `chardet` lib has detected on this random binary data. This makes SDK basically unusable for retreving and using binary secrets, as one can not make any educated guess using only sdk-provided data on what encoding to use to get the payload as raw bytes. Instead, do what barbicanclient does, and return raw bytes (Response.content) for everything but content type "text/plain", for which decode those bytes to UTF-8. This will also make it easier to transition from barbicanclient to openstacksdk if needed. Change-Id: I0e5ac1288c0d0423fa3a7a4e63173675b78aae79 --- openstack/key_manager/v1/secret.py | 5 +++- .../tests/unit/key_manager/v1/test_secret.py | 23 ++++++++++++++----- ...ret-payload-as-bytes-d04370d85c9efc4c.yaml | 16 +++++++++++++ 3 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/secret-payload-as-bytes-d04370d85c9efc4c.yaml diff --git a/openstack/key_manager/v1/secret.py b/openstack/key_manager/v1/secret.py index aba4bca9f..04a530473 100644 --- a/openstack/key_manager/v1/secret.py +++ b/openstack/key_manager/v1/secret.py @@ -112,7 +112,10 @@ def fetch( headers={"Accept": content_type}, skip_cache=skip_cache, ) - response["payload"] = payload.text + if content_type == "text/plain": + response["payload"] = payload.content.decode("UTF-8") + else: + response["payload"] = payload.content # We already have the JSON here so don't call into _translate_response self._update_from_body_attrs(response) diff --git a/openstack/tests/unit/key_manager/v1/test_secret.py b/openstack/tests/unit/key_manager/v1/test_secret.py index 819e2a930..b0493fa18 100644 --- a/openstack/tests/unit/key_manager/v1/test_secret.py +++ b/openstack/tests/unit/key_manager/v1/test_secret.py @@ -100,16 +100,14 @@ def test_get_no_payload(self): sess.get.assert_called_once_with("secrets/id") - def _test_payload(self, sot, metadata, content_type): - content_type = "some/type" - + def _test_payload(self, sot, metadata, content_type="some/type"): metadata_response = mock.Mock() # Use copy because the dict gets consumed. metadata_response.json = mock.Mock(return_value=metadata.copy()) payload_response = mock.Mock() - payload = "secret info" - payload_response.text = payload + payload = b"secret info" + payload_response.content = payload sess = mock.Mock() sess.get = mock.Mock(side_effect=[metadata_response, payload_response]) @@ -129,7 +127,11 @@ def _test_payload(self, sot, metadata, content_type): ] ) - self.assertEqual(rv.payload, payload) + if content_type == "text/plain": + expected_payload = payload.decode("utf-8") + else: + expected_payload = payload + self.assertEqual(rv.payload, expected_payload) self.assertEqual(rv.status, metadata["status"]) def test_get_with_payload_from_argument(self): @@ -146,3 +148,12 @@ def test_get_with_payload_from_content_types(self): } sot = secret.Secret(id="id") self._test_payload(sot, metadata, content_type) + + def test_get_with_text_payload(self): + content_type = "text/plain" + metadata = { + "status": "fine", + "content_types": {"default": content_type}, + } + sot = secret.Secret(id="id") + self._test_payload(sot, metadata, content_type) diff --git a/releasenotes/notes/secret-payload-as-bytes-d04370d85c9efc4c.yaml b/releasenotes/notes/secret-payload-as-bytes-d04370d85c9efc4c.yaml new file mode 100644 index 000000000..446728fb5 --- /dev/null +++ b/releasenotes/notes/secret-payload-as-bytes-d04370d85c9efc4c.yaml @@ -0,0 +1,16 @@ +--- +features: + - | + For Barbican secrets with detected or provided content type other than + "text/plain" SDK now returns the secret payload as raw bytes. + For secrets with content type "text/plain", the payload is returned + as string, decoded to UTF-8. + This behavior is following python-barbicanclient, and allows to use + SDK with Barbican secrets that have binary payloads + (e.g. "application/octet-stream"). +upgrade: + - | + The payload of Barbican secrets with other than "text/plain" content type + is now returned as raw bytes. + For secrets with content type "text/plain", the payload is returned + as string, decoded to UTF-8. From 8c8b396742ac29dbf702d2a0f3803e7d697e61dd Mon Sep 17 00:00:00 2001 From: Pavlo Shchelokovskyy Date: Thu, 12 Sep 2024 16:39:11 +0300 Subject: [PATCH 3562/3836] Followup to I0e5ac1288c0d0423fa3a7a4e63173675b78aae79 add code comment on why using response.text is a bad idea here. Change-Id: I15188a4424e7bbaed47bddc4df07adc7c14913f0 --- openstack/key_manager/v1/secret.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openstack/key_manager/v1/secret.py b/openstack/key_manager/v1/secret.py index 04a530473..fc8515f7a 100644 --- a/openstack/key_manager/v1/secret.py +++ b/openstack/key_manager/v1/secret.py @@ -112,6 +112,13 @@ def fetch( headers={"Accept": content_type}, skip_cache=skip_cache, ) + # NOTE(pas-ha): do not return payload.text here, + # as it will be decoded to whatever requests and chardet detected, + # and if they are wrong, there'd be no way of getting original + # bytes back and try to re-decode. + # At least this way, the encoding is always the same, + # so SDK user can always get original bytes back and fix things. + # Besides, this is exactly what python-barbicanclient does. if content_type == "text/plain": response["payload"] = payload.content.decode("UTF-8") else: From 6910a4e1d5a76bc430d04ff967784b669bc26fbe Mon Sep 17 00:00:00 2001 From: Tomas Rinblad Date: Sat, 1 Jun 2024 14:23:11 +0200 Subject: [PATCH 3563/3836] compute: Add support for targeting host during migrate Current setup of the migrate command does not support targeting a specific compute node. With this change I want to introduce the possibility to target specific compute nodes. Change-Id: I7ae40d4513d685e4e93ab4120058a53a849a2d19 --- openstack/compute/v2/_proxy.py | 5 +++-- openstack/compute/v2/server.py | 13 +++++++++++-- .../support-migration-to-host-b2958b3b8c5ca1fb.yaml | 5 +++++ 3 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/support-migration-to-host-b2958b3b8c5ca1fb.yaml diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 33086fd25..675b47003 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -2095,15 +2095,16 @@ def volume_attachments(self, server, **query): # ========== Server Migrations ========== - def migrate_server(self, server): + def migrate_server(self, server, *, host=None): """Migrate a server from one host to another :param server: Either the ID of a server or a :class:`~openstack.compute.v2.server.Server` instance. + :param str host: The host to which to migrate the server. :returns: None """ server = self._get_resource(_server.Server, server) - server.migrate(self) + server.migrate(self, host=host) def live_migrate_server( self, diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 2099e554f..e7d18637b 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -841,13 +841,22 @@ def unshelve(self, session, availability_zone=_sentinel, host=None): body = {'unshelve': data or None} self._action(session, body) - def migrate(self, session): + def migrate(self, session, *, host=None): """Migrate the server. :param session: The session to use for making this request. + :param host: The host to migrate the server to. (Optional) :returns: None """ - body = {"migrate": None} + if host and not utils.supports_microversion(session, '2.56'): + raise ValueError( + "The 'host' option is only supported on microversion 2.56 or " + "greater." + ) + + body: ty.Dict[str, ty.Any] = {"migrate": None} + if host: + body["migrate"] = {"host": host} self._action(session, body) def trigger_crash_dump(self, session): diff --git a/releasenotes/notes/support-migration-to-host-b2958b3b8c5ca1fb.yaml b/releasenotes/notes/support-migration-to-host-b2958b3b8c5ca1fb.yaml new file mode 100644 index 000000000..839b4c0f2 --- /dev/null +++ b/releasenotes/notes/support-migration-to-host-b2958b3b8c5ca1fb.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + The ``migrate_server`` compute proxy API and the ``Server.migrate`` API now + accept a ``host`` parameter to migrate to a given host. From ad03b4f64271d11e5b0d984cb51e30c26ba3fab9 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 16 Sep 2024 11:51:31 +0100 Subject: [PATCH 3564/3836] tests: Migrate quota tests to use a new project We also combine multiple tests into a single test, as per best practices nowadays. Change-Id: I220ed9434c28469ca001d59e84f274ea35902a17 Signed-off-by: Stephen Finucane --- .../tests/functional/cloud/test_limits.py | 8 +-- .../functional/compute/v2/test_quota_set.py | 57 +++++++++++-------- .../tests/functional/network/v2/test_quota.py | 54 +++++++++++------- .../test_quota_class_set.py | 40 +++++++------ 4 files changed, 88 insertions(+), 71 deletions(-) diff --git a/openstack/tests/functional/cloud/test_limits.py b/openstack/tests/functional/cloud/test_limits.py index 5d6da9bf7..e4e6a19e7 100644 --- a/openstack/tests/functional/cloud/test_limits.py +++ b/openstack/tests/functional/cloud/test_limits.py @@ -23,7 +23,7 @@ class TestUsage(base.BaseFunctionalTest): def test_get_our_compute_limits(self): - '''Test quotas functionality''' + """Test limits functionality""" limits = self.user_cloud.get_compute_limits() self.assertIsNotNone(limits) @@ -32,7 +32,7 @@ def test_get_our_compute_limits(self): self.assertIsNotNone(limits.image_meta) def test_get_other_compute_limits(self): - '''Test quotas functionality''' + """Test limits functionality""" if not self.operator_cloud: self.skipTest("Operator cloud is required for this test") @@ -44,13 +44,13 @@ def test_get_other_compute_limits(self): self.assertFalse(hasattr(limits, 'maxImageMeta')) def test_get_our_volume_limits(self): - '''Test quotas functionality''' + """Test limits functionality""" limits = self.user_cloud.get_volume_limits() self.assertIsNotNone(limits) self.assertFalse(hasattr(limits, 'maxTotalVolumes')) def test_get_other_volume_limits(self): - '''Test quotas functionality''' + """Test limits functionality""" if not self.operator_cloud: self.skipTest("Operator cloud is required for this test") diff --git a/openstack/tests/functional/compute/v2/test_quota_set.py b/openstack/tests/functional/compute/v2/test_quota_set.py index ba7461bef..1b992e1b7 100644 --- a/openstack/tests/functional/compute/v2/test_quota_set.py +++ b/openstack/tests/functional/compute/v2/test_quota_set.py @@ -10,35 +10,42 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.compute.v2 import quota_set as _quota_set from openstack.tests.functional import base -class TestQS(base.BaseFunctionalTest): - def test_qs(self): - sot = self.conn.compute.get_quota_set(self.conn.current_project_id) - self.assertIsNotNone(sot.key_pairs) +class TestQuotaSet(base.BaseFunctionalTest): + def setUp(self): + super().setUp() - def test_qs_user(self): - sot = self.conn.compute.get_quota_set( - self.conn.current_project_id, - user_id=self.conn.session.auth.get_user_id(self.conn.compute), - ) - self.assertIsNotNone(sot.key_pairs) - - def test_update(self): - sot = self.conn.compute.get_quota_set(self.conn.current_project_id) - self.conn.compute.update_quota_set( - sot, - query={ - 'user_id': self.conn.session.auth.get_user_id( - self.conn.compute - ) - }, - key_pairs=100, + if not self.operator_cloud: + self.skipTest("Operator cloud required for this test") + + self.project = self.create_temporary_project() + + def test_quota_set(self): + # update quota + + quota_set = self.operator_cloud.compute.update_quota_set( + self.project.id, key_pairs=123 ) + self.assertIsInstance(quota_set, _quota_set.QuotaSet) + self.assertEqual(quota_set.key_pairs, 123) + + # retrieve details of the (updated) quota - def test_revert(self): - self.conn.compute.revert_quota_set( - self.conn.current_project_id, - user_id=self.conn.session.auth.get_user_id(self.conn.compute), + quota_set = self.operator_cloud.compute.get_quota_set(self.project.id) + self.assertIsInstance(quota_set, _quota_set.QuotaSet) + self.assertEqual(quota_set.key_pairs, 123) + + # retrieve quota defaults + + defaults = self.operator_cloud.compute.get_quota_set_defaults( + self.project.id ) + self.assertIsInstance(defaults, _quota_set.QuotaSet) + self.assertNotEqual(defaults.key_pairs, 123) + + # revert quota + + self.operator_cloud.compute.revert_quota_set(self.project.id) diff --git a/openstack/tests/functional/network/v2/test_quota.py b/openstack/tests/functional/network/v2/test_quota.py index fdab400ab..248f81583 100644 --- a/openstack/tests/functional/network/v2/test_quota.py +++ b/openstack/tests/functional/network/v2/test_quota.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import quota as _quota from openstack.tests.functional import base @@ -20,26 +21,37 @@ def setUp(self): if not self.operator_cloud: self.skipTest("Operator cloud required for this test") - def test_list(self): - for qot in self.operator_cloud.network.quotas(): - self.assertIsNotNone(qot.project_id) - self.assertIsNotNone(qot.networks) + self.project = self.create_temporary_project() - def test_list_details(self): - expected_keys = ["limit", "used", "reserved"] - project_id = self.operator_cloud.session.get_project_id() - quota_details = self.operator_cloud.network.get_quota( - project_id, details=True + def test_quota(self): + # update quota + + quota = self.operator_cloud.network.update_quota( + self.project.id, networks=123456789 + ) + self.assertIsInstance(quota, _quota.Quota) + self.assertEqual(quota.networks, 123456789) + + # retrieve details of the (updated) quota + + quota = self.operator_cloud.network.get_quota(self.project.id) + self.assertIsInstance(quota, _quota.Quota) + self.assertEqual(quota.networks, 123456789) + + # retrieve quota defaults + + defaults = self.operator_cloud.network.get_quota_default( + self.project.id ) - for details in quota_details._body.attributes.values(): - for expected_key in expected_keys: - self.assertTrue(expected_key in details.keys()) - - def test_set(self): - attrs = {"networks": 123456789} - for project_quota in self.operator_cloud.network.quotas(): - self.operator_cloud.network.update_quota(project_quota, **attrs) - new_quota = self.operator_cloud.network.get_quota( - project_quota.project_id - ) - self.assertEqual(123456789, new_quota.networks) + self.assertIsInstance(defaults, _quota.QuotaDefault) + self.assertNotEqual(defaults.networks, 123456789) + + # list quotas + + quotas = list(self.operator_cloud.network.quotas()) + self.assertIn(self.project.id, [x.project_id for x in quotas]) + + # revert quota + + ret = self.operator_cloud.network.delete_quota(self.project.id) + self.assertIsNone(ret) diff --git a/openstack/tests/functional/shared_file_system/test_quota_class_set.py b/openstack/tests/functional/shared_file_system/test_quota_class_set.py index a9815e704..1bc7cfb34 100644 --- a/openstack/tests/functional/shared_file_system/test_quota_class_set.py +++ b/openstack/tests/functional/shared_file_system/test_quota_class_set.py @@ -10,34 +10,32 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.shared_file_system.v2 import quota_class_set as _quota_class_set from openstack.tests.functional.shared_file_system import base class QuotaClassSetTest(base.BaseSharedFileSystemTest): - def test_quota_class_set(self): - project_id = self.operator_cloud.current_project_id + def setUp(self): + super().setUp() - initial_quota_class_set = ( - self.operator_cloud.share.get_quota_class_set(project_id) - ) - self.assertIn('shares', initial_quota_class_set) + if not self.operator_cloud: + self.skipTest("Operator cloud required for this test") - initial_backups_value = initial_quota_class_set['backups'] + self.project = self.create_temporary_project() - updated_quota_class_set = ( - self.operator_cloud.share.update_quota_class_set( - project_id, - **{ - "backups": initial_backups_value + 1, - }, - ) - ) - self.assertEqual( - updated_quota_class_set['backups'], initial_backups_value + 1 - ) + def test_quota_class_set(self): + # set quota class set - reverted = self.operator_cloud.share.update_quota_class_set( - project_id, **{"backups": initial_backups_value} + quota_class_set = self.operator_cloud.share.update_quota_class_set( + self.project.id, backups=123 ) + self.assertIsInstance(quota_class_set, _quota_class_set.QuotaClassSet) + self.assertEqual(quota_class_set.backups, 123) + + # retrieve details of the (updated) quota class set - self.assertEqual(initial_quota_class_set, reverted) + quota_class_set = self.operator_cloud.share.get_quota_class_set( + self.project.id + ) + self.assertIsInstance(quota_class_set, _quota_class_set.QuotaClassSet) + self.assertEqual(quota_class_set.backups, 123) From e7f14b09d71783521dad565b7bbd37e732063753 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 16 Sep 2024 11:27:47 +0100 Subject: [PATCH 3565/3836] tests: Add helpers to create, delete temp projects This should help us avoid quota issues going forward. Change-Id: Ief5578335cb1a40bc2ac7e8329ed2c11ba5c9ed0 Signed-off-by: Stephen Finucane --- openstack/tests/functional/base.py | 39 ++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index 83c4cb518..464561000 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -256,6 +256,45 @@ def getUniqueString(self, prefix=None): time=int(time.time()), uuid=uuid.uuid4().hex ) + def create_temporary_project(self): + """Create a new temporary project. + + This is useful for tests that modify things like quotas, which would + cause issues for other tests. + """ + project_name = self.getUniqueString('project-') + project = self.operator_cloud.get_project(project_name) + if not project: + params = { + 'name': project_name, + 'description': f'Temporary project created for {self.id()}', + # assume identity API v3 for now + 'domain_id': self.operator_cloud.get_domain('default')['id'], + } + project = self.operator_cloud.create_project(**params) + + # Grant the current user access to the project + user_id = self.operator_cloud.current_user_id + role_assignment = self.operator_cloud.list_role_assignments( + {'user': user_id, 'project': project['id']} + ) + if not role_assignment: + self.operator_cloud.grant_role( + 'member', user=user_id, project=project['id'], wait=True + ) + + self.addCleanup(self._delete_temporary_project, project) + + return project + + def _delete_temporary_project(self, project): + self.operator_cloud.revoke_role( + 'member', + user=self.operator_cloud.current_user_id, + project=project.id, + ) + self.operator_cloud.delete_project(project.id) + class KeystoneBaseFunctionalTest(BaseFunctionalTest): def setUp(self): From b119c1be6dc2bf9fd7b8c7fd06c808c55c61bf0d Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Tue, 17 Sep 2024 00:55:57 +0900 Subject: [PATCH 3566/3836] Fix internal error when floating ip network is not found by name Ensure that the appropriate exception (NotFoundException) is raised in case the network is not found by the specified name, to avoid ugly TypeError raised from internal logic. Closes-Bug: #2080859 Co-Authored-By: Alexander Stepanov Change-Id: I12ee35c1fc1bd9691b230f0b5e3de9111f0d46a7 --- openstack/cloud/_network_common.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openstack/cloud/_network_common.py b/openstack/cloud/_network_common.py index 43633d7cd..b504eb114 100644 --- a/openstack/cloud/_network_common.py +++ b/openstack/cloud/_network_common.py @@ -806,7 +806,9 @@ def _neutron_create_floating_ip( if not network_id: if network_name_or_id: try: - network = self.network.find_network(network_name_or_id) + network = self.network.find_network( + network_name_or_id, ignore_missing=False + ) except exceptions.NotFoundException: raise exceptions.NotFoundException( "unable to find network for floating ips with ID " From 6469251280e09ad284d8129cb5525d4192f4a617 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 17 Sep 2024 10:38:59 +0100 Subject: [PATCH 3567/3836] tests: Add functional tests for block storage quotas Only v3 is added since we don not have an environment to run v2 in. Change-Id: I46f99d55a6ed750de22045c69fdede954504e47b Signed-off-by: Stephen Finucane --- .../block_storage/v3/test_quota_set.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 openstack/tests/functional/block_storage/v3/test_quota_set.py diff --git a/openstack/tests/functional/block_storage/v3/test_quota_set.py b/openstack/tests/functional/block_storage/v3/test_quota_set.py new file mode 100644 index 000000000..f978ba96b --- /dev/null +++ b/openstack/tests/functional/block_storage/v3/test_quota_set.py @@ -0,0 +1,53 @@ +# 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.block_storage.v3 import quota_set as _quota_set +from openstack.tests.functional import base + + +class TestQuotaSet(base.BaseFunctionalTest): + def setUp(self): + super().setUp() + + if not self.operator_cloud: + self.skipTest("Operator cloud required for this test") + + self.project = self.create_temporary_project() + + def test_quota_set(self): + # update quota + + quota_set = self.operator_cloud.block_storage.update_quota_set( + self.project.id, volumes=123 + ) + self.assertIsInstance(quota_set, _quota_set.QuotaSet) + self.assertEqual(quota_set.volumes, 123) + + # retrieve details of the (updated) quota + + quota_set = self.operator_cloud.block_storage.get_quota_set( + self.project.id + ) + self.assertIsInstance(quota_set, _quota_set.QuotaSet) + self.assertEqual(quota_set.volumes, 123) + + # retrieve quota defaults + + defaults = self.operator_cloud.block_storage.get_quota_set_defaults( + self.project.id + ) + self.assertIsInstance(defaults, _quota_set.QuotaSet) + self.assertNotEqual(defaults.volumes, 123) + + # revert quota + + self.operator_cloud.block_storage.revert_quota_set(self.project.id) From a1badd2864d7691df5fd00a8b6277342b483092d Mon Sep 17 00:00:00 2001 From: "hnn.ynh" Date: Sun, 1 Sep 2024 00:01:40 +0900 Subject: [PATCH 3568/3836] Add getting the status of the services in designate system The feature is specified in the DNS API docs but not supported in the SDK. - Add ServiceStatus declaration - Add methods to support - service_statuses(), get_service_status() Change-Id: Ide1f6e7bdffc949cd814171bfb7ede4e503b8ac3 --- doc/source/user/proxies/dns.rst | 7 +++ doc/source/user/resources/dns/index.rst | 1 + .../user/resources/dns/v2/service_status.rst | 13 +++++ openstack/dns/v2/_proxy.py | 22 ++++++++ openstack/dns/v2/service_status.py | 51 ++++++++++++++++++ .../functional/dns/v2/test_service_status.py | 54 +++++++++++++++++++ openstack/tests/unit/dns/v2/test_proxy.py | 13 +++++ .../tests/unit/dns/v2/test_service_status.py | 27 ++++++++++ ...d-dns-service-status-bf1e1cfd811e59a0.yaml | 4 ++ 9 files changed, 192 insertions(+) create mode 100644 doc/source/user/resources/dns/v2/service_status.rst create mode 100644 openstack/dns/v2/service_status.py create mode 100644 openstack/tests/functional/dns/v2/test_service_status.py create mode 100644 openstack/tests/unit/dns/v2/test_service_status.py create mode 100644 releasenotes/notes/add-dns-service-status-bf1e1cfd811e59a0.yaml diff --git a/doc/source/user/proxies/dns.rst b/doc/source/user/proxies/dns.rst index 8c53c5418..212651df8 100644 --- a/doc/source/user/proxies/dns.rst +++ b/doc/source/user/proxies/dns.rst @@ -68,3 +68,10 @@ Zone Share Operations :noindex: :members: create_zone_share, delete_zone_share, get_zone_share, find_zone_share, zone_shares + +Service Status Operations +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.dns.v2._proxy.Proxy + :noindex: + :members: service_statuses, get_service_status diff --git a/doc/source/user/resources/dns/index.rst b/doc/source/user/resources/dns/index.rst index 77860a6a0..c270c67e1 100644 --- a/doc/source/user/resources/dns/index.rst +++ b/doc/source/user/resources/dns/index.rst @@ -11,3 +11,4 @@ DNS Resources v2/zone_share v2/floating_ip v2/recordset + v2/service_status diff --git a/doc/source/user/resources/dns/v2/service_status.rst b/doc/source/user/resources/dns/v2/service_status.rst new file mode 100644 index 000000000..492f9182a --- /dev/null +++ b/doc/source/user/resources/dns/v2/service_status.rst @@ -0,0 +1,13 @@ +openstack.dns.v2.service_status +=============================== + +.. automodule:: openstack.dns.v2.service_status + +The ServiceStatus Class +----------------------- + +The ``ServiceStatus`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.dns.v2.service_status.ServiceStatus + :members: diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py index 5b1e35d85..a5497da62 100644 --- a/openstack/dns/v2/_proxy.py +++ b/openstack/dns/v2/_proxy.py @@ -12,6 +12,7 @@ from openstack.dns.v2 import floating_ip as _fip from openstack.dns.v2 import recordset as _rs +from openstack.dns.v2 import service_status as _svc_status from openstack.dns.v2 import zone as _zone from openstack.dns.v2 import zone_export as _zone_export from openstack.dns.v2 import zone_import as _zone_import @@ -24,6 +25,7 @@ class Proxy(proxy.Proxy): _resource_registry = { "floating_ip": _fip.FloatingIP, "recordset": _rs.Recordset, + "service_status": _svc_status.ServiceStatus, "zone": _zone.Zone, "zone_export": _zone_export.ZoneExport, "zone_import": _zone_import.ZoneImport, @@ -656,6 +658,26 @@ def delete_zone_share(self, zone, zone_share, ignore_missing=True): zone_id=zone_obj.id, ) + # ======== Service Statuses ======== + def service_statuses(self): + """Retrieve a generator of service statuses + + :returns: A generator of service statuses + :class:`~openstack.dns.v2.service_status.ServiceStatus` instances. + """ + return self._list(_svc_status.ServiceStatus) + + def get_service_status(self, service): + """Get a status of a service in the Designate system + + :param service: The value can be the ID of a service + or a :class:`~openstack.dns.v2.service_status.ServiceStatus` instance. + + :returns: ServiceStatus instance. + :rtype: :class:`~openstack.dns.v2.service_status.ServiceStatus` + """ + return self._get(_svc_status.ServiceStatus, service) + def _get_cleanup_dependencies(self): # DNS may depend on floating ip return {'dns': {'before': ['network']}} diff --git a/openstack/dns/v2/service_status.py b/openstack/dns/v2/service_status.py new file mode 100644 index 000000000..245817e16 --- /dev/null +++ b/openstack/dns/v2/service_status.py @@ -0,0 +1,51 @@ +# 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.dns.v2 import _base +from openstack import resource + + +class ServiceStatus(_base.Resource): + """Designate Service Statuses""" + + resources_key = 'service_statuses' + base_path = '/service_statuses' + + # capabilities + allow_create = False + allow_fetch = True + allow_commit = False + allow_delete = False + allow_list = True + + #: Capabilities for the service + capabilities = resource.Body('capabilities', type=dict) + #: Timestamp when the resource was created + created_at = resource.Body('created_at') + #: Timestamp when the last heartbeat was received + heartbeated_at = resource.Body('heartbeated_at') + #: Hostname of the host with the service instance + #: *Type: str* + hostname = resource.Body('hostname') + #: Links contains a `self` pertaining to this service status or a `next` pertaining + #: to next page + links = resource.Body('links', type=dict) + #: The name of the Designate service instance + #: *Type: str* + service_name = resource.Body('service_name') + #: Statistics for the service + stats = resource.Body('stats', type=dict) + #: The status of the resource + #: *Type: enum* + status = resource.Body('status') + #: Timestamp when the resource was last updated + updated_at = resource.Body('updated_at') diff --git a/openstack/tests/functional/dns/v2/test_service_status.py b/openstack/tests/functional/dns/v2/test_service_status.py new file mode 100644 index 000000000..562e7b69f --- /dev/null +++ b/openstack/tests/functional/dns/v2/test_service_status.py @@ -0,0 +1,54 @@ +# 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 connection +from openstack.tests.functional import base + + +class TestServiceStatus(base.BaseFunctionalTest): + def setUp(self): + super().setUp() + self.require_service('dns') + + self.conn = connection.from_config(cloud_name=base.TEST_CLOUD_NAME) + + self.service_names = [ + "api", + "backend", + "central", + "mdns", + "producer", + "sink", + "storage", + "worker", + ] + self.service_status = ["UP", "DOWN"] + + def test_service_status(self): + service_statuses = list(self.conn.dns.service_statuses()) + if not service_statuses: + self.skipTest( + "The Service in Designate System is required for this test" + ) + + names = [f.service_name for f in service_statuses] + statuses = [f.status for f in service_statuses] + + self.assertTrue( + all(status in self.service_status for status in statuses) + ) + self.assertTrue(all(name in self.service_names for name in names)) + + # Test that we can fetch a service status + service_status = self.conn.dns.get_service_status(service_statuses[0]) + self.assertIn(service_status.service_name, self.service_names) + self.assertIn(service_status.status, self.service_status) diff --git a/openstack/tests/unit/dns/v2/test_proxy.py b/openstack/tests/unit/dns/v2/test_proxy.py index 035701ef8..97b60fe2f 100644 --- a/openstack/tests/unit/dns/v2/test_proxy.py +++ b/openstack/tests/unit/dns/v2/test_proxy.py @@ -13,6 +13,7 @@ from openstack.dns.v2 import _proxy from openstack.dns.v2 import floating_ip from openstack.dns.v2 import recordset +from openstack.dns.v2 import service_status from openstack.dns.v2 import zone from openstack.dns.v2 import zone_export from openstack.dns.v2 import zone_import @@ -311,3 +312,15 @@ def test_zone_shares(self): expected_args=[], expected_kwargs={'zone_id': 'zone'}, ) + + +class TestDnsServiceStatus(TestDnsProxy): + def test_service_statuses(self): + self.verify_list( + self.proxy.service_statuses, service_status.ServiceStatus + ) + + def test_service_status_get(self): + self.verify_get( + self.proxy.get_service_status, service_status.ServiceStatus + ) diff --git a/openstack/tests/unit/dns/v2/test_service_status.py b/openstack/tests/unit/dns/v2/test_service_status.py new file mode 100644 index 000000000..d2bcc7fea --- /dev/null +++ b/openstack/tests/unit/dns/v2/test_service_status.py @@ -0,0 +1,27 @@ +# 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.dns.v2 import service_status as svc_status +from openstack.tests.unit import base + + +class TestServiceStatus(base.TestCase): + def test_basic(self): + sot = svc_status.ServiceStatus() + self.assertEqual(None, sot.resource_key) + self.assertEqual('service_statuses', sot.resources_key) + self.assertEqual('/service_statuses', sot.base_path) + self.assertTrue(sot.allow_list) + self.assertFalse(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_delete) diff --git a/releasenotes/notes/add-dns-service-status-bf1e1cfd811e59a0.yaml b/releasenotes/notes/add-dns-service-status-bf1e1cfd811e59a0.yaml new file mode 100644 index 000000000..b10c55a6d --- /dev/null +++ b/releasenotes/notes/add-dns-service-status-bf1e1cfd811e59a0.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add getting the status of one or all services in Designate (DNS) system. From 91dbb4c9f3def0a83e5f20b4eaf88d989f971bd0 Mon Sep 17 00:00:00 2001 From: ArtofBugs Date: Mon, 16 Sep 2024 15:15:26 -0700 Subject: [PATCH 3569/3836] Identity: Support `options` property for roles Change-Id: I09bff2ce926d253a0e065b225556b2496b57e655 --- openstack/identity/v3/role.py | 3 +++ openstack/tests/unit/identity/v3/test_role.py | 2 ++ .../add-identity-role-options-property-5d99d3fd909f01eb.yaml | 4 ++++ 3 files changed, 9 insertions(+) create mode 100644 releasenotes/notes/add-identity-role-options-property-5d99d3fd909f01eb.yaml diff --git a/openstack/identity/v3/role.py b/openstack/identity/v3/role.py index ddca5cd05..283847edc 100644 --- a/openstack/identity/v3/role.py +++ b/openstack/identity/v3/role.py @@ -35,5 +35,8 @@ class Role(resource.Resource): description = resource.Body('description') #: References the domain ID which owns the role. *Type: string* domain_id = resource.Body('domain_id') + #: The resource options for the role. Available resource options are + #: immutable. + options = resource.Body('options', type=dict) #: The links for the service resource. links = resource.Body('links') diff --git a/openstack/tests/unit/identity/v3/test_role.py b/openstack/tests/unit/identity/v3/test_role.py index c9cc49c92..d8d6b8601 100644 --- a/openstack/tests/unit/identity/v3/test_role.py +++ b/openstack/tests/unit/identity/v3/test_role.py @@ -21,6 +21,7 @@ 'name': '2', 'description': 'test description for role', 'domain_id': 'default', + 'options': {'immutable': True}, } @@ -54,3 +55,4 @@ def test_make_it(self): self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['description'], sot.description) self.assertEqual(EXAMPLE['domain_id'], sot.domain_id) + self.assertEqual(EXAMPLE['options'], sot.options) diff --git a/releasenotes/notes/add-identity-role-options-property-5d99d3fd909f01eb.yaml b/releasenotes/notes/add-identity-role-options-property-5d99d3fd909f01eb.yaml new file mode 100644 index 000000000..8b7e7eaf9 --- /dev/null +++ b/releasenotes/notes/add-identity-role-options-property-5d99d3fd909f01eb.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add support for `options` property for roles in identity. From d4eb9b04b65ebb8cfd746aaebd4d7c99f5604c66 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 20 Sep 2024 11:55:35 +0100 Subject: [PATCH 3570/3836] compute, volume: Tweak 'update_quota_set' compat shim The compatibility shim should modify the QuotaSet as a side-effect. Change-Id: Ia9546afe7762d119430b2a5ab1d7d00296245dde Closes-bug: #2081292 Signed-off-by: Stephen Finucane --- openstack/block_storage/v2/_proxy.py | 16 +++++--- openstack/block_storage/v3/_proxy.py | 16 +++++--- openstack/compute/v2/_proxy.py | 37 +++++++++++++------ .../tests/unit/block_storage/v2/test_proxy.py | 29 +++++++++++++++ .../tests/unit/block_storage/v3/test_proxy.py | 28 ++++++++++++++ openstack/tests/unit/compute/v2/test_proxy.py | 26 +++++++++++++ .../notes/bug-2081292-def552ed9c4e24a3.yaml | 10 +++++ 7 files changed, 138 insertions(+), 24 deletions(-) create mode 100644 releasenotes/notes/bug-2081292-def552ed9c4e24a3.yaml diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 0f322a459..f2d41fd11 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -824,18 +824,22 @@ def update_quota_set(self, project, **attrs): "with the other quota set methods.", os_warnings.RemovedInSDK50Warning, ) - if isinstance(project, _quota_set.QuotaSet): - attrs['project_id'] = project.project_id - # cinder doesn't support any query parameters so we simply pop # these if 'query' in attrs: - attrs.pop('params') + warnings.warn( + "The query argument is no longer supported and should " + "be removed.", + os_warnings.RemovedInSDK50Warning, + ) + attrs.pop('query') + + res = self._get_resource(_quota_set.QuotaSet, project, **attrs) + return res.commit(self) else: project = self._get_resource(_project.Project, project) attrs['project_id'] = project.id - - return self._update(_quota_set.QuotaSet, None, **attrs) + return self._update(_quota_set.QuotaSet, None, **attrs) # ====== VOLUME METADATA ====== def get_volume_metadata(self, volume): diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index a648eb9c3..09ac39e5c 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -1820,18 +1820,22 @@ def update_quota_set(self, project, **attrs): "with the other quota set methods.", os_warnings.RemovedInSDK50Warning, ) - if isinstance(project, _quota_set.QuotaSet): - attrs['project_id'] = project.project_id - # cinder doesn't support any query parameters so we simply pop # these if 'query' in attrs: - attrs.pop('params') + warnings.warn( + "The query argument is no longer supported and should " + "be removed.", + os_warnings.RemovedInSDK50Warning, + ) + attrs.pop('query') + + res = self._get_resource(_quota_set.QuotaSet, project, **attrs) + return res.commit(self) else: project = self._get_resource(_project.Project, project) attrs['project_id'] = project.id - - return self._update(_quota_set.QuotaSet, None, **attrs) + return self._update(_quota_set.QuotaSet, None, **attrs) # ====== SERVICES ====== @ty.overload diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 675b47003..410c36265 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -2450,7 +2450,7 @@ def update_quota_class_set(self, quota_class_set, **attrs): # ========== Quota sets ========== def get_quota_set(self, project, usage=False, **query): - """Show QuotaSet information for the project + """Show QuotaSet information for the project. :param project: ID or instance of :class:`~openstack.identity.project.Project` of the project for @@ -2473,7 +2473,7 @@ def get_quota_set(self, project, usage=False, **query): return res.fetch(self, base_path=base_path, **query) def get_quota_set_defaults(self, project): - """Show QuotaSet defaults for the project + """Show QuotaSet defaults for the project. :param project: ID or instance of :class:`~openstack.identity.project.Project` of the project for @@ -2525,8 +2525,6 @@ def update_quota_set(self, project, *, user=None, **attrs): :returns: The updated QuotaSet :rtype: :class:`~openstack.compute.v2.quota_set.QuotaSet` """ - query = {} - if 'project_id' in attrs or isinstance(project, _quota_set.QuotaSet): warnings.warn( "The signature of 'update_quota_set' has changed and it " @@ -2534,23 +2532,38 @@ def update_quota_set(self, project, *, user=None, **attrs): "with the other quota set methods.", os_warnings.RemovedInSDK50Warning, ) - if isinstance(project, _quota_set.QuotaSet): - attrs['project_id'] = project.project_id + if user is not None: + raise exceptions.SDKException( + 'The user argument can only be provided once the entire ' + 'call has been updated.' + ) if 'query' in attrs: - query = attrs.pop('query') + warnings.warn( + "The query argument is no longer supported and should " + "be removed.", + os_warnings.RemovedInSDK50Warning, + ) + query = attrs.pop('query') or {} + else: + query = {} + + res = self._get_resource(_quota_set.QuotaSet, project, **attrs) + return res.commit(self, **query) else: project = self._get_resource(_project.Project, project) attrs['project_id'] = project.id if user: user = self._get_resource(_user.User, user) - query['user_id'] = user.id + query = {'user_id': user.id} + else: + query = {} - # we don't use Proxy._update since that doesn't allow passing arbitrary - # query string parameters - quota_set = self._get_resource(_quota_set.QuotaSet, None, **attrs) - return quota_set.commit(self, **query) + # we don't use Proxy._update since that doesn't allow passing + # arbitrary query string parameters + quota_set = self._get_resource(_quota_set.QuotaSet, None, **attrs) + return quota_set.commit(self, **query) # ========== Server actions ========== diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index 2831d06a5..c25d5d54a 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -9,7 +9,9 @@ # 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 unittest import mock +import warnings from openstack.block_storage.v2 import _proxy from openstack.block_storage.v2 import backup @@ -24,6 +26,7 @@ from openstack.identity.v3 import project from openstack import proxy as proxy_base from openstack.tests.unit import test_proxy_base +from openstack import warnings as os_warnings class TestVolumeProxy(test_proxy_base.TestProxyBase): @@ -577,3 +580,29 @@ def test_quota_set_update(self, mock_get): expected_kwargs={'project_id': 'prj', 'volumes': 123}, ) mock_get.assert_called_once_with(project.Project, 'prj') + + @mock.patch.object(proxy_base.Proxy, "_get_resource") + def test_quota_set_update__legacy(self, mock_get): + fake_quota_set = quota_set.QuotaSet(project_id='prj') + mock_get.side_effect = [fake_quota_set] + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + + self._verify( + 'openstack.resource.Resource.commit', + self.proxy.update_quota_set, + method_args=[fake_quota_set], + method_kwargs={'ram': 123}, + expected_args=[self.proxy], + expected_kwargs={}, + ) + + self.assertEqual(1, len(w)) + self.assertEqual( + os_warnings.RemovedInSDK50Warning, + w[-1].category, + ) + self.assertIn( + "The signature of 'update_quota_set' has changed ", + str(w[-1]), + ) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 27a9ee0e5..7a5514790 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -11,6 +11,7 @@ # under the License. from unittest import mock +import warnings from openstack.block_storage.v3 import _proxy from openstack.block_storage.v3 import backup @@ -30,6 +31,7 @@ from openstack.identity.v3 import project from openstack import proxy as proxy_base from openstack.tests.unit import test_proxy_base +from openstack import warnings as os_warnings class TestVolumeProxy(test_proxy_base.TestProxyBase): @@ -1050,3 +1052,29 @@ def test_quota_set_update(self, mock_get): expected_kwargs={'project_id': 'prj', 'volumes': 123}, ) mock_get.assert_called_once_with(project.Project, 'prj') + + @mock.patch.object(proxy_base.Proxy, "_get_resource") + def test_quota_set_update__legacy(self, mock_get): + fake_quota_set = quota_set.QuotaSet(project_id='prj') + mock_get.side_effect = [fake_quota_set] + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + + self._verify( + 'openstack.resource.Resource.commit', + self.proxy.update_quota_set, + method_args=[fake_quota_set], + method_kwargs={'ram': 123}, + expected_args=[self.proxy], + expected_kwargs={}, + ) + + self.assertEqual(1, len(w)) + self.assertEqual( + os_warnings.RemovedInSDK50Warning, + w[-1].category, + ) + self.assertIn( + "The signature of 'update_quota_set' has changed ", + str(w[-1]), + ) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 05ac01a2f..c6a423189 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -1738,6 +1738,32 @@ def test_quota_set_update(self, mock_get): ] ) + @mock.patch.object(proxy_base.Proxy, "_get_resource") + def test_quota_set_update__legacy(self, mock_get): + fake_quota_set = quota_set.QuotaSet(project_id='prj') + mock_get.side_effect = [fake_quota_set] + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + + self._verify( + 'openstack.resource.Resource.commit', + self.proxy.update_quota_set, + method_args=[fake_quota_set], + method_kwargs={'ram': 123}, + expected_args=[self.proxy], + expected_kwargs={}, + ) + + self.assertEqual(1, len(w)) + self.assertEqual( + os_warnings.RemovedInSDK50Warning, + w[-1].category, + ) + self.assertIn( + "The signature of 'update_quota_set' has changed ", + str(w[-1]), + ) + class TestServerAction(TestComputeProxy): def test_server_action_get(self): diff --git a/releasenotes/notes/bug-2081292-def552ed9c4e24a3.yaml b/releasenotes/notes/bug-2081292-def552ed9c4e24a3.yaml new file mode 100644 index 000000000..8acad24b0 --- /dev/null +++ b/releasenotes/notes/bug-2081292-def552ed9c4e24a3.yaml @@ -0,0 +1,10 @@ +--- +fixes: + - | + The ``update_quota_set`` methods in the Compute and Block Storage (v2, v3) + proxy APIs were modified in v3.3.0 to accept ``Project`` objects as the + first argument. A compatibility shim was included to handle callers still + passing ``QuotaSet`` objects, but this shim did not modify the provided + ``QuotaSet`` object in place as the previous code did. This has now been + fixed. The shim is still expected to be removed in v5.0.0. + [`bug 2081292 `_] From 6ae5d3bef649e18f88d16f234bbda59a161aae7e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 24 Sep 2024 15:43:42 +0100 Subject: [PATCH 3571/3836] pre-commit: Bump versions Change-Id: Id4a832f5253f84008cf6b813eb84409eb4436157 Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4f4516377..8b85fead9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: files: .*\.(yaml|yml)$ exclude: '^zuul.d/.*$' - repo: https://github.com/PyCQA/doc8 - rev: v1.1.1 + rev: v1.1.2 hooks: - id: doc8 - repo: https://github.com/asottile/pyupgrade @@ -24,7 +24,7 @@ repos: - id: pyupgrade args: ['--py38-plus'] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.2 + rev: v0.6.7 hooks: - id: ruff args: ['--fix'] From bd6ab754e6e6dc2fa57a830e1d0953716e0115c6 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 16 Sep 2024 16:11:31 +0100 Subject: [PATCH 3572/3836] cloud: Consistently set 'ignore_missing' arguments Change-Id: I3f543b82ec6b663e6a0dbabba323963c5b24e378 Signed-off-by: Stephen Finucane --- openstack/cloud/_baremetal.py | 14 +++---- openstack/cloud/_block_storage.py | 11 +++-- openstack/cloud/_compute.py | 33 ++++++++++----- openstack/cloud/_dns.py | 16 +++---- openstack/cloud/_identity.py | 15 ++++--- openstack/cloud/_image.py | 2 +- openstack/cloud/_network.py | 67 +++++++++++++++++++++--------- openstack/cloud/_network_common.py | 4 +- 8 files changed, 102 insertions(+), 60 deletions(-) diff --git a/openstack/cloud/_baremetal.py b/openstack/cloud/_baremetal.py index e9eee152b..9d1841a31 100644 --- a/openstack/cloud/_baremetal.py +++ b/openstack/cloud/_baremetal.py @@ -82,10 +82,7 @@ def get_machine(self, name_or_id): :rtype: :class:`~openstack.baremetal.v1.node.Node`. :returns: The node found or None if no nodes are found. """ - try: - return self.baremetal.find_node(name_or_id, ignore_missing=False) - except exceptions.NotFoundException: - return None + return self.baremetal.find_node(name_or_id, ignore_missing=True) def get_machine_by_mac(self, mac): """Get machine by port MAC address @@ -417,7 +414,7 @@ def attach_port_to_machine(self, name_or_id, port_name_or_id): :return: Nothing. """ machine = self.get_machine(name_or_id) - port = self.network.find_port(port_name_or_id) + port = self.network.find_port(port_name_or_id, ignore_missing=False) self.baremetal.attach_vif_to_node(machine, port['id']) def detach_port_from_machine(self, name_or_id, port_name_or_id): @@ -429,7 +426,7 @@ def detach_port_from_machine(self, name_or_id, port_name_or_id): :return: Nothing. """ machine = self.get_machine(name_or_id) - port = self.network.find_port(port_name_or_id) + port = self.network.find_port(port_name_or_id, ignore_missing=False) self.baremetal.detach_vif_from_node(machine, port['id']) def list_ports_attached_to_machine(self, name_or_id): @@ -441,7 +438,10 @@ def list_ports_attached_to_machine(self, name_or_id): """ machine = self.get_machine(name_or_id) vif_ids = self.baremetal.list_node_vifs(machine) - return [self.network.find_port(vif) for vif in vif_ids] + return [ + self.network.find_port(vif, ignore_missing=False) + for vif in vif_ids + ] def validate_machine(self, name_or_id, for_deploy=True): """Validate parameters of the machine. diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index 71e0b1b83..56e650313 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -136,7 +136,7 @@ def create_volume( if isinstance(image, dict): kwargs['imageRef'] = image['id'] else: # object - image_obj = self.image.find_image(image) + image_obj = self.image.find_image(image, ignore_missing=True) if not image_obj: raise exceptions.SDKException( f"Image {image} was requested as the basis for a new " @@ -221,8 +221,9 @@ def delete_volume( :raises: :class:`~openstack.exceptions.SDKException` on operation error. """ - volume = self.block_storage.find_volume(name_or_id) - + volume = self.block_storage.find_volume( + name_or_id, ignore_missing=True + ) if not volume: self.log.debug( "Volume %(name_or_id)s does not exist", @@ -265,7 +266,9 @@ def get_volume_limits(self, name_or_id=None): """ params = {} if name_or_id: - project = self.identity.find_project(name_or_id) + project = self.identity.find_project( + name_or_id, ignore_missing=True + ) if not project: raise exceptions.SDKException("project does not exist") params['project'] = project diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index e0a54db40..28641ffa2 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -380,7 +380,9 @@ def get_compute_limits(self, name_or_id=None): """ params = {} if name_or_id: - project = self.identity.find_project(name_or_id) + project = self.identity.find_project( + name_or_id, ignore_missing=True + ) if not project: raise exceptions.SDKException( f"Project {name_or_id} was requested but was not found " @@ -832,7 +834,9 @@ def create_server( # accept strings or Image objects group_id = group['id'] else: # object - group_obj = self.compute.find_server_group(group) + group_obj = self.compute.find_server_group( + group, ignore_missing=True + ) if not group_obj: raise exceptions.SDKException( "Server Group {group} was requested but was not found" @@ -866,7 +870,9 @@ def create_server( # accept strings or Network objects network_id = net['id'] else: - network_obj = self.network.find_network(net) + network_obj = self.network.find_network( + net, ignore_missing=True + ) if not network_obj: raise exceptions.SDKException( 'Network {network} is not a valid network in' @@ -895,7 +901,9 @@ def create_server( nic.pop('net-name', None) elif 'net-name' in nic: net_name = nic.pop('net-name') - nic_net = self.network.find_network(net_name) + nic_net = self.network.find_network( + net_name, ignore_missing=True + ) if not nic_net: raise exceptions.SDKException( "Requested network {net} could not be found.".format( @@ -940,7 +948,7 @@ def create_server( if isinstance(image, dict): kwargs['imageRef'] = image['id'] else: - image_obj = self.image.find_image(image) + image_obj = self.image.find_image(image, ignore_missing=True) if not image_obj: raise exc.OpenStackCloudException( f"Image {image} was requested but was not found " @@ -1046,7 +1054,9 @@ def _get_boot_from_volume_kwargs( if isinstance(boot_volume, dict): volume_id = boot_volume['id'] else: - volume = self.block_storage.find_volume(boot_volume) + volume = self.block_storage.find_volume( + boot_volume, ignore_missing=True + ) if not volume: raise exceptions.SDKException( f"Volume {volume} was requested but was not found " @@ -1068,7 +1078,7 @@ def _get_boot_from_volume_kwargs( if isinstance(image, dict): image_obj = image else: - image_obj = self.image.find_image(image) + image_obj = self.image.find_image(image, ignore_missing=True) if not image_obj: raise exceptions.SDKException( f"Image {image} was requested but was not found " @@ -1104,7 +1114,9 @@ def _get_boot_from_volume_kwargs( if isinstance(volume, dict): volume_id = volume['id'] else: - volume_obj = self.block_storage.find_volume(volume) + volume_obj = self.block_storage.find_volume( + volume, ignore_missing=True + ) if not volume_obj: raise exceptions.SDKException( f"Volume {volume} was requested but was not found " @@ -1433,7 +1445,6 @@ def update_server(self, name_or_id, detailed=False, bare=False, **kwargs): error. """ server = self.compute.find_server(name_or_id, ignore_missing=False) - server = self.compute.update_server(server, **kwargs) return self._expand_server(server, bare=bare, detailed=detailed) @@ -1531,7 +1542,7 @@ def delete_flavor(self, name_or_id): error. """ try: - flavor = self.compute.find_flavor(name_or_id) + flavor = self.compute.find_flavor(name_or_id, ignore_missing=True) if not flavor: self.log.debug("Flavor %s not found for deleting", name_or_id) return False @@ -1830,7 +1841,7 @@ def parse_date(date): if isinstance(end, str): end = parse_date(end) - proj = self.identity.find_project(name_or_id) + proj = self.identity.find_project(name_or_id, ignore_missing=True) if not proj: raise exceptions.SDKException( f"Project {name_or_id} was requested but was not found " diff --git a/openstack/cloud/_dns.py b/openstack/cloud/_dns.py index d495b8689..2fe13bed8 100644 --- a/openstack/cloud/_dns.py +++ b/openstack/cloud/_dns.py @@ -39,12 +39,9 @@ def get_zone(self, name_or_id, filters=None): """ if not filters: filters = {} - zone = self.dns.find_zone( + return self.dns.find_zone( name_or_id=name_or_id, ignore_missing=True, **filters ) - if not zone: - return None - return zone def search_zones(self, name_or_id=None, filters=None): zones = self.list_zones(filters) @@ -136,7 +133,7 @@ def delete_zone(self, name_or_id): error. """ - zone = self.dns.find_zone(name_or_id) + zone = self.dns.find_zone(name_or_id, ignore_missing=True) if not zone: self.log.debug("Zone %s not found for deleting", name_or_id) return False @@ -179,12 +176,9 @@ def get_recordset(self, zone, name_or_id): zone_obj = self.get_zone(zone) if not zone_obj: raise exceptions.SDKException("Zone %s not found." % zone) - try: - return self.dns.find_recordset( - zone=zone_obj, name_or_id=name_or_id, ignore_missing=False - ) - except Exception: - return None + return self.dns.find_recordset( + zone=zone_obj, name_or_id=name_or_id, ignore_missing=True + ) def search_recordsets(self, zone, name_or_id=None, filters=None): recordsets = self.list_recordsets(zone=zone) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index eb78bb2ed..0c7b82078 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -165,6 +165,7 @@ def update_project( project = self.identity.find_project( name_or_id=name_or_id, domain_id=domain_id, + ignore_missing=True, ) if not project: raise exceptions.SDKException("Project %s not found." % name_or_id) @@ -211,7 +212,7 @@ def delete_project(self, name_or_id, domain_id=None): """ try: project = self.identity.find_project( - name_or_id=name_or_id, ignore_missing=True, domain_id=domain_id + name_or_id=name_or_id, domain_id=domain_id, ignore_missing=True ) if not project: self.log.debug("Project %s not found for deleting", name_or_id) @@ -848,7 +849,7 @@ def get_domain(self, domain_id=None, name_or_id=None, filters=None): wrong during the OpenStack API call. """ if domain_id is None: - return self.identity.find_domain(name_or_id) + return self.identity.find_domain(name_or_id, ignore_missing=False) else: return self.identity.get_domain(domain_id) @@ -948,7 +949,9 @@ def update_group( :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ - group = self.identity.find_group(name_or_id, **kwargs) + group = self.identity.find_group( + name_or_id, ignore_missing=True, **kwargs + ) if group is None: raise exceptions.SDKException( f"Group {name_or_id} not found for updating" @@ -974,7 +977,7 @@ def delete_group(self, name_or_id): wrong during the OpenStack API call. """ try: - group = self.identity.find_group(name_or_id) + group = self.identity.find_group(name_or_id, ignore_missing=True) if group is None: self.log.debug("Group %s not found for deleting", name_or_id) return False @@ -1215,7 +1218,9 @@ def _get_grant_revoke_params( # group, role, project search_args['domain_id'] = data['domain'].id - data['role'] = self.identity.find_role(name_or_id=role) + data['role'] = self.identity.find_role( + name_or_id=role, ignore_missing=True + ) if not data['role']: raise exceptions.SDKException(f'Role {role} not found.') diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py index 00b232321..48ce92714 100644 --- a/openstack/cloud/_image.py +++ b/openstack/cloud/_image.py @@ -122,7 +122,7 @@ def download_image( ' however only one can be used at once' ) - image = self.image.find_image(name_or_id) + image = self.image.find_image(name_or_id, ignore_missing=True) if not image: raise exceptions.NotFoundException( "No images with name or ID %s were found" % name_or_id, None diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index 1abd9a436..f51c7b897 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -617,7 +617,7 @@ def set_network_quotas(self, name_or_id, **kwargs): :raises: :class:`~openstack.exceptions.SDKException` if the resource to set the quota does not exist. """ - proj = self.identity.find_project(name_or_id) + proj = self.identity.find_project(name_or_id, ignore_missing=True) if not proj: raise exceptions.SDKException( f"Project {name_or_id} was requested by was not found " @@ -636,7 +636,7 @@ def get_network_quotas(self, name_or_id, details=False): :raises: :class:`~openstack.exceptions.SDKException` if it's not a valid project """ - proj = self.identity.find_project(name_or_id) + proj = self.identity.find_project(name_or_id, ignore_missing=True) if not proj: raise exc.OpenStackCloudException( f"Project {name_or_id} was requested by was not found " @@ -660,7 +660,7 @@ def delete_network_quotas(self, name_or_id): :raises: :class:`~openstack.exceptions.SDKException` if it's not a valid project or the network client call failed """ - proj = self.identity.find_project(name_or_id) + proj = self.identity.find_project(name_or_id, ignore_missing=True) if not proj: raise exceptions.SDKException( f"Project {name_or_id} was requested by was not found " @@ -1351,7 +1351,9 @@ def update_qos_policy(self, name_or_id, **kwargs): self.log.debug("No QoS policy data to update") return - curr_policy = self.network.find_qos_policy(name_or_id) + curr_policy = self.network.find_qos_policy( + name_or_id, ignore_missing=True + ) if not curr_policy: raise exceptions.SDKException( "QoS policy %s not found." % name_or_id @@ -1372,7 +1374,7 @@ def delete_qos_policy(self, name_or_id): raise exc.OpenStackCloudUnavailableExtension( 'QoS extension is not available on target cloud' ) - policy = self.network.find_qos_policy(name_or_id) + policy = self.network.find_qos_policy(name_or_id, ignore_missing=True) if not policy: self.log.debug("QoS policy %s not found for deleting", name_or_id) return False @@ -1419,7 +1421,9 @@ def list_qos_bandwidth_limit_rules(self, policy_name_or_id, filters=None): 'QoS extension is not available on target cloud' ) - policy = self.network.find_qos_policy(policy_name_or_id) + policy = self.network.find_qos_policy( + policy_name_or_id, ignore_missing=True + ) if not policy: raise exceptions.NotFoundException( "QoS policy {name_or_id} not Found.".format( @@ -1451,7 +1455,9 @@ def get_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): 'QoS extension is not available on target cloud' ) - policy = self.network.find_qos_policy(policy_name_or_id) + policy = self.network.find_qos_policy( + policy_name_or_id, ignore_missing=True + ) if not policy: raise exceptions.NotFoundException( "QoS policy {name_or_id} not Found.".format( @@ -1487,7 +1493,9 @@ def create_qos_bandwidth_limit_rule( 'QoS extension is not available on target cloud' ) - policy = self.network.find_qos_policy(policy_name_or_id) + policy = self.network.find_qos_policy( + policy_name_or_id, ignore_missing=True + ) if not policy: raise exceptions.NotFoundException( "QoS policy {name_or_id} not Found.".format( @@ -1581,7 +1589,9 @@ def delete_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): 'QoS extension is not available on target cloud' ) - policy = self.network.find_qos_policy(policy_name_or_id) + policy = self.network.find_qos_policy( + policy_name_or_id, ignore_missing=True + ) if not policy: raise exceptions.NotFoundException( "QoS policy {name_or_id} not Found.".format( @@ -1671,7 +1681,9 @@ def get_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): 'QoS extension is not available on target cloud' ) - policy = self.network.find_qos_policy(policy_name_or_id) + policy = self.network.find_qos_policy( + policy_name_or_id, ignore_missing=True + ) if not policy: raise exceptions.NotFoundException( "QoS policy {name_or_id} not Found.".format( @@ -1701,7 +1713,9 @@ def create_qos_dscp_marking_rule( 'QoS extension is not available on target cloud' ) - policy = self.network.find_qos_policy(policy_name_or_id) + policy = self.network.find_qos_policy( + policy_name_or_id, ignore_missing=True + ) if not policy: raise exceptions.NotFoundException( "QoS policy {name_or_id} not Found.".format( @@ -1733,7 +1747,9 @@ def update_qos_dscp_marking_rule( 'QoS extension is not available on target cloud' ) - policy = self.network.find_qos_policy(policy_name_or_id) + policy = self.network.find_qos_policy( + policy_name_or_id, ignore_missing=True + ) if not policy: raise exceptions.NotFoundException( "QoS policy {name_or_id} not Found.".format( @@ -1771,7 +1787,9 @@ def delete_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): 'QoS extension is not available on target cloud' ) - policy = self.network.find_qos_policy(policy_name_or_id) + policy = self.network.find_qos_policy( + policy_name_or_id, ignore_missing=True + ) if not policy: raise exceptions.NotFoundException( "QoS policy {name_or_id} not Found.".format( @@ -1836,7 +1854,9 @@ def list_qos_minimum_bandwidth_rules( 'QoS extension is not available on target cloud' ) - policy = self.network.find_qos_policy(policy_name_or_id) + policy = self.network.find_qos_policy( + policy_name_or_id, ignore_missing=True + ) if not policy: raise exceptions.NotFoundException( "QoS policy {name_or_id} not Found.".format( @@ -1866,7 +1886,9 @@ def get_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): 'QoS extension is not available on target cloud' ) - policy = self.network.find_qos_policy(policy_name_or_id) + policy = self.network.find_qos_policy( + policy_name_or_id, ignore_missing=True + ) if not policy: raise exceptions.NotFoundException( "QoS policy {name_or_id} not Found.".format( @@ -1900,7 +1922,9 @@ def create_qos_minimum_bandwidth_rule( 'QoS extension is not available on target cloud' ) - policy = self.network.find_qos_policy(policy_name_or_id) + policy = self.network.find_qos_policy( + policy_name_or_id, ignore_missing=True + ) if not policy: raise exceptions.NotFoundException( "QoS policy {name_or_id} not Found.".format( @@ -1934,7 +1958,9 @@ def update_qos_minimum_bandwidth_rule( 'QoS extension is not available on target cloud' ) - policy = self.network.find_qos_policy(policy_name_or_id) + policy = self.network.find_qos_policy( + policy_name_or_id, ignore_missing=True + ) if not policy: raise exceptions.NotFoundException( "QoS policy {name_or_id} not Found.".format( @@ -1974,7 +2000,9 @@ def delete_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): 'QoS extension is not available on target cloud' ) - policy = self.network.find_qos_policy(policy_name_or_id) + policy = self.network.find_qos_policy( + policy_name_or_id, ignore_missing=True + ) if not policy: raise exceptions.NotFoundException( "QoS policy {name_or_id} not Found.".format( @@ -2669,8 +2697,7 @@ def delete_port(self, name_or_id): :raises: :class:`~openstack.exceptions.SDKException` on operation error. """ - port = self.network.find_port(name_or_id) - + port = self.network.find_port(name_or_id, ignore_missing=True) if port is None: self.log.debug("Port %s not found for deleting", name_or_id) return False diff --git a/openstack/cloud/_network_common.py b/openstack/cloud/_network_common.py index b504eb114..622886b98 100644 --- a/openstack/cloud/_network_common.py +++ b/openstack/cloud/_network_common.py @@ -1528,7 +1528,9 @@ def _nat_destination_port( if not fixed_address: if len(ports) > 1: if nat_destination: - nat_network = self.network.find_network(nat_destination) + nat_network = self.network.find_network( + nat_destination, ignore_missing=True + ) if not nat_network: raise exceptions.SDKException( 'NAT Destination {nat_destination} was configured' From 52672a57f07907cca3c2d1187957bf79e9e5fff3 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 16 Sep 2024 16:28:30 +0100 Subject: [PATCH 3573/3836] cloud: Stop reimplementing ignore_missing=False Rather than setting this to True and then checking locally for Noneness, just set it to False. We lose a small bit of granularity in error message but remove quite a bit more code. Change-Id: Ic5f8c0d0c3bc4e86cf7bbe5a159f78dd4e384aaa Signed-off-by: Stephen Finucane --- openstack/cloud/_block_storage.py | 11 +-- openstack/cloud/_compute.py | 73 +++---------------- openstack/cloud/_identity.py | 16 +--- openstack/cloud/_image.py | 7 +- openstack/tests/unit/cloud/test_project.py | 10 +-- .../tests/unit/cloud/test_role_assignment.py | 10 +-- 6 files changed, 20 insertions(+), 107 deletions(-) diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index 56e650313..f3cc729be 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -136,12 +136,7 @@ def create_volume( if isinstance(image, dict): kwargs['imageRef'] = image['id'] else: # object - image_obj = self.image.find_image(image, ignore_missing=True) - if not image_obj: - raise exceptions.SDKException( - f"Image {image} was requested as the basis for a new " - f"volume but was not found on the cloud" - ) + image_obj = self.image.find_image(image, ignore_missing=False) kwargs['imageRef'] = image_obj['id'] kwargs = self._get_volume_kwargs(kwargs) kwargs['size'] = size @@ -267,10 +262,8 @@ def get_volume_limits(self, name_or_id=None): params = {} if name_or_id: project = self.identity.find_project( - name_or_id, ignore_missing=True + name_or_id, ignore_missing=False ) - if not project: - raise exceptions.SDKException("project does not exist") params['project'] = project return self.block_storage.get_limits(**params) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 28641ffa2..b1ac1b542 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -381,13 +381,8 @@ def get_compute_limits(self, name_or_id=None): params = {} if name_or_id: project = self.identity.find_project( - name_or_id, ignore_missing=True + name_or_id, ignore_missing=False ) - if not project: - raise exceptions.SDKException( - f"Project {name_or_id} was requested but was not found " - f"on the cloud" - ) params['tenant_id'] = project.id return self.compute.get_limits(**params).absolute @@ -429,13 +424,12 @@ def get_flavor(self, name_or_id, filters=None, get_extra=True): """ if not filters: filters = {} - flavor = self.compute.find_flavor( + return self.compute.find_flavor( name_or_id, get_extra_specs=get_extra, ignore_missing=True, **filters, ) - return flavor def get_flavor_by_id(self, id, get_extra=False): """Get a flavor by ID @@ -835,13 +829,8 @@ def create_server( group_id = group['id'] else: # object group_obj = self.compute.find_server_group( - group, ignore_missing=True + group, ignore_missing=False ) - if not group_obj: - raise exceptions.SDKException( - "Server Group {group} was requested but was not found" - " on the cloud".format(group=group) - ) group_id = group_obj['id'] if 'scheduler_hints' not in kwargs: kwargs['scheduler_hints'] = {} @@ -871,17 +860,8 @@ def create_server( network_id = net['id'] else: network_obj = self.network.find_network( - net, ignore_missing=True + net, ignore_missing=False ) - if not network_obj: - raise exceptions.SDKException( - 'Network {network} is not a valid network in' - ' {cloud}:{region}'.format( - network=network, - cloud=self.name, - region=self._compute_region, - ) - ) network_id = network_obj['id'] nics.append({'net-id': network_id}) @@ -902,14 +882,8 @@ def create_server( elif 'net-name' in nic: net_name = nic.pop('net-name') nic_net = self.network.find_network( - net_name, ignore_missing=True + net_name, ignore_missing=False ) - if not nic_net: - raise exceptions.SDKException( - "Requested network {net} could not be found.".format( - net=net_name - ) - ) net['uuid'] = nic_net['id'] for ip_key in ('v4-fixed-ip', 'v6-fixed-ip', 'fixed_ip'): fixed_ip = nic.pop(ip_key, None) @@ -948,12 +922,7 @@ def create_server( if isinstance(image, dict): kwargs['imageRef'] = image['id'] else: - image_obj = self.image.find_image(image, ignore_missing=True) - if not image_obj: - raise exc.OpenStackCloudException( - f"Image {image} was requested but was not found " - f"on the cloud" - ) + image_obj = self.image.find_image(image, ignore_missing=False) kwargs['imageRef'] = image_obj.id # TODO(stephenfin): Drop support for dicts: we should only accept @@ -1055,13 +1024,8 @@ def _get_boot_from_volume_kwargs( volume_id = boot_volume['id'] else: volume = self.block_storage.find_volume( - boot_volume, ignore_missing=True + boot_volume, ignore_missing=False ) - if not volume: - raise exceptions.SDKException( - f"Volume {volume} was requested but was not found " - f"on the cloud" - ) volume_id = volume['id'] block_mapping = { 'boot_index': '0', @@ -1078,12 +1042,7 @@ def _get_boot_from_volume_kwargs( if isinstance(image, dict): image_obj = image else: - image_obj = self.image.find_image(image, ignore_missing=True) - if not image_obj: - raise exceptions.SDKException( - f"Image {image} was requested but was not found " - f"on the cloud" - ) + image_obj = self.image.find_image(image, ignore_missing=False) block_mapping = { 'boot_index': '0', @@ -1115,13 +1074,8 @@ def _get_boot_from_volume_kwargs( volume_id = volume['id'] else: volume_obj = self.block_storage.find_volume( - volume, ignore_missing=True + volume, ignore_missing=False ) - if not volume_obj: - raise exceptions.SDKException( - f"Volume {volume} was requested but was not found " - f"on the cloud" - ) volume_id = volume_obj['id'] block_mapping = { 'boot_index': '-1', @@ -1841,14 +1795,9 @@ def parse_date(date): if isinstance(end, str): end = parse_date(end) - proj = self.identity.find_project(name_or_id, ignore_missing=True) - if not proj: - raise exceptions.SDKException( - f"Project {name_or_id} was requested but was not found " - f"on the cloud" - ) + project = self.identity.find_project(name_or_id, ignore_missing=False) - return self.compute.get_usage(proj, start, end) + return self.compute.get_usage(project, start, end) def _encode_server_userdata(self, userdata): if hasattr(userdata, 'read'): diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index 0c7b82078..478b77ed5 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -165,10 +165,8 @@ def update_project( project = self.identity.find_project( name_or_id=name_or_id, domain_id=domain_id, - ignore_missing=True, + ignore_missing=False, ) - if not project: - raise exceptions.SDKException("Project %s not found." % name_or_id) if enabled is not None: kwargs.update({'enabled': enabled}) project = self.identity.update_project(project, **kwargs) @@ -950,12 +948,8 @@ def update_group( wrong during the OpenStack API call. """ group = self.identity.find_group( - name_or_id, ignore_missing=True, **kwargs + name_or_id, ignore_missing=False, **kwargs ) - if group is None: - raise exceptions.SDKException( - f"Group {name_or_id} not found for updating" - ) group_ref = {} if name: @@ -1218,11 +1212,7 @@ def _get_grant_revoke_params( # group, role, project search_args['domain_id'] = data['domain'].id - data['role'] = self.identity.find_role( - name_or_id=role, ignore_missing=True - ) - if not data['role']: - raise exceptions.SDKException(f'Role {role} not found.') + data['role'] = self.identity.find_role(role, ignore_missing=False) if user: # use cloud.get_user to save us from bad searching by name diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py index 48ce92714..1a0fe61a9 100644 --- a/openstack/cloud/_image.py +++ b/openstack/cloud/_image.py @@ -122,12 +122,7 @@ def download_image( ' however only one can be used at once' ) - image = self.image.find_image(name_or_id, ignore_missing=True) - if not image: - raise exceptions.NotFoundException( - "No images with name or ID %s were found" % name_or_id, None - ) - + image = self.image.find_image(name_or_id, ignore_missing=False) return self.image.download_image( image, output=output_file or output_path, chunk_size=chunk_size ) diff --git a/openstack/tests/unit/cloud/test_project.py b/openstack/tests/unit/cloud/test_project.py index dfbf4b05b..cbd7d2971 100644 --- a/openstack/tests/unit/cloud/test_project.py +++ b/openstack/tests/unit/cloud/test_project.py @@ -122,15 +122,7 @@ def test_update_project_not_found(self): ), ] ) - # NOTE(notmorgan): This test (and shade) does not represent a case - # where the project is in the project list but a 404 is raised when - # the PATCH is issued. This is a bug in shade and should be fixed, - # shade will raise an attribute error instead of the proper - # project not found exception. - with testtools.ExpectedException( - exceptions.SDKException, - "Project %s not found." % project_data.project_id, - ): + with testtools.ExpectedException(exceptions.NotFoundException): self.cloud.update_project(project_data.project_id) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_role_assignment.py b/openstack/tests/unit/cloud/test_role_assignment.py index 7a0c2452e..ffc78fac9 100644 --- a/openstack/tests/unit/cloud/test_role_assignment.py +++ b/openstack/tests/unit/cloud/test_role_assignment.py @@ -1742,10 +1742,7 @@ def test_grant_no_role(self): ) self.register_uris(uris) - with testtools.ExpectedException( - exceptions.SDKException, - f'Role {self.role_data.role_name} not found', - ): + with testtools.ExpectedException(exceptions.NotFoundException): self.cloud.grant_role( self.role_data.role_name, group=self.group_data.group_name, @@ -1782,10 +1779,7 @@ def test_revoke_no_role(self): ) self.register_uris(uris) - with testtools.ExpectedException( - exceptions.SDKException, - f'Role {self.role_data.role_name} not found', - ): + with testtools.ExpectedException(exceptions.NotFoundException): self.cloud.revoke_role( self.role_data.role_name, group=self.group_data.group_name, From a888c8c953d6ad35b6441ed4844a425151eae733 Mon Sep 17 00:00:00 2001 From: kyeongnapark Date: Thu, 5 Sep 2024 21:39:28 +0900 Subject: [PATCH 3574/3836] Add create, delete, find, list and UPDATE example connect code - create zone, recordset - list zone - delete zone, recordset - example connect Change-Id: I1859170b06311330c954beec7919ade0e76b6c48 --- doc/source/user/guides/dns.rst | 93 +++++++++++++++++++++++++++++++++- examples/dns/create.py | 69 +++++++++++++++++++++++++ examples/dns/delete.py | 47 +++++++++++++++++ examples/dns/find.py | 62 +++++++++++++++++++++++ examples/dns/list.py | 15 ++++++ 5 files changed, 285 insertions(+), 1 deletion(-) create mode 100644 examples/dns/create.py create mode 100644 examples/dns/delete.py create mode 100644 examples/dns/find.py diff --git a/doc/source/user/guides/dns.rst b/doc/source/user/guides/dns.rst index c2d268083..88caba15e 100644 --- a/doc/source/user/guides/dns.rst +++ b/doc/source/user/guides/dns.rst @@ -5,14 +5,105 @@ Before working with the DNS service, you'll need to create a connection to your OpenStack cloud by following the :doc:`connect` user guide. This will provide you with the ``conn`` variable used in the examples below. -.. TODO(gtema): Implement this guide +.. contents:: Table of Contents + :local: + +The primary resource of the DNS service is the server. List Zones ---------- +**Zone** is a logical grouping of DNS records for a domain, allowing for the +centralized management of DNS resources, including domain names, +nameservers, and DNS queries. + .. literalinclude:: ../examples/dns/list.py :pyobject: list_zones Full example: `dns resource list`_ +List Recordsets +--------------- + +**Recordsets** allow for the centralized management of various DNS records +within a Zone, helping to define how a domain responds to different types +of DNS queries. + +.. literalinclude:: ../examples/dns/list.py + :pyobject: list_recordsets + +Full example: `dns resource list`_ + +Create Zone +----------- + +Create a zone. +It allows users to define and manage the DNS namespace for a particular domain. + +.. literalinclude:: ../examples/dns/create.py + :pyobject: create_zone + +Full example: `dns resource list`_ + +Create Recordset +---------------- + +Create a recordset. It accepts several parameters that define the DNS +record's properties and sends an API request to OpenStack to create the +recordset within a specified DNS zone. + +.. literalinclude:: ../examples/dns/create.py + :pyobject: create_recordset + +Full example: `dns resource list`_ + +Delete Zone +----------- + +Delete a zone. +It allows users to completely delete the DNS management for a specified domain. + +.. literalinclude:: ../examples/dns/delete.py + :pyobject: delete_zone + +Full example: `dns resource list`_ + +Delete Recordset +---------------- + +Delete a recordset. + +.. literalinclude:: ../examples/dns/delete.py + :pyobject: delete_recordset + +Full example: `dns resource list`_ + +Find Zone +--------- + +The find_zone function searches for and returns a DNS zone by its name +using a given connection object. + +.. literalinclude:: ../examples/dns/find.py + :pyobject: find_zone + +Full example: `dns resource list`_ + +Find Recordset +-------------- + +The find_recordset function searches for a DNS recordset +with a specific name and type +within a given zone. If multiple recordsets +with the same name exist, +the record type can be specified to find the exact match. + +.. literalinclude:: ../examples/dns/find.py + :pyobject: find_recordset + +Full example: `dns resource list`_ + .. _dns resource list: https://opendev.org/openstack/openstacksdk/src/branch/master/examples/dns/list.py +.. _dns resource create: https://opendev.org/openstack/openstacksdk/src/branch/master/examples/dns/create.py +.. _dns resource delete: https://opendev.org/openstack/openstacksdk/src/branch/master/examples/dns/delete.py +.. _dns resource find: https://opendev.org/openstack/openstacksdk/src/branch/master/examples/dns/find.py diff --git a/examples/dns/create.py b/examples/dns/create.py new file mode 100644 index 000000000..94cd81f08 --- /dev/null +++ b/examples/dns/create.py @@ -0,0 +1,69 @@ +# 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. + +""" +Create resources from the DNS service. + +For a full guide see +https://docs.openstack.org/openstacksdk/latest/user/guides/dns.html +""" + + +def create_zone( + conn, + name, + email, + ttl=3600, + description="Default description", + zone_type="PRIMARY", +): + print("Create Zone: ") + + zone = { + "name": name, + "email": email, + "ttl": ttl, + "description": description, + "type": zone_type, + } + + print(conn.dns.create_zone(**zone)) + + +def create_recordset( + conn, + name_or_id, + recordset_name, + recordset_type="A", + records=["192.168.1.1"], + ttl=3600, + description="Default description", +): + print("Create Recordset: ") + + zone = conn.dns.find_zone(name_or_id) + + if not zone: + print("Zone not found.") + return None + + zone_id = zone.id + + recordset_data = { + "name": recordset_name, + "type": recordset_type, + "records": records, + "ttl": ttl, + "description": description, + } + + print(conn.dns.create_recordset(zone_id, **recordset_data)) diff --git a/examples/dns/delete.py b/examples/dns/delete.py new file mode 100644 index 000000000..9ad8f80f1 --- /dev/null +++ b/examples/dns/delete.py @@ -0,0 +1,47 @@ +# 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. + +""" +Delete resources from the DNS service. + +For a full guide see +https://docs.openstack.org/openstacksdk/latest/user/guides/dns.html +""" + + +def delete_zone(conn, name_or_id): + print(f"Delete Zone: {name_or_id}") + + zone = conn.dns.find_zone(name_or_id) + + if zone: + conn.dns.delete_zone(zone.id) + else: + return None + + +def delete_recordset(conn, name_or_id, recordset_name): + print(f"Deleting Recordset: {recordset_name} in Zone: {name_or_id}") + + zone = conn.dns.find_zone(name_or_id) + + if zone: + try: + recordset = conn.dns.find_recordset(zone.id, recordset_name) + if recordset: + conn.dns.delete_recordset(recordset, zone.id) + else: + print("Recordset not found") + except Exception as e: + print(f"{e}") + else: + return None diff --git a/examples/dns/find.py b/examples/dns/find.py new file mode 100644 index 000000000..b5f0c26d8 --- /dev/null +++ b/examples/dns/find.py @@ -0,0 +1,62 @@ +# 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. + +""" +Find resources from the DNS service. + +For a full guide see +https://docs.openstack.org/openstacksdk/latest/user/guides/dns.html +""" + + +def find_zone(conn, name_or_id): + print(f"Find Zone: {name_or_id}") + + zone = conn.dns.find_zone(name_or_id) + + if zone: + print(zone) + return zone + else: + print("Zone not found.") + return None + + +def find_recordset(conn, name_or_id, recordset_name, recordset_type=None): + print(f"Find Recordset: {recordset_name} in Zone: {name_or_id}") + + zone = conn.dns.find_zone(name_or_id) + + if not zone: + print("Zone not found.") + return None + + zone_id = zone.id + + try: + if recordset_type: + recordset = conn.dns.find_recordset( + zone_id, recordset_name, type=recordset_type + ) + else: + recordset = conn.dns.find_recordset(zone_id, recordset_name) + + if recordset: + print(recordset) + return recordset + else: + print("Recordset not found in Zone.") + return None + + except Exception as e: + print(f"{e}") + return None diff --git a/examples/dns/list.py b/examples/dns/list.py index 50a5e8170..782334c4b 100644 --- a/examples/dns/list.py +++ b/examples/dns/list.py @@ -23,3 +23,18 @@ def list_zones(conn): for zone in conn.dns.zones(): print(zone) + + +def list_recordsets(conn, name_or_id): + print("List Recordsets for Zone") + + zone = conn.dns.find_zone(name_or_id) + + if zone: + zone_id = zone.id + recordsets = conn.dns.recordsets(zone_id) + + for recordset in recordsets: + print(recordset) + else: + print("Zone not found.") From 7994f88a3594e35d5fc3af252556d71d6a75d08e Mon Sep 17 00:00:00 2001 From: Slawek Kaplonski Date: Mon, 2 Sep 2024 11:28:05 +0200 Subject: [PATCH 3575/3836] Add "trusted" attribute to "port" resource Related-bug: #2060916 Change-Id: Id065db33d7a71f0b4aac8ee95d4d3947736a7bda --- openstack/network/v2/port.py | 3 +++ openstack/tests/unit/network/v2/test_port.py | 2 ++ .../notes/Add-trusted-vif-to-the-port-e306789f92e181b2.yaml | 6 ++++++ 3 files changed, 11 insertions(+) create mode 100644 releasenotes/notes/Add-trusted-vif-to-the-port-e306789f92e181b2.yaml diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index d85427576..03d6b0970 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -161,5 +161,8 @@ class Port(_base.NetworkResource, tag.TagMixin): #: sub_ports is a list of dicts with keys: #: port_id, segmentation_type, segmentation_id, mac_address* trunk_details = resource.Body('trunk_details', type=dict) + #: Status of the trusted VIF setting, this value is added to the + #: binding:profile field and passed to services which needs, it, like Nova + trusted = resource.Body('trusted', type=bool) #: Timestamp when the port was last updated. updated_at = resource.Body('updated_at') diff --git a/openstack/tests/unit/network/v2/test_port.py b/openstack/tests/unit/network/v2/test_port.py index 4fe890ee4..9fb34dcd1 100644 --- a/openstack/tests/unit/network/v2/test_port.py +++ b/openstack/tests/unit/network/v2/test_port.py @@ -67,6 +67,7 @@ } ], }, + 'trusted': True, 'updated_at': '2016-07-09T12:14:57.233772', } @@ -165,4 +166,5 @@ def test_make_it(self): self.assertEqual(EXAMPLE['status'], sot.status) self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['trunk_details'], sot.trunk_details) + self.assertEqual(EXAMPLE['trusted'], sot.trusted) self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) diff --git a/releasenotes/notes/Add-trusted-vif-to-the-port-e306789f92e181b2.yaml b/releasenotes/notes/Add-trusted-vif-to-the-port-e306789f92e181b2.yaml new file mode 100644 index 000000000..e4cfe953f --- /dev/null +++ b/releasenotes/notes/Add-trusted-vif-to-the-port-e306789f92e181b2.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add ``trusted`` attribute to ``port`` resourse. Users can use this + attribute to set port to be trusted what will be then populated into + the ``binding:profile`` dictionary. From 7069879b584d092dcf93fe4861061b7fcbd4f9d8 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 16 Jul 2024 12:57:24 +0100 Subject: [PATCH 3576/3836] Deprecate unnecessary options, aliases Change-Id: Ia2203bea15c8593611f01eca6ab511c0e11ae8b6 Signed-off-by: Stephen Finucane --- openstack/baremetal/v1/node.py | 14 ++++++++---- openstack/cloud/_block_storage.py | 36 ++++++++++++++++++++----------- openstack/cloud/_identity.py | 9 +++++++- openstack/cloud/_object_store.py | 12 +++++++++-- openstack/compute/v2/_proxy.py | 2 -- openstack/config/loader.py | 25 +++++++++++++++++---- openstack/proxy.py | 9 +++++++- openstack/tests/fixtures.py | 2 +- openstack/warnings.py | 11 ++++++++-- 9 files changed, 91 insertions(+), 29 deletions(-) diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 8605e9e91..6688a1030 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -13,11 +13,13 @@ import collections import enum import typing as ty +import warnings from openstack.baremetal.v1 import _common from openstack import exceptions from openstack import resource from openstack import utils +from openstack import warnings as os_warnings class ValidationResult: @@ -1399,15 +1401,19 @@ def set_console_mode(self, session, enabled): ) exceptions.raise_from_response(response, error_message=msg) - # TODO(stephenfin): Drop 'node_id' and use 'self.id' instead or convert to - # a classmethod def get_node_inventory(self, session, node_id): """Get a node's inventory. :param session: The session to use for making this request. - :param node_id: The ID of the node. + :param node_id: **DEPRECATED** The ID of the node. :returns: The HTTP response. """ + if node_id is not None: + warnings.warn( + "The 'node_id' field is unnecessary and will be removed in " + "a future release.", + os_warnings.RemovedInSDK60Warning, + ) session = self._get_session(session) version = self._get_microversion(session, action='fetch') request = self._prepare_request(requires_id=True) @@ -1421,7 +1427,7 @@ def get_node_inventory(self, session, node_id): ) msg = "Failed to get inventory for node {node}".format( - node=node_id, + node=self.id, ) exceptions.raise_from_response(response, error_message=msg) return response.json() diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index 7cebb80e2..b2a4b0027 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -20,21 +20,20 @@ class BlockStorageCloudMixin(openstackcloud._OpenStackCloudMixin): - # TODO(stephenfin): Remove 'cache' in a future major version - def list_volumes(self, cache=True): + def list_volumes(self, cache=None): """List all available volumes. :param cache: **DEPRECATED** This parameter no longer does anything. :returns: A list of volume ``Volume`` objects. """ - warnings.warn( - "the 'cache' argument is deprecated and no longer does anything; " - "consider removing it from calls", - os_warnings.RemovedInSDK50Warning, - ) + if cache is not None: + warnings.warn( + "the 'cache' argument is deprecated and no longer does " + "anything; consider removing it from calls", + os_warnings.RemovedInSDK50Warning, + ) return list(self.block_storage.volumes()) - # TODO(stephenfin): Remove 'get_extra' in a future major version def list_volume_types(self, get_extra=None): """List all available volume types. @@ -248,14 +247,22 @@ def delete_volume( return True - # TODO(stephenfin): Remove 'cache' in a future major version - def get_volumes(self, server, cache=True): + def get_volumes(self, server, cache=None): """Get volumes for a server. :param server: The server to fetch volumes for. :param cache: **DEPRECATED** This parameter no longer does anything. :returns: A list of volume ``Volume`` objects. """ + if cache is not None: + warnings.warn( + "the 'cache' argument is deprecated and no longer does " + "anything; consider removing it from calls", + os_warnings.RemovedInSDK50Warning, + ) + # avoid spamming warnings + cache = None + volumes = [] for volume in self.list_volumes(cache=cache): for attach in volume['attachments']: @@ -722,7 +729,6 @@ def search_volume_backups(self, name_or_id=None, filters=None): volume_backups = self.list_volume_backups() return _utils._filter_list(volume_backups, name_or_id, filters) - # TODO(stephenfin): Remove 'get_extra' in a future major version def search_volume_types( self, name_or_id=None, @@ -747,7 +753,13 @@ def search_volume_types( :returns: A list of volume ``Type`` objects, if any are found. """ - volume_types = self.list_volume_types(get_extra=get_extra) + if get_extra is not None: + warnings.warn( + "the 'get_extra' argument is deprecated and no longer does " + "anything; consider removing it from calls", + os_warnings.RemovedInSDK50Warning, + ) + volume_types = self.list_volume_types() return _utils._filter_list(volume_types, name_or_id, filters) def get_volume_type_access(self, name_or_id): diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index 986bbe794..2e8e5795b 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -300,12 +300,19 @@ def get_user(self, name_or_id, filters=None, **kwargs): return _utils._get_entity(self, 'user', name_or_id, filters, **kwargs) # TODO(stephenfin): Remove normalize since it doesn't do anything - def get_user_by_id(self, user_id, normalize=True): + def get_user_by_id(self, user_id, normalize=None): """Get a user by ID. :param string user_id: user ID :returns: an identity ``User`` object """ + if normalize is not None: + warnings.warn( + "The 'normalize' field is unnecessary and will be removed in " + "a future release.", + os_warnings.RemovedInSDK60Warning, + ) + return self.identity.get_user(user_id) @_utils.valid_kwargs( diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index 3d95d799e..594a06856 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -12,12 +12,15 @@ import concurrent.futures import urllib.parse +import warnings import keystoneauth1.exceptions from openstack.cloud import _utils from openstack.cloud import openstackcloud from openstack import exceptions +from openstack import warnings as os_warnings + OBJECT_CONTAINER_ACLS = { 'public': '.r:*,.rlistings', @@ -26,8 +29,7 @@ class ObjectStoreCloudMixin(openstackcloud._OpenStackCloudMixin): - # TODO(stephenfin): Remove 'full_listing' as it's a noop - def list_containers(self, full_listing=True, prefix=None): + def list_containers(self, full_listing=None, prefix=None): """List containers. :param full_listing: Ignored. Present for backwards compat @@ -37,6 +39,12 @@ def list_containers(self, full_listing=True, prefix=None): :raises: :class:`~openstack.exceptions.SDKException` on operation error. """ + if full_listing is not None: + warnings.warn( + "The 'full_listing' field is unnecessary and will be removed " + "in a future release.", + os_warnings.RemovedInSDK60Warning, + ) return list(self.object_store.containers(prefix=prefix)) def search_containers(self, name=None, filters=None): diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 9ca015f10..d32becf40 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -1366,8 +1366,6 @@ def create_server_interface(self, server, **attrs): **attrs, ) - # TODO(stephenfin): Does this work? There's no 'value' parameter for the - # call to '_delete' def delete_server_interface( self, server_interface, diff --git a/openstack/config/loader.py b/openstack/config/loader.py index bee1a5f16..01f35f3df 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -984,8 +984,13 @@ def get_all(self): ) return clouds - # TODO(mordred) Backwards compat for OSC transition - get_all_clouds = get_all + def get_all_clouds(self): + warnings.warn( + "The 'get_all_clouds' method is a deprecated alias for " + "'get_clouds' and will be removed in a future release.", + os_warnings.RemovedInSDK60Warning, + ) + return self.get_all() def _fix_args(self, args=None, argparse=None): """Massage the passed-in options @@ -1343,8 +1348,20 @@ def get_one(self, cloud=None, validate=True, argparse=None, **kwargs): influxdb_config=influxdb_config, ) - # TODO(mordred) Backwards compat for OSC transition - get_one_cloud = get_one + def get_one_cloud( + self, cloud=None, validate=True, argparse=None, **kwargs + ): + warnings.warn( + "The 'get_one_cloud' method is a deprecated alias for 'get_one' " + "and will be removed in a future release.", + os_warnings.RemovedInSDK60Warning, + ) + return self.get_one( + cloud=cloud, + validate=validate, + argparse=argparse, + **kwargs, + ) def get_one_cloud_osc( self, cloud=None, validate=True, argparse=None, **kwargs diff --git a/openstack/proxy.py b/openstack/proxy.py index 41bb93fa9..36f72db47 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -14,6 +14,7 @@ import typing as ty import urllib from urllib.parse import urlparse +import warnings try: import simplejson @@ -28,6 +29,7 @@ from openstack import _log from openstack import exceptions from openstack import resource +from openstack import warnings as os_warnings ResourceType = ty.TypeVar('ResourceType', bound=resource.Resource) @@ -208,7 +210,6 @@ def request( self._report_stats(None, url, method, e) raise - # TODO(stephenfin): service_type is unused and should be dropped @functools.lru_cache(maxsize=256) def _extract_name(self, url, service_type=None, project_id=None): """Produce a key name to use in logging/metrics from the URL path. @@ -225,6 +226,12 @@ def _extract_name(self, url, service_type=None, project_id=None): /servers/{id}/os-security-groups -> ['server', 'os-security-groups'] /v2.0/networks.json -> ['networks'] """ + if service_type is not None: + warnings.warn( + "The 'service_type' parameter is unnecesary and will be " + "removed in a future release.", + os_warnings.RemovedInSDK60Warning, + ) url_path = urllib.parse.urlparse(url).path.strip() # Remove / from the beginning to keep the list indexes of interesting diff --git a/openstack/tests/fixtures.py b/openstack/tests/fixtures.py index 3ee41849c..b5d76bace 100644 --- a/openstack/tests/fixtures.py +++ b/openstack/tests/fixtures.py @@ -47,7 +47,7 @@ def setUp(self): ) warnings.filterwarnings( "ignore", - category=os_warnings.RemovedInSDK50Warning, + category=os_warnings._RemovedInSDKWarning, ) # also ignore our own general warnings diff --git a/openstack/warnings.py b/openstack/warnings.py index e12d85d13..885bd4e52 100644 --- a/openstack/warnings.py +++ b/openstack/warnings.py @@ -42,11 +42,18 @@ class LegacyAPIWarning(OpenStackDeprecationWarning): # function parameters. -class RemovedInSDK50Warning(PendingDeprecationWarning): +class _RemovedInSDKWarning(PendingDeprecationWarning): + """Indicates an argument that is deprecated for removal. + + This is a base class and should not be used directly. + """ + + +class RemovedInSDK50Warning(_RemovedInSDKWarning): """Indicates an argument that is deprecated for removal in SDK 5.0.""" -class RemovedInSDK60Warning(PendingDeprecationWarning): +class RemovedInSDK60Warning(_RemovedInSDKWarning): """Indicates an argument that is deprecated for removal in SDK 6.0.""" From b753d4b849af30b8b7076a7e5196601507218a7d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 3 Oct 2024 13:39:45 +0100 Subject: [PATCH 3577/3836] cloud: Don't raise error on missing domain We inverted logic in I3f543b82ec6b663e6a0dbabba323963c5b24e378. Revert that change. Note that we don't add any new tests. We already had a test for this. It's simply not running right now due to the issue we're addressing separately, in change I3fc33a26b0d7f057d7bd6a094fc2ceeaa88f177b. Change-Id: I0d828e0871bd8b4c845694ee94fde0a167f73709 Signed-off-by: Stephen Finucane --- openstack/cloud/_identity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index 1ce3b1fca..4dbbabb7f 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -854,7 +854,7 @@ def get_domain(self, domain_id=None, name_or_id=None, filters=None): wrong during the OpenStack API call. """ if domain_id is None: - return self.identity.find_domain(name_or_id, ignore_missing=False) + return self.identity.find_domain(name_or_id, ignore_missing=True) else: return self.identity.get_domain(domain_id) From c2470a21de72969812642ed4acf1c7c49b45dd6e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 3 Oct 2024 11:33:45 +0100 Subject: [PATCH 3578/3836] tox: Fix functional tests Without this comma, tox runs the standard testenv instead of this one. This is due to how tox calculates factors (parts of a testenv name) and checks for their existence [1]. [1] https://github.com/tox-dev/tox/issues/3219 Change-Id: I3fc33a26b0d7f057d7bd6a094fc2ceeaa88f177b Signed-off-by: Stephen Finucane --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 86a1e794f..ca14f55af 100644 --- a/tox.ini +++ b/tox.ini @@ -23,7 +23,7 @@ commands = stestr run {posargs} stestr slowest -[testenv:functional{-py39,-py310,-py311,-py312}] +[testenv:functional{,-py39,-py310,-py311,-py312}] description = Run functional tests. # Some jobs (especially heat) takes longer, therefore increase default timeout From 1cc397991d5f089b348f963ab12f9b0200c289f4 Mon Sep 17 00:00:00 2001 From: Rajat Dhasmana Date: Mon, 27 May 2024 12:31:53 +0530 Subject: [PATCH 3579/3836] Add support for default volume types This patch adds support for default volume type operations: 1. Set 2. Unset 3. List 4. Get Change-Id: Iac22d3c989b2b19fe4f167011aec1304becc4f34 --- doc/source/user/proxies/block_storage_v3.rst | 8 ++ openstack/block_storage/v3/_proxy.py | 78 +++++++++++++++++++ openstack/block_storage/v3/default_type.py | 56 +++++++++++++ .../block_storage/v3/test_default_type.py | 66 ++++++++++++++++ .../block_storage/v3/test_default_type.py | 55 +++++++++++++ ...default-type-support-aaa1e54b8bd16d86.yaml | 9 +++ 6 files changed, 272 insertions(+) create mode 100644 openstack/block_storage/v3/default_type.py create mode 100644 openstack/tests/functional/block_storage/v3/test_default_type.py create mode 100644 openstack/tests/unit/block_storage/v3/test_default_type.py create mode 100644 releasenotes/notes/add-default-type-support-aaa1e54b8bd16d86.yaml diff --git a/doc/source/user/proxies/block_storage_v3.rst b/doc/source/user/proxies/block_storage_v3.rst index 61657eb34..0498baffb 100644 --- a/doc/source/user/proxies/block_storage_v3.rst +++ b/doc/source/user/proxies/block_storage_v3.rst @@ -173,3 +173,11 @@ Helpers .. autoclass:: openstack.block_storage.v3._proxy.Proxy :noindex: :members: wait_for_status, wait_for_delete + +Default Volume Types +^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v3._proxy.Proxy + :noindex: + :members: default_types, show_default_type, set_default_type, + unset_default_type diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index a648eb9c3..faad7036d 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -19,6 +19,7 @@ from openstack.block_storage.v3 import backup as _backup from openstack.block_storage.v3 import block_storage_summary as _summary from openstack.block_storage.v3 import capabilities as _capabilities +from openstack.block_storage.v3 import default_type as _default_type from openstack.block_storage.v3 import extension as _extension from openstack.block_storage.v3 import group as _group from openstack.block_storage.v3 import group_snapshot as _group_snapshot @@ -535,6 +536,83 @@ def update_type_encryption( return self._update(_type.TypeEncryption, encryption, **attrs) + # ====== DEFAULT TYPES ====== + + def default_types(self): + """Lists default types. + + :returns: List of default types associated to projects. + """ + # This is required since previously default types did not accept + # URL with project ID + if not utils.supports_microversion(self, '3.67'): + raise exceptions.SDKException( + 'List default types require at least microversion 3.67' + ) + + return self._list(_default_type.DefaultType) + + def show_default_type(self, project): + """Show default type for a project. + + :param project: The value can be either the ID of a project or a + :class:`~openstack.identity.v3.project.Project` instance. + + :returns: Default type associated to the project. + """ + # This is required since previously default types did not accept + # URL with project ID + if not utils.supports_microversion(self, '3.67'): + raise exceptions.SDKException( + 'Show default type require at least microversion 3.67' + ) + + project_id = resource.Resource._get_id(project) + return self._get(_default_type.DefaultType, project_id) + + def set_default_type(self, project, type): + """Set default type for a project. + + :param project: The value can be either the ID of a project or a + :class:`~openstack.identity.v3.project.Project` instance. + :param type: The value can be either the ID of a type or a + :class:`~openstack.block_storage.v3.type.Type` instance. + + :returns: Dictionary of project ID and it's associated default type. + """ + # This is required since previously default types did not accept + # URL with project ID + if not utils.supports_microversion(self, '3.67'): + raise exceptions.SDKException( + 'Set default type require at least microversion 3.67' + ) + + type_id = resource.Resource._get_id(type) + project_id = resource.Resource._get_id(project) + return self._create( + _default_type.DefaultType, + id=project_id, + volume_type_id=type_id, + ) + + def unset_default_type(self, project): + """Unset default type for a project. + + :param project: The value can be either the ID of a project or a + :class:`~openstack.identity.v3.project.Project` instance. + + :returns: ``None`` + """ + # This is required since previously default types did not accept + # URL with project ID + if not utils.supports_microversion(self, '3.67'): + raise exceptions.SDKException( + 'Unset default type require at least microversion 3.67' + ) + + project_id = resource.Resource._get_id(project) + self._delete(_default_type.DefaultType, project_id) + # ====== VOLUMES ====== def get_volume(self, volume): """Get a single volume diff --git a/openstack/block_storage/v3/default_type.py b/openstack/block_storage/v3/default_type.py new file mode 100644 index 000000000..c2dc5a36f --- /dev/null +++ b/openstack/block_storage/v3/default_type.py @@ -0,0 +1,56 @@ +# 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 resource + + +class DefaultType(resource.Resource): + resource_key = "default_type" + resources_key = "default_types" + base_path = "/default-types" + + # capabilities + allow_fetch = True + allow_create = True + allow_delete = True + allow_list = True + + # Create and update use the same PUT API + create_requires_id = True + create_method = 'PUT' + + _max_microversion = "3.67" + + # Properties + #: The UUID of the project. + project_id = resource.Body("project_id") + #: The UUID for an existing volume type. + volume_type_id = resource.Body("volume_type_id") + + def _prepare_request_body( + self, + patch, + prepend_key, + *, + resource_request_key=None, + ): + body = self._body.dirty + # Set operation expects volume_type instead of + # volume_type_id + if body.get('volume_type_id'): + body['volume_type'] = body.pop('volume_type_id') + # When setting a default type, we want the ID to be + # appended in URL but not in the request body + if body.get('id'): + body.pop('id') + body = {self.resource_key: body} + return body diff --git a/openstack/tests/functional/block_storage/v3/test_default_type.py b/openstack/tests/functional/block_storage/v3/test_default_type.py new file mode 100644 index 000000000..82458dd84 --- /dev/null +++ b/openstack/tests/functional/block_storage/v3/test_default_type.py @@ -0,0 +1,66 @@ +# 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.block_storage.v3 import default_type as _default_type +from openstack.tests.functional.block_storage.v3 import base + + +class TestDefaultType(base.BaseBlockStorageTest): + def setUp(self): + super().setUp() + if not self._op_name: + self.skip("Operator cloud must be set for this test") + self._set_operator_cloud(block_storage_api_version='3.67') + self.PROJECT_ID = self.create_temporary_project().id + + def test_default_type(self): + # Create a volume type + type_name = self.getUniqueString() + volume_type_id = self.operator_cloud.block_storage.create_type( + name=type_name, + ).id + + # Set default type for a project + default_type = self.conn.block_storage.set_default_type( + self.PROJECT_ID, + volume_type_id, + ) + self.assertIsInstance(default_type, _default_type.DefaultType) + + # Show default type for a project + default_type = self.conn.block_storage.show_default_type( + self.PROJECT_ID + ) + self.assertIsInstance(default_type, _default_type.DefaultType) + self.assertEqual(volume_type_id, default_type.volume_type_id) + + # List all default types + default_types = self.conn.block_storage.default_types() + for default_type in default_types: + self.assertIsInstance(default_type, _default_type.DefaultType) + # There could be existing default types set in the environment + # Just verify that the default type we have set is correct + if self.PROJECT_ID == default_type.project_id: + self.assertEqual(volume_type_id, default_type.volume_type_id) + + # Unset default type for a project + default_type = self.conn.block_storage.unset_default_type( + self.PROJECT_ID + ) + self.assertIsNone(default_type) + + # Delete the volume type + vol_type = self.operator_cloud.block_storage.delete_type( + volume_type_id, + ignore_missing=False, + ) + self.assertIsNone(vol_type) diff --git a/openstack/tests/unit/block_storage/v3/test_default_type.py b/openstack/tests/unit/block_storage/v3/test_default_type.py new file mode 100644 index 000000000..447eaac94 --- /dev/null +++ b/openstack/tests/unit/block_storage/v3/test_default_type.py @@ -0,0 +1,55 @@ +# 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 unittest import mock + +from keystoneauth1 import adapter + +from openstack.block_storage.v3 import default_type +from openstack.tests.unit import base + + +PROJECT_ID = 'd5e678b5-f88b-411c-876b-f6ec2ba999bf' +VOLUME_TYPE_ID = 'adef1cf8-736e-4b62-a2db-f8b6b6c1d953' + +DEFAULT_TYPE = { + 'project_id': PROJECT_ID, + 'volume_type_id': VOLUME_TYPE_ID, +} + + +class TestDefaultType(base.TestCase): + def setUp(self): + super().setUp() + self.resp = mock.Mock() + self.resp.body = None + self.resp.status_code = 200 + self.resp.json = mock.Mock(return_value=self.resp.body) + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.default_microversion = '3.67' + self.sess.post = mock.Mock(return_value=self.resp) + self.sess._get_connection = mock.Mock(return_value=self.cloud) + + def test_basic(self): + sot = default_type.DefaultType(**DEFAULT_TYPE) + self.assertEqual("default_type", sot.resource_key) + self.assertEqual("default_types", sot.resources_key) + self.assertEqual("/default-types", sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_create(self): + sot = default_type.DefaultType(**DEFAULT_TYPE) + self.assertEqual(DEFAULT_TYPE["project_id"], sot.project_id) + self.assertEqual(DEFAULT_TYPE["volume_type_id"], sot.volume_type_id) diff --git a/releasenotes/notes/add-default-type-support-aaa1e54b8bd16d86.yaml b/releasenotes/notes/add-default-type-support-aaa1e54b8bd16d86.yaml new file mode 100644 index 000000000..b2ea6adde --- /dev/null +++ b/releasenotes/notes/add-default-type-support-aaa1e54b8bd16d86.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Added support for the following operations: + + * Set default volume type + * Get default volume type + * List default volume type + * Unset default volume type From e53b1b7606b178d7d66ba0d46357a1c061ef8bb4 Mon Sep 17 00:00:00 2001 From: Junwoo Park Date: Wed, 4 Sep 2024 01:48:25 +0900 Subject: [PATCH 3580/3836] Add getting info about the Limit in a DNS SDK. I added this because showing the info lists for a limit as specified in the DNS API documentation is not implemented in the SDK. Change-Id: I4d7b9a3c0f652f9a07fbf70df35e3c780462514c --- doc/source/user/proxies/dns.rst | 7 ++++ doc/source/user/resources/dns/index.rst | 1 + doc/source/user/resources/dns/v2/limit.rst | 12 ++++++ openstack/dns/v2/_proxy.py | 11 ++++++ openstack/dns/v2/limit.py | 44 ++++++++++++++++++++++ openstack/tests/unit/dns/v2/test_limit.py | 35 +++++++++++++++++ 6 files changed, 110 insertions(+) create mode 100644 doc/source/user/resources/dns/v2/limit.rst create mode 100644 openstack/dns/v2/limit.py create mode 100644 openstack/tests/unit/dns/v2/test_limit.py diff --git a/doc/source/user/proxies/dns.rst b/doc/source/user/proxies/dns.rst index 212651df8..cc46163a2 100644 --- a/doc/source/user/proxies/dns.rst +++ b/doc/source/user/proxies/dns.rst @@ -69,6 +69,13 @@ Zone Share Operations :members: create_zone_share, delete_zone_share, get_zone_share, find_zone_share, zone_shares +Limit Operations +^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.dns.v2._proxy.Proxy + :noindex: + :members: limits + Service Status Operations ^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/dns/index.rst b/doc/source/user/resources/dns/index.rst index c270c67e1..f7cb0e96b 100644 --- a/doc/source/user/resources/dns/index.rst +++ b/doc/source/user/resources/dns/index.rst @@ -11,4 +11,5 @@ DNS Resources v2/zone_share v2/floating_ip v2/recordset + v2/limit v2/service_status diff --git a/doc/source/user/resources/dns/v2/limit.rst b/doc/source/user/resources/dns/v2/limit.rst new file mode 100644 index 000000000..b5af05ea2 --- /dev/null +++ b/doc/source/user/resources/dns/v2/limit.rst @@ -0,0 +1,12 @@ +openstack.dns.v2.limit +====================== + +.. automodule:: openstack.dns.v2.limit + +The Limit Class +--------------- + +The ``Limit`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.dns.v2.limit.Limit + :members: diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py index a5497da62..a3b6a021b 100644 --- a/openstack/dns/v2/_proxy.py +++ b/openstack/dns/v2/_proxy.py @@ -11,6 +11,7 @@ # under the License. from openstack.dns.v2 import floating_ip as _fip +from openstack.dns.v2 import limit as _limit from openstack.dns.v2 import recordset as _rs from openstack.dns.v2 import service_status as _svc_status from openstack.dns.v2 import zone as _zone @@ -24,6 +25,7 @@ class Proxy(proxy.Proxy): _resource_registry = { "floating_ip": _fip.FloatingIP, + "limits": _limit.Limit, "recordset": _rs.Recordset, "service_status": _svc_status.ServiceStatus, "zone": _zone.Zone, @@ -658,6 +660,15 @@ def delete_zone_share(self, zone, zone_share, ignore_missing=True): zone_id=zone_obj.id, ) + # ======== Limits ======== + def limits(self, **query): + """Retrieve a generator of limits + + :returns: A generator of limits + (:class:`~openstack.dns.v2.limit.Limit`) instances + """ + return self._list(_limit.Limit, **query) + # ======== Service Statuses ======== def service_statuses(self): """Retrieve a generator of service statuses diff --git a/openstack/dns/v2/limit.py b/openstack/dns/v2/limit.py new file mode 100644 index 000000000..c1ac459b6 --- /dev/null +++ b/openstack/dns/v2/limit.py @@ -0,0 +1,44 @@ +# 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.dns.v2 import _base +from openstack import resource + + +class Limit(_base.Resource): + """DNS Limit Resource""" + + resource_key = 'limit' + base_path = '/limits' + + # capabilities + allow_list = True + + #: Properties + #: The max amount of items allowed per page + max_page_limit = resource.Body('max_page_limit', type=int) + #: The max length of a recordset name + max_recordset_name_length = resource.Body( + 'max_recordset_name_length', type=int + ) + #: The max amount of records contained in a recordset + max_recordset_records = resource.Body('max_recordset_records', type=int) + #: The max length of a zone name + max_zone_name_length = resource.Body('max_zone_name_length', type=int) + #: The max amount of records in a zone + max_zone_records = resource.Body('max_zone_records', type=int) + #: The max amount of recordsets per zone + max_zone_recordsets = resource.Body('max_zone_recordsets', type=int) + #: The max amount of zones for this project + max_zones = resource.Body('max_zones', type=int) + #: The lowest ttl allowed on this system + min_ttl = resource.Body('min_ttl', type=int) diff --git a/openstack/tests/unit/dns/v2/test_limit.py b/openstack/tests/unit/dns/v2/test_limit.py new file mode 100644 index 000000000..e8ace272b --- /dev/null +++ b/openstack/tests/unit/dns/v2/test_limit.py @@ -0,0 +1,35 @@ +# 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.dns.v2 import limit as _limit +from openstack.tests.unit import base + +IDENTIFIER = 'limit' +EXAMPLE = { + "max_page_limit": 1000, + "max_recordset_name_length": 255, + "max_recordset_records": 20, + "max_zone_name_length": 255, + "max_zone_records": 500, + "max_zone_recordsets": 500, + "max_zones": 10, + "min_ttl": 100, +} + + +class TestLimit(base.TestCase): + def test_basic(self): + sot = _limit.Limit() + self.assertEqual('limit', sot.resource_key) + self.assertEqual(None, sot.resources_key) + self.assertEqual('/limits', sot.base_path) + self.assertTrue(sot.allow_list) From 0b592bceb927a7f43264ce642fae7e38d931674f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Ribaud?= Date: Wed, 9 Oct 2024 12:11:00 +0200 Subject: [PATCH 3581/3836] Avoid race condition between rule removal and share deletion in tearDown The test_create_delete_access_rule_with_locks test occasionally fails because tearDown attempts to delete the share before the access rule, which involves locking, has been fully removed. This patch ensures that the access rule is properly removed before proceeding with share deletion, thus preventing the race condition. Change-Id: I61f889bc07d84c80116db2f2b15ce6dc5eb10c5d --- .../functional/shared_file_system/test_share_access_rule.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openstack/tests/functional/shared_file_system/test_share_access_rule.py b/openstack/tests/functional/shared_file_system/test_share_access_rule.py index b242cc52f..4050a3507 100644 --- a/openstack/tests/functional/shared_file_system/test_share_access_rule.py +++ b/openstack/tests/functional/shared_file_system/test_share_access_rule.py @@ -85,6 +85,8 @@ def test_create_delete_access_rule_with_locks(self): lock_deletion=True, lock_visibility=True, ) + self.user_cloud.share.delete_access_rule( access_rule['id'], self.SHARE_ID, unrestrict=True ) + self.user_cloud.shared_file_system.wait_for_delete(access_rule) From a9ad50640f3d17555a208099c9f802560fc7549a Mon Sep 17 00:00:00 2001 From: Rajat Dhasmana Date: Tue, 8 Oct 2024 09:26:21 +0000 Subject: [PATCH 3582/3836] Fix volume backup restore response Previously the backup restore response only included ``id`` whereas the restore API returns ``backup_id``, ``volume_id`` and ``volume_name`` fields. Turns out the resource_response_key was missing in the translate response method and the has_body parameter was set to False indicating that the response doesn't return a body which is not true. This patch fixes the above stated issues. Story: 2011235 Task: 51137 Change-Id: Id5c7fd2f0fcb55474b44b688bfdebaca4c670bd2 --- openstack/block_storage/v2/backup.py | 4 ++- openstack/block_storage/v3/backup.py | 4 ++- .../unit/block_storage/v2/test_backup.py | 36 +++++++++++++++++++ .../unit/block_storage/v3/test_backup.py | 36 +++++++++++++++++++ .../fix-restore-resp-4e0bf3a246f3dc59.yaml | 6 ++++ 5 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/fix-restore-resp-4e0bf3a246f3dc59.yaml diff --git a/openstack/block_storage/v2/backup.py b/openstack/block_storage/v2/backup.py index 12c78f1cb..afde91ea6 100644 --- a/openstack/block_storage/v2/backup.py +++ b/openstack/block_storage/v2/backup.py @@ -86,6 +86,8 @@ class Backup(resource.Resource): updated_at = resource.Body("updated_at") #: The UUID of the volume. volume_id = resource.Body("volume_id") + #: The name of the volume. + volume_name = resource.Body("volume_name") def create(self, session, prepend_key=True, base_path=None, **params): """Create a remote resource based on this instance. @@ -186,7 +188,7 @@ def restore(self, session, volume_id=None, name=None): 'Either of `name` or `volume_id` must be specified.' ) response = session.post(url, json=body) - self._translate_response(response, has_body=False) + self._translate_response(response, resource_response_key='restore') return self def force_delete(self, session): diff --git a/openstack/block_storage/v3/backup.py b/openstack/block_storage/v3/backup.py index b942fe98d..f37fdb411 100644 --- a/openstack/block_storage/v3/backup.py +++ b/openstack/block_storage/v3/backup.py @@ -100,6 +100,8 @@ class Backup(resource.Resource): user_id = resource.Body('user_id') #: The UUID of the volume. volume_id = resource.Body("volume_id") + #: The name of the volume. + volume_name = resource.Body("volume_name") _max_microversion = "3.64" @@ -202,7 +204,7 @@ def restore(self, session, volume_id=None, name=None): 'Either of `name` or `volume_id` must be specified.' ) response = session.post(url, json=body) - self._translate_response(response, has_body=False) + self._translate_response(response, resource_response_key='restore') return self def force_delete(self, session): diff --git a/openstack/tests/unit/block_storage/v2/test_backup.py b/openstack/tests/unit/block_storage/v2/test_backup.py index 040063024..0e7000076 100644 --- a/openstack/tests/unit/block_storage/v2/test_backup.py +++ b/openstack/tests/unit/block_storage/v2/test_backup.py @@ -137,6 +137,18 @@ def test_create_incremental(self): def test_restore(self): sot = backup.Backup(**BACKUP) + restore_response = mock.Mock() + restore_response.status_code = 202 + restore_response.json.return_value = { + "restore": { + "backup_id": "back", + "volume_id": "vol", + "volume_name": "name", + } + } + restore_response.headers = {} + self.sess.post.return_value = restore_response + self.assertEqual(sot, sot.restore(self.sess, 'vol', 'name')) url = 'backups/%s/restore' % FAKE_ID @@ -146,6 +158,18 @@ def test_restore(self): def test_restore_name(self): sot = backup.Backup(**BACKUP) + restore_response = mock.Mock() + restore_response.status_code = 202 + restore_response.json.return_value = { + "restore": { + "backup_id": "back", + "volume_id": "vol", + "volume_name": "name", + } + } + restore_response.headers = {} + self.sess.post.return_value = restore_response + self.assertEqual(sot, sot.restore(self.sess, name='name')) url = 'backups/%s/restore' % FAKE_ID @@ -155,6 +179,18 @@ def test_restore_name(self): def test_restore_vol_id(self): sot = backup.Backup(**BACKUP) + restore_response = mock.Mock() + restore_response.status_code = 202 + restore_response.json.return_value = { + "restore": { + "backup_id": "back", + "volume_id": "vol", + "volume_name": "name", + } + } + restore_response.headers = {} + self.sess.post.return_value = restore_response + self.assertEqual(sot, sot.restore(self.sess, volume_id='vol')) url = 'backups/%s/restore' % FAKE_ID diff --git a/openstack/tests/unit/block_storage/v3/test_backup.py b/openstack/tests/unit/block_storage/v3/test_backup.py index a80b97d46..63eb95975 100644 --- a/openstack/tests/unit/block_storage/v3/test_backup.py +++ b/openstack/tests/unit/block_storage/v3/test_backup.py @@ -150,6 +150,18 @@ def test_create_incremental(self): def test_restore(self): sot = backup.Backup(**BACKUP) + restore_response = mock.Mock() + restore_response.status_code = 202 + restore_response.json.return_value = { + "restore": { + "backup_id": "back", + "volume_id": "vol", + "volume_name": "name", + } + } + restore_response.headers = {} + self.sess.post.return_value = restore_response + self.assertEqual(sot, sot.restore(self.sess, 'vol', 'name')) url = 'backups/%s/restore' % FAKE_ID @@ -159,6 +171,18 @@ def test_restore(self): def test_restore_name(self): sot = backup.Backup(**BACKUP) + restore_response = mock.Mock() + restore_response.status_code = 202 + restore_response.json.return_value = { + "restore": { + "backup_id": "back", + "volume_id": "vol", + "volume_name": "name", + } + } + restore_response.headers = {} + self.sess.post.return_value = restore_response + self.assertEqual(sot, sot.restore(self.sess, name='name')) url = 'backups/%s/restore' % FAKE_ID @@ -168,6 +192,18 @@ def test_restore_name(self): def test_restore_vol_id(self): sot = backup.Backup(**BACKUP) + restore_response = mock.Mock() + restore_response.status_code = 202 + restore_response.json.return_value = { + "restore": { + "backup_id": "back", + "volume_id": "vol", + "volume_name": "name", + } + } + restore_response.headers = {} + self.sess.post.return_value = restore_response + self.assertEqual(sot, sot.restore(self.sess, volume_id='vol')) url = 'backups/%s/restore' % FAKE_ID diff --git a/releasenotes/notes/fix-restore-resp-4e0bf3a246f3dc59.yaml b/releasenotes/notes/fix-restore-resp-4e0bf3a246f3dc59.yaml new file mode 100644 index 000000000..884b93c4c --- /dev/null +++ b/releasenotes/notes/fix-restore-resp-4e0bf3a246f3dc59.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Previously the volume backup restore response only + returned ``id`` and now it also returns ``volume_id`` + and ``volume_name`` fields. From e4536fff7995d2d1b5f2fb3708498fc9691ba5c2 Mon Sep 17 00:00:00 2001 From: goldmung Date: Tue, 1 Oct 2024 15:01:36 +0900 Subject: [PATCH 3583/3836] Add TSIG key support for DNS zones in SDK This commit adds functionality for managing TSIG keys for DNS zones, which was missing in the SDK despite being documented in the DNS API. - Implemented create, delete, and list operations for TSIG keys related to zones. Change-Id: Id13e434aa1497a46b60d35fe03188d30e974b50e --- openstack/dns/v2/_proxy.py | 75 +++++++++++++++++++++ openstack/dns/v2/tsigkey.py | 62 +++++++++++++++++ openstack/tests/unit/dns/v2/test_proxy.py | 28 ++++++++ openstack/tests/unit/dns/v2/test_tsigkey.py | 58 ++++++++++++++++ 4 files changed, 223 insertions(+) create mode 100644 openstack/dns/v2/tsigkey.py create mode 100644 openstack/tests/unit/dns/v2/test_tsigkey.py diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py index a5497da62..f07895fd5 100644 --- a/openstack/dns/v2/_proxy.py +++ b/openstack/dns/v2/_proxy.py @@ -13,6 +13,7 @@ from openstack.dns.v2 import floating_ip as _fip from openstack.dns.v2 import recordset as _rs from openstack.dns.v2 import service_status as _svc_status +from openstack.dns.v2 import tsigkey as _tsigkey from openstack.dns.v2 import zone as _zone from openstack.dns.v2 import zone_export as _zone_export from openstack.dns.v2 import zone_import as _zone_import @@ -27,6 +28,7 @@ class Proxy(proxy.Proxy): "recordset": _rs.Recordset, "service_status": _svc_status.ServiceStatus, "zone": _zone.Zone, + "tsigkey": _tsigkey.TSIGKey, "zone_export": _zone_export.ZoneExport, "zone_import": _zone_import.ZoneImport, "zone_share": _zone_share.ZoneShare, @@ -719,3 +721,76 @@ def _service_cleanup( filters=filters, resource_evaluation_fn=resource_evaluation_fn, ) + + # ====== TSIG keys ====== + def tsigkeys(self, **query): + """Retrieve a generator of zones + + :param dict query: Optional query parameters to be sent to limit the + resources being returned. + + :returns: A generator of zone + :class: `~openstack.dns.v2.tsigkey.TSIGKey` instances. + """ + return self._list(_tsigkey.TSIGKey, **query) + + def create_tsigkey(self, **attrs): + """Create a new tsigkey from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.dns.v2.tsigkey.Tsigkey`, + comprised of the properties on the Tsigkey class. + :returns: The results of zone creation. + :rtype: :class:`~openstack.dns.v2.tsigkey.Tsigkey` + """ + return self._create(_tsigkey.TSIGKey, prepend_key=False, **attrs) + + def get_tsigkey(self, tsigkey): + """Get a zone + + :param tsigkey: The value can be the ID of a tsigkey + or a :class:'~openstack.dns.v2.tsigkey.TSIGKey' instance. + :returns: A generator of tsigkey + :class:'~openstack.dns.v2.tsigkey.TSIGKey' instances. + """ + return self._get(_tsigkey.TSIGKey, tsigkey) + + def delete_tsigkey( + self, tsigkey, ignore_missing=True, delete_shares=False + ): + """Delete a TSIG key + + :param tsigkey: The value can be the ID of a TSIG key + or a :class:`~openstack.dns.v2.tsigkey.TSIGKey` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the TSIG key does not exist. + When set to ``True``, no exception will be set when attempting to + delete a nonexistent TSIG key. + + :returns: TSIG Key that has been deleted + :rtype: :class:`~openstack.dns.v2.tsigkey.TSIGKey` + """ + + return self._delete( + _tsigkey.TSIGKey, + tsigkey, + ignore_missing=ignore_missing, + delete_shares=delete_shares, + ) + + def find_tsigkey(self, name_or_id, ignore_missing=True): + """Find a single tsigkey + + :param name_or_id: The name or ID of a tsigkey + :param bool ignore_missing: When set to ``False`` + :class: `!openstack.exceptions.ResourceNotFound` will be raised + when the tsigkey does not exit. + Wehn set to ``True``, no exception will be set when attempting + to delete a nonexitstent zone. + + :returns::class:`~openstack.dns.v2.tsigkey.TSIGKey` + """ + return self._find( + _tsigkey.TSIGKey, name_or_id, ignore_missing=ignore_missing + ) diff --git a/openstack/dns/v2/tsigkey.py b/openstack/dns/v2/tsigkey.py new file mode 100644 index 000000000..089642f21 --- /dev/null +++ b/openstack/dns/v2/tsigkey.py @@ -0,0 +1,62 @@ +# 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.dns.v2 import _base +from openstack import resource +# from openstack import exceptions +# from openstack import utils + + +class TSIGKey(_base.Resource): + """DNS TSIGKEY Resource""" + + resources_key = 'tsigkeys' + base_path = '/tsigkeys' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + commit_method = "PATCH" + + _query_mapping = resource.QueryParameters( + 'name', + 'algorithm', + 'scope', + 'limit', + 'marker', + ) + + #: Properties + + #: ID for the resource + id = resource.Body('id') + #: resource id for this tsigkey which can be either zone or pool id + resource_id = resource.Body('resource_id') + #: TSIGKey name + name = resource.Body('name') + #: scope for this tsigkey which can be either ZONE or POOL scope + scope = resource.Body('scope') + #: The actual key to be used + secret = resource.Body('secret') + #: The encryption algorithm for this tsigkey + algorithm = resource.Body('algorithm') + #: Timestamp when the tsigkey was created + created_at = resource.Body('created_at') + #: Timestamp when the tsigkey was last updated + updated_at = resource.Body('updated_at') + #: Links contains a 'self' pertaining to this tsigkey or a 'next' pertaining + #: to next page + links = resource.Body('links', type=dict) diff --git a/openstack/tests/unit/dns/v2/test_proxy.py b/openstack/tests/unit/dns/v2/test_proxy.py index 97b60fe2f..c08d5cc6a 100644 --- a/openstack/tests/unit/dns/v2/test_proxy.py +++ b/openstack/tests/unit/dns/v2/test_proxy.py @@ -14,6 +14,7 @@ from openstack.dns.v2 import floating_ip from openstack.dns.v2 import recordset from openstack.dns.v2 import service_status +from openstack.dns.v2 import tsigkey from openstack.dns.v2 import zone from openstack.dns.v2 import zone_export from openstack.dns.v2 import zone_import @@ -324,3 +325,30 @@ def test_service_status_get(self): self.verify_get( self.proxy.get_service_status, service_status.ServiceStatus ) + + +class TestDnsTsigKey(TestDnsProxy): + def test_tsigkey_create(self): + self.verify_create( + self.proxy.create_tsigkey, + tsigkey.TSIGKey, + method_kwargs={'name': 'id'}, + expected_kwargs={'name': 'id', 'prepend_key': False}, + ) + + def test_tsigkey_delete(self): + self.verify_delete( + self.proxy.delete_tsigkey, + tsigkey.TSIGKey, + True, + expected_kwargs={'ignore_missing': True, 'delete_shares': False}, + ) + + def test_tsigkey_find(self): + self.verify_find(self.proxy.find_tsigkey, tsigkey.TSIGKey) + + def test_tsigkey_get(self): + self.verify_get(self.proxy.get_tsigkey, tsigkey.TSIGKey) + + def test_tesigkeys(self): + self.verify_list(self.proxy.tsigkeys, tsigkey.TSIGKey) diff --git a/openstack/tests/unit/dns/v2/test_tsigkey.py b/openstack/tests/unit/dns/v2/test_tsigkey.py new file mode 100644 index 000000000..bf4d4ba91 --- /dev/null +++ b/openstack/tests/unit/dns/v2/test_tsigkey.py @@ -0,0 +1,58 @@ +# 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.dns.v2 import tsigkey +from openstack.tests.unit import base + +IDENTIFIER = '4c72c7d3-6cfa-4fe1-9984-7705119f0228' +EXAMPLE = { + "id": IDENTIFIER, + "name": 'test-key', + "algorithm": 'hmac-sha512', + "secret": 'test-secret', + "scope": 'POOL', + "resource_id": IDENTIFIER, +} + + +class TestTsigKey(base.TestCase): + def test_basic(self): + sot = tsigkey.TSIGKey() + self.assertEqual(None, sot.resource_key) + self.assertEqual('tsigkeys', sot.resources_key) + self.assertEqual('/tsigkeys', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertEqual('PATCH', sot.commit_method) + + self.assertDictEqual( + { + 'name': 'name', + 'algorithm': 'algorithm', + 'scope': 'scope', + 'limit': 'limit', + 'marker': 'marker', + }, + sot._query_mapping._mapping, + ) + + def test_make_it(self): + sot = tsigkey.TSIGKey(**EXAMPLE) + self.assertEqual(IDENTIFIER, sot.id) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['algorithm'], sot.algorithm) + self.assertEqual(EXAMPLE['scope'], sot.scope) + self.assertEqual(EXAMPLE['resource_id'], sot.resource_id) + self.assertEqual(EXAMPLE['secret'], sot.secret) From f6b9a3907a115e079a9a7d925a6950280805e8a6 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 15 Oct 2024 13:07:25 +0100 Subject: [PATCH 3584/3836] compute: Add locked filter for Server Change-Id: If47890ec259249a9cdec4bb758f91889c7991f09 Closes-bug: #2084547 Signed-off-by: Stephen Finucane --- openstack/compute/v2/server.py | 1 + openstack/tests/unit/compute/v2/test_server.py | 1 + 2 files changed, 2 insertions(+) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index e7d18637b..925699474 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -69,6 +69,7 @@ class Server(resource.Resource, metadata.MetadataMixin, tag.TagMixin): "key_name", "launch_index", "launched_at", + "locked", "locked_by", "name", "node", diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index af0a02841..2c9e59c5c 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -160,6 +160,7 @@ def test_basic(self): "launch_index": "launch_index", "launched_at": "launched_at", "limit": "limit", + "locked": "locked", "locked_by": "locked_by", "marker": "marker", "name": "name", From 886fb80fbfca65821dc78a7634a30fd8ffe74111 Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Fri, 18 Oct 2024 09:01:28 +0000 Subject: [PATCH 3585/3836] Add ``trunk_details`` to ``ports`` resource Add ``trunk_details`` attribute to ``port`` resource. This attribute is a dictionary with the trunk ID and a list of subports. Each element in the subports list is a dictionary with the subport ID, the segmentation type and segmentation ID. Related-Bug: #2074187 Change-Id: I2765b308be8af0ff2ee9faaa983f13e42fbe04f9 --- openstack/baremetal/v1/port.py | 4 ++++ .../notes/add-port-trunk-details-ed2d98a36ce70c0f.yaml | 7 +++++++ 2 files changed, 11 insertions(+) create mode 100644 releasenotes/notes/add-port-trunk-details-ed2d98a36ce70c0f.yaml diff --git a/openstack/baremetal/v1/port.py b/openstack/baremetal/v1/port.py index 2105508e6..9db9fc12e 100644 --- a/openstack/baremetal/v1/port.py +++ b/openstack/baremetal/v1/port.py @@ -76,6 +76,10 @@ class Port(_common.Resource): #: The UUID of PortGroup this port belongs to. Added in API microversion #: 1.24. port_group_id = resource.Body('portgroup_uuid') + #: Read-only. The parent port trunk details dictionary, with the trunk ID + # and the subports information (port ID, segmentation ID and segmentation + # type). + trunk_details = resource.Body('trunk_details', type=dict) #: Timestamp at which the port was last updated. updated_at = resource.Body('updated_at') diff --git a/releasenotes/notes/add-port-trunk-details-ed2d98a36ce70c0f.yaml b/releasenotes/notes/add-port-trunk-details-ed2d98a36ce70c0f.yaml new file mode 100644 index 000000000..ccfbffc75 --- /dev/null +++ b/releasenotes/notes/add-port-trunk-details-ed2d98a36ce70c0f.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Add ``trunk_details`` attribute to ``port`` resource. This attribute is + a dictionary with the trunk ID and a list of subports. Each element in the + subports list is a dictionary with the subport ID, the segmentation type + and segmentation ID. From 7e592aabe3172dc0758076e5a62de07d36d494e1 Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Fri, 18 Oct 2024 14:57:14 +0000 Subject: [PATCH 3586/3836] Add functional test for tags in Neutron resources Change-Id: I05d55e3ddddf52e477ed53a8bfdaefefd7874efd --- openstack/network/v2/_proxy.py | 61 +++++++++++++ .../tests/functional/network/v2/common.py | 90 +++++++++++++++++++ .../functional/network/v2/test_floating_ip.py | 20 ++--- .../functional/network/v2/test_network.py | 19 +--- .../tests/functional/network/v2/test_port.py | 21 ++--- .../functional/network/v2/test_router.py | 19 +--- .../network/v2/test_security_group.py | 19 +--- .../functional/network/v2/test_subnet.py | 21 ++--- .../functional/network/v2/test_subnet_pool.py | 21 ++--- 9 files changed, 183 insertions(+), 108 deletions(-) create mode 100644 openstack/tests/functional/network/v2/common.py diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index f69c8e0b5..1ea397046 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -5408,6 +5408,17 @@ def _check_tag_support(resource): % resource.__class__.__name__ ) + def get_tags(self, resource): + """Retrieve the tags of a specified resource + + :param resource: :class:`~openstack.resource.Resource` instance. + + :returns: The resource tags list + :rtype: "list" + """ + self._check_tag_support(resource) + return resource.fetch_tags(self).tags + def set_tags(self, resource, tags): """Replace tags of a specified resource with specified tags @@ -5422,6 +5433,56 @@ def set_tags(self, resource, tags): self._check_tag_support(resource) return resource.set_tags(self, tags) + def add_tag(self, resource, tag): + """Add one single tag to a specified resource + + :param resource: :class:`~openstack.resource.Resource` instance. + :param tag: New tag to be set. + :type tag: "str" + + :returns: The updated resource + :rtype: :class:`~openstack.resource.Resource` + """ + self._check_tag_support(resource) + return resource.add_tag(self, tag) + + def remove_tag(self, resource, tag): + """Remove one single tag of a specified resource + + :param resource: :class:`~openstack.resource.Resource` instance. + :param tag: New tag to be set. + :type tag: "str" + + :returns: The updated resource + :rtype: :class:`~openstack.resource.Resource` + """ + self._check_tag_support(resource) + return resource.remove_tag(self, tag) + + def remove_all_tags(self, resource): + """Remove all tags of a specified resource + + :param resource: :class:`~openstack.resource.Resource` instance. + + :returns: The updated resource + :rtype: :class:`~openstack.resource.Resource` + """ + self._check_tag_support(resource) + return resource.remove_all_tags(self) + + def check_tag(self, resource, tag): + """Checks if tag exists on the specified resource + + :param resource: :class:`~openstack.resource.Resource` instance. + :param tag: Tag to be tested + :type tags: "string" + + :returns: If the tag exists in the specified resource + :rtype: bool + """ + self._check_tag_support(resource) + return resource.check_tag(self, tag) + def create_trunk(self, **attrs): """Create a new trunk from attributes diff --git a/openstack/tests/functional/network/v2/common.py b/openstack/tests/functional/network/v2/common.py new file mode 100644 index 000000000..e958c3f9b --- /dev/null +++ b/openstack/tests/functional/network/v2/common.py @@ -0,0 +1,90 @@ +# 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 unittest import mock + +from openstack import exceptions +from openstack.tests.functional import base + + +# NOTE: method to make mypy happy. +def _get_command(*args): + return mock.Mock() + + +class TestTagNeutron(base.BaseFunctionalTest): + get_command = _get_command + + def test_set_tags(self): + sot = self.get_command(self.ID) + self.assertEqual([], sot.tags) + + self.user_cloud.network.set_tags(sot, ["blue"]) + sot = self.get_command(self.ID) + self.assertEqual(["blue"], sot.tags) + + self.user_cloud.network.set_tags(sot, []) + sot = self.get_command(self.ID) + self.assertEqual([], sot.tags) + + def test_get_tags(self): + sot = self.get_command(self.ID) + self.assertEqual([], sot.tags) + + self.user_cloud.network.set_tags(sot, ["blue", "red"]) + tags = self.user_cloud.network.get_tags(sot) + self.assertEqual(["blue", "red"], tags) + + def test_add_tag(self): + sot = self.get_command(self.ID) + self.assertEqual([], sot.tags) + + self.user_cloud.network.add_tag(sot, "blue") + tags = self.user_cloud.network.get_tags(sot) + self.assertEqual(["blue"], tags) + + # The operation is idempotent. + self.user_cloud.network.add_tag(sot, "blue") + tags = self.user_cloud.network.get_tags(sot) + self.assertEqual(["blue"], tags) + + def test_remove_tag(self): + sot = self.get_command(self.ID) + self.assertEqual([], sot.tags) + + self.user_cloud.network.set_tags(sot, ["blue"]) + tags = self.user_cloud.network.get_tags(sot) + self.assertEqual(["blue"], tags) + + self.user_cloud.network.remove_tag(sot, "blue") + tags = self.user_cloud.network.get_tags(sot) + self.assertEqual([], tags) + + # The operation is not idempotent. + self.assertRaises( + exceptions.NotFoundException, + self.user_cloud.network.remove_tag, + sot, + "blue", + ) + + def test_remove_all_tags(self): + sot = self.get_command(self.ID) + self.assertEqual([], sot.tags) + + self.user_cloud.network.set_tags(sot, ["blue", "red"]) + sot = self.get_command(self.ID) + self.assertEqual(["blue", "red"], sot.tags) + + self.user_cloud.network.remove_all_tags(sot) + sot = self.get_command(self.ID) + self.assertEqual([], sot.tags) diff --git a/openstack/tests/functional/network/v2/test_floating_ip.py b/openstack/tests/functional/network/v2/test_floating_ip.py index 3ea1ea9c4..aa489519a 100644 --- a/openstack/tests/functional/network/v2/test_floating_ip.py +++ b/openstack/tests/functional/network/v2/test_floating_ip.py @@ -9,17 +9,17 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. - +# mypy: disable-error-code="method-assign" from openstack.network.v2 import floating_ip from openstack.network.v2 import network from openstack.network.v2 import port from openstack.network.v2 import router from openstack.network.v2 import subnet -from openstack.tests.functional import base +from openstack.tests.functional.network.v2 import common -class TestFloatingIP(base.BaseFunctionalTest): +class TestFloatingIP(common.TestTagNeutron): IPV4 = 4 EXT_CIDR = "10.100.0.0/24" INT_CIDR = "10.101.0.0/24" @@ -105,6 +105,8 @@ def setUp(self): fip = self.user_cloud.network.create_ip(**fip_args) assert isinstance(fip, floating_ip.FloatingIP) self.FIP = fip + self.ID = self.FIP.id + self.get_command = self.user_cloud.network.get_ip def tearDown(self): sot = self.user_cloud.network.delete_ip( @@ -194,18 +196,6 @@ def test_update(self): self._assert_port_details(self.PORT, sot.port_details) self.assertEqual(self.FIP.id, sot.id) - def test_set_tags(self): - sot = self.user_cloud.network.get_ip(self.FIP.id) - self.assertEqual([], sot.tags) - - self.user_cloud.network.set_tags(sot, ["blue"]) - sot = self.user_cloud.network.get_ip(self.FIP.id) - self.assertEqual(["blue"], sot.tags) - - self.user_cloud.network.set_tags(sot, []) - sot = self.user_cloud.network.get_ip(self.FIP.id) - self.assertEqual([], sot.tags) - def _assert_port_details(self, port, port_details): self.assertEqual(port.name, port_details["name"]) self.assertEqual(port.network_id, port_details["network_id"]) diff --git a/openstack/tests/functional/network/v2/test_network.py b/openstack/tests/functional/network/v2/test_network.py index f48cf16d2..f2a116e00 100644 --- a/openstack/tests/functional/network/v2/test_network.py +++ b/openstack/tests/functional/network/v2/test_network.py @@ -9,10 +9,10 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. - +# mypy: disable-error-code="method-assign" from openstack.network.v2 import network -from openstack.tests.functional import base +from openstack.tests.functional.network.v2 import common def create_network(conn, name, cidr): @@ -35,7 +35,7 @@ def delete_network(conn, network, subnet): conn.network.delete_network(network) -class TestNetwork(base.BaseFunctionalTest): +class TestNetwork(common.TestTagNeutron): ID = None def setUp(self): @@ -45,6 +45,7 @@ def setUp(self): assert isinstance(sot, network.Network) self.assertEqual(self.NAME, sot.name) self.ID = sot.id + self.get_command = self.user_cloud.network.get_network def tearDown(self): sot = self.user_cloud.network.delete_network( @@ -83,15 +84,3 @@ def test_get(self): def test_list(self): names = [o.name for o in self.user_cloud.network.networks()] self.assertIn(self.NAME, names) - - def test_set_tags(self): - sot = self.user_cloud.network.get_network(self.ID) - self.assertEqual([], sot.tags) - - self.user_cloud.network.set_tags(sot, ["blue"]) - sot = self.user_cloud.network.get_network(self.ID) - self.assertEqual(["blue"], sot.tags) - - self.user_cloud.network.set_tags(sot, []) - sot = self.user_cloud.network.get_network(self.ID) - self.assertEqual([], sot.tags) diff --git a/openstack/tests/functional/network/v2/test_port.py b/openstack/tests/functional/network/v2/test_port.py index 5a57686e0..f62c8e11b 100644 --- a/openstack/tests/functional/network/v2/test_port.py +++ b/openstack/tests/functional/network/v2/test_port.py @@ -9,15 +9,15 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. - +# mypy: disable-error-code="method-assign" from openstack.network.v2 import network from openstack.network.v2 import port from openstack.network.v2 import subnet -from openstack.tests.functional import base +from openstack.tests.functional.network.v2 import common -class TestPort(base.BaseFunctionalTest): +class TestPort(common.TestTagNeutron): IPV4 = 4 CIDR = "10.100.0.0/24" NET_ID = None @@ -48,7 +48,8 @@ def setUp(self): ) assert isinstance(prt, port.Port) self.assertEqual(self.PORT_NAME, prt.name) - self.PORT_ID = prt.id + self.PORT_ID = self.ID = prt.id + self.get_command = self.user_cloud.network.get_port def tearDown(self): sot = self.user_cloud.network.delete_port( @@ -84,15 +85,3 @@ def test_update(self): self.PORT_ID, name=self.UPDATE_NAME ) self.assertEqual(self.UPDATE_NAME, sot.name) - - def test_set_tags(self): - sot = self.user_cloud.network.get_port(self.PORT_ID) - self.assertEqual([], sot.tags) - - self.user_cloud.network.set_tags(sot, ["blue"]) - sot = self.user_cloud.network.get_port(self.PORT_ID) - self.assertEqual(["blue"], sot.tags) - - self.user_cloud.network.set_tags(sot, []) - sot = self.user_cloud.network.get_port(self.PORT_ID) - self.assertEqual([], sot.tags) diff --git a/openstack/tests/functional/network/v2/test_router.py b/openstack/tests/functional/network/v2/test_router.py index cd55c8417..b40deec26 100644 --- a/openstack/tests/functional/network/v2/test_router.py +++ b/openstack/tests/functional/network/v2/test_router.py @@ -9,13 +9,13 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. - +# mypy: disable-error-code="method-assign" from openstack.network.v2 import router -from openstack.tests.functional import base +from openstack.tests.functional.network.v2 import common -class TestRouter(base.BaseFunctionalTest): +class TestRouter(common.TestTagNeutron): ID = None def setUp(self): @@ -26,6 +26,7 @@ def setUp(self): assert isinstance(sot, router.Router) self.assertEqual(self.NAME, sot.name) self.ID = sot.id + self.get_command = self.user_cloud.network.get_router def tearDown(self): sot = self.user_cloud.network.delete_router( @@ -57,15 +58,3 @@ def test_update(self): self.ID, name=self.UPDATE_NAME ) self.assertEqual(self.UPDATE_NAME, sot.name) - - def test_set_tags(self): - sot = self.user_cloud.network.get_router(self.ID) - self.assertEqual([], sot.tags) - - self.user_cloud.network.set_tags(sot, ["blue"]) - sot = self.user_cloud.network.get_router(self.ID) - self.assertEqual(["blue"], sot.tags) - - self.user_cloud.network.set_tags(sot, []) - sot = self.user_cloud.network.get_router(self.ID) - self.assertEqual([], sot.tags) diff --git a/openstack/tests/functional/network/v2/test_security_group.py b/openstack/tests/functional/network/v2/test_security_group.py index 023accfe9..0f4e18f28 100644 --- a/openstack/tests/functional/network/v2/test_security_group.py +++ b/openstack/tests/functional/network/v2/test_security_group.py @@ -9,13 +9,13 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. - +# mypy: disable-error-code="method-assign" from openstack.network.v2 import security_group -from openstack.tests.functional import base +from openstack.tests.functional.network.v2 import common -class TestSecurityGroup(base.BaseFunctionalTest): +class TestSecurityGroup(common.TestTagNeutron): ID = None def setUp(self): @@ -25,6 +25,7 @@ def setUp(self): assert isinstance(sot, security_group.SecurityGroup) self.assertEqual(self.NAME, sot.name) self.ID = sot.id + self.get_command = self.user_cloud.network.get_security_group def tearDown(self): sot = self.user_cloud.network.delete_security_group( @@ -51,15 +52,3 @@ def test_list_query_list_of_ids(self): o.id for o in self.user_cloud.network.security_groups(id=[self.ID]) ] self.assertIn(self.ID, ids) - - def test_set_tags(self): - sot = self.user_cloud.network.get_security_group(self.ID) - self.assertEqual([], sot.tags) - - self.user_cloud.network.set_tags(sot, ["blue"]) - sot = self.user_cloud.network.get_security_group(self.ID) - self.assertEqual(["blue"], sot.tags) - - self.user_cloud.network.set_tags(sot, []) - sot = self.user_cloud.network.get_security_group(self.ID) - self.assertEqual([], sot.tags) diff --git a/openstack/tests/functional/network/v2/test_subnet.py b/openstack/tests/functional/network/v2/test_subnet.py index 23c8744de..0cdda41c7 100644 --- a/openstack/tests/functional/network/v2/test_subnet.py +++ b/openstack/tests/functional/network/v2/test_subnet.py @@ -9,14 +9,14 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. - +# mypy: disable-error-code="method-assign" from openstack.network.v2 import network from openstack.network.v2 import subnet -from openstack.tests.functional import base +from openstack.tests.functional.network.v2 import common -class TestSubnet(base.BaseFunctionalTest): +class TestSubnet(common.TestTagNeutron): IPV4 = 4 CIDR = "10.100.0.0/24" DNS_SERVERS = ["8.8.4.4", "8.8.8.8"] @@ -45,7 +45,8 @@ def setUp(self): ) assert isinstance(sub, subnet.Subnet) self.assertEqual(self.SUB_NAME, sub.name) - self.SUB_ID = sub.id + self.SUB_ID = self.ID = sub.id + self.get_command = self.user_cloud.network.get_subnet def tearDown(self): sot = self.user_cloud.network.delete_subnet(self.SUB_ID) @@ -81,15 +82,3 @@ def test_update(self): self.SUB_ID, name=self.UPDATE_NAME ) self.assertEqual(self.UPDATE_NAME, sot.name) - - def test_set_tags(self): - sot = self.user_cloud.network.get_subnet(self.SUB_ID) - self.assertEqual([], sot.tags) - - self.user_cloud.network.set_tags(sot, ["blue"]) - sot = self.user_cloud.network.get_subnet(self.SUB_ID) - self.assertEqual(["blue"], sot.tags) - - self.user_cloud.network.set_tags(sot, []) - sot = self.user_cloud.network.get_subnet(self.SUB_ID) - self.assertEqual([], sot.tags) diff --git a/openstack/tests/functional/network/v2/test_subnet_pool.py b/openstack/tests/functional/network/v2/test_subnet_pool.py index a453c6095..c8f88d780 100644 --- a/openstack/tests/functional/network/v2/test_subnet_pool.py +++ b/openstack/tests/functional/network/v2/test_subnet_pool.py @@ -9,13 +9,13 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. - +# mypy: disable-error-code="method-assign" from openstack.network.v2 import subnet_pool as _subnet_pool -from openstack.tests.functional import base +from openstack.tests.functional.network.v2 import common -class TestSubnetPool(base.BaseFunctionalTest): +class TestSubnetPool(common.TestTagNeutron): SUBNET_POOL_ID = None MINIMUM_PREFIX_LENGTH = 8 DEFAULT_PREFIX_LENGTH = 24 @@ -40,7 +40,8 @@ def setUp(self): ) assert isinstance(subnet_pool, _subnet_pool.SubnetPool) self.assertEqual(self.SUBNET_POOL_NAME, subnet_pool.name) - self.SUBNET_POOL_ID = subnet_pool.id + self.SUBNET_POOL_ID = self.ID = subnet_pool.id + self.get_command = self.user_cloud.network.get_subnet_pool def tearDown(self): sot = self.user_cloud.network.delete_subnet_pool(self.SUBNET_POOL_ID) @@ -71,15 +72,3 @@ def test_update(self): self.SUBNET_POOL_ID, name=self.SUBNET_POOL_NAME_UPDATED ) self.assertEqual(self.SUBNET_POOL_NAME_UPDATED, sot.name) - - def test_set_tags(self): - sot = self.user_cloud.network.get_subnet_pool(self.SUBNET_POOL_ID) - self.assertEqual([], sot.tags) - - self.user_cloud.network.set_tags(sot, ["blue"]) - sot = self.user_cloud.network.get_subnet_pool(self.SUBNET_POOL_ID) - self.assertEqual(["blue"], sot.tags) - - self.user_cloud.network.set_tags(sot, []) - sot = self.user_cloud.network.get_subnet_pool(self.SUBNET_POOL_ID) - self.assertEqual([], sot.tags) From 545f0af99c577c78ddfec53a1b957a298b14dce1 Mon Sep 17 00:00:00 2001 From: Yosef S Date: Sun, 20 Oct 2024 14:00:06 +0330 Subject: [PATCH 3587/3836] Add options property to User openstack.identity.v3.user.User missing options property while it is in the API. Closes-Bug: #2085014 Change-Id: Ib1e0d3936b9536dcac4fc175f6701c3a02cd53f3 --- openstack/identity/v3/user.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openstack/identity/v3/user.py b/openstack/identity/v3/user.py index 609c9f652..d05cb64aa 100644 --- a/openstack/identity/v3/user.py +++ b/openstack/identity/v3/user.py @@ -71,3 +71,5 @@ class User(resource.Resource): #: This is a response object attribute, not valid for requests. #: *New in version 3.7* password_expires_at = resource.Body('password_expires_at') + #: A dictionary of users extra options. + options = resource.Body('options', type=dict, default={}) From aff5e358ac9e9da184129303ac54a8dda268ef29 Mon Sep 17 00:00:00 2001 From: 0weng Date: Mon, 16 Sep 2024 15:06:31 -0700 Subject: [PATCH 3588/3836] Identity: Support assigning inherited roles Change-Id: I7ab6a693f486b5093533e123e6f9d0cefa3c1a83 --- openstack/cloud/_identity.py | 42 +++--- openstack/identity/v3/_proxy.py | 80 +++++++---- openstack/identity/v3/domain.py | 38 +++-- openstack/identity/v3/project.py | 38 +++-- .../tests/unit/cloud/test_role_assignment.py | 130 +++++++++++++++++- .../tests/unit/identity/v3/test_domain.py | 114 +++++++++++++-- .../tests/unit/identity/v3/test_project.py | 114 +++++++++++++-- .../tests/unit/identity/v3/test_proxy.py | 12 ++ ...ixin-inherited-roles-ed66bb78ddeca2c9.yaml | 6 + 9 files changed, 488 insertions(+), 86 deletions(-) create mode 100644 releasenotes/notes/identity-cloud-mixin-inherited-roles-ed66bb78ddeca2c9.yaml diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index 4dbbabb7f..d70be679a 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -1256,6 +1256,7 @@ def grant_role( project=None, domain=None, system=None, + inherited=False, wait=False, timeout=60, ): @@ -1267,6 +1268,7 @@ def grant_role( :param string project: The name or id of the project. :param string domain: The id of the domain. (v3) :param bool system: The name of the system. (v3) + :param bool inherited: Whether the role assignment is inherited. (v3) :param bool wait: Wait for role to be granted :param int timeout: Timeout to wait for role to be granted @@ -1303,40 +1305,46 @@ def grant_role( # Proceed with project - precedence over domain and system if user: has_role = self.identity.validate_user_has_project_role( - project, user, role + project, user, role, inherited=inherited ) if has_role: self.log.debug('Assignment already exists') return False - self.identity.assign_project_role_to_user(project, user, role) + self.identity.assign_project_role_to_user( + project, user, role, inherited=inherited + ) else: has_role = self.identity.validate_group_has_project_role( - project, group, role + project, group, role, inherited=inherited ) if has_role: self.log.debug('Assignment already exists') return False self.identity.assign_project_role_to_group( - project, group, role + project, group, role, inherited=inherited ) elif domain: # Proceed with domain - precedence over system if user: has_role = self.identity.validate_user_has_domain_role( - domain, user, role + domain, user, role, inherited=inherited ) if has_role: self.log.debug('Assignment already exists') return False - self.identity.assign_domain_role_to_user(domain, user, role) + self.identity.assign_domain_role_to_user( + domain, user, role, inherited=inherited + ) else: has_role = self.identity.validate_group_has_domain_role( - domain, group, role + domain, group, role, inherited=inherited ) if has_role: self.log.debug('Assignment already exists') return False - self.identity.assign_domain_role_to_group(domain, group, role) + self.identity.assign_domain_role_to_group( + domain, group, role, inherited=inherited + ) else: # Proceed with system # System name must be 'all' due to checks performed in @@ -1367,6 +1375,7 @@ def revoke_role( project=None, domain=None, system=None, + inherited=False, wait=False, timeout=60, ): @@ -1378,6 +1387,7 @@ def revoke_role( :param string project: The name or id of the project. :param string domain: The id of the domain. (v3) :param bool system: The name of the system. (v3) + :param bool inherited: Whether the role assignment is inherited. :param bool wait: Wait for role to be revoked :param int timeout: Timeout to wait for role to be revoked @@ -1411,45 +1421,45 @@ def revoke_role( # Proceed with project - precedence over domain and system if user: has_role = self.identity.validate_user_has_project_role( - project, user, role + project, user, role, inherited=inherited ) if not has_role: self.log.debug('Assignment does not exists') return False self.identity.unassign_project_role_from_user( - project, user, role + project, user, role, inherited=inherited ) else: has_role = self.identity.validate_group_has_project_role( - project, group, role + project, group, role, inherited=inherited ) if not has_role: self.log.debug('Assignment does not exists') return False self.identity.unassign_project_role_from_group( - project, group, role + project, group, role, inherited=inherited ) elif domain: # Proceed with domain - precedence over system if user: has_role = self.identity.validate_user_has_domain_role( - domain, user, role + domain, user, role, inherited=inherited ) if not has_role: self.log.debug('Assignment does not exists') return False self.identity.unassign_domain_role_from_user( - domain, user, role + domain, user, role, inherited=inherited ) else: has_role = self.identity.validate_group_has_domain_role( - domain, group, role + domain, group, role, inherited=inherited ) if not has_role: self.log.debug('Assignment does not exists') return False self.identity.unassign_domain_role_from_group( - domain, group, role + domain, group, role, inherited=inherited ) else: # Proceed with system diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 48c061e19..860d994e3 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -1233,7 +1233,9 @@ def role_assignments(self, **query): """ return self._list(_role_assignment.RoleAssignment, **query) - def assign_domain_role_to_user(self, domain, user, role): + def assign_domain_role_to_user( + self, domain, user, role, *, inherited=False + ): """Assign role to user on a domain :param domain: Either the ID of a domain or a @@ -1242,14 +1244,17 @@ def assign_domain_role_to_user(self, domain, user, role): :class:`~openstack.identity.v3.user.User` instance. :param role: Either the ID of a role or a :class:`~openstack.identity.v3.role.Role` instance. + :param bool inherited: Whether the role assignment is inherited. :return: ``None`` """ domain = self._get_resource(_domain.Domain, domain) user = self._get_resource(_user.User, user) role = self._get_resource(_role.Role, role) - domain.assign_role_to_user(self, user, role) + domain.assign_role_to_user(self, user, role, inherited) - def unassign_domain_role_from_user(self, domain, user, role): + def unassign_domain_role_from_user( + self, domain, user, role, *, inherited=False + ): """Unassign role from user on a domain :param domain: Either the ID of a domain or a @@ -1258,14 +1263,17 @@ def unassign_domain_role_from_user(self, domain, user, role): :class:`~openstack.identity.v3.user.User` instance. :param role: Either the ID of a role or a :class:`~openstack.identity.v3.role.Role` instance. + :param bool inherited: Whether the role assignment is inherited. :return: ``None`` """ domain = self._get_resource(_domain.Domain, domain) user = self._get_resource(_user.User, user) role = self._get_resource(_role.Role, role) - domain.unassign_role_from_user(self, user, role) + domain.unassign_role_from_user(self, user, role, inherited) - def validate_user_has_domain_role(self, domain, user, role): + def validate_user_has_domain_role( + self, domain, user, role, *, inherited=False + ): """Validates that a user has a role on a domain :param domain: Either the ID of a domain or a @@ -1279,9 +1287,11 @@ def validate_user_has_domain_role(self, domain, user, role): domain = self._get_resource(_domain.Domain, domain) user = self._get_resource(_user.User, user) role = self._get_resource(_role.Role, role) - return domain.validate_user_has_role(self, user, role) + return domain.validate_user_has_role(self, user, role, inherited) - def assign_domain_role_to_group(self, domain, group, role): + def assign_domain_role_to_group( + self, domain, group, role, *, inherited=False + ): """Assign role to group on a domain :param domain: Either the ID of a domain or a @@ -1290,14 +1300,17 @@ def assign_domain_role_to_group(self, domain, group, role): :class:`~openstack.identity.v3.group.Group` instance. :param role: Either the ID of a role or a :class:`~openstack.identity.v3.role.Role` instance. + :param bool inherited: Whether the role assignment is inherited. :return: ``None`` """ domain = self._get_resource(_domain.Domain, domain) group = self._get_resource(_group.Group, group) role = self._get_resource(_role.Role, role) - domain.assign_role_to_group(self, group, role) + domain.assign_role_to_group(self, group, role, inherited) - def unassign_domain_role_from_group(self, domain, group, role): + def unassign_domain_role_from_group( + self, domain, group, role, *, inherited=False + ): """Unassign role from group on a domain :param domain: Either the ID of a domain or a @@ -1306,14 +1319,17 @@ def unassign_domain_role_from_group(self, domain, group, role): :class:`~openstack.identity.v3.group.Group` instance. :param role: Either the ID of a role or a :class:`~openstack.identity.v3.role.Role` instance. + :param bool inherited: Whether the role assignment is inherited. :return: ``None`` """ domain = self._get_resource(_domain.Domain, domain) group = self._get_resource(_group.Group, group) role = self._get_resource(_role.Role, role) - domain.unassign_role_from_group(self, group, role) + domain.unassign_role_from_group(self, group, role, inherited) - def validate_group_has_domain_role(self, domain, group, role): + def validate_group_has_domain_role( + self, domain, group, role, *, inherited=False + ): """Validates that a group has a role on a domain :param domain: Either the ID of a domain or a @@ -1327,9 +1343,11 @@ def validate_group_has_domain_role(self, domain, group, role): domain = self._get_resource(_domain.Domain, domain) group = self._get_resource(_group.Group, group) role = self._get_resource(_role.Role, role) - return domain.validate_group_has_role(self, group, role) + return domain.validate_group_has_role(self, group, role, inherited) - def assign_project_role_to_user(self, project, user, role): + def assign_project_role_to_user( + self, project, user, role, *, inherited=False + ): """Assign role to user on a project :param project: Either the ID of a project or a @@ -1339,14 +1357,17 @@ def assign_project_role_to_user(self, project, user, role): :class:`~openstack.identity.v3.user.User` instance. :param role: Either the ID of a role or a :class:`~openstack.identity.v3.role.Role` instance. + :param bool inherited: Whether the role assignment is inherited. :return: ``None`` """ project = self._get_resource(_project.Project, project) user = self._get_resource(_user.User, user) role = self._get_resource(_role.Role, role) - project.assign_role_to_user(self, user, role) + project.assign_role_to_user(self, user, role, inherited) - def unassign_project_role_from_user(self, project, user, role): + def unassign_project_role_from_user( + self, project, user, role, *, inherited=False + ): """Unassign role from user on a project :param project: Either the ID of a project or a @@ -1356,14 +1377,17 @@ def unassign_project_role_from_user(self, project, user, role): :class:`~openstack.identity.v3.user.User` instance. :param role: Either the ID of a role or a :class:`~openstack.identity.v3.role.Role` instance. + :param bool inherited: Whether the role assignment is inherited. :return: ``None`` """ project = self._get_resource(_project.Project, project) user = self._get_resource(_user.User, user) role = self._get_resource(_role.Role, role) - project.unassign_role_from_user(self, user, role) + project.unassign_role_from_user(self, user, role, inherited) - def validate_user_has_project_role(self, project, user, role): + def validate_user_has_project_role( + self, project, user, role, *, inherited=False + ): """Validates that a user has a role on a project :param project: Either the ID of a project or a @@ -1378,9 +1402,11 @@ def validate_user_has_project_role(self, project, user, role): project = self._get_resource(_project.Project, project) user = self._get_resource(_user.User, user) role = self._get_resource(_role.Role, role) - return project.validate_user_has_role(self, user, role) + return project.validate_user_has_role(self, user, role, inherited) - def assign_project_role_to_group(self, project, group, role): + def assign_project_role_to_group( + self, project, group, role, *, inherited=False + ): """Assign role to group on a project :param project: Either the ID of a project or a @@ -1390,14 +1416,17 @@ def assign_project_role_to_group(self, project, group, role): :class:`~openstack.identity.v3.group.Group` instance. :param role: Either the ID of a role or a :class:`~openstack.identity.v3.role.Role` instance. + :param bool inherited: Whether the role assignment is inherited. :return: ``None`` """ project = self._get_resource(_project.Project, project) group = self._get_resource(_group.Group, group) role = self._get_resource(_role.Role, role) - project.assign_role_to_group(self, group, role) + project.assign_role_to_group(self, group, role, inherited) - def unassign_project_role_from_group(self, project, group, role): + def unassign_project_role_from_group( + self, project, group, role, *, inherited=False + ): """Unassign role from group on a project :param project: Either the ID of a project or a @@ -1407,14 +1436,17 @@ def unassign_project_role_from_group(self, project, group, role): :class:`~openstack.identity.v3.group.Group` instance. :param role: Either the ID of a role or a :class:`~openstack.identity.v3.role.Role` instance. + :param bool inherited: Whether the role assignment is inherited. :return: ``None`` """ project = self._get_resource(_project.Project, project) group = self._get_resource(_group.Group, group) role = self._get_resource(_role.Role, role) - project.unassign_role_from_group(self, group, role) + project.unassign_role_from_group(self, group, role, inherited) - def validate_group_has_project_role(self, project, group, role): + def validate_group_has_project_role( + self, project, group, role, *, inherited=False + ): """Validates that a group has a role on a project :param project: Either the ID of a project or a @@ -1429,7 +1461,7 @@ def validate_group_has_project_role(self, project, group, role): project = self._get_resource(_project.Project, project) group = self._get_resource(_group.Group, group) role = self._get_resource(_role.Role, role) - return project.validate_group_has_role(self, group, role) + return project.validate_group_has_role(self, group, role, inherited) def assign_system_role_to_user(self, user, role, system): """Assign a role to user on a system diff --git a/openstack/identity/v3/domain.py b/openstack/identity/v3/domain.py index 832143195..30179d3f6 100644 --- a/openstack/identity/v3/domain.py +++ b/openstack/identity/v3/domain.py @@ -48,11 +48,18 @@ class Domain(resource.Resource): #: The links related to the domain resource. links = resource.Body('links') - def assign_role_to_user(self, session, user, role): + def assign_role_to_user(self, session, user, role, inherited): """Assign role to user on domain""" url = utils.urljoin( - self.base_path, self.id, 'users', user.id, 'roles', role.id + self.base_path, + self.id, + 'users', + user.id, + 'roles', + role.id, ) + if inherited: + url = utils.urljoin('OS-INHERIT', url, 'inherited_to_projects') resp = session.put( url, ) @@ -60,11 +67,13 @@ def assign_role_to_user(self, session, user, role): return True return False - def validate_user_has_role(self, session, user, role): + def validate_user_has_role(self, session, user, role, inherited): """Validates that a user has a role on a domain""" url = utils.urljoin( self.base_path, self.id, 'users', user.id, 'roles', role.id ) + if inherited: + url = utils.urljoin('OS-INHERIT', url, 'inherited_to_projects') resp = session.head( url, ) @@ -72,11 +81,13 @@ def validate_user_has_role(self, session, user, role): return True return False - def unassign_role_from_user(self, session, user, role): + def unassign_role_from_user(self, session, user, role, inherited): """Unassigns a role from a user on a domain""" url = utils.urljoin( self.base_path, self.id, 'users', user.id, 'roles', role.id ) + if inherited: + url = utils.urljoin('OS-INHERIT', url, 'inherited_to_projects') resp = session.delete( url, ) @@ -84,11 +95,18 @@ def unassign_role_from_user(self, session, user, role): return True return False - def assign_role_to_group(self, session, group, role): + def assign_role_to_group(self, session, group, role, inherited): """Assign role to group on domain""" url = utils.urljoin( - self.base_path, self.id, 'groups', group.id, 'roles', role.id + self.base_path, + self.id, + 'groups', + group.id, + 'roles', + role.id, ) + if inherited: + url = utils.urljoin('OS-INHERIT', url, 'inherited_to_projects') resp = session.put( url, ) @@ -96,11 +114,13 @@ def assign_role_to_group(self, session, group, role): return True return False - def validate_group_has_role(self, session, group, role): + def validate_group_has_role(self, session, group, role, inherited): """Validates that a group has a role on a domain""" url = utils.urljoin( self.base_path, self.id, 'groups', group.id, 'roles', role.id ) + if inherited: + url = utils.urljoin('OS-INHERIT', url, 'inherited_to_projects') resp = session.head( url, ) @@ -108,11 +128,13 @@ def validate_group_has_role(self, session, group, role): return True return False - def unassign_role_from_group(self, session, group, role): + def unassign_role_from_group(self, session, group, role, inherited): """Unassigns a role from a group on a domain""" url = utils.urljoin( self.base_path, self.id, 'groups', group.id, 'roles', role.id ) + if inherited: + url = utils.urljoin('OS-INHERIT', url, 'inherited_to_projects') resp = session.delete( url, ) diff --git a/openstack/identity/v3/project.py b/openstack/identity/v3/project.py index 677ce35d7..0d2937fa3 100644 --- a/openstack/identity/v3/project.py +++ b/openstack/identity/v3/project.py @@ -62,11 +62,18 @@ class Project(resource.Resource, tag.TagMixin): #: New in version 3.4 parent_id = resource.Body('parent_id') - def assign_role_to_user(self, session, user, role): + def assign_role_to_user(self, session, user, role, inherited): """Assign role to user on project""" url = utils.urljoin( - self.base_path, self.id, 'users', user.id, 'roles', role.id + self.base_path, + self.id, + 'users', + user.id, + 'roles', + role.id, ) + if inherited: + url = utils.urljoin('OS-INHERIT', url, 'inherited_to_projects') resp = session.put( url, ) @@ -74,11 +81,13 @@ def assign_role_to_user(self, session, user, role): return True return False - def validate_user_has_role(self, session, user, role): + def validate_user_has_role(self, session, user, role, inherited): """Validates that a user has a role on a project""" url = utils.urljoin( self.base_path, self.id, 'users', user.id, 'roles', role.id ) + if inherited: + url = utils.urljoin('OS-INHERIT', url, 'inherited_to_projects') resp = session.head( url, ) @@ -86,11 +95,13 @@ def validate_user_has_role(self, session, user, role): return True return False - def unassign_role_from_user(self, session, user, role): + def unassign_role_from_user(self, session, user, role, inherited): """Unassigns a role from a user on a project""" url = utils.urljoin( self.base_path, self.id, 'users', user.id, 'roles', role.id ) + if inherited: + url = utils.urljoin('OS-INHERIT', url, 'inherited_to_projects') resp = session.delete( url, ) @@ -98,11 +109,18 @@ def unassign_role_from_user(self, session, user, role): return True return False - def assign_role_to_group(self, session, group, role): + def assign_role_to_group(self, session, group, role, inherited): """Assign role to group on project""" url = utils.urljoin( - self.base_path, self.id, 'groups', group.id, 'roles', role.id + self.base_path, + self.id, + 'groups', + group.id, + 'roles', + role.id, ) + if inherited: + url = utils.urljoin('OS-INHERIT', url, 'inherited_to_projects') resp = session.put( url, ) @@ -110,11 +128,13 @@ def assign_role_to_group(self, session, group, role): return True return False - def validate_group_has_role(self, session, group, role): + def validate_group_has_role(self, session, group, role, inherited): """Validates that a group has a role on a project""" url = utils.urljoin( self.base_path, self.id, 'groups', group.id, 'roles', role.id ) + if inherited: + url = utils.urljoin('OS-INHERIT', url, 'inherited_to_projects') resp = session.head( url, ) @@ -122,11 +142,13 @@ def validate_group_has_role(self, session, group, role): return True return False - def unassign_role_from_group(self, session, group, role): + def unassign_role_from_group(self, session, group, role, inherited): """Unassigns a role from a group on a project""" url = utils.urljoin( self.base_path, self.id, 'groups', group.id, 'roles', role.id ) + if inherited: + url = utils.urljoin('OS-INHERIT', url, 'inherited_to_projects') resp = session.delete( url, ) diff --git a/openstack/tests/unit/cloud/test_role_assignment.py b/openstack/tests/unit/cloud/test_role_assignment.py index ffc78fac9..2d9626f39 100644 --- a/openstack/tests/unit/cloud/test_role_assignment.py +++ b/openstack/tests/unit/cloud/test_role_assignment.py @@ -19,6 +19,8 @@ class TestRoleAssignment(base.TestCase): + IS_INHERITED = False + def _build_role_assignment_response( self, role_id, scope_type, scope_id, entity_type, entity_id ): @@ -119,7 +121,13 @@ def get_mock_url( append=None, base_url_append='v3', qs_elements=None, + inherited=False, ): + if inherited: + base_url_append = base_url_append + '/OS-INHERIT' + if append and inherited: + append.append('inherited_to_projects') + return super().get_mock_url( service_type, interface, @@ -318,6 +326,7 @@ def test_grant_role_user_id_project(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=404, @@ -333,6 +342,7 @@ def test_grant_role_user_id_project(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), status_code=200, ), @@ -345,6 +355,7 @@ def test_grant_role_user_id_project(self): self.role_data.role_name, user=self.user_data.user_id, project=self.project_data.project_id, + inherited=self.IS_INHERITED, ) ) self.assert_calls() @@ -370,6 +381,7 @@ def test_grant_role_user_name_project(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=404, @@ -385,6 +397,7 @@ def test_grant_role_user_name_project(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), status_code=200, ), @@ -397,6 +410,7 @@ def test_grant_role_user_name_project(self): self.role_data.role_name, user=self.user_data.name, project=self.project_data.project_id, + inherited=self.IS_INHERITED, ) ) @@ -419,6 +433,7 @@ def test_grant_role_user_id_project_exists(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=204, @@ -432,6 +447,7 @@ def test_grant_role_user_id_project_exists(self): self.role_data.role_id, user=self.user_data.user_id, project=self.project_data.project_id, + inherited=self.IS_INHERITED, ) ) self.assert_calls() @@ -457,6 +473,7 @@ def test_grant_role_user_name_project_exists(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=204, @@ -470,6 +487,7 @@ def test_grant_role_user_name_project_exists(self): self.role_data.role_name, user=self.user_data.name, project=self.project_data.project_id, + inherited=self.IS_INHERITED, ) ) self.assert_calls() @@ -494,6 +512,7 @@ def test_grant_role_group_id_project(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=404, @@ -509,6 +528,7 @@ def test_grant_role_group_id_project(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), status_code=200, ), @@ -521,6 +541,7 @@ def test_grant_role_group_id_project(self): self.role_data.role_name, group=self.group_data.group_id, project=self.project_data.project_id, + inherited=self.IS_INHERITED, ) ) self.assert_calls() @@ -546,6 +567,7 @@ def test_grant_role_group_name_project(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=404, @@ -561,6 +583,7 @@ def test_grant_role_group_name_project(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), status_code=200, ), @@ -573,6 +596,7 @@ def test_grant_role_group_name_project(self): self.role_data.role_name, group=self.group_data.group_name, project=self.project_data.project_id, + inherited=self.IS_INHERITED, ) ) self.assert_calls() @@ -596,6 +620,7 @@ def test_grant_role_group_id_project_exists(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=204, @@ -609,6 +634,7 @@ def test_grant_role_group_id_project_exists(self): self.role_data.role_id, group=self.group_data.group_id, project=self.project_data.project_id, + inherited=self.IS_INHERITED, ) ) self.assert_calls() @@ -634,6 +660,7 @@ def test_grant_role_group_name_project_exists(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=204, @@ -647,6 +674,7 @@ def test_grant_role_group_name_project_exists(self): self.role_data.role_name, group=self.group_data.group_name, project=self.project_data.project_id, + inherited=self.IS_INHERITED, ) ) self.assert_calls() @@ -672,6 +700,7 @@ def test_grant_role_user_id_domain(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=404, @@ -687,6 +716,7 @@ def test_grant_role_user_id_domain(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), status_code=200, ), @@ -699,6 +729,7 @@ def test_grant_role_user_id_domain(self): self.role_data.role_name, user=self.user_data.user_id, domain=self.domain_data.domain_id, + inherited=self.IS_INHERITED, ) ) self.assert_calls() @@ -724,6 +755,7 @@ def test_grant_role_user_name_domain(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=404, @@ -739,6 +771,7 @@ def test_grant_role_user_name_domain(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), status_code=200, ), @@ -751,6 +784,7 @@ def test_grant_role_user_name_domain(self): self.role_data.role_name, user=self.user_data.name, domain=self.domain_data.domain_id, + inherited=self.IS_INHERITED, ) ) @@ -773,6 +807,7 @@ def test_grant_role_user_id_domain_exists(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=204, @@ -786,6 +821,7 @@ def test_grant_role_user_id_domain_exists(self): self.role_data.role_id, user=self.user_data.user_id, domain=self.domain_data.domain_id, + inherited=self.IS_INHERITED, ) ) self.assert_calls() @@ -811,6 +847,7 @@ def test_grant_role_user_name_domain_exists(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=204, @@ -824,6 +861,7 @@ def test_grant_role_user_name_domain_exists(self): self.role_data.role_name, user=self.user_data.name, domain=self.domain_data.domain_id, + inherited=self.IS_INHERITED, ) ) self.assert_calls() @@ -848,6 +886,7 @@ def test_grant_role_group_id_domain(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=404, @@ -863,6 +902,7 @@ def test_grant_role_group_id_domain(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), status_code=200, ), @@ -875,6 +915,7 @@ def test_grant_role_group_id_domain(self): self.role_data.role_name, group=self.group_data.group_id, domain=self.domain_data.domain_id, + inherited=self.IS_INHERITED, ) ) self.assert_calls() @@ -900,6 +941,7 @@ def test_grant_role_group_name_domain(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=404, @@ -915,6 +957,7 @@ def test_grant_role_group_name_domain(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), status_code=200, ), @@ -927,6 +970,7 @@ def test_grant_role_group_name_domain(self): self.role_data.role_name, group=self.group_data.group_name, domain=self.domain_data.domain_id, + inherited=self.IS_INHERITED, ) ) self.assert_calls() @@ -950,6 +994,7 @@ def test_grant_role_group_id_domain_exists(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=204, @@ -963,6 +1008,7 @@ def test_grant_role_group_id_domain_exists(self): self.role_data.role_id, group=self.group_data.group_id, domain=self.domain_data.domain_id, + inherited=self.IS_INHERITED, ) ) self.assert_calls() @@ -988,6 +1034,7 @@ def test_grant_role_group_name_domain_exists(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=204, @@ -1001,6 +1048,7 @@ def test_grant_role_group_name_domain_exists(self): self.role_data.role_name, group=self.group_data.group_name, domain=self.domain_data.domain_id, + inherited=self.IS_INHERITED, ) ) self.assert_calls() @@ -1026,6 +1074,7 @@ def test_revoke_role_user_id_project(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=204, @@ -1041,6 +1090,7 @@ def test_revoke_role_user_id_project(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), status_code=200, ), @@ -1053,6 +1103,7 @@ def test_revoke_role_user_id_project(self): self.role_data.role_name, user=self.user_data.user_id, project=self.project_data.project_id, + inherited=self.IS_INHERITED, ) ) self.assert_calls() @@ -1078,6 +1129,7 @@ def test_revoke_role_user_name_project(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=204, @@ -1093,6 +1145,7 @@ def test_revoke_role_user_name_project(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), status_code=200, ), @@ -1105,6 +1158,7 @@ def test_revoke_role_user_name_project(self): self.role_data.role_name, user=self.user_data.name, project=self.project_data.project_id, + inherited=self.IS_INHERITED, ) ) @@ -1127,6 +1181,7 @@ def test_revoke_role_user_id_project_not_exists(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=404, @@ -1140,6 +1195,7 @@ def test_revoke_role_user_id_project_not_exists(self): self.role_data.role_id, user=self.user_data.user_id, project=self.project_data.project_id, + inherited=self.IS_INHERITED, ) ) self.assert_calls() @@ -1165,6 +1221,7 @@ def test_revoke_role_user_name_project_not_exists(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=404, @@ -1178,6 +1235,7 @@ def test_revoke_role_user_name_project_not_exists(self): self.role_data.role_name, user=self.user_data.name, project=self.project_data.project_id, + inherited=self.IS_INHERITED, ) ) self.assert_calls() @@ -1202,6 +1260,7 @@ def test_revoke_role_group_id_project(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=204, @@ -1217,6 +1276,7 @@ def test_revoke_role_group_id_project(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), status_code=200, ), @@ -1229,6 +1289,7 @@ def test_revoke_role_group_id_project(self): self.role_data.role_name, group=self.group_data.group_id, project=self.project_data.project_id, + inherited=self.IS_INHERITED, ) ) self.assert_calls() @@ -1254,6 +1315,7 @@ def test_revoke_role_group_name_project(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=204, @@ -1269,6 +1331,7 @@ def test_revoke_role_group_name_project(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), status_code=200, ), @@ -1281,6 +1344,7 @@ def test_revoke_role_group_name_project(self): self.role_data.role_name, group=self.group_data.group_name, project=self.project_data.project_id, + inherited=self.IS_INHERITED, ) ) self.assert_calls() @@ -1304,6 +1368,7 @@ def test_revoke_role_group_id_project_not_exists(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=404, @@ -1317,6 +1382,7 @@ def test_revoke_role_group_id_project_not_exists(self): self.role_data.role_id, group=self.group_data.group_id, project=self.project_data.project_id, + inherited=self.IS_INHERITED, ) ) self.assert_calls() @@ -1342,6 +1408,7 @@ def test_revoke_role_group_name_project_not_exists(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=404, @@ -1355,6 +1422,7 @@ def test_revoke_role_group_name_project_not_exists(self): self.role_data.role_name, group=self.group_data.group_name, project=self.project_data.project_id, + inherited=self.IS_INHERITED, ) ) self.assert_calls() @@ -1380,6 +1448,7 @@ def test_revoke_role_user_id_domain(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=204, @@ -1395,6 +1464,7 @@ def test_revoke_role_user_id_domain(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), status_code=200, ), @@ -1407,6 +1477,7 @@ def test_revoke_role_user_id_domain(self): self.role_data.role_name, user=self.user_data.user_id, domain=self.domain_data.domain_id, + inherited=self.IS_INHERITED, ) ) self.assert_calls() @@ -1432,6 +1503,7 @@ def test_revoke_role_user_name_domain(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=204, @@ -1447,6 +1519,7 @@ def test_revoke_role_user_name_domain(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), status_code=200, ), @@ -1459,6 +1532,7 @@ def test_revoke_role_user_name_domain(self): self.role_data.role_name, user=self.user_data.name, domain=self.domain_data.domain_id, + inherited=self.IS_INHERITED, ) ) @@ -1481,6 +1555,7 @@ def test_revoke_role_user_id_domain_not_exists(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=404, @@ -1494,6 +1569,7 @@ def test_revoke_role_user_id_domain_not_exists(self): self.role_data.role_id, user=self.user_data.user_id, domain=self.domain_data.domain_id, + inherited=self.IS_INHERITED, ) ) self.assert_calls() @@ -1519,6 +1595,7 @@ def test_revoke_role_user_name_domain_not_exists(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=404, @@ -1532,6 +1609,7 @@ def test_revoke_role_user_name_domain_not_exists(self): self.role_data.role_name, user=self.user_data.name, domain=self.domain_data.domain_id, + inherited=self.IS_INHERITED, ) ) self.assert_calls() @@ -1556,6 +1634,7 @@ def test_revoke_role_group_id_domain(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=204, @@ -1571,6 +1650,7 @@ def test_revoke_role_group_id_domain(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), status_code=200, ), @@ -1583,6 +1663,7 @@ def test_revoke_role_group_id_domain(self): self.role_data.role_name, group=self.group_data.group_id, domain=self.domain_data.domain_id, + inherited=self.IS_INHERITED, ) ) self.assert_calls() @@ -1608,6 +1689,7 @@ def test_revoke_role_group_name_domain(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=204, @@ -1623,6 +1705,7 @@ def test_revoke_role_group_name_domain(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), status_code=200, ), @@ -1635,6 +1718,7 @@ def test_revoke_role_group_name_domain(self): self.role_data.role_name, group=self.group_data.group_name, domain=self.domain_data.domain_id, + inherited=self.IS_INHERITED, ) ) self.assert_calls() @@ -1658,6 +1742,7 @@ def test_revoke_role_group_id_domain_not_exists(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=404, @@ -1671,6 +1756,7 @@ def test_revoke_role_group_id_domain_not_exists(self): self.role_data.role_id, group=self.group_data.group_id, domain=self.domain_data.domain_id, + inherited=self.IS_INHERITED, ) ) self.assert_calls() @@ -1696,6 +1782,7 @@ def test_revoke_role_group_name_domain_not_exists(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=404, @@ -1709,6 +1796,7 @@ def test_revoke_role_group_name_domain_not_exists(self): self.role_data.role_name, group=self.group_data.group_name, domain=self.domain_data.domain_id, + inherited=self.IS_INHERITED, ) ) self.assert_calls() @@ -1747,6 +1835,7 @@ def test_grant_no_role(self): self.role_data.role_name, group=self.group_data.group_name, domain=self.domain_data.domain_name, + inherited=self.IS_INHERITED, ) self.assert_calls() @@ -1784,6 +1873,7 @@ def test_revoke_no_role(self): self.role_data.role_name, group=self.group_data.group_name, domain=self.domain_data.domain_name, + inherited=self.IS_INHERITED, ) self.assert_calls() @@ -1796,7 +1886,10 @@ def test_grant_no_user_or_group_specified(self): exceptions.SDKException, 'Must specify either a user or a group', ): - self.cloud.grant_role(self.role_data.role_name) + self.cloud.grant_role( + self.role_data.role_name, + inherited=self.IS_INHERITED, + ) self.assert_calls() def test_revoke_no_user_or_group_specified(self): @@ -1808,7 +1901,10 @@ def test_revoke_no_user_or_group_specified(self): exceptions.SDKException, 'Must specify either a user or a group', ): - self.cloud.revoke_role(self.role_data.role_name) + self.cloud.revoke_role( + self.role_data.role_name, + inherited=self.IS_INHERITED, + ) self.assert_calls() def test_grant_no_user_or_group(self): @@ -1825,7 +1921,9 @@ def test_grant_no_user_or_group(self): 'Must specify either a user or a group', ): self.cloud.grant_role( - self.role_data.role_name, user=self.user_data.name + self.role_data.role_name, + user=self.user_data.name, + inherited=self.IS_INHERITED, ) self.assert_calls() @@ -1843,7 +1941,9 @@ def test_revoke_no_user_or_group(self): 'Must specify either a user or a group', ): self.cloud.revoke_role( - self.role_data.role_name, user=self.user_data.name + self.role_data.role_name, + user=self.user_data.name, + inherited=self.IS_INHERITED, ) self.assert_calls() @@ -1867,6 +1967,7 @@ def test_grant_both_user_and_group(self): self.role_data.role_name, user=self.user_data.name, group=self.group_data.group_name, + inherited=self.IS_INHERITED, ) self.assert_calls() @@ -1890,6 +1991,7 @@ def test_revoke_both_user_and_group(self): self.role_data.role_name, user=self.user_data.name, group=self.group_data.group_name, + inherited=self.IS_INHERITED, ) def test_grant_both_project_and_domain(self): @@ -1917,6 +2019,7 @@ def test_grant_both_project_and_domain(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=404, @@ -1932,6 +2035,7 @@ def test_grant_both_project_and_domain(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), status_code=200, ), @@ -1945,6 +2049,7 @@ def test_grant_both_project_and_domain(self): user=self.user_data.name, project=self.project_data.project_name, domain=self.domain_data.domain_name, + inherited=self.IS_INHERITED, ) ) @@ -1973,6 +2078,7 @@ def test_revoke_both_project_and_domain(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), complete_qs=True, status_code=204, @@ -1988,6 +2094,7 @@ def test_revoke_both_project_and_domain(self): 'roles', self.role_data.role_id, ], + inherited=self.IS_INHERITED, ), status_code=200, ), @@ -2001,6 +2108,7 @@ def test_revoke_both_project_and_domain(self): user=self.user_data.name, project=self.project_data.project_name, domain=self.domain_data.domain_name, + inherited=self.IS_INHERITED, ) ) @@ -2019,7 +2127,9 @@ def test_grant_no_project_or_domain(self): 'Must specify either a domain, project or system', ): self.cloud.grant_role( - self.role_data.role_name, user=self.user_data.name + self.role_data.role_name, + user=self.user_data.name, + inherited=self.IS_INHERITED, ) self.assert_calls() @@ -2038,7 +2148,9 @@ def test_revoke_no_project_or_domain_or_system(self): 'Must specify either a domain, project or system', ): self.cloud.revoke_role( - self.role_data.role_name, user=self.user_data.name + self.role_data.role_name, + user=self.user_data.name, + inherited=self.IS_INHERITED, ) self.assert_calls() @@ -2066,6 +2178,7 @@ def test_grant_bad_domain_exception(self): self.role_data.role_name, user=self.user_data.name, domain='baddomain', + inherited=self.IS_INHERITED, ) self.assert_calls() @@ -2093,5 +2206,10 @@ def test_revoke_bad_domain_exception(self): self.role_data.role_name, user=self.user_data.name, domain='baddomain', + inherited=self.IS_INHERITED, ) self.assert_calls() + + +class TestInheritedRoleAssignment(TestRoleAssignment): + IS_INHERITED = True diff --git a/openstack/tests/unit/identity/v3/test_domain.py b/openstack/tests/unit/identity/v3/test_domain.py index baea9560c..cb0d7d1d9 100644 --- a/openstack/tests/unit/identity/v3/test_domain.py +++ b/openstack/tests/unit/identity/v3/test_domain.py @@ -84,12 +84,27 @@ def test_assign_role_to_user_good(self): self.assertTrue( sot.assign_role_to_user( - self.sess, user.User(id='1'), role.Role(id='2') + self.sess, user.User(id='1'), role.Role(id='2'), False ) ) self.sess.put.assert_called_with('domains/IDENTIFIER/users/1/roles/2') + def test_assign_inherited_role_to_user_good(self): + sot = domain.Domain(**EXAMPLE) + resp = self.good_resp + self.sess.put = mock.Mock(return_value=resp) + + self.assertTrue( + sot.assign_role_to_user( + self.sess, user.User(id='1'), role.Role(id='2'), True + ) + ) + + self.sess.put.assert_called_with( + 'OS-INHERIT/domains/IDENTIFIER/users/1/roles/2/inherited_to_projects' + ) + def test_assign_role_to_user_bad(self): sot = domain.Domain(**EXAMPLE) resp = self.bad_resp @@ -97,7 +112,7 @@ def test_assign_role_to_user_bad(self): self.assertFalse( sot.assign_role_to_user( - self.sess, user.User(id='1'), role.Role(id='2') + self.sess, user.User(id='1'), role.Role(id='2'), False ) ) @@ -108,12 +123,27 @@ def test_validate_user_has_role_good(self): self.assertTrue( sot.validate_user_has_role( - self.sess, user.User(id='1'), role.Role(id='2') + self.sess, user.User(id='1'), role.Role(id='2'), False ) ) self.sess.head.assert_called_with('domains/IDENTIFIER/users/1/roles/2') + def test_validate_user_has_inherited_role_good(self): + sot = domain.Domain(**EXAMPLE) + resp = self.good_resp + self.sess.head = mock.Mock(return_value=resp) + + self.assertTrue( + sot.validate_user_has_role( + self.sess, user.User(id='1'), role.Role(id='2'), True + ) + ) + + self.sess.head.assert_called_with( + 'OS-INHERIT/domains/IDENTIFIER/users/1/roles/2/inherited_to_projects' + ) + def test_validate_user_has_role_bad(self): sot = domain.Domain(**EXAMPLE) resp = self.bad_resp @@ -121,7 +151,7 @@ def test_validate_user_has_role_bad(self): self.assertFalse( sot.validate_user_has_role( - self.sess, user.User(id='1'), role.Role(id='2') + self.sess, user.User(id='1'), role.Role(id='2'), False ) ) @@ -132,7 +162,7 @@ def test_unassign_role_from_user_good(self): self.assertTrue( sot.unassign_role_from_user( - self.sess, user.User(id='1'), role.Role(id='2') + self.sess, user.User(id='1'), role.Role(id='2'), False ) ) @@ -140,6 +170,21 @@ def test_unassign_role_from_user_good(self): 'domains/IDENTIFIER/users/1/roles/2' ) + def test_unassign_inherited_role_from_user_good(self): + sot = domain.Domain(**EXAMPLE) + resp = self.good_resp + self.sess.delete = mock.Mock(return_value=resp) + + self.assertTrue( + sot.unassign_role_from_user( + self.sess, user.User(id='1'), role.Role(id='2'), True + ) + ) + + self.sess.delete.assert_called_with( + 'OS-INHERIT/domains/IDENTIFIER/users/1/roles/2/inherited_to_projects' + ) + def test_unassign_role_from_user_bad(self): sot = domain.Domain(**EXAMPLE) resp = self.bad_resp @@ -147,7 +192,7 @@ def test_unassign_role_from_user_bad(self): self.assertFalse( sot.unassign_role_from_user( - self.sess, user.User(id='1'), role.Role(id='2') + self.sess, user.User(id='1'), role.Role(id='2'), False ) ) @@ -158,12 +203,27 @@ def test_assign_role_to_group_good(self): self.assertTrue( sot.assign_role_to_group( - self.sess, group.Group(id='1'), role.Role(id='2') + self.sess, group.Group(id='1'), role.Role(id='2'), False ) ) self.sess.put.assert_called_with('domains/IDENTIFIER/groups/1/roles/2') + def test_assign_inherited_role_to_group_good(self): + sot = domain.Domain(**EXAMPLE) + resp = self.good_resp + self.sess.put = mock.Mock(return_value=resp) + + self.assertTrue( + sot.assign_role_to_group( + self.sess, group.Group(id='1'), role.Role(id='2'), True + ) + ) + + self.sess.put.assert_called_with( + 'OS-INHERIT/domains/IDENTIFIER/groups/1/roles/2/inherited_to_projects' + ) + def test_assign_role_to_group_bad(self): sot = domain.Domain(**EXAMPLE) resp = self.bad_resp @@ -171,7 +231,7 @@ def test_assign_role_to_group_bad(self): self.assertFalse( sot.assign_role_to_group( - self.sess, group.Group(id='1'), role.Role(id='2') + self.sess, group.Group(id='1'), role.Role(id='2'), False ) ) @@ -182,7 +242,7 @@ def test_validate_group_has_role_good(self): self.assertTrue( sot.validate_group_has_role( - self.sess, group.Group(id='1'), role.Role(id='2') + self.sess, group.Group(id='1'), role.Role(id='2'), False ) ) @@ -190,6 +250,21 @@ def test_validate_group_has_role_good(self): 'domains/IDENTIFIER/groups/1/roles/2' ) + def test_validate_group_has_inherited_role_good(self): + sot = domain.Domain(**EXAMPLE) + resp = self.good_resp + self.sess.head = mock.Mock(return_value=resp) + + self.assertTrue( + sot.validate_group_has_role( + self.sess, group.Group(id='1'), role.Role(id='2'), True + ) + ) + + self.sess.head.assert_called_with( + 'OS-INHERIT/domains/IDENTIFIER/groups/1/roles/2/inherited_to_projects' + ) + def test_validate_group_has_role_bad(self): sot = domain.Domain(**EXAMPLE) resp = self.bad_resp @@ -197,7 +272,7 @@ def test_validate_group_has_role_bad(self): self.assertFalse( sot.validate_group_has_role( - self.sess, group.Group(id='1'), role.Role(id='2') + self.sess, group.Group(id='1'), role.Role(id='2'), False ) ) @@ -208,7 +283,7 @@ def test_unassign_role_from_group_good(self): self.assertTrue( sot.unassign_role_from_group( - self.sess, group.Group(id='1'), role.Role(id='2') + self.sess, group.Group(id='1'), role.Role(id='2'), False ) ) @@ -216,6 +291,21 @@ def test_unassign_role_from_group_good(self): 'domains/IDENTIFIER/groups/1/roles/2' ) + def test_unassign_inherited_role_from_group_good(self): + sot = domain.Domain(**EXAMPLE) + resp = self.good_resp + self.sess.delete = mock.Mock(return_value=resp) + + self.assertTrue( + sot.unassign_role_from_group( + self.sess, group.Group(id='1'), role.Role(id='2'), True + ) + ) + + self.sess.delete.assert_called_with( + 'OS-INHERIT/domains/IDENTIFIER/groups/1/roles/2/inherited_to_projects' + ) + def test_unassign_role_from_group_bad(self): sot = domain.Domain(**EXAMPLE) resp = self.bad_resp @@ -223,6 +313,6 @@ def test_unassign_role_from_group_bad(self): self.assertFalse( sot.unassign_role_from_group( - self.sess, group.Group(id='1'), role.Role(id='2') + self.sess, group.Group(id='1'), role.Role(id='2'), False ) ) diff --git a/openstack/tests/unit/identity/v3/test_project.py b/openstack/tests/unit/identity/v3/test_project.py index 965700951..e57e003df 100644 --- a/openstack/tests/unit/identity/v3/test_project.py +++ b/openstack/tests/unit/identity/v3/test_project.py @@ -97,12 +97,27 @@ def test_assign_role_to_user_good(self): self.assertTrue( sot.assign_role_to_user( - self.sess, user.User(id='1'), role.Role(id='2') + self.sess, user.User(id='1'), role.Role(id='2'), False ) ) self.sess.put.assert_called_with('projects/IDENTIFIER/users/1/roles/2') + def test_assign_inherited_role_to_user_good(self): + sot = project.Project(**EXAMPLE) + resp = self.good_resp + self.sess.put = mock.Mock(return_value=resp) + + self.assertTrue( + sot.assign_role_to_user( + self.sess, user.User(id='1'), role.Role(id='2'), True + ) + ) + + self.sess.put.assert_called_with( + 'OS-INHERIT/projects/IDENTIFIER/users/1/roles/2/inherited_to_projects' + ) + def test_assign_role_to_user_bad(self): sot = project.Project(**EXAMPLE) resp = self.bad_resp @@ -110,7 +125,7 @@ def test_assign_role_to_user_bad(self): self.assertFalse( sot.assign_role_to_user( - self.sess, user.User(id='1'), role.Role(id='2') + self.sess, user.User(id='1'), role.Role(id='2'), False ) ) @@ -121,7 +136,7 @@ def test_validate_user_has_role_good(self): self.assertTrue( sot.validate_user_has_role( - self.sess, user.User(id='1'), role.Role(id='2') + self.sess, user.User(id='1'), role.Role(id='2'), False ) ) @@ -129,6 +144,21 @@ def test_validate_user_has_role_good(self): 'projects/IDENTIFIER/users/1/roles/2' ) + def test_validate_user_has_inherited_role_good(self): + sot = project.Project(**EXAMPLE) + resp = self.good_resp + self.sess.head = mock.Mock(return_value=resp) + + self.assertTrue( + sot.validate_user_has_role( + self.sess, user.User(id='1'), role.Role(id='2'), True + ) + ) + + self.sess.head.assert_called_with( + 'OS-INHERIT/projects/IDENTIFIER/users/1/roles/2/inherited_to_projects' + ) + def test_validate_user_has_role_bad(self): sot = project.Project(**EXAMPLE) resp = self.bad_resp @@ -136,7 +166,7 @@ def test_validate_user_has_role_bad(self): self.assertFalse( sot.validate_user_has_role( - self.sess, user.User(id='1'), role.Role(id='2') + self.sess, user.User(id='1'), role.Role(id='2'), False ) ) @@ -147,7 +177,7 @@ def test_unassign_role_from_user_good(self): self.assertTrue( sot.unassign_role_from_user( - self.sess, user.User(id='1'), role.Role(id='2') + self.sess, user.User(id='1'), role.Role(id='2'), False ) ) @@ -155,6 +185,21 @@ def test_unassign_role_from_user_good(self): 'projects/IDENTIFIER/users/1/roles/2' ) + def test_unassign_inherited_role_from_user_good(self): + sot = project.Project(**EXAMPLE) + resp = self.good_resp + self.sess.delete = mock.Mock(return_value=resp) + + self.assertTrue( + sot.unassign_role_from_user( + self.sess, user.User(id='1'), role.Role(id='2'), True + ) + ) + + self.sess.delete.assert_called_with( + 'OS-INHERIT/projects/IDENTIFIER/users/1/roles/2/inherited_to_projects' + ) + def test_unassign_role_from_user_bad(self): sot = project.Project(**EXAMPLE) resp = self.bad_resp @@ -162,7 +207,7 @@ def test_unassign_role_from_user_bad(self): self.assertFalse( sot.unassign_role_from_user( - self.sess, user.User(id='1'), role.Role(id='2') + self.sess, user.User(id='1'), role.Role(id='2'), False ) ) @@ -173,7 +218,7 @@ def test_assign_role_to_group_good(self): self.assertTrue( sot.assign_role_to_group( - self.sess, group.Group(id='1'), role.Role(id='2') + self.sess, group.Group(id='1'), role.Role(id='2'), False ) ) @@ -181,6 +226,21 @@ def test_assign_role_to_group_good(self): 'projects/IDENTIFIER/groups/1/roles/2' ) + def test_assign_inherited_role_to_group_good(self): + sot = project.Project(**EXAMPLE) + resp = self.good_resp + self.sess.put = mock.Mock(return_value=resp) + + self.assertTrue( + sot.assign_role_to_group( + self.sess, group.Group(id='1'), role.Role(id='2'), True + ) + ) + + self.sess.put.assert_called_with( + 'OS-INHERIT/projects/IDENTIFIER/groups/1/roles/2/inherited_to_projects' + ) + def test_assign_role_to_group_bad(self): sot = project.Project(**EXAMPLE) resp = self.bad_resp @@ -188,7 +248,7 @@ def test_assign_role_to_group_bad(self): self.assertFalse( sot.assign_role_to_group( - self.sess, group.Group(id='1'), role.Role(id='2') + self.sess, group.Group(id='1'), role.Role(id='2'), False ) ) @@ -199,7 +259,7 @@ def test_validate_group_has_role_good(self): self.assertTrue( sot.validate_group_has_role( - self.sess, group.Group(id='1'), role.Role(id='2') + self.sess, group.Group(id='1'), role.Role(id='2'), False ) ) @@ -207,6 +267,21 @@ def test_validate_group_has_role_good(self): 'projects/IDENTIFIER/groups/1/roles/2' ) + def test_validate_group_has_inherited_role_good(self): + sot = project.Project(**EXAMPLE) + resp = self.good_resp + self.sess.head = mock.Mock(return_value=resp) + + self.assertTrue( + sot.validate_group_has_role( + self.sess, group.Group(id='1'), role.Role(id='2'), True + ) + ) + + self.sess.head.assert_called_with( + 'OS-INHERIT/projects/IDENTIFIER/groups/1/roles/2/inherited_to_projects' + ) + def test_validate_group_has_role_bad(self): sot = project.Project(**EXAMPLE) resp = self.bad_resp @@ -214,7 +289,7 @@ def test_validate_group_has_role_bad(self): self.assertFalse( sot.validate_group_has_role( - self.sess, group.Group(id='1'), role.Role(id='2') + self.sess, group.Group(id='1'), role.Role(id='2'), False ) ) @@ -225,7 +300,7 @@ def test_unassign_role_from_group_good(self): self.assertTrue( sot.unassign_role_from_group( - self.sess, group.Group(id='1'), role.Role(id='2') + self.sess, group.Group(id='1'), role.Role(id='2'), False ) ) @@ -233,6 +308,21 @@ def test_unassign_role_from_group_good(self): 'projects/IDENTIFIER/groups/1/roles/2' ) + def test_unassign_inherited_role_from_group_good(self): + sot = project.Project(**EXAMPLE) + resp = self.good_resp + self.sess.delete = mock.Mock(return_value=resp) + + self.assertTrue( + sot.unassign_role_from_group( + self.sess, group.Group(id='1'), role.Role(id='2'), True + ) + ) + + self.sess.delete.assert_called_with( + 'OS-INHERIT/projects/IDENTIFIER/groups/1/roles/2/inherited_to_projects' + ) + def test_unassign_role_from_group_bad(self): sot = project.Project(**EXAMPLE) resp = self.bad_resp @@ -240,7 +330,7 @@ def test_unassign_role_from_group_bad(self): self.assertFalse( sot.unassign_role_from_group( - self.sess, group.Group(id='1'), role.Role(id='2') + self.sess, group.Group(id='1'), role.Role(id='2'), False ) ) diff --git a/openstack/tests/unit/identity/v3/test_proxy.py b/openstack/tests/unit/identity/v3/test_proxy.py index 3f5c28252..8e8e5bd15 100644 --- a/openstack/tests/unit/identity/v3/test_proxy.py +++ b/openstack/tests/unit/identity/v3/test_proxy.py @@ -495,6 +495,7 @@ def test_assign_domain_role_to_user(self): self.proxy, self.proxy._get_resource(user.User, 'uid'), self.proxy._get_resource(role.Role, 'rid'), + False, ], ) @@ -508,6 +509,7 @@ def test_unassign_domain_role_from_user(self): self.proxy, self.proxy._get_resource(user.User, 'uid'), self.proxy._get_resource(role.Role, 'rid'), + False, ], ) @@ -521,6 +523,7 @@ def test_validate_user_has_domain_role(self): self.proxy, self.proxy._get_resource(user.User, 'uid'), self.proxy._get_resource(role.Role, 'rid'), + False, ], ) @@ -534,6 +537,7 @@ def test_assign_domain_role_to_group(self): self.proxy, self.proxy._get_resource(group.Group, 'uid'), self.proxy._get_resource(role.Role, 'rid'), + False, ], ) @@ -547,6 +551,7 @@ def test_unassign_domain_role_from_group(self): self.proxy, self.proxy._get_resource(group.Group, 'uid'), self.proxy._get_resource(role.Role, 'rid'), + False, ], ) @@ -560,6 +565,7 @@ def test_validate_group_has_domain_role(self): self.proxy, self.proxy._get_resource(group.Group, 'uid'), self.proxy._get_resource(role.Role, 'rid'), + False, ], ) @@ -573,6 +579,7 @@ def test_assign_project_role_to_user(self): self.proxy, self.proxy._get_resource(user.User, 'uid'), self.proxy._get_resource(role.Role, 'rid'), + False, ], ) @@ -586,6 +593,7 @@ def test_unassign_project_role_from_user(self): self.proxy, self.proxy._get_resource(user.User, 'uid'), self.proxy._get_resource(role.Role, 'rid'), + False, ], ) @@ -599,6 +607,7 @@ def test_validate_user_has_project_role(self): self.proxy, self.proxy._get_resource(user.User, 'uid'), self.proxy._get_resource(role.Role, 'rid'), + False, ], ) @@ -612,6 +621,7 @@ def test_assign_project_role_to_group(self): self.proxy, self.proxy._get_resource(group.Group, 'uid'), self.proxy._get_resource(role.Role, 'rid'), + False, ], ) @@ -625,6 +635,7 @@ def test_unassign_project_role_from_group(self): self.proxy, self.proxy._get_resource(group.Group, 'uid'), self.proxy._get_resource(role.Role, 'rid'), + False, ], ) @@ -638,6 +649,7 @@ def test_validate_group_has_project_role(self): self.proxy, self.proxy._get_resource(group.Group, 'uid'), self.proxy._get_resource(role.Role, 'rid'), + False, ], ) diff --git a/releasenotes/notes/identity-cloud-mixin-inherited-roles-ed66bb78ddeca2c9.yaml b/releasenotes/notes/identity-cloud-mixin-inherited-roles-ed66bb78ddeca2c9.yaml new file mode 100644 index 000000000..0af650b87 --- /dev/null +++ b/releasenotes/notes/identity-cloud-mixin-inherited-roles-ed66bb78ddeca2c9.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add support for granting inherited roles. + Roles assignments can be added to a user or group + on the system, a domain, or a project. From b07720cbd868d4a4f15f9b76c66d373f2a0e5c31 Mon Sep 17 00:00:00 2001 From: Thobias Salazar Trevisan Date: Thu, 24 Oct 2024 18:15:05 -0300 Subject: [PATCH 3589/3836] Fix missing 'f' prefix in exception message Issue: In the OpenStackConfig class, within the _validate_config_file method, the exception message raised when a configuration file is empty or not a valid mapping was missing the 'f' prefix required for string interpolation. Change-Id: Iba0bfe7e5e63280e5789de13fa195e046070e6fa --- openstack/config/loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 01f35f3df..4566f052b 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -477,7 +477,7 @@ def _validate_config_file(self, path: str, data: ty.Any) -> bool: """ if not isinstance(data, dict): raise exceptions.ConfigException( - 'Configuration file {path} is empty or not a valid mapping' + f'Configuration file {path} is empty or not a valid mapping' ) if 'clouds' not in data: From 1621b490b94a7b95dd9cbc5bae85ec3fae21b0e8 Mon Sep 17 00:00:00 2001 From: "Dr. Jens Harbott" Date: Tue, 5 Nov 2024 16:16:04 +0100 Subject: [PATCH 3590/3836] Skip disabling compute service The openstack.tests.functional.compute.v2.test_service.TestService.test_update test seems to sometimes cause following tests to fail until the compute service has fully recovered. Change-Id: I69e27afd67dd4f578283a3eafa946bc9aa80bfed --- openstack/tests/functional/compute/v2/test_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openstack/tests/functional/compute/v2/test_service.py b/openstack/tests/functional/compute/v2/test_service.py index 271d21cf4..70d3941dc 100644 --- a/openstack/tests/functional/compute/v2/test_service.py +++ b/openstack/tests/functional/compute/v2/test_service.py @@ -30,6 +30,7 @@ def test_disable_enable(self): self.conn.compute.enable_service(srv) def test_update(self): + self.skipTest("Test is breaking tests that follow") for srv in self.conn.compute.services(): if srv.name == 'nova-compute': self.conn.compute.update_service_forced_down( From 7005482787c3478589889d4e1457f569ac398ae3 Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Sat, 19 Oct 2024 14:43:47 +0000 Subject: [PATCH 3591/3836] Add method to create network resource tags Depends-On: https://review.opendev.org/c/openstack/neutron/+/924724 Related-Bug: #2073836 Change-Id: I920595d36352f778b6488dfa3fe5d6278c0e7ec6 --- openstack/network/v2/_base.py | 20 +++++++++++++++++++ openstack/network/v2/_proxy.py | 13 ++++++++++++ openstack/network/v2/floating_ip.py | 6 +++--- openstack/network/v2/network.py | 6 +++--- openstack/network/v2/port.py | 5 ++--- openstack/network/v2/router.py | 6 +++--- openstack/network/v2/security_group.py | 6 +++--- openstack/network/v2/security_group_rule.py | 6 +++--- openstack/network/v2/subnet.py | 6 +++--- openstack/network/v2/subnet_pool.py | 7 ++++--- .../tests/functional/network/v2/common.py | 18 +++++++++++++++++ ...k-create-tags-method-ccb37b01ed52a58c.yaml | 5 +++++ 12 files changed, 80 insertions(+), 24 deletions(-) create mode 100644 releasenotes/notes/network-create-tags-method-ccb37b01ed52a58c.yaml diff --git a/openstack/network/v2/_base.py b/openstack/network/v2/_base.py index e55b900ec..8282cd694 100644 --- a/openstack/network/v2/_base.py +++ b/openstack/network/v2/_base.py @@ -9,7 +9,11 @@ # 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.common import tag +from openstack import exceptions from openstack import resource +from openstack import utils class NetworkResource(resource.Resource): @@ -39,3 +43,19 @@ def _prepare_request( if if_revision is not None: req.headers['If-Match'] = "revision_number=%d" % if_revision return req + + +class TagMixinNetwork(tag.TagMixin): + def add_tags(self, session, tags): + """Create the tags on the resource + + :param session: The session to use for making this request. + :param tags: List with tags to be set on the resource + """ + tags = tags or [] + url = utils.urljoin(self.base_path, self.id, 'tags') + session = self._get_session(session) + response = session.post(url, json={'tags': tags}) + exceptions.raise_from_response(response) + self._body.attributes.update({'tags': tags}) + return self diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 1ea397046..bf3150c2c 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -5433,6 +5433,19 @@ def set_tags(self, resource, tags): self._check_tag_support(resource) return resource.set_tags(self, tags) + def add_tags(self, resource, tags): + """Add tags to a specified resource + + :param resource: :class:`~openstack.resource.Resource` instance. + :param tags: New tags to be set. + :type tags: "list" + + :returns: The updated resource + :rtype: :class:`~openstack.resource.Resource` + """ + self._check_tag_support(resource) + return resource.add_tags(self, tags) + def add_tag(self, resource, tag): """Add one single tag to a specified resource diff --git a/openstack/network/v2/floating_ip.py b/openstack/network/v2/floating_ip.py index cb75e7a47..562ce763e 100644 --- a/openstack/network/v2/floating_ip.py +++ b/openstack/network/v2/floating_ip.py @@ -9,12 +9,12 @@ # 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.common import tag + from openstack.network.v2 import _base from openstack import resource -class FloatingIP(_base.NetworkResource, tag.TagMixin): +class FloatingIP(_base.NetworkResource, _base.TagMixinNetwork): name_attribute = "floating_ip_address" resource_name = "floating ip" resource_key = 'floatingip' @@ -43,7 +43,7 @@ class FloatingIP(_base.NetworkResource, tag.TagMixin): 'sort_key', 'sort_dir', tenant_id='project_id', - **tag.TagMixin._tag_query_parameters, + **_base.TagMixinNetwork._tag_query_parameters, ) # Properties diff --git a/openstack/network/v2/network.py b/openstack/network/v2/network.py index a894668b0..12e0e4b21 100644 --- a/openstack/network/v2/network.py +++ b/openstack/network/v2/network.py @@ -9,12 +9,12 @@ # 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.common import tag + from openstack.network.v2 import _base from openstack import resource -class Network(_base.NetworkResource, tag.TagMixin): +class Network(_base.NetworkResource, _base.TagMixinNetwork): resource_key = 'network' resources_key = 'networks' base_path = '/networks' @@ -43,7 +43,7 @@ class Network(_base.NetworkResource, tag.TagMixin): provider_network_type='provider:network_type', provider_physical_network='provider:physical_network', provider_segmentation_id='provider:segmentation_id', - **tag.TagMixin._tag_query_parameters, + **_base.TagMixinNetwork._tag_query_parameters, ) # Properties diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index ffecae3b2..a49fa4088 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -10,12 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.common import tag from openstack.network.v2 import _base from openstack import resource -class Port(_base.NetworkResource, tag.TagMixin): +class Port(_base.NetworkResource, _base.TagMixinNetwork): resource_key = 'port' resources_key = 'ports' base_path = '/ports' @@ -53,7 +52,7 @@ class Port(_base.NetworkResource, tag.TagMixin): is_admin_state_up='admin_state_up', is_port_security_enabled='port_security_enabled', security_group_ids='security_groups', - **tag.TagMixin._tag_query_parameters, + **_base.TagMixinNetwork._tag_query_parameters, ) # Properties diff --git a/openstack/network/v2/router.py b/openstack/network/v2/router.py index 41aba3018..42a84bbfa 100644 --- a/openstack/network/v2/router.py +++ b/openstack/network/v2/router.py @@ -9,14 +9,14 @@ # 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.common import tag + from openstack import exceptions from openstack.network.v2 import _base from openstack import resource from openstack import utils -class Router(_base.NetworkResource, tag.TagMixin): +class Router(_base.NetworkResource, _base.TagMixinNetwork): resource_key = 'router' resources_key = 'routers' base_path = '/routers' @@ -40,7 +40,7 @@ class Router(_base.NetworkResource, tag.TagMixin): is_admin_state_up='admin_state_up', is_distributed='distributed', is_ha='ha', - **tag.TagMixin._tag_query_parameters, + **_base.TagMixinNetwork._tag_query_parameters, ) # Properties diff --git a/openstack/network/v2/security_group.py b/openstack/network/v2/security_group.py index 9e1797104..425878793 100644 --- a/openstack/network/v2/security_group.py +++ b/openstack/network/v2/security_group.py @@ -9,12 +9,12 @@ # 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.common import tag + from openstack.network.v2 import _base from openstack import resource -class SecurityGroup(_base.NetworkResource, tag.TagMixin): +class SecurityGroup(_base.NetworkResource, _base.TagMixinNetwork): resource_key = 'security_group' resources_key = 'security_groups' base_path = '/security-groups' @@ -37,7 +37,7 @@ class SecurityGroup(_base.NetworkResource, tag.TagMixin): 'revision_number', 'sort_dir', 'sort_key', - **tag.TagMixin._tag_query_parameters, + **_base.TagMixinNetwork._tag_query_parameters, ) # Properties diff --git a/openstack/network/v2/security_group_rule.py b/openstack/network/v2/security_group_rule.py index ef9504d14..ba65c737c 100644 --- a/openstack/network/v2/security_group_rule.py +++ b/openstack/network/v2/security_group_rule.py @@ -9,12 +9,12 @@ # 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.common import tag + from openstack.network.v2 import _base from openstack import resource -class SecurityGroupRule(_base.NetworkResource, tag.TagMixin): +class SecurityGroupRule(_base.NetworkResource, _base.TagMixinNetwork): resource_key = 'security_group_rule' resources_key = 'security_group_rules' base_path = '/security-group-rules' @@ -43,7 +43,7 @@ class SecurityGroupRule(_base.NetworkResource, tag.TagMixin): 'sort_dir', 'sort_key', ether_type='ethertype', - **tag.TagMixin._tag_query_parameters, + **_base.TagMixinNetwork._tag_query_parameters, ) # Properties diff --git a/openstack/network/v2/subnet.py b/openstack/network/v2/subnet.py index 5acc22744..959e29592 100644 --- a/openstack/network/v2/subnet.py +++ b/openstack/network/v2/subnet.py @@ -9,12 +9,12 @@ # 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.common import tag + from openstack.network.v2 import _base from openstack import resource -class Subnet(_base.NetworkResource, tag.TagMixin): +class Subnet(_base.NetworkResource, _base.TagMixinNetwork): resource_key = 'subnet' resources_key = 'subnets' base_path = '/subnets' @@ -44,7 +44,7 @@ class Subnet(_base.NetworkResource, tag.TagMixin): is_dhcp_enabled='enable_dhcp', subnet_pool_id='subnetpool_id', use_default_subnet_pool='use_default_subnetpool', - **tag.TagMixin._tag_query_parameters, + **_base.TagMixinNetwork._tag_query_parameters, ) # Properties diff --git a/openstack/network/v2/subnet_pool.py b/openstack/network/v2/subnet_pool.py index 5da9a4c94..f87248046 100644 --- a/openstack/network/v2/subnet_pool.py +++ b/openstack/network/v2/subnet_pool.py @@ -9,11 +9,12 @@ # 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.common import tag + +from openstack.network.v2 import _base from openstack import resource -class SubnetPool(resource.Resource, tag.TagMixin): +class SubnetPool(resource.Resource, _base.TagMixinNetwork): resource_key = 'subnetpool' resources_key = 'subnetpools' base_path = '/subnetpools' @@ -37,7 +38,7 @@ class SubnetPool(resource.Resource, tag.TagMixin): 'sort_key', 'sort_dir', is_shared='shared', - **tag.TagMixin._tag_query_parameters, + **_base.TagMixinNetwork._tag_query_parameters, ) # Properties diff --git a/openstack/tests/functional/network/v2/common.py b/openstack/tests/functional/network/v2/common.py index e958c3f9b..8908d524e 100644 --- a/openstack/tests/functional/network/v2/common.py +++ b/openstack/tests/functional/network/v2/common.py @@ -88,3 +88,21 @@ def test_remove_all_tags(self): self.user_cloud.network.remove_all_tags(sot) sot = self.get_command(self.ID) self.assertEqual([], sot.tags) + + def test_add_tags(self): + sot = self.get_command(self.ID) + self.assertEqual([], sot.tags) + + self.user_cloud.network.add_tags(sot, ["red", "green"]) + self.user_cloud.network.add_tags(sot, ["blue", "yellow"]) + sot = self.get_command(self.ID) + self.assertEqual(["blue", "green", "red", "yellow"], sot.tags) + + # The operation is idempotent. + self.user_cloud.network.add_tags(sot, ["blue", "yellow"]) + sot = self.get_command(self.ID) + self.assertEqual(["blue", "green", "red", "yellow"], sot.tags) + + self.user_cloud.network.add_tags(sot, []) + sot = self.get_command(self.ID) + self.assertEqual(["blue", "green", "red", "yellow"], sot.tags) diff --git a/releasenotes/notes/network-create-tags-method-ccb37b01ed52a58c.yaml b/releasenotes/notes/network-create-tags-method-ccb37b01ed52a58c.yaml new file mode 100644 index 000000000..ff48f14ed --- /dev/null +++ b/releasenotes/notes/network-create-tags-method-ccb37b01ed52a58c.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added a method to create (POST) tags in the network resources. This method + is idempotent. From 677192bf82319380d8b5eff34f2ca6470f2fad9d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 6 Nov 2024 10:38:36 +0000 Subject: [PATCH 3592/3836] identity: Add functional tests for projects Change-Id: Ic5248c57ac2e83a522426e9a2dc4a6b8ceb89d67 Signed-off-by: Stephen Finucane --- .../functional/identity/v3/test_projects.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 openstack/tests/functional/identity/v3/test_projects.py diff --git a/openstack/tests/functional/identity/v3/test_projects.py b/openstack/tests/functional/identity/v3/test_projects.py new file mode 100644 index 000000000..80225d6d0 --- /dev/null +++ b/openstack/tests/functional/identity/v3/test_projects.py @@ -0,0 +1,79 @@ +# 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.identity.v3 import project as _project +from openstack.tests.functional import base + + +class TestProject(base.BaseFunctionalTest): + def setUp(self): + super().setUp() + + self.project_name = self.getUniqueString('project') + self.project_description = self.getUniqueString('project') + + def _delete_project(self, project): + ret = self.operator_cloud.identity.delete_project(project) + self.assertIsNone(ret) + + def test_project(self): + # create the project + + project = self.operator_cloud.identity.create_project( + name=self.project_name, + ) + self.assertIsInstance(project, _project.Project) + self.assertEqual('', project.description) + self.addCleanup(self._delete_project, project) + + # update the project + + project = self.operator_cloud.identity.update_project( + project, description=self.project_description + ) + self.assertIsInstance(project, _project.Project) + self.assertEqual(self.project_description, project.description) + + # retrieve details of the (updated) project by ID + + project = self.operator_cloud.identity.get_project(project.id) + self.assertIsInstance(project, _project.Project) + self.assertEqual(self.project_description, project.description) + + # retrieve details of the (updated) project by name + + project = self.operator_cloud.identity.find_project(project.name) + self.assertIsInstance(project, _project.Project) + self.assertEqual(self.project_description, project.description) + + # list all projects + + projects = list(self.operator_cloud.identity.projects()) + self.assertIsInstance(projects[0], _project.Project) + self.assertIn( + self.project_name, + {x.name for x in projects}, + ) + + def test_user_project(self): + # list all user projects + + user_projects = list( + self.operator_cloud.identity.user_projects( + self.operator_cloud.current_user_id + ) + ) + self.assertIsInstance(user_projects[0], _project.UserProject) + self.assertIn( + self.operator_cloud.current_project_id, + {x.id for x in user_projects}, + ) From 30153433a94922ec20878429b0d5cfe76a44fddc Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 8 May 2024 17:41:30 +0100 Subject: [PATCH 3593/3836] compute: Add additional migration parameters Change-Id: I94f6f3ffe36326e3c0531a6618ee72c5f56b7901 Signed-off-by: Stephen Finucane --- openstack/compute/v2/_proxy.py | 6 +++++- openstack/tests/unit/compute/v2/test_proxy.py | 12 +++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index d32becf40..5dac96bcd 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -2110,6 +2110,7 @@ def live_migrate_server( host=None, force=False, block_migration=None, + disk_over_commit=None, ): """Live migrate a server from one host to target host @@ -2128,14 +2129,17 @@ def live_migrate_server( Some clouds are too old to support 'auto', in which case a ValueError will be thrown. If omitted, the value will be 'auto' on clouds that support it, and False on clouds that do not. + :param disk_over_commit: Whether to allow disk over-commit on the + destination host. (Optional) :returns: None """ server = self._get_resource(_server.Server, server) server.live_migrate( self, - host, + host=host, force=force, block_migration=block_migration, + disk_over_commit=disk_over_commit, ) def abort_server_migration( diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 5049939bf..09cf2a50e 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -1447,9 +1447,15 @@ def test_live_migrate_server(self): self._verify( 'openstack.compute.v2.server.Server.live_migrate', self.proxy.live_migrate_server, - method_args=["value", "host1", False], - expected_args=[self.proxy, "host1"], - expected_kwargs={'force': False, 'block_migration': None}, + method_args=["value"], + method_kwargs={'host': 'host1', 'force': False}, + expected_args=[self.proxy], + expected_kwargs={ + 'host': 'host1', + 'force': False, + 'block_migration': None, + 'disk_over_commit': None, + }, ) def test_abort_server_migration(self): From 1453d56c99a37bc39f8761fadfa7c7a968ff19c8 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 19 Jul 2024 16:46:37 +0100 Subject: [PATCH 3594/3836] block storage: Add user_id query param for Volume We also combine two unit test classes since the existing layout was resulting in unnecessary duplicated tests being run. Change-Id: I2cfd8593b4b231469d2a0684d20dc3a2fd88290b Signed-off-by: Stephen Finucane --- openstack/block_storage/v3/volume.py | 1 + .../unit/block_storage/v3/test_volume.py | 25 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index 17ccdc9df..9b463f796 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -27,6 +27,7 @@ class Volume(resource.Resource, metadata.MetadataMixin): _query_mapping = resource.QueryParameters( 'name', 'status', + 'user_id', 'project_id', 'created_at', 'updated_at', diff --git a/openstack/tests/unit/block_storage/v3/test_volume.py b/openstack/tests/unit/block_storage/v3/test_volume.py index 8aaa4d09e..89ac68037 100644 --- a/openstack/tests/unit/block_storage/v3/test_volume.py +++ b/openstack/tests/unit/block_storage/v3/test_volume.py @@ -69,6 +69,17 @@ class TestVolume(base.TestCase): + def setUp(self): + super().setUp() + self.resp = mock.Mock() + self.resp.body = None + self.resp.status_code = 200 + self.resp.json = mock.Mock(return_value=self.resp.body) + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.default_microversion = '3.60' + self.sess.post = mock.Mock(return_value=self.resp) + self.sess._get_connection = mock.Mock(return_value=self.cloud) + def test_basic(self): sot = volume.Volume(VOLUME) self.assertEqual("volume", sot.resource_key) @@ -85,6 +96,7 @@ def test_basic(self): "name": "name", "status": "status", "all_projects": "all_tenants", + "user_id": "user_id", "project_id": "project_id", "created_at": "created_at", "updated_at": "updated_at", @@ -141,19 +153,6 @@ def test_create(self): VOLUME["OS-SCH-HNT:scheduler_hints"], sot.scheduler_hints ) - -class TestVolumeActions(TestVolume): - def setUp(self): - super().setUp() - self.resp = mock.Mock() - self.resp.body = None - self.resp.status_code = 200 - self.resp.json = mock.Mock(return_value=self.resp.body) - self.sess = mock.Mock(spec=adapter.Adapter) - self.sess.default_microversion = '3.60' - self.sess.post = mock.Mock(return_value=self.resp) - self.sess._get_connection = mock.Mock(return_value=self.cloud) - def test_extend(self): sot = volume.Volume(**VOLUME) From b268388602c8312c0621f673ccd5cf9d1279d0e3 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 21 Mar 2024 14:27:13 +0000 Subject: [PATCH 3595/3836] doc: Add multiple examples of authentication configuration This duplicates keystoneauth docs somewhat, but it's useful to have as a quick reference. (psst: the credentials inline are from a throwaway DevStack cloud and don't reference anything useful anymore) Change-Id: I2c62194df2af260cb7b0e6f35cf7f599304e63a5 Signed-off-by: Stephen Finucane --- doc/source/user/config/configuration.rst | 112 +++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/doc/source/user/config/configuration.rst b/doc/source/user/config/configuration.rst index febee1486..6f8525bde 100644 --- a/doc/source/user/config/configuration.rst +++ b/doc/source/user/config/configuration.rst @@ -512,3 +512,115 @@ In the above example, the ``project_id`` configuration values will be ignored in favor of the ``project_name`` configuration values, and the higher-level project will be chosen over the auth-specified project. So the actual project used will be ```myfavoriteproject```. + + +Examples +-------- + +``auth`` +~~~~~~~~ + +.. rubric:: Password-based authentication (domain-scoped) + +.. code-block:: yaml + + example: + auth: + auth_url: http://example.com/identity + password: password + project_domain_id: default + project_name: admin + user_domain_id: default + username: admin + region_name: RegionOne + +.. rubric:: Password-based authentication (trust-scoped) + +.. code-block:: yaml + + example-trust: + auth: + auth_url: http://example.com/identity + password: password + username: admin + trust_id: 95946f9eef864fdc993079d8fe3e5747 + region_name: RegionOne + +.. rubric:: Password-based authentication (system-scoped) + +.. code-block:: yaml + + example-system: + auth: + auth_url: http://example.com/identity + password: password + system_scope: all + username: admin + region_name: RegionOne + +.. rubric:: Application credential-based authentication + +.. code-block:: yaml + + example-appcred: + auth: + auth_url: http://example.com/identity + application_credential_id: 9da0a8da3d394d09bf49dfc27014d254 + application_credential_secret: pKfDSvUOFwO2t2_XxCajAFhzCKAVHI7yfqPb6xjshVDnMUHF7ifju8gMdhHTI4Eo56UP_hEc8ssmgA1NNtKMpA + auth_type: v3applicationcredential + region_name: RegionOne + +.. rubric:: Token-based authentication + +.. code-block:: yaml + + example-token: + auth: + auth_url: http://example.com/identity + token: gAAAAABl32ptw2PN6L9JyBeO16PwQU1SrdMUvUz8Eon7LC2PFItdGRWFpOkK0qwH3JkukTuEM5qbYK9ucowRXET1RBMjZlfVpUa8Nz3qjQdzXw7pBKH4w1e4tekvDCOKfn15ZoujBOvdGqgtpW-febVGaW9oJzf6R3WTMDxWz3YRJjmiOBpwcN8 + project_id: 1fd93a4455c74d2ea94b929fc5f0e488 + auth_type: v3token + region_name: RegionOne + +.. note:: + + This is a toy example: by their very definition token's are short-lived. + You are unlikely to store them in a `clouds.yaml` file. + Instead, you would likely pass the TOTP token via the command line + (``--os-token``) or as an environment variable (``OS_TOKEN``). + +.. rubric:: TOTP-based authentication + +.. code-block:: yaml + + example-totp: + auth: + auth_url: http://example.com/identity + passcode: password + project_domain_id: default + project_name: admin + user_domain_id: default + username: admin + auth_type: v3totp + region_name: RegionOne + +.. note:: + + This is a toy example: by their very definition TOTP token's are + short-lived. You are unlikely to store them in a `clouds.yaml` file. + Instead, you would likely pass the TOTP token via the command line + (``--os-passcode``) or as an environment variable (``OS_PASSCODE``). + +.. rubric:: OAuth1-based authentication + +.. code-block:: yaml + + example-oauth: + auth: + auth_url: http://example.com/identity + consumer_key: foo + consumer_secret: secret + access_key: bar + access_secret: secret + auth_type: v3oauth1 + region_name: RegionOne From adf152f972502161ebac3f3f8cb0dbd0e34f4cf2 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 5 Nov 2024 17:33:38 +0000 Subject: [PATCH 3596/3836] identity: Add support for endpoint projects This is an extension. We require it to support migration to OSC. No functional tests are added yet since we don't currently have the ability to associate a project with an endpoint. https://docs.openstack.org/api-ref/identity/v3-ext/index.html#create-association Change-Id: I599ff3e88d4e1e9ffafc638bb74186f2739b5a77 Signed-off-by: Stephen Finucane --- doc/source/user/proxies/identity_v3.rst | 2 +- openstack/identity/v3/_proxy.py | 17 +++++++++++++++++ openstack/identity/v3/project.py | 19 +++++++++++++++++-- .../tests/unit/identity/v3/test_proxy.py | 9 +++++++++ 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/doc/source/user/proxies/identity_v3.rst b/doc/source/user/proxies/identity_v3.rst index 64a87f5b6..a848866e7 100644 --- a/doc/source/user/proxies/identity_v3.rst +++ b/doc/source/user/proxies/identity_v3.rst @@ -67,7 +67,7 @@ Project Operations .. autoclass:: openstack.identity.v3._proxy.Proxy :noindex: :members: create_project, update_project, delete_project, get_project, - find_project, projects, user_projects + find_project, projects, user_projects, endpoint_projects Service Operations ^^^^^^^^^^^^^^^^^^ diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 48c061e19..99a65557a 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -719,6 +719,23 @@ def user_projects(self, user, **query): user = self._get_resource(_user.User, user) return self._list(_project.UserProject, user_id=user.id, **query) + def endpoint_projects(self, endpoint, **query): + """Retrieve a generator of projects which are associated with the + endpoint. + + :param endpoint: Either the endpoint ID or an instance of + :class:`~openstack.identity.v3.endpoint.Endpoint` + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. + + :returns: A generator of project instances. + :rtype: :class:`~openstack.identity.v3.project.EndpointProject` + """ + endpoint_id = self._get_resource(_endpoint.Endpoint, endpoint).id + return self._list( + _project.EndpointProject, endpoint_id=endpoint_id, **query + ) + def update_project(self, project, **attrs): """Update a project diff --git a/openstack/identity/v3/project.py b/openstack/identity/v3/project.py index 677ce35d7..93af1153a 100644 --- a/openstack/identity/v3/project.py +++ b/openstack/identity/v3/project.py @@ -136,10 +136,25 @@ def unassign_role_from_group(self, session, group, role): class UserProject(Project): - resource_key = 'project' - resources_key = 'projects' base_path = '/users/%(user_id)s/projects' + #: The ID for the user from the URI of the resource + user_id = resource.URI('user_id') + + # capabilities + allow_create = False + allow_fetch = False + allow_commit = False + allow_delete = False + allow_list = True + + +class EndpointProject(Project): + base_path = '/OS-EP-FILTER/endpoints/%(endpoint_id)s/projects' + + #: The ID for the endpoint from the URI of the resource + endpoint_id = resource.URI('endpoint_id') + # capabilities allow_create = False allow_fetch = False diff --git a/openstack/tests/unit/identity/v3/test_proxy.py b/openstack/tests/unit/identity/v3/test_proxy.py index 3f5c28252..2a6825500 100644 --- a/openstack/tests/unit/identity/v3/test_proxy.py +++ b/openstack/tests/unit/identity/v3/test_proxy.py @@ -36,6 +36,7 @@ from openstack.tests.unit import test_proxy_base USER_ID = 'user-id-' + uuid.uuid4().hex +ENDPOINT_ID = 'user-id-' + uuid.uuid4().hex class TestIdentityProxyBase(test_proxy_base.TestProxyBase): @@ -302,6 +303,14 @@ def test_user_projects(self): expected_kwargs={'user_id': USER_ID}, ) + def test_endpoint_projects(self): + self.verify_list( + self.proxy.endpoint_projects, + project.EndpointProject, + method_kwargs={'endpoint': ENDPOINT_ID}, + expected_kwargs={'endpoint_id': ENDPOINT_ID}, + ) + def test_project_update(self): self.verify_update(self.proxy.update_project, project.Project) From 30d41a4b3f1eb003a5f6c8d1975c8bceed39a5f2 Mon Sep 17 00:00:00 2001 From: Michael Still Date: Sun, 10 Nov 2024 18:38:02 +1100 Subject: [PATCH 3597/3836] Timing tests should use a threshold. The stats tests were using an exact value for a timing statistic, which results in flakey results. Instead, let's set a small maximum threshold. Change-Id: I0be868daf3edbc6570585968a573e6e167c6bd1c --- openstack/tests/unit/test_stats.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/openstack/tests/unit/test_stats.py b/openstack/tests/unit/test_stats.py index 6a466b1bb..a9871fab4 100644 --- a/openstack/tests/unit/test_stats.py +++ b/openstack/tests/unit/test_stats.py @@ -102,7 +102,7 @@ def assert_reported_stat(self, key, value=None, kind=None): Check statsd return values. A ``value`` should specify a ``kind``, however a ``kind`` may be specified without a - ``value`` for a generic match. Leave both empy to just check + ``value`` for a generic match. Leave both empty to just check for key presence. :arg str key: The statsd key @@ -113,6 +113,9 @@ def assert_reported_stat(self, key, value=None, kind=None): - ``g`` gauge - ``ms`` timing - ``s`` set + + Note that for ``ms`` type, you are expressing a maximum value, + not an exact value. This is to avoid flakey tests. """ self.assertIsNotNone(self.statsd) @@ -149,9 +152,9 @@ def assert_reported_stat(self, key, value=None, kind=None): # timing results into float of indeterminate # length, hence foiling string matching. if kind == 'ms': - if float(value) == float(s_value): + if float(value) >= float(s_value): return True - if value == s_value: + elif value == s_value: return True # otherwise keep looking for other matches continue @@ -255,7 +258,7 @@ def test_servers(self): ) self.assert_reported_stat( 'openstack.api.compute.GET.servers_detail.200', - value='0', + value='5', kind='ms', ) self.assert_prometheus_stat( @@ -290,7 +293,7 @@ def test_servers_no_detail(self): 'openstack.api.compute.GET.servers.200', value='1', kind='c' ) self.assert_reported_stat( - 'openstack.api.compute.GET.servers.200', value='0', kind='ms' + 'openstack.api.compute.GET.servers.200', value='5', kind='ms' ) self.assert_reported_stat( 'openstack.api.compute.GET.servers.attempted', value='1', kind='c' @@ -320,7 +323,7 @@ def test_servers_error(self): 'openstack.api.compute.GET.servers.500', value='1', kind='c' ) self.assert_reported_stat( - 'openstack.api.compute.GET.servers.500', value='0', kind='ms' + 'openstack.api.compute.GET.servers.500', value='5', kind='ms' ) self.assert_reported_stat( 'openstack.api.compute.GET.servers.attempted', value='1', kind='c' From 33b5bfac571801029fd8baeaeb6f8e87dae44729 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 12 Nov 2024 14:12:37 +0000 Subject: [PATCH 3598/3836] pre-commit: Bump versions Change-Id: Ifc2d76bf9e9de41a49f8d79887d31efb7a7ddb0e Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8b85fead9..e2241a53c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ --- repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: mixed-line-ending @@ -19,12 +19,12 @@ repos: hooks: - id: doc8 - repo: https://github.com/asottile/pyupgrade - rev: v3.17.0 + rev: v3.19.0 hooks: - id: pyupgrade args: ['--py38-plus'] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.7 + rev: v0.7.3 hooks: - id: ruff args: ['--fix'] @@ -37,7 +37,7 @@ repos: - flake8-import-order~=0.18.2 exclude: '^(doc|releasenotes|tools)/.*$' - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.11.2 + rev: v1.13.0 hooks: - id: mypy additional_dependencies: From 399dfcc0e59124fde68376b77089b20485f6566f Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 12 Sep 2024 17:49:32 +0100 Subject: [PATCH 3599/3836] pre-commit: Migrate pyupgrade to ruff-format openstack/tests/unit/cloud/test_stack.py needs some manual fixes but this is otherwise auto-generated. Change-Id: If0d202ece232181c16ee4990eb428e9ad6e91cd5 Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 7 +- examples/compute/create.py | 8 +- openstack/__main__.py | 5 +- openstack/accelerator/v2/deployable.py | 2 +- openstack/baremetal/configdrive.py | 9 +- openstack/baremetal/v1/_proxy.py | 9 +- openstack/baremetal/v1/allocation.py | 5 +- openstack/baremetal/v1/driver.py | 4 +- openstack/baremetal/v1/node.py | 123 +++---- .../v1/introspection.py | 9 +- openstack/block_storage/_base_proxy.py | 4 +- openstack/block_storage/v2/backup.py | 2 +- openstack/block_storage/v3/backup.py | 2 +- openstack/cloud/_baremetal.py | 27 +- openstack/cloud/_block_storage.py | 22 +- openstack/cloud/_coe.py | 4 +- openstack/cloud/_compute.py | 55 ++- openstack/cloud/_dns.py | 15 +- openstack/cloud/_identity.py | 22 +- openstack/cloud/_image.py | 9 +- openstack/cloud/_network.py | 72 ++-- openstack/cloud/_network_common.py | 110 +++--- openstack/cloud/_object_store.py | 28 +- openstack/cloud/_utils.py | 20 +- openstack/cloud/exc.py | 4 +- openstack/cloud/meta.py | 18 +- openstack/cloud/openstackcloud.py | 16 +- openstack/compute/v2/server.py | 30 +- openstack/compute/v2/server_group.py | 2 +- openstack/config/cloud_region.py | 64 ++-- openstack/config/loader.py | 47 ++- openstack/config/vendors/__init__.py | 8 +- openstack/exceptions.py | 23 +- openstack/format.py | 4 +- openstack/image/_download.py | 4 +- openstack/image/v2/_proxy.py | 24 +- openstack/key_manager/v1/_format.py | 2 +- openstack/message/v2/message.py | 2 +- openstack/network/v2/_proxy.py | 9 +- openstack/object_store/v1/_proxy.py | 34 +- .../orchestration/util/environment_format.py | 2 +- openstack/orchestration/util/event_utils.py | 2 +- .../orchestration/util/template_utils.py | 9 +- openstack/orchestration/util/utils.py | 2 +- openstack/orchestration/v1/_proxy.py | 2 +- openstack/orchestration/v1/stack.py | 13 +- openstack/proxy.py | 27 +- openstack/resource.py | 73 ++-- openstack/service_description.py | 32 +- openstack/shared_file_system/v2/_proxy.py | 2 +- openstack/test/fakes.py | 10 +- openstack/tests/base.py | 14 +- openstack/tests/fakes.py | 20 +- .../baremetal/test_baremetal_driver.py | 8 +- openstack/tests/functional/base.py | 12 +- .../cloud/test_cluster_templates.py | 4 +- .../tests/functional/cloud/test_compute.py | 4 +- .../tests/functional/cloud/test_recordset.py | 8 +- .../tests/functional/dns/v2/test_zone.py | 4 +- .../tests/unit/baremetal/v1/test_node.py | 92 ++--- openstack/tests/unit/base.py | 23 +- .../unit/block_storage/v2/test_backup.py | 10 +- .../unit/block_storage/v2/test_snapshot.py | 2 +- .../tests/unit/block_storage/v2/test_type.py | 6 +- .../unit/block_storage/v2/test_volume.py | 42 +-- .../unit/block_storage/v3/test_attachment.py | 2 +- .../unit/block_storage/v3/test_backup.py | 10 +- .../tests/unit/block_storage/v3/test_group.py | 4 +- .../unit/block_storage/v3/test_snapshot.py | 8 +- .../unit/block_storage/v3/test_transfer.py | 4 +- .../tests/unit/block_storage/v3/test_type.py | 6 +- .../unit/block_storage/v3/test_volume.py | 66 ++-- openstack/tests/unit/cloud/test__utils.py | 12 +- .../unit/cloud/test_availability_zones.py | 12 +- .../tests/unit/cloud/test_baremetal_node.py | 6 +- .../tests/unit/cloud/test_baremetal_ports.py | 12 +- openstack/tests/unit/cloud/test_endpoints.py | 2 +- openstack/tests/unit/cloud/test_flavors.py | 84 ++--- .../unit/cloud/test_floating_ip_neutron.py | 2 +- .../tests/unit/cloud/test_floating_ip_pool.py | 4 +- openstack/tests/unit/cloud/test_fwaas.py | 4 +- .../tests/unit/cloud/test_identity_roles.py | 4 +- openstack/tests/unit/cloud/test_image.py | 172 +++------- .../tests/unit/cloud/test_image_snapshot.py | 10 +- openstack/tests/unit/cloud/test_meta.py | 24 +- openstack/tests/unit/cloud/test_network.py | 6 +- openstack/tests/unit/cloud/test_object.py | 312 ++++------------- openstack/tests/unit/cloud/test_operator.py | 4 +- openstack/tests/unit/cloud/test_port.py | 2 +- openstack/tests/unit/cloud/test_project.py | 6 +- .../cloud/test_qos_bandwidth_limit_rule.py | 12 +- .../unit/cloud/test_qos_dscp_marking_rule.py | 10 +- .../cloud/test_qos_minimum_bandwidth_rule.py | 10 +- openstack/tests/unit/cloud/test_qos_policy.py | 6 +- openstack/tests/unit/cloud/test_router.py | 8 +- .../tests/unit/cloud/test_security_groups.py | 108 ++---- .../tests/unit/cloud/test_server_console.py | 16 +- openstack/tests/unit/cloud/test_stack.py | 323 ++++-------------- openstack/tests/unit/cloud/test_subnet.py | 22 +- .../tests/unit/cloud/test_update_server.py | 4 +- openstack/tests/unit/cloud/test_users.py | 6 +- openstack/tests/unit/cloud/test_volume.py | 10 +- .../tests/unit/clustering/v1/test_cluster.py | 28 +- .../tests/unit/clustering/v1/test_node.py | 8 +- .../unit/clustering/v1/test_profile_type.py | 2 +- openstack/tests/unit/config/test_from_conf.py | 4 +- .../tests/unit/database/v1/test_instance.py | 10 +- openstack/tests/unit/image/v2/test_image.py | 4 +- .../unit/key_manager/v1/test_container.py | 2 +- .../tests/unit/key_manager/v1/test_order.py | 4 +- .../tests/unit/key_manager/v1/test_secret.py | 2 +- openstack/tests/unit/message/v2/test_queue.py | 12 +- .../tests/unit/network/v2/test_bgp_speaker.py | 2 +- .../unit/object_store/v1/test_container.py | 4 +- .../tests/unit/object_store/v1/test_proxy.py | 18 +- .../tests/unit/orchestration/v1/test_stack.py | 6 +- .../unit/shared_file_system/v2/test_share.py | 2 +- openstack/tests/unit/test_connection.py | 68 ++-- openstack/tests/unit/test_proxy.py | 2 +- openstack/tests/unit/test_stats.py | 2 +- openstack/utils.py | 14 +- openstack/workflow/v2/workflow.py | 2 +- pyproject.toml | 3 + tools/nova_version.py | 2 +- tools/print-services.py | 4 +- 125 files changed, 967 insertions(+), 1838 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e2241a53c..291b9cf51 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,16 +18,11 @@ repos: rev: v1.1.2 hooks: - id: doc8 - - repo: https://github.com/asottile/pyupgrade - rev: v3.19.0 - hooks: - - id: pyupgrade - args: ['--py38-plus'] - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.7.3 hooks: - id: ruff - args: ['--fix'] + args: ['--fix', '--unsafe-fixes'] - id: ruff-format - repo: https://opendev.org/openstack/hacking rev: 7.0.0 diff --git a/examples/compute/create.py b/examples/compute/create.py index d4250efec..dc68af1ae 100644 --- a/examples/compute/create.py +++ b/examples/compute/create.py @@ -46,7 +46,7 @@ def create_keypair(conn): raise e with open(PRIVATE_KEYPAIR_FILE, 'w') as f: - f.write("%s" % keypair.private_key) + f.write(str(keypair.private_key)) os.chmod(PRIVATE_KEYPAIR_FILE, 0o400) @@ -71,8 +71,4 @@ def create_server(conn): server = conn.compute.wait_for_server(server) - print( - "ssh -i {key} root@{ip}".format( - key=PRIVATE_KEYPAIR_FILE, ip=server.access_ipv4 - ) - ) + print(f"ssh -i {PRIVATE_KEYPAIR_FILE} root@{server.access_ipv4}") diff --git a/openstack/__main__.py b/openstack/__main__.py index cf3fcb34f..631429cd4 100644 --- a/openstack/__main__.py +++ b/openstack/__main__.py @@ -20,8 +20,9 @@ def show_version(args): print( - "OpenstackSDK Version %s" - % pbr.version.VersionInfo('openstacksdk').version_string_with_vcs() + "OpenstackSDK Version {}".format( + pbr.version.VersionInfo('openstacksdk').version_string_with_vcs() + ) ) diff --git a/openstack/accelerator/v2/deployable.py b/openstack/accelerator/v2/deployable.py index 27873937e..910caa3e9 100644 --- a/openstack/accelerator/v2/deployable.py +++ b/openstack/accelerator/v2/deployable.py @@ -64,7 +64,7 @@ def _commit( call = getattr(session, method.lower()) except AttributeError: raise exceptions.ResourceFailure( - "Invalid commit method: %s" % method + f"Invalid commit method: {method}" ) request.url = request.url + "/program" diff --git a/openstack/baremetal/configdrive.py b/openstack/baremetal/configdrive.py index 24bedba82..ebfa63cdc 100644 --- a/openstack/baremetal/configdrive.py +++ b/openstack/baremetal/configdrive.py @@ -67,9 +67,7 @@ def populate_directory( # Strictly speaking, user data is binary, but in many cases # it's actually a text (cloud-init, ignition, etc). flag = 't' if isinstance(user_data, str) else 'b' - with open( - os.path.join(subdir, 'user_data'), 'w%s' % flag - ) as fp: + with open(os.path.join(subdir, 'user_data'), f'w{flag}') as fp: fp.write(user_data) yield d @@ -147,15 +145,14 @@ def pack(path: str) -> str: raise RuntimeError( 'Error generating the configdrive. Make sure the ' '"genisoimage", "mkisofs" or "xorrisofs" tool is installed. ' - 'Error: %s' % error + f'Error: {error}' ) stdout, stderr = p.communicate() if p.returncode != 0: raise RuntimeError( 'Error generating the configdrive.' - 'Stdout: "%(stdout)s". Stderr: "%(stderr)s"' - % {'stdout': stdout.decode(), 'stderr': stderr.decode()} + f'Stdout: "{stdout.decode()}". Stderr: "{stderr.decode()}"' ) tmpfile.seek(0) diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 7cb2b5f45..38907b460 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -63,9 +63,7 @@ def _get_with_fields(self, resource_type, value, fields=None): kwargs['fields'] = _common.fields_type(fields, resource_type) return res.fetch( self, - error_message="No {resource_type} found for {value}".format( - resource_type=resource_type.__name__, value=value - ), + error_message=f"No {resource_type.__name__} found for {value}", **kwargs, ) @@ -560,9 +558,8 @@ def wait_for_nodes_provision_state( try: for count in utils.iterate_timeout( timeout, - "Timeout waiting for nodes %(nodes)s to reach " - "target state '%(state)s'" - % {'nodes': log_nodes, 'state': expected_state}, + f"Timeout waiting for nodes {log_nodes} to reach " + f"target state '{expected_state}'", ): nodes = [self.get_node(n) for n in remaining] remaining = [] diff --git a/openstack/baremetal/v1/allocation.py b/openstack/baremetal/v1/allocation.py index d4f78163e..eb0dad8d9 100644 --- a/openstack/baremetal/v1/allocation.py +++ b/openstack/baremetal/v1/allocation.py @@ -93,14 +93,13 @@ def wait(self, session, timeout=None, ignore_error=False): return self for count in utils.iterate_timeout( - timeout, "Timeout waiting for the allocation %s" % self.id + timeout, f"Timeout waiting for the allocation {self.id}" ): self.fetch(session) if self.state == 'error' and not ignore_error: raise exceptions.ResourceFailure( - "Allocation %(allocation)s failed: %(error)s" - % {'allocation': self.id, 'error': self.last_error} + f"Allocation {self.id} failed: {self.last_error}" ) elif self.state != 'allocating': return self diff --git a/openstack/baremetal/v1/driver.py b/openstack/baremetal/v1/driver.py index cea8f73de..d8a9e97fe 100644 --- a/openstack/baremetal/v1/driver.py +++ b/openstack/baremetal/v1/driver.py @@ -188,8 +188,6 @@ def call_vendor_passthru( retriable_status_codes=_common.RETRIABLE_STATUS_CODES, ) - msg = "Failed call to method {method} on driver {driver_name}".format( - method=method, driver_name=self.name - ) + msg = f"Failed call to method {method} on driver {self.name}" exceptions.raise_from_response(response, error_message=msg) return response diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 06fc452ae..b7d203ae1 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -324,9 +324,8 @@ def create(self, session, *args, **kwargs): microversion = _common.STATE_VERSIONS[expected_provision_state] except KeyError: raise ValueError( - "Node's provision_state must be one of %s for creation, " - "got %s" - % ( + "Node's provision_state must be one of {} for creation, " + "got {}".format( ', '.join(_common.STATE_VERSIONS), expected_provision_state, ) @@ -334,7 +333,7 @@ def create(self, session, *args, **kwargs): else: error_message = ( "Cannot create a node with initial provision " - "state %s" % expected_provision_state + f"state {expected_provision_state}" ) # Nodes cannot be created as available using new API versions maximum = ( @@ -546,8 +545,8 @@ def set_provision_state( expected_state = _common.EXPECTED_STATES[target] except KeyError: raise ValueError( - 'For target %s the expected state is not ' - 'known, cannot wait for it' % target + f'For target {target} the expected state is not ' + 'known, cannot wait for it' ) request = self._prepare_request(requires_id=True) @@ -561,8 +560,8 @@ def set_provision_state( ) msg = ( - "Failed to set provision state for bare metal node {node} " - "to {target}".format(node=self.id, target=target) + f"Failed to set provision state for bare metal node {self.id} " + f"to {target}" ) exceptions.raise_from_response(response, error_message=msg) @@ -588,9 +587,8 @@ def wait_for_power_state(self, session, expected_state, timeout=None): """ for count in utils.iterate_timeout( timeout, - "Timeout waiting for node %(node)s to reach " - "power state '%(state)s'" - % {'node': self.id, 'state': expected_state}, + f"Timeout waiting for node {self.id} to reach " + f"power state '{expected_state}'", ): self.fetch(session) if self.power_state == expected_state: @@ -629,9 +627,8 @@ def wait_for_provision_state( """ for count in utils.iterate_timeout( timeout, - "Timeout waiting for node %(node)s to reach " - "target state '%(state)s'" - % {'node': self.id, 'state': expected_state}, + f"Timeout waiting for node {self.id} to reach " + f"target state '{expected_state}'", ): self.fetch(session) if self._check_state_reached( @@ -677,7 +674,7 @@ def wait_for_reservation(self, session, timeout=None): for count in utils.iterate_timeout( timeout, - "Timeout waiting for the lock to be released on node %s" % self.id, + f"Timeout waiting for the lock to be released on node {self.id}", ): self.fetch(session) if self.reservation is None: @@ -719,13 +716,8 @@ def _check_state_reached( or self.provision_state == 'error' ): raise exceptions.ResourceFailure( - "Node %(node)s reached failure state \"%(state)s\"; " - "the last error is %(error)s" - % { - 'node': self.id, - 'state': self.provision_state, - 'error': self.last_error, - } + f"Node {self.id} reached failure state \"{self.provision_state}\"; " + f"the last error is {self.last_error}" ) # Special case: a failure state for "manage" transition can be # "enroll" @@ -735,10 +727,9 @@ def _check_state_reached( and self.last_error ): raise exceptions.ResourceFailure( - "Node %(node)s could not reach state manageable: " + f"Node {self.id} could not reach state manageable: " "failed to verify management credentials; " - "the last error is %(error)s" - % {'node': self.id, 'error': self.last_error} + f"the last error is {self.last_error}" ) def inject_nmi(self, session): @@ -789,8 +780,8 @@ def set_power_state(self, session, target, wait=False, timeout=None): expected = _common.EXPECTED_POWER_STATES[target] except KeyError: raise ValueError( - "Cannot use target power state %s with wait, " - "the expected state is not known" % target + f"Cannot use target power state {target} with wait, " + "the expected state is not known" ) session = self._get_session(session) @@ -816,8 +807,8 @@ def set_power_state(self, session, target, wait=False, timeout=None): ) msg = ( - "Failed to set power state for bare metal node {node} " - "to {target}".format(node=self.id, target=target) + f"Failed to set power state for bare metal node {self.id} " + f"to {target}" ) exceptions.raise_from_response(response, error_message=msg) @@ -893,9 +884,7 @@ def attach_vif( retriable_status_codes=retriable_status_codes, ) - msg = "Failed to attach VIF {vif} to bare metal node {node}".format( - node=self.id, vif=vif_id - ) + msg = f"Failed to attach VIF {vif_id} to bare metal node {self.id}" exceptions.raise_from_response(response, error_message=msg) def detach_vif(self, session, vif_id, ignore_missing=True): @@ -940,9 +929,7 @@ def detach_vif(self, session, vif_id, ignore_missing=True): ) return False - msg = "Failed to detach VIF {vif} from bare metal node {node}".format( - node=self.id, vif=vif_id - ) + msg = f"Failed to detach VIF {vif_id} from bare metal node {self.id}" exceptions.raise_from_response(response, error_message=msg) return True @@ -973,9 +960,7 @@ def list_vifs(self, session): request.url, headers=request.headers, microversion=version ) - msg = "Failed to list VIFs attached to bare metal node {node}".format( - node=self.id - ) + msg = f"Failed to list VIFs attached to bare metal node {self.id}" exceptions.raise_from_response(response, error_message=msg) return [vif['id'] for vif in response.json()['vifs']] @@ -1015,8 +1000,8 @@ def validate(self, session, required=('boot', 'deploy', 'power')): if failed: raise exceptions.ValidationException( - 'Validation failed for required interfaces of node {node}:' - ' {failures}'.format( + 'Validation failed for required interfaces of node ' + '{node}: {failures}'.format( node=self.id, failures=', '.join(failed) ) ) @@ -1058,9 +1043,7 @@ def _do_maintenance_action(self, session, verb, body=None): headers=request.headers, microversion=version, ) - msg = "Failed to change maintenance mode for node {node}".format( - node=self.id - ) + msg = f"Failed to change maintenance mode for node {self.id}" exceptions.raise_from_response(response, error_message=msg) def get_boot_device(self, session): @@ -1081,9 +1064,7 @@ def get_boot_device(self, session): retriable_status_codes=_common.RETRIABLE_STATUS_CODES, ) - msg = "Failed to get boot device for node {node}".format( - node=self.id, - ) + msg = f"Failed to get boot device for node {self.id}" exceptions.raise_from_response(response, error_message=msg) return response.json() @@ -1138,9 +1119,7 @@ def get_supported_boot_devices(self, session): retriable_status_codes=_common.RETRIABLE_STATUS_CODES, ) - msg = "Failed to get supported boot devices for node {node}".format( - node=self.id, - ) + msg = f"Failed to get supported boot devices for node {self.id}" exceptions.raise_from_response(response, error_message=msg) return response.json() @@ -1164,8 +1143,8 @@ def set_boot_mode(self, session, target): request.url = utils.urljoin(request.url, 'states', 'boot_mode') if target not in ('uefi', 'bios'): raise ValueError( - "Unrecognized boot mode %s." - "Boot mode should be one of 'uefi' or 'bios'." % target + f"Unrecognized boot mode {target}." + "Boot mode should be one of 'uefi' or 'bios'." ) body = {'target': target} @@ -1200,8 +1179,8 @@ def set_secure_boot(self, session, target): request.url = utils.urljoin(request.url, 'states', 'secure_boot') if not isinstance(target, bool): raise ValueError( - "Invalid target %s. It should be True or False " - "corresponding to secure boot state 'on' or 'off'" % target + f"Invalid target {target}. It should be True or False " + "corresponding to secure boot state 'on' or 'off'" ) body = {'target': target} @@ -1213,9 +1192,7 @@ def set_secure_boot(self, session, target): retriable_status_codes=_common.RETRIABLE_STATUS_CODES, ) - msg = "Failed to change secure boot state for {node}".format( - node=self.id - ) + msg = f"Failed to change secure boot state for {self.id}" exceptions.raise_from_response(response, error_message=msg) def add_trait(self, session, trait): @@ -1237,9 +1214,7 @@ def add_trait(self, session, trait): retriable_status_codes=_common.RETRIABLE_STATUS_CODES, ) - msg = "Failed to add trait {trait} for node {node}".format( - trait=trait, node=self.id - ) + msg = f"Failed to add trait {trait} for node {self.id}" exceptions.raise_from_response(response, error_message=msg) self.traits = list(set(self.traits or ()) | {trait}) @@ -1342,10 +1317,8 @@ def call_vendor_passthru(self, session, verb, method, body=None): ) msg = ( - "Failed to call vendor_passthru for node {node}, verb {verb}" - " and method {method}".format( - node=self.id, verb=verb, method=method - ) + f"Failed to call vendor_passthru for node {self.id}, verb {verb} " + f"and method {method}" ) exceptions.raise_from_response(response, error_message=msg) @@ -1369,9 +1342,7 @@ def list_vendor_passthru(self, session): retriable_status_codes=_common.RETRIABLE_STATUS_CODES, ) - msg = "Failed to list vendor_passthru methods for node {node}".format( - node=self.id - ) + msg = f"Failed to list vendor_passthru methods for node {self.id}" exceptions.raise_from_response(response, error_message=msg) return response.json() @@ -1394,9 +1365,7 @@ def get_console(self, session): retriable_status_codes=_common.RETRIABLE_STATUS_CODES, ) - msg = "Failed to get console for node {node}".format( - node=self.id, - ) + msg = f"Failed to get console for node {self.id}" exceptions.raise_from_response(response, error_message=msg) return response.json() @@ -1414,8 +1383,8 @@ def set_console_mode(self, session, enabled): request.url = utils.urljoin(request.url, 'states', 'console') if not isinstance(enabled, bool): raise ValueError( - "Invalid enabled %s. It should be True or False " - "corresponding to console enabled or disabled" % enabled + f"Invalid enabled {enabled}. It should be True or False " + "corresponding to console enabled or disabled" ) body = {'enabled': enabled} @@ -1427,9 +1396,7 @@ def set_console_mode(self, session, enabled): retriable_status_codes=_common.RETRIABLE_STATUS_CODES, ) - msg = "Failed to change console mode for {node}".format( - node=self.id, - ) + msg = f"Failed to change console mode for {self.id}" exceptions.raise_from_response(response, error_message=msg) def get_node_inventory(self, session, node_id): @@ -1457,9 +1424,7 @@ def get_node_inventory(self, session, node_id): retriable_status_codes=_common.RETRIABLE_STATUS_CODES, ) - msg = "Failed to get inventory for node {node}".format( - node=self.id, - ) + msg = f"Failed to get inventory for node {node_id}" exceptions.raise_from_response(response, error_message=msg) return response.json() @@ -1487,9 +1452,7 @@ def list_firmware(self, session): retriable_status_codes=_common.RETRIABLE_STATUS_CODES, ) - msg = "Failed to list firmware components for node {node}".format( - node=self.id - ) + msg = f"Failed to list firmware components for node {self.id}" exceptions.raise_from_response(response, error_message=msg) return response.json() diff --git a/openstack/baremetal_introspection/v1/introspection.py b/openstack/baremetal_introspection/v1/introspection.py index db9eda0c5..d7f3b878d 100644 --- a/openstack/baremetal_introspection/v1/introspection.py +++ b/openstack/baremetal_introspection/v1/introspection.py @@ -102,9 +102,7 @@ def get_data(self, session, processed=True): response = session.get( request.url, headers=request.headers, microversion=version ) - msg = "Failed to fetch introspection data for node {id}".format( - id=self.id - ) + msg = f"Failed to fetch introspection data for node {self.id}" exceptions.raise_from_response(response, error_message=msg) return response.json() @@ -127,7 +125,7 @@ def wait(self, session, timeout=None, ignore_error=False): return self for count in utils.iterate_timeout( - timeout, "Timeout waiting for introspection on node %s" % self.id + timeout, f"Timeout waiting for introspection on node {self.id}" ): self.fetch(session) if self._check_state(ignore_error): @@ -142,8 +140,7 @@ def wait(self, session, timeout=None, ignore_error=False): def _check_state(self, ignore_error): if self.state == 'error' and not ignore_error: raise exceptions.ResourceFailure( - "Introspection of node %(node)s failed: %(error)s" - % {'node': self.id, 'error': self.error} + f"Introspection of node {self.id} failed: {self.error}" ) else: return self.is_finished diff --git a/openstack/block_storage/_base_proxy.py b/openstack/block_storage/_base_proxy.py index cba07da93..809d069b9 100644 --- a/openstack/block_storage/_base_proxy.py +++ b/openstack/block_storage/_base_proxy.py @@ -38,8 +38,8 @@ def create_image( volume_obj = self.get_volume(volume) if not volume_obj: raise exceptions.SDKException( - "Volume {volume} given to create_image could" - " not be found".format(volume=volume) + f"Volume {volume} given to create_image could " + f"not be found" ) volume_id = volume_obj['id'] data = self.post( diff --git a/openstack/block_storage/v2/backup.py b/openstack/block_storage/v2/backup.py index 12c78f1cb..5fb7adf3b 100644 --- a/openstack/block_storage/v2/backup.py +++ b/openstack/block_storage/v2/backup.py @@ -142,7 +142,7 @@ def create(self, session, prepend_key=True, base_path=None, **params): else: # Just for safety of the implementation (since PUT removed) raise exceptions.ResourceFailure( - "Invalid create method: %s" % self.create_method + f"Invalid create method: {self.create_method}" ) has_body = ( diff --git a/openstack/block_storage/v3/backup.py b/openstack/block_storage/v3/backup.py index b942fe98d..9efcd3418 100644 --- a/openstack/block_storage/v3/backup.py +++ b/openstack/block_storage/v3/backup.py @@ -158,7 +158,7 @@ def create(self, session, prepend_key=True, base_path=None, **params): else: # Just for safety of the implementation (since PUT removed) raise exceptions.ResourceFailure( - "Invalid create method: %s" % self.create_method + f"Invalid create method: {self.create_method}" ) has_body = ( diff --git a/openstack/cloud/_baremetal.py b/openstack/cloud/_baremetal.py index c6e74c50d..9a0bc077c 100644 --- a/openstack/cloud/_baremetal.py +++ b/openstack/cloud/_baremetal.py @@ -35,7 +35,7 @@ def _normalize_port_list(nics): except KeyError: raise TypeError( "Either 'address' or 'mac' must be provided " - "for port %s" % row + f"for port {row}" ) ports.append(dict(row, address=address)) return ports @@ -126,10 +126,9 @@ def inspect_machine(self, name_or_id, wait=False, timeout=3600): if node.provision_state == 'available': if node.instance_id: raise exceptions.SDKException( - "Refusing to inspect available machine %(node)s " + f"Refusing to inspect available machine {node.id} " "which is associated with an instance " - "(instance_uuid %(inst)s)" - % {'node': node.id, 'inst': node.instance_id} + f"(instance_uuid {node.instance_id})" ) return_to_available = True @@ -142,10 +141,9 @@ def inspect_machine(self, name_or_id, wait=False, timeout=3600): if node.provision_state not in ('manageable', 'inspect failed'): raise exceptions.SDKException( - "Machine %(node)s must be in 'manageable', 'inspect failed' " + f"Machine {node.id} must be in 'manageable', 'inspect failed' " "or 'available' provision state to start inspection, the " - "current state is %(state)s" - % {'node': node.id, 'state': node.provision_state} + f"current state is {node.provision_state}" ) node = self.baremetal.set_node_provision_state( @@ -229,7 +227,7 @@ def register_machine( if provision_state not in ('enroll', 'manageable', 'available'): raise ValueError( 'Initial provision state must be enroll, ' - 'manageable or available, got %s' % provision_state + f'manageable or available, got {provision_state}' ) # Available is tricky: it cannot be directly requested on newer API @@ -306,8 +304,8 @@ def unregister_machine(self, nics, uuid, wait=None, timeout=600): invalid_states = ['active', 'cleaning', 'clean wait', 'clean failed'] if machine['provision_state'] in invalid_states: raise exceptions.SDKException( - "Error unregistering node '%s' due to current provision " - "state '%s'" % (uuid, machine['provision_state']) + "Error unregistering node '{}' due to current provision " + "state '{}'".format(uuid, machine['provision_state']) ) # NOTE(TheJulia) There is a high possibility of a lock being present @@ -318,8 +316,8 @@ def unregister_machine(self, nics, uuid, wait=None, timeout=600): self.baremetal.wait_for_node_reservation(machine, timeout) except exceptions.SDKException as e: raise exceptions.SDKException( - "Error unregistering node '%s': Exception occured while" - " waiting to be able to proceed: %s" % (machine['uuid'], e) + "Error unregistering node '{}': Exception occured while " + "waiting to be able to proceed: {}".format(machine['uuid'], e) ) for nic in _normalize_port_list(nics): @@ -382,7 +380,7 @@ def update_machine(self, name_or_id, **attrs): machine = self.get_machine(name_or_id) if not machine: raise exceptions.SDKException( - "Machine update failed to find Machine: %s. " % name_or_id + f"Machine update failed to find Machine: {name_or_id}. " ) new_config = dict(machine._to_munch(), **attrs) @@ -394,8 +392,7 @@ def update_machine(self, name_or_id, **attrs): except Exception as e: raise exceptions.SDKException( "Machine update failed - Error generating JSON patch object " - "for submission to the API. Machine: %s Error: %s" - % (name_or_id, e) + f"for submission to the API. Machine: {name_or_id} Error: {e}" ) if not patch: diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index 244aca56a..5e6c5fe02 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -169,7 +169,7 @@ def update_volume(self, name_or_id, **kwargs): volume = self.get_volume(name_or_id) if not volume: - raise exceptions.SDKException("Volume %s not found." % name_or_id) + raise exceptions.SDKException(f"Volume {name_or_id} not found.") volume = self.block_storage.update_volume(volume, **kwargs) @@ -193,9 +193,7 @@ def set_volume_bootable(self, name_or_id, bootable=True): if not volume: raise exceptions.SDKException( - "Volume {name_or_id} does not exist".format( - name_or_id=name_or_id - ) + f"Volume {name_or_id} does not exist" ) self.block_storage.set_volume_bootable_status(volume, bootable) @@ -371,14 +369,16 @@ def attach_volume( dev = self.get_volume_attach_device(volume, server['id']) if dev: raise exceptions.SDKException( - "Volume %s already attached to server %s on device %s" - % (volume['id'], server['id'], dev) + "Volume {} already attached to server {} on device {}".format( + volume['id'], server['id'], dev + ) ) if volume['status'] != 'available': raise exceptions.SDKException( - "Volume %s is not available. Status is '%s'" - % (volume['id'], volume['status']) + "Volume {} is not available. Status is '{}'".format( + volume['id'], volume['status'] + ) ) payload = {} @@ -766,7 +766,7 @@ def get_volume_type_access(self, name_or_id): volume_type = self.get_volume_type(name_or_id) if not volume_type: raise exceptions.SDKException( - "VolumeType not found: %s" % name_or_id + f"VolumeType not found: {name_or_id}" ) return self.block_storage.get_type_access(volume_type) @@ -786,7 +786,7 @@ def add_volume_type_access(self, name_or_id, project_id): volume_type = self.get_volume_type(name_or_id) if not volume_type: raise exceptions.SDKException( - "VolumeType not found: %s" % name_or_id + f"VolumeType not found: {name_or_id}" ) self.block_storage.add_type_access(volume_type, project_id) @@ -804,7 +804,7 @@ def remove_volume_type_access(self, name_or_id, project_id): volume_type = self.get_volume_type(name_or_id) if not volume_type: raise exceptions.SDKException( - "VolumeType not found: %s" % name_or_id + f"VolumeType not found: {name_or_id}" ) self.block_storage.remove_type_access(volume_type, project_id) diff --git a/openstack/cloud/_coe.py b/openstack/cloud/_coe.py index 01fa92f8b..8783aea6c 100644 --- a/openstack/cloud/_coe.py +++ b/openstack/cloud/_coe.py @@ -124,7 +124,7 @@ def update_coe_cluster(self, name_or_id, **kwargs): cluster = self.get_coe_cluster(name_or_id) if not cluster: raise exceptions.SDKException( - "COE cluster %s not found." % name_or_id + f"COE cluster {name_or_id} not found." ) cluster = self.container_infrastructure_management.update_cluster( @@ -283,7 +283,7 @@ def update_cluster_template(self, name_or_id, **kwargs): cluster_template = self.get_cluster_template(name_or_id) if not cluster_template: raise exceptions.SDKException( - "Cluster template %s not found." % name_or_id + f"Cluster template {name_or_id} not found." ) cluster_template = ( diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index aabd3ddc4..6bbfaf89a 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -111,9 +111,7 @@ def get_flavor_by_ram(self, ram, include=None, get_extra=True): ): return flavor raise exceptions.SDKException( - "Could not find a flavor with {ram} and '{include}'".format( - ram=ram, include=include - ) + f"Could not find a flavor with {ram} and '{include}'" ) def search_keypairs(self, name_or_id=None, filters=None): @@ -622,8 +620,8 @@ def create_image_snapshot( server_obj = self.get_server(server, bare=True) if not server_obj: raise exceptions.SDKException( - "Server {server} could not be found and therefore" - " could not be snapshotted.".format(server=server) + f"Server {server} could not be found and therefore " + f"could not be snapshotted." ) server = server_obj image = self.compute.create_server_image( @@ -853,8 +851,8 @@ def create_server( kwargs['nics'] = [kwargs['nics']] else: raise exceptions.SDKException( - 'nics parameter to create_server takes a list of dicts.' - ' Got: {nics}'.format(nics=kwargs['nics']) + 'nics parameter to create_server takes a list of dicts. ' + 'Got: {nics}'.format(nics=kwargs['nics']) ) if network and ('nics' not in kwargs or not kwargs['nics']): @@ -902,8 +900,8 @@ def create_server( fixed_ip = nic.pop(ip_key, None) if fixed_ip and net.get('fixed_ip'): raise exceptions.SDKException( - "Only one of v4-fixed-ip, v6-fixed-ip or fixed_ip" - " may be given" + "Only one of v4-fixed-ip, v6-fixed-ip or fixed_ip " + "may be given" ) if fixed_ip: net['fixed_ip'] = fixed_ip @@ -917,8 +915,8 @@ def create_server( net['tag'] = nic.pop('tag') if nic: raise exceptions.SDKException( - "Additional unsupported keys given for server network" - " creation: {keys}".format(keys=nic.keys()) + f"Additional unsupported keys given for server network " + f"creation: {nic.keys()}" ) networks.append(net) if networks: @@ -1220,23 +1218,21 @@ def get_active_server( ) self.log.debug( - 'Server %(server)s reached ACTIVE state without' - ' being allocated an IP address.' - ' Deleting server.', - {'server': server['id']}, + f'Server {server["id"]} reached ACTIVE state without ' + f'being allocated an IP address. Deleting server.', ) try: self._delete_server(server=server, wait=wait, timeout=timeout) except Exception as e: raise exceptions.SDKException( - 'Server reached ACTIVE state without being' - ' allocated an IP address AND then could not' - ' be deleted: {}'.format(e), + f'Server reached ACTIVE state without being ' + f'allocated an IP address AND then could not ' + f'be deleted: {e}', extra_data=dict(server=server), ) raise exceptions.SDKException( - 'Server reached ACTIVE state without being' - ' allocated an IP address.', + 'Server reached ACTIVE state without being ' + 'allocated an IP address.', extra_data=dict(server=server), ) return None @@ -1378,9 +1374,9 @@ def _delete_server_floating_ips(self, server, delete_ip_retry): deleted = self.delete_floating_ip(ip['id'], retry=delete_ip_retry) if not deleted: raise exceptions.SDKException( - "Tried to delete floating ip {floating_ip}" - " associated with server {id} but there was" - " an error deleting it. Not deleting server.".format( + "Tried to delete floating ip {floating_ip} " + "associated with server {id} but there was " + "an error deleting it. Not deleting server.".format( floating_ip=ip['floating_ip_address'], id=server['id'] ) ) @@ -1725,7 +1721,7 @@ def set_aggregate_metadata(self, name_or_id, metadata): aggregate = self.get_aggregate(name_or_id) if not aggregate: raise exceptions.SDKException( - "Host aggregate %s not found." % name_or_id + f"Host aggregate {name_or_id} not found." ) return self.compute.set_aggregate_metadata(aggregate, metadata) @@ -1742,7 +1738,7 @@ def add_host_to_aggregate(self, name_or_id, host_name): aggregate = self.get_aggregate(name_or_id) if not aggregate: raise exceptions.SDKException( - "Host aggregate %s not found." % name_or_id + f"Host aggregate {name_or_id} not found." ) return self.compute.add_host_to_aggregate(aggregate, host_name) @@ -1759,7 +1755,7 @@ def remove_host_from_aggregate(self, name_or_id, host_name): aggregate = self.get_aggregate(name_or_id) if not aggregate: raise exceptions.SDKException( - "Host aggregate %s not found." % name_or_id + f"Host aggregate {name_or_id} not found." ) return self.compute.remove_host_from_aggregate(aggregate, host_name) @@ -1823,9 +1819,8 @@ def parse_date(date): # implementation detail - and the error message is actually # less informative. raise exceptions.SDKException( - "Date given, {date}, is invalid. Please pass in a date" - " string in ISO 8601 format -" - " YYYY-MM-DDTHH:MM:SS".format(date=date) + f"Date given, {date}, is invalid. Please pass in a date " + f"string in ISO 8601 format (YYYY-MM-DDTHH:MM:SS)" ) if isinstance(start, str): @@ -1844,7 +1839,7 @@ def _encode_server_userdata(self, userdata): if not isinstance(userdata, bytes): # If the userdata passed in is bytes, just send it unmodified if not isinstance(userdata, str): - raise TypeError("%s can't be encoded" % type(userdata)) + raise TypeError(f"{type(userdata)} can't be encoded") # If it's not bytes, make it bytes userdata = userdata.encode('utf-8', 'strict') diff --git a/openstack/cloud/_dns.py b/openstack/cloud/_dns.py index 2fe13bed8..c1a58a5a5 100644 --- a/openstack/cloud/_dns.py +++ b/openstack/cloud/_dns.py @@ -78,8 +78,7 @@ def create_zone( zone_type = zone_type.upper() if zone_type not in ('PRIMARY', 'SECONDARY'): raise exceptions.SDKException( - "Invalid type %s, valid choices are PRIMARY or SECONDARY" - % zone_type + f"Invalid type {zone_type}, valid choices are PRIMARY or SECONDARY" ) zone = { @@ -119,7 +118,7 @@ def update_zone(self, name_or_id, **kwargs): """ zone = self.get_zone(name_or_id) if not zone: - raise exceptions.SDKException("Zone %s not found." % name_or_id) + raise exceptions.SDKException(f"Zone {name_or_id} not found.") return self.dns.update_zone(zone['id'], **kwargs) @@ -156,7 +155,7 @@ def list_recordsets(self, zone): else: zone_obj = self.get_zone(zone) if zone_obj is None: - raise exceptions.SDKException("Zone %s not found." % zone) + raise exceptions.SDKException(f"Zone {zone} not found.") return list(self.dns.recordsets(zone_obj)) def get_recordset(self, zone, name_or_id): @@ -175,7 +174,7 @@ def get_recordset(self, zone, name_or_id): else: zone_obj = self.get_zone(zone) if not zone_obj: - raise exceptions.SDKException("Zone %s not found." % zone) + raise exceptions.SDKException(f"Zone {name_or_id} not found.") return self.dns.find_recordset( zone=zone_obj, name_or_id=name_or_id, ignore_missing=True ) @@ -206,7 +205,7 @@ def create_recordset( else: zone_obj = self.get_zone(zone) if not zone_obj: - raise exceptions.SDKException("Zone %s not found." % zone) + raise exceptions.SDKException(f"Zone {zone} not found.") # We capitalize the type in case the user sends in lowercase recordset_type = recordset_type.upper() @@ -239,9 +238,7 @@ def update_recordset(self, zone, name_or_id, **kwargs): rs = self.get_recordset(zone, name_or_id) if not rs: - raise exceptions.SDKException( - "Recordset %s not found." % name_or_id - ) + raise exceptions.SDKException(f"Recordset {name_or_id} not found.") rs = self.dns.update_recordset(recordset=rs, **kwargs) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index 4dbbabb7f..b7f6f8175 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -167,6 +167,8 @@ def update_project( domain_id=domain_id, ignore_missing=False, ) + if not project: + raise exceptions.SDKException(f"Project {name_or_id} not found.") if enabled is not None: kwargs.update({'enabled': enabled}) project = self.identity.update_project(project, **kwargs) @@ -218,11 +220,7 @@ def delete_project(self, name_or_id, domain_id=None): self.identity.delete_project(project) return True except exceptions.SDKException: - self.log.exception( - "Error in deleting project {project}".format( - project=name_or_id - ) - ) + self.log.exception(f"Error in deleting project {name_or_id}") return False @_utils.valid_kwargs('domain_id', 'name') @@ -577,9 +575,7 @@ def create_endpoint( service = self.get_service(name_or_id=service_name_or_id) if service is None: raise exceptions.SDKException( - "service {service} not found".format( - service=service_name_or_id - ) + f"service {service_name_or_id} not found" ) endpoints_args = [] @@ -786,7 +782,7 @@ def delete_domain(self, domain_id=None, name_or_id=None): return True except exceptions.SDKException: - self.log.exception("Failed to delete domain %s" % domain_id) + self.log.exception(f"Failed to delete domain {domain_id}") raise def list_domains(self, **filters): @@ -928,8 +924,8 @@ def create_group(self, name, description, domain=None): dom = self.get_domain(domain) if not dom: raise exceptions.SDKException( - "Creating group {group} failed: Invalid domain " - "{domain}".format(group=name, domain=domain) + f"Creating group {name} failed: Invalid domain " + f"{domain}" ) group_ref['domain_id'] = dom['id'] @@ -1124,11 +1120,11 @@ def list_role_assignments(self, filters=None): for k in ['role', 'group', 'user']: if k in filters: - filters['%s_id' % k] = filters.pop(k) + filters[f'{k}_id'] = filters.pop(k) for k in ['domain', 'project']: if k in filters: - filters['scope_%s_id' % k] = filters.pop(k) + filters[f'scope_{k}_id'] = filters.pop(k) if 'system' in filters: system_scope = filters.pop('system') diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py index 1a0fe61a9..d521c80e9 100644 --- a/openstack/cloud/_image.py +++ b/openstack/cloud/_image.py @@ -113,16 +113,17 @@ def download_image( """ if output_path is None and output_file is None: raise exceptions.SDKException( - 'No output specified, an output path or file object' - ' is necessary to write the image data to' + 'No output specified, an output path or file object ' + 'is necessary to write the image data to' ) elif output_path is not None and output_file is not None: raise exceptions.SDKException( - 'Both an output path and file object were provided,' - ' however only one can be used at once' + 'Both an output path and file object were provided, ' + 'however only one can be used at once' ) image = self.image.find_image(name_or_id, ignore_missing=False) + return self.image.download_image( image, output=output_file or output_path, chunk_size=chunk_size ) diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index f51c7b897..9538b5475 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -579,7 +579,7 @@ def update_network(self, name_or_id, **kwargs): network = self.get_network(name_or_id) if not network: - raise exceptions.SDKException("Network %s not found." % name_or_id) + raise exceptions.SDKException(f"Network {name_or_id} not found.") network = self.network.update_network(network, **kwargs) @@ -1356,7 +1356,7 @@ def update_qos_policy(self, name_or_id, **kwargs): ) if not curr_policy: raise exceptions.SDKException( - "QoS policy %s not found." % name_or_id + f"QoS policy {name_or_id} not found." ) return self.network.update_qos_policy(curr_policy, **kwargs) @@ -1426,9 +1426,7 @@ def list_qos_bandwidth_limit_rules(self, policy_name_or_id, filters=None): ) if not policy: raise exceptions.NotFoundException( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id - ) + f"QoS policy {policy_name_or_id} not Found." ) # Translate None from search interface to empty {} for kwargs below @@ -1460,9 +1458,7 @@ def get_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): ) if not policy: raise exceptions.NotFoundException( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id - ) + f"QoS policy {policy_name_or_id} not Found." ) return self.network.get_qos_bandwidth_limit_rule(rule_id, policy) @@ -1498,9 +1494,7 @@ def create_qos_bandwidth_limit_rule( ) if not policy: raise exceptions.NotFoundException( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id - ) + f"QoS policy {policy_name_or_id} not Found." ) if kwargs.get("direction") is not None: @@ -1544,9 +1538,7 @@ def update_qos_bandwidth_limit_rule( ) if not policy: raise exceptions.NotFoundException( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id - ) + f"QoS policy {policy_name_or_id} not Found." ) if kwargs.get("direction") is not None: @@ -1594,9 +1586,7 @@ def delete_qos_bandwidth_limit_rule(self, policy_name_or_id, rule_id): ) if not policy: raise exceptions.NotFoundException( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id - ) + f"QoS policy {policy_name_or_id} not Found." ) try: @@ -1657,9 +1647,7 @@ def list_qos_dscp_marking_rules(self, policy_name_or_id, filters=None): ) if not policy: raise exceptions.NotFoundException( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id - ) + f"QoS policy {policy_name_or_id} not Found." ) # Translate None from search interface to empty {} for kwargs below @@ -1686,9 +1674,7 @@ def get_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): ) if not policy: raise exceptions.NotFoundException( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id - ) + f"QoS policy {policy_name_or_id} not Found." ) return self.network.get_qos_dscp_marking_rule(rule_id, policy) @@ -1718,9 +1704,7 @@ def create_qos_dscp_marking_rule( ) if not policy: raise exceptions.NotFoundException( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id - ) + f"QoS policy {policy_name_or_id} not Found." ) return self.network.create_qos_dscp_marking_rule( @@ -1752,9 +1736,7 @@ def update_qos_dscp_marking_rule( ) if not policy: raise exceptions.NotFoundException( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id - ) + f"QoS policy {policy_name_or_id} not Found." ) if not kwargs: @@ -1792,9 +1774,7 @@ def delete_qos_dscp_marking_rule(self, policy_name_or_id, rule_id): ) if not policy: raise exceptions.NotFoundException( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id - ) + f"QoS policy {policy_name_or_id} not Found." ) try: @@ -1859,9 +1839,7 @@ def list_qos_minimum_bandwidth_rules( ) if not policy: raise exceptions.NotFoundException( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id - ) + f"QoS policy {policy_name_or_id} not Found." ) # Translate None from search interface to empty {} for kwargs below @@ -1891,9 +1869,7 @@ def get_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): ) if not policy: raise exceptions.NotFoundException( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id - ) + f"QoS policy {policy_name_or_id} not Found." ) return self.network.get_qos_minimum_bandwidth_rule(rule_id, policy) @@ -1927,9 +1903,7 @@ def create_qos_minimum_bandwidth_rule( ) if not policy: raise exceptions.NotFoundException( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id - ) + f"QoS policy {policy_name_or_id} not Found." ) kwargs['min_kbps'] = min_kbps @@ -1963,9 +1937,7 @@ def update_qos_minimum_bandwidth_rule( ) if not policy: raise exceptions.NotFoundException( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id - ) + f"QoS policy {policy_name_or_id} not Found." ) if not kwargs: @@ -2005,9 +1977,7 @@ def delete_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): ) if not policy: raise exceptions.NotFoundException( - "QoS policy {name_or_id} not Found.".format( - name_or_id=policy_name_or_id - ) + f"QoS policy {policy_name_or_id} not Found." ) try: @@ -2235,7 +2205,7 @@ def update_router( curr_router = self.get_router(name_or_id) if not curr_router: - raise exceptions.SDKException("Router %s not found." % name_or_id) + raise exceptions.SDKException(f"Router {name_or_id} not found.") return self.network.update_router(curr_router, **router) @@ -2348,7 +2318,7 @@ def create_subnet( network = self.get_network(network_name_or_id, filters) if not network: raise exceptions.SDKException( - "Network %s not found." % network_name_or_id + f"Network {network_name_or_id} not found." ) if disable_gateway_ip and gateway_ip: @@ -2378,7 +2348,7 @@ def create_subnet( subnetpool = self.get_subnetpool(subnetpool_name_or_id) if not subnetpool: raise exceptions.SDKException( - "Subnetpool %s not found." % subnetpool_name_or_id + f"Subnetpool {subnetpool_name_or_id} not found." ) # Be friendly on ip_version and allow strings @@ -2523,7 +2493,7 @@ def update_subnet( curr_subnet = self.get_subnet(name_or_id) if not curr_subnet: - raise exceptions.SDKException("Subnet %s not found." % name_or_id) + raise exceptions.SDKException(f"Subnet {name_or_id} not found.") return self.network.update_subnet(curr_subnet, **subnet) diff --git a/openstack/cloud/_network_common.py b/openstack/cloud/_network_common.py index cd440f99a..3a9187892 100644 --- a/openstack/cloud/_network_common.py +++ b/openstack/cloud/_network_common.py @@ -174,11 +174,11 @@ def _set_interesting_networks(self): if nat_source: raise exceptions.SDKException( 'Multiple networks were found matching ' - '{nat_net} which is the network configured ' + f'{self._nat_source} which is the network configured ' 'to be the NAT source. Please check your ' 'cloud resources. It is probably a good idea ' 'to configure this network by ID rather than ' - 'by name.'.format(nat_net=self._nat_source) + 'by name.' ) external_ipv4_floating_networks.append(network) nat_source = network @@ -192,11 +192,11 @@ def _set_interesting_networks(self): if nat_destination: raise exceptions.SDKException( 'Multiple networks were found matching ' - '{nat_net} which is the network configured ' + f'{self._nat_destination} which is the network configured ' 'to be the NAT destination. Please check your ' 'cloud resources. It is probably a good idea ' 'to configure this network by ID rather than ' - 'by name.'.format(nat_net=self._nat_destination) + 'by name.' ) nat_destination = network elif self._nat_destination is None: @@ -230,12 +230,12 @@ def _set_interesting_networks(self): if default_network: raise exceptions.SDKException( 'Multiple networks were found matching ' - '{default_net} which is the network ' + f'{self._default_network} which is the network ' 'configured to be the default interface ' 'network. Please check your cloud resources. ' 'It is probably a good idea ' 'to configure this network by ID rather than ' - 'by name.'.format(default_net=self._default_network) + 'by name.' ) default_network = network @@ -243,58 +243,50 @@ def _set_interesting_networks(self): for net_name in self._external_ipv4_names: if net_name not in [net['name'] for net in external_ipv4_networks]: raise exceptions.SDKException( - "Networks: {network} was provided for external IPv4 " - "access and those networks could not be found".format( - network=net_name - ) + f"Networks: {net_name} was provided for external IPv4 " + "access and those networks could not be found" ) for net_name in self._internal_ipv4_names: if net_name not in [net['name'] for net in internal_ipv4_networks]: raise exceptions.SDKException( - "Networks: {network} was provided for internal IPv4 " - "access and those networks could not be found".format( - network=net_name - ) + f"Networks: {net_name} was provided for internal IPv4 " + "access and those networks could not be found" ) for net_name in self._external_ipv6_names: if net_name not in [net['name'] for net in external_ipv6_networks]: raise exceptions.SDKException( - "Networks: {network} was provided for external IPv6 " - "access and those networks could not be found".format( - network=net_name - ) + f"Networks: {net_name} was provided for external IPv6 " + "access and those networks could not be found" ) for net_name in self._internal_ipv6_names: if net_name not in [net['name'] for net in internal_ipv6_networks]: raise exceptions.SDKException( - "Networks: {network} was provided for internal IPv6 " - "access and those networks could not be found".format( - network=net_name - ) + f"Networks: {net_name} was provided for internal IPv6 " + "access and those networks could not be found" ) if self._nat_destination and not nat_destination: raise exceptions.SDKException( - 'Network {network} was configured to be the ' + f'Network {self._nat_destination} was configured to be the ' 'destination for inbound NAT but it could not be ' - 'found'.format(network=self._nat_destination) + 'found' ) if self._nat_source and not nat_source: raise exceptions.SDKException( - 'Network {network} was configured to be the ' + f'Network {self._nat_source} was configured to be the ' 'source for inbound NAT but it could not be ' - 'found'.format(network=self._nat_source) + 'found' ) if self._default_network and not default_network: raise exceptions.SDKException( - 'Network {network} was configured to be the ' + f'Network {self._default_network} was configured to be the ' 'default network interface but it could not be ' - 'found'.format(network=self._default_network) + 'found' ) self._external_ipv4_networks = external_ipv4_networks @@ -812,7 +804,7 @@ def _neutron_create_floating_ip( except exceptions.NotFoundException: raise exceptions.NotFoundException( "unable to find network for floating ips with ID " - "{}".format(network_name_or_id) + f"{network_name_or_id}" ) network_id = network['id'] else: @@ -879,8 +871,8 @@ def _neutron_create_floating_ip( ) else: raise exceptions.SDKException( - "Attempted to create FIP on port {port} " - "but something went wrong".format(port=port) + f"Attempted to create FIP on port {port} " + "but something went wrong" ) return fip @@ -970,9 +962,7 @@ def _nova_delete_floating_ip(self, floating_ip_id): try: proxy._json_response( self.compute.delete(f'/os-floating-ips/{floating_ip_id}'), - error_message='Unable to delete floating IP {fip_id}'.format( - fip_id=floating_ip_id - ), + error_message=f'Unable to delete floating IP {floating_ip_id}', ) except exceptions.NotFoundException: return False @@ -1123,8 +1113,8 @@ def _nova_attach_ip_to_server( raise exceptions.SDKException( f"unable to find floating IP {floating_ip_id}" ) - error_message = "Error attaching IP {ip} to instance {id}".format( - ip=floating_ip_id, id=server_id + error_message = ( + f"Error attaching IP {floating_ip_id} to instance {server_id}" ) body = {'address': f_ip['floating_ip_address']} if fixed_address: @@ -1174,9 +1164,7 @@ def _neutron_detach_ip_from_server(self, server_id, floating_ip_id): self.network.update_ip(floating_ip_id, port_id=None) except exceptions.SDKException: raise exceptions.SDKException( - "Error detaching IP {ip} from server {server_id}".format( - ip=floating_ip_id, server_id=server_id - ) + f"Error detaching IP {floating_ip_id} from server {server_id}" ) return True @@ -1187,8 +1175,8 @@ def _nova_detach_ip_from_server(self, server_id, floating_ip_id): raise exceptions.SDKException( f"unable to find floating IP {floating_ip_id}" ) - error_message = "Error detaching IP {ip} from instance {id}".format( - ip=floating_ip_id, id=server_id + error_message = ( + f"Error detaching IP {floating_ip_id} from instance {server_id}" ) return proxy._json_response( self.compute.post( @@ -1533,27 +1521,25 @@ def _nat_destination_port( ) if not nat_network: raise exceptions.SDKException( - 'NAT Destination {nat_destination} was configured' - ' but not found on the cloud. Please check your' - ' config and your cloud and try again.'.format( - nat_destination=nat_destination - ) + f'NAT Destination {nat_destination} was ' + f'configured but not found on the cloud. Please ' + f'check your config and your cloud and try again.' ) else: nat_network = self.get_nat_destination() if not nat_network: raise exceptions.SDKException( - 'Multiple ports were found for server {server}' - ' but none of the networks are a valid NAT' - ' destination, so it is impossible to add a' - ' floating IP. If you have a network that is a valid' - ' destination for NAT and we could not find it,' - ' please file a bug. But also configure the' - ' nat_destination property of the networks list in' - ' your clouds.yaml file. If you do not have a' - ' clouds.yaml file, please make one - your setup' - ' is complicated.'.format(server=server['id']) + f'Multiple ports were found for server {server["id"]} ' + f'but none of the networks are a valid NAT ' + f'destination, so it is impossible to add a ' + f'floating IP. If you have a network that is a valid ' + f'destination for NAT and we could not find it, ' + f'please file a bug. But also configure the ' + f'nat_destination property of the networks list in ' + f'your clouds.yaml file. If you do not have a ' + f'clouds.yaml file, please make one - your setup ' + f'is complicated.' ) maybe_ports = [] @@ -1562,11 +1548,9 @@ def _nat_destination_port( maybe_ports.append(maybe_port) if not maybe_ports: raise exceptions.SDKException( - 'No port on server {server} was found matching' - ' your NAT destination network {dest}. Please ' - ' check your config'.format( - server=server['id'], dest=nat_network['name'] - ) + f'No port on server {server["id"]} was found matching ' + f'your NAT destination network {nat_network["name"]}.' + f'Please check your config' ) ports = maybe_ports @@ -1914,7 +1898,7 @@ def update_security_group(self, name_or_id, **kwargs): if group is None: raise exceptions.SDKException( - "Security group %s not found." % name_or_id + f"Security group {name_or_id} not found." ) if self._use_neutron_secgroups(): @@ -2006,7 +1990,7 @@ def create_security_group_rule( secgroup = self.get_security_group(secgroup_name_or_id) if not secgroup: raise exceptions.SDKException( - "Security group %s not found." % secgroup_name_or_id + f"Security group {secgroup_name_or_id} not found." ) if self._use_neutron_secgroups(): diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index 594a06856..e7815413e 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -114,11 +114,9 @@ def delete_container(self, name): return False except exceptions.ConflictException: raise exceptions.SDKException( - 'Attempt to delete container {container} failed. The' - ' container is not empty. Please delete the objects' - ' inside it before deleting the container'.format( - container=name - ) + f'Attempt to delete container {name} failed. The ' + f'container is not empty. Please delete the objects ' + f'inside it before deleting the container' ) def update_container(self, name, headers): @@ -142,8 +140,8 @@ def set_container_access(self, name, access, refresh=False): """ if access not in OBJECT_CONTAINER_ACLS: raise exceptions.SDKException( - "Invalid container access specified: %s. Must be one of %s" - % (access, list(OBJECT_CONTAINER_ACLS.keys())) + f"Invalid container access specified: {access}. " + f"Must be one of {list(OBJECT_CONTAINER_ACLS.keys())}" ) return self.object_store.set_container_metadata( name, read_ACL=OBJECT_CONTAINER_ACLS[access], refresh=refresh @@ -159,7 +157,7 @@ def get_container_access(self, name): """ container = self.get_container(name, skip_cache=True) if not container: - raise exceptions.SDKException("Container not found: %s" % name) + raise exceptions.SDKException(f"Container not found: {name}") acl = container.read_ACL or '' for key, value in OBJECT_CONTAINER_ACLS.items(): # Convert to string for the comparison because swiftclient @@ -168,7 +166,7 @@ def get_container_access(self, name): if str(acl) == str(value): return key raise exceptions.SDKException( - "Could not determine container access for ACL: %s." % acl + f"Could not determine container access for ACL: {acl}." ) def get_object_capabilities(self): @@ -423,13 +421,9 @@ def get_object_raw(self, container, obj, query_string=None, stream=False): def _get_object_endpoint(self, container, obj=None, query_string=None): endpoint = urllib.parse.quote(container) if obj: - endpoint = '{endpoint}/{object}'.format( - endpoint=endpoint, object=urllib.parse.quote(obj) - ) + endpoint = f'{endpoint}/{urllib.parse.quote(obj)}' if query_string: - endpoint = '{endpoint}?{query_string}'.format( - endpoint=endpoint, query_string=query_string - ) + endpoint = f'{endpoint}?{query_string}' return endpoint def stream_object( @@ -517,9 +511,7 @@ def _wait_for_futures(self, futures, raise_on_error=True): keystoneauth1.exceptions.RetriableConnectionFailure, exceptions.HttpException, ) as e: - error_text = "Exception processing async task: {}".format( - str(e) - ) + error_text = f"Exception processing async task: {str(e)}" if raise_on_error: self.log.exception(error_text) raise diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index a4f5ebc1f..df3603c24 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -159,21 +159,21 @@ def _get_entity(cloud, resource, name_or_id, filters, **kwargs): # If a uuid is passed short-circuit it calling the # get__by_id method if getattr(cloud, 'use_direct_get', False) and _is_uuid_like(name_or_id): - get_resource = getattr(cloud, 'get_%s_by_id' % resource, None) + get_resource = getattr(cloud, f'get_{resource}_by_id', None) if get_resource: return get_resource(name_or_id) search = ( resource if callable(resource) - else getattr(cloud, 'search_%ss' % resource, None) + else getattr(cloud, f'search_{resource}s', None) ) if search: entities = search(name_or_id, filters, **kwargs) if entities: if len(entities) > 1: raise exceptions.SDKException( - "Multiple matches found for %s" % name_or_id + f"Multiple matches found for {name_or_id}" ) return entities[0] return None @@ -213,8 +213,8 @@ def func_wrapper(func, *args, **kwargs): for k in kwargs: if k not in argspec.args[1:] and k not in valid_args: raise TypeError( - "{f}() got an unexpected keyword argument " - "'{arg}'".format(f=inspect.stack()[1][3], arg=k) + f"{inspect.stack()[1][3]}() got an unexpected keyword argument " + f"'{k}'" ) return func(*args, **kwargs) @@ -270,9 +270,7 @@ def safe_dict_min(key, data): except ValueError: raise exceptions.SDKException( "Search for minimum value failed. " - "Value for {key} is not an integer: {value}".format( - key=key, value=d[key] - ) + f"Value for {key} is not an integer: {d[key]}" ) if (min_value is None) or (val < min_value): min_value = val @@ -303,9 +301,7 @@ def safe_dict_max(key, data): except ValueError: raise exceptions.SDKException( "Search for maximum value failed. " - "Value for {key} is not an integer: {value}".format( - key=key, value=d[key] - ) + f"Value for {key} is not an integer: {d[key]}" ) if (max_value is None) or (val > max_value): max_value = val @@ -423,7 +419,7 @@ def generate_patches_from_kwargs(operation, **kwargs): """ patches = [] for k, v in kwargs.items(): - patch = {'op': operation, 'value': v, 'path': '/%s' % k} + patch = {'op': operation, 'value': v, 'path': f'/{k}'} patches.append(patch) return sorted(patches) diff --git a/openstack/cloud/exc.py b/openstack/cloud/exc.py index 9c5ddfe66..986a1873c 100644 --- a/openstack/cloud/exc.py +++ b/openstack/cloud/exc.py @@ -29,9 +29,7 @@ class OpenStackCloudUnavailableFeature(OpenStackCloudException): class OpenStackCloudCreateException(OpenStackCloudException): def __init__(self, resource, resource_id, extra_data=None, **kwargs): super().__init__( - message="Error creating {resource}: {resource_id}".format( - resource=resource, resource_id=resource_id - ), + message=f"Error creating {resource}: {resource_id}", extra_data=extra_data, **kwargs, ) diff --git a/openstack/cloud/meta.py b/openstack/cloud/meta.py index 245b4e7c7..79bba5173 100644 --- a/openstack/cloud/meta.py +++ b/openstack/cloud/meta.py @@ -245,7 +245,7 @@ def find_best_address(addresses, public=False, cloud_public=True): for address in addresses: try: for count in utils.iterate_timeout( - 5, "Timeout waiting for %s" % address, wait=0.1 + 5, f"Timeout waiting for {address}", wait=0.1 ): # Return the first one that is reachable try: @@ -275,10 +275,10 @@ def find_best_address(addresses, public=False, cloud_public=True): if do_check: log = _log.setup_logging('openstack') log.debug( - "The cloud returned multiple addresses %s:, and we could not " + f"The cloud returned multiple addresses {addresses}:, and we could not " "connect to port 22 on either. That might be what you wanted, " "but we have no clue what's going on, so we picked the first one " - "%s" % (addresses, addresses[0]) + f"{addresses[0]}" ) return addresses[0] @@ -379,7 +379,7 @@ def get_groups_from_server(cloud, server, server_vars): if extra_group: groups.append(extra_group) - groups.append('instance-%s' % server['id']) + groups.append('instance-{}'.format(server['id'])) for key in ('flavor', 'image'): if 'name' in server_vars[key]: @@ -439,11 +439,11 @@ def _get_supplemental_addresses(cloud, server): if fixed_net is None: log = _log.setup_logging('openstack') log.debug( - "The cloud returned floating ip %(fip)s attached" - " to server %(server)s but the fixed ip associated" - " with the floating ip in the neutron listing" - " does not exist in the nova listing. Something" - " is exceptionally broken.", + "The cloud returned floating ip %(fip)s attached " + "to server %(server)s but the fixed ip associated " + "with the floating ip in the neutron listing " + "does not exist in the nova listing. Something " + "is exceptionally broken.", dict(fip=fip['id'], server=server['id']), ) else: diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index a8c8e22c1..bb82a8023 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -540,13 +540,8 @@ def get_session_endpoint(self, service_key, **kwargs): raise except Exception as e: raise exceptions.SDKException( - "Error getting {service} endpoint on {cloud}:{region}: " - "{error}".format( - service=service_key, - cloud=self.name, - region=self.config.get_region_name(service_key), - error=str(e), - ) + f"Error getting {service_key} endpoint on {self.name}:{self.config.get_region_name(service_key)}: " + f"{str(e)}" ) return endpoint @@ -611,15 +606,14 @@ def search_resources( (service_name, resource_name) = resource_type.split('.') if not hasattr(self, service_name): raise exceptions.SDKException( - "service %s is not existing/enabled" % service_name + f"service {service_name} is not existing/enabled" ) service_proxy = getattr(self, service_name) try: resource_type = service_proxy._resource_registry[resource_name] except KeyError: raise exceptions.SDKException( - "Resource %s is not known in service %s" - % (resource_name, service_name) + f"Resource {resource_name} is not known in service {service_name}" ) if name_or_id: @@ -745,6 +739,6 @@ def cleanup_task(graph, service, fn): fn() except Exception: log = _log.setup_logging('openstack.project_cleanup') - log.exception('Error in the %s cleanup function' % service) + log.exception(f'Error in the {service} cleanup function') finally: graph.node_done(service) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 925699474..653d77f99 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -894,7 +894,7 @@ def get_console_url(self, session, console_type): """ action = CONSOLE_TYPE_ACTION_MAPPING.get(console_type) if not action: - raise ValueError("Unsupported console type %s" % console_type) + raise ValueError(f"Unsupported console type {console_type}") body = {action: {'type': console_type}} resp = self._action(session, body) return resp.json().get('console') @@ -967,12 +967,12 @@ def _live_migrate_25(self, session, host, force, block_migration): body['host'] = host if not force: raise ValueError( - "Live migration on this cloud implies 'force'" - " if the 'host' option has been given and it is not" - " possible to disable. It is recommended to not use 'host'" - " at all on this cloud as it is inherently unsafe, but if" - " it is unavoidable, please supply 'force=True' so that it" - " is clear you understand the risks." + "Live migration on this cloud implies 'force' " + "if the 'host' option has been given and it is not " + "possible to disable. It is recommended to not use 'host' " + "at all on this cloud as it is inherently unsafe, but if " + "it is unavoidable, please supply 'force=True' so that it " + "is clear you understand the risks." ) self._action( session, @@ -994,8 +994,8 @@ def _live_migrate( } if block_migration == 'auto': raise ValueError( - "Live migration on this cloud does not support 'auto' as" - " a parameter to block_migration, but only True and False." + "Live migration on this cloud does not support 'auto' as " + "a parameter to block_migration, but only True and False." ) body['block_migration'] = block_migration or False body['disk_over_commit'] = disk_over_commit or False @@ -1003,12 +1003,12 @@ def _live_migrate( body['host'] = host if not force: raise ValueError( - "Live migration on this cloud implies 'force'" - " if the 'host' option has been given and it is not" - " possible to disable. It is recommended to not use 'host'" - " at all on this cloud as it is inherently unsafe, but if" - " it is unavoidable, please supply 'force=True' so that it" - " is clear you understand the risks." + "Live migration on this cloud implies 'force' " + "if the 'host' option has been given and it is not " + "possible to disable. It is recommended to not use 'host' " + "at all on this cloud as it is inherently unsafe, but if " + "it is unavoidable, please supply 'force=True' so that it " + "is clear you understand the risks." ) self._action( session, diff --git a/openstack/compute/v2/server_group.py b/openstack/compute/v2/server_group.py index fa70a9557..f61382b16 100644 --- a/openstack/compute/v2/server_group.py +++ b/openstack/compute/v2/server_group.py @@ -122,7 +122,7 @@ def create(self, session, prepend_key=True, base_path=None, **params): ) else: raise exceptions.ResourceFailure( - "Invalid create method: %s" % self.create_method + f"Invalid create method: {self.create_method}" ) has_body = ( diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index a7ad99201..a769f4870 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -190,10 +190,8 @@ def from_conf(conf, session=None, service_types=None, **kwargs): _disable_service( config_dict, st, - reason="No section for project '{project}' (service type " - "'{service_type}') was present in the config.".format( - project=project_name, service_type=st - ), + reason=f"No section for project '{project_name}' (service type " + f"'{st}') was present in the config.", ) continue opt_dict: ty.Dict[str, str] = {} @@ -212,16 +210,10 @@ def from_conf(conf, session=None, service_types=None, **kwargs): # *that* blow up. reason = ( "Encountered an exception attempting to process config " - "for project '{project}' (service type " - "'{service_type}'): {exception}".format( - project=project_name, service_type=st, exception=e - ) - ) - _logger.warning( - "Disabling service '{service_type}': {reason}".format( - service_type=st, reason=reason - ) + f"for project '{project_name}' (service type " + f"'{st}'): {e}" ) + _logger.warning(f"Disabling service '{st}': {reason}") _disable_service(config_dict, st, reason=reason) continue # Load them into config_dict under keys prefixed by ${service_type}_ @@ -699,9 +691,8 @@ def get_session(self): # cert verification if not verify: self.log.debug( - 'Turning off SSL warnings for %(full_name)s since ' - 'verify=False', - {'full_name': self.full_name}, + f"Turning off SSL warnings for {self.full_name} " + f"since verify=False" ) requestsexceptions.squelch_warnings(insecure_requests=not verify) self._keystone_session = self._session_constructor( @@ -765,13 +756,10 @@ def _get_version_request(self, service_type, version): and implied_microversion != default_microversion ): raise exceptions.ConfigException( - "default_microversion of {default_microversion} was given" - " for {service_type}, but api_version looks like a" - " microversion as well. Please set api_version to just the" - " desired major version, or omit default_microversion".format( - default_microversion=default_microversion, - service_type=service_type, - ) + f"default_microversion of {default_microversion} was given " + f"for {service_type}, but api_version looks like a " + f"microversion as well. Please set api_version to just the " + f"desired major version, or omit default_microversion" ) if implied_microversion: default_microversion = implied_microversion @@ -896,10 +884,10 @@ def get_session_client( ): if self.get_default_microversion(service_type): raise exceptions.ConfigException( - "A default microversion for service {service_type} of" - " {default_microversion} was requested, but the cloud" - " only supports a minimum of {min_microversion} and" - " a maximum of {max_microversion}.".format( + "A default microversion for service {service_type} of " + "{default_microversion} was requested, but the cloud " + "only supports a minimum of {min_microversion} and " + "a maximum of {max_microversion}.".format( service_type=service_type, default_microversion=default_microversion, min_microversion=discover.version_to_string( @@ -912,17 +900,17 @@ def get_session_client( ) else: raise exceptions.ConfigException( - "A default microversion for service {service_type} of" - " {default_microversion} was requested, but the cloud" - " only supports a minimum of {min_microversion} and" - " a maximum of {max_microversion}. The default" - " microversion was set because a microversion" - " formatted version string, '{api_version}', was" - " passed for the api_version of the service. If it" - " was not intended to set a default microversion" - " please remove anything other than an integer major" - " version from the version setting for" - " the service.".format( + "A default microversion for service {service_type} of " + "{default_microversion} was requested, but the cloud " + "only supports a minimum of {min_microversion} and " + "a maximum of {max_microversion}. The default " + "microversion was set because a microversion " + "formatted version string, '{api_version}', was " + "passed for the api_version of the service. If it " + "was not intended to set a default microversion " + "please remove anything other than an integer major " + "version from the version setting for " + "the service.".format( service_type=service_type, api_version=self.get_api_version(service_type), default_microversion=default_microversion, diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 01f35f3df..5b5af3f74 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -138,10 +138,10 @@ def _fix_argv(argv): overlap.extend(old) if overlap: raise exceptions.ConfigException( - "The following options were given: '{options}' which contain" - " duplicates except that one has _ and one has -. There is" - " no sane way for us to know what you're doing. Remove the" - " duplicate option and try again".format(options=','.join(overlap)) + "The following options were given: '{options}' which contain " + "duplicates except that one has _ and one has -. There is " + "no sane way for us to know what you're doing. Remove the " + "duplicate option and try again".format(options=','.join(overlap)) ) @@ -264,12 +264,11 @@ def __init__( self.envvar_key = self._get_envvar('OS_CLOUD_NAME', 'envvars') if self.envvar_key in self.cloud_config['clouds']: raise exceptions.ConfigException( - '"{0}" defines a cloud named "{1}", but' - ' OS_CLOUD_NAME is also set to "{1}". Please rename' - ' either your environment based cloud, or one of your' - ' file-based clouds.'.format( - self.config_filename, self.envvar_key - ) + f'{self.config_filename!r} defines a cloud named ' + f'{self.envvar_key!r}, but OS_CLOUD_NAME is also set to ' + f'{self.envvar_key!r}. ' + f'Please rename either your environment-based cloud, ' + f'or one of your file-based clouds.' ) self.default_cloud = self._get_envvar('OS_CLOUD') @@ -501,7 +500,7 @@ def _expand_regions(self, regions): region ): raise exceptions.ConfigException( - 'Invalid region entry at: %s' % region + f'Invalid region entry at: {region}' ) if 'values' not in region: region['values'] = {} @@ -564,9 +563,9 @@ def _get_region(self, cloud=None, region_name=''): return region raise exceptions.ConfigException( - 'Region {region_name} is not a valid region name for cloud' - ' {cloud}. Valid choices are {region_list}. Please note that' - ' region names are case sensitive.'.format( + 'Region {region_name} is not a valid region name for cloud ' + '{cloud}. Valid choices are {region_list}. Please note that ' + 'region names are case sensitive.'.format( region_name=region_name, region_list=','.join([r['name'] for r in regions]), cloud=cloud, @@ -638,10 +637,8 @@ def _expand_vendor_profile(self, name, cloud, our_cloud): ) elif status == 'shutdown': raise exceptions.ConfigException( - "{profile_name} references a cloud that no longer" - " exists: {message}".format( - profile_name=profile_name, message=message - ) + f"{profile_name} references a cloud that no longer " + f"exists: {message}" ) _auth_update(cloud, profile_data) else: @@ -665,8 +662,8 @@ def _validate_networks(self, networks, key): for net in networks: if value and net[key]: raise exceptions.ConfigException( - "Duplicate network entries for {key}: {net1} and {net2}." - " Only one network can be flagged with {key}".format( + "Duplicate network entries for {key}: {net1} and {net2}. " + "Only one network can be flagged with {key}".format( key=key, net1=value['name'], net2=net['name'] ) ) @@ -705,9 +702,9 @@ def _fix_backwards_networks(self, cloud): external = key.startswith('external') if key in cloud and 'networks' in cloud: raise exceptions.ConfigException( - "Both {key} and networks were specified in the config." - " Please remove {key} from the config and use the network" - " list to configure network behavior.".format(key=key) + f"Both {key} and networks were specified in the config. " + f"Please remove {key} from the config and use the network " + f"list to configure network behavior." ) if key in cloud: warnings.warn( @@ -906,8 +903,8 @@ def register_argparse_arguments(self, parser, argv, service_keys=None): options, _args = parser.parse_known_args(argv) plugin_names = loading.get_available_plugin_names() raise exceptions.ConfigException( - "An invalid auth-type was specified: {auth_type}." - " Valid choices are: {plugin_names}.".format( + "An invalid auth-type was specified: {auth_type}. " + "Valid choices are: {plugin_names}.".format( auth_type=options.os_auth_type, plugin_names=",".join(plugin_names), ) diff --git a/openstack/config/vendors/__init__.py b/openstack/config/vendors/__init__.py index 68d68b74e..28671f713 100644 --- a/openstack/config/vendors/__init__.py +++ b/openstack/config/vendors/__init__.py @@ -58,12 +58,8 @@ def get_profile(profile_name): response = requests.get(well_known_url) if not response.ok: raise exceptions.ConfigException( - "{profile_name} is a remote profile that could not be fetched:" - " {status_code} {reason}".format( - profile_name=profile_name, - status_code=response.status_code, - reason=response.reason, - ) + f"{profile_name} is a remote profile that could not be fetched: " + f"{response.status_code} {response.reason}" ) vendor_defaults[profile_name] = None return diff --git a/openstack/exceptions.py b/openstack/exceptions.py index 528cc4c0f..f9f767b58 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -69,13 +69,9 @@ def __init__( # to be None once we're not mocking Session everywhere. if not message: if response is not None: - message = "{name}: {code}".format( - name=self.__class__.__name__, code=response.status_code - ) + message = f"{self.__class__.__name__}: {response.status_code}" else: - message = "{name}: Unknown error".format( - name=self.__class__.__name__ - ) + message = f"{self.__class__.__name__}: Unknown error" # Call directly rather than via super to control parameters SDKException.__init__(self, message=message) @@ -102,18 +98,13 @@ def __str__(self): if not self.url or self.message == 'Error': return self.message if self.url: - remote_error = "{source} Error for url: {url}".format( - source=self.source, url=self.url - ) + remote_error = f"{self.source} Error for url: {self.url}" if self.details: remote_error += ', ' if self.details: remote_error += str(self.details) - return "{message}: {remote_error}".format( - message=super().__str__(), - remote_error=remote_error, - ) + return f"{super().__str__()}: {remote_error}" class BadRequestException(HttpException): @@ -146,11 +137,7 @@ def __init__(self, resource, method): except AttributeError: name = resource.__class__.__name__ - message = 'The {} method is not supported for {}.{}'.format( - method, - resource.__module__, - name, - ) + message = f'The {method} method is not supported for {resource.__module__}.{name}' super().__init__(message=message) diff --git a/openstack/format.py b/openstack/format.py index b72c34f97..51d92b2d0 100644 --- a/openstack/format.py +++ b/openstack/format.py @@ -28,6 +28,4 @@ def deserialize(cls, value): elif "false" == expr: return False else: - raise ValueError( - "Unable to deserialize boolean string: %s" % value - ) + raise ValueError(f"Unable to deserialize boolean string: {value}") diff --git a/openstack/image/_download.py b/openstack/image/_download.py index ce9ad5285..1751bf237 100644 --- a/openstack/image/_download.py +++ b/openstack/image/_download.py @@ -85,9 +85,7 @@ def download( return resp except Exception as e: - raise exceptions.SDKException( - "Unable to download image: %s" % e - ) + raise exceptions.SDKException(f"Unable to download image: {e}") # if we are returning the repsonse object, ensure that it # has the content-md5 header so that the caller doesn't # need to jump through the same hoops through which we diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 5d54dbff5..543a6c9df 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -464,7 +464,7 @@ def stage_image(self, image, *, filename=None, data=None): if 'queued' != image.status: raise exceptions.SDKException( 'Image stage is only possible for images in the queued state. ' - 'Current state is {status}'.format(status=image.status) + f'Current state is {image.status}' ) if filename: @@ -694,9 +694,9 @@ def _upload_image_task( ): if not self._connection.has_service('object-store'): raise exceptions.SDKException( - "The cloud {cloud} is configured to use tasks for image " + f"The cloud {self._connection.config.name} is configured to use tasks for image " "upload, but no object-store service is available. " - "Aborting.".format(cloud=self._connection.config.name) + "Aborting." ) properties = image_kwargs.get('properties', {}) @@ -759,9 +759,7 @@ def _upload_image_task( except exceptions.ResourceFailure as e: glance_task = self.get_task(glance_task) raise exceptions.SDKException( - "Image creation failed: {message}".format( - message=e.message - ), + f"Image creation failed: {e.message}", extra_data=glance_task, ) finally: @@ -1839,9 +1837,7 @@ def wait_for_task( return task name = f"{task.__class__.__name__}:{task.id}" - msg = "Timeout waiting for {name} to transition to {status}".format( - name=name, status=status - ) + msg = f"Timeout waiting for {name} to transition to {status}" for count in utils.iterate_timeout( timeout=wait, message=msg, wait=interval @@ -1850,9 +1846,7 @@ def wait_for_task( if not task: raise exceptions.ResourceFailure( - "{name} went away while waiting for {status}".format( - name=name, status=status - ) + f"{name} went away while waiting for {status}" ) new_status = task.status @@ -1863,12 +1857,10 @@ def wait_for_task( if task.message == _IMAGE_ERROR_396: task_args = {'input': task.input, 'type': task.type} task = self.create_task(**task_args) - self.log.debug('Got error 396. Recreating task %s' % task) + self.log.debug(f'Got error 396. Recreating task {task}') else: raise exceptions.ResourceFailure( - "{name} transitioned to failure state {status}".format( - name=name, status=new_status - ) + f"{name} transitioned to failure state {new_status}" ) self.log.debug( diff --git a/openstack/key_manager/v1/_format.py b/openstack/key_manager/v1/_format.py index 58313c0a0..74ad58366 100644 --- a/openstack/key_manager/v1/_format.py +++ b/openstack/key_manager/v1/_format.py @@ -24,7 +24,7 @@ def deserialize(cls, value): # Only try to proceed if we have an actual URI. # Just check that we have a scheme, netloc, and path. if not all(parts[:3]): - raise ValueError("Unable to convert %s to an ID" % value) + raise ValueError(f"Unable to convert {value} to an ID") # The UUID will be the last portion of the URI. return parts.path.split("/")[-1] diff --git a/openstack/message/v2/message.py b/openstack/message/v2/message.py index 0b73b0605..4041b181d 100644 --- a/openstack/message/v2/message.py +++ b/openstack/message/v2/message.py @@ -155,7 +155,7 @@ def delete( # parameter when deleting a message that has been claimed, we # rebuild the request URI if claim_id is not None. if self.claim_id: - request.url += '?claim_id=%s' % self.claim_id + request.url += f'?claim_id={self.claim_id}' response = session.delete(request.url, headers=headers) self._translate_response(response, has_body=False) diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 1ea397046..74122ac59 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -5404,8 +5404,7 @@ def _check_tag_support(resource): resource.tags except AttributeError: raise exceptions.InvalidRequest( - '%s resource does not support tag' - % resource.__class__.__name__ + f'{resource.__class__.__name__} resource does not support tag' ) def get_tags(self, resource): @@ -7104,7 +7103,7 @@ def _service_cleanup( for port in self.ports( project_id=project_id, network_id=net.id ): - self.log.debug('Looking at port %s' % port) + self.log.debug(f'Looking at port {port}') if port.device_owner in [ 'network:router_interface', 'network:router_interface_distributed', @@ -7127,7 +7126,7 @@ def _service_cleanup( if network_has_ports_allocated: # If some ports are on net - we cannot delete it continue - self.log.debug('Network %s should be deleted' % net) + self.log.debug(f'Network {net} should be deleted') # __Check__ if we need to drop network according to filters network_must_be_deleted = self._service_cleanup_del_res( self.delete_network, @@ -7167,7 +7166,7 @@ def _service_cleanup( router=port.device_id, port_id=port.id ) except exceptions.SDKException: - self.log.error('Cannot delete object %s' % obj) + self.log.error(f'Cannot delete object {obj}') # router disconnected, drop it self._service_cleanup_del_res( self.delete_router, diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index 9bb6de618..a419ad1c6 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -430,9 +430,7 @@ def create_object( metadata[self._connection._OBJECT_SHA256_KEY] = sha256 container_name = self._get_container_name(container=container) - endpoint = '{container}/{name}'.format( - container=container_name, name=name - ) + endpoint = f'{container_name}/{name}' if data is not None: self.log.debug( @@ -582,9 +580,7 @@ def is_object_stale( metadata = self.get_object_metadata(name, container).metadata except exceptions.NotFoundException: self._connection.log.debug( - "swift stale check, no object: {container}/{name}".format( - container=container, name=name - ) + f"swift stale check, no object: {container}/{name}" ) return True @@ -608,7 +604,7 @@ def is_object_stale( if not up_to_date: self._connection.log.debug( "swift checksum mismatch: " - " %(filename)s!=%(container)s/%(name)s", + "%(filename)s!=%(container)s/%(name)s", {'filename': filename, 'container': container, 'name': name}, ) return True @@ -758,9 +754,7 @@ def _get_file_segments(self, endpoint, filename, file_size, segment_size): offset, segment_size if segment_size < remaining else remaining, ) - name = '{endpoint}/{index:0>6}'.format( - endpoint=endpoint, index=index - ) + name = f'{endpoint}/{index:0>6}' segments[name] = segment return segments @@ -878,8 +872,8 @@ def _check_temp_url_key(self, container=None, temp_url_key=None): temp_url_key = self.get_temp_url_key(container) if not temp_url_key: raise exceptions.SDKException( - 'temp_url_key was not given, nor was a temporary url key' - ' found for the account or the container.' + 'temp_url_key was not given, nor was a temporary url key ' + 'found for the account or the container.' ) return temp_url_key @@ -933,13 +927,7 @@ def generate_form_signature( endpoint = parse.urlparse(self.get_endpoint()) path = '/'.join([endpoint.path, res.name, object_prefix]) - data = '{}\n{}\n{}\n{}\n{}'.format( - path, - redirect_url, - max_file_size, - max_upload_count, - expires, - ) + data = f'{path}\n{redirect_url}\n{max_file_size}\n{max_upload_count}\n{expires}' sig = hmac.new(temp_url_key, data.encode(), sha1).hexdigest() return (expires, sig) @@ -1067,7 +1055,7 @@ def generate_temp_url( ip_range = ip_range.decode('utf-8') except UnicodeDecodeError: raise ValueError('ip_range must be representable as UTF-8') - hmac_parts.insert(0, "ip=%s" % ip_range) + hmac_parts.insert(0, f"ip={ip_range}") hmac_body = '\n'.join(hmac_parts) @@ -1084,11 +1072,7 @@ def generate_temp_url( else: exp = str(expiration) - temp_url = '{path}?temp_url_sig={sig}&temp_url_expires={exp}'.format( - path=path_for_body, - sig=sig, - exp=exp, - ) + temp_url = f'{path_for_body}?temp_url_sig={sig}&temp_url_expires={exp}' if ip_range: temp_url += f'&temp_url_ip_range={ip_range}' diff --git a/openstack/orchestration/util/environment_format.py b/openstack/orchestration/util/environment_format.py index 7afbe06b9..f547550a5 100644 --- a/openstack/orchestration/util/environment_format.py +++ b/openstack/orchestration/util/environment_format.py @@ -58,6 +58,6 @@ def parse(env_str): for param in env: if param not in SECTIONS: - raise ValueError('environment has wrong section "%s"' % param) + raise ValueError(f'environment has wrong section "{param}"') return env diff --git a/openstack/orchestration/util/event_utils.py b/openstack/orchestration/util/event_utils.py index 4e079f4e0..d5f86905f 100644 --- a/openstack/orchestration/util/event_utils.py +++ b/openstack/orchestration/util/event_utils.py @@ -50,7 +50,7 @@ def poll_for_events( """Continuously poll events and logs for performed action on stack.""" def stop_check_action(a): - stop_status = ('%s_FAILED' % action, '%s_COMPLETE' % action) + stop_status = (f'{action}_FAILED', f'{action}_COMPLETE') return a in stop_status def stop_check_no_action(a): diff --git a/openstack/orchestration/util/template_utils.py b/openstack/orchestration/util/template_utils.py index 48797f24d..0d8e9ebec 100644 --- a/openstack/orchestration/util/template_utils.py +++ b/openstack/orchestration/util/template_utils.py @@ -50,13 +50,13 @@ def get_template_contents( return {}, None else: raise exceptions.SDKException( - 'Must provide one of template_file,' - ' template_url or template_object' + 'Must provide one of template_file, template_url or ' + 'template_object' ) if not tpl: raise exceptions.SDKException( - 'Could not fetch template from %s' % template_url + f'Could not fetch template from {template_url}' ) try: @@ -65,8 +65,7 @@ def get_template_contents( template = template_format.parse(tpl) except ValueError as e: raise exceptions.SDKException( - 'Error parsing template %(url)s %(error)s' - % {'url': template_url, 'error': e} + f'Error parsing template {template_url} {e}' ) tmpl_base_url = utils.base_url_for_url(template_url) diff --git a/openstack/orchestration/util/utils.py b/openstack/orchestration/util/utils.py index 6a166c574..7644d6fa8 100644 --- a/openstack/orchestration/util/utils.py +++ b/openstack/orchestration/util/utils.py @@ -40,7 +40,7 @@ def read_url_content(url): # TODO(mordred) Use requests content = request.urlopen(url).read() except error.URLError: - raise exceptions.SDKException('Could not fetch contents for %s' % url) + raise exceptions.SDKException(f'Could not fetch contents for {url}') if content: try: diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index 49904115f..050aefeef 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -573,7 +573,7 @@ def get_template_contents( ) except Exception as e: raise exceptions.SDKException( - "Error in processing template files: %s" % str(e) + f"Error in processing template files: {str(e)}" ) def _get_cleanup_dependencies(self): diff --git a/openstack/orchestration/v1/stack.py b/openstack/orchestration/v1/stack.py index 5247a7f60..2571e2a61 100644 --- a/openstack/orchestration/v1/stack.py +++ b/openstack/orchestration/v1/stack.py @@ -133,16 +133,11 @@ def commit( # we need to use other endpoint for update preview. base_path = None if self.name and self.id: - base_path = '/stacks/{stack_name}/{stack_id}'.format( - stack_name=self.name, - stack_id=self.id, - ) + base_path = f'/stacks/{self.name}/{self.id}' elif self.name or self.id: # We have only one of name/id. Do not try to build a stacks/NAME/ID # path - base_path = '/stacks/{stack_identity}'.format( - stack_identity=self.name or self.id - ) + base_path = f'/stacks/{self.name or self.id}' request = self._prepare_request( prepend_key=False, requires_id=False, base_path=base_path ) @@ -248,9 +243,7 @@ def fetch( self._translate_response(response, **kwargs) if self and self.status in ['DELETE_COMPLETE', 'ADOPT_COMPLETE']: - raise exceptions.NotFoundException( - "No stack found for %s" % self.id - ) + raise exceptions.NotFoundException(f"No stack found for {self.id}") return self @classmethod diff --git a/openstack/proxy.py b/openstack/proxy.py index 36f72db47..2ecebf820 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -51,13 +51,12 @@ def check(self, expected, actual=None, *args, **kwargs): and actual is not None and not isinstance(actual, resource.Resource) ): - raise ValueError("A %s must be passed" % expected.__name__) + raise ValueError(f"A {expected.__name__} must be passed") elif isinstance(actual, resource.Resource) and not isinstance( actual, expected ): raise ValueError( - "Expected %s but received %s" - % (expected.__name__, actual.__class__.__name__) + f"Expected {expected.__name__} but received {actual.__class__.__name__}" ) return method(self, expected, actual, *args, **kwargs) @@ -340,16 +339,14 @@ def _report_stats_statsd(self, response, url=None, method=None, exc=None): with self._statsd_client.pipeline() as pipe: if response is not None: duration = int(response.elapsed.total_seconds() * 1000) - metric_name = '{}.{}'.format( - key, str(response.status_code) - ) + metric_name = f'{key}.{str(response.status_code)}' pipe.timing(metric_name, duration) pipe.incr(metric_name) if duration > 1000: - pipe.incr('%s.over_1000' % key) + pipe.incr(f'{key}.over_1000') elif exc is not None: - pipe.incr('%s.failed' % key) - pipe.incr('%s.attempted' % key) + pipe.incr(f'{key}.failed') + pipe.incr(f'{key}.attempted') except Exception: # We do not want errors in metric reporting ever break client self.log.exception("Exception reporting metrics") @@ -362,8 +359,8 @@ def _report_stats_prometheus( if response is not None and not method: method = response.request.method parsed_url = urlparse(url) - endpoint = "{}://{}{}".format( - parsed_url.scheme, parsed_url.netloc, parsed_url.path + endpoint = ( + f"{parsed_url.scheme}://{parsed_url.netloc}{parsed_url.path}" ) if response is not None: labels = dict( @@ -713,9 +710,7 @@ def _get( requires_id=requires_id, base_path=base_path, skip_cache=skip_cache, - error_message="No {resource_type} found for {value}".format( - resource_type=resource_type.__name__, value=value - ), + error_message=f"No {resource_type.__name__} found for {value}", ) def _list( @@ -875,8 +870,8 @@ def _service_cleanup_resource_filters_evaluation(self, obj, filters=None): # There are filters set, but we can't get required # attribute, so skip the resource self.log.debug( - 'Requested cleanup attribute %s is not ' - 'available on the resource' % k + f'Requested cleanup attribute {k} is not ' + 'available on the resource' ) part_cond.append(False) except Exception: diff --git a/openstack/resource.py b/openstack/resource.py index 979c1cc3f..6edc9f945 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -221,8 +221,9 @@ def warn_if_deprecated_property(self, value): if value and deprecated: warnings.warn( - "The field %r has been deprecated. %s" - % (self.name, deprecation_reason or "Avoid usage."), + "The field {!r} has been deprecated. {}".format( + self.name, deprecation_reason or "Avoid usage." + ), os_warnings.RemovedFieldWarning, ) return value @@ -386,8 +387,9 @@ def _validate(self, query, base_path=None, allow_unknown_params=False): else: if not allow_unknown_params: raise exceptions.InvalidResourceQuery( - message="Invalid query params: %s" - % ",".join(invalid_keys), + message="Invalid query params: {}".format( + ",".join(invalid_keys) + ), extra_data=invalid_keys, ) else: @@ -620,9 +622,7 @@ def __repr__(self): ] args = ", ".join(pairs) - return "{}.{}({})".format( - self.__module__, self.__class__.__name__, args - ) + return f"{self.__module__}.{self.__class__.__name__}({args})" def __eq__(self, comparand): """Return True if another resource has the same contents""" @@ -687,9 +687,8 @@ def __getitem__(self, name): for attr, component in self._attributes_iterator(tuple([Body])): if component.name == name: warnings.warn( - "Access to '%s[%s]' is deprecated. " - "Use '%s.%s' attribute instead" - % (self.__class__, name, self.__class__, attr), + f"Access to '{self.__class__}[{name}]' is deprecated. " + f"Use '{self.__class__}.{attr}' attribute instead", os_warnings.LegacyAPIWarning, ) return getattr(self, attr) @@ -710,13 +709,9 @@ def __setitem__(self, name, value): self._unknown_attrs_in_body[name] = value return raise KeyError( - "{name} is not found. {module}.{cls} objects do not support" - " setting arbitrary keys through the" - " dict interface.".format( - module=self.__module__, - cls=self.__class__.__name__, - name=name, - ) + f"{name} is not found. " + f"{self.__module__}.{self.__class__.__name__} objects do not " + f"support setting arbitrary keys through the dict interface." ) def _attributes( @@ -1340,9 +1335,9 @@ def _get_session(cls, session): if isinstance(session, adapter.Adapter): return session raise ValueError( - "The session argument to Resource methods requires either an" - " instance of an openstack.proxy.Proxy object or at the very least" - " a raw keystoneauth1.adapter.Adapter." + "The session argument to Resource methods requires either an " + "instance of an openstack.proxy.Proxy object or at the very least " + "a raw keystoneauth1.adapter.Adapter." ) @classmethod @@ -1373,7 +1368,7 @@ def _get_microversion(cls, session, *, action): 'delete', 'patch', }: - raise ValueError('Invalid action: %s' % action) + raise ValueError(f'Invalid action: {action}') if session.default_microversion: return session.default_microversion @@ -1414,9 +1409,9 @@ def _raise(message): if actual is None: message = ( - "API version %s is required, but the default " + f"API version {expected} is required, but the default " "version will be used." - ) % expected + ) _raise(message) actual_n = discover.normalize_version_number(actual) @@ -1424,9 +1419,9 @@ def _raise(message): expected_n = discover.normalize_version_number(expected) if actual_n < expected_n: message = ( - "API version %(expected)s is required, but %(actual)s " + f"API version {expected} is required, but {actual} " "will be used." - ) % {'expected': expected, 'actual': actual} + ) _raise(message) if maximum is not None: maximum_n = discover.normalize_version_number(maximum) @@ -1514,7 +1509,7 @@ def create( ) else: raise exceptions.ResourceFailure( - "Invalid create method: %s" % self.create_method + f"Invalid create method: {self.create_method}" ) has_body = ( @@ -1576,7 +1571,7 @@ def bulk_create( and isinstance(data, list) and all([isinstance(x, dict) for x in data]) ): - raise ValueError('Invalid data passed: %s' % data) + raise ValueError(f'Invalid data passed: {data}') session = cls._get_session(session) if microversion is None: @@ -1592,7 +1587,7 @@ def bulk_create( method = session.post else: raise exceptions.ResourceFailure( - "Invalid create method: %s" % cls.create_method + f"Invalid create method: {cls.create_method}" ) _body: ty.List[ty.Any] = [] @@ -1831,7 +1826,7 @@ def _commit( call = getattr(session, method.lower()) except AttributeError: raise exceptions.ResourceFailure( - "Invalid commit method: %s" % method + f"Invalid commit method: {method}" ) response = call( @@ -1858,7 +1853,7 @@ def _convert_patch(self, patch): parts = path.lstrip('/').split('/', 1) field = parts[0] except (KeyError, IndexError): - raise ValueError("Malformed or missing path in %s" % item) + raise ValueError(f"Malformed or missing path in {item}") try: component = getattr(self.__class__, field) @@ -1870,7 +1865,7 @@ def _convert_patch(self, patch): if len(parts) > 1: new_path = f'/{server_field}/{parts[1]}' else: - new_path = '/%s' % server_field + new_path = f'/{server_field}' converted.append(dict(item, path=new_path)) return converted @@ -2435,9 +2430,7 @@ def wait_for_status( failures = [f.lower() for f in failures] name = f"{resource.__class__.__name__}:{resource.id}" - msg = "Timeout waiting for {name} to transition to {status}".format( - name=name, status=status - ) + msg = f"Timeout waiting for {name} to transition to {status}" for count in utils.iterate_timeout( timeout=wait, message=msg, wait=interval @@ -2445,9 +2438,7 @@ def wait_for_status( resource = resource.fetch(session, skip_cache=True) if not resource: raise exceptions.ResourceFailure( - "{name} went away while waiting for {status}".format( - name=name, status=status - ) + f"{name} went away while waiting for {status}" ) new_status = getattr(resource, attribute) @@ -2456,9 +2447,7 @@ def wait_for_status( return resource elif normalized_status in failures: raise exceptions.ResourceFailure( - "{name} transitioned to failure state {status}".format( - name=name, status=new_status - ) + f"{name} transitioned to failure state {new_status}" ) LOG.debug( @@ -2494,9 +2483,7 @@ def wait_for_delete(session, resource, interval, wait, callback=None): orig_resource = resource for count in utils.iterate_timeout( timeout=wait, - message="Timeout waiting for {res}:{id} to delete".format( - res=resource.__class__.__name__, id=resource.id - ), + message=f"Timeout waiting for {resource.__class__.__name__}:{resource.id} to delete", wait=interval, ): try: diff --git a/openstack/service_description.py b/openstack/service_description.py index efe1e783b..a749d60ed 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -224,9 +224,7 @@ def _make_proxy(self, instance): if not data and instance._strict_proxies: raise exceptions.ServiceDiscoveryException( "Failed to create a working proxy for service " - "{service_type}: No endpoint data found.".format( - service_type=self.service_type - ) + f"{self.service_type}: No endpoint data found." ) # If we've gotten here with a proxy object it means we have @@ -279,8 +277,8 @@ def _make_proxy(self, instance): ) else: version_kwargs['min_version'] = str(supported_versions[0]) - version_kwargs['max_version'] = '{version}.latest'.format( - version=str(supported_versions[-1]) + version_kwargs['max_version'] = ( + f'{str(supported_versions[-1])}.latest' ) temp_adapter = config.get_session_client( @@ -291,21 +289,15 @@ def _make_proxy(self, instance): region_name = instance.config.get_region_name(self.service_type) if version_kwargs: raise exceptions.NotSupported( - "The {service_type} service for {cloud}:{region_name}" - " exists but does not have any supported versions.".format( - service_type=self.service_type, - cloud=instance.name, - region_name=region_name, - ) + f"The {self.service_type} service for " + f"{instance.name}:{region_name} exists but does not have " + f"any supported versions." ) else: raise exceptions.NotSupported( - "The {service_type} service for {cloud}:{region_name}" - " exists but no version was discoverable.".format( - service_type=self.service_type, - cloud=instance.name, - region_name=region_name, - ) + f"The {self.service_type} service for " + f"{instance.name}:{region_name} exists but no version " + f"was discoverable." ) proxy_class = self.supported_versions.get(str(found_version[0])) if proxy_class: @@ -322,11 +314,9 @@ def _make_proxy(self, instance): # service catalog that also doesn't have any useful # version discovery? warnings.warn( - "Service {service_type} has no discoverable version. " + f"Service {self.service_type} has no discoverable version. " "The resulting Proxy object will only have direct " - "passthrough REST capabilities.".format( - service_type=self.service_type - ), + "passthrough REST capabilities.", category=os_warnings.UnsupportedServiceVersion, ) return temp_adapter diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index dfa41e98a..0bc53f7cc 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -1094,7 +1094,7 @@ def delete_share_metadata(self, share_id, keys, ignore_missing=True): keys_failed_to_delete.append(key) if keys_failed_to_delete: raise exceptions.SDKException( - "Some keys failed to be deleted %s" % keys_failed_to_delete + f"Some keys failed to be deleted {keys_failed_to_delete}" ) def resource_locks(self, **query): diff --git a/openstack/test/fakes.py b/openstack/test/fakes.py index 63315b1b7..d82f88011 100644 --- a/openstack/test/fakes.py +++ b/openstack/test/fakes.py @@ -103,10 +103,7 @@ def generate_fake_resource( base_attrs[name] = [uuid.uuid4().hex] else: # Everything else - msg = "Fake value for {}.{} can not be generated".format( - resource_type.__name__, - name, - ) + msg = f"Fake value for {resource_type.__name__}.{name} can not be generated" raise NotImplementedError(msg) elif issubclass(target_type, list) and value.list_type is None: # List of str @@ -130,10 +127,7 @@ def generate_fake_resource( base_attrs[name] = dict() else: # Everything else - msg = "Fake value for {}.{} can not be generated".format( - resource_type.__name__, - name, - ) + msg = f"Fake value for {resource_type.__name__}.{name} can not be generated" raise NotImplementedError(msg) if isinstance(value, resource.URI): diff --git a/openstack/tests/base.py b/openstack/tests/base.py index be5bbb06d..3226f6be5 100644 --- a/openstack/tests/base.py +++ b/openstack/tests/base.py @@ -132,11 +132,7 @@ def assertSubdict(self, part, whole): if not whole[key] and part[key]: missing_keys.append(key) if missing_keys: - self.fail( - "Keys {} are in {} but not in {}".format( - missing_keys, part, whole - ) - ) + self.fail(f"Keys {missing_keys} are in {part} but not in {whole}") wrong_values = [ (key, part[key], whole[key]) for key in part @@ -144,8 +140,10 @@ def assertSubdict(self, part, whole): ] if wrong_values: self.fail( - "Mismatched values: %s" - % ", ".join( - "for %s got %s and %s" % tpl for tpl in wrong_values + "Mismatched values: {}".format( + ", ".join( + "for {} got {} and {}".format(*tpl) + for tpl in wrong_values + ) ) ) diff --git a/openstack/tests/fakes.py b/openstack/tests/fakes.py index c7b6dbf73..7c9dc5a5d 100644 --- a/openstack/tests/fakes.py +++ b/openstack/tests/fakes.py @@ -30,9 +30,7 @@ CHOCOLATE_FLAVOR_ID = '0c1d9008-f546-4608-9e8f-f8bdaec8ddde' STRAWBERRY_FLAVOR_ID = '0c1d9008-f546-4608-9e8f-f8bdaec8dddf' COMPUTE_ENDPOINT = 'https://compute.example.com/v2.1' -ORCHESTRATION_ENDPOINT = 'https://orchestration.example.com/v1/{p}'.format( - p=PROJECT_ID -) +ORCHESTRATION_ENDPOINT = f'https://orchestration.example.com/v1/{PROJECT_ID}' NO_MD5 = '93b885adfe0da089cdf634904fd59f71' NO_SHA256 = '6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d' FAKE_PUBLIC_KEY = ( @@ -53,15 +51,11 @@ def make_fake_flavor(flavor_id, name, ram=100, disk=1600, vcpus=24): 'id': flavor_id, 'links': [ { - 'href': '{endpoint}/flavors/{id}'.format( - endpoint=COMPUTE_ENDPOINT, id=flavor_id - ), + 'href': f'{COMPUTE_ENDPOINT}/flavors/{flavor_id}', 'rel': 'self', }, { - 'href': '{endpoint}/flavors/{id}'.format( - endpoint=COMPUTE_ENDPOINT, id=flavor_id - ), + 'href': f'{COMPUTE_ENDPOINT}/flavors/{flavor_id}', 'rel': 'bookmark', }, ], @@ -231,9 +225,7 @@ def make_fake_stack_event( "rel": "resource", }, { - "href": "{endpoint}/stacks/{name}/{id}".format( - endpoint=ORCHESTRATION_ENDPOINT, name=name, id=id - ), + "href": f"{ORCHESTRATION_ENDPOINT}/stacks/{name}/{id}", "rel": "stack", }, ], @@ -288,9 +280,7 @@ def make_fake_image( 'created_at': '2016-02-10T05:03:11Z', 'owner_specified.openstack.md5': md5 or NO_MD5, 'owner_specified.openstack.sha256': sha256 or NO_SHA256, - 'owner_specified.openstack.object': 'images/{name}'.format( - name=image_name - ), + 'owner_specified.openstack.object': f'images/{image_name}', 'protected': False, } diff --git a/openstack/tests/functional/baremetal/test_baremetal_driver.py b/openstack/tests/functional/baremetal/test_baremetal_driver.py index fb775c642..1c3b31eb8 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_driver.py +++ b/openstack/tests/functional/baremetal/test_baremetal_driver.py @@ -41,10 +41,10 @@ def test_fake_hardware_get(self): self.assertEqual('fake-hardware', driver.name) for iface in ('boot', 'deploy', 'management', 'power'): self.assertIn( - 'fake', getattr(driver, 'enabled_%s_interfaces' % iface) + 'fake', getattr(driver, f'enabled_{iface}_interfaces') ) self.assertEqual( - 'fake', getattr(driver, 'default_%s_interface' % iface) + 'fake', getattr(driver, f'default_{iface}_interface') ) self.assertNotEqual([], driver.hosts) @@ -53,8 +53,8 @@ def test_fake_hardware_list_details(self): driver = [d for d in drivers if d.name == 'fake-hardware'][0] for iface in ('boot', 'deploy', 'management', 'power'): self.assertIn( - 'fake', getattr(driver, 'enabled_%s_interfaces' % iface) + 'fake', getattr(driver, f'enabled_{iface}_interfaces') ) self.assertEqual( - 'fake', getattr(driver, 'default_%s_interface' % iface) + 'fake', getattr(driver, f'default_{iface}_interface') ) diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index 464561000..24f6affdf 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -220,11 +220,7 @@ def setUp(self): :returns: True if the service exists, otherwise False. """ if not self.conn.has_service(service_type): - self.skipTest( - 'Service {service_type} not found in cloud'.format( - service_type=service_type - ) - ) + self.skipTest(f'Service {service_type} not found in cloud') if not min_microversion: return @@ -252,9 +248,9 @@ def getUniqueString(self, prefix=None): # Globally unique names can only rely on some form of uuid # unix_t is also used to easier determine orphans when running real # functional tests on a real cloud - return (prefix if prefix else '') + "{time}-{uuid}".format( - time=int(time.time()), uuid=uuid.uuid4().hex - ) + return ( + prefix if prefix else '' + ) + f"{int(time.time())}-{uuid.uuid4().hex}" def create_temporary_project(self): """Create a new temporary project. diff --git a/openstack/tests/functional/cloud/test_cluster_templates.py b/openstack/tests/functional/cloud/test_cluster_templates.py index a52f3283a..a98c85a6f 100644 --- a/openstack/tests/functional/cloud/test_cluster_templates.py +++ b/openstack/tests/functional/cloud/test_cluster_templates.py @@ -58,12 +58,12 @@ def test_cluster_templates(self): '-N', '', '-f', - '%s/id_rsa_sdk' % self.ssh_directory, + f'{self.ssh_directory}/id_rsa_sdk', ] ) # add keypair to nova - with open('%s/id_rsa_sdk.pub' % self.ssh_directory) as f: + with open(f'{self.ssh_directory}/id_rsa_sdk.pub') as f: key_content = f.read() self.user_cloud.create_keypair('testkey', key_content) diff --git a/openstack/tests/functional/cloud/test_compute.py b/openstack/tests/functional/cloud/test_compute.py index c7e270f4a..037a7e025 100644 --- a/openstack/tests/functional/cloud/test_compute.py +++ b/openstack/tests/functional/cloud/test_compute.py @@ -339,9 +339,7 @@ def _wait_for_detach(self, volume_id): # consistency! for count in utils.iterate_timeout( 60, - 'Timeout waiting for volume {volume_id} to detach'.format( - volume_id=volume_id - ), + f'Timeout waiting for volume {volume_id} to detach', ): volume = self.user_cloud.get_volume(volume_id) if volume.status in ( diff --git a/openstack/tests/functional/cloud/test_recordset.py b/openstack/tests/functional/cloud/test_recordset.py index fff2b1b60..ce6e9127c 100644 --- a/openstack/tests/functional/cloud/test_recordset.py +++ b/openstack/tests/functional/cloud/test_recordset.py @@ -35,9 +35,9 @@ def test_recordsets_with_zone_id(self): '''Test DNS recordsets functionality''' sub = ''.join(random.choice(string.ascii_lowercase) for _ in range(6)) - zone = '%s.example2.net.' % sub + zone = f'{sub}.example2.net.' email = 'test@example2.net' - name = 'www.%s' % zone + name = f'www.{zone}' type_ = 'a' description = 'Test recordset' ttl = 3600 @@ -96,9 +96,9 @@ def test_recordsets_with_zone_name(self): '''Test DNS recordsets functionality''' sub = ''.join(random.choice(string.ascii_lowercase) for _ in range(6)) - zone = '%s.example2.net.' % sub + zone = f'{sub}.example2.net.' email = 'test@example2.net' - name = 'www.%s' % zone + name = f'www.{zone}' type_ = 'a' description = 'Test recordset' ttl = 3600 diff --git a/openstack/tests/functional/dns/v2/test_zone.py b/openstack/tests/functional/dns/v2/test_zone.py index 417897516..3875c78ad 100644 --- a/openstack/tests/functional/dns/v2/test_zone.py +++ b/openstack/tests/functional/dns/v2/test_zone.py @@ -54,9 +54,7 @@ def test_update_zone(self): self.assertEqual( current_ttl + 1, updated_zone_ttl, - 'Failed, updated TTL value is:{} instead of expected:{}'.format( - updated_zone_ttl, current_ttl + 1 - ), + f'Failed, updated TTL value is:{updated_zone_ttl} instead of expected:{current_ttl + 1}', ) def test_create_rs(self): diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index f86f33c4f..96d47f8a1 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -310,7 +310,7 @@ def test_no_arguments(self): result = self.node.set_provision_state(self.session, 'active') self.assertIs(result, self.node) self.session.put.assert_called_once_with( - 'nodes/%s/states/provision' % self.node.id, + f'nodes/{self.node.id}/states/provision', json={'target': 'active'}, headers=mock.ANY, microversion=None, @@ -321,7 +321,7 @@ def test_manage(self): result = self.node.set_provision_state(self.session, 'manage') self.assertIs(result, self.node) self.session.put.assert_called_once_with( - 'nodes/%s/states/provision' % self.node.id, + f'nodes/{self.node.id}/states/provision', json={'target': 'manage'}, headers=mock.ANY, microversion='1.4', @@ -334,7 +334,7 @@ def test_deploy_with_configdrive(self): ) self.assertIs(result, self.node) self.session.put.assert_called_once_with( - 'nodes/%s/states/provision' % self.node.id, + f'nodes/{self.node.id}/states/provision', json={'target': 'active', 'configdrive': 'abcd'}, headers=mock.ANY, microversion=None, @@ -348,7 +348,7 @@ def test_deploy_with_configdrive_as_bytestring(self): ) self.assertIs(result, self.node) self.session.put.assert_called_once_with( - 'nodes/%s/states/provision' % self.node.id, + f'nodes/{self.node.id}/states/provision', json={'target': 'active', 'configdrive': config_drive.decode()}, headers=mock.ANY, microversion=None, @@ -361,7 +361,7 @@ def test_rebuild_with_configdrive(self): ) self.assertIs(result, self.node) self.session.put.assert_called_once_with( - 'nodes/%s/states/provision' % self.node.id, + f'nodes/{self.node.id}/states/provision', json={'target': 'rebuild', 'configdrive': 'abcd'}, headers=mock.ANY, microversion='1.35', @@ -376,7 +376,7 @@ def test_configdrive_as_dict(self): ) self.assertIs(result, self.node) self.session.put.assert_called_once_with( - 'nodes/%s/states/provision' % self.node.id, + f'nodes/{self.node.id}/states/provision', json={'target': target, 'configdrive': {'user_data': 'abcd'}}, headers=mock.ANY, microversion='1.56', @@ -391,7 +391,7 @@ def test_deploy_with_deploy_steps(self): self.assertIs(result, self.node) self.session.put.assert_called_once_with( - 'nodes/%s/states/provision' % self.node.id, + f'nodes/{self.node.id}/states/provision', json={'target': 'active', 'deploy_steps': deploy_steps}, headers=mock.ANY, microversion='1.69', @@ -406,7 +406,7 @@ def test_rebuild_with_deploy_steps(self): self.assertIs(result, self.node) self.session.put.assert_called_once_with( - 'nodes/%s/states/provision' % self.node.id, + f'nodes/{self.node.id}/states/provision', json={'target': 'rebuild', 'deploy_steps': deploy_steps}, headers=mock.ANY, microversion='1.69', @@ -418,7 +418,7 @@ def test_set_provision_state_unhold(self): self.assertIs(result, self.node) self.session.put.assert_called_once_with( - 'nodes/%s/states/provision' % self.node.id, + f'nodes/{self.node.id}/states/provision', json={'target': 'unhold'}, headers=mock.ANY, microversion='1.85', @@ -433,7 +433,7 @@ def test_set_provision_state_service(self): self.assertIs(result, self.node) self.session.put.assert_called_once_with( - 'nodes/%s/states/provision' % self.node.id, + f'nodes/{self.node.id}/states/provision', json={'target': 'service', 'service_steps': service_steps}, headers=mock.ANY, microversion='1.87', @@ -448,7 +448,7 @@ def test_set_provision_state_clean_runbook(self): self.assertIs(result, self.node) self.session.put.assert_called_once_with( - 'nodes/%s/states/provision' % self.node.id, + f'nodes/{self.node.id}/states/provision', json={'target': 'clean', 'runbook': runbook}, headers=mock.ANY, microversion='1.92', @@ -463,7 +463,7 @@ def test_set_provision_state_service_runbook(self): self.assertIs(result, self.node) self.session.put.assert_called_once_with( - 'nodes/%s/states/provision' % self.node.id, + f'nodes/{self.node.id}/states/provision', json={'target': 'service', 'runbook': runbook}, headers=mock.ANY, microversion='1.92', @@ -602,7 +602,7 @@ def setUp(self): def test_attach_vif(self): self.assertIsNone(self.node.attach_vif(self.session, self.vif_id)) self.session.post.assert_called_once_with( - 'nodes/%s/vifs' % self.node.id, + f'nodes/{self.node.id}/vifs', json={'id': self.vif_id}, headers=mock.ANY, microversion='1.67', @@ -616,7 +616,7 @@ def test_attach_vif_no_retries(self): ) ) self.session.post.assert_called_once_with( - 'nodes/%s/vifs' % self.node.id, + f'nodes/{self.node.id}/vifs', json={'id': self.vif_id}, headers=mock.ANY, microversion='1.67', @@ -630,7 +630,7 @@ def test_attach_vif_with_port_uuid(self): ) ) self.session.post.assert_called_once_with( - 'nodes/%s/vifs' % self.node.id, + f'nodes/{self.node.id}/vifs', json={'id': self.vif_id, 'port_uuid': self.vif_port_uuid}, headers=mock.ANY, microversion='1.67', @@ -646,7 +646,7 @@ def test_attach_vif_with_portgroup_uuid(self): ) ) self.session.post.assert_called_once_with( - 'nodes/%s/vifs' % self.node.id, + f'nodes/{self.node.id}/vifs', json={ 'id': self.vif_id, 'portgroup_uuid': self.vif_portgroup_uuid, @@ -695,7 +695,7 @@ def test_list_vifs(self): res = self.node.list_vifs(self.session) self.assertEqual(['1234', '5678'], res) self.session.get.assert_called_once_with( - 'nodes/%s/vifs' % self.node.id, + f'nodes/{self.node.id}/vifs', headers=mock.ANY, microversion='1.67', ) @@ -849,7 +849,7 @@ def setUp(self): def test_inject_nmi(self): self.node.inject_nmi(self.session) self.session.put.assert_called_once_with( - 'nodes/%s/management/inject_nmi' % FAKE['uuid'], + 'nodes/{}/management/inject_nmi'.format(FAKE['uuid']), json={}, headers=mock.ANY, microversion='1.29', @@ -878,7 +878,7 @@ def setUp(self): def test_power_on(self): self.node.set_power_state(self.session, 'power on') self.session.put.assert_called_once_with( - 'nodes/%s/states/power' % FAKE['uuid'], + 'nodes/{}/states/power'.format(FAKE['uuid']), json={'target': 'power on'}, headers=mock.ANY, microversion=None, @@ -888,7 +888,7 @@ def test_power_on(self): def test_soft_power_on(self): self.node.set_power_state(self.session, 'soft power off') self.session.put.assert_called_once_with( - 'nodes/%s/states/power' % FAKE['uuid'], + 'nodes/{}/states/power'.format(FAKE['uuid']), json={'target': 'soft power off'}, headers=mock.ANY, microversion='1.27', @@ -912,7 +912,7 @@ def setUp(self): def test_set(self): self.node.set_maintenance(self.session) self.session.put.assert_called_once_with( - 'nodes/%s/maintenance' % self.node.id, + f'nodes/{self.node.id}/maintenance', json={'reason': None}, headers=mock.ANY, microversion=mock.ANY, @@ -921,7 +921,7 @@ def test_set(self): def test_set_with_reason(self): self.node.set_maintenance(self.session, 'No work on Monday') self.session.put.assert_called_once_with( - 'nodes/%s/maintenance' % self.node.id, + f'nodes/{self.node.id}/maintenance', json={'reason': 'No work on Monday'}, headers=mock.ANY, microversion=mock.ANY, @@ -930,7 +930,7 @@ def test_set_with_reason(self): def test_unset(self): self.node.unset_maintenance(self.session) self.session.delete.assert_called_once_with( - 'nodes/%s/maintenance' % self.node.id, + f'nodes/{self.node.id}/maintenance', json=None, headers=mock.ANY, microversion=mock.ANY, @@ -940,7 +940,7 @@ def test_set_via_update(self): self.node.is_maintenance = True self.node.commit(self.session) self.session.put.assert_called_once_with( - 'nodes/%s/maintenance' % self.node.id, + f'nodes/{self.node.id}/maintenance', json={'reason': None}, headers=mock.ANY, microversion=mock.ANY, @@ -953,7 +953,7 @@ def test_set_with_reason_via_update(self): self.node.maintenance_reason = 'No work on Monday' self.node.commit(self.session) self.session.put.assert_called_once_with( - 'nodes/%s/maintenance' % self.node.id, + f'nodes/{self.node.id}/maintenance', json={'reason': 'No work on Monday'}, headers=mock.ANY, microversion=mock.ANY, @@ -965,14 +965,14 @@ def test_set_with_other_fields(self): self.node.name = 'lazy-3000' self.node.commit(self.session) self.session.put.assert_called_once_with( - 'nodes/%s/maintenance' % self.node.id, + f'nodes/{self.node.id}/maintenance', json={'reason': None}, headers=mock.ANY, microversion=mock.ANY, ) self.session.patch.assert_called_once_with( - 'nodes/%s' % self.node.id, + f'nodes/{self.node.id}', json=[{'path': '/name', 'op': 'replace', 'value': 'lazy-3000'}], headers=mock.ANY, microversion=mock.ANY, @@ -984,14 +984,14 @@ def test_set_with_reason_and_other_fields(self): self.node.name = 'lazy-3000' self.node.commit(self.session) self.session.put.assert_called_once_with( - 'nodes/%s/maintenance' % self.node.id, + f'nodes/{self.node.id}/maintenance', json={'reason': 'No work on Monday'}, headers=mock.ANY, microversion=mock.ANY, ) self.session.patch.assert_called_once_with( - 'nodes/%s' % self.node.id, + f'nodes/{self.node.id}', json=[{'path': '/name', 'op': 'replace', 'value': 'lazy-3000'}], headers=mock.ANY, microversion=mock.ANY, @@ -1009,7 +1009,7 @@ def test_set_unset_maintenance(self): self.node.commit(self.session) self.session.put.assert_called_once_with( - 'nodes/%s/maintenance' % self.node.id, + f'nodes/{self.node.id}/maintenance', json={'reason': 'No work on Monday'}, headers=mock.ANY, microversion=mock.ANY, @@ -1020,7 +1020,7 @@ def test_set_unset_maintenance(self): self.assertIsNone(self.node.maintenance_reason) self.session.delete.assert_called_once_with( - 'nodes/%s/maintenance' % self.node.id, + f'nodes/{self.node.id}/maintenance', json=None, headers=mock.ANY, microversion=mock.ANY, @@ -1040,7 +1040,7 @@ def setUp(self): def test_get_boot_device(self): self.node.get_boot_device(self.session) self.session.get.assert_called_once_with( - 'nodes/%s/management/boot_device' % self.node.id, + f'nodes/{self.node.id}/management/boot_device', headers=mock.ANY, microversion=mock.ANY, retriable_status_codes=_common.RETRIABLE_STATUS_CODES, @@ -1049,7 +1049,7 @@ def test_get_boot_device(self): def test_set_boot_device(self): self.node.set_boot_device(self.session, 'pxe', persistent=False) self.session.put.assert_called_once_with( - 'nodes/%s/management/boot_device' % self.node.id, + f'nodes/{self.node.id}/management/boot_device', json={'boot_device': 'pxe', 'persistent': False}, headers=mock.ANY, microversion=mock.ANY, @@ -1059,7 +1059,7 @@ def test_set_boot_device(self): def test_get_supported_boot_devices(self): self.node.get_supported_boot_devices(self.session) self.session.get.assert_called_once_with( - 'nodes/%s/management/boot_device/supported' % self.node.id, + f'nodes/{self.node.id}/management/boot_device/supported', headers=mock.ANY, microversion=mock.ANY, retriable_status_codes=_common.RETRIABLE_STATUS_CODES, @@ -1080,7 +1080,7 @@ def setUp(self): def test_node_set_boot_mode(self): self.node.set_boot_mode(self.session, 'uefi') self.session.put.assert_called_once_with( - 'nodes/%s/states/boot_mode' % self.node.id, + f'nodes/{self.node.id}/states/boot_mode', json={'target': 'uefi'}, headers=mock.ANY, microversion=mock.ANY, @@ -1107,7 +1107,7 @@ def setUp(self): def test_node_set_secure_boot(self): self.node.set_secure_boot(self.session, True) self.session.put.assert_called_once_with( - 'nodes/%s/states/secure_boot' % self.node.id, + f'nodes/{self.node.id}/states/secure_boot', json={'target': True}, headers=mock.ANY, microversion=mock.ANY, @@ -1167,7 +1167,7 @@ def test_set_traits(self): traits = ['CUSTOM_FAKE', 'CUSTOM_REAL', 'CUSTOM_MISSING'] self.node.set_traits(self.session, traits) self.session.put.assert_called_once_with( - 'nodes/%s/traits' % self.node.id, + f'nodes/{self.node.id}/traits', json={'traits': ['CUSTOM_FAKE', 'CUSTOM_REAL', 'CUSTOM_MISSING']}, headers=mock.ANY, microversion='1.37', @@ -1264,7 +1264,7 @@ def setUp(self): def test_get_passthru(self): self.node.call_vendor_passthru(self.session, "GET", "test_method") self.session.get.assert_called_once_with( - 'nodes/%s/vendor_passthru?method=test_method' % self.node.id, + f'nodes/{self.node.id}/vendor_passthru?method=test_method', headers=mock.ANY, microversion='1.37', retriable_status_codes=_common.RETRIABLE_STATUS_CODES, @@ -1273,7 +1273,7 @@ def test_get_passthru(self): def test_post_passthru(self): self.node.call_vendor_passthru(self.session, "POST", "test_method") self.session.post.assert_called_once_with( - 'nodes/%s/vendor_passthru?method=test_method' % self.node.id, + f'nodes/{self.node.id}/vendor_passthru?method=test_method', headers=mock.ANY, microversion='1.37', retriable_status_codes=_common.RETRIABLE_STATUS_CODES, @@ -1282,7 +1282,7 @@ def test_post_passthru(self): def test_put_passthru(self): self.node.call_vendor_passthru(self.session, "PUT", "test_method") self.session.put.assert_called_once_with( - 'nodes/%s/vendor_passthru?method=test_method' % self.node.id, + f'nodes/{self.node.id}/vendor_passthru?method=test_method', headers=mock.ANY, microversion='1.37', retriable_status_codes=_common.RETRIABLE_STATUS_CODES, @@ -1291,7 +1291,7 @@ def test_put_passthru(self): def test_delete_passthru(self): self.node.call_vendor_passthru(self.session, "DELETE", "test_method") self.session.delete.assert_called_once_with( - 'nodes/%s/vendor_passthru?method=test_method' % self.node.id, + f'nodes/{self.node.id}/vendor_passthru?method=test_method', headers=mock.ANY, microversion='1.37', retriable_status_codes=_common.RETRIABLE_STATUS_CODES, @@ -1300,7 +1300,7 @@ def test_delete_passthru(self): def test_list_passthru(self): self.node.list_vendor_passthru(self.session) self.session.get.assert_called_once_with( - 'nodes/%s/vendor_passthru/methods' % self.node.id, + f'nodes/{self.node.id}/vendor_passthru/methods', headers=mock.ANY, microversion='1.37', retriable_status_codes=_common.RETRIABLE_STATUS_CODES, @@ -1321,7 +1321,7 @@ def setUp(self): def test_get_console(self): self.node.get_console(self.session) self.session.get.assert_called_once_with( - 'nodes/%s/states/console' % self.node.id, + f'nodes/{self.node.id}/states/console', headers=mock.ANY, microversion=mock.ANY, retriable_status_codes=_common.RETRIABLE_STATUS_CODES, @@ -1330,7 +1330,7 @@ def test_get_console(self): def test_set_console_mode(self): self.node.set_console_mode(self.session, True) self.session.put.assert_called_once_with( - 'nodes/%s/states/console' % self.node.id, + f'nodes/{self.node.id}/states/console', json={'enabled': True}, headers=mock.ANY, microversion=mock.ANY, @@ -1382,7 +1382,7 @@ def test_get_inventory(self): self.assertEqual(node_inventory, res) self.session.get.assert_called_once_with( - 'nodes/%s/inventory' % self.node.id, + f'nodes/{self.node.id}/inventory', headers=mock.ANY, microversion='1.81', retriable_status_codes=_common.RETRIABLE_STATUS_CODES, @@ -1427,7 +1427,7 @@ def test_list_firmware(self): self.assertEqual(node_firmware, res) self.session.get.assert_called_once_with( - 'nodes/%s/firmware' % self.node.id, + f'nodes/{self.node.id}/firmware', headers=mock.ANY, microversion='1.86', retriable_status_codes=_common.RETRIABLE_STATUS_CODES, diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index b1d609ce8..b63aeb535 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -203,7 +203,7 @@ def get_mock_url( if append: to_join.extend([urllib.parse.quote(i) for i in append]) if qs_elements is not None: - qs = '?%s' % '&'.join(qs_elements) + qs = '?{}'.format('&'.join(qs_elements)) return '{uri}{qs}'.format(uri='/'.join(to_join), qs=qs) def mock_for_keystone_projects( @@ -811,17 +811,13 @@ def __do_register_uris(self, uri_mock_list=None): # NOTE(notmorgan): make sure the delimiter is non-url-safe, in this # case "|" is used so that the split can be a bit easier on # maintainers of this code. - key = '{method}|{uri}|{params}'.format( - method=method, uri=uri, params=kw_params - ) + key = f'{method}|{uri}|{kw_params}' validate = to_mock.pop('validate', {}) valid_keys = {'json', 'headers', 'params', 'data'} invalid_keys = set(validate.keys()) - valid_keys if invalid_keys: raise TypeError( - "Invalid values passed to validate: {keys}".format( - keys=invalid_keys - ) + f"Invalid values passed to validate: {invalid_keys}" ) headers = structures.CaseInsensitiveDict( to_mock.pop('headers', {}) @@ -841,11 +837,10 @@ def __do_register_uris(self, uri_mock_list=None): 'PROGRAMMING ERROR: key-word-params ' 'should be part of the uri_key and cannot change, ' 'it will affect the matcher in requests_mock. ' - '%(old)r != %(new)r' - % { - 'old': self._uri_registry[key]['kw_params'], - 'new': kw_params, - } + '{old!r} != {new!r}'.format( + old=self._uri_registry[key]['kw_params'], + new=kw_params, + ) ) self._uri_registry[key]['response_list'].append(to_mock) @@ -900,9 +895,7 @@ def assert_calls(self, stop_after=None, do_count=True): 'call': '{method} {url}'.format( method=call['method'], url=call['url'] ), - 'history': '{method} {url}'.format( - method=history.method, url=history.url - ), + 'history': f'{history.method} {history.url}', } ), ) diff --git a/openstack/tests/unit/block_storage/v2/test_backup.py b/openstack/tests/unit/block_storage/v2/test_backup.py index 040063024..600ed8855 100644 --- a/openstack/tests/unit/block_storage/v2/test_backup.py +++ b/openstack/tests/unit/block_storage/v2/test_backup.py @@ -139,7 +139,7 @@ def test_restore(self): self.assertEqual(sot, sot.restore(self.sess, 'vol', 'name')) - url = 'backups/%s/restore' % FAKE_ID + url = f'backups/{FAKE_ID}/restore' body = {"restore": {"volume_id": "vol", "name": "name"}} self.sess.post.assert_called_with(url, json=body) @@ -148,7 +148,7 @@ def test_restore_name(self): self.assertEqual(sot, sot.restore(self.sess, name='name')) - url = 'backups/%s/restore' % FAKE_ID + url = f'backups/{FAKE_ID}/restore' body = {"restore": {"name": "name"}} self.sess.post.assert_called_with(url, json=body) @@ -157,7 +157,7 @@ def test_restore_vol_id(self): self.assertEqual(sot, sot.restore(self.sess, volume_id='vol')) - url = 'backups/%s/restore' % FAKE_ID + url = f'backups/{FAKE_ID}/restore' body = {"restore": {"volume_id": "vol"}} self.sess.post.assert_called_with(url, json=body) @@ -171,7 +171,7 @@ def test_force_delete(self): self.assertIsNone(sot.force_delete(self.sess)) - url = 'backups/%s/action' % FAKE_ID + url = f'backups/{FAKE_ID}/action' body = {'os-force_delete': None} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -182,7 +182,7 @@ def test_reset(self): self.assertIsNone(sot.reset(self.sess, 'new_status')) - url = 'backups/%s/action' % FAKE_ID + url = f'backups/{FAKE_ID}/action' body = {'os-reset_status': {'status': 'new_status'}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion diff --git a/openstack/tests/unit/block_storage/v2/test_snapshot.py b/openstack/tests/unit/block_storage/v2/test_snapshot.py index 0c2909404..657ffb1e0 100644 --- a/openstack/tests/unit/block_storage/v2/test_snapshot.py +++ b/openstack/tests/unit/block_storage/v2/test_snapshot.py @@ -97,7 +97,7 @@ def test_reset(self): self.assertIsNone(sot.reset(self.sess, 'new_status')) - url = 'snapshots/%s/action' % FAKE_ID + url = f'snapshots/{FAKE_ID}/action' body = {'os-reset_status': {'status': 'new_status'}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion diff --git a/openstack/tests/unit/block_storage/v2/test_type.py b/openstack/tests/unit/block_storage/v2/test_type.py index d67934d06..edfade8ff 100644 --- a/openstack/tests/unit/block_storage/v2/test_type.py +++ b/openstack/tests/unit/block_storage/v2/test_type.py @@ -73,7 +73,7 @@ def test_get_private_access(self): ) self.sess.get.assert_called_with( - "types/%s/os-volume-type-access" % sot.id + f"types/{sot.id}/os-volume-type-access" ) def test_add_private_access(self): @@ -81,7 +81,7 @@ def test_add_private_access(self): self.assertIsNone(sot.add_private_access(self.sess, "a")) - url = "types/%s/action" % sot.id + url = f"types/{sot.id}/action" body = {"addProjectAccess": {"project": "a"}} self.sess.post.assert_called_with(url, json=body) @@ -90,6 +90,6 @@ def test_remove_private_access(self): self.assertIsNone(sot.remove_private_access(self.sess, "a")) - url = "types/%s/action" % sot.id + url = f"types/{sot.id}/action" body = {"removeProjectAccess": {"project": "a"}} self.sess.post.assert_called_with(url, json=body) diff --git a/openstack/tests/unit/block_storage/v2/test_volume.py b/openstack/tests/unit/block_storage/v2/test_volume.py index 3f13ed80a..1e80ce9f0 100644 --- a/openstack/tests/unit/block_storage/v2/test_volume.py +++ b/openstack/tests/unit/block_storage/v2/test_volume.py @@ -153,7 +153,7 @@ def test_extend(self): self.assertIsNone(sot.extend(self.sess, '20')) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {"os-extend": {"new_size": "20"}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -164,7 +164,7 @@ def test_set_volume_readonly(self): self.assertIsNone(sot.set_readonly(self.sess, True)) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-update_readonly_flag': {'readonly': True}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -175,7 +175,7 @@ def test_set_volume_readonly_false(self): self.assertIsNone(sot.set_readonly(self.sess, False)) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-update_readonly_flag': {'readonly': False}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -186,7 +186,7 @@ def test_set_volume_bootable(self): self.assertIsNone(sot.set_bootable_status(self.sess)) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-set_bootable': {'bootable': True}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -197,7 +197,7 @@ def test_set_volume_bootable_false(self): self.assertIsNone(sot.set_bootable_status(self.sess, False)) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-set_bootable': {'bootable': False}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -208,7 +208,7 @@ def test_set_image_metadata(self): self.assertIsNone(sot.set_image_metadata(self.sess, {'foo': 'bar'})) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-set_image_metadata': {'foo': 'bar'}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -224,7 +224,7 @@ def test_delete_image_metadata(self): self.assertIsNone(sot.delete_image_metadata(self.sess)) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body_a = {'os-unset_image_metadata': 'foo'} body_b = {'os-unset_image_metadata': 'baz'} self.sess.post.assert_has_calls( @@ -243,7 +243,7 @@ def test_delete_image_metadata_item(self): self.assertIsNone(sot.delete_image_metadata_item(self.sess, 'foo')) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-unset_image_metadata': 'foo'} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -254,7 +254,7 @@ def test_reset_status(self): self.assertIsNone(sot.reset_status(self.sess, '1', '2', '3')) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = { 'os-reset_status': { 'status': '1', @@ -271,7 +271,7 @@ def test_reset_status__single_option(self): self.assertIsNone(sot.reset_status(self.sess, status='1')) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = { 'os-reset_status': { 'status': '1', @@ -286,7 +286,7 @@ def test_attach_instance(self): self.assertIsNone(sot.attach(self.sess, '1', '2')) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-attach': {'mountpoint': '1', 'instance_uuid': '2'}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -297,7 +297,7 @@ def test_detach(self): self.assertIsNone(sot.detach(self.sess, '1')) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-detach': {'attachment_id': '1'}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -308,7 +308,7 @@ def test_detach_force(self): self.assertIsNone(sot.detach(self.sess, '1', force=True)) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-force_detach': {'attachment_id': '1'}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -319,7 +319,7 @@ def test_unmanage(self): self.assertIsNone(sot.unmanage(self.sess)) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-unmanage': None} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -330,7 +330,7 @@ def test_retype(self): self.assertIsNone(sot.retype(self.sess, '1')) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-retype': {'new_type': '1'}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -341,7 +341,7 @@ def test_retype_mp(self): self.assertIsNone(sot.retype(self.sess, '1', migration_policy='2')) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-retype': {'new_type': '1', 'migration_policy': '2'}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -352,7 +352,7 @@ def test_migrate(self): self.assertIsNone(sot.migrate(self.sess, host='1')) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-migrate_volume': {'host': '1'}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -367,7 +367,7 @@ def test_migrate_flags(self): ) ) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = { 'os-migrate_volume': { 'host': '1', @@ -384,7 +384,7 @@ def test_complete_migration(self): self.assertIsNone(sot.complete_migration(self.sess, new_volume_id='1')) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = { 'os-migrate_volume_completion': {'new_volume': '1', 'error': False} } @@ -399,7 +399,7 @@ def test_complete_migration_error(self): sot.complete_migration(self.sess, new_volume_id='1', error=True) ) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = { 'os-migrate_volume_completion': {'new_volume': '1', 'error': True} } @@ -412,7 +412,7 @@ def test_force_delete(self): self.assertIsNone(sot.force_delete(self.sess)) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-force_delete': None} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion diff --git a/openstack/tests/unit/block_storage/v3/test_attachment.py b/openstack/tests/unit/block_storage/v3/test_attachment.py index af725a0ae..006760219 100644 --- a/openstack/tests/unit/block_storage/v3/test_attachment.py +++ b/openstack/tests/unit/block_storage/v3/test_attachment.py @@ -180,7 +180,7 @@ def test_complete(self, mock_translate): sot.id = FAKE_ID sot.complete(self.sess) self.sess.post.assert_called_with( - '/attachments/%s/action' % FAKE_ID, + f'/attachments/{FAKE_ID}/action', json={ 'os-complete': '92dc3671-d0ab-4370-8058-c88a71661ec5', }, diff --git a/openstack/tests/unit/block_storage/v3/test_backup.py b/openstack/tests/unit/block_storage/v3/test_backup.py index a80b97d46..796cd05ae 100644 --- a/openstack/tests/unit/block_storage/v3/test_backup.py +++ b/openstack/tests/unit/block_storage/v3/test_backup.py @@ -152,7 +152,7 @@ def test_restore(self): self.assertEqual(sot, sot.restore(self.sess, 'vol', 'name')) - url = 'backups/%s/restore' % FAKE_ID + url = f'backups/{FAKE_ID}/restore' body = {"restore": {"volume_id": "vol", "name": "name"}} self.sess.post.assert_called_with(url, json=body) @@ -161,7 +161,7 @@ def test_restore_name(self): self.assertEqual(sot, sot.restore(self.sess, name='name')) - url = 'backups/%s/restore' % FAKE_ID + url = f'backups/{FAKE_ID}/restore' body = {"restore": {"name": "name"}} self.sess.post.assert_called_with(url, json=body) @@ -170,7 +170,7 @@ def test_restore_vol_id(self): self.assertEqual(sot, sot.restore(self.sess, volume_id='vol')) - url = 'backups/%s/restore' % FAKE_ID + url = f'backups/{FAKE_ID}/restore' body = {"restore": {"volume_id": "vol"}} self.sess.post.assert_called_with(url, json=body) @@ -184,7 +184,7 @@ def test_force_delete(self): self.assertIsNone(sot.force_delete(self.sess)) - url = 'backups/%s/action' % FAKE_ID + url = f'backups/{FAKE_ID}/action' body = {'os-force_delete': None} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -195,7 +195,7 @@ def test_reset(self): self.assertIsNone(sot.reset(self.sess, 'new_status')) - url = 'backups/%s/action' % FAKE_ID + url = f'backups/{FAKE_ID}/action' body = {'os-reset_status': {'status': 'new_status'}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion diff --git a/openstack/tests/unit/block_storage/v3/test_group.py b/openstack/tests/unit/block_storage/v3/test_group.py index e75cf97ac..cc6592885 100644 --- a/openstack/tests/unit/block_storage/v3/test_group.py +++ b/openstack/tests/unit/block_storage/v3/test_group.py @@ -87,7 +87,7 @@ def test_delete(self): self.assertIsNone(sot.delete(self.sess)) - url = 'groups/%s/action' % GROUP_ID + url = f'groups/{GROUP_ID}/action' body = {'delete': {'delete-volumes': False}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -98,7 +98,7 @@ def test_reset(self): self.assertIsNone(sot.reset(self.sess, 'new_status')) - url = 'groups/%s/action' % GROUP_ID + url = f'groups/{GROUP_ID}/action' body = {'reset_status': {'status': 'new_status'}} self.sess.post.assert_called_with( url, diff --git a/openstack/tests/unit/block_storage/v3/test_snapshot.py b/openstack/tests/unit/block_storage/v3/test_snapshot.py index b377d5fca..e925493f1 100644 --- a/openstack/tests/unit/block_storage/v3/test_snapshot.py +++ b/openstack/tests/unit/block_storage/v3/test_snapshot.py @@ -105,7 +105,7 @@ def test_force_delete(self): self.assertIsNone(sot.force_delete(self.sess)) - url = 'snapshots/%s/action' % FAKE_ID + url = f'snapshots/{FAKE_ID}/action' body = {'os-force_delete': None} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -116,7 +116,7 @@ def test_reset(self): self.assertIsNone(sot.reset(self.sess, 'new_status')) - url = 'snapshots/%s/action' % FAKE_ID + url = f'snapshots/{FAKE_ID}/action' body = {'os-reset_status': {'status': 'new_status'}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -127,7 +127,7 @@ def test_set_status(self): self.assertIsNone(sot.set_status(self.sess, 'new_status')) - url = 'snapshots/%s/action' % FAKE_ID + url = f'snapshots/{FAKE_ID}/action' body = {'os-update_snapshot_status': {'status': 'new_status'}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -206,7 +206,7 @@ def test_unmanage(self): self.assertIsNone(sot.unmanage(self.sess)) - url = 'snapshots/%s/action' % FAKE_ID + url = f'snapshots/{FAKE_ID}/action' body = {'os-unmanage': None} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion diff --git a/openstack/tests/unit/block_storage/v3/test_transfer.py b/openstack/tests/unit/block_storage/v3/test_transfer.py index 4114ab30b..d048fd29f 100644 --- a/openstack/tests/unit/block_storage/v3/test_transfer.py +++ b/openstack/tests/unit/block_storage/v3/test_transfer.py @@ -112,7 +112,7 @@ def test_accept(self, mock_mv, mock_translate): sot.accept(self.sess, auth_key=FAKE_AUTH_KEY) self.sess.post.assert_called_with( - 'volume-transfers/%s/accept' % FAKE_TRANSFER, + f'volume-transfers/{FAKE_TRANSFER}/accept', json={ 'accept': { 'auth_key': FAKE_AUTH_KEY, @@ -134,7 +134,7 @@ def test_accept_pre_v355(self, mock_mv, mock_translate): sot.accept(self.sess, auth_key=FAKE_AUTH_KEY) self.sess.post.assert_called_with( - 'os-volume-transfer/%s/accept' % FAKE_TRANSFER, + f'os-volume-transfer/{FAKE_TRANSFER}/accept', json={ 'accept': { 'auth_key': FAKE_AUTH_KEY, diff --git a/openstack/tests/unit/block_storage/v3/test_type.py b/openstack/tests/unit/block_storage/v3/test_type.py index 19f10cd14..174d213b7 100644 --- a/openstack/tests/unit/block_storage/v3/test_type.py +++ b/openstack/tests/unit/block_storage/v3/test_type.py @@ -150,7 +150,7 @@ def test_get_private_access(self): ) self.sess.get.assert_called_with( - "types/%s/os-volume-type-access" % sot.id + f"types/{sot.id}/os-volume-type-access" ) def test_add_private_access(self): @@ -158,7 +158,7 @@ def test_add_private_access(self): self.assertIsNone(sot.add_private_access(self.sess, "a")) - url = "types/%s/action" % sot.id + url = f"types/{sot.id}/action" body = {"addProjectAccess": {"project": "a"}} self.sess.post.assert_called_with(url, json=body) @@ -167,6 +167,6 @@ def test_remove_private_access(self): self.assertIsNone(sot.remove_private_access(self.sess, "a")) - url = "types/%s/action" % sot.id + url = f"types/{sot.id}/action" body = {"removeProjectAccess": {"project": "a"}} self.sess.post.assert_called_with(url, json=body) diff --git a/openstack/tests/unit/block_storage/v3/test_volume.py b/openstack/tests/unit/block_storage/v3/test_volume.py index 8aaa4d09e..dc6f5600b 100644 --- a/openstack/tests/unit/block_storage/v3/test_volume.py +++ b/openstack/tests/unit/block_storage/v3/test_volume.py @@ -159,7 +159,7 @@ def test_extend(self): self.assertIsNone(sot.extend(self.sess, '20')) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {"os-extend": {"new_size": "20"}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -170,7 +170,7 @@ def test_set_volume_readonly(self): self.assertIsNone(sot.set_readonly(self.sess, True)) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-update_readonly_flag': {'readonly': True}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -181,7 +181,7 @@ def test_set_volume_readonly_false(self): self.assertIsNone(sot.set_readonly(self.sess, False)) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-update_readonly_flag': {'readonly': False}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -192,7 +192,7 @@ def test_set_volume_bootable(self): self.assertIsNone(sot.set_bootable_status(self.sess)) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-set_bootable': {'bootable': True}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -203,7 +203,7 @@ def test_set_volume_bootable_false(self): self.assertIsNone(sot.set_bootable_status(self.sess, False)) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-set_bootable': {'bootable': False}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -214,7 +214,7 @@ def test_set_image_metadata(self): self.assertIsNone(sot.set_image_metadata(self.sess, {'foo': 'bar'})) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-set_image_metadata': {'foo': 'bar'}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -230,7 +230,7 @@ def test_delete_image_metadata(self): self.assertIsNone(sot.delete_image_metadata(self.sess)) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body_a = {'os-unset_image_metadata': 'foo'} body_b = {'os-unset_image_metadata': 'baz'} self.sess.post.assert_has_calls( @@ -249,7 +249,7 @@ def test_delete_image_metadata_item(self): self.assertIsNone(sot.delete_image_metadata_item(self.sess, 'foo')) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-unset_image_metadata': 'foo'} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -260,7 +260,7 @@ def test_reset_status(self): self.assertIsNone(sot.reset_status(self.sess, '1', '2', '3')) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = { 'os-reset_status': { 'status': '1', @@ -277,7 +277,7 @@ def test_reset_status__single_option(self): self.assertIsNone(sot.reset_status(self.sess, status='1')) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = { 'os-reset_status': { 'status': '1', @@ -309,7 +309,7 @@ def test_revert_to_snapshot_after_340(self, mv_mock): self.assertIsNone(sot.revert_to_snapshot(self.sess, '1')) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'revert': {'snapshot_id': '1'}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -321,7 +321,7 @@ def test_attach_instance(self): self.assertIsNone(sot.attach(self.sess, '1', instance='2')) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-attach': {'mountpoint': '1', 'instance_uuid': '2'}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -332,7 +332,7 @@ def test_attach_host(self): self.assertIsNone(sot.attach(self.sess, '1', host_name='2')) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-attach': {'mountpoint': '1', 'host_name': '2'}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -348,7 +348,7 @@ def test_detach(self): self.assertIsNone(sot.detach(self.sess, '1')) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-detach': {'attachment_id': '1'}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -361,7 +361,7 @@ def test_detach_force(self): sot.detach(self.sess, '1', force=True, connector={'a': 'b'}) ) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = { 'os-force_detach': {'attachment_id': '1', 'connector': {'a': 'b'}} } @@ -374,7 +374,7 @@ def test_unmanage(self): self.assertIsNone(sot.unmanage(self.sess)) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-unmanage': None} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -385,7 +385,7 @@ def test_retype(self): self.assertIsNone(sot.retype(self.sess, '1')) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-retype': {'new_type': '1'}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -396,7 +396,7 @@ def test_retype_mp(self): self.assertIsNone(sot.retype(self.sess, '1', migration_policy='2')) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-retype': {'new_type': '1', 'migration_policy': '2'}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -407,7 +407,7 @@ def test_migrate(self): self.assertIsNone(sot.migrate(self.sess, host='1')) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-migrate_volume': {'host': '1'}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -422,7 +422,7 @@ def test_migrate_flags(self): ) ) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = { 'os-migrate_volume': { 'host': '1', @@ -448,7 +448,7 @@ def test_migrate_cluster(self, mv_mock): ) ) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = { 'os-migrate_volume': { 'cluster': '1', @@ -466,7 +466,7 @@ def test_complete_migration(self): self.assertIsNone(sot.complete_migration(self.sess, new_volume_id='1')) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = { 'os-migrate_volume_completion': {'new_volume': '1', 'error': False} } @@ -481,7 +481,7 @@ def test_complete_migration_error(self): sot.complete_migration(self.sess, new_volume_id='1', error=True) ) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = { 'os-migrate_volume_completion': {'new_volume': '1', 'error': True} } @@ -494,7 +494,7 @@ def test_force_delete(self): self.assertIsNone(sot.force_delete(self.sess)) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-force_delete': None} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -511,7 +511,7 @@ def test_upload_image(self): self.assertDictEqual({'a': 'b'}, sot.upload_to_image(self.sess, '1')) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-volume_upload_image': {'image_name': '1', 'force': False}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -543,7 +543,7 @@ def test_upload_image_args(self, mv_mock): ), ) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = { 'os-volume_upload_image': { 'image_name': '1', @@ -564,7 +564,7 @@ def test_reserve(self): self.assertIsNone(sot.reserve(self.sess)) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-reserve': None} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -575,7 +575,7 @@ def test_unreserve(self): self.assertIsNone(sot.unreserve(self.sess)) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-unreserve': None} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -586,7 +586,7 @@ def test_begin_detaching(self): self.assertIsNone(sot.begin_detaching(self.sess)) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-begin_detaching': None} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -597,7 +597,7 @@ def test_abort_detaching(self): self.assertIsNone(sot.abort_detaching(self.sess)) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-roll_detaching': None} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -615,7 +615,7 @@ def test_init_attachment(self): {'c': 'd'}, sot.init_attachment(self.sess, {'a': 'b'}) ) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-initialize_connection': {'connector': {'a': 'b'}}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -626,7 +626,7 @@ def test_terminate_attachment(self): self.assertIsNone(sot.terminate_attachment(self.sess, {'a': 'b'})) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {'os-terminate_connection': {'connector': {'a': 'b'}}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion @@ -734,6 +734,6 @@ def test_set_microversion(self): self.sess.default_microversion = '3.50' self.assertIsNone(sot.extend(self.sess, '20')) - url = 'volumes/%s/action' % FAKE_ID + url = f'volumes/{FAKE_ID}/action' body = {"os-extend": {"new_size": "20"}} self.sess.post.assert_called_with(url, json=body, microversion="3.50") diff --git a/openstack/tests/unit/cloud/test__utils.py b/openstack/tests/unit/cloud/test__utils.py index 6aac3f834..1fd1a3f3c 100644 --- a/openstack/tests/unit/cloud/test__utils.py +++ b/openstack/tests/unit/cloud/test__utils.py @@ -331,7 +331,7 @@ def test_get_entity_no_use_direct_get(self): # if the use_direct_get flag is set to False(default). uuid = uuid4().hex resource = 'network' - func = 'search_%ss' % resource + func = f'search_{resource}s' filters = {} with mock.patch.object(self.cloud, func) as search: _utils._get_entity(self.cloud, resource, uuid, filters) @@ -343,7 +343,7 @@ def test_get_entity_no_uuid_like(self): self.cloud.use_direct_get = True name = 'name_no_uuid' resource = 'network' - func = 'search_%ss' % resource + func = f'search_{resource}s' filters = {} with mock.patch.object(self.cloud, func) as search: _utils._get_entity(self.cloud, resource, name, filters) @@ -363,7 +363,7 @@ def test_get_entity_pass_uuid(self): 'security_group', ] for r in resources: - f = 'get_%s_by_id' % r + f = f'get_{r}_by_id' with mock.patch.object(self.cloud, f) as get: _utils._get_entity(self.cloud, r, uuid, {}) get.assert_called_once_with(uuid) @@ -383,7 +383,7 @@ def test_get_entity_pass_search_methods(self): filters = {} name = 'name_no_uuid' for r in resources: - f = 'search_%ss' % r + f = f'search_{r}s' with mock.patch.object(self.cloud, f) as search: _utils._get_entity(self.cloud, r, name, {}) search.assert_called_once_with(name, filters) @@ -400,5 +400,5 @@ def test_get_entity_get_and_search(self): 'security_group', ] for r in resources: - self.assertTrue(hasattr(self.cloud, 'get_%s_by_id' % r)) - self.assertTrue(hasattr(self.cloud, 'search_%ss' % r)) + self.assertTrue(hasattr(self.cloud, f'get_{r}_by_id')) + self.assertTrue(hasattr(self.cloud, f'search_{r}s')) diff --git a/openstack/tests/unit/cloud/test_availability_zones.py b/openstack/tests/unit/cloud/test_availability_zones.py index 68d67a64b..aa277f669 100644 --- a/openstack/tests/unit/cloud/test_availability_zones.py +++ b/openstack/tests/unit/cloud/test_availability_zones.py @@ -29,9 +29,7 @@ def test_list_availability_zone_names(self): [ dict( method='GET', - uri='{endpoint}/os-availability-zone'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/os-availability-zone', json=_fake_zone_list, ), ] @@ -46,9 +44,7 @@ def test_unauthorized_availability_zone_names(self): [ dict( method='GET', - uri='{endpoint}/os-availability-zone'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/os-availability-zone', status_code=403, ), ] @@ -63,9 +59,7 @@ def test_list_all_availability_zone_names(self): [ dict( method='GET', - uri='{endpoint}/os-availability-zone'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/os-availability-zone', json=_fake_zone_list, ), ] diff --git a/openstack/tests/unit/cloud/test_baremetal_node.py b/openstack/tests/unit/cloud/test_baremetal_node.py index 616fbc332..5c7fd8dca 100644 --- a/openstack/tests/unit/cloud/test_baremetal_node.py +++ b/openstack/tests/unit/cloud/test_baremetal_node.py @@ -88,7 +88,7 @@ def test_get_machine_by_mac(self): uri=self.get_mock_url( resource='ports', append=['detail'], - qs_elements=['address=%s' % mac_address], + qs_elements=[f'address={mac_address}'], ), json={ 'ports': [ @@ -2041,7 +2041,7 @@ def test_unregister_machine(self): method='GET', uri=self.get_mock_url( resource='ports', - qs_elements=['address=%s' % mac_address], + qs_elements=[f'address={mac_address}'], ), json={ 'ports': [ @@ -2129,7 +2129,7 @@ def test_unregister_machine_retries(self): method='GET', uri=self.get_mock_url( resource='ports', - qs_elements=['address=%s' % mac_address], + qs_elements=[f'address={mac_address}'], ), json={ 'ports': [ diff --git a/openstack/tests/unit/cloud/test_baremetal_ports.py b/openstack/tests/unit/cloud/test_baremetal_ports.py index 3cd4c043d..5a75676cc 100644 --- a/openstack/tests/unit/cloud/test_baremetal_ports.py +++ b/openstack/tests/unit/cloud/test_baremetal_ports.py @@ -83,7 +83,9 @@ def test_list_nics_for_machine(self): resource='ports', append=['detail'], qs_elements=[ - 'node_uuid=%s' % self.fake_baremetal_node['uuid'] + 'node_uuid={}'.format( + self.fake_baremetal_node['uuid'] + ) ], ), json={ @@ -112,7 +114,9 @@ def test_list_nics_for_machine_failure(self): resource='ports', append=['detail'], qs_elements=[ - 'node_uuid=%s' % self.fake_baremetal_node['uuid'] + 'node_uuid={}'.format( + self.fake_baremetal_node['uuid'] + ) ], ), status_code=400, @@ -136,7 +140,7 @@ def test_get_nic_by_mac(self): uri=self.get_mock_url( resource='ports', append=['detail'], - qs_elements=['address=%s' % mac], + qs_elements=[f'address={mac}'], ), json={'ports': [self.fake_baremetal_port]}, ), @@ -157,7 +161,7 @@ def test_get_nic_by_mac_failure(self): uri=self.get_mock_url( resource='ports', append=['detail'], - qs_elements=['address=%s' % mac], + qs_elements=[f'address={mac}'], ), json={'ports': []}, ), diff --git a/openstack/tests/unit/cloud/test_endpoints.py b/openstack/tests/unit/cloud/test_endpoints.py index d992e1555..01191e912 100644 --- a/openstack/tests/unit/cloud/test_endpoints.py +++ b/openstack/tests/unit/cloud/test_endpoints.py @@ -40,7 +40,7 @@ def get_mock_url( ) def _dummy_url(self): - return 'https://%s.example.com/' % uuid.uuid4().hex + return f'https://{uuid.uuid4().hex}.example.com/' def test_create_endpoint_v3(self): service_data = self._get_service_data() diff --git a/openstack/tests/unit/cloud/test_flavors.py b/openstack/tests/unit/cloud/test_flavors.py index f65dab2cd..de3601815 100644 --- a/openstack/tests/unit/cloud/test_flavors.py +++ b/openstack/tests/unit/cloud/test_flavors.py @@ -26,9 +26,7 @@ def test_create_flavor(self): [ dict( method='POST', - uri='{endpoint}/flavors'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/flavors', json={'flavor': fakes.FAKE_FLAVOR}, validate=dict( json={ @@ -64,16 +62,12 @@ def test_delete_flavor(self): [ dict( method='GET', - uri='{endpoint}/flavors/vanilla'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/flavors/vanilla', json=fakes.FAKE_FLAVOR, ), dict( method='DELETE', - uri='{endpoint}/flavors/{id}'.format( - endpoint=fakes.COMPUTE_ENDPOINT, id=fakes.FLAVOR_ID - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/flavors/{fakes.FLAVOR_ID}', ), ] ) @@ -87,16 +81,12 @@ def test_delete_flavor_not_found(self): [ dict( method='GET', - uri='{endpoint}/flavors/invalid'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/flavors/invalid', status_code=404, ), dict( method='GET', - uri='{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/flavors/detail?is_public=None', json={'flavors': fakes.FAKE_FLAVOR_LIST}, ), ] @@ -112,23 +102,17 @@ def test_delete_flavor_exception(self): [ dict( method='GET', - uri='{endpoint}/flavors/vanilla'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/flavors/vanilla', json=fakes.FAKE_FLAVOR, ), dict( method='GET', - uri='{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/flavors/detail?is_public=None', json={'flavors': fakes.FAKE_FLAVOR_LIST}, ), dict( method='DELETE', - uri='{endpoint}/flavors/{id}'.format( - endpoint=fakes.COMPUTE_ENDPOINT, id=fakes.FLAVOR_ID - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/flavors/{fakes.FLAVOR_ID}', status_code=503, ), ] @@ -145,9 +129,7 @@ def test_list_flavors(self): uris_to_mock = [ dict( method='GET', - uri='{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/flavors/detail?is_public=None', json={'flavors': fakes.FAKE_FLAVOR_LIST}, ), ] @@ -173,9 +155,7 @@ def test_list_flavors_with_extra(self): uris_to_mock = [ dict( method='GET', - uri='{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/flavors/detail?is_public=None', json={'flavors': fakes.FAKE_FLAVOR_LIST}, ), ] @@ -213,9 +193,7 @@ def test_get_flavor_by_ram(self): uris_to_mock = [ dict( method='GET', - uri='{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/flavors/detail?is_public=None', json={'flavors': fakes.FAKE_FLAVOR_LIST}, ), ] @@ -241,9 +219,7 @@ def test_get_flavor_by_ram_and_include(self): uris_to_mock = [ dict( method='GET', - uri='{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/flavors/detail?is_public=None', json={'flavors': fakes.FAKE_FLAVOR_LIST}, ), ] @@ -269,9 +245,7 @@ def test_get_flavor_by_ram_not_found(self): [ dict( method='GET', - uri='{endpoint}/flavors/detail?is_public=None'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/flavors/detail?is_public=None', json={'flavors': []}, ) ] @@ -284,8 +258,8 @@ def test_get_flavor_by_ram_not_found(self): def test_get_flavor_string_and_int(self): self.use_compute_discovery() - flavor_resource_uri = '{endpoint}/flavors/1/os-extra_specs'.format( - endpoint=fakes.COMPUTE_ENDPOINT + flavor_resource_uri = ( + f'{fakes.COMPUTE_ENDPOINT}/flavors/1/os-extra_specs' ) flavor = fakes.make_fake_flavor('1', 'vanilla') flavor_json = {'extra_specs': {}} @@ -294,9 +268,7 @@ def test_get_flavor_string_and_int(self): [ dict( method='GET', - uri='{endpoint}/flavors/1'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/flavors/1', json=flavor, ), dict(method='GET', uri=flavor_resource_uri, json=flavor_json), @@ -315,9 +287,7 @@ def test_set_flavor_specs(self): [ dict( method='POST', - uri='{endpoint}/flavors/{id}/os-extra_specs'.format( - endpoint=fakes.COMPUTE_ENDPOINT, id=1 - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/flavors/{1}/os-extra_specs', json=dict(extra_specs=extra_specs), ) ] @@ -333,9 +303,7 @@ def test_unset_flavor_specs(self): [ dict( method='DELETE', - uri='{endpoint}/flavors/{id}/os-extra_specs/{key}'.format( - endpoint=fakes.COMPUTE_ENDPOINT, id=1, key=key - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/flavors/{1}/os-extra_specs/{key}', ) for key in keys ] @@ -394,9 +362,7 @@ def test_list_flavor_access(self): [ dict( method='GET', - uri='{endpoint}/flavors/vanilla/os-flavor-access'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/flavors/vanilla/os-flavor-access', json={ 'flavor_access': [ {'flavor_id': 'vanilla', 'tenant_id': 'tenant_id'} @@ -410,9 +376,7 @@ def test_list_flavor_access(self): def test_get_flavor_by_id(self): self.use_compute_discovery() - flavor_uri = '{endpoint}/flavors/1'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ) + flavor_uri = f'{fakes.COMPUTE_ENDPOINT}/flavors/1' flavor_json = {'flavor': fakes.make_fake_flavor('1', 'vanilla')} self.register_uris( @@ -430,12 +394,8 @@ def test_get_flavor_by_id(self): def test_get_flavor_with_extra_specs(self): self.use_compute_discovery() - flavor_uri = '{endpoint}/flavors/1'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ) - flavor_extra_uri = '{endpoint}/flavors/1/os-extra_specs'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ) + flavor_uri = f'{fakes.COMPUTE_ENDPOINT}/flavors/1' + flavor_extra_uri = f'{fakes.COMPUTE_ENDPOINT}/flavors/1/os-extra_specs' flavor_json = {'flavor': fakes.make_fake_flavor('1', 'vanilla')} flavor_extra_json = {'extra_specs': {'name': 'test'}} diff --git a/openstack/tests/unit/cloud/test_floating_ip_neutron.py b/openstack/tests/unit/cloud/test_floating_ip_neutron.py index ad821cb67..17bc87516 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_neutron.py +++ b/openstack/tests/unit/cloud/test_floating_ip_neutron.py @@ -258,7 +258,7 @@ def test_get_floating_ip_by_id(self): dict( method='GET', uri='https://network.example.com/v2.0/floatingips/' - '{id}'.format(id=fid), + f'{fid}', json=self.mock_floating_ip_new_rep, ) ] diff --git a/openstack/tests/unit/cloud/test_floating_ip_pool.py b/openstack/tests/unit/cloud/test_floating_ip_pool.py index ee3d55eae..3f424c419 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_pool.py +++ b/openstack/tests/unit/cloud/test_floating_ip_pool.py @@ -31,9 +31,7 @@ def test_list_floating_ip_pools(self): [ dict( method='GET', - uri='{endpoint}/os-floating-ip-pools'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/os-floating-ip-pools', json={"floating_ip_pools": [{"name": "public"}]}, ), ] diff --git a/openstack/tests/unit/cloud/test_fwaas.py b/openstack/tests/unit/cloud/test_fwaas.py index 935e08dbf..72e03dd3d 100644 --- a/openstack/tests/unit/cloud/test_fwaas.py +++ b/openstack/tests/unit/cloud/test_fwaas.py @@ -1274,7 +1274,7 @@ def test_create_firewall_group(self): 'network', 'public', append=['v2.0', 'ports'], - qs_elements=['name=%s' % self.mock_port['name']], + qs_elements=['name={}'.format(self.mock_port['name'])], ), json={'ports': [self.mock_port]}, ), @@ -1580,7 +1580,7 @@ def test_update_firewall_group(self): 'network', 'public', append=['v2.0', 'ports'], - qs_elements=['name=%s' % self.mock_port['name']], + qs_elements=['name={}'.format(self.mock_port['name'])], ), json={'ports': [self.mock_port]}, ), diff --git a/openstack/tests/unit/cloud/test_identity_roles.py b/openstack/tests/unit/cloud/test_identity_roles.py index 1349d7ec2..344f9cbb2 100644 --- a/openstack/tests/unit/cloud/test_identity_roles.py +++ b/openstack/tests/unit/cloud/test_identity_roles.py @@ -286,8 +286,8 @@ def test_list_role_assignments_filters(self): uri=self.get_mock_url( resource='role_assignments', qs_elements=[ - 'scope.domain.id=%s' % domain_data.domain_id, - 'user.id=%s' % user_data.user_id, + f'scope.domain.id={domain_data.domain_id}', + f'user.id={user_data.user_id}', 'effective=True', ], ), diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 18fc33853..07748031c 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -89,16 +89,12 @@ def test_download_image_no_images_found(self): [ dict( method='GET', - uri='https://image.example.com/v2/images/{name}'.format( - name=self.image_name - ), + uri=f'https://image.example.com/v2/images/{self.image_name}', status_code=404, ), dict( method='GET', - uri='https://image.example.com/v2/images?name={name}'.format( # noqa: E501 - name=self.image_name - ), + uri=f'https://image.example.com/v2/images?name={self.image_name}', # noqa: E501 json=dict(images=[]), ), dict( @@ -121,23 +117,17 @@ def _register_image_mocks(self): [ dict( method='GET', - uri='https://image.example.com/v2/images/{name}'.format( - name=self.image_name - ), + uri=f'https://image.example.com/v2/images/{self.image_name}', status_code=404, ), dict( method='GET', - uri='https://image.example.com/v2/images?name={name}'.format( # noqa: E501 - name=self.image_name - ), + uri=f'https://image.example.com/v2/images?name={self.image_name}', # noqa: E501 json=self.fake_search_return, ), dict( method='GET', - uri='https://image.example.com/v2/images/{id}/file'.format( - id=self.image_id - ), + uri=f'https://image.example.com/v2/images/{self.image_id}/file', content=self.output, headers={ 'Content-Type': 'application/octet-stream', @@ -417,9 +407,7 @@ def test_list_images_paginated(self): ), json={ 'images': [self.fake_image_dict], - 'next': '/v2/images?marker={marker}'.format( - marker=marker - ), + 'next': f'/v2/images?marker={marker}', }, ), dict( @@ -821,16 +809,12 @@ def test_create_image_task(self): ), dict( method='HEAD', - uri='{endpoint}/{container}'.format( - endpoint=endpoint, container=self.container_name - ), + uri=f'{endpoint}/{self.container_name}', status_code=404, ), dict( method='PUT', - uri='{endpoint}/{container}'.format( - endpoint=endpoint, container=self.container_name - ), + uri=f'{endpoint}/{self.container_name}', status_code=201, headers={ 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', @@ -840,9 +824,7 @@ def test_create_image_task(self): ), dict( method='HEAD', - uri='{endpoint}/{container}'.format( - endpoint=endpoint, container=self.container_name - ), + uri=f'{endpoint}/{self.container_name}', headers={ 'Content-Length': '0', 'X-Container-Object-Count': '0', @@ -867,20 +849,12 @@ def test_create_image_task(self): ), dict( method='HEAD', - uri='{endpoint}/{container}/{object}'.format( - endpoint=endpoint, - container=self.container_name, - object=self.image_name, - ), + uri=f'{endpoint}/{self.container_name}/{self.image_name}', status_code=404, ), dict( method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=endpoint, - container=self.container_name, - object=self.image_name, - ), + uri=f'{endpoint}/{self.container_name}/{self.image_name}', status_code=201, validate=dict( headers={ @@ -903,10 +877,7 @@ def test_create_image_task(self): json=dict( type='import', input={ - 'import_from': '{container}/{object}'.format( - container=self.container_name, - object=self.image_name, - ), + 'import_from': f'{self.container_name}/{self.image_name}', 'image_properties': {'name': self.image_name}, }, ) @@ -952,10 +923,7 @@ def test_create_image_task(self): [ { 'op': 'add', - 'value': '{container}/{object}'.format( - container=self.container_name, - object=self.image_name, - ), + 'value': f'{self.container_name}/{self.image_name}', 'path': '/owner_specified.openstack.object', # noqa: E501 }, { @@ -983,11 +951,7 @@ def test_create_image_task(self): ), dict( method='HEAD', - uri='{endpoint}/{container}/{object}'.format( - endpoint=endpoint, - container=self.container_name, - object=self.image_name, - ), + uri=f'{endpoint}/{self.container_name}/{self.image_name}', headers={ 'X-Timestamp': '1429036140.50253', 'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1', @@ -1007,11 +971,7 @@ def test_create_image_task(self): ), dict( method='DELETE', - uri='{endpoint}/{container}/{object}'.format( - endpoint=endpoint, - container=self.container_name, - object=self.image_name, - ), + uri=f'{endpoint}/{self.container_name}/{self.image_name}', ), dict( method='GET', @@ -1069,15 +1029,11 @@ def test_delete_image_task(self): ), dict( method='DELETE', - uri='https://image.example.com/v2/images/{id}'.format( - id=self.image_id - ), + uri=f'https://image.example.com/v2/images/{self.image_id}', ), dict( method='HEAD', - uri='{endpoint}/{object}'.format( - endpoint=endpoint, object=object_path - ), + uri=f'{endpoint}/{object_path}', headers={ 'X-Timestamp': '1429036140.50253', 'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1', @@ -1097,9 +1053,7 @@ def test_delete_image_task(self): ), dict( method='DELETE', - uri='{endpoint}/{object}'.format( - endpoint=endpoint, object=object_path - ), + uri=f'{endpoint}/{object_path}', ), ] ) @@ -1187,11 +1141,7 @@ def test_delete_autocreated_image_objects(self): ), dict( method='DELETE', - uri='{endpoint}/{container}/{object}'.format( - endpoint=endpoint, - container=self.container_name, - object=self.image_name, - ), + uri=f'{endpoint}/{self.container_name}/{self.image_name}', ), ] ) @@ -1230,9 +1180,7 @@ def test_create_image_put_v1(self): 'properties': { 'owner_specified.openstack.md5': fakes.NO_MD5, 'owner_specified.openstack.sha256': fakes.NO_SHA256, - 'owner_specified.openstack.object': 'images/{name}'.format( - name=self.image_name - ), + 'owner_specified.openstack.object': f'images/{self.image_name}', 'is_public': False, }, } @@ -1263,9 +1211,7 @@ def test_create_image_put_v1(self): ), dict( method='PUT', - uri='https://image.example.com/v1/images/{id}'.format( - id=self.image_id - ), + uri=f'https://image.example.com/v1/images/{self.image_id}', json={'image': ret}, validate=dict( headers={ @@ -1297,9 +1243,7 @@ def test_create_image_put_v1_bad_delete(self): 'properties': { 'owner_specified.openstack.md5': fakes.NO_MD5, 'owner_specified.openstack.sha256': fakes.NO_SHA256, - 'owner_specified.openstack.object': 'images/{name}'.format( - name=self.image_name - ), + 'owner_specified.openstack.object': f'images/{self.image_name}', 'is_public': False, }, 'validate_checksum': True, @@ -1331,9 +1275,7 @@ def test_create_image_put_v1_bad_delete(self): ), dict( method='PUT', - uri='https://image.example.com/v1/images/{id}'.format( - id=self.image_id - ), + uri=f'https://image.example.com/v1/images/{self.image_id}', status_code=400, validate=dict( headers={ @@ -1344,9 +1286,7 @@ def test_create_image_put_v1_bad_delete(self): ), dict( method='DELETE', - uri='https://image.example.com/v1/images/{id}'.format( - id=self.image_id - ), + uri=f'https://image.example.com/v1/images/{self.image_id}', json={'images': [ret]}, ), ] @@ -1369,9 +1309,7 @@ def test_update_image_no_patch(self): 'disk_format': 'qcow2', 'owner_specified.openstack.md5': fakes.NO_MD5, 'owner_specified.openstack.sha256': fakes.NO_SHA256, - 'owner_specified.openstack.object': 'images/{name}'.format( - name=self.image_name - ), + 'owner_specified.openstack.object': f'images/{self.image_name}', 'visibility': 'private', } @@ -1382,9 +1320,7 @@ def test_update_image_no_patch(self): self.cloud.update_image_properties( image=image.Image.existing(**ret), **{ - 'owner_specified.openstack.object': 'images/{name}'.format( - name=self.image_name - ) + 'owner_specified.openstack.object': f'images/{self.image_name}' }, ) @@ -1399,9 +1335,7 @@ def test_create_image_put_v2_bad_delete(self): 'disk_format': 'qcow2', 'owner_specified.openstack.md5': fakes.NO_MD5, 'owner_specified.openstack.sha256': fakes.NO_SHA256, - 'owner_specified.openstack.object': 'images/{name}'.format( - name=self.image_name - ), + 'owner_specified.openstack.object': f'images/{self.image_name}', 'visibility': 'private', } @@ -1449,9 +1383,7 @@ def test_create_image_put_v2_bad_delete(self): ), dict( method='PUT', - uri='https://image.example.com/v2/images/{id}/file'.format( - id=self.image_id - ), + uri=f'https://image.example.com/v2/images/{self.image_id}/file', status_code=400, validate=dict( headers={ @@ -1461,9 +1393,7 @@ def test_create_image_put_v2_bad_delete(self): ), dict( method='DELETE', - uri='https://image.example.com/v2/images/{id}'.format( - id=self.image_id - ), + uri=f'https://image.example.com/v2/images/{self.image_id}', ), ] ) @@ -1530,9 +1460,7 @@ def test_create_image_put_v2_wrong_checksum_delete(self): ), dict( method='DELETE', - uri='https://image.example.com/v2/images/{id}'.format( - id=self.image_id - ), + uri=f'https://image.example.com/v2/images/{self.image_id}', ), ] ) @@ -1574,9 +1502,7 @@ def test_create_image_put_user_int(self): 'disk_format': 'qcow2', 'owner_specified.openstack.md5': fakes.NO_MD5, 'owner_specified.openstack.sha256': fakes.NO_SHA256, - 'owner_specified.openstack.object': 'images/{name}'.format( - name=self.image_name - ), + 'owner_specified.openstack.object': f'images/{self.image_name}', 'int_v': '12345', 'visibility': 'private', 'min_disk': 0, @@ -1627,9 +1553,7 @@ def test_create_image_put_user_int(self): ), dict( method='PUT', - uri='https://image.example.com/v2/images/{id}/file'.format( - id=self.image_id - ), + uri=f'https://image.example.com/v2/images/{self.image_id}/file', validate=dict( headers={ 'Content-Type': 'application/octet-stream', @@ -1638,9 +1562,7 @@ def test_create_image_put_user_int(self): ), dict( method='GET', - uri='https://image.example.com/v2/images/{id}'.format( - id=self.image_id - ), + uri=f'https://image.example.com/v2/images/{self.image_id}', json=ret, ), dict( @@ -1667,9 +1589,7 @@ def test_create_image_put_meta_int(self): 'disk_format': 'qcow2', 'owner_specified.openstack.md5': fakes.NO_MD5, 'owner_specified.openstack.sha256': fakes.NO_SHA256, - 'owner_specified.openstack.object': 'images/{name}'.format( - name=self.image_name - ), + 'owner_specified.openstack.object': f'images/{self.image_name}', 'int_v': 12345, 'visibility': 'private', 'min_disk': 0, @@ -1721,9 +1641,7 @@ def test_create_image_put_meta_int(self): ), dict( method='PUT', - uri='https://image.example.com/v2/images/{id}/file'.format( - id=self.image_id - ), + uri=f'https://image.example.com/v2/images/{self.image_id}/file', validate=dict( headers={ 'Content-Type': 'application/octet-stream', @@ -1732,9 +1650,7 @@ def test_create_image_put_meta_int(self): ), dict( method='GET', - uri='https://image.example.com/v2/images/{id}'.format( - id=self.image_id - ), + uri=f'https://image.example.com/v2/images/{self.image_id}', json=ret, ), dict( @@ -1761,9 +1677,7 @@ def test_create_image_put_protected(self): 'disk_format': 'qcow2', 'owner_specified.openstack.md5': fakes.NO_MD5, 'owner_specified.openstack.sha256': fakes.NO_SHA256, - 'owner_specified.openstack.object': 'images/{name}'.format( - name=self.image_name - ), + 'owner_specified.openstack.object': f'images/{self.image_name}', 'int_v': '12345', 'protected': False, 'visibility': 'private', @@ -1816,9 +1730,7 @@ def test_create_image_put_protected(self): ), dict( method='PUT', - uri='https://image.example.com/v2/images/{id}/file'.format( - id=self.image_id - ), + uri=f'https://image.example.com/v2/images/{self.image_id}/file', validate=dict( headers={ 'Content-Type': 'application/octet-stream', @@ -1827,9 +1739,7 @@ def test_create_image_put_protected(self): ), dict( method='GET', - uri='https://image.example.com/v2/images/{id}'.format( - id=self.image_id - ), + uri=f'https://image.example.com/v2/images/{self.image_id}', json=ret, ), dict( @@ -1892,9 +1802,7 @@ def test_list_images_paginated(self): ), json={ 'images': [self.fake_image_dict], - 'next': '/v2/images?marker={marker}'.format( - marker=marker - ), + 'next': f'/v2/images?marker={marker}', }, ), dict( diff --git a/openstack/tests/unit/cloud/test_image_snapshot.py b/openstack/tests/unit/cloud/test_image_snapshot.py index bf497f794..a0d582015 100644 --- a/openstack/tests/unit/cloud/test_image_snapshot.py +++ b/openstack/tests/unit/cloud/test_image_snapshot.py @@ -37,10 +37,7 @@ def test_create_image_snapshot_wait_until_active_never_active(self): self.get_nova_discovery_mock_dict(), dict( method='POST', - uri='{endpoint}/servers/{server_id}/action'.format( - endpoint=fakes.COMPUTE_ENDPOINT, - server_id=self.server_id, - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/servers/{self.server_id}/action', headers=dict( Location='{endpoint}/images/{image_id}'.format( endpoint='https://images.example.com', @@ -87,10 +84,7 @@ def test_create_image_snapshot_wait_active(self): self.get_nova_discovery_mock_dict(), dict( method='POST', - uri='{endpoint}/servers/{server_id}/action'.format( - endpoint=fakes.COMPUTE_ENDPOINT, - server_id=self.server_id, - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/servers/{self.server_id}/action', headers=dict( Location='{endpoint}/images/{image_id}'.format( endpoint='https://images.example.com', diff --git a/openstack/tests/unit/cloud/test_meta.py b/openstack/tests/unit/cloud/test_meta.py index 7b40371d5..58cbd162e 100644 --- a/openstack/tests/unit/cloud/test_meta.py +++ b/openstack/tests/unit/cloud/test_meta.py @@ -530,9 +530,7 @@ def test_get_server_private_ip_devstack( ), dict( method='GET', - uri='{endpoint}/servers/test-id/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/servers/test-id/os-security-groups', json={'security_groups': []}, ), ] @@ -609,9 +607,7 @@ def test_get_server_private_ip_no_fip( ), dict( method='GET', - uri='{endpoint}/servers/test-id/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/servers/test-id/os-security-groups', json={'security_groups': []}, ), ] @@ -685,9 +681,7 @@ def test_get_server_cloud_no_fips( ), dict( method='GET', - uri='{endpoint}/servers/test-id/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/servers/test-id/os-security-groups', json={'security_groups': []}, ), ] @@ -804,9 +798,7 @@ def test_get_server_cloud_missing_fips( ), dict( method='GET', - uri='{endpoint}/servers/test-id/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/servers/test-id/os-security-groups', json={'security_groups': []}, ), ] @@ -865,9 +857,7 @@ def test_get_server_cloud_rackspace_v6( ), dict( method='GET', - uri='{endpoint}/servers/test-id/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/servers/test-id/os-security-groups', json={'security_groups': []}, ), ] @@ -947,9 +937,7 @@ def test_get_server_cloud_osic_split( ), dict( method='GET', - uri='{endpoint}/servers/test-id/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/servers/test-id/os-security-groups', json={'security_groups': []}, ), ] diff --git a/openstack/tests/unit/cloud/test_network.py b/openstack/tests/unit/cloud/test_network.py index 98d0c8646..fe460ab14 100644 --- a/openstack/tests/unit/cloud/test_network.py +++ b/openstack/tests/unit/cloud/test_network.py @@ -373,7 +373,7 @@ def test_update_network_provider(self): 'network', 'public', append=['v2.0', 'networks'], - qs_elements=['name=%s' % network_name], + qs_elements=[f'name={network_name}'], ), json={'networks': [network]}, ), @@ -574,7 +574,7 @@ def test_delete_network(self): 'network', 'public', append=['v2.0', 'networks'], - qs_elements=['name=%s' % network_name], + qs_elements=[f'name={network_name}'], ), json={'networks': [network]}, ), @@ -640,7 +640,7 @@ def test_delete_network_exception(self): 'network', 'public', append=['v2.0', 'networks'], - qs_elements=['name=%s' % network_name], + qs_elements=[f'name={network_name}'], ), json={'networks': [network]}, ), diff --git a/openstack/tests/unit/cloud/test_object.py b/openstack/tests/unit/cloud/test_object.py index 28a85fb89..88d58f8ce 100644 --- a/openstack/tests/unit/cloud/test_object.py +++ b/openstack/tests/unit/cloud/test_object.py @@ -33,12 +33,8 @@ def setUp(self): self.container = self.getUniqueString() self.object = self.getUniqueString() self.endpoint = self.cloud.object_store.get_endpoint() - self.container_endpoint = '{endpoint}/{container}'.format( - endpoint=self.endpoint, container=self.container - ) - self.object_endpoint = '{endpoint}/{object}'.format( - endpoint=self.container_endpoint, object=self.object - ) + self.container_endpoint = f'{self.endpoint}/{self.container}' + self.object_endpoint = f'{self.container_endpoint}/{self.object}' def _compare_containers(self, exp, real): self.assertDictEqual( @@ -330,7 +326,7 @@ def test_get_container_access_not_found(self): ) with testtools.ExpectedException( exceptions.SDKException, - "Container not found: %s" % self.container, + f"Container not found: {self.container}", ): self.cloud.get_container_access(self.container) @@ -594,9 +590,7 @@ def test_set_container_temp_url_key_secondary(self): self.assert_calls() def test_list_objects(self): - endpoint = '{endpoint}?format=json'.format( - endpoint=self.container_endpoint - ) + endpoint = f'{self.container_endpoint}?format=json' objects = [ { @@ -619,9 +613,7 @@ def test_list_objects(self): self._compare_objects(a, b) def test_list_objects_with_prefix(self): - endpoint = '{endpoint}?format=json&prefix=test'.format( - endpoint=self.container_endpoint - ) + endpoint = f'{self.container_endpoint}?format=json&prefix=test' objects = [ { @@ -644,9 +636,7 @@ def test_list_objects_with_prefix(self): self._compare_objects(a, b) def test_list_objects_exception(self): - endpoint = '{endpoint}?format=json'.format( - endpoint=self.container_endpoint - ) + endpoint = f'{self.container_endpoint}?format=json' self.register_uris( [ dict( @@ -903,20 +893,12 @@ def test_create_object(self): ), dict( method='HEAD', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}', status_code=404, ), dict( method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}', status_code=201, validate=dict( headers={ @@ -972,11 +954,7 @@ def test_create_directory_marker_object(self): [ dict( method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}', status_code=201, validate=dict( headers={ @@ -1008,11 +986,7 @@ def test_create_dynamic_large_object(self): ), dict( method='HEAD', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}', status_code=404, ), ] @@ -1021,12 +995,7 @@ def test_create_dynamic_large_object(self): [ dict( method='PUT', - uri='{endpoint}/{container}/{object}/{index:0>6}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - index=index, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}/{index:0>6}', status_code=201, ) for index, offset in enumerate( @@ -1038,17 +1007,11 @@ def test_create_dynamic_large_object(self): uris_to_mock.append( dict( method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}', status_code=201, validate=dict( headers={ - 'x-object-manifest': '{container}/{object}'.format( - container=self.container, object=self.object - ), + 'x-object-manifest': f'{self.container}/{self.object}', 'x-object-meta-x-sdk-md5': self.md5, 'x-object-meta-x-sdk-sha256': self.sha256, } @@ -1088,11 +1051,7 @@ def test_create_static_large_object(self): ), dict( method='HEAD', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}', status_code=404, ), ] @@ -1101,12 +1060,7 @@ def test_create_static_large_object(self): [ dict( method='PUT', - uri='{endpoint}/{container}/{object}/{index:0>6}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - index=index, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}/{index:0>6}', status_code=201, headers=dict(Etag=f'etag{index}'), ) @@ -1119,11 +1073,7 @@ def test_create_static_large_object(self): uris_to_mock.append( dict( method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}', status_code=201, validate=dict( params={'multipart-manifest', 'put'}, @@ -1153,37 +1103,27 @@ def test_create_static_large_object(self): 'header mismatch in manifest call', ) - base_object = '/{container}/{object}'.format( - container=self.container, object=self.object - ) + base_object = f'/{self.container}/{self.object}' self.assertEqual( [ { - 'path': "{base_object}/000000".format( - base_object=base_object - ), + 'path': f"{base_object}/000000", 'size_bytes': 25, 'etag': 'etag0', }, { - 'path': "{base_object}/000001".format( - base_object=base_object - ), + 'path': f"{base_object}/000001", 'size_bytes': 25, 'etag': 'etag1', }, { - 'path': "{base_object}/000002".format( - base_object=base_object - ), + 'path': f"{base_object}/000002", 'size_bytes': 25, 'etag': 'etag2', }, { - 'path': "{base_object}/000003".format( - base_object=base_object - ), + 'path': f"{base_object}/000003", 'size_bytes': len(self.object) - 75, 'etag': 'etag3', }, @@ -1210,11 +1150,7 @@ def test_slo_manifest_retry(self): ), dict( method='HEAD', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}', status_code=404, ), ] @@ -1223,12 +1159,7 @@ def test_slo_manifest_retry(self): [ dict( method='PUT', - uri='{endpoint}/{container}/{object}/{index:0>6}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - index=index, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}/{index:0>6}', status_code=201, headers=dict(Etag=f'etag{index}'), ) @@ -1243,11 +1174,7 @@ def test_slo_manifest_retry(self): [ dict( method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}', status_code=400, validate=dict( params={'multipart-manifest', 'put'}, @@ -1259,11 +1186,7 @@ def test_slo_manifest_retry(self): ), dict( method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}', status_code=400, validate=dict( params={'multipart-manifest', 'put'}, @@ -1275,11 +1198,7 @@ def test_slo_manifest_retry(self): ), dict( method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}', status_code=201, validate=dict( params={'multipart-manifest', 'put'}, @@ -1311,37 +1230,27 @@ def test_slo_manifest_retry(self): 'header mismatch in manifest call', ) - base_object = '/{container}/{object}'.format( - container=self.container, object=self.object - ) + base_object = f'/{self.container}/{self.object}' self.assertEqual( [ { - 'path': "{base_object}/000000".format( - base_object=base_object - ), + 'path': f"{base_object}/000000", 'size_bytes': 25, 'etag': 'etag0', }, { - 'path': "{base_object}/000001".format( - base_object=base_object - ), + 'path': f"{base_object}/000001", 'size_bytes': 25, 'etag': 'etag1', }, { - 'path': "{base_object}/000002".format( - base_object=base_object - ), + 'path': f"{base_object}/000002", 'size_bytes': 25, 'etag': 'etag2', }, { - 'path': "{base_object}/000003".format( - base_object=base_object - ), + 'path': f"{base_object}/000003", 'size_bytes': len(self.object) - 75, 'etag': 'etag3', }, @@ -1369,11 +1278,7 @@ def test_slo_manifest_fail(self): ), dict( method='HEAD', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}', status_code=404, ), ] @@ -1382,12 +1287,7 @@ def test_slo_manifest_fail(self): [ dict( method='PUT', - uri='{endpoint}/{container}/{object}/{index:0>6}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - index=index, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}/{index:0>6}', status_code=201, headers=dict(Etag=f'etag{index}'), ) @@ -1402,11 +1302,7 @@ def test_slo_manifest_fail(self): [ dict( method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}', status_code=400, validate=dict( params={'multipart-manifest', 'put'}, @@ -1418,11 +1314,7 @@ def test_slo_manifest_fail(self): ), dict( method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}', status_code=400, validate=dict( params={'multipart-manifest', 'put'}, @@ -1434,11 +1326,7 @@ def test_slo_manifest_fail(self): ), dict( method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}', status_code=400, validate=dict( params={'multipart-manifest', 'put'}, @@ -1459,9 +1347,7 @@ def test_slo_manifest_fail(self): [ dict( method='GET', - uri='{endpoint}/images?format=json&prefix={prefix}'.format( - endpoint=self.endpoint, prefix=self.object - ), + uri=f'{self.endpoint}/images?format=json&prefix={self.object}', complete_qs=True, json=[ { @@ -1475,9 +1361,7 @@ def test_slo_manifest_fail(self): ), dict( method='HEAD', - uri='{endpoint}/images/{object}'.format( - endpoint=self.endpoint, object=self.object - ), + uri=f'{self.endpoint}/images/{self.object}', headers={ 'X-Timestamp': '1429036140.50253', 'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1', @@ -1495,9 +1379,7 @@ def test_slo_manifest_fail(self): ), dict( method='DELETE', - uri='{endpoint}/images/{object}'.format( - endpoint=self.endpoint, object=self.object - ), + uri=f'{self.endpoint}/images/{self.object}', ), ] ) @@ -1536,56 +1418,32 @@ def test_object_segment_retry_failure(self): ), dict( method='HEAD', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}', status_code=404, ), dict( method='PUT', - uri='{endpoint}/{container}/{object}/000000'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}/000000', status_code=201, ), dict( method='PUT', - uri='{endpoint}/{container}/{object}/000001'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}/000001', status_code=201, ), dict( method='PUT', - uri='{endpoint}/{container}/{object}/000002'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}/000002', status_code=201, ), dict( method='PUT', - uri='{endpoint}/{container}/{object}/000003'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}/000003', status_code=501, ), dict( method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}', status_code=201, ), ] @@ -1619,69 +1477,41 @@ def test_object_segment_retries(self): ), dict( method='HEAD', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}', status_code=404, ), dict( method='PUT', - uri='{endpoint}/{container}/{object}/000000'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}/000000', headers={'etag': 'etag0'}, status_code=201, ), dict( method='PUT', - uri='{endpoint}/{container}/{object}/000001'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}/000001', headers={'etag': 'etag1'}, status_code=201, ), dict( method='PUT', - uri='{endpoint}/{container}/{object}/000002'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}/000002', headers={'etag': 'etag2'}, status_code=201, ), dict( method='PUT', - uri='{endpoint}/{container}/{object}/000003'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}/000003', status_code=501, ), dict( method='PUT', - uri='{endpoint}/{container}/{object}/000003'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}/000003', status_code=201, headers={'etag': 'etag3'}, ), dict( method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}', status_code=201, validate=dict( params={'multipart-manifest', 'put'}, @@ -1711,37 +1541,27 @@ def test_object_segment_retries(self): 'header mismatch in manifest call', ) - base_object = '/{container}/{object}'.format( - container=self.container, object=self.object - ) + base_object = f'/{self.container}/{self.object}' self.assertEqual( [ { - 'path': "{base_object}/000000".format( - base_object=base_object - ), + 'path': f"{base_object}/000000", 'size_bytes': 25, 'etag': 'etag0', }, { - 'path': "{base_object}/000001".format( - base_object=base_object - ), + 'path': f"{base_object}/000001", 'size_bytes': 25, 'etag': 'etag1', }, { - 'path': "{base_object}/000002".format( - base_object=base_object - ), + 'path': f"{base_object}/000002", 'size_bytes': 25, 'etag': 'etag2', }, { - 'path': "{base_object}/000003".format( - base_object=base_object - ), + 'path': f"{base_object}/000003", 'size_bytes': len(self.object) - 75, 'etag': 'etag3', }, @@ -1762,20 +1582,12 @@ def test_create_object_skip_checksum(self): ), dict( method='HEAD', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}', status_code=200, ), dict( method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}', status_code=201, validate=dict(headers={}), ), @@ -1796,11 +1608,7 @@ def test_create_object_data(self): [ dict( method='PUT', - uri='{endpoint}/{container}/{object}'.format( - endpoint=self.endpoint, - container=self.container, - object=self.object, - ), + uri=f'{self.endpoint}/{self.container}/{self.object}', status_code=201, validate=dict( headers={}, diff --git a/openstack/tests/unit/cloud/test_operator.py b/openstack/tests/unit/cloud/test_operator.py index 1487d3208..e92ddab83 100644 --- a/openstack/tests/unit/cloud/test_operator.py +++ b/openstack/tests/unit/cloud/test_operator.py @@ -91,8 +91,8 @@ def side_effect(*args, **kwargs): self.cloud.config.config['region_name'] = 'testregion' with testtools.ExpectedException( exceptions.SDKException, - "Error getting image endpoint on testcloud:testregion:" - " No service", + "Error getting image endpoint on testcloud:testregion: " + "No service", ): self.cloud.get_session_endpoint("image") diff --git a/openstack/tests/unit/cloud/test_port.py b/openstack/tests/unit/cloud/test_port.py index d184ca440..da82cb2d0 100644 --- a/openstack/tests/unit/cloud/test_port.py +++ b/openstack/tests/unit/cloud/test_port.py @@ -507,7 +507,7 @@ def test_delete_subnet_multiple_found(self): 'network', 'public', append=['v2.0', 'ports'], - qs_elements=['name=%s' % port_name], + qs_elements=[f'name={port_name}'], ), json={'ports': [port1, port2]}, ), diff --git a/openstack/tests/unit/cloud/test_project.py b/openstack/tests/unit/cloud/test_project.py index cbd7d2971..8afa40b67 100644 --- a/openstack/tests/unit/cloud/test_project.py +++ b/openstack/tests/unit/cloud/test_project.py @@ -179,7 +179,7 @@ def test_list_projects_v3(self): method='GET', uri=self.get_mock_url( resource=( - 'projects?domain_id=%s' % project_data.domain_id + f'projects?domain_id={project_data.domain_id}' ) ), status_code=200, @@ -204,7 +204,7 @@ def test_list_projects_v3_kwarg(self): method='GET', uri=self.get_mock_url( resource=( - 'projects?domain_id=%s' % project_data.domain_id + f'projects?domain_id={project_data.domain_id}' ) ), status_code=200, @@ -250,7 +250,7 @@ def test_list_projects_search_compat_v3(self): method='GET', uri=self.get_mock_url( resource=( - 'projects?domain_id=%s' % project_data.domain_id + f'projects?domain_id={project_data.domain_id}' ) ), status_code=200, diff --git a/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py b/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py index 1a88497d2..3cdffd4e5 100644 --- a/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/unit/cloud/test_qos_bandwidth_limit_rule.py @@ -104,7 +104,7 @@ def test_get_qos_bandwidth_limit_rule(self): 'network', 'public', append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name], + qs_elements=[f'name={self.policy_name}'], ), json={'policies': [self.mock_policy]}, ), @@ -157,7 +157,7 @@ def test_get_qos_bandwidth_limit_rule_no_qos_policy_found(self): 'network', 'public', append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name], + qs_elements=[f'name={self.policy_name}'], ), json={'policies': []}, ), @@ -216,7 +216,7 @@ def test_create_qos_bandwidth_limit_rule(self): 'network', 'public', append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name], + qs_elements=[f'name={self.policy_name}'], ), json={'policies': [self.mock_policy]}, ), @@ -288,7 +288,7 @@ def test_create_qos_bandwidth_limit_rule_no_qos_direction_extension(self): 'network', 'public', append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name], + qs_elements=[f'name={self.policy_name}'], ), json={'policies': [self.mock_policy]}, ), @@ -516,7 +516,7 @@ def test_delete_qos_bandwidth_limit_rule(self): 'network', 'public', append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name], + qs_elements=[f'name={self.policy_name}'], ), json={'policies': [self.mock_policy]}, ), @@ -590,7 +590,7 @@ def test_delete_qos_bandwidth_limit_rule_not_found(self): 'network', 'public', append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name], + qs_elements=[f'name={self.policy_name}'], ), json={'policies': [self.mock_policy]}, ), diff --git a/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py b/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py index 30af9b7cc..c3e1fe94e 100644 --- a/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py +++ b/openstack/tests/unit/cloud/test_qos_dscp_marking_rule.py @@ -87,7 +87,7 @@ def test_get_qos_dscp_marking_rule(self): 'network', 'public', append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name], + qs_elements=[f'name={self.policy_name}'], ), json={'policies': [self.mock_policy]}, ), @@ -140,7 +140,7 @@ def test_get_qos_dscp_marking_rule_no_qos_policy_found(self): 'network', 'public', append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name], + qs_elements=[f'name={self.policy_name}'], ), json={'policies': []}, ), @@ -199,7 +199,7 @@ def test_create_qos_dscp_marking_rule(self): 'network', 'public', append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name], + qs_elements=[f'name={self.policy_name}'], ), json={'policies': [self.mock_policy]}, ), @@ -361,7 +361,7 @@ def test_delete_qos_dscp_marking_rule(self): 'network', 'public', append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name], + qs_elements=[f'name={self.policy_name}'], ), json={'policies': [self.mock_policy]}, ), @@ -435,7 +435,7 @@ def test_delete_qos_dscp_marking_rule_not_found(self): 'network', 'public', append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name], + qs_elements=[f'name={self.policy_name}'], ), json={'policies': [self.mock_policy]}, ), diff --git a/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py b/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py index ac04d8d5b..5ef881371 100644 --- a/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py +++ b/openstack/tests/unit/cloud/test_qos_minimum_bandwidth_rule.py @@ -88,7 +88,7 @@ def test_get_qos_minimum_bandwidth_rule(self): 'network', 'public', append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name], + qs_elements=[f'name={self.policy_name}'], ), json={'policies': [self.mock_policy]}, ), @@ -141,7 +141,7 @@ def test_get_qos_minimum_bandwidth_rule_no_qos_policy_found(self): 'network', 'public', append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name], + qs_elements=[f'name={self.policy_name}'], ), json={'policies': []}, ), @@ -200,7 +200,7 @@ def test_create_qos_minimum_bandwidth_rule(self): 'network', 'public', append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name], + qs_elements=[f'name={self.policy_name}'], ), json={'policies': [self.mock_policy]}, ), @@ -361,7 +361,7 @@ def test_delete_qos_minimum_bandwidth_rule(self): 'network', 'public', append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name], + qs_elements=[f'name={self.policy_name}'], ), json={'policies': [self.mock_policy]}, ), @@ -435,7 +435,7 @@ def test_delete_qos_minimum_bandwidth_rule_not_found(self): 'network', 'public', append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name], + qs_elements=[f'name={self.policy_name}'], ), json={'policies': [self.mock_policy]}, ), diff --git a/openstack/tests/unit/cloud/test_qos_policy.py b/openstack/tests/unit/cloud/test_qos_policy.py index bec3e3bee..534633e61 100644 --- a/openstack/tests/unit/cloud/test_qos_policy.py +++ b/openstack/tests/unit/cloud/test_qos_policy.py @@ -86,7 +86,7 @@ def test_get_qos_policy(self): 'network', 'public', append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name], + qs_elements=[f'name={self.policy_name}'], ), json={'policies': [self.mock_policy]}, ), @@ -225,7 +225,7 @@ def test_delete_qos_policy(self): 'network', 'public', append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name], + qs_elements=[f'name={self.policy_name}'], ), json={'policies': [self.mock_policy]}, ), @@ -323,7 +323,7 @@ def test_delete_qos_policy_multiple_found(self): 'network', 'public', append=['v2.0', 'qos', 'policies'], - qs_elements=['name=%s' % self.policy_name], + qs_elements=[f'name={self.policy_name}'], ), json={'policies': [policy1, policy2]}, ), diff --git a/openstack/tests/unit/cloud/test_router.py b/openstack/tests/unit/cloud/test_router.py index ab67ac385..0d3064561 100644 --- a/openstack/tests/unit/cloud/test_router.py +++ b/openstack/tests/unit/cloud/test_router.py @@ -100,7 +100,7 @@ def test_get_router(self): 'network', 'public', append=['v2.0', 'routers'], - qs_elements=['name=%s' % self.router_name], + qs_elements=[f'name={self.router_name}'], ), json={'routers': [self.mock_router_rep]}, ), @@ -450,7 +450,7 @@ def test_delete_router(self): 'network', 'public', append=['v2.0', 'routers'], - qs_elements=['name=%s' % self.router_name], + qs_elements=[f'name={self.router_name}'], ), json={'routers': [self.mock_router_rep]}, ), @@ -486,7 +486,7 @@ def test_delete_router_not_found(self): 'network', 'public', append=['v2.0', 'routers'], - qs_elements=['name=%s' % self.router_name], + qs_elements=[f'name={self.router_name}'], ), json={'routers': []}, ), @@ -576,7 +576,7 @@ def _test_list_router_interfaces( 'network', 'public', append=['v2.0', 'ports'], - qs_elements=["device_id=%s" % self.router_id], + qs_elements=[f"device_id={self.router_id}"], ), json={'ports': (internal_ports + external_ports)}, ) diff --git a/openstack/tests/unit/cloud/test_security_groups.py b/openstack/tests/unit/cloud/test_security_groups.py index 9ccb8a593..745d59300 100644 --- a/openstack/tests/unit/cloud/test_security_groups.py +++ b/openstack/tests/unit/cloud/test_security_groups.py @@ -74,7 +74,7 @@ def test_list_security_groups_neutron(self): 'network', 'public', append=['v2.0', 'security-groups'], - qs_elements=["project_id=%s" % project_id], + qs_elements=[f"project_id={project_id}"], ), json={'security_groups': [neutron_grp_dict]}, ) @@ -88,9 +88,7 @@ def test_list_security_groups_nova(self): [ dict( method='GET', - uri='{endpoint}/os-security-groups?project_id=42'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/os-security-groups?project_id=42', json={'security_groups': []}, ), ] @@ -126,7 +124,7 @@ def test_delete_security_group_neutron(self): uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'security-groups', '%s' % sg_id], + append=['v2.0', 'security-groups', f'{sg_id}'], ), status_code=200, json={}, @@ -144,16 +142,12 @@ def test_delete_security_group_nova(self): [ dict( method='GET', - uri='{endpoint}/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/os-security-groups', json={'security_groups': nova_return}, ), dict( method='DELETE', - uri='{endpoint}/os-security-groups/2'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/os-security-groups/2', ), ] ) @@ -184,9 +178,7 @@ def test_delete_security_group_nova_not_found(self): [ dict( method='GET', - uri='{endpoint}/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/os-security-groups', json={'security_groups': nova_return}, ), ] @@ -240,8 +232,8 @@ def test_create_security_group_neutron_specific_tenant(self): project_id = "861808a93da0484ea1767967c4df8a23" group_name = self.getUniqueString() group_desc = ( - 'security group from' - ' test_create_security_group_neutron_specific_tenant' + 'security group from ' + 'test_create_security_group_neutron_specific_tenant' ) new_group = fakes.make_fake_neutron_security_group( id='2', @@ -331,9 +323,7 @@ def test_create_security_group_nova(self): [ dict( method='POST', - uri='{endpoint}/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/os-security-groups', json={'security_group': new_group}, validate=dict( json={ @@ -385,7 +375,7 @@ def test_update_security_group_neutron(self): uri=self.get_mock_url( 'network', 'public', - append=['v2.0', 'security-groups', '%s' % sg_id], + append=['v2.0', 'security-groups', f'{sg_id}'], ), json={'security_group': update_return}, validate=dict( @@ -418,16 +408,12 @@ def test_update_security_group_nova(self): [ dict( method='GET', - uri='{endpoint}/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/os-security-groups', json={'security_groups': nova_return}, ), dict( method='PUT', - uri='{endpoint}/os-security-groups/2'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/os-security-groups/2', json={'security_group': update_return}, ), ] @@ -586,16 +572,12 @@ def test_create_security_group_rule_nova(self): [ dict( method='GET', - uri='{endpoint}/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/os-security-groups', json={'security_groups': nova_return}, ), dict( method='POST', - uri='{endpoint}/os-security-group-rules'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/os-security-group-rules', json={'security_group_rule': new_rule}, validate=dict( json={ @@ -642,16 +624,12 @@ def test_create_security_group_rule_nova_no_ports(self): [ dict( method='GET', - uri='{endpoint}/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/os-security-groups', json={'security_groups': nova_return}, ), dict( method='POST', - uri='{endpoint}/os-security-group-rules'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/os-security-group-rules', json={'security_group_rule': new_rule}, validate=dict( json={ @@ -700,7 +678,7 @@ def test_delete_security_group_rule_neutron(self): append=[ 'v2.0', 'security-group-rules', - '%s' % rule_id, + f'{rule_id}', ], ), json={}, @@ -717,9 +695,7 @@ def test_delete_security_group_rule_nova(self): [ dict( method='DELETE', - uri='{endpoint}/os-security-group-rules/xyz'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/os-security-group-rules/xyz', ), ] ) @@ -760,9 +736,7 @@ def test_delete_security_group_rule_not_found_nova(self): [ dict( method='GET', - uri='{endpoint}/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/os-security-groups', json={'security_groups': [nova_grp_dict]}, ), ] @@ -779,9 +753,7 @@ def test_nova_egress_security_group_rule(self): [ dict( method='GET', - uri='{endpoint}/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/os-security-groups', json={'security_groups': [nova_grp_dict]}, ), ] @@ -842,16 +814,15 @@ def test_add_security_group_to_server_nova(self): [ dict( method='GET', - uri='{endpoint}/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT, - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/os-security-groups', json={'security_groups': [nova_grp_dict]}, ), self.get_nova_discovery_mock_dict(), dict( method='POST', - uri='%s/servers/%s/action' - % (fakes.COMPUTE_ENDPOINT, '1234'), + uri='{}/servers/{}/action'.format( + fakes.COMPUTE_ENDPOINT, '1234' + ), validate=dict( json={'addSecurityGroup': {'name': 'nova-sec-group'}} ), @@ -894,8 +865,9 @@ def test_add_security_group_to_server_neutron(self): ), dict( method='POST', - uri='%s/servers/%s/action' - % (fakes.COMPUTE_ENDPOINT, '1234'), + uri='{}/servers/{}/action'.format( + fakes.COMPUTE_ENDPOINT, '1234' + ), validate=dict( json={ 'addSecurityGroup': {'name': 'neutron-sec-group'} @@ -921,16 +893,15 @@ def test_remove_security_group_from_server_nova(self): [ dict( method='GET', - uri='{endpoint}/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/os-security-groups', json={'security_groups': [nova_grp_dict]}, ), self.get_nova_discovery_mock_dict(), dict( method='POST', - uri='%s/servers/%s/action' - % (fakes.COMPUTE_ENDPOINT, '1234'), + uri='{}/servers/{}/action'.format( + fakes.COMPUTE_ENDPOINT, '1234' + ), validate=dict( json={ 'removeSecurityGroup': {'name': 'nova-sec-group'} @@ -974,8 +945,9 @@ def test_remove_security_group_from_server_neutron(self): ), dict( method='POST', - uri='%s/servers/%s/action' - % (fakes.COMPUTE_ENDPOINT, '1234'), + uri='{}/servers/{}/action'.format( + fakes.COMPUTE_ENDPOINT, '1234' + ), validate=dict(json=validate), ), ] @@ -1000,16 +972,12 @@ def test_add_bad_security_group_to_server_nova(self): self.get_nova_discovery_mock_dict(), dict( method='GET', - uri='{endpoint}/servers/detail'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/servers/detail', json={'servers': [fake_server]}, ), dict( method='GET', - uri='{endpoint}/os-security-groups'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/os-security-groups', json={'security_groups': [nova_grp_dict]}, ), ] @@ -1064,9 +1032,7 @@ def test_add_security_group_to_bad_server(self): self.get_nova_discovery_mock_dict(), dict( method='GET', - uri='{endpoint}/servers/detail'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/servers/detail', json={'servers': [fake_server]}, ), ] diff --git a/openstack/tests/unit/cloud/test_server_console.py b/openstack/tests/unit/cloud/test_server_console.py index 872fe27d8..4d1bef820 100644 --- a/openstack/tests/unit/cloud/test_server_console.py +++ b/openstack/tests/unit/cloud/test_server_console.py @@ -33,9 +33,7 @@ def test_get_server_console_dict(self): self.get_nova_discovery_mock_dict(), dict( method='POST', - uri='{endpoint}/servers/{id}/action'.format( - endpoint=fakes.COMPUTE_ENDPOINT, id=self.server_id - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/servers/{self.server_id}/action', json={"output": self.output}, validate=dict(json={'os-getConsoleOutput': {'length': 5}}), ), @@ -53,16 +51,12 @@ def test_get_server_console_name_or_id(self): self.get_nova_discovery_mock_dict(), dict( method='GET', - uri='{endpoint}/servers/detail'.format( - endpoint=fakes.COMPUTE_ENDPOINT - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/servers/detail', json={"servers": [self.server]}, ), dict( method='POST', - uri='{endpoint}/servers/{id}/action'.format( - endpoint=fakes.COMPUTE_ENDPOINT, id=self.server_id - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/servers/{self.server_id}/action', json={"output": self.output}, validate=dict(json={'os-getConsoleOutput': {}}), ), @@ -81,9 +75,7 @@ def test_get_server_console_no_console(self): self.get_nova_discovery_mock_dict(), dict( method='POST', - uri='{endpoint}/servers/{id}/action'.format( - endpoint=fakes.COMPUTE_ENDPOINT, id=self.server_id - ), + uri=f'{fakes.COMPUTE_ENDPOINT}/servers/{self.server_id}/action', status_code=400, validate=dict(json={'os-getConsoleOutput': {}}), ), diff --git a/openstack/tests/unit/cloud/test_stack.py b/openstack/tests/unit/cloud/test_stack.py index 91b7acce1..265b199d6 100644 --- a/openstack/tests/unit/cloud/test_stack.py +++ b/openstack/tests/unit/cloud/test_stack.py @@ -45,9 +45,7 @@ def test_list_stacks(self): [ dict( method='GET', - uri='{endpoint}/stacks'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks', json={"stacks": fake_stacks}, ), ] @@ -88,9 +86,7 @@ def test_list_stacks_exception(self): [ dict( method='GET', - uri='{endpoint}/stacks'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks', status_code=404, ) ] @@ -110,9 +106,7 @@ def test_search_stacks(self): [ dict( method='GET', - uri='{endpoint}/stacks'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks', json={"stacks": fake_stacks}, ), ] @@ -134,9 +128,7 @@ def test_search_stacks_filters(self): [ dict( method='GET', - uri='{endpoint}/stacks'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks', json={"stacks": fake_stacks}, ), ] @@ -151,9 +143,7 @@ def test_search_stacks_exception(self): [ dict( method='GET', - uri='{endpoint}/stacks'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks', status_code=404, ) ] @@ -167,36 +157,20 @@ def test_delete_stack(self): [ dict( method='GET', - uri='{endpoint}/stacks/{name}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name, - resolve=resolve, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}?{resolve}', status_code=302, headers=dict( - location='{endpoint}/stacks/{name}/{id}?{resolve}'.format( # noqa: E501 - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - name=self.stack_name, - resolve=resolve, - ) + location=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}', # noqa: E501 ), ), dict( method='GET', - uri='{endpoint}/stacks/{name}/{id}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - name=self.stack_name, - resolve=resolve, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}', json={"stack": self.stack}, ), dict( method='DELETE', - uri='{endpoint}/stacks/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, id=self.stack_id - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_id}', ), ] ) @@ -209,9 +183,7 @@ def test_delete_stack_not_found(self): [ dict( method='GET', - uri='{endpoint}/stacks/stack_name?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, resolve=resolve - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/stack_name?{resolve}', status_code=404, ), ] @@ -225,36 +197,20 @@ def test_delete_stack_exception(self): [ dict( method='GET', - uri='{endpoint}/stacks/{id}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - resolve=resolve, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_id}?{resolve}', status_code=302, headers=dict( - location='{endpoint}/stacks/{name}/{id}?{resolve}'.format( # noqa: E501 - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - name=self.stack_name, - resolve=resolve, - ) + location=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}', # noqa: E501 ), ), dict( method='GET', - uri='{endpoint}/stacks/{name}/{id}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - name=self.stack_name, - resolve=resolve, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}', json={"stack": self.stack}, ), dict( method='DELETE', - uri='{endpoint}/stacks/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, id=self.stack_id - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_id}', status_code=400, reason="ouch", ), @@ -279,29 +235,15 @@ def test_delete_stack_by_name_wait(self): [ dict( method='GET', - uri='{endpoint}/stacks/{name}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name, - resolve=resolve, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}?{resolve}', status_code=302, headers=dict( - location='{endpoint}/stacks/{name}/{id}?{resolve}'.format( # noqa: E501 - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - name=self.stack_name, - resolve=resolve, - ) + location=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}', # noqa: E501 ), ), dict( method='GET', - uri='{endpoint}/stacks/{name}/{id}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - name=self.stack_name, - resolve=resolve, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}', json={"stack": self.stack}, ), dict( @@ -316,17 +258,11 @@ def test_delete_stack_by_name_wait(self): ), dict( method='DELETE', - uri='{endpoint}/stacks/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, id=self.stack_id - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_id}', ), dict( method='GET', - uri='{endpoint}/stacks/{name}/events?{qs}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name, - qs=marker_qs, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/events?{marker_qs}', complete_qs=True, json={ "events": [ @@ -341,11 +277,7 @@ def test_delete_stack_by_name_wait(self): ), dict( method='GET', - uri='{endpoint}/stacks/{name}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name, - resolve=resolve, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}?{resolve}', status_code=404, ), ] @@ -369,29 +301,15 @@ def test_delete_stack_by_id_wait(self): [ dict( method='GET', - uri='{endpoint}/stacks/{id}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - resolve=resolve, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_id}?{resolve}', status_code=302, headers=dict( - location='{endpoint}/stacks/{name}/{id}?{resolve}'.format( # noqa: E501 - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - name=self.stack_name, - resolve=resolve, - ) + location=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}', # noqa: E501 ), ), dict( method='GET', - uri='{endpoint}/stacks/{name}/{id}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - name=self.stack_name, - resolve=resolve, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}', json={"stack": self.stack}, ), dict( @@ -406,17 +324,11 @@ def test_delete_stack_by_id_wait(self): ), dict( method='DELETE', - uri='{endpoint}/stacks/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, id=self.stack_id - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_id}', ), dict( method='GET', - uri='{endpoint}/stacks/{id}/events?{qs}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - qs=marker_qs, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_id}/events?{marker_qs}', complete_qs=True, json={ "events": [ @@ -430,11 +342,7 @@ def test_delete_stack_by_id_wait(self): ), dict( method='GET', - uri='{endpoint}/stacks/{id}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - resolve=resolve, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_id}?{resolve}', status_code=404, ), ] @@ -457,29 +365,15 @@ def test_delete_stack_wait_failed(self): [ dict( method='GET', - uri='{endpoint}/stacks/{id}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - resolve=resolve, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_id}?{resolve}', status_code=302, headers=dict( - location='{endpoint}/stacks/{name}/{id}?{resolve}'.format( # noqa: E501 - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - name=self.stack_name, - resolve=resolve, - ) + location=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}', # noqa: E501 ), ), dict( method='GET', - uri='{endpoint}/stacks/{name}/{id}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - name=self.stack_name, - resolve=resolve, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}', json={"stack": self.stack}, ), dict( @@ -494,17 +388,11 @@ def test_delete_stack_wait_failed(self): ), dict( method='DELETE', - uri='{endpoint}/stacks/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, id=self.stack_id - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_id}', ), dict( method='GET', - uri='{endpoint}/stacks/{id}/events?{qs}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - qs=marker_qs, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_id}/events?{marker_qs}', complete_qs=True, json={ "events": [ @@ -518,27 +406,15 @@ def test_delete_stack_wait_failed(self): ), dict( method='GET', - uri='{endpoint}/stacks/{id}?resolve_outputs=False'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, id=self.stack_id - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_id}?resolve_outputs=False', status_code=302, headers=dict( - location='{endpoint}/stacks/{name}/{id}?{resolve}'.format( # noqa: E501 - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - name=self.stack_name, - resolve=resolve, - ) + location=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}', # noqa: E501 ), ), dict( method='GET', - uri='{endpoint}/stacks/{name}/{id}?{resolve}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - name=self.stack_name, - resolve=resolve, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}', json={"stack": failed_stack}, ), ] @@ -557,9 +433,7 @@ def test_create_stack(self): [ dict( method='POST', - uri='{endpoint}/stacks'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks', json={"stack": self.stack}, validate=dict( json={ @@ -574,26 +448,15 @@ def test_create_stack(self): ), dict( method='GET', - uri='{endpoint}/stacks/{name}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}', status_code=302, headers=dict( - location='{endpoint}/stacks/{name}/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - name=self.stack_name, - ) + location=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}' ), ), dict( method='GET', - uri='{endpoint}/stacks/{name}/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - name=self.stack_name, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}', json={"stack": self.stack}, ), ] @@ -616,9 +479,7 @@ def test_create_stack_wait(self): [ dict( method='POST', - uri='{endpoint}/stacks'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks', json={"stack": self.stack}, validate=dict( json={ @@ -633,10 +494,7 @@ def test_create_stack_wait(self): ), dict( method='GET', - uri='{endpoint}/stacks/{name}/events?sort_dir=asc'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/events?sort_dir=asc', json={ "events": [ fakes.make_fake_stack_event( @@ -650,26 +508,15 @@ def test_create_stack_wait(self): ), dict( method='GET', - uri='{endpoint}/stacks/{name}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}', status_code=302, headers=dict( - location='{endpoint}/stacks/{name}/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - name=self.stack_name, - ) + location=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}' ), ), dict( method='GET', - uri='{endpoint}/stacks/{name}/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - name=self.stack_name, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}', json={"stack": self.stack}, ), ] @@ -692,10 +539,7 @@ def test_update_stack(self): [ dict( method='PUT', - uri='{endpoint}/stacks/{name}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}', validate=dict( json={ 'disable_rollback': False, @@ -709,26 +553,15 @@ def test_update_stack(self): ), dict( method='GET', - uri='{endpoint}/stacks/{name}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}', status_code=302, headers=dict( - location='{endpoint}/stacks/{name}/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - name=self.stack_name, - ) + location=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}' ), ), dict( method='GET', - uri='{endpoint}/stacks/{name}/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - name=self.stack_name, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}', json={"stack": self.stack}, ), ] @@ -768,10 +601,7 @@ def test_update_stack_wait(self): ), dict( method='PUT', - uri='{endpoint}/stacks/{name}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}', validate=dict( json={ 'disable_rollback': False, @@ -785,11 +615,7 @@ def test_update_stack_wait(self): ), dict( method='GET', - uri='{endpoint}/stacks/{name}/events?{qs}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name, - qs=marker_qs, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/events?{marker_qs}', json={ "events": [ fakes.make_fake_stack_event( @@ -803,26 +629,15 @@ def test_update_stack_wait(self): ), dict( method='GET', - uri='{endpoint}/stacks/{name}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}', status_code=302, headers=dict( - location='{endpoint}/stacks/{name}/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - name=self.stack_name, - ) + location=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}' ), ), dict( method='GET', - uri='{endpoint}/stacks/{name}/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - name=self.stack_name, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}', json={"stack": self.stack}, ), ] @@ -841,26 +656,15 @@ def test_get_stack(self): [ dict( method='GET', - uri='{endpoint}/stacks/{name}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}', status_code=302, headers=dict( - location='{endpoint}/stacks/{name}/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - name=self.stack_name, - ) + location=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}' ), ), dict( method='GET', - uri='{endpoint}/stacks/{name}/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - name=self.stack_name, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}', json={"stack": self.stack}, ), ] @@ -881,26 +685,15 @@ def test_get_stack_in_progress(self): [ dict( method='GET', - uri='{endpoint}/stacks/{name}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - name=self.stack_name, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}', status_code=302, headers=dict( - location='{endpoint}/stacks/{name}/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - name=self.stack_name, - ) + location=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}' ), ), dict( method='GET', - uri='{endpoint}/stacks/{name}/{id}'.format( - endpoint=fakes.ORCHESTRATION_ENDPOINT, - id=self.stack_id, - name=self.stack_name, - ), + uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}', json={"stack": in_progress}, ), ] diff --git a/openstack/tests/unit/cloud/test_subnet.py b/openstack/tests/unit/cloud/test_subnet.py index 0005a1969..11bf50579 100644 --- a/openstack/tests/unit/cloud/test_subnet.py +++ b/openstack/tests/unit/cloud/test_subnet.py @@ -88,7 +88,7 @@ def test_get_subnet(self): 'network', 'public', append=['v2.0', 'subnets'], - qs_elements=['name=%s' % self.subnet_name], + qs_elements=[f'name={self.subnet_name}'], ), json={'subnets': [self.mock_subnet_rep]}, ), @@ -143,7 +143,7 @@ def test_create_subnet(self): 'network', 'public', append=['v2.0', 'networks'], - qs_elements=['name=%s' % self.network_name], + qs_elements=[f'name={self.network_name}'], ), json={'networks': [self.mock_network_rep]}, ), @@ -198,7 +198,7 @@ def test_create_subnet_string_ip_version(self): 'network', 'public', append=['v2.0', 'networks'], - qs_elements=['name=%s' % self.network_name], + qs_elements=[f'name={self.network_name}'], ), json={'networks': [self.mock_network_rep]}, ), @@ -246,7 +246,7 @@ def test_create_subnet_bad_ip_version(self): 'network', 'public', append=['v2.0', 'networks'], - qs_elements=['name=%s' % self.network_name], + qs_elements=[f'name={self.network_name}'], ), json={'networks': [self.mock_network_rep]}, ), @@ -284,7 +284,7 @@ def test_create_subnet_without_gateway_ip(self): 'network', 'public', append=['v2.0', 'networks'], - qs_elements=['name=%s' % self.network_name], + qs_elements=[f'name={self.network_name}'], ), json={'networks': [self.mock_network_rep]}, ), @@ -345,7 +345,7 @@ def test_create_subnet_with_gateway_ip(self): 'network', 'public', append=['v2.0', 'networks'], - qs_elements=['name=%s' % self.network_name], + qs_elements=[f'name={self.network_name}'], ), json={'networks': [self.mock_network_rep]}, ), @@ -468,7 +468,7 @@ def test_create_subnet_non_unique_network(self): 'network', 'public', append=['v2.0', 'networks'], - qs_elements=['name=%s' % self.network_name], + qs_elements=[f'name={self.network_name}'], ), json={'networks': [net1, net2]}, ), @@ -513,7 +513,7 @@ def test_create_subnet_from_subnetpool_with_prefixlen(self): 'network', 'public', append=['v2.0', 'networks'], - qs_elements=['name=%s' % self.network_name], + qs_elements=[f'name={self.network_name}'], ), json={'networks': [self.mock_network_rep]}, ), @@ -585,7 +585,7 @@ def test_create_subnet_from_specific_subnetpool(self): 'network', 'public', append=['v2.0', 'networks'], - qs_elements=['name=%s' % self.network_name], + qs_elements=[f'name={self.network_name}'], ), json={'networks': [self.mock_network_rep]}, ), @@ -659,7 +659,7 @@ def test_delete_subnet(self): 'network', 'public', append=['v2.0', 'subnets'], - qs_elements=['name=%s' % self.subnet_name], + qs_elements=[f'name={self.subnet_name}'], ), json={'subnets': [self.mock_subnet_rep]}, ), @@ -724,7 +724,7 @@ def test_delete_subnet_multiple_found(self): 'network', 'public', append=['v2.0', 'subnets'], - qs_elements=['name=%s' % self.subnet_name], + qs_elements=[f'name={self.subnet_name}'], ), json={'subnets': [subnet1, subnet2]}, ), diff --git a/openstack/tests/unit/cloud/test_update_server.py b/openstack/tests/unit/cloud/test_update_server.py index 275c0f65a..214a3e4e4 100644 --- a/openstack/tests/unit/cloud/test_update_server.py +++ b/openstack/tests/unit/cloud/test_update_server.py @@ -57,7 +57,7 @@ def test_update_server_with_update_exception(self): 'compute', 'public', append=['servers', 'detail'], - qs_elements=['name=%s' % self.server_name], + qs_elements=[f'name={self.server_name}'], ), json={'servers': [self.fake_server]}, ), @@ -108,7 +108,7 @@ def test_update_server_name(self): 'compute', 'public', append=['servers', 'detail'], - qs_elements=['name=%s' % self.server_name], + qs_elements=[f'name={self.server_name}'], ), json={'servers': [self.fake_server]}, ), diff --git a/openstack/tests/unit/cloud/test_users.py b/openstack/tests/unit/cloud/test_users.py index de5a12b27..042916d10 100644 --- a/openstack/tests/unit/cloud/test_users.py +++ b/openstack/tests/unit/cloud/test_users.py @@ -84,8 +84,8 @@ def test_create_user_v3_no_domain(self): ) with testtools.ExpectedException( exceptions.SDKException, - "User or project creation requires an explicit" - " domain_id argument.", + "User or project creation requires an explicit " + "domain_id argument.", ): self.cloud.create_user( name=user_data.name, @@ -105,7 +105,7 @@ def test_delete_user(self): method='GET', uri=self._get_keystone_mock_url( resource='users', - qs_elements=['name=%s' % user_data.name], + qs_elements=[f'name={user_data.name}'], ), status_code=200, json=self._get_user_list(user_data), diff --git a/openstack/tests/unit/cloud/test_volume.py b/openstack/tests/unit/cloud/test_volume.py index 8c41006b8..44ba20831 100644 --- a/openstack/tests/unit/cloud/test_volume.py +++ b/openstack/tests/unit/cloud/test_volume.py @@ -235,8 +235,9 @@ def test_attach_volume_not_available(self): with testtools.ExpectedException( exceptions.SDKException, - "Volume %s is not available. Status is '%s'" - % (volume['id'], volume['status']), + "Volume {} is not available. Status is '{}'".format( + volume['id'], volume['status'] + ), ): self.cloud.attach_volume(server, volume) self.assertEqual(0, len(self.adapter.request_history)) @@ -251,8 +252,9 @@ def test_attach_volume_already_attached(self): with testtools.ExpectedException( exceptions.SDKException, - "Volume %s already attached to server %s on device %s" - % (volume['id'], server['id'], device_id), + "Volume {} already attached to server {} on device {}".format( + volume['id'], server['id'], device_id + ), ): self.cloud.attach_volume(server, volume) self.assertEqual(0, len(self.adapter.request_history)) diff --git a/openstack/tests/unit/clustering/v1/test_cluster.py b/openstack/tests/unit/clustering/v1/test_cluster.py index a359150c6..4a5da101c 100644 --- a/openstack/tests/unit/clustering/v1/test_cluster.py +++ b/openstack/tests/unit/clustering/v1/test_cluster.py @@ -121,7 +121,7 @@ def test_scale_in(self): sess = mock.Mock() sess.post = mock.Mock(return_value=resp) self.assertEqual('', sot.scale_in(sess, 3)) - url = 'clusters/%s/actions' % sot.id + url = f'clusters/{sot.id}/actions' body = {'scale_in': {'count': 3}} sess.post.assert_called_once_with(url, json=body) @@ -133,7 +133,7 @@ def test_scale_out(self): sess = mock.Mock() sess.post = mock.Mock(return_value=resp) self.assertEqual('', sot.scale_out(sess, 3)) - url = 'clusters/%s/actions' % sot.id + url = f'clusters/{sot.id}/actions' body = {'scale_out': {'count': 3}} sess.post.assert_called_once_with(url, json=body) @@ -145,7 +145,7 @@ def test_resize(self): sess = mock.Mock() sess.post = mock.Mock(return_value=resp) self.assertEqual('', sot.resize(sess, foo='bar', zoo=5)) - url = 'clusters/%s/actions' % sot.id + url = f'clusters/{sot.id}/actions' body = {'resize': {'foo': 'bar', 'zoo': 5}} sess.post.assert_called_once_with(url, json=body) @@ -157,7 +157,7 @@ def test_add_nodes(self): sess = mock.Mock() sess.post = mock.Mock(return_value=resp) self.assertEqual('', sot.add_nodes(sess, ['node-33'])) - url = 'clusters/%s/actions' % sot.id + url = f'clusters/{sot.id}/actions' body = {'add_nodes': {'nodes': ['node-33']}} sess.post.assert_called_once_with(url, json=body) @@ -169,7 +169,7 @@ def test_del_nodes(self): sess = mock.Mock() sess.post = mock.Mock(return_value=resp) self.assertEqual('', sot.del_nodes(sess, ['node-11'])) - url = 'clusters/%s/actions' % sot.id + url = f'clusters/{sot.id}/actions' body = {'del_nodes': {'nodes': ['node-11']}} sess.post.assert_called_once_with(url, json=body) @@ -184,7 +184,7 @@ def test_del_nodes_with_params(self): 'destroy_after_deletion': True, } self.assertEqual('', sot.del_nodes(sess, ['node-11'], **params)) - url = 'clusters/%s/actions' % sot.id + url = f'clusters/{sot.id}/actions' body = { 'del_nodes': { 'nodes': ['node-11'], @@ -201,7 +201,7 @@ def test_replace_nodes(self): sess = mock.Mock() sess.post = mock.Mock(return_value=resp) self.assertEqual('', sot.replace_nodes(sess, {'node-22': 'node-44'})) - url = 'clusters/%s/actions' % sot.id + url = f'clusters/{sot.id}/actions' body = {'replace_nodes': {'nodes': {'node-22': 'node-44'}}} sess.post.assert_called_once_with(url, json=body) @@ -217,7 +217,7 @@ def test_policy_attach(self): } self.assertEqual('', sot.policy_attach(sess, 'POLICY', **params)) - url = 'clusters/%s/actions' % sot.id + url = f'clusters/{sot.id}/actions' body = { 'policy_attach': { 'policy_id': 'POLICY', @@ -235,7 +235,7 @@ def test_policy_detach(self): sess.post = mock.Mock(return_value=resp) self.assertEqual('', sot.policy_detach(sess, 'POLICY')) - url = 'clusters/%s/actions' % sot.id + url = f'clusters/{sot.id}/actions' body = {'policy_detach': {'policy_id': 'POLICY'}} sess.post.assert_called_once_with(url, json=body) @@ -249,7 +249,7 @@ def test_policy_update(self): params = {'enabled': False} self.assertEqual('', sot.policy_update(sess, 'POLICY', **params)) - url = 'clusters/%s/actions' % sot.id + url = f'clusters/{sot.id}/actions' body = {'policy_update': {'policy_id': 'POLICY', 'enabled': False}} sess.post.assert_called_once_with(url, json=body) @@ -261,7 +261,7 @@ def test_check(self): sess = mock.Mock() sess.post = mock.Mock(return_value=resp) self.assertEqual('', sot.check(sess)) - url = 'clusters/%s/actions' % sot.id + url = f'clusters/{sot.id}/actions' body = {'check': {}} sess.post.assert_called_once_with(url, json=body) @@ -273,7 +273,7 @@ def test_recover(self): sess = mock.Mock() sess.post = mock.Mock(return_value=resp) self.assertEqual('', sot.recover(sess)) - url = 'clusters/%s/actions' % sot.id + url = f'clusters/{sot.id}/actions' body = {'recover': {}} sess.post.assert_called_once_with(url, json=body) @@ -285,7 +285,7 @@ def test_operation(self): sess = mock.Mock() sess.post = mock.Mock(return_value=resp) self.assertEqual('', sot.op(sess, 'dance', style='tango')) - url = 'clusters/%s/ops' % sot.id + url = f'clusters/{sot.id}/ops' body = {'dance': {'style': 'tango'}} sess.post.assert_called_once_with(url, json=body) @@ -302,6 +302,6 @@ def test_force_delete(self): res = sot.force_delete(sess) self.assertEqual(fake_action_id, res.id) - url = 'clusters/%s' % sot.id + url = f'clusters/{sot.id}' body = {'force': True} sess.delete.assert_called_once_with(url, json=body) diff --git a/openstack/tests/unit/clustering/v1/test_node.py b/openstack/tests/unit/clustering/v1/test_node.py index 28bdaf814..0627efe81 100644 --- a/openstack/tests/unit/clustering/v1/test_node.py +++ b/openstack/tests/unit/clustering/v1/test_node.py @@ -75,7 +75,7 @@ def test_check(self): sess = mock.Mock() sess.post = mock.Mock(return_value=resp) self.assertEqual('', sot.check(sess)) - url = 'nodes/%s/actions' % sot.id + url = f'nodes/{sot.id}/actions' body = {'check': {}} sess.post.assert_called_once_with(url, json=body) @@ -87,7 +87,7 @@ def test_recover(self): sess = mock.Mock() sess.post = mock.Mock(return_value=resp) self.assertEqual('', sot.recover(sess)) - url = 'nodes/%s/actions' % sot.id + url = f'nodes/{sot.id}/actions' body = {'recover': {}} sess.post.assert_called_once_with(url, json=body) @@ -99,7 +99,7 @@ def test_operation(self): sess = mock.Mock() sess.post = mock.Mock(return_value=resp) self.assertEqual('', sot.op(sess, 'dance', style='tango')) - url = 'nodes/%s/ops' % sot.id + url = f'nodes/{sot.id}/ops' sess.post.assert_called_once_with( url, json={'dance': {'style': 'tango'}} ) @@ -150,7 +150,7 @@ def test_force_delete(self): res = sot.force_delete(sess) self.assertEqual(fake_action_id, res.id) - url = 'nodes/%s' % sot.id + url = f'nodes/{sot.id}' body = {'force': True} sess.delete.assert_called_once_with(url, json=body) diff --git a/openstack/tests/unit/clustering/v1/test_profile_type.py b/openstack/tests/unit/clustering/v1/test_profile_type.py index d2f084147..b61030c9e 100644 --- a/openstack/tests/unit/clustering/v1/test_profile_type.py +++ b/openstack/tests/unit/clustering/v1/test_profile_type.py @@ -53,5 +53,5 @@ def test_ops(self): sess = mock.Mock() sess.get = mock.Mock(return_value=resp) self.assertEqual('', sot.type_ops(sess)) - url = 'profile-types/%s/ops' % sot.id + url = f'profile-types/{sot.id}/ops' sess.get.assert_called_once_with(url) diff --git a/openstack/tests/unit/config/test_from_conf.py b/openstack/tests/unit/config/test_from_conf.py index a6e82c12a..032d72c42 100644 --- a/openstack/tests/unit/config/test_from_conf.py +++ b/openstack/tests/unit/config/test_from_conf.py @@ -285,8 +285,8 @@ def assert_service_disabled( exceptions.ServiceDisabledException, getattr, adap, 'get' ) self.assertIn( - "Service '%s' is disabled because its configuration " - "could not be loaded." % service_type, + f"Service '{service_type}' is disabled because its configuration " + "could not be loaded.", ex.message, ) self.assertIn(expected_reason, ex.message) diff --git a/openstack/tests/unit/database/v1/test_instance.py b/openstack/tests/unit/database/v1/test_instance.py index bb2df451d..6a0d6d879 100644 --- a/openstack/tests/unit/database/v1/test_instance.py +++ b/openstack/tests/unit/database/v1/test_instance.py @@ -67,7 +67,7 @@ def test_enable_root_user(self): self.assertEqual(response.body['user'], sot.enable_root_user(sess)) - url = "instances/%s/root" % IDENTIFIER + url = f"instances/{IDENTIFIER}/root" sess.post.assert_called_with( url, ) @@ -82,7 +82,7 @@ def test_is_root_enabled(self): self.assertTrue(sot.is_root_enabled(sess)) - url = "instances/%s/root" % IDENTIFIER + url = f"instances/{IDENTIFIER}/root" sess.get.assert_called_with( url, ) @@ -96,7 +96,7 @@ def test_action_restart(self): self.assertIsNone(sot.restart(sess)) - url = "instances/%s/action" % IDENTIFIER + url = f"instances/{IDENTIFIER}/action" body = {'restart': None} sess.post.assert_called_with(url, json=body) @@ -110,7 +110,7 @@ def test_action_resize(self): self.assertIsNone(sot.resize(sess, flavor)) - url = "instances/%s/action" % IDENTIFIER + url = f"instances/{IDENTIFIER}/action" body = {'resize': {'flavorRef': flavor}} sess.post.assert_called_with(url, json=body) @@ -124,6 +124,6 @@ def test_action_resize_volume(self): self.assertIsNone(sot.resize_volume(sess, size)) - url = "instances/%s/action" % IDENTIFIER + url = f"instances/{IDENTIFIER}/action" body = {'resize': {'volume': size}} sess.post.assert_called_with(url, json=body) diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index f402c7215..9638c7771 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -257,7 +257,7 @@ def test_add_tag(self): sot.add_tag(self.sess, tag) self.sess.put.assert_called_with( - 'images/IDENTIFIER/tags/%s' % tag, + f'images/IDENTIFIER/tags/{tag}', ) def test_remove_tag(self): @@ -266,7 +266,7 @@ def test_remove_tag(self): sot.remove_tag(self.sess, tag) self.sess.delete.assert_called_with( - 'images/IDENTIFIER/tags/%s' % tag, + f'images/IDENTIFIER/tags/{tag}', ) def test_import_image(self): diff --git a/openstack/tests/unit/key_manager/v1/test_container.py b/openstack/tests/unit/key_manager/v1/test_container.py index bb43d6f5b..82d9b2376 100644 --- a/openstack/tests/unit/key_manager/v1/test_container.py +++ b/openstack/tests/unit/key_manager/v1/test_container.py @@ -15,7 +15,7 @@ ID_VAL = "123" -IDENTIFIER = 'http://localhost/containers/%s' % ID_VAL +IDENTIFIER = f'http://localhost/containers/{ID_VAL}' EXAMPLE = { 'container_ref': IDENTIFIER, 'created': '2015-03-09T12:14:57.233772', diff --git a/openstack/tests/unit/key_manager/v1/test_order.py b/openstack/tests/unit/key_manager/v1/test_order.py index bdb1198f0..a7b93c5d6 100644 --- a/openstack/tests/unit/key_manager/v1/test_order.py +++ b/openstack/tests/unit/key_manager/v1/test_order.py @@ -16,13 +16,13 @@ ID_VAL = "123" SECRET_ID = "5" -IDENTIFIER = 'http://localhost/orders/%s' % ID_VAL +IDENTIFIER = f'http://localhost/orders/{ID_VAL}' EXAMPLE = { 'created': '1', 'creator_id': '2', 'meta': {'key': '3'}, 'order_ref': IDENTIFIER, - 'secret_ref': 'http://localhost/secrets/%s' % SECRET_ID, + 'secret_ref': f'http://localhost/secrets/{SECRET_ID}', 'status': '6', 'sub_status': '7', 'sub_status_message': '8', diff --git a/openstack/tests/unit/key_manager/v1/test_secret.py b/openstack/tests/unit/key_manager/v1/test_secret.py index b0493fa18..6a9aa8317 100644 --- a/openstack/tests/unit/key_manager/v1/test_secret.py +++ b/openstack/tests/unit/key_manager/v1/test_secret.py @@ -16,7 +16,7 @@ from openstack.tests.unit import base ID_VAL = "123" -IDENTIFIER = 'http://localhost:9311/v1/secrets/%s' % ID_VAL +IDENTIFIER = f'http://localhost:9311/v1/secrets/{ID_VAL}' EXAMPLE = { 'algorithm': '1', 'bit_length': '2', diff --git a/openstack/tests/unit/message/v2/test_queue.py b/openstack/tests/unit/message/v2/test_queue.py index ca837d5ee..03932d99f 100644 --- a/openstack/tests/unit/message/v2/test_queue.py +++ b/openstack/tests/unit/message/v2/test_queue.py @@ -70,7 +70,7 @@ def test_create(self, mock_uuid): sot._translate_response = mock.Mock() res = sot.create(sess) - url = 'queues/%s' % FAKE1['name'] + url = 'queues/{}'.format(FAKE1['name']) headers = { 'Client-ID': 'NEW_CLIENT_ID', 'X-PROJECT-ID': 'NEW_PROJECT_ID', @@ -89,7 +89,7 @@ def test_create_client_id_project_id_exist(self): sot._translate_response = mock.Mock() res = sot.create(sess) - url = 'queues/%s' % FAKE2['name'] + url = 'queues/{}'.format(FAKE2['name']) headers = { 'Client-ID': 'OLD_CLIENT_ID', 'X-PROJECT-ID': 'OLD_PROJECT_ID', @@ -110,7 +110,7 @@ def test_get(self, mock_uuid): sot._translate_response = mock.Mock() res = sot.fetch(sess) - url = 'queues/%s' % FAKE1['name'] + url = 'queues/{}'.format(FAKE1['name']) headers = { 'Client-ID': 'NEW_CLIENT_ID', 'X-PROJECT-ID': 'NEW_PROJECT_ID', @@ -129,7 +129,7 @@ def test_get_client_id_project_id_exist(self): sot._translate_response = mock.Mock() res = sot.fetch(sess) - url = 'queues/%s' % FAKE2['name'] + url = 'queues/{}'.format(FAKE2['name']) headers = { 'Client-ID': 'OLD_CLIENT_ID', 'X-PROJECT-ID': 'OLD_PROJECT_ID', @@ -150,7 +150,7 @@ def test_delete(self, mock_uuid): sot._translate_response = mock.Mock() sot.delete(sess) - url = 'queues/%s' % FAKE1['name'] + url = 'queues/{}'.format(FAKE1['name']) headers = { 'Client-ID': 'NEW_CLIENT_ID', 'X-PROJECT-ID': 'NEW_PROJECT_ID', @@ -168,7 +168,7 @@ def test_delete_client_id_project_id_exist(self): sot._translate_response = mock.Mock() sot.delete(sess) - url = 'queues/%s' % FAKE2['name'] + url = 'queues/{}'.format(FAKE2['name']) headers = { 'Client-ID': 'OLD_CLIENT_ID', 'X-PROJECT-ID': 'OLD_PROJECT_ID', diff --git a/openstack/tests/unit/network/v2/test_bgp_speaker.py b/openstack/tests/unit/network/v2/test_bgp_speaker.py index 848aa590d..45cf617e0 100644 --- a/openstack/tests/unit/network/v2/test_bgp_speaker.py +++ b/openstack/tests/unit/network/v2/test_bgp_speaker.py @@ -169,7 +169,7 @@ def test_add_bgp_speaker_to_dragent(self): self.assertIsNone(sot.add_bgp_speaker_to_dragent(sess, agent_id)) body = {'bgp_speaker_id': sot.id} - url = 'agents/%s/bgp-drinstances' % agent_id + url = f'agents/{agent_id}/bgp-drinstances' sess.post.assert_called_with(url, json=body) def test_remove_bgp_speaker_from_dragent(self): diff --git a/openstack/tests/unit/object_store/v1/test_container.py b/openstack/tests/unit/object_store/v1/test_container.py index d67e72dc3..7908ff7fe 100644 --- a/openstack/tests/unit/object_store/v1/test_container.py +++ b/openstack/tests/unit/object_store/v1/test_container.py @@ -21,9 +21,7 @@ def setUp(self): super().setUp() self.container = self.getUniqueString() self.endpoint = self.cloud.object_store.get_endpoint() + '/' - self.container_endpoint = '{endpoint}{container}'.format( - endpoint=self.endpoint, container=self.container - ) + self.container_endpoint = f'{self.endpoint}{self.container}' self.body = { "count": 2, diff --git a/openstack/tests/unit/object_store/v1/test_proxy.py b/openstack/tests/unit/object_store/v1/test_proxy.py index 14f6535d5..55cc5fe28 100644 --- a/openstack/tests/unit/object_store/v1/test_proxy.py +++ b/openstack/tests/unit/object_store/v1/test_proxy.py @@ -45,9 +45,7 @@ def setUp(self): self.proxy = self.cloud.object_store self.container = self.getUniqueString() self.endpoint = self.cloud.object_store.get_endpoint() + '/' - self.container_endpoint = '{endpoint}{container}'.format( - endpoint=self.endpoint, container=self.container - ) + self.container_endpoint = f'{self.endpoint}{self.container}' def test_account_metadata_get(self): self.verify_head( @@ -132,13 +130,13 @@ def test_object_create_no_container(self): def test_object_get(self): with requests_mock.Mocker() as m: - m.get("%scontainer/object" % self.endpoint, text="data") + m.get(f"{self.endpoint}container/object", text="data") res = self.proxy.get_object("object", container="container") self.assertIsNone(res.data) def test_object_get_write_file(self): with requests_mock.Mocker() as m: - m.get("%scontainer/object" % self.endpoint, text="data") + m.get(f"{self.endpoint}container/object", text="data") with tempfile.NamedTemporaryFile() as f: self.proxy.get_object( "object", container="container", outfile=f.name @@ -148,7 +146,7 @@ def test_object_get_write_file(self): def test_object_get_remember_content(self): with requests_mock.Mocker() as m: - m.get("%scontainer/object" % self.endpoint, text="data") + m.get(f"{self.endpoint}container/object", text="data") res = self.proxy.get_object( "object", container="container", remember_content=True ) @@ -657,8 +655,8 @@ class TestTempURLUnicodePathAndKey(TestTempURL): url = '/v1/\u00e4/c/\u00f3' key = 'k\u00e9y' expected_url = ( - '%s?temp_url_sig=temp_url_signature&temp_url_expires=1400003600' - ) % url + f'{url}?temp_url_sig=temp_url_signature&temp_url_expires=1400003600' + ) expected_body = '\n'.join( [ 'GET', @@ -672,8 +670,8 @@ class TestTempURLUnicodePathBytesKey(TestTempURL): url = '/v1/\u00e4/c/\u00f3' key = 'k\u00e9y'.encode() expected_url = ( - '%s?temp_url_sig=temp_url_signature&temp_url_expires=1400003600' - ) % url + f'{url}?temp_url_sig=temp_url_signature&temp_url_expires=1400003600' + ) expected_body = '\n'.join( [ 'GET', diff --git a/openstack/tests/unit/orchestration/v1/test_stack.py b/openstack/tests/unit/orchestration/v1/test_stack.py index 9019887dc..5b9c72e90 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack.py +++ b/openstack/tests/unit/orchestration/v1/test_stack.py @@ -237,7 +237,7 @@ def test_fetch(self): ex = self.assertRaises(exceptions.NotFoundException, sot.fetch, sess) self.assertEqual('oops', str(ex)) ex = self.assertRaises(exceptions.NotFoundException, sot.fetch, sess) - self.assertEqual('No stack found for %s' % FAKE_ID, str(ex)) + self.assertEqual(f'No stack found for {FAKE_ID}', str(ex)) def test_abandon(self): sess = mock.Mock() @@ -333,7 +333,7 @@ def test_suspend(self): mock_response.headers = {} mock_response.json.return_value = {} sess.post = mock.Mock(return_value=mock_response) - url = "stacks/%s/actions" % FAKE_ID + url = f"stacks/{FAKE_ID}/actions" body = {"suspend": None} sot = stack.Stack(**FAKE) @@ -350,7 +350,7 @@ def test_resume(self): mock_response.headers = {} mock_response.json.return_value = {} sess.post = mock.Mock(return_value=mock_response) - url = "stacks/%s/actions" % FAKE_ID + url = f"stacks/{FAKE_ID}/actions" body = {"resume": None} diff --git a/openstack/tests/unit/shared_file_system/v2/test_share.py b/openstack/tests/unit/shared_file_system/v2/test_share.py index f45bc9c6c..61f98fdce 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_share.py +++ b/openstack/tests/unit/shared_file_system/v2/test_share.py @@ -224,7 +224,7 @@ def test_unmanage_share(self): self.assertIsNone(sot.unmanage(self.sess)) - url = 'shares/%s/action' % IDENTIFIER + url = f'shares/{IDENTIFIER}/action' body = {'unmanage': None} self.sess.post.assert_called_with( diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index d1ac99c7a..f5cf7ce1f 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -31,71 +31,65 @@ CONFIG_PROJECT = "TheGrandPrizeGame" CONFIG_CACERT = "TrustMe" -CLOUD_CONFIG = """ +CLOUD_CONFIG = f""" clouds: sample-cloud: region_name: RegionOne auth: - auth_url: {auth_url} - username: {username} - password: {password} - project_name: {project} + auth_url: {CONFIG_AUTH_URL} + username: {CONFIG_USERNAME} + password: {CONFIG_PASSWORD} + project_name: {CONFIG_PROJECT} insecure-cloud: auth: - auth_url: {auth_url} - username: {username} - password: {password} - project_name: {project} - cacert: {cacert} + auth_url: {CONFIG_AUTH_URL} + username: {CONFIG_USERNAME} + password: {CONFIG_PASSWORD} + project_name: {CONFIG_PROJECT} + cacert: {CONFIG_CACERT} verify: False insecure-cloud-alternative-format: auth: - auth_url: {auth_url} - username: {username} - password: {password} - project_name: {project} + auth_url: {CONFIG_AUTH_URL} + username: {CONFIG_USERNAME} + password: {CONFIG_PASSWORD} + project_name: {CONFIG_PROJECT} insecure: True cacert-cloud: auth: - auth_url: {auth_url} - username: {username} - password: {password} - project_name: {project} - cacert: {cacert} + auth_url: {CONFIG_AUTH_URL} + username: {CONFIG_USERNAME} + password: {CONFIG_PASSWORD} + project_name: {CONFIG_PROJECT} + cacert: {CONFIG_CACERT} profiled-cloud: profile: dummy auth: - username: {username} - password: {password} - project_name: {project} - cacert: {cacert} -""".format( - auth_url=CONFIG_AUTH_URL, - username=CONFIG_USERNAME, - password=CONFIG_PASSWORD, - project=CONFIG_PROJECT, - cacert=CONFIG_CACERT, -) - -VENDOR_CONFIG = """ + username: {CONFIG_USERNAME} + password: {CONFIG_PASSWORD} + project_name: {CONFIG_PROJECT} + cacert: {CONFIG_CACERT} +""" + +VENDOR_CONFIG = f""" {{ "name": "dummy", "profile": {{ "auth": {{ - "auth_url": "{auth_url}" + "auth_url": "{CONFIG_AUTH_URL}" }}, "vendor_hook": "openstack.tests.unit.test_connection:vendor_hook" }} }} -""".format(auth_url=CONFIG_AUTH_URL) +""" -PUBLIC_CLOUDS_YAML = """ +PUBLIC_CLOUDS_YAML = f""" public-clouds: dummy: auth: - auth_url: {auth_url} + auth_url: {CONFIG_AUTH_URL} vendor_hook: openstack.tests.unit.test_connection:vendor_hook -""".format(auth_url=CONFIG_AUTH_URL) +""" class _TestConnectionBase(base.TestCase): diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index 78470afb6..9cc02d60b 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -638,7 +638,7 @@ def setUp(self): self.sot.service_type = 'srv' def _get_key(self, id): - return "srv.fake.fake/%s.{'microversion': None, 'params': {}}" % id + return f"srv.fake.fake/{id}.{{'microversion': None, 'params': {{}}}}" def test_get_not_in_cache(self): self.cloud._cache_expirations['srv.fake'] = 5 diff --git a/openstack/tests/unit/test_stats.py b/openstack/tests/unit/test_stats.py index 6a466b1bb..42beb4505 100644 --- a/openstack/tests/unit/test_stats.py +++ b/openstack/tests/unit/test_stats.py @@ -160,7 +160,7 @@ def assert_reported_stat(self, key, value=None, kind=None): return True time.sleep(0.1) - raise Exception("Key %s not found in reported stats" % key) + raise Exception(f"Key {key} not found in reported stats") def assert_prometheus_stat(self, name, value, labels=None): sample_value = self._registry.get_sample_value(name, labels) diff --git a/openstack/utils.py b/openstack/utils.py index 7e54ef4bf..7ccc4c7bc 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -59,8 +59,8 @@ def iterate_timeout(timeout, message, wait=2): wait = float(wait) except ValueError: raise exceptions.SDKException( - "Wait value must be an int or float value. {wait} given" - " instead".format(wait=wait) + f"Wait value must be an int or float value. " + f"{wait} given instead" ) start = time.time() @@ -172,17 +172,15 @@ def supports_microversion(adapter, microversion, raise_exception=False): supports = discover.version_match(required, candidate) if raise_exception and not supports: raise exceptions.SDKException( - 'Required microversion {ver} is higher than currently ' - 'selected {curr}'.format( - ver=microversion, curr=adapter.default_microversion - ) + f'Required microversion {microversion} is higher than ' + f'currently selected {adapter.default_microversion}' ) return supports return True if raise_exception: raise exceptions.SDKException( - 'Required microversion {ver} is not supported ' - 'by the server side'.format(ver=microversion) + f'Required microversion {microversion} is not supported ' + f'by the server side' ) return False diff --git a/openstack/workflow/v2/workflow.py b/openstack/workflow/v2/workflow.py index fc21f0322..8b6df87b0 100644 --- a/openstack/workflow/v2/workflow.py +++ b/openstack/workflow/v2/workflow.py @@ -58,7 +58,7 @@ def _request_kwargs(self, prepend_key=True, base_path=None): "data": self.definition, } - scope = "?scope=%s" % self.scope + scope = f"?scope={self.scope}" uri = request.url + scope request.headers.update(headers) diff --git a/pyproject.toml b/pyproject.toml index 3ab847ac0..7113b0c03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,3 +4,6 @@ line-length = 79 [tool.ruff.format] quote-style = "preserve" docstring-code-format = true + +[tool.ruff.lint] +select = ["E4", "E7", "E9", "F", "U"] diff --git a/tools/nova_version.py b/tools/nova_version.py index ad8ea0cbc..e5593fbc9 100644 --- a/tools/nova_version.py +++ b/tools/nova_version.py @@ -28,7 +28,7 @@ print(endpoint) r = c.get(endpoint).json() except Exception: - print("Error with %s" % cloud.name) + print(f"Error with {cloud.name}") continue for version in r['versions']: if version['status'] == 'CURRENT': diff --git a/tools/print-services.py b/tools/print-services.py index 98602070d..c478fc997 100644 --- a/tools/print-services.py +++ b/tools/print-services.py @@ -47,9 +47,7 @@ def make_names(): dc = desc_class.__name__ services.append( - "{st} = {dm}.{dc}(service_type='{service_type}')".format( - st=st, dm=dm, dc=dc, service_type=service_type - ), + f"{st} = {dm}.{dc}(service_type='{service_type}')", ) # Register the descriptor class with every known alias. Don't From c5d74ad33b59553bf7821e59f0d235b23a8bf3d3 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 13 Nov 2024 16:44:38 +0000 Subject: [PATCH 3600/3836] identity: Add tags, options to Domain We also remove an unnecessary 'name' field since it's defined on the parent Resource class and add a missing links field to Project too. Change-Id: Id1b7b00fe5b96f0cc922716afabcc678193f0f57 Signed-off-by: Stephen Finucane --- openstack/identity/v3/domain.py | 9 ++++++--- openstack/identity/v3/project.py | 3 +++ openstack/tests/unit/identity/v3/test_domain.py | 8 +++++++- openstack/tests/unit/identity/v3/test_project.py | 2 ++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/openstack/identity/v3/domain.py b/openstack/identity/v3/domain.py index 832143195..ba311c0b5 100644 --- a/openstack/identity/v3/domain.py +++ b/openstack/identity/v3/domain.py @@ -10,11 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.common import tag from openstack import resource from openstack import utils -class Domain(resource.Resource): +class Domain(resource.Resource, tag.TagMixin): resource_key = 'domain' resources_key = 'domains' base_path = '/domains' @@ -30,6 +31,7 @@ class Domain(resource.Resource): _query_mapping = resource.QueryParameters( 'name', is_enabled='enabled', + **tag.TagMixin._tag_query_parameters, ) # Properties @@ -43,8 +45,9 @@ class Domain(resource.Resource): #: Re-enabling a domain does not re-enable pre-existing tokens. #: *Type: bool* is_enabled = resource.Body('enabled', type=bool) - #: The globally unique name of this domain. *Type: string* - name = resource.Body('name') + #: The resource options for the project. Available resource options are + #: immutable. + options = resource.Body('options', type=dict) #: The links related to the domain resource. links = resource.Body('links') diff --git a/openstack/identity/v3/project.py b/openstack/identity/v3/project.py index 677ce35d7..324b520cd 100644 --- a/openstack/identity/v3/project.py +++ b/openstack/identity/v3/project.py @@ -9,6 +9,7 @@ # 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.common import tag from openstack import resource from openstack import utils @@ -61,6 +62,8 @@ class Project(resource.Resource, tag.TagMixin): #: The ID of the parent of the project. #: New in version 3.4 parent_id = resource.Body('parent_id') + #: The links related to the project resource. + links = resource.Body('links') def assign_role_to_user(self, session, user, role): """Assign role to user on project""" diff --git a/openstack/tests/unit/identity/v3/test_domain.py b/openstack/tests/unit/identity/v3/test_domain.py index baea9560c..f765b70d0 100644 --- a/openstack/tests/unit/identity/v3/test_domain.py +++ b/openstack/tests/unit/identity/v3/test_domain.py @@ -26,8 +26,9 @@ 'description': '1', 'enabled': True, 'id': IDENTIFIER, - 'links': {'self': 'http://example.com/identity/v3/domains/id'}, + 'links': {'self': f'http://example.com/identity/v3/domains/{IDENTIFIER}'}, 'name': '4', + 'options': {'foo': 'bar'}, } @@ -65,6 +66,10 @@ def test_basic(self): 'is_enabled': 'enabled', 'limit': 'limit', 'marker': 'marker', + 'tags': 'tags', + 'any_tags': 'tags-any', + 'not_tags': 'not-tags', + 'not_any_tags': 'not-tags-any', }, sot._query_mapping._mapping, ) @@ -76,6 +81,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['links'], sot.links) self.assertEqual(EXAMPLE['name'], sot.name) + self.assertDictEqual(EXAMPLE['options'], sot.options) def test_assign_role_to_user_good(self): sot = domain.Domain(**EXAMPLE) diff --git a/openstack/tests/unit/identity/v3/test_project.py b/openstack/tests/unit/identity/v3/test_project.py index 965700951..64c0223ab 100644 --- a/openstack/tests/unit/identity/v3/test_project.py +++ b/openstack/tests/unit/identity/v3/test_project.py @@ -28,6 +28,7 @@ 'enabled': True, 'id': IDENTIFIER, 'is_domain': False, + 'links': {'self': f'http://example.com/identity/v3/projects/{IDENTIFIER}'}, 'name': '5', 'parent_id': '6', 'options': {'foo': 'bar'}, @@ -86,6 +87,7 @@ def test_make_it(self): self.assertFalse(sot.is_domain) self.assertTrue(sot.is_enabled) self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['links'], sot.links) self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['parent_id'], sot.parent_id) self.assertDictEqual(EXAMPLE['options'], sot.options) From 1a196d9eb21612fca85f727806f908077c18d5cc Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Thu, 14 Nov 2024 10:17:15 +0000 Subject: [PATCH 3601/3836] reno: Update master for unmaintained/2023.1 Update the 2023.1 release notes configuration to build from unmaintained/2023.1. Change-Id: I39375a1f9001bdc186efa149845859fe2c09c65f --- releasenotes/source/2023.1.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/source/2023.1.rst b/releasenotes/source/2023.1.rst index d1238479b..2c9a36fae 100644 --- a/releasenotes/source/2023.1.rst +++ b/releasenotes/source/2023.1.rst @@ -3,4 +3,4 @@ =========================== .. release-notes:: - :branch: stable/2023.1 + :branch: unmaintained/2023.1 From 85a14888cd7bcf3e4416fae1849744380271b51a Mon Sep 17 00:00:00 2001 From: elajkat Date: Fri, 12 Apr 2024 17:28:11 +0200 Subject: [PATCH 3602/3836] Add port bindings to SDK Related-Bug: #1580880 Change-Id: Ic963fbca27511a7242eb79ae6256f0aa15b0ddcc --- openstack/network/v2/_proxy.py | 86 +++++++++++++++++++ openstack/network/v2/port_binding.py | 72 ++++++++++++++++ .../unit/network/v2/test_port_binding.py | 45 ++++++++++ openstack/tests/unit/network/v2/test_proxy.py | 48 +++++++++++ 4 files changed, 251 insertions(+) create mode 100644 openstack/network/v2/port_binding.py create mode 100644 openstack/tests/unit/network/v2/test_port_binding.py diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 1ea397046..e571de02a 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -58,6 +58,7 @@ from openstack.network.v2 import pool as _pool from openstack.network.v2 import pool_member as _pool_member from openstack.network.v2 import port as _port +from openstack.network.v2 import port_binding as _port_binding from openstack.network.v2 import port_forwarding as _port_forwarding from openstack.network.v2 import ( qos_bandwidth_limit_rule as _qos_bandwidth_limit_rule, @@ -3010,6 +3011,91 @@ def get_subnet_ports(self, subnet_id): result.append(puerta) return result + def create_port_binding(self, port, **attrs): + """Create a port binding + + :param port: The value can be the ID of a port or a + :class:`~openstack.network.v2.port.Port` instance. + :param attrs: Keyword arguments which will be used to create + a :class:`~openstack.network.v2.port.Port`, + comprised of the properties on the Port class. + + :returns: The results of port binding creation + :rtype: :class:`~openstack.network.v2.port_binding.PortBinding` + """ + port_id = self._get(_port.Port, port).id + return self._create( + _port_binding.PortBinding, + port_id=port_id, + **attrs, + ) + + def activate_port_binding( + self, + port, + **attrs, + ): + """Activate a port binding + + :param port: The value can be the ID of a port or a + :class:`~openstack.network.v2.port.Port` instance. + :param attrs: Keyword arguments which will be used to create + a :class:`~openstack.network.v2.port.Port`, + comprised of the properties on the Port class. + + :returns: The results of port binding creation + :rtype: :class:`~openstack.network.v2.port_binding.PortBinding` + """ + port_id = self._get(_port.Port, port).id + host = attrs['host'] + bindings_on_host = self.port_bindings(port=port_id, host=host) + # There can be only 1 binding on a host at a time + for binding in bindings_on_host: + return binding.activate_port_binding(self, **attrs) + + def port_bindings(self, port, **query): + """Get a single port binding + + :param port: The value can be the ID of a port or a + :class:`~openstack.network.v2.port.Port` instance. + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. Available parameters include: + + * ``host``: The host on which the port is bound. + * ``vif_type``: The mechanism used for the port like bridge or ovs. + * ``vnic_type``: The type of the vnic, like normal or baremetal. + * ``status``: The port status. Value is ``ACTIVE`` or ``DOWN``. + + :returns: A generator of PortBinding objects + :rtype: :class:`~openstack.network.v2.port_binding.PortBinding` + """ + port_id = self._get(_port.Port, port).id + return self._list( + _port_binding.PortBinding, + port_id=port_id, + **query, + ) + + def delete_port_binding(self, port, host): + """Delete a Port Binding + + :param port: The value can be either the ID of a port or a + :class:`~openstack.network.v2.port.Port` instance. + :param host: The host on which the port is bound. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the port does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent port. + + :returns: ``None`` + """ + port_id = self._get(_port.Port, port).id + bindings_on_host = self.port_bindings(port=port_id, host=host) + # There can be only 1 binding on a host at a time + for binding in bindings_on_host: + return binding.delete_port_binding(self, host=host) + def create_qos_bandwidth_limit_rule(self, qos_policy, **attrs): """Create a new bandwidth limit rule diff --git a/openstack/network/v2/port_binding.py b/openstack/network/v2/port_binding.py new file mode 100644 index 000000000..2b339fbf5 --- /dev/null +++ b/openstack/network/v2/port_binding.py @@ -0,0 +1,72 @@ +# 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 import resource +from openstack import utils + + +class PortBinding(resource.Resource): + name_attribute = "bindings" + resource_name = "binding" + resource_key = 'binding' + resources_key = 'bindings' + base_path = '/ports/%(port_id)s/bindings' + + _allow_unknown_attrs_in_body = True + + # capabilities + allow_create = True + allow_fetch = False + allow_commit = True + allow_delete = True + allow_list = True + + requires_id = False + + # Properties + #: The port ID of the binding + port_id = resource.URI('port_id') + #: The hostname of the system the agent is running on. + host = resource.Body('host') + #: A dictionary that enables the application running on + # the specific host to pass and receive vif port information + # specific to the networking back-end. + profile = resource.Body('profile', type=dict) + #: A dictionary which contains additional information on the + # port. The following fields are defined: port_filter and + # ovs_hybrid_plug, both are booleans. + vif_details = resource.Body('vif_details', type=dict) + #: The type of which mechanism is used for the port. + # Currently the following values are supported: ovs, bridge, + # macvtap, hw_veb, hostdev_physical, vhostuser, distributed and + # other. + vif_type = resource.Body('vif_type') + #: The type of vNIC which this port should be attached to. + # The valid values are normal, macvtap, direct, baremetal, + # direct-physical, virtio-forwarder, smart-nic and remote-managed. + vnic_type = resource.Body('vnic_type') + + def activate_port_binding(self, session, **attrs): + host = attrs['host'] + url = utils.urljoin( + '/ports', self.port_id, 'bindings', host, 'activate' + ) + resp = session.put(url, json={'binding': attrs}) + exceptions.raise_from_response(resp) + self._body.attributes.update(resp.json()) + return self + + def delete_port_binding(self, session, host): + url = utils.urljoin('/ports', self.port_id, 'bindings', host) + resp = session.delete(url) + exceptions.raise_from_response(resp) diff --git a/openstack/tests/unit/network/v2/test_port_binding.py b/openstack/tests/unit/network/v2/test_port_binding.py new file mode 100644 index 000000000..79e162e2e --- /dev/null +++ b/openstack/tests/unit/network/v2/test_port_binding.py @@ -0,0 +1,45 @@ +# 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.network.v2 import port_binding +from openstack.tests.unit import base + + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'host': 'host1', + 'profile': {}, + 'vif_details': {'bridge_name': 'br-int'}, + 'vif_type': 'ovs', + 'vnic_type': 'normal', +} + + +class TestPortBinding(base.TestCase): + def test_basic(self): + sot = port_binding.PortBinding() + self.assertEqual('binding', sot.resource_key) + self.assertEqual('bindings', sot.resources_key) + self.assertEqual('/ports/%(port_id)s/bindings', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertFalse(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = port_binding.PortBinding(**EXAMPLE) + self.assertEqual(EXAMPLE['host'], sot.host) + self.assertEqual(EXAMPLE['profile'], sot.profile) + self.assertEqual(EXAMPLE['vif_details'], sot.vif_details) + self.assertEqual(EXAMPLE['vif_type'], sot.vif_type) + self.assertCountEqual(EXAMPLE['vnic_type'], sot.vnic_type) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 3b8265101..b70945851 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -47,6 +47,7 @@ from openstack.network.v2 import pool from openstack.network.v2 import pool_member from openstack.network.v2 import port +from openstack.network.v2 import port_binding from openstack.network.v2 import port_forwarding from openstack.network.v2 import qos_bandwidth_limit_rule from openstack.network.v2 import qos_dscp_marking_rule @@ -83,6 +84,7 @@ CT_HELPER_ID = 'ct-helper-id-' + uuid.uuid4().hex LOCAL_IP_ID = 'lip-id-' + uuid.uuid4().hex BGPVPN_ID = 'bgpvpn-id-' + uuid.uuid4().hex +PORT_ID = 'port-id-' + uuid.uuid4().hex class TestNetworkProxy(test_proxy_base.TestProxyBase): @@ -2666,3 +2668,49 @@ def test_tap_mirrors(self): def test_update_tap_mirror(self): self.verify_update(self.proxy.update_tap_mirror, tap_mirror.TapMirror) + + +class TestNetworkPortBinding(TestNetworkProxy): + @mock.patch.object(proxy_base.Proxy, '_get') + def test_create_port_binding(self, mock_get): + res_port = port.Port.new(id=PORT_ID) + mock_get.return_value = res_port + + self.verify_create( + self.proxy.create_port_binding, + port_binding.PortBinding, + method_kwargs={'port': PORT_ID}, + expected_kwargs={'port_id': PORT_ID}, + ) + + @mock.patch('openstack.network.v2._proxy.Proxy.activate_port_binding') + def test_activate_port_binding(self, activate_binding): + data = mock.sentinel + self.proxy.activate_port_binding(port_binding.PortBinding, data) + activate_binding.assert_called_once_with( + port_binding.PortBinding, data + ) + + @mock.patch.object(proxy_base.Proxy, '_get') + def test_port_bindings(self, mock_get): + res_port = port.Port.new(id=PORT_ID) + mock_get.return_value = res_port + + self.verify_list( + self.proxy.port_bindings, + port_binding.PortBinding, + method_kwargs={'port': PORT_ID}, + expected_kwargs={'port_id': PORT_ID}, + ) + + @mock.patch('openstack.network.v2._proxy.Proxy.delete_port_binding') + @mock.patch.object(proxy_base.Proxy, '_get') + def test_delete_port_binding(self, mock_get, delete_port_binding): + res_port = port.Port.new(id=PORT_ID) + mock_get.return_value = res_port + data = mock.sentinel + + self.proxy.delete_port_binding(port_binding.PortBinding, data) + delete_port_binding.assert_called_once_with( + port_binding.PortBinding, data + ) From 6ad5fbe1778ef06dbf028357aabe073e74de071f Mon Sep 17 00:00:00 2001 From: Clark Boylan Date: Thu, 14 Nov 2024 14:21:16 -0800 Subject: [PATCH 3603/3836] Fix swift metadata setting documentation Prevuously the docs indicated you could modify a container object in place to set new metadata then pass that container to set_container_metadata() to update the metadata to match in swift. Experimentally this doesn't work with at least one public cloud. Instead I had to pass the metadata I wished to update as kwargs to set_container_metadata. This matches the documentation for set_container_metadata [0]; let's update the example docs to match. [0] https://docs.openstack.org/openstacksdk/latest/user/proxies/object_store.html#container-operations Change-Id: I023ff425789ef6d5649ace30f9f667dcf6ce4fef --- doc/source/user/guides/object_store.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/source/user/guides/object_store.rst b/doc/source/user/guides/object_store.rst index 094bac5e9..dab88096e 100644 --- a/doc/source/user/guides/object_store.rst +++ b/doc/source/user/guides/object_store.rst @@ -77,12 +77,12 @@ To set the metadata for a container, use the :meth:`~openstack.object_store.v1._proxy.Proxy.set_container_metadata` method. This method takes a :class:`~openstack.object_store.v1.container.Container` object. For example, to grant another user write access to this container, -you can set the -:attr:`~openstack.object_store.v1.container.Container.write_ACL` on a -resource and pass it to `set_container_metadata`. :: +you can call `set_container_metadata` passing it the `Container` to update +and keyward argument key/value pairs representing the metadata name and +value to set. :: - >>> cont.write_ACL = "big_project:another_user" - >>> conn.object_store.set_container_metadata(cont) + >>> acl = "big_project:another_user" + >>> conn.object_store.set_container_metadata(cont, write_ACL=acl) openstack.object_store.v1.container.Container: {'content-length': '0', 'x-container-object-count': '0', 'name': u'my new container', 'accept-ranges': 'bytes', From 1c13a05e13363740e62178502c4d8ae86b5d9f5e Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Mon, 18 Nov 2024 12:59:46 +0900 Subject: [PATCH 3604/3836] Remove reference to removed nova services nova-consoleauth and nova-objectstore were both removed long ago. Change-Id: I0c25ac9647fe1d90205144f85dd6c94f7fbf4f1a --- zuul.d/functional-jobs.yaml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/zuul.d/functional-jobs.yaml b/zuul.d/functional-jobs.yaml index a0a0b08aa..ca397634f 100644 --- a/zuul.d/functional-jobs.yaml +++ b/zuul.d/functional-jobs.yaml @@ -39,7 +39,6 @@ q-metering: true q-svc: true # sdk doesn't need vnc access - n-cauth: false n-novnc: false # sdk testing uses config drive only n-api-meta: false @@ -290,11 +289,9 @@ s-proxy: false n-api: false n-api-meta: false - n-cauth: false n-cond: false n-cpu: false n-novnc: false - n-obj: false n-sch: false nova: false placement-api: false @@ -432,11 +429,9 @@ s-proxy: false n-api: false n-api-meta: false - n-cauth: false n-cond: false n-cpu: false n-novnc: false - n-obj: false n-sch: false nova: false placement-api: false From 85aa947ee7293401db5ea187c5fe214370694ffd Mon Sep 17 00:00:00 2001 From: "Dr. Jens Harbott" Date: Fri, 22 Nov 2024 12:54:40 +0100 Subject: [PATCH 3605/3836] Skip disabling compute service The tests.functional.compute.v2.test_service.TestService.disable_enable test seems to sometimes cause following tests to fail until the compute service has fully recovered. Change-Id: Ie0c11e487404375d4341bfc50e76aa6b117de601 --- openstack/tests/functional/compute/v2/test_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openstack/tests/functional/compute/v2/test_service.py b/openstack/tests/functional/compute/v2/test_service.py index 70d3941dc..e2fd99d20 100644 --- a/openstack/tests/functional/compute/v2/test_service.py +++ b/openstack/tests/functional/compute/v2/test_service.py @@ -23,6 +23,7 @@ def test_list(self): self.assertIsNotNone(sot) def test_disable_enable(self): + self.skipTest("Test is breaking tests that follow") for srv in self.conn.compute.services(): # only nova-compute can be updated if srv.name == 'nova-compute': From 7df9a721efdbe854e6fd4d39d6d1c92c5d1c6c43 Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Mon, 7 Oct 2024 01:08:31 +0900 Subject: [PATCH 3606/3836] Replace netifaces library The natifaces library[1] was abandoned more than 3 years ago. Replace it by psutils which is already used by a few other projects. [1] https://github.com/al45tair/netifaces Closes-Bug: #2071596 Change-Id: I805321229e84a57312bbe160d330281e6c13ab97 --- openstack/cloud/_utils.py | 22 ++++++++++++------- .../replace-netifaces-632f60884fb7ae00.yaml | 7 ++++++ requirements.txt | 2 +- 3 files changed, 22 insertions(+), 9 deletions(-) create mode 100644 releasenotes/notes/replace-netifaces-632f60884fb7ae00.yaml diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index a4f5ebc1f..5f7efc65f 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -15,12 +15,14 @@ import contextlib import fnmatch import inspect +import ipaddress import re +import socket import uuid from decorator import decorator import jmespath -import netifaces +import psutil from openstack import _log from openstack import exceptions @@ -182,15 +184,19 @@ def _get_entity(cloud, resource, name_or_id, filters, **kwargs): def localhost_supports_ipv6(): """Determine whether the local host supports IPv6 - We look for a default route that supports the IPv6 address family, - and assume that if it is present, this host has globally routable - IPv6 connectivity. + We look for the all ip addresses configured to this node, and assume that + if any of these is IPv6 address (but not loopback or link local), this host + has IPv6 connectivity. """ - try: - return netifaces.AF_INET6 in netifaces.gateways()['default'] - except AttributeError: - return False + for ifname, if_addrs in psutil.net_if_addrs().items(): + for if_addr in if_addrs: + if if_addr.family != socket.AF_INET6: + continue + addr = ipaddress.ip_address(if_addr.address) + if not addr.is_link_local and not addr.is_loopback: + return True + return False def valid_kwargs(*valid_args): diff --git a/releasenotes/notes/replace-netifaces-632f60884fb7ae00.yaml b/releasenotes/notes/replace-netifaces-632f60884fb7ae00.yaml new file mode 100644 index 000000000..a7f97eec3 --- /dev/null +++ b/releasenotes/notes/replace-netifaces-632f60884fb7ae00.yaml @@ -0,0 +1,7 @@ +--- +upgrade: + - | + IPv6 support now is detected according to the IP addresses assigned to all + network interfaces, instead of presence of IPv6 default route. In case + there is any IP v6 address, which is not loopback or link-local, then + the host is considered to support IPv6. diff --git a/requirements.txt b/requirements.txt index ad1c7e95a..1637cc40e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,9 +5,9 @@ iso8601>=0.1.11 # MIT jmespath>=0.9.0 # MIT jsonpatch!=1.20,>=1.16 # BSD keystoneauth1>=3.18.0 # Apache-2.0 -netifaces>=0.10.4 # MIT os-service-types>=1.7.0 # Apache-2.0 pbr!=2.1.0,>=2.0.0 # Apache-2.0 platformdirs>=3 # MIT License +psutil>=3.2.2 # BSD PyYAML>=3.13 # MIT requestsexceptions>=1.2.0 # Apache-2.0 From 413c68f9d4cc50b876ef3435d506e8200ee2912a Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 27 Nov 2024 12:20:50 +0000 Subject: [PATCH 3607/3836] docs: Update authentication configuration examples Correct some typos that slipped through. Change-Id: I2c4353895d9317fb6dce93d6a4af8908937093c9 Signed-off-by: Stephen Finucane --- doc/source/user/config/configuration.rst | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/doc/source/user/config/configuration.rst b/doc/source/user/config/configuration.rst index 6f8525bde..3bbba6f96 100644 --- a/doc/source/user/config/configuration.rst +++ b/doc/source/user/config/configuration.rst @@ -520,7 +520,7 @@ Examples ``auth`` ~~~~~~~~ -.. rubric:: Password-based authentication (domain-scoped) +.. rubric:: Password-based authentication (project-scoped) .. code-block:: yaml @@ -534,6 +534,18 @@ Examples username: admin region_name: RegionOne +.. rubric:: Password-based authentication (domain-scoped) + +.. code-block:: yaml + + example: + auth: + auth_url: http://example.com/identity + domain_id: default + password: password + username: admin + region_name: RegionOne + .. rubric:: Password-based authentication (trust-scoped) .. code-block:: yaml @@ -584,8 +596,8 @@ Examples .. note:: - This is a toy example: by their very definition token's are short-lived. - You are unlikely to store them in a `clouds.yaml` file. + This is a toy example: by their very definition tokens are short-lived. + You are unlikely to store them in a ``clouds.yaml`` file. Instead, you would likely pass the TOTP token via the command line (``--os-token``) or as an environment variable (``OS_TOKEN``). @@ -606,8 +618,8 @@ Examples .. note:: - This is a toy example: by their very definition TOTP token's are - short-lived. You are unlikely to store them in a `clouds.yaml` file. + This is a toy example: by their very definition TOTP tokens are + short-lived. You are unlikely to store them in a ``clouds.yaml`` file. Instead, you would likely pass the TOTP token via the command line (``--os-passcode``) or as an environment variable (``OS_PASSCODE``). From ad6beea71dbeeefbbb84a9ff7fe2433120b459b0 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 3 Dec 2024 15:10:11 +0000 Subject: [PATCH 3608/3836] compute: Use security group name or ID Despite what Nova's api-ref currently says [1], the 'addSecurityGroup' action only requires a security group name if using nova-network: it will happily accept either a name or ID if using neutron. We are using 'Proxy._get_resource' to allow users to pass either an ID or an 'openstack.network.v2.security_group.SecurityGroup' object (there's no implementation of Nova's deprecated SecurityGroup resource). If given an ID, this help method will only populate the 'id' field of the created resource. Thus, we need to use this if provided. [1] https://docs.openstack.org/api-ref/compute/#add-security-group-to-a-server-addsecuritygroup-action Change-Id: I3dd56414b32207a16c6c83971f0494ccb86e07d4 Signed-off-by: Stephen Finucane Closes-bug: #2089821 --- openstack/compute/v2/_proxy.py | 14 ++++++++++---- openstack/tests/unit/compute/v2/test_proxy.py | 4 ++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 5dac96bcd..2e0d3d76b 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -1267,7 +1267,7 @@ def add_security_group_to_server(self, server, security_group): :param server: Either the ID of a server or a :class:`~openstack.compute.v2.server.Server` instance. - :param security_group: Either the ID, Name of a security group or a + :param security_group: Either the ID or name of a security group or a :class:`~openstack.network.v2.security_group.SecurityGroup` instance. @@ -1275,14 +1275,17 @@ def add_security_group_to_server(self, server, security_group): """ server = self._get_resource(_server.Server, server) security_group = self._get_resource(_sg.SecurityGroup, security_group) - server.add_security_group(self, security_group.name) + server.add_security_group( + self, + security_group.name or security_group.id, + ) def remove_security_group_from_server(self, server, security_group): """Remove a security group from a server :param server: Either the ID of a server or a :class:`~openstack.compute.v2.server.Server` instance. - :param security_group: Either the ID of a security group or a + :param security_group: Either the ID or name of a security group or a :class:`~openstack.network.v2.security_group.SecurityGroup` instance. @@ -1290,7 +1293,10 @@ def remove_security_group_from_server(self, server, security_group): """ server = self._get_resource(_server.Server, server) security_group = self._get_resource(_sg.SecurityGroup, security_group) - server.remove_security_group(self, security_group.name) + server.remove_security_group( + self, + security_group.name or security_group.id, + ) # ========== Server IPs ========== diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 09cf2a50e..9fa750d61 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -1528,7 +1528,7 @@ def test_add_security_groups(self): self._verify( 'openstack.compute.v2.server.Server.add_security_group', self.proxy.add_security_group_to_server, - method_args=["value", {'id': 'id', 'name': 'sg'}], + method_args=["value", 'sg'], expected_args=[self.proxy, 'sg'], ) @@ -1536,7 +1536,7 @@ def test_remove_security_groups(self): self._verify( 'openstack.compute.v2.server.Server.remove_security_group', self.proxy.remove_security_group_from_server, - method_args=["value", {'id': 'id', 'name': 'sg'}], + method_args=["value", 'sg'], expected_args=[self.proxy, 'sg'], ) From 66c387de7d66282019d9c94d3eb84f9ef7a0758e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 9 Dec 2024 18:23:30 +0000 Subject: [PATCH 3609/3836] identity: Extract info from created limit The server returns a list response that we must extract. This involves a lot of duplication of the parent class' method, but it is the best we can do currently. Change-Id: I623e0bdf9fc26a259880b9e5fd845b7e60e9c746 Signed-off-by: Stephen Finucane --- openstack/identity/v3/limit.py | 67 ++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/openstack/identity/v3/limit.py b/openstack/identity/v3/limit.py index 54190932b..a3edd26d4 100644 --- a/openstack/identity/v3/limit.py +++ b/openstack/identity/v3/limit.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import exceptions from openstack import resource @@ -63,3 +64,69 @@ def _prepare_request_body( # request body for creating limit is a list instead of dict. body = {self.resources_key: [body]} return body + + def _translate_response( + self, + response, + has_body=None, + error_message=None, + *, + resource_response_key='limits', + ): + """Given a KSA response, inflate this instance with its data + + DELETE operations don't return a body, so only try to work + with a body when has_body is True. + + This method updates attributes that correspond to headers + and body on this instance and clears the dirty set. + """ + if has_body is None: + has_body = self.has_body + + exceptions.raise_from_response(response, error_message=error_message) + + if has_body: + try: + body = response.json() + if resource_response_key and resource_response_key in body: + body = body[resource_response_key] + elif self.resource_key and self.resource_key in body: + body = body[self.resource_key] + + # Keystone support bunch create for unified limit. So the + # response body for creating limit is a list instead of dict. + if isinstance(body, list): + body = body[0] + + # Do not allow keys called "self" through. Glance chose + # to name a key "self", so we need to pop it out because + # we can't send it through cls.existing and into the + # Resource initializer. "self" is already the first + # argument and is practically a reserved word. + body.pop("self", None) + + body_attrs = self._consume_body_attrs(body) + if self._allow_unknown_attrs_in_body: + body_attrs.update(body) + self._unknown_attrs_in_body.update(body) + elif self._store_unknown_attrs_as_properties: + body_attrs = self._pack_attrs_under_properties( + body_attrs, body + ) + + self._body.attributes.update(body_attrs) + self._body.clean() + if self.commit_jsonpatch or self.allow_patch: + # We need the original body to compare against + self._original_body = body_attrs.copy() + except ValueError: + # Server returned not parse-able response (202, 204, etc) + # Do simply nothing + pass + + headers = self._consume_header_attrs(response.headers) + self._header.attributes.update(headers) + self._header.clean() + self._update_location() + dict.update(self, self.to_dict()) From 371efde4f0b83d4e92d13a16f888b9a33784b8e0 Mon Sep 17 00:00:00 2001 From: Antonia Gaete Date: Thu, 7 Nov 2024 19:07:27 +0000 Subject: [PATCH 3610/3836] identity: Add support for project endpoints Change-Id: I9aa39810fe94f7ee9b68d34050f4adb9dbdfccb8 --- doc/source/user/proxies/identity_v3.rst | 2 +- openstack/identity/v3/_proxy.py | 17 +++++++++++++++++ openstack/identity/v3/endpoint.py | 14 ++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/doc/source/user/proxies/identity_v3.rst b/doc/source/user/proxies/identity_v3.rst index 64a87f5b6..5e9001212 100644 --- a/doc/source/user/proxies/identity_v3.rst +++ b/doc/source/user/proxies/identity_v3.rst @@ -42,7 +42,7 @@ Endpoint Operations .. autoclass:: openstack.identity.v3._proxy.Proxy :noindex: :members: create_endpoint, update_endpoint, delete_endpoint, get_endpoint, - find_endpoint, endpoints + find_endpoint, endpoints, project_endpoints Group Operations ^^^^^^^^^^^^^^^^ diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 860d994e3..ef8531e0c 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -414,6 +414,23 @@ def update_endpoint(self, endpoint, **attrs): """ return self._update(_endpoint.Endpoint, endpoint, **attrs) + def project_endpoints(self, project, **query): + """Retrieve a generator of endpoints which are associated with the + project. + + :param project: Either the project ID or an instance of + :class:`~openstack.identity.v3.project.Project` + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. + + :returns: A generator of endpoint instances. + :rtype: :class:`~openstack.identity.v3.endpoint.ProjectEndpoint` + """ + project_id = self._get_resource(_project.Project, project).id + return self._list( + _endpoint.ProjectEndpoint, project_id=project_id, **query + ) + # ========== Groups ========== def create_group(self, **attrs): diff --git a/openstack/identity/v3/endpoint.py b/openstack/identity/v3/endpoint.py index bc58135c2..0fb3fca9d 100644 --- a/openstack/identity/v3/endpoint.py +++ b/openstack/identity/v3/endpoint.py @@ -57,3 +57,17 @@ class Endpoint(resource.Resource): service_id = resource.Body('service_id') #: Fully qualified URL of the service endpoint. *Type: string* url = resource.Body('url') + + +class ProjectEndpoint(Endpoint): + base_path = '/OS-EP-FILTER/projects/%(project_id)s/endpoints' + + #: The ID for the project from the URI of the resource + project_id = resource.URI('project_id') + + # capabilities + allow_create = False + allow_fetch = False + allow_commit = False + allow_delete = False + allow_list = True From 6c52edae93821406838e8b11ebc1fb2f270ea964 Mon Sep 17 00:00:00 2001 From: Antonia Gaete Date: Thu, 7 Nov 2024 19:07:27 +0000 Subject: [PATCH 3611/3836] identity: Add support for endpoint/project associations https://docs.openstack.org/api-ref/identity/v3-ext/#create-association Change-Id: I0e447fdb7a7c620bcb12ec3a40620180ead397ee --- doc/source/user/proxies/identity_v3.rst | 3 +- openstack/identity/v3/_proxy.py | 28 ++++++++++++++++ openstack/identity/v3/project.py | 33 +++++++++++++++++++ .../tests/unit/identity/v3/test_proxy.py | 27 ++++++++++++++- 4 files changed, 89 insertions(+), 2 deletions(-) diff --git a/doc/source/user/proxies/identity_v3.rst b/doc/source/user/proxies/identity_v3.rst index e044066f9..548abf4ca 100644 --- a/doc/source/user/proxies/identity_v3.rst +++ b/doc/source/user/proxies/identity_v3.rst @@ -42,7 +42,8 @@ Endpoint Operations .. autoclass:: openstack.identity.v3._proxy.Proxy :noindex: :members: create_endpoint, update_endpoint, delete_endpoint, get_endpoint, - find_endpoint, endpoints, project_endpoints + find_endpoint, endpoints, project_endpoints, + associate_endpoint_with_project, disassociate_endpoint_from_project Group Operations ^^^^^^^^^^^^^^^^ diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index b37a8eeb1..45cbaa042 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -414,6 +414,8 @@ def update_endpoint(self, endpoint, **attrs): """ return self._update(_endpoint.Endpoint, endpoint, **attrs) + # ========== Project endpoints ========== + def project_endpoints(self, project, **query): """Retrieve a generator of endpoints which are associated with the project. @@ -431,6 +433,32 @@ def project_endpoints(self, project, **query): _endpoint.ProjectEndpoint, project_id=project_id, **query ) + def associate_endpoint_with_project(self, project, endpoint): + """Creates a direct association between project and endpoint + + :param project: Either the ID of a project or a + :class:`~openstack.identity.v3.project.Project` instance. + :param endpoint: Either the ID of an endpoint or a + :class:`~openstack.identity.v3.endpoint.Endpoint` instance. + :returns: None + """ + project = self._get_resource(_project.Project, project) + endpoint = self._get_resource(_endpoint.Endpoint, endpoint) + project.associate_endpoint(self, endpoint.id) + + def disassociate_endpoint_from_project(self, project, endpoint): + """Removes a direct association between project and endpoint + + :param project: Either the ID of a project or a + :class:`~openstack.identity.v3.project.Project` instance. + :param endpoint: Either the ID of an endpoint or a + :class:`~openstack.identity.v3.endpoint.Endpoint` instance. + :returns: None + """ + project = self._get_resource(_project.Project, project) + endpoint = self._get_resource(_endpoint.Endpoint, endpoint) + project.disassociate_endpoint(self, endpoint.id) + # ========== Groups ========== def create_group(self, **attrs): diff --git a/openstack/identity/v3/project.py b/openstack/identity/v3/project.py index f5f23196c..bb56d4c42 100644 --- a/openstack/identity/v3/project.py +++ b/openstack/identity/v3/project.py @@ -11,6 +11,7 @@ # under the License. from openstack.common import tag +from openstack import exceptions from openstack import resource from openstack import utils @@ -159,6 +160,38 @@ def unassign_role_from_group(self, session, group, role, inherited): return True return False + def associate_endpoint(self, session, endpoint_id): + """Associate endpoint with project. + + :param session: The session to use for making this request. + :param endpoint_id: The ID of an endpoint. + :returns: None + """ + url = utils.urljoin( + '/OS-EP-FILTER/projects', + self.id, + 'endpoints', + endpoint_id, + ) + response = session.put(url) + exceptions.raise_from_response(response) + + def disassociate_endpoint(self, session, endpoint_id): + """Disassociate endpoint from project. + + :param session: The session to use for making this request. + :param endpoint_id: The ID of an endpoint. + :returns: None + """ + url = utils.urljoin( + '/OS-EP-FILTER/projects', + self.id, + 'endpoints', + endpoint_id, + ) + response = session.delete(url) + exceptions.raise_from_response(response) + class UserProject(Project): base_path = '/users/%(user_id)s/projects' diff --git a/openstack/tests/unit/identity/v3/test_proxy.py b/openstack/tests/unit/identity/v3/test_proxy.py index 1556fe169..e582f31f8 100644 --- a/openstack/tests/unit/identity/v3/test_proxy.py +++ b/openstack/tests/unit/identity/v3/test_proxy.py @@ -36,7 +36,8 @@ from openstack.tests.unit import test_proxy_base USER_ID = 'user-id-' + uuid.uuid4().hex -ENDPOINT_ID = 'user-id-' + uuid.uuid4().hex +ENDPOINT_ID = 'endpoint-id-' + uuid.uuid4().hex +PROJECT_ID = 'project-id-' + uuid.uuid4().hex class TestIdentityProxyBase(test_proxy_base.TestProxyBase): @@ -185,6 +186,14 @@ def test_endpoint_get(self): def test_endpoints(self): self.verify_list(self.proxy.endpoints, endpoint.Endpoint) + def test_project_endpoints(self): + self.verify_list( + self.proxy.project_endpoints, + endpoint.ProjectEndpoint, + method_kwargs={'project': PROJECT_ID}, + expected_kwargs={'project_id': PROJECT_ID}, + ) + def test_endpoint_update(self): self.verify_update(self.proxy.update_endpoint, endpoint.Endpoint) @@ -314,6 +323,22 @@ def test_endpoint_projects(self): def test_project_update(self): self.verify_update(self.proxy.update_project, project.Project) + def test_project_associate_endpoint(self): + self._verify( + 'openstack.identity.v3.project.Project.associate_endpoint', + self.proxy.associate_endpoint_with_project, + method_args=['project_id', 'endpoint_id'], + expected_args=[self.proxy, 'endpoint_id'], + ) + + def test_project_disassociate_endpoint(self): + self._verify( + 'openstack.identity.v3.project.Project.disassociate_endpoint', + self.proxy.disassociate_endpoint_from_project, + method_args=['project_id', 'endpoint_id'], + expected_args=[self.proxy, 'endpoint_id'], + ) + class TestIdentityProxyService(TestIdentityProxyBase): def test_service_create_attrs(self): From 8d443a45610cf1b811dd1d1a023e1c2b31a674ea Mon Sep 17 00:00:00 2001 From: yatinkarel Date: Wed, 18 Dec 2024 09:24:28 +0530 Subject: [PATCH 3612/3836] Skip add_tags test when tag-creation extension disabled Was missed in recent patch[1] which added the support. [1] https://review.opendev.org/c/openstack/openstacksdk/+/927779 Related-Bug: #2073836 Change-Id: Icdcf4418ce1bc6524265bdaa339ba396df00d545 --- openstack/tests/functional/network/v2/common.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openstack/tests/functional/network/v2/common.py b/openstack/tests/functional/network/v2/common.py index 8908d524e..cbabe0106 100644 --- a/openstack/tests/functional/network/v2/common.py +++ b/openstack/tests/functional/network/v2/common.py @@ -90,6 +90,10 @@ def test_remove_all_tags(self): self.assertEqual([], sot.tags) def test_add_tags(self): + # Skip the test if tag-creation extension is not enabled. + if not self.user_cloud.network.find_extension("tag-creation"): + self.skipTest("Network tag-creation extension disabled") + sot = self.get_command(self.ID) self.assertEqual([], sot.tags) From 597c53fc0eeec9802846ffd20eb391ae363a10bb Mon Sep 17 00:00:00 2001 From: rornfl916 Date: Mon, 12 Aug 2024 20:26:24 +0900 Subject: [PATCH 3613/3836] dns: Add zone nameservers support Change-Id: Iaba051a3d058525388cd00d64af653c3919e5a21 --- openstack/dns/v2/_proxy.py | 19 +++++++++ openstack/dns/v2/zone_nameserver.py | 37 +++++++++++++++++ openstack/tests/unit/dns/v2/test_proxy.py | 11 +++++ .../tests/unit/dns/v2/test_zone_nameserver.py | 40 +++++++++++++++++++ 4 files changed, 107 insertions(+) create mode 100644 openstack/dns/v2/zone_nameserver.py create mode 100644 openstack/tests/unit/dns/v2/test_zone_nameserver.py diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py index 523426364..30466913a 100644 --- a/openstack/dns/v2/_proxy.py +++ b/openstack/dns/v2/_proxy.py @@ -18,9 +18,11 @@ from openstack.dns.v2 import zone as _zone from openstack.dns.v2 import zone_export as _zone_export from openstack.dns.v2 import zone_import as _zone_import +from openstack.dns.v2 import zone_nameserver as _zone_nameserver from openstack.dns.v2 import zone_share as _zone_share from openstack.dns.v2 import zone_transfer as _zone_transfer from openstack import proxy +from openstack import resource class Proxy(proxy.Proxy): @@ -33,6 +35,7 @@ class Proxy(proxy.Proxy): "tsigkey": _tsigkey.TSIGKey, "zone_export": _zone_export.ZoneExport, "zone_import": _zone_import.ZoneImport, + "zone_nameserver": _zone_nameserver.ZoneNameserver, "zone_share": _zone_share.ZoneShare, "zone_transfer_request": _zone_transfer.ZoneTransferRequest, } @@ -151,6 +154,22 @@ def xfr_zone(self, zone, **attrs): zone = self._get_resource(_zone.Zone, zone) return zone.xfr(self) + # ======== Zone nameservers ======== + def zone_nameservers(self, zone): + """Retrieve a generator of nameservers for a zone + + :param zone: The value can be the ID of a zone or a + :class:`~openstack.dns.v2.zone.Zone` instance. + :return: A generator of + :class:`~openstack.dns.v2.zone_nameserver.ZoneNameserver` + instances. + """ + zone_id = resource.Resource._get_id(zone) + return self._list( + _zone_nameserver.ZoneNameserver, + zone_id=zone_id, + ) + # ======== Recordsets ======== def recordsets(self, zone=None, **query): """Retrieve a generator of recordsets diff --git a/openstack/dns/v2/zone_nameserver.py b/openstack/dns/v2/zone_nameserver.py new file mode 100644 index 000000000..f91871f0a --- /dev/null +++ b/openstack/dns/v2/zone_nameserver.py @@ -0,0 +1,37 @@ +# 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.dns.v2 import _base +from openstack import resource + + +class ZoneNameserver(_base.Resource): + """DNS Zone Nameserver resource""" + + resources_key = 'nameservers' + base_path = '/zones/%(zone_id)s/nameservers' + + # capabilities + allow_list = True + + _query_mapping = resource.QueryParameters( + include_pagination_defaults=False, + ) + + #: ID for the zone + zone_id = resource.URI('zone_id') + #: The hostname of the nameserver + #: *Type: str* + hostname = resource.Body('hostname') + #: The priority of the nameserver + #: *Type: int* + priority = resource.Body('priority') diff --git a/openstack/tests/unit/dns/v2/test_proxy.py b/openstack/tests/unit/dns/v2/test_proxy.py index c08d5cc6a..aacc699ae 100644 --- a/openstack/tests/unit/dns/v2/test_proxy.py +++ b/openstack/tests/unit/dns/v2/test_proxy.py @@ -18,6 +18,7 @@ from openstack.dns.v2 import zone from openstack.dns.v2 import zone_export from openstack.dns.v2 import zone_import +from openstack.dns.v2 import zone_nameserver from openstack.dns.v2 import zone_share from openstack.dns.v2 import zone_transfer from openstack.tests.unit import test_proxy_base @@ -75,6 +76,16 @@ def test_zone_xfr(self): ) +class TestDnsZoneNameserver(TestDnsProxy): + def test_get_zone_nameservers(self): + self.verify_list( + self.proxy.zone_nameservers, + zone_nameserver.ZoneNameserver, + method_kwargs={'zone': 'id'}, + expected_kwargs={'zone_id': 'id'}, + ) + + class TestDnsRecordset(TestDnsProxy): def test_recordset_create(self): self.verify_create( diff --git a/openstack/tests/unit/dns/v2/test_zone_nameserver.py b/openstack/tests/unit/dns/v2/test_zone_nameserver.py new file mode 100644 index 000000000..76d98fa89 --- /dev/null +++ b/openstack/tests/unit/dns/v2/test_zone_nameserver.py @@ -0,0 +1,40 @@ +# 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.dns.v2 import zone_nameserver +from openstack.tests.unit import base + + +class TestZoneNameserver(base.TestCase): + def test_basic(self): + sot = zone_nameserver.ZoneNameserver() + self.assertEqual(None, sot.resource_key) + self.assertEqual('nameservers', sot.resources_key) + self.assertEqual('/zones/%(zone_id)s/nameservers', sot.base_path) + self.assertTrue(sot.allow_list) + self.assertFalse(sot.allow_create) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_delete) + self.assertFalse(sot.allow_commit) + + self.assertDictEqual({}, sot._query_mapping._mapping) + + def test_make_it(self): + hostname = 'bogus-hostname' + priority = 123 + + sot = zone_nameserver.ZoneNameserver( + hostname=hostname, priority=priority + ) + self.assertEqual(hostname, sot.hostname) + self.assertEqual(priority, sot.priority) From 059ae3f8f8c7ddc2f6f17dbb05dec40289649d31 Mon Sep 17 00:00:00 2001 From: elajkat Date: Tue, 7 Jan 2025 09:56:39 +0100 Subject: [PATCH 3614/3836] BGP: remove debug print added in [1] [1]: https://review.opendev.org/c/openstack/openstacksdk/+/920670 Related-Bug: #2067039 Change-Id: I069b8975e1cdc3b2c3686dfa35fd64f773ebad1f --- openstack/tests/unit/network/v2/test_bgp_speaker.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openstack/tests/unit/network/v2/test_bgp_speaker.py b/openstack/tests/unit/network/v2/test_bgp_speaker.py index 4323be777..4c07c189c 100644 --- a/openstack/tests/unit/network/v2/test_bgp_speaker.py +++ b/openstack/tests/unit/network/v2/test_bgp_speaker.py @@ -167,7 +167,6 @@ def test_get_bgp_dragents(self, mock_list): url = 'bgp-speakers/IDENTIFIER/bgp-dragents' sess.get.assert_called_with(url) - print('BBBBB0 ', ret) self.assertEqual(ret, [agent.Agent(**response.body['agents'][0])]) def test_add_bgp_speaker_to_dragent(self): From da17a2762ef432ad6ffdd59df481067aa02e8863 Mon Sep 17 00:00:00 2001 From: Pierre Riteau Date: Sat, 18 Jan 2025 10:31:13 +0100 Subject: [PATCH 3615/3836] Fix TestKeypairAdmin tests and compute.*_keypair The get_keypair and delete_keypair calls need to specify the user_id if the keypair belongs to another user. This was only working by accident because the first element returned by list_users was the current user. However, the ordering of the user list is now different following a change in Keystone [1]. This failing test uncovered additionally that keypair methods are not properly setting user_id into the query parameters. Overload fetch and delete methods to properly pass user_id as a parameter. [1] https://review.opendev.org/c/openstack/keystone/+/938814 Closes-Bug: #2095312 Co-Authored-By: Artem Goncharov Change-Id: Ic552dee83d56278d2b866de0cb365a0c394fe26a --- openstack/compute/v2/_proxy.py | 28 +++-- .../functional/compute/v2/test_keypair.py | 4 +- openstack/tests/unit/compute/v2/test_proxy.py | 116 ++++++++++++++++-- ...-user-id-bug-2095312-a01dc5b9b26dbe93.yaml | 6 + 4 files changed, 131 insertions(+), 23 deletions(-) create mode 100644 releasenotes/notes/fix-keypair-user-id-bug-2095312-a01dc5b9b26dbe93.yaml diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 9b7fca831..85d2362fa 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -643,13 +643,17 @@ def delete_keypair(self, keypair, ignore_missing=True, user_id=None): :returns: ``None`` """ - attrs = {'user_id': user_id} if user_id else {} - self._delete( - _keypair.Keypair, - keypair, - ignore_missing=ignore_missing, - **attrs, - ) + # NOTE(gtema): it is necessary to overload normal logic since query + # parameters are not properly respected in typical DELETE case + res = self._get_resource(_keypair.Keypair, keypair) + + try: + delete_params = {'user_id': user_id} if user_id else {} + res.delete(self, params=delete_params) + except exceptions.NotFoundException: + if ignore_missing: + return None + raise def get_keypair(self, keypair, user_id=None): """Get a single keypair @@ -662,8 +666,14 @@ def get_keypair(self, keypair, user_id=None): :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ - attrs = {'user_id': user_id} if user_id else {} - return self._get(_keypair.Keypair, keypair, **attrs) + # NOTE(gtema): it is necessary to overload normal logic since query + # parameters are not properly respected in typical fetch case + res = self._get_resource(_keypair.Keypair, keypair) + + get_params = {'user_id': user_id} if user_id else {} + return res.fetch( + self, error_message=f"No Keypair found for {keypair}", **get_params + ) def find_keypair(self, name_or_id, ignore_missing=True, *, user_id=None): """Find a single keypair diff --git a/openstack/tests/functional/compute/v2/test_keypair.py b/openstack/tests/functional/compute/v2/test_keypair.py index 5fba46cc0..753be8106 100644 --- a/openstack/tests/functional/compute/v2/test_keypair.py +++ b/openstack/tests/functional/compute/v2/test_keypair.py @@ -65,12 +65,12 @@ def setUp(self): self._keypair = sot def tearDown(self): - sot = self.conn.compute.delete_keypair(self._keypair) + sot = self.conn.compute.delete_keypair(self.NAME, user_id=self.USER.id) self.assertIsNone(sot) super().tearDown() def test_get(self): - sot = self.conn.compute.get_keypair(self.NAME) + sot = self.conn.compute.get_keypair(self.NAME, user_id=self.USER.id) self.assertEqual(self.NAME, sot.name) self.assertEqual(self.NAME, sot.id) self.assertEqual(self.USER.id, sot.user_id) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 3cab14da1..77f41a356 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -12,6 +12,7 @@ import contextlib import datetime +import fixtures from unittest import mock import uuid import warnings @@ -40,6 +41,7 @@ from openstack.compute.v2 import volume_attachment from openstack.identity.v3 import project from openstack import proxy as proxy_base +from openstack.tests.unit import base from openstack.tests.unit import test_proxy_base from openstack import warnings as os_warnings @@ -266,18 +268,32 @@ def test_keypair_create(self): self.verify_create(self.proxy.create_keypair, keypair.Keypair) def test_keypair_delete(self): - self.verify_delete(self.proxy.delete_keypair, keypair.Keypair, False) + self._verify( + "openstack.compute.v2.keypair.Keypair.delete", + self.proxy.delete_keypair, + method_args=["value"], + expected_args=[self.proxy], + expected_kwargs={"params": {}}, + ) def test_keypair_delete_ignore(self): - self.verify_delete(self.proxy.delete_keypair, keypair.Keypair, True) + self._verify( + "openstack.compute.v2.keypair.Keypair.delete", + self.proxy.delete_keypair, + method_args=["value", True], + method_kwargs={"user_id": "fake_user"}, + expected_args=[self.proxy], + expected_kwargs={"params": {"user_id": "fake_user"}}, + ) def test_keypair_delete_user_id(self): - self.verify_delete( + self._verify( + "openstack.compute.v2.keypair.Keypair.delete", self.proxy.delete_keypair, - keypair.Keypair, - True, - method_kwargs={'user_id': 'fake_user'}, - expected_kwargs={'user_id': 'fake_user'}, + method_args=["value"], + method_kwargs={"user_id": "fake_user"}, + expected_args=[self.proxy], + expected_kwargs={"params": {"user_id": "fake_user"}}, ) def test_keypair_find(self): @@ -292,14 +308,28 @@ def test_keypair_find_user_id(self): ) def test_keypair_get(self): - self.verify_get(self.proxy.get_keypair, keypair.Keypair) + self._verify( + "openstack.compute.v2.keypair.Keypair.fetch", + self.proxy.get_keypair, + method_args=["value"], + method_kwargs={}, + expected_args=[self.proxy], + expected_kwargs={ + "error_message": "No Keypair found for value", + }, + ) def test_keypair_get_user_id(self): - self.verify_get( + self._verify( + "openstack.compute.v2.keypair.Keypair.fetch", self.proxy.get_keypair, - keypair.Keypair, - method_kwargs={'user_id': 'fake_user'}, - expected_kwargs={'user_id': 'fake_user'}, + method_args=["value"], + method_kwargs={"user_id": "fake_user"}, + expected_args=[self.proxy], + expected_kwargs={ + "error_message": "No Keypair found for value", + "user_id": "fake_user", + }, ) def test_keypairs(self): @@ -314,6 +344,68 @@ def test_keypairs_user_id(self): ) +class TestKeyPairUrl(base.TestCase): + def setUp(self): + super().setUp() + self.useFixture( + fixtures.MonkeyPatch( + "openstack.utils.maximum_supported_microversion", + lambda *args, **kwargs: "2.10", + ) + ) + + def test_keypair_find_user_id(self): + self.register_uris( + [ + dict( + method="GET", + uri=self.get_mock_url( + "compute", + "public", + append=["os-keypairs", "fake_keypair"], + qs_elements=["user_id=fake_user"], + ), + ), + ] + ) + + self.cloud.compute.find_keypair("fake_keypair", user_id="fake_user") + + def test_keypair_get_user_id(self): + self.register_uris( + [ + dict( + method="GET", + uri=self.get_mock_url( + "compute", + "public", + append=["os-keypairs", "fake_keypair"], + qs_elements=["user_id=fake_user"], + ), + ), + ] + ) + + self.cloud.compute.get_keypair("fake_keypair", user_id="fake_user") + + def test_keypair_delete_user_id(self): + self.register_uris( + [ + dict( + method="DELETE", + uri=self.get_mock_url( + "compute", + "public", + append=["os-keypairs", "fake_keypair"], + qs_elements=["user_id=fake_user"], + ), + ), + ] + ) + + self.cloud.compute.delete_keypair("fake_keypair", user_id="fake_user") + + class TestAggregate(TestComputeProxy): def test_aggregate_create(self): self.verify_create(self.proxy.create_aggregate, aggregate.Aggregate) diff --git a/releasenotes/notes/fix-keypair-user-id-bug-2095312-a01dc5b9b26dbe93.yaml b/releasenotes/notes/fix-keypair-user-id-bug-2095312-a01dc5b9b26dbe93.yaml new file mode 100644 index 000000000..e30967457 --- /dev/null +++ b/releasenotes/notes/fix-keypair-user-id-bug-2095312-a01dc5b9b26dbe93.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fix the ``delete_keypair``, ``get_keypair`` and ``find_keypair`` methods + not including the optional ``user_id`` parameter in API queries. + [`bug 2095312 `_] From bf862933628e48d70cfee121e24833660bb80eea Mon Sep 17 00:00:00 2001 From: Slawek Kaplonski Date: Tue, 21 Jan 2025 11:50:04 +0100 Subject: [PATCH 3616/3836] Add "vlan_qinq" attribute to the "network" resource Depends-On: https://review.opendev.org/c/openstack/python-openstackclient/+/940111 Related-Bug: #1915151 Change-Id: I58e9a8bde3dc9425c8ea698a6f4ec07ce21f1e5a --- openstack/network/v2/network.py | 2 ++ openstack/tests/unit/cloud/test_network.py | 1 + openstack/tests/unit/network/v2/test_network.py | 2 ++ .../add-vlan_qinq-to-the-network-72d69e4f8856d48f.yaml | 6 ++++++ 4 files changed, 11 insertions(+) create mode 100644 releasenotes/notes/add-vlan_qinq-to-the-network-72d69e4f8856d48f.yaml diff --git a/openstack/network/v2/network.py b/openstack/network/v2/network.py index 12e0e4b21..1abc37031 100644 --- a/openstack/network/v2/network.py +++ b/openstack/network/v2/network.py @@ -115,6 +115,8 @@ class Network(_base.NetworkResource, _base.TagMixinNetwork): updated_at = resource.Body('updated_at') #: Indicates the VLAN transparency mode of the network is_vlan_transparent = resource.Body('vlan_transparent', type=bool) + #: Indicates the VLAN QinQ mode of the network + is_vlan_qinq = resource.Body('vlan_qinq', type=bool) class DHCPAgentHostingNetwork(Network): diff --git a/openstack/tests/unit/cloud/test_network.py b/openstack/tests/unit/cloud/test_network.py index fe460ab14..53b3c5740 100644 --- a/openstack/tests/unit/cloud/test_network.py +++ b/openstack/tests/unit/cloud/test_network.py @@ -162,6 +162,7 @@ class TestNetworks(base.TestCase): 'mtu': 0, 'dns_domain': 'sample.openstack.org.', 'vlan_transparent': None, + 'vlan_qinq': None, 'segments': None, } diff --git a/openstack/tests/unit/network/v2/test_network.py b/openstack/tests/unit/network/v2/test_network.py index ad909d172..f902dfec9 100644 --- a/openstack/tests/unit/network/v2/test_network.py +++ b/openstack/tests/unit/network/v2/test_network.py @@ -42,6 +42,7 @@ 'subnets': ['18', '19'], 'updated_at': '2016-07-09T12:14:57.233772', 'vlan_transparent': False, + 'vlan_qinq': False, } @@ -97,6 +98,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['subnets'], sot.subnet_ids) self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) self.assertEqual(EXAMPLE['vlan_transparent'], sot.is_vlan_transparent) + self.assertEqual(EXAMPLE['vlan_qinq'], sot.is_vlan_qinq) self.assertDictEqual( { diff --git a/releasenotes/notes/add-vlan_qinq-to-the-network-72d69e4f8856d48f.yaml b/releasenotes/notes/add-vlan_qinq-to-the-network-72d69e4f8856d48f.yaml new file mode 100644 index 000000000..95e2e2f70 --- /dev/null +++ b/releasenotes/notes/add-vlan_qinq-to-the-network-72d69e4f8856d48f.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add ``vlan_qinq`` attribute to the ``network`` resource. Users can use this + attribute to create network which will allow to configure VLANs + transparently in the guest VM and will use ethertype ``0x8a88 (802.1ad)``. From 2e85eca80e2956737b823abe21bd090332faeb67 Mon Sep 17 00:00:00 2001 From: Maurice Escher Date: Thu, 4 Jul 2024 14:16:49 +0200 Subject: [PATCH 3617/3836] cloud floating ip: fix add_ip_list() for single ip Fixes bug introduced by I44385c25fcb010f3e62b4098fd34ae3290292630. `!=` should be changed to `is not`. Change-Id: I3e2a4083df5d72db2f9afb5f3d2e322ce4963566 --- openstack/cloud/_network_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/cloud/_network_common.py b/openstack/cloud/_network_common.py index 3a9187892..e80995bce 100644 --- a/openstack/cloud/_network_common.py +++ b/openstack/cloud/_network_common.py @@ -1276,7 +1276,7 @@ def add_ip_list( error. """ - if type(ips) is list: + if type(ips) is not list: ips = [ips] for ip in ips: From d8a69406dadbb85a11adcf1b03e60502553da4c6 Mon Sep 17 00:00:00 2001 From: Thobias Salazar Trevisan Date: Fri, 7 Feb 2025 09:00:26 -0300 Subject: [PATCH 3618/3836] Correct docstrings for multiple find_* methods Replaced "delete" with "find" in the docstrings of the following methods to accurately describe their behavior: - find_load_balancer - find_pool - find_flavor_profile - find_flavor - find_availability_zone_profile - find_availability_zone Change-Id: Ic15126e462955805545489153383583141ff5e1d --- openstack/load_balancer/v2/_proxy.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index ff5947819..250a2b0c2 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -125,7 +125,7 @@ def find_load_balancer(self, name_or_id, ignore_missing=True): :class:`~openstack.exceptions.NotFoundException` will be raised when the load balancer does not exist. When set to ``True``, no exception will be set when attempting - to delete a nonexistent load balancer. + to find a nonexistent load balancer. :returns: ``None`` """ @@ -352,7 +352,7 @@ def find_pool(self, name_or_id, ignore_missing=True): :class:`~openstack.exceptions.NotFoundException` will be raised when the pool does not exist. When set to ``True``, no exception will be set when attempting - to delete a nonexistent pool. + to find a nonexistent pool. :returns: ``None`` """ @@ -935,7 +935,7 @@ def find_flavor_profile(self, name_or_id, ignore_missing=True): :class:`~openstack.exceptions.NotFoundException` will be raised when the flavor profile does not exist. When set to ``True``, no exception will be set when attempting - to delete a nonexistent flavor profile. + to find a nonexistent flavor profile. :returns: ``None`` """ @@ -1015,7 +1015,7 @@ def find_flavor(self, name_or_id, ignore_missing=True): :class:`~openstack.exceptions.NotFoundException` will be raised when the flavor does not exist. When set to ``True``, no exception will be set when attempting - to delete a nonexistent flavor. + to find a nonexistent flavor. :returns: ``None`` """ @@ -1159,7 +1159,7 @@ def find_availability_zone_profile(self, name_or_id, ignore_missing=True): :class:`~openstack.exceptions.NotFoundException` will be raised when the availability zone profile does not exist. When set to ``True``, no exception will be set when attempting - to delete a nonexistent availability zone profile. + to find a nonexistent availability zone profile. :returns: ``None`` """ @@ -1253,7 +1253,7 @@ def find_availability_zone(self, name_or_id, ignore_missing=True): :class:`~openstack.exceptions.NotFoundException` will be raised when the availability zone does not exist. When set to ``True``, no exception will be set when attempting - to delete a nonexistent availability zone. + to find a nonexistent availability zone. :returns: ``None`` """ From ec4720d8fcf1154a8e1e5a2b25d716e46331e0a8 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Tue, 3 Sep 2024 05:49:31 +0000 Subject: [PATCH 3619/3836] Support streaming download of the image This changes adds support for downloading the images in chunked streams. This allows the image download to happen without holding everything in memory which can be an issue with OOM. Change-Id: I2d2c7e5f0d7fbf0d4296a5e2a08d18fcdbe2b143 --- openstack/cloud/_image.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py index d521c80e9..c5ff08eff 100644 --- a/openstack/cloud/_image.py +++ b/openstack/cloud/_image.py @@ -88,6 +88,7 @@ def download_image( output_path=None, output_file=None, chunk_size=1024 * 1024, + stream=False, ): """Download an image by name or ID @@ -99,6 +100,7 @@ def download_image( this or output_path must be specified :param int chunk_size: size in bytes to read from the wire and buffer at one time. Defaults to 1024 * 1024 = 1 MiB + :param: bool stream: whether to stream the output in chunk_size. :returns: When output_path and output_file are not given - the bytes comprising the given Image when stream is False, otherwise a @@ -125,7 +127,10 @@ def download_image( image = self.image.find_image(name_or_id, ignore_missing=False) return self.image.download_image( - image, output=output_file or output_path, chunk_size=chunk_size + image, + output=output_file or output_path, + chunk_size=chunk_size, + stream=stream, ) def get_image_exclude(self, name_or_id, exclude): From 4beac2a236993bd7890279f3f22a2eda3cd89bad Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 19 Feb 2025 10:46:44 +0000 Subject: [PATCH 3620/3836] pre-commit: Prepare for bump Add all the automatically-generated changes separately to make the manual changes needed more obvious. This was generated bump bumping ruff to the latest version (v0.9.3) and running: pre-commit run -a Before undoing the changes to '.pre-commit-config.yaml'. The only needed manual change was the removal of a now-unused 'typing' import. Change-Id: I8b6ff24311baff77546089541467a87c84a1218d Signed-off-by: Stephen Finucane --- examples/image/import.py | 3 +- openstack/_log.py | 4 +-- openstack/baremetal/v1/node.py | 6 ++-- openstack/block_storage/_base_proxy.py | 3 +- openstack/block_storage/v2/backup.py | 3 +- openstack/block_storage/v2/volume.py | 3 +- openstack/block_storage/v3/backup.py | 3 +- openstack/block_storage/v3/volume.py | 3 +- openstack/cloud/_baremetal.py | 5 +++- openstack/cloud/_identity.py | 3 +- openstack/cloud/_network.py | 22 +++++++++++---- openstack/cloud/openstackcloud.py | 5 ++-- openstack/common/quota_set.py | 2 +- openstack/compute/v2/server.py | 12 ++++---- openstack/config/cloud_region.py | 3 +- openstack/config/loader.py | 8 +++--- openstack/config/vendors/__init__.py | 3 +- openstack/dns/v2/_base.py | 4 +-- openstack/exceptions.py | 3 +- openstack/image/v2/_proxy.py | 3 +- openstack/image/v2/image.py | 2 +- openstack/network/v2/_proxy.py | 4 +-- openstack/object_store/v1/_base.py | 6 ++-- .../orchestration/util/template_utils.py | 9 +++--- openstack/proxy.py | 28 +++++++++---------- openstack/resource.py | 22 +++++++-------- openstack/service_description.py | 5 ++-- openstack/test/fakes.py | 16 +++++------ .../tests/functional/instance_ha/test_host.py | 6 ++-- .../functional/network/v2/test_qos_policy.py | 3 +- .../shared_file_system/test_share_metadata.py | 3 +- .../unit/cloud/test_floating_ip_neutron.py | 3 +- openstack/tests/unit/cloud/test_fwaas.py | 27 ++++++++++-------- openstack/tests/unit/cloud/test_operator.py | 3 +- openstack/tests/unit/network/v2/test_proxy.py | 3 +- .../tests/unit/object_store/v1/test_proxy.py | 6 ++-- openstack/tests/unit/test_resource.py | 15 ++++++---- openstack/utils.py | 9 +++--- 38 files changed, 134 insertions(+), 137 deletions(-) diff --git a/examples/image/import.py b/examples/image/import.py index 44be08213..be03176ec 100644 --- a/examples/image/import.py +++ b/examples/image/import.py @@ -25,8 +25,7 @@ def import_image(conn): # Url where glance can download the image uri = ( - 'https://download.cirros-cloud.net/0.4.0/' - 'cirros-0.4.0-x86_64-disk.img' + 'https://download.cirros-cloud.net/0.4.0/cirros-0.4.0-x86_64-disk.img' ) # Build the image attributes and import the image. diff --git a/openstack/_log.py b/openstack/_log.py index 62f2c7d36..5182bf702 100644 --- a/openstack/_log.py +++ b/openstack/_log.py @@ -19,7 +19,7 @@ def setup_logging( name: str, - handlers: ty.Optional[ty.List[logging.Handler]] = None, + handlers: ty.Optional[list[logging.Handler]] = None, level: ty.Optional[int] = None, ) -> logging.Logger: """Set up logging for a named logger. @@ -54,7 +54,7 @@ def enable_logging( stream: ty.Optional[ty.TextIO] = None, format_stream: bool = False, format_template: str = '%(asctime)s %(levelname)s: %(name)s %(message)s', - handlers: ty.Optional[ty.List[logging.Handler]] = None, + handlers: ty.Optional[list[logging.Handler]] = None, ) -> None: """Enable logging output. diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index b7d203ae1..7084b41c0 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -527,16 +527,14 @@ def set_provision_state( if service_steps is not None: if target != 'service': raise ValueError( - 'Service steps can only be provided with ' - '"service" target' + 'Service steps can only be provided with "service" target' ) body['service_steps'] = service_steps if rescue_password is not None: if target != 'rescue': raise ValueError( - 'Rescue password can only be provided with ' - '"rescue" target' + 'Rescue password can only be provided with "rescue" target' ) body['rescue_password'] = rescue_password diff --git a/openstack/block_storage/_base_proxy.py b/openstack/block_storage/_base_proxy.py index 809d069b9..cf8271d19 100644 --- a/openstack/block_storage/_base_proxy.py +++ b/openstack/block_storage/_base_proxy.py @@ -38,8 +38,7 @@ def create_image( volume_obj = self.get_volume(volume) if not volume_obj: raise exceptions.SDKException( - f"Volume {volume} given to create_image could " - f"not be found" + f"Volume {volume} given to create_image could not be found" ) volume_id = volume_obj['id'] data = self.post( diff --git a/openstack/block_storage/v2/backup.py b/openstack/block_storage/v2/backup.py index aa68366b6..cbb11fed3 100644 --- a/openstack/block_storage/v2/backup.py +++ b/openstack/block_storage/v2/backup.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import typing as ty from openstack import exceptions from openstack import resource @@ -178,7 +177,7 @@ def restore(self, session, volume_id=None, name=None): :return: Updated backup instance """ url = utils.urljoin(self.base_path, self.id, "restore") - body: ty.Dict[str, ty.Dict] = {'restore': {}} + body: dict[str, dict] = {'restore': {}} if volume_id: body['restore']['volume_id'] = volume_id if name: diff --git a/openstack/block_storage/v2/volume.py b/openstack/block_storage/v2/volume.py index 196213805..77ddf9f1b 100644 --- a/openstack/block_storage/v2/volume.py +++ b/openstack/block_storage/v2/volume.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import typing as ty from openstack.common import metadata from openstack import format @@ -142,7 +141,7 @@ def reset_status( self, session, status=None, attach_status=None, migration_status=None ): """Reset volume statuses (admin operation)""" - body: ty.Dict[str, ty.Dict[str, str]] = {'os-reset_status': {}} + body: dict[str, dict[str, str]] = {'os-reset_status': {}} if status: body['os-reset_status']['status'] = status if attach_status: diff --git a/openstack/block_storage/v3/backup.py b/openstack/block_storage/v3/backup.py index 636a0d3f5..1fd7204e0 100644 --- a/openstack/block_storage/v3/backup.py +++ b/openstack/block_storage/v3/backup.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import typing as ty from openstack import exceptions from openstack import resource @@ -194,7 +193,7 @@ def restore(self, session, volume_id=None, name=None): :return: Updated backup instance """ url = utils.urljoin(self.base_path, self.id, "restore") - body: ty.Dict[str, ty.Dict] = {'restore': {}} + body: dict[str, dict] = {'restore': {}} if volume_id: body['restore']['volume_id'] = volume_id if name: diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index 9b463f796..bdf840a65 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import typing as ty from openstack.common import metadata from openstack import exceptions @@ -161,7 +160,7 @@ def reset_status( self, session, status=None, attach_status=None, migration_status=None ): """Reset volume statuses (admin operation)""" - body: ty.Dict[str, ty.Dict[str, str]] = {'os-reset_status': {}} + body: dict[str, dict[str, str]] = {'os-reset_status': {}} if status: body['os-reset_status']['status'] = status if attach_status: diff --git a/openstack/cloud/_baremetal.py b/openstack/cloud/_baremetal.py index 9a0bc077c..c4dcdfd5b 100644 --- a/openstack/cloud/_baremetal.py +++ b/openstack/cloud/_baremetal.py @@ -202,7 +202,10 @@ def register_machine( Example:: - [{'address': 'aa:bb:cc:dd:ee:01'}, {'address': 'aa:bb:cc:dd:ee:02'}] + [ + {'address': 'aa:bb:cc:dd:ee:01'}, + {'address': 'aa:bb:cc:dd:ee:02'}, + ] Alternatively, you can provide an array of MAC addresses. :param wait: Boolean value, defaulting to false, to wait for the node diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index 0e270f886..e87f15c2d 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -924,8 +924,7 @@ def create_group(self, name, description, domain=None): dom = self.get_domain(domain) if not dom: raise exceptions.SDKException( - f"Creating group {name} failed: Invalid domain " - f"{domain}" + f"Creating group {name} failed: Invalid domain {domain}" ) group_ref['domain_id'] = dom['id'] diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index 9538b5475..7779e1062 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -1309,8 +1309,7 @@ def create_qos_policy(self, **kwargs): kwargs['is_default'] = default else: self.log.debug( - "'qos-default' extension is not available on " - "target cloud" + "'qos-default' extension is not available on target cloud" ) return self.network.create_qos_policy(**kwargs) @@ -1343,8 +1342,7 @@ def update_qos_policy(self, name_or_id, **kwargs): kwargs['is_default'] = default else: self.log.debug( - "'qos-default' extension is not available on " - "target cloud" + "'qos-default' extension is not available on target cloud" ) if not kwargs: @@ -2552,7 +2550,13 @@ def create_port(self, network_id, **kwargs): :param allowed_address_pairs: Allowed address pairs list (Optional) For example:: - [{"ip_address": "23.23.23.1", "mac_address": "fa:16:3e:c4:cd:3f"}, ...] + [ + { + "ip_address": "23.23.23.1", + "mac_address": "fa:16:3e:c4:cd:3f", + }, + ..., + ] :param extra_dhcp_opts: Extra DHCP options. (Optional). For example:: @@ -2631,7 +2635,13 @@ def update_port(self, name_or_id, **kwargs): :param allowed_address_pairs: Allowed address pairs list (Optional) For example:: - [{"ip_address": "23.23.23.1", "mac_address": "fa:16:3e:c4:cd:3f"}, ...] + [ + { + "ip_address": "23.23.23.1", + "mac_address": "fa:16:3e:c4:cd:3f", + }, + ..., + ] :param extra_dhcp_opts: Extra DHCP options. (Optional). For example:: diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index bb82a8023..9d667c08e 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -15,7 +15,6 @@ import copy import functools import queue -import typing as ty import warnings import weakref @@ -492,7 +491,7 @@ def range_search(self, data, filters): :raises: :class:`~openstack.exceptions.SDKException` on invalid range expressions. """ - filtered: ty.List[object] = [] + filtered: list[object] = [] for key, range_value in filters.items(): # We always want to operate on the full data set so that @@ -697,7 +696,7 @@ def project_cleanup( for dep in v.get('after', []): dep_graph.add_edge(dep, k) - cleanup_resources: ty.Dict[str, resource.Resource] = {} + cleanup_resources: dict[str, resource.Resource] = {} for service in dep_graph.walk(timeout=wait_timeout): fn = None diff --git a/openstack/common/quota_set.py b/openstack/common/quota_set.py index 35d70fa1d..d07059e2c 100644 --- a/openstack/common/quota_set.py +++ b/openstack/common/quota_set.py @@ -105,7 +105,7 @@ def _translate_response( body.pop("self", None) # Process body_attrs to strip usage and reservation out - normalized_attrs: ty.Dict[str, ty.Any] = dict( + normalized_attrs: dict[str, ty.Any] = dict( reservation={}, usage={}, ) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 653d77f99..a5720cbb2 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -696,7 +696,7 @@ def lock(self, session, locked_reason=None): :param locked_reason: The reason for locking the server. :returns: None """ - body: ty.Dict[str, ty.Any] = {"lock": None} + body: dict[str, ty.Any] = {"lock": None} if locked_reason is not None: body["lock"] = { "locked_reason": locked_reason, @@ -724,7 +724,7 @@ def rescue(self, session, admin_pass=None, image_ref=None): provided, the server will use the existing image. (Optional) :returns: None """ - body: ty.Dict[str, ty.Any] = {"rescue": {}} + body: dict[str, ty.Any] = {"rescue": {}} if admin_pass is not None: body["rescue"]["adminPass"] = admin_pass if image_ref is not None: @@ -761,7 +761,7 @@ def evacuate( (Optional) (Only supported before microversion 2.14) :returns: None """ - body: ty.Dict[str, ty.Any] = {"evacuate": {}} + body: dict[str, ty.Any] = {"evacuate": {}} if host is not None: body["evacuate"]["host"] = host if admin_pass is not None: @@ -855,7 +855,7 @@ def migrate(self, session, *, host=None): "greater." ) - body: ty.Dict[str, ty.Any] = {"migrate": None} + body: dict[str, ty.Any] = {"migrate": None} if host: body["migrate"] = {"host": host} self._action(session, body) @@ -877,7 +877,7 @@ def get_console_output(self, session, length=None): (Optional) :returns: None """ - body: ty.Dict[str, ty.Any] = {"os-getConsoleOutput": {}} + body: dict[str, ty.Any] = {"os-getConsoleOutput": {}} if length is not None: body["os-getConsoleOutput"]["length"] = length resp = self._action(session, body) @@ -989,7 +989,7 @@ def _live_migrate( disk_over_commit, ): microversion = None - body: ty.Dict[str, ty.Any] = { + body: dict[str, ty.Any] = { 'host': None, } if block_migration == 'auto': diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index a769f4870..6a176f143 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -14,7 +14,6 @@ import copy import os.path -import typing as ty from urllib import parse import warnings @@ -194,7 +193,7 @@ def from_conf(conf, session=None, service_types=None, **kwargs): f"'{st}') was present in the config.", ) continue - opt_dict: ty.Dict[str, str] = {} + opt_dict: dict[str, str] = {} # Populate opt_dict with (appropriately processed) Adapter conf opts try: ks_load_adap.process_conf_options(conf[project_name], opt_dict) diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 8b7d23110..991d64200 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -132,7 +132,7 @@ def _fix_argv(argv): argv[index] = "=".join(split_args) # Save both for later so we can throw an error about dupes processed[new].add(orig) - overlap: ty.List[str] = [] + overlap: list[str] = [] for new, old in processed.items(): if len(old) > 1: overlap.extend(old) @@ -303,8 +303,8 @@ def __init__( self._cache_expiration_time = 0 self._cache_path = CACHE_PATH self._cache_class = 'dogpile.cache.null' - self._cache_arguments: ty.Dict[str, ty.Any] = {} - self._cache_expirations: ty.Dict[str, int] = {} + self._cache_arguments: dict[str, ty.Any] = {} + self._cache_expirations: dict[str, int] = {} self._influxdb_config = {} if 'cache' in self.cloud_config: cache_settings = _util.normalize_keys(self.cloud_config['cache']) @@ -537,7 +537,7 @@ def _get_known_regions(self, cloud): return self._expand_regions(regions) else: # crappit. we don't have a region defined. - new_cloud: ty.Dict[str, ty.Any] = {} + new_cloud: dict[str, ty.Any] = {} our_cloud = self.cloud_config['clouds'].get(cloud, {}) self._expand_vendor_profile(cloud, new_cloud, our_cloud) if 'regions' in new_cloud and new_cloud['regions']: diff --git a/openstack/config/vendors/__init__.py b/openstack/config/vendors/__init__.py index 28671f713..89bdd7bcf 100644 --- a/openstack/config/vendors/__init__.py +++ b/openstack/config/vendors/__init__.py @@ -15,7 +15,6 @@ import glob import json import os -import typing as ty import urllib import requests @@ -25,7 +24,7 @@ from openstack import exceptions _VENDORS_PATH = os.path.dirname(os.path.realpath(__file__)) -_VENDOR_DEFAULTS: ty.Dict[str, ty.Dict] = {} +_VENDOR_DEFAULTS: dict[str, dict] = {} _WELL_KNOWN_PATH = "{scheme}://{netloc}/.well-known/openstack/api" diff --git a/openstack/dns/v2/_base.py b/openstack/dns/v2/_base.py index 14d4fd138..210beb599 100644 --- a/openstack/dns/v2/_base.py +++ b/openstack/dns/v2/_base.py @@ -80,7 +80,7 @@ def list( all_projects=None, **params, ): - headers: ty.Union[ty.Union[ty.Dict[str, str], None]] = ( + headers: ty.Union[ty.Union[dict[str, str], None]] = ( {} if project_id or all_projects else None ) @@ -95,7 +95,7 @@ def list( @classmethod def _get_next_link(cls, uri, response, data, marker, limit, total_yielded): next_link = None - params: ty.Dict[str, ty.Union[ty.List[str], str]] = {} + params: dict[str, ty.Union[list[str], str]] = {} if isinstance(data, dict): links = data.get('links') if links: diff --git a/openstack/exceptions.py b/openstack/exceptions.py index f9f767b58..cf7b9d3c3 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -18,7 +18,6 @@ import json import re -import typing as ty from requests import exceptions as _rex @@ -180,7 +179,7 @@ def raise_from_response(response, error_message=None): if response.status_code < 400: return - cls: ty.Type[SDKException] + cls: type[SDKException] if response.status_code == 400: cls = BadRequestException elif response.status_code == 403: diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 543a6c9df..986203b8b 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -12,7 +12,6 @@ import os import time -import typing as ty import warnings from openstack import exceptions @@ -601,7 +600,7 @@ def _upload_image( raise exceptions.SDKException(f"Image creation failed: {str(e)}") def _make_v2_image_params(self, meta, properties): - ret: ty.Dict = {} + ret: dict = {} for k, v in iter(properties.items()): if k in _INT_PROPERTIES: ret[k] = int(v) diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index 28e7c19b0..66336e57c 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -338,7 +338,7 @@ def import_image( stores = stores or [] url = utils.urljoin(self.base_path, self.id, 'import') - data: ty.Dict[str, ty.Any] = {'method': {'name': method}} + data: dict[str, ty.Any] = {'method': {'name': method}} if uri: if method != 'web-download': diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index bcf07ae9d..2ccd2f4ba 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -195,7 +195,7 @@ class Proxy(proxy.Proxy): @proxy._check_resource(strict=False) def _update( self, - resource_type: ty.Type[resource.Resource], + resource_type: type[resource.Resource], value, base_path=None, if_revision=None, @@ -207,7 +207,7 @@ def _update( @proxy._check_resource(strict=False) def _delete( self, - resource_type: ty.Type[resource.Resource], + resource_type: type[resource.Resource], value, ignore_missing=True, if_revision=None, diff --git a/openstack/object_store/v1/_base.py b/openstack/object_store/v1/_base.py index b0f9ce6e0..e23789254 100644 --- a/openstack/object_store/v1/_base.py +++ b/openstack/object_store/v1/_base.py @@ -22,11 +22,11 @@ class BaseResource(resource.Resource): create_method = 'PUT' #: Metadata stored for this resource. *Type: dict* - metadata: ty.Dict[str, ty.Any] = {} + metadata: dict[str, ty.Any] = {} _custom_metadata_prefix: str - _system_metadata: ty.Dict[str, ty.Any] = {} - _last_headers: ty.Dict[str, ty.Any] = {} + _system_metadata: dict[str, ty.Any] = {} + _last_headers: dict[str, ty.Any] = {} def __init__(self, metadata=None, **attrs): """Process and save metadata known at creation stage""" diff --git a/openstack/orchestration/util/template_utils.py b/openstack/orchestration/util/template_utils.py index 0d8e9ebec..0aec0bd02 100644 --- a/openstack/orchestration/util/template_utils.py +++ b/openstack/orchestration/util/template_utils.py @@ -14,7 +14,6 @@ import collections.abc import json -import typing as ty from urllib import parse from urllib import request @@ -221,8 +220,8 @@ def process_multiple_environments_and_files( :return: tuple of files dict and a dict of the consolidated environment :rtype: tuple """ - merged_files: ty.Dict[str, str] = {} - merged_env: ty.Dict[str, ty.Dict] = {} + merged_files: dict[str, str] = {} + merged_env: dict[str, dict] = {} # If we're keeping a list of environment files separately, include the # contents of the files in the files dict @@ -275,8 +274,8 @@ def process_environment_and_files( :return: tuple of files dict and the loaded environment as a dict :rtype: (dict, dict) """ - files: ty.Dict[str, str] = {} - env: ty.Dict[str, ty.Dict] = {} + files: dict[str, str] = {} + env: dict[str, dict] = {} is_object = env_path_is_object and env_path_is_object(env_path) diff --git a/openstack/proxy.py b/openstack/proxy.py index 2ecebf820..85ad957f5 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -75,14 +75,14 @@ def normalize_metric_name(name): class Proxy(adapter.Adapter): """Represents a service.""" - retriable_status_codes: ty.Optional[ty.List[int]] = None + retriable_status_codes: ty.Optional[list[int]] = None """HTTP status codes that should be retried by default. The number of retries is defined by the configuration in parameters called ``_status_code_retries``. """ - _resource_registry: ty.Dict[str, ty.Type[resource.Resource]] = {} + _resource_registry: dict[str, type[resource.Resource]] = {} """Registry of the supported resourses. Dictionary of resource names (key) types (value). @@ -436,7 +436,7 @@ def _get_connection(self): ) def _get_resource( - self, resource_type: ty.Type[ResourceType], value, **attrs + self, resource_type: type[ResourceType], value, **attrs ) -> ResourceType: """Get a resource object to work on @@ -486,7 +486,7 @@ def _get_uri_attribute(self, child, parent, name): @ty.overload def _find( self, - resource_type: ty.Type[ResourceType], + resource_type: type[ResourceType], name_or_id: str, ignore_missing: ty.Literal[True] = True, **attrs, @@ -495,7 +495,7 @@ def _find( @ty.overload def _find( self, - resource_type: ty.Type[ResourceType], + resource_type: type[ResourceType], name_or_id: str, ignore_missing: ty.Literal[False], **attrs, @@ -506,7 +506,7 @@ def _find( @ty.overload def _find( self, - resource_type: ty.Type[ResourceType], + resource_type: type[ResourceType], name_or_id: str, ignore_missing: bool, **attrs, @@ -514,7 +514,7 @@ def _find( def _find( self, - resource_type: ty.Type[ResourceType], + resource_type: type[ResourceType], name_or_id: str, ignore_missing: bool = True, **attrs, @@ -540,7 +540,7 @@ def _find( @_check_resource(strict=False) def _delete( self, - resource_type: ty.Type[ResourceType], + resource_type: type[ResourceType], value, ignore_missing=True, **attrs, @@ -582,7 +582,7 @@ def _delete( @_check_resource(strict=False) def _update( self, - resource_type: ty.Type[ResourceType], + resource_type: type[ResourceType], value, base_path=None, **attrs, @@ -612,7 +612,7 @@ def _update( def _create( self, - resource_type: ty.Type[ResourceType], + resource_type: type[ResourceType], base_path=None, **attrs, ) -> ResourceType: @@ -648,7 +648,7 @@ def _create( def _bulk_create( self, - resource_type: ty.Type[ResourceType], + resource_type: type[ResourceType], data, base_path=None, ) -> ty.Generator[ResourceType, None, None]: @@ -674,7 +674,7 @@ def _bulk_create( @_check_resource(strict=False) def _get( self, - resource_type: ty.Type[ResourceType], + resource_type: type[ResourceType], value=None, requires_id=True, base_path=None, @@ -715,7 +715,7 @@ def _get( def _list( self, - resource_type: ty.Type[ResourceType], + resource_type: type[ResourceType], paginated=True, base_path=None, jmespath_filters=None, @@ -765,7 +765,7 @@ def _list( def _head( self, - resource_type: ty.Type[ResourceType], + resource_type: type[ResourceType], value=None, base_path=None, **attrs, diff --git a/openstack/resource.py b/openstack/resource.py index 6edc9f945..b03c6785c 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -99,7 +99,7 @@ class _BaseComponent(abc.ABC): # The name this component is being tracked as in the Resource key: str # The class to be used for mappings - _map_cls: ty.Type[ty.Mapping] = dict + _map_cls: type[ty.Mapping] = dict #: Marks the property as deprecated. deprecated = False @@ -273,7 +273,7 @@ class Computed(_BaseComponent): class _ComponentManager(collections.abc.MutableMapping): """Storage of a component type""" - attributes: ty.Dict[str, ty.Any] + attributes: dict[str, ty.Any] def __init__(self, attributes=None, synchronized=False): self.attributes = dict() if attributes is None else attributes.copy() @@ -354,7 +354,7 @@ def __init__( parameters, ``limit`` and ``marker``. These are the most common query parameters used for listing resources in OpenStack APIs. """ - self._mapping: ty.Dict[str, ty.Union[str, ty.Dict]] = {} + self._mapping: dict[str, ty.Union[str, dict]] = {} if include_pagination_defaults: self._mapping.update({"limit": "limit", "marker": "marker"}) self._mapping.update({name: name for name in names}) @@ -520,13 +520,13 @@ class Resource(dict): _header: _ComponentManager _uri: _ComponentManager _computed: _ComponentManager - _original_body: ty.Dict[str, ty.Any] = {} + _original_body: dict[str, ty.Any] = {} _store_unknown_attrs_as_properties = False _allow_unknown_attrs_in_body = False - _unknown_attrs_in_body: ty.Dict[str, ty.Any] = {} + _unknown_attrs_in_body: dict[str, ty.Any] = {} # Placeholder for aliases as dict of {__alias__:__original} - _attr_aliases: ty.Dict[str, str] = {} + _attr_aliases: dict[str, str] = {} def __init__(self, _synchronized=False, connection=None, **attrs): """The base resource @@ -1070,13 +1070,13 @@ def to_dict( :return: A dictionary of key/value pairs where keys are named as they exist as attributes of this class. """ - mapping: ty.Union[utils.Munch, ty.Dict] + mapping: ty.Union[utils.Munch, dict] if _to_munch: mapping = utils.Munch() else: mapping = {} - components: ty.List[ty.Type[_BaseComponent]] = [] + components: list[type[_BaseComponent]] = [] if body: components.append(Body) if headers: @@ -1164,7 +1164,7 @@ def _prepare_request_body( *, resource_request_key=None, ): - body: ty.Union[ty.Dict[str, ty.Any], ty.List[ty.Any]] + body: ty.Union[dict[str, ty.Any], list[ty.Any]] if patch: if not self._store_unknown_attrs_as_properties: # Default case @@ -1590,7 +1590,7 @@ def bulk_create( f"Invalid create method: {cls.create_method}" ) - _body: ty.List[ty.Any] = [] + _body: list[ty.Any] = [] resources = [] for attrs in data: # NOTE(gryf): we need to create resource objects, since @@ -1605,7 +1605,7 @@ def bulk_create( ) _body.append(request.body) - body: ty.Union[ty.Dict[str, ty.Any], ty.List[ty.Any]] = _body + body: ty.Union[dict[str, ty.Any], list[ty.Any]] = _body if prepend_key: assert cls.resources_key diff --git a/openstack/service_description.py b/openstack/service_description.py index a749d60ed..4ea2dda37 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -11,7 +11,6 @@ # License for the specific language governing permissions and limitations # under the License. -import typing as ty import warnings import os_service_types @@ -45,11 +44,11 @@ def __getattr__(self, item): class ServiceDescription: #: Dictionary of supported versions and proxy classes for that version - supported_versions: ty.Dict[str, ty.Type[proxy_mod.Proxy]] = {} + supported_versions: dict[str, type[proxy_mod.Proxy]] = {} #: main service_type to use to find this service in the catalog service_type: str #: list of aliases this service might be registered as - aliases: ty.List[str] = [] + aliases: list[str] = [] def __init__(self, service_type, supported_versions=None, aliases=None): """Class describing how to interact with a REST service. diff --git a/openstack/test/fakes.py b/openstack/test/fakes.py index d82f88011..0d70051de 100644 --- a/openstack/test/fakes.py +++ b/openstack/test/fakes.py @@ -24,12 +24,10 @@ import random from typing import ( Any, - Dict, - Generator, Optional, - Type, TypeVar, ) +from collections.abc import Generator from unittest import mock import uuid @@ -43,8 +41,8 @@ def generate_fake_resource( - resource_type: Type[Resource], - **attrs: Dict[str, Any], + resource_type: type[Resource], + **attrs: dict[str, Any], ) -> Resource: """Generate a fake resource @@ -67,7 +65,7 @@ def generate_fake_resource( :raises NotImplementedError: If a resource attribute specifies a ``type`` or ``list_type`` that cannot be automatically generated """ - base_attrs: Dict[str, Any] = {} + base_attrs: dict[str, Any] = {} for name, value in inspect.getmembers( resource_type, predicate=lambda x: isinstance(x, (resource.Body, resource.URI)), @@ -140,9 +138,9 @@ def generate_fake_resource( def generate_fake_resources( - resource_type: Type[Resource], + resource_type: type[Resource], count: int = 1, - attrs: Optional[Dict[str, Any]] = None, + attrs: Optional[dict[str, Any]] = None, ) -> Generator[Resource, None, None]: """Generate a given number of fake resource entities @@ -175,7 +173,7 @@ def generate_fake_resources( # various proxy methods also, but doing so requires deep code introspection or # (better) type annotations def generate_fake_proxy( - service: Type[service_description.ServiceDescription], + service: type[service_description.ServiceDescription], api_version: Optional[str] = None, ) -> proxy.Proxy: """Generate a fake proxy for the given service type diff --git a/openstack/tests/functional/instance_ha/test_host.py b/openstack/tests/functional/instance_ha/test_host.py index 9447790d1..4577863e0 100644 --- a/openstack/tests/functional/instance_ha/test_host.py +++ b/openstack/tests/functional/instance_ha/test_host.py @@ -13,13 +13,12 @@ # License for the specific language governing permissions and limitations # under the License. -import typing as ty from openstack.compute.v2 import hypervisor from openstack import connection from openstack.tests.functional import base -HYPERVISORS: ty.List[hypervisor.Hypervisor] = [] +HYPERVISORS: list[hypervisor.Hypervisor] = [] def hypervisors(): @@ -40,8 +39,7 @@ def setUp(self): if not hypervisors(): self.skipTest( - "Skip TestHost as there are no hypervisors " - "configured in nova" + "Skip TestHost as there are no hypervisors configured in nova" ) # Create segment diff --git a/openstack/tests/functional/network/v2/test_qos_policy.py b/openstack/tests/functional/network/v2/test_qos_policy.py index d889d79db..8eccedf48 100644 --- a/openstack/tests/functional/network/v2/test_qos_policy.py +++ b/openstack/tests/functional/network/v2/test_qos_policy.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import typing as ty from openstack.network.v2 import qos_policy as _qos_policy from openstack.tests.functional import base @@ -20,7 +19,7 @@ class TestQoSPolicy(base.BaseFunctionalTest): QOS_POLICY_ID = None IS_SHARED = False IS_DEFAULT = False - RULES: ty.List[str] = [] + RULES: list[str] = [] QOS_POLICY_DESCRIPTION = "QoS policy description" def setUp(self): diff --git a/openstack/tests/functional/shared_file_system/test_share_metadata.py b/openstack/tests/functional/shared_file_system/test_share_metadata.py index e4b042f39..e68c572fe 100644 --- a/openstack/tests/functional/shared_file_system/test_share_metadata.py +++ b/openstack/tests/functional/shared_file_system/test_share_metadata.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import typing as ty from openstack.shared_file_system.v2 import share as _share from openstack.tests.functional.shared_file_system import base @@ -82,7 +81,7 @@ def test_update(self): new_meta = {"newFoo": "newBar"} full_meta = {"foo": "bar", "newFoo": "newBar"} - empty_meta: ty.Dict[str, str] = {} + empty_meta: dict[str, str] = {} updated_share = ( self.user_cloud.shared_file_system.update_share_metadata( diff --git a/openstack/tests/unit/cloud/test_floating_ip_neutron.py b/openstack/tests/unit/cloud/test_floating_ip_neutron.py index 17bc87516..bc7e7d863 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_neutron.py +++ b/openstack/tests/unit/cloud/test_floating_ip_neutron.py @@ -257,8 +257,7 @@ def test_get_floating_ip_by_id(self): [ dict( method='GET', - uri='https://network.example.com/v2.0/floatingips/' - f'{fid}', + uri=f'https://network.example.com/v2.0/floatingips/{fid}', json=self.mock_floating_ip_new_rep, ) ] diff --git a/openstack/tests/unit/cloud/test_fwaas.py b/openstack/tests/unit/cloud/test_fwaas.py index 72e03dd3d..41dc0cab7 100644 --- a/openstack/tests/unit/cloud/test_fwaas.py +++ b/openstack/tests/unit/cloud/test_fwaas.py @@ -183,9 +183,10 @@ def test_delete_firewall_rule_not_found(self): ] ) - with mock.patch.object( - self.cloud.network, 'delete_firewall_rule' - ), mock.patch.object(self.cloud.log, 'debug'): + with ( + mock.patch.object(self.cloud.network, 'delete_firewall_rule'), + mock.patch.object(self.cloud.log, 'debug'), + ): self.assertFalse( self.cloud.delete_firewall_rule(self.firewall_rule_name) ) @@ -501,11 +502,14 @@ def test_delete_firewall_policy_filters(self): ] ) - with mock.patch.object( - self.cloud.network, - 'find_firewall_policy', - return_value=self.mock_firewall_policy, - ), mock.patch.object(self.cloud.log, 'debug'): + with ( + mock.patch.object( + self.cloud.network, + 'find_firewall_policy', + return_value=self.mock_firewall_policy, + ), + mock.patch.object(self.cloud.log, 'debug'), + ): self.assertTrue( self.cloud.delete_firewall_policy( self.firewall_policy_name, filters @@ -1151,9 +1155,10 @@ def test_remove_rule_from_policy_not_associated(self): ] ) - with mock.patch.object( - self.cloud.network, 'remove_rule_from_policy' - ), mock.patch.object(self.cloud.log, 'debug'): + with ( + mock.patch.object(self.cloud.network, 'remove_rule_from_policy'), + mock.patch.object(self.cloud.log, 'debug'), + ): r = self.cloud.remove_rule_from_policy(policy['id'], rule['id']) self.assertDictEqual(policy, r.to_dict()) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_operator.py b/openstack/tests/unit/cloud/test_operator.py index e92ddab83..d3fd1f62b 100644 --- a/openstack/tests/unit/cloud/test_operator.py +++ b/openstack/tests/unit/cloud/test_operator.py @@ -91,8 +91,7 @@ def side_effect(*args, **kwargs): self.cloud.config.config['region_name'] = 'testregion' with testtools.ExpectedException( exceptions.SDKException, - "Error getting image endpoint on testcloud:testregion: " - "No service", + "Error getting image endpoint on testcloud:testregion: No service", ): self.cloud.get_session_endpoint("image") diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index b70945851..81f80bfd2 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -188,8 +188,7 @@ def test_add_addresses_to_address_group(self, add_addresses): add_addresses.assert_called_once_with(address_group.AddressGroup, data) @mock.patch( - 'openstack.network.v2._proxy.Proxy.' - 'remove_addresses_from_address_group' + 'openstack.network.v2._proxy.Proxy.remove_addresses_from_address_group' ) def test_remove_addresses_from_address_group(self, remove_addresses): data = mock.sentinel diff --git a/openstack/tests/unit/object_store/v1/test_proxy.py b/openstack/tests/unit/object_store/v1/test_proxy.py index 55cc5fe28..58797971d 100644 --- a/openstack/tests/unit/object_store/v1/test_proxy.py +++ b/openstack/tests/unit/object_store/v1/test_proxy.py @@ -685,7 +685,7 @@ class TestTempURLBytesPathUnicodeKey(TestTempURL): url = '/v1/\u00e4/c/\u00f3'.encode() key = 'k\u00e9y' expected_url = url + ( - b'?temp_url_sig=temp_url_signature' b'&temp_url_expires=1400003600' + b'?temp_url_sig=temp_url_signature&temp_url_expires=1400003600' ) expected_body = b'\n'.join( [ @@ -700,7 +700,7 @@ class TestTempURLBytesPathAndKey(TestTempURL): url = '/v1/\u00e4/c/\u00f3'.encode() key = 'k\u00e9y'.encode() expected_url = url + ( - b'?temp_url_sig=temp_url_signature' b'&temp_url_expires=1400003600' + b'?temp_url_sig=temp_url_signature&temp_url_expires=1400003600' ) expected_body = b'\n'.join( [ @@ -715,7 +715,7 @@ class TestTempURLBytesPathAndNonUtf8Key(TestTempURL): url = '/v1/\u00e4/c/\u00f3'.encode() key = b'k\xffy' expected_url = url + ( - b'?temp_url_sig=temp_url_signature' b'&temp_url_expires=1400003600' + b'?temp_url_sig=temp_url_signature&temp_url_expires=1400003600' ) expected_body = b'\n'.join( [ diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 22cbda381..337b3b549 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -3358,11 +3358,16 @@ def test_no_match_return(self): ) def test_find_result_name_not_in_query_parameters(self): - with mock.patch.object( - self.one_result, 'existing', side_effect=self.OneResult.existing - ) as mock_existing, mock.patch.object( - self.one_result, 'list', side_effect=self.OneResult.list - ) as mock_list: + with ( + mock.patch.object( + self.one_result, + 'existing', + side_effect=self.OneResult.existing, + ) as mock_existing, + mock.patch.object( + self.one_result, 'list', side_effect=self.OneResult.list + ) as mock_list, + ): self.assertEqual( self.result, self.one_result.find(self.cloud.compute, "name") ) diff --git a/openstack/utils.py b/openstack/utils.py index 7ccc4c7bc..8d080eb1b 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -59,8 +59,7 @@ def iterate_timeout(timeout, message, wait=2): wait = float(wait) except ValueError: raise exceptions.SDKException( - f"Wait value must be an int or float value. " - f"{wait} given instead" + f"Wait value must be an int or float value. {wait} given instead" ) start = time.time() @@ -407,7 +406,7 @@ def _start_traverse(self): def _get_in_degree(self): """Calculate the in_degree (count incoming) for nodes""" - _in_degree: ty.Dict[str, int] = {u: 0 for u in self._graph.keys()} + _in_degree: dict[str, int] = {u: 0 for u in self._graph.keys()} for u in self._graph: for v in self._graph[u]: _in_degree[v] += 1 @@ -547,7 +546,7 @@ def setdefault(self, k, d=None): def munchify(x, factory=Munch): """Recursively transforms a dictionary into a Munch via copy.""" # Munchify x, using `seen` to track object cycles - seen: ty.Dict[int, ty.Any] = dict() + seen: dict[int, ty.Any] = dict() def munchify_cycles(obj): try: @@ -587,7 +586,7 @@ def unmunchify(x): """Recursively converts a Munch into a dictionary.""" # Munchify x, using `seen` to track object cycles - seen: ty.Dict[int, ty.Any] = dict() + seen: dict[int, ty.Any] = dict() def unmunchify_cycles(obj): try: From ec1d510ea72aa872d551bfc4a53851e2bfdf92bb Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 23 Dec 2024 14:35:20 +0000 Subject: [PATCH 3621/3836] pre-commit: Bump versions Fix the remaining issues it highlights. Most of the automatic changes are kept to separate preceding change. Change-Id: Ib1eebd3359099155a44cf94c134734d929f95597 Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 4 ++-- openstack/baremetal/v1/node.py | 2 +- openstack/cloud/_baremetal.py | 4 +++- openstack/network/v2/_base.py | 2 +- .../baremetal/test_baremetal_allocation.py | 2 +- .../functional/baremetal/test_baremetal_node.py | 2 +- openstack/tests/functional/cloud/test_endpoints.py | 14 +++++++------- openstack/tests/functional/cloud/test_object.py | 4 ++-- openstack/tests/unit/base.py | 12 +++--------- openstack/tests/unit/test_resource.py | 3 +-- 10 files changed, 22 insertions(+), 27 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 291b9cf51..1f37a389e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: hooks: - id: doc8 - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.7.3 + rev: v0.9.6 hooks: - id: ruff args: ['--fix', '--unsafe-fixes'] @@ -32,7 +32,7 @@ repos: - flake8-import-order~=0.18.2 exclude: '^(doc|releasenotes|tools)/.*$' - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.13.0 + rev: v1.15.0 hooks: - id: mypy additional_dependencies: diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 7084b41c0..5d62d8fd5 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -452,7 +452,7 @@ def set_provision_state( version = None if target in _common.PROVISIONING_VERSIONS: - version = '1.%d' % _common.PROVISIONING_VERSIONS[target] + version = f'1.{_common.PROVISIONING_VERSIONS[target]}' if config_drive: # Some config drive actions require a higher version. diff --git a/openstack/cloud/_baremetal.py b/openstack/cloud/_baremetal.py index c4dcdfd5b..1f06dccd6 100644 --- a/openstack/cloud/_baremetal.py +++ b/openstack/cloud/_baremetal.py @@ -348,7 +348,9 @@ def patch_machine(self, name_or_id, patch): patch = [] patch.append({'op': 'remove', 'path': '/instance_info'}) - patch.append({'op': 'replace', 'path': '/name', 'value': 'newname'}) + patch.append( + {'op': 'replace', 'path': '/name', 'value': 'newname'} + ) patch.append( { 'op': 'add', diff --git a/openstack/network/v2/_base.py b/openstack/network/v2/_base.py index 8282cd694..7858eeef3 100644 --- a/openstack/network/v2/_base.py +++ b/openstack/network/v2/_base.py @@ -41,7 +41,7 @@ def _prepare_request( params=params, ) if if_revision is not None: - req.headers['If-Match'] = "revision_number=%d" % if_revision + req.headers['If-Match'] = f"revision_number={if_revision}" return req diff --git a/openstack/tests/functional/baremetal/test_baremetal_allocation.py b/openstack/tests/functional/baremetal/test_baremetal_allocation.py index 5433f8b73..f36ea4c10 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_allocation.py +++ b/openstack/tests/functional/baremetal/test_baremetal_allocation.py @@ -21,7 +21,7 @@ def setUp(self): super().setUp() # NOTE(dtantsur): generate a unique resource class to prevent parallel # tests from clashing. - self.resource_class = 'baremetal-%d' % random.randrange(1024) + self.resource_class = f'baremetal-{random.randrange(1024)}' self.node = self._create_available_node() def _create_available_node(self): diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index 50125224b..a1a731304 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -181,7 +181,7 @@ def test_node_create_in_enroll_provide(self): self.assertEqual(node.provision_state, 'available') def test_node_create_in_enroll_provide_by_name(self): - name = 'node-%d' % random.randint(0, 1000) + name = f'node-{random.randint(0, 1000)}' node = self.create_node(name=name) self.node_id = node.id diff --git a/openstack/tests/functional/cloud/test_endpoints.py b/openstack/tests/functional/cloud/test_endpoints.py index b21bd8d27..30feeb5f7 100644 --- a/openstack/tests/functional/cloud/test_endpoints.py +++ b/openstack/tests/functional/cloud/test_endpoints.py @@ -52,15 +52,15 @@ def setUp(self): def _cleanup_endpoints(self): exception_list = list() - for e in self.operator_cloud.list_endpoints(): - if e.get('region') is not None and e['region'].startswith( - self.new_item_name - ): + for endpoint in self.operator_cloud.list_endpoints(): + if endpoint.get('region') is not None and endpoint[ + 'region' + ].startswith(self.new_item_name): try: - self.operator_cloud.delete_endpoint(id=e['id']) - except Exception as e: + self.operator_cloud.delete_endpoint(id=endpoint['id']) + except Exception as exc: # We were unable to delete a service, let's try with next - exception_list.append(str(e)) + exception_list.append(str(exc)) continue if exception_list: # Raise an error: we must make users aware that something went diff --git a/openstack/tests/functional/cloud/test_object.py b/openstack/tests/functional/cloud/test_object.py index 9ca628f16..87de55359 100644 --- a/openstack/tests/functional/cloud/test_object.py +++ b/openstack/tests/functional/cloud/test_object.py @@ -59,7 +59,7 @@ def test_create_object(self): fake_file.write(fake_content) fake_file.flush() - name = 'test-%d' % size + name = f'test-{size}' self.addCleanup( self.user_cloud.delete_object, container_name, name ) @@ -136,7 +136,7 @@ def test_download_object_to_file(self): fake_file.write(fake_content) fake_file.flush() - name = 'test-%d' % size + name = f'test-{size}' self.addCleanup( self.user_cloud.delete_object, container_name, name ) diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index b63aeb535..a2e85f22c 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -887,16 +887,10 @@ def assert_calls(self, stop_after=None, do_count=True): urllib.parse.parse_qs(history_uri_parts.query), ), ( - 'REST mismatch on call %(index)d. Expected %(call)r. ' - 'Got %(history)r). ' + f'REST mismatch on call {x}. ' + f'Expected {call["method"]} {call["url"]}. ' + f'Got {history.method} {history.url}. ' 'NOTE: query string order differences wont cause mismatch' - % { - 'index': x, - 'call': '{method} {url}'.format( - method=call['method'], url=call['url'] - ), - 'history': f'{history.method} {history.url}', - } ), ) if 'json' in call: diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 337b3b549..14bf83508 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -2398,8 +2398,7 @@ def test_list_response_paginated_with_links_and_query(self): "resources": [{"id": ids[0]}], "resources_links": [ { - "href": "https://example.com/next-url?limit=%d" - % q_limit, + "href": f"https://example.com/next-url?limit={q_limit}", "rel": "next", } ], From ff88f4d8042c2c44238abb6685131cc9ba400451 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 6 Aug 2024 13:08:42 +0100 Subject: [PATCH 3622/3836] cloud: Remove object container cache As with previous resource-specific cache removals, this can be removed since a user can now do caching globally at the proxy layer. We also remove a unused variable which should have been removed in change Idd200a1a5a72710b8eb4556bf9fb95b96be6d99b. Change-Id: Ie97030b6bf2ff987732f44f06801769b3c22a3eb Signed-off-by: Stephen Finucane --- openstack/cloud/_object_store.py | 21 ++++++++------------- openstack/cloud/openstackcloud.py | 2 -- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index e7815413e..e245be3b6 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -64,25 +64,21 @@ def search_containers(self, name=None, filters=None): containers = self.list_containers() return _utils._filter_list(containers, name, filters) + # TODO(stephenfin): Remove 'skip_cache' as it no longer does anything def get_container(self, name, skip_cache=False): """Get metadata about a container. :param str name: Name of the container to get metadata for. - :param bool skip_cache: - Ignore the cache of container metadata for this container. - Defaults to ``False``. + :param bool skip_cache: Ignored. Present for backwards compatibility. :returns: An object store ``Container`` object if found, else None. """ - if skip_cache or name not in self._container_cache: - try: - container = self.object_store.get_container_metadata(name) - self._container_cache[name] = container - except exceptions.HttpException as ex: - if ex.response.status_code == 404: - return None - raise - return self._container_cache[name] + try: + return self.object_store.get_container_metadata(name) + except exceptions.HttpException as ex: + if ex.response.status_code == 404: + return None + raise def create_container(self, name, public=False): """Create an object-store container. @@ -108,7 +104,6 @@ def delete_container(self, name): """ try: self.object_store.delete_container(name, ignore_missing=False) - self._container_cache.pop(name, None) return True except exceptions.NotFoundException: return False diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 9d667c08e..f6fdf6cf2 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -248,8 +248,6 @@ def __init__( self._cache_expirations[expire_key] = expirations[expire_key] self._api_cache_keys = set() - self._container_cache = dict() - self._file_hash_cache = dict() self._local_ipv6 = ( _utils.localhost_supports_ipv6() if not self.force_ipv4 else False From 137ec3fc660cc442bb76a62ac64f3d43f258753f Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 18 Feb 2025 18:56:55 +0000 Subject: [PATCH 3623/3836] mypy: Move configuration to pyproject.toml For consistency with other projects. We also start following imports, which avoids weirdness where we modify a single file that relies on type hints from another for proper function. Change-Id: I12ca6baac2498efc920c570a3bcdbfffb9c61220 Signed-off-by: Stephen Finucane --- pyproject.toml | 22 ++++++++++++++++++++++ setup.cfg | 20 -------------------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7113b0c03..2e99ffe5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,25 @@ +[tool.mypy] +python_version = "3.9" +show_column_numbers = true +show_error_context = true +ignore_missing_imports = true +follow_imports = "normal" +incremental = true +check_untyped_defs = true +warn_unused_ignores = false +# keep this in-sync with 'mypy.exclude' in '.pre-commit-config.yaml' +exclude = ''' +(?x)( + doc + | examples + | releasenotes + ) +''' + +[[tool.mypy.overrides]] +module = ["openstack.tests.unit.*"] +ignore_errors = true + [tool.ruff] line-length = 79 diff --git a/setup.cfg b/setup.cfg index 51a4a853c..24bace6ac 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,23 +28,3 @@ packages = [entry_points] console_scripts = openstack-inventory = openstack.cloud.cmd.inventory:main - -[mypy] -python_version = 3.9 -show_column_numbers = true -show_error_context = true -ignore_missing_imports = true -# follow_imports = normal -follow_imports = skip -incremental = true -check_untyped_defs = true -warn_unused_ignores = false -# keep this in-sync with 'mypy.exclude' in '.pre-commit-config.yaml' -exclude = (?x)( - doc - | examples - | releasenotes - ) - -[mypy-openstack.tests.unit.*] -ignore_errors = true From 7bd67a1c0884867c6948dcdcc68fc61abecb6d23 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 20 Nov 2024 12:31:20 +0000 Subject: [PATCH 3624/3836] mypy: Add keystoneauth1 as dependency Now that it is typed. Change-Id: I11f6285e6b28c97d7b55b09532c9ce8009f7fcbf Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1f37a389e..75b6fbbc5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,6 +36,7 @@ repos: hooks: - id: mypy additional_dependencies: + - keystoneauth1 - types-decorator - types-PyYAML - types-requests From a8b8016f11296a5135c819816db3325f89cc2eed Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 18 Feb 2025 19:00:07 +0000 Subject: [PATCH 3625/3836] mypy: Enable stricter mode There are a few TODOs to address before we make things stricter again. We'll address those in follow-ups. Change-Id: I876e34dbe9c913dca029929f64d1e3a8b323464b Signed-off-by: Stephen Finucane --- pyproject.toml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2e99ffe5f..4ff0f43d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,9 +4,21 @@ show_column_numbers = true show_error_context = true ignore_missing_imports = true follow_imports = "normal" -incremental = true check_untyped_defs = true -warn_unused_ignores = false +warn_unused_ignores = true +# many of the following are false while we incrementally add typing +warn_return_any = false +warn_unused_configs = true +warn_redundant_casts = true +strict_equality = true +disallow_untyped_decorators = false +disallow_any_generics = false +disallow_subclassing_any = false +disallow_untyped_calls = false +disallow_incomplete_defs = false +disallow_untyped_defs = false +no_implicit_reexport = false +extra_checks = true # keep this in-sync with 'mypy.exclude' in '.pre-commit-config.yaml' exclude = ''' (?x)( From 5c0ca60b7d01774fc6655bcab128a1392961f1bd Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 19 Feb 2025 10:47:55 +0000 Subject: [PATCH 3626/3836] mypy: Enable disallow_incomplete_defs Change-Id: I28a4d4c1f2e04c6284cbc1dc4f543df0a0706d59 Signed-off-by: Stephen Finucane --- openstack/__init__.py | 2 +- openstack/baremetal/v1/_proxy.py | 15 +++++-- openstack/baremetal/v1/driver.py | 11 +++++- openstack/baremetal/v1/node.py | 4 +- openstack/block_storage/v3/_proxy.py | 8 ++-- openstack/image/v2/_proxy.py | 2 +- openstack/network/v2/_proxy.py | 27 ++++++++----- openstack/network/v2/router.py | 4 +- openstack/proxy.py | 58 +++++++++++++++------------- openstack/resource.py | 25 ++++++------ pyproject.toml | 2 +- requirements.txt | 1 + 12 files changed, 94 insertions(+), 65 deletions(-) diff --git a/openstack/__init__.py b/openstack/__init__.py index f90b4ec92..792305e5a 100644 --- a/openstack/__init__.py +++ b/openstack/__init__.py @@ -70,7 +70,7 @@ def connect( options: ty.Optional[argparse.Namespace] = None, load_yaml_config: bool = True, load_envvars: bool = True, - **kwargs, + **kwargs: ty.Any, ) -> openstack.connection.Connection: """Create a :class:`~openstack.connection.Connection` diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 38907b460..fc1eba857 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -12,6 +12,8 @@ import typing as ty +import requests + from openstack.baremetal.v1 import _common from openstack.baremetal.v1 import allocation as _allocation from openstack.baremetal.v1 import chassis as _chassis @@ -225,8 +227,12 @@ def list_driver_vendor_passthru(self, driver): return driver.list_vendor_passthru(self) def call_driver_vendor_passthru( - self, driver, verb: str, method: str, body=None - ): + self, + driver: ty.Union[str, _driver.Driver], + verb: str, + method: str, + body: object = None, + ) -> requests.Response: """Call driver's vendor_passthru method. :param driver: The value can be the name of a driver or a @@ -238,8 +244,9 @@ def call_driver_vendor_passthru( :returns: Server response """ - driver = self.get_driver(driver) - return driver.call_vendor_passthru(self, verb, method, body) + return self.get_driver(driver).call_vendor_passthru( + self, verb, method, body + ) def nodes(self, details=False, **query): """Retrieve a generator of nodes. diff --git a/openstack/baremetal/v1/driver.py b/openstack/baremetal/v1/driver.py index d8a9e97fe..3f476379f 100644 --- a/openstack/baremetal/v1/driver.py +++ b/openstack/baremetal/v1/driver.py @@ -12,6 +12,9 @@ import typing as ty +from keystoneauth1 import adapter +import requests + from openstack.baremetal.v1 import _common from openstack import exceptions from openstack import resource @@ -155,8 +158,12 @@ def list_vendor_passthru(self, session): return response.json() def call_vendor_passthru( - self, session, verb: str, method: str, body: ty.Optional[dict] = None - ): + self, + session: adapter.Adapter, + verb: str, + method: str, + body: ty.Optional[dict] = None, + ) -> requests.Response: """Call a vendor specific passthru method Contents of body are params passed to the hardware driver diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 5d62d8fd5..b60cf6b49 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -15,6 +15,8 @@ import typing as ty import warnings +from keystoneauth1 import adapter + from openstack.baremetal.v1 import _common from openstack import exceptions from openstack import resource @@ -815,7 +817,7 @@ def set_power_state(self, session, target, wait=False, timeout=None): def attach_vif( self, - session, + session: adapter.Adapter, vif_id: str, retry_on_conflict: bool = True, *, diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index e9b7bd3ef..28224c13f 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -1921,7 +1921,7 @@ def find_service( self, name_or_id: str, ignore_missing: ty.Literal[True] = True, - **query, + **query: ty.Any, ) -> ty.Optional[_service.Service]: ... @ty.overload @@ -1929,7 +1929,7 @@ def find_service( self, name_or_id: str, ignore_missing: ty.Literal[False], - **query, + **query: ty.Any, ) -> _service.Service: ... # excuse the duplication here: it's mypy's fault @@ -1939,14 +1939,14 @@ def find_service( self, name_or_id: str, ignore_missing: bool, - **query, + **query: ty.Any, ) -> ty.Optional[_service.Service]: ... def find_service( self, name_or_id: str, ignore_missing: bool = True, - **query, + **query: ty.Any, ) -> ty.Optional[_service.Service]: """Find a single service diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 986203b8b..bc2e4559b 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -340,7 +340,7 @@ def create_image( properties = image_kwargs.pop('properties', {}) image_kwargs.update(self._make_v2_image_params(meta, properties)) image_kwargs['name'] = name - image = self._create(_image.Image, **image_kwargs) + image = self._create(_image.Image, **image_kwargs) # type: ignore[arg-type] return image diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 2ccd2f4ba..5c5462cf5 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -196,10 +196,10 @@ class Proxy(proxy.Proxy): def _update( self, resource_type: type[resource.Resource], - value, - base_path=None, - if_revision=None, - **attrs, + value: ty.Union[str, resource.Resource], + base_path: ty.Optional[str] = None, + if_revision: ty.Optional[int] = None, + **attrs: ty.Any, ) -> resource.Resource: res = self._get_resource(resource_type, value, **attrs) return res.commit(self, base_path=base_path, if_revision=if_revision) @@ -208,10 +208,10 @@ def _update( def _delete( self, resource_type: type[resource.Resource], - value, - ignore_missing=True, - if_revision=None, - **attrs, + value: ty.Union[str, resource.Resource], + ignore_missing: bool = True, + if_revision: ty.Optional[int] = None, + **attrs: ty.Any, ) -> ty.Optional[resource.Resource]: res = self._get_resource(resource_type, value, **attrs) @@ -306,7 +306,9 @@ def address_groups(self, **query): return self._list(_address_group.AddressGroup, **query) def update_address_group( - self, address_group, **attrs + self, + address_group: ty.Union[str, _address_group.AddressGroup], + **attrs: ty.Any, ) -> _address_group.AddressGroup: """Update an address group @@ -2979,7 +2981,12 @@ def ports(self, **query): """ return self._list(_port.Port, **query) - def update_port(self, port, if_revision=None, **attrs) -> _port.Port: + def update_port( + self, + port: ty.Union[str, _port.Port], + if_revision: ty.Optional[int] = None, + **attrs: ty.Any, + ) -> _port.Port: """Update a port :param port: Either the id of a port or a diff --git a/openstack/network/v2/router.py b/openstack/network/v2/router.py index 42a84bbfa..f5123c522 100644 --- a/openstack/network/v2/router.py +++ b/openstack/network/v2/router.py @@ -121,7 +121,7 @@ def remove_interface(self, session, **body): resp = self._put(session, url, body) return resp.json() - def add_extra_routes(self, session, body) -> 'Router': + def add_extra_routes(self, session, body): """Add extra routes to a logical router. :param session: The session to communicate through. @@ -137,7 +137,7 @@ def add_extra_routes(self, session, body) -> 'Router': self._translate_response(resp) return self - def remove_extra_routes(self, session, body) -> 'Router': + def remove_extra_routes(self, session, body): """Remove extra routes from a logical router. :param session: The session to communicate through. diff --git a/openstack/proxy.py b/openstack/proxy.py index 85ad957f5..7cfe0252d 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -29,6 +29,7 @@ from openstack import _log from openstack import exceptions from openstack import resource +from openstack import utils from openstack import warnings as os_warnings @@ -436,7 +437,10 @@ def _get_connection(self): ) def _get_resource( - self, resource_type: type[ResourceType], value, **attrs + self, + resource_type: type[ResourceType], + value: ty.Union[None, str, ResourceType, utils.Munch], + **attrs: ty.Any, ) -> ResourceType: """Get a resource object to work on @@ -489,7 +493,7 @@ def _find( resource_type: type[ResourceType], name_or_id: str, ignore_missing: ty.Literal[True] = True, - **attrs, + **attrs: ty.Any, ) -> ty.Optional[ResourceType]: ... @ty.overload @@ -498,7 +502,7 @@ def _find( resource_type: type[ResourceType], name_or_id: str, ignore_missing: ty.Literal[False], - **attrs, + **attrs: ty.Any, ) -> ResourceType: ... # excuse the duplication here: it's mypy's fault @@ -509,7 +513,7 @@ def _find( resource_type: type[ResourceType], name_or_id: str, ignore_missing: bool, - **attrs, + **attrs: ty.Any, ) -> ty.Optional[ResourceType]: ... def _find( @@ -517,7 +521,7 @@ def _find( resource_type: type[ResourceType], name_or_id: str, ignore_missing: bool = True, - **attrs, + **attrs: ty.Any, ) -> ty.Optional[ResourceType]: """Find a resource @@ -541,9 +545,9 @@ def _find( def _delete( self, resource_type: type[ResourceType], - value, - ignore_missing=True, - **attrs, + value: ty.Union[str, ResourceType], + ignore_missing: bool = True, + **attrs: ty.Any, ) -> ty.Optional[ResourceType]: """Delete a resource @@ -583,9 +587,9 @@ def _delete( def _update( self, resource_type: type[ResourceType], - value, - base_path=None, - **attrs, + value: ty.Union[str, ResourceType], + base_path: ty.Optional[str] = None, + **attrs: ty.Any, ) -> ResourceType: """Update a resource @@ -613,8 +617,8 @@ def _update( def _create( self, resource_type: type[ResourceType], - base_path=None, - **attrs, + base_path: ty.Optional[str] = None, + **attrs: ty.Any, ) -> ResourceType: """Create a resource from attributes @@ -649,8 +653,8 @@ def _create( def _bulk_create( self, resource_type: type[ResourceType], - data, - base_path=None, + data: list[dict[str, ty.Any]], + base_path: ty.Optional[str] = None, ) -> ty.Generator[ResourceType, None, None]: """Create a resource from attributes @@ -675,11 +679,11 @@ def _bulk_create( def _get( self, resource_type: type[ResourceType], - value=None, - requires_id=True, - base_path=None, - skip_cache=False, - **attrs, + value: ty.Union[str, ResourceType, None] = None, + requires_id: bool = True, + base_path: ty.Optional[str] = None, + skip_cache: bool = False, + **attrs: ty.Any, ) -> ResourceType: """Fetch a resource @@ -716,10 +720,10 @@ def _get( def _list( self, resource_type: type[ResourceType], - paginated=True, - base_path=None, - jmespath_filters=None, - **attrs, + paginated: bool = True, + base_path: ty.Optional[str] = None, + jmespath_filters: ty.Optional[str] = None, + **attrs: ty.Any, ) -> ty.Generator[ResourceType, None, None]: """List a resource @@ -766,9 +770,9 @@ def _list( def _head( self, resource_type: type[ResourceType], - value=None, - base_path=None, - **attrs, + value: ty.Union[str, ResourceType, None] = None, + base_path: ty.Optional[str] = None, + **attrs: ty.Any, ) -> ResourceType: """Retrieve a resource's header diff --git a/openstack/resource.py b/openstack/resource.py index b03c6785c..5e189e3c2 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -38,6 +38,7 @@ class that represent a remote resource. The attributes that import itertools import operator import typing as ty +import typing_extensions as ty_ext import urllib.parse import warnings @@ -2252,29 +2253,29 @@ def _get_one_match(cls, name_or_id, results): @classmethod def find( cls, - session, + session: adapter.Adapter, name_or_id: str, ignore_missing: ty.Literal[True] = True, list_base_path: ty.Optional[str] = None, *, microversion: ty.Optional[str] = None, all_projects: ty.Optional[bool] = None, - **params, - ) -> ty.Optional['Resource']: ... + **params: ty.Any, + ) -> ty.Optional[ty_ext.Self]: ... @ty.overload @classmethod def find( cls, - session, + session: adapter.Adapter, name_or_id: str, ignore_missing: ty.Literal[False], list_base_path: ty.Optional[str] = None, *, microversion: ty.Optional[str] = None, all_projects: ty.Optional[bool] = None, - **params, - ) -> 'Resource': ... + **params: ty.Any, + ) -> ty_ext.Self: ... # excuse the duplication here: it's mypy's fault # https://github.com/python/mypy/issues/14764 @@ -2282,28 +2283,28 @@ def find( @classmethod def find( cls, - session, + session: adapter.Adapter, name_or_id: str, ignore_missing: bool, list_base_path: ty.Optional[str] = None, *, microversion: ty.Optional[str] = None, all_projects: ty.Optional[bool] = None, - **params, - ): ... + **params: ty.Any, + ) -> ty.Optional[ty_ext.Self]: ... @classmethod def find( cls, - session, + session: adapter.Adapter, name_or_id: str, ignore_missing: bool = True, list_base_path: ty.Optional[str] = None, *, microversion: ty.Optional[str] = None, all_projects: ty.Optional[bool] = None, - **params, - ): + **params: ty.Any, + ) -> ty.Optional[ty_ext.Self]: """Find a resource by its name or id. :param session: The session to use for making this request. diff --git a/pyproject.toml b/pyproject.toml index 4ff0f43d3..b8fed96a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ disallow_untyped_decorators = false disallow_any_generics = false disallow_subclassing_any = false disallow_untyped_calls = false -disallow_incomplete_defs = false +disallow_incomplete_defs = true disallow_untyped_defs = false no_implicit_reexport = false extra_checks = true diff --git a/requirements.txt b/requirements.txt index 1637cc40e..a57c83ed3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,4 @@ platformdirs>=3 # MIT License psutil>=3.2.2 # BSD PyYAML>=3.13 # MIT requestsexceptions>=1.2.0 # Apache-2.0 +typing-extensions>=4.12.0 # PSF From 561e8cb8cd39bfdcaa6506644fd5ceb8808e8cbc Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 19 Feb 2025 10:48:12 +0000 Subject: [PATCH 3627/3836] typing: Annotate Resource.wait_for_* utils Change-Id: I0826abd00f61b127b7e0190a4c61bdd9f5d3143d Signed-off-by: Stephen Finucane --- openstack/resource.py | 62 ++++++++++++++++++++++++------------------- openstack/utils.py | 19 +++++++++++-- 2 files changed, 52 insertions(+), 29 deletions(-) diff --git a/openstack/resource.py b/openstack/resource.py index 5e189e3c2..5f0cacc25 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -2378,34 +2378,35 @@ def find( ) -def _normalize_status(status): +def _normalize_status(status: ty.Optional[str]) -> ty.Optional[str]: if status is not None: status = status.lower() return status +ResourceT = ty.TypeVar('ResourceT', bound=Resource) + + def wait_for_status( - session, - resource, - status, - failures, - interval=None, - wait=None, - attribute='status', - callback=None, -): + session: adapter.Adapter, + resource: ResourceT, + status: str, + failures: list[str], + interval: ty.Union[int, float, None] = 2, + wait: ty.Optional[int] = None, + attribute: str = 'status', + callback: ty.Optional[ty.Callable[[int], None]] = None, +) -> ResourceT: """Wait for the resource to be in a particular status. :param session: The session to use for making this request. - :type session: :class:`~keystoneauth1.adapter.Adapter` :param resource: The resource to wait on to reach the status. The resource must have a status attribute specified via ``attribute``. - :type resource: :class:`~openstack.resource.Resource` :param status: Desired status of the resource. - :param list failures: Statuses that would indicate the transition + :param failures: Statuses that would indicate the transition failed such as 'ERROR'. Defaults to ['ERROR']. - :param interval: Number of seconds to wait between checks. - Set to ``None`` to use the default interval. + :param interval: Number of seconds to wait between checks. Set to ``None`` + to use the default interval. :param wait: Maximum number of seconds to wait for transition. Set to ``None`` to wait forever. :param attribute: Name of the resource attribute that contains the status. @@ -2414,14 +2415,13 @@ def wait_for_status( value from 0-100. :return: The updated resource. - :raises: :class:`~openstack.exceptions.ResourceTimeout` transition - to status failed to occur in wait seconds. - :raises: :class:`~openstack.exceptions.ResourceFailure` resource - transitioned to one of the failure states. - :raises: :class:`~AttributeError` if the resource does not have a status - attribute + :raises: :class:`~openstack.exceptions.ResourceTimeout` if the transition + to status failed to occur in ``wait`` seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + transitioned to one of the states in ``failures``. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute """ - current_status = getattr(resource, attribute) if _normalize_status(current_status) == _normalize_status(status): return resource @@ -2463,21 +2463,27 @@ def wait_for_status( progress = getattr(resource, 'progress', None) or 0 callback(progress) + raise RuntimeError('cannot reach this') -def wait_for_delete(session, resource, interval, wait, callback=None): - """Wait for the resource to be deleted. + +def wait_for_delete( + session: adapter.Adapter, + resource: ResourceT, + interval: ty.Union[int, float, None] = 2, + wait: ty.Optional[int] = None, + callback: ty.Optional[ty.Callable[[int], None]] = None, +) -> ResourceT: + """Wait for a resource to be deleted. :param session: The session to use for making this request. - :type session: :class:`~keystoneauth1.adapter.Adapter` :param resource: The resource to wait on to be deleted. - :type resource: :class:`~openstack.resource.Resource` :param interval: Number of seconds to wait between checks. :param wait: Maximum number of seconds to wait for the delete. :param callback: A callback function. This will be called with a single value, progress. This is API specific but is generally a percentage value from 0-100. - :return: Method returns self on success. + :return: The original resource. :raises: :class:`~openstack.exceptions.ResourceTimeout` transition to status failed to occur in wait seconds. """ @@ -2501,3 +2507,5 @@ def wait_for_delete(session, resource, interval, wait, callback=None): if callback: progress = getattr(resource, 'progress', None) or 0 callback(progress) + + raise RuntimeError('cannot reach this') diff --git a/openstack/utils.py b/openstack/utils.py index 8d080eb1b..06a9ad6d0 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -36,13 +36,28 @@ def urljoin(*args): return '/'.join(str(a or '').strip('/') for a in args) -def iterate_timeout(timeout, message, wait=2): +def iterate_timeout( + timeout: ty.Optional[int], + message: str, + wait: ty.Union[int, float, None] = 2, +) -> ty.Generator[int, None, None]: """Iterate and raise an exception on timeout. This is a generator that will continually yield and sleep for wait seconds, and if the timeout is reached, will raise an exception with . + :param timeout: Maximum number of seconds to wait for transition. Set to + ``None`` to wait forever. + :param message: The message to use for the exception if the timeout is + reached. + :param wait: Number of seconds to wait between checks. Set to ``None`` + to use the default interval. + + :returns: None + :raises: :class:`~openstack.exceptions.ResourceTimeout` transition + :raises: :class:`~openstack.exceptions.SDKException` if ``wait`` is not a + valid float, integer or None. """ log = _log.setup_logging('openstack.iterate_timeout') @@ -59,7 +74,7 @@ def iterate_timeout(timeout, message, wait=2): wait = float(wait) except ValueError: raise exceptions.SDKException( - f"Wait value must be an int or float value. {wait} given instead" + f"Wait value must be an int or float value. {wait!r} given instead" ) start = time.time() From 6395008300df3c8c4e9d621ac6ead2dea262f363 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 19 Feb 2025 10:49:15 +0000 Subject: [PATCH 3628/3836] proxy: Add 'wait_for_*' helpers to all proxy APIs These are very useful but were inconsistently available. Correct this. Change-Id: I9977427e83136ed4a0b3058887aba4cee4ace6a6 Signed-off-by: Stephen Finucane --- openstack/accelerator/v2/_proxy.py | 74 +++++ openstack/baremetal/v1/_proxy.py | 294 ++++++++++++------ .../baremetal_introspection/v1/_proxy.py | 70 +++++ openstack/block_storage/v2/_proxy.py | 150 +++++---- openstack/block_storage/v3/_proxy.py | 90 +++--- openstack/clustering/v1/_proxy.py | 108 ++++--- openstack/compute/v2/_proxy.py | 67 +++- .../v1/_proxy.py | 81 ++++- openstack/database/v1/_proxy.py | 66 ++++ openstack/dns/v2/_proxy.py | 65 ++++ openstack/identity/v2/_proxy.py | 66 ++++ openstack/identity/v3/_proxy.py | 65 ++++ openstack/image/v1/_proxy.py | 65 ++++ openstack/image/v2/_proxy.py | 60 +++- openstack/instance_ha/v1/_proxy.py | 65 ++++ openstack/key_manager/v1/_proxy.py | 66 ++++ openstack/load_balancer/v2/_proxy.py | 65 ++++ openstack/message/v2/_proxy.py | 65 ++++ openstack/network/v2/_proxy.py | 63 ++++ openstack/object_store/v1/_proxy.py | 64 ++++ openstack/orchestration/v1/_proxy.py | 185 ++++++----- openstack/placement/v1/_proxy.py | 65 ++++ openstack/resource.py | 2 +- openstack/shared_file_system/v2/_proxy.py | 122 ++++---- .../tests/unit/block_storage/v2/test_proxy.py | 12 +- .../tests/unit/block_storage/v3/test_proxy.py | 12 +- .../tests/unit/clustering/v1/test_proxy.py | 19 +- .../unit/shared_file_system/v2/test_proxy.py | 6 +- openstack/workflow/v2/_proxy.py | 66 ++++ 29 files changed, 1767 insertions(+), 431 deletions(-) diff --git a/openstack/accelerator/v2/_proxy.py b/openstack/accelerator/v2/_proxy.py index 69f2b2cba..b30c360f0 100644 --- a/openstack/accelerator/v2/_proxy.py +++ b/openstack/accelerator/v2/_proxy.py @@ -10,14 +10,19 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + from openstack.accelerator.v2 import accelerator_request as _arq from openstack.accelerator.v2 import deployable as _deployable from openstack.accelerator.v2 import device as _device from openstack.accelerator.v2 import device_profile as _device_profile from openstack import proxy +from openstack import resource class Proxy(proxy.Proxy): + # ========== Deployables ========== + def deployables(self, **query): """Retrieve a generator of deployables. @@ -48,6 +53,8 @@ def update_deployable(self, uuid, patch): self, patch ) + # ========== Devices ========== + def devices(self, **query): """Retrieve a generator of devices. @@ -81,6 +88,8 @@ def get_device(self, uuid, fields=None): """ return self._get(_device.Device, uuid) + # ========== Device profiles ========== + def device_profiles(self, **query): """Retrieve a generator of device profiles. @@ -129,6 +138,8 @@ def get_device_profile(self, uuid, fields=None): """ return self._get(_device_profile.DeviceProfile, uuid) + # ========== Accelerator requests ========== + def accelerator_requests(self, **query): """Retrieve a generator of accelerator requests. @@ -192,3 +203,66 @@ def update_accelerator_request(self, uuid, properties): return self._get_resource(_arq.AcceleratorRequest, uuid).patch( self, properties ) + + # ========== Utilities ========== + + def wait_for_status( + self, + res: resource.ResourceT, + status: str, + failures: ty.Optional[list[str]] = None, + interval: ty.Union[int, float, None] = 2, + wait: ty.Optional[int] = None, + attribute: str = 'status', + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for the resource to be in a particular status. + + :param session: The session to use for making this request. + :param resource: The resource to wait on to reach the status. The + resource must have a status attribute specified via ``attribute``. + :param status: Desired status of the resource. + :param failures: Statuses that would indicate the transition + failed such as 'ERROR'. Defaults to ['ERROR']. + :param interval: Number of seconds to wait between checks. + :param wait: Maximum number of seconds to wait for transition. + Set to ``None`` to wait forever. + :param attribute: Name of the resource attribute that contains the + status. + :param callback: A callback function. This will be called with a single + value, progress. This is API specific but is generally a percentage + value from 0-100. + + :return: The updated resource. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if the + transition to status failed to occur in ``wait`` seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + transitioned to one of the states in ``failures``. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute + """ + return resource.wait_for_status( + self, res, status, failures, interval, wait, attribute, callback + ) + + def wait_for_delete( + self, + res: resource.ResourceT, + interval: int = 2, + wait: int = 120, + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for a resource to be deleted. + + :param res: The resource to wait on to be deleted. + :param interval: Number of seconds to wait before to consecutive + checks. + :param wait: Maximum number of seconds to wait before the change. + :param callback: A callback function. This will be called with a single + value, progress, which is a percentage value from 0-100. + + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete(self, res, interval, wait, callback) diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index fc1eba857..3a51cc9c7 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -27,6 +27,7 @@ from openstack.baremetal.v1 import volume_target as _volumetarget from openstack import exceptions from openstack import proxy +from openstack import resource from openstack import utils @@ -69,6 +70,8 @@ def _get_with_fields(self, resource_type, value, fields=None): **kwargs, ) + # ========== Chassis ========== + def chassis(self, details=False, **query): """Retrieve a generator of chassis. @@ -186,6 +189,8 @@ def delete_chassis(self, chassis, ignore_missing=True): _chassis.Chassis, chassis, ignore_missing=ignore_missing ) + # ========== Drivers ========== + def drivers(self, details=False, **query): """Retrieve a generator of drivers. @@ -248,6 +253,8 @@ def call_driver_vendor_passthru( self, verb, method, body ) + # ========== Nodes ========== + def nodes(self, details=False, **query): """Retrieve a generator of nodes. @@ -718,6 +725,114 @@ def delete_node(self, node, ignore_missing=True): """ return self._delete(_node.Node, node, ignore_missing=ignore_missing) + # ========== Node actions ========== + + def add_node_trait(self, node, trait): + """Add a trait to a node. + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :param trait: trait to remove from the node. + :returns: The updated node + """ + res = self._get_resource(_node.Node, node) + return res.add_trait(self, trait) + + def remove_node_trait(self, node, trait, ignore_missing=True): + """Remove a trait from a node. + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :param trait: trait to remove from the node. + :param bool ignore_missing: When set to ``False``, an exception + :class:`~openstack.exceptions.NotFoundException` will be raised + when the trait could not be found. When set to ``True``, no + exception will be raised when attempting to delete a non-existent + trait. + :returns: The updated :class:`~openstack.baremetal.v1.node.Node` + """ + res = self._get_resource(_node.Node, node) + return res.remove_trait(self, trait, ignore_missing=ignore_missing) + + def call_node_vendor_passthru(self, node, verb, method, body=None): + """Calls vendor_passthru for a node. + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :param verb: The HTTP verb, one of GET, SET, POST, DELETE. + :param method: The method to call using vendor_passthru. + :param body: The JSON body in the HTTP call. + :returns: The raw response from the method. + """ + res = self._get_resource(_node.Node, node) + return res.call_vendor_passthru(self, verb, method, body) + + def list_node_vendor_passthru(self, node): + """Lists vendor_passthru for a node. + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :returns: A list of vendor_passthru methods for the node. + """ + res = self._get_resource(_node.Node, node) + return res.list_vendor_passthru(self) + + def get_node_console(self, node): + """Get the console for a node. + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :returns: Connection information for the console. + """ + res = self._get_resource(_node.Node, node) + return res.get_node_console(self) + + def enable_node_console(self, node): + """Enable the console for a node. + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :returns: None + """ + res = self._get_resource(_node.Node, node) + return res.set_console_mode(self, True) + + def disable_node_console(self, node): + """Disable the console for a node. + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :returns: None + """ + res = self._get_resource(_node.Node, node) + return res.set_console_mode(self, False) + + def set_node_traits(self, node, traits): + """Set traits for a node. + + Removes any existing traits and adds the traits passed in to this + method. + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :param traits: list of traits to add to the node. + :returns: The updated :class:`~openstack.baremetal.v1.node.Node` + """ + res = self._get_resource(_node.Node, node) + return res.set_traits(self, traits) + + def list_node_firmware(self, node): + """Lists firmware components for a node. + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :returns: A list of the node's firmware components. + """ + res = self._get_resource(_node.Node, node) + return res.list_firmware(self) + + # ========== Ports ========== + def ports(self, details=False, **query): """Retrieve a generator of ports. @@ -843,6 +958,8 @@ def delete_port(self, port, ignore_missing=True): """ return self._delete(_port.Port, port, ignore_missing=ignore_missing) + # ========== Port groups ========== + def port_groups(self, details=False, **query): """Retrieve a generator of port groups. @@ -970,6 +1087,8 @@ def delete_port_group(self, port_group, ignore_missing=True): _portgroup.PortGroup, port_group, ignore_missing=ignore_missing ) + # ========== VIFs ========== + def attach_vif_to_node( self, node: ty.Union[_node.Node, str], @@ -1049,6 +1168,8 @@ def list_node_vifs(self, node): res = self._get_resource(_node.Node, node) return res.list_vifs(self) + # ========== Allocations ========== + def allocations(self, **query): """Retrieve a generator of allocations. @@ -1175,109 +1296,7 @@ def wait_for_allocation( res = self._get_resource(_allocation.Allocation, allocation) return res.wait(self, timeout=timeout, ignore_error=ignore_error) - def add_node_trait(self, node, trait): - """Add a trait to a node. - - :param node: The value can be the name or ID of a node or a - :class:`~openstack.baremetal.v1.node.Node` instance. - :param trait: trait to remove from the node. - :returns: The updated node - """ - res = self._get_resource(_node.Node, node) - return res.add_trait(self, trait) - - def remove_node_trait(self, node, trait, ignore_missing=True): - """Remove a trait from a node. - - :param node: The value can be the name or ID of a node or a - :class:`~openstack.baremetal.v1.node.Node` instance. - :param trait: trait to remove from the node. - :param bool ignore_missing: When set to ``False``, an exception - :class:`~openstack.exceptions.NotFoundException` will be raised - when the trait could not be found. When set to ``True``, no - exception will be raised when attempting to delete a non-existent - trait. - :returns: The updated :class:`~openstack.baremetal.v1.node.Node` - """ - res = self._get_resource(_node.Node, node) - return res.remove_trait(self, trait, ignore_missing=ignore_missing) - - def call_node_vendor_passthru(self, node, verb, method, body=None): - """Calls vendor_passthru for a node. - - :param node: The value can be the name or ID of a node or a - :class:`~openstack.baremetal.v1.node.Node` instance. - :param verb: The HTTP verb, one of GET, SET, POST, DELETE. - :param method: The method to call using vendor_passthru. - :param body: The JSON body in the HTTP call. - :returns: The raw response from the method. - """ - res = self._get_resource(_node.Node, node) - return res.call_vendor_passthru(self, verb, method, body) - - def list_node_vendor_passthru(self, node): - """Lists vendor_passthru for a node. - - :param node: The value can be the name or ID of a node or a - :class:`~openstack.baremetal.v1.node.Node` instance. - :returns: A list of vendor_passthru methods for the node. - """ - res = self._get_resource(_node.Node, node) - return res.list_vendor_passthru(self) - - def get_node_console(self, node): - """Get the console for a node. - - :param node: The value can be the name or ID of a node or a - :class:`~openstack.baremetal.v1.node.Node` instance. - :returns: Connection information for the console. - """ - res = self._get_resource(_node.Node, node) - return res.get_node_console(self) - - def enable_node_console(self, node): - """Enable the console for a node. - - :param node: The value can be the name or ID of a node or a - :class:`~openstack.baremetal.v1.node.Node` instance. - :returns: None - """ - res = self._get_resource(_node.Node, node) - return res.set_console_mode(self, True) - - def disable_node_console(self, node): - """Disable the console for a node. - - :param node: The value can be the name or ID of a node or a - :class:`~openstack.baremetal.v1.node.Node` instance. - :returns: None - """ - res = self._get_resource(_node.Node, node) - return res.set_console_mode(self, False) - - def set_node_traits(self, node, traits): - """Set traits for a node. - - Removes any existing traits and adds the traits passed in to this - method. - - :param node: The value can be the name or ID of a node or a - :class:`~openstack.baremetal.v1.node.Node` instance. - :param traits: list of traits to add to the node. - :returns: The updated :class:`~openstack.baremetal.v1.node.Node` - """ - res = self._get_resource(_node.Node, node) - return res.set_traits(self, traits) - - def list_node_firmware(self, node): - """Lists firmware components for a node. - - :param node: The value can be the name or ID of a node or a - :class:`~openstack.baremetal.v1.node.Node` instance. - :returns: A list of the node's firmware components. - """ - res = self._get_resource(_node.Node, node) - return res.list_firmware(self) + # ========== Volume connectors ========== def volume_connectors(self, details=False, **query): """Retrieve a generator of volume_connector. @@ -1426,6 +1445,8 @@ def delete_volume_connector(self, volume_connector, ignore_missing=True): ignore_missing=ignore_missing, ) + # ========== Volume targets ========== + def volume_targets(self, details=False, **query): """Retrieve a generator of volume_target. @@ -1568,6 +1589,8 @@ def delete_volume_target(self, volume_target, ignore_missing=True): ignore_missing=ignore_missing, ) + # ========== Deploy templates ========== + def deploy_templates(self, details=False, **query): """Retrieve a generator of deploy_templates. @@ -1678,6 +1701,8 @@ def patch_deploy_template(self, deploy_template, patch): _deploytemplates.DeployTemplate, deploy_template ).patch(self, patch) + # ========== Conductors ========== + def conductors(self, details=False, **query): """Retrieve a generator of conductors. @@ -1705,3 +1730,66 @@ def get_conductor(self, conductor, fields=None): return self._get_with_fields( _conductor.Conductor, conductor, fields=fields ) + + # ========== Utilities ========== + + def wait_for_status( + self, + res: resource.ResourceT, + status: str, + failures: ty.Optional[list[str]] = None, + interval: ty.Union[int, float, None] = 2, + wait: ty.Optional[int] = None, + attribute: str = 'status', + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for the resource to be in a particular status. + + :param session: The session to use for making this request. + :param resource: The resource to wait on to reach the status. The + resource must have a status attribute specified via ``attribute``. + :param status: Desired status of the resource. + :param failures: Statuses that would indicate the transition + failed such as 'ERROR'. Defaults to ['ERROR']. + :param interval: Number of seconds to wait between checks. + :param wait: Maximum number of seconds to wait for transition. + Set to ``None`` to wait forever. + :param attribute: Name of the resource attribute that contains the + status. + :param callback: A callback function. This will be called with a single + value, progress. This is API specific but is generally a percentage + value from 0-100. + + :return: The updated resource. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if the + transition to status failed to occur in ``wait`` seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + transitioned to one of the states in ``failures``. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute + """ + return resource.wait_for_status( + self, res, status, failures, interval, wait, attribute, callback + ) + + def wait_for_delete( + self, + res: resource.ResourceT, + interval: int = 2, + wait: int = 120, + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for a resource to be deleted. + + :param res: The resource to wait on to be deleted. + :param interval: Number of seconds to wait before to consecutive + checks. + :param wait: Maximum number of seconds to wait before the change. + :param callback: A callback function. This will be called with a single + value, progress, which is a percentage value from 0-100. + + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete(self, res, interval, wait, callback) diff --git a/openstack/baremetal_introspection/v1/_proxy.py b/openstack/baremetal_introspection/v1/_proxy.py index eca563c7c..31b99fa09 100644 --- a/openstack/baremetal_introspection/v1/_proxy.py +++ b/openstack/baremetal_introspection/v1/_proxy.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + from openstack import _log from openstack.baremetal.v1 import node as _node from openstack.baremetal_introspection.v1 import introspection as _introspect @@ -18,6 +20,7 @@ ) from openstack import exceptions from openstack import proxy +from openstack import resource _logger = _log.setup_logging('openstack') @@ -29,6 +32,8 @@ class Proxy(proxy.Proxy): "introspection_rule": _introspection_rule.IntrospectionRule, } + # ========== Introspections ========== + def introspections(self, **query): """Retrieve a generator of introspection records. @@ -155,6 +160,8 @@ def wait_for_introspection( res = self._get_resource(_introspect.Introspection, introspection) return res.wait(self, timeout=timeout, ignore_error=ignore_error) + # ========== Introspection ruless ========== + def create_introspection_rule(self, **attrs): """Create a new introspection rules from attributes. @@ -227,3 +234,66 @@ def introspection_rules(self, **query): objects """ return self._list(_introspection_rule.IntrospectionRule, **query) + + # ========== Utilities ========== + + def wait_for_status( + self, + res: resource.ResourceT, + status: str, + failures: ty.Optional[list[str]] = None, + interval: ty.Union[int, float, None] = 2, + wait: ty.Optional[int] = None, + attribute: str = 'status', + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for the resource to be in a particular status. + + :param session: The session to use for making this request. + :param resource: The resource to wait on to reach the status. The + resource must have a status attribute specified via ``attribute``. + :param status: Desired status of the resource. + :param failures: Statuses that would indicate the transition + failed such as 'ERROR'. Defaults to ['ERROR']. + :param interval: Number of seconds to wait between checks. + :param wait: Maximum number of seconds to wait for transition. + Set to ``None`` to wait forever. + :param attribute: Name of the resource attribute that contains the + status. + :param callback: A callback function. This will be called with a single + value, progress. This is API specific but is generally a percentage + value from 0-100. + + :return: The updated resource. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if the + transition to status failed to occur in ``wait`` seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + transitioned to one of the states in ``failures``. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute + """ + return resource.wait_for_status( + self, res, status, failures, interval, wait, attribute, callback + ) + + def wait_for_delete( + self, + res: resource.ResourceT, + interval: int = 2, + wait: int = 120, + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for a resource to be deleted. + + :param res: The resource to wait on to be deleted. + :param interval: Number of seconds to wait before to consecutive + checks. + :param wait: Maximum number of seconds to wait before the change. + :param callback: A callback function. This will be called with a single + value, progress, which is a percentage value from 0-100. + + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete(self, res, interval, wait, callback) diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index f2d41fd11..8bba0b935 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty import warnings from openstack.block_storage import _base_proxy @@ -29,7 +30,18 @@ class Proxy(_base_proxy.BaseBlockStorageProxy): - # ====== SNAPSHOTS ====== + # ========== Extensions ========== + + def extensions(self): + """Return a generator of extensions + + :returns: A generator of extension + :rtype: :class:`~openstack.block_storage.v2.extension.Extension` + """ + return self._list(_extension.Extension) + + # ========== Snapshots ========== + def get_snapshot(self, snapshot): """Get a single snapshot @@ -142,7 +154,8 @@ def delete_snapshot(self, snapshot, ignore_missing=True): _snapshot.Snapshot, snapshot, ignore_missing=ignore_missing ) - # ====== SNAPSHOT ACTIONS ====== + # ========== Snapshot actions ========== + def reset_snapshot(self, snapshot, status): """Reset status of the snapshot @@ -155,7 +168,8 @@ def reset_snapshot(self, snapshot, status): snapshot = self._get_resource(_snapshot.Snapshot, snapshot) snapshot.reset(self, status) - # ====== TYPES ====== + # ========== Types ========== + def get_type(self, type): """Get a single type @@ -260,7 +274,8 @@ def remove_type_access(self, type, project_id): res = self._get_resource(_type.Type, type) return res.remove_private_access(self, project_id) - # ====== VOLUMES ====== + # ========== Volumes ========== + def get_volume(self, volume): """Get a single volume @@ -367,7 +382,8 @@ def delete_volume(self, volume, ignore_missing=True, force=False): volume = self._get_resource(_volume.Volume, volume) volume.force_delete(self) - # ====== VOLUME ACTIONS ====== + # ========== Volume actions ========== + def extend_volume(self, volume, size): """Extend a volume @@ -548,7 +564,8 @@ def complete_volume_migration(self, volume, new_volume, error=False): volume = self._get_resource(_volume.Volume, volume) volume.complete_migration(self, new_volume, error) - # ====== BACKEND POOLS ====== + # ========== Backend pools ========== + def backend_pools(self, **query): """Returns a generator of cinder Back-end storage pools @@ -559,7 +576,8 @@ def backend_pools(self, **query): """ return self._list(_stats.Pools, **query) - # ====== BACKUPS ====== + # ========== Backups ========== + def backups(self, details=True, **query): """Retrieve a generator of backups @@ -654,7 +672,8 @@ def delete_backup(self, backup, ignore_missing=True, force=False): backup = self._get_resource(_backup.Backup, backup) backup.force_delete(self) - # ====== BACKUP ACTIONS ====== + # ========== Backup actions ========== + def restore_backup(self, backup, volume_id, name): """Restore a Backup to volume @@ -681,7 +700,8 @@ def reset_backup(self, backup, status): backup = self._get_resource(_backup.Backup, backup) backup.reset(self, status) - # ====== LIMITS ====== + # ========== Limits ========== + def get_limits(self, project=None): """Retrieves limits @@ -698,7 +718,8 @@ def get_limits(self, project=None): params['project_id'] = resource.Resource._get_id(project) return self._get(_limits.Limits, requires_id=False, **params) - # ====== CAPABILITIES ====== + # ========== Capabilities ========== + def get_capabilities(self, host): """Get a backend's capabilites @@ -711,7 +732,8 @@ def get_capabilities(self, host): """ return self._get(_capabilities.Capabilities, host) - # ====== QUOTA CLASS SETS ====== + # ========== Quota class sets ========== + def get_quota_class_set(self, quota_class_set='default'): """Get a single quota class set @@ -748,7 +770,8 @@ def update_quota_class_set(self, quota_class_set, **attrs): _quota_class_set.QuotaClassSet, quota_class_set, **attrs ) - # ====== QUOTA SETS ====== + # ========== Quota sets ========== + def get_quota_set(self, project, usage=False, **query): """Show QuotaSet information for the project @@ -841,7 +864,8 @@ def update_quota_set(self, project, **attrs): attrs['project_id'] = project.id return self._update(_quota_set.QuotaSet, None, **attrs) - # ====== VOLUME METADATA ====== + # ========== Volume metadata ========== + def get_volume_metadata(self, volume): """Return a dictionary of metadata for a volume @@ -888,7 +912,8 @@ def delete_volume_metadata(self, volume, keys=None): else: volume.delete_metadata(self) - # ====== SNAPSHOT METADATA ====== + # ========== Snapshot metadata ========== + def get_snapshot_metadata(self, snapshot): """Return a dictionary of metadata for a snapshot @@ -937,79 +962,68 @@ def delete_snapshot_metadata(self, snapshot, keys=None): else: snapshot.delete_metadata(self) - # ====== EXTENSIONS ====== - def extensions(self): - """Return a generator of extensions + # ========== Utilities ========== - :returns: A generator of extension - :rtype: :class:`~openstack.block_storage.v2.extension.Extension` - """ - return self._list(_extension.Extension) - - # ====== UTILS ====== def wait_for_status( self, - res, - status='available', - failures=None, - interval=2, - wait=120, - callback=None, - ): - """Wait for a resource to be in a particular status. - - :param res: The resource to wait on to reach the specified status. - The resource must have a ``status`` attribute. - :type resource: A :class:`~openstack.resource.Resource` object. - :param status: Desired status. - :param failures: Statuses that would be interpreted as failures. - :type failures: :py:class:`list` - :param interval: Number of seconds to wait before to consecutive - checks. Default to 2. - :param wait: Maximum number of seconds to wait before the change. - Default to 120. + res: resource.ResourceT, + status: str = 'available', + failures: ty.Optional[list[str]] = None, + interval: ty.Union[int, float, None] = 2, + wait: ty.Optional[int] = None, + attribute: str = 'status', + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for the resource to be in a particular status. + + :param session: The session to use for making this request. + :param resource: The resource to wait on to reach the status. The + resource must have a status attribute specified via ``attribute``. + :param status: Desired status of the resource. + :param failures: Statuses that would indicate the transition + failed such as 'ERROR'. Defaults to ['ERROR']. + :param interval: Number of seconds to wait between checks. + :param wait: Maximum number of seconds to wait for transition. + Set to ``None`` to wait forever. + :param attribute: Name of the resource attribute that contains the + status. :param callback: A callback function. This will be called with a single - value, progress. + value, progress. This is API specific but is generally a percentage + value from 0-100. - :returns: The resource is returned on success. - :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition - to the desired status failed to occur in specified seconds. + :return: The updated resource. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if the + transition to status failed to occur in ``wait`` seconds. :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource - has transited to one of the failure statuses. + transitioned to one of the states in ``failures``. :raises: :class:`~AttributeError` if the resource does not have a - ``status`` attribute. + ``status`` attribute """ - failures = ['error'] if failures is None else failures + if failures is None: + failures = ['error'] + return resource.wait_for_status( - self, - res, - status, - failures, - interval, - wait, - callback=callback, + self, res, status, failures, interval, wait, attribute, callback ) - def wait_for_delete(self, res, interval=2, wait=120, callback=None): + def wait_for_delete( + self, + res: resource.ResourceT, + interval: int = 2, + wait: int = 120, + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: """Wait for a resource to be deleted. :param res: The resource to wait on to be deleted. - :type resource: A :class:`~openstack.resource.Resource` object. :param interval: Number of seconds to wait before to consecutive - checks. Default to 2. + checks. :param wait: Maximum number of seconds to wait before the change. - Default to 120. :param callback: A callback function. This will be called with a single - value, progress. + value, progress, which is a percentage value from 0-100. :returns: The resource is returned on success. :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition to delete failed to occur in the specified seconds. """ - return resource.wait_for_delete( - self, - res, - interval, - wait, - callback=callback, - ) + return resource.wait_for_delete(self, res, interval, wait, callback) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 28224c13f..f6f0cbee5 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -2192,69 +2192,67 @@ def accept_transfer(self, transfer_id, auth_key): # ====== UTILS ====== def wait_for_status( self, - res, - status='available', - failures=None, - interval=2, - wait=120, - callback=None, - ): - """Wait for a resource to be in a particular status. - - :param res: The resource to wait on to reach the specified status. - The resource must have a ``status`` attribute. - :type resource: A :class:`~openstack.resource.Resource` object. - :param str status: Desired status. - :param list failures: Statuses that would be interpreted as failures. - :param interval: Number of seconds to wait before to consecutive - checks. Default to 2. - :param wait: Maximum number of seconds to wait before the change. - Default to 120. + res: resource.ResourceT, + status: str = 'available', + failures: ty.Optional[list[str]] = None, + interval: ty.Union[int, float, None] = 2, + wait: ty.Optional[int] = None, + attribute: str = 'status', + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for the resource to be in a particular status. + + :param session: The session to use for making this request. + :param resource: The resource to wait on to reach the status. The + resource must have a status attribute specified via ``attribute``. + :param status: Desired status of the resource. + :param failures: Statuses that would indicate the transition + failed such as 'ERROR'. Defaults to ['ERROR']. + :param interval: Number of seconds to wait between checks. + :param wait: Maximum number of seconds to wait for transition. + Set to ``None`` to wait forever. + :param attribute: Name of the resource attribute that contains the + status. :param callback: A callback function. This will be called with a single - value, progress. + value, progress. This is API specific but is generally a percentage + value from 0-100. - :returns: The resource is returned on success. - :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition - to the desired status failed to occur in specified seconds. + :return: The updated resource. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if the + transition to status failed to occur in ``wait`` seconds. :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource - has transited to one of the failure statuses. + transitioned to one of the states in ``failures``. :raises: :class:`~AttributeError` if the resource does not have a - ``status`` attribute. + ``status`` attribute """ - failures = ['error'] if failures is None else failures + if failures is None: + failures = ['error'] + return resource.wait_for_status( - self, - res, - status, - failures, - interval, - wait, - callback=callback, + self, res, status, failures, interval, wait, attribute, callback ) - def wait_for_delete(self, res, interval=2, wait=120, callback=None): + def wait_for_delete( + self, + res: resource.ResourceT, + interval: int = 2, + wait: int = 120, + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: """Wait for a resource to be deleted. :param res: The resource to wait on to be deleted. - :type resource: A :class:`~openstack.resource.Resource` object. - :param int interval: Number of seconds to wait before two consecutive - checks. Default to 2. - :param int wait: Maximum number of seconds to wait before the change. - Default to 120. + :param interval: Number of seconds to wait before to consecutive + checks. + :param wait: Maximum number of seconds to wait before the change. :param callback: A callback function. This will be called with a single - value, progress. + value, progress, which is a percentage value from 0-100. :returns: The resource is returned on success. :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition to delete failed to occur in the specified seconds. """ - return resource.wait_for_delete( - self, - res, - interval, - wait, - callback=callback, - ) + return resource.wait_for_delete(self, res, interval, wait, callback) def _get_cleanup_dependencies(self): return {'block_storage': {'before': []}} diff --git a/openstack/clustering/v1/_proxy.py b/openstack/clustering/v1/_proxy.py index f63b95934..a46d87f92 100644 --- a/openstack/clustering/v1/_proxy.py +++ b/openstack/clustering/v1/_proxy.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + from openstack.clustering.v1 import action as _action from openstack.clustering.v1 import build_info from openstack.clustering.v1 import cluster as _cluster @@ -1046,49 +1048,6 @@ def events(self, **query): """ return self._list(_event.Event, **query) - def wait_for_status( - self, res, status, failures=None, interval=2, wait=120 - ): - """Wait for a resource to be in a particular status. - - :param res: The resource to wait on to reach the specified status. - The resource must have a ``status`` attribute. - :type resource: A :class:`~openstack.resource.Resource` object. - :param status: Desired status. - :param failures: Statuses that would be interpreted as failures. - :type failures: :py:class:`list` - :param interval: Number of seconds to wait before to consecutive - checks. Default to 2. - :param wait: Maximum number of seconds to wait before the change. - Default to 120. - :returns: The resource is returned on success. - :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition - to the desired status failed to occur in specified seconds. - :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource - has transited to one of the failure statuses. - :raises: :class:`~AttributeError` if the resource does not have a - ``status`` attribute. - """ - failures = [] if failures is None else failures - return resource.wait_for_status( - self, res, status, failures, interval, wait - ) - - def wait_for_delete(self, res, interval=2, wait=120): - """Wait for a resource to be deleted. - - :param res: The resource to wait on to be deleted. - :type resource: A :class:`~openstack.resource.Resource` object. - :param interval: Number of seconds to wait before to consecutive - checks. Default to 2. - :param wait: Maximum number of seconds to wait before the change. - Default to 120. - :returns: The resource is returned on success. - :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition - to delete failed to occur in the specified seconds. - """ - return resource.wait_for_delete(self, res, interval, wait) - def services(self, **query): """Get a generator of services. @@ -1111,3 +1070,66 @@ def list_profile_type_operations(self, profile_type): """ obj = self._get_resource(_profile_type.ProfileType, profile_type) return obj.type_ops(self) + + # ========== Utilities ========== + + def wait_for_status( + self, + res: resource.ResourceT, + status: str, + failures: ty.Optional[list[str]] = None, + interval: ty.Union[int, float, None] = 2, + wait: ty.Optional[int] = None, + attribute: str = 'status', + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for the resource to be in a particular status. + + :param session: The session to use for making this request. + :param resource: The resource to wait on to reach the status. The + resource must have a status attribute specified via ``attribute``. + :param status: Desired status of the resource. + :param failures: Statuses that would indicate the transition + failed such as 'ERROR'. Defaults to ['ERROR']. + :param interval: Number of seconds to wait between checks. + :param wait: Maximum number of seconds to wait for transition. + Set to ``None`` to wait forever. + :param attribute: Name of the resource attribute that contains the + status. + :param callback: A callback function. This will be called with a single + value, progress. This is API specific but is generally a percentage + value from 0-100. + + :return: The updated resource. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if the + transition to status failed to occur in ``wait`` seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + transitioned to one of the states in ``failures``. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute + """ + return resource.wait_for_status( + self, res, status, failures, interval, wait, attribute, callback + ) + + def wait_for_delete( + self, + res: resource.ResourceT, + interval: int = 2, + wait: int = 120, + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for a resource to be deleted. + + :param res: The resource to wait on to be deleted. + :param interval: Number of seconds to wait before to consecutive + checks. + :param wait: Maximum number of seconds to wait before the change. + :param callback: A callback function. This will be called with a single + value, progress, which is a percentage value from 0-100. + + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete(self, res, interval, wait, callback) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 85d2362fa..18c403cf0 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty import warnings from openstack.block_storage.v3 import volume as _volume @@ -2630,13 +2631,13 @@ def server_actions(self, server): def wait_for_server( self, - server, - status='ACTIVE', - failures=None, - interval=2, - wait=120, - callback=None, - ): + server: _server.Server, + status: str = 'ACTIVE', + failures: ty.Optional[list[str]] = None, + interval: ty.Union[int, float, None] = 2, + wait: ty.Optional[int] = 120, + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> _server.Server: """Wait for a server to be in a particular status. :param server: The :class:`~openstack.compute.v2.server.Server` to wait @@ -2651,7 +2652,6 @@ def wait_for_server( :type interval: int :param wait: Maximum number of seconds to wait before the change. Default to 120. - :type wait: int :param callback: A callback function. This will be called with a single value, progress, which is a percentage value from 0-100. :type callback: callable @@ -2675,15 +2675,58 @@ def wait_for_server( callback=callback, ) - def wait_for_delete(self, res, interval=2, wait=120, callback=None): + def wait_for_status( + self, + res: resource.ResourceT, + status: str, + failures: ty.Optional[list[str]] = None, + interval: ty.Union[int, float, None] = 2, + wait: ty.Optional[int] = None, + attribute: str = 'status', + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for the resource to be in a particular status. + + :param session: The session to use for making this request. + :param resource: The resource to wait on to reach the status. The + resource must have a status attribute specified via ``attribute``. + :param status: Desired status of the resource. + :param failures: Statuses that would indicate the transition + failed such as 'ERROR'. Defaults to ['ERROR']. + :param interval: Number of seconds to wait between checks. + :param wait: Maximum number of seconds to wait for transition. + Set to ``None`` to wait forever. + :param attribute: Name of the resource attribute that contains the + status. + :param callback: A callback function. This will be called with a single + value, progress. This is API specific but is generally a percentage + value from 0-100. + + :return: The updated resource. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if the + transition to status failed to occur in ``wait`` seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + transitioned to one of the states in ``failures``. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute + """ + return resource.wait_for_status( + self, res, status, failures, interval, wait, attribute, callback + ) + + def wait_for_delete( + self, + res: resource.ResourceT, + interval: int = 2, + wait: int = 120, + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: """Wait for a resource to be deleted. :param res: The resource to wait on to be deleted. - :type resource: A :class:`~openstack.resource.Resource` object. :param interval: Number of seconds to wait before to consecutive - checks. Default to 2. + checks. :param wait: Maximum number of seconds to wait before the change. - Default to 120. :param callback: A callback function. This will be called with a single value, progress, which is a percentage value from 0-100. diff --git a/openstack/container_infrastructure_management/v1/_proxy.py b/openstack/container_infrastructure_management/v1/_proxy.py index e11db1398..fb9f23a75 100644 --- a/openstack/container_infrastructure_management/v1/_proxy.py +++ b/openstack/container_infrastructure_management/v1/_proxy.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + from openstack.container_infrastructure_management.v1 import ( cluster as _cluster, ) @@ -23,6 +25,7 @@ service as _service, ) from openstack import proxy +from openstack import resource class Proxy(proxy.Proxy): @@ -32,6 +35,8 @@ class Proxy(proxy.Proxy): "service": _service.Service, } + # ========== Clusters ========== + def create_cluster(self, **attrs): """Create a new cluster from attributes @@ -51,9 +56,9 @@ def delete_cluster(self, cluster, ignore_missing=True): :class:`~openstack.container_infrastructure_management.v1.cluster.Cluster` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the cluster does not exist. When set to ``True``, no exception will - be set when attempting to delete a nonexistent cluster. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the cluster does not exist. When set to ``True``, no exception + will be set when attempting to delete a nonexistent cluster. :returns: ``None`` """ self._delete(_cluster.Cluster, cluster, ignore_missing=ignore_missing) @@ -119,6 +124,7 @@ def update_cluster(self, cluster, **attrs): return self._update(_cluster.Cluster, cluster, **attrs) # ============== Cluster Templates ============== + def create_cluster_template(self, **attrs): """Create a new cluster_template from attributes @@ -139,8 +145,8 @@ def delete_cluster_template(self, cluster_template, ignore_missing=True): :class:`~openstack.container_infrastructure_management.v1.cluster_template.ClusterTemplate` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the cluster_template does not exist. When set to ``True``, no + :class:`~openstack.exceptions.NotFoundException` will be raised + when the cluster_template does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent cluster_template. :returns: ``None`` @@ -215,6 +221,7 @@ def update_cluster_template(self, cluster_template, **attrs): ) # ============== Cluster Certificates ============== + def create_cluster_certificate(self, **attrs): """Create a new cluster_certificate from CSR @@ -243,6 +250,7 @@ def get_cluster_certificate(self, cluster_certificate): return self._get(_cluster_cert.ClusterCertificate, cluster_certificate) # ============== Services ============== + def services(self): """Return a generator of services @@ -251,3 +259,66 @@ def services(self): :class:`~openstack.container_infrastructure_management.v1.service.Service` """ return self._list(_service.Service) + + # ========== Utilities ========== + + def wait_for_status( + self, + res: resource.ResourceT, + status: str, + failures: ty.Optional[list[str]] = None, + interval: ty.Union[int, float, None] = 2, + wait: ty.Optional[int] = None, + attribute: str = 'status', + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for the resource to be in a particular status. + + :param session: The session to use for making this request. + :param resource: The resource to wait on to reach the status. The + resource must have a status attribute specified via ``attribute``. + :param status: Desired status of the resource. + :param failures: Statuses that would indicate the transition + failed such as 'ERROR'. Defaults to ['ERROR']. + :param interval: Number of seconds to wait between checks. + :param wait: Maximum number of seconds to wait for transition. + Set to ``None`` to wait forever. + :param attribute: Name of the resource attribute that contains the + status. + :param callback: A callback function. This will be called with a single + value, progress. This is API specific but is generally a percentage + value from 0-100. + + :return: The updated resource. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if the + transition to status failed to occur in ``wait`` seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + transitioned to one of the states in ``failures``. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute + """ + return resource.wait_for_status( + self, res, status, failures, interval, wait, attribute, callback + ) + + def wait_for_delete( + self, + res: resource.ResourceT, + interval: int = 2, + wait: int = 120, + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for a resource to be deleted. + + :param res: The resource to wait on to be deleted. + :param interval: Number of seconds to wait before to consecutive + checks. + :param wait: Maximum number of seconds to wait before the change. + :param callback: A callback function. This will be called with a single + value, progress, which is a percentage value from 0-100. + + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete(self, res, interval, wait, callback) diff --git a/openstack/database/v1/_proxy.py b/openstack/database/v1/_proxy.py index 6a4d88145..a601e2f07 100644 --- a/openstack/database/v1/_proxy.py +++ b/openstack/database/v1/_proxy.py @@ -10,11 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + from openstack.database.v1 import database as _database from openstack.database.v1 import flavor as _flavor from openstack.database.v1 import instance as _instance from openstack.database.v1 import user as _user from openstack import proxy +from openstack import resource class Proxy(proxy.Proxy): @@ -332,3 +335,66 @@ def get_user(self, user, instance=None): """ instance = self._get_resource(_instance.Instance, instance) return self._get(_user.User, user) + + # ========== Utilities ========== + + def wait_for_status( + self, + res: resource.ResourceT, + status: str, + failures: ty.Optional[list[str]] = None, + interval: ty.Union[int, float, None] = 2, + wait: ty.Optional[int] = None, + attribute: str = 'status', + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for the resource to be in a particular status. + + :param session: The session to use for making this request. + :param resource: The resource to wait on to reach the status. The + resource must have a status attribute specified via ``attribute``. + :param status: Desired status of the resource. + :param failures: Statuses that would indicate the transition + failed such as 'ERROR'. Defaults to ['ERROR']. + :param interval: Number of seconds to wait between checks. + :param wait: Maximum number of seconds to wait for transition. + Set to ``None`` to wait forever. + :param attribute: Name of the resource attribute that contains the + status. + :param callback: A callback function. This will be called with a single + value, progress. This is API specific but is generally a percentage + value from 0-100. + + :return: The updated resource. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if the + transition to status failed to occur in ``wait`` seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + transitioned to one of the states in ``failures``. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute + """ + return resource.wait_for_status( + self, res, status, failures, interval, wait, attribute, callback + ) + + def wait_for_delete( + self, + res: resource.ResourceT, + interval: int = 2, + wait: int = 120, + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for a resource to be deleted. + + :param res: The resource to wait on to be deleted. + :param interval: Number of seconds to wait before to consecutive + checks. + :param wait: Maximum number of seconds to wait before the change. + :param callback: A callback function. This will be called with a single + value, progress, which is a percentage value from 0-100. + + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete(self, res, interval, wait, callback) diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py index 523426364..2ace1a047 100644 --- a/openstack/dns/v2/_proxy.py +++ b/openstack/dns/v2/_proxy.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + from openstack.dns.v2 import floating_ip as _fip from openstack.dns.v2 import limit as _limit from openstack.dns.v2 import recordset as _rs @@ -21,6 +23,7 @@ from openstack.dns.v2 import zone_share as _zone_share from openstack.dns.v2 import zone_transfer as _zone_transfer from openstack import proxy +from openstack import resource class Proxy(proxy.Proxy): @@ -691,6 +694,68 @@ def get_service_status(self, service): """ return self._get(_svc_status.ServiceStatus, service) + # ========== Utilities ========== + def wait_for_status( + self, + res: resource.ResourceT, + status: str, + failures: ty.Optional[list[str]] = None, + interval: ty.Union[int, float, None] = 2, + wait: ty.Optional[int] = None, + attribute: str = 'status', + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for the resource to be in a particular status. + + :param session: The session to use for making this request. + :param resource: The resource to wait on to reach the status. The + resource must have a status attribute specified via ``attribute``. + :param status: Desired status of the resource. + :param failures: Statuses that would indicate the transition + failed such as 'ERROR'. Defaults to ['ERROR']. + :param interval: Number of seconds to wait between checks. + :param wait: Maximum number of seconds to wait for transition. + Set to ``None`` to wait forever. + :param attribute: Name of the resource attribute that contains the + status. + :param callback: A callback function. This will be called with a single + value, progress. This is API specific but is generally a percentage + value from 0-100. + + :return: The updated resource. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if the + transition to status failed to occur in ``wait`` seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + transitioned to one of the states in ``failures``. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute + """ + return resource.wait_for_status( + self, res, status, failures, interval, wait, attribute, callback + ) + + def wait_for_delete( + self, + res: resource.ResourceT, + interval: int = 2, + wait: int = 120, + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for a resource to be deleted. + + :param res: The resource to wait on to be deleted. + :param interval: Number of seconds to wait before to consecutive + checks. + :param wait: Maximum number of seconds to wait before the change. + :param callback: A callback function. This will be called with a single + value, progress, which is a percentage value from 0-100. + + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete(self, res, interval, wait, callback) + def _get_cleanup_dependencies(self): # DNS may depend on floating ip return {'dns': {'before': ['network']}} diff --git a/openstack/identity/v2/_proxy.py b/openstack/identity/v2/_proxy.py index 141279f00..d6282799f 100644 --- a/openstack/identity/v2/_proxy.py +++ b/openstack/identity/v2/_proxy.py @@ -10,11 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + from openstack.identity.v2 import extension as _extension from openstack.identity.v2 import role as _role from openstack.identity.v2 import tenant as _tenant from openstack.identity.v2 import user as _user from openstack import proxy +from openstack import resource class Proxy(proxy.Proxy): @@ -272,3 +275,66 @@ def update_user(self, user, **attrs): :rtype: :class:`~openstack.identity.v2.user.User` """ return self._update(_user.User, user, **attrs) + + # ========== Utilities ========== + + def wait_for_status( + self, + res: resource.ResourceT, + status: str, + failures: ty.Optional[list[str]] = None, + interval: ty.Union[int, float, None] = 2, + wait: ty.Optional[int] = None, + attribute: str = 'status', + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for the resource to be in a particular status. + + :param session: The session to use for making this request. + :param resource: The resource to wait on to reach the status. The + resource must have a status attribute specified via ``attribute``. + :param status: Desired status of the resource. + :param failures: Statuses that would indicate the transition + failed such as 'ERROR'. Defaults to ['ERROR']. + :param interval: Number of seconds to wait between checks. + :param wait: Maximum number of seconds to wait for transition. + Set to ``None`` to wait forever. + :param attribute: Name of the resource attribute that contains the + status. + :param callback: A callback function. This will be called with a single + value, progress. This is API specific but is generally a percentage + value from 0-100. + + :return: The updated resource. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if the + transition to status failed to occur in ``wait`` seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + transitioned to one of the states in ``failures``. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute + """ + return resource.wait_for_status( + self, res, status, failures, interval, wait, attribute, callback + ) + + def wait_for_delete( + self, + res: resource.ResourceT, + interval: int = 2, + wait: int = 120, + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for a resource to be deleted. + + :param res: The resource to wait on to be deleted. + :param interval: Number of seconds to wait before to consecutive + checks. + :param wait: Maximum number of seconds to wait before the change. + :param callback: A callback function. This will be called with a single + value, progress, which is a percentage value from 0-100. + + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete(self, res, interval, wait, callback) diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 45cbaa042..26a2e5347 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + import openstack.exceptions as exception from openstack.identity.v3 import ( application_credential as _application_credential, @@ -2372,3 +2374,66 @@ def update_service_provider(self, service_provider, **attrs): return self._update( _service_provider.ServiceProvider, service_provider, **attrs ) + + # ========== Utilities ========== + + def wait_for_status( + self, + res: resource.ResourceT, + status: str, + failures: ty.Optional[list[str]] = None, + interval: ty.Union[int, float, None] = 2, + wait: ty.Optional[int] = None, + attribute: str = 'status', + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for the resource to be in a particular status. + + :param session: The session to use for making this request. + :param resource: The resource to wait on to reach the status. The + resource must have a status attribute specified via ``attribute``. + :param status: Desired status of the resource. + :param failures: Statuses that would indicate the transition + failed such as 'ERROR'. Defaults to ['ERROR']. + :param interval: Number of seconds to wait between checks. + :param wait: Maximum number of seconds to wait for transition. + Set to ``None`` to wait forever. + :param attribute: Name of the resource attribute that contains the + status. + :param callback: A callback function. This will be called with a single + value, progress. This is API specific but is generally a percentage + value from 0-100. + + :return: The updated resource. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if the + transition to status failed to occur in ``wait`` seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + transitioned to one of the states in ``failures``. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute + """ + return resource.wait_for_status( + self, res, status, failures, interval, wait, attribute, callback + ) + + def wait_for_delete( + self, + res: resource.ResourceT, + interval: int = 2, + wait: int = 120, + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for a resource to be deleted. + + :param res: The resource to wait on to be deleted. + :param interval: Number of seconds to wait before to consecutive + checks. + :param wait: Maximum number of seconds to wait before the change. + :param callback: A callback function. This will be called with a single + value, progress, which is a percentage value from 0-100. + + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete(self, res, interval, wait, callback) diff --git a/openstack/image/v1/_proxy.py b/openstack/image/v1/_proxy.py index 5419f8c11..bbe96b8f0 100644 --- a/openstack/image/v1/_proxy.py +++ b/openstack/image/v1/_proxy.py @@ -11,11 +11,13 @@ # under the License. import os +import typing as ty import warnings from openstack import exceptions as exc from openstack.image.v1 import image as _image from openstack import proxy +from openstack import resource from openstack import utils from openstack import warnings as os_warnings @@ -473,3 +475,66 @@ def update_image_properties( img_props[k] = v return self._update_image_properties(image, meta, img_props) + + # ========== Utilities ========== + + def wait_for_status( + self, + res: resource.ResourceT, + status: str, + failures: ty.Optional[list[str]] = None, + interval: ty.Union[int, float, None] = 2, + wait: ty.Optional[int] = None, + attribute: str = 'status', + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for the resource to be in a particular status. + + :param session: The session to use for making this request. + :param resource: The resource to wait on to reach the status. The + resource must have a status attribute specified via ``attribute``. + :param status: Desired status of the resource. + :param failures: Statuses that would indicate the transition + failed such as 'ERROR'. Defaults to ['ERROR']. + :param interval: Number of seconds to wait between checks. + :param wait: Maximum number of seconds to wait for transition. + Set to ``None`` to wait forever. + :param attribute: Name of the resource attribute that contains the + status. + :param callback: A callback function. This will be called with a single + value, progress. This is API specific but is generally a percentage + value from 0-100. + + :return: The updated resource. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if the + transition to status failed to occur in ``wait`` seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + transitioned to one of the states in ``failures``. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute + """ + return resource.wait_for_status( + self, res, status, failures, interval, wait, attribute, callback + ) + + def wait_for_delete( + self, + res: resource.ResourceT, + interval: int = 2, + wait: int = 120, + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for a resource to be deleted. + + :param res: The resource to wait on to be deleted. + :param interval: Number of seconds to wait before to consecutive + checks. + :param wait: Maximum number of seconds to wait before the change. + :param callback: A callback function. This will be called with a single + value, progress, which is a percentage value from 0-100. + + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete(self, res, interval, wait, callback) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index bc2e4559b..b2de555bd 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -12,6 +12,7 @@ import os import time +import typing as ty import warnings from openstack import exceptions @@ -1891,21 +1892,68 @@ def get_import_info(self): """ return self._get(_si.Import, requires_id=False) - # ====== UTILS ====== - def wait_for_delete(self, res, interval=2, wait=120): + # ========== Utilities ========== + + def wait_for_status( + self, + res: resource.ResourceT, + status: str, + failures: ty.Optional[list[str]] = None, + interval: ty.Union[int, float, None] = 2, + wait: ty.Optional[int] = None, + attribute: str = 'status', + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for the resource to be in a particular status. + + :param session: The session to use for making this request. + :param resource: The resource to wait on to reach the status. The + resource must have a status attribute specified via ``attribute``. + :param status: Desired status of the resource. + :param failures: Statuses that would indicate the transition + failed such as 'ERROR'. Defaults to ['ERROR']. + :param interval: Number of seconds to wait between checks. + :param wait: Maximum number of seconds to wait for transition. + Set to ``None`` to wait forever. + :param attribute: Name of the resource attribute that contains the + status. + :param callback: A callback function. This will be called with a single + value, progress. This is API specific but is generally a percentage + value from 0-100. + + :return: The updated resource. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if the + transition to status failed to occur in ``wait`` seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + transitioned to one of the states in ``failures``. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute + """ + return resource.wait_for_status( + self, res, status, failures, interval, wait, attribute, callback + ) + + def wait_for_delete( + self, + res: resource.ResourceT, + interval: int = 2, + wait: int = 120, + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: """Wait for a resource to be deleted. :param res: The resource to wait on to be deleted. - :type resource: A :class:`~openstack.resource.Resource` object. :param interval: Number of seconds to wait before to consecutive - checks. Default to 2. + checks. :param wait: Maximum number of seconds to wait before the change. - Default to 120. + :param callback: A callback function. This will be called with a single + value, progress, which is a percentage value from 0-100. + :returns: The resource is returned on success. :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition to delete failed to occur in the specified seconds. """ - return resource.wait_for_delete(self, res, interval, wait) + return resource.wait_for_delete(self, res, interval, wait, callback) def _get_cleanup_dependencies(self): return {'image': {'before': ['identity']}} diff --git a/openstack/instance_ha/v1/_proxy.py b/openstack/instance_ha/v1/_proxy.py index 2a073709e..b98b63cf3 100644 --- a/openstack/instance_ha/v1/_proxy.py +++ b/openstack/instance_ha/v1/_proxy.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import typing as ty + from openstack import exceptions from openstack.instance_ha.v1 import host as _host from openstack.instance_ha.v1 import notification as _notification @@ -258,3 +260,66 @@ def get_vmove(self, vmove, notification): vmove_id, notification_id=notification_id, ) + + # ========== Utilities ========== + + def wait_for_status( + self, + res: resource.ResourceT, + status: str, + failures: ty.Optional[list[str]] = None, + interval: ty.Union[int, float, None] = 2, + wait: ty.Optional[int] = None, + attribute: str = 'status', + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for the resource to be in a particular status. + + :param session: The session to use for making this request. + :param resource: The resource to wait on to reach the status. The + resource must have a status attribute specified via ``attribute``. + :param status: Desired status of the resource. + :param failures: Statuses that would indicate the transition + failed such as 'ERROR'. Defaults to ['ERROR']. + :param interval: Number of seconds to wait between checks. + :param wait: Maximum number of seconds to wait for transition. + Set to ``None`` to wait forever. + :param attribute: Name of the resource attribute that contains the + status. + :param callback: A callback function. This will be called with a single + value, progress. This is API specific but is generally a percentage + value from 0-100. + + :return: The updated resource. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if the + transition to status failed to occur in ``wait`` seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + transitioned to one of the states in ``failures``. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute + """ + return resource.wait_for_status( + self, res, status, failures, interval, wait, attribute, callback + ) + + def wait_for_delete( + self, + res: resource.ResourceT, + interval: int = 2, + wait: int = 120, + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for a resource to be deleted. + + :param res: The resource to wait on to be deleted. + :param interval: Number of seconds to wait before to consecutive + checks. + :param wait: Maximum number of seconds to wait before the change. + :param callback: A callback function. This will be called with a single + value, progress, which is a percentage value from 0-100. + + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete(self, res, interval, wait, callback) diff --git a/openstack/key_manager/v1/_proxy.py b/openstack/key_manager/v1/_proxy.py index f1240be11..7aad2cffd 100644 --- a/openstack/key_manager/v1/_proxy.py +++ b/openstack/key_manager/v1/_proxy.py @@ -10,10 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + from openstack.key_manager.v1 import container as _container from openstack.key_manager.v1 import order as _order from openstack.key_manager.v1 import secret as _secret from openstack import proxy +from openstack import resource class Proxy(proxy.Proxy): @@ -266,3 +269,66 @@ def update_secret(self, secret, **attrs): :rtype: :class:`~openstack.key_manager.v1.secret.Secret` """ return self._update(_secret.Secret, secret, **attrs) + + # ========== Utilities ========== + + def wait_for_status( + self, + res: resource.ResourceT, + status: str, + failures: ty.Optional[list[str]] = None, + interval: ty.Union[int, float, None] = 2, + wait: ty.Optional[int] = None, + attribute: str = 'status', + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for the resource to be in a particular status. + + :param session: The session to use for making this request. + :param resource: The resource to wait on to reach the status. The + resource must have a status attribute specified via ``attribute``. + :param status: Desired status of the resource. + :param failures: Statuses that would indicate the transition + failed such as 'ERROR'. Defaults to ['ERROR']. + :param interval: Number of seconds to wait between checks. + :param wait: Maximum number of seconds to wait for transition. + Set to ``None`` to wait forever. + :param attribute: Name of the resource attribute that contains the + status. + :param callback: A callback function. This will be called with a single + value, progress. This is API specific but is generally a percentage + value from 0-100. + + :return: The updated resource. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if the + transition to status failed to occur in ``wait`` seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + transitioned to one of the states in ``failures``. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute + """ + return resource.wait_for_status( + self, res, status, failures, interval, wait, attribute, callback + ) + + def wait_for_delete( + self, + res: resource.ResourceT, + interval: int = 2, + wait: int = 120, + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for a resource to be deleted. + + :param res: The resource to wait on to be deleted. + :param interval: Number of seconds to wait before to consecutive + checks. + :param wait: Maximum number of seconds to wait before the change. + :param callback: A callback function. This will be called with a single + value, progress, which is a percentage value from 0-100. + + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete(self, res, interval, wait, callback) diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index 250a2b0c2..4474955a2 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + from openstack.load_balancer.v2 import amphora as _amphora from openstack.load_balancer.v2 import availability_zone as _availability_zone from openstack.load_balancer.v2 import ( @@ -1280,3 +1282,66 @@ def update_availability_zone(self, availability_zone, **attrs): return self._update( _availability_zone.AvailabilityZone, availability_zone, **attrs ) + + # ========== Utilities ========== + + def wait_for_status( + self, + res: resource.ResourceT, + status: str, + failures: ty.Optional[list[str]] = None, + interval: ty.Union[int, float, None] = 2, + wait: ty.Optional[int] = None, + attribute: str = 'status', + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for the resource to be in a particular status. + + :param session: The session to use for making this request. + :param resource: The resource to wait on to reach the status. The + resource must have a status attribute specified via ``attribute``. + :param status: Desired status of the resource. + :param failures: Statuses that would indicate the transition + failed such as 'ERROR'. Defaults to ['ERROR']. + :param interval: Number of seconds to wait between checks. + :param wait: Maximum number of seconds to wait for transition. + Set to ``None`` to wait forever. + :param attribute: Name of the resource attribute that contains the + status. + :param callback: A callback function. This will be called with a single + value, progress. This is API specific but is generally a percentage + value from 0-100. + + :return: The updated resource. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if the + transition to status failed to occur in ``wait`` seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + transitioned to one of the states in ``failures``. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute + """ + return resource.wait_for_status( + self, res, status, failures, interval, wait, attribute, callback + ) + + def wait_for_delete( + self, + res: resource.ResourceT, + interval: int = 2, + wait: int = 120, + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for a resource to be deleted. + + :param res: The resource to wait on to be deleted. + :param interval: Number of seconds to wait before to consecutive + checks. + :param wait: Maximum number of seconds to wait before the change. + :param callback: A callback function. This will be called with a single + value, progress, which is a percentage value from 0-100. + + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete(self, res, interval, wait, callback) diff --git a/openstack/message/v2/_proxy.py b/openstack/message/v2/_proxy.py index 4560ace5b..f23373ae4 100644 --- a/openstack/message/v2/_proxy.py +++ b/openstack/message/v2/_proxy.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + from openstack.message.v2 import claim as _claim from openstack.message.v2 import message as _message from openstack.message.v2 import queue as _queue @@ -302,3 +304,66 @@ def delete_claim(self, queue_name, claim, ignore_missing=True): queue_name=queue_name, ignore_missing=ignore_missing, ) + + # ========== Utilities ========== + + def wait_for_status( + self, + res: resource.ResourceT, + status: str, + failures: ty.Optional[list[str]] = None, + interval: ty.Union[int, float, None] = 2, + wait: ty.Optional[int] = None, + attribute: str = 'status', + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for the resource to be in a particular status. + + :param session: The session to use for making this request. + :param resource: The resource to wait on to reach the status. The + resource must have a status attribute specified via ``attribute``. + :param status: Desired status of the resource. + :param failures: Statuses that would indicate the transition + failed such as 'ERROR'. Defaults to ['ERROR']. + :param interval: Number of seconds to wait between checks. + :param wait: Maximum number of seconds to wait for transition. + Set to ``None`` to wait forever. + :param attribute: Name of the resource attribute that contains the + status. + :param callback: A callback function. This will be called with a single + value, progress. This is API specific but is generally a percentage + value from 0-100. + + :return: The updated resource. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if the + transition to status failed to occur in ``wait`` seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + transitioned to one of the states in ``failures``. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute + """ + return resource.wait_for_status( + self, res, status, failures, interval, wait, attribute, callback + ) + + def wait_for_delete( + self, + res: resource.ResourceT, + interval: int = 2, + wait: int = 120, + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for a resource to be deleted. + + :param res: The resource to wait on to be deleted. + :param interval: Number of seconds to wait before to consecutive + checks. + :param wait: Maximum number of seconds to wait before the change. + :param callback: A callback function. This will be called with a single + value, progress, which is a percentage value from 0-100. + + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete(self, res, interval, wait, callback) diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 5c5462cf5..52de4dd65 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -7075,6 +7075,69 @@ def sfc_service_graphs(self, **query): """ return self._list(_sfc_sservice_graph.SfcServiceGraph, **query) + # ========== Utilities ========== + + def wait_for_status( + self, + res: resource.ResourceT, + status: str, + failures: ty.Optional[list[str]] = None, + interval: ty.Union[int, float, None] = 2, + wait: ty.Optional[int] = None, + attribute: str = 'status', + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for the resource to be in a particular status. + + :param session: The session to use for making this request. + :param resource: The resource to wait on to reach the status. The + resource must have a status attribute specified via ``attribute``. + :param status: Desired status of the resource. + :param failures: Statuses that would indicate the transition + failed such as 'ERROR'. Defaults to ['ERROR']. + :param interval: Number of seconds to wait between checks. + :param wait: Maximum number of seconds to wait for transition. + Set to ``None`` to wait forever. + :param attribute: Name of the resource attribute that contains the + status. + :param callback: A callback function. This will be called with a single + value, progress. This is API specific but is generally a percentage + value from 0-100. + + :return: The updated resource. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if the + transition to status failed to occur in ``wait`` seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + transitioned to one of the states in ``failures``. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute + """ + return resource.wait_for_status( + self, res, status, failures, interval, wait, attribute, callback + ) + + def wait_for_delete( + self, + res: resource.ResourceT, + interval: int = 2, + wait: int = 120, + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for a resource to be deleted. + + :param res: The resource to wait on to be deleted. + :param interval: Number of seconds to wait before to consecutive + checks. + :param wait: Maximum number of seconds to wait before the change. + :param callback: A callback function. This will be called with a single + value, progress, which is a percentage value from 0-100. + + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete(self, res, interval, wait, callback) + def _get_cleanup_dependencies(self): return {'network': {'before': ['identity']}} diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index a419ad1c6..8e3b3439e 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -29,6 +29,7 @@ from openstack.object_store.v1 import info as _info from openstack.object_store.v1 import obj as _obj from openstack import proxy +from openstack import resource from openstack import utils DEFAULT_OBJECT_SEGMENT_SIZE = 1073741824 # 1GB @@ -1116,6 +1117,69 @@ def _delete_autocreated_image_objects( deleted = True return deleted + # ========== Utilities ========== + + def wait_for_status( + self, + res: resource.ResourceT, + status: str, + failures: ty.Optional[list[str]] = None, + interval: ty.Union[int, float, None] = 2, + wait: ty.Optional[int] = None, + attribute: str = 'status', + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for the resource to be in a particular status. + + :param session: The session to use for making this request. + :param resource: The resource to wait on to reach the status. The + resource must have a status attribute specified via ``attribute``. + :param status: Desired status of the resource. + :param failures: Statuses that would indicate the transition + failed such as 'ERROR'. Defaults to ['ERROR']. + :param interval: Number of seconds to wait between checks. + :param wait: Maximum number of seconds to wait for transition. + Set to ``None`` to wait forever. + :param attribute: Name of the resource attribute that contains the + status. + :param callback: A callback function. This will be called with a single + value, progress. This is API specific but is generally a percentage + value from 0-100. + + :return: The updated resource. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if the + transition to status failed to occur in ``wait`` seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + transitioned to one of the states in ``failures``. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute + """ + return resource.wait_for_status( + self, res, status, failures, interval, wait, attribute, callback + ) + + def wait_for_delete( + self, + res: resource.ResourceT, + interval: int = 2, + wait: int = 120, + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for a resource to be deleted. + + :param res: The resource to wait on to be deleted. + :param interval: Number of seconds to wait before to consecutive + checks. + :param wait: Maximum number of seconds to wait before the change. + :param callback: A callback function. This will be called with a single + value, progress, which is a percentage value from 0-100. + + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete(self, res, interval, wait, callback) + # ========== Project Cleanup ========== def _get_cleanup_dependencies(self): return {'object_store': {'before': []}} diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index 050aefeef..9a7a40b60 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + from openstack import exceptions from openstack.orchestration.util import template_utils from openstack.orchestration.v1 import resource as _resource @@ -25,9 +27,8 @@ from openstack import resource -# TODO(rladntjr4): Some of these methods support lookup by ID, while -# others support lookup by ID or name. We should choose one and use -# it consistently. +# TODO(rladntjr4): Some of these methods support lookup by ID, while others +# support lookup by ID or name. We should choose one and use it consistently. class Proxy(proxy.Proxy): _resource_registry = { "resource": _resource.Resource, @@ -514,49 +515,6 @@ def validate_template( ignore_errors=ignore_errors, ) - def wait_for_status( - self, res, status='ACTIVE', failures=None, interval=2, wait=120 - ): - """Wait for a resource to be in a particular status. - - :param res: The resource to wait on to reach the specified status. - The resource must have a ``status`` attribute. - :type resource: A :class:`~openstack.resource.Resource` object. - :param status: Desired status. - :param failures: Statuses that would be interpreted as failures. - :type failures: :py:class:`list` - :param interval: Number of seconds to wait before to consecutive - checks. Default to 2. - :param wait: Maximum number of seconds to wait before the change. - Default to 120. - :returns: The resource is returned on success. - :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition - to the desired status failed to occur in specified seconds. - :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource - has transited to one of the failure statuses. - :raises: :class:`~AttributeError` if the resource does not have a - ``status`` attribute. - """ - failures = [] if failures is None else failures - return resource.wait_for_status( - self, res, status, failures, interval, wait - ) - - def wait_for_delete(self, res, interval=2, wait=120): - """Wait for a resource to be deleted. - - :param res: The resource to wait on to be deleted. - :type resource: A :class:`~openstack.resource.Resource` object. - :param interval: Number of seconds to wait before to consecutive - checks. Default to 2. - :param wait: Maximum number of seconds to wait before the change. - Default to 120. - :returns: The resource is returned on success. - :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition - to delete failed to occur in the specified seconds. - """ - return resource.wait_for_delete(self, res, interval, wait) - def get_template_contents( self, template_file=None, @@ -576,47 +534,15 @@ def get_template_contents( f"Error in processing template files: {str(e)}" ) - def _get_cleanup_dependencies(self): - return { - 'orchestration': {'before': ['compute', 'network', 'identity']} - } - - def _service_cleanup( - self, - dry_run=True, - client_status_queue=None, - identified_resources=None, - filters=None, - resource_evaluation_fn=None, - skip_resources=None, - ): - if self.should_skip_resource_cleanup("stack", skip_resources): - return - - stacks = [] - for obj in self.stacks(): - need_delete = self._service_cleanup_del_res( - self.delete_stack, - obj, - dry_run=dry_run, - client_status_queue=client_status_queue, - identified_resources=identified_resources, - filters=filters, - resource_evaluation_fn=resource_evaluation_fn, - ) - if not dry_run and need_delete: - stacks.append(obj) - - for stack in stacks: - self.wait_for_delete(stack) + # ========== Stack events ========== def stack_events(self, stack, resource_name=None, **attr): """Get a stack events :param stack: The value can be the ID of a stack or an instance of :class:`~openstack.orchestration.v1.stack.Stack` - :param resource_name: The name of resource. If the resource_name is not None, - the base_path changes. + :param resource_name: The name of resource. If the resource_name is not + None, the base_path changes. :returns: A generator of stack_events objects :rtype: :class:`~openstack.orchestration.v1.stack_event.StackEvent` @@ -644,3 +570,100 @@ def stack_events(self, stack, resource_name=None, **attr): stack_id=obj.id, **attr, ) + + # ========== Utilities ========== + + def wait_for_status( + self, + res: resource.ResourceT, + status: str, + failures: ty.Optional[list[str]] = None, + interval: ty.Union[int, float, None] = 2, + wait: ty.Optional[int] = None, + attribute: str = 'status', + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for the resource to be in a particular status. + + :param session: The session to use for making this request. + :param resource: The resource to wait on to reach the status. The + resource must have a status attribute specified via ``attribute``. + :param status: Desired status of the resource. + :param failures: Statuses that would indicate the transition + failed such as 'ERROR'. Defaults to ['ERROR']. + :param interval: Number of seconds to wait between checks. + :param wait: Maximum number of seconds to wait for transition. + Set to ``None`` to wait forever. + :param attribute: Name of the resource attribute that contains the + status. + :param callback: A callback function. This will be called with a single + value, progress. This is API specific but is generally a percentage + value from 0-100. + + :return: The updated resource. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if the + transition to status failed to occur in ``wait`` seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + transitioned to one of the states in ``failures``. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute + """ + return resource.wait_for_status( + self, res, status, failures, interval, wait, attribute, callback + ) + + def wait_for_delete( + self, + res: resource.ResourceT, + interval: int = 2, + wait: int = 120, + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for a resource to be deleted. + + :param res: The resource to wait on to be deleted. + :param interval: Number of seconds to wait before to consecutive + checks. + :param wait: Maximum number of seconds to wait before the change. + :param callback: A callback function. This will be called with a single + value, progress, which is a percentage value from 0-100. + + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete(self, res, interval, wait, callback) + + def _get_cleanup_dependencies(self): + return { + 'orchestration': {'before': ['compute', 'network', 'identity']} + } + + def _service_cleanup( + self, + dry_run=True, + client_status_queue=None, + identified_resources=None, + filters=None, + resource_evaluation_fn=None, + skip_resources=None, + ): + if self.should_skip_resource_cleanup("stack", skip_resources): + return + + stacks = [] + for obj in self.stacks(): + need_delete = self._service_cleanup_del_res( + self.delete_stack, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn, + ) + if not dry_run and need_delete: + stacks.append(obj) + + for stack in stacks: + self.wait_for_delete(stack) diff --git a/openstack/placement/v1/_proxy.py b/openstack/placement/v1/_proxy.py index 80f64a510..3c726a016 100644 --- a/openstack/placement/v1/_proxy.py +++ b/openstack/placement/v1/_proxy.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + from openstack.placement.v1 import resource_class as _resource_class from openstack.placement.v1 import resource_provider as _resource_provider from openstack.placement.v1 import ( @@ -460,3 +462,66 @@ def traits(self, **query): :returns: A generator of trait objects """ return self._list(_trait.Trait, **query) + + # ========== Utilities ========== + + def wait_for_status( + self, + res: resource.ResourceT, + status: str, + failures: ty.Optional[list[str]] = None, + interval: ty.Union[int, float, None] = 2, + wait: ty.Optional[int] = None, + attribute: str = 'status', + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for the resource to be in a particular status. + + :param session: The session to use for making this request. + :param resource: The resource to wait on to reach the status. The + resource must have a status attribute specified via ``attribute``. + :param status: Desired status of the resource. + :param failures: Statuses that would indicate the transition + failed such as 'ERROR'. Defaults to ['ERROR']. + :param interval: Number of seconds to wait between checks. + :param wait: Maximum number of seconds to wait for transition. + Set to ``None`` to wait forever. + :param attribute: Name of the resource attribute that contains the + status. + :param callback: A callback function. This will be called with a single + value, progress. This is API specific but is generally a percentage + value from 0-100. + + :return: The updated resource. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if the + transition to status failed to occur in ``wait`` seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + transitioned to one of the states in ``failures``. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute + """ + return resource.wait_for_status( + self, res, status, failures, interval, wait, attribute, callback + ) + + def wait_for_delete( + self, + res: resource.ResourceT, + interval: int = 2, + wait: int = 120, + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for a resource to be deleted. + + :param res: The resource to wait on to be deleted. + :param interval: Number of seconds to wait before to consecutive + checks. + :param wait: Maximum number of seconds to wait before the change. + :param callback: A callback function. This will be called with a single + value, progress, which is a percentage value from 0-100. + + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete(self, res, interval, wait, callback) diff --git a/openstack/resource.py b/openstack/resource.py index 5f0cacc25..95f191ddb 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -2391,7 +2391,7 @@ def wait_for_status( session: adapter.Adapter, resource: ResourceT, status: str, - failures: list[str], + failures: ty.Optional[list[str]] = None, interval: ty.Union[int, float, None] = 2, wait: ty.Optional[int] = None, attribute: str = 'status', diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index 0bc53f7cc..217089469 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -9,6 +9,9 @@ # 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 typing as ty + from openstack import exceptions from openstack import proxy from openstack import resource @@ -357,47 +360,6 @@ def delete_share_group(self, share_group_id, ignore_missing=True): ignore_missing=ignore_missing, ) - def wait_for_status( - self, - res, - status='active', - failures=None, - interval=2, - wait=120, - status_attr_name='status', - ): - """Wait for a resource to be in a particular status. - :param res: The resource to wait on to reach the specified status. - The resource must have a ``status`` attribute. - :type resource: A :class:`~openstack.resource.Resource` object. - :param status: Desired status. - :param failures: Statuses that would be interpreted as failures. - :type failures: :py:class:`list` - :param interval: Number of seconds to wait before to consecutive - checks. Default to 2. - :param wait: Maximum number of seconds to wait before the change. - Default to 120. - :param status_attr_name: name of the attribute to reach the desired - status. - :returns: The resource is returned on success. - :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition - to the desired status failed to occur in specified seconds. - :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource - has transited to one of the failure statuses. - :raises: :class:`~AttributeError` if the resource does not have a - ``status`` attribute. - """ - failures = [] if failures is None else failures - return resource.wait_for_status( - self, - res, - status, - failures, - interval, - wait, - attribute=status_attr_name, - ) - def storage_pools(self, details=True, **query): """Lists all back-end storage pools with details @@ -619,21 +581,6 @@ def delete_share_network_subnet( ignore_missing=ignore_missing, ) - def wait_for_delete(self, res, interval=2, wait=120): - """Wait for a resource to be deleted. - - :param res: The resource to wait on to be deleted. - :type resource: A :class:`~openstack.resource.Resource` object. - :param interval: Number of seconds to wait before to consecutive - checks. Default to 2. - :param wait: Maximum number of seconds to wait before the change. - Default to 120. - :returns: The resource is returned on success. - :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition - to delete failed to occur in the specified seconds. - """ - return resource.wait_for_delete(self, res, interval, wait) - def share_snapshot_instances(self, details=True, **query): """Lists all share snapshot instances with details. @@ -1237,3 +1184,66 @@ def update_quota_class_set(self, quota_class_name, **attrs): return self._update( _quota_class_set.QuotaClassSet, quota_class_name, **attrs ) + + # ========== Utilities ========== + + def wait_for_status( + self, + res: resource.ResourceT, + status: str, + failures: ty.Optional[list[str]] = None, + interval: ty.Union[int, float, None] = 2, + wait: ty.Optional[int] = None, + attribute: str = 'status', + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for the resource to be in a particular status. + + :param session: The session to use for making this request. + :param resource: The resource to wait on to reach the status. The + resource must have a status attribute specified via ``attribute``. + :param status: Desired status of the resource. + :param failures: Statuses that would indicate the transition + failed such as 'ERROR'. Defaults to ['ERROR']. + :param interval: Number of seconds to wait between checks. + :param wait: Maximum number of seconds to wait for transition. + Set to ``None`` to wait forever. + :param attribute: Name of the resource attribute that contains the + status. + :param callback: A callback function. This will be called with a single + value, progress. This is API specific but is generally a percentage + value from 0-100. + + :return: The updated resource. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if the + transition to status failed to occur in ``wait`` seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + transitioned to one of the states in ``failures``. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute + """ + return resource.wait_for_status( + self, res, status, failures, interval, wait, attribute, callback + ) + + def wait_for_delete( + self, + res: resource.ResourceT, + interval: int = 2, + wait: int = 120, + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for a resource to be deleted. + + :param res: The resource to wait on to be deleted. + :param interval: Number of seconds to wait before to consecutive + checks. + :param wait: Maximum number of seconds to wait before the change. + :param callback: A callback function. This will be called with a single + value, progress, which is a percentage value from 0-100. + + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete(self, res, interval, wait, callback) diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index c25d5d54a..6c7e2437d 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -127,8 +127,16 @@ def test_volume_wait_for(self): self.verify_wait_for_status( self.proxy.wait_for_status, method_args=[value], - expected_args=[self.proxy, value, 'available', ['error'], 2, 120], - expected_kwargs={'callback': None}, + expected_args=[ + self.proxy, + value, + 'available', + ['error'], + 2, + None, + 'status', + None, + ], ) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 7a5514790..3cbaab574 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -126,8 +126,16 @@ def test_volume_wait_for(self): self.verify_wait_for_status( self.proxy.wait_for_status, method_args=[value], - expected_args=[self.proxy, value, 'available', ['error'], 2, 120], - expected_kwargs={'callback': None}, + expected_args=[ + self.proxy, + value, + 'available', + ['error'], + 2, + None, + 'status', + None, + ], ) diff --git a/openstack/tests/unit/clustering/v1/test_proxy.py b/openstack/tests/unit/clustering/v1/test_proxy.py index e25d90290..46d717e54 100644 --- a/openstack/tests/unit/clustering/v1/test_proxy.py +++ b/openstack/tests/unit/clustering/v1/test_proxy.py @@ -410,7 +410,7 @@ def test_wait_for(self, mock_wait): self.proxy.wait_for_status(mock_resource, 'ACTIVE') mock_wait.assert_called_once_with( - self.proxy, mock_resource, 'ACTIVE', [], 2, 120 + self.proxy, mock_resource, 'ACTIVE', None, 2, None, 'status', None ) @mock.patch("openstack.resource.wait_for_status") @@ -421,7 +421,14 @@ def test_wait_for_params(self, mock_wait): self.proxy.wait_for_status(mock_resource, 'ACTIVE', ['ERROR'], 1, 2) mock_wait.assert_called_once_with( - self.proxy, mock_resource, 'ACTIVE', ['ERROR'], 1, 2 + self.proxy, + mock_resource, + 'ACTIVE', + ['ERROR'], + 1, + 2, + 'status', + None, ) @mock.patch("openstack.resource.wait_for_delete") @@ -431,7 +438,9 @@ def test_wait_for_delete(self, mock_wait): self.proxy.wait_for_delete(mock_resource) - mock_wait.assert_called_once_with(self.proxy, mock_resource, 2, 120) + mock_wait.assert_called_once_with( + self.proxy, mock_resource, 2, 120, None + ) @mock.patch("openstack.resource.wait_for_delete") def test_wait_for_delete_params(self, mock_wait): @@ -440,7 +449,9 @@ def test_wait_for_delete_params(self, mock_wait): self.proxy.wait_for_delete(mock_resource, 1, 2) - mock_wait.assert_called_once_with(self.proxy, mock_resource, 1, 2) + mock_wait.assert_called_once_with( + self.proxy, mock_resource, 1, 2, None + ) def test_get_cluster_metadata(self): self._verify( diff --git a/openstack/tests/unit/shared_file_system/v2/test_proxy.py b/openstack/tests/unit/shared_file_system/v2/test_proxy.py index a412c3217..9643e4233 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_proxy.py +++ b/openstack/tests/unit/shared_file_system/v2/test_proxy.py @@ -131,7 +131,7 @@ def test_wait_for(self, mock_wait): self.proxy.wait_for_status(mock_resource, 'ACTIVE') mock_wait.assert_called_once_with( - self.proxy, mock_resource, 'ACTIVE', [], 2, 120, attribute='status' + self.proxy, mock_resource, 'ACTIVE', None, 2, None, 'status', None ) @@ -308,7 +308,9 @@ def test_wait_for_delete(self, mock_wait): self.proxy.wait_for_delete(mock_resource) - mock_wait.assert_called_once_with(self.proxy, mock_resource, 2, 120) + mock_wait.assert_called_once_with( + self.proxy, mock_resource, 2, 120, None + ) class TestShareSnapshotInstanceResource(test_proxy_base.TestProxyBase): diff --git a/openstack/workflow/v2/_proxy.py b/openstack/workflow/v2/_proxy.py index 72994bb92..881fd5361 100644 --- a/openstack/workflow/v2/_proxy.py +++ b/openstack/workflow/v2/_proxy.py @@ -10,7 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + from openstack import proxy +from openstack import resource from openstack.workflow.v2 import cron_trigger as _cron_trigger from openstack.workflow.v2 import execution as _execution from openstack.workflow.v2 import workflow as _workflow @@ -291,3 +294,66 @@ def find_cron_trigger( all_projects=all_projects, **query, ) + + # ========== Utilities ========== + + def wait_for_status( + self, + res: resource.ResourceT, + status: str, + failures: ty.Optional[list[str]] = None, + interval: ty.Union[int, float, None] = 2, + wait: ty.Optional[int] = None, + attribute: str = 'status', + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for the resource to be in a particular status. + + :param session: The session to use for making this request. + :param resource: The resource to wait on to reach the status. The + resource must have a status attribute specified via ``attribute``. + :param status: Desired status of the resource. + :param failures: Statuses that would indicate the transition + failed such as 'ERROR'. Defaults to ['ERROR']. + :param interval: Number of seconds to wait between checks. + :param wait: Maximum number of seconds to wait for transition. + Set to ``None`` to wait forever. + :param attribute: Name of the resource attribute that contains the + status. + :param callback: A callback function. This will be called with a single + value, progress. This is API specific but is generally a percentage + value from 0-100. + + :return: The updated resource. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if the + transition to status failed to occur in ``wait`` seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + transitioned to one of the states in ``failures``. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute + """ + return resource.wait_for_status( + self, res, status, failures, interval, wait, attribute, callback + ) + + def wait_for_delete( + self, + res: resource.ResourceT, + interval: int = 2, + wait: int = 120, + callback: ty.Optional[ty.Callable[[int], None]] = None, + ) -> resource.ResourceT: + """Wait for a resource to be deleted. + + :param res: The resource to wait on to be deleted. + :param interval: Number of seconds to wait before to consecutive + checks. + :param wait: Maximum number of seconds to wait before the change. + :param callback: A callback function. This will be called with a single + value, progress, which is a percentage value from 0-100. + + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete(self, res, interval, wait, callback) From a11df932f671c1d7ad7679c418fb74b126f5851c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Sat, 20 Jul 2024 20:02:53 +0100 Subject: [PATCH 3629/3836] resource: Rework fields The Body, Header, URI and Computed field types are not values in their own right: rather, they are indicators of where the real values will come from. At some point, we should probably make better use of type annotations but for now, modify things so that 'openstack.resource.Body' et al are functions that return 'Any' and don't conflict with explicit type annotations. This allows us to change some of the attributes of the base resource - namely the 'name' and 'location' fields - to reflect their "real" types. Change-Id: Ia771d18cf6d8d72dbf995418166e996515f5d52c Signed-off-by: Stephen Finucane --- openstack/baremetal/v1/_common.py | 3 +- openstack/common/metadata.py | 2 +- openstack/common/tag.py | 2 +- openstack/fields.py | 235 +++++++++++ openstack/image/_download.py | 3 +- openstack/image/v2/metadef_property.py | 7 +- .../orchestration/v1/stack_environment.py | 4 +- openstack/orchestration/v1/stack_files.py | 4 +- .../v1/resource_provider_inventory.py | 5 +- openstack/placement/v1/trait.py | 5 +- openstack/resource.py | 384 +++++++----------- openstack/test/fakes.py | 7 +- openstack/tests/unit/test_fields.py | 224 ++++++++++ openstack/tests/unit/test_resource.py | 221 +--------- 14 files changed, 629 insertions(+), 477 deletions(-) create mode 100644 openstack/fields.py create mode 100644 openstack/tests/unit/test_fields.py diff --git a/openstack/baremetal/v1/_common.py b/openstack/baremetal/v1/_common.py index dcfc18e58..cac214c6b 100644 --- a/openstack/baremetal/v1/_common.py +++ b/openstack/baremetal/v1/_common.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import fields from openstack import resource @@ -144,7 +145,7 @@ def fields_type(value, resource_type): resource_mapping = { key: value.name for key, value in resource_type.__dict__.items() - if isinstance(value, resource.Body) + if isinstance(value, fields.Body) } return comma_separated_list(resource_mapping.get(x, x) for x in value) diff --git a/openstack/common/metadata.py b/openstack/common/metadata.py index 36c30b1d0..69902ee1a 100644 --- a/openstack/common/metadata.py +++ b/openstack/common/metadata.py @@ -16,7 +16,7 @@ class MetadataMixin: - id: resource.Body + id: str base_path: str _body: resource._ComponentManager diff --git a/openstack/common/tag.py b/openstack/common/tag.py index 09e2323ba..24ec63227 100644 --- a/openstack/common/tag.py +++ b/openstack/common/tag.py @@ -16,7 +16,7 @@ class TagMixin: - id: resource.Body + id: str base_path: str _body: resource._ComponentManager diff --git a/openstack/fields.py b/openstack/fields.py new file mode 100644 index 000000000..ad98ceab0 --- /dev/null +++ b/openstack/fields.py @@ -0,0 +1,235 @@ +# 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 typing as ty +import warnings + +from requests import structures + +from openstack import format +from openstack import warnings as os_warnings + +_SEEN_FORMAT = '{name}_seen' + + +def _convert_type(value, data_type, list_type=None): + # This should allow handling list of dicts that have their own + # Component type directly. See openstack/compute/v2/limits.py + # and the RateLimit type for an example. + if not data_type: + return value + if issubclass(data_type, list): + if isinstance(value, (list, tuple, set)): + if not list_type: + return value + ret = [] + for raw in value: + ret.append(_convert_type(raw, list_type)) + return ret + elif list_type: + return [_convert_type(value, list_type)] + # "if-match" in Object is a good example of the need here + return [value] + elif isinstance(value, data_type): + return value + if not isinstance(value, data_type): + if issubclass(data_type, format.Formatter): + return data_type.deserialize(value) + # This should allow handling sub-dicts that have their own + # Component type directly. See openstack/compute/v2/limits.py + # and the AbsoluteLimits type for an example. + if isinstance(value, dict): + return data_type(**value) + try: + return data_type(value) + except ValueError: + # If we can not convert data to the expected type return empty + # instance of the expected type. + # This is necessary to handle issues like with flavor.swap where + # empty string means "0". + return data_type() + + +class _BaseComponent(abc.ABC): + # The name this component is being tracked as in the Resource + key: str + # The class to be used for mappings + _map_cls: type[ty.Mapping] = dict + + #: Marks the property as deprecated. + deprecated = False + #: Deprecation reason message used to warn users when deprecated == True + deprecation_reason = None + + #: Control field used to manage the deprecation warning. We want to warn + #: only once when the attribute is retrieved in the code. + already_warned_deprecation = False + + def __init__( + self, + name, + type=None, + default=None, + alias=None, + aka=None, + alternate_id=False, + list_type=None, + coerce_to_default=False, + deprecated=False, + deprecation_reason=None, + **kwargs, + ): + """A typed descriptor for a component that makes up a Resource + + :param name: The name this component exists as on the server + :param type: + The type this component is expected to be by the server. + By default this is None, meaning any value you specify + will work. If you specify type=dict and then set a + component to a string, __set__ will fail, for example. + :param default: Typically None, but any other default can be set. + :param alias: If set, alternative attribute on object to return. + :param aka: If set, additional name attribute would be available under. + :param alternate_id: + When `True`, this property is known internally as a value that + can be sent with requests that require an ID but when `id` is + not a name the Resource has. This is a relatively uncommon case, + and this setting should only be used once per Resource. + :param list_type: + If type is `list`, list_type designates what the type of the + elements of the list should be. + :param coerce_to_default: + If the Component is None or not present, force the given default + to be used. If a default is not given but a type is given, + construct an empty version of the type in question. + :param deprecated: + Indicates if the option is deprecated. If it is, we display a + warning message to the user. + :param deprecation_reason: + Custom deprecation message. + """ + self.name = name + self.type = type + if type is not None and coerce_to_default and not default: + self.default = type() + else: + self.default = default + self.alias = alias + self.aka = aka + self.alternate_id = alternate_id + self.list_type = list_type + self.coerce_to_default = coerce_to_default + + self.deprecated = deprecated + self.deprecation_reason = deprecation_reason + + def __get__(self, instance, owner): + if instance is None: + return self + + attributes = getattr(instance, self.key) + + try: + value = attributes[self.name] + except KeyError: + value = self.default + if self.alias: + # Resource attributes can be aliased to each other. If neither + # of them exist, then simply doing a + # getattr(instance, self.alias) here sends things into + # infinite recursion (this _get method is what gets called + # when getattr(instance) is called. + # To combat that, we set a flag on the instance saying that + # we have seen the current name, and we check before trying + # to resolve the alias if there is already a flag set for that + # alias name. We then remove the seen flag for ourselves after + # we exit the alias getattr to clean up after ourselves for + # the next time. + alias_flag = _SEEN_FORMAT.format(name=self.alias) + if not getattr(instance, alias_flag, False): + seen_flag = _SEEN_FORMAT.format(name=self.name) + # Prevent infinite recursion + setattr(instance, seen_flag, True) + value = getattr(instance, self.alias) + delattr(instance, seen_flag) + self.warn_if_deprecated_property(value) + return value + + # self.type() should not be called on None objects. + if value is None: + return None + + # This warning are pretty intruisive. Every time attribute is accessed + # a warning is being thrown. In neutron clients we have way too many + # places that still refer to tenant_id even though they may also + # properly support project_id. For now we silence tenant_id warnings. + if self.name != "tenant_id": + self.warn_if_deprecated_property(value) + return _convert_type(value, self.type, self.list_type) + + def warn_if_deprecated_property(self, value): + deprecated = object.__getattribute__(self, 'deprecated') + deprecation_reason = object.__getattribute__( + self, + 'deprecation_reason', + ) + + if value and deprecated: + warnings.warn( + "The field {!r} has been deprecated. {}".format( + self.name, deprecation_reason or "Avoid usage." + ), + os_warnings.RemovedFieldWarning, + ) + return value + + def __set__(self, instance, value): + if self.coerce_to_default and value is None: + value = self.default + if value != self.default: + value = _convert_type(value, self.type, self.list_type) + + attributes = getattr(instance, self.key) + attributes[self.name] = value + + def __delete__(self, instance): + try: + attributes = getattr(instance, self.key) + del attributes[self.name] + except KeyError: + pass + + +class Body(_BaseComponent): + """Body attributes""" + + key = "_body" + + +class Header(_BaseComponent): + """Header attributes""" + + key = "_header" + _map_cls = structures.CaseInsensitiveDict + + +class URI(_BaseComponent): + """URI attributes""" + + key = "_uri" + + +class Computed(_BaseComponent): + """Computed attributes""" + + key = "_computed" diff --git a/openstack/image/_download.py b/openstack/image/_download.py index 1751bf237..160fbdcda 100644 --- a/openstack/image/_download.py +++ b/openstack/image/_download.py @@ -14,7 +14,6 @@ import io from openstack import exceptions -from openstack import resource from openstack import utils @@ -28,7 +27,7 @@ def _verify_checksum(md5, checksum): class DownloadMixin: - id: resource.Body + id: str base_path: str def fetch( diff --git a/openstack/image/v2/metadef_property.py b/openstack/image/v2/metadef_property.py index 0f31b0239..b92216ab9 100644 --- a/openstack/image/v2/metadef_property.py +++ b/openstack/image/v2/metadef_property.py @@ -11,6 +11,7 @@ # under the License. from openstack import exceptions +from openstack import fields from openstack import resource @@ -57,7 +58,7 @@ class MetadefProperty(resource.Resource): # FIXME(stephenfin): This is causing conflicts due to the 'dict.items' # method. Perhaps we need to rename it? #: Schema for the items in an array. - items = resource.Body('items', type=dict) # type: ignore + items = resource.Body('items', type=dict) #: Indicates whether all values in the array must be distinct. require_unique_items = resource.Body( 'uniqueItems', type=bool, default=False @@ -114,7 +115,7 @@ def list( # Known attr hasattr(cls, k) # Is real attr property - and isinstance(getattr(cls, k), resource.Body) + and isinstance(getattr(cls, k), fields.Body) # not included in the query_params and k not in cls._query_mapping._mapping.keys() ): @@ -125,7 +126,7 @@ def list( for k, v in params.items(): # We need to gather URI parts to set them on the resource later - if hasattr(cls, k) and isinstance(getattr(cls, k), resource.URI): + if hasattr(cls, k) and isinstance(getattr(cls, k), fields.URI): uri_params[k] = v def _dict_filter(f, d): diff --git a/openstack/orchestration/v1/stack_environment.py b/openstack/orchestration/v1/stack_environment.py index 475efdb22..e61bea9da 100644 --- a/openstack/orchestration/v1/stack_environment.py +++ b/openstack/orchestration/v1/stack_environment.py @@ -29,9 +29,9 @@ class StackEnvironment(resource.Resource): # Backwards compat stack_name = name #: ID of the stack where the template is referenced. - id = resource.URI('stack_id') # type: ignore + id = resource.URI('stack_id') # Backwards compat - stack_id = id # type: ignore + stack_id = id #: A list of parameter names whose values are encrypted encrypted_param_names = resource.Body('encrypted_param_names') #: A list of event sinks diff --git a/openstack/orchestration/v1/stack_files.py b/openstack/orchestration/v1/stack_files.py index e38907c12..6c3919b0a 100644 --- a/openstack/orchestration/v1/stack_files.py +++ b/openstack/orchestration/v1/stack_files.py @@ -29,9 +29,9 @@ class StackFiles(resource.Resource): # Backwards compat stack_name = name #: ID of the stack where the template is referenced. - id = resource.URI('stack_id') # type: ignore + id = resource.URI('stack_id') # Backwards compat - stack_id = id # type: ignore + stack_id = id def fetch( self, session, requires_id=False, base_path=None, *args, **kwargs diff --git a/openstack/placement/v1/resource_provider_inventory.py b/openstack/placement/v1/resource_provider_inventory.py index 4261e6ba1..d178e88a5 100644 --- a/openstack/placement/v1/resource_provider_inventory.py +++ b/openstack/placement/v1/resource_provider_inventory.py @@ -11,6 +11,7 @@ # under the License. from openstack import exceptions +from openstack import fields from openstack import resource @@ -125,7 +126,7 @@ def list( # Known attr hasattr(cls, k) # Is real attr property - and isinstance(getattr(cls, k), resource.Body) + and isinstance(getattr(cls, k), fields.Body) # not included in the query_params and k not in cls._query_mapping._mapping.keys() ): @@ -136,7 +137,7 @@ def list( for k, v in params.items(): # We need to gather URI parts to set them on the resource later - if hasattr(cls, k) and isinstance(getattr(cls, k), resource.URI): + if hasattr(cls, k) and isinstance(getattr(cls, k), fields.URI): uri_params[k] = v def _dict_filter(f, d): diff --git a/openstack/placement/v1/trait.py b/openstack/placement/v1/trait.py index d87c60856..75b7875ba 100644 --- a/openstack/placement/v1/trait.py +++ b/openstack/placement/v1/trait.py @@ -11,6 +11,7 @@ # under the License. from openstack import exceptions +from openstack import fields from openstack import resource @@ -76,7 +77,7 @@ def list( # Known attr hasattr(cls, k) # Is real attr property - and isinstance(getattr(cls, k), resource.Body) + and isinstance(getattr(cls, k), fields.Body) # not included in the query_params and k not in cls._query_mapping._mapping.keys() ): @@ -87,7 +88,7 @@ def list( for k, v in params.items(): # We need to gather URI parts to set them on the resource later - if hasattr(cls, k) and isinstance(getattr(cls, k), resource.URI): + if hasattr(cls, k) and isinstance(getattr(cls, k), fields.URI): uri_params[k] = v def _dict_filter(f, d): diff --git a/openstack/resource.py b/openstack/resource.py index 95f191ddb..5ecceabbd 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -16,8 +16,8 @@ class that represent a remote resource. The attributes that comprise a request or response for this resource are specified as class members on the Resource subclass where their values -are of a component type, including :class:`~openstack.resource.Body`, -:class:`~openstack.resource.Header`, and :class:`~openstack.resource.URI`. +are of a component type, including :class:`~openstack.fields.Body`, +:class:`~openstack.fields.Header`, and :class:`~openstack.fields.URI`. For update management, :class:`~openstack.resource.Resource` employs a series of :class:`~openstack.resource._ComponentManager` instances @@ -32,7 +32,6 @@ class that represent a remote resource. The attributes that and then returned to the caller. """ -import abc import collections import inspect import itertools @@ -45,230 +44,126 @@ class that represent a remote resource. The attributes that import jsonpatch from keystoneauth1 import adapter from keystoneauth1 import discover -from requests import structures from openstack import _log from openstack import exceptions -from openstack import format +from openstack import fields from openstack import utils from openstack import warnings as os_warnings -_SEEN_FORMAT = '{name}_seen' - LOG = _log.setup_logging(__name__) -def _convert_type(value, data_type, list_type=None): - # This should allow handling list of dicts that have their own - # Component type directly. See openstack/compute/v2/limits.py - # and the RateLimit type for an example. - if not data_type: - return value - if issubclass(data_type, list): - if isinstance(value, (list, tuple, set)): - if not list_type: - return value - ret = [] - for raw in value: - ret.append(_convert_type(raw, list_type)) - return ret - elif list_type: - return [_convert_type(value, list_type)] - # "if-match" in Object is a good example of the need here - return [value] - elif isinstance(value, data_type): - return value - if not isinstance(value, data_type): - if issubclass(data_type, format.Formatter): - return data_type.deserialize(value) - # This should allow handling sub-dicts that have their own - # Component type directly. See openstack/compute/v2/limits.py - # and the AbsoluteLimits type for an example. - if isinstance(value, dict): - return data_type(**value) - try: - return data_type(value) - except ValueError: - # If we can not convert data to the expected type return empty - # instance of the expected type. - # This is necessary to handle issues like with flavor.swap where - # empty string means "0". - return data_type() - - -class _BaseComponent(abc.ABC): - # The name this component is being tracked as in the Resource - key: str - # The class to be used for mappings - _map_cls: type[ty.Mapping] = dict - - #: Marks the property as deprecated. - deprecated = False - #: Deprecation reason message used to warn users when deprecated == True - deprecation_reason = None - - #: Control field used to manage the deprecation warning. We want to warn - #: only once when the attribute is retrieved in the code. - already_warned_deprecation = False - - def __init__( - self, +def Body( + name, + type=None, + default=None, + alias=None, + aka=None, + alternate_id=False, + list_type=None, + coerce_to_default=False, + deprecated=False, + deprecation_reason=None, + **kwargs, +): + return fields.Body( name, - type=None, - default=None, - alias=None, - aka=None, - alternate_id=False, - list_type=None, - coerce_to_default=False, - deprecated=False, - deprecation_reason=None, + type=type, + default=default, + alias=alias, + aka=aka, + alternate_id=alternate_id, + list_type=list_type, + coerce_to_default=coerce_to_default, + deprecated=deprecated, + deprecation_reason=deprecation_reason, **kwargs, - ): - """A typed descriptor for a component that makes up a Resource - - :param name: The name this component exists as on the server - :param type: - The type this component is expected to be by the server. - By default this is None, meaning any value you specify - will work. If you specify type=dict and then set a - component to a string, __set__ will fail, for example. - :param default: Typically None, but any other default can be set. - :param alias: If set, alternative attribute on object to return. - :param aka: If set, additional name attribute would be available under. - :param alternate_id: - When `True`, this property is known internally as a value that - can be sent with requests that require an ID but when `id` is - not a name the Resource has. This is a relatively uncommon case, - and this setting should only be used once per Resource. - :param list_type: - If type is `list`, list_type designates what the type of the - elements of the list should be. - :param coerce_to_default: - If the Component is None or not present, force the given default - to be used. If a default is not given but a type is given, - construct an empty version of the type in question. - :param deprecated: - Indicates if the option is deprecated. If it is, we display a - warning message to the user. - :param deprecation_reason: - Custom deprecation message. - """ - self.name = name - self.type = type - if type is not None and coerce_to_default and not default: - self.default = type() - else: - self.default = default - self.alias = alias - self.aka = aka - self.alternate_id = alternate_id - self.list_type = list_type - self.coerce_to_default = coerce_to_default - - self.deprecated = deprecated - self.deprecation_reason = deprecation_reason - - def __get__(self, instance, owner): - if instance is None: - return self - - attributes = getattr(instance, self.key) - - try: - value = attributes[self.name] - except KeyError: - value = self.default - if self.alias: - # Resource attributes can be aliased to each other. If neither - # of them exist, then simply doing a - # getattr(instance, self.alias) here sends things into - # infinite recursion (this _get method is what gets called - # when getattr(instance) is called. - # To combat that, we set a flag on the instance saying that - # we have seen the current name, and we check before trying - # to resolve the alias if there is already a flag set for that - # alias name. We then remove the seen flag for ourselves after - # we exit the alias getattr to clean up after ourselves for - # the next time. - alias_flag = _SEEN_FORMAT.format(name=self.alias) - if not getattr(instance, alias_flag, False): - seen_flag = _SEEN_FORMAT.format(name=self.name) - # Prevent infinite recursion - setattr(instance, seen_flag, True) - value = getattr(instance, self.alias) - delattr(instance, seen_flag) - self.warn_if_deprecated_property(value) - return value - - # self.type() should not be called on None objects. - if value is None: - return None - - # This warning are pretty intruisive. Every time attribute is accessed - # a warning is being thrown. In neutron clients we have way too many - # places that still refer to tenant_id even though they may also - # properly support project_id. For now we silence tenant_id warnings. - if self.name != "tenant_id": - self.warn_if_deprecated_property(value) - return _convert_type(value, self.type, self.list_type) - - def warn_if_deprecated_property(self, value): - deprecated = object.__getattribute__(self, 'deprecated') - deprecation_reason = object.__getattribute__( - self, - 'deprecation_reason', - ) - - if value and deprecated: - warnings.warn( - "The field {!r} has been deprecated. {}".format( - self.name, deprecation_reason or "Avoid usage." - ), - os_warnings.RemovedFieldWarning, - ) - return value - - def __set__(self, instance, value): - if self.coerce_to_default and value is None: - value = self.default - if value != self.default: - value = _convert_type(value, self.type, self.list_type) - - attributes = getattr(instance, self.key) - attributes[self.name] = value - - def __delete__(self, instance): - try: - attributes = getattr(instance, self.key) - del attributes[self.name] - except KeyError: - pass - - -class Body(_BaseComponent): - """Body attributes""" - - key = "_body" - - -class Header(_BaseComponent): - """Header attributes""" - - key = "_header" - _map_cls = structures.CaseInsensitiveDict - - -class URI(_BaseComponent): - """URI attributes""" - - key = "_uri" - - -class Computed(_BaseComponent): - """Computed attributes""" - - key = "_computed" + ) + + +def Header( + name, + type=None, + default=None, + alias=None, + aka=None, + alternate_id=False, + list_type=None, + coerce_to_default=False, + deprecated=False, + deprecation_reason=None, + **kwargs, +): + return fields.Header( + name, + type=type, + default=default, + alias=alias, + aka=aka, + alternate_id=alternate_id, + list_type=list_type, + coerce_to_default=coerce_to_default, + deprecated=deprecated, + deprecation_reason=deprecation_reason, + **kwargs, + ) + + +def URI( + name, + type=None, + default=None, + alias=None, + aka=None, + alternate_id=False, + list_type=None, + coerce_to_default=False, + deprecated=False, + deprecation_reason=None, + **kwargs, +): + return fields.URI( + name, + type=type, + default=default, + alias=alias, + aka=aka, + alternate_id=alternate_id, + list_type=list_type, + coerce_to_default=coerce_to_default, + deprecated=deprecated, + deprecation_reason=deprecation_reason, + **kwargs, + ) + + +def Computed( + name, + type=None, + default=None, + alias=None, + aka=None, + alternate_id=False, + list_type=None, + coerce_to_default=False, + deprecated=False, + deprecation_reason=None, + **kwargs, +): + return fields.Computed( + name, + type=type, + default=default, + alias=alias, + aka=aka, + alternate_id=alternate_id, + list_type=list_type, + coerce_to_default=coerce_to_default, + deprecated=deprecated, + deprecation_reason=deprecation_reason, + **kwargs, + ) class _ComponentManager(collections.abc.MutableMapping): @@ -465,9 +360,9 @@ class Resource(dict): id = Body("id") #: The name of this resource. - name: ty.Union[Body, URI] = Body("name") + name: str = Body("name") #: The OpenStack location of this resource. - location: ty.Union[Computed, Body, Header] = Computed('location') + location: dict[str, ty.Any] = Computed('location') #: Mapping of accepted query parameter names. _query_mapping = QueryParameters() @@ -599,7 +494,9 @@ def __init__(self, _synchronized=False, connection=None, **attrs): dict.update(self, self.to_dict()) @classmethod - def _attributes_iterator(cls, components=tuple([Body, Header])): + def _attributes_iterator( + cls, components=tuple([fields.Body, fields.Header]) + ): """Iterator over all Resource attributes""" # isinstance stricly requires this to be a tuple # Since we're looking at class definitions we need to include @@ -678,14 +575,14 @@ def __getitem__(self, name): # Not found? But we know an alias exists. name = self._attr_aliases[name] real_item = getattr(self.__class__, name, None) - if isinstance(real_item, _BaseComponent): + if isinstance(real_item, fields._BaseComponent): return getattr(self, name) if not real_item: # In order to maintain backwards compatibility where we were # returning Munch (and server side names) and Resource object with # normalized attributes we can offer dict access via server side # names. - for attr, component in self._attributes_iterator(tuple([Body])): + for attr, component in self._attributes_iterator((fields.Body,)): if component.name == name: warnings.warn( f"Access to '{self.__class__}[{name}]' is deprecated. " @@ -703,7 +600,7 @@ def __delitem__(self, name): def __setitem__(self, name, value): real_item = getattr(self.__class__, name, None) - if isinstance(real_item, _BaseComponent): + if isinstance(real_item, fields._BaseComponent): self.__setattr__(name, value) else: if self._allow_unknown_attrs_in_body: @@ -722,7 +619,12 @@ def _attributes( attributes = [] if not components: - components = tuple([Body, Header, Computed, URI]) + components = ( + fields.Body, + fields.Header, + fields.Computed, + fields.URI, + ) for attr, component in self._attributes_iterator(components): key = attr if not remote_names else component.name @@ -841,13 +743,13 @@ def _compute_attributes(self, body, header, uri): return {} def _consume_body_attrs(self, attrs): - return self._consume_mapped_attrs(Body, attrs) + return self._consume_mapped_attrs(fields.Body, attrs) def _consume_header_attrs(self, attrs): - return self._consume_mapped_attrs(Header, attrs) + return self._consume_mapped_attrs(fields.Header, attrs) def _consume_uri_attrs(self, attrs): - return self._consume_mapped_attrs(URI, attrs) + return self._consume_mapped_attrs(fields.URI, attrs) def _update_from_body_attrs(self, attrs): body = self._consume_body_attrs(attrs) @@ -925,22 +827,22 @@ def _get_mapping(cls, component): @classmethod def _body_mapping(cls): """Return all Body members of this class""" - return cls._get_mapping(Body) + return cls._get_mapping(fields.Body) @classmethod def _header_mapping(cls): """Return all Header members of this class""" - return cls._get_mapping(Header) + return cls._get_mapping(fields.Header) @classmethod def _uri_mapping(cls): """Return all URI members of this class""" - return cls._get_mapping(URI) + return cls._get_mapping(fields.URI) @classmethod def _computed_mapping(cls): - """Return all URI members of this class""" - return cls._get_mapping(Computed) + """Return all Computed members of this class""" + return cls._get_mapping(fields.Computed) @classmethod def _alternate_id(cls): @@ -953,7 +855,7 @@ def _alternate_id(cls): consumed by _get_id and passed to getattr. """ for value in cls.__dict__.values(): - if isinstance(value, Body): + if isinstance(value, fields.Body): if value.alternate_id: return value.name return "" @@ -1054,11 +956,11 @@ def to_dict( ): """Return a dictionary of this resource's contents - :param bool body: Include the :class:`~openstack.resource.Body` + :param bool body: Include the :class:`~openstack.fields.Body` attributes in the returned dictionary. - :param bool headers: Include the :class:`~openstack.resource.Header` + :param bool headers: Include the :class:`~openstack.fields.Header` attributes in the returned dictionary. - :param bool computed: Include the :class:`~openstack.resource.Computed` + :param bool computed: Include the :class:`~openstack.fields.Computed` attributes in the returned dictionary. :param bool ignore_none: When True, exclude key/value pairs where the value is None. This will exclude attributes that the server @@ -1077,13 +979,13 @@ def to_dict( else: mapping = {} - components: list[type[_BaseComponent]] = [] + components: list[type[fields._BaseComponent]] = [] if body: - components.append(Body) + components.append(fields.Body) if headers: - components.append(Header) + components.append(fields.Header) if computed: - components.append(Computed) + components.append(fields.Computed) if not components: raise ValueError( "At least one of `body`, `headers` or `computed` must be True" @@ -2050,7 +1952,7 @@ def list( # Known attr hasattr(cls, k) # Is real attr property - and isinstance(getattr(cls, k), Body) + and isinstance(getattr(cls, k), fields.Body) # not included in the query_params and k not in cls._query_mapping._mapping.keys() ): @@ -2063,7 +1965,7 @@ def list( for k, v in params.items(): # We need to gather URI parts to set them on the resource later - if hasattr(cls, k) and isinstance(getattr(cls, k), URI): + if hasattr(cls, k) and isinstance(getattr(cls, k), fields.URI): uri_params[k] = v def _dict_filter(f, d): diff --git a/openstack/test/fakes.py b/openstack/test/fakes.py index 0d70051de..a93a78d1d 100644 --- a/openstack/test/fakes.py +++ b/openstack/test/fakes.py @@ -31,6 +31,7 @@ from unittest import mock import uuid +from openstack import fields from openstack import format as _format from openstack import proxy from openstack import resource @@ -68,9 +69,9 @@ def generate_fake_resource( base_attrs: dict[str, Any] = {} for name, value in inspect.getmembers( resource_type, - predicate=lambda x: isinstance(x, (resource.Body, resource.URI)), + predicate=lambda x: isinstance(x, (fields.Body, fields.URI)), ): - if isinstance(value, resource.Body): + if isinstance(value, fields.Body): target_type = value.type if target_type is None: if ( @@ -128,7 +129,7 @@ def generate_fake_resource( msg = f"Fake value for {resource_type.__name__}.{name} can not be generated" raise NotImplementedError(msg) - if isinstance(value, resource.URI): + if isinstance(value, fields.URI): # For URI we just generate something base_attrs[name] = uuid.uuid4().hex diff --git a/openstack/tests/unit/test_fields.py b/openstack/tests/unit/test_fields.py new file mode 100644 index 000000000..e2a3b111f --- /dev/null +++ b/openstack/tests/unit/test_fields.py @@ -0,0 +1,224 @@ +# 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 fields +from openstack import format +from openstack.tests.unit import base + + +class TestComponent(base.TestCase): + class ExampleComponent(fields._BaseComponent): + key = "_example" + + # Since we're testing ExampleComponent, which is as isolated as we + # can test _BaseComponent due to it's needing to be a data member + # of a class that has an attribute on the parent class named `key`, + # each test has to implement a class with a name that is the same + # as ExampleComponent.key, which should be a dict containing the + # keys and values to test against. + + def test_implementations(self): + self.assertEqual("_body", fields.Body.key) + self.assertEqual("_header", fields.Header.key) + self.assertEqual("_uri", fields.URI.key) + + def test_creation(self): + sot = fields._BaseComponent( + "name", type=int, default=1, alternate_id=True, aka="alias" + ) + + self.assertEqual("name", sot.name) + self.assertEqual(int, sot.type) + self.assertEqual(1, sot.default) + self.assertEqual("alias", sot.aka) + self.assertTrue(sot.alternate_id) + + def test_get_no_instance(self): + sot = fields._BaseComponent("test") + + # Test that we short-circuit everything when given no instance. + result = sot.__get__(None, None) + self.assertIs(sot, result) + + # NOTE: Some tests will use a default=1 setting when testing result + # values that should be None because the default-for-default is also None. + def test_get_name_None(self): + name = "name" + + class Parent: + _example = {name: None} + + instance = Parent() + sot = TestComponent.ExampleComponent(name, default=1) + + # Test that we short-circuit any typing of a None value. + result = sot.__get__(instance, None) + self.assertIsNone(result) + + def test_get_default(self): + expected_result = 123 + + class Parent: + _example = {} + + instance = Parent() + # NOTE: type=dict but the default value is an int. If we didn't + # short-circuit the typing part of __get__ it would fail. + sot = TestComponent.ExampleComponent( + "name", type=dict, default=expected_result + ) + + # Test that we directly return any default value. + result = sot.__get__(instance, None) + self.assertEqual(expected_result, result) + + def test_get_name_untyped(self): + name = "name" + expected_result = 123 + + class Parent: + _example = {name: expected_result} + + instance = Parent() + sot = TestComponent.ExampleComponent("name") + + # Test that we return any the value as it is set. + result = sot.__get__(instance, None) + self.assertEqual(expected_result, result) + + # The code path for typing after a raw value has been found is the same. + def test_get_name_typed(self): + name = "name" + value = "123" + + class Parent: + _example = {name: value} + + instance = Parent() + sot = TestComponent.ExampleComponent("name", type=int) + + # Test that we run the underlying value through type conversion. + result = sot.__get__(instance, None) + self.assertEqual(int(value), result) + + def test_get_name_formatter(self): + name = "name" + value = "123" + expected_result = "one hundred twenty three" + + class Parent: + _example = {name: value} + + class FakeFormatter(format.Formatter): + @classmethod + def deserialize(cls, value): + return expected_result + + instance = Parent() + sot = TestComponent.ExampleComponent("name", type=FakeFormatter) + + # Mock out issubclass rather than having an actual format.Formatter + # This can't be mocked via decorator, isolate it to wrapping the call. + result = sot.__get__(instance, None) + self.assertEqual(expected_result, result) + + def test_set_name_untyped(self): + name = "name" + expected_value = "123" + + class Parent: + _example = {} + + instance = Parent() + sot = TestComponent.ExampleComponent("name") + + # Test that we don't run the value through type conversion. + sot.__set__(instance, expected_value) + self.assertEqual(expected_value, instance._example[name]) + + def test_set_name_typed(self): + expected_value = "123" + + class Parent: + _example = {} + + instance = Parent() + + # The type we give to ExampleComponent has to be an actual type, + # not an instance, so we can't get the niceties of a mock.Mock + # instance that would allow us to call `assert_called_once_with` to + # ensure that we're sending the value through the type. + # Instead, we use this tiny version of a similar thing. + class FakeType: + calls = [] + + def __init__(self, arg): + FakeType.calls.append(arg) + + sot = TestComponent.ExampleComponent("name", type=FakeType) + + # Test that we run the value through type conversion. + sot.__set__(instance, expected_value) + self.assertEqual([expected_value], FakeType.calls) + + def test_set_name_formatter(self): + expected_value = "123" + + class Parent: + _example = {} + + instance = Parent() + + # As with test_set_name_typed, create a pseudo-Mock to track what + # gets called on the type. + class FakeFormatter(format.Formatter): + calls = [] + + @classmethod + def deserialize(cls, arg): + FakeFormatter.calls.append(arg) + + sot = TestComponent.ExampleComponent("name", type=FakeFormatter) + + # Test that we run the value through type conversion. + sot.__set__(instance, expected_value) + self.assertEqual([expected_value], FakeFormatter.calls) + + def test_delete_name(self): + name = "name" + expected_value = "123" + + class Parent: + _example = {name: expected_value} + + instance = Parent() + + sot = TestComponent.ExampleComponent("name") + + sot.__delete__(instance) + + self.assertNotIn(name, instance._example) + + def test_delete_name_doesnt_exist(self): + name = "name" + expected_value = "123" + + class Parent: + _example = {"what": expected_value} + + instance = Parent() + + sot = TestComponent.ExampleComponent(name) + + sot.__delete__(instance) + + self.assertNotIn(name, instance._example) diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 14bf83508..eeeae5a75 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -20,7 +20,7 @@ from openstack import dns from openstack import exceptions -from openstack import format +from openstack import fields from openstack import resource from openstack.tests.unit import base from openstack import utils @@ -37,215 +37,6 @@ def json(self): return self.body -class TestComponent(base.TestCase): - class ExampleComponent(resource._BaseComponent): - key = "_example" - - # Since we're testing ExampleComponent, which is as isolated as we - # can test _BaseComponent due to it's needing to be a data member - # of a class that has an attribute on the parent class named `key`, - # each test has to implement a class with a name that is the same - # as ExampleComponent.key, which should be a dict containing the - # keys and values to test against. - - def test_implementations(self): - self.assertEqual("_body", resource.Body.key) - self.assertEqual("_header", resource.Header.key) - self.assertEqual("_uri", resource.URI.key) - - def test_creation(self): - sot = resource._BaseComponent( - "name", type=int, default=1, alternate_id=True, aka="alias" - ) - - self.assertEqual("name", sot.name) - self.assertEqual(int, sot.type) - self.assertEqual(1, sot.default) - self.assertEqual("alias", sot.aka) - self.assertTrue(sot.alternate_id) - - def test_get_no_instance(self): - sot = resource._BaseComponent("test") - - # Test that we short-circuit everything when given no instance. - result = sot.__get__(None, None) - self.assertIs(sot, result) - - # NOTE: Some tests will use a default=1 setting when testing result - # values that should be None because the default-for-default is also None. - def test_get_name_None(self): - name = "name" - - class Parent: - _example = {name: None} - - instance = Parent() - sot = TestComponent.ExampleComponent(name, default=1) - - # Test that we short-circuit any typing of a None value. - result = sot.__get__(instance, None) - self.assertIsNone(result) - - def test_get_default(self): - expected_result = 123 - - class Parent: - _example = {} - - instance = Parent() - # NOTE: type=dict but the default value is an int. If we didn't - # short-circuit the typing part of __get__ it would fail. - sot = TestComponent.ExampleComponent( - "name", type=dict, default=expected_result - ) - - # Test that we directly return any default value. - result = sot.__get__(instance, None) - self.assertEqual(expected_result, result) - - def test_get_name_untyped(self): - name = "name" - expected_result = 123 - - class Parent: - _example = {name: expected_result} - - instance = Parent() - sot = TestComponent.ExampleComponent("name") - - # Test that we return any the value as it is set. - result = sot.__get__(instance, None) - self.assertEqual(expected_result, result) - - # The code path for typing after a raw value has been found is the same. - def test_get_name_typed(self): - name = "name" - value = "123" - - class Parent: - _example = {name: value} - - instance = Parent() - sot = TestComponent.ExampleComponent("name", type=int) - - # Test that we run the underlying value through type conversion. - result = sot.__get__(instance, None) - self.assertEqual(int(value), result) - - def test_get_name_formatter(self): - name = "name" - value = "123" - expected_result = "one hundred twenty three" - - class Parent: - _example = {name: value} - - class FakeFormatter(format.Formatter): - @classmethod - def deserialize(cls, value): - return expected_result - - instance = Parent() - sot = TestComponent.ExampleComponent("name", type=FakeFormatter) - - # Mock out issubclass rather than having an actual format.Formatter - # This can't be mocked via decorator, isolate it to wrapping the call. - result = sot.__get__(instance, None) - self.assertEqual(expected_result, result) - - def test_set_name_untyped(self): - name = "name" - expected_value = "123" - - class Parent: - _example = {} - - instance = Parent() - sot = TestComponent.ExampleComponent("name") - - # Test that we don't run the value through type conversion. - sot.__set__(instance, expected_value) - self.assertEqual(expected_value, instance._example[name]) - - def test_set_name_typed(self): - expected_value = "123" - - class Parent: - _example = {} - - instance = Parent() - - # The type we give to ExampleComponent has to be an actual type, - # not an instance, so we can't get the niceties of a mock.Mock - # instance that would allow us to call `assert_called_once_with` to - # ensure that we're sending the value through the type. - # Instead, we use this tiny version of a similar thing. - class FakeType: - calls = [] - - def __init__(self, arg): - FakeType.calls.append(arg) - - sot = TestComponent.ExampleComponent("name", type=FakeType) - - # Test that we run the value through type conversion. - sot.__set__(instance, expected_value) - self.assertEqual([expected_value], FakeType.calls) - - def test_set_name_formatter(self): - expected_value = "123" - - class Parent: - _example = {} - - instance = Parent() - - # As with test_set_name_typed, create a pseudo-Mock to track what - # gets called on the type. - class FakeFormatter(format.Formatter): - calls = [] - - @classmethod - def deserialize(cls, arg): - FakeFormatter.calls.append(arg) - - sot = TestComponent.ExampleComponent("name", type=FakeFormatter) - - # Test that we run the value through type conversion. - sot.__set__(instance, expected_value) - self.assertEqual([expected_value], FakeFormatter.calls) - - def test_delete_name(self): - name = "name" - expected_value = "123" - - class Parent: - _example = {name: expected_value} - - instance = Parent() - - sot = TestComponent.ExampleComponent("name") - - sot.__delete__(instance) - - self.assertNotIn(name, instance._example) - - def test_delete_name_doesnt_exist(self): - name = "name" - expected_value = "123" - - class Parent: - _example = {"what": expected_value} - - instance = Parent() - - sot = TestComponent.ExampleComponent(name) - - sot.__delete__(instance) - - self.assertNotIn(name, instance._example) - - class TestComponentManager(base.TestCase): def test_create_basic(self): sot = resource._ComponentManager() @@ -747,16 +538,12 @@ class Test(resource.Resource): self.assertEqual( sorted(['bar', '_bar', 'bar_local', 'id', 'name', 'location']), - sorted( - sot._attributes( - components=tuple([resource.Body, resource.Computed]) - ) - ), + sorted(sot._attributes(components=(fields.Body, fields.Computed))), ) self.assertEqual( ('foo',), - tuple(sot._attributes(components=tuple([resource.Header]))), + tuple(sot._attributes(components=(fields.Header,))), ) def test__attributes_iterator(self): @@ -780,7 +567,7 @@ class Child(Parent): # Check we iterate only over headers for attr, component in sot._attributes_iterator( - components=tuple([resource.Header]) + components=(fields.Header,) ): if attr in expected: expected.remove(attr) From bf1b56e23f1c8cbdd3a2041b09506fb4b7091ed3 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 25 Oct 2024 11:11:49 +0100 Subject: [PATCH 3630/3836] fields: Tweak _convert_type We would like to do a larger rework of this, but for now simply reflow information somewhat to make that larger rework a little more understandable. The main functional change here is the removal of an 'isinstance(value, data_type)' branch that would skip casting of the value if the value was already of the expected type. This is removed since it provides limited value (e.g. int(5) == 5, str('test') == 'test') and would make the future rework slightly more complex. We also add lots of tests to prevent regressions in future changes. Change-Id: Icb6b26127d8e200b650687578f450490e71d24c3 Signed-off-by: Stephen Finucane --- openstack/fields.py | 44 +++++------ openstack/tests/unit/common/test_tag.py | 8 +- openstack/tests/unit/test_fields.py | 101 ++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 26 deletions(-) diff --git a/openstack/fields.py b/openstack/fields.py index ad98ceab0..c73efb10d 100644 --- a/openstack/fields.py +++ b/openstack/fields.py @@ -28,36 +28,35 @@ def _convert_type(value, data_type, list_type=None): # and the RateLimit type for an example. if not data_type: return value - if issubclass(data_type, list): + elif issubclass(data_type, list): if isinstance(value, (list, tuple, set)): if not list_type: - return value - ret = [] - for raw in value: - ret.append(_convert_type(raw, list_type)) - return ret + return data_type(value) + return [_convert_type(raw, list_type) for raw in value] elif list_type: return [_convert_type(value, list_type)] - # "if-match" in Object is a good example of the need here - return [value] + else: + # "if-match" in Object is a good example of the need here + return [value] elif isinstance(value, data_type): return value - if not isinstance(value, data_type): - if issubclass(data_type, format.Formatter): - return data_type.deserialize(value) + elif issubclass(data_type, format.Formatter): + return data_type.deserialize(value) + elif isinstance(value, dict): # This should allow handling sub-dicts that have their own # Component type directly. See openstack/compute/v2/limits.py # and the AbsoluteLimits type for an example. - if isinstance(value, dict): - return data_type(**value) - try: - return data_type(value) - except ValueError: - # If we can not convert data to the expected type return empty - # instance of the expected type. - # This is necessary to handle issues like with flavor.swap where - # empty string means "0". - return data_type() + # NOTE(stephenfin): This will fail if value is not one of a select set + # of types (basically dict or list of two item tuples/lists) + return data_type(**value) + try: + return data_type(value) + except ValueError: + # If we can not convert data to the expected type return empty + # instance of the expected type. + # This is necessary to handle issues like with flavor.swap where + # empty string means "0". + return data_type() class _BaseComponent(abc.ABC): @@ -169,12 +168,13 @@ def __get__(self, instance, owner): if value is None: return None - # This warning are pretty intruisive. Every time attribute is accessed + # This warning are pretty intrusive. Every time attribute is accessed # a warning is being thrown. In neutron clients we have way too many # places that still refer to tenant_id even though they may also # properly support project_id. For now we silence tenant_id warnings. if self.name != "tenant_id": self.warn_if_deprecated_property(value) + return _convert_type(value, self.type, self.list_type) def warn_if_deprecated_property(self, value): diff --git a/openstack/tests/unit/common/test_tag.py b/openstack/tests/unit/common/test_tag.py index 390ef8999..7d9910f3e 100644 --- a/openstack/tests/unit/common/test_tag.py +++ b/openstack/tests/unit/common/test_tag.py @@ -85,7 +85,7 @@ def test_set_tags(self): sess = self.session # Set some initial value to check rewrite - res.tags.extend(['blue_old', 'green_old']) + res.tags = ['blue_old', 'green_old'] result = res.set_tags(sess, ['blue', 'green']) # Check tags attribute is updated @@ -100,7 +100,7 @@ def test_remove_all_tags(self): sess = self.session # Set some initial value to check removal - res.tags.extend(['blue_old', 'green_old']) + res.tags = ['blue_old', 'green_old'] result = res.remove_all_tags(sess) # Check tags attribute is updated @@ -114,7 +114,7 @@ def test_remove_single_tag(self): res = self.sot sess = self.session - res.tags.extend(['blue', 'dummy']) + res.tags = ['blue', 'dummy'] result = res.remove_tag(sess, 'dummy') # Check tags attribute is updated @@ -162,7 +162,7 @@ def test_add_tag(self): sess = self.session # Set some initial value to check add - res.tags.extend(['blue', 'green']) + res.tags = ['blue', 'green'] result = res.add_tag(sess, 'lila') # Check tags attribute is updated diff --git a/openstack/tests/unit/test_fields.py b/openstack/tests/unit/test_fields.py index e2a3b111f..e60be0f66 100644 --- a/openstack/tests/unit/test_fields.py +++ b/openstack/tests/unit/test_fields.py @@ -12,9 +12,110 @@ from openstack import fields from openstack import format +from openstack import resource from openstack.tests.unit import base +class TestConvertValue(base.TestCase): + def test_convert_value(self): + class FakeResource(resource.Resource): + abc = fields.Body('abc', type=int) + + test_data = [ + { + 'name': 'no data_type', + 'value': '123', + 'data_type': None, + 'expected': '123', + }, + { + 'name': 'convert list to list with no list_type', + 'value': ['123'], + 'data_type': list, + 'expected': ['123'], + }, + { + 'name': 'convert tuple to list with no list_type', + 'value': ('123',), + 'data_type': list, + 'expected': ['123'], + }, + { + 'name': 'convert set to list with no list_type', + 'value': {'123'}, + 'data_type': list, + 'expected': ['123'], + }, + { + 'name': 'convert list to list with list_type', + 'value': ['123'], + 'data_type': list, + 'list_type': int, + 'expected': [123], + }, + { + 'name': 'convert tuple to list with list_type', + 'value': ('123',), + 'data_type': list, + 'list_type': int, + 'expected': [123], + }, + { + 'name': 'convert set to list with list_type', + 'value': {'123'}, + 'data_type': list, + 'list_type': int, + 'expected': [123], + }, + { + 'name': 'convert with formatter', + 'value': 'true', + 'data_type': format.BoolStr, + 'expected': True, + }, + { + 'name': 'convert to resource', + 'value': {'abc': '123'}, + 'data_type': FakeResource, + # NOTE(stephenfin): The Resource.__eq__ compares the underlying + # value types, not the converted value types, so we need a + # string here + 'expected': FakeResource(abc='123'), + }, + { + 'name': 'convert string to int', + 'value': '123', + 'data_type': int, + 'expected': 123, + }, + { + 'name': 'convert invalid string to int', + 'value': 'abc', + 'data_type': int, + 'expected': 0, + }, + { + 'name': 'convert valid int to string', + 'value': 123, + 'data_type': str, + 'expected': '123', + }, + { + 'name': 'convert string to bool', + 'value': 'anything', + 'data_type': bool, + 'expected': True, + }, + ] + + for data in test_data: + with self.subTest(msg=data['name']): + ret = fields._convert_type( + data['value'], data['data_type'], data.get('list_type') + ) + self.assertEqual(ret, data['expected']) + + class TestComponent(base.TestCase): class ExampleComponent(fields._BaseComponent): key = "_example" From 1d82105ae11c3d014d648d735815a0b1e9cc8149 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 23 Dec 2024 12:37:43 +0000 Subject: [PATCH 3631/3836] fields: Be more explicit in our conversion This helps massively as we add typing. Change-Id: I1494fecd244429304785507e0d5ae9d45f65b9a8 Signed-off-by: Stephen Finucane --- openstack/fields.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/openstack/fields.py b/openstack/fields.py index c73efb10d..546d41e54 100644 --- a/openstack/fields.py +++ b/openstack/fields.py @@ -40,8 +40,6 @@ def _convert_type(value, data_type, list_type=None): return [value] elif isinstance(value, data_type): return value - elif issubclass(data_type, format.Formatter): - return data_type.deserialize(value) elif isinstance(value, dict): # This should allow handling sub-dicts that have their own # Component type directly. See openstack/compute/v2/limits.py @@ -49,13 +47,28 @@ def _convert_type(value, data_type, list_type=None): # NOTE(stephenfin): This will fail if value is not one of a select set # of types (basically dict or list of two item tuples/lists) return data_type(**value) + elif issubclass(data_type, format.Formatter): + return data_type.deserialize(value) + elif issubclass(data_type, bool): + return data_type(value) + elif issubclass(data_type, (int, float)): + if isinstance(value, (int, float)): + return data_type(value) + if isinstance(value, str): + if issubclass(data_type, int) and value.isdigit(): + return data_type(value) + elif issubclass(data_type, float) and ( + x.isdigit() for x in value.split() + ): + return data_type(value) + return data_type() + + # at this point we expect to have a str and you can convert basically + # anything to a string, but there could be untyped code out there passing + # random monstrosities so we need the try-catch to be safe try: return data_type(value) except ValueError: - # If we can not convert data to the expected type return empty - # instance of the expected type. - # This is necessary to handle issues like with flavor.swap where - # empty string means "0". return data_type() From 6c764f1bc4e2a0f826aa77749f1dac7e807c2e36 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 24 Jul 2024 15:05:35 +0100 Subject: [PATCH 3632/3836] typing: Annotate openstack.format Change-Id: I38184277951e0f8decd16d98d9f3f24a7d878b63 Signed-off-by: Stephen Finucane --- openstack/format.py | 12 ++++++++---- openstack/key_manager/v1/_format.py | 4 ++-- pyproject.toml | 10 ++++++++++ 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/openstack/format.py b/openstack/format.py index 51d92b2d0..0e7c76540 100644 --- a/openstack/format.py +++ b/openstack/format.py @@ -10,17 +10,21 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty -class Formatter: +_T = ty.TypeVar('_T') + + +class Formatter(ty.Generic[_T]): @classmethod - def deserialize(cls, value): + def deserialize(cls, value: ty.Any) -> _T: """Return a formatted object representing the value""" raise NotImplementedError -class BoolStr(Formatter): +class BoolStr(Formatter[bool]): @classmethod - def deserialize(cls, value): + def deserialize(cls, value: ty.Any) -> bool: """Convert a boolean string to a boolean""" expr = str(value).lower() if "true" == expr: diff --git a/openstack/key_manager/v1/_format.py b/openstack/key_manager/v1/_format.py index 74ad58366..58a72d893 100644 --- a/openstack/key_manager/v1/_format.py +++ b/openstack/key_manager/v1/_format.py @@ -15,9 +15,9 @@ from openstack import format -class HREFToUUID(format.Formatter): +class HREFToUUID(format.Formatter[str]): @classmethod - def deserialize(cls, value): + def deserialize(cls, value: str) -> str: """Convert a HREF to the UUID portion""" parts = parse.urlsplit(value) diff --git a/pyproject.toml b/pyproject.toml index b8fed96a1..21880d36d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,16 @@ exclude = ''' ) ''' +[[tool.mypy.overrides]] +module = ["openstack.format"] +warn_return_any = true +disallow_untyped_decorators = true +disallow_any_generics = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_defs = true +no_implicit_reexport = true + [[tool.mypy.overrides]] module = ["openstack.tests.unit.*"] ignore_errors = true From 5ef01accc5a457d5fc065d8325225b875c7baa5f Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 23 Dec 2024 12:39:33 +0000 Subject: [PATCH 3633/3836] typing: Annotate openstack.fields (1/2) This focuses solely on annotating the '_convert_value' helper, on account of the size of the annotations. This size is a side effect of the amount of potential data_type types we can receive, each of which will result in a slightly different return value. These are all called out and hopefully make some sense when taken individually. Change-Id: I812d36142e90a1f0f38619ece76ce772f671acc7 Signed-off-by: Stephen Finucane --- openstack/fields.py | 95 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 83 insertions(+), 12 deletions(-) diff --git a/openstack/fields.py b/openstack/fields.py index 546d41e54..e6bfc8c8d 100644 --- a/openstack/fields.py +++ b/openstack/fields.py @@ -22,31 +22,102 @@ _SEEN_FORMAT = '{name}_seen' -def _convert_type(value, data_type, list_type=None): +_T1 = ty.TypeVar('_T1') +_T2 = ty.TypeVar('_T2') +_T3 = ty.TypeVar('_T3', str, bool, int, float) + + +# case 1: data_type is unset -> return value as-is +@ty.overload +def _convert_type( + value: _T1, + data_type: None, + list_type: None = None, +) -> _T1: ... + + +# case 2: data_type is primitive type -> return value as said primitive type +@ty.overload +def _convert_type( + value: _T1, + data_type: type[_T3], + list_type: None = None, +) -> _T3: ... + + +# case 3: data_type is list, no list_type -> return value as list of whatever +# we got +@ty.overload +def _convert_type( + value: _T1, + data_type: type[list[ty.Any]], + list_type: None = None, +) -> list[_T1]: ... + + +# case 4: data_type is list, list_type is primitive type -> return value as +# list of said primitive type +@ty.overload +def _convert_type( + value: ty.Any, + data_type: type[list[ty.Any]], + list_type: type[_T3], +) -> list[_T3]: ... + + +# case 5: data_type is dict or Resource -> return value as dict/Resource +@ty.overload +def _convert_type( + value: ty.Any, + data_type: type[dict[ty.Any, ty.Any]], + list_type: None = None, +) -> dict[ty.Any, ty.Any]: ... + + +# case 6: data_type is a Formatter -> return value after conversion +@ty.overload +def _convert_type( + value: ty.Any, + data_type: type[format.Formatter[type[_T2]]], + list_type: None = None, +) -> _T2: ... + + +def _convert_type( + value: _T1, + data_type: ty.Optional[ + type[ + ty.Union[ + _T3, + list[ty.Any], + dict[ty.Any, ty.Any], + format.Formatter[_T2], + ], + ] + ], + list_type: ty.Optional[type[_T3]] = None, +) -> ty.Union[_T1, _T3, list[_T3], list[_T1], dict[ty.Any, ty.Any], _T2]: # This should allow handling list of dicts that have their own # Component type directly. See openstack/compute/v2/limits.py # and the RateLimit type for an example. - if not data_type: + if data_type is None: return value elif issubclass(data_type, list): - if isinstance(value, (list, tuple, set)): + if isinstance(value, (list, set, tuple)): if not list_type: return data_type(value) - return [_convert_type(raw, list_type) for raw in value] + return [_convert_type(x, list_type) for x in value] elif list_type: return [_convert_type(value, list_type)] else: - # "if-match" in Object is a good example of the need here return [value] elif isinstance(value, data_type): return value - elif isinstance(value, dict): - # This should allow handling sub-dicts that have their own - # Component type directly. See openstack/compute/v2/limits.py - # and the AbsoluteLimits type for an example. - # NOTE(stephenfin): This will fail if value is not one of a select set - # of types (basically dict or list of two item tuples/lists) - return data_type(**value) + elif issubclass(data_type, dict): + if isinstance(value, dict): + return data_type(**value) + # TODO(stephenfin): This should be a warning/error + return dict() elif issubclass(data_type, format.Formatter): return data_type.deserialize(value) elif issubclass(data_type, bool): From 9629d81a9058b5cb521d95dc13b18040ce2ce10f Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 29 Oct 2024 10:25:21 +0000 Subject: [PATCH 3634/3836] typing: Annotate openstack.fields (2/2) Change-Id: Ic25a63a048040d4a8bd39ceeedc7286ad0d50e7b Signed-off-by: Stephen Finucane --- openstack/fields.py | 109 +++++++++--------- openstack/image/v2/metadef_property.py | 8 +- openstack/resource.py | 99 ++++++++-------- .../v2/share_export_locations.py | 2 +- pyproject.toml | 5 +- 5 files changed, 113 insertions(+), 110 deletions(-) diff --git a/openstack/fields.py b/openstack/fields.py index e6bfc8c8d..68fed3013 100644 --- a/openstack/fields.py +++ b/openstack/fields.py @@ -21,7 +21,6 @@ _SEEN_FORMAT = '{name}_seen' - _T1 = ty.TypeVar('_T1') _T2 = ty.TypeVar('_T2') _T3 = ty.TypeVar('_T3', str, bool, int, float) @@ -145,32 +144,33 @@ def _convert_type( class _BaseComponent(abc.ABC): # The name this component is being tracked as in the Resource - key: str + key: ty.ClassVar[str] # The class to be used for mappings - _map_cls: type[ty.Mapping] = dict - - #: Marks the property as deprecated. - deprecated = False - #: Deprecation reason message used to warn users when deprecated == True - deprecation_reason = None - - #: Control field used to manage the deprecation warning. We want to warn - #: only once when the attribute is retrieved in the code. - already_warned_deprecation = False + _map_cls: ty.ClassVar[type[ty.MutableMapping[str, ty.Any]]] = dict + + name: str + data_type: ty.Optional[ty.Any] + default: ty.Any + alias: ty.Optional[str] + aka: ty.Optional[str] + alternate_id: bool + list_type: ty.Optional[ty.Any] + coerce_to_default: bool + deprecated: bool + deprecation_reason: ty.Optional[str] def __init__( self, - name, - type=None, - default=None, - alias=None, - aka=None, - alternate_id=False, - list_type=None, - coerce_to_default=False, - deprecated=False, - deprecation_reason=None, - **kwargs, + name: str, + type: ty.Optional[ty.Any] = None, + default: ty.Any = None, + alias: ty.Optional[str] = None, + aka: ty.Optional[str] = None, + alternate_id: bool = False, + list_type: ty.Optional[ty.Any] = None, + coerce_to_default: bool = False, + deprecated: bool = False, + deprecation_reason: ty.Optional[str] = None, ): """A typed descriptor for a component that makes up a Resource @@ -183,26 +183,21 @@ def __init__( :param default: Typically None, but any other default can be set. :param alias: If set, alternative attribute on object to return. :param aka: If set, additional name attribute would be available under. - :param alternate_id: - When `True`, this property is known internally as a value that - can be sent with requests that require an ID but when `id` is - not a name the Resource has. This is a relatively uncommon case, - and this setting should only be used once per Resource. - :param list_type: - If type is `list`, list_type designates what the type of the - elements of the list should be. - :param coerce_to_default: - If the Component is None or not present, force the given default - to be used. If a default is not given but a type is given, - construct an empty version of the type in question. - :param deprecated: - Indicates if the option is deprecated. If it is, we display a - warning message to the user. - :param deprecation_reason: - Custom deprecation message. + :param alternate_id: When `True`, this property is known internally as + a value that can be sent with requests that require an ID but when + `id` is not a name the Resource has. This is a relatively uncommon + case, and this setting should only be used once per Resource. + :param list_type: If type is `list`, list_type designates what the type + of the elements of the list should be. + :param coerce_to_default: If the Component is None or not present, + force the given default to be used. If a default is not given but a + type is given, construct an empty version of the type in question. + :param deprecated: Indicates if the option is deprecated. If it is, we + display a warning message to the user. + :param deprecation_reason: Custom deprecation message. """ self.name = name - self.type = type + self.data_type = type if type is not None and coerce_to_default and not default: self.default = type() else: @@ -216,7 +211,11 @@ def __init__( self.deprecated = deprecated self.deprecation_reason = deprecation_reason - def __get__(self, instance, owner): + def __get__( + self, + instance: object, + owner: ty.Optional[type[object]] = None, + ) -> ty.Any: if instance is None: return self @@ -248,7 +247,7 @@ def __get__(self, instance, owner): self.warn_if_deprecated_property(value) return value - # self.type() should not be called on None objects. + # self.data_type() should not be called on None objects. if value is None: return None @@ -259,9 +258,14 @@ def __get__(self, instance, owner): if self.name != "tenant_id": self.warn_if_deprecated_property(value) - return _convert_type(value, self.type, self.list_type) + return _convert_type(value, self.data_type, self.list_type) - def warn_if_deprecated_property(self, value): + @property + def type(self) -> ty.Optional[ty.Any]: + # deprecated alias proxy + return self.data_type + + def warn_if_deprecated_property(self, value: ty.Any) -> None: deprecated = object.__getattribute__(self, 'deprecated') deprecation_reason = object.__getattribute__( self, @@ -275,18 +279,19 @@ def warn_if_deprecated_property(self, value): ), os_warnings.RemovedFieldWarning, ) - return value - def __set__(self, instance, value): + def __set__(self, instance: object, value: ty.Any) -> None: if self.coerce_to_default and value is None: - value = self.default - if value != self.default: - value = _convert_type(value, self.type, self.list_type) + value_ = self.default + elif value != self.default: + value_ = _convert_type(value, self.data_type, self.list_type) + else: + value_ = value attributes = getattr(instance, self.key) - attributes[self.name] = value + attributes[self.name] = value_ - def __delete__(self, instance): + def __delete__(self, instance: object) -> None: try: attributes = getattr(instance, self.key) del attributes[self.name] diff --git a/openstack/image/v2/metadef_property.py b/openstack/image/v2/metadef_property.py index b92216ab9..9abbaacac 100644 --- a/openstack/image/v2/metadef_property.py +++ b/openstack/image/v2/metadef_property.py @@ -52,9 +52,9 @@ class MetadefProperty(resource.Resource): #: that a string value must match. pattern = resource.Body('pattern') #: Minimum allowed string length. - min_length = resource.Body('minLength', type=int, minimum=0, default=0) + min_length = resource.Body('minLength', type=int, default=0) #: Maximum allowed string length. - max_length = resource.Body('maxLength', type=int, minimum=0) + max_length = resource.Body('maxLength', type=int) # FIXME(stephenfin): This is causing conflicts due to the 'dict.items' # method. Perhaps we need to rename it? #: Schema for the items in an array. @@ -64,9 +64,9 @@ class MetadefProperty(resource.Resource): 'uniqueItems', type=bool, default=False ) #: Minimum length of an array. - min_items = resource.Body('minItems', type=int, minimum=0, default=0) + min_items = resource.Body('minItems', type=int, default=0) #: Maximum length of an array. - max_items = resource.Body('maxItems', type=int, minimum=0) + max_items = resource.Body('maxItems', type=int) #: Describes extra items, if you use tuple typing. If the value of #: ``items`` is an array (tuple typing) and the instance is longer than #: the list of schemas in ``items``, the additional items are described by diff --git a/openstack/resource.py b/openstack/resource.py index 5ecceabbd..4a93ceb8e 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -54,19 +54,21 @@ class that represent a remote resource. The attributes that LOG = _log.setup_logging(__name__) +# TODO(stephenfin): We should deprecate the 'type' and 'list_type' arguments +# for all of the below in favour of annotations. To that end, we have stuck +# with Any rather than generating super complex types def Body( - name, - type=None, - default=None, - alias=None, - aka=None, - alternate_id=False, - list_type=None, - coerce_to_default=False, - deprecated=False, - deprecation_reason=None, - **kwargs, -): + name: str, + type: ty.Optional[ty.Any] = None, + default: ty.Any = None, + alias: ty.Optional[str] = None, + aka: ty.Optional[str] = None, + alternate_id: bool = False, + list_type: ty.Optional[ty.Any] = None, + coerce_to_default: bool = False, + deprecated: bool = False, + deprecation_reason: ty.Optional[str] = None, +) -> ty.Any: return fields.Body( name, type=type, @@ -78,23 +80,21 @@ def Body( coerce_to_default=coerce_to_default, deprecated=deprecated, deprecation_reason=deprecation_reason, - **kwargs, ) def Header( - name, - type=None, - default=None, - alias=None, - aka=None, - alternate_id=False, - list_type=None, - coerce_to_default=False, - deprecated=False, - deprecation_reason=None, - **kwargs, -): + name: str, + type: ty.Optional[ty.Any] = None, + default: ty.Any = None, + alias: ty.Optional[str] = None, + aka: ty.Optional[str] = None, + alternate_id: bool = False, + list_type: ty.Optional[ty.Any] = None, + coerce_to_default: bool = False, + deprecated: bool = False, + deprecation_reason: ty.Optional[str] = None, +) -> ty.Any: return fields.Header( name, type=type, @@ -106,23 +106,21 @@ def Header( coerce_to_default=coerce_to_default, deprecated=deprecated, deprecation_reason=deprecation_reason, - **kwargs, ) def URI( - name, - type=None, - default=None, - alias=None, - aka=None, - alternate_id=False, - list_type=None, - coerce_to_default=False, - deprecated=False, - deprecation_reason=None, - **kwargs, -): + name: str, + type: ty.Optional[ty.Any] = None, + default: ty.Any = None, + alias: ty.Optional[str] = None, + aka: ty.Optional[str] = None, + alternate_id: bool = False, + list_type: ty.Optional[ty.Any] = None, + coerce_to_default: bool = False, + deprecated: bool = False, + deprecation_reason: ty.Optional[str] = None, +) -> ty.Any: return fields.URI( name, type=type, @@ -134,23 +132,21 @@ def URI( coerce_to_default=coerce_to_default, deprecated=deprecated, deprecation_reason=deprecation_reason, - **kwargs, ) def Computed( - name, - type=None, - default=None, - alias=None, - aka=None, - alternate_id=False, - list_type=None, - coerce_to_default=False, - deprecated=False, - deprecation_reason=None, - **kwargs, -): + name: str, + type: ty.Optional[ty.Any] = None, + default: ty.Any = None, + alias: ty.Optional[str] = None, + aka: ty.Optional[str] = None, + alternate_id: bool = False, + list_type: ty.Optional[ty.Any] = None, + coerce_to_default: bool = False, + deprecated: bool = False, + deprecation_reason: ty.Optional[str] = None, +) -> ty.Any: return fields.Computed( name, type=type, @@ -162,7 +158,6 @@ def Computed( coerce_to_default=coerce_to_default, deprecated=deprecated, deprecation_reason=deprecation_reason, - **kwargs, ) diff --git a/openstack/shared_file_system/v2/share_export_locations.py b/openstack/shared_file_system/v2/share_export_locations.py index 9e83b7cc7..71eb625c7 100644 --- a/openstack/shared_file_system/v2/share_export_locations.py +++ b/openstack/shared_file_system/v2/share_export_locations.py @@ -31,7 +31,7 @@ class ShareExportLocation(resource.Resource): #: Properties # The share ID, part of the URI for export locations - share_id = resource.URI("share_id", type='str') + share_id = resource.URI("share_id", type=str) #: The path of the export location. path = resource.Body("path", type=str) #: Indicate if export location is preferred. diff --git a/pyproject.toml b/pyproject.toml index 21880d36d..629a97faa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,10 @@ exclude = ''' ''' [[tool.mypy.overrides]] -module = ["openstack.format"] +module = [ + "openstack.fields", + "openstack.format", +] warn_return_any = true disallow_untyped_decorators = true disallow_any_generics = true From c3aaec563755de086b75d9977da4bc4ac02269b9 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 28 Jan 2025 14:52:48 +0000 Subject: [PATCH 3635/3836] typing: Don't rely on kwargs for _translate_response mypy doesn't like it, and it's wholly unnecessary in this instance. Change-Id: I1045711f10a4a7c9053d1a042fbfb8b23399b303 Signed-off-by: Stephen Finucane --- openstack/block_storage/v3/group.py | 2 +- openstack/block_storage/v3/transfer.py | 2 +- openstack/compute/v2/aggregate.py | 2 +- openstack/object_store/v1/info.py | 5 +-- openstack/resource.py | 36 +++++++++-------- openstack/tests/unit/test_resource.py | 55 +++++++++++++++++--------- 6 files changed, 61 insertions(+), 41 deletions(-) diff --git a/openstack/block_storage/v3/group.py b/openstack/block_storage/v3/group.py index 13c9871c9..61f7bde79 100644 --- a/openstack/block_storage/v3/group.py +++ b/openstack/block_storage/v3/group.py @@ -95,5 +95,5 @@ def create_from_source( exceptions.raise_from_response(response) group = Group() - group._translate_response(response=response) + group._translate_response(response) return group diff --git a/openstack/block_storage/v3/transfer.py b/openstack/block_storage/v3/transfer.py index 0081d9413..512802684 100644 --- a/openstack/block_storage/v3/transfer.py +++ b/openstack/block_storage/v3/transfer.py @@ -199,5 +199,5 @@ def accept(self, session, *, auth_key=None): exceptions.raise_from_response(resp) transfer = Transfer() - transfer._translate_response(response=resp) + transfer._translate_response(resp) return transfer diff --git a/openstack/compute/v2/aggregate.py b/openstack/compute/v2/aggregate.py index e3f47ed9b..787d57a09 100644 --- a/openstack/compute/v2/aggregate.py +++ b/openstack/compute/v2/aggregate.py @@ -55,7 +55,7 @@ def _action(self, session, body, microversion=None): response = session.post(url, json=body, microversion=microversion) exceptions.raise_from_response(response) aggregate = Aggregate() - aggregate._translate_response(response=response) + aggregate._translate_response(response) return aggregate def add_host(self, session, host): diff --git a/openstack/object_store/v1/info.py b/openstack/object_store/v1/info.py index 572c1bb66..c1dde93b7 100644 --- a/openstack/object_store/v1/info.py +++ b/openstack/object_store/v1/info.py @@ -87,10 +87,7 @@ def fetch( microversion = self._get_microversion(session, action='fetch') response = session.get(info_url, microversion=microversion) - kwargs = {} - if error_message: - kwargs['error_message'] = error_message self.microversion = microversion - self._translate_response(response, **kwargs) + self._translate_response(response, error_message=error_message) return self diff --git a/openstack/resource.py b/openstack/resource.py index 4a93ceb8e..82a79b94e 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1417,13 +1417,11 @@ def create( ) self.microversion = microversion - response_kwargs = { - "has_body": has_body, - } - if resource_response_key is not None: - response_kwargs['resource_response_key'] = resource_response_key - - self._translate_response(response, **response_kwargs) + self._translate_response( + response, + has_body=has_body, + resource_response_key=resource_response_key, + ) # direct comparision to False since we need to rule out None if self.has_body and self.create_returns_body is False: # fetch the body if it's required but not returned by create @@ -1583,23 +1581,25 @@ def fetch( requires_id=requires_id, base_path=base_path, ) + session = self._get_session(session) if microversion is None: microversion = self._get_microversion(session, action='fetch') + self.microversion = microversion + response = session.get( request.url, microversion=microversion, params=params, skip_cache=skip_cache, ) - kwargs = {} - if error_message: - kwargs['error_message'] = error_message - self.microversion = microversion - if resource_response_key is not None: - kwargs['resource_response_key'] = resource_response_key - self._translate_response(response, **kwargs) + self._translate_response( + response, + error_message=error_message, + resource_response_key=resource_response_key, + ) + return self def head(self, session, base_path=None, *, microversion=None): @@ -1623,12 +1623,13 @@ def head(self, session, base_path=None, *, microversion=None): session = self._get_session(session) if microversion is None: microversion = self._get_microversion(session, action='fetch') + self.microversion = microversion request = self._prepare_request(base_path=base_path) response = session.head(request.url, microversion=microversion) - self.microversion = microversion self._translate_response(response, has_body=False) + return self @property @@ -1736,6 +1737,7 @@ def _commit( ) self.microversion = microversion + self._translate_response(response, has_body=has_body) return self @@ -1856,7 +1858,9 @@ def delete( if error_message: kwargs['error_message'] = error_message - self._translate_response(response, has_body=False, **kwargs) + self._translate_response( + response, has_body=False, error_message=error_message + ) return self def _raw_delete(self, session, microversion=None, **kwargs): diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index eeeae5a75..21fb4f46a 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1419,11 +1419,10 @@ def _test_create( ) self.assertEqual(sot.microversion, microversion) - res_kwargs = {} - if resource_response_key is not None: - res_kwargs['resource_response_key'] = resource_response_key sot._translate_response.assert_called_once_with( - self.response, has_body=sot.has_body, **res_kwargs + self.response, + has_body=sot.has_body, + resource_response_key=resource_response_key, ) self.assertEqual(result, sot) @@ -1574,7 +1573,9 @@ def test_fetch(self): ) self.assertIsNone(self.sot.microversion) - self.sot._translate_response.assert_called_once_with(self.response) + self.sot._translate_response.assert_called_once_with( + self.response, error_message=None, resource_response_key=None + ) self.assertEqual(result, self.sot) def test_fetch_with_override_key(self): @@ -1589,7 +1590,7 @@ def test_fetch_with_override_key(self): self.assertIsNone(self.sot.microversion) self.sot._translate_response.assert_called_once_with( - self.response, resource_response_key="SomeKey" + self.response, error_message=None, resource_response_key="SomeKey" ) self.assertEqual(result, self.sot) @@ -1607,7 +1608,9 @@ def test_fetch_with_params(self): ) self.assertIsNone(self.sot.microversion) - self.sot._translate_response.assert_called_once_with(self.response) + self.sot._translate_response.assert_called_once_with( + self.response, error_message=None, resource_response_key=None + ) self.assertEqual(result, self.sot) def test_fetch_with_microversion(self): @@ -1631,7 +1634,9 @@ class Test(resource.Resource): ) self.assertEqual(sot.microversion, '1.42') - sot._translate_response.assert_called_once_with(self.response) + sot._translate_response.assert_called_once_with( + self.response, error_message=None, resource_response_key=None + ) self.assertEqual(result, sot) def test_fetch_with_explicit_microversion(self): @@ -1655,7 +1660,9 @@ class Test(resource.Resource): ) self.assertEqual(sot.microversion, '1.42') - sot._translate_response.assert_called_once_with(self.response) + sot._translate_response.assert_called_once_with( + self.response, error_message=None, resource_response_key=None + ) self.assertEqual(result, sot) def test_fetch_not_requires_id(self): @@ -1668,7 +1675,9 @@ def test_fetch_not_requires_id(self): self.request.url, microversion=None, params={}, skip_cache=False ) - self.sot._translate_response.assert_called_once_with(self.response) + self.sot._translate_response.assert_called_once_with( + self.response, error_message=None, resource_response_key=None + ) self.assertEqual(result, self.sot) def test_fetch_base_path(self): @@ -1681,7 +1690,9 @@ def test_fetch_base_path(self): self.request.url, microversion=None, params={}, skip_cache=False ) - self.sot._translate_response.assert_called_once_with(self.response) + self.sot._translate_response.assert_called_once_with( + self.response, error_message=None, resource_response_key=None + ) self.assertEqual(result, self.sot) def test_head(self): @@ -1694,7 +1705,8 @@ def test_head(self): self.assertIsNone(self.sot.microversion) self.sot._translate_response.assert_called_once_with( - self.response, has_body=False + self.response, + has_body=False, ) self.assertEqual(result, self.sot) @@ -1708,7 +1720,8 @@ def test_head_base_path(self): self.assertIsNone(self.sot.microversion) self.sot._translate_response.assert_called_once_with( - self.response, has_body=False + self.response, + has_body=False, ) self.assertEqual(result, self.sot) @@ -1732,7 +1745,8 @@ class Test(resource.Resource): self.assertEqual(sot.microversion, '1.42') sot._translate_response.assert_called_once_with( - self.response, has_body=False + self.response, + has_body=False, ) self.assertEqual(result, sot) @@ -1796,7 +1810,8 @@ def _test_commit( self.assertEqual(self.sot.microversion, microversion) self.sot._translate_response.assert_called_once_with( - self.response, has_body=has_body + self.response, + has_body=has_body, ) def test_commit_put(self): @@ -1937,7 +1952,9 @@ def test_delete(self): ) self.sot._translate_response.assert_called_once_with( - self.response, has_body=False + self.response, + has_body=False, + error_message=None, ) self.assertEqual(result, self.sot) @@ -1960,7 +1977,9 @@ class Test(resource.Resource): ) sot._translate_response.assert_called_once_with( - self.response, has_body=False + self.response, + has_body=False, + error_message=None, ) self.assertEqual(result, sot) @@ -1983,7 +2002,7 @@ class Test(resource.Resource): ) sot._translate_response.assert_called_once_with( - self.response, has_body=False + self.response, has_body=False, error_message=None ) self.assertEqual(result, sot) From 56e0692f56d270ab9addce763ad634db2d9418ed Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 18 Feb 2025 19:18:20 +0000 Subject: [PATCH 3636/3836] typing: Annotate openstack.exceptions Change-Id: I3511b256a44ee6f83f4b41d741196749714a0ca2 Signed-off-by: Stephen Finucane --- openstack/exceptions.py | 53 ++++++++++++------- .../cloud/test_qos_bandwidth_limit_rule.py | 2 +- .../cloud/test_qos_dscp_marking_rule.py | 2 +- .../cloud/test_qos_minimum_bandwidth_rule.py | 2 +- pyproject.toml | 1 + 5 files changed, 39 insertions(+), 21 deletions(-) diff --git a/openstack/exceptions.py b/openstack/exceptions.py index cf7b9d3c3..a1419052e 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -18,14 +18,21 @@ import json import re +import typing as ty +import requests from requests import exceptions as _rex +if ty.TYPE_CHECKING: + from openstack import resource + class SDKException(Exception): """The base exception class for all exceptions this library raises.""" - def __init__(self, message=None, extra_data=None): + def __init__( + self, message: ty.Optional[str] = None, extra_data: ty.Any = None + ): self.message = self.__class__.__name__ if message is None else message self.extra_data = extra_data super().__init__(self.message) @@ -34,35 +41,37 @@ def __init__(self, message=None, extra_data=None): class EndpointNotFound(SDKException): """A mismatch occurred between what the client and server expect.""" - def __init__(self, message=None): + def __init__(self, message: ty.Optional[str] = None): super().__init__(message) class InvalidResponse(SDKException): """The response from the server is not valid for this request.""" - def __init__(self, response): - super().__init__() - self.response = response + def __init__(self, message: ty.Optional[str] = None): + super().__init__(message) class InvalidRequest(SDKException): """The request to the server is not valid.""" - def __init__(self, message=None): + def __init__(self, message: ty.Optional[str] = None): super().__init__(message) class HttpException(SDKException, _rex.HTTPError): """The base exception for all HTTP error responses.""" + source: str + status_code: ty.Optional[int] + def __init__( self, - message='Error', - response=None, - http_status=None, - details=None, - request_id=None, + message: ty.Optional[str] = 'Error', + response: ty.Optional[requests.Response] = None, + http_status: ty.Optional[int] = None, + details: ty.Optional[str] = None, + request_id: ty.Optional[str] = None, ): # TODO(shade) Remove http_status parameter and the ability for response # to be None once we're not mocking Session everywhere. @@ -89,7 +98,7 @@ def __init__( if self.status_code is not None and (400 <= self.status_code < 500): self.source = "Client" - def __str__(self): + def __str__(self) -> str: # 'Error' is the default value for self.message. If self.message isn't # 'Error', then someone has set a more informative error message # and we should use it. If it is 'Error', then we should construct a @@ -129,7 +138,11 @@ class PreconditionFailedException(HttpException): class MethodNotSupported(SDKException): """The resource does not support this operation type.""" - def __init__(self, resource, method): + def __init__( + self, + resource: ty.Union['resource.Resource', type['resource.Resource']], + method: str, + ): # This needs to work with both classes and instances. try: name = resource.__name__ @@ -156,14 +169,14 @@ class InvalidResourceQuery(SDKException): """Invalid query params for resource.""" -def _extract_message(obj): +def _extract_message(obj: ty.Any) -> ty.Optional[str]: if isinstance(obj, dict): # Most of services: compute, network if obj.get('message'): - return obj['message'] + return str(obj['message']) # Ironic starting with Stein elif obj.get('faultstring'): - return obj['faultstring'] + return str(obj['faultstring']) elif isinstance(obj, str): # Ironic before Stein has double JSON encoding, nobody remembers why. try: @@ -172,9 +185,13 @@ def _extract_message(obj): pass else: return _extract_message(obj) + return None -def raise_from_response(response, error_message=None): +def raise_from_response( + response: requests.Response, + error_message: ty.Optional[str] = None, +) -> None: """Raise an instance of an HTTPException based on keystoneauth response.""" if response.status_code < 400: return @@ -219,7 +236,7 @@ def raise_from_response(response, error_message=None): messages.append(message) # Return joined string separated by colons. - details = ': '.join(messages) + details = ': '.join(msg for msg in messages if msg) if not details: details = response.reason if response.reason else response.text diff --git a/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py b/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py index 09334afad..ab7e8b700 100644 --- a/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/functional/cloud/test_qos_bandwidth_limit_rule.py @@ -41,7 +41,7 @@ def _cleanup_qos_policy(self): try: self.operator_cloud.delete_qos_policy(self.policy['id']) except Exception as e: - raise exceptions.SDKException(e) + raise exceptions.SDKException(str(e)) def test_qos_bandwidth_limit_rule_lifecycle(self): max_kbps = 1500 diff --git a/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py b/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py index ee6fba631..d7c19a932 100644 --- a/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py +++ b/openstack/tests/functional/cloud/test_qos_dscp_marking_rule.py @@ -41,7 +41,7 @@ def _cleanup_qos_policy(self): try: self.operator_cloud.delete_qos_policy(self.policy['id']) except Exception as e: - raise exceptions.SDKException(e) + raise exceptions.SDKException(str(e)) def test_qos_dscp_marking_rule_lifecycle(self): dscp_mark = 16 diff --git a/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py b/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py index 8da1ff651..834e2a216 100644 --- a/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py +++ b/openstack/tests/functional/cloud/test_qos_minimum_bandwidth_rule.py @@ -41,7 +41,7 @@ def _cleanup_qos_policy(self): try: self.operator_cloud.delete_qos_policy(self.policy['id']) except Exception as e: - raise exceptions.SDKException(e) + raise exceptions.SDKException(str(e)) def test_qos_minimum_bandwidth_rule_lifecycle(self): min_kbps = 1500 diff --git a/pyproject.toml b/pyproject.toml index 629a97faa..8773aabbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ exclude = ''' [[tool.mypy.overrides]] module = [ + "openstack.exceptions", "openstack.fields", "openstack.format", ] From 8d26eeaeb444c0e28114f607da6136ae23054ea1 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 19 Feb 2025 10:51:48 +0000 Subject: [PATCH 3637/3836] typing: Annotate openstack.utils We need to include some ignores while we wait for typed keystoneauth to be released. Change-Id: I489400b3a34c2a6fa00d6210ac38b7f07ce780b3 Signed-off-by: Stephen Finucane --- openstack/compute/v2/server.py | 2 +- openstack/tests/unit/test_utils.py | 25 +-- openstack/utils.py | 300 ++++++++++++++++------------- pyproject.toml | 4 +- 4 files changed, 188 insertions(+), 143 deletions(-) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index a5720cbb2..5ecf09898 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -1022,7 +1022,7 @@ def fetch_topology(self, session): :param session: The session to use for making this request. :returns: None """ - utils.require_microversion(session, 2.78) + utils.require_microversion(session, '2.78') url = utils.urljoin(Server.base_path, self.id, 'topology') diff --git a/openstack/tests/unit/test_utils.py b/openstack/tests/unit/test_utils.py index 347ee50f0..ee193b0e7 100644 --- a/openstack/tests/unit/test_utils.py +++ b/openstack/tests/unit/test_utils.py @@ -244,25 +244,28 @@ class TestTinyDAG(base.TestCase): 'g': ['e'], } + @classmethod + def _create_tinydag(cls, data): + sot = utils.TinyDAG() + for k, v in data.items(): + sot.add_node(k) + for dep in v: + sot.add_edge(k, dep) + return sot + def _verify_order(self, test_graph, test_list): for k, v in test_graph.items(): for dep in v: self.assertTrue(test_list.index(k) < test_list.index(dep)) - def test_from_dict(self): - sot = utils.TinyDAG() - sot.from_dict(self.test_graph) - def test_topological_sort(self): - sot = utils.TinyDAG() - sot.from_dict(self.test_graph) + sot = self._create_tinydag(self.test_graph) sorted_list = sot.topological_sort() self._verify_order(sot.graph, sorted_list) self.assertEqual(len(self.test_graph.keys()), len(sorted_list)) def test_walk(self): - sot = utils.TinyDAG() - sot.from_dict(self.test_graph) + sot = self._create_tinydag(self.test_graph) sorted_list = [] for node in sot.walk(): sorted_list.append(node) @@ -271,8 +274,7 @@ def test_walk(self): self.assertEqual(len(self.test_graph.keys()), len(sorted_list)) def test_walk_parallel(self): - sot = utils.TinyDAG() - sot.from_dict(self.test_graph) + sot = self._create_tinydag(self.test_graph) sorted_list = [] with concurrent.futures.ThreadPoolExecutor(max_workers=15) as executor: for node in sot.walk(timeout=1): @@ -281,8 +283,7 @@ def test_walk_parallel(self): self.assertEqual(len(self.test_graph.keys()), len(sorted_list)) def test_walk_raise(self): - sot = utils.TinyDAG() - sot.from_dict(self.test_graph) + sot = self._create_tinydag(self.test_graph) bad_node = 'f' with testtools.ExpectedException(exceptions.SDKException): for node in sot.walk(timeout=1): diff --git a/openstack/utils.py b/openstack/utils.py index 06a9ad6d0..62d21db4f 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -10,8 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. -from collections.abc import Mapping +import collections.abc import hashlib +import io import queue import string import threading @@ -26,7 +27,7 @@ from openstack import exceptions -def urljoin(*args): +def urljoin(*args: ty.Optional[str]) -> str: """A custom version of urljoin that simply joins strings into a path. The real urljoin takes into account web semantics like when joining a url @@ -90,14 +91,16 @@ def iterate_timeout( class _AccessSaver: __slots__ = ('keys',) - def __init__(self): - self.keys = [] + def __init__(self) -> None: + self.keys: list[str] = [] - def __getitem__(self, key): + def __getitem__(self, key: str) -> None: self.keys.append(key) -def get_string_format_keys(fmt_string, old_style=True): +def get_string_format_keys( + fmt_string: str, old_style: bool = True +) -> list[str]: """Gets a list of required keys from a format string Required mostly for parsing base_path urls for required keys, which @@ -135,20 +138,36 @@ def supports_version( :raises: :class:`~openstack.exceptions.SDKException` when ``raise_exception`` is ``True`` and requested version is not supported. """ - required = discover.normalize_version_number(version) - if discover.version_match(required, adapter.get_api_major_version()): + def _supports_version() -> bool: + required = discover.normalize_version_number(version) + major_version = adapter.get_api_major_version() + + if not major_version: + return False + + if not discover.version_match(required, major_version): + return False + return True - if raise_exception: + supported = _supports_version() + + if not supported and raise_exception: raise exceptions.SDKException( f'Required version {version} is not supported by the server' ) - return False + return supported -def supports_microversion(adapter, microversion, raise_exception=False): +def supports_microversion( + adapter: ks_adapter.Adapter, + microversion: ty.Union[ + str, int, float, ty.Iterable[ty.Union[str, int, float]] + ], + raise_exception: bool = False, +) -> bool: """Determine if the given adapter supports the given microversion. Checks the min and max microversion asserted by the service and ensures @@ -156,17 +175,21 @@ def supports_microversion(adapter, microversion, raise_exception=False): taken into consideration to ensure ``microversion <= default``. :param adapter: :class:`~keystoneauth1.adapter.Adapter` instance. - :param str microversion: String containing the desired microversion. - :param bool raise_exception: Raise exception when requested microversion + :param microversion: String containing the desired microversion. + :param raise_exception: Raise exception when requested microversion is not supported by the server or is higher than the current default microversion. :returns: True if the service supports the microversion, else False. - :rtype: bool :raises: :class:`~openstack.exceptions.SDKException` when ``raise_exception`` is ``True`` and requested microversion is not supported. """ endpoint_data = adapter.get_endpoint_data() + if endpoint_data is None: + if raise_exception: + raise exceptions.SDKException('Could not retrieve endpoint data') + return False + if ( endpoint_data.min_microversion and endpoint_data.max_microversion @@ -189,17 +212,20 @@ def supports_microversion(adapter, microversion, raise_exception=False): f'Required microversion {microversion} is higher than ' f'currently selected {adapter.default_microversion}' ) - return supports + return supports # type: ignore[no-any-return] + return True + if raise_exception: raise exceptions.SDKException( f'Required microversion {microversion} is not supported ' f'by the server side' ) + return False -def require_microversion(adapter, required): +def require_microversion(adapter: ks_adapter.Adapter, required: str) -> None: """Require microversion. :param adapter: :class:`~keystoneauth1.adapter.Adapter` instance. @@ -210,13 +236,13 @@ def require_microversion(adapter, required): supports_microversion(adapter, required, raise_exception=True) -def pick_microversion(session, required): +def pick_microversion( + session: ks_adapter.Adapter, required: str +) -> ty.Optional[str]: """Get a new microversion if it is higher than session's default. :param session: The session to use for making this request. - :type session: :class:`~keystoneauth1.adapter.Adapter` :param required: Minimum version that is required for an action. - :type required: String or tuple or None. :return: ``required`` as a string if the ``session``'s default is too low, otherwise the ``session``'s default. Returns ``None`` if both are ``None``. @@ -224,33 +250,39 @@ def pick_microversion(session, required): :raises: :class:`~openstack.exceptions.SDKException` if requested microversion is not supported. """ + required_normalized = None if required is not None: - required = discover.normalize_version_number(required) + required_normalized = discover.normalize_version_number(required) if session.default_microversion is not None: default = discover.normalize_version_number( session.default_microversion ) - if required is None: - required = default + if required_normalized is None: + required_normalized = default else: - required = ( + required_normalized = ( default - if discover.version_match(required, default) - else required + if discover.version_match(required_normalized, default) + else required_normalized ) - if required is not None: - if not supports_microversion(session, required): - raise exceptions.SDKException( - 'Requested microversion is not supported by the server side ' - 'or the default microversion is too low' - ) - return discover.version_to_string(required) + if required_normalized is None: + return None + + if not supports_microversion(session, required_normalized): + raise exceptions.SDKException( + 'Requested microversion is not supported by the server side ' + 'or the default microversion is too low' + ) + return discover.version_to_string(required_normalized) # type: ignore[no-any-return] -def maximum_supported_microversion(adapter, client_maximum): +def maximum_supported_microversion( + adapter: ks_adapter.Adapter, + client_maximum: ty.Optional[str], +) -> ty.Optional[str]: """Determine the maximum microversion supported by both client and server. :param adapter: :class:`~keystoneauth1.adapter.Adapter` instance. @@ -295,10 +327,15 @@ def maximum_supported_microversion(adapter, client_maximum): return None result = min(client_max, server_max) - return discover.version_to_string(result) + return discover.version_to_string(result) # type: ignore[no-any-return] -def _hashes_up_to_date(md5, sha256, md5_key, sha256_key): +def _hashes_up_to_date( + md5: ty.Optional[str], + sha256: ty.Optional[str], + md5_key: str, + sha256_key: str, +) -> bool: """Compare md5 and sha256 hashes for being up to date md5 and sha256 are the current values. @@ -316,26 +353,34 @@ def _hashes_up_to_date(md5, sha256, md5_key, sha256_key): return up_to_date -def _calculate_data_hashes(data): +def _calculate_data_hashes( + data: ty.Union[io.BufferedReader, bytes], +) -> tuple[str, str]: _md5 = hashlib.md5(usedforsecurity=False) _sha256 = hashlib.sha256() - if hasattr(data, 'read'): + if isinstance(data, io.BufferedIOBase): for chunk in iter(lambda: data.read(8192), b''): _md5.update(chunk) _sha256.update(chunk) - else: + elif isinstance(data, bytes): _md5.update(data) _sha256.update(data) - return (_md5.hexdigest(), _sha256.hexdigest()) + else: + raise TypeError( + 'unsupported type for data; expected IO stream or bytes; got ' + '{type(data)}' + ) + + return _md5.hexdigest(), _sha256.hexdigest() -def _get_file_hashes(filename): - (_md5, _sha256) = (None, None) +def _get_file_hashes(filename: str) -> tuple[str, str]: + _md5, _sha256 = (None, None) with open(filename, 'rb') as file_obj: - (_md5, _sha256) = _calculate_data_hashes(file_obj) + _md5, _sha256 = _calculate_data_hashes(file_obj) - return (_md5, _sha256) + return _md5, _sha256 class TinyDAG: @@ -345,45 +390,36 @@ class TinyDAG: (parallel execution of the workflow items). """ - def __init__(self, data=None): + def __init__(self) -> None: self._reset() self._lock = threading.Lock() - if data and isinstance(data, dict): - self.from_dict(data) - def _reset(self): - self._graph = dict() + def _reset(self) -> None: + self._graph: dict[str, set[str]] = {} self._wait_timeout = 120 @property - def graph(self): + def graph(self) -> dict[str, set[str]]: """Get graph as adjacency dict""" return self._graph - def add_node(self, node): + def add_node(self, node: str) -> None: self._graph.setdefault(node, set()) - def add_edge(self, u, v): + def add_edge(self, u: str, v: str) -> None: self._graph[u].add(v) - def from_dict(self, data): - self._reset() - for k, v in data.items(): - self.add_node(k) - for dep in v: - self.add_edge(k, dep) - - def walk(self, timeout=None): + def walk(self, timeout: ty.Optional[int] = None) -> 'TinyDAG': """Start the walking from the beginning.""" if timeout: self._wait_timeout = timeout return self - def __iter__(self): + def __iter__(self) -> 'TinyDAG': self._start_traverse() return self - def __next__(self): + def __next__(self) -> str: # Start waiting if it is expected to get something # (counting down from graph length to 0). if self._it_cnt > 0: @@ -399,7 +435,7 @@ def __next__(self): else: raise StopIteration - def node_done(self, node): + def node_done(self, node: str) -> None: """Mark node as "processed" and put following items into the queue""" self._done.add(node) @@ -408,18 +444,18 @@ def node_done(self, node): if self._run_in_degree[v] == 0: self._queue.put(v) - def _start_traverse(self): + def _start_traverse(self) -> None: """Initialize graph traversing""" self._run_in_degree = self._get_in_degree() self._queue: queue.Queue[str] = queue.Queue() - self._done = set() + self._done: set[str] = set() self._it_cnt = len(self._graph) for k, v in self._run_in_degree.items(): if v == 0: self._queue.put(k) - def _get_in_degree(self): + def _get_in_degree(self) -> dict[str, int]: """Calculate the in_degree (count incoming) for nodes""" _in_degree: dict[str, int] = {u: 0 for u in self._graph.keys()} for u in self._graph: @@ -428,7 +464,7 @@ def _get_in_degree(self): return _in_degree - def topological_sort(self): + def topological_sort(self) -> list[str]: """Return the graph nodes in the topological order""" result = [] for node in self: @@ -437,24 +473,24 @@ def topological_sort(self): return result - def size(self): + def size(self) -> int: return len(self._graph.keys()) - def is_complete(self): + def is_complete(self) -> bool: return len(self._done) == self.size() # Importing Munch is a relatively expensive operation (0.3s) while we do not # really even need much of it. Before we can rework all places where we rely on # it we can have a reduced version. -class Munch(dict): +class Munch(dict[str, ty.Any]): """A slightly stripped version of munch.Munch class""" - def __init__(self, *args, **kwargs): + def __init__(self, *args: ty.Any, **kwargs: ty.Any): self.update(*args, **kwargs) # only called if k not found in normal places - def __getattr__(self, k): + def __getattr__(self, k: str) -> ty.Any: """Gets key if it exists, otherwise throws AttributeError.""" try: return object.__getattribute__(self, k) @@ -464,7 +500,7 @@ def __getattr__(self, k): except KeyError: raise AttributeError(k) - def __setattr__(self, k, v): + def __setattr__(self, k: str, v: ty.Any) -> None: """Sets attribute k if it exists, otherwise sets key k. A KeyError raised by set-item (only likely if you subclass Munch) will propagate as an AttributeError instead. @@ -480,7 +516,7 @@ def __setattr__(self, k, v): else: object.__setattr__(self, k, v) - def __delattr__(self, k): + def __delattr__(self, k: str) -> None: """Deletes attribute k if it exists, otherwise deletes key k. A KeyError raised by deleting the key - such as when the key is missing @@ -497,43 +533,83 @@ def __delattr__(self, k): else: object.__delattr__(self, k) - def toDict(self): + def toDict(self) -> dict[str, ty.Any]: """Recursively converts a munch back into a dictionary.""" return unmunchify(self) @property - def __dict__(self): + def __dict__(self) -> dict[str, ty.Any]: # type: ignore[override] return self.toDict() - def __repr__(self): + def __repr__(self) -> str: """Invertible* string-form of a Munch.""" return f'{self.__class__.__name__}({dict.__repr__(self)})' - def __dir__(self): + def __dir__(self) -> list[str]: return list(self.keys()) - def __getstate__(self): + def __getstate__(self) -> dict[str, ty.Any]: """Implement a serializable interface used for pickling. See https://docs.python.org/3.6/library/pickle.html. """ return {k: v for k, v in self.items()} - def __setstate__(self, state): + def __setstate__(self, state: dict[str, ty.Any]) -> None: """Implement a serializable interface used for pickling. See https://docs.python.org/3.6/library/pickle.html. """ self.clear() self.update(state) + # TODO(stephenfin): This needs to be stricter in the types that it will + # accept. By limiting it to the primitive types (or subclasses of same) we + # should cover everything we (sdk) care about and will be able to type the + # results. @classmethod - def fromDict(cls, d): + def fromDict(cls, d: dict[str, ty.Any]) -> 'Munch': """Recursively transforms a dictionary into a Munch via copy.""" - return munchify(d, cls) + # Munchify x, using `seen` to track object cycles + seen: dict[int, ty.Any] = dict() + + def munchify_cycles(obj: ty.Any) -> ty.Any: + try: + return seen[id(obj)] + except KeyError: + pass + + seen[id(obj)] = partial = pre_munchify(obj) + return post_munchify(partial, obj) + + def pre_munchify(obj: ty.Any) -> ty.Any: + if isinstance(obj, collections.abc.Mapping): + return cls({}) + elif isinstance(obj, list): + return type(obj)() + elif isinstance(obj, tuple): + type_factory = getattr(obj, "_make", type(obj)) + return type_factory(munchify_cycles(item) for item in obj) + else: + return obj + + def post_munchify(partial: ty.Any, obj: ty.Any) -> ty.Any: + if isinstance(obj, collections.abc.Mapping): + partial.update( + (k, munchify_cycles(obj[k])) for k in obj.keys() + ) + elif isinstance(obj, list): + partial.extend(munchify_cycles(item) for item in obj) + elif isinstance(obj, tuple): + for item_partial, item in zip(partial, obj): + post_munchify(item_partial, item) + + return partial + + return ty.cast('Munch', munchify_cycles(d)) - def copy(self): - return type(self).fromDict(self) + def copy(self) -> 'Munch': + return self.fromDict(self) - def update(self, *args, **kwargs): + def update(self, *args: ty.Any, **kwargs: ty.Any) -> None: """ Override built-in method to call custom __setitem__ method that may be defined in subclasses. @@ -541,7 +617,7 @@ def update(self, *args, **kwargs): for k, v in dict(*args, **kwargs).items(): self[k] = v - def get(self, k, d=None): + def get(self, k: str, d: ty.Any = None) -> ty.Any: """ D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None. """ @@ -549,7 +625,7 @@ def get(self, k, d=None): return d return self[k] - def setdefault(self, k, d=None): + def setdefault(self, k: str, d: ty.Any = None) -> ty.Any: """ D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D """ @@ -558,52 +634,18 @@ def setdefault(self, k, d=None): return self[k] -def munchify(x, factory=Munch): +def munchify(x: dict[str, ty.Any], factory: type[Munch] = Munch) -> Munch: """Recursively transforms a dictionary into a Munch via copy.""" - # Munchify x, using `seen` to track object cycles - seen: dict[int, ty.Any] = dict() - - def munchify_cycles(obj): - try: - return seen[id(obj)] - except KeyError: - pass - - seen[id(obj)] = partial = pre_munchify(obj) - return post_munchify(partial, obj) - - def pre_munchify(obj): - if isinstance(obj, Mapping): - return factory({}) - elif isinstance(obj, list): - return type(obj)() - elif isinstance(obj, tuple): - type_factory = getattr(obj, "_make", type(obj)) - return type_factory(munchify_cycles(item) for item in obj) - else: - return obj - - def post_munchify(partial, obj): - if isinstance(obj, Mapping): - partial.update((k, munchify_cycles(obj[k])) for k in obj.keys()) - elif isinstance(obj, list): - partial.extend(munchify_cycles(item) for item in obj) - elif isinstance(obj, tuple): - for item_partial, item in zip(partial, obj): - post_munchify(item_partial, item) - - return partial - - return munchify_cycles(x) + return Munch.fromDict(x) -def unmunchify(x): +def unmunchify(x: Munch) -> dict[str, ty.Any]: """Recursively converts a Munch into a dictionary.""" # Munchify x, using `seen` to track object cycles seen: dict[int, ty.Any] = dict() - def unmunchify_cycles(obj): + def unmunchify_cycles(obj: ty.Any) -> ty.Any: try: return seen[id(obj)] except KeyError: @@ -612,8 +654,8 @@ def unmunchify_cycles(obj): seen[id(obj)] = partial = pre_unmunchify(obj) return post_unmunchify(partial, obj) - def pre_unmunchify(obj): - if isinstance(obj, Mapping): + def pre_unmunchify(obj: ty.Any) -> ty.Any: + if isinstance(obj, collections.abc.Mapping): return dict() elif isinstance(obj, list): return type(obj)() @@ -623,8 +665,8 @@ def pre_unmunchify(obj): else: return obj - def post_unmunchify(partial, obj): - if isinstance(obj, Mapping): + def post_unmunchify(partial: ty.Any, obj: ty.Any) -> ty.Any: + if isinstance(obj, collections.abc.Mapping): partial.update((k, unmunchify_cycles(obj[k])) for k in obj.keys()) elif isinstance(obj, list): partial.extend(unmunchify_cycles(v) for v in obj) @@ -634,4 +676,4 @@ def post_unmunchify(partial, obj): return partial - return unmunchify_cycles(x) + return ty.cast(dict[str, ty.Any], unmunchify_cycles(x)) diff --git a/pyproject.toml b/pyproject.toml index 8773aabbd..7de016796 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,8 @@ show_error_context = true ignore_missing_imports = true follow_imports = "normal" check_untyped_defs = true -warn_unused_ignores = true +# TODO(stephenfin): Remove this when typed keystoneauth1 (5.10.0?) is released +warn_unused_ignores = false # many of the following are false while we incrementally add typing warn_return_any = false warn_unused_configs = true @@ -33,6 +34,7 @@ module = [ "openstack.exceptions", "openstack.fields", "openstack.format", + "openstack.utils", ] warn_return_any = true disallow_untyped_decorators = true From c5a2e3d4fcb82a97172bcefa60261de1a71cc3d3 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 19 Feb 2025 12:35:49 +0000 Subject: [PATCH 3638/3836] volume: Remove _base_proxy module There is not enough here to justify a different layout to every other service, and it confuses mypy. Remove it. Change-Id: I0d640992f638c27c8d4e95cec449406b8fce51a7 Signed-off-by: Stephen Finucane --- openstack/block_storage/_base_proxy.py | 58 -------------------------- openstack/block_storage/v2/_proxy.py | 49 +++++++++++++++++++++- openstack/block_storage/v3/_proxy.py | 47 ++++++++++++++++++++- 3 files changed, 92 insertions(+), 62 deletions(-) delete mode 100644 openstack/block_storage/_base_proxy.py diff --git a/openstack/block_storage/_base_proxy.py b/openstack/block_storage/_base_proxy.py deleted file mode 100644 index cf8271d19..000000000 --- a/openstack/block_storage/_base_proxy.py +++ /dev/null @@ -1,58 +0,0 @@ -# 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 - -from openstack import exceptions -from openstack import proxy - - -class BaseBlockStorageProxy(proxy.Proxy, metaclass=abc.ABCMeta): - def create_image( - self, - name, - volume, - allow_duplicates, - container_format, - disk_format, - wait, - timeout, - ): - if not disk_format: - disk_format = self._connection.config.config['image_format'] - if not container_format: - # https://docs.openstack.org/image-guide/image-formats.html - container_format = 'bare' - - if 'id' in volume: - volume_id = volume['id'] - else: - volume_obj = self.get_volume(volume) - if not volume_obj: - raise exceptions.SDKException( - f"Volume {volume} given to create_image could not be found" - ) - volume_id = volume_obj['id'] - data = self.post( - f'/volumes/{volume_id}/action', - json={ - 'os-volume_upload_image': { - 'force': allow_duplicates, - 'image_name': name, - 'container_format': container_format, - 'disk_format': disk_format, - } - }, - ) - response = self._connection._get_and_munchify( - 'os-volume_upload_image', data - ) - return self._connection.image._existing_image(id=response['image_id']) diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 8bba0b935..2ae9b5642 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -13,7 +13,6 @@ import typing as ty import warnings -from openstack.block_storage import _base_proxy from openstack.block_storage.v2 import backup as _backup from openstack.block_storage.v2 import capabilities as _capabilities from openstack.block_storage.v2 import extension as _extension @@ -24,12 +23,14 @@ from openstack.block_storage.v2 import stats as _stats from openstack.block_storage.v2 import type as _type from openstack.block_storage.v2 import volume as _volume +from openstack import exceptions from openstack.identity.v3 import project as _project +from openstack import proxy from openstack import resource from openstack import warnings as os_warnings -class Proxy(_base_proxy.BaseBlockStorageProxy): +class Proxy(proxy.Proxy): # ========== Extensions ========== def extensions(self): @@ -40,6 +41,50 @@ def extensions(self): """ return self._list(_extension.Extension) + # ========== Images ========== + + # TODO(stephenfin): Convert to use resources/proxy rather than direct calls + def create_image( + self, + name, + volume, + allow_duplicates, + container_format, + disk_format, + wait, + timeout, + ): + if not disk_format: + disk_format = self._connection.config.config['image_format'] + if not container_format: + # https://docs.openstack.org/image-guide/image-formats.html + container_format = 'bare' + + if 'id' in volume: + volume_id = volume['id'] + else: + volume_obj = self.get_volume(volume) + if not volume_obj: + raise exceptions.SDKException( + f"Volume {volume} given to create_image could not be found" + ) + volume_id = volume_obj['id'] + data = self.post( + f'/volumes/{volume_id}/action', + json={ + 'os-volume_upload_image': { + 'force': allow_duplicates, + 'image_name': name, + 'container_format': container_format, + 'disk_format': disk_format, + } + }, + ) + response = self._connection._get_and_munchify( + 'os-volume_upload_image', data + ) + return self._connection.image._existing_image(id=response['image_id']) + # ========== Snapshots ========== def get_snapshot(self, snapshot): diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index f6f0cbee5..962011619 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -13,7 +13,6 @@ import typing as ty import warnings -from openstack.block_storage import _base_proxy from openstack.block_storage.v3 import attachment as _attachment from openstack.block_storage.v3 import availability_zone from openstack.block_storage.v3 import backup as _backup @@ -36,12 +35,13 @@ from openstack.block_storage.v3 import volume as _volume from openstack import exceptions from openstack.identity.v3 import project as _project +from openstack import proxy from openstack import resource from openstack import utils from openstack import warnings as os_warnings -class Proxy(_base_proxy.BaseBlockStorageProxy): +class Proxy(proxy.Proxy): _resource_registry = { "availability_zone": availability_zone.AvailabilityZone, "attachment": _attachment.Attachment, @@ -62,6 +62,49 @@ class Proxy(_base_proxy.BaseBlockStorageProxy): "volume": _volume.Volume, } + # ====== IMAGES ====== + # TODO(stephenfin): Convert to use resources/proxy rather than direct calls + def create_image( + self, + name, + volume, + allow_duplicates, + container_format, + disk_format, + wait, + timeout, + ): + if not disk_format: + disk_format = self._connection.config.config['image_format'] + if not container_format: + # https://docs.openstack.org/image-guide/image-formats.html + container_format = 'bare' + + if 'id' in volume: + volume_id = volume['id'] + else: + volume_obj = self.get_volume(volume) + if not volume_obj: + raise exceptions.SDKException( + f"Volume {volume} given to create_image could not be found" + ) + volume_id = volume_obj['id'] + data = self.post( + f'/volumes/{volume_id}/action', + json={ + 'os-volume_upload_image': { + 'force': allow_duplicates, + 'image_name': name, + 'container_format': container_format, + 'disk_format': disk_format, + } + }, + ) + response = self._connection._get_and_munchify( + 'os-volume_upload_image', data + ) + return self._connection.image._existing_image(id=response['image_id']) + # ====== SNAPSHOTS ====== def get_snapshot(self, snapshot): """Get a single snapshot From 4d30f3fc26392d3e38cfaabf73e7bc30138b0443 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 28 Jan 2025 16:16:42 +0000 Subject: [PATCH 3639/3836] network: Set If-Match header properly Deprecate the neutron-specific path in favour of a more generic header. Change-Id: Idf14a520fb8f1a897093566bd233c059cb1e3063 Signed-off-by: Stephen Finucane --- openstack/network/v2/_base.py | 9 +++++---- openstack/network/v2/_proxy.py | 18 ++++++++++++++++-- .../tests/unit/cloud/test_security_groups.py | 2 ++ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/openstack/network/v2/_base.py b/openstack/network/v2/_base.py index 7858eeef3..7acc05f8d 100644 --- a/openstack/network/v2/_base.py +++ b/openstack/network/v2/_base.py @@ -20,6 +20,10 @@ class NetworkResource(resource.Resource): #: Revision number of the resource. *Type: int* revision_number = resource.Body('revision_number', type=int) + # Headers for HEAD and GET requests + #: See http://www.ietf.org/rfc/rfc2616.txt. + if_match = resource.Header("if-match", type=list) + _allow_unknown_attrs_in_body = True def _prepare_request( @@ -29,8 +33,6 @@ def _prepare_request( patch=False, base_path=None, params=None, - *, - if_revision=None, **kwargs, ): req = super()._prepare_request( @@ -39,9 +41,8 @@ def _prepare_request( patch=patch, base_path=base_path, params=params, + **kwargs, ) - if if_revision is not None: - req.headers['If-Match'] = f"revision_number={if_revision}" return req diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 52de4dd65..6994f6f35 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -13,6 +13,7 @@ import typing as ty from openstack import exceptions +from openstack.network.v2 import _base from openstack.network.v2 import address_group as _address_group from openstack.network.v2 import address_scope as _address_scope from openstack.network.v2 import agent as _agent @@ -201,8 +202,15 @@ def _update( if_revision: ty.Optional[int] = None, **attrs: ty.Any, ) -> resource.Resource: + if ( + issubclass(resource_type, _base.NetworkResource) + and if_revision is not None + ): + attrs.update({'if_match': f'revision_number={if_revision}'}) + res = self._get_resource(resource_type, value, **attrs) - return res.commit(self, base_path=base_path, if_revision=if_revision) + + return res.commit(self, base_path=base_path) @proxy._check_resource(strict=False) def _delete( @@ -213,10 +221,16 @@ def _delete( if_revision: ty.Optional[int] = None, **attrs: ty.Any, ) -> ty.Optional[resource.Resource]: + if ( + issubclass(resource_type, _base.NetworkResource) + and if_revision is not None + ): + attrs.update({'if_match': f'revision_number={if_revision}'}) + res = self._get_resource(resource_type, value, **attrs) try: - rv = res.delete(self, if_revision=if_revision) + rv = res.delete(self) except exceptions.NotFoundException: if ignore_missing: return None diff --git a/openstack/tests/unit/cloud/test_security_groups.py b/openstack/tests/unit/cloud/test_security_groups.py index 745d59300..98bf9b388 100644 --- a/openstack/tests/unit/cloud/test_security_groups.py +++ b/openstack/tests/unit/cloud/test_security_groups.py @@ -488,6 +488,7 @@ def test_create_security_group_rule_neutron(self): new_rule.pop('revision_number') new_rule.pop('tags') new_rule.pop('updated_at') + new_rule.pop('if-match') self.assertEqual(expected_new_rule, new_rule) self.assert_calls() @@ -551,6 +552,7 @@ def test_create_security_group_rule_neutron_specific_tenant(self): new_rule.pop('revision_number') new_rule.pop('tags') new_rule.pop('updated_at') + new_rule.pop('if-match') self.assertEqual(expected_new_rule, new_rule) self.assert_calls() From b3e195199745b37558ec45d7f267c4a1aab9404f Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 19 Feb 2025 12:20:58 +0000 Subject: [PATCH 3640/3836] typing: Adapt to typed keystoneauth Change-Id: I0a1bc9e660e8265f19b6a0ba862f5ee6a6c3a8d6 Signed-off-by: Stephen Finucane --- openstack/block_storage/v3/service.py | 2 +- openstack/config/loader.py | 12 ++++++++++-- openstack/connection.py | 2 +- openstack/identity/v3/_proxy.py | 3 ++- openstack/object_store/v1/_proxy.py | 8 +++++++- openstack/proxy.py | 13 ++++++++++++- openstack/resource.py | 5 ++++- 7 files changed, 37 insertions(+), 8 deletions(-) diff --git a/openstack/block_storage/v3/service.py b/openstack/block_storage/v3/service.py index 438e1f717..871c51f9a 100644 --- a/openstack/block_storage/v3/service.py +++ b/openstack/block_storage/v3/service.py @@ -153,7 +153,7 @@ def failover( body['backend_id'] = backend_id action = 'failover_host' - if utils.supports_microversion(self, '3.26'): + if utils.supports_microversion(session, '3.26'): action = 'failover' return self._action(session, action, body) diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 991d64200..8bd0e86f9 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -37,8 +37,11 @@ from openstack import exceptions from openstack import warnings as os_warnings +if ty.TYPE_CHECKING: + from keystoneauth1.loading._plugins.identity import v3 as v3_loaders + PLATFORMDIRS = platformdirs.PlatformDirs( - 'openstack', 'OpenStack', multipath='/etc' + 'openstack', 'OpenStack', multipath=True ) CONFIG_HOME = PLATFORMDIRS.user_config_dir CACHE_PATH = PLATFORMDIRS.user_cache_dir @@ -1061,7 +1064,9 @@ def _get_auth_loader(self, config): # doing what they want causes them sorrow. config['auth_type'] = 'admin_token' - loader = loading.get_plugin_loader(config['auth_type']) + loader: loading.BaseLoader[ty.Any] = loading.get_plugin_loader( + config['auth_type'] + ) # As the name would suggest, v3multifactor uses multiple factors for # authentication. As a result, we need to register the configuration @@ -1074,6 +1079,9 @@ def _get_auth_loader(self, config): # options in keystoneauth1.loading._plugins.identity.v3.MultiAuth # without calling 'load_from_options'. if config['auth_type'] == 'v3multifactor': + if ty.TYPE_CHECKING: + # narrow types + assert isinstance(loader, v3_loaders.MultiFactor) # We use '.get' since we can't be sure this key is set yet - # validation happens later, in _validate_auth loader._methods = config.get('auth_methods') diff --git a/openstack/connection.py b/openstack/connection.py index a0eecac6e..8a8e8bc4a 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -561,7 +561,7 @@ def authorize(self): try: return self.session.get_token() except keystoneauth1.exceptions.ClientException as e: - raise exceptions.SDKException(e) + raise exceptions.SDKException(str(e)) def connect_as(self, **kwargs): """Make a new Connection object with new auth context. diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 26a2e5347..cd437ab3c 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -378,7 +378,8 @@ def find_endpoint(self, name_or_id, ignore_missing=True): _endpoint.Endpoint, name_or_id, ignore_missing=ignore_missing ) - def get_endpoint(self, endpoint): + # TODO(stephenfin): This conflicts with Adapter.get_endpoint + def get_endpoint(self, endpoint): # type: ignore[override] """Get a single endpoint :param endpoint: The value can be the ID of an endpoint or a diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index 8e3b3439e..5191801f2 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -926,7 +926,13 @@ def generate_form_signature( res = self._get_resource(_container.Container, container) endpoint = parse.urlparse(self.get_endpoint()) - path = '/'.join([endpoint.path, res.name, object_prefix]) + if isinstance(endpoint.path, bytes): + # To keep mypy happy: the output type will be the same as the input + # type + path = endpoint.path.decode() + else: + path = endpoint.path + path = '/'.join([path, res.name, object_prefix]) data = f'{path}\n{redirect_url}\n{max_file_size}\n{max_upload_count}\n{expires}' sig = hmac.new(temp_url_key, data.encode(), sha1).hexdigest() diff --git a/openstack/proxy.py b/openstack/proxy.py index 7cfe0252d..3dcfd22fb 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -32,6 +32,9 @@ from openstack import utils from openstack import warnings as os_warnings +if ty.TYPE_CHECKING: + from openstack import connection + ResourceType = ty.TypeVar('ResourceType', bound=resource.Resource) @@ -89,6 +92,8 @@ class Proxy(adapter.Adapter): Dictionary of resource names (key) types (value). """ + _connection: 'connection.Connection' + def __init__( self, session, @@ -106,7 +111,9 @@ def __init__( kwargs.setdefault( 'retriable_status_codes', self.retriable_status_codes ) - super().__init__(session=session, *args, **kwargs) + # TODO(stephenfin): Resolve this by copying the signature of + # adapter.Adapter.__init__ + super().__init__(session=session, *args, **kwargs) # type: ignore self._statsd_client = statsd_client self._statsd_prefix = statsd_prefix self._prometheus_counter = prometheus_counter @@ -889,6 +896,10 @@ def should_skip_resource_cleanup(self, resource=None, skip_resources=None): if resource is None or skip_resources is None: return False + if self.service_type is None: + # to keep mypy happy - this should never happen + return False + resource_name = f"{self.service_type.replace('-', '_')}.{resource}" if resource_name in skip_resources: diff --git a/openstack/resource.py b/openstack/resource.py index 82a79b94e..de204f1e7 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -2235,9 +2235,12 @@ def find( # Try to short-circuit by looking directly for a matching ID. try: + # TODO(stephenfin): Our types say we accept a ksa Adapter, but this + # requires an SDK Proxy. Do we update the types or rework this to + # support use of an adapter. match = cls.existing( id=name_or_id, - connection=session._get_connection(), + connection=session._get_connection(), # type: ignore **params, ) return match.fetch(session, microversion=microversion, **params) From f546d61cf8436738a7617b6daa86d3420848c099 Mon Sep 17 00:00:00 2001 From: Vasyl Saienko Date: Mon, 24 Feb 2025 07:28:45 +0000 Subject: [PATCH 3641/3836] Fix baremetal get_node_console Fix name for calling function in Node object. Partial-Bug: #2099872 Change-Id: If4e70c180972991a65dc47599d6baa2aa6c935c2 --- openstack/baremetal/v1/_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 3a51cc9c7..5a86066e6 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -785,7 +785,7 @@ def get_node_console(self, node): :returns: Connection information for the console. """ res = self._get_resource(_node.Node, node) - return res.get_node_console(self) + return res.get_console(self) def enable_node_console(self, node): """Enable the console for a node. From 37bb57f4d54ba886c447b1ef8f4c86546b374929 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 26 May 2022 11:51:40 +0100 Subject: [PATCH 3642/3836] cloud: Switch remaining image functions to proxy The 'filters' parameter of 'get_image' is deprecated since it doesn't do anything. Change-Id: Ib8086b252f72a3e4dbc343be1a56c08a6d22920d --- openstack/cloud/_image.py | 23 ++++- openstack/tests/unit/base.py | 11 ++- openstack/tests/unit/cloud/test_image.py | 91 +++++++++++++------ .../tests/unit/cloud/test_image_snapshot.py | 26 ++++-- 4 files changed, 112 insertions(+), 39 deletions(-) diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py index d521c80e9..4c85837b7 100644 --- a/openstack/cloud/_image.py +++ b/openstack/cloud/_image.py @@ -10,10 +10,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +import warnings + from openstack.cloud import _utils from openstack.cloud import openstackcloud from openstack import exceptions from openstack import utils +from openstack import warnings as os_warnings class ImageCloudMixin(openstackcloud._OpenStackCloudMixin): @@ -69,10 +72,26 @@ def get_image(self, name_or_id, filters=None): OR A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - :returns: An image :class:`openstack.image.v2.image.Image` object. """ - return _utils._get_entity(self, 'image', name_or_id, filters) + if filters is not None: + warnings.warn( + "The 'filters' argument is deprecated; use 'search_images' " + "instead", + os_warnings.RemovedInSDK60Warning, + ) + entities = self.search_images(name_or_id, filters) + if not entities: + return None + + if len(entities) > 1: + raise exceptions.SDKException( + f"Multiple matches found for {name_or_id}", + ) + + return entities[0] + + return self.image.find_image(name_or_id) def get_image_by_id(self, id): """Get a image by ID diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index a2e85f22c..9347c9a84 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -914,8 +914,15 @@ def assert_calls(self, stop_after=None, do_count=True): len(self.calls), len(self.adapter.request_history), "Expected:\n{}'\nGot:\n{}".format( - '\n'.join([c['url'] for c in self.calls]), - '\n'.join([h.url for h in self.adapter.request_history]), + '\n'.join( + [f'{c["method"]} {c["url"]}' for c in self.calls] + ), + '\n'.join( + [ + f'{h.method} {h.url}' + for h in self.adapter.request_history + ] + ), ), ) diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 07748031c..aaec255ee 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -19,7 +19,6 @@ import uuid from openstack.cloud import meta -from openstack import connection from openstack import exceptions from openstack.image.v1 import image as image_v1 from openstack.image.v2 import image @@ -155,17 +154,17 @@ def test_download_image_with_path(self): self.assertEqual(output_file.read(), self.output) self.assert_calls() - @mock.patch.object(connection.Connection, 'search_images') - def test_get_images(self, mock_search): + @mock.patch('openstack.image.v2._proxy.Proxy.find_image') + def test_get_images(self, mock_find): image1 = dict(id='123', name='mickey') - mock_search.return_value = [image1] + mock_find.return_value = image1 r = self.cloud.get_image('mickey') self.assertIsNotNone(r) self.assertDictEqual(image1, r) - @mock.patch.object(connection.Connection, 'search_images') - def test_get_image_not_found(self, mock_search): - mock_search.return_value = [] + @mock.patch('openstack.image.v2._proxy.Proxy.find_image') + def test_get_image_not_found(self, mock_find): + mock_find.return_value = None r = self.cloud.get_image('doesNotExist') self.assertIsNone(r) @@ -509,10 +508,12 @@ def test_create_image_put_v2_no_import(self): dict( method='GET', uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2' + 'image', + append=['images', self.fake_image_dict['id']], + base_url_append='v2', ), complete_qs=True, - json=self.fake_search_return, + json=self.fake_image_dict, ), ] ) @@ -616,10 +617,12 @@ def test_create_image_put_v2_import_supported(self): dict( method='GET', uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2' + 'image', + append=['images', self.fake_image_dict['id']], + base_url_append='v2', ), complete_qs=True, - json=self.fake_search_return, + json=self.fake_image_dict, ), ] ) @@ -732,10 +735,12 @@ def test_create_image_use_import(self): dict( method='GET', uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2' + 'image', + append=['images', self.fake_image_dict['id']], + base_url_append='v2', ), complete_qs=True, - json=self.fake_search_return, + json=self.fake_image_dict, ), ] ) @@ -976,10 +981,12 @@ def test_create_image_task(self): dict( method='GET', uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2' + 'image', + append=['images', self.image_id], + base_url_append='v2', ), complete_qs=True, - json=self.fake_search_return, + json=image_no_checksums, ), ] ) @@ -1023,9 +1030,11 @@ def test_delete_image_task(self): dict( method='GET', uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2' + 'image', + append=['images', self.image_id], + base_url_append='v2', ), - json=self.fake_search_return, + json=self.fake_image_dict, ), dict( method='DELETE', @@ -1212,7 +1221,7 @@ def test_create_image_put_v1(self): dict( method='PUT', uri=f'https://image.example.com/v1/images/{self.image_id}', - json={'image': ret}, + json=ret, validate=dict( headers={ 'x-image-meta-checksum': fakes.NO_MD5, @@ -1225,6 +1234,16 @@ def test_create_image_put_v1(self): uri='https://image.example.com/v1/images/detail', json={'images': [ret]}, ), + dict( + method='GET', + uri=self.get_mock_url( + 'image', + append=['images', self.image_id], + base_url_append='v1', + ), + complete_qs=True, + json=ret, + ), ] ) self._call_create_image(self.image_name) @@ -1567,9 +1586,13 @@ def test_create_image_put_user_int(self): ), dict( method='GET', - uri='https://image.example.com/v2/images', + uri=self.get_mock_url( + 'image', + append=['images', self.image_id], + base_url_append='v2', + ), complete_qs=True, - json={'images': [ret]}, + json=ret, ), ] ) @@ -1655,9 +1678,13 @@ def test_create_image_put_meta_int(self): ), dict( method='GET', - uri='https://image.example.com/v2/images', + uri=self.get_mock_url( + 'image', + append=['images', self.image_id], + base_url_append='v2', + ), complete_qs=True, - json={'images': [ret]}, + json=ret, ), ] ) @@ -1744,9 +1771,13 @@ def test_create_image_put_protected(self): ), dict( method='GET', - uri='https://image.example.com/v2/images', + uri=self.get_mock_url( + 'image', + append=['images', self.image_id], + base_url_append='v2', + ), complete_qs=True, - json={'images': [ret]}, + json=ret, ), ] ) @@ -1864,9 +1895,11 @@ def test_create_image_volume(self): dict( method='GET', uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2' + 'image', + append=['images', self.image_id], + base_url_append='v2', ), - json=self.fake_search_return, + json=self.fake_image_dict, ), ] ) @@ -1913,9 +1946,11 @@ def test_create_image_volume_duplicate(self): dict( method='GET', uri=self.get_mock_url( - 'image', append=['images'], base_url_append='v2' + 'image', + append=['images', self.image_id], + base_url_append='v2', ), - json=self.fake_search_return, + json=self.fake_image_dict, ), ] ) diff --git a/openstack/tests/unit/cloud/test_image_snapshot.py b/openstack/tests/unit/cloud/test_image_snapshot.py index a0d582015..5d7c64c63 100644 --- a/openstack/tests/unit/cloud/test_image_snapshot.py +++ b/openstack/tests/unit/cloud/test_image_snapshot.py @@ -31,7 +31,7 @@ def setUp(self): def test_create_image_snapshot_wait_until_active_never_active(self): snapshot_name = 'test-snapshot' - fake_image = fakes.make_fake_image(self.image_id, status='pending') + pending_image = fakes.make_fake_image(self.image_id, status='pending') self.register_uris( [ self.get_nova_discovery_mock_dict(), @@ -56,8 +56,12 @@ def test_create_image_snapshot_wait_until_active_never_active(self): self.get_glance_discovery_mock_dict(), dict( method='GET', - uri='https://image.example.com/v2/images', - json=dict(images=[fake_image]), + uri=self.get_mock_url( + 'image', + append=['images', self.image_id], + base_url_append='v2', + ), + json=pending_image, ), ] ) @@ -103,13 +107,21 @@ def test_create_image_snapshot_wait_active(self): self.get_glance_discovery_mock_dict(), dict( method='GET', - uri='https://image.example.com/v2/images', - json=dict(images=[pending_image]), + uri=self.get_mock_url( + 'image', + append=['images', self.image_id], + base_url_append='v2', + ), + json=pending_image, ), dict( method='GET', - uri='https://image.example.com/v2/images', - json=dict(images=[fake_image]), + uri=self.get_mock_url( + 'image', + append=['images', self.image_id], + base_url_append='v2', + ), + json=fake_image, ), ] ) From e4a09ed8a469c88fb34f45d54e6e6b28b5231efa Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 24 Feb 2025 15:21:09 +0000 Subject: [PATCH 3643/3836] cloud: Switch remaining block storage functions to proxy There are a couple of changes we want to make here: - None of the 'get' functions should allow specifying additional filters as this is the responsibility of the 'search' functionality. We deprecated these. - A couple of parameters are no longer used. These are deprecated for removal. Change-Id: I2c9a54f9dd7fa49f2422c5270bb860dbce2aac1e Signed-off-by: Stephen Finucane --- openstack/cloud/_block_storage.py | 76 ++++++++++++++++- .../unit/cloud/test_delete_volume_snapshot.py | 16 ++-- openstack/tests/unit/cloud/test_volume.py | 16 ++-- .../tests/unit/cloud/test_volume_access.py | 84 ++++++++++++++++++- .../tests/unit/cloud/test_volume_backups.py | 36 +++++++- 5 files changed, 205 insertions(+), 23 deletions(-) diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index 5e6c5fe02..6d1df090f 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -69,7 +69,24 @@ def get_volume(self, name_or_id, filters=None): :returns: A volume ``Volume`` object if found, else None. """ - return _utils._get_entity(self, 'volume', name_or_id, filters) + if filters is not None: + warnings.warn( + "The 'filters' argument is deprecated; use " + "'search_volumes' instead", + os_warnings.RemovedInSDK60Warning, + ) + entities = self.search_volumes(name_or_id, filters) + if not entities: + return None + + if len(entities) > 1: + raise exceptions.SDKException( + f"Multiple matches found for {name_or_id}", + ) + + return entities[0] + + return self.block_storage.find_volume(name_or_id) def get_volume_by_id(self, id): """Get a volume by ID @@ -99,7 +116,24 @@ def get_volume_type(self, name_or_id, filters=None): :returns: A volume ``Type`` object if found, else None. """ - return _utils._get_entity(self, 'volume_type', name_or_id, filters) + if filters is not None: + warnings.warn( + "The 'filters' argument is deprecated; use " + "'search_volume_types' instead", + os_warnings.RemovedInSDK60Warning, + ) + entities = self.search_volume_types(name_or_id, filters) + if not entities: + return None + + if len(entities) > 1: + raise exceptions.SDKException( + f"Multiple matches found for {name_or_id}", + ) + + return entities[0] + + return self.block_storage.find_type(name_or_id) def create_volume( self, @@ -481,7 +515,24 @@ def get_volume_snapshot(self, name_or_id, filters=None): :returns: A volume ``Snapshot`` object if found, else None. """ - return _utils._get_entity(self, 'volume_snapshot', name_or_id, filters) + if filters is not None: + warnings.warn( + "The 'filters' argument is deprecated; use " + "'search_volume_snapshots' instead", + os_warnings.RemovedInSDK60Warning, + ) + entities = self.search_volume_snapshots(name_or_id, filters) + if not entities: + return None + + if len(entities) > 1: + raise exceptions.SDKException( + f"Multiple matches found for {name_or_id}", + ) + + return entities[0] + + return self.block_storage.find_snapshot(name_or_id) def create_volume_backup( self, @@ -551,7 +602,24 @@ def get_volume_backup(self, name_or_id, filters=None): :returns: A volume ``Backup`` object if found, else None. """ - return _utils._get_entity(self, 'volume_backup', name_or_id, filters) + if filters is not None: + warnings.warn( + "The 'filters' argument is deprecated; use " + "'search_volume_backups' instead", + os_warnings.RemovedInSDK60Warning, + ) + entities = self.search_volume_backups(name_or_id, filters) + if not entities: + return None + + if len(entities) > 1: + raise exceptions.SDKException( + f"Multiple matches found for {name_or_id}", + ) + + return entities[0] + + return self.block_storage.find_backup(name_or_id) def list_volume_snapshots(self, detailed=True, filters=None): """List all volume snapshots. diff --git a/openstack/tests/unit/cloud/test_delete_volume_snapshot.py b/openstack/tests/unit/cloud/test_delete_volume_snapshot.py index 6e1c543a8..86860c199 100644 --- a/openstack/tests/unit/cloud/test_delete_volume_snapshot.py +++ b/openstack/tests/unit/cloud/test_delete_volume_snapshot.py @@ -43,9 +43,11 @@ def test_delete_volume_snapshot(self): dict( method='GET', uri=self.get_mock_url( - 'volumev3', 'public', append=['snapshots', 'detail'] + 'volumev3', + 'public', + append=['snapshots', fake_snapshot.id], ), - json={'snapshots': [fake_snapshot_dict]}, + json={'snapshot': fake_snapshot_dict}, ), dict( method='DELETE', @@ -77,9 +79,11 @@ def test_delete_volume_snapshot_with_error(self): dict( method='GET', uri=self.get_mock_url( - 'volumev3', 'public', append=['snapshots', 'detail'] + 'volumev3', + 'public', + append=['snapshots', fake_snapshot.id], ), - json={'snapshots': [fake_snapshot_dict]}, + json={'snapshot': fake_snapshot_dict}, ), dict( method='DELETE', @@ -115,7 +119,9 @@ def test_delete_volume_snapshot_with_timeout(self): dict( method='GET', uri=self.get_mock_url( - 'volumev3', 'public', append=['snapshots', 'detail'] + 'volumev3', + 'public', + append=['snapshots', fake_snapshot.id], ), json={'snapshots': [fake_snapshot_dict]}, ), diff --git a/openstack/tests/unit/cloud/test_volume.py b/openstack/tests/unit/cloud/test_volume.py index 44ba20831..0c56ef405 100644 --- a/openstack/tests/unit/cloud/test_volume.py +++ b/openstack/tests/unit/cloud/test_volume.py @@ -371,9 +371,9 @@ def test_detach_volume_wait(self): dict( method='GET', uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', 'detail'] + 'volumev3', 'public', append=['volumes', volume.id] ), - json={'volumes': [avail_volume]}, + json={'volume': avail_volume}, ), ] ) @@ -420,9 +420,9 @@ def test_detach_volume_wait_error(self): dict( method='GET', uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', 'detail'] + 'volumev3', 'public', append=['volumes', volume.id] ), - json={'volumes': [errored_volume]}, + json={'volume': errored_volume}, ), dict( method='GET', @@ -565,9 +565,9 @@ def test_set_volume_bootable(self): dict( method='GET', uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', 'detail'] + 'volumev3', 'public', append=['volumes', volume.id] ), - json={'volumes': [volume]}, + json={'volume': volume}, ), dict( method='POST', @@ -597,9 +597,9 @@ def test_set_volume_bootable_false(self): dict( method='GET', uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', 'detail'] + 'volumev3', 'public', append=['volumes', volume.id] ), - json={'volumes': [volume]}, + json={'volume': volume}, ), dict( method='POST', diff --git a/openstack/tests/unit/cloud/test_volume_access.py b/openstack/tests/unit/cloud/test_volume_access.py index 035291b7a..d1516c0cf 100644 --- a/openstack/tests/unit/cloud/test_volume_access.py +++ b/openstack/tests/unit/cloud/test_volume_access.py @@ -51,13 +51,24 @@ def test_get_volume_type(self): ) self.register_uris( [ + # "find" will attempt to retrieve using the name as an ID + # first, but cinder only supports lookup by ID so we'll see 404 + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', + 'public', + append=['types', volume_type['name']], + ), + status_code=404, + ), dict( method='GET', uri=self.get_mock_url( 'volumev3', 'public', append=['types'] ), json={'volume_types': [volume_type]}, - ) + ), ] ) volume_type_got = self.cloud.get_volume_type(volume_type['name']) @@ -76,6 +87,17 @@ def test_get_volume_type_access(self): ] self.register_uris( [ + # "find" will attempt to retrieve using the name as an ID + # first, but cinder only supports lookup by ID so we'll see 404 + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', + 'public', + append=['types', volume_type['name']], + ), + status_code=404, + ), dict( method='GET', uri=self.get_mock_url( @@ -119,6 +141,17 @@ def test_remove_volume_type_access(self): volume_type_access = [project_001, project_002] self.register_uris( [ + # "find" will attempt to retrieve using the name as an ID + # first, but cinder only supports lookup by ID so we'll see 404 + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', + 'public', + append=['types', volume_type['name']], + ), + status_code=404, + ), dict( method='GET', uri=self.get_mock_url( @@ -139,6 +172,15 @@ def test_remove_volume_type_access(self): ), json={'volume_type_access': volume_type_access}, ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', + 'public', + append=['types', volume_type['name']], + ), + status_code=404, + ), dict( method='GET', uri=self.get_mock_url( @@ -166,6 +208,15 @@ def test_remove_volume_type_access(self): } ), ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', + 'public', + append=['types', volume_type['name']], + ), + status_code=404, + ), dict( method='GET', uri=self.get_mock_url( @@ -215,6 +266,17 @@ def test_add_volume_type_access(self): volume_type_access = [project_001, project_002] self.register_uris( [ + # "find" will attempt to retrieve using the name as an ID + # first, but cinder only supports lookup by ID so we'll see 404 + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', + 'public', + append=['types', volume_type['name']], + ), + status_code=404, + ), dict( method='GET', uri=self.get_mock_url( @@ -242,6 +304,15 @@ def test_add_volume_type_access(self): } ), ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', + 'public', + append=['types', volume_type['name']], + ), + status_code=404, + ), dict( method='GET', uri=self.get_mock_url( @@ -284,13 +355,22 @@ def test_add_volume_type_access_missing(self): ) self.register_uris( [ + # "find" will attempt to retrieve using the name as an ID + # first, but cinder only supports lookup by ID so we'll see 404 + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['types', 'MISSING'] + ), + status_code=404, + ), dict( method='GET', uri=self.get_mock_url( 'volumev3', 'public', append=['types'] ), json={'volume_types': [volume_type]}, - ) + ), ] ) with testtools.ExpectedException( diff --git a/openstack/tests/unit/cloud/test_volume_backups.py b/openstack/tests/unit/cloud/test_volume_backups.py index 8419f0e7f..da4da4a67 100644 --- a/openstack/tests/unit/cloud/test_volume_backups.py +++ b/openstack/tests/unit/cloud/test_volume_backups.py @@ -49,6 +49,34 @@ def test_search_volume_backups(self): self.assert_calls() def test_get_volume_backup(self): + name = 'Volume1' + backup = {'name': name, 'availability_zone': 'az1'} + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', 'public', append=['backups', name] + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'volumev3', + 'public', + append=['backups', 'detail'], + qs_elements=[f'name={name}'], + ), + json={"backups": [backup]}, + ), + ] + ) + result = self.cloud.get_volume_backup(name) + self._compare_backups(backup, result) + self.assert_calls() + + def test_get_volume_backup_with_filters(self): name = 'Volume1' vol1 = {'name': name, 'availability_zone': 'az1'} vol2 = {'name': name, 'availability_zone': 'az2'} @@ -104,9 +132,9 @@ def test_delete_volume_backup_wait(self): dict( method='GET', uri=self.get_mock_url( - 'volumev3', 'public', append=['backups', 'detail'] + 'volumev3', 'public', append=['backups', backup_id] ), - json={"backups": [backup]}, + json={'backup': backup}, ), dict( method='DELETE', @@ -141,9 +169,9 @@ def test_delete_volume_backup_force(self): dict( method='GET', uri=self.get_mock_url( - 'volumev3', 'public', append=['backups', 'detail'] + 'volumev3', 'public', append=['backups', backup_id] ), - json={"backups": [backup]}, + json={'backup': backup}, ), dict( method='POST', From 4124120260eb0e7ec9a9efaccc3d5d0d9e19dbf0 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 24 Feb 2025 15:22:13 +0000 Subject: [PATCH 3644/3836] cloud: Replace use of cloud methods in identity cloud layer Always use the proxy layer methods instead. We also change the logic of the '_get_grant_revoke_params' helper. This previously returned the same error about a user needing to be specified if (a) a user was not specified or (b) a user was specified but it was invalid. (b) makes no sense and as such no longer happens. Change-Id: I81a6ed6143c43eea1ba7202db6d54f3859852a53 Signed-off-by: Stephen Finucane --- openstack/cloud/_identity.py | 21 ++-- .../tests/unit/cloud/test_role_assignment.py | 102 +++++++----------- 2 files changed, 47 insertions(+), 76 deletions(-) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index e87f15c2d..c47771c9d 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -1216,19 +1216,11 @@ def _get_grant_revoke_params( data['role'] = self.identity.find_role(role, ignore_missing=False) - if user: - # use cloud.get_user to save us from bad searching by name - data['user'] = self.get_user(user, filters=search_args) - if group: - data['group'] = self.identity.find_group( - group, ignore_missing=False, **search_args - ) - - if data.get('user') and data.get('group'): + if user and group: raise exceptions.SDKException( 'Specify either a group or a user, not both' ) - if data.get('user') is None and data.get('group') is None: + if user is None and group is None: raise exceptions.SDKException( 'Must specify either a user or a group' ) @@ -1237,10 +1229,19 @@ def _get_grant_revoke_params( 'Must specify either a domain, project or system' ) + if user: + data['user'] = self.identity.find_user( + user, ignore_missing=False, **search_args + ) + if group: + data['group'] = self.identity.find_group( + group, ignore_missing=False, **search_args + ) if project: data['project'] = self.identity.find_project( project, ignore_missing=False, **search_args ) + return data def grant_role( diff --git a/openstack/tests/unit/cloud/test_role_assignment.py b/openstack/tests/unit/cloud/test_role_assignment.py index 2d9626f39..8cc5df3b1 100644 --- a/openstack/tests/unit/cloud/test_role_assignment.py +++ b/openstack/tests/unit/cloud/test_role_assignment.py @@ -198,30 +198,45 @@ def __get( ), ] - def __user_mocks(self, user_data, use_name, is_found=True): + def __user_mocks( + self, user_data, use_name, is_found=True, domain_data=None + ): + qs_elements = [] + if domain_data: + qs_elements = ['domain_id=' + domain_data.domain_id] uri_mocks = [] if not use_name: uri_mocks.append( dict( method='GET', - uri=self.get_mock_url(resource='users'), - status_code=200, - json={ - 'users': ( - [user_data.json_response['user']] - if is_found - else [] - ) - }, - ) + uri=self.get_mock_url( + resource='users', + append=[user_data.user_id], + # TODO(stephenfin): We shouldn't be passing domain ID + # here since it's unnecessary, but that requires a much + # larger rework of the Resource.find method. + qs_elements=qs_elements, + ), + json=user_data.json_response if is_found else None, + status_code=200 if is_found else 404, + ), ) else: - uri_mocks.append( + uri_mocks += [ dict( method='GET', uri=self.get_mock_url( resource='users', - qs_elements=['name=' + user_data.name], + append=[user_data.name], + qs_elements=qs_elements, + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + resource='users', + qs_elements=qs_elements + ['name=' + user_data.name], ), status_code=200, json={ @@ -231,8 +246,8 @@ def __user_mocks(self, user_data, use_name, is_found=True): else [] ) }, - ) - ) + ), + ] return uri_mocks def _get_mock_role_query_urls( @@ -279,7 +294,12 @@ def _get_mock_role_query_urls( if user_data: uri_mocks.extend( - self.__user_mocks(user_data, use_user_name, is_found=True) + self.__user_mocks( + user_data, + use_user_name, + is_found=True, + domain_data=domain_data, + ) ) if group_data: @@ -1907,56 +1927,10 @@ def test_revoke_no_user_or_group_specified(self): ) self.assert_calls() - def test_grant_no_user_or_group(self): - uris = self.__get( - 'role', self.role_data, 'role_name', [], use_name=True - ) - uris.extend( - self.__user_mocks(self.user_data, use_name=True, is_found=False) - ) - self.register_uris(uris) - - with testtools.ExpectedException( - exceptions.SDKException, - 'Must specify either a user or a group', - ): - self.cloud.grant_role( - self.role_data.role_name, - user=self.user_data.name, - inherited=self.IS_INHERITED, - ) - self.assert_calls() - - def test_revoke_no_user_or_group(self): - uris = self.__get( - 'role', self.role_data, 'role_name', [], use_name=True - ) - uris.extend( - self.__user_mocks(self.user_data, use_name=True, is_found=False) - ) - self.register_uris(uris) - - with testtools.ExpectedException( - exceptions.SDKException, - 'Must specify either a user or a group', - ): - self.cloud.revoke_role( - self.role_data.role_name, - user=self.user_data.name, - inherited=self.IS_INHERITED, - ) - self.assert_calls() - def test_grant_both_user_and_group(self): uris = self.__get( 'role', self.role_data, 'role_name', [], use_name=True ) - uris.extend(self.__user_mocks(self.user_data, use_name=True)) - uris.extend( - self.__get( - 'group', self.group_data, 'group_name', [], use_name=True - ) - ) self.register_uris(uris) with testtools.ExpectedException( @@ -2115,9 +2089,7 @@ def test_revoke_both_project_and_domain(self): def test_grant_no_project_or_domain(self): uris = self._get_mock_role_query_urls( self.role_data, - user_data=self.user_data, use_role_name=True, - use_user_name=True, ) self.register_uris(uris) @@ -2136,9 +2108,7 @@ def test_grant_no_project_or_domain(self): def test_revoke_no_project_or_domain_or_system(self): uris = self._get_mock_role_query_urls( self.role_data, - user_data=self.user_data, use_role_name=True, - use_user_name=True, ) self.register_uris(uris) From 51806366c9644090f56139a882ebd0397368437f Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 25 Jun 2024 16:34:36 +0100 Subject: [PATCH 3645/3836] cloud: Switch remaining identity functions to proxy Once again, there are a couple of functions that had useless parameters. These are deprecated for removal. Change-Id: I3a0f6eba74ddffc63b56f0686b38113febb22d3e Signed-off-by: Stephen Finucane --- openstack/cloud/_identity.py | 166 +++++++++++++++--- openstack/tests/unit/base.py | 2 +- .../tests/unit/cloud/test_domain_params.py | 30 +++- openstack/tests/unit/cloud/test_endpoints.py | 26 ++- openstack/tests/unit/cloud/test_groups.py | 4 +- .../tests/unit/cloud/test_identity_roles.py | 32 +++- openstack/tests/unit/cloud/test_services.py | 98 +++++++---- openstack/tests/unit/cloud/test_users.py | 56 ++++-- 8 files changed, 317 insertions(+), 97 deletions(-) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index c47771c9d..72ed3ceb5 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -144,8 +144,27 @@ def get_project(self, name_or_id, filters=None, domain_id=None): :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ - return _utils._get_entity( - self, 'project', name_or_id, filters, domain_id=domain_id + if filters is not None: + warnings.warn( + "the 'filters' argument is deprecated; use " + "'search_projects' instead", + os_warnings.RemovedInSDK60Warning, + ) + entities = self.search_projects( + name_or_id, filters, domain_id=domain_id + ) + if not entities: + return None + + if len(entities) > 1: + raise exceptions.SDKException( + f"Multiple matches found for {name_or_id}", + ) + + return entities[0] + + return self.identity.find_project( + name_or_id=name_or_id, domain_id=domain_id ) def update_project( @@ -270,9 +289,7 @@ def search_users(self, name_or_id=None, filters=None, domain_id=None): return _utils._filter_list(users, name_or_id, filters) # TODO(stephenfin): Remove 'filters' in a future major version - # TODO(stephenfin): Remove 'kwargs' since it doesn't do anything - @_utils.valid_kwargs('domain_id') - def get_user(self, name_or_id, filters=None, **kwargs): + def get_user(self, name_or_id, filters=None, domain_id=None): """Get exactly one user. :param name_or_id: Name or unique ID of the user. @@ -288,12 +305,30 @@ def get_user(self, name_or_id, filters=None, **kwargs): Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :param domain_id: Domain ID to scope the retrieved user. :returns: an identity ``User`` object :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ - return _utils._get_entity(self, 'user', name_or_id, filters, **kwargs) + if filters is not None: + warnings.warn( + "the 'filters' argument is deprecated; use " + "'search_user' instead", + os_warnings.RemovedInSDK60Warning, + ) + entities = self.search_users( + name_or_id, filters, domain_id=domain_id + ) + if not entities: + return None + + if len(entities) > 1: + raise exceptions.SDKException( + f"Multiple matches found for {name_or_id}", + ) + + return self.identity.find_user(name_or_id, domain_id=domain_id) # TODO(stephenfin): Remove normalize since it doesn't do anything def get_user_by_id(self, user_id, normalize=None): @@ -500,7 +535,7 @@ def search_services(self, name_or_id=None, filters=None): services = self.list_services() return _utils._filter_list(services, name_or_id, filters) - # TODO(stephenfin): Remove 'filters' since it's a noop + # TODO(stephenfin): Remove 'filters' in a future major version def get_service(self, name_or_id, filters=None): """Get exactly one Keystone service. @@ -511,7 +546,24 @@ def get_service(self, name_or_id, filters=None): wrong during the OpenStack API call or if multiple matches are found. """ - return _utils._get_entity(self, 'service', name_or_id, filters) + if filters is not None: + warnings.warn( + "the 'filters' argument is deprecated; use " + "'search_services' instead", + os_warnings.RemovedInSDK60Warning, + ) + entities = self.search_services(name_or_id, filters) + if not entities: + return None + + if len(entities) > 1: + raise exceptions.SDKException( + f"Multiple matches found for {name_or_id}", + ) + + return entities[0] + + return self.identity.find_service(name_or_id=name_or_id) def delete_service(self, name_or_id): """Delete a Keystone service. @@ -673,7 +725,24 @@ def get_endpoint(self, id, filters=None): :param id: ID of endpoint. :returns: An identity ``Endpoint`` object """ - return _utils._get_entity(self, 'endpoint', id, filters) + if filters is not None: + warnings.warn( + "the 'filters' argument is deprecated; use " + "'search_endpoints' instead", + os_warnings.RemovedInSDK60Warning, + ) + entities = self.search_endpoints(id, filters) + if not entities: + return None + + if len(entities) > 1: + raise exceptions.SDKException( + f"Multiple matches found for {id}", + ) + + return entities[0] + + return self.identity.find_endpoint(name_or_id=id) def delete_endpoint(self, id): """Delete a Keystone endpoint. @@ -849,6 +918,13 @@ def get_domain(self, domain_id=None, name_or_id=None, filters=None): :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ + if filters is not None: + warnings.warn( + "The 'filters' argument is deprecated for removal. It is a " + "no-op and can be safely removed.", + os_warnings.RemovedInSDK60Warning, + ) + if domain_id is None: return self.identity.find_domain(name_or_id, ignore_missing=True) else: @@ -866,8 +942,7 @@ def list_groups(self, **kwargs): """ return list(self.identity.groups(**kwargs)) - @_utils.valid_kwargs('domain_id') - def search_groups(self, name_or_id=None, filters=None, **kwargs): + def search_groups(self, name_or_id=None, filters=None, domain_id=None): """Search Keystone groups. :param name_or_id: Name or ID of the group(s). @@ -883,28 +958,48 @@ def search_groups(self, name_or_id=None, filters=None, **kwargs): Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" - :param domain_id: domain id. + :param domain_id: Domain ID to scope the searched groups. :returns: A list of identity ``Group`` objects :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ - groups = self.list_groups(**kwargs) + groups = self.list_groups(domain_id=domain_id) return _utils._filter_list(groups, name_or_id, filters) - # TODO(stephenfin): Remove filters since it's a noop - # TODO(stephenfin): Remove kwargs since it's a noop - @_utils.valid_kwargs('domain_id') - def get_group(self, name_or_id, filters=None, **kwargs): + # TODO(stephenfin): Remove 'filters' in a future major version + def get_group(self, name_or_id, filters=None, domain_id=None): """Get exactly one Keystone group. :param name_or_id: Name or unique ID of the group(s). + :param domain_id: Domain ID to scope the retrieved group. :returns: An identity ``Group`` object :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ - return _utils._get_entity(self, 'group', name_or_id, filters, **kwargs) + if filters is not None: + warnings.warn( + "the 'filters' argument is deprecated; use " + "'search_projects' instead", + os_warnings.RemovedInSDK60Warning, + ) + entities = self.search_groups( + name_or_id, filters, domain_id=domain_id + ) + if not entities: + return None + + if len(entities) > 1: + raise exceptions.SDKException( + f"Multiple matches found for {name_or_id}", + ) + + return entities[0] + + return self.identity.find_group( + name_or_id=name_or_id, domain_id=domain_id + ) def create_group(self, name, description, domain=None): """Create a group. @@ -995,7 +1090,7 @@ def list_roles(self, **kwargs): """ return list(self.identity.roles(**kwargs)) - def search_roles(self, name_or_id=None, filters=None): + def search_roles(self, name_or_id=None, filters=None, domain_id=None): """Seach Keystone roles. :param name: Name or ID of the role(s). @@ -1011,27 +1106,48 @@ def search_roles(self, name_or_id=None, filters=None): Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :param domain_id: Domain ID to scope the searched roles. :returns: a list of identity ``Role`` objects :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ - roles = self.list_roles() + roles = self.list_roles(domain_id=domain_id) return _utils._filter_list(roles, name_or_id, filters) - # TODO(stephenfin): Remove filters since it's a noop - # TODO(stephenfin): Remove kwargs since it's a noop - @_utils.valid_kwargs('domain_id') - def get_role(self, name_or_id, filters=None, **kwargs): + # TODO(stephenfin): Remove 'filters' in a future major version + def get_role(self, name_or_id, filters=None, domain_id=None): """Get a Keystone role. :param name_or_id: Name or unique ID of the role. + :param domain_id: Domain ID to scope the retrieved role. :returns: An identity ``Role`` object if found, else None. :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ - return _utils._get_entity(self, 'role', name_or_id, filters, **kwargs) + if filters is not None: + warnings.warn( + "the 'filters' argument is deprecated; use " + "'search_roles' instead", + os_warnings.RemovedInSDK60Warning, + ) + entities = self.search_roles( + name_or_id, filters, domain_id=domain_id + ) + if not entities: + return None + + if len(entities) > 1: + raise exceptions.SDKException( + f"Multiple matches found for {name_or_id}", + ) + + return entities[0] + + return self.identity.find_role( + name_or_id=name_or_id, domain_id=domain_id + ) def _keystone_v3_role_assignments(self, **filters): # NOTE(samueldmq): different parameters have different representation diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 9347c9a84..65ed6762e 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -403,7 +403,7 @@ def _get_service_data( self, type=None, name=None, description=None, enabled=True ): service_id = uuid.uuid4().hex - name = name or uuid.uuid4().hex + name = name or f'name-{uuid.uuid4().hex}' type = type or uuid.uuid4().hex response = { diff --git a/openstack/tests/unit/cloud/test_domain_params.py b/openstack/tests/unit/cloud/test_domain_params.py index bbf7f4c08..84535ea7d 100644 --- a/openstack/tests/unit/cloud/test_domain_params.py +++ b/openstack/tests/unit/cloud/test_domain_params.py @@ -15,17 +15,43 @@ class TestDomainParams(base.TestCase): + def get_mock_url( + self, + service_type='identity', + interface='public', + resource='projects', + append=None, + base_url_append='v3', + qs_elements=None, + ): + return super().get_mock_url( + service_type, + interface, + resource, + append, + base_url_append, + qs_elements, + ) + def test_identity_params_v3(self): project_data = self._get_project_data(v3=True) self.register_uris( [ + # can't retrieve by name dict( method='GET', - uri='https://identity.example.com/v3/projects', + uri=self.get_mock_url(append=[project_data.project_name]), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + qs_elements=[f'name={project_data.project_name}'] + ), json=dict( projects=[project_data.json_response['project']] ), - ) + ), ] ) diff --git a/openstack/tests/unit/cloud/test_endpoints.py b/openstack/tests/unit/cloud/test_endpoints.py index 01191e912..f4852efb3 100644 --- a/openstack/tests/unit/cloud/test_endpoints.py +++ b/openstack/tests/unit/cloud/test_endpoints.py @@ -42,7 +42,7 @@ def get_mock_url( def _dummy_url(self): return f'https://{uuid.uuid4().hex}.example.com/' - def test_create_endpoint_v3(self): + def test_create_endpoint(self): service_data = self._get_service_data() public_endpoint_data = self._get_endpoint_v3_data( service_id=service_data.service_id, @@ -72,11 +72,11 @@ def test_create_endpoint_v3(self): [ dict( method='GET', - uri=self.get_mock_url(resource='services'), + uri=self.get_mock_url( + resource='services', append=[service_data.service_id] + ), status_code=200, - json={ - 'services': [service_data.json_response_v3['service']] - }, + json=service_data.json_response_v3, ), dict( method='POST', @@ -89,11 +89,11 @@ def test_create_endpoint_v3(self): ), dict( method='GET', - uri=self.get_mock_url(resource='services'), + uri=self.get_mock_url( + resource='services', append=[service_data.service_id] + ), status_code=200, - json={ - 'services': [service_data.json_response_v3['service']] - }, + json=service_data.json_response_v3, ), dict( method='POST', @@ -186,7 +186,7 @@ def test_create_endpoint_v3(self): ) self.assert_calls() - def test_update_endpoint_v3(self): + def test_update_endpoint(self): service_data = self._get_service_data() dummy_url = self._dummy_url() endpoint_data = self._get_endpoint_v3_data( @@ -363,11 +363,9 @@ def test_delete_endpoint(self): [ dict( method='GET', - uri=self.get_mock_url(), + uri=self.get_mock_url(append=[endpoint_data.endpoint_id]), status_code=200, - json={ - 'endpoints': [endpoint_data.json_response['endpoint']] - }, + json=endpoint_data.json_response['endpoint'], ), dict( method='DELETE', diff --git a/openstack/tests/unit/cloud/test_groups.py b/openstack/tests/unit/cloud/test_groups.py index 273852dd3..cd3f27ccc 100644 --- a/openstack/tests/unit/cloud/test_groups.py +++ b/openstack/tests/unit/cloud/test_groups.py @@ -55,9 +55,9 @@ def test_get_group(self): [ dict( method='GET', - uri=self.get_mock_url(), + uri=self.get_mock_url(append=[group_data.group_id]), status_code=200, - json={'groups': [group_data.json_response['group']]}, + json=group_data.json_response, ), ] ) diff --git a/openstack/tests/unit/cloud/test_identity_roles.py b/openstack/tests/unit/cloud/test_identity_roles.py index 344f9cbb2..467e7bec0 100644 --- a/openstack/tests/unit/cloud/test_identity_roles.py +++ b/openstack/tests/unit/cloud/test_identity_roles.py @@ -95,10 +95,17 @@ def test_get_role_by_name(self): [ dict( method='GET', - uri=self.get_mock_url(), + uri=self.get_mock_url(append=[role_data.role_name]), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + qs_elements=[f'name={role_data.role_name}'] + ), status_code=200, json={'roles': [role_data.json_response['role']]}, - ) + ), ] ) role = self.cloud.get_role(role_data.role_name) @@ -114,9 +121,9 @@ def test_get_role_by_id(self): [ dict( method='GET', - uri=self.get_mock_url(), + uri=self.get_mock_url(append=[role_data.role_id]), status_code=200, - json={'roles': [role_data.json_response['role']]}, + json=role_data.json_response, ) ] ) @@ -155,9 +162,9 @@ def test_update_role(self): [ dict( method='GET', - uri=self.get_mock_url(), + uri=self.get_mock_url(append=[role_data.role_id]), status_code=200, - json={'roles': [role_data.json_response['role']]}, + json=role_data.json_response, ), dict( method='PATCH', @@ -182,9 +189,9 @@ def test_delete_role_by_id(self): [ dict( method='GET', - uri=self.get_mock_url(), + uri=self.get_mock_url(append=[role_data.role_id]), status_code=200, - json={'roles': [role_data.json_response['role']]}, + json=role_data.json_response, ), dict( method='DELETE', @@ -203,7 +210,14 @@ def test_delete_role_by_name(self): [ dict( method='GET', - uri=self.get_mock_url(), + uri=self.get_mock_url(append=[role_data.role_name]), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + qs_elements=[f'name={role_data.role_name}'] + ), status_code=200, json={'roles': [role_data.json_response['role']]}, ), diff --git a/openstack/tests/unit/cloud/test_services.py b/openstack/tests/unit/cloud/test_services.py index 3ea0d619b..58a3ea4e7 100644 --- a/openstack/tests/unit/cloud/test_services.py +++ b/openstack/tests/unit/cloud/test_services.py @@ -19,6 +19,8 @@ Tests Keystone services commands. """ +import warnings + from testtools import matchers from openstack import exceptions @@ -36,9 +38,15 @@ def get_mock_url( resource='services', append=None, base_url_append='v3', + qs_elements=None, ): return super().get_mock_url( - service_type, interface, resource, append, base_url_append + service_type, + interface, + resource, + append, + base_url_append, + qs_elements, ) def test_create_service_v3(self): @@ -89,9 +97,9 @@ def test_update_service_v3(self): [ dict( method='GET', - uri=self.get_mock_url(), + uri=self.get_mock_url(append=[service_data.service_id]), status_code=200, - json={'services': [resp['service']]}, + json=service_data.json_request, ), dict( method='PATCH', @@ -148,29 +156,20 @@ def test_list_services(self): def test_get_service(self): service_data = self._get_service_data() service2_data = self._get_service_data() + self.register_uris( [ dict( method='GET', - uri=self.get_mock_url(), + uri=self.get_mock_url(append=[service_data.service_id]), status_code=200, - json={ - 'services': [ - service_data.json_response_v3['service'], - service2_data.json_response_v3['service'], - ] - }, + json=service_data.json_response_v3, ), + # you can't retrieve by name dict( method='GET', - uri=self.get_mock_url(), - status_code=200, - json={ - 'services': [ - service_data.json_response_v3['service'], - service2_data.json_response_v3['service'], - ] - }, + uri=self.get_mock_url(append=[service_data.service_name]), + status_code=404, ), dict( method='GET', @@ -183,7 +182,19 @@ def test_get_service(self): ] }, ), - dict(method='GET', uri=self.get_mock_url(), status_code=400), + # you can't retrieve by name, especially if it doesn't exist + dict( + method='GET', + uri=self.get_mock_url(append=['INVALID SERVICE']), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + qs_elements=['name=INVALID SERVICE'] + ), + json={'services': []}, + ), ] ) @@ -200,14 +211,35 @@ def test_get_service(self): service = self.cloud.get_service(name_or_id='INVALID SERVICE') self.assertIs(None, service) + def test_get_service__multiple_matches(self): + service_a_data = self._get_service_data(type='type2') + service_b_data = self._get_service_data(type='type2') + + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url(), + status_code=200, + json={ + 'services': [ + service_a_data.json_response_v3['service'], + service_b_data.json_response_v3['service'], + ] + }, + ), + ] + ) + # Multiple matches # test we are getting an Exception - self.assertRaises( - exceptions.SDKException, - self.cloud.get_service, - name_or_id=None, - filters={'type': 'type2'}, - ) + with warnings.catch_warnings(record=True): + self.assertRaises( + exceptions.SDKException, + self.cloud.get_service, + name_or_id=None, + filters={'type': 'type2'}, + ) self.assert_calls() def test_search_services(self): @@ -304,9 +336,17 @@ def test_delete_service(self): service_data = self._get_service_data() self.register_uris( [ + # you can't retrieve by name dict( method='GET', - uri=self.get_mock_url(), + uri=self.get_mock_url(append=[service_data.service_name]), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + qs_elements=[f'name={service_data.service_name}'] + ), status_code=200, json={ 'services': [service_data.json_response_v3['service']] @@ -319,11 +359,9 @@ def test_delete_service(self): ), dict( method='GET', - uri=self.get_mock_url(), + uri=self.get_mock_url(append=[service_data.service_id]), status_code=200, - json={ - 'services': [service_data.json_response_v3['service']] - }, + json=service_data.json_response_v3, ), dict( method='DELETE', diff --git a/openstack/tests/unit/cloud/test_users.py b/openstack/tests/unit/cloud/test_users.py index 042916d10..cbb0e6ded 100644 --- a/openstack/tests/unit/cloud/test_users.py +++ b/openstack/tests/unit/cloud/test_users.py @@ -101,6 +101,14 @@ def test_delete_user(self): self.register_uris( [ + # you can't lookup by name, so return 404 for that attempt + dict( + method='GET', + uri=self._get_keystone_mock_url( + resource='users', append=[user_data.name] + ), + status_code=404, + ), dict( method='GET', uri=self._get_keystone_mock_url( @@ -118,17 +126,25 @@ def test_delete_user(self): self.assert_calls() def test_delete_user_not_found(self): + nonexistent_user_id = self.getUniqueString() self.register_uris( [ + dict( + method='GET', + uri=self._get_keystone_mock_url( + resource='users', append=[nonexistent_user_id] + ), + status_code=404, + ), dict( method='GET', uri=self._get_keystone_mock_url(resource='users'), status_code=200, json={'users': []}, - ) + ), ] ) - self.assertFalse(self.cloud.delete_user(self.getUniqueString())) + self.assertFalse(self.cloud.delete_user(nonexistent_user_id)) def test_add_user_to_group(self): user_data = self._get_user_data() @@ -138,15 +154,19 @@ def test_add_user_to_group(self): [ dict( method='GET', - uri=self._get_keystone_mock_url(resource='users'), + uri=self._get_keystone_mock_url( + resource='users', append=[user_data.user_id] + ), status_code=200, - json=self._get_user_list(user_data), + json=user_data.json_response, ), dict( method='GET', - uri=self._get_keystone_mock_url(resource='groups'), + uri=self._get_keystone_mock_url( + resource='groups', append=[group_data.group_id] + ), status_code=200, - json={'groups': [group_data.json_response['group']]}, + json=group_data.json_response, ), dict( method='PUT', @@ -173,15 +193,19 @@ def test_is_user_in_group(self): [ dict( method='GET', - uri=self._get_keystone_mock_url(resource='users'), + uri=self._get_keystone_mock_url( + resource='users', append=[user_data.user_id] + ), status_code=200, - json=self._get_user_list(user_data), + json=user_data.json_response, ), dict( method='GET', - uri=self._get_keystone_mock_url(resource='groups'), + uri=self._get_keystone_mock_url( + resource='groups', append=[group_data.group_id] + ), status_code=200, - json={'groups': [group_data.json_response['group']]}, + json=group_data.json_response, ), dict( method='HEAD', @@ -211,14 +235,18 @@ def test_remove_user_from_group(self): [ dict( method='GET', - uri=self._get_keystone_mock_url(resource='users'), - json=self._get_user_list(user_data), + uri=self._get_keystone_mock_url( + resource='users', append=[user_data.user_id] + ), + json=user_data.json_response, ), dict( method='GET', - uri=self._get_keystone_mock_url(resource='groups'), + uri=self._get_keystone_mock_url( + resource='groups', append=[group_data.group_id] + ), status_code=200, - json={'groups': [group_data.json_response['group']]}, + json=group_data.json_response, ), dict( method='DELETE', From fbebf106a576c398a78cee2e255cce9af3989e73 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 24 Feb 2025 16:00:29 +0000 Subject: [PATCH 3646/3836] deps: Bump minimum keystoneauth We want typing. Change-Id: I83cff62f92f942257f178921a6b94d49f25c69c9 Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 2 +- openstack/utils.py | 6 +++--- pyproject.toml | 3 +-- requirements.txt | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 75b6fbbc5..118cf6153 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,7 +36,7 @@ repos: hooks: - id: mypy additional_dependencies: - - keystoneauth1 + - keystoneauth1>=5.10.0 - types-decorator - types-PyYAML - types-requests diff --git a/openstack/utils.py b/openstack/utils.py index 62d21db4f..efde58e89 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -212,7 +212,7 @@ def supports_microversion( f'Required microversion {microversion} is higher than ' f'currently selected {adapter.default_microversion}' ) - return supports # type: ignore[no-any-return] + return supports return True @@ -276,7 +276,7 @@ def pick_microversion( 'Requested microversion is not supported by the server side ' 'or the default microversion is too low' ) - return discover.version_to_string(required_normalized) # type: ignore[no-any-return] + return discover.version_to_string(required_normalized) def maximum_supported_microversion( @@ -327,7 +327,7 @@ def maximum_supported_microversion( return None result = min(client_max, server_max) - return discover.version_to_string(result) # type: ignore[no-any-return] + return discover.version_to_string(result) def _hashes_up_to_date( diff --git a/pyproject.toml b/pyproject.toml index 7de016796..1fab36c2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,8 +5,7 @@ show_error_context = true ignore_missing_imports = true follow_imports = "normal" check_untyped_defs = true -# TODO(stephenfin): Remove this when typed keystoneauth1 (5.10.0?) is released -warn_unused_ignores = false +warn_unused_ignores = true # many of the following are false while we incrementally add typing warn_return_any = false warn_unused_configs = true diff --git a/requirements.txt b/requirements.txt index a57c83ed3..b2a10c619 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ dogpile.cache>=0.6.5 # BSD iso8601>=0.1.11 # MIT jmespath>=0.9.0 # MIT jsonpatch!=1.20,>=1.16 # BSD -keystoneauth1>=3.18.0 # Apache-2.0 +keystoneauth1>=5.10.0 # Apache-2.0 os-service-types>=1.7.0 # Apache-2.0 pbr!=2.1.0,>=2.0.0 # Apache-2.0 platformdirs>=3 # MIT License From 62a496d52f444b62dfac53072fe02d274aba63ec Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 24 Feb 2025 17:09:32 +0000 Subject: [PATCH 3647/3836] zuul: Make openstacksdk-functional-devstack-manila voting In the process, fix an issue introduced recently in change I9977427e83136ed4a0b3058887aba4cee4ace6a6 that was able to slip by because this was not voting. Change-Id: I2b8943b3cde60048ed29081bc59636944d4867a3 Signed-off-by: Stephen Finucane --- .../tests/functional/shared_file_system/test_resource_lock.py | 2 +- zuul.d/project.yaml | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/openstack/tests/functional/shared_file_system/test_resource_lock.py b/openstack/tests/functional/shared_file_system/test_resource_lock.py index 42198fb54..b9004b072 100644 --- a/openstack/tests/functional/shared_file_system/test_resource_lock.py +++ b/openstack/tests/functional/shared_file_system/test_resource_lock.py @@ -46,7 +46,7 @@ def setUp(self): failures=['error'], interval=5, wait=self._wait_for_timeout, - status_attr_name='state', + attribute='state', ) self.assertIsNotNone(share) self.assertIsNotNone(share.id) diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index 084449319..995d91efb 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -25,8 +25,7 @@ - openstacksdk-functional-devstack-networking-ext - openstacksdk-functional-devstack-magnum: voting: false - - openstacksdk-functional-devstack-manila: - voting: false + - openstacksdk-functional-devstack-manila - openstacksdk-functional-devstack-masakari: voting: false - openstacksdk-functional-devstack-ironic: From 49f2e2d169b7e4d6572f35d41f0cc982737a026f Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 24 Feb 2025 17:14:48 +0000 Subject: [PATCH 3648/3836] zuul: Make openstacksdk-functional-devstack-masakari voting Change-Id: I40ad032545acccfacfa7384176c7eda2692712aa Signed-off-by: Stephen Finucane --- zuul.d/project.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index 995d91efb..197ef92ea 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -26,8 +26,7 @@ - openstacksdk-functional-devstack-magnum: voting: false - openstacksdk-functional-devstack-manila - - openstacksdk-functional-devstack-masakari: - voting: false + - openstacksdk-functional-devstack-masakari - openstacksdk-functional-devstack-ironic: voting: false - osc-functional-devstack-tips: From 4697775f8aa31698062bf68740093eb6088ee121 Mon Sep 17 00:00:00 2001 From: ArtofBugs Date: Mon, 24 Feb 2025 14:48:16 -0800 Subject: [PATCH 3649/3836] identity: Extract info from created registered limit Do the same as https://review.opendev.org/c/openstack/openstacksdk/+/937397 but for registered limits. Change-Id: Ic8de738f9b48e05bbed7d758df4c6245442f3af2 --- openstack/identity/v3/registered_limit.py | 69 ++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/openstack/identity/v3/registered_limit.py b/openstack/identity/v3/registered_limit.py index 6cdf07a20..b7a42dd20 100644 --- a/openstack/identity/v3/registered_limit.py +++ b/openstack/identity/v3/registered_limit.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import exceptions from openstack import resource @@ -57,8 +58,74 @@ def _prepare_request_body( if patch: body = {self.resource_key: body} else: - # Keystone support bunch create for unified limit. So the + # Keystone supports bunch create for registered limit. So the # request body for creating registered_limit is a list instead # of dict. body = {self.resources_key: [body]} return body + + def _translate_response( + self, + response, + has_body=None, + error_message=None, + *, + resource_response_key='registered_limits', + ): + """Given a KSA response, inflate this instance with its data + + DELETE operations don't return a body, so only try to work + with a body when has_body is True. + + This method updates attributes that correspond to headers + and body on this instance and clears the dirty set. + """ + if has_body is None: + has_body = self.has_body + + exceptions.raise_from_response(response, error_message=error_message) + + if has_body: + try: + body = response.json() + if resource_response_key and resource_response_key in body: + body = body[resource_response_key] + elif self.resource_key and self.resource_key in body: + body = body[self.resource_key] + + # Keystone supports bunch create for registered limit. So the + # response body for creating registered_limit is a list instead of dict. + if isinstance(body, list): + body = body[0] + + # Do not allow keys called "self" through. Glance chose + # to name a key "self", so we need to pop it out because + # we can't send it through cls.existing and into the + # Resource initializer. "self" is already the first + # argument and is practically a reserved word. + body.pop("self", None) + + body_attrs = self._consume_body_attrs(body) + if self._allow_unknown_attrs_in_body: + body_attrs.update(body) + self._unknown_attrs_in_body.update(body) + elif self._store_unknown_attrs_as_properties: + body_attrs = self._pack_attrs_under_properties( + body_attrs, body + ) + + self._body.attributes.update(body_attrs) + self._body.clean() + if self.commit_jsonpatch or self.allow_patch: + # We need the original body to compare against + self._original_body = body_attrs.copy() + except ValueError: + # Server returned not parse-able response (202, 204, etc) + # Do simply nothing + pass + + headers = self._consume_header_attrs(response.headers) + self._header.attributes.update(headers) + self._header.clean() + self._update_location() + dict.update(self, self.to_dict()) From 76010b1fceaa8e034f5ac4b75c5aaa3b766cf74b Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 24 Feb 2025 15:24:56 +0000 Subject: [PATCH 3650/3836] cloud: Switch remaining compute functions to proxy Change-Id: I921a2eea00efe393b611d4c5d97cbc18aad9fad1 Signed-off-by: Stephen Finucane --- openstack/cloud/_compute.py | 120 ++++++++++++++---- openstack/tests/unit/cloud/test_compute.py | 33 ++++- .../tests/unit/cloud/test_create_server.py | 28 ++-- .../tests/unit/cloud/test_security_groups.py | 83 +++++++++++- .../tests/unit/cloud/test_server_console.py | 4 +- .../unit/cloud/test_server_delete_metadata.py | 10 +- .../tests/unit/cloud/test_server_group.py | 13 +- .../unit/cloud/test_server_set_metadata.py | 18 ++- openstack/tests/unit/test_microversions.py | 40 ++++-- 9 files changed, 277 insertions(+), 72 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 6bbfaf89a..067f9d254 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -11,7 +11,6 @@ # limitations under the License. import base64 -import functools import operator import time import warnings @@ -387,31 +386,49 @@ def get_compute_limits(self, name_or_id=None): params['tenant_id'] = project.id return self.compute.get_limits(**params).absolute - def get_keypair(self, name_or_id, filters=None): + def get_keypair(self, name_or_id, filters=None, *, user_id=None): """Get a keypair by name or ID. :param name_or_id: Name or ID of the keypair. - :param filters: A dictionary of meta data to use for further filtering. - Elements of this dictionary may, themselves, be dictionaries. - Example:: + :param filters: **DEPRECATED** A dictionary of meta data to use for + further filtering. Elements of this dictionary may, themselves, be + dictionaries. Example:: {'last_name': 'Smith', 'other': {'gender': 'Female'}} OR A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" + :param user_id: User to retrieve keypair from. :returns: A compute ``Keypair`` object if found, else None. """ - return _utils._get_entity(self, 'keypair', name_or_id, filters) + if filters is not None: + warnings.warn( + "The 'filters' argument is deprecated; use 'search_keypairs' " + "instead", + os_warnings.RemovedInSDK60Warning, + ) + entities = self.search_keypairs(name_or_id, filters) + if not entities: + return None + + if len(entities) > 1: + raise exceptions.SDKException( + f"Multiple matches found for {name_or_id}", + ) + + return entities[0] + + return self.compute.find_keypair(name_or_id, user_id=user_id) def get_flavor(self, name_or_id, filters=None, get_extra=True): """Get a flavor by name or ID. :param name_or_id: Name or ID of the flavor. - :param filters: A dictionary of meta data to use for further filtering. - Elements of this dictionary may, themselves, be dictionaries. - Example:: + :param filters: **DEPRECATED** A dictionary of meta data to use for + further filtering. Elements of this dictionary may, themselves, be + dictionaries. Example:: {'last_name': 'Smith', 'other': {'gender': 'Female'}} @@ -423,6 +440,13 @@ def get_flavor(self, name_or_id, filters=None, get_extra=True): extra flavor specs. :returns: A compute ``Flavor`` object if found, else None. """ + if filters is not None: + warnings.warn( + "The 'filters' argument is deprecated; use 'search_flavors' " + "instead", + os_warnings.RemovedInSDK60Warning, + ) + if not filters: filters = {} return self.compute.find_flavor( @@ -487,9 +511,9 @@ def get_server( """Get a server by name or ID. :param name_or_id: Name or ID of the server. - :param filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: + :param filters: **DEPRECATED** A dictionary of meta data to use for + further filtering. Elements of this dictionary may, themselves, be + dictionaries. Example:: {'last_name': 'Smith', 'other': {'gender': 'Female'}} @@ -506,13 +530,37 @@ def get_server( the current auth scoped project. :returns: A compute ``Server`` object if found, else None. """ - searchfunc = functools.partial( - self.search_servers, - detailed=detailed, - bare=True, + if filters is not None: + warnings.warn( + "The 'filters' argument is deprecated; use " + "'search_servers' instead", + os_warnings.RemovedInSDK60Warning, + ) + entities = self.search_servers( + name_or_id, + filters, + detailed=detailed, + bare=True, + all_projects=all_projects, + ) + if not entities: + return None + + if len(entities) > 1: + raise exceptions.SDKException( + f"Multiple matches found for {name_or_id}", + ) + + server = entities[0] + return self._expand_server(server, detailed, bare) + + server = self.compute.find_server( + name_or_id, + # detailed controls whether we fetch more information about images, + # volumes etc., not the initial list operation + details=True, all_projects=all_projects, ) - server = _utils._get_entity(self, searchfunc, name_or_id, filters) return self._expand_server(server, detailed, bare) def _expand_server(self, server, detailed, bare): @@ -540,9 +588,9 @@ def get_server_group(self, name_or_id=None, filters=None): """Get a server group by name or ID. :param name_or_id: Name or ID of the server group. - :param filters: A dictionary of meta data to use for further filtering. - Elements of this dictionary may, themselves, be dictionaries. - Example:: + :param filters: **DEPRECATED** A dictionary of meta data to use for + further filtering. Elements of this dictionary may, themselves, be + dictionaries. Example:: { 'policy': 'affinity', @@ -554,7 +602,24 @@ def get_server_group(self, name_or_id=None, filters=None): :returns: A compute ``ServerGroup`` object if found, else None. """ - return _utils._get_entity(self, 'server_group', name_or_id, filters) + if filters is not None: + warnings.warn( + "The 'filters' argument is deprecated; use " + "'search_server_groups' instead", + os_warnings.RemovedInSDK60Warning, + ) + entities = self.search_server_groups(name_or_id, filters) + if not entities: + return None + + if len(entities) > 1: + raise exceptions.SDKException( + f"Multiple matches found for {name_or_id}", + ) + + return entities[0] + + return self.compute.find_server_group(name_or_id) def create_keypair(self, name, public_key=None): """Create a new keypair. @@ -1640,9 +1705,9 @@ def get_aggregate(self, name_or_id, filters=None): """Get an aggregate by name or ID. :param name_or_id: Name or ID of the aggregate. - :param dict filters: - A dictionary of meta data to use for further filtering. Elements - of this dictionary may, themselves, be dictionaries. Example:: + :param dict filters: **DEPRECATED** A dictionary of meta data to use + for further filtering. Elements of this dictionary may, themselves, + be dictionaries. Example:: { 'availability_zone': 'nova', @@ -1652,6 +1717,13 @@ def get_aggregate(self, name_or_id, filters=None): :returns: An aggregate dict or None if no matching aggregate is found. """ + if filters is not None: + warnings.warn( + "The 'filters' argument is deprecated; use " + "'search_aggregates' instead", + os_warnings.RemovedInSDK60Warning, + ) + return self.compute.find_aggregate(name_or_id, ignore_missing=True) def create_aggregate(self, name, availability_zone=None): diff --git a/openstack/tests/unit/cloud/test_compute.py b/openstack/tests/unit/cloud/test_compute.py index 2c06ecc85..f07984514 100644 --- a/openstack/tests/unit/cloud/test_compute.py +++ b/openstack/tests/unit/cloud/test_compute.py @@ -28,7 +28,17 @@ def test_get_server(self): dict( method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'] + 'compute', 'public', append=['servers', 'mickey'] + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', 'detail'], + qs_elements=['name=mickey'], ), json={'servers': [server1, server2]}, ), @@ -55,7 +65,17 @@ def test_get_server_not_found(self): dict( method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'] + 'compute', 'public', append=['servers', 'doesNotExist'] + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', 'detail'], + qs_elements=['name=doesNotExist'], ), json={'servers': []}, ), @@ -100,6 +120,7 @@ def test_list_servers(self): def test_list_server_private_ip(self): self.has_neutron = True + server_id = "97fe35e9-756a-41a2-960a-1d057d2c9ee4" fake_server = { "OS-EXT-STS:task_state": None, "addresses": { @@ -145,7 +166,7 @@ def test_list_server_private_ip(self): } ], }, - "id": "97fe35e9-756a-41a2-960a-1d057d2c9ee4", + "id": server_id, "security_groups": [{"name": "default"}], "user_id": "c17534835f8f42bf98fc367e0bf35e09", "OS-DCF:diskConfig": "MANUAL", @@ -277,9 +298,9 @@ def test_list_server_private_ip(self): dict( method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'] + 'compute', 'public', append=['servers', server_id] ), - json={'servers': [fake_server]}, + json={'server': fake_server}, ), dict( method='GET', @@ -298,7 +319,7 @@ def test_list_server_private_ip(self): ] ) - r = self.cloud.get_server('97fe35e9-756a-41a2-960a-1d057d2c9ee4') + r = self.cloud.get_server(server_id) self.assertEqual('10.4.0.13', r['private_v4']) diff --git a/openstack/tests/unit/cloud/test_create_server.py b/openstack/tests/unit/cloud/test_create_server.py index 59f0c4a08..6ce8168ea 100644 --- a/openstack/tests/unit/cloud/test_create_server.py +++ b/openstack/tests/unit/cloud/test_create_server.py @@ -181,16 +181,16 @@ def test_create_server_wait_server_error(self): dict( method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'] + 'compute', 'public', append=['servers', '1234'] ), - json={'servers': [build_server]}, + json={'server': build_server}, ), dict( method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'] + 'compute', 'public', append=['servers', '1234'] ), - json={'servers': [error_server]}, + json={'server': error_server}, ), ] ) @@ -243,9 +243,9 @@ def test_create_server_with_timeout(self): dict( method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'] + 'compute', 'public', append=['servers', '1234'] ), - json={'servers': [fake_server]}, + json={'server': fake_server}, ), ] ) @@ -539,6 +539,7 @@ def test_create_server_with_admin_pass_wait(self, mock_wait): flavor=dict(id='flavor-id'), admin_pass=admin_pass, wait=True, + timeout=0.01, ) # Assert that we did wait @@ -709,6 +710,7 @@ def test_create_server_wait(self, mock_wait): dict(id='image-id'), dict(id='flavor-id'), wait=True, + timeout=0.01, ) # This is a pretty dirty hack to ensure we in principle use object with @@ -728,7 +730,7 @@ def test_create_server_wait(self, mock_wait): ips=None, ip_pool=None, reuse=True, - timeout=180, + timeout=0.01, nat_destination=None, ) self.assert_calls() @@ -775,16 +777,9 @@ def test_create_server_no_addresses(self, mock_add_ips_to_server): dict( method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'] - ), - json={'servers': [build_server]}, - ), - dict( - method='GET', - uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'] + 'compute', 'public', append=['servers', '1234'] ), - json={'servers': [fake_server]}, + json={'server': fake_server}, ), dict( method='GET', @@ -820,6 +815,7 @@ def test_create_server_no_addresses(self, mock_add_ips_to_server): {'id': 'image-id'}, {'id': 'flavor-id'}, wait=True, + timeout=0.01, ) self.assert_calls() diff --git a/openstack/tests/unit/cloud/test_security_groups.py b/openstack/tests/unit/cloud/test_security_groups.py index 745d59300..080a72b27 100644 --- a/openstack/tests/unit/cloud/test_security_groups.py +++ b/openstack/tests/unit/cloud/test_security_groups.py @@ -852,7 +852,19 @@ def test_add_security_group_to_server_neutron(self): dict( method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'] + 'compute', + 'public', + append=['servers', 'server-name'], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', 'detail'], + qs_elements=['name=server-name'], ), json={'servers': [fake_server]}, ), @@ -932,7 +944,19 @@ def test_remove_security_group_from_server_neutron(self): dict( method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'] + 'compute', + 'public', + append=['servers', 'server-name'], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', 'detail'], + qs_elements=['name=server-name'], ), json={'servers': [fake_server]}, ), @@ -972,7 +996,21 @@ def test_add_bad_security_group_to_server_nova(self): self.get_nova_discovery_mock_dict(), dict( method='GET', - uri=f'{fakes.COMPUTE_ENDPOINT}/servers/detail', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', 'server-name'], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', 'detail'], + qs_elements=['name=server-name'], + ), json={'servers': [fake_server]}, ), dict( @@ -1003,7 +1041,19 @@ def test_add_bad_security_group_to_server_neutron(self): dict( method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'] + 'compute', + 'public', + append=['servers', 'server-name'], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', 'detail'], + qs_elements=['name=server-name'], ), json={'servers': [fake_server]}, ), @@ -1027,12 +1077,35 @@ def test_add_security_group_to_bad_server(self): # fake to get server by name, server-name must match fake_server = fakes.make_fake_server('1234', 'server-name', 'ACTIVE') + print( + self.get_mock_url( + 'compute', + 'public', + append=['servers', 'unknown-server-name'], + base_url_append='v2.1', + ) + ) + self.register_uris( [ self.get_nova_discovery_mock_dict(), dict( method='GET', - uri=f'{fakes.COMPUTE_ENDPOINT}/servers/detail', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', 'unknown-server-name'], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', 'detail'], + qs_elements=['name=unknown-server-name'], + ), json={'servers': [fake_server]}, ), ] diff --git a/openstack/tests/unit/cloud/test_server_console.py b/openstack/tests/unit/cloud/test_server_console.py index 4d1bef820..679b0ed40 100644 --- a/openstack/tests/unit/cloud/test_server_console.py +++ b/openstack/tests/unit/cloud/test_server_console.py @@ -51,8 +51,8 @@ def test_get_server_console_name_or_id(self): self.get_nova_discovery_mock_dict(), dict( method='GET', - uri=f'{fakes.COMPUTE_ENDPOINT}/servers/detail', - json={"servers": [self.server]}, + uri=f'{fakes.COMPUTE_ENDPOINT}/servers/{self.server_id}', + json={'server': self.server}, ), dict( method='POST', diff --git a/openstack/tests/unit/cloud/test_server_delete_metadata.py b/openstack/tests/unit/cloud/test_server_delete_metadata.py index 94f912433..b2d581f32 100644 --- a/openstack/tests/unit/cloud/test_server_delete_metadata.py +++ b/openstack/tests/unit/cloud/test_server_delete_metadata.py @@ -43,9 +43,9 @@ def test_server_delete_metadata_with_exception(self): dict( method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'] + 'compute', 'public', append=['servers', self.server_id] ), - json={'servers': [self.fake_server]}, + json={'server': self.fake_server}, ), dict( method='DELETE', @@ -67,7 +67,7 @@ def test_server_delete_metadata_with_exception(self): self.assertRaises( exceptions.NotFoundException, self.cloud.delete_server_metadata, - self.server_name, + self.server_id, ['key'], ) @@ -80,9 +80,9 @@ def test_server_delete_metadata(self): dict( method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'] + 'compute', 'public', append=['servers', self.server_id] ), - json={'servers': [self.fake_server]}, + json={'server': self.fake_server}, ), dict( method='DELETE', diff --git a/openstack/tests/unit/cloud/test_server_group.py b/openstack/tests/unit/cloud/test_server_group.py index 14c10d23c..bf37d30d6 100644 --- a/openstack/tests/unit/cloud/test_server_group.py +++ b/openstack/tests/unit/cloud/test_server_group.py @@ -62,7 +62,18 @@ def test_delete_server_group(self): dict( method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['os-server-groups'] + 'compute', + 'public', + append=['os-server-groups', self.group_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['os-server-groups'], ), json={'server_groups': [self.fake_group]}, ), diff --git a/openstack/tests/unit/cloud/test_server_set_metadata.py b/openstack/tests/unit/cloud/test_server_set_metadata.py index d27724a27..522c1270f 100644 --- a/openstack/tests/unit/cloud/test_server_set_metadata.py +++ b/openstack/tests/unit/cloud/test_server_set_metadata.py @@ -40,7 +40,19 @@ def test_server_set_metadata_with_exception(self): dict( method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'] + 'compute', + 'public', + append=['servers', self.server_name], + ), + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', 'detail'], + qs_elements=[f'name={self.server_name}'], ), json={'servers': [self.fake_server]}, ), @@ -75,9 +87,9 @@ def test_server_set_metadata(self): dict( method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'] + 'compute', 'public', append=['servers', self.server_id] ), - json={'servers': [self.fake_server]}, + json={'server': self.fake_server}, ), dict( method='POST', diff --git a/openstack/tests/unit/test_microversions.py b/openstack/tests/unit/test_microversions.py index e77646b3c..a72189f0d 100644 --- a/openstack/tests/unit/test_microversions.py +++ b/openstack/tests/unit/test_microversions.py @@ -67,50 +67,70 @@ def test_get_bad_default_min_microversion(self): def test_inferred_default_microversion(self): self.cloud.config.config['compute_api_version'] = '2.42' - server1 = fakes.make_fake_server('123', 'mickey') - server2 = fakes.make_fake_server('345', 'mouse') + server = fakes.make_fake_server('123', 'mickey') self.register_uris( [ dict( method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'] + 'compute', 'public', append=['servers', 'mickey'] ), request_headers={'OpenStack-API-Version': 'compute 2.42'}, - json={'servers': [server1, server2]}, + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', 'detail'], + qs_elements=['name=mickey'], + ), + request_headers={'OpenStack-API-Version': 'compute 2.42'}, + json={'servers': [server]}, ), ] ) r = self.cloud.get_server('mickey', bare=True) self.assertIsNotNone(r) - self.assertEqual(server1['name'], r['name']) + self.assertEqual(server['name'], r['name']) self.assert_calls() def test_default_microversion(self): self.cloud.config.config['compute_default_microversion'] = '2.42' - server1 = fakes.make_fake_server('123', 'mickey') - server2 = fakes.make_fake_server('345', 'mouse') + server = fakes.make_fake_server('123', 'mickey') self.register_uris( [ dict( method='GET', uri=self.get_mock_url( - 'compute', 'public', append=['servers', 'detail'] + 'compute', 'public', append=['servers', 'mickey'] + ), + request_headers={'OpenStack-API-Version': 'compute 2.42'}, + status_code=404, + ), + dict( + method='GET', + uri=self.get_mock_url( + 'compute', + 'public', + append=['servers', 'detail'], + qs_elements=['name=mickey'], ), request_headers={'OpenStack-API-Version': 'compute 2.42'}, - json={'servers': [server1, server2]}, + json={'servers': [server]}, ), ] ) r = self.cloud.get_server('mickey', bare=True) self.assertIsNotNone(r) - self.assertEqual(server1['name'], r['name']) + self.assertEqual(server['name'], r['name']) self.assert_calls() From 8f333517dcf9ad67de8cf444ddb3c4783d6fc74a Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 25 Jun 2024 16:49:57 +0100 Subject: [PATCH 3651/3836] cloud: Add missing parameter docs Change-Id: I098db6a876beb4ca2f7d5ec658d79b9db7906059 Signed-off-by: Stephen Finucane --- openstack/cloud/_compute.py | 71 ++++++++++++++++++++++++++++++------- 1 file changed, 59 insertions(+), 12 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 067f9d254..650c29cb7 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -80,6 +80,7 @@ def get_flavor_name(self, flavor_id): """Get the name of a flavor. :param flavor_id: ID of the flavor. + :returns: The name of the flavor if a match if found, else None. """ flavor = self.get_flavor(flavor_id, get_extra=False) @@ -97,7 +98,7 @@ def get_flavor_by_ram(self, ram, include=None, get_extra=True): :param int ram: Minimum amount of RAM. :param string include: If given, will return a flavor whose name contains this string as a substring. - :param get_extra: + :param get_extra: Whether to fetch extra specs. :returns: A compute ``Flavor`` object. :raises: :class:`~openstack.exceptions.SDKException` if no @@ -116,8 +117,18 @@ def get_flavor_by_ram(self, ram, include=None, get_extra=True): def search_keypairs(self, name_or_id=None, filters=None): """Search keypairs. - :param name_or_id: - :param filters: + :param name_or_id: Name or unique ID of the keypair(s). + :param filters: A dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. + Example:: + + {'last_name': 'Smith', 'other': {'gender': 'Female'}} + + OR + + A string containing a jmespath expression for further filtering. + Invalid filters will be ignored. + :returns: A list of compute ``Keypair`` objects matching the search criteria. """ @@ -129,9 +140,10 @@ def search_keypairs(self, name_or_id=None, filters=None): def search_flavors(self, name_or_id=None, filters=None, get_extra=True): """Search flavors. - :param name_or_id: - :param flavors: - :param get_extra: + :param name_or_id: Name or unique ID of the flavor(s). + :param filters: + :param get_extra: Whether to fetch extra specs. + :returns: A list of compute ``Flavor`` objects matching the search criteria. """ @@ -148,11 +160,21 @@ def search_servers( ): """Search servers. - :param name_or_id: - :param filters: + :param name_or_id: Name or unique ID of the server(s). + :param filters: A dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. + Example:: + + {'last_name': 'Smith', 'other': {'gender': 'Female'}} + + OR + + A string containing a jmespath expression for further filtering. + Invalid filters will be ignored. :param detailed: :param all_projects: :param bare: + :returns: A list of compute ``Server`` objects matching the search criteria. """ @@ -165,7 +187,16 @@ def search_server_groups(self, name_or_id=None, filters=None): """Search server groups. :param name_or_id: Name or unique ID of the server group(s). - :param filters: A dict containing additional filters to use. + :param filters: A dictionary of meta data to use for further filtering. + Elements of this dictionary may, themselves, be dictionaries. + Example:: + + {'last_name': 'Smith', 'other': {'gender': 'Female'}} + + OR + + A string containing a jmespath expression for further filtering. + Invalid filters will be ignored. :returns: A list of compute ``ServerGroup`` objects matching the search criteria. @@ -273,6 +304,8 @@ def add_server_security_groups(self, server, security_groups): Add existing security groups to an existing server. If the security groups are already present on the server this will continue unaffected. + :param server: The server to remove security groups from. + :param security_groups: A list of security groups to remove. :returns: False if server or security groups are undefined, True otherwise. :raises: :class:`~openstack.exceptions.SDKException` on operation @@ -297,6 +330,8 @@ def remove_server_security_groups(self, server, security_groups): security groups are not present on the server this will continue unaffected. + :param server: The server to remove security groups from. + :param security_groups: A list of security groups to remove. :returns: False if server or security groups are undefined, True otherwise. :raises: :class:`~openstack.exceptions.SDKException` on operation @@ -1612,6 +1647,7 @@ def set_flavor_specs(self, flavor_id, extra_specs): :param string flavor_id: ID of the flavor to update. :param dict extra_specs: Dictionary of key-value pairs. + :returns: None :raises: :class:`~openstack.exceptions.SDKException` on operation error. :raises: :class:`~openstack.exceptions.BadRequestException` if flavor @@ -1625,6 +1661,7 @@ def unset_flavor_specs(self, flavor_id, keys): :param string flavor_id: ID of the flavor to update. :param keys: List of spec keys to delete. + :returns: None :raises: :class:`~openstack.exceptions.SDKException` on operation error. :raises: :class:`~openstack.exceptions.BadRequestException` if flavor @@ -1639,6 +1676,7 @@ def add_flavor_access(self, flavor_id, project_id): :param string flavor_id: ID of the private flavor. :param string project_id: ID of the project/tenant. + :returns: None :raises: :class:`~openstack.exceptions.SDKException` on operation error. """ @@ -1650,6 +1688,7 @@ def remove_flavor_access(self, flavor_id, project_id): :param string flavor_id: ID of the private flavor. :param string project_id: ID of the project/tenant. + :returns: None :raises: :class:`~openstack.exceptions.SDKException` on operation error. """ @@ -1669,7 +1708,8 @@ def list_flavor_access(self, flavor_id): def list_hypervisors(self, filters=None): """List all hypervisors - :param filters: + :param filters: Additional query parameters passed to the API server. + :returns: A list of compute ``Hypervisor`` objects. """ if not filters: @@ -1680,8 +1720,15 @@ def list_hypervisors(self, filters=None): def search_aggregates(self, name_or_id=None, filters=None): """Seach host aggregates. - :param name: aggregate name or id. - :param filters: a dict containing additional filters to use. + :param name: Aggregate name or id. + :param dict filters: A dictionary of meta data to use for further + filtering. Elements of this dictionary may, themselves, be + dictionaries. Example:: + + { + 'availability_zone': 'nova', + 'metadata': {'cpu_allocation_ratio': '1.0'}, + } :returns: A list of compute ``Aggregate`` objects matching the search criteria. From c68ae72d75f641b437a36c724c0e60ddc31c019e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 24 Feb 2025 15:58:08 +0000 Subject: [PATCH 3652/3836] cloud: Deprecate jmespath-style filters jmespath is no longer well maintained [1]. This has always been a bit of a weird feature in the library so let's deprecate it for removal. [1] https://github.com/jmespath/jmespath.py Change-Id: I49c01f8e17ff9cf8a4143401bf9fce7d2d1a72b7 Signed-off-by: Stephen Finucane --- openstack/cloud/_utils.py | 8 ++++++++ openstack/proxy.py | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index 2861ca551..8978b7746 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -19,6 +19,7 @@ import re import socket import uuid +import warnings from decorator import decorator import jmespath @@ -26,6 +27,7 @@ from openstack import _log from openstack import exceptions +from openstack import warnings as os_warnings def _dictify_resource(resource): @@ -100,6 +102,12 @@ def _filter_list(data, name_or_id, filters): return data if isinstance(filters, str): + warnings.warn( + 'Support for jmespath-style filters is deprecated and will be ' + 'removed in a future release. Consider using dictionary-style ' + 'filters instead.', + os_warnings.RemovedInSDK60Warning, + ) return jmespath.search(filters, data) def _dict_filter(f, d): diff --git a/openstack/proxy.py b/openstack/proxy.py index 3dcfd22fb..9adfe24fd 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -770,6 +770,11 @@ def _list( ) if jmespath_filters and isinstance(jmespath_filters, str): + warnings.warn( + 'Support for jmespath-style filters is deprecated and will be ' + 'removed in a future release.', + os_warnings.RemovedInSDK60Warning, + ) return jmespath.search(jmespath_filters, data) return data From 82789523ab75fb755da630614b68b98b1dfb96c4 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 25 Oct 2024 18:35:05 +0100 Subject: [PATCH 3653/3836] typing: Add missing proxy arguments The Proxy._delete method requires a 'value' argument, even if that argument is 'None'. This currently works only because the '_check_resource' decorator is filling in the missing value for us. Change-Id: I2f0305edfc8c9bc5ecda182f0efcd8cde8b142ee Signed-off-by: Stephen Finucane --- openstack/compute/v2/_proxy.py | 1 + openstack/identity/v3/_proxy.py | 1 + openstack/image/v2/_proxy.py | 1 + openstack/network/v2/_proxy.py | 12 ++++++------ openstack/tests/unit/compute/v2/test_proxy.py | 6 +++--- openstack/tests/unit/identity/v3/test_proxy.py | 4 ++-- openstack/tests/unit/image/v2/test_proxy.py | 4 ++-- 7 files changed, 16 insertions(+), 13 deletions(-) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 18c403cf0..e4f50209a 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -2058,6 +2058,7 @@ def delete_volume_attachment(self, server, volume, ignore_missing=True): self._delete( _volume_attachment.VolumeAttachment, + None, id=volume_id, server_id=server_id, ignore_missing=ignore_missing, diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index cd437ab3c..3c4a9211c 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -292,6 +292,7 @@ def delete_domain_config(self, domain, ignore_missing=True): domain_id = resource.Resource._get_id(domain) self._delete( _domain_config.DomainConfig, + None, domain_id=domain_id, ignore_missing=ignore_missing, ) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index b2de555bd..44ee0e72c 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -1026,6 +1026,7 @@ def remove_member(self, member, image=None, ignore_missing=True): member_id = resource.Resource._get_id(member) self._delete( _member.Member, + None, member_id=member_id, image_id=image_id, ignore_missing=ignore_missing, diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 6994f6f35..ee2ed815a 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -196,12 +196,12 @@ class Proxy(proxy.Proxy): @proxy._check_resource(strict=False) def _update( self, - resource_type: type[resource.Resource], - value: ty.Union[str, resource.Resource], + resource_type: type[resource.ResourceT], + value: ty.Union[str, resource.ResourceT], base_path: ty.Optional[str] = None, if_revision: ty.Optional[int] = None, **attrs: ty.Any, - ) -> resource.Resource: + ) -> resource.ResourceT: if ( issubclass(resource_type, _base.NetworkResource) and if_revision is not None @@ -215,12 +215,12 @@ def _update( @proxy._check_resource(strict=False) def _delete( self, - resource_type: type[resource.Resource], - value: ty.Union[str, resource.Resource], + resource_type: type[resource.ResourceT], + value: ty.Union[str, resource.ResourceT], ignore_missing: bool = True, if_revision: ty.Optional[int] = None, **attrs: ty.Any, - ) -> ty.Optional[resource.Resource]: + ) -> ty.Optional[resource.ResourceT]: if ( issubclass(resource_type, _base.NetworkResource) and if_revision is not None diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 77f41a356..2d2bc2f43 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -663,7 +663,7 @@ def test_volume_attachment_delete(self): ignore_missing=False, method_args=[fake_server, fake_volume], method_kwargs={}, - expected_args=[], + expected_args=[None], expected_kwargs={ 'id': fake_volume.id, 'server_id': fake_server.id, @@ -682,7 +682,7 @@ def test_volume_attachment_delete__ignore(self): ignore_missing=True, method_args=[fake_server, fake_volume], method_kwargs={}, - expected_args=[], + expected_args=[None], expected_kwargs={ 'id': fake_volume.id, 'server_id': fake_server.id, @@ -708,7 +708,7 @@ def test_volume_attachment_delete__legacy_parameters(self): ignore_missing=False, method_args=[fake_volume.id, fake_server.id], method_kwargs={}, - expected_args=[], + expected_args=[None], expected_kwargs={ 'id': fake_volume.id, 'server_id': fake_server.id, diff --git a/openstack/tests/unit/identity/v3/test_proxy.py b/openstack/tests/unit/identity/v3/test_proxy.py index e582f31f8..816a4b8da 100644 --- a/openstack/tests/unit/identity/v3/test_proxy.py +++ b/openstack/tests/unit/identity/v3/test_proxy.py @@ -116,7 +116,7 @@ def test_domain_config_delete(self): ignore_missing=False, method_args=['domain_id'], method_kwargs={}, - expected_args=[], + expected_args=[None], expected_kwargs={ 'domain_id': 'domain_id', }, @@ -129,7 +129,7 @@ def test_domain_config_delete_ignore(self): ignore_missing=True, method_args=['domain_id'], method_kwargs={}, - expected_args=[], + expected_args=[None], expected_kwargs={ 'domain_id': 'domain_id', }, diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 0885e0b7b..b5c183caa 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -596,7 +596,7 @@ def test_member_delete(self): self.proxy.remove_member, method_args=["member_id"], method_kwargs={"image": "image_id", "ignore_missing": False}, - expected_args=[_member.Member], + expected_args=[_member.Member, None], expected_kwargs={ "member_id": "member_id", "image_id": "image_id", @@ -610,7 +610,7 @@ def test_member_delete_ignore(self): self.proxy.remove_member, method_args=["member_id"], method_kwargs={"image": "image_id"}, - expected_args=[_member.Member], + expected_args=[_member.Member, None], expected_kwargs={ "member_id": "member_id", "image_id": "image_id", From e9f60bbf72927a19c50697ef3425b47c0e56be2a Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 25 Feb 2025 10:27:15 +0000 Subject: [PATCH 3654/3836] typing: Don't abuse Proxy._update The failover and configure operations of load balancers can and should be implemented as discrete methods, rather than using a custom resource that has no bearing on a real API resource. Make it so. Change-Id: Ib213ece0ffa3188b805c27040533429987653dd5 Signed-off-by: Stephen Finucane --- openstack/load_balancer/v2/_proxy.py | 15 +++--- openstack/load_balancer/v2/amphora.py | 48 +++++++++++++++++++ openstack/load_balancer/v2/load_balancer.py | 24 ++++++++++ .../tests/unit/load_balancer/v2/test_proxy.py | 21 ++++---- 4 files changed, 90 insertions(+), 18 deletions(-) diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index 4474955a2..0e2cb618c 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -189,7 +189,7 @@ def wait_for_load_balancer( attribute='provisioning_status', ) - def failover_load_balancer(self, load_balancer, **attrs): + def failover_load_balancer(self, load_balancer): """Failover a load balancer :param load_balancer: The value can be the ID of a load balancer @@ -198,7 +198,8 @@ def failover_load_balancer(self, load_balancer, **attrs): :returns: ``None`` """ - return self._update(_lb.LoadBalancerFailover, lb_id=load_balancer) + lb = self._get_resource(_lb.LoadBalancer, load_balancer) + lb.failover(self) def create_listener(self, **attrs): """Create a new listener from attributes @@ -1072,23 +1073,25 @@ def find_amphora(self, amphora_id, ignore_missing=True): _amphora.Amphora, amphora_id, ignore_missing=ignore_missing ) - def configure_amphora(self, amphora_id, **attrs): + def configure_amphora(self, amphora_id): """Update the configuration of an amphora agent :param amphora_id: The ID of an amphora :returns: ``None`` """ - return self._update(_amphora.AmphoraConfig, amphora_id=amphora_id) + lb = self._get_resource(_amphora.Amphora, amphora_id) + lb.configure(self) - def failover_amphora(self, amphora_id, **attrs): + def failover_amphora(self, amphora_id): """Failover an amphora :param amphora_id: The ID of an amphora :returns: ``None`` """ - return self._update(_amphora.AmphoraFailover, amphora_id=amphora_id) + lb = self._get_resource(_amphora.Amphora, amphora_id) + lb.failover(self) def create_availability_zone_profile(self, **attrs): """Create a new availability zone profile from attributes diff --git a/openstack/load_balancer/v2/amphora.py b/openstack/load_balancer/v2/amphora.py index 31f22db25..f8e9c4de5 100644 --- a/openstack/load_balancer/v2/amphora.py +++ b/openstack/load_balancer/v2/amphora.py @@ -12,7 +12,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import exceptions from openstack import resource +from openstack import utils class Amphora(resource.Resource): @@ -93,7 +95,52 @@ class Amphora(resource.Resource): #: The ID of the compute flavor used for the amphora. compute_flavor = resource.Body('compute_flavor') + def configure(self, session): + """Configure load balancer. + Update the amphora agent configuration. This will push the new + configuration to the amphora agent and will update the configuration + options that are mutatable. + + :param session: The session to use for making this request. + :returns: None + """ + session = self._get_session(session) + version = self._get_microversion(session, action='patch') + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'config') + + response = session.put( + request.url, + headers=request.headers, + microversion=version, + ) + + msg = f"Failed to configure load balancer {self.id}" + exceptions.raise_from_response(response, error_message=msg) + + def failover(self, session): + """Failover load balancer. + + :param session: The session to use for making this request. + :returns: None + """ + session = self._get_session(session) + version = self._get_microversion(session, action='patch') + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'failover') + + response = session.put( + request.url, + headers=request.headers, + microversion=version, + ) + + msg = f"Failed to failover load balancer {self.id}" + exceptions.raise_from_response(response, error_message=msg) + + +# TODO(stephenfin): Delete this: it's useless class AmphoraConfig(resource.Resource): base_path = '/octavia/amphorae/%(amphora_id)s/config' @@ -119,6 +166,7 @@ def commit( return super().commit(session, prepend_key, has_body, *args, *kwargs) +# TODO(stephenfin): Delete this: it's useless class AmphoraFailover(resource.Resource): base_path = '/octavia/amphorae/%(amphora_id)s/failover' diff --git a/openstack/load_balancer/v2/load_balancer.py b/openstack/load_balancer/v2/load_balancer.py index d435c2a70..02fd6a235 100644 --- a/openstack/load_balancer/v2/load_balancer.py +++ b/openstack/load_balancer/v2/load_balancer.py @@ -9,8 +9,11 @@ # 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.common import tag +from openstack import exceptions from openstack import resource +from openstack import utils class LoadBalancer(resource.Resource, tag.TagMixin): @@ -99,6 +102,26 @@ def delete(self, session, error_message=None, **kwargs): ) return self + def failover(self, session): + """Failover load balancer. + + :param session: The session to use for making this request. + :returns: None + """ + session = self._get_session(session) + version = self._get_microversion(session, action='patch') + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'failover') + + response = session.put( + request.url, + headers=request.headers, + microversion=version, + ) + + msg = f"Failed to failover load balancer {self.id}" + exceptions.raise_from_response(response, error_message=msg) + class LoadBalancerStats(resource.Resource): resource_key = 'stats' @@ -126,6 +149,7 @@ class LoadBalancerStats(resource.Resource): total_connections = resource.Body('total_connections', type=int) +# TODO(stephenfin): Delete this: it's useless class LoadBalancerFailover(resource.Resource): base_path = '/lbaas/loadbalancers/%(lb_id)s/failover' diff --git a/openstack/tests/unit/load_balancer/v2/test_proxy.py b/openstack/tests/unit/load_balancer/v2/test_proxy.py index db7391218..8cb57f7f3 100644 --- a/openstack/tests/unit/load_balancer/v2/test_proxy.py +++ b/openstack/tests/unit/load_balancer/v2/test_proxy.py @@ -103,12 +103,11 @@ def test_load_balancer_update(self): self.verify_update(self.proxy.update_load_balancer, lb.LoadBalancer) def test_load_balancer_failover(self): - self.verify_update( + self._verify( + "openstack.load_balancer.v2.load_balancer.LoadBalancer.failover", self.proxy.failover_load_balancer, - lb.LoadBalancerFailover, method_args=[self.LB_ID], - expected_args=[], - expected_kwargs={'lb_id': self.LB_ID}, + expected_args=[self.proxy], ) def test_listeners(self): @@ -411,21 +410,19 @@ def test_amphora_find(self): self.verify_find(self.proxy.find_amphora, amphora.Amphora) def test_amphora_configure(self): - self.verify_update( + self._verify( + "openstack.load_balancer.v2.amphora.Amphora.configure", self.proxy.configure_amphora, - amphora.AmphoraConfig, method_args=[self.AMPHORA_ID], - expected_args=[], - expected_kwargs={'amphora_id': self.AMPHORA_ID}, + expected_args=[self.proxy], ) def test_amphora_failover(self): - self.verify_update( + self._verify( + "openstack.load_balancer.v2.amphora.Amphora.failover", self.proxy.failover_amphora, - amphora.AmphoraFailover, method_args=[self.AMPHORA_ID], - expected_args=[], - expected_kwargs={'amphora_id': self.AMPHORA_ID}, + expected_args=[self.proxy], ) def test_availability_zone_profiles(self): From 6438e3b653a64a790f4ec9b30c9ebd0e63ae6055 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 19 Feb 2025 20:24:36 +0000 Subject: [PATCH 3655/3836] typing: Remove duplicate TypeVar No need for two of these. Change-Id: Ic7ea92680641bee477e07b7ea74f1678d98bb514 Signed-off-by: Stephen Finucane --- openstack/proxy.py | 61 ++++++++++++++++++++++------------------------ 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/openstack/proxy.py b/openstack/proxy.py index 3dcfd22fb..f7067f9c2 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -36,9 +36,6 @@ from openstack import connection -ResourceType = ty.TypeVar('ResourceType', bound=resource.Resource) - - # The _check_resource decorator is used on Proxy methods to ensure that # the `actual` argument is in fact the type of the `expected` argument. # It does so under two cases: @@ -445,10 +442,10 @@ def _get_connection(self): def _get_resource( self, - resource_type: type[ResourceType], - value: ty.Union[None, str, ResourceType, utils.Munch], + resource_type: type[resource.ResourceT], + value: ty.Union[None, str, resource.ResourceT, utils.Munch], **attrs: ty.Any, - ) -> ResourceType: + ) -> resource.ResourceT: """Get a resource object to work on :param resource_type: The type of resource to operate on. This should @@ -497,39 +494,39 @@ def _get_uri_attribute(self, child, parent, name): @ty.overload def _find( self, - resource_type: type[ResourceType], + resource_type: type[resource.ResourceT], name_or_id: str, ignore_missing: ty.Literal[True] = True, **attrs: ty.Any, - ) -> ty.Optional[ResourceType]: ... + ) -> ty.Optional[resource.ResourceT]: ... @ty.overload def _find( self, - resource_type: type[ResourceType], + resource_type: type[resource.ResourceT], name_or_id: str, ignore_missing: ty.Literal[False], **attrs: ty.Any, - ) -> ResourceType: ... + ) -> resource.ResourceT: ... # excuse the duplication here: it's mypy's fault # https://github.com/python/mypy/issues/14764 @ty.overload def _find( self, - resource_type: type[ResourceType], + resource_type: type[resource.ResourceT], name_or_id: str, ignore_missing: bool, **attrs: ty.Any, - ) -> ty.Optional[ResourceType]: ... + ) -> ty.Optional[resource.ResourceT]: ... def _find( self, - resource_type: type[ResourceType], + resource_type: type[resource.ResourceT], name_or_id: str, ignore_missing: bool = True, **attrs: ty.Any, - ) -> ty.Optional[ResourceType]: + ) -> ty.Optional[resource.ResourceT]: """Find a resource :param name_or_id: The name or ID of a resource to find. @@ -551,11 +548,11 @@ def _find( @_check_resource(strict=False) def _delete( self, - resource_type: type[ResourceType], - value: ty.Union[str, ResourceType], + resource_type: type[resource.ResourceT], + value: ty.Union[str, resource.ResourceT], ignore_missing: bool = True, **attrs: ty.Any, - ) -> ty.Optional[ResourceType]: + ) -> ty.Optional[resource.ResourceT]: """Delete a resource :param resource_type: The type of resource to delete. This should @@ -593,11 +590,11 @@ def _delete( @_check_resource(strict=False) def _update( self, - resource_type: type[ResourceType], - value: ty.Union[str, ResourceType], + resource_type: type[resource.ResourceT], + value: ty.Union[str, resource.ResourceT], base_path: ty.Optional[str] = None, **attrs: ty.Any, - ) -> ResourceType: + ) -> resource.ResourceT: """Update a resource :param resource_type: The type of resource to update. @@ -623,10 +620,10 @@ def _update( def _create( self, - resource_type: type[ResourceType], + resource_type: type[resource.ResourceT], base_path: ty.Optional[str] = None, **attrs: ty.Any, - ) -> ResourceType: + ) -> resource.ResourceT: """Create a resource from attributes :param resource_type: The type of resource to create. @@ -659,10 +656,10 @@ def _create( def _bulk_create( self, - resource_type: type[ResourceType], + resource_type: type[resource.ResourceT], data: list[dict[str, ty.Any]], base_path: ty.Optional[str] = None, - ) -> ty.Generator[ResourceType, None, None]: + ) -> ty.Generator[resource.ResourceT, None, None]: """Create a resource from attributes :param resource_type: The type of resource to create. @@ -685,13 +682,13 @@ def _bulk_create( @_check_resource(strict=False) def _get( self, - resource_type: type[ResourceType], - value: ty.Union[str, ResourceType, None] = None, + resource_type: type[resource.ResourceT], + value: ty.Union[str, resource.ResourceT, None] = None, requires_id: bool = True, base_path: ty.Optional[str] = None, skip_cache: bool = False, **attrs: ty.Any, - ) -> ResourceType: + ) -> resource.ResourceT: """Fetch a resource :param resource_type: The type of resource to get. @@ -726,12 +723,12 @@ def _get( def _list( self, - resource_type: type[ResourceType], + resource_type: type[resource.ResourceT], paginated: bool = True, base_path: ty.Optional[str] = None, jmespath_filters: ty.Optional[str] = None, **attrs: ty.Any, - ) -> ty.Generator[ResourceType, None, None]: + ) -> ty.Generator[resource.ResourceT, None, None]: """List a resource :param resource_type: The type of resource to list. This should @@ -776,11 +773,11 @@ def _list( def _head( self, - resource_type: type[ResourceType], - value: ty.Union[str, ResourceType, None] = None, + resource_type: type[resource.ResourceT], + value: ty.Union[str, resource.ResourceT, None] = None, base_path: ty.Optional[str] = None, **attrs: ty.Any, - ) -> ResourceType: + ) -> resource.ResourceT: """Retrieve a resource's header :param resource_type: The type of resource to retrieve. From 73a7c8e5dbd9f37c7ae1e89d834db96c49429de0 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 26 Feb 2025 11:23:36 +0000 Subject: [PATCH 3656/3836] typing: Add (more) missing proxy arguments The Proxy._update method also requires a value argument. Once again, this currently works only because the '_check_resource' decorator is filling in the missing value for us. mypy didn't/couldn't pick this up because of our use of untyped '**attrs' in both the base class and all callers. Another one we'd like to resolve long-term but haven't managed to do yet. Change-Id: I73920092a474744f1d7b154d36bc2af1aeb5eb53 Signed-off-by: Stephen Finucane --- openstack/compute/v2/_proxy.py | 1 + openstack/identity/v3/_proxy.py | 1 + openstack/image/v2/_proxy.py | 1 + openstack/tests/unit/compute/v2/test_proxy.py | 1 + openstack/tests/unit/identity/v3/test_proxy.py | 2 +- openstack/tests/unit/image/v2/test_proxy.py | 2 +- 6 files changed, 6 insertions(+), 2 deletions(-) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index e4f50209a..f48e1f684 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -1988,6 +1988,7 @@ def update_volume_attachment( return self._update( _volume_attachment.VolumeAttachment, + None, id=volume_id, server_id=server_id, volume_id=new_volume_id, diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 3c4a9211c..84c9ef25a 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -329,6 +329,7 @@ def update_domain_config(self, domain, **attrs): domain_id = resource.Resource._get_id(domain) return self._update( _domain_config.DomainConfig, + None, domain_id=domain_id, **attrs, ) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 44ee0e72c..307e2af9e 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -1109,6 +1109,7 @@ def update_member(self, member, image, **attrs): image_id = resource.Resource._get_id(image) return self._update( _member.Member, + None, member_id=member_id, image_id=image_id, **attrs, diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 2d2bc2f43..0e9cd9843 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -644,6 +644,7 @@ def test_volume_attachment_update(self): volume_attachment.VolumeAttachment, method_args=[], method_kwargs={'server': 'server_id', 'volume': 'volume_id'}, + expected_args=[None], expected_kwargs={ 'id': 'volume_id', 'server_id': 'server_id', diff --git a/openstack/tests/unit/identity/v3/test_proxy.py b/openstack/tests/unit/identity/v3/test_proxy.py index 816a4b8da..faaa3a5e6 100644 --- a/openstack/tests/unit/identity/v3/test_proxy.py +++ b/openstack/tests/unit/identity/v3/test_proxy.py @@ -158,7 +158,7 @@ def test_domain_config_update(self): domain_config.DomainConfig, method_args=['domain_id'], method_kwargs={}, - expected_args=[], + expected_args=[None], expected_kwargs={ 'domain_id': 'domain_id', }, diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index b5c183caa..c4644ef75 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -623,7 +623,7 @@ def test_member_update(self): "openstack.proxy.Proxy._update", self.proxy.update_member, method_args=['member_id', 'image_id'], - expected_args=[_member.Member], + expected_args=[_member.Member, None], expected_kwargs={'member_id': 'member_id', 'image_id': 'image_id'}, ) From 6822ab9d168fe906aafbd6c1611546ee8e9e1bfc Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 6 Aug 2024 15:17:40 +0100 Subject: [PATCH 3657/3836] proxy: Remove '_check_resource' decorator This was never being configured with strict=true, meaning the logic can be moved into the '_get_resource' helper. Fixing this highlights a few places where we need to be a little more permissive in our proxy function signatures, so these are adjusted along with the doc strings for same. Change-Id: Iba51d19ddb1420f297bc79402f008512c041a721 Signed-off-by: Stephen Finucane --- openstack/block_storage/v2/_proxy.py | 9 ++- openstack/network/v2/_proxy.py | 14 ++-- openstack/proxy.py | 108 +++++++++++---------------- openstack/tests/unit/test_proxy.py | 42 ----------- 4 files changed, 54 insertions(+), 119 deletions(-) diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 2ae9b5642..0cd12a39b 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -758,10 +758,13 @@ def get_limits(self, project=None): :class:`~openstack.block_storage.v2.limits.RateLimit` :rtype: :class:`~openstack.block_storage.v2.limits.Limits` """ - params = {} if project: - params['project_id'] = resource.Resource._get_id(project) - return self._get(_limits.Limits, requires_id=False, **params) + return self._get( + _limits.Limits, + requires_id=False, + project_id=resource.Resource._get_id(project), + ) + return self._get(_limits.Limits, requires_id=False) # ========== Capabilities ========== diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index ee2ed815a..aa3c5f338 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -193,11 +193,10 @@ class Proxy(proxy.Proxy): "vpn_service": _vpn_service.VpnService, } - @proxy._check_resource(strict=False) def _update( self, resource_type: type[resource.ResourceT], - value: ty.Union[str, resource.ResourceT], + value: ty.Union[str, resource.ResourceT, None], base_path: ty.Optional[str] = None, if_revision: ty.Optional[int] = None, **attrs: ty.Any, @@ -212,11 +211,10 @@ def _update( return res.commit(self, base_path=base_path) - @proxy._check_resource(strict=False) def _delete( self, resource_type: type[resource.ResourceT], - value: ty.Union[str, resource.ResourceT], + value: ty.Union[str, resource.ResourceT, None], ignore_missing: bool = True, if_revision: ty.Optional[int] = None, **attrs: ty.Any, @@ -3594,10 +3592,10 @@ def delete_qos_minimum_packet_rate_rule( rule belongs or a :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the resource does not exist. When set to ``True``, no exception - will be set when attempting to delete a nonexistent minimum packet - rate rule. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the resource does not exist. When set to ``True``, no + exception will be set when attempting to delete a nonexistent + minimum packet rate rule. :returns: ``None`` """ diff --git a/openstack/proxy.py b/openstack/proxy.py index f7067f9c2..f6c1832c1 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -36,37 +36,6 @@ from openstack import connection -# The _check_resource decorator is used on Proxy methods to ensure that -# the `actual` argument is in fact the type of the `expected` argument. -# It does so under two cases: -# 1. When strict=False, if and only if `actual` is a Resource instance, -# it is checked to see that it's an instance of the `expected` class. -# This allows `actual` to be other types, such as strings, when it makes -# sense to accept a raw id value. -# 2. When strict=True, `actual` must be an instance of the `expected` class. -def _check_resource(strict=False): - def wrap(method): - def check(self, expected, actual=None, *args, **kwargs): - if ( - strict - and actual is not None - and not isinstance(actual, resource.Resource) - ): - raise ValueError(f"A {expected.__name__} must be passed") - elif isinstance(actual, resource.Resource) and not isinstance( - actual, expected - ): - raise ValueError( - f"Expected {expected.__name__} but received {actual.__class__.__name__}" - ) - - return method(self, expected, actual, *args, **kwargs) - - return check - - return wrap - - def normalize_metric_name(name): name = name.replace('.', '_') name = name.replace(':', '_') @@ -471,6 +440,11 @@ class if using an existing instance, or ``utils.Munch``, res = resource_type.new(id=value, connection=conn, **attrs) else: # An existing resource instance + if not isinstance(value, resource_type): + raise ValueError( + f'Expected {resource_type.__name__} but received ' + f'{value.__class__.__name__}' + ) res = value res._update(**attrs) @@ -529,6 +503,8 @@ def _find( ) -> ty.Optional[resource.ResourceT]: """Find a resource + :param resource_type: The type of resource to find. This should be a + :class:`~openstack.resource.Resource` subclass. :param name_or_id: The name or ID of a resource to find. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.NotFoundException` will be @@ -545,22 +521,22 @@ def _find( self, name_or_id, ignore_missing=ignore_missing, **attrs ) - @_check_resource(strict=False) def _delete( self, resource_type: type[resource.ResourceT], - value: ty.Union[str, resource.ResourceT], + value: ty.Union[str, resource.ResourceT, None], ignore_missing: bool = True, **attrs: ty.Any, ) -> ty.Optional[resource.ResourceT]: """Delete a resource - :param resource_type: The type of resource to delete. This should - be a :class:`~openstack.resource.Resource` - subclass with a ``from_id`` method. - :param value: The value to delete. Can be either the ID of a - resource or a :class:`~openstack.resource.Resource` - subclass. + :param resource_type: The type of resource to delete. This should be a + :class:`~openstack.resource.Resource` subclass. + :param value: The resource to delete. This can be the ID of a resource, + a :class:`~openstack.resource.Resource` subclass instance, or None + for resources that don't have their own identifier or have + identifiers with multiple parts. If None, you must pass these other + identifiers as kwargs. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.NotFoundException` will be raised when the resource does not exist. @@ -587,21 +563,22 @@ def _delete( return rv - @_check_resource(strict=False) def _update( self, resource_type: type[resource.ResourceT], - value: ty.Union[str, resource.ResourceT], + value: ty.Union[str, resource.ResourceT, None], base_path: ty.Optional[str] = None, **attrs: ty.Any, ) -> resource.ResourceT: """Update a resource - :param resource_type: The type of resource to update. - :type resource_type: :class:`~openstack.resource.Resource` - :param value: The resource to update. This must either be a - :class:`~openstack.resource.Resource` or an id - that corresponds to a resource. + :param resource_type: The type of resource to update. This should be a + :class:`~openstack.resource.Resource` subclass. + :param value: The resource to update. This can be the ID of a resource, + a :class:`~openstack.resource.Resource` subclass instance, or None + for resources that don't have their own identifier or have + identifiers with multiple parts. If None, you must pass these other + identifiers as kwargs. :param str base_path: Base part of the URI for updating resources, if different from :data:`~openstack.resource.Resource.base_path`. @@ -626,13 +603,10 @@ def _create( ) -> resource.ResourceT: """Create a resource from attributes - :param resource_type: The type of resource to create. - :type resource_type: :class:`~openstack.resource.Resource` - :param str base_path: Base part of the URI for creating resources, if - different from - :data:`~openstack.resource.Resource.base_path`. - :param path_args: A dict containing arguments for forming the request - URL, if needed. + :param resource_type: The type of resource to create. This should be a + :class:`~openstack.resource.Resource` subclass. + :param base_path: Base part of the URI for creating resources, if + different from :data:`~openstack.resource.Resource.base_path`. :param dict attrs: Attributes to be passed onto the :meth:`~openstack.resource.Resource.create` method to be created. These should correspond @@ -662,9 +636,9 @@ def _bulk_create( ) -> ty.Generator[resource.ResourceT, None, None]: """Create a resource from attributes - :param resource_type: The type of resource to create. - :type resource_type: :class:`~openstack.resource.Resource` - :param list data: List of attributes dicts to be passed onto the + :param resource_type: The type of resource to create. This should be a + :class:`~openstack.resource.Resource` subclass. + :param data: List of attributes dicts to be passed onto the :meth:`~openstack.resource.Resource.create` method to be created. These should correspond to either :class:`~openstack.resource.Body` @@ -679,7 +653,6 @@ def _bulk_create( """ return resource_type.bulk_create(self, data, base_path=base_path) - @_check_resource(strict=False) def _get( self, resource_type: type[resource.ResourceT], @@ -691,17 +664,20 @@ def _get( ) -> resource.ResourceT: """Fetch a resource - :param resource_type: The type of resource to get. - :type resource_type: :class:`~openstack.resource.Resource` - :param value: The value to get. Can be either the ID of a - resource or a :class:`~openstack.resource.Resource` - subclass. - :param str base_path: Base part of the URI for fetching resources, if + :param resource_type: The type of resource to get. This should be a + :class:`~openstack.resource.Resource` subclass. + :param value: The resource to get. This can be the ID of a resource, + a :class:`~openstack.resource.Resource` subclass instance, or None + for resources that don't have their own identifier or have + identifiers with multiple parts. If None, you must pass these other + identifiers as kwargs. + :param requires_id: Whether the resource is identified by an ID or not. + :param base_path: Base part of the URI for fetching resources, if different from :data:`~openstack.resource.Resource.base_path`. - :param bool skip_cache: A boolean indicating whether optional API + :param skip_cache: A boolean indicating whether optional API cache should be skipped for this invocation. - :param dict attrs: Attributes to be passed onto the + :param attrs: Attributes to be passed onto the :meth:`~openstack.resource.Resource.get` method. These should correspond to either :class:`~openstack.resource.Body` @@ -782,7 +758,7 @@ def _head( :param resource_type: The type of resource to retrieve. :type resource_type: :class:`~openstack.resource.Resource` - :param value: The value of a specific resource to retreive headers + :param value: The value of a specific resource to retrieve headers for. Can be either the ID of a resource, a :class:`~openstack.resource.Resource` subclass, or ``None``. diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index 9cc02d60b..431d70d77 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -73,48 +73,6 @@ def method(self, expected_type, value): self.fake_proxy = proxy.Proxy(self.session) self.fake_proxy._connection = self.cloud - def _test_correct(self, value): - decorated = proxy._check_resource(strict=False)(self.sot.method) - rv = decorated(self.sot, resource.Resource, value) - - self.assertEqual(value, rv) - - def test__check_resource_correct_resource(self): - res = resource.Resource() - self._test_correct(res) - - def test__check_resource_notstrict_id(self): - self._test_correct("abc123-id") - - def test__check_resource_strict_id(self): - decorated = proxy._check_resource(strict=True)(self.sot.method) - self.assertRaisesRegex( - ValueError, - "A Resource must be passed", - decorated, - self.sot, - resource.Resource, - "this-is-not-a-resource", - ) - - def test__check_resource_incorrect_resource(self): - class OneType(resource.Resource): - pass - - class AnotherType(resource.Resource): - pass - - value = AnotherType() - decorated = proxy._check_resource(strict=False)(self.sot.method) - self.assertRaisesRegex( - ValueError, - "Expected OneType but received AnotherType", - decorated, - self.sot, - OneType, - value, - ) - def test__get_uri_attribute_no_parent(self): class Child(resource.Resource): something = resource.Body("something") From ca93cf5a27bfb66c07c1532acfc0a9035d9ab862 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 26 Feb 2025 12:18:11 +0000 Subject: [PATCH 3658/3836] identity: Fix limit, registered limit creation Change I1045711f10a4a7c9053d1a042fbfb8b23399b303 started always passing 'resource_response_key' to '_translate_response', which means we never use the default value. This accidentally broke creation of the two resources, since both APIs respond with a pluralized key. Change-Id: Iebb1558ece3155d4a50463dc962aa231b08f52b4 Signed-off-by: Stephen Finucane --- openstack/identity/v3/limit.py | 23 ++++++++++++++++++++++- openstack/identity/v3/registered_limit.py | 23 ++++++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/openstack/identity/v3/limit.py b/openstack/identity/v3/limit.py index a3edd26d4..891aed2b0 100644 --- a/openstack/identity/v3/limit.py +++ b/openstack/identity/v3/limit.py @@ -48,6 +48,27 @@ class Limit(resource.Resource): #: ID of project. *Type: string* project_id = resource.Body('project_id') + def create( + self, + session, + prepend_key=True, + base_path=None, + *, + resource_request_key=None, + resource_response_key='limits', + microversion=None, + **params, + ): + return super().create( + session, + prepend_key=prepend_key, + base_path=base_path, + resource_request_key=resource_request_key, + resource_response_key=resource_response_key, + microversion=microversion, + **params, + ) + def _prepare_request_body( self, patch, @@ -71,7 +92,7 @@ def _translate_response( has_body=None, error_message=None, *, - resource_response_key='limits', + resource_response_key=None, ): """Given a KSA response, inflate this instance with its data diff --git a/openstack/identity/v3/registered_limit.py b/openstack/identity/v3/registered_limit.py index b7a42dd20..5a7de0cfe 100644 --- a/openstack/identity/v3/registered_limit.py +++ b/openstack/identity/v3/registered_limit.py @@ -46,6 +46,27 @@ class RegisteredLimit(resource.Resource): #: The default limit value. *Type: int* default_limit = resource.Body('default_limit') + def create( + self, + session, + prepend_key=True, + base_path=None, + *, + resource_request_key=None, + resource_response_key='registered_limits', + microversion=None, + **params, + ): + return super().create( + session, + prepend_key=prepend_key, + base_path=base_path, + resource_request_key=resource_request_key, + resource_response_key=resource_response_key, + microversion=microversion, + **params, + ) + def _prepare_request_body( self, patch, @@ -70,7 +91,7 @@ def _translate_response( has_body=None, error_message=None, *, - resource_response_key='registered_limits', + resource_response_key=None, ): """Given a KSA response, inflate this instance with its data From 7fb2a33f3cde4991ad89d4b4e06e43093966ce74 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 26 Feb 2025 18:55:45 +0000 Subject: [PATCH 3659/3836] exceptions: Deprecate http_status, request_id params Change-Id: I2b26ad6d96988022b5c08ae7dd6d17e4eb7ca905 Signed-off-by: Stephen Finucane --- openstack/cloud/_network.py | 24 +++----------- openstack/exceptions.py | 30 +++++++++++++---- openstack/tests/unit/test_exceptions.py | 44 ++++++++++++++++++++----- openstack/tests/unit/test_proxy.py | 15 ++++++--- 4 files changed, 72 insertions(+), 41 deletions(-) diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index 7779e1062..42ca1ffd9 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -1863,12 +1863,8 @@ def get_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): ) policy = self.network.find_qos_policy( - policy_name_or_id, ignore_missing=True + policy_name_or_id, ignore_missing=False ) - if not policy: - raise exceptions.NotFoundException( - f"QoS policy {policy_name_or_id} not Found." - ) return self.network.get_qos_minimum_bandwidth_rule(rule_id, policy) @@ -1897,12 +1893,8 @@ def create_qos_minimum_bandwidth_rule( ) policy = self.network.find_qos_policy( - policy_name_or_id, ignore_missing=True + policy_name_or_id, ignore_missing=False ) - if not policy: - raise exceptions.NotFoundException( - f"QoS policy {policy_name_or_id} not Found." - ) kwargs['min_kbps'] = min_kbps @@ -1931,12 +1923,8 @@ def update_qos_minimum_bandwidth_rule( ) policy = self.network.find_qos_policy( - policy_name_or_id, ignore_missing=True + policy_name_or_id, ignore_missing=False ) - if not policy: - raise exceptions.NotFoundException( - f"QoS policy {policy_name_or_id} not Found." - ) if not kwargs: self.log.debug("No QoS minimum bandwidth rule data to update") @@ -1971,12 +1959,8 @@ def delete_qos_minimum_bandwidth_rule(self, policy_name_or_id, rule_id): ) policy = self.network.find_qos_policy( - policy_name_or_id, ignore_missing=True + policy_name_or_id, ignore_missing=False ) - if not policy: - raise exceptions.NotFoundException( - f"QoS policy {policy_name_or_id} not Found." - ) try: self.network.delete_qos_minimum_bandwidth_rule( diff --git a/openstack/exceptions.py b/openstack/exceptions.py index a1419052e..4b068399d 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -19,10 +19,13 @@ import json import re import typing as ty +import warnings import requests from requests import exceptions as _rex +from openstack import warnings as os_warnings + if ty.TYPE_CHECKING: from openstack import resource @@ -73,13 +76,31 @@ def __init__( details: ty.Optional[str] = None, request_id: ty.Optional[str] = None, ): - # TODO(shade) Remove http_status parameter and the ability for response - # to be None once we're not mocking Session everywhere. + if http_status is not None: + warnings.warn( + "The 'http_status' parameter is unnecessary and will be " + "removed in a future release", + os_warnings.RemovedInSDK50Warning, + ) + + if request_id is not None: + warnings.warn( + "The 'request_id' parameter is unnecessary and will be " + "removed in a future release", + os_warnings.RemovedInSDK50Warning, + ) + if not message: if response is not None: message = f"{self.__class__.__name__}: {response.status_code}" else: message = f"{self.__class__.__name__}: Unknown error" + status = ( + response.status_code + if response is not None + else 'Unknown error' + ) + message = f'{self.__class__.__name__}: {status}' # Call directly rather than via super to control parameters SDKException.__init__(self, message=message) @@ -241,15 +262,10 @@ def raise_from_response( if not details: details = response.reason if response.reason else response.text - http_status = response.status_code - request_id = response.headers.get('x-openstack-request-id') - raise cls( message=error_message, response=response, details=details, - http_status=http_status, - request_id=request_id, ) diff --git a/openstack/tests/unit/test_exceptions.py b/openstack/tests/unit/test_exceptions.py index 86a2b62f6..1f11f218a 100644 --- a/openstack/tests/unit/test_exceptions.py +++ b/openstack/tests/unit/test_exceptions.py @@ -13,9 +13,11 @@ import json from unittest import mock import uuid +import warnings from openstack import exceptions from openstack.tests.unit import base +from openstack.tests.unit import fakes class Test_Exception(base.TestCase): @@ -23,7 +25,7 @@ def test_method_not_supported(self): exc = exceptions.MethodNotSupported(self.__class__, 'list') expected = ( 'The list method is not supported for ' - + 'openstack.tests.unit.test_exceptions.Test_Exception' + 'openstack.tests.unit.test_exceptions.Test_Exception' ) self.assertEqual(expected, str(exc)) @@ -31,14 +33,29 @@ def test_method_not_supported(self): class Test_HttpException(base.TestCase): def setUp(self): super().setUp() - self.message = "mayday" + self.message = 'mayday' + self.response = fakes.FakeResponse( + status_code=401, + data={ + 'error': { + 'code': 401, + 'message': ( + 'The request you have made requires authentication.' + ), + 'title': 'Unauthorized', + }, + }, + ) def _do_raise(self, *args, **kwargs): raise exceptions.HttpException(*args, **kwargs) def test_message(self): exc = self.assertRaises( - exceptions.HttpException, self._do_raise, self.message + exceptions.HttpException, + self._do_raise, + self.message, + response=self.response, ) self.assertEqual(self.message, exc.message) @@ -49,6 +66,7 @@ def test_details(self): exceptions.HttpException, self._do_raise, self.message, + response=self.response, details=details, ) @@ -57,16 +75,24 @@ def test_details(self): def test_http_status(self): http_status = 123 - exc = self.assertRaises( - exceptions.HttpException, - self._do_raise, - self.message, - http_status=http_status, - ) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + + exc = self.assertRaises( + exceptions.HttpException, + self._do_raise, + self.message, + http_status=http_status, + ) self.assertEqual(self.message, exc.message) self.assertEqual(http_status, exc.status_code) + self.assertIn( + "The 'http_status' parameter is unnecessary", + str(w[-1]), + ) + class TestRaiseFromResponse(base.TestCase): def setUp(self): diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index 9cc02d60b..e5eb9d003 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -21,6 +21,7 @@ from openstack import proxy from openstack import resource from openstack.tests.unit import base +from openstack.tests.unit import fakes from openstack import utils @@ -237,7 +238,8 @@ def test_delete(self): def test_delete_ignore_missing(self): self.res.delete.side_effect = exceptions.NotFoundException( - message="test", http_status=404 + message="test", + response=fakes.FakeResponse(status_code=404, data={'error': None}), ) rv = self.sot._delete(DeleteableResource, self.fake_id) @@ -245,7 +247,8 @@ def test_delete_ignore_missing(self): def test_delete_NotFound(self): self.res.delete.side_effect = exceptions.NotFoundException( - message="test", http_status=404 + message="test", + response=fakes.FakeResponse(status_code=404, data={'error': None}), ) self.assertRaisesRegex( @@ -259,8 +262,9 @@ def test_delete_NotFound(self): ) def test_delete_HttpException(self): - self.res.delete.side_effect = exceptions.HttpException( - message="test", http_status=500 + self.res.delete.side_effect = exceptions.ResourceNotFound( + message="test", + response=fakes.FakeResponse(status_code=500, data={'error': None}), ) self.assertRaises( @@ -468,7 +472,8 @@ def test_get_base_path(self): def test_get_not_found(self): self.res.fetch.side_effect = exceptions.NotFoundException( - message="test", http_status=404 + message="test", + response=fakes.FakeResponse(status_code=404, data={'error': None}), ) self.assertRaisesRegex( From f10faaafc96db9db1696495463631043d3288b2c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 25 Feb 2025 15:54:19 +0000 Subject: [PATCH 3660/3836] tests: Rename cloud name variables Change-Id: I0278d2617d8766d46b74871bee34810fba0b66e1 Signed-off-by: Stephen Finucane --- openstack/tests/functional/base.py | 43 +++++++++++-------- .../block_storage/v3/test_default_type.py | 2 +- .../functional/block_storage/v3/test_type.py | 2 +- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index 24f6affdf..dfa14ded2 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -49,35 +49,38 @@ class BaseFunctionalTest(base.TestCase): def setUp(self): super().setUp() + self.conn = connection.Connection(config=TEST_CLOUD_REGION) _disable_keep_alive(self.conn) - self._demo_name = os.environ.get('OPENSTACKSDK_DEMO_CLOUD', 'devstack') - if not self._demo_name: + self.config = openstack.config.OpenStackConfig() + + self._user_cloud_name = os.environ.get( + 'OPENSTACKSDK_DEMO_CLOUD', 'devstack' + ) + if not self._user_cloud_name: raise self.failureException( - "OPENSTACKSDK_OPERATOR_CLOUD must be set to a non-empty value" + "OPENSTACKSDK_DEMO_CLOUD must be set to a non-empty value" ) - self._demo_name_alt = os.environ.get( - 'OPENSTACKSDK_DEMO_CLOUD_ALT', - 'devstack-alt', + self._user_alt_cloud_name = os.environ.get( + 'OPENSTACKSDK_DEMO_CLOUD_ALT', 'devstack-alt' ) - if not self._demo_name_alt: + if not self._user_alt_cloud_name: raise self.failureException( - "OPENSTACKSDK_OPERATOR_CLOUD must be set to a non-empty value" + "OPENSTACKSDK_DEMO_CLOUD_ALT must be set to a non-empty value" ) - self._op_name = os.environ.get( - 'OPENSTACKSDK_OPERATOR_CLOUD', - 'devstack-admin', + self._set_user_cloud() + + self._operator_cloud_name = os.environ.get( + 'OPENSTACKSDK_OPERATOR_CLOUD', 'devstack-admin' ) - if not self._op_name: + if not self._operator_cloud_name: raise self.failureException( "OPENSTACKSDK_OPERATOR_CLOUD must be set to a non-empty value" ) - self.config = openstack.config.OpenStackConfig() - self._set_user_cloud() self._set_operator_cloud() self.identity_version = self.user_cloud.config.get_api_version( @@ -97,18 +100,22 @@ def setUp(self): ) def _set_user_cloud(self, **kwargs): - user_config = self.config.get_one(cloud=self._demo_name, **kwargs) + user_config = self.config.get_one( + cloud=self._user_cloud_name, **kwargs + ) self.user_cloud = connection.Connection(config=user_config) _disable_keep_alive(self.user_cloud) user_config_alt = self.config.get_one( - cloud=self._demo_name_alt, **kwargs + cloud=self._user_alt_cloud_name, **kwargs ) self.user_cloud_alt = connection.Connection(config=user_config_alt) _disable_keep_alive(self.user_cloud_alt) def _set_operator_cloud(self, **kwargs): - operator_config = self.config.get_one(cloud=self._op_name, **kwargs) + operator_config = self.config.get_one( + cloud=self._operator_cloud_name, **kwargs + ) self.operator_cloud = connection.Connection(config=operator_config) _disable_keep_alive(self.operator_cloud) @@ -122,7 +129,6 @@ def _pick_flavor(self): return None flavors = self.user_cloud.list_flavors(get_extra=False) - # self.add_info_on_exception('flavors', flavors) flavor_name = os.environ.get('OPENSTACKSDK_FLAVOR') @@ -163,7 +169,6 @@ def _pick_image(self): return None images = self.user_cloud.list_images() - # self.add_info_on_exception('images', images) image_name = os.environ.get('OPENSTACKSDK_IMAGE') diff --git a/openstack/tests/functional/block_storage/v3/test_default_type.py b/openstack/tests/functional/block_storage/v3/test_default_type.py index 82458dd84..ce8bb42c1 100644 --- a/openstack/tests/functional/block_storage/v3/test_default_type.py +++ b/openstack/tests/functional/block_storage/v3/test_default_type.py @@ -17,7 +17,7 @@ class TestDefaultType(base.BaseBlockStorageTest): def setUp(self): super().setUp() - if not self._op_name: + if not self._operator_cloud_name: self.skip("Operator cloud must be set for this test") self._set_operator_cloud(block_storage_api_version='3.67') self.PROJECT_ID = self.create_temporary_project().id diff --git a/openstack/tests/functional/block_storage/v3/test_type.py b/openstack/tests/functional/block_storage/v3/test_type.py index 1bce5096e..2f2db1fb0 100644 --- a/openstack/tests/functional/block_storage/v3/test_type.py +++ b/openstack/tests/functional/block_storage/v3/test_type.py @@ -21,7 +21,7 @@ def setUp(self): self.TYPE_NAME = self.getUniqueString() self.TYPE_ID = None - if not self._op_name: + if not self._operator_cloud_name: self.skip("Operator cloud must be set for this test") self._set_operator_cloud(block_storage_api_version='3') sot = self.operator_cloud.block_storage.create_type( From 6250b98b5b0ec64b25ec9adfe4850a0f89c09e61 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 27 Feb 2025 13:36:22 +0000 Subject: [PATCH 3661/3836] tests: Drop support for testing identity v2 This is effectively impossible to support, given identity v2 was removed in Queens, way back in March 2018 (feeling old yet?). We also rename openstack/tests/functional/cloud/test_identity.py to openstack/tests/functional/cloud/test_roles.py to better capture its purpose. Change-Id: I997fc3553846ce1739119e6db4fe30c67a421a22 Signed-off-by: Stephen Finucane --- openstack/tests/functional/base.py | 17 +---- .../tests/functional/cloud/test_domain.py | 7 +- .../tests/functional/cloud/test_endpoints.py | 75 ++++++++----------- .../tests/functional/cloud/test_groups.py | 6 +- .../tests/functional/cloud/test_project.py | 20 ++--- .../cloud/{test_identity.py => test_roles.py} | 30 +++----- .../tests/functional/cloud/test_services.py | 49 +++++------- .../tests/functional/cloud/test_users.py | 14 +--- .../v2/test_auto_allocated_topology.py | 7 +- 9 files changed, 84 insertions(+), 141 deletions(-) rename openstack/tests/functional/cloud/{test_identity.py => test_roles.py} (91%) diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index 24f6affdf..780176aaa 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -80,10 +80,6 @@ def setUp(self): self._set_user_cloud() self._set_operator_cloud() - self.identity_version = self.user_cloud.config.get_api_version( - 'identity' - ) - self.flavor = self._pick_flavor() self.image = self._pick_image() @@ -296,12 +292,7 @@ class KeystoneBaseFunctionalTest(BaseFunctionalTest): def setUp(self): super().setUp() - use_keystone_v2 = os.environ.get('OPENSTACKSDK_USE_KEYSTONE_V2', False) - if use_keystone_v2: - # keystone v2 has special behavior for the admin - # interface and some of the operations, so make a new cloud - # object with interface set to admin. - # We only do it for keystone tests on v2 because otherwise - # the admin interface is not a thing that wants to actually - # be used - self._set_operator_cloud(interface='admin') + # we only support v3, since v2 was deprecated in Queens (2018) + + if not self.conn.has_service('identity', '3'): + self.skipTest('identity service not supported by cloud') diff --git a/openstack/tests/functional/cloud/test_domain.py b/openstack/tests/functional/cloud/test_domain.py index 7cbd4b828..7c9a503bf 100644 --- a/openstack/tests/functional/cloud/test_domain.py +++ b/openstack/tests/functional/cloud/test_domain.py @@ -21,14 +21,13 @@ from openstack.tests.functional import base -class TestDomain(base.BaseFunctionalTest): +class TestDomain(base.KeystoneBaseFunctionalTest): def setUp(self): super().setUp() + if not self.operator_cloud: self.skipTest("Operator cloud is required for this test") - i_ver = self.operator_cloud.config.get_api_version('identity') - if i_ver in ('2', '2.0'): - self.skipTest('Identity service does not support domains') + self.domain_prefix = self.getUniqueString('domain') self.addCleanup(self._cleanup_domains) diff --git a/openstack/tests/functional/cloud/test_endpoints.py b/openstack/tests/functional/cloud/test_endpoints.py index 30feeb5f7..ed88057d1 100644 --- a/openstack/tests/functional/cloud/test_endpoints.py +++ b/openstack/tests/functional/cloud/test_endpoints.py @@ -22,7 +22,6 @@ import random import string -from openstack.cloud.exc import OpenStackCloudUnavailableFeature from openstack import exceptions from openstack.tests.functional import base @@ -39,6 +38,7 @@ class TestEndpoints(base.KeystoneBaseFunctionalTest): def setUp(self): super().setUp() + if not self.operator_cloud: self.skipTest("Operator cloud is required for this test") @@ -117,47 +117,38 @@ def test_create_endpoint(self): self.assertIsNotNone(endpoints[0].get('id')) def test_update_endpoint(self): - ver = self.operator_cloud.config.get_api_version('identity') - if ver.startswith('2'): - # NOTE(SamYaple): Update endpoint only works with v3 api - self.assertRaises( - OpenStackCloudUnavailableFeature, - self.operator_cloud.update_endpoint, - 'endpoint_id1', - ) - else: - # service operations require existing region. Do not test updating - # region for now - region = list(self.operator_cloud.identity.regions())[0].id - - service = self.operator_cloud.create_service( - name='service1', type='test_type' - ) - endpoint = self.operator_cloud.create_endpoint( - service_name_or_id=service['id'], - url='http://admin.url/', - interface='admin', - region=region, - enabled=False, - )[0] - - new_service = self.operator_cloud.create_service( - name='service2', type='test_type' - ) - new_endpoint = self.operator_cloud.update_endpoint( - endpoint.id, - service_name_or_id=new_service.id, - url='http://public.url/', - interface='public', - region=region, - enabled=True, - ) - - self.assertEqual(new_endpoint.url, 'http://public.url/') - self.assertEqual(new_endpoint.interface, 'public') - self.assertEqual(new_endpoint.region_id, region) - self.assertEqual(new_endpoint.service_id, new_service.id) - self.assertTrue(new_endpoint.is_enabled) + # service operations require existing region. Do not test updating + # region for now + region = list(self.operator_cloud.identity.regions())[0].id + + service = self.operator_cloud.create_service( + name='service1', type='test_type' + ) + endpoint = self.operator_cloud.create_endpoint( + service_name_or_id=service['id'], + url='http://admin.url/', + interface='admin', + region=region, + enabled=False, + )[0] + + new_service = self.operator_cloud.create_service( + name='service2', type='test_type' + ) + new_endpoint = self.operator_cloud.update_endpoint( + endpoint.id, + service_name_or_id=new_service.id, + url='http://public.url/', + interface='public', + region=region, + enabled=True, + ) + + self.assertEqual(new_endpoint.url, 'http://public.url/') + self.assertEqual(new_endpoint.interface, 'public') + self.assertEqual(new_endpoint.region_id, region) + self.assertEqual(new_endpoint.service_id, new_service.id) + self.assertTrue(new_endpoint.is_enabled) def test_list_endpoints(self): service_name = self.new_item_name + '_list' diff --git a/openstack/tests/functional/cloud/test_groups.py b/openstack/tests/functional/cloud/test_groups.py index 6a9dd0ab4..fffbdfb79 100644 --- a/openstack/tests/functional/cloud/test_groups.py +++ b/openstack/tests/functional/cloud/test_groups.py @@ -21,15 +21,13 @@ from openstack.tests.functional import base -class TestGroup(base.BaseFunctionalTest): +class TestGroup(base.KeystoneBaseFunctionalTest): def setUp(self): super().setUp() + if not self.operator_cloud: self.skipTest("Operator cloud is required for this test") - i_ver = self.operator_cloud.config.get_api_version('identity') - if i_ver in ('2', '2.0'): - self.skipTest('Identity service does not support groups') self.group_prefix = self.getUniqueString('group') self.addCleanup(self._cleanup_groups) diff --git a/openstack/tests/functional/cloud/test_project.py b/openstack/tests/functional/cloud/test_project.py index 7924c9096..9331f9ee6 100644 --- a/openstack/tests/functional/cloud/test_project.py +++ b/openstack/tests/functional/cloud/test_project.py @@ -28,6 +28,7 @@ class TestProject(base.KeystoneBaseFunctionalTest): def setUp(self): super().setUp() + if not self.operator_cloud: self.skipTest("Operator cloud is required for this test") @@ -52,11 +53,8 @@ def test_create_project(self): params = { 'name': project_name, 'description': 'test_create_project', + 'domain_id': self.operator_cloud.get_domain('default')['id'], } - if self.identity_version == '3': - params['domain_id'] = self.operator_cloud.get_domain('default')[ - 'id' - ] project = self.operator_cloud.create_project(**params) @@ -94,11 +92,8 @@ def test_update_project(self): 'name': project_name, 'description': 'test_update_project', 'enabled': True, + 'domain_id': self.operator_cloud.get_domain('default')['id'], } - if self.identity_version == '3': - params['domain_id'] = self.operator_cloud.get_domain('default')[ - 'id' - ] project = self.operator_cloud.create_project(**params) updated_project = self.operator_cloud.update_project( @@ -126,11 +121,10 @@ def test_update_project(self): def test_delete_project(self): project_name = self.new_project_name + '_delete' - params = {'name': project_name} - if self.identity_version == '3': - params['domain_id'] = self.operator_cloud.get_domain('default')[ - 'id' - ] + params = { + 'name': project_name, + 'domain_id': self.operator_cloud.get_domain('default')['id'], + } project = self.operator_cloud.create_project(**params) self.assertIsNotNone(project) self.assertTrue(self.operator_cloud.delete_project(project['id'])) diff --git a/openstack/tests/functional/cloud/test_identity.py b/openstack/tests/functional/cloud/test_roles.py similarity index 91% rename from openstack/tests/functional/cloud/test_identity.py rename to openstack/tests/functional/cloud/test_roles.py index ae8b8ce3e..37ce193ff 100644 --- a/openstack/tests/functional/cloud/test_identity.py +++ b/openstack/tests/functional/cloud/test_roles.py @@ -11,10 +11,10 @@ # under the License. """ -test_identity +test_roles ---------------------------------- -Functional tests for identity methods. +Functional tests for role methods. """ import random @@ -24,11 +24,13 @@ from openstack.tests.functional import base -class TestIdentity(base.KeystoneBaseFunctionalTest): +class TestRoles(base.KeystoneBaseFunctionalTest): def setUp(self): super().setUp() + if not self.operator_cloud: self.skipTest("Operator cloud is required for this test") + self.role_prefix = 'test_role' + ''.join( random.choice(string.ascii_lowercase) for _ in range(5) ) @@ -36,8 +38,7 @@ def setUp(self): self.group_prefix = self.getUniqueString('group') self.addCleanup(self._cleanup_users) - if self.identity_version not in ('2', '2.0'): - self.addCleanup(self._cleanup_groups) + self.addCleanup(self._cleanup_groups) self.addCleanup(self._cleanup_roles) def _cleanup_groups(self): @@ -82,11 +83,10 @@ def _cleanup_roles(self): raise exceptions.SDKException('\n'.join(exception_list)) def _create_user(self, **kwargs): - domain_id = None - if self.identity_version not in ('2', '2.0'): - domain = self.operator_cloud.get_domain('default') - domain_id = domain['id'] - return self.operator_cloud.create_user(domain_id=domain_id, **kwargs) + domain = self.operator_cloud.get_domain('default') + return self.operator_cloud.create_user( + domain_id=domain['id'], **kwargs + ) def test_list_roles(self): roles = self.operator_cloud.list_roles() @@ -124,8 +124,6 @@ def test_delete_role(self): # need to make this test a little more specific, and add more for testing # filtering functionality. def test_list_role_assignments(self): - if self.identity_version in ('2', '2.0'): - self.skipTest("Identity service does not support role assignments") assignments = self.operator_cloud.list_role_assignments() self.assertIsInstance(assignments, list) self.assertGreater(len(assignments), 0) @@ -177,8 +175,6 @@ def test_grant_revoke_role_user_project(self): self.assertEqual(0, len(assignments)) def test_grant_revoke_role_group_project(self): - if self.identity_version in ('2', '2.0'): - self.skipTest("Identity service does not support group") role_name = self.role_prefix + '_grant_group_project' role = self.operator_cloud.create_role(role_name) group_name = self.group_prefix + '_group_project' @@ -215,8 +211,6 @@ def test_grant_revoke_role_group_project(self): self.assertEqual(0, len(assignments)) def test_grant_revoke_role_user_domain(self): - if self.identity_version in ('2', '2.0'): - self.skipTest("Identity service does not support domain") role_name = self.role_prefix + '_grant_user_domain' role = self.operator_cloud.create_role(role_name) user_name = self.user_prefix + '_user_domain' @@ -254,8 +248,6 @@ def test_grant_revoke_role_user_domain(self): self.assertEqual(0, len(assignments)) def test_grant_revoke_role_group_domain(self): - if self.identity_version in ('2', '2.0'): - self.skipTest("Identity service does not support domain or group") role_name = self.role_prefix + '_grant_group_domain' role = self.operator_cloud.create_role(role_name) group_name = self.group_prefix + '_group_domain' @@ -321,8 +313,6 @@ def test_grant_revoke_role_user_system(self): self.assertEqual(0, len(assignments)) def test_grant_revoke_role_group_system(self): - if self.identity_version in ('2', '2.0'): - self.skipTest("Identity service does not support system or group") role_name = self.role_prefix + '_grant_group_system' role = self.operator_cloud.create_role(role_name) group_name = self.group_prefix + '_group_system' diff --git a/openstack/tests/functional/cloud/test_services.py b/openstack/tests/functional/cloud/test_services.py index df311d3dc..48df7cd9f 100644 --- a/openstack/tests/functional/cloud/test_services.py +++ b/openstack/tests/functional/cloud/test_services.py @@ -22,7 +22,6 @@ import random import string -from openstack.cloud import exc from openstack import exceptions from openstack.tests.functional import base @@ -68,36 +67,24 @@ def test_create_service(self): self.assertIsNotNone(service.get('id')) def test_update_service(self): - ver = self.operator_cloud.config.get_api_version('identity') - if ver.startswith('2'): - # NOTE(SamYaple): Update service only works with v3 api - self.assertRaises( - exc.OpenStackCloudUnavailableFeature, - self.operator_cloud.update_service, - 'service_id', - name='new name', - ) - else: - service = self.operator_cloud.create_service( - name=self.new_service_name + '_create', - type='test_type', - description='this is a test description', - enabled=True, - ) - new_service = self.operator_cloud.update_service( - service.id, - name=self.new_service_name + '_update', - description='this is an updated description', - enabled=False, - ) - self.assertEqual( - new_service.name, self.new_service_name + '_update' - ) - self.assertEqual( - new_service.description, 'this is an updated description' - ) - self.assertFalse(new_service.is_enabled) - self.assertEqual(service.id, new_service.id) + service = self.operator_cloud.create_service( + name=self.new_service_name + '_create', + type='test_type', + description='this is a test description', + enabled=True, + ) + new_service = self.operator_cloud.update_service( + service.id, + name=self.new_service_name + '_update', + description='this is an updated description', + enabled=False, + ) + self.assertEqual(new_service.name, self.new_service_name + '_update') + self.assertEqual( + new_service.description, 'this is an updated description' + ) + self.assertFalse(new_service.is_enabled) + self.assertEqual(service.id, new_service.id) def test_list_services(self): service = self.operator_cloud.create_service( diff --git a/openstack/tests/functional/cloud/test_users.py b/openstack/tests/functional/cloud/test_users.py index cdce80bdb..9c21bee0e 100644 --- a/openstack/tests/functional/cloud/test_users.py +++ b/openstack/tests/functional/cloud/test_users.py @@ -44,12 +44,10 @@ def _cleanup_users(self): raise exceptions.SDKException('\n'.join(exception_list)) def _create_user(self, **kwargs): - domain_id = None - i_ver = self.operator_cloud.config.get_api_version('identity') - if i_ver not in ('2', '2.0'): - domain = self.operator_cloud.get_domain('default') - domain_id = domain['id'] - return self.operator_cloud.create_user(domain_id=domain_id, **kwargs) + domain = self.operator_cloud.get_domain('default') + return self.operator_cloud.create_user( + domain_id=domain['id'], **kwargs + ) def test_list_users(self): users = self.operator_cloud.list_users() @@ -154,10 +152,6 @@ def test_update_user_password(self): self.assertIsNotNone(new_cloud.service_catalog) def test_users_and_groups(self): - i_ver = self.operator_cloud.config.get_api_version('identity') - if i_ver in ('2', '2.0'): - self.skipTest('Identity service does not support groups') - group_name = self.getUniqueString('group') self.addCleanup(self.operator_cloud.delete_group, group_name) diff --git a/openstack/tests/functional/network/v2/test_auto_allocated_topology.py b/openstack/tests/functional/network/v2/test_auto_allocated_topology.py index 3716eeca6..ff1bdd636 100644 --- a/openstack/tests/functional/network/v2/test_auto_allocated_topology.py +++ b/openstack/tests/functional/network/v2/test_auto_allocated_topology.py @@ -20,8 +20,10 @@ class TestAutoAllocatedTopology(base.BaseFunctionalTest): def setUp(self): super().setUp() + if not self.operator_cloud: self.skipTest("Operator cloud is required for this test") + if not self.operator_cloud._has_neutron_extension( "auto-allocated-topology" ): @@ -55,11 +57,8 @@ def _create_project(self): 'test project used only for the ' 'TestAutoAllocatedTopology tests class' ), + 'domain_id': self.operator_cloud.get_domain('default')['id'], } - if self.identity_version == '3': - params['domain_id'] = self.operator_cloud.get_domain( - 'default' - )['id'] project = self.operator_cloud.create_project(**params) From 5a0f3f1dd5ac0a28648f5db12d95b8e2b8a76e93 Mon Sep 17 00:00:00 2001 From: Sina Sadeghi Date: Thu, 27 Feb 2025 14:29:27 +1100 Subject: [PATCH 3662/3836] Support server unshelve to specific availability zone Closes-Bug: #2100345 Change-Id: I6d16b9251ea342ea921c9a508965eba681ca76e8 --- openstack/compute/v2/_proxy.py | 7 ++- openstack/compute/v2/server.py | 61 ++++++++----------- openstack/tests/unit/compute/v2/test_proxy.py | 8 +-- openstack/types.py | 23 +++++++ .../notes/fix-bug-9e1a976958d2543b.yaml | 5 ++ 5 files changed, 61 insertions(+), 43 deletions(-) create mode 100644 openstack/types.py create mode 100644 releasenotes/notes/fix-bug-9e1a976958d2543b.yaml diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index f48e1f684..1cf020e09 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -42,6 +42,7 @@ from openstack.network.v2 import security_group as _sg from openstack import proxy from openstack import resource +from openstack import types from openstack import utils from openstack import warnings as os_warnings @@ -1194,7 +1195,9 @@ def shelve_offload_server(self, server): server = self._get_resource(_server.Server, server) server.shelve_offload(self) - def unshelve_server(self, server, *, host=None): + def unshelve_server( + self, server, *, host=None, availability_zone=types.UNSET + ): """Unshelves or restores a shelved server. Policy defaults enable only users with administrative role or the @@ -1208,7 +1211,7 @@ def unshelve_server(self, server, *, host=None): :returns: None """ server = self._get_resource(_server.Server, server) - server.unshelve(self, host=host) + server.unshelve(self, host=host, availability_zone=availability_zone) def trigger_server_crash_dump(self, server): """Trigger a crash dump in a server. diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 5ecf09898..f8aeebac6 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -19,18 +19,10 @@ from openstack import exceptions from openstack.image.v2 import image from openstack import resource +from openstack import types from openstack import utils -# Workaround Python's lack of an undefined sentinel -# https://python-patterns.guide/python/sentinel-object/ -class Unset: - def __bool__(self) -> ty.Literal[False]: - return False - - -UNSET: Unset = Unset() - CONSOLE_TYPE_ACTION_MAPPING = { 'novnc': 'os-getVNCConsole', 'xvpvnc': 'os-getVNCConsole', @@ -52,11 +44,6 @@ class Server(resource.Resource, metadata.MetadataMixin, tag.TagMixin): allow_delete = True allow_list = True - # Sentinel used to differentiate API called without parameter or None - # Ex unshelve API can be called without an availability_zone or with - # availability_zone = None to unpin the az. - _sentinel = object() - _query_mapping = resource.QueryParameters( "auto_disk_config", "availability_zone", @@ -409,17 +396,17 @@ def rebuild( self, session, image, - name=UNSET, - admin_password=UNSET, - preserve_ephemeral=UNSET, - access_ipv4=UNSET, - access_ipv6=UNSET, - metadata=UNSET, - user_data=UNSET, - key_name=UNSET, - description=UNSET, - trusted_image_certificates=UNSET, - hostname=UNSET, + name=types.UNSET, + admin_password=types.UNSET, + preserve_ephemeral=types.UNSET, + access_ipv4=types.UNSET, + access_ipv6=types.UNSET, + metadata=types.UNSET, + user_data=types.UNSET, + key_name=types.UNSET, + description=types.UNSET, + trusted_image_certificates=types.UNSET, + hostname=types.UNSET, ): """Rebuild the server with the given arguments. @@ -448,27 +435,27 @@ def rebuild( :returns: The updated server. """ action = {'imageRef': resource.Resource._get_id(image)} - if preserve_ephemeral is not UNSET: + if preserve_ephemeral is not types.UNSET: action['preserve_ephemeral'] = preserve_ephemeral - if name is not UNSET: + if name is not types.UNSET: action['name'] = name - if admin_password is not UNSET: + if admin_password is not types.UNSET: action['adminPass'] = admin_password - if access_ipv4 is not UNSET: + if access_ipv4 is not types.UNSET: action['accessIPv4'] = access_ipv4 - if access_ipv6 is not UNSET: + if access_ipv6 is not types.UNSET: action['accessIPv6'] = access_ipv6 - if metadata is not UNSET: + if metadata is not types.UNSET: action['metadata'] = metadata - if user_data is not UNSET: + if user_data is not types.UNSET: action['user_data'] = user_data - if key_name is not UNSET: + if key_name is not types.UNSET: action['key_name'] = key_name - if description is not UNSET: + if description is not types.UNSET: action['description'] = description - if trusted_image_certificates is not UNSET: + if trusted_image_certificates is not types.UNSET: action['trusted_image_certificates'] = trusted_image_certificates - if hostname is not UNSET: + if hostname is not types.UNSET: action['hostname'] = hostname body = {'rebuild': action} @@ -820,7 +807,7 @@ def shelve_offload(self, session): body = {"shelveOffload": None} self._action(session, body) - def unshelve(self, session, availability_zone=_sentinel, host=None): + def unshelve(self, session, availability_zone=types.UNSET, host=None): """Unshelve the server. :param session: The session to use for making this request. diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 0e9cd9843..9c2f26238 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -43,6 +43,7 @@ from openstack import proxy as proxy_base from openstack.tests.unit import base from openstack.tests.unit import test_proxy_base +from openstack import types from openstack import warnings as os_warnings @@ -1371,6 +1372,7 @@ def test_server_unshelve(self): expected_args=[self.proxy], expected_kwargs={ "host": None, + "availability_zone": types.UNSET, }, ) @@ -1379,11 +1381,9 @@ def test_server_unshelve_with_options(self): "openstack.compute.v2.server.Server.unshelve", self.proxy.unshelve_server, method_args=["value"], - method_kwargs={"host": "HOST2"}, + method_kwargs={"host": "HOST2", "availability_zone": "AZ2"}, expected_args=[self.proxy], - expected_kwargs={ - "host": "HOST2", - }, + expected_kwargs={"host": "HOST2", "availability_zone": "AZ2"}, ) def test_server_trigger_dump(self): diff --git a/openstack/types.py b/openstack/types.py new file mode 100644 index 000000000..d84685e88 --- /dev/null +++ b/openstack/types.py @@ -0,0 +1,23 @@ +# 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 typing as ty + + +# Workaround Python's lack of an undefined sentinel +# https://python-patterns.guide/python/sentinel-object/ +class Unset: + def __bool__(self) -> ty.Literal[False]: + return False + + +UNSET: Unset = Unset() diff --git a/releasenotes/notes/fix-bug-9e1a976958d2543b.yaml b/releasenotes/notes/fix-bug-9e1a976958d2543b.yaml new file mode 100644 index 000000000..913d33a10 --- /dev/null +++ b/releasenotes/notes/fix-bug-9e1a976958d2543b.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixed the issue that unshelving a server to a specific availability zone + was failed due to unhandled ``availability_zone`` option. From 55904234c0d6298d71c63456ae911b94ac879292 Mon Sep 17 00:00:00 2001 From: Douglas Viroel Date: Thu, 9 Jan 2025 15:54:20 -0300 Subject: [PATCH 3663/3836] Bump compute max microversion to 2.100 As proposed in [1], and implemented in [2],a new compute microserviosn was added to support returning the associated scheduler_hints in ``GET /servers/{server_id}``, `GET /servers/detail``,``PUT /servers/{server_id}`` and ``POST /server/{server_id}/action`` (rebuild) responses. ``GET /servers/{server_id}`` and ``GET /servers/detail`. Note that ``scheduler_hints`` is already part of Server resource. [1] https://review.opendev.org/c/openstack/nova-specs/+/936140 [2] https://review.opendev.org/c/openstack/nova/+/938604 Change-Id: Iaa5476ba720a4d5f31e1adfae5755c852a0547c0 --- openstack/compute/v2/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 5ecf09898..b998ace79 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -102,7 +102,7 @@ class Server(resource.Resource, metadata.MetadataMixin, tag.TagMixin): **tag.TagMixin._tag_query_parameters, ) - _max_microversion = '2.96' + _max_microversion = '2.100' #: A list of dictionaries holding links relevant to this server. links = resource.Body('links') From 2c96712ca975f173dde9f29bbd8cff8ce8dd4d59 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 4 Mar 2025 10:21:23 +0000 Subject: [PATCH 3664/3836] Do not pin openstacksdk to master The theory behind this seems to have been to stop people using stable branches of openstacksdk. Well that hasn't worked, based on the fact that virtually all of our stable branches have unset this [1]. Stop pretending we can support this and remove the branch override. [1] https://review.opendev.org/q/topic:gate-fix-unpin-osdk+NOT+branch:master Change-Id: Iad10f68e7caff999981f82c6d098199014d0adee Signed-off-by: Stephen Finucane --- zuul.d/functional-jobs.yaml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/zuul.d/functional-jobs.yaml b/zuul.d/functional-jobs.yaml index ca397634f..502d824e6 100644 --- a/zuul.d/functional-jobs.yaml +++ b/zuul.d/functional-jobs.yaml @@ -10,14 +10,8 @@ # NOTE: We pull in roles from the tempest repo for stackviz processing. - zuul: opendev.org/openstack/tempest required-projects: - # These jobs will DTRT when openstacksdk triggers them, but we want to - # make sure stable branches of openstacksdk never get cloned by other - # people, since stable branches of openstacksdk are, well, not actually - # things. - name: openstack/openstacksdk - override-branch: master - name: openstack/os-client-config - override-branch: master timeout: 9000 vars: devstack_localrc: From 88e1c8a92f6a07c6b91bc72bb0b9f7021b40ae7e Mon Sep 17 00:00:00 2001 From: Omer Date: Tue, 4 Mar 2025 13:58:52 +0100 Subject: [PATCH 3665/3836] Fix DNS secondary zone creation So far, openstacksdk required providing email and ttl when one creates secondary dns zone. However, Designate does not allow providing email nor ttl on secondary zones creation: https://opendev.org/openstack/designate/src/branch/master/designate/ \ objects/zone.py#L132 This patch pops those 2 parameters on a secondary zone creation request. Note: this behavior is similar to the Designate one: https://opendev.org/openstack/designate/src/branch/master/designate/ \ api/v2/controllers/zones/__init__.py#L99 where Designate overrides the incorrect fields. Closes-Bug: #2100856 Change-Id: I0bf698e27ceb18c9764bba87b2421eac798ce788 --- openstack/dns/v2/_proxy.py | 3 +++ .../fix-dns-secondary-zones-creation-78ed84fa7d514998.yaml | 6 ++++++ 2 files changed, 9 insertions(+) create mode 100644 releasenotes/notes/fix-dns-secondary-zones-creation-78ed84fa7d514998.yaml diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py index 8e77b857a..b4978699e 100644 --- a/openstack/dns/v2/_proxy.py +++ b/openstack/dns/v2/_proxy.py @@ -70,6 +70,9 @@ def create_zone(self, **attrs): :returns: The results of zone creation. :rtype: :class:`~openstack.dns.v2.zone.Zone` """ + if attrs.get('type') == "SECONDARY": + attrs.pop('email', None) + attrs.pop('ttl', None) return self._create(_zone.Zone, prepend_key=False, **attrs) def get_zone(self, zone): diff --git a/releasenotes/notes/fix-dns-secondary-zones-creation-78ed84fa7d514998.yaml b/releasenotes/notes/fix-dns-secondary-zones-creation-78ed84fa7d514998.yaml new file mode 100644 index 000000000..90b9cd857 --- /dev/null +++ b/releasenotes/notes/fix-dns-secondary-zones-creation-78ed84fa7d514998.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixed DNS secondary zone creation. Now secondary zones do not + require email not ttl, as both get overriden by the following + zone transfer. From abdcb8901c4b4ac79c1c5848040082c483718692 Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Tue, 4 Mar 2025 22:09:53 +0900 Subject: [PATCH 3666/3836] Bump hacking in unit tests ... to the version actually used in pep8 check. Change-Id: Ie2b8cced263f435884f40bc0de074d8477792b08 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index b771e0ca3..ad4d20706 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,7 @@ coverage!=4.4,>=4.0 # Apache-2.0 ddt>=1.0.1 # MIT fixtures>=3.0.0 # Apache-2.0/BSD -hacking>=3.1.0,<4.0.0 # Apache-2.0 +hacking>=7.0.0,<7.1.0 # Apache-2.0 jsonschema>=3.2.0 # MIT oslo.config>=6.1.0 # Apache-2.0 oslotest>=3.2.0 # Apache-2.0 From d322b49fea0e1d7b30e01dd8803aa745ae79080f Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 6 Mar 2025 11:42:39 +0000 Subject: [PATCH 3667/3836] resource: Make _assert_microversion_for a classmethod We also remove an unnecessary argument and rename some variables to better explain their purpose. Change-Id: I79dadd1895ae1f50446e4df537257e4409ab2b85 Signed-off-by: Stephen Finucane --- openstack/baremetal/v1/node.py | 59 ++++++++----------- openstack/resource.py | 32 +++++----- .../tests/unit/baremetal/v1/test_node.py | 2 +- openstack/tests/unit/test_resource.py | 13 ++-- 4 files changed, 50 insertions(+), 56 deletions(-) diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index b60cf6b49..3643822b6 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -334,7 +334,7 @@ def create(self, session, *args, **kwargs): ) else: error_message = ( - "Cannot create a node with initial provision " + f"Cannot create a node with initial provision " f"state {expected_provision_state}" ) # Nodes cannot be created as available using new API versions @@ -343,7 +343,6 @@ def create(self, session, *args, **kwargs): ) microversion = self._assert_microversion_for( session, - 'create', microversion, maximum=maximum, error_message=error_message, @@ -452,26 +451,26 @@ def set_provision_state( """ session = self._get_session(session) - version = None + microversion = None if target in _common.PROVISIONING_VERSIONS: - version = f'1.{_common.PROVISIONING_VERSIONS[target]}' + microversion = f'1.{_common.PROVISIONING_VERSIONS[target]}' if config_drive: # Some config drive actions require a higher version. if isinstance(config_drive, dict): - version = _common.CONFIG_DRIVE_DICT_VERSION + microversion = _common.CONFIG_DRIVE_DICT_VERSION elif target == 'rebuild': - version = _common.CONFIG_DRIVE_REBUILD_VERSION + microversion = _common.CONFIG_DRIVE_REBUILD_VERSION if deploy_steps: - version = _common.DEPLOY_STEPS_VERSION + microversion = _common.DEPLOY_STEPS_VERSION - version = self._assert_microversion_for(session, 'commit', version) + microversion = self._assert_microversion_for(session, microversion) body = {'target': target} if runbook: - version = self._assert_microversion_for( - session, 'commit', _common.RUNBOOKS_VERSION + microversion = self._assert_microversion_for( + session, _common.RUNBOOKS_VERSION ) if clean_steps is not None: @@ -546,7 +545,7 @@ def set_provision_state( except KeyError: raise ValueError( f'For target {target} the expected state is not ' - 'known, cannot wait for it' + f'known, cannot wait for it' ) request = self._prepare_request(requires_id=True) @@ -555,7 +554,7 @@ def set_provision_state( request.url, json=body, headers=request.headers, - microversion=version, + microversion=microversion, retriable_status_codes=_common.RETRIABLE_STATUS_CODES, ) @@ -740,9 +739,7 @@ def inject_nmi(self, session): """ session = self._get_session(session) version = self._assert_microversion_for( - session, - 'commit', - _common.INJECT_NMI_VERSION, + session, _common.INJECT_NMI_VERSION ) request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'management', 'inject_nmi') @@ -781,17 +778,17 @@ def set_power_state(self, session, target, wait=False, timeout=None): except KeyError: raise ValueError( f"Cannot use target power state {target} with wait, " - "the expected state is not known" + f"the expected state is not known" ) session = self._get_session(session) if target.startswith("soft "): - version = '1.27' + microversion = '1.27' else: - version = None + microversion = None - version = self._assert_microversion_for(session, 'commit', version) + microversion = self._assert_microversion_for(session, microversion) # TODO(dtantsur): server timeout support body = {'target': target} @@ -802,7 +799,7 @@ def set_power_state(self, session, target, wait=False, timeout=None): request.url, json=body, headers=request.headers, - microversion=version, + microversion=microversion, retriable_status_codes=_common.RETRIABLE_STATUS_CODES, ) @@ -856,13 +853,12 @@ def attach_vif( session = self._get_session(session) if port_id or port_group_id: - required_version = _common.VIF_OPTIONAL_PARAMS_VERSION + microversion = _common.VIF_OPTIONAL_PARAMS_VERSION else: - required_version = _common.VIF_VERSION - version = self._assert_microversion_for( + microversion = _common.VIF_VERSION + microversion = self._assert_microversion_for( session, - 'commit', - required_version, + microversion, error_message=("Cannot use VIF attachment API"), ) @@ -880,7 +876,7 @@ def attach_vif( request.url, json=body, headers=request.headers, - microversion=version, + microversion=microversion, retriable_status_codes=retriable_status_codes, ) @@ -908,7 +904,6 @@ def detach_vif(self, session, vif_id, ignore_missing=True): session = self._get_session(session) version = self._assert_microversion_for( session, - 'commit', _common.VIF_VERSION, error_message=("Cannot use VIF attachment API"), ) @@ -949,7 +944,6 @@ def list_vifs(self, session): session = self._get_session(session) version = self._assert_microversion_for( session, - 'fetch', _common.VIF_VERSION, error_message=("Cannot use VIF attachment API"), ) @@ -1144,7 +1138,7 @@ def set_boot_mode(self, session, target): if target not in ('uefi', 'bios'): raise ValueError( f"Unrecognized boot mode {target}." - "Boot mode should be one of 'uefi' or 'bios'." + f"Boot mode should be one of 'uefi' or 'bios'." ) body = {'target': target} @@ -1180,7 +1174,7 @@ def set_secure_boot(self, session, target): if not isinstance(target, bool): raise ValueError( f"Invalid target {target}. It should be True or False " - "corresponding to secure boot state 'on' or 'off'" + f"corresponding to secure boot state 'on' or 'off'" ) body = {'target': target} @@ -1384,7 +1378,7 @@ def set_console_mode(self, session, enabled): if not isinstance(enabled, bool): raise ValueError( f"Invalid enabled {enabled}. It should be True or False " - "corresponding to console enabled or disabled" + f"corresponding to console enabled or disabled" ) body = {'enabled': enabled} @@ -1437,7 +1431,6 @@ def list_firmware(self, session): session = self._get_session(session) version = self._assert_microversion_for( session, - 'fetch', _common.FIRMWARE_VERSION, error_message=("Cannot use node list firmware components API"), ) @@ -1482,7 +1475,7 @@ def patch( session = self._get_session(session) microversion = self._assert_microversion_for( - session, 'commit', _common.RESET_INTERFACES_VERSION + session, _common.RESET_INTERFACES_VERSION ) params = [('reset_interfaces', reset_interfaces)] diff --git a/openstack/resource.py b/openstack/resource.py index de204f1e7..78e2134cf 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1238,8 +1238,9 @@ def _get_session(cls, session): "a raw keystoneauth1.adapter.Adapter." ) + # TODO(stephenfin): Drop action argument. It has never been used. @classmethod - def _get_microversion(cls, session, *, action): + def _get_microversion(cls, session, *, action=None): """Get microversion to use for the given action. The base version uses the following logic: @@ -1265,6 +1266,7 @@ def _get_microversion(cls, session, *, action): 'create', 'delete', 'patch', + None, }: raise ValueError(f'Invalid action: {action}') @@ -1275,18 +1277,18 @@ def _get_microversion(cls, session, *, action): session, cls._max_microversion ) + @classmethod def _assert_microversion_for( - self, - session, - action, - expected, - error_message=None, - maximum=None, - ): + cls, + session: adapter.Adapter, + expected: ty.Optional[str], + *, + error_message: ty.Optional[str] = None, + maximum: ty.Optional[str] = None, + ) -> str: """Enforce that the microversion for action satisfies the requirement. :param session: :class`keystoneauth1.adapter.Adapter` - :param action: One of "fetch", "commit", "create", "delete". :param expected: Expected microversion. :param error_message: Optional error message with details. Will be prepended to the message generated here. @@ -1296,21 +1298,22 @@ def _assert_microversion_for( used for the action is lower than the expected one. """ - def _raise(message): + def _raise(message: str) -> ty.NoReturn: if error_message: error_message.rstrip('.') message = f'{error_message}. {message}' raise exceptions.NotSupported(message) - actual = self._get_microversion(session, action=action) + actual = cls._get_microversion(session) if actual is None: message = ( f"API version {expected} is required, but the default " - "version will be used." + f"version will be used." ) _raise(message) + actual_n = discover.normalize_version_number(actual) if expected is not None: @@ -1318,14 +1321,15 @@ def _raise(message): if actual_n < expected_n: message = ( f"API version {expected} is required, but {actual} " - "will be used." + f"will be used." ) _raise(message) + if maximum is not None: maximum_n = discover.normalize_version_number(maximum) # Assume that if a service supports higher versions, it also # supports lower ones. Breaks for services that remove old API - # versions (which is not something they should do). + # versions (which is not something that has been done yet). if actual_n > maximum_n: return maximum diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 96d47f8a1..008559fe8 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -102,7 +102,7 @@ } -def _fake_assert(self, session, action, expected, error_message=None): +def _fake_assert(self, session, expected, error_message=None): return expected diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 21fb4f46a..bd3b36ae7 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -3631,9 +3631,9 @@ def test_compatible(self, mock_get_ver): self.assertEqual( '1.42', - self.res._assert_microversion_for(self.session, 'fetch', '1.6'), + self.res._assert_microversion_for(self.session, '1.6'), ) - mock_get_ver.assert_called_once_with(self.session, action='fetch') + mock_get_ver.assert_called_once_with(self.session) def test_incompatible(self, mock_get_ver): mock_get_ver.return_value = '1.1' @@ -3643,10 +3643,9 @@ def test_incompatible(self, mock_get_ver): '1.6 is required, but 1.1 will be used', self.res._assert_microversion_for, self.session, - 'fetch', '1.6', ) - mock_get_ver.assert_called_once_with(self.session, action='fetch') + mock_get_ver.assert_called_once_with(self.session) def test_custom_message(self, mock_get_ver): mock_get_ver.return_value = '1.1' @@ -3656,11 +3655,10 @@ def test_custom_message(self, mock_get_ver): 'boom.*1.6 is required, but 1.1 will be used', self.res._assert_microversion_for, self.session, - 'fetch', '1.6', error_message='boom', ) - mock_get_ver.assert_called_once_with(self.session, action='fetch') + mock_get_ver.assert_called_once_with(self.session) def test_none(self, mock_get_ver): mock_get_ver.return_value = None @@ -3670,7 +3668,6 @@ def test_none(self, mock_get_ver): '1.6 is required, but the default version', self.res._assert_microversion_for, self.session, - 'fetch', '1.6', ) - mock_get_ver.assert_called_once_with(self.session, action='fetch') + mock_get_ver.assert_called_once_with(self.session) From 30101a049d35a934d40d9d871d9b0f86214f6407 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 6 Mar 2025 13:22:45 +0000 Subject: [PATCH 3668/3836] resource: Drop 'action' arg for '_get_microversion' This is never used either. Change-Id: I466046249c11a515905a8d981336da26f233e485 Signed-off-by: Stephen Finucane --- .../accelerator/v2/accelerator_request.py | 2 +- openstack/baremetal/v1/node.py | 20 +++++------ .../v1/introspection.py | 8 ++--- openstack/block_storage/v2/backup.py | 2 +- openstack/block_storage/v3/attachment.py | 2 +- openstack/block_storage/v3/backup.py | 2 +- openstack/block_storage/v3/group.py | 4 +-- openstack/block_storage/v3/group_type.py | 10 +++--- openstack/block_storage/v3/transfer.py | 2 +- openstack/block_storage/v3/volume.py | 4 +-- openstack/compute/v2/flavor.py | 10 +++--- openstack/compute/v2/hypervisor.py | 2 +- openstack/compute/v2/server.py | 6 ++-- openstack/compute/v2/server_group.py | 2 +- openstack/compute/v2/server_migration.py | 2 +- openstack/dns/v2/zone_export.py | 2 +- openstack/dns/v2/zone_import.py | 2 +- openstack/image/v2/cache.py | 2 +- openstack/image/v2/metadef_property.py | 2 +- openstack/load_balancer/v2/amphora.py | 4 +-- openstack/load_balancer/v2/load_balancer.py | 2 +- openstack/object_store/v1/info.py | 2 +- openstack/object_store/v1/obj.py | 2 +- openstack/orchestration/v1/stack.py | 4 +-- openstack/placement/v1/resource_provider.py | 4 +-- .../v1/resource_provider_inventory.py | 2 +- openstack/placement/v1/trait.py | 2 +- openstack/resource.py | 36 +++++++------------ openstack/shared_file_system/v2/share.py | 4 +-- .../v2/share_access_rule.py | 4 +-- .../v2/share_group_snapshot.py | 10 ++---- .../shared_file_system/v2/share_instance.py | 8 ++--- .../unit/shared_file_system/v2/test_share.py | 8 ++--- .../v2/test_share_instance.py | 4 +-- 34 files changed, 80 insertions(+), 102 deletions(-) diff --git a/openstack/accelerator/v2/accelerator_request.py b/openstack/accelerator/v2/accelerator_request.py index cf1c6f372..35b0940fc 100644 --- a/openstack/accelerator/v2/accelerator_request.py +++ b/openstack/accelerator/v2/accelerator_request.py @@ -84,7 +84,7 @@ def patch( request = self._prepare_request( prepend_key=prepend_key, base_path=base_path, patch=True ) - microversion = self._get_microversion(session, action='patch') + microversion = self._get_microversion(session) if patch: request.body = self._convert_patch(patch) diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 3643822b6..8debcf8b5 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -973,7 +973,7 @@ def validate(self, session, required=('boot', 'deploy', 'power')): fails for a required interface. """ session = self._get_session(session) - version = self._get_microversion(session, action='fetch') + version = self._get_microversion(session) request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'validate') @@ -1028,7 +1028,7 @@ def unset_maintenance(self, session): def _do_maintenance_action(self, session, verb, body=None): session = self._get_session(session) - version = self._get_microversion(session, action='commit') + version = self._get_microversion(session) request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'maintenance') response = getattr(session, verb)( @@ -1047,7 +1047,7 @@ def get_boot_device(self, session): :returns: The HTTP response. """ session = self._get_session(session) - version = self._get_microversion(session, action='fetch') + version = self._get_microversion(session) request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'management', 'boot_device') @@ -1073,7 +1073,7 @@ def set_boot_device(self, session, boot_device, persistent=False): :returns: ``None`` """ session = self._get_session(session) - version = self._get_microversion(session, action='commit') + version = self._get_microversion(session) request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'management', 'boot_device') @@ -1097,7 +1097,7 @@ def get_supported_boot_devices(self, session): :returns: The HTTP response. """ session = self._get_session(session) - version = self._get_microversion(session, action='fetch') + version = self._get_microversion(session) request = self._prepare_request(requires_id=True) request.url = utils.urljoin( request.url, @@ -1295,7 +1295,7 @@ def call_vendor_passthru(self, session, verb, method, body=None): :returns: The HTTP response. """ session = self._get_session(session) - version = self._get_microversion(session, action='commit') + version = self._get_microversion(session) request = self._prepare_request(requires_id=True) request.url = utils.urljoin( request.url, f'vendor_passthru?method={method}' @@ -1325,7 +1325,7 @@ def list_vendor_passthru(self, session): :returns: The HTTP response. """ session = self._get_session(session) - version = self._get_microversion(session, action='fetch') + version = self._get_microversion(session) request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'vendor_passthru/methods') @@ -1348,7 +1348,7 @@ def get_console(self, session): :returns: The HTTP response. """ session = self._get_session(session) - version = self._get_microversion(session, action='fetch') + version = self._get_microversion(session) request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'states', 'console') @@ -1372,7 +1372,7 @@ def set_console_mode(self, session, enabled): :return: ``None`` """ session = self._get_session(session) - version = self._get_microversion(session, action='commit') + version = self._get_microversion(session) request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'states', 'console') if not isinstance(enabled, bool): @@ -1407,7 +1407,7 @@ def get_node_inventory(self, session, node_id): os_warnings.RemovedInSDK60Warning, ) session = self._get_session(session) - version = self._get_microversion(session, action='fetch') + version = self._get_microversion(session) request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'inventory') diff --git a/openstack/baremetal_introspection/v1/introspection.py b/openstack/baremetal_introspection/v1/introspection.py index d7f3b878d..d268a5c98 100644 --- a/openstack/baremetal_introspection/v1/introspection.py +++ b/openstack/baremetal_introspection/v1/introspection.py @@ -62,7 +62,7 @@ def abort(self, session): session = self._get_session(session) - version = self._get_microversion(session, action='delete') + version = self._get_microversion(session) request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'abort') response = session.post( @@ -90,11 +90,7 @@ def get_data(self, session, processed=True): """ session = self._get_session(session) - version = ( - self._get_microversion(session, action='fetch') - if processed - else '1.17' - ) + version = self._get_microversion(session) if processed else '1.17' request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'data') if not processed: diff --git a/openstack/block_storage/v2/backup.py b/openstack/block_storage/v2/backup.py index cbb11fed3..e94c6ed8a 100644 --- a/openstack/block_storage/v2/backup.py +++ b/openstack/block_storage/v2/backup.py @@ -108,7 +108,7 @@ def create(self, session, prepend_key=True, base_path=None, **params): raise exceptions.MethodNotSupported(self, "create") session = self._get_session(session) - microversion = self._get_microversion(session, action='create') + microversion = self._get_microversion(session) requires_id = ( self.create_requires_id if self.create_requires_id is not None diff --git a/openstack/block_storage/v3/attachment.py b/openstack/block_storage/v3/attachment.py index ba6e845a0..4a459b397 100644 --- a/openstack/block_storage/v3/attachment.py +++ b/openstack/block_storage/v3/attachment.py @@ -81,7 +81,7 @@ def complete(self, session, *, microversion=None): """Mark the attachment as completed.""" body = {'os-complete': self.id} if not microversion: - microversion = self._get_microversion(session, action='commit') + microversion = self._get_microversion(session) url = os.path.join(Attachment.base_path, self.id, 'action') response = session.post(url, json=body, microversion=microversion) exceptions.raise_from_response(response) diff --git a/openstack/block_storage/v3/backup.py b/openstack/block_storage/v3/backup.py index 1fd7204e0..6029f7763 100644 --- a/openstack/block_storage/v3/backup.py +++ b/openstack/block_storage/v3/backup.py @@ -124,7 +124,7 @@ def create(self, session, prepend_key=True, base_path=None, **params): raise exceptions.MethodNotSupported(self, "create") session = self._get_session(session) - microversion = self._get_microversion(session, action='create') + microversion = self._get_microversion(session) requires_id = ( self.create_requires_id if self.create_requires_id is not None diff --git a/openstack/block_storage/v3/group.py b/openstack/block_storage/v3/group.py index 61f7bde79..7c67709ff 100644 --- a/openstack/block_storage/v3/group.py +++ b/openstack/block_storage/v3/group.py @@ -54,7 +54,7 @@ class Group(resource.Resource): def _action(self, session, body): """Preform group actions given the message body.""" session = self._get_session(session) - microversion = self._get_microversion(session, action='create') + microversion = self._get_microversion(session) url = utils.urljoin(self.base_path, self.id, 'action') response = session.post(url, json=body, microversion=microversion) exceptions.raise_from_response(response) @@ -81,7 +81,7 @@ def create_from_source( ): """Creates a new group from source.""" session = cls._get_session(session) - microversion = cls._get_microversion(session, action='create') + microversion = cls._get_microversion(session) url = utils.urljoin(cls.base_path, 'action') body = { 'create-from-src': { diff --git a/openstack/block_storage/v3/group_type.py b/openstack/block_storage/v3/group_type.py index 8ff929e38..eb09cf02a 100644 --- a/openstack/block_storage/v3/group_type.py +++ b/openstack/block_storage/v3/group_type.py @@ -51,7 +51,7 @@ def fetch_group_specs(self, session): :returns: An updated version of this object. """ url = utils.urljoin(GroupType.base_path, self.id, 'group_specs') - microversion = self._get_microversion(session, action='fetch') + microversion = self._get_microversion(session) response = session.get(url, microversion=microversion) exceptions.raise_from_response(response) specs = response.json().get('group_specs', {}) @@ -69,7 +69,7 @@ def create_group_specs(self, session, specs): :returns: An updated version of this object. """ url = utils.urljoin(GroupType.base_path, self.id, 'group_specs') - microversion = self._get_microversion(session, action='create') + microversion = self._get_microversion(session) response = session.post( url, json={'group_specs': specs}, @@ -88,7 +88,7 @@ def get_group_specs_property(self, session, prop): :returns: The value of the group spec property. """ url = utils.urljoin(GroupType.base_path, self.id, 'group_specs', prop) - microversion = self._get_microversion(session, action='fetch') + microversion = self._get_microversion(session) response = session.get(url, microversion=microversion) exceptions.raise_from_response(response) val = response.json().get(prop) @@ -103,7 +103,7 @@ def update_group_specs_property(self, session, prop, val): :returns: The updated value of the group spec property. """ url = utils.urljoin(GroupType.base_path, self.id, 'group_specs', prop) - microversion = self._get_microversion(session, action='commit') + microversion = self._get_microversion(session) response = session.put( url, json={prop: val}, microversion=microversion ) @@ -119,6 +119,6 @@ def delete_group_specs_property(self, session, prop): :returns: None """ url = utils.urljoin(GroupType.base_path, self.id, 'group_specs', prop) - microversion = self._get_microversion(session, action='delete') + microversion = self._get_microversion(session) response = session.delete(url, microversion=microversion) exceptions.raise_from_response(response) diff --git a/openstack/block_storage/v3/transfer.py b/openstack/block_storage/v3/transfer.py index 512802684..9c2643181 100644 --- a/openstack/block_storage/v3/transfer.py +++ b/openstack/block_storage/v3/transfer.py @@ -190,7 +190,7 @@ def accept(self, session, *, auth_key=None): path = '/os-volume-transfer' url = utils.urljoin(path, self.id, 'accept') - microversion = self._get_microversion(session, action='commit') + microversion = self._get_microversion(session) resp = session.post( url, json=body, diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index bdf840a65..4f756255a 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -113,14 +113,14 @@ class Volume(resource.Resource, metadata.MetadataMixin): _max_microversion = "3.60" - def _action(self, session, body, microversion=None, action='patch'): + def _action(self, session, body, microversion=None): """Preform volume actions given the message body.""" # NOTE: This is using Volume.base_path instead of self.base_path # as both Volume and VolumeDetail instances can be acted on, but # the URL used is sans any additional /detail/ part. url = utils.urljoin(Volume.base_path, self.id, 'action') if microversion is None: - microversion = self._get_microversion(session, action=action) + microversion = self._get_microversion(session) resp = session.post(url, json=body, microversion=microversion) exceptions.raise_from_response(resp) return resp diff --git a/openstack/compute/v2/flavor.py b/openstack/compute/v2/flavor.py index e6d1390b3..40d7a2588 100644 --- a/openstack/compute/v2/flavor.py +++ b/openstack/compute/v2/flavor.py @@ -164,7 +164,7 @@ def fetch_extra_specs(self, session): :returns: The updated flavor. """ url = utils.urljoin(Flavor.base_path, self.id, 'os-extra_specs') - microversion = self._get_microversion(session, action='fetch') + microversion = self._get_microversion(session) response = session.get(url, microversion=microversion) exceptions.raise_from_response(response) specs = response.json().get('extra_specs', {}) @@ -179,7 +179,7 @@ def create_extra_specs(self, session, specs): :returns: The updated flavor. """ url = utils.urljoin(Flavor.base_path, self.id, 'os-extra_specs') - microversion = self._get_microversion(session, action='create') + microversion = self._get_microversion(session) response = session.post( url, json={'extra_specs': specs}, microversion=microversion ) @@ -196,7 +196,7 @@ def get_extra_specs_property(self, session, prop): :returns: The value of the property if it exists, else ``None``. """ url = utils.urljoin(Flavor.base_path, self.id, 'os-extra_specs', prop) - microversion = self._get_microversion(session, action='fetch') + microversion = self._get_microversion(session) response = session.get(url, microversion=microversion) exceptions.raise_from_response(response) val = response.json().get(prop) @@ -211,7 +211,7 @@ def update_extra_specs_property(self, session, prop, val): :returns: The updated value of the property. """ url = utils.urljoin(Flavor.base_path, self.id, 'os-extra_specs', prop) - microversion = self._get_microversion(session, action='commit') + microversion = self._get_microversion(session) response = session.put( url, json={prop: val}, microversion=microversion ) @@ -227,7 +227,7 @@ def delete_extra_specs_property(self, session, prop): :returns: None """ url = utils.urljoin(Flavor.base_path, self.id, 'os-extra_specs', prop) - microversion = self._get_microversion(session, action='delete') + microversion = self._get_microversion(session) response = session.delete(url, microversion=microversion) exceptions.raise_from_response(response) diff --git a/openstack/compute/v2/hypervisor.py b/openstack/compute/v2/hypervisor.py index fdcb1afee..c80016941 100644 --- a/openstack/compute/v2/hypervisor.py +++ b/openstack/compute/v2/hypervisor.py @@ -95,7 +95,7 @@ def get_uptime(self, session): 'Hypervisor.get_uptime is not supported anymore' ) url = utils.urljoin(self.base_path, self.id, 'uptime') - microversion = self._get_microversion(session, action='fetch') + microversion = self._get_microversion(session) response = session.get(url, microversion=microversion) self._translate_response(response) return self diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 5ecf09898..069caacd1 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -333,7 +333,7 @@ def _action(self, session, body, microversion=None): # these aren't all necessary "commit" actions (i.e. updates) but it's # good enough... if microversion is None: - microversion = self._get_microversion(session, action='commit') + microversion = self._get_microversion(session) response = session.post( url, @@ -362,7 +362,7 @@ def get_password(self, session, *, microversion=None): """ url = utils.urljoin(Server.base_path, self.id, 'os-server-password') if microversion is None: - microversion = self._get_microversion(session, action='commit') + microversion = self._get_microversion(session) response = session.get(url, microversion=microversion) exceptions.raise_from_response(response) @@ -381,7 +381,7 @@ def clear_password(self, session, *, microversion=None): """ url = utils.urljoin(Server.base_path, self.id, 'os-server-password') if microversion is None: - microversion = self._get_microversion(session, action='commit') + microversion = self._get_microversion(session) response = session.delete(url, microversion=microversion) exceptions.raise_from_response(response) diff --git a/openstack/compute/v2/server_group.py b/openstack/compute/v2/server_group.py index f61382b16..597453a4d 100644 --- a/openstack/compute/v2/server_group.py +++ b/openstack/compute/v2/server_group.py @@ -76,7 +76,7 @@ def create(self, session, prepend_key=True, base_path=None, **params): raise exceptions.MethodNotSupported(self, 'create') session = self._get_session(session) - microversion = self._get_microversion(session, action='create') + microversion = self._get_microversion(session) requires_id = ( self.create_requires_id if self.create_requires_id is not None diff --git a/openstack/compute/v2/server_migration.py b/openstack/compute/v2/server_migration.py index 42585ab32..fbc6a2ccc 100644 --- a/openstack/compute/v2/server_migration.py +++ b/openstack/compute/v2/server_migration.py @@ -74,7 +74,7 @@ class ServerMigration(resource.Resource): def _action(self, session, body): """Preform server migration actions given the message body.""" session = self._get_session(session) - microversion = self._get_microversion(session, action='list') + microversion = self._get_microversion(session) url = utils.urljoin( self.base_path % {'server_id': self.server_id}, diff --git a/openstack/dns/v2/zone_export.py b/openstack/dns/v2/zone_export.py index 34e0012e2..7a4215413 100644 --- a/openstack/dns/v2/zone_export.py +++ b/openstack/dns/v2/zone_export.py @@ -70,7 +70,7 @@ def create(self, session, prepend_key=True, base_path=None, **kwargs): raise exceptions.MethodNotSupported(self, "create") session = self._get_session(session) - microversion = self._get_microversion(session, action='create') + microversion = self._get_microversion(session) # Create ZoneExport requires empty body # skip _prepare_request completely, since we need just empty body request = resource._Request(self.base_path, None, None) diff --git a/openstack/dns/v2/zone_import.py b/openstack/dns/v2/zone_import.py index 8f875f7b1..8d2c3e145 100644 --- a/openstack/dns/v2/zone_import.py +++ b/openstack/dns/v2/zone_import.py @@ -70,7 +70,7 @@ def create(self, session, prepend_key=True, base_path=None, **kwargs): raise exceptions.MethodNotSupported(self, "create") session = self._get_session(session) - microversion = self._get_microversion(session, action='create') + microversion = self._get_microversion(session) # Create ZoneImport requires empty body and 'text/dns' as content-type # skip _prepare_request completely, since we need just empty body request = resource._Request( diff --git a/openstack/image/v2/cache.py b/openstack/image/v2/cache.py index f181cf59b..b0ad60834 100644 --- a/openstack/image/v2/cache.py +++ b/openstack/image/v2/cache.py @@ -46,7 +46,7 @@ def queue(self, session, image, *, microversion=None): :returns: The server response """ if microversion is None: - microversion = self._get_microversion(session, action='commit') + microversion = self._get_microversion(session) image_id = resource.Resource._get_id(image) url = utils.urljoin(self.base_path, image_id) diff --git a/openstack/image/v2/metadef_property.py b/openstack/image/v2/metadef_property.py index 9abbaacac..20dcc808c 100644 --- a/openstack/image/v2/metadef_property.py +++ b/openstack/image/v2/metadef_property.py @@ -102,7 +102,7 @@ def list( session = cls._get_session(session) if microversion is None: - microversion = cls._get_microversion(session, action='list') + microversion = cls._get_microversion(session) if base_path is None: base_path = cls.base_path diff --git a/openstack/load_balancer/v2/amphora.py b/openstack/load_balancer/v2/amphora.py index f8e9c4de5..b84156ef8 100644 --- a/openstack/load_balancer/v2/amphora.py +++ b/openstack/load_balancer/v2/amphora.py @@ -106,7 +106,7 @@ def configure(self, session): :returns: None """ session = self._get_session(session) - version = self._get_microversion(session, action='patch') + version = self._get_microversion(session) request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'config') @@ -126,7 +126,7 @@ def failover(self, session): :returns: None """ session = self._get_session(session) - version = self._get_microversion(session, action='patch') + version = self._get_microversion(session) request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'failover') diff --git a/openstack/load_balancer/v2/load_balancer.py b/openstack/load_balancer/v2/load_balancer.py index 02fd6a235..1668e484e 100644 --- a/openstack/load_balancer/v2/load_balancer.py +++ b/openstack/load_balancer/v2/load_balancer.py @@ -109,7 +109,7 @@ def failover(self, session): :returns: None """ session = self._get_session(session) - version = self._get_microversion(session, action='patch') + version = self._get_microversion(session) request = self._prepare_request(requires_id=True) request.url = utils.urljoin(request.url, 'failover') diff --git a/openstack/object_store/v1/info.py b/openstack/object_store/v1/info.py index c1dde93b7..5b874067f 100644 --- a/openstack/object_store/v1/info.py +++ b/openstack/object_store/v1/info.py @@ -85,7 +85,7 @@ def fetch( session = self._get_session(session) info_url = self._get_info_url(session.get_endpoint()) - microversion = self._get_microversion(session, action='fetch') + microversion = self._get_microversion(session) response = session.get(info_url, microversion=microversion) self.microversion = microversion diff --git a/openstack/object_store/v1/obj.py b/openstack/object_store/v1/obj.py index 3b036e55a..9b51641c0 100644 --- a/openstack/object_store/v1/obj.py +++ b/openstack/object_store/v1/obj.py @@ -337,7 +337,7 @@ def _raw_delete(self, session, microversion=None, **kwargs): request = self._prepare_request(**kwargs) session = self._get_session(session) if microversion is None: - microversion = self._get_microversion(session, action='delete') + microversion = self._get_microversion(session) if self.is_static_large_object is None: # Fetch metadata to determine SLO flag diff --git a/openstack/orchestration/v1/stack.py b/openstack/orchestration/v1/stack.py index 2571e2a61..8e8712d35 100644 --- a/openstack/orchestration/v1/stack.py +++ b/openstack/orchestration/v1/stack.py @@ -142,7 +142,7 @@ def commit( prepend_key=False, requires_id=False, base_path=base_path ) - microversion = self._get_microversion(session, action='commit') + microversion = self._get_microversion(session) request_url = request.url if preview: @@ -225,7 +225,7 @@ def fetch( requires_id=requires_id, base_path=base_path ) # session = self._get_session(session) - microversion = self._get_microversion(session, action='fetch') + microversion = self._get_microversion(session) # NOTE(gtema): would be nice to simply use QueryParameters, however # Heat return 302 with parameters being set into URL and requests diff --git a/openstack/placement/v1/resource_provider.py b/openstack/placement/v1/resource_provider.py index 4da2d3bcc..4419ab4b2 100644 --- a/openstack/placement/v1/resource_provider.py +++ b/openstack/placement/v1/resource_provider.py @@ -71,7 +71,7 @@ def fetch_aggregates(self, session): :return: The resource provider with aggregates populated """ url = utils.urljoin(self.base_path, self.id, 'aggregates') - microversion = self._get_microversion(session, action='fetch') + microversion = self._get_microversion(session) response = session.get(url, microversion=microversion) exceptions.raise_from_response(response) @@ -92,7 +92,7 @@ def set_aggregates(self, session, aggregates=None): :return: The resource provider with updated aggregates populated """ url = utils.urljoin(self.base_path, self.id, 'aggregates') - microversion = self._get_microversion(session, action='commit') + microversion = self._get_microversion(session) body = { 'aggregates': aggregates or [], diff --git a/openstack/placement/v1/resource_provider_inventory.py b/openstack/placement/v1/resource_provider_inventory.py index d178e88a5..443483a9c 100644 --- a/openstack/placement/v1/resource_provider_inventory.py +++ b/openstack/placement/v1/resource_provider_inventory.py @@ -113,7 +113,7 @@ def list( session = cls._get_session(session) if microversion is None: - microversion = cls._get_microversion(session, action='list') + microversion = cls._get_microversion(session) if base_path is None: base_path = cls.base_path diff --git a/openstack/placement/v1/trait.py b/openstack/placement/v1/trait.py index 75b7875ba..be941f2b7 100644 --- a/openstack/placement/v1/trait.py +++ b/openstack/placement/v1/trait.py @@ -64,7 +64,7 @@ def list( session = cls._get_session(session) if microversion is None: - microversion = cls._get_microversion(session, action='list') + microversion = cls._get_microversion(session) if base_path is None: base_path = cls.base_path diff --git a/openstack/resource.py b/openstack/resource.py index 78e2134cf..9caef9029 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -53,6 +53,8 @@ class that represent a remote resource. The attributes that LOG = _log.setup_logging(__name__) +AdapterT = ty.TypeVar('AdapterT', bound=adapter.Adapter) + # TODO(stephenfin): We should deprecate the 'type' and 'list_type' arguments # for all of the below in favour of annotations. To that end, we have stuck @@ -1218,7 +1220,7 @@ def _translate_response( dict.update(self, self.to_dict()) @classmethod - def _get_session(cls, session): + def _get_session(cls, session: AdapterT) -> AdapterT: """Attempt to get an Adapter from a raw session. Some older code used conn.session has the session argument to Resource @@ -1238,9 +1240,8 @@ def _get_session(cls, session): "a raw keystoneauth1.adapter.Adapter." ) - # TODO(stephenfin): Drop action argument. It has never been used. @classmethod - def _get_microversion(cls, session, *, action=None): + def _get_microversion(cls, session: adapter.Adapter) -> ty.Optional[str]: """Get microversion to use for the given action. The base version uses the following logic: @@ -1255,21 +1256,8 @@ def _get_microversion(cls, session, *, action=None): :param session: The session to use for making the request. :type session: :class:`~keystoneauth1.adapter.Adapter` - :param action: One of "fetch", "commit", "create", "delete", "patch". - :type action: str :return: Microversion as string or ``None`` """ - if action not in { - 'list', - 'fetch', - 'commit', - 'create', - 'delete', - 'patch', - None, - }: - raise ValueError(f'Invalid action: {action}') - if session.default_microversion: return session.default_microversion @@ -1372,7 +1360,7 @@ def create( session = self._get_session(session) if microversion is None: - microversion = self._get_microversion(session, action='create') + microversion = self._get_microversion(session) requires_id = ( self.create_requires_id if self.create_requires_id is not None @@ -1475,7 +1463,7 @@ def bulk_create( session = cls._get_session(session) if microversion is None: - microversion = cls._get_microversion(session, action='create') + microversion = cls._get_microversion(session) requires_id = ( cls.create_requires_id if cls.create_requires_id is not None @@ -1588,7 +1576,7 @@ def fetch( session = self._get_session(session) if microversion is None: - microversion = self._get_microversion(session, action='fetch') + microversion = self._get_microversion(session) self.microversion = microversion response = session.get( @@ -1626,7 +1614,7 @@ def head(self, session, base_path=None, *, microversion=None): session = self._get_session(session) if microversion is None: - microversion = self._get_microversion(session, action='fetch') + microversion = self._get_microversion(session) self.microversion = microversion request = self._prepare_request(base_path=base_path) @@ -1694,7 +1682,7 @@ def commit( **kwargs, ) if microversion is None: - microversion = self._get_microversion(session, action='commit') + microversion = self._get_microversion(session) return self._commit( session, @@ -1824,7 +1812,7 @@ def patch( patch=True, ) if microversion is None: - microversion = self._get_microversion(session, action='patch') + microversion = self._get_microversion(session) if patch: request.body += self._convert_patch(patch) @@ -1874,7 +1862,7 @@ def _raw_delete(self, session, microversion=None, **kwargs): request = self._prepare_request(**kwargs) session = self._get_session(session) if microversion is None: - microversion = self._get_microversion(session, action='delete') + microversion = self._get_microversion(session) return session.delete( request.url, @@ -1938,7 +1926,7 @@ def list( session = cls._get_session(session) if microversion is None: - microversion = cls._get_microversion(session, action='list') + microversion = cls._get_microversion(session) if base_path is None: base_path = cls.base_path diff --git a/openstack/shared_file_system/v2/share.py b/openstack/shared_file_system/v2/share.py index 621ea729d..8ec1fbe85 100644 --- a/openstack/shared_file_system/v2/share.py +++ b/openstack/shared_file_system/v2/share.py @@ -107,13 +107,13 @@ class Share(resource.Resource, metadata.MetadataMixin): #: Display description for updating description display_description = resource.Body("display_description", type=str) - def _action(self, session, body, action='patch', microversion=None): + def _action(self, session, body, microversion=None): """Perform share instance actions given the message body""" url = utils.urljoin(self.base_path, self.id, 'action') headers = {'Accept': ''} if microversion is None: - microversion = self._get_microversion(session, action=action) + microversion = self._get_microversion(session) response = session.post( url, json=body, headers=headers, microversion=microversion diff --git a/openstack/shared_file_system/v2/share_access_rule.py b/openstack/shared_file_system/v2/share_access_rule.py index 792cc8100..06cc78a84 100644 --- a/openstack/shared_file_system/v2/share_access_rule.py +++ b/openstack/shared_file_system/v2/share_access_rule.py @@ -63,11 +63,11 @@ class ShareAccessRule(resource.Resource): #: Reason for placing the loc lock_reason = resource.Body("lock_reason", type=bool) - def _action(self, session, body, url, action='patch', microversion=None): + def _action(self, session, body, url, microversion=None): headers = {'Accept': ''} if microversion is None: - microversion = self._get_microversion(session, action=action) + microversion = self._get_microversion(session) return session.post( url, json=body, headers=headers, microversion=microversion diff --git a/openstack/shared_file_system/v2/share_group_snapshot.py b/openstack/shared_file_system/v2/share_group_snapshot.py index 85b86ab95..504efc38e 100644 --- a/openstack/shared_file_system/v2/share_group_snapshot.py +++ b/openstack/shared_file_system/v2/share_group_snapshot.py @@ -59,16 +59,14 @@ class ShareGroupSnapshot(resource.Resource): #: NFS, CIFS, GlusterFS, HDFS, CephFS or MAPRFS. share_protocol = resource.Body("share_proto", type=str) - def _action(self, session, body, action='patch', microversion=None): + def _action(self, session, body, microversion=None): """Perform ShareGroupSnapshot actions given the message body.""" # NOTE: This is using ShareGroupSnapshot.base_path instead of # self.base_path as ShareGroupSnapshot instances can be acted on, # but the URL used is sans any additional /detail/ part. url = utils.urljoin(self.base_path, self.id, 'action') headers = {'Accept': ''} - microversion = microversion or self._get_microversion( - session, action=action - ) + microversion = microversion or self._get_microversion(session) extra_attrs = {'microversion': microversion} session.post(url, json=body, headers=headers, **extra_attrs) @@ -78,9 +76,7 @@ def reset_status(self, session, status): def get_members(self, session, microversion=None): url = utils.urljoin(self.base_path, self.id, 'members') - microversion = microversion or self._get_microversion( - session, action='list' - ) + microversion = microversion or self._get_microversion(session) headers = {'Accept': ''} response = session.get(url, headers=headers, microversion=microversion) return response.json() diff --git a/openstack/shared_file_system/v2/share_instance.py b/openstack/shared_file_system/v2/share_instance.py index a0d7bd056..93f3b7fe7 100644 --- a/openstack/shared_file_system/v2/share_instance.py +++ b/openstack/shared_file_system/v2/share_instance.py @@ -57,7 +57,7 @@ class ShareInstance(resource.Resource): #: The share or share instance status. status = resource.Body("status", type=str) - def _action(self, session, body, action='patch', microversion=None): + def _action(self, session, body, microversion=None): """Perform share instance actions given the message body""" url = utils.urljoin(self.base_path, self.id, 'action') headers = {'Accept': ''} @@ -66,9 +66,7 @@ def _action(self, session, body, action='patch', microversion=None): # Set microversion override extra_attrs['microversion'] = microversion else: - extra_attrs['microversion'] = self._get_microversion( - session, action=action - ) + extra_attrs['microversion'] = self._get_microversion(session) response = session.post(url, json=body, headers=headers, **extra_attrs) exceptions.raise_from_response(response) return response @@ -81,4 +79,4 @@ def reset_status(self, session, reset_status): def force_delete(self, session): """Force delete share instance""" body = {"force_delete": None} - self._action(session, body, action='delete') + self._action(session, body) diff --git a/openstack/tests/unit/shared_file_system/v2/test_share.py b/openstack/tests/unit/shared_file_system/v2/test_share.py index 61f98fdce..9fd4c6eea 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_share.py +++ b/openstack/tests/unit/shared_file_system/v2/test_share.py @@ -144,7 +144,7 @@ def setUp(self): def test_shrink_share(self): sot = share.Share(**EXAMPLE) - microversion = sot._get_microversion(self.sess, action='patch') + microversion = sot._get_microversion(self.sess) self.assertIsNone(sot.shrink_share(self.sess, new_size=1)) @@ -158,7 +158,7 @@ def test_shrink_share(self): def test_extend_share(self): sot = share.Share(**EXAMPLE) - microversion = sot._get_microversion(self.sess, action='patch') + microversion = sot._get_microversion(self.sess) self.assertIsNone(sot.extend_share(self.sess, new_size=3)) @@ -172,7 +172,7 @@ def test_extend_share(self): def test_revert_to_snapshot(self): sot = share.Share(**EXAMPLE) - microversion = sot._get_microversion(self.sess, action='patch') + microversion = sot._get_microversion(self.sess) self.assertIsNone(sot.revert_to_snapshot(self.sess, "fake_id")) @@ -220,7 +220,7 @@ def test_manage_share(self): def test_unmanage_share(self): sot = share.Share(**EXAMPLE) - microversion = sot._get_microversion(self.sess, action='patch') + microversion = sot._get_microversion(self.sess) self.assertIsNone(sot.unmanage(self.sess)) diff --git a/openstack/tests/unit/shared_file_system/v2/test_share_instance.py b/openstack/tests/unit/shared_file_system/v2/test_share_instance.py index 6d9be78ef..cb461ffa8 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_share_instance.py +++ b/openstack/tests/unit/shared_file_system/v2/test_share_instance.py @@ -96,7 +96,7 @@ def setUp(self): def test_reset_status(self): sot = share_instance.ShareInstance(**EXAMPLE) - microversion = sot._get_microversion(self.sess, action='patch') + microversion = sot._get_microversion(self.sess) self.assertIsNone(sot.reset_status(self.sess, 'active')) @@ -109,7 +109,7 @@ def test_reset_status(self): def test_force_delete(self): sot = share_instance.ShareInstance(**EXAMPLE) - microversion = sot._get_microversion(self.sess, action='delete') + microversion = sot._get_microversion(self.sess) self.assertIsNone(sot.force_delete(self.sess)) From c4f19bb721168aa3ed38542abad3fde52d805b1c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 6 Mar 2025 11:50:31 +0000 Subject: [PATCH 3669/3836] volume: Add service 'set-log', 'get-log' actions Change-Id: Ie833b4d42a4d72a3db1ce9e3c3e38922e0949755 Signed-off-by: Stephen Finucane --- doc/source/user/proxies/block_storage_v3.rst | 3 +- openstack/block_storage/v3/_proxy.py | 44 +++++++ openstack/block_storage/v3/service.py | 118 +++++++++++++++++- .../tests/unit/block_storage/v3/test_proxy.py | 27 ++++ .../unit/block_storage/v3/test_service.py | 63 +++++++++- 5 files changed, 247 insertions(+), 8 deletions(-) diff --git a/doc/source/user/proxies/block_storage_v3.rst b/doc/source/user/proxies/block_storage_v3.rst index 0498baffb..28711a8ff 100644 --- a/doc/source/user/proxies/block_storage_v3.rst +++ b/doc/source/user/proxies/block_storage_v3.rst @@ -99,7 +99,8 @@ Service Operations .. autoclass:: openstack.block_storage.v3._proxy.Proxy :noindex: :members: find_service, services, enable_service, disable_service, - thaw_service, freeze_service, failover_service + thaw_service, freeze_service, failover_service, + get_service_log_levels, set_service_log_levels Type Operations ^^^^^^^^^^^^^^^ diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 962011619..427924d67 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -2090,6 +2090,50 @@ def freeze_service( service_obj = self._get_resource(_service.Service, service) return service_obj.freeze(self) + def set_service_log_levels( + self, + *, + level: _service.Level, + binary: ty.Optional[_service.Binary] = None, + server: ty.Optional[str] = None, + prefix: ty.Optional[str] = None, + ) -> None: + """Set log level for services. + + :param level: The log level to set, case insensitive, accepted values + are ``INFO``, ``WARNING``, ``ERROR`` and ``DEBUG``. + :param binary: The binary name of the service. + :param server: The name of the host. + :param prefix: The prefix for the log path we are querying, for example + ``cinder.`` or ``sqlalchemy.engine.`` When not present or the empty + string is passed all log levels will be retrieved. + :returns: None. + """ + return _service.Service.set_log_levels( + self, level=level, binary=binary, server=server, prefix=prefix + ) + + def get_service_log_levels( + self, + *, + binary: ty.Optional[_service.Binary] = None, + server: ty.Optional[str] = None, + prefix: ty.Optional[str] = None, + ) -> ty.Generator[_service.LogLevel, None, None]: + """Get log level for services. + + :param binary: The binary name of the service. + :param server: The name of the host. + :param prefix: The prefix for the log path we are querying, for example + ``cinder.`` or ``sqlalchemy.engine.`` When not present or the empty + string is passed all log levels will be retrieved. + :returns: A generator of + :class:`~openstack.block_storage.v3.log_level.LogLevel` objects. + """ + return _service.Service.get_log_levels( + self, binary=binary, server=server, prefix=prefix + ) + def failover_service( self, service: ty.Union[str, _service.Service], diff --git a/openstack/block_storage/v3/service.py b/openstack/block_storage/v3/service.py index 871c51f9a..2d536a271 100644 --- a/openstack/block_storage/v3/service.py +++ b/openstack/block_storage/v3/service.py @@ -10,11 +10,48 @@ # License for the specific language governing permissions and limitations # under the License. +import enum +import typing as ty + +from keystoneauth1 import adapter as ksa_adapter + from openstack import exceptions from openstack import resource from openstack import utils +class Level(enum.Enum): + ERROR = 'ERROR' + WARNING = 'WARNING' + INFO = 'INFO' + DEBUG = 'DEBUG' + + +class Binary(enum.Enum): + ANY = '*' + API = 'cinder-api' + VOLUME = 'cinder-volume' + SCHEDULER = 'cinder-scheduler' + BACKUP = 'cinder-backup' + + +class LogLevel(resource.Resource): + # Properties + #: The binary name of the service. + binary = resource.Body('binary') + # TODO(stephenfin): Do we need these? They are request-only. + # #: The name of the host. + # server = resource.Body('server') + # #: he prefix for the log path we are querying, for example ``cinder.`` or + # #: ``sqlalchemy.engine.`` When not present or the empty string is passed + # #: all log levels will be retrieved. + # prefix = resource.Body('prefix') + #: The name of the host. + host = resource.Body('host') + #: The current log level that queried. + levels = resource.Body('levels', type=dict) + + class Service(resource.Resource): resources_key = 'services' base_path = '/os-services' @@ -55,8 +92,8 @@ class Service(resource.Resource): #: The date and time when the resource was updated updated_at = resource.Body('updated_at') - # 3.7 introduced the 'cluster' field - _max_microversion = '3.7' + # 3.32 introduced the 'set-log' action + _max_microversion = '3.32' @classmethod def find(cls, session, name_or_id, ignore_missing=True, **params): @@ -107,9 +144,6 @@ def _action(self, session, action, body, microversion=None): self._translate_response(response) return self - # TODO(stephenfin): Add support for log levels once we have the resource - # modelled (it can be done on a deployment wide basis) - def enable(self, session): """Enable service.""" body = {'binary': self.binary, 'host': self.host} @@ -135,6 +169,80 @@ def freeze(self, session): body = {'host': self.host} return self._action(session, 'freeze', body) + @classmethod + def set_log_levels( + cls, + session: ksa_adapter.Adapter, + *, + level: Level, + binary: ty.Optional[Binary] = None, + server: ty.Optional[str] = None, + prefix: ty.Optional[str] = None, + ) -> None: + """Set log level for services. + + :param session: The session to use for making this request. + :param level: The log level to set, case insensitive, accepted values + are ``INFO``, ``WARNING``, ``ERROR`` and ``DEBUG``. + :param binary: The binary name of the service. + :param server: The name of the host. + :param prefix: The prefix for the log path we are querying, for example + ``cinder.`` or ``sqlalchemy.engine.`` When not present or the empty + string is passed all log levels will be retrieved. + :returns: None. + """ + microversion = cls._assert_microversion_for( + session, '3.32', error_message="Cannot use set-log action" + ) + body = { + 'level': level, + 'binary': binary or '', # cinder insists on strings + 'server': server, + 'prefix': prefix, + } + url = utils.urljoin(cls.base_path, 'set-log') + response = session.put(url, json=body, microversion=microversion) + exceptions.raise_from_response(response) + + @classmethod + def get_log_levels( + cls, + session: ksa_adapter.Adapter, + *, + binary: ty.Optional[Binary] = None, + server: ty.Optional[str] = None, + prefix: ty.Optional[str] = None, + ) -> ty.Generator[LogLevel, None, None]: + """Get log level for services. + + :param session: The session to use for making this request. + :param binary: The binary name of the service. + :param server: The name of the host. + :param prefix: The prefix for the log path we are querying, for example + ``cinder.`` or ``sqlalchemy.engine.`` When not present or the empty + string is passed all log levels will be retrieved. + :returns: A generator of + :class:`~openstack.block_storage.v3.service.LogLevel` objects. + """ + microversion = cls._assert_microversion_for( + session, '3.32', error_message="Cannot use get-log action" + ) + body = { + 'binary': binary or '', # cinder insists on strings + 'server': server, + 'prefix': prefix, + } + url = utils.urljoin(cls.base_path, 'get-log') + response = session.put(url, json=body, microversion=microversion) + exceptions.raise_from_response(response) + + for entry in response.json()['log_levels']: + yield LogLevel( + binary=entry['binary'], + host=entry['host'], + levels=entry['levels'], + ) + def failover( self, session, diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 3cbaab574..ef021ed82 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -367,6 +367,33 @@ def test_freeze_service(self): expected_args=[self.proxy], ) + def test_set_service_log_levels(self): + self._verify( + 'openstack.block_storage.v3.service.Service.set_log_levels', + self.proxy.set_service_log_levels, + method_kwargs={"level": service.Level.INFO}, + expected_args=[self.proxy], + expected_kwargs={ + "level": service.Level.INFO, + "binary": None, + "server": None, + "prefix": None, + }, + ) + + def test_get_service_log_level(self): + self._verify( + 'openstack.block_storage.v3.service.Service.get_log_levels', + self.proxy.get_service_log_levels, + method_args=[], + expected_args=[self.proxy], + expected_kwargs={ + "binary": None, + "server": None, + "prefix": None, + }, + ) + def test_failover_service(self): self._verify( 'openstack.block_storage.v3.service.Service.failover', diff --git a/openstack/tests/unit/block_storage/v3/test_service.py b/openstack/tests/unit/block_storage/v3/test_service.py index 7f66a17b0..fe2f30133 100644 --- a/openstack/tests/unit/block_storage/v3/test_service.py +++ b/openstack/tests/unit/block_storage/v3/test_service.py @@ -30,8 +30,8 @@ class TestService(base.TestCase): def setUp(self): super().setUp() self.resp = mock.Mock() - self.resp.body = {'service': {}} - self.resp.json = mock.Mock(return_value=self.resp.body) + self.resp.body = None # nothing uses this + self.resp.json = mock.Mock(return_value={'service': {}}) self.resp.status_code = 200 self.resp.headers = {} self.sess = mock.Mock() @@ -150,6 +150,65 @@ def test_freeze(self): microversion=self.sess.default_microversion, ) + def test_set_log_levels(self): + self.sess.default_microversion = '3.32' + res = service.Service.set_log_levels( + self.sess, + level=service.Level.DEBUG, + binary=service.Binary.ANY, + server='foo', + prefix='cinder.', + ) + self.assertIsNone(res) + + url = 'os-services/set-log' + body = { + 'level': service.Level.DEBUG, + 'binary': service.Binary.ANY, + 'server': 'foo', + 'prefix': 'cinder.', + } + self.sess.put.assert_called_with( + url, + json=body, + microversion=self.sess.default_microversion, + ) + + def test_get_log_levels(self): + self.sess.default_microversion = '3.32' + self.resp.json = mock.Mock( + return_value={ + 'log_levels': [ + { + "binary": "cinder-api", + "host": "devstack", + "levels": {"cinder.volume.api": "DEBUG"}, + }, + ], + }, + ) + res = list( + service.Service.get_log_levels( + self.sess, + binary=service.Binary.ANY, + server='foo', + prefix='cinder.', + ) + ) + self.assertIsNotNone(res) + + url = 'os-services/get-log' + body = { + 'binary': service.Binary.ANY, + 'server': 'foo', + 'prefix': 'cinder.', + } + self.sess.put.assert_called_with( + url, + json=body, + microversion=self.sess.default_microversion, + ) + @mock.patch( 'openstack.utils.supports_microversion', autospec=True, From 699422369140256f693c4f25ffbcadab69fb3b4e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 6 Mar 2025 18:17:45 +0000 Subject: [PATCH 3670/3836] volume: Add Service to volume v2 API Another one needed for OSC purposes. Change-Id: I5d69d3f71df1a489f860bab3f6244e5661ac4ca0 Signed-off-by: Stephen Finucane --- doc/source/user/proxies/block_storage_v2.rst | 8 + .../resources/block_storage/v2/service.rst | 12 ++ openstack/block_storage/v2/_proxy.py | 154 +++++++++++++++- openstack/block_storage/v2/service.py | 143 +++++++++++++++ openstack/block_storage/v3/service.py | 2 +- .../tests/unit/block_storage/v2/test_proxy.py | 48 +++++ .../unit/block_storage/v2/test_service.py | 165 ++++++++++++++++++ 7 files changed, 530 insertions(+), 2 deletions(-) create mode 100644 doc/source/user/resources/block_storage/v2/service.rst create mode 100644 openstack/block_storage/v2/service.py create mode 100644 openstack/tests/unit/block_storage/v2/test_service.py diff --git a/doc/source/user/proxies/block_storage_v2.rst b/doc/source/user/proxies/block_storage_v2.rst index 511dbdd41..d3438876a 100644 --- a/doc/source/user/proxies/block_storage_v2.rst +++ b/doc/source/user/proxies/block_storage_v2.rst @@ -82,6 +82,14 @@ QuotaSet Operations :members: get_quota_set, get_quota_set_defaults, revert_quota_set, update_quota_set +Service Operations +^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v2._proxy.Proxy + :noindex: + :members: find_service, services, enable_service, disable_service, + thaw_service, freeze_service, failover_service + Helpers ^^^^^^^ diff --git a/doc/source/user/resources/block_storage/v2/service.rst b/doc/source/user/resources/block_storage/v2/service.rst new file mode 100644 index 000000000..60a451de1 --- /dev/null +++ b/doc/source/user/resources/block_storage/v2/service.rst @@ -0,0 +1,12 @@ +openstack.block_storage.v2.service +================================== + +.. automodule:: openstack.block_storage.v2.service + +The Service Class +----------------- + +The ``Service`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v2.service.Service + :members: diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 0cd12a39b..c59e72289 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -19,6 +19,7 @@ from openstack.block_storage.v2 import limits as _limits from openstack.block_storage.v2 import quota_class_set as _quota_class_set from openstack.block_storage.v2 import quota_set as _quota_set +from openstack.block_storage.v2 import service as _service from openstack.block_storage.v2 import snapshot as _snapshot from openstack.block_storage.v2 import stats as _stats from openstack.block_storage.v2 import type as _type @@ -886,7 +887,7 @@ def update_quota_set(self, project, **attrs): by ``quota_set``. :returns: The updated QuotaSet - :rtype: :class:`~openstack.block_storage.v3.quota_set.QuotaSet` + :rtype: :class:`~openstack.block_storage.v2.quota_set.QuotaSet` """ if 'project_id' in attrs or isinstance(project, _quota_set.QuotaSet): warnings.warn( @@ -912,6 +913,157 @@ def update_quota_set(self, project, **attrs): attrs['project_id'] = project.id return self._update(_quota_set.QuotaSet, None, **attrs) + # ========== Services ========== + @ty.overload + def find_service( + self, + name_or_id: str, + ignore_missing: ty.Literal[True] = True, + **query: ty.Any, + ) -> ty.Optional[_service.Service]: ... + + @ty.overload + def find_service( + self, + name_or_id: str, + ignore_missing: ty.Literal[False], + **query: ty.Any, + ) -> _service.Service: ... + + # excuse the duplication here: it's mypy's fault + # https://github.com/python/mypy/issues/14764 + @ty.overload + def find_service( + self, + name_or_id: str, + ignore_missing: bool, + **query: ty.Any, + ) -> ty.Optional[_service.Service]: ... + + def find_service( + self, + name_or_id: str, + ignore_missing: bool = True, + **query: ty.Any, + ) -> ty.Optional[_service.Service]: + """Find a single service + + :param name_or_id: The name or ID of a service + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.NotFoundException` will be raised when + the resource does not exist. + When set to ``True``, None will be returned when attempting to find + a nonexistent resource. + :param dict query: Additional attributes like 'host' + + :returns: One: class:`~openstack.block_storage.v2.service.Service` or None + :raises: :class:`~openstack.exceptions.NotFoundException` + when no resource can be found. + :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple + resources are found. + """ + return self._find( + _service.Service, + name_or_id, + ignore_missing=ignore_missing, + **query, + ) + + def services( + self, + **query: ty.Any, + ) -> ty.Generator[_service.Service, None, None]: + """Return a generator of service + + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. + :returns: A generator of Service objects + :rtype: class: `~openstack.block_storage.v2.service.Service` + """ + return self._list(_service.Service, **query) + + def enable_service( + self, + service: ty.Union[str, _service.Service], + ) -> _service.Service: + """Enable a service + + :param service: Either the ID of a service or a + :class:`~openstack.block_storage.v2.service.Service` instance. + + :returns: Updated service instance + :rtype: class: `~openstack.block_storage.v2.service.Service` + """ + service_obj = self._get_resource(_service.Service, service) + return service_obj.enable(self) + + def disable_service( + self, + service: ty.Union[str, _service.Service], + *, + reason: ty.Optional[str] = None, + ) -> _service.Service: + """Disable a service + + :param service: Either the ID of a service or a + :class:`~openstack.block_storage.v2.service.Service` instance + :param str reason: The reason to disable a service + + :returns: Updated service instance + :rtype: class: `~openstack.block_storage.v2.service.Service` + """ + service_obj = self._get_resource(_service.Service, service) + return service_obj.disable(self, reason=reason) + + def thaw_service( + self, + service: ty.Union[str, _service.Service], + ) -> _service.Service: + """Thaw a service + + :param service: Either the ID of a service or a + :class:`~openstack.block_storage.v2.service.Service` instance + + :returns: Updated service instance + :rtype: class: `~openstack.block_storage.v2.service.Service` + """ + service_obj = self._get_resource(_service.Service, service) + return service_obj.thaw(self) + + def freeze_service( + self, + service: ty.Union[str, _service.Service], + ) -> _service.Service: + """Freeze a service + + :param service: Either the ID of a service or a + :class:`~openstack.block_storage.v2.service.Service` instance + + :returns: Updated service instance + :rtype: class: `~openstack.block_storage.v2.service.Service` + """ + service_obj = self._get_resource(_service.Service, service) + return service_obj.freeze(self) + + def failover_service( + self, + service: ty.Union[str, _service.Service], + *, + backend_id: ty.Optional[str] = None, + ) -> _service.Service: + """Failover a service + + Only applies to replicating cinder-volume services. + + :param service: Either the ID of a service or a + :class:`~openstack.block_storage.v2.service.Service` instance + + :returns: Updated service instance + :rtype: class: `~openstack.block_storage.v2.service.Service` + """ + service_obj = self._get_resource(_service.Service, service) + return service_obj.failover(self, backend_id=backend_id) + # ========== Volume metadata ========== def get_volume_metadata(self, volume): diff --git a/openstack/block_storage/v2/service.py b/openstack/block_storage/v2/service.py new file mode 100644 index 000000000..38682d1f3 --- /dev/null +++ b/openstack/block_storage/v2/service.py @@ -0,0 +1,143 @@ +# 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 import resource +from openstack import utils + + +class Service(resource.Resource): + resources_key = 'services' + base_path = '/os-services' + + # capabilities + allow_list = True + + _query_mapping = resource.QueryParameters( + 'binary', + 'host', + ) + + # Properties + #: The ID of active storage backend (cinder-volume services only) + active_backend_id = resource.Body('active_backend_id') + #: The availability zone of service + availability_zone = resource.Body('zone') + #: Binary name of service + binary = resource.Body('binary') + #: Disabled reason of service + disabled_reason = resource.Body('disabled_reason') + #: The name of the host where service runs + host = resource.Body('host') + # Whether the host is frozen or not (cinder-volume services only) + is_frozen = resource.Body('frozen') + #: Service name + name = resource.Body('name', alias='binary') + #: The volume service replication status (cinder-volume services only) + replication_status = resource.Body('replication_status') + #: State of service + state = resource.Body('state') + #: Status of service + status = resource.Body('status') + #: The date and time when the resource was updated + updated_at = resource.Body('updated_at') + + @classmethod + def find(cls, session, name_or_id, ignore_missing=True, **params): + # No direct request possible, thus go directly to list + data = cls.list(session, **params) + + result = None + for maybe_result in data: + # Since ID might be both int and str force cast + id_value = str(cls._get_id(maybe_result)) + name_value = maybe_result.name + + if str(name_or_id) in (id_value, name_value): + if 'host' in params and maybe_result['host'] != params['host']: + continue + # Only allow one resource to be found. If we already + # found a match, raise an exception to show it. + if result is None: + result = maybe_result + else: + msg = "More than one %s exists with the name '%s'." + msg = msg % (cls.__name__, name_or_id) + raise exceptions.DuplicateResource(msg) + + if result is not None: + return result + + if ignore_missing: + return None + raise exceptions.NotFoundException( + f"No {cls.__name__} found for {name_or_id}" + ) + + def commit(self, session, prepend_key=False, *args, **kwargs): + # we need to set prepend_key to false + return super().commit( + session, + prepend_key, + *args, + **kwargs, + ) + + def _action(self, session, action, body, microversion=None): + if not microversion: + microversion = session.default_microversion + url = utils.urljoin(Service.base_path, action) + response = session.put(url, json=body, microversion=microversion) + self._translate_response(response) + return self + + def enable(self, session): + """Enable service.""" + body = {'binary': self.binary, 'host': self.host} + return self._action(session, 'enable', body) + + def disable(self, session, *, reason=None): + """Disable service.""" + body = {'binary': self.binary, 'host': self.host} + + if not reason: + action = 'disable' + else: + action = 'disable-log-reason' + body['disabled_reason'] = reason + + return self._action(session, action, body) + + def thaw(self, session): + body = {'host': self.host} + return self._action(session, 'thaw', body) + + def freeze(self, session): + body = {'host': self.host} + return self._action(session, 'freeze', body) + + def failover( + self, + session, + *, + backend_id=None, + ): + """Failover a service + + Only applies to replicating cinder-volume services. + """ + body = {'host': self.host} + if backend_id: + body['backend_id'] = backend_id + + return self._action(session, 'failover_host', body) diff --git a/openstack/block_storage/v3/service.py b/openstack/block_storage/v3/service.py index 2d536a271..f7e39716a 100644 --- a/openstack/block_storage/v3/service.py +++ b/openstack/block_storage/v3/service.py @@ -69,7 +69,7 @@ class Service(resource.Resource): active_backend_id = resource.Body('active_backend_id') #: The availability zone of service availability_zone = resource.Body('zone') - #: The state of storage backend (cinder-volume services only) + #: The state of storage backend (cinder-volume services only) (since 3.49) backend_state = resource.Body('backend_state') #: Binary name of service binary = resource.Body('binary') diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index 6c7e2437d..bcd12d8c0 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -19,6 +19,7 @@ from openstack.block_storage.v2 import limits from openstack.block_storage.v2 import quota_class_set from openstack.block_storage.v2 import quota_set +from openstack.block_storage.v2 import service from openstack.block_storage.v2 import snapshot from openstack.block_storage.v2 import stats from openstack.block_storage.v2 import type @@ -614,3 +615,50 @@ def test_quota_set_update__legacy(self, mock_get): "The signature of 'update_quota_set' has changed ", str(w[-1]), ) + + +class TestService(TestVolumeProxy): + def test_services(self): + self.verify_list(self.proxy.services, service.Service) + + def test_enable_service(self): + self._verify( + 'openstack.block_storage.v2.service.Service.enable', + self.proxy.enable_service, + method_args=["value"], + expected_args=[self.proxy], + ) + + def test_disable_service(self): + self._verify( + 'openstack.block_storage.v2.service.Service.disable', + self.proxy.disable_service, + method_args=["value"], + expected_kwargs={"reason": None}, + expected_args=[self.proxy], + ) + + def test_thaw_service(self): + self._verify( + 'openstack.block_storage.v2.service.Service.thaw', + self.proxy.thaw_service, + method_args=["value"], + expected_args=[self.proxy], + ) + + def test_freeze_service(self): + self._verify( + 'openstack.block_storage.v2.service.Service.freeze', + self.proxy.freeze_service, + method_args=["value"], + expected_args=[self.proxy], + ) + + def test_failover_service(self): + self._verify( + 'openstack.block_storage.v2.service.Service.failover', + self.proxy.failover_service, + method_args=["value"], + expected_args=[self.proxy], + expected_kwargs={"backend_id": None}, + ) diff --git a/openstack/tests/unit/block_storage/v2/test_service.py b/openstack/tests/unit/block_storage/v2/test_service.py new file mode 100644 index 000000000..35a6429cd --- /dev/null +++ b/openstack/tests/unit/block_storage/v2/test_service.py @@ -0,0 +1,165 @@ +# 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 unittest import mock + +from openstack.block_storage.v2 import service +from openstack.tests.unit import base + +EXAMPLE = { + "binary": "cinder-scheduler", + "disabled_reason": None, + "host": "devstack", + "state": "up", + "status": "enabled", + "updated_at": "2017-06-29T05:50:35.000000", + "zone": "nova", +} + + +class TestService(base.TestCase): + def setUp(self): + super().setUp() + self.resp = mock.Mock() + self.resp.body = None # nothing uses this + self.resp.json = mock.Mock(return_value={'service': {}}) + self.resp.status_code = 200 + self.resp.headers = {} + self.sess = mock.Mock() + self.sess.put = mock.Mock(return_value=self.resp) + self.sess.default_microversion = '3.0' + + def test_basic(self): + sot = service.Service() + self.assertIsNone(sot.resource_key) + self.assertEqual('services', sot.resources_key) + self.assertEqual('/os-services', sot.base_path) + self.assertFalse(sot.allow_commit) + self.assertTrue(sot.allow_list) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_delete) + + self.assertDictEqual( + { + 'binary': 'binary', + 'host': 'host', + 'limit': 'limit', + 'marker': 'marker', + }, + sot._query_mapping._mapping, + ) + + def test_make_it(self): + sot = service.Service(**EXAMPLE) + self.assertEqual(EXAMPLE['binary'], sot.binary) + self.assertEqual(EXAMPLE['binary'], sot.name) + self.assertEqual(EXAMPLE['disabled_reason'], sot.disabled_reason) + self.assertEqual(EXAMPLE['host'], sot.host) + self.assertEqual(EXAMPLE['state'], sot.state) + self.assertEqual(EXAMPLE['status'], sot.status) + self.assertEqual(EXAMPLE['zone'], sot.availability_zone) + + def test_enable(self): + sot = service.Service(**EXAMPLE) + + res = sot.enable(self.sess) + self.assertIsNotNone(res) + + url = 'os-services/enable' + body = { + 'binary': 'cinder-scheduler', + 'host': 'devstack', + } + self.sess.put.assert_called_with( + url, + json=body, + microversion=self.sess.default_microversion, + ) + + def test_disable(self): + sot = service.Service(**EXAMPLE) + + res = sot.disable(self.sess) + self.assertIsNotNone(res) + + url = 'os-services/disable' + body = { + 'binary': 'cinder-scheduler', + 'host': 'devstack', + } + self.sess.put.assert_called_with( + url, + json=body, + microversion=self.sess.default_microversion, + ) + + def test_disable__with_reason(self): + sot = service.Service(**EXAMPLE) + reason = 'fencing' + + res = sot.disable(self.sess, reason=reason) + + self.assertIsNotNone(res) + + url = 'os-services/disable-log-reason' + body = { + 'binary': 'cinder-scheduler', + 'host': 'devstack', + 'disabled_reason': reason, + } + self.sess.put.assert_called_with( + url, + json=body, + microversion=self.sess.default_microversion, + ) + + def test_thaw(self): + sot = service.Service(**EXAMPLE) + + res = sot.thaw(self.sess) + self.assertIsNotNone(res) + + url = 'os-services/thaw' + body = {'host': 'devstack'} + self.sess.put.assert_called_with( + url, + json=body, + microversion=self.sess.default_microversion, + ) + + def test_freeze(self): + sot = service.Service(**EXAMPLE) + + res = sot.freeze(self.sess) + self.assertIsNotNone(res) + + url = 'os-services/freeze' + body = {'host': 'devstack'} + self.sess.put.assert_called_with( + url, + json=body, + microversion=self.sess.default_microversion, + ) + + def test_failover(self): + sot = service.Service(**EXAMPLE) + + res = sot.failover(self.sess) + self.assertIsNotNone(res) + + url = 'os-services/failover_host' + body = {'host': 'devstack'} + self.sess.put.assert_called_with( + url, + json=body, + microversion=self.sess.default_microversion, + ) From 4b63e1729cc99ac661b280ee4594ad5c76a80edc Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 7 Mar 2025 10:25:45 +0000 Subject: [PATCH 3671/3836] docs: Sort proxy docs alphabetically Change-Id: I3ef165ce97f68fae4f4c1a54c194a45329b51a2f Signed-off-by: Stephen Finucane --- doc/source/user/proxies/block_storage_v2.rst | 62 ++++----- doc/source/user/proxies/block_storage_v3.rst | 134 +++++++++---------- 2 files changed, 98 insertions(+), 98 deletions(-) diff --git a/doc/source/user/proxies/block_storage_v2.rst b/doc/source/user/proxies/block_storage_v2.rst index d3438876a..3f26fc80b 100644 --- a/doc/source/user/proxies/block_storage_v2.rst +++ b/doc/source/user/proxies/block_storage_v2.rst @@ -12,19 +12,6 @@ The block_storage high-level interface is available through the ``block_storage`` member of a :class:`~openstack.connection.Connection` object. The ``block_storage`` member will only be added if the service is detected. -Volume Operations -^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.block_storage.v2._proxy.Proxy - :noindex: - :members: create_volume, delete_volume, get_volume, - find_volume, volumes, get_volume_metadata, set_volume_metadata, - delete_volume_metadata, extend_volume, - retype_volume, set_volume_bootable_status, reset_volume_status, - set_volume_image_metadata, delete_volume_image_metadata, - attach_volume, detach_volume, - unmanage_volume, migrate_volume, complete_volume_migration - Backup Operations ^^^^^^^^^^^^^^^^^ @@ -46,12 +33,28 @@ Limits Operations :noindex: :members: get_limits -Type Operations -^^^^^^^^^^^^^^^ +QuotaClassSet Operations +^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.block_storage.v2._proxy.Proxy :noindex: - :members: create_type, delete_type, get_type, types + :members: get_quota_class_set, update_quota_class_set + +QuotaSet Operations +^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v2._proxy.Proxy + :noindex: + :members: get_quota_set, get_quota_set_defaults, + revert_quota_set, update_quota_set + +Service Operations +^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v2._proxy.Proxy + :noindex: + :members: find_service, services, enable_service, disable_service, + thaw_service, freeze_service, failover_service Snapshot Operations ^^^^^^^^^^^^^^^^^^^ @@ -67,28 +70,25 @@ Stats Operations :noindex: :members: backend_pools -QuotaClassSet Operations -^^^^^^^^^^^^^^^^^^^^^^^^ +Type Operations +^^^^^^^^^^^^^^^ .. autoclass:: openstack.block_storage.v2._proxy.Proxy :noindex: - :members: get_quota_class_set, update_quota_class_set + :members: create_type, delete_type, get_type, types -QuotaSet Operations -^^^^^^^^^^^^^^^^^^^ +Volume Operations +^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.block_storage.v2._proxy.Proxy :noindex: - :members: get_quota_set, get_quota_set_defaults, - revert_quota_set, update_quota_set - -Service Operations -^^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.block_storage.v2._proxy.Proxy - :noindex: - :members: find_service, services, enable_service, disable_service, - thaw_service, freeze_service, failover_service + :members: create_volume, delete_volume, get_volume, + find_volume, volumes, get_volume_metadata, set_volume_metadata, + delete_volume_metadata, extend_volume, + retype_volume, set_volume_bootable_status, reset_volume_status, + set_volume_image_metadata, delete_volume_image_metadata, + attach_volume, detach_volume, + unmanage_volume, migrate_volume, complete_volume_migration Helpers ^^^^^^^ diff --git a/doc/source/user/proxies/block_storage_v3.rst b/doc/source/user/proxies/block_storage_v3.rst index 28711a8ff..21270bf34 100644 --- a/doc/source/user/proxies/block_storage_v3.rst +++ b/doc/source/user/proxies/block_storage_v3.rst @@ -12,22 +12,20 @@ The block_storage high-level interface is available through the ``block_storage`` member of a :class:`~openstack.connection.Connection` object. The ``block_storage`` member will only be added if the service is detected. -Volume Operations -^^^^^^^^^^^^^^^^^ +Attachments +^^^^^^^^^^^ .. autoclass:: openstack.block_storage.v3._proxy.Proxy :noindex: - :members: create_volume, delete_volume, update_volume, get_volume, - find_volume, volumes, get_volume_metadata, set_volume_metadata, - delete_volume_metadata, extend_volume, set_volume_readonly, - retype_volume, set_volume_bootable_status, reset_volume_status, - set_volume_image_metadata, delete_volume_image_metadata, - revert_volume_to_snapshot, attach_volume, detach_volume, - unmanage_volume, migrate_volume, complete_volume_migration, - upload_volume_to_image, reserve_volume, unreserve_volume, - begin_volume_detaching, abort_volume_detaching, - init_volume_attachment, terminate_volume_attachment, - manage_volume, + :members: create_attachment, get_attachment, attachments, + delete_attachment, update_attachment, complete_attachment + +Availability Zone Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v3._proxy.Proxy + :noindex: + :members: availability_zones Backend Pools Operations ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -42,28 +40,36 @@ Backup Operations .. autoclass:: openstack.block_storage.v3._proxy.Proxy :noindex: :members: create_backup, delete_backup, get_backup, find_backup, backups, - restore_backup, reset_backup, + restore_backup, reset_backup -Availability Zone Operations -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +BlockStorageSummary Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.block_storage.v3._proxy.Proxy :noindex: - :members: availability_zones + :members: summary -Limits Operations -^^^^^^^^^^^^^^^^^ +Capabilities Operations +^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.block_storage.v3._proxy.Proxy :noindex: - :members: get_limits + :members: get_capabilities -Capabilities Operations -^^^^^^^^^^^^^^^^^^^^^^^ +Default Volume Types +^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.block_storage.v3._proxy.Proxy :noindex: - :members: get_capabilities + :members: default_types, show_default_type, set_default_type, + unset_default_type + +Limits Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v3._proxy.Proxy + :noindex: + :members: get_limits Group Operations ^^^^^^^^^^^^^^^^ @@ -93,6 +99,21 @@ Group Type Operations update_group_type_group_specs_property, delete_group_type_group_specs_property +QuotaClassSet Operations +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v3._proxy.Proxy + :noindex: + :members: get_quota_class_set, update_quota_class_set + +QuotaSet Operations +^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v3._proxy.Proxy + :noindex: + :members: get_quota_set, get_quota_set_defaults, + revert_quota_set, update_quota_set + Service Operations ^^^^^^^^^^^^^^^^^^ @@ -102,17 +123,6 @@ Service Operations thaw_service, freeze_service, failover_service, get_service_log_levels, set_service_log_levels -Type Operations -^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.block_storage.v3._proxy.Proxy - :noindex: - :members: create_type, delete_type, update_type, get_type, find_type, types, - update_type_extra_specs, delete_type_extra_specs, get_type_access, - add_type_access, remove_type_access, get_type_encryption, - create_type_encryption, delete_type_encryption, - update_type_encryption - Snapshot Operations ^^^^^^^^^^^^^^^^^^^ @@ -130,35 +140,16 @@ Stats Operations :noindex: :members: backend_pools -QuotaClassSet Operations -^^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.block_storage.v3._proxy.Proxy - :noindex: - :members: get_quota_class_set, update_quota_class_set - -QuotaSet Operations -^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.block_storage.v3._proxy.Proxy - :noindex: - :members: get_quota_set, get_quota_set_defaults, - revert_quota_set, update_quota_set - -BlockStorageSummary Operations -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.block_storage.v3._proxy.Proxy - :noindex: - :members: summary - -Attachments -^^^^^^^^^^^ +Type Operations +^^^^^^^^^^^^^^^ .. autoclass:: openstack.block_storage.v3._proxy.Proxy :noindex: - :members: create_attachment, get_attachment, attachments, - delete_attachment, update_attachment, complete_attachment + :members: create_type, delete_type, update_type, get_type, find_type, types, + update_type_extra_specs, delete_type_extra_specs, get_type_access, + add_type_access, remove_type_access, get_type_encryption, + create_type_encryption, delete_type_encryption, + update_type_encryption Transfer Operations ^^^^^^^^^^^^^^^^^^^ @@ -168,17 +159,26 @@ Transfer Operations :members: create_transfer, delete_transfer, find_transfer, get_transfer, transfers, accept_transfer -Helpers -^^^^^^^ +Volume Operations +^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.block_storage.v3._proxy.Proxy :noindex: - :members: wait_for_status, wait_for_delete + :members: create_volume, delete_volume, update_volume, get_volume, + find_volume, volumes, get_volume_metadata, set_volume_metadata, + delete_volume_metadata, extend_volume, set_volume_readonly, + retype_volume, set_volume_bootable_status, reset_volume_status, + set_volume_image_metadata, delete_volume_image_metadata, + revert_volume_to_snapshot, attach_volume, detach_volume, + unmanage_volume, migrate_volume, complete_volume_migration, + upload_volume_to_image, reserve_volume, unreserve_volume, + begin_volume_detaching, abort_volume_detaching, + init_volume_attachment, terminate_volume_attachment, + manage_volume, -Default Volume Types -^^^^^^^^^^^^^^^^^^^^ +Helpers +^^^^^^^ .. autoclass:: openstack.block_storage.v3._proxy.Proxy :noindex: - :members: default_types, show_default_type, set_default_type, - unset_default_type + :members: wait_for_status, wait_for_delete From 0fd18edc4308fcf389e925121b3403e20d9ac43c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 7 Mar 2025 11:17:50 +0000 Subject: [PATCH 3672/3836] volume: Add support for cascaded deletions Change-Id: Idc546dbee9ef6da83d7f607659a681d95587fea8 Signed-off-by: Stephen Finucane --- openstack/block_storage/v2/_proxy.py | 21 +++++++++++++------ openstack/block_storage/v3/_proxy.py | 21 +++++++++++++------ .../tests/unit/block_storage/v2/test_proxy.py | 11 ++++++---- .../tests/unit/block_storage/v3/test_proxy.py | 11 ++++++---- openstack/tests/unit/cloud/test_volume.py | 10 +++++++-- 5 files changed, 52 insertions(+), 22 deletions(-) diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index c59e72289..a9d57da73 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -408,7 +408,9 @@ def create_volume(self, **attrs): """ return self._create(_volume.Volume, **attrs) - def delete_volume(self, volume, ignore_missing=True, force=False): + def delete_volume( + self, volume, ignore_missing=True, *, force=False, cascade=False + ): """Delete a volume :param volume: The value can be either the ID of a volume or a @@ -419,14 +421,21 @@ def delete_volume(self, volume, ignore_missing=True, force=False): exception will be set when attempting to delete a nonexistent volume. :param bool force: Whether to try forcing volume deletion. + :param bool cascade: Whether to remove any snapshots along with the + volume. :returns: ``None`` """ - if not force: - self._delete(_volume.Volume, volume, ignore_missing=ignore_missing) - else: - volume = self._get_resource(_volume.Volume, volume) - volume.force_delete(self) + volume = self._get_resource(_volume.Volume, volume) + try: + if not force: + volume.delete(self, params={'cascade': cascade}) + else: + volume.force_delete(self) + except exceptions.NotFoundException: + if ignore_missing: + return None + raise # ========== Volume actions ========== diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 427924d67..3eff3e4f9 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -742,7 +742,9 @@ def create_volume(self, **attrs): """ return self._create(_volume.Volume, **attrs) - def delete_volume(self, volume, ignore_missing=True, force=False): + def delete_volume( + self, volume, ignore_missing=True, *, force=False, cascade=False + ): """Delete a volume :param volume: The value can be either the ID of a volume or a @@ -753,14 +755,21 @@ def delete_volume(self, volume, ignore_missing=True, force=False): When set to ``True``, no exception will be set when attempting to delete a nonexistent volume. :param bool force: Whether to try forcing volume deletion. + :param bool cascade: Whether to remove any snapshots along with the + volume. :returns: ``None`` """ - if not force: - self._delete(_volume.Volume, volume, ignore_missing=ignore_missing) - else: - volume = self._get_resource(_volume.Volume, volume) - volume.force_delete(self) + volume = self._get_resource(_volume.Volume, volume) + try: + if not force: + volume.delete(self, params={'cascade': cascade}) + else: + volume.force_delete(self) + except exceptions.NotFoundException: + if ignore_missing: + return None + raise def update_volume(self, volume, **attrs): """Update a volume diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index bcd12d8c0..7ba1b153b 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -74,10 +74,13 @@ def test_volume_create_attrs(self): self.verify_create(self.proxy.create_volume, volume.Volume) def test_volume_delete(self): - self.verify_delete(self.proxy.delete_volume, volume.Volume, False) - - def test_volume_delete_ignore(self): - self.verify_delete(self.proxy.delete_volume, volume.Volume, True) + self._verify( + "openstack.block_storage.v2.volume.Volume.delete", + self.proxy.delete_volume, + method_args=["value"], + expected_args=[self.proxy], + expected_kwargs={"params": {"cascade": False}}, + ) def test_volume_delete_force(self): self._verify( diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index ef021ed82..0cff81c58 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -75,10 +75,13 @@ def test_volume_create_attrs(self): self.verify_create(self.proxy.create_volume, volume.Volume) def test_volume_delete(self): - self.verify_delete(self.proxy.delete_volume, volume.Volume, False) - - def test_volume_delete_ignore(self): - self.verify_delete(self.proxy.delete_volume, volume.Volume, True) + self._verify( + "openstack.block_storage.v3.volume.Volume.delete", + self.proxy.delete_volume, + method_args=["value"], + expected_args=[self.proxy], + expected_kwargs={"params": {"cascade": False}}, + ) def test_volume_delete_force(self): self._verify( diff --git a/openstack/tests/unit/cloud/test_volume.py b/openstack/tests/unit/cloud/test_volume.py index 0c56ef405..6a418a25e 100644 --- a/openstack/tests/unit/cloud/test_volume.py +++ b/openstack/tests/unit/cloud/test_volume.py @@ -460,7 +460,10 @@ def test_delete_volume_deletes(self): dict( method='DELETE', uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', volume.id] + 'volumev3', + 'public', + append=['volumes', volume.id], + qs_elements=['cascade=False'], ), ), dict( @@ -496,7 +499,10 @@ def test_delete_volume_gone_away(self): dict( method='DELETE', uri=self.get_mock_url( - 'volumev3', 'public', append=['volumes', volume.id] + 'volumev3', + 'public', + append=['volumes', volume.id], + qs_elements=['cascade=False'], ), status_code=404, ), From a33dde1935f700cf5e7791a902a96b20f2352617 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 7 Mar 2025 11:18:16 +0000 Subject: [PATCH 3673/3836] volume: Use force delete param when possible This is the modern way to do things. Change-Id: I60f32a6dae28418d5cb72eafb347a3301fde3e51 Signed-off-by: Stephen Finucane --- openstack/block_storage/v3/_proxy.py | 11 +++++-- .../tests/unit/block_storage/v3/test_proxy.py | 29 +++++++++++++++++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 3eff3e4f9..715f76ce2 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -761,11 +761,16 @@ def delete_volume( :returns: ``None`` """ volume = self._get_resource(_volume.Volume, volume) + + params = {'cascade': cascade} + if utils.supports_microversion(self, '3.23'): + params['force'] = force + try: - if not force: - volume.delete(self, params={'cascade': cascade}) - else: + if force and not utils.supports_microversion(self, '3.23'): volume.force_delete(self) + else: + volume.delete(self, params=params) except exceptions.NotFoundException: if ignore_missing: return None diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 0cff81c58..88ed3494b 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -74,7 +74,12 @@ def test_volumes_not_detailed(self): def test_volume_create_attrs(self): self.verify_create(self.proxy.create_volume, volume.Volume) - def test_volume_delete(self): + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=False, + ) + def test_volume_delete(self, mock_mv): self._verify( "openstack.block_storage.v3.volume.Volume.delete", self.proxy.delete_volume, @@ -83,7 +88,12 @@ def test_volume_delete(self): expected_kwargs={"params": {"cascade": False}}, ) - def test_volume_delete_force(self): + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=False, + ) + def test_volume_delete_force(self, mock_mv): self._verify( "openstack.block_storage.v3.volume.Volume.force_delete", self.proxy.delete_volume, @@ -92,6 +102,21 @@ def test_volume_delete_force(self): expected_args=[self.proxy], ) + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=True, + ) + def test_volume_delete_force_v323(self, mock_mv): + self._verify( + "openstack.block_storage.v3.volume.Volume.delete", + self.proxy.delete_volume, + method_args=["value"], + method_kwargs={"force": True}, + expected_args=[self.proxy], + expected_kwargs={"params": {"cascade": False, "force": True}}, + ) + def test_get_volume_metadata(self): self._verify( "openstack.block_storage.v3.volume.Volume.fetch_metadata", From c87a4b93f43c60729abc599511dd1bc7d21cb592 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 7 Mar 2025 14:23:45 +0000 Subject: [PATCH 3674/3836] Update master for stable/2025.1 Add file to the reno documentation build to show release notes for stable/2025.1. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/2025.1. Sem-Ver: feature Change-Id: Ib9a4bf593b70b42f84f404ae24a46f8fcbfa3463 --- releasenotes/source/2025.1.rst | 6 ++++++ releasenotes/source/index.rst | 1 + 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/2025.1.rst diff --git a/releasenotes/source/2025.1.rst b/releasenotes/source/2025.1.rst new file mode 100644 index 000000000..3add0e53a --- /dev/null +++ b/releasenotes/source/2025.1.rst @@ -0,0 +1,6 @@ +=========================== +2025.1 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2025.1 diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index efdec8e85..8326e0dbc 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ :maxdepth: 1 unreleased + 2025.1 2024.2 2024.1 2023.2 From 0f6deb2002245533205a0db9dcfac4d3e3c5047a Mon Sep 17 00:00:00 2001 From: Pavlo Shchelokovskyy Date: Tue, 11 Mar 2025 16:08:53 +0000 Subject: [PATCH 3675/3836] Fix token-based auth loading from config currently token-related options are parsed and mangled for backward compatibility twice. As a result, the documented way of using token auth in clouds.yaml (`cloud["auth"]["token"]`) does not work. Stop double-fixing those backward-compat options. Change-Id: I2c672d2b6fcab14f3a4b3f262941bfe9f936579f --- openstack/config/loader.py | 8 --- openstack/tests/unit/config/test_config.py | 74 ++++++++++++++++++++++ 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 8bd0e86f9..312840c5c 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -1188,14 +1188,6 @@ def _clean_up_after_ourselves(self, config, p_opt, winning_value): def magic_fixes(self, config): """Perform the set of magic argument fixups""" - # Infer token plugin if a token was given - if ( - ('auth' in config and 'token' in config['auth']) - or ('auth_token' in config and config['auth_token']) - or ('token' in config and config['token']) - ): - config.setdefault('token', config.pop('auth_token', None)) - # Infer passcode if it was given separately # This is generally absolutely impractical to require setting passcode # in the clouds.yaml diff --git a/openstack/tests/unit/config/test_config.py b/openstack/tests/unit/config/test_config.py index fccee9e06..ea20921ae 100644 --- a/openstack/tests/unit/config/test_config.py +++ b/openstack/tests/unit/config/test_config.py @@ -1591,3 +1591,77 @@ def test_single_default_interface(self): self.assertRaises( exceptions.ConfigException, c._fix_backwards_networks, cloud ) + + def test_token_auth(self): + c = config.OpenStackConfig( + config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] + ) + expected = { + "auth_type": "v3token", + "auth": { + "token": "my_token", + }, + "networks": [], + } + + cloud = { + "auth_type": "v3token", + "auth": { + "token": "my_token", + }, + } + result = c.magic_fixes(cloud) + self.assertEqual(expected, result) + + cloud = { + "auth_type": "v3token", + "auth": { + "auth_token": "my_token", + }, + } + result = c.magic_fixes(cloud) + self.assertEqual(expected, result) + + cloud = { + "auth_type": "v3token", + "auth": { + "auth-token": "my_token", + }, + } + result = c.magic_fixes(cloud) + self.assertEqual(expected, result) + + cloud = { + "auth_type": "v3token", + "auth": {}, + "token": "my_token", + } + result = c.magic_fixes(cloud) + self.assertEqual(expected, result) + + cloud = { + "auth_type": "v3token", + "auth": {}, + "auth_token": "my_token", + } + result = c.magic_fixes(cloud) + self.assertEqual(expected, result) + + cloud = { + "auth_type": "v3token", + "auth": {}, + "auth-token": "my_token", + } + result = c.magic_fixes(cloud) + self.assertEqual(expected, result) + + # test priority + cloud = { + "auth_type": "v3token", + "auth": { + "token": "I will be ignored", + }, + "token": "my_token", + } + result = c.magic_fixes(cloud) + self.assertEqual(expected, result) From 97958bbd403e86461c67087e3721eca54e5fe222 Mon Sep 17 00:00:00 2001 From: Pavlo Shchelokovskyy Date: Wed, 12 Mar 2025 12:37:32 +0000 Subject: [PATCH 3676/3836] Refactor and expand loader backward compat tests - do not test each private config mangling method separately, always test the whole `magic_fixes` method - add more tests for existing config manglings Change-Id: Ibf4508ef03bfa8dbb55685934150ba1ca5ff71d7 --- openstack/tests/unit/config/test_config.py | 169 +++++++++++++-------- 1 file changed, 109 insertions(+), 60 deletions(-) diff --git a/openstack/tests/unit/config/test_config.py b/openstack/tests/unit/config/test_config.py index ea20921ae..c734bf919 100644 --- a/openstack/tests/unit/config/test_config.py +++ b/openstack/tests/unit/config/test_config.py @@ -24,6 +24,7 @@ from openstack import config from openstack.config import cloud_region from openstack.config import defaults +from openstack.config import loader from openstack import exceptions from openstack.tests.unit.config import base @@ -1409,30 +1410,33 @@ def test_set_no_default(self): self.assertEqual('password', cc.auth_type) -class TestBackwardsCompatibility(base.TestCase): - def test_set_no_default(self): +class TestMagicFixes(base.TestCase): + def _test_magic_fixes(self, cloud, expected): c = config.OpenStackConfig( config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] ) + result = c.magic_fixes(cloud) + self.assertEqual(expected, result) + + def test_set_no_default(self): cloud = { + 'auth': {}, 'identity_endpoint_type': 'admin', 'compute_endpoint_type': 'private', 'endpoint_type': 'public', 'auth_type': 'v3password', } - result = c._fix_backwards_interface(cloud) expected = { 'identity_interface': 'admin', 'compute_interface': 'private', 'interface': 'public', 'auth_type': 'v3password', + 'auth': {}, + 'networks': [], } - self.assertDictEqual(expected, result) + self._test_magic_fixes(cloud, expected) def test_project_v2password(self): - c = config.OpenStackConfig( - config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] - ) cloud = { 'auth_type': 'v2password', 'auth': { @@ -1440,20 +1444,17 @@ def test_project_v2password(self): 'project-id': 'my_project_id', }, } - result = c._fix_backwards_project(cloud) expected = { 'auth_type': 'v2password', 'auth': { 'tenant_name': 'my_project_name', 'tenant_id': 'my_project_id', }, + 'networks': [], } - self.assertEqual(expected, result) + self._test_magic_fixes(cloud, expected) def test_project_password(self): - c = config.OpenStackConfig( - config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] - ) cloud = { 'auth_type': 'password', 'auth': { @@ -1461,15 +1462,15 @@ def test_project_password(self): 'project-id': 'my_project_id', }, } - result = c._fix_backwards_project(cloud) expected = { 'auth_type': 'password', 'auth': { 'project_name': 'my_project_name', 'project_id': 'my_project_id', }, + 'networks': [], } - self.assertEqual(expected, result) + self._test_magic_fixes(cloud, expected) def test_project_conflict_priority(self): """The order of priority should be @@ -1480,23 +1481,20 @@ def test_project_conflict_priority(self): inherited credentials in clouds.yaml. """ - c = config.OpenStackConfig( - config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] - ) cloud = { 'auth_type': 'password', 'auth': { 'project_id': 'my_project_id', }, } - result = c._fix_backwards_project(cloud) expected = { 'auth_type': 'password', 'auth': { 'project_id': 'my_project_id', }, + 'networks': [], } - self.assertEqual(expected, result) + self._test_magic_fixes(cloud, expected) cloud = { 'auth_type': 'password', @@ -1505,39 +1503,36 @@ def test_project_conflict_priority(self): }, 'project_id': 'different_project_id', } - result = c._fix_backwards_project(cloud) expected = { 'auth_type': 'password', 'auth': { 'project_id': 'different_project_id', }, + 'networks': [], } - self.assertEqual(expected, result) + self._test_magic_fixes(cloud, expected) def test_backwards_network_fail(self): - c = config.OpenStackConfig( - config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] - ) cloud = { + 'auth': {}, 'external_network': 'public', 'networks': [ {'name': 'private', 'routes_externally': False}, ], } self.assertRaises( - exceptions.ConfigException, c._fix_backwards_networks, cloud + exceptions.ConfigException, self._test_magic_fixes, cloud, {} ) def test_backwards_network(self): - c = config.OpenStackConfig( - config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] - ) cloud = { + 'auth': {}, 'external_network': 'public', 'internal_network': 'private', } - result = c._fix_backwards_networks(cloud) expected = { + 'auth': {}, + 'auth_type': None, 'external_network': 'public', 'internal_network': 'private', 'networks': [ @@ -1555,15 +1550,13 @@ def test_backwards_network(self): }, ], } - self.assertEqual(expected, result) + self._test_magic_fixes(cloud, expected) def test_normalize_network(self): - c = config.OpenStackConfig( - config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] - ) - cloud = {'networks': [{'name': 'private'}]} - result = c._fix_backwards_networks(cloud) + cloud = {'auth': {}, 'networks': [{'name': 'private'}]} expected = { + 'auth': {}, + 'auth_type': None, 'networks': [ { 'name': 'private', @@ -1574,34 +1567,29 @@ def test_normalize_network(self): 'routes_ipv4_externally': False, 'routes_ipv6_externally': False, }, - ] + ], } - self.assertEqual(expected, result) + self._test_magic_fixes(cloud, expected) def test_single_default_interface(self): - c = config.OpenStackConfig( - config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] - ) cloud = { + 'auth': {}, 'networks': [ {'name': 'blue', 'default_interface': True}, {'name': 'purple', 'default_interface': True}, - ] + ], } self.assertRaises( - exceptions.ConfigException, c._fix_backwards_networks, cloud + exceptions.ConfigException, self._test_magic_fixes, cloud, {} ) def test_token_auth(self): - c = config.OpenStackConfig( - config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml] - ) expected = { "auth_type": "v3token", "auth": { "token": "my_token", }, - "networks": [], + 'networks': [], } cloud = { @@ -1610,8 +1598,7 @@ def test_token_auth(self): "token": "my_token", }, } - result = c.magic_fixes(cloud) - self.assertEqual(expected, result) + self._test_magic_fixes(cloud, expected) cloud = { "auth_type": "v3token", @@ -1619,8 +1606,7 @@ def test_token_auth(self): "auth_token": "my_token", }, } - result = c.magic_fixes(cloud) - self.assertEqual(expected, result) + self._test_magic_fixes(cloud, expected) cloud = { "auth_type": "v3token", @@ -1628,32 +1614,28 @@ def test_token_auth(self): "auth-token": "my_token", }, } - result = c.magic_fixes(cloud) - self.assertEqual(expected, result) + self._test_magic_fixes(cloud, expected) cloud = { "auth_type": "v3token", "auth": {}, "token": "my_token", } - result = c.magic_fixes(cloud) - self.assertEqual(expected, result) + self._test_magic_fixes(cloud, expected) cloud = { "auth_type": "v3token", "auth": {}, "auth_token": "my_token", } - result = c.magic_fixes(cloud) - self.assertEqual(expected, result) + self._test_magic_fixes(cloud, expected) cloud = { "auth_type": "v3token", "auth": {}, "auth-token": "my_token", } - result = c.magic_fixes(cloud) - self.assertEqual(expected, result) + self._test_magic_fixes(cloud, expected) # test priority cloud = { @@ -1663,5 +1645,72 @@ def test_token_auth(self): }, "token": "my_token", } - result = c.magic_fixes(cloud) - self.assertEqual(expected, result) + self._test_magic_fixes(cloud, expected) + + def test_passcode(self): + cloud = { + "auth": {}, + "passcode": "totp", + } + expected = { + "auth": { + "passcode": "totp", + }, + 'auth_type': None, + 'networks': [], + } + self._test_magic_fixes(cloud, expected) + + def test_endpoint_type_to_interface(self): + cloud = { + 'auth': {}, + "endpoint_type": "public", + "foo_endpoint_type": "internal", + } + expected = { + "auth": {}, + 'auth_type': None, + 'networks': [], + "interface": "public", + "foo_interface": "internal", + } + self._test_magic_fixes(cloud, expected) + + def test_bool_keys(self): + cloud = {"auth": {}} + cloud.update({k: "True" for k in loader.BOOL_KEYS}) + expected = { + "auth_type": None, + "auth": {}, + "networks": [], + } + expected.update({k: True for k in loader.BOOL_KEYS}) + self._test_magic_fixes(cloud, expected) + + def test_csv_keys(self): + cloud = {"auth": {}} + cloud.update({k: "spam,ham" for k in loader.CSV_KEYS}) + expected = { + "auth_type": None, + "auth": {}, + "networks": [], + } + expected.update({k: ["spam", "ham"] for k in loader.CSV_KEYS}) + self._test_magic_fixes(cloud, expected) + + def test_auth_url_formatting(self): + cloud = { + "auth": { + "auth_url": "https://my.cloud/{region_id}", + }, + "region_id": "RegionOne", + } + expected = { + "auth_type": None, + "auth": { + "auth_url": "https://my.cloud/RegionOne", + }, + "region_id": "RegionOne", + "networks": [], + } + self._test_magic_fixes(cloud, expected) From f25c47e0ba30ced0cb9b8e6f841b394796b799f7 Mon Sep 17 00:00:00 2001 From: Pavlo Shchelokovskyy Date: Fri, 14 Mar 2025 18:59:44 +0200 Subject: [PATCH 3677/3836] Fix argparse integration fix docstrings and pass correct arguments around. Change-Id: I2b029b6b93853744e1a6d29f2473ae15980462a9 Closes-Bug: #2102667 --- openstack/__init__.py | 9 ++++----- openstack/config/__init__.py | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/openstack/__init__.py b/openstack/__init__.py index 792305e5a..a32487112 100644 --- a/openstack/__init__.py +++ b/openstack/__init__.py @@ -67,7 +67,7 @@ def connect( cloud: ty.Optional[str] = None, app_name: ty.Optional[str] = None, app_version: ty.Optional[str] = None, - options: ty.Optional[argparse.Namespace] = None, + options: ty.Optional[argparse.ArgumentParser] = None, load_yaml_config: bool = True, load_envvars: bool = True, **kwargs: ty.Any, @@ -78,10 +78,9 @@ def connect( The name of the configuration to load from clouds.yaml. Defaults to 'envvars' which will load configuration settings from environment variables that start with ``OS_``. - :param argparse.Namespace options: - An argparse Namespace object. Allows direct passing in of - argparse options to be added to the cloud config. Values - of None and '' will be removed. + :param argparse.ArgumentParser options: + An argparse ArgumentParser object. SDK-specific options will be + registered, parsed out and used to configure the connection. :param bool load_yaml_config: Whether or not to load config settings from clouds.yaml files. Defaults to True. diff --git a/openstack/config/__init__.py b/openstack/config/__init__.py index 3e118b719..75b623db4 100644 --- a/openstack/config/__init__.py +++ b/openstack/config/__init__.py @@ -34,8 +34,8 @@ def get_cloud_region( ) if options: config.register_argparse_arguments(options, sys.argv, service_key) - parsed_options = options.parse_known_args(sys.argv) + parsed_options, _rest_of_argv = options.parse_known_args(sys.argv) else: parsed_options = None - return config.get_one(options=parsed_options, **kwargs) + return config.get_one(argparse=parsed_options, **kwargs) From cf84ae1d71847cdbd56a1b58aad0e38fba9f748d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 19 Mar 2025 11:30:24 +0000 Subject: [PATCH 3678/3836] Indicate providers that are dead/unresponsive - betacloud - fuga - ibmcloud - internap - ultimum - unitedstack We can remove these in a future release. Change-Id: I4618df06d31f25efa6e373920cd9566bdba5c64e Signed-off-by: Stephen Finucane --- openstack/config/vendors/__init__.py | 7 +++++++ openstack/config/vendors/betacloud.json | 4 +++- openstack/config/vendors/fuga.json | 4 +++- openstack/config/vendors/ibmcloud.json | 4 +++- openstack/config/vendors/internap.json | 4 +++- openstack/config/vendors/ultimum.json | 4 +++- openstack/config/vendors/unitedstack.json | 4 +++- 7 files changed, 25 insertions(+), 6 deletions(-) diff --git a/openstack/config/vendors/__init__.py b/openstack/config/vendors/__init__.py index 89bdd7bcf..06e740fa6 100644 --- a/openstack/config/vendors/__init__.py +++ b/openstack/config/vendors/__init__.py @@ -30,15 +30,18 @@ def _get_vendor_defaults(): global _VENDOR_DEFAULTS + if not _VENDOR_DEFAULTS: for vendor in glob.glob(os.path.join(_VENDORS_PATH, '*.yaml')): with open(vendor) as f: vendor_data = yaml.safe_load(f) _VENDOR_DEFAULTS[vendor_data['name']] = vendor_data['profile'] + for vendor in glob.glob(os.path.join(_VENDORS_PATH, '*.json')): with open(vendor) as f: vendor_data = json.load(f) _VENDOR_DEFAULTS[vendor_data['name']] = vendor_data['profile'] + return _VENDOR_DEFAULTS @@ -46,10 +49,12 @@ def get_profile(profile_name): vendor_defaults = _get_vendor_defaults() if profile_name in vendor_defaults: return vendor_defaults[profile_name].copy() + profile_url = urllib.parse.urlparse(profile_name) if not profile_url.netloc: # This isn't a url, and we already don't have it. return + well_known_url = _WELL_KNOWN_PATH.format( scheme=profile_url.scheme, netloc=profile_url.netloc, @@ -62,6 +67,7 @@ def get_profile(profile_name): ) vendor_defaults[profile_name] = None return + vendor_data = response.json() name = vendor_data['name'] # Merge named and url cloud config, but make named config override the @@ -77,4 +83,5 @@ def get_profile(profile_name): # how we're called. vendor_defaults[profile_name] = profile vendor_defaults[name] = profile + return profile diff --git a/openstack/config/vendors/betacloud.json b/openstack/config/vendors/betacloud.json index 87aeeeda5..a254abf39 100644 --- a/openstack/config/vendors/betacloud.json +++ b/openstack/config/vendors/betacloud.json @@ -9,6 +9,8 @@ ], "identity_api_version": "3", "image_format": "raw", - "block_storage_api_version": "3" + "block_storage_api_version": "3", + "status": "shutdown", + "message": "betacloud.de has ceased business" } } diff --git a/openstack/config/vendors/fuga.json b/openstack/config/vendors/fuga.json index c73198e7f..8f2da8421 100644 --- a/openstack/config/vendors/fuga.json +++ b/openstack/config/vendors/fuga.json @@ -10,6 +10,8 @@ "cystack" ], "identity_api_version": "3", - "block_storage_api_version": "3" + "block_storage_api_version": "3", + "status": "deprecated", + "message": "the API Endpoint is no longer responsive" } } diff --git a/openstack/config/vendors/ibmcloud.json b/openstack/config/vendors/ibmcloud.json index dc0a18220..a0950dc2a 100644 --- a/openstack/config/vendors/ibmcloud.json +++ b/openstack/config/vendors/ibmcloud.json @@ -8,6 +8,8 @@ "identity_api_version": "3", "regions": [ "london" - ] + ], + "status": "shutdown", + "message": "the API Endpoint is no longer responsive" } } diff --git a/openstack/config/vendors/internap.json b/openstack/config/vendors/internap.json index 03ce2418d..55a0b3ad6 100644 --- a/openstack/config/vendors/internap.json +++ b/openstack/config/vendors/internap.json @@ -12,6 +12,8 @@ "sjc01" ], "identity_api_version": "3", - "floating_ip_source": "None" + "floating_ip_source": "None", + "status": "shutdown", + "message": "Internap have been rebranded as HorizonIQ" } } diff --git a/openstack/config/vendors/ultimum.json b/openstack/config/vendors/ultimum.json index c24f4b530..83128a289 100644 --- a/openstack/config/vendors/ultimum.json +++ b/openstack/config/vendors/ultimum.json @@ -6,6 +6,8 @@ }, "identity_api_version": "3", "block_storage_api_version": "1", - "region-name": "RegionOne" + "region-name": "RegionOne", + "status": "shutdown", + "message": "ultimum-cloud.com has ceased business" } } diff --git a/openstack/config/vendors/unitedstack.json b/openstack/config/vendors/unitedstack.json index 319707d7b..b7ee678fc 100644 --- a/openstack/config/vendors/unitedstack.json +++ b/openstack/config/vendors/unitedstack.json @@ -11,6 +11,8 @@ "block_storage_api_version": "1", "identity_api_version": "3", "image_format": "raw", - "floating_ip_source": "None" + "floating_ip_source": "None", + "status": "shutdown", + "message": "the API Endpoint is no longer responsive" } } From ef9b958d23a4b834935ce12e6306ad58fde83a0e Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Fri, 21 Mar 2025 09:00:33 +0000 Subject: [PATCH 3679/3836] Define the correct QoS rules deletion method This patch fixes the teardown method used to delete the created QoS policy in the functional test class. Trivial-Fix Change-Id: Icc852c0e4916fe867ffa4591e107d9916f020446 --- .../functional/network/v2/test_qos_bandwidth_limit_rule.py | 2 +- .../tests/functional/network/v2/test_qos_dscp_marking_rule.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py b/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py index 47037062c..58a258fb3 100644 --- a/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py +++ b/openstack/tests/functional/network/v2/test_qos_bandwidth_limit_rule.py @@ -63,7 +63,7 @@ def setUp(self): self.RULE_ID = qos_rule.id def tearDown(self): - rule = self.operator_cloud.network.delete_qos_minimum_bandwidth_rule( + rule = self.operator_cloud.network.delete_qos_bandwidth_limit_rule( self.RULE_ID, self.QOS_POLICY_ID ) qos_policy = self.operator_cloud.network.delete_qos_policy( diff --git a/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py b/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py index 7849c3020..fcda4ccb9 100644 --- a/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py +++ b/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py @@ -51,7 +51,7 @@ def setUp(self): self.RULE_ID = qos_rule.id def tearDown(self): - rule = self.conn.network.delete_qos_minimum_bandwidth_rule( + rule = self.conn.network.delete_qos_dscp_marking_rule( self.RULE_ID, self.QOS_POLICY_ID ) qos_policy = self.conn.network.delete_qos_policy(self.QOS_POLICY_ID) From d31db239f526ab40410c7c6aadf044e0a24f87f3 Mon Sep 17 00:00:00 2001 From: Pavlo Shchelokovskyy Date: Wed, 12 Mar 2025 14:42:33 +0000 Subject: [PATCH 3680/3836] Refactor loader magic fixes de-duplicate and extract some methods Change-Id: Ia224035bbb7ae58cbd6e995aa17690344a57cee6 --- openstack/config/loader.py | 46 ++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 312840c5c..d55c01faf 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -750,10 +750,7 @@ def _handle_domain_id(self, cloud): cloud['auth'].pop(target_key, None) return cloud - def _fix_backwards_project(self, cloud): - # Do the lists backwards so that project_name is the ultimate winner - # Also handle moving domain names into auth so that domain mapping - # is easier + def _fix_backwards_auth(self, cloud): mappings = { 'domain_id': ('domain_id', 'domain-id'), 'domain_name': ('domain_name', 'domain-name'), @@ -765,6 +762,7 @@ def _fix_backwards_project(self, cloud): 'project-domain-name', ), 'token': ('auth-token', 'auth_token', 'token'), + 'passcode': ('passcode',), } if cloud.get('auth_type', None) == 'v2password': # If v2password is explcitly requested, this is to deal with old @@ -798,12 +796,12 @@ def _fix_backwards_project(self, cloud): for target_key, possible_values in mappings.items(): target = None for key in possible_values: - # Prefer values from the 'auth' section - # as they may contain cli or environment overrides. - # See story 2010784 for context. if key in cloud['auth']: target = str(cloud['auth'][key]) del cloud['auth'][key] + # Prefer values NOT from the 'auth' section + # as they may contain cli or environment overrides. + # See story 2010784 for context. if key in cloud: target = str(cloud[key]) del cloud[key] @@ -1185,36 +1183,30 @@ def _clean_up_after_ourselves(self, config, p_opt, winning_value): config['auth'][p_opt.dest] = winning_value return config + def _handle_value_types(self, config: dict) -> dict: + for key in BOOL_KEYS: + if key in config: + if not isinstance(config[key], bool): + config[key] = get_boolean(config[key]) + + for key in CSV_KEYS: + if key in config: + if isinstance(config[key], str): + config[key] = config[key].split(',') + return config + def magic_fixes(self, config): """Perform the set of magic argument fixups""" - # Infer passcode if it was given separately - # This is generally absolutely impractical to require setting passcode - # in the clouds.yaml - if 'auth' in config and 'passcode' in config: - config['auth']['passcode'] = config.pop('passcode', None) - # These backwards compat values are only set via argparse. If it's # there, it's because it was passed in explicitly, and should win config = self._fix_backwards_api_timeout(config) - if 'endpoint_type' in config: - config['interface'] = config.pop('endpoint_type') - config = self._fix_backwards_auth_plugin(config) - config = self._fix_backwards_project(config) + config = self._fix_backwards_auth(config) config = self._fix_backwards_interface(config) config = self._fix_backwards_networks(config) config = self._handle_domain_id(config) - - for key in BOOL_KEYS: - if key in config: - if type(config[key]) is not bool: - config[key] = get_boolean(config[key]) - - for key in CSV_KEYS: - if key in config: - if isinstance(config[key], str): - config[key] = config[key].split(',') + config = self._handle_value_types(config) # TODO(mordred): Special casing auth_url here. We should # come back to this betterer later so that it's From 55d0c692d27456ed79fba0df0215f3a7e4293615 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 24 Mar 2025 11:51:17 +0000 Subject: [PATCH 3681/3836] zuul: Replace removed jobs The "metalsmith-integration-glance-netboot-cirros-direct" job was removed over 3 years ago. Use its replacement. Change-Id: I76c50f9648669b81fd57539574f518879dbb5805 Signed-off-by: Stephen Finucane --- zuul.d/metal-jobs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zuul.d/metal-jobs.yaml b/zuul.d/metal-jobs.yaml index a9da32274..22a837612 100644 --- a/zuul.d/metal-jobs.yaml +++ b/zuul.d/metal-jobs.yaml @@ -3,7 +3,7 @@ # out of general entry. - job: name: metalsmith-integration-openstacksdk-src - parent: metalsmith-integration-glance-netboot-cirros-direct + parent: metalsmith-integration-http-cirros required-projects: - openstack/openstacksdk From cbf29120bdf4a62df058d8558baeb9414687c856 Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Mon, 17 Mar 2025 16:06:22 +0000 Subject: [PATCH 3682/3836] Add QoS packet rate limit rule object and CRUD operations Related-Bug: #2099747 Change-Id: I061772cc7fd45ba137ceab7a2f90c3dce15d5c9d --- doc/source/user/proxies/network.rst | 7 +- .../network/v2/qos_packet_rate_limit_rule.rst | 13 ++ openstack/network/v2/_proxy.py | 145 ++++++++++++++++++ .../network/v2/qos_packet_rate_limit_rule.py | 39 +++++ .../v2/test_qos_packet_rate_limit_rule.py | 115 ++++++++++++++ openstack/tests/unit/network/v2/test_proxy.py | 77 ++++++++++ .../v2/test_qos_packet_rate_limit_rule.py | 49 ++++++ ...cket-rate-limit-rule-385945e2e831ab0d.yaml | 5 + 8 files changed, 449 insertions(+), 1 deletion(-) create mode 100644 doc/source/user/resources/network/v2/qos_packet_rate_limit_rule.rst create mode 100644 openstack/network/v2/qos_packet_rate_limit_rule.py create mode 100644 openstack/tests/functional/network/v2/test_qos_packet_rate_limit_rule.py create mode 100644 openstack/tests/unit/network/v2/test_qos_packet_rate_limit_rule.py create mode 100644 releasenotes/notes/qos-packet-rate-limit-rule-385945e2e831ab0d.yaml diff --git a/doc/source/user/proxies/network.rst b/doc/source/user/proxies/network.rst index 4e5ef4dfb..0cd04b220 100644 --- a/doc/source/user/proxies/network.rst +++ b/doc/source/user/proxies/network.rst @@ -152,7 +152,12 @@ QoS Operations qos_bandwidth_limit_rules, create_qos_dscp_marking_rule, update_qos_dscp_marking_rule, delete_qos_dscp_marking_rule, get_qos_dscp_marking_rule, - find_qos_dscp_marking_rule, qos_dscp_marking_rules + find_qos_dscp_marking_rule, qos_dscp_marking_rules, + create_qos_packet_rate_limit_rule, + update_qos_packet_rate_limit_rule, + delete_qos_packet_rate_limit_rule, + get_qos_packet_rate_limit_rule, + find_qos_packet_rate_limit_rule, Agent Operations ^^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/network/v2/qos_packet_rate_limit_rule.rst b/doc/source/user/resources/network/v2/qos_packet_rate_limit_rule.rst new file mode 100644 index 000000000..a4b527ca3 --- /dev/null +++ b/doc/source/user/resources/network/v2/qos_packet_rate_limit_rule.rst @@ -0,0 +1,13 @@ +openstack.network.v2.qos_packet_rate_limit_rule +=============================================== + +.. automodule:: openstack.network.v2.qos_packet_rate_limit_rule + +The QoSPacketRateLimitRule Class +---------------------------------- + +The ``QoSPacketRateLimitRule`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.network.v2.qos_packet_rate_limit_rule.QoSPacketRateLimitRule + :members: diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index aa3c5f338..f46df3c4c 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -73,6 +73,9 @@ from openstack.network.v2 import ( qos_minimum_packet_rate_rule as _qos_minimum_packet_rate_rule, ) +from openstack.network.v2 import ( + qos_packet_rate_limit_rule as _qos_packet_rate_limit_rule, +) from openstack.network.v2 import qos_policy as _qos_policy from openstack.network.v2 import qos_rule_type as _qos_rule_type from openstack.network.v2 import quota as _quota @@ -3702,6 +3705,148 @@ def update_qos_minimum_packet_rate_rule( **attrs, ) + def create_qos_packet_rate_limit_rule(self, qos_policy, **attrs): + """Create a new packet rate limit rule + + :param attrs: Keyword arguments which will be used to create a + :class:`~openstack.network.v2.qos_packet_rate_limit_rule.QoSPacketRateLimitRule`, + comprised of the properties on the QoSPacketRateLimitRule class. + :param qos_policy: The value can be the ID of the QoS policy that the + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. + + :returns: The results of resource creation + :rtype: + :class:`~openstack.network.v2.qos_packet_rate_limit_rule.QoSPacketRateLimitRule` + """ + policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) + return self._create( + _qos_packet_rate_limit_rule.QoSPacketRateLimitRule, + qos_policy_id=policy.id, + **attrs, + ) + + def delete_qos_packet_rate_limit_rule( + self, qos_rule, qos_policy, ignore_missing=True + ): + """Delete a packet rate limit rule + + :param qos_rule: The value can be either the ID of a packet rate limit + rule or a + :class:`~openstack.network.v2.qos_packet_rate_limit_rule.QoSPacketRateLimitRule` + instance. + :param qos_policy: The value can be the ID of the QoS policy that the + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.NotFoundException` will be raised when + the resource does not exist. When set to ``True``, no exception + will be set when attempting to delete a nonexistent minimum packet + rate rule. + + :returns: ``None`` + """ + policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) + self._delete( + _qos_packet_rate_limit_rule.QoSPacketRateLimitRule, + qos_rule, + ignore_missing=ignore_missing, + qos_policy_id=policy.id, + ) + + def find_qos_packet_rate_limit_rule( + self, qos_rule_id, qos_policy, ignore_missing=True, **query + ): + """Find a packet rate limit rule + + :param qos_rule_id: The ID of a packet rate limit rule. + :param qos_policy: The value can be the ID of the QoS policy that the + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.NotFoundException` will be raised when + the resource does not exist. When set to ``True``, None will be + returned when attempting to find a nonexistent resource. + :param dict query: Any additional parameters to be passed into + underlying methods. such as query filters. + :returns: One + :class:`~openstack.network.v2.qos_packet_rate_limit_rule.QoSPacketRateLimitRule` + or None + """ + policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) + return self._find( + _qos_packet_rate_limit_rule.QoSPacketRateLimitRule, + qos_rule_id, + ignore_missing=ignore_missing, + qos_policy_id=policy.id, + **query, + ) + + def get_qos_packet_rate_limit_rule(self, qos_rule, qos_policy): + """Get a single packet rate limit rule + + :param qos_rule: The value can be the ID of a packet rate limit rule + or a + :class:`~openstack.network.v2.qos_packet_rate_limit_rule.QoSPacketRateLimitRule` + instance. + :param qos_policy: The value can be the ID of the QoS policy that the + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. + :returns: One + :class:`~openstack.network.v2.qos_packet_rate_limit_rule.QoSPacketRateLimitRule` + :raises: :class:`~openstack.exceptions.NotFoundException` when no + resource can be found. + """ + policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) + return self._get( + _qos_packet_rate_limit_rule.QoSPacketRateLimitRule, + qos_rule, + qos_policy_id=policy.id, + ) + + def qos_packet_rate_limit_rules(self, qos_policy, **query): + """Return a generator of packet rate limit rules + + :param qos_policy: The value can be the ID of the QoS policy that the + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. + :param kwargs query: Optional query parameters to be sent to limit the + resources being returned. + :returns: A generator of minimum packet rate rule objects + :rtype: + :class:`~openstack.network.v2.qos_packet_rate_limit_rule.QoSPacketRateLimitRule` + """ + policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) + return self._list( + _qos_packet_rate_limit_rule.QoSPacketRateLimitRule, + qos_policy_id=policy.id, + **query, + ) + + def update_qos_packet_rate_limit_rule(self, qos_rule, qos_policy, **attrs): + """Update a minimum packet rate rule + + :param qos_rule: Either the id of a minimum packet rate rule or a + :class:`~openstack.network.v2.qos_packet_rate_limit_rule.QoSPacketRateLimitRule` + instance. + :param qos_policy: The value can be the ID of the QoS policy that the + rule belongs or a + :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. + :param attrs: The attributes to update on the minimum packet rate rule + represented by ``qos_rule``. + + :returns: The updated minimum packet rate rule + :rtype: + :class:`~openstack.network.v2.qos_packet_rate_limit_rule.QoSPacketRateLimitRule` + """ + policy = self._get_resource(_qos_policy.QoSPolicy, qos_policy) + return self._update( + _qos_packet_rate_limit_rule.QoSPacketRateLimitRule, + qos_rule, + qos_policy_id=policy.id, + **attrs, + ) + def create_qos_policy(self, **attrs): """Create a new QoS policy from attributes diff --git a/openstack/network/v2/qos_packet_rate_limit_rule.py b/openstack/network/v2/qos_packet_rate_limit_rule.py new file mode 100644 index 000000000..7bbad38ee --- /dev/null +++ b/openstack/network/v2/qos_packet_rate_limit_rule.py @@ -0,0 +1,39 @@ +# 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 resource + + +class QoSPacketRateLimitRule(resource.Resource): + resource_key = 'packet_rate_limit_rule' + resources_key = resource_key + 's' + base_path = '/qos/policies/%(qos_policy_id)s/' + resources_key + + _allow_unknown_attrs_in_body = True + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + # Properties + #: The ID of the QoS policy who owns rule. + qos_policy_id = resource.URI('qos_policy_id') + #: Maximum packet rare in kpps. + max_kpps = resource.Body('max_kpps') + #: Maximum burst packet rate in kpps. + max_burst_kpps = resource.Body('max_burst_kpps') + #: Traffic direction from the tenant point of view ('egress', 'ingress', + # 'any'). + direction = resource.Body('direction') diff --git a/openstack/tests/functional/network/v2/test_qos_packet_rate_limit_rule.py b/openstack/tests/functional/network/v2/test_qos_packet_rate_limit_rule.py new file mode 100644 index 000000000..9071d8b81 --- /dev/null +++ b/openstack/tests/functional/network/v2/test_qos_packet_rate_limit_rule.py @@ -0,0 +1,115 @@ +# 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.network.v2 import ( + qos_packet_rate_limit_rule as _qos_packet_rate_limit_rule, +) +from openstack.tests.functional import base + + +class TestQoSPacketRateLimitRule(base.BaseFunctionalTest): + QOS_POLICY_ID = None + QOS_IS_SHARED = False + QOS_POLICY_DESCRIPTION = 'QoS policy description' + RULE_MAX_KPPS = 1500 + RULE_MAX_KPPS_NEW = 1800 + RULE_MAX_BURST_KPPS = 1100 + RULE_MAX_BURST_KPPS_NEW = 1300 + RULE_DIRECTION = 'egress' + RULE_DIRECTION_NEW = 'ingress' + RULE_DIRECTION_NEW_2 = 'any' + + def setUp(self): + super().setUp() + + if not self.operator_cloud: + self.skipTest('Operator cloud required for this test') + + # Skip the tests if qos-bw-limit-direction extension is not enabled. + if not self.operator_cloud.network.find_extension('qos-pps'): + self.skipTest("Network qos-pps extension disabled") + + self.QOS_POLICY_NAME = self.getUniqueString() + self.RULE_ID = self.getUniqueString() + qos_policy = self.operator_cloud.network.create_qos_policy( + description=self.QOS_POLICY_DESCRIPTION, + name=self.QOS_POLICY_NAME, + shared=self.QOS_IS_SHARED, + ) + self.QOS_POLICY_ID = qos_policy.id + qos_rule = ( + self.operator_cloud.network.create_qos_packet_rate_limit_rule( + self.QOS_POLICY_ID, + max_kpps=self.RULE_MAX_KPPS, + max_burst_kpps=self.RULE_MAX_BURST_KPPS, + direction=self.RULE_DIRECTION, + ) + ) + assert isinstance( + qos_rule, _qos_packet_rate_limit_rule.QoSPacketRateLimitRule + ) + self.assertEqual(self.RULE_MAX_KPPS, qos_rule.max_kpps) + self.assertEqual(self.RULE_MAX_BURST_KPPS, qos_rule.max_burst_kpps) + self.assertEqual(self.RULE_DIRECTION, qos_rule.direction) + self.RULE_ID = qos_rule.id + + def tearDown(self): + rule = self.operator_cloud.network.delete_qos_packet_rate_limit_rule( + self.RULE_ID, self.QOS_POLICY_ID + ) + qos_policy = self.operator_cloud.network.delete_qos_policy( + self.QOS_POLICY_ID + ) + self.assertIsNone(rule) + self.assertIsNone(qos_policy) + super().tearDown() + + def test_find(self): + sot = self.operator_cloud.network.find_qos_packet_rate_limit_rule( + self.RULE_ID, self.QOS_POLICY_ID + ) + self.assertEqual(self.RULE_ID, sot.id) + self.assertEqual(self.RULE_MAX_KPPS, sot.max_kpps) + self.assertEqual(self.RULE_MAX_BURST_KPPS, sot.max_burst_kpps) + self.assertEqual(self.RULE_DIRECTION, sot.direction) + + def test_get(self): + sot = self.operator_cloud.network.get_qos_packet_rate_limit_rule( + self.RULE_ID, self.QOS_POLICY_ID + ) + self.assertEqual(self.RULE_ID, sot.id) + self.assertEqual(self.QOS_POLICY_ID, sot.qos_policy_id) + self.assertEqual(self.RULE_MAX_KPPS, sot.max_kpps) + self.assertEqual(self.RULE_MAX_BURST_KPPS, sot.max_burst_kpps) + self.assertEqual(self.RULE_DIRECTION, sot.direction) + + def test_list(self): + rule_ids = [ + o.id + for o in self.operator_cloud.network.qos_packet_rate_limit_rules( + self.QOS_POLICY_ID + ) + ] + self.assertIn(self.RULE_ID, rule_ids) + + def test_update(self): + sot = self.operator_cloud.network.update_qos_packet_rate_limit_rule( + self.RULE_ID, + self.QOS_POLICY_ID, + max_kpps=self.RULE_MAX_KPPS_NEW, + max_burst_kpps=self.RULE_MAX_BURST_KPPS_NEW, + direction=self.RULE_DIRECTION_NEW, + ) + self.assertEqual(self.RULE_MAX_KPPS_NEW, sot.max_kpps) + self.assertEqual(self.RULE_MAX_BURST_KPPS_NEW, sot.max_burst_kpps) + self.assertEqual(self.RULE_DIRECTION_NEW, sot.direction) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index 81f80bfd2..00e4fb09a 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -53,6 +53,7 @@ from openstack.network.v2 import qos_dscp_marking_rule from openstack.network.v2 import qos_minimum_bandwidth_rule from openstack.network.v2 import qos_minimum_packet_rate_rule +from openstack.network.v2 import qos_packet_rate_limit_rule from openstack.network.v2 import qos_policy from openstack.network.v2 import qos_rule_type from openstack.network.v2 import quota @@ -1232,6 +1233,82 @@ def test_qos_minimum_packet_rate_rule_update(self): ) +class TestNetworkQosPacketRateLimitRule(TestNetworkProxy): + def test_qos_packet_rate_limit_rule_create_attrs(self): + self.verify_create( + self.proxy.create_qos_packet_rate_limit_rule, + qos_packet_rate_limit_rule.QoSPacketRateLimitRule, + method_kwargs={'qos_policy': QOS_POLICY_ID}, + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}, + ) + + def test_qos_packet_rate_limit_rule_delete(self): + self.verify_delete( + self.proxy.delete_qos_packet_rate_limit_rule, + qos_packet_rate_limit_rule.QoSPacketRateLimitRule, + ignore_missing=False, + method_args=["resource_or_id", QOS_POLICY_ID], + expected_args=["resource_or_id"], + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}, + ) + + def test_qos_packet_rate_limit_rule_delete_ignore(self): + self.verify_delete( + self.proxy.delete_qos_packet_rate_limit_rule, + qos_packet_rate_limit_rule.QoSPacketRateLimitRule, + ignore_missing=True, + method_args=["resource_or_id", QOS_POLICY_ID], + expected_args=["resource_or_id"], + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}, + ) + + def test_qos_packet_rate_limit_rule_find(self): + policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) + self._verify( + 'openstack.proxy.Proxy._find', + self.proxy.find_qos_packet_rate_limit_rule, + method_args=['rule_id', policy], + expected_args=[ + qos_packet_rate_limit_rule.QoSPacketRateLimitRule, + 'rule_id', + ], + expected_kwargs={ + 'ignore_missing': True, + 'qos_policy_id': QOS_POLICY_ID, + }, + ) + + def test_qos_packet_rate_limit_rule_get(self): + self.verify_get( + self.proxy.get_qos_packet_rate_limit_rule, + qos_packet_rate_limit_rule.QoSPacketRateLimitRule, + method_kwargs={'qos_policy': QOS_POLICY_ID}, + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}, + ) + + def test_qos_packet_rate_limit_rules(self): + self.verify_list( + self.proxy.qos_packet_rate_limit_rules, + qos_packet_rate_limit_rule.QoSPacketRateLimitRule, + method_kwargs={'qos_policy': QOS_POLICY_ID}, + expected_kwargs={'qos_policy_id': QOS_POLICY_ID}, + ) + + def test_qos_packet_rate_limit_rule_update(self): + policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) + self._verify( + 'openstack.network.v2._proxy.Proxy._update', + self.proxy.update_qos_packet_rate_limit_rule, + method_args=['rule_id', policy], + method_kwargs={'foo': 'bar'}, + expected_args=[ + qos_packet_rate_limit_rule.QoSPacketRateLimitRule, + 'rule_id', + ], + expected_kwargs={'qos_policy_id': QOS_POLICY_ID, 'foo': 'bar'}, + ) + + class TestNetworkQosRuleType(TestNetworkProxy): def test_qos_rule_type_find(self): self.verify_find( diff --git a/openstack/tests/unit/network/v2/test_qos_packet_rate_limit_rule.py b/openstack/tests/unit/network/v2/test_qos_packet_rate_limit_rule.py new file mode 100644 index 000000000..5bf210c52 --- /dev/null +++ b/openstack/tests/unit/network/v2/test_qos_packet_rate_limit_rule.py @@ -0,0 +1,49 @@ +# 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 uuid + +from openstack.network.v2 import qos_packet_rate_limit_rule +from openstack.tests.unit import base + + +EXAMPLE = { + 'id': 'IDENTIFIER', + 'qos_policy_id': 'qos-policy-' + uuid.uuid4().hex, + 'max_kpps': 1600, + 'max_burst_kpps': 1300, + 'direction': 'any', +} + + +class TestQoSBandwidthLimitRule(base.TestCase): + def test_basic(self): + sot = qos_packet_rate_limit_rule.QoSPacketRateLimitRule() + self.assertEqual('packet_rate_limit_rule', sot.resource_key) + self.assertEqual('packet_rate_limit_rules', sot.resources_key) + self.assertEqual( + '/qos/policies/%(qos_policy_id)s/packet_rate_limit_rules', + sot.base_path, + ) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = qos_packet_rate_limit_rule.QoSPacketRateLimitRule(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['qos_policy_id'], sot.qos_policy_id) + self.assertEqual(EXAMPLE['max_kpps'], sot.max_kpps) + self.assertEqual(EXAMPLE['max_burst_kpps'], sot.max_burst_kpps) + self.assertEqual(EXAMPLE['direction'], sot.direction) diff --git a/releasenotes/notes/qos-packet-rate-limit-rule-385945e2e831ab0d.yaml b/releasenotes/notes/qos-packet-rate-limit-rule-385945e2e831ab0d.yaml new file mode 100644 index 000000000..83c600f3e --- /dev/null +++ b/releasenotes/notes/qos-packet-rate-limit-rule-385945e2e831ab0d.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added Neutron QoS packet rate limit rule object and introduced support for + CRUD operations. From 1b28ce7bf180ab9c500b58bb4050fe7dd6f0267f Mon Sep 17 00:00:00 2001 From: cid Date: Thu, 27 Mar 2025 08:20:34 +0100 Subject: [PATCH 3683/3836] Adds support for the 'description' field in ports Depends-On: https://review.opendev.org/c/openstack/ironic/+/944769 Change-Id: I70ef5a57acce598ba35a95621391e9a968f9e7d6 --- openstack/baremetal/v1/port.py | 5 ++++- openstack/tests/unit/baremetal/v1/test_port.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/openstack/baremetal/v1/port.py b/openstack/baremetal/v1/port.py index 9db9fc12e..72d61f584 100644 --- a/openstack/baremetal/v1/port.py +++ b/openstack/baremetal/v1/port.py @@ -41,13 +41,16 @@ class Port(_common.Resource): # The is_smartnic field added in 1.53 # Query filter by shard added in 1.82 # The name field added in 1.88 - _max_microversion = '1.88' + # The description field added in 1.97 + _max_microversion = '1.97' #: The physical hardware address of the network port, typically the #: hardware MAC address. address = resource.Body('address') #: Timestamp at which the port was created. created_at = resource.Body('created_at') + #: The description for the port + description = resource.Body('description') #: A set of one or more arbitrary metadata key and value pairs. extra = resource.Body('extra') #: The UUID of the port diff --git a/openstack/tests/unit/baremetal/v1/test_port.py b/openstack/tests/unit/baremetal/v1/test_port.py index d688d7fbe..a8ae65e97 100644 --- a/openstack/tests/unit/baremetal/v1/test_port.py +++ b/openstack/tests/unit/baremetal/v1/test_port.py @@ -17,6 +17,7 @@ FAKE = { "address": "11:11:11:11:11:11", "created_at": "2016-08-18T22:28:49.946416+00:00", + "description": "Physical network", "extra": {}, "internal_info": {}, "is_smartnic": True, From 84f7c57741e4d4689f8dbb4cfda0cc20c5a2cd03 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Sat, 8 Mar 2025 09:45:45 +0000 Subject: [PATCH 3684/3836] volume: Use consistent reset_*_state functions Change-Id: Ib12693e2f5251cb2e83ba33ba9968bd2eac8690b Signed-off-by: Stephen Finucane --- doc/source/user/proxies/block_storage_v2.rst | 6 ++- doc/source/user/proxies/block_storage_v3.rst | 11 +++-- openstack/block_storage/v2/_proxy.py | 24 +++++++-- openstack/block_storage/v2/backup.py | 12 ++++- openstack/block_storage/v2/snapshot.py | 13 ++++- openstack/block_storage/v3/_proxy.py | 49 ++++++++++++++++--- openstack/block_storage/v3/backup.py | 12 ++++- openstack/block_storage/v3/group.py | 13 ++++- openstack/block_storage/v3/group_snapshot.py | 13 ++++- openstack/block_storage/v3/snapshot.py | 13 ++++- .../unit/block_storage/v2/test_backup.py | 4 +- .../tests/unit/block_storage/v2/test_proxy.py | 14 +++--- .../unit/block_storage/v2/test_snapshot.py | 4 +- .../unit/block_storage/v3/test_backup.py | 4 +- .../tests/unit/block_storage/v3/test_group.py | 4 +- .../block_storage/v3/test_group_snapshot.py | 33 +++++++++++++ .../tests/unit/block_storage/v3/test_proxy.py | 21 +++++--- .../unit/block_storage/v3/test_snapshot.py | 4 +- ...istent-volume-status-5a527cd561af5e7a.yaml | 11 +++++ 19 files changed, 215 insertions(+), 50 deletions(-) create mode 100644 releasenotes/notes/consistent-volume-status-5a527cd561af5e7a.yaml diff --git a/doc/source/user/proxies/block_storage_v2.rst b/doc/source/user/proxies/block_storage_v2.rst index 3f26fc80b..2cc73b43e 100644 --- a/doc/source/user/proxies/block_storage_v2.rst +++ b/doc/source/user/proxies/block_storage_v2.rst @@ -17,7 +17,8 @@ Backup Operations .. autoclass:: openstack.block_storage.v2._proxy.Proxy :noindex: - :members: create_backup, delete_backup, get_backup, backups, restore_backup + :members: create_backup, delete_backup, get_backup, backups, restore_backup, + reset_backup_status Capabilities Operations ^^^^^^^^^^^^^^^^^^^^^^^ @@ -61,7 +62,8 @@ Snapshot Operations .. autoclass:: openstack.block_storage.v2._proxy.Proxy :noindex: - :members: create_snapshot, delete_snapshot, get_snapshot, snapshots + :members: create_snapshot, delete_snapshot, get_snapshot, snapshots, + reset_snapshot_status Stats Operations ^^^^^^^^^^^^^^^^ diff --git a/doc/source/user/proxies/block_storage_v3.rst b/doc/source/user/proxies/block_storage_v3.rst index 21270bf34..1fa7c573b 100644 --- a/doc/source/user/proxies/block_storage_v3.rst +++ b/doc/source/user/proxies/block_storage_v3.rst @@ -40,7 +40,7 @@ Backup Operations .. autoclass:: openstack.block_storage.v3._proxy.Proxy :noindex: :members: create_backup, delete_backup, get_backup, find_backup, backups, - restore_backup, reset_backup + restore_backup, reset_backup_status BlockStorageSummary Operations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -77,7 +77,7 @@ Group Operations .. autoclass:: openstack.block_storage.v3._proxy.Proxy :noindex: :members: create_group, create_group_from_source, delete_group, update_group, - get_group, find_group, groups, reset_group_state + get_group, find_group, groups, reset_group_status Group Snapshot Operations ^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -85,7 +85,7 @@ Group Snapshot Operations .. autoclass:: openstack.block_storage.v3._proxy.Proxy :noindex: :members: create_group_snapshot, delete_group_snapshot, get_group_snapshot, - find_group_snapshot, group_snapshots, reset_group_snapshot_state + find_group_snapshot, group_snapshots, reset_group_snapshot_status Group Type Operations ^^^^^^^^^^^^^^^^^^^^^ @@ -130,8 +130,9 @@ Snapshot Operations :noindex: :members: create_snapshot, delete_snapshot, update_snapshot, get_snapshot, find_snapshot, snapshots, get_snapshot_metadata, - set_snapshot_metadata, delete_snapshot_metadata, reset_snapshot, - set_snapshot_status, manage_snapshot, unmanage_snapshot + set_snapshot_metadata, delete_snapshot_metadata, + reset_snapshot_status, set_snapshot_status, manage_snapshot, + unmanage_snapshot Stats Operations ^^^^^^^^^^^^^^^^ diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index a9d57da73..529b50a5f 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -202,7 +202,7 @@ def delete_snapshot(self, snapshot, ignore_missing=True): # ========== Snapshot actions ========== - def reset_snapshot(self, snapshot, status): + def reset_snapshot_status(self, snapshot, status): """Reset status of the snapshot :param snapshot: The value can be either the ID of a backup or a @@ -212,7 +212,15 @@ def reset_snapshot(self, snapshot, status): :returns: None """ snapshot = self._get_resource(_snapshot.Snapshot, snapshot) - snapshot.reset(self, status) + snapshot.reset_status(self, status) + + def reset_snapshot(self, snapshot, status): + warnings.warn( + "reset_snapshot is a deprecated alias for reset_snapshot_status " + "and will be removed in a future release.", + os_warnings.RemovedInSDK60Warning, + ) + return self.reset_snapshot_status(snapshot, status) # ========== Types ========== @@ -743,7 +751,7 @@ def restore_backup(self, backup, volume_id, name): backup = self._get_resource(_backup.Backup, backup) return backup.restore(self, volume_id=volume_id, name=name) - def reset_backup(self, backup, status): + def reset_backup_status(self, backup, status): """Reset status of the backup :param backup: The value can be either the ID of a backup or a @@ -753,7 +761,15 @@ def reset_backup(self, backup, status): :returns: None """ backup = self._get_resource(_backup.Backup, backup) - backup.reset(self, status) + backup.reset_status(self, status) + + def reset_backup(self, backup, status): + warnings.warn( + "reset_backup is a deprecated alias for reset_backup_status " + "and will be removed in a future release.", + os_warnings.RemovedInSDK60Warning, + ) + return self.reset_backup_status(backup, status) # ========== Limits ========== diff --git a/openstack/block_storage/v2/backup.py b/openstack/block_storage/v2/backup.py index e94c6ed8a..4c363b958 100644 --- a/openstack/block_storage/v2/backup.py +++ b/openstack/block_storage/v2/backup.py @@ -10,10 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. +import warnings from openstack import exceptions from openstack import resource from openstack import utils +from openstack import warnings as os_warnings class Backup(resource.Resource): @@ -195,10 +197,18 @@ def force_delete(self, session): body = {'os-force_delete': None} self._action(session, body) - def reset(self, session, status): + def reset_status(self, session, status): """Reset the status of the backup""" body = {'os-reset_status': {'status': status}} self._action(session, body) + def reset(self, session, status): + warnings.warn( + "reset is a deprecated alias for reset_status and will be " + "removed in a future release.", + os_warnings.RemovedInSDK60Warning, + ) + self.reset_status(session, status) + BackupDetail = Backup diff --git a/openstack/block_storage/v2/snapshot.py b/openstack/block_storage/v2/snapshot.py index c2b49d704..4d726c651 100644 --- a/openstack/block_storage/v2/snapshot.py +++ b/openstack/block_storage/v2/snapshot.py @@ -10,11 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. +import warnings + from openstack.common import metadata from openstack import exceptions from openstack import format from openstack import resource from openstack import utils +from openstack import warnings as os_warnings class Snapshot(resource.Resource, metadata.MetadataMixin): @@ -60,10 +63,18 @@ def _action(self, session, body, microversion=None): exceptions.raise_from_response(resp) return resp - def reset(self, session, status): + def reset_status(self, session, status): """Reset the status of the snapshot.""" body = {'os-reset_status': {'status': status}} self._action(session, body) + def reset(self, session, status): + warnings.warn( + "reset is a deprecated alias for reset_status and will be " + "removed in a future release.", + os_warnings.RemovedInSDK60Warning, + ) + self.reset_status(session, status) + SnapshotDetail = Snapshot diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 715f76ce2..5cfc1ddbb 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -280,7 +280,7 @@ def delete_snapshot_metadata(self, snapshot, keys=None): snapshot.delete_metadata(self) # ====== SNAPSHOT ACTIONS ====== - def reset_snapshot(self, snapshot, status): + def reset_snapshot_status(self, snapshot, status): """Reset status of the snapshot :param snapshot: The value can be either the ID of a backup or a @@ -290,7 +290,15 @@ def reset_snapshot(self, snapshot, status): :returns: None """ snapshot = self._get_resource(_snapshot.Snapshot, snapshot) - snapshot.reset(self, status) + snapshot.reset_status(self, status) + + def reset_snapshot(self, snapshot, status): + warnings.warn( + "reset_snapshot is a deprecated alias for reset_snapshot_status " + "and will be removed in a future release.", + os_warnings.RemovedInSDK60Warning, + ) + return self.reset_snapshot_status(snapshot, status) def set_snapshot_status(self, snapshot, status, progress=None): """Update fields related to the status of a snapshot. @@ -1402,7 +1410,7 @@ def restore_backup(self, backup, volume_id=None, name=None): backup = self._get_resource(_backup.Backup, backup) return backup.restore(self, volume_id=volume_id, name=name) - def reset_backup(self, backup, status): + def reset_backup_status(self, backup, status): """Reset status of the backup :param backup: The value can be either the ID of a backup or a @@ -1412,7 +1420,15 @@ def reset_backup(self, backup, status): :returns: None """ backup = self._get_resource(_backup.Backup, backup) - backup.reset(self, status) + backup.reset_status(self, status) + + def reset_backup(self, backup, status): + warnings.warn( + "reset_backup is a deprecated alias for reset_backup_status " + "and will be removed in a future release.", + os_warnings.RemovedInSDK60Warning, + ) + return self.reset_backup_status(backup, status) # ====== LIMITS ====== def get_limits(self, project=None): @@ -1535,7 +1551,7 @@ def create_group_from_source(self, **attrs): """ return _group.Group.create_from_source(self, **attrs) - def reset_group_state(self, group, status): + def reset_group_status(self, group, status): """Reset group status :param group: The :class:`~openstack.block_storage.v3.group.Group` @@ -1547,6 +1563,14 @@ def reset_group_state(self, group, status): res = self._get_resource(_group.Group, group) return res.reset_status(self, status) + def reset_group_state(self, group, status): + warnings.warn( + "reset_group_state is a deprecated alias for reset_group_status " + "and will be removed in a future release.", + os_warnings.RemovedInSDK60Warning, + ) + return self.reset_group_status(group, status) + def delete_group(self, group, delete_volumes=False): """Delete a group @@ -1654,20 +1678,29 @@ def create_group_snapshot(self, **attrs): """ return self._create(_group_snapshot.GroupSnapshot, **attrs) - def reset_group_snapshot_state(self, group_snapshot, state): + def reset_group_snapshot_status(self, group_snapshot, status): """Reset group snapshot status :param group_snapshot: The :class:`~openstack.block_storage.v3.group_snapshot.GroupSnapshot` to set the state. - :param state: The state of the group snapshot to be set. + :param state: The status of the group snapshot to be set. :returns: None """ resource = self._get_resource( _group_snapshot.GroupSnapshot, group_snapshot ) - resource.reset_state(self, state) + resource.reset_state(self, status) + + def reset_group_snapshot_state(self, group_snapshot, state): + warnings.warn( + "reset_group_snapshot_state is a deprecated alias for " + "reset_group_snapshot_status and will be removed in a future " + "release.", + os_warnings.RemovedInSDK60Warning, + ) + return self.reset_group_snapshot_status(group_snapshot, state) def delete_group_snapshot(self, group_snapshot, ignore_missing=True): """Delete a group snapshot diff --git a/openstack/block_storage/v3/backup.py b/openstack/block_storage/v3/backup.py index 6029f7763..982c89e92 100644 --- a/openstack/block_storage/v3/backup.py +++ b/openstack/block_storage/v3/backup.py @@ -10,10 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. +import warnings from openstack import exceptions from openstack import resource from openstack import utils +from openstack import warnings as os_warnings class Backup(resource.Resource): @@ -211,10 +213,18 @@ def force_delete(self, session): body = {'os-force_delete': None} self._action(session, body) - def reset(self, session, status): + def reset_status(self, session, status): """Reset the status of the backup""" body = {'os-reset_status': {'status': status}} self._action(session, body) + def reset(self, session, status): + warnings.warn( + "reset is a deprecated alias for reset_status and will be " + "removed in a future release.", + os_warnings.RemovedInSDK60Warning, + ) + self.reset_status(session, status) + BackupDetail = Backup diff --git a/openstack/block_storage/v3/group.py b/openstack/block_storage/v3/group.py index 7c67709ff..7d70cc848 100644 --- a/openstack/block_storage/v3/group.py +++ b/openstack/block_storage/v3/group.py @@ -10,9 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. +import warnings + from openstack import exceptions from openstack import resource from openstack import utils +from openstack import warnings as os_warnings class Group(resource.Resource): @@ -65,11 +68,19 @@ def delete(self, session, *args, delete_volumes=False, **kwargs): body = {'delete': {'delete-volumes': delete_volumes}} self._action(session, body) - def reset(self, session, status): + def reset_status(self, session, status): """Resets the status for a group.""" body = {'reset_status': {'status': status}} self._action(session, body) + def reset(self, session, status): + warnings.warn( + "reset is a deprecated alias for reset_status and will be " + "removed in a future release.", + os_warnings.RemovedInSDK60Warning, + ) + self.reset_status(session, status) + @classmethod def create_from_source( cls, diff --git a/openstack/block_storage/v3/group_snapshot.py b/openstack/block_storage/v3/group_snapshot.py index c820aa7b4..ab745287d 100644 --- a/openstack/block_storage/v3/group_snapshot.py +++ b/openstack/block_storage/v3/group_snapshot.py @@ -10,9 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. +import warnings + from openstack import exceptions from openstack import resource from openstack import utils +from openstack import warnings as os_warnings class GroupSnapshot(resource.Resource): @@ -80,7 +83,15 @@ def _action(self, session, body, microversion=None): exceptions.raise_from_response(response) return response - def reset_state(self, session, state): + def reset_status(self, session, state): """Resets the status for a group snapshot.""" body = {'reset_status': {'status': state}} return self._action(session, body) + + def reset_state(self, session, status): + warnings.warn( + "reset_state is a deprecated alias for reset_status and will be " + "removed in a future release.", + os_warnings.RemovedInSDK60Warning, + ) + self.reset_status(session, status) diff --git a/openstack/block_storage/v3/snapshot.py b/openstack/block_storage/v3/snapshot.py index d98d8e459..4331029bd 100644 --- a/openstack/block_storage/v3/snapshot.py +++ b/openstack/block_storage/v3/snapshot.py @@ -10,11 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. +import warnings + from openstack.common import metadata from openstack import exceptions from openstack import format from openstack import resource from openstack import utils +from openstack import warnings as os_warnings class Snapshot(resource.Resource, metadata.MetadataMixin): @@ -92,11 +95,19 @@ def force_delete(self, session): body = {'os-force_delete': None} self._action(session, body) - def reset(self, session, status): + def reset_status(self, session, status): """Reset the status of the snapshot.""" body = {'os-reset_status': {'status': status}} self._action(session, body) + def reset(self, session, status): + warnings.warn( + "reset is a deprecated alias for reset_status and will be " + "removed in a future release.", + os_warnings.RemovedInSDK60Warning, + ) + self.reset_status(session, status) + def set_status(self, session, status, progress=None): """Update fields related to the status of a snapshot.""" body = {'os-update_snapshot_status': {'status': status}} diff --git a/openstack/tests/unit/block_storage/v2/test_backup.py b/openstack/tests/unit/block_storage/v2/test_backup.py index 20b1413a4..269623367 100644 --- a/openstack/tests/unit/block_storage/v2/test_backup.py +++ b/openstack/tests/unit/block_storage/v2/test_backup.py @@ -213,10 +213,10 @@ def test_force_delete(self): url, json=body, microversion=sot._max_microversion ) - def test_reset(self): + def test_reset_status(self): sot = backup.Backup(**BACKUP) - self.assertIsNone(sot.reset(self.sess, 'new_status')) + self.assertIsNone(sot.reset_status(self.sess, 'new_status')) url = f'backups/{FAKE_ID}/action' body = {'os-reset_status': {'status': 'new_status'}} diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index 7ba1b153b..649bd8475 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -177,7 +177,7 @@ def test_volume_set_bootable(self): expected_args=[self.proxy, True], ) - def test_volume_reset_volume_status(self): + def test_volume_reset_status(self): self._verify( "openstack.block_storage.v2.volume.Volume.reset_status", self.proxy.reset_volume_status, @@ -341,10 +341,10 @@ def test_backup_restore(self): expected_kwargs={'volume_id': 'vol_id', 'name': 'name'}, ) - def test_backup_reset(self): + def test_backup_reset_status(self): self._verify( - "openstack.block_storage.v2.backup.Backup.reset", - self.proxy.reset_backup, + "openstack.block_storage.v2.backup.Backup.reset_status", + self.proxy.reset_backup_status, method_args=["value", "new_status"], expected_args=[self.proxy, "new_status"], ) @@ -407,10 +407,10 @@ def test_snapshot_delete(self): def test_snapshot_delete_ignore(self): self.verify_delete(self.proxy.delete_snapshot, snapshot.Snapshot, True) - def test_reset(self): + def test_snapshot_reset_status(self): self._verify( - "openstack.block_storage.v2.snapshot.Snapshot.reset", - self.proxy.reset_snapshot, + "openstack.block_storage.v2.snapshot.Snapshot.reset_status", + self.proxy.reset_snapshot_status, method_args=["value", "new_status"], expected_args=[self.proxy, "new_status"], ) diff --git a/openstack/tests/unit/block_storage/v2/test_snapshot.py b/openstack/tests/unit/block_storage/v2/test_snapshot.py index 657ffb1e0..beca9660f 100644 --- a/openstack/tests/unit/block_storage/v2/test_snapshot.py +++ b/openstack/tests/unit/block_storage/v2/test_snapshot.py @@ -92,10 +92,10 @@ def setUp(self): self.sess.post = mock.Mock(return_value=self.resp) self.sess.default_microversion = None - def test_reset(self): + def test_reset_status(self): sot = snapshot.Snapshot(**SNAPSHOT) - self.assertIsNone(sot.reset(self.sess, 'new_status')) + self.assertIsNone(sot.reset_status(self.sess, 'new_status')) url = f'snapshots/{FAKE_ID}/action' body = {'os-reset_status': {'status': 'new_status'}} diff --git a/openstack/tests/unit/block_storage/v3/test_backup.py b/openstack/tests/unit/block_storage/v3/test_backup.py index ef68e88d4..f0dfb1f1b 100644 --- a/openstack/tests/unit/block_storage/v3/test_backup.py +++ b/openstack/tests/unit/block_storage/v3/test_backup.py @@ -226,10 +226,10 @@ def test_force_delete(self): url, json=body, microversion=sot._max_microversion ) - def test_reset(self): + def test_reset_status(self): sot = backup.Backup(**BACKUP) - self.assertIsNone(sot.reset(self.sess, 'new_status')) + self.assertIsNone(sot.reset_status(self.sess, 'new_status')) url = f'backups/{FAKE_ID}/action' body = {'os-reset_status': {'status': 'new_status'}} diff --git a/openstack/tests/unit/block_storage/v3/test_group.py b/openstack/tests/unit/block_storage/v3/test_group.py index cc6592885..838954a6d 100644 --- a/openstack/tests/unit/block_storage/v3/test_group.py +++ b/openstack/tests/unit/block_storage/v3/test_group.py @@ -93,10 +93,10 @@ def test_delete(self): url, json=body, microversion=sot._max_microversion ) - def test_reset(self): + def test_reset_status(self): sot = group.Group(**GROUP) - self.assertIsNone(sot.reset(self.sess, 'new_status')) + self.assertIsNone(sot.reset_status(self.sess, 'new_status')) url = f'groups/{GROUP_ID}/action' body = {'reset_status': {'status': 'new_status'}} diff --git a/openstack/tests/unit/block_storage/v3/test_group_snapshot.py b/openstack/tests/unit/block_storage/v3/test_group_snapshot.py index 8005bd6da..0db81196e 100644 --- a/openstack/tests/unit/block_storage/v3/test_group_snapshot.py +++ b/openstack/tests/unit/block_storage/v3/test_group_snapshot.py @@ -10,6 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. +from unittest import mock + +from keystoneauth1 import adapter + from openstack.block_storage.v3 import group_snapshot from openstack.tests.unit import base @@ -49,3 +53,32 @@ def test_make_resource(self): self.assertEqual(GROUP_SNAPSHOT["name"], resource.name) self.assertEqual(GROUP_SNAPSHOT["project_id"], resource.project_id) self.assertEqual(GROUP_SNAPSHOT["status"], resource.status) + + +class TestGroupSnapshotActions(base.TestCase): + def setUp(self): + super().setUp() + self.resp = mock.Mock() + self.resp.body = None + self.resp.json = mock.Mock(return_value=self.resp.body) + self.resp.headers = {} + self.resp.status_code = 202 + + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.get = mock.Mock() + self.sess.post = mock.Mock(return_value=self.resp) + self.sess.default_microversion = '3.0' + + def test_reset_status(self): + resource = group_snapshot.GroupSnapshot(**GROUP_SNAPSHOT) + + self.assertIsNotNone(resource.reset_status(self.sess, 'new_status')) + + url = f'group_snapshots/{GROUP_SNAPSHOT["id"]}/action' + body = {'reset_status': {'status': 'new_status'}} + self.sess.post.assert_called_with( + url, + json=body, + headers={'Accept': ''}, + microversion='3.0', + ) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 88ed3494b..7f5e9fd20 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -233,8 +233,13 @@ def test_group_delete(self): def test_group_update(self): self.verify_update(self.proxy.update_group, group.Group) - def reset_group_state(self): - self._verify(self.proxy.reset_group_state, group.Group) + def test_reset_group_status(self): + self._verify( + "openstack.block_storage.v3.group.Group.reset_status", + self.proxy.reset_group_status, + method_args=["value", "new_status"], + expected_args=[self.proxy, "new_status"], + ) class TestGroupSnapshot(TestVolumeProxy): @@ -759,10 +764,10 @@ def test_backup_restore(self): expected_kwargs={'volume_id': 'vol_id', 'name': 'name'}, ) - def test_backup_reset(self): + def test_backup_reset_status(self): self._verify( - "openstack.block_storage.v3.backup.Backup.reset", - self.proxy.reset_backup, + "openstack.block_storage.v3.backup.Backup.reset_status", + self.proxy.reset_backup_status, method_args=["value", "new_status"], expected_args=[self.proxy, "new_status"], ) @@ -822,10 +827,10 @@ def test_snapshot_delete_force(self): expected_args=[self.proxy], ) - def test_reset(self): + def test_snapshot_reset_status(self): self._verify( - "openstack.block_storage.v3.snapshot.Snapshot.reset", - self.proxy.reset_snapshot, + "openstack.block_storage.v3.snapshot.Snapshot.reset_status", + self.proxy.reset_snapshot_status, method_args=["value", "new_status"], expected_args=[self.proxy, "new_status"], ) diff --git a/openstack/tests/unit/block_storage/v3/test_snapshot.py b/openstack/tests/unit/block_storage/v3/test_snapshot.py index e925493f1..ca8a19d2a 100644 --- a/openstack/tests/unit/block_storage/v3/test_snapshot.py +++ b/openstack/tests/unit/block_storage/v3/test_snapshot.py @@ -111,10 +111,10 @@ def test_force_delete(self): url, json=body, microversion=sot._max_microversion ) - def test_reset(self): + def test_reset_status(self): sot = snapshot.Snapshot(**SNAPSHOT) - self.assertIsNone(sot.reset(self.sess, 'new_status')) + self.assertIsNone(sot.reset_status(self.sess, 'new_status')) url = f'snapshots/{FAKE_ID}/action' body = {'os-reset_status': {'status': 'new_status'}} diff --git a/releasenotes/notes/consistent-volume-status-5a527cd561af5e7a.yaml b/releasenotes/notes/consistent-volume-status-5a527cd561af5e7a.yaml new file mode 100644 index 000000000..e161e23c7 --- /dev/null +++ b/releasenotes/notes/consistent-volume-status-5a527cd561af5e7a.yaml @@ -0,0 +1,11 @@ +--- +upgrade: + - | + The following volume (v2 and v3) helpers have been renamed: + + - ``reset_snapshot`` -> ``reset_snapshot_status`` + - ``reset_backup`` -> ``reset_backup_status`` + - ``reset_group_state`` -> ``reset_group_status`` + - ``reset_group_snapshot_state`` -> ``reset_group_snapshot_status`` + + Aliases are provided for backwards compatibility. From 72c76fe09d654895a642c5fc3b09781f6e3f34bb Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Sat, 8 Mar 2025 09:46:04 +0000 Subject: [PATCH 3685/3836] volume: Add backup metadata helpers We also remove remnants of the 'allow_get' keyword which was replaced by 'allow_fetch' and 'allow_list' some time ago. Change-Id: I0974576bf56d4a2134a9b8ee93389c1d3de97c1e Signed-off-by: Stephen Finucane --- openstack/block_storage/v2/backup.py | 1 - openstack/block_storage/v3/_proxy.py | 57 +++++++++++++++++++ openstack/block_storage/v3/attachment.py | 1 - openstack/block_storage/v3/backup.py | 5 +- openstack/block_storage/v3/transfer.py | 1 - .../unit/block_storage/v2/test_backup.py | 1 - .../unit/block_storage/v3/test_attachment.py | 1 - .../unit/block_storage/v3/test_backup.py | 2 +- .../tests/unit/block_storage/v3/test_proxy.py | 38 +++++++++++++ 9 files changed, 99 insertions(+), 8 deletions(-) diff --git a/openstack/block_storage/v2/backup.py b/openstack/block_storage/v2/backup.py index 4c363b958..bcbee4288 100644 --- a/openstack/block_storage/v2/backup.py +++ b/openstack/block_storage/v2/backup.py @@ -42,7 +42,6 @@ class Backup(resource.Resource): allow_create = True allow_delete = True allow_list = True - allow_get = True #: Properties #: backup availability zone diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 5cfc1ddbb..19eb751f2 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -1395,6 +1395,63 @@ def delete_backup(self, backup, ignore_missing=True, force=False): backup = self._get_resource(_backup.Backup, backup) backup.force_delete(self) + def update_backup(self, backup, **attrs): + """Update a backup + + :param backup: Either the ID of a backup or a + :class:`~openstack.block_storage.v3.backup.Backup`. + :param dict attrs: The attributes to update on the volume. + + :returns: The updated backup + :rtype: :class:`~openstack.block_storage.v3.backup.Backup` + """ + return self._update(_backup.Backup, backup, **attrs) + + def get_backup_metadata(self, backup): + """Return a dictionary of metadata for a backup + + :param backup: Either the ID of a backup or a + :class:`~openstack.block_storage.v3.backup.Backup`. + + :returns: A :class:`~openstack.block_storage.v3.backup.Backup` with the + backup's metadata. + :rtype: :class:`~openstack.block_storage.v3.backup.Backup` + """ + backup = self._get_resource(_backup.Backup, backup) + return backup.fetch_metadata(self) + + def set_backup_metadata(self, backup, **metadata): + """Update metadata for a backup + + :param backup: Either the ID of a backup or a + :class:`~openstack.block_storage.v3.backup.Backup`. + :param metadata: Key/value pairs to be updated in the backup's + metadata. No other metadata is modified by this call. + + :returns: A :class:`~openstack.block_storage.v3.backup.Backup` with the + backup's metadata. + :rtype: :class:`~openstack.block_storage.v3.backup.Backup` + """ + backup = self._get_resource(_backup.Backup, backup) + return backup.set_metadata(self, metadata=metadata) + + def delete_backup_metadata(self, backup, keys=None): + """Delete metadata for a backup + + :param backup: Either the ID of a backup or a + :class:`~openstack.block_storage.v3.backup.Backup`. + :param list keys: The keys to delete. If left empty complete + metadata will be removed. + + :rtype: ``None`` + """ + backup = self._get_resource(_backup.Backup, backup) + if keys is not None: + for key in keys: + backup.delete_metadata_item(self, key) + else: + backup.delete_metadata(self) + # ====== BACKUP ACTIONS ====== def restore_backup(self, backup, volume_id=None, name=None): """Restore a Backup to volume diff --git a/openstack/block_storage/v3/attachment.py b/openstack/block_storage/v3/attachment.py index 4a459b397..7e49a745c 100644 --- a/openstack/block_storage/v3/attachment.py +++ b/openstack/block_storage/v3/attachment.py @@ -27,7 +27,6 @@ class Attachment(resource.Resource): allow_delete = True allow_commit = True allow_list = True - allow_get = True allow_fetch = True _max_microversion = "3.54" diff --git a/openstack/block_storage/v3/backup.py b/openstack/block_storage/v3/backup.py index 982c89e92..26c45b1da 100644 --- a/openstack/block_storage/v3/backup.py +++ b/openstack/block_storage/v3/backup.py @@ -12,13 +12,14 @@ import warnings +from openstack.common import metadata from openstack import exceptions from openstack import resource from openstack import utils from openstack import warnings as os_warnings -class Backup(resource.Resource): +class Backup(resource.Resource, metadata.MetadataMixin): """Volume Backup""" resource_key = "backup" @@ -46,8 +47,8 @@ class Backup(resource.Resource): allow_fetch = True allow_create = True allow_delete = True + allow_commit = True allow_list = True - allow_get = True #: Properties #: backup availability zone diff --git a/openstack/block_storage/v3/transfer.py b/openstack/block_storage/v3/transfer.py index 9c2643181..660581f27 100644 --- a/openstack/block_storage/v3/transfer.py +++ b/openstack/block_storage/v3/transfer.py @@ -25,7 +25,6 @@ class Transfer(resource.Resource): allow_delete = True allow_fetch = True allow_list = True - allow_get = True # Properties #: UUID of the transfer. diff --git a/openstack/tests/unit/block_storage/v2/test_backup.py b/openstack/tests/unit/block_storage/v2/test_backup.py index 269623367..cf230d971 100644 --- a/openstack/tests/unit/block_storage/v2/test_backup.py +++ b/openstack/tests/unit/block_storage/v2/test_backup.py @@ -61,7 +61,6 @@ def test_basic(self): self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertTrue(sot.allow_get) self.assertTrue(sot.allow_fetch) self.assertDictEqual( diff --git a/openstack/tests/unit/block_storage/v3/test_attachment.py b/openstack/tests/unit/block_storage/v3/test_attachment.py index 006760219..e19778c96 100644 --- a/openstack/tests/unit/block_storage/v3/test_attachment.py +++ b/openstack/tests/unit/block_storage/v3/test_attachment.py @@ -90,7 +90,6 @@ def test_basic(self): self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertTrue(sot.allow_get) self.assertTrue(sot.allow_commit) self.assertIsNotNone(sot._max_microversion) diff --git a/openstack/tests/unit/block_storage/v3/test_backup.py b/openstack/tests/unit/block_storage/v3/test_backup.py index f0dfb1f1b..1645db341 100644 --- a/openstack/tests/unit/block_storage/v3/test_backup.py +++ b/openstack/tests/unit/block_storage/v3/test_backup.py @@ -65,8 +65,8 @@ def test_basic(self): self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_delete) self.assertTrue(sot.allow_list) - self.assertTrue(sot.allow_get) self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) self.assertIsNotNone(sot._max_microversion) self.assertDictEqual( diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 7f5e9fd20..e9ceb4808 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -117,6 +117,9 @@ def test_volume_delete_force_v323(self, mock_mv): expected_kwargs={"params": {"cascade": False, "force": True}}, ) + def test_volume_update(self): + self.verify_update(self.proxy.update_volume, volume.Volume) + def test_get_volume_metadata(self): self._verify( "openstack.block_storage.v3.volume.Volume.fetch_metadata", @@ -745,6 +748,9 @@ def test_backup_delete_force(self): expected_args=[self.proxy], ) + def test_backup_update(self): + self.verify_update(self.proxy.update_backup, backup.Backup) + def test_backup_create_attrs(self): # NOTE: mock has_service self.proxy._connection = mock.Mock() @@ -772,6 +778,38 @@ def test_backup_reset_status(self): expected_args=[self.proxy, "new_status"], ) + def test_backup_get_metadata(self): + self._verify( + "openstack.block_storage.v3.backup.Backup.fetch_metadata", + self.proxy.get_backup_metadata, + method_args=["value"], + expected_args=[self.proxy], + expected_result=volume.Volume(id="value", metadata={}), + ) + + def test_backup_set_metadata(self): + kwargs = {"a": "1", "b": "2"} + id = "an_id" + self._verify( + "openstack.block_storage.v3.backup.Backup.set_metadata", + self.proxy.set_backup_metadata, + method_args=[id], + method_kwargs=kwargs, + method_result=volume.Volume.existing(id=id, metadata=kwargs), + expected_args=[self.proxy], + expected_kwargs={'metadata': kwargs}, + expected_result=volume.Volume.existing(id=id, metadata=kwargs), + ) + + def test_backup_delete_metadata(self): + self._verify( + "openstack.block_storage.v3.backup.Backup.delete_metadata_item", + self.proxy.delete_backup_metadata, + expected_result=None, + method_args=["value", ["key"]], + expected_args=[self.proxy, "key"], + ) + class TestSnapshot(TestVolumeProxy): def test_snapshot_get(self): From 1a57c14ee9c7594c08baf4d477e60345a590d65f Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 10 Mar 2025 16:57:48 +0000 Subject: [PATCH 3686/3836] volume: Add Snapshot.manage, unmanage to volume v2 API Change-Id: I4d51c26108ef4daceedc15e6dde846d3049c71e2 Signed-off-by: Stephen Finucane --- openstack/block_storage/v2/_proxy.py | 24 +++++++++ openstack/block_storage/v2/snapshot.py | 38 ++++++++++++-- .../tests/unit/block_storage/v2/test_proxy.py | 27 ++++++++++ .../unit/block_storage/v2/test_snapshot.py | 41 +++++++++++++++- .../tests/unit/block_storage/v3/test_proxy.py | 49 +++++++++++-------- 5 files changed, 153 insertions(+), 26 deletions(-) diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 529b50a5f..ead6a3e0d 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -222,6 +222,30 @@ def reset_snapshot(self, snapshot, status): ) return self.reset_snapshot_status(snapshot, status) + def manage_snapshot(self, **attrs): + """Creates a snapshot by using existing storage rather than + allocating new storage. + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.block_storage.v2.snapshot.Snapshot`, + comprised of the properties on the Snapshot class. + + :returns: The results of snapshot creation + :rtype: :class:`~openstack.block_storage.v2.snapshot.Snapshot` + """ + return _snapshot.Snapshot.manage(self, **attrs) + + def unmanage_snapshot(self, snapshot): + """Unmanage a snapshot from block storage provisioning. + + :param snapshot: Either the ID of a snapshot or a + :class:`~openstack.block_storage.v2.snapshot.Snapshot`. + + :returns: None + """ + snapshot_obj = self._get_resource(_snapshot.Snapshot, snapshot) + snapshot_obj.unmanage(self) + # ========== Types ========== def get_type(self, type): diff --git a/openstack/block_storage/v2/snapshot.py b/openstack/block_storage/v2/snapshot.py index 4d726c651..6e3929187 100644 --- a/openstack/block_storage/v2/snapshot.py +++ b/openstack/block_storage/v2/snapshot.py @@ -54,12 +54,10 @@ class Snapshot(resource.Resource, metadata.MetadataMixin): #: The ID of the volume this snapshot was taken of. volume_id = resource.Body("volume_id") - def _action(self, session, body, microversion=None): + def _action(self, session, body): """Preform backup actions given the message body.""" url = utils.urljoin(self.base_path, self.id, 'action') - resp = session.post( - url, json=body, microversion=self._max_microversion - ) + resp = session.post(url, json=body) exceptions.raise_from_response(resp) return resp @@ -76,5 +74,37 @@ def reset(self, session, status): ) self.reset_status(session, status) + @classmethod + def manage( + cls, + session, + volume_id, + ref, + name=None, + description=None, + metadata=None, + ): + """Manage a snapshot under block storage provisioning.""" + url = '/os-snapshot-manage' + body = { + 'snapshot': { + 'volume_id': volume_id, + 'ref': ref, + 'name': name, + 'description': description, + 'metadata': metadata, + } + } + resp = session.post(url, json=body) + exceptions.raise_from_response(resp) + snapshot = Snapshot() + snapshot._translate_response(resp) + return snapshot + + def unmanage(self, session): + """Unmanage a snapshot from block storage provisioning.""" + body = {'os-unmanage': None} + self._action(session, body) + SnapshotDetail = Snapshot diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index 649bd8475..37124fc4b 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -415,6 +415,33 @@ def test_snapshot_reset_status(self): expected_args=[self.proxy, "new_status"], ) + def test_snapshot_manage(self): + kwargs = { + "volume_id": "fake_id", + "remote_source": "fake_volume", + "snapshot_name": "fake_snap", + "description": "test_snap", + "property": {"k": "v"}, + } + self._verify( + "openstack.block_storage.v2.snapshot.Snapshot.manage", + self.proxy.manage_snapshot, + method_kwargs=kwargs, + method_result=snapshot.Snapshot(id="fake_id"), + expected_args=[self.proxy], + expected_kwargs=kwargs, + expected_result=snapshot.Snapshot(id="fake_id"), + ) + + def test_snapshot_unmanage(self): + self._verify( + "openstack.block_storage.v2.snapshot.Snapshot.unmanage", + self.proxy.unmanage_snapshot, + method_args=["value"], + expected_args=[self.proxy], + expected_result=None, + ) + def test_get_snapshot_metadata(self): self._verify( "openstack.block_storage.v2.snapshot.Snapshot.fetch_metadata", diff --git a/openstack/tests/unit/block_storage/v2/test_snapshot.py b/openstack/tests/unit/block_storage/v2/test_snapshot.py index beca9660f..501317893 100644 --- a/openstack/tests/unit/block_storage/v2/test_snapshot.py +++ b/openstack/tests/unit/block_storage/v2/test_snapshot.py @@ -9,6 +9,8 @@ # 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 from unittest import mock from keystoneauth1 import adapter @@ -18,6 +20,7 @@ FAKE_ID = "ffa9bc5e-1172-4021-acaf-cdcd78a9584d" +FAKE_VOLUME_ID = "5aa119a8-d25b-45a7-8d1b-88e127885635" SNAPSHOT = { "status": "creating", @@ -99,6 +102,40 @@ def test_reset_status(self): url = f'snapshots/{FAKE_ID}/action' body = {'os-reset_status': {'status': 'new_status'}} - self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion + self.sess.post.assert_called_with(url, json=body) + + def test_manage(self): + resp = mock.Mock() + resp.body = {'snapshot': copy.deepcopy(SNAPSHOT)} + resp.json = mock.Mock(return_value=resp.body) + resp.headers = {} + resp.status_code = 202 + + self.sess.post = mock.Mock(return_value=resp) + + sot = snapshot.Snapshot.manage( + self.sess, volume_id=FAKE_VOLUME_ID, ref=FAKE_ID ) + + self.assertIsNotNone(sot) + + url = '/os-snapshot-manage' + body = { + 'snapshot': { + 'volume_id': FAKE_VOLUME_ID, + 'ref': FAKE_ID, + 'name': None, + 'description': None, + 'metadata': None, + } + } + self.sess.post.assert_called_with(url, json=body) + + def test_unmanage(self): + sot = snapshot.Snapshot(**SNAPSHOT) + + self.assertIsNone(sot.unmanage(self.sess)) + + url = f'snapshots/{FAKE_ID}/action' + body = {'os-unmanage': None} + self.sess.post.assert_called_with(url, json=body) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index e9ceb4808..cf17c3654 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -873,7 +873,7 @@ def test_snapshot_reset_status(self): expected_args=[self.proxy, "new_status"], ) - def test_set_status(self): + def test_snapshot_set_status(self): self._verify( "openstack.block_storage.v3.snapshot.Snapshot.set_status", self.proxy.set_snapshot_status, @@ -881,7 +881,7 @@ def test_set_status(self): expected_args=[self.proxy, "new_status", None], ) - def test_set_status_percentage(self): + def test_snapshot_set_status_percentage(self): self._verify( "openstack.block_storage.v3.snapshot.Snapshot.set_status", self.proxy.set_snapshot_status, @@ -889,6 +889,33 @@ def test_set_status_percentage(self): expected_args=[self.proxy, "new_status", "per"], ) + def test_snapshot_manage(self): + kwargs = { + "volume_id": "fake_id", + "remote_source": "fake_volume", + "snapshot_name": "fake_snap", + "description": "test_snap", + "property": {"k": "v"}, + } + self._verify( + "openstack.block_storage.v3.snapshot.Snapshot.manage", + self.proxy.manage_snapshot, + method_kwargs=kwargs, + method_result=snapshot.Snapshot(id="fake_id"), + expected_args=[self.proxy], + expected_kwargs=kwargs, + expected_result=snapshot.Snapshot(id="fake_id"), + ) + + def test_snapshot_unmanage(self): + self._verify( + "openstack.block_storage.v3.snapshot.Snapshot.unmanage", + self.proxy.unmanage_snapshot, + method_args=["value"], + expected_args=[self.proxy], + expected_result=None, + ) + def test_get_snapshot_metadata(self): self._verify( "openstack.block_storage.v3.snapshot.Snapshot.fetch_metadata", @@ -922,24 +949,6 @@ def test_delete_snapshot_metadata(self): expected_args=[self.proxy, "key"], ) - def test_manage_snapshot(self): - kwargs = { - "volume_id": "fake_id", - "remote_source": "fake_volume", - "snapshot_name": "fake_snap", - "description": "test_snap", - "property": {"k": "v"}, - } - self._verify( - "openstack.block_storage.v3.snapshot.Snapshot.manage", - self.proxy.manage_snapshot, - method_kwargs=kwargs, - method_result=snapshot.Snapshot(id="fake_id"), - expected_args=[self.proxy], - expected_kwargs=kwargs, - expected_result=snapshot.Snapshot(id="fake_id"), - ) - class TestType(TestVolumeProxy): def test_type_get(self): From 3199a70773043789b2d2aacf9c7ec673af2202f7 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 27 Mar 2025 19:27:19 +0000 Subject: [PATCH 3687/3836] volume: Stop setting microversion for block storage v2 This will always be None since v2 does not support microversions. Change-Id: Idae0f7ca47186406845435472fb4e94abc315a6e Signed-off-by: Stephen Finucane --- openstack/block_storage/v2/backup.py | 9 +- openstack/block_storage/v2/service.py | 6 +- openstack/block_storage/v2/volume.py | 2 +- .../unit/block_storage/v2/test_backup.py | 11 +-- .../unit/block_storage/v2/test_service.py | 37 ++------ .../unit/block_storage/v2/test_snapshot.py | 1 - .../tests/unit/block_storage/v2/test_type.py | 1 - .../unit/block_storage/v2/test_volume.py | 89 +++++-------------- 8 files changed, 35 insertions(+), 121 deletions(-) diff --git a/openstack/block_storage/v2/backup.py b/openstack/block_storage/v2/backup.py index bcbee4288..4a336f593 100644 --- a/openstack/block_storage/v2/backup.py +++ b/openstack/block_storage/v2/backup.py @@ -109,7 +109,6 @@ def create(self, session, prepend_key=True, base_path=None, **params): raise exceptions.MethodNotSupported(self, "create") session = self._get_session(session) - microversion = self._get_microversion(session) requires_id = ( self.create_requires_id if self.create_requires_id is not None @@ -138,7 +137,6 @@ def create(self, session, prepend_key=True, base_path=None, **params): request.url, json=request.body, headers=request.headers, - microversion=microversion, params=params, ) else: @@ -152,7 +150,6 @@ def create(self, session, prepend_key=True, base_path=None, **params): if self.create_returns_body is None else self.create_returns_body ) - self.microversion = microversion self._translate_response(response, has_body=has_body) # direct comparision to False since we need to rule out None if self.has_body and self.create_returns_body is False: @@ -160,12 +157,10 @@ def create(self, session, prepend_key=True, base_path=None, **params): return self.fetch(session) return self - def _action(self, session, body, microversion=None): + def _action(self, session, body): """Preform backup actions given the message body.""" url = utils.urljoin(self.base_path, self.id, 'action') - resp = session.post( - url, json=body, microversion=self._max_microversion - ) + resp = session.post(url, json=body) exceptions.raise_from_response(resp) return resp diff --git a/openstack/block_storage/v2/service.py b/openstack/block_storage/v2/service.py index 38682d1f3..cc091bb42 100644 --- a/openstack/block_storage/v2/service.py +++ b/openstack/block_storage/v2/service.py @@ -93,11 +93,9 @@ def commit(self, session, prepend_key=False, *args, **kwargs): **kwargs, ) - def _action(self, session, action, body, microversion=None): - if not microversion: - microversion = session.default_microversion + def _action(self, session, action, body): url = utils.urljoin(Service.base_path, action) - response = session.put(url, json=body, microversion=microversion) + response = session.put(url, json=body) self._translate_response(response) return self diff --git a/openstack/block_storage/v2/volume.py b/openstack/block_storage/v2/volume.py index 77ddf9f1b..45431351e 100644 --- a/openstack/block_storage/v2/volume.py +++ b/openstack/block_storage/v2/volume.py @@ -104,7 +104,7 @@ def _action(self, session, body): # as both Volume and VolumeDetail instances can be acted on, but # the URL used is sans any additional /detail/ part. url = utils.urljoin(Volume.base_path, self.id, 'action') - return session.post(url, json=body, microversion=None) + return session.post(url, json=body) def extend(self, session, size): """Extend a volume size.""" diff --git a/openstack/tests/unit/block_storage/v2/test_backup.py b/openstack/tests/unit/block_storage/v2/test_backup.py index cf230d971..760f46ecb 100644 --- a/openstack/tests/unit/block_storage/v2/test_backup.py +++ b/openstack/tests/unit/block_storage/v2/test_backup.py @@ -51,7 +51,6 @@ def setUp(self): self.sess = mock.Mock(spec=adapter.Adapter) self.sess.get = mock.Mock() self.sess.post = mock.Mock(return_value=self.resp) - self.sess.default_microversion = None def test_basic(self): sot = backup.Backup(BACKUP) @@ -116,7 +115,6 @@ def test_create_incremental(self): 'incremental': True, } }, - microversion=None, params={}, ) @@ -129,7 +127,6 @@ def test_create_incremental(self): 'incremental': False, } }, - microversion=None, params={}, ) @@ -208,9 +205,7 @@ def test_force_delete(self): url = f'backups/{FAKE_ID}/action' body = {'os-force_delete': None} - self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion - ) + self.sess.post.assert_called_with(url, json=body) def test_reset_status(self): sot = backup.Backup(**BACKUP) @@ -219,6 +214,4 @@ def test_reset_status(self): url = f'backups/{FAKE_ID}/action' body = {'os-reset_status': {'status': 'new_status'}} - self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion - ) + self.sess.post.assert_called_with(url, json=body) diff --git a/openstack/tests/unit/block_storage/v2/test_service.py b/openstack/tests/unit/block_storage/v2/test_service.py index 35a6429cd..dc9dc2eb7 100644 --- a/openstack/tests/unit/block_storage/v2/test_service.py +++ b/openstack/tests/unit/block_storage/v2/test_service.py @@ -36,7 +36,6 @@ def setUp(self): self.resp.headers = {} self.sess = mock.Mock() self.sess.put = mock.Mock(return_value=self.resp) - self.sess.default_microversion = '3.0' def test_basic(self): sot = service.Service() @@ -79,11 +78,7 @@ def test_enable(self): 'binary': 'cinder-scheduler', 'host': 'devstack', } - self.sess.put.assert_called_with( - url, - json=body, - microversion=self.sess.default_microversion, - ) + self.sess.put.assert_called_with(url, json=body) def test_disable(self): sot = service.Service(**EXAMPLE) @@ -96,11 +91,7 @@ def test_disable(self): 'binary': 'cinder-scheduler', 'host': 'devstack', } - self.sess.put.assert_called_with( - url, - json=body, - microversion=self.sess.default_microversion, - ) + self.sess.put.assert_called_with(url, json=body) def test_disable__with_reason(self): sot = service.Service(**EXAMPLE) @@ -116,11 +107,7 @@ def test_disable__with_reason(self): 'host': 'devstack', 'disabled_reason': reason, } - self.sess.put.assert_called_with( - url, - json=body, - microversion=self.sess.default_microversion, - ) + self.sess.put.assert_called_with(url, json=body) def test_thaw(self): sot = service.Service(**EXAMPLE) @@ -130,11 +117,7 @@ def test_thaw(self): url = 'os-services/thaw' body = {'host': 'devstack'} - self.sess.put.assert_called_with( - url, - json=body, - microversion=self.sess.default_microversion, - ) + self.sess.put.assert_called_with(url, json=body) def test_freeze(self): sot = service.Service(**EXAMPLE) @@ -144,11 +127,7 @@ def test_freeze(self): url = 'os-services/freeze' body = {'host': 'devstack'} - self.sess.put.assert_called_with( - url, - json=body, - microversion=self.sess.default_microversion, - ) + self.sess.put.assert_called_with(url, json=body) def test_failover(self): sot = service.Service(**EXAMPLE) @@ -158,8 +137,4 @@ def test_failover(self): url = 'os-services/failover_host' body = {'host': 'devstack'} - self.sess.put.assert_called_with( - url, - json=body, - microversion=self.sess.default_microversion, - ) + self.sess.put.assert_called_with(url, json=body) diff --git a/openstack/tests/unit/block_storage/v2/test_snapshot.py b/openstack/tests/unit/block_storage/v2/test_snapshot.py index 501317893..590c7d38b 100644 --- a/openstack/tests/unit/block_storage/v2/test_snapshot.py +++ b/openstack/tests/unit/block_storage/v2/test_snapshot.py @@ -93,7 +93,6 @@ def setUp(self): self.sess = mock.Mock(spec=adapter.Adapter) self.sess.get = mock.Mock() self.sess.post = mock.Mock(return_value=self.resp) - self.sess.default_microversion = None def test_reset_status(self): sot = snapshot.Snapshot(**SNAPSHOT) diff --git a/openstack/tests/unit/block_storage/v2/test_type.py b/openstack/tests/unit/block_storage/v2/test_type.py index edfade8ff..5271f2c35 100644 --- a/openstack/tests/unit/block_storage/v2/test_type.py +++ b/openstack/tests/unit/block_storage/v2/test_type.py @@ -31,7 +31,6 @@ def setUp(self): self.resp.status_code = 200 self.resp.json = mock.Mock(return_value=self.resp.body) self.sess = mock.Mock(spec=adapter.Adapter) - self.sess.default_microversion = '3.0' self.sess.post = mock.Mock(return_value=self.resp) self.sess._get_connection = mock.Mock(return_value=self.cloud) diff --git a/openstack/tests/unit/block_storage/v2/test_volume.py b/openstack/tests/unit/block_storage/v2/test_volume.py index 1e80ce9f0..2ac2529b2 100644 --- a/openstack/tests/unit/block_storage/v2/test_volume.py +++ b/openstack/tests/unit/block_storage/v2/test_volume.py @@ -144,7 +144,6 @@ def setUp(self): self.resp.status_code = 200 self.resp.json = mock.Mock(return_value=self.resp.body) self.sess = mock.Mock(spec=adapter.Adapter) - self.sess.default_microversion = '3.0' self.sess.post = mock.Mock(return_value=self.resp) self.sess._get_connection = mock.Mock(return_value=self.cloud) @@ -155,9 +154,7 @@ def test_extend(self): url = f'volumes/{FAKE_ID}/action' body = {"os-extend": {"new_size": "20"}} - self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion - ) + self.sess.post.assert_called_with(url, json=body) def test_set_volume_readonly(self): sot = volume.Volume(**VOLUME) @@ -166,9 +163,7 @@ def test_set_volume_readonly(self): url = f'volumes/{FAKE_ID}/action' body = {'os-update_readonly_flag': {'readonly': True}} - self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion - ) + self.sess.post.assert_called_with(url, json=body) def test_set_volume_readonly_false(self): sot = volume.Volume(**VOLUME) @@ -177,9 +172,7 @@ def test_set_volume_readonly_false(self): url = f'volumes/{FAKE_ID}/action' body = {'os-update_readonly_flag': {'readonly': False}} - self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion - ) + self.sess.post.assert_called_with(url, json=body) def test_set_volume_bootable(self): sot = volume.Volume(**VOLUME) @@ -188,9 +181,7 @@ def test_set_volume_bootable(self): url = f'volumes/{FAKE_ID}/action' body = {'os-set_bootable': {'bootable': True}} - self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion - ) + self.sess.post.assert_called_with(url, json=body) def test_set_volume_bootable_false(self): sot = volume.Volume(**VOLUME) @@ -199,9 +190,7 @@ def test_set_volume_bootable_false(self): url = f'volumes/{FAKE_ID}/action' body = {'os-set_bootable': {'bootable': False}} - self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion - ) + self.sess.post.assert_called_with(url, json=body) def test_set_image_metadata(self): sot = volume.Volume(**VOLUME) @@ -210,9 +199,7 @@ def test_set_image_metadata(self): url = f'volumes/{FAKE_ID}/action' body = {'os-set_image_metadata': {'foo': 'bar'}} - self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion - ) + self.sess.post.assert_called_with(url, json=body) def test_delete_image_metadata(self): _volume = copy.deepcopy(VOLUME) @@ -229,12 +216,8 @@ def test_delete_image_metadata(self): body_b = {'os-unset_image_metadata': 'baz'} self.sess.post.assert_has_calls( [ - mock.call( - url, json=body_a, microversion=sot._max_microversion - ), - mock.call( - url, json=body_b, microversion=sot._max_microversion - ), + mock.call(url, json=body_a), + mock.call(url, json=body_b), ] ) @@ -245,9 +228,7 @@ def test_delete_image_metadata_item(self): url = f'volumes/{FAKE_ID}/action' body = {'os-unset_image_metadata': 'foo'} - self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion - ) + self.sess.post.assert_called_with(url, json=body) def test_reset_status(self): sot = volume.Volume(**VOLUME) @@ -262,9 +243,7 @@ def test_reset_status(self): 'migration_status': '3', } } - self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion - ) + self.sess.post.assert_called_with(url, json=body) def test_reset_status__single_option(self): sot = volume.Volume(**VOLUME) @@ -277,9 +256,7 @@ def test_reset_status__single_option(self): 'status': '1', } } - self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion - ) + self.sess.post.assert_called_with(url, json=body) def test_attach_instance(self): sot = volume.Volume(**VOLUME) @@ -288,9 +265,7 @@ def test_attach_instance(self): url = f'volumes/{FAKE_ID}/action' body = {'os-attach': {'mountpoint': '1', 'instance_uuid': '2'}} - self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion - ) + self.sess.post.assert_called_with(url, json=body) def test_detach(self): sot = volume.Volume(**VOLUME) @@ -299,9 +274,7 @@ def test_detach(self): url = f'volumes/{FAKE_ID}/action' body = {'os-detach': {'attachment_id': '1'}} - self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion - ) + self.sess.post.assert_called_with(url, json=body) def test_detach_force(self): sot = volume.Volume(**VOLUME) @@ -310,9 +283,7 @@ def test_detach_force(self): url = f'volumes/{FAKE_ID}/action' body = {'os-force_detach': {'attachment_id': '1'}} - self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion - ) + self.sess.post.assert_called_with(url, json=body) def test_unmanage(self): sot = volume.Volume(**VOLUME) @@ -321,9 +292,7 @@ def test_unmanage(self): url = f'volumes/{FAKE_ID}/action' body = {'os-unmanage': None} - self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion - ) + self.sess.post.assert_called_with(url, json=body) def test_retype(self): sot = volume.Volume(**VOLUME) @@ -332,9 +301,7 @@ def test_retype(self): url = f'volumes/{FAKE_ID}/action' body = {'os-retype': {'new_type': '1'}} - self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion - ) + self.sess.post.assert_called_with(url, json=body) def test_retype_mp(self): sot = volume.Volume(**VOLUME) @@ -343,9 +310,7 @@ def test_retype_mp(self): url = f'volumes/{FAKE_ID}/action' body = {'os-retype': {'new_type': '1', 'migration_policy': '2'}} - self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion - ) + self.sess.post.assert_called_with(url, json=body) def test_migrate(self): sot = volume.Volume(**VOLUME) @@ -354,9 +319,7 @@ def test_migrate(self): url = f'volumes/{FAKE_ID}/action' body = {'os-migrate_volume': {'host': '1'}} - self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion - ) + self.sess.post.assert_called_with(url, json=body) def test_migrate_flags(self): sot = volume.Volume(**VOLUME) @@ -375,9 +338,7 @@ def test_migrate_flags(self): 'lock_volume': True, } } - self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion - ) + self.sess.post.assert_called_with(url, json=body) def test_complete_migration(self): sot = volume.Volume(**VOLUME) @@ -388,9 +349,7 @@ def test_complete_migration(self): body = { 'os-migrate_volume_completion': {'new_volume': '1', 'error': False} } - self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion - ) + self.sess.post.assert_called_with(url, json=body) def test_complete_migration_error(self): sot = volume.Volume(**VOLUME) @@ -403,9 +362,7 @@ def test_complete_migration_error(self): body = { 'os-migrate_volume_completion': {'new_volume': '1', 'error': True} } - self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion - ) + self.sess.post.assert_called_with(url, json=body) def test_force_delete(self): sot = volume.Volume(**VOLUME) @@ -414,6 +371,4 @@ def test_force_delete(self): url = f'volumes/{FAKE_ID}/action' body = {'os-force_delete': None} - self.sess.post.assert_called_with( - url, json=body, microversion=sot._max_microversion - ) + self.sess.post.assert_called_with(url, json=body) From 8a5143e5e26194f42afbcfb11e59ac0464dc8f9e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 28 Mar 2025 09:12:04 +0000 Subject: [PATCH 3688/3836] volume: Add update_snapshot to v2 API Change-Id: I1df027ac2094c6a0c9ded81a90b050bac3381d25 Signed-off-by: Stephen Finucane --- openstack/block_storage/v2/_proxy.py | 12 ++++++++++++ openstack/tests/unit/block_storage/v2/test_proxy.py | 3 +++ 2 files changed, 15 insertions(+) diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index ead6a3e0d..3484d2f60 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -182,6 +182,18 @@ def create_snapshot(self, **attrs): """ return self._create(_snapshot.Snapshot, **attrs) + def update_snapshot(self, snapshot, **attrs): + """Update a snapshot + + :param snapshot: Either the ID of a snapshot or a + :class:`~openstack.block_storage.v2.snapshot.Snapshot` instance. + :param dict attrs: The attributes to update on the snapshot. + + :returns: The updated snapshot + :rtype: :class:`~openstack.block_storage.v2.snapshot.Snapshot` + """ + return self._update(_snapshot.Snapshot, snapshot, **attrs) + def delete_snapshot(self, snapshot, ignore_missing=True): """Delete a snapshot diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index 37124fc4b..c97dd7044 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -399,6 +399,9 @@ def test_snapshots_not_detailed(self): def test_snapshot_create_attrs(self): self.verify_create(self.proxy.create_snapshot, snapshot.Snapshot) + def test_snapshot_update(self): + self.verify_update(self.proxy.update_snapshot, snapshot.Snapshot) + def test_snapshot_delete(self): self.verify_delete( self.proxy.delete_snapshot, snapshot.Snapshot, False From 54faf1b2380b92612fce56a50e00ef1b44dc9cbd Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 27 Feb 2025 10:52:41 +0000 Subject: [PATCH 3689/3836] tests: Remove redundant _set_operator_cloud calls None of these were actually using the configured connection. Change-Id: I64f109a8d7bcaccdcfac0252a8a4ceab461d692f Signed-off-by: Stephen Finucane --- openstack/tests/functional/compute/v2/test_keypair.py | 3 +-- openstack/tests/functional/compute/v2/test_server.py | 1 - openstack/tests/functional/compute/v2/test_service.py | 4 ---- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/openstack/tests/functional/compute/v2/test_keypair.py b/openstack/tests/functional/compute/v2/test_keypair.py index 753be8106..fbc80ec73 100644 --- a/openstack/tests/functional/compute/v2/test_keypair.py +++ b/openstack/tests/functional/compute/v2/test_keypair.py @@ -51,10 +51,9 @@ def test_list(self): class TestKeypairAdmin(base.BaseFunctionalTest): def setUp(self): super().setUp() - self._set_operator_cloud(interface='admin') self.NAME = self.getUniqueString().split('.')[-1] - self.USER = self.operator_cloud.list_users()[0] + self.USER = self.conn.list_users()[0] sot = self.conn.compute.create_keypair( name=self.NAME, user_id=self.USER.id diff --git a/openstack/tests/functional/compute/v2/test_server.py b/openstack/tests/functional/compute/v2/test_server.py index 215e7ccb8..0c679b351 100644 --- a/openstack/tests/functional/compute/v2/test_server.py +++ b/openstack/tests/functional/compute/v2/test_server.py @@ -18,7 +18,6 @@ class TestServerAdmin(ft_base.BaseComputeTest): def setUp(self): super().setUp() - self._set_operator_cloud(interface='admin') self.NAME = 'needstobeshortandlowercase' self.USERDATA = 'SSdtIGFjdHVhbGx5IGEgZ29hdC4=' volume = self.conn.create_volume(1) diff --git a/openstack/tests/functional/compute/v2/test_service.py b/openstack/tests/functional/compute/v2/test_service.py index e2fd99d20..c29da9fed 100644 --- a/openstack/tests/functional/compute/v2/test_service.py +++ b/openstack/tests/functional/compute/v2/test_service.py @@ -14,10 +14,6 @@ class TestService(base.BaseFunctionalTest): - def setUp(self): - super().setUp() - self._set_operator_cloud(interface='admin') - def test_list(self): sot = list(self.conn.compute.services()) self.assertIsNotNone(sot) From 7aa5a9d0c745d3d77cf1a389e59c5acb03a4052b Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 20 Nov 2024 12:57:20 +0000 Subject: [PATCH 3690/3836] typing: Annotate openstack.proxy There are a quite a few hints included here for the Resource class, but only because the Proxy class needs them typed. Change-Id: I1e03c5cc2c1f33bd04b026c714a6508a4b9332ed Signed-off-by: Stephen Finucane --- openstack/block_storage/v3/_proxy.py | 10 +- openstack/cloud/_network_common.py | 2 - openstack/config/cloud_region.py | 43 +- openstack/proxy.py | 381 +++++++++++++----- openstack/resource.py | 156 ++++--- .../v2/share_access_rule.py | 4 +- pyproject.toml | 1 + 7 files changed, 406 insertions(+), 191 deletions(-) diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 19eb751f2..86435d35a 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -1499,14 +1499,18 @@ def get_limits(self, project=None): :class:`~openstack.block_storage.v3.limits.RateLimit` :rtype: :class:`~openstack.block_storage.v3.limits.Limits` """ - params = {} + project_id = None if project: - params['project_id'] = resource.Resource._get_id(project) + project_id = resource.Resource._get_id(project) # we don't use Proxy._get since that doesn't allow passing arbitrary # query string parameters res = self._get_resource(_limits.Limits, None) - return res.fetch(self, requires_id=False, **params) + return res.fetch( + self, + requires_id=False, + project_id=project_id, + ) # ====== CAPABILITIES ====== def get_capabilities(self, host): diff --git a/openstack/cloud/_network_common.py b/openstack/cloud/_network_common.py index e80995bce..99ebe0495 100644 --- a/openstack/cloud/_network_common.py +++ b/openstack/cloud/_network_common.py @@ -1729,7 +1729,6 @@ def list_security_groups(self, filters=None): if not filters: filters = {} - data = [] # Handle neutron security groups if self._use_neutron_secgroups(): # pass filters dict to the list to filter as much as possible on @@ -1815,7 +1814,6 @@ def create_security_group( "Unavailable feature: security groups" ) - data = [] security_group_json = {'name': name, 'description': description} if stateful is not None: security_group_json['stateful'] = stateful diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 6a176f143..c4cd9cc08 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -14,6 +14,7 @@ import copy import os.path +import typing as ty from urllib import parse import warnings @@ -30,17 +31,17 @@ import requestsexceptions try: - import statsd + import statsd as statsd_client except ImportError: - statsd = None + statsd_client = None try: import prometheus_client except ImportError: prometheus_client = None try: - import influxdb + import influxdb as influxdb_client except ImportError: - influxdb = None + influxdb_client = None from openstack import _log from openstack.config import _util @@ -1131,8 +1132,10 @@ def get_concurrency(self, service_type=None): 'concurrency', service_type=service_type ) - def get_statsd_client(self): - if not statsd: + def get_statsd_client( + self, + ) -> ty.Optional['statsd_client.StatsClientBase']: + if not statsd_client: if self._statsd_host: self.log.warning( 'StatsD python library is not available. ' @@ -1146,25 +1149,29 @@ def get_statsd_client(self): statsd_args['port'] = self._statsd_port if statsd_args: try: - return statsd.StatsClient(**statsd_args) + return statsd_client.StatsClient(**statsd_args) except Exception: self.log.warning('Cannot establish connection to statsd') return None else: return None - def get_statsd_prefix(self): + def get_statsd_prefix(self) -> str: return self._statsd_prefix or 'openstack.api' - def get_prometheus_registry(self): + def get_prometheus_registry( + self, + ) -> ty.Optional['prometheus_client.CollectorRegistry']: if not self._collector_registry and prometheus_client: self._collector_registry = prometheus_client.REGISTRY return self._collector_registry - def get_prometheus_histogram(self): + def get_prometheus_histogram( + self, + ) -> ty.Optional['prometheus_client.Histogram']: registry = self.get_prometheus_registry() if not registry or not prometheus_client: - return + return None # We have to hide a reference to the histogram on the registry # object, because it's collectors must be singletons for a given # registry but register at creation time. @@ -1184,10 +1191,12 @@ def get_prometheus_histogram(self): registry._openstacksdk_histogram = hist return hist - def get_prometheus_counter(self): + def get_prometheus_counter( + self, + ) -> ty.Optional['prometheus_client.Counter']: registry = self.get_prometheus_registry() if not registry or not prometheus_client: - return + return None counter = getattr(registry, '_openstacksdk_counter', None) if not counter: counter = prometheus_client.Counter( @@ -1224,7 +1233,9 @@ def get_disabled_reason(self, service_type): d_key = _make_key('disabled_reason', service_type) return self.config.get(d_key) - def get_influxdb_client(self): + def get_influxdb_client( + self, + ) -> ty.Optional['influxdb_client.InfluxDBClient']: influx_args = {} if not self._influxdb_config: return None @@ -1240,9 +1251,9 @@ def get_influxdb_client(self): for key in ['host', 'username', 'password', 'database', 'timeout']: if key in self._influxdb_config: influx_args[key] = self._influxdb_config[key] - if influxdb and influx_args: + if influxdb_client and influx_args: try: - return influxdb.InfluxDBClient(**influx_args) + return influxdb_client.InfluxDBClient(**influx_args) except Exception: self.log.warning('Cannot establish connection to InfluxDB') else: diff --git a/openstack/proxy.py b/openstack/proxy.py index 6ca840ab6..b13786c6c 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -10,7 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. +# This is needed due to https://github.com/eventlet/eventlet/issues/1026 which +# nova (and possibly others) expose +from __future__ import annotations + +import collections.abc import functools +import logging +import queue import typing as ty import urllib from urllib.parse import urlparse @@ -25,6 +32,7 @@ import iso8601 import jmespath from keystoneauth1 import adapter +from keystoneauth1 import session from openstack import _log from openstack import exceptions @@ -33,19 +41,30 @@ from openstack import warnings as os_warnings if ty.TYPE_CHECKING: + import influxdb as influxdb_client + from keystoneauth1 import plugin + import prometheus_client + import requests + from statsd.client import base as statsd_client + from openstack import connection -def normalize_metric_name(name): +def normalize_metric_name(name: str) -> str: name = name.replace('.', '_') name = name.replace(':', '_') return name +class CleanupDependency(ty.TypedDict): + before: list[str] + after: list[str] + + class Proxy(adapter.Adapter): """Represents a service.""" - retriable_status_codes: ty.Optional[list[int]] = None + retriable_status_codes: list[int] | None = None """HTTP status codes that should be retried by default. The number of retries is defined by the configuration in parameters called @@ -58,28 +77,82 @@ class Proxy(adapter.Adapter): Dictionary of resource names (key) types (value). """ - _connection: 'connection.Connection' + _connection: connection.Connection def __init__( self, - session, - statsd_client=None, - statsd_prefix=None, - prometheus_counter=None, - prometheus_histogram=None, - influxdb_config=None, - influxdb_client=None, - *args, - **kwargs, + session: session.Session, + *, + service_type: str | None = None, + service_name: str | None = None, + interface: str | None = None, + region_name: str | None = None, + endpoint_override: str | None = None, + version: str | None = None, + auth: plugin.BaseAuthPlugin | None = None, + user_agent: str | None = None, + connect_retries: int | None = None, + logger: logging.Logger | None = None, + allow: dict[str, ty.Any] | None = None, + additional_headers: collections.abc.MutableMapping[str, str] + | None = None, + client_name: str | None = None, + client_version: str | None = None, + allow_version_hack: bool | None = None, + global_request_id: str | None = None, + min_version: str | None = None, + max_version: str | None = None, + default_microversion: str | None = None, + status_code_retries: int | None = None, + retriable_status_codes: list[int] | None = None, + raise_exc: bool | None = None, + rate_limit: float | None = None, + concurrency: int | None = None, + connect_retry_delay: float | None = None, + status_code_retry_delay: float | None = None, + # everything from here on is SDK-specific + statsd_client: statsd_client.StatsClient | None = None, + statsd_prefix: str | None = None, + prometheus_counter: prometheus_client.Counter | None = None, + prometheus_histogram: prometheus_client.Histogram | None = None, + influxdb_config: dict[str, ty.Any] | None = None, + influxdb_client: influxdb_client.InfluxDBClient | None = None, ): # NOTE(dtantsur): keystoneauth defaults retriable_status_codes to None, # override it with a class-level value. - kwargs.setdefault( - 'retriable_status_codes', self.retriable_status_codes + if retriable_status_codes is None: + retriable_status_codes = self.retriable_status_codes + + super().__init__( + session=session, + service_type=service_type, + service_name=service_name, + interface=interface, + region_name=region_name, + endpoint_override=endpoint_override, + version=version, + auth=auth, + user_agent=user_agent, + connect_retries=connect_retries, + logger=logger, + allow=allow, + additional_headers=additional_headers, + client_name=client_name, + client_version=client_version, + allow_version_hack=allow_version_hack, + global_request_id=global_request_id, + min_version=min_version, + max_version=max_version, + default_microversion=default_microversion, + status_code_retries=status_code_retries, + retriable_status_codes=retriable_status_codes, + raise_exc=raise_exc, + rate_limit=rate_limit, + concurrency=concurrency, + connect_retry_delay=connect_retry_delay, + status_code_retry_delay=status_code_retry_delay, ) - # TODO(stephenfin): Resolve this by copying the signature of - # adapter.Adapter.__init__ - super().__init__(session=session, *args, **kwargs) # type: ignore + self._statsd_client = statsd_client self._statsd_prefix = statsd_prefix self._prometheus_counter = prometheus_counter @@ -92,15 +165,23 @@ def __init__( log_name = 'openstack' self.log = _log.setup_logging(log_name) - def _get_cache_key_prefix(self, url): + def _get_cache_key_prefix(self, url: str) -> str: """Calculate cache prefix for the url""" + if not self.service_type: + # narrow type + raise RuntimeError('expected service_type to be set') + name_parts = self._extract_name( url, self.service_type, self.session.get_project_id() ) return '.'.join([self.service_type] + name_parts) - def _invalidate_cache(self, conn, key_prefix): + def _invalidate_cache( + self, + conn: connection.Connection, + key_prefix: str, + ) -> None: """Invalidate all cache entries starting with given prefix""" for k in set(conn._api_cache_keys): if k.startswith(key_prefix): @@ -109,16 +190,20 @@ def _invalidate_cache(self, conn, key_prefix): def request( self, - url, - method, - error_message=None, - raise_exc=False, - connect_retries=1, - global_request_id=None, - *args, - **kwargs, - ): + url: str, + method: str, + error_message: str | None = None, + raise_exc: bool = False, + connect_retries: int = 1, + global_request_id: str | None = None, + *args: ty.Any, + **kwargs: ty.Any, + ) -> requests.Response: conn = self._get_connection() + if not conn: + # narrow type + raise RuntimeError('no connection found') + if not global_request_id: # Per-request setting should take precedence global_request_id = conn._global_request_id @@ -143,20 +228,21 @@ def request( conn._cache_expirations.get(key_prefix, 0) ) # Get from cache or execute and cache - response = conn._cache.get_or_create( + _response = conn._cache.get_or_create( key=key, creator=super().request, creator_args=( [url, method], - dict( - connect_retries=connect_retries, - raise_exc=raise_exc, - global_request_id=global_request_id, + { + 'connect_retries': connect_retries, + 'raise_exc': raise_exc, + 'global_request_id': global_request_id, **kwargs, - ), + }, ), expiration_time=expiration_time, ) + response = ty.cast('requests.Response', _response) else: # invalidate cache if we send modification request or user # asked for cache bypass @@ -184,7 +270,12 @@ def request( raise @functools.lru_cache(maxsize=256) - def _extract_name(self, url, service_type=None, project_id=None): + def _extract_name( + self, + url: str, + service_type: str | None = None, + project_id: str | None = None, + ) -> list[str]: """Produce a key name to use in logging/metrics from the URL path. We want to be able to logic/metric sane general things, so we pull @@ -260,7 +351,9 @@ def _extract_name(self, url, service_type=None, project_id=None): # Strip out anything that's empty or None return [part for part in name_parts if part] - def _extract_name_consume_url_parts(self, url_parts): + def _extract_name_consume_url_parts( + self, url_parts: list[str] + ) -> list[str]: """Pull out every other URL portion. For example, ``GET /servers/{id}/os-security-groups`` returns @@ -282,20 +375,41 @@ def _extract_name_consume_url_parts(self, url_parts): return name_parts - def _report_stats(self, response, url=None, method=None, exc=None): - if self._statsd_client: - self._report_stats_statsd(response, url, method, exc) - if self._prometheus_counter and self._prometheus_histogram: - self._report_stats_prometheus(response, url, method, exc) - if self._influxdb_client: - self._report_stats_influxdb(response, url, method, exc) + def _report_stats( + self, + response: requests.Response | None, + url: str | None = None, + method: str | None = None, + exc: BaseException | None = None, + ) -> None: + self._report_stats_statsd(response, url, method, exc) + self._report_stats_prometheus(response, url, method, exc) + self._report_stats_influxdb(response, url, method, exc) + + def _report_stats_statsd( + self, + response: requests.Response | None, + url: str | None = None, + method: str | None = None, + exc: BaseException | None = None, + ) -> None: + if not self._statsd_prefix: + return None + + if not self._statsd_client: + return None - def _report_stats_statsd(self, response, url=None, method=None, exc=None): try: if response is not None and not url: url = response.request.url if response is not None and not method: method = response.request.method + + # narrow types + assert url is not None + assert method is not None + assert self.service_type is not None + name_parts = [ normalize_metric_name(f) for f in self._extract_name( @@ -326,31 +440,51 @@ def _report_stats_statsd(self, response, url=None, method=None, exc=None): self.log.exception("Exception reporting metrics") def _report_stats_prometheus( - self, response, url=None, method=None, exc=None - ): + self, + response: requests.Response | None, + url: str | None = None, + method: str | None = None, + exc: BaseException | None = None, + ) -> None: + if not self._prometheus_counter: + return None + + if not self._prometheus_histogram: + return None + if response is not None and not url: url = response.request.url if response is not None and not method: method = response.request.method parsed_url = urlparse(url) endpoint = ( - f"{parsed_url.scheme}://{parsed_url.netloc}{parsed_url.path}" + f"{parsed_url.scheme}://{parsed_url.netloc}{parsed_url.path}" # type: ignore[str-bytes-safe] ) if response is not None: - labels = dict( - method=method, - endpoint=endpoint, - service_type=self.service_type, - status_code=response.status_code, - ) + labels = { + 'method': method, + 'endpoint': endpoint, + 'service_type': self.service_type, + 'status_code': response.status_code, + } self._prometheus_counter.labels(**labels).inc() self._prometheus_histogram.labels(**labels).observe( response.elapsed.total_seconds() * 1000 ) def _report_stats_influxdb( - self, response, url=None, method=None, exc=None - ): + self, + response: requests.Response | None, + url: str | None = None, + method: str | None = None, + exc: BaseException | None = None, + ) -> None: + if not self._influxdb_client: + return None + + if not self._influxdb_config: + return None + # NOTE(gtema): status_code is saved both as tag and field to give # ability showing it as a value and not only as a legend. # However Influx is not ok with having same name in tags and fields, @@ -359,9 +493,9 @@ def _report_stats_influxdb( url = response.request.url if response is not None and not method: method = response.request.method - tags = dict( - method=method, - name='_'.join( + tags = { + 'method': method, + 'name': '_'.join( [ normalize_metric_name(f) for f in self._extract_name( @@ -369,8 +503,8 @@ def _report_stats_influxdb( ) ] ), - ) - fields = dict(attempted=1) + } + fields = {'attempted': 1} if response is not None: fields['duration'] = int(response.elapsed.total_seconds() * 1000) tags['status_code'] = str(response.status_code) @@ -392,13 +526,13 @@ def _report_stats_influxdb( ) # Note(gtema) append service name into the measurement name measurement = f'{measurement}.{self.service_type}' - data = [dict(measurement=measurement, tags=tags, fields=fields)] + data = [{'measurement': measurement, 'tags': tags, 'fields': fields}] try: self._influxdb_client.write_points(data) except Exception: self.log.exception('Error writing statistics to InfluxDB') - def _get_connection(self): + def _get_connection(self) -> connection.Connection | None: """Get the Connection object associated with this Proxy. When the Session is created, a reference to the Connection is attached @@ -412,7 +546,7 @@ def _get_connection(self): def _get_resource( self, resource_type: type[resource.ResourceT], - value: ty.Union[None, str, resource.ResourceT, utils.Munch], + value: None | str | resource.ResourceT | utils.Munch, **attrs: ty.Any, ) -> resource.ResourceT: """Get a resource object to work on @@ -450,7 +584,12 @@ class if using an existing instance, or ``utils.Munch``, return res - def _get_uri_attribute(self, child, parent, name): + def _get_uri_attribute( + self, + child: resource.Resource | str, + parent: resource.Resource | str | None, + name: str, + ) -> str: """Get a value to be associated with a URI attribute `child` will not be None here as it's a required argument @@ -461,9 +600,10 @@ def _get_uri_attribute(self, child, parent, name): """ if parent is None: value = getattr(child, name) - else: - value = resource.Resource._get_id(parent) - return value + assert isinstance(value, str) # narrow type + return value + + return resource.Resource._get_id(parent) @ty.overload def _find( @@ -472,7 +612,7 @@ def _find( name_or_id: str, ignore_missing: ty.Literal[True] = True, **attrs: ty.Any, - ) -> ty.Optional[resource.ResourceT]: ... + ) -> resource.ResourceT | None: ... @ty.overload def _find( @@ -492,7 +632,7 @@ def _find( name_or_id: str, ignore_missing: bool, **attrs: ty.Any, - ) -> ty.Optional[resource.ResourceT]: ... + ) -> resource.ResourceT | None: ... def _find( self, @@ -500,7 +640,7 @@ def _find( name_or_id: str, ignore_missing: bool = True, **attrs: ty.Any, - ) -> ty.Optional[resource.ResourceT]: + ) -> resource.ResourceT | None: """Find a resource :param resource_type: The type of resource to find. This should be a @@ -524,10 +664,10 @@ def _find( def _delete( self, resource_type: type[resource.ResourceT], - value: ty.Union[str, resource.ResourceT, None], + value: str | resource.ResourceT | None, ignore_missing: bool = True, **attrs: ty.Any, - ) -> ty.Optional[resource.ResourceT]: + ) -> resource.ResourceT | None: """Delete a resource :param resource_type: The type of resource to delete. This should be a @@ -566,8 +706,8 @@ def _delete( def _update( self, resource_type: type[resource.ResourceT], - value: ty.Union[str, resource.ResourceT, None], - base_path: ty.Optional[str] = None, + value: str | resource.ResourceT | None, + base_path: str | None = None, **attrs: ty.Any, ) -> resource.ResourceT: """Update a resource @@ -598,7 +738,7 @@ def _update( def _create( self, resource_type: type[resource.ResourceT], - base_path: ty.Optional[str] = None, + base_path: str | None = None, **attrs: ty.Any, ) -> resource.ResourceT: """Create a resource from attributes @@ -632,7 +772,7 @@ def _bulk_create( self, resource_type: type[resource.ResourceT], data: list[dict[str, ty.Any]], - base_path: ty.Optional[str] = None, + base_path: str | None = None, ) -> ty.Generator[resource.ResourceT, None, None]: """Create a resource from attributes @@ -656,9 +796,9 @@ def _bulk_create( def _get( self, resource_type: type[resource.ResourceT], - value: ty.Union[str, resource.ResourceT, None] = None, + value: str | resource.ResourceT | None = None, requires_id: bool = True, - base_path: ty.Optional[str] = None, + base_path: str | None = None, skip_cache: bool = False, **attrs: ty.Any, ) -> resource.ResourceT: @@ -701,8 +841,8 @@ def _list( self, resource_type: type[resource.ResourceT], paginated: bool = True, - base_path: ty.Optional[str] = None, - jmespath_filters: ty.Optional[str] = None, + base_path: str | None = None, + jmespath_filters: str | None = None, **attrs: ty.Any, ) -> ty.Generator[resource.ResourceT, None, None]: """List a resource @@ -748,15 +888,15 @@ def _list( 'removed in a future release.', os_warnings.RemovedInSDK60Warning, ) - return jmespath.search(jmespath_filters, data) + return jmespath.search(jmespath_filters, data) # type: ignore[no-any-return] return data def _head( self, resource_type: type[resource.ResourceT], - value: ty.Union[str, resource.ResourceT, None] = None, - base_path: ty.Optional[str] = None, + value: str | resource.ResourceT | None = None, + base_path: str | None = None, **attrs: ty.Any, ) -> resource.ResourceT: """Retrieve a resource's header @@ -781,30 +921,50 @@ def _head( res = self._get_resource(resource_type, value, **attrs) return res.head(self, base_path=base_path) - def _get_cleanup_dependencies(self): + def _get_cleanup_dependencies( + self, + ) -> dict[str, CleanupDependency] | None: return None + # TODO(stephenfin): Add type for filters. We expect the created_at or + # updated_at keys def _service_cleanup( self, - dry_run=True, - client_status_queue=None, - identified_resources=None, - filters=None, - resource_evaluation_fn=None, - skip_resources=None, - ): + dry_run: bool = True, + client_status_queue: queue.Queue[resource.Resource] | None = None, + identified_resources: dict[str, resource.Resource] | None = None, + filters: dict[str, ty.Any] | None = None, + resource_evaluation_fn: ty.Callable[ + [ + resource.Resource, + dict[str, ty.Any] | None, + dict[str, resource.Resource] | None, + ], + bool, + ] + | None = None, + skip_resources: list[str] | None = None, + ) -> None: return None def _service_cleanup_del_res( self, - del_fn, - obj, - dry_run=True, - client_status_queue=None, - identified_resources=None, - filters=None, - resource_evaluation_fn=None, - ): + del_fn: ty.Callable[[resource.Resource], None], + obj: resource.Resource, + dry_run: bool = True, + client_status_queue: queue.Queue[resource.Resource] | None = None, + identified_resources: dict[str, resource.Resource] | None = None, + filters: dict[str, ty.Any] | None = None, + resource_evaluation_fn: ty.Callable[ + [ + resource.Resource, + dict[str, ty.Any] | None, + dict[str, resource.Resource] | None, + ], + bool, + ] + | None = None, + ) -> bool: need_delete = False try: if resource_evaluation_fn and callable(resource_evaluation_fn): @@ -838,7 +998,11 @@ def _service_cleanup_del_res( self.log.exception('Cannot delete resource %s: %s', obj, str(e)) return need_delete - def _service_cleanup_resource_filters_evaluation(self, obj, filters=None): + def _service_cleanup_resource_filters_evaluation( + self, + obj: resource.Resource, + filters: dict[str, ty.Any] | None = None, + ) -> bool: part_cond = [] if filters is not None and isinstance(filters, dict): for k, v in filters.items(): @@ -870,8 +1034,10 @@ def _service_cleanup_resource_filters_evaluation(self, obj, filters=None): else: return False - def should_skip_resource_cleanup(self, resource=None, skip_resources=None): - if resource is None or skip_resources is None: + def should_skip_resource_cleanup( + self, resource: str, skip_resources: list[str] | None = None + ) -> bool: + if skip_resources is None: return False if self.service_type is None: @@ -891,7 +1057,10 @@ def should_skip_resource_cleanup(self, resource=None, skip_resources=None): # TODO(stephenfin): Remove this and all users. Use of this generally indicates # a missing Resource type. -def _json_response(response, result_key=None, error_message=None): +def _json_response( + response: requests.Response, + error_message: str | None = None, +) -> requests.Response | ty.Any: """Temporary method to use to bridge from ShadeAdapter to SDK calls.""" exceptions.raise_from_response(response, error_message=error_message) @@ -900,11 +1069,11 @@ def _json_response(response, result_key=None, error_message=None): return response # Some REST calls do not return json content. Don't decode it. - if 'application/json' not in response.headers.get('Content-Type'): + content_type = response.headers.get('Content-Type') + if not content_type or 'application/json' not in content_type: return response try: - result_json = response.json() + return response.json() except JSONDecodeError: return response - return result_json diff --git a/openstack/resource.py b/openstack/resource.py index 9caef9029..0b83ed696 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -51,9 +51,13 @@ class that represent a remote resource. The attributes that from openstack import utils from openstack import warnings as os_warnings +if ty.TYPE_CHECKING: + from openstack import connection + LOG = _log.setup_logging(__name__) AdapterT = ty.TypeVar('AdapterT', bound=adapter.Adapter) +ResourceT = ty.TypeVar('ResourceT', bound='Resource') # TODO(stephenfin): We should deprecate the 'type' and 'list_type' arguments @@ -354,7 +358,7 @@ class Resource(dict): pagination_key: ty.Optional[str] = None #: The ID of this resource. - id = Body("id") + id: str = Body("id") #: The name of this resource. name: str = Body("name") @@ -658,7 +662,7 @@ def items(self): res.append((attr, self[attr])) return res - def _update(self, **attrs): + def _update(self, **attrs: ty.Any) -> None: """Given attributes, update them on this instance This is intended to be used from within the proxy @@ -858,7 +862,7 @@ def _alternate_id(cls): return "" @staticmethod - def _get_id(value): + def _get_id(value: ty.Union['Resource', str]) -> str: """If a value is a Resource, return the canonical ID This will return either the value specified by `id` or @@ -872,7 +876,7 @@ def _get_id(value): return value @classmethod - def new(cls, **kwargs): + def new(cls, **kwargs: ty.Any) -> ty_ext.Self: """Create a new instance of this resource. When creating the instance set the ``_synchronized`` parameter @@ -903,7 +907,12 @@ def existing(cls, connection=None, **kwargs): return cls(_synchronized=True, connection=connection, **kwargs) @classmethod - def _from_munch(cls, obj, synchronized=True, connection=None): + def _from_munch( + cls, + obj: dict[str, ty.Union], + synchronized: bool = True, + connection: ty.Optional['connection.Connection'] = None, + ) -> ty_ext.Self: """Create an instance from a ``utils.Munch`` object. This is intended as a temporary measure to convert between shade-style @@ -1325,15 +1334,15 @@ def _raise(message: str) -> ty.NoReturn: def create( self, - session, - prepend_key=True, - base_path=None, + session: adapter.Adapter, + prepend_key: bool = True, + base_path: ty.Optional[str] = None, *, - resource_request_key=None, - resource_response_key=None, - microversion=None, - **params, - ): + resource_request_key: ty.Optional[str] = None, + resource_response_key: ty.Optional[str] = None, + microversion: ty.Optional[str] = None, + **params: ty.Any, + ) -> ty_ext.Self: """Create a remote resource based on this instance. :param session: The session to use for making this request. @@ -1417,23 +1426,23 @@ def create( # direct comparision to False since we need to rule out None if self.has_body and self.create_returns_body is False: # fetch the body if it's required but not returned by create - fetch_kwargs = {} - if resource_response_key is not None: - fetch_kwargs = {'resource_response_key': resource_response_key} - return self.fetch(session, **fetch_kwargs) + return self.fetch( + session, + resource_response_key=resource_response_key, + ) return self @classmethod def bulk_create( cls, - session, - data, - prepend_key=True, - base_path=None, + session: adapter.Adapter, + data: list[dict[str, ty.Any]], + prepend_key: bool = True, + base_path: ty.Optional[str] = None, *, - microversion=None, - **params, - ): + microversion: ty.Optional[str] = None, + **params: ty.Any, + ) -> ty.Generator[ty_ext.Self, None, None]: """Create multiple remote resources based on this class and data. :param session: The session to use for making this request. @@ -1486,7 +1495,10 @@ def bulk_create( # Those objects will be used in case where request doesn't return # JSON data representing created resource, and yet it's required # to return newly created resource objects. - resource = cls.new(connection=session._get_connection(), **attrs) + # TODO(stephenfin): Our types say we accept a ksa Adapter, but this + # requires an SDK Proxy. Do we update the types or rework this to + # support use of an adapter. + resource = cls.new(connection=session._get_connection(), **attrs) # type: ignore resources.append(resource) request = resource._prepare_request( requires_id=requires_id, base_path=base_path @@ -1507,13 +1519,17 @@ def bulk_create( params=params, ) exceptions.raise_from_response(response) - data = response.json() + json = response.json() if cls.resources_key: - data = data[cls.resources_key] + json = json[cls.resources_key] + else: + json = json - if not isinstance(data, list): - data = [data] + if isinstance(data, list): + json = json + else: + json = [json] has_body = ( cls.has_body @@ -1524,26 +1540,29 @@ def bulk_create( return (r.fetch(session) for r in resources) else: return ( + # TODO(stephenfin): Our types say we accept a ksa Adapter, but + # this requires an SDK Proxy. Do we update the types or rework + # this to support use of an adapter. cls.existing( microversion=microversion, - connection=session._get_connection(), + connection=session._get_connection(), # type: ignore **res_dict, ) - for res_dict in data + for res_dict in json ) def fetch( self, - session, - requires_id=True, - base_path=None, - error_message=None, - skip_cache=False, + session: adapter.Adapter, + requires_id: bool = True, + base_path: ty.Optional[str] = None, + error_message: ty.Optional[str] = None, + skip_cache: bool = False, *, - resource_response_key=None, - microversion=None, - **params, - ): + resource_response_key: ty.Optional[str] = None, + microversion: ty.Optional[str] = None, + **params: ty.Any, + ) -> ty_ext.Self: """Get a remote resource based on this instance. :param session: The session to use for making this request. @@ -1594,7 +1613,13 @@ def fetch( return self - def head(self, session, base_path=None, *, microversion=None): + def head( + self, + session: adapter.Adapter, + base_path: ty.Optional[str] = None, + *, + microversion: ty.Optional[str] = None, + ) -> ty_ext.Self: """Get headers from a remote resource based on this instance. :param session: The session to use for making this request. @@ -1633,15 +1658,15 @@ def requires_commit(self): def commit( self, - session, - prepend_key=True, - has_body=True, - retry_on_conflict=None, - base_path=None, + session: adapter.Adapter, + prepend_key: bool = True, + has_body: bool = True, + retry_on_conflict: ty.Optional[bool] = None, + base_path: ty.Optional[str] = None, *, - microversion=None, - **kwargs, - ): + microversion: ty.Optional[str] = None, + **kwargs: ty.Any, + ) -> ty_ext.Self: """Commit the state of the instance to the remote resource. :param session: The session to use for making this request. @@ -1826,8 +1851,13 @@ def patch( ) def delete( - self, session, error_message=None, *, microversion=None, **kwargs - ): + self, + session: adapter.Adapter, + error_message: ty.Optional[str] = None, + *, + microversion: ty.Optional[str] = None, + **kwargs: ty.Any, + ) -> ty_ext.Self: """Delete the remote resource based on this instance. :param session: The session to use for making this request. @@ -1873,15 +1903,15 @@ def _raw_delete(self, session, microversion=None, **kwargs): @classmethod def list( cls, - session, - paginated=True, - base_path=None, - allow_unknown_params=False, + session: adapter.Adapter, + paginated: bool = True, + base_path: ty.Optional[str] = None, + allow_unknown_params: bool = False, *, - microversion=None, - headers=None, - **params, - ): + microversion: ty.Optional[str] = None, + headers: ty.Optional[dict[str, str]] = None, + **params: ty.Any, + ) -> ty.Generator[ty_ext.Self, None, None]: """This method is a generator which yields resource objects. This resource object list generator handles pagination and takes query @@ -2012,9 +2042,12 @@ def _dict_filter(f, d): # We want that URI props are available on the resource raw_resource.update(uri_params) + # TODO(stephenfin): Our types say we accept a ksa Adapter, but + # this requires an SDK Proxy. Do we update the types or rework + # this to support use of an adapter. value = cls.existing( microversion=microversion, - connection=session._get_connection(), + connection=session._get_connection(), # type: ignore **raw_resource, ) marker = value.id @@ -2280,9 +2313,6 @@ def _normalize_status(status: ty.Optional[str]) -> ty.Optional[str]: return status -ResourceT = ty.TypeVar('ResourceT', bound=Resource) - - def wait_for_status( session: adapter.Adapter, resource: ResourceT, diff --git a/openstack/shared_file_system/v2/share_access_rule.py b/openstack/shared_file_system/v2/share_access_rule.py index 06cc78a84..1a1f7932d 100644 --- a/openstack/shared_file_system/v2/share_access_rule.py +++ b/openstack/shared_file_system/v2/share_access_rule.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + from openstack import resource from openstack import utils @@ -91,7 +93,7 @@ def delete( unrestrict=False, **kwargs, ): - body = {'deny_access': {'access_id': self.id}} + body: dict[str, ty.Any] = {'deny_access': {'access_id': self.id}} if unrestrict: body['deny_access']['unrestrict'] = True url = utils.urljoin("/shares", self.share_id, "action") diff --git a/pyproject.toml b/pyproject.toml index 1fab36c2d..621605328 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ module = [ "openstack.exceptions", "openstack.fields", "openstack.format", + "openstack.proxy", "openstack.utils", ] warn_return_any = true From 4d85cf076a071d1d774a8a788009010e656d30ba Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 25 Feb 2025 13:42:19 +0000 Subject: [PATCH 3691/3836] typing: Annotate openstack.connection Change-Id: Ib3e98f926782469c20007bfe8e7a0a3f451ac14d Signed-off-by: Stephen Finucane --- openstack/cloud/_image.py | 50 ++++++- openstack/cloud/openstackcloud.py | 89 +++++++----- openstack/config/__init__.py | 38 +++-- openstack/config/cloud_region.py | 176 +++++++++++++++++------- openstack/config/loader.py | 41 +++--- openstack/connection.py | 96 ++++++++----- openstack/service_description.py | 18 ++- openstack/tests/unit/test_connection.py | 8 +- pyproject.toml | 2 + 9 files changed, 364 insertions(+), 154 deletions(-) diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py index 4c85837b7..9e67aeb9c 100644 --- a/openstack/cloud/_image.py +++ b/openstack/cloud/_image.py @@ -10,6 +10,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import typing as ty import warnings from openstack.cloud import _utils @@ -18,10 +19,55 @@ from openstack import utils from openstack import warnings as os_warnings +if ty.TYPE_CHECKING: + import concurrent.futures + from keystoneauth1 import session as ks_session + from oslo_config import cfg + + from openstack.config import cloud_region + from openstack import service_description + class ImageCloudMixin(openstackcloud._OpenStackCloudMixin): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__( + self, + cloud: ty.Optional[str] = None, + config: ty.Optional['cloud_region.CloudRegion'] = None, + session: ty.Optional['ks_session.Session'] = None, + app_name: ty.Optional[str] = None, + app_version: ty.Optional[str] = None, + extra_services: ty.Optional[ + list['service_description.ServiceDescription'] + ] = None, + strict: bool = False, + use_direct_get: ty.Optional[bool] = None, + task_manager: ty.Any = None, + rate_limit: ty.Union[float, dict[str, float], None] = None, + oslo_conf: ty.Optional['cfg.ConfigOpts'] = None, + service_types: ty.Optional[list[str]] = None, + global_request_id: ty.Optional[str] = None, + strict_proxies: bool = False, + pool_executor: ty.Optional['concurrent.futures.Executor'] = None, + **kwargs: ty.Any, + ): + super().__init__( + cloud=cloud, + config=config, + session=session, + app_name=app_name, + app_version=app_version, + extra_services=extra_services, + strict=strict, + use_direct_get=use_direct_get, + task_manager=task_manager, + rate_limit=rate_limit, + oslo_conf=oslo_conf, + service_types=service_types, + global_request_id=global_request_id, + strict_proxies=strict_proxies, + pool_executor=pool_executor, + **kwargs, + ) self.image_api_use_tasks = self.config.config['image_api_use_tasks'] diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index f6fdf6cf2..cd176f329 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -15,6 +15,8 @@ import copy import functools import queue +import types +import typing as ty import warnings import weakref @@ -22,19 +24,26 @@ import keystoneauth1.exceptions import requests.models import requestsexceptions +import typing_extensions as ty_ext from openstack import _log from openstack import _services_mixin from openstack.cloud import _utils from openstack.cloud import meta -import openstack.config -import openstack.config.cloud_region +from openstack import config as cloud_config +from openstack.config import cloud_region from openstack import exceptions from openstack import proxy from openstack import resource from openstack import utils from openstack import warnings as os_warnings +if ty.TYPE_CHECKING: + from keystoneauth1 import session as ks_session + from oslo_config import cfg + + from openstack import service_description + class _OpenStackCloudMixin(_services_mixin.ServicesMixin): """Represent a connection to an OpenStack Cloud. @@ -59,24 +68,28 @@ class _OpenStackCloudMixin(_services_mixin.ServicesMixin): _SHADE_OBJECT_SHA256_KEY = 'x-object-meta-x-shade-sha256' _SHADE_OBJECT_AUTOCREATE_KEY = 'x-object-meta-x-shade-autocreated' + config: cloud_region.CloudRegion + def __init__( self, - cloud=None, - config=None, - session=None, - app_name=None, - app_version=None, - extra_services=None, - strict=False, - use_direct_get=None, - task_manager=None, - rate_limit=None, - oslo_conf=None, - service_types=None, - global_request_id=None, - strict_proxies=False, - pool_executor=None, - **kwargs, + cloud: ty.Optional[str] = None, + config: ty.Optional[cloud_region.CloudRegion] = None, + session: ty.Optional['ks_session.Session'] = None, + app_name: ty.Optional[str] = None, + app_version: ty.Optional[str] = None, + extra_services: ty.Optional[ + list['service_description.ServiceDescription'] + ] = None, + strict: bool = False, + use_direct_get: ty.Optional[bool] = None, + task_manager: ty.Any = None, + rate_limit: ty.Union[float, dict[str, float], None] = None, + oslo_conf: ty.Optional['cfg.ConfigOpts'] = None, + service_types: ty.Optional[list[str]] = None, + global_request_id: ty.Optional[str] = None, + strict_proxies: bool = False, + pool_executor: ty.Optional[concurrent.futures.Executor] = None, + **kwargs: ty.Any, ): """Create a connection to a cloud. @@ -161,16 +174,17 @@ def __init__( os_warnings.RemovedInSDK50Warning, ) - self.config = config self._extra_services = {} self._strict_proxies = strict_proxies if extra_services: for service in extra_services: self._extra_services[service.service_type] = service - if not self.config: + if config: + self.config = config + else: if oslo_conf: - self.config = openstack.config.cloud_region.from_conf( + self.config = cloud_region.from_conf( oslo_conf, session=session, app_name=app_name, @@ -178,7 +192,7 @@ def __init__( service_types=service_types, ) elif session: - self.config = openstack.config.cloud_region.from_session( + self.config = cloud_region.from_session( session=session, app_name=app_name, app_version=app_version, @@ -188,7 +202,7 @@ def __init__( **kwargs, ) else: - self.config = openstack.config.get_cloud_region( + self.config = cloud_config.get_cloud_region( cloud=cloud, app_name=app_name, app_version=app_version, @@ -199,7 +213,7 @@ def __init__( ) self._session = None - self._proxies = {} + self._proxies: dict[str, proxy.Proxy] = {} self.__pool_executor = pool_executor self._global_request_id = global_request_id self.use_direct_get = use_direct_get or False @@ -226,7 +240,7 @@ def __init__( # InsecureRequestWarning references a Warning class or is None warnings.filterwarnings('ignore', category=category) - self._disable_warnings = {} + self._disable_warnings: dict[str, bool] = {} cache_expiration_time = int(self.config.get_cache_expiration_time()) cache_class = self.config.get_cache_class() @@ -247,7 +261,7 @@ def __init__( for expire_key in expirations.keys(): self._cache_expirations[expire_key] = expirations[expire_key] - self._api_cache_keys = set() + self._api_cache_keys: set[str] = set() self._local_ipv6 = ( _utils.localhost_supports_ipv6() if not self.force_ipv4 else False @@ -267,27 +281,32 @@ def session(self): return self._session @property - def _pool_executor(self): + def _pool_executor(self) -> concurrent.futures.Executor: if not self.__pool_executor: self.__pool_executor = concurrent.futures.ThreadPoolExecutor( max_workers=5 ) return self.__pool_executor - def close(self): + def close(self) -> None: """Release any resources held open.""" self.config.set_auth_cache() if self.__pool_executor: self.__pool_executor.shutdown() atexit.unregister(self.close) - def __enter__(self): + def __enter__(self) -> ty_ext.Self: return self - def __exit__(self, exc_type, exc_value, traceback): + def __exit__( + self, + exc_type: ty.Optional[type[BaseException]], + exc_value: ty.Optional[BaseException], + traceback: ty.Optional[types.TracebackType], + ) -> None: self.close() - def set_global_request_id(self, global_request_id): + def set_global_request_id(self, global_request_id: str) -> None: self._global_request_id = global_request_id def global_request(self, global_request_id): @@ -323,7 +342,7 @@ def global_request(self, global_request_id): :param global_request_id: The `global_request_id` to send. """ params = copy.deepcopy(self.config.config) - cloud_region = openstack.config.cloud_region.from_session( + config = cloud_region.from_session( session=self.session, app_name=self.config._app_name, app_version=self.config._app_version, @@ -332,11 +351,11 @@ def global_request(self, global_request_id): ) # Override the cloud name so that logging/location work right - cloud_region._name = self.name - cloud_region.config['profile'] = self.name + config._name = self.name + config.config['profile'] = self.name # Use self.__class__ so that we return whatever this is, like if it's # a subclass in the case of shade wrapping sdk. - new_conn = self.__class__(config=cloud_region) + new_conn = self.__class__(config=config) new_conn.set_global_request_id(global_request_id) return new_conn diff --git a/openstack/config/__init__.py b/openstack/config/__init__.py index 75b623db4..48db4e8ae 100644 --- a/openstack/config/__init__.py +++ b/openstack/config/__init__.py @@ -12,20 +12,40 @@ # License for the specific language governing permissions and limitations # under the License. +import argparse import sys +import typing as ty from openstack.config.loader import OpenStackConfig # noqa +if ty.TYPE_CHECKING: + from openstack.config.cloud import cloud_region + +# TODO(stephenfin): Expand kwargs once we've typed OpenstackConfig.get_one def get_cloud_region( - service_key=None, - options=None, - app_name=None, - app_version=None, - load_yaml_config=True, - load_envvars=True, - **kwargs, -): + service_key: ty.Optional[str] = None, + options: ty.Optional[argparse.ArgumentParser] = None, + app_name: ty.Optional[str] = None, + app_version: ty.Optional[str] = None, + load_yaml_config: bool = True, + load_envvars: bool = True, + **kwargs: ty.Any, +) -> 'cloud_region.CloudRegion': + """Retrieve a single CloudRegion and merge additional options + + :param service_key: Service this argparse should be specialized for, if + known. This will be used as the default value for service_type. + :param options: Parser to attach additional options to + :param app_name: Name of the application to be added to User Agent. + :param app_version: Version of the application to be added to User Agent. + :param load_yaml_config: Whether to load configuration from clouds.yaml and + related configuration files. + :param load_envvars: Whether to load configuration from environment + variables + :returns: A populated + :class:`~openstack.config.cloud.cloud_region.CloudRegion` object. + """ config = OpenStackConfig( load_yaml_config=load_yaml_config, load_envvars=load_envvars, @@ -34,7 +54,7 @@ def get_cloud_region( ) if options: config.register_argparse_arguments(options, sys.argv, service_key) - parsed_options, _rest_of_argv = options.parse_known_args(sys.argv) + parsed_options, _ = options.parse_known_args(sys.argv) else: parsed_options = None diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index c4cd9cc08..cb444d767 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -25,7 +25,9 @@ from keystoneauth1 import discover import keystoneauth1.exceptions.catalog +from keystoneauth1.identity import base as ks_identity_base from keystoneauth1.loading import adapter as ks_load_adap +from keystoneauth1 import plugin from keystoneauth1 import session as ks_session import os_service_types import requestsexceptions @@ -51,6 +53,11 @@ from openstack import version as openstack_version from openstack import warnings as os_warnings +if ty.TYPE_CHECKING: + from oslo_config import cfg + + from openstack.config import loader + _logger = _log.setup_logging('openstack') @@ -66,6 +73,10 @@ _ENOENT = object() +class _PasswordCallback(ty.Protocol): + def __call__(self, prompt: ty.Optional[str] = None) -> str: ... + + def _make_key(key, service_type): if not service_type: return key @@ -96,14 +107,14 @@ def _get_implied_microversion(version): def from_session( - session, - name=None, - region_name=None, - force_ipv4=False, - app_name=None, - app_version=None, - **kwargs, -): + session: ks_session.Session, + name: ty.Optional[str] = None, + region_name: ty.Optional[str] = None, + force_ipv4: bool = False, + app_name: ty.Optional[str] = None, + app_version: ty.Optional[str] = None, + **kwargs: ty.Any, +) -> 'CloudRegion': """Construct a CloudRegion from an existing `keystoneauth1.session.Session` When a Session already exists, we don't actually even need to go through @@ -139,7 +150,12 @@ def from_session( ) -def from_conf(conf, session=None, service_types=None, **kwargs): +def from_conf( + conf: 'cfg.ConfigOpts', + session: ty.Optional[ks_session.Session] = None, + service_types: ty.Optional[list[str]] = None, + **kwargs: ty.Any, +) -> 'CloudRegion': """Create a CloudRegion from oslo.config ConfigOpts. :param oslo_config.cfg.ConfigOpts conf: @@ -230,12 +246,13 @@ class CloudRegion: A CloudRegion encapsulates the config information needed for connections to all of the services in a Region of a Cloud. - :param str region_name: + :param name: + :param region_name: The default region name for all services in this CloudRegion. If both ``region_name`` and ``config['region_name']`` are specified, the kwarg takes precedence. May be overridden for a given ${service} via a ${service}_region_name key in the ``config`` dict. - :param dict config: + :param config: A dict of configuration values for the CloudRegion and its services. The key for a ${config_option} for a specific ${service} should be ${service}_${config_option}. For example, to configure @@ -249,35 +266,59 @@ class CloudRegion: key, e.g.:: 'interface': 'public' + :param force_ipv4: + :param auth_plugin: + :param openstack_config: + :param session_constructor: + :param app_name: + :param app_version: + :param session: + :param discovery_cache: + :param extra_config: + :param cache_expiration_time: + :param cache_expirations: + :param cache_path: + :param cache_class: + :param cache_arguments: + :param password_callback: + :param statsd_host: + :param statsd_port: + :param statsd_prefix: + :param influxdb_config: + :param collector_registry: + :param cache_auth: """ def __init__( self, - name=None, - region_name=None, - config=None, - force_ipv4=False, - auth_plugin=None, - openstack_config=None, - session_constructor=None, - app_name=None, - app_version=None, - session=None, - discovery_cache=None, - extra_config=None, - cache_expiration_time=0, - cache_expirations=None, - cache_path=None, - cache_class='dogpile.cache.null', - cache_arguments=None, - password_callback=None, - statsd_host=None, - statsd_port=None, - statsd_prefix=None, - influxdb_config=None, - collector_registry=None, - cache_auth=False, - ): + name: ty.Optional[str] = None, + region_name: ty.Optional[str] = None, + config: ty.Optional[dict[str, ty.Any]] = None, + force_ipv4: bool = False, + auth_plugin: ty.Optional[plugin.BaseAuthPlugin] = None, + openstack_config: ty.Optional['loader.OpenStackConfig'] = None, + session_constructor: ty.Optional[type[ks_session.Session]] = None, + app_name: ty.Optional[str] = None, + app_version: ty.Optional[str] = None, + session: ty.Optional[ks_session.Session] = None, + discovery_cache: ty.Optional[dict[str, discover.Discover]] = None, + extra_config: ty.Optional[dict[str, ty.Any]] = None, + cache_expiration_time: int = 0, + cache_expirations: ty.Optional[dict[str, int]] = None, + cache_path: ty.Optional[str] = None, + cache_class: str = 'dogpile.cache.null', + cache_arguments: ty.Optional[dict[str, ty.Any]] = None, + password_callback: ty.Optional[_PasswordCallback] = None, + statsd_host: ty.Optional[str] = None, + statsd_port: ty.Optional[str] = None, + statsd_prefix: ty.Optional[str] = None, + # TODO(stephenfin): Add better types + influxdb_config: ty.Optional[dict[str, ty.Any]] = None, + collector_registry: ty.Optional[ + 'prometheus_client.CollectorRegistry' + ] = None, + cache_auth: bool = False, + ) -> None: self._name = name self.config = _util.normalize_keys(config) # NOTE(efried): For backward compatibility: a) continue to accept the @@ -335,13 +376,20 @@ def __ne__(self, other): @property def name(self): - if self._name is None: - try: - self._name = parse.urlparse( - self.get_session().auth.auth_url - ).hostname - except Exception: - self._name = self._app_name or '' + if self._name is not None: + return self._name + + auth = self.get_session().auth + # not all auth plugins are identity plugins + if ( + auth + and isinstance(auth, ks_identity_base.BaseIdentityPlugin) + and auth.auth_url + ): + self._name = parse.urlparse(auth.auth_url).hostname + else: + self._name = self._app_name or '' + return self._name @property @@ -529,16 +577,18 @@ def get_service_type(self, service_type): def get_service_name(self, service_type): return self._get_config('service_name', service_type) - def get_endpoint(self, service_type): + def get_endpoint(self, service_type: str) -> ty.Optional[str]: auth = self.config.get('auth', {}) value = self._get_config('endpoint_override', service_type) if not value: value = self._get_config('endpoint', service_type) + if not value and self.config.get('auth_type') == 'none': # If endpoint is given and we're using the none auth type, # then the endpoint value is the endpoint_override for every # service. value = auth.get('endpoint') + if ( not value and service_type == 'identity' @@ -548,6 +598,7 @@ def get_endpoint(self, service_type): # Specifically, looking up a list of projects/domains/system to # scope to. value = auth.get('auth_url') + # Because of course. Seriously. # We have to override the Rackspace block-storage endpoint because # only v1 is in the catalog but the service actually does support @@ -561,11 +612,15 @@ def get_endpoint(self, service_type): and service_type == 'block-storage' ): value = value + auth.get('project_id') - return value + + return str(value) if value else None def get_endpoint_from_catalog( - self, service_type, interface=None, region_name=None - ): + self, + service_type: str, + interface: ty.Optional[str] = None, + region_name: ty.Optional[str] = None, + ) -> ty.Optional[str]: """Return the endpoint for a given service as found in the catalog. For values respecting endpoint overrides, see @@ -584,8 +639,14 @@ def get_endpoint_from_catalog( interface = interface or self.get_interface(service_type) region_name = region_name or self.get_region_name(service_type) session = self.get_session() - catalog = session.auth.get_access(session).service_catalog + + auth = session.auth + if not isinstance(auth, ks_identity_base.BaseIdentityPlugin): + return None + + catalog = auth.get_access(session).service_catalog try: + # FIXME(stephenfin): Remove once keystoneauth1 has type hints return catalog.url_for( service_type=service_type, interface=interface, @@ -629,6 +690,8 @@ def load_auth_from_cache(self): if self.skip_auth_cache(): return + assert self._auth is not None # narrow type + cache_id = self._auth.get_cache_id() # skip if the plugin does not support caching @@ -644,10 +707,12 @@ def load_auth_from_cache(self): self.log.debug('Reusing authentication from keyring') self._auth.set_auth_state(state) - def set_auth_cache(self): + def set_auth_cache(self) -> None: if self.skip_auth_cache(): return + assert self._auth is not None # narrow type + cache_id = self._auth.get_cache_id() state = self._auth.get_auth_state() @@ -674,11 +739,14 @@ def insert_user_agent(self): :class:`~openstack.config.cloud_region.CloudRegion` it may be desirable. """ + if not self._keystone_session: + return + self._keystone_session.additional_user_agent.append( ('openstacksdk', openstack_version.__version__) ) - def get_session(self): + def get_session(self) -> ks_session.Session: """Return a keystoneauth session based on the auth credentials.""" if self._keystone_session is None: if not self._auth: @@ -716,6 +784,9 @@ def get_session(self): def get_service_catalog(self): """Helper method to grab the service catalog.""" + # not all auth plugins are identity plugins + if not isinstance(self._auth, ks_identity_base.BaseIdentityPlugin): + return None return self._auth.get_access(self.get_session()).service_catalog def _get_version_request(self, service_type, version): @@ -1223,7 +1294,7 @@ def has_service(self, service_type): def disable_service(self, service_type, reason=None): _disable_service(self.config, service_type, reason=reason) - def enable_service(self, service_type): + def enable_service(self, service_type: str) -> None: service_type = service_type.lower().replace('-', '_') key = f'has_{service_type}' self.config[key] = True @@ -1236,7 +1307,8 @@ def get_disabled_reason(self, service_type): def get_influxdb_client( self, ) -> ty.Optional['influxdb_client.InfluxDBClient']: - influx_args = {} + # TODO(stephenfin): We could do with a typed dict here. + influx_args: dict[str, ty.Any] = {} if not self._influxdb_config: return None use_udp = bool(self._influxdb_config.get('use_udp', False)) diff --git a/openstack/config/loader.py b/openstack/config/loader.py index d55c01faf..e21dc3687 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -26,6 +26,7 @@ from keystoneauth1 import adapter from keystoneauth1 import loading +from keystoneauth1 import session import platformdirs import yaml @@ -157,22 +158,22 @@ class OpenStackConfig: def __init__( self, - config_files=None, - vendor_files=None, - override_defaults=None, - force_ipv4=None, - envvar_prefix=None, - secure_files=None, - pw_func=None, - session_constructor=None, - app_name=None, - app_version=None, - load_yaml_config=True, - load_envvars=True, - statsd_host=None, - statsd_port=None, - statsd_prefix=None, - influxdb_config=None, + config_files: ty.Optional[list[str]] = None, + vendor_files: ty.Optional[list[str]] = None, + override_defaults: ty.Optional[dict[str, ty.Any]] = None, + force_ipv4: ty.Optional[bool] = None, + envvar_prefix: ty.Optional[str] = None, + secure_files: ty.Optional[list[str]] = None, + pw_func: ty.Optional[cloud_region._PasswordCallback] = None, + session_constructor: ty.Optional[type[session.Session]] = None, + app_name: ty.Optional[str] = None, + app_version: ty.Optional[str] = None, + load_yaml_config: bool = True, + load_envvars: bool = True, + statsd_host: ty.Optional[str] = None, + statsd_port: ty.Optional[str] = None, + statsd_prefix: ty.Optional[str] = None, + influxdb_config: ty.Optional[dict[str, ty.Any]] = None, ): self.log = _log.setup_logging('openstack.config') self._session_constructor = session_constructor @@ -1218,7 +1219,13 @@ def magic_fixes(self, config): return config - def get_one(self, cloud=None, validate=True, argparse=None, **kwargs): + def get_one( + self, + cloud: ty.Optional[str] = None, + validate: bool = True, + argparse: ty.Optional[argparse_mod.Namespace] = None, + **kwargs: ty.Any, + ) -> cloud_region.CloudRegion: """Retrieve a single CloudRegion and merge additional options :param string cloud: diff --git a/openstack/connection.py b/openstack/connection.py index 8a8e8bc4a..39ef30840 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -271,12 +271,17 @@ :ref:`service-proxies` documentation. """ +import argparse +import concurrent.futures import copy import importlib.metadata as importlib_metadata +import typing as ty import warnings import keystoneauth1.exceptions +from keystoneauth1 import session as ks_session import requestsexceptions +import typing_extensions as ty_ext from openstack import _log from openstack.cloud import _accelerator @@ -296,6 +301,12 @@ from openstack import exceptions from openstack import service_description +if ty.TYPE_CHECKING: + from oslo_config import cfg + + from openstack.config import cloud_region + from openstack import proxy + __all__ = [ 'from_config', 'Connection', @@ -309,7 +320,12 @@ _logger = _log.setup_logging('openstack') -def from_config(cloud=None, config=None, options=None, **kwargs): +def from_config( + cloud: ty.Optional[str] = None, + config: ty.Optional['cloud_region.CloudRegion'] = None, + options: ty.Optional[argparse.Namespace] = None, + **kwargs: ty.Any, +) -> 'Connection': """Create a Connection using openstack.config :param str cloud: @@ -355,22 +371,24 @@ class Connection( ): def __init__( self, - cloud=None, - config=None, - session=None, - app_name=None, - app_version=None, - extra_services=None, - strict=False, - use_direct_get=None, - task_manager=None, - rate_limit=None, - oslo_conf=None, - service_types=None, - global_request_id=None, - strict_proxies=False, - pool_executor=None, - **kwargs, + cloud: ty.Optional[str] = None, + config: ty.Optional['cloud_region.CloudRegion'] = None, + session: ty.Optional[ks_session.Session] = None, + app_name: ty.Optional[str] = None, + app_version: ty.Optional[str] = None, + extra_services: ty.Optional[ + list[service_description.ServiceDescription] + ] = None, + strict: bool = False, + use_direct_get: ty.Optional[bool] = None, + task_manager: ty.Any = None, + rate_limit: ty.Union[float, dict[str, float], None] = None, + oslo_conf: ty.Optional['cfg.ConfigOpts'] = None, + service_types: ty.Optional[list[str]] = None, + global_request_id: ty.Optional[str] = None, + strict_proxies: bool = False, + pool_executor: ty.Optional[concurrent.futures.Executor] = None, + **kwargs: ty.Any, ): """Create a connection to a cloud. @@ -507,7 +525,9 @@ def __init__( self.config.config['additional_metric_tags'] ) - def add_service(self, service): + def add_service( + self, service: service_description.ServiceDescription + ) -> None: """Add a service to the Connection. Attaches an instance of the :class:`~openstack.proxy.Proxy` @@ -530,8 +550,10 @@ class contained in service = service_description.ServiceDescription(service) # Directly invoke descriptor of the ServiceDescription - def getter(self): - return service.__get__(self, service) + def getter(self: 'Connection') -> 'proxy.Proxy': + # TODO(stephenfin): Remove ignore once we have typed + # ServiceDescription + return service.__get__(self, service) # type: ignore # Register the ServiceDescription class (as property) # with every known alias for a "runtime descriptor" @@ -543,7 +565,7 @@ def getter(self): ) self.config.enable_service(service.service_type) - def authorize(self): + def authorize(self) -> str: """Authorize this Connection .. note:: @@ -554,16 +576,16 @@ def authorize(self): :returns: A string token. :raises: :class:`~openstack.exceptions.HttpException` if the - authorization fails due to reasons like the credentials - provided are unable to be authorized or the `auth_type` - argument is missing, etc. + authorization fails due to reasons like the credentials provided + are unable to be authorized or the `auth_type` argument is missing, + etc. """ try: - return self.session.get_token() + return ty.cast(str, self.session.get_token()) except keystoneauth1.exceptions.ClientException as e: raise exceptions.SDKException(str(e)) - def connect_as(self, **kwargs): + def connect_as(self, **kwargs: ty.Any) -> ty_ext.Self: """Make a new Connection object with new auth context. Take the existing settings from the current cloud and construct a new @@ -600,7 +622,12 @@ def connect_as(self, **kwargs): params.pop('profile', None) # Utility function to help with the stripping below. - def pop_keys(params, auth, name_key, id_key): + def pop_keys( + params: dict[str, dict[str, ty.Optional[str]]], + auth: dict[str, ty.Optional[str]], + name_key: str, + id_key: str, + ) -> None: if name_key in auth or id_key in auth: params['auth'].pop(name_key, None) params['auth'].pop(id_key, None) @@ -635,7 +662,7 @@ def pop_keys(params, auth, name_key, id_key): # a subclass in the case of shade wrapping sdk. return self.__class__(config=cloud_region) - def connect_as_project(self, project): + def connect_as_project(self, project: str) -> ty_ext.Self: """Make a new Connection object with a new project. Take the existing settings from the current cloud and construct a new @@ -665,7 +692,12 @@ def connect_as_project(self, project): auth['project_name'] = project return self.connect_as(**auth) - def endpoint_for(self, service_type, interface=None, region_name=None): + def endpoint_for( + self, + service_type: str, + interface: ty.Optional[str] = None, + region_name: ty.Optional[str] = None, + ) -> ty.Optional[str]: """Return the endpoint for a given service. Respects config values for Connection, including @@ -683,10 +715,12 @@ def endpoint_for(self, service_type, interface=None, region_name=None): :returns: The endpoint of the service, or None if not found. """ + # FIXME(stephenfin): Why is self.config showing as Any? + endpoint_override = self.config.get_endpoint(service_type) if endpoint_override: - return endpoint_override - return self.config.get_endpoint_from_catalog( + return endpoint_override # type: ignore + return self.config.get_endpoint_from_catalog( # type: ignore service_type=service_type, interface=interface, region_name=region_name, diff --git a/openstack/service_description.py b/openstack/service_description.py index 4ea2dda37..c29835836 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -11,6 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty import warnings import os_service_types @@ -50,7 +51,14 @@ class ServiceDescription: #: list of aliases this service might be registered as aliases: list[str] = [] - def __init__(self, service_type, supported_versions=None, aliases=None): + def __init__( + self, + service_type: str, + supported_versions: ty.Optional[ + dict[str, type[proxy_mod.Proxy]] + ] = None, + aliases: ty.Optional[list[str]] = None, + ): """Class describing how to interact with a REST service. Each service in an OpenStack cloud needs to be found by looking @@ -67,11 +75,9 @@ def __init__(self, service_type, supported_versions=None, aliases=None): a service-specific subclass can be used that sets the attributes directly. - :param string service_type: - service_type to look for in the keystone catalog - :param list aliases: - Optional list of aliases, if there is more than one name that might - be used to register the service in the catalog. + :param service_type: service_type to look for in the keystone catalog + :param aliases: Optional list of aliases, if there is more than one + name that might be used to register the service in the catalog. """ self.service_type = service_type or self.service_type self.supported_versions = ( diff --git a/openstack/tests/unit/test_connection.py b/openstack/tests/unit/test_connection.py index f5cf7ce1f..9c21caa89 100644 --- a/openstack/tests/unit/test_connection.py +++ b/openstack/tests/unit/test_connection.py @@ -14,6 +14,7 @@ from unittest import mock import fixtures +from keystoneauth1 import identity from keystoneauth1 import session import openstack.config @@ -116,8 +117,11 @@ def test_other_parameters(self): def test_session_provided(self): mock_session = mock.Mock(spec=session.Session) - mock_session.auth = mock.Mock() - mock_session.auth.auth_url = 'https://auth.example.com' + mock_session.auth = identity.V3Password( + auth_url='https://auth.example.com', + password='passw0rd', + user_id='fake', + ) conn = connection.Connection(session=mock_session, cert='cert') self.assertEqual(mock_session, conn.session) self.assertEqual('auth.example.com', conn.config.name) diff --git a/pyproject.toml b/pyproject.toml index 621605328..6b80e03f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,8 @@ exclude = ''' [[tool.mypy.overrides]] module = [ + # "openstack.config.cloud_region", + "openstack.connection", "openstack.exceptions", "openstack.fields", "openstack.format", From 7b3ea15398f878effc0a9b201b5e22a7e003a564 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 25 Feb 2025 16:16:55 +0000 Subject: [PATCH 3692/3836] test: Use specific cloud in functional test Change-Id: Ic76a488edcb06adfc76c0445a0e1b1e115b1766f Signed-off-by: Stephen Finucane --- openstack/tests/functional/baremetal/base.py | 42 +- .../baremetal/test_baremetal_allocation.py | 90 ++-- .../baremetal/test_baremetal_chassis.py | 32 +- .../baremetal/test_baremetal_conductor.py | 6 +- .../test_baremetal_deploy_templates.py | 40 +- .../baremetal/test_baremetal_driver.py | 10 +- .../baremetal/test_baremetal_node.py | 190 ++++---- .../baremetal/test_baremetal_port.py | 50 +- .../baremetal/test_baremetal_port_group.py | 58 ++- .../test_baremetal_volume_connector.py | 97 ++-- .../baremetal/test_baremetal_volume_target.py | 102 ++-- openstack/tests/functional/base.py | 9 +- .../block_storage/v3/test_attachment.py | 6 +- .../v3/test_availability_zone.py | 4 +- .../v3/test_block_storage_summary.py | 2 +- .../block_storage/v3/test_capabilities.py | 2 +- .../block_storage/v3/test_default_type.py | 8 +- .../block_storage/v3/test_extension.py | 2 +- .../functional/block_storage/v3/test_group.py | 92 ++-- .../block_storage/v3/test_limits.py | 2 +- .../block_storage/v3/test_resource_filters.py | 4 +- .../block_storage/v3/test_transfer.py | 6 +- .../functional/cloud/test_project_cleanup.py | 189 ++++---- .../functional/clustering/test_cluster.py | 48 +- .../functional/compute/v2/test_extension.py | 2 +- .../functional/compute/v2/test_flavor.py | 38 +- .../functional/compute/v2/test_hypervisor.py | 10 +- .../tests/functional/compute/v2/test_image.py | 42 +- .../functional/compute/v2/test_keypair.py | 24 +- .../functional/compute/v2/test_limits.py | 2 +- .../functional/compute/v2/test_server.py | 78 ++-- .../functional/compute/v2/test_service.py | 22 +- .../functional/dns/v2/test_service_status.py | 9 +- .../tests/functional/dns/v2/test_zone.py | 39 +- .../functional/dns/v2/test_zone_share.py | 2 +- .../tests/functional/examples/test_compute.py | 32 +- .../functional/examples/test_identity.py | 24 +- .../tests/functional/examples/test_image.py | 14 +- .../tests/functional/examples/test_network.py | 24 +- .../identity/v3/test_access_rule.py | 16 +- .../v3/test_application_credential.py | 14 +- .../tests/functional/image/v2/test_image.py | 28 +- .../image/v2/test_metadef_namespace.py | 22 +- .../image/v2/test_metadef_object.py | 26 +- .../image/v2/test_metadef_property.py | 30 +- .../image/v2/test_metadef_resource_type.py | 22 +- .../image/v2/test_metadef_schema.py | 32 +- .../tests/functional/image/v2/test_schema.py | 8 +- .../tests/functional/image/v2/test_task.py | 2 +- .../tests/functional/instance_ha/test_host.py | 18 +- .../functional/instance_ha/test_segment.py | 17 +- .../load_balancer/v2/test_load_balancer.py | 435 +++++++++++------- .../network/v2/test_qos_dscp_marking_rule.py | 20 +- .../object_store/v1/test_account.py | 34 +- .../object_store/v1/test_container.py | 90 ++-- .../functional/object_store/v1/test_obj.py | 66 +-- .../functional/orchestration/v1/test_stack.py | 34 +- .../placement/v1/test_resource_provider.py | 2 +- .../functional/shared_file_system/base.py | 2 +- .../shared_file_system/test_share_group.py | 4 +- .../shared_file_system/test_user_message.py | 4 +- 61 files changed, 1404 insertions(+), 975 deletions(-) diff --git a/openstack/tests/functional/baremetal/base.py b/openstack/tests/functional/baremetal/base.py index 4150619ae..0d186eda1 100644 --- a/openstack/tests/functional/baremetal/base.py +++ b/openstack/tests/functional/baremetal/base.py @@ -26,28 +26,30 @@ def setUp(self): ) def create_allocation(self, **kwargs): - allocation = self.conn.baremetal.create_allocation(**kwargs) + allocation = self.operator_cloud.baremetal.create_allocation(**kwargs) self.addCleanup( - lambda: self.conn.baremetal.delete_allocation( + lambda: self.operator_cloud.baremetal.delete_allocation( allocation.id, ignore_missing=True ) ) return allocation def create_chassis(self, **kwargs): - chassis = self.conn.baremetal.create_chassis(**kwargs) + chassis = self.operator_cloud.baremetal.create_chassis(**kwargs) self.addCleanup( - lambda: self.conn.baremetal.delete_chassis( + lambda: self.operator_cloud.baremetal.delete_chassis( chassis.id, ignore_missing=True ) ) return chassis def create_node(self, driver='fake-hardware', **kwargs): - node = self.conn.baremetal.create_node(driver=driver, **kwargs) + node = self.operator_cloud.baremetal.create_node( + driver=driver, **kwargs + ) self.node_id = node.id self.addCleanup( - lambda: self.conn.baremetal.delete_node( + lambda: self.operator_cloud.baremetal.delete_node( self.node_id, ignore_missing=True ) ) @@ -56,9 +58,11 @@ def create_node(self, driver='fake-hardware', **kwargs): def create_port(self, node_id=None, **kwargs): node_id = node_id or self.node_id - port = self.conn.baremetal.create_port(node_uuid=node_id, **kwargs) + port = self.operator_cloud.baremetal.create_port( + node_uuid=node_id, **kwargs + ) self.addCleanup( - lambda: self.conn.baremetal.delete_port( + lambda: self.operator_cloud.baremetal.delete_port( port.id, ignore_missing=True ) ) @@ -66,11 +70,11 @@ def create_port(self, node_id=None, **kwargs): def create_port_group(self, node_id=None, **kwargs): node_id = node_id or self.node_id - port_group = self.conn.baremetal.create_port_group( + port_group = self.operator_cloud.baremetal.create_port_group( node_uuid=node_id, **kwargs ) self.addCleanup( - lambda: self.conn.baremetal.delete_port_group( + lambda: self.operator_cloud.baremetal.delete_port_group( port_group.id, ignore_missing=True ) ) @@ -78,12 +82,14 @@ def create_port_group(self, node_id=None, **kwargs): def create_volume_connector(self, node_id=None, **kwargs): node_id = node_id or self.node_id - volume_connector = self.conn.baremetal.create_volume_connector( - node_uuid=node_id, **kwargs + volume_connector = ( + self.operator_cloud.baremetal.create_volume_connector( + node_uuid=node_id, **kwargs + ) ) self.addCleanup( - lambda: self.conn.baremetal.delete_volume_connector( + lambda: self.operator_cloud.baremetal.delete_volume_connector( volume_connector.id, ignore_missing=True ) ) @@ -91,12 +97,12 @@ def create_volume_connector(self, node_id=None, **kwargs): def create_volume_target(self, node_id=None, **kwargs): node_id = node_id or self.node_id - volume_target = self.conn.baremetal.create_volume_target( + volume_target = self.operator_cloud.baremetal.create_volume_target( node_uuid=node_id, **kwargs ) self.addCleanup( - lambda: self.conn.baremetal.delete_volume_target( + lambda: self.operator_cloud.baremetal.delete_volume_target( volume_target.id, ignore_missing=True ) ) @@ -105,10 +111,12 @@ def create_volume_target(self, node_id=None, **kwargs): def create_deploy_template(self, **kwargs): """Create a new deploy_template from attributes.""" - deploy_template = self.conn.baremetal.create_deploy_template(**kwargs) + deploy_template = self.operator_cloud.baremetal.create_deploy_template( + **kwargs + ) self.addCleanup( - lambda: self.conn.baremetal.delete_deploy_template( + lambda: self.operator_cloud.baremetal.delete_deploy_template( deploy_template.id, ignore_missing=True ) ) diff --git a/openstack/tests/functional/baremetal/test_baremetal_allocation.py b/openstack/tests/functional/baremetal/test_baremetal_allocation.py index f36ea4c10..be6c846cb 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_allocation.py +++ b/openstack/tests/functional/baremetal/test_baremetal_allocation.py @@ -26,14 +26,18 @@ def setUp(self): def _create_available_node(self): node = self.create_node(resource_class=self.resource_class) - self.conn.baremetal.set_node_provision_state(node, 'manage', wait=True) - self.conn.baremetal.set_node_provision_state( + self.operator_cloud.baremetal.set_node_provision_state( + node, 'manage', wait=True + ) + self.operator_cloud.baremetal.set_node_provision_state( node, 'provide', wait=True ) # Make sure the node has non-empty power state by forcing power off. - self.conn.baremetal.set_node_power_state(node, 'power off') + self.operator_cloud.baremetal.set_node_power_state(node, 'power off') self.addCleanup( - lambda: self.conn.baremetal.update_node(node.id, instance_id=None) + lambda: self.operator_cloud.baremetal.update_node( + node.id, instance_id=None + ) ) return node @@ -47,25 +51,27 @@ def test_allocation_create_get_delete(self): self.assertIsNone(allocation.node_id) self.assertIsNone(allocation.last_error) - loaded = self.conn.baremetal.wait_for_allocation(allocation) + loaded = self.operator_cloud.baremetal.wait_for_allocation(allocation) self.assertEqual(loaded.id, allocation.id) self.assertEqual('active', allocation.state) self.assertEqual(self.node.id, allocation.node_id) self.assertIsNone(allocation.last_error) - with_fields = self.conn.baremetal.get_allocation( + with_fields = self.operator_cloud.baremetal.get_allocation( allocation.id, fields=['uuid', 'node_uuid'] ) self.assertEqual(allocation.id, with_fields.id) self.assertIsNone(with_fields.state) - node = self.conn.baremetal.get_node(self.node.id) + node = self.operator_cloud.baremetal.get_node(self.node.id) self.assertEqual(allocation.id, node.allocation_id) - self.conn.baremetal.delete_allocation(allocation, ignore_missing=False) + self.operator_cloud.baremetal.delete_allocation( + allocation, ignore_missing=False + ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.get_allocation, + self.operator_cloud.baremetal.get_allocation, allocation.id, ) @@ -77,21 +83,25 @@ def test_allocation_list(self): resource_class=self.resource_class + '-fail' ) - self.conn.baremetal.wait_for_allocation(allocation1) - self.conn.baremetal.wait_for_allocation(allocation2, ignore_error=True) + self.operator_cloud.baremetal.wait_for_allocation(allocation1) + self.operator_cloud.baremetal.wait_for_allocation( + allocation2, ignore_error=True + ) - allocations = self.conn.baremetal.allocations() + allocations = self.operator_cloud.baremetal.allocations() self.assertEqual( {p.id for p in allocations}, {allocation1.id, allocation2.id} ) - allocations = self.conn.baremetal.allocations(state='active') + allocations = self.operator_cloud.baremetal.allocations(state='active') self.assertEqual([p.id for p in allocations], [allocation1.id]) - allocations = self.conn.baremetal.allocations(node=self.node.id) + allocations = self.operator_cloud.baremetal.allocations( + node=self.node.id + ) self.assertEqual([p.id for p in allocations], [allocation1.id]) - allocations = self.conn.baremetal.allocations( + allocations = self.operator_cloud.baremetal.allocations( resource_class=self.resource_class + '-fail' ) self.assertEqual([p.id for p in allocations], [allocation2.id]) @@ -102,11 +112,13 @@ def test_allocation_negative_failure(self): ) self.assertRaises( exceptions.SDKException, - self.conn.baremetal.wait_for_allocation, + self.operator_cloud.baremetal.wait_for_allocation, allocation, ) - allocation = self.conn.baremetal.get_allocation(allocation.id) + allocation = self.operator_cloud.baremetal.get_allocation( + allocation.id + ) self.assertEqual('error', allocation.state) self.assertIn(self.resource_class + '-fail', allocation.last_error) @@ -114,20 +126,22 @@ def test_allocation_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.get_allocation, + self.operator_cloud.baremetal.get_allocation, uuid, ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.delete_allocation, + self.operator_cloud.baremetal.delete_allocation, uuid, ignore_missing=False, ) - self.assertIsNone(self.conn.baremetal.delete_allocation(uuid)) + self.assertIsNone( + self.operator_cloud.baremetal.delete_allocation(uuid) + ) def test_allocation_fields(self): self.create_allocation(resource_class=self.resource_class) - result = self.conn.baremetal.allocations(fields=['uuid']) + result = self.operator_cloud.baremetal.allocations(fields=['uuid']) for item in result: self.assertIsNotNone(item.id) self.assertIsNone(item.resource_class) @@ -140,26 +154,30 @@ def test_allocation_update(self): name = 'ossdk-name1' allocation = self.create_allocation(resource_class=self.resource_class) - allocation = self.conn.baremetal.wait_for_allocation(allocation) + allocation = self.operator_cloud.baremetal.wait_for_allocation( + allocation + ) self.assertEqual('active', allocation.state) self.assertIsNone(allocation.last_error) self.assertIsNone(allocation.name) self.assertEqual({}, allocation.extra) - allocation = self.conn.baremetal.update_allocation( + allocation = self.operator_cloud.baremetal.update_allocation( allocation, name=name, extra={'answer': 42} ) self.assertEqual(name, allocation.name) self.assertEqual({'answer': 42}, allocation.extra) - allocation = self.conn.baremetal.get_allocation(name) + allocation = self.operator_cloud.baremetal.get_allocation(name) self.assertEqual(name, allocation.name) self.assertEqual({'answer': 42}, allocation.extra) - self.conn.baremetal.delete_allocation(allocation, ignore_missing=False) + self.operator_cloud.baremetal.delete_allocation( + allocation, ignore_missing=False + ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.get_allocation, + self.operator_cloud.baremetal.get_allocation, allocation.id, ) @@ -167,13 +185,15 @@ def test_allocation_patch(self): name = 'ossdk-name2' allocation = self.create_allocation(resource_class=self.resource_class) - allocation = self.conn.baremetal.wait_for_allocation(allocation) + allocation = self.operator_cloud.baremetal.wait_for_allocation( + allocation + ) self.assertEqual('active', allocation.state) self.assertIsNone(allocation.last_error) self.assertIsNone(allocation.name) self.assertEqual({}, allocation.extra) - allocation = self.conn.baremetal.patch_allocation( + allocation = self.operator_cloud.baremetal.patch_allocation( allocation, [ {'op': 'replace', 'path': '/name', 'value': name}, @@ -183,11 +203,11 @@ def test_allocation_patch(self): self.assertEqual(name, allocation.name) self.assertEqual({'answer': 42}, allocation.extra) - allocation = self.conn.baremetal.get_allocation(name) + allocation = self.operator_cloud.baremetal.get_allocation(name) self.assertEqual(name, allocation.name) self.assertEqual({'answer': 42}, allocation.extra) - allocation = self.conn.baremetal.patch_allocation( + allocation = self.operator_cloud.baremetal.patch_allocation( allocation, [ {'op': 'remove', 'path': '/name'}, @@ -197,13 +217,17 @@ def test_allocation_patch(self): self.assertIsNone(allocation.name) self.assertEqual({}, allocation.extra) - allocation = self.conn.baremetal.get_allocation(allocation.id) + allocation = self.operator_cloud.baremetal.get_allocation( + allocation.id + ) self.assertIsNone(allocation.name) self.assertEqual({}, allocation.extra) - self.conn.baremetal.delete_allocation(allocation, ignore_missing=False) + self.operator_cloud.baremetal.delete_allocation( + allocation, ignore_missing=False + ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.get_allocation, + self.operator_cloud.baremetal.get_allocation, allocation.id, ) diff --git a/openstack/tests/functional/baremetal/test_baremetal_chassis.py b/openstack/tests/functional/baremetal/test_baremetal_chassis.py index 2fdb2afbd..c6e638bad 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_chassis.py +++ b/openstack/tests/functional/baremetal/test_baremetal_chassis.py @@ -19,13 +19,15 @@ class TestBareMetalChassis(base.BaseBaremetalTest): def test_chassis_create_get_delete(self): chassis = self.create_chassis() - loaded = self.conn.baremetal.get_chassis(chassis.id) + loaded = self.operator_cloud.baremetal.get_chassis(chassis.id) self.assertEqual(loaded.id, chassis.id) - self.conn.baremetal.delete_chassis(chassis, ignore_missing=False) + self.operator_cloud.baremetal.delete_chassis( + chassis, ignore_missing=False + ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.get_chassis, + self.operator_cloud.baremetal.get_chassis, chassis.id, ) @@ -33,42 +35,44 @@ def test_chassis_update(self): chassis = self.create_chassis() chassis.extra = {'answer': 42} - chassis = self.conn.baremetal.update_chassis(chassis) + chassis = self.operator_cloud.baremetal.update_chassis(chassis) self.assertEqual({'answer': 42}, chassis.extra) - chassis = self.conn.baremetal.get_chassis(chassis.id) + chassis = self.operator_cloud.baremetal.get_chassis(chassis.id) self.assertEqual({'answer': 42}, chassis.extra) def test_chassis_patch(self): chassis = self.create_chassis() - chassis = self.conn.baremetal.patch_chassis( + chassis = self.operator_cloud.baremetal.patch_chassis( chassis, dict(path='/extra/answer', op='add', value=42) ) self.assertEqual({'answer': 42}, chassis.extra) - chassis = self.conn.baremetal.get_chassis(chassis.id) + chassis = self.operator_cloud.baremetal.get_chassis(chassis.id) self.assertEqual({'answer': 42}, chassis.extra) def test_chassis_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" self.assertRaises( - exceptions.NotFoundException, self.conn.baremetal.get_chassis, uuid + exceptions.NotFoundException, + self.operator_cloud.baremetal.get_chassis, + uuid, ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.find_chassis, + self.operator_cloud.baremetal.find_chassis, uuid, ignore_missing=False, ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.delete_chassis, + self.operator_cloud.baremetal.delete_chassis, uuid, ignore_missing=False, ) - self.assertIsNone(self.conn.baremetal.find_chassis(uuid)) - self.assertIsNone(self.conn.baremetal.delete_chassis(uuid)) + self.assertIsNone(self.operator_cloud.baremetal.find_chassis(uuid)) + self.assertIsNone(self.operator_cloud.baremetal.delete_chassis(uuid)) class TestBareMetalChassisFields(base.BaseBaremetalTest): @@ -76,7 +80,9 @@ class TestBareMetalChassisFields(base.BaseBaremetalTest): def test_chassis_fields(self): self.create_chassis(description='something') - result = self.conn.baremetal.chassis(fields=['uuid', 'extra']) + result = self.operator_cloud.baremetal.chassis( + fields=['uuid', 'extra'] + ) for ch in result: self.assertIsNotNone(ch.id) self.assertIsNone(ch.description) diff --git a/openstack/tests/functional/baremetal/test_baremetal_conductor.py b/openstack/tests/functional/baremetal/test_baremetal_conductor.py index bd7f64da6..e03d530e2 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_conductor.py +++ b/openstack/tests/functional/baremetal/test_baremetal_conductor.py @@ -19,10 +19,12 @@ class TestBareMetalConductor(base.BaseBaremetalTest): def test_list_get_conductor(self): node = self.create_node(name='node-name') - conductors = self.conn.baremetal.conductors() + conductors = self.operator_cloud.baremetal.conductors() hostname_list = [conductor.hostname for conductor in conductors] self.assertIn(node.conductor, hostname_list) - conductor1 = self.conn.baremetal.get_conductor(node.conductor) + conductor1 = self.operator_cloud.baremetal.get_conductor( + node.conductor + ) self.assertIsNotNone(conductor1.conductor_group) self.assertIsNotNone(conductor1.links) self.assertTrue(conductor1.alive) diff --git a/openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py b/openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py index b09d2680c..5630578cb 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py +++ b/openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py @@ -34,14 +34,16 @@ def test_baremetal_deploy_create_get_delete(self): deploy_template = self.create_deploy_template( name='CUSTOM_DEPLOY_TEMPLATE', steps=steps ) - loaded = self.conn.baremetal.get_deploy_template(deploy_template.id) + loaded = self.operator_cloud.baremetal.get_deploy_template( + deploy_template.id + ) self.assertEqual(loaded.id, deploy_template.id) - self.conn.baremetal.delete_deploy_template( + self.operator_cloud.baremetal.delete_deploy_template( deploy_template, ignore_missing=False ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.get_deploy_template, + self.operator_cloud.baremetal.get_deploy_template, deploy_template.id, ) @@ -63,20 +65,20 @@ def test_baremetal_deploy_template_list(self): deploy_template2 = self.create_deploy_template( name='CUSTOM_DEPLOY_TEMPLATE2', steps=steps ) - deploy_templates = self.conn.baremetal.deploy_templates() + deploy_templates = self.operator_cloud.baremetal.deploy_templates() ids = [template.id for template in deploy_templates] self.assertIn(deploy_template1.id, ids) self.assertIn(deploy_template2.id, ids) - deploy_templates_with_details = self.conn.baremetal.deploy_templates( - details=True + deploy_templates_with_details = ( + self.operator_cloud.baremetal.deploy_templates(details=True) ) for dp in deploy_templates_with_details: self.assertIsNotNone(dp.id) self.assertIsNotNone(dp.name) - deploy_tempalte_with_fields = self.conn.baremetal.deploy_templates( - fields=['uuid'] + deploy_tempalte_with_fields = ( + self.operator_cloud.baremetal.deploy_templates(fields=['uuid']) ) for dp in deploy_tempalte_with_fields: self.assertIsNotNone(dp.id) @@ -99,16 +101,16 @@ def test_baremetal_deploy_list_update_delete(self): self.assertFalse(deploy_template.extra) deploy_template.extra = {'answer': 42} - deploy_template = self.conn.baremetal.update_deploy_template( + deploy_template = self.operator_cloud.baremetal.update_deploy_template( deploy_template ) self.assertEqual({'answer': 42}, deploy_template.extra) - deploy_template = self.conn.baremetal.get_deploy_template( + deploy_template = self.operator_cloud.baremetal.get_deploy_template( deploy_template.id ) - self.conn.baremetal.delete_deploy_template( + self.operator_cloud.baremetal.delete_deploy_template( deploy_template.id, ignore_missing=False ) @@ -128,12 +130,12 @@ def test_baremetal_deploy_update(self): ) deploy_template.extra = {'answer': 42} - deploy_template = self.conn.baremetal.update_deploy_template( + deploy_template = self.operator_cloud.baremetal.update_deploy_template( deploy_template ) self.assertEqual({'answer': 42}, deploy_template.extra) - deploy_template = self.conn.baremetal.get_deploy_template( + deploy_template = self.operator_cloud.baremetal.get_deploy_template( deploy_template.id ) self.assertEqual({'answer': 42}, deploy_template.extra) @@ -151,13 +153,13 @@ def test_deploy_template_patch(self): } ] deploy_template = self.create_deploy_template(name=name, steps=steps) - deploy_template = self.conn.baremetal.patch_deploy_template( + deploy_template = self.operator_cloud.baremetal.patch_deploy_template( deploy_template, dict(path='/extra/answer', op='add', value=42) ) self.assertEqual({'answer': 42}, deploy_template.extra) self.assertEqual(name, deploy_template.name) - deploy_template = self.conn.baremetal.get_deploy_template( + deploy_template = self.operator_cloud.baremetal.get_deploy_template( deploy_template.id ) self.assertEqual({'answer': 42}, deploy_template.extra) @@ -166,13 +168,15 @@ def test_deploy_template_negative_non_existing(self): uuid = "bbb45f41-d4bc-4307-8d1d-32f95ce1e920" self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.get_deploy_template, + self.operator_cloud.baremetal.get_deploy_template, uuid, ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.delete_deploy_template, + self.operator_cloud.baremetal.delete_deploy_template, uuid, ignore_missing=False, ) - self.assertIsNone(self.conn.baremetal.delete_deploy_template(uuid)) + self.assertIsNone( + self.operator_cloud.baremetal.delete_deploy_template(uuid) + ) diff --git a/openstack/tests/functional/baremetal/test_baremetal_driver.py b/openstack/tests/functional/baremetal/test_baremetal_driver.py index 1c3b31eb8..67fa99140 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_driver.py +++ b/openstack/tests/functional/baremetal/test_baremetal_driver.py @@ -17,18 +17,18 @@ class TestBareMetalDriver(base.BaseBaremetalTest): def test_fake_hardware_get(self): - driver = self.conn.baremetal.get_driver('fake-hardware') + driver = self.operator_cloud.baremetal.get_driver('fake-hardware') self.assertEqual('fake-hardware', driver.name) self.assertNotEqual([], driver.hosts) def test_fake_hardware_list(self): - drivers = self.conn.baremetal.drivers() + drivers = self.operator_cloud.baremetal.drivers() self.assertIn('fake-hardware', [d.name for d in drivers]) def test_driver_negative_non_existing(self): self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.get_driver, + self.operator_cloud.baremetal.get_driver, 'not-a-driver', ) @@ -37,7 +37,7 @@ class TestBareMetalDriverDetails(base.BaseBaremetalTest): min_microversion = '1.30' def test_fake_hardware_get(self): - driver = self.conn.baremetal.get_driver('fake-hardware') + driver = self.operator_cloud.baremetal.get_driver('fake-hardware') self.assertEqual('fake-hardware', driver.name) for iface in ('boot', 'deploy', 'management', 'power'): self.assertIn( @@ -49,7 +49,7 @@ def test_fake_hardware_get(self): self.assertNotEqual([], driver.hosts) def test_fake_hardware_list_details(self): - drivers = self.conn.baremetal.drivers(details=True) + drivers = self.operator_cloud.baremetal.drivers(details=True) driver = [d for d in drivers if d.name == 'fake-hardware'][0] for iface in ('boot', 'deploy', 'management', 'power'): self.assertIn( diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index a1a731304..92bbd68ff 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -28,16 +28,16 @@ def test_node_create_get_delete(self): # NOTE(dtantsur): get_node and find_node only differ in handing missing # nodes, otherwise they are identical. for call, ident in [ - (self.conn.baremetal.get_node, self.node_id), - (self.conn.baremetal.get_node, 'node-name'), - (self.conn.baremetal.find_node, self.node_id), - (self.conn.baremetal.find_node, 'node-name'), + (self.operator_cloud.baremetal.get_node, self.node_id), + (self.operator_cloud.baremetal.get_node, 'node-name'), + (self.operator_cloud.baremetal.find_node, self.node_id), + (self.operator_cloud.baremetal.find_node, 'node-name'), ]: found = call(ident) self.assertEqual(node.id, found.id) self.assertEqual(node.name, found.name) - with_fields = self.conn.baremetal.get_node( + with_fields = self.operator_cloud.baremetal.get_node( 'node-name', fields=['uuid', 'driver', 'instance_id'] ) self.assertEqual(node.id, with_fields.id) @@ -45,13 +45,13 @@ def test_node_create_get_delete(self): self.assertIsNone(with_fields.name) self.assertIsNone(with_fields.provision_state) - nodes = self.conn.baremetal.nodes() + nodes = self.operator_cloud.baremetal.nodes() self.assertIn(node.id, [n.id for n in nodes]) - self.conn.baremetal.delete_node(node, ignore_missing=False) + self.operator_cloud.baremetal.delete_node(node, ignore_missing=False) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.get_node, + self.operator_cloud.baremetal.get_node, self.node_id, ) @@ -61,10 +61,10 @@ def test_node_create_in_available(self): self.assertEqual(node.driver, 'fake-hardware') self.assertEqual(node.provision_state, 'available') - self.conn.baremetal.delete_node(node, ignore_missing=False) + self.operator_cloud.baremetal.delete_node(node, ignore_missing=False) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.get_node, + self.operator_cloud.baremetal.get_node, self.node_id, ) @@ -74,40 +74,46 @@ def test_node_update(self): node.extra = {'answer': 42} instance_uuid = str(uuid.uuid4()) - node = self.conn.baremetal.update_node(node, instance_id=instance_uuid) + node = self.operator_cloud.baremetal.update_node( + node, instance_id=instance_uuid + ) self.assertEqual('new-name', node.name) self.assertEqual({'answer': 42}, node.extra) self.assertEqual(instance_uuid, node.instance_id) - node = self.conn.baremetal.get_node('new-name') + node = self.operator_cloud.baremetal.get_node('new-name') self.assertEqual('new-name', node.name) self.assertEqual({'answer': 42}, node.extra) self.assertEqual(instance_uuid, node.instance_id) - node = self.conn.baremetal.update_node(node, instance_id=None) + node = self.operator_cloud.baremetal.update_node( + node, instance_id=None + ) self.assertIsNone(node.instance_id) - node = self.conn.baremetal.get_node('new-name') + node = self.operator_cloud.baremetal.get_node('new-name') self.assertIsNone(node.instance_id) def test_node_update_by_name(self): self.create_node(name='node-name', extra={'foo': 'bar'}) instance_uuid = str(uuid.uuid4()) - node = self.conn.baremetal.update_node( + node = self.operator_cloud.baremetal.update_node( 'node-name', instance_id=instance_uuid, extra={'answer': 42} ) self.assertEqual({'answer': 42}, node.extra) self.assertEqual(instance_uuid, node.instance_id) - node = self.conn.baremetal.get_node('node-name') + node = self.operator_cloud.baremetal.get_node('node-name') self.assertEqual({'answer': 42}, node.extra) self.assertEqual(instance_uuid, node.instance_id) - node = self.conn.baremetal.update_node('node-name', instance_id=None) + node = self.operator_cloud.baremetal.update_node( + 'node-name', instance_id=None + ) self.assertIsNone(node.instance_id) - node = self.conn.baremetal.get_node('node-name') + node = self.operator_cloud.baremetal.get_node('node-name') self.assertIsNone(node.instance_id) def test_node_patch(self): @@ -115,7 +121,7 @@ def test_node_patch(self): node.name = 'new-name' instance_uuid = str(uuid.uuid4()) - node = self.conn.baremetal.patch_node( + node = self.operator_cloud.baremetal.patch_node( node, [ dict(path='/instance_id', op='replace', value=instance_uuid), @@ -126,12 +132,12 @@ def test_node_patch(self): self.assertEqual({'foo': 'bar', 'answer': 42}, node.extra) self.assertEqual(instance_uuid, node.instance_id) - node = self.conn.baremetal.get_node('new-name') + node = self.operator_cloud.baremetal.get_node('new-name') self.assertEqual('new-name', node.name) self.assertEqual({'foo': 'bar', 'answer': 42}, node.extra) self.assertEqual(instance_uuid, node.instance_id) - node = self.conn.baremetal.patch_node( + node = self.operator_cloud.baremetal.patch_node( node, [ dict(path='/instance_id', op='remove'), @@ -141,7 +147,7 @@ def test_node_patch(self): self.assertIsNone(node.instance_id) self.assertNotIn('answer', node.extra) - node = self.conn.baremetal.get_node('new-name') + node = self.operator_cloud.baremetal.get_node('new-name') self.assertIsNone(node.instance_id) self.assertNotIn('answer', node.extra) @@ -149,7 +155,7 @@ def test_node_list_update_delete(self): self.create_node(name='node-name', extra={'foo': 'bar'}) node = next( n - for n in self.conn.baremetal.nodes( + for n in self.operator_cloud.baremetal.nodes( details=True, provision_state='enroll', is_maintenance=False, @@ -160,8 +166,8 @@ def test_node_list_update_delete(self): self.assertEqual(node.extra, {'foo': 'bar'}) # This test checks that resources returned from listing are usable - self.conn.baremetal.update_node(node, extra={'foo': 42}) - self.conn.baremetal.delete_node(node, ignore_missing=False) + self.operator_cloud.baremetal.update_node(node, extra={'foo': 42}) + self.operator_cloud.baremetal.delete_node(node, ignore_missing=False) def test_node_create_in_enroll_provide(self): node = self.create_node() @@ -172,10 +178,12 @@ def test_node_create_in_enroll_provide(self): self.assertIsNone(node.power_state) self.assertFalse(node.is_maintenance) - self.conn.baremetal.set_node_provision_state(node, 'manage', wait=True) + self.operator_cloud.baremetal.set_node_provision_state( + node, 'manage', wait=True + ) self.assertEqual(node.provision_state, 'manageable') - self.conn.baremetal.set_node_provision_state( + self.operator_cloud.baremetal.set_node_provision_state( node, 'provide', wait=True ) self.assertEqual(node.provision_state, 'available') @@ -190,12 +198,12 @@ def test_node_create_in_enroll_provide_by_name(self): self.assertIsNone(node.power_state) self.assertFalse(node.is_maintenance) - node = self.conn.baremetal.set_node_provision_state( + node = self.operator_cloud.baremetal.set_node_provision_state( name, 'manage', wait=True ) self.assertEqual(node.provision_state, 'manageable') - node = self.conn.baremetal.set_node_provision_state( + node = self.operator_cloud.baremetal.set_node_provision_state( name, 'provide', wait=True ) self.assertEqual(node.provision_state, 'available') @@ -204,18 +212,22 @@ def test_node_power_state(self): node = self.create_node() self.assertIsNone(node.power_state) - self.conn.baremetal.set_node_power_state(node, 'power on', wait=True) - node = self.conn.baremetal.get_node(node.id) + self.operator_cloud.baremetal.set_node_power_state( + node, 'power on', wait=True + ) + node = self.operator_cloud.baremetal.get_node(node.id) self.assertEqual('power on', node.power_state) - self.conn.baremetal.set_node_power_state(node, 'power off', wait=True) - node = self.conn.baremetal.get_node(node.id) + self.operator_cloud.baremetal.set_node_power_state( + node, 'power off', wait=True + ) + node = self.operator_cloud.baremetal.get_node(node.id) self.assertEqual('power off', node.power_state) def test_node_validate(self): node = self.create_node() # Fake hardware passes validation for all interfaces - result = self.conn.baremetal.validate_node(node) + result = self.operator_cloud.baremetal.validate_node(node) for iface in ('boot', 'deploy', 'management', 'power'): self.assertTrue(result[iface].result) self.assertFalse(result[iface].reason) @@ -223,28 +235,30 @@ def test_node_validate(self): def test_node_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" self.assertRaises( - exceptions.NotFoundException, self.conn.baremetal.get_node, uuid + exceptions.NotFoundException, + self.operator_cloud.baremetal.get_node, + uuid, ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.find_node, + self.operator_cloud.baremetal.find_node, uuid, ignore_missing=False, ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.delete_node, + self.operator_cloud.baremetal.delete_node, uuid, ignore_missing=False, ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.update_node, + self.operator_cloud.baremetal.update_node, uuid, name='new-name', ) - self.assertIsNone(self.conn.baremetal.find_node(uuid)) - self.assertIsNone(self.conn.baremetal.delete_node(uuid)) + self.assertIsNone(self.operator_cloud.baremetal.find_node(uuid)) + self.assertIsNone(self.operator_cloud.baremetal.delete_node(uuid)) def test_maintenance(self): reason = "Prepating for taking over the world" @@ -254,27 +268,27 @@ def test_maintenance(self): self.assertIsNone(node.maintenance_reason) # Initial setting without the reason - node = self.conn.baremetal.set_node_maintenance(node) + node = self.operator_cloud.baremetal.set_node_maintenance(node) self.assertTrue(node.is_maintenance) self.assertIsNone(node.maintenance_reason) # Updating the reason later - node = self.conn.baremetal.set_node_maintenance(node, reason) + node = self.operator_cloud.baremetal.set_node_maintenance(node, reason) self.assertTrue(node.is_maintenance) self.assertEqual(reason, node.maintenance_reason) # Removing the reason later - node = self.conn.baremetal.set_node_maintenance(node) + node = self.operator_cloud.baremetal.set_node_maintenance(node) self.assertTrue(node.is_maintenance) self.assertIsNone(node.maintenance_reason) # Unsetting maintenance - node = self.conn.baremetal.unset_node_maintenance(node) + node = self.operator_cloud.baremetal.unset_node_maintenance(node) self.assertFalse(node.is_maintenance) self.assertIsNone(node.maintenance_reason) # Initial setting with the reason - node = self.conn.baremetal.set_node_maintenance(node, reason) + node = self.operator_cloud.baremetal.set_node_maintenance(node, reason) self.assertTrue(node.is_maintenance) self.assertEqual(reason, node.maintenance_reason) @@ -284,44 +298,50 @@ def test_maintenance_via_update(self): node = self.create_node() # Initial setting without the reason - node = self.conn.baremetal.update_node(node, is_maintenance=True) + node = self.operator_cloud.baremetal.update_node( + node, is_maintenance=True + ) self.assertTrue(node.is_maintenance) self.assertIsNone(node.maintenance_reason) # Make sure the change has effect on the remote side. - node = self.conn.baremetal.get_node(node.id) + node = self.operator_cloud.baremetal.get_node(node.id) self.assertTrue(node.is_maintenance) self.assertIsNone(node.maintenance_reason) # Updating the reason later - node = self.conn.baremetal.update_node(node, maintenance_reason=reason) + node = self.operator_cloud.baremetal.update_node( + node, maintenance_reason=reason + ) self.assertTrue(node.is_maintenance) self.assertEqual(reason, node.maintenance_reason) # Make sure the change has effect on the remote side. - node = self.conn.baremetal.get_node(node.id) + node = self.operator_cloud.baremetal.get_node(node.id) self.assertTrue(node.is_maintenance) self.assertEqual(reason, node.maintenance_reason) # Unsetting maintenance - node = self.conn.baremetal.update_node(node, is_maintenance=False) + node = self.operator_cloud.baremetal.update_node( + node, is_maintenance=False + ) self.assertFalse(node.is_maintenance) self.assertIsNone(node.maintenance_reason) # Make sure the change has effect on the remote side. - node = self.conn.baremetal.get_node(node.id) + node = self.operator_cloud.baremetal.get_node(node.id) self.assertFalse(node.is_maintenance) self.assertIsNone(node.maintenance_reason) # Initial setting with the reason - node = self.conn.baremetal.update_node( + node = self.operator_cloud.baremetal.update_node( node, is_maintenance=True, maintenance_reason=reason ) self.assertTrue(node.is_maintenance) self.assertEqual(reason, node.maintenance_reason) # Make sure the change has effect on the remote side. - node = self.conn.baremetal.get_node(node.id) + node = self.operator_cloud.baremetal.get_node(node.id) self.assertTrue(node.is_maintenance) self.assertEqual(reason, node.maintenance_reason) @@ -335,44 +355,48 @@ def test_retired(self): node = self.create_node() # Set retired without reason - node = self.conn.baremetal.update_node(node, is_retired=True) + node = self.operator_cloud.baremetal.update_node(node, is_retired=True) self.assertTrue(node.is_retired) self.assertIsNone(node.retired_reason) # Verify set retired on server side - node = self.conn.baremetal.get_node(node.id) + node = self.operator_cloud.baremetal.get_node(node.id) self.assertTrue(node.is_retired) self.assertIsNone(node.retired_reason) # Add the reason - node = self.conn.baremetal.update_node(node, retired_reason=reason) + node = self.operator_cloud.baremetal.update_node( + node, retired_reason=reason + ) self.assertTrue(node.is_retired) self.assertEqual(reason, node.retired_reason) # Verify the reason on server side - node = self.conn.baremetal.get_node(node.id) + node = self.operator_cloud.baremetal.get_node(node.id) self.assertTrue(node.is_retired) self.assertEqual(reason, node.retired_reason) # Unset retired - node = self.conn.baremetal.update_node(node, is_retired=False) + node = self.operator_cloud.baremetal.update_node( + node, is_retired=False + ) self.assertFalse(node.is_retired) self.assertIsNone(node.retired_reason) # Verify on server side - node = self.conn.baremetal.get_node(node.id) + node = self.operator_cloud.baremetal.get_node(node.id) self.assertFalse(node.is_retired) self.assertIsNone(node.retired_reason) # Set retired with reason - node = self.conn.baremetal.update_node( + node = self.operator_cloud.baremetal.update_node( node, is_retired=True, retired_reason=reason ) self.assertTrue(node.is_retired) self.assertEqual(reason, node.retired_reason) # Verify on server side - node = self.conn.baremetal.get_node(node.id) + node = self.operator_cloud.baremetal.get_node(node.id) self.assertTrue(node.is_retired) self.assertEqual(reason, node.retired_reason) @@ -382,7 +406,7 @@ def test_retired_in_available(self): # Set retired when node state available should fail! self.assertRaises( exceptions.ConflictException, - self.conn.baremetal.update_node, + self.operator_cloud.baremetal.update_node, node, is_retired=True, ) @@ -393,7 +417,7 @@ class TestBareMetalNodeFields(base.BaseBaremetalTest): def test_node_fields(self): self.create_node() - result = self.conn.baremetal.nodes( + result = self.operator_cloud.baremetal.nodes( fields=['uuid', 'name', 'instance_id'] ) for item in result: @@ -410,11 +434,13 @@ def setUp(self): self.vif_id = "200712fc-fdfb-47da-89a6-2d19f76c7618" def test_node_vif_attach_detach(self): - self.conn.baremetal.attach_vif_to_node(self.node, self.vif_id) + self.operator_cloud.baremetal.attach_vif_to_node( + self.node, self.vif_id + ) # NOTE(dtantsur): The noop networking driver is completely noop - the # VIF list does not return anything of value. - self.conn.baremetal.list_node_vifs(self.node) - res = self.conn.baremetal.detach_vif_from_node( + self.operator_cloud.baremetal.list_node_vifs(self.node) + res = self.operator_cloud.baremetal.detach_vif_from_node( self.node, self.vif_id, ignore_missing=False ) self.assertTrue(res) @@ -423,18 +449,18 @@ def test_node_vif_negative(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.attach_vif_to_node, + self.operator_cloud.baremetal.attach_vif_to_node, uuid, self.vif_id, ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.list_node_vifs, + self.operator_cloud.baremetal.list_node_vifs, uuid, ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.detach_vif_from_node, + self.operator_cloud.baremetal.detach_vif_from_node, uuid, self.vif_id, ignore_missing=False, @@ -449,45 +475,45 @@ def setUp(self): self.node = self.create_node() def test_add_remove_node_trait(self): - node = self.conn.baremetal.get_node(self.node) + node = self.operator_cloud.baremetal.get_node(self.node) self.assertEqual([], node.traits) - self.conn.baremetal.add_node_trait(self.node, 'CUSTOM_FAKE') + self.operator_cloud.baremetal.add_node_trait(self.node, 'CUSTOM_FAKE') self.assertEqual(['CUSTOM_FAKE'], self.node.traits) - node = self.conn.baremetal.get_node(self.node) + node = self.operator_cloud.baremetal.get_node(self.node) self.assertEqual(['CUSTOM_FAKE'], node.traits) - self.conn.baremetal.add_node_trait(self.node, 'CUSTOM_REAL') + self.operator_cloud.baremetal.add_node_trait(self.node, 'CUSTOM_REAL') self.assertEqual( sorted(['CUSTOM_FAKE', 'CUSTOM_REAL']), sorted(self.node.traits) ) - node = self.conn.baremetal.get_node(self.node) + node = self.operator_cloud.baremetal.get_node(self.node) self.assertEqual( sorted(['CUSTOM_FAKE', 'CUSTOM_REAL']), sorted(node.traits) ) - self.conn.baremetal.remove_node_trait( + self.operator_cloud.baremetal.remove_node_trait( node, 'CUSTOM_FAKE', ignore_missing=False ) self.assertEqual(['CUSTOM_REAL'], self.node.traits) - node = self.conn.baremetal.get_node(self.node) + node = self.operator_cloud.baremetal.get_node(self.node) self.assertEqual(['CUSTOM_REAL'], node.traits) def test_set_node_traits(self): - node = self.conn.baremetal.get_node(self.node) + node = self.operator_cloud.baremetal.get_node(self.node) self.assertEqual([], node.traits) traits1 = ['CUSTOM_FAKE', 'CUSTOM_REAL'] traits2 = ['CUSTOM_FOOBAR'] - self.conn.baremetal.set_node_traits(self.node, traits1) + self.operator_cloud.baremetal.set_node_traits(self.node, traits1) self.assertEqual(sorted(traits1), sorted(self.node.traits)) - node = self.conn.baremetal.get_node(self.node) + node = self.operator_cloud.baremetal.get_node(self.node) self.assertEqual(sorted(traits1), sorted(node.traits)) - self.conn.baremetal.set_node_traits(self.node, traits2) + self.operator_cloud.baremetal.set_node_traits(self.node, traits2) self.assertEqual(['CUSTOM_FOOBAR'], self.node.traits) - node = self.conn.baremetal.get_node(self.node) + node = self.operator_cloud.baremetal.get_node(self.node) self.assertEqual(['CUSTOM_FOOBAR'], node.traits) @@ -497,5 +523,5 @@ class TestBareMetalNodeListFirmware(base.BaseBaremetalTest): def test_list_firmware(self): node = self.create_node(firmware_interface="no-firmware") self.assertEqual("no-firmware", node.firmware_interface) - result = self.conn.baremetal.list_node_firmware(node) + result = self.operator_cloud.baremetal.list_node_firmware(node) self.assertEqual({'firmware': []}, result) diff --git a/openstack/tests/functional/baremetal/test_baremetal_port.py b/openstack/tests/functional/baremetal/test_baremetal_port.py index 56e9ff9ae..69fd46dcb 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_port.py +++ b/openstack/tests/functional/baremetal/test_baremetal_port.py @@ -28,19 +28,21 @@ def test_port_create_get_delete(self): self.assertNotEqual(port.is_pxe_enabled, False) self.assertIsNone(port.port_group_id) - loaded = self.conn.baremetal.get_port(port.id) + loaded = self.operator_cloud.baremetal.get_port(port.id) self.assertEqual(loaded.id, port.id) self.assertIsNotNone(loaded.address) - with_fields = self.conn.baremetal.get_port( + with_fields = self.operator_cloud.baremetal.get_port( port.id, fields=['uuid', 'extra', 'node_id'] ) self.assertEqual(port.id, with_fields.id) self.assertIsNone(with_fields.address) - self.conn.baremetal.delete_port(port, ignore_missing=False) + self.operator_cloud.baremetal.delete_port(port, ignore_missing=False) self.assertRaises( - exceptions.NotFoundException, self.conn.baremetal.get_port, port.id + exceptions.NotFoundException, + self.operator_cloud.baremetal.get_port, + port.id, ) def test_port_list(self): @@ -51,13 +53,15 @@ def test_port_list(self): address='11:22:33:44:55:77', node_id=self.node.id ) - ports = self.conn.baremetal.ports(address='11:22:33:44:55:77') + ports = self.operator_cloud.baremetal.ports( + address='11:22:33:44:55:77' + ) self.assertEqual([p.id for p in ports], [port2.id]) - ports = self.conn.baremetal.ports(node=node2.id) + ports = self.operator_cloud.baremetal.ports(node=node2.id) self.assertEqual([p.id for p in ports], [port1.id]) - ports = self.conn.baremetal.ports(node='test-node') + ports = self.operator_cloud.baremetal.ports(node='test-node') self.assertEqual([p.id for p in ports], [port1.id]) def test_port_list_update_delete(self): @@ -67,26 +71,26 @@ def test_port_list_update_delete(self): extra={'foo': 'bar'}, ) port = next( - self.conn.baremetal.ports( + self.operator_cloud.baremetal.ports( details=True, address='11:22:33:44:55:66' ) ) self.assertEqual(port.extra, {'foo': 'bar'}) # This test checks that resources returned from listing are usable - self.conn.baremetal.update_port(port, extra={'foo': 42}) - self.conn.baremetal.delete_port(port, ignore_missing=False) + self.operator_cloud.baremetal.update_port(port, extra={'foo': 42}) + self.operator_cloud.baremetal.delete_port(port, ignore_missing=False) def test_port_update(self): port = self.create_port(address='11:22:33:44:55:66') port.address = '66:55:44:33:22:11' port.extra = {'answer': 42} - port = self.conn.baremetal.update_port(port) + port = self.operator_cloud.baremetal.update_port(port) self.assertEqual('66:55:44:33:22:11', port.address) self.assertEqual({'answer': 42}, port.extra) - port = self.conn.baremetal.get_port(port.id) + port = self.operator_cloud.baremetal.get_port(port.id) self.assertEqual('66:55:44:33:22:11', port.address) self.assertEqual({'answer': 42}, port.extra) @@ -94,41 +98,43 @@ def test_port_patch(self): port = self.create_port(address='11:22:33:44:55:66') port.address = '66:55:44:33:22:11' - port = self.conn.baremetal.patch_port( + port = self.operator_cloud.baremetal.patch_port( port, dict(path='/extra/answer', op='add', value=42) ) self.assertEqual('66:55:44:33:22:11', port.address) self.assertEqual({'answer': 42}, port.extra) - port = self.conn.baremetal.get_port(port.id) + port = self.operator_cloud.baremetal.get_port(port.id) self.assertEqual('66:55:44:33:22:11', port.address) self.assertEqual({'answer': 42}, port.extra) def test_port_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" self.assertRaises( - exceptions.NotFoundException, self.conn.baremetal.get_port, uuid + exceptions.NotFoundException, + self.operator_cloud.baremetal.get_port, + uuid, ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.find_port, + self.operator_cloud.baremetal.find_port, uuid, ignore_missing=False, ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.delete_port, + self.operator_cloud.baremetal.delete_port, uuid, ignore_missing=False, ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.update_port, + self.operator_cloud.baremetal.update_port, uuid, pxe_enabled=True, ) - self.assertIsNone(self.conn.baremetal.find_port(uuid)) - self.assertIsNone(self.conn.baremetal.delete_port(uuid)) + self.assertIsNone(self.operator_cloud.baremetal.find_port(uuid)) + self.assertIsNone(self.operator_cloud.baremetal.delete_port(uuid)) class TestBareMetalPortFields(base.BaseBaremetalTest): @@ -137,7 +143,9 @@ class TestBareMetalPortFields(base.BaseBaremetalTest): def test_port_fields(self): self.create_node() self.create_port(address='11:22:33:44:55:66') - result = self.conn.baremetal.ports(fields=['uuid', 'node_id']) + result = self.operator_cloud.baremetal.ports( + fields=['uuid', 'node_id'] + ) for item in result: self.assertIsNotNone(item.id) self.assertIsNone(item.address) diff --git a/openstack/tests/functional/baremetal/test_baremetal_port_group.py b/openstack/tests/functional/baremetal/test_baremetal_port_group.py index 4aaf76df3..5e6f3f640 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_port_group.py +++ b/openstack/tests/functional/baremetal/test_baremetal_port_group.py @@ -25,20 +25,22 @@ def setUp(self): def test_port_group_create_get_delete(self): port_group = self.create_port_group() - loaded = self.conn.baremetal.get_port_group(port_group.id) + loaded = self.operator_cloud.baremetal.get_port_group(port_group.id) self.assertEqual(loaded.id, port_group.id) self.assertIsNotNone(loaded.node_id) - with_fields = self.conn.baremetal.get_port_group( + with_fields = self.operator_cloud.baremetal.get_port_group( port_group.id, fields=['uuid', 'extra'] ) self.assertEqual(port_group.id, with_fields.id) self.assertIsNone(with_fields.node_id) - self.conn.baremetal.delete_port_group(port_group, ignore_missing=False) + self.operator_cloud.baremetal.delete_port_group( + port_group, ignore_missing=False + ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.get_port_group, + self.operator_cloud.baremetal.get_port_group, port_group.id, ) @@ -52,13 +54,15 @@ def test_port_list(self): address='11:22:33:44:55:77', node_id=self.node.id ) - pgs = self.conn.baremetal.port_groups(address='11:22:33:44:55:77') + pgs = self.operator_cloud.baremetal.port_groups( + address='11:22:33:44:55:77' + ) self.assertEqual([p.id for p in pgs], [pg2.id]) - pgs = self.conn.baremetal.port_groups(node=node2.id) + pgs = self.operator_cloud.baremetal.port_groups(node=node2.id) self.assertEqual([p.id for p in pgs], [pg1.id]) - pgs = self.conn.baremetal.port_groups(node='test-node') + pgs = self.operator_cloud.baremetal.port_groups(node='test-node') self.assertEqual([p.id for p in pgs], [pg1.id]) def test_port_list_update_delete(self): @@ -66,63 +70,77 @@ def test_port_list_update_delete(self): address='11:22:33:44:55:66', extra={'foo': 'bar'} ) port_group = next( - self.conn.baremetal.port_groups( + self.operator_cloud.baremetal.port_groups( details=True, address='11:22:33:44:55:66' ) ) self.assertEqual(port_group.extra, {'foo': 'bar'}) # This test checks that resources returned from listing are usable - self.conn.baremetal.update_port_group(port_group, extra={'foo': 42}) - self.conn.baremetal.delete_port_group(port_group, ignore_missing=False) + self.operator_cloud.baremetal.update_port_group( + port_group, extra={'foo': 42} + ) + self.operator_cloud.baremetal.delete_port_group( + port_group, ignore_missing=False + ) def test_port_group_update(self): port_group = self.create_port_group() port_group.extra = {'answer': 42} - port_group = self.conn.baremetal.update_port_group(port_group) + port_group = self.operator_cloud.baremetal.update_port_group( + port_group + ) self.assertEqual({'answer': 42}, port_group.extra) - port_group = self.conn.baremetal.get_port_group(port_group.id) + port_group = self.operator_cloud.baremetal.get_port_group( + port_group.id + ) self.assertEqual({'answer': 42}, port_group.extra) def test_port_group_patch(self): port_group = self.create_port_group() - port_group = self.conn.baremetal.patch_port_group( + port_group = self.operator_cloud.baremetal.patch_port_group( port_group, dict(path='/extra/answer', op='add', value=42) ) self.assertEqual({'answer': 42}, port_group.extra) - port_group = self.conn.baremetal.get_port_group(port_group.id) + port_group = self.operator_cloud.baremetal.get_port_group( + port_group.id + ) self.assertEqual({'answer': 42}, port_group.extra) def test_port_group_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.get_port_group, + self.operator_cloud.baremetal.get_port_group, uuid, ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.find_port_group, + self.operator_cloud.baremetal.find_port_group, uuid, ignore_missing=False, ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.delete_port_group, + self.operator_cloud.baremetal.delete_port_group, uuid, ignore_missing=False, ) - self.assertIsNone(self.conn.baremetal.find_port_group(uuid)) - self.assertIsNone(self.conn.baremetal.delete_port_group(uuid)) + self.assertIsNone(self.operator_cloud.baremetal.find_port_group(uuid)) + self.assertIsNone( + self.operator_cloud.baremetal.delete_port_group(uuid) + ) def test_port_group_fields(self): self.create_node() self.create_port_group(address='11:22:33:44:55:66') - result = self.conn.baremetal.port_groups(fields=['uuid', 'name']) + result = self.operator_cloud.baremetal.port_groups( + fields=['uuid', 'name'] + ) for item in result: self.assertIsNotNone(item.id) self.assertIsNone(item.address) diff --git a/openstack/tests/functional/baremetal/test_baremetal_volume_connector.py b/openstack/tests/functional/baremetal/test_baremetal_volume_connector.py index cdcc11ce4..de0594af6 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_volume_connector.py +++ b/openstack/tests/functional/baremetal/test_baremetal_volume_connector.py @@ -23,43 +23,49 @@ def setUp(self): self.node = self.create_node(provision_state='enroll') def test_volume_connector_create_get_delete(self): - self.conn.baremetal.set_node_provision_state( + self.operator_cloud.baremetal.set_node_provision_state( self.node, 'manage', wait=True ) - self.conn.baremetal.set_node_power_state(self.node, 'power off') + self.operator_cloud.baremetal.set_node_power_state( + self.node, 'power off' + ) volume_connector = self.create_volume_connector( connector_id='iqn.2017-07.org.openstack:01:d9a51732c3f', type='iqn' ) - loaded = self.conn.baremetal.get_volume_connector(volume_connector.id) + loaded = self.operator_cloud.baremetal.get_volume_connector( + volume_connector.id + ) self.assertEqual(loaded.id, volume_connector.id) self.assertIsNotNone(loaded.node_id) - with_fields = self.conn.baremetal.get_volume_connector( + with_fields = self.operator_cloud.baremetal.get_volume_connector( volume_connector.id, fields=['uuid', 'extra'] ) self.assertEqual(volume_connector.id, with_fields.id) self.assertIsNone(with_fields.node_id) - self.conn.baremetal.delete_volume_connector( + self.operator_cloud.baremetal.delete_volume_connector( volume_connector, ignore_missing=False ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.get_volume_connector, + self.operator_cloud.baremetal.get_volume_connector, volume_connector.id, ) def test_volume_connector_list(self): node2 = self.create_node(name='test-node') - self.conn.baremetal.set_node_provision_state( + self.operator_cloud.baremetal.set_node_provision_state( node2, 'manage', wait=True ) - self.conn.baremetal.set_node_power_state(node2, 'power off') - self.conn.baremetal.set_node_provision_state( + self.operator_cloud.baremetal.set_node_power_state(node2, 'power off') + self.operator_cloud.baremetal.set_node_provision_state( self.node, 'manage', wait=True ) - self.conn.baremetal.set_node_power_state(self.node, 'power off') + self.operator_cloud.baremetal.set_node_power_state( + self.node, 'power off' + ) vc1 = self.create_volume_connector( connector_id='iqn.2018-07.org.openstack:01:d9a514g2c32', node_id=node2.id, @@ -71,20 +77,24 @@ def test_volume_connector_list(self): type='iqn', ) - vcs = self.conn.baremetal.volume_connectors(node=self.node.id) + vcs = self.operator_cloud.baremetal.volume_connectors( + node=self.node.id + ) self.assertEqual([v.id for v in vcs], [vc2.id]) - vcs = self.conn.baremetal.volume_connectors(node=node2.id) + vcs = self.operator_cloud.baremetal.volume_connectors(node=node2.id) self.assertEqual([v.id for v in vcs], [vc1.id]) - vcs = self.conn.baremetal.volume_connectors(node='test-node') + vcs = self.operator_cloud.baremetal.volume_connectors(node='test-node') self.assertEqual([v.id for v in vcs], [vc1.id]) def test_volume_connector_list_update_delete(self): - self.conn.baremetal.set_node_provision_state( + self.operator_cloud.baremetal.set_node_provision_state( self.node, 'manage', wait=True ) - self.conn.baremetal.set_node_power_state(self.node, 'power off') + self.operator_cloud.baremetal.set_node_power_state( + self.node, 'power off' + ) self.create_volume_connector( connector_id='iqn.2020-07.org.openstack:02:d9451472ce2', node_id=self.node.id, @@ -92,25 +102,27 @@ def test_volume_connector_list_update_delete(self): extra={'foo': 'bar'}, ) volume_connector = next( - self.conn.baremetal.volume_connectors( + self.operator_cloud.baremetal.volume_connectors( details=True, node=self.node.id ) ) self.assertEqual(volume_connector.extra, {'foo': 'bar'}) # This test checks that resources returned from listing are usable - self.conn.baremetal.update_volume_connector( + self.operator_cloud.baremetal.update_volume_connector( volume_connector, extra={'foo': 42} ) - self.conn.baremetal.delete_volume_connector( + self.operator_cloud.baremetal.delete_volume_connector( volume_connector, ignore_missing=False ) def test_volume_connector_update(self): - self.conn.baremetal.set_node_provision_state( + self.operator_cloud.baremetal.set_node_provision_state( self.node, 'manage', wait=True ) - self.conn.baremetal.set_node_power_state(self.node, 'power off') + self.operator_cloud.baremetal.set_node_power_state( + self.node, 'power off' + ) volume_connector = self.create_volume_connector( connector_id='iqn.2019-07.org.openstack:03:de45b472c40', node_id=self.node.id, @@ -118,33 +130,40 @@ def test_volume_connector_update(self): ) volume_connector.extra = {'answer': 42} - volume_connector = self.conn.baremetal.update_volume_connector( - volume_connector + volume_connector = ( + self.operator_cloud.baremetal.update_volume_connector( + volume_connector + ) ) self.assertEqual({'answer': 42}, volume_connector.extra) - volume_connector = self.conn.baremetal.get_volume_connector( + volume_connector = self.operator_cloud.baremetal.get_volume_connector( volume_connector.id ) self.assertEqual({'answer': 42}, volume_connector.extra) def test_volume_connector_patch(self): vol_conn_id = 'iqn.2020-07.org.openstack:04:de45b472c40' - self.conn.baremetal.set_node_provision_state( + self.operator_cloud.baremetal.set_node_provision_state( self.node, 'manage', wait=True ) - self.conn.baremetal.set_node_power_state(self.node, 'power off') + self.operator_cloud.baremetal.set_node_power_state( + self.node, 'power off' + ) volume_connector = self.create_volume_connector( connector_id=vol_conn_id, node_id=self.node.id, type='iqn' ) - volume_connector = self.conn.baremetal.patch_volume_connector( - volume_connector, dict(path='/extra/answer', op='add', value=42) + volume_connector = ( + self.operator_cloud.baremetal.patch_volume_connector( + volume_connector, + dict(path='/extra/answer', op='add', value=42), + ) ) self.assertEqual({'answer': 42}, volume_connector.extra) self.assertEqual(vol_conn_id, volume_connector.connector_id) - volume_connector = self.conn.baremetal.get_volume_connector( + volume_connector = self.operator_cloud.baremetal.get_volume_connector( volume_connector.id ) self.assertEqual({'answer': 42}, volume_connector.extra) @@ -153,36 +172,42 @@ def test_volume_connector_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.get_volume_connector, + self.operator_cloud.baremetal.get_volume_connector, uuid, ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.find_volume_connector, + self.operator_cloud.baremetal.find_volume_connector, uuid, ignore_missing=False, ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.delete_volume_connector, + self.operator_cloud.baremetal.delete_volume_connector, uuid, ignore_missing=False, ) - self.assertIsNone(self.conn.baremetal.find_volume_connector(uuid)) - self.assertIsNone(self.conn.baremetal.delete_volume_connector(uuid)) + self.assertIsNone( + self.operator_cloud.baremetal.find_volume_connector(uuid) + ) + self.assertIsNone( + self.operator_cloud.baremetal.delete_volume_connector(uuid) + ) def test_volume_connector_fields(self): self.create_node() - self.conn.baremetal.set_node_provision_state( + self.operator_cloud.baremetal.set_node_provision_state( self.node, 'manage', wait=True ) - self.conn.baremetal.set_node_power_state(self.node, 'power off') + self.operator_cloud.baremetal.set_node_power_state( + self.node, 'power off' + ) self.create_volume_connector( connector_id='iqn.2018-08.org.openstack:04:de45f37c48', node_id=self.node.id, type='iqn', ) - result = self.conn.baremetal.volume_connectors( + result = self.operator_cloud.baremetal.volume_connectors( fields=['uuid', 'node_id'] ) for item in result: diff --git a/openstack/tests/functional/baremetal/test_baremetal_volume_target.py b/openstack/tests/functional/baremetal/test_baremetal_volume_target.py index b64d7d69b..b6d22fa8b 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_volume_target.py +++ b/openstack/tests/functional/baremetal/test_baremetal_volume_target.py @@ -23,45 +23,51 @@ def setUp(self): self.node = self.create_node(provision_state='enroll') def test_volume_target_create_get_delete(self): - self.conn.baremetal.set_node_provision_state( + self.operator_cloud.baremetal.set_node_provision_state( self.node, 'manage', wait=True ) - self.conn.baremetal.set_node_power_state(self.node, 'power off') + self.operator_cloud.baremetal.set_node_power_state( + self.node, 'power off' + ) volume_target = self.create_volume_target( boot_index=0, volume_id='04452bed-5367-4202-8bf5-de4335ac56d2', volume_type='iscsi', ) - loaded = self.conn.baremetal.get_volume_target(volume_target.id) + loaded = self.operator_cloud.baremetal.get_volume_target( + volume_target.id + ) self.assertEqual(loaded.id, volume_target.id) self.assertIsNotNone(loaded.node_id) - with_fields = self.conn.baremetal.get_volume_target( + with_fields = self.operator_cloud.baremetal.get_volume_target( volume_target.id, fields=['uuid', 'extra'] ) self.assertEqual(volume_target.id, with_fields.id) self.assertIsNone(with_fields.node_id) - self.conn.baremetal.delete_volume_target( + self.operator_cloud.baremetal.delete_volume_target( volume_target, ignore_missing=False ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.get_volume_target, + self.operator_cloud.baremetal.get_volume_target, volume_target.id, ) def test_volume_target_list(self): node2 = self.create_node(name='test-node') - self.conn.baremetal.set_node_provision_state( + self.operator_cloud.baremetal.set_node_provision_state( node2, 'manage', wait=True ) - self.conn.baremetal.set_node_power_state(node2, 'power off') - self.conn.baremetal.set_node_provision_state( + self.operator_cloud.baremetal.set_node_power_state(node2, 'power off') + self.operator_cloud.baremetal.set_node_provision_state( self.node, 'manage', wait=True ) - self.conn.baremetal.set_node_power_state(self.node, 'power off') + self.operator_cloud.baremetal.set_node_power_state( + self.node, 'power off' + ) vt1 = self.create_volume_target( boot_index=0, volume_id='bd4d008c-7d31-463d-abf9-6c23d9d55f7f', @@ -75,21 +81,23 @@ def test_volume_target_list(self): volume_type='iscsi', ) - vts = self.conn.baremetal.volume_targets(node=self.node.id) + vts = self.operator_cloud.baremetal.volume_targets(node=self.node.id) self.assertEqual([v.id for v in vts], [vt2.id]) - vts = self.conn.baremetal.volume_targets(node=node2.id) + vts = self.operator_cloud.baremetal.volume_targets(node=node2.id) self.assertEqual([v.id for v in vts], [vt1.id]) - vts = self.conn.baremetal.volume_targets(node='test-node') + vts = self.operator_cloud.baremetal.volume_targets(node='test-node') self.assertEqual([v.id for v in vts], [vt1.id]) - vts_with_details = self.conn.baremetal.volume_targets(details=True) + vts_with_details = self.operator_cloud.baremetal.volume_targets( + details=True + ) for i in vts_with_details: self.assertIsNotNone(i.id) self.assertIsNotNone(i.volume_type) - vts_with_fields = self.conn.baremetal.volume_targets( + vts_with_fields = self.operator_cloud.baremetal.volume_targets( fields=['uuid', 'node_uuid'] ) for i in vts_with_fields: @@ -98,10 +106,12 @@ def test_volume_target_list(self): self.assertIsNotNone(i.node_id) def test_volume_target_list_update_delete(self): - self.conn.baremetal.set_node_provision_state( + self.operator_cloud.baremetal.set_node_provision_state( self.node, 'manage', wait=True ) - self.conn.baremetal.set_node_power_state(self.node, 'power off') + self.operator_cloud.baremetal.set_node_power_state( + self.node, 'power off' + ) self.create_volume_target( boot_index=0, volume_id='04452bed-5367-4202-8bf5-de4335ac57h3', @@ -110,23 +120,27 @@ def test_volume_target_list_update_delete(self): extra={'foo': 'bar'}, ) volume_target = next( - self.conn.baremetal.volume_targets(details=True, node=self.node.id) + self.operator_cloud.baremetal.volume_targets( + details=True, node=self.node.id + ) ) self.assertEqual(volume_target.extra, {'foo': 'bar'}) # This test checks that resources returned from listing are usable - self.conn.baremetal.update_volume_target( + self.operator_cloud.baremetal.update_volume_target( volume_target, extra={'foo': 42} ) - self.conn.baremetal.delete_volume_target( + self.operator_cloud.baremetal.delete_volume_target( volume_target, ignore_missing=False ) def test_volume_target_update(self): - self.conn.baremetal.set_node_provision_state( + self.operator_cloud.baremetal.set_node_provision_state( self.node, 'manage', wait=True ) - self.conn.baremetal.set_node_power_state(self.node, 'power off') + self.operator_cloud.baremetal.set_node_power_state( + self.node, 'power off' + ) volume_target = self.create_volume_target( boot_index=0, volume_id='04452bed-5367-4202-8bf5-de4335ac53h7', @@ -135,18 +149,24 @@ def test_volume_target_update(self): ) volume_target.extra = {'answer': 42} - volume_target = self.conn.baremetal.update_volume_target(volume_target) + volume_target = self.operator_cloud.baremetal.update_volume_target( + volume_target + ) self.assertEqual({'answer': 42}, volume_target.extra) - volume_target = self.conn.baremetal.get_volume_target(volume_target.id) + volume_target = self.operator_cloud.baremetal.get_volume_target( + volume_target.id + ) self.assertEqual({'answer': 42}, volume_target.extra) def test_volume_target_patch(self): vol_targ_id = '04452bed-5367-4202-9cg6-de4335ac53h7' - self.conn.baremetal.set_node_provision_state( + self.operator_cloud.baremetal.set_node_provision_state( self.node, 'manage', wait=True ) - self.conn.baremetal.set_node_power_state(self.node, 'power off') + self.operator_cloud.baremetal.set_node_power_state( + self.node, 'power off' + ) volume_target = self.create_volume_target( boot_index=0, volume_id=vol_targ_id, @@ -154,49 +174,59 @@ def test_volume_target_patch(self): volume_type='isci', ) - volume_target = self.conn.baremetal.patch_volume_target( + volume_target = self.operator_cloud.baremetal.patch_volume_target( volume_target, dict(path='/extra/answer', op='add', value=42) ) self.assertEqual({'answer': 42}, volume_target.extra) self.assertEqual(vol_targ_id, volume_target.volume_id) - volume_target = self.conn.baremetal.get_volume_target(volume_target.id) + volume_target = self.operator_cloud.baremetal.get_volume_target( + volume_target.id + ) self.assertEqual({'answer': 42}, volume_target.extra) def test_volume_target_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.get_volume_target, + self.operator_cloud.baremetal.get_volume_target, uuid, ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.find_volume_target, + self.operator_cloud.baremetal.find_volume_target, uuid, ignore_missing=False, ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.delete_volume_target, + self.operator_cloud.baremetal.delete_volume_target, uuid, ignore_missing=False, ) - self.assertIsNone(self.conn.baremetal.find_volume_target(uuid)) - self.assertIsNone(self.conn.baremetal.delete_volume_target(uuid)) + self.assertIsNone( + self.operator_cloud.baremetal.find_volume_target(uuid) + ) + self.assertIsNone( + self.operator_cloud.baremetal.delete_volume_target(uuid) + ) def test_volume_target_fields(self): self.create_node() - self.conn.baremetal.set_node_provision_state( + self.operator_cloud.baremetal.set_node_provision_state( self.node, 'manage', wait=True ) - self.conn.baremetal.set_node_power_state(self.node, 'power off') + self.operator_cloud.baremetal.set_node_power_state( + self.node, 'power off' + ) self.create_volume_target( boot_index=0, volume_id='04452bed-5367-4202-8bf5-99ae634d8971', node_id=self.node.id, volume_type='iscsi', ) - result = self.conn.baremetal.volume_targets(fields=['uuid', 'node_id']) + result = self.operator_cloud.baremetal.volume_targets( + fields=['uuid', 'node_id'] + ) for item in result: self.assertIsNotNone(item.id) diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index 17d25954f..a3e948e6d 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -50,9 +50,6 @@ class BaseFunctionalTest(base.TestCase): def setUp(self): super().setUp() - self.conn = connection.Connection(config=TEST_CLOUD_REGION) - _disable_keep_alive(self.conn) - self.config = openstack.config.OpenStackConfig() self._user_cloud_name = os.environ.get( @@ -220,13 +217,13 @@ def setUp(self): :returns: True if the service exists, otherwise False. """ - if not self.conn.has_service(service_type): + if not self.operator_cloud.has_service(service_type): self.skipTest(f'Service {service_type} not found in cloud') if not min_microversion: return - data = self.conn.session.get_endpoint_data( + data = self.operator_cloud.session.get_endpoint_data( service_type=service_type, **kwargs ) @@ -299,5 +296,5 @@ def setUp(self): # we only support v3, since v2 was deprecated in Queens (2018) - if not self.conn.has_service('identity', '3'): + if not self.user_cloud.has_service('identity', '3'): self.skipTest('identity service not supported by cloud') diff --git a/openstack/tests/functional/block_storage/v3/test_attachment.py b/openstack/tests/functional/block_storage/v3/test_attachment.py index b7b85752e..97b4380ab 100644 --- a/openstack/tests/functional/block_storage/v3/test_attachment.py +++ b/openstack/tests/functional/block_storage/v3/test_attachment.py @@ -64,15 +64,15 @@ def setUp(self): def tearDown(self): # Since delete_on_termination flag is set to True, we # don't need to cleanup the volume manually - result = self.conn.compute.delete_server(self.server.id) - self.conn.compute.wait_for_delete( + result = self.operator_cloud.compute.delete_server(self.server.id) + self.operator_cloud.compute.wait_for_delete( self.server, wait=self._wait_for_timeout ) self.assertIsNone(result) super().tearDown() def test_attachment(self): - attachment = self.conn.block_storage.create_attachment( + attachment = self.operator_cloud.block_storage.create_attachment( self.VOLUME_ID, connector={}, instance_id=self.server.id, diff --git a/openstack/tests/functional/block_storage/v3/test_availability_zone.py b/openstack/tests/functional/block_storage/v3/test_availability_zone.py index 2c3c6e074..36da98915 100644 --- a/openstack/tests/functional/block_storage/v3/test_availability_zone.py +++ b/openstack/tests/functional/block_storage/v3/test_availability_zone.py @@ -16,7 +16,9 @@ class TestAvailabilityZone(base.BaseFunctionalTest): def test_list(self): - availability_zones = list(self.conn.block_storage.availability_zones()) + availability_zones = list( + self.operator_cloud.block_storage.availability_zones() + ) self.assertGreater(len(availability_zones), 0) for az in availability_zones: diff --git a/openstack/tests/functional/block_storage/v3/test_block_storage_summary.py b/openstack/tests/functional/block_storage/v3/test_block_storage_summary.py index 1efff92a3..7de150f16 100644 --- a/openstack/tests/functional/block_storage/v3/test_block_storage_summary.py +++ b/openstack/tests/functional/block_storage/v3/test_block_storage_summary.py @@ -15,7 +15,7 @@ class TestBlockStorageSummary(base.BaseBlockStorageTest): def test_get(self): - sot = self.conn.block_storage.summary(all_projects=True) + sot = self.operator_cloud.block_storage.summary(all_projects=True) self.assertIn('total_size', sot) self.assertIn('total_count', sot) self.assertIn('metadata', sot) diff --git a/openstack/tests/functional/block_storage/v3/test_capabilities.py b/openstack/tests/functional/block_storage/v3/test_capabilities.py index 541b4d7c8..5ffbea8c9 100644 --- a/openstack/tests/functional/block_storage/v3/test_capabilities.py +++ b/openstack/tests/functional/block_storage/v3/test_capabilities.py @@ -25,7 +25,7 @@ def test_get(self): if service.binary == 'cinder-volume' ][0].host - sot = self.conn.block_storage.get_capabilities(host) + sot = self.operator_cloud.block_storage.get_capabilities(host) self.assertIn('description', sot) self.assertIn('display_name', sot) self.assertIn('driver_version', sot) diff --git a/openstack/tests/functional/block_storage/v3/test_default_type.py b/openstack/tests/functional/block_storage/v3/test_default_type.py index ce8bb42c1..7520ec632 100644 --- a/openstack/tests/functional/block_storage/v3/test_default_type.py +++ b/openstack/tests/functional/block_storage/v3/test_default_type.py @@ -30,21 +30,21 @@ def test_default_type(self): ).id # Set default type for a project - default_type = self.conn.block_storage.set_default_type( + default_type = self.operator_cloud.block_storage.set_default_type( self.PROJECT_ID, volume_type_id, ) self.assertIsInstance(default_type, _default_type.DefaultType) # Show default type for a project - default_type = self.conn.block_storage.show_default_type( + default_type = self.operator_cloud.block_storage.show_default_type( self.PROJECT_ID ) self.assertIsInstance(default_type, _default_type.DefaultType) self.assertEqual(volume_type_id, default_type.volume_type_id) # List all default types - default_types = self.conn.block_storage.default_types() + default_types = self.operator_cloud.block_storage.default_types() for default_type in default_types: self.assertIsInstance(default_type, _default_type.DefaultType) # There could be existing default types set in the environment @@ -53,7 +53,7 @@ def test_default_type(self): self.assertEqual(volume_type_id, default_type.volume_type_id) # Unset default type for a project - default_type = self.conn.block_storage.unset_default_type( + default_type = self.operator_cloud.block_storage.unset_default_type( self.PROJECT_ID ) self.assertIsNone(default_type) diff --git a/openstack/tests/functional/block_storage/v3/test_extension.py b/openstack/tests/functional/block_storage/v3/test_extension.py index 2313f9024..ecf582b1a 100644 --- a/openstack/tests/functional/block_storage/v3/test_extension.py +++ b/openstack/tests/functional/block_storage/v3/test_extension.py @@ -15,7 +15,7 @@ class Extensions(base.BaseBlockStorageTest): def test_get(self): - extensions = list(self.conn.block_storage.extensions()) + extensions = list(self.operator_cloud.block_storage.extensions()) for extension in extensions: self.assertIsInstance(extension.alias, str) diff --git a/openstack/tests/functional/block_storage/v3/test_group.py b/openstack/tests/functional/block_storage/v3/test_group.py index 7d6c735f8..86d402e13 100644 --- a/openstack/tests/functional/block_storage/v3/test_group.py +++ b/openstack/tests/functional/block_storage/v3/test_group.py @@ -23,18 +23,18 @@ def setUp(self): super().setUp() # there will always be at least one volume type, i.e. the default one - volume_types = list(self.conn.block_storage.types()) + volume_types = list(self.operator_cloud.block_storage.types()) self.volume_type = volume_types[0] group_type_name = self.getUniqueString() - self.group_type = self.conn.block_storage.create_group_type( + self.group_type = self.operator_cloud.block_storage.create_group_type( name=group_type_name, ) self.assertIsInstance(self.group_type, _group_type.GroupType) self.assertEqual(group_type_name, self.group_type.name) group_name = self.getUniqueString() - self.group = self.conn.block_storage.create_group( + self.group = self.operator_cloud.block_storage.create_group( name=group_name, group_type=self.group_type.id, volume_types=[self.volume_type.id], @@ -46,27 +46,31 @@ def tearDown(self): # we do this in tearDown rather than via 'addCleanup' since we need to # wait for the deletion of the group before moving onto the deletion of # the group type - self.conn.block_storage.delete_group(self.group, delete_volumes=True) - self.conn.block_storage.wait_for_delete(self.group) + self.operator_cloud.block_storage.delete_group( + self.group, delete_volumes=True + ) + self.operator_cloud.block_storage.wait_for_delete(self.group) - self.conn.block_storage.delete_group_type(self.group_type) - self.conn.block_storage.wait_for_delete(self.group_type) + self.operator_cloud.block_storage.delete_group_type(self.group_type) + self.operator_cloud.block_storage.wait_for_delete(self.group_type) super().tearDown() def test_group_type(self): # get - group_type = self.conn.block_storage.get_group_type(self.group_type.id) + group_type = self.operator_cloud.block_storage.get_group_type( + self.group_type.id + ) self.assertEqual(self.group_type.name, group_type.name) # find - group_type = self.conn.block_storage.find_group_type( + group_type = self.operator_cloud.block_storage.find_group_type( self.group_type.name, ) self.assertEqual(self.group_type.id, group_type.id) # list - group_types = list(self.conn.block_storage.group_types()) + group_types = list(self.operator_cloud.block_storage.group_types()) # other tests may have created group types and there can be defaults so # we don't assert that this is the *only* group type present self.assertIn(self.group_type.id, {g.id for g in group_types}) @@ -74,66 +78,76 @@ def test_group_type(self): # update group_type_name = self.getUniqueString() group_type_description = self.getUniqueString() - group_type = self.conn.block_storage.update_group_type( + group_type = self.operator_cloud.block_storage.update_group_type( self.group_type, name=group_type_name, description=group_type_description, ) self.assertIsInstance(group_type, _group_type.GroupType) - group_type = self.conn.block_storage.get_group_type(self.group_type.id) + group_type = self.operator_cloud.block_storage.get_group_type( + self.group_type.id + ) self.assertEqual(group_type_name, group_type.name) self.assertEqual(group_type_description, group_type.description) def test_group_type_group_specs(self): # create - group_type = self.conn.block_storage.create_group_type_group_specs( - self.group_type, - {'foo': 'bar', 'acme': 'buzz'}, + group_type = ( + self.operator_cloud.block_storage.create_group_type_group_specs( + self.group_type, + {'foo': 'bar', 'acme': 'buzz'}, + ) ) self.assertIsInstance(group_type, _group_type.GroupType) - group_type = self.conn.block_storage.get_group_type(self.group_type.id) + group_type = self.operator_cloud.block_storage.get_group_type( + self.group_type.id + ) self.assertEqual( {'foo': 'bar', 'acme': 'buzz'}, group_type.group_specs ) # get - spec = self.conn.block_storage.get_group_type_group_specs_property( + spec = self.operator_cloud.block_storage.get_group_type_group_specs_property( self.group_type, 'foo', ) self.assertEqual('bar', spec) # update - spec = self.conn.block_storage.update_group_type_group_specs_property( + spec = self.operator_cloud.block_storage.update_group_type_group_specs_property( self.group_type, 'foo', 'baz', ) self.assertEqual('baz', spec) - group_type = self.conn.block_storage.get_group_type(self.group_type.id) + group_type = self.operator_cloud.block_storage.get_group_type( + self.group_type.id + ) self.assertEqual( {'foo': 'baz', 'acme': 'buzz'}, group_type.group_specs ) # delete - self.conn.block_storage.delete_group_type_group_specs_property( + self.operator_cloud.block_storage.delete_group_type_group_specs_property( self.group_type, 'foo', ) - group_type = self.conn.block_storage.get_group_type(self.group_type.id) + group_type = self.operator_cloud.block_storage.get_group_type( + self.group_type.id + ) self.assertEqual({'acme': 'buzz'}, group_type.group_specs) def test_group(self): # get - group = self.conn.block_storage.get_group(self.group.id) + group = self.operator_cloud.block_storage.get_group(self.group.id) self.assertEqual(self.group.name, group.name) # find - group = self.conn.block_storage.find_group(self.group.name) + group = self.operator_cloud.block_storage.find_group(self.group.name) self.assertEqual(self.group.id, group.id) # list - groups = self.conn.block_storage.groups() + groups = self.operator_cloud.block_storage.groups() # other tests may have created groups and there can be defaults so we # don't assert that this is the *only* group present self.assertIn(self.group.id, {g.id for g in groups}) @@ -141,13 +155,13 @@ def test_group(self): # update group_name = self.getUniqueString() group_description = self.getUniqueString() - group = self.conn.block_storage.update_group( + group = self.operator_cloud.block_storage.update_group( self.group, name=group_name, description=group_description, ) self.assertIsInstance(group, _group.Group) - group = self.conn.block_storage.get_group(self.group.id) + group = self.operator_cloud.block_storage.get_group(self.group.id) self.assertEqual(group_name, group.name) self.assertEqual(group_description, group.description) @@ -157,13 +171,13 @@ def test_group_snapshot(self): # 'delete_volumes' flag) will handle this but we do need to wait for # the thing to be created volume_name = self.getUniqueString() - self.volume = self.conn.block_storage.create_volume( + self.volume = self.operator_cloud.block_storage.create_volume( name=volume_name, volume_type=self.volume_type.id, group_id=self.group.id, size=1, ) - self.conn.block_storage.wait_for_status( + self.operator_cloud.block_storage.wait_for_status( self.volume, status='available', failures=['error'], @@ -173,11 +187,13 @@ def test_group_snapshot(self): self.assertIsInstance(self.volume, _volume.Volume) group_snapshot_name = self.getUniqueString() - self.group_snapshot = self.conn.block_storage.create_group_snapshot( - name=group_snapshot_name, - group_id=self.group.id, + self.group_snapshot = ( + self.operator_cloud.block_storage.create_group_snapshot( + name=group_snapshot_name, + group_id=self.group.id, + ) ) - self.conn.block_storage.wait_for_status( + self.operator_cloud.block_storage.wait_for_status( self.group_snapshot, status='available', failures=['error'], @@ -190,19 +206,19 @@ def test_group_snapshot(self): ) # get - group_snapshot = self.conn.block_storage.get_group_snapshot( + group_snapshot = self.operator_cloud.block_storage.get_group_snapshot( self.group_snapshot.id, ) self.assertEqual(self.group_snapshot.name, group_snapshot.name) # find - group_snapshot = self.conn.block_storage.find_group_snapshot( + group_snapshot = self.operator_cloud.block_storage.find_group_snapshot( self.group_snapshot.name, ) self.assertEqual(self.group_snapshot.id, group_snapshot.id) # list - group_snapshots = self.conn.block_storage.group_snapshots() + group_snapshots = self.operator_cloud.block_storage.group_snapshots() # other tests may have created group snapshot and there can be defaults # so we don't assert that this is the *only* group snapshot present self.assertIn(self.group_snapshot.id, {g.id for g in group_snapshots}) @@ -210,5 +226,7 @@ def test_group_snapshot(self): # update (not supported) # delete - self.conn.block_storage.delete_group_snapshot(self.group_snapshot) - self.conn.block_storage.wait_for_delete(self.group_snapshot) + self.operator_cloud.block_storage.delete_group_snapshot( + self.group_snapshot + ) + self.operator_cloud.block_storage.wait_for_delete(self.group_snapshot) diff --git a/openstack/tests/functional/block_storage/v3/test_limits.py b/openstack/tests/functional/block_storage/v3/test_limits.py index 762f4f0b9..70e8ba072 100644 --- a/openstack/tests/functional/block_storage/v3/test_limits.py +++ b/openstack/tests/functional/block_storage/v3/test_limits.py @@ -16,7 +16,7 @@ class TestLimits(base.BaseBlockStorageTest): def test_get(self): - sot = self.conn.block_storage.get_limits() + sot = self.operator_cloud.block_storage.get_limits() self.assertIsNotNone(sot.absolute.max_total_backup_gigabytes) self.assertIsNotNone(sot.absolute.max_total_backups) self.assertIsNotNone(sot.absolute.max_total_snapshots) diff --git a/openstack/tests/functional/block_storage/v3/test_resource_filters.py b/openstack/tests/functional/block_storage/v3/test_resource_filters.py index 9252a104c..0b148c00c 100644 --- a/openstack/tests/functional/block_storage/v3/test_resource_filters.py +++ b/openstack/tests/functional/block_storage/v3/test_resource_filters.py @@ -16,7 +16,9 @@ class ResourceFilters(base.BaseBlockStorageTest): def test_get(self): - resource_filters = list(self.conn.block_storage.resource_filters()) + resource_filters = list( + self.operator_cloud.block_storage.resource_filters() + ) for rf in resource_filters: self.assertIsInstance(rf.filters, list) diff --git a/openstack/tests/functional/block_storage/v3/test_transfer.py b/openstack/tests/functional/block_storage/v3/test_transfer.py index 18504deec..786d15b7a 100644 --- a/openstack/tests/functional/block_storage/v3/test_transfer.py +++ b/openstack/tests/functional/block_storage/v3/test_transfer.py @@ -41,9 +41,11 @@ def tearDown(self): super().tearDown() def test_transfer(self): - if not utils.supports_microversion(self.conn.block_storage, "3.55"): + if not utils.supports_microversion( + self.operator_cloud.block_storage, "3.55" + ): self.skipTest("Cannot test new transfer API if MV < 3.55") - sot = self.conn.block_storage.create_transfer( + sot = self.operator_cloud.block_storage.create_transfer( volume_id=self.VOLUME_ID, name=self.VOLUME_NAME, ) diff --git a/openstack/tests/functional/cloud/test_project_cleanup.py b/openstack/tests/functional/cloud/test_project_cleanup.py index 6dfbf9cbb..35bf2f5f3 100644 --- a/openstack/tests/functional/cloud/test_project_cleanup.py +++ b/openstack/tests/functional/cloud/test_project_cleanup.py @@ -32,24 +32,22 @@ def setUp(self): if not self.user_cloud_alt: self.skipTest("Alternate demo cloud is required for this test") - self.conn = self.user_cloud_alt self.network_name = self.getUniqueString('network') def _create_network_resources(self): - conn = self.conn - self.net = conn.network.create_network( + self.net = self.user_cloud_alt.network.create_network( name=self.network_name, ) - self.subnet = conn.network.create_subnet( + self.subnet = self.user_cloud_alt.network.create_subnet( name=self.getUniqueString('subnet'), network_id=self.net.id, cidr='192.169.1.0/24', ip_version=4, ) - self.router = conn.network.create_router( + self.router = self.user_cloud_alt.network.create_router( name=self.getUniqueString('router') ) - conn.network.add_interface_to_router( + self.user_cloud_alt.network.add_interface_to_router( self.router.id, subnet_id=self.subnet.id ) @@ -58,7 +56,7 @@ def test_cleanup(self): status_queue: queue.Queue[resource.Resource] = queue.Queue() # First round - check no resources are old enough - self.conn.project_cleanup( + self.user_cloud_alt.project_cleanup( dry_run=True, wait_timeout=120, status_queue=status_queue, @@ -69,7 +67,7 @@ def test_cleanup(self): # Second round - resource evaluation function return false, ensure # nothing identified - self.conn.project_cleanup( + self.user_cloud_alt.project_cleanup( dry_run=True, wait_timeout=120, status_queue=status_queue, @@ -80,7 +78,7 @@ def test_cleanup(self): self.assertTrue(status_queue.empty()) # Third round - filters set too low - self.conn.project_cleanup( + self.user_cloud_alt.project_cleanup( dry_run=True, wait_timeout=120, status_queue=status_queue, @@ -96,7 +94,7 @@ def test_cleanup(self): self.assertIn(self.network_name, net_names) # Fourth round - dry run with no filters, ensure everything identified - self.conn.project_cleanup( + self.user_cloud_alt.project_cleanup( dry_run=True, wait_timeout=120, status_queue=status_queue ) @@ -108,11 +106,11 @@ def test_cleanup(self): self.assertIn(self.network_name, net_names) # Ensure network still exists - net = self.conn.network.get_network(self.net.id) + net = self.user_cloud_alt.network.get_network(self.net.id) self.assertEqual(net.name, self.net.name) # Last round - do a real cleanup - self.conn.project_cleanup( + self.user_cloud_alt.project_cleanup( dry_run=False, wait_timeout=600, status_queue=status_queue ) @@ -120,7 +118,7 @@ def test_cleanup(self): while not status_queue.empty(): objects.append(status_queue.get()) - nets = self.conn.network.networks() + nets = self.user_cloud_alt.network.networks() net_names = list(obj.name for obj in nets) # Since we might not have enough privs to drop all nets - ensure # we do not have our known one @@ -132,23 +130,27 @@ def test_block_storage_cleanup(self): status_queue: queue.Queue[resource.Resource] = queue.Queue() - vol = self.conn.block_storage.create_volume(name='vol1', size='1') - self.conn.block_storage.wait_for_status(vol) - s1 = self.conn.block_storage.create_snapshot(volume_id=vol.id) - self.conn.block_storage.wait_for_status(s1) - b1 = self.conn.block_storage.create_backup(volume_id=vol.id) - self.conn.block_storage.wait_for_status(b1) - b2 = self.conn.block_storage.create_backup( + vol = self.user_cloud_alt.block_storage.create_volume( + name='vol1', size='1' + ) + self.user_cloud_alt.block_storage.wait_for_status(vol) + s1 = self.user_cloud_alt.block_storage.create_snapshot( + volume_id=vol.id + ) + self.user_cloud_alt.block_storage.wait_for_status(s1) + b1 = self.user_cloud_alt.block_storage.create_backup(volume_id=vol.id) + self.user_cloud_alt.block_storage.wait_for_status(b1) + b2 = self.user_cloud_alt.block_storage.create_backup( volume_id=vol.id, is_incremental=True, snapshot_id=s1.id ) - self.conn.block_storage.wait_for_status(b2) - b3 = self.conn.block_storage.create_backup( + self.user_cloud_alt.block_storage.wait_for_status(b2) + b3 = self.user_cloud_alt.block_storage.create_backup( volume_id=vol.id, is_incremental=True, snapshot_id=s1.id ) - self.conn.block_storage.wait_for_status(b3) + self.user_cloud_alt.block_storage.wait_for_status(b3) # First round - check no resources are old enough - self.conn.project_cleanup( + self.user_cloud_alt.project_cleanup( dry_run=True, wait_timeout=120, status_queue=status_queue, @@ -159,7 +161,7 @@ def test_block_storage_cleanup(self): # Second round - resource evaluation function return false, ensure # nothing identified - self.conn.project_cleanup( + self.user_cloud_alt.project_cleanup( dry_run=True, wait_timeout=120, status_queue=status_queue, @@ -170,7 +172,7 @@ def test_block_storage_cleanup(self): self.assertTrue(status_queue.empty()) # Third round - filters set too low - self.conn.project_cleanup( + self.user_cloud_alt.project_cleanup( dry_run=True, wait_timeout=120, status_queue=status_queue, @@ -186,7 +188,7 @@ def test_block_storage_cleanup(self): self.assertIn(vol.id, volumes) # Fourth round - dry run with no filters, ensure everything identified - self.conn.project_cleanup( + self.user_cloud_alt.project_cleanup( dry_run=True, wait_timeout=120, status_queue=status_queue ) @@ -198,17 +200,21 @@ def test_block_storage_cleanup(self): self.assertIn(vol.id, vol_ids) # Ensure volume still exists - vol_check = self.conn.block_storage.get_volume(vol.id) + vol_check = self.user_cloud_alt.block_storage.get_volume(vol.id) self.assertEqual(vol.name, vol_check.name) # Last round - do a real cleanup - self.conn.project_cleanup( + self.user_cloud_alt.project_cleanup( dry_run=False, wait_timeout=600, status_queue=status_queue ) # Ensure no backups remain - self.assertEqual(0, len(list(self.conn.block_storage.backups()))) + self.assertEqual( + 0, len(list(self.user_cloud_alt.block_storage.backups())) + ) # Ensure no snapshots remain - self.assertEqual(0, len(list(self.conn.block_storage.snapshots()))) + self.assertEqual( + 0, len(list(self.user_cloud_alt.block_storage.snapshots())) + ) def test_cleanup_swift(self): if not self.user_cloud.has_service('object-store'): @@ -216,14 +222,14 @@ def test_cleanup_swift(self): status_queue: queue.Queue[resource.Resource] = queue.Queue() - self.conn.object_store.create_container('test_cleanup') + self.user_cloud_alt.object_store.create_container('test_cleanup') for i in range(1, 10): - self.conn.object_store.create_object( + self.user_cloud_alt.object_store.create_object( "test_cleanup", f"test{i}", data="test{i}" ) # First round - check no resources are old enough - self.conn.project_cleanup( + self.user_cloud_alt.project_cleanup( dry_run=True, wait_timeout=120, status_queue=status_queue, @@ -233,7 +239,7 @@ def test_cleanup_swift(self): self.assertTrue(status_queue.empty()) # Second round - filters set too low - self.conn.project_cleanup( + self.user_cloud_alt.project_cleanup( dry_run=True, wait_timeout=120, status_queue=status_queue, @@ -248,27 +254,31 @@ def test_cleanup_swift(self): self.assertIn('test1', obj_names) # Ensure object still exists - obj = self.conn.object_store.get_object("test1", "test_cleanup") + obj = self.user_cloud_alt.object_store.get_object( + "test1", "test_cleanup" + ) self.assertIsNotNone(obj) # Last round - do a real cleanup - self.conn.project_cleanup( + self.user_cloud_alt.project_cleanup( dry_run=False, wait_timeout=600, status_queue=status_queue ) objects.clear() while not status_queue.empty(): objects.append(status_queue.get()) - self.assertIsNone(self.conn.get_container('test_container')) + self.assertIsNone(self.user_cloud_alt.get_container('test_container')) def test_cleanup_vpnaas(self): - if not list(self.conn.network.service_providers(service_type="VPN")): + if not list( + self.user_cloud_alt.network.service_providers(service_type="VPN") + ): self.skipTest("VPNaaS plugin is requred, but not available") status_queue: queue.Queue[resource.Resource] = queue.Queue() # Find available external networks and use one - for network in self.conn.network.networks(): + for network in self.user_cloud_alt.network.networks(): if network.is_router_external: external_network: _network.Network = network break @@ -276,55 +286,65 @@ def test_cleanup_vpnaas(self): self.skipTest("External network is required, but not available") # Create left network resources - network_left = self.conn.network.create_network(name="network_left") - subnet_left = self.conn.network.create_subnet( + network_left = self.user_cloud_alt.network.create_network( + name="network_left" + ) + subnet_left = self.user_cloud_alt.network.create_subnet( name="subnet_left", network_id=network_left.id, cidr="192.168.1.0/24", ip_version=4, ) - router_left = self.conn.network.create_router(name="router_left") - self.conn.network.add_interface_to_router( + router_left = self.user_cloud_alt.network.create_router( + name="router_left" + ) + self.user_cloud_alt.network.add_interface_to_router( router=router_left.id, subnet_id=subnet_left.id ) - router_left = self.conn.network.update_router( + router_left = self.user_cloud_alt.network.update_router( router_left, external_gateway_info={"network_id": external_network.id}, ) # Create right network resources - network_right = self.conn.network.create_network(name="network_right") - subnet_right = self.conn.network.create_subnet( + network_right = self.user_cloud_alt.network.create_network( + name="network_right" + ) + subnet_right = self.user_cloud_alt.network.create_subnet( name="subnet_right", network_id=network_right.id, cidr="192.168.2.0/24", ip_version=4, ) - router_right = self.conn.network.create_router(name="router_right") - self.conn.network.add_interface_to_router( + router_right = self.user_cloud_alt.network.create_router( + name="router_right" + ) + self.user_cloud_alt.network.add_interface_to_router( router=router_right.id, subnet_id=subnet_right.id ) - router_right = self.conn.network.update_router( + router_right = self.user_cloud_alt.network.update_router( router_right, external_gateway_info={"network_id": external_network.id}, ) # Create VPNaaS resources - ike_policy = self.conn.network.create_vpn_ike_policy(name="ike_policy") - ipsec_policy = self.conn.network.create_vpn_ipsec_policy( + ike_policy = self.user_cloud_alt.network.create_vpn_ike_policy( + name="ike_policy" + ) + ipsec_policy = self.user_cloud_alt.network.create_vpn_ipsec_policy( name="ipsec_policy" ) - vpn_service = self.conn.network.create_vpn_service( + vpn_service = self.user_cloud_alt.network.create_vpn_service( name="vpn_service", router_id=router_left.id ) - ep_group_local = self.conn.network.create_vpn_endpoint_group( + ep_group_local = self.user_cloud_alt.network.create_vpn_endpoint_group( name="endpoint_group_local", type="subnet", endpoints=[subnet_left.id], ) - ep_group_peer = self.conn.network.create_vpn_endpoint_group( + ep_group_peer = self.user_cloud_alt.network.create_vpn_endpoint_group( name="endpoint_group_peer", type="cidr", endpoints=[subnet_right.cidr], @@ -333,20 +353,22 @@ def test_cleanup_vpnaas(self): router_right_ip = router_right.external_gateway_info[ 'external_fixed_ips' ][0]['ip_address'] - ipsec_site_conn = self.conn.network.create_vpn_ipsec_site_connection( - name="ipsec_site_connection", - vpnservice_id=vpn_service.id, - ikepolicy_id=ike_policy.id, - ipsecpolicy_id=ipsec_policy.id, - local_ep_group_id=ep_group_local.id, - peer_ep_group_id=ep_group_peer.id, - psk="test", - peer_address=router_right_ip, - peer_id=router_right_ip, + ipsec_site_conn = ( + self.user_cloud_alt.network.create_vpn_ipsec_site_connection( + name="ipsec_site_connection", + vpnservice_id=vpn_service.id, + ikepolicy_id=ike_policy.id, + ipsecpolicy_id=ipsec_policy.id, + local_ep_group_id=ep_group_local.id, + peer_ep_group_id=ep_group_peer.id, + psk="test", + peer_address=router_right_ip, + peer_id=router_right_ip, + ) ) # First round - check no resources are old enough - self.conn.project_cleanup( + self.user_cloud_alt.project_cleanup( dry_run=True, wait_timeout=120, status_queue=status_queue, @@ -356,7 +378,7 @@ def test_cleanup_vpnaas(self): # Second round - resource evaluation function return false, ensure # nothing identified - self.conn.project_cleanup( + self.user_cloud_alt.project_cleanup( dry_run=True, wait_timeout=120, status_queue=status_queue, @@ -366,7 +388,7 @@ def test_cleanup_vpnaas(self): self.assertTrue(status_queue.empty()) # Third round - filters set too low - self.conn.project_cleanup( + self.user_cloud_alt.project_cleanup( dry_run=True, wait_timeout=120, status_queue=status_queue, @@ -382,7 +404,7 @@ def test_cleanup_vpnaas(self): self.assertIn(network_left.id, resource_ids) # Fourth round - dry run with no filters, ensure everything identified - self.conn.project_cleanup( + self.user_cloud_alt.project_cleanup( dry_run=True, wait_timeout=120, status_queue=status_queue ) objects = [] @@ -393,20 +415,33 @@ def test_cleanup_vpnaas(self): self.assertIn(ipsec_site_conn.id, resource_ids) # Ensure vpn resources still exist - site_conn_check = self.conn.network.get_vpn_ipsec_site_connection( - ipsec_site_conn.id + site_conn_check = ( + self.user_cloud_alt.network.get_vpn_ipsec_site_connection( + ipsec_site_conn.id + ) ) self.assertEqual(site_conn_check.name, ipsec_site_conn.name) # Last round - do a real cleanup - self.conn.project_cleanup( + self.user_cloud_alt.project_cleanup( dry_run=False, wait_timeout=600, status_queue=status_queue ) # Ensure no VPN resources remain - self.assertEqual(0, len(list(self.conn.network.vpn_ike_policies()))) - self.assertEqual(0, len(list(self.conn.network.vpn_ipsec_policies()))) - self.assertEqual(0, len(list(self.conn.network.vpn_services()))) - self.assertEqual(0, len(list(self.conn.network.vpn_endpoint_groups()))) self.assertEqual( - 0, len(list(self.conn.network.vpn_ipsec_site_connections())) + 0, len(list(self.user_cloud_alt.network.vpn_ike_policies())) + ) + self.assertEqual( + 0, len(list(self.user_cloud_alt.network.vpn_ipsec_policies())) + ) + self.assertEqual( + 0, len(list(self.user_cloud_alt.network.vpn_services())) + ) + self.assertEqual( + 0, len(list(self.user_cloud_alt.network.vpn_endpoint_groups())) + ) + self.assertEqual( + 0, + len( + list(self.user_cloud_alt.network.vpn_ipsec_site_connections()) + ), ) diff --git a/openstack/tests/functional/clustering/test_cluster.py b/openstack/tests/functional/clustering/test_cluster.py index 47739c6cd..63bae64c8 100644 --- a/openstack/tests/functional/clustering/test_cluster.py +++ b/openstack/tests/functional/clustering/test_cluster.py @@ -27,7 +27,7 @@ def setUp(self): self.cidr = '10.99.99.0/16' self.network, self.subnet = test_network.create_network( - self.conn, self.getUniqueString(), self.cidr + self.operator_cloud, self.getUniqueString(), self.cidr ) self.assertIsNotNone(self.network) @@ -45,7 +45,9 @@ def setUp(self): }, } - self.profile = self.conn.clustering.create_profile(**profile_attrs) + self.profile = self.operator_cloud.clustering.create_profile( + **profile_attrs + ) self.assertIsNotNone(self.profile) self.cluster_name = self.getUniqueString() @@ -57,57 +59,63 @@ def setUp(self): "desired_capacity": 0, } - self.cluster = self.conn.clustering.create_cluster(**cluster_spec) - self.conn.clustering.wait_for_status( + self.cluster = self.operator_cloud.clustering.create_cluster( + **cluster_spec + ) + self.operator_cloud.clustering.wait_for_status( self.cluster, 'ACTIVE', wait=self._wait_for_timeout ) assert isinstance(self.cluster, cluster.Cluster) def tearDown(self): if self.cluster: - self.conn.clustering.delete_cluster(self.cluster.id) - self.conn.clustering.wait_for_delete( + self.operator_cloud.clustering.delete_cluster(self.cluster.id) + self.operator_cloud.clustering.wait_for_delete( self.cluster, wait=self._wait_for_timeout ) - test_network.delete_network(self.conn, self.network, self.subnet) + test_network.delete_network( + self.operator_cloud, self.network, self.subnet + ) - self.conn.clustering.delete_profile(self.profile) + self.operator_cloud.clustering.delete_profile(self.profile) super().tearDown() def test_find(self): - sot = self.conn.clustering.find_cluster(self.cluster.id) + sot = self.operator_cloud.clustering.find_cluster(self.cluster.id) self.assertEqual(self.cluster.id, sot.id) def test_get(self): - sot = self.conn.clustering.get_cluster(self.cluster) + sot = self.operator_cloud.clustering.get_cluster(self.cluster) self.assertEqual(self.cluster.id, sot.id) def test_list(self): - names = [o.name for o in self.conn.clustering.clusters()] + names = [o.name for o in self.operator_cloud.clustering.clusters()] self.assertIn(self.cluster_name, names) def test_update(self): new_cluster_name = self.getUniqueString() - sot = self.conn.clustering.update_cluster( + sot = self.operator_cloud.clustering.update_cluster( self.cluster, name=new_cluster_name, profile_only=False ) time.sleep(2) - sot = self.conn.clustering.get_cluster(self.cluster) + sot = self.operator_cloud.clustering.get_cluster(self.cluster) self.assertEqual(new_cluster_name, sot.name) def test_delete(self): - cluster_delete_action = self.conn.clustering.delete_cluster( + cluster_delete_action = self.operator_cloud.clustering.delete_cluster( self.cluster.id ) - self.conn.clustering.wait_for_delete( + self.operator_cloud.clustering.wait_for_delete( self.cluster, wait=self._wait_for_timeout ) - action = self.conn.clustering.get_action(cluster_delete_action.id) + action = self.operator_cloud.clustering.get_action( + cluster_delete_action.id + ) self.assertEqual(action.target_id, self.cluster.id) self.assertEqual(action.action, 'CLUSTER_DELETE') self.assertEqual(action.status, 'SUCCEEDED') @@ -115,15 +123,17 @@ def test_delete(self): self.cluster = None def test_force_delete(self): - cluster_delete_action = self.conn.clustering.delete_cluster( + cluster_delete_action = self.operator_cloud.clustering.delete_cluster( self.cluster.id, False, True ) - self.conn.clustering.wait_for_delete( + self.operator_cloud.clustering.wait_for_delete( self.cluster, wait=self._wait_for_timeout ) - action = self.conn.clustering.get_action(cluster_delete_action.id) + action = self.operator_cloud.clustering.get_action( + cluster_delete_action.id + ) self.assertEqual(action.target_id, self.cluster.id) self.assertEqual(action.action, 'CLUSTER_DELETE') self.assertEqual(action.status, 'SUCCEEDED') diff --git a/openstack/tests/functional/compute/v2/test_extension.py b/openstack/tests/functional/compute/v2/test_extension.py index 8bb0e83ce..92674ca98 100644 --- a/openstack/tests/functional/compute/v2/test_extension.py +++ b/openstack/tests/functional/compute/v2/test_extension.py @@ -16,7 +16,7 @@ class TestExtension(base.BaseFunctionalTest): def test_list(self): - extensions = list(self.conn.compute.extensions()) + extensions = list(self.operator_cloud.compute.extensions()) self.assertGreater(len(extensions), 0) for ext in extensions: diff --git a/openstack/tests/functional/compute/v2/test_flavor.py b/openstack/tests/functional/compute/v2/test_flavor.py index 7fc5f861a..15f2e0a51 100644 --- a/openstack/tests/functional/compute/v2/test_flavor.py +++ b/openstack/tests/functional/compute/v2/test_flavor.py @@ -19,10 +19,10 @@ class TestFlavor(base.BaseFunctionalTest): def setUp(self): super().setUp() self.new_item_name = self.getUniqueString('flavor') - self.one_flavor = list(self.conn.compute.flavors())[0] + self.one_flavor = list(self.operator_cloud.compute.flavors())[0] def test_flavors(self): - flavors = list(self.conn.compute.flavors()) + flavors = list(self.operator_cloud.compute.flavors()) self.assertGreater(len(flavors), 0) for flavor in flavors: @@ -33,15 +33,15 @@ def test_flavors(self): self.assertIsInstance(flavor.vcpus, int) def test_find_flavors_by_id(self): - rslt = self.conn.compute.find_flavor(self.one_flavor.id) + rslt = self.operator_cloud.compute.find_flavor(self.one_flavor.id) self.assertEqual(rslt.id, self.one_flavor.id) def test_find_flavors_by_name(self): - rslt = self.conn.compute.find_flavor(self.one_flavor.name) + rslt = self.operator_cloud.compute.find_flavor(self.one_flavor.name) self.assertEqual(rslt.name, self.one_flavor.name) def test_find_flavors_no_match_ignore_true(self): - rslt = self.conn.compute.find_flavor( + rslt = self.operator_cloud.compute.find_flavor( "not a flavor", ignore_missing=True ) self.assertIsNone(rslt) @@ -49,7 +49,7 @@ def test_find_flavors_no_match_ignore_true(self): def test_find_flavors_no_match_ignore_false(self): self.assertRaises( exceptions.NotFoundException, - self.conn.compute.find_flavor, + self.operator_cloud.compute.find_flavor, "not a flavor", ignore_missing=False, ) @@ -86,7 +86,7 @@ def test_flavor_access(self): flv = self.operator_cloud.compute.create_flavor( is_public=False, name=flavor_name, ram=128, vcpus=1, disk=0 ) - self.addCleanup(self.conn.compute.delete_flavor, flv.id) + self.addCleanup(self.operator_cloud.compute.delete_flavor, flv.id) # Validate the 'demo' user cannot see the new flavor flv_cmp = self.user_cloud.compute.find_flavor(flavor_name) self.assertIsNone(flv_cmp) @@ -118,23 +118,31 @@ def test_flavor_access(self): def test_extra_props_calls(self): flavor_name = uuid.uuid4().hex - flv = self.conn.compute.create_flavor( + flv = self.operator_cloud.compute.create_flavor( is_public=False, name=flavor_name, ram=128, vcpus=1, disk=0 ) - self.addCleanup(self.conn.compute.delete_flavor, flv.id) + self.addCleanup(self.operator_cloud.compute.delete_flavor, flv.id) # Create extra_specs specs = {'a': 'b'} - self.conn.compute.create_flavor_extra_specs(flv, extra_specs=specs) + self.operator_cloud.compute.create_flavor_extra_specs( + flv, extra_specs=specs + ) # verify specs - flv_cmp = self.conn.compute.fetch_flavor_extra_specs(flv) + flv_cmp = self.operator_cloud.compute.fetch_flavor_extra_specs(flv) self.assertDictEqual(specs, flv_cmp.extra_specs) # update - self.conn.compute.update_flavor_extra_specs_property(flv, 'c', 'd') - val_cmp = self.conn.compute.get_flavor_extra_specs_property(flv, 'c') + self.operator_cloud.compute.update_flavor_extra_specs_property( + flv, 'c', 'd' + ) + val_cmp = self.operator_cloud.compute.get_flavor_extra_specs_property( + flv, 'c' + ) # fetch single prop self.assertEqual('d', val_cmp) # drop new prop - self.conn.compute.delete_flavor_extra_specs_property(flv, 'c') + self.operator_cloud.compute.delete_flavor_extra_specs_property( + flv, 'c' + ) # re-fetch and ensure prev state - flv_cmp = self.conn.compute.fetch_flavor_extra_specs(flv) + flv_cmp = self.operator_cloud.compute.fetch_flavor_extra_specs(flv) self.assertDictEqual(specs, flv_cmp.extra_specs) diff --git a/openstack/tests/functional/compute/v2/test_hypervisor.py b/openstack/tests/functional/compute/v2/test_hypervisor.py index 150ee83a5..1c5f5cccc 100644 --- a/openstack/tests/functional/compute/v2/test_hypervisor.py +++ b/openstack/tests/functional/compute/v2/test_hypervisor.py @@ -18,13 +18,13 @@ def setUp(self): super().setUp() def test_list_hypervisors(self): - rslt = list(self.conn.compute.hypervisors()) + rslt = list(self.operator_cloud.compute.hypervisors()) self.assertIsNotNone(rslt) - rslt = list(self.conn.compute.hypervisors(details=True)) + rslt = list(self.operator_cloud.compute.hypervisors(details=True)) self.assertIsNotNone(rslt) def test_get_find_hypervisors(self): - for hypervisor in self.conn.compute.hypervisors(): - self.conn.compute.get_hypervisor(hypervisor.id) - self.conn.compute.find_hypervisor(hypervisor.id) + for hypervisor in self.operator_cloud.compute.hypervisors(): + self.operator_cloud.compute.get_hypervisor(hypervisor.id) + self.operator_cloud.compute.find_hypervisor(hypervisor.id) diff --git a/openstack/tests/functional/compute/v2/test_image.py b/openstack/tests/functional/compute/v2/test_image.py index 5b6e131f2..864442bb0 100644 --- a/openstack/tests/functional/compute/v2/test_image.py +++ b/openstack/tests/functional/compute/v2/test_image.py @@ -17,13 +17,13 @@ class TestImage(base.BaseFunctionalTest): def test_images(self): - images = list(self.conn.compute.images()) + images = list(self.operator_cloud.compute.images()) self.assertGreater(len(images), 0) for image in images: self.assertIsInstance(image.id, str) def _get_non_test_image(self): - images = self.conn.compute.images() + images = self.operator_cloud.compute.images() image = next(images) if image.name == TEST_IMAGE_NAME: @@ -34,14 +34,14 @@ def _get_non_test_image(self): def test_find_image(self): image = self._get_non_test_image() self.assertIsNotNone(image) - sot = self.conn.compute.find_image(image.id) + sot = self.operator_cloud.compute.find_image(image.id) self.assertEqual(image.id, sot.id) self.assertEqual(image.name, sot.name) def test_get_image(self): image = self._get_non_test_image() self.assertIsNotNone(image) - sot = self.conn.compute.get_image(image.id) + sot = self.operator_cloud.compute.get_image(image.id) self.assertEqual(image.id, sot.id) self.assertEqual(image.name, sot.name) self.assertIsNotNone(image.links) @@ -55,36 +55,38 @@ def test_image_metadata(self): image = self._get_non_test_image() # delete pre-existing metadata - self.conn.compute.delete_image_metadata(image, image.metadata.keys()) - image = self.conn.compute.get_image_metadata(image) + self.operator_cloud.compute.delete_image_metadata( + image, image.metadata.keys() + ) + image = self.operator_cloud.compute.get_image_metadata(image) self.assertFalse(image.metadata) # get metadata - image = self.conn.compute.get_image_metadata(image) + image = self.operator_cloud.compute.get_image_metadata(image) self.assertFalse(image.metadata) # set no metadata - self.conn.compute.set_image_metadata(image) - image = self.conn.compute.get_image_metadata(image) + self.operator_cloud.compute.set_image_metadata(image) + image = self.operator_cloud.compute.get_image_metadata(image) self.assertFalse(image.metadata) # set empty metadata - self.conn.compute.set_image_metadata(image, k0='') - image = self.conn.compute.get_image_metadata(image) + self.operator_cloud.compute.set_image_metadata(image, k0='') + image = self.operator_cloud.compute.get_image_metadata(image) self.assertIn('k0', image.metadata) self.assertEqual('', image.metadata['k0']) # set metadata - self.conn.compute.set_image_metadata(image, k1='v1') - image = self.conn.compute.get_image_metadata(image) + self.operator_cloud.compute.set_image_metadata(image, k1='v1') + image = self.operator_cloud.compute.get_image_metadata(image) self.assertTrue(image.metadata) self.assertEqual(2, len(image.metadata)) self.assertIn('k1', image.metadata) self.assertEqual('v1', image.metadata['k1']) # set more metadata - self.conn.compute.set_image_metadata(image, k2='v2') - image = self.conn.compute.get_image_metadata(image) + self.operator_cloud.compute.set_image_metadata(image, k2='v2') + image = self.operator_cloud.compute.get_image_metadata(image) self.assertTrue(image.metadata) self.assertEqual(3, len(image.metadata)) self.assertIn('k1', image.metadata) @@ -93,8 +95,8 @@ def test_image_metadata(self): self.assertEqual('v2', image.metadata['k2']) # update metadata - self.conn.compute.set_image_metadata(image, k1='v1.1') - image = self.conn.compute.get_image_metadata(image) + self.operator_cloud.compute.set_image_metadata(image, k1='v1.1') + image = self.operator_cloud.compute.get_image_metadata(image) self.assertTrue(image.metadata) self.assertEqual(3, len(image.metadata)) self.assertIn('k1', image.metadata) @@ -103,6 +105,8 @@ def test_image_metadata(self): self.assertEqual('v2', image.metadata['k2']) # delete metadata - self.conn.compute.delete_image_metadata(image, image.metadata.keys()) - image = self.conn.compute.get_image_metadata(image) + self.operator_cloud.compute.delete_image_metadata( + image, image.metadata.keys() + ) + image = self.operator_cloud.compute.get_image_metadata(image) self.assertFalse(image.metadata) diff --git a/openstack/tests/functional/compute/v2/test_keypair.py b/openstack/tests/functional/compute/v2/test_keypair.py index fbc80ec73..bdb990f5a 100644 --- a/openstack/tests/functional/compute/v2/test_keypair.py +++ b/openstack/tests/functional/compute/v2/test_keypair.py @@ -22,29 +22,31 @@ def setUp(self): # Keypairs can't have .'s in the name. Because why? self.NAME = self.getUniqueString().split('.')[-1] - sot = self.conn.compute.create_keypair(name=self.NAME, type='ssh') + sot = self.operator_cloud.compute.create_keypair( + name=self.NAME, type='ssh' + ) assert isinstance(sot, keypair.Keypair) self.assertEqual(self.NAME, sot.name) self._keypair = sot def tearDown(self): - sot = self.conn.compute.delete_keypair(self._keypair) + sot = self.operator_cloud.compute.delete_keypair(self._keypair) self.assertIsNone(sot) super().tearDown() def test_find(self): - sot = self.conn.compute.find_keypair(self.NAME) + sot = self.operator_cloud.compute.find_keypair(self.NAME) self.assertEqual(self.NAME, sot.name) self.assertEqual(self.NAME, sot.id) def test_get(self): - sot = self.conn.compute.get_keypair(self.NAME) + sot = self.operator_cloud.compute.get_keypair(self.NAME) self.assertEqual(self.NAME, sot.name) self.assertEqual(self.NAME, sot.id) self.assertEqual('ssh', sot.type) def test_list(self): - names = [o.name for o in self.conn.compute.keypairs()] + names = [o.name for o in self.operator_cloud.compute.keypairs()] self.assertIn(self.NAME, names) @@ -53,9 +55,9 @@ def setUp(self): super().setUp() self.NAME = self.getUniqueString().split('.')[-1] - self.USER = self.conn.list_users()[0] + self.USER = self.operator_cloud.list_users()[0] - sot = self.conn.compute.create_keypair( + sot = self.operator_cloud.compute.create_keypair( name=self.NAME, user_id=self.USER.id ) assert isinstance(sot, keypair.Keypair) @@ -64,12 +66,16 @@ def setUp(self): self._keypair = sot def tearDown(self): - sot = self.conn.compute.delete_keypair(self.NAME, user_id=self.USER.id) + sot = self.operator_cloud.compute.delete_keypair( + self.NAME, user_id=self.USER.id + ) self.assertIsNone(sot) super().tearDown() def test_get(self): - sot = self.conn.compute.get_keypair(self.NAME, user_id=self.USER.id) + sot = self.operator_cloud.compute.get_keypair( + self.NAME, user_id=self.USER.id + ) self.assertEqual(self.NAME, sot.name) self.assertEqual(self.NAME, sot.id) self.assertEqual(self.USER.id, sot.user_id) diff --git a/openstack/tests/functional/compute/v2/test_limits.py b/openstack/tests/functional/compute/v2/test_limits.py index 382fcd20b..1b99f1e3c 100644 --- a/openstack/tests/functional/compute/v2/test_limits.py +++ b/openstack/tests/functional/compute/v2/test_limits.py @@ -15,7 +15,7 @@ class TestLimits(base.BaseFunctionalTest): def test_limits(self): - sot = self.conn.compute.get_limits() + sot = self.operator_cloud.compute.get_limits() self.assertIsNotNone(sot.absolute['instances']) self.assertIsNotNone(sot.absolute['total_ram']) self.assertIsNotNone(sot.absolute['keypairs']) diff --git a/openstack/tests/functional/compute/v2/test_server.py b/openstack/tests/functional/compute/v2/test_server.py index 0c679b351..b8dd6105f 100644 --- a/openstack/tests/functional/compute/v2/test_server.py +++ b/openstack/tests/functional/compute/v2/test_server.py @@ -20,8 +20,8 @@ def setUp(self): super().setUp() self.NAME = 'needstobeshortandlowercase' self.USERDATA = 'SSdtIGFjdHVhbGx5IGEgZ29hdC4=' - volume = self.conn.create_volume(1) - sot = self.conn.compute.create_server( + volume = self.operator_cloud.create_volume(1) + sot = self.operator_cloud.compute.create_server( name=self.NAME, flavor_id=self.flavor.id, image_id=self.image.id, @@ -38,21 +38,23 @@ def setUp(self): }, ], ) - self.conn.compute.wait_for_server(sot, wait=self._wait_for_timeout) + self.operator_cloud.compute.wait_for_server( + sot, wait=self._wait_for_timeout + ) assert isinstance(sot, server.Server) self.assertEqual(self.NAME, sot.name) self.server = sot def tearDown(self): - sot = self.conn.compute.delete_server(self.server.id) - self.conn.compute.wait_for_delete( + sot = self.operator_cloud.compute.delete_server(self.server.id) + self.operator_cloud.compute.wait_for_delete( self.server, wait=self._wait_for_timeout ) self.assertIsNone(sot) super().tearDown() def test_get(self): - sot = self.conn.compute.get_server(self.server.id) + sot = self.operator_cloud.compute.get_server(self.server.id) self.assertIsNotNone(sot.reservation_id) self.assertIsNotNone(sot.launch_index) self.assertIsNotNone(sot.ramdisk_id) @@ -72,64 +74,74 @@ def setUp(self): self.cidr = '10.99.99.0/16' self.network, self.subnet = test_network.create_network( - self.conn, self.NAME, self.cidr + self.operator_cloud, self.NAME, self.cidr ) self.assertIsNotNone(self.network) - sot = self.conn.compute.create_server( + sot = self.operator_cloud.compute.create_server( name=self.NAME, flavor_id=self.flavor.id, image_id=self.image.id, networks=[{"uuid": self.network.id}], ) - self.conn.compute.wait_for_server(sot, wait=self._wait_for_timeout) + self.operator_cloud.compute.wait_for_server( + sot, wait=self._wait_for_timeout + ) assert isinstance(sot, server.Server) self.assertEqual(self.NAME, sot.name) self.server = sot def tearDown(self): - sot = self.conn.compute.delete_server(self.server.id) + sot = self.operator_cloud.compute.delete_server(self.server.id) self.assertIsNone(sot) # Need to wait for the stack to go away before network delete - self.conn.compute.wait_for_delete( + self.operator_cloud.compute.wait_for_delete( self.server, wait=self._wait_for_timeout ) - test_network.delete_network(self.conn, self.network, self.subnet) + test_network.delete_network( + self.operator_cloud, self.network, self.subnet + ) super().tearDown() def test_find(self): - sot = self.conn.compute.find_server(self.NAME) + sot = self.operator_cloud.compute.find_server(self.NAME) self.assertEqual(self.server.id, sot.id) def test_get(self): - sot = self.conn.compute.get_server(self.server.id) + sot = self.operator_cloud.compute.get_server(self.server.id) self.assertEqual(self.NAME, sot.name) self.assertEqual(self.server.id, sot.id) def test_list(self): - names = [o.name for o in self.conn.compute.servers()] + names = [o.name for o in self.operator_cloud.compute.servers()] self.assertIn(self.NAME, names) def test_server_metadata(self): - test_server = self.conn.compute.get_server(self.server.id) + test_server = self.operator_cloud.compute.get_server(self.server.id) # get metadata - test_server = self.conn.compute.get_server_metadata(test_server) + test_server = self.operator_cloud.compute.get_server_metadata( + test_server + ) self.assertFalse(test_server.metadata) # set no metadata - self.conn.compute.set_server_metadata(test_server) - test_server = self.conn.compute.get_server_metadata(test_server) + self.operator_cloud.compute.set_server_metadata(test_server) + test_server = self.operator_cloud.compute.get_server_metadata( + test_server + ) self.assertFalse(test_server.metadata) # set empty metadata - self.conn.compute.set_server_metadata(test_server, k0='') - server = self.conn.compute.get_server_metadata(test_server) + self.operator_cloud.compute.set_server_metadata(test_server, k0='') + server = self.operator_cloud.compute.get_server_metadata(test_server) self.assertTrue(server.metadata) # set metadata - self.conn.compute.set_server_metadata(test_server, k1='v1') - test_server = self.conn.compute.get_server_metadata(test_server) + self.operator_cloud.compute.set_server_metadata(test_server, k1='v1') + test_server = self.operator_cloud.compute.get_server_metadata( + test_server + ) self.assertTrue(test_server.metadata) self.assertEqual(2, len(test_server.metadata)) self.assertIn('k0', test_server.metadata) @@ -138,8 +150,10 @@ def test_server_metadata(self): self.assertEqual('v1', test_server.metadata['k1']) # set more metadata - self.conn.compute.set_server_metadata(test_server, k2='v2') - test_server = self.conn.compute.get_server_metadata(test_server) + self.operator_cloud.compute.set_server_metadata(test_server, k2='v2') + test_server = self.operator_cloud.compute.get_server_metadata( + test_server + ) self.assertTrue(test_server.metadata) self.assertEqual(3, len(test_server.metadata)) self.assertIn('k0', test_server.metadata) @@ -150,8 +164,10 @@ def test_server_metadata(self): self.assertEqual('v2', test_server.metadata['k2']) # update metadata - self.conn.compute.set_server_metadata(test_server, k1='v1.1') - test_server = self.conn.compute.get_server_metadata(test_server) + self.operator_cloud.compute.set_server_metadata(test_server, k1='v1.1') + test_server = self.operator_cloud.compute.get_server_metadata( + test_server + ) self.assertTrue(test_server.metadata) self.assertEqual(3, len(test_server.metadata)) self.assertIn('k0', test_server.metadata) @@ -162,14 +178,16 @@ def test_server_metadata(self): self.assertEqual('v2', test_server.metadata['k2']) # delete metadata - self.conn.compute.delete_server_metadata( + self.operator_cloud.compute.delete_server_metadata( test_server, test_server.metadata.keys() ) - test_server = self.conn.compute.get_server_metadata(test_server) + test_server = self.operator_cloud.compute.get_server_metadata( + test_server + ) self.assertFalse(test_server.metadata) def test_server_remote_console(self): - console = self.conn.compute.create_server_remote_console( + console = self.operator_cloud.compute.create_server_remote_console( self.server, protocol='vnc', type='novnc' ) self.assertEqual('vnc', console.protocol) diff --git a/openstack/tests/functional/compute/v2/test_service.py b/openstack/tests/functional/compute/v2/test_service.py index c29da9fed..8c7787f7f 100644 --- a/openstack/tests/functional/compute/v2/test_service.py +++ b/openstack/tests/functional/compute/v2/test_service.py @@ -15,33 +15,35 @@ class TestService(base.BaseFunctionalTest): def test_list(self): - sot = list(self.conn.compute.services()) + sot = list(self.operator_cloud.compute.services()) self.assertIsNotNone(sot) def test_disable_enable(self): self.skipTest("Test is breaking tests that follow") - for srv in self.conn.compute.services(): + for srv in self.operator_cloud.compute.services(): # only nova-compute can be updated if srv.name == 'nova-compute': - self.conn.compute.disable_service(srv) - self.conn.compute.enable_service(srv) + self.operator_cloud.compute.disable_service(srv) + self.operator_cloud.compute.enable_service(srv) def test_update(self): self.skipTest("Test is breaking tests that follow") - for srv in self.conn.compute.services(): + for srv in self.operator_cloud.compute.services(): if srv.name == 'nova-compute': - self.conn.compute.update_service_forced_down( + self.operator_cloud.compute.update_service_forced_down( srv, None, None, True ) - self.conn.compute.update_service_forced_down( + self.operator_cloud.compute.update_service_forced_down( srv, srv.host, srv.binary, False ) - self.conn.compute.update_service(srv, status='enabled') + self.operator_cloud.compute.update_service( + srv, status='enabled' + ) def test_find(self): - for srv in self.conn.compute.services(): + for srv in self.operator_cloud.compute.services(): if srv.name != 'nova-conductor': # In devstack there are 2 nova-conductor instances on same host - self.conn.compute.find_service( + self.operator_cloud.compute.find_service( srv.name, host=srv.host, ignore_missing=False ) diff --git a/openstack/tests/functional/dns/v2/test_service_status.py b/openstack/tests/functional/dns/v2/test_service_status.py index 562e7b69f..0fa6e9c22 100644 --- a/openstack/tests/functional/dns/v2/test_service_status.py +++ b/openstack/tests/functional/dns/v2/test_service_status.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack import connection from openstack.tests.functional import base @@ -19,8 +18,6 @@ def setUp(self): super().setUp() self.require_service('dns') - self.conn = connection.from_config(cloud_name=base.TEST_CLOUD_NAME) - self.service_names = [ "api", "backend", @@ -34,7 +31,7 @@ def setUp(self): self.service_status = ["UP", "DOWN"] def test_service_status(self): - service_statuses = list(self.conn.dns.service_statuses()) + service_statuses = list(self.operator_cloud.dns.service_statuses()) if not service_statuses: self.skipTest( "The Service in Designate System is required for this test" @@ -49,6 +46,8 @@ def test_service_status(self): self.assertTrue(all(name in self.service_names for name in names)) # Test that we can fetch a service status - service_status = self.conn.dns.get_service_status(service_statuses[0]) + service_status = self.operator_cloud.dns.get_service_status( + service_statuses[0] + ) self.assertIn(service_status.service_name, self.service_names) self.assertIn(service_status.status, self.service_status) diff --git a/openstack/tests/functional/dns/v2/test_zone.py b/openstack/tests/functional/dns/v2/test_zone.py index 3875c78ad..eb9fcd133 100644 --- a/openstack/tests/functional/dns/v2/test_zone.py +++ b/openstack/tests/functional/dns/v2/test_zone.py @@ -11,7 +11,6 @@ # under the License. import random -from openstack import connection from openstack import exceptions from openstack.tests.functional import base from openstack import utils @@ -22,35 +21,33 @@ def setUp(self): super().setUp() self.require_service('dns') - self.conn = connection.from_config(cloud_name=base.TEST_CLOUD_NAME) - # Note: zone deletion is not an immediate operation, so each time # chose a new zone name for a test # getUniqueString is not guaranteed to return unique string between # different tests of the same class. self.ZONE_NAME = f'example-{random.randint(1, 10000)}.org.' - self.zone = self.conn.dns.create_zone( + self.zone = self.operator_cloud.dns.create_zone( name=self.ZONE_NAME, email='joe@example.org', type='PRIMARY', ttl=7200, description='example zone', ) - self.addCleanup(self.conn.dns.delete_zone, self.zone) + self.addCleanup(self.operator_cloud.dns.delete_zone, self.zone) def test_get_zone(self): - zone = self.conn.dns.get_zone(self.zone) + zone = self.operator_cloud.dns.get_zone(self.zone) self.assertEqual(self.zone, zone) def test_list_zones(self): - names = [f.name for f in self.conn.dns.zones()] + names = [f.name for f in self.operator_cloud.dns.zones()] self.assertIn(self.ZONE_NAME, names) def test_update_zone(self): - current_ttl = self.conn.dns.get_zone(self.zone)['ttl'] - self.conn.dns.update_zone(self.zone, ttl=current_ttl + 1) - updated_zone_ttl = self.conn.dns.get_zone(self.zone)['ttl'] + current_ttl = self.operator_cloud.dns.get_zone(self.zone)['ttl'] + self.operator_cloud.dns.update_zone(self.zone, ttl=current_ttl + 1) + updated_zone_ttl = self.operator_cloud.dns.get_zone(self.zone)['ttl'] self.assertEqual( current_ttl + 1, updated_zone_ttl, @@ -58,9 +55,9 @@ def test_update_zone(self): ) def test_create_rs(self): - zone = self.conn.dns.get_zone(self.zone) + zone = self.operator_cloud.dns.get_zone(self.zone) self.assertIsNotNone( - self.conn.dns.create_recordset( + self.operator_cloud.dns.create_recordset( zone=zone, name=f'www.{zone.name}', type='A', @@ -72,30 +69,34 @@ def test_create_rs(self): def test_delete_zone_with_shares(self): # Make sure the API under test has shared zones support - if not utils.supports_version(self.conn.dns, '2.1'): + if not utils.supports_version(self.operator_cloud.dns, '2.1'): self.skipTest( 'Designate API version does not support shared zones.' ) zone_name = f'example-{random.randint(1, 10000)}.org.' - zone = self.conn.dns.create_zone( + zone = self.operator_cloud.dns.create_zone( name=zone_name, email='joe@example.org', type='PRIMARY', ttl=7200, description='example zone', ) - self.addCleanup(self.conn.dns.delete_zone, zone) + self.addCleanup(self.operator_cloud.dns.delete_zone, zone) demo_project_id = self.operator_cloud.get_project('demo')['id'] - zone_share = self.conn.dns.create_zone_share( + zone_share = self.operator_cloud.dns.create_zone_share( zone, target_project_id=demo_project_id ) - self.addCleanup(self.conn.dns.delete_zone_share, zone, zone_share) + self.addCleanup( + self.operator_cloud.dns.delete_zone_share, zone, zone_share + ) # Test that we cannot delete a zone with shares self.assertRaises( - exceptions.BadRequestException, self.conn.dns.delete_zone, zone + exceptions.BadRequestException, + self.operator_cloud.dns.delete_zone, + zone, ) - self.conn.dns.delete_zone(zone, delete_shares=True) + self.operator_cloud.dns.delete_zone(zone, delete_shares=True) diff --git a/openstack/tests/functional/dns/v2/test_zone_share.py b/openstack/tests/functional/dns/v2/test_zone_share.py index 9bed932da..f3c286e62 100644 --- a/openstack/tests/functional/dns/v2/test_zone_share.py +++ b/openstack/tests/functional/dns/v2/test_zone_share.py @@ -31,7 +31,7 @@ def setUp(self): self.ZONE_NAME = f'example-{uuid.uuid4().hex}.org.' # Make sure the API under test has shared zones support - if not utils.supports_version(self.conn.dns, '2.1'): + if not utils.supports_version(self.operator_cloud.dns, '2.1'): self.skipTest( 'Designate API version does not support shared zones.' ) diff --git a/openstack/tests/functional/examples/test_compute.py b/openstack/tests/functional/examples/test_compute.py index 2b0ba6b7b..ea814b6b8 100644 --- a/openstack/tests/functional/examples/test_compute.py +++ b/openstack/tests/functional/examples/test_compute.py @@ -14,7 +14,6 @@ from examples.compute import delete from examples.compute import find as compute_find from examples.compute import list as compute_list -from examples import connect from examples.network import find as network_find from examples.network import list as network_list @@ -24,27 +23,22 @@ class TestCompute(base.BaseFunctionalTest): """Test the compute examples - The purpose of these tests is to ensure the examples run without erring - out. + The purpose of these tests is to ensure the examples run successfully. """ - def setUp(self): - super().setUp() - self.conn = connect.create_connection_from_config() - def test_compute(self): - compute_list.list_servers(self.conn) - compute_list.list_images(self.conn) - compute_list.list_flavors(self.conn) - compute_list.list_keypairs(self.conn) - network_list.list_networks(self.conn) + compute_list.list_servers(self.operator_cloud) + compute_list.list_images(self.operator_cloud) + compute_list.list_flavors(self.operator_cloud) + compute_list.list_keypairs(self.operator_cloud) + network_list.list_networks(self.operator_cloud) - compute_find.find_image(self.conn) - compute_find.find_flavor(self.conn) - compute_find.find_keypair(self.conn) - network_find.find_network(self.conn) + compute_find.find_image(self.operator_cloud) + compute_find.find_flavor(self.operator_cloud) + compute_find.find_keypair(self.operator_cloud) + network_find.find_network(self.operator_cloud) - create.create_server(self.conn) + create.create_server(self.operator_cloud) - delete.delete_keypair(self.conn) - delete.delete_server(self.conn) + delete.delete_keypair(self.operator_cloud) + delete.delete_server(self.operator_cloud) diff --git a/openstack/tests/functional/examples/test_identity.py b/openstack/tests/functional/examples/test_identity.py index a779eb912..285c1c6fb 100644 --- a/openstack/tests/functional/examples/test_identity.py +++ b/openstack/tests/functional/examples/test_identity.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from examples import connect from examples.identity import list as identity_list from openstack.tests.functional import base @@ -19,20 +18,15 @@ class TestIdentity(base.BaseFunctionalTest): """Test the identity examples - The purpose of these tests is to ensure the examples run without erring - out. + The purpose of these tests is to ensure the examples run successfully. """ - def setUp(self): - super().setUp() - self.conn = connect.create_connection_from_config() - def test_identity(self): - identity_list.list_users(self.conn) - identity_list.list_credentials(self.conn) - identity_list.list_projects(self.conn) - identity_list.list_domains(self.conn) - identity_list.list_groups(self.conn) - identity_list.list_services(self.conn) - identity_list.list_endpoints(self.conn) - identity_list.list_regions(self.conn) + identity_list.list_users(self.operator_cloud) + identity_list.list_credentials(self.operator_cloud) + identity_list.list_projects(self.operator_cloud) + identity_list.list_domains(self.operator_cloud) + identity_list.list_groups(self.operator_cloud) + identity_list.list_services(self.operator_cloud) + identity_list.list_endpoints(self.operator_cloud) + identity_list.list_regions(self.operator_cloud) diff --git a/openstack/tests/functional/examples/test_image.py b/openstack/tests/functional/examples/test_image.py index 5dfd6c7bb..157f4a84d 100644 --- a/openstack/tests/functional/examples/test_image.py +++ b/openstack/tests/functional/examples/test_image.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from examples import connect from examples.image import create as image_create from examples.image import delete as image_delete from examples.image import list as image_list @@ -21,17 +20,12 @@ class TestImage(base.BaseFunctionalTest): """Test the image examples - The purpose of these tests is to ensure the examples run without erring - out. + The purpose of these tests is to ensure the examples run successfully. """ - def setUp(self): - super().setUp() - self.conn = connect.create_connection_from_config() - def test_image(self): - image_list.list_images(self.conn) + image_list.list_images(self.operator_cloud) - image_create.upload_image(self.conn) + image_create.upload_image(self.operator_cloud) - image_delete.delete_image(self.conn) + image_delete.delete_image(self.operator_cloud) diff --git a/openstack/tests/functional/examples/test_network.py b/openstack/tests/functional/examples/test_network.py index e6064b919..b1dceb603 100644 --- a/openstack/tests/functional/examples/test_network.py +++ b/openstack/tests/functional/examples/test_network.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from examples import connect from examples.network import create as network_create from examples.network import delete as network_delete from examples.network import find as network_find @@ -22,22 +21,17 @@ class TestNetwork(base.BaseFunctionalTest): """Test the network examples - The purpose of these tests is to ensure the examples run without erring - out. + The purpose of these tests is to ensure the examples run successfully. """ - def setUp(self): - super().setUp() - self.conn = connect.create_connection_from_config() - def test_network(self): - network_list.list_networks(self.conn) - network_list.list_subnets(self.conn) - network_list.list_ports(self.conn) - network_list.list_security_groups(self.conn) - network_list.list_routers(self.conn) + network_list.list_networks(self.operator_cloud) + network_list.list_subnets(self.operator_cloud) + network_list.list_ports(self.operator_cloud) + network_list.list_security_groups(self.operator_cloud) + network_list.list_routers(self.operator_cloud) - network_find.find_network(self.conn) + network_find.find_network(self.operator_cloud) - network_create.create_network(self.conn) - network_delete.delete_network(self.conn) + network_create.create_network(self.operator_cloud) + network_delete.delete_network(self.operator_cloud) diff --git a/openstack/tests/functional/identity/v3/test_access_rule.py b/openstack/tests/functional/identity/v3/test_access_rule.py index cdcec1a36..4a4c83b9a 100644 --- a/openstack/tests/functional/identity/v3/test_access_rule.py +++ b/openstack/tests/functional/identity/v3/test_access_rule.py @@ -22,7 +22,7 @@ def setUp(self): def _create_application_credential_with_access_rule(self): """create application credential with access_rule.""" - app_cred = self.conn.identity.create_application_credential( + app_cred = self.operator_cloud.identity.create_application_credential( user=self.user_id, name='app_cred', access_rules=[ @@ -34,7 +34,7 @@ def _create_application_credential_with_access_rule(self): ], ) self.addCleanup( - self.conn.identity.delete_application_credential, + self.operator_cloud.identity.delete_application_credential, self.user_id, app_cred['id'], ) @@ -43,7 +43,7 @@ def _create_application_credential_with_access_rule(self): def test_get_access_rule(self): app_cred = self._create_application_credential_with_access_rule() access_rule_id = app_cred['access_rules'][0]['id'] - access_rule = self.conn.identity.get_access_rule( + access_rule = self.operator_cloud.identity.get_access_rule( user=self.user_id, access_rule=access_rule_id ) self.assertEqual(access_rule['id'], access_rule_id) @@ -52,7 +52,9 @@ def test_get_access_rule(self): def test_list_access_rules(self): app_cred = self._create_application_credential_with_access_rule() access_rule_id = app_cred['access_rules'][0]['id'] - access_rules = self.conn.identity.access_rules(user=self.user_id) + access_rules = self.operator_cloud.identity.access_rules( + user=self.user_id + ) self.assertEqual(1, len(list(access_rules))) for access_rule in access_rules: self.assertEqual(app_cred['user_id'], self.user_id) @@ -66,16 +68,16 @@ def test_delete_access_rule(self): # in use for app_cred. self.assertRaises( exceptions.HttpException, - self.conn.identity.delete_access_rule, + self.operator_cloud.identity.delete_access_rule, user=self.user_id, access_rule=access_rule_id, ) # delete application credential first to delete access rule - self.conn.identity.delete_application_credential( + self.operator_cloud.identity.delete_application_credential( user=self.user_id, application_credential=app_cred['id'] ) # delete orphaned access rules - self.conn.identity.delete_access_rule( + self.operator_cloud.identity.delete_access_rule( user=self.user_id, access_rule=access_rule_id ) diff --git a/openstack/tests/functional/identity/v3/test_application_credential.py b/openstack/tests/functional/identity/v3/test_application_credential.py index f05db4b4e..2afbaac09 100644 --- a/openstack/tests/functional/identity/v3/test_application_credential.py +++ b/openstack/tests/functional/identity/v3/test_application_credential.py @@ -20,11 +20,11 @@ def setUp(self): self.user_id = self.operator_cloud.current_user_id def _create_application_credentials(self): - app_creds = self.conn.identity.create_application_credential( + app_creds = self.operator_cloud.identity.create_application_credential( user=self.user_id, name='app_cred' ) self.addCleanup( - self.conn.identity.delete_application_credential, + self.operator_cloud.identity.delete_application_credential, self.user_id, app_creds['id'], ) @@ -36,7 +36,7 @@ def test_create_application_credentials(self): def test_get_application_credential(self): app_creds = self._create_application_credentials() - app_cred = self.conn.identity.get_application_credential( + app_cred = self.operator_cloud.identity.get_application_credential( user=self.user_id, application_credential=app_creds['id'] ) self.assertEqual(app_cred['id'], app_creds['id']) @@ -44,7 +44,7 @@ def test_get_application_credential(self): def test_application_credentials(self): self._create_application_credentials() - app_creds = self.conn.identity.application_credentials( + app_creds = self.operator_cloud.identity.application_credentials( user=self.user_id ) for app_cred in app_creds: @@ -52,7 +52,7 @@ def test_application_credentials(self): def test_find_application_credential(self): app_creds = self._create_application_credentials() - app_cred = self.conn.identity.find_application_credential( + app_cred = self.operator_cloud.identity.find_application_credential( user=self.user_id, name_or_id=app_creds['id'] ) self.assertEqual(app_cred['id'], app_creds['id']) @@ -60,12 +60,12 @@ def test_find_application_credential(self): def test_delete_application_credential(self): app_creds = self._create_application_credentials() - self.conn.identity.delete_application_credential( + self.operator_cloud.identity.delete_application_credential( user=self.user_id, application_credential=app_creds['id'] ) self.assertRaises( exceptions.NotFoundException, - self.conn.identity.get_application_credential, + self.operator_cloud.identity.get_application_credential, user=self.user_id, application_credential=app_creds['id'], ) diff --git a/openstack/tests/functional/image/v2/test_image.py b/openstack/tests/functional/image/v2/test_image.py index 6590bddd1..7eafe828c 100644 --- a/openstack/tests/functional/image/v2/test_image.py +++ b/openstack/tests/functional/image/v2/test_image.py @@ -23,7 +23,7 @@ def setUp(self): super().setUp() # there's a limit on name length - self.image = self.conn.image.create_image( + self.image = self.operator_cloud.image.create_image( name=TEST_IMAGE_NAME, disk_format='raw', container_format='bare', @@ -38,50 +38,50 @@ def setUp(self): def tearDown(self): # we do this in tearDown rather than via 'addCleanup' since we want to # wait for the deletion of the resource to ensure it completes - self.conn.image.delete_image(self.image) - self.conn.image.wait_for_delete(self.image) + self.operator_cloud.image.delete_image(self.image) + self.operator_cloud.image.wait_for_delete(self.image) super().tearDown() def test_images(self): # get image - image = self.conn.image.get_image(self.image.id) + image = self.operator_cloud.image.get_image(self.image.id) self.assertEqual(self.image.name, image.name) # find image - image = self.conn.image.find_image(self.image.name) + image = self.operator_cloud.image.find_image(self.image.name) self.assertEqual(self.image.id, image.id) # list - images = list(self.conn.image.images()) + images = list(self.operator_cloud.image.images()) # there are many other images so we don't assert that this is the # *only* image present self.assertIn(self.image.id, {i.id for i in images}) # update image_name = self.getUniqueString() - image = self.conn.image.update_image( + image = self.operator_cloud.image.update_image( self.image, name=image_name, ) self.assertIsInstance(image, _image.Image) - image = self.conn.image.get_image(self.image.id) + image = self.operator_cloud.image.get_image(self.image.id) self.assertEqual(image_name, image.name) def test_tags(self): # add tag - image = self.conn.image.get_image(self.image) - self.conn.image.add_tag(image, 't1') - self.conn.image.add_tag(image, 't2') + image = self.operator_cloud.image.get_image(self.image) + self.operator_cloud.image.add_tag(image, 't1') + self.operator_cloud.image.add_tag(image, 't2') # filter image by tags - image = list(self.conn.image.images(tag=['t1', 't2']))[0] + image = list(self.operator_cloud.image.images(tag=['t1', 't2']))[0] self.assertEqual(image.id, image.id) self.assertIn('t1', image.tags) self.assertIn('t2', image.tags) # remove tag - self.conn.image.remove_tag(image, 't1') - image = self.conn.image.get_image(self.image) + self.operator_cloud.image.remove_tag(image, 't1') + image = self.operator_cloud.image.get_image(self.image) self.assertIn('t2', image.tags) self.assertNotIn('t1', image.tags) diff --git a/openstack/tests/functional/image/v2/test_metadef_namespace.py b/openstack/tests/functional/image/v2/test_metadef_namespace.py index 8fb9a83d4..641926cc9 100644 --- a/openstack/tests/functional/image/v2/test_metadef_namespace.py +++ b/openstack/tests/functional/image/v2/test_metadef_namespace.py @@ -21,8 +21,10 @@ def setUp(self): # there's a limit on namespace length namespace = self.getUniqueString().split('.')[-1] - self.metadef_namespace = self.conn.image.create_metadef_namespace( - namespace=namespace, + self.metadef_namespace = ( + self.operator_cloud.image.create_metadef_namespace( + namespace=namespace, + ) ) self.assertIsInstance( self.metadef_namespace, @@ -33,14 +35,16 @@ def setUp(self): def tearDown(self): # we do this in tearDown rather than via 'addCleanup' since we want to # wait for the deletion of the resource to ensure it completes - self.conn.image.delete_metadef_namespace(self.metadef_namespace) - self.conn.image.wait_for_delete(self.metadef_namespace) + self.operator_cloud.image.delete_metadef_namespace( + self.metadef_namespace + ) + self.operator_cloud.image.wait_for_delete(self.metadef_namespace) super().tearDown() def test_metadef_namespace(self): # get - metadef_namespace = self.conn.image.get_metadef_namespace( + metadef_namespace = self.operator_cloud.image.get_metadef_namespace( self.metadef_namespace.namespace ) self.assertEqual( @@ -51,7 +55,9 @@ def test_metadef_namespace(self): # (no find_metadef_namespace method) # list - metadef_namespaces = list(self.conn.image.metadef_namespaces()) + metadef_namespaces = list( + self.operator_cloud.image.metadef_namespaces() + ) # there are a load of default metadef namespaces so we don't assert # that this is the *only* metadef namespace present self.assertIn( @@ -64,7 +70,7 @@ def test_metadef_namespace(self): # inherent need for randomness so we use fixed strings metadef_namespace_display_name = 'A display name' metadef_namespace_description = 'A description' - metadef_namespace = self.conn.image.update_metadef_namespace( + metadef_namespace = self.operator_cloud.image.update_metadef_namespace( self.metadef_namespace, display_name=metadef_namespace_display_name, description=metadef_namespace_description, @@ -73,7 +79,7 @@ def test_metadef_namespace(self): metadef_namespace, _metadef_namespace.MetadefNamespace, ) - metadef_namespace = self.conn.image.get_metadef_namespace( + metadef_namespace = self.operator_cloud.image.get_metadef_namespace( self.metadef_namespace.namespace ) self.assertEqual( diff --git a/openstack/tests/functional/image/v2/test_metadef_object.py b/openstack/tests/functional/image/v2/test_metadef_object.py index 2edd2e364..1dd14f9c7 100644 --- a/openstack/tests/functional/image/v2/test_metadef_object.py +++ b/openstack/tests/functional/image/v2/test_metadef_object.py @@ -21,8 +21,10 @@ def setUp(self): # create namespace for object namespace = self.getUniqueString().split('.')[-1] - self.metadef_namespace = self.conn.image.create_metadef_namespace( - namespace=namespace, + self.metadef_namespace = ( + self.operator_cloud.image.create_metadef_namespace( + namespace=namespace, + ) ) self.assertIsInstance( self.metadef_namespace, @@ -32,7 +34,7 @@ def setUp(self): # create object object = self.getUniqueString().split('.')[-1] - self.metadef_object = self.conn.image.create_metadef_object( + self.metadef_object = self.operator_cloud.image.create_metadef_object( name=object, namespace=self.metadef_namespace, ) @@ -43,20 +45,22 @@ def setUp(self): self.assertEqual(object, self.metadef_object.name) def tearDown(self): - self.conn.image.delete_metadef_object( + self.operator_cloud.image.delete_metadef_object( self.metadef_object, self.metadef_object.namespace_name, ) - self.conn.image.wait_for_delete(self.metadef_object) + self.operator_cloud.image.wait_for_delete(self.metadef_object) - self.conn.image.delete_metadef_namespace(self.metadef_namespace) - self.conn.image.wait_for_delete(self.metadef_namespace) + self.operator_cloud.image.delete_metadef_namespace( + self.metadef_namespace + ) + self.operator_cloud.image.wait_for_delete(self.metadef_namespace) super().tearDown() def test_metadef_objects(self): # get - metadef_object = self.conn.image.get_metadef_object( + metadef_object = self.operator_cloud.image.get_metadef_object( self.metadef_object.name, self.metadef_namespace, ) @@ -71,7 +75,9 @@ def test_metadef_objects(self): # list metadef_objects = list( - self.conn.image.metadef_objects(self.metadef_object.namespace_name) + self.operator_cloud.image.metadef_objects( + self.metadef_object.namespace_name + ) ) # there are a load of default metadef objects so we don't assert # that this is the *only* metadef objects present @@ -83,7 +89,7 @@ def test_metadef_objects(self): # update metadef_object_new_name = 'New object name' metadef_object_new_description = 'New object description' - metadef_object = self.conn.image.update_metadef_object( + metadef_object = self.operator_cloud.image.update_metadef_object( self.metadef_object.name, namespace=self.metadef_object.namespace_name, name=metadef_object_new_name, diff --git a/openstack/tests/functional/image/v2/test_metadef_property.py b/openstack/tests/functional/image/v2/test_metadef_property.py index f490faab4..64cd53d4b 100644 --- a/openstack/tests/functional/image/v2/test_metadef_property.py +++ b/openstack/tests/functional/image/v2/test_metadef_property.py @@ -26,8 +26,10 @@ def setUp(self): namespace = 'test_' + ''.join( random.choice(string.ascii_lowercase) for _ in range(75) ) - self.metadef_namespace = self.conn.image.create_metadef_namespace( - namespace=namespace, + self.metadef_namespace = ( + self.operator_cloud.image.create_metadef_namespace( + namespace=namespace, + ) ) self.assertIsInstance( self.metadef_namespace, @@ -46,8 +48,10 @@ def setUp(self): 'description': 'Web Server port', 'enum': ["80", "443"], } - self.metadef_property = self.conn.image.create_metadef_property( - self.metadef_namespace.namespace, **self.attrs + self.metadef_property = ( + self.operator_cloud.image.create_metadef_property( + self.metadef_namespace.namespace, **self.attrs + ) ) self.assertIsInstance( self.metadef_property, _metadef_property.MetadefProperty @@ -63,17 +67,19 @@ def setUp(self): def tearDown(self): # we do this in tearDown rather than via 'addCleanup' since we want to # wait for the deletion of the resource to ensure it completes - self.conn.image.delete_metadef_property( + self.operator_cloud.image.delete_metadef_property( self.metadef_property, self.metadef_namespace ) - self.conn.image.delete_metadef_namespace(self.metadef_namespace) - self.conn.image.wait_for_delete(self.metadef_namespace) + self.operator_cloud.image.delete_metadef_namespace( + self.metadef_namespace + ) + self.operator_cloud.image.wait_for_delete(self.metadef_namespace) super().tearDown() def test_metadef_property(self): # get metadef property - metadef_property = self.conn.image.get_metadef_property( + metadef_property = self.operator_cloud.image.get_metadef_property( self.metadef_property, self.metadef_namespace ) self.assertIsNotNone(metadef_property) @@ -92,7 +98,9 @@ def test_metadef_property(self): # list metadef_properties = list( - self.conn.image.metadef_properties(self.metadef_namespace) + self.operator_cloud.image.metadef_properties( + self.metadef_namespace + ) ) self.assertIsNotNone(metadef_properties) self.assertIsInstance( @@ -106,7 +114,7 @@ def test_metadef_property(self): self.attrs['description'] = ''.join( random.choice(string.ascii_lowercase) for _ in range(10) ) - metadef_property = self.conn.image.update_metadef_property( + metadef_property = self.operator_cloud.image.update_metadef_property( self.metadef_property, self.metadef_namespace.namespace, **self.attrs, @@ -116,7 +124,7 @@ def test_metadef_property(self): metadef_property, _metadef_property.MetadefProperty, ) - metadef_property = self.conn.image.get_metadef_property( + metadef_property = self.operator_cloud.image.get_metadef_property( self.metadef_property.name, self.metadef_namespace ) self.assertEqual( diff --git a/openstack/tests/functional/image/v2/test_metadef_resource_type.py b/openstack/tests/functional/image/v2/test_metadef_resource_type.py index 7663f6950..797ea3176 100644 --- a/openstack/tests/functional/image/v2/test_metadef_resource_type.py +++ b/openstack/tests/functional/image/v2/test_metadef_resource_type.py @@ -21,8 +21,10 @@ def setUp(self): # there's a limit on namespace length namespace = self.getUniqueString().split('.')[-1] - self.metadef_namespace = self.conn.image.create_metadef_namespace( - namespace=namespace, + self.metadef_namespace = ( + self.operator_cloud.image.create_metadef_namespace( + namespace=namespace, + ) ) self.assertIsInstance( self.metadef_namespace, @@ -33,7 +35,7 @@ def setUp(self): resource_type_name = 'test-resource-type' resource_type = {'name': resource_type_name} self.metadef_resource_type = ( - self.conn.image.create_metadef_resource_type_association( + self.operator_cloud.image.create_metadef_resource_type_association( metadef_namespace=namespace, **resource_type ) ) @@ -46,15 +48,17 @@ def setUp(self): def tearDown(self): # we do this in tearDown rather than via 'addCleanup' since we want to # wait for the deletion of the resource to ensure it completes - self.conn.image.delete_metadef_namespace(self.metadef_namespace) - self.conn.image.wait_for_delete(self.metadef_namespace) + self.operator_cloud.image.delete_metadef_namespace( + self.metadef_namespace + ) + self.operator_cloud.image.wait_for_delete(self.metadef_namespace) super().tearDown() def test_metadef_resource_types(self): # list resource type associations associations = list( - self.conn.image.metadef_resource_type_associations( + self.operator_cloud.image.metadef_resource_type_associations( metadef_namespace=self.metadef_namespace ) ) @@ -66,14 +70,16 @@ def test_metadef_resource_types(self): # (no find_metadef_resource_type_association method) # list resource types - resource_types = list(self.conn.image.metadef_resource_types()) + resource_types = list( + self.operator_cloud.image.metadef_resource_types() + ) self.assertIn( self.metadef_resource_type.name, {t.name for t in resource_types} ) # delete - self.conn.image.delete_metadef_resource_type_association( + self.operator_cloud.image.delete_metadef_resource_type_association( self.metadef_resource_type, metadef_namespace=self.metadef_namespace, ) diff --git a/openstack/tests/functional/image/v2/test_metadef_schema.py b/openstack/tests/functional/image/v2/test_metadef_schema.py index 6ef26fbe1..d427a8de9 100644 --- a/openstack/tests/functional/image/v2/test_metadef_schema.py +++ b/openstack/tests/functional/image/v2/test_metadef_schema.py @@ -16,51 +16,63 @@ class TestMetadefSchema(base.BaseImageTest): def test_get_metadef_namespace_schema(self): - metadef_schema = self.conn.image.get_metadef_namespace_schema() + metadef_schema = ( + self.operator_cloud.image.get_metadef_namespace_schema() + ) self.assertIsNotNone(metadef_schema) self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) def test_get_metadef_namespaces_schema(self): - metadef_schema = self.conn.image.get_metadef_namespaces_schema() + metadef_schema = ( + self.operator_cloud.image.get_metadef_namespaces_schema() + ) self.assertIsNotNone(metadef_schema) self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) def test_get_metadef_resource_type_schema(self): - metadef_schema = self.conn.image.get_metadef_resource_type_schema() + metadef_schema = ( + self.operator_cloud.image.get_metadef_resource_type_schema() + ) self.assertIsNotNone(metadef_schema) self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) def test_get_metadef_resource_types_schema(self): - metadef_schema = self.conn.image.get_metadef_resource_types_schema() + metadef_schema = ( + self.operator_cloud.image.get_metadef_resource_types_schema() + ) self.assertIsNotNone(metadef_schema) self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) def test_get_metadef_object_schema(self): - metadef_schema = self.conn.image.get_metadef_object_schema() + metadef_schema = self.operator_cloud.image.get_metadef_object_schema() self.assertIsNotNone(metadef_schema) self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) def test_get_metadef_objects_schema(self): - metadef_schema = self.conn.image.get_metadef_objects_schema() + metadef_schema = self.operator_cloud.image.get_metadef_objects_schema() self.assertIsNotNone(metadef_schema) self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) def test_get_metadef_property_schema(self): - metadef_schema = self.conn.image.get_metadef_property_schema() + metadef_schema = ( + self.operator_cloud.image.get_metadef_property_schema() + ) self.assertIsNotNone(metadef_schema) self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) def test_get_metadef_properties_schema(self): - metadef_schema = self.conn.image.get_metadef_properties_schema() + metadef_schema = ( + self.operator_cloud.image.get_metadef_properties_schema() + ) self.assertIsNotNone(metadef_schema) self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) def test_get_metadef_tag_schema(self): - metadef_schema = self.conn.image.get_metadef_tag_schema() + metadef_schema = self.operator_cloud.image.get_metadef_tag_schema() self.assertIsNotNone(metadef_schema) self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) def test_get_metadef_tags_schema(self): - metadef_schema = self.conn.image.get_metadef_tags_schema() + metadef_schema = self.operator_cloud.image.get_metadef_tags_schema() self.assertIsNotNone(metadef_schema) self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) diff --git a/openstack/tests/functional/image/v2/test_schema.py b/openstack/tests/functional/image/v2/test_schema.py index 6b16894b0..29f84a255 100644 --- a/openstack/tests/functional/image/v2/test_schema.py +++ b/openstack/tests/functional/image/v2/test_schema.py @@ -16,21 +16,21 @@ class TestSchema(base.BaseImageTest): def test_get_images_schema(self): - schema = self.conn.image.get_images_schema() + schema = self.operator_cloud.image.get_images_schema() self.assertIsNotNone(schema) self.assertIsInstance(schema, _schema.Schema) def test_get_image_schema(self): - schema = self.conn.image.get_image_schema() + schema = self.operator_cloud.image.get_image_schema() self.assertIsNotNone(schema) self.assertIsInstance(schema, _schema.Schema) def test_get_members_schema(self): - schema = self.conn.image.get_members_schema() + schema = self.operator_cloud.image.get_members_schema() self.assertIsNotNone(schema) self.assertIsInstance(schema, _schema.Schema) def test_get_member_schema(self): - schema = self.conn.image.get_member_schema() + schema = self.operator_cloud.image.get_member_schema() self.assertIsNotNone(schema) self.assertIsInstance(schema, _schema.Schema) diff --git a/openstack/tests/functional/image/v2/test_task.py b/openstack/tests/functional/image/v2/test_task.py index 19fc52736..d48819a72 100644 --- a/openstack/tests/functional/image/v2/test_task.py +++ b/openstack/tests/functional/image/v2/test_task.py @@ -15,7 +15,7 @@ class TestTask(base.BaseImageTest): def test_tasks(self): - tasks = list(self.conn.image.tasks()) + tasks = list(self.operator_cloud.image.tasks()) # NOTE(stephenfin): Yes, this is a dumb test. Basically all that we're # checking is that the API endpoint is correct. It would be nice to # have a proper check here that includes creation of tasks but we don't diff --git a/openstack/tests/functional/instance_ha/test_host.py b/openstack/tests/functional/instance_ha/test_host.py index 4577863e0..71801fc1e 100644 --- a/openstack/tests/functional/instance_ha/test_host.py +++ b/openstack/tests/functional/instance_ha/test_host.py @@ -43,13 +43,13 @@ def setUp(self): ) # Create segment - self.segment = self.conn.ha.create_segment( + self.segment = self.operator_cloud.ha.create_segment( name=self.NAME, recovery_method='auto', service_type='COMPUTE' ) # Create valid host self.NAME = HYPERVISORS[0].name - self.host = self.conn.ha.create_host( + self.host = self.operator_cloud.ha.create_host( segment_id=self.segment.uuid, name=self.NAME, type='COMPUTE', @@ -58,15 +58,19 @@ def setUp(self): # Delete host self.addCleanup( - self.conn.ha.delete_host, self.segment.uuid, self.host.uuid + self.operator_cloud.ha.delete_host, + self.segment.uuid, + self.host.uuid, ) # Delete segment - self.addCleanup(self.conn.ha.delete_segment, self.segment.uuid) + self.addCleanup( + self.operator_cloud.ha.delete_segment, self.segment.uuid + ) def test_list(self): names = [ o.name - for o in self.conn.ha.hosts( + for o in self.operator_cloud.ha.hosts( self.segment.uuid, failover_segment_id=self.segment.uuid, type='COMPUTE', @@ -75,12 +79,12 @@ def test_list(self): self.assertIn(self.NAME, names) def test_update(self): - updated_host = self.conn.ha.update_host( + updated_host = self.operator_cloud.ha.update_host( self.host['uuid'], segment_id=self.segment.uuid, on_maintenance='True', ) - get_host = self.conn.ha.get_host( + get_host = self.operator_cloud.ha.get_host( updated_host.uuid, updated_host.segment_id ) self.assertEqual(True, get_host.on_maintenance) diff --git a/openstack/tests/functional/instance_ha/test_segment.py b/openstack/tests/functional/instance_ha/test_segment.py index 1f8611562..0d147bc5e 100644 --- a/openstack/tests/functional/instance_ha/test_segment.py +++ b/openstack/tests/functional/instance_ha/test_segment.py @@ -23,20 +23,27 @@ def setUp(self): self.NAME = self.getUniqueString() # Create segment - self.segment = self.conn.ha.create_segment( + self.segment = self.operator_cloud.ha.create_segment( name=self.NAME, recovery_method='auto', service_type='COMPUTE' ) # Delete segment - self.addCleanup(self.conn.ha.delete_segment, self.segment['uuid']) + self.addCleanup( + self.operator_cloud.ha.delete_segment, self.segment['uuid'] + ) def test_list(self): - names = [o.name for o in self.conn.ha.segments(recovery_method='auto')] + names = [ + o.name + for o in self.operator_cloud.ha.segments(recovery_method='auto') + ] self.assertIn(self.NAME, names) def test_update(self): - updated_segment = self.conn.ha.update_segment( + updated_segment = self.operator_cloud.ha.update_segment( self.segment['uuid'], name='UPDATED-NAME' ) - get_updated_segment = self.conn.ha.get_segment(updated_segment.uuid) + get_updated_segment = self.operator_cloud.ha.get_segment( + updated_segment.uuid + ) self.assertEqual('UPDATED-NAME', get_updated_segment.name) diff --git a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py index c43ba6ff4..89e6bf18c 100644 --- a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py +++ b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py @@ -79,10 +79,10 @@ def setUp(self): self.FLAVOR_NAME = self.getUniqueString() self.AVAILABILITY_ZONE_PROFILE_NAME = self.getUniqueString() self.AVAILABILITY_ZONE_NAME = self.getUniqueString() - subnets = list(self.conn.network.subnets()) + subnets = list(self.operator_cloud.network.subnets()) self.VIP_SUBNET_ID = subnets[0].id - self.PROJECT_ID = self.conn.session.get_project_id() - test_quota = self.conn.load_balancer.update_quota( + self.PROJECT_ID = self.operator_cloud.session.get_project_id() + test_quota = self.operator_cloud.load_balancer.update_quota( self.PROJECT_ID, **{ 'load_balancer': 100, @@ -95,16 +95,18 @@ def setUp(self): assert isinstance(test_quota, quota.Quota) self.assertEqual(self.PROJECT_ID, test_quota.id) - test_flavor_profile = self.conn.load_balancer.create_flavor_profile( - name=self.FLAVOR_PROFILE_NAME, - provider_name=self.AMPHORA, - flavor_data=self.FLAVOR_DATA, + test_flavor_profile = ( + self.operator_cloud.load_balancer.create_flavor_profile( + name=self.FLAVOR_PROFILE_NAME, + provider_name=self.AMPHORA, + flavor_data=self.FLAVOR_DATA, + ) ) assert isinstance(test_flavor_profile, flavor_profile.FlavorProfile) self.assertEqual(self.FLAVOR_PROFILE_NAME, test_flavor_profile.name) self.FLAVOR_PROFILE_ID = test_flavor_profile.id - test_flavor = self.conn.load_balancer.create_flavor( + test_flavor = self.operator_cloud.load_balancer.create_flavor( name=self.FLAVOR_NAME, flavor_profile_id=self.FLAVOR_PROFILE_ID, is_enabled=True, @@ -115,7 +117,7 @@ def setUp(self): self.FLAVOR_ID = test_flavor.id test_az_profile = ( - self.conn.load_balancer.create_availability_zone_profile( + self.operator_cloud.load_balancer.create_availability_zone_profile( name=self.AVAILABILITY_ZONE_PROFILE_NAME, provider_name=self.AMPHORA, availability_zone_data=self.AVAILABILITY_ZONE_DATA, @@ -129,7 +131,7 @@ def setUp(self): ) self.AVAILABILITY_ZONE_PROFILE_ID = test_az_profile.id - test_az = self.conn.load_balancer.create_availability_zone( + test_az = self.operator_cloud.load_balancer.create_availability_zone( name=self.AVAILABILITY_ZONE_NAME, availability_zone_profile_id=self.AVAILABILITY_ZONE_PROFILE_ID, is_enabled=True, @@ -138,7 +140,7 @@ def setUp(self): assert isinstance(test_az, availability_zone.AvailabilityZone) self.assertEqual(self.AVAILABILITY_ZONE_NAME, test_az.name) - test_lb = self.conn.load_balancer.create_load_balancer( + test_lb = self.operator_cloud.load_balancer.create_load_balancer( name=self.LB_NAME, vip_subnet_id=self.VIP_SUBNET_ID, project_id=self.PROJECT_ID, @@ -147,16 +149,18 @@ def setUp(self): self.assertEqual(self.LB_NAME, test_lb.name) # Wait for the LB to go ACTIVE. On non-virtualization enabled hosts # it can take nova up to ten minutes to boot a VM. - self.conn.load_balancer.wait_for_load_balancer( + self.operator_cloud.load_balancer.wait_for_load_balancer( test_lb.id, interval=1, wait=self._wait_for_timeout ) self.LB_ID = test_lb.id - amphorae = self.conn.load_balancer.amphorae(loadbalancer_id=self.LB_ID) + amphorae = self.operator_cloud.load_balancer.amphorae( + loadbalancer_id=self.LB_ID + ) for amp in amphorae: self.AMPHORA_ID = amp.id - test_listener = self.conn.load_balancer.create_listener( + test_listener = self.operator_cloud.load_balancer.create_listener( name=self.LISTENER_NAME, protocol=self.PROTOCOL, protocol_port=self.PROTOCOL_PORT, @@ -165,11 +169,11 @@ def setUp(self): assert isinstance(test_listener, listener.Listener) self.assertEqual(self.LISTENER_NAME, test_listener.name) self.LISTENER_ID = test_listener.id - self.conn.load_balancer.wait_for_load_balancer( + self.operator_cloud.load_balancer.wait_for_load_balancer( self.LB_ID, wait=self._wait_for_timeout ) - test_pool = self.conn.load_balancer.create_pool( + test_pool = self.operator_cloud.load_balancer.create_pool( name=self.POOL_NAME, protocol=self.PROTOCOL, lb_algorithm=self.LB_ALGORITHM, @@ -178,11 +182,11 @@ def setUp(self): assert isinstance(test_pool, pool.Pool) self.assertEqual(self.POOL_NAME, test_pool.name) self.POOL_ID = test_pool.id - self.conn.load_balancer.wait_for_load_balancer( + self.operator_cloud.load_balancer.wait_for_load_balancer( self.LB_ID, wait=self._wait_for_timeout ) - test_member = self.conn.load_balancer.create_member( + test_member = self.operator_cloud.load_balancer.create_member( pool=self.POOL_ID, name=self.MEMBER_NAME, address=self.MEMBER_ADDRESS, @@ -192,11 +196,11 @@ def setUp(self): assert isinstance(test_member, member.Member) self.assertEqual(self.MEMBER_NAME, test_member.name) self.MEMBER_ID = test_member.id - self.conn.load_balancer.wait_for_load_balancer( + self.operator_cloud.load_balancer.wait_for_load_balancer( self.LB_ID, wait=self._wait_for_timeout ) - test_hm = self.conn.load_balancer.create_health_monitor( + test_hm = self.operator_cloud.load_balancer.create_health_monitor( pool_id=self.POOL_ID, name=self.HM_NAME, delay=self.DELAY, @@ -207,11 +211,11 @@ def setUp(self): assert isinstance(test_hm, health_monitor.HealthMonitor) self.assertEqual(self.HM_NAME, test_hm.name) self.HM_ID = test_hm.id - self.conn.load_balancer.wait_for_load_balancer( + self.operator_cloud.load_balancer.wait_for_load_balancer( self.LB_ID, wait=self._wait_for_timeout ) - test_l7policy = self.conn.load_balancer.create_l7_policy( + test_l7policy = self.operator_cloud.load_balancer.create_l7_policy( listener_id=self.LISTENER_ID, name=self.L7POLICY_NAME, action=self.ACTION, @@ -220,11 +224,11 @@ def setUp(self): assert isinstance(test_l7policy, l7_policy.L7Policy) self.assertEqual(self.L7POLICY_NAME, test_l7policy.name) self.L7POLICY_ID = test_l7policy.id - self.conn.load_balancer.wait_for_load_balancer( + self.operator_cloud.load_balancer.wait_for_load_balancer( self.LB_ID, wait=self._wait_for_timeout ) - test_l7rule = self.conn.load_balancer.create_l7_rule( + test_l7rule = self.operator_cloud.load_balancer.create_l7_rule( l7_policy=self.L7POLICY_ID, compare_type=self.COMPARE_TYPE, type=self.L7RULE_TYPE, @@ -233,94 +237,102 @@ def setUp(self): assert isinstance(test_l7rule, l7_rule.L7Rule) self.assertEqual(self.COMPARE_TYPE, test_l7rule.compare_type) self.L7RULE_ID = test_l7rule.id - self.conn.load_balancer.wait_for_load_balancer( + self.operator_cloud.load_balancer.wait_for_load_balancer( self.LB_ID, wait=self._wait_for_timeout ) def tearDown(self): - self.conn.load_balancer.get_load_balancer(self.LB_ID) - self.conn.load_balancer.wait_for_load_balancer( + self.operator_cloud.load_balancer.get_load_balancer(self.LB_ID) + self.operator_cloud.load_balancer.wait_for_load_balancer( self.LB_ID, wait=self._wait_for_timeout ) - self.conn.load_balancer.delete_quota( + self.operator_cloud.load_balancer.delete_quota( self.PROJECT_ID, ignore_missing=False ) - self.conn.load_balancer.delete_l7_rule( + self.operator_cloud.load_balancer.delete_l7_rule( self.L7RULE_ID, l7_policy=self.L7POLICY_ID, ignore_missing=False ) - self.conn.load_balancer.wait_for_load_balancer( + self.operator_cloud.load_balancer.wait_for_load_balancer( self.LB_ID, wait=self._wait_for_timeout ) - self.conn.load_balancer.delete_l7_policy( + self.operator_cloud.load_balancer.delete_l7_policy( self.L7POLICY_ID, ignore_missing=False ) - self.conn.load_balancer.wait_for_load_balancer( + self.operator_cloud.load_balancer.wait_for_load_balancer( self.LB_ID, wait=self._wait_for_timeout ) - self.conn.load_balancer.delete_health_monitor( + self.operator_cloud.load_balancer.delete_health_monitor( self.HM_ID, ignore_missing=False ) - self.conn.load_balancer.wait_for_load_balancer( + self.operator_cloud.load_balancer.wait_for_load_balancer( self.LB_ID, wait=self._wait_for_timeout ) - self.conn.load_balancer.delete_member( + self.operator_cloud.load_balancer.delete_member( self.MEMBER_ID, self.POOL_ID, ignore_missing=False ) - self.conn.load_balancer.wait_for_load_balancer( + self.operator_cloud.load_balancer.wait_for_load_balancer( self.LB_ID, wait=self._wait_for_timeout ) - self.conn.load_balancer.delete_pool(self.POOL_ID, ignore_missing=False) - self.conn.load_balancer.wait_for_load_balancer( + self.operator_cloud.load_balancer.delete_pool( + self.POOL_ID, ignore_missing=False + ) + self.operator_cloud.load_balancer.wait_for_load_balancer( self.LB_ID, wait=self._wait_for_timeout ) - self.conn.load_balancer.delete_listener( + self.operator_cloud.load_balancer.delete_listener( self.LISTENER_ID, ignore_missing=False ) - self.conn.load_balancer.wait_for_load_balancer( + self.operator_cloud.load_balancer.wait_for_load_balancer( self.LB_ID, wait=self._wait_for_timeout ) - self.conn.load_balancer.delete_load_balancer( + self.operator_cloud.load_balancer.delete_load_balancer( self.LB_ID, ignore_missing=False ) super().tearDown() - self.conn.load_balancer.delete_flavor( + self.operator_cloud.load_balancer.delete_flavor( self.FLAVOR_ID, ignore_missing=False ) - self.conn.load_balancer.delete_flavor_profile( + self.operator_cloud.load_balancer.delete_flavor_profile( self.FLAVOR_PROFILE_ID, ignore_missing=False ) - self.conn.load_balancer.delete_availability_zone( + self.operator_cloud.load_balancer.delete_availability_zone( self.AVAILABILITY_ZONE_NAME, ignore_missing=False ) - self.conn.load_balancer.delete_availability_zone_profile( + self.operator_cloud.load_balancer.delete_availability_zone_profile( self.AVAILABILITY_ZONE_PROFILE_ID, ignore_missing=False ) def test_lb_find(self): - test_lb = self.conn.load_balancer.find_load_balancer(self.LB_NAME) + test_lb = self.operator_cloud.load_balancer.find_load_balancer( + self.LB_NAME + ) self.assertEqual(self.LB_ID, test_lb.id) def test_lb_get(self): - test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) + test_lb = self.operator_cloud.load_balancer.get_load_balancer( + self.LB_ID + ) self.assertEqual(self.LB_NAME, test_lb.name) self.assertEqual(self.LB_ID, test_lb.id) self.assertEqual(self.VIP_SUBNET_ID, test_lb.vip_subnet_id) def test_lb_get_stats(self): - test_lb_stats = self.conn.load_balancer.get_load_balancer_statistics( - self.LB_ID + test_lb_stats = ( + self.operator_cloud.load_balancer.get_load_balancer_statistics( + self.LB_ID + ) ) self.assertEqual(0, test_lb_stats.active_connections) self.assertEqual(0, test_lb_stats.bytes_in) @@ -329,52 +341,65 @@ def test_lb_get_stats(self): self.assertEqual(0, test_lb_stats.total_connections) def test_lb_list(self): - names = [lb.name for lb in self.conn.load_balancer.load_balancers()] + names = [ + lb.name + for lb in self.operator_cloud.load_balancer.load_balancers() + ] self.assertIn(self.LB_NAME, names) def test_lb_update(self): - self.conn.load_balancer.update_load_balancer( + self.operator_cloud.load_balancer.update_load_balancer( self.LB_ID, name=self.UPDATE_NAME ) - self.conn.load_balancer.wait_for_load_balancer( + self.operator_cloud.load_balancer.wait_for_load_balancer( self.LB_ID, wait=self._wait_for_timeout ) - test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) + test_lb = self.operator_cloud.load_balancer.get_load_balancer( + self.LB_ID + ) self.assertEqual(self.UPDATE_NAME, test_lb.name) - self.conn.load_balancer.update_load_balancer( + self.operator_cloud.load_balancer.update_load_balancer( self.LB_ID, name=self.LB_NAME ) - self.conn.load_balancer.wait_for_load_balancer( + self.operator_cloud.load_balancer.wait_for_load_balancer( self.LB_ID, wait=self._wait_for_timeout ) - test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) + test_lb = self.operator_cloud.load_balancer.get_load_balancer( + self.LB_ID + ) self.assertEqual(self.LB_NAME, test_lb.name) def test_lb_failover(self): - self.conn.load_balancer.failover_load_balancer(self.LB_ID) - self.conn.load_balancer.wait_for_load_balancer( + self.operator_cloud.load_balancer.failover_load_balancer(self.LB_ID) + self.operator_cloud.load_balancer.wait_for_load_balancer( self.LB_ID, wait=self._wait_for_timeout ) - test_lb = self.conn.load_balancer.get_load_balancer(self.LB_ID) + test_lb = self.operator_cloud.load_balancer.get_load_balancer( + self.LB_ID + ) self.assertEqual(self.LB_NAME, test_lb.name) def test_listener_find(self): - test_listener = self.conn.load_balancer.find_listener( + test_listener = self.operator_cloud.load_balancer.find_listener( self.LISTENER_NAME ) self.assertEqual(self.LISTENER_ID, test_listener.id) def test_listener_get(self): - test_listener = self.conn.load_balancer.get_listener(self.LISTENER_ID) + test_listener = self.operator_cloud.load_balancer.get_listener( + self.LISTENER_ID + ) self.assertEqual(self.LISTENER_NAME, test_listener.name) self.assertEqual(self.LISTENER_ID, test_listener.id) self.assertEqual(self.PROTOCOL, test_listener.protocol) self.assertEqual(self.PROTOCOL_PORT, test_listener.protocol_port) def test_listener_get_stats(self): - test_listener_stats = self.conn.load_balancer.get_listener_statistics( - self.LISTENER_ID + test_listener_stats = ( + self.operator_cloud.load_balancer.get_listener_statistics( + self.LISTENER_ID + ) ) self.assertEqual(0, test_listener_stats.active_connections) self.assertEqual(0, test_listener_stats.bytes_in) @@ -383,71 +408,81 @@ def test_listener_get_stats(self): self.assertEqual(0, test_listener_stats.total_connections) def test_listener_list(self): - names = [ls.name for ls in self.conn.load_balancer.listeners()] + names = [ + ls.name for ls in self.operator_cloud.load_balancer.listeners() + ] self.assertIn(self.LISTENER_NAME, names) def test_listener_update(self): - self.conn.load_balancer.get_load_balancer(self.LB_ID) + self.operator_cloud.load_balancer.get_load_balancer(self.LB_ID) - self.conn.load_balancer.update_listener( + self.operator_cloud.load_balancer.update_listener( self.LISTENER_ID, name=self.UPDATE_NAME ) - self.conn.load_balancer.wait_for_load_balancer( + self.operator_cloud.load_balancer.wait_for_load_balancer( self.LB_ID, wait=self._wait_for_timeout ) - test_listener = self.conn.load_balancer.get_listener(self.LISTENER_ID) + test_listener = self.operator_cloud.load_balancer.get_listener( + self.LISTENER_ID + ) self.assertEqual(self.UPDATE_NAME, test_listener.name) - self.conn.load_balancer.update_listener( + self.operator_cloud.load_balancer.update_listener( self.LISTENER_ID, name=self.LISTENER_NAME ) - self.conn.load_balancer.wait_for_load_balancer( + self.operator_cloud.load_balancer.wait_for_load_balancer( self.LB_ID, wait=self._wait_for_timeout ) - test_listener = self.conn.load_balancer.get_listener(self.LISTENER_ID) + test_listener = self.operator_cloud.load_balancer.get_listener( + self.LISTENER_ID + ) self.assertEqual(self.LISTENER_NAME, test_listener.name) def test_pool_find(self): - test_pool = self.conn.load_balancer.find_pool(self.POOL_NAME) + test_pool = self.operator_cloud.load_balancer.find_pool(self.POOL_NAME) self.assertEqual(self.POOL_ID, test_pool.id) def test_pool_get(self): - test_pool = self.conn.load_balancer.get_pool(self.POOL_ID) + test_pool = self.operator_cloud.load_balancer.get_pool(self.POOL_ID) self.assertEqual(self.POOL_NAME, test_pool.name) self.assertEqual(self.POOL_ID, test_pool.id) self.assertEqual(self.PROTOCOL, test_pool.protocol) def test_pool_list(self): - names = [pool.name for pool in self.conn.load_balancer.pools()] + names = [ + pool.name for pool in self.operator_cloud.load_balancer.pools() + ] self.assertIn(self.POOL_NAME, names) def test_pool_update(self): - self.conn.load_balancer.get_load_balancer(self.LB_ID) + self.operator_cloud.load_balancer.get_load_balancer(self.LB_ID) - self.conn.load_balancer.update_pool( + self.operator_cloud.load_balancer.update_pool( self.POOL_ID, name=self.UPDATE_NAME ) - self.conn.load_balancer.wait_for_load_balancer( + self.operator_cloud.load_balancer.wait_for_load_balancer( self.LB_ID, wait=self._wait_for_timeout ) - test_pool = self.conn.load_balancer.get_pool(self.POOL_ID) + test_pool = self.operator_cloud.load_balancer.get_pool(self.POOL_ID) self.assertEqual(self.UPDATE_NAME, test_pool.name) - self.conn.load_balancer.update_pool(self.POOL_ID, name=self.POOL_NAME) - self.conn.load_balancer.wait_for_load_balancer( + self.operator_cloud.load_balancer.update_pool( + self.POOL_ID, name=self.POOL_NAME + ) + self.operator_cloud.load_balancer.wait_for_load_balancer( self.LB_ID, wait=self._wait_for_timeout ) - test_pool = self.conn.load_balancer.get_pool(self.POOL_ID) + test_pool = self.operator_cloud.load_balancer.get_pool(self.POOL_ID) self.assertEqual(self.POOL_NAME, test_pool.name) def test_member_find(self): - test_member = self.conn.load_balancer.find_member( + test_member = self.operator_cloud.load_balancer.find_member( self.MEMBER_NAME, self.POOL_ID ) self.assertEqual(self.MEMBER_ID, test_member.id) def test_member_get(self): - test_member = self.conn.load_balancer.get_member( + test_member = self.operator_cloud.load_balancer.get_member( self.MEMBER_ID, self.POOL_ID ) self.assertEqual(self.MEMBER_NAME, test_member.name) @@ -458,41 +493,46 @@ def test_member_get(self): def test_member_list(self): names = [ - mb.name for mb in self.conn.load_balancer.members(self.POOL_ID) + mb.name + for mb in self.operator_cloud.load_balancer.members(self.POOL_ID) ] self.assertIn(self.MEMBER_NAME, names) def test_member_update(self): - self.conn.load_balancer.get_load_balancer(self.LB_ID) + self.operator_cloud.load_balancer.get_load_balancer(self.LB_ID) - self.conn.load_balancer.update_member( + self.operator_cloud.load_balancer.update_member( self.MEMBER_ID, self.POOL_ID, name=self.UPDATE_NAME ) - self.conn.load_balancer.wait_for_load_balancer( + self.operator_cloud.load_balancer.wait_for_load_balancer( self.LB_ID, wait=self._wait_for_timeout ) - test_member = self.conn.load_balancer.get_member( + test_member = self.operator_cloud.load_balancer.get_member( self.MEMBER_ID, self.POOL_ID ) self.assertEqual(self.UPDATE_NAME, test_member.name) - self.conn.load_balancer.update_member( + self.operator_cloud.load_balancer.update_member( self.MEMBER_ID, self.POOL_ID, name=self.MEMBER_NAME ) - self.conn.load_balancer.wait_for_load_balancer( + self.operator_cloud.load_balancer.wait_for_load_balancer( self.LB_ID, wait=self._wait_for_timeout ) - test_member = self.conn.load_balancer.get_member( + test_member = self.operator_cloud.load_balancer.get_member( self.MEMBER_ID, self.POOL_ID ) self.assertEqual(self.MEMBER_NAME, test_member.name) def test_health_monitor_find(self): - test_hm = self.conn.load_balancer.find_health_monitor(self.HM_NAME) + test_hm = self.operator_cloud.load_balancer.find_health_monitor( + self.HM_NAME + ) self.assertEqual(self.HM_ID, test_hm.id) def test_health_monitor_get(self): - test_hm = self.conn.load_balancer.get_health_monitor(self.HM_ID) + test_hm = self.operator_cloud.load_balancer.get_health_monitor( + self.HM_ID + ) self.assertEqual(self.HM_NAME, test_hm.name) self.assertEqual(self.HM_ID, test_hm.id) self.assertEqual(self.DELAY, test_hm.delay) @@ -501,38 +541,45 @@ def test_health_monitor_get(self): self.assertEqual(self.HM_TYPE, test_hm.type) def test_health_monitor_list(self): - names = [hm.name for hm in self.conn.load_balancer.health_monitors()] + names = [ + hm.name + for hm in self.operator_cloud.load_balancer.health_monitors() + ] self.assertIn(self.HM_NAME, names) def test_health_monitor_update(self): - self.conn.load_balancer.get_load_balancer(self.LB_ID) + self.operator_cloud.load_balancer.get_load_balancer(self.LB_ID) - self.conn.load_balancer.update_health_monitor( + self.operator_cloud.load_balancer.update_health_monitor( self.HM_ID, name=self.UPDATE_NAME ) - self.conn.load_balancer.wait_for_load_balancer( + self.operator_cloud.load_balancer.wait_for_load_balancer( self.LB_ID, wait=self._wait_for_timeout ) - test_hm = self.conn.load_balancer.get_health_monitor(self.HM_ID) + test_hm = self.operator_cloud.load_balancer.get_health_monitor( + self.HM_ID + ) self.assertEqual(self.UPDATE_NAME, test_hm.name) - self.conn.load_balancer.update_health_monitor( + self.operator_cloud.load_balancer.update_health_monitor( self.HM_ID, name=self.HM_NAME ) - self.conn.load_balancer.wait_for_load_balancer( + self.operator_cloud.load_balancer.wait_for_load_balancer( self.LB_ID, wait=self._wait_for_timeout ) - test_hm = self.conn.load_balancer.get_health_monitor(self.HM_ID) + test_hm = self.operator_cloud.load_balancer.get_health_monitor( + self.HM_ID + ) self.assertEqual(self.HM_NAME, test_hm.name) def test_l7_policy_find(self): - test_l7_policy = self.conn.load_balancer.find_l7_policy( + test_l7_policy = self.operator_cloud.load_balancer.find_l7_policy( self.L7POLICY_NAME ) self.assertEqual(self.L7POLICY_ID, test_l7_policy.id) def test_l7_policy_get(self): - test_l7_policy = self.conn.load_balancer.get_l7_policy( + test_l7_policy = self.operator_cloud.load_balancer.get_l7_policy( self.L7POLICY_ID ) self.assertEqual(self.L7POLICY_NAME, test_l7_policy.name) @@ -540,43 +587,45 @@ def test_l7_policy_get(self): self.assertEqual(self.ACTION, test_l7_policy.action) def test_l7_policy_list(self): - names = [l7.name for l7 in self.conn.load_balancer.l7_policies()] + names = [ + l7.name for l7 in self.operator_cloud.load_balancer.l7_policies() + ] self.assertIn(self.L7POLICY_NAME, names) def test_l7_policy_update(self): - self.conn.load_balancer.get_load_balancer(self.LB_ID) + self.operator_cloud.load_balancer.get_load_balancer(self.LB_ID) - self.conn.load_balancer.update_l7_policy( + self.operator_cloud.load_balancer.update_l7_policy( self.L7POLICY_ID, name=self.UPDATE_NAME ) - self.conn.load_balancer.wait_for_load_balancer( + self.operator_cloud.load_balancer.wait_for_load_balancer( self.LB_ID, wait=self._wait_for_timeout ) - test_l7_policy = self.conn.load_balancer.get_l7_policy( + test_l7_policy = self.operator_cloud.load_balancer.get_l7_policy( self.L7POLICY_ID ) self.assertEqual(self.UPDATE_NAME, test_l7_policy.name) - self.conn.load_balancer.update_l7_policy( + self.operator_cloud.load_balancer.update_l7_policy( self.L7POLICY_ID, name=self.L7POLICY_NAME ) - self.conn.load_balancer.wait_for_load_balancer( + self.operator_cloud.load_balancer.wait_for_load_balancer( self.LB_ID, wait=self._wait_for_timeout ) - test_l7_policy = self.conn.load_balancer.get_l7_policy( + test_l7_policy = self.operator_cloud.load_balancer.get_l7_policy( self.L7POLICY_ID ) self.assertEqual(self.L7POLICY_NAME, test_l7_policy.name) def test_l7_rule_find(self): - test_l7_rule = self.conn.load_balancer.find_l7_rule( + test_l7_rule = self.operator_cloud.load_balancer.find_l7_rule( self.L7RULE_ID, self.L7POLICY_ID ) self.assertEqual(self.L7RULE_ID, test_l7_rule.id) self.assertEqual(self.L7RULE_TYPE, test_l7_rule.type) def test_l7_rule_get(self): - test_l7_rule = self.conn.load_balancer.get_l7_rule( + test_l7_rule = self.operator_cloud.load_balancer.get_l7_rule( self.L7RULE_ID, l7_policy=self.L7POLICY_ID ) self.assertEqual(self.L7RULE_ID, test_l7_rule.id) @@ -587,73 +636,79 @@ def test_l7_rule_get(self): def test_l7_rule_list(self): ids = [ l7.id - for l7 in self.conn.load_balancer.l7_rules( + for l7 in self.operator_cloud.load_balancer.l7_rules( l7_policy=self.L7POLICY_ID ) ] self.assertIn(self.L7RULE_ID, ids) def test_l7_rule_update(self): - self.conn.load_balancer.get_load_balancer(self.LB_ID) + self.operator_cloud.load_balancer.get_load_balancer(self.LB_ID) - self.conn.load_balancer.update_l7_rule( + self.operator_cloud.load_balancer.update_l7_rule( self.L7RULE_ID, l7_policy=self.L7POLICY_ID, rule_value=self.UPDATE_NAME, ) - self.conn.load_balancer.wait_for_load_balancer( + self.operator_cloud.load_balancer.wait_for_load_balancer( self.LB_ID, wait=self._wait_for_timeout ) - test_l7_rule = self.conn.load_balancer.get_l7_rule( + test_l7_rule = self.operator_cloud.load_balancer.get_l7_rule( self.L7RULE_ID, l7_policy=self.L7POLICY_ID ) self.assertEqual(self.UPDATE_NAME, test_l7_rule.rule_value) - self.conn.load_balancer.update_l7_rule( + self.operator_cloud.load_balancer.update_l7_rule( self.L7RULE_ID, l7_policy=self.L7POLICY_ID, rule_value=self.L7RULE_VALUE, ) - self.conn.load_balancer.wait_for_load_balancer( + self.operator_cloud.load_balancer.wait_for_load_balancer( self.LB_ID, wait=self._wait_for_timeout ) - test_l7_rule = self.conn.load_balancer.get_l7_rule( + test_l7_rule = self.operator_cloud.load_balancer.get_l7_rule( self.L7RULE_ID, l7_policy=self.L7POLICY_ID, ) self.assertEqual(self.L7RULE_VALUE, test_l7_rule.rule_value) def test_quota_list(self): - for qot in self.conn.load_balancer.quotas(): + for qot in self.operator_cloud.load_balancer.quotas(): self.assertIsNotNone(qot.project_id) def test_quota_get(self): - test_quota = self.conn.load_balancer.get_quota(self.PROJECT_ID) + test_quota = self.operator_cloud.load_balancer.get_quota( + self.PROJECT_ID + ) self.assertEqual(self.PROJECT_ID, test_quota.id) def test_quota_update(self): attrs = {'load_balancer': 12345, 'pool': 67890} - for project_quota in self.conn.load_balancer.quotas(): - self.conn.load_balancer.update_quota(project_quota, **attrs) - new_quota = self.conn.load_balancer.get_quota( + for project_quota in self.operator_cloud.load_balancer.quotas(): + self.operator_cloud.load_balancer.update_quota( + project_quota, **attrs + ) + new_quota = self.operator_cloud.load_balancer.get_quota( project_quota.project_id ) self.assertEqual(12345, new_quota.load_balancers) self.assertEqual(67890, new_quota.pools) def test_default_quota(self): - self.conn.load_balancer.get_quota_default() + self.operator_cloud.load_balancer.get_quota_default() def test_providers(self): - providers = self.conn.load_balancer.providers() + providers = self.operator_cloud.load_balancer.providers() # Make sure our default provider is in the list self.assertTrue( any(prov['name'] == self.AMPHORA for prov in providers) ) def test_provider_flavor_capabilities(self): - capabilities = self.conn.load_balancer.provider_flavor_capabilities( - self.AMPHORA + capabilities = ( + self.operator_cloud.load_balancer.provider_flavor_capabilities( + self.AMPHORA + ) ) # Make sure a known capability is in the default provider self.assertTrue( @@ -661,14 +716,16 @@ def test_provider_flavor_capabilities(self): ) def test_flavor_profile_find(self): - test_profile = self.conn.load_balancer.find_flavor_profile( + test_profile = self.operator_cloud.load_balancer.find_flavor_profile( self.FLAVOR_PROFILE_NAME ) self.assertEqual(self.FLAVOR_PROFILE_ID, test_profile.id) def test_flavor_profile_get(self): - test_flavor_profile = self.conn.load_balancer.get_flavor_profile( - self.FLAVOR_PROFILE_ID + test_flavor_profile = ( + self.operator_cloud.load_balancer.get_flavor_profile( + self.FLAVOR_PROFILE_ID + ) ) self.assertEqual(self.FLAVOR_PROFILE_NAME, test_flavor_profile.name) self.assertEqual(self.FLAVOR_PROFILE_ID, test_flavor_profile.id) @@ -676,85 +733,112 @@ def test_flavor_profile_get(self): self.assertEqual(self.FLAVOR_DATA, test_flavor_profile.flavor_data) def test_flavor_profile_list(self): - names = [fv.name for fv in self.conn.load_balancer.flavor_profiles()] + names = [ + fv.name + for fv in self.operator_cloud.load_balancer.flavor_profiles() + ] self.assertIn(self.FLAVOR_PROFILE_NAME, names) def test_flavor_profile_update(self): - self.conn.load_balancer.update_flavor_profile( + self.operator_cloud.load_balancer.update_flavor_profile( self.FLAVOR_PROFILE_ID, name=self.UPDATE_NAME ) - test_flavor_profile = self.conn.load_balancer.get_flavor_profile( - self.FLAVOR_PROFILE_ID + test_flavor_profile = ( + self.operator_cloud.load_balancer.get_flavor_profile( + self.FLAVOR_PROFILE_ID + ) ) self.assertEqual(self.UPDATE_NAME, test_flavor_profile.name) - self.conn.load_balancer.update_flavor_profile( + self.operator_cloud.load_balancer.update_flavor_profile( self.FLAVOR_PROFILE_ID, name=self.FLAVOR_PROFILE_NAME ) - test_flavor_profile = self.conn.load_balancer.get_flavor_profile( - self.FLAVOR_PROFILE_ID + test_flavor_profile = ( + self.operator_cloud.load_balancer.get_flavor_profile( + self.FLAVOR_PROFILE_ID + ) ) self.assertEqual(self.FLAVOR_PROFILE_NAME, test_flavor_profile.name) def test_flavor_find(self): - test_flavor = self.conn.load_balancer.find_flavor(self.FLAVOR_NAME) + test_flavor = self.operator_cloud.load_balancer.find_flavor( + self.FLAVOR_NAME + ) self.assertEqual(self.FLAVOR_ID, test_flavor.id) def test_flavor_get(self): - test_flavor = self.conn.load_balancer.get_flavor(self.FLAVOR_ID) + test_flavor = self.operator_cloud.load_balancer.get_flavor( + self.FLAVOR_ID + ) self.assertEqual(self.FLAVOR_NAME, test_flavor.name) self.assertEqual(self.FLAVOR_ID, test_flavor.id) self.assertEqual(self.DESCRIPTION, test_flavor.description) self.assertEqual(self.FLAVOR_PROFILE_ID, test_flavor.flavor_profile_id) def test_flavor_list(self): - names = [fv.name for fv in self.conn.load_balancer.flavors()] + names = [fv.name for fv in self.operator_cloud.load_balancer.flavors()] self.assertIn(self.FLAVOR_NAME, names) def test_flavor_update(self): - self.conn.load_balancer.update_flavor( + self.operator_cloud.load_balancer.update_flavor( self.FLAVOR_ID, name=self.UPDATE_NAME ) - test_flavor = self.conn.load_balancer.get_flavor(self.FLAVOR_ID) + test_flavor = self.operator_cloud.load_balancer.get_flavor( + self.FLAVOR_ID + ) self.assertEqual(self.UPDATE_NAME, test_flavor.name) - self.conn.load_balancer.update_flavor( + self.operator_cloud.load_balancer.update_flavor( self.FLAVOR_ID, name=self.FLAVOR_NAME ) - test_flavor = self.conn.load_balancer.get_flavor(self.FLAVOR_ID) + test_flavor = self.operator_cloud.load_balancer.get_flavor( + self.FLAVOR_ID + ) self.assertEqual(self.FLAVOR_NAME, test_flavor.name) def test_amphora_list(self): - amp_ids = [amp.id for amp in self.conn.load_balancer.amphorae()] + amp_ids = [ + amp.id for amp in self.operator_cloud.load_balancer.amphorae() + ] self.assertIn(self.AMPHORA_ID, amp_ids) def test_amphora_find(self): - test_amphora = self.conn.load_balancer.find_amphora(self.AMPHORA_ID) + test_amphora = self.operator_cloud.load_balancer.find_amphora( + self.AMPHORA_ID + ) self.assertEqual(self.AMPHORA_ID, test_amphora.id) def test_amphora_get(self): - test_amphora = self.conn.load_balancer.get_amphora(self.AMPHORA_ID) + test_amphora = self.operator_cloud.load_balancer.get_amphora( + self.AMPHORA_ID + ) self.assertEqual(self.AMPHORA_ID, test_amphora.id) def test_amphora_configure(self): - self.conn.load_balancer.configure_amphora(self.AMPHORA_ID) - test_amp = self.conn.load_balancer.get_amphora(self.AMPHORA_ID) + self.operator_cloud.load_balancer.configure_amphora(self.AMPHORA_ID) + test_amp = self.operator_cloud.load_balancer.get_amphora( + self.AMPHORA_ID + ) self.assertEqual(self.AMPHORA_ID, test_amp.id) def test_amphora_failover(self): - self.conn.load_balancer.failover_amphora(self.AMPHORA_ID) - test_amp = self.conn.load_balancer.get_amphora(self.AMPHORA_ID) + self.operator_cloud.load_balancer.failover_amphora(self.AMPHORA_ID) + test_amp = self.operator_cloud.load_balancer.get_amphora( + self.AMPHORA_ID + ) self.assertEqual(self.AMPHORA_ID, test_amp.id) def test_availability_zone_profile_find(self): - test_profile = self.conn.load_balancer.find_availability_zone_profile( - self.AVAILABILITY_ZONE_PROFILE_NAME + test_profile = ( + self.operator_cloud.load_balancer.find_availability_zone_profile( + self.AVAILABILITY_ZONE_PROFILE_NAME + ) ) self.assertEqual(self.AVAILABILITY_ZONE_PROFILE_ID, test_profile.id) def test_availability_zone_profile_get(self): test_availability_zone_profile = ( - self.conn.load_balancer.get_availability_zone_profile( + self.operator_cloud.load_balancer.get_availability_zone_profile( self.AVAILABILITY_ZONE_PROFILE_ID ) ) @@ -777,27 +861,27 @@ def test_availability_zone_profile_get(self): def test_availability_zone_profile_list(self): names = [ az.name - for az in self.conn.load_balancer.availability_zone_profiles() + for az in self.operator_cloud.load_balancer.availability_zone_profiles() ] self.assertIn(self.AVAILABILITY_ZONE_PROFILE_NAME, names) def test_availability_zone_profile_update(self): - self.conn.load_balancer.update_availability_zone_profile( + self.operator_cloud.load_balancer.update_availability_zone_profile( self.AVAILABILITY_ZONE_PROFILE_ID, name=self.UPDATE_NAME ) test_availability_zone_profile = ( - self.conn.load_balancer.get_availability_zone_profile( + self.operator_cloud.load_balancer.get_availability_zone_profile( self.AVAILABILITY_ZONE_PROFILE_ID ) ) self.assertEqual(self.UPDATE_NAME, test_availability_zone_profile.name) - self.conn.load_balancer.update_availability_zone_profile( + self.operator_cloud.load_balancer.update_availability_zone_profile( self.AVAILABILITY_ZONE_PROFILE_ID, name=self.AVAILABILITY_ZONE_PROFILE_NAME, ) test_availability_zone_profile = ( - self.conn.load_balancer.get_availability_zone_profile( + self.operator_cloud.load_balancer.get_availability_zone_profile( self.AVAILABILITY_ZONE_PROFILE_ID ) ) @@ -808,7 +892,7 @@ def test_availability_zone_profile_update(self): def test_availability_zone_find(self): test_availability_zone = ( - self.conn.load_balancer.find_availability_zone( + self.operator_cloud.load_balancer.find_availability_zone( self.AVAILABILITY_ZONE_NAME ) ) @@ -817,8 +901,10 @@ def test_availability_zone_find(self): ) def test_availability_zone_get(self): - test_availability_zone = self.conn.load_balancer.get_availability_zone( - self.AVAILABILITY_ZONE_NAME + test_availability_zone = ( + self.operator_cloud.load_balancer.get_availability_zone( + self.AVAILABILITY_ZONE_NAME + ) ) self.assertEqual( self.AVAILABILITY_ZONE_NAME, test_availability_zone.name @@ -831,25 +917,30 @@ def test_availability_zone_get(self): def test_availability_zone_list(self): names = [ - az.name for az in self.conn.load_balancer.availability_zones() + az.name + for az in self.operator_cloud.load_balancer.availability_zones() ] self.assertIn(self.AVAILABILITY_ZONE_NAME, names) def test_availability_zone_update(self): - self.conn.load_balancer.update_availability_zone( + self.operator_cloud.load_balancer.update_availability_zone( self.AVAILABILITY_ZONE_NAME, description=self.UPDATE_DESCRIPTION ) - test_availability_zone = self.conn.load_balancer.get_availability_zone( - self.AVAILABILITY_ZONE_NAME + test_availability_zone = ( + self.operator_cloud.load_balancer.get_availability_zone( + self.AVAILABILITY_ZONE_NAME + ) ) self.assertEqual( self.UPDATE_DESCRIPTION, test_availability_zone.description ) - self.conn.load_balancer.update_availability_zone( + self.operator_cloud.load_balancer.update_availability_zone( self.AVAILABILITY_ZONE_NAME, description=self.DESCRIPTION ) - test_availability_zone = self.conn.load_balancer.get_availability_zone( - self.AVAILABILITY_ZONE_NAME + test_availability_zone = ( + self.operator_cloud.load_balancer.get_availability_zone( + self.AVAILABILITY_ZONE_NAME + ) ) self.assertEqual(self.DESCRIPTION, test_availability_zone.description) diff --git a/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py b/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py index fcda4ccb9..44f79ba25 100644 --- a/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py +++ b/openstack/tests/functional/network/v2/test_qos_dscp_marking_rule.py @@ -31,18 +31,18 @@ def setUp(self): self.skipTest("Operator cloud is required for this test") # Skip the tests if qos extension is not enabled. - if not self.conn.network.find_extension("qos"): + if not self.operator_cloud.network.find_extension("qos"): self.skipTest("Network qos extension disabled") self.QOS_POLICY_NAME = self.getUniqueString() self.RULE_ID = self.getUniqueString() - qos_policy = self.conn.network.create_qos_policy( + qos_policy = self.operator_cloud.network.create_qos_policy( description=self.QOS_POLICY_DESCRIPTION, name=self.QOS_POLICY_NAME, shared=self.QOS_IS_SHARED, ) self.QOS_POLICY_ID = qos_policy.id - qos_rule = self.conn.network.create_qos_dscp_marking_rule( + qos_rule = self.operator_cloud.network.create_qos_dscp_marking_rule( self.QOS_POLICY_ID, dscp_mark=self.RULE_DSCP_MARK, ) @@ -51,23 +51,25 @@ def setUp(self): self.RULE_ID = qos_rule.id def tearDown(self): - rule = self.conn.network.delete_qos_dscp_marking_rule( + rule = self.operator_cloud.network.delete_qos_dscp_marking_rule( self.RULE_ID, self.QOS_POLICY_ID ) - qos_policy = self.conn.network.delete_qos_policy(self.QOS_POLICY_ID) + qos_policy = self.operator_cloud.network.delete_qos_policy( + self.QOS_POLICY_ID + ) self.assertIsNone(rule) self.assertIsNone(qos_policy) super().tearDown() def test_find(self): - sot = self.conn.network.find_qos_dscp_marking_rule( + sot = self.operator_cloud.network.find_qos_dscp_marking_rule( self.RULE_ID, self.QOS_POLICY_ID ) self.assertEqual(self.RULE_ID, sot.id) self.assertEqual(self.RULE_DSCP_MARK, sot.dscp_mark) def test_get(self): - sot = self.conn.network.get_qos_dscp_marking_rule( + sot = self.operator_cloud.network.get_qos_dscp_marking_rule( self.RULE_ID, self.QOS_POLICY_ID ) self.assertEqual(self.RULE_ID, sot.id) @@ -77,14 +79,14 @@ def test_get(self): def test_list(self): rule_ids = [ o.id - for o in self.conn.network.qos_dscp_marking_rules( + for o in self.operator_cloud.network.qos_dscp_marking_rules( self.QOS_POLICY_ID ) ] self.assertIn(self.RULE_ID, rule_ids) def test_update(self): - sot = self.conn.network.update_qos_dscp_marking_rule( + sot = self.operator_cloud.network.update_qos_dscp_marking_rule( self.RULE_ID, self.QOS_POLICY_ID, dscp_mark=self.RULE_DSCP_MARK_NEW ) self.assertEqual(self.RULE_DSCP_MARK_NEW, sot.dscp_mark) diff --git a/openstack/tests/functional/object_store/v1/test_account.py b/openstack/tests/functional/object_store/v1/test_account.py index ac518bc2c..f855cad52 100644 --- a/openstack/tests/functional/object_store/v1/test_account.py +++ b/openstack/tests/functional/object_store/v1/test_account.py @@ -19,42 +19,44 @@ def setUp(self): self.require_service('object-store') def tearDown(self): - account = self.conn.object_store.get_account_metadata() - self.conn.object_store.delete_account_metadata(account.metadata.keys()) + account = self.operator_cloud.object_store.get_account_metadata() + self.operator_cloud.object_store.delete_account_metadata( + account.metadata.keys() + ) super().tearDown() def test_system_metadata(self): - account = self.conn.object_store.get_account_metadata() + account = self.operator_cloud.object_store.get_account_metadata() self.assertGreaterEqual(account.account_bytes_used, 0) self.assertGreaterEqual(account.account_container_count, 0) self.assertGreaterEqual(account.account_object_count, 0) def test_custom_metadata(self): # get custom metadata - account = self.conn.object_store.get_account_metadata() + account = self.operator_cloud.object_store.get_account_metadata() self.assertFalse(account.metadata) # set no custom metadata - self.conn.object_store.set_account_metadata() - account = self.conn.object_store.get_account_metadata() + self.operator_cloud.object_store.set_account_metadata() + account = self.operator_cloud.object_store.get_account_metadata() self.assertFalse(account.metadata) # set empty custom metadata - self.conn.object_store.set_account_metadata(k0='') - account = self.conn.object_store.get_account_metadata() + self.operator_cloud.object_store.set_account_metadata(k0='') + account = self.operator_cloud.object_store.get_account_metadata() self.assertFalse(account.metadata) # set custom metadata - self.conn.object_store.set_account_metadata(k1='v1') - account = self.conn.object_store.get_account_metadata() + self.operator_cloud.object_store.set_account_metadata(k1='v1') + account = self.operator_cloud.object_store.get_account_metadata() self.assertTrue(account.metadata) self.assertEqual(1, len(account.metadata)) self.assertIn('k1', account.metadata) self.assertEqual('v1', account.metadata['k1']) # set more custom metadata - self.conn.object_store.set_account_metadata(k2='v2') - account = self.conn.object_store.get_account_metadata() + self.operator_cloud.object_store.set_account_metadata(k2='v2') + account = self.operator_cloud.object_store.get_account_metadata() self.assertTrue(account.metadata) self.assertEqual(2, len(account.metadata)) self.assertIn('k1', account.metadata) @@ -63,8 +65,8 @@ def test_custom_metadata(self): self.assertEqual('v2', account.metadata['k2']) # update custom metadata - self.conn.object_store.set_account_metadata(k1='v1.1') - account = self.conn.object_store.get_account_metadata() + self.operator_cloud.object_store.set_account_metadata(k1='v1.1') + account = self.operator_cloud.object_store.get_account_metadata() self.assertTrue(account.metadata) self.assertEqual(2, len(account.metadata)) self.assertIn('k1', account.metadata) @@ -73,8 +75,8 @@ def test_custom_metadata(self): self.assertEqual('v2', account.metadata['k2']) # unset custom metadata - self.conn.object_store.delete_account_metadata(['k1']) - account = self.conn.object_store.get_account_metadata() + self.operator_cloud.object_store.delete_account_metadata(['k1']) + account = self.operator_cloud.object_store.get_account_metadata() self.assertTrue(account.metadata) self.assertEqual(1, len(account.metadata)) self.assertIn('k2', account.metadata) diff --git a/openstack/tests/functional/object_store/v1/test_container.py b/openstack/tests/functional/object_store/v1/test_container.py index 00b81bb1d..da1d3d6b4 100644 --- a/openstack/tests/functional/object_store/v1/test_container.py +++ b/openstack/tests/functional/object_store/v1/test_container.py @@ -20,9 +20,11 @@ def setUp(self): self.require_service('object-store') self.NAME = self.getUniqueString() - container = self.conn.object_store.create_container(name=self.NAME) + container = self.operator_cloud.object_store.create_container( + name=self.NAME + ) self.addEmptyCleanup( - self.conn.object_store.delete_container, + self.operator_cloud.object_store.delete_container, self.NAME, ignore_missing=False, ) @@ -30,39 +32,49 @@ def setUp(self): self.assertEqual(self.NAME, container.name) def test_list(self): - names = [o.name for o in self.conn.object_store.containers()] + names = [o.name for o in self.operator_cloud.object_store.containers()] self.assertIn(self.NAME, names) def test_system_metadata(self): # get system metadata - container = self.conn.object_store.get_container_metadata(self.NAME) + container = self.operator_cloud.object_store.get_container_metadata( + self.NAME + ) self.assertEqual(0, container.object_count) self.assertEqual(0, container.bytes_used) # set system metadata - container = self.conn.object_store.get_container_metadata(self.NAME) + container = self.operator_cloud.object_store.get_container_metadata( + self.NAME + ) self.assertIsNone(container.read_ACL) self.assertIsNone(container.write_ACL) - self.conn.object_store.set_container_metadata( + self.operator_cloud.object_store.set_container_metadata( container, read_ACL='.r:*', write_ACL='demo:demo' ) - container = self.conn.object_store.get_container_metadata(self.NAME) + container = self.operator_cloud.object_store.get_container_metadata( + self.NAME + ) self.assertEqual('.r:*', container.read_ACL) self.assertEqual('demo:demo', container.write_ACL) # update system metadata - self.conn.object_store.set_container_metadata( + self.operator_cloud.object_store.set_container_metadata( container, read_ACL='.r:demo' ) - container = self.conn.object_store.get_container_metadata(self.NAME) + container = self.operator_cloud.object_store.get_container_metadata( + self.NAME + ) self.assertEqual('.r:demo', container.read_ACL) self.assertEqual('demo:demo', container.write_ACL) # set system metadata and custom metadata - self.conn.object_store.set_container_metadata( + self.operator_cloud.object_store.set_container_metadata( container, k0='v0', sync_key='1234' ) - container = self.conn.object_store.get_container_metadata(self.NAME) + container = self.operator_cloud.object_store.get_container_metadata( + self.NAME + ) self.assertTrue(container.metadata) self.assertIn('k0', container.metadata) self.assertEqual('v0', container.metadata['k0']) @@ -71,10 +83,12 @@ def test_system_metadata(self): self.assertEqual('1234', container.sync_key) # unset system metadata - self.conn.object_store.delete_container_metadata( + self.operator_cloud.object_store.delete_container_metadata( container, ['sync_key'] ) - container = self.conn.object_store.get_container_metadata(self.NAME) + container = self.operator_cloud.object_store.get_container_metadata( + self.NAME + ) self.assertTrue(container.metadata) self.assertIn('k0', container.metadata) self.assertEqual('v0', container.metadata['k0']) @@ -84,30 +98,46 @@ def test_system_metadata(self): def test_custom_metadata(self): # get custom metadata - container = self.conn.object_store.get_container_metadata(self.NAME) + container = self.operator_cloud.object_store.get_container_metadata( + self.NAME + ) self.assertFalse(container.metadata) # set no custom metadata - self.conn.object_store.set_container_metadata(container) - container = self.conn.object_store.get_container_metadata(container) + self.operator_cloud.object_store.set_container_metadata(container) + container = self.operator_cloud.object_store.get_container_metadata( + container + ) self.assertFalse(container.metadata) # set empty custom metadata - self.conn.object_store.set_container_metadata(container, k0='') - container = self.conn.object_store.get_container_metadata(container) + self.operator_cloud.object_store.set_container_metadata( + container, k0='' + ) + container = self.operator_cloud.object_store.get_container_metadata( + container + ) self.assertFalse(container.metadata) # set custom metadata - self.conn.object_store.set_container_metadata(container, k1='v1') - container = self.conn.object_store.get_container_metadata(container) + self.operator_cloud.object_store.set_container_metadata( + container, k1='v1' + ) + container = self.operator_cloud.object_store.get_container_metadata( + container + ) self.assertTrue(container.metadata) self.assertEqual(1, len(container.metadata)) self.assertIn('k1', container.metadata) self.assertEqual('v1', container.metadata['k1']) # set more custom metadata by named container - self.conn.object_store.set_container_metadata(self.NAME, k2='v2') - container = self.conn.object_store.get_container_metadata(container) + self.operator_cloud.object_store.set_container_metadata( + self.NAME, k2='v2' + ) + container = self.operator_cloud.object_store.get_container_metadata( + container + ) self.assertTrue(container.metadata) self.assertEqual(2, len(container.metadata)) self.assertIn('k1', container.metadata) @@ -116,8 +146,12 @@ def test_custom_metadata(self): self.assertEqual('v2', container.metadata['k2']) # update metadata - self.conn.object_store.set_container_metadata(container, k1='v1.1') - container = self.conn.object_store.get_container_metadata(self.NAME) + self.operator_cloud.object_store.set_container_metadata( + container, k1='v1.1' + ) + container = self.operator_cloud.object_store.get_container_metadata( + self.NAME + ) self.assertTrue(container.metadata) self.assertEqual(2, len(container.metadata)) self.assertIn('k1', container.metadata) @@ -126,8 +160,12 @@ def test_custom_metadata(self): self.assertEqual('v2', container.metadata['k2']) # delete metadata - self.conn.object_store.delete_container_metadata(container, ['k1']) - container = self.conn.object_store.get_container_metadata(self.NAME) + self.operator_cloud.object_store.delete_container_metadata( + container, ['k1'] + ) + container = self.operator_cloud.object_store.get_container_metadata( + self.NAME + ) self.assertTrue(container.metadata) self.assertEqual(1, len(container.metadata)) self.assertIn('k2', container.metadata) diff --git a/openstack/tests/functional/object_store/v1/test_obj.py b/openstack/tests/functional/object_store/v1/test_obj.py index b74f45154..af43a8d10 100644 --- a/openstack/tests/functional/object_store/v1/test_obj.py +++ b/openstack/tests/functional/object_store/v1/test_obj.py @@ -22,13 +22,15 @@ def setUp(self): self.FOLDER = self.getUniqueString() self.FILE = self.getUniqueString() - self.conn.object_store.create_container(name=self.FOLDER) - self.addCleanup(self.conn.object_store.delete_container, self.FOLDER) - self.sot = self.conn.object_store.upload_object( + self.operator_cloud.object_store.create_container(name=self.FOLDER) + self.addCleanup( + self.operator_cloud.object_store.delete_container, self.FOLDER + ) + self.sot = self.operator_cloud.object_store.upload_object( container=self.FOLDER, name=self.FILE, data=self.DATA ) self.addEmptyCleanup( - self.conn.object_store.delete_object, + self.operator_cloud.object_store.delete_object, self.sot, ignore_missing=False, ) @@ -36,21 +38,23 @@ def setUp(self): def test_list(self): names = [ o.name - for o in self.conn.object_store.objects(container=self.FOLDER) + for o in self.operator_cloud.object_store.objects( + container=self.FOLDER + ) ] self.assertIn(self.FILE, names) def test_download_object(self): - result = self.conn.object_store.download_object( + result = self.operator_cloud.object_store.download_object( self.FILE, container=self.FOLDER ) self.assertEqual(self.DATA, result) - result = self.conn.object_store.download_object(self.sot) + result = self.operator_cloud.object_store.download_object(self.sot) self.assertEqual(self.DATA, result) def test_system_metadata(self): # get system metadata - obj = self.conn.object_store.get_object_metadata( + obj = self.operator_cloud.object_store.get_object_metadata( self.FILE, container=self.FOLDER ) # TODO(shade) obj.bytes is coming up None on python3 but not python2 @@ -58,39 +62,39 @@ def test_system_metadata(self): self.assertIsNotNone(obj.etag) # set system metadata - obj = self.conn.object_store.get_object_metadata( + obj = self.operator_cloud.object_store.get_object_metadata( self.FILE, container=self.FOLDER ) self.assertIsNone(obj.content_disposition) self.assertIsNone(obj.content_encoding) - self.conn.object_store.set_object_metadata( + self.operator_cloud.object_store.set_object_metadata( obj, content_disposition='attachment', content_encoding='gzip' ) - obj = self.conn.object_store.get_object_metadata(obj) + obj = self.operator_cloud.object_store.get_object_metadata(obj) self.assertEqual('attachment', obj.content_disposition) self.assertEqual('gzip', obj.content_encoding) # update system metadata - self.conn.object_store.set_object_metadata( + self.operator_cloud.object_store.set_object_metadata( obj, content_encoding='deflate' ) - obj = self.conn.object_store.get_object_metadata(obj) + obj = self.operator_cloud.object_store.get_object_metadata(obj) self.assertEqual('attachment', obj.content_disposition) self.assertEqual('deflate', obj.content_encoding) # set custom metadata - self.conn.object_store.set_object_metadata(obj, k0='v0') - obj = self.conn.object_store.get_object_metadata(obj) + self.operator_cloud.object_store.set_object_metadata(obj, k0='v0') + obj = self.operator_cloud.object_store.get_object_metadata(obj) self.assertIn('k0', obj.metadata) self.assertEqual('v0', obj.metadata['k0']) self.assertEqual('attachment', obj.content_disposition) self.assertEqual('deflate', obj.content_encoding) # unset more system metadata - self.conn.object_store.delete_object_metadata( + self.operator_cloud.object_store.delete_object_metadata( obj, keys=['content_disposition'] ) - obj = self.conn.object_store.get_object_metadata(obj) + obj = self.operator_cloud.object_store.get_object_metadata(obj) self.assertIn('k0', obj.metadata) self.assertEqual('v0', obj.metadata['k0']) self.assertIsNone(obj.content_disposition) @@ -99,34 +103,34 @@ def test_system_metadata(self): def test_custom_metadata(self): # get custom metadata - obj = self.conn.object_store.get_object_metadata( + obj = self.operator_cloud.object_store.get_object_metadata( self.FILE, container=self.FOLDER ) self.assertFalse(obj.metadata) # set no custom metadata - self.conn.object_store.set_object_metadata(obj) - obj = self.conn.object_store.get_object_metadata(obj) + self.operator_cloud.object_store.set_object_metadata(obj) + obj = self.operator_cloud.object_store.get_object_metadata(obj) self.assertFalse(obj.metadata) # set empty custom metadata - self.conn.object_store.set_object_metadata(obj, k0='') - obj = self.conn.object_store.get_object_metadata(obj) + self.operator_cloud.object_store.set_object_metadata(obj, k0='') + obj = self.operator_cloud.object_store.get_object_metadata(obj) self.assertFalse(obj.metadata) # set custom metadata - self.conn.object_store.set_object_metadata(obj, k1='v1') - obj = self.conn.object_store.get_object_metadata(obj) + self.operator_cloud.object_store.set_object_metadata(obj, k1='v1') + obj = self.operator_cloud.object_store.get_object_metadata(obj) self.assertTrue(obj.metadata) self.assertEqual(1, len(obj.metadata)) self.assertIn('k1', obj.metadata) self.assertEqual('v1', obj.metadata['k1']) # set more custom metadata by named object and container - self.conn.object_store.set_object_metadata( + self.operator_cloud.object_store.set_object_metadata( self.FILE, self.FOLDER, k2='v2' ) - obj = self.conn.object_store.get_object_metadata(obj) + obj = self.operator_cloud.object_store.get_object_metadata(obj) self.assertTrue(obj.metadata) self.assertEqual(2, len(obj.metadata)) self.assertIn('k1', obj.metadata) @@ -135,8 +139,8 @@ def test_custom_metadata(self): self.assertEqual('v2', obj.metadata['k2']) # update custom metadata - self.conn.object_store.set_object_metadata(obj, k1='v1.1') - obj = self.conn.object_store.get_object_metadata(obj) + self.operator_cloud.object_store.set_object_metadata(obj, k1='v1.1') + obj = self.operator_cloud.object_store.get_object_metadata(obj) self.assertTrue(obj.metadata) self.assertEqual(2, len(obj.metadata)) self.assertIn('k1', obj.metadata) @@ -145,8 +149,10 @@ def test_custom_metadata(self): self.assertEqual('v2', obj.metadata['k2']) # unset custom metadata - self.conn.object_store.delete_object_metadata(obj, keys=['k1']) - obj = self.conn.object_store.get_object_metadata(obj) + self.operator_cloud.object_store.delete_object_metadata( + obj, keys=['k1'] + ) + obj = self.operator_cloud.object_store.get_object_metadata(obj) self.assertTrue(obj.metadata) self.assertEqual(1, len(obj.metadata)) self.assertIn('k2', obj.metadata) diff --git a/openstack/tests/functional/orchestration/v1/test_stack.py b/openstack/tests/functional/orchestration/v1/test_stack.py index 91b5c489e..285ded783 100644 --- a/openstack/tests/functional/orchestration/v1/test_stack.py +++ b/openstack/tests/functional/orchestration/v1/test_stack.py @@ -31,9 +31,9 @@ def setUp(self): super().setUp() self.require_service('orchestration') - if self.conn.compute.find_keypair(self.NAME) is None: - self.conn.compute.create_keypair(name=self.NAME) - image = next(self.conn.image.images()) + if self.operator_cloud.compute.find_keypair(self.NAME) is None: + self.operator_cloud.compute.create_keypair(name=self.NAME) + image = next(self.operator_cloud.image.images()) tname = "openstack/tests/functional/orchestration/v1/hello_world.yaml" with open(tname) as f: template = yaml.safe_load(f) @@ -41,14 +41,14 @@ def setUp(self): # the shade layer. template['heat_template_version'] = '2013-05-23' self.network, self.subnet = test_network.create_network( - self.conn, self.NAME, self.cidr + self.operator_cloud, self.NAME, self.cidr ) parameters = { 'image': image.id, 'key_name': self.NAME, 'network': self.network.id, } - sot = self.conn.orchestration.create_stack( + sot = self.operator_cloud.orchestration.create_stack( name=self.NAME, parameters=parameters, template=template, @@ -57,7 +57,7 @@ def setUp(self): self.assertEqual(True, (sot.id is not None)) self.stack = sot self.assertEqual(self.NAME, sot.name) - self.conn.orchestration.wait_for_status( + self.operator_cloud.orchestration.wait_for_status( sot, status='CREATE_COMPLETE', failures=['CREATE_FAILED'], @@ -65,20 +65,24 @@ def setUp(self): ) def tearDown(self): - self.conn.orchestration.delete_stack(self.stack, ignore_missing=False) - self.conn.compute.delete_keypair(self.NAME) + self.operator_cloud.orchestration.delete_stack( + self.stack, ignore_missing=False + ) + self.operator_cloud.compute.delete_keypair(self.NAME) # Need to wait for the stack to go away before network delete try: - self.conn.orchestration.wait_for_status( + self.operator_cloud.orchestration.wait_for_status( self.stack, 'DELETE_COMPLETE', wait=self._wait_for_timeout ) except exceptions.NotFoundException: pass - test_network.delete_network(self.conn, self.network, self.subnet) + test_network.delete_network( + self.operator_cloud, self.network, self.subnet + ) super().tearDown() def test_list(self): - names = [o.name for o in self.conn.orchestration.stacks()] + names = [o.name for o in self.operator_cloud.orchestration.stacks()] self.assertIn(self.NAME, names) def test_suspend_resume(self): @@ -87,8 +91,8 @@ def test_suspend_resume(self): resume_status = "RESUME_COMPLETE" # when - self.conn.orchestration.suspend_stack(self.stack) - sot = self.conn.orchestration.wait_for_status( + self.operator_cloud.orchestration.suspend_stack(self.stack) + sot = self.operator_cloud.orchestration.wait_for_status( self.stack, suspend_status, wait=self._wait_for_timeout ) @@ -96,8 +100,8 @@ def test_suspend_resume(self): self.assertEqual(suspend_status, sot.status) # when - self.conn.orchestration.resume_stack(self.stack) - sot = self.conn.orchestration.wait_for_status( + self.operator_cloud.orchestration.resume_stack(self.stack) + sot = self.operator_cloud.orchestration.wait_for_status( self.stack, resume_status, wait=self._wait_for_timeout ) diff --git a/openstack/tests/functional/placement/v1/test_resource_provider.py b/openstack/tests/functional/placement/v1/test_resource_provider.py index 516182242..35b7e9d24 100644 --- a/openstack/tests/functional/placement/v1/test_resource_provider.py +++ b/openstack/tests/functional/placement/v1/test_resource_provider.py @@ -37,7 +37,7 @@ def setUp(self): self.resource_provider = resource_provider def tearDown(self): - result = self.conn.placement.delete_resource_provider( + result = self.operator_cloud.placement.delete_resource_provider( self.resource_provider, ) self.assertIsNone(result) diff --git a/openstack/tests/functional/shared_file_system/base.py b/openstack/tests/functional/shared_file_system/base.py index 3a5e2d9bf..c8e235bb1 100644 --- a/openstack/tests/functional/shared_file_system/base.py +++ b/openstack/tests/functional/shared_file_system/base.py @@ -71,7 +71,7 @@ def create_share_snapshot(self, share_id, **kwargs): def create_share_group(self, **kwargs): share_group = self.user_cloud.share.create_share_group(**kwargs) self.addCleanup( - self.conn.share.delete_share_group, + self.operator_cloud.share.delete_share_group, share_group.id, ignore_missing=True, ) diff --git a/openstack/tests/functional/shared_file_system/test_share_group.py b/openstack/tests/functional/shared_file_system/test_share_group.py index 376ff5ee0..17ba4aadb 100644 --- a/openstack/tests/functional/shared_file_system/test_share_group.py +++ b/openstack/tests/functional/shared_file_system/test_share_group.py @@ -55,7 +55,9 @@ def test_list_delete_share_group(self): for attribute in ('id', 'name', 'created_at'): self.assertTrue(hasattr(s_grp, attribute)) - sot = self.conn.shared_file_system.delete_share_group(s_grp) + sot = self.operator_cloud.shared_file_system.delete_share_group( + s_grp + ) self.assertIsNone(sot) def test_update(self): diff --git a/openstack/tests/functional/shared_file_system/test_user_message.py b/openstack/tests/functional/shared_file_system/test_user_message.py index b03e55c39..bc8a56de0 100644 --- a/openstack/tests/functional/shared_file_system/test_user_message.py +++ b/openstack/tests/functional/shared_file_system/test_user_message.py @@ -36,4 +36,6 @@ def test_user_messages(self): self.assertTrue(hasattr(u_message, attribute)) self.assertIsInstance(getattr(u_message, attribute), str) - self.conn.shared_file_system.delete_user_message(u_message) + self.operator_cloud.shared_file_system.delete_user_message( + u_message + ) From 4b997e783dc54727800661119c3075ce4b76de79 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 27 Feb 2025 11:26:41 +0000 Subject: [PATCH 3693/3836] tests: Use correct cloud We have a few tests that appear to have been using a non-privileged user but were actually using the privileged user. Correct this. Change-Id: I51ad168dbac8dcc5867a5861ce1f729142581bc4 Signed-off-by: Stephen Finucane --- .../functional/compute/v2/test_keypair.py | 10 +-- .../functional/compute/v2/test_server.py | 62 +++++++------------ 2 files changed, 29 insertions(+), 43 deletions(-) diff --git a/openstack/tests/functional/compute/v2/test_keypair.py b/openstack/tests/functional/compute/v2/test_keypair.py index bdb990f5a..78ccde09c 100644 --- a/openstack/tests/functional/compute/v2/test_keypair.py +++ b/openstack/tests/functional/compute/v2/test_keypair.py @@ -22,7 +22,7 @@ def setUp(self): # Keypairs can't have .'s in the name. Because why? self.NAME = self.getUniqueString().split('.')[-1] - sot = self.operator_cloud.compute.create_keypair( + sot = self.user_cloud.compute.create_keypair( name=self.NAME, type='ssh' ) assert isinstance(sot, keypair.Keypair) @@ -30,23 +30,23 @@ def setUp(self): self._keypair = sot def tearDown(self): - sot = self.operator_cloud.compute.delete_keypair(self._keypair) + sot = self.user_cloud.compute.delete_keypair(self._keypair) self.assertIsNone(sot) super().tearDown() def test_find(self): - sot = self.operator_cloud.compute.find_keypair(self.NAME) + sot = self.user_cloud.compute.find_keypair(self.NAME) self.assertEqual(self.NAME, sot.name) self.assertEqual(self.NAME, sot.id) def test_get(self): - sot = self.operator_cloud.compute.get_keypair(self.NAME) + sot = self.user_cloud.compute.get_keypair(self.NAME) self.assertEqual(self.NAME, sot.name) self.assertEqual(self.NAME, sot.id) self.assertEqual('ssh', sot.type) def test_list(self): - names = [o.name for o in self.operator_cloud.compute.keypairs()] + names = [o.name for o in self.user_cloud.compute.keypairs()] self.assertIn(self.NAME, names) diff --git a/openstack/tests/functional/compute/v2/test_server.py b/openstack/tests/functional/compute/v2/test_server.py index b8dd6105f..95631b257 100644 --- a/openstack/tests/functional/compute/v2/test_server.py +++ b/openstack/tests/functional/compute/v2/test_server.py @@ -74,17 +74,17 @@ def setUp(self): self.cidr = '10.99.99.0/16' self.network, self.subnet = test_network.create_network( - self.operator_cloud, self.NAME, self.cidr + self.user_cloud, self.NAME, self.cidr ) self.assertIsNotNone(self.network) - sot = self.operator_cloud.compute.create_server( + sot = self.user_cloud.compute.create_server( name=self.NAME, flavor_id=self.flavor.id, image_id=self.image.id, networks=[{"uuid": self.network.id}], ) - self.operator_cloud.compute.wait_for_server( + self.user_cloud.compute.wait_for_server( sot, wait=self._wait_for_timeout ) assert isinstance(sot, server.Server) @@ -92,56 +92,48 @@ def setUp(self): self.server = sot def tearDown(self): - sot = self.operator_cloud.compute.delete_server(self.server.id) + sot = self.user_cloud.compute.delete_server(self.server.id) self.assertIsNone(sot) # Need to wait for the stack to go away before network delete - self.operator_cloud.compute.wait_for_delete( + self.user_cloud.compute.wait_for_delete( self.server, wait=self._wait_for_timeout ) - test_network.delete_network( - self.operator_cloud, self.network, self.subnet - ) + test_network.delete_network(self.user_cloud, self.network, self.subnet) super().tearDown() def test_find(self): - sot = self.operator_cloud.compute.find_server(self.NAME) + sot = self.user_cloud.compute.find_server(self.NAME) self.assertEqual(self.server.id, sot.id) def test_get(self): - sot = self.operator_cloud.compute.get_server(self.server.id) + sot = self.user_cloud.compute.get_server(self.server.id) self.assertEqual(self.NAME, sot.name) self.assertEqual(self.server.id, sot.id) def test_list(self): - names = [o.name for o in self.operator_cloud.compute.servers()] + names = [o.name for o in self.user_cloud.compute.servers()] self.assertIn(self.NAME, names) def test_server_metadata(self): - test_server = self.operator_cloud.compute.get_server(self.server.id) + test_server = self.user_cloud.compute.get_server(self.server.id) # get metadata - test_server = self.operator_cloud.compute.get_server_metadata( - test_server - ) + test_server = self.user_cloud.compute.get_server_metadata(test_server) self.assertFalse(test_server.metadata) # set no metadata - self.operator_cloud.compute.set_server_metadata(test_server) - test_server = self.operator_cloud.compute.get_server_metadata( - test_server - ) + self.user_cloud.compute.set_server_metadata(test_server) + test_server = self.user_cloud.compute.get_server_metadata(test_server) self.assertFalse(test_server.metadata) # set empty metadata - self.operator_cloud.compute.set_server_metadata(test_server, k0='') - server = self.operator_cloud.compute.get_server_metadata(test_server) + self.user_cloud.compute.set_server_metadata(test_server, k0='') + server = self.user_cloud.compute.get_server_metadata(test_server) self.assertTrue(server.metadata) # set metadata - self.operator_cloud.compute.set_server_metadata(test_server, k1='v1') - test_server = self.operator_cloud.compute.get_server_metadata( - test_server - ) + self.user_cloud.compute.set_server_metadata(test_server, k1='v1') + test_server = self.user_cloud.compute.get_server_metadata(test_server) self.assertTrue(test_server.metadata) self.assertEqual(2, len(test_server.metadata)) self.assertIn('k0', test_server.metadata) @@ -150,10 +142,8 @@ def test_server_metadata(self): self.assertEqual('v1', test_server.metadata['k1']) # set more metadata - self.operator_cloud.compute.set_server_metadata(test_server, k2='v2') - test_server = self.operator_cloud.compute.get_server_metadata( - test_server - ) + self.user_cloud.compute.set_server_metadata(test_server, k2='v2') + test_server = self.user_cloud.compute.get_server_metadata(test_server) self.assertTrue(test_server.metadata) self.assertEqual(3, len(test_server.metadata)) self.assertIn('k0', test_server.metadata) @@ -164,10 +154,8 @@ def test_server_metadata(self): self.assertEqual('v2', test_server.metadata['k2']) # update metadata - self.operator_cloud.compute.set_server_metadata(test_server, k1='v1.1') - test_server = self.operator_cloud.compute.get_server_metadata( - test_server - ) + self.user_cloud.compute.set_server_metadata(test_server, k1='v1.1') + test_server = self.user_cloud.compute.get_server_metadata(test_server) self.assertTrue(test_server.metadata) self.assertEqual(3, len(test_server.metadata)) self.assertIn('k0', test_server.metadata) @@ -178,16 +166,14 @@ def test_server_metadata(self): self.assertEqual('v2', test_server.metadata['k2']) # delete metadata - self.operator_cloud.compute.delete_server_metadata( + self.user_cloud.compute.delete_server_metadata( test_server, test_server.metadata.keys() ) - test_server = self.operator_cloud.compute.get_server_metadata( - test_server - ) + test_server = self.user_cloud.compute.get_server_metadata(test_server) self.assertFalse(test_server.metadata) def test_server_remote_console(self): - console = self.operator_cloud.compute.create_server_remote_console( + console = self.user_cloud.compute.create_server_remote_console( self.server, protocol='vnc', type='novnc' ) self.assertEqual('vnc', console.protocol) From 02f039cfcf38619b14e0919a05bee08b43a4ae4f Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 27 Feb 2025 11:37:11 +0000 Subject: [PATCH 3694/3836] tests: Streamline keypair tests We should do this for all tests. Need a way to do so in an automated fashion first though :) Change-Id: I98caa2a907ea642b93b4ddd868dc21b3a9866f2e Signed-off-by: Stephen Finucane --- .../functional/compute/v2/test_keypair.py | 95 ++++++++++--------- 1 file changed, 51 insertions(+), 44 deletions(-) diff --git a/openstack/tests/functional/compute/v2/test_keypair.py b/openstack/tests/functional/compute/v2/test_keypair.py index 78ccde09c..33c1b59f1 100644 --- a/openstack/tests/functional/compute/v2/test_keypair.py +++ b/openstack/tests/functional/compute/v2/test_keypair.py @@ -11,7 +11,7 @@ # under the License. -from openstack.compute.v2 import keypair +from openstack.compute.v2 import keypair as _keypair from openstack.tests.functional import base @@ -20,62 +20,69 @@ def setUp(self): super().setUp() # Keypairs can't have .'s in the name. Because why? - self.NAME = self.getUniqueString().split('.')[-1] + self.keypair_name = self.getUniqueString().split('.')[-1] - sot = self.user_cloud.compute.create_keypair( - name=self.NAME, type='ssh' + def _delete_keypair(self, keypair): + ret = self.user_cloud.compute.delete_keypair(keypair) + self.assertIsNone(ret) + + def test_keypair(self): + # create the keypair + + keypair = self.user_cloud.compute.create_keypair( + name=self.keypair_name, type='ssh' ) - assert isinstance(sot, keypair.Keypair) - self.assertEqual(self.NAME, sot.name) - self._keypair = sot + self.assertIsInstance(keypair, _keypair.Keypair) + self.assertEqual(self.keypair_name, keypair.name) + self.addCleanup(self._delete_keypair, keypair) + + # retrieve details of the keypair by ID + + keypair = self.user_cloud.compute.get_keypair(self.keypair_name) + self.assertIsInstance(keypair, _keypair.Keypair) + self.assertEqual(self.keypair_name, keypair.name) + self.assertEqual(self.keypair_name, keypair.id) + self.assertEqual('ssh', keypair.type) - def tearDown(self): - sot = self.user_cloud.compute.delete_keypair(self._keypair) - self.assertIsNone(sot) - super().tearDown() + # retrieve details of the keypair by name - def test_find(self): - sot = self.user_cloud.compute.find_keypair(self.NAME) - self.assertEqual(self.NAME, sot.name) - self.assertEqual(self.NAME, sot.id) + keypair = self.user_cloud.compute.find_keypair(self.keypair_name) + self.assertIsInstance(keypair, _keypair.Keypair) + self.assertEqual(self.keypair_name, keypair.name) + self.assertEqual(self.keypair_name, keypair.id) - def test_get(self): - sot = self.user_cloud.compute.get_keypair(self.NAME) - self.assertEqual(self.NAME, sot.name) - self.assertEqual(self.NAME, sot.id) - self.assertEqual('ssh', sot.type) + # list all keypairs - def test_list(self): - names = [o.name for o in self.user_cloud.compute.keypairs()] - self.assertIn(self.NAME, names) + keypairs = list(self.user_cloud.compute.keypairs()) + self.assertIsInstance(keypair, _keypair.Keypair) + self.assertIn(self.keypair_name, {x.name for x in keypairs}) class TestKeypairAdmin(base.BaseFunctionalTest): def setUp(self): super().setUp() - self.NAME = self.getUniqueString().split('.')[-1] - self.USER = self.operator_cloud.list_users()[0] + self.keypair_name = self.getUniqueString().split('.')[-1] + self.user = self.operator_cloud.list_users()[0] - sot = self.operator_cloud.compute.create_keypair( - name=self.NAME, user_id=self.USER.id - ) - assert isinstance(sot, keypair.Keypair) - self.assertEqual(self.NAME, sot.name) - self.assertEqual(self.USER.id, sot.user_id) - self._keypair = sot - - def tearDown(self): - sot = self.operator_cloud.compute.delete_keypair( - self.NAME, user_id=self.USER.id + def _delete_keypair(self, keypair): + ret = self.user_cloud.compute.delete_keypair(keypair) + self.assertIsNone(ret) + + def test_keypair(self): + # create the keypair (for another user) + keypair = self.operator_cloud.compute.create_keypair( + name=self.keypair_name, user_id=self.user.id ) - self.assertIsNone(sot) - super().tearDown() + self.assertIsInstance(keypair, _keypair.Keypair) + self.assertEqual(self.keypair_name, keypair.name) + self.addCleanup(self._delete_keypair, keypair) + + # retrieve details of the keypair by ID (for another user) - def test_get(self): - sot = self.operator_cloud.compute.get_keypair( - self.NAME, user_id=self.USER.id + keypair = self.operator_cloud.compute.get_keypair( + self.keypair_name, user_id=self.user.id ) - self.assertEqual(self.NAME, sot.name) - self.assertEqual(self.NAME, sot.id) - self.assertEqual(self.USER.id, sot.user_id) + self.assertEqual(self.keypair_name, keypair.name) + self.assertEqual(self.keypair_name, keypair.id) + self.assertEqual(self.user.id, keypair.user_id) From 446c47ad747ede4bf8dece84391f38ab646533a1 Mon Sep 17 00:00:00 2001 From: Michael Still Date: Thu, 3 Apr 2025 20:25:05 +1100 Subject: [PATCH 3695/3836] Add support for spice-direct console types. This patch adds support for Nova microversion 2.99 which exposes the new spice-direct console type and the pre-existing /os-console-auth-token/ API. +----------+----------------------------------------------------------+ | Field | Value | +----------+----------------------------------------------------------+ | protocol | spice | | type | spice-direct | | url | http://127.0.0.1:13002/nova?token=f78009fb-41ad-... | +----------+----------------------------------------------------------+ +----------------------+--------------------------------------+ | Field | Value | +----------------------+--------------------------------------+ | host | 127.0.0.1 | | instance_uuid | f2477018-aa93-... | | internal_access_path | None | | port | 5900 | | tls_port | 5901 | +----------------------+--------------------------------------+ Change-Id: I97e3415ec3110374cf95d244cd75bcdf6c5e7871 --- doc/source/user/proxies/compute.rst | 11 +++++- .../compute/v2/console_auth_token.rst | 13 +++++++ openstack/compute/v2/_proxy.py | 15 +++++++- openstack/compute/v2/console_auth_token.py | 35 +++++++++++++++++++ openstack/compute/v2/server.py | 4 ++- openstack/compute/v2/server_remote_console.py | 10 +++++- openstack/tests/unit/compute/v2/test_proxy.py | 9 +++++ .../tests/unit/compute/v2/test_server.py | 8 +++++ ...e-console-auth-token-999b790aec83de85.yaml | 13 +++++++ 9 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 doc/source/user/resources/compute/v2/console_auth_token.rst create mode 100644 openstack/compute/v2/console_auth_token.py create mode 100644 releasenotes/notes/compute-add-validate-console-auth-token-999b790aec83de85.yaml diff --git a/doc/source/user/proxies/compute.rst b/doc/source/user/proxies/compute.rst index eec5c8f2a..c82a990e2 100644 --- a/doc/source/user/proxies/compute.rst +++ b/doc/source/user/proxies/compute.rst @@ -42,7 +42,7 @@ Starting, Stopping, etc. reboot_server, restore_server, shelve_server, unshelve_server, lock_server, unlock_server, pause_server, unpause_server, rescue_server, unrescue_server, evacuate_server, migrate_server, - get_server_console_output, live_migrate_server + live_migrate_server Modifying a Server ****************** @@ -187,6 +187,15 @@ Migration Operations :noindex: :members: migrations +Interactive Consoles +^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.compute.v2._proxy.Proxy + :noindex: + :members: create_server_remote_console, get_server_console_url, + validate_console_auth_token, get_server_console_output, + create_console + Helpers ^^^^^^^ diff --git a/doc/source/user/resources/compute/v2/console_auth_token.rst b/doc/source/user/resources/compute/v2/console_auth_token.rst new file mode 100644 index 000000000..86b0718fd --- /dev/null +++ b/doc/source/user/resources/compute/v2/console_auth_token.rst @@ -0,0 +1,13 @@ +openstack.compute.v2.console_auth_token +======================================= + +.. automodule:: openstack.compute.v2.console_auth_token + +The ServerRemoteConsole Class +----------------------------- + +The ``ConsoleAuthToken`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.compute.v2.console_auth_token.ConsoleAuthToken + :members: diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index f48e1f684..63e67277a 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -16,6 +16,7 @@ from openstack.block_storage.v3 import volume as _volume from openstack.compute.v2 import aggregate as _aggregate from openstack.compute.v2 import availability_zone +from openstack.compute.v2 import console_auth_token as _console_auth_token from openstack.compute.v2 import extension from openstack.compute.v2 import flavor as _flavor from openstack.compute.v2 import hypervisor as _hypervisor @@ -57,6 +58,7 @@ class Proxy(proxy.Proxy): "keypair": _keypair.Keypair, "limits": limits.Limits, "migration": _migration.Migration, + "os_console_auth_token": _console_auth_token.ConsoleAuthToken, "quota_class_set": _quota_class_set.QuotaClassSet, "quota_set": _quota_set.QuotaSet, "server": _server.Server, @@ -2384,6 +2386,15 @@ def get_server_console_url(self, server, console_type): server = self._get_resource(_server.Server, server) return server.get_console_url(self, console_type) + def validate_console_auth_token(self, console_token): + """Lookup console connection information for a console auth token. + + :param console_token: The console auth token as returned in the URL + from get_server_console_url. + :returns: Dictionary with connection details, varying by console type. + """ + return self._get(_console_auth_token.ConsoleAuthToken, console_token) + def get_server_console_output(self, server, length=None): """Return the console output for a server. @@ -2411,10 +2422,12 @@ def create_console(self, server, console_type, console_protocol=None): * rdp-html5 * serial * webmks (supported after 2.8) + * spice-direct (supported after 2.99) :param console_protocol: Optional console protocol (is respected only after microversion 2.6). - :returns: Dictionary with console type, url and optionally protocol. + :returns: Dictionary with console type, connection details (a url), and + optionally protocol. """ server = self._get_resource(_server.Server, server) # NOTE: novaclient supports undocumented type xcpvnc also supported diff --git a/openstack/compute/v2/console_auth_token.py b/openstack/compute/v2/console_auth_token.py new file mode 100644 index 000000000..c6aa5ac95 --- /dev/null +++ b/openstack/compute/v2/console_auth_token.py @@ -0,0 +1,35 @@ +# 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 resource + + +class ConsoleAuthToken(resource.Resource): + resource_key = 'console' + base_path = '/os-console-auth-tokens' + + # capabilities + allow_fetch = True + + _max_microversion = '2.99' + + # Properties + #: Instance UUID + instance_uuid = resource.Body('instance_uuid') + #: Hypervisor host + host = resource.Body('host') + #: Hypervisor port + port = resource.Body('port') + #: Hypervisor TLS port + tls_port = resource.Body('tls_port') + #: Internal access path + internal_access_path = resource.Body('internal_access_path') diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 069caacd1..f4cdc6443 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -35,6 +35,7 @@ def __bool__(self) -> ty.Literal[False]: 'novnc': 'os-getVNCConsole', 'xvpvnc': 'os-getVNCConsole', 'spice-html5': 'os-getSPICEConsole', + 'spice-direct': 'os-getSPICEConsole', 'rdp-html5': 'os-getRDPConsole', 'serial': 'os-getSerialConsole', } @@ -889,7 +890,8 @@ def get_console_url(self, session, console_type): :param session: The session to use for making this request. :param console_type: The type of console to return. This is cloud-specific. One of: ``novnc``, ``xvpvnc``, ``spice-html5``, - ``rdp-html5``, ``serial``. + ``spice-direct`` (after Nova microversion 2.99), ``rdp-html5``, + or ``serial``. :returns: None """ action = CONSOLE_TYPE_ACTION_MAPPING.get(console_type) diff --git a/openstack/compute/v2/server_remote_console.py b/openstack/compute/v2/server_remote_console.py index 7a845b6ab..7d62ea6dd 100644 --- a/openstack/compute/v2/server_remote_console.py +++ b/openstack/compute/v2/server_remote_console.py @@ -17,6 +17,7 @@ 'novnc': 'vnc', 'xvpvnc': 'vnc', 'spice-html5': 'spice', + 'spice-direct': 'spice', 'rdp-html5': 'rdp', 'serial': 'serial', 'webmks': 'mks', @@ -34,7 +35,7 @@ class ServerRemoteConsole(resource.Resource): allow_delete = False allow_list = False - _max_microversion = '2.8' + _max_microversion = '2.99' #: Protocol of the remote console. protocol = resource.Body('protocol') @@ -55,6 +56,13 @@ def create(self, session, prepend_key=True, base_path=None, **params): raise ValueError( 'Console type webmks is not supported on server side' ) + if ( + not utils.supports_microversion(session, '2.99') + and self.type == 'spice-direct' + ): + raise ValueError( + 'Console type spice-direct is not supported on server side' + ) return super().create( session, prepend_key=prepend_key, base_path=base_path, **params ) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 0e9cd9843..c579f1c10 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -21,6 +21,7 @@ from openstack.compute.v2 import _proxy from openstack.compute.v2 import aggregate from openstack.compute.v2 import availability_zone as az +from openstack.compute.v2 import console_auth_token from openstack.compute.v2 import extension from openstack.compute.v2 import flavor from openstack.compute.v2 import hypervisor @@ -1885,3 +1886,11 @@ def test_server_actions(self): method_kwargs={'server': 'server_a'}, expected_kwargs={'server_id': 'server_a'}, ) + + +class TestValidateConsoleAuthToken(TestComputeProxy): + def test_validate_console_auth_token(self): + self.verify_get( + self.proxy.validate_console_auth_token, + console_auth_token.ConsoleAuthToken, + ) diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 2c9e59c5c..f2cdc7e3d 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -1256,6 +1256,14 @@ def test_get_console_url(self): microversion=self.sess.default_microversion, ) + sot.get_console_url(self.sess, 'spice-direct') + self.sess.post.assert_called_with( + 'servers/IDENTIFIER/action', + json={'os-getSPICEConsole': {'type': 'spice-direct'}}, + headers={'Accept': ''}, + microversion=self.sess.default_microversion, + ) + sot.get_console_url(self.sess, 'rdp-html5') self.sess.post.assert_called_with( 'servers/IDENTIFIER/action', diff --git a/releasenotes/notes/compute-add-validate-console-auth-token-999b790aec83de85.yaml b/releasenotes/notes/compute-add-validate-console-auth-token-999b790aec83de85.yaml new file mode 100644 index 000000000..474e42c1e --- /dev/null +++ b/releasenotes/notes/compute-add-validate-console-auth-token-999b790aec83de85.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + Add the ``validate_console_auth_token`` method to the Compute proxy. This + method uses the pre-existing ``os-console-auth-tokens`` OpenStack Compute + API to validate a console access token as produced by + ``get_console_url``. In addition, the method returns hypervisor connection + information for the console (hypervisor IP and port numbers), as this call + is generally used by the console proxies which users connect to. + + By default, callers of this method must have ``admin`` access to the + OpenStack Compute API due to the privileged nature of the hypervisor + connection information returned. From f3ba347b06144e3518359d140327340abf4db267 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 29 Aug 2023 19:18:29 +0100 Subject: [PATCH 3696/3836] baremetal: Add 'details' parameter to various 'find' proxy methods In change I13f32e6ca9be9ed4eb42398aace47e3c5205a81f, we introduced support for a 'details' parameter to various proxy methods that supported it. Unfortunately there was a bug in the baremetal proxy layer changes, which were using a mixin to modify the 'Resource.list' class method to allow listing detailed responses easily via the resource layer. This necessitated a revert, change Icc70bbdc06b5f32722a93775aee2da4d7b7ca4ae. Take another go at this by reverting the revert and making the necessary changes to the mixin and proxy layer to make things compatible. We also fix a class of bugs whereby certain resources have the mixin applied, resulting in them attempting to use legacy path-based mechanism to retrieve detailed resources (GET /resource/detail) despite not supporting this. Change-Id: I6acbcb4d9af35e68c04bb86e50c8844487bd7d6c Signed-off-by: Stephen Finucane --- openstack/baremetal/v1/_common.py | 134 ++++++++++++++++-- openstack/baremetal/v1/_proxy.py | 108 ++++++++++++-- openstack/baremetal/v1/allocation.py | 2 +- openstack/baremetal/v1/conductor.py | 2 +- openstack/baremetal/v1/deploy_templates.py | 2 +- openstack/baremetal/v1/volume_connector.py | 2 +- openstack/baremetal/v1/volume_target.py | 2 +- .../v1/introspection_rule.py | 3 +- .../tests/unit/baremetal/v1/test_proxy.py | 76 +++++++++- ...r-find-proxy-methods-10ecdff59f5c6913.yaml | 14 ++ 10 files changed, 308 insertions(+), 37 deletions(-) create mode 100644 releasenotes/notes/retrieve-detailed-view-for-find-proxy-methods-10ecdff59f5c6913.yaml diff --git a/openstack/baremetal/v1/_common.py b/openstack/baremetal/v1/_common.py index cac214c6b..c7c82fe70 100644 --- a/openstack/baremetal/v1/_common.py +++ b/openstack/baremetal/v1/_common.py @@ -101,10 +101,40 @@ class Resource(resource.Resource): + """A subclass for resources that use the path to request a detailed view. + + Two patterns exist for fetching the detailed view when listing resources. + + - As part of the path. For example: + + GET /v1/ports/detail + + - As a query parameter. For example: + + GET /v1/conductors?detail=True + + This handles resources that use the former pattern, namely: + + - chassis + - nodes + - ports + - portgroups + """ + base_path: str @classmethod - def list(cls, session, details=False, **params): + def list( + cls, + session, + paginated=True, + base_path=None, + allow_unknown_params=False, + *, + microversion=None, + details=False, + **params, + ): """This method is a generator which yields resource objects. This resource object list generator handles pagination and takes query @@ -112,22 +142,102 @@ def list(cls, session, details=False, **params): :param session: The session to use for making this request. :type session: :class:`~keystoneauth1.adapter.Adapter` - :param bool details: Whether to return detailed node records + :param bool paginated: ``True`` if a GET to this resource returns + a paginated series of responses, or ``False`` if a GET returns only + one page of data. **When paginated is False only one page of data + will be returned regardless of the API's support of pagination.** + :param str base_path: Base part of the URI for listing resources, if + different from :data:`~openstack.resource.Resource.base_path`. + :param bool allow_unknown_params: ``True`` to accept, but discard + unknown query parameters. This allows getting list of 'filters' and + passing everything known to the server. ``False`` will result in + validation exception when unknown query parameters are passed. + :param str microversion: API version to override the negotiated one. + :param bool details: Whether to return detailed resource records. :param dict params: These keyword arguments are passed through the - :meth:`~openstack.resource.QueryParameter._transpose` method - to find if any of them match expected query parameters to be - sent in the *params* argument to - :meth:`~keystoneauth1.adapter.Adapter.get`. + :meth:`~openstack.resource.QueryParamter._transpose` method + to find if any of them match expected query parameters to be sent + in the *params* argument to + :meth:`~keystoneauth1.adapter.Adapter.get`. They are additionally + checked against the :data:`~openstack.resource.Resource.base_path` + format string to see if any path fragments need to be filled in by + the contents of this argument. + Parameters supported as filters by the server side are passed in + the API call, remaining parameters are applied as filters to the + retrieved results. - :return: A generator of :class:`openstack.resource.Resource` objects. + :return: A generator of :class:`Resource` objects. + :raises: :exc:`~openstack.exceptions.MethodNotSupported` if + :data:`Resource.allow_list` is not set to ``True``. :raises: :exc:`~openstack.exceptions.InvalidResourceQuery` if query - contains invalid params. + contains invalid params. """ - base_path = cls.base_path - if details: - base_path += '/detail' + if not base_path: + base_path = cls.base_path + if details: + base_path += '/detail' + return super().list( - session, paginated=True, base_path=base_path, **params + session, + paginated=paginated, + base_path=base_path, + allow_unknown_params=allow_unknown_params, + microversion=microversion, + **params, + ) + + @classmethod + def find( + cls, + session, + name_or_id, + ignore_missing=True, + list_base_path=None, + *, + microversion=None, + all_projects=None, + details=False, + **params, + ): + """Find a resource by its name or id. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param name_or_id: This resource's identifier, if needed by + the request. The default is ``None``. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the resource does not exist. When set to ``True``, None will be + returned when attempting to find a nonexistent resource. + :param str list_base_path: base_path to be used when need listing + resources. + :param str microversion: API version to override the negotiated one. + :param bool details: Whether to return detailed resource records. + :param dict params: Any additional parameters to be passed into + underlying methods, such as to + :meth:`~openstack.resource.Resource.existing` in order to pass on + URI parameters. + + :return: The :class:`Resource` object matching the given name or id + or None if nothing matches. + :raises: :class:`openstack.exceptions.DuplicateResource` if more + than one resource is found for this request. + :raises: :class:`openstack.exceptions.ResourceNotFound` if nothing + is found and ignore_missing is ``False``. + """ + if not list_base_path: + list_base_path = cls.base_path + if details: + list_base_path += '/detail' + + return super().find( + session, + name_or_id, + ignore_missing=ignore_missing, + list_base_path=list_base_path, + microversion=microversion, + all_projects=all_projects, + **params, ) diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 5a86066e6..b3815f229 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -118,7 +118,9 @@ def create_chassis(self, **attrs): """ return self._create(_chassis.Chassis, **attrs) - def find_chassis(self, name_or_id, ignore_missing=True): + # TODO(stephenfin): Delete this. You can't lookup a chassis by name so this + # is identical to get_chassis + def find_chassis(self, name_or_id, ignore_missing=True, *, details=True): """Find a single chassis. :param str name_or_id: The ID of a chassis. @@ -126,11 +128,17 @@ def find_chassis(self, name_or_id, ignore_missing=True): :class:`~openstack.exceptions.NotFoundException` will be raised when the chassis does not exist. When set to `True``, None will be returned when attempting to find a nonexistent chassis. + :param details: A boolean indicating whether the detailed information + for the chassis should be returned. + :returns: One :class:`~openstack.baremetal.v1.chassis.Chassis` object or None. """ return self._find( - _chassis.Chassis, name_or_id, ignore_missing=ignore_missing + _chassis.Chassis, + name_or_id, + ignore_missing=ignore_missing, + details=details, ) def get_chassis(self, chassis, fields=None): @@ -319,7 +327,7 @@ def create_node(self, **attrs): """ return self._create(_node.Node, **attrs) - def find_node(self, name_or_id, ignore_missing=True): + def find_node(self, name_or_id, ignore_missing=True, *, details=True): """Find a single node. :param str name_or_id: The name or ID of a node. @@ -327,11 +335,16 @@ def find_node(self, name_or_id, ignore_missing=True): :class:`~openstack.exceptions.NotFoundException` will be raised when the node does not exist. When set to `True``, None will be returned when attempting to find a nonexistent node. + :param details: A boolean indicating whether the detailed information + for the node should be returned. :returns: One :class:`~openstack.baremetal.v1.node.Node` object or None. """ return self._find( - _node.Node, name_or_id, ignore_missing=ignore_missing + _node.Node, + name_or_id, + ignore_missing=ignore_missing, + details=details, ) def get_node(self, node, fields=None): @@ -889,7 +902,9 @@ def create_port(self, **attrs): """ return self._create(_port.Port, **attrs) - def find_port(self, name_or_id, ignore_missing=True): + # TODO(stephenfin): Delete this. You can't lookup a port by name so this is + # identical to get_port + def find_port(self, name_or_id, ignore_missing=True, *, details=True): """Find a single port. :param str name_or_id: The ID of a port. @@ -897,11 +912,16 @@ def find_port(self, name_or_id, ignore_missing=True): :class:`~openstack.exceptions.NotFoundException` will be raised when the port does not exist. When set to `True``, None will be returned when attempting to find a nonexistent port. + :param details: A boolean indicating whether the detailed information + for every port should be returned. :returns: One :class:`~openstack.baremetal.v1.port.Port` object or None. """ return self._find( - _port.Port, name_or_id, ignore_missing=ignore_missing + _port.Port, + name_or_id, + ignore_missing=ignore_missing, + details=details, ) def get_port(self, port, fields=None): @@ -1010,7 +1030,13 @@ def create_port_group(self, **attrs): """ return self._create(_portgroup.PortGroup, **attrs) - def find_port_group(self, name_or_id, ignore_missing=True): + def find_port_group( + self, + name_or_id, + ignore_missing=True, + *, + details=True, + ): """Find a single port group. :param str name_or_id: The name or ID of a portgroup. @@ -1018,11 +1044,16 @@ def find_port_group(self, name_or_id, ignore_missing=True): :class:`~openstack.exceptions.NotFoundException` will be raised when the port group does not exist. When set to `True``, None will be returned when attempting to find a nonexistent port group. + :param details: A boolean indicating whether the detailed information + for the port group should be returned. :returns: One :class:`~openstack.baremetal.v1.port_group.PortGroup` object or None. """ return self._find( - _portgroup.PortGroup, name_or_id, ignore_missing=ignore_missing + _portgroup.PortGroup, + name_or_id, + ignore_missing=ignore_missing, + details=details, ) def get_port_group(self, port_group, fields=None): @@ -1349,16 +1380,26 @@ def create_volume_connector(self, **attrs): """ return self._create(_volumeconnector.VolumeConnector, **attrs) - def find_volume_connector(self, vc_id, ignore_missing=True): + # TODO(stephenfin): Delete this. You can't lookup a volume connector by + # name so this is identical to get_volume_connector + def find_volume_connector( + self, + vc_id, + ignore_missing=True, + *, + details=True, + ): """Find a single volume connector. :param str vc_id: The ID of a volume connector. - :param bool ignore_missing: When set to ``False``, an exception of :class:`~openstack.exceptions.NotFoundException` will be raised when the volume connector does not exist. When set to `True``, None will be returned when attempting to find a nonexistent volume connector. + :param details: A boolean indicating whether the detailed information + for the volume connector should be returned. + :returns: One :class:`~openstack.baremetal.v1.volumeconnector.VolumeConnector` object or None. @@ -1367,6 +1408,7 @@ def find_volume_connector(self, vc_id, ignore_missing=True): _volumeconnector.VolumeConnector, vc_id, ignore_missing=ignore_missing, + details=details, ) def get_volume_connector(self, volume_connector, fields=None): @@ -1498,22 +1540,29 @@ def create_volume_target(self, **attrs): """ return self._create(_volumetarget.VolumeTarget, **attrs) - def find_volume_target(self, vt_id, ignore_missing=True): + # TODO(stephenfin): Delete this. You can't lookup a volume target by + # name so this is identical to get_volume_connector + def find_volume_target(self, vt_id, ignore_missing=True, *, details=True): """Find a single volume target. :param str vt_id: The ID of a volume target. - :param bool ignore_missing: When set to ``False``, an exception of :class:`~openstack.exceptions.NotFoundException` will be raised when the volume connector does not exist. When set to `True``, None will be returned when attempting to find a nonexistent volume target. + :param details: A boolean indicating whether the detailed information + for the volume target should be returned. + :returns: One :class:`~openstack.baremetal.v1.volumetarget.VolumeTarget` object or None. """ return self._find( - _volumetarget.VolumeTarget, vt_id, ignore_missing=ignore_missing + _volumetarget.VolumeTarget, + vt_id, + ignore_missing=ignore_missing, + details=details, ) def get_volume_target(self, volume_target, fields=None): @@ -1683,6 +1732,35 @@ def get_deploy_template(self, deploy_template, fields=None): _deploytemplates.DeployTemplate, deploy_template, fields=fields ) + def find_deploy_template( + self, + name_or_id, + ignore_missing=True, + *, + details=True, + ): + """Find a single deployment template. + + :param str name_or_id: The name or ID of a deployment template. + :param bool ignore_missing: When set to ``False``, an exception of + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the deployment template does not exist. When set to `True``, + None will be returned when attempting to find a nonexistent + deployment template. + :param details: A boolean indicating whether the detailed information + for the deployment template should be returned. + + :returns: One + :class:`~openstack.baremetal.v1.deploy_templates.DeployTemplate` or + None. + """ + return self._find( + _deploytemplates.DeployTemplate, + name_or_id, + ignore_missing=ignore_missing, + details=details, + ) + def patch_deploy_template(self, deploy_template, patch): """Apply a JSON patch to the deploy_templates. @@ -1716,6 +1794,10 @@ def conductors(self, details=False, **query): query['details'] = True return _conductor.Conductor.list(self, **query) + # NOTE(stephenfin): There is no 'find_conductor' since conductors are + # identified by the host name, not an arbitrary UUID, meaning + # 'find_conductor' would be identical to 'get_conductor' + def get_conductor(self, conductor, fields=None): """Get a specific conductor. diff --git a/openstack/baremetal/v1/allocation.py b/openstack/baremetal/v1/allocation.py index eb0dad8d9..b2f79fa6d 100644 --- a/openstack/baremetal/v1/allocation.py +++ b/openstack/baremetal/v1/allocation.py @@ -16,7 +16,7 @@ from openstack import utils -class Allocation(_common.Resource): +class Allocation(resource.Resource): resources_key = 'allocations' base_path = '/allocations' diff --git a/openstack/baremetal/v1/conductor.py b/openstack/baremetal/v1/conductor.py index ff1f00346..febc0220a 100644 --- a/openstack/baremetal/v1/conductor.py +++ b/openstack/baremetal/v1/conductor.py @@ -14,7 +14,7 @@ from openstack import resource -class Conductor(_common.Resource): +class Conductor(resource.Resource): resources_key = 'conductors' base_path = '/conductors' diff --git a/openstack/baremetal/v1/deploy_templates.py b/openstack/baremetal/v1/deploy_templates.py index edb8a8fa0..d97747235 100644 --- a/openstack/baremetal/v1/deploy_templates.py +++ b/openstack/baremetal/v1/deploy_templates.py @@ -14,7 +14,7 @@ from openstack import resource -class DeployTemplate(_common.Resource): +class DeployTemplate(resource.Resource): resources_key = 'deploy_templates' base_path = '/deploy_templates' diff --git a/openstack/baremetal/v1/volume_connector.py b/openstack/baremetal/v1/volume_connector.py index 06495e876..60f20b634 100644 --- a/openstack/baremetal/v1/volume_connector.py +++ b/openstack/baremetal/v1/volume_connector.py @@ -14,7 +14,7 @@ from openstack import resource -class VolumeConnector(_common.Resource): +class VolumeConnector(resource.Resource): resources_key = 'connectors' base_path = '/volume/connectors' diff --git a/openstack/baremetal/v1/volume_target.py b/openstack/baremetal/v1/volume_target.py index 12d9da5e5..e5050adf6 100644 --- a/openstack/baremetal/v1/volume_target.py +++ b/openstack/baremetal/v1/volume_target.py @@ -14,7 +14,7 @@ from openstack import resource -class VolumeTarget(_common.Resource): +class VolumeTarget(resource.Resource): resources_key = 'targets' base_path = '/volume/targets' diff --git a/openstack/baremetal_introspection/v1/introspection_rule.py b/openstack/baremetal_introspection/v1/introspection_rule.py index 27bf61d79..129426e2f 100644 --- a/openstack/baremetal_introspection/v1/introspection_rule.py +++ b/openstack/baremetal_introspection/v1/introspection_rule.py @@ -10,11 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.baremetal.v1 import _common from openstack import resource -class IntrospectionRule(_common.Resource): +class IntrospectionRule(resource.Resource): resources_key = 'rules' base_path = '/rules' diff --git a/openstack/tests/unit/baremetal/v1/test_proxy.py b/openstack/tests/unit/baremetal/v1/test_proxy.py index 5d14bb312..bd330d1af 100644 --- a/openstack/tests/unit/baremetal/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal/v1/test_proxy.py @@ -15,6 +15,7 @@ from openstack.baremetal.v1 import _proxy from openstack.baremetal.v1 import allocation from openstack.baremetal.v1 import chassis +from openstack.baremetal.v1 import deploy_templates from openstack.baremetal.v1 import driver from openstack.baremetal.v1 import node from openstack.baremetal.v1 import port @@ -60,7 +61,11 @@ def test_create_chassis(self): self.verify_create(self.proxy.create_chassis, chassis.Chassis) def test_find_chassis(self): - self.verify_find(self.proxy.find_chassis, chassis.Chassis) + self.verify_find( + self.proxy.find_chassis, + chassis.Chassis, + expected_kwargs={'details': True}, + ) def test_get_chassis(self): self.verify_get( @@ -110,7 +115,11 @@ def test_create_node(self): self.verify_create(self.proxy.create_node, node.Node) def test_find_node(self): - self.verify_find(self.proxy.find_node, node.Node) + self.verify_find( + self.proxy.find_node, + node.Node, + expected_kwargs={'details': True}, + ) def test_get_node(self): self.verify_get( @@ -162,7 +171,11 @@ def test_create_port(self): self.verify_create(self.proxy.create_port, port.Port) def test_find_port(self): - self.verify_find(self.proxy.find_port, port.Port) + self.verify_find( + self.proxy.find_port, + port.Port, + expected_kwargs={'details': True}, + ) def test_get_port(self): self.verify_get( @@ -236,7 +249,9 @@ def test_create_volume_connector(self): def test_find_volume_connector(self): self.verify_find( - self.proxy.find_volume_connector, volume_connector.VolumeConnector + self.proxy.find_volume_connector, + volume_connector.VolumeConnector, + expected_kwargs={'details': True}, ) def test_get_volume_connector(self): @@ -282,7 +297,9 @@ def test_create_volume_target(self): def test_find_volume_target(self): self.verify_find( - self.proxy.find_volume_target, volume_target.VolumeTarget + self.proxy.find_volume_target, + volume_target.VolumeTarget, + expected_kwargs={'details': True}, ) def test_get_volume_target(self): @@ -304,6 +321,55 @@ def test_delete_volume_target_ignore(self): ) +class TestDeployTemplate(TestBaremetalProxy): + @mock.patch.object(deploy_templates.DeployTemplate, 'list') + def test_deploy_templates_detailed(self, mock_list): + result = self.proxy.deploy_templates(details=True, query=1) + self.assertIs(result, mock_list.return_value) + mock_list.assert_called_once_with(self.proxy, detail=True, query=1) + + @mock.patch.object(deploy_templates.DeployTemplate, 'list') + def test_deploy_templates_not_detailed(self, mock_list): + result = self.proxy.deploy_templates(query=1) + self.assertIs(result, mock_list.return_value) + mock_list.assert_called_once_with(self.proxy, query=1) + + def test_create_deploy_template(self): + self.verify_create( + self.proxy.create_deploy_template, + deploy_templates.DeployTemplate, + ) + + def test_find_deploy_template(self): + self.verify_find( + self.proxy.find_deploy_template, + deploy_templates.DeployTemplate, + expected_kwargs={'details': True}, + ) + + def test_get_deploy_template(self): + self.verify_get( + self.proxy.get_deploy_template, + deploy_templates.DeployTemplate, + mock_method=_MOCK_METHOD, + expected_kwargs={'fields': None}, + ) + + def test_delete_deploy_template(self): + self.verify_delete( + self.proxy.delete_deploy_template, + deploy_templates.DeployTemplate, + False, + ) + + def test_delete_deploy_template_ignore(self): + self.verify_delete( + self.proxy.delete_deploy_template, + deploy_templates.DeployTemplate, + True, + ) + + class TestMisc(TestBaremetalProxy): @mock.patch.object(node.Node, 'fetch', autospec=True) def test__get_with_fields_none(self, mock_fetch): diff --git a/releasenotes/notes/retrieve-detailed-view-for-find-proxy-methods-10ecdff59f5c6913.yaml b/releasenotes/notes/retrieve-detailed-view-for-find-proxy-methods-10ecdff59f5c6913.yaml new file mode 100644 index 000000000..3497f18f7 --- /dev/null +++ b/releasenotes/notes/retrieve-detailed-view-for-find-proxy-methods-10ecdff59f5c6913.yaml @@ -0,0 +1,14 @@ +--- +features: + - | + The following proxy ``find_*`` operations will now retrieve a detailed + resource by default when retrieving by name: + + * Bare metal (v1) + + * ``find_chassis`` + * ``find_node`` + * ``find_port`` + * ``find_port_group`` + * ``find_volume_connector`` + * ``find_volume_target`` From 329b28f38c06359bec0d133c00ccda4524686cbb Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 26 Feb 2025 13:24:18 +0000 Subject: [PATCH 3697/3836] identity: Add functional tests for limits, registered limits Change-Id: If2a3f137e9404271fb435027e1473616b7a4304a Signed-off-by: Stephen Finucane --- openstack/tests/functional/base.py | 18 +++ .../functional/identity/v3/test_limit.py | 98 ++++++++++++++++ .../v3/{test_projects.py => test_project.py} | 0 .../identity/v3/test_registered_limit.py | 111 ++++++++++++++++++ 4 files changed, 227 insertions(+) create mode 100644 openstack/tests/functional/identity/v3/test_limit.py rename openstack/tests/functional/identity/v3/{test_projects.py => test_project.py} (100%) create mode 100644 openstack/tests/functional/identity/v3/test_registered_limit.py diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index a3e948e6d..f42f719ff 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -50,6 +50,16 @@ class BaseFunctionalTest(base.TestCase): def setUp(self): super().setUp() + self._system_admin_name = os.environ.get( + 'OPENSTACKSDK_SYSTEM_ADMIN_CLOUD', + 'devstack-system-admin', + ) + if not self._system_admin_name: + raise self.failureException( + "OPENSTACKSDK_SYSTEM_ADMIN_CLOUD must be set to a non-empty " + "value" + ) + self.config = openstack.config.OpenStackConfig() self._user_cloud_name = os.environ.get( @@ -112,6 +122,14 @@ def _set_operator_cloud(self, **kwargs): self.operator_cloud = connection.Connection(config=operator_config) _disable_keep_alive(self.operator_cloud) + system_admin_config = self.config.get_one( + cloud=self._system_admin_name, **kwargs + ) + self.system_admin_cloud = connection.Connection( + config=system_admin_config + ) + _disable_keep_alive(self.system_admin_cloud) + def _pick_flavor(self): """Pick a sensible flavor to run tests with. diff --git a/openstack/tests/functional/identity/v3/test_limit.py b/openstack/tests/functional/identity/v3/test_limit.py new file mode 100644 index 000000000..b49884fbc --- /dev/null +++ b/openstack/tests/functional/identity/v3/test_limit.py @@ -0,0 +1,98 @@ +# 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.identity.v3 import limit as _limit +from openstack.tests.functional import base + + +class TestLimit(base.BaseFunctionalTest): + def setUp(self): + super().setUp() + + self.service_name = self.getUniqueString('service') + self.service_type = self.getUniqueString('type') + self.service = self.system_admin_cloud.identity.create_service( + name=self.service_name, + type=self.service_type, + ) + self.addCleanup( + self.system_admin_cloud.identity.delete_service, self.service + ) + + self.resource_name = self.getUniqueString('resource') + self.registered_limit = ( + self.system_admin_cloud.identity.create_registered_limit( + resource_name=self.resource_name, + service_id=self.service.id, + default_limit=10, + ) + ) + self.addCleanup( + self.system_admin_cloud.identity.delete_registered_limit, + self.registered_limit, + ) + + self.project_name = self.getUniqueString('project') + self.project = self.system_admin_cloud.identity.create_project( + name=self.project_name, + ) + self.addCleanup( + self.system_admin_cloud.identity.delete_project, self.project + ) + + self.limit_description = self.getUniqueString('limit') + + def _delete_limit(self, limit): + ret = self.system_admin_cloud.identity.delete_limit(limit) + self.assertIsNone(ret) + + def test_limit(self): + # create the limit + + limit = self.system_admin_cloud.identity.create_limit( + resource_name=self.resource_name, + service_id=self.service.id, + project_id=self.project.id, + resource_limit=50, + ) + self.addCleanup(self._delete_limit, limit) + self.assertIsInstance(limit, _limit.Limit) + self.assertIsNotNone(limit.id) + self.assertIsNone(limit.description) + self.assertEqual(self.service.id, limit.service_id) + self.assertEqual(self.project.id, limit.project_id) + self.assertEqual(50, limit.resource_limit) + + # update the limit + + limit = self.system_admin_cloud.identity.update_limit( + limit, description=self.limit_description + ) + self.assertIsInstance(limit, _limit.Limit) + self.assertEqual(self.limit_description, limit.description) + + # retrieve details of the (updated) limit by ID + + limit = self.system_admin_cloud.identity.get_limit(limit.id) + self.assertIsInstance(limit, _limit.Limit) + self.assertEqual(self.limit_description, limit.description) + + # (there's no name, so no way to retrieve by name) + + # list all limits + + limits = list(self.system_admin_cloud.identity.limits()) + self.assertIsInstance(limits[0], _limit.Limit) + self.assertIn( + self.resource_name, + {x.resource_name for x in limits}, + ) diff --git a/openstack/tests/functional/identity/v3/test_projects.py b/openstack/tests/functional/identity/v3/test_project.py similarity index 100% rename from openstack/tests/functional/identity/v3/test_projects.py rename to openstack/tests/functional/identity/v3/test_project.py diff --git a/openstack/tests/functional/identity/v3/test_registered_limit.py b/openstack/tests/functional/identity/v3/test_registered_limit.py new file mode 100644 index 000000000..6701cfc15 --- /dev/null +++ b/openstack/tests/functional/identity/v3/test_registered_limit.py @@ -0,0 +1,111 @@ +# 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.identity.v3 import registered_limit as _registered_limit +from openstack.tests.functional import base + + +class TestRegisteredLimit(base.BaseFunctionalTest): + def setUp(self): + super().setUp() + + self.region_name = self.getUniqueString('region') + self.region = self.system_admin_cloud.identity.create_region( + name=self.region_name + ) + self.addCleanup( + self.system_admin_cloud.identity.delete_region, self.region + ) + + self.service_name = self.getUniqueString('service') + self.service_type = self.getUniqueString('type') + self.service = self.system_admin_cloud.identity.create_service( + name=self.service_name, + type=self.service_type, + ) + self.addCleanup( + self.system_admin_cloud.identity.delete_service, self.service + ) + + self.resource_name = self.getUniqueString('resource') + self.registered_limit_description = self.getUniqueString( + 'registered_limit' + ) + + def _delete_registered_limit(self, registered_limit): + ret = self.system_admin_cloud.identity.delete_registered_limit( + registered_limit + ) + self.assertIsNone(ret) + + def test_registered_limit(self): + # create the registered limit + + registered_limit = ( + self.system_admin_cloud.identity.create_registered_limit( + resource_name=self.resource_name, + service_id=self.service.id, + region_id=self.region.id, + default_limit=10, + ) + ) + self.addCleanup(self._delete_registered_limit, registered_limit) + self.assertIsInstance( + registered_limit, _registered_limit.RegisteredLimit + ) + self.assertIsNotNone(registered_limit.id) + self.assertIsNone(registered_limit.description) + self.assertEqual(self.service.id, registered_limit.service_id) + self.assertEqual(self.region.id, registered_limit.region_id) + + # update the registered limit + + registered_limit = ( + self.system_admin_cloud.identity.update_registered_limit( + registered_limit, description=self.registered_limit_description + ) + ) + self.assertIsInstance( + registered_limit, _registered_limit.RegisteredLimit + ) + self.assertEqual( + self.registered_limit_description, registered_limit.description + ) + + # retrieve details of the (updated) registered limit by ID + + registered_limit = ( + self.system_admin_cloud.identity.get_registered_limit( + registered_limit.id + ) + ) + self.assertIsInstance( + registered_limit, _registered_limit.RegisteredLimit + ) + self.assertEqual( + self.registered_limit_description, registered_limit.description + ) + + # (there's no name, so no way to retrieve by name) + + # list all registered limits + + registered_limits = list( + self.system_admin_cloud.identity.registered_limits() + ) + self.assertIsInstance( + registered_limits[0], _registered_limit.RegisteredLimit + ) + self.assertIn( + self.resource_name, + {x.resource_name for x in registered_limits}, + ) From 6e5e7713630387493da04feadcae436b11557446 Mon Sep 17 00:00:00 2001 From: Konrad Gube Date: Mon, 13 Mar 2023 12:17:39 +0100 Subject: [PATCH 3698/3836] Add os-extend_volume_completion volume action. Change-Id: I5a42092f41a140ba850104017132fbc08c8183f0 --- openstack/block_storage/v3/_proxy.py | 13 ++++++++++ openstack/block_storage/v3/volume.py | 7 ++++- .../tests/unit/block_storage/v3/test_proxy.py | 16 ++++++++++++ .../unit/block_storage/v3/test_volume.py | 26 +++++++++++++++++-- ...e-completion-support-712217dafff8ce28.yaml | 4 +++ 5 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/add-extend-volume-completion-support-712217dafff8ce28.yaml diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 86435d35a..b7cfef0c6 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -876,6 +876,19 @@ def extend_volume(self, volume, size): volume = self._get_resource(_volume.Volume, volume) volume.extend(self, size) + def complete_volume_extend(self, volume, error=False): + """Complete a volume extend operation. + + :param volume: The value can be either the ID of a volume or a + :class:`~openstack.block_storage.v3.volume.Volume` instance. + :param bool error: Used to indicate if an error has occured that + requires Cinder to roll back the extend operation. + + :returns: None + """ + volume = self._get_resource(_volume.Volume, volume) + volume.complete_extend(self, error) + def set_volume_readonly(self, volume, readonly=True): """Set a volume's read-only flag. diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index 4f756255a..380346cda 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -111,7 +111,7 @@ class Volume(resource.Resource, metadata.MetadataMixin): #: The name of the associated volume type. volume_type = resource.Body("volume_type") - _max_microversion = "3.60" + _max_microversion = "3.71" def _action(self, session, body, microversion=None): """Preform volume actions given the message body.""" @@ -130,6 +130,11 @@ def extend(self, session, size): body = {'os-extend': {'new_size': size}} self._action(session, body) + def complete_extend(self, session, error=False): + """Complete volume extend operation""" + body = {'os-extend_volume_completion': {'error': error}} + self._action(session, body) + def set_bootable_status(self, session, bootable=True): """Set volume bootable status flag""" body = {'os-set_bootable': {'bootable': bootable}} diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index cf17c3654..e4d289227 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -454,6 +454,22 @@ def test_volume_extend(self): expected_args=[self.proxy, "new-size"], ) + def test_complete_extend(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.complete_extend", + self.proxy.complete_volume_extend, + method_args=["value"], + expected_args=[self.proxy, False], + ) + + def test_complete_extend_error(self): + self._verify( + "openstack.block_storage.v3.volume.Volume.complete_extend", + self.proxy.complete_volume_extend, + method_args=["value", True], + expected_args=[self.proxy, True], + ) + def test_volume_set_readonly_no_argument(self): self._verify( "openstack.block_storage.v3.volume.Volume.set_readonly", diff --git a/openstack/tests/unit/block_storage/v3/test_volume.py b/openstack/tests/unit/block_storage/v3/test_volume.py index 89a620bd8..5ee01ec73 100644 --- a/openstack/tests/unit/block_storage/v3/test_volume.py +++ b/openstack/tests/unit/block_storage/v3/test_volume.py @@ -76,7 +76,7 @@ def setUp(self): self.resp.status_code = 200 self.resp.json = mock.Mock(return_value=self.resp.body) self.sess = mock.Mock(spec=adapter.Adapter) - self.sess.default_microversion = '3.60' + self.sess.default_microversion = '3.71' self.sess.post = mock.Mock(return_value=self.resp) self.sess._get_connection = mock.Mock(return_value=self.cloud) @@ -164,6 +164,28 @@ def test_extend(self): url, json=body, microversion=sot._max_microversion ) + def test_complete_extend(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.complete_extend(self.sess)) + + url = f'volumes/{FAKE_ID}/action' + body = {'os-extend_volume_completion': {'error': False}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion + ) + + def test_complete_extend_error(self): + sot = volume.Volume(**VOLUME) + + self.assertIsNone(sot.complete_extend(self.sess, error=True)) + + url = f'volumes/{FAKE_ID}/action' + body = {'os-extend_volume_completion': {'error': True}} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion + ) + def test_set_volume_readonly(self): sot = volume.Volume(**VOLUME) @@ -661,7 +683,7 @@ def test_create_scheduler_hints(self): self.sess.post.assert_called_with( url, json=body, - microversion='3.60', + microversion=sot._max_microversion, headers={}, params={}, ) diff --git a/releasenotes/notes/add-extend-volume-completion-support-712217dafff8ce28.yaml b/releasenotes/notes/add-extend-volume-completion-support-712217dafff8ce28.yaml new file mode 100644 index 000000000..665b92da1 --- /dev/null +++ b/releasenotes/notes/add-extend-volume-completion-support-712217dafff8ce28.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Added support for the extend volume completion action. From fbeb5eae181764971657822637acb4d1c4883105 Mon Sep 17 00:00:00 2001 From: songwenping Date: Tue, 16 May 2023 17:02:25 +0800 Subject: [PATCH 3699/3836] Adding basic implementation for Accelerator attribute resource This patch supports Cyborg attribute resource APIs. Change-Id: Ie098e0421469a33b52893ea83fbedeba14903206 --- doc/source/user/proxies/accelerator.rst | 27 +++++++--- .../user/resources/accelerator/index.rst | 6 +-- .../resources/accelerator/v2/attribute.rst | 12 +++++ .../resources/accelerator/v2/deployable.rst | 2 +- .../user/resources/accelerator/v2/device.rst | 4 +- .../accelerator/v2/device_profile.rst | 2 +- openstack/accelerator/v2/_proxy.py | 50 +++++++++++++++++++ openstack/accelerator/v2/attribute.py | 37 ++++++++++++++ .../unit/accelerator/v2/test_attribute.py | 45 +++++++++++++++++ .../tests/unit/accelerator/v2/test_proxy.py | 28 +++++++++++ ...r-attributes-support-492cae3594272818.yaml | 5 ++ 11 files changed, 204 insertions(+), 14 deletions(-) create mode 100644 doc/source/user/resources/accelerator/v2/attribute.rst create mode 100644 openstack/accelerator/v2/attribute.py create mode 100644 openstack/tests/unit/accelerator/v2/test_attribute.py create mode 100644 releasenotes/notes/add-accelerator-attributes-support-492cae3594272818.yaml diff --git a/doc/source/user/proxies/accelerator.rst b/doc/source/user/proxies/accelerator.rst index 38e4da488..d47933a4c 100644 --- a/doc/source/user/proxies/accelerator.rst +++ b/doc/source/user/proxies/accelerator.rst @@ -10,20 +10,19 @@ The accelerator high-level interface is available through the ``accelerator`` member of a :class:`~openstack.connection.Connection` object. The ``accelerator`` member will only be added if the service is detected. - -Device Operations -^^^^^^^^^^^^^^^^^ +Deployable Operations +^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.accelerator.v2._proxy.Proxy :noindex: - :members: devices, get_device + :members: deployables, get_deployable, update_deployable -Deployable Operations -^^^^^^^^^^^^^^^^^^^^^ +Device Operations +^^^^^^^^^^^^^^^^^ .. autoclass:: openstack.accelerator.v2._proxy.Proxy :noindex: - :members: deployables, get_deployable, update_deployable + :members: devices, get_device Device Profile Operations ^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -41,3 +40,17 @@ Accelerator Request Operations :members: accelerator_requests, get_accelerator_request, create_accelerator_request, delete_accelerator_request, update_accelerator_request + +Attribute Operations +^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.accelerator.v2._proxy.Proxy + :noindex: + :members: attributes, create_attribute, delete_attribute, get_attribute + +Helpers +^^^^^^^ + +.. autoclass:: openstack.accelerator.v2._proxy.Proxy + :noindex: + :members: wait_for_status, wait_for_delete diff --git a/doc/source/user/resources/accelerator/index.rst b/doc/source/user/resources/accelerator/index.rst index 5e09cf616..74bd93df6 100644 --- a/doc/source/user/resources/accelerator/index.rst +++ b/doc/source/user/resources/accelerator/index.rst @@ -4,8 +4,8 @@ Accelerator v2 Resources .. toctree:: :maxdepth: 1 - v2/device + v2/attribute + v2/accelerator_request v2/deployable + v2/device v2/device_profile - v2/accelerator_request - diff --git a/doc/source/user/resources/accelerator/v2/attribute.rst b/doc/source/user/resources/accelerator/v2/attribute.rst new file mode 100644 index 000000000..92ac0efda --- /dev/null +++ b/doc/source/user/resources/accelerator/v2/attribute.rst @@ -0,0 +1,12 @@ +openstack.accelerator.v2.attribute +================================== + +.. automodule:: openstack.accelerator.v2.attribute + +The Attribute Class +------------------- + +The ``Attribute`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.accelerator.v2.attribute.Attribute + :members: diff --git a/doc/source/user/resources/accelerator/v2/deployable.rst b/doc/source/user/resources/accelerator/v2/deployable.rst index 383795c2a..4044c261d 100644 --- a/doc/source/user/resources/accelerator/v2/deployable.rst +++ b/doc/source/user/resources/accelerator/v2/deployable.rst @@ -1,5 +1,5 @@ openstack.accelerator.v2.deployable -============================================ +=================================== .. automodule:: openstack.accelerator.v2.deployable diff --git a/doc/source/user/resources/accelerator/v2/device.rst b/doc/source/user/resources/accelerator/v2/device.rst index 943743546..97e370c43 100644 --- a/doc/source/user/resources/accelerator/v2/device.rst +++ b/doc/source/user/resources/accelerator/v2/device.rst @@ -1,10 +1,10 @@ openstack.accelerator.v2.device -============================================ +=============================== .. automodule:: openstack.accelerator.v2.device The Device Class --------------------- +---------------- The ``Device`` class inherits from :class:`~openstack.resource.Resource`. diff --git a/doc/source/user/resources/accelerator/v2/device_profile.rst b/doc/source/user/resources/accelerator/v2/device_profile.rst index 9849c7833..71e05d194 100644 --- a/doc/source/user/resources/accelerator/v2/device_profile.rst +++ b/doc/source/user/resources/accelerator/v2/device_profile.rst @@ -1,5 +1,5 @@ openstack.accelerator.v2.device_profile -============================================ +======================================= .. automodule:: openstack.accelerator.v2.device_profile diff --git a/openstack/accelerator/v2/_proxy.py b/openstack/accelerator/v2/_proxy.py index b30c360f0..801198d47 100644 --- a/openstack/accelerator/v2/_proxy.py +++ b/openstack/accelerator/v2/_proxy.py @@ -13,6 +13,7 @@ import typing as ty from openstack.accelerator.v2 import accelerator_request as _arq +from openstack.accelerator.v2 import attribute as _attribute from openstack.accelerator.v2 import deployable as _deployable from openstack.accelerator.v2 import device as _device from openstack.accelerator.v2 import device_profile as _device_profile @@ -204,6 +205,55 @@ def update_accelerator_request(self, uuid, properties): self, properties ) + # ========== Attributes ========== + + def attributes(self, **query): + """Retrieve a generator of attributes. + + :param kwargs query: Optional query parameters to be sent to + restrict the attributes to be returned. + :returns: A generator of attribute instances. + """ + return self._list(_attribute.Attribute, **query) + + def create_attribute(self, **attrs): + """Create a attribute. + + :param kwargs attrs: a list of attributes. + :returns: The list of created attributes + """ + return self._create(_attribute.Attribute, **attrs) + + def delete_attribute(self, attribute, ignore_missing=True): + """Delete a attribute + + :param attribute: The value can be either the ID of a attributes or a + :class:`~openstack.accelerator.v2.attribute.Attributes` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the device profile does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent device profile. + :returns: ``None`` + """ + return self._delete( + _attribute.Attribute, + attribute, + ignore_missing=ignore_missing, + ) + + def get_attribute(self, uuid, fields=None): + """Get a single device profile. + + :param uuid: The value can be the UUID of a attribute. + :returns: One :class: + `~openstack.accelerator.v2.attribute.Attribute` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + device profile matching the criteria could be found. + """ + return self._get(_attribute.Attribute, uuid) + # ========== Utilities ========== def wait_for_status( diff --git a/openstack/accelerator/v2/attribute.py b/openstack/accelerator/v2/attribute.py new file mode 100644 index 000000000..f27273c9c --- /dev/null +++ b/openstack/accelerator/v2/attribute.py @@ -0,0 +1,37 @@ +# 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 resource + + +class Attribute(resource.Resource): + resource_key = 'attribute' + resources_key = 'attributes' + base_path = '/attributes' + # capabilities + allow_create = True + allow_fetch = True + allow_commit = False + allow_delete = True + allow_list = True + + #: The timestamp when this attribute was created. + created_at = resource.Body('created_at') + #: The deployable_id of the attribute + deployable_id = resource.Body('deployable_id') + #: The key of the attribute + key = resource.Body('key') + #: The value of the attribute + value = resource.Body('value') + #: The timestamp when this attribute was updated. + updated_at = resource.Body('updated_at') + #: The uuid of the attribute + uuid = resource.Body('uuid', alternate_id=True) diff --git a/openstack/tests/unit/accelerator/v2/test_attribute.py b/openstack/tests/unit/accelerator/v2/test_attribute.py new file mode 100644 index 000000000..381c57678 --- /dev/null +++ b/openstack/tests/unit/accelerator/v2/test_attribute.py @@ -0,0 +1,45 @@ +# 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.accelerator.v2 import attribute +from openstack.tests.unit import base + + +FAKE = { + "id": 1, + "uuid": "a95e10ae-b3e3-4eab-a513-1afae6f17c51", + "deployable_id": 1, + "key": "traits1", + 'value': 'CUSTOM_FAKE_DEVICE', +} + + +class TestAttribute(base.TestCase): + def test_basic(self): + sot = attribute.Attribute() + self.assertEqual('attribute', sot.resource_key) + self.assertEqual('attributes', sot.resources_key) + self.assertEqual('/attributes', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertFalse(sot.allow_patch) + + def test_make_it(self): + sot = attribute.Attribute(**FAKE) + self.assertEqual(FAKE['id'], sot.id) + self.assertEqual(FAKE['uuid'], sot.uuid) + self.assertEqual(FAKE['deployable_id'], sot.deployable_id) + self.assertEqual(FAKE['key'], sot.key) + self.assertEqual(FAKE['value'], sot.value) diff --git a/openstack/tests/unit/accelerator/v2/test_proxy.py b/openstack/tests/unit/accelerator/v2/test_proxy.py index bd2bbdd43..c5823421b 100644 --- a/openstack/tests/unit/accelerator/v2/test_proxy.py +++ b/openstack/tests/unit/accelerator/v2/test_proxy.py @@ -12,6 +12,7 @@ from openstack.accelerator.v2 import _proxy from openstack.accelerator.v2 import accelerator_request +from openstack.accelerator.v2 import attribute from openstack.accelerator.v2 import deployable from openstack.accelerator.v2 import device_profile from openstack.tests.unit import test_proxy_base as test_proxy_base @@ -91,3 +92,30 @@ def test_get_accelerator_request(self): self.proxy.get_accelerator_request, accelerator_request.AcceleratorRequest, ) + + +class TestAttribute(TestAcceleratorProxy): + def test_list_attribute(self): + self.verify_list( + self.proxy.attributes, + attribute.Attribute, + ) + + def test_create_attribute(self): + self.verify_create( + self.proxy.create_attribute, + attribute.Attribute, + ) + + def test_delete_attribute(self): + self.verify_delete( + self.proxy.delete_attribute, + attribute.Attribute, + False, + ) + + def test_get_attribute(self): + self.verify_get( + self.proxy.get_attribute, + attribute.Attribute, + ) diff --git a/releasenotes/notes/add-accelerator-attributes-support-492cae3594272818.yaml b/releasenotes/notes/add-accelerator-attributes-support-492cae3594272818.yaml new file mode 100644 index 000000000..c0f9ec50d --- /dev/null +++ b/releasenotes/notes/add-accelerator-attributes-support-492cae3594272818.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Support for the attributes API of the Accelerator service (Cyborg) has + been added. From 873e5a899e4a92bfc53a9e8a2c1b77d0cc551651 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 4 Apr 2025 14:10:15 +0100 Subject: [PATCH 3700/3836] Add virtual media attachment and detachment support This change introduces methods for attaching and detaching virtual media devices to/from nodes, enhancing the capabilities of the baremetal API. Changes include: - Added `VMEDIA_VERSION` constant in `_common.py` for API versioning. - Introduced `attach_vmedia` and `detach_vmedia` methods in the `Node` class. - Added `attach_vmedia_to_node` and `detach_vmedia_from_node` methods in the `Proxy` class. - Added unit and functional test for the features. Change-Id: Id41d45ad78b07f8ce9cca92444e1603f3882fe53 --- doc/source/user/proxies/baremetal.rst | 6 ++ openstack/baremetal/v1/_common.py | 3 + openstack/baremetal/v1/_proxy.py | 49 ++++++++++ openstack/baremetal/v1/node.py | 96 ++++++++++++++++++- .../baremetal/test_baremetal_node.py | 32 +++++++ .../tests/unit/baremetal/v1/test_node.py | 86 +++++++++++++++++ .../add-vmedia-support-20494ed415e5b32b.yaml | 4 + 7 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-vmedia-support-20494ed415e5b32b.yaml diff --git a/doc/source/user/proxies/baremetal.rst b/doc/source/user/proxies/baremetal.rst index 557c8d4eb..f30beae15 100644 --- a/doc/source/user/proxies/baremetal.rst +++ b/doc/source/user/proxies/baremetal.rst @@ -58,6 +58,12 @@ Chassis Operations :members: chassis, find_chassis, get_chassis, create_chassis, update_chassis, patch_chassis, delete_chassis +Virtual Media Operations +^^^^^^^^^^^^^^^^^^^^^^^^ +.. autoclass:: openstack.baremetal.v1._proxy.Proxy + :noindex: + :members: attach_vmedia_to_node, detach_vmedia_from_node + VIF Operations ^^^^^^^^^^^^^^ .. autoclass:: openstack.baremetal.v1._proxy.Proxy diff --git a/openstack/baremetal/v1/_common.py b/openstack/baremetal/v1/_common.py index cac214c6b..da8a6c4ed 100644 --- a/openstack/baremetal/v1/_common.py +++ b/openstack/baremetal/v1/_common.py @@ -95,6 +95,9 @@ FIRMWARE_VERSION = '1.86' """API version in which firmware components of a node can be accessed""" +VMEDIA_VERSION = '1.89' +"""API version in which the virtual media operations were introduced.""" + RUNBOOKS_VERSION = '1.92' """API version in which a runbook can be used in place of arbitrary steps for provisioning""" diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 5a86066e6..3661354b4 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -1087,6 +1087,55 @@ def delete_port_group(self, port_group, ignore_missing=True): _portgroup.PortGroup, port_group, ignore_missing=ignore_missing ) + # ========== Virtual Media ========== + + def attach_vmedia_to_node( + self, + node, + device_type, + image_url, + image_download_source=None, + retry_on_conflict=True, + ): + """Attach virtual media device to a node. + + :param node: The value can be either the name or ID of a node or + a :class:`~openstack.baremetal.v1.node.Node` instance. + :param device_type: The type of virtual media device. + :param image_url: The URL of the image to attach. + :param image_download_source: The source of the image download. + :param retry_on_conflict: Whether to retry HTTP CONFLICT errors. + This can happen when either the virtual media is already used on + a node or the node is locked. Since the latter happens more often, + the default value is True. + :return: ``None`` + :raises: :exc:`~openstack.exceptions.NotSupported` if the server + does not support the VMEDIA API. + """ + res = self._get_resource(_node.Node, node) + res.attach_vmedia( + self, + device_type=device_type, + image_url=image_url, + image_download_source=image_download_source, + retry_on_conflict=retry_on_conflict, + ) + + def detach_vmedia_from_node(self, node, device_types=None): + """Detach virtual media from the node. + + :param node: The value can be either the name or ID of a node or + a :class:`~openstack.baremetal.v1.node.Node` instance. + :param device_types: A list with the types of virtual media + devices to detach. + :return: ``True`` if the virtual media was detached, + otherwise ``False``. + :raises: :exc:`~openstack.exceptions.NotSupported` if the server + does not support the VMEDIA API. + """ + res = self._get_resource(_node.Node, node) + return res.detach_vmedia(self, device_types=device_types) + # ========== VIFs ========== def attach_vif_to_node( diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 8debcf8b5..c6b2d12ec 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -212,7 +212,7 @@ class Node(_common.Resource): #: unit of a specific type of resource. Added in API microversion 1.21. resource_class = resource.Body("resource_class") #: A string representing the current service step being executed upon. - #: Added in API microversion 1.87. + #: Added in API microversion 1.89. service_step = resource.Body("service_step") #: A string representing the uuid or logical name of a runbook as an #: alternative to providing ``clean_steps`` or ``service_steps``. @@ -812,6 +812,100 @@ def set_power_state(self, session, target, wait=False, timeout=None): if wait: self.wait_for_power_state(session, expected, timeout=timeout) + def attach_vmedia( + self, + session, + device_type, + image_url, + image_download_source=None, + retry_on_conflict=True, + ): + """Attach virtual media device to a node. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param device_type: The type of virtual media device. + :param image_url: The URL of the image to attach. + :param image_download_source: The source of the image download. + :param retry_on_conflict: Whether to retry HTTP CONFLICT errors. + This can happen when either the virtual media is already used on + a node or the node is locked. Since the latter happens more often, + the default value is True. + :return: ``None`` + :raises: :exc:`~openstack.exceptions.NotSupported` if the server + does not support the VMEDIA API. + + """ + session = self._get_session(session) + version = self._assert_microversion_for( + session, + _common.VMEDIA_VERSION, + error_message=("Cannot use virtual media API"), + ) + # Prepare the request and create the request body + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'vmedia') + body = {"device_type": device_type, "image_url": image_url} + if image_download_source: + body["image_download_source"] = image_download_source + retriable_status_codes = _common.RETRIABLE_STATUS_CODES + if not retry_on_conflict: + retriable_status_codes = list(set(retriable_status_codes) - {409}) + response = session.post( + request.url, + json=body, + headers=request.headers, + microversion=version, + retriable_status_codes=retriable_status_codes, + ) + + msg = f"Failed to attach Virtual Media to bare metal node {self.id}" + exceptions.raise_from_response(response, error_message=msg) + + def detach_vmedia(self, session, device_types=None): + """Detach virtual media from a node + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param device_types: A list with the types of virtual media + devices to detach. + :return: ``True`` if the virtual media was detached, + otherwise ``False``. + :raises: :exc:`~openstack.exceptions.NotSupported` if the server + does not support the VMEDIA API + """ + session = self._get_session(session) + version = self._assert_microversion_for( + session, + _common.VMEDIA_VERSION, + error_message=("Cannot use virtual media API"), + ) + + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'vmedia') + + delete_kwargs = { + 'headers': request.headers, + 'microversion': version, + 'retriable_status_codes': _common.RETRIABLE_STATUS_CODES, + } + + if device_types: + delete_kwargs['json'] = { + 'device_types': _common.comma_separated_list(device_types) + } + + response = session.delete(request.url, **delete_kwargs) + + if response.status_code == 400: + session.log.debug( + "Virtual media doesn't exist for node %(node)s", + {'node': self.id}, + ) + + msg = f"Failed to detach virtual media from bare metal node {self.id}" + exceptions.raise_from_response(response, error_message=msg) + def attach_vif( self, session: adapter.Adapter, diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index 92bbd68ff..54fc7c9c9 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -467,6 +467,38 @@ def test_node_vif_negative(self): ) +class TestBareMetalVirtualMedia(base.BaseBaremetalTest): + min_microversion = '1.89' + + def setUp(self): + super().setUp() + self.node = self.create_node(network_interface='noop') + self.device_type = "CDROM" + self.image_url = "http://image" + + def test_node_vmedia_attach_detach(self): + self.conn.baremetal.attach_vmedia_to_node( + self.node, self.device_type, self.image_url + ) + res = self.conn.baremetal.detach_vmedia_from_node(self.node) + self.assertNone(res) + + def test_node_vmedia_negative(self): + uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.attach_vmedia_to_node, + uuid, + self.device_type, + self.image_url, + ) + self.assertRaises( + exceptions.ResourceNotFound, + self.conn.baremetal.detach_vmedia_from_node, + uuid, + ) + + class TestTraits(base.BaseBaremetalTest): min_microversion = '1.37' diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 008559fe8..f1300c275 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -736,6 +736,92 @@ def test_incompatible_microversion_optional_params(self): ) +@mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) +@mock.patch.object(node.Node, '_get_session', lambda self, x: x) +class TestNodeVmedia(base.TestCase): + def setUp(self): + super().setUp() + self.session = mock.Mock(spec=adapter.Adapter) + self.session.default_microversion = '1.89' + self.session.log = mock.Mock() + self.node = node.Node( + id='c29db401-b6a7-4530-af8e-20a720dee946', driver=FAKE['driver'] + ) + self.device_type = "CDROM" + self.image_url = "http://image" + + def test_attach_vmedia(self): + self.assertIsNone( + self.node.attach_vmedia( + self.session, self.device_type, self.image_url + ) + ) + self.session.post.assert_called_once_with( + f'nodes/{self.node.id}/vmedia', + json={ + 'device_type': self.device_type, + 'image_url': self.image_url, + }, + headers=mock.ANY, + microversion='1.89', + retriable_status_codes=[409, 503], + ) + + def test_attach_vmedia_no_retries(self): + self.assertIsNone( + self.node.attach_vmedia( + self.session, + self.device_type, + self.image_url, + retry_on_conflict=False, + ) + ) + self.session.post.assert_called_once_with( + f'nodes/{self.node.id}/vmedia', + json={ + 'device_type': self.device_type, + 'image_url': self.image_url, + }, + headers=mock.ANY, + microversion='1.89', + retriable_status_codes=[503], + ) + + def test_detach_vmedia_existing(self): + self.assertIsNone(self.node.detach_vmedia(self.session)) + self.session.delete.assert_called_once_with( + f'nodes/{self.node.id}/vmedia', + headers=mock.ANY, + microversion='1.89', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) + + def test_detach_vmedia_missing(self): + self.session.delete.return_value.status_code = 400 + self.assertIsNone(self.node.detach_vmedia(self.session)) + self.session.delete.assert_called_once_with( + f'nodes/{self.node.id}/vmedia', + headers=mock.ANY, + microversion='1.89', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES, + ) + + def test_incompatible_microversion(self): + self.session.default_microversion = '1.1' + self.assertRaises( + exceptions.NotSupported, + self.node.attach_vmedia, + self.session, + self.device_type, + self.image_url, + ) + self.assertRaises( + exceptions.NotSupported, + self.node.detach_vmedia, + self.session, + ) + + @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) @mock.patch.object(node.Node, '_get_session', lambda self, x: x) class TestNodeValidate(base.TestCase): diff --git a/releasenotes/notes/add-vmedia-support-20494ed415e5b32b.yaml b/releasenotes/notes/add-vmedia-support-20494ed415e5b32b.yaml new file mode 100644 index 000000000..346953282 --- /dev/null +++ b/releasenotes/notes/add-vmedia-support-20494ed415e5b32b.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Implements virtual media attach/detach API for bare metal nodes. From 32388ccc9987c116025859bd8c425c91b8f2164c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Sun, 30 Jun 2024 14:39:15 +0100 Subject: [PATCH 3701/3836] pre-commit: Enable bandit checks Change-Id: Ic991a656785d27626fd9e5f86577d138b4df07ae Signed-off-by: Stephen Finucane --- examples/image/download.py | 2 +- openstack/baremetal/configdrive.py | 5 ++--- openstack/cloud/_baremetal.py | 4 +++- openstack/cloud/_compute.py | 3 ++- openstack/cloud/_network_common.py | 3 ++- openstack/cloud/meta.py | 5 +++-- openstack/config/vendors/__init__.py | 2 +- openstack/exceptions.py | 3 ++- openstack/orchestration/util/environment_format.py | 2 +- openstack/orchestration/util/template_format.py | 2 +- openstack/orchestration/util/template_utils.py | 4 ++-- openstack/orchestration/util/utils.py | 2 +- openstack/resource.py | 6 +++++- openstack/test/fakes.py | 6 +++--- pyproject.toml | 10 +++++++++- 15 files changed, 38 insertions(+), 21 deletions(-) diff --git a/examples/image/download.py b/examples/image/download.py index 791a65235..e62804431 100644 --- a/examples/image/download.py +++ b/examples/image/download.py @@ -30,7 +30,7 @@ def download_image_stream(conn): # and in your own code, you are now responsible for checking # the integrity of the data. Create an MD5 has to be computed # after all of the data has been consumed. - md5 = hashlib.md5() + md5 = hashlib.md5(usedforsecurity=False) with open("myimage.qcow2", "wb") as local_image: response = conn.image.download_image(image, stream=True) diff --git a/openstack/baremetal/configdrive.py b/openstack/baremetal/configdrive.py index ebfa63cdc..e7458804b 100644 --- a/openstack/baremetal/configdrive.py +++ b/openstack/baremetal/configdrive.py @@ -110,11 +110,10 @@ def pack(path: str) -> str: with tempfile.NamedTemporaryFile() as tmpfile: # NOTE(toabctl): Luckily, genisoimage, mkisofs and xorrisofs understand # the same parameters which are currently used. - cmds = ['genisoimage', 'mkisofs', 'xorrisofs'] error: ty.Optional[Exception] - for c in cmds: + for c in ['genisoimage', 'mkisofs', 'xorrisofs']: try: - p = subprocess.Popen( + p = subprocess.Popen( # noqa: S603 [ c, '-o', diff --git a/openstack/cloud/_baremetal.py b/openstack/cloud/_baremetal.py index 1f06dccd6..bdbf295bb 100644 --- a/openstack/cloud/_baremetal.py +++ b/openstack/cloud/_baremetal.py @@ -266,7 +266,9 @@ def register_machine( for uuid in created_nics: try: self.baremetal.delete_port(uuid) - except Exception: + except Exception: # noqa: S110 + # the port might not have been actually created, so a + # failure to delete isn't necessarily an issue pass raise diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 650c29cb7..a0be0f459 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -1247,7 +1247,8 @@ def wait_for_server( ): try: server = self.get_server(server_id) - except Exception: + except Exception: # noqa: S112 + # if it hasn't appeared yet, that's okay continue if not server: continue diff --git a/openstack/cloud/_network_common.py b/openstack/cloud/_network_common.py index 99ebe0495..3ff6f5be2 100644 --- a/openstack/cloud/_network_common.py +++ b/openstack/cloud/_network_common.py @@ -1566,7 +1566,8 @@ def _nat_destination_port( for address in port.get('fixed_ips', list()): try: ip = ipaddress.ip_address(address['ip_address']) - except Exception: + except Exception: # noqa: S112 + # the address might be unset; ignore if so continue if ip.version == 4: fixed_address = address['ip_address'] diff --git a/openstack/cloud/meta.py b/openstack/cloud/meta.py index 79bba5173..224f74597 100644 --- a/openstack/cloud/meta.py +++ b/openstack/cloud/meta.py @@ -220,7 +220,7 @@ def get_server_external_ipv4(cloud, server): for interface in interfaces: try: ip = ipaddress.ip_address(interface['addr']) - except Exception: + except Exception: # noqa: S112 # Skip any error, we're looking for a working ip - if the # cloud returns garbage, it wouldn't be the first weird thing # but it still doesn't meet the requirement of "be a working @@ -268,7 +268,8 @@ def find_best_address(addresses, public=False, cloud_public=True): # will fail fast, but can often come alive # when retried. continue - except Exception: + except Exception: # noqa: S110 + # This is best effort. Ignore any errors. pass # Give up and return the first - none work as far as we can tell diff --git a/openstack/config/vendors/__init__.py b/openstack/config/vendors/__init__.py index 06e740fa6..af0d3cd3d 100644 --- a/openstack/config/vendors/__init__.py +++ b/openstack/config/vendors/__init__.py @@ -59,7 +59,7 @@ def get_profile(profile_name): scheme=profile_url.scheme, netloc=profile_url.netloc, ) - response = requests.get(well_known_url) + response = requests.get(well_known_url, timeout=10) if not response.ok: raise exceptions.ConfigException( f"{profile_name} is a remote profile that could not be fetched: " diff --git a/openstack/exceptions.py b/openstack/exceptions.py index 4b068399d..445b4ae16 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -202,7 +202,8 @@ def _extract_message(obj: ty.Any) -> ty.Optional[str]: # Ironic before Stein has double JSON encoding, nobody remembers why. try: obj = json.loads(obj) - except Exception: + except Exception: # noqa: S110 + # This is best effort. Ignore any errors. pass else: return _extract_message(obj) diff --git a/openstack/orchestration/util/environment_format.py b/openstack/orchestration/util/environment_format.py index f547550a5..8d14df442 100644 --- a/openstack/orchestration/util/environment_format.py +++ b/openstack/orchestration/util/environment_format.py @@ -39,7 +39,7 @@ def parse(env_str): YAML format. """ try: - env = yaml.load(env_str, Loader=template_format.yaml_loader) + env = yaml.load(env_str, Loader=template_format.yaml_loader) # noqa: S506 except yaml.YAMLError: # NOTE(prazumovsky): we need to return more informative error for # user, so use SafeLoader, which return error message with template diff --git a/openstack/orchestration/util/template_format.py b/openstack/orchestration/util/template_format.py index ab1844444..b08715bfe 100644 --- a/openstack/orchestration/util/template_format.py +++ b/openstack/orchestration/util/template_format.py @@ -52,7 +52,7 @@ def parse(tmpl_str): tpl = json.loads(tmpl_str) else: try: - tpl = yaml.load(tmpl_str, Loader=HeatYamlLoader) + tpl = yaml.load(tmpl_str, Loader=HeatYamlLoader) # noqa: S506 except yaml.YAMLError: # NOTE(prazumovsky): we need to return more informative error for # user, so use SafeLoader, which return error message with template diff --git a/openstack/orchestration/util/template_utils.py b/openstack/orchestration/util/template_utils.py index 0aec0bd02..43518e38f 100644 --- a/openstack/orchestration/util/template_utils.py +++ b/openstack/orchestration/util/template_utils.py @@ -39,7 +39,7 @@ def get_template_contents( template_url = utils.normalise_file_path_to_url(template_file) if template_url: - tpl = request.urlopen(template_url).read() + tpl = request.urlopen(template_url).read() # noqa: S310 elif template_object: is_object = True @@ -295,7 +295,7 @@ def process_environment_and_files( elif env_path: env_url = utils.normalise_file_path_to_url(env_path) env_base_url = utils.base_url_for_url(env_url) - raw_env = request.urlopen(env_url).read() + raw_env = request.urlopen(env_url).read() # noqa: S310 env = environment_format.parse(raw_env) diff --git a/openstack/orchestration/util/utils.py b/openstack/orchestration/util/utils.py index 7644d6fa8..5822d2642 100644 --- a/openstack/orchestration/util/utils.py +++ b/openstack/orchestration/util/utils.py @@ -38,7 +38,7 @@ def normalise_file_path_to_url(path): def read_url_content(url): try: # TODO(mordred) Use requests - content = request.urlopen(url).read() + content = request.urlopen(url).read() # noqa: S310 except error.URLError: raise exceptions.SDKException(f'Could not fetch contents for {url}') diff --git a/openstack/resource.py b/openstack/resource.py index 0b83ed696..47ffdde69 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1508,7 +1508,11 @@ def bulk_create( body: ty.Union[dict[str, ty.Any], list[ty.Any]] = _body if prepend_key: - assert cls.resources_key + if not cls.resources_key: + raise exceptions.ResourceFailure( + "Cannot request prepend_key with Unset resources key" + ) + body = {cls.resources_key: body} response = method( diff --git a/openstack/test/fakes.py b/openstack/test/fakes.py index a93a78d1d..055b28335 100644 --- a/openstack/test/fakes.py +++ b/openstack/test/fakes.py @@ -112,15 +112,15 @@ def generate_fake_resource( base_attrs[name] = uuid.uuid4().hex elif issubclass(target_type, int): # int - base_attrs[name] = random.randint(1, 100) + base_attrs[name] = random.randint(1, 100) # noqa: S311 elif issubclass(target_type, float): # float - base_attrs[name] = random.random() + base_attrs[name] = random.random() # noqa: S311 elif issubclass(target_type, bool) or issubclass( target_type, _format.BoolStr ): # bool - base_attrs[name] = random.choice([True, False]) + base_attrs[name] = random.choice([True, False]) # noqa: S311 elif issubclass(target_type, dict): # some dict - without further details leave it empty base_attrs[name] = dict() diff --git a/pyproject.toml b/pyproject.toml index 6b80e03f3..ec5d2d65c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,4 +58,12 @@ quote-style = "preserve" docstring-code-format = true [tool.ruff.lint] -select = ["E4", "E7", "E9", "F", "U"] +select = ["E4", "E7", "E9", "F", "S", "U"] +ignore = [ + # we only use asserts for type narrowing + "S101", +] + +[tool.ruff.lint.per-file-ignores] +"openstack/tests/*" = ["S"] +"examples/*" = ["S"] From 2440aa7e2dfb4a7e34165d316f5e5138f395468c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 4 Apr 2025 17:15:45 +0100 Subject: [PATCH 3702/3836] volume: Add Transfer to volume v2 API Change-Id: Icc83eb5e70bba89ff2b496e9aa01c160cf9e4270 Signed-off-by: Stephen Finucane --- doc/source/user/proxies/block_storage_v2.rst | 8 ++ doc/source/user/proxies/block_storage_v3.rst | 16 +-- .../resources/block_storage/v2/transfer.rst | 13 +++ openstack/block_storage/v2/_proxy.py | 103 ++++++++++++++++++ openstack/block_storage/v2/transfer.py | 59 ++++++++++ .../tests/unit/block_storage/v3/test_proxy.py | 34 ++++++ 6 files changed, 225 insertions(+), 8 deletions(-) create mode 100644 doc/source/user/resources/block_storage/v2/transfer.rst create mode 100644 openstack/block_storage/v2/transfer.py diff --git a/doc/source/user/proxies/block_storage_v2.rst b/doc/source/user/proxies/block_storage_v2.rst index 2cc73b43e..0ef865c28 100644 --- a/doc/source/user/proxies/block_storage_v2.rst +++ b/doc/source/user/proxies/block_storage_v2.rst @@ -72,6 +72,14 @@ Stats Operations :noindex: :members: backend_pools +Transfer Operations +^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v2._proxy.Proxy + :noindex: + :members: create_transfer, delete_transfer, find_transfer, + get_transfer, transfers, accept_transfer + Type Operations ^^^^^^^^^^^^^^^ diff --git a/doc/source/user/proxies/block_storage_v3.rst b/doc/source/user/proxies/block_storage_v3.rst index 1fa7c573b..16bd1aa02 100644 --- a/doc/source/user/proxies/block_storage_v3.rst +++ b/doc/source/user/proxies/block_storage_v3.rst @@ -141,6 +141,14 @@ Stats Operations :noindex: :members: backend_pools +Transfer Operations +^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v3._proxy.Proxy + :noindex: + :members: create_transfer, delete_transfer, find_transfer, + get_transfer, transfers, accept_transfer + Type Operations ^^^^^^^^^^^^^^^ @@ -152,14 +160,6 @@ Type Operations create_type_encryption, delete_type_encryption, update_type_encryption -Transfer Operations -^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: openstack.block_storage.v3._proxy.Proxy - :noindex: - :members: create_transfer, delete_transfer, find_transfer, - get_transfer, transfers, accept_transfer - Volume Operations ^^^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/block_storage/v2/transfer.rst b/doc/source/user/resources/block_storage/v2/transfer.rst new file mode 100644 index 000000000..e738c7529 --- /dev/null +++ b/doc/source/user/resources/block_storage/v2/transfer.rst @@ -0,0 +1,13 @@ +openstack.block_storage.v3.transfer +=================================== + +.. automodule:: openstack.block_storage.v3.transfer + +The Volume Transfer Class +------------------------- + +The ``Volume Transfer`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v3.transfer.Transfer + :members: diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 3484d2f60..15cf89c2d 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -22,12 +22,14 @@ from openstack.block_storage.v2 import service as _service from openstack.block_storage.v2 import snapshot as _snapshot from openstack.block_storage.v2 import stats as _stats +from openstack.block_storage.v2 import transfer as _transfer from openstack.block_storage.v2 import type as _type from openstack.block_storage.v2 import volume as _volume from openstack import exceptions from openstack.identity.v3 import project as _project from openstack import proxy from openstack import resource +from openstack import utils from openstack import warnings as os_warnings @@ -1223,6 +1225,107 @@ def delete_snapshot_metadata(self, snapshot, keys=None): else: snapshot.delete_metadata(self) + # ========== Transfers ========== + + def create_transfer(self, **attrs): + """Create a new Transfer record + + :param volume_id: The value is ID of the volume. + :param name: The value is name of the transfer + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.block_storage.v2.transfer.Transfer` + comprised of the properties on the Transfer class. + :returns: The results of Transfer creation + :rtype: :class:`~openstack.block_storage.v2.transfer.Transfer` + """ + return self._create(_transfer.Transfer, **attrs) + + def delete_transfer(self, transfer, ignore_missing=True): + """Delete a volume transfer + + :param transfer: The value can be either the ID of a transfer or a + :class:`~openstack.block_storage.v2.transfer.Transfer`` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.NotFoundException` will be + raised when the transfer does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent transfer. + + :returns: ``None`` + """ + self._delete( + _transfer.Transfer, + transfer, + ignore_missing=ignore_missing, + ) + + def find_transfer(self, name_or_id, ignore_missing=True): + """Find a single transfer + + :param name_or_id: The name or ID a transfer + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.NotFoundException` will be raised + when the volume transfer does not exist. + + :returns: One :class:`~openstack.block_storage.v2.transfer.Transfer` + :raises: :class:`~openstack.exceptions.NotFoundException` + when no resource can be found. + :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple + resources are found. + """ + return self._find( + _transfer.Transfer, + name_or_id, + ignore_missing=ignore_missing, + ) + + def get_transfer(self, transfer): + """Get a single transfer + + :param transfer: The value can be the ID of a transfer or a + :class:`~openstack.block_storage.v2.transfer.Transfer` + instance. + + :returns: One :class:`~openstack.block_storage.v2.transfer.Transfer` + :raises: :class:`~openstack.exceptions.NotFoundException` + when no resource can be found. + """ + return self._get(_transfer.Transfer, transfer) + + def transfers(self, *, details=True, all_projects=False, **query): + """Retrieve a generator of transfers + + :param bool details: When set to ``False`` no extended attributes + will be returned. The default, ``True``, will cause objects with + additional attributes to be returned. + :param bool all_projects: When set to ``True``, list transfers from + all projects. Admin-only by default. + :param kwargs query: Optional query parameters to be sent to limit + the transfers being returned. + + :returns: A generator of transfer objects. + """ + if all_projects: + query['all_projects'] = True + base_path = '/volume-transfers' + if details: + base_path = utils.urljoin(base_path, 'detail') + return self._list(_transfer.Transfer, base_path=base_path, **query) + + def accept_transfer(self, transfer_id, auth_key): + """Accept a Transfer + + :param transfer_id: The value can be the ID of a transfer or a + :class:`~openstack.block_storage.v2.transfer.Transfer` + instance. + :param auth_key: The key to authenticate volume transfer. + + :returns: The results of Transfer creation + :rtype: :class:`~openstack.block_storage.v2.transfer.Transfer` + """ + transfer = self._get_resource(_transfer.Transfer, transfer_id) + return transfer.accept(self, auth_key=auth_key) + # ========== Utilities ========== def wait_for_status( diff --git a/openstack/block_storage/v2/transfer.py b/openstack/block_storage/v2/transfer.py new file mode 100644 index 000000000..d017a6643 --- /dev/null +++ b/openstack/block_storage/v2/transfer.py @@ -0,0 +1,59 @@ +# 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 import resource +from openstack import utils + + +class Transfer(resource.Resource): + resource_key = "transfer" + resources_key = "transfers" + base_path = "/os-volume-transfer" + + # capabilities + allow_create = True + allow_delete = True + allow_fetch = True + allow_list = True + + # Properties + #: UUID of the transfer. + id = resource.Body("id") + #: The date and time when the resource was created. + created_at = resource.Body("created_at") + #: Name of the volume to transfer. + name = resource.Body("name") + #: ID of the volume to transfer. + volume_id = resource.Body("volume_id") + #: Auth key for the transfer. + auth_key = resource.Body("auth_key") + #: A list of links associated with this volume. *Type: list* + links = resource.Body("links") + + def accept(self, session, *, auth_key=None): + """Accept a volume transfer. + + :param session: The session to use for making this request. + :param auth_key: The authentication key for the volume transfer. + + :return: This :class:`Transfer` instance. + """ + body = {'accept': {'auth_key': auth_key}} + + url = utils.urljoin(self.base_path, self.id, 'accept') + resp = session.post(url, json=body) + exceptions.raise_from_response(resp) + + transfer = Transfer() + transfer._translate_response(resp) + return transfer diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index cf17c3654..2d8925508 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -26,6 +26,7 @@ from openstack.block_storage.v3 import service from openstack.block_storage.v3 import snapshot from openstack.block_storage.v3 import stats +from openstack.block_storage.v3 import transfer from openstack.block_storage.v3 import type from openstack.block_storage.v3 import volume from openstack.identity.v3 import project @@ -1068,6 +1069,39 @@ def test_type_encryption_delete_ignore(self): ) +class TestTransfer(TestVolumeProxy): + def test_transfer_create(self): + self.verify_create(self.proxy.create_transfer, transfer.Transfer) + + def test_transfer_delete(self): + self.verify_delete( + self.proxy.delete_transfer, transfer.Transfer, False + ) + + def test_transfer_get(self): + self.verify_get(self.proxy.get_transfer, transfer.Transfer) + + def test_transfer_find(self): + self.verify_find(self.proxy.find_transfer, transfer.Transfer) + + @mock.patch( + 'openstack.utils.supports_microversion', + autospec=True, + return_value=False, + ) + def test_transfers(self, mock_mv): + self.verify_list(self.proxy.transfers, transfer.Transfer) + + def test_accept_transfer(self): + self._verify( + 'openstack.block_storage.v3.transfer.Transfer.accept', + self.proxy.accept_transfer, + method_args=['value', 'auth_key'], + expected_args=[self.proxy], + expected_kwargs={'auth_key': 'auth_key'}, + ) + + class TestQuotaClassSet(TestVolumeProxy): def test_quota_class_set_get(self): self.verify_get( From 3ea1b09087d6ce3f847cd0a0cabf73bec0d576b0 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 4 Apr 2025 17:16:40 +0100 Subject: [PATCH 3703/3836] typing: Correct types for fake generator Change-Id: I1f62d14f72e86388a3cbc3dd532d8a750bda9180 Signed-off-by: Stephen Finucane --- openstack/test/fakes.py | 44 ++++++++++++++--------------------------- 1 file changed, 15 insertions(+), 29 deletions(-) diff --git a/openstack/test/fakes.py b/openstack/test/fakes.py index a93a78d1d..7b7df9265 100644 --- a/openstack/test/fakes.py +++ b/openstack/test/fakes.py @@ -20,14 +20,10 @@ :class:`~openstack.proxy.Proxy` instances. """ +from collections.abc import Generator import inspect import random -from typing import ( - Any, - Optional, - TypeVar, -) -from collections.abc import Generator +import typing as ty from unittest import mock import uuid @@ -38,18 +34,12 @@ from openstack import service_description -Resource = TypeVar('Resource', bound=resource.Resource) - - def generate_fake_resource( - resource_type: type[Resource], - **attrs: dict[str, Any], -) -> Resource: + resource_type: type[resource.ResourceT], + **attrs: ty.Any, +) -> resource.ResourceT: """Generate a fake resource - :param type resource_type: Object class - :param dict attrs: Optional attributes to be set on resource - Example usage: .. code-block:: python @@ -59,14 +49,14 @@ def generate_fake_resource( >>> fakes.generate_fake_resource(server.Server) openstack.compute.v2.server.Server(...) - :param type resource_type: Object class - :param dict attrs: Optional attributes to be set on resource + :param resource_type: Object class + :param attrs: Optional attributes to be set on resource :return: Instance of ``resource_type`` class populated with fake values of expected types :raises NotImplementedError: If a resource attribute specifies a ``type`` or ``list_type`` that cannot be automatically generated """ - base_attrs: dict[str, Any] = {} + base_attrs: dict[str, ty.Any] = {} for name, value in inspect.getmembers( resource_type, predicate=lambda x: isinstance(x, (fields.Body, fields.URI)), @@ -139,16 +129,12 @@ def generate_fake_resource( def generate_fake_resources( - resource_type: type[Resource], + resource_type: type[resource.ResourceT], count: int = 1, - attrs: Optional[dict[str, Any]] = None, -) -> Generator[Resource, None, None]: + attrs: ty.Optional[dict[str, ty.Any]] = None, +) -> Generator[resource.ResourceT, None, None]: """Generate a given number of fake resource entities - :param type resource_type: Object class - :param int count: Number of objects to return - :param dict attrs: Attribute values to set into each instance - Example usage: .. code-block:: python @@ -158,9 +144,9 @@ def generate_fake_resources( >>> fakes.generate_fake_resources(server.Server, count=3) - :param type resource_type: Object class - :param int count: Number of objects to return - :param dict attrs: Attribute values to set into each instance + :param resource_type: Object class + :param count: Number of objects to return + :param attrs: Attribute values to set into each instance :return: Generator of ``resource_type`` class instances populated with fake values of expected types. """ @@ -175,7 +161,7 @@ def generate_fake_resources( # (better) type annotations def generate_fake_proxy( service: type[service_description.ServiceDescription], - api_version: Optional[str] = None, + api_version: ty.Optional[str] = None, ) -> proxy.Proxy: """Generate a fake proxy for the given service type From c7f3d1494e7417e8f55fe26a3a598b62abe58087 Mon Sep 17 00:00:00 2001 From: Niklas Schwarz Date: Thu, 13 Feb 2025 10:31:25 +0100 Subject: [PATCH 3704/3836] Add listing of groups for a user Add method to list the groups for a defined user from the identity endpoint `/user/{user_id}/groups` Change-Id: I32ef1634081e8194137fde3f8691650019892241 --- doc/source/user/proxies/identity_v3.rst | 1 + doc/source/user/resources/identity/v3/group.rst | 9 +++++++++ openstack/identity/v3/_proxy.py | 12 ++++++++++++ openstack/identity/v3/group.py | 14 ++++++++++++++ openstack/tests/unit/identity/v3/test_proxy.py | 8 ++++++++ 5 files changed, 44 insertions(+) diff --git a/doc/source/user/proxies/identity_v3.rst b/doc/source/user/proxies/identity_v3.rst index 548abf4ca..fe02e2fbc 100644 --- a/doc/source/user/proxies/identity_v3.rst +++ b/doc/source/user/proxies/identity_v3.rst @@ -84,6 +84,7 @@ User Operations .. autoclass:: openstack.identity.v3._proxy.Proxy :noindex: :members: create_user, update_user, delete_user, get_user, find_user, users, + user_groups Trust Operations ^^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/identity/v3/group.rst b/doc/source/user/resources/identity/v3/group.rst index fe6c4462a..d1215113d 100644 --- a/doc/source/user/resources/identity/v3/group.rst +++ b/doc/source/user/resources/identity/v3/group.rst @@ -10,3 +10,12 @@ The ``Group`` class inherits from :class:`~openstack.resource.Resource`. .. autoclass:: openstack.identity.v3.group.Group :members: + +The UserGroup Class +------------------- + +The ``UserGroup`` class inherits from +:class:`~openstack.identity.v3.group.Group` + +.. autoclass:: openstack.identity.v3.group.UserGroup + :members: diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 45cbaa042..99d74a199 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -934,6 +934,18 @@ def get_user(self, user): """ return self._get(_user.User, user) + def user_groups(self, user): + """List groups a user is in + + :param user: Either the ID of a user or a + :class:`~openstack.identity.v3.user.User` instance + + :return: List of :class:`~openstack.identity.v3.group.group` + """ + user_id = self._get_resource(_user.User, user).id + groups = self._list(_group.UserGroup, user_id=user_id) + return groups + def users(self, **query): """Retrieve a generator of users diff --git a/openstack/identity/v3/group.py b/openstack/identity/v3/group.py index c1c11d9a7..d795f96f5 100644 --- a/openstack/identity/v3/group.py +++ b/openstack/identity/v3/group.py @@ -74,3 +74,17 @@ def check_user(self, session, user): if resp.status_code == 204: return True return False + + +class UserGroup(Group): + base_path = '/users/%(user_id)%/groups' + + #: The ID for the user from the URI of the resource + user_id = resource.URI('user_id') + + # capabilities + allow_create = False + allow_fetch = False + allow_commit = False + allow_delete = False + allow_list = True diff --git a/openstack/tests/unit/identity/v3/test_proxy.py b/openstack/tests/unit/identity/v3/test_proxy.py index e582f31f8..5c04ce76f 100644 --- a/openstack/tests/unit/identity/v3/test_proxy.py +++ b/openstack/tests/unit/identity/v3/test_proxy.py @@ -385,6 +385,14 @@ def test_users(self): def test_user_update(self): self.verify_update(self.proxy.update_user, user.User) + def test_user_groups(self): + self.verify_list( + self.proxy.user_groups, + group.UserGroup, + method_kwargs={"user": 'user'}, + expected_kwargs={"user_id": "user"}, + ) + class TestIdentityProxyTrust(TestIdentityProxyBase): def test_trust_create_attrs(self): From 13098d8bbca547cf155d90339edbbfc1c7f159b1 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 8 Apr 2025 12:03:49 +0100 Subject: [PATCH 3705/3836] tests: Add tests for v2 volume transfer module These were missed in Icc83eb5e70bba89ff2b496e9aa01c160cf9e4270. Change-Id: I44b22a8ded9c4bf1eba621c3d13028f9a4683b17 Signed-off-by: Stephen Finucane --- .../unit/block_storage/v2/test_transfer.py | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 openstack/tests/unit/block_storage/v2/test_transfer.py diff --git a/openstack/tests/unit/block_storage/v2/test_transfer.py b/openstack/tests/unit/block_storage/v2/test_transfer.py new file mode 100644 index 000000000..458e481c3 --- /dev/null +++ b/openstack/tests/unit/block_storage/v2/test_transfer.py @@ -0,0 +1,84 @@ +# 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 unittest import mock + +from keystoneauth1 import adapter + +from openstack.block_storage.v2 import transfer +from openstack.tests.unit import base + + +FAKE_ID = "09d18b36-9e8d-4438-a4da-3f5eff5e1130" +FAKE_VOL_ID = "390de1bc-19d1-41e7-ba67-c492bb36cae5" +FAKE_VOL_NAME = "test-volume" +FAKE_TRANSFER = "7d048960-7c3f-4bf0-952f-4312fdea1dec" +FAKE_AUTH_KEY = "95bc670c0068821d" + +TRANSFER = { + "auth_key": FAKE_AUTH_KEY, + "created_at": "2023-06-27T08:47:23.035010", + "id": FAKE_ID, + "name": FAKE_VOL_NAME, + "volume_id": FAKE_VOL_ID, +} + + +class TestTransfer(base.TestCase): + def setUp(self): + super().setUp() + self.resp = mock.Mock() + self.resp.body = {'transfer': TRANSFER} + self.resp.json = mock.Mock(return_value=self.resp.body) + self.resp.headers = {} + self.resp.status_code = 202 + + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.post = mock.Mock(return_value=self.resp) + self.sess.default_microversion = "3.55" + + def test_basic(self): + sot = transfer.Transfer(TRANSFER) + self.assertEqual("transfer", sot.resource_key) + self.assertEqual("transfers", sot.resources_key) + self.assertEqual("/os-volume-transfer", sot.base_path) + self.assertTrue(sot.allow_create) + + self.assertDictEqual( + { + "limit": "limit", + "marker": "marker", + }, + sot._query_mapping._mapping, + ) + + def test_create(self): + sot = transfer.Transfer(**TRANSFER) + self.assertEqual(TRANSFER["auth_key"], sot.auth_key) + self.assertEqual(TRANSFER["created_at"], sot.created_at) + self.assertEqual(TRANSFER["id"], sot.id) + self.assertEqual(TRANSFER["name"], sot.name) + self.assertEqual(TRANSFER["volume_id"], sot.volume_id) + + def test_accept(self): + sot = transfer.Transfer() + sot.id = FAKE_TRANSFER + + sot.accept(self.sess, auth_key=FAKE_AUTH_KEY) + self.sess.post.assert_called_with( + f'os-volume-transfer/{FAKE_TRANSFER}/accept', + json={ + 'accept': { + 'auth_key': FAKE_AUTH_KEY, + } + }, + ) From df1b9e4f8ca877a7caddcbb9afc66de225a8dd21 Mon Sep 17 00:00:00 2001 From: Rajat Dhasmana Date: Tue, 15 Apr 2025 14:05:15 +0530 Subject: [PATCH 3706/3836] Fix: Quota show defaults The ``quota show --default`` command has the URL ``os-quota-sets/{project_id}/defaults`` whereas we used ``os-quota-sets/defaults`` which results in passes "defaults" as the project ID. This was uncovered by adding project ID validation in the cinder quotas API[2]. Closes-Bug: #2107375 [1] https://docs.openstack.org/api-ref/block-storage/v3/#get-default-quotas-for-a-project [2] https://review.opendev.org/c/openstack/cinder/+/784763 Change-Id: Ibc8a084dbf41f18af28ab5592d9f5638f481c007 --- openstack/block_storage/v3/_proxy.py | 4 +++- openstack/tests/unit/block_storage/v3/test_proxy.py | 5 +++-- .../notes/fix-quota-show-defaults-0a8c388926eae18b.yaml | 5 +++++ 3 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/fix-quota-show-defaults-0a8c388926eae18b.yaml diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index b7cfef0c6..421644aa0 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -2024,7 +2024,9 @@ def get_quota_set_defaults(self, project): res = self._get_resource( _quota_set.QuotaSet, None, project_id=project.id ) - return res.fetch(self, base_path='/os-quota-sets/defaults') + return res.fetch( + self, base_path=(f'/os-quota-sets/{project.id}/defaults') + ) def revert_quota_set(self, project, **query): """Reset Quota for the project/user. diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 67f8778a7..eb4cbfe8e 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -1176,14 +1176,15 @@ def test_quota_set_get_query(self): ) def test_quota_set_get_defaults(self): + project_id = 'prj' self._verify( 'openstack.resource.Resource.fetch', self.proxy.get_quota_set_defaults, - method_args=['prj'], + method_args=[project_id], expected_args=[ self.proxy, False, - '/os-quota-sets/defaults', + f'/os-quota-sets/{project_id}/defaults', None, False, ], diff --git a/releasenotes/notes/fix-quota-show-defaults-0a8c388926eae18b.yaml b/releasenotes/notes/fix-quota-show-defaults-0a8c388926eae18b.yaml new file mode 100644 index 000000000..428abef0e --- /dev/null +++ b/releasenotes/notes/fix-quota-show-defaults-0a8c388926eae18b.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixed issue with ``quota show --default`` command by + correcting the API URL. From 85200e02b74e90522ce178b8913ff37acf1e1d76 Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Wed, 7 May 2025 14:58:40 +0900 Subject: [PATCH 3707/3836] Replace deprecated datetime.datetime.utcnow It was deprecated in Python 3.12 . Change-Id: I0df1b6e3454f5780a17f925b1c9e9bdacb3cf414 --- openstack/tests/unit/compute/v2/test_proxy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index c579f1c10..6593f638b 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -1638,7 +1638,7 @@ def test_usages(self): self.verify_list(self.proxy.usages, usage.Usage) def test_usages__with_kwargs(self): - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) start = now - datetime.timedelta(weeks=4) end = end = now + datetime.timedelta(days=1) self.verify_list( @@ -1662,7 +1662,7 @@ def test_get_usage(self): ) def test_get_usage__with_kwargs(self): - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) start = now - datetime.timedelta(weeks=4) end = end = now + datetime.timedelta(days=1) self._verify( From f19567b3cca10bf9563cbda2fe51f76741787a1e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 7 May 2025 12:31:51 +0100 Subject: [PATCH 3708/3836] Drop support for Python 3.9 We also make the openstacksdk-functional-devstack-masakari non-voting while we wait for them to fix their gate. Change-Id: Ic46d9e99dd2dd7300299bddcf21d663c4d7223d4 Signed-off-by: Stephen Finucane Depends-on: https://review.opendev.org/c/openstack/python-openstackclient/+/949014 --- releasenotes/notes/drop-python-39-e2d54d859007a575.yaml | 5 +++++ setup.cfg | 4 ++-- zuul.d/project.yaml | 5 ++++- 3 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/drop-python-39-e2d54d859007a575.yaml diff --git a/releasenotes/notes/drop-python-39-e2d54d859007a575.yaml b/releasenotes/notes/drop-python-39-e2d54d859007a575.yaml new file mode 100644 index 000000000..4583e98cf --- /dev/null +++ b/releasenotes/notes/drop-python-39-e2d54d859007a575.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + Support for Python 3.9 has been dropped. The minimum version of Python now + supported in 3.10. diff --git a/setup.cfg b/setup.cfg index 24bace6ac..34410f3df 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,7 +6,7 @@ description_file = author = OpenStack author_email = openstack-discuss@lists.openstack.org home_page = https://docs.openstack.org/openstacksdk/ -python_requires = >=3.9 +python_requires = >=3.10 classifier = Environment :: OpenStack Intended Audience :: Information Technology @@ -15,10 +15,10 @@ classifier = Operating System :: POSIX :: Linux Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 + Programming Language :: Python :: 3.13 [files] packages = diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index 197ef92ea..5d6e9cde4 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -26,7 +26,10 @@ - openstacksdk-functional-devstack-magnum: voting: false - openstacksdk-functional-devstack-manila - - openstacksdk-functional-devstack-masakari + # TODO(stephenfin): We can make this voting again once [1] merges + # [1] https://review.opendev.org/c/openstack/masakari/+/949153 + - openstacksdk-functional-devstack-masakari: + voting: false - openstacksdk-functional-devstack-ironic: voting: false - osc-functional-devstack-tips: From e0f57ad81fb6a9f5100cfdefdd30661326edb031 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 7 May 2025 12:33:13 +0100 Subject: [PATCH 3709/3836] Bump Python version used for linters to 3.10 This looks very large, but the only manual change is in pyproject.toml and the bump of the ruff pre-commit hook: the rest is entirely ruff converting our use of e.g. 'typing.Union[X, Y]' to 'X | Y', as added by PEP-604 [1]. [1] https://peps.python.org/pep-0604/ Change-Id: I3ed176018cf78c417e751834e57412d72884a69b Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 2 +- openstack/__init__.py | 8 +- openstack/_log.py | 10 +- openstack/accelerator/v2/_proxy.py | 10 +- openstack/baremetal/configdrive.py | 3 +- openstack/baremetal/v1/_proxy.py | 18 +- openstack/baremetal/v1/driver.py | 3 +- openstack/baremetal/v1/node.py | 5 +- .../baremetal_introspection/v1/_proxy.py | 10 +- openstack/block_storage/v2/_proxy.py | 30 ++-- openstack/block_storage/v3/_proxy.py | 44 ++--- openstack/block_storage/v3/service.py | 12 +- openstack/cloud/_compute.py | 4 +- openstack/cloud/_image.py | 19 +-- openstack/cloud/openstackcloud.py | 29 ++-- openstack/clustering/v1/_proxy.py | 10 +- openstack/compute/v2/_proxy.py | 18 +- openstack/config/__init__.py | 8 +- openstack/config/cloud_region.py | 58 +++---- openstack/config/loader.py | 32 ++-- openstack/connection.py | 37 ++--- .../v1/_proxy.py | 10 +- openstack/database/v1/_proxy.py | 10 +- openstack/dns/v2/_base.py | 5 +- openstack/dns/v2/_proxy.py | 10 +- openstack/exceptions.py | 26 ++- openstack/fields.py | 48 +++--- openstack/identity/v2/_proxy.py | 10 +- openstack/identity/v3/_proxy.py | 10 +- openstack/image/v1/_proxy.py | 10 +- openstack/image/v2/_proxy.py | 10 +- openstack/instance_ha/v1/_proxy.py | 10 +- openstack/key_manager/v1/_proxy.py | 10 +- openstack/load_balancer/v2/_proxy.py | 10 +- openstack/message/v2/_proxy.py | 10 +- openstack/message/v2/message.py | 3 +- openstack/network/v2/_proxy.py | 28 ++-- openstack/object_store/v1/_proxy.py | 12 +- .../orchestration/util/template_utils.py | 2 +- openstack/orchestration/v1/_proxy.py | 10 +- openstack/placement/v1/_proxy.py | 10 +- openstack/resource.py | 154 +++++++++--------- openstack/service_description.py | 7 +- openstack/shared_file_system/v2/_proxy.py | 10 +- openstack/test/fakes.py | 6 +- openstack/tests/functional/baremetal/base.py | 3 +- .../functional/shared_file_system/base.py | 3 +- openstack/utils.py | 24 ++- openstack/workflow/v2/_proxy.py | 10 +- pyproject.toml | 3 +- 50 files changed, 411 insertions(+), 433 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 118cf6153..42a37dd89 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: hooks: - id: doc8 - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.6 + rev: v0.11.8 hooks: - id: ruff args: ['--fix', '--unsafe-fixes'] diff --git a/openstack/__init__.py b/openstack/__init__.py index a32487112..5c4920f53 100644 --- a/openstack/__init__.py +++ b/openstack/__init__.py @@ -64,10 +64,10 @@ def connect( - cloud: ty.Optional[str] = None, - app_name: ty.Optional[str] = None, - app_version: ty.Optional[str] = None, - options: ty.Optional[argparse.ArgumentParser] = None, + cloud: str | None = None, + app_name: str | None = None, + app_version: str | None = None, + options: argparse.ArgumentParser | None = None, load_yaml_config: bool = True, load_envvars: bool = True, **kwargs: ty.Any, diff --git a/openstack/_log.py b/openstack/_log.py index 5182bf702..d9d2a842d 100644 --- a/openstack/_log.py +++ b/openstack/_log.py @@ -19,8 +19,8 @@ def setup_logging( name: str, - handlers: ty.Optional[list[logging.Handler]] = None, - level: ty.Optional[int] = None, + handlers: list[logging.Handler] | None = None, + level: int | None = None, ) -> logging.Logger: """Set up logging for a named logger. @@ -50,11 +50,11 @@ def setup_logging( def enable_logging( debug: bool = False, http_debug: bool = False, - path: ty.Optional[str] = None, - stream: ty.Optional[ty.TextIO] = None, + path: str | None = None, + stream: ty.TextIO | None = None, format_stream: bool = False, format_template: str = '%(asctime)s %(levelname)s: %(name)s %(message)s', - handlers: ty.Optional[list[logging.Handler]] = None, + handlers: list[logging.Handler] | None = None, ) -> None: """Enable logging output. diff --git a/openstack/accelerator/v2/_proxy.py b/openstack/accelerator/v2/_proxy.py index 801198d47..cc6a08ba3 100644 --- a/openstack/accelerator/v2/_proxy.py +++ b/openstack/accelerator/v2/_proxy.py @@ -260,11 +260,11 @@ def wait_for_status( self, res: resource.ResourceT, status: str, - failures: ty.Optional[list[str]] = None, - interval: ty.Union[int, float, None] = 2, - wait: ty.Optional[int] = None, + failures: list[str] | None = None, + interval: int | float | None = 2, + wait: int | None = None, attribute: str = 'status', - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for the resource to be in a particular status. @@ -300,7 +300,7 @@ def wait_for_delete( res: resource.ResourceT, interval: int = 2, wait: int = 120, - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for a resource to be deleted. diff --git a/openstack/baremetal/configdrive.py b/openstack/baremetal/configdrive.py index e7458804b..578802e07 100644 --- a/openstack/baremetal/configdrive.py +++ b/openstack/baremetal/configdrive.py @@ -20,7 +20,6 @@ import shutil import subprocess import tempfile -import typing as ty @contextlib.contextmanager @@ -110,7 +109,7 @@ def pack(path: str) -> str: with tempfile.NamedTemporaryFile() as tmpfile: # NOTE(toabctl): Luckily, genisoimage, mkisofs and xorrisofs understand # the same parameters which are currently used. - error: ty.Optional[Exception] + error: Exception | None for c in ['genisoimage', 'mkisofs', 'xorrisofs']: try: p = subprocess.Popen( # noqa: S603 diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 3e3fe42ed..4d9b4d6c0 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -241,7 +241,7 @@ def list_driver_vendor_passthru(self, driver): def call_driver_vendor_passthru( self, - driver: ty.Union[str, _driver.Driver], + driver: str | _driver.Driver, verb: str, method: str, body: object = None, @@ -1171,12 +1171,12 @@ def detach_vmedia_from_node(self, node, device_types=None): def attach_vif_to_node( self, - node: ty.Union[_node.Node, str], + node: _node.Node | str, vif_id: str, retry_on_conflict: bool = True, *, - port_id: ty.Optional[str] = None, - port_group_id: ty.Optional[str] = None, + port_id: str | None = None, + port_group_id: str | None = None, ) -> None: """Attach a VIF to the node. @@ -1868,11 +1868,11 @@ def wait_for_status( self, res: resource.ResourceT, status: str, - failures: ty.Optional[list[str]] = None, - interval: ty.Union[int, float, None] = 2, - wait: ty.Optional[int] = None, + failures: list[str] | None = None, + interval: int | float | None = 2, + wait: int | None = None, attribute: str = 'status', - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for the resource to be in a particular status. @@ -1908,7 +1908,7 @@ def wait_for_delete( res: resource.ResourceT, interval: int = 2, wait: int = 120, - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for a resource to be deleted. diff --git a/openstack/baremetal/v1/driver.py b/openstack/baremetal/v1/driver.py index 3f476379f..81dcaaad6 100644 --- a/openstack/baremetal/v1/driver.py +++ b/openstack/baremetal/v1/driver.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import typing as ty from keystoneauth1 import adapter import requests @@ -162,7 +161,7 @@ def call_vendor_passthru( session: adapter.Adapter, verb: str, method: str, - body: ty.Optional[dict] = None, + body: dict | None = None, ) -> requests.Response: """Call a vendor specific passthru method diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index c6b2d12ec..375c56c55 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -12,7 +12,6 @@ import collections import enum -import typing as ty import warnings from keystoneauth1 import adapter @@ -912,8 +911,8 @@ def attach_vif( vif_id: str, retry_on_conflict: bool = True, *, - port_id: ty.Optional[str] = None, - port_group_id: ty.Optional[str] = None, + port_id: str | None = None, + port_group_id: str | None = None, ) -> None: """Attach a VIF to the node. diff --git a/openstack/baremetal_introspection/v1/_proxy.py b/openstack/baremetal_introspection/v1/_proxy.py index 31b99fa09..dad69372c 100644 --- a/openstack/baremetal_introspection/v1/_proxy.py +++ b/openstack/baremetal_introspection/v1/_proxy.py @@ -241,11 +241,11 @@ def wait_for_status( self, res: resource.ResourceT, status: str, - failures: ty.Optional[list[str]] = None, - interval: ty.Union[int, float, None] = 2, - wait: ty.Optional[int] = None, + failures: list[str] | None = None, + interval: int | float | None = 2, + wait: int | None = None, attribute: str = 'status', - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for the resource to be in a particular status. @@ -281,7 +281,7 @@ def wait_for_delete( res: resource.ResourceT, interval: int = 2, wait: int = 120, - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for a resource to be deleted. diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 15cf89c2d..8af6d8e84 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -983,7 +983,7 @@ def find_service( name_or_id: str, ignore_missing: ty.Literal[True] = True, **query: ty.Any, - ) -> ty.Optional[_service.Service]: ... + ) -> _service.Service | None: ... @ty.overload def find_service( @@ -1001,14 +1001,14 @@ def find_service( name_or_id: str, ignore_missing: bool, **query: ty.Any, - ) -> ty.Optional[_service.Service]: ... + ) -> _service.Service | None: ... def find_service( self, name_or_id: str, ignore_missing: bool = True, **query: ty.Any, - ) -> ty.Optional[_service.Service]: + ) -> _service.Service | None: """Find a single service :param name_or_id: The name or ID of a service @@ -1047,7 +1047,7 @@ def services( def enable_service( self, - service: ty.Union[str, _service.Service], + service: str | _service.Service, ) -> _service.Service: """Enable a service @@ -1062,9 +1062,9 @@ def enable_service( def disable_service( self, - service: ty.Union[str, _service.Service], + service: str | _service.Service, *, - reason: ty.Optional[str] = None, + reason: str | None = None, ) -> _service.Service: """Disable a service @@ -1080,7 +1080,7 @@ def disable_service( def thaw_service( self, - service: ty.Union[str, _service.Service], + service: str | _service.Service, ) -> _service.Service: """Thaw a service @@ -1095,7 +1095,7 @@ def thaw_service( def freeze_service( self, - service: ty.Union[str, _service.Service], + service: str | _service.Service, ) -> _service.Service: """Freeze a service @@ -1110,9 +1110,9 @@ def freeze_service( def failover_service( self, - service: ty.Union[str, _service.Service], + service: str | _service.Service, *, - backend_id: ty.Optional[str] = None, + backend_id: str | None = None, ) -> _service.Service: """Failover a service @@ -1332,11 +1332,11 @@ def wait_for_status( self, res: resource.ResourceT, status: str = 'available', - failures: ty.Optional[list[str]] = None, - interval: ty.Union[int, float, None] = 2, - wait: ty.Optional[int] = None, + failures: list[str] | None = None, + interval: int | float | None = 2, + wait: int | None = None, attribute: str = 'status', - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for the resource to be in a particular status. @@ -1375,7 +1375,7 @@ def wait_for_delete( res: resource.ResourceT, interval: int = 2, wait: int = 120, - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for a resource to be deleted. diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 421644aa0..56e7d5149 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -2088,7 +2088,7 @@ def find_service( name_or_id: str, ignore_missing: ty.Literal[True] = True, **query: ty.Any, - ) -> ty.Optional[_service.Service]: ... + ) -> _service.Service | None: ... @ty.overload def find_service( @@ -2106,14 +2106,14 @@ def find_service( name_or_id: str, ignore_missing: bool, **query: ty.Any, - ) -> ty.Optional[_service.Service]: ... + ) -> _service.Service | None: ... def find_service( self, name_or_id: str, ignore_missing: bool = True, **query: ty.Any, - ) -> ty.Optional[_service.Service]: + ) -> _service.Service | None: """Find a single service :param name_or_id: The name or ID of a service @@ -2152,7 +2152,7 @@ def services( def enable_service( self, - service: ty.Union[str, _service.Service], + service: str | _service.Service, ) -> _service.Service: """Enable a service @@ -2167,9 +2167,9 @@ def enable_service( def disable_service( self, - service: ty.Union[str, _service.Service], + service: str | _service.Service, *, - reason: ty.Optional[str] = None, + reason: str | None = None, ) -> _service.Service: """Disable a service @@ -2185,7 +2185,7 @@ def disable_service( def thaw_service( self, - service: ty.Union[str, _service.Service], + service: str | _service.Service, ) -> _service.Service: """Thaw a service @@ -2200,7 +2200,7 @@ def thaw_service( def freeze_service( self, - service: ty.Union[str, _service.Service], + service: str | _service.Service, ) -> _service.Service: """Freeze a service @@ -2217,9 +2217,9 @@ def set_service_log_levels( self, *, level: _service.Level, - binary: ty.Optional[_service.Binary] = None, - server: ty.Optional[str] = None, - prefix: ty.Optional[str] = None, + binary: _service.Binary | None = None, + server: str | None = None, + prefix: str | None = None, ) -> None: """Set log level for services. @@ -2239,9 +2239,9 @@ def set_service_log_levels( def get_service_log_levels( self, *, - binary: ty.Optional[_service.Binary] = None, - server: ty.Optional[str] = None, - prefix: ty.Optional[str] = None, + binary: _service.Binary | None = None, + server: str | None = None, + prefix: str | None = None, ) -> ty.Generator[_service.LogLevel, None, None]: """Get log level for services. @@ -2259,10 +2259,10 @@ def get_service_log_levels( def failover_service( self, - service: ty.Union[str, _service.Service], + service: str | _service.Service, *, - cluster: ty.Optional[str] = None, - backend_id: ty.Optional[str] = None, + cluster: str | None = None, + backend_id: str | None = None, ) -> _service.Service: """Failover a service @@ -2404,11 +2404,11 @@ def wait_for_status( self, res: resource.ResourceT, status: str = 'available', - failures: ty.Optional[list[str]] = None, - interval: ty.Union[int, float, None] = 2, - wait: ty.Optional[int] = None, + failures: list[str] | None = None, + interval: int | float | None = 2, + wait: int | None = None, attribute: str = 'status', - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for the resource to be in a particular status. @@ -2447,7 +2447,7 @@ def wait_for_delete( res: resource.ResourceT, interval: int = 2, wait: int = 120, - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for a resource to be deleted. diff --git a/openstack/block_storage/v3/service.py b/openstack/block_storage/v3/service.py index f7e39716a..cefd65975 100644 --- a/openstack/block_storage/v3/service.py +++ b/openstack/block_storage/v3/service.py @@ -175,9 +175,9 @@ def set_log_levels( session: ksa_adapter.Adapter, *, level: Level, - binary: ty.Optional[Binary] = None, - server: ty.Optional[str] = None, - prefix: ty.Optional[str] = None, + binary: Binary | None = None, + server: str | None = None, + prefix: str | None = None, ) -> None: """Set log level for services. @@ -209,9 +209,9 @@ def get_log_levels( cls, session: ksa_adapter.Adapter, *, - binary: ty.Optional[Binary] = None, - server: ty.Optional[str] = None, - prefix: ty.Optional[str] = None, + binary: Binary | None = None, + server: str | None = None, + prefix: str | None = None, ) -> ty.Generator[LogLevel, None, None]: """Get log level for services. diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index a0be0f459..9b9c91550 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -278,7 +278,7 @@ def _get_server_security_groups(self, server, security_groups): self.log.debug('Server %s not found', server) return None, None - if not isinstance(security_groups, (list, tuple)): + if not isinstance(security_groups, list | tuple): security_groups = [security_groups] sec_group_objs = [] @@ -1812,7 +1812,7 @@ def delete_aggregate(self, name_or_id): :raises: :class:`~openstack.exceptions.SDKException` on operation error. """ - if isinstance(name_or_id, (str, bytes)) and not name_or_id.isdigit(): + if isinstance(name_or_id, str | bytes) and not name_or_id.isdigit(): aggregate = self.get_aggregate(name_or_id) if not aggregate: self.log.debug( diff --git a/openstack/cloud/_image.py b/openstack/cloud/_image.py index 4c2235938..c88c7010e 100644 --- a/openstack/cloud/_image.py +++ b/openstack/cloud/_image.py @@ -31,21 +31,20 @@ class ImageCloudMixin(openstackcloud._OpenStackCloudMixin): def __init__( self, - cloud: ty.Optional[str] = None, + cloud: str | None = None, config: ty.Optional['cloud_region.CloudRegion'] = None, session: ty.Optional['ks_session.Session'] = None, - app_name: ty.Optional[str] = None, - app_version: ty.Optional[str] = None, - extra_services: ty.Optional[ - list['service_description.ServiceDescription'] - ] = None, + app_name: str | None = None, + app_version: str | None = None, + extra_services: list['service_description.ServiceDescription'] + | None = None, strict: bool = False, - use_direct_get: ty.Optional[bool] = None, + use_direct_get: bool | None = None, task_manager: ty.Any = None, - rate_limit: ty.Union[float, dict[str, float], None] = None, + rate_limit: float | dict[str, float] | None = None, oslo_conf: ty.Optional['cfg.ConfigOpts'] = None, - service_types: ty.Optional[list[str]] = None, - global_request_id: ty.Optional[str] = None, + service_types: list[str] | None = None, + global_request_id: str | None = None, strict_proxies: bool = False, pool_executor: ty.Optional['concurrent.futures.Executor'] = None, **kwargs: ty.Any, diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index cd176f329..e7e3857ee 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -72,23 +72,22 @@ class _OpenStackCloudMixin(_services_mixin.ServicesMixin): def __init__( self, - cloud: ty.Optional[str] = None, - config: ty.Optional[cloud_region.CloudRegion] = None, + cloud: str | None = None, + config: cloud_region.CloudRegion | None = None, session: ty.Optional['ks_session.Session'] = None, - app_name: ty.Optional[str] = None, - app_version: ty.Optional[str] = None, - extra_services: ty.Optional[ - list['service_description.ServiceDescription'] - ] = None, + app_name: str | None = None, + app_version: str | None = None, + extra_services: list['service_description.ServiceDescription'] + | None = None, strict: bool = False, - use_direct_get: ty.Optional[bool] = None, + use_direct_get: bool | None = None, task_manager: ty.Any = None, - rate_limit: ty.Union[float, dict[str, float], None] = None, + rate_limit: float | dict[str, float] | None = None, oslo_conf: ty.Optional['cfg.ConfigOpts'] = None, - service_types: ty.Optional[list[str]] = None, - global_request_id: ty.Optional[str] = None, + service_types: list[str] | None = None, + global_request_id: str | None = None, strict_proxies: bool = False, - pool_executor: ty.Optional[concurrent.futures.Executor] = None, + pool_executor: concurrent.futures.Executor | None = None, **kwargs: ty.Any, ): """Create a connection to a cloud. @@ -300,9 +299,9 @@ def __enter__(self) -> ty_ext.Self: def __exit__( self, - exc_type: ty.Optional[type[BaseException]], - exc_value: ty.Optional[BaseException], - traceback: ty.Optional[types.TracebackType], + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: types.TracebackType | None, ) -> None: self.close() diff --git a/openstack/clustering/v1/_proxy.py b/openstack/clustering/v1/_proxy.py index a46d87f92..fcd355b72 100644 --- a/openstack/clustering/v1/_proxy.py +++ b/openstack/clustering/v1/_proxy.py @@ -1077,11 +1077,11 @@ def wait_for_status( self, res: resource.ResourceT, status: str, - failures: ty.Optional[list[str]] = None, - interval: ty.Union[int, float, None] = 2, - wait: ty.Optional[int] = None, + failures: list[str] | None = None, + interval: int | float | None = 2, + wait: int | None = None, attribute: str = 'status', - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for the resource to be in a particular status. @@ -1117,7 +1117,7 @@ def wait_for_delete( res: resource.ResourceT, interval: int = 2, wait: int = 120, - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for a resource to be deleted. diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 63e67277a..1f03c51ab 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -2648,10 +2648,10 @@ def wait_for_server( self, server: _server.Server, status: str = 'ACTIVE', - failures: ty.Optional[list[str]] = None, - interval: ty.Union[int, float, None] = 2, - wait: ty.Optional[int] = 120, - callback: ty.Optional[ty.Callable[[int], None]] = None, + failures: list[str] | None = None, + interval: int | float | None = 2, + wait: int | None = 120, + callback: ty.Callable[[int], None] | None = None, ) -> _server.Server: """Wait for a server to be in a particular status. @@ -2694,11 +2694,11 @@ def wait_for_status( self, res: resource.ResourceT, status: str, - failures: ty.Optional[list[str]] = None, - interval: ty.Union[int, float, None] = 2, - wait: ty.Optional[int] = None, + failures: list[str] | None = None, + interval: int | float | None = 2, + wait: int | None = None, attribute: str = 'status', - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for the resource to be in a particular status. @@ -2734,7 +2734,7 @@ def wait_for_delete( res: resource.ResourceT, interval: int = 2, wait: int = 120, - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for a resource to be deleted. diff --git a/openstack/config/__init__.py b/openstack/config/__init__.py index 48db4e8ae..3825aa27e 100644 --- a/openstack/config/__init__.py +++ b/openstack/config/__init__.py @@ -24,10 +24,10 @@ # TODO(stephenfin): Expand kwargs once we've typed OpenstackConfig.get_one def get_cloud_region( - service_key: ty.Optional[str] = None, - options: ty.Optional[argparse.ArgumentParser] = None, - app_name: ty.Optional[str] = None, - app_version: ty.Optional[str] = None, + service_key: str | None = None, + options: argparse.ArgumentParser | None = None, + app_name: str | None = None, + app_version: str | None = None, load_yaml_config: bool = True, load_envvars: bool = True, **kwargs: ty.Any, diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index cb444d767..4af0745ae 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -74,7 +74,7 @@ class _PasswordCallback(ty.Protocol): - def __call__(self, prompt: ty.Optional[str] = None) -> str: ... + def __call__(self, prompt: str | None = None) -> str: ... def _make_key(key, service_type): @@ -108,11 +108,11 @@ def _get_implied_microversion(version): def from_session( session: ks_session.Session, - name: ty.Optional[str] = None, - region_name: ty.Optional[str] = None, + name: str | None = None, + region_name: str | None = None, force_ipv4: bool = False, - app_name: ty.Optional[str] = None, - app_version: ty.Optional[str] = None, + app_name: str | None = None, + app_version: str | None = None, **kwargs: ty.Any, ) -> 'CloudRegion': """Construct a CloudRegion from an existing `keystoneauth1.session.Session` @@ -152,8 +152,8 @@ def from_session( def from_conf( conf: 'cfg.ConfigOpts', - session: ty.Optional[ks_session.Session] = None, - service_types: ty.Optional[list[str]] = None, + session: ks_session.Session | None = None, + service_types: list[str] | None = None, **kwargs: ty.Any, ) -> 'CloudRegion': """Create a CloudRegion from oslo.config ConfigOpts. @@ -291,29 +291,29 @@ class CloudRegion: def __init__( self, - name: ty.Optional[str] = None, - region_name: ty.Optional[str] = None, - config: ty.Optional[dict[str, ty.Any]] = None, + name: str | None = None, + region_name: str | None = None, + config: dict[str, ty.Any] | None = None, force_ipv4: bool = False, - auth_plugin: ty.Optional[plugin.BaseAuthPlugin] = None, + auth_plugin: plugin.BaseAuthPlugin | None = None, openstack_config: ty.Optional['loader.OpenStackConfig'] = None, - session_constructor: ty.Optional[type[ks_session.Session]] = None, - app_name: ty.Optional[str] = None, - app_version: ty.Optional[str] = None, - session: ty.Optional[ks_session.Session] = None, - discovery_cache: ty.Optional[dict[str, discover.Discover]] = None, - extra_config: ty.Optional[dict[str, ty.Any]] = None, + session_constructor: type[ks_session.Session] | None = None, + app_name: str | None = None, + app_version: str | None = None, + session: ks_session.Session | None = None, + discovery_cache: dict[str, discover.Discover] | None = None, + extra_config: dict[str, ty.Any] | None = None, cache_expiration_time: int = 0, - cache_expirations: ty.Optional[dict[str, int]] = None, - cache_path: ty.Optional[str] = None, + cache_expirations: dict[str, int] | None = None, + cache_path: str | None = None, cache_class: str = 'dogpile.cache.null', - cache_arguments: ty.Optional[dict[str, ty.Any]] = None, - password_callback: ty.Optional[_PasswordCallback] = None, - statsd_host: ty.Optional[str] = None, - statsd_port: ty.Optional[str] = None, - statsd_prefix: ty.Optional[str] = None, + cache_arguments: dict[str, ty.Any] | None = None, + password_callback: _PasswordCallback | None = None, + statsd_host: str | None = None, + statsd_port: str | None = None, + statsd_prefix: str | None = None, # TODO(stephenfin): Add better types - influxdb_config: ty.Optional[dict[str, ty.Any]] = None, + influxdb_config: dict[str, ty.Any] | None = None, collector_registry: ty.Optional[ 'prometheus_client.CollectorRegistry' ] = None, @@ -577,7 +577,7 @@ def get_service_type(self, service_type): def get_service_name(self, service_type): return self._get_config('service_name', service_type) - def get_endpoint(self, service_type: str) -> ty.Optional[str]: + def get_endpoint(self, service_type: str) -> str | None: auth = self.config.get('auth', {}) value = self._get_config('endpoint_override', service_type) if not value: @@ -618,9 +618,9 @@ def get_endpoint(self, service_type: str) -> ty.Optional[str]: def get_endpoint_from_catalog( self, service_type: str, - interface: ty.Optional[str] = None, - region_name: ty.Optional[str] = None, - ) -> ty.Optional[str]: + interface: str | None = None, + region_name: str | None = None, + ) -> str | None: """Return the endpoint for a given service as found in the catalog. For values respecting endpoint overrides, see diff --git a/openstack/config/loader.py b/openstack/config/loader.py index e21dc3687..836fab380 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -158,22 +158,22 @@ class OpenStackConfig: def __init__( self, - config_files: ty.Optional[list[str]] = None, - vendor_files: ty.Optional[list[str]] = None, - override_defaults: ty.Optional[dict[str, ty.Any]] = None, - force_ipv4: ty.Optional[bool] = None, - envvar_prefix: ty.Optional[str] = None, - secure_files: ty.Optional[list[str]] = None, - pw_func: ty.Optional[cloud_region._PasswordCallback] = None, - session_constructor: ty.Optional[type[session.Session]] = None, - app_name: ty.Optional[str] = None, - app_version: ty.Optional[str] = None, + config_files: list[str] | None = None, + vendor_files: list[str] | None = None, + override_defaults: dict[str, ty.Any] | None = None, + force_ipv4: bool | None = None, + envvar_prefix: str | None = None, + secure_files: list[str] | None = None, + pw_func: cloud_region._PasswordCallback | None = None, + session_constructor: type[session.Session] | None = None, + app_name: str | None = None, + app_version: str | None = None, load_yaml_config: bool = True, load_envvars: bool = True, - statsd_host: ty.Optional[str] = None, - statsd_port: ty.Optional[str] = None, - statsd_prefix: ty.Optional[str] = None, - influxdb_config: ty.Optional[dict[str, ty.Any]] = None, + statsd_host: str | None = None, + statsd_port: str | None = None, + statsd_prefix: str | None = None, + influxdb_config: dict[str, ty.Any] | None = None, ): self.log = _log.setup_logging('openstack.config') self._session_constructor = session_constructor @@ -1221,9 +1221,9 @@ def magic_fixes(self, config): def get_one( self, - cloud: ty.Optional[str] = None, + cloud: str | None = None, validate: bool = True, - argparse: ty.Optional[argparse_mod.Namespace] = None, + argparse: argparse_mod.Namespace | None = None, **kwargs: ty.Any, ) -> cloud_region.CloudRegion: """Retrieve a single CloudRegion and merge additional options diff --git a/openstack/connection.py b/openstack/connection.py index 39ef30840..a45c465f8 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -321,9 +321,9 @@ def from_config( - cloud: ty.Optional[str] = None, + cloud: str | None = None, config: ty.Optional['cloud_region.CloudRegion'] = None, - options: ty.Optional[argparse.Namespace] = None, + options: argparse.Namespace | None = None, **kwargs: ty.Any, ) -> 'Connection': """Create a Connection using openstack.config @@ -371,23 +371,22 @@ class Connection( ): def __init__( self, - cloud: ty.Optional[str] = None, + cloud: str | None = None, config: ty.Optional['cloud_region.CloudRegion'] = None, - session: ty.Optional[ks_session.Session] = None, - app_name: ty.Optional[str] = None, - app_version: ty.Optional[str] = None, - extra_services: ty.Optional[ - list[service_description.ServiceDescription] - ] = None, + session: ks_session.Session | None = None, + app_name: str | None = None, + app_version: str | None = None, + extra_services: list[service_description.ServiceDescription] + | None = None, strict: bool = False, - use_direct_get: ty.Optional[bool] = None, + use_direct_get: bool | None = None, task_manager: ty.Any = None, - rate_limit: ty.Union[float, dict[str, float], None] = None, + rate_limit: float | dict[str, float] | None = None, oslo_conf: ty.Optional['cfg.ConfigOpts'] = None, - service_types: ty.Optional[list[str]] = None, - global_request_id: ty.Optional[str] = None, + service_types: list[str] | None = None, + global_request_id: str | None = None, strict_proxies: bool = False, - pool_executor: ty.Optional[concurrent.futures.Executor] = None, + pool_executor: concurrent.futures.Executor | None = None, **kwargs: ty.Any, ): """Create a connection to a cloud. @@ -623,8 +622,8 @@ def connect_as(self, **kwargs: ty.Any) -> ty_ext.Self: # Utility function to help with the stripping below. def pop_keys( - params: dict[str, dict[str, ty.Optional[str]]], - auth: dict[str, ty.Optional[str]], + params: dict[str, dict[str, str | None]], + auth: dict[str, str | None], name_key: str, id_key: str, ) -> None: @@ -695,9 +694,9 @@ def connect_as_project(self, project: str) -> ty_ext.Self: def endpoint_for( self, service_type: str, - interface: ty.Optional[str] = None, - region_name: ty.Optional[str] = None, - ) -> ty.Optional[str]: + interface: str | None = None, + region_name: str | None = None, + ) -> str | None: """Return the endpoint for a given service. Respects config values for Connection, including diff --git a/openstack/container_infrastructure_management/v1/_proxy.py b/openstack/container_infrastructure_management/v1/_proxy.py index fb9f23a75..09e2d864a 100644 --- a/openstack/container_infrastructure_management/v1/_proxy.py +++ b/openstack/container_infrastructure_management/v1/_proxy.py @@ -266,11 +266,11 @@ def wait_for_status( self, res: resource.ResourceT, status: str, - failures: ty.Optional[list[str]] = None, - interval: ty.Union[int, float, None] = 2, - wait: ty.Optional[int] = None, + failures: list[str] | None = None, + interval: int | float | None = 2, + wait: int | None = None, attribute: str = 'status', - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for the resource to be in a particular status. @@ -306,7 +306,7 @@ def wait_for_delete( res: resource.ResourceT, interval: int = 2, wait: int = 120, - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for a resource to be deleted. diff --git a/openstack/database/v1/_proxy.py b/openstack/database/v1/_proxy.py index a601e2f07..5e3cba2b8 100644 --- a/openstack/database/v1/_proxy.py +++ b/openstack/database/v1/_proxy.py @@ -342,11 +342,11 @@ def wait_for_status( self, res: resource.ResourceT, status: str, - failures: ty.Optional[list[str]] = None, - interval: ty.Union[int, float, None] = 2, - wait: ty.Optional[int] = None, + failures: list[str] | None = None, + interval: int | float | None = 2, + wait: int | None = None, attribute: str = 'status', - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for the resource to be in a particular status. @@ -382,7 +382,7 @@ def wait_for_delete( res: resource.ResourceT, interval: int = 2, wait: int = 120, - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for a resource to be deleted. diff --git a/openstack/dns/v2/_base.py b/openstack/dns/v2/_base.py index 210beb599..6b2d8804a 100644 --- a/openstack/dns/v2/_base.py +++ b/openstack/dns/v2/_base.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import typing as ty import urllib.parse from openstack import exceptions @@ -80,7 +79,7 @@ def list( all_projects=None, **params, ): - headers: ty.Union[ty.Union[dict[str, str], None]] = ( + headers: dict[str, str] | None = ( {} if project_id or all_projects else None ) @@ -95,7 +94,7 @@ def list( @classmethod def _get_next_link(cls, uri, response, data, marker, limit, total_yielded): next_link = None - params: dict[str, ty.Union[list[str], str]] = {} + params: dict[str, list[str] | str] = {} if isinstance(data, dict): links = data.get('links') if links: diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py index b4978699e..c7c4539c7 100644 --- a/openstack/dns/v2/_proxy.py +++ b/openstack/dns/v2/_proxy.py @@ -720,11 +720,11 @@ def wait_for_status( self, res: resource.ResourceT, status: str, - failures: ty.Optional[list[str]] = None, - interval: ty.Union[int, float, None] = 2, - wait: ty.Optional[int] = None, + failures: list[str] | None = None, + interval: int | float | None = 2, + wait: int | None = None, attribute: str = 'status', - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for the resource to be in a particular status. @@ -760,7 +760,7 @@ def wait_for_delete( res: resource.ResourceT, interval: int = 2, wait: int = 120, - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for a resource to be deleted. diff --git a/openstack/exceptions.py b/openstack/exceptions.py index 445b4ae16..dcef7f561 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -33,9 +33,7 @@ class SDKException(Exception): """The base exception class for all exceptions this library raises.""" - def __init__( - self, message: ty.Optional[str] = None, extra_data: ty.Any = None - ): + def __init__(self, message: str | None = None, extra_data: ty.Any = None): self.message = self.__class__.__name__ if message is None else message self.extra_data = extra_data super().__init__(self.message) @@ -44,21 +42,21 @@ def __init__( class EndpointNotFound(SDKException): """A mismatch occurred between what the client and server expect.""" - def __init__(self, message: ty.Optional[str] = None): + def __init__(self, message: str | None = None): super().__init__(message) class InvalidResponse(SDKException): """The response from the server is not valid for this request.""" - def __init__(self, message: ty.Optional[str] = None): + def __init__(self, message: str | None = None): super().__init__(message) class InvalidRequest(SDKException): """The request to the server is not valid.""" - def __init__(self, message: ty.Optional[str] = None): + def __init__(self, message: str | None = None): super().__init__(message) @@ -66,15 +64,15 @@ class HttpException(SDKException, _rex.HTTPError): """The base exception for all HTTP error responses.""" source: str - status_code: ty.Optional[int] + status_code: int | None def __init__( self, - message: ty.Optional[str] = 'Error', - response: ty.Optional[requests.Response] = None, - http_status: ty.Optional[int] = None, - details: ty.Optional[str] = None, - request_id: ty.Optional[str] = None, + message: str | None = 'Error', + response: requests.Response | None = None, + http_status: int | None = None, + details: str | None = None, + request_id: str | None = None, ): if http_status is not None: warnings.warn( @@ -190,7 +188,7 @@ class InvalidResourceQuery(SDKException): """Invalid query params for resource.""" -def _extract_message(obj: ty.Any) -> ty.Optional[str]: +def _extract_message(obj: ty.Any) -> str | None: if isinstance(obj, dict): # Most of services: compute, network if obj.get('message'): @@ -212,7 +210,7 @@ def _extract_message(obj: ty.Any) -> ty.Optional[str]: def raise_from_response( response: requests.Response, - error_message: ty.Optional[str] = None, + error_message: str | None = None, ) -> None: """Raise an instance of an HTTPException based on keystoneauth response.""" if response.status_code < 400: diff --git a/openstack/fields.py b/openstack/fields.py index 68fed3013..78c5a1b16 100644 --- a/openstack/fields.py +++ b/openstack/fields.py @@ -84,25 +84,19 @@ def _convert_type( def _convert_type( value: _T1, - data_type: ty.Optional[ - type[ - ty.Union[ - _T3, - list[ty.Any], - dict[ty.Any, ty.Any], - format.Formatter[_T2], - ], - ] - ], - list_type: ty.Optional[type[_T3]] = None, -) -> ty.Union[_T1, _T3, list[_T3], list[_T1], dict[ty.Any, ty.Any], _T2]: + data_type: type[ + _T3 | list[ty.Any] | dict[ty.Any, ty.Any] | format.Formatter[_T2], + ] + | None, + list_type: type[_T3] | None = None, +) -> _T1 | _T3 | list[_T3] | list[_T1] | dict[ty.Any, ty.Any] | _T2: # This should allow handling list of dicts that have their own # Component type directly. See openstack/compute/v2/limits.py # and the RateLimit type for an example. if data_type is None: return value elif issubclass(data_type, list): - if isinstance(value, (list, set, tuple)): + if isinstance(value, list | set | tuple): if not list_type: return data_type(value) return [_convert_type(x, list_type) for x in value] @@ -121,8 +115,8 @@ def _convert_type( return data_type.deserialize(value) elif issubclass(data_type, bool): return data_type(value) - elif issubclass(data_type, (int, float)): - if isinstance(value, (int, float)): + elif issubclass(data_type, int | float): + if isinstance(value, int | float): return data_type(value) if isinstance(value, str): if issubclass(data_type, int) and value.isdigit(): @@ -149,28 +143,28 @@ class _BaseComponent(abc.ABC): _map_cls: ty.ClassVar[type[ty.MutableMapping[str, ty.Any]]] = dict name: str - data_type: ty.Optional[ty.Any] + data_type: ty.Any | None default: ty.Any - alias: ty.Optional[str] - aka: ty.Optional[str] + alias: str | None + aka: str | None alternate_id: bool - list_type: ty.Optional[ty.Any] + list_type: ty.Any | None coerce_to_default: bool deprecated: bool - deprecation_reason: ty.Optional[str] + deprecation_reason: str | None def __init__( self, name: str, - type: ty.Optional[ty.Any] = None, + type: ty.Any | None = None, default: ty.Any = None, - alias: ty.Optional[str] = None, - aka: ty.Optional[str] = None, + alias: str | None = None, + aka: str | None = None, alternate_id: bool = False, - list_type: ty.Optional[ty.Any] = None, + list_type: ty.Any | None = None, coerce_to_default: bool = False, deprecated: bool = False, - deprecation_reason: ty.Optional[str] = None, + deprecation_reason: str | None = None, ): """A typed descriptor for a component that makes up a Resource @@ -214,7 +208,7 @@ def __init__( def __get__( self, instance: object, - owner: ty.Optional[type[object]] = None, + owner: type[object] | None = None, ) -> ty.Any: if instance is None: return self @@ -261,7 +255,7 @@ def __get__( return _convert_type(value, self.data_type, self.list_type) @property - def type(self) -> ty.Optional[ty.Any]: + def type(self) -> ty.Any | None: # deprecated alias proxy return self.data_type diff --git a/openstack/identity/v2/_proxy.py b/openstack/identity/v2/_proxy.py index d6282799f..e61143821 100644 --- a/openstack/identity/v2/_proxy.py +++ b/openstack/identity/v2/_proxy.py @@ -282,11 +282,11 @@ def wait_for_status( self, res: resource.ResourceT, status: str, - failures: ty.Optional[list[str]] = None, - interval: ty.Union[int, float, None] = 2, - wait: ty.Optional[int] = None, + failures: list[str] | None = None, + interval: int | float | None = 2, + wait: int | None = None, attribute: str = 'status', - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for the resource to be in a particular status. @@ -322,7 +322,7 @@ def wait_for_delete( res: resource.ResourceT, interval: int = 2, wait: int = 120, - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for a resource to be deleted. diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 207ba661c..b0e78818b 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -2396,11 +2396,11 @@ def wait_for_status( self, res: resource.ResourceT, status: str, - failures: ty.Optional[list[str]] = None, - interval: ty.Union[int, float, None] = 2, - wait: ty.Optional[int] = None, + failures: list[str] | None = None, + interval: int | float | None = 2, + wait: int | None = None, attribute: str = 'status', - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for the resource to be in a particular status. @@ -2436,7 +2436,7 @@ def wait_for_delete( res: resource.ResourceT, interval: int = 2, wait: int = 120, - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for a resource to be deleted. diff --git a/openstack/image/v1/_proxy.py b/openstack/image/v1/_proxy.py index bbe96b8f0..9736ab289 100644 --- a/openstack/image/v1/_proxy.py +++ b/openstack/image/v1/_proxy.py @@ -482,11 +482,11 @@ def wait_for_status( self, res: resource.ResourceT, status: str, - failures: ty.Optional[list[str]] = None, - interval: ty.Union[int, float, None] = 2, - wait: ty.Optional[int] = None, + failures: list[str] | None = None, + interval: int | float | None = 2, + wait: int | None = None, attribute: str = 'status', - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for the resource to be in a particular status. @@ -522,7 +522,7 @@ def wait_for_delete( res: resource.ResourceT, interval: int = 2, wait: int = 120, - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for a resource to be deleted. diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 307e2af9e..6b017ee46 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -1900,11 +1900,11 @@ def wait_for_status( self, res: resource.ResourceT, status: str, - failures: ty.Optional[list[str]] = None, - interval: ty.Union[int, float, None] = 2, - wait: ty.Optional[int] = None, + failures: list[str] | None = None, + interval: int | float | None = 2, + wait: int | None = None, attribute: str = 'status', - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for the resource to be in a particular status. @@ -1940,7 +1940,7 @@ def wait_for_delete( res: resource.ResourceT, interval: int = 2, wait: int = 120, - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for a resource to be deleted. diff --git a/openstack/instance_ha/v1/_proxy.py b/openstack/instance_ha/v1/_proxy.py index b98b63cf3..de3f37af8 100644 --- a/openstack/instance_ha/v1/_proxy.py +++ b/openstack/instance_ha/v1/_proxy.py @@ -267,11 +267,11 @@ def wait_for_status( self, res: resource.ResourceT, status: str, - failures: ty.Optional[list[str]] = None, - interval: ty.Union[int, float, None] = 2, - wait: ty.Optional[int] = None, + failures: list[str] | None = None, + interval: int | float | None = 2, + wait: int | None = None, attribute: str = 'status', - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for the resource to be in a particular status. @@ -307,7 +307,7 @@ def wait_for_delete( res: resource.ResourceT, interval: int = 2, wait: int = 120, - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for a resource to be deleted. diff --git a/openstack/key_manager/v1/_proxy.py b/openstack/key_manager/v1/_proxy.py index 7aad2cffd..05f3f347a 100644 --- a/openstack/key_manager/v1/_proxy.py +++ b/openstack/key_manager/v1/_proxy.py @@ -276,11 +276,11 @@ def wait_for_status( self, res: resource.ResourceT, status: str, - failures: ty.Optional[list[str]] = None, - interval: ty.Union[int, float, None] = 2, - wait: ty.Optional[int] = None, + failures: list[str] | None = None, + interval: int | float | None = 2, + wait: int | None = None, attribute: str = 'status', - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for the resource to be in a particular status. @@ -316,7 +316,7 @@ def wait_for_delete( res: resource.ResourceT, interval: int = 2, wait: int = 120, - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for a resource to be deleted. diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index 0e2cb618c..0c5f74785 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -1292,11 +1292,11 @@ def wait_for_status( self, res: resource.ResourceT, status: str, - failures: ty.Optional[list[str]] = None, - interval: ty.Union[int, float, None] = 2, - wait: ty.Optional[int] = None, + failures: list[str] | None = None, + interval: int | float | None = 2, + wait: int | None = None, attribute: str = 'status', - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for the resource to be in a particular status. @@ -1332,7 +1332,7 @@ def wait_for_delete( res: resource.ResourceT, interval: int = 2, wait: int = 120, - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for a resource to be deleted. diff --git a/openstack/message/v2/_proxy.py b/openstack/message/v2/_proxy.py index f23373ae4..db5c11f28 100644 --- a/openstack/message/v2/_proxy.py +++ b/openstack/message/v2/_proxy.py @@ -311,11 +311,11 @@ def wait_for_status( self, res: resource.ResourceT, status: str, - failures: ty.Optional[list[str]] = None, - interval: ty.Union[int, float, None] = 2, - wait: ty.Optional[int] = None, + failures: list[str] | None = None, + interval: int | float | None = 2, + wait: int | None = None, attribute: str = 'status', - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for the resource to be in a particular status. @@ -351,7 +351,7 @@ def wait_for_delete( res: resource.ResourceT, interval: int = 2, wait: int = 120, - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for a resource to be deleted. diff --git a/openstack/message/v2/message.py b/openstack/message/v2/message.py index 4041b181d..325a08c52 100644 --- a/openstack/message/v2/message.py +++ b/openstack/message/v2/message.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import typing as ty import uuid from openstack import resource @@ -56,7 +55,7 @@ class Message(resource.Resource): # FIXME(stephenfin): This is actually a query arg but we need it for # deletions and resource.delete doesn't respect these currently - claim_id: ty.Optional[str] = None + claim_id: str | None = None def post(self, session, messages): request = self._prepare_request(requires_id=False, prepend_key=True) diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index f46df3c4c..f0eab0065 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -199,9 +199,9 @@ class Proxy(proxy.Proxy): def _update( self, resource_type: type[resource.ResourceT], - value: ty.Union[str, resource.ResourceT, None], - base_path: ty.Optional[str] = None, - if_revision: ty.Optional[int] = None, + value: str | resource.ResourceT | None, + base_path: str | None = None, + if_revision: int | None = None, **attrs: ty.Any, ) -> resource.ResourceT: if ( @@ -217,11 +217,11 @@ def _update( def _delete( self, resource_type: type[resource.ResourceT], - value: ty.Union[str, resource.ResourceT, None], + value: str | resource.ResourceT | None, ignore_missing: bool = True, - if_revision: ty.Optional[int] = None, + if_revision: int | None = None, **attrs: ty.Any, - ) -> ty.Optional[resource.ResourceT]: + ) -> resource.ResourceT | None: if ( issubclass(resource_type, _base.NetworkResource) and if_revision is not None @@ -322,7 +322,7 @@ def address_groups(self, **query): def update_address_group( self, - address_group: ty.Union[str, _address_group.AddressGroup], + address_group: str | _address_group.AddressGroup, **attrs: ty.Any, ) -> _address_group.AddressGroup: """Update an address group @@ -2998,8 +2998,8 @@ def ports(self, **query): def update_port( self, - port: ty.Union[str, _port.Port], - if_revision: ty.Optional[int] = None, + port: str | _port.Port, + if_revision: int | None = None, **attrs: ty.Any, ) -> _port.Port: """Update a port @@ -7238,11 +7238,11 @@ def wait_for_status( self, res: resource.ResourceT, status: str, - failures: ty.Optional[list[str]] = None, - interval: ty.Union[int, float, None] = 2, - wait: ty.Optional[int] = None, + failures: list[str] | None = None, + interval: int | float | None = 2, + wait: int | None = None, attribute: str = 'status', - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for the resource to be in a particular status. @@ -7278,7 +7278,7 @@ def wait_for_delete( res: resource.ResourceT, interval: int = 2, wait: int = 120, - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for a resource to be deleted. diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index 5191801f2..1958a16d4 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -1044,7 +1044,7 @@ def generate_temp_url( method.upper(), ) - expiration: ty.Union[float, int] + expiration: float | int if not absolute: expiration = _get_expiration(timestamp) else: @@ -1129,11 +1129,11 @@ def wait_for_status( self, res: resource.ResourceT, status: str, - failures: ty.Optional[list[str]] = None, - interval: ty.Union[int, float, None] = 2, - wait: ty.Optional[int] = None, + failures: list[str] | None = None, + interval: int | float | None = 2, + wait: int | None = None, attribute: str = 'status', - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for the resource to be in a particular status. @@ -1169,7 +1169,7 @@ def wait_for_delete( res: resource.ResourceT, interval: int = 2, wait: int = 120, - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for a resource to be deleted. diff --git a/openstack/orchestration/util/template_utils.py b/openstack/orchestration/util/template_utils.py index 43518e38f..6673c5b71 100644 --- a/openstack/orchestration/util/template_utils.py +++ b/openstack/orchestration/util/template_utils.py @@ -89,7 +89,7 @@ def ignore_if(key, value): return False def recurse_if(value): - return isinstance(value, (dict, list)) + return isinstance(value, dict | list) get_file_contents( template, diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index 9a7a40b60..a20c2d5a8 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -577,11 +577,11 @@ def wait_for_status( self, res: resource.ResourceT, status: str, - failures: ty.Optional[list[str]] = None, - interval: ty.Union[int, float, None] = 2, - wait: ty.Optional[int] = None, + failures: list[str] | None = None, + interval: int | float | None = 2, + wait: int | None = None, attribute: str = 'status', - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for the resource to be in a particular status. @@ -617,7 +617,7 @@ def wait_for_delete( res: resource.ResourceT, interval: int = 2, wait: int = 120, - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for a resource to be deleted. diff --git a/openstack/placement/v1/_proxy.py b/openstack/placement/v1/_proxy.py index 3c726a016..d0f058233 100644 --- a/openstack/placement/v1/_proxy.py +++ b/openstack/placement/v1/_proxy.py @@ -469,11 +469,11 @@ def wait_for_status( self, res: resource.ResourceT, status: str, - failures: ty.Optional[list[str]] = None, - interval: ty.Union[int, float, None] = 2, - wait: ty.Optional[int] = None, + failures: list[str] | None = None, + interval: int | float | None = 2, + wait: int | None = None, attribute: str = 'status', - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for the resource to be in a particular status. @@ -509,7 +509,7 @@ def wait_for_delete( res: resource.ResourceT, interval: int = 2, wait: int = 120, - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for a resource to be deleted. diff --git a/openstack/resource.py b/openstack/resource.py index 47ffdde69..cb8281b89 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -65,15 +65,15 @@ class that represent a remote resource. The attributes that # with Any rather than generating super complex types def Body( name: str, - type: ty.Optional[ty.Any] = None, + type: ty.Any | None = None, default: ty.Any = None, - alias: ty.Optional[str] = None, - aka: ty.Optional[str] = None, + alias: str | None = None, + aka: str | None = None, alternate_id: bool = False, - list_type: ty.Optional[ty.Any] = None, + list_type: ty.Any | None = None, coerce_to_default: bool = False, deprecated: bool = False, - deprecation_reason: ty.Optional[str] = None, + deprecation_reason: str | None = None, ) -> ty.Any: return fields.Body( name, @@ -91,15 +91,15 @@ def Body( def Header( name: str, - type: ty.Optional[ty.Any] = None, + type: ty.Any | None = None, default: ty.Any = None, - alias: ty.Optional[str] = None, - aka: ty.Optional[str] = None, + alias: str | None = None, + aka: str | None = None, alternate_id: bool = False, - list_type: ty.Optional[ty.Any] = None, + list_type: ty.Any | None = None, coerce_to_default: bool = False, deprecated: bool = False, - deprecation_reason: ty.Optional[str] = None, + deprecation_reason: str | None = None, ) -> ty.Any: return fields.Header( name, @@ -117,15 +117,15 @@ def Header( def URI( name: str, - type: ty.Optional[ty.Any] = None, + type: ty.Any | None = None, default: ty.Any = None, - alias: ty.Optional[str] = None, - aka: ty.Optional[str] = None, + alias: str | None = None, + aka: str | None = None, alternate_id: bool = False, - list_type: ty.Optional[ty.Any] = None, + list_type: ty.Any | None = None, coerce_to_default: bool = False, deprecated: bool = False, - deprecation_reason: ty.Optional[str] = None, + deprecation_reason: str | None = None, ) -> ty.Any: return fields.URI( name, @@ -143,15 +143,15 @@ def URI( def Computed( name: str, - type: ty.Optional[ty.Any] = None, + type: ty.Any | None = None, default: ty.Any = None, - alias: ty.Optional[str] = None, - aka: ty.Optional[str] = None, + alias: str | None = None, + aka: str | None = None, alternate_id: bool = False, - list_type: ty.Optional[ty.Any] = None, + list_type: ty.Any | None = None, coerce_to_default: bool = False, deprecated: bool = False, - deprecation_reason: ty.Optional[str] = None, + deprecation_reason: str | None = None, ) -> ty.Any: return fields.Computed( name, @@ -251,7 +251,7 @@ def __init__( parameters, ``limit`` and ``marker``. These are the most common query parameters used for listing resources in OpenStack APIs. """ - self._mapping: dict[str, ty.Union[str, dict]] = {} + self._mapping: dict[str, str | dict] = {} if include_pagination_defaults: self._mapping.update({"limit": "limit", "marker": "marker"}) self._mapping.update({name: name for name in names}) @@ -351,11 +351,11 @@ class Resource(dict): # will work properly. #: Singular form of key for resource. - resource_key: ty.Optional[str] = None + resource_key: str | None = None #: Plural form of key for resource. - resources_key: ty.Optional[str] = None + resources_key: str | None = None #: Key used for pagination links - pagination_key: ty.Optional[str] = None + pagination_key: str | None = None #: The ID of this resource. id: str = Body("id") @@ -399,16 +399,16 @@ class Resource(dict): #: Do calls for this resource require an id requires_id = True #: Whether create requires an ID (determined from method if None). - create_requires_id: ty.Optional[bool] = None + create_requires_id: bool | None = None #: Whether create should exclude ID in the body of the request. create_exclude_id_from_body = False #: Do responses for this resource have bodies has_body = True #: Does create returns a body (if False requires ID), defaults to has_body - create_returns_body: ty.Optional[bool] = None + create_returns_body: bool | None = None #: Maximum microversion to use for getting/creating/updating the Resource - _max_microversion: ty.Optional[str] = None + _max_microversion: str | None = None #: API microversion (string or None) this Resource was loaded with microversion = None @@ -979,7 +979,7 @@ def to_dict( :return: A dictionary of key/value pairs where keys are named as they exist as attributes of this class. """ - mapping: ty.Union[utils.Munch, dict] + mapping: utils.Munch | dict if _to_munch: mapping = utils.Munch() else: @@ -1073,7 +1073,7 @@ def _prepare_request_body( *, resource_request_key=None, ): - body: ty.Union[dict[str, ty.Any], list[ty.Any]] + body: dict[str, ty.Any] | list[ty.Any] if patch: if not self._store_unknown_attrs_as_properties: # Default case @@ -1250,7 +1250,7 @@ def _get_session(cls, session: AdapterT) -> AdapterT: ) @classmethod - def _get_microversion(cls, session: adapter.Adapter) -> ty.Optional[str]: + def _get_microversion(cls, session: adapter.Adapter) -> str | None: """Get microversion to use for the given action. The base version uses the following logic: @@ -1278,10 +1278,10 @@ def _get_microversion(cls, session: adapter.Adapter) -> ty.Optional[str]: def _assert_microversion_for( cls, session: adapter.Adapter, - expected: ty.Optional[str], + expected: str | None, *, - error_message: ty.Optional[str] = None, - maximum: ty.Optional[str] = None, + error_message: str | None = None, + maximum: str | None = None, ) -> str: """Enforce that the microversion for action satisfies the requirement. @@ -1336,11 +1336,11 @@ def create( self, session: adapter.Adapter, prepend_key: bool = True, - base_path: ty.Optional[str] = None, + base_path: str | None = None, *, - resource_request_key: ty.Optional[str] = None, - resource_response_key: ty.Optional[str] = None, - microversion: ty.Optional[str] = None, + resource_request_key: str | None = None, + resource_response_key: str | None = None, + microversion: str | None = None, **params: ty.Any, ) -> ty_ext.Self: """Create a remote resource based on this instance. @@ -1438,9 +1438,9 @@ def bulk_create( session: adapter.Adapter, data: list[dict[str, ty.Any]], prepend_key: bool = True, - base_path: ty.Optional[str] = None, + base_path: str | None = None, *, - microversion: ty.Optional[str] = None, + microversion: str | None = None, **params: ty.Any, ) -> ty.Generator[ty_ext.Self, None, None]: """Create multiple remote resources based on this class and data. @@ -1505,7 +1505,7 @@ def bulk_create( ) _body.append(request.body) - body: ty.Union[dict[str, ty.Any], list[ty.Any]] = _body + body: dict[str, ty.Any] | list[ty.Any] = _body if prepend_key: if not cls.resources_key: @@ -1559,12 +1559,12 @@ def fetch( self, session: adapter.Adapter, requires_id: bool = True, - base_path: ty.Optional[str] = None, - error_message: ty.Optional[str] = None, + base_path: str | None = None, + error_message: str | None = None, skip_cache: bool = False, *, - resource_response_key: ty.Optional[str] = None, - microversion: ty.Optional[str] = None, + resource_response_key: str | None = None, + microversion: str | None = None, **params: ty.Any, ) -> ty_ext.Self: """Get a remote resource based on this instance. @@ -1620,9 +1620,9 @@ def fetch( def head( self, session: adapter.Adapter, - base_path: ty.Optional[str] = None, + base_path: str | None = None, *, - microversion: ty.Optional[str] = None, + microversion: str | None = None, ) -> ty_ext.Self: """Get headers from a remote resource based on this instance. @@ -1665,10 +1665,10 @@ def commit( session: adapter.Adapter, prepend_key: bool = True, has_body: bool = True, - retry_on_conflict: ty.Optional[bool] = None, - base_path: ty.Optional[str] = None, + retry_on_conflict: bool | None = None, + base_path: str | None = None, *, - microversion: ty.Optional[str] = None, + microversion: str | None = None, **kwargs: ty.Any, ) -> ty_ext.Self: """Commit the state of the instance to the remote resource. @@ -1857,9 +1857,9 @@ def patch( def delete( self, session: adapter.Adapter, - error_message: ty.Optional[str] = None, + error_message: str | None = None, *, - microversion: ty.Optional[str] = None, + microversion: str | None = None, **kwargs: ty.Any, ) -> ty_ext.Self: """Delete the remote resource based on this instance. @@ -1909,11 +1909,11 @@ def list( cls, session: adapter.Adapter, paginated: bool = True, - base_path: ty.Optional[str] = None, + base_path: str | None = None, allow_unknown_params: bool = False, *, - microversion: ty.Optional[str] = None, - headers: ty.Optional[dict[str, str]] = None, + microversion: str | None = None, + headers: dict[str, str] | None = None, **params: ty.Any, ) -> ty.Generator[ty_ext.Self, None, None]: """This method is a generator which yields resource objects. @@ -2186,12 +2186,12 @@ def find( session: adapter.Adapter, name_or_id: str, ignore_missing: ty.Literal[True] = True, - list_base_path: ty.Optional[str] = None, + list_base_path: str | None = None, *, - microversion: ty.Optional[str] = None, - all_projects: ty.Optional[bool] = None, + microversion: str | None = None, + all_projects: bool | None = None, **params: ty.Any, - ) -> ty.Optional[ty_ext.Self]: ... + ) -> ty_ext.Self | None: ... @ty.overload @classmethod @@ -2200,10 +2200,10 @@ def find( session: adapter.Adapter, name_or_id: str, ignore_missing: ty.Literal[False], - list_base_path: ty.Optional[str] = None, + list_base_path: str | None = None, *, - microversion: ty.Optional[str] = None, - all_projects: ty.Optional[bool] = None, + microversion: str | None = None, + all_projects: bool | None = None, **params: ty.Any, ) -> ty_ext.Self: ... @@ -2216,12 +2216,12 @@ def find( session: adapter.Adapter, name_or_id: str, ignore_missing: bool, - list_base_path: ty.Optional[str] = None, + list_base_path: str | None = None, *, - microversion: ty.Optional[str] = None, - all_projects: ty.Optional[bool] = None, + microversion: str | None = None, + all_projects: bool | None = None, **params: ty.Any, - ) -> ty.Optional[ty_ext.Self]: ... + ) -> ty_ext.Self | None: ... @classmethod def find( @@ -2229,12 +2229,12 @@ def find( session: adapter.Adapter, name_or_id: str, ignore_missing: bool = True, - list_base_path: ty.Optional[str] = None, + list_base_path: str | None = None, *, - microversion: ty.Optional[str] = None, - all_projects: ty.Optional[bool] = None, + microversion: str | None = None, + all_projects: bool | None = None, **params: ty.Any, - ) -> ty.Optional[ty_ext.Self]: + ) -> ty_ext.Self | None: """Find a resource by its name or id. :param session: The session to use for making this request. @@ -2311,7 +2311,7 @@ def find( ) -def _normalize_status(status: ty.Optional[str]) -> ty.Optional[str]: +def _normalize_status(status: str | None) -> str | None: if status is not None: status = status.lower() return status @@ -2321,11 +2321,11 @@ def wait_for_status( session: adapter.Adapter, resource: ResourceT, status: str, - failures: ty.Optional[list[str]] = None, - interval: ty.Union[int, float, None] = 2, - wait: ty.Optional[int] = None, + failures: list[str] | None = None, + interval: int | float | None = 2, + wait: int | None = None, attribute: str = 'status', - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> ResourceT: """Wait for the resource to be in a particular status. @@ -2399,9 +2399,9 @@ def wait_for_status( def wait_for_delete( session: adapter.Adapter, resource: ResourceT, - interval: ty.Union[int, float, None] = 2, - wait: ty.Optional[int] = None, - callback: ty.Optional[ty.Callable[[int], None]] = None, + interval: int | float | None = 2, + wait: int | None = None, + callback: ty.Callable[[int], None] | None = None, ) -> ResourceT: """Wait for a resource to be deleted. diff --git a/openstack/service_description.py b/openstack/service_description.py index c29835836..5d4dc3903 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -11,7 +11,6 @@ # License for the specific language governing permissions and limitations # under the License. -import typing as ty import warnings import os_service_types @@ -54,10 +53,8 @@ class ServiceDescription: def __init__( self, service_type: str, - supported_versions: ty.Optional[ - dict[str, type[proxy_mod.Proxy]] - ] = None, - aliases: ty.Optional[list[str]] = None, + supported_versions: dict[str, type[proxy_mod.Proxy]] | None = None, + aliases: list[str] | None = None, ): """Class describing how to interact with a REST service. diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index 217089469..e49cf43f9 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -1191,11 +1191,11 @@ def wait_for_status( self, res: resource.ResourceT, status: str, - failures: ty.Optional[list[str]] = None, - interval: ty.Union[int, float, None] = 2, - wait: ty.Optional[int] = None, + failures: list[str] | None = None, + interval: int | float | None = 2, + wait: int | None = None, attribute: str = 'status', - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for the resource to be in a particular status. @@ -1231,7 +1231,7 @@ def wait_for_delete( res: resource.ResourceT, interval: int = 2, wait: int = 120, - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for a resource to be deleted. diff --git a/openstack/test/fakes.py b/openstack/test/fakes.py index 5c11b6238..3ac22b863 100644 --- a/openstack/test/fakes.py +++ b/openstack/test/fakes.py @@ -59,7 +59,7 @@ def generate_fake_resource( base_attrs: dict[str, ty.Any] = {} for name, value in inspect.getmembers( resource_type, - predicate=lambda x: isinstance(x, (fields.Body, fields.URI)), + predicate=lambda x: isinstance(x, fields.Body | fields.URI), ): if isinstance(value, fields.Body): target_type = value.type @@ -131,7 +131,7 @@ def generate_fake_resource( def generate_fake_resources( resource_type: type[resource.ResourceT], count: int = 1, - attrs: ty.Optional[dict[str, ty.Any]] = None, + attrs: dict[str, ty.Any] | None = None, ) -> Generator[resource.ResourceT, None, None]: """Generate a given number of fake resource entities @@ -161,7 +161,7 @@ def generate_fake_resources( # (better) type annotations def generate_fake_proxy( service: type[service_description.ServiceDescription], - api_version: ty.Optional[str] = None, + api_version: str | None = None, ) -> proxy.Proxy: """Generate a fake proxy for the given service type diff --git a/openstack/tests/functional/baremetal/base.py b/openstack/tests/functional/baremetal/base.py index 0d186eda1..55260574d 100644 --- a/openstack/tests/functional/baremetal/base.py +++ b/openstack/tests/functional/baremetal/base.py @@ -10,13 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -import typing as ty from openstack.tests.functional import base class BaseBaremetalTest(base.BaseFunctionalTest): - min_microversion: ty.Optional[str] = None + min_microversion: str | None = None node_id: str def setUp(self): diff --git a/openstack/tests/functional/shared_file_system/base.py b/openstack/tests/functional/shared_file_system/base.py index c8e235bb1..ab36de174 100644 --- a/openstack/tests/functional/shared_file_system/base.py +++ b/openstack/tests/functional/shared_file_system/base.py @@ -10,14 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -import typing as ty from openstack import resource from openstack.tests.functional import base class BaseSharedFileSystemTest(base.BaseFunctionalTest): - min_microversion: ty.Optional[str] = None + min_microversion: str | None = None def setUp(self): super().setUp() diff --git a/openstack/utils.py b/openstack/utils.py index efde58e89..74e9525b6 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -27,7 +27,7 @@ from openstack import exceptions -def urljoin(*args: ty.Optional[str]) -> str: +def urljoin(*args: str | None) -> str: """A custom version of urljoin that simply joins strings into a path. The real urljoin takes into account web semantics like when joining a url @@ -38,9 +38,9 @@ def urljoin(*args: ty.Optional[str]) -> str: def iterate_timeout( - timeout: ty.Optional[int], + timeout: int | None, message: str, - wait: ty.Union[int, float, None] = 2, + wait: int | float | None = 2, ) -> ty.Generator[int, None, None]: """Iterate and raise an exception on timeout. @@ -163,9 +163,7 @@ def _supports_version() -> bool: def supports_microversion( adapter: ks_adapter.Adapter, - microversion: ty.Union[ - str, int, float, ty.Iterable[ty.Union[str, int, float]] - ], + microversion: str | int | float | ty.Iterable[str | int | float], raise_exception: bool = False, ) -> bool: """Determine if the given adapter supports the given microversion. @@ -238,7 +236,7 @@ def require_microversion(adapter: ks_adapter.Adapter, required: str) -> None: def pick_microversion( session: ks_adapter.Adapter, required: str -) -> ty.Optional[str]: +) -> str | None: """Get a new microversion if it is higher than session's default. :param session: The session to use for making this request. @@ -281,8 +279,8 @@ def pick_microversion( def maximum_supported_microversion( adapter: ks_adapter.Adapter, - client_maximum: ty.Optional[str], -) -> ty.Optional[str]: + client_maximum: str | None, +) -> str | None: """Determine the maximum microversion supported by both client and server. :param adapter: :class:`~keystoneauth1.adapter.Adapter` instance. @@ -331,8 +329,8 @@ def maximum_supported_microversion( def _hashes_up_to_date( - md5: ty.Optional[str], - sha256: ty.Optional[str], + md5: str | None, + sha256: str | None, md5_key: str, sha256_key: str, ) -> bool: @@ -354,7 +352,7 @@ def _hashes_up_to_date( def _calculate_data_hashes( - data: ty.Union[io.BufferedReader, bytes], + data: io.BufferedReader | bytes, ) -> tuple[str, str]: _md5 = hashlib.md5(usedforsecurity=False) _sha256 = hashlib.sha256() @@ -409,7 +407,7 @@ def add_node(self, node: str) -> None: def add_edge(self, u: str, v: str) -> None: self._graph[u].add(v) - def walk(self, timeout: ty.Optional[int] = None) -> 'TinyDAG': + def walk(self, timeout: int | None = None) -> 'TinyDAG': """Start the walking from the beginning.""" if timeout: self._wait_timeout = timeout diff --git a/openstack/workflow/v2/_proxy.py b/openstack/workflow/v2/_proxy.py index 881fd5361..3751eefc1 100644 --- a/openstack/workflow/v2/_proxy.py +++ b/openstack/workflow/v2/_proxy.py @@ -301,11 +301,11 @@ def wait_for_status( self, res: resource.ResourceT, status: str, - failures: ty.Optional[list[str]] = None, - interval: ty.Union[int, float, None] = 2, - wait: ty.Optional[int] = None, + failures: list[str] | None = None, + interval: int | float | None = 2, + wait: int | None = None, attribute: str = 'status', - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for the resource to be in a particular status. @@ -341,7 +341,7 @@ def wait_for_delete( res: resource.ResourceT, interval: int = 2, wait: int = 120, - callback: ty.Optional[ty.Callable[[int], None]] = None, + callback: ty.Callable[[int], None] | None = None, ) -> resource.ResourceT: """Wait for a resource to be deleted. diff --git a/pyproject.toml b/pyproject.toml index ec5d2d65c..7ba37e0b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.mypy] -python_version = "3.9" +python_version = "3.10" show_column_numbers = true show_error_context = true ignore_missing_imports = true @@ -52,6 +52,7 @@ ignore_errors = true [tool.ruff] line-length = 79 +target-version = "py310" [tool.ruff.format] quote-style = "preserve" From fa8939195707652352afa6526195619885686c10 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 7 Mar 2025 15:24:32 +0000 Subject: [PATCH 3710/3836] volume: Add missing fields to Volume Change-Id: I87110f91a14af0fd44d6504c42eb4a2313123c44 Signed-off-by: Stephen Finucane Depends-on: https://review.opendev.org/c/openstack/python-openstackclient/+/950300 --- openstack/block_storage/v2/volume.py | 6 +++-- openstack/block_storage/v3/volume.py | 37 ++++++++++++++++++++++------ 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/openstack/block_storage/v2/volume.py b/openstack/block_storage/v2/volume.py index 45431351e..b78616788 100644 --- a/openstack/block_storage/v2/volume.py +++ b/openstack/block_storage/v2/volume.py @@ -42,8 +42,6 @@ class Volume(resource.Resource, metadata.MetadataMixin): consistency_group_id = resource.Body("consistencygroup_id") #: The timestamp of this volume creation. created_at = resource.Body("created_at") - #: The date and time when the resource was updated. - updated_at = resource.Body("updated_at") #: The volume description. description = resource.Body("description") #: Extended replication status on this volume. @@ -61,6 +59,8 @@ class Volume(resource.Resource, metadata.MetadataMixin): #: ``True`` if this volume is encrypted, ``False`` if not. #: *Type: bool* is_encrypted = resource.Body("encrypted", type=format.BoolStr) + #: Whether volume will be sharable or not. + is_multiattach = resource.Body("multiattach", type=format.BoolStr) #: The volume ID that this volume's name on the back-end is based on. migration_id = resource.Body("os-vol-mig-status-attr:name_id") #: The status of this volume's migration (None means that a migration @@ -91,6 +91,8 @@ class Volume(resource.Resource, metadata.MetadataMixin): #: error_restoring. For details on these statuses, see the #: Block Storage API documentation. status = resource.Body("status") + #: The date and time when the resource was updated. + updated_at = resource.Body("updated_at") #: The user ID associated with the volume user_id = resource.Body("user_id") #: One or more metadata key and value pairs about image diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index 380346cda..a38671eb0 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -41,23 +41,33 @@ class Volume(resource.Resource, metadata.MetadataMixin): allow_list = True # Properties - #: TODO(briancurtin): This is currently undocumented in the API. - attachments = resource.Body("attachments") + #: Instance attachment information. If this volume is attached to a server + #: instance, the attachments list includes the UUID of the attached server, + #: an attachment UUID, the name of the attached host, if any, the volume + #: UUID, the device, and the device UUID. Otherwise, this list is empty. + attachments = resource.Body("attachments", type=list) #: The availability zone. availability_zone = resource.Body("availability_zone") #: ID of the consistency group. consistency_group_id = resource.Body("consistencygroup_id") + #: Whether this resource consumes quota or not. Resources that not counted + #: for quota usage are usually temporary internal resources created to + #: perform an operation. (since 3.65) + consumes_quota = resource.Body("consumes_quota") + #: The cluster name of volume backend. (since 3.61) + cluster_name = resource.Body("cluster_name") #: The timestamp of this volume creation. created_at = resource.Body("created_at") - #: The date and time when the resource was updated. - updated_at = resource.Body("updated_at") #: The volume description. description = resource.Body("description") + #: The UUID of the encryption key. Only included for encrypted volumes. + #: (since 3.64) + encryption_key_id = resource.Body("encryption_key_id") #: Extended replication status on this volume. extended_replication_status = resource.Body( "os-volume-replication:extended_status" ) - #: The ID of the group that the volume belongs to. + #: The ID of the group that the volume belongs to. (since 3.13) group_id = resource.Body("group_id") #: The volume's current back-end. host = resource.Body("os-vol-host-attr:host") @@ -83,13 +93,22 @@ class Volume(resource.Resource, metadata.MetadataMixin): replication_driver_data = resource.Body( "os-volume-replication:driver_data" ) - #: The provider ID for the volume. + #: The provider ID for the volume. (since 3.21) provider_id = resource.Body("provider_id") #: Status of replication on this volume. replication_status = resource.Body("replication_status") #: Scheduler hints for the volume scheduler_hints = resource.Body('OS-SCH-HNT:scheduler_hints', type=dict) - #: The size of the volume, in GBs. *Type: int* + #: A unique identifier that's used to indicate what node the volume-service + #: for a particular volume is being serviced by. (since 3.48) + service_uuid = resource.Body("service_uuid") + #: An indicator whether the host connecting the volume should lock for the + #: whole attach/detach process or not. true means only is iSCSI initiator + #: running on host doesn't support manual scans, false means never use + #: locks, and null means to always use locks. Look at os-brick's + #: guard_connection context manager. Default=True. (since 3.48) + shared_targets = resource.Body("shared_targets", type=bool) + #: The size of the volume, in GBs. size = resource.Body("size", type=int) #: To create a volume from an existing snapshot, specify the ID of #: the existing volume snapshot. If specified, the volume is created @@ -104,12 +123,16 @@ class Volume(resource.Resource, metadata.MetadataMixin): #: error_restoring. For details on these statuses, see the #: Block Storage API documentation. status = resource.Body("status") + #: The date and time when the resource was updated. + updated_at = resource.Body("updated_at") #: The user ID associated with the volume user_id = resource.Body("user_id") #: One or more metadata key and value pairs about image volume_image_metadata = resource.Body("volume_image_metadata") #: The name of the associated volume type. volume_type = resource.Body("volume_type") + #: The associated volume type ID for the volume. (since 3.61) + volume_type_id = resource.Body("volume_type_id") _max_microversion = "3.71" From 4dbbd3d46860e4312e647c270e0bec00a70d698a Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 19 May 2025 16:56:40 +0100 Subject: [PATCH 3711/3836] volume: Add volume group replication actions Change-Id: I5237464de7b4669b1267b91164c9487227c046c2 Signed-off-by: Stephen Finucane --- openstack/block_storage/v3/_proxy.py | 78 ++++++++++++++---- openstack/block_storage/v3/group.py | 59 +++++++++++++- .../tests/unit/block_storage/v3/test_group.py | 79 +++++++++++++++++++ .../tests/unit/block_storage/v3/test_proxy.py | 30 ++++++- ...-replication-actions-c64b2641625a5a2a.yaml | 4 + 5 files changed, 232 insertions(+), 18 deletions(-) create mode 100644 releasenotes/notes/add-volume-group-replication-actions-c64b2641625a5a2a.yaml diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 421644aa0..dd8bd947a 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -1625,6 +1625,31 @@ def create_group_from_source(self, **attrs): """ return _group.Group.create_from_source(self, **attrs) + def delete_group(self, group, delete_volumes=False): + """Delete a group + + :param group: The :class:`~openstack.block_storage.v3.group.Group` to + delete. + :param bool delete_volumes: When set to ``True``, volumes in group + will be deleted. + + :returns: ``None``. + """ + res = self._get_resource(_group.Group, group) + res.delete(self, delete_volumes=delete_volumes) + + def update_group(self, group, **attrs): + """Update a group + + :param group: The value can be the ID of a group or a + :class:`~openstack.block_storage.v3.group.Group` instance. + :param dict attrs: The attributes to update on the group. + + :returns: The updated group + :rtype: :class:`~openstack.volume.v3.group.Group` + """ + return self._update(_group.Group, group, **attrs) + def reset_group_status(self, group, status): """Reset group status @@ -1645,30 +1670,51 @@ def reset_group_state(self, group, status): ) return self.reset_group_status(group, status) - def delete_group(self, group, delete_volumes=False): - """Delete a group + def enable_group_replication(self, group): + """Enable replication for a group - :param group: The :class:`~openstack.block_storage.v3.group.Group` to - delete. - :param bool delete_volumes: When set to ``True``, volumes in group - will be deleted. + :param group: The :class:`~openstack.block_storage.v3.group.Group` + to enable replication for. - :returns: ``None``. + :returns: ``None`` """ res = self._get_resource(_group.Group, group) - res.delete(self, delete_volumes=delete_volumes) + return res.enable_replication(self) - def update_group(self, group, **attrs): - """Update a group + def disable_group_replication(self, group): + """Disable replication for a group - :param group: The value can be the ID of a group or a - :class:`~openstack.block_storage.v3.group.Group` instance. - :param dict attrs: The attributes to update on the group. + :param group: The :class:`~openstack.block_storage.v3.group.Group` + to disable replication for. - :returns: The updated group - :rtype: :class:`~openstack.volume.v3.group.Group` + :returns: ``None`` """ - return self._update(_group.Group, group, **attrs) + res = self._get_resource(_group.Group, group) + return res.disable_replication(self) + + def failover_group_replication( + self, + group, + *, + allowed_attached_volume=False, + secondary_backend_id=None, + ): + """Failover replication for a group + + :param group: The :class:`~openstack.block_storage.v3.group.Group` + to failover replication for. + :param allowed_attached_volume: Whether to allow attached volumes in + the group. + :param secondary_backend_id: The secondary backend ID. + + :returns: ``None`` + """ + res = self._get_resource(_group.Group, group) + return res.failover_replication( + self, + allowed_attached_volume=allowed_attached_volume, + secondary_backend_id=secondary_backend_id, + ) # ====== AVAILABILITY ZONES ====== def availability_zones(self): diff --git a/openstack/block_storage/v3/group.py b/openstack/block_storage/v3/group.py index 7d70cc848..f4973d78b 100644 --- a/openstack/block_storage/v3/group.py +++ b/openstack/block_storage/v3/group.py @@ -46,6 +46,7 @@ class Group(resource.Resource): group_snapshot_id = resource.Body("group_snapshot_id") group_type = resource.Body("group_type") project_id = resource.Body("project_id") + replication_targets = resource.Body("replication_targets", type=list) replication_status = resource.Body("replication_status") source_group_id = resource.Body("source_group_id") status = resource.Body("status") @@ -68,8 +69,64 @@ def delete(self, session, *args, delete_volumes=False, **kwargs): body = {'delete': {'delete-volumes': delete_volumes}} self._action(session, body) + def fetch_replication_targets(self, session): + """Fetch replication targets for the group. + + :param session: The session to use for making this request. + :return: This group with the ``replication_targets`` field populated. + """ + body = {'list_replication_targets': None} + response = self._action(session, body) + self._body.attributes.update( + {'replication_targets': response.json()['replication_targets']} + ) + return self + + def enable_replication(self, session): + """Enable replication for the group. + + :param session: The session to use for making this request. + """ + body = {'enable_replication': None} + self._action(session, body) + + def disable_replication(self, session): + """Disable replication for the group. + + :param session: The session to use for making this request. + """ + body = {'disable_replication': None} + self._action(session, body) + + def failover_replication( + self, + session, + *, + allowed_attached_volume=False, + secondary_backend_id=None, + ): + """Failover replication for the group. + + :param session: The session to use for making this request. + :param allowed_attached_volume: Whether to allow attached volumes in + the group. + :param secondary_backend_id: The secondary backend ID. + :returns: None + """ + body = { + 'modify_body_for_action': { + 'allow_attached_volume': allowed_attached_volume, + 'secondary_backend_id': secondary_backend_id, + }, + } + self._action(session, body) + def reset_status(self, session, status): - """Resets the status for a group.""" + """Resets the status for a group. + + :param session: The session to use for making this request. + :param status: The status for the group. + """ body = {'reset_status': {'status': status}} self._action(session, body) diff --git a/openstack/tests/unit/block_storage/v3/test_group.py b/openstack/tests/unit/block_storage/v3/test_group.py index 838954a6d..3925d15e9 100644 --- a/openstack/tests/unit/block_storage/v3/test_group.py +++ b/openstack/tests/unit/block_storage/v3/test_group.py @@ -93,6 +93,85 @@ def test_delete(self): url, json=body, microversion=sot._max_microversion ) + def test_enable_replication(self): + sot = group.Group(**GROUP) + + ret = sot.enable_replication(self.sess) + self.assertIsNone(ret) + + url = f'groups/{GROUP_ID}/action' + body = {'enable_replication': None} + self.sess.post.assert_called_with( + url, + json=body, + microversion=sot._max_microversion, + ) + + def test_disable_replication(self): + sot = group.Group(**GROUP) + + ret = sot.disable_replication(self.sess) + self.assertIsNone(ret) + + url = f'groups/{GROUP_ID}/action' + body = {'disable_replication': None} + self.sess.post.assert_called_with( + url, + json=body, + microversion=sot._max_microversion, + ) + + def test_failover_replication(self): + sot = group.Group(**GROUP) + + ret = sot.failover_replication( + self.sess, allowed_attached_volume=True, secondary_backend_id=None + ) + self.assertIsNone(ret) + + url = f'groups/{GROUP_ID}/action' + body = { + 'modify_body_for_action': { + 'allow_attached_volume': True, + 'secondary_backend_id': None, + } + } + self.sess.post.assert_called_with( + url, + json=body, + microversion=sot._max_microversion, + ) + + def test_fetch_replication_targets(self): + resp = mock.Mock() + resp.links = {} + resp.json = mock.Mock( + return_value={ + 'replication_targets': [ + { + 'backend_id': 'vendor-id-1', + 'unique_key': 'val1', + }, + ], + } + ) + resp.status_code = 200 + + self.sess.post = mock.Mock(return_value=resp) + + sot = group.Group(**GROUP) + result = sot.fetch_replication_targets(self.sess) + self.assertEqual( + [ + { + 'backend_id': 'vendor-id-1', + 'unique_key': 'val1', + }, + ], + sot.replication_targets, + ) + self.assertEqual(sot, result) + def test_reset_status(self): sot = group.Group(**GROUP) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index eb4cbfe8e..048c18df9 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -237,7 +237,7 @@ def test_group_delete(self): def test_group_update(self): self.verify_update(self.proxy.update_group, group.Group) - def test_reset_group_status(self): + def test_group_reset_status(self): self._verify( "openstack.block_storage.v3.group.Group.reset_status", self.proxy.reset_group_status, @@ -245,6 +245,34 @@ def test_reset_group_status(self): expected_args=[self.proxy, "new_status"], ) + def test_group_enable_replication(self): + self._verify( + "openstack.block_storage.v3.group.Group.enable_replication", + self.proxy.enable_group_replication, + method_args=["value"], + expected_args=[self.proxy], + ) + + def test_group_disable_replication(self): + self._verify( + "openstack.block_storage.v3.group.Group.disable_replication", + self.proxy.disable_group_replication, + method_args=["value"], + expected_args=[self.proxy], + ) + + def test_group_failover_replication(self): + self._verify( + "openstack.block_storage.v3.group.Group.failover_replication", + self.proxy.failover_group_replication, + method_args=["value"], + expected_args=[self.proxy], + expected_kwargs={ + 'allowed_attached_volume': False, + 'secondary_backend_id': None, + }, + ) + class TestGroupSnapshot(TestVolumeProxy): def test_group_snapshot_get(self): diff --git a/releasenotes/notes/add-volume-group-replication-actions-c64b2641625a5a2a.yaml b/releasenotes/notes/add-volume-group-replication-actions-c64b2641625a5a2a.yaml new file mode 100644 index 000000000..f271a5cdb --- /dev/null +++ b/releasenotes/notes/add-volume-group-replication-actions-c64b2641625a5a2a.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Added support for the volume group replication actions. From f823b616ec543c36d3e0e5ff2cf8d8eb7ecd390e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 31 Mar 2025 16:49:20 +0100 Subject: [PATCH 3712/3836] typing: Correct import Change-Id: I7933ec59643768b3f7394f18d3b753567ead0cbb Signed-off-by: Stephen Finucane --- openstack/cloud/openstackcloud.py | 2 +- openstack/config/__init__.py | 2 +- openstack/connection.py | 6 ++---- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index e7e3857ee..960699d2d 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -211,7 +211,7 @@ def __init__( **kwargs, ) - self._session = None + self._session: ks_session.Session | None = None self._proxies: dict[str, proxy.Proxy] = {} self.__pool_executor = pool_executor self._global_request_id = global_request_id diff --git a/openstack/config/__init__.py b/openstack/config/__init__.py index 3825aa27e..5b9d3d417 100644 --- a/openstack/config/__init__.py +++ b/openstack/config/__init__.py @@ -19,7 +19,7 @@ from openstack.config.loader import OpenStackConfig # noqa if ty.TYPE_CHECKING: - from openstack.config.cloud import cloud_region + from openstack.config import cloud_region # TODO(stephenfin): Expand kwargs once we've typed OpenstackConfig.get_one diff --git a/openstack/connection.py b/openstack/connection.py index a45c465f8..74bcdf777 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -714,12 +714,10 @@ def endpoint_for( :returns: The endpoint of the service, or None if not found. """ - # FIXME(stephenfin): Why is self.config showing as Any? - endpoint_override = self.config.get_endpoint(service_type) if endpoint_override: - return endpoint_override # type: ignore - return self.config.get_endpoint_from_catalog( # type: ignore + return endpoint_override + return self.config.get_endpoint_from_catalog( service_type=service_type, interface=interface, region_name=region_name, From cabd5eac640d0e12b32c2661d2c389bc3d58da60 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 26 May 2025 12:32:46 +0100 Subject: [PATCH 3713/3836] pre-commit: Bump versions Rename ruff to ruff-check per changes upstream. Change-Id: I7d24a3fa515547c64a16dc619e2ebf560003cc33 Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 42a37dd89..90712f8a3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,9 +19,9 @@ repos: hooks: - id: doc8 - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.8 + rev: v0.11.11 hooks: - - id: ruff + - id: ruff-check args: ['--fix', '--unsafe-fixes'] - id: ruff-format - repo: https://opendev.org/openstack/hacking From e29ff30d3c6967b11a6de778a6a137711cbe6840 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 28 Mar 2025 16:14:14 +0000 Subject: [PATCH 3714/3836] typing: Annotate openstack.common Change-Id: I784395cda5fc3c4954296da5f4e034c313c2d6b3 Signed-off-by: Stephen Finucane --- openstack/common/metadata.py | 33 ++++-- openstack/common/quota_set.py | 41 ++++---- openstack/common/tag.py | 33 +++--- openstack/identity/v3/limit.py | 2 +- openstack/identity/v3/registered_limit.py | 2 +- openstack/resource.py | 122 +++++++++++++++------- pyproject.toml | 5 + 7 files changed, 157 insertions(+), 81 deletions(-) diff --git a/openstack/common/metadata.py b/openstack/common/metadata.py index 69902ee1a..2ecabfef3 100644 --- a/openstack/common/metadata.py +++ b/openstack/common/metadata.py @@ -10,6 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + +import typing_extensions as ty_ext + from openstack import exceptions from openstack import resource from openstack import utils @@ -23,7 +27,7 @@ class MetadataMixin: #: *Type: list of tag strings* metadata = resource.Body('metadata', type=dict) - def fetch_metadata(self, session): + def fetch_metadata(self, session: resource.AdapterT) -> ty_ext.Self: """Lists metadata set on the entity. :param session: The session to use for making this request. @@ -38,7 +42,12 @@ def fetch_metadata(self, session): self._body.attributes.update({'metadata': json['metadata']}) return self - def set_metadata(self, session, metadata=None, replace=False): + def set_metadata( + self, + session: resource.AdapterT, + metadata: dict[str, ty.Any] | None = None, + replace: bool = False, + ) -> ty_ext.Self: """Sets/Replaces metadata key value pairs on the resource. :param session: The session to use for making this request. @@ -57,7 +66,11 @@ def set_metadata(self, session, metadata=None, replace=False): self._body.attributes.update({'metadata': metadata}) return self - def replace_metadata(self, session, metadata=None): + def replace_metadata( + self, + session: resource.AdapterT, + metadata: dict[str, ty.Any] | None = None, + ) -> ty_ext.Self: """Replaces all metadata key value pairs on the resource. :param session: The session to use for making this request. @@ -67,7 +80,7 @@ def replace_metadata(self, session, metadata=None): """ return self.set_metadata(session, metadata, replace=True) - def delete_metadata(self, session): + def delete_metadata(self, session: resource.AdapterT) -> ty_ext.Self: """Removes all metadata on the entity. :param session: The session to use for making this request. @@ -75,7 +88,9 @@ def delete_metadata(self, session): self.set_metadata(session, None, replace=True) return self - def get_metadata_item(self, session, key): + def get_metadata_item( + self, session: resource.AdapterT, key: str + ) -> ty_ext.Self: """Get the single metadata item on the entity. If the metadata key does not exist a 404 will be returned @@ -96,7 +111,9 @@ def get_metadata_item(self, session, key): return self - def set_metadata_item(self, session, key, value): + def set_metadata_item( + self, session: resource.AdapterT, key: str, value: ty.Any + ) -> ty_ext.Self: """Create or replace single metadata item to the resource. :param session: The session to use for making this request. @@ -112,7 +129,9 @@ def set_metadata_item(self, session, key, value): self._body.attributes.update({'metadata': metadata}) return self - def delete_metadata_item(self, session, key): + def delete_metadata_item( + self, session: resource.AdapterT, key: str + ) -> ty_ext.Self: """Removes a single metadata item from the specified resource. :param session: The session to use for making this request. diff --git a/openstack/common/quota_set.py b/openstack/common/quota_set.py index d07059e2c..ce8517010 100644 --- a/openstack/common/quota_set.py +++ b/openstack/common/quota_set.py @@ -12,6 +12,9 @@ import typing as ty +import requests +import typing_extensions as ty_ext + from openstack import exceptions from openstack import resource @@ -51,16 +54,16 @@ class QuotaSet(resource.Resource): def fetch( self, - session, - requires_id=False, - base_path=None, - error_message=None, - skip_cache=False, + session: resource.AdapterT, + requires_id: bool = False, + base_path: str | None = None, + error_message: str | None = None, + skip_cache: bool = False, *, - resource_response_key=None, - microversion=None, - **params, - ): + resource_response_key: str | None = None, + microversion: str | None = None, + **params: ty.Any, + ) -> ty_ext.Self: return super().fetch( session, requires_id, @@ -74,12 +77,12 @@ def fetch( def _translate_response( self, - response, - has_body=None, - error_message=None, + response: requests.Response, + has_body: bool | None = None, + error_message: str | None = None, *, - resource_response_key=None, - ): + resource_response_key: str | None = None, + ) -> None: """Given a KSA response, inflate this instance with its data DELETE operations don't return a body, so only try to work @@ -139,15 +142,15 @@ def _translate_response( self._header.attributes.update(headers) self._header.clean() self._update_location() - dict.update(self, self.to_dict()) + dict.update(self, self.to_dict()) # type: ignore def _prepare_request_body( self, - patch, - prepend_key, + patch: bool, + prepend_key: bool, *, - resource_request_key=None, - ): + resource_request_key: str | None = None, + ) -> dict[str, ty.Any] | list[ty.Any]: body = self._body.dirty # Ensure we never try to send meta props reservation and usage body.pop('reservation', None) diff --git a/openstack/common/tag.py b/openstack/common/tag.py index 24ec63227..a1f80a78e 100644 --- a/openstack/common/tag.py +++ b/openstack/common/tag.py @@ -10,20 +10,25 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + +import typing_extensions as ty_ext + from openstack import exceptions from openstack import resource from openstack import utils -class TagMixin: - id: str - base_path: str - _body: resource._ComponentManager +# https://github.com/python/mypy/issues/11583 +class _TagQueryParameters(ty.TypedDict): + tags: str + any_tags: str + not_tags: str + not_any_tags: str - @classmethod - def _get_session(cls, session): ... - _tag_query_parameters = { +class TagMixin(resource.ResourceMixinProtocol): + _tag_query_parameters: _TagQueryParameters = { 'tags': 'tags', 'any_tags': 'tags-any', 'not_tags': 'not-tags', @@ -34,7 +39,7 @@ def _get_session(cls, session): ... #: *Type: list of tag strings* tags = resource.Body('tags', type=list, default=[]) - def fetch_tags(self, session): + def fetch_tags(self, session: resource.AdapterT) -> ty_ext.Self: """Lists tags set on the entity. :param session: The session to use for making this request. @@ -52,7 +57,9 @@ def fetch_tags(self, session): self._body.attributes.update({'tags': json['tags']}) return self - def set_tags(self, session, tags): + def set_tags( + self, session: resource.AdapterT, tags: list[str] + ) -> ty_ext.Self: """Sets/Replaces all tags on the resource. :param session: The session to use for making this request. @@ -65,7 +72,7 @@ def set_tags(self, session, tags): self._body.attributes.update({'tags': tags}) return self - def remove_all_tags(self, session): + def remove_all_tags(self, session: resource.AdapterT) -> ty_ext.Self: """Removes all tags on the entity. :param session: The session to use for making this request. @@ -77,7 +84,7 @@ def remove_all_tags(self, session): self._body.attributes.update({'tags': []}) return self - def check_tag(self, session, tag): + def check_tag(self, session: resource.AdapterT, tag: str) -> ty_ext.Self: """Checks if tag exists on the entity. If the tag does not exist a 404 will be returned @@ -93,7 +100,7 @@ def check_tag(self, session, tag): ) return self - def add_tag(self, session, tag): + def add_tag(self, session: resource.AdapterT, tag: str) -> ty_ext.Self: """Adds a single tag to the resource. :param session: The session to use for making this request. @@ -109,7 +116,7 @@ def add_tag(self, session, tag): self._body.attributes.update({'tags': tags}) return self - def remove_tag(self, session, tag): + def remove_tag(self, session: resource.AdapterT, tag: str) -> ty_ext.Self: """Removes a single tag from the specified resource. :param session: The session to use for making this request. diff --git a/openstack/identity/v3/limit.py b/openstack/identity/v3/limit.py index 891aed2b0..0b83e1cac 100644 --- a/openstack/identity/v3/limit.py +++ b/openstack/identity/v3/limit.py @@ -150,4 +150,4 @@ def _translate_response( self._header.attributes.update(headers) self._header.clean() self._update_location() - dict.update(self, self.to_dict()) + dict.update(self, self.to_dict()) # type: ignore diff --git a/openstack/identity/v3/registered_limit.py b/openstack/identity/v3/registered_limit.py index 5a7de0cfe..4931a04f5 100644 --- a/openstack/identity/v3/registered_limit.py +++ b/openstack/identity/v3/registered_limit.py @@ -149,4 +149,4 @@ def _translate_response( self._header.attributes.update(headers) self._header.clean() self._update_location() - dict.update(self, self.to_dict()) + dict.update(self, self.to_dict()) # type: ignore diff --git a/openstack/resource.py b/openstack/resource.py index cb8281b89..91186d805 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -33,17 +33,19 @@ class that represent a remote resource. The attributes that """ import collections +import collections.abc import inspect import itertools import operator import typing as ty -import typing_extensions as ty_ext import urllib.parse import warnings import jsonpatch from keystoneauth1 import adapter from keystoneauth1 import discover +import requests +import typing_extensions as ty_ext from openstack import _log from openstack import exceptions @@ -202,11 +204,11 @@ def __len__(self): return len(self.attributes) @property - def dirty(self): + def dirty(self) -> dict[str, ty.Any]: """Return a dict of modified attributes""" return {key: self.attributes.get(key, None) for key in self._dirty} - def clean(self, only=None): + def clean(self, only: collections.abc.Iterable[str] | None = None) -> None: """Signal that the resource no longer has modified attributes. :param only: an optional set of attributes to no longer consider @@ -227,18 +229,26 @@ def __init__(self, url, body, headers): self.headers = headers +class QueryMapping(ty.TypedDict): + name: ty_ext.NotRequired[str] + type: ty_ext.NotRequired[ty.Callable[[ty.Any, type[ResourceT]], ResourceT]] + + class QueryParameters: def __init__( self, - *names, - include_pagination_defaults=True, - **mappings, + *names: str, + include_pagination_defaults: bool = True, + **mappings: str | QueryMapping, ): """Create a dict of accepted query parameters :param names: List of strings containing client-side query parameter names. Each name in the list maps directly to the name expected by the server. + :param include_pagination_defaults: If true, include default pagination + parameters, ``limit`` and ``marker``. These are the most common + query parameters used for listing resources in OpenStack APIs. :param mappings: Key-value pairs where the key is the client-side name we'll accept here and the value is the name the server expects, e.g, ``changes_since=changes-since``. @@ -247,11 +257,8 @@ def __init__( - ``name`` - server-side name, - ``type`` - callable to convert from client to server representation - :param include_pagination_defaults: If true, include default pagination - parameters, ``limit`` and ``marker``. These are the most common - query parameters used for listing resources in OpenStack APIs. """ - self._mapping: dict[str, str | dict] = {} + self._mapping: dict[str, str | QueryMapping] = {} if include_pagination_defaults: self._mapping.update({"limit": "limit", "marker": "marker"}) self._mapping.update({name: name for name in names}) @@ -333,12 +340,25 @@ def _transpose(self, query, resource_type): if provide_resource_type: result[name] = type_(value, resource_type) else: - result[name] = type_(value) + result[name] = type_(value) # type: ignore else: result[name] = value return result +class ResourceMixinProtocol(ty.Protocol): + id: str + base_path: str + + _body: _ComponentManager + _header: _ComponentManager + _uri: _ComponentManager + _computed: _ComponentManager + + @classmethod + def _get_session(cls, session: AdapterT) -> AdapterT: ... + + class Resource(dict): # TODO(mordred) While this behaves mostly like a munch for the purposes # we need, sub-resources, such as Server.security_groups, which is a list @@ -492,7 +512,7 @@ def __init__(self, _synchronized=False, connection=None, **attrs): # obj.items() ... but I think the if not obj: is short-circuiting down # in the C code and thus since we don't store the data in self[] it's # always False even if we override __len__ or __bool__. - dict.update(self, self.to_dict()) + dict.update(self, self.to_dict()) # type: ignore @classmethod def _attributes_iterator( @@ -683,7 +703,7 @@ def _update(self, **attrs: ty.Any) -> None: # obj.items() ... but I think the if not obj: is short-circuiting down # in the C code and thus since we don't store the data in self[] it's # always False even if we override __len__ or __bool__. - dict.update(self, self.to_dict()) + dict.update(self, self.to_dict()) # type: ignore def _collect_attrs(self, attrs): """Given attributes, return a dict per type of attribute @@ -718,7 +738,7 @@ def _collect_attrs(self, attrs): return body, header, uri, computed - def _update_location(self): + def _update_location(self) -> None: """Update location to include resource project/zone information. Location should describe the location of the resource. For some @@ -743,35 +763,55 @@ def _compute_attributes(self, body, header, uri): """Compute additional attributes from the remote resource.""" return {} - def _consume_body_attrs(self, attrs): + def _consume_body_attrs( + self, attrs: collections.abc.MutableMapping[str, ty.Any] + ) -> dict[str, ty.Any]: return self._consume_mapped_attrs(fields.Body, attrs) - def _consume_header_attrs(self, attrs): + def _consume_header_attrs( + self, attrs: collections.abc.MutableMapping[str, ty.Any] + ) -> dict[str, ty.Any]: return self._consume_mapped_attrs(fields.Header, attrs) - def _consume_uri_attrs(self, attrs): + def _consume_uri_attrs( + self, attrs: collections.abc.MutableMapping[str, ty.Any] + ) -> dict[str, ty.Any]: return self._consume_mapped_attrs(fields.URI, attrs) - def _update_from_body_attrs(self, attrs): + def _update_from_body_attrs( + self, attrs: collections.abc.MutableMapping[str, ty.Any] + ) -> None: body = self._consume_body_attrs(attrs) self._body.attributes.update(body) self._body.clean() - def _update_from_header_attrs(self, attrs): + def _update_from_header_attrs( + self, attrs: collections.abc.MutableMapping[str, ty.Any] + ) -> None: headers = self._consume_header_attrs(attrs) self._header.attributes.update(headers) self._header.clean() - def _update_uri_from_attrs(self, attrs): + def _update_uri_from_attrs( + self, attrs: collections.abc.MutableMapping[str, ty.Any] + ) -> None: uri = self._consume_uri_attrs(attrs) self._uri.attributes.update(uri) self._uri.clean() - def _consume_mapped_attrs(self, mapping_cls, attrs): + def _consume_mapped_attrs( + self, + mapping_cls: type[fields._BaseComponent], + attrs: collections.abc.MutableMapping[str, ty.Any], + ) -> dict[str, ty.Any]: mapping = self._get_mapping(mapping_cls) return self._consume_attrs(mapping, attrs) - def _consume_attrs(self, mapping, attrs): + def _consume_attrs( + self, + mapping: collections.abc.MutableMapping[str, ty.Any], + attrs: collections.abc.MutableMapping[str, ty.Any], + ) -> dict[str, ty.Any]: """Given a mapping and attributes, return relevant matches This method finds keys in attrs that exist in the mapping, then @@ -811,7 +851,9 @@ def _clean_body_attrs(self, attrs): self._original_body[attr] = self._body[attr] @classmethod - def _get_mapping(cls, component): + def _get_mapping( + cls, component: type[fields._BaseComponent] + ) -> ty.MutableMapping[str, ty.Any]: """Return a dict of attributes of a given component on the class""" mapping = component._map_cls() ret = component._map_cls() @@ -953,13 +995,13 @@ def _attr_to_dict(self, attr, to_munch): def to_dict( self, - body=True, - headers=True, - computed=True, - ignore_none=False, - original_names=False, - _to_munch=False, - ): + body: bool = True, + headers: bool = True, + computed: bool = True, + ignore_none: bool = False, + original_names: bool = False, + _to_munch: bool = False, + ) -> dict[str, ty.Any]: """Return a dictionary of this resource's contents :param bool body: Include the :class:`~openstack.fields.Body` @@ -1068,11 +1110,11 @@ def _pack_attrs_under_properties(self, body, attrs): def _prepare_request_body( self, - patch, - prepend_key, + patch: bool, + prepend_key: bool, *, - resource_request_key=None, - ): + resource_request_key: str | None = None, + ) -> dict[str, ty.Any] | list[ty.Any]: body: dict[str, ty.Any] | list[ty.Any] if patch: if not self._store_unknown_attrs_as_properties: @@ -1169,12 +1211,12 @@ def _prepare_request( def _translate_response( self, - response, - has_body=None, - error_message=None, + response: requests.Response, + has_body: bool | None = None, + error_message: str | None = None, *, - resource_response_key=None, - ): + resource_response_key: str | None = None, + ) -> None: """Given a KSA response, inflate this instance with its data DELETE operations don't return a body, so only try to work @@ -1226,7 +1268,7 @@ def _translate_response( self._header.attributes.update(headers) self._header.clean() self._update_location() - dict.update(self, self.to_dict()) + dict.update(self, self.to_dict()) # type: ignore @classmethod def _get_session(cls, session: AdapterT) -> AdapterT: diff --git a/pyproject.toml b/pyproject.toml index 7ba37e0b2..2709343bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,13 +30,18 @@ exclude = ''' [[tool.mypy.overrides]] module = [ + "openstack.common", + "openstack.common.*", # "openstack.config.cloud_region", "openstack.connection", "openstack.exceptions", "openstack.fields", "openstack.format", + "openstack._log", "openstack.proxy", "openstack.utils", + "openstack.version", + "openstack.warnings", ] warn_return_any = true disallow_untyped_decorators = true From efe5adc07c8c0224c95d61c4db2639b553cac71c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 26 May 2025 11:55:49 +0100 Subject: [PATCH 3715/3836] tests: Fix random failure Something in these tests is having a side effect on the (global) dictionary. Rework the tests so that it's only used once like everywhere else (and therefore the side-effects don't matter) Change-Id: I5566d5c570c222a7c2d670b8249a47e3e55852a2 Signed-off-by: Stephen Finucane --- .../tests/unit/block_storage/v2/test_transfer.py | 10 ++++++---- .../tests/unit/block_storage/v3/test_transfer.py | 12 ++++++++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/openstack/tests/unit/block_storage/v2/test_transfer.py b/openstack/tests/unit/block_storage/v2/test_transfer.py index 458e481c3..01f603b7d 100644 --- a/openstack/tests/unit/block_storage/v2/test_transfer.py +++ b/openstack/tests/unit/block_storage/v2/test_transfer.py @@ -15,6 +15,7 @@ from keystoneauth1 import adapter from openstack.block_storage.v2 import transfer +from openstack import resource from openstack.tests.unit import base @@ -37,7 +38,7 @@ class TestTransfer(base.TestCase): def setUp(self): super().setUp() self.resp = mock.Mock() - self.resp.body = {'transfer': TRANSFER} + self.resp.body = None # nothing uses this self.resp.json = mock.Mock(return_value=self.resp.body) self.resp.headers = {} self.resp.status_code = 202 @@ -47,7 +48,7 @@ def setUp(self): self.sess.default_microversion = "3.55" def test_basic(self): - sot = transfer.Transfer(TRANSFER) + sot = transfer.Transfer() self.assertEqual("transfer", sot.resource_key) self.assertEqual("transfers", sot.resources_key) self.assertEqual("/os-volume-transfer", sot.base_path) @@ -61,7 +62,7 @@ def test_basic(self): sot._query_mapping._mapping, ) - def test_create(self): + def test_make_it(self): sot = transfer.Transfer(**TRANSFER) self.assertEqual(TRANSFER["auth_key"], sot.auth_key) self.assertEqual(TRANSFER["created_at"], sot.created_at) @@ -69,7 +70,8 @@ def test_create(self): self.assertEqual(TRANSFER["name"], sot.name) self.assertEqual(TRANSFER["volume_id"], sot.volume_id) - def test_accept(self): + @mock.patch.object(resource.Resource, '_translate_response') + def test_accept(self, mock_translate): sot = transfer.Transfer() sot.id = FAKE_TRANSFER diff --git a/openstack/tests/unit/block_storage/v3/test_transfer.py b/openstack/tests/unit/block_storage/v3/test_transfer.py index d048fd29f..185146fc7 100644 --- a/openstack/tests/unit/block_storage/v3/test_transfer.py +++ b/openstack/tests/unit/block_storage/v3/test_transfer.py @@ -38,7 +38,7 @@ class TestTransfer(base.TestCase): def setUp(self): super().setUp() self.resp = mock.Mock() - self.resp.body = None + self.resp.body = None # nothing uses this self.resp.json = mock.Mock(return_value=self.resp.body) self.resp.headers = {} self.resp.status_code = 202 @@ -48,7 +48,7 @@ def setUp(self): self.sess.default_microversion = "3.55" def test_basic(self): - tr = transfer.Transfer(TRANSFER) + tr = transfer.Transfer() self.assertEqual("transfer", tr.resource_key) self.assertEqual("transfers", tr.resources_key) self.assertEqual("/volume-transfers", tr.base_path) @@ -63,6 +63,14 @@ def test_basic(self): tr._query_mapping._mapping, ) + def test_make_it(self): + sot = transfer.Transfer(**TRANSFER) + self.assertEqual(TRANSFER["auth_key"], sot.auth_key) + self.assertEqual(TRANSFER["created_at"], sot.created_at) + self.assertEqual(TRANSFER["id"], sot.id) + self.assertEqual(TRANSFER["name"], sot.name) + self.assertEqual(TRANSFER["volume_id"], sot.volume_id) + @mock.patch( 'openstack.utils.supports_microversion', autospec=True, From b82a434d3ad49848bd50622f5df375aa548a7739 Mon Sep 17 00:00:00 2001 From: Afonne-CID Date: Wed, 30 Apr 2025 15:47:57 +0100 Subject: [PATCH 3716/3836] Follow-up: Add support for runbooks Related-Change: https://review.opendev.org/c/openstack/openstacksdk/+/926414 Change-Id: Iac38e4436b08f6a0acfb9e3ad2691f9fc8b3c108 --- doc/source/user/proxies/baremetal.rst | 8 + doc/source/user/resources/baremetal/index.rst | 1 + .../user/resources/baremetal/v1/runbooks.rst | 13 ++ openstack/baremetal/v1/_proxy.py | 102 +++++++++ openstack/tests/functional/baremetal/base.py | 12 ++ .../baremetal/test_baremetal_runbooks.py | 194 ++++++++++++++++++ 6 files changed, 330 insertions(+) create mode 100644 doc/source/user/resources/baremetal/v1/runbooks.rst create mode 100644 openstack/tests/functional/baremetal/test_baremetal_runbooks.py diff --git a/doc/source/user/proxies/baremetal.rst b/doc/source/user/proxies/baremetal.rst index f30beae15..4d054dc05 100644 --- a/doc/source/user/proxies/baremetal.rst +++ b/doc/source/user/proxies/baremetal.rst @@ -102,6 +102,14 @@ Deploy Template Operations create_deploy_template, update_deploy_template, patch_deploy_template, delete_deploy_template +Runbook Operations +^^^^^^^^^^^^^^^^^^^ +.. autoclass:: openstack.baremetal.v1._proxy.Proxy + :noindex: + :members: runbooks, get_runbook, + create_runbook, update_runbook, + patch_runbook, delete_runbook + Utilities --------- diff --git a/doc/source/user/resources/baremetal/index.rst b/doc/source/user/resources/baremetal/index.rst index 1fabe14b2..fa198a0ab 100644 --- a/doc/source/user/resources/baremetal/index.rst +++ b/doc/source/user/resources/baremetal/index.rst @@ -14,3 +14,4 @@ Baremetal Resources v1/volume_target v1/deploy_templates v1/conductor + v1/runbooks diff --git a/doc/source/user/resources/baremetal/v1/runbooks.rst b/doc/source/user/resources/baremetal/v1/runbooks.rst new file mode 100644 index 000000000..f46217141 --- /dev/null +++ b/doc/source/user/resources/baremetal/v1/runbooks.rst @@ -0,0 +1,13 @@ +openstack.baremetal.v1.runbooks +=============================== + +.. automodule:: openstack.baremetal.v1.runbooks + +The Runbook Class +----------------- + +The ``Runbook`` class inherits +from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.baremetal.v1.runbooks.Runbook + :members: diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 4d9b4d6c0..ecccf17cc 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -23,6 +23,7 @@ from openstack.baremetal.v1 import node as _node from openstack.baremetal.v1 import port as _port from openstack.baremetal.v1 import port_group as _portgroup +from openstack.baremetal.v1 import runbooks as _runbooks from openstack.baremetal.v1 import volume_connector as _volumeconnector from openstack.baremetal.v1 import volume_target as _volumetarget from openstack import exceptions @@ -43,6 +44,7 @@ class Proxy(proxy.Proxy): "node": _node.Node, "port": _port.Port, "port_group": _portgroup.PortGroup, + "runbook": _runbooks.Runbook, "volume_connector": _volumeconnector.VolumeConnector, "volume_target": _volumetarget.VolumeTarget, } @@ -1828,6 +1830,106 @@ def patch_deploy_template(self, deploy_template, patch): _deploytemplates.DeployTemplate, deploy_template ).patch(self, patch) + # ========== Runbooks ========== + + def runbooks(self, details=False, **query): + """Retrieve a generator of runbooks. + + :param details: A boolean indicating whether the detailed information + for every runbook should be returned. + :param dict query: Optional query parameters to be sent to + restrict the runbooks to be returned. + + :returns: A generator of Runbooks instances. + """ + if details: + query['detail'] = True + return _runbooks.Runbook.list(self, **query) + + def create_runbook(self, **attrs): + """Create a new runbook from attributes. + + :param dict attrs: Keyword arguments that will be used to create a + :class:`~openstack.baremetal.v1.runbooks.Runbook`. + + :returns: The results of runbook creation. + :rtype: :class:`~openstack.baremetal.v1.runbooks.Runbook`. + """ + return self._create(_runbooks.Runbook, **attrs) + + def update_runbook(self, runbook, **attrs): + """Update a runbook. + + :param runbook: Either the ID of a runbook, + or an instance of + :class:`~openstack.baremetal.v1.runbooks.Runbook`. + :param dict attrs: The attributes to update on + the runbook represented + by the ``runbook`` parameter. + + :returns: The updated runbook. + :rtype: :class:`~openstack.baremetal.v1.runbooks.Runbook` + """ + return self._update(_runbooks.Runbook, runbook, **attrs) + + def delete_runbook(self, runbook, ignore_missing=True): + """Delete a runbook. + + :param runbook:The value can be + either the ID of a runbook or a + :class:`~openstack.baremetal.v1.runbooks.Runbook` + instance. + + :param bool ignore_missing: When set to ``False``, + an exception:class:`~openstack.exceptions.NotFoundException` + will be raised when the runbook could not be found. + When set to ``True``, no exception will be raised when attempting + to delete a non-existent runbook. + + :returns: The instance of the runbook which was deleted. + :rtype: :class:`~openstack.baremetal.v1.runbooks.Runbook`. + """ + + return self._delete( + _runbooks.Runbook, + runbook, + ignore_missing=ignore_missing, + ) + + def get_runbook(self, runbook, fields=None): + """Get a specific runbook. + + :param runbook: The value can be the name or ID + of a runbook + :class:`~openstack.baremetal.v1.runbooks.Runbook` + instance. + + :param fields: Limit the resource fields to fetch. + + :returns: One + :class:`~openstack.baremetal.v1.runbooks.Runbook` + :raises: :class:`~openstack.exceptions.NotFoundException` + when no runbook matching the name or ID could be found. + """ + return self._get_with_fields(_runbooks.Runbook, runbook, fields=fields) + + def patch_runbook(self, runbook, patch): + """Apply a JSON patch to the runbook. + + :param runbooks: The value can be the ID of a + runbook or a :class:`~openstack.baremetal.v1.runbooks.Runbook` + instance. + + :param patch: JSON patch to apply. + + :returns: The updated runbook. + :rtype: + :class:`~openstack.baremetal.v1.runbooks.Runbook` + """ + return self._get_resource(_runbooks.Runbook, runbook).patch( + self, patch + ) + # ========== Conductors ========== def conductors(self, details=False, **query): diff --git a/openstack/tests/functional/baremetal/base.py b/openstack/tests/functional/baremetal/base.py index 55260574d..11a0cbadd 100644 --- a/openstack/tests/functional/baremetal/base.py +++ b/openstack/tests/functional/baremetal/base.py @@ -120,3 +120,15 @@ def create_deploy_template(self, **kwargs): ) ) return deploy_template + + def create_runbook(self, **kwargs): + """Create a new runbook from attributes.""" + + runbook = self.conn.baremetal.create_runbook(**kwargs) + + self.addCleanup( + lambda: self.conn.baremetal.delete_runbook( + runbook.id, ignore_missing=True + ) + ) + return runbook diff --git a/openstack/tests/functional/baremetal/test_baremetal_runbooks.py b/openstack/tests/functional/baremetal/test_baremetal_runbooks.py new file mode 100644 index 000000000..6fb9737f1 --- /dev/null +++ b/openstack/tests/functional/baremetal/test_baremetal_runbooks.py @@ -0,0 +1,194 @@ +# 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.tests.functional.baremetal import base + + +class TestBareMetalRunbook(base.BaseBaremetalTest): + min_microversion = '1.92' + + def setUp(self): + super().setUp() + + def test_baremetal_runbook_create_get_delete(self): + steps = [ + { + "interface": "bios", + "step": "apply_configuration", + "args": { + "settings": [{"name": "LogicalProc", "value": "Enabled"}] + }, + "priority": 150, + } + ] + runbook = self.create_runbook(name='CUSTOM_RUNBOOK', steps=steps) + loaded = self.conn.baremetal.get_runbook(runbook.id) + self.assertEqual(loaded.id, runbook.id) + self.conn.baremetal.delete_runbook(runbook, ignore_missing=False) + self.assertRaises( + exceptions.NotFoundException, + self.conn.baremetal.get_runbook, + runbook.id, + ) + + def test_baremetal_runbook_list(self): + steps = [ + { + "interface": "bios", + "step": "apply_configuration", + "args": { + "settings": [{"name": "LogicalProc", "value": "Enabled"}] + }, + "priority": 150, + } + ] + + runbook1 = self.create_runbook(name='CUSTOM_RUNBOOK1', steps=steps) + runbook2 = self.create_runbook(name='CUSTOM_RUNBOOK2', steps=steps) + runbooks = self.conn.baremetal.runbooks() + ids = [runbook.id for runbook in runbooks] + self.assertIn(runbook1.id, ids) + self.assertIn(runbook2.id, ids) + + runbooks_with_details = self.conn.baremetal.runbooks(details=True) + for runbook in runbooks_with_details: + self.assertIsNotNone(runbook.id) + self.assertIsNotNone(runbook.name) + + runbook_with_fields = self.conn.baremetal.runbooks(fields=['uuid']) + for runbook in runbook_with_fields: + self.assertIsNotNone(runbook.id) + self.assertIsNone(runbook.name) + + def test_baremetal_runbook_list_update_delete(self): + steps = [ + { + "interface": "bios", + "step": "apply_configuration", + "args": { + "settings": [{"name": "LogicalProc", "value": "Enabled"}] + }, + "priority": 150, + } + ] + runbook = self.create_runbook(name='CUSTOM_RUNBOOK4', steps=steps) + self.assertFalse(runbook.extra) + runbook.extra = {'answer': 42} + + runbook = self.conn.baremetal.update_runbook(runbook) + self.assertEqual({'answer': 42}, runbook.extra) + + runbook = self.conn.baremetal.get_runbook(runbook.id) + + self.conn.baremetal.delete_runbook(runbook.id, ignore_missing=False) + + def test_baremetal_runbook_update(self): + steps = [ + { + "interface": "bios", + "step": "apply_configuration", + "args": { + "settings": [{"name": "LogicalProc", "value": "Enabled"}] + }, + "priority": 150, + } + ] + runbook = self.create_runbook(name='CUSTOM_RUNBOOK4', steps=steps) + runbook.extra = {'answer': 42} + + runbook = self.conn.baremetal.update_runbook(runbook) + self.assertEqual({'answer': 42}, runbook.extra) + + runbook = self.conn.baremetal.get_runbook(runbook.id) + self.assertEqual({'answer': 42}, runbook.extra) + + def test_runbook_patch(self): + name = "CUSTOM_HYPERTHREADING_ON" + steps = [ + { + "interface": "bios", + "step": "apply_configuration", + "args": { + "settings": [{"name": "LogicalProc", "value": "Enabled"}] + }, + "priority": 150, + } + ] + runbook = self.create_runbook(name=name, steps=steps) + runbook = self.conn.baremetal.patch_runbook( + runbook, dict(path='/extra/answer', op='add', value=42) + ) + self.assertEqual({'answer': 42}, runbook.extra) + self.assertEqual(name, runbook.name) + + runbook = self.conn.baremetal.get_runbook(runbook.id) + self.assertEqual({'answer': 42}, runbook.extra) + + def test_runbook_negative_non_existing(self): + uuid = "b4145fbb-d4bc-0d1d-4382-e1e922f9035c" + self.assertRaises( + exceptions.NotFoundException, + self.conn.baremetal.get_runbook, + uuid, + ) + self.assertRaises( + exceptions.NotFoundException, + self.conn.baremetal.delete_runbook, + uuid, + ignore_missing=False, + ) + self.assertIsNone(self.conn.baremetal.delete_runbook(uuid)) + + def test_runbook_rbac_project_scoped(self): + steps = [ + { + "interface": "bios", + "step": "apply_configuration", + "args": { + "settings": [{"name": "LogicalProc", "value": "Enabled"}] + }, + "priority": 150, + } + ] + + runbook = self.create_runbook( + name='CUSTOM_PROJ_AWESOME', + steps=steps, + owner=self.conn.current_project_id, + ) + self.assertFalse(runbook.public) + self.assertEqual(self.conn.current_project_id, runbook.owner) + + # is accessible to the owner + loaded = self.conn.baremetal.get_runbook(runbook.id) + self.assertEqual(loaded.id, runbook.id) + + def test_runbook_rbac_system_scoped(self): + steps = [ + { + "interface": "bios", + "step": "apply_configuration", + "args": { + "settings": [{"name": "LogicalProc", "value": "Enabled"}] + }, + "priority": 150, + } + ] + + runbook = self.create_runbook(name='CUSTOM_SYS_AWESOME', steps=steps) + self.assertFalse(runbook.public) + self.assertIsNone(runbook.owner) + + # is accessible to system-scoped users + loaded = self.conn.baremetal.get_runbook(runbook.id) + self.assertEqual(loaded.id, runbook.id) From de9129e766a5834d82428f1f71382986fb4321f2 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 29 May 2025 15:45:27 +0100 Subject: [PATCH 3717/3836] Update .git-blame-ignore-revs Add some recent changes that should be ignored. Change-Id: Ie11140bcc5cb197ed0e22daae444b07972810822 Signed-off-by: Stephen Finucane --- .git-blame-ignore-revs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index ef89dbb03..a4bdd6106 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,6 +1,10 @@ # You can configure git to automatically use this file with the following config: # git config --global blame.ignoreRevsFile .git-blame-ignore-revs +e0f57ad81fb6a9f5100cfdefdd30661326edb031 # Bump Python version used for linters to 3.10 +4beac2a236993bd7890279f3f22a2eda3cd89bad # pre-commit: Prepare for bump +399dfcc0e59124fde68376b77089b20485f6566f # pre-commit: Migrate pyupgrade to ruff-format +187e88a5593e4f475379738811043ee4528a7df3 # pre-commit: Migrate from black to ruff format c7010a2f929de9fad4e1a7c7f5a17cb8e210432a # Bump black to 23.3.0 a36f514295a4b4e6157ce69a210f653bcc4df7f2 # Blackify everything else 004c7352d0a4fb467a319ae9743eb6ca5ee9ce7f # Blackify openstack.cloud From 4aeedc36e192f5e7bbfd7f78dd7cad6060586b44 Mon Sep 17 00:00:00 2001 From: elajkat Date: Mon, 19 May 2025 14:23:15 +0200 Subject: [PATCH 3718/3836] Add shared field to security-groups During the testing of [1] I realized that is_shared is missing from security_group.SecurityGroup openstackclient patch to use --share and --no-share: https://review.opendev.org/c/openstack/python-openstackclient/+/950494 [1]: https://review.opendev.org/c/openstack/horizon/+/946269 Related-Bug: #1999774 Change-Id: Ieb1a919a715350ac6a0ba3dc9246d7a6afe31814 Depends-On: https://review.opendev.org/c/openstack/python-openstackclient/+/950863 --- openstack/network/v2/security_group.py | 4 ++++ openstack/tests/unit/network/v2/test_security_group.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/openstack/network/v2/security_group.py b/openstack/network/v2/security_group.py index 425878793..268b9f3dc 100644 --- a/openstack/network/v2/security_group.py +++ b/openstack/network/v2/security_group.py @@ -37,6 +37,7 @@ class SecurityGroup(_base.NetworkResource, _base.TagMixinNetwork): 'revision_number', 'sort_dir', 'sort_key', + is_shared='shared', **_base.TagMixinNetwork._tag_query_parameters, ) @@ -59,3 +60,6 @@ class SecurityGroup(_base.NetworkResource, _base.TagMixinNetwork): tenant_id = resource.Body('tenant_id', deprecated=True) #: Timestamp when the security group was last updated. updated_at = resource.Body('updated_at') + #: Indicates whether this Security Group is shared across all projects. + #: *Type: bool* + is_shared = resource.Body('shared', type=bool) diff --git a/openstack/tests/unit/network/v2/test_security_group.py b/openstack/tests/unit/network/v2/test_security_group.py index 1597dc9e5..b9978a7d1 100644 --- a/openstack/tests/unit/network/v2/test_security_group.py +++ b/openstack/tests/unit/network/v2/test_security_group.py @@ -59,6 +59,7 @@ 'project_id': '4', 'updated_at': '2016-10-14T12:16:57.233772', 'tags': ['5'], + 'is_shared': True, } @@ -80,6 +81,7 @@ def test_basic(self): 'description': 'description', 'fields': 'fields', 'id': 'id', + 'is_shared': 'shared', 'limit': 'limit', 'marker': 'marker', 'name': 'name', @@ -111,3 +113,4 @@ def test_make_it(self): self.assertEqual(EXAMPLE['project_id'], sot.project_id) self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) self.assertEqual(EXAMPLE['tags'], sot.tags) + self.assertEqual(EXAMPLE['is_shared'], sot.is_shared) From 9afc09407685f5934125dc67ee24be36f2b958da Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 26 May 2025 16:54:47 +0100 Subject: [PATCH 3719/3836] typing: Annotate openstack.config.cloud_region We also annotate the '_util' and 'defaults' module from the same packages since these are used internally. Change-Id: Ia96c80e4567e0555f7c413dbbb733da943a86cc3 Signed-off-by: Stephen Finucane Depends-on: https://review.opendev.org/c/openstack/os-client-config/+/951401 --- .pre-commit-config.yaml | 2 +- openstack/config/_util.py | 20 +- openstack/config/cloud_region.py | 400 +++++++++++------- openstack/config/defaults.py | 17 +- openstack/tests/unit/cloud/test_operator.py | 6 +- .../tests/unit/config/test_cloud_config.py | 15 +- pyproject.toml | 6 +- 7 files changed, 287 insertions(+), 179 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 90712f8a3..d358fedda 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,7 +36,7 @@ repos: hooks: - id: mypy additional_dependencies: - - keystoneauth1>=5.10.0 + - keystoneauth1>=5.11.0 - types-decorator - types-PyYAML - types-requests diff --git a/openstack/config/_util.py b/openstack/config/_util.py index d886179c9..3f6e8a9a5 100644 --- a/openstack/config/_util.py +++ b/openstack/config/_util.py @@ -12,9 +12,11 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty -def normalize_keys(config): - new_config = {} + +def normalize_keys(config: dict[str, ty.Any]) -> dict[str, ty.Any]: + new_config: dict[str, ty.Any] = {} for key, value in config.items(): key = key.replace('-', '_') if isinstance(value, dict): @@ -33,7 +35,9 @@ def normalize_keys(config): return new_config -def merge_clouds(old_dict, new_dict): +def merge_clouds( + old_dict: dict[str, ty.Any], new_dict: dict[str, ty.Any] +) -> dict[str, ty.Any]: """Like dict.update, except handling nested dicts.""" ret = old_dict.copy() for k, v in new_dict.items(): @@ -50,11 +54,11 @@ def merge_clouds(old_dict, new_dict): class VersionRequest: def __init__( self, - version=None, - min_api_version=None, - max_api_version=None, - default_microversion=None, - ): + version: str | None = None, + min_api_version: str | None = None, + max_api_version: str | None = None, + default_microversion: str | None = None, + ) -> None: self.version = version self.min_api_version = min_api_version self.max_api_version = max_api_version diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 4af0745ae..75cba2c27 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. +import collections.abc import copy import os.path import typing as ty @@ -23,6 +24,7 @@ except ImportError: keyring = None +from keystoneauth1.access import service_catalog as ks_service_catalog from keystoneauth1 import discover import keystoneauth1.exceptions.catalog from keystoneauth1.identity import base as ks_identity_base @@ -59,6 +61,8 @@ from openstack.config import loader +_T = ty.TypeVar('_T') + _logger = _log.setup_logging('openstack') SCOPE_KEYS = { @@ -77,7 +81,7 @@ class _PasswordCallback(ty.Protocol): def __call__(self, prompt: str | None = None) -> str: ... -def _make_key(key, service_type): +def _make_key(key: str, service_type: str | None) -> str: if not service_type: return key else: @@ -85,7 +89,11 @@ def _make_key(key, service_type): return "_".join([service_type, key]) -def _disable_service(config, service_type, reason=None): +def _disable_service( + config: dict[str, ty.Any], + service_type: str, + reason: str | None = None, +) -> None: service_type = service_type.lower().replace('-', '_') key = f'has_{service_type}' config[key] = False @@ -94,10 +102,8 @@ def _disable_service(config, service_type, reason=None): config[d_key] = reason -def _get_implied_microversion(version): - if not version: - return - if '.' in version: +def _get_implied_microversion(version: str | None) -> str | None: + if version and '.' in version: # Some services historically had a .0 in their normal api version. # Neutron springs to mind with version "2.0". If a user has "2.0" # set in a variable or config file just because history, we don't @@ -105,6 +111,8 @@ def _get_implied_microversion(version): if version.split('.')[1] != "0": return version + return None + def from_session( session: ks_session.Session, @@ -320,7 +328,7 @@ def __init__( cache_auth: bool = False, ) -> None: self._name = name - self.config = _util.normalize_keys(config) + self.config = _util.normalize_keys(config or {}) # NOTE(efried): For backward compatibility: a) continue to accept the # region_name kwarg; b) make it take precedence over (non-service_type- # specific) region_name set in the config dict. @@ -354,7 +362,7 @@ def __init__( self._service_type_manager = os_service_types.ServiceTypes() - def __getattr__(self, key): + def __getattr__(self, key: str) -> ty.Any: """Return arbitrary attributes.""" if key.startswith('os_'): @@ -365,17 +373,20 @@ def __getattr__(self, key): else: return None - def __iter__(self): + def __iter__(self) -> collections.abc.Iterator[ty.Any]: return self.config.__iter__() - def __eq__(self, other): + def __eq__(self, other: object) -> bool: + if not isinstance(other, CloudRegion): + return NotImplemented + return self.name == other.name and self.config == other.config - def __ne__(self, other): + def __ne__(self, other: object) -> bool: return not self == other @property - def name(self): + def name(self) -> str: if self._name is not None: return self._name @@ -386,14 +397,16 @@ def name(self): and isinstance(auth, ks_identity_base.BaseIdentityPlugin) and auth.auth_url ): - self._name = parse.urlparse(auth.auth_url).hostname + name = parse.urlparse(auth.auth_url).hostname or '' else: - self._name = self._app_name or '' + name = self._app_name or '' + + self._name = name - return self._name + return name @property - def full_name(self): + def full_name(self) -> str: """Return a string that can be used as an identifier. Always returns a valid string. It will have name and region_name @@ -409,15 +422,19 @@ def full_name(self): else: return 'unknown' - def set_service_value(self, key, service_type, value): + def set_service_value( + self, key: str, service_type: str, value: ty.Any + ) -> None: key = _make_key(key, service_type) self.config[key] = value - def set_session_constructor(self, session_constructor): + def set_session_constructor( + self, session_constructor: type[ks_session.Session] + ) -> None: """Sets the Session constructor.""" self._session_constructor = session_constructor - def get_requests_verify_args(self): + def get_requests_verify_args(self) -> tuple[bool, str | None]: """Return the verify and cert values for the requests library.""" insecure = self.config.get('insecure', False) verify = self.config.get('verify', True) @@ -440,10 +457,10 @@ def get_requests_verify_args(self): if cert: cert = os.path.expanduser(cert) if self.config.get('key'): - cert = (cert, os.path.expanduser(self.config.get('key'))) + cert = (cert, os.path.expanduser(self.config['key'])) return (verify, cert) - def get_services(self): + def get_services(self) -> list[str]: """Return a list of service types we know something about.""" services = [] for key, val in self.config.items(): @@ -455,7 +472,7 @@ def get_services(self): services.append("_".join(key.split('_')[:-2])) return list(set(services)) - def get_enabled_services(self): + def get_enabled_services(self) -> set[str]: services = set() all_services = [ @@ -472,18 +489,35 @@ def get_enabled_services(self): return services - def get_auth_args(self): - return self.config.get('auth', {}) + def get_auth_args(self) -> dict[str, ty.Any]: + return ty.cast(dict[str, ty.Any], self.config.get('auth', {})) + + @ty.overload + def _get_config( + self, + key: str, + service_type: str | None, + default: _T, + fallback_to_unprefixed: bool = False, + ) -> _T: ... + + @ty.overload + def _get_config( + self, + key: str, + service_type: str | None, + default: None = None, + fallback_to_unprefixed: bool = False, + ) -> ty.Any | None: ... def _get_config( self, - key, - service_type, - default=None, - fallback_to_unprefixed=False, - converter=None, - ): - '''Get a config value for a service_type. + key: str, + service_type: str | None, + default: _T | None = None, + fallback_to_unprefixed: bool = False, + ) -> _T | ty.Any | None: + """Get a config value for a service_type. Finds the config value for a key, looking first for it prefixed by the given service_type, then by any known aliases of that service_type. @@ -491,74 +525,101 @@ def _get_config( for without a prefix to support the config values where a global default makes sense. - For instance, ``_get_config('example', 'block-storage', True)`` would + For instance, ``_get_config('example', 'block-storage')`` would first look for ``block_storage_example``, then ``volumev3_example``, ``volumev2_example`` and ``volume_example``. If no value was found, it would look for ``example``. If none of that works, it returns the value in ``default``. - ''' + """ if service_type is None: - return self.config.get(key) - - for st in self._service_type_manager.get_all_types(service_type): - value = self.config.get(_make_key(key, st)) - if value is not None: - break + value = self.config.get(key) else: - if fallback_to_unprefixed: - value = self.config.get(key) + for st in self._service_type_manager.get_all_types(service_type): + _key = _make_key(key, st) + value = self.config.get(_key) + if value is not None: + key = _key + break + else: + if fallback_to_unprefixed: + value = self.config.get(key) if value is None: return default else: - if converter is not None: - value = converter(value) return value - def _get_service_config(self, key, service_type): + def _get_service_config( + self, key: str, service_type: str + ) -> ty.Any | None: config_dict = self.config.get(key) if not config_dict: return None + if not isinstance(config_dict, dict): - return config_dict + raise RuntimeError( + f'invalid configuration for service type {service_type!r}' + ) for st in self._service_type_manager.get_all_types(service_type): if st in config_dict: return config_dict[st] - def get_region_name(self, service_type=None): + return None + + def get_region_name(self, service_type: str | None = None) -> str | None: # If a region_name for the specific service_type is configured, use it; # else use the one configured for the CloudRegion as a whole. - return self._get_config( + value = self._get_config( 'region_name', service_type, fallback_to_unprefixed=True ) + return str(value) if value is not None else value - def get_interface(self, service_type=None): - return self._get_config( + def get_interface( + self, service_type: str | None = None + ) -> list[str] | str | None: + value = self._get_config( 'interface', service_type, fallback_to_unprefixed=True ) + if value is None: + return value + if isinstance(value, str): + return value + if isinstance(value, list) and all( + {isinstance(x, str) or x is None for x in value} + ): + return value + + raise exceptions.ConfigException( + f'interface should be str, list of str or None but is ' + f'{type(value)}' + ) - def get_api_version(self, service_type): + def get_api_version(self, service_type: str) -> str | None: version = self._get_config('api_version', service_type) - if version: - try: - float(version) - except ValueError: - if 'latest' in version: - warnings.warn( - "You have a configured API_VERSION with 'latest' in " - "it. In the context of openstacksdk this doesn't make " - "any sense.", - os_warnings.ConfigurationWarning, - ) - return None - return version + if not version: + return None + + try: + float(version) + except ValueError: + if 'latest' in version: + warnings.warn( + "You have a configured API_VERSION with 'latest' in " + "it. In the context of openstacksdk this doesn't make " + "any sense.", + os_warnings.ConfigurationWarning, + ) + return None - def get_default_microversion(self, service_type): - return self._get_config('default_microversion', service_type) + return str(version) - def get_service_type(self, service_type): + def get_default_microversion(self, service_type: str) -> str | None: + value = self._get_config('default_microversion', service_type) + return str(value) if value is not None else value + + def get_service_type(self, service_type: str) -> str: # People requesting 'volume' are doing so because os-client-config # let them. What they want is block-storage, not explicitly the # v1 of cinder. If someone actually wants v1, they'll have api_version @@ -570,12 +631,14 @@ def get_service_type(self, service_type): service_type = self._service_type_manager.get_service_type( service_type ) - return self._get_config( + value = self._get_config( 'service_type', service_type, default=service_type ) + return str(value) if value is not None else value - def get_service_name(self, service_type): - return self._get_config('service_name', service_type) + def get_service_name(self, service_type: str) -> str | None: + value = self._get_config('service_name', service_type) + return str(value) if value is not None else value def get_endpoint(self, service_type: str) -> str | None: auth = self.config.get('auth', {}) @@ -618,7 +681,7 @@ def get_endpoint(self, service_type: str) -> str | None: def get_endpoint_from_catalog( self, service_type: str, - interface: str | None = None, + interface: list[str] | str | None = None, region_name: str | None = None, ) -> str | None: """Return the endpoint for a given service as found in the catalog. @@ -646,7 +709,6 @@ def get_endpoint_from_catalog( catalog = auth.get_access(session).service_catalog try: - # FIXME(stephenfin): Remove once keystoneauth1 has type hints return catalog.url_for( service_type=service_type, interface=interface, @@ -655,38 +717,34 @@ def get_endpoint_from_catalog( except (keystoneauth1.exceptions.catalog.EndpointNotFound, ValueError): return None - def get_connect_retries(self, service_type): - return self._get_config( - 'connect_retries', - service_type, - fallback_to_unprefixed=True, - converter=int, + def get_connect_retries(self, service_type: str) -> int | None: + value = self._get_config( + 'connect_retries', service_type, fallback_to_unprefixed=True ) + return int(value) if value is not None else value - def get_status_code_retries(self, service_type): - return self._get_config( - 'status_code_retries', - service_type, - fallback_to_unprefixed=True, - converter=int, + def get_status_code_retries(self, service_type: str) -> int | None: + value = self._get_config( + 'status_code_retries', service_type, fallback_to_unprefixed=True ) + return int(value) if value is not None else value @property - def prefer_ipv6(self): + def prefer_ipv6(self) -> bool: return not self._force_ipv4 @property - def force_ipv4(self): + def force_ipv4(self) -> bool: return self._force_ipv4 - def get_auth(self): + def get_auth(self) -> plugin.BaseAuthPlugin | None: """Return a keystoneauth plugin from the auth credentials.""" return self._auth - def skip_auth_cache(self): + def skip_auth_cache(self) -> bool: return not keyring or not self._auth or not self._cache_auth - def load_auth_from_cache(self): + def load_auth_from_cache(self) -> None: if self.skip_auth_cache(): return @@ -725,7 +783,7 @@ def set_auth_cache(self) -> None: except RuntimeError: # the fail backend raises this self.log.debug('Failed to set auth into keyring') - def insert_user_agent(self): + def insert_user_agent(self) -> None: """Set sdk information into the user agent of the Session. .. warning:: @@ -768,7 +826,7 @@ def get_session(self) -> ks_session.Session: verify=verify, cert=cert, timeout=self.config.get('api_timeout'), - collect_timing=self.config.get('timing'), + collect_timing=bool(self.config.get('timing')), discovery_cache=self._discovery_cache, ) self.insert_user_agent() @@ -782,14 +840,18 @@ def get_session(self) -> ks_session.Session: self._keystone_session.app_version = self._app_version return self._keystone_session - def get_service_catalog(self): + def get_service_catalog( + self, + ) -> ks_service_catalog.ServiceCatalog | None: """Helper method to grab the service catalog.""" # not all auth plugins are identity plugins if not isinstance(self._auth, ks_identity_base.BaseIdentityPlugin): return None return self._auth.get_access(self.get_session()).service_catalog - def _get_version_request(self, service_type, version): + def _get_version_request( + self, service_type: str, version: str | None + ) -> _util.VersionRequest: """Translate OCC version args to those needed by ksa adapter. If no version is requested explicitly and we have a configured version, @@ -833,6 +895,7 @@ def _get_version_request(self, service_type, version): f"desired major version, or omit default_microversion" ) if implied_microversion: + assert version is not None # type narrow default_microversion = implied_microversion # If we're inferring a microversion, don't pass the whole # string in as api_version, since that tells keystoneauth @@ -843,7 +906,9 @@ def _get_version_request(self, service_type, version): return version_request - def get_all_version_data(self, service_type): + def get_all_version_data( + self, service_type: str + ) -> list[discover.VersionData]: # Seriously. Don't think about the existential crisis # that is the next line. You'll wind up in cthulhu's lair. service_type = self.get_service_type(service_type) @@ -853,23 +918,34 @@ def get_all_version_data(self, service_type): interface=self.get_interface(service_type), region_name=region_name, ) - region_versions = versions.get(region_name, {}) + + region_versions = versions.get(region_name, {}) # type: ignore interface_versions = region_versions.get( - self.get_interface(service_type), {} + self.get_interface(service_type), # type: ignore + {}, ) return interface_versions.get(service_type, []) - def _get_endpoint_from_catalog(self, service_type, constructor): + def _get_endpoint_from_catalog( + self, + service_type: str, + constructor: type[proxy.Proxy], + ) -> str: adapter = constructor( session=self.get_session(), service_type=self.get_service_type(service_type), service_name=self.get_service_name(service_type), - interface=self.get_interface(service_type), + # https://review.opendev.org/c/openstack/keystoneauth/+/951183 + interface=self.get_interface(service_type), # type: ignore region_name=self.get_region_name(service_type), ) - return adapter.get_endpoint() + endpoint = adapter.get_endpoint() + assert endpoint is not None # narrow type + return endpoint - def _get_hardcoded_endpoint(self, service_type, constructor): + def _get_hardcoded_endpoint( + self, service_type: str, constructor: type[proxy.Proxy] + ) -> str: endpoint = self._get_endpoint_from_catalog(service_type, constructor) if not endpoint.rstrip().rsplit('/')[-1] == 'v2.0': if not endpoint.endswith('/'): @@ -878,8 +954,12 @@ def _get_hardcoded_endpoint(self, service_type, constructor): return endpoint def get_session_client( - self, service_type, version=None, constructor=proxy.Proxy, **kwargs - ): + self, + service_type: str, + version: str | None = None, + constructor: type[proxy.Proxy] = proxy.Proxy, + **kwargs: ty.Any, + ) -> proxy.Proxy: """Return a prepped keystoneauth Adapter for a given service. This is useful for making direct requests calls against a @@ -935,7 +1015,8 @@ def get_session_client( session=self.get_session(), service_type=self.get_service_type(service_type), service_name=self.get_service_name(service_type), - interface=self.get_interface(service_type), + # https://review.opendev.org/c/openstack/keystoneauth/+/951183 + interface=self.get_interface(service_type), # type: ignore version=version, min_version=min_api_version, max_version=max_api_version, @@ -948,7 +1029,7 @@ def get_session_client( if version_request.default_microversion: default_microversion = version_request.default_microversion info = client.get_endpoint_data() - if not discover.version_between( + if info and not discover.version_between( info.min_microversion, info.max_microversion, default_microversion, @@ -962,10 +1043,10 @@ def get_session_client( service_type=service_type, default_microversion=default_microversion, min_microversion=discover.version_to_string( - info.min_microversion + info.min_microversion or (0,) ), max_microversion=discover.version_to_string( - info.max_microversion + info.max_microversion or (0,) ), ) ) @@ -986,18 +1067,21 @@ def get_session_client( api_version=self.get_api_version(service_type), default_microversion=default_microversion, min_microversion=discover.version_to_string( - info.min_microversion + info.min_microversion or (0,) ), max_microversion=discover.version_to_string( - info.max_microversion + info.max_microversion or (0,) ), ) ) return client def get_session_endpoint( - self, service_type, min_version=None, max_version=None - ): + self, + service_type: str, + min_version: str | None = None, + max_version: str | None = None, + ) -> str | None: """Return the endpoint from config or the catalog. If a configuration lists an explicit endpoint for a service, @@ -1015,12 +1099,6 @@ def get_session_endpoint( service_name = self.get_service_name(service_type) interface = self.get_interface(service_type) session = self.get_session() - # Do this as kwargs because of os-client-config unittest mocking - version_kwargs = {} - if min_version: - version_kwargs['min_version'] = min_version - if max_version: - version_kwargs['max_version'] = max_version try: # Return the highest version we find that matches # the request @@ -1029,7 +1107,8 @@ def get_session_endpoint( region_name=region_name, interface=interface, service_name=service_name, - **version_kwargs, + min_version=min_version, + max_version=max_version, ) except keystoneauth1.exceptions.catalog.EndpointNotFound: endpoint = None @@ -1045,23 +1124,25 @@ def get_session_endpoint( ) return endpoint - def get_cache_expiration_time(self): + def get_cache_expiration_time(self) -> int: # TODO(mordred) We should be validating/transforming this on input return int(self._cache_expiration_time) - def get_cache_path(self): + def get_cache_path(self) -> str | None: return self._cache_path - def get_cache_class(self): + def get_cache_class(self) -> str: return self._cache_class - def get_cache_arguments(self): + def get_cache_arguments(self) -> dict[str, ty.Any] | None: return copy.deepcopy(self._cache_arguments) - def get_cache_expirations(self): + def get_cache_expirations(self) -> dict[str, int]: return copy.deepcopy(self._cache_expirations) - def get_cache_resource_expiration(self, resource, default=None): + def get_cache_resource_expiration( + self, resource: str, default: float | None = None + ) -> float | None: """Get expiration time for a resource :param resource: Name of the resource type @@ -1074,19 +1155,22 @@ def get_cache_resource_expiration(self, resource, default=None): return default return float(self._cache_expirations[resource]) - def requires_floating_ip(self): + def requires_floating_ip(self) -> bool | None: """Return whether or not this cloud requires floating ips. - :returns: True of False if know, None if discovery is needed. + :returns: True or False if know, None if discovery is needed. If requires_floating_ip is not configured but the cloud is known to not provide floating ips, will return False. """ if self.config['floating_ip_source'] == "None": return False - return self.config.get('requires_floating_ip') + requires_floating_ip = self.config.get('requires_floating_ip') + if requires_floating_ip is None: + return None + return bool(requires_floating_ip) - def get_external_networks(self): + def get_external_networks(self) -> list[str]: """Get list of network names for external networks.""" return [ net['name'] @@ -1094,68 +1178,72 @@ def get_external_networks(self): if net['routes_externally'] ] - def get_external_ipv4_networks(self): + def get_external_ipv4_networks(self) -> list[str]: """Get list of network names for external IPv4 networks.""" return [ - net['name'] + str(net['name']) for net in self.config.get('networks', []) if net['routes_ipv4_externally'] ] - def get_external_ipv6_networks(self): + def get_external_ipv6_networks(self) -> list[str]: """Get list of network names for external IPv6 networks.""" return [ - net['name'] + str(net['name']) for net in self.config.get('networks', []) if net['routes_ipv6_externally'] ] - def get_internal_networks(self): + def get_internal_networks(self) -> list[str]: """Get list of network names for internal networks.""" return [ - net['name'] + str(net['name']) for net in self.config.get('networks', []) if not net['routes_externally'] ] - def get_internal_ipv4_networks(self): + def get_internal_ipv4_networks(self) -> list[str]: """Get list of network names for internal IPv4 networks.""" return [ - net['name'] + str(net['name']) for net in self.config.get('networks', []) if not net['routes_ipv4_externally'] ] - def get_internal_ipv6_networks(self): + def get_internal_ipv6_networks(self) -> list[str]: """Get list of network names for internal IPv6 networks.""" return [ - net['name'] + str(net['name']) for net in self.config.get('networks', []) if not net['routes_ipv6_externally'] ] - def get_default_network(self): + def get_default_network(self) -> str | None: """Get network used for default interactions.""" for net in self.config.get('networks', []): if net['default_interface']: - return net['name'] + return str(net['name']) return None - def get_nat_destination(self): + def get_nat_destination(self) -> str | None: """Get network used for NAT destination.""" for net in self.config.get('networks', []): if net['nat_destination']: - return net['name'] + return str(net['name']) return None - def get_nat_source(self): + def get_nat_source(self) -> str | None: """Get network used for NAT source.""" for net in self.config.get('networks', []): if net.get('nat_source'): - return net['name'] + return str(net['name']) return None - def _get_extra_config(self, key, defaults=None): + def _get_extra_config( + self, + key: str | None, + defaults: dict[str, ty.Any] | None = None, + ) -> dict[str, ty.Any]: """Fetch an arbitrary extra chunk of config, laying in defaults. :param string key: name of the config section to fetch @@ -1164,12 +1252,16 @@ def _get_extra_config(self, key, defaults=None): """ defaults = _util.normalize_keys(defaults or {}) if not key: - return defaults + return defaults or {} return _util.merge_clouds( defaults, _util.normalize_keys(self._extra_config.get(key, {})) ) - def get_client_config(self, name=None, defaults=None): + def get_client_config( + self, + name: str | None = None, + defaults: dict[str, ty.Any] | None = None, + ) -> dict[str, ty.Any] | None: """Get config settings for a named client. Settings will also be looked for in a section called 'client'. @@ -1190,15 +1282,15 @@ def get_client_config(self, name=None, defaults=None): name, self._get_extra_config('client', defaults) ) - def get_password_callback(self): + def get_password_callback(self) -> _PasswordCallback | None: return self._password_callback - def get_rate_limit(self, service_type=None): + def get_rate_limit(self, service_type: str) -> float | None: return self._get_service_config( 'rate_limit', service_type=service_type ) - def get_concurrency(self, service_type=None): + def get_concurrency(self, service_type: str) -> int | None: return self._get_service_config( 'concurrency', service_type=service_type ) @@ -1284,14 +1376,18 @@ def get_prometheus_counter( registry._openstacksdk_counter = counter return counter - def has_service(self, service_type): + def has_service(self, service_type: str) -> bool: service_type = service_type.lower().replace('-', '_') key = f'has_{service_type}' - return self.config.get( + value = self.config.get( key, self._service_type_manager.is_official(service_type) ) + assert isinstance(value, bool) + return value - def disable_service(self, service_type, reason=None): + def disable_service( + self, service_type: str, reason: str | None = None + ) -> None: _disable_service(self.config, service_type, reason=reason) def enable_service(self, service_type: str) -> None: @@ -1299,7 +1395,7 @@ def enable_service(self, service_type: str) -> None: key = f'has_{service_type}' self.config[key] = True - def get_disabled_reason(self, service_type): + def get_disabled_reason(self, service_type: str) -> str | None: service_type = service_type.lower().replace('-', '_') d_key = _make_key('disabled_reason', service_type) return self.config.get(d_key) diff --git a/openstack/config/defaults.py b/openstack/config/defaults.py index a4fc6f05d..927a7f99c 100644 --- a/openstack/config/defaults.py +++ b/openstack/config/defaults.py @@ -15,6 +15,7 @@ import json import os import threading +import typing as ty _json_path = os.path.join( os.path.dirname(os.path.realpath(__file__)), 'defaults.json' @@ -24,7 +25,7 @@ # json_path argument is there for os-client-config -def get_defaults(json_path=_json_path): +def get_defaults(json_path: str = _json_path) -> dict[str, ty.Any]: global _defaults if _defaults is not None: return _defaults.copy() @@ -39,13 +40,13 @@ def get_defaults(json_path=_json_path): # NOTE(harlowja): update a in-memory dict, before updating # the global one so that other callers of get_defaults do not # see the partially filled one. - tmp_defaults = dict( - api_timeout=None, - verify=True, - cacert=None, - cert=None, - key=None, - ) + tmp_defaults = { + 'api_timeout': None, + 'verify': True, + 'cacert': None, + 'cert': None, + 'key': None, + } with open(json_path) as json_file: updates = json.load(json_file) if updates is not None: diff --git a/openstack/tests/unit/cloud/test_operator.py b/openstack/tests/unit/cloud/test_operator.py index d3fd1f62b..fa70a4dc0 100644 --- a/openstack/tests/unit/cloud/test_operator.py +++ b/openstack/tests/unit/cloud/test_operator.py @@ -109,10 +109,12 @@ def test_get_session_endpoint_identity(self, get_session_mock): get_session_mock.return_value = session_mock self.cloud.get_session_endpoint('identity') kwargs = dict( - interface='public', + service_type='identity', region_name='RegionOne', + interface='public', service_name=None, - service_type='identity', + min_version=None, + max_version=None, ) session_mock.get_endpoint.assert_called_with(**kwargs) diff --git a/openstack/tests/unit/config/test_cloud_config.py b/openstack/tests/unit/config/test_cloud_config.py index 16dbc4501..855a30894 100644 --- a/openstack/tests/unit/config/test_cloud_config.py +++ b/openstack/tests/unit/config/test_cloud_config.py @@ -88,7 +88,8 @@ def test_get_config(self): self.assertIsNone(cc._get_config('nothing', None)) # This is what is happening behind the scenes in get_default_interface. self.assertEqual( - fake_services_dict['interface'], cc._get_config('interface', None) + fake_services_dict['interface'], + cc._get_config('interface', None), ) # The same call as above, but from one step up the stack self.assertEqual(fake_services_dict['interface'], cc.get_interface()) @@ -299,7 +300,7 @@ def test_get_session(self, mock_session): verify=True, cert=None, timeout=None, - collect_timing=None, + collect_timing=False, discovery_cache=None, ) self.assertEqual( @@ -330,7 +331,7 @@ def test_get_session_with_app_name(self, mock_session): verify=True, cert=None, timeout=None, - collect_timing=None, + collect_timing=False, discovery_cache=None, ) self.assertEqual(fake_session.app_name, "test_app") @@ -357,7 +358,7 @@ def test_get_session_with_timeout(self, mock_session): verify=True, cert=None, timeout=9, - collect_timing=None, + collect_timing=False, discovery_cache=None, ) self.assertEqual( @@ -425,10 +426,12 @@ def test_session_endpoint(self, mock_get_session): ) cc.get_session_endpoint('orchestration') mock_session.get_endpoint.assert_called_with( + service_type='orchestration', + region_name='region-al', interface='public', service_name=None, - region_name='region-al', - service_type='orchestration', + min_version=None, + max_version=None, ) @mock.patch.object(cloud_region.CloudRegion, 'get_session') diff --git a/pyproject.toml b/pyproject.toml index 2709343bc..f446deb91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,14 +30,16 @@ exclude = ''' [[tool.mypy.overrides]] module = [ + "openstack._log", "openstack.common", "openstack.common.*", - # "openstack.config.cloud_region", + "openstack.config._util", + "openstack.config.defaults", + "openstack.config.cloud_region", "openstack.connection", "openstack.exceptions", "openstack.fields", "openstack.format", - "openstack._log", "openstack.proxy", "openstack.utils", "openstack.version", From 5a2b3bd81a65587c9815a346e0c31d434a1f5a0c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 26 May 2025 17:05:47 +0100 Subject: [PATCH 3720/3836] typing: Annotate openstack.config.vendors Change-Id: I397dd90847dbab8523a6394501470ecd71e9a1e8 Signed-off-by: Stephen Finucane --- openstack/config/vendors/__init__.py | 9 +++++---- pyproject.toml | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/openstack/config/vendors/__init__.py b/openstack/config/vendors/__init__.py index af0d3cd3d..94c8d5b8c 100644 --- a/openstack/config/vendors/__init__.py +++ b/openstack/config/vendors/__init__.py @@ -15,6 +15,7 @@ import glob import json import os +import typing as ty import urllib import requests @@ -24,11 +25,11 @@ from openstack import exceptions _VENDORS_PATH = os.path.dirname(os.path.realpath(__file__)) -_VENDOR_DEFAULTS: dict[str, dict] = {} +_VENDOR_DEFAULTS: dict[str, dict[str, ty.Any]] = {} _WELL_KNOWN_PATH = "{scheme}://{netloc}/.well-known/openstack/api" -def _get_vendor_defaults(): +def _get_vendor_defaults() -> dict[str, dict[str, ty.Any]]: global _VENDOR_DEFAULTS if not _VENDOR_DEFAULTS: @@ -45,7 +46,7 @@ def _get_vendor_defaults(): return _VENDOR_DEFAULTS -def get_profile(profile_name): +def get_profile(profile_name: str) -> dict[str, ty.Any] | None: vendor_defaults = _get_vendor_defaults() if profile_name in vendor_defaults: return vendor_defaults[profile_name].copy() @@ -53,7 +54,7 @@ def get_profile(profile_name): profile_url = urllib.parse.urlparse(profile_name) if not profile_url.netloc: # This isn't a url, and we already don't have it. - return + return None well_known_url = _WELL_KNOWN_PATH.format( scheme=profile_url.scheme, diff --git a/pyproject.toml b/pyproject.toml index f446deb91..732bec82f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ module = [ "openstack.config._util", "openstack.config.defaults", "openstack.config.cloud_region", + "openstack.config.vendors", "openstack.connection", "openstack.exceptions", "openstack.fields", From 550c15c83bd165879b744fe8c373c546caac2405 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 26 May 2025 16:57:17 +0100 Subject: [PATCH 3721/3836] typing: Annotate openstack.config.loader Change-Id: I712bc5dc6ff43fd9508fcad1ddb86230e7c59c65 Signed-off-by: Stephen Finucane --- openstack/config/__init__.py | 10 +- openstack/config/loader.py | 215 +++++++++++++++++++++++++---------- pyproject.toml | 1 + tools/nova_version.py | 2 + 4 files changed, 167 insertions(+), 61 deletions(-) diff --git a/openstack/config/__init__.py b/openstack/config/__init__.py index 5b9d3d417..f11ead173 100644 --- a/openstack/config/__init__.py +++ b/openstack/config/__init__.py @@ -22,6 +22,13 @@ from openstack.config import cloud_region +__all__ = [ + 'OpenStackConfig', + 'cloud_region', + 'get_cloud_region', +] + + # TODO(stephenfin): Expand kwargs once we've typed OpenstackConfig.get_one def get_cloud_region( service_key: str | None = None, @@ -53,7 +60,8 @@ def get_cloud_region( app_version=app_version, ) if options: - config.register_argparse_arguments(options, sys.argv, service_key) + service_keys = [service_key] if service_key is not None else [] + config.register_argparse_arguments(options, sys.argv, service_keys) parsed_options, _ = options.parse_known_args(sys.argv) else: parsed_options = None diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 836fab380..3f8871d4b 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -40,6 +40,7 @@ if ty.TYPE_CHECKING: from keystoneauth1.loading._plugins.identity import v3 as v3_loaders + from keystoneauth1.loading import opts PLATFORMDIRS = platformdirs.PlatformDirs( 'openstack', 'OpenStack', multipath=True @@ -95,7 +96,7 @@ FORMAT_EXCLUSIONS = frozenset(['password']) -def get_boolean(value): +def get_boolean(value: ty.Any) -> bool: if value is None: return False if type(value) is bool: @@ -105,7 +106,9 @@ def get_boolean(value): return False -def _auth_update(old_dict, new_dict_source): +def _auth_update( + old_dict: dict[str, ty.Any], new_dict_source: dict[str, ty.Any] +) -> dict[str, ty.Any]: """Like dict.update, except handling the nested dict called auth.""" new_dict = copy.deepcopy(new_dict_source) for k, v in new_dict.items(): @@ -119,7 +122,7 @@ def _auth_update(old_dict, new_dict_source): return old_dict -def _fix_argv(argv): +def _fix_argv(argv: list[str]) -> None: # Transform any _ characters in arg names to - so that we don't # have to throw billions of compat argparse arguments around all # over the place. @@ -156,6 +159,15 @@ class OpenStackConfig: _cloud_region_class = cloud_region.CloudRegion _defaults_module = defaults + #: config_filename is the filename that configuration was loaded from, if + #: any. + config_filename: str | None + #: secure_config_filename is the filename that secure configuration was + #: loaded from, if any. + secure_config_filename: str | None + #: cloud_config contains the combined loaded configuration. + cloud_config: dict[str, ty.Any] + def __init__( self, config_files: list[str] | None = None, @@ -213,21 +225,25 @@ def __init__( self.defaults.update(override_defaults) # First, use a config file if it exists where expected - self.config_filename, self.cloud_config = self._load_config_file() - if self.config_filename: - self._validate_config_file(self.config_filename, self.cloud_config) + config_filename, cloud_config = self._load_config_file() + if config_filename and cloud_config: + self._validate_config_file(config_filename, cloud_config) secure_config_filename, secure_config = self._load_secure_file() - if secure_config: + if secure_config_filename and secure_config: self._validate_config_file(secure_config_filename, secure_config) - self.cloud_config = _util.merge_clouds( - self.cloud_config, secure_config + cloud_config = _util.merge_clouds( + cloud_config or {}, secure_config ) - if not self.cloud_config: + self.config_filename = config_filename + self.secure_config_filename = secure_config_filename + if not cloud_config: self.cloud_config = {'clouds': {}} - elif 'clouds' not in self.cloud_config: - self.cloud_config['clouds'] = {} + else: + self.cloud_config = cloud_config + if 'clouds' not in self.cloud_config: + self.cloud_config['clouds'] = {} # Save the other config self.extra_config = copy.deepcopy(self.cloud_config) @@ -400,7 +416,9 @@ def __init__( # password = self._pw_callback(prompt="Password: ") self._pw_callback = pw_func - def _get_os_environ(self, envvar_prefix=None): + def _get_os_environ( + self, envvar_prefix: str | None = None + ) -> dict[str, ty.Any] | None: ret = self._defaults_module.get_defaults() if not envvar_prefix: # This makes the or below be OS_ or OS_ which is a no-op @@ -429,12 +447,14 @@ def _get_os_environ(self, envvar_prefix=None): return ret return None - def _get_envvar(self, key, default=None): + def _get_envvar(self, key: str, default: str | None = None) -> str | None: if not self._load_envvars: return default return os.environ.get(key, default) - def get_extra_config(self, key, defaults=None): + def get_extra_config( + self, key: str, defaults: dict[str, ty.Any] | None = None + ) -> dict[str, ty.Any]: """Fetch an arbitrary extra chunk of config, laying in defaults. :param string key: name of the config section to fetch @@ -442,22 +462,31 @@ def get_extra_config(self, key, defaults=None): found config """ defaults = _util.normalize_keys(defaults or {}) + assert defaults is not None # narrow type if not key: return defaults return _util.merge_clouds( defaults, _util.normalize_keys(self.cloud_config.get(key, {})) ) - def _load_config_file(self): + def _load_config_file( + self, + ) -> tuple[str, dict[str, ty.Any]] | tuple[None, None]: return self._load_yaml_json_file(self._config_files) - def _load_secure_file(self): + def _load_secure_file( + self, + ) -> tuple[str, dict[str, ty.Any]] | tuple[None, None]: return self._load_yaml_json_file(self._secure_files) - def _load_vendor_file(self): + def _load_vendor_file( + self, + ) -> tuple[str, dict[str, ty.Any]] | tuple[None, None]: return self._load_yaml_json_file(self._vendor_files) - def _load_yaml_json_file(self, filelist): + def _load_yaml_json_file( + self, filelist: list[str] + ) -> tuple[str, dict[str, ty.Any]] | tuple[None, None]: for path in filelist: if os.path.exists(path): try: @@ -471,7 +500,7 @@ def _load_yaml_json_file(self, filelist): # Can't access file so let's continue to the next # file continue - return (None, {}) + return (None, None) def _validate_config_file(self, path: str, data: ty.Any) -> bool: """Validate config file contains a clouds entry. @@ -492,10 +521,12 @@ def _validate_config_file(self, path: str, data: ty.Any) -> bool: return True - def _expand_region_name(self, region_name): + def _expand_region_name(self, region_name: str) -> dict[str, ty.Any]: return {'name': region_name, 'values': {}} - def _expand_regions(self, regions): + def _expand_regions( + self, regions: list[str | dict[str, ty.Any]] + ) -> list[dict[str, ty.Any]]: ret = [] for region in regions: if isinstance(region, dict): @@ -513,7 +544,7 @@ def _expand_regions(self, regions): ret.append(self._expand_region_name(region)) return ret - def _get_regions(self, cloud): + def _get_regions(self, cloud: str) -> list[dict[str, ty.Any]]: if cloud not in self.cloud_config['clouds']: return [self._expand_region_name('')] regions = self._get_known_regions(cloud) @@ -522,7 +553,7 @@ def _get_regions(self, cloud): regions = [self._expand_region_name('')] return regions - def _get_known_regions(self, cloud): + def _get_known_regions(self, cloud: str) -> list[dict[str, ty.Any]]: config = _util.normalize_keys(self.cloud_config['clouds'][cloud]) if 'regions' in config: return self._expand_regions(config['regions']) @@ -549,9 +580,14 @@ def _get_known_regions(self, cloud): elif 'region_name' in new_cloud and new_cloud['region_name']: return [self._expand_region_name(new_cloud['region_name'])] - def _get_region(self, cloud=None, region_name=''): + return [] + + def _get_region( + self, cloud: str | None = None, region_name: str = '' + ) -> dict[str, ty.Any]: if region_name is None: region_name = '' + if not cloud: return self._expand_region_name(region_name) @@ -576,11 +612,13 @@ def _get_region(self, cloud=None, region_name=''): ) ) - def get_cloud_names(self): - return self.cloud_config['clouds'].keys() + def get_cloud_names(self) -> list[str]: + return list(self.cloud_config['clouds'].keys()) - def _get_base_cloud_config(self, name, profile=None): - cloud = dict() + def _get_base_cloud_config( + self, name: str | None, profile: str | None = None + ) -> dict[str, ty.Any]: + cloud = {} # Only validate cloud name if one was given if name and name not in self.cloud_config['clouds']: @@ -595,7 +633,7 @@ def _get_base_cloud_config(self, name, profile=None): self._expand_vendor_profile(name, cloud, our_cloud) if 'auth' not in cloud: - cloud['auth'] = dict() + cloud['auth'] = {} _auth_update(cloud, our_cloud) if 'cloud' in cloud: @@ -603,7 +641,12 @@ def _get_base_cloud_config(self, name, profile=None): return cloud - def _expand_vendor_profile(self, name, cloud, our_cloud): + def _expand_vendor_profile( + self, + name: str | None, + cloud: dict[str, ty.Any], + our_cloud: dict[str, ty.Any], + ) -> None: # Expand a profile if it exists. 'cloud' is an old confusing name # for this. profile_name = our_cloud.get('profile', our_cloud.get('cloud', None)) @@ -653,7 +696,7 @@ def _expand_vendor_profile(self, name, cloud, our_cloud): os_warnings.ConfigurationWarning, ) - def _project_scoped(self, cloud): + def _project_scoped(self, cloud: dict[str, ty.Any]) -> bool: return ( 'project_id' in cloud or 'project_name' in cloud @@ -661,7 +704,9 @@ def _project_scoped(self, cloud): or 'project_name' in cloud['auth'] ) - def _validate_networks(self, networks, key): + def _validate_networks( + self, networks: list[dict[str, ty.Any]], key: str + ) -> None: value = None for net in networks: if value and net[key]: @@ -674,7 +719,9 @@ def _validate_networks(self, networks, key): if not value and net[key]: value = net - def _fix_backwards_networks(self, cloud): + def _fix_backwards_networks( + self, cloud: dict[str, ty.Any] + ) -> dict[str, ty.Any]: # Leave the external_network and internal_network keys in the # dict because consuming code might be expecting them. networks = [] @@ -733,7 +780,7 @@ def _fix_backwards_networks(self, cloud): cloud['networks'] = networks return cloud - def _handle_domain_id(self, cloud): + def _handle_domain_id(self, cloud: dict[str, ty.Any]) -> dict[str, ty.Any]: # Allow people to just specify domain once if it's the same mappings = { 'domain_id': ('user_domain_id', 'project_domain_id'), @@ -751,7 +798,9 @@ def _handle_domain_id(self, cloud): cloud['auth'].pop(target_key, None) return cloud - def _fix_backwards_auth(self, cloud): + def _fix_backwards_auth( + self, cloud: dict[str, ty.Any] + ) -> dict[str, ty.Any]: mappings = { 'domain_id': ('domain_id', 'domain-id'), 'domain_name': ('domain_name', 'domain-name'), @@ -810,7 +859,9 @@ def _fix_backwards_auth(self, cloud): cloud['auth'][target_key] = target return cloud - def _fix_backwards_auth_plugin(self, cloud): + def _fix_backwards_auth_plugin( + self, cloud: dict[str, ty.Any] + ) -> dict[str, ty.Any]: # Do the lists backwards so that auth_type is the ultimate winner mappings = { 'auth_type': ('auth_plugin', 'auth_type'), @@ -828,7 +879,12 @@ def _fix_backwards_auth_plugin(self, cloud): # completely broken return cloud - def register_argparse_arguments(self, parser, argv, service_keys=None): + def register_argparse_arguments( + self, + parser: argparse_mod.ArgumentParser, + argv: list[str], + service_keys: list[str] | None = None, + ) -> None: """Register all of the common argparse options needed. Given an argparse parser, register the keystoneauth Session arguments, @@ -937,7 +993,9 @@ def register_argparse_arguments(self, parser, argv, service_keys=None): parser.add_argument('--os-endpoint-type', help=argparse_mod.SUPPRESS) parser.add_argument('--endpoint-type', help=argparse_mod.SUPPRESS) - def _fix_backwards_interface(self, cloud): + def _fix_backwards_interface( + self, cloud: dict[str, ty.Any] + ) -> dict[str, ty.Any]: new_cloud = {} for key in cloud.keys(): if key.endswith('endpoint_type'): @@ -947,7 +1005,9 @@ def _fix_backwards_interface(self, cloud): new_cloud[target_key] = cloud[key] return new_cloud - def _fix_backwards_api_timeout(self, cloud): + def _fix_backwards_api_timeout( + self, cloud: dict[str, ty.Any] + ) -> dict[str, ty.Any]: new_cloud = {} # requests can only have one timeout, which means that in a single # cloud there is no point in different timeout values. However, @@ -972,7 +1032,7 @@ def _fix_backwards_api_timeout(self, cloud): new_cloud['api_timeout'] = new_cloud.pop('timeout') return new_cloud - def get_all(self): + def get_all(self) -> list[cloud_region.CloudRegion]: clouds = [] for cloud in self.get_cloud_names(): @@ -983,7 +1043,7 @@ def get_all(self): ) return clouds - def get_all_clouds(self): + def get_all_clouds(self) -> list[cloud_region.CloudRegion]: warnings.warn( "The 'get_all_clouds' method is a deprecated alias for " "'get_clouds' and will be removed in a future release.", @@ -991,7 +1051,11 @@ def get_all_clouds(self): ) return self.get_all() - def _fix_args(self, args=None, argparse=None): + def _fix_args( + self, + args: dict[str, ty.Any] | None = None, + argparse: argparse_mod.Namespace | None = None, + ) -> dict[str, ty.Any]: """Massage the passed-in options Replace - with _ and strip os_ prefixes. @@ -1011,10 +1075,10 @@ def _fix_args(self, args=None, argparse=None): parsed_args[k] = o_dict[k] args.update(parsed_args) - os_args = dict() - new_args = dict() + os_args = {} + new_args = {} for key, val in iter(args.items()): - if type(args[key]) is dict: + if isinstance(args[key], dict): # dive into the auth dict new_args[key] = self._fix_args(args[key]) continue @@ -1027,7 +1091,9 @@ def _fix_args(self, args=None, argparse=None): new_args.update(os_args) return new_args - def _find_winning_auth_value(self, opt, config): + def _find_winning_auth_value( + self, opt: 'opts.Opt', config: dict[str, dict[str, ty.Any]] + ) -> dict[str, ty.Any] | None: opt_name = opt.name.replace('-', '_') if opt_name in config: return config[opt_name] @@ -1040,7 +1106,9 @@ def _find_winning_auth_value(self, opt, config): if d_opt_name in config: return config[d_opt_name] - def auth_config_hook(self, config): + return None + + def auth_config_hook(self, config: dict[str, ty.Any]) -> dict[str, ty.Any]: """Allow examination of config values before loading auth plugin OpenStackClient will override this to perform additional checks @@ -1048,7 +1116,9 @@ def auth_config_hook(self, config): """ return config - def _get_auth_loader(self, config): + def _get_auth_loader( + self, config: dict[str, ty.Any] + ) -> loading.BaseLoader[ty.Any]: # Use the 'none' plugin for variants of None specified, # since it does not look up endpoints or tokens but rather # does a passthrough. This is useful for things like Ironic @@ -1087,7 +1157,9 @@ def _get_auth_loader(self, config): return loader - def _validate_auth(self, config, loader): + def _validate_auth( + self, config: dict[str, ty.Any], loader: loading.BaseLoader[ty.Any] + ) -> dict[str, ty.Any]: # May throw a keystoneauth1.exceptions.NoMatchingPlugin plugin_options = loader.get_options() @@ -1127,7 +1199,9 @@ def _validate_auth(self, config, loader): return config - def _validate_auth_correctly(self, config, loader): + def _validate_auth_correctly( + self, config: dict[str, ty.Any], loader: loading.BaseLoader[ty.Any] + ) -> dict[str, ty.Any]: # May throw a keystoneauth1.exceptions.NoMatchingPlugin plugin_options = loader.get_options() @@ -1158,7 +1232,9 @@ def _validate_auth_correctly(self, config, loader): return config - def option_prompt(self, config, p_opt): + def option_prompt( + self, config: dict[str, ty.Any], p_opt: 'opts.Opt' + ) -> dict[str, ty.Any]: """Prompt user for option that requires a value""" if ( getattr(p_opt, 'prompt', None) is not None @@ -1168,7 +1244,12 @@ def option_prompt(self, config, p_opt): config['auth'][p_opt.dest] = self._pw_callback(p_opt.prompt) return config - def _clean_up_after_ourselves(self, config, p_opt, winning_value): + def _clean_up_after_ourselves( + self, + config: dict[str, ty.Any], + p_opt: 'opts.Opt', + winning_value: ty.Any, + ) -> dict[str, ty.Any]: # Clean up after ourselves for opt in [p_opt.name] + [o.name for o in p_opt.deprecated]: opt = opt.replace('-', '_') @@ -1184,7 +1265,9 @@ def _clean_up_after_ourselves(self, config, p_opt, winning_value): config['auth'][p_opt.dest] = winning_value return config - def _handle_value_types(self, config: dict) -> dict: + def _handle_value_types( + self, config: dict[str, ty.Any] + ) -> dict[str, ty.Any]: for key in BOOL_KEYS: if key in config: if not isinstance(config[key], bool): @@ -1196,7 +1279,7 @@ def _handle_value_types(self, config: dict) -> dict: config[key] = config[key].split(',') return config - def magic_fixes(self, config): + def magic_fixes(self, config: dict[str, ty.Any]) -> dict[str, ty.Any]: """Perform the set of magic argument fixups""" # These backwards compat values are only set via argparse. If it's @@ -1345,8 +1428,12 @@ def get_one( ) def get_one_cloud( - self, cloud=None, validate=True, argparse=None, **kwargs - ): + self, + cloud: str | None = None, + validate: bool = True, + argparse: argparse_mod.Namespace | None = None, + **kwargs: ty.Any, + ) -> cloud_region.CloudRegion: warnings.warn( "The 'get_one_cloud' method is a deprecated alias for 'get_one' " "and will be removed in a future release.", @@ -1360,8 +1447,12 @@ def get_one_cloud( ) def get_one_cloud_osc( - self, cloud=None, validate=True, argparse=None, **kwargs - ): + self, + cloud: str | None = None, + validate: bool = True, + argparse: argparse_mod.Namespace | None = None, + **kwargs: ty.Any, + ) -> cloud_region.CloudRegion: """Retrieve a single CloudRegion and merge additional options :param string cloud: @@ -1456,7 +1547,11 @@ def get_one_cloud_osc( ) @staticmethod - def set_one_cloud(config_file, cloud, set_config=None): + def set_one_cloud( + config_file: str, + cloud: str, + set_config: dict[str, ty.Any] | None = None, + ) -> None: """Set a single cloud configuration. :param string config_file: diff --git a/pyproject.toml b/pyproject.toml index 732bec82f..e204658e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ module = [ "openstack.config._util", "openstack.config.defaults", "openstack.config.cloud_region", + "openstack.config.loader", "openstack.config.vendors", "openstack.connection", "openstack.exceptions", diff --git a/tools/nova_version.py b/tools/nova_version.py index e5593fbc9..65546a9f4 100644 --- a/tools/nova_version.py +++ b/tools/nova_version.py @@ -24,6 +24,8 @@ try: raw_endpoint = c.get_endpoint() have_current = False + if raw_endpoint is None: + raise Exception('endpoint was empty') endpoint = raw_endpoint.rsplit('/', 2)[0] print(endpoint) r = c.get(endpoint).json() From 36a7ccb6f547664d23bf13b4f2cf86ca442c792b Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 28 Mar 2025 16:37:36 +0000 Subject: [PATCH 3722/3836] typing: Annotate openstack.config.cloud_config Change-Id: Ia5ebf735fbe5e24d54f7c4fd502a5c114235e68b Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 1 + openstack/config/cloud_config.py | 76 +++++++++++++++++++++++++++++- openstack/proxy.py | 1 + openstack/tests/functional/base.py | 2 +- pyproject.toml | 1 + 5 files changed, 78 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d358fedda..c8ecad6d4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,6 +36,7 @@ repos: hooks: - id: mypy additional_dependencies: + - dogpile.cache - keystoneauth1>=5.11.0 - types-decorator - types-PyYAML diff --git a/openstack/config/cloud_config.py b/openstack/config/cloud_config.py index fdb4925b8..3c695a8f5 100644 --- a/openstack/config/cloud_config.py +++ b/openstack/config/cloud_config.py @@ -12,12 +12,84 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty +import warnings # TODO(mordred) This is only here to ease the OSC transition from openstack.config import cloud_region +from openstack import warnings as os_warnings + +if ty.TYPE_CHECKING: + from keystoneauth1 import discover + from keystoneauth1 import plugin + from keystoneauth1 import session as ks_session + import prometheus_client + + from openstack.config import loader class CloudConfig(cloud_region.CloudRegion): - def __init__(self, name, region, config, **kwargs): - super().__init__(name, region, config, **kwargs) + def __init__( + self, + name: str | None, + region: str | None, + config: dict[str, ty.Any] | None, + force_ipv4: bool = False, + auth_plugin: ty.Optional['plugin.BaseAuthPlugin'] = None, + openstack_config: ty.Optional['loader.OpenStackConfig'] = None, + session_constructor: type['ks_session.Session'] | None = None, + app_name: str | None = None, + app_version: str | None = None, + session: ty.Optional['ks_session.Session'] = None, + discovery_cache: dict[str, 'discover.Discover'] | None = None, + extra_config: dict[str, ty.Any] | None = None, + cache_expiration_time: int = 0, + cache_expirations: dict[str, int] | None = None, + cache_path: str | None = None, + cache_class: str = 'dogpile.cache.null', + cache_arguments: dict[str, ty.Any] | None = None, + password_callback: cloud_region._PasswordCallback | None = None, + statsd_host: str | None = None, + statsd_port: str | None = None, + statsd_prefix: str | None = None, + # TODO(stephenfin): Add better types + influxdb_config: dict[str, ty.Any] | None = None, + collector_registry: ty.Optional[ + 'prometheus_client.CollectorRegistry' + ] = None, + cache_auth: bool = False, + ) -> None: + warnings.warn( + 'The CloudConfig class has been deprecated in favour of ' + 'CloudRegion. Please update your references.', + os_warnings.RemovedInSDK60Warning, + ) + self.region = region + + super().__init__( + name, + region, + config, + force_ipv4=force_ipv4, + auth_plugin=auth_plugin, + openstack_config=openstack_config, + session_constructor=session_constructor, + app_name=app_name, + app_version=app_version, + session=session, + discovery_cache=discovery_cache, + extra_config=extra_config, + cache_expiration_time=cache_expiration_time, + cache_expirations=cache_expirations, + cache_path=cache_path, + cache_class=cache_class, + cache_arguments=cache_arguments, + password_callback=password_callback, + statsd_host=statsd_host, + statsd_port=statsd_port, + statsd_prefix=statsd_prefix, + influxdb_config=influxdb_config, + collector_registry=collector_registry, + cache_auth=cache_auth, + ) diff --git a/openstack/proxy.py b/openstack/proxy.py index b13786c6c..3c028aabd 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -222,6 +222,7 @@ def request( try: if conn.cache_enabled and not skip_cache and method == 'GET': + assert key is not None # type narrow # Get the object expiration time from config # default to 0 to disable caching for this resource type expiration_time = int( diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index f42f719ff..c2c0fdab8 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -245,7 +245,7 @@ def setUp(self): service_type=service_type, **kwargs ) - if not ( + if not data or not ( data.min_microversion and data.max_microversion and discover.version_between( diff --git a/pyproject.toml b/pyproject.toml index e204658e9..7faf07ff8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ module = [ "openstack.common.*", "openstack.config._util", "openstack.config.defaults", + "openstack.config.cloud_config", "openstack.config.cloud_region", "openstack.config.loader", "openstack.config.vendors", From 7c1e42b4175cd34a2eefb86996221fa55ab895d2 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 5 Jun 2025 17:25:21 +0100 Subject: [PATCH 3723/3836] config: Handle interface list in CloudRegion.get_all_version_data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If a connection is created via an oslo.config cfg.ConfigOpts object, then it's possible for interface to be a list of interfaces (due to valid_interfaces). We were not previously handling this, resulting in the following error: ❯ python -m cfg_test --config-file sample.conf Traceback (most recent call last): File "", line 198, in _run_module_as_main File "", line 88, in _run_code File "/dev/cfg-test/__main__.py", line 25, in print(conn.config.get_all_version_data('placement')) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^ File "/dev/openstacksdk/openstack/config/cloud_region.py", line 926, in get_all_version_data interface_versions = region_versions.get(interface, {}) TypeError: unhashable type: 'list' Correct this by iterating through all interfaces until we find a non-null version information. Change-Id: I6e5e1b0f5357afa0462703eb18b9f2da340e90a4 Signed-off-by: Stephen Finucane --- openstack/config/cloud_region.py | 28 +++++++++++++++++++++------- openstack/connection.py | 2 +- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 75cba2c27..b7899358b 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -909,22 +909,36 @@ def _get_version_request( def get_all_version_data( self, service_type: str ) -> list[discover.VersionData]: + """Retrieve version data for the given service. + + :param service_type: The service to fetch version data for. + :returns: A `~keystoneauth1.discover.VersionData` object containing the + version data for the requested service. + """ # Seriously. Don't think about the existential crisis # that is the next line. You'll wind up in cthulhu's lair. service_type = self.get_service_type(service_type) region_name = self.get_region_name(service_type) + assert region_name is not None # narrow type + interface = self.get_interface(service_type) + assert interface is not None # narrow type + + interfaces = interface if isinstance(interface, list) else [interface] + versions = self.get_session().get_all_version_data( service_type=service_type, - interface=self.get_interface(service_type), + interface=interface, region_name=region_name, ) - region_versions = versions.get(region_name, {}) # type: ignore - interface_versions = region_versions.get( - self.get_interface(service_type), # type: ignore - {}, - ) - return interface_versions.get(service_type, []) + region_versions = versions.get(region_name, {}) + for interface in interfaces: + interface_versions = region_versions.get(interface, {}) + service_version_data = interface_versions.get(service_type) + if service_version_data is not None: + return service_version_data + + return [] def _get_endpoint_from_catalog( self, diff --git a/openstack/connection.py b/openstack/connection.py index 74bcdf777..b5b75c833 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -134,8 +134,8 @@ .. code-block:: python from keystoneauth1 import loading as ks_loading - from oslo_config import cfg from openstack import connection + from oslo_config import cfg CONF = cfg.CONF From 5a909ee17aa7a67d2840e1b7ba1a0761c9ea4514 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 26 May 2025 17:16:12 +0100 Subject: [PATCH 3724/3836] typing: Annotate openstack.config.openstackcloud The last module in this package. Change-Id: Ia01a1a75193a54ee9156a97bb41d0c24a5d4e764 Signed-off-by: Stephen Finucane --- openstack/cloud/openstackcloud.py | 93 +++++++++++++++++++++++-------- pyproject.toml | 8 +-- 2 files changed, 72 insertions(+), 29 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 960699d2d..57256e0d0 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -22,6 +22,7 @@ import dogpile.cache import keystoneauth1.exceptions +from keystoneauth1.identity import base as ks_plugin_base import requests.models import requestsexceptions import typing_extensions as ty_ext @@ -39,6 +40,8 @@ from openstack import warnings as os_warnings if ty.TYPE_CHECKING: + from dogpile.cache import region as cache_region + from keystoneauth1.access import service_catalog as ks_service_catalog from keystoneauth1 import session as ks_session from oslo_config import cfg @@ -89,7 +92,7 @@ def __init__( strict_proxies: bool = False, pool_executor: concurrent.futures.Executor | None = None, **kwargs: ty.Any, - ): + ) -> None: """Create a connection to a cloud. A connection needs information about how to connect, how to @@ -270,13 +273,13 @@ def __init__( atexit.register(self.close) @property - def session(self): + def session(self) -> 'ks_session.Session': if not self._session: self._session = self.config.get_session() # Hide a reference to the connection on the session to help with # backwards compatibility for folks trying to just pass # conn.session to a Resource method's session argument. - self.session._sdk_connection = weakref.proxy(self) + setattr(self.session, '_sdk_connection', weakref.proxy(self)) return self._session @property @@ -308,7 +311,7 @@ def __exit__( def set_global_request_id(self, global_request_id: str) -> None: self._global_request_id = global_request_id - def global_request(self, global_request_id): + def global_request(self, global_request_id: str) -> ty_ext.Self: """Make a new Connection object with a global request id set. Take the existing settings from the current Connection and construct a @@ -358,14 +361,21 @@ def global_request(self, global_request_id): new_conn.set_global_request_id(global_request_id) return new_conn - def _make_cache(self, cache_class, expiration_time, arguments): + def _make_cache( + self, + cache_class: str, + expiration_time: int, + arguments: dict[str, ty.Any] | None, + ) -> 'cache_region.CacheRegion': return dogpile.cache.make_region( function_key_generator=self._make_cache_key ).configure( cache_class, expiration_time=expiration_time, arguments=arguments ) - def _make_cache_key(self, namespace, fn): + def _make_cache_key( + self, namespace: str, fn: ty.Callable[..., ty.Any] + ) -> ty.Callable[..., str]: fname = fn.__name__ if namespace is None: name_key = self.name @@ -384,7 +394,7 @@ def generate_key(*args, **kwargs): return generate_key - def pprint(self, resource): + def pprint(self, resource: object) -> None: """Wrapper around pprint that groks munch objects""" # import late since this is a utility function import pprint @@ -392,7 +402,7 @@ def pprint(self, resource): new_resource = _utils._dictify_resource(resource) pprint.pprint(new_resource) - def pformat(self, resource): + def pformat(self, resource: object) -> str: """Wrapper around pformat that groks munch objects""" # import late since this is a utility function import pprint @@ -401,26 +411,50 @@ def pformat(self, resource): return pprint.pformat(new_resource) @property - def _keystone_catalog(self): + def _keystone_catalog(self) -> 'ks_service_catalog.ServiceCatalog': + if self.session.auth is None: + raise exceptions.ConfigException( + 'session has no auth information attached' + ) + + if not isinstance( + self.session.auth, ks_plugin_base.BaseIdentityPlugin + ): + raise exceptions.ConfigException( + 'cannot fetch catalog for non-keystone auth plugin' + ) + return self.session.auth.get_access(self.session).service_catalog @property - def service_catalog(self): + def service_catalog(self) -> list[dict[str, ty.Any]]: return self._keystone_catalog.catalog @property - def auth_token(self): + def auth_token(self) -> str | None: # Keystone's session will reuse a token if it is still valid. # We don't need to track validity here, just get_token() each time. return self.session.get_token() @property - def current_user_id(self): + def current_user_id(self) -> str | None: """Get the id of the currently logged-in user from the token.""" + if self.session.auth is None: + raise exceptions.ConfigException( + 'session has no auth information attached' + ) + + if not isinstance( + self.session.auth, ks_plugin_base.BaseIdentityPlugin + ): + raise exceptions.ConfigException( + 'cannot fetch catalog for non-keystone auth plugin' + ) + return self.session.auth.get_access(self.session).user_id @property - def current_project_id(self): + def current_project_id(self) -> str | None: """Get the current project ID. Returns the project_id of the current token scope. None means that @@ -434,11 +468,11 @@ def current_project_id(self): return self.session.get_project_id() @property - def current_project(self): + def current_project(self) -> utils.Munch: """Return a ``utils.Munch`` describing the current project""" return self._get_project_info() - def _get_project_info(self, project_id=None): + def _get_project_info(self, project_id: str | None = None) -> utils.Munch: project_info = utils.Munch( id=project_id, name=None, @@ -464,11 +498,15 @@ def _get_project_info(self, project_id=None): return project_info @property - def current_location(self): + def current_location(self) -> utils.Munch: """Return a ``utils.Munch`` explaining the current cloud location.""" return self._get_current_location() - def _get_current_location(self, project_id=None, zone=None): + def _get_current_location( + self, + project_id: str | None = None, + zone: str | None = None, + ) -> utils.Munch: return utils.Munch( cloud=self.name, # TODO(efried): This is wrong, but it only seems to be used in a @@ -538,14 +576,21 @@ def _get_and_munchify(self, key, data): data = proxy._json_response(data) return meta.get_and_munchify(key, data) - def get_name(self): + def get_name(self) -> str: return self.name - def get_session_endpoint(self, service_key, **kwargs): - if not kwargs: - kwargs = {} + def get_session_endpoint( + self, + service_key: str, + min_version: str | None = None, + max_version: str | None = None, + ) -> str | None: try: - return self.config.get_session_endpoint(service_key, **kwargs) + return self.config.get_session_endpoint( + service_key, + min_version=min_version, + max_version=max_version, + ) except keystoneauth1.exceptions.catalog.EndpointNotFound as e: self.log.debug( "Endpoint not found in %s cloud: %s", self.name, str(e) @@ -560,7 +605,9 @@ def get_session_endpoint(self, service_key, **kwargs): ) return endpoint - def has_service(self, service_key, version=None): + def has_service( + self, service_key: str, version: str | None = None + ) -> bool: if not self.config.has_service(service_key): # TODO(mordred) add a stamp here so that we only report this once if not ( diff --git a/pyproject.toml b/pyproject.toml index 7faf07ff8..2284ac9ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,12 +33,8 @@ module = [ "openstack._log", "openstack.common", "openstack.common.*", - "openstack.config._util", - "openstack.config.defaults", - "openstack.config.cloud_config", - "openstack.config.cloud_region", - "openstack.config.loader", - "openstack.config.vendors", + "openstack.config", + "openstack.config.*", "openstack.connection", "openstack.exceptions", "openstack.fields", From 6b6d5b2934abf60387cfd25c99b06db0378a84c2 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 10 Jun 2025 14:23:13 +0100 Subject: [PATCH 3725/3836] docs: Fix typo This is a v2 resource, not a v3 resource. Change-Id: I47d55afaecd35b0d327771705a6f01a7dcb72e90 Signed-off-by: Stephen Finucane --- doc/source/user/resources/block_storage/v2/transfer.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/user/resources/block_storage/v2/transfer.rst b/doc/source/user/resources/block_storage/v2/transfer.rst index e738c7529..b51f76991 100644 --- a/doc/source/user/resources/block_storage/v2/transfer.rst +++ b/doc/source/user/resources/block_storage/v2/transfer.rst @@ -1,7 +1,7 @@ -openstack.block_storage.v3.transfer +openstack.block_storage.v2.transfer =================================== -.. automodule:: openstack.block_storage.v3.transfer +.. automodule:: openstack.block_storage.v2.transfer The Volume Transfer Class ------------------------- @@ -9,5 +9,5 @@ The Volume Transfer Class The ``Volume Transfer`` class inherits from :class:`~openstack.resource.Resource`. -.. autoclass:: openstack.block_storage.v3.transfer.Transfer +.. autoclass:: openstack.block_storage.v2.transfer.Transfer :members: From 4b74d73aebd48929ae3a4fc5329b63999568cdb1 Mon Sep 17 00:00:00 2001 From: Vincent Lequertier Date: Mon, 26 May 2025 17:21:57 +0200 Subject: [PATCH 3726/3836] Add backup export command Change-Id: I35830e1e534d609be1ce79dde7da4c8173d48a1e --- openstack/block_storage/v2/_proxy.py | 12 ++++++++++++ openstack/block_storage/v2/backup.py | 11 +++++++++++ openstack/block_storage/v3/_proxy.py | 12 ++++++++++++ openstack/block_storage/v3/backup.py | 11 +++++++++++ openstack/cloud/_block_storage.py | 15 +++++++++++++++ .../tests/unit/block_storage/v3/test_backup.py | 12 ++++++++++++ 6 files changed, 73 insertions(+) diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 8af6d8e84..c7886be8f 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -716,6 +716,18 @@ def get_backup(self, backup): """ return self._get(_backup.Backup, backup) + def export_record(self, backup): + """Get a backup + + :param backup: The value can be the ID of a backup + or a :class:`~openstack.block_storage.v2.backup.Backup` + instance. + + :returns: The backup export record fields + """ + backup = self._get_resource(_backup.Backup, backup) + return backup.export(self) + def find_backup(self, name_or_id, ignore_missing=True, *, details=True): """Find a single backup diff --git a/openstack/block_storage/v2/backup.py b/openstack/block_storage/v2/backup.py index 4a336f593..73f30f249 100644 --- a/openstack/block_storage/v2/backup.py +++ b/openstack/block_storage/v2/backup.py @@ -164,6 +164,17 @@ def _action(self, session, body): exceptions.raise_from_response(resp) return resp + def export(self, session): + """Export the current backup + + :param session: openstack session + :return: The backup export record fields + """ + url = utils.urljoin(self.base_path, self.id, "export_record") + resp = session.get(url) + exceptions.raise_from_response(resp) + return resp.json() + def restore(self, session, volume_id=None, name=None): """Restore current backup to volume diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index f6689a842..a197f9da6 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -1433,6 +1433,18 @@ def get_backup_metadata(self, backup): backup = self._get_resource(_backup.Backup, backup) return backup.fetch_metadata(self) + def export_record(self, backup): + """Get a backup meatadata to export + + :param backup: The value can be the ID of a backup + or a :class:`~openstack.block_storage.v2.backup.Backup` + instance. + + :returns: The backup export record fields + """ + backup = self._get_resource(_backup.Backup, backup) + return backup.export(self) + def set_backup_metadata(self, backup, **metadata): """Update metadata for a backup diff --git a/openstack/block_storage/v3/backup.py b/openstack/block_storage/v3/backup.py index 26c45b1da..b3db77ba6 100644 --- a/openstack/block_storage/v3/backup.py +++ b/openstack/block_storage/v3/backup.py @@ -187,6 +187,17 @@ def _action(self, session, body, microversion=None): exceptions.raise_from_response(resp) return resp + def export(self, session): + """Export the current backup + + :param session: openstack session + :return: The backup export record fields + """ + url = utils.urljoin(self.base_path, self.id, "export_record") + resp = session.get(url) + exceptions.raise_from_response(resp) + return resp + def restore(self, session, volume_id=None, name=None): """Restore current backup to volume diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index 6d1df090f..0d55761bb 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -582,6 +582,21 @@ def create_volume_backup( return backup + def export_volume_backup(self, backup_id): + """Export a volume backup. + + :param backup_id: the ID of the backup. + + :returns: The backup export record fields + :raises: :class:`~openstack.exceptions.ResourceTimeout` if wait time + exceeded. + :raises: :class:`~openstack.exceptions.SDKException` on operation + error. + """ + payload = {'backup': backup_id} + + return self.block_storage.export_record(**payload) + # TODO(stephenfin): Remove 'filters' in a future major version def get_volume_backup(self, name_or_id, filters=None): """Get a volume backup by name or ID. diff --git a/openstack/tests/unit/block_storage/v3/test_backup.py b/openstack/tests/unit/block_storage/v3/test_backup.py index 1645db341..cff2e1742 100644 --- a/openstack/tests/unit/block_storage/v3/test_backup.py +++ b/openstack/tests/unit/block_storage/v3/test_backup.py @@ -147,6 +147,18 @@ def test_create_incremental(self): params={}, ) + def test_export(self): + sot = backup.Backup(**BACKUP) + + create_response = mock.Mock() + create_response.status_code = 200 + create_response.json.return_value = {} + create_response.headers = {} + self.sess.get.return_value = create_response + url = f'backups/{FAKE_ID}/export_record' + sot.export(self.sess) + self.sess.get.assert_called_with(url) + def test_restore(self): sot = backup.Backup(**BACKUP) From ad9896c12ad85451673c1cb8cafc558e39909e7e Mon Sep 17 00:00:00 2001 From: cid Date: Thu, 27 Mar 2025 09:33:21 +0100 Subject: [PATCH 3727/3836] Add support for inspection rules Related-Change: https://review.opendev.org/c/openstack/ironic/+/939217 Change-Id: I41141fd0aff9a2c8961caf0ef2caadc4a4a45e19 --- doc/source/user/proxies/baremetal.rst | 8 + doc/source/user/resources/baremetal/index.rst | 1 + .../baremetal/v1/inspection_rules.rst | 13 ++ openstack/baremetal/v1/_proxy.py | 104 ++++++++++ openstack/baremetal/v1/inspection_rules.py | 57 ++++++ openstack/tests/functional/baremetal/base.py | 12 ++ .../test_baremetal_inspection_rules.py | 192 ++++++++++++++++++ .../baremetal/v1/test_inspection_rules.py | 90 ++++++++ .../tests/unit/baremetal/v1/test_proxy.py | 47 +++++ .../inspection-rules-86b1c59def73f757.yaml | 6 + 10 files changed, 530 insertions(+) create mode 100644 doc/source/user/resources/baremetal/v1/inspection_rules.rst create mode 100644 openstack/baremetal/v1/inspection_rules.py create mode 100644 openstack/tests/functional/baremetal/test_baremetal_inspection_rules.py create mode 100644 openstack/tests/unit/baremetal/v1/test_inspection_rules.py create mode 100644 releasenotes/notes/inspection-rules-86b1c59def73f757.yaml diff --git a/doc/source/user/proxies/baremetal.rst b/doc/source/user/proxies/baremetal.rst index 4d054dc05..1fc657ad7 100644 --- a/doc/source/user/proxies/baremetal.rst +++ b/doc/source/user/proxies/baremetal.rst @@ -110,6 +110,14 @@ Runbook Operations create_runbook, update_runbook, patch_runbook, delete_runbook +Inspection Rule Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. autoclass:: openstack.baremetal.v1._proxy.Proxy + :noindex: + :members: inspection_rules, get_inspection_rule, + create_inspection_rule, update_inspection_rule, + patch_inspection_rule, delete_inspection_rule + Utilities --------- diff --git a/doc/source/user/resources/baremetal/index.rst b/doc/source/user/resources/baremetal/index.rst index fa198a0ab..4e0a5f196 100644 --- a/doc/source/user/resources/baremetal/index.rst +++ b/doc/source/user/resources/baremetal/index.rst @@ -15,3 +15,4 @@ Baremetal Resources v1/deploy_templates v1/conductor v1/runbooks + v1/inspection_rules diff --git a/doc/source/user/resources/baremetal/v1/inspection_rules.rst b/doc/source/user/resources/baremetal/v1/inspection_rules.rst new file mode 100644 index 000000000..dcfdc6320 --- /dev/null +++ b/doc/source/user/resources/baremetal/v1/inspection_rules.rst @@ -0,0 +1,13 @@ +openstack.baremetal.v1.inspection_rules +======================================= + +.. automodule:: openstack.baremetal.v1.inspection_rules + +The InspectionRule Class +------------------------- + +The ``InspectionRule`` class inherits +from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.baremetal.v1.inspection_rules.InspectionRule + :members: diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index ecccf17cc..98b589102 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -20,6 +20,7 @@ from openstack.baremetal.v1 import conductor as _conductor from openstack.baremetal.v1 import deploy_templates as _deploytemplates from openstack.baremetal.v1 import driver as _driver +from openstack.baremetal.v1 import inspection_rules as _inspectionrules from openstack.baremetal.v1 import node as _node from openstack.baremetal.v1 import port as _port from openstack.baremetal.v1 import port_group as _portgroup @@ -47,6 +48,7 @@ class Proxy(proxy.Proxy): "runbook": _runbooks.Runbook, "volume_connector": _volumeconnector.VolumeConnector, "volume_target": _volumetarget.VolumeTarget, + "inspection_rules": _inspectionrules.InspectionRule, } def _get_with_fields(self, resource_type, value, fields=None): @@ -2026,3 +2028,105 @@ def wait_for_delete( to delete failed to occur in the specified seconds. """ return resource.wait_for_delete(self, res, interval, wait, callback) + + # ========== Inspection Rules ========== + + def inspection_rules(self, details=False, **query): + """Retrieve a generator of inspection rules. + + :param dict query: Optional query parameters to be sent to + restrict the inspection rules to be returned. + + :returns: A generator of InspectionRule instances. + """ + if details: + query['details'] = True + return _inspectionrules.InspectionRule.list(self, **query) + + def create_inspection_rule(self, **attrs): + """Create a new inspection rule from attributes. + + :param dict attrs: Keyword arguments that will be used to create a + :class:`~openstack.baremetal.v1.inspection_rules.InspectionRule`. + + :returns: The results of inspection rule creation. + :rtype: + :class:`~openstack.baremetal.v1.inspection_rules.InspectionRule`. + """ + return self._create(_inspectionrules.InspectionRule, **attrs) + + def get_inspection_rule(self, inspection_rule, fields=None): + """Get a specific inspection rule. + + :param inspection_rule: The ID of an inspection rule + :class:`~openstack.baremetal.v1.inspection_rules.InspectionRule` + instance. + + :param fields: Limit the resource fields to fetch. + + :returns: One + :class:`~openstack.baremetal.v1.inspection_rules.InspectionRule` + :raises: :class:`~openstack.exceptions.NotFoundException` when no + inspection rule matching the ID could be found. + """ + return self._get_with_fields( + _inspectionrules.InspectionRule, inspection_rule, fields=fields + ) + + def update_inspection_rule(self, inspection_rule, **attrs): + """Update an inspection rule. + + :param inspection_rule: Either the ID of an inspection rule + or an instance of + :class:`~openstack.baremetal.v1.inspection_rules.InspectionRule`. + :param dict attrs: The attributes to update on the + inspection rule represented by the ``inspection_rule`` parameter. + + :returns: The updated inspection rule. + :rtype: + :class:`~openstack.baremetal.v1.inspection_rules.InspectionRule` + """ + return self._update( + _inspectionrules.InspectionRule, inspection_rule, **attrs + ) + + def delete_inspection_rule(self, inspection_rule, ignore_missing=True): + """Delete an inspection rule. + + :param inspection_rule: The value can be either the ID of a + inspection_rule or a + :class:`~openstack.baremetal.v1.inspection_rules.InspectionRule` + instance. + :param bool ignore_missing: When set to ``False``, an exception + :class:`~openstack.exceptions.NotFoundException` will be raised + when the inspection rule could not be found. + When set to ``True``, no exception will be raised when + attempting to delete a non-existent inspection rule. + + :returns: The instance of the inspection rule which was deleted. + :rtype: + :class:`~openstack.baremetal.v1.inspection_rules.InspectionRule`. + """ + return self._delete( + _inspectionrules.InspectionRule, + inspection_rule, + ignore_missing=ignore_missing, + ) + + def patch_inspection_rule(self, inspection_rule, patch): + """Apply a JSON patch to the inspection rule. + + :param inspection_rule: The value can be the ID of a + inspection_rule or a + :class:`~openstack.baremetal.v1.inspection_rules.InspectionRule` + instance. + + :param patch: JSON patch to apply. + + :returns: The updated inspection rule. + :rtype: + :class:`~openstack.baremetal.v1.inspection_rules.InspectionRule` + """ + return self._get_resource( + _inspectionrules.InspectionRule, inspection_rule + ).patch(self, patch) diff --git a/openstack/baremetal/v1/inspection_rules.py b/openstack/baremetal/v1/inspection_rules.py new file mode 100644 index 000000000..10c509e3c --- /dev/null +++ b/openstack/baremetal/v1/inspection_rules.py @@ -0,0 +1,57 @@ +# 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.baremetal.v1 import _common +from openstack import resource + + +class InspectionRule(_common.Resource): + resources_key = 'inspection_rules' + base_path = '/inspection_rules' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + allow_patch = True + commit_method = 'PATCH' + commit_jsonpatch = True + + _query_mapping = resource.QueryParameters( + 'detail', + fields={'type': _common.fields_type}, + ) + + # Inspection rules is available since 1.96 + _max_microversion = '1.96' + #: The actions to be executed when the rule conditions are met. + actions = resource.Body('actions', type=list) + #: A brief explanation about the inspection rule. + description = resource.Body('description') + #: The conditions under which the rule should be triggered. + conditions = resource.Body('conditions', type=list) + #: Timestamp at which the resource was created. + created_at = resource.Body('created_at') + #: A list of relative links. Includes the self and bookmark links. + links = resource.Body('links', type=list) + #: Specifies the phase when the rule should run, defaults to 'main'. + phase = resource.Body('phase') + #: Specifies the rule's precedence level during execution. + priority = resource.Body('priority') + #: Indicates whether the rule contains sensitive information. + sensitive = resource.Body('sensitive', type=bool) + #: Timestamp at which the resource was last updated. + updated_at = resource.Body('updated_at') + #: The UUID of the resource. + id = resource.Body('uuid', alternate_id=True) diff --git a/openstack/tests/functional/baremetal/base.py b/openstack/tests/functional/baremetal/base.py index 11a0cbadd..4fb49446d 100644 --- a/openstack/tests/functional/baremetal/base.py +++ b/openstack/tests/functional/baremetal/base.py @@ -132,3 +132,15 @@ def create_runbook(self, **kwargs): ) ) return runbook + + def create_inspection_rule(self, **kwargs): + """Create a new inspection_rule from attributes.""" + + inspection_rule = self.conn.baremetal.create_inspection_rule(**kwargs) + + self.addCleanup( + lambda: self.conn.baremetal.delete_inspection_rule( + inspection_rule.id, ignore_missing=True + ) + ) + return inspection_rule diff --git a/openstack/tests/functional/baremetal/test_baremetal_inspection_rules.py b/openstack/tests/functional/baremetal/test_baremetal_inspection_rules.py new file mode 100644 index 000000000..6424048ca --- /dev/null +++ b/openstack/tests/functional/baremetal/test_baremetal_inspection_rules.py @@ -0,0 +1,192 @@ +# 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.tests.functional.baremetal import base + + +class TestBareMetalInspectionRule(base.BaseBaremetalTest): + min_microversion = '1.96' + + def setUp(self): + super().setUp() + + def test_baremetal_inspection_rule_create_get_delete(self): + actions = [{"op": "set-attribute", "args": ["/driver", "idrac"]}] + conditions = [ + {"op": "eq", "args": ["node:memory_mb", 4096], "multiple": "all"} + ] + inspection_rule = self.create_inspection_rule( + actions=actions, + conditions=conditions, + description="Test inspection rule", + phase="main", + priority=100, + sensitive=False, + ) + loaded = self.conn.baremetal.get_inspection_rule(inspection_rule.id) + self.assertEqual(loaded.id, inspection_rule.id) + self.conn.baremetal.delete_inspection_rule( + inspection_rule, ignore_missing=False + ) + self.assertRaises( + exceptions.NotFoundException, + self.conn.baremetal.get_inspection_rule, + inspection_rule.id, + ) + + def test_baremetal_inspection_rule_list(self): + actions = [{"op": "set-attribute", "args": ["/driver", "idrac"]}] + conditions = [ + { + "op": "is-true", + "args": ["{node.auto_discovered}"], + "multiple": "any", + } + ] + + inspection_rule1 = self.create_inspection_rule( + actions=actions, + conditions=conditions, + description="Test inspection rule 1", + ) + inspection_rule2 = self.create_inspection_rule( + actions=actions, + conditions=conditions, + description="Test inspection rule 2", + ) + inspection_rules = self.conn.baremetal.inspection_rules() + ids = [rule.id for rule in inspection_rules] + self.assertIn(inspection_rule1.id, ids) + self.assertIn(inspection_rule2.id, ids) + + inspection_rules_with_details = self.conn.baremetal.inspection_rules( + details=True + ) + for rule in inspection_rules_with_details: + self.assertIsNotNone(rule.id) + self.assertIsNotNone(rule.description) + + inspection_rule_with_fields = self.conn.baremetal.inspection_rules( + fields=['uuid'] + ) + for rule in inspection_rule_with_fields: + self.assertIsNotNone(rule.id) + self.assertIsNone(rule.description) + + def test_baremetal_inspection_rule_list_update_delete(self): + actions = [{"op": "set-attribute", "args": ["/driver", "idrac"]}] + conditions = [ + { + "op": "eq", + "args": ["node:cpu_arch", "x86_64"], + "multiple": "all", + } + ] + inspection_rule = self.create_inspection_rule( + actions=actions, + conditions=conditions, + description="Test inspection rule", + ) + self.assertFalse(inspection_rule.extra) + inspection_rule.description = 'Updated inspection rule' + + inspection_rule = self.conn.baremetal.update_inspection_rule( + inspection_rule + ) + self.assertEqual( + 'Updated inspection rule', inspection_rule.description + ) + + inspection_rule = self.conn.baremetal.get_inspection_rule( + inspection_rule.id + ) + + self.conn.baremetal.delete_inspection_rule( + inspection_rule.id, ignore_missing=False + ) + + def test_baremetal_inspection_rule_update(self): + actions = [{"op": "set-attribute", "args": ["/driver", "idrac"]}] + conditions = [ + {"op": "ge", "args": ["node:memory_mb", 4096], "multiple": "all"} + ] + inspection_rule = self.create_inspection_rule( + actions=actions, conditions=conditions, phase="main", priority=100 + ) + inspection_rule.priority = 150 + + inspection_rule = self.conn.baremetal.update_inspection_rule( + inspection_rule + ) + self.assertEqual(150, inspection_rule.priority) + + inspection_rule = self.conn.baremetal.get_inspection_rule( + inspection_rule.id + ) + self.assertEqual(150, inspection_rule.priority) + + def test_inspection_rule_patch(self): + description = "BIOS configuration rule" + actions = [ + { + "op": "set-attribute", + "args": ["/properties/capabilities", "boot_mode:uefi"], + } + ] + conditions = [ + { + "op": "is-true", + "args": ["{node.auto_discovered}"], + "multiple": "any", + } + ] + inspection_rule = self.create_inspection_rule( + actions=actions, + conditions=conditions, + description=description, + sensitive=False, + ) + + updated_actions = [ + { + "op": "set-attribute", + "args": ["/driver", "fake"], + } + ] + + inspection_rule = self.conn.baremetal.patch_inspection_rule( + inspection_rule, + dict(path='/actions', op='add', value=updated_actions), + ) + self.assertEqual(updated_actions, inspection_rule.actions) + self.assertEqual(description, inspection_rule.description) + + inspection_rule = self.conn.baremetal.get_inspection_rule( + inspection_rule.id + ) + self.assertEqual(updated_actions, inspection_rule.actions) + + def test_inspection_rule_negative_non_existing(self): + uuid = "bbb45f41-d4bc-4307-8d1d-32f95ce1e920" + self.assertRaises( + exceptions.NotFoundException, + self.conn.baremetal.get_inspection_rule, + uuid, + ) + self.assertRaises( + exceptions.NotFoundException, + self.conn.baremetal.delete_inspection_rule, + uuid, + ignore_missing=False, + ) + self.assertIsNone(self.conn.baremetal.delete_inspection_rule(uuid)) diff --git a/openstack/tests/unit/baremetal/v1/test_inspection_rules.py b/openstack/tests/unit/baremetal/v1/test_inspection_rules.py new file mode 100644 index 000000000..446dba659 --- /dev/null +++ b/openstack/tests/unit/baremetal/v1/test_inspection_rules.py @@ -0,0 +1,90 @@ +# 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.baremetal.v1 import inspection_rules +from openstack.tests.unit import base + + +FAKE = { + "created_at": "2025-03-18T22:28:48.643434+11:11", + "description": "BMC credentials", + "phase": "main", + "priority": 100, + "sensitive": False, + "actions": [ + { + "op": "set-attribute", + "args": { + "path": "/properties/cpus", + "value": "{inventory[cpu][count]}", + }, + }, + { + "op": "set-attribute", + "args": { + "path": "/properties/memory_mb", + "value": "{inventory[memory][physical_mb]}", + }, + }, + { + "op": "set-attribute", + "args": { + "path": "/properties/cpu_arch", + "value": "{inventory[cpu][architecture]}", + }, + }, + ], + "conditions": [ + {"op": "is-true", "args": {"value": "{inventory[cpu][count]}"}} + ], + "links": [ + { + "href": "http://10.60.253.180:6385/v1/inspection_rules" + "/783bf33a-a8e3-1e23-a645-1e95a1f95186", + "rel": "self", + }, + { + "href": "http://10.60.253.180:6385/inspection_rules" + "/783bf33a-a8e3-1e23-a645-1e95a1f95186", + "rel": "bookmark", + }, + ], + "updated_at": None, + "uuid": "783bf33a-a8e3-1e23-a645-1e95a1f95186", +} + + +class InspectionRules(base.TestCase): + def test_basic(self): + sot = inspection_rules.InspectionRule() + self.assertIsNone(sot.resource_key) + self.assertEqual('inspection_rules', sot.resources_key) + self.assertEqual('/inspection_rules', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertEqual('PATCH', sot.commit_method) + + def test_instantiate(self): + sot = inspection_rules.InspectionRule(**FAKE) + self.assertEqual(FAKE['actions'], sot.actions) + self.assertEqual(FAKE['description'], sot.description) + self.assertEqual(FAKE['conditions'], sot.conditions) + self.assertEqual(FAKE['created_at'], sot.created_at) + self.assertEqual(FAKE['links'], sot.links) + self.assertEqual(FAKE['phase'], sot.phase) + self.assertEqual(FAKE['priority'], sot.priority) + self.assertEqual(FAKE['sensitive'], sot.sensitive) + self.assertEqual(FAKE['updated_at'], sot.updated_at) + self.assertEqual(FAKE['uuid'], sot.id) diff --git a/openstack/tests/unit/baremetal/v1/test_proxy.py b/openstack/tests/unit/baremetal/v1/test_proxy.py index bd330d1af..0afad7ba7 100644 --- a/openstack/tests/unit/baremetal/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal/v1/test_proxy.py @@ -17,6 +17,7 @@ from openstack.baremetal.v1 import chassis from openstack.baremetal.v1 import deploy_templates from openstack.baremetal.v1 import driver +from openstack.baremetal.v1 import inspection_rules from openstack.baremetal.v1 import node from openstack.baremetal.v1 import port from openstack.baremetal.v1 import port_group @@ -510,3 +511,49 @@ def _fake_get(_self, node): self.assertEqual(['1'], [x.id for x in result.success]) self.assertEqual(['3'], [x.id for x in result.timeout]) self.assertEqual(['2'], [x.id for x in result.failure]) + + +class TestInspectionRules(TestBaremetalProxy): + @mock.patch.object(inspection_rules.InspectionRule, 'list') + def test_inspection_rules_detailed(self, mock_list): + result = self.proxy.inspection_rules(details=True, query=1) + self.assertIs(result, mock_list.return_value) + mock_list.assert_called_once_with(self.proxy, details=True, query=1) + + @mock.patch.object(inspection_rules.InspectionRule, 'list') + def test_inspection_rules_not_detailed(self, mock_list): + result = self.proxy.inspection_rules(query=1) + self.assertIs(result, mock_list.return_value) + mock_list.assert_called_once_with(self.proxy, query=1) + + def test_create_inspection_rule(self): + self.verify_create( + self.proxy.create_inspection_rule, inspection_rules.InspectionRule + ) + + def test_get_inspection_rule(self): + self.verify_get( + self.proxy.get_inspection_rule, + inspection_rules.InspectionRule, + mock_method=_MOCK_METHOD, + expected_kwargs={'fields': None}, + ) + + def test_update_inspection_rule(self): + self.verify_update( + self.proxy.update_inspection_rule, inspection_rules.InspectionRule + ) + + def test_delete_inspection_rule(self): + self.verify_delete( + self.proxy.delete_inspection_rule, + inspection_rules.InspectionRule, + False, + ) + + def test_delete_inspection_rule_ignore(self): + self.verify_delete( + self.proxy.delete_inspection_rule, + inspection_rules.InspectionRule, + True, + ) diff --git a/releasenotes/notes/inspection-rules-86b1c59def73f757.yaml b/releasenotes/notes/inspection-rules-86b1c59def73f757.yaml new file mode 100644 index 000000000..1712e5863 --- /dev/null +++ b/releasenotes/notes/inspection-rules-86b1c59def73f757.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Adds support for inspection rules; an API feature to create a resource + containing conditions that evaluate against inspection data and actions + that run on a node when conditions are met during inspection. From 4f9946ffee1e0f973cfe06f040c03a41eecdb720 Mon Sep 17 00:00:00 2001 From: Vincent Lequertier Date: Wed, 11 Jun 2025 17:09:04 +0200 Subject: [PATCH 3728/3836] Bump python version from 3.9 to 3.13 in testing docs After commit e0f57ad8 the required Python version is >=3.13 Change-Id: I175dd105cf937577d56a289381fe20486c2fa2a2 --- doc/source/contributor/testing.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/source/contributor/testing.rst b/doc/source/contributor/testing.rst index 275a418b2..91041c7d1 100644 --- a/doc/source/contributor/testing.rst +++ b/doc/source/contributor/testing.rst @@ -31,12 +31,12 @@ During development, it may be more convenient to run a subset of the tests to keep test time to a minimum. You can choose to run the tests only on one version. A step further is to run only the tests you are working on.:: - # Run run the tests on Python 3.9 - $ tox -e py39 - # Run only the compute unit tests on Python 3.9 - $ tox -e py39 openstack.tests.unit.compute - # Run only the tests in a specific file on Python 3.9 - $ tox -e py39 -- -n openstack/tests/unit/compute/test_version.py + # Run run the tests on Python 3.13 + $ tox -e py313 + # Run only the compute unit tests on Python 3.13 + $ tox -e py313 openstack.tests.unit.compute + # Run only the tests in a specific file on Python 3.13 + $ tox -e py313 -- -n openstack/tests/unit/compute/test_version.py Functional Tests From 07f52229bd1897de3c59367fd39b8f010fe52e61 Mon Sep 17 00:00:00 2001 From: Arnaud Morin Date: Mon, 23 Jun 2025 11:23:16 +0200 Subject: [PATCH 3729/3836] Clean small nit comment Change-Id: Ic9a0d7798d2b74564aee0ad531a7f95a9f348881 Signed-off-by: Arnaud Morin --- openstack/config/loader.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 3f8871d4b..ce57746be 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -342,7 +342,6 @@ def __init__( # If cache class is given, use that. If not, but if cache time # is given, default to memory. Otherwise, default to nothing. - # to memory. if self._cache_expiration_time: self._cache_class = 'dogpile.cache.memory' self._cache_class = self.cloud_config['cache'].get( From 198cb9e024030918b88e8c61e6d890070c343195 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 30 May 2025 12:38:11 +0100 Subject: [PATCH 3730/3836] zuul: Make openstacksdk-functional-devstack-ironic voting Some actions require a system scoped token. We also need to fix some outdated references that have crept in while this job has been broken before returning the job to the voting list. Change-Id: Ib6fa256d2ecb46e0966a7b3e533e1e996bc1c164 Depends-on: https://review.opendev.org/c/openstack/devstack/+/942840 --- openstack/baremetal/v1/inspection_rules.py | 2 +- openstack/tests/functional/baremetal/base.py | 20 +++-- .../baremetal/test_baremetal_chassis.py | 28 ++++--- .../baremetal/test_baremetal_conductor.py | 4 +- .../test_baremetal_deploy_templates.py | 59 +++++++------ .../baremetal/test_baremetal_driver.py | 10 +-- .../test_baremetal_inspection_rules.py | 70 ++++++++++------ .../baremetal/test_baremetal_node.py | 14 ++-- .../baremetal/test_baremetal_runbooks.py | 84 +++++++++++-------- openstack/tests/unit/base.py | 2 +- zuul.d/project.yaml | 4 +- 11 files changed, 171 insertions(+), 126 deletions(-) diff --git a/openstack/baremetal/v1/inspection_rules.py b/openstack/baremetal/v1/inspection_rules.py index 10c509e3c..10460933f 100644 --- a/openstack/baremetal/v1/inspection_rules.py +++ b/openstack/baremetal/v1/inspection_rules.py @@ -14,7 +14,7 @@ from openstack import resource -class InspectionRule(_common.Resource): +class InspectionRule(resource.Resource): resources_key = 'inspection_rules' base_path = '/inspection_rules' diff --git a/openstack/tests/functional/baremetal/base.py b/openstack/tests/functional/baremetal/base.py index 4fb49446d..9094c4055 100644 --- a/openstack/tests/functional/baremetal/base.py +++ b/openstack/tests/functional/baremetal/base.py @@ -34,9 +34,9 @@ def create_allocation(self, **kwargs): return allocation def create_chassis(self, **kwargs): - chassis = self.operator_cloud.baremetal.create_chassis(**kwargs) + chassis = self.system_admin_cloud.baremetal.create_chassis(**kwargs) self.addCleanup( - lambda: self.operator_cloud.baremetal.delete_chassis( + lambda: self.system_admin_cloud.baremetal.delete_chassis( chassis.id, ignore_missing=True ) ) @@ -110,12 +110,12 @@ def create_volume_target(self, node_id=None, **kwargs): def create_deploy_template(self, **kwargs): """Create a new deploy_template from attributes.""" - deploy_template = self.operator_cloud.baremetal.create_deploy_template( - **kwargs + deploy_template = ( + self.system_admin_cloud.baremetal.create_deploy_template(**kwargs) ) self.addCleanup( - lambda: self.operator_cloud.baremetal.delete_deploy_template( + lambda: self.system_admin_cloud.baremetal.delete_deploy_template( deploy_template.id, ignore_missing=True ) ) @@ -124,10 +124,10 @@ def create_deploy_template(self, **kwargs): def create_runbook(self, **kwargs): """Create a new runbook from attributes.""" - runbook = self.conn.baremetal.create_runbook(**kwargs) + runbook = self.operator_cloud.baremetal.create_runbook(**kwargs) self.addCleanup( - lambda: self.conn.baremetal.delete_runbook( + lambda: self.operator_cloud.baremetal.delete_runbook( runbook.id, ignore_missing=True ) ) @@ -136,10 +136,12 @@ def create_runbook(self, **kwargs): def create_inspection_rule(self, **kwargs): """Create a new inspection_rule from attributes.""" - inspection_rule = self.conn.baremetal.create_inspection_rule(**kwargs) + inspection_rule = ( + self.system_admin_cloud.baremetal.create_inspection_rule(**kwargs) + ) self.addCleanup( - lambda: self.conn.baremetal.delete_inspection_rule( + lambda: self.system_admin_cloud.baremetal.delete_inspection_rule( inspection_rule.id, ignore_missing=True ) ) diff --git a/openstack/tests/functional/baremetal/test_baremetal_chassis.py b/openstack/tests/functional/baremetal/test_baremetal_chassis.py index c6e638bad..04a7a72a7 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_chassis.py +++ b/openstack/tests/functional/baremetal/test_baremetal_chassis.py @@ -19,15 +19,15 @@ class TestBareMetalChassis(base.BaseBaremetalTest): def test_chassis_create_get_delete(self): chassis = self.create_chassis() - loaded = self.operator_cloud.baremetal.get_chassis(chassis.id) + loaded = self.system_admin_cloud.baremetal.get_chassis(chassis.id) self.assertEqual(loaded.id, chassis.id) - self.operator_cloud.baremetal.delete_chassis( + self.system_admin_cloud.baremetal.delete_chassis( chassis, ignore_missing=False ) self.assertRaises( exceptions.NotFoundException, - self.operator_cloud.baremetal.get_chassis, + self.system_admin_cloud.baremetal.get_chassis, chassis.id, ) @@ -35,44 +35,46 @@ def test_chassis_update(self): chassis = self.create_chassis() chassis.extra = {'answer': 42} - chassis = self.operator_cloud.baremetal.update_chassis(chassis) + chassis = self.system_admin_cloud.baremetal.update_chassis(chassis) self.assertEqual({'answer': 42}, chassis.extra) - chassis = self.operator_cloud.baremetal.get_chassis(chassis.id) + chassis = self.system_admin_cloud.baremetal.get_chassis(chassis.id) self.assertEqual({'answer': 42}, chassis.extra) def test_chassis_patch(self): chassis = self.create_chassis() - chassis = self.operator_cloud.baremetal.patch_chassis( + chassis = self.system_admin_cloud.baremetal.patch_chassis( chassis, dict(path='/extra/answer', op='add', value=42) ) self.assertEqual({'answer': 42}, chassis.extra) - chassis = self.operator_cloud.baremetal.get_chassis(chassis.id) + chassis = self.system_admin_cloud.baremetal.get_chassis(chassis.id) self.assertEqual({'answer': 42}, chassis.extra) def test_chassis_negative_non_existing(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" self.assertRaises( exceptions.NotFoundException, - self.operator_cloud.baremetal.get_chassis, + self.system_admin_cloud.baremetal.get_chassis, uuid, ) self.assertRaises( exceptions.NotFoundException, - self.operator_cloud.baremetal.find_chassis, + self.system_admin_cloud.baremetal.find_chassis, uuid, ignore_missing=False, ) self.assertRaises( exceptions.NotFoundException, - self.operator_cloud.baremetal.delete_chassis, + self.system_admin_cloud.baremetal.delete_chassis, uuid, ignore_missing=False, ) - self.assertIsNone(self.operator_cloud.baremetal.find_chassis(uuid)) - self.assertIsNone(self.operator_cloud.baremetal.delete_chassis(uuid)) + self.assertIsNone(self.system_admin_cloud.baremetal.find_chassis(uuid)) + self.assertIsNone( + self.system_admin_cloud.baremetal.delete_chassis(uuid) + ) class TestBareMetalChassisFields(base.BaseBaremetalTest): @@ -80,7 +82,7 @@ class TestBareMetalChassisFields(base.BaseBaremetalTest): def test_chassis_fields(self): self.create_chassis(description='something') - result = self.operator_cloud.baremetal.chassis( + result = self.system_admin_cloud.baremetal.chassis( fields=['uuid', 'extra'] ) for ch in result: diff --git a/openstack/tests/functional/baremetal/test_baremetal_conductor.py b/openstack/tests/functional/baremetal/test_baremetal_conductor.py index e03d530e2..03aea6949 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_conductor.py +++ b/openstack/tests/functional/baremetal/test_baremetal_conductor.py @@ -19,10 +19,10 @@ class TestBareMetalConductor(base.BaseBaremetalTest): def test_list_get_conductor(self): node = self.create_node(name='node-name') - conductors = self.operator_cloud.baremetal.conductors() + conductors = self.system_admin_cloud.baremetal.conductors() hostname_list = [conductor.hostname for conductor in conductors] self.assertIn(node.conductor, hostname_list) - conductor1 = self.operator_cloud.baremetal.get_conductor( + conductor1 = self.system_admin_cloud.baremetal.get_conductor( node.conductor ) self.assertIsNotNone(conductor1.conductor_group) diff --git a/openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py b/openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py index 5630578cb..c2b9e209b 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py +++ b/openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py @@ -17,9 +17,6 @@ class TestBareMetalDeployTemplate(base.BaseBaremetalTest): min_microversion = '1.55' - def setUp(self): - super().setUp() - def test_baremetal_deploy_create_get_delete(self): steps = [ { @@ -34,16 +31,16 @@ def test_baremetal_deploy_create_get_delete(self): deploy_template = self.create_deploy_template( name='CUSTOM_DEPLOY_TEMPLATE', steps=steps ) - loaded = self.operator_cloud.baremetal.get_deploy_template( + loaded = self.system_admin_cloud.baremetal.get_deploy_template( deploy_template.id ) self.assertEqual(loaded.id, deploy_template.id) - self.operator_cloud.baremetal.delete_deploy_template( + self.system_admin_cloud.baremetal.delete_deploy_template( deploy_template, ignore_missing=False ) self.assertRaises( exceptions.NotFoundException, - self.operator_cloud.baremetal.get_deploy_template, + self.system_admin_cloud.baremetal.get_deploy_template, deploy_template.id, ) @@ -65,20 +62,20 @@ def test_baremetal_deploy_template_list(self): deploy_template2 = self.create_deploy_template( name='CUSTOM_DEPLOY_TEMPLATE2', steps=steps ) - deploy_templates = self.operator_cloud.baremetal.deploy_templates() + deploy_templates = self.system_admin_cloud.baremetal.deploy_templates() ids = [template.id for template in deploy_templates] self.assertIn(deploy_template1.id, ids) self.assertIn(deploy_template2.id, ids) deploy_templates_with_details = ( - self.operator_cloud.baremetal.deploy_templates(details=True) + self.system_admin_cloud.baremetal.deploy_templates(details=True) ) for dp in deploy_templates_with_details: self.assertIsNotNone(dp.id) self.assertIsNotNone(dp.name) deploy_tempalte_with_fields = ( - self.operator_cloud.baremetal.deploy_templates(fields=['uuid']) + self.system_admin_cloud.baremetal.deploy_templates(fields=['uuid']) ) for dp in deploy_tempalte_with_fields: self.assertIsNotNone(dp.id) @@ -101,16 +98,20 @@ def test_baremetal_deploy_list_update_delete(self): self.assertFalse(deploy_template.extra) deploy_template.extra = {'answer': 42} - deploy_template = self.operator_cloud.baremetal.update_deploy_template( - deploy_template + deploy_template = ( + self.system_admin_cloud.baremetal.update_deploy_template( + deploy_template + ) ) self.assertEqual({'answer': 42}, deploy_template.extra) - deploy_template = self.operator_cloud.baremetal.get_deploy_template( - deploy_template.id + deploy_template = ( + self.system_admin_cloud.baremetal.get_deploy_template( + deploy_template.id + ) ) - self.operator_cloud.baremetal.delete_deploy_template( + self.system_admin_cloud.baremetal.delete_deploy_template( deploy_template.id, ignore_missing=False ) @@ -130,13 +131,17 @@ def test_baremetal_deploy_update(self): ) deploy_template.extra = {'answer': 42} - deploy_template = self.operator_cloud.baremetal.update_deploy_template( - deploy_template + deploy_template = ( + self.system_admin_cloud.baremetal.update_deploy_template( + deploy_template + ) ) self.assertEqual({'answer': 42}, deploy_template.extra) - deploy_template = self.operator_cloud.baremetal.get_deploy_template( - deploy_template.id + deploy_template = ( + self.system_admin_cloud.baremetal.get_deploy_template( + deploy_template.id + ) ) self.assertEqual({'answer': 42}, deploy_template.extra) @@ -153,14 +158,18 @@ def test_deploy_template_patch(self): } ] deploy_template = self.create_deploy_template(name=name, steps=steps) - deploy_template = self.operator_cloud.baremetal.patch_deploy_template( - deploy_template, dict(path='/extra/answer', op='add', value=42) + deploy_template = ( + self.system_admin_cloud.baremetal.patch_deploy_template( + deploy_template, dict(path='/extra/answer', op='add', value=42) + ) ) self.assertEqual({'answer': 42}, deploy_template.extra) self.assertEqual(name, deploy_template.name) - deploy_template = self.operator_cloud.baremetal.get_deploy_template( - deploy_template.id + deploy_template = ( + self.system_admin_cloud.baremetal.get_deploy_template( + deploy_template.id + ) ) self.assertEqual({'answer': 42}, deploy_template.extra) @@ -168,15 +177,15 @@ def test_deploy_template_negative_non_existing(self): uuid = "bbb45f41-d4bc-4307-8d1d-32f95ce1e920" self.assertRaises( exceptions.NotFoundException, - self.operator_cloud.baremetal.get_deploy_template, + self.system_admin_cloud.baremetal.get_deploy_template, uuid, ) self.assertRaises( exceptions.NotFoundException, - self.operator_cloud.baremetal.delete_deploy_template, + self.system_admin_cloud.baremetal.delete_deploy_template, uuid, ignore_missing=False, ) self.assertIsNone( - self.operator_cloud.baremetal.delete_deploy_template(uuid) + self.system_admin_cloud.baremetal.delete_deploy_template(uuid) ) diff --git a/openstack/tests/functional/baremetal/test_baremetal_driver.py b/openstack/tests/functional/baremetal/test_baremetal_driver.py index 67fa99140..7d53292f5 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_driver.py +++ b/openstack/tests/functional/baremetal/test_baremetal_driver.py @@ -17,18 +17,18 @@ class TestBareMetalDriver(base.BaseBaremetalTest): def test_fake_hardware_get(self): - driver = self.operator_cloud.baremetal.get_driver('fake-hardware') + driver = self.system_admin_cloud.baremetal.get_driver('fake-hardware') self.assertEqual('fake-hardware', driver.name) self.assertNotEqual([], driver.hosts) def test_fake_hardware_list(self): - drivers = self.operator_cloud.baremetal.drivers() + drivers = self.system_admin_cloud.baremetal.drivers() self.assertIn('fake-hardware', [d.name for d in drivers]) def test_driver_negative_non_existing(self): self.assertRaises( exceptions.NotFoundException, - self.operator_cloud.baremetal.get_driver, + self.system_admin_cloud.baremetal.get_driver, 'not-a-driver', ) @@ -37,7 +37,7 @@ class TestBareMetalDriverDetails(base.BaseBaremetalTest): min_microversion = '1.30' def test_fake_hardware_get(self): - driver = self.operator_cloud.baremetal.get_driver('fake-hardware') + driver = self.system_admin_cloud.baremetal.get_driver('fake-hardware') self.assertEqual('fake-hardware', driver.name) for iface in ('boot', 'deploy', 'management', 'power'): self.assertIn( @@ -49,7 +49,7 @@ def test_fake_hardware_get(self): self.assertNotEqual([], driver.hosts) def test_fake_hardware_list_details(self): - drivers = self.operator_cloud.baremetal.drivers(details=True) + drivers = self.system_admin_cloud.baremetal.drivers(details=True) driver = [d for d in drivers if d.name == 'fake-hardware'][0] for iface in ('boot', 'deploy', 'management', 'power'): self.assertIn( diff --git a/openstack/tests/functional/baremetal/test_baremetal_inspection_rules.py b/openstack/tests/functional/baremetal/test_baremetal_inspection_rules.py index 6424048ca..c7c75a597 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_inspection_rules.py +++ b/openstack/tests/functional/baremetal/test_baremetal_inspection_rules.py @@ -33,14 +33,16 @@ def test_baremetal_inspection_rule_create_get_delete(self): priority=100, sensitive=False, ) - loaded = self.conn.baremetal.get_inspection_rule(inspection_rule.id) + loaded = self.system_admin_cloud.baremetal.get_inspection_rule( + inspection_rule.id + ) self.assertEqual(loaded.id, inspection_rule.id) - self.conn.baremetal.delete_inspection_rule( + self.system_admin_cloud.baremetal.delete_inspection_rule( inspection_rule, ignore_missing=False ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.get_inspection_rule, + self.system_admin_cloud.baremetal.get_inspection_rule, inspection_rule.id, ) @@ -64,20 +66,20 @@ def test_baremetal_inspection_rule_list(self): conditions=conditions, description="Test inspection rule 2", ) - inspection_rules = self.conn.baremetal.inspection_rules() + inspection_rules = self.system_admin_cloud.baremetal.inspection_rules() ids = [rule.id for rule in inspection_rules] self.assertIn(inspection_rule1.id, ids) self.assertIn(inspection_rule2.id, ids) - inspection_rules_with_details = self.conn.baremetal.inspection_rules( - details=True + inspection_rules_with_details = ( + self.system_admin_cloud.baremetal.inspection_rules(details=True) ) for rule in inspection_rules_with_details: self.assertIsNotNone(rule.id) self.assertIsNotNone(rule.description) - inspection_rule_with_fields = self.conn.baremetal.inspection_rules( - fields=['uuid'] + inspection_rule_with_fields = ( + self.system_admin_cloud.baremetal.inspection_rules(fields=['uuid']) ) for rule in inspection_rule_with_fields: self.assertIsNotNone(rule.id) @@ -97,41 +99,48 @@ def test_baremetal_inspection_rule_list_update_delete(self): conditions=conditions, description="Test inspection rule", ) - self.assertFalse(inspection_rule.extra) inspection_rule.description = 'Updated inspection rule' - inspection_rule = self.conn.baremetal.update_inspection_rule( - inspection_rule + inspection_rule = ( + self.system_admin_cloud.baremetal.update_inspection_rule( + inspection_rule + ) ) self.assertEqual( 'Updated inspection rule', inspection_rule.description ) - inspection_rule = self.conn.baremetal.get_inspection_rule( - inspection_rule.id + inspection_rule = ( + self.system_admin_cloud.baremetal.get_inspection_rule( + inspection_rule.id + ) ) - self.conn.baremetal.delete_inspection_rule( + self.system_admin_cloud.baremetal.delete_inspection_rule( inspection_rule.id, ignore_missing=False ) def test_baremetal_inspection_rule_update(self): actions = [{"op": "set-attribute", "args": ["/driver", "idrac"]}] conditions = [ - {"op": "ge", "args": ["node:memory_mb", 4096], "multiple": "all"} + {"op": "gt", "args": ["node:memory_mb", 4096], "multiple": "all"} ] inspection_rule = self.create_inspection_rule( actions=actions, conditions=conditions, phase="main", priority=100 ) inspection_rule.priority = 150 - inspection_rule = self.conn.baremetal.update_inspection_rule( - inspection_rule + inspection_rule = ( + self.system_admin_cloud.baremetal.update_inspection_rule( + inspection_rule + ) ) self.assertEqual(150, inspection_rule.priority) - inspection_rule = self.conn.baremetal.get_inspection_rule( - inspection_rule.id + inspection_rule = ( + self.system_admin_cloud.baremetal.get_inspection_rule( + inspection_rule.id + ) ) self.assertEqual(150, inspection_rule.priority) @@ -160,19 +169,24 @@ def test_inspection_rule_patch(self): updated_actions = [ { "op": "set-attribute", + "loop": [], "args": ["/driver", "fake"], } ] - inspection_rule = self.conn.baremetal.patch_inspection_rule( - inspection_rule, - dict(path='/actions', op='add', value=updated_actions), + inspection_rule = ( + self.system_admin_cloud.baremetal.patch_inspection_rule( + inspection_rule, + dict(path='/actions', op='add', value=updated_actions), + ) ) self.assertEqual(updated_actions, inspection_rule.actions) self.assertEqual(description, inspection_rule.description) - inspection_rule = self.conn.baremetal.get_inspection_rule( - inspection_rule.id + inspection_rule = ( + self.system_admin_cloud.baremetal.get_inspection_rule( + inspection_rule.id + ) ) self.assertEqual(updated_actions, inspection_rule.actions) @@ -180,13 +194,15 @@ def test_inspection_rule_negative_non_existing(self): uuid = "bbb45f41-d4bc-4307-8d1d-32f95ce1e920" self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.get_inspection_rule, + self.system_admin_cloud.baremetal.get_inspection_rule, uuid, ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.delete_inspection_rule, + self.system_admin_cloud.baremetal.delete_inspection_rule, uuid, ignore_missing=False, ) - self.assertIsNone(self.conn.baremetal.delete_inspection_rule(uuid)) + self.assertIsNone( + self.system_admin_cloud.baremetal.delete_inspection_rule(uuid) + ) diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index 54fc7c9c9..cddfb922e 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -473,28 +473,28 @@ class TestBareMetalVirtualMedia(base.BaseBaremetalTest): def setUp(self): super().setUp() self.node = self.create_node(network_interface='noop') - self.device_type = "CDROM" - self.image_url = "http://image" + self.device_type = 'cdrom' + self.image_url = 'http://image' def test_node_vmedia_attach_detach(self): - self.conn.baremetal.attach_vmedia_to_node( + self.operator_cloud.baremetal.attach_vmedia_to_node( self.node, self.device_type, self.image_url ) - res = self.conn.baremetal.detach_vmedia_from_node(self.node) - self.assertNone(res) + res = self.operator_cloud.baremetal.detach_vmedia_from_node(self.node) + self.assertIsNone(res) def test_node_vmedia_negative(self): uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971" self.assertRaises( exceptions.ResourceNotFound, - self.conn.baremetal.attach_vmedia_to_node, + self.operator_cloud.baremetal.attach_vmedia_to_node, uuid, self.device_type, self.image_url, ) self.assertRaises( exceptions.ResourceNotFound, - self.conn.baremetal.detach_vmedia_from_node, + self.operator_cloud.baremetal.detach_vmedia_from_node, uuid, ) diff --git a/openstack/tests/functional/baremetal/test_baremetal_runbooks.py b/openstack/tests/functional/baremetal/test_baremetal_runbooks.py index 6fb9737f1..2c3783426 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_runbooks.py +++ b/openstack/tests/functional/baremetal/test_baremetal_runbooks.py @@ -17,9 +17,6 @@ class TestBareMetalRunbook(base.BaseBaremetalTest): min_microversion = '1.92' - def setUp(self): - super().setUp() - def test_baremetal_runbook_create_get_delete(self): steps = [ { @@ -28,16 +25,18 @@ def test_baremetal_runbook_create_get_delete(self): "args": { "settings": [{"name": "LogicalProc", "value": "Enabled"}] }, - "priority": 150, + "order": 150, } ] runbook = self.create_runbook(name='CUSTOM_RUNBOOK', steps=steps) - loaded = self.conn.baremetal.get_runbook(runbook.id) + loaded = self.operator_cloud.baremetal.get_runbook(runbook.id) self.assertEqual(loaded.id, runbook.id) - self.conn.baremetal.delete_runbook(runbook, ignore_missing=False) + self.operator_cloud.baremetal.delete_runbook( + runbook, ignore_missing=False + ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.get_runbook, + self.operator_cloud.baremetal.get_runbook, runbook.id, ) @@ -49,23 +48,27 @@ def test_baremetal_runbook_list(self): "args": { "settings": [{"name": "LogicalProc", "value": "Enabled"}] }, - "priority": 150, + "order": 150, } ] runbook1 = self.create_runbook(name='CUSTOM_RUNBOOK1', steps=steps) runbook2 = self.create_runbook(name='CUSTOM_RUNBOOK2', steps=steps) - runbooks = self.conn.baremetal.runbooks() + runbooks = self.operator_cloud.baremetal.runbooks() ids = [runbook.id for runbook in runbooks] self.assertIn(runbook1.id, ids) self.assertIn(runbook2.id, ids) - runbooks_with_details = self.conn.baremetal.runbooks(details=True) + runbooks_with_details = self.operator_cloud.baremetal.runbooks( + details=True + ) for runbook in runbooks_with_details: self.assertIsNotNone(runbook.id) self.assertIsNotNone(runbook.name) - runbook_with_fields = self.conn.baremetal.runbooks(fields=['uuid']) + runbook_with_fields = self.operator_cloud.baremetal.runbooks( + fields=['uuid'] + ) for runbook in runbook_with_fields: self.assertIsNotNone(runbook.id) self.assertIsNone(runbook.name) @@ -78,19 +81,21 @@ def test_baremetal_runbook_list_update_delete(self): "args": { "settings": [{"name": "LogicalProc", "value": "Enabled"}] }, - "priority": 150, + "order": 150, } ] runbook = self.create_runbook(name='CUSTOM_RUNBOOK4', steps=steps) self.assertFalse(runbook.extra) runbook.extra = {'answer': 42} - runbook = self.conn.baremetal.update_runbook(runbook) + runbook = self.operator_cloud.baremetal.update_runbook(runbook) self.assertEqual({'answer': 42}, runbook.extra) - runbook = self.conn.baremetal.get_runbook(runbook.id) + runbook = self.operator_cloud.baremetal.get_runbook(runbook.id) - self.conn.baremetal.delete_runbook(runbook.id, ignore_missing=False) + self.operator_cloud.baremetal.delete_runbook( + runbook.id, ignore_missing=False + ) def test_baremetal_runbook_update(self): steps = [ @@ -100,16 +105,16 @@ def test_baremetal_runbook_update(self): "args": { "settings": [{"name": "LogicalProc", "value": "Enabled"}] }, - "priority": 150, + "order": 150, } ] runbook = self.create_runbook(name='CUSTOM_RUNBOOK4', steps=steps) runbook.extra = {'answer': 42} - runbook = self.conn.baremetal.update_runbook(runbook) + runbook = self.operator_cloud.baremetal.update_runbook(runbook) self.assertEqual({'answer': 42}, runbook.extra) - runbook = self.conn.baremetal.get_runbook(runbook.id) + runbook = self.operator_cloud.baremetal.get_runbook(runbook.id) self.assertEqual({'answer': 42}, runbook.extra) def test_runbook_patch(self): @@ -121,33 +126,33 @@ def test_runbook_patch(self): "args": { "settings": [{"name": "LogicalProc", "value": "Enabled"}] }, - "priority": 150, + "order": 150, } ] runbook = self.create_runbook(name=name, steps=steps) - runbook = self.conn.baremetal.patch_runbook( + runbook = self.operator_cloud.baremetal.patch_runbook( runbook, dict(path='/extra/answer', op='add', value=42) ) self.assertEqual({'answer': 42}, runbook.extra) self.assertEqual(name, runbook.name) - runbook = self.conn.baremetal.get_runbook(runbook.id) + runbook = self.operator_cloud.baremetal.get_runbook(runbook.id) self.assertEqual({'answer': 42}, runbook.extra) def test_runbook_negative_non_existing(self): uuid = "b4145fbb-d4bc-0d1d-4382-e1e922f9035c" self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.get_runbook, + self.operator_cloud.baremetal.get_runbook, uuid, ) self.assertRaises( exceptions.NotFoundException, - self.conn.baremetal.delete_runbook, + self.operator_cloud.baremetal.delete_runbook, uuid, ignore_missing=False, ) - self.assertIsNone(self.conn.baremetal.delete_runbook(uuid)) + self.assertIsNone(self.operator_cloud.baremetal.delete_runbook(uuid)) def test_runbook_rbac_project_scoped(self): steps = [ @@ -157,20 +162,23 @@ def test_runbook_rbac_project_scoped(self): "args": { "settings": [{"name": "LogicalProc", "value": "Enabled"}] }, - "priority": 150, + "order": 150, } ] - runbook = self.create_runbook( - name='CUSTOM_PROJ_AWESOME', - steps=steps, - owner=self.conn.current_project_id, + runbook = self.operator_cloud.baremetal.create_runbook( + name='CUSTOM_PROJ_AWESOME', steps=steps + ) + self.addCleanup( + lambda: self.operator_cloud.baremetal.delete_runbook( + runbook.id, ignore_missing=True + ) ) self.assertFalse(runbook.public) - self.assertEqual(self.conn.current_project_id, runbook.owner) + self.assertEqual(self.operator_cloud.current_project_id, runbook.owner) # is accessible to the owner - loaded = self.conn.baremetal.get_runbook(runbook.id) + loaded = self.operator_cloud.baremetal.get_runbook(runbook.id) self.assertEqual(loaded.id, runbook.id) def test_runbook_rbac_system_scoped(self): @@ -181,14 +189,22 @@ def test_runbook_rbac_system_scoped(self): "args": { "settings": [{"name": "LogicalProc", "value": "Enabled"}] }, - "priority": 150, + "order": 150, } ] - runbook = self.create_runbook(name='CUSTOM_SYS_AWESOME', steps=steps) + runbook = self.system_admin_cloud.baremetal.create_runbook( + name='CUSTOM_SYS_AWESOME', steps=steps + ) + self.addCleanup( + lambda: self.system_admin_cloud.baremetal.delete_runbook( + runbook.id, ignore_missing=True + ) + ) + self.assertFalse(runbook.public) self.assertIsNone(runbook.owner) # is accessible to system-scoped users - loaded = self.conn.baremetal.get_runbook(runbook.id) + loaded = self.system_admin_cloud.baremetal.get_runbook(runbook.id) self.assertEqual(loaded.id, runbook.id) diff --git a/openstack/tests/unit/base.py b/openstack/tests/unit/base.py index 65ed6762e..41ff6373a 100644 --- a/openstack/tests/unit/base.py +++ b/openstack/tests/unit/base.py @@ -854,7 +854,7 @@ def __do_register_uris(self, uri_mock_list=None): ) def assert_no_calls(self): - # TODO(mordred) For now, creating the adapter for self.conn is + # TODO(mordred) For now, creating the adapter for connections is # triggering catalog lookups. Make sure no_calls is only 2. # When we can make that on-demand through a descriptor object, # drop this to 0. diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index 5d6e9cde4..b0200656b 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -30,8 +30,7 @@ # [1] https://review.opendev.org/c/openstack/masakari/+/949153 - openstacksdk-functional-devstack-masakari: voting: false - - openstacksdk-functional-devstack-ironic: - voting: false + - openstacksdk-functional-devstack-ironic - osc-functional-devstack-tips: voting: false - ansible-collections-openstack-functional-devstack: @@ -42,3 +41,4 @@ - openstacksdk-functional-devstack - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-networking-ext + - openstacksdk-functional-devstack-ironic From da4961cabc7b8e52b579033fc637146940a6bcb5 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 30 May 2025 12:40:30 +0100 Subject: [PATCH 3731/3836] zuul: Make openstacksdk-functional-devstack-masakari voting The relevant fix has now merged. Change-Id: I09324e89346836281d3dffd42c4c07c0aac6b52c Signed-off-by: Stephen Finucane --- zuul.d/project.yaml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index b0200656b..a677dd5e0 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -26,10 +26,7 @@ - openstacksdk-functional-devstack-magnum: voting: false - openstacksdk-functional-devstack-manila - # TODO(stephenfin): We can make this voting again once [1] merges - # [1] https://review.opendev.org/c/openstack/masakari/+/949153 - - openstacksdk-functional-devstack-masakari: - voting: false + - openstacksdk-functional-devstack-masakari - openstacksdk-functional-devstack-ironic - osc-functional-devstack-tips: voting: false From 2eb367c6e01ba1919c7add61fc7129872e3ba8f5 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 26 Jun 2025 18:56:06 +0100 Subject: [PATCH 3732/3836] Migrate setup configuration to pyproject.toml Change-Id: Id1fc500278bbd905516f430d56c8e214e3f48a95 Signed-off-by: Stephen Finucane --- pyproject.toml | 47 +++++++++++++++++++++++++++++++++++++++++++++++ setup.cfg | 28 ---------------------------- 2 files changed, 47 insertions(+), 28 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2284ac9ad..1a4bb3154 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,50 @@ +[build-system] +requires = ["pbr>=6.1.1"] +build-backend = "pbr.build" + +[project] +name = "openstacksdk" +description = "An SDK for building applications to work with OpenStack" +authors = [ + {name = "OpenStack"}, + {email="openstack-discuss@lists.openstack.org"} +] +readme = {file = "README.rst", content-type = "text/x-rst"} +license = {text = "Apache-2.0"} +dynamic = ["version", "dependencies"] +# dependencies = [ ] +requires-python = ">=3.10" +classifiers = [ + "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.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] + +# [project.optional-dependencies] +# test = [ +# ] + +[project.urls] +Homepage = "https://docs.openstack.org/openstacksdk" +Repository = "https://opendev.org/openstack/openstacksdk/" + +[project.scripts] +# TODO(mordred) Move this to an OSC command at some point +openstack-inventory = "openstack.cloud.cmd.inventory:main" + +[tool.setuptools] +packages = [ + "openstack" +] + [tool.mypy] python_version = "3.10" show_column_numbers = true diff --git a/setup.cfg b/setup.cfg index 34410f3df..682722d6e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,30 +1,2 @@ [metadata] name = openstacksdk -summary = An SDK for building applications to work with OpenStack -description_file = - README.rst -author = OpenStack -author_email = openstack-discuss@lists.openstack.org -home_page = https://docs.openstack.org/openstacksdk/ -python_requires = >=3.10 -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.10 - Programming Language :: Python :: 3.11 - Programming Language :: Python :: 3.12 - Programming Language :: Python :: 3.13 - -[files] -packages = - openstack - -# TODO(mordred) Move this to an OSC command before 1.0 -[entry_points] -console_scripts = - openstack-inventory = openstack.cloud.cmd.inventory:main From 83fc95d25b1bd6032c6d6c3f28b7f61442ea7bf7 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 27 Jun 2025 09:04:59 +0100 Subject: [PATCH 3733/3836] Remove duplicate Python version declarations ruff can automatically detect this [1] now that it is defined in pyproject.toml. mypy defaults to the version of Python used to run mypy [2][3] so we need to keep its configuration around a while longer. We also fix the 'project.authors' definition while we're here. [1] https://docs.astral.sh/ruff/settings/#target-version [2] https://mypy.readthedocs.io/en/stable/config_file.html#confval-python_version [3] https://github.com/python/mypy/issues/19349 Change-Id: I1feaa80c2e11bce951ae0ab99c6f1f4b00faadb8 Signed-off-by: Stephen Finucane --- pyproject.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1a4bb3154..131e6051f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,8 +6,7 @@ build-backend = "pbr.build" name = "openstacksdk" description = "An SDK for building applications to work with OpenStack" authors = [ - {name = "OpenStack"}, - {email="openstack-discuss@lists.openstack.org"} + {name = "OpenStack", email = "openstack-discuss@lists.openstack.org"}, ] readme = {file = "README.rst", content-type = "text/x-rst"} license = {text = "Apache-2.0"} @@ -105,7 +104,6 @@ ignore_errors = true [tool.ruff] line-length = 79 -target-version = "py310" [tool.ruff.format] quote-style = "preserve" From 65a97e1a7142785939da610f3026b0f93b5299de Mon Sep 17 00:00:00 2001 From: Eric Harney Date: Wed, 9 Jul 2025 08:14:45 -0400 Subject: [PATCH 3734/3836] Fix create_volume_snapshot() description. s/volume/snapshot/ Change-Id: I306205851ef7d0213beb75f706a7cd6673ae94ca Signed-off-by: Eric Harney --- openstack/cloud/_block_storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index 6d1df090f..79fd35906 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -454,7 +454,7 @@ def create_volume_snapshot( timeout=None, **kwargs, ): - """Create a volume. + """Create a snapshot. :param volume_id: the ID of the volume to snapshot. :param force: If set to True the snapshot will be created even if the From 82bc005071a7b29d3a29bc5c1712588494479d53 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 10 Jul 2025 11:48:46 +0100 Subject: [PATCH 3735/3836] Remove loop variable Ironic have changed an API response without a microversion :( We must also bump the lower-constraint of os-service-types, since we have a test that verifies that we are using the latest and greatest version. Change-Id: I1e7ac6fbf70519190f1e51c0d8651cc5f4a776fc Signed-off-by: Stephen Finucane --- .../functional/baremetal/test_baremetal_inspection_rules.py | 1 - openstack/tests/unit/test_utils.py | 5 ++--- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/openstack/tests/functional/baremetal/test_baremetal_inspection_rules.py b/openstack/tests/functional/baremetal/test_baremetal_inspection_rules.py index c7c75a597..c9d3e14c7 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_inspection_rules.py +++ b/openstack/tests/functional/baremetal/test_baremetal_inspection_rules.py @@ -169,7 +169,6 @@ def test_inspection_rule_patch(self): updated_actions = [ { "op": "set-attribute", - "loop": [], "args": ["/driver", "fake"], } ] diff --git a/openstack/tests/unit/test_utils.py b/openstack/tests/unit/test_utils.py index ee193b0e7..ea726039b 100644 --- a/openstack/tests/unit/test_utils.py +++ b/openstack/tests/unit/test_utils.py @@ -223,13 +223,12 @@ def test_value_less_than_min(self): class TestOsServiceTypesVersion(base.TestCase): def test_ost_version(self): - ost_version = '2019-05-01T19:53:21.498745' + ost_version = '2022-09-13T15:34:32.154125' self.assertEqual( ost_version, os_service_types.ServiceTypes().version, "This project must be pinned to the latest version of " - "os-service-types. Please bump requirements.txt and " - "lower-constraints.txt accordingly.", + "os-service-types. Please bump requirements.txt accordingly.", ) diff --git a/requirements.txt b/requirements.txt index b2a10c619..5a531f3f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ iso8601>=0.1.11 # MIT jmespath>=0.9.0 # MIT jsonpatch!=1.20,>=1.16 # BSD keystoneauth1>=5.10.0 # Apache-2.0 -os-service-types>=1.7.0 # Apache-2.0 +os-service-types>=1.8.0 # Apache-2.0 pbr!=2.1.0,>=2.0.0 # Apache-2.0 platformdirs>=3 # MIT License psutil>=3.2.2 # BSD From 3a774c1c5aab66b9d8535fef2b226cea89e8a70a Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 18 Jul 2025 11:18:35 +0100 Subject: [PATCH 3736/3836] Fix Volume.set_image_metadata These need to be enveloped in a metadata key for reasons. Change-Id: I45337dfa0f7cd63ca202e1d63d74d4fda9096472 Signed-off-by: Stephen Finucane --- openstack/block_storage/v2/volume.py | 2 +- openstack/block_storage/v3/volume.py | 2 +- openstack/tests/unit/block_storage/v2/test_volume.py | 2 +- openstack/tests/unit/block_storage/v3/test_volume.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openstack/block_storage/v2/volume.py b/openstack/block_storage/v2/volume.py index b78616788..240b0880b 100644 --- a/openstack/block_storage/v2/volume.py +++ b/openstack/block_storage/v2/volume.py @@ -125,7 +125,7 @@ def set_readonly(self, session, readonly): def set_image_metadata(self, session, metadata): """Sets image metadata key-value pairs on the volume""" - body = {'os-set_image_metadata': metadata} + body = {'os-set_image_metadata': {'metadata': metadata}} self._action(session, body) def delete_image_metadata(self, session): diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index a38671eb0..2d7d187fd 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -170,7 +170,7 @@ def set_readonly(self, session, readonly): def set_image_metadata(self, session, metadata): """Sets image metadata key-value pairs on the volume""" - body = {'os-set_image_metadata': metadata} + body = {'os-set_image_metadata': {'metadata': metadata}} self._action(session, body) def delete_image_metadata(self, session): diff --git a/openstack/tests/unit/block_storage/v2/test_volume.py b/openstack/tests/unit/block_storage/v2/test_volume.py index 2ac2529b2..2672ebf36 100644 --- a/openstack/tests/unit/block_storage/v2/test_volume.py +++ b/openstack/tests/unit/block_storage/v2/test_volume.py @@ -198,7 +198,7 @@ def test_set_image_metadata(self): self.assertIsNone(sot.set_image_metadata(self.sess, {'foo': 'bar'})) url = f'volumes/{FAKE_ID}/action' - body = {'os-set_image_metadata': {'foo': 'bar'}} + body = {'os-set_image_metadata': {'metadata': {'foo': 'bar'}}} self.sess.post.assert_called_with(url, json=body) def test_delete_image_metadata(self): diff --git a/openstack/tests/unit/block_storage/v3/test_volume.py b/openstack/tests/unit/block_storage/v3/test_volume.py index 5ee01ec73..825e4a49e 100644 --- a/openstack/tests/unit/block_storage/v3/test_volume.py +++ b/openstack/tests/unit/block_storage/v3/test_volume.py @@ -236,7 +236,7 @@ def test_set_image_metadata(self): self.assertIsNone(sot.set_image_metadata(self.sess, {'foo': 'bar'})) url = f'volumes/{FAKE_ID}/action' - body = {'os-set_image_metadata': {'foo': 'bar'}} + body = {'os-set_image_metadata': {'metadata': {'foo': 'bar'}}} self.sess.post.assert_called_with( url, json=body, microversion=sot._max_microversion ) From 4b0b78e195f2d8e6d80d79a8f46a88b80488f79c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 17 Jul 2025 16:27:35 +0100 Subject: [PATCH 3737/3836] Allow filtering of volumes by properties This needs to be a stringified dict, which is kind of ugly but what can you do. Change-Id: Ied1ffb40c85c7eb69c43b4fef6a6156d6bf7b8da Signed-off-by: Stephen Finucane --- openstack/block_storage/v3/volume.py | 1 + openstack/tests/unit/block_storage/v3/test_volume.py | 1 + 2 files changed, 2 insertions(+) diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index a38671eb0..41bb6b501 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -30,6 +30,7 @@ class Volume(resource.Resource, metadata.MetadataMixin): 'project_id', 'created_at', 'updated_at', + properties='metadata', all_projects='all_tenants', ) diff --git a/openstack/tests/unit/block_storage/v3/test_volume.py b/openstack/tests/unit/block_storage/v3/test_volume.py index 5ee01ec73..eaf7ccb78 100644 --- a/openstack/tests/unit/block_storage/v3/test_volume.py +++ b/openstack/tests/unit/block_storage/v3/test_volume.py @@ -100,6 +100,7 @@ def test_basic(self): "project_id": "project_id", "created_at": "created_at", "updated_at": "updated_at", + "properties": "metadata", "limit": "limit", "marker": "marker", }, From c2465fee2d0d7814f03927b20babe051bcef26e0 Mon Sep 17 00:00:00 2001 From: Alexey Stupnikov Date: Fri, 18 Jul 2025 21:12:25 +0200 Subject: [PATCH 3738/3836] Increase swap allocation for devstack-networking devstack-networking tests are failing often because OOM kills different processes (mysqld mostly). This behavior is consistently reproducible. Closes-bug: #2117288 Change-Id: Id04456762a5a82ecc451f382e8f671f2ae63af3f Signed-off-by: Alexey Stupnikov --- zuul.d/functional-jobs.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zuul.d/functional-jobs.yaml b/zuul.d/functional-jobs.yaml index 502d824e6..067146257 100644 --- a/zuul.d/functional-jobs.yaml +++ b/zuul.d/functional-jobs.yaml @@ -99,7 +99,7 @@ - openstack/designate - openstack/octavia vars: - configure_swap_size: 4096 + configure_swap_size: 8192 devstack_local_conf: post-config: $OCTAVIA_CONF: @@ -148,7 +148,7 @@ - openstack/tap-as-a-service vars: INSTALL_OVN: False - configure_swap_size: 4096 + configure_swap_size: 8192 devstack_local_conf: post-config: $OCTAVIA_CONF: From cb04f4a706d49c2d25f117768744f938f8a6f5e0 Mon Sep 17 00:00:00 2001 From: Alexey Stupnikov Date: Wed, 9 Jul 2025 22:58:22 +0200 Subject: [PATCH 3739/3836] Fix hostname attribute withing request body The hostname attribute exists with two different names, depending on whether it's in a request or a response. When we're preparing a request, include it in the request body as hostname, otherwise expect OS-EXT-SRV-ATTR:hostname in the response body such as for a GET. Closes-bug: #2091656 Change-Id: Ia23126592a28ff2a45dfe8f64f641920acba1927 Signed-off-by: Alexey Stupnikov --- openstack/compute/v2/server.py | 5 +++++ openstack/tests/unit/compute/v2/test_server.py | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 74f09182e..267cac23c 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -308,6 +308,11 @@ def _prepare_request( if hint_key in server_body: request.body[hint_key] = server_body.pop(hint_key) + # hostname exists with a prefix on response, but not request + hostname_key = "OS-EXT-SRV-ATTR:hostname" + if hostname_key in server_body: + server_body["hostname"] = server_body.pop(hostname_key) + return request def _action(self, session, body, microversion=None): diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index f2cdc7e3d..98a96acae 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -274,6 +274,7 @@ def test__prepare_server(self): zone = 1 data = 2 hints = {"hint": 3} + hostname = 'foo' sot = server.Server( id=1, @@ -282,6 +283,7 @@ def test__prepare_server(self): scheduler_hints=hints, min_count=2, max_count=3, + hostname=hostname, ) request = sot._prepare_request() @@ -302,6 +304,11 @@ def test__prepare_server(self): ) self.assertEqual(request.body["OS-SCH-HNT:scheduler_hints"], hints) + self.assertNotIn( + "OS-EXT-SRV-ATTR:hostname", request.body[sot.resource_key] + ) + self.assertEqual(request.body[sot.resource_key]["hostname"], hostname) + self.assertEqual(2, request.body[sot.resource_key]['min_count']) self.assertEqual(3, request.body[sot.resource_key]['max_count']) From d649aada8d5f72af3537b8c413adc05da1706b53 Mon Sep 17 00:00:00 2001 From: djp Date: Mon, 2 Jun 2025 21:31:19 +0900 Subject: [PATCH 3740/3836] resource: add max_items parameter to resource Currently the limit option is used ambiguously, the first is the total amount of resources returned, and the second is the size of the page. To separate this, we add a parameter called max_items, which is the total amount of resources returned. Change-Id: I56fdb5ef480da6bca82462ce3ca4e0cbcb2830de Signed-off-by: djp --- openstack/dns/v2/_base.py | 1 + openstack/resource.py | 21 ++++- openstack/tests/unit/test_resource.py | 80 ++++++++++++++++++- ...d_max_item_parameter-3ab3c2e1cd2312c5.yaml | 8 ++ 4 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/add_max_item_parameter-3ab3c2e1cd2312c5.yaml diff --git a/openstack/dns/v2/_base.py b/openstack/dns/v2/_base.py index 6b2d8804a..16aa81079 100644 --- a/openstack/dns/v2/_base.py +++ b/openstack/dns/v2/_base.py @@ -123,4 +123,5 @@ def _get_next_link(cls, uri, response, data, marker, limit, total_yielded): next_link = uri params['marker'] = marker params['limit'] = limit + return next_link, params diff --git a/openstack/resource.py b/openstack/resource.py index 91186d805..0196e6197 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1956,6 +1956,7 @@ def list( *, microversion: str | None = None, headers: dict[str, str] | None = None, + max_items: int | None = None, **params: ty.Any, ) -> ty.Generator[ty_ext.Self, None, None]: """This method is a generator which yields resource objects. @@ -1978,6 +1979,8 @@ def list( :param str microversion: API version to override the negotiated one. :param dict headers: Additional headers to inject into the HTTP request. + :param int max_items: The maximum number of items to return. Typically + this must be used with ``paginated=True``. :param dict params: These keyword arguments are passed through the :meth:`~openstack.resource.QueryParamter._transpose` method to find if any of them match expected query parameters to be sent @@ -2028,6 +2031,17 @@ def list( uri = base_path % params uri_params = {} + if max_items and not query_params.get('limit'): + # If a user requested max_items but not a limit, set limit to + # max_items on the assumption that if (a) the value is smaller than + # the maximum server allowed value for limit then we'll be able to + # do a single call to get everything, while (b) if the value is + # larger then the server will ignore the value (or rather use its + # own hardcoded limit) making this is a no-op. + # If a user requested both max_items and limit then we assume they + # know what they're doing. + query_params['limit'] = max_items + limit = query_params.get('limit') for k, v in params.items(): @@ -2079,6 +2093,11 @@ def _dict_filter(f, d): marker = None for raw_resource in resources: + # We return as soon as we hit our limit, even if we have items + # remaining + if max_items and total_yielded >= max_items: + return + # Do not allow keys called "self" through. Glance chose # to name a key "self", so we need to pop it out because # we can't send it through cls.existing and into the @@ -2136,7 +2155,7 @@ def _dict_filter(f, d): @classmethod def _get_next_link(cls, uri, response, data, marker, limit, total_yielded): next_link = None - params = {} + params: dict[str, str | list[str] | int] = {} if isinstance(data, dict): pagination_key = cls.pagination_key diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index bd3b36ae7..0ca99ae25 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -2313,6 +2313,85 @@ class Test(resource.Resource): self.assertEqual(2, len(self.session.get.call_args_list)) self.assertIsInstance(results[0], Test) + def test_list_response_paginated_with_max_items(self): + """Test pagination with a 'max_items' in the response. + + The limit variable is used in two meanings. + To make it clear, we add the max_items parameter and + use this value to determine the number of resources to be returned. + """ + ids = [1, 2, 3, 4] + + def make_mock_response(): + resp = mock.Mock() + resp.status_code = 200 + resp.links = {} + resp.json.return_value = { + "resources": [ + {"id": 1}, + {"id": 2}, + {"id": 3}, + {"id": 4}, + ], + } + return resp + + self.session.get.side_effect = [ + make_mock_response(), + make_mock_response(), + make_mock_response(), + ] + + # Since the limit value is 3 but the max_items value is 2, two resources are returned. + results = self.sot.list( + self.session, limit=3, paginated=True, max_items=2 + ) + + result0 = next(results) + self.assertEqual(result0.id, ids[0]) + result1 = next(results) + self.assertEqual(result1.id, ids[1]) + self.session.get.assert_called_with( + self.base_path, + headers={"Accept": "application/json"}, + params={"limit": 3}, + microversion=None, + ) + self.assertRaises(StopIteration, next, results) + + # max_items is set and limit in unset (so limit defaults to max_items) + results = self.sot.list(self.session, paginated=True, max_items=2) + result0 = next(results) + self.assertEqual(result0.id, ids[0]) + result1 = next(results) + self.assertEqual(result1.id, ids[1]) + self.session.get.assert_called_with( + self.base_path, + headers={"Accept": "application/json"}, + params={"limit": 2}, + microversion=None, + ) + self.assertRaises(StopIteration, next, results) + + # both max_items and limit are set, and max_items is greater than limit + # (the opposite of this test: we should see multiple requests for limit resources each time) + results = self.sot.list( + self.session, limit=1, paginated=True, max_items=3 + ) + result0 = next(results) + self.assertEqual(result0.id, ids[0]) + result1 = next(results) + self.assertEqual(result1.id, ids[1]) + result2 = next(results) + self.assertEqual(result2.id, ids[2]) + self.session.get.assert_called_with( + self.base_path, + headers={"Accept": "application/json"}, + params={"limit": 1}, + microversion=None, + ) + self.assertRaises(StopIteration, next, results) + def test_list_response_paginated_with_microversions(self): class Test(resource.Resource): service = self.service_name @@ -2812,7 +2891,6 @@ def test_list_multi_page_inferred_additional(self): self.session.get.side_effect = [resp1, resp2] results = self.sot.list(self.session, limit=2, paginated=True) - # Get the first page's two items result0 = next(results) self.assertEqual(result0.id, ids[0]) diff --git a/releasenotes/notes/add_max_item_parameter-3ab3c2e1cd2312c5.yaml b/releasenotes/notes/add_max_item_parameter-3ab3c2e1cd2312c5.yaml new file mode 100644 index 000000000..5eb60a84f --- /dev/null +++ b/releasenotes/notes/add_max_item_parameter-3ab3c2e1cd2312c5.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + A new parameter, ``max_items``, is added to the ``Resource.list`` + method. This allows users to specify the maximum number of resources + that should be returned to the user, as opposed to the maximum number + of items that should be requested from the server in a single request. + The latter is already handled by the ``limit`` parameter From b2ada579cecf45e04d19a3de60b073ac09c51fc5 Mon Sep 17 00:00:00 2001 From: Mridula Joshi Date: Mon, 26 Feb 2024 10:51:18 +0000 Subject: [PATCH 3741/3836] create_image: support other import methods Change-Id: I31a09f3906ec506d93d65e3209a813df1319e1e3 Signed-off-by: Cyril Roelandt --- openstack/image/v2/_proxy.py | 67 +++++++++++++++++-- openstack/tests/unit/cloud/test_image.py | 1 + openstack/tests/unit/image/v2/test_proxy.py | 25 +++++++ ...t-all-import-methods-48e4e382b7091dd3.yaml | 6 ++ 4 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/create-image-support-all-import-methods-48e4e382b7091dd3.yaml diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 6b017ee46..988ef1f86 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -131,6 +131,11 @@ def create_image( timeout=3600, validate_checksum=False, use_import=False, + import_method=None, + uri=None, + remote_region=None, + remote_image_id=None, + remote_service_interface=None, stores=None, all_stores=None, all_stores_must_succeed=None, @@ -198,6 +203,20 @@ def create_image( cloud to transform image format. If the cloud has disabled direct uploads, this will default to true. If you wish to use other import methods, use the ``import_image`` method instead. + :param import_method: Method to use for importing the image. Not all + deployments support all methods. One of: ``glance-direct`` + (default), ``web-download``, ``glance-download`` (``copy-image`` is + not used with create). Use of ``glance-direct`` requires the image + be first staged. + :param uri: Required only if using the ``web-download`` import method. + This url is where the data is made available to the Image + service. + :param remote_region: The remote glance region to download the image + from when using glance-download. + :param remote_image_id: The ID of the image to import from the + remote glance when using glance-download. + :param remote_service_interface: The remote glance service interface to + use when using glance-download. :param stores: List of stores to be used when enabled_backends is activated in glance. List values can be the id of a store or a :class:`~openstack.image.v2.service_info.Store` instance. @@ -322,7 +341,7 @@ def create_image( if tags: image_kwargs['tags'] = tags - if filename or data: + if filename or data or import_method: image = self._upload_image( name, filename=filename, @@ -332,6 +351,11 @@ def create_image( timeout=timeout, validate_checksum=validate_checksum, use_import=use_import, + import_method=import_method, + uri=uri, + remote_region=remote_region, + remote_image_id=remote_image_id, + remote_service_interface=remote_service_interface, stores=stores, all_stores=all_stores, all_stores_must_succeed=all_stores_must_succeed, @@ -547,6 +571,11 @@ def _upload_image( timeout=None, validate_checksum=True, use_import=False, + import_method=None, + uri=None, + remote_region=None, + remote_image_id=None, + remote_service_interface=None, stores=None, all_stores=None, all_stores_must_succeed=None, @@ -589,6 +618,11 @@ def _upload_image( meta=meta, validate_checksum=validate_checksum, use_import=use_import, + import_method=import_method, + uri=uri, + remote_region=remote_region, + remote_image_id=remote_image_id, + remote_service_interface=remote_service_interface, stores=stores, all_stores=all_stores, all_stores_must_succeed=all_stores_must_succeed, @@ -623,11 +657,21 @@ def _upload_image_put( meta, validate_checksum, use_import=False, + import_method=None, + uri=None, + remote_region=None, + remote_image_id=None, + remote_service_interface=None, stores=None, all_stores=None, all_stores_must_succeed=None, **image_kwargs, ): + if all_stores and stores: + raise exceptions.InvalidRequest( + "all_stores is mutually exclusive with stores" + ) + # use of any of these imply use_import=True if stores or all_stores or all_stores_must_succeed: use_import = True @@ -647,7 +691,7 @@ def _upload_image_put( supports_import = ( image.image_import_methods - and 'glance-direct' in image.image_import_methods + and import_method in image.image_import_methods ) if use_import and not supports_import: raise exceptions.SDKException( @@ -660,8 +704,23 @@ def _upload_image_put( response = image.upload(self) exceptions.raise_from_response(response) if use_import: - image.stage(self) - image.import_image(self) + kwargs = {} + if stores is not None: + kwargs['stores'] = stores + else: + kwargs['all_stores'] = all_stores + kwargs['all_stores_must_succeed'] = all_stores_must_succeed + if import_method == 'glance-direct': + image.stage(self) + elif import_method == 'web-download': + kwargs['uri'] = uri + elif import_method == 'glance-download': + kwargs.update( + remote_region=remote_region, + remote_image_id=remote_image_id, + remote_service_interface=remote_service_interface, + ) + self.import_image(image, method=import_method, **kwargs) # image_kwargs are flat here md5 = image_kwargs.get(self._IMAGE_MD5_KEY) diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index aaec255ee..cf3ef15cc 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -754,6 +754,7 @@ def test_create_image_use_import(self): is_public=False, validate_checksum=True, use_import=True, + import_method='glance-direct', ) self.assert_calls() diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index c4644ef75..3ce35672e 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -145,6 +145,11 @@ def test_image_create_file_as_name(self): 'validate_checksum': False, 'use_import': False, 'stores': None, + 'import_method': None, + 'uri': None, + 'remote_region': None, + 'remote_image_id': None, + 'remote_service_interface': None, 'all_stores': None, 'all_stores_must_succeed': None, 'disk_format': 'qcow2', @@ -255,6 +260,11 @@ def test_image_create_validate_checksum_data_binary(self): timeout=3600, validate_checksum=True, use_import=False, + import_method=None, + uri=None, + remote_region=None, + remote_image_id=None, + remote_service_interface=None, stores=None, all_stores=None, all_stores_must_succeed=None, @@ -303,6 +313,11 @@ def test_image_create_data_binary(self): timeout=3600, validate_checksum=False, use_import=False, + import_method=None, + uri=None, + remote_region=None, + remote_image_id=None, + remote_service_interface=None, stores=None, all_stores=None, all_stores_must_succeed=None, @@ -365,6 +380,11 @@ def test_image_create_with_stores(self): timeout=3600, validate_checksum=False, use_import=True, + import_method=None, + uri=None, + remote_region=None, + remote_image_id=None, + remote_service_interface=None, stores=['cinder', 'swift'], all_stores=None, all_stores_must_succeed=None, @@ -402,6 +422,11 @@ def test_image_create_with_all_stores(self): timeout=3600, validate_checksum=False, use_import=True, + import_method=None, + uri=None, + remote_region=None, + remote_image_id=None, + remote_service_interface=None, stores=None, all_stores=True, all_stores_must_succeed=True, diff --git a/releasenotes/notes/create-image-support-all-import-methods-48e4e382b7091dd3.yaml b/releasenotes/notes/create-image-support-all-import-methods-48e4e382b7091dd3.yaml new file mode 100644 index 000000000..f42ea61ef --- /dev/null +++ b/releasenotes/notes/create-image-support-all-import-methods-48e4e382b7091dd3.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + The ``create_image`` method now takes new parameters (``import_method``, + ``uri``, ``remote_region``, ``remote_image_id`` and + ``remote_service_interface``) to support all import methods from Glance. From b14c73d356d375771033a4c7e1fede814aa2fc1a Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 4 Jul 2025 12:03:16 +0100 Subject: [PATCH 3742/3836] Remove commented out sections We cannot use this without some rework of pbr tooling. Remove it for now. Change-Id: I493d0eb50af4acf5c1c00b4a6db546b1bb468993 Signed-off-by: Stephen Finucane --- pyproject.toml | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 131e6051f..a313c98bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,26 +11,21 @@ authors = [ readme = {file = "README.rst", content-type = "text/x-rst"} license = {text = "Apache-2.0"} dynamic = ["version", "dependencies"] -# dependencies = [ ] requires-python = ">=3.10" classifiers = [ - "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.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", + "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.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] -# [project.optional-dependencies] -# test = [ -# ] - [project.urls] Homepage = "https://docs.openstack.org/openstacksdk" Repository = "https://opendev.org/openstack/openstacksdk/" From f2788fd0cb0184ffc770625bf1f4eaa0f4ce9a4f Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 26 Jun 2025 14:30:02 +0100 Subject: [PATCH 3743/3836] tests: Streamline flavor test Change-Id: Ic9c8c574cb973b3e8e87171cfc4b7e1eec7dd0e8 Signed-off-by: Stephen Finucane --- .../functional/compute/v2/test_flavor.py | 221 +++++++++++------- 1 file changed, 131 insertions(+), 90 deletions(-) diff --git a/openstack/tests/functional/compute/v2/test_flavor.py b/openstack/tests/functional/compute/v2/test_flavor.py index 15f2e0a51..8cd597a04 100644 --- a/openstack/tests/functional/compute/v2/test_flavor.py +++ b/openstack/tests/functional/compute/v2/test_flavor.py @@ -11,138 +11,179 @@ # under the License. import uuid -from openstack import exceptions +from openstack.compute.v2 import flavor as _flavor from openstack.tests.functional import base class TestFlavor(base.BaseFunctionalTest): def setUp(self): super().setUp() - self.new_item_name = self.getUniqueString('flavor') - self.one_flavor = list(self.operator_cloud.compute.flavors())[0] - def test_flavors(self): + self.public_flavor_name = uuid.uuid4().hex + self.private_flavor_name = uuid.uuid4().hex + + def _delete_flavor(self, flavor): + ret = self.operator_cloud.compute.delete_flavor(flavor) + self.assertIsNone(ret) + + def test_flavor(self): + # create flavors + # + # create a public and private flavor so we can test that they are both + # listed for an operator + + public_flavor = self.operator_cloud.compute.create_flavor( + name=self.public_flavor_name, + ram=1024, + vcpus=2, + disk=10, + is_public=True, + ) + self.addCleanup(self._delete_flavor, public_flavor) + self.assertIsInstance(public_flavor, _flavor.Flavor) + + private_flavor = self.operator_cloud.compute.create_flavor( + name=self.private_flavor_name, + ram=1024, + vcpus=2, + disk=10, + is_public=False, + ) + self.addCleanup(self._delete_flavor, private_flavor) + self.assertIsInstance(private_flavor, _flavor.Flavor) + + # list all flavors + # + # flavor list will include the standard devstack flavors. We just want + # to make sure both of the flavors we just created are present. flavors = list(self.operator_cloud.compute.flavors()) - self.assertGreater(len(flavors), 0) + self.assertIn(self.public_flavor_name, {x.name for x in flavors}) + self.assertIn(self.private_flavor_name, {x.name for x in flavors}) - for flavor in flavors: - self.assertIsInstance(flavor.id, str) - self.assertIsInstance(flavor.name, str) - self.assertIsInstance(flavor.disk, int) - self.assertIsInstance(flavor.ram, int) - self.assertIsInstance(flavor.vcpus, int) + # get flavor by ID - def test_find_flavors_by_id(self): - rslt = self.operator_cloud.compute.find_flavor(self.one_flavor.id) - self.assertEqual(rslt.id, self.one_flavor.id) + flavor = self.operator_cloud.compute.get_flavor(public_flavor.id) + self.assertEqual(flavor.id, public_flavor.id) - def test_find_flavors_by_name(self): - rslt = self.operator_cloud.compute.find_flavor(self.one_flavor.name) - self.assertEqual(rslt.name, self.one_flavor.name) + # find flavor by name - def test_find_flavors_no_match_ignore_true(self): - rslt = self.operator_cloud.compute.find_flavor( - "not a flavor", ignore_missing=True - ) - self.assertIsNone(rslt) + flavor = self.operator_cloud.compute.find_flavor(public_flavor.name) + self.assertEqual(flavor.name, public_flavor.name) - def test_find_flavors_no_match_ignore_false(self): - self.assertRaises( - exceptions.NotFoundException, - self.operator_cloud.compute.find_flavor, - "not a flavor", - ignore_missing=False, - ) + # update a flavor - def test_list_flavors(self): - pub_flavor_name = self.new_item_name + '_public' - priv_flavor_name = self.new_item_name + '_private' - public_kwargs = dict( - name=pub_flavor_name, ram=1024, vcpus=2, disk=10, is_public=True - ) - private_kwargs = dict( - name=priv_flavor_name, ram=1024, vcpus=2, disk=10, is_public=False + self.operator_cloud.compute.update_flavor( + public_flavor, + description="updated description", ) - # Create a public and private flavor. We expect both to be listed - # for an operator. - self.operator_cloud.compute.create_flavor(**public_kwargs) - self.operator_cloud.compute.create_flavor(**private_kwargs) + # fetch the updated flavor - flavors = self.operator_cloud.compute.flavors() - - # Flavor list will include the standard devstack flavors. We just want - # to make sure both of the flavors we just created are present. - found = [] - for f in flavors: - # extra_specs should be added within list_flavors() - self.assertIn('extra_specs', f) - if f['name'] in (pub_flavor_name, priv_flavor_name): - found.append(f) - self.assertEqual(2, len(found)) + flavor = self.operator_cloud.compute.get_flavor(public_flavor.id) + self.assertEqual(flavor.description, "updated description") def test_flavor_access(self): + # create private flavor + flavor_name = uuid.uuid4().hex - flv = self.operator_cloud.compute.create_flavor( - is_public=False, name=flavor_name, ram=128, vcpus=1, disk=0 + flavor = self.operator_cloud.compute.create_flavor( + name=flavor_name, ram=128, vcpus=1, disk=0, is_public=False ) - self.addCleanup(self.operator_cloud.compute.delete_flavor, flv.id) - # Validate the 'demo' user cannot see the new flavor - flv_cmp = self.user_cloud.compute.find_flavor(flavor_name) - self.assertIsNone(flv_cmp) + self.addCleanup(self._delete_flavor, flavor) + self.assertIsInstance(flavor, _flavor.Flavor) + + # validate the 'demo' user cannot see the new flavor - # Validate we can see the new flavor ourselves - flv_cmp = self.operator_cloud.compute.find_flavor(flavor_name) - self.assertIsNotNone(flv_cmp) - self.assertEqual(flavor_name, flv_cmp.name) + flavor = self.user_cloud.compute.find_flavor( + flavor_name, ignore_missing=True + ) + self.assertIsNone(flavor) + + # validate we can see the new flavor ourselves + + flavor = self.operator_cloud.compute.find_flavor( + flavor_name, ignore_missing=True + ) + self.assertIsNotNone(flavor) + self.assertEqual(flavor_name, flavor.name) + + # get demo project for access control project = self.operator_cloud.get_project('demo') self.assertIsNotNone(project) - # Now give 'demo' access + # give 'demo' access to the flavor + self.operator_cloud.compute.flavor_add_tenant_access( - flv.id, project['id'] + flavor.id, project['id'] ) - # Now see if the 'demo' user has access to it - flv_cmp = self.user_cloud.compute.find_flavor(flavor_name) - self.assertIsNotNone(flv_cmp) + # verify that the 'demo' user now has access to it + + flavor = self.user_cloud.compute.find_flavor( + flavor_name, ignore_missing=True + ) + self.assertIsNotNone(flavor) + + # remove 'demo' access and check we can't find it anymore - # Now remove 'demo' access and check we can't find it self.operator_cloud.compute.flavor_remove_tenant_access( - flv.id, project['id'] + flavor.id, project['id'] ) - flv_cmp = self.user_cloud.compute.find_flavor(flavor_name) - self.assertIsNone(flv_cmp) + flavor = self.user_cloud.compute.find_flavor( + flavor_name, ignore_missing=True + ) + self.assertIsNone(flavor) + + def test_flavor_extra_specs(self): + # create private flavor - def test_extra_props_calls(self): flavor_name = uuid.uuid4().hex - flv = self.operator_cloud.compute.create_flavor( + flavor = self.operator_cloud.compute.create_flavor( is_public=False, name=flavor_name, ram=128, vcpus=1, disk=0 ) - self.addCleanup(self.operator_cloud.compute.delete_flavor, flv.id) - # Create extra_specs + self.addCleanup(self._delete_flavor, flavor) + self.assertIsInstance(flavor, _flavor.Flavor) + + # create extra_specs + specs = {'a': 'b'} self.operator_cloud.compute.create_flavor_extra_specs( - flv, extra_specs=specs + flavor, extra_specs=specs + ) + + # verify specs were created correctly + + flavor_with_specs = ( + self.operator_cloud.compute.fetch_flavor_extra_specs(flavor) ) - # verify specs - flv_cmp = self.operator_cloud.compute.fetch_flavor_extra_specs(flv) - self.assertDictEqual(specs, flv_cmp.extra_specs) - # update + self.assertDictEqual(specs, flavor_with_specs.extra_specs) + + # update/add a single extra spec property + self.operator_cloud.compute.update_flavor_extra_specs_property( - flv, 'c', 'd' + flavor, 'c', 'd' ) - val_cmp = self.operator_cloud.compute.get_flavor_extra_specs_property( - flv, 'c' + + # fetch single property value + + prop_value = ( + self.operator_cloud.compute.get_flavor_extra_specs_property( + flavor, 'c' + ) ) - # fetch single prop - self.assertEqual('d', val_cmp) - # drop new prop + self.assertEqual('d', prop_value) + + # delete the new property + self.operator_cloud.compute.delete_flavor_extra_specs_property( - flv, 'c' + flavor, 'c' + ) + + # re-fetch and ensure we're back to the previous state + + flavor_with_specs = ( + self.operator_cloud.compute.fetch_flavor_extra_specs(flavor) ) - # re-fetch and ensure prev state - flv_cmp = self.operator_cloud.compute.fetch_flavor_extra_specs(flv) - self.assertDictEqual(specs, flv_cmp.extra_specs) + self.assertDictEqual(specs, flavor_with_specs.extra_specs) From 135d3db34585ccb6a0c98108bc912608b29655b3 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 26 Jun 2025 14:50:38 +0100 Subject: [PATCH 3744/3836] tests: Streamline hypervisor test Change-Id: I8711442dab2ae38bc6e30b7eac206c3cdf8cb992 Signed-off-by: Stephen Finucane --- .../functional/compute/v2/test_hypervisor.py | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/openstack/tests/functional/compute/v2/test_hypervisor.py b/openstack/tests/functional/compute/v2/test_hypervisor.py index 1c5f5cccc..760fc81a8 100644 --- a/openstack/tests/functional/compute/v2/test_hypervisor.py +++ b/openstack/tests/functional/compute/v2/test_hypervisor.py @@ -10,21 +10,28 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.compute.v2 import hypervisor as _hypervisor from openstack.tests.functional import base class TestHypervisor(base.BaseFunctionalTest): - def setUp(self): - super().setUp() + def test_hypervisors(self): + hypervisors = list(self.operator_cloud.compute.hypervisors()) + self.assertIsNotNone(hypervisors) - def test_list_hypervisors(self): - rslt = list(self.operator_cloud.compute.hypervisors()) - self.assertIsNotNone(rslt) + hypervisors = list( + self.operator_cloud.compute.hypervisors(details=True) + ) + self.assertIsNotNone(hypervisors) - rslt = list(self.operator_cloud.compute.hypervisors(details=True)) - self.assertIsNotNone(rslt) + hypervisor = self.operator_cloud.compute.get_hypervisor( + hypervisors[0].id + ) + self.assertIsInstance(hypervisor, _hypervisor.Hypervisor) + self.assertEqual(hypervisor.id, hypervisors[0].id) - def test_get_find_hypervisors(self): - for hypervisor in self.operator_cloud.compute.hypervisors(): - self.operator_cloud.compute.get_hypervisor(hypervisor.id) - self.operator_cloud.compute.find_hypervisor(hypervisor.id) + hypervisor = self.operator_cloud.compute.find_hypervisor( + hypervisors[0].name, ignore_missing=False + ) + self.assertIsInstance(hypervisor, _hypervisor.Hypervisor) + self.assertEqual(hypervisor.id, hypervisors[0].id) From 3a78cecb4e9fa084a56cdc8ad38087e35b8bd039 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 26 Jun 2025 14:57:42 +0100 Subject: [PATCH 3745/3836] tests: Streamline (compute) images test Change-Id: If3cf9b60c15455e1b7202205edadc15547035f49 Signed-off-by: Stephen Finucane --- .../tests/functional/compute/v2/test_image.py | 100 +++++++++--------- 1 file changed, 52 insertions(+), 48 deletions(-) diff --git a/openstack/tests/functional/compute/v2/test_image.py b/openstack/tests/functional/compute/v2/test_image.py index 864442bb0..51b720feb 100644 --- a/openstack/tests/functional/compute/v2/test_image.py +++ b/openstack/tests/functional/compute/v2/test_image.py @@ -11,82 +11,84 @@ # under the License. +from openstack.compute.v2 import image as _image from openstack.tests.functional import base from openstack.tests.functional.image.v2.test_image import TEST_IMAGE_NAME class TestImage(base.BaseFunctionalTest): - def test_images(self): + def setUp(self): + super().setUp() + + # get a non-test image to work with + images = self.operator_cloud.compute.images() + self.image = next(images) + + if self.image.name == TEST_IMAGE_NAME: + self.image = next(images) + + def test_image(self): + # list all images + images = list(self.operator_cloud.compute.images()) self.assertGreater(len(images), 0) for image in images: self.assertIsInstance(image.id, str) - def _get_non_test_image(self): - images = self.operator_cloud.compute.images() - image = next(images) - - if image.name == TEST_IMAGE_NAME: - image = next(images) - - return image - - def test_find_image(self): - image = self._get_non_test_image() - self.assertIsNotNone(image) - sot = self.operator_cloud.compute.find_image(image.id) - self.assertEqual(image.id, sot.id) - self.assertEqual(image.name, sot.name) - - def test_get_image(self): - image = self._get_non_test_image() - self.assertIsNotNone(image) - sot = self.operator_cloud.compute.get_image(image.id) - self.assertEqual(image.id, sot.id) - self.assertEqual(image.name, sot.name) - self.assertIsNotNone(image.links) - self.assertIsNotNone(image.min_disk) - self.assertIsNotNone(image.min_ram) - self.assertIsNotNone(image.metadata) - self.assertIsNotNone(image.progress) - self.assertIsNotNone(image.status) + # find image by name - def test_image_metadata(self): - image = self._get_non_test_image() + image = self.operator_cloud.compute.find_image(self.image.name) + self.assertIsInstance(image, _image.Image) + self.assertEqual(self.image.id, image.id) + self.assertEqual(self.image.name, image.name) + + # get image by ID + + image = self.operator_cloud.compute.get_image(self.image.id) + self.assertIsInstance(image, _image.Image) + self.assertEqual(self.image.id, image.id) + self.assertEqual(self.image.name, image.name) + def test_image_metadata(self): # delete pre-existing metadata + self.operator_cloud.compute.delete_image_metadata( - image, image.metadata.keys() + self.image, self.image.metadata.keys() ) - image = self.operator_cloud.compute.get_image_metadata(image) + image = self.operator_cloud.compute.get_image_metadata(self.image) self.assertFalse(image.metadata) - # get metadata - image = self.operator_cloud.compute.get_image_metadata(image) + # get metadata (should be empty) + + image = self.operator_cloud.compute.get_image_metadata(self.image) self.assertFalse(image.metadata) # set no metadata - self.operator_cloud.compute.set_image_metadata(image) - image = self.operator_cloud.compute.get_image_metadata(image) + + self.operator_cloud.compute.set_image_metadata(self.image) + image = self.operator_cloud.compute.get_image_metadata(self.image) self.assertFalse(image.metadata) # set empty metadata - self.operator_cloud.compute.set_image_metadata(image, k0='') - image = self.operator_cloud.compute.get_image_metadata(image) + + self.operator_cloud.compute.set_image_metadata(self.image, k0='') + image = self.operator_cloud.compute.get_image_metadata(self.image) self.assertIn('k0', image.metadata) self.assertEqual('', image.metadata['k0']) # set metadata - self.operator_cloud.compute.set_image_metadata(image, k1='v1') - image = self.operator_cloud.compute.get_image_metadata(image) + + self.operator_cloud.compute.set_image_metadata(self.image, k1='v1') + image = self.operator_cloud.compute.get_image_metadata(self.image) self.assertTrue(image.metadata) self.assertEqual(2, len(image.metadata)) self.assertIn('k1', image.metadata) self.assertEqual('v1', image.metadata['k1']) # set more metadata - self.operator_cloud.compute.set_image_metadata(image, k2='v2') - image = self.operator_cloud.compute.get_image_metadata(image) + + self.operator_cloud.compute.set_image_metadata(self.image, k2='v2') + image = self.operator_cloud.compute.get_image_metadata(self.image) self.assertTrue(image.metadata) self.assertEqual(3, len(image.metadata)) self.assertIn('k1', image.metadata) @@ -95,8 +97,9 @@ def test_image_metadata(self): self.assertEqual('v2', image.metadata['k2']) # update metadata - self.operator_cloud.compute.set_image_metadata(image, k1='v1.1') - image = self.operator_cloud.compute.get_image_metadata(image) + + self.operator_cloud.compute.set_image_metadata(self.image, k1='v1.1') + image = self.operator_cloud.compute.get_image_metadata(self.image) self.assertTrue(image.metadata) self.assertEqual(3, len(image.metadata)) self.assertIn('k1', image.metadata) @@ -104,9 +107,10 @@ def test_image_metadata(self): self.assertIn('k2', image.metadata) self.assertEqual('v2', image.metadata['k2']) - # delete metadata + # delete all metadata (cleanup) + self.operator_cloud.compute.delete_image_metadata( - image, image.metadata.keys() + self.image, image.metadata.keys() ) - image = self.operator_cloud.compute.get_image_metadata(image) + image = self.operator_cloud.compute.get_image_metadata(self.image) self.assertFalse(image.metadata) From c0913ce33991428b2f7a48bfc85648806933663f Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 26 Jun 2025 16:05:29 +0100 Subject: [PATCH 3746/3836] tests: Streamline servers functional test Change-Id: I8e50c87c096d85e05be40eef1180440a7c433d20 Signed-off-by: Stephen Finucane --- .../functional/compute/v2/test_server.py | 259 +++++++++++------- 1 file changed, 157 insertions(+), 102 deletions(-) diff --git a/openstack/tests/functional/compute/v2/test_server.py b/openstack/tests/functional/compute/v2/test_server.py index 95631b257..a32aa88a6 100644 --- a/openstack/tests/functional/compute/v2/test_server.py +++ b/openstack/tests/functional/compute/v2/test_server.py @@ -10,23 +10,34 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.compute.v2 import server -from openstack.tests.functional.compute import base as ft_base +from openstack.compute.v2 import server as _server +from openstack.tests.functional.compute import base as base from openstack.tests.functional.network.v2 import test_network -class TestServerAdmin(ft_base.BaseComputeTest): +class TestServerAdmin(base.BaseComputeTest): def setUp(self): super().setUp() - self.NAME = 'needstobeshortandlowercase' - self.USERDATA = 'SSdtIGFjdHVhbGx5IGEgZ29hdC4=' + self.server_name = 'needstobeshortandlowercase' + self.user_data = 'SSdtIGFjdHVhbGx5IGEgZ29hdC4=' + + def _delete_server(self, server): + sot = self.operator_cloud.compute.delete_server(server.id) + self.operator_cloud.compute.wait_for_delete( + server, wait=self._wait_for_timeout + ) + self.assertIsNone(sot) + + def test_server(self): + # create server with volume + volume = self.operator_cloud.create_volume(1) - sot = self.operator_cloud.compute.create_server( - name=self.NAME, + server = self.operator_cloud.compute.create_server( + name=self.server_name, flavor_id=self.flavor.id, image_id=self.image.id, networks='none', - user_data=self.USERDATA, + user_data=self.user_data, block_device_mapping=[ { 'uuid': volume.id, @@ -39,142 +50,186 @@ def setUp(self): ], ) self.operator_cloud.compute.wait_for_server( - sot, wait=self._wait_for_timeout + server, wait=self._wait_for_timeout ) - assert isinstance(sot, server.Server) - self.assertEqual(self.NAME, sot.name) - self.server = sot + self.assertIsInstance(server, _server.Server) + self.assertEqual(self.server_name, server.name) + self.addCleanup(self._delete_server, server) - def tearDown(self): - sot = self.operator_cloud.compute.delete_server(self.server.id) - self.operator_cloud.compute.wait_for_delete( - self.server, wait=self._wait_for_timeout - ) - self.assertIsNone(sot) - super().tearDown() + # get server details (admin-specific fields) - def test_get(self): - sot = self.operator_cloud.compute.get_server(self.server.id) - self.assertIsNotNone(sot.reservation_id) - self.assertIsNotNone(sot.launch_index) - self.assertIsNotNone(sot.ramdisk_id) - self.assertIsNotNone(sot.kernel_id) - self.assertEqual(self.NAME, sot.hostname) - self.assertTrue(sot.root_device_name.startswith('/dev')) - self.assertEqual(self.USERDATA, sot.user_data) - self.assertTrue(sot.attached_volumes[0]['delete_on_termination']) + server = self.operator_cloud.compute.get_server(server.id) + self.assertIsNotNone(server.reservation_id) + self.assertIsNotNone(server.launch_index) + self.assertIsNotNone(server.ramdisk_id) + self.assertIsNotNone(server.kernel_id) + self.assertEqual(self.server_name, server.hostname) + self.assertTrue(server.root_device_name.startswith('/dev')) + self.assertEqual(self.user_data, server.user_data) + self.assertTrue(server.attached_volumes[0]['delete_on_termination']) -class TestServer(ft_base.BaseComputeTest): +class TestServer(base.BaseComputeTest): def setUp(self): super().setUp() - self.NAME = self.getUniqueString() - self.network = None - self.subnet = None + self.server_name = self.getUniqueString() self.cidr = '10.99.99.0/16' + # create network for server + self.network, self.subnet = test_network.create_network( - self.user_cloud, self.NAME, self.cidr + self.user_cloud, self.server_name, self.cidr ) self.assertIsNotNone(self.network) + self.addCleanup(self._delete_network, self.network, self.subnet) + + def _delete_server(self, server): + sot = self.user_cloud.compute.delete_server(server.id) + self.assertIsNone(sot) + # Need to wait for the stack to go away before network delete + self.user_cloud.compute.wait_for_delete( + server, wait=self._wait_for_timeout + ) + + def _delete_network(self, network, subnet): + test_network.delete_network(self.user_cloud, network, subnet) + + def test_server(self): + # create server - sot = self.user_cloud.compute.create_server( - name=self.NAME, + self.server = self.user_cloud.compute.create_server( + name=self.server_name, flavor_id=self.flavor.id, image_id=self.image.id, networks=[{"uuid": self.network.id}], ) self.user_cloud.compute.wait_for_server( - sot, wait=self._wait_for_timeout - ) - assert isinstance(sot, server.Server) - self.assertEqual(self.NAME, sot.name) - self.server = sot - - def tearDown(self): - sot = self.user_cloud.compute.delete_server(self.server.id) - self.assertIsNone(sot) - # Need to wait for the stack to go away before network delete - self.user_cloud.compute.wait_for_delete( self.server, wait=self._wait_for_timeout ) - test_network.delete_network(self.user_cloud, self.network, self.subnet) - super().tearDown() + self.addCleanup(self._delete_server, self.server) + self.assertIsInstance(self.server, _server.Server) + self.assertEqual(self.server_name, self.server.name) + + # find server by name + + server = self.user_cloud.compute.find_server(self.server_name) + self.assertEqual(self.server.id, server.id) - def test_find(self): - sot = self.user_cloud.compute.find_server(self.NAME) - self.assertEqual(self.server.id, sot.id) + # get server by ID - def test_get(self): - sot = self.user_cloud.compute.get_server(self.server.id) - self.assertEqual(self.NAME, sot.name) - self.assertEqual(self.server.id, sot.id) + server = self.user_cloud.compute.get_server(self.server.id) + self.assertEqual(self.server_name, server.name) + self.assertEqual(self.server.id, server.id) - def test_list(self): - names = [o.name for o in self.user_cloud.compute.servers()] - self.assertIn(self.NAME, names) + # list servers + + server = self.user_cloud.compute.servers() + self.assertIn(self.server_name, {x.name for x in server}) def test_server_metadata(self): - test_server = self.user_cloud.compute.get_server(self.server.id) + # create server - # get metadata - test_server = self.user_cloud.compute.get_server_metadata(test_server) - self.assertFalse(test_server.metadata) + server = self.user_cloud.compute.create_server( + name=self.server_name, + flavor_id=self.flavor.id, + image_id=self.image.id, + networks=[{"uuid": self.network.id}], + ) + self.user_cloud.compute.wait_for_server( + server, wait=self._wait_for_timeout + ) + self.assertIsInstance(server, _server.Server) + self.addCleanup(self._delete_server, server) + + # get metadata (should be empty initially) + + server = self.user_cloud.compute.get_server_metadata(server) + self.assertFalse(server.metadata) # set no metadata - self.user_cloud.compute.set_server_metadata(test_server) - test_server = self.user_cloud.compute.get_server_metadata(test_server) - self.assertFalse(test_server.metadata) + + self.user_cloud.compute.set_server_metadata(server) + server = self.user_cloud.compute.get_server_metadata(server) + self.assertFalse(server.metadata) # set empty metadata - self.user_cloud.compute.set_server_metadata(test_server, k0='') - server = self.user_cloud.compute.get_server_metadata(test_server) + + self.user_cloud.compute.set_server_metadata(server, k0='') + server = self.user_cloud.compute.get_server_metadata(server) self.assertTrue(server.metadata) # set metadata - self.user_cloud.compute.set_server_metadata(test_server, k1='v1') - test_server = self.user_cloud.compute.get_server_metadata(test_server) - self.assertTrue(test_server.metadata) - self.assertEqual(2, len(test_server.metadata)) - self.assertIn('k0', test_server.metadata) - self.assertEqual('', test_server.metadata['k0']) - self.assertIn('k1', test_server.metadata) - self.assertEqual('v1', test_server.metadata['k1']) + + self.user_cloud.compute.set_server_metadata(server, k1='v1') + server = self.user_cloud.compute.get_server_metadata(server) + self.assertTrue(server.metadata) + self.assertEqual(2, len(server.metadata)) + self.assertIn('k0', server.metadata) + self.assertEqual('', server.metadata['k0']) + self.assertIn('k1', server.metadata) + self.assertEqual('v1', server.metadata['k1']) # set more metadata - self.user_cloud.compute.set_server_metadata(test_server, k2='v2') - test_server = self.user_cloud.compute.get_server_metadata(test_server) - self.assertTrue(test_server.metadata) - self.assertEqual(3, len(test_server.metadata)) - self.assertIn('k0', test_server.metadata) - self.assertEqual('', test_server.metadata['k0']) - self.assertIn('k1', test_server.metadata) - self.assertEqual('v1', test_server.metadata['k1']) - self.assertIn('k2', test_server.metadata) - self.assertEqual('v2', test_server.metadata['k2']) + + self.user_cloud.compute.set_server_metadata(server, k2='v2') + server = self.user_cloud.compute.get_server_metadata(server) + self.assertTrue(server.metadata) + self.assertEqual(3, len(server.metadata)) + self.assertIn('k0', server.metadata) + self.assertEqual('', server.metadata['k0']) + self.assertIn('k1', server.metadata) + self.assertEqual('v1', server.metadata['k1']) + self.assertIn('k2', server.metadata) + self.assertEqual('v2', server.metadata['k2']) # update metadata - self.user_cloud.compute.set_server_metadata(test_server, k1='v1.1') - test_server = self.user_cloud.compute.get_server_metadata(test_server) - self.assertTrue(test_server.metadata) - self.assertEqual(3, len(test_server.metadata)) - self.assertIn('k0', test_server.metadata) - self.assertEqual('', test_server.metadata['k0']) - self.assertIn('k1', test_server.metadata) - self.assertEqual('v1.1', test_server.metadata['k1']) - self.assertIn('k2', test_server.metadata) - self.assertEqual('v2', test_server.metadata['k2']) - - # delete metadata + + self.user_cloud.compute.set_server_metadata(server, k1='v1.1') + server = self.user_cloud.compute.get_server_metadata(server) + self.assertTrue(server.metadata) + self.assertEqual(3, len(server.metadata)) + self.assertIn('k0', server.metadata) + self.assertEqual('', server.metadata['k0']) + self.assertIn('k1', server.metadata) + self.assertEqual('v1.1', server.metadata['k1']) + self.assertIn('k2', server.metadata) + self.assertEqual('v2', server.metadata['k2']) + + # delete all metadata (cleanup) + self.user_cloud.compute.delete_server_metadata( - test_server, test_server.metadata.keys() + server, server.metadata.keys() ) - test_server = self.user_cloud.compute.get_server_metadata(test_server) - self.assertFalse(test_server.metadata) + server = self.user_cloud.compute.get_server_metadata(server) + self.assertFalse(server.metadata) def test_server_remote_console(self): + # create network for server + + network, subnet = test_network.create_network( + self.user_cloud, self.server_name, self.cidr + ) + self.assertIsNotNone(network) + self.addCleanup(self._delete_network, network, subnet) + + # create server + + server = self.user_cloud.compute.create_server( + name=self.server_name, + flavor_id=self.flavor.id, + image_id=self.image.id, + networks=[{"uuid": network.id}], + ) + self.user_cloud.compute.wait_for_server( + server, wait=self._wait_for_timeout + ) + self.assertIsInstance(server, _server.Server) + self.addCleanup(self._delete_server, server) + + # create remote console + console = self.user_cloud.compute.create_server_remote_console( - self.server, protocol='vnc', type='novnc' + server, protocol='vnc', type='novnc' ) self.assertEqual('vnc', console.protocol) self.assertEqual('novnc', console.type) From e4e979158238a473c4a0d403b71dc74b3e42f3b9 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 26 Jun 2025 16:43:31 +0100 Subject: [PATCH 3747/3836] tests: Streamline services functional test We also remove some tests that have been long-since skipped due to their tendency to break other tests. Change-Id: Iac413f99a99605b6e5586677cc77240994cc470e Signed-off-by: Stephen Finucane --- .../functional/compute/v2/test_service.py | 40 ++++--------------- 1 file changed, 8 insertions(+), 32 deletions(-) diff --git a/openstack/tests/functional/compute/v2/test_service.py b/openstack/tests/functional/compute/v2/test_service.py index 8c7787f7f..cd5a61a76 100644 --- a/openstack/tests/functional/compute/v2/test_service.py +++ b/openstack/tests/functional/compute/v2/test_service.py @@ -14,36 +14,12 @@ class TestService(base.BaseFunctionalTest): - def test_list(self): - sot = list(self.operator_cloud.compute.services()) - self.assertIsNotNone(sot) + def test_service(self): + # list all services + services = list(self.operator_cloud.compute.services()) + self.assertIsNotNone(services) - def test_disable_enable(self): - self.skipTest("Test is breaking tests that follow") - for srv in self.operator_cloud.compute.services(): - # only nova-compute can be updated - if srv.name == 'nova-compute': - self.operator_cloud.compute.disable_service(srv) - self.operator_cloud.compute.enable_service(srv) - - def test_update(self): - self.skipTest("Test is breaking tests that follow") - for srv in self.operator_cloud.compute.services(): - if srv.name == 'nova-compute': - self.operator_cloud.compute.update_service_forced_down( - srv, None, None, True - ) - self.operator_cloud.compute.update_service_forced_down( - srv, srv.host, srv.binary, False - ) - self.operator_cloud.compute.update_service( - srv, status='enabled' - ) - - def test_find(self): - for srv in self.operator_cloud.compute.services(): - if srv.name != 'nova-conductor': - # In devstack there are 2 nova-conductor instances on same host - self.operator_cloud.compute.find_service( - srv.name, host=srv.host, ignore_missing=False - ) + # find a service + self.operator_cloud.compute.find_service( + services[0].name, host=services[0].host, ignore_missing=False + ) From d8ae2e1824145b11c23f8de472b4d330b70314a7 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 26 Jun 2025 18:55:28 +0100 Subject: [PATCH 3748/3836] tests: Cleanup volume attachment tests Change-Id: Id4533ca18ec8a862b8e043c2a7894c5dc025d2e2 Signed-off-by: Stephen Finucane --- .../compute/v2/test_volume_attachment.py | 78 +++++++------------ 1 file changed, 29 insertions(+), 49 deletions(-) diff --git a/openstack/tests/functional/compute/v2/test_volume_attachment.py b/openstack/tests/functional/compute/v2/test_volume_attachment.py index 1e357c855..c4281930c 100644 --- a/openstack/tests/functional/compute/v2/test_volume_attachment.py +++ b/openstack/tests/functional/compute/v2/test_volume_attachment.py @@ -10,13 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.block_storage.v3 import volume as volume_ -from openstack.compute.v2 import server as server_ -from openstack.compute.v2 import volume_attachment as volume_attachment_ -from openstack.tests.functional.compute import base as ft_base +from openstack.block_storage.v3 import volume as _volume +from openstack.compute.v2 import server as _server +from openstack.compute.v2 import volume_attachment as _volume_attachment +from openstack.tests.functional.compute import base -class TestServerVolumeAttachment(ft_base.BaseComputeTest): +class TestServerVolumeAttachment(base.BaseComputeTest): def setUp(self): super().setUp() @@ -35,57 +35,48 @@ def setUp(self): networks='none', ) self.user_cloud.compute.wait_for_server( - server, - wait=self._wait_for_timeout, + server, wait=self._wait_for_timeout ) - self.assertIsInstance(server, server_.Server) + self.addCleanup(self._delete_server, server) + self.assertIsInstance(server, _server.Server) self.assertEqual(self.server_name, server.name) volume = self.user_cloud.block_storage.create_volume( - name=self.volume_name, - size=1, + name=self.volume_name, size=1 ) self.user_cloud.block_storage.wait_for_status( - volume, - status='available', - wait=self._wait_for_timeout, + volume, status='available', wait=self._wait_for_timeout ) - self.assertIsInstance(volume, volume_.Volume) + self.addCleanup(self._delete_volume, volume) + self.assertIsInstance(volume, _volume.Volume) self.assertEqual(self.volume_name, volume.name) self.server = server self.volume = volume - def tearDown(self): - self.user_cloud.compute.delete_server(self.server.id) + def _delete_server(self, server): + self.user_cloud.compute.delete_server(server.id) self.user_cloud.compute.wait_for_delete( - self.server, - wait=self._wait_for_timeout, + server, wait=self._wait_for_timeout ) - self.user_cloud.block_storage.delete_volume(self.volume.id) + def _delete_volume(self, volume): + self.user_cloud.block_storage.delete_volume(volume.id) self.user_cloud.block_storage.wait_for_delete( - self.volume, - wait=self._wait_for_timeout, + volume, wait=self._wait_for_timeout ) - super().tearDown() - def test_volume_attachment(self): # create the volume attachment volume_attachment = self.user_cloud.compute.create_volume_attachment( - self.server, - self.volume, + self.server, self.volume ) self.assertIsInstance( - volume_attachment, - volume_attachment_.VolumeAttachment, + volume_attachment, _volume_attachment.VolumeAttachment ) self.user_cloud.block_storage.wait_for_status( - self.volume, - status='in-use', - wait=self._wait_for_timeout, + self.volume, status='in-use', wait=self._wait_for_timeout ) # list all attached volume attachments (there should only be one) @@ -95,55 +86,44 @@ def test_volume_attachment(self): ) self.assertEqual(1, len(volume_attachments)) self.assertIsInstance( - volume_attachments[0], - volume_attachment_.VolumeAttachment, + volume_attachments[0], _volume_attachment.VolumeAttachment ) # update the volume attachment volume_attachment = self.user_cloud.compute.update_volume_attachment( - self.server, - self.volume, - delete_on_termination=True, + self.server, self.volume, delete_on_termination=True ) self.assertIsInstance( - volume_attachment, - volume_attachment_.VolumeAttachment, + volume_attachment, _volume_attachment.VolumeAttachment ) # retrieve details of the (updated) volume attachment volume_attachment = self.user_cloud.compute.get_volume_attachment( - self.server, - self.volume, + self.server, self.volume ) self.assertIsInstance( - volume_attachment, - volume_attachment_.VolumeAttachment, + volume_attachment, _volume_attachment.VolumeAttachment ) self.assertTrue(volume_attachment.delete_on_termination) # delete the volume attachment result = self.user_cloud.compute.delete_volume_attachment( - self.server, - self.volume, - ignore_missing=False, + self.server, self.volume, ignore_missing=False ) self.assertIsNone(result) self.user_cloud.block_storage.wait_for_status( - self.volume, - status='available', - wait=self._wait_for_timeout, + self.volume, status='available', wait=self._wait_for_timeout ) # Wait for the attachment to be deleted. # This is done to prevent a race between the BDM # record being deleted and we trying to delete the server. self.user_cloud.compute.wait_for_delete( - volume_attachment, - wait=self._wait_for_timeout, + volume_attachment, wait=self._wait_for_timeout ) # Verify the server doesn't have any volume attachment From 1649ed0cdaec9bd408082a33dab0c333c1c3fd99 Mon Sep 17 00:00:00 2001 From: Goutham Pacha Ravi Date: Thu, 31 Jul 2025 22:24:56 -0700 Subject: [PATCH 3749/3836] Replace CLA with DCO Signed-off-by: Goutham Pacha Ravi Change-Id: Ieaf9c3925f8fcba60fc4aef11d691941c38d444b --- CONTRIBUTING.rst | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 790776c3e..047e59e65 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -7,23 +7,27 @@ Contributing to openstacksdk If you're interested in contributing to the openstacksdk project, the following will help get you started. -Contributor License Agreement ------------------------------ +Developer Certificate of Origin +------------------------------- .. index:: single: license; agreement -In order to contribute to the openstacksdk project, you need to have -signed OpenStack's contributor's agreement. +In order to contribute to the Tacker-Horizon project, you need to adhere +to the `Developer Certificate of Origin`_. OpenStack utilizes the Developer +Certificate of Origin (DCO) as a lightweight means to confirm that you are +entitled to contribute the code you submit. This ensures that you are +providing your contributions under the project’s license and that you have +the right to do so. Please read `DeveloperWorkflow`_ before sending your first patch for review. Pull requests submitted through GitHub will be ignored. .. seealso:: - * https://wiki.openstack.org/wiki/How_To_Contribute - * https://wiki.openstack.org/wiki/CLA + * https://docs.openstack.org/contributors/common/dco.html +.. _Developer Certificate of Origin: https://developercertificate.org/ .. _DeveloperWorkflow: https://docs.openstack.org/infra/manual/developers.html#development-workflow Project Hosting Details From 61323654ebc0a8c8bd64cc4adfef8fd695d311bf Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 1 Aug 2025 14:32:19 +0100 Subject: [PATCH 3750/3836] tests: Use binary mode to open files A recent change to the CONTRIBUTING.rst file introduced unicode characters, which requests does not handle correctly when the file is open in text mode. requests 3.0 is removing support for opening files in text mode so we kill two birds with one stone here by switching to opening the file in binary mode. We also remove the unicode character (I tested the fix separately), correct the project name in the document, and use the raise from syntax to get better error handling in these cases. Change-Id: I47d65383233e23ec5edf1e6735c39c66eeafb804 Signed-off-by: Stephen Finucane --- CONTRIBUTING.rst | 4 ++-- openstack/image/v2/_proxy.py | 6 ++++-- openstack/tests/functional/image/v2/test_image.py | 5 ++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 047e59e65..1c61eb426 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -13,11 +13,11 @@ Developer Certificate of Origin .. index:: single: license; agreement -In order to contribute to the Tacker-Horizon project, you need to adhere +In order to contribute to the openstacksdk project, you need to adhere to the `Developer Certificate of Origin`_. OpenStack utilizes the Developer Certificate of Origin (DCO) as a lightweight means to confirm that you are entitled to contribute the code you submit. This ensures that you are -providing your contributions under the project’s license and that you have +providing your contributions under the project's license and that you have the right to do so. Please read `DeveloperWorkflow`_ before sending your first patch for review. diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 988ef1f86..004d09f0f 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -632,7 +632,9 @@ def _upload_image( self.log.debug("Image creation failed", exc_info=True) raise except Exception as e: - raise exceptions.SDKException(f"Image creation failed: {str(e)}") + raise exceptions.SDKException( + f"Image creation failed: {str(e)}" + ) from e def _make_v2_image_params(self, meta, properties): ret: dict = {} @@ -820,7 +822,7 @@ def _upload_image_task( raise exceptions.SDKException( f"Image creation failed: {e.message}", extra_data=glance_task, - ) + ) from e finally: # Clean up after ourselves. The object we created is not # needed after the import is done. diff --git a/openstack/tests/functional/image/v2/test_image.py b/openstack/tests/functional/image/v2/test_image.py index 7eafe828c..a5b694c66 100644 --- a/openstack/tests/functional/image/v2/test_image.py +++ b/openstack/tests/functional/image/v2/test_image.py @@ -22,6 +22,9 @@ class TestImage(base.BaseImageTest): def setUp(self): super().setUp() + with open('CONTRIBUTING.rst', 'rb') as fh: + data = fh.read() + # there's a limit on name length self.image = self.operator_cloud.image.create_image( name=TEST_IMAGE_NAME, @@ -30,7 +33,7 @@ def setUp(self): properties={ 'description': 'This is not an image', }, - data=open('CONTRIBUTING.rst'), + data=data, ) self.assertIsInstance(self.image, _image.Image) self.assertEqual(TEST_IMAGE_NAME, self.image.name) From 2d8ff660307d5af10b3f62a4b96f225485dbd7f3 Mon Sep 17 00:00:00 2001 From: Mridula Joshi Date: Tue, 13 Aug 2024 18:11:10 +0000 Subject: [PATCH 3751/3836] Add SDK support for ``glance image-tasks`` This patch adds the missing sdk support for the image-tasks to retrieve tasks associated to a particular image Change-Id: Ifa4d7ca58da98a96b86dc7a55a87fbd78f8d45bd Signed-off-by: Cyril Roelandt --- doc/source/user/proxies/image_v2.rst | 7 +++ openstack/image/v2/_proxy.py | 14 +++++ openstack/image/v2/image_tasks.py | 57 ++++++++++++++++++ .../tests/unit/image/v2/test_image_tasks.py | 60 +++++++++++++++++++ openstack/tests/unit/image/v2/test_proxy.py | 9 +++ .../get-image-tasks-c66a05c2c67976db.yaml | 5 ++ 6 files changed, 152 insertions(+) create mode 100644 openstack/image/v2/image_tasks.py create mode 100644 openstack/tests/unit/image/v2/test_image_tasks.py create mode 100644 releasenotes/notes/get-image-tasks-c66a05c2c67976db.yaml diff --git a/doc/source/user/proxies/image_v2.rst b/doc/source/user/proxies/image_v2.rst index e0b5caeec..2d3fd442f 100644 --- a/doc/source/user/proxies/image_v2.rst +++ b/doc/source/user/proxies/image_v2.rst @@ -22,6 +22,13 @@ Image Operations deactivate_image, reactivate_image, stage_image, add_tag, remove_tag +Image Task Operations +^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.image.v2._proxy.Proxy + :noindex: + :members: image_tasks + Member Operations ^^^^^^^^^^^^^^^^^ diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 004d09f0f..6977bc370 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -18,6 +18,7 @@ from openstack import exceptions from openstack.image.v2 import cache as _cache from openstack.image.v2 import image as _image +from openstack.image.v2 import image_tasks as _image_tasks from openstack.image.v2 import member as _member from openstack.image.v2 import metadef_namespace as _metadef_namespace from openstack.image.v2 import metadef_object as _metadef_object @@ -1021,6 +1022,19 @@ def update_image_properties( return True + def image_tasks(self, image): + """Return a generator of Image Tasks + + :param image: The value can be either the name of an image or a + :class:`~openstack.image.v2.image.Image` instance. + :return: A generator object of image tasks + :rtype: :class: ~openstack.image.v2.image_tasks.ImageTasks + :raises: :class:`~openstack.exceptions.NotFoundException` + when no resource can be found. + """ + image_id = resource.Resource._get_id(image) + return self._list(_image_tasks.ImageTasks, image_id=image_id) + def add_tag(self, image, tag): """Add a tag to an image diff --git a/openstack/image/v2/image_tasks.py b/openstack/image/v2/image_tasks.py new file mode 100644 index 000000000..0608caad8 --- /dev/null +++ b/openstack/image/v2/image_tasks.py @@ -0,0 +1,57 @@ +# Copyright 2024 RedHat 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 openstack import resource + + +class ImageTasks(resource.Resource): + resources_key = 'tasks' + base_path = '/images/%(image_id)s/tasks' + + allow_list = True + + _max_microversion = '2.17' + + #: The type of task represented by this content + type = resource.Body('type') + #: The current status of this task. The value can be pending, processing, + #: success or failure + status = resource.Body('status') + #: An identifier for the owner of the task, usually the tenant ID + owner = resource.Body('owner') + #: The date and time when the task is subject to removal (ISO8601 format) + expires_at = resource.Body('expires_at') + #: The date and time when the task was created (ISO8601 format) + created_at = resource.Body('created_at') + #: The date and time when the task was updated (ISO8601 format) + updated_at = resource.Body('updated_at') + #: The date and time when the task was deleted (ISO8601 format) + deleted_at = resource.Body('deleted_at') + #: Whether the task was deleted + deleted = resource.Body('deleted') + #: The ID of the image associated to this task + image_id = resource.Body('image_id') + #: The request ID of the user message + request_id = resource.Body('request_id') + #: The user id associated with this task + user_id = resource.Body('user_id') + #: A JSON object specifying the input parameters to the task + input = resource.Body('input') + #: A JSON object specifying information about the ultimate outcome of the + #: task + result = resource.Body('result') + #: Human-readable text, possibly an empty string, usually displayed in a + #: error situation to provide more information about what has occurred + message = resource.Body('message') diff --git a/openstack/tests/unit/image/v2/test_image_tasks.py b/openstack/tests/unit/image/v2/test_image_tasks.py new file mode 100644 index 000000000..05e84ffef --- /dev/null +++ b/openstack/tests/unit/image/v2/test_image_tasks.py @@ -0,0 +1,60 @@ +# Copyright 2024 RedHat 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 testtools + +from openstack.image.v2 import image_tasks + +EXAMPLE = { + 'id': '56ab5f98-2bb7-44c7-bc05-52bde37eb53b', + 'type': 'import', + 'status': 'failure', + 'owner': '2858d31bc5f54f4db66e53ab905ef566', + 'expires_at': '2024-10-10T09:28:58.000000', + 'created_at': '2024-10-08T09:28:58.000000', + 'updated_at': '2024-10-08T09:28:58.000000', + 'deleted_at': None, + 'deleted': False, + 'image_id': '56a39162-730d-401c-8a77-11bc078cf3e2', + 'request_id': 'req-7d2f073c-f6f8-4807-9fdb-5ce6b10c65c5', + 'user_id': 'dec9b6d341ec481abddf1027576c2d60', + 'input': {'image_id': '56a39162-730d-401c-8a77-11bc078cf3e2'}, + 'result': None, + 'message': "Input does not contain 'import_from' field", +} + + +class TestImageTasks(testtools.TestCase): + def test_basic(self): + sot = image_tasks.ImageTasks() + self.assertEqual('/images/%(image_id)s/tasks', sot.base_path) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = image_tasks.ImageTasks(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['type'], sot.type) + self.assertEqual(EXAMPLE['status'], sot.status) + self.assertEqual(EXAMPLE['owner'], sot.owner) + self.assertEqual(EXAMPLE['expires_at'], sot.expires_at) + self.assertEqual(EXAMPLE['created_at'], sot.created_at) + self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) + self.assertEqual(EXAMPLE['deleted_at'], sot.deleted_at) + self.assertEqual(EXAMPLE['deleted'], sot.deleted) + self.assertEqual(EXAMPLE['image_id'], sot.image_id) + self.assertEqual(EXAMPLE['request_id'], sot.request_id) + self.assertEqual(EXAMPLE['user_id'], sot.user_id) + self.assertEqual(EXAMPLE['input'], sot.input) + self.assertEqual(EXAMPLE['result'], sot.result) + self.assertEqual(EXAMPLE['message'], sot.message) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 3ce35672e..6d8a6edc0 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -21,6 +21,7 @@ from openstack.image.v2 import _proxy from openstack.image.v2 import cache as _cache from openstack.image.v2 import image as _image +from openstack.image.v2 import image_tasks as _image_tasks from openstack.image.v2 import member as _member from openstack.image.v2 import metadef_namespace as _metadef_namespace from openstack.image.v2 import metadef_object as _metadef_object @@ -605,6 +606,14 @@ def test_reactivate_image(self): expected_args=[self.proxy], ) + def test_image_tasks(self): + self.verify_list( + self.proxy.image_tasks, + _image_tasks.ImageTasks, + method_kwargs={'image': 'image_1'}, + expected_kwargs={'image_id': 'image_1'}, + ) + class TestMember(TestImageProxy): def test_member_create(self): diff --git a/releasenotes/notes/get-image-tasks-c66a05c2c67976db.yaml b/releasenotes/notes/get-image-tasks-c66a05c2c67976db.yaml new file mode 100644 index 000000000..a6841099d --- /dev/null +++ b/releasenotes/notes/get-image-tasks-c66a05c2c67976db.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Adds operation which retrieves tasks associated + to a particular image. From 53e19fe0a2bd3e4dfe6721ebc4d944747137663d Mon Sep 17 00:00:00 2001 From: lajoskatona Date: Wed, 6 Aug 2025 12:46:04 +0200 Subject: [PATCH 3752/3836] Add id to query_mapping for Networking Some of the basic Networking resources miss the id field in the query_mapping, this patch fixes that. Change-Id: I15aa570cdc9eacd8711478e96ae1efb239511623 Signed-off-by: lajoskatona --- openstack/network/v2/floating_ip.py | 1 + openstack/network/v2/network.py | 1 + openstack/network/v2/qos_policy.py | 1 + openstack/network/v2/router.py | 1 + openstack/network/v2/subnet.py | 1 + openstack/tests/unit/network/v2/test_floating_ip.py | 1 + openstack/tests/unit/network/v2/test_network.py | 1 + 7 files changed, 7 insertions(+) diff --git a/openstack/network/v2/floating_ip.py b/openstack/network/v2/floating_ip.py index 562ce763e..b48538277 100644 --- a/openstack/network/v2/floating_ip.py +++ b/openstack/network/v2/floating_ip.py @@ -34,6 +34,7 @@ class FloatingIP(_base.NetworkResource, _base.TagMixinNetwork): 'fixed_ip_address', 'floating_ip_address', 'floating_network_id', + 'id', 'port_id', 'router_id', 'status', diff --git a/openstack/network/v2/network.py b/openstack/network/v2/network.py index 1abc37031..537b643e5 100644 --- a/openstack/network/v2/network.py +++ b/openstack/network/v2/network.py @@ -34,6 +34,7 @@ class Network(_base.NetworkResource, _base.TagMixinNetwork): 'project_id', 'sort_key', 'sort_dir', + 'id', ipv4_address_scope_id='ipv4_address_scope', ipv6_address_scope_id='ipv6_address_scope', is_admin_state_up='admin_state_up', diff --git a/openstack/network/v2/qos_policy.py b/openstack/network/v2/qos_policy.py index 5f75f1990..bc96fdbb1 100644 --- a/openstack/network/v2/qos_policy.py +++ b/openstack/network/v2/qos_policy.py @@ -31,6 +31,7 @@ class QoSPolicy(resource.Resource, tag.TagMixin): _query_mapping = resource.QueryParameters( 'name', 'description', + 'id', 'is_default', 'project_id', 'sort_key', diff --git a/openstack/network/v2/router.py b/openstack/network/v2/router.py index f5123c522..9a9eb3d2a 100644 --- a/openstack/network/v2/router.py +++ b/openstack/network/v2/router.py @@ -32,6 +32,7 @@ class Router(_base.NetworkResource, _base.TagMixinNetwork): _query_mapping = resource.QueryParameters( 'description', 'flavor_id', + 'id', 'name', 'status', 'project_id', diff --git a/openstack/network/v2/subnet.py b/openstack/network/v2/subnet.py index 959e29592..6e6720251 100644 --- a/openstack/network/v2/subnet.py +++ b/openstack/network/v2/subnet.py @@ -31,6 +31,7 @@ class Subnet(_base.NetworkResource, _base.TagMixinNetwork): 'cidr', 'description', 'gateway_ip', + 'id', 'ip_version', 'ipv6_address_mode', 'ipv6_ra_mode', diff --git a/openstack/tests/unit/network/v2/test_floating_ip.py b/openstack/tests/unit/network/v2/test_floating_ip.py index d2e20e429..c75309099 100644 --- a/openstack/tests/unit/network/v2/test_floating_ip.py +++ b/openstack/tests/unit/network/v2/test_floating_ip.py @@ -87,6 +87,7 @@ def test_make_it(self): 'fixed_ip_address': 'fixed_ip_address', 'floating_ip_address': 'floating_ip_address', 'floating_network_id': 'floating_network_id', + 'id': 'id', 'tags': 'tags', 'any_tags': 'tags-any', 'not_tags': 'not-tags', diff --git a/openstack/tests/unit/network/v2/test_network.py b/openstack/tests/unit/network/v2/test_network.py index f902dfec9..e7e964bf1 100644 --- a/openstack/tests/unit/network/v2/test_network.py +++ b/openstack/tests/unit/network/v2/test_network.py @@ -105,6 +105,7 @@ def test_make_it(self): 'limit': 'limit', 'marker': 'marker', 'description': 'description', + 'id': 'id', 'name': 'name', 'project_id': 'project_id', 'status': 'status', From 1f27246a73dda62e36f26d3a0839068ea7652a9f Mon Sep 17 00:00:00 2001 From: "thor.huhji" Date: Sat, 21 Jun 2025 15:24:13 +0900 Subject: [PATCH 3753/3836] Add support for dns blacklist CRUD - Add dns blacklist CRUD supports - Add unit test code for blacklist CRUD - Add blacklist docs - Add functional test for blacklist CRUD Change-Id: Idc003887abc0a120e382e3f49a77bf7c509b4c15 Signed-off-by: jihyun huh --- doc/source/user/proxies/dns.rst | 9 +++ doc/source/user/resources/dns/index.rst | 1 + .../user/resources/dns/v2/blacklist.rst | 12 ++++ openstack/dns/v2/_proxy.py | 59 ++++++++++++++++++ openstack/dns/v2/blacklist.py | 47 ++++++++++++++ .../tests/functional/dns/v2/test_blacklist.py | 61 +++++++++++++++++++ openstack/tests/unit/dns/v2/test_blacklist.py | 41 +++++++++++++ openstack/tests/unit/dns/v2/test_proxy.py | 30 +++++++++ 8 files changed, 260 insertions(+) create mode 100644 doc/source/user/resources/dns/v2/blacklist.rst create mode 100644 openstack/dns/v2/blacklist.py create mode 100644 openstack/tests/functional/dns/v2/test_blacklist.py create mode 100644 openstack/tests/unit/dns/v2/test_blacklist.py diff --git a/doc/source/user/proxies/dns.rst b/doc/source/user/proxies/dns.rst index cc46163a2..7e61198b5 100644 --- a/doc/source/user/proxies/dns.rst +++ b/doc/source/user/proxies/dns.rst @@ -82,3 +82,12 @@ Service Status Operations .. autoclass:: openstack.dns.v2._proxy.Proxy :noindex: :members: service_statuses, get_service_status + + +Blacklist Operations +^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.dns.v2._proxy.Proxy + :noindex: + :members: blacklists, get_blacklist, create_blacklist, + update_blacklist, delete_blacklist diff --git a/doc/source/user/resources/dns/index.rst b/doc/source/user/resources/dns/index.rst index f7cb0e96b..e1826ca61 100644 --- a/doc/source/user/resources/dns/index.rst +++ b/doc/source/user/resources/dns/index.rst @@ -13,3 +13,4 @@ DNS Resources v2/recordset v2/limit v2/service_status + v2/blacklist diff --git a/doc/source/user/resources/dns/v2/blacklist.rst b/doc/source/user/resources/dns/v2/blacklist.rst new file mode 100644 index 000000000..d483eb9a1 --- /dev/null +++ b/doc/source/user/resources/dns/v2/blacklist.rst @@ -0,0 +1,12 @@ +openstack.dns.v2.blacklist +========================== + +.. automodule:: openstack.dns.v2.blacklist + +The Blacklist Class +------------------- + +The ``Blacklist`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.dns.v2.blacklist.Blacklist + :members: diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py index c7c4539c7..a3d376b24 100644 --- a/openstack/dns/v2/_proxy.py +++ b/openstack/dns/v2/_proxy.py @@ -12,6 +12,7 @@ import typing as ty +from openstack.dns.v2 import blacklist as _blacklist from openstack.dns.v2 import floating_ip as _fip from openstack.dns.v2 import limit as _limit from openstack.dns.v2 import recordset as _rs @@ -29,6 +30,7 @@ class Proxy(proxy.Proxy): _resource_registry = { + "blacklist": _blacklist.Blacklist, "floating_ip": _fip.FloatingIP, "limits": _limit.Limit, "recordset": _rs.Recordset, @@ -891,3 +893,60 @@ def find_tsigkey(self, name_or_id, ignore_missing=True): return self._find( _tsigkey.TSIGKey, name_or_id, ignore_missing=ignore_missing ) + + # ======== Blacklists ======== + def blacklists(self, **query): + """Retrieve a generator of blacklists + + :returns: A generator of blacklist + (:class:`~openstack.dns.v2.blacklist.Blacklist`) instances + """ + return self._list(_blacklist.Blacklist, **query) + + def get_blacklist(self, blacklist): + """Get a blacklist + + :param blacklist: The value can be the ID of a blacklist + or a :class:`~openstack.dns.v2.blacklist.Blacklist` instance. + + :returns: Blacklist instance. + :rtype: :class:`~openstack.dns.v2.blacklist.Blacklist` + """ + return self._get(_blacklist.Blacklist, blacklist) + + def create_blacklist(self, **attrs): + """Create a new blacklist + + :param attrs: Keyword arguments which will be used to create + a :class:`~openstack.dns.v2.blacklist.Blacklist`, + comprised of the properties on the Blacklist class. + + :returns: The results of blacklist creation. + :rtype: :class:`~openstack.dns.v2.blacklist.Blacklist` + """ + return self._create(_blacklist.Blacklist, prepend_key=False, **attrs) + + def update_blacklist(self, blacklist, **attrs): + """Update blacklist attributes + + :param blacklist: The id or an instance of + :class: `~openstack.dns.v2.blacklist.Blacklist`. + :param attrs: attributes for update on + :class: `~openstack.dns.v2.blacklist.Blacklist`. + + :rtype: :class: `~openstack.dns.v2.blacklist.Blacklist`. + """ + return self._update(_blacklist.Blacklist, blacklist, **attrs) + + def delete_blacklist(self, blacklist, ignore_missing=True): + """Delete a blacklist + + :param blacklist: The id or an instance of + :class: `~openstack.dns.v2.blacklist.Blacklist`. + + :returns: Blacklist been deleted + :rtype: :class:`~openstack.dns.v2.blacklist.Blacklist` + """ + return self._delete( + _blacklist.Blacklist, blacklist, ignore_missing=ignore_missing + ) diff --git a/openstack/dns/v2/blacklist.py b/openstack/dns/v2/blacklist.py new file mode 100644 index 000000000..6c6d048d7 --- /dev/null +++ b/openstack/dns/v2/blacklist.py @@ -0,0 +1,47 @@ +# 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.dns.v2 import _base +from openstack import resource + + +class Blacklist(_base.Resource): + """DNS Blacklist Resource""" + + resources_key = 'blacklists' + base_path = '/blacklists' + + # capabilities + allow_list = True + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + commit_method = "PATCH" + + _query_mapping = resource.QueryParameters( + 'pattern', + ) + + #: Properties + #: ID for the resource + id = resource.Body('id') + #: Pattern for this blacklist + pattern = resource.Body('pattern') + #: Description for this blacklist + description = resource.Body("description") + #: Timestampe when the blacklist created + created_at = resource.Body("created_at") + #: Timestampe when the blacklist last updated + updated_at = resource.Body("updated_at") + #: Links to the resource, and the other related resources. + links = resource.Body("links") diff --git a/openstack/tests/functional/dns/v2/test_blacklist.py b/openstack/tests/functional/dns/v2/test_blacklist.py new file mode 100644 index 000000000..2ea931ae2 --- /dev/null +++ b/openstack/tests/functional/dns/v2/test_blacklist.py @@ -0,0 +1,61 @@ +# 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 uuid + +from openstack.dns.v2 import blacklist as _blacklist +from openstack.tests.functional import base + + +class TestBlackList(base.BaseFunctionalTest): + def setUp(self): + super().setUp() + self.require_service('dns') + + # Note: use a unique UUID pattern to avoid test collisions + self.pattern = rf".*\.test-{uuid.uuid4().hex}.com" + self.description = self.getUniqueString('blacklist') + + def _delete_blacklist(self, blacklist): + ret = self.operator_cloud.dns.delete_blacklist(blacklist.id) + self.assertIsNone(ret) + + def test_blacklist(self): + # create blacklist + blacklist = self.operator_cloud.dns.create_blacklist( + pattern=self.pattern, + description=self.description, + ) + self.assertIsNotNone(blacklist.id) + self.assertIsInstance(blacklist, _blacklist.Blacklist) + self.assertEqual(self.pattern, blacklist.pattern) + self.assertEqual(self.description, blacklist.description) + self.addCleanup(self._delete_blacklist, blacklist) + + # update blacklist + blacklist = self.operator_cloud.dns.update_blacklist( + blacklist, pattern=self.pattern, description=self.description + ) + self.assertIsInstance(blacklist, _blacklist.Blacklist) + self.assertEqual(self.pattern, blacklist.pattern) + self.assertEqual(self.description, blacklist.description) + + # get blacklist + blacklist = self.operator_cloud.dns.get_blacklist(blacklist.id) + self.assertIsInstance(blacklist, _blacklist.Blacklist) + self.assertEqual(self.pattern, blacklist.pattern) + self.assertEqual(self.description, blacklist.description) + + # list all blacklists + blacklists = list(self.operator_cloud.dns.blacklists()) + self.assertIsInstance(blacklists[0], _blacklist.Blacklist) + self.assertIn(self.pattern, {x.pattern for x in blacklists}) + self.operator_cloud.dns.delete_blacklist(blacklist.id) diff --git a/openstack/tests/unit/dns/v2/test_blacklist.py b/openstack/tests/unit/dns/v2/test_blacklist.py new file mode 100644 index 000000000..e7a166d3f --- /dev/null +++ b/openstack/tests/unit/dns/v2/test_blacklist.py @@ -0,0 +1,41 @@ +# 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.dns.v2 import blacklist +from openstack.tests.unit import base + +IDENTIFIER = '373cb85e-0f4a-487a-846e-dce7a65cca4d' +EXAMPLE = { + 'id': IDENTIFIER, + 'description': 'blacklist test description', + 'pattern': '.*example.com.', +} + + +class TestBlackList(base.TestCase): + def test_basic(self): + sot = blacklist.Blacklist() + self.assertEqual(None, sot.resource_key) + self.assertEqual('blacklists', sot.resources_key) + self.assertEqual('/blacklists', sot.base_path) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertEqual('PATCH', sot.commit_method) + + def test_make_it(self): + sot = blacklist.Blacklist(**EXAMPLE) + self.assertEqual(IDENTIFIER, sot.id) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['pattern'], sot.pattern) diff --git a/openstack/tests/unit/dns/v2/test_proxy.py b/openstack/tests/unit/dns/v2/test_proxy.py index aacc699ae..cb9541617 100644 --- a/openstack/tests/unit/dns/v2/test_proxy.py +++ b/openstack/tests/unit/dns/v2/test_proxy.py @@ -11,6 +11,7 @@ # under the License. from openstack.dns.v2 import _proxy +from openstack.dns.v2 import blacklist from openstack.dns.v2 import floating_ip from openstack.dns.v2 import recordset from openstack.dns.v2 import service_status @@ -363,3 +364,32 @@ def test_tsigkey_get(self): def test_tesigkeys(self): self.verify_list(self.proxy.tsigkeys, tsigkey.TSIGKey) + + +class TestDnsBlacklist(TestDnsProxy): + def test_blacklist_create(self): + self.verify_create( + self.proxy.create_blacklist, + blacklist.Blacklist, + method_kwargs={'pattern': '.*\.example\.com'}, + expected_kwargs={ + 'pattern': '.*\.example\.com', + 'prepend_key': False, + }, + ) + + def test_blacklist_delete(self): + self.verify_delete( + self.proxy.delete_blacklist, + blacklist.Blacklist, + ignore_missing=True, + ) + + def test_blacklist_update(self): + self.verify_update(self.proxy.update_blacklist, blacklist.Blacklist) + + def test_blacklist_get(self): + self.verify_get(self.proxy.get_blacklist, blacklist.Blacklist) + + def test_blacklists(self): + self.verify_list(self.proxy.blacklists, blacklist.Blacklist) From 53a9d0737f003f5f76a490d722e70c953f0f663e Mon Sep 17 00:00:00 2001 From: Kim-Yukyung Date: Wed, 20 Aug 2025 21:02:37 +0900 Subject: [PATCH 3754/3836] Add functional tests for identity endpoint CRUD This patch adds functional tests for OpenStack Identity (v3) endpoints - add endpoint CRUD operations - add filtering support for service_id, interface, region_id Change-Id: Ibc2d807dc88a0a62673766ba755738e787b12c3b Signed-off-by: Kim-Yukyung --- .../functional/identity/v3/test_endpoint.py | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 openstack/tests/functional/identity/v3/test_endpoint.py diff --git a/openstack/tests/functional/identity/v3/test_endpoint.py b/openstack/tests/functional/identity/v3/test_endpoint.py new file mode 100644 index 000000000..d37392683 --- /dev/null +++ b/openstack/tests/functional/identity/v3/test_endpoint.py @@ -0,0 +1,136 @@ +# 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.identity.v3 import endpoint as _endpoint +from openstack.tests.functional import base + + +class TestEndpoint(base.BaseFunctionalTest): + def setUp(self): + super().setUp() + + self.service_name = self.getUniqueString('service') + self.service_type = self.getUniqueString('type') + self.service = self.operator_cloud.identity.create_service( + name=self.service_name, + type=self.service_type, + ) + self.addCleanup( + self.operator_cloud.identity.delete_service, self.service + ) + + self.region_name = self.getUniqueString('region') + self.region = self.operator_cloud.identity.create_region( + name=self.region_name + ) + self.addCleanup( + self.operator_cloud.identity.delete_region, self.region + ) + + unique_base = self.getUniqueString('endpoint') + self.test_url = f'https://{unique_base}.example.com/v1' + self.updated_url = f'https://{unique_base}.example.com/v2' + + def _delete_endpoint(self, endpoint): + ret = self.operator_cloud.identity.delete_endpoint(endpoint) + self.assertIsNone(ret) + + def test_endpoint(self): + # Create public endpoint + public_endpoint = self.operator_cloud.identity.create_endpoint( + service_id=self.service.id, + interface='public', + url=self.test_url, + region_id=self.region.id, + is_enabled=True, + ) + self.addCleanup(self._delete_endpoint, public_endpoint) + self.assertIsInstance(public_endpoint, _endpoint.Endpoint) + self.assertIsNotNone(public_endpoint.id) + self.assertEqual(self.service.id, public_endpoint.service_id) + self.assertEqual('public', public_endpoint.interface) + self.assertEqual(self.test_url, public_endpoint.url) + self.assertEqual(self.region.id, public_endpoint.region_id) + self.assertTrue(public_endpoint.is_enabled) + + # Create internal endpoint for filter testing + internal_endpoint = self.operator_cloud.identity.create_endpoint( + service_id=self.service.id, + interface='internal', + url=self.test_url, + region_id=self.region.id, + ) + self.addCleanup(self._delete_endpoint, internal_endpoint) + self.assertIsInstance(internal_endpoint, _endpoint.Endpoint) + self.assertIsNotNone(internal_endpoint.id) + self.assertEqual('internal', internal_endpoint.interface) + + # Update public endpoint + public_endpoint = self.operator_cloud.identity.update_endpoint( + public_endpoint, + url=self.updated_url, + is_enabled=False, + ) + self.assertIsInstance(public_endpoint, _endpoint.Endpoint) + self.assertEqual(self.updated_url, public_endpoint.url) + self.assertFalse(public_endpoint.is_enabled) + + # Get endpoint by ID + public_endpoint = self.operator_cloud.identity.get_endpoint( + public_endpoint.id + ) + self.assertIsInstance(public_endpoint, _endpoint.Endpoint) + self.assertEqual(self.updated_url, public_endpoint.url) + self.assertFalse(public_endpoint.is_enabled) + + # Find endpoint + found_endpoint = self.operator_cloud.identity.find_endpoint( + public_endpoint.id + ) + self.assertIsInstance(found_endpoint, _endpoint.Endpoint) + self.assertEqual(public_endpoint.id, found_endpoint.id) + + # List endpoints + endpoints = list(self.operator_cloud.identity.endpoints()) + self.assertIsInstance(endpoints[0], _endpoint.Endpoint) + endpoint_ids = {ep.id for ep in endpoints} + self.assertIn(public_endpoint.id, endpoint_ids) + self.assertIn(internal_endpoint.id, endpoint_ids) + + # Test service filter + service_endpoints = list( + self.operator_cloud.identity.endpoints(service_id=self.service.id) + ) + service_endpoint_ids = {ep.id for ep in service_endpoints} + self.assertIn(public_endpoint.id, service_endpoint_ids) + self.assertIn(internal_endpoint.id, service_endpoint_ids) + + # Test interface filter + public_endpoints = list( + self.operator_cloud.identity.endpoints(interface='public') + ) + public_endpoint_ids = {ep.id for ep in public_endpoints} + self.assertIn(public_endpoint.id, public_endpoint_ids) + + internal_endpoints = list( + self.operator_cloud.identity.endpoints(interface='internal') + ) + internal_endpoint_ids = {ep.id for ep in internal_endpoints} + self.assertIn(internal_endpoint.id, internal_endpoint_ids) + + # Test region filter + region_endpoints = list( + self.operator_cloud.identity.endpoints(region_id=self.region.id) + ) + region_endpoint_ids = {ep.id for ep in region_endpoints} + self.assertIn(public_endpoint.id, region_endpoint_ids) + self.assertIn(internal_endpoint.id, region_endpoint_ids) From 68aaecee417a86fdc68423a7411e73eb785fdb2d Mon Sep 17 00:00:00 2001 From: jbeen Date: Sun, 17 Aug 2025 16:09:02 +0900 Subject: [PATCH 3755/3836] tests: Add functional test for image members Add a functional test for image member operations with admin and demo contexts covering add, list, get, update to accepted and remove. Change-Id: I242ea5295981b215a684ec6d39439c06fcf6f510 Signed-off-by: jbeen --- .../tests/functional/image/v2/test_member.py | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 openstack/tests/functional/image/v2/test_member.py diff --git a/openstack/tests/functional/image/v2/test_member.py b/openstack/tests/functional/image/v2/test_member.py new file mode 100644 index 000000000..ad3bc71a4 --- /dev/null +++ b/openstack/tests/functional/image/v2/test_member.py @@ -0,0 +1,87 @@ +# 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 as sdk_exc +from openstack.image.v2 import image as _image +from openstack.image.v2 import member as _member +from openstack.tests.functional.image.v2 import base + + +TEST_IMAGE_NAME = 'Test Image for Sharing' +MEMBER_STATUS_PENDING = 'pending' +MEMBER_STATUS_ACCEPTED = 'accepted' + + +class TestImageMember(base.BaseImageTest): + def setUp(self): + super().setUp() + + # NOTE(jbeen): 1-byte dummy image data for sharing tests; not bootable. + self.image = self.operator_cloud.image.create_image( + name=TEST_IMAGE_NAME, + disk_format='raw', + container_format='bare', + visibility='shared', + data=b'0', + ) + self.assertIsInstance(self.image, _image.Image) + self.assertEqual(TEST_IMAGE_NAME, self.image.name) + + self.member_id = self.user_cloud.session.get_project_id() + self.assertIsNotNone(self.member_id) + + def tearDown(self): + self.operator_cloud.image.delete_image(self.image) + self.operator_cloud.image.wait_for_delete(self.image) + + super().tearDown() + + def test_image_members(self): + # add member + member = self.operator_cloud.image.add_member( + image=self.image, member=self.member_id + ) + self.assertIsInstance(member, _member.Member) + self.assertEqual(self.member_id, member.member_id) + self.assertEqual(MEMBER_STATUS_PENDING, member.status) + + # get member + member = self.operator_cloud.image.get_member( + image=self.image, member=self.member_id + ) + self.assertIsInstance(member, _member.Member) + self.assertEqual(self.member_id, member.member_id) + + # list members + members = list(self.operator_cloud.image.members(image=self.image)) + self.assertIn(self.member_id, {m.id for m in members}) + + # update member + member = self.user_cloud.image.update_member( + image=self.image, + member=self.member_id, + status=MEMBER_STATUS_ACCEPTED, + ) + self.assertIsInstance(member, _member.Member) + self.assertEqual(self.member_id, member.member_id) + self.assertEqual(MEMBER_STATUS_ACCEPTED, member.status) + + # remove member + self.operator_cloud.image.remove_member( + image=self.image, member=self.member_id + ) + self.assertRaises( + sdk_exc.NotFoundException, + self.operator_cloud.image.get_member, + image=self.image, + member=self.member_id, + ) From bf8bdb04133f8112d46dfe42cf1863e7b3b4b8f3 Mon Sep 17 00:00:00 2001 From: Luan Utimura Date: Thu, 28 Aug 2025 15:18:29 -0300 Subject: [PATCH 3756/3836] volume: Add missing backup_id field to Volume Change-Id: I4a05f571b1c0eb1bfd2d4e92ba3983c1c86eb072 Signed-off-by: Luan Utimura Depends-on: https://review.opendev.org/c/openstack/python-openstackclient/+/958872 --- openstack/block_storage/v3/volume.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index 7f5a0ef2d..04a0b0cec 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -49,6 +49,9 @@ class Volume(resource.Resource, metadata.MetadataMixin): attachments = resource.Body("attachments", type=list) #: The availability zone. availability_zone = resource.Body("availability_zone") + #: To create a volume from an existing backup, specify the ID of + #: the existing volume backup. (since 3.47) + backup_id = resource.Body("backup_id") #: ID of the consistency group. consistency_group_id = resource.Body("consistencygroup_id") #: Whether this resource consumes quota or not. Resources that not counted From 03649beb7939a72b8ede9fcb592e7b244af596f7 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Thu, 4 Sep 2025 13:38:10 +0000 Subject: [PATCH 3757/3836] Update master for stable/2025.2 Add file to the reno documentation build to show release notes for stable/2025.2. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/2025.2. Sem-Ver: feature Change-Id: I671ae5d01f0504655d6b0d3892703eb0952a6fb4 Signed-off-by: OpenStack Release Bot Generated-By: openstack/project-config:roles/copy-release-tools-scripts/files/release-tools/add_release_note_page.sh --- releasenotes/source/2025.2.rst | 6 ++++++ releasenotes/source/index.rst | 1 + 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/2025.2.rst diff --git a/releasenotes/source/2025.2.rst b/releasenotes/source/2025.2.rst new file mode 100644 index 000000000..4dae18d86 --- /dev/null +++ b/releasenotes/source/2025.2.rst @@ -0,0 +1,6 @@ +=========================== +2025.2 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2025.2 diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 8326e0dbc..f2270bc5a 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ :maxdepth: 1 unreleased + 2025.2 2025.1 2024.2 2024.1 From c2b66e54bf26a1ea85050aa33fbb7103c5f0bd45 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 8 Sep 2025 16:43:41 +0100 Subject: [PATCH 3758/3836] image: Provide default import method We should default to 'glance-direct' as we did previously. Change-Id: Ie1665f9f6226a64a125c1c88b12410d00f863129 Signed-off-by: Stephen Finucane Closes-bug: #2122344 --- openstack/image/v2/_proxy.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 6977bc370..46bc17e80 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -679,6 +679,9 @@ def _upload_image_put( if stores or all_stores or all_stores_must_succeed: use_import = True + if use_import and not import_method: + import_method = 'glance-direct' + if filename and not data: image_data = open(filename, 'rb') else: From 8db08a11bf3a71b15db3f1f1781e48c16470087c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 9 Sep 2025 16:20:38 +0100 Subject: [PATCH 3759/3836] Make ironic job non-voting The following tests are currently failing: * openstack.tests.functional.baremetal.test_baremetal_allocation.TestBareMetalAllocationUpdate.test_allocation_update * openstack.tests.functional.baremetal.test_baremetal_allocation.TestBareMetalAllocationUpdate.test_allocation_patch Both with the following error: Schema error: Additional properties are not allowed ('owner' was unexpected) I have not been able to set up an environment locally to get to the bottom of the issue yet so let's make this non-voting so we can get a fix for the last release in. Change-Id: I225465fa59ca7e3b16ed43b1dd6561172971e59e Signed-off-by: Stephen Finucane --- zuul.d/project.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index a677dd5e0..407a300a1 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -27,7 +27,10 @@ voting: false - openstacksdk-functional-devstack-manila - openstacksdk-functional-devstack-masakari - - openstacksdk-functional-devstack-ironic + # TODO(stephenfin): Make this voting once we get to the bottom of the + # 2 failures + - openstacksdk-functional-devstack-ironic: + voting: false - osc-functional-devstack-tips: voting: false - ansible-collections-openstack-functional-devstack: @@ -38,4 +41,5 @@ - openstacksdk-functional-devstack - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-networking-ext - - openstacksdk-functional-devstack-ironic + # TODO(stephenfin): Per above + # - openstacksdk-functional-devstack-ironic From 74ba5f3732170d2538a9b5bb065277eb94150983 Mon Sep 17 00:00:00 2001 From: Michal Nasiadka Date: Tue, 9 Sep 2025 19:36:53 +0200 Subject: [PATCH 3760/3836] Remove dib-nodepool-functional-openstack-ubuntu-noble-src It's getting removed from dib after being replaced with devstack job. Change-Id: I7ad86b72b892eca4d7c154759b14dd642bd8ff71 Signed-off-by: Michal Nasiadka --- zuul.d/project.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index a677dd5e0..94ed6a7af 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -18,8 +18,6 @@ - opendev-buildset-registry - nodepool-build-image-siblings: voting: false - - dib-nodepool-functional-openstack-ubuntu-noble-src: - voting: false - openstacksdk-functional-devstack - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-networking-ext From 633c73018ee6a029eea9faaa2311a8e46e45bb5d Mon Sep 17 00:00:00 2001 From: chjung99 Date: Sat, 31 Aug 2024 14:50:15 +0900 Subject: [PATCH 3761/3836] dns: Add support for TLDs - Add DNS TLD resource to the SDK - Implement proxy methods: create_tld(), find_tld(), get_tld(), update_tld(), delete_tld(), tlds() Change-Id: I9e61ac2e7984e7594dea701adee1415db5093bf6 Signed-off-by: chjung99 Co-authored-by: Kim Yubin Co-authored-by: Stephen Finucane --- doc/source/user/proxies/dns.rst | 7 ++ doc/source/user/resources/dns/index.rst | 1 + doc/source/user/resources/dns/v2/tld.rst | 12 +++ openstack/dns/v2/_proxy.py | 83 +++++++++++++++++++ openstack/dns/v2/tld.py | 49 +++++++++++ openstack/tests/functional/dns/v2/test_tld.py | 61 ++++++++++++++ openstack/tests/unit/dns/v2/test_proxy.py | 31 +++++++ openstack/tests/unit/dns/v2/test_tld.py | 66 +++++++++++++++ .../notes/add-dns-tld-d3cfac70f76637e3.yaml | 5 ++ 9 files changed, 315 insertions(+) create mode 100644 doc/source/user/resources/dns/v2/tld.rst create mode 100644 openstack/dns/v2/tld.py create mode 100644 openstack/tests/functional/dns/v2/test_tld.py create mode 100644 openstack/tests/unit/dns/v2/test_tld.py create mode 100644 releasenotes/notes/add-dns-tld-d3cfac70f76637e3.yaml diff --git a/doc/source/user/proxies/dns.rst b/doc/source/user/proxies/dns.rst index 7e61198b5..a629375ed 100644 --- a/doc/source/user/proxies/dns.rst +++ b/doc/source/user/proxies/dns.rst @@ -51,6 +51,13 @@ FloatingIP Operations :noindex: :members: floating_ips, get_floating_ip, update_floating_ip +TLD Operations +^^^^^^^^^^^^^^ + +.. autoclass:: openstack.dns.v2._proxy.Proxy + :noindex: + :members: create_tld, delete_tld, get_tld, find_tld, tlds + Zone Transfer Operations ^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/dns/index.rst b/doc/source/user/resources/dns/index.rst index e1826ca61..90a58a82d 100644 --- a/doc/source/user/resources/dns/index.rst +++ b/doc/source/user/resources/dns/index.rst @@ -10,6 +10,7 @@ DNS Resources v2/zone_import v2/zone_share v2/floating_ip + v2/tld v2/recordset v2/limit v2/service_status diff --git a/doc/source/user/resources/dns/v2/tld.rst b/doc/source/user/resources/dns/v2/tld.rst new file mode 100644 index 000000000..bcafc5cc9 --- /dev/null +++ b/doc/source/user/resources/dns/v2/tld.rst @@ -0,0 +1,12 @@ +openstack.dns.v2.tld +============================== + +.. automodule:: openstack.dns.v2.tld + +The TLD Class +-------------- + +The ``DNS`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.dns.v2.tld.TLD + :members: diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py index a3d376b24..a28f235f0 100644 --- a/openstack/dns/v2/_proxy.py +++ b/openstack/dns/v2/_proxy.py @@ -17,6 +17,7 @@ from openstack.dns.v2 import limit as _limit from openstack.dns.v2 import recordset as _rs from openstack.dns.v2 import service_status as _svc_status +from openstack.dns.v2 import tld as _tld from openstack.dns.v2 import tsigkey as _tsigkey from openstack.dns.v2 import zone as _zone from openstack.dns.v2 import zone_export as _zone_export @@ -42,6 +43,7 @@ class Proxy(proxy.Proxy): "zone_nameserver": _zone_nameserver.ZoneNameserver, "zone_share": _zone_share.ZoneShare, "zone_transfer_request": _zone_transfer.ZoneTransferRequest, + "tld": _tld.TLD, } # ======== Zones ======== @@ -717,6 +719,87 @@ def get_service_status(self, service): """ return self._get(_svc_status.ServiceStatus, service) + # ======== TLDs ======== + def tlds(self, **query): + """Retrieve a generator of tlds + + :param dict query: Optional query parameters to be sent to limit the + resources being returned. + + * `name`: TLD Name field. + + :returns: A generator of tld + :class:`~openstack.dns.v2.tld.TLD` instances. + """ + return self._list(_tld.TLD, **query) + + def create_tld(self, **attrs): + """Create a new tld from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~openstack.dns.v2.tld.TLD`, + comprised of the properties on the TLD class. + :returns: The results of TLD creation. + :rtype: :class:`~openstack.dns.v2.tld.TLD` + """ + return self._create(_tld.TLD, prepend_key=False, **attrs) + + def get_tld(self, tld): + """Get a tld + + :param tld: The value can be the ID of a tld + or a :class:`~openstack.dns.v2.tld.TLD` instance. + :returns: tld instance. + :rtype: :class:`~openstack.dns.v2.tld.TLD` + """ + return self._get(_tld.TLD, tld) + + def delete_tld(self, tld, ignore_missing=True): + """Delete a tld + + :param tld: The value can be the ID of a tld + or a :class:`~openstack.dns.v2.tld.TLD` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.NotFoundException` will be raised when + the tld does not exist. + When set to ``True``, no exception will be set when attempting to + delete a nonexistent tld. + + :returns: TLD been deleted + :rtype: :class:`~openstack.dns.v2.tld.TLD` + """ + return self._delete( + _tld.TLD, + tld, + ignore_missing=ignore_missing, + ) + + def update_tld(self, tld, **attrs): + """Update tld attributes + + :param tld: The id or an instance of + :class:`~openstack.dns.v2.tld.TLD`. + :param dict attrs: attributes for update on + :class:`~openstack.dns.v2.tld.TLD`. + + :rtype: :class:`~openstack.dns.v2.tld.TLD` + """ + return self._update(_tld.TLD, tld, **attrs) + + def find_tld(self, name_or_id, ignore_missing=True): + """Find a single tld + + :param name_or_id: The name or ID of a tld + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.NotFoundException` will be raised + when the tld does not exist. + When set to ``True``, no exception will be set when attempting + to delete a nonexistent tld. + + :returns: :class:`~openstack.dns.v2.tld.TLD` + """ + return self._find(_tld.TLD, name_or_id, ignore_missing=ignore_missing) + # ========== Utilities ========== def wait_for_status( self, diff --git a/openstack/dns/v2/tld.py b/openstack/dns/v2/tld.py new file mode 100644 index 000000000..0a96481b8 --- /dev/null +++ b/openstack/dns/v2/tld.py @@ -0,0 +1,49 @@ +# 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.dns.v2 import _base +from openstack import resource + + +class TLD(_base.Resource): + """DNS TLD Resource""" + + resources_key = "tlds" + base_path = "/tlds" + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + commit_method = "PATCH" + + _query_mapping = resource.QueryParameters( + "name", + "description", + "limit", + "marker", + ) + + #: TLD name + name = resource.Body("name") + #: TLD description + description = resource.Body("description") + #: Timestamp when the tld was created + created_at = resource.Body("created_at") + #: Timestamp when the tld was last updated + updated_at = resource.Body("updated_at") + #: Links contains a `self` pertaining to this tld or a `next` pertaining + #: to next page + links = resource.Body("links", type=dict) diff --git a/openstack/tests/functional/dns/v2/test_tld.py b/openstack/tests/functional/dns/v2/test_tld.py new file mode 100644 index 000000000..ffb40de1b --- /dev/null +++ b/openstack/tests/functional/dns/v2/test_tld.py @@ -0,0 +1,61 @@ +# 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.dns.v2 import tld as _tld +from openstack.tests.functional import base + + +class TestTLD(base.BaseFunctionalTest): + def setUp(self): + super().setUp() + + self.require_service('dns') + + self.tld_name = 'xyz' + self.tld_description = 'The xyz TLD' + + def test_tld(self): + # create the tld + + tld = self.operator_cloud.dns.create_tld( + name=self.tld_name, description=self.tld_description + ) + self.assertIsInstance(tld, _tld.TLD) + self.assertEqual(self.tld_description, tld.description) + self.addCleanup(self.operator_cloud.dns.delete_tld, tld) + + # update the tld + + tld = self.operator_cloud.dns.update_tld( + tld, description=self.tld_description + ) + self.assertIsInstance(tld, _tld.TLD) + self.assertEqual(self.tld_description, tld.description) + + # retrieve details of the (updated) tld by ID + + tld = self.operator_cloud.dns.get_tld(tld.id) + self.assertIsInstance(tld, _tld.TLD) + self.assertEqual(self.tld_description, tld.description) + + # retrieve details of the (updated) tld by name + + tld = self.operator_cloud.dns.find_tld(tld.name) + self.assertIsInstance(tld, _tld.TLD) + self.assertEqual(self.tld_description, tld.description) + + # list all tlds + tlds = list(self.operator_cloud.dns.tlds()) + self.assertIsInstance(tlds[0], _tld.TLD) + self.assertIn( + self.tld_name, {x.name for x in self.operator_cloud.dns.tlds()} + ) diff --git a/openstack/tests/unit/dns/v2/test_proxy.py b/openstack/tests/unit/dns/v2/test_proxy.py index cb9541617..14773738d 100644 --- a/openstack/tests/unit/dns/v2/test_proxy.py +++ b/openstack/tests/unit/dns/v2/test_proxy.py @@ -15,6 +15,7 @@ from openstack.dns.v2 import floating_ip from openstack.dns.v2 import recordset from openstack.dns.v2 import service_status +from openstack.dns.v2 import tld from openstack.dns.v2 import tsigkey from openstack.dns.v2 import zone from openstack.dns.v2 import zone_export @@ -393,3 +394,33 @@ def test_blacklist_get(self): def test_blacklists(self): self.verify_list(self.proxy.blacklists, blacklist.Blacklist) + + +class TestDnsTLD(TestDnsProxy): + def test_tld_create(self): + self.verify_create( + self.proxy.create_tld, + tld.TLD, + method_kwargs={"name": "id"}, + expected_kwargs={"name": "id", "prepend_key": False}, + ) + + def test_tld_delete(self): + self.verify_delete( + self.proxy.delete_tld, + tld.TLD, + True, + expected_kwargs={"ignore_missing": True}, + ) + + def test_tld_find(self): + self.verify_find(self.proxy.find_tld, tld.TLD) + + def test_tld_get(self): + self.verify_get(self.proxy.get_tld, tld.TLD) + + def test_tlds(self): + self.verify_list(self.proxy.tlds, tld.TLD) + + def test_tld_update(self): + self.verify_update(self.proxy.update_tld, tld.TLD) diff --git a/openstack/tests/unit/dns/v2/test_tld.py b/openstack/tests/unit/dns/v2/test_tld.py new file mode 100644 index 000000000..c8d714eba --- /dev/null +++ b/openstack/tests/unit/dns/v2/test_tld.py @@ -0,0 +1,66 @@ +# 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 unittest import mock + +from keystoneauth1 import adapter + +from openstack.dns.v2 import tld +from openstack.tests.unit import base + +IDENTIFIER = "NAME" +EXAMPLE = { + "id": IDENTIFIER, + "name": "com", + "description": "tld description", +} + + +class TestTLD(base.TestCase): + def setUp(self): + super().setUp() + self.resp = mock.Mock() + self.resp.body = None + self.resp.json = mock.Mock(return_value=self.resp.body) + self.resp.status_code = 200 + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.post = mock.Mock(return_value=self.resp) + self.sess.default_microversion = None + + def test_basic(self): + sot = tld.TLD() + self.assertEqual(None, sot.resource_key) + self.assertEqual("tlds", sot.resources_key) + self.assertEqual("/tlds", sot.base_path) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + + self.assertEqual("PATCH", sot.commit_method) + + self.assertDictEqual( + { + "description": "description", + "name": "name", + "limit": "limit", + "marker": "marker", + }, + sot._query_mapping._mapping, + ) + + def test_make_it(self): + sot = tld.TLD(**EXAMPLE) + self.assertEqual(IDENTIFIER, sot.id) + self.assertEqual(EXAMPLE["description"], sot.description) + self.assertEqual(EXAMPLE["name"], sot.name) diff --git a/releasenotes/notes/add-dns-tld-d3cfac70f76637e3.yaml b/releasenotes/notes/add-dns-tld-d3cfac70f76637e3.yaml new file mode 100644 index 000000000..d33e47f44 --- /dev/null +++ b/releasenotes/notes/add-dns-tld-d3cfac70f76637e3.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Adds support for `dns tld + `_ service. From c826687bbf59b76eb3d7dada5e94c4f7d1cad2f3 Mon Sep 17 00:00:00 2001 From: choieastsea Date: Tue, 3 Sep 2024 22:51:54 +0900 Subject: [PATCH 3762/3836] dns: Add support for quotas This patch add quota support for designate(DNS) API - add dns quota resource & proxy methods - add unit & functional test - add docs and release note - fix tld functional test code Change-Id: Id194b6871e2bb80d08b0bb0113bf3fa64e419385 Signed-off-by: choieastsea --- doc/source/user/proxies/dns.rst | 7 ++ doc/source/user/resources/dns/index.rst | 1 + doc/source/user/resources/dns/v2/quota.rst | 12 ++ openstack/dns/v2/_proxy.py | 56 +++++++++ openstack/dns/v2/quota.py | 106 ++++++++++++++++++ .../tests/functional/dns/v2/test_quota.py | 71 ++++++++++++ openstack/tests/unit/dns/v2/test_proxy.py | 28 +++++ openstack/tests/unit/dns/v2/test_quota.py | 51 +++++++++ .../notes/add-dns-quota-49ae659a88eeeab9.yaml | 4 + 9 files changed, 336 insertions(+) create mode 100644 doc/source/user/resources/dns/v2/quota.rst create mode 100644 openstack/dns/v2/quota.py create mode 100644 openstack/tests/functional/dns/v2/test_quota.py create mode 100644 openstack/tests/unit/dns/v2/test_quota.py create mode 100644 releasenotes/notes/add-dns-quota-49ae659a88eeeab9.yaml diff --git a/doc/source/user/proxies/dns.rst b/doc/source/user/proxies/dns.rst index a629375ed..0229eb4ef 100644 --- a/doc/source/user/proxies/dns.rst +++ b/doc/source/user/proxies/dns.rst @@ -83,6 +83,13 @@ Limit Operations :noindex: :members: limits +Quota Operations +^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.dns.v2._proxy.Proxy + :noindex: + :members: quotas, get_quota, update_quota, delete_quota + Service Status Operations ^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/dns/index.rst b/doc/source/user/resources/dns/index.rst index 90a58a82d..576c16c9f 100644 --- a/doc/source/user/resources/dns/index.rst +++ b/doc/source/user/resources/dns/index.rst @@ -13,5 +13,6 @@ DNS Resources v2/tld v2/recordset v2/limit + v2/quota v2/service_status v2/blacklist diff --git a/doc/source/user/resources/dns/v2/quota.rst b/doc/source/user/resources/dns/v2/quota.rst new file mode 100644 index 000000000..9ad1f6304 --- /dev/null +++ b/doc/source/user/resources/dns/v2/quota.rst @@ -0,0 +1,12 @@ +openstack.dns.v2.quota +====================== + +.. automodule:: openstack.dns.v2.quota + +The Quota Class +--------------- + +The ``Quota`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.dns.v2.quota.Quota + :members: diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py index a28f235f0..e0a561454 100644 --- a/openstack/dns/v2/_proxy.py +++ b/openstack/dns/v2/_proxy.py @@ -15,6 +15,7 @@ from openstack.dns.v2 import blacklist as _blacklist from openstack.dns.v2 import floating_ip as _fip from openstack.dns.v2 import limit as _limit +from openstack.dns.v2 import quota as _quota from openstack.dns.v2 import recordset as _rs from openstack.dns.v2 import service_status as _svc_status from openstack.dns.v2 import tld as _tld @@ -34,6 +35,7 @@ class Proxy(proxy.Proxy): "blacklist": _blacklist.Blacklist, "floating_ip": _fip.FloatingIP, "limits": _limit.Limit, + "quota": _quota.Quota, "recordset": _rs.Recordset, "service_status": _svc_status.ServiceStatus, "zone": _zone.Zone, @@ -699,6 +701,60 @@ def limits(self, **query): """ return self._list(_limit.Limit, **query) + # ======== Quotas ======== + def quotas(self, **query): + """Return a generator of quotas + + :param dict query: Optional query parameters to be sent to limit the + resources being returned. + + :returns: A generator of quota objects + :rtype: :class:`~openstack.dns.v2.quota.Quota` + """ + return self._list(_quota.Quota, **query) + + def get_quota(self, quota): + """Get a quota + + :param quota: The value can be the ID of a quota or a + :class:`~openstack.dns.v2.quota.Quota` instance. + The ID of a quota is the same as the project ID for the quota. + + :returns: One :class:`~openstack.dns.v2.quota.Quota` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + """ + return self._get(_quota.Quota, quota) + + def update_quota(self, quota, **attrs): + """Update a quota + + :param quota: Either the ID of a quota or a + :class:`~openstack.dns.v2.quota.Quota` instance. The ID of a quota + is the same as the project ID for the quota. + :param dict attrs: The attributes to update on the quota represented + by ``quota``. + + :returns: The updated quota + :rtype: :class:`~openstack.dns.v2.quota.Quota` + """ + return self._update(_quota.Quota, quota, **attrs) + + def delete_quota(self, quota, ignore_missing=True): + """Delete a quota (i.e. reset to the default quota) + + :param quota: The value can be the ID of a quota or a + :class:`~openstack.dns.v2.quota.Quota` instance. + The ID of a quota is the same as the project ID for the quota. + :param bool ignore_missing: When set to ``False``, + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the quota does not exist. + When set to ``True``, no exception will be set when attempting to + delete a nonexistent quota. + + :returns: ``None`` + """ + return self._delete(_quota.Quota, quota, ignore_missing=ignore_missing) + # ======== Service Statuses ======== def service_statuses(self): """Retrieve a generator of service statuses diff --git a/openstack/dns/v2/quota.py b/openstack/dns/v2/quota.py new file mode 100644 index 000000000..3e8e70e80 --- /dev/null +++ b/openstack/dns/v2/quota.py @@ -0,0 +1,106 @@ +# 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 typing as ty + +from keystoneauth1 import adapter +import typing_extensions as ty_ext + +from openstack.dns.v2 import _base +from openstack import resource + + +class Quota(_base.Resource): + """DNS Quota Resource""" + + base_path = "/quotas" + + # capabilities + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + commit_method = "PATCH" + + # Properties + #: The ID of the project. + project = resource.URI("project", alternate_id=True) + #: The maximum amount of recordsets allowed in a zone export. *Type: int* + api_export_size = resource.Body("api_export_size", type=int) + #: The maximum amount of records allowed per recordset. *Type: int* + recordset_records = resource.Body("recordset_records", type=int) + #: The maximum amount of records allowed per zone. *Type: int* + zone_records = resource.Body("zone_records", type=int) + #: The maximum amount of recordsets allowed per zone. *Type: int* + zone_recordsets = resource.Body("zone_recordsets", type=int) + #: The maximum amount of zones allowed per project. *Type: int* + zones = resource.Body("zones", type=int) + + def _prepare_request( + self, + requires_id=True, + prepend_key=False, + patch=False, + base_path=None, + params=None, + *, + resource_request_key=None, + **kwargs, + ): + _request = super()._prepare_request( + requires_id, prepend_key, base_path=base_path + ) + if self.resource_key in _request.body: + _body = _request.body[self.resource_key] + else: + _body = _request.body + if "id" in _body: + del _body["id"] + _request.headers = {'x-auth-sudo-project-id': self.id} + return _request + + def fetch( + self, + session: adapter.Adapter, + requires_id: bool = True, + base_path: str | None = None, + error_message: str | None = None, + skip_cache: bool = False, + *, + resource_response_key: str | None = None, + microversion: str | None = None, + **params: ty.Any, + ) -> ty_ext.Self: + request = self._prepare_request( + requires_id=requires_id, + base_path=base_path, + ) + session = self._get_session(session) + if microversion is None: + microversion = self._get_microversion(session) + self.microversion = microversion + + response = session.get( + request.url, + microversion=microversion, + params=params, + skip_cache=skip_cache, + headers=request.headers, + ) + + self._translate_response( + response, + error_message=error_message, + resource_response_key=resource_response_key, + ) + + return self diff --git a/openstack/tests/functional/dns/v2/test_quota.py b/openstack/tests/functional/dns/v2/test_quota.py new file mode 100644 index 000000000..3a642a60c --- /dev/null +++ b/openstack/tests/functional/dns/v2/test_quota.py @@ -0,0 +1,71 @@ +# 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.tests.functional import base + + +class TestQuota(base.BaseFunctionalTest): + def setUp(self): + super().setUp() + + self.require_service("dns") + if not self._operator_cloud_name: + self.skip("Operator cloud must be set for this test") + + self.project = self.create_temporary_project() + + def test_quota(self): + # set quota + + attrs = { + "api_export_size": 1, + "recordset_records": 2, + "zone_records": 3, + "zone_recordsets": 4, + "zones": 5, + } + new_quota = self.operator_cloud.dns.update_quota( + self.project.id, **attrs + ) + self.assertEqual(attrs["api_export_size"], new_quota.api_export_size) + self.assertEqual( + attrs["recordset_records"], new_quota.recordset_records + ) + self.assertEqual(attrs["zone_records"], new_quota.zone_records) + self.assertEqual(attrs["zone_recordsets"], new_quota.zone_recordsets) + self.assertEqual(attrs["zones"], new_quota.zones) + + # get quota + + expected_keys = [ + "id", + "api_export_size", + "recordset_records", + "zone_records", + "zone_recordsets", + "zones", + ] + test_quota = self.operator_cloud.dns.get_quota(self.project.id) + for actual_key in test_quota._body.attributes.keys(): + self.assertIn(actual_key, expected_keys) + self.assertEqual(self.project.id, test_quota.id) + self.assertEqual(attrs["api_export_size"], test_quota.api_export_size) + self.assertEqual( + attrs["recordset_records"], test_quota.recordset_records + ) + self.assertEqual(attrs["zone_records"], test_quota.zone_records) + self.assertEqual(attrs["zone_recordsets"], test_quota.zone_recordsets) + self.assertEqual(attrs["zones"], test_quota.zones) + + # reset quota + + self.operator_cloud.dns.delete_quota(self.project.id) diff --git a/openstack/tests/unit/dns/v2/test_proxy.py b/openstack/tests/unit/dns/v2/test_proxy.py index 14773738d..5874ce1d0 100644 --- a/openstack/tests/unit/dns/v2/test_proxy.py +++ b/openstack/tests/unit/dns/v2/test_proxy.py @@ -13,6 +13,7 @@ from openstack.dns.v2 import _proxy from openstack.dns.v2 import blacklist from openstack.dns.v2 import floating_ip +from openstack.dns.v2 import quota from openstack.dns.v2 import recordset from openstack.dns.v2 import service_status from openstack.dns.v2 import tld @@ -424,3 +425,30 @@ def test_tlds(self): def test_tld_update(self): self.verify_update(self.proxy.update_tld, tld.TLD) + + +class TestDnsQuota(TestDnsProxy): + def test_quotas(self): + self.verify_list(self.proxy.quotas, quota.Quota) + + def test_quota_get(self): + self.verify_get(self.proxy.get_quota, quota.Quota) + + def test_quota_update(self): + self.verify_update(self.proxy.update_quota, quota.Quota) + + def test_quota_delete(self): + self.verify_delete( + self.proxy.delete_quota, + quota.Quota, + False, + expected_kwargs={'ignore_missing': False}, + ) + + def test_quota_delete_ignore(self): + self.verify_delete( + self.proxy.delete_quota, + quota.Quota, + True, + expected_kwargs={'ignore_missing': True}, + ) diff --git a/openstack/tests/unit/dns/v2/test_quota.py b/openstack/tests/unit/dns/v2/test_quota.py new file mode 100644 index 000000000..e788b02cf --- /dev/null +++ b/openstack/tests/unit/dns/v2/test_quota.py @@ -0,0 +1,51 @@ +# 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.dns.v2 import quota +from openstack.tests.unit import base + +IDENTIFIER = "IDENTIFIER" +EXAMPLE = { + "zones": 10, + "zone_recordsets": 500, + "zone_records": 500, + "recordset_records": 20, + "api_export_size": 1000, +} + + +class TestQuota(base.TestCase): + def test_basic(self): + sot = quota.Quota() + self.assertIsNone(sot.resources_key) + self.assertIsNone(sot.resource_key) + self.assertEqual("/quotas", sot.base_path) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.commit_method, "PATCH") + + def test_make_it(self): + sot = quota.Quota(project='FAKE_PROJECT', **EXAMPLE) + self.assertEqual(EXAMPLE['zones'], sot.zones) + self.assertEqual(EXAMPLE['zone_recordsets'], sot.zone_recordsets) + self.assertEqual(EXAMPLE['zone_records'], sot.zone_records) + self.assertEqual(EXAMPLE['recordset_records'], sot.recordset_records) + self.assertEqual(EXAMPLE['api_export_size'], sot.api_export_size) + self.assertEqual('FAKE_PROJECT', sot.project) + + def test_prepare_request(self): + body = {'id': 'ABCDEFGH', 'zones': 20} + quota_obj = quota.Quota(**body) + response = quota_obj._prepare_request() + self.assertNotIn('id', response) diff --git a/releasenotes/notes/add-dns-quota-49ae659a88eeeab9.yaml b/releasenotes/notes/add-dns-quota-49ae659a88eeeab9.yaml new file mode 100644 index 000000000..c2be9828a --- /dev/null +++ b/releasenotes/notes/add-dns-quota-49ae659a88eeeab9.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add quota support for designate(DNS) API. From c5da8300bab0719fa48886f075c164eff0ebef58 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 10 Sep 2025 11:45:49 +0100 Subject: [PATCH 3763/3836] trivial: Reshuffle code Change-Id: I6a8aa9090689938b09466089b2a4229054cd1863 Signed-off-by: Stephen Finucane --- openstack/dns/v2/_proxy.py | 208 ++++++++++++++++++------------------- 1 file changed, 104 insertions(+), 104 deletions(-) diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py index e0a561454..f12426b2f 100644 --- a/openstack/dns/v2/_proxy.py +++ b/openstack/dns/v2/_proxy.py @@ -856,110 +856,6 @@ def find_tld(self, name_or_id, ignore_missing=True): """ return self._find(_tld.TLD, name_or_id, ignore_missing=ignore_missing) - # ========== Utilities ========== - def wait_for_status( - self, - res: resource.ResourceT, - status: str, - failures: list[str] | None = None, - interval: int | float | None = 2, - wait: int | None = None, - attribute: str = 'status', - callback: ty.Callable[[int], None] | None = None, - ) -> resource.ResourceT: - """Wait for the resource to be in a particular status. - - :param session: The session to use for making this request. - :param resource: The resource to wait on to reach the status. The - resource must have a status attribute specified via ``attribute``. - :param status: Desired status of the resource. - :param failures: Statuses that would indicate the transition - failed such as 'ERROR'. Defaults to ['ERROR']. - :param interval: Number of seconds to wait between checks. - :param wait: Maximum number of seconds to wait for transition. - Set to ``None`` to wait forever. - :param attribute: Name of the resource attribute that contains the - status. - :param callback: A callback function. This will be called with a single - value, progress. This is API specific but is generally a percentage - value from 0-100. - - :return: The updated resource. - :raises: :class:`~openstack.exceptions.ResourceTimeout` if the - transition to status failed to occur in ``wait`` seconds. - :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource - transitioned to one of the states in ``failures``. - :raises: :class:`~AttributeError` if the resource does not have a - ``status`` attribute - """ - return resource.wait_for_status( - self, res, status, failures, interval, wait, attribute, callback - ) - - def wait_for_delete( - self, - res: resource.ResourceT, - interval: int = 2, - wait: int = 120, - callback: ty.Callable[[int], None] | None = None, - ) -> resource.ResourceT: - """Wait for a resource to be deleted. - - :param res: The resource to wait on to be deleted. - :param interval: Number of seconds to wait before to consecutive - checks. - :param wait: Maximum number of seconds to wait before the change. - :param callback: A callback function. This will be called with a single - value, progress, which is a percentage value from 0-100. - - :returns: The resource is returned on success. - :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition - to delete failed to occur in the specified seconds. - """ - return resource.wait_for_delete(self, res, interval, wait, callback) - - def _get_cleanup_dependencies(self): - # DNS may depend on floating ip - return {'dns': {'before': ['network']}} - - def _service_cleanup( - self, - dry_run=True, - client_status_queue=False, - identified_resources=None, - filters=None, - resource_evaluation_fn=None, - skip_resources=None, - ): - if not self.should_skip_resource_cleanup("zone", skip_resources): - # Delete all zones - for obj in self.zones(): - self._service_cleanup_del_res( - self.delete_zone, - obj, - dry_run=dry_run, - client_status_queue=client_status_queue, - identified_resources=identified_resources, - filters=filters, - resource_evaluation_fn=resource_evaluation_fn, - ) - - if not self.should_skip_resource_cleanup( - "floating_ip", skip_resources - ): - # Unset all floatingIPs - # NOTE: FloatingIPs are not cleaned when filters are set - for obj in self.floating_ips(): - self._service_cleanup_del_res( - self.unset_floating_ip, - obj, - dry_run=dry_run, - client_status_queue=client_status_queue, - identified_resources=identified_resources, - filters=filters, - resource_evaluation_fn=resource_evaluation_fn, - ) - # ====== TSIG keys ====== def tsigkeys(self, **query): """Retrieve a generator of zones @@ -1089,3 +985,107 @@ def delete_blacklist(self, blacklist, ignore_missing=True): return self._delete( _blacklist.Blacklist, blacklist, ignore_missing=ignore_missing ) + + # ========== Utilities ========== + def wait_for_status( + self, + res: resource.ResourceT, + status: str, + failures: list[str] | None = None, + interval: int | float | None = 2, + wait: int | None = None, + attribute: str = 'status', + callback: ty.Callable[[int], None] | None = None, + ) -> resource.ResourceT: + """Wait for the resource to be in a particular status. + + :param session: The session to use for making this request. + :param resource: The resource to wait on to reach the status. The + resource must have a status attribute specified via ``attribute``. + :param status: Desired status of the resource. + :param failures: Statuses that would indicate the transition + failed such as 'ERROR'. Defaults to ['ERROR']. + :param interval: Number of seconds to wait between checks. + :param wait: Maximum number of seconds to wait for transition. + Set to ``None`` to wait forever. + :param attribute: Name of the resource attribute that contains the + status. + :param callback: A callback function. This will be called with a single + value, progress. This is API specific but is generally a percentage + value from 0-100. + + :return: The updated resource. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if the + transition to status failed to occur in ``wait`` seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + transitioned to one of the states in ``failures``. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute + """ + return resource.wait_for_status( + self, res, status, failures, interval, wait, attribute, callback + ) + + def wait_for_delete( + self, + res: resource.ResourceT, + interval: int = 2, + wait: int = 120, + callback: ty.Callable[[int], None] | None = None, + ) -> resource.ResourceT: + """Wait for a resource to be deleted. + + :param res: The resource to wait on to be deleted. + :param interval: Number of seconds to wait before to consecutive + checks. + :param wait: Maximum number of seconds to wait before the change. + :param callback: A callback function. This will be called with a single + value, progress, which is a percentage value from 0-100. + + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to delete failed to occur in the specified seconds. + """ + return resource.wait_for_delete(self, res, interval, wait, callback) + + def _get_cleanup_dependencies(self): + # DNS may depend on floating ip + return {'dns': {'before': ['network']}} + + def _service_cleanup( + self, + dry_run=True, + client_status_queue=False, + identified_resources=None, + filters=None, + resource_evaluation_fn=None, + skip_resources=None, + ): + if not self.should_skip_resource_cleanup("zone", skip_resources): + # Delete all zones + for obj in self.zones(): + self._service_cleanup_del_res( + self.delete_zone, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn, + ) + + if not self.should_skip_resource_cleanup( + "floating_ip", skip_resources + ): + # Unset all floatingIPs + # NOTE: FloatingIPs are not cleaned when filters are set + for obj in self.floating_ips(): + self._service_cleanup_del_res( + self.unset_floating_ip, + obj, + dry_run=dry_run, + client_status_queue=client_status_queue, + identified_resources=identified_resources, + filters=filters, + resource_evaluation_fn=resource_evaluation_fn, + ) From a42b3c51f335337937143ed30885bc1eae369114 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 10 Sep 2025 11:59:51 +0100 Subject: [PATCH 3764/3836] message: De-duplicate common code There are only minor changes in the 'list' method to keep mypy happy. Change-Id: I3b400cffc31b4511b18c07ed660cb17b698106c6 Signed-off-by: Stephen Finucane --- openstack/message/v2/_base.py | 129 +++++++++++++++++++++++++++ openstack/message/v2/claim.py | 58 ++---------- openstack/message/v2/message.py | 82 +++-------------- openstack/message/v2/queue.py | 96 +------------------- openstack/message/v2/subscription.py | 98 +------------------- 5 files changed, 153 insertions(+), 310 deletions(-) create mode 100644 openstack/message/v2/_base.py diff --git a/openstack/message/v2/_base.py b/openstack/message/v2/_base.py new file mode 100644 index 000000000..018bcbfe9 --- /dev/null +++ b/openstack/message/v2/_base.py @@ -0,0 +1,129 @@ +# 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 typing as ty +import uuid + +from keystoneauth1 import adapter +import typing_extensions as ty_ext + +from openstack import resource + + +class MessageResource(resource.Resource): + # FIXME(anyone): The name string of `location` field of Zaqar API response + # is lower case. That is inconsistent with the guide from API-WG. This is + # a workaround for this issue. + location = resource.Header("location") + + #: The ID to identify the client accessing Zaqar API. Must be specified + #: in header for each API request. + client_id = resource.Header("Client-ID") + #: The ID to identify the project. Must be provided when keystone + #: authentication is not enabled in Zaqar service. + project_id = resource.Header("X-PROJECT-ID") + + @classmethod + def list( + cls, + session: adapter.Adapter, + paginated: bool = True, + base_path: str | None = None, + allow_unknown_params: bool = False, + *, + microversion: str | None = None, + headers: dict[str, str] | None = None, + max_items: int | None = None, + **params: ty.Any, + ) -> ty.Generator[ty_ext.Self, None, None]: + """This method is a generator which yields resource objects. + + This is almost the copy of list method of resource.Resource class. + The only difference is the request header now includes `Client-ID` + and `X-PROJECT-ID` fields which are required by Zaqar v2 API. + """ + more_data = True + + if base_path is None: + base_path = cls.base_path + + uri = base_path % params + + project_id = params.get('project_id', None) or session.get_project_id() + assert project_id is not None + + headers = { + "Client-ID": params.get('client_id', None) or str(uuid.uuid4()), + "X-PROJECT-ID": project_id, + } + + query_params = cls._query_mapping._transpose(params, cls) + while more_data: + resp = session.get( + uri, headers=headers, params=query_params + ).json()[cls.resources_key] + + if not resp: + more_data = False + + yielded = 0 + new_marker = None + for data in resp: + value = cls.existing(**data) + new_marker = value.id + yielded += 1 + yield value + + if not paginated: + return + if "limit" in query_params and yielded < query_params["limit"]: + return + query_params["limit"] = yielded + query_params["marker"] = new_marker + + def fetch( + self, + session, + requires_id=True, + base_path=None, + error_message=None, + skip_cache=False, + **kwargs, + ): + request = self._prepare_request( + requires_id=requires_id, base_path=base_path + ) + headers = { + "Client-ID": self.client_id or str(uuid.uuid4()), + "X-PROJECT-ID": self.project_id or session.get_project_id(), + } + request.headers.update(headers) + response = session.get( + request.url, headers=headers, skip_cache=skip_cache + ) + self._translate_response(response) + + return self + + def delete( + self, session, error_message=None, *, microversion=None, **kwargs + ): + request = self._prepare_request() + headers = { + "Client-ID": self.client_id or str(uuid.uuid4()), + "X-PROJECT-ID": self.project_id or session.get_project_id(), + } + request.headers.update(headers) + response = session.delete(request.url, headers=headers) + + self._translate_response(response, has_body=False) + return self diff --git a/openstack/message/v2/claim.py b/openstack/message/v2/claim.py index 2aedf57ef..0ee2fbf72 100644 --- a/openstack/message/v2/claim.py +++ b/openstack/message/v2/claim.py @@ -12,15 +12,11 @@ import uuid +from openstack.message.v2 import _base from openstack import resource -class Claim(resource.Resource): - # FIXME(anyone): The name string of `location` field of Zaqar API response - # is lower case. That is inconsistent with the guide from API-WG. This is - # a workaround for this issue. - location = resource.Header("location") - +class Claim(_base.MessageResource): resources_key = 'claims' base_path = '/queues/%(queue_name)s/claims' @@ -48,12 +44,6 @@ class Claim(resource.Resource): ttl = resource.Body("ttl") #: The name of queue to claim message from. queue_name = resource.URI("queue_name") - #: The ID to identify the client accessing Zaqar API. Must be specified - #: in header for each API request. - client_id = resource.Header("Client-ID") - #: The ID to identify the project. Must be provided when keystone - #: authentication is not enabled in Zaqar service. - project_id = resource.Header("X-PROJECT-ID") def _translate_response( self, @@ -63,6 +53,12 @@ def _translate_response( *, resource_response_key=None, ): + # For case no message was claimed successfully, 204 No Content + # message will be returned. In other cases, we translate response + # body which has `messages` field(list) included. + if response.status_code == 204: + return + super()._translate_response( response, has_body, @@ -94,31 +90,6 @@ def create(self, session, prepend_key=False, base_path=None, **kwargs): return self - def fetch( - self, - session, - requires_id=True, - base_path=None, - error_message=None, - skip_cache=False, - **kwargs, - ): - request = self._prepare_request( - requires_id=requires_id, base_path=base_path - ) - headers = { - "Client-ID": self.client_id or str(uuid.uuid4()), - "X-PROJECT-ID": self.project_id or session.get_project_id(), - } - - request.headers.update(headers) - response = session.get( - request.url, headers=request.headers, skip_cache=False - ) - self._translate_response(response) - - return self - def commit( self, session, @@ -140,16 +111,3 @@ def commit( session.patch(request.url, json=request.body, headers=request.headers) return self - - def delete(self, session, *args, **kwargs): - request = self._prepare_request() - headers = { - "Client-ID": self.client_id or str(uuid.uuid4()), - "X-PROJECT-ID": self.project_id or session.get_project_id(), - } - - request.headers.update(headers) - response = session.delete(request.url, headers=request.headers) - - self._translate_response(response, has_body=False) - return self diff --git a/openstack/message/v2/message.py b/openstack/message/v2/message.py index 325a08c52..091aefdc7 100644 --- a/openstack/message/v2/message.py +++ b/openstack/message/v2/message.py @@ -12,15 +12,11 @@ import uuid +from openstack.message.v2 import _base from openstack import resource -class Message(resource.Resource): - # FIXME(anyone): The name string of `location` field of Zaqar API response - # is lower case. That is inconsistent with the guide from API-WG. This is - # a workaround for this issue. - location = resource.Header("location") - +class Message(_base.MessageResource): resources_key = 'messages' base_path = '/queues/%(queue_name)s/messages' @@ -46,12 +42,6 @@ class Message(resource.Resource): ttl = resource.Body("ttl") #: The name of target queue message is post to or got from. queue_name = resource.URI("queue_name") - #: The ID to identify the client accessing Zaqar API. Must be specified - #: in header for each API request. - client_id = resource.Header("Client-ID") - #: The ID to identify the project accessing Zaqar API. Must be specified - #: in case keystone auth is not enabled in Zaqar service. - project_id = resource.Header("X-PROJECT-ID") # FIXME(stephenfin): This is actually a query arg but we need it for # deletions and resource.delete doesn't respect these currently @@ -71,72 +61,24 @@ def post(self, session, messages): return response.json()['resources'] - @classmethod - def list(cls, session, paginated=True, base_path=None, **params): - """This method is a generator which yields message objects. - - This is almost the copy of list method of resource.Resource class. - The only difference is the request header now includes `Client-ID` - and `X-PROJECT-ID` fields which are required by Zaqar v2 API. - """ - more_data = True - - if base_path is None: - base_path = cls.base_path - - uri = base_path % params - headers = { - "Client-ID": params.get('client_id', None) or str(uuid.uuid4()), - "X-PROJECT-ID": params.get('project_id', None) - or session.get_project_id(), - } - - query_params = cls._query_mapping._transpose(params, cls) - while more_data: - resp = session.get(uri, headers=headers, params=query_params) - resp = resp.json() - resp = resp[cls.resources_key] - - if not resp: - more_data = False - - yielded = 0 - new_marker = None - for data in resp: - value = cls.existing(**data) - new_marker = value.id - yielded += 1 - yield value - - if not paginated: - return - if "limit" in query_params and yielded < query_params["limit"]: - return - query_params["limit"] = yielded - query_params["marker"] = new_marker - - def fetch( - self, - session, - requires_id=True, - base_path=None, - error_message=None, - skip_cache=False, - **kwargs, - ): + def create(self, session, prepend_key=False, base_path=None, **kwargs): request = self._prepare_request( - requires_id=requires_id, base_path=base_path + requires_id=False, prepend_key=prepend_key, base_path=base_path ) headers = { "Client-ID": self.client_id or str(uuid.uuid4()), "X-PROJECT-ID": self.project_id or session.get_project_id(), } - request.headers.update(headers) - response = session.get( - request.url, headers=headers, skip_cache=skip_cache + response = session.post( + request.url, json=request.body, headers=request.headers ) - self._translate_response(response) + + # For case no message was claimed successfully, 204 No Content + # message will be returned. In other cases, we translate response + # body which has `messages` field(list) included. + if response.status_code != 204: + self._translate_response(response) return self diff --git a/openstack/message/v2/queue.py b/openstack/message/v2/queue.py index 42b0b00c5..9e23e33cc 100644 --- a/openstack/message/v2/queue.py +++ b/openstack/message/v2/queue.py @@ -12,15 +12,11 @@ import uuid +from openstack.message.v2 import _base from openstack import resource -class Queue(resource.Resource): - # FIXME(anyone): The name string of `location` field of Zaqar API response - # is lower case. That is inconsistent with the guide from API-WG. This is - # a workaround for this issue. - location = resource.Header("location") - +class Queue(_base.MessageResource): resources_key = "queues" base_path = "/queues" @@ -43,12 +39,6 @@ class Queue(resource.Resource): #: must not exceed 64 bytes in length, and it is limited to US-ASCII #: letters, digits, underscores, and hyphens. name = resource.Body("name", alternate_id=True) - #: The ID to identify the client accessing Zaqar API. Must be specified - #: in header for each API request. - client_id = resource.Header("Client-ID") - #: The ID to identify the project accessing Zaqar API. Must be specified - #: in case keystone auth is not enabled in Zaqar service. - project_id = resource.Header("X-PROJECT-ID") def create(self, session, prepend_key=False, base_path=None, **kwargs): request = self._prepare_request( @@ -65,85 +55,3 @@ def create(self, session, prepend_key=False, base_path=None, **kwargs): self._translate_response(response, has_body=False) return self - - @classmethod - def list(cls, session, paginated=False, base_path=None, **params): - """This method is a generator which yields queue objects. - - This is almost the copy of list method of resource.Resource class. - The only difference is the request header now includes `Client-ID` - and `X-PROJECT-ID` fields which are required by Zaqar v2 API. - """ - more_data = True - query_params = cls._query_mapping._transpose(params, cls) - - if base_path is None: - base_path = cls.base_path - - uri = base_path % params - headers = { - "Client-ID": params.get('client_id', None) or str(uuid.uuid4()), - "X-PROJECT-ID": params.get('project_id', None) - or session.get_project_id(), - } - - while more_data: - resp = session.get(uri, headers=headers, params=query_params) - resp = resp.json() - resp = resp[cls.resources_key] - - if not resp: - more_data = False - - yielded = 0 - new_marker = None - for data in resp: - value = cls.existing(**data) - new_marker = value.id - yielded += 1 - yield value - - if not paginated: - return - if "limit" in query_params and yielded < query_params["limit"]: - return - query_params["limit"] = yielded - query_params["marker"] = new_marker - - def fetch( - self, - session, - requires_id=True, - base_path=None, - error_message=None, - skip_cache=False, - **kwargs, - ): - request = self._prepare_request( - requires_id=requires_id, base_path=base_path - ) - headers = { - "Client-ID": self.client_id or str(uuid.uuid4()), - "X-PROJECT-ID": self.project_id or session.get_project_id(), - } - request.headers.update(headers) - response = session.get( - request.url, headers=headers, skip_cache=skip_cache - ) - self._translate_response(response) - - return self - - def delete( - self, session, error_message=None, *, microversion=None, **kwargs - ): - request = self._prepare_request() - headers = { - "Client-ID": self.client_id or str(uuid.uuid4()), - "X-PROJECT-ID": self.project_id or session.get_project_id(), - } - request.headers.update(headers) - response = session.delete(request.url, headers=headers) - - self._translate_response(response, has_body=False) - return self diff --git a/openstack/message/v2/subscription.py b/openstack/message/v2/subscription.py index 884c1ea83..5da496246 100644 --- a/openstack/message/v2/subscription.py +++ b/openstack/message/v2/subscription.py @@ -12,15 +12,11 @@ import uuid +from openstack.message.v2 import _base from openstack import resource -class Subscription(resource.Resource): - # FIXME(anyone): The name string of `location` field of Zaqar API response - # is lower case. That is inconsistent with the guide from API-WG. This is - # a workaround for this issue. - location = resource.Header("location") - +class Subscription(_base.MessageResource): resources_key = 'subscriptions' base_path = '/queues/%(queue_name)s/subscriptions' @@ -51,12 +47,6 @@ class Subscription(resource.Resource): ttl = resource.Body("ttl") #: The queue name which the subscription is registered on. queue_name = resource.URI("queue_name") - #: The ID to identify the client accessing Zaqar API. Must be specified - #: in header for each API request. - client_id = resource.Header("Client-ID") - #: The ID to identify the project. Must be provided when keystone - #: authentication is not enabled in Zaqar service. - project_id = resource.Header("X-PROJECT-ID") def create(self, session, prepend_key=False, base_path=None, **kwargs): request = self._prepare_request( @@ -73,87 +63,3 @@ def create(self, session, prepend_key=False, base_path=None, **kwargs): self._translate_response(response) return self - - @classmethod - def list(cls, session, paginated=True, base_path=None, **params): - """This method is a generator which yields subscription objects. - - This is almost the copy of list method of resource.Resource class. - The only difference is the request header now includes `Client-ID` - and `X-PROJECT-ID` fields which are required by Zaqar v2 API. - """ - more_data = True - - if base_path is None: - base_path = cls.base_path - - uri = base_path % params - headers = { - "Client-ID": params.get('client_id', None) or str(uuid.uuid4()), - "X-PROJECT-ID": params.get('project_id', None) - or session.get_project_id(), - } - - query_params = cls._query_mapping._transpose(params, cls) - while more_data: - resp = session.get(uri, headers=headers, params=query_params) - resp = resp.json() - resp = resp[cls.resources_key] - - if not resp: - more_data = False - - yielded = 0 - new_marker = None - for data in resp: - value = cls.existing(**data) - new_marker = value.id - yielded += 1 - yield value - - if not paginated: - return - if "limit" in query_params and yielded < query_params["limit"]: - return - query_params["limit"] = yielded - query_params["marker"] = new_marker - - def fetch( - self, - session, - requires_id=True, - base_path=None, - error_message=None, - skip_cache=False, - **kwargs, - ): - request = self._prepare_request( - requires_id=requires_id, base_path=base_path - ) - headers = { - "Client-ID": self.client_id or str(uuid.uuid4()), - "X-PROJECT-ID": self.project_id or session.get_project_id(), - } - - request.headers.update(headers) - response = session.get( - request.url, headers=request.headers, skip_cache=skip_cache - ) - self._translate_response(response) - - return self - - def delete( - self, session, error_message=None, *, microversion=None, **kwargs - ): - request = self._prepare_request() - headers = { - "Client-ID": self.client_id or str(uuid.uuid4()), - "X-PROJECT-ID": self.project_id or session.get_project_id(), - } - - request.headers.update(headers) - response = session.delete(request.url, headers=request.headers) - - self._translate_response(response, has_body=False) - return self From 66e5e3b59cbbfa5a8b3fc96eb362520c06ebe664 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 9 Jun 2025 12:14:24 +0100 Subject: [PATCH 3765/3836] pre-commit: Bump versions This brings in a new version of mypy which requires a number of changes. Change-Id: I1bdbde55107ffebe287a7344086c34ebdac734f1 Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 10 +-- openstack/block_storage/v2/service.py | 69 +++++++++++++++- openstack/block_storage/v3/service.py | 72 ++++++++++++++++- openstack/cloud/openstackcloud.py | 9 ++- openstack/common/quota_set.py | 2 +- openstack/compute/v2/flavor.py | 29 +++++-- openstack/compute/v2/server_ip.py | 24 ++++-- openstack/compute/v2/service.py | 75 +++++++++++++++++- openstack/config/cloud_region.py | 4 +- openstack/dns/v2/_base.py | 97 ++++++++++++++++++++--- openstack/identity/v2/extension.py | 18 ++++- openstack/identity/v3/limit.py | 2 +- openstack/identity/v3/registered_limit.py | 2 +- openstack/identity/version.py | 17 +++- openstack/image/v1/image.py | 70 +++++++++++++++- openstack/image/v2/image.py | 69 +++++++++++++++- openstack/orchestration/v1/stack.py | 67 +++++++++++++++- openstack/resource.py | 6 +- 18 files changed, 587 insertions(+), 55 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c8ecad6d4..9fdd03479 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,13 +1,13 @@ --- repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: mixed-line-ending args: ['--fix', 'lf'] exclude: '.*\.(svg)$' - - id: check-byte-order-marker + - id: fix-byte-order-marker - id: check-executables-have-shebangs - id: check-merge-conflict - id: debug-statements @@ -15,11 +15,11 @@ repos: files: .*\.(yaml|yml)$ exclude: '^zuul.d/.*$' - repo: https://github.com/PyCQA/doc8 - rev: v1.1.2 + rev: v2.0.0 hooks: - id: doc8 - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.11 + rev: v0.12.12 hooks: - id: ruff-check args: ['--fix', '--unsafe-fixes'] @@ -32,7 +32,7 @@ repos: - flake8-import-order~=0.18.2 exclude: '^(doc|releasenotes|tools)/.*$' - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.15.0 + rev: v1.17.1 hooks: - id: mypy additional_dependencies: diff --git a/openstack/block_storage/v2/service.py b/openstack/block_storage/v2/service.py index cc091bb42..0f8c46c9b 100644 --- a/openstack/block_storage/v2/service.py +++ b/openstack/block_storage/v2/service.py @@ -10,6 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + +from keystoneauth1 import adapter +import typing_extensions as ty_ext from openstack import exceptions from openstack import resource @@ -52,9 +56,72 @@ class Service(resource.Resource): #: The date and time when the resource was updated updated_at = resource.Body('updated_at') + @ty.overload + @classmethod + def find( + cls, + session: adapter.Adapter, + name_or_id: str, + ignore_missing: ty.Literal[True] = True, + list_base_path: str | None = None, + *, + microversion: str | None = None, + all_projects: bool | None = None, + **params: ty.Any, + ) -> ty_ext.Self | None: ... + + @ty.overload + @classmethod + def find( + cls, + session: adapter.Adapter, + name_or_id: str, + ignore_missing: ty.Literal[False], + list_base_path: str | None = None, + *, + microversion: str | None = None, + all_projects: bool | None = None, + **params: ty.Any, + ) -> ty_ext.Self: ... + + # excuse the duplication here: it's mypy's fault + # https://github.com/python/mypy/issues/14764 + @ty.overload + @classmethod + def find( + cls, + session: adapter.Adapter, + name_or_id: str, + ignore_missing: bool, + list_base_path: str | None = None, + *, + microversion: str | None = None, + all_projects: bool | None = None, + **params: ty.Any, + ) -> ty_ext.Self | None: ... + @classmethod - def find(cls, session, name_or_id, ignore_missing=True, **params): + def find( + cls, + session: adapter.Adapter, + name_or_id: str, + ignore_missing: bool = True, + list_base_path: str | None = None, + *, + microversion: str | None = None, + all_projects: bool | None = None, + **params: ty.Any, + ) -> ty_ext.Self | None: # No direct request possible, thus go directly to list + if list_base_path: + params['base_path'] = list_base_path + + # all_projects is a special case that is used by multiple services. We + # handle it here since it doesn't make sense to pass it to the .fetch + # call above + if all_projects is not None: + params['all_projects'] = all_projects + data = cls.list(session, **params) result = None diff --git a/openstack/block_storage/v3/service.py b/openstack/block_storage/v3/service.py index cefd65975..6141d3c06 100644 --- a/openstack/block_storage/v3/service.py +++ b/openstack/block_storage/v3/service.py @@ -13,7 +13,8 @@ import enum import typing as ty -from keystoneauth1 import adapter as ksa_adapter +from keystoneauth1 import adapter +import typing_extensions as ty_ext from openstack import exceptions from openstack import resource @@ -95,9 +96,72 @@ class Service(resource.Resource): # 3.32 introduced the 'set-log' action _max_microversion = '3.32' + @ty.overload @classmethod - def find(cls, session, name_or_id, ignore_missing=True, **params): + def find( + cls, + session: adapter.Adapter, + name_or_id: str, + ignore_missing: ty.Literal[True] = True, + list_base_path: str | None = None, + *, + microversion: str | None = None, + all_projects: bool | None = None, + **params: ty.Any, + ) -> ty_ext.Self | None: ... + + @ty.overload + @classmethod + def find( + cls, + session: adapter.Adapter, + name_or_id: str, + ignore_missing: ty.Literal[False], + list_base_path: str | None = None, + *, + microversion: str | None = None, + all_projects: bool | None = None, + **params: ty.Any, + ) -> ty_ext.Self: ... + + # excuse the duplication here: it's mypy's fault + # https://github.com/python/mypy/issues/14764 + @ty.overload + @classmethod + def find( + cls, + session: adapter.Adapter, + name_or_id: str, + ignore_missing: bool, + list_base_path: str | None = None, + *, + microversion: str | None = None, + all_projects: bool | None = None, + **params: ty.Any, + ) -> ty_ext.Self | None: ... + + @classmethod + def find( + cls, + session: adapter.Adapter, + name_or_id: str, + ignore_missing: bool = True, + list_base_path: str | None = None, + *, + microversion: str | None = None, + all_projects: bool | None = None, + **params: ty.Any, + ) -> ty_ext.Self | None: # No direct request possible, thus go directly to list + if list_base_path: + params['base_path'] = list_base_path + + # all_projects is a special case that is used by multiple services. We + # handle it here since it doesn't make sense to pass it to the .fetch + # call above + if all_projects is not None: + params['all_projects'] = all_projects + data = cls.list(session, **params) result = None @@ -172,7 +236,7 @@ def freeze(self, session): @classmethod def set_log_levels( cls, - session: ksa_adapter.Adapter, + session: adapter.Adapter, *, level: Level, binary: Binary | None = None, @@ -207,7 +271,7 @@ def set_log_levels( @classmethod def get_log_levels( cls, - session: ksa_adapter.Adapter, + session: adapter.Adapter, *, binary: Binary | None = None, server: str | None = None, diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 57256e0d0..2f45e94ce 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -73,6 +73,13 @@ class _OpenStackCloudMixin(_services_mixin.ServicesMixin): config: cloud_region.CloudRegion + cache_enabled: bool + _cache_expirations: dict[str, int] + _cache: 'cache_region.CacheRegion' + + verify: bool | str | None + cert: str | tuple[str, str] | None + def __init__( self, cloud: str | None = None, @@ -228,7 +235,7 @@ def __init__( self.default_interface = self.config.get_interface() self.force_ipv4 = self.config.force_ipv4 - (self.verify, self.cert) = self.config.get_requests_verify_args() + self.verify, self.cert = self.config.get_requests_verify_args() # Turn off urllib3 warnings about insecure certs if we have # explicitly configured requests to tell it we do not want diff --git a/openstack/common/quota_set.py b/openstack/common/quota_set.py index ce8517010..08c7a7bef 100644 --- a/openstack/common/quota_set.py +++ b/openstack/common/quota_set.py @@ -142,7 +142,7 @@ def _translate_response( self._header.attributes.update(headers) self._header.clean() self._update_location() - dict.update(self, self.to_dict()) # type: ignore + dict.update(self, self.to_dict()) def _prepare_request_body( self, diff --git a/openstack/compute/v2/flavor.py b/openstack/compute/v2/flavor.py index 40d7a2588..e17b57aa7 100644 --- a/openstack/compute/v2/flavor.py +++ b/openstack/compute/v2/flavor.py @@ -10,6 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + +from keystoneauth1 import adapter +import typing_extensions as ty_ext + from openstack import exceptions from openstack import resource from openstack import utils @@ -91,11 +96,16 @@ def __getattribute__(self, name): @classmethod def list( cls, - session, - paginated=True, - base_path='/flavors/detail', - **params, - ): + session: adapter.Adapter, + paginated: bool = True, + base_path: str | None = '/flavors/detail', + allow_unknown_params: bool = False, + *, + microversion: str | None = None, + headers: dict[str, str] | None = None, + max_items: int | None = None, + **params: ty.Any, + ) -> ty.Generator[ty_ext.Self, None, None]: # Find will invoke list when name was passed. Since we want to return # flavor with details (same as direct get) we need to swap default here # and list with "/flavors" if no details explicitely requested @@ -104,7 +114,14 @@ def list( # Force it to string to avoid requests skipping it. params['is_public'] = 'None' return super().list( - session, paginated=paginated, base_path=base_path, **params + session, + paginated=paginated, + base_path=base_path, + allow_unknown_params=allow_unknown_params, + microversion=microversion, + headers=headers, + max_items=max_items, + **params, ) def _action(self, session, body, microversion=None): diff --git a/openstack/compute/v2/server_ip.py b/openstack/compute/v2/server_ip.py index 8bac8d10b..519a48fc6 100644 --- a/openstack/compute/v2/server_ip.py +++ b/openstack/compute/v2/server_ip.py @@ -10,6 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + +from keystoneauth1 import adapter +import typing_extensions as ty_ext + from openstack import resource from openstack import utils @@ -34,13 +39,18 @@ class ServerIP(resource.Resource): @classmethod def list( cls, - session, - paginated=False, - server_id=None, - network_label=None, - base_path=None, - **params, - ): + session: adapter.Adapter, + paginated: bool = False, + base_path: str | None = None, + allow_unknown_params: bool = False, + *, + microversion: str | None = None, + headers: dict[str, str] | None = None, + max_items: int | None = None, + server_id: str | None = None, + network_label: str | None = None, + **params: ty.Any, + ) -> ty.Generator[ty_ext.Self, None, None]: if base_path is None: base_path = cls.base_path diff --git a/openstack/compute/v2/service.py b/openstack/compute/v2/service.py index 0623fa0a7..68b0ee5bc 100644 --- a/openstack/compute/v2/service.py +++ b/openstack/compute/v2/service.py @@ -10,6 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + +from keystoneauth1 import adapter +import typing_extensions as ty_ext + from openstack import exceptions from openstack import resource from openstack import utils @@ -55,10 +60,76 @@ class Service(resource.Resource): _max_microversion = '2.69' + @ty.overload + @classmethod + def find( + cls, + session: adapter.Adapter, + name_or_id: str, + ignore_missing: ty.Literal[True] = True, + list_base_path: str | None = None, + *, + microversion: str | None = None, + all_projects: bool | None = None, + **params: ty.Any, + ) -> ty_ext.Self | None: ... + + @ty.overload + @classmethod + def find( + cls, + session: adapter.Adapter, + name_or_id: str, + ignore_missing: ty.Literal[False], + list_base_path: str | None = None, + *, + microversion: str | None = None, + all_projects: bool | None = None, + **params: ty.Any, + ) -> ty_ext.Self: ... + + # excuse the duplication here: it's mypy's fault + # https://github.com/python/mypy/issues/14764 + @ty.overload + @classmethod + def find( + cls, + session: adapter.Adapter, + name_or_id: str, + ignore_missing: bool, + list_base_path: str | None = None, + *, + microversion: str | None = None, + all_projects: bool | None = None, + **params: ty.Any, + ) -> ty_ext.Self | None: ... + @classmethod - def find(cls, session, name_or_id, ignore_missing=True, **params): + def find( + cls, + session: adapter.Adapter, + name_or_id: str, + ignore_missing: bool = True, + list_base_path: str | None = None, + *, + microversion: str | None = None, + all_projects: bool | None = None, + **params: ty.Any, + ) -> ty_ext.Self | None: # No direct request possible, thus go directly to list - data = cls.list(session, **params) + if list_base_path: + params['base_path'] = list_base_path + + # all_projects is a special case that is used by multiple services. We + # handle it here since it doesn't make sense to pass it to the .fetch + # call above + if all_projects is not None: + params['all_projects'] = all_projects + + data = cls.list( + session, + base_path=list_base_path, + ) result = None for maybe_result in data: diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index b7899358b..ffb58e5be 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -434,7 +434,9 @@ def set_session_constructor( """Sets the Session constructor.""" self._session_constructor = session_constructor - def get_requests_verify_args(self) -> tuple[bool, str | None]: + def get_requests_verify_args( + self, + ) -> tuple[bool | str | None, str | tuple[str, str] | None]: """Return the verify and cert values for the requests library.""" insecure = self.config.get('insecure', False) verify = self.config.get('verify', True) diff --git a/openstack/dns/v2/_base.py b/openstack/dns/v2/_base.py index 16aa81079..f7c6acf6c 100644 --- a/openstack/dns/v2/_base.py +++ b/openstack/dns/v2/_base.py @@ -10,15 +10,73 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty import urllib.parse +from keystoneauth1 import adapter +import typing_extensions as ty_ext + from openstack import exceptions from openstack import resource class Resource(resource.Resource): + @ty.overload + @classmethod + def find( + cls, + session: adapter.Adapter, + name_or_id: str, + ignore_missing: ty.Literal[True] = True, + list_base_path: str | None = None, + *, + microversion: str | None = None, + all_projects: bool | None = None, + **params: ty.Any, + ) -> ty_ext.Self | None: ... + + @ty.overload + @classmethod + def find( + cls, + session: adapter.Adapter, + name_or_id: str, + ignore_missing: ty.Literal[False], + list_base_path: str | None = None, + *, + microversion: str | None = None, + all_projects: bool | None = None, + **params: ty.Any, + ) -> ty_ext.Self: ... + + # excuse the duplication here: it's mypy's fault + # https://github.com/python/mypy/issues/14764 + @ty.overload + @classmethod + def find( + cls, + session: adapter.Adapter, + name_or_id: str, + ignore_missing: bool, + list_base_path: str | None = None, + *, + microversion: str | None = None, + all_projects: bool | None = None, + **params: ty.Any, + ) -> ty_ext.Self | None: ... + @classmethod - def find(cls, session, name_or_id, ignore_missing=True, **params): + def find( + cls, + session: adapter.Adapter, + name_or_id: str, + ignore_missing: bool = True, + list_base_path: str | None = None, + *, + microversion: str | None = None, + all_projects: bool | None = None, + **params: ty.Any, + ) -> ty_ext.Self | None: """Find a resource by its name or id. :param session: The session to use for making this request. @@ -46,7 +104,9 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): # Try to short-circuit by looking directly for a matching ID. try: match = cls.existing( - id=name_or_id, connection=session._get_connection(), **params + id=name_or_id, + connection=session._get_connection(), # type: ignore + **params, ) return match.fetch(session) except exceptions.SDKException: @@ -59,7 +119,13 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): ): params['name'] = name_or_id - data = cls.list(session, **params) + data = cls.list( + session, + list_base_path=list_base_path, + microversion=microversion, + all_projects=all_projects, + **params, + ) result = cls._get_one_match(name_or_id, data) if result is not None: @@ -74,16 +140,21 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): @classmethod def list( cls, - session, - project_id=None, - all_projects=None, - **params, - ): - headers: dict[str, str] | None = ( - {} if project_id or all_projects else None - ) - - if headers is not None: + session: adapter.Adapter, + paginated: bool = True, + base_path: str | None = None, + allow_unknown_params: bool = False, + *, + microversion: str | None = None, + headers: dict[str, str] | None = None, + max_items: int | None = None, + project_id: str | None = None, + all_projects: bool | None = None, + **params: ty.Any, + ) -> ty.Generator[ty_ext.Self, None, None]: + if project_id or all_projects is not None: + if headers is None: + headers = {} if project_id: headers["x-auth-sudo-project-id"] = str(project_id) if all_projects: diff --git a/openstack/identity/v2/extension.py b/openstack/identity/v2/extension.py index 7e47bf610..629e31d64 100644 --- a/openstack/identity/v2/extension.py +++ b/openstack/identity/v2/extension.py @@ -10,6 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + +from keystoneauth1 import adapter +import typing_extensions as ty_ext + from openstack import resource @@ -43,7 +48,18 @@ class Extension(resource.Resource): updated_at = resource.Body('updated') @classmethod - def list(cls, session, paginated=False, base_path=None, **params): + def list( + cls, + session: adapter.Adapter, + paginated: bool = True, + base_path: str | None = None, + allow_unknown_params: bool = False, + *, + microversion: str | None = None, + headers: dict[str, str] | None = None, + max_items: int | None = None, + **params: ty.Any, + ) -> ty.Generator[ty_ext.Self, None, None]: if base_path is None: base_path = cls.base_path diff --git a/openstack/identity/v3/limit.py b/openstack/identity/v3/limit.py index 0b83e1cac..891aed2b0 100644 --- a/openstack/identity/v3/limit.py +++ b/openstack/identity/v3/limit.py @@ -150,4 +150,4 @@ def _translate_response( self._header.attributes.update(headers) self._header.clean() self._update_location() - dict.update(self, self.to_dict()) # type: ignore + dict.update(self, self.to_dict()) diff --git a/openstack/identity/v3/registered_limit.py b/openstack/identity/v3/registered_limit.py index 4931a04f5..5a7de0cfe 100644 --- a/openstack/identity/v3/registered_limit.py +++ b/openstack/identity/v3/registered_limit.py @@ -149,4 +149,4 @@ def _translate_response( self._header.attributes.update(headers) self._header.clean() self._update_location() - dict.update(self, self.to_dict()) # type: ignore + dict.update(self, self.to_dict()) diff --git a/openstack/identity/version.py b/openstack/identity/version.py index 2742e7a22..ceafc70a7 100644 --- a/openstack/identity/version.py +++ b/openstack/identity/version.py @@ -10,6 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + +from keystoneauth1 import adapter +import typing_extensions as ty_ext + from openstack import resource @@ -27,7 +32,17 @@ class Version(resource.Resource): updated = resource.Body('updated') @classmethod - def list(cls, session, paginated=False, base_path=None, **params): + def list( + cls, + session: adapter.Adapter, + paginated: bool = True, + base_path: str | None = None, + allow_unknown_params: bool = False, + *, + microversion: str | None = None, + headers: dict[str, str] | None = None, + **params: ty.Any, + ) -> ty.Generator[ty_ext.Self, None, None]: if base_path is None: base_path = cls.base_path diff --git a/openstack/image/v1/image.py b/openstack/image/v1/image.py index 173e2544a..9868d0c34 100644 --- a/openstack/image/v1/image.py +++ b/openstack/image/v1/image.py @@ -10,6 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + +from keystoneauth1 import adapter +import typing_extensions as ty_ext + from openstack import exceptions from openstack.image import _download from openstack import resource @@ -87,8 +92,62 @@ class Image(resource.Resource, _download.DownloadMixin): #: The timestamp when this image was last updated. updated_at = resource.Body('updated_at') + @ty.overload @classmethod - def find(cls, session, name_or_id, ignore_missing=True, **params): + def find( + cls, + session: adapter.Adapter, + name_or_id: str, + ignore_missing: ty.Literal[True] = True, + list_base_path: str | None = None, + *, + microversion: str | None = None, + all_projects: bool | None = None, + **params: ty.Any, + ) -> ty_ext.Self | None: ... + + @ty.overload + @classmethod + def find( + cls, + session: adapter.Adapter, + name_or_id: str, + ignore_missing: ty.Literal[False], + list_base_path: str | None = None, + *, + microversion: str | None = None, + all_projects: bool | None = None, + **params: ty.Any, + ) -> ty_ext.Self: ... + + # excuse the duplication here: it's mypy's fault + # https://github.com/python/mypy/issues/14764 + @ty.overload + @classmethod + def find( + cls, + session: adapter.Adapter, + name_or_id: str, + ignore_missing: bool, + list_base_path: str | None = None, + *, + microversion: str | None = None, + all_projects: bool | None = None, + **params: ty.Any, + ) -> ty_ext.Self | None: ... + + @classmethod + def find( + cls, + session: adapter.Adapter, + name_or_id: str, + ignore_missing: bool = True, + list_base_path: str | None = None, + *, + microversion: str | None = None, + all_projects: bool | None = None, + **params: ty.Any, + ) -> ty_ext.Self | None: """Find a resource by its name or id. :param session: The session to use for making this request. @@ -117,7 +176,7 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): try: match = cls.existing( id=name_or_id, - connection=session._get_connection(), + connection=session._get_connection(), # type: ignore **params, ) return match.fetch(session, **params) @@ -126,7 +185,12 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): params['name'] = name_or_id - data = cls.list(session, base_path='/images/detail', **params) + data = cls.list( + session, + base_path='/images/detail', + all_projects=all_projects, + **params, + ) result = cls._get_one_match(name_or_id, data) if result is not None: diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index 66336e57c..914e97565 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -12,6 +12,9 @@ import typing as ty +from keystoneauth1 import adapter +import typing_extensions as ty_ext + from openstack.common import tag from openstack import exceptions from openstack.image import _download @@ -403,10 +406,72 @@ def _prepare_request( return request + @ty.overload + @classmethod + def find( + cls, + session: adapter.Adapter, + name_or_id: str, + ignore_missing: ty.Literal[True] = True, + list_base_path: str | None = None, + *, + microversion: str | None = None, + all_projects: bool | None = None, + **params: ty.Any, + ) -> ty_ext.Self | None: ... + + @ty.overload + @classmethod + def find( + cls, + session: adapter.Adapter, + name_or_id: str, + ignore_missing: ty.Literal[False], + list_base_path: str | None = None, + *, + microversion: str | None = None, + all_projects: bool | None = None, + **params: ty.Any, + ) -> ty_ext.Self: ... + + # excuse the duplication here: it's mypy's fault + # https://github.com/python/mypy/issues/14764 + @ty.overload @classmethod - def find(cls, session, name_or_id, ignore_missing=True, **params): + def find( + cls, + session: adapter.Adapter, + name_or_id: str, + ignore_missing: bool, + list_base_path: str | None = None, + *, + microversion: str | None = None, + all_projects: bool | None = None, + **params: ty.Any, + ) -> ty_ext.Self | None: ... + + @classmethod + def find( + cls, + session: adapter.Adapter, + name_or_id: str, + ignore_missing: bool = True, + list_base_path: str | None = None, + *, + microversion: str | None = None, + all_projects: bool | None = None, + **params: ty.Any, + ) -> ty_ext.Self | None: # Do a regular search first (ignoring missing) - result = super().find(session, name_or_id, True, **params) + result = super().find( + session, + name_or_id, + ignore_missing=True, + list_base_path=list_base_path, + microversion=microversion, + all_projects=all_projects, + **params, + ) if result: return result diff --git a/openstack/orchestration/v1/stack.py b/openstack/orchestration/v1/stack.py index 8e8712d35..4bd96a77e 100644 --- a/openstack/orchestration/v1/stack.py +++ b/openstack/orchestration/v1/stack.py @@ -9,6 +9,12 @@ # 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 typing as ty + +from keystoneauth1 import adapter +import typing_extensions as ty_ext + from openstack.common import tag from openstack import exceptions from openstack import resource @@ -246,8 +252,62 @@ def fetch( raise exceptions.NotFoundException(f"No stack found for {self.id}") return self + @ty.overload @classmethod - def find(cls, session, name_or_id, ignore_missing=True, **params): + def find( + cls, + session: adapter.Adapter, + name_or_id: str, + ignore_missing: ty.Literal[True] = True, + list_base_path: str | None = None, + *, + microversion: str | None = None, + all_projects: bool | None = None, + **params: ty.Any, + ) -> ty_ext.Self | None: ... + + @ty.overload + @classmethod + def find( + cls, + session: adapter.Adapter, + name_or_id: str, + ignore_missing: ty.Literal[False], + list_base_path: str | None = None, + *, + microversion: str | None = None, + all_projects: bool | None = None, + **params: ty.Any, + ) -> ty_ext.Self: ... + + # excuse the duplication here: it's mypy's fault + # https://github.com/python/mypy/issues/14764 + @ty.overload + @classmethod + def find( + cls, + session: adapter.Adapter, + name_or_id: str, + ignore_missing: bool, + list_base_path: str | None = None, + *, + microversion: str | None = None, + all_projects: bool | None = None, + **params: ty.Any, + ) -> ty_ext.Self | None: ... + + @classmethod + def find( + cls, + session: adapter.Adapter, + name_or_id: str, + ignore_missing: bool = True, + list_base_path: str | None = None, + *, + microversion: str | None = None, + all_projects: bool | None = None, + **params: ty.Any, + ) -> ty_ext.Self | None: """Find a resource by its name or id. :param session: The session to use for making this request. @@ -275,7 +335,9 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): # Try to short-circuit by looking directly for a matching ID. try: match = cls.existing( - id=name_or_id, connection=session._get_connection(), **params + id=name_or_id, + connection=session._get_connection(), # type: ignore + **params, ) return match.fetch(session, **params) except exceptions.NotFoundException: @@ -286,6 +348,7 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): if ignore_missing: return None + raise exceptions.NotFoundException( f"No {cls.__name__} found for {name_or_id}" ) diff --git a/openstack/resource.py b/openstack/resource.py index 0196e6197..1d3bf2dea 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -512,7 +512,7 @@ def __init__(self, _synchronized=False, connection=None, **attrs): # obj.items() ... but I think the if not obj: is short-circuiting down # in the C code and thus since we don't store the data in self[] it's # always False even if we override __len__ or __bool__. - dict.update(self, self.to_dict()) # type: ignore + dict.update(self, self.to_dict()) @classmethod def _attributes_iterator( @@ -703,7 +703,7 @@ def _update(self, **attrs: ty.Any) -> None: # obj.items() ... but I think the if not obj: is short-circuiting down # in the C code and thus since we don't store the data in self[] it's # always False even if we override __len__ or __bool__. - dict.update(self, self.to_dict()) # type: ignore + dict.update(self, self.to_dict()) def _collect_attrs(self, attrs): """Given attributes, return a dict per type of attribute @@ -1268,7 +1268,7 @@ def _translate_response( self._header.attributes.update(headers) self._header.clean() self._update_location() - dict.update(self, self.to_dict()) # type: ignore + dict.update(self, self.to_dict()) @classmethod def _get_session(cls, session: AdapterT) -> AdapterT: From 1317e807c28b41113f3acfd94abae0c9d7c53a90 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 9 Sep 2025 17:35:23 +0100 Subject: [PATCH 3766/3836] Revert "Make ironic job non-voting" This reverts commit 8db08a11bf3a71b15db3f1f1781e48c16470087c. Change-Id: I8eaabee62d5395e0294355d92034f5a72ca9a16c Signed-off-by: Stephen Finucane Related: https://review.opendev.org/c/openstack/ironic/+/960288 --- zuul.d/project.yaml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index 407a300a1..a677dd5e0 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -27,10 +27,7 @@ voting: false - openstacksdk-functional-devstack-manila - openstacksdk-functional-devstack-masakari - # TODO(stephenfin): Make this voting once we get to the bottom of the - # 2 failures - - openstacksdk-functional-devstack-ironic: - voting: false + - openstacksdk-functional-devstack-ironic - osc-functional-devstack-tips: voting: false - ansible-collections-openstack-functional-devstack: @@ -41,5 +38,4 @@ - openstacksdk-functional-devstack - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-networking-ext - # TODO(stephenfin): Per above - # - openstacksdk-functional-devstack-ironic + - openstacksdk-functional-devstack-ironic From 1e58bc49448ad6bc8dba559c93041741680c4938 Mon Sep 17 00:00:00 2001 From: choieastsea Date: Thu, 11 Sep 2025 00:27:33 +0900 Subject: [PATCH 3767/3836] doc: fix volume size unit documentation to GiB The SDK documentation used "GB" while the Cinder API documentation specifies "GiB". This change aligns the SDK with the API reference for consistency. Change-Id: I3961fd21b549b9588140b3418e5b5562dbe85ef3 Signed-off-by: choieastsea --- openstack/block_storage/v2/quota_class_set.py | 6 +++--- openstack/block_storage/v2/snapshot.py | 2 +- openstack/block_storage/v2/volume.py | 2 +- openstack/block_storage/v3/quota_class_set.py | 4 ++-- openstack/block_storage/v3/snapshot.py | 2 +- openstack/block_storage/v3/volume.py | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/openstack/block_storage/v2/quota_class_set.py b/openstack/block_storage/v2/quota_class_set.py index d09c34ab8..8cf62d272 100644 --- a/openstack/block_storage/v2/quota_class_set.py +++ b/openstack/block_storage/v2/quota_class_set.py @@ -22,16 +22,16 @@ class QuotaClassSet(resource.Resource): allow_commit = True # Properties - #: The size (GB) of backups that are allowed for each project. + #: The size (GiB) of backups that are allowed for each project. backup_gigabytes = resource.Body('backup_gigabytes', type=int) #: The number of backups that are allowed for each project. backups = resource.Body('backups', type=int) - #: The size (GB) of volumes and snapshots that are allowed for each + #: The size (GiB) of volumes and snapshots that are allowed for each #: project. gigabytes = resource.Body('gigabytes', type=int) #: The number of groups that are allowed for each project. groups = resource.Body('groups', type=int) - #: The size (GB) of volumes in request that are allowed for each volume. + #: The size (GiB) of volumes in request that are allowed for each volume. per_volume_gigabytes = resource.Body('per_volume_gigabytes', type=int) #: The number of snapshots that are allowed for each project. snapshots = resource.Body('snapshots', type=int) diff --git a/openstack/block_storage/v2/snapshot.py b/openstack/block_storage/v2/snapshot.py index 6e3929187..748bf96df 100644 --- a/openstack/block_storage/v2/snapshot.py +++ b/openstack/block_storage/v2/snapshot.py @@ -44,7 +44,7 @@ class Snapshot(resource.Resource, metadata.MetadataMixin): #: Indicate whether to create snapshot, even if the volume is attached. #: Default is ``False``. *Type: bool* is_forced = resource.Body("force", type=format.BoolStr) - #: The size of the volume, in GBs. + #: The size of the volume, in gibibytes (GiB). size = resource.Body("size", type=int) #: The current status of this snapshot. Potential values are creating, #: available, deleting, error, and error_deleting. diff --git a/openstack/block_storage/v2/volume.py b/openstack/block_storage/v2/volume.py index 240b0880b..0eb8808f7 100644 --- a/openstack/block_storage/v2/volume.py +++ b/openstack/block_storage/v2/volume.py @@ -76,7 +76,7 @@ class Volume(resource.Resource, metadata.MetadataMixin): replication_status = resource.Body("replication_status") #: Scheduler hints for the volume scheduler_hints = resource.Body('OS-SCH-HNT:scheduler_hints', type=dict) - #: The size of the volume, in GBs. *Type: int* + #: The size of the volume, in gibibytes (GiB). *Type: int* size = resource.Body("size", type=int) #: To create a volume from an existing snapshot, specify the ID of #: the existing volume snapshot. If specified, the volume is created diff --git a/openstack/block_storage/v3/quota_class_set.py b/openstack/block_storage/v3/quota_class_set.py index d09c34ab8..440fa2bd7 100644 --- a/openstack/block_storage/v3/quota_class_set.py +++ b/openstack/block_storage/v3/quota_class_set.py @@ -22,11 +22,11 @@ class QuotaClassSet(resource.Resource): allow_commit = True # Properties - #: The size (GB) of backups that are allowed for each project. + #: The size (GiB) of backups that are allowed for each project. backup_gigabytes = resource.Body('backup_gigabytes', type=int) #: The number of backups that are allowed for each project. backups = resource.Body('backups', type=int) - #: The size (GB) of volumes and snapshots that are allowed for each + #: The size (GiB) of volumes and snapshots that are allowed for each #: project. gigabytes = resource.Body('gigabytes', type=int) #: The number of groups that are allowed for each project. diff --git a/openstack/block_storage/v3/snapshot.py b/openstack/block_storage/v3/snapshot.py index 4331029bd..00a6b38c1 100644 --- a/openstack/block_storage/v3/snapshot.py +++ b/openstack/block_storage/v3/snapshot.py @@ -66,7 +66,7 @@ class Snapshot(resource.Resource, metadata.MetadataMixin): progress = resource.Body("os-extended-snapshot-attributes:progress") #: The project ID this snapshot is associated with. project_id = resource.Body("os-extended-snapshot-attributes:project_id") - #: The size of the volume, in GBs. + #: The size of the volume, in gibibytes (GiB). size = resource.Body("size", type=int) #: The current status of this snapshot. Potential values are creating, #: available, deleting, error, and error_deleting. diff --git a/openstack/block_storage/v3/volume.py b/openstack/block_storage/v3/volume.py index 04a0b0cec..8fe389d8e 100644 --- a/openstack/block_storage/v3/volume.py +++ b/openstack/block_storage/v3/volume.py @@ -112,7 +112,7 @@ class Volume(resource.Resource, metadata.MetadataMixin): #: locks, and null means to always use locks. Look at os-brick's #: guard_connection context manager. Default=True. (since 3.48) shared_targets = resource.Body("shared_targets", type=bool) - #: The size of the volume, in GBs. + #: The size of the volume, in gibibytes (GiB). size = resource.Body("size", type=int) #: To create a volume from an existing snapshot, specify the ID of #: the existing volume snapshot. If specified, the volume is created From a4239651d82a7bf25ae69285a9d78ededdbcc590 Mon Sep 17 00:00:00 2001 From: Kim-Yukyung Date: Sat, 30 Aug 2025 16:02:51 +0900 Subject: [PATCH 3768/3836] Add Secret Stores API support to Key Manager Implement the following Barbican Secret Stores APIs - GET /v1/secret-stores - GET /v1/secret-stores/global-default - GET /v1/secret-stores/preferred Includes SecretStore resource class, proxy methods, comprehensive tests, and documentation. Change-Id: I9dc3a46578b79537b0787d03082a2fa217161458 Signed-off-by: Kim-Yukyung --- doc/source/user/proxies/key_manager.rst | 8 +++ .../user/resources/key_manager/index.rst | 1 + .../resources/key_manager/v1/secret_store.rst | 12 ++++ openstack/key_manager/v1/_proxy.py | 43 ++++++++++++++ openstack/key_manager/v1/secret_store.py | 58 +++++++++++++++++++ .../tests/functional/key_manager/__init__.py | 0 .../functional/key_manager/v1/__init__.py | 0 .../key_manager/v1/test_secret_store.py | 55 ++++++++++++++++++ .../tests/unit/key_manager/v1/test_proxy.py | 6 ++ .../unit/key_manager/v1/test_secret_store.py | 55 ++++++++++++++++++ 10 files changed, 238 insertions(+) create mode 100644 doc/source/user/resources/key_manager/v1/secret_store.rst create mode 100644 openstack/key_manager/v1/secret_store.py create mode 100644 openstack/tests/functional/key_manager/__init__.py create mode 100644 openstack/tests/functional/key_manager/v1/__init__.py create mode 100644 openstack/tests/functional/key_manager/v1/test_secret_store.py create mode 100644 openstack/tests/unit/key_manager/v1/test_secret_store.py diff --git a/doc/source/user/proxies/key_manager.rst b/doc/source/user/proxies/key_manager.rst index 2b611e9dc..f5d7309ac 100644 --- a/doc/source/user/proxies/key_manager.rst +++ b/doc/source/user/proxies/key_manager.rst @@ -37,3 +37,11 @@ Order Operations :noindex: :members: create_order, update_order, delete_order, get_order, find_order, orders + +Secret Store Operations +^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.key_manager.v1._proxy.Proxy + :noindex: + :members: secret_stores, get_global_default_secret_store, + get_preferred_secret_store diff --git a/doc/source/user/resources/key_manager/index.rst b/doc/source/user/resources/key_manager/index.rst index 76b6659c6..5f881b458 100644 --- a/doc/source/user/resources/key_manager/index.rst +++ b/doc/source/user/resources/key_manager/index.rst @@ -7,3 +7,4 @@ KeyManager Resources v1/container v1/order v1/secret + v1/secret_store diff --git a/doc/source/user/resources/key_manager/v1/secret_store.rst b/doc/source/user/resources/key_manager/v1/secret_store.rst new file mode 100644 index 000000000..f40902b50 --- /dev/null +++ b/doc/source/user/resources/key_manager/v1/secret_store.rst @@ -0,0 +1,12 @@ +openstack.key_manager.v1.secret_store +===================================== + +.. automodule:: openstack.key_manager.v1.secret_store + +The SecretStore Class +--------------------- + +The ``SecretStore`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.key_manager.v1.secret_store.SecretStore + :members: diff --git a/openstack/key_manager/v1/_proxy.py b/openstack/key_manager/v1/_proxy.py index 05f3f347a..c7e1addea 100644 --- a/openstack/key_manager/v1/_proxy.py +++ b/openstack/key_manager/v1/_proxy.py @@ -15,6 +15,7 @@ from openstack.key_manager.v1 import container as _container from openstack.key_manager.v1 import order as _order from openstack.key_manager.v1 import secret as _secret +from openstack.key_manager.v1 import secret_store as _secret_store from openstack import proxy from openstack import resource @@ -24,6 +25,7 @@ class Proxy(proxy.Proxy): "container": _container.Container, "order": _order.Order, "secret": _secret.Secret, + "secret_store": _secret_store.SecretStore, } def create_container(self, **attrs): @@ -270,6 +272,47 @@ def update_secret(self, secret, **attrs): """ return self._update(_secret.Secret, secret, **attrs) + # ========== Secret Store Operations ========== + + def secret_stores(self, **query): + """Return a generator of secret stores + + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. + + :returns: A generator of secret store objects + :rtype: :class:`~openstack.key_manager.v1.secret_store.SecretStore` + """ + return self._list(_secret_store.SecretStore, **query) + + def get_global_default_secret_store(self): + """Get the global default secret store + + :returns: One :class:`~openstack.key_manager.v1.secret_store.SecretStore` + :raises: :class:`~openstack.exceptions.NotFoundException` + when no resource can be found. + """ + return self._get( + _secret_store.SecretStore, + None, + requires_id=False, + base_path='/secret-stores/global-default', + ) + + def get_preferred_secret_store(self): + """Get the preferred secret store for the current project + + :returns: One :class:`~openstack.key_manager.v1.secret_store.SecretStore` + :raises: :class:`~openstack.exceptions.NotFoundException` + when no resource can be found. + """ + return self._get( + _secret_store.SecretStore, + None, + requires_id=False, + base_path='/secret-stores/preferred', + ) + # ========== Utilities ========== def wait_for_status( diff --git a/openstack/key_manager/v1/secret_store.py b/openstack/key_manager/v1/secret_store.py new file mode 100644 index 000000000..f6798d96c --- /dev/null +++ b/openstack/key_manager/v1/secret_store.py @@ -0,0 +1,58 @@ +# 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.key_manager.v1 import _format +from openstack import resource + + +class SecretStore(resource.Resource): + resources_key = 'secret_stores' + base_path = '/secret-stores' + + # capabilities + allow_create = False + allow_fetch = True + allow_commit = False + allow_delete = False + allow_list = True + + _query_mapping = resource.QueryParameters( + "name", + "status", + "global_default", + "crypto_plugin", + "secret_store_plugin", + "created", + "updated", + ) + + # Properties + #: The name of the secret store + name = resource.Body('name') + #: The status of the secret store + status = resource.Body('status') + #: Timestamp of when the secret store was created + created_at = resource.Body('created') + #: Timestamp of when the secret store was last updated + updated_at = resource.Body('updated') + #: A URI to the secret store + secret_store_ref = resource.Body('secret_store_ref') + #: The ID of the secret store + secret_store_id = resource.Body( + 'secret_store_ref', alternate_id=True, type=_format.HREFToUUID + ) + #: Flag indicating if this secret store is global default + global_default = resource.Body('global_default', type=bool) + #: The crypto plugin name + crypto_plugin = resource.Body('crypto_plugin') + #: The secret store plugin name + secret_store_plugin = resource.Body('secret_store_plugin') diff --git a/openstack/tests/functional/key_manager/__init__.py b/openstack/tests/functional/key_manager/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/functional/key_manager/v1/__init__.py b/openstack/tests/functional/key_manager/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/functional/key_manager/v1/test_secret_store.py b/openstack/tests/functional/key_manager/v1/test_secret_store.py new file mode 100644 index 000000000..8c1b5b0f8 --- /dev/null +++ b/openstack/tests/functional/key_manager/v1/test_secret_store.py @@ -0,0 +1,55 @@ +# 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.key_manager.v1 import secret_store as _secret_store +from openstack.tests.functional import base + + +class TestSecretStore(base.BaseFunctionalTest): + def setUp(self): + super().setUp() + self.require_service('key-manager') + + def test_secret_store(self): + """Test Secret Store operations""" + key_manager = self.operator_cloud.key_manager + + # Test list secret stores + secret_stores = list(key_manager.secret_stores()) + self.assertIsInstance(secret_stores, list) + + for store in secret_stores: + self.assertIsInstance(store, _secret_store.SecretStore) + self.assertIsNotNone(store.name) + self.assertIsNotNone(store.status) + + # Test list secret stores with filters + global_default_stores = list( + key_manager.secret_stores(global_default=True) + ) + self.assertIsInstance(global_default_stores, list) + + active_stores = list(key_manager.secret_stores(status="ACTIVE")) + self.assertIsInstance(active_stores, list) + + # Test get global default secret store + if global_default_stores: + default_store = key_manager.get_global_default_secret_store() + self.assertIsInstance(default_store, _secret_store.SecretStore) + self.assertIsNotNone(default_store.name) + self.assertTrue(default_store.global_default) + + # Test get preferred secret store + if secret_stores: + preferred_store = key_manager.get_preferred_secret_store() + self.assertIsInstance(preferred_store, _secret_store.SecretStore) + self.assertIsNotNone(preferred_store.name) diff --git a/openstack/tests/unit/key_manager/v1/test_proxy.py b/openstack/tests/unit/key_manager/v1/test_proxy.py index f770f11d6..4ff6ceb44 100644 --- a/openstack/tests/unit/key_manager/v1/test_proxy.py +++ b/openstack/tests/unit/key_manager/v1/test_proxy.py @@ -14,6 +14,7 @@ from openstack.key_manager.v1 import container from openstack.key_manager.v1 import order from openstack.key_manager.v1 import secret +from openstack.key_manager.v1 import secret_store from openstack.tests.unit import test_proxy_base @@ -97,3 +98,8 @@ def test_secrets(self): def test_secret_update(self): self.verify_update(self.proxy.update_secret, secret.Secret) + + +class TestKeyManagerSecretStore(TestKeyManagerProxy): + def test_secret_stores(self): + self.verify_list(self.proxy.secret_stores, secret_store.SecretStore) diff --git a/openstack/tests/unit/key_manager/v1/test_secret_store.py b/openstack/tests/unit/key_manager/v1/test_secret_store.py new file mode 100644 index 000000000..08fee411d --- /dev/null +++ b/openstack/tests/unit/key_manager/v1/test_secret_store.py @@ -0,0 +1,55 @@ +# 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.key_manager.v1 import secret_store +from openstack.tests.unit import base + + +EXAMPLE = { + "status": "ACTIVE", + "updated": "2016-08-22T23:46:45.114283", + "name": "PKCS11 HSM", + "created": "2016-08-22T23:46:45.114283", + "secret_store_ref": "http://localhost:9311/v1/secret-stores/4d27b7a7-b82f-491d-88c0-746bd67dadc8", + "global_default": True, + "crypto_plugin": "p11_crypto", + "secret_store_plugin": "store_crypto", +} + + +class TestSecretStore(base.TestCase): + def test_basic(self): + sot = secret_store.SecretStore() + self.assertEqual('secret_stores', sot.resources_key) + self.assertEqual('/secret-stores', sot.base_path) + self.assertFalse(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = secret_store.SecretStore(**EXAMPLE) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['status'], sot.status) + self.assertEqual(EXAMPLE['created'], sot.created_at) + self.assertEqual(EXAMPLE['updated'], sot.updated_at) + self.assertEqual(EXAMPLE['secret_store_ref'], sot.secret_store_ref) + self.assertEqual(EXAMPLE['global_default'], sot.global_default) + self.assertEqual(EXAMPLE['crypto_plugin'], sot.crypto_plugin) + self.assertEqual( + EXAMPLE['secret_store_plugin'], sot.secret_store_plugin + ) + # Test the alternate_id extraction + self.assertEqual( + '4d27b7a7-b82f-491d-88c0-746bd67dadc8', sot.secret_store_id + ) From 8e59a7f1a7616d0ff9794ca3a89e1790c905a144 Mon Sep 17 00:00:00 2001 From: kchaea Date: Sat, 13 Sep 2025 14:07:47 +0900 Subject: [PATCH 3769/3836] Add functional tests for user CRUD This patch adds functional test support for the following user commands: - user create - user show - user list - user set - user delete Change-Id: Idab3ae6b8e2d7c2a7222ab9b2a32cb6a3bc69810 Signed-off-by: kchaea --- .../tests/functional/identity/v3/test_user.py | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 openstack/tests/functional/identity/v3/test_user.py diff --git a/openstack/tests/functional/identity/v3/test_user.py b/openstack/tests/functional/identity/v3/test_user.py new file mode 100644 index 000000000..61e8614ef --- /dev/null +++ b/openstack/tests/functional/identity/v3/test_user.py @@ -0,0 +1,71 @@ +# 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.identity.v3 import user as _user +from openstack.tests.functional import base + + +class TestUser(base.BaseFunctionalTest): + def setUp(self): + super().setUp() + + self.username = self.getUniqueString('user') + self.password = "test_password_123" # noqa: S105 + self.email = f"{self.username}@example.com" + self.description = "Test user for functional testing" + + def _delete_user(self, user): + ret = self.operator_cloud.identity.delete_user(user) + self.assertIsNone(ret) + + def test_user(self): + # Create user + user = self.operator_cloud.identity.create_user( + name=self.username, + password=self.password, + email=self.email, + description=self.description, + ) + self.addCleanup(self._delete_user, user) + self.assertIsInstance(user, _user.User) + self.assertIsNotNone(user.id) + self.assertEqual(self.username, user.name) + self.assertEqual(self.email, user.email) + self.assertEqual(self.description, user.description) + + # Update user + new_email = f"updated_{self.username}@example.com" + new_description = "Updated description for test user" + + updated_user = self.operator_cloud.identity.update_user( + user.id, email=new_email, description=new_description + ) + self.assertIsInstance(updated_user, _user.User) + self.assertEqual(new_email, updated_user.email) + self.assertEqual(new_description, updated_user.description) + self.assertEqual( + self.username, updated_user.name + ) # Name should remain unchanged + + # Read user list + users = list(self.operator_cloud.identity.users()) + self.assertIsInstance(users[0], _user.User) + user_ids = {ep.id for ep in users} + self.assertIn(user.id, user_ids) + + # Read user by ID + user = self.operator_cloud.identity.get_user(user.id) + self.assertIsInstance(user, _user.User) + self.assertEqual(user.id, user.id) + self.assertEqual(self.username, user.name) + self.assertEqual(new_email, user.email) + self.assertEqual(new_description, user.description) From acbb787757eb048c38ffc96348db2d7c610a1765 Mon Sep 17 00:00:00 2001 From: Tony Breeds Date: Tue, 24 Jun 2025 12:31:21 +1000 Subject: [PATCH 3770/3836] Remove un-needed nodepool testing Signed-off-by: Tony Breeds Change-Id: I92db678e31a749fccf266dd667e4bf49f0ff9af3 --- zuul.d/project.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index c01f57827..8f60cf08a 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -15,9 +15,6 @@ check: jobs: - openstack-tox-py312 - - opendev-buildset-registry - - nodepool-build-image-siblings: - voting: false - openstacksdk-functional-devstack - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-networking-ext @@ -35,7 +32,6 @@ voting: false gate: jobs: - - opendev-buildset-registry - openstacksdk-functional-devstack - openstacksdk-functional-devstack-networking - openstacksdk-functional-devstack-networking-ext From 3ba50349249ad4466f48fb11f9ed3afaf312cfd2 Mon Sep 17 00:00:00 2001 From: jbeen Date: Sat, 6 Sep 2025 21:16:21 +0900 Subject: [PATCH 3771/3836] Add support for key manager project quota API Add get, update, and delete methods for ProjectQuota in key-manager. Include related documentation, unit tests, and functional tests. Change-Id: I9d995066a2183dd9310ad276a273fa725c0e57b2 Signed-off-by: jbeen --- doc/source/user/proxies/key_manager.rst | 7 ++ .../user/resources/key_manager/index.rst | 1 + .../key_manager/v1/project_quota.rst | 12 +++ openstack/key_manager/v1/_proxy.py | 43 +++++++++ openstack/key_manager/v1/project_quota.py | 38 ++++++++ .../key_manager/v1/test_project_quota.py | 87 +++++++++++++++++++ .../unit/key_manager/v1/test_project_quota.py | 44 ++++++++++ .../tests/unit/key_manager/v1/test_proxy.py | 23 +++++ ...nager-project-quotas-281845cccdc52ad2.yaml | 4 + 9 files changed, 259 insertions(+) create mode 100644 doc/source/user/resources/key_manager/v1/project_quota.rst create mode 100644 openstack/key_manager/v1/project_quota.py create mode 100644 openstack/tests/functional/key_manager/v1/test_project_quota.py create mode 100644 openstack/tests/unit/key_manager/v1/test_project_quota.py create mode 100644 releasenotes/notes/add-key-manager-project-quotas-281845cccdc52ad2.yaml diff --git a/doc/source/user/proxies/key_manager.rst b/doc/source/user/proxies/key_manager.rst index f5d7309ac..9210e7a3c 100644 --- a/doc/source/user/proxies/key_manager.rst +++ b/doc/source/user/proxies/key_manager.rst @@ -45,3 +45,10 @@ Secret Store Operations :noindex: :members: secret_stores, get_global_default_secret_store, get_preferred_secret_store + +ProjectQuota Operations +^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.key_manager.v1._proxy.Proxy + :noindex: + :members: update_project_quota, delete_project_quota, get_project_quota diff --git a/doc/source/user/resources/key_manager/index.rst b/doc/source/user/resources/key_manager/index.rst index 5f881b458..4f28d3a64 100644 --- a/doc/source/user/resources/key_manager/index.rst +++ b/doc/source/user/resources/key_manager/index.rst @@ -6,5 +6,6 @@ KeyManager Resources v1/container v1/order + v1/project_quota v1/secret v1/secret_store diff --git a/doc/source/user/resources/key_manager/v1/project_quota.rst b/doc/source/user/resources/key_manager/v1/project_quota.rst new file mode 100644 index 000000000..7f1f0ad9d --- /dev/null +++ b/doc/source/user/resources/key_manager/v1/project_quota.rst @@ -0,0 +1,12 @@ +openstack.key_manager.v1.project_quota +====================================== + +.. automodule:: openstack.key_manager.v1.project_quota + +The ProjectQuota Class +---------------------- + +The ``ProjectQuota`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.key_manager.v1.project_quota.ProjectQuota + :members: diff --git a/openstack/key_manager/v1/_proxy.py b/openstack/key_manager/v1/_proxy.py index c7e1addea..3abbc963e 100644 --- a/openstack/key_manager/v1/_proxy.py +++ b/openstack/key_manager/v1/_proxy.py @@ -14,6 +14,7 @@ from openstack.key_manager.v1 import container as _container from openstack.key_manager.v1 import order as _order +from openstack.key_manager.v1 import project_quota as _project_quota from openstack.key_manager.v1 import secret as _secret from openstack.key_manager.v1 import secret_store as _secret_store from openstack import proxy @@ -24,6 +25,7 @@ class Proxy(proxy.Proxy): _resource_registry = { "container": _container.Container, "order": _order.Order, + "project_quota": _project_quota.ProjectQuota, "secret": _secret.Secret, "secret_store": _secret_store.SecretStore, } @@ -313,6 +315,47 @@ def get_preferred_secret_store(self): base_path='/secret-stores/preferred', ) + def delete_project_quota(self, project_id, ignore_missing=True): + """Delete a project quota + + :param project_id: A project ID. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.NotFoundException` will be + raised when the project quota does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent project quota. + + :returns: ``None`` + """ + self._delete( + _project_quota.ProjectQuota, + project_id, + ignore_missing=ignore_missing, + ) + + def get_project_quota(self, project_id): + """Get a single project quota + + :param project_id: A project ID. + + :returns: One :class:`~openstack.key_manager.v1.project_quota.ProjectQuota` + :raises: :class:`~openstack.exceptions.NotFoundException` + when no resource can be found. + """ + return self._get(_project_quota.ProjectQuota, project_id) + + def update_project_quota(self, project_id, **attrs): + """Update a project quota + + :param project_id: A project ID. + :param attrs: The attributes to update on the project quota represented + by ``project quota``. + + :returns: The updated project quota + :rtype: :class:`~openstack.key_manager.v1.project_quota.ProjectQuota` + """ + return self._update(_project_quota.ProjectQuota, project_id, **attrs) + # ========== Utilities ========== def wait_for_status( diff --git a/openstack/key_manager/v1/project_quota.py b/openstack/key_manager/v1/project_quota.py new file mode 100644 index 000000000..34ebf1d83 --- /dev/null +++ b/openstack/key_manager/v1/project_quota.py @@ -0,0 +1,38 @@ +# 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 resource + + +class ProjectQuota(resource.Resource): + resource_key = 'project_quotas' + resources_key = 'project_quotas' + base_path = '/project-quotas' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + # Properties + #: Contains the configured quota value of the requested project for the secret resource. + secrets = resource.Body("secrets") + #: Contains the configured quota value of the requested project for the orders resource. + orders = resource.Body("orders") + #: Contains the configured quota value of the requested project for the containers resource. + containers = resource.Body("containers") + #: Contains the configured quota value of the requested project for the consumers resource. + consumers = resource.Body("consumers") + #: Contains the configured quota value of the requested project for the CAs resource. + cas = resource.Body("cas") diff --git a/openstack/tests/functional/key_manager/v1/test_project_quota.py b/openstack/tests/functional/key_manager/v1/test_project_quota.py new file mode 100644 index 000000000..0d33848b1 --- /dev/null +++ b/openstack/tests/functional/key_manager/v1/test_project_quota.py @@ -0,0 +1,87 @@ +# 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 as sdk_exc +from openstack.key_manager.v1 import project_quota as _project_quota +from openstack.tests.functional import base + +# NOTE(jbeen): Barbican policy may require 'key-manager:service-admin' for +# project quotas. Create and assign it per test project to avoid 403 errors. +ADMIN_ROLE_NAME = 'key-manager:service-admin' + + +class TestProjectQuota(base.BaseFunctionalTest): + def setUp(self): + super().setUp() + self.require_service('key-manager') + + self.project_name = self.getUniqueString('project') + self.project = self.system_admin_cloud.identity.create_project( + name=self.project_name, + ) + self.addCleanup( + self.system_admin_cloud.identity.delete_project, self.project + ) + + self.role = self.system_admin_cloud.identity.create_role( + name=ADMIN_ROLE_NAME + ) + self.addCleanup( + self.system_admin_cloud.identity.delete_role, self.role.id + ) + + self.user_id = self.system_admin_cloud.current_user_id + self.system_admin_cloud.identity.assign_project_role_to_user( + project=self.project, user=self.user_id, role=self.role + ) + self.addCleanup( + self.system_admin_cloud.identity.unassign_project_role_from_user, + project=self.project, + user=self.user_id, + role=self.role, + ) + + self._set_operator_cloud(project_id=self.project.id) + + def test_project_quotas(self): + # update project quota + project_quota = self.operator_cloud.key_manager.update_project_quota( + self.project.id, + secrets=1, + orders=2, + containers=3, + consumers=4, + cas=5, + ) + + self.assertIsInstance(project_quota, _project_quota.ProjectQuota) + self.assertIsNotNone(project_quota.id) + self.assertEqual(1, project_quota.secrets) + self.assertEqual(2, project_quota.orders) + self.assertEqual(3, project_quota.containers) + self.assertEqual(4, project_quota.consumers) + self.assertEqual(5, project_quota.cas) + + # get project quota + project_id = self.project.id + project_quota = self.operator_cloud.key_manager.get_project_quota( + project_id + ) + self.assertIsInstance(project_quota, _project_quota.ProjectQuota) + + # delete project quota + self.operator_cloud.key_manager.delete_project_quota(project_quota) + self.assertRaises( + sdk_exc.NotFoundException, + self.operator_cloud.key_manager.get_project_quota, + project_quota, + ) diff --git a/openstack/tests/unit/key_manager/v1/test_project_quota.py b/openstack/tests/unit/key_manager/v1/test_project_quota.py new file mode 100644 index 000000000..7bb4b8b53 --- /dev/null +++ b/openstack/tests/unit/key_manager/v1/test_project_quota.py @@ -0,0 +1,44 @@ +# 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.key_manager.v1 import project_quota +from openstack.tests.unit import base + + +EXAMPLE = { + 'secrets': 10, + 'orders': 20, + 'containers': -1, + 'consumers': 10, + 'cas': 5, +} + + +class TestProjectQuota(base.TestCase): + def test_basic(self): + sot = project_quota.ProjectQuota() + self.assertEqual('project_quotas', sot.resource_key) + self.assertEqual('project_quotas', sot.resources_key) + self.assertEqual('/project-quotas', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = project_quota.ProjectQuota(**EXAMPLE) + self.assertEqual(EXAMPLE['secrets'], sot.secrets) + self.assertEqual(EXAMPLE['orders'], sot.orders) + self.assertEqual(EXAMPLE['containers'], sot.containers) + self.assertEqual(EXAMPLE['consumers'], sot.consumers) + self.assertEqual(EXAMPLE['cas'], sot.cas) diff --git a/openstack/tests/unit/key_manager/v1/test_proxy.py b/openstack/tests/unit/key_manager/v1/test_proxy.py index 4ff6ceb44..146e8decc 100644 --- a/openstack/tests/unit/key_manager/v1/test_proxy.py +++ b/openstack/tests/unit/key_manager/v1/test_proxy.py @@ -13,6 +13,7 @@ from openstack.key_manager.v1 import _proxy from openstack.key_manager.v1 import container from openstack.key_manager.v1 import order +from openstack.key_manager.v1 import project_quota from openstack.key_manager.v1 import secret from openstack.key_manager.v1 import secret_store from openstack.tests.unit import test_proxy_base @@ -103,3 +104,25 @@ def test_secret_update(self): class TestKeyManagerSecretStore(TestKeyManagerProxy): def test_secret_stores(self): self.verify_list(self.proxy.secret_stores, secret_store.SecretStore) + + +class TestKeyManagerProjectQuota(TestKeyManagerProxy): + def test_project_quota_delete(self): + self.verify_delete( + self.proxy.delete_project_quota, project_quota.ProjectQuota, False + ) + + def test_project_quota_delete_ignore(self): + self.verify_delete( + self.proxy.delete_project_quota, project_quota.ProjectQuota, True + ) + + def test_project_quota_get(self): + self.verify_get( + self.proxy.get_project_quota, project_quota.ProjectQuota + ) + + def test_project_quota_update(self): + self.verify_update( + self.proxy.update_project_quota, project_quota.ProjectQuota + ) diff --git a/releasenotes/notes/add-key-manager-project-quotas-281845cccdc52ad2.yaml b/releasenotes/notes/add-key-manager-project-quotas-281845cccdc52ad2.yaml new file mode 100644 index 000000000..75ada9b4e --- /dev/null +++ b/releasenotes/notes/add-key-manager-project-quotas-281845cccdc52ad2.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add support for key manager project quota API From a0da527edce0dc05813db75008ac763bd4d3ac54 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 5 Sep 2025 12:15:59 +0100 Subject: [PATCH 3772/3836] identity: Add tokens This is mostly handled by keystoneauth, but there are a few non-auth APIs that we can currently only handle with keystoneclient. Close that gap. Change-Id: Iff8bbcf982b817098b42e69201f125202b41fb04 Signed-off-by: Stephen Finucane --- doc/source/user/proxies/identity_v3.rst | 7 + .../user/resources/identity/v3/token.rst | 12 ++ openstack/identity/v3/_proxy.py | 39 ++++ openstack/identity/v3/token.py | 115 ++++++++++ .../tests/unit/identity/v3/test_proxy.py | 32 +++ .../tests/unit/identity/v3/test_token.py | 198 ++++++++++++++++++ 6 files changed, 403 insertions(+) create mode 100644 doc/source/user/resources/identity/v3/token.rst create mode 100644 openstack/identity/v3/token.py create mode 100644 openstack/tests/unit/identity/v3/test_token.py diff --git a/doc/source/user/proxies/identity_v3.rst b/doc/source/user/proxies/identity_v3.rst index fe02e2fbc..e51f3c94e 100644 --- a/doc/source/user/proxies/identity_v3.rst +++ b/doc/source/user/proxies/identity_v3.rst @@ -86,6 +86,13 @@ User Operations :members: create_user, update_user, delete_user, get_user, find_user, users, user_groups +Token Operations +^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.identity.v3._proxy.Proxy + :noindex: + :members: validate_token, check_token, revoke_token + Trust Operations ^^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/identity/v3/token.rst b/doc/source/user/resources/identity/v3/token.rst new file mode 100644 index 000000000..106390c57 --- /dev/null +++ b/doc/source/user/resources/identity/v3/token.rst @@ -0,0 +1,12 @@ +openstack.identity.v3.token +=========================== + +.. automodule:: openstack.identity.v3.token + +The Token Class +--------------- + +The ``Token`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.identity.v3.token.Token + :members: diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index b0e78818b..15dbc84ef 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -53,6 +53,7 @@ from openstack.identity.v3 import service as _service from openstack.identity.v3 import service_provider as _service_provider from openstack.identity.v3 import system as _system +from openstack.identity.v3 import token as _token from openstack.identity.v3 import trust as _trust from openstack.identity.v3 import user as _user from openstack import proxy @@ -87,6 +88,7 @@ class Proxy(proxy.Proxy): "service": _service.Service, "system": _system.System, "trust": _trust.Trust, + "token": _token.Token, "user": _user.User, } @@ -976,6 +978,43 @@ def update_user(self, user, **attrs): """ return self._update(_user.User, user, **attrs) + # ========== Tokens ========== + + def validate_token( + self, token: str, nocatalog: bool = False, allow_expired: bool = False + ) -> _token.Token: + """Validate a token + + :param token: The token to validate. + :param nocatalog: Whether the returned token should not include a + catalog. + :param allow_expired: Whether to allow expired tokens. + + :returns: A :class:`~openstack.identity.v3.token.Token`. + """ + return _token.Token.validate( + self, token, nocatalog=nocatalog, allow_expired=allow_expired + ) + + def check_token(self, token: str, allow_expired: bool = False) -> bool: + """Check if a token is valid. + + :param token: The token to check. + :param allow_expired: Whether to allow expired tokens. + + :returns: True if valid, else False. + """ + return _token.Token.check(self, token, allow_expired=allow_expired) + + def revoke_token(self, token: str) -> None: + """Revoke a token. + + :param token: The token to revoke. + + :returns: None + """ + _token.Token.revoke(self, token) + # ========== Trusts ========== def create_trust(self, **attrs): diff --git a/openstack/identity/v3/token.py b/openstack/identity/v3/token.py new file mode 100644 index 000000000..54518858e --- /dev/null +++ b/openstack/identity/v3/token.py @@ -0,0 +1,115 @@ +# 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 keystoneauth1 import adapter + +from openstack import exceptions +from openstack import resource + + +class Token(resource.Resource): + resource_key = 'token' + base_path = '/auth/tokens' + + # capabilities + allow_fetch = False + allow_delete = False + allow_list = False + allow_head = False + + # Properties + #: An authentication token. This is used rather than X-Auth-Token to allow + #: users check or revoke a token other than their own. + subject_token = resource.Header('x-subject-token') + + #: A list of one or two audit IDs. An audit ID is a unique, randomly + #: generated, URL-safe string that you can use to track a token. The first + #: audit ID is the current audit ID for the token. The second audit ID is + #: present for only re-scoped tokens and is the audit ID from the token + #: before it was re-scoped. A re- scoped token is one that was exchanged + #: for another token of the same or different scope. You can use these + #: audit IDs to track the use of a token or chain of tokens across multiple + #: requests and endpoints without exposing the token ID to non-privileged + #: users. + audit_ids = resource.Body('audit_ids', type=list) + #: The service catalog. + catalog = resource.Body('catalog', type=list, list_type=dict) + #: The date and time when the token expires. + expires_at = resource.Body('expires_at') + #: The date and time when the token was issued. + issued_at = resource.Body('issued_at') + #: The authentication method. + methods = resource.Body('methods', type=list) + #: The user that owns the token. + user = resource.Body('user', type=dict) + #: The project that the token is scoped to, if any. + project = resource.Body('project', type=dict) + #: The domain that the token is scoped to, if any. + domain = resource.Body('domain', type=dict) + #: Whether the project, if set, is acting as a domain. + is_domain = resource.Body('is_domain', type=bool) + #: The parts of the system the token is scoped to, if system-scoped. + system = resource.Body('system', type=dict) + #: The roles associated with the user. + roles = resource.Body('roles', type=list, list_type=dict) + + @classmethod + def validate( + cls, + session: adapter.Adapter, + token: str, + *, + nocatalog: bool = False, + allow_expired: bool = False, + ) -> 'Token': + path = cls.base_path + + params: dict[str, bool] = {} + if nocatalog: + params['nocatalog'] = nocatalog + if allow_expired: + params['allow_expired'] = allow_expired + + response = session.get( + path, headers={'x-subject-token': token}, params=params + ) + exceptions.raise_from_response(response) + + ret = cls() + ret._translate_response( + response, resource_response_key=cls.resource_key + ) + return ret + + @classmethod + def check( + cls, + session: adapter.Adapter, + token: str, + *, + allow_expired: bool = False, + ) -> bool: + params: dict[str, bool] = {} + if allow_expired: + params['allow_expired'] = allow_expired + + response = session.head( + cls.base_path, headers={'x-subject-token': token}, params=params + ) + return response.status_code == 200 + + @classmethod + def revoke(cls, session: adapter.Adapter, token: str) -> None: + response = session.delete( + cls.base_path, headers={'x-subject-token': token} + ) + exceptions.raise_from_response(response) diff --git a/openstack/tests/unit/identity/v3/test_proxy.py b/openstack/tests/unit/identity/v3/test_proxy.py index 13b930b77..fd153d978 100644 --- a/openstack/tests/unit/identity/v3/test_proxy.py +++ b/openstack/tests/unit/identity/v3/test_proxy.py @@ -394,6 +394,38 @@ def test_user_groups(self): ) +class TestIdentityProxyToken(TestIdentityProxyBase): + def test_token_validate(self): + self._verify( + "openstack.identity.v3.token.Token.validate", + self.proxy.validate_token, + method_args=['token'], + method_kwargs={'nocatalog': False, 'allow_expired': False}, + expected_args=[self.proxy, 'token'], + expected_kwargs={'nocatalog': False, 'allow_expired': False}, + ) + + def test_token_check(self): + self._verify( + "openstack.identity.v3.token.Token.check", + self.proxy.check_token, + method_args=['token'], + method_kwargs={'allow_expired': False}, + expected_args=[self.proxy, 'token'], + expected_kwargs={'allow_expired': False}, + ) + + def test_token_revoke(self): + self._verify( + "openstack.identity.v3.token.Token.revoke", + self.proxy.revoke_token, + method_args=['token'], + method_kwargs={}, + expected_args=[self.proxy, 'token'], + expected_kwargs={}, + ) + + class TestIdentityProxyTrust(TestIdentityProxyBase): def test_trust_create_attrs(self): self.verify_create(self.proxy.create_trust, trust.Trust) diff --git a/openstack/tests/unit/identity/v3/test_token.py b/openstack/tests/unit/identity/v3/test_token.py new file mode 100644 index 000000000..bc78f3c43 --- /dev/null +++ b/openstack/tests/unit/identity/v3/test_token.py @@ -0,0 +1,198 @@ +# 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 unittest import mock + +from keystoneauth1 import adapter + +from openstack import exceptions +from openstack.identity.v3 import token +from openstack.tests.unit import base + +IDENTIFIER = 'IDENTIFIER' +TOKEN_DATA = { + 'audit_ids': ['VcxU2JEMTjufVx7sVk7bPw'], + 'catalog': [ + { + 'endpoints': [ + { + 'id': '068d1b359ee84b438266cb736d81de97', + 'interface': 'public', + 'region': 'RegionOne', + 'region_id': 'RegionOne', + 'url': 'http://example.com/v2.1', + } + ], + 'id': '050726f278654128aba89757ae25950c', + 'name': 'nova', + 'type': 'compute', + } + ], + 'domain': {'id': 'default', 'name': 'Default'}, + 'expires_at': '2013-02-27T18:30:59.999999Z', + 'issued_at': '2013-02-27T16:30:59.999999Z', + 'methods': ['password'], + 'project': { + 'domain': {'id': 'default', 'name': 'Default'}, + 'id': '8538a3f13f9541b28c2620eb19065e45', + 'name': 'admin', + }, + 'roles': [{'id': 'c703057be878458588961ce9a0ce686b', 'name': 'admin'}], + 'system': {'all': True}, + 'user': { + 'domain': {'id': 'default', 'name': 'Default'}, + 'id': '10a2e6e717a245d9acad3e5f97aeca3d', + 'name': 'admin', + 'password_expires_at': None, + }, + 'is_domain': False, +} + +EXAMPLE = {'token': TOKEN_DATA} + + +class TestToken(base.TestCase): + def setUp(self): + super().setUp() + self.session = mock.Mock(spec=adapter.Adapter) + + def test_basic(self): + sot = token.Token() + self.assertEqual('token', sot.resource_key) + self.assertEqual('/auth/tokens', sot.base_path) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_delete) + self.assertFalse(sot.allow_list) + self.assertFalse(sot.allow_head) + + def test_make_it(self): + sot = token.Token(**TOKEN_DATA) + self.assertEqual(TOKEN_DATA['audit_ids'], sot.audit_ids) + self.assertEqual(TOKEN_DATA['catalog'], sot.catalog) + self.assertEqual(TOKEN_DATA['expires_at'], sot.expires_at) + self.assertEqual(TOKEN_DATA['issued_at'], sot.issued_at) + self.assertEqual(TOKEN_DATA['methods'], sot.methods) + self.assertEqual(TOKEN_DATA['user'], sot.user) + self.assertEqual(TOKEN_DATA['project'], sot.project) + self.assertEqual(TOKEN_DATA['domain'], sot.domain) + self.assertEqual(TOKEN_DATA['is_domain'], sot.is_domain) + self.assertEqual(TOKEN_DATA['system'], sot.system) + self.assertEqual(TOKEN_DATA['roles'], sot.roles) + + def test_validate(self): + response = mock.Mock() + response.status_code = 200 + response.json.return_value = EXAMPLE + response.headers = {'content-type': 'application/json'} + self.session.get.return_value = response + + result = token.Token.validate(self.session, 'token') + + self.session.get.assert_called_once_with( + '/auth/tokens', headers={'x-subject-token': 'token'}, params={} + ) + self.assertIsInstance(result, token.Token) + + def test_validate_with_params(self): + response = mock.Mock() + response.status_code = 200 + response.json.return_value = EXAMPLE + response.headers = {'content-type': 'application/json'} + self.session.get.return_value = response + + result = token.Token.validate( + self.session, 'token', nocatalog=True, allow_expired=True + ) + + self.session.get.assert_called_once_with( + '/auth/tokens', + headers={'x-subject-token': 'token'}, + params={'nocatalog': True, 'allow_expired': True}, + ) + self.assertIsInstance(result, token.Token) + + def test_validate_error(self): + response = mock.Mock() + response.status_code = 404 + response.json.return_value = {} + response.headers = {'content-type': 'application/json'} + self.session.get.return_value = response + + self.assertRaises( + exceptions.NotFoundException, + token.Token.validate, + self.session, + 'token', + ) + + def test_check(self): + response = mock.Mock() + response.status_code = 200 + self.session.head.return_value = response + + result = token.Token.check(self.session, 'token') + + self.session.head.assert_called_once_with( + '/auth/tokens', headers={'x-subject-token': 'token'}, params={} + ) + self.assertTrue(result) + + def test_check_with_param(self): + response = mock.Mock() + response.status_code = 200 + self.session.head.return_value = response + + result = token.Token.check(self.session, 'token', allow_expired=True) + + self.session.head.assert_called_once_with( + '/auth/tokens', + headers={'x-subject-token': 'token'}, + params={'allow_expired': True}, + ) + self.assertTrue(result) + + def test_check_invalid_token(self): + response = mock.Mock() + response.status_code = 404 + self.session.head.return_value = response + + result = token.Token.check(self.session, 'token') + + self.session.head.assert_called_once_with( + '/auth/tokens', headers={'x-subject-token': 'token'}, params={} + ) + self.assertFalse(result) + + def test_revoke(self): + response = mock.Mock() + response.status_code = 204 + self.session.delete.return_value = response + + token.Token.revoke(self.session, 'token') + + self.session.delete.assert_called_once_with( + '/auth/tokens', headers={'x-subject-token': 'token'} + ) + + def test_revoke_error(self): + response = mock.Mock() + response.status_code = 404 + response.json.return_value = {} + response.headers = {'content-type': 'application/json'} + self.session.delete.return_value = response + + self.assertRaises( + exceptions.NotFoundException, + token.Token.revoke, + self.session, + 'token', + ) From e864c1fe759c465f8df3143b3c5abe66d8963260 Mon Sep 17 00:00:00 2001 From: platanus-kr Date: Fri, 29 Aug 2025 23:58:15 +0900 Subject: [PATCH 3773/3836] Add functional test for keystone user group This functional tests for Keystone group management and membership operations. - Create, update, get, find, and list groups - Assert default (empty) and updated descriptions - Add/remove a user to/from a group and check membership - List users in a group and validate returned user types Change-Id: I386f87a329c0ec9453c11e3b92787e3cff42d351 Signed-off-by: platanus-kr --- .../functional/identity/v3/test_group.py | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 openstack/tests/functional/identity/v3/test_group.py diff --git a/openstack/tests/functional/identity/v3/test_group.py b/openstack/tests/functional/identity/v3/test_group.py new file mode 100644 index 000000000..c9070066a --- /dev/null +++ b/openstack/tests/functional/identity/v3/test_group.py @@ -0,0 +1,99 @@ +# 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.identity.v3 import group as _group +from openstack.identity.v3 import user as _user +from openstack.tests.functional import base + + +class TestGroup(base.BaseFunctionalTest): + def setUp(self): + super().setUp() + + self.group_name = self.getUniqueString('group') + self.group_description = self.getUniqueString('group') + self.user_name = self.getUniqueString('user') + self.user_email = f"{self.user_name}@example.com" + + self.user = self.operator_cloud.identity.create_user( + name=self.user_name, + email=self.user_email, + ) + self.addCleanup(self._delete_user, self.user) + + def _delete_group(self, group): + ret = self.operator_cloud.identity.delete_group(group) + self.assertIsNone(ret) + + def _delete_user(self, user): + ret = self.operator_cloud.identity.delete_user(user) + self.assertIsNone(ret) + + def test_group(self): + # create the group + + group = self.operator_cloud.identity.create_group( + name=self.group_name, + ) + self.addCleanup(self._delete_group, group) + self.assertIsInstance(group, _group.Group) + self.assertEqual('', group.description) + + # update the group + + group = self.operator_cloud.identity.update_group( + group, description=self.group_description + ) + self.assertIsInstance(group, _group.Group) + self.assertEqual(self.group_description, group.description) + + # retrieve details of the (updated) group by ID + + group = self.operator_cloud.identity.get_group(group.id) + self.assertIsInstance(group, _group.Group) + self.assertEqual(self.group_description, group.description) + + # retrieve details of the (updated) group by name + + group = self.operator_cloud.identity.find_group(group.name) + self.assertIsInstance(group, _group.Group) + self.assertEqual(self.group_description, group.description) + + # list all groups + + groups = list(self.operator_cloud.identity.groups()) + self.assertIsInstance(groups[0], _group.Group) + self.assertIn( + self.group_name, + {x.name for x in groups}, + ) + + # add user to group + self.operator_cloud.identity.add_user_to_group(self.user, group) + + is_in_group = self.operator_cloud.identity.check_user_in_group( + self.user, group + ) + self.assertTrue(is_in_group) + + group_users = list(self.operator_cloud.identity.group_users(group)) + self.assertIsInstance(group_users[0], _user.User) + self.assertIn(self.user_name, {x.name for x in group_users}) + + # remove user from group + + self.operator_cloud.identity.remove_user_from_group(self.user, group) + + is_in_group = self.operator_cloud.identity.check_user_in_group( + self.user, group + ) + self.assertFalse(is_in_group) From d3cc7e0960ffed202856ac094bf92492e90331af Mon Sep 17 00:00:00 2001 From: Kim-Yukyung Date: Fri, 17 Oct 2025 01:17:17 +0900 Subject: [PATCH 3774/3836] Add find and delete examples to Key Manager - Add find.py with find_secret() function for searching secrets - Add delete.py with delete_secret() function for deleting secrets - Update key_manager.rst documentation with new sections Change-Id: Id6db6bc2871700f04a636a66fb71fc0e3a5e7283 Signed-off-by: Kim-Yukyung --- doc/source/user/guides/key_manager.rst | 24 ++++++++++++++++++++ examples/key_manager/delete.py | 29 ++++++++++++++++++++++++ examples/key_manager/find.py | 31 ++++++++++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 examples/key_manager/delete.py create mode 100644 examples/key_manager/find.py diff --git a/doc/source/user/guides/key_manager.rst b/doc/source/user/guides/key_manager.rst index 1efd12b7a..9b147f63f 100644 --- a/doc/source/user/guides/key_manager.rst +++ b/doc/source/user/guides/key_manager.rst @@ -54,3 +54,27 @@ when making this request. .. literalinclude:: ../examples/key_manager/get.py :pyobject: get_secret_payload + +Find Secret +----------- + +To find a secret by name or ID, use the +:meth:`~openstack.key_manager.v1._proxy.Proxy.find_secret` method. +This method can search for a :class:`~openstack.key_manager.v1.secret.Secret` +by either its name or ID, making it flexible when you don't have +the exact secret ID. + +.. literalinclude:: ../examples/key_manager/find.py + :pyobject: find_secret + +Delete Secret +------------- + +To delete a secret, use the +:meth:`~openstack.key_manager.v1._proxy.Proxy.delete_secret` method. +The secret can be identified by its ID or by using +:meth:`~openstack.key_manager.v1._proxy.Proxy.find_secret` to locate +it by name first. + +.. literalinclude:: ../examples/key_manager/delete.py + :pyobject: delete_secret diff --git a/examples/key_manager/delete.py b/examples/key_manager/delete.py new file mode 100644 index 000000000..fe01fa983 --- /dev/null +++ b/examples/key_manager/delete.py @@ -0,0 +1,29 @@ +# 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. + +""" +Delete resources from the Key Manager service. + +For a full guide see +https://docs.openstack.org/openstacksdk/latest/user/guides/key_manager.html +""" + + +def delete_secret(conn, name_or_id): + print(f"Delete Secret: {name_or_id}") + + secret = conn.key_manager.find_secret(name_or_id) + + if secret: + conn.key_manager.delete_secret(secret) + else: + print("Secret not found") diff --git a/examples/key_manager/find.py b/examples/key_manager/find.py new file mode 100644 index 000000000..c8682b1cc --- /dev/null +++ b/examples/key_manager/find.py @@ -0,0 +1,31 @@ +# 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. + +""" +Find resources from the Key Manager service. + +For a full guide see +https://docs.openstack.org/openstacksdk/latest/user/guides/key_manager.html +""" + + +def find_secret(conn, name_or_id): + print(f"Find Secret: {name_or_id}") + + secret = conn.key_manager.find_secret(name_or_id) + + if secret: + print(secret) + return secret + else: + print("Secret not found") + return None From fb794edea9c1d1e9e4da8397f0dc9adc73bd1de1 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 31 Oct 2025 12:05:03 +0000 Subject: [PATCH 3775/3836] reno: Update master for unmaintained/2024.1 Update the 2024.1 release notes configuration to build from unmaintained/2024.1. Change-Id: I3f60ea9e72c95749c8aef3398bfb397a1050db5d Signed-off-by: OpenStack Release Bot Generated-By: openstack/project-config:roles/copy-release-tools-scripts/files/release-tools/change_reno_branch_to_unmaintained.sh --- releasenotes/source/2024.1.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/source/2024.1.rst b/releasenotes/source/2024.1.rst index 4977a4f1a..6896656be 100644 --- a/releasenotes/source/2024.1.rst +++ b/releasenotes/source/2024.1.rst @@ -3,4 +3,4 @@ =========================== .. release-notes:: - :branch: stable/2024.1 + :branch: unmaintained/2024.1 From 1f931b61bba011649eefd687194ae3c3554d09e5 Mon Sep 17 00:00:00 2001 From: Jan Jasek Date: Wed, 12 Nov 2025 00:25:29 +0100 Subject: [PATCH 3776/3836] update requirements for os-service-types to new release 1.8.1 Update requirements for os-service-types to new release 1.8.1 and also related test. Change-Id: I3f5916abf94ddd25eaeb3eddfda63e08a574a6a5 Signed-off-by: Jan Jasek --- openstack/tests/unit/test_utils.py | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openstack/tests/unit/test_utils.py b/openstack/tests/unit/test_utils.py index ea726039b..78bbbd042 100644 --- a/openstack/tests/unit/test_utils.py +++ b/openstack/tests/unit/test_utils.py @@ -223,7 +223,7 @@ def test_value_less_than_min(self): class TestOsServiceTypesVersion(base.TestCase): def test_ost_version(self): - ost_version = '2022-09-13T15:34:32.154125' + ost_version = '2024-05-08T19:22:13.804707' self.assertEqual( ost_version, os_service_types.ServiceTypes().version, diff --git a/requirements.txt b/requirements.txt index 5a531f3f9..d63e00eb6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ iso8601>=0.1.11 # MIT jmespath>=0.9.0 # MIT jsonpatch!=1.20,>=1.16 # BSD keystoneauth1>=5.10.0 # Apache-2.0 -os-service-types>=1.8.0 # Apache-2.0 +os-service-types>=1.8.1 # Apache-2.0 pbr!=2.1.0,>=2.0.0 # Apache-2.0 platformdirs>=3 # MIT License psutil>=3.2.2 # BSD From 1bb4de93d4cf92fc8837fede0cb463491eef1493 Mon Sep 17 00:00:00 2001 From: Jan Jasek Date: Tue, 11 Nov 2025 20:06:52 +0100 Subject: [PATCH 3777/3836] Add _max_microversion to ShareGroup Shared_file_system SDK does not support ShareGroup by default. The first microversion that supports ShareGroup calls is 2.55. (Share Group APIs are not considered experimental since API version 2.55) Change-Id: Ie91f1f4c5a0450fd80c2e58cd3fed20f53645bc1 Signed-off-by: Jan Jasek --- openstack/shared_file_system/v2/share_group.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openstack/shared_file_system/v2/share_group.py b/openstack/shared_file_system/v2/share_group.py index 1dadbd9e4..4dd369e3b 100644 --- a/openstack/shared_file_system/v2/share_group.py +++ b/openstack/shared_file_system/v2/share_group.py @@ -20,6 +20,9 @@ class ShareGroup(resource.Resource): _query_mapping = resource.QueryParameters("share_group_id") + # The share group API is experimental until 2.55. + _max_microversion = "2.55" + # capabilities allow_create = True allow_fetch = True From ca878d73dfa57e1ca8792fac11517d9223829329 Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Wed, 12 Nov 2025 01:41:55 +0900 Subject: [PATCH 3778/3836] ruff: Use more specific name to enable pyupgrade rule UP is the exact name of the rule, instead of U. Use the exact name to avoid potential problems caused by any UX rules which can be added in the future. Change-Id: I671817c1200af2ca68bec0d715a49f0ef431714f Signed-off-by: Takashi Kajinami --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a313c98bd..ff3f3b410 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,7 +105,7 @@ quote-style = "preserve" docstring-code-format = true [tool.ruff.lint] -select = ["E4", "E7", "E9", "F", "S", "U"] +select = ["E4", "E7", "E9", "F", "S", "UP"] ignore = [ # we only use asserts for type narrowing "S101", From 880ad46dfd70f93105ae26f848cf7e44a79d1f3b Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Thu, 13 Nov 2025 07:52:43 -0700 Subject: [PATCH 3779/3836] ci: ironic: don't use tinyipa Ironic CI jobs, espescially when we wanted to reduce the memory footprint being used, leveraged TinyIPA. Unfortunately, TinyIPA was always intended with testing and ultimately became harder and harder to patch because it was a separately maintained and mirrored project which resulted in a high number of job failures as well. This became problematic enough for the Ironic project that it chose to intentionally deprecate and remove tinyipa. And surprise, the job is still here trying to use it. Which means this change also needs to be backported to 2025.2 at a minimum. Change-Id: I5d3699312e4f2243b8c43e0e9f8c5624b433ad89 Signed-off-by: Julia Kreger --- zuul.d/functional-jobs.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/zuul.d/functional-jobs.yaml b/zuul.d/functional-jobs.yaml index 067146257..70c613bf6 100644 --- a/zuul.d/functional-jobs.yaml +++ b/zuul.d/functional-jobs.yaml @@ -265,10 +265,9 @@ IRONIC_BUILD_DEPLOY_RAMDISK: false IRONIC_CALLBACK_TIMEOUT: 600 IRONIC_DEPLOY_DRIVER: ipmi - IRONIC_RAMDISK_TYPE: tinyipa IRONIC_VM_COUNT: 2 IRONIC_VM_LOG_DIR: '{{ devstack_base_dir }}/ironic-bm-logs' - IRONIC_VM_SPECS_RAM: 1024 + IRONIC_VM_SPECS_RAM: 2500 devstack_plugins: ironic: https://opendev.org/openstack/ironic devstack_services: From 6a707b7872945fd85558ed7315425033ab441bdf Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 17 Nov 2025 12:48:52 +0000 Subject: [PATCH 3780/3836] trivial: Replace smart quotes, NBSP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Just a series of sed invocations: ❯ sed -i "s/creator’s/creator's/" $(ag 'creator’s' -l) ❯ sed -i "s/users’s/user's/" $(ag 'users’s' -l) ❯ sed -i "s/container’s/container's/" $(ag 'container’s' -l) ❯ sed -i "s/share’s/share's/" $(ag 'share’s' -l) ❯ sed -i "s/‘yyyy-mm-dd’/'yyyy-mm-dd'/" $(ag '‘yyyy-mm-dd’' -l) ❯ sed -i "s/service’s/services's/" $(ag 'service’s' -l) ❯ sed -i 's/\xC2\xA0/ /g' $(ag $'\u00A0' -l) Change-Id: I88ca7017513bd4b4ea1637beb91b22a74397511d Signed-off-by: Stephen Finucane --- .../v1/cluster_template.py | 4 ++-- openstack/image/v2/image_tasks.py | 4 ++-- openstack/shared_file_system/v2/_proxy.py | 12 ++++++------ openstack/shared_file_system/v2/resource_locks.py | 4 ++-- openstack/shared_file_system/v2/share.py | 2 +- openstack/shared_file_system/v2/share_access_rule.py | 4 ++-- openstack/shared_file_system/v2/share_group.py | 2 +- openstack/shared_file_system/v2/share_instance.py | 2 +- openstack/shared_file_system/v2/share_network.py | 4 ++-- openstack/shared_file_system/v2/share_snapshot.py | 2 +- .../shared_file_system/v2/share_snapshot_instance.py | 4 ++-- 11 files changed, 22 insertions(+), 22 deletions(-) diff --git a/openstack/container_infrastructure_management/v1/cluster_template.py b/openstack/container_infrastructure_management/v1/cluster_template.py index 0f743ca03..38b74844d 100644 --- a/openstack/container_infrastructure_management/v1/cluster_template.py +++ b/openstack/container_infrastructure_management/v1/cluster_template.py @@ -39,7 +39,7 @@ class ClusterTemplate(resource.Resource): #: The date and time when the resource was created. created_at = resource.Body('created_at') #: The name of a driver to manage the storage for the images and the - #: container’s writable layer. + #: container's writable layer. docker_storage_driver = resource.Body('docker_storage_driver') #: The size in GB for the local storage on each server for the Docker #: daemon to cache the images and host the containers. @@ -71,7 +71,7 @@ class ClusterTemplate(resource.Resource): #: The name or UUID of the base image in Glance to boot the servers for the #: bay/cluster. image_id = resource.Body('image_id') - #: The URL pointing to users’s own private insecure docker + #: The URL pointing to user's own private insecure docker #: registry to deploy and run docker containers. insecure_registry = resource.Body('insecure_registry') #: Whether enable or not using the floating IP of cloud provider. diff --git a/openstack/image/v2/image_tasks.py b/openstack/image/v2/image_tasks.py index 0608caad8..d38cda9b0 100644 --- a/openstack/image/v2/image_tasks.py +++ b/openstack/image/v2/image_tasks.py @@ -26,12 +26,12 @@ class ImageTasks(resource.Resource): #: The type of task represented by this content type = resource.Body('type') - #: The current status of this task. The value can be pending, processing, + #: The current status of this task. The value can be pending, processing, #: success or failure status = resource.Body('status') #: An identifier for the owner of the task, usually the tenant ID owner = resource.Body('owner') - #: The date and time when the task is subject to removal (ISO8601 format) + #: The date and time when the task is subject to removal (ISO8601 format) expires_at = resource.Body('expires_at') #: The date and time when the task was created (ISO8601 format) created_at = resource.Body('created_at') diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index e49cf43f9..fcc778005 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -89,7 +89,7 @@ def shares(self, details=True, **query): * share_type_id: The UUID of a share type to query resources by. * name: The user defined name of the resource to filter resources by. - * snapshot_id: The UUID of the share’s base snapshot to filter + * snapshot_id: The UUID of the share's base snapshot to filter the request based on. * host: The host name of the resource to query with. * share_network_id: The UUID of the share network to filter @@ -273,7 +273,7 @@ def share_groups(self, **query): to filter resources. * project_id: The project ID of the user or service. * share_server_id: The UUID of the share server. - * snapshot_id: The UUID of the share’s base snapshot to filter + * snapshot_id: The UUID of the share's base snapshot to filter the request based on. * host: The host name for the back end. * share_network_id: The UUID of the share network to filter @@ -590,7 +590,7 @@ def share_snapshot_instances(self, details=True, **query): the share snapshot instance being returned. Available parameters include: - * snapshot_id: The UUID of the share’s base snapshot to filter + * snapshot_id: The UUID of the share's base snapshot to filter the request based on. * project_id: The project ID of the user or service making the request. @@ -1060,14 +1060,14 @@ def resource_locks(self, **query): locks. * resource_type: The type of the resource that the locks pertain to filter resource locks by. - * lock_context: The lock creator’s context to filter locks by. + * lock_context: The lock creator's context to filter locks by. * lock_reason: The lock reason that can be used to filter resource locks. (Inexact search is also available with lock_reason~) * created_since: Search for the list of resources that were created - after the specified date. The date is in ‘yyyy-mm-dd’ format. + after the specified date. The date is in 'yyyy-mm-dd' format. * created_before: Search for the list of resources that were created prior to the specified date. The date is in - ‘yyyy-mm-dd’ format. + 'yyyy-mm-dd' format. * limit: The maximum number of resource locks to return. * offset: The offset to define start point of resource lock listing. diff --git a/openstack/shared_file_system/v2/resource_locks.py b/openstack/shared_file_system/v2/resource_locks.py index 2d5731e13..b7939eb68 100644 --- a/openstack/shared_file_system/v2/resource_locks.py +++ b/openstack/shared_file_system/v2/resource_locks.py @@ -52,10 +52,10 @@ class ResourceLock(resource.Resource): #: Properties #: The date and time stamp when the resource was created within the - #: service’s database. + #: services's database. created_at = resource.Body("created_at", type=str) #: The date and time stamp when the resource was last modified within the - #: service’s database. + #: services's database. updated_at = resource.Body("updated_at", type=str) #: The ID of the user that owns the lock user_id = resource.Body("user_id", type=str) diff --git a/openstack/shared_file_system/v2/share.py b/openstack/shared_file_system/v2/share.py index 8ec1fbe85..5b4a3e239 100644 --- a/openstack/shared_file_system/v2/share.py +++ b/openstack/shared_file_system/v2/share.py @@ -36,7 +36,7 @@ class Share(resource.Resource, metadata.MetadataMixin): #: The availability zone. availability_zone = resource.Body("availability_zone", type=str) #: The date and time stamp when the resource was created within the - #: service’s database. + #: services's database. created_at = resource.Body("created_at", type=str) #: The user defined description of the resource. description = resource.Body("description", type=str) diff --git a/openstack/shared_file_system/v2/share_access_rule.py b/openstack/shared_file_system/v2/share_access_rule.py index 1a1f7932d..64fdbb957 100644 --- a/openstack/shared_file_system/v2/share_access_rule.py +++ b/openstack/shared_file_system/v2/share_access_rule.py @@ -46,7 +46,7 @@ class ShareAccessRule(resource.Resource): #: The access rule type. access_type = resource.Body("access_type", type=str) #: The date and time stamp when the resource was created within the - #: service’s database. + #: services's database. created_at = resource.Body("created_at", type=str) #: One or more access rule metadata key and value pairs as a dictionary #: of strings. @@ -56,7 +56,7 @@ class ShareAccessRule(resource.Resource): #: The state of the access rule. state = resource.Body("state", type=str) #: The date and time stamp when the resource was last updated within - #: the service’s database. + #: the services's database. updated_at = resource.Body("updated_at", type=str) #: Whether the visibility of some sensitive fields is restricted or not lock_visibility = resource.Body("lock_visibility", type=bool) diff --git a/openstack/shared_file_system/v2/share_group.py b/openstack/shared_file_system/v2/share_group.py index 4dd369e3b..78a5df6a1 100644 --- a/openstack/shared_file_system/v2/share_group.py +++ b/openstack/shared_file_system/v2/share_group.py @@ -39,7 +39,7 @@ class ShareGroup(resource.Resource): "consistent_snapshot_support", type=str ) #: The date and time stamp when the resource was created within the - #: service’s database. + #: services's database. created_at = resource.Body("created_at", type=str) #: The user defined description of the resource. description = resource.Body("description", type=str) diff --git a/openstack/shared_file_system/v2/share_instance.py b/openstack/shared_file_system/v2/share_instance.py index 93f3b7fe7..37e378cab 100644 --- a/openstack/shared_file_system/v2/share_instance.py +++ b/openstack/shared_file_system/v2/share_instance.py @@ -38,7 +38,7 @@ class ShareInstance(resource.Resource): #: set to True, all existing access rules be cast to read/only. cast_rules_to_readonly = resource.Body("cast_rules_to_readonly", type=bool) #: The date and time stamp when the resource was created within the - #: service’s database. + #: services's database. created_at = resource.Body("created_at", type=str) #: The host name of the service back end that the resource is #: contained within. diff --git a/openstack/shared_file_system/v2/share_network.py b/openstack/shared_file_system/v2/share_network.py index 6a4fdd3f3..91463b0f3 100644 --- a/openstack/shared_file_system/v2/share_network.py +++ b/openstack/shared_file_system/v2/share_network.py @@ -41,7 +41,7 @@ class ShareNetwork(resource.Resource): #: Properties #: The date and time stamp when the resource was created within the - #: service’s database. + #: services's database. created_at = resource.Body("created_at") #: The user defined description of the resource. description = resource.Body("description", type=str) @@ -61,5 +61,5 @@ class ShareNetwork(resource.Resource): #: a share network subnet with neutron. neutron_subnet_id = resource.Body("neutron_subnet_id", type=str) #: The date and time stamp when the resource was last updated within - #: the service’s database. + #: the services's database. updated_at = resource.Body("updated_at", type=str) diff --git a/openstack/shared_file_system/v2/share_snapshot.py b/openstack/shared_file_system/v2/share_snapshot.py index 999f7af6e..18d8fea3f 100644 --- a/openstack/shared_file_system/v2/share_snapshot.py +++ b/openstack/shared_file_system/v2/share_snapshot.py @@ -30,7 +30,7 @@ class ShareSnapshot(resource.Resource): #: Properties #: The date and time stamp when the resource was - #: created within the service’s database. + #: created within the services's database. created_at = resource.Body("created_at") #: The user defined description of the resource. description = resource.Body("description", type=str) diff --git a/openstack/shared_file_system/v2/share_snapshot_instance.py b/openstack/shared_file_system/v2/share_snapshot_instance.py index 6b0acb96b..13f53d6b7 100644 --- a/openstack/shared_file_system/v2/share_snapshot_instance.py +++ b/openstack/shared_file_system/v2/share_snapshot_instance.py @@ -28,7 +28,7 @@ class ShareSnapshotInstance(resource.Resource): #: Properties #: The date and time stamp when the resource was created within the - #: service’s database. + #: services's database. created_at = resource.Body("created_at", type=str) #: The progress of the snapshot creation. progress = resource.Body("progress", type=str) @@ -43,5 +43,5 @@ class ShareSnapshotInstance(resource.Resource): #: The snapshot instance status. status = resource.Body("status", type=str) #: The date and time stamp when the resource was updated within the - #: service’s database. + #: services's database. updated_at = resource.Body("updated_at", type=str) From 4a49b59d586e1342f9e88ec1dba5ec2ea99c068c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 17 Nov 2025 13:19:28 +0000 Subject: [PATCH 3781/3836] ruff: Enable E5 errors Change-Id: I10b51e91fc091c266b9e3f87ae7a3ec6e21e2eeb Signed-off-by: Stephen Finucane --- openstack/accelerator/v2/_proxy.py | 8 +-- openstack/baremetal/v1/node.py | 5 +- openstack/block_storage/v2/_proxy.py | 17 +++-- openstack/block_storage/v3/_proxy.py | 28 ++++---- openstack/cloud/_dns.py | 3 +- openstack/cloud/_network_common.py | 12 ++-- openstack/cloud/_utils.py | 4 +- openstack/cloud/meta.py | 8 +-- openstack/cloud/openstackcloud.py | 6 +- openstack/clustering/v1/_proxy.py | 35 +++++----- openstack/compute/v2/_proxy.py | 66 ++++++++++--------- openstack/config/cloud_region.py | 6 +- openstack/connection.py | 8 ++- openstack/dns/v2/_proxy.py | 52 +++++++-------- openstack/dns/v2/service_status.py | 4 +- openstack/dns/v2/tsigkey.py | 4 +- openstack/exceptions.py | 5 +- openstack/identity/v3/_proxy.py | 32 +++++---- openstack/identity/v3/registered_limit.py | 3 +- openstack/identity/v3/role_assignment.py | 8 +-- openstack/image/v2/_proxy.py | 26 ++++---- openstack/key_manager/v1/_proxy.py | 9 ++- openstack/key_manager/v1/project_quota.py | 15 +++-- openstack/load_balancer/v2/_proxy.py | 54 ++++++++------- openstack/network/v2/_proxy.py | 55 ++++++++-------- openstack/object_store/v1/_proxy.py | 15 +++-- openstack/orchestration/v1/_proxy.py | 6 +- openstack/placement/v1/_proxy.py | 22 +++---- openstack/resource.py | 5 +- openstack/shared_file_system/v2/_proxy.py | 3 +- .../shared_file_system/v2/quota_class_set.py | 15 +++-- openstack/test/fakes.py | 12 +++- .../functional/block_storage/v3/test_group.py | 4 +- .../tests/functional/dns/v2/test_zone.py | 3 +- .../load_balancer/v2/test_load_balancer.py | 2 +- .../v1/test_resource_provider_inventory.py | 2 +- .../test_share_group_snapshot.py | 2 +- .../tests/unit/baremetal/v1/test_node.py | 4 +- .../unit/block_storage/v3/test_attachment.py | 2 +- openstack/tests/unit/cloud/test_image.py | 8 +-- openstack/tests/unit/cloud/test_object.py | 6 +- .../tests/unit/object_store/v1/test_info.py | 4 +- .../tests/unit/orchestration/v1/test_proxy.py | 2 +- openstack/tests/unit/test_resource.py | 6 +- openstack/workflow/v2/_proxy.py | 6 +- pyproject.toml | 3 +- 46 files changed, 326 insertions(+), 279 deletions(-) diff --git a/openstack/accelerator/v2/_proxy.py b/openstack/accelerator/v2/_proxy.py index cc6a08ba3..01c6bb23d 100644 --- a/openstack/accelerator/v2/_proxy.py +++ b/openstack/accelerator/v2/_proxy.py @@ -170,10 +170,10 @@ def delete_accelerator_request( :class:`~openstack.accelerator.v2.device_profile.DeviceProfile` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the device profile does not exist. - When set to ``True``, no exception will be set when attempting to - delete a nonexistent accelerator request. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the device profile does not exist. When set to ``True``, no + exception will be set when attempting to delete a nonexistent + accelerator request. :returns: ``None`` """ return self._delete( diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 375c56c55..80768b408 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -714,8 +714,9 @@ def _check_state_reached( or self.provision_state == 'error' ): raise exceptions.ResourceFailure( - f"Node {self.id} reached failure state \"{self.provision_state}\"; " - f"the last error is {self.last_error}" + f"Node {self.id} reached failure state " + f"'{self.provision_state}'; the last error is " + f"{self.last_error}" ) # Special case: a failure state for "manage" transition can be # "enroll" diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index c7886be8f..adb149d04 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -771,10 +771,9 @@ def delete_backup(self, backup, ignore_missing=True, force=False): :param backup: The value can be the ID of a backup or a :class:`~openstack.block_storage.v2.backup.Backup` instance :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the zone does not exist. - When set to ``True``, no exception will be set when attempting to - delete a nonexistent zone. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the zone does not exist. When set to ``True``, no exception + will be set when attempting to delete a nonexistent zone. :param bool force: Whether to try forcing backup deletion :returns: ``None`` @@ -1025,13 +1024,13 @@ def find_service( :param name_or_id: The name or ID of a service :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the resource does not exist. - When set to ``True``, None will be returned when attempting to find - a nonexistent resource. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the resource does not exist. When set to ``True``, None will + be returned when attempting to find a nonexistent resource. :param dict query: Additional attributes like 'host' - :returns: One: class:`~openstack.block_storage.v2.service.Service` or None + :returns: One: class:`~openstack.block_storage.v2.service.Service` or + None :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index a197f9da6..2c76de7aa 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -1031,8 +1031,8 @@ def manage_volume(self, **attrs): allocating new storage. :param dict attrs: Keyword arguments which will be used to create - a :class:`~openstack.block_storage.v3.volume.Volume`, - comprised of the properties on the Volume class. + a :class:`~openstack.block_storage.v3.volume.Volume`, comprised of + the properties on the Volume class. :returns: The results of volume creation :rtype: :class:`~openstack.block_storage.v3.volume.Volume` """ @@ -1394,10 +1394,9 @@ def delete_backup(self, backup, ignore_missing=True, force=False): :param backup: The value can be the ID of a backup or a :class:`~openstack.block_storage.v3.backup.Backup` instance :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the zone does not exist. - When set to ``True``, no exception will be set when attempting to - delete a nonexistent zone. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the zone does not exist. When set to ``True``, no exception + will be set when attempting to delete a nonexistent zone. :param bool force: Whether to try forcing backup deletion :returns: ``None`` @@ -1925,10 +1924,9 @@ def delete_group_type(self, group_type, ignore_missing=True): or a :class:`~openstack.block_storage.v3.group_type.GroupType` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the zone does not exist. - When set to ``True``, no exception will be set when attempting to - delete a nonexistent zone. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the zone does not exist. When set to ``True``, no exception + will be set when attempting to delete a nonexistent zone. :returns: None """ @@ -2176,13 +2174,13 @@ def find_service( :param name_or_id: The name or ID of a service :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the resource does not exist. - When set to ``True``, None will be returned when attempting to find - a nonexistent resource. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the resource does not exist. When set to ``True``, None will + be returned when attempting to find a nonexistent resource. :param dict query: Additional attributes like 'host' - :returns: One: class:`~openstack.block_storage.v3.service.Service` or None + :returns: One: class:`~openstack.block_storage.v3.service.Service` or + None :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple diff --git a/openstack/cloud/_dns.py b/openstack/cloud/_dns.py index c1a58a5a5..41b915fbd 100644 --- a/openstack/cloud/_dns.py +++ b/openstack/cloud/_dns.py @@ -78,7 +78,8 @@ def create_zone( zone_type = zone_type.upper() if zone_type not in ('PRIMARY', 'SECONDARY'): raise exceptions.SDKException( - f"Invalid type {zone_type}, valid choices are PRIMARY or SECONDARY" + f"Invalid type {zone_type}, valid choices are PRIMARY or " + f"SECONDARY" ) zone = { diff --git a/openstack/cloud/_network_common.py b/openstack/cloud/_network_common.py index 3ff6f5be2..98a03de37 100644 --- a/openstack/cloud/_network_common.py +++ b/openstack/cloud/_network_common.py @@ -191,12 +191,12 @@ def _set_interesting_networks(self): if self._nat_destination in (network['name'], network['id']): if nat_destination: raise exceptions.SDKException( - 'Multiple networks were found matching ' - f'{self._nat_destination} which is the network configured ' - 'to be the NAT destination. Please check your ' - 'cloud resources. It is probably a good idea ' - 'to configure this network by ID rather than ' - 'by name.' + f'Multiple networks were found matching ' + f'{self._nat_destination} which is the network ' + f'configured to be the NAT destination. Please check ' + f'your cloud resources. It is probably a good idea ' + f'to configure this network by ID rather than ' + f'by name.' ) nat_destination = network elif self._nat_destination is None: diff --git a/openstack/cloud/_utils.py b/openstack/cloud/_utils.py index 8978b7746..38f2e075a 100644 --- a/openstack/cloud/_utils.py +++ b/openstack/cloud/_utils.py @@ -227,8 +227,8 @@ def func_wrapper(func, *args, **kwargs): for k in kwargs: if k not in argspec.args[1:] and k not in valid_args: raise TypeError( - f"{inspect.stack()[1][3]}() got an unexpected keyword argument " - f"'{k}'" + f"{inspect.stack()[1][3]}() got an unexpected keyword " + f"argument '{k}'" ) return func(*args, **kwargs) diff --git a/openstack/cloud/meta.py b/openstack/cloud/meta.py index 224f74597..2080e3572 100644 --- a/openstack/cloud/meta.py +++ b/openstack/cloud/meta.py @@ -276,10 +276,10 @@ def find_best_address(addresses, public=False, cloud_public=True): if do_check: log = _log.setup_logging('openstack') log.debug( - f"The cloud returned multiple addresses {addresses}:, and we could not " - "connect to port 22 on either. That might be what you wanted, " - "but we have no clue what's going on, so we picked the first one " - f"{addresses[0]}" + f"The cloud returned multiple addresses ({addresses}) and we " + f"could not connect to port 22 on either. That might be what you " + f"wanted, but we have no clue what's going on, so we picked the " + f"first one {addresses[0]}" ) return addresses[0] diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 2f45e94ce..f7b76b5ee 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -607,7 +607,8 @@ def get_session_endpoint( raise except Exception as e: raise exceptions.SDKException( - f"Error getting {service_key} endpoint on {self.name}:{self.config.get_region_name(service_key)}: " + f"Error getting {service_key} endpoint on " + f"{self.name}:{self.config.get_region_name(service_key)}: " f"{str(e)}" ) return endpoint @@ -682,7 +683,8 @@ def search_resources( resource_type = service_proxy._resource_registry[resource_name] except KeyError: raise exceptions.SDKException( - f"Resource {resource_name} is not known in service {service_name}" + f"Resource {resource_name} is not known in service " + f"{service_name}" ) if name_or_id: diff --git a/openstack/clustering/v1/_proxy.py b/openstack/clustering/v1/_proxy.py index fcd355b72..73c644211 100644 --- a/openstack/clustering/v1/_proxy.py +++ b/openstack/clustering/v1/_proxy.py @@ -114,9 +114,10 @@ def delete_profile(self, profile, ignore_missing=True): :param profile: The value can be either the name or ID of a profile or a :class:`~openstack.clustering.v1.profile.Profile` instance. :param bool ignore_missing: When set to ``False``, an exception - :class:`~openstack.exceptions.NotFoundException` will be raised when - the profile could not be found. When set to ``True``, no exception - will be raised when attempting to delete a non-existent profile. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the profile could not be found. When set to ``True``, no + exception will be raised when attempting to delete a non-existent + profile. :returns: ``None`` """ @@ -221,9 +222,10 @@ def delete_cluster(self, cluster, ignore_missing=True, force_delete=False): :param cluster: The value can be either the name or ID of a cluster or a :class:`~openstack.cluster.v1.cluster.Cluster` instance. :param bool ignore_missing: When set to ``False``, an exception - :class:`~openstack.exceptions.NotFoundException` will be raised when - the cluster could not be found. When set to ``True``, no exception - will be raised when attempting to delete a non-existent cluster. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the cluster could not be found. When set to ``True``, no + exception will be raised when attempting to delete a non-existent + cluster. :param bool force_delete: When set to ``True``, the cluster deletion will be forced immediately. @@ -562,9 +564,10 @@ def delete_node(self, node, ignore_missing=True, force_delete=False): :param node: The value can be either the name or ID of a node or a :class:`~openstack.cluster.v1.node.Node` instance. :param bool ignore_missing: When set to ``False``, an exception - :class:`~openstack.exceptions.NotFoundException` will be raised when - the node could not be found. When set to ``True``, no exception - will be raised when attempting to delete a non-existent node. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the node could not be found. When set to ``True``, no + exception will be raised when attempting to delete a non-existent + node. :param bool force_delete: When set to ``True``, the node deletion will be forced immediately. @@ -743,9 +746,10 @@ def delete_policy(self, policy, ignore_missing=True): :param policy: The value can be either the name or ID of a policy or a :class:`~openstack.clustering.v1.policy.Policy` instance. :param bool ignore_missing: When set to ``False``, an exception - :class:`~openstack.exceptions.NotFoundException` will be raised when - the policy could not be found. When set to ``True``, no exception - will be raised when attempting to delete a non-existent policy. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the policy could not be found. When set to ``True``, no + exception will be raised when attempting to delete a non-existent + policy. :returns: ``None`` """ @@ -895,9 +899,10 @@ def delete_receiver(self, receiver, ignore_missing=True): :param receiver: The value can be either the name or ID of a receiver or a :class:`~openstack.clustering.v1.receiver.Receiver` instance. :param bool ignore_missing: When set to ``False``, an exception - :class:`~openstack.exceptions.NotFoundException` will be raised when - the receiver could not be found. When set to ``True``, no exception - will be raised when attempting to delete a non-existent receiver. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the receiver could not be found. When set to ``True``, no + exception will be raised when attempting to delete a non-existent + receiver. :returns: ``None`` """ diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 6426b8ea8..4e395b13d 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -123,9 +123,9 @@ def find_flavor( :param name_or_id: The name or ID of a flavor. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the resource does not exist. When set to ``True``, None will be - returned when attempting to find a nonexistent resource. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the resource does not exist. When set to ``True``, None will + be returned when attempting to find a nonexistent resource. :param bool get_extra_specs: When set to ``True`` and extra_specs not present in the response will invoke additional API call to fetch extra_specs. @@ -351,9 +351,9 @@ def find_aggregate(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of an aggregate. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the resource does not exist. When set to ``True``, None will be - returned when attempting to find a nonexistent resource. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the resource does not exist. When set to ``True``, None will + be returned when attempting to find a nonexistent resource. :returns: One :class:`~openstack.compute.v2.aggregate.Aggregate` or None @@ -640,9 +640,10 @@ def delete_keypair(self, keypair, ignore_missing=True, user_id=None): :param keypair: The value can be either the ID of a keypair or a :class:`~openstack.compute.v2.keypair.Keypair` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the keypair does not exist. When set to ``True``, no exception - will be set when attempting to delete a nonexistent keypair. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the keypair does not exist. When set to ``True``, no + exception will be set when attempting to delete a nonexistent + keypair. :param str user_id: Optional user_id owning the keypair :returns: ``None`` @@ -684,9 +685,9 @@ def find_keypair(self, name_or_id, ignore_missing=True, *, user_id=None): :param name_or_id: The name or ID of a keypair. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the resource does not exist. When set to ``True``, None will be - returned when attempting to find a nonexistent resource. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the resource does not exist. When set to ``True``, None will + be returned when attempting to find a nonexistent resource. :param str user_id: Optional user_id owning the keypair :returns: One :class:`~openstack.compute.v2.keypair.Keypair` or None @@ -773,9 +774,9 @@ def find_server( :param name_or_id: The name or ID of a server. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the resource does not exist. When set to ``True``, None will be - returned when attempting to find a nonexistent resource. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the resource does not exist. When set to ``True``, None will + be returned when attempting to find a nonexistent resource. :param bool details: When set to ``False`` instances with only basic data will be returned. The default, ``True``, will cause instances with full data to be returned. @@ -1610,9 +1611,9 @@ def find_server_group( :param name_or_id: The name or ID of a server group. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the resource does not exist. When set to ``True``, None will be - returned when attempting to find a nonexistent resource. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the resource does not exist. When set to ``True``, None will + be returned when attempting to find a nonexistent resource. :param bool all_projects: When set to ``True``, search for server groups by name across all projects. Note that this will likely result in a higher chance of duplicates. Admin-only by default. @@ -1699,9 +1700,9 @@ def find_hypervisor( :param name_or_id: The name or ID of a hypervisor :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the resource does not exist. When set to ``True``, None will be - returned when attempting to find a nonexistent resource. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the resource does not exist. When set to ``True``, None will + be returned when attempting to find a nonexistent resource. :param bool details: When set to ``False`` instances with only basic data will be returned. The default, ``True``, will cause instances with full data to be returned. @@ -1870,9 +1871,10 @@ def delete_service(self, service, ignore_missing=True): The value can be either the ID of a service or a :class:`~openstack.compute.v2.service.Service` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the service does not exist. When set to ``True``, no exception - will be set when attempting to delete a nonexistent service. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the service does not exist. When set to ``True``, no + exception will be set when attempting to delete a nonexistent + service. :returns: ``None`` """ @@ -2050,8 +2052,8 @@ def delete_volume_attachment(self, server, volume, ignore_missing=True): :param volume: The value can be the ID of a volume or a :class:`~openstack.block_storage.v3.volume.Volume` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the volume attachment does not exist. When set to ``True``, no + :class:`~openstack.exceptions.NotFoundException` will be raised + when the volume attachment does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent volume attachment. @@ -2183,8 +2185,8 @@ def abort_server_migration( server or a :class:`~openstack.compute.v2.server.Server` instance that the migration belongs to. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the server migration does not exist. When set to ``True``, no + :class:`~openstack.exceptions.NotFoundException` will be raised + when the server migration does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent server migration. @@ -2247,8 +2249,8 @@ def get_server_migration( :class:`~openstack.compute.v2.server.Server` instance that the migration belongs to. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the server migration does not exist. When set to ``True``, no + :class:`~openstack.exceptions.NotFoundException` will be raised + when the server migration does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent server migration. @@ -2614,8 +2616,8 @@ def get_server_action(self, server_action, server, ignore_missing=True): :class:`~openstack.compute.v2.server.Server` instance that the action is associated with. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the server action does not exist. When set to ``True``, no + :class:`~openstack.exceptions.NotFoundException` will be raised + when the server action does not exist. When set to ``True``, no exception will be set when attempting to retrieve a non-existent server action. diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index ffb58e5be..a9d662cbf 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -214,8 +214,10 @@ def from_conf( _disable_service( config_dict, st, - reason=f"No section for project '{project_name}' (service type " - f"'{st}') was present in the config.", + reason=( + f"No section for project '{project_name}' (service " + f"type '{st}') was present in the config." + ), ) continue opt_dict: dict[str, str] = {} diff --git a/openstack/connection.py b/openstack/connection.py index b5b75c833..d264337d0 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -147,7 +147,9 @@ CONF() auth = ks_loading.load_auth_from_conf_options(CONF, 'neutron') - sess = ks_loading.load_session_from_conf_options(CONF, 'neutron', auth=auth) + sess = ks_loading.load_session_from_conf_options( + CONF, 'neutron', auth=auth + ) conn = connection.Connection( session=sess, @@ -202,7 +204,9 @@ service_auth = ks_loading.load_auth_from_conf_options(CONF, 'service_user') auth = service_token.ServiceTokenAuthWrapper(user_auth, service_auth) - sess = ks_loading.load_session_from_conf_options(CONF, 'neutron', auth=auth) + sess = ks_loading.load_session_from_conf_options( + CONF, 'neutron', auth=auth + ) conn = connection.Connection( session=sess, diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py index f12426b2f..6ebecb37a 100644 --- a/openstack/dns/v2/_proxy.py +++ b/openstack/dns/v2/_proxy.py @@ -97,10 +97,9 @@ def delete_zone(self, zone, ignore_missing=True, delete_shares=False): :param zone: The value can be the ID of a zone or a :class:`~openstack.dns.v2.zone.Zone` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the zone does not exist. - When set to ``True``, no exception will be set when attempting to - delete a nonexistent zone. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the zone does not exist. When set to ``True``, no exception + will be set when attempting to delete a nonexistent zone. :param bool delete_shares: When True, delete the zone shares along with the zone. @@ -257,9 +256,9 @@ def delete_recordset(self, recordset, zone=None, ignore_missing=True): :param zone: The value can be the ID of a zone or a :class:`~openstack.dns.v2.zone.Zone` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the zone does not exist. When set to ``True``, no exception will - be set when attempting to delete a nonexistent zone. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the zone does not exist. When set to ``True``, no exception + will be set when attempting to delete a nonexistent zone. :returns: Recordset instance been deleted :rtype: :class:`~openstack.dns.v2.recordset.Recordset` @@ -339,10 +338,9 @@ def delete_zone_import(self, zone_import, ignore_missing=True): :param zone_import: The value can be the ID of a zone import or a :class:`~openstack.dns.v2.zone_import.ZoneImport` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the zone does not exist. - When set to ``True``, no exception will be set when attempting to - delete a nonexistent zone. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the zone does not exist. When set to ``True``, no exception + will be set when attempting to delete a nonexistent zone. :returns: None """ @@ -416,10 +414,9 @@ def delete_zone_export(self, zone_export, ignore_missing=True): :param zone_export: The value can be the ID of a zone import or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the zone does not exist. - When set to ``True``, no exception will be set when attempting to - delete a nonexistent zone. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the zone does not exist. When set to ``True``, no exception + will be set when attempting to delete a nonexistent zone. :returns: None """ @@ -546,10 +543,9 @@ def delete_zone_transfer_request(self, request, ignore_missing=True): or a :class:`~openstack.dns.v2.zone_transfer.ZoneTransferRequest` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the zone does not exist. - When set to ``True``, no exception will be set when attempting to - delete a nonexistent zone. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the zone does not exist. When set to ``True``, no exception + will be set when attempting to delete a nonexistent zone. :returns: None """ @@ -677,10 +673,10 @@ def delete_zone_share(self, zone, zone_share, ignore_missing=True): share or a :class:`~openstack.dns.v2.zone_share.ZoneShare` instance that the zone share belongs to. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the zone share does not exist. - When set to ``True``, no exception will be set when attempting to - delete a nonexistent zone share. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the zone share does not exist. When set to ``True``, no + exception will be set when attempting to delete a nonexistent zone + share. :returns: ``None`` """ @@ -768,7 +764,8 @@ def get_service_status(self, service): """Get a status of a service in the Designate system :param service: The value can be the ID of a service - or a :class:`~openstack.dns.v2.service_status.ServiceStatus` instance. + or a :class:`~openstack.dns.v2.service_status.ServiceStatus` + instance. :returns: ServiceStatus instance. :rtype: :class:`~openstack.dns.v2.service_status.ServiceStatus` @@ -816,10 +813,9 @@ def delete_tld(self, tld, ignore_missing=True): :param tld: The value can be the ID of a tld or a :class:`~openstack.dns.v2.tld.TLD` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the tld does not exist. - When set to ``True``, no exception will be set when attempting to - delete a nonexistent tld. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the tld does not exist. When set to ``True``, no exception + will be set when attempting to delete a nonexistent tld. :returns: TLD been deleted :rtype: :class:`~openstack.dns.v2.tld.TLD` diff --git a/openstack/dns/v2/service_status.py b/openstack/dns/v2/service_status.py index 245817e16..86933f571 100644 --- a/openstack/dns/v2/service_status.py +++ b/openstack/dns/v2/service_status.py @@ -36,8 +36,8 @@ class ServiceStatus(_base.Resource): #: Hostname of the host with the service instance #: *Type: str* hostname = resource.Body('hostname') - #: Links contains a `self` pertaining to this service status or a `next` pertaining - #: to next page + #: Links contains a `self` pertaining to this service status or a `next` + #: pertaining to next page links = resource.Body('links', type=dict) #: The name of the Designate service instance #: *Type: str* diff --git a/openstack/dns/v2/tsigkey.py b/openstack/dns/v2/tsigkey.py index 089642f21..2f65d7cfd 100644 --- a/openstack/dns/v2/tsigkey.py +++ b/openstack/dns/v2/tsigkey.py @@ -57,6 +57,6 @@ class TSIGKey(_base.Resource): created_at = resource.Body('created_at') #: Timestamp when the tsigkey was last updated updated_at = resource.Body('updated_at') - #: Links contains a 'self' pertaining to this tsigkey or a 'next' pertaining - #: to next page + #: Links contains a 'self' pertaining to this tsigkey or a 'next' + #: pertaining to next page links = resource.Body('links', type=dict) diff --git a/openstack/exceptions.py b/openstack/exceptions.py index dcef7f561..1e4a7151c 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -168,7 +168,10 @@ def __init__( except AttributeError: name = resource.__class__.__name__ - message = f'The {method} method is not supported for {resource.__module__}.{name}' + message = ( + f'The {method} method is not supported for ' + f'{resource.__module__}.{name}' + ) super().__init__(message=message) diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 15dbc84ef..bda818d7f 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -1680,7 +1680,8 @@ def registered_limits(self, **query): the registered_limits being returned. :returns: A generator of registered_limits instances. - :rtype: :class:`~openstack.identity.v3.registered_limit.RegisteredLimit` + :rtype: + :class:`~openstack.identity.v3.registered_limit.RegisteredLimit` """ return self._list(_registered_limit.RegisteredLimit, **query) @@ -1692,7 +1693,8 @@ def get_registered_limit(self, registered_limit): :class:`~openstack.identity.v3.registered_limit.RegisteredLimit` instance. - :returns: One :class:`~openstack.identity.v3.registered_limit.RegisteredLimit` + :returns: One + :class:`~openstack.identity.v3.registered_limit.RegisteredLimit` :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ @@ -1706,7 +1708,8 @@ def create_registered_limit(self, **attrs): comprised of the properties on the RegisteredLimit class. :returns: The results of registered_limit creation. - :rtype: :class:`~openstack.identity.v3.registered_limit.RegisteredLimit` + :rtype: + :class:`~openstack.identity.v3.registered_limit.RegisteredLimit` """ return self._create(_registered_limit.RegisteredLimit, **attrs) @@ -1735,8 +1738,8 @@ def delete_registered_limit(self, registered_limit, ignore_missing=True): :class:`~openstack.identity.v3.registered_limit.RegisteredLimit` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the registered_limit does not exist. When set to ``True``, no + :class:`~openstack.exceptions.NotFoundException` will be raised + when the registered_limit does not exist. When set to ``True``, no exception will be thrown when attempting to delete a nonexistent registered_limit. @@ -1804,9 +1807,9 @@ def delete_limit(self, limit, ignore_missing=True): :param limit: The value can be either the ID of a limit or a :class:`~openstack.identity.v3.limit.Limit` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the limit does not exist. When set to ``True``, no exception will - be thrown when attempting to delete a nonexistent limit. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the limit does not exist. When set to ``True``, no exception + will be thrown when attempting to delete a nonexistent limit. :returns: ``None`` """ @@ -2316,9 +2319,10 @@ def delete_access_rule(self, user, access_rule, ignore_missing=True): :param access rule: The value can be either the ID of an access rule or a :class:`~.access_rule.AccessRule` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the access rule does not exist. When set to ``True``, no exception - will be thrown when attempting to delete a nonexistent access rule. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the access rule does not exist. When set to ``True``, no + exception will be thrown when attempting to delete a nonexistent + access rule. :returns: ``None`` """ @@ -2370,9 +2374,9 @@ def find_service_provider(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a service provider :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the resource does not exist. When set to ``True``, None will be - returned when attempting to find a nonexistent resource. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the resource does not exist. When set to ``True``, None will + be returned when attempting to find a nonexistent resource. :returns: The details of an service provider or None. :rtype: diff --git a/openstack/identity/v3/registered_limit.py b/openstack/identity/v3/registered_limit.py index 5a7de0cfe..db51b475d 100644 --- a/openstack/identity/v3/registered_limit.py +++ b/openstack/identity/v3/registered_limit.py @@ -115,7 +115,8 @@ def _translate_response( body = body[self.resource_key] # Keystone supports bunch create for registered limit. So the - # response body for creating registered_limit is a list instead of dict. + # response body for creating registered_limit is a list instead + # of dict. if isinstance(body, list): body = body[0] diff --git a/openstack/identity/v3/role_assignment.py b/openstack/identity/v3/role_assignment.py index 9ac6502f8..611cc7e8c 100644 --- a/openstack/identity/v3/role_assignment.py +++ b/openstack/identity/v3/role_assignment.py @@ -42,11 +42,11 @@ class RoleAssignment(resource.Resource): # Properties #: The links for the service resource. links = resource.Body('links') - #: The role (dictionary contains only id) *Type: dict* + #: The role (dictionary contains only id) role = resource.Body('role', type=dict) - #: The scope (either domain or project; dictionary contains only id) *Type: dict* + #: The scope (either domain or project; dictionary contains only id) scope = resource.Body('scope', type=dict) - #: The user (dictionary contains only id) *Type: dict* + #: The user (dictionary contains only id) user = resource.Body('user', type=dict) - #: The group (dictionary contains only id) *Type: dict* + #: The group (dictionary contains only id) group = resource.Body('group', type=dict) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 46bc17e80..d28f23581 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -91,8 +91,8 @@ def cache_delete_image(self, image, ignore_missing=True): :class:`~openstack.image.v2.image.Image` instance. :param bool ignore_missing: When set to ``False``, - :class:`~openstack.exceptions.NotFoundException` will be raised when - the metadef namespace does not exist. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the metadef namespace does not exist. :returns: ``None`` """ return self._delete(_cache.Cache, image, ignore_missing=ignore_missing) @@ -759,9 +759,9 @@ def _upload_image_task( ): if not self._connection.has_service('object-store'): raise exceptions.SDKException( - f"The cloud {self._connection.config.name} is configured to use tasks for image " - "upload, but no object-store service is available. " - "Aborting." + f"The cloud {self._connection.config.name} is configured to " + f"use tasks for image upload, but no object-store service is " + f"available. Aborting." ) properties = image_kwargs.get('properties', {}) @@ -1094,9 +1094,9 @@ def remove_member(self, member, image=None, ignore_missing=True): :class:`~openstack.image.v2.image.Image` instance that the member is part of. This is required if ``member`` is an ID. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the member does not exist. When set to ``True``, no exception will - be set when attempting to delete a nonexistent member. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the member does not exist. When set to ``True``, no exception + will be set when attempting to delete a nonexistent member. :returns: ``None`` """ @@ -1214,8 +1214,8 @@ def delete_metadef_namespace(self, metadef_namespace, ignore_missing=True): :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` instance. :param bool ignore_missing: When set to ``False``, - :class:`~openstack.exceptions.NotFoundException` will be raised when - the metadef namespace does not exist. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the metadef namespace does not exist. :returns: ``None`` """ self._delete( @@ -1460,8 +1460,8 @@ def delete_metadef_resource_type_association( :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` instance :param bool ignore_missing: When set to ``False``, - :class:`~openstack.exceptions.NotFoundException` will be raised when - the metadef resource type association does not exist. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the metadef resource type association does not exist. :returns: ``None`` """ namespace_name = resource.Resource._get_id(metadef_namespace) @@ -1619,7 +1619,7 @@ def get_metadef_property( ) def delete_all_metadef_properties(self, metadef_namespace): - """Delete all metadata definitions property inside a specific namespace. + """Delete all metadata definitions property inside a namespace. :param metadef_namespace: The value can be either the name of a metadef namespace or a diff --git a/openstack/key_manager/v1/_proxy.py b/openstack/key_manager/v1/_proxy.py index 3abbc963e..fe668633f 100644 --- a/openstack/key_manager/v1/_proxy.py +++ b/openstack/key_manager/v1/_proxy.py @@ -290,7 +290,8 @@ def secret_stores(self, **query): def get_global_default_secret_store(self): """Get the global default secret store - :returns: One :class:`~openstack.key_manager.v1.secret_store.SecretStore` + :returns: One + :class:`~openstack.key_manager.v1.secret_store.SecretStore` :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ @@ -304,7 +305,8 @@ def get_global_default_secret_store(self): def get_preferred_secret_store(self): """Get the preferred secret store for the current project - :returns: One :class:`~openstack.key_manager.v1.secret_store.SecretStore` + :returns: One + :class:`~openstack.key_manager.v1.secret_store.SecretStore` :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ @@ -338,7 +340,8 @@ def get_project_quota(self, project_id): :param project_id: A project ID. - :returns: One :class:`~openstack.key_manager.v1.project_quota.ProjectQuota` + :returns: One + :class:`~openstack.key_manager.v1.project_quota.ProjectQuota` :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ diff --git a/openstack/key_manager/v1/project_quota.py b/openstack/key_manager/v1/project_quota.py index 34ebf1d83..254b48bac 100644 --- a/openstack/key_manager/v1/project_quota.py +++ b/openstack/key_manager/v1/project_quota.py @@ -26,13 +26,18 @@ class ProjectQuota(resource.Resource): allow_list = True # Properties - #: Contains the configured quota value of the requested project for the secret resource. + #: Contains the configured quota value of the requested project for the + #: secret resource. secrets = resource.Body("secrets") - #: Contains the configured quota value of the requested project for the orders resource. + #: Contains the configured quota value of the requested project for the + #: orders resource. orders = resource.Body("orders") - #: Contains the configured quota value of the requested project for the containers resource. + #: Contains the configured quota value of the requested project for the + #: containers resource. containers = resource.Body("containers") - #: Contains the configured quota value of the requested project for the consumers resource. + #: Contains the configured quota value of the requested project for the + #: consumers resource. consumers = resource.Body("consumers") - #: Contains the configured quota value of the requested project for the CAs resource. + #: Contains the configured quota value of the requested project for the CAs + #: resource. cas = resource.Body("cas") diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index 0c5f74785..1e34765ce 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -104,10 +104,10 @@ def delete_load_balancer( :class:`~openstack.load_balancer.v2.load_balancer.LoadBalancer` instance :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the load balancer does not exist. - When set to ``True``, no exception will be set when attempting to - delete a nonexistent load balancer. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the load balancer does not exist. When set to ``True``, no + exception will be set when attempting to delete a nonexistent load + balancer. :param bool cascade: If true will delete all child objects of the load balancer. @@ -338,10 +338,9 @@ def delete_pool(self, pool, ignore_missing=True): :class:`~openstack.load_balancer.v2.pool.Pool` instance :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the pool does not exist. - When set to ``True``, no exception will be set when attempting to - delete a nonexistent pool. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the pool does not exist. When set to ``True``, no exception + will be set when attempting to delete a nonexistent pool. :returns: ``None`` """ @@ -568,10 +567,10 @@ def delete_health_monitor(self, healthmonitor, ignore_missing=True): :class:`~openstack.load_balancer.v2.healthmonitor.HealthMonitor` instance :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the healthmonitor does not exist. - When set to ``True``, no exception will be set when attempting to - delete a nonexistent healthmonitor. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the healthmonitor does not exist. When set to ``True``, no + exception will be set when attempting to delete a nonexistent + healthmonitor. :returns: ``None`` """ @@ -917,10 +916,10 @@ def delete_flavor_profile(self, flavor_profile, ignore_missing=True): :class:`~openstack.load_balancer.v2.flavor_profile.FlavorProfile` instance :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the flavor profile does not exist. - When set to ``True``, no exception will be set when attempting to - delete a nonexistent flavor profile. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the flavor profile does not exist. When set to ``True``, no + exception will be set when attempting to delete a nonexistent + flavor profile. :returns: ``None`` """ @@ -1001,10 +1000,9 @@ def delete_flavor(self, flavor, ignore_missing=True): :param flavor: The flavorcan be either the ID or a :class:`~openstack.load_balancer.v2.flavor.Flavor` instance :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the flavor does not exist. - When set to ``True``, no exception will be set when attempting to - delete a nonexistent flavor. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the flavor does not exist. When set to ``True``, no exception + will be set when attempting to delete a nonexistent flavor. :returns: ``None`` """ @@ -1143,10 +1141,10 @@ def delete_availability_zone_profile( :class:`~openstack.load_balancer.v2.availability_zone_profile.AvailabilityZoneProfile` instance :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the availability zone profile does not exist. - When set to ``True``, no exception will be set when attempting to - delete a nonexistent availability zone profile. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the availability zone profile does not exist. When set to + ``True``, no exception will be set when attempting to delete a + nonexistent availability zone profile. :returns: ``None`` """ @@ -1237,10 +1235,10 @@ def delete_availability_zone(self, availability_zone, ignore_missing=True): :class:`~openstack.load_balancer.v2.availability_zone.AvailabilityZone` instance :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the availability zone does not exist. - When set to ``True``, no exception will be set when attempting to - delete a nonexistent availability zone. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the availability zone does not exist. When set to ``True``, no + exception will be set when attempting to delete a nonexistent + availability zone. :returns: ``None`` """ diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index f0eab0065..9177ee798 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -3620,9 +3620,9 @@ def find_qos_minimum_packet_rate_rule( rule belongs or a :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the resource does not exist. When set to ``True``, None will be - returned when attempting to find a nonexistent resource. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the resource does not exist. When set to ``True``, None will + be returned when attempting to find a nonexistent resource. :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One @@ -3739,10 +3739,10 @@ def delete_qos_packet_rate_limit_rule( rule belongs or a :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the resource does not exist. When set to ``True``, no exception - will be set when attempting to delete a nonexistent minimum packet - rate rule. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the resource does not exist. When set to ``True``, no + exception will be set when attempting to delete a nonexistent + minimum packet rate rule. :returns: ``None`` """ @@ -3764,9 +3764,9 @@ def find_qos_packet_rate_limit_rule( rule belongs or a :class:`~openstack.network.v2.qos_policy.QoSPolicy` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the resource does not exist. When set to ``True``, None will be - returned when attempting to find a nonexistent resource. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the resource does not exist. When set to ``True``, None will + be returned when attempting to find a nonexistent resource. :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: One @@ -4472,9 +4472,9 @@ def find_ndp_proxy(self, ndp_proxy_id, ignore_missing=True, **query): :param ndp_proxy_id: The ID of a ndp proxy. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the resource does not exist. When set to ``True``, None will be - returned when attempting to find a nonexistent resource. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the resource does not exist. When set to ``True``, None will + be returned when attempting to find a nonexistent resource. :param dict query: Any additional parameters to be passed into underlying methods. such as query filters. :returns: @@ -4494,9 +4494,9 @@ def delete_ndp_proxy(self, ndp_proxy, ignore_missing=True): or a :class:`~openstack.network.v2.ndp_proxy.NDPProxy` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the router does not exist. When set to ``True``, no exception will - be set when attempting to delete a nonexistent ndp proxy. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the router does not exist. When set to ``True``, no exception + will be set when attempting to delete a nonexistent ndp proxy. :returns: ``None`` """ @@ -6074,10 +6074,10 @@ def delete_vpn_ipsec_site_connection( instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the IPsec site connection does not exist. - When set to ``True``, no exception will be set when attempting to - delete a nonexistent IPsec site connection. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the IPsec site connection does not exist. When set to + ``True``, no exception will be set when attempting to delete a + nonexistent IPsec site connection. :returns: ``None`` """ @@ -6779,7 +6779,8 @@ def get_sfc_flow_classifier(self, flow_classifier): :param flow_classifier: The value can be the ID of an SFC flow classifier or a - :class:`~openstack.network.v2.sfc_flow_classifier.SfcFlowClassifier` instance. + :class:`~openstack.network.v2.sfc_flow_classifier.SfcFlowClassifier` + instance. :returns: :class:`~openstack.network.v2.sfc_flow_classifier.SfcFlowClassifier` @@ -7446,8 +7447,9 @@ def _service_cleanup( identified_resources and port.device_id not in identified_resources ): - # It seems some no other service identified this resource - # to be deleted. We can assume it doesn't count + # It seems some no other service identified this + # resource to be deleted. We can assume it doesn't + # count network_has_ports_allocated = True if network_has_ports_allocated: # If some ports are on net - we cannot delete it @@ -7474,8 +7476,8 @@ def _service_cleanup( router = self.get_router(port.device_id) if not dry_run: - # Router interfaces cannot be deleted when the router has - # static routes, so remove those first + # Router interfaces cannot be deleted when the router + # has static routes, so remove those first if len(router.routes) > 0: try: self.remove_extra_routes_from_router( @@ -7484,7 +7486,8 @@ def _service_cleanup( ) except exceptions.SDKException: self.log.error( - f"Cannot delete routes {router.routes} from router {router}" + f"Cannot delete routes {router.routes} " + f"from router {router}" ) try: diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index 1958a16d4..fb93aa744 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -160,9 +160,10 @@ def delete_container(self, container, ignore_missing=True): :param container: The value can be either the name of a container or a :class:`~openstack.object_store.v1.container.Container` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the container does not exist. When set to ``True``, no exception - will be set when attempting to delete a nonexistent server. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the container does not exist. When set to ``True``, no + exception will be set when attempting to delete a nonexistent + server. :returns: ``None`` """ @@ -492,9 +493,9 @@ def delete_object(self, obj, ignore_missing=True, container=None): :param container: The value can be the ID of a container or a :class:`~openstack.object_store.v1.container.Container` instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the object does not exist. When set to ``True``, no exception will - be set when attempting to delete a nonexistent server. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the object does not exist. When set to ``True``, no exception + will be set when attempting to delete a nonexistent server. :returns: ``None`` """ @@ -934,7 +935,7 @@ def generate_form_signature( path = endpoint.path path = '/'.join([path, res.name, object_prefix]) - data = f'{path}\n{redirect_url}\n{max_file_size}\n{max_upload_count}\n{expires}' + data = f'{path}\n{redirect_url}\n{max_file_size}\n{max_upload_count}\n{expires}' # noqa: E501 sig = hmac.new(temp_url_key, data.encode(), sha1).hexdigest() return (expires, sig) diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index a20c2d5a8..cb6111fa9 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -554,13 +554,15 @@ def stack_events(self, stack, resource_name=None, **attr): obj = self._get(_stack.Stack, stack) if resource_name: - base_path = '/stacks/%(stack_name)s/%(stack_id)s/resources/%(resource_name)s/events' return self._list( _stack_event.StackEvent, stack_name=obj.name, stack_id=obj.id, resource_name=resource_name, - base_path=base_path, + base_path=( + '/stacks/%(stack_name)s/%(stack_id)s/resources/' + '%(resource_name)s/events' + ), **attr, ) diff --git a/openstack/placement/v1/_proxy.py b/openstack/placement/v1/_proxy.py index d0f058233..b3496c897 100644 --- a/openstack/placement/v1/_proxy.py +++ b/openstack/placement/v1/_proxy.py @@ -50,8 +50,8 @@ class or an :class:`~openstack.placement.v1.resource_class.ResourceClass`, instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the resource class does not exist. When set to ``True``, no + :class:`~openstack.exceptions.NotFoundException` will be raised + when the resource class does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent resource class. @@ -132,8 +132,8 @@ def delete_resource_provider(self, resource_provider, ignore_missing=True): :class:`~openstack.placement.v1.resource_provider.ResourceProvider`, instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the resource provider does not exist. When set to ``True``, no + :class:`~openstack.exceptions.NotFoundException` will be raised + when the resource provider does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent resource provider. @@ -187,9 +187,9 @@ def find_resource_provider(self, name_or_id, ignore_missing=True): :param name_or_id: The name or ID of a resource provider. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the resource does not exist. When set to ``True``, None will be - returned when attempting to find a nonexistent resource. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the resource does not exist. When set to ``True``, None will + be returned when attempting to find a nonexistent resource. :returns: An instance of :class:`~openstack.placement.v1.resource_provider.ResourceProvider` @@ -307,8 +307,8 @@ def delete_resource_provider_inventory( instance. This value must be specified when ``resource_provider_inventory`` is an ID. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the resource provider inventory does not exist. When set to + :class:`~openstack.exceptions.NotFoundException` will be raised + when the resource provider inventory does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent resource provider inventory. @@ -431,8 +431,8 @@ def delete_trait(self, trait, ignore_missing=True): :param trait: The value can be either the ID of a trait or an :class:`~openstack.placement.v1.trait.Trait`, instance. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the resource provider inventory does not exist. When set to + :class:`~openstack.exceptions.NotFoundException` will be raised + when the resource provider inventory does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent resource provider inventory. diff --git a/openstack/resource.py b/openstack/resource.py index 1d3bf2dea..dd5030473 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -2481,7 +2481,10 @@ def wait_for_delete( orig_resource = resource for count in utils.iterate_timeout( timeout=wait, - message=f"Timeout waiting for {resource.__class__.__name__}:{resource.id} to delete", + message=( + f"Timeout waiting for {resource.__class__.__name__}:{resource.id} " + f"to delete" + ), wait=interval, ): try: diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index fcc778005..2c74894f2 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -1019,7 +1019,8 @@ def delete_share_metadata(self, share_id, keys, ignore_missing=True): :param share_id: The ID of the share :param keys: The list of share metadata keys to be deleted - :param ignore_missing: Boolean indicating if missing keys should be ignored. + :param ignore_missing: Boolean indicating if missing keys should be + ignored. :returns: None :rtype: None diff --git a/openstack/shared_file_system/v2/quota_class_set.py b/openstack/shared_file_system/v2/quota_class_set.py index cf2090de5..325f00b91 100644 --- a/openstack/shared_file_system/v2/quota_class_set.py +++ b/openstack/shared_file_system/v2/quota_class_set.py @@ -36,22 +36,25 @@ class QuotaClassSet(resource.Resource): snapshots = resource.Body("snapshots", type=int) #: The maximum number of snapshot gigabytes that are allowed in a project. snapshot_gigabytes = resource.Body("snapshot_gigabytes", type=int) - #: The total maximum number of snapshot gigabytes that are allowed in a project. + #: The total maximum number of snapshot gigabytes that are allowed in a + #: project. shares = resource.Body("shares", type=int) #: The maximum number of share-networks that are allowed in a project. share_networks = resource.Body("share_networks", type=int) #: The maximum number of share replicas that is allowed. share_replicas = resource.Body("share_replicas", type=int) - #: The total maximum number of share gigabytes that are allowed in a project. - #: You cannot request a share that exceeds the allowed gigabytes quota. + #: The total maximum number of share gigabytes that are allowed in a + #: project. You cannot request a share that exceeds the allowed gigabytes + #: quota. gigabytes = resource.Body("gigabytes", type=int) #: The maximum number of replica gigabytes that are allowed in a project. - #: You cannot create a share, share replica, manage a share or extend a share - #: if it is going to exceed the allowed replica gigabytes quota. + #: You cannot create a share, share replica, manage a share or extend a + #: share if it is going to exceed the allowed replica gigabytes quota. replica_gigabytes = resource.Body("replica_gigabytes", type=int) #: The number of gigabytes per share allowed in a project. per_share_gigabytes = resource.Body("per_share_gigabytes", type=int) #: The total maximum number of share backups that are allowed in a project. backups = resource.Body("backups", type=int) - #: The total maximum number of backup gigabytes that are allowed in a project. + #: The total maximum number of backup gigabytes that are allowed in a + #: project. backup_gigabytes = resource.Body("backup_gigabytes", type=int) diff --git a/openstack/test/fakes.py b/openstack/test/fakes.py index 3ac22b863..e1b1e7afa 100644 --- a/openstack/test/fakes.py +++ b/openstack/test/fakes.py @@ -92,7 +92,10 @@ def generate_fake_resource( base_attrs[name] = [uuid.uuid4().hex] else: # Everything else - msg = f"Fake value for {resource_type.__name__}.{name} can not be generated" + msg = ( + f"Fake value for {resource_type.__name__}.{name} can " + f"not be generated" + ) raise NotImplementedError(msg) elif issubclass(target_type, list) and value.list_type is None: # List of str @@ -116,7 +119,10 @@ def generate_fake_resource( base_attrs[name] = dict() else: # Everything else - msg = f"Fake value for {resource_type.__name__}.{name} can not be generated" + msg = ( + f"Fake value for {resource_type.__name__}.{name} can not " + f"be generated" + ) raise NotImplementedError(msg) if isinstance(value, fields.URI): @@ -209,7 +215,7 @@ def generate_fake_proxy( ``api_version`` is not supported :returns: An autospecced mock of the :class:`~openstack.proxy.Proxy` implementation for the specified service type and API version - """ + """ # noqa: E501 if not issubclass(service, service_description.ServiceDescription): raise ValueError( f"Service {service.__name__} is not a valid ServiceDescription" diff --git a/openstack/tests/functional/block_storage/v3/test_group.py b/openstack/tests/functional/block_storage/v3/test_group.py index 86d402e13..009efda0d 100644 --- a/openstack/tests/functional/block_storage/v3/test_group.py +++ b/openstack/tests/functional/block_storage/v3/test_group.py @@ -107,14 +107,14 @@ def test_group_type_group_specs(self): ) # get - spec = self.operator_cloud.block_storage.get_group_type_group_specs_property( + spec = self.operator_cloud.block_storage.get_group_type_group_specs_property( # noqa: E501 self.group_type, 'foo', ) self.assertEqual('bar', spec) # update - spec = self.operator_cloud.block_storage.update_group_type_group_specs_property( + spec = self.operator_cloud.block_storage.update_group_type_group_specs_property( # noqa: E501 self.group_type, 'foo', 'baz', diff --git a/openstack/tests/functional/dns/v2/test_zone.py b/openstack/tests/functional/dns/v2/test_zone.py index eb9fcd133..513e924c4 100644 --- a/openstack/tests/functional/dns/v2/test_zone.py +++ b/openstack/tests/functional/dns/v2/test_zone.py @@ -51,7 +51,8 @@ def test_update_zone(self): self.assertEqual( current_ttl + 1, updated_zone_ttl, - f'Failed, updated TTL value is:{updated_zone_ttl} instead of expected:{current_ttl + 1}', + f'Failed, updated TTL value is:{updated_zone_ttl} instead of ' + f'expected:{current_ttl + 1}', ) def test_create_rs(self): diff --git a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py index 89e6bf18c..9837e6659 100644 --- a/openstack/tests/functional/load_balancer/v2/test_load_balancer.py +++ b/openstack/tests/functional/load_balancer/v2/test_load_balancer.py @@ -861,7 +861,7 @@ def test_availability_zone_profile_get(self): def test_availability_zone_profile_list(self): names = [ az.name - for az in self.operator_cloud.load_balancer.availability_zone_profiles() + for az in self.operator_cloud.load_balancer.availability_zone_profiles() # noqa: E501 ] self.assertIn(self.AVAILABILITY_ZONE_PROFILE_NAME, names) diff --git a/openstack/tests/functional/placement/v1/test_resource_provider_inventory.py b/openstack/tests/functional/placement/v1/test_resource_provider_inventory.py index 16d4bc5fa..0294781de 100644 --- a/openstack/tests/functional/placement/v1/test_resource_provider_inventory.py +++ b/openstack/tests/functional/placement/v1/test_resource_provider_inventory.py @@ -98,7 +98,7 @@ def test_resource_provider_inventory(self): # update the resource provider inventory - resource_provider_inventory = self.operator_cloud.placement.update_resource_provider_inventory( + resource_provider_inventory = self.operator_cloud.placement.update_resource_provider_inventory( # noqa: E501 resource_provider_inventory, total=20, resource_provider_generation=resource_provider_inventory.resource_provider_generation, diff --git a/openstack/tests/functional/shared_file_system/test_share_group_snapshot.py b/openstack/tests/functional/shared_file_system/test_share_group_snapshot.py index 0ba40a263..a7b407610 100644 --- a/openstack/tests/functional/shared_file_system/test_share_group_snapshot.py +++ b/openstack/tests/functional/shared_file_system/test_share_group_snapshot.py @@ -95,7 +95,7 @@ def test_update(self): self.assertEqual('updated share group snapshot', get_u_ss.description) def test_reset(self): - res = self.operator_cloud.shared_file_system.reset_share_group_snapshot_status( + res = self.operator_cloud.shared_file_system.reset_share_group_snapshot_status( # noqa: E501 self.SHARE_GROUP_SNAPSHOT_ID, 'error' ) self.assertIsNone(res) diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index f1300c275..bb2d95b7b 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -232,7 +232,7 @@ def _get_side_effect(_self, session): self.assertRaisesRegex( exceptions.ResourceFailure, - 'failure state "deploy failed"', + "failure state 'deploy failed'", self.node.wait_for_provision_state, self.session, 'manageable', @@ -247,7 +247,7 @@ def _get_side_effect(_self, session): self.assertRaisesRegex( exceptions.ResourceFailure, - 'failure state "error"', + "failure state 'error'", self.node.wait_for_provision_state, self.session, 'manageable', diff --git a/openstack/tests/unit/block_storage/v3/test_attachment.py b/openstack/tests/unit/block_storage/v3/test_attachment.py index e19778c96..0d8e7467b 100644 --- a/openstack/tests/unit/block_storage/v3/test_attachment.py +++ b/openstack/tests/unit/block_storage/v3/test_attachment.py @@ -47,7 +47,7 @@ "initiator": "iqn.2005-03.org.open-iscsi:1f6474a01f9a", "ip": "127.0.0.1", "multipath": False, - "nqn": "nqn.2014-08.org.nvmexpress:uuid:4dfe457e-6206-4a61-b547-5a9d0e2fa557", + "nqn": "nqn.2014-08.org.nvmexpress:uuid:4dfe457e-6206-4a61-b547-5a9d0e2fa557", # noqa: E501 "nvme_native_multipath": False, "os_type": "linux", "platform": "x86_64", diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index cf3ef15cc..57ff59a9a 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -883,7 +883,7 @@ def test_create_image_task(self): json=dict( type='import', input={ - 'import_from': f'{self.container_name}/{self.image_name}', + 'import_from': f'{self.container_name}/{self.image_name}', # noqa: E501 'image_properties': {'name': self.image_name}, }, ) @@ -929,7 +929,7 @@ def test_create_image_task(self): [ { 'op': 'add', - 'value': f'{self.container_name}/{self.image_name}', + 'value': f'{self.container_name}/{self.image_name}', # noqa: E501 'path': '/owner_specified.openstack.object', # noqa: E501 }, { @@ -1190,7 +1190,7 @@ def test_create_image_put_v1(self): 'properties': { 'owner_specified.openstack.md5': fakes.NO_MD5, 'owner_specified.openstack.sha256': fakes.NO_SHA256, - 'owner_specified.openstack.object': f'images/{self.image_name}', + 'owner_specified.openstack.object': f'images/{self.image_name}', # noqa: E501 'is_public': False, }, } @@ -1263,7 +1263,7 @@ def test_create_image_put_v1_bad_delete(self): 'properties': { 'owner_specified.openstack.md5': fakes.NO_MD5, 'owner_specified.openstack.sha256': fakes.NO_SHA256, - 'owner_specified.openstack.object': f'images/{self.image_name}', + 'owner_specified.openstack.object': f'images/{self.image_name}', # noqa: E501 'is_public': False, }, 'validate_checksum': True, diff --git a/openstack/tests/unit/cloud/test_object.py b/openstack/tests/unit/cloud/test_object.py index 88d58f8ce..0ea67d8b1 100644 --- a/openstack/tests/unit/cloud/test_object.py +++ b/openstack/tests/unit/cloud/test_object.py @@ -103,7 +103,7 @@ def test_create_container_public(self): 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', 'Content-Length': '0', 'Content-Type': 'text/html; charset=UTF-8', - 'x-container-read': _object_store.OBJECT_CONTAINER_ACLS[ + 'x-container-read': _object_store.OBJECT_CONTAINER_ACLS[ # noqa: E501 'public' ], }, @@ -243,7 +243,7 @@ def test_set_container_access_public(self): status_code=204, validate=dict( headers={ - 'x-container-read': _object_store.OBJECT_CONTAINER_ACLS[ + 'x-container-read': _object_store.OBJECT_CONTAINER_ACLS[ # noqa: E501 'public' ] } @@ -265,7 +265,7 @@ def test_set_container_access_private(self): status_code=204, validate=dict( headers={ - 'x-container-read': _object_store.OBJECT_CONTAINER_ACLS[ + 'x-container-read': _object_store.OBJECT_CONTAINER_ACLS[ # noqa: E501 'private' ] } diff --git a/openstack/tests/unit/object_store/v1/test_info.py b/openstack/tests/unit/object_store/v1/test_info.py index 9a6dbc583..9551bed30 100644 --- a/openstack/tests/unit/object_store/v1/test_info.py +++ b/openstack/tests/unit/object_store/v1/test_info.py @@ -36,8 +36,8 @@ def test_get_info_url(self): 'http://object.cloud.example.com/v1.0/test': 'http://object.cloud.example.com/info', 'https://object.cloud.example.com/swift/v1/AUTH_%(tenant_id)s': 'https://object.cloud.example.com/swift/info', 'https://object.cloud.example.com/swift/v1/AUTH_%(project_id)s': 'https://object.cloud.example.com/swift/info', - 'https://object.cloud.example.com/services/swift/v1/AUTH_%(project_id)s': 'https://object.cloud.example.com/services/swift/info', - 'https://object.cloud.example.com/services/swift/v1/AUTH_%(project_id)s/': 'https://object.cloud.example.com/services/swift/info', + 'https://object.cloud.example.com/services/swift/v1/AUTH_%(project_id)s': 'https://object.cloud.example.com/services/swift/info', # noqa: E501 + 'https://object.cloud.example.com/services/swift/v1/AUTH_%(project_id)s/': 'https://object.cloud.example.com/services/swift/info', # noqa: E501 'https://object.cloud.example.com/info/v1/AUTH_%(project_id)s/': 'https://object.cloud.example.com/info/info', } for uri_k, uri_v in test_urls.items(): diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index 846d44068..c9321d309 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -541,7 +541,7 @@ def test_stack_events_with_resource_name(self): stack_id = '1234' stack_name = 'test_stack' resource_name = 'id' - base_path = '/stacks/%(stack_name)s/%(stack_id)s/resources/%(resource_name)s/events' + base_path = '/stacks/%(stack_name)s/%(stack_id)s/resources/%(resource_name)s/events' # noqa: E501 stk = stack.Stack(id=stack_id, name=stack_name) self._verify( diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 0ca99ae25..3396a9b78 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -2342,7 +2342,8 @@ def make_mock_response(): make_mock_response(), ] - # Since the limit value is 3 but the max_items value is 2, two resources are returned. + # Since the limit value is 3 but the max_items value is 2, two + # resources are returned. results = self.sot.list( self.session, limit=3, paginated=True, max_items=2 ) @@ -2374,7 +2375,8 @@ def make_mock_response(): self.assertRaises(StopIteration, next, results) # both max_items and limit are set, and max_items is greater than limit - # (the opposite of this test: we should see multiple requests for limit resources each time) + # (the opposite of this test: we should see multiple requests for limit + # resources each time) results = self.sot.list( self.session, limit=1, paginated=True, max_items=3 ) diff --git a/openstack/workflow/v2/_proxy.py b/openstack/workflow/v2/_proxy.py index 3751eefc1..8fa7bb336 100644 --- a/openstack/workflow/v2/_proxy.py +++ b/openstack/workflow/v2/_proxy.py @@ -271,9 +271,9 @@ def find_cron_trigger( :param name_or_id: The name or ID of a cron trigger. :param bool ignore_missing: When set to ``False`` - :class:`~openstack.exceptions.NotFoundException` will be raised when - the resource does not exist. When set to ``True``, None will be - returned when attempting to find a nonexistent resource. + :class:`~openstack.exceptions.NotFoundException` will be raised + when the resource does not exist. When set to ``True``, None will + be returned when attempting to find a nonexistent resource. :param bool all_projects: When set to ``True``, search for cron triggers by name across all projects. Note that this will likely result in a higher chance of duplicates. diff --git a/pyproject.toml b/pyproject.toml index ff3f3b410..9ad2b1774 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,7 +105,7 @@ quote-style = "preserve" docstring-code-format = true [tool.ruff.lint] -select = ["E4", "E7", "E9", "F", "S", "UP"] +select = ["E4", "E5", "E7", "E9", "F", "S", "UP"] ignore = [ # we only use asserts for type narrowing "S101", @@ -113,4 +113,5 @@ ignore = [ [tool.ruff.lint.per-file-ignores] "openstack/tests/*" = ["S"] +"openstack/_services_mixin.py" = ["E501"] "examples/*" = ["S"] From 1f56f48243147c14a11c8568f5e26b35331f2f2d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 17 Nov 2025 11:22:17 +0000 Subject: [PATCH 3782/3836] ruff: Enable RUF rules Change-Id: I1b5a02ff65ea2ccf10d7ecd17673ad73ee5139b3 Signed-off-by: Stephen Finucane --- openstack/cloud/_compute.py | 2 +- openstack/cloud/_identity.py | 2 +- openstack/cloud/_object_store.py | 2 +- openstack/cloud/openstackcloud.py | 7 ++----- openstack/compute/v2/flavor.py | 2 +- openstack/config/__init__.py | 2 +- openstack/config/loader.py | 9 ++++----- openstack/connection.py | 2 +- openstack/exceptions.py | 4 ++-- openstack/image/v2/_proxy.py | 2 +- openstack/orchestration/v1/_proxy.py | 2 +- .../placement/v1/resource_provider_inventory.py | 2 +- openstack/proxy.py | 4 ++-- openstack/service_description.py | 6 +++--- openstack/test/fakes.py | 2 +- .../functional/baremetal/test_baremetal_driver.py | 2 +- .../block_storage/v3/test_capabilities.py | 4 ++-- openstack/tests/functional/cloud/test_endpoints.py | 8 ++++---- openstack/tests/functional/cloud/test_object.py | 4 ++-- .../tests/functional/identity/v3/test_user.py | 2 +- openstack/tests/functional/image/v2/test_image.py | 2 +- openstack/tests/unit/baremetal/v1/test_driver.py | 2 +- .../tests/unit/block_storage/v3/test_proxy.py | 10 +++++----- .../tests/unit/cloud/test_floating_ip_neutron.py | 10 +++++----- openstack/tests/unit/cloud/test_fwaas.py | 2 +- openstack/tests/unit/cloud/test_image.py | 4 ++-- openstack/tests/unit/cloud/test_operator.py | 4 ++-- openstack/tests/unit/cloud/test_operator_noauth.py | 6 +++--- openstack/tests/unit/cloud/test_role_assignment.py | 8 +++++--- openstack/tests/unit/cloud/test_stack.py | 12 ++++++------ openstack/tests/unit/compute/v2/test_proxy.py | 2 +- openstack/tests/unit/image/v2/test_proxy.py | 4 ++-- openstack/tests/unit/test_proxy_base.py | 14 +++++++------- pyproject.toml | 4 +++- tools/keystone_version.py | 2 +- tools/print-services.py | 2 +- 36 files changed, 79 insertions(+), 79 deletions(-) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 9b9c91550..02619599e 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -1306,7 +1306,7 @@ def get_active_server( ) if server['status'] == 'ACTIVE': - if 'addresses' in server and server['addresses']: + if server.get('addresses'): return self.add_ips_to_server( server, auto_ip, diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index 72ed3ceb5..b074af6ae 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -357,7 +357,7 @@ def get_user_by_id(self, user_id, normalize=None): ) def update_user(self, name_or_id, **kwargs): user_kwargs = {} - if 'domain_id' in kwargs and kwargs['domain_id']: + if kwargs.get('domain_id'): user_kwargs['domain_id'] = kwargs['domain_id'] user = self.get_user(name_or_id, **user_kwargs) diff --git a/openstack/cloud/_object_store.py b/openstack/cloud/_object_store.py index e245be3b6..350f25775 100644 --- a/openstack/cloud/_object_store.py +++ b/openstack/cloud/_object_store.py @@ -506,7 +506,7 @@ def _wait_for_futures(self, futures, raise_on_error=True): keystoneauth1.exceptions.RetriableConnectionFailure, exceptions.HttpException, ) as e: - error_text = f"Exception processing async task: {str(e)}" + error_text = f"Exception processing async task: {e!s}" if raise_on_error: self.log.exception(error_text) raise diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index f7b76b5ee..f1ebcb04d 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -609,7 +609,7 @@ def get_session_endpoint( raise exceptions.SDKException( f"Error getting {service_key} endpoint on " f"{self.name}:{self.config.get_region_name(service_key)}: " - f"{str(e)}" + f"{e!s}" ) return endpoint @@ -618,10 +618,7 @@ def has_service( ) -> bool: if not self.config.has_service(service_key): # TODO(mordred) add a stamp here so that we only report this once - if not ( - service_key in self._disable_warnings - and self._disable_warnings[service_key] - ): + if not (self._disable_warnings.get(service_key)): self.log.debug( "Disabling %(service_key)s entry in catalog per config", {'service_key': service_key}, diff --git a/openstack/compute/v2/flavor.py b/openstack/compute/v2/flavor.py index e17b57aa7..594d111ac 100644 --- a/openstack/compute/v2/flavor.py +++ b/openstack/compute/v2/flavor.py @@ -88,7 +88,7 @@ def __getattribute__(self, name): # To get this handled try sequentially to access it from various # places until we find first non-empty value. for xname in ["id", "name", "original_name"]: - if xname in self._body and self._body[xname]: + if self._body.get(xname): return self._body[xname] else: return super().__getattribute__(name) diff --git a/openstack/config/__init__.py b/openstack/config/__init__.py index f11ead173..30cf605f0 100644 --- a/openstack/config/__init__.py +++ b/openstack/config/__init__.py @@ -16,7 +16,7 @@ import sys import typing as ty -from openstack.config.loader import OpenStackConfig # noqa +from openstack.config.loader import OpenStackConfig if ty.TYPE_CHECKING: from openstack.config import cloud_region diff --git a/openstack/config/loader.py b/openstack/config/loader.py index ce57746be..5b61084d7 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -574,9 +574,9 @@ def _get_known_regions(self, cloud: str) -> list[dict[str, ty.Any]]: new_cloud: dict[str, ty.Any] = {} our_cloud = self.cloud_config['clouds'].get(cloud, {}) self._expand_vendor_profile(cloud, new_cloud, our_cloud) - if 'regions' in new_cloud and new_cloud['regions']: + if new_cloud.get('regions'): return self._expand_regions(new_cloud['regions']) - elif 'region_name' in new_cloud and new_cloud['region_name']: + elif new_cloud.get('region_name'): return [self._expand_region_name(new_cloud['region_name'])] return [] @@ -635,8 +635,7 @@ def _get_base_cloud_config( cloud['auth'] = {} _auth_update(cloud, our_cloud) - if 'cloud' in cloud: - del cloud['cloud'] + cloud.pop('cloud', None) return cloud @@ -1027,7 +1026,7 @@ def _fix_backwards_api_timeout( # The common argparse arg from keystoneauth is called timeout, but # os-client-config expects it to be called api_timeout if self._argv_timeout: - if 'timeout' in new_cloud and new_cloud['timeout']: + if new_cloud.get('timeout'): new_cloud['api_timeout'] = new_cloud.pop('timeout') return new_cloud diff --git a/openstack/connection.py b/openstack/connection.py index d264337d0..66cffe01e 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -312,8 +312,8 @@ from openstack import proxy __all__ = [ - 'from_config', 'Connection', + 'from_config', ] if requestsexceptions.SubjectAltNameWarning: diff --git a/openstack/exceptions.py b/openstack/exceptions.py index 1e4a7151c..2fb600ebf 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -111,8 +111,8 @@ def __init__( self.request_id = request_id self.status_code = http_status self.details = details - self.url = self.request and self.request.url or None - self.method = self.request and self.request.method or None + self.url = (self.request and self.request.url) or None + self.method = (self.request and self.request.method) or None self.source = "Server" if self.status_code is not None and (400 <= self.status_code < 500): self.source = "Client" diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index d28f23581..9a214b015 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -634,7 +634,7 @@ def _upload_image( raise except Exception as e: raise exceptions.SDKException( - f"Image creation failed: {str(e)}" + f"Image creation failed: {e!s}" ) from e def _make_v2_image_params(self, meta, properties): diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index cb6111fa9..cc2749731 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -531,7 +531,7 @@ def get_template_contents( ) except Exception as e: raise exceptions.SDKException( - f"Error in processing template files: {str(e)}" + f"Error in processing template files: {e!s}" ) # ========== Stack events ========== diff --git a/openstack/placement/v1/resource_provider_inventory.py b/openstack/placement/v1/resource_provider_inventory.py index 443483a9c..217333898 100644 --- a/openstack/placement/v1/resource_provider_inventory.py +++ b/openstack/placement/v1/resource_provider_inventory.py @@ -166,7 +166,7 @@ def _dict_filter(f, d): 'resource_class': resource_class, 'resource_provider_generation': data[ 'resource_provider_generation' - ], # noqa: E501 + ], **resource_data, **uri_params, } diff --git a/openstack/proxy.py b/openstack/proxy.py index 3c028aabd..b75708d98 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -175,7 +175,7 @@ def _get_cache_key_prefix(self, url: str) -> str: url, self.service_type, self.session.get_project_id() ) - return '.'.join([self.service_type] + name_parts) + return '.'.join([self.service_type, *name_parts]) def _invalidate_cache( self, @@ -428,7 +428,7 @@ def _report_stats_statsd( with self._statsd_client.pipeline() as pipe: if response is not None: duration = int(response.elapsed.total_seconds() * 1000) - metric_name = f'{key}.{str(response.status_code)}' + metric_name = f'{key}.{response.status_code!s}' pipe.timing(metric_name, duration) pipe.incr(metric_name) if duration > 1000: diff --git a/openstack/service_description.py b/openstack/service_description.py index 5d4dc3903..8f0ace1f2 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -82,7 +82,7 @@ def __init__( ) self.aliases = aliases or self.aliases - self.all_types = [service_type] + self.aliases + self.all_types = [service_type, *self.aliases] def __get__(self, instance, owner): if instance is None: @@ -175,7 +175,7 @@ def _make_proxy(self, instance): # If the user doesn't give a version in config, but we only support # one version, then just use that version. if not version_string and len(self.supported_versions) == 1: - version_string = list(self.supported_versions)[0] + version_string = next(iter(self.supported_versions)) proxy_obj = None if endpoint_override and version_string: @@ -280,7 +280,7 @@ def _make_proxy(self, instance): else: version_kwargs['min_version'] = str(supported_versions[0]) version_kwargs['max_version'] = ( - f'{str(supported_versions[-1])}.latest' + f'{supported_versions[-1]!s}.latest' ) temp_adapter = config.get_session_client( diff --git a/openstack/test/fakes.py b/openstack/test/fakes.py index e1b1e7afa..f77148bcb 100644 --- a/openstack/test/fakes.py +++ b/openstack/test/fakes.py @@ -230,7 +230,7 @@ def generate_fake_proxy( f"provides multiple API versions" ) else: - api_version = list(supported_versions)[0] + api_version = next(iter(supported_versions)) elif api_version not in supported_versions: raise ValueError( f"API version {api_version} is not supported by openstacksdk. " diff --git a/openstack/tests/functional/baremetal/test_baremetal_driver.py b/openstack/tests/functional/baremetal/test_baremetal_driver.py index 7d53292f5..e45b0f0ac 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_driver.py +++ b/openstack/tests/functional/baremetal/test_baremetal_driver.py @@ -50,7 +50,7 @@ def test_fake_hardware_get(self): def test_fake_hardware_list_details(self): drivers = self.system_admin_cloud.baremetal.drivers(details=True) - driver = [d for d in drivers if d.name == 'fake-hardware'][0] + driver = next(d for d in drivers if d.name == 'fake-hardware') for iface in ('boot', 'deploy', 'management', 'power'): self.assertIn( 'fake', getattr(driver, f'enabled_{iface}_interfaces') diff --git a/openstack/tests/functional/block_storage/v3/test_capabilities.py b/openstack/tests/functional/block_storage/v3/test_capabilities.py index 5ffbea8c9..f0fd8e721 100644 --- a/openstack/tests/functional/block_storage/v3/test_capabilities.py +++ b/openstack/tests/functional/block_storage/v3/test_capabilities.py @@ -19,11 +19,11 @@ class TestCapabilities(base.BaseBlockStorageTest): def test_get(self): services = list(self.operator_cloud.block_storage.services()) - host = [ + host = next( service for service in services if service.binary == 'cinder-volume' - ][0].host + ).host sot = self.operator_cloud.block_storage.get_capabilities(host) self.assertIn('description', sot) diff --git a/openstack/tests/functional/cloud/test_endpoints.py b/openstack/tests/functional/cloud/test_endpoints.py index ed88057d1..0c50e18a2 100644 --- a/openstack/tests/functional/cloud/test_endpoints.py +++ b/openstack/tests/functional/cloud/test_endpoints.py @@ -87,7 +87,7 @@ def _cleanup_services(self): def test_create_endpoint(self): service_name = self.new_item_name + '_create' - region = list(self.operator_cloud.identity.regions())[0].id + region = next(iter(self.operator_cloud.identity.regions())).id service = self.operator_cloud.create_service( name=service_name, @@ -119,7 +119,7 @@ def test_create_endpoint(self): def test_update_endpoint(self): # service operations require existing region. Do not test updating # region for now - region = list(self.operator_cloud.identity.regions())[0].id + region = next(iter(self.operator_cloud.identity.regions())).id service = self.operator_cloud.create_service( name='service1', type='test_type' @@ -153,7 +153,7 @@ def test_update_endpoint(self): def test_list_endpoints(self): service_name = self.new_item_name + '_list' - region = list(self.operator_cloud.identity.regions())[0].id + region = next(iter(self.operator_cloud.identity.regions())).id service = self.operator_cloud.create_service( name=service_name, @@ -193,7 +193,7 @@ def test_list_endpoints(self): def test_delete_endpoint(self): service_name = self.new_item_name + '_delete' - region = list(self.operator_cloud.identity.regions())[0].id + region = next(iter(self.operator_cloud.identity.regions())).id service = self.operator_cloud.create_service( name=service_name, diff --git a/openstack/tests/functional/cloud/test_object.py b/openstack/tests/functional/cloud/test_object.py index 87de55359..4b9d22c5f 100644 --- a/openstack/tests/functional/cloud/test_object.py +++ b/openstack/tests/functional/cloud/test_object.py @@ -48,7 +48,7 @@ def test_create_object(self): (64 * 1024, 5), # 64MB, 5 segments ) for size, nseg in sizes: - segment_size = int(round(size / nseg)) + segment_size = round(size / nseg) with tempfile.NamedTemporaryFile() as fake_file: fake_content = ''.join( random.SystemRandom().choice( @@ -125,7 +125,7 @@ def test_download_object_to_file(self): ) for size, nseg in sizes: fake_content = b'' - segment_size = int(round(size / nseg)) + segment_size = round(size / nseg) with tempfile.NamedTemporaryFile() as fake_file: fake_content = ''.join( random.SystemRandom().choice( diff --git a/openstack/tests/functional/identity/v3/test_user.py b/openstack/tests/functional/identity/v3/test_user.py index 61e8614ef..2a0121590 100644 --- a/openstack/tests/functional/identity/v3/test_user.py +++ b/openstack/tests/functional/identity/v3/test_user.py @@ -19,7 +19,7 @@ def setUp(self): super().setUp() self.username = self.getUniqueString('user') - self.password = "test_password_123" # noqa: S105 + self.password = "test_password_123" self.email = f"{self.username}@example.com" self.description = "Test user for functional testing" diff --git a/openstack/tests/functional/image/v2/test_image.py b/openstack/tests/functional/image/v2/test_image.py index a5b694c66..89238db7c 100644 --- a/openstack/tests/functional/image/v2/test_image.py +++ b/openstack/tests/functional/image/v2/test_image.py @@ -78,7 +78,7 @@ def test_tags(self): self.operator_cloud.image.add_tag(image, 't2') # filter image by tags - image = list(self.operator_cloud.image.images(tag=['t1', 't2']))[0] + image = next(iter(self.operator_cloud.image.images(tag=['t1', 't2']))) self.assertEqual(image.id, image.id) self.assertIn('t1', image.tags) self.assertIn('t2', image.tags) diff --git a/openstack/tests/unit/baremetal/v1/test_driver.py b/openstack/tests/unit/baremetal/v1/test_driver.py index c009fd621..5aec6bcc1 100644 --- a/openstack/tests/unit/baremetal/v1/test_driver.py +++ b/openstack/tests/unit/baremetal/v1/test_driver.py @@ -35,7 +35,7 @@ "name": "agent_ipmitool", "properties": [ { - "href": "http://127.0.0.1:6385/v1/drivers/agent_ipmitool/properties", # noqa: E501 + "href": "http://127.0.0.1:6385/v1/drivers/agent_ipmitool/properties", "rel": "self", }, { diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 048c18df9..669a93d82 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -355,7 +355,7 @@ def test_group_type_update(self): def test_group_type_fetch_group_specs(self): self._verify( - "openstack.block_storage.v3.group_type.GroupType.fetch_group_specs", # noqa: E501 + "openstack.block_storage.v3.group_type.GroupType.fetch_group_specs", self.proxy.fetch_group_type_group_specs, method_args=["value"], expected_args=[self.proxy], @@ -363,7 +363,7 @@ def test_group_type_fetch_group_specs(self): def test_group_type_create_group_specs(self): self._verify( - "openstack.block_storage.v3.group_type.GroupType.create_group_specs", # noqa: E501 + "openstack.block_storage.v3.group_type.GroupType.create_group_specs", self.proxy.create_group_type_group_specs, method_args=["value", {'a': 'b'}], expected_args=[self.proxy], @@ -372,7 +372,7 @@ def test_group_type_create_group_specs(self): def test_group_type_get_group_specs_prop(self): self._verify( - "openstack.block_storage.v3.group_type.GroupType.get_group_specs_property", # noqa: E501 + "openstack.block_storage.v3.group_type.GroupType.get_group_specs_property", self.proxy.get_group_type_group_specs_property, method_args=["value", "prop"], expected_args=[self.proxy, "prop"], @@ -380,7 +380,7 @@ def test_group_type_get_group_specs_prop(self): def test_group_type_update_group_specs_prop(self): self._verify( - "openstack.block_storage.v3.group_type.GroupType.update_group_specs_property", # noqa: E501 + "openstack.block_storage.v3.group_type.GroupType.update_group_specs_property", self.proxy.update_group_type_group_specs_property, method_args=["value", "prop", "val"], expected_args=[self.proxy, "prop", "val"], @@ -388,7 +388,7 @@ def test_group_type_update_group_specs_prop(self): def test_group_type_delete_group_specs_prop(self): self._verify( - "openstack.block_storage.v3.group_type.GroupType.delete_group_specs_property", # noqa: E501 + "openstack.block_storage.v3.group_type.GroupType.delete_group_specs_property", self.proxy.delete_group_type_group_specs_property, method_args=["value", "prop"], expected_args=[self.proxy, "prop"], diff --git a/openstack/tests/unit/cloud/test_floating_ip_neutron.py b/openstack/tests/unit/cloud/test_floating_ip_neutron.py index bc7e7d863..da90b6de8 100644 --- a/openstack/tests/unit/cloud/test_floating_ip_neutron.py +++ b/openstack/tests/unit/cloud/test_floating_ip_neutron.py @@ -510,7 +510,7 @@ def test_auto_ip_pool_no_reuse(self): ), dict( method='GET', - uri='https://network.example.com/v2.0/networks?name=ext-net', # noqa: E501 + uri='https://network.example.com/v2.0/networks?name=ext-net', json={ "networks": [ { @@ -568,7 +568,7 @@ def test_auto_ip_pool_no_reuse(self): "security_groups": [ "9fb5ba44-5c46-4357-8e60-8b55526cab54" ], - "device_id": server_id, # noqa: E501 + "device_id": server_id, } ] }, @@ -627,13 +627,13 @@ def test_auto_ip_pool_no_reuse(self): }, "key_name": None, "image": { - "id": "95e4c449-8abf-486e-97d9-dc3f82417d2d" # noqa: E501 + "id": "95e4c449-8abf-486e-97d9-dc3f82417d2d" }, "OS-EXT-STS:task_state": None, "OS-EXT-STS:vm_state": "active", "OS-SRV-USG:launched_at": "2017-02-06T20:59:48.000000", # noqa: E501 "flavor": { - "id": "2186bd79-a05e-4953-9dde-ddefb63c88d4" # noqa: E501 + "id": "2186bd79-a05e-4953-9dde-ddefb63c88d4" }, "id": server_id, "security_groups": [{"name": "default"}], @@ -642,7 +642,7 @@ def test_auto_ip_pool_no_reuse(self): "user_id": "c17534835f8f42bf98fc367e0bf35e09", "name": "testmt", "created": "2017-02-06T20:59:44Z", - "tenant_id": "65222a4d09ea4c68934fa1028c77f394", # noqa: E501 + "tenant_id": "65222a4d09ea4c68934fa1028c77f394", "OS-DCF:diskConfig": "MANUAL", "os-extended-volumes:volumes_attached": [], "accessIPv4": "", diff --git a/openstack/tests/unit/cloud/test_fwaas.py b/openstack/tests/unit/cloud/test_fwaas.py index 41dc0cab7..eb67f4953 100644 --- a/openstack/tests/unit/cloud/test_fwaas.py +++ b/openstack/tests/unit/cloud/test_fwaas.py @@ -26,7 +26,7 @@ def _make_mock_url(self, *args, **params): return self.get_mock_url( 'network', 'public', - append=['v2.0', 'fwaas'] + list(args), + append=['v2.0', 'fwaas', *list(args)], qs_elements=params_list or None, ) diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 57ff59a9a..21b419c48 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -93,7 +93,7 @@ def test_download_image_no_images_found(self): ), dict( method='GET', - uri=f'https://image.example.com/v2/images?name={self.image_name}', # noqa: E501 + uri=f'https://image.example.com/v2/images?name={self.image_name}', json=dict(images=[]), ), dict( @@ -121,7 +121,7 @@ def _register_image_mocks(self): ), dict( method='GET', - uri=f'https://image.example.com/v2/images?name={self.image_name}', # noqa: E501 + uri=f'https://image.example.com/v2/images?name={self.image_name}', json=self.fake_search_return, ), dict( diff --git a/openstack/tests/unit/cloud/test_operator.py b/openstack/tests/unit/cloud/test_operator.py index fa70a4dc0..d82843744 100644 --- a/openstack/tests/unit/cloud/test_operator.py +++ b/openstack/tests/unit/cloud/test_operator.py @@ -143,7 +143,7 @@ def test_list_hypervisors(self): [ dict( method='GET', - uri='https://compute.example.com/v2.1/os-hypervisors/detail', # noqa: E501 + uri='https://compute.example.com/v2.1/os-hypervisors/detail', json={ 'hypervisors': [ fakes.make_fake_hypervisor(uuid1, 'testserver1'), @@ -177,7 +177,7 @@ def test_list_old_hypervisors(self): [ dict( method='GET', - uri='https://compute.example.com/v2.1/os-hypervisors/detail', # noqa: E501 + uri='https://compute.example.com/v2.1/os-hypervisors/detail', json={ 'hypervisors': [ fakes.make_fake_hypervisor('1', 'testserver1'), diff --git a/openstack/tests/unit/cloud/test_operator_noauth.py b/openstack/tests/unit/cloud/test_operator_noauth.py index c3b83b652..3d1e65f3e 100644 --- a/openstack/tests/unit/cloud/test_operator_noauth.py +++ b/openstack/tests/unit/cloud/test_operator_noauth.py @@ -159,7 +159,7 @@ def setUp(self): "id": "v1", "links": [ { - "href": "https://baremetal.example.com/v1", # noqa: E501 + "href": "https://baremetal.example.com/v1", "rel": "self", } ], @@ -189,7 +189,7 @@ def setUp(self): ], "ports": [ { - "href": "https://baremetal.example.com/v1/ports/", # noqa: E501 + "href": "https://baremetal.example.com/v1/ports/", "rel": "self", }, { @@ -199,7 +199,7 @@ def setUp(self): ], "nodes": [ { - "href": "https://baremetal.example.com/v1/nodes/", # noqa: E501 + "href": "https://baremetal.example.com/v1/nodes/", "rel": "self", }, { diff --git a/openstack/tests/unit/cloud/test_role_assignment.py b/openstack/tests/unit/cloud/test_role_assignment.py index 8cc5df3b1..e1e34bc91 100644 --- a/openstack/tests/unit/cloud/test_role_assignment.py +++ b/openstack/tests/unit/cloud/test_role_assignment.py @@ -190,8 +190,10 @@ def __get( method='GET', uri=self.get_mock_url( resource=resource + 's', - qs_elements=['name=' + getattr(data, attr)] - + qs_elements, + qs_elements=[ + 'name=' + getattr(data, attr), + *qs_elements, + ], ), status_code=200, json={(resource + 's'): [data.json_response[resource]]}, @@ -236,7 +238,7 @@ def __user_mocks( method='GET', uri=self.get_mock_url( resource='users', - qs_elements=qs_elements + ['name=' + user_data.name], + qs_elements=[*qs_elements, 'name=' + user_data.name], ), status_code=200, json={ diff --git a/openstack/tests/unit/cloud/test_stack.py b/openstack/tests/unit/cloud/test_stack.py index 265b199d6..28bb63961 100644 --- a/openstack/tests/unit/cloud/test_stack.py +++ b/openstack/tests/unit/cloud/test_stack.py @@ -160,7 +160,7 @@ def test_delete_stack(self): uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}?{resolve}', status_code=302, headers=dict( - location=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}', # noqa: E501 + location=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}', ), ), dict( @@ -200,7 +200,7 @@ def test_delete_stack_exception(self): uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_id}?{resolve}', status_code=302, headers=dict( - location=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}', # noqa: E501 + location=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}', ), ), dict( @@ -238,7 +238,7 @@ def test_delete_stack_by_name_wait(self): uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}?{resolve}', status_code=302, headers=dict( - location=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}', # noqa: E501 + location=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}', ), ), dict( @@ -304,7 +304,7 @@ def test_delete_stack_by_id_wait(self): uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_id}?{resolve}', status_code=302, headers=dict( - location=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}', # noqa: E501 + location=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}', ), ), dict( @@ -368,7 +368,7 @@ def test_delete_stack_wait_failed(self): uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_id}?{resolve}', status_code=302, headers=dict( - location=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}', # noqa: E501 + location=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}', ), ), dict( @@ -409,7 +409,7 @@ def test_delete_stack_wait_failed(self): uri=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_id}?resolve_outputs=False', status_code=302, headers=dict( - location=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}', # noqa: E501 + location=f'{fakes.ORCHESTRATION_ENDPOINT}/stacks/{self.stack_name}/{self.stack_id}?{resolve}', ), ), dict( diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index a81216a7a..7323e5796 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -1569,7 +1569,7 @@ def test_abort_server_migration(self): def test_force_complete_server_migration(self): self._verify( - 'openstack.compute.v2.server_migration.ServerMigration.force_complete', # noqa: E501 + 'openstack.compute.v2.server_migration.ServerMigration.force_complete', self.proxy.force_complete_server_migration, method_args=['server_migration', 'server'], expected_args=[self.proxy], diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 6d8a6edc0..2da4d0972 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -375,7 +375,7 @@ def test_image_create_with_stores(self): meta={}, properties={ self.proxy._IMAGE_MD5_KEY: '', - self.proxy._IMAGE_SHA256_KEY: '', # noqa: E501 + self.proxy._IMAGE_SHA256_KEY: '', self.proxy._IMAGE_OBJECT_KEY: 'bare/fake', }, timeout=3600, @@ -417,7 +417,7 @@ def test_image_create_with_all_stores(self): meta={}, properties={ self.proxy._IMAGE_MD5_KEY: '', - self.proxy._IMAGE_SHA256_KEY: '', # noqa: E501 + self.proxy._IMAGE_SHA256_KEY: '', self.proxy._IMAGE_OBJECT_KEY: 'bare/fake', }, timeout=3600, diff --git a/openstack/tests/unit/test_proxy_base.py b/openstack/tests/unit/test_proxy_base.py index d1d749006..0b512ef0f 100644 --- a/openstack/tests/unit/test_proxy_base.py +++ b/openstack/tests/unit/test_proxy_base.py @@ -113,7 +113,7 @@ def verify_create( test_method, method_args=method_args, method_kwargs=method_kwargs, - expected_args=[resource_type] + expected_args, + expected_args=[resource_type, *expected_args], expected_kwargs=expected_kwargs, expected_result=expected_result, ) @@ -145,7 +145,7 @@ def verify_delete( test_method, method_args=method_args, method_kwargs=method_kwargs, - expected_args=[resource_type] + expected_args, + expected_args=[resource_type, *expected_args], expected_kwargs=expected_kwargs, ) @@ -176,7 +176,7 @@ def verify_get( test_method, method_args=method_args, method_kwargs=method_kwargs, - expected_args=[resource_type] + expected_args, + expected_args=[resource_type, *expected_args], expected_kwargs=expected_kwargs, ) @@ -216,7 +216,7 @@ def verify_head( test_method, method_args=method_args, method_kwargs=method_kwargs, - expected_args=[resource_type] + expected_args, + expected_args=[resource_type, *expected_args], expected_kwargs=expected_kwargs, ) @@ -244,7 +244,7 @@ def verify_find( test_method, method_args=method_args, method_kwargs=method_kwargs, - expected_args=[resource_type] + expected_args, + expected_args=[resource_type, *expected_args], expected_kwargs=expected_kwargs, ) @@ -279,7 +279,7 @@ def verify_list( test_method, method_args=method_args, method_kwargs=method_kwargs, - expected_args=[resource_type] + expected_args, + expected_args=[resource_type, *expected_args], expected_kwargs=expected_kwargs, ) @@ -311,7 +311,7 @@ def verify_update( test_method, method_args=method_args, method_kwargs=method_kwargs, - expected_args=[resource_type] + expected_args, + expected_args=[resource_type, *expected_args], expected_kwargs=expected_kwargs, ) diff --git a/pyproject.toml b/pyproject.toml index 9ad2b1774..ee94fbf7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,8 +105,10 @@ quote-style = "preserve" docstring-code-format = true [tool.ruff.lint] -select = ["E4", "E5", "E7", "E9", "F", "S", "UP"] +select = ["E4", "E5", "E7", "E9", "F", "RUF", "S", "UP"] ignore = [ + # there are a lot of these to fix + "RUF012", # we only use asserts for type narrowing "S101", ] diff --git a/tools/keystone_version.py b/tools/keystone_version.py index 5de9b63b3..fc08bbf78 100644 --- a/tools/keystone_version.py +++ b/tools/keystone_version.py @@ -57,7 +57,7 @@ def print_version(version): if verbose: pprint.pprint(r) except Exception as e: - print(f"Error with {cloud.name}: {str(e)}") + print(f"Error with {cloud.name}: {e!s}") continue if 'version' in r: print_version(r['version']) diff --git a/tools/print-services.py b/tools/print-services.py index c478fc997..079105909 100644 --- a/tools/print-services.py +++ b/tools/print-services.py @@ -115,7 +115,7 @@ def _find_service_description_class(service_type): # as an opt-in for people trying to figure out why something # didn't work. warnings.warn( - f"Could not import {service_type} service description: {str(e)}", + f"Could not import {service_type} service description: {e!s}", ImportWarning, ) return service_description.ServiceDescription From 1e58792db9c019a99fc0839c77b449ce2d3f66e4 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 17 Nov 2025 13:54:54 +0000 Subject: [PATCH 3783/3836] ruff: Enable W rules Change-Id: I1981fdf512a8f7c30bebc4aa0e9ccf2e53466707 Signed-off-by: Stephen Finucane --- openstack/tests/unit/dns/v2/test_proxy.py | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openstack/tests/unit/dns/v2/test_proxy.py b/openstack/tests/unit/dns/v2/test_proxy.py index 5874ce1d0..90cedb79d 100644 --- a/openstack/tests/unit/dns/v2/test_proxy.py +++ b/openstack/tests/unit/dns/v2/test_proxy.py @@ -373,9 +373,9 @@ def test_blacklist_create(self): self.verify_create( self.proxy.create_blacklist, blacklist.Blacklist, - method_kwargs={'pattern': '.*\.example\.com'}, + method_kwargs={'pattern': r'.*\.example\.com'}, expected_kwargs={ - 'pattern': '.*\.example\.com', + 'pattern': r'.*\.example\.com', 'prepend_key': False, }, ) diff --git a/pyproject.toml b/pyproject.toml index ee94fbf7c..eebf048a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,7 +105,7 @@ quote-style = "preserve" docstring-code-format = true [tool.ruff.lint] -select = ["E4", "E5", "E7", "E9", "F", "RUF", "S", "UP"] +select = ["E4", "E5", "E7", "E9", "F", "RUF", "S", "UP", "W"] ignore = [ # there are a lot of these to fix "RUF012", From 4accf3e5098bf858c37d28fff6ea99ceef6a1756 Mon Sep 17 00:00:00 2001 From: Mridula Joshi Date: Wed, 4 Oct 2023 05:09:49 +0000 Subject: [PATCH 3784/3836] Add SDK support for adding/removing metadef tags Co-Authored-By: Stephen Finucane Co-Authored-By: Cyril Roelandt Change-Id: I11594469ec1b6ad9fdf5deb94538695161a8df36 Signed-off-by: Mridula Joshi Signed-off-by: Stephen Finucane Signed-off-by: Cyril Roelandt --- openstack/image/v2/_proxy.py | 56 ++++++++++++++++--- openstack/image/v2/metadef_namespace.py | 53 +++++++++++++++++- .../image/v2/test_metadef_namespace.py | 45 +++++++++++++++ .../unit/image/v2/test_metadef_namespace.py | 53 ++++++++++++++++++ ...d-image-metadef-tags-c980ec5e6502d76c.yaml | 5 ++ 5 files changed, 204 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/add-image-metadef-tags-c980ec5e6502d76c.yaml diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 6977bc370..8fb322680 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -1039,9 +1039,8 @@ def add_tag(self, image, tag): """Add a tag to an image :param image: The value can be the ID of a image or a - :class:`~openstack.image.v2.image.Image` instance - that the member will be created for. - :param str tag: The tag to be added + :class:`~openstack.image.v2.image.Image` instance. + :param tag: The tag to be added. :returns: None """ @@ -1049,12 +1048,11 @@ def add_tag(self, image, tag): image.add_tag(self, tag) def remove_tag(self, image, tag): - """Remove a tag to an image + """Remove a tag from an image :param image: The value can be the ID of a image or a - :class:`~openstack.image.v2.image.Image` instance - that the member will be created for. - :param str tag: The tag to be removed + :class:`~openstack.image.v2.image.Image` instance. + :param tag: The tag to be removed. :returns: None """ @@ -1275,6 +1273,50 @@ def update_metadef_namespace(self, metadef_namespace, **attrs): **attrs, ) + def add_tag_to_metadef_namespace(self, namespace, tag): + """Add a tag to a metadef namespace + + :param metadef_namespace: Either the name of a metadef namespace or an + :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` + instance. + :param str tag: The tag to be added. + + :returns: None + """ + namespace = self._get_resource( + _metadef_namespace.MetadefNamespace, namespace + ) + namespace.add_tag(self, tag) + + def remove_tag_from_metadef_namespace(self, namespace, tag): + """Remove a tag from a metadef namespace + + :param metadef_namespace: Either the name of a metadef namespace or an + :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` + instance. + :param str tag: The tag to be removed. + + :returns: None + """ + namespace = self._get_resource( + _metadef_namespace.MetadefNamespace, namespace + ) + namespace.remove_tag(self, tag) + + def remove_tags_from_metadef_namespace(self, namespace): + """Remove all tags from a metadef namespace + + :param metadef_namespace: Either the name of a metadef namespace or an + :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` + instance. + + :returns: None + """ + namespace = self._get_resource( + _metadef_namespace.MetadefNamespace, namespace + ) + namespace.remove_all_tags(self) + # ====== METADEF OBJECT ====== def create_metadef_object(self, namespace, **attrs): """Create a new object from namespace diff --git a/openstack/image/v2/metadef_namespace.py b/openstack/image/v2/metadef_namespace.py index 88e028f8e..b7f3e50ac 100644 --- a/openstack/image/v2/metadef_namespace.py +++ b/openstack/image/v2/metadef_namespace.py @@ -10,12 +10,16 @@ # License for the specific language governing permissions and limitations # under the License. + +import typing_extensions as ty_ext + +from openstack.common import tag from openstack import exceptions from openstack import resource from openstack import utils -class MetadefNamespace(resource.Resource): +class MetadefNamespace(resource.Resource, tag.TagMixin): resources_key = 'namespaces' base_path = '/metadefs/namespaces' @@ -98,3 +102,50 @@ def delete_all_objects(self, session): """ url = utils.urljoin(self.base_path, self.id, 'objects') return self._delete_all(session, url) + + # NOTE(mrjoshi): This method is re-implemented as we require a ``POST`` + # call while the original method does a ``PUT`` call. + def add_tag(self, session: resource.AdapterT, tag: str) -> ty_ext.Self: + """Adds a single tag to the resource. + + :param session: The session to use for making this request. + :param tag: The tag as a string. + """ + url = utils.urljoin(self.base_path, self.id, 'tags', tag) + session = self._get_session(session) + response = session.post(url) + exceptions.raise_from_response(response) + # we do not want to update tags directly + tags = self.tags + tags.append(tag) + self._body.attributes.update({'tags': tags}) + return self + + # NOTE(mrjoshi): This method is re-implemented to add support for the + # 'append' option. This method uses a ``POST`` call rather than the + # standard ``PUT`` call. + def set_tags( + self, session: resource.AdapterT, tags: list[str], append: bool = False + ) -> ty_ext.Self: + """Sets/Replaces all tags on the resource. + + :param session: The session to use for making this request. + :param list tags: List with tags to be set on the resource + :param append: If set to true, adds new tags to existing tags, + else overwrites the existing tags with new ones. + """ + url = utils.urljoin(self.base_path, self.id, 'tags') + session = self._get_session(session) + + headers = {'X-OpenStack-Append': 'False'} + if append: + headers['X-Openstack-Append'] = 'True' + + response = session.post( + url, headers=headers, json={'tags': [{'name': x} for x in tags]} + ) + exceptions.raise_from_response(response) + + self._body.attributes.update({'tags': tags}) + + return self diff --git a/openstack/tests/functional/image/v2/test_metadef_namespace.py b/openstack/tests/functional/image/v2/test_metadef_namespace.py index 641926cc9..c340b33d5 100644 --- a/openstack/tests/functional/image/v2/test_metadef_namespace.py +++ b/openstack/tests/functional/image/v2/test_metadef_namespace.py @@ -90,3 +90,48 @@ def test_metadef_namespace(self): metadef_namespace_description, metadef_namespace.description, ) + + def test_tags(self): + # add tag + metadef_namespace = self.operator_cloud.image.get_metadef_namespace( + self.metadef_namespace.namespace + ) + metadef_namespace.add_tag(self.operator_cloud.image, 't1') + metadef_namespace.add_tag(self.operator_cloud.image, 't2') + + # list tags + metadef_namespace.fetch_tags(self.operator_cloud.image) + md_tags = [tag['name'] for tag in metadef_namespace.tags] + self.assertIn('t1', md_tags) + self.assertIn('t2', md_tags) + + # remove tag + metadef_namespace.remove_tag(self.operator_cloud.image, 't1') + metadef_namespace = self.operator_cloud.image.get_metadef_namespace( + self.metadef_namespace.namespace + ) + md_tags = [tag['name'] for tag in metadef_namespace.tags] + self.assertIn('t2', md_tags) + self.assertNotIn('t1', md_tags) + + # add tags without append + metadef_namespace.set_tags(self.operator_cloud.image, ["t1", "t2"]) + metadef_namespace.fetch_tags(self.operator_cloud.image) + md_tags = [tag['name'] for tag in metadef_namespace.tags] + self.assertIn('t1', md_tags) + self.assertIn('t2', md_tags) + + # add tags with append + metadef_namespace.set_tags( + self.operator_cloud.image, ["t3", "t4"], append=True + ) + metadef_namespace.fetch_tags(self.operator_cloud.image) + md_tags = [tag['name'] for tag in metadef_namespace.tags] + self.assertIn('t1', md_tags) + self.assertIn('t2', md_tags) + self.assertIn('t3', md_tags) + self.assertIn('t4', md_tags) + + # remove all tags + metadef_namespace.remove_all_tags(self.operator_cloud.image) + self.assertEqual([], metadef_namespace.tags) diff --git a/openstack/tests/unit/image/v2/test_metadef_namespace.py b/openstack/tests/unit/image/v2/test_metadef_namespace.py index cf614f2a8..b8a829055 100644 --- a/openstack/tests/unit/image/v2/test_metadef_namespace.py +++ b/openstack/tests/unit/image/v2/test_metadef_namespace.py @@ -18,6 +18,7 @@ from openstack import exceptions from openstack.image.v2 import metadef_namespace from openstack.tests.unit import base +from openstack.tests.unit.test_resource import FakeResponse EXAMPLE = { @@ -97,3 +98,55 @@ def test_delete_all_objects(self): session.delete.assert_called_with( 'metadefs/namespaces/OS::Cinder::Volumetype/objects' ) + + +class TestMetadefNamespaceTags(base.TestCase): + # The tests in this class are very similar to those provided by + # TestTagMixin. The main differences are: + # - test_add_tag uses a ``PUT`` call instead of a ``POST`` call + # - test_set_tag uses a ``PUT`` call instead of a ``POST`` call + # - test_set_tag uses an optional ``X-OpenStack-Append`` header + def setUp(self): + super().setUp() + self.base_path = 'metadefs/namespaces' + self.response = FakeResponse({}) + + self.session = mock.Mock(spec=adapter.Adapter) + self.session.post = mock.Mock(return_value=self.response) + + def test_add_tag(self): + res = metadef_namespace.MetadefNamespace(**EXAMPLE) + sess = self.session + + # Set some initial value to check add + res.tags = ['blue', 'green'] + + result = res.add_tag(sess, 'lila') + # Check tags attribute is updated + self.assertEqual(['blue', 'green', 'lila'], res.tags) + # Check the passed resource is returned + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/tags/lila' + sess.post.assert_called_once_with(url) + + def test_set_tags(self): + res = metadef_namespace.MetadefNamespace(**EXAMPLE) + sess = self.session + + # Set some initial value to check rewrite + res.tags = ['blue_old', 'green_old'] + + result = res.set_tags(sess, ['blue', 'green']) + # Check tags attribute is updated + self.assertEqual(['blue', 'green'], res.tags) + # Check the passed resource is returned + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/tags' + headers = {'X-OpenStack-Append': 'False'} + jsonargs = { + 'tags': [ + {'name': 'blue'}, + {'name': 'green'}, + ] + } + sess.post.assert_called_once_with(url, headers=headers, json=jsonargs) diff --git a/releasenotes/notes/add-image-metadef-tags-c980ec5e6502d76c.yaml b/releasenotes/notes/add-image-metadef-tags-c980ec5e6502d76c.yaml new file mode 100644 index 000000000..11ac84232 --- /dev/null +++ b/releasenotes/notes/add-image-metadef-tags-c980ec5e6502d76c.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add support for Image Metadef Tags to create, remove + create-multiple, update tags. From bffe676997463b61954c67602e7a3a49cb775900 Mon Sep 17 00:00:00 2001 From: Afonne-CID Date: Tue, 25 Nov 2025 18:27:54 +0100 Subject: [PATCH 3785/3836] Fix mypy type check errors Resolve mypy type checking errors around possble ``None`` values and type inferrence. Change-Id: I8bf6c84dc0f7691aa5fb97bafa5a4366fcb0be9c Signed-off-by: Afonne-CID --- openstack/config/cloud_region.py | 15 +++++++++++++-- tools/print-services.py | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index a9d662cbf..729a134a5 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -206,6 +206,13 @@ def from_conf( ) continue project_name = stm.get_project_name(st) + if project_name is None: + _disable_service( + config_dict, + st, + reason=f"No project name found for service type '{st}'.", + ) + continue if project_name not in conf: if '-' in project_name: project_name = project_name.replace('-', '_') @@ -632,9 +639,11 @@ def get_service_type(self, service_type: str) -> str: # will still look for config by alias, but starting with the official # type will get us things in the right order. if self._service_type_manager.is_known(service_type): - service_type = self._service_type_manager.get_service_type( + official_type = self._service_type_manager.get_service_type( service_type ) + if official_type is not None: + service_type = official_type value = self._get_config( 'service_type', service_type, default=service_type ) @@ -670,9 +679,11 @@ def get_endpoint(self, service_type: str) -> str | None: # We have to override the Rackspace block-storage endpoint because # only v1 is in the catalog but the service actually does support # v2. But the endpoint needs the project_id. - service_type = self._service_type_manager.get_service_type( + official_type = self._service_type_manager.get_service_type( service_type ) + if official_type is not None: + service_type = official_type if ( value and self.config.get('profile') == 'rackspace' diff --git a/tools/print-services.py b/tools/print-services.py index 079105909..cd65bff62 100644 --- a/tools/print-services.py +++ b/tools/print-services.py @@ -65,7 +65,7 @@ def make_names(): print(imp) print('\n') print("class ServicesMixin:\n") - for service in services: + for service in services: # type: ignore[assignment] if service: print(f" {service}") else: From 32734dbb7e2eea6fd4737da37d7e05bc3c1b87da Mon Sep 17 00:00:00 2001 From: ArtofBugs Date: Wed, 19 Nov 2025 14:00:07 -0800 Subject: [PATCH 3786/3836] Identity: Deprecate find_region() Regions don't actually have a name attribute, so this isn't particularly useful. Change-Id: If01a8016821cfe7fb4b4a544d3d2908003086b92 Signed-off-by: ArtofBugs --- openstack/identity/v3/_proxy.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index bda818d7f..07a23297e 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -11,6 +11,7 @@ # under the License. import typing as ty +import warnings import openstack.exceptions as exception from openstack.identity.v3 import ( @@ -59,6 +60,7 @@ from openstack import proxy from openstack import resource from openstack import utils +from openstack import warnings as os_warnings class Proxy(proxy.Proxy): @@ -1123,6 +1125,11 @@ def find_region(self, name_or_id, ignore_missing=True): attempting to find a nonexistent region. :returns: One :class:`~openstack.identity.v3.region.Region` or None """ + warnings.warn( + "find_region is deprecated and will be removed in a future " + "release; please use get_region instead.", + os_warnings.RemovedInSDK60Warning, + ) return self._find( _region.Region, name_or_id, ignore_missing=ignore_missing ) From 263dd48a1774354580d97121da2528f46117b8f7 Mon Sep 17 00:00:00 2001 From: Afonne-CID Date: Thu, 6 Nov 2025 16:52:12 +0100 Subject: [PATCH 3787/3836] Add secure hash validation for image downloads Add support for validating image downloads using secure hash algorithms (SHA256, SHA512, etc.) via the ``os_hash_algo`` and ``os_hash_value`` metadata fields. This addresses the limitation of MD5-only validation and improves data integrity verification. Closes-Bug: #2130662 Change-Id: I16c0c949c2382274daaf9fd6dad3f0ecad353b9c Signed-off-by: Afonne-CID --- openstack/image/_download.py | 155 ++++++++--- openstack/tests/fakes.py | 21 +- openstack/tests/unit/cloud/test_image.py | 5 + openstack/tests/unit/image/v2/test_image.py | 284 ++++++++++++++++---- tools/print-services.py | 6 +- 5 files changed, 359 insertions(+), 112 deletions(-) diff --git a/openstack/image/_download.py b/openstack/image/_download.py index 160fbdcda..605128053 100644 --- a/openstack/image/_download.py +++ b/openstack/image/_download.py @@ -10,22 +10,65 @@ # License for the specific language governing permissions and limitations # under the License. +import collections.abc import hashlib import io +import typing as ty from openstack import exceptions from openstack import utils -def _verify_checksum(md5, checksum): - if checksum: - digest = md5.hexdigest() - if digest != checksum: +def _verify_checksum( + hasher: ty.Any, + expected_hash: str | None, + hash_algo: str | None = None, +) -> None: + """Verify checksum using the provided hasher. + + :param hasher: A hashlib hash object + :param expected_hash: The expected hexdigest value + :param hash_algo: Optional name of the hash algorithm for error messages + :raises: InvalidResponse if the hash doesn't match + """ + if expected_hash: + digest = hasher.hexdigest() + if digest != expected_hash: + algo_msg = f" ({hash_algo})" if hash_algo else "" raise exceptions.InvalidResponse( - f"checksum mismatch: {checksum} != {digest}" + f"checksum mismatch{algo_msg}: {expected_hash} != {digest}" ) +def _integrity_iter( + iterable: collections.abc.Iterable[bytes], + hasher: ty.Any, + expected_hash: str | None, + hash_algo: str | None, +) -> collections.abc.Iterator[bytes]: + """Check image data integrity + + :param iterable: Iterable containing the image data chunks + :param hasher: A hashlib hash object + :param expected_hash: The expected hexdigest value + :param hash_algo: The hash algorithm + :yields: Chunks of data while computing hash + :raises: InvalidResponse if the hash doesn't match + """ + for chunk in iterable: + hasher.update(chunk) + yield chunk + _verify_checksum(hasher, expected_hash, hash_algo) + + +def _write_chunks( + fd: io.IOBase, chunks: collections.abc.Iterable[bytes] +) -> None: + """Write chunks to file descriptor.""" + for chunk in chunks: + fd.write(chunk) + + class DownloadMixin: id: str base_path: str @@ -44,62 +87,86 @@ def fetch( ): ... def download( - self, - session, - stream=False, - output=None, - chunk_size=1024 * 1024, + self, session, stream=False, output=None, chunk_size=1024 * 1024 ): - """Download the data contained in an image""" - # TODO(briancurtin): This method should probably offload the get - # operation into another thread or something of that nature. + """Download the data contained in an image. + + Checksum validation uses the hash algorithm metadata fields + (hash_value + hash_algo) if available, otherwise falls back to MD5 via + 'checksum' or 'Content-MD5'. No validation is performed if neither is + available. + """ + + # Fetch image metadata first to get hash info before downloading. + # This prevents race conditions and the need for a second conditional + # metadata retrieval if Content-MD5 is missing (story/1619675). + details = self.fetch(session) + meta_checksum = getattr(details, 'checksum', None) + meta_hash_value = getattr(details, 'hash_value', None) + meta_hash_algo = getattr(details, 'hash_algo', None) + url = utils.urljoin(self.base_path, self.id, 'file') resp = session.get(url, stream=stream) - # See the following bug report for details on why the checksum - # code may sometimes depend on a second GET call. - # https://storyboard.openstack.org/#!/story/1619675 - checksum = resp.headers.get("Content-MD5") + hasher = None + expected_hash = None + hash_algo = None + header_checksum = resp.headers.get("Content-MD5") - if checksum is None: - # If we don't receive the Content-MD5 header with the download, - # make an additional call to get the image details and look at - # the checksum attribute. - details = self.fetch(session) - checksum = details.checksum + if meta_hash_value and meta_hash_algo: + try: + hasher = hashlib.new(str(meta_hash_algo)) + expected_hash = meta_hash_value + hash_algo = meta_hash_algo + except ValueError as ve: + if not str(ve).startswith('unsupported hash type'): + raise exceptions.SDKException( + f"Unsupported hash algorithm '{meta_hash_algo}': {ve}" + ) + + # Fall back to MD5 from metadata or header + if not hasher: + md5_source = meta_checksum or header_checksum + if md5_source: + hasher = hashlib.md5(usedforsecurity=False) + expected_hash = md5_source + hash_algo = 'md5' + + if hasher is None: + session.log.warning( + "Unable to verify the integrity of image %s " + "- no hash available", + self.id, + ) - md5 = hashlib.md5(usedforsecurity=False) if output: try: + chunks = resp.iter_content(chunk_size=chunk_size) + if hasher is not None: + chunks = _integrity_iter( + chunks, hasher, expected_hash, hash_algo + ) + if isinstance(output, io.IOBase): - for chunk in resp.iter_content(chunk_size=chunk_size): - output.write(chunk) - md5.update(chunk) + _write_chunks(output, chunks) else: with open(output, 'wb') as fd: - for chunk in resp.iter_content(chunk_size=chunk_size): - fd.write(chunk) - md5.update(chunk) - _verify_checksum(md5, checksum) + _write_chunks(fd, chunks) return resp except Exception as e: raise exceptions.SDKException(f"Unable to download image: {e}") - # if we are returning the repsonse object, ensure that it - # has the content-md5 header so that the caller doesn't - # need to jump through the same hoops through which we - # just jumped. + if stream: - resp.headers['content-md5'] = checksum + # Set content-md5 header for backward compatibility with callers + # who expect hash info in the response when streaming + if hash_algo == 'md5' and expected_hash: + resp.headers['content-md5'] = expected_hash return resp - if checksum is not None: - _verify_checksum( - hashlib.md5(resp.content, usedforsecurity=False), checksum - ) - else: - session.log.warning( - "Unable to verify the integrity of image %s", (self.id) - ) + if hasher is not None: + # Loads entire image into memory! + hasher.update(resp.content) + _verify_checksum(hasher, expected_hash, hash_algo) return resp diff --git a/openstack/tests/fakes.py b/openstack/tests/fakes.py index 7c9dc5a5d..70ca88e80 100644 --- a/openstack/tests/fakes.py +++ b/openstack/tests/fakes.py @@ -247,14 +247,19 @@ def make_fake_image( checksum='ee36e35a297980dee1b514de9803ec6d', ): if data: - md5 = hashlib.md5(usedforsecurity=False) - sha256 = hashlib.sha256() + md5_hash = hashlib.md5(usedforsecurity=False) + sha256_hash = hashlib.sha256() + sha512_hash = hashlib.sha512() with open(data, 'rb') as file_obj: for chunk in iter(lambda: file_obj.read(8192), b''): - md5.update(chunk) - sha256.update(chunk) - md5 = md5.hexdigest() - sha256 = sha256.hexdigest() + md5_hash.update(chunk) + sha256_hash.update(chunk) + sha512_hash.update(chunk) + md5 = md5_hash.hexdigest() + sha256 = sha256_hash.hexdigest() + sha512 = sha512_hash.hexdigest() + else: + sha512 = None return { 'image_state': 'available', 'container_format': 'bare', @@ -282,6 +287,10 @@ def make_fake_image( 'owner_specified.openstack.sha256': sha256 or NO_SHA256, 'owner_specified.openstack.object': f'images/{image_name}', 'protected': False, + # Add secure hash fields (os_hash_algo and os_hash_value) + # Default to sha512 if data was provided, otherwise None + 'os_hash_algo': 'sha512' if sha512 else None, + 'os_hash_value': sha512 if sha512 else None, } diff --git a/openstack/tests/unit/cloud/test_image.py b/openstack/tests/unit/cloud/test_image.py index 21b419c48..e72c39c98 100644 --- a/openstack/tests/unit/cloud/test_image.py +++ b/openstack/tests/unit/cloud/test_image.py @@ -124,6 +124,11 @@ def _register_image_mocks(self): uri=f'https://image.example.com/v2/images?name={self.image_name}', json=self.fake_search_return, ), + dict( + method='GET', + uri=f'https://image.example.com/v2/images/{self.image_id}', + json=self.fake_image_dict, + ), dict( method='GET', uri=f'https://image.example.com/v2/images/{self.image_id}/file', diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 9638c7771..a4f66a475 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -13,6 +13,7 @@ import hashlib import io import operator +import os import tempfile from unittest import mock @@ -381,73 +382,92 @@ def test_stage_error(self): self.assertRaises(exceptions.SDKException, sot.stage, self.sess) def test_download_checksum_match(self): - sot = image.Image(**EXAMPLE) + expected_hash = hashlib.sha512(b"abc").hexdigest() + example_with_hash = EXAMPLE.copy() + example_with_hash['os_hash_value'] = expected_hash + sot = image.Image(**example_with_hash) - resp = FakeResponse( + resp1 = FakeResponse(example_with_hash) + resp2 = FakeResponse( b"abc", - headers={ - "Content-MD5": "900150983cd24fb0d6963f7d28e17f72", - "Content-Type": "application/octet-stream", - }, + headers={"Content-Type": "application/octet-stream"}, ) - self.sess.get.return_value = resp + + self.sess.get.side_effect = [resp1, resp2] rv = sot.download(self.sess) - self.sess.get.assert_called_with( - 'images/IDENTIFIER/file', stream=False + self.sess.get.assert_has_calls( + [ + mock.call( + 'images/IDENTIFIER', + microversion=None, + params={}, + skip_cache=False, + ), + mock.call('images/IDENTIFIER/file', stream=False), + ] ) - self.assertEqual(rv, resp) + self.assertEqual(rv, resp2) def test_download_checksum_mismatch(self): - sot = image.Image(**EXAMPLE) + example_with_wrong_hash = EXAMPLE.copy() + example_with_wrong_hash['os_hash_value'] = "wrong_hash_value" + sot = image.Image(**example_with_wrong_hash) - resp = FakeResponse( + resp1 = FakeResponse(example_with_wrong_hash) + resp2 = FakeResponse( b"abc", - headers={ - "Content-MD5": "the wrong checksum", - "Content-Type": "application/octet-stream", - }, + headers={"Content-Type": "application/octet-stream"}, ) - self.sess.get.return_value = resp + + self.sess.get.side_effect = [resp1, resp2] self.assertRaises(exceptions.InvalidResponse, sot.download, self.sess) - def test_download_no_checksum_header(self): - sot = image.Image(**EXAMPLE) + def test_download_md5_fallback(self): + expected_md5 = hashlib.md5(b"abc", usedforsecurity=False).hexdigest() + example_md5_only = EXAMPLE.copy() + example_md5_only['os_hash_algo'] = None + example_md5_only['os_hash_value'] = None + example_md5_only['checksum'] = expected_md5 + sot = image.Image(**example_md5_only) - resp1 = FakeResponse( + resp1 = FakeResponse(example_md5_only) + resp2 = FakeResponse( b"abc", headers={"Content-Type": "application/octet-stream"} ) - resp2 = FakeResponse({"checksum": "900150983cd24fb0d6963f7d28e17f72"}) - self.sess.get.side_effect = [resp1, resp2] rv = sot.download(self.sess) self.sess.get.assert_has_calls( [ - mock.call('images/IDENTIFIER/file', stream=False), mock.call( 'images/IDENTIFIER', microversion=None, params={}, skip_cache=False, ), + mock.call('images/IDENTIFIER/file', stream=False), ] ) - self.assertEqual(rv, resp1) + self.assertEqual(rv, resp2) def test_download_no_checksum_at_all2(self): - sot = image.Image(**EXAMPLE) - - resp1 = FakeResponse( + # No hash available at all + example_no_hash = EXAMPLE.copy() + example_no_hash['os_hash_algo'] = None + example_no_hash['os_hash_value'] = None + example_no_hash['checksum'] = None + sot = image.Image(**example_no_hash) + + resp1 = FakeResponse(example_no_hash) + resp2 = FakeResponse( b"abc", headers={"Content-Type": "application/octet-stream"} ) - resp2 = FakeResponse({"checksum": None}) - self.sess.get.side_effect = [resp1, resp2] with self.assertLogs(logger='openstack', level="WARNING") as log: @@ -457,74 +477,220 @@ def test_download_no_checksum_at_all2(self): len(log.records), 1, "Too many warnings were logged" ) self.assertEqual( - "Unable to verify the integrity of image %s", + "Unable to verify the integrity of image %s " + "- no hash available", log.records[0].msg, ) self.assertEqual((sot.id,), log.records[0].args) self.sess.get.assert_has_calls( [ - mock.call('images/IDENTIFIER/file', stream=False), mock.call( 'images/IDENTIFIER', microversion=None, params={}, skip_cache=False, ), + mock.call('images/IDENTIFIER/file', stream=False), ] ) - self.assertEqual(rv, resp1) + self.assertEqual(rv, resp2) def test_download_stream(self): - sot = image.Image(**EXAMPLE) + expected_hash = hashlib.sha512(b"abc").hexdigest() + example_with_hash = EXAMPLE.copy() + example_with_hash['os_hash_value'] = expected_hash + sot = image.Image(**example_with_hash) - resp = FakeResponse( + resp1 = FakeResponse(example_with_hash) + resp2 = FakeResponse( b"abc", - headers={ - "Content-MD5": "900150983cd24fb0d6963f7d28e17f72", - "Content-Type": "application/octet-stream", - }, + headers={"Content-Type": "application/octet-stream"}, ) - self.sess.get.return_value = resp + + self.sess.get.side_effect = [resp1, resp2] rv = sot.download(self.sess, stream=True) - self.sess.get.assert_called_with('images/IDENTIFIER/file', stream=True) + self.sess.get.assert_has_calls( + [ + mock.call( + 'images/IDENTIFIER', + microversion=None, + params={}, + skip_cache=False, + ), + mock.call('images/IDENTIFIER/file', stream=True), + ] + ) - self.assertEqual(rv, resp) + self.assertEqual(rv, resp2) + self.assertIsNone(rv.headers.get('content-md5')) def test_image_download_output_fd(self): output_file = io.BytesIO() - sot = image.Image(**EXAMPLE) + expected_hash = hashlib.sha512(b'0102').hexdigest() + example_with_hash = EXAMPLE.copy() + example_with_hash['os_hash_value'] = expected_hash + sot = image.Image(**example_with_hash) + + fetch_response = FakeResponse(example_with_hash) response = mock.Mock() response.status_code = 200 response.iter_content.return_value = [b'01', b'02'] - response.headers = { - 'Content-MD5': calculate_md5_checksum( - response.iter_content.return_value - ) - } - self.sess.get = mock.Mock(return_value=response) + response.headers = {} + + self.sess.get = mock.Mock(side_effect=[fetch_response, response]) sot.download(self.sess, output=output_file) output_file.seek(0) self.assertEqual(b'0102', output_file.read()) def test_image_download_output_file(self): - sot = image.Image(**EXAMPLE) + expected_hash = hashlib.sha512(b'0102').hexdigest() + example_with_hash = EXAMPLE.copy() + example_with_hash['os_hash_value'] = expected_hash + sot = image.Image(**example_with_hash) + + fetch_response = FakeResponse(example_with_hash) response = mock.Mock() response.status_code = 200 response.iter_content.return_value = [b'01', b'02'] - response.headers = { - 'Content-MD5': calculate_md5_checksum( - response.iter_content.return_value - ) - } - self.sess.get = mock.Mock(return_value=response) + response.headers = {} + + self.sess.get = mock.Mock(side_effect=[fetch_response, response]) + + output_file = tempfile.NamedTemporaryFile(delete=False) + output_file.close() + try: + sot.download(self.sess, output=output_file.name) + with open(output_file.name, 'rb') as fd: + self.assertEqual(b'0102', fd.read()) + finally: + os.unlink(output_file.name) + + def test_download_secure_hash_sha256(self): + expected_hash = hashlib.sha256(b"abc").hexdigest() + example_with_sha256 = EXAMPLE.copy() + example_with_sha256['os_hash_algo'] = 'sha256' + example_with_sha256['os_hash_value'] = expected_hash + sot = image.Image(**example_with_sha256) + + resp1 = FakeResponse(example_with_sha256) + resp2 = FakeResponse( + b"abc", + headers={"Content-Type": "application/octet-stream"}, + ) - output_file = tempfile.NamedTemporaryFile() - sot.download(self.sess, output=output_file.name) - output_file.seek(0) - self.assertEqual(b'0102', output_file.read()) + self.sess.get.side_effect = [resp1, resp2] + + rv = sot.download(self.sess) + self.assertEqual(rv, resp2) + + def test_download_secure_hash_sha384(self): + expected_hash = hashlib.sha384(b"abc").hexdigest() + example_with_sha384 = EXAMPLE.copy() + example_with_sha384['os_hash_algo'] = 'sha384' + example_with_sha384['os_hash_value'] = expected_hash + sot = image.Image(**example_with_sha384) + + resp1 = FakeResponse(example_with_sha384) + resp2 = FakeResponse( + b"abc", + headers={"Content-Type": "application/octet-stream"}, + ) + + self.sess.get.side_effect = [resp1, resp2] + + rv = sot.download(self.sess) + self.assertEqual(rv, resp2) + + def test_download_content_md5_header_ignored(self): + correct_hash = hashlib.sha512(b"abc").hexdigest() + example_with_hash = EXAMPLE.copy() + example_with_hash['os_hash_value'] = correct_hash + sot = image.Image(**example_with_hash) + + resp1 = FakeResponse(example_with_hash) + resp2 = FakeResponse( + b"abc", + headers={ + "Content-MD5": "wrong_header_hash_that_should_be_ignored", + "Content-Type": "application/octet-stream", + }, + ) + + self.sess.get.side_effect = [resp1, resp2] + + # Succeeds since only metadata hash is used + rv = sot.download(self.sess) + self.assertEqual(rv, resp2) + + def test_download_secure_hash_mismatch_sha512(self): + example_with_wrong_hash = EXAMPLE.copy() + example_with_wrong_hash['os_hash_value'] = "wrong_sha512_hash" + sot = image.Image(**example_with_wrong_hash) + + resp1 = FakeResponse(example_with_wrong_hash) + resp2 = FakeResponse( + b"abc", + headers={"Content-Type": "application/octet-stream"}, + ) + + self.sess.get.side_effect = [resp1, resp2] + + self.assertRaises(exceptions.InvalidResponse, sot.download, self.sess) + + def test_download_md5_fallback_mismatch(self): + example_md5_only = EXAMPLE.copy() + example_md5_only['os_hash_algo'] = None + example_md5_only['os_hash_value'] = None + example_md5_only['checksum'] = "wrong_md5_checksum" + sot = image.Image(**example_md5_only) + + resp1 = FakeResponse(example_md5_only) + resp2 = FakeResponse( + b"abc", + headers={"Content-Type": "application/octet-stream"}, + ) + + self.sess.get.side_effect = [resp1, resp2] + + self.assertRaises(exceptions.InvalidResponse, sot.download, self.sess) + + def test_download_unsupported_hash_algo_raises(self): + example_unsupported = EXAMPLE.copy() + example_unsupported['os_hash_algo'] = 'unsupported_algo' + example_unsupported['os_hash_value'] = 'some_hash_value' + sot = image.Image(**example_unsupported) + + resp1 = FakeResponse(example_unsupported) + resp2 = FakeResponse( + b"abc", + headers={"Content-Type": "application/octet-stream"}, + ) + + self.sess.get.side_effect = [resp1, resp2] + + self.assertRaises(exceptions.SDKException, sot.download, self.sess) + + def test_download_unsupported_hash_algo_falls_back_to_md5(self): + correct_md5 = hashlib.md5(b"abc", usedforsecurity=False).hexdigest() + example_unsupported = EXAMPLE.copy() + example_unsupported['os_hash_algo'] = 'ancient_hash_algo' + example_unsupported['os_hash_value'] = 'irrelevant_hash' + example_unsupported['checksum'] = correct_md5 + sot = image.Image(**example_unsupported) + + resp1 = FakeResponse(example_unsupported) + resp2 = FakeResponse( + b"abc", + headers={"Content-Type": "application/octet-stream"}, + ) + + self.sess.get.side_effect = [resp1, resp2] + + rv = sot.download(self.sess) + self.assertEqual(rv, resp2) def test_image_update(self): values = EXAMPLE.copy() diff --git a/tools/print-services.py b/tools/print-services.py index cd65bff62..0d3eb171b 100644 --- a/tools/print-services.py +++ b/tools/print-services.py @@ -65,9 +65,9 @@ def make_names(): print(imp) print('\n') print("class ServicesMixin:\n") - for service in services: # type: ignore[assignment] - if service: - print(f" {service}") + for attr in services: + if attr: + print(f" {attr}") else: print() From eda56909db9844499cfb2dcb9b202fe743a1dc89 Mon Sep 17 00:00:00 2001 From: Afonne-CID Date: Thu, 27 Nov 2025 14:38:35 +0100 Subject: [PATCH 3788/3836] Fix `_original_body` synchronization after commit Currently, ``_original_body`` is assigned ``body_attrs.copy()`` which only contains attributes from the response; contrary to what the inline comment suggests/intends. When ``_body.attributes`` has additional fields (from partial responses or previous operations), ``_original_body`` becomes incomplete. This causes subsequent patches to incorrectly include already-committed fields. Change-Id: Icc67f4761df51e51c2ff6101be9b7d1c16200a72 Signed-off-by: Afonne-CID --- openstack/resource.py | 2 +- .../tests/unit/baremetal/v1/test_node.py | 59 ++++++++++++++++++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/openstack/resource.py b/openstack/resource.py index dd5030473..f221b7e9b 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1258,7 +1258,7 @@ def _translate_response( self._body.clean() if self.commit_jsonpatch or self.allow_patch: # We need the original body to compare against - self._original_body = body_attrs.copy() + self._original_body = self._body.attributes.copy() except ValueError: # Server returned not parse-able response (202, 204, etc) # Do simply nothing diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index bb2d95b7b..c29600ab4 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -1268,7 +1268,9 @@ def setUp(self): super().setUp() self.node = node.Node(**FAKE) self.session = mock.Mock( - spec=adapter.Adapter, default_microversion=None + spec=adapter.Adapter, + default_microversion='1.1', + retriable_status_codes=None, ) self.session.log = mock.Mock() @@ -1303,6 +1305,61 @@ def test_node_patch_reset_interfaces( self.assertEqual(commit_kwargs['retry_on_conflict'], True) mock_patch.assert_not_called() + @mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) + @mock.patch.object(node.Node, '_get_session', lambda self, x: x) + def test_original_body_sync_after_commit(self, mock_patch): + """Test _original_body synchronization after commit().""" + + self.node.name = 'server-1' + self.node.description = 'initial-desc' + + mock_response1 = mock.Mock() + mock_response1.status_code = 200 + mock_response1.headers = {} + mock_response1.json.return_value = { + 'uuid': FAKE['uuid'], + 'name': 'server-1', + 'driver': FAKE['driver'], + } + self.session.patch.return_value = mock_response1 + + self.node.commit(self.session) + + self.assertEqual(self.node._original_body.get('name'), 'server-1') + self.assertEqual(self.node._body.attributes.get('name'), 'server-1') + + # NOTE(cid): _original_body should have description even though + # it wasn't in the response, because it exists in _body.attributes + self.assertEqual( + self.node._original_body.get('description'), + self.node._body.attributes.get('description'), + '_original_body is not in sync with _body.attributes', + ) + + self.node.description = 'updated-desc' + patch = self.node._prepare_request_body(patch=True, prepend_key=False) + + # Verify patch only contains description change + patch_paths = [op.get('path') for op in patch] + self.assertIn( + '/description', + patch_paths, + 'Patch should include description change', + ) + self.assertNotIn( + '/name', + patch_paths, + 'Patch should NOT include name (already committed)', + ) + + # Verify only one operation in patch + self.assertEqual( + len(patch), 1, f'Patch should only have description, got: {patch}' + ) + self.assertEqual(patch[0]['path'], '/description') + self.assertEqual(patch[0]['op'], 'replace') + self.assertEqual(patch[0]['value'], 'updated-desc') + @mock.patch('time.sleep', lambda _t: None) @mock.patch.object(node.Node, 'fetch', autospec=True) From 9821bc3289f3005bd014af7fd4c8c7d41664716a Mon Sep 17 00:00:00 2001 From: Riccardo Pittau Date: Mon, 1 Dec 2025 15:11:59 +0100 Subject: [PATCH 3789/3836] Update name of bifrost CI job Ironic does not support tinyipa images anymore, all jobs use DIB based IPA ramdisks Depends-On: I569a766826405513f7beab5d45a52a8bbf42ddfd Change-Id: I048d10aa2ca1657260bcac5547fc3edfbbead81d Signed-off-by: Riccardo Pittau --- zuul.d/metal-jobs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zuul.d/metal-jobs.yaml b/zuul.d/metal-jobs.yaml index 22a837612..26118f6d2 100644 --- a/zuul.d/metal-jobs.yaml +++ b/zuul.d/metal-jobs.yaml @@ -9,7 +9,7 @@ - job: name: bifrost-integration-openstacksdk-src - parent: bifrost-integration-tinyipa-ubuntu-jammy + parent: bifrost-integration-on-ubuntu-jammy required-projects: - openstack/ansible-collections-openstack - openstack/openstacksdk From d07cfac62667ed7066abc42d6b0d707ce581e4f7 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 5 Dec 2025 16:19:45 +0000 Subject: [PATCH 3790/3836] image: Deprecate rewriting of is_public property We should not be in the business of managing things like this. Change-Id: I6080d21bf506afaf7ef7d4e262e9ced9f227aeff Signed-off-by: Stephen Finucane --- openstack/image/v2/_proxy.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 9a214b015..3a920204a 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -587,6 +587,12 @@ def _upload_image( # is_public, we know what they mean. If they give us visibility, they # know that they mean. if 'is_public' in kwargs['properties']: + warnings.warn( + "The 'is_public' property is not supported by Glance v2: use " + "'visibility=public/private' instead", + os_warnings.RemovedInSDK60Warning, + ) + is_public = kwargs['properties'].pop('is_public') if is_public: kwargs['visibility'] = 'public' From 2793a02884be5b7f9e1caf40a790fad51ac0db73 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 5 Dec 2025 16:21:52 +0000 Subject: [PATCH 3791/3836] image: Move some code around This is referenced in multiple places. Place it before all of them. Change-Id: I1314b581848ecd146020c7d1743d2b9757493643 Signed-off-by: Stephen Finucane --- openstack/image/v2/_proxy.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 3a920204a..3dba409cb 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -113,6 +113,21 @@ def clear_cache(self, target='both'): # ====== IMAGES ====== + def _make_v2_image_params(self, meta, properties): + ret: dict = {} + for k, v in iter(properties.items()): + if k in _INT_PROPERTIES: + ret[k] = int(v) + elif k in _RAW_PROPERTIES: + ret[k] = v + else: + if v is None: + ret[k] = None + else: + ret[k] = str(v) + ret.update(meta) + return ret + def create_image( self, name, @@ -643,21 +658,6 @@ def _upload_image( f"Image creation failed: {e!s}" ) from e - def _make_v2_image_params(self, meta, properties): - ret: dict = {} - for k, v in iter(properties.items()): - if k in _INT_PROPERTIES: - ret[k] = int(v) - elif k in _RAW_PROPERTIES: - ret[k] = v - else: - if v is None: - ret[k] = None - else: - ret[k] = str(v) - ret.update(meta) - return ret - def _upload_image_put( self, name, From 155915415eca6eaa7ae96c4fa02b088b85b0415a Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Mon, 8 Dec 2025 08:52:18 +0100 Subject: [PATCH 3792/3836] Fix Python 3.14 annotation Under 3.14, one should use builtins.list, not just list. Signed-off-by: Thomas Goirand Change-Id: I2311a213e301d21b38a83ee26561da01a3212af6 --- openstack/resource.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openstack/resource.py b/openstack/resource.py index dd5030473..5ebb16a68 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -32,6 +32,7 @@ class that represent a remote resource. The attributes that and then returned to the caller. """ +import builtins import collections import collections.abc import inspect @@ -1114,7 +1115,7 @@ def _prepare_request_body( prepend_key: bool, *, resource_request_key: str | None = None, - ) -> dict[str, ty.Any] | list[ty.Any]: + ) -> dict[str, ty.Any] | builtins.list[ty.Any]: body: dict[str, ty.Any] | list[ty.Any] if patch: if not self._store_unknown_attrs_as_properties: @@ -1478,7 +1479,7 @@ def create( def bulk_create( cls, session: adapter.Adapter, - data: list[dict[str, ty.Any]], + data: builtins.list[dict[str, ty.Any]], prepend_key: bool = True, base_path: str | None = None, *, From 6f9f4c04aeca654047b51128ebafa4f4adbd8920 Mon Sep 17 00:00:00 2001 From: Andriy Kurilin Date: Mon, 8 Dec 2025 16:01:34 +0100 Subject: [PATCH 3793/3836] Define tenant_id as a server-side filter for Port resource Closes-bug: 2134391 Change-Id: Ia90d5dba805ee45f2d76e8cc5fcbed78b56eef07 Signed-off-by: Andriy Kurilin --- openstack/network/v2/port.py | 2 ++ openstack/tests/unit/network/v2/test_port.py | 1 + 2 files changed, 3 insertions(+) diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index a49fa4088..e68f28be1 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -49,6 +49,8 @@ class Port(_base.NetworkResource, _base.TagMixinNetwork): 'security_groups', 'sort_key', 'sort_dir', + # For backward compatibility include tenant_id as query param + tenant_id='project_id', is_admin_state_up='admin_state_up', is_port_security_enabled='port_security_enabled', security_group_ids='security_groups', diff --git a/openstack/tests/unit/network/v2/test_port.py b/openstack/tests/unit/network/v2/test_port.py index 9fb34dcd1..6376ae487 100644 --- a/openstack/tests/unit/network/v2/test_port.py +++ b/openstack/tests/unit/network/v2/test_port.py @@ -107,6 +107,7 @@ def test_basic(self): "is_admin_state_up": "admin_state_up", "is_port_security_enabled": "port_security_enabled", "project_id": "project_id", + "tenant_id": "project_id", "security_group_ids": "security_groups", "limit": "limit", "marker": "marker", From 2d36afcd045b5f8d526d981fb39fb3c3f4375ebb Mon Sep 17 00:00:00 2001 From: Afonne-CID Date: Tue, 15 Jul 2025 21:30:39 +0100 Subject: [PATCH 3794/3836] Add Node.instance_name support Change-Id: Ic952451ac61ff27a0e8898408cbdec03e66d8aaa Signed-off-by: Afonne-CID --- openstack/baremetal/v1/node.py | 8 +- openstack/tests/fakes.py | 2 + .../baremetal/test_baremetal_node.py | 131 ++++++++++++++++++ .../tests/unit/baremetal/v1/test_node.py | 2 + openstack/tests/unit/test_utils.py | 2 +- 5 files changed, 142 insertions(+), 3 deletions(-) diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 80768b408..b573a9665 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -94,6 +94,7 @@ class Node(_common.Resource): 'driver', 'fault', 'include_children', + 'instance_name', 'parent_node', 'provision_state', 'resource_class', @@ -103,8 +104,8 @@ class Node(_common.Resource): is_maintenance='maintenance', ) - # Ability to run predefined sets of steps on a node using runbooks. - _max_microversion = '1.92' + # The name of the instance associated with this node. + _max_microversion = '1.104' # Properties #: The UUID of the allocation associated with this node. Added in API @@ -151,6 +152,9 @@ class Node(_common.Resource): instance_info = resource.Body("instance_info") #: UUID of the nova instance associated with this node. instance_id = resource.Body("instance_uuid") + #: The name of the instance associated with this node. Added in API + #: microversion 1.104. + instance_name = resource.Body("instance_name") #: Override enabling of automated cleaning. Added in API microversion 1.47. is_automated_clean_enabled = resource.Body("automated_clean", type=bool) #: Whether console access is enabled on this node. diff --git a/openstack/tests/fakes.py b/openstack/tests/fakes.py index 7c9dc5a5d..de1ceb065 100644 --- a/openstack/tests/fakes.py +++ b/openstack/tests/fakes.py @@ -399,6 +399,7 @@ def __init__( driver_info=None, chassis_uuid=None, instance_info=None, + instance_name=None, instance_uuid=None, properties=None, reservation=None, @@ -411,6 +412,7 @@ def __init__( self.driver_info = driver_info self.chassis_uuid = chassis_uuid self.instance_info = instance_info + self.instance_name = instance_name self.instance_uuid = instance_uuid self.properties = properties self.reservation = reservation diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index cddfb922e..d4dbdb2aa 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -557,3 +557,134 @@ def test_list_firmware(self): self.assertEqual("no-firmware", node.firmware_interface) result = self.operator_cloud.baremetal.list_node_firmware(node) self.assertEqual({'firmware': []}, result) + + +class TestBareMetalNodeInstanceName(base.BaseBaremetalTest): + min_microversion = '1.104' + + def test_node_instance_name(self): + """Test instance_name field functionality.""" + node = self.create_node(name='test-node-instance-name') + self.assertIsNone(node.instance_name) + + # Set instance_name + instance_name = "test-instance-123" + node = self.operator_cloud.baremetal.patch_node( + node.id, + [{'op': 'add', 'path': '/instance_name', 'value': instance_name}], + ) + self.assertEqual(instance_name, node.instance_name) + + # Verify on server side + node = self.operator_cloud.baremetal.get_node(node.id) + self.assertEqual(instance_name, node.instance_name) + + # Clear instance_name + node = self.operator_cloud.baremetal.patch_node( + node.id, [{'op': 'remove', 'path': '/instance_name'}] + ) + self.assertIsNone(node.instance_name) + + # Verify on server side + node = self.operator_cloud.baremetal.get_node(node.id) + self.assertIsNone(node.instance_name) + + def test_node_instance_name_query(self): + """Test querying nodes by instance_name.""" + node1 = self.create_node(name='node1') + node2 = self.create_node(name='node2') + + # Set different instance names using explicit patches + self.operator_cloud.baremetal.patch_node( + node1.id, + [{'op': 'add', 'path': '/instance_name', 'value': 'instance-1'}], + ) + self.operator_cloud.baremetal.patch_node( + node2.id, + [{'op': 'add', 'path': '/instance_name', 'value': 'instance-2'}], + ) + + # Query by instance_name + result = list( + self.operator_cloud.baremetal.nodes( + instance_name="instance-1", details=True + ) + ) + self.assertEqual(1, len(result)) + self.assertEqual(node1.id, result[0].id) + self.assertEqual("instance-1", result[0].instance_name) + + # Query by different instance_name + result = list( + self.operator_cloud.baremetal.nodes( + instance_name="instance-2", details=True + ) + ) + self.assertEqual(1, len(result)) + self.assertEqual(node2.id, result[0].id) + self.assertEqual("instance-2", result[0].instance_name) + + # Query by non-existent instance_name + result = list( + self.operator_cloud.baremetal.nodes( + instance_name="non-existent", details=True + ) + ) + self.assertEqual(0, len(result)) + + def test_node_instance_name_with_instance_info(self): + """Test that instance_name works with instance_info.display_name.""" + node = self.create_node(name='test-node-display-name') + + # Set instance_info.display_name first + self.operator_cloud.baremetal.patch_node( + node.id, + [ + { + 'op': 'add', + 'path': '/instance_info', + 'value': {'display_name': 'display-name-123'}, + } + ], + ) + + # Verify instance_name was automatically set + node = self.operator_cloud.baremetal.get_node(node.id) + self.assertEqual('display-name-123', node.instance_name) + self.assertEqual( + {'display_name': 'display-name-123'}, node.instance_info + ) + + # Set instance_name explicitly + self.operator_cloud.baremetal.patch_node( + node.id, + [ + { + 'op': 'replace', + 'path': '/instance_name', + 'value': 'explicit-name', + } + ], + ) + + # Verify explicit instance_name takes precedence + node = self.operator_cloud.baremetal.get_node(node.id) + self.assertEqual('explicit-name', node.instance_name) + + self.operator_cloud.baremetal.patch_node( + node.id, + [ + { + 'op': 'replace', + 'path': '/instance_info', + 'value': {'display_name': 'new-display-name'}, + } + ], + ) + + # Verify instance_name was not overridden + node = self.operator_cloud.baremetal.get_node(node.id) + self.assertEqual('explicit-name', node.instance_name) + self.assertEqual( + {'display_name': 'new-display-name'}, node.instance_info + ) diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index bb2d95b7b..358cc3f1a 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -40,6 +40,7 @@ "inspection_finished_at": None, "inspection_started_at": None, "instance_info": {}, + "instance_name": "test-instance", "instance_uuid": None, "last_error": None, "lessee": None, @@ -141,6 +142,7 @@ def test_instantiate(self): self.assertEqual(FAKE['extra'], sot.extra) self.assertEqual(FAKE['firmware_interface'], sot.firmware_interface) self.assertEqual(FAKE['instance_info'], sot.instance_info) + self.assertEqual(FAKE['instance_name'], sot.instance_name) self.assertEqual(FAKE['instance_uuid'], sot.instance_id) self.assertEqual(FAKE['console_enabled'], sot.is_console_enabled) self.assertEqual(FAKE['maintenance'], sot.is_maintenance) diff --git a/openstack/tests/unit/test_utils.py b/openstack/tests/unit/test_utils.py index 78bbbd042..b9ecb69c2 100644 --- a/openstack/tests/unit/test_utils.py +++ b/openstack/tests/unit/test_utils.py @@ -211,7 +211,7 @@ def test_with_value(self): def test_value_more_than_max(self): self.assertEqual( - '1.99', utils.maximum_supported_microversion(self.adapter, '1.100') + '1.99', utils.maximum_supported_microversion(self.adapter, '1.104') ) def test_value_less_than_min(self): From 42509424ceb67509d057b2406781bc7aeedfd224 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Tue, 6 Jan 2026 08:20:20 +1300 Subject: [PATCH 3795/3836] Do not use the global Prometheus collector registry The openstacksdk documentation states that if applications want to opt-in to Prometheus metrics provided by openstacksdk, collector_registry should have a prometheus_client.CollectorRegistry object passed to it when instantiating the openstack.Connection object. However, with the way it is currently implemented, openstacksdk will use the global prometheus_client.REGISTRY object if collector_registry was not set to a non-None value. This means that if the prometheus-client package is installed, but not configured in openstacksdk, openstacksdk *always* publishes metrics to the global Prometheus collector registry, even if it is not used. This not only has the potential to cause unintended pollution of the global Prometheus collector registry with unwanted metrics, it also can be a potential cause of memory leaks if openstacksdk is used to make a large amount/variety of requests. This potentially affects a wide variety of OpenStack services due to oslo.messaging depending on oslo.metrics, which requires prometheus-client. Change-Id: Iab6fde785d9a7fb44088e951e5e8c3bc398a621f Closes-Bug: 2137505 Signed-off-by: Callum Dickinson --- openstack/config/cloud_region.py | 2 -- .../notes/bug-2137505-9390f7f914817f81.yaml | 13 +++++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/bug-2137505-9390f7f914817f81.yaml diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 729a134a5..45b84615b 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -1354,8 +1354,6 @@ def get_statsd_prefix(self) -> str: def get_prometheus_registry( self, ) -> ty.Optional['prometheus_client.CollectorRegistry']: - if not self._collector_registry and prometheus_client: - self._collector_registry = prometheus_client.REGISTRY return self._collector_registry def get_prometheus_histogram( diff --git a/releasenotes/notes/bug-2137505-9390f7f914817f81.yaml b/releasenotes/notes/bug-2137505-9390f7f914817f81.yaml new file mode 100644 index 000000000..a3132317e --- /dev/null +++ b/releasenotes/notes/bug-2137505-9390f7f914817f81.yaml @@ -0,0 +1,13 @@ +--- +fixes: + - | + openstacksdk was publishing metrics to the global Prometheus collector + registry (``prometheus_client.REGISTRY``) if the ``prometheus-client`` + library was installed, but the ``Connection`` object was not configured + to publish Prometheus metrics to a custom registry. This was causing the + global Prometheus collector registry to be polluted with potentially + unwanted metrics, and was also a potential cause of memory leaks if + openstacksdk is used to make a large number of requests. This issue has + now been fixed; openstacksdk will only publish Prometheus metrics when + ``collector_registry`` has been passed to the connection object, and + will only publish to that registry. From 19e1a74727806d3b417dab5ded47aa312a95336d Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Tue, 13 Jan 2026 17:21:34 -0600 Subject: [PATCH 3796/3836] feat: add missing baremetal port fields The vendor and category fields were added in ironic API 1.100 and 1.101 respectively which were released as part of 2025.2. Change-Id: I3ab166ac30155ebcb6b649830ebc7a4e7b69b2fa Signed-off-by: Doug Goldstein --- openstack/baremetal/v1/port.py | 8 +++++++- ...d-baremetal-port-vendor-category-a544098f87558c8c.yaml | 5 +++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-baremetal-port-vendor-category-a544098f87558c8c.yaml diff --git a/openstack/baremetal/v1/port.py b/openstack/baremetal/v1/port.py index 72d61f584..f2ee56d17 100644 --- a/openstack/baremetal/v1/port.py +++ b/openstack/baremetal/v1/port.py @@ -42,11 +42,15 @@ class Port(_common.Resource): # Query filter by shard added in 1.82 # The name field added in 1.88 # The description field added in 1.97 - _max_microversion = '1.97' + # The vendor field added in 1.100 + # The category field added in 1.101 + _max_microversion = '1.101' #: The physical hardware address of the network port, typically the #: hardware MAC address. address = resource.Body('address') + #: The category for the port + category = resource.Body('category') #: Timestamp at which the port was created. created_at = resource.Body('created_at') #: The description for the port @@ -85,6 +89,8 @@ class Port(_common.Resource): trunk_details = resource.Body('trunk_details', type=dict) #: Timestamp at which the port was last updated. updated_at = resource.Body('updated_at') + #: The vendor for the port + vendor = resource.Body('vendor') PortDetail = Port diff --git a/releasenotes/notes/add-baremetal-port-vendor-category-a544098f87558c8c.yaml b/releasenotes/notes/add-baremetal-port-vendor-category-a544098f87558c8c.yaml new file mode 100644 index 000000000..0b08304a7 --- /dev/null +++ b/releasenotes/notes/add-baremetal-port-vendor-category-a544098f87558c8c.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add the ``vendor`` field and ``category`` field on the baremetal ``Port`` + object as added in Ironic API 1.100 and 1.101 respectively. From 4fd950b9e7ae441860c3add43df5fe7acf86139e Mon Sep 17 00:00:00 2001 From: Johannes Beisiegel Date: Fri, 16 Jan 2026 13:35:25 +0100 Subject: [PATCH 3797/3836] fix Proxy.server_actions signature to support kwargs The compute Proxy.server_actions method was missing **query in its signature, preventing callers like python-openstackclient from passing filter arguments like 'changes_since'. Related-Bug: #2138489 Change-Id: I1ef9670664b8497328013c181d8f9b1158d0ad06 Signed-off-by: Johannes Beisiegel --- openstack/compute/v2/_proxy.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 4e395b13d..c7505bd6f 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -2635,17 +2635,23 @@ def get_server_action(self, server_action, server, ignore_missing=True): ignore_missing=ignore_missing, ) - def server_actions(self, server): + def server_actions(self, server, **query): """Return a generator of server actions :param server: The server can be either the ID of a server or a :class:`~openstack.compute.v2.server.Server`. + :param kwargs query: Optional query parameters to be sent to limit + the actions being returned. :returns: A generator of ServerAction objects :rtype: :class:`~openstack.compute.v2.server_action.ServerAction` """ server_id = resource.Resource._get_id(server) - return self._list(_server_action.ServerAction, server_id=server_id) + return self._list( + _server_action.ServerAction, + server_id=server_id, + **query, + ) # ========== Utilities ========== From d4948c6d89a9021ae9b3504eec1715006419d848 Mon Sep 17 00:00:00 2001 From: Pavlo Shchelokovskyy Date: Mon, 19 Jan 2026 16:17:06 +0200 Subject: [PATCH 3798/3836] Mention OS_CLIENT_SECURE_FILE env var in docs Change-Id: I335d52585188a5e5b756cbc53819bc095d4321d0 Signed-off-by: Pavlo Shchelokovskyy --- doc/source/user/config/configuration.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/source/user/config/configuration.rst b/doc/source/user/config/configuration.rst index 3bbba6f96..9a70cb2b6 100644 --- a/doc/source/user/config/configuration.rst +++ b/doc/source/user/config/configuration.rst @@ -164,6 +164,10 @@ the same location rules as `clouds.yaml`. It can contain anything you put in `clouds.yaml` and will take precedence over anything in the `clouds.yaml` file. +You can also set the environment variable `OS_CLIENT_SECURE_FILE` to an +absolute path of a file to look for and that location will be inserted at the +front of the file search list. + .. code-block:: yaml # clouds.yaml From 7056d3b3228939939f2762395610cbc32226c0e4 Mon Sep 17 00:00:00 2001 From: Masahito Muroi Date: Wed, 21 Jan 2026 00:52:10 +0900 Subject: [PATCH 3799/3836] Add microversion to tag operation API request The tag operation by the TagMixin doens't add any microversion information unless client code specifies microversion information when creating resource class. As a result, some service API request fails to run tag operation even though API side CURRENT API version supports the tag operation. e.g. Nova API return 2.100 as CURRENT microapiversion, but the TagMixin uses 2.1, then add_tag_to_server method fails because of using the unsupported version. This commit adds microversion information to the tag operation methods in the TagMixin. It prevents the unsupported error situation. Closes-Bug: #2138658 Change-Id: I7fcc50d04a1e5020794c6c1895f00ce1ac4d353b Signed-off-by: Masahito Muroi --- openstack/common/tag.py | 20 ++-- openstack/resource.py | 3 + openstack/tests/unit/common/test_tag.py | 100 ++++++++++++++++++-- openstack/tests/unit/image/v2/test_image.py | 2 + 4 files changed, 113 insertions(+), 12 deletions(-) diff --git a/openstack/common/tag.py b/openstack/common/tag.py index a1f80a78e..7fa506170 100644 --- a/openstack/common/tag.py +++ b/openstack/common/tag.py @@ -47,7 +47,8 @@ def fetch_tags(self, session: resource.AdapterT) -> ty_ext.Self: """ url = utils.urljoin(self.base_path, self.id, 'tags') session = self._get_session(session) - response = session.get(url) + microversion = self._get_microversion(session) + response = session.get(url, microversion=microversion) exceptions.raise_from_response(response) # NOTE(gtema): since this is a common method # we can't rely on the resource_key, because tags are returned @@ -67,7 +68,10 @@ def set_tags( """ url = utils.urljoin(self.base_path, self.id, 'tags') session = self._get_session(session) - response = session.put(url, json={'tags': tags}) + microversion = self._get_microversion(session) + response = session.put( + url, json={'tags': tags}, microversion=microversion + ) exceptions.raise_from_response(response) self._body.attributes.update({'tags': tags}) return self @@ -79,7 +83,8 @@ def remove_all_tags(self, session: resource.AdapterT) -> ty_ext.Self: """ url = utils.urljoin(self.base_path, self.id, 'tags') session = self._get_session(session) - response = session.delete(url) + microversion = self._get_microversion(session) + response = session.delete(url, microversion=microversion) exceptions.raise_from_response(response) self._body.attributes.update({'tags': []}) return self @@ -94,7 +99,8 @@ def check_tag(self, session: resource.AdapterT, tag: str) -> ty_ext.Self: """ url = utils.urljoin(self.base_path, self.id, 'tags', tag) session = self._get_session(session) - response = session.get(url) + microversion = self._get_microversion(session) + response = session.get(url, microversion=microversion) exceptions.raise_from_response( response, error_message='Tag does not exist' ) @@ -108,7 +114,8 @@ def add_tag(self, session: resource.AdapterT, tag: str) -> ty_ext.Self: """ url = utils.urljoin(self.base_path, self.id, 'tags', tag) session = self._get_session(session) - response = session.put(url) + microversion = self._get_microversion(session) + response = session.put(url, microversion=microversion) exceptions.raise_from_response(response) # we do not want to update tags directly tags = self.tags @@ -124,7 +131,8 @@ def remove_tag(self, session: resource.AdapterT, tag: str) -> ty_ext.Self: """ url = utils.urljoin(self.base_path, self.id, 'tags', tag) session = self._get_session(session) - response = session.delete(url) + microversion = self._get_microversion(session) + response = session.delete(url, microversion=microversion) exceptions.raise_from_response(response) # we do not want to update tags directly tags = self.tags diff --git a/openstack/resource.py b/openstack/resource.py index 5ebb16a68..4757e4e41 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -359,6 +359,9 @@ class ResourceMixinProtocol(ty.Protocol): @classmethod def _get_session(cls, session: AdapterT) -> AdapterT: ... + @classmethod + def _get_microversion(cls, session: adapter.Adapter) -> str | None: ... + class Resource(dict): # TODO(mordred) While this behaves mostly like a munch for the purposes diff --git a/openstack/tests/unit/common/test_tag.py b/openstack/tests/unit/common/test_tag.py index 7d9910f3e..e61d04f06 100644 --- a/openstack/tests/unit/common/test_tag.py +++ b/openstack/tests/unit/common/test_tag.py @@ -50,6 +50,7 @@ class Test(resource.Resource, tag.TagMixin): self.sot = Test.new(id="id", tags=[]) self.sot._prepare_request = mock.Mock(return_value=self.request) self.sot._translate_response = mock.Mock() + self.sot._get_microversion = mock.Mock(return_value=None) self.session = mock.Mock(spec=adapter.Adapter) self.session.get = mock.Mock(return_value=self.response) @@ -78,7 +79,7 @@ def test_fetch_tags(self): # Check the passed resource is returned self.assertEqual(res, result) url = self.base_path + '/' + res.id + '/tags' - sess.get.assert_called_once_with(url) + sess.get.assert_called_once_with(url, microversion=None) def test_set_tags(self): res = self.sot @@ -93,7 +94,9 @@ def test_set_tags(self): # Check the passed resource is returned self.assertEqual(res, result) url = self.base_path + '/' + res.id + '/tags' - sess.put.assert_called_once_with(url, json={'tags': ['blue', 'green']}) + sess.put.assert_called_once_with( + url, json={'tags': ['blue', 'green']}, microversion=None + ) def test_remove_all_tags(self): res = self.sot @@ -108,7 +111,7 @@ def test_remove_all_tags(self): # Check the passed resource is returned self.assertEqual(res, result) url = self.base_path + '/' + res.id + '/tags' - sess.delete.assert_called_once_with(url) + sess.delete.assert_called_once_with(url, microversion=None) def test_remove_single_tag(self): res = self.sot @@ -122,7 +125,7 @@ def test_remove_single_tag(self): # Check the passed resource is returned self.assertEqual(res, result) url = self.base_path + '/' + res.id + '/tags/dummy' - sess.delete.assert_called_once_with(url) + sess.delete.assert_called_once_with(url, microversion=None) def test_check_tag_exists(self): res = self.sot @@ -136,7 +139,7 @@ def test_check_tag_exists(self): # Check the passed resource is returned self.assertEqual(res, result) url = self.base_path + '/' + res.id + '/tags/blue' - sess.get.assert_called_once_with(url) + sess.get.assert_called_once_with(url, microversion=None) def test_check_tag_not_exists(self): res = self.sot @@ -170,7 +173,92 @@ def test_add_tag(self): # Check the passed resource is returned self.assertEqual(res, result) url = self.base_path + '/' + res.id + '/tags/lila' - sess.put.assert_called_once_with(url) + sess.put.assert_called_once_with(url, microversion=None) + + def test_add_tag_with_microversion(self): + res = self.sot + res._get_microversion = mock.Mock(return_value='2.26') + sess = self.session + + res.tags = ['blue', 'green'] + + result = res.add_tag(sess, 'lila') + self.assertEqual(['blue', 'green', 'lila'], res.tags) + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/tags/lila' + sess.put.assert_called_once_with(url, microversion='2.26') + + def test_remove_single_tag_with_microversion(self): + res = self.sot + res._get_microversion = mock.Mock(return_value='2.26') + sess = self.session + + res.tags = ['blue', 'dummy'] + + result = res.remove_tag(sess, 'dummy') + self.assertEqual(['blue'], res.tags) + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/tags/dummy' + sess.delete.assert_called_once_with(url, microversion='2.26') + + def test_fetch_tags_with_microversion(self): + res = self.sot + res._get_microversion = mock.Mock(return_value='2.26') + sess = self.session + + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.links = {} + mock_response.json.return_value = {'tags': ['blue1', 'green1']} + + sess.get.side_effect = [mock_response] + + result = res.fetch_tags(sess) + self.assertEqual(['blue1', 'green1'], res.tags) + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/tags' + sess.get.assert_called_once_with(url, microversion='2.26') + + def test_set_tags_with_microversion(self): + res = self.sot + res._get_microversion = mock.Mock(return_value='2.26') + sess = self.session + + res.tags = ['blue_old', 'green_old'] + + result = res.set_tags(sess, ['blue', 'green']) + self.assertEqual(['blue', 'green'], res.tags) + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/tags' + sess.put.assert_called_once_with( + url, json={'tags': ['blue', 'green']}, microversion='2.26' + ) + + def test_remove_all_tags_with_microversion(self): + res = self.sot + res._get_microversion = mock.Mock(return_value='2.26') + sess = self.session + + res.tags = ['blue_old', 'green_old'] + + result = res.remove_all_tags(sess) + self.assertEqual([], res.tags) + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/tags' + sess.delete.assert_called_once_with(url, microversion='2.26') + + def test_check_tag_with_microversion(self): + res = self.sot + res._get_microversion = mock.Mock(return_value='2.26') + sess = self.session + + sess.get.side_effect = [FakeResponse(None, 202)] + + result = res.check_tag(sess, 'blue') + self.assertEqual([], res.tags) + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/tags/blue' + sess.get.assert_called_once_with(url, microversion='2.26') def test_tagged_resource_always_created_with_empty_tag_list(self): res = self.sot diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index a4f66a475..f548d476b 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -259,6 +259,7 @@ def test_add_tag(self): sot.add_tag(self.sess, tag) self.sess.put.assert_called_with( f'images/IDENTIFIER/tags/{tag}', + microversion=None, ) def test_remove_tag(self): @@ -268,6 +269,7 @@ def test_remove_tag(self): sot.remove_tag(self.sess, tag) self.sess.delete.assert_called_with( f'images/IDENTIFIER/tags/{tag}', + microversion=None, ) def test_import_image(self): From 0edb9fd4a2a239aacebecb2b127338245460fad9 Mon Sep 17 00:00:00 2001 From: Jacob Anders Date: Wed, 14 Jan 2026 01:02:08 +1000 Subject: [PATCH 3800/3836] Add node health field for Redfish health monitoring Add support for the node health status field introduced in Ironic API microversion 1.109. This field exposes the hardware health condition (OK, Warning, Critical) as reported by the BMC via Redfish. Depends-on: https://review.opendev.org/c/openstack/ironic/+/966946 Assisted-by: Claude (Anthropic) version 4.5 Change-Id: I5d41a231b6f13bc552790afbf0e283a904f10acf Signed-off-by: Jacob Anders Partial-Bug: #2133522 --- openstack/baremetal/v1/node.py | 7 +++++-- .../notes/add-node-health-field-bd30892473f3f9f2.yaml | 5 +++++ 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/add-node-health-field-bd30892473f3f9f2.yaml diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index b573a9665..538bf9973 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -104,8 +104,8 @@ class Node(_common.Resource): is_maintenance='maintenance', ) - # The name of the instance associated with this node. - _max_microversion = '1.104' + # Add health field to node object. + _max_microversion = '1.109' # Properties #: The UUID of the allocation associated with this node. Added in API @@ -144,6 +144,9 @@ class Node(_common.Resource): #: Fault type that caused the node to enter maintenance mode. #: Introduced in API microversion 1.42. fault = resource.Body("fault") + #: The health status of the node from the BMC (e.g. 'OK', 'Warning', + #: 'Critical'). Introduced in API microversion 1.109. + health = resource.Body("health") #: The UUID of the node resource. id = resource.Body("uuid", alternate_id=True) #: Information used to customize the deployed image, e.g. size of root diff --git a/releasenotes/notes/add-node-health-field-bd30892473f3f9f2.yaml b/releasenotes/notes/add-node-health-field-bd30892473f3f9f2.yaml new file mode 100644 index 000000000..40e2c812c --- /dev/null +++ b/releasenotes/notes/add-node-health-field-bd30892473f3f9f2.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add support for the node ``health`` field which exposes hardware health + status from the BMC via Redfish. Introduced in API microversion 1.109. From 99bcb43aefb41e654b0fe7cb471fcec7d825e8ac Mon Sep 17 00:00:00 2001 From: Afonne-CID Date: Tue, 13 Jan 2026 19:28:39 +0100 Subject: [PATCH 3801/3836] Add support for filtering by `conductor_groups` Change-Id: I60885738db1ecb7a9525302ec1e22f7f2fbcd6c6 Signed-off-by: Afonne-CID --- openstack/baremetal/v1/_proxy.py | 2 ++ openstack/baremetal/v1/port.py | 2 ++ .../tests/unit/baremetal/v1/test_port.py | 30 +++++++++++++++++++ ..._by_conductor_groups-7e21ddc8eb941536.yaml | 4 +++ 4 files changed, 38 insertions(+) create mode 100644 releasenotes/notes/add_filter_ports_by_conductor_groups-7e21ddc8eb941536.yaml diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 98b589102..c519f913e 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -860,6 +860,8 @@ def ports(self, details=False, **query): * ``address``: Only return ports with the specified physical hardware address, typically a MAC address. + * ``conductor_groups``: Only return ports associated with nodes + in the specified conductor group(s). * ``driver``: Only return those with the specified ``driver``. * ``fields``: A list containing one or more fields to be returned in the response. This may lead to some performance gain diff --git a/openstack/baremetal/v1/port.py b/openstack/baremetal/v1/port.py index f2ee56d17..f0d5c55ac 100644 --- a/openstack/baremetal/v1/port.py +++ b/openstack/baremetal/v1/port.py @@ -30,6 +30,7 @@ class Port(_common.Resource): _query_mapping = resource.QueryParameters( 'address', + 'conductor_groups', 'node', 'portgroup', 'shard', @@ -42,6 +43,7 @@ class Port(_common.Resource): # Query filter by shard added in 1.82 # The name field added in 1.88 # The description field added in 1.97 + # Filter by conductor_groups added in 1.99 # The vendor field added in 1.100 # The category field added in 1.101 _max_microversion = '1.101' diff --git a/openstack/tests/unit/baremetal/v1/test_port.py b/openstack/tests/unit/baremetal/v1/test_port.py index a8ae65e97..3bb24dd48 100644 --- a/openstack/tests/unit/baremetal/v1/test_port.py +++ b/openstack/tests/unit/baremetal/v1/test_port.py @@ -10,6 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. +from unittest import mock + +from keystoneauth1 import adapter + from openstack.baremetal.v1 import port from openstack.tests.unit import base @@ -69,3 +73,29 @@ def test_instantiate(self): self.assertEqual(FAKE['portgroup_uuid'], sot.port_group_id) self.assertEqual(FAKE['pxe_enabled'], sot.is_pxe_enabled) self.assertEqual(FAKE['updated_at'], sot.updated_at) + + def test_list_conductor_groups(self): + self.port = port.Port() + self.session = mock.Mock( + spec=adapter.Adapter, default_microversion=None + ) + + self.session.default_microversion = float(self.port._max_microversion) + self.session.get.return_value.status_code = 200 + self.session.get.return_value.json.return_value = {'ports': []} + + result = list( + self.port.list( + self.session, + details=False, + conductor_groups=['group1', 'group2'], + allow_unknown_params=True, + ) + ) + self.assertEqual(0, len(result)) + self.session.get.assert_called_once_with( + '/ports', + headers={'Accept': 'application/json'}, + params={'conductor_groups': ['group1', 'group2']}, + microversion=float(self.port._max_microversion), + ) diff --git a/releasenotes/notes/add_filter_ports_by_conductor_groups-7e21ddc8eb941536.yaml b/releasenotes/notes/add_filter_ports_by_conductor_groups-7e21ddc8eb941536.yaml new file mode 100644 index 000000000..7c4faa5ab --- /dev/null +++ b/releasenotes/notes/add_filter_ports_by_conductor_groups-7e21ddc8eb941536.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Adds support for filtering baremetal ports by ``conductor_groups``. From b107b01aacdde4227489cc6433d5c4dfed464468 Mon Sep 17 00:00:00 2001 From: Juan Larriba Date: Wed, 21 Jan 2026 12:06:11 +0100 Subject: [PATCH 3802/3836] Add _query_mapping to Share resource for query parameter support The Share resource in the shared file system service was missing the _query_mapping attribute, causing query parameters like all_tenants to be silently ignored when listing shares. This prevented admin users from listing shares across all projects and affected telemetry systems like Ceilometer that need to poll all shares. This change adds _query_mapping with the standard Manila API query parameters including all_tenants, project_id, status, name, and others. Change-Id: I1e2e52801d24eca3638f381d28b8bebf80f2f12a Signed-off-by: Juan Larriba --- openstack/shared_file_system/v2/share.py | 21 ++++++++++++++++ .../unit/shared_file_system/v2/test_share.py | 25 +++++++++++++++++++ ...re-query-mapping-fix-7f3c2d8e9a1b5c4f.yaml | 9 +++++++ 3 files changed, 55 insertions(+) create mode 100644 releasenotes/notes/add-share-query-mapping-fix-7f3c2d8e9a1b5c4f.yaml diff --git a/openstack/shared_file_system/v2/share.py b/openstack/shared_file_system/v2/share.py index 5b4a3e239..c395144fa 100644 --- a/openstack/shared_file_system/v2/share.py +++ b/openstack/shared_file_system/v2/share.py @@ -21,6 +21,27 @@ class Share(resource.Resource, metadata.MetadataMixin): resources_key = "shares" base_path = "/shares" + _query_mapping = resource.QueryParameters( + "project_id", + "name", + "status", + "share_server_id", + "metadata", + "share_type_id", + "snapshot_id", + "host", + "share_network_id", + "is_public", + "share_group_id", + "export_location_id", + "export_location_path", + "limit", + "offset", + "sort_key", + "sort_dir", + all_projects="all_tenants", + ) + # capabilities allow_create = True allow_fetch = True diff --git a/openstack/tests/unit/shared_file_system/v2/test_share.py b/openstack/tests/unit/shared_file_system/v2/test_share.py index 9fd4c6eea..08b830fd7 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_share.py +++ b/openstack/tests/unit/shared_file_system/v2/test_share.py @@ -64,6 +64,31 @@ def test_basic(self): self.assertTrue(shares_resource.allow_commit) self.assertTrue(shares_resource.allow_delete) + self.assertDictEqual( + { + "limit": "limit", + "marker": "marker", + "project_id": "project_id", + "name": "name", + "status": "status", + "share_server_id": "share_server_id", + "metadata": "metadata", + "share_type_id": "share_type_id", + "snapshot_id": "snapshot_id", + "host": "host", + "share_network_id": "share_network_id", + "is_public": "is_public", + "share_group_id": "share_group_id", + "export_location_id": "export_location_id", + "export_location_path": "export_location_path", + "offset": "offset", + "sort_key": "sort_key", + "sort_dir": "sort_dir", + "all_projects": "all_tenants", + }, + shares_resource._query_mapping._mapping, + ) + def test_make_shares(self): shares_resource = share.Share(**EXAMPLE) self.assertEqual(EXAMPLE['id'], shares_resource.id) diff --git a/releasenotes/notes/add-share-query-mapping-fix-7f3c2d8e9a1b5c4f.yaml b/releasenotes/notes/add-share-query-mapping-fix-7f3c2d8e9a1b5c4f.yaml new file mode 100644 index 000000000..2100e1d8c --- /dev/null +++ b/releasenotes/notes/add-share-query-mapping-fix-7f3c2d8e9a1b5c4f.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + Fixed the ``Share`` resource in the shared file system service to properly + support query parameters when listing shares. Previously, query parameters + such as ``all_tenants`` were silently ignored because the ``_query_mapping`` + attribute was missing. This affected admin users trying to list shares + across all projects, as well as filtering by status, name, and other + attributes. From 365296d4438fea1e8a5ae52ee64003981bfa168c Mon Sep 17 00:00:00 2001 From: Cyril Roelandt Date: Tue, 27 Jan 2026 04:14:35 +0100 Subject: [PATCH 3803/3836] upload_image: improve testing As we are thinking about un-deprecating this method and change its signature, we need to make sure our tests actually test multiple valid ways of calling it. Assisted-by: Claude Sonnet 4.5 Change-Id: I19dedb20617f629ce414ad8bb275a4b2b7b74dc5 Signed-off-by: Cyril Roelandt --- openstack/tests/unit/image/v2/test_proxy.py | 73 +++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 2da4d0972..65530a73a 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -460,6 +460,79 @@ def test_image_upload(self): created_image.upload.assert_called_with(self.proxy) self.assertEqual(rv, created_image) + def test_image_upload_positional_args(self): + """Test upload_image with positional arguments only""" + created_image = mock.Mock(spec=_image.Image(id="id")) + + self.proxy._create = mock.Mock() + self.proxy._create.return_value = created_image + + # Call with positional args for container_format, disk_format, data + rv = self.proxy.upload_image("bare", "qcow2", "imagedata") + + self.proxy._create.assert_called_with( + _image.Image, + container_format="bare", + disk_format="qcow2", + ) + created_image.upload.assert_called_with(self.proxy) + self.assertEqual(rv, created_image) + self.assertEqual(created_image.data, "imagedata") + + def test_image_upload_keyword_args(self): + """Test upload_image with keyword arguments only""" + created_image = mock.Mock(spec=_image.Image(id="id")) + + self.proxy._create = mock.Mock() + self.proxy._create.return_value = created_image + + rv = self.proxy.upload_image( + container_format="bare", + disk_format="qcow2", + data="imagedata", + name="test-image", + visibility="public", + ) + + self.proxy._create.assert_called_with( + _image.Image, + container_format="bare", + disk_format="qcow2", + name="test-image", + visibility="public", + ) + created_image.upload.assert_called_with(self.proxy) + self.assertEqual(rv, created_image) + self.assertEqual(created_image.data, "imagedata") + + def test_image_upload_mixed_args(self): + """Test upload_image with both positional and keyword arguments""" + created_image = mock.Mock(spec=_image.Image(id="id")) + + self.proxy._create = mock.Mock() + self.proxy._create.return_value = created_image + + # Positional: container_format, disk_format + # Keyword: data, name, tags + rv = self.proxy.upload_image( + "bare", + "qcow2", + data="imagedata", + name="test-image", + tags=["tag1", "tag2"], + ) + + self.proxy._create.assert_called_with( + _image.Image, + container_format="bare", + disk_format="qcow2", + name="test-image", + tags=["tag1", "tag2"], + ) + created_image.upload.assert_called_with(self.proxy) + self.assertEqual(rv, created_image) + self.assertEqual(created_image.data, "imagedata") + def test_image_download(self): original_image = _image.Image(**EXAMPLE) self._verify( From 34d242c5a028bebd1ba836f42cc523df362ecab2 Mon Sep 17 00:00:00 2001 From: Myles Penner Date: Mon, 19 Jan 2026 10:31:46 -0800 Subject: [PATCH 3804/3836] Postpone annotation evaluation for Python 3.14 Python 3.14 introspection used by unittest.mock autospec evaluates annotations more eagerly. openstack.resource triggers a TypeError ("classmethod object is not subscriptable") during that evaluation, causing unit tests to fail. Also modernize type hints after enabling future annotations. Closes-bug: 2138664 Change-Id: I350fd3fedc8a048c466771b7c3b44731b8a12a90 Signed-off-by: Myles Penner --- openstack/resource.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openstack/resource.py b/openstack/resource.py index 5ebb16a68..576e3232c 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -32,6 +32,8 @@ class that represent a remote resource. The attributes that and then returned to the caller. """ +from __future__ import annotations + import builtins import collections import collections.abc @@ -905,7 +907,7 @@ def _alternate_id(cls): return "" @staticmethod - def _get_id(value: ty.Union['Resource', str]) -> str: + def _get_id(value: Resource | str) -> str: """If a value is a Resource, return the canonical ID This will return either the value specified by `id` or @@ -954,7 +956,7 @@ def _from_munch( cls, obj: dict[str, ty.Union], synchronized: bool = True, - connection: ty.Optional['connection.Connection'] = None, + connection: connection.Connection | None = None, ) -> ty_ext.Self: """Create an instance from a ``utils.Munch`` object. From c7b718254d17c2a6024cf5df327e8f948d0c30e3 Mon Sep 17 00:00:00 2001 From: Johannes Beisiegel Date: Mon, 19 Jan 2026 12:38:45 +0100 Subject: [PATCH 3805/3836] Add missing query_mappings to volume attachment This fixes ability for python-openstackclient to filter and list attachments correctly. Related-Bug: #2133168 Change-Id: I1c202e2d231de1a1421b3b097d3738f47f3b91d7 Signed-off-by: Johannes Beisiegel --- openstack/block_storage/v3/attachment.py | 14 +++++++++++++- .../tests/unit/block_storage/v3/test_attachment.py | 8 +++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/openstack/block_storage/v3/attachment.py b/openstack/block_storage/v3/attachment.py index 7e49a745c..263bd2b9f 100644 --- a/openstack/block_storage/v3/attachment.py +++ b/openstack/block_storage/v3/attachment.py @@ -22,6 +22,15 @@ class Attachment(resource.Resource): resources_key = "attachments" base_path = "/attachments" + _query_mapping = resource.QueryParameters( + 'id', + 'status', + 'project_id', + 'volume_id', + 'instance_id', + all_projects='all_tenants', + ) + # capabilities allow_create = True allow_delete = True @@ -29,7 +38,7 @@ class Attachment(resource.Resource): allow_list = True allow_fetch = True - _max_microversion = "3.54" + _max_microversion = "3.71" # Properties #: The ID of the attachment. @@ -100,3 +109,6 @@ def _prepare_request_body( if prepend_key and self.resource_key is not None: body = {self.resource_key: body} return body + + +AttachmentDetail = Attachment diff --git a/openstack/tests/unit/block_storage/v3/test_attachment.py b/openstack/tests/unit/block_storage/v3/test_attachment.py index 0d8e7467b..2f6639c99 100644 --- a/openstack/tests/unit/block_storage/v3/test_attachment.py +++ b/openstack/tests/unit/block_storage/v3/test_attachment.py @@ -95,6 +95,12 @@ def test_basic(self): self.assertDictEqual( { + "id": "id", + "status": "status", + "project_id": "project_id", + "volume_id": "volume_id", + "instance_id": "instance_id", + "all_projects": "all_tenants", "limit": "limit", "marker": "marker", }, @@ -142,7 +148,7 @@ def test_create_no_mode_no_instance_id(self, mock_translate, mock_mv): 'mode': 'rw', }, ) - self.sess.default_microversion = "3.54" + self.sess.default_microversion = "3.71" @mock.patch( 'openstack.utils.supports_microversion', From 0db10b7fb0d460440447cdb01268f61a49934ebb Mon Sep 17 00:00:00 2001 From: Brian Haley Date: Sat, 7 Feb 2026 14:35:54 -0500 Subject: [PATCH 3806/3836] Use project_id key in neutron API calls Start using project_id key in neutron API calls, fix tests accordingly. Also had to set flake8-import-order version to newer than 0.19.0 to fix an issue with python setuptools removing pkg_resources. Blueprint: https://blueprints.launchpad.net/neutron/+spec/keystone-v3 Change-Id: I02e4c8cf88cdba5e8b60d0ba55a735786365b60d Signed-off-by: Brian Haley --- .pre-commit-config.yaml | 2 +- openstack/tests/functional/cloud/test_router.py | 4 ++-- openstack/tests/unit/cloud/test_port.py | 8 ++++---- openstack/tests/unit/cloud/test_router.py | 14 ++++++-------- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9fdd03479..7616a2bfd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: hooks: - id: hacking additional_dependencies: - - flake8-import-order~=0.18.2 + - flake8-import-order>=0.19.0 exclude: '^(doc|releasenotes|tools)/.*$' - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.17.1 diff --git a/openstack/tests/functional/cloud/test_router.py b/openstack/tests/functional/cloud/test_router.py index a805df895..e547f94ce 100644 --- a/openstack/tests/functional/cloud/test_router.py +++ b/openstack/tests/functional/cloud/test_router.py @@ -143,7 +143,7 @@ def test_create_router_project(self): self.assertEqual(router_name, router['name']) self.assertEqual('ACTIVE', router['status']) - self.assertEqual(proj_id, router['tenant_id']) + self.assertEqual(proj_id, router['project_id']) self.assertEqual(net1['id'], ext_gw_info['network_id']) self.assertTrue(ext_gw_info['enable_snat']) @@ -228,7 +228,7 @@ def test_add_remove_router_interface(self): # Test return values *after* the interface is detached so the # resources we've created can be cleaned up if these asserts fail. self.assertIsNotNone(iface) - for key in ('id', 'subnet_id', 'port_id', 'tenant_id'): + for key in ('id', 'subnet_id', 'port_id', 'project_id'): self.assertIn(key, iface) self.assertEqual(router['id'], iface['id']) self.assertEqual(sub['id'], iface['subnet_id']) diff --git a/openstack/tests/unit/cloud/test_port.py b/openstack/tests/unit/cloud/test_port.py index da82cb2d0..7be6f693f 100644 --- a/openstack/tests/unit/cloud/test_port.py +++ b/openstack/tests/unit/cloud/test_port.py @@ -33,7 +33,7 @@ class TestPort(base.TestCase): 'allowed_address_pairs': [], 'admin_state_up': True, 'network_id': 'test-net-id', - 'tenant_id': 'test-tenant-id', + 'project_id': 'test-project-id', 'binding:vif_details': {}, 'binding:vnic_type': 'normal', 'binding:vif_type': 'unbound', @@ -58,7 +58,7 @@ class TestPort(base.TestCase): 'allowed_address_pairs': [], 'admin_state_up': True, 'network_id': 'test-net-id', - 'tenant_id': 'test-tenant-id', + 'project_id': 'test-project-id', 'binding:vif_details': {}, 'extra_dhcp_opts': [], 'binding:vnic_type': 'normal', @@ -84,7 +84,7 @@ class TestPort(base.TestCase): 'allowed_address_pairs': [], 'admin_state_up': True, 'network_id': '70c1db1f-b701-45bd-96e0-a313ee3430b3', - 'tenant_id': '', + 'project_id': '', 'extra_dhcp_opts': [], 'binding:vif_details': { 'port_filter': True, @@ -112,7 +112,7 @@ class TestPort(base.TestCase): 'allowed_address_pairs': [], 'admin_state_up': True, 'network_id': 'f27aa545-cbdd-4907-b0c6-c9e8b039dcc2', - 'tenant_id': 'd397de8a63f341818f198abb0966f6f3', + 'project_id': 'd397de8a63f341818f198abb0966f6f3', 'extra_dhcp_opts': [], 'binding:vif_details': { 'port_filter': True, diff --git a/openstack/tests/unit/cloud/test_router.py b/openstack/tests/unit/cloud/test_router.py index 0d3064561..cbe134dac 100644 --- a/openstack/tests/unit/cloud/test_router.py +++ b/openstack/tests/unit/cloud/test_router.py @@ -42,13 +42,12 @@ class TestRouter(base.TestCase): 'project_id': '861808a93da0484ea1767967c4df8a23', 'routes': [{"destination": "179.24.1.0/24", "nexthop": "172.24.3.99"}], 'status': 'ACTIVE', - 'tenant_id': '861808a93da0484ea1767967c4df8a23', } mock_router_interface_rep = { 'network_id': '53aee281-b06d-47fc-9e1a-37f045182b8e', 'subnet_id': '1f1696eb-7f47-47f6-835c-4889bff88604', - 'tenant_id': '861808a93da0484ea1767967c4df8a23', + 'project_id': '861808a93da0484ea1767967c4df8a23', 'subnet_ids': [subnet_id], 'port_id': '23999891-78b3-4a6b-818d-d1b713f67848', 'id': '57076620-dcfb-42ed-8ad6-79ccb4a79ed2', @@ -166,11 +165,10 @@ def test_create_router(self): self._compare_routers(self.mock_router_rep, new_router) self.assert_calls() - def test_create_router_specific_tenant(self): - new_router_tenant_id = "project_id_value" + def test_create_router_specific_project(self): + new_router_project_id = "project_id_value" mock_router_rep = copy.copy(self.mock_router_rep) - mock_router_rep['tenant_id'] = new_router_tenant_id - mock_router_rep['project_id'] = new_router_tenant_id + mock_router_rep['project_id'] = new_router_project_id self.register_uris( [ dict( @@ -184,7 +182,7 @@ def test_create_router_specific_tenant(self): 'router': { 'name': self.router_name, 'admin_state_up': True, - 'project_id': new_router_tenant_id, + 'project_id': new_router_project_id, } } ), @@ -193,7 +191,7 @@ def test_create_router_specific_tenant(self): ) self.cloud.create_router( - self.router_name, project_id=new_router_tenant_id + self.router_name, project_id=new_router_project_id ) self.assert_calls() From df37bba12fc726a3c901116ecfd19e8e42959c6f Mon Sep 17 00:00:00 2001 From: ArtofBugs Date: Tue, 10 Feb 2026 15:11:22 -0800 Subject: [PATCH 3807/3836] Identity: Deprecate find_mapping() ID and name are the same thing for mappings, so this isn't particularly useful. Change-Id: Idc6315460881052a70e761c39812c6716fd92017 Signed-off-by: 0weng --- openstack/identity/v3/_proxy.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 07a23297e..036241ccc 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -2146,6 +2146,11 @@ def find_mapping(self, name_or_id, ignore_missing=True): attempting to find a nonexistent resource. :returns: One :class:`~openstack.identity.v3.mapping.Mapping` or None """ + warnings.warn( + "find_mapping is deprecated and will be removed in a future " + "release; please use get_mapping instead.", + os_warnings.RemovedInSDK60Warning, + ) return self._find( _mapping.Mapping, name_or_id, ignore_missing=ignore_missing ) From c8ea34e5cd354dfcdb24150cece5aa09853cb03b Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 11 Feb 2026 11:05:08 +0000 Subject: [PATCH 3808/3836] pre-commit: Bump versions The bump of ruff brings in some new fixes. Change-Id: I239f0e601ffc9057524684ae4e6691cb5ba3fa5d Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 8 ++++---- openstack/config/loader.py | 2 +- openstack/tests/unit/config/test_cloud_config.py | 14 +++++++------- openstack/tests/unit/config/test_loader.py | 14 ++++++-------- openstack/tests/unit/image/v2/test_proxy.py | 2 +- 5 files changed, 19 insertions(+), 21 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7616a2bfd..5fdf6e375 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,20 +19,20 @@ repos: hooks: - id: doc8 - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.12 + rev: v0.15.0 hooks: - id: ruff-check args: ['--fix', '--unsafe-fixes'] - id: ruff-format - repo: https://opendev.org/openstack/hacking - rev: 7.0.0 + rev: 8.0.0 hooks: - id: hacking additional_dependencies: - - flake8-import-order>=0.19.0 + - flake8-import-order~=0.19.2 exclude: '^(doc|releasenotes|tools)/.*$' - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.17.1 + rev: v1.19.1 hooks: - id: mypy additional_dependencies: diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 5b61084d7..48f529ffc 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -658,7 +658,7 @@ def _expand_vendor_profile( os_warnings.OpenStackDeprecationWarning, ) - vendor_filename, vendor_file = self._load_vendor_file() + _, vendor_file = self._load_vendor_file() if ( vendor_file and 'public-clouds' in vendor_file diff --git a/openstack/tests/unit/config/test_cloud_config.py b/openstack/tests/unit/config/test_cloud_config.py index 855a30894..9b078ead2 100644 --- a/openstack/tests/unit/config/test_cloud_config.py +++ b/openstack/tests/unit/config/test_cloud_config.py @@ -102,17 +102,17 @@ def test_verify(self): config_dict['verify'] = False cc = cloud_region.CloudRegion("test1", "region-xx", config_dict) - (verify, cert) = cc.get_requests_verify_args() + verify, _ = cc.get_requests_verify_args() self.assertFalse(verify) config_dict['verify'] = True cc = cloud_region.CloudRegion("test1", "region-xx", config_dict) - (verify, cert) = cc.get_requests_verify_args() + verify, _ = cc.get_requests_verify_args() self.assertTrue(verify) config_dict['insecure'] = True cc = cloud_region.CloudRegion("test1", "region-xx", config_dict) - (verify, cert) = cc.get_requests_verify_args() + verify, _ = cc.get_requests_verify_args() self.assertFalse(verify) def test_verify_cacert(self): @@ -121,17 +121,17 @@ def test_verify_cacert(self): config_dict['verify'] = False cc = cloud_region.CloudRegion("test1", "region-xx", config_dict) - (verify, cert) = cc.get_requests_verify_args() + verify, _ = cc.get_requests_verify_args() self.assertFalse(verify) config_dict['verify'] = True cc = cloud_region.CloudRegion("test1", "region-xx", config_dict) - (verify, cert) = cc.get_requests_verify_args() + verify, _ = cc.get_requests_verify_args() self.assertEqual("certfile", verify) config_dict['insecure'] = True cc = cloud_region.CloudRegion("test1", "region-xx", config_dict) - (verify, cert) = cc.get_requests_verify_args() + verify, _ = cc.get_requests_verify_args() self.assertEqual(False, verify) def test_cert_with_key(self): @@ -143,7 +143,7 @@ def test_cert_with_key(self): config_dict['key'] = 'key' cc = cloud_region.CloudRegion("test1", "region-xx", config_dict) - (verify, cert) = cc.get_requests_verify_args() + _, cert = cc.get_requests_verify_args() self.assertEqual(("cert", "key"), cert) def test_ipv6(self): diff --git a/openstack/tests/unit/config/test_loader.py b/openstack/tests/unit/config/test_loader.py index 30890c5b6..710921cfc 100644 --- a/openstack/tests/unit/config/test_loader.py +++ b/openstack/tests/unit/config/test_loader.py @@ -63,7 +63,7 @@ def test_base_load_yaml_json_file(self): fp.write(value) tested_files.append(fn) - path, result = loader.OpenStackConfig()._load_yaml_json_file( + path, _ = loader.OpenStackConfig()._load_yaml_json_file( tested_files ) # NOTE(hberaud): Prefer to test path rather than file because @@ -82,7 +82,7 @@ def test__load_yaml_json_file_without_json(self): fp.write(value) tested_files.append(fn) - path, result = loader.OpenStackConfig()._load_yaml_json_file( + path, _ = loader.OpenStackConfig()._load_yaml_json_file( tested_files ) # NOTE(hberaud): Prefer to test path rather than file because @@ -98,7 +98,7 @@ def test__load_yaml_json_file_without_json_yaml(self): fp.write(FILES['txt']) tested_files.append(fn) - path, result = loader.OpenStackConfig()._load_yaml_json_file( + path, _ = loader.OpenStackConfig()._load_yaml_json_file( tested_files ) self.assertEqual(fn, path) @@ -109,10 +109,10 @@ def test__load_yaml_json_file_without_perm(self): fn = os.path.join(tmpdir, 'file.txt') with open(fn, 'w+') as fp: fp.write(FILES['txt']) - os.chmod(fn, 222) + os.chmod(fn, 0o222) tested_files.append(fn) - path, result = loader.OpenStackConfig()._load_yaml_json_file( + path, _ = loader.OpenStackConfig()._load_yaml_json_file( tested_files ) self.assertEqual(None, path) @@ -122,9 +122,7 @@ def test__load_yaml_json_file_nonexisting(self): fn = os.path.join('/fake', 'file.txt') tested_files.append(fn) - path, result = loader.OpenStackConfig()._load_yaml_json_file( - tested_files - ) + path, _ = loader.OpenStackConfig()._load_yaml_json_file(tested_files) self.assertEqual(None, path) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 65530a73a..7646cfcaa 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -348,7 +348,7 @@ def test_image_create_protected(self): **properties, ) - args, kwargs = self.proxy._create.call_args + _, kwargs = self.proxy._create.call_args self.assertEqual(kwargs["is_protected"], True) def test_image_create_with_stores(self): From cfd3165164650f134e7ef015535e650a8c1e5d9b Mon Sep 17 00:00:00 2001 From: ArtofBugs Date: Thu, 12 Feb 2026 13:11:27 -0800 Subject: [PATCH 3809/3836] Identity: Deprecate find_identity_provider ID and name are the same thing for identity providers, so this isn't particularly useful. Change-Id: Ib3293e22de373c50a45371e499b59c427e8fda9e Signed-off-by: 0weng --- openstack/identity/v3/_proxy.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index 036241ccc..eeebf0f7e 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -2240,6 +2240,11 @@ def find_identity_provider(self, name_or_id, ignore_missing=True): :rtype: :class:`~openstack.identity.v3.identity_provider.IdentityProvider` """ + warnings.warn( + "find_identity_provider is deprecated and will be removed in a " + "future release; please use get_identity_provider instead.", + os_warnings.RemovedInSDK60Warning, + ) return self._find( _identity_provider.IdentityProvider, name_or_id, From 887b1d6db45ee4f61d4f7bc32e3f03336467a942 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 13 Feb 2026 18:29:45 +0000 Subject: [PATCH 3810/3836] Deprecate support for InfluxDB This has not been maintained and it is not apparent anyone is using it. Deprecate it. If anyone shows us to fix things we can undeprecate it. Change-Id: Idcdf5958df7bf5230fc46852f37393906fca9227 Signed-off-by: Stephen Finucane --- openstack/config/cloud_region.py | 25 ++++++++++++++++++- openstack/config/loader.py | 10 ++++++++ ...ate-influxdb-support-aca3c74f7dc25572.yaml | 6 +++++ 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/deprecate-influxdb-support-aca3c74f7dc25572.yaml diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 45b84615b..2aa26c921 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -367,6 +367,21 @@ def __init__( self._statsd_client = None self._influxdb_config = influxdb_config self._influxdb_client = None + + if influxdb_config is not None: + # NOTE(stephenfin): If you are a user and care about InfluxDB, + # please propose patches to migrate this to the influxdb3-python + # library [1]. Any migration should include tests. + # + # [1] https://github.com/InfluxCommunity/influxdb3-python + warnings.warn( + 'Support for InfluxDB requires the influxdb library which ' + 'only supports InfluxDB 1.x and is deprecated. As a result, ' + 'influxdb is also deprecated and will be removed in a future ' + 'release.', + os_warnings.RemovedInSDK60Warning, + ) + self._collector_registry = collector_registry self._service_type_manager = os_service_types.ServiceTypes() @@ -1430,10 +1445,18 @@ def get_disabled_reason(self, service_type: str) -> str | None: def get_influxdb_client( self, ) -> ty.Optional['influxdb_client.InfluxDBClient']: - # TODO(stephenfin): We could do with a typed dict here. influx_args: dict[str, ty.Any] = {} if not self._influxdb_config: return None + + warnings.warn( + 'Support for InfluxDB requires the influxdb library which ' + 'only supports InfluxDB 1.x and is deprecated. As a result, ' + 'influxdb is also deprecated and will be removed in a future ' + 'release.', + os_warnings.RemovedInSDK60Warning, + ) + use_udp = bool(self._influxdb_config.get('use_udp', False)) port = self._influxdb_config.get('port') if use_udp: diff --git a/openstack/config/loader.py b/openstack/config/loader.py index 48f529ffc..a4aad7f91 100644 --- a/openstack/config/loader.py +++ b/openstack/config/loader.py @@ -373,6 +373,16 @@ def __init__( influxdb_config.update(influxdb_cfg) if influxdb_config: + # NOTE(stephenfin): defer the warning to here so we catch config in + # both clouds.yaml and directly passed in + warnings.warn( + 'Support for InfluxDB requires the influxdb library which ' + 'only supports InfluxDB 1.x and is deprecated. As a result, ' + 'influxdb is also deprecated and will be removed in a future ' + 'release.', + os_warnings.RemovedInSDK60Warning, + ) + config = {} if 'use_udp' in influxdb_config: use_udp = influxdb_config['use_udp'] diff --git a/releasenotes/notes/deprecate-influxdb-support-aca3c74f7dc25572.yaml b/releasenotes/notes/deprecate-influxdb-support-aca3c74f7dc25572.yaml new file mode 100644 index 000000000..63523760e --- /dev/null +++ b/releasenotes/notes/deprecate-influxdb-support-aca3c74f7dc25572.yaml @@ -0,0 +1,6 @@ +--- +deprecations: + - | + Support for reporting metrics to InfluxDB has been deprecated for removal. + The implementation relied on an EOL Python library and only supported + InfluxDB v1. From fa89a618490ff67341569d295cd886c9285e8b8e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 13 Feb 2026 18:12:01 +0000 Subject: [PATCH 3811/3836] trivial: Align tox indentation with other SDK projects This makes sharing snippets between projects easier. Change-Id: Iac210b1165482300065da4fa3758ba1b820b1a5a Signed-off-by: Stephen Finucane --- tox.ini | 138 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 69 insertions(+), 69 deletions(-) diff --git a/tox.ini b/tox.ini index ca14f55af..8d91a4779 100644 --- a/tox.ini +++ b/tox.ini @@ -4,141 +4,141 @@ envlist = pep8,py3 [testenv] description = - Run unit tests. + Run unit tests. passenv = - OS_* - OPENSTACKSDK_* + OS_* + OPENSTACKSDK_* setenv = - LANG=en_US.UTF-8 - LANGUAGE=en_US:en - LC_ALL=C - OS_LOG_CAPTURE={env:OS_LOG_CAPTURE:true} - OS_STDOUT_CAPTURE={env:OS_STDOUT_CAPTURE:true} - OS_STDERR_CAPTURE={env:OS_STDERR_CAPTURE:true} + LANG=en_US.UTF-8 + LANGUAGE=en_US:en + LC_ALL=C + OS_LOG_CAPTURE={env:OS_LOG_CAPTURE:true} + OS_STDOUT_CAPTURE={env:OS_STDOUT_CAPTURE:true} + OS_STDERR_CAPTURE={env:OS_STDERR_CAPTURE:true} deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} - -r{toxinidir}/test-requirements.txt - -r{toxinidir}/requirements.txt + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -r{toxinidir}/test-requirements.txt + -r{toxinidir}/requirements.txt commands = - stestr run {posargs} - stestr slowest + stestr run {posargs} + stestr slowest [testenv:functional{,-py39,-py310,-py311,-py312}] description = - Run functional tests. + Run functional tests. # Some jobs (especially heat) takes longer, therefore increase default timeout # This timeout should not be smaller, than the longest individual timeout setenv = - {[testenv]setenv} - OS_TEST_TIMEOUT=600 - OPENSTACKSDK_FUNC_TEST_TIMEOUT_LOAD_BALANCER=600 - OPENSTACKSDK_EXAMPLE_CONFIG_KEY=functional - OPENSTACKSDK_FUNC_TEST_TIMEOUT_PROJECT_CLEANUP=1200 + {[testenv]setenv} + OS_TEST_TIMEOUT=600 + OPENSTACKSDK_FUNC_TEST_TIMEOUT_LOAD_BALANCER=600 + OPENSTACKSDK_EXAMPLE_CONFIG_KEY=functional + OPENSTACKSDK_FUNC_TEST_TIMEOUT_PROJECT_CLEANUP=1200 commands = - stestr --test-path ./openstack/tests/functional/{env:OPENSTACKSDK_TESTS_SUBDIR:} run --serial {posargs} - stestr slowest + stestr --test-path ./openstack/tests/functional/{env:OPENSTACKSDK_TESTS_SUBDIR:} run --serial {posargs} + stestr slowest # Acceptance tests are the ones running on real clouds [testenv:acceptance-regular-user] description = - Run acceptance tests. + Run acceptance tests. # This env intends to test functions of a regular user without admin privileges # Some jobs (especially heat) takes longer, therefore increase default timeout # This timeout should not be smaller, than the longest individual timeout setenv = - {[testenv]setenv} - OS_TEST_TIMEOUT=600 - OPENSTACKSDK_FUNC_TEST_TIMEOUT_LOAD_BALANCER=600 - # OPENSTACKSDK_DEMO_CLOUD and OS_CLOUD should point to the cloud to test - # Othee clouds are explicitly set empty to let tests detect absense - OPENSTACKSDK_DEMO_CLOUD_ALT= - OPENSTACKSDK_OPERATOR_CLOUD= + {[testenv]setenv} + OS_TEST_TIMEOUT=600 + OPENSTACKSDK_FUNC_TEST_TIMEOUT_LOAD_BALANCER=600 + # OPENSTACKSDK_DEMO_CLOUD and OS_CLOUD should point to the cloud to test + # Othee clouds are explicitly set empty to let tests detect absense + OPENSTACKSDK_DEMO_CLOUD_ALT= + OPENSTACKSDK_OPERATOR_CLOUD= commands = - stestr --test-path ./openstack/tests/functional/{env:OPENSTACKSDK_TESTS_SUBDIR:} run --serial {posargs} --include-list include-acceptance-regular-user.txt - stestr slowest + stestr --test-path ./openstack/tests/functional/{env:OPENSTACKSDK_TESTS_SUBDIR:} run --serial {posargs} --include-list include-acceptance-regular-user.txt + stestr slowest [testenv:pep8] description = - Run style checks. + Run style checks. skip_install = true deps = - pre-commit + pre-commit commands = - pre-commit run --all-files --show-diff-on-failure + pre-commit run --all-files --show-diff-on-failure [testenv:venv] description = - Run specified command in a virtual environment with all dependencies installed. + Run specified command in a virtual environment with all dependencies installed. deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} - -r{toxinidir}/test-requirements.txt - -r{toxinidir}/requirements.txt - -r{toxinidir}/doc/requirements.txt + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -r{toxinidir}/test-requirements.txt + -r{toxinidir}/requirements.txt + -r{toxinidir}/doc/requirements.txt commands = {posargs} [testenv:debug] description = - Run specified tests through oslo_debug_helper, which allows use of pdb. + Run specified tests through oslo_debug_helper, which allows use of pdb. # allow 1 year, or 31536000 seconds, to debug a test before it times out setenv = - OS_TEST_TIMEOUT=31536000 + OS_TEST_TIMEOUT=31536000 allowlist_externals = find commands = - find . -type f -name "*.pyc" -delete - oslo_debug_helper -t openstack/tests {posargs} + find . -type f -name "*.pyc" -delete + oslo_debug_helper -t openstack/tests {posargs} [testenv:cover] description = - Run unit tests and generate coverage report. + Run unit tests and generate coverage report. setenv = - {[testenv]setenv} - PYTHON=coverage run --source openstack --parallel-mode + {[testenv]setenv} + PYTHON=coverage run --source openstack --parallel-mode commands = - stestr run {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml + stestr run {posargs} + coverage combine + coverage html -d cover + coverage xml -o cover/coverage.xml [testenv:ansible] description = - Run ansible tests. + Run ansible tests. # Need to pass some env vars for the Ansible playbooks passenv = - HOME - USER - ANSIBLE_VAR_* + HOME + USER + ANSIBLE_VAR_* deps = - {[testenv]deps} - ansible + {[testenv]deps} + ansible commands = {toxinidir}/extras/run-ansible-tests.sh -e {envdir} {posargs} [testenv:docs] description = - Build documentation in HTML format. + Build documentation in HTML format. deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} - -r{toxinidir}/doc/requirements.txt + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -r{toxinidir}/doc/requirements.txt commands = - sphinx-build -W --keep-going -b html -j auto doc/source/ doc/build/html + sphinx-build -W --keep-going -b html -j auto doc/source/ doc/build/html [testenv:pdf-docs] description = - Build documentation in PDF format. + Build documentation in PDF format. deps = {[testenv:docs]deps} allowlist_externals = - make + make commands = - sphinx-build -W --keep-going -b latex -j auto doc/source/ doc/build/pdf - make -C doc/build/pdf + sphinx-build -W --keep-going -b latex -j auto doc/source/ doc/build/pdf + make -C doc/build/pdf [testenv:releasenotes] description = - Build release note documentation in HTML format. + Build release note documentation in HTML format. deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} - -r{toxinidir}/doc/requirements.txt + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -r{toxinidir}/doc/requirements.txt commands = - sphinx-build -W --keep-going -b html -j auto releasenotes/source releasenotes/build/html + sphinx-build -W --keep-going -b html -j auto releasenotes/source releasenotes/build/html [flake8] # We only enable the hacking (H) and openstacksdk (O) checks From e0845449d3570e0a6bc90660440fb6f46721fd2b Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 13 Feb 2026 18:13:40 +0000 Subject: [PATCH 3812/3836] Run mypy from tox This avoids the need to duplicate our dependency list in multiple places and allows us to take advantage of tox's dependency management infrastructure, to ensure we always get the latest and greatest version of a package allowed by upper-constraints. Change-Id: I46eac909846b2d6bd41608efedbed7db08df0c20 Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 20 +------------------- openstack/config/cloud_region.py | 21 ++++++++++++++------- openstack/exceptions.py | 2 +- openstack/proxy.py | 2 +- pyproject.toml | 13 +++---------- test-requirements.txt | 1 + tox.ini | 19 +++++++++++++++++-- 7 files changed, 38 insertions(+), 40 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5fdf6e375..eec38c36d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: hooks: - id: doc8 - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.0 + rev: v0.15.1 hooks: - id: ruff-check args: ['--fix', '--unsafe-fixes'] @@ -31,21 +31,3 @@ repos: additional_dependencies: - flake8-import-order~=0.19.2 exclude: '^(doc|releasenotes|tools)/.*$' - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.19.1 - hooks: - - id: mypy - additional_dependencies: - - dogpile.cache - - keystoneauth1>=5.11.0 - - types-decorator - - types-PyYAML - - types-requests - - types-simplejson - # keep this in-sync with '[mypy] exclude' in 'setup.cfg' - exclude: | - (?x)( - doc/.* - | examples/.* - | releasenotes/.* - ) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 2aa26c921..d15764838 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -22,7 +22,7 @@ try: import keyring except ImportError: - keyring = None + keyring = None # type: ignore[assignment] from keystoneauth1.access import service_catalog as ks_service_catalog from keystoneauth1 import discover @@ -41,9 +41,11 @@ try: import prometheus_client except ImportError: - prometheus_client = None + prometheus_client = None # type: ignore[assignment] try: - import influxdb as influxdb_client + # NOTE(stephenfin): This library is EOL so we explicitly don't have it in + # our dependencies + import influxdb as influxdb_client # type: ignore[import-not-found] except ImportError: influxdb_client = None @@ -789,6 +791,9 @@ def load_auth_from_cache(self) -> None: try: state = keyring.get_password('openstacksdk', cache_id) except RuntimeError: # the fail backend raises this + state = None + + if not state: self.log.debug('Failed to fetch auth from keyring') return @@ -802,10 +807,12 @@ def set_auth_cache(self) -> None: assert self._auth is not None # narrow type cache_id = self._auth.get_cache_id() - state = self._auth.get_auth_state() + # NOTE(stephenfin): The actual return type of this is a serialized JSON + # object + state = ty.cast(str, self._auth.get_auth_state()) try: - if state: + if cache_id and state: # NOTE: under some conditions the method may be invoked when # auth is empty. This may lead to exception in the keyring lib, # thus do nothing. @@ -1393,7 +1400,7 @@ def get_prometheus_histogram( ], registry=registry, ) - registry._openstacksdk_histogram = hist + setattr(registry, '_openstacksdk_histogram', hist) return hist def get_prometheus_counter( @@ -1415,7 +1422,7 @@ def get_prometheus_counter( ], registry=registry, ) - registry._openstacksdk_counter = counter + setattr(registry, '_openstacksdk_counter', counter) return counter def has_service(self, service_type: str) -> bool: diff --git a/openstack/exceptions.py b/openstack/exceptions.py index 2fb600ebf..02617bb28 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -102,7 +102,7 @@ def __init__( # Call directly rather than via super to control parameters SDKException.__init__(self, message=message) - _rex.HTTPError.__init__(self, message, response=response) + _rex.HTTPError.__init__(self, message, response=response) # type: ignore if response is not None: self.request_id = response.headers.get('x-openstack-request-id') diff --git a/openstack/proxy.py b/openstack/proxy.py index b75708d98..63227316f 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -41,7 +41,7 @@ from openstack import warnings as os_warnings if ty.TYPE_CHECKING: - import influxdb as influxdb_client + import influxdb as influxdb_client # type: ignore[import-not-found] from keystoneauth1 import plugin import prometheus_client import requests diff --git a/pyproject.toml b/pyproject.toml index eebf048a6..576dfe120 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,6 @@ packages = [ python_version = "3.10" show_column_numbers = true show_error_context = true -ignore_missing_imports = true follow_imports = "normal" check_untyped_defs = true warn_unused_ignores = true @@ -58,16 +57,10 @@ disallow_subclassing_any = false disallow_untyped_calls = false disallow_incomplete_defs = true disallow_untyped_defs = false -no_implicit_reexport = false +no_implicit_reexport = true extra_checks = true -# keep this in-sync with 'mypy.exclude' in '.pre-commit-config.yaml' -exclude = ''' -(?x)( - doc - | examples - | releasenotes - ) -''' +disable_error_code = ["import-untyped"] +exclude = '(?x)(doc | examples | releasenotes)' [[tool.mypy.overrides]] module = [ diff --git a/test-requirements.txt b/test-requirements.txt index ad4d20706..846753c01 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -11,3 +11,4 @@ statsd>=3.3.0 stestr>=1.0.0 # Apache-2.0 testscenarios>=0.4 # Apache-2.0/BSD testtools>=2.2.0 # MIT +keyring>=24.0.0 # MIT diff --git a/tox.ini b/tox.ini index 8d91a4779..883fc19d4 100644 --- a/tox.ini +++ b/tox.ini @@ -60,11 +60,26 @@ commands = [testenv:pep8] description = Run style checks. -skip_install = true deps = pre-commit + {[testenv:mypy]deps} commands = - pre-commit run --all-files --show-diff-on-failure + pre-commit run -a + {[testenv:mypy]commands} + +[testenv:mypy] +description = + Run type checks. +deps = + {[testenv]deps} + mypy + types-decorator + types-jmespath + types-PyYAML + types-requests + types-simplejson +commands = + mypy --cache-dir="{envdir}/mypy_cache" {posargs:openstack} [testenv:venv] description = From 46ef2717cd4de1d7b0fdb76569c34c597c77d54a Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Sat, 7 Feb 2026 16:54:04 +0900 Subject: [PATCH 3813/3836] Remove MANIFEST.in This file isn't needed as far as we use pbr. Trivial-Fix Change-Id: Iacd11130047b368d2a68c9fc8dd7f60acc31f8af Signed-off-by: Takashi Kajinami --- MANIFEST.in | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 90f8a7aef..000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,6 +0,0 @@ -include AUTHORS -include ChangeLog -exclude .gitignore -exclude .gitreview - -global-exclude *.pyc \ No newline at end of file From 9877ca74f9bb50515fce3c643d0dc71e41072f16 Mon Sep 17 00:00:00 2001 From: jgilaber Date: Tue, 24 Feb 2026 12:00:56 +0100 Subject: [PATCH 3814/3836] Allow configuring connect_retry_delay Allow reading connect_retry_delay from the oslo conf. Change-Id: Ia6e5ac655f94661fd5f5a0afbd113504bab6c331 Signed-off-by: jgilaber Closes-Bug: #2142571 --- openstack/config/cloud_region.py | 6 ++++++ openstack/tests/unit/config/test_cloud_config.py | 4 ++++ .../notes/parse_connect_retries_delay-4306e9a0f50ee006.yaml | 5 +++++ 3 files changed, 15 insertions(+) create mode 100644 releasenotes/notes/parse_connect_retries_delay-4306e9a0f50ee006.yaml diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index d15764838..759b2f4b8 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -755,6 +755,12 @@ def get_connect_retries(self, service_type: str) -> int | None: ) return int(value) if value is not None else value + def get_connect_retry_delay(self, service_type: str) -> float | None: + value = self._get_config( + 'connect_retry_delay', service_type, fallback_to_unprefixed=True + ) + return float(value) if value is not None else value + def get_status_code_retries(self, service_type: str) -> int | None: value = self._get_config( 'status_code_retries', service_type, fallback_to_unprefixed=True diff --git a/openstack/tests/unit/config/test_cloud_config.py b/openstack/tests/unit/config/test_cloud_config.py index 9b078ead2..f836c999a 100644 --- a/openstack/tests/unit/config/test_cloud_config.py +++ b/openstack/tests/unit/config/test_cloud_config.py @@ -34,8 +34,10 @@ 'volume_api_version': '1', 'auth': {'password': 'hunter2', 'username': 'AzureDiamond'}, 'connect_retries': 1, + 'connect_retry_delay': 0.5, 'baremetal_status_code_retries': 5, 'baremetal_connect_retries': 3, + 'baremetal_connect_retry_delay': 1.5, } @@ -183,6 +185,8 @@ def test_getters(self): self.assertEqual(5, cc.get_status_code_retries('baremetal')) self.assertEqual(1, cc.get_connect_retries('compute')) self.assertEqual(3, cc.get_connect_retries('baremetal')) + self.assertEqual(0.5, cc.get_connect_retry_delay('compute')) + self.assertEqual(1.5, cc.get_connect_retry_delay('baremetal')) def test_rackspace_workaround(self): # We're skipping loader here, so we have to expand relevant diff --git a/releasenotes/notes/parse_connect_retries_delay-4306e9a0f50ee006.yaml b/releasenotes/notes/parse_connect_retries_delay-4306e9a0f50ee006.yaml new file mode 100644 index 000000000..91680ba52 --- /dev/null +++ b/releasenotes/notes/parse_connect_retries_delay-4306e9a0f50ee006.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + ``Session.connect_retry_delay`` is now configurable via ``clouds.yaml`` using + the ``_connect_retry_delay`` and ``connect_retry_delay`` options. From ca1e4c50646e67d0ef152f692cad548f5d7dfa4a Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 26 Feb 2026 20:31:49 +0000 Subject: [PATCH 3815/3836] Fix type issue We have a new version of types-requests. Change-Id: Ib071b1fd9057125ea7bf883711dbc65c49b25990 Signed-off-by: Stephen Finucane --- openstack/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/exceptions.py b/openstack/exceptions.py index 02617bb28..2fb600ebf 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -102,7 +102,7 @@ def __init__( # Call directly rather than via super to control parameters SDKException.__init__(self, message=message) - _rex.HTTPError.__init__(self, message, response=response) # type: ignore + _rex.HTTPError.__init__(self, message, response=response) if response is not None: self.request_id = response.headers.get('x-openstack-request-id') From 77d4fb04ab8c4e5cbd6e13e6ca4181088a710200 Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Sun, 8 Feb 2026 16:05:15 +0900 Subject: [PATCH 3816/3836] Remove unused babel.cfg This files is no longer needed. Change-Id: Ia85c15891abc7ab67f38af35cb6dcfde3442a4ce Signed-off-by: Takashi Kajinami --- babel.cfg | 1 - 1 file changed, 1 deletion(-) delete mode 100644 babel.cfg diff --git a/babel.cfg b/babel.cfg deleted file mode 100644 index efceab818..000000000 --- a/babel.cfg +++ /dev/null @@ -1 +0,0 @@ -[python: **.py] From 3c6e1be114878968cda08d89f1f9b33f0e757911 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 14 Nov 2025 12:52:53 +0000 Subject: [PATCH 3817/3836] trivial: Misc test fixes Make typing easier. Change-Id: I6657112fe74f37f44a483abd58050c1f96c6155c Signed-off-by: Stephen Finucane --- .../functional/baremetal/test_baremetal_node.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index d4dbdb2aa..320da900f 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -27,13 +27,12 @@ def test_node_create_get_delete(self): # NOTE(dtantsur): get_node and find_node only differ in handing missing # nodes, otherwise they are identical. - for call, ident in [ - (self.operator_cloud.baremetal.get_node, self.node_id), - (self.operator_cloud.baremetal.get_node, 'node-name'), - (self.operator_cloud.baremetal.find_node, self.node_id), - (self.operator_cloud.baremetal.find_node, 'node-name'), - ]: - found = call(ident) + for ident in (self.node_id, 'node-name'): + found = self.operator_cloud.baremetal.get_node(ident) + self.assertEqual(node.id, found.id) + self.assertEqual(node.name, found.name) + + found = self.operator_cloud.baremetal.find_node(ident) self.assertEqual(node.id, found.id) self.assertEqual(node.name, found.name) From e8b3c26174e0dc076757292d5cc53caf18c29ece Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 26 Feb 2026 12:08:24 +0000 Subject: [PATCH 3818/3836] tests: Use common structure for functional tests Effectively a lift and shift. Change-Id: Ib5823260d1d31fb3eb700d29d16ddc2163743b78 Signed-off-by: Stephen Finucane --- openstack/tests/functional/baremetal/v1/__init__.py | 0 openstack/tests/functional/baremetal/{ => v1}/base.py | 0 .../test_allocation.py} | 2 +- .../{test_baremetal_chassis.py => v1/test_chassis.py} | 2 +- .../{test_baremetal_conductor.py => v1/test_conductor.py} | 2 +- .../test_deploy_templates.py} | 2 +- .../{test_baremetal_driver.py => v1/test_driver.py} | 2 +- .../test_inspection_rules.py} | 2 +- .../baremetal/{test_baremetal_node.py => v1/test_node.py} | 2 +- .../baremetal/{test_baremetal_port.py => v1/test_port.py} | 2 +- .../test_port_group.py} | 2 +- .../{test_baremetal_runbooks.py => v1/test_runbooks.py} | 2 +- .../test_volume_connector.py} | 2 +- .../test_volume_target.py} | 2 +- openstack/tests/functional/clustering/v1/__init__.py | 0 .../tests/functional/clustering/{ => v1}/test_cluster.py | 0 openstack/tests/functional/compute/{ => v2}/base.py | 0 openstack/tests/functional/compute/v2/test_extension.py | 5 ++--- openstack/tests/functional/compute/v2/test_flavor.py | 5 +++-- openstack/tests/functional/compute/v2/test_hypervisor.py | 4 ++-- openstack/tests/functional/compute/v2/test_image.py | 5 ++--- openstack/tests/functional/compute/v2/test_keypair.py | 7 +++---- openstack/tests/functional/compute/v2/test_limits.py | 4 ++-- openstack/tests/functional/compute/v2/test_quota_set.py | 4 ++-- openstack/tests/functional/compute/v2/test_server.py | 2 +- openstack/tests/functional/compute/v2/test_service.py | 4 ++-- .../tests/functional/compute/v2/test_volume_attachment.py | 2 +- openstack/tests/functional/instance_ha/v1/__init__.py | 0 .../tests/functional/instance_ha/{ => v1}/test_host.py | 1 - .../tests/functional/instance_ha/{ => v1}/test_segment.py | 0 .../tests/functional/shared_file_system/v2/__init__.py | 0 .../tests/functional/shared_file_system/{ => v2}/base.py | 0 .../shared_file_system/{ => v2}/test_availability_zone.py | 2 +- .../shared_file_system/{ => v2}/test_export_locations.py | 2 +- .../functional/shared_file_system/{ => v2}/test_limit.py | 2 +- .../shared_file_system/{ => v2}/test_quota_class_set.py | 2 +- .../shared_file_system/{ => v2}/test_resource_lock.py | 2 +- .../functional/shared_file_system/{ => v2}/test_share.py | 2 +- .../shared_file_system/{ => v2}/test_share_access_rule.py | 2 +- .../shared_file_system/{ => v2}/test_share_group.py | 2 +- .../{ => v2}/test_share_group_snapshot.py | 2 +- .../shared_file_system/{ => v2}/test_share_instance.py | 2 +- .../shared_file_system/{ => v2}/test_share_metadata.py | 2 +- .../shared_file_system/{ => v2}/test_share_network.py | 2 +- .../{ => v2}/test_share_network_subnet.py | 2 +- .../shared_file_system/{ => v2}/test_share_snapshot.py | 2 +- .../{ => v2}/test_share_snapshot_instance.py | 2 +- .../shared_file_system/{ => v2}/test_storage_pool.py | 2 +- .../shared_file_system/{ => v2}/test_user_message.py | 2 +- 49 files changed, 49 insertions(+), 52 deletions(-) create mode 100644 openstack/tests/functional/baremetal/v1/__init__.py rename openstack/tests/functional/baremetal/{ => v1}/base.py (100%) rename openstack/tests/functional/baremetal/{test_baremetal_allocation.py => v1/test_allocation.py} (99%) rename openstack/tests/functional/baremetal/{test_baremetal_chassis.py => v1/test_chassis.py} (98%) rename openstack/tests/functional/baremetal/{test_baremetal_conductor.py => v1/test_conductor.py} (95%) rename openstack/tests/functional/baremetal/{test_baremetal_deploy_templates.py => v1/test_deploy_templates.py} (99%) rename openstack/tests/functional/baremetal/{test_baremetal_driver.py => v1/test_driver.py} (97%) rename openstack/tests/functional/baremetal/{test_baremetal_inspection_rules.py => v1/test_inspection_rules.py} (99%) rename openstack/tests/functional/baremetal/{test_baremetal_node.py => v1/test_node.py} (99%) rename openstack/tests/functional/baremetal/{test_baremetal_port.py => v1/test_port.py} (98%) rename openstack/tests/functional/baremetal/{test_baremetal_port_group.py => v1/test_port_group.py} (98%) rename openstack/tests/functional/baremetal/{test_baremetal_runbooks.py => v1/test_runbooks.py} (99%) rename openstack/tests/functional/baremetal/{test_baremetal_volume_connector.py => v1/test_volume_connector.py} (99%) rename openstack/tests/functional/baremetal/{test_baremetal_volume_target.py => v1/test_volume_target.py} (99%) create mode 100644 openstack/tests/functional/clustering/v1/__init__.py rename openstack/tests/functional/clustering/{ => v1}/test_cluster.py (100%) rename openstack/tests/functional/compute/{ => v2}/base.py (100%) create mode 100644 openstack/tests/functional/instance_ha/v1/__init__.py rename openstack/tests/functional/instance_ha/{ => v1}/test_host.py (99%) rename openstack/tests/functional/instance_ha/{ => v1}/test_segment.py (100%) create mode 100644 openstack/tests/functional/shared_file_system/v2/__init__.py rename openstack/tests/functional/shared_file_system/{ => v2}/base.py (100%) rename openstack/tests/functional/shared_file_system/{ => v2}/test_availability_zone.py (93%) rename openstack/tests/functional/shared_file_system/{ => v2}/test_export_locations.py (96%) rename openstack/tests/functional/shared_file_system/{ => v2}/test_limit.py (95%) rename openstack/tests/functional/shared_file_system/{ => v2}/test_quota_class_set.py (95%) rename openstack/tests/functional/shared_file_system/{ => v2}/test_resource_lock.py (98%) rename openstack/tests/functional/shared_file_system/{ => v2}/test_share.py (99%) rename openstack/tests/functional/shared_file_system/{ => v2}/test_share_access_rule.py (97%) rename openstack/tests/functional/shared_file_system/{ => v2}/test_share_group.py (97%) rename openstack/tests/functional/shared_file_system/{ => v2}/test_share_group_snapshot.py (98%) rename openstack/tests/functional/shared_file_system/{ => v2}/test_share_instance.py (97%) rename openstack/tests/functional/shared_file_system/{ => v2}/test_share_metadata.py (98%) rename openstack/tests/functional/shared_file_system/{ => v2}/test_share_network.py (98%) rename openstack/tests/functional/shared_file_system/{ => v2}/test_share_network_subnet.py (97%) rename openstack/tests/functional/shared_file_system/{ => v2}/test_share_snapshot.py (98%) rename openstack/tests/functional/shared_file_system/{ => v2}/test_share_snapshot_instance.py (95%) rename openstack/tests/functional/shared_file_system/{ => v2}/test_storage_pool.py (93%) rename openstack/tests/functional/shared_file_system/{ => v2}/test_user_message.py (95%) diff --git a/openstack/tests/functional/baremetal/v1/__init__.py b/openstack/tests/functional/baremetal/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/functional/baremetal/base.py b/openstack/tests/functional/baremetal/v1/base.py similarity index 100% rename from openstack/tests/functional/baremetal/base.py rename to openstack/tests/functional/baremetal/v1/base.py diff --git a/openstack/tests/functional/baremetal/test_baremetal_allocation.py b/openstack/tests/functional/baremetal/v1/test_allocation.py similarity index 99% rename from openstack/tests/functional/baremetal/test_baremetal_allocation.py rename to openstack/tests/functional/baremetal/v1/test_allocation.py index be6c846cb..99c7b570e 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_allocation.py +++ b/openstack/tests/functional/baremetal/v1/test_allocation.py @@ -13,7 +13,7 @@ import random from openstack import exceptions -from openstack.tests.functional.baremetal import base +from openstack.tests.functional.baremetal.v1 import base class Base(base.BaseBaremetalTest): diff --git a/openstack/tests/functional/baremetal/test_baremetal_chassis.py b/openstack/tests/functional/baremetal/v1/test_chassis.py similarity index 98% rename from openstack/tests/functional/baremetal/test_baremetal_chassis.py rename to openstack/tests/functional/baremetal/v1/test_chassis.py index 04a7a72a7..8217763e5 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_chassis.py +++ b/openstack/tests/functional/baremetal/v1/test_chassis.py @@ -12,7 +12,7 @@ from openstack import exceptions -from openstack.tests.functional.baremetal import base +from openstack.tests.functional.baremetal.v1 import base class TestBareMetalChassis(base.BaseBaremetalTest): diff --git a/openstack/tests/functional/baremetal/test_baremetal_conductor.py b/openstack/tests/functional/baremetal/v1/test_conductor.py similarity index 95% rename from openstack/tests/functional/baremetal/test_baremetal_conductor.py rename to openstack/tests/functional/baremetal/v1/test_conductor.py index 03aea6949..68f2d805e 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_conductor.py +++ b/openstack/tests/functional/baremetal/v1/test_conductor.py @@ -11,7 +11,7 @@ # under the License. -from openstack.tests.functional.baremetal import base +from openstack.tests.functional.baremetal.v1 import base class TestBareMetalConductor(base.BaseBaremetalTest): diff --git a/openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py b/openstack/tests/functional/baremetal/v1/test_deploy_templates.py similarity index 99% rename from openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py rename to openstack/tests/functional/baremetal/v1/test_deploy_templates.py index c2b9e209b..f3a3ec08c 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_deploy_templates.py +++ b/openstack/tests/functional/baremetal/v1/test_deploy_templates.py @@ -11,7 +11,7 @@ # under the License. from openstack import exceptions -from openstack.tests.functional.baremetal import base +from openstack.tests.functional.baremetal.v1 import base class TestBareMetalDeployTemplate(base.BaseBaremetalTest): diff --git a/openstack/tests/functional/baremetal/test_baremetal_driver.py b/openstack/tests/functional/baremetal/v1/test_driver.py similarity index 97% rename from openstack/tests/functional/baremetal/test_baremetal_driver.py rename to openstack/tests/functional/baremetal/v1/test_driver.py index e45b0f0ac..b48a5693b 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_driver.py +++ b/openstack/tests/functional/baremetal/v1/test_driver.py @@ -12,7 +12,7 @@ from openstack import exceptions -from openstack.tests.functional.baremetal import base +from openstack.tests.functional.baremetal.v1 import base class TestBareMetalDriver(base.BaseBaremetalTest): diff --git a/openstack/tests/functional/baremetal/test_baremetal_inspection_rules.py b/openstack/tests/functional/baremetal/v1/test_inspection_rules.py similarity index 99% rename from openstack/tests/functional/baremetal/test_baremetal_inspection_rules.py rename to openstack/tests/functional/baremetal/v1/test_inspection_rules.py index c9d3e14c7..bbfe6627c 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_inspection_rules.py +++ b/openstack/tests/functional/baremetal/v1/test_inspection_rules.py @@ -11,7 +11,7 @@ # under the License. from openstack import exceptions -from openstack.tests.functional.baremetal import base +from openstack.tests.functional.baremetal.v1 import base class TestBareMetalInspectionRule(base.BaseBaremetalTest): diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/v1/test_node.py similarity index 99% rename from openstack/tests/functional/baremetal/test_baremetal_node.py rename to openstack/tests/functional/baremetal/v1/test_node.py index 320da900f..5b5b70785 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/v1/test_node.py @@ -14,7 +14,7 @@ import uuid from openstack import exceptions -from openstack.tests.functional.baremetal import base +from openstack.tests.functional.baremetal.v1 import base class TestBareMetalNode(base.BaseBaremetalTest): diff --git a/openstack/tests/functional/baremetal/test_baremetal_port.py b/openstack/tests/functional/baremetal/v1/test_port.py similarity index 98% rename from openstack/tests/functional/baremetal/test_baremetal_port.py rename to openstack/tests/functional/baremetal/v1/test_port.py index 69fd46dcb..63f8f86df 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_port.py +++ b/openstack/tests/functional/baremetal/v1/test_port.py @@ -12,7 +12,7 @@ from openstack import exceptions -from openstack.tests.functional.baremetal import base +from openstack.tests.functional.baremetal.v1 import base class TestBareMetalPort(base.BaseBaremetalTest): diff --git a/openstack/tests/functional/baremetal/test_baremetal_port_group.py b/openstack/tests/functional/baremetal/v1/test_port_group.py similarity index 98% rename from openstack/tests/functional/baremetal/test_baremetal_port_group.py rename to openstack/tests/functional/baremetal/v1/test_port_group.py index 5e6f3f640..335338de1 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_port_group.py +++ b/openstack/tests/functional/baremetal/v1/test_port_group.py @@ -12,7 +12,7 @@ from openstack import exceptions -from openstack.tests.functional.baremetal import base +from openstack.tests.functional.baremetal.v1 import base class TestBareMetalPortGroup(base.BaseBaremetalTest): diff --git a/openstack/tests/functional/baremetal/test_baremetal_runbooks.py b/openstack/tests/functional/baremetal/v1/test_runbooks.py similarity index 99% rename from openstack/tests/functional/baremetal/test_baremetal_runbooks.py rename to openstack/tests/functional/baremetal/v1/test_runbooks.py index 2c3783426..36dc119e8 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_runbooks.py +++ b/openstack/tests/functional/baremetal/v1/test_runbooks.py @@ -11,7 +11,7 @@ # under the License. from openstack import exceptions -from openstack.tests.functional.baremetal import base +from openstack.tests.functional.baremetal.v1 import base class TestBareMetalRunbook(base.BaseBaremetalTest): diff --git a/openstack/tests/functional/baremetal/test_baremetal_volume_connector.py b/openstack/tests/functional/baremetal/v1/test_volume_connector.py similarity index 99% rename from openstack/tests/functional/baremetal/test_baremetal_volume_connector.py rename to openstack/tests/functional/baremetal/v1/test_volume_connector.py index de0594af6..7190f524c 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_volume_connector.py +++ b/openstack/tests/functional/baremetal/v1/test_volume_connector.py @@ -12,7 +12,7 @@ from openstack import exceptions -from openstack.tests.functional.baremetal import base +from openstack.tests.functional.baremetal.v1 import base class TestBareMetalVolumeconnector(base.BaseBaremetalTest): diff --git a/openstack/tests/functional/baremetal/test_baremetal_volume_target.py b/openstack/tests/functional/baremetal/v1/test_volume_target.py similarity index 99% rename from openstack/tests/functional/baremetal/test_baremetal_volume_target.py rename to openstack/tests/functional/baremetal/v1/test_volume_target.py index b6d22fa8b..a3567d756 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_volume_target.py +++ b/openstack/tests/functional/baremetal/v1/test_volume_target.py @@ -12,7 +12,7 @@ from openstack import exceptions -from openstack.tests.functional.baremetal import base +from openstack.tests.functional.baremetal.v1 import base class TestBareMetalVolumetarget(base.BaseBaremetalTest): diff --git a/openstack/tests/functional/clustering/v1/__init__.py b/openstack/tests/functional/clustering/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/functional/clustering/test_cluster.py b/openstack/tests/functional/clustering/v1/test_cluster.py similarity index 100% rename from openstack/tests/functional/clustering/test_cluster.py rename to openstack/tests/functional/clustering/v1/test_cluster.py diff --git a/openstack/tests/functional/compute/base.py b/openstack/tests/functional/compute/v2/base.py similarity index 100% rename from openstack/tests/functional/compute/base.py rename to openstack/tests/functional/compute/v2/base.py diff --git a/openstack/tests/functional/compute/v2/test_extension.py b/openstack/tests/functional/compute/v2/test_extension.py index 92674ca98..84581f63f 100644 --- a/openstack/tests/functional/compute/v2/test_extension.py +++ b/openstack/tests/functional/compute/v2/test_extension.py @@ -10,11 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.tests.functional.compute.v2 import base -from openstack.tests.functional import base - -class TestExtension(base.BaseFunctionalTest): +class TestExtension(base.BaseComputeTest): def test_list(self): extensions = list(self.operator_cloud.compute.extensions()) self.assertGreater(len(extensions), 0) diff --git a/openstack/tests/functional/compute/v2/test_flavor.py b/openstack/tests/functional/compute/v2/test_flavor.py index 8cd597a04..142168e62 100644 --- a/openstack/tests/functional/compute/v2/test_flavor.py +++ b/openstack/tests/functional/compute/v2/test_flavor.py @@ -9,13 +9,14 @@ # 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 uuid from openstack.compute.v2 import flavor as _flavor -from openstack.tests.functional import base +from openstack.tests.functional.compute.v2 import base -class TestFlavor(base.BaseFunctionalTest): +class TestFlavor(base.BaseComputeTest): def setUp(self): super().setUp() diff --git a/openstack/tests/functional/compute/v2/test_hypervisor.py b/openstack/tests/functional/compute/v2/test_hypervisor.py index 760fc81a8..0d5310f17 100644 --- a/openstack/tests/functional/compute/v2/test_hypervisor.py +++ b/openstack/tests/functional/compute/v2/test_hypervisor.py @@ -11,10 +11,10 @@ # under the License. from openstack.compute.v2 import hypervisor as _hypervisor -from openstack.tests.functional import base +from openstack.tests.functional.compute.v2 import base -class TestHypervisor(base.BaseFunctionalTest): +class TestHypervisor(base.BaseComputeTest): def test_hypervisors(self): hypervisors = list(self.operator_cloud.compute.hypervisors()) self.assertIsNotNone(hypervisors) diff --git a/openstack/tests/functional/compute/v2/test_image.py b/openstack/tests/functional/compute/v2/test_image.py index 51b720feb..bb7c73fa0 100644 --- a/openstack/tests/functional/compute/v2/test_image.py +++ b/openstack/tests/functional/compute/v2/test_image.py @@ -10,13 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. - from openstack.compute.v2 import image as _image -from openstack.tests.functional import base +from openstack.tests.functional.compute.v2 import base from openstack.tests.functional.image.v2.test_image import TEST_IMAGE_NAME -class TestImage(base.BaseFunctionalTest): +class TestImage(base.BaseComputeTest): def setUp(self): super().setUp() diff --git a/openstack/tests/functional/compute/v2/test_keypair.py b/openstack/tests/functional/compute/v2/test_keypair.py index 33c1b59f1..a7a0a83a2 100644 --- a/openstack/tests/functional/compute/v2/test_keypair.py +++ b/openstack/tests/functional/compute/v2/test_keypair.py @@ -10,12 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. - from openstack.compute.v2 import keypair as _keypair -from openstack.tests.functional import base +from openstack.tests.functional.compute.v2 import base -class TestKeypair(base.BaseFunctionalTest): +class TestKeypair(base.BaseComputeTest): def setUp(self): super().setUp() @@ -58,7 +57,7 @@ def test_keypair(self): self.assertIn(self.keypair_name, {x.name for x in keypairs}) -class TestKeypairAdmin(base.BaseFunctionalTest): +class TestKeypairAdmin(base.BaseComputeTest): def setUp(self): super().setUp() diff --git a/openstack/tests/functional/compute/v2/test_limits.py b/openstack/tests/functional/compute/v2/test_limits.py index 1b99f1e3c..66a72fd37 100644 --- a/openstack/tests/functional/compute/v2/test_limits.py +++ b/openstack/tests/functional/compute/v2/test_limits.py @@ -10,10 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.functional import base +from openstack.tests.functional.compute.v2 import base -class TestLimits(base.BaseFunctionalTest): +class TestLimits(base.BaseComputeTest): def test_limits(self): sot = self.operator_cloud.compute.get_limits() self.assertIsNotNone(sot.absolute['instances']) diff --git a/openstack/tests/functional/compute/v2/test_quota_set.py b/openstack/tests/functional/compute/v2/test_quota_set.py index 1b992e1b7..febdf2fe1 100644 --- a/openstack/tests/functional/compute/v2/test_quota_set.py +++ b/openstack/tests/functional/compute/v2/test_quota_set.py @@ -11,10 +11,10 @@ # under the License. from openstack.compute.v2 import quota_set as _quota_set -from openstack.tests.functional import base +from openstack.tests.functional.compute.v2 import base -class TestQuotaSet(base.BaseFunctionalTest): +class TestQuotaSet(base.BaseComputeTest): def setUp(self): super().setUp() diff --git a/openstack/tests/functional/compute/v2/test_server.py b/openstack/tests/functional/compute/v2/test_server.py index a32aa88a6..b1e7a8cca 100644 --- a/openstack/tests/functional/compute/v2/test_server.py +++ b/openstack/tests/functional/compute/v2/test_server.py @@ -11,7 +11,7 @@ # under the License. from openstack.compute.v2 import server as _server -from openstack.tests.functional.compute import base as base +from openstack.tests.functional.compute.v2 import base from openstack.tests.functional.network.v2 import test_network diff --git a/openstack/tests/functional/compute/v2/test_service.py b/openstack/tests/functional/compute/v2/test_service.py index cd5a61a76..a8359190d 100644 --- a/openstack/tests/functional/compute/v2/test_service.py +++ b/openstack/tests/functional/compute/v2/test_service.py @@ -10,10 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.functional import base +from openstack.tests.functional.compute.v2 import base -class TestService(base.BaseFunctionalTest): +class TestService(base.BaseComputeTest): def test_service(self): # list all services services = list(self.operator_cloud.compute.services()) diff --git a/openstack/tests/functional/compute/v2/test_volume_attachment.py b/openstack/tests/functional/compute/v2/test_volume_attachment.py index c4281930c..ebe3e2ff3 100644 --- a/openstack/tests/functional/compute/v2/test_volume_attachment.py +++ b/openstack/tests/functional/compute/v2/test_volume_attachment.py @@ -13,7 +13,7 @@ from openstack.block_storage.v3 import volume as _volume from openstack.compute.v2 import server as _server from openstack.compute.v2 import volume_attachment as _volume_attachment -from openstack.tests.functional.compute import base +from openstack.tests.functional.compute.v2 import base class TestServerVolumeAttachment(base.BaseComputeTest): diff --git a/openstack/tests/functional/instance_ha/v1/__init__.py b/openstack/tests/functional/instance_ha/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/functional/instance_ha/test_host.py b/openstack/tests/functional/instance_ha/v1/test_host.py similarity index 99% rename from openstack/tests/functional/instance_ha/test_host.py rename to openstack/tests/functional/instance_ha/v1/test_host.py index 71801fc1e..2058b7bed 100644 --- a/openstack/tests/functional/instance_ha/test_host.py +++ b/openstack/tests/functional/instance_ha/v1/test_host.py @@ -13,7 +13,6 @@ # License for the specific language governing permissions and limitations # under the License. - from openstack.compute.v2 import hypervisor from openstack import connection from openstack.tests.functional import base diff --git a/openstack/tests/functional/instance_ha/test_segment.py b/openstack/tests/functional/instance_ha/v1/test_segment.py similarity index 100% rename from openstack/tests/functional/instance_ha/test_segment.py rename to openstack/tests/functional/instance_ha/v1/test_segment.py diff --git a/openstack/tests/functional/shared_file_system/v2/__init__.py b/openstack/tests/functional/shared_file_system/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/functional/shared_file_system/base.py b/openstack/tests/functional/shared_file_system/v2/base.py similarity index 100% rename from openstack/tests/functional/shared_file_system/base.py rename to openstack/tests/functional/shared_file_system/v2/base.py diff --git a/openstack/tests/functional/shared_file_system/test_availability_zone.py b/openstack/tests/functional/shared_file_system/v2/test_availability_zone.py similarity index 93% rename from openstack/tests/functional/shared_file_system/test_availability_zone.py rename to openstack/tests/functional/shared_file_system/v2/test_availability_zone.py index 07f694d47..41bb70585 100644 --- a/openstack/tests/functional/shared_file_system/test_availability_zone.py +++ b/openstack/tests/functional/shared_file_system/v2/test_availability_zone.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.functional.shared_file_system import base +from openstack.tests.functional.shared_file_system.v2 import base class AvailabilityZoneTest(base.BaseSharedFileSystemTest): diff --git a/openstack/tests/functional/shared_file_system/test_export_locations.py b/openstack/tests/functional/shared_file_system/v2/test_export_locations.py similarity index 96% rename from openstack/tests/functional/shared_file_system/test_export_locations.py rename to openstack/tests/functional/shared_file_system/v2/test_export_locations.py index ee801f1e0..3fec84d13 100644 --- a/openstack/tests/functional/shared_file_system/test_export_locations.py +++ b/openstack/tests/functional/shared_file_system/v2/test_export_locations.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.functional.shared_file_system import base +from openstack.tests.functional.shared_file_system.v2 import base class TestExportLocation(base.BaseSharedFileSystemTest): diff --git a/openstack/tests/functional/shared_file_system/test_limit.py b/openstack/tests/functional/shared_file_system/v2/test_limit.py similarity index 95% rename from openstack/tests/functional/shared_file_system/test_limit.py rename to openstack/tests/functional/shared_file_system/v2/test_limit.py index 91f6b2ca5..2bdde5735 100644 --- a/openstack/tests/functional/shared_file_system/test_limit.py +++ b/openstack/tests/functional/shared_file_system/v2/test_limit.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.functional.shared_file_system import base +from openstack.tests.functional.shared_file_system.v2 import base class LimitTest(base.BaseSharedFileSystemTest): diff --git a/openstack/tests/functional/shared_file_system/test_quota_class_set.py b/openstack/tests/functional/shared_file_system/v2/test_quota_class_set.py similarity index 95% rename from openstack/tests/functional/shared_file_system/test_quota_class_set.py rename to openstack/tests/functional/shared_file_system/v2/test_quota_class_set.py index 1bc7cfb34..ea6ac1759 100644 --- a/openstack/tests/functional/shared_file_system/test_quota_class_set.py +++ b/openstack/tests/functional/shared_file_system/v2/test_quota_class_set.py @@ -11,7 +11,7 @@ # under the License. from openstack.shared_file_system.v2 import quota_class_set as _quota_class_set -from openstack.tests.functional.shared_file_system import base +from openstack.tests.functional.shared_file_system.v2 import base class QuotaClassSetTest(base.BaseSharedFileSystemTest): diff --git a/openstack/tests/functional/shared_file_system/test_resource_lock.py b/openstack/tests/functional/shared_file_system/v2/test_resource_lock.py similarity index 98% rename from openstack/tests/functional/shared_file_system/test_resource_lock.py rename to openstack/tests/functional/shared_file_system/v2/test_resource_lock.py index b9004b072..52e4dda59 100644 --- a/openstack/tests/functional/shared_file_system/test_resource_lock.py +++ b/openstack/tests/functional/shared_file_system/v2/test_resource_lock.py @@ -11,7 +11,7 @@ # under the License. from openstack.shared_file_system.v2 import resource_locks as _resource_locks -from openstack.tests.functional.shared_file_system import base +from openstack.tests.functional.shared_file_system.v2 import base class ResourceLocksTest(base.BaseSharedFileSystemTest): diff --git a/openstack/tests/functional/shared_file_system/test_share.py b/openstack/tests/functional/shared_file_system/v2/test_share.py similarity index 99% rename from openstack/tests/functional/shared_file_system/test_share.py rename to openstack/tests/functional/shared_file_system/v2/test_share.py index cab1c6c07..1437c5aab 100644 --- a/openstack/tests/functional/shared_file_system/test_share.py +++ b/openstack/tests/functional/shared_file_system/v2/test_share.py @@ -12,7 +12,7 @@ from openstack import exceptions from openstack.shared_file_system.v2 import share as _share -from openstack.tests.functional.shared_file_system import base +from openstack.tests.functional.shared_file_system.v2 import base class ShareTest(base.BaseSharedFileSystemTest): diff --git a/openstack/tests/functional/shared_file_system/test_share_access_rule.py b/openstack/tests/functional/shared_file_system/v2/test_share_access_rule.py similarity index 97% rename from openstack/tests/functional/shared_file_system/test_share_access_rule.py rename to openstack/tests/functional/shared_file_system/v2/test_share_access_rule.py index 4050a3507..b59f125e5 100644 --- a/openstack/tests/functional/shared_file_system/test_share_access_rule.py +++ b/openstack/tests/functional/shared_file_system/v2/test_share_access_rule.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.functional.shared_file_system import base +from openstack.tests.functional.shared_file_system.v2 import base class ShareAccessRuleTest(base.BaseSharedFileSystemTest): diff --git a/openstack/tests/functional/shared_file_system/test_share_group.py b/openstack/tests/functional/shared_file_system/v2/test_share_group.py similarity index 97% rename from openstack/tests/functional/shared_file_system/test_share_group.py rename to openstack/tests/functional/shared_file_system/v2/test_share_group.py index 17ba4aadb..d3161b8ba 100644 --- a/openstack/tests/functional/shared_file_system/test_share_group.py +++ b/openstack/tests/functional/shared_file_system/v2/test_share_group.py @@ -11,7 +11,7 @@ # under the License. from openstack.shared_file_system.v2 import share_group as _share_group -from openstack.tests.functional.shared_file_system import base +from openstack.tests.functional.shared_file_system.v2 import base class ShareGroupTest(base.BaseSharedFileSystemTest): diff --git a/openstack/tests/functional/shared_file_system/test_share_group_snapshot.py b/openstack/tests/functional/shared_file_system/v2/test_share_group_snapshot.py similarity index 98% rename from openstack/tests/functional/shared_file_system/test_share_group_snapshot.py rename to openstack/tests/functional/shared_file_system/v2/test_share_group_snapshot.py index a7b407610..1dd8f80ee 100644 --- a/openstack/tests/functional/shared_file_system/test_share_group_snapshot.py +++ b/openstack/tests/functional/shared_file_system/v2/test_share_group_snapshot.py @@ -14,7 +14,7 @@ from openstack.shared_file_system.v2 import ( share_group_snapshot as _share_group_snapshot, ) -from openstack.tests.functional.shared_file_system import base +from openstack.tests.functional.shared_file_system.v2 import base class ShareGroupSnapshotTest(base.BaseSharedFileSystemTest): diff --git a/openstack/tests/functional/shared_file_system/test_share_instance.py b/openstack/tests/functional/shared_file_system/v2/test_share_instance.py similarity index 97% rename from openstack/tests/functional/shared_file_system/test_share_instance.py rename to openstack/tests/functional/shared_file_system/v2/test_share_instance.py index ece9db913..082f2c0b9 100644 --- a/openstack/tests/functional/shared_file_system/test_share_instance.py +++ b/openstack/tests/functional/shared_file_system/v2/test_share_instance.py @@ -13,7 +13,7 @@ from openstack import resource from openstack.shared_file_system.v2 import share_instance as _share_instance -from openstack.tests.functional.shared_file_system import base +from openstack.tests.functional.shared_file_system.v2 import base class ShareInstanceTest(base.BaseSharedFileSystemTest): diff --git a/openstack/tests/functional/shared_file_system/test_share_metadata.py b/openstack/tests/functional/shared_file_system/v2/test_share_metadata.py similarity index 98% rename from openstack/tests/functional/shared_file_system/test_share_metadata.py rename to openstack/tests/functional/shared_file_system/v2/test_share_metadata.py index e68c572fe..46c6089e5 100644 --- a/openstack/tests/functional/shared_file_system/test_share_metadata.py +++ b/openstack/tests/functional/shared_file_system/v2/test_share_metadata.py @@ -12,7 +12,7 @@ from openstack.shared_file_system.v2 import share as _share -from openstack.tests.functional.shared_file_system import base +from openstack.tests.functional.shared_file_system.v2 import base class ShareMetadataTest(base.BaseSharedFileSystemTest): diff --git a/openstack/tests/functional/shared_file_system/test_share_network.py b/openstack/tests/functional/shared_file_system/v2/test_share_network.py similarity index 98% rename from openstack/tests/functional/shared_file_system/test_share_network.py rename to openstack/tests/functional/shared_file_system/v2/test_share_network.py index 5497ac1f9..ed82754d1 100644 --- a/openstack/tests/functional/shared_file_system/test_share_network.py +++ b/openstack/tests/functional/shared_file_system/v2/test_share_network.py @@ -11,7 +11,7 @@ # under the License. from openstack.shared_file_system.v2 import share_network as _share_network -from openstack.tests.functional.shared_file_system import base +from openstack.tests.functional.shared_file_system.v2 import base class ShareNetworkTest(base.BaseSharedFileSystemTest): diff --git a/openstack/tests/functional/shared_file_system/test_share_network_subnet.py b/openstack/tests/functional/shared_file_system/v2/test_share_network_subnet.py similarity index 97% rename from openstack/tests/functional/shared_file_system/test_share_network_subnet.py rename to openstack/tests/functional/shared_file_system/v2/test_share_network_subnet.py index bfd096f19..b59769a90 100644 --- a/openstack/tests/functional/shared_file_system/test_share_network_subnet.py +++ b/openstack/tests/functional/shared_file_system/v2/test_share_network_subnet.py @@ -13,7 +13,7 @@ from openstack.shared_file_system.v2 import ( share_network_subnet as _share_network_subnet, ) -from openstack.tests.functional.shared_file_system import base +from openstack.tests.functional.shared_file_system.v2 import base class ShareNetworkSubnetTest(base.BaseSharedFileSystemTest): diff --git a/openstack/tests/functional/shared_file_system/test_share_snapshot.py b/openstack/tests/functional/shared_file_system/v2/test_share_snapshot.py similarity index 98% rename from openstack/tests/functional/shared_file_system/test_share_snapshot.py rename to openstack/tests/functional/shared_file_system/v2/test_share_snapshot.py index aed32fa5f..156d3ffb8 100644 --- a/openstack/tests/functional/shared_file_system/test_share_snapshot.py +++ b/openstack/tests/functional/shared_file_system/v2/test_share_snapshot.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.functional.shared_file_system import base +from openstack.tests.functional.shared_file_system.v2 import base class ShareSnapshotTest(base.BaseSharedFileSystemTest): diff --git a/openstack/tests/functional/shared_file_system/test_share_snapshot_instance.py b/openstack/tests/functional/shared_file_system/v2/test_share_snapshot_instance.py similarity index 95% rename from openstack/tests/functional/shared_file_system/test_share_snapshot_instance.py rename to openstack/tests/functional/shared_file_system/v2/test_share_snapshot_instance.py index f426895ff..6c2abda6f 100644 --- a/openstack/tests/functional/shared_file_system/test_share_snapshot_instance.py +++ b/openstack/tests/functional/shared_file_system/v2/test_share_snapshot_instance.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.functional.shared_file_system import base +from openstack.tests.functional.shared_file_system.v2 import base class ShareSnapshotInstanceTest(base.BaseSharedFileSystemTest): diff --git a/openstack/tests/functional/shared_file_system/test_storage_pool.py b/openstack/tests/functional/shared_file_system/v2/test_storage_pool.py similarity index 93% rename from openstack/tests/functional/shared_file_system/test_storage_pool.py rename to openstack/tests/functional/shared_file_system/v2/test_storage_pool.py index f1be9c177..37e8c9f82 100644 --- a/openstack/tests/functional/shared_file_system/test_storage_pool.py +++ b/openstack/tests/functional/shared_file_system/v2/test_storage_pool.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.functional.shared_file_system import base +from openstack.tests.functional.shared_file_system.v2 import base class StoragePoolTest(base.BaseSharedFileSystemTest): diff --git a/openstack/tests/functional/shared_file_system/test_user_message.py b/openstack/tests/functional/shared_file_system/v2/test_user_message.py similarity index 95% rename from openstack/tests/functional/shared_file_system/test_user_message.py rename to openstack/tests/functional/shared_file_system/v2/test_user_message.py index bc8a56de0..1ee76da6d 100644 --- a/openstack/tests/functional/shared_file_system/test_user_message.py +++ b/openstack/tests/functional/shared_file_system/v2/test_user_message.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.tests.functional.shared_file_system import base +from openstack.tests.functional.shared_file_system.v2 import base class UserMessageTest(base.BaseSharedFileSystemTest): From 1029e5d237cc9b610e1962a3dbf60cb2c8e8d681 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 14 Nov 2025 12:37:38 +0000 Subject: [PATCH 3819/3836] proxy: Add api_version attribute Change-Id: I84660e379c78ea5946a3f5cedb3b2cffc81b76b0 Signed-off-by: Stephen Finucane --- openstack/accelerator/v2/_proxy.py | 2 ++ openstack/baremetal/v1/_proxy.py | 2 ++ openstack/baremetal_introspection/v1/_proxy.py | 2 ++ openstack/block_storage/v2/_proxy.py | 2 ++ openstack/block_storage/v3/_proxy.py | 2 ++ openstack/clustering/v1/_proxy.py | 2 ++ openstack/compute/v2/_proxy.py | 2 ++ openstack/container_infrastructure_management/v1/_proxy.py | 2 ++ openstack/database/v1/_proxy.py | 2 ++ openstack/dns/v2/_proxy.py | 2 ++ openstack/identity/v2/_proxy.py | 2 ++ openstack/identity/v3/_proxy.py | 2 ++ openstack/image/v1/_proxy.py | 2 ++ openstack/image/v2/_proxy.py | 2 ++ openstack/instance_ha/v1/_proxy.py | 5 +---- openstack/key_manager/v1/_proxy.py | 2 ++ openstack/load_balancer/v2/_proxy.py | 2 ++ openstack/message/v2/_proxy.py | 2 ++ openstack/network/v2/_proxy.py | 2 ++ openstack/object_store/v1/_proxy.py | 2 ++ openstack/orchestration/v1/_proxy.py | 2 ++ openstack/placement/v1/_proxy.py | 2 ++ openstack/proxy.py | 6 ++++++ openstack/shared_file_system/v2/_proxy.py | 2 ++ openstack/workflow/v2/_proxy.py | 2 ++ 25 files changed, 53 insertions(+), 4 deletions(-) diff --git a/openstack/accelerator/v2/_proxy.py b/openstack/accelerator/v2/_proxy.py index 01c6bb23d..37b69d338 100644 --- a/openstack/accelerator/v2/_proxy.py +++ b/openstack/accelerator/v2/_proxy.py @@ -22,6 +22,8 @@ class Proxy(proxy.Proxy): + api_version = '2' + # ========== Deployables ========== def deployables(self, **query): diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index c519f913e..be3bfe29b 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -34,6 +34,8 @@ class Proxy(proxy.Proxy): + api_version = '1' + retriable_status_codes = _common.RETRIABLE_STATUS_CODES _resource_registry = { diff --git a/openstack/baremetal_introspection/v1/_proxy.py b/openstack/baremetal_introspection/v1/_proxy.py index dad69372c..0991f4dea 100644 --- a/openstack/baremetal_introspection/v1/_proxy.py +++ b/openstack/baremetal_introspection/v1/_proxy.py @@ -27,6 +27,8 @@ class Proxy(proxy.Proxy): + api_version = '1' + _resource_registry = { "introspection": _introspect.Introspection, "introspection_rule": _introspection_rule.IntrospectionRule, diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index adb149d04..febb9c35a 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -34,6 +34,8 @@ class Proxy(proxy.Proxy): + api_version = '2' + # ========== Extensions ========== def extensions(self): diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 2c76de7aa..e3b2d0983 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -42,6 +42,8 @@ class Proxy(proxy.Proxy): + api_version = '3' + _resource_registry = { "availability_zone": availability_zone.AvailabilityZone, "attachment": _attachment.Attachment, diff --git a/openstack/clustering/v1/_proxy.py b/openstack/clustering/v1/_proxy.py index 73c644211..c63bbb39f 100644 --- a/openstack/clustering/v1/_proxy.py +++ b/openstack/clustering/v1/_proxy.py @@ -30,6 +30,8 @@ class Proxy(proxy.Proxy): + api_version = '1' + _resource_registry = { "action": _action.Action, "build_info": build_info.BuildInfo, diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 4e395b13d..9b79eef24 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -49,6 +49,8 @@ class Proxy(proxy.Proxy): + api_version = '2' + _resource_registry = { "aggregate": _aggregate.Aggregate, "availability_zone": availability_zone.AvailabilityZone, diff --git a/openstack/container_infrastructure_management/v1/_proxy.py b/openstack/container_infrastructure_management/v1/_proxy.py index 09e2d864a..6aeab8969 100644 --- a/openstack/container_infrastructure_management/v1/_proxy.py +++ b/openstack/container_infrastructure_management/v1/_proxy.py @@ -29,6 +29,8 @@ class Proxy(proxy.Proxy): + api_version = '1' + _resource_registry = { "cluster": _cluster.Cluster, "cluster_template": _cluster_template.ClusterTemplate, diff --git a/openstack/database/v1/_proxy.py b/openstack/database/v1/_proxy.py index 5e3cba2b8..bf5ba9e34 100644 --- a/openstack/database/v1/_proxy.py +++ b/openstack/database/v1/_proxy.py @@ -21,6 +21,8 @@ class Proxy(proxy.Proxy): + api_version = '1' + _resource_registry = { "database": _database.Database, "flavor": _flavor.Flavor, diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py index 6ebecb37a..db5cff873 100644 --- a/openstack/dns/v2/_proxy.py +++ b/openstack/dns/v2/_proxy.py @@ -31,6 +31,8 @@ class Proxy(proxy.Proxy): + api_version = '2' + _resource_registry = { "blacklist": _blacklist.Blacklist, "floating_ip": _fip.FloatingIP, diff --git a/openstack/identity/v2/_proxy.py b/openstack/identity/v2/_proxy.py index e61143821..778ad9002 100644 --- a/openstack/identity/v2/_proxy.py +++ b/openstack/identity/v2/_proxy.py @@ -21,6 +21,8 @@ class Proxy(proxy.Proxy): + api_version = '2' + def extensions(self): """Retrieve a generator of extensions diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index eeebf0f7e..fbcb0ff27 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -64,6 +64,8 @@ class Proxy(proxy.Proxy): + api_version = '3' + _resource_registry = { "application_credential": _application_credential.ApplicationCredential, # noqa: E501 "access_rule": _access_rule.AccessRule, diff --git a/openstack/image/v1/_proxy.py b/openstack/image/v1/_proxy.py index 9736ab289..5bdb90d85 100644 --- a/openstack/image/v1/_proxy.py +++ b/openstack/image/v1/_proxy.py @@ -37,6 +37,8 @@ def _get_name_and_filename(name, image_format): class Proxy(proxy.Proxy): + api_version = '1' + retriable_status_codes = [503] _IMAGE_MD5_KEY = 'owner_specified.openstack.md5' diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 1b020c17a..b2082084e 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -54,6 +54,8 @@ def _get_name_and_filename(name, image_format): class Proxy(proxy.Proxy): + api_version = '2' + _resource_registry = { "cache": _cache.Cache, "image": _image.Image, diff --git a/openstack/instance_ha/v1/_proxy.py b/openstack/instance_ha/v1/_proxy.py index de3f37af8..88c6f760d 100644 --- a/openstack/instance_ha/v1/_proxy.py +++ b/openstack/instance_ha/v1/_proxy.py @@ -24,10 +24,7 @@ class Proxy(proxy.Proxy): - """Proxy class for ha resource handling. - - Create method for each action of each API. - """ + api_version = '1' _resource_registry = { "host": _host.Host, diff --git a/openstack/key_manager/v1/_proxy.py b/openstack/key_manager/v1/_proxy.py index fe668633f..0c5924712 100644 --- a/openstack/key_manager/v1/_proxy.py +++ b/openstack/key_manager/v1/_proxy.py @@ -22,6 +22,8 @@ class Proxy(proxy.Proxy): + api_version = '1' + _resource_registry = { "container": _container.Container, "order": _order.Order, diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index 1e34765ce..69769e111 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -33,6 +33,8 @@ class Proxy(proxy.Proxy): + api_version = '2' + _resource_registry = { "amphora": _amphora.Amphora, "availability_zone": _availability_zone.AvailabilityZone, diff --git a/openstack/message/v2/_proxy.py b/openstack/message/v2/_proxy.py index db5c11f28..0c3a32d55 100644 --- a/openstack/message/v2/_proxy.py +++ b/openstack/message/v2/_proxy.py @@ -21,6 +21,8 @@ class Proxy(proxy.Proxy): + api_version = '2' + _resource_registry = { "claim": _claim.Claim, "message": _message.Message, diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 9177ee798..bbd5ab408 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -109,6 +109,8 @@ class Proxy(proxy.Proxy): + api_version = '2' + _resource_registry = { "address_group": _address_group.AddressGroup, "address_scope": _address_scope.AddressScope, diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index fb93aa744..1995d085a 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -43,6 +43,8 @@ def _get_expiration(expiration): class Proxy(proxy.Proxy): + api_version = '1' + _resource_registry = { "account": _account.Account, "container": _container.Container, diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index cc2749731..541678398 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -30,6 +30,8 @@ # TODO(rladntjr4): Some of these methods support lookup by ID, while others # support lookup by ID or name. We should choose one and use it consistently. class Proxy(proxy.Proxy): + api_version = '1' + _resource_registry = { "resource": _resource.Resource, "software_config": _sc.SoftwareConfig, diff --git a/openstack/placement/v1/_proxy.py b/openstack/placement/v1/_proxy.py index b3496c897..56cc733c4 100644 --- a/openstack/placement/v1/_proxy.py +++ b/openstack/placement/v1/_proxy.py @@ -23,6 +23,8 @@ class Proxy(proxy.Proxy): + api_version = '1' + _resource_registry = { "resource_class": _resource_class.ResourceClass, "resource_provider": _resource_provider.ResourceProvider, diff --git a/openstack/proxy.py b/openstack/proxy.py index 63227316f..ca04e2ed6 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -64,6 +64,12 @@ class CleanupDependency(ty.TypedDict): class Proxy(adapter.Adapter): """Represents a service.""" + api_version: ty.ClassVar[str] + """The API version. + + This is used as a descriminating attribute for type checking. + """ + retriable_status_codes: list[int] | None = None """HTTP status codes that should be retried by default. diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index 2c74894f2..c38cd41d7 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -46,6 +46,8 @@ class Proxy(proxy.Proxy): + api_version = '2' + _resource_registry = { "availability_zone": _availability_zone.AvailabilityZone, "share_snapshot": _share_snapshot.ShareSnapshot, diff --git a/openstack/workflow/v2/_proxy.py b/openstack/workflow/v2/_proxy.py index 8fa7bb336..bae87831d 100644 --- a/openstack/workflow/v2/_proxy.py +++ b/openstack/workflow/v2/_proxy.py @@ -20,6 +20,8 @@ class Proxy(proxy.Proxy): + api_version = '2' + _resource_registry = { "execution": _execution.Execution, "workflow": _workflow.Workflow, From bf751f5e16eacc49818f738afdfd28ab4f36e8cf Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 26 Nov 2025 21:43:20 +0000 Subject: [PATCH 3820/3836] service_description: Trivial fixups Reduce some indentation and reorder some parameters to match the signature of the called function. Change-Id: I1aad0c7544d6775b825dedd3000a7949ac22b648 Signed-off-by: Stephen Finucane --- openstack/service_description.py | 88 ++++++++++++++++++-------------- 1 file changed, 50 insertions(+), 38 deletions(-) diff --git a/openstack/service_description.py b/openstack/service_description.py index 8f0ace1f2..014daae7b 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -87,31 +87,40 @@ def __init__( def __get__(self, instance, owner): if instance is None: return self - if self.service_type not in instance._proxies: - proxy = self._make_proxy(instance) - if not isinstance(proxy, _ServiceDisabledProxyShim): - # The keystone proxy has a method called get_endpoint - # that is about managing keystone endpoints. This is - # unfortunate. - try: - endpoint = proxy_mod.Proxy.get_endpoint(proxy) - except IndexError: - # It's best not to look to closely here. This is - # to support old placement. - # There was a time when it had no status entry - # in its version discovery doc (OY) In this case, - # no endpoints get through version discovery - # filtering. In order to deal with that, catch - # the IndexError thrown by keystoneauth and - # set an endpoint_override for the user to the - # url in the catalog and try again. - self._set_override_from_catalog(instance.config) - proxy = self._make_proxy(instance) - endpoint = proxy_mod.Proxy.get_endpoint(proxy) - if instance._strict_proxies: - self._validate_proxy(proxy, endpoint) - proxy._connection = instance + + if self.service_type in instance._proxies: + return instance._proxies[self.service_type] + + proxy = self._make_proxy(instance) + if isinstance(proxy, _ServiceDisabledProxyShim): instance._proxies[self.service_type] = proxy + return instance._proxies[self.service_type] + + # The keystone proxy has a method called get_endpoint + # that is about managing keystone endpoints. This is + # unfortunate. + try: + endpoint = proxy_mod.Proxy.get_endpoint(proxy) + except IndexError: + # It's best not to look to closely here. This is + # to support old placement. + # There was a time when it had no status entry + # in its version discovery doc (OY) In this case, + # no endpoints get through version discovery + # filtering. In order to deal with that, catch + # the IndexError thrown by keystoneauth and + # set an endpoint_override for the user to the + # url in the catalog and try again. + self._set_override_from_catalog(instance.config) + proxy = self._make_proxy(instance) + endpoint = proxy_mod.Proxy.get_endpoint(proxy) + + if instance._strict_proxies: + self._validate_proxy(proxy, endpoint) + + proxy._connection = instance + + instance._proxies[self.service_type] = proxy return instance._proxies[self.service_type] def _set_override_from_catalog(self, config): @@ -146,19 +155,20 @@ def _validate_proxy(self, proxy, endpoint): def _make_proxy(self, instance): """Create a Proxy for the service in question. - :param instance: - The `openstack.connection.Connection` we're working with. + :param instance: The `openstack.connection.Connection` we're working + with. """ config = instance.config + # This is not a valid service. if not config.has_service(self.service_type): return _ServiceDisabledProxyShim( self.service_type, config.get_disabled_reason(self.service_type), ) - # We don't know anything about this service, so the user is - # explicitly just using us for a passthrough REST adapter. + # This is a valid service type, but we don't know anything about it so + # the user is explicitly just using us for a passthrough REST adapter. # Skip all the lower logic. if not self.supported_versions: temp_client = config.get_session_client( @@ -167,8 +177,8 @@ def _make_proxy(self, instance): ) return temp_client - # Check to see if we've got config that matches what we - # understand in the SDK. + # Check to see if we've got config that matches what we understand in + # the SDK. version_string = config.get_api_version(self.service_type) endpoint_override = config.get_endpoint(self.service_type) @@ -179,8 +189,8 @@ def _make_proxy(self, instance): proxy_obj = None if endpoint_override and version_string: - # Both endpoint override and version_string are set, we don't - # need to do discovery - just trust the user. + # Both endpoint override and version_string are set. We therefore + # don't need to do discovery: just trust the user. proxy_class = self.supported_versions.get(version_string[0]) if proxy_class: proxy_obj = config.get_session_client( @@ -271,11 +281,11 @@ def _make_proxy(self, instance): return config.get_session_client( self.service_type, - allow_version_hack=True, + version=version_string, constructor=self.supported_versions[ str(supported_versions[0]) ], - version=version_string, + allow_version_hack=True, ) else: version_kwargs['min_version'] = str(supported_versions[0]) @@ -284,7 +294,9 @@ def _make_proxy(self, instance): ) temp_adapter = config.get_session_client( - self.service_type, allow_version_hack=True, **version_kwargs + self.service_type, + allow_version_hack=True, + **version_kwargs, ) found_version = temp_adapter.get_api_major_version() if found_version is None: @@ -305,8 +317,8 @@ def _make_proxy(self, instance): if proxy_class: return config.get_session_client( self.service_type, - allow_version_hack=True, constructor=proxy_class, + allow_version_hack=True, **version_kwargs, ) @@ -332,8 +344,8 @@ def __delete__(self, instance): # downstream we need to allow overriding default implementation by # deleting service_type attribute of the connection and then # "add_service" with new implementation. - # This is implemented explicitely not very comfortable to use - # to show how bad it is not to contribute changes back + # This is intentionally designed to be hard to use to show how bad it + # is not to contribute changes back for service_type in self.all_types: if service_type in instance._proxies: del instance._proxies[service_type] From a2d2c6ec62d809c3537f9524e8e1e81df71ce434 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 26 Nov 2025 22:01:48 +0000 Subject: [PATCH 3821/3836] service_description: Add missing None checks Service discovery can fail. Be more careful in handling cases where that happens. Change-Id: Ie35005d52e65c0eb4abfd68c335e64603d404733 Signed-off-by: Stephen Finucane --- openstack/service_description.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/openstack/service_description.py b/openstack/service_description.py index 014daae7b..67d051069 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -207,7 +207,20 @@ def _make_proxy(self, instance): ) elif endpoint_override: temp_adapter = config.get_session_client(self.service_type) - api_version = temp_adapter.get_endpoint_data().api_version + endpoint_data = temp_adapter.get_endpoint_data() + if not endpoint_data: + raise exceptions.ServiceDiscoveryException( + f"Failed to create a working proxy for service " + f"{self.service_type}: No endpoint data found." + ) + + api_version = endpoint_data.api_version + if not api_version: + raise exceptions.ServiceDiscoveryException( + f"Failed to create a working proxy for service " + f"{self.service_type}: No version in endpoint data." + ) + proxy_class = self.supported_versions.get(str(api_version[0])) if proxy_class: proxy_obj = config.get_session_client( @@ -235,7 +248,7 @@ def _make_proxy(self, instance): data = proxy_obj.get_endpoint_data() if not data and instance._strict_proxies: raise exceptions.ServiceDiscoveryException( - "Failed to create a working proxy for service " + f"Failed to create a working proxy for service " f"{self.service_type}: No endpoint data found." ) @@ -254,6 +267,7 @@ def _make_proxy(self, instance): self.service_type, constructor=proxy_class, ) + return proxy_obj # Make an adapter to let discovery take over From e7c009883d01a04f06477ee7b289df8082316e28 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 26 Nov 2025 22:08:41 +0000 Subject: [PATCH 3822/3836] service_description: Avoid use of built kwargs It's a little more duplication but it's *much* easier to type. We also simplify an exception conditional: version information was *always* set so they're only one path possible. Change-Id: Ib1c81a304ea4fdb372b5ad32aa819fb47b96513b Signed-off-by: Stephen Finucane --- openstack/service_description.py | 50 +++++++++++++++++--------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/openstack/service_description.py b/openstack/service_description.py index 67d051069..c85d40823 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -271,10 +271,8 @@ def _make_proxy(self, instance): return proxy_obj # Make an adapter to let discovery take over - version_kwargs = {} supported_versions = sorted([int(f) for f in self.supported_versions]) if version_string: - version_kwargs['version'] = version_string if getattr( self.supported_versions[str(supported_versions[0])], 'skip_discovery', @@ -301,39 +299,45 @@ def _make_proxy(self, instance): ], allow_version_hack=True, ) + + temp_adapter = config.get_session_client( + self.service_type, + allow_version_hack=True, + version=version_string, + ) else: - version_kwargs['min_version'] = str(supported_versions[0]) - version_kwargs['max_version'] = ( - f'{supported_versions[-1]!s}.latest' + temp_adapter = config.get_session_client( + self.service_type, + allow_version_hack=True, + max_version=f'{supported_versions[-1]!s}.latest', + min_version=f'{supported_versions[0]!s}', ) - temp_adapter = config.get_session_client( - self.service_type, - allow_version_hack=True, - **version_kwargs, - ) found_version = temp_adapter.get_api_major_version() if found_version is None: region_name = instance.config.get_region_name(self.service_type) - if version_kwargs: - raise exceptions.NotSupported( - f"The {self.service_type} service for " - f"{instance.name}:{region_name} exists but does not have " - f"any supported versions." - ) - else: - raise exceptions.NotSupported( - f"The {self.service_type} service for " - f"{instance.name}:{region_name} exists but no version " - f"was discoverable." - ) + raise exceptions.NotSupported( + f"The {self.service_type} service for " + f"{instance.name}:{region_name} exists but does not have " + f"any supported versions." + ) + proxy_class = self.supported_versions.get(str(found_version[0])) if proxy_class: + if version_string: + return config.get_session_client( + self.service_type, + constructor=proxy_class, + allow_version_hack=True, + version=version_string, + ) + return config.get_session_client( self.service_type, constructor=proxy_class, allow_version_hack=True, - **version_kwargs, + max_version=f'{supported_versions[-1]!s}.latest', + min_version=f'{supported_versions[0]!s}', ) # No proxy_class From 8334b199c5de464961fe9d9ba17fc0eabfc4d869 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 26 Feb 2026 00:35:09 +0000 Subject: [PATCH 3823/3836] utils: Add ensure_service_version helper This allows us to narrow the type of the Proxy APIs without isinstance checks or assertions. This is an alternative to replacing e.g. `block_storage` with `block_storage_v2` / `block_storage_v3` or implementing identical methods across the different Proxy implementations (which I suspect was the original plan, but it hasn't worked out). Change-Id: I07941d2036a46d5345841f7cbb163e1386494107 Signed-off-by: Stephen Finucane --- openstack/cloud/_block_storage.py | 18 +- openstack/cloud/_compute.py | 19 +- openstack/cloud/_identity.py | 252 ++++++++++++------ openstack/cloud/_network.py | 10 +- .../tests/functional/block_storage/v3/base.py | 12 + .../block_storage/v3/test_attachment.py | 4 +- .../v3/test_availability_zone.py | 7 +- .../v3/test_block_storage_summary.py | 2 +- .../block_storage/v3/test_default_type.py | 15 +- .../functional/block_storage/v3/test_group.py | 66 ++--- .../block_storage/v3/test_resource_filters.py | 2 +- .../block_storage/v3/test_service.py | 10 +- .../block_storage/v3/test_volume.py | 16 +- .../tests/functional/cloud/test_endpoints.py | 21 +- openstack/tests/functional/compute/v2/base.py | 32 +++ .../functional/compute/v2/test_extension.py | 2 +- .../functional/compute/v2/test_flavor.py | 52 ++-- .../functional/compute/v2/test_hypervisor.py | 10 +- .../tests/functional/compute/v2/test_image.py | 38 +-- .../functional/compute/v2/test_keypair.py | 16 +- .../functional/compute/v2/test_limits.py | 2 +- .../functional/compute/v2/test_quota_set.py | 11 +- .../functional/compute/v2/test_server.py | 66 ++--- .../functional/compute/v2/test_service.py | 4 +- .../compute/v2/test_volume_attachment.py | 37 ++- .../tests/functional/identity/v3/base.py | 32 +++ .../identity/v3/test_access_rule.py | 18 +- .../v3/test_application_credential.py | 18 +- .../identity/v3/test_domain_config.py | 16 +- .../functional/identity/v3/test_endpoint.py | 36 ++- .../functional/identity/v3/test_group.py | 30 +-- .../functional/identity/v3/test_limit.py | 26 +- .../functional/identity/v3/test_project.py | 18 +- .../identity/v3/test_registered_limit.py | 22 +- .../tests/functional/identity/v3/test_user.py | 14 +- openstack/tests/functional/image/v2/base.py | 11 + .../tests/functional/image/v2/test_image.py | 28 +- .../tests/functional/image/v2/test_member.py | 18 +- .../image/v2/test_metadef_namespace.py | 38 ++- .../image/v2/test_metadef_object.py | 18 +- .../image/v2/test_metadef_property.py | 20 +- .../image/v2/test_metadef_resource_type.py | 16 +- .../image/v2/test_metadef_schema.py | 24 +- .../tests/functional/image/v2/test_schema.py | 8 +- .../tests/functional/image/v2/test_task.py | 2 +- .../key_manager/v1/test_project_quota.py | 25 +- .../network/v2/test_address_group.py | 1 + .../tests/functional/network/v2/test_port.py | 1 + openstack/utils.py | 75 ++++++ 49 files changed, 746 insertions(+), 493 deletions(-) create mode 100644 openstack/tests/functional/identity/v3/base.py diff --git a/openstack/cloud/_block_storage.py b/openstack/cloud/_block_storage.py index f38afa8f5..687934fb9 100644 --- a/openstack/cloud/_block_storage.py +++ b/openstack/cloud/_block_storage.py @@ -16,6 +16,7 @@ from openstack.cloud import openstackcloud from openstack import exceptions from openstack import resource +from openstack import utils from openstack import warnings as os_warnings @@ -205,7 +206,8 @@ def update_volume(self, name_or_id, **kwargs): if not volume: raise exceptions.SDKException(f"Volume {name_or_id} not found.") - volume = self.block_storage.update_volume(volume, **kwargs) + block_storage = utils.ensure_service_version(self.block_storage, '3') + volume = block_storage.update_volume(volume, **kwargs) return volume @@ -306,9 +308,8 @@ def get_volume_limits(self, name_or_id=None): """ params = {} if name_or_id: - project = self.identity.find_project( - name_or_id, ignore_missing=False - ) + identity = utils.ensure_service_version(self.identity, '3') + project = identity.find_project(name_or_id, ignore_missing=False) params['project'] = project return self.block_storage.get_limits(**params) @@ -901,7 +902,8 @@ def set_volume_quotas(self, name_or_id, **kwargs): :raises: :class:`~openstack.exceptions.SDKException` if the resource to set the quota does not exist. """ - project = self.identity.find_project(name_or_id, ignore_missing=False) + identity = utils.ensure_service_version(self.identity, '3') + project = identity.find_project(name_or_id, ignore_missing=False) self.block_storage.update_quota_set(project=project, **kwargs) @@ -914,7 +916,8 @@ def get_volume_quotas(self, name_or_id): :raises: :class:`~openstack.exceptions.SDKException` if it's not a valid project """ - proj = self.identity.find_project(name_or_id, ignore_missing=False) + identity = utils.ensure_service_version(self.identity, '3') + proj = identity.find_project(name_or_id, ignore_missing=False) return self.block_storage.get_quota_set(proj) @@ -927,6 +930,7 @@ def delete_volume_quotas(self, name_or_id): :raises: :class:`~openstack.exceptions.SDKException` if it's not a valid project or the call failed """ - proj = self.identity.find_project(name_or_id, ignore_missing=False) + identity = utils.ensure_service_version(self.identity, '3') + proj = identity.find_project(name_or_id, ignore_missing=False) return self.block_storage.revert_quota_set(proj) diff --git a/openstack/cloud/_compute.py b/openstack/cloud/_compute.py index 02619599e..1093d61d5 100644 --- a/openstack/cloud/_compute.py +++ b/openstack/cloud/_compute.py @@ -415,9 +415,8 @@ def get_compute_limits(self, name_or_id=None): """ params = {} if name_or_id: - project = self.identity.find_project( - name_or_id, ignore_missing=False - ) + identity = utils.ensure_service_version(self.identity, '3') + project = identity.find_project(name_or_id, ignore_missing=False) params['tenant_id'] = project.id return self.compute.get_limits(**params).absolute @@ -1372,7 +1371,7 @@ def rebuild_server( admin_pass = server.get('adminPass') or admin_pass server = self.compute.wait_for_server(server, wait=timeout) if server['status'] == 'ACTIVE': - server.adminPass = admin_pass + server.admin_password = admin_pass return self._expand_server(server, detailed=detailed, bare=bare) @@ -1889,7 +1888,8 @@ def set_compute_quotas(self, name_or_id, **kwargs): :raises: :class:`~openstack.exceptions.SDKException` if the resource to set the quota does not exist. """ - project = self.identity.find_project(name_or_id, ignore_missing=False) + identity = utils.ensure_service_version(self.identity, '3') + project = identity.find_project(name_or_id, ignore_missing=False) kwargs['force'] = True self.compute.update_quota_set(project=project, **kwargs) @@ -1902,7 +1902,8 @@ def get_compute_quotas(self, name_or_id): :raises: :class:`~openstack.exceptions.SDKException` if it's not a valid project """ - proj = self.identity.find_project(name_or_id, ignore_missing=False) + identity = utils.ensure_service_version(self.identity, '3') + proj = identity.find_project(name_or_id, ignore_missing=False) return self.compute.get_quota_set(proj) def delete_compute_quotas(self, name_or_id): @@ -1913,7 +1914,8 @@ def delete_compute_quotas(self, name_or_id): :raises: :class:`~openstack.exceptions.SDKException` if it's not a valid project or the nova client call failed """ - proj = self.identity.find_project(name_or_id, ignore_missing=False) + identity = utils.ensure_service_version(self.identity, '3') + proj = identity.find_project(name_or_id, ignore_missing=False) self.compute.revert_quota_set(proj) def get_compute_usage(self, name_or_id, start=None, end=None): @@ -1948,7 +1950,8 @@ def parse_date(date): if isinstance(end, str): end = parse_date(end) - project = self.identity.find_project(name_or_id, ignore_missing=False) + identity = utils.ensure_service_version(self.identity, '3') + project = identity.find_project(name_or_id, ignore_missing=False) return self.compute.get_usage(project, start, end) diff --git a/openstack/cloud/_identity.py b/openstack/cloud/_identity.py index b074af6ae..c8036e660 100644 --- a/openstack/cloud/_identity.py +++ b/openstack/cloud/_identity.py @@ -85,6 +85,8 @@ def list_projects(self, domain_id=None, name_or_id=None, filters=None): :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ + identity = utils.ensure_service_version(self.identity, '3') + if not filters: filters = {} query = dict(**filters) @@ -93,7 +95,7 @@ def list_projects(self, domain_id=None, name_or_id=None, filters=None): if domain_id: query['domain_id'] = domain_id - return list(self.identity.projects(**query)) + return list(identity.projects(**query)) def search_projects(self, name_or_id=None, filters=None, domain_id=None): """Backwards compatibility method for search_projects @@ -144,6 +146,8 @@ def get_project(self, name_or_id, filters=None, domain_id=None): :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ + identity = utils.ensure_service_version(self.identity, '3') + if filters is not None: warnings.warn( "the 'filters' argument is deprecated; use " @@ -163,7 +167,7 @@ def get_project(self, name_or_id, filters=None, domain_id=None): return entities[0] - return self.identity.find_project( + return identity.find_project( name_or_id=name_or_id, domain_id=domain_id ) @@ -181,7 +185,9 @@ def update_project( :param domain_id: Domain ID to scope the retrieved project. :returns: An identity ``Project`` object. """ - project = self.identity.find_project( + identity = utils.ensure_service_version(self.identity, '3') + + project = identity.find_project( name_or_id=name_or_id, domain_id=domain_id, ignore_missing=False, @@ -190,7 +196,7 @@ def update_project( raise exceptions.SDKException(f"Project {name_or_id} not found.") if enabled is not None: kwargs.update({'enabled': enabled}) - project = self.identity.update_project(project, **kwargs) + project = identity.update_project(project, **kwargs) return project def create_project( @@ -209,6 +215,8 @@ def create_project( :param enabled: :returns: An identity ``Project`` object. """ + identity = utils.ensure_service_version(self.identity, '3') + attrs = dict( name=name, description=description, @@ -217,7 +225,7 @@ def create_project( ) if kwargs: attrs.update(kwargs) - return self.identity.create_project(**attrs) + return identity.create_project(**attrs) def delete_project(self, name_or_id, domain_id=None): """Delete a project. @@ -229,14 +237,16 @@ def delete_project(self, name_or_id, domain_id=None): :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call """ + identity = utils.ensure_service_version(self.identity, '3') + try: - project = self.identity.find_project( + project = identity.find_project( name_or_id=name_or_id, domain_id=domain_id, ignore_missing=True ) if not project: self.log.debug("Project %s not found for deleting", name_or_id) return False - self.identity.delete_project(project) + identity.delete_project(project) return True except exceptions.SDKException: self.log.exception(f"Error in deleting project {name_or_id}") @@ -253,7 +263,9 @@ def list_users(self, **kwargs): :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ - return list(self.identity.users(**kwargs)) + identity = utils.ensure_service_version(self.identity, '3') + + return list(identity.users(**kwargs)) def search_users(self, name_or_id=None, filters=None, domain_id=None): """Search users. @@ -311,6 +323,8 @@ def get_user(self, name_or_id, filters=None, domain_id=None): :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ + identity = utils.ensure_service_version(self.identity, '3') + if filters is not None: warnings.warn( "the 'filters' argument is deprecated; use " @@ -328,7 +342,7 @@ def get_user(self, name_or_id, filters=None, domain_id=None): f"Multiple matches found for {name_or_id}", ) - return self.identity.find_user(name_or_id, domain_id=domain_id) + return identity.find_user(name_or_id, domain_id=domain_id) # TODO(stephenfin): Remove normalize since it doesn't do anything def get_user_by_id(self, user_id, normalize=None): @@ -337,6 +351,8 @@ def get_user_by_id(self, user_id, normalize=None): :param string user_id: user ID :returns: an identity ``User`` object """ + identity = utils.ensure_service_version(self.identity, '3') + if normalize is not None: warnings.warn( "The 'normalize' field is unnecessary and will be removed in " @@ -344,7 +360,7 @@ def get_user_by_id(self, user_id, normalize=None): os_warnings.RemovedInSDK60Warning, ) - return self.identity.get_user(user_id) + return identity.get_user(user_id) @_utils.valid_kwargs( 'name', @@ -356,6 +372,8 @@ def get_user_by_id(self, user_id, normalize=None): 'default_project', ) def update_user(self, name_or_id, **kwargs): + identity = utils.ensure_service_version(self.identity, '3') + user_kwargs = {} if kwargs.get('domain_id'): user_kwargs['domain_id'] = kwargs['domain_id'] @@ -368,7 +386,7 @@ def update_user(self, name_or_id, **kwargs): # if None. keystoneclient drops keys with None values. if 'domain_id' in kwargs and kwargs['domain_id'] is None: del kwargs['domain_id'] - user = self.identity.update_user(user, **kwargs) + user = identity.update_user(user, **kwargs) return user @@ -383,6 +401,8 @@ def create_user( description=None, ): """Create a user.""" + identity = utils.ensure_service_version(self.identity, '3') + params = self._get_identity_params(domain_id, default_project) params.update({'name': name, 'email': email, 'enabled': enabled}) if password is not None: @@ -390,19 +410,21 @@ def create_user( if description is not None: params['description'] = description - user = self.identity.create_user(**params) + user = identity.create_user(**params) return user @_utils.valid_kwargs('domain_id') def delete_user(self, name_or_id, **kwargs): + identity = utils.ensure_service_version(self.identity, '3') + try: user = self.get_user(name_or_id, **kwargs) if not user: self.log.debug(f"User {name_or_id} not found for deleting") return False - self.identity.delete_user(user) + identity.delete_user(user) return True except exceptions.SDKException: @@ -431,9 +453,11 @@ def add_user_to_group(self, name_or_id, group_name_or_id): :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call """ + identity = utils.ensure_service_version(self.identity, '3') + user, group = self._get_user_and_group(name_or_id, group_name_or_id) - self.identity.add_user_to_group(user, group) + identity.add_user_to_group(user, group) def is_user_in_group(self, name_or_id, group_name_or_id): """Check to see if a user is in a group. @@ -445,9 +469,11 @@ def is_user_in_group(self, name_or_id, group_name_or_id): :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call """ + identity = utils.ensure_service_version(self.identity, '3') + user, group = self._get_user_and_group(name_or_id, group_name_or_id) - return self.identity.check_user_in_group(user, group) + return identity.check_user_in_group(user, group) def remove_user_from_group(self, name_or_id, group_name_or_id): """Remove a user from a group. @@ -458,9 +484,11 @@ def remove_user_from_group(self, name_or_id, group_name_or_id): :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call """ + identity = utils.ensure_service_version(self.identity, '3') + user, group = self._get_user_and_group(name_or_id, group_name_or_id) - self.identity.remove_user_from_group(user, group) + identity.remove_user_from_group(user, group) @_utils.valid_kwargs('type', 'service_type', 'description') def create_service(self, name, enabled=True, **kwargs): @@ -476,6 +504,8 @@ def create_service(self, name, enabled=True, **kwargs): :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ + identity = utils.ensure_service_version(self.identity, '3') + type_ = kwargs.pop('type', None) service_type = kwargs.pop('service_type', None) @@ -485,12 +515,14 @@ def create_service(self, name, enabled=True, **kwargs): kwargs['is_enabled'] = enabled kwargs['name'] = name - return self.identity.create_service(**kwargs) + return identity.create_service(**kwargs) @_utils.valid_kwargs( 'name', 'enabled', 'type', 'service_type', 'description' ) def update_service(self, name_or_id, **kwargs): + identity = utils.ensure_service_version(self.identity, '3') + # NOTE(SamYaple): Keystone v3 only accepts 'type' but shade accepts # both 'type' and 'service_type' with a preference # towards 'type' @@ -500,7 +532,7 @@ def update_service(self, name_or_id, **kwargs): kwargs['type'] = type_ or service_type service = self.get_service(name_or_id) - return self.identity.update_service(service, **kwargs) + return identity.update_service(service, **kwargs) def list_services(self): """List all Keystone services. @@ -509,7 +541,9 @@ def list_services(self): :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ - return list(self.identity.services()) + identity = utils.ensure_service_version(self.identity, '3') + + return list(identity.services()) def search_services(self, name_or_id=None, filters=None): """Search Keystone services. @@ -546,6 +580,8 @@ def get_service(self, name_or_id, filters=None): wrong during the OpenStack API call or if multiple matches are found. """ + identity = utils.ensure_service_version(self.identity, '3') + if filters is not None: warnings.warn( "the 'filters' argument is deprecated; use " @@ -563,7 +599,7 @@ def get_service(self, name_or_id, filters=None): return entities[0] - return self.identity.find_service(name_or_id=name_or_id) + return identity.find_service(name_or_id=name_or_id) def delete_service(self, name_or_id): """Delete a Keystone service. @@ -574,13 +610,15 @@ def delete_service(self, name_or_id): :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call """ + identity = utils.ensure_service_version(self.identity, '3') + service = self.get_service(name_or_id=name_or_id) if service is None: self.log.debug("Service %s not found for deleting", name_or_id) return False try: - self.identity.delete_service(service) + identity.delete_service(service) return True except exceptions.SDKException: self.log.exception( @@ -614,6 +652,8 @@ def create_endpoint( cannot be found or if something goes wrong during the OpenStack API call. """ + identity = utils.ensure_service_version(self.identity, '3') + public_url = kwargs.pop('public_url', None) internal_url = kwargs.pop('internal_url', None) admin_url = kwargs.pop('admin_url', None) @@ -666,20 +706,22 @@ def create_endpoint( endpoints = [] for args in endpoints_args: - endpoints.append(self.identity.create_endpoint(**args)) + endpoints.append(identity.create_endpoint(**args)) return endpoints @_utils.valid_kwargs( 'enabled', 'service_name_or_id', 'url', 'interface', 'region' ) def update_endpoint(self, endpoint_id, **kwargs): + identity = utils.ensure_service_version(self.identity, '3') + service_name_or_id = kwargs.pop('service_name_or_id', None) if service_name_or_id is not None: kwargs['service_id'] = service_name_or_id if 'region' in kwargs: kwargs['region_id'] = kwargs.pop('region') - return self.identity.update_endpoint(endpoint_id, **kwargs) + return identity.update_endpoint(endpoint_id, **kwargs) def list_endpoints(self): """List Keystone endpoints. @@ -688,7 +730,9 @@ def list_endpoints(self): :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ - return list(self.identity.endpoints()) + identity = utils.ensure_service_version(self.identity, '3') + + return list(identity.endpoints()) def search_endpoints(self, id=None, filters=None): """List Keystone endpoints. @@ -725,6 +769,8 @@ def get_endpoint(self, id, filters=None): :param id: ID of endpoint. :returns: An identity ``Endpoint`` object """ + identity = utils.ensure_service_version(self.identity, '3') + if filters is not None: warnings.warn( "the 'filters' argument is deprecated; use " @@ -742,7 +788,7 @@ def get_endpoint(self, id, filters=None): return entities[0] - return self.identity.find_endpoint(name_or_id=id) + return identity.find_endpoint(name_or_id=id) def delete_endpoint(self, id): """Delete a Keystone endpoint. @@ -753,13 +799,15 @@ def delete_endpoint(self, id): :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ + identity = utils.ensure_service_version(self.identity, '3') + endpoint = self.get_endpoint(id=id) if endpoint is None: self.log.debug("Endpoint %s not found for deleting", id) return False try: - self.identity.delete_endpoint(id) + identity.delete_endpoint(id) return True except exceptions.SDKException: self.log.exception(f"Failed to delete endpoint {id}") @@ -775,10 +823,12 @@ def create_domain(self, name, description=None, enabled=True): :raises: :class:`~openstack.exceptions.SDKException` if the domain cannot be created. """ + identity = utils.ensure_service_version(self.identity, '3') + domain_ref = {'name': name, 'enabled': enabled} if description is not None: domain_ref['description'] = description - return self.identity.create_domain(**domain_ref) + return identity.create_domain(**domain_ref) # TODO(stephenfin): domain_id and name_or_id are the same thing now; # deprecate one of them @@ -801,6 +851,8 @@ def update_domain( :raises: :class:`~openstack.exceptions.SDKException` if the domain cannot be updated """ + identity = utils.ensure_service_version(self.identity, '3') + if domain_id is None: if name_or_id is None: raise exceptions.SDKException( @@ -817,7 +869,7 @@ def update_domain( domain_ref.update({'name': name} if name else {}) domain_ref.update({'description': description} if description else {}) domain_ref.update({'enabled': enabled} if enabled is not None else {}) - return self.identity.update_domain(domain_id, **domain_ref) + return identity.update_domain(domain_id, **domain_ref) # TODO(stephenfin): domain_id and name_or_id are the same thing now; # deprecate one of them @@ -831,6 +883,8 @@ def delete_domain(self, domain_id=None, name_or_id=None): :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ + identity = utils.ensure_service_version(self.identity, '3') + try: if domain_id is None: if name_or_id is None: @@ -846,8 +900,8 @@ def delete_domain(self, domain_id=None, name_or_id=None): domain_id = dom['id'] # A domain must be disabled before deleting - self.identity.update_domain(domain_id, is_enabled=False) - self.identity.delete_domain(domain_id, ignore_missing=False) + identity.update_domain(domain_id, is_enabled=False) + identity.delete_domain(domain_id, ignore_missing=False) return True except exceptions.SDKException: @@ -861,7 +915,9 @@ def list_domains(self, **filters): :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ - return list(self.identity.domains(**filters)) + identity = utils.ensure_service_version(self.identity, '3') + + return list(identity.domains(**filters)) # TODO(stephenfin): These arguments are backwards from everything else. def search_domains(self, filters=None, name_or_id=None): @@ -918,6 +974,8 @@ def get_domain(self, domain_id=None, name_or_id=None, filters=None): :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ + identity = utils.ensure_service_version(self.identity, '3') + if filters is not None: warnings.warn( "The 'filters' argument is deprecated for removal. It is a " @@ -926,9 +984,9 @@ def get_domain(self, domain_id=None, name_or_id=None, filters=None): ) if domain_id is None: - return self.identity.find_domain(name_or_id, ignore_missing=True) + return identity.find_domain(name_or_id, ignore_missing=True) else: - return self.identity.get_domain(domain_id) + return identity.get_domain(domain_id) @_utils.valid_kwargs('domain_id') def list_groups(self, **kwargs): @@ -940,7 +998,9 @@ def list_groups(self, **kwargs): :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ - return list(self.identity.groups(**kwargs)) + identity = utils.ensure_service_version(self.identity, '3') + + return list(identity.groups(**kwargs)) def search_groups(self, name_or_id=None, filters=None, domain_id=None): """Search Keystone groups. @@ -978,6 +1038,8 @@ def get_group(self, name_or_id, filters=None, domain_id=None): :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ + identity = utils.ensure_service_version(self.identity, '3') + if filters is not None: warnings.warn( "the 'filters' argument is deprecated; use " @@ -997,9 +1059,7 @@ def get_group(self, name_or_id, filters=None, domain_id=None): return entities[0] - return self.identity.find_group( - name_or_id=name_or_id, domain_id=domain_id - ) + return identity.find_group(name_or_id=name_or_id, domain_id=domain_id) def create_group(self, name, description, domain=None): """Create a group. @@ -1012,6 +1072,8 @@ def create_group(self, name, description, domain=None): :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ + identity = utils.ensure_service_version(self.identity, '3') + group_ref = {'name': name} if description: group_ref['description'] = description @@ -1023,7 +1085,7 @@ def create_group(self, name, description, domain=None): ) group_ref['domain_id'] = dom['id'] - group = self.identity.create_group(**group_ref) + group = identity.create_group(**group_ref) return group @@ -1044,9 +1106,9 @@ def update_group( :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ - group = self.identity.find_group( - name_or_id, ignore_missing=False, **kwargs - ) + identity = utils.ensure_service_version(self.identity, '3') + + group = identity.find_group(name_or_id, ignore_missing=False, **kwargs) group_ref = {} if name: @@ -1054,7 +1116,7 @@ def update_group( if description: group_ref['description'] = description - group = self.identity.update_group(group, **group_ref) + group = identity.update_group(group, **group_ref) return group @@ -1067,13 +1129,15 @@ def delete_group(self, name_or_id): :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ + identity = utils.ensure_service_version(self.identity, '3') + try: - group = self.identity.find_group(name_or_id, ignore_missing=True) + group = identity.find_group(name_or_id, ignore_missing=True) if group is None: self.log.debug("Group %s not found for deleting", name_or_id) return False - self.identity.delete_group(group) + identity.delete_group(group) return True @@ -1088,7 +1152,9 @@ def list_roles(self, **kwargs): :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ - return list(self.identity.roles(**kwargs)) + identity = utils.ensure_service_version(self.identity, '3') + + return list(identity.roles(**kwargs)) def search_roles(self, name_or_id=None, filters=None, domain_id=None): """Seach Keystone roles. @@ -1126,6 +1192,8 @@ def get_role(self, name_or_id, filters=None, domain_id=None): :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ + identity = utils.ensure_service_version(self.identity, '3') + if filters is not None: warnings.warn( "the 'filters' argument is deprecated; use " @@ -1145,11 +1213,11 @@ def get_role(self, name_or_id, filters=None, domain_id=None): return entities[0] - return self.identity.find_role( - name_or_id=name_or_id, domain_id=domain_id - ) + return identity.find_role(name_or_id=name_or_id, domain_id=domain_id) def _keystone_v3_role_assignments(self, **filters): + identity = utils.ensure_service_version(self.identity, '3') + # NOTE(samueldmq): different parameters have different representation # patterns as query parameters in the call to the list role assignments # API. The code below handles each set of patterns separately and @@ -1186,7 +1254,7 @@ def _keystone_v3_role_assignments(self, **filters): ] del filters['os_inherit_extension_inherited_to'] - return list(self.identity.role_assignments(**filters)) + return list(identity.role_assignments(**filters)) def list_role_assignments(self, filters=None): """List Keystone role assignments @@ -1215,6 +1283,8 @@ def list_role_assignments(self, filters=None): :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ + identity = utils.ensure_service_version(self.identity, '3') + # NOTE(samueldmq): although 'include_names' is a valid query parameter # in the keystone v3 list role assignments API, it would have NO effect # on shade due to normalization. It is not documented as an acceptable @@ -1255,7 +1325,7 @@ def list_role_assignments(self, filters=None): 'os_inherit_extension_inherited_to' ) - return list(self.identity.role_assignments(**filters)) + return list(identity.role_assignments(**filters)) @_utils.valid_kwargs('domain_id') def create_role(self, name, **kwargs): @@ -1267,8 +1337,10 @@ def create_role(self, name, **kwargs): :raises: :class:`~openstack.exceptions.SDKException` if the role cannot be created """ + identity = utils.ensure_service_version(self.identity, '3') + kwargs['name'] = name - return self.identity.create_role(**kwargs) + return identity.create_role(**kwargs) @_utils.valid_kwargs('domain_id') def update_role(self, name_or_id, name, **kwargs): @@ -1281,12 +1353,14 @@ def update_role(self, name_or_id, name, **kwargs): :raises: :class:`~openstack.exceptions.SDKException` if the role cannot be created """ + identity = utils.ensure_service_version(self.identity, '3') + role = self.get_role(name_or_id, **kwargs) if role is None: self.log.debug("Role %s not found for updating", name_or_id) return False - return self.identity.update_role(role, name=name, **kwargs) + return identity.update_role(role, name=name, **kwargs) @_utils.valid_kwargs('domain_id') def delete_role(self, name_or_id, **kwargs): @@ -1299,13 +1373,15 @@ def delete_role(self, name_or_id, **kwargs): :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ + identity = utils.ensure_service_version(self.identity, '3') + role = self.get_role(name_or_id, **kwargs) if role is None: self.log.debug("Role %s not found for deleting", name_or_id) return False try: - self.identity.delete_role(role) + identity.delete_role(role) return True except exceptions.SDKException: self.log.exception(f"Unable to delete role {name_or_id}") @@ -1320,17 +1396,17 @@ def _get_grant_revoke_params( domain=None, system=None, ): + identity = utils.ensure_service_version(self.identity, '3') + data = {} search_args = {} if domain: - data['domain'] = self.identity.find_domain( - domain, ignore_missing=False - ) + data['domain'] = identity.find_domain(domain, ignore_missing=False) # We have domain. We should use it for further searching user, # group, role, project search_args['domain_id'] = data['domain'].id - data['role'] = self.identity.find_role(role, ignore_missing=False) + data['role'] = identity.find_role(role, ignore_missing=False) if user and group: raise exceptions.SDKException( @@ -1346,15 +1422,15 @@ def _get_grant_revoke_params( ) if user: - data['user'] = self.identity.find_user( + data['user'] = identity.find_user( user, ignore_missing=False, **search_args ) if group: - data['group'] = self.identity.find_group( + data['group'] = identity.find_group( group, ignore_missing=False, **search_args ) if project: - data['project'] = self.identity.find_project( + data['project'] = identity.find_project( project, ignore_missing=False, **search_args ) @@ -1398,6 +1474,8 @@ def grant_role( :raises: :class:`~openstack.exceptions.SDKException` if the role cannot be granted """ + identity = utils.ensure_service_version(self.identity, '3') + data = self._get_grant_revoke_params( name_or_id, user=user, @@ -1416,45 +1494,45 @@ def grant_role( if project: # Proceed with project - precedence over domain and system if user: - has_role = self.identity.validate_user_has_project_role( + has_role = identity.validate_user_has_project_role( project, user, role, inherited=inherited ) if has_role: self.log.debug('Assignment already exists') return False - self.identity.assign_project_role_to_user( + identity.assign_project_role_to_user( project, user, role, inherited=inherited ) else: - has_role = self.identity.validate_group_has_project_role( + has_role = identity.validate_group_has_project_role( project, group, role, inherited=inherited ) if has_role: self.log.debug('Assignment already exists') return False - self.identity.assign_project_role_to_group( + identity.assign_project_role_to_group( project, group, role, inherited=inherited ) elif domain: # Proceed with domain - precedence over system if user: - has_role = self.identity.validate_user_has_domain_role( + has_role = identity.validate_user_has_domain_role( domain, user, role, inherited=inherited ) if has_role: self.log.debug('Assignment already exists') return False - self.identity.assign_domain_role_to_user( + identity.assign_domain_role_to_user( domain, user, role, inherited=inherited ) else: - has_role = self.identity.validate_group_has_domain_role( + has_role = identity.validate_group_has_domain_role( domain, group, role, inherited=inherited ) if has_role: self.log.debug('Assignment already exists') return False - self.identity.assign_domain_role_to_group( + identity.assign_domain_role_to_group( domain, group, role, inherited=inherited ) else: @@ -1462,21 +1540,21 @@ def grant_role( # System name must be 'all' due to checks performed in # _get_grant_revoke_params if user: - has_role = self.identity.validate_user_has_system_role( + has_role = identity.validate_user_has_system_role( user, role, system ) if has_role: self.log.debug('Assignment already exists') return False - self.identity.assign_system_role_to_user(user, role, system) + identity.assign_system_role_to_user(user, role, system) else: - has_role = self.identity.validate_group_has_system_role( + has_role = identity.validate_group_has_system_role( group, role, system ) if has_role: self.log.debug('Assignment already exists') return False - self.identity.assign_system_role_to_group(group, role, system) + identity.assign_system_role_to_group(group, role, system) return True def revoke_role( @@ -1514,6 +1592,8 @@ def revoke_role( :raises: :class:`~openstack.exceptions.SDKException` if the role cannot be removed """ + identity = utils.ensure_service_version(self.identity, '3') + data = self._get_grant_revoke_params( name_or_id, user=user, @@ -1532,45 +1612,45 @@ def revoke_role( if project: # Proceed with project - precedence over domain and system if user: - has_role = self.identity.validate_user_has_project_role( + has_role = identity.validate_user_has_project_role( project, user, role, inherited=inherited ) if not has_role: self.log.debug('Assignment does not exists') return False - self.identity.unassign_project_role_from_user( + identity.unassign_project_role_from_user( project, user, role, inherited=inherited ) else: - has_role = self.identity.validate_group_has_project_role( + has_role = identity.validate_group_has_project_role( project, group, role, inherited=inherited ) if not has_role: self.log.debug('Assignment does not exists') return False - self.identity.unassign_project_role_from_group( + identity.unassign_project_role_from_group( project, group, role, inherited=inherited ) elif domain: # Proceed with domain - precedence over system if user: - has_role = self.identity.validate_user_has_domain_role( + has_role = identity.validate_user_has_domain_role( domain, user, role, inherited=inherited ) if not has_role: self.log.debug('Assignment does not exists') return False - self.identity.unassign_domain_role_from_user( + identity.unassign_domain_role_from_user( domain, user, role, inherited=inherited ) else: - has_role = self.identity.validate_group_has_domain_role( + has_role = identity.validate_group_has_domain_role( domain, group, role, inherited=inherited ) if not has_role: self.log.debug('Assignment does not exists') return False - self.identity.unassign_domain_role_from_group( + identity.unassign_domain_role_from_group( domain, group, role, inherited=inherited ) else: @@ -1578,23 +1658,19 @@ def revoke_role( # System name must be 'all' due to checks performed in # _get_grant_revoke_params if user: - has_role = self.identity.validate_user_has_system_role( + has_role = identity.validate_user_has_system_role( user, role, system ) if not has_role: self.log.debug('Assignment does not exist') return False - self.identity.unassign_system_role_from_user( - user, role, system - ) + identity.unassign_system_role_from_user(user, role, system) else: - has_role = self.identity.validate_group_has_system_role( + has_role = identity.validate_group_has_system_role( group, role, system ) if not has_role: self.log.debug('Assignment does not exist') return False - self.identity.unassign_system_role_from_group( - group, role, system - ) + identity.unassign_system_role_from_group(group, role, system) return True diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index 42ca1ffd9..610c318cc 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -14,6 +14,7 @@ from openstack.cloud import _utils from openstack.cloud import exc from openstack import exceptions +from openstack import utils class NetworkCloudMixin(_network_common.NetworkCommonCloudMixin): @@ -617,7 +618,8 @@ def set_network_quotas(self, name_or_id, **kwargs): :raises: :class:`~openstack.exceptions.SDKException` if the resource to set the quota does not exist. """ - proj = self.identity.find_project(name_or_id, ignore_missing=True) + identity = utils.ensure_service_version(self.identity, '3') + proj = identity.find_project(name_or_id, ignore_missing=True) if not proj: raise exceptions.SDKException( f"Project {name_or_id} was requested by was not found " @@ -636,7 +638,8 @@ def get_network_quotas(self, name_or_id, details=False): :raises: :class:`~openstack.exceptions.SDKException` if it's not a valid project """ - proj = self.identity.find_project(name_or_id, ignore_missing=True) + identity = utils.ensure_service_version(self.identity, '3') + proj = identity.find_project(name_or_id, ignore_missing=True) if not proj: raise exc.OpenStackCloudException( f"Project {name_or_id} was requested by was not found " @@ -660,7 +663,8 @@ def delete_network_quotas(self, name_or_id): :raises: :class:`~openstack.exceptions.SDKException` if it's not a valid project or the network client call failed """ - proj = self.identity.find_project(name_or_id, ignore_missing=True) + identity = utils.ensure_service_version(self.identity, '3') + proj = identity.find_project(name_or_id, ignore_missing=True) if not proj: raise exceptions.SDKException( f"Project {name_or_id} was requested by was not found " diff --git a/openstack/tests/functional/block_storage/v3/base.py b/openstack/tests/functional/block_storage/v3/base.py index d53a68792..d28c278ee 100644 --- a/openstack/tests/functional/block_storage/v3/base.py +++ b/openstack/tests/functional/block_storage/v3/base.py @@ -10,14 +10,26 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.block_storage.v3 import _proxy as _block_storage_v3 from openstack.tests.functional import base +from openstack import utils class BaseBlockStorageTest(base.BaseFunctionalTest): _wait_for_timeout_key = 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_BLOCK_STORAGE' + admin_block_storage_client: _block_storage_v3.Proxy + block_storage_client: _block_storage_v3.Proxy + def setUp(self): super().setUp() self._set_user_cloud(block_storage_api_version='3') if not self.user_cloud.has_service('block-storage', '3'): self.skipTest('block-storage service not supported by cloud') + + self.admin_block_storage_client = utils.ensure_service_version( + self.operator_cloud.block_storage, '3' + ) + self.block_storage_client = utils.ensure_service_version( + self.user_cloud.block_storage, '3' + ) diff --git a/openstack/tests/functional/block_storage/v3/test_attachment.py b/openstack/tests/functional/block_storage/v3/test_attachment.py index 97b4380ab..79d8312fd 100644 --- a/openstack/tests/functional/block_storage/v3/test_attachment.py +++ b/openstack/tests/functional/block_storage/v3/test_attachment.py @@ -72,7 +72,7 @@ def tearDown(self): super().tearDown() def test_attachment(self): - attachment = self.operator_cloud.block_storage.create_attachment( + attachment = self.admin_block_storage_client.create_attachment( self.VOLUME_ID, connector={}, instance_id=self.server.id, @@ -85,6 +85,6 @@ def test_attachment(self): self.assertIn('detached_at', attachment) self.assertIn('attach_mode', attachment) self.assertIn('connection_info', attachment) - attachment = self.user_cloud.block_storage.delete_attachment( + attachment = self.block_storage_client.delete_attachment( attachment.id, ignore_missing=False ) diff --git a/openstack/tests/functional/block_storage/v3/test_availability_zone.py b/openstack/tests/functional/block_storage/v3/test_availability_zone.py index 36da98915..3d1d4f246 100644 --- a/openstack/tests/functional/block_storage/v3/test_availability_zone.py +++ b/openstack/tests/functional/block_storage/v3/test_availability_zone.py @@ -10,15 +10,16 @@ # License for the specific language governing permissions and limitations # under the License. - from openstack.tests.functional import base +from openstack import utils class TestAvailabilityZone(base.BaseFunctionalTest): def test_list(self): - availability_zones = list( - self.operator_cloud.block_storage.availability_zones() + block_storage = utils.ensure_service_version( + self.operator_cloud.block_storage, '3' ) + availability_zones = list(block_storage.availability_zones()) self.assertGreater(len(availability_zones), 0) for az in availability_zones: diff --git a/openstack/tests/functional/block_storage/v3/test_block_storage_summary.py b/openstack/tests/functional/block_storage/v3/test_block_storage_summary.py index 7de150f16..084e9e63f 100644 --- a/openstack/tests/functional/block_storage/v3/test_block_storage_summary.py +++ b/openstack/tests/functional/block_storage/v3/test_block_storage_summary.py @@ -15,7 +15,7 @@ class TestBlockStorageSummary(base.BaseBlockStorageTest): def test_get(self): - sot = self.operator_cloud.block_storage.summary(all_projects=True) + sot = self.admin_block_storage_client.summary(all_projects=True) self.assertIn('total_size', sot) self.assertIn('total_count', sot) self.assertIn('metadata', sot) diff --git a/openstack/tests/functional/block_storage/v3/test_default_type.py b/openstack/tests/functional/block_storage/v3/test_default_type.py index 7520ec632..3bafb6998 100644 --- a/openstack/tests/functional/block_storage/v3/test_default_type.py +++ b/openstack/tests/functional/block_storage/v3/test_default_type.py @@ -20,31 +20,34 @@ def setUp(self): if not self._operator_cloud_name: self.skip("Operator cloud must be set for this test") self._set_operator_cloud(block_storage_api_version='3.67') + block_storage = self.operator_cloud.block_storage + assert block_storage.api_version == '3' + self.admin_block_storage_client = block_storage self.PROJECT_ID = self.create_temporary_project().id def test_default_type(self): # Create a volume type type_name = self.getUniqueString() - volume_type_id = self.operator_cloud.block_storage.create_type( + volume_type_id = self.admin_block_storage_client.create_type( name=type_name, ).id # Set default type for a project - default_type = self.operator_cloud.block_storage.set_default_type( + default_type = self.admin_block_storage_client.set_default_type( self.PROJECT_ID, volume_type_id, ) self.assertIsInstance(default_type, _default_type.DefaultType) # Show default type for a project - default_type = self.operator_cloud.block_storage.show_default_type( + default_type = self.admin_block_storage_client.show_default_type( self.PROJECT_ID ) self.assertIsInstance(default_type, _default_type.DefaultType) self.assertEqual(volume_type_id, default_type.volume_type_id) # List all default types - default_types = self.operator_cloud.block_storage.default_types() + default_types = self.admin_block_storage_client.default_types() for default_type in default_types: self.assertIsInstance(default_type, _default_type.DefaultType) # There could be existing default types set in the environment @@ -53,13 +56,13 @@ def test_default_type(self): self.assertEqual(volume_type_id, default_type.volume_type_id) # Unset default type for a project - default_type = self.operator_cloud.block_storage.unset_default_type( + default_type = self.admin_block_storage_client.unset_default_type( self.PROJECT_ID ) self.assertIsNone(default_type) # Delete the volume type - vol_type = self.operator_cloud.block_storage.delete_type( + vol_type = self.admin_block_storage_client.delete_type( volume_type_id, ignore_missing=False, ) diff --git a/openstack/tests/functional/block_storage/v3/test_group.py b/openstack/tests/functional/block_storage/v3/test_group.py index 009efda0d..8bf27bf3f 100644 --- a/openstack/tests/functional/block_storage/v3/test_group.py +++ b/openstack/tests/functional/block_storage/v3/test_group.py @@ -23,18 +23,18 @@ def setUp(self): super().setUp() # there will always be at least one volume type, i.e. the default one - volume_types = list(self.operator_cloud.block_storage.types()) + volume_types = list(self.admin_block_storage_client.types()) self.volume_type = volume_types[0] group_type_name = self.getUniqueString() - self.group_type = self.operator_cloud.block_storage.create_group_type( + self.group_type = self.admin_block_storage_client.create_group_type( name=group_type_name, ) self.assertIsInstance(self.group_type, _group_type.GroupType) self.assertEqual(group_type_name, self.group_type.name) group_name = self.getUniqueString() - self.group = self.operator_cloud.block_storage.create_group( + self.group = self.admin_block_storage_client.create_group( name=group_name, group_type=self.group_type.id, volume_types=[self.volume_type.id], @@ -46,31 +46,31 @@ def tearDown(self): # we do this in tearDown rather than via 'addCleanup' since we need to # wait for the deletion of the group before moving onto the deletion of # the group type - self.operator_cloud.block_storage.delete_group( + self.admin_block_storage_client.delete_group( self.group, delete_volumes=True ) - self.operator_cloud.block_storage.wait_for_delete(self.group) + self.admin_block_storage_client.wait_for_delete(self.group) - self.operator_cloud.block_storage.delete_group_type(self.group_type) - self.operator_cloud.block_storage.wait_for_delete(self.group_type) + self.admin_block_storage_client.delete_group_type(self.group_type) + self.admin_block_storage_client.wait_for_delete(self.group_type) super().tearDown() def test_group_type(self): # get - group_type = self.operator_cloud.block_storage.get_group_type( + group_type = self.admin_block_storage_client.get_group_type( self.group_type.id ) self.assertEqual(self.group_type.name, group_type.name) # find - group_type = self.operator_cloud.block_storage.find_group_type( + group_type = self.admin_block_storage_client.find_group_type( self.group_type.name, ) self.assertEqual(self.group_type.id, group_type.id) # list - group_types = list(self.operator_cloud.block_storage.group_types()) + group_types = list(self.admin_block_storage_client.group_types()) # other tests may have created group types and there can be defaults so # we don't assert that this is the *only* group type present self.assertIn(self.group_type.id, {g.id for g in group_types}) @@ -78,13 +78,13 @@ def test_group_type(self): # update group_type_name = self.getUniqueString() group_type_description = self.getUniqueString() - group_type = self.operator_cloud.block_storage.update_group_type( + group_type = self.admin_block_storage_client.update_group_type( self.group_type, name=group_type_name, description=group_type_description, ) self.assertIsInstance(group_type, _group_type.GroupType) - group_type = self.operator_cloud.block_storage.get_group_type( + group_type = self.admin_block_storage_client.get_group_type( self.group_type.id ) self.assertEqual(group_type_name, group_type.name) @@ -93,13 +93,13 @@ def test_group_type(self): def test_group_type_group_specs(self): # create group_type = ( - self.operator_cloud.block_storage.create_group_type_group_specs( + self.admin_block_storage_client.create_group_type_group_specs( self.group_type, {'foo': 'bar', 'acme': 'buzz'}, ) ) self.assertIsInstance(group_type, _group_type.GroupType) - group_type = self.operator_cloud.block_storage.get_group_type( + group_type = self.admin_block_storage_client.get_group_type( self.group_type.id ) self.assertEqual( @@ -107,20 +107,20 @@ def test_group_type_group_specs(self): ) # get - spec = self.operator_cloud.block_storage.get_group_type_group_specs_property( # noqa: E501 + spec = self.admin_block_storage_client.get_group_type_group_specs_property( # noqa: E501 self.group_type, 'foo', ) self.assertEqual('bar', spec) # update - spec = self.operator_cloud.block_storage.update_group_type_group_specs_property( # noqa: E501 + spec = self.admin_block_storage_client.update_group_type_group_specs_property( # noqa: E501 self.group_type, 'foo', 'baz', ) self.assertEqual('baz', spec) - group_type = self.operator_cloud.block_storage.get_group_type( + group_type = self.admin_block_storage_client.get_group_type( self.group_type.id ) self.assertEqual( @@ -128,26 +128,26 @@ def test_group_type_group_specs(self): ) # delete - self.operator_cloud.block_storage.delete_group_type_group_specs_property( + self.admin_block_storage_client.delete_group_type_group_specs_property( self.group_type, 'foo', ) - group_type = self.operator_cloud.block_storage.get_group_type( + group_type = self.admin_block_storage_client.get_group_type( self.group_type.id ) self.assertEqual({'acme': 'buzz'}, group_type.group_specs) def test_group(self): # get - group = self.operator_cloud.block_storage.get_group(self.group.id) + group = self.admin_block_storage_client.get_group(self.group.id) self.assertEqual(self.group.name, group.name) # find - group = self.operator_cloud.block_storage.find_group(self.group.name) + group = self.admin_block_storage_client.find_group(self.group.name) self.assertEqual(self.group.id, group.id) # list - groups = self.operator_cloud.block_storage.groups() + groups = self.admin_block_storage_client.groups() # other tests may have created groups and there can be defaults so we # don't assert that this is the *only* group present self.assertIn(self.group.id, {g.id for g in groups}) @@ -155,13 +155,13 @@ def test_group(self): # update group_name = self.getUniqueString() group_description = self.getUniqueString() - group = self.operator_cloud.block_storage.update_group( + group = self.admin_block_storage_client.update_group( self.group, name=group_name, description=group_description, ) self.assertIsInstance(group, _group.Group) - group = self.operator_cloud.block_storage.get_group(self.group.id) + group = self.admin_block_storage_client.get_group(self.group.id) self.assertEqual(group_name, group.name) self.assertEqual(group_description, group.description) @@ -171,13 +171,13 @@ def test_group_snapshot(self): # 'delete_volumes' flag) will handle this but we do need to wait for # the thing to be created volume_name = self.getUniqueString() - self.volume = self.operator_cloud.block_storage.create_volume( + self.volume = self.admin_block_storage_client.create_volume( name=volume_name, volume_type=self.volume_type.id, group_id=self.group.id, size=1, ) - self.operator_cloud.block_storage.wait_for_status( + self.admin_block_storage_client.wait_for_status( self.volume, status='available', failures=['error'], @@ -188,12 +188,12 @@ def test_group_snapshot(self): group_snapshot_name = self.getUniqueString() self.group_snapshot = ( - self.operator_cloud.block_storage.create_group_snapshot( + self.admin_block_storage_client.create_group_snapshot( name=group_snapshot_name, group_id=self.group.id, ) ) - self.operator_cloud.block_storage.wait_for_status( + self.admin_block_storage_client.wait_for_status( self.group_snapshot, status='available', failures=['error'], @@ -206,19 +206,19 @@ def test_group_snapshot(self): ) # get - group_snapshot = self.operator_cloud.block_storage.get_group_snapshot( + group_snapshot = self.admin_block_storage_client.get_group_snapshot( self.group_snapshot.id, ) self.assertEqual(self.group_snapshot.name, group_snapshot.name) # find - group_snapshot = self.operator_cloud.block_storage.find_group_snapshot( + group_snapshot = self.admin_block_storage_client.find_group_snapshot( self.group_snapshot.name, ) self.assertEqual(self.group_snapshot.id, group_snapshot.id) # list - group_snapshots = self.operator_cloud.block_storage.group_snapshots() + group_snapshots = self.admin_block_storage_client.group_snapshots() # other tests may have created group snapshot and there can be defaults # so we don't assert that this is the *only* group snapshot present self.assertIn(self.group_snapshot.id, {g.id for g in group_snapshots}) @@ -226,7 +226,7 @@ def test_group_snapshot(self): # update (not supported) # delete - self.operator_cloud.block_storage.delete_group_snapshot( + self.admin_block_storage_client.delete_group_snapshot( self.group_snapshot ) - self.operator_cloud.block_storage.wait_for_delete(self.group_snapshot) + self.admin_block_storage_client.wait_for_delete(self.group_snapshot) diff --git a/openstack/tests/functional/block_storage/v3/test_resource_filters.py b/openstack/tests/functional/block_storage/v3/test_resource_filters.py index 0b148c00c..104ba6036 100644 --- a/openstack/tests/functional/block_storage/v3/test_resource_filters.py +++ b/openstack/tests/functional/block_storage/v3/test_resource_filters.py @@ -17,7 +17,7 @@ class ResourceFilters(base.BaseBlockStorageTest): def test_get(self): resource_filters = list( - self.operator_cloud.block_storage.resource_filters() + self.admin_block_storage_client.resource_filters() ) for rf in resource_filters: diff --git a/openstack/tests/functional/block_storage/v3/test_service.py b/openstack/tests/functional/block_storage/v3/test_service.py index d97b915ad..346d86629 100644 --- a/openstack/tests/functional/block_storage/v3/test_service.py +++ b/openstack/tests/functional/block_storage/v3/test_service.py @@ -11,6 +11,7 @@ # under the License. from openstack.tests.functional import base +from openstack import utils class TestService(base.BaseFunctionalTest): @@ -22,11 +23,14 @@ def test_list(self): self.assertIsNotNone(sot) def test_disable_enable(self): - for srv in self.operator_cloud.block_storage.services(): + block_storage = utils.ensure_service_version( + self.operator_cloud.block_storage, '3' + ) + for srv in block_storage.services(): # only nova-block_storage can be updated if srv.name == 'nova-block_storage': - self.operator_cloud.block_storage.disable_service(srv) - self.operator_cloud.block_storage.enable_service(srv) + block_storage.disable_service(srv) + block_storage.enable_service(srv) break def test_find(self): diff --git a/openstack/tests/functional/block_storage/v3/test_volume.py b/openstack/tests/functional/block_storage/v3/test_volume.py index ec4caf0da..4f76c01ce 100644 --- a/openstack/tests/functional/block_storage/v3/test_volume.py +++ b/openstack/tests/functional/block_storage/v3/test_volume.py @@ -23,11 +23,11 @@ def setUp(self): volume_name = self.getUniqueString() - self.volume = self.user_cloud.block_storage.create_volume( + self.volume = self.block_storage_client.create_volume( name=volume_name, size=1, ) - self.user_cloud.block_storage.wait_for_status( + self.block_storage_client.wait_for_status( self.volume, status='available', failures=['error'], @@ -38,20 +38,20 @@ def setUp(self): self.assertEqual(volume_name, self.volume.name) def tearDown(self): - self.user_cloud.block_storage.delete_volume(self.volume) + self.block_storage_client.delete_volume(self.volume) super().tearDown() def test_volume(self): # get - volume = self.user_cloud.block_storage.get_volume(self.volume.id) + volume = self.block_storage_client.get_volume(self.volume.id) self.assertEqual(self.volume.name, volume.name) # find - volume = self.user_cloud.block_storage.find_volume(self.volume.name) + volume = self.block_storage_client.find_volume(self.volume.name) self.assertEqual(self.volume.id, volume.id) # list - volumes = self.user_cloud.block_storage.volumes() + volumes = self.block_storage_client.volumes() # other tests may have created volumes so we don't assert that this is # the *only* volume present self.assertIn(self.volume.id, {v.id for v in volumes}) @@ -59,12 +59,12 @@ def test_volume(self): # update volume_name = self.getUniqueString() volume_description = self.getUniqueString() - volume = self.user_cloud.block_storage.update_volume( + volume = self.block_storage_client.update_volume( self.volume, name=volume_name, description=volume_description, ) self.assertIsInstance(volume, _volume.Volume) - volume = self.user_cloud.block_storage.get_volume(self.volume.id) + volume = self.block_storage_client.get_volume(self.volume.id) self.assertEqual(volume_name, volume.name) self.assertEqual(volume_description, volume.description) diff --git a/openstack/tests/functional/cloud/test_endpoints.py b/openstack/tests/functional/cloud/test_endpoints.py index 0c50e18a2..4d7ec81b7 100644 --- a/openstack/tests/functional/cloud/test_endpoints.py +++ b/openstack/tests/functional/cloud/test_endpoints.py @@ -24,6 +24,7 @@ from openstack import exceptions from openstack.tests.functional import base +from openstack import utils class TestEndpoints(base.KeystoneBaseFunctionalTest): @@ -87,7 +88,10 @@ def _cleanup_services(self): def test_create_endpoint(self): service_name = self.new_item_name + '_create' - region = next(iter(self.operator_cloud.identity.regions())).id + identity = utils.ensure_service_version( + self.operator_cloud.identity, '3' + ) + region = next(iter(identity.regions())).id service = self.operator_cloud.create_service( name=service_name, @@ -119,7 +123,10 @@ def test_create_endpoint(self): def test_update_endpoint(self): # service operations require existing region. Do not test updating # region for now - region = next(iter(self.operator_cloud.identity.regions())).id + identity = utils.ensure_service_version( + self.operator_cloud.identity, '3' + ) + region = next(iter(identity.regions())).id service = self.operator_cloud.create_service( name='service1', type='test_type' @@ -153,7 +160,10 @@ def test_update_endpoint(self): def test_list_endpoints(self): service_name = self.new_item_name + '_list' - region = next(iter(self.operator_cloud.identity.regions())).id + identity = utils.ensure_service_version( + self.operator_cloud.identity, '3' + ) + region = next(iter(identity.regions())).id service = self.operator_cloud.create_service( name=service_name, @@ -193,7 +203,10 @@ def test_list_endpoints(self): def test_delete_endpoint(self): service_name = self.new_item_name + '_delete' - region = next(iter(self.operator_cloud.identity.regions())).id + identity = utils.ensure_service_version( + self.operator_cloud.identity, '3' + ) + region = next(iter(identity.regions())).id service = self.operator_cloud.create_service( name=service_name, diff --git a/openstack/tests/functional/compute/v2/base.py b/openstack/tests/functional/compute/v2/base.py index 844c30a1e..00a2a36f7 100644 --- a/openstack/tests/functional/compute/v2/base.py +++ b/openstack/tests/functional/compute/v2/base.py @@ -10,8 +10,40 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.block_storage.v3 import _proxy as _block_storage_proxy +from openstack.compute.v2 import _proxy as _compute_proxy from openstack.tests.functional import base +from openstack import utils class BaseComputeTest(base.BaseFunctionalTest): _wait_for_timeout_key = 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_COMPUTE' + + admin_compute_client: _compute_proxy.Proxy + compute_client: _compute_proxy.Proxy + + admin_block_storage_client: _block_storage_proxy.Proxy + block_storage_client: _block_storage_proxy.Proxy + + def setUp(self): + super().setUp() + self._set_user_cloud(compute_api_version='2') + if not self.user_cloud.has_service('compute', '2'): + self.skipTest('compute service not supported by cloud') + + self.admin_compute_client = utils.ensure_service_version( + self.operator_cloud.compute, '2' + ) + self.compute_client = utils.ensure_service_version( + self.user_cloud.compute, '2' + ) + + if not self.user_cloud.has_service('block-storage', '3'): + self.skipTest('block-storage service not supported by cloud') + + self.admin_block_storage_client = utils.ensure_service_version( + self.operator_cloud.block_storage, '3' + ) + self.block_storage_client = utils.ensure_service_version( + self.user_cloud.block_storage, '3' + ) diff --git a/openstack/tests/functional/compute/v2/test_extension.py b/openstack/tests/functional/compute/v2/test_extension.py index 84581f63f..547579a34 100644 --- a/openstack/tests/functional/compute/v2/test_extension.py +++ b/openstack/tests/functional/compute/v2/test_extension.py @@ -15,7 +15,7 @@ class TestExtension(base.BaseComputeTest): def test_list(self): - extensions = list(self.operator_cloud.compute.extensions()) + extensions = list(self.admin_compute_client.extensions()) self.assertGreater(len(extensions), 0) for ext in extensions: diff --git a/openstack/tests/functional/compute/v2/test_flavor.py b/openstack/tests/functional/compute/v2/test_flavor.py index 142168e62..5fd64ec5f 100644 --- a/openstack/tests/functional/compute/v2/test_flavor.py +++ b/openstack/tests/functional/compute/v2/test_flavor.py @@ -24,7 +24,7 @@ def setUp(self): self.private_flavor_name = uuid.uuid4().hex def _delete_flavor(self, flavor): - ret = self.operator_cloud.compute.delete_flavor(flavor) + ret = self.admin_compute_client.delete_flavor(flavor) self.assertIsNone(ret) def test_flavor(self): @@ -33,7 +33,7 @@ def test_flavor(self): # create a public and private flavor so we can test that they are both # listed for an operator - public_flavor = self.operator_cloud.compute.create_flavor( + public_flavor = self.admin_compute_client.create_flavor( name=self.public_flavor_name, ram=1024, vcpus=2, @@ -43,7 +43,7 @@ def test_flavor(self): self.addCleanup(self._delete_flavor, public_flavor) self.assertIsInstance(public_flavor, _flavor.Flavor) - private_flavor = self.operator_cloud.compute.create_flavor( + private_flavor = self.admin_compute_client.create_flavor( name=self.private_flavor_name, ram=1024, vcpus=2, @@ -57,37 +57,37 @@ def test_flavor(self): # # flavor list will include the standard devstack flavors. We just want # to make sure both of the flavors we just created are present. - flavors = list(self.operator_cloud.compute.flavors()) + flavors = list(self.admin_compute_client.flavors()) self.assertIn(self.public_flavor_name, {x.name for x in flavors}) self.assertIn(self.private_flavor_name, {x.name for x in flavors}) # get flavor by ID - flavor = self.operator_cloud.compute.get_flavor(public_flavor.id) + flavor = self.admin_compute_client.get_flavor(public_flavor.id) self.assertEqual(flavor.id, public_flavor.id) # find flavor by name - flavor = self.operator_cloud.compute.find_flavor(public_flavor.name) + flavor = self.admin_compute_client.find_flavor(public_flavor.name) self.assertEqual(flavor.name, public_flavor.name) # update a flavor - self.operator_cloud.compute.update_flavor( + self.admin_compute_client.update_flavor( public_flavor, description="updated description", ) # fetch the updated flavor - flavor = self.operator_cloud.compute.get_flavor(public_flavor.id) + flavor = self.admin_compute_client.get_flavor(public_flavor.id) self.assertEqual(flavor.description, "updated description") def test_flavor_access(self): # create private flavor flavor_name = uuid.uuid4().hex - flavor = self.operator_cloud.compute.create_flavor( + flavor = self.admin_compute_client.create_flavor( name=flavor_name, ram=128, vcpus=1, disk=0, is_public=False ) self.addCleanup(self._delete_flavor, flavor) @@ -95,14 +95,14 @@ def test_flavor_access(self): # validate the 'demo' user cannot see the new flavor - flavor = self.user_cloud.compute.find_flavor( + flavor = self.compute_client.find_flavor( flavor_name, ignore_missing=True ) self.assertIsNone(flavor) # validate we can see the new flavor ourselves - flavor = self.operator_cloud.compute.find_flavor( + flavor = self.admin_compute_client.find_flavor( flavor_name, ignore_missing=True ) self.assertIsNotNone(flavor) @@ -115,24 +115,24 @@ def test_flavor_access(self): # give 'demo' access to the flavor - self.operator_cloud.compute.flavor_add_tenant_access( + self.admin_compute_client.flavor_add_tenant_access( flavor.id, project['id'] ) # verify that the 'demo' user now has access to it - flavor = self.user_cloud.compute.find_flavor( + flavor = self.compute_client.find_flavor( flavor_name, ignore_missing=True ) self.assertIsNotNone(flavor) # remove 'demo' access and check we can't find it anymore - self.operator_cloud.compute.flavor_remove_tenant_access( + self.admin_compute_client.flavor_remove_tenant_access( flavor.id, project['id'] ) - flavor = self.user_cloud.compute.find_flavor( + flavor = self.compute_client.find_flavor( flavor_name, ignore_missing=True ) self.assertIsNone(flavor) @@ -141,7 +141,7 @@ def test_flavor_extra_specs(self): # create private flavor flavor_name = uuid.uuid4().hex - flavor = self.operator_cloud.compute.create_flavor( + flavor = self.admin_compute_client.create_flavor( is_public=False, name=flavor_name, ram=128, vcpus=1, disk=0 ) self.addCleanup(self._delete_flavor, flavor) @@ -150,41 +150,39 @@ def test_flavor_extra_specs(self): # create extra_specs specs = {'a': 'b'} - self.operator_cloud.compute.create_flavor_extra_specs( + self.admin_compute_client.create_flavor_extra_specs( flavor, extra_specs=specs ) # verify specs were created correctly - flavor_with_specs = ( - self.operator_cloud.compute.fetch_flavor_extra_specs(flavor) + flavor_with_specs = self.admin_compute_client.fetch_flavor_extra_specs( + flavor ) self.assertDictEqual(specs, flavor_with_specs.extra_specs) # update/add a single extra spec property - self.operator_cloud.compute.update_flavor_extra_specs_property( + self.admin_compute_client.update_flavor_extra_specs_property( flavor, 'c', 'd' ) # fetch single property value - prop_value = ( - self.operator_cloud.compute.get_flavor_extra_specs_property( - flavor, 'c' - ) + prop_value = self.admin_compute_client.get_flavor_extra_specs_property( + flavor, 'c' ) self.assertEqual('d', prop_value) # delete the new property - self.operator_cloud.compute.delete_flavor_extra_specs_property( + self.admin_compute_client.delete_flavor_extra_specs_property( flavor, 'c' ) # re-fetch and ensure we're back to the previous state - flavor_with_specs = ( - self.operator_cloud.compute.fetch_flavor_extra_specs(flavor) + flavor_with_specs = self.admin_compute_client.fetch_flavor_extra_specs( + flavor ) self.assertDictEqual(specs, flavor_with_specs.extra_specs) diff --git a/openstack/tests/functional/compute/v2/test_hypervisor.py b/openstack/tests/functional/compute/v2/test_hypervisor.py index 0d5310f17..025eb2b94 100644 --- a/openstack/tests/functional/compute/v2/test_hypervisor.py +++ b/openstack/tests/functional/compute/v2/test_hypervisor.py @@ -16,21 +16,19 @@ class TestHypervisor(base.BaseComputeTest): def test_hypervisors(self): - hypervisors = list(self.operator_cloud.compute.hypervisors()) + hypervisors = list(self.admin_compute_client.hypervisors()) self.assertIsNotNone(hypervisors) - hypervisors = list( - self.operator_cloud.compute.hypervisors(details=True) - ) + hypervisors = list(self.admin_compute_client.hypervisors(details=True)) self.assertIsNotNone(hypervisors) - hypervisor = self.operator_cloud.compute.get_hypervisor( + hypervisor = self.admin_compute_client.get_hypervisor( hypervisors[0].id ) self.assertIsInstance(hypervisor, _hypervisor.Hypervisor) self.assertEqual(hypervisor.id, hypervisors[0].id) - hypervisor = self.operator_cloud.compute.find_hypervisor( + hypervisor = self.admin_compute_client.find_hypervisor( hypervisors[0].name, ignore_missing=False ) self.assertIsInstance(hypervisor, _hypervisor.Hypervisor) diff --git a/openstack/tests/functional/compute/v2/test_image.py b/openstack/tests/functional/compute/v2/test_image.py index bb7c73fa0..61bf751b5 100644 --- a/openstack/tests/functional/compute/v2/test_image.py +++ b/openstack/tests/functional/compute/v2/test_image.py @@ -20,7 +20,7 @@ def setUp(self): super().setUp() # get a non-test image to work with - images = self.operator_cloud.compute.images() + images = self.admin_compute_client.images() self.image = next(images) if self.image.name == TEST_IMAGE_NAME: @@ -29,21 +29,21 @@ def setUp(self): def test_image(self): # list all images - images = list(self.operator_cloud.compute.images()) + images = list(self.admin_compute_client.images()) self.assertGreater(len(images), 0) for image in images: self.assertIsInstance(image.id, str) # find image by name - image = self.operator_cloud.compute.find_image(self.image.name) + image = self.admin_compute_client.find_image(self.image.name) self.assertIsInstance(image, _image.Image) self.assertEqual(self.image.id, image.id) self.assertEqual(self.image.name, image.name) # get image by ID - image = self.operator_cloud.compute.get_image(self.image.id) + image = self.admin_compute_client.get_image(self.image.id) self.assertIsInstance(image, _image.Image) self.assertEqual(self.image.id, image.id) self.assertEqual(self.image.name, image.name) @@ -51,34 +51,34 @@ def test_image(self): def test_image_metadata(self): # delete pre-existing metadata - self.operator_cloud.compute.delete_image_metadata( + self.admin_compute_client.delete_image_metadata( self.image, self.image.metadata.keys() ) - image = self.operator_cloud.compute.get_image_metadata(self.image) + image = self.admin_compute_client.get_image_metadata(self.image) self.assertFalse(image.metadata) # get metadata (should be empty) - image = self.operator_cloud.compute.get_image_metadata(self.image) + image = self.admin_compute_client.get_image_metadata(self.image) self.assertFalse(image.metadata) # set no metadata - self.operator_cloud.compute.set_image_metadata(self.image) - image = self.operator_cloud.compute.get_image_metadata(self.image) + self.admin_compute_client.set_image_metadata(self.image) + image = self.admin_compute_client.get_image_metadata(self.image) self.assertFalse(image.metadata) # set empty metadata - self.operator_cloud.compute.set_image_metadata(self.image, k0='') - image = self.operator_cloud.compute.get_image_metadata(self.image) + self.admin_compute_client.set_image_metadata(self.image, k0='') + image = self.admin_compute_client.get_image_metadata(self.image) self.assertIn('k0', image.metadata) self.assertEqual('', image.metadata['k0']) # set metadata - self.operator_cloud.compute.set_image_metadata(self.image, k1='v1') - image = self.operator_cloud.compute.get_image_metadata(self.image) + self.admin_compute_client.set_image_metadata(self.image, k1='v1') + image = self.admin_compute_client.get_image_metadata(self.image) self.assertTrue(image.metadata) self.assertEqual(2, len(image.metadata)) self.assertIn('k1', image.metadata) @@ -86,8 +86,8 @@ def test_image_metadata(self): # set more metadata - self.operator_cloud.compute.set_image_metadata(self.image, k2='v2') - image = self.operator_cloud.compute.get_image_metadata(self.image) + self.admin_compute_client.set_image_metadata(self.image, k2='v2') + image = self.admin_compute_client.get_image_metadata(self.image) self.assertTrue(image.metadata) self.assertEqual(3, len(image.metadata)) self.assertIn('k1', image.metadata) @@ -97,8 +97,8 @@ def test_image_metadata(self): # update metadata - self.operator_cloud.compute.set_image_metadata(self.image, k1='v1.1') - image = self.operator_cloud.compute.get_image_metadata(self.image) + self.admin_compute_client.set_image_metadata(self.image, k1='v1.1') + image = self.admin_compute_client.get_image_metadata(self.image) self.assertTrue(image.metadata) self.assertEqual(3, len(image.metadata)) self.assertIn('k1', image.metadata) @@ -108,8 +108,8 @@ def test_image_metadata(self): # delete all metadata (cleanup) - self.operator_cloud.compute.delete_image_metadata( + self.admin_compute_client.delete_image_metadata( self.image, image.metadata.keys() ) - image = self.operator_cloud.compute.get_image_metadata(self.image) + image = self.admin_compute_client.get_image_metadata(self.image) self.assertFalse(image.metadata) diff --git a/openstack/tests/functional/compute/v2/test_keypair.py b/openstack/tests/functional/compute/v2/test_keypair.py index a7a0a83a2..c560f92e2 100644 --- a/openstack/tests/functional/compute/v2/test_keypair.py +++ b/openstack/tests/functional/compute/v2/test_keypair.py @@ -22,13 +22,13 @@ def setUp(self): self.keypair_name = self.getUniqueString().split('.')[-1] def _delete_keypair(self, keypair): - ret = self.user_cloud.compute.delete_keypair(keypair) + ret = self.compute_client.delete_keypair(keypair) self.assertIsNone(ret) def test_keypair(self): # create the keypair - keypair = self.user_cloud.compute.create_keypair( + keypair = self.compute_client.create_keypair( name=self.keypair_name, type='ssh' ) self.assertIsInstance(keypair, _keypair.Keypair) @@ -37,7 +37,7 @@ def test_keypair(self): # retrieve details of the keypair by ID - keypair = self.user_cloud.compute.get_keypair(self.keypair_name) + keypair = self.compute_client.get_keypair(self.keypair_name) self.assertIsInstance(keypair, _keypair.Keypair) self.assertEqual(self.keypair_name, keypair.name) self.assertEqual(self.keypair_name, keypair.id) @@ -45,14 +45,14 @@ def test_keypair(self): # retrieve details of the keypair by name - keypair = self.user_cloud.compute.find_keypair(self.keypair_name) + keypair = self.compute_client.find_keypair(self.keypair_name) self.assertIsInstance(keypair, _keypair.Keypair) self.assertEqual(self.keypair_name, keypair.name) self.assertEqual(self.keypair_name, keypair.id) # list all keypairs - keypairs = list(self.user_cloud.compute.keypairs()) + keypairs = list(self.compute_client.keypairs()) self.assertIsInstance(keypair, _keypair.Keypair) self.assertIn(self.keypair_name, {x.name for x in keypairs}) @@ -65,12 +65,12 @@ def setUp(self): self.user = self.operator_cloud.list_users()[0] def _delete_keypair(self, keypair): - ret = self.user_cloud.compute.delete_keypair(keypair) + ret = self.compute_client.delete_keypair(keypair) self.assertIsNone(ret) def test_keypair(self): # create the keypair (for another user) - keypair = self.operator_cloud.compute.create_keypair( + keypair = self.admin_compute_client.create_keypair( name=self.keypair_name, user_id=self.user.id ) self.assertIsInstance(keypair, _keypair.Keypair) @@ -79,7 +79,7 @@ def test_keypair(self): # retrieve details of the keypair by ID (for another user) - keypair = self.operator_cloud.compute.get_keypair( + keypair = self.admin_compute_client.get_keypair( self.keypair_name, user_id=self.user.id ) self.assertEqual(self.keypair_name, keypair.name) diff --git a/openstack/tests/functional/compute/v2/test_limits.py b/openstack/tests/functional/compute/v2/test_limits.py index 66a72fd37..51757c194 100644 --- a/openstack/tests/functional/compute/v2/test_limits.py +++ b/openstack/tests/functional/compute/v2/test_limits.py @@ -15,7 +15,7 @@ class TestLimits(base.BaseComputeTest): def test_limits(self): - sot = self.operator_cloud.compute.get_limits() + sot = self.admin_compute_client.get_limits() self.assertIsNotNone(sot.absolute['instances']) self.assertIsNotNone(sot.absolute['total_ram']) self.assertIsNotNone(sot.absolute['keypairs']) diff --git a/openstack/tests/functional/compute/v2/test_quota_set.py b/openstack/tests/functional/compute/v2/test_quota_set.py index febdf2fe1..276bd53fd 100644 --- a/openstack/tests/functional/compute/v2/test_quota_set.py +++ b/openstack/tests/functional/compute/v2/test_quota_set.py @@ -18,15 +18,12 @@ class TestQuotaSet(base.BaseComputeTest): def setUp(self): super().setUp() - if not self.operator_cloud: - self.skipTest("Operator cloud required for this test") - self.project = self.create_temporary_project() def test_quota_set(self): # update quota - quota_set = self.operator_cloud.compute.update_quota_set( + quota_set = self.admin_compute_client.update_quota_set( self.project.id, key_pairs=123 ) self.assertIsInstance(quota_set, _quota_set.QuotaSet) @@ -34,13 +31,13 @@ def test_quota_set(self): # retrieve details of the (updated) quota - quota_set = self.operator_cloud.compute.get_quota_set(self.project.id) + quota_set = self.admin_compute_client.get_quota_set(self.project.id) self.assertIsInstance(quota_set, _quota_set.QuotaSet) self.assertEqual(quota_set.key_pairs, 123) # retrieve quota defaults - defaults = self.operator_cloud.compute.get_quota_set_defaults( + defaults = self.admin_compute_client.get_quota_set_defaults( self.project.id ) self.assertIsInstance(defaults, _quota_set.QuotaSet) @@ -48,4 +45,4 @@ def test_quota_set(self): # revert quota - self.operator_cloud.compute.revert_quota_set(self.project.id) + self.admin_compute_client.revert_quota_set(self.project.id) diff --git a/openstack/tests/functional/compute/v2/test_server.py b/openstack/tests/functional/compute/v2/test_server.py index b1e7a8cca..8c76e38e6 100644 --- a/openstack/tests/functional/compute/v2/test_server.py +++ b/openstack/tests/functional/compute/v2/test_server.py @@ -22,17 +22,19 @@ def setUp(self): self.user_data = 'SSdtIGFjdHVhbGx5IGEgZ29hdC4=' def _delete_server(self, server): - sot = self.operator_cloud.compute.delete_server(server.id) - self.operator_cloud.compute.wait_for_delete( + sot = self.admin_compute_client.delete_server(server.id) + self.admin_compute_client.wait_for_delete( server, wait=self._wait_for_timeout ) self.assertIsNone(sot) def test_server(self): # create server with volume - - volume = self.operator_cloud.create_volume(1) - server = self.operator_cloud.compute.create_server( + volume = self.admin_block_storage_client.create_volume(size=1) + self.admin_block_storage_client.wait_for_status( + volume, wait=self._wait_for_timeout + ) + server = self.admin_compute_client.create_server( name=self.server_name, flavor_id=self.flavor.id, image_id=self.image.id, @@ -49,7 +51,7 @@ def test_server(self): }, ], ) - self.operator_cloud.compute.wait_for_server( + self.admin_compute_client.wait_for_server( server, wait=self._wait_for_timeout ) self.assertIsInstance(server, _server.Server) @@ -58,7 +60,7 @@ def test_server(self): # get server details (admin-specific fields) - server = self.operator_cloud.compute.get_server(server.id) + server = self.admin_compute_client.get_server(server.id) self.assertIsNotNone(server.reservation_id) self.assertIsNotNone(server.launch_index) self.assertIsNotNone(server.ramdisk_id) @@ -84,10 +86,10 @@ def setUp(self): self.addCleanup(self._delete_network, self.network, self.subnet) def _delete_server(self, server): - sot = self.user_cloud.compute.delete_server(server.id) + sot = self.compute_client.delete_server(server.id) self.assertIsNone(sot) # Need to wait for the stack to go away before network delete - self.user_cloud.compute.wait_for_delete( + self.compute_client.wait_for_delete( server, wait=self._wait_for_timeout ) @@ -97,13 +99,13 @@ def _delete_network(self, network, subnet): def test_server(self): # create server - self.server = self.user_cloud.compute.create_server( + self.server = self.compute_client.create_server( name=self.server_name, flavor_id=self.flavor.id, image_id=self.image.id, networks=[{"uuid": self.network.id}], ) - self.user_cloud.compute.wait_for_server( + self.compute_client.wait_for_server( self.server, wait=self._wait_for_timeout ) self.addCleanup(self._delete_server, self.server) @@ -112,30 +114,30 @@ def test_server(self): # find server by name - server = self.user_cloud.compute.find_server(self.server_name) + server = self.compute_client.find_server(self.server_name) self.assertEqual(self.server.id, server.id) # get server by ID - server = self.user_cloud.compute.get_server(self.server.id) + server = self.compute_client.get_server(self.server.id) self.assertEqual(self.server_name, server.name) self.assertEqual(self.server.id, server.id) # list servers - server = self.user_cloud.compute.servers() + server = self.compute_client.servers() self.assertIn(self.server_name, {x.name for x in server}) def test_server_metadata(self): # create server - server = self.user_cloud.compute.create_server( + server = self.compute_client.create_server( name=self.server_name, flavor_id=self.flavor.id, image_id=self.image.id, networks=[{"uuid": self.network.id}], ) - self.user_cloud.compute.wait_for_server( + self.compute_client.wait_for_server( server, wait=self._wait_for_timeout ) self.assertIsInstance(server, _server.Server) @@ -143,25 +145,25 @@ def test_server_metadata(self): # get metadata (should be empty initially) - server = self.user_cloud.compute.get_server_metadata(server) + server = self.compute_client.get_server_metadata(server) self.assertFalse(server.metadata) # set no metadata - self.user_cloud.compute.set_server_metadata(server) - server = self.user_cloud.compute.get_server_metadata(server) + self.compute_client.set_server_metadata(server) + server = self.compute_client.get_server_metadata(server) self.assertFalse(server.metadata) # set empty metadata - self.user_cloud.compute.set_server_metadata(server, k0='') - server = self.user_cloud.compute.get_server_metadata(server) + self.compute_client.set_server_metadata(server, k0='') + server = self.compute_client.get_server_metadata(server) self.assertTrue(server.metadata) # set metadata - self.user_cloud.compute.set_server_metadata(server, k1='v1') - server = self.user_cloud.compute.get_server_metadata(server) + self.compute_client.set_server_metadata(server, k1='v1') + server = self.compute_client.get_server_metadata(server) self.assertTrue(server.metadata) self.assertEqual(2, len(server.metadata)) self.assertIn('k0', server.metadata) @@ -171,8 +173,8 @@ def test_server_metadata(self): # set more metadata - self.user_cloud.compute.set_server_metadata(server, k2='v2') - server = self.user_cloud.compute.get_server_metadata(server) + self.compute_client.set_server_metadata(server, k2='v2') + server = self.compute_client.get_server_metadata(server) self.assertTrue(server.metadata) self.assertEqual(3, len(server.metadata)) self.assertIn('k0', server.metadata) @@ -184,8 +186,8 @@ def test_server_metadata(self): # update metadata - self.user_cloud.compute.set_server_metadata(server, k1='v1.1') - server = self.user_cloud.compute.get_server_metadata(server) + self.compute_client.set_server_metadata(server, k1='v1.1') + server = self.compute_client.get_server_metadata(server) self.assertTrue(server.metadata) self.assertEqual(3, len(server.metadata)) self.assertIn('k0', server.metadata) @@ -197,10 +199,10 @@ def test_server_metadata(self): # delete all metadata (cleanup) - self.user_cloud.compute.delete_server_metadata( + self.compute_client.delete_server_metadata( server, server.metadata.keys() ) - server = self.user_cloud.compute.get_server_metadata(server) + server = self.compute_client.get_server_metadata(server) self.assertFalse(server.metadata) def test_server_remote_console(self): @@ -214,13 +216,13 @@ def test_server_remote_console(self): # create server - server = self.user_cloud.compute.create_server( + server = self.compute_client.create_server( name=self.server_name, flavor_id=self.flavor.id, image_id=self.image.id, networks=[{"uuid": network.id}], ) - self.user_cloud.compute.wait_for_server( + self.compute_client.wait_for_server( server, wait=self._wait_for_timeout ) self.assertIsInstance(server, _server.Server) @@ -228,7 +230,7 @@ def test_server_remote_console(self): # create remote console - console = self.user_cloud.compute.create_server_remote_console( + console = self.compute_client.create_server_remote_console( server, protocol='vnc', type='novnc' ) self.assertEqual('vnc', console.protocol) diff --git a/openstack/tests/functional/compute/v2/test_service.py b/openstack/tests/functional/compute/v2/test_service.py index a8359190d..2062558dc 100644 --- a/openstack/tests/functional/compute/v2/test_service.py +++ b/openstack/tests/functional/compute/v2/test_service.py @@ -16,10 +16,10 @@ class TestService(base.BaseComputeTest): def test_service(self): # list all services - services = list(self.operator_cloud.compute.services()) + services = list(self.admin_compute_client.services()) self.assertIsNotNone(services) # find a service - self.operator_cloud.compute.find_service( + self.admin_compute_client.find_service( services[0].name, host=services[0].host, ignore_missing=False ) diff --git a/openstack/tests/functional/compute/v2/test_volume_attachment.py b/openstack/tests/functional/compute/v2/test_volume_attachment.py index ebe3e2ff3..37edde41b 100644 --- a/openstack/tests/functional/compute/v2/test_volume_attachment.py +++ b/openstack/tests/functional/compute/v2/test_volume_attachment.py @@ -20,31 +20,28 @@ class TestServerVolumeAttachment(base.BaseComputeTest): def setUp(self): super().setUp() - if not self.user_cloud.has_service('block-storage'): - self.skipTest('block-storage service not supported by cloud') - self.server_name = self.getUniqueString() self.volume_name = self.getUniqueString() # create the server and volume - server = self.user_cloud.compute.create_server( + server = self.compute_client.create_server( name=self.server_name, flavor_id=self.flavor.id, image_id=self.image.id, networks='none', ) - self.user_cloud.compute.wait_for_server( + self.compute_client.wait_for_server( server, wait=self._wait_for_timeout ) self.addCleanup(self._delete_server, server) self.assertIsInstance(server, _server.Server) self.assertEqual(self.server_name, server.name) - volume = self.user_cloud.block_storage.create_volume( + volume = self.block_storage_client.create_volume( name=self.volume_name, size=1 ) - self.user_cloud.block_storage.wait_for_status( + self.block_storage_client.wait_for_status( volume, status='available', wait=self._wait_for_timeout ) self.addCleanup(self._delete_volume, volume) @@ -55,34 +52,34 @@ def setUp(self): self.volume = volume def _delete_server(self, server): - self.user_cloud.compute.delete_server(server.id) - self.user_cloud.compute.wait_for_delete( + self.compute_client.delete_server(server.id) + self.compute_client.wait_for_delete( server, wait=self._wait_for_timeout ) def _delete_volume(self, volume): - self.user_cloud.block_storage.delete_volume(volume.id) - self.user_cloud.block_storage.wait_for_delete( + self.block_storage_client.delete_volume(volume.id) + self.block_storage_client.wait_for_delete( volume, wait=self._wait_for_timeout ) def test_volume_attachment(self): # create the volume attachment - volume_attachment = self.user_cloud.compute.create_volume_attachment( + volume_attachment = self.compute_client.create_volume_attachment( self.server, self.volume ) self.assertIsInstance( volume_attachment, _volume_attachment.VolumeAttachment ) - self.user_cloud.block_storage.wait_for_status( + self.block_storage_client.wait_for_status( self.volume, status='in-use', wait=self._wait_for_timeout ) # list all attached volume attachments (there should only be one) volume_attachments = list( - self.user_cloud.compute.volume_attachments(self.server) + self.compute_client.volume_attachments(self.server) ) self.assertEqual(1, len(volume_attachments)) self.assertIsInstance( @@ -91,7 +88,7 @@ def test_volume_attachment(self): # update the volume attachment - volume_attachment = self.user_cloud.compute.update_volume_attachment( + volume_attachment = self.compute_client.update_volume_attachment( self.server, self.volume, delete_on_termination=True ) self.assertIsInstance( @@ -100,7 +97,7 @@ def test_volume_attachment(self): # retrieve details of the (updated) volume attachment - volume_attachment = self.user_cloud.compute.get_volume_attachment( + volume_attachment = self.compute_client.get_volume_attachment( self.server, self.volume ) self.assertIsInstance( @@ -110,24 +107,24 @@ def test_volume_attachment(self): # delete the volume attachment - result = self.user_cloud.compute.delete_volume_attachment( + result = self.compute_client.delete_volume_attachment( self.server, self.volume, ignore_missing=False ) self.assertIsNone(result) - self.user_cloud.block_storage.wait_for_status( + self.block_storage_client.wait_for_status( self.volume, status='available', wait=self._wait_for_timeout ) # Wait for the attachment to be deleted. # This is done to prevent a race between the BDM # record being deleted and we trying to delete the server. - self.user_cloud.compute.wait_for_delete( + self.compute_client.wait_for_delete( volume_attachment, wait=self._wait_for_timeout ) # Verify the server doesn't have any volume attachment volume_attachments = list( - self.user_cloud.compute.volume_attachments(self.server) + self.compute_client.volume_attachments(self.server) ) self.assertEqual(0, len(volume_attachments)) diff --git a/openstack/tests/functional/identity/v3/base.py b/openstack/tests/functional/identity/v3/base.py new file mode 100644 index 000000000..7ca748def --- /dev/null +++ b/openstack/tests/functional/identity/v3/base.py @@ -0,0 +1,32 @@ +# 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.identity.v3 import _proxy as _identity_v3 +from openstack.tests.functional import base +from openstack import utils + + +class BaseIdentityTest(base.BaseFunctionalTest): + admin_identity_client: _identity_v3.Proxy + system_admin_identity_client: _identity_v3.Proxy + + def setUp(self): + super().setUp() + if not self.operator_cloud.has_service('identity', '3'): + self.skipTest('identity service not supported by cloud') + + self.admin_identity_client = utils.ensure_service_version( + self.operator_cloud.identity, '3' + ) + self.system_admin_identity_client = utils.ensure_service_version( + self.system_admin_cloud.identity, '3' + ) diff --git a/openstack/tests/functional/identity/v3/test_access_rule.py b/openstack/tests/functional/identity/v3/test_access_rule.py index 4a4c83b9a..e6bda334a 100644 --- a/openstack/tests/functional/identity/v3/test_access_rule.py +++ b/openstack/tests/functional/identity/v3/test_access_rule.py @@ -11,10 +11,10 @@ # under the License. from openstack import exceptions -from openstack.tests.functional import base +from openstack.tests.functional.identity.v3 import base -class TestAccessRule(base.BaseFunctionalTest): +class TestAccessRule(base.BaseIdentityTest): def setUp(self): super().setUp() self.user_id = self.operator_cloud.current_user_id @@ -22,7 +22,7 @@ def setUp(self): def _create_application_credential_with_access_rule(self): """create application credential with access_rule.""" - app_cred = self.operator_cloud.identity.create_application_credential( + app_cred = self.admin_identity_client.create_application_credential( user=self.user_id, name='app_cred', access_rules=[ @@ -34,7 +34,7 @@ def _create_application_credential_with_access_rule(self): ], ) self.addCleanup( - self.operator_cloud.identity.delete_application_credential, + self.admin_identity_client.delete_application_credential, self.user_id, app_cred['id'], ) @@ -43,7 +43,7 @@ def _create_application_credential_with_access_rule(self): def test_get_access_rule(self): app_cred = self._create_application_credential_with_access_rule() access_rule_id = app_cred['access_rules'][0]['id'] - access_rule = self.operator_cloud.identity.get_access_rule( + access_rule = self.admin_identity_client.get_access_rule( user=self.user_id, access_rule=access_rule_id ) self.assertEqual(access_rule['id'], access_rule_id) @@ -52,7 +52,7 @@ def test_get_access_rule(self): def test_list_access_rules(self): app_cred = self._create_application_credential_with_access_rule() access_rule_id = app_cred['access_rules'][0]['id'] - access_rules = self.operator_cloud.identity.access_rules( + access_rules = self.admin_identity_client.access_rules( user=self.user_id ) self.assertEqual(1, len(list(access_rules))) @@ -68,16 +68,16 @@ def test_delete_access_rule(self): # in use for app_cred. self.assertRaises( exceptions.HttpException, - self.operator_cloud.identity.delete_access_rule, + self.admin_identity_client.delete_access_rule, user=self.user_id, access_rule=access_rule_id, ) # delete application credential first to delete access rule - self.operator_cloud.identity.delete_application_credential( + self.admin_identity_client.delete_application_credential( user=self.user_id, application_credential=app_cred['id'] ) # delete orphaned access rules - self.operator_cloud.identity.delete_access_rule( + self.admin_identity_client.delete_access_rule( user=self.user_id, access_rule=access_rule_id ) diff --git a/openstack/tests/functional/identity/v3/test_application_credential.py b/openstack/tests/functional/identity/v3/test_application_credential.py index 2afbaac09..9b36e63e2 100644 --- a/openstack/tests/functional/identity/v3/test_application_credential.py +++ b/openstack/tests/functional/identity/v3/test_application_credential.py @@ -11,20 +11,20 @@ # under the License. from openstack import exceptions -from openstack.tests.functional import base +from openstack.tests.functional.identity.v3 import base -class TestApplicationCredentials(base.BaseFunctionalTest): +class TestApplicationCredentials(base.BaseIdentityTest): def setUp(self): super().setUp() self.user_id = self.operator_cloud.current_user_id def _create_application_credentials(self): - app_creds = self.operator_cloud.identity.create_application_credential( + app_creds = self.admin_identity_client.create_application_credential( user=self.user_id, name='app_cred' ) self.addCleanup( - self.operator_cloud.identity.delete_application_credential, + self.admin_identity_client.delete_application_credential, self.user_id, app_creds['id'], ) @@ -36,7 +36,7 @@ def test_create_application_credentials(self): def test_get_application_credential(self): app_creds = self._create_application_credentials() - app_cred = self.operator_cloud.identity.get_application_credential( + app_cred = self.admin_identity_client.get_application_credential( user=self.user_id, application_credential=app_creds['id'] ) self.assertEqual(app_cred['id'], app_creds['id']) @@ -44,7 +44,7 @@ def test_get_application_credential(self): def test_application_credentials(self): self._create_application_credentials() - app_creds = self.operator_cloud.identity.application_credentials( + app_creds = self.admin_identity_client.application_credentials( user=self.user_id ) for app_cred in app_creds: @@ -52,7 +52,7 @@ def test_application_credentials(self): def test_find_application_credential(self): app_creds = self._create_application_credentials() - app_cred = self.operator_cloud.identity.find_application_credential( + app_cred = self.admin_identity_client.find_application_credential( user=self.user_id, name_or_id=app_creds['id'] ) self.assertEqual(app_cred['id'], app_creds['id']) @@ -60,12 +60,12 @@ def test_find_application_credential(self): def test_delete_application_credential(self): app_creds = self._create_application_credentials() - self.operator_cloud.identity.delete_application_credential( + self.admin_identity_client.delete_application_credential( user=self.user_id, application_credential=app_creds['id'] ) self.assertRaises( exceptions.NotFoundException, - self.operator_cloud.identity.get_application_credential, + self.admin_identity_client.get_application_credential, user=self.user_id, application_credential=app_creds['id'], ) diff --git a/openstack/tests/functional/identity/v3/test_domain_config.py b/openstack/tests/functional/identity/v3/test_domain_config.py index e294498f4..e2159bb93 100644 --- a/openstack/tests/functional/identity/v3/test_domain_config.py +++ b/openstack/tests/functional/identity/v3/test_domain_config.py @@ -14,10 +14,10 @@ from openstack.identity.v3 import domain as _domain from openstack.identity.v3 import domain_config as _domain_config -from openstack.tests.functional import base +from openstack.tests.functional.identity.v3 import base -class TestDomainConfig(base.BaseFunctionalTest): +class TestDomainConfig(base.BaseIdentityTest): def setUp(self): super().setUp() @@ -32,16 +32,16 @@ def setUp(self): self.addCleanup(self._delete_domain) def _delete_domain(self): - self.operator_cloud.identity.update_domain( + self.admin_identity_client.update_domain( self.domain, enabled=False, ) - self.operator_cloud.identity.delete_domain(self.domain) + self.admin_identity_client.delete_domain(self.domain) def test_domain_config(self): # create the domain config - domain_config = self.operator_cloud.identity.create_domain_config( + domain_config = self.admin_identity_client.create_domain_config( self.domain, identity={'driver': uuid.uuid4().hex}, ldap={'url': uuid.uuid4().hex}, @@ -54,7 +54,7 @@ def test_domain_config(self): # update the domain config ldap_url = uuid.uuid4().hex - domain_config = self.operator_cloud.identity.update_domain_config( + domain_config = self.admin_identity_client.update_domain_config( self.domain, ldap={'url': ldap_url}, ) @@ -65,7 +65,7 @@ def test_domain_config(self): # retrieve details of the (updated) domain config - domain_config = self.operator_cloud.identity.get_domain_config( + domain_config = self.admin_identity_client.get_domain_config( self.domain, ) self.assertIsInstance( @@ -76,7 +76,7 @@ def test_domain_config(self): # delete the domain config - result = self.operator_cloud.identity.delete_domain_config( + result = self.admin_identity_client.delete_domain_config( self.domain, ignore_missing=False, ) diff --git a/openstack/tests/functional/identity/v3/test_endpoint.py b/openstack/tests/functional/identity/v3/test_endpoint.py index d37392683..2f81b0e0d 100644 --- a/openstack/tests/functional/identity/v3/test_endpoint.py +++ b/openstack/tests/functional/identity/v3/test_endpoint.py @@ -11,42 +11,40 @@ # under the License. from openstack.identity.v3 import endpoint as _endpoint -from openstack.tests.functional import base +from openstack.tests.functional.identity.v3 import base -class TestEndpoint(base.BaseFunctionalTest): +class TestEndpoint(base.BaseIdentityTest): def setUp(self): super().setUp() self.service_name = self.getUniqueString('service') self.service_type = self.getUniqueString('type') - self.service = self.operator_cloud.identity.create_service( + self.service = self.admin_identity_client.create_service( name=self.service_name, type=self.service_type, ) self.addCleanup( - self.operator_cloud.identity.delete_service, self.service + self.admin_identity_client.delete_service, self.service ) self.region_name = self.getUniqueString('region') - self.region = self.operator_cloud.identity.create_region( + self.region = self.admin_identity_client.create_region( name=self.region_name ) - self.addCleanup( - self.operator_cloud.identity.delete_region, self.region - ) + self.addCleanup(self.admin_identity_client.delete_region, self.region) unique_base = self.getUniqueString('endpoint') self.test_url = f'https://{unique_base}.example.com/v1' self.updated_url = f'https://{unique_base}.example.com/v2' def _delete_endpoint(self, endpoint): - ret = self.operator_cloud.identity.delete_endpoint(endpoint) + ret = self.admin_identity_client.delete_endpoint(endpoint) self.assertIsNone(ret) def test_endpoint(self): # Create public endpoint - public_endpoint = self.operator_cloud.identity.create_endpoint( + public_endpoint = self.admin_identity_client.create_endpoint( service_id=self.service.id, interface='public', url=self.test_url, @@ -63,7 +61,7 @@ def test_endpoint(self): self.assertTrue(public_endpoint.is_enabled) # Create internal endpoint for filter testing - internal_endpoint = self.operator_cloud.identity.create_endpoint( + internal_endpoint = self.admin_identity_client.create_endpoint( service_id=self.service.id, interface='internal', url=self.test_url, @@ -75,7 +73,7 @@ def test_endpoint(self): self.assertEqual('internal', internal_endpoint.interface) # Update public endpoint - public_endpoint = self.operator_cloud.identity.update_endpoint( + public_endpoint = self.admin_identity_client.update_endpoint( public_endpoint, url=self.updated_url, is_enabled=False, @@ -85,7 +83,7 @@ def test_endpoint(self): self.assertFalse(public_endpoint.is_enabled) # Get endpoint by ID - public_endpoint = self.operator_cloud.identity.get_endpoint( + public_endpoint = self.admin_identity_client.get_endpoint( public_endpoint.id ) self.assertIsInstance(public_endpoint, _endpoint.Endpoint) @@ -93,14 +91,14 @@ def test_endpoint(self): self.assertFalse(public_endpoint.is_enabled) # Find endpoint - found_endpoint = self.operator_cloud.identity.find_endpoint( + found_endpoint = self.admin_identity_client.find_endpoint( public_endpoint.id ) self.assertIsInstance(found_endpoint, _endpoint.Endpoint) self.assertEqual(public_endpoint.id, found_endpoint.id) # List endpoints - endpoints = list(self.operator_cloud.identity.endpoints()) + endpoints = list(self.admin_identity_client.endpoints()) self.assertIsInstance(endpoints[0], _endpoint.Endpoint) endpoint_ids = {ep.id for ep in endpoints} self.assertIn(public_endpoint.id, endpoint_ids) @@ -108,7 +106,7 @@ def test_endpoint(self): # Test service filter service_endpoints = list( - self.operator_cloud.identity.endpoints(service_id=self.service.id) + self.admin_identity_client.endpoints(service_id=self.service.id) ) service_endpoint_ids = {ep.id for ep in service_endpoints} self.assertIn(public_endpoint.id, service_endpoint_ids) @@ -116,20 +114,20 @@ def test_endpoint(self): # Test interface filter public_endpoints = list( - self.operator_cloud.identity.endpoints(interface='public') + self.admin_identity_client.endpoints(interface='public') ) public_endpoint_ids = {ep.id for ep in public_endpoints} self.assertIn(public_endpoint.id, public_endpoint_ids) internal_endpoints = list( - self.operator_cloud.identity.endpoints(interface='internal') + self.admin_identity_client.endpoints(interface='internal') ) internal_endpoint_ids = {ep.id for ep in internal_endpoints} self.assertIn(internal_endpoint.id, internal_endpoint_ids) # Test region filter region_endpoints = list( - self.operator_cloud.identity.endpoints(region_id=self.region.id) + self.admin_identity_client.endpoints(region_id=self.region.id) ) region_endpoint_ids = {ep.id for ep in region_endpoints} self.assertIn(public_endpoint.id, region_endpoint_ids) diff --git a/openstack/tests/functional/identity/v3/test_group.py b/openstack/tests/functional/identity/v3/test_group.py index c9070066a..a8a3b4c56 100644 --- a/openstack/tests/functional/identity/v3/test_group.py +++ b/openstack/tests/functional/identity/v3/test_group.py @@ -12,10 +12,10 @@ from openstack.identity.v3 import group as _group from openstack.identity.v3 import user as _user -from openstack.tests.functional import base +from openstack.tests.functional.identity.v3 import base -class TestGroup(base.BaseFunctionalTest): +class TestGroup(base.BaseIdentityTest): def setUp(self): super().setUp() @@ -24,24 +24,24 @@ def setUp(self): self.user_name = self.getUniqueString('user') self.user_email = f"{self.user_name}@example.com" - self.user = self.operator_cloud.identity.create_user( + self.user = self.admin_identity_client.create_user( name=self.user_name, email=self.user_email, ) self.addCleanup(self._delete_user, self.user) def _delete_group(self, group): - ret = self.operator_cloud.identity.delete_group(group) + ret = self.admin_identity_client.delete_group(group) self.assertIsNone(ret) def _delete_user(self, user): - ret = self.operator_cloud.identity.delete_user(user) + ret = self.admin_identity_client.delete_user(user) self.assertIsNone(ret) def test_group(self): # create the group - group = self.operator_cloud.identity.create_group( + group = self.admin_identity_client.create_group( name=self.group_name, ) self.addCleanup(self._delete_group, group) @@ -50,7 +50,7 @@ def test_group(self): # update the group - group = self.operator_cloud.identity.update_group( + group = self.admin_identity_client.update_group( group, description=self.group_description ) self.assertIsInstance(group, _group.Group) @@ -58,19 +58,19 @@ def test_group(self): # retrieve details of the (updated) group by ID - group = self.operator_cloud.identity.get_group(group.id) + group = self.admin_identity_client.get_group(group.id) self.assertIsInstance(group, _group.Group) self.assertEqual(self.group_description, group.description) # retrieve details of the (updated) group by name - group = self.operator_cloud.identity.find_group(group.name) + group = self.admin_identity_client.find_group(group.name) self.assertIsInstance(group, _group.Group) self.assertEqual(self.group_description, group.description) # list all groups - groups = list(self.operator_cloud.identity.groups()) + groups = list(self.admin_identity_client.groups()) self.assertIsInstance(groups[0], _group.Group) self.assertIn( self.group_name, @@ -78,22 +78,22 @@ def test_group(self): ) # add user to group - self.operator_cloud.identity.add_user_to_group(self.user, group) + self.admin_identity_client.add_user_to_group(self.user, group) - is_in_group = self.operator_cloud.identity.check_user_in_group( + is_in_group = self.admin_identity_client.check_user_in_group( self.user, group ) self.assertTrue(is_in_group) - group_users = list(self.operator_cloud.identity.group_users(group)) + group_users = list(self.admin_identity_client.group_users(group)) self.assertIsInstance(group_users[0], _user.User) self.assertIn(self.user_name, {x.name for x in group_users}) # remove user from group - self.operator_cloud.identity.remove_user_from_group(self.user, group) + self.admin_identity_client.remove_user_from_group(self.user, group) - is_in_group = self.operator_cloud.identity.check_user_in_group( + is_in_group = self.admin_identity_client.check_user_in_group( self.user, group ) self.assertFalse(is_in_group) diff --git a/openstack/tests/functional/identity/v3/test_limit.py b/openstack/tests/functional/identity/v3/test_limit.py index b49884fbc..59be355fc 100644 --- a/openstack/tests/functional/identity/v3/test_limit.py +++ b/openstack/tests/functional/identity/v3/test_limit.py @@ -11,54 +11,54 @@ # under the License. from openstack.identity.v3 import limit as _limit -from openstack.tests.functional import base +from openstack.tests.functional.identity.v3 import base -class TestLimit(base.BaseFunctionalTest): +class TestLimit(base.BaseIdentityTest): def setUp(self): super().setUp() self.service_name = self.getUniqueString('service') self.service_type = self.getUniqueString('type') - self.service = self.system_admin_cloud.identity.create_service( + self.service = self.system_admin_identity_client.create_service( name=self.service_name, type=self.service_type, ) self.addCleanup( - self.system_admin_cloud.identity.delete_service, self.service + self.system_admin_identity_client.delete_service, self.service ) self.resource_name = self.getUniqueString('resource') self.registered_limit = ( - self.system_admin_cloud.identity.create_registered_limit( + self.system_admin_identity_client.create_registered_limit( resource_name=self.resource_name, service_id=self.service.id, default_limit=10, ) ) self.addCleanup( - self.system_admin_cloud.identity.delete_registered_limit, + self.system_admin_identity_client.delete_registered_limit, self.registered_limit, ) self.project_name = self.getUniqueString('project') - self.project = self.system_admin_cloud.identity.create_project( + self.project = self.system_admin_identity_client.create_project( name=self.project_name, ) self.addCleanup( - self.system_admin_cloud.identity.delete_project, self.project + self.system_admin_identity_client.delete_project, self.project ) self.limit_description = self.getUniqueString('limit') def _delete_limit(self, limit): - ret = self.system_admin_cloud.identity.delete_limit(limit) + ret = self.system_admin_identity_client.delete_limit(limit) self.assertIsNone(ret) def test_limit(self): # create the limit - limit = self.system_admin_cloud.identity.create_limit( + limit = self.system_admin_identity_client.create_limit( resource_name=self.resource_name, service_id=self.service.id, project_id=self.project.id, @@ -74,7 +74,7 @@ def test_limit(self): # update the limit - limit = self.system_admin_cloud.identity.update_limit( + limit = self.system_admin_identity_client.update_limit( limit, description=self.limit_description ) self.assertIsInstance(limit, _limit.Limit) @@ -82,7 +82,7 @@ def test_limit(self): # retrieve details of the (updated) limit by ID - limit = self.system_admin_cloud.identity.get_limit(limit.id) + limit = self.system_admin_identity_client.get_limit(limit.id) self.assertIsInstance(limit, _limit.Limit) self.assertEqual(self.limit_description, limit.description) @@ -90,7 +90,7 @@ def test_limit(self): # list all limits - limits = list(self.system_admin_cloud.identity.limits()) + limits = list(self.system_admin_identity_client.limits()) self.assertIsInstance(limits[0], _limit.Limit) self.assertIn( self.resource_name, diff --git a/openstack/tests/functional/identity/v3/test_project.py b/openstack/tests/functional/identity/v3/test_project.py index 80225d6d0..20455bd55 100644 --- a/openstack/tests/functional/identity/v3/test_project.py +++ b/openstack/tests/functional/identity/v3/test_project.py @@ -11,10 +11,10 @@ # under the License. from openstack.identity.v3 import project as _project -from openstack.tests.functional import base +from openstack.tests.functional.identity.v3 import base -class TestProject(base.BaseFunctionalTest): +class TestProject(base.BaseIdentityTest): def setUp(self): super().setUp() @@ -22,13 +22,13 @@ def setUp(self): self.project_description = self.getUniqueString('project') def _delete_project(self, project): - ret = self.operator_cloud.identity.delete_project(project) + ret = self.admin_identity_client.delete_project(project) self.assertIsNone(ret) def test_project(self): # create the project - project = self.operator_cloud.identity.create_project( + project = self.admin_identity_client.create_project( name=self.project_name, ) self.assertIsInstance(project, _project.Project) @@ -37,7 +37,7 @@ def test_project(self): # update the project - project = self.operator_cloud.identity.update_project( + project = self.admin_identity_client.update_project( project, description=self.project_description ) self.assertIsInstance(project, _project.Project) @@ -45,19 +45,19 @@ def test_project(self): # retrieve details of the (updated) project by ID - project = self.operator_cloud.identity.get_project(project.id) + project = self.admin_identity_client.get_project(project.id) self.assertIsInstance(project, _project.Project) self.assertEqual(self.project_description, project.description) # retrieve details of the (updated) project by name - project = self.operator_cloud.identity.find_project(project.name) + project = self.admin_identity_client.find_project(project.name) self.assertIsInstance(project, _project.Project) self.assertEqual(self.project_description, project.description) # list all projects - projects = list(self.operator_cloud.identity.projects()) + projects = list(self.admin_identity_client.projects()) self.assertIsInstance(projects[0], _project.Project) self.assertIn( self.project_name, @@ -68,7 +68,7 @@ def test_user_project(self): # list all user projects user_projects = list( - self.operator_cloud.identity.user_projects( + self.admin_identity_client.user_projects( self.operator_cloud.current_user_id ) ) diff --git a/openstack/tests/functional/identity/v3/test_registered_limit.py b/openstack/tests/functional/identity/v3/test_registered_limit.py index 6701cfc15..d9c3d8230 100644 --- a/openstack/tests/functional/identity/v3/test_registered_limit.py +++ b/openstack/tests/functional/identity/v3/test_registered_limit.py @@ -11,29 +11,29 @@ # under the License. from openstack.identity.v3 import registered_limit as _registered_limit -from openstack.tests.functional import base +from openstack.tests.functional.identity.v3 import base -class TestRegisteredLimit(base.BaseFunctionalTest): +class TestRegisteredLimit(base.BaseIdentityTest): def setUp(self): super().setUp() self.region_name = self.getUniqueString('region') - self.region = self.system_admin_cloud.identity.create_region( + self.region = self.system_admin_identity_client.create_region( name=self.region_name ) self.addCleanup( - self.system_admin_cloud.identity.delete_region, self.region + self.system_admin_identity_client.delete_region, self.region ) self.service_name = self.getUniqueString('service') self.service_type = self.getUniqueString('type') - self.service = self.system_admin_cloud.identity.create_service( + self.service = self.system_admin_identity_client.create_service( name=self.service_name, type=self.service_type, ) self.addCleanup( - self.system_admin_cloud.identity.delete_service, self.service + self.system_admin_identity_client.delete_service, self.service ) self.resource_name = self.getUniqueString('resource') @@ -42,7 +42,7 @@ def setUp(self): ) def _delete_registered_limit(self, registered_limit): - ret = self.system_admin_cloud.identity.delete_registered_limit( + ret = self.system_admin_identity_client.delete_registered_limit( registered_limit ) self.assertIsNone(ret) @@ -51,7 +51,7 @@ def test_registered_limit(self): # create the registered limit registered_limit = ( - self.system_admin_cloud.identity.create_registered_limit( + self.system_admin_identity_client.create_registered_limit( resource_name=self.resource_name, service_id=self.service.id, region_id=self.region.id, @@ -70,7 +70,7 @@ def test_registered_limit(self): # update the registered limit registered_limit = ( - self.system_admin_cloud.identity.update_registered_limit( + self.system_admin_identity_client.update_registered_limit( registered_limit, description=self.registered_limit_description ) ) @@ -84,7 +84,7 @@ def test_registered_limit(self): # retrieve details of the (updated) registered limit by ID registered_limit = ( - self.system_admin_cloud.identity.get_registered_limit( + self.system_admin_identity_client.get_registered_limit( registered_limit.id ) ) @@ -100,7 +100,7 @@ def test_registered_limit(self): # list all registered limits registered_limits = list( - self.system_admin_cloud.identity.registered_limits() + self.system_admin_identity_client.registered_limits() ) self.assertIsInstance( registered_limits[0], _registered_limit.RegisteredLimit diff --git a/openstack/tests/functional/identity/v3/test_user.py b/openstack/tests/functional/identity/v3/test_user.py index 2a0121590..a10399a3b 100644 --- a/openstack/tests/functional/identity/v3/test_user.py +++ b/openstack/tests/functional/identity/v3/test_user.py @@ -11,10 +11,10 @@ # under the License. from openstack.identity.v3 import user as _user -from openstack.tests.functional import base +from openstack.tests.functional.identity.v3 import base -class TestUser(base.BaseFunctionalTest): +class TestUser(base.BaseIdentityTest): def setUp(self): super().setUp() @@ -24,12 +24,12 @@ def setUp(self): self.description = "Test user for functional testing" def _delete_user(self, user): - ret = self.operator_cloud.identity.delete_user(user) + ret = self.admin_identity_client.delete_user(user) self.assertIsNone(ret) def test_user(self): # Create user - user = self.operator_cloud.identity.create_user( + user = self.admin_identity_client.create_user( name=self.username, password=self.password, email=self.email, @@ -46,7 +46,7 @@ def test_user(self): new_email = f"updated_{self.username}@example.com" new_description = "Updated description for test user" - updated_user = self.operator_cloud.identity.update_user( + updated_user = self.admin_identity_client.update_user( user.id, email=new_email, description=new_description ) self.assertIsInstance(updated_user, _user.User) @@ -57,13 +57,13 @@ def test_user(self): ) # Name should remain unchanged # Read user list - users = list(self.operator_cloud.identity.users()) + users = list(self.admin_identity_client.users()) self.assertIsInstance(users[0], _user.User) user_ids = {ep.id for ep in users} self.assertIn(user.id, user_ids) # Read user by ID - user = self.operator_cloud.identity.get_user(user.id) + user = self.admin_identity_client.get_user(user.id) self.assertIsInstance(user, _user.User) self.assertEqual(user.id, user.id) self.assertEqual(self.username, user.name) diff --git a/openstack/tests/functional/image/v2/base.py b/openstack/tests/functional/image/v2/base.py index 290ee0a46..cc698ed37 100644 --- a/openstack/tests/functional/image/v2/base.py +++ b/openstack/tests/functional/image/v2/base.py @@ -10,12 +10,17 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.image.v2 import _proxy as _image_v2 from openstack.tests.functional import base +from openstack import utils class BaseImageTest(base.BaseFunctionalTest): _wait_for_timeout_key = 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_IMAGE' + admin_image_client: _image_v2.Proxy + image_client: _image_v2.Proxy + def setUp(self): super().setUp() self._set_user_cloud(image_api_version='2') @@ -23,3 +28,9 @@ def setUp(self): if not self.user_cloud.has_service('image', '2'): self.skipTest('image service not supported by cloud') + self.admin_image_client = utils.ensure_service_version( + self.operator_cloud.image, '2' + ) + self.image_client = utils.ensure_service_version( + self.user_cloud.image, '2' + ) diff --git a/openstack/tests/functional/image/v2/test_image.py b/openstack/tests/functional/image/v2/test_image.py index 89238db7c..4b9234229 100644 --- a/openstack/tests/functional/image/v2/test_image.py +++ b/openstack/tests/functional/image/v2/test_image.py @@ -26,7 +26,7 @@ def setUp(self): data = fh.read() # there's a limit on name length - self.image = self.operator_cloud.image.create_image( + self.image = self.admin_image_client.create_image( name=TEST_IMAGE_NAME, disk_format='raw', container_format='bare', @@ -41,50 +41,50 @@ def setUp(self): def tearDown(self): # we do this in tearDown rather than via 'addCleanup' since we want to # wait for the deletion of the resource to ensure it completes - self.operator_cloud.image.delete_image(self.image) - self.operator_cloud.image.wait_for_delete(self.image) + self.admin_image_client.delete_image(self.image) + self.admin_image_client.wait_for_delete(self.image) super().tearDown() def test_images(self): # get image - image = self.operator_cloud.image.get_image(self.image.id) + image = self.admin_image_client.get_image(self.image.id) self.assertEqual(self.image.name, image.name) # find image - image = self.operator_cloud.image.find_image(self.image.name) + image = self.admin_image_client.find_image(self.image.name) self.assertEqual(self.image.id, image.id) # list - images = list(self.operator_cloud.image.images()) + images = list(self.admin_image_client.images()) # there are many other images so we don't assert that this is the # *only* image present self.assertIn(self.image.id, {i.id for i in images}) # update image_name = self.getUniqueString() - image = self.operator_cloud.image.update_image( + image = self.admin_image_client.update_image( self.image, name=image_name, ) self.assertIsInstance(image, _image.Image) - image = self.operator_cloud.image.get_image(self.image.id) + image = self.admin_image_client.get_image(self.image.id) self.assertEqual(image_name, image.name) def test_tags(self): # add tag - image = self.operator_cloud.image.get_image(self.image) - self.operator_cloud.image.add_tag(image, 't1') - self.operator_cloud.image.add_tag(image, 't2') + image = self.admin_image_client.get_image(self.image) + self.admin_image_client.add_tag(image, 't1') + self.admin_image_client.add_tag(image, 't2') # filter image by tags - image = next(iter(self.operator_cloud.image.images(tag=['t1', 't2']))) + image = next(iter(self.admin_image_client.images(tag=['t1', 't2']))) self.assertEqual(image.id, image.id) self.assertIn('t1', image.tags) self.assertIn('t2', image.tags) # remove tag - self.operator_cloud.image.remove_tag(image, 't1') - image = self.operator_cloud.image.get_image(self.image) + self.admin_image_client.remove_tag(image, 't1') + image = self.admin_image_client.get_image(self.image) self.assertIn('t2', image.tags) self.assertNotIn('t1', image.tags) diff --git a/openstack/tests/functional/image/v2/test_member.py b/openstack/tests/functional/image/v2/test_member.py index ad3bc71a4..5b96a721a 100644 --- a/openstack/tests/functional/image/v2/test_member.py +++ b/openstack/tests/functional/image/v2/test_member.py @@ -26,7 +26,7 @@ def setUp(self): super().setUp() # NOTE(jbeen): 1-byte dummy image data for sharing tests; not bootable. - self.image = self.operator_cloud.image.create_image( + self.image = self.admin_image_client.create_image( name=TEST_IMAGE_NAME, disk_format='raw', container_format='bare', @@ -40,14 +40,14 @@ def setUp(self): self.assertIsNotNone(self.member_id) def tearDown(self): - self.operator_cloud.image.delete_image(self.image) - self.operator_cloud.image.wait_for_delete(self.image) + self.admin_image_client.delete_image(self.image) + self.admin_image_client.wait_for_delete(self.image) super().tearDown() def test_image_members(self): # add member - member = self.operator_cloud.image.add_member( + member = self.admin_image_client.add_member( image=self.image, member=self.member_id ) self.assertIsInstance(member, _member.Member) @@ -55,18 +55,18 @@ def test_image_members(self): self.assertEqual(MEMBER_STATUS_PENDING, member.status) # get member - member = self.operator_cloud.image.get_member( + member = self.admin_image_client.get_member( image=self.image, member=self.member_id ) self.assertIsInstance(member, _member.Member) self.assertEqual(self.member_id, member.member_id) # list members - members = list(self.operator_cloud.image.members(image=self.image)) + members = list(self.admin_image_client.members(image=self.image)) self.assertIn(self.member_id, {m.id for m in members}) # update member - member = self.user_cloud.image.update_member( + member = self.image_client.update_member( image=self.image, member=self.member_id, status=MEMBER_STATUS_ACCEPTED, @@ -76,12 +76,12 @@ def test_image_members(self): self.assertEqual(MEMBER_STATUS_ACCEPTED, member.status) # remove member - self.operator_cloud.image.remove_member( + self.admin_image_client.remove_member( image=self.image, member=self.member_id ) self.assertRaises( sdk_exc.NotFoundException, - self.operator_cloud.image.get_member, + self.admin_image_client.get_member, image=self.image, member=self.member_id, ) diff --git a/openstack/tests/functional/image/v2/test_metadef_namespace.py b/openstack/tests/functional/image/v2/test_metadef_namespace.py index c340b33d5..353091ae2 100644 --- a/openstack/tests/functional/image/v2/test_metadef_namespace.py +++ b/openstack/tests/functional/image/v2/test_metadef_namespace.py @@ -22,7 +22,7 @@ def setUp(self): # there's a limit on namespace length namespace = self.getUniqueString().split('.')[-1] self.metadef_namespace = ( - self.operator_cloud.image.create_metadef_namespace( + self.admin_image_client.create_metadef_namespace( namespace=namespace, ) ) @@ -35,16 +35,16 @@ def setUp(self): def tearDown(self): # we do this in tearDown rather than via 'addCleanup' since we want to # wait for the deletion of the resource to ensure it completes - self.operator_cloud.image.delete_metadef_namespace( + self.admin_image_client.delete_metadef_namespace( self.metadef_namespace ) - self.operator_cloud.image.wait_for_delete(self.metadef_namespace) + self.admin_image_client.wait_for_delete(self.metadef_namespace) super().tearDown() def test_metadef_namespace(self): # get - metadef_namespace = self.operator_cloud.image.get_metadef_namespace( + metadef_namespace = self.admin_image_client.get_metadef_namespace( self.metadef_namespace.namespace ) self.assertEqual( @@ -55,9 +55,7 @@ def test_metadef_namespace(self): # (no find_metadef_namespace method) # list - metadef_namespaces = list( - self.operator_cloud.image.metadef_namespaces() - ) + metadef_namespaces = list(self.admin_image_client.metadef_namespaces()) # there are a load of default metadef namespaces so we don't assert # that this is the *only* metadef namespace present self.assertIn( @@ -70,7 +68,7 @@ def test_metadef_namespace(self): # inherent need for randomness so we use fixed strings metadef_namespace_display_name = 'A display name' metadef_namespace_description = 'A description' - metadef_namespace = self.operator_cloud.image.update_metadef_namespace( + metadef_namespace = self.admin_image_client.update_metadef_namespace( self.metadef_namespace, display_name=metadef_namespace_display_name, description=metadef_namespace_description, @@ -79,7 +77,7 @@ def test_metadef_namespace(self): metadef_namespace, _metadef_namespace.MetadefNamespace, ) - metadef_namespace = self.operator_cloud.image.get_metadef_namespace( + metadef_namespace = self.admin_image_client.get_metadef_namespace( self.metadef_namespace.namespace ) self.assertEqual( @@ -93,21 +91,21 @@ def test_metadef_namespace(self): def test_tags(self): # add tag - metadef_namespace = self.operator_cloud.image.get_metadef_namespace( + metadef_namespace = self.admin_image_client.get_metadef_namespace( self.metadef_namespace.namespace ) - metadef_namespace.add_tag(self.operator_cloud.image, 't1') - metadef_namespace.add_tag(self.operator_cloud.image, 't2') + metadef_namespace.add_tag(self.admin_image_client, 't1') + metadef_namespace.add_tag(self.admin_image_client, 't2') # list tags - metadef_namespace.fetch_tags(self.operator_cloud.image) + metadef_namespace.fetch_tags(self.admin_image_client) md_tags = [tag['name'] for tag in metadef_namespace.tags] self.assertIn('t1', md_tags) self.assertIn('t2', md_tags) # remove tag - metadef_namespace.remove_tag(self.operator_cloud.image, 't1') - metadef_namespace = self.operator_cloud.image.get_metadef_namespace( + metadef_namespace.remove_tag(self.admin_image_client, 't1') + metadef_namespace = self.admin_image_client.get_metadef_namespace( self.metadef_namespace.namespace ) md_tags = [tag['name'] for tag in metadef_namespace.tags] @@ -115,17 +113,17 @@ def test_tags(self): self.assertNotIn('t1', md_tags) # add tags without append - metadef_namespace.set_tags(self.operator_cloud.image, ["t1", "t2"]) - metadef_namespace.fetch_tags(self.operator_cloud.image) + metadef_namespace.set_tags(self.admin_image_client, ["t1", "t2"]) + metadef_namespace.fetch_tags(self.admin_image_client) md_tags = [tag['name'] for tag in metadef_namespace.tags] self.assertIn('t1', md_tags) self.assertIn('t2', md_tags) # add tags with append metadef_namespace.set_tags( - self.operator_cloud.image, ["t3", "t4"], append=True + self.admin_image_client, ["t3", "t4"], append=True ) - metadef_namespace.fetch_tags(self.operator_cloud.image) + metadef_namespace.fetch_tags(self.admin_image_client) md_tags = [tag['name'] for tag in metadef_namespace.tags] self.assertIn('t1', md_tags) self.assertIn('t2', md_tags) @@ -133,5 +131,5 @@ def test_tags(self): self.assertIn('t4', md_tags) # remove all tags - metadef_namespace.remove_all_tags(self.operator_cloud.image) + metadef_namespace.remove_all_tags(self.admin_image_client) self.assertEqual([], metadef_namespace.tags) diff --git a/openstack/tests/functional/image/v2/test_metadef_object.py b/openstack/tests/functional/image/v2/test_metadef_object.py index 1dd14f9c7..ac27c0597 100644 --- a/openstack/tests/functional/image/v2/test_metadef_object.py +++ b/openstack/tests/functional/image/v2/test_metadef_object.py @@ -22,7 +22,7 @@ def setUp(self): # create namespace for object namespace = self.getUniqueString().split('.')[-1] self.metadef_namespace = ( - self.operator_cloud.image.create_metadef_namespace( + self.admin_image_client.create_metadef_namespace( namespace=namespace, ) ) @@ -34,7 +34,7 @@ def setUp(self): # create object object = self.getUniqueString().split('.')[-1] - self.metadef_object = self.operator_cloud.image.create_metadef_object( + self.metadef_object = self.admin_image_client.create_metadef_object( name=object, namespace=self.metadef_namespace, ) @@ -45,22 +45,22 @@ def setUp(self): self.assertEqual(object, self.metadef_object.name) def tearDown(self): - self.operator_cloud.image.delete_metadef_object( + self.admin_image_client.delete_metadef_object( self.metadef_object, self.metadef_object.namespace_name, ) - self.operator_cloud.image.wait_for_delete(self.metadef_object) + self.admin_image_client.wait_for_delete(self.metadef_object) - self.operator_cloud.image.delete_metadef_namespace( + self.admin_image_client.delete_metadef_namespace( self.metadef_namespace ) - self.operator_cloud.image.wait_for_delete(self.metadef_namespace) + self.admin_image_client.wait_for_delete(self.metadef_namespace) super().tearDown() def test_metadef_objects(self): # get - metadef_object = self.operator_cloud.image.get_metadef_object( + metadef_object = self.admin_image_client.get_metadef_object( self.metadef_object.name, self.metadef_namespace, ) @@ -75,7 +75,7 @@ def test_metadef_objects(self): # list metadef_objects = list( - self.operator_cloud.image.metadef_objects( + self.admin_image_client.metadef_objects( self.metadef_object.namespace_name ) ) @@ -89,7 +89,7 @@ def test_metadef_objects(self): # update metadef_object_new_name = 'New object name' metadef_object_new_description = 'New object description' - metadef_object = self.operator_cloud.image.update_metadef_object( + metadef_object = self.admin_image_client.update_metadef_object( self.metadef_object.name, namespace=self.metadef_object.namespace_name, name=metadef_object_new_name, diff --git a/openstack/tests/functional/image/v2/test_metadef_property.py b/openstack/tests/functional/image/v2/test_metadef_property.py index 64cd53d4b..0b24af698 100644 --- a/openstack/tests/functional/image/v2/test_metadef_property.py +++ b/openstack/tests/functional/image/v2/test_metadef_property.py @@ -27,7 +27,7 @@ def setUp(self): random.choice(string.ascii_lowercase) for _ in range(75) ) self.metadef_namespace = ( - self.operator_cloud.image.create_metadef_namespace( + self.admin_image_client.create_metadef_namespace( namespace=namespace, ) ) @@ -49,7 +49,7 @@ def setUp(self): 'enum': ["80", "443"], } self.metadef_property = ( - self.operator_cloud.image.create_metadef_property( + self.admin_image_client.create_metadef_property( self.metadef_namespace.namespace, **self.attrs ) ) @@ -67,19 +67,19 @@ def setUp(self): def tearDown(self): # we do this in tearDown rather than via 'addCleanup' since we want to # wait for the deletion of the resource to ensure it completes - self.operator_cloud.image.delete_metadef_property( + self.admin_image_client.delete_metadef_property( self.metadef_property, self.metadef_namespace ) - self.operator_cloud.image.delete_metadef_namespace( + self.admin_image_client.delete_metadef_namespace( self.metadef_namespace ) - self.operator_cloud.image.wait_for_delete(self.metadef_namespace) + self.admin_image_client.wait_for_delete(self.metadef_namespace) super().tearDown() def test_metadef_property(self): # get metadef property - metadef_property = self.operator_cloud.image.get_metadef_property( + metadef_property = self.admin_image_client.get_metadef_property( self.metadef_property, self.metadef_namespace ) self.assertIsNotNone(metadef_property) @@ -98,9 +98,7 @@ def test_metadef_property(self): # list metadef_properties = list( - self.operator_cloud.image.metadef_properties( - self.metadef_namespace - ) + self.admin_image_client.metadef_properties(self.metadef_namespace) ) self.assertIsNotNone(metadef_properties) self.assertIsInstance( @@ -114,7 +112,7 @@ def test_metadef_property(self): self.attrs['description'] = ''.join( random.choice(string.ascii_lowercase) for _ in range(10) ) - metadef_property = self.operator_cloud.image.update_metadef_property( + metadef_property = self.admin_image_client.update_metadef_property( self.metadef_property, self.metadef_namespace.namespace, **self.attrs, @@ -124,7 +122,7 @@ def test_metadef_property(self): metadef_property, _metadef_property.MetadefProperty, ) - metadef_property = self.operator_cloud.image.get_metadef_property( + metadef_property = self.admin_image_client.get_metadef_property( self.metadef_property.name, self.metadef_namespace ) self.assertEqual( diff --git a/openstack/tests/functional/image/v2/test_metadef_resource_type.py b/openstack/tests/functional/image/v2/test_metadef_resource_type.py index 797ea3176..ada01115b 100644 --- a/openstack/tests/functional/image/v2/test_metadef_resource_type.py +++ b/openstack/tests/functional/image/v2/test_metadef_resource_type.py @@ -22,7 +22,7 @@ def setUp(self): # there's a limit on namespace length namespace = self.getUniqueString().split('.')[-1] self.metadef_namespace = ( - self.operator_cloud.image.create_metadef_namespace( + self.admin_image_client.create_metadef_namespace( namespace=namespace, ) ) @@ -35,7 +35,7 @@ def setUp(self): resource_type_name = 'test-resource-type' resource_type = {'name': resource_type_name} self.metadef_resource_type = ( - self.operator_cloud.image.create_metadef_resource_type_association( + self.admin_image_client.create_metadef_resource_type_association( metadef_namespace=namespace, **resource_type ) ) @@ -48,17 +48,17 @@ def setUp(self): def tearDown(self): # we do this in tearDown rather than via 'addCleanup' since we want to # wait for the deletion of the resource to ensure it completes - self.operator_cloud.image.delete_metadef_namespace( + self.admin_image_client.delete_metadef_namespace( self.metadef_namespace ) - self.operator_cloud.image.wait_for_delete(self.metadef_namespace) + self.admin_image_client.wait_for_delete(self.metadef_namespace) super().tearDown() def test_metadef_resource_types(self): # list resource type associations associations = list( - self.operator_cloud.image.metadef_resource_type_associations( + self.admin_image_client.metadef_resource_type_associations( metadef_namespace=self.metadef_namespace ) ) @@ -70,16 +70,14 @@ def test_metadef_resource_types(self): # (no find_metadef_resource_type_association method) # list resource types - resource_types = list( - self.operator_cloud.image.metadef_resource_types() - ) + resource_types = list(self.admin_image_client.metadef_resource_types()) self.assertIn( self.metadef_resource_type.name, {t.name for t in resource_types} ) # delete - self.operator_cloud.image.delete_metadef_resource_type_association( + self.admin_image_client.delete_metadef_resource_type_association( self.metadef_resource_type, metadef_namespace=self.metadef_namespace, ) diff --git a/openstack/tests/functional/image/v2/test_metadef_schema.py b/openstack/tests/functional/image/v2/test_metadef_schema.py index d427a8de9..083559f14 100644 --- a/openstack/tests/functional/image/v2/test_metadef_schema.py +++ b/openstack/tests/functional/image/v2/test_metadef_schema.py @@ -16,63 +16,59 @@ class TestMetadefSchema(base.BaseImageTest): def test_get_metadef_namespace_schema(self): - metadef_schema = ( - self.operator_cloud.image.get_metadef_namespace_schema() - ) + metadef_schema = self.admin_image_client.get_metadef_namespace_schema() self.assertIsNotNone(metadef_schema) self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) def test_get_metadef_namespaces_schema(self): metadef_schema = ( - self.operator_cloud.image.get_metadef_namespaces_schema() + self.admin_image_client.get_metadef_namespaces_schema() ) self.assertIsNotNone(metadef_schema) self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) def test_get_metadef_resource_type_schema(self): metadef_schema = ( - self.operator_cloud.image.get_metadef_resource_type_schema() + self.admin_image_client.get_metadef_resource_type_schema() ) self.assertIsNotNone(metadef_schema) self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) def test_get_metadef_resource_types_schema(self): metadef_schema = ( - self.operator_cloud.image.get_metadef_resource_types_schema() + self.admin_image_client.get_metadef_resource_types_schema() ) self.assertIsNotNone(metadef_schema) self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) def test_get_metadef_object_schema(self): - metadef_schema = self.operator_cloud.image.get_metadef_object_schema() + metadef_schema = self.admin_image_client.get_metadef_object_schema() self.assertIsNotNone(metadef_schema) self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) def test_get_metadef_objects_schema(self): - metadef_schema = self.operator_cloud.image.get_metadef_objects_schema() + metadef_schema = self.admin_image_client.get_metadef_objects_schema() self.assertIsNotNone(metadef_schema) self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) def test_get_metadef_property_schema(self): - metadef_schema = ( - self.operator_cloud.image.get_metadef_property_schema() - ) + metadef_schema = self.admin_image_client.get_metadef_property_schema() self.assertIsNotNone(metadef_schema) self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) def test_get_metadef_properties_schema(self): metadef_schema = ( - self.operator_cloud.image.get_metadef_properties_schema() + self.admin_image_client.get_metadef_properties_schema() ) self.assertIsNotNone(metadef_schema) self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) def test_get_metadef_tag_schema(self): - metadef_schema = self.operator_cloud.image.get_metadef_tag_schema() + metadef_schema = self.admin_image_client.get_metadef_tag_schema() self.assertIsNotNone(metadef_schema) self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) def test_get_metadef_tags_schema(self): - metadef_schema = self.operator_cloud.image.get_metadef_tags_schema() + metadef_schema = self.admin_image_client.get_metadef_tags_schema() self.assertIsNotNone(metadef_schema) self.assertIsInstance(metadef_schema, _metadef_schema.MetadefSchema) diff --git a/openstack/tests/functional/image/v2/test_schema.py b/openstack/tests/functional/image/v2/test_schema.py index 29f84a255..7a732585f 100644 --- a/openstack/tests/functional/image/v2/test_schema.py +++ b/openstack/tests/functional/image/v2/test_schema.py @@ -16,21 +16,21 @@ class TestSchema(base.BaseImageTest): def test_get_images_schema(self): - schema = self.operator_cloud.image.get_images_schema() + schema = self.admin_image_client.get_images_schema() self.assertIsNotNone(schema) self.assertIsInstance(schema, _schema.Schema) def test_get_image_schema(self): - schema = self.operator_cloud.image.get_image_schema() + schema = self.admin_image_client.get_image_schema() self.assertIsNotNone(schema) self.assertIsInstance(schema, _schema.Schema) def test_get_members_schema(self): - schema = self.operator_cloud.image.get_members_schema() + schema = self.admin_image_client.get_members_schema() self.assertIsNotNone(schema) self.assertIsInstance(schema, _schema.Schema) def test_get_member_schema(self): - schema = self.operator_cloud.image.get_member_schema() + schema = self.admin_image_client.get_member_schema() self.assertIsNotNone(schema) self.assertIsInstance(schema, _schema.Schema) diff --git a/openstack/tests/functional/image/v2/test_task.py b/openstack/tests/functional/image/v2/test_task.py index d48819a72..31bcf700d 100644 --- a/openstack/tests/functional/image/v2/test_task.py +++ b/openstack/tests/functional/image/v2/test_task.py @@ -15,7 +15,7 @@ class TestTask(base.BaseImageTest): def test_tasks(self): - tasks = list(self.operator_cloud.image.tasks()) + tasks = list(self.admin_image_client.tasks()) # NOTE(stephenfin): Yes, this is a dumb test. Basically all that we're # checking is that the API endpoint is correct. It would be nice to # have a proper check here that includes creation of tasks but we don't diff --git a/openstack/tests/functional/key_manager/v1/test_project_quota.py b/openstack/tests/functional/key_manager/v1/test_project_quota.py index 0d33848b1..9ed974889 100644 --- a/openstack/tests/functional/key_manager/v1/test_project_quota.py +++ b/openstack/tests/functional/key_manager/v1/test_project_quota.py @@ -11,6 +11,7 @@ # under the License. from openstack import exceptions as sdk_exc +from openstack.identity.v3 import _proxy as _identity_v3 from openstack.key_manager.v1 import project_quota as _project_quota from openstack.tests.functional import base @@ -20,31 +21,31 @@ class TestProjectQuota(base.BaseFunctionalTest): + _identity: _identity_v3.Proxy + def setUp(self): super().setUp() self.require_service('key-manager') + identity = self.system_admin_cloud.identity + assert identity.api_version == '3' + self._identity = identity + self.project_name = self.getUniqueString('project') - self.project = self.system_admin_cloud.identity.create_project( + self.project = self._identity.create_project( name=self.project_name, ) - self.addCleanup( - self.system_admin_cloud.identity.delete_project, self.project - ) + self.addCleanup(self._identity.delete_project, self.project) - self.role = self.system_admin_cloud.identity.create_role( - name=ADMIN_ROLE_NAME - ) - self.addCleanup( - self.system_admin_cloud.identity.delete_role, self.role.id - ) + self.role = self._identity.create_role(name=ADMIN_ROLE_NAME) + self.addCleanup(self._identity.delete_role, self.role.id) self.user_id = self.system_admin_cloud.current_user_id - self.system_admin_cloud.identity.assign_project_role_to_user( + self._identity.assign_project_role_to_user( project=self.project, user=self.user_id, role=self.role ) self.addCleanup( - self.system_admin_cloud.identity.unassign_project_role_from_user, + self._identity.unassign_project_role_from_user, project=self.project, user=self.user_id, role=self.role, diff --git a/openstack/tests/functional/network/v2/test_address_group.py b/openstack/tests/functional/network/v2/test_address_group.py index 7938930d2..f507c957b 100644 --- a/openstack/tests/functional/network/v2/test_address_group.py +++ b/openstack/tests/functional/network/v2/test_address_group.py @@ -65,6 +65,7 @@ def test_list(self): self.assertIn(self.ADDRESS_GROUP_NAME, names) def test_update(self): + assert self.ADDRESS_GROUP_ID is not None sot = self.user_cloud.network.update_address_group( self.ADDRESS_GROUP_ID, name=self.ADDRESS_GROUP_NAME_UPDATED, diff --git a/openstack/tests/functional/network/v2/test_port.py b/openstack/tests/functional/network/v2/test_port.py index f62c8e11b..9017c7bc4 100644 --- a/openstack/tests/functional/network/v2/test_port.py +++ b/openstack/tests/functional/network/v2/test_port.py @@ -81,6 +81,7 @@ def test_list(self): self.assertIn(self.PORT_ID, ids) def test_update(self): + assert self.PORT_ID is not None sot = self.user_cloud.network.update_port( self.PORT_ID, name=self.UPDATE_NAME ) diff --git a/openstack/utils.py b/openstack/utils.py index 74e9525b6..9750b3722 100644 --- a/openstack/utils.py +++ b/openstack/utils.py @@ -26,6 +26,16 @@ from openstack import _log from openstack import exceptions +_ProxyT = ty.TypeVar('_ProxyT') + +if ty.TYPE_CHECKING: + from openstack.block_storage.v2 import _proxy as _block_storage_v2 + from openstack.block_storage.v3 import _proxy as _block_storage_v3 + from openstack.identity.v2 import _proxy as _identity_v2 + from openstack.identity.v3 import _proxy as _identity_v3 + from openstack.image.v1 import _proxy as _image_v1 + from openstack.image.v2 import _proxy as _image_v2 + def urljoin(*args: str | None) -> str: """A custom version of urljoin that simply joins strings into a path. @@ -161,6 +171,71 @@ def _supports_version() -> bool: return supported +@ty.overload +def ensure_service_version( + proxy: '_identity_v2.Proxy | _identity_v3.Proxy', + version: ty.Literal['2'], +) -> '_identity_v2.Proxy': ... + + +@ty.overload +def ensure_service_version( + proxy: '_identity_v2.Proxy | _identity_v3.Proxy', + version: ty.Literal['3'], +) -> '_identity_v3.Proxy': ... + + +@ty.overload +def ensure_service_version( + proxy: '_block_storage_v2.Proxy | _block_storage_v3.Proxy', + version: ty.Literal['2'], +) -> '_block_storage_v2.Proxy': ... + + +@ty.overload +def ensure_service_version( + proxy: '_block_storage_v2.Proxy | _block_storage_v3.Proxy', + version: ty.Literal['3'], +) -> '_block_storage_v3.Proxy': ... + + +@ty.overload +def ensure_service_version( + proxy: '_image_v1.Proxy | _image_v2.Proxy', + version: ty.Literal['1'], +) -> '_image_v1.Proxy': ... + + +@ty.overload +def ensure_service_version( + proxy: '_image_v1.Proxy | _image_v2.Proxy', + version: ty.Literal['2'], +) -> '_image_v2.Proxy': ... + + +@ty.overload +def ensure_service_version(proxy: _ProxyT, version: str) -> _ProxyT: ... + + +def ensure_service_version(proxy: ty.Any, version: str) -> ty.Any: + """Ensure the provided proxy is for a given version. + + This is intended for type narrowing. + + :param proxy: A versioned service proxy. + :param version: The required API version string. + :returns: The proxy, typed as the specific version requested. + :raises: :class:`~openstack.exceptions.SDKException` if the proxy is not + the requested version. + """ + if proxy.api_version != version: + raise exceptions.SDKException( + f"Service requires API version {version!r} but the configured " + f"version is {proxy.api_version!r}" + ) + return proxy + + def supports_microversion( adapter: ks_adapter.Adapter, microversion: str | int | float | ty.Iterable[str | int | float], From a17e53ce7af84fcebd02df3d76cbebf21e2d7e61 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 26 Nov 2025 21:50:05 +0000 Subject: [PATCH 3824/3836] typing: Annotate openstack.service_description This is rather complicated. [1] is helpful to understand what we're doing here. [1] https://adamj.eu/tech/2021/10/18/python-type-hints-how-to-type-a-descriptor/ Change-Id: I51475a28c98906d84d07ebead48cefc998276d81 Signed-off-by: Stephen Finucane --- openstack/_services_mixin.py | 92 +++++++++++-------- openstack/accelerator/accelerator_service.py | 8 +- openstack/accelerator/v2/_proxy.py | 2 +- openstack/baremetal/baremetal_service.py | 2 +- openstack/baremetal/v1/_proxy.py | 2 +- .../baremetal_introspection_service.py | 4 +- .../baremetal_introspection/v1/_proxy.py | 2 +- .../block_storage/block_storage_service.py | 4 +- openstack/block_storage/v2/_proxy.py | 2 +- openstack/block_storage/v3/_proxy.py | 2 +- openstack/cloud/openstackcloud.py | 3 +- openstack/clustering/clustering_service.py | 2 +- openstack/clustering/v1/_proxy.py | 2 +- openstack/compute/compute_service.py | 2 +- openstack/compute/v2/_proxy.py | 2 +- openstack/config/cloud_region.py | 23 ++++- openstack/connection.py | 15 +-- ...ainer_infrastructure_management_service.py | 2 +- .../v1/_proxy.py | 2 +- openstack/database/database_service.py | 2 +- openstack/database/v1/_proxy.py | 2 +- openstack/dns/dns_service.py | 2 +- openstack/dns/v2/_proxy.py | 2 +- openstack/identity/identity_service.py | 4 +- openstack/identity/v2/_proxy.py | 2 +- openstack/identity/v3/_proxy.py | 2 +- openstack/image/image_service.py | 4 +- openstack/image/v1/_proxy.py | 2 +- openstack/image/v2/_proxy.py | 2 +- openstack/instance_ha/instance_ha_service.py | 2 +- openstack/instance_ha/v1/_proxy.py | 2 +- openstack/key_manager/key_manager_service.py | 2 +- openstack/key_manager/v1/_proxy.py | 2 +- .../load_balancer/load_balancer_service.py | 4 +- openstack/load_balancer/v2/_proxy.py | 2 +- openstack/message/message_service.py | 2 +- openstack/message/v2/_proxy.py | 2 +- openstack/network/network_service.py | 2 +- openstack/network/v2/_proxy.py | 2 +- .../object_store/object_store_service.py | 2 +- openstack/object_store/v1/_proxy.py | 2 +- .../orchestration/orchestration_service.py | 4 +- openstack/orchestration/v1/_proxy.py | 6 +- openstack/placement/placement_service.py | 2 +- openstack/placement/v1/_proxy.py | 2 +- openstack/proxy.py | 3 + openstack/service_description.py | 69 +++++++++++--- .../shared_file_system_service.py | 4 +- openstack/shared_file_system/v2/_proxy.py | 2 +- .../functional/orchestration/v1/test_stack.py | 21 ++--- openstack/workflow/v2/_proxy.py | 2 +- openstack/workflow/workflow_service.py | 4 +- tools/print-services.py | 3 + 53 files changed, 225 insertions(+), 120 deletions(-) diff --git a/openstack/_services_mixin.py b/openstack/_services_mixin.py index 9e2f4ace6..7df757f7a 100644 --- a/openstack/_services_mixin.py +++ b/openstack/_services_mixin.py @@ -1,4 +1,6 @@ # Generated file, to change, run tools/print-services.py +import typing as ty + from openstack import service_description from openstack.accelerator import accelerator_service from openstack.baremetal import baremetal_service @@ -24,6 +26,12 @@ from openstack.shared_file_system import shared_file_system_service from openstack.workflow import workflow_service +if ty.TYPE_CHECKING: + # the noqa is necessary as 'proxy' is only referenced in string subscripts + # and ruff doesn't scan for name usage since they're not in annotation + # positions + from openstack import proxy # noqa: F401 + class ServicesMixin: identity = identity_service.IdentityService(service_type='identity') @@ -46,7 +54,7 @@ class ServicesMixin: resource_cluster = clustering cluster = clustering - data_processing = service_description.ServiceDescription( + data_processing = service_description.ServiceDescription['proxy.Proxy']( service_type='data-processing' ) @@ -63,17 +71,17 @@ class ServicesMixin: service_type='key-manager' ) - resource_optimization = service_description.ServiceDescription( - service_type='resource-optimization' - ) + resource_optimization = service_description.ServiceDescription[ + 'proxy.Proxy' + ](service_type='resource-optimization') infra_optim = resource_optimization message = message_service.MessageService(service_type='message') messaging = message - application_catalog = service_description.ServiceDescription( - service_type='application-catalog' - ) + application_catalog = service_description.ServiceDescription[ + 'proxy.Proxy' + ](service_type='application-catalog') container_infrastructure_management = container_infrastructure_management_service.ContainerInfrastructureManagementService( service_type='container-infrastructure-management' @@ -81,15 +89,19 @@ class ServicesMixin: container_infra = container_infrastructure_management container_infrastructure = container_infrastructure_management - search = service_description.ServiceDescription(service_type='search') + search = service_description.ServiceDescription['proxy.Proxy']( + service_type='search' + ) dns = dns_service.DnsService(service_type='dns') workflow = workflow_service.WorkflowService(service_type='workflow') - rating = service_description.ServiceDescription(service_type='rating') + rating = service_description.ServiceDescription['proxy.Proxy']( + service_type='rating' + ) - operator_policy = service_description.ServiceDescription( + operator_policy = service_description.ServiceDescription['proxy.Proxy']( service_type='operator-policy' ) policy = operator_policy @@ -99,9 +111,9 @@ class ServicesMixin: ) share = shared_file_system - data_protection_orchestration = service_description.ServiceDescription( - service_type='data-protection-orchestration' - ) + data_protection_orchestration = service_description.ServiceDescription[ + 'proxy.Proxy' + ](service_type='data-protection-orchestration') orchestration = orchestration_service.OrchestrationService( service_type='orchestration' @@ -113,56 +125,64 @@ class ServicesMixin: block_store = block_storage volume = block_storage - alarm = service_description.ServiceDescription(service_type='alarm') + alarm = service_description.ServiceDescription['proxy.Proxy']( + service_type='alarm' + ) alarming = alarm - meter = service_description.ServiceDescription(service_type='meter') + meter = service_description.ServiceDescription['proxy.Proxy']( + service_type='meter' + ) metering = meter telemetry = meter - event = service_description.ServiceDescription(service_type='event') + event = service_description.ServiceDescription['proxy.Proxy']( + service_type='event' + ) events = event - application_deployment = service_description.ServiceDescription( - service_type='application-deployment' - ) + application_deployment = service_description.ServiceDescription[ + 'proxy.Proxy' + ](service_type='application-deployment') application_deployment = application_deployment - multi_region_network_automation = service_description.ServiceDescription( - service_type='multi-region-network-automation' - ) + multi_region_network_automation = service_description.ServiceDescription[ + 'proxy.Proxy' + ](service_type='multi-region-network-automation') tricircle = multi_region_network_automation database = database_service.DatabaseService(service_type='database') - application_container = service_description.ServiceDescription( - service_type='application-container' - ) + application_container = service_description.ServiceDescription[ + 'proxy.Proxy' + ](service_type='application-container') container = application_container - root_cause_analysis = service_description.ServiceDescription( - service_type='root-cause-analysis' - ) + root_cause_analysis = service_description.ServiceDescription[ + 'proxy.Proxy' + ](service_type='root-cause-analysis') rca = root_cause_analysis - nfv_orchestration = service_description.ServiceDescription( + nfv_orchestration = service_description.ServiceDescription['proxy.Proxy']( service_type='nfv-orchestration' ) network = network_service.NetworkService(service_type='network') - backup = service_description.ServiceDescription(service_type='backup') + backup = service_description.ServiceDescription['proxy.Proxy']( + service_type='backup' + ) - monitoring_logging = service_description.ServiceDescription( + monitoring_logging = service_description.ServiceDescription['proxy.Proxy']( service_type='monitoring-logging' ) monitoring_log_api = monitoring_logging - monitoring = service_description.ServiceDescription( + monitoring = service_description.ServiceDescription['proxy.Proxy']( service_type='monitoring' ) - monitoring_events = service_description.ServiceDescription( + monitoring_events = service_description.ServiceDescription['proxy.Proxy']( service_type='monitoring-events' ) @@ -173,11 +193,11 @@ class ServicesMixin: ) ha = instance_ha - reservation = service_description.ServiceDescription( + reservation = service_description.ServiceDescription['proxy.Proxy']( service_type='reservation' ) - function_engine = service_description.ServiceDescription( + function_engine = service_description.ServiceDescription['proxy.Proxy']( service_type='function-engine' ) @@ -185,7 +205,7 @@ class ServicesMixin: service_type='accelerator' ) - admin_logic = service_description.ServiceDescription( + admin_logic = service_description.ServiceDescription['proxy.Proxy']( service_type='admin-logic' ) registration = admin_logic diff --git a/openstack/accelerator/accelerator_service.py b/openstack/accelerator/accelerator_service.py index fc01ea666..00a5b817c 100644 --- a/openstack/accelerator/accelerator_service.py +++ b/openstack/accelerator/accelerator_service.py @@ -10,13 +10,15 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.accelerator.v2 import _proxy as _proxy_v2 +from openstack.accelerator.v2 import _proxy from openstack import service_description -class AcceleratorService(service_description.ServiceDescription): +class AcceleratorService( + service_description.ServiceDescription[_proxy.Proxy], +): """The accelerator service.""" supported_versions = { - '2': _proxy_v2.Proxy, + '2': _proxy.Proxy, } diff --git a/openstack/accelerator/v2/_proxy.py b/openstack/accelerator/v2/_proxy.py index 37b69d338..6d82d0e8b 100644 --- a/openstack/accelerator/v2/_proxy.py +++ b/openstack/accelerator/v2/_proxy.py @@ -22,7 +22,7 @@ class Proxy(proxy.Proxy): - api_version = '2' + api_version: ty.ClassVar[ty.Literal['2']] = '2' # ========== Deployables ========== diff --git a/openstack/baremetal/baremetal_service.py b/openstack/baremetal/baremetal_service.py index 5bbecf6bd..78398a3a5 100644 --- a/openstack/baremetal/baremetal_service.py +++ b/openstack/baremetal/baremetal_service.py @@ -14,7 +14,7 @@ from openstack import service_description -class BaremetalService(service_description.ServiceDescription): +class BaremetalService(service_description.ServiceDescription[_proxy.Proxy]): """The bare metal service.""" supported_versions = { diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index be3bfe29b..ed023e6bc 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -34,7 +34,7 @@ class Proxy(proxy.Proxy): - api_version = '1' + api_version: ty.ClassVar[ty.Literal['1']] = '1' retriable_status_codes = _common.RETRIABLE_STATUS_CODES diff --git a/openstack/baremetal_introspection/baremetal_introspection_service.py b/openstack/baremetal_introspection/baremetal_introspection_service.py index acbdaa894..6ca452e87 100644 --- a/openstack/baremetal_introspection/baremetal_introspection_service.py +++ b/openstack/baremetal_introspection/baremetal_introspection_service.py @@ -14,7 +14,9 @@ from openstack import service_description -class BaremetalIntrospectionService(service_description.ServiceDescription): +class BaremetalIntrospectionService( + service_description.ServiceDescription[_proxy.Proxy] +): """The bare metal introspection service.""" supported_versions = { diff --git a/openstack/baremetal_introspection/v1/_proxy.py b/openstack/baremetal_introspection/v1/_proxy.py index 0991f4dea..b55963ea0 100644 --- a/openstack/baremetal_introspection/v1/_proxy.py +++ b/openstack/baremetal_introspection/v1/_proxy.py @@ -27,7 +27,7 @@ class Proxy(proxy.Proxy): - api_version = '1' + api_version: ty.ClassVar[ty.Literal['1']] = '1' _resource_registry = { "introspection": _introspect.Introspection, diff --git a/openstack/block_storage/block_storage_service.py b/openstack/block_storage/block_storage_service.py index 5ab0ca409..e3d35e40f 100644 --- a/openstack/block_storage/block_storage_service.py +++ b/openstack/block_storage/block_storage_service.py @@ -15,7 +15,9 @@ from openstack import service_description -class BlockStorageService(service_description.ServiceDescription): +class BlockStorageService( + service_description.ServiceDescription[_v2_proxy.Proxy | _v3_proxy.Proxy] +): """The block storage service.""" supported_versions = { diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index febb9c35a..c7c036d82 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -34,7 +34,7 @@ class Proxy(proxy.Proxy): - api_version = '2' + api_version: ty.ClassVar[ty.Literal['2']] = '2' # ========== Extensions ========== diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index e3b2d0983..f97e76fc3 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -42,7 +42,7 @@ class Proxy(proxy.Proxy): - api_version = '3' + api_version: ty.ClassVar[ty.Literal['3']] = '3' _resource_registry = { "availability_zone": availability_zone.AvailabilityZone, diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index f1ebcb04d..286f10285 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -670,11 +670,12 @@ def search_resources( # User used string notation. Try to find proper # resource - (service_name, resource_name) = resource_type.split('.') + service_name, resource_name = resource_type.split('.') if not hasattr(self, service_name): raise exceptions.SDKException( f"service {service_name} is not existing/enabled" ) + service_proxy = getattr(self, service_name) try: resource_type = service_proxy._resource_registry[resource_name] diff --git a/openstack/clustering/clustering_service.py b/openstack/clustering/clustering_service.py index 5920fe50a..7abd5b156 100644 --- a/openstack/clustering/clustering_service.py +++ b/openstack/clustering/clustering_service.py @@ -14,7 +14,7 @@ from openstack import service_description -class ClusteringService(service_description.ServiceDescription): +class ClusteringService(service_description.ServiceDescription[_proxy.Proxy]): """The clustering service.""" supported_versions = { diff --git a/openstack/clustering/v1/_proxy.py b/openstack/clustering/v1/_proxy.py index c63bbb39f..8c70c1afa 100644 --- a/openstack/clustering/v1/_proxy.py +++ b/openstack/clustering/v1/_proxy.py @@ -30,7 +30,7 @@ class Proxy(proxy.Proxy): - api_version = '1' + api_version: ty.ClassVar[ty.Literal['1']] = '1' _resource_registry = { "action": _action.Action, diff --git a/openstack/compute/compute_service.py b/openstack/compute/compute_service.py index 3b4a7c00c..eb7d4cc14 100644 --- a/openstack/compute/compute_service.py +++ b/openstack/compute/compute_service.py @@ -14,7 +14,7 @@ from openstack import service_description -class ComputeService(service_description.ServiceDescription): +class ComputeService(service_description.ServiceDescription[_proxy.Proxy]): """The compute service.""" supported_versions = { diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 9b79eef24..a50f0c420 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -49,7 +49,7 @@ class Proxy(proxy.Proxy): - api_version = '2' + api_version: ty.ClassVar[ty.Literal['2']] = '2' _resource_registry = { "aggregate": _aggregate.Aggregate, diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 759b2f4b8..c842b5c4b 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -1010,11 +1010,29 @@ def _get_hardcoded_endpoint( endpoint = parse.urljoin(endpoint, 'v2.0') return endpoint + @ty.overload + def get_session_client( + self, + service_type: str, + version: str | None = None, + constructor: None = None, + **kwargs: ty.Any, + ) -> proxy.Proxy: ... + + @ty.overload def get_session_client( self, service_type: str, version: str | None = None, - constructor: type[proxy.Proxy] = proxy.Proxy, + constructor: type[proxy.ProxyT] = ..., + **kwargs: ty.Any, + ) -> proxy.ProxyT: ... + + def get_session_client( + self, + service_type: str, + version: str | None = None, + constructor: type[proxy.Proxy] | None = None, **kwargs: ty.Any, ) -> proxy.Proxy: """Return a prepped keystoneauth Adapter for a given service. @@ -1030,6 +1048,9 @@ def get_session_client( and it will work like you think. """ + if constructor is None: + constructor = proxy.Proxy + version_request = self._get_version_request(service_type, version) kwargs.setdefault('region_name', self.get_region_name(service_type)) diff --git a/openstack/connection.py b/openstack/connection.py index 66cffe01e..5f197fa76 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -380,8 +380,9 @@ def __init__( session: ks_session.Session | None = None, app_name: str | None = None, app_version: str | None = None, - extra_services: list[service_description.ServiceDescription] - | None = None, + extra_services: ( + list[service_description.ServiceDescription[ty.Any]] | None + ) = None, strict: bool = False, use_direct_get: bool | None = None, task_manager: ty.Any = None, @@ -529,7 +530,7 @@ def __init__( ) def add_service( - self, service: service_description.ServiceDescription + self, service: service_description.ServiceDescription['proxy.Proxy'] ) -> None: """Add a service to the Connection. @@ -550,13 +551,13 @@ class contained in # If we don't have a proxy, just instantiate Proxy so that # we get an adapter. if isinstance(service, str): - service = service_description.ServiceDescription(service) + service = service_description.ServiceDescription['proxy.Proxy']( + service + ) # Directly invoke descriptor of the ServiceDescription def getter(self: 'Connection') -> 'proxy.Proxy': - # TODO(stephenfin): Remove ignore once we have typed - # ServiceDescription - return service.__get__(self, service) # type: ignore + return service.__get__(self, type(self)) # Register the ServiceDescription class (as property) # with every known alias for a "runtime descriptor" diff --git a/openstack/container_infrastructure_management/container_infrastructure_management_service.py b/openstack/container_infrastructure_management/container_infrastructure_management_service.py index e71676d08..df9a0d57b 100644 --- a/openstack/container_infrastructure_management/container_infrastructure_management_service.py +++ b/openstack/container_infrastructure_management/container_infrastructure_management_service.py @@ -15,7 +15,7 @@ class ContainerInfrastructureManagementService( - service_description.ServiceDescription, + service_description.ServiceDescription[_proxy.Proxy], ): """The container infrastructure management service.""" diff --git a/openstack/container_infrastructure_management/v1/_proxy.py b/openstack/container_infrastructure_management/v1/_proxy.py index 6aeab8969..eb24995b5 100644 --- a/openstack/container_infrastructure_management/v1/_proxy.py +++ b/openstack/container_infrastructure_management/v1/_proxy.py @@ -29,7 +29,7 @@ class Proxy(proxy.Proxy): - api_version = '1' + api_version: ty.ClassVar[ty.Literal['1']] = '1' _resource_registry = { "cluster": _cluster.Cluster, diff --git a/openstack/database/database_service.py b/openstack/database/database_service.py index e37f45715..bc595facf 100644 --- a/openstack/database/database_service.py +++ b/openstack/database/database_service.py @@ -14,7 +14,7 @@ from openstack import service_description -class DatabaseService(service_description.ServiceDescription): +class DatabaseService(service_description.ServiceDescription[_proxy.Proxy]): """The database service.""" supported_versions = { diff --git a/openstack/database/v1/_proxy.py b/openstack/database/v1/_proxy.py index bf5ba9e34..be5a7c67f 100644 --- a/openstack/database/v1/_proxy.py +++ b/openstack/database/v1/_proxy.py @@ -21,7 +21,7 @@ class Proxy(proxy.Proxy): - api_version = '1' + api_version: ty.ClassVar[ty.Literal['1']] = '1' _resource_registry = { "database": _database.Database, diff --git a/openstack/dns/dns_service.py b/openstack/dns/dns_service.py index 6fa162b57..5eeaaeed2 100644 --- a/openstack/dns/dns_service.py +++ b/openstack/dns/dns_service.py @@ -14,7 +14,7 @@ from openstack import service_description -class DnsService(service_description.ServiceDescription): +class DnsService(service_description.ServiceDescription[_proxy.Proxy]): """The DNS service.""" supported_versions = { diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py index db5cff873..6b5175652 100644 --- a/openstack/dns/v2/_proxy.py +++ b/openstack/dns/v2/_proxy.py @@ -31,7 +31,7 @@ class Proxy(proxy.Proxy): - api_version = '2' + api_version: ty.ClassVar[ty.Literal['2']] = '2' _resource_registry = { "blacklist": _blacklist.Blacklist, diff --git a/openstack/identity/identity_service.py b/openstack/identity/identity_service.py index df8051170..e50d535cb 100644 --- a/openstack/identity/identity_service.py +++ b/openstack/identity/identity_service.py @@ -15,7 +15,9 @@ from openstack import service_description -class IdentityService(service_description.ServiceDescription): +class IdentityService( + service_description.ServiceDescription[_proxy_v2.Proxy | _proxy_v3.Proxy] +): """The identity service.""" supported_versions = { diff --git a/openstack/identity/v2/_proxy.py b/openstack/identity/v2/_proxy.py index 778ad9002..41a7a1618 100644 --- a/openstack/identity/v2/_proxy.py +++ b/openstack/identity/v2/_proxy.py @@ -21,7 +21,7 @@ class Proxy(proxy.Proxy): - api_version = '2' + api_version: ty.ClassVar[ty.Literal['2']] = '2' def extensions(self): """Retrieve a generator of extensions diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index fbcb0ff27..b38eb837f 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -64,7 +64,7 @@ class Proxy(proxy.Proxy): - api_version = '3' + api_version: ty.ClassVar[ty.Literal['3']] = '3' _resource_registry = { "application_credential": _application_credential.ApplicationCredential, # noqa: E501 diff --git a/openstack/image/image_service.py b/openstack/image/image_service.py index b091a0100..7c18b4372 100644 --- a/openstack/image/image_service.py +++ b/openstack/image/image_service.py @@ -15,7 +15,9 @@ from openstack import service_description -class ImageService(service_description.ServiceDescription): +class ImageService( + service_description.ServiceDescription[_proxy_v1.Proxy | _proxy_v2.Proxy] +): """The image service.""" supported_versions = { diff --git a/openstack/image/v1/_proxy.py b/openstack/image/v1/_proxy.py index 5bdb90d85..1f8930c4b 100644 --- a/openstack/image/v1/_proxy.py +++ b/openstack/image/v1/_proxy.py @@ -37,7 +37,7 @@ def _get_name_and_filename(name, image_format): class Proxy(proxy.Proxy): - api_version = '1' + api_version: ty.ClassVar[ty.Literal['1']] = '1' retriable_status_codes = [503] diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index b2082084e..c629616e1 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -54,7 +54,7 @@ def _get_name_and_filename(name, image_format): class Proxy(proxy.Proxy): - api_version = '2' + api_version: ty.ClassVar[ty.Literal['2']] = '2' _resource_registry = { "cache": _cache.Cache, diff --git a/openstack/instance_ha/instance_ha_service.py b/openstack/instance_ha/instance_ha_service.py index 89a1d9a22..0d311fb18 100644 --- a/openstack/instance_ha/instance_ha_service.py +++ b/openstack/instance_ha/instance_ha_service.py @@ -16,7 +16,7 @@ from openstack import service_description -class InstanceHaService(service_description.ServiceDescription): +class InstanceHaService(service_description.ServiceDescription[_proxy.Proxy]): """The HA service.""" supported_versions = { diff --git a/openstack/instance_ha/v1/_proxy.py b/openstack/instance_ha/v1/_proxy.py index 88c6f760d..fd715262b 100644 --- a/openstack/instance_ha/v1/_proxy.py +++ b/openstack/instance_ha/v1/_proxy.py @@ -24,7 +24,7 @@ class Proxy(proxy.Proxy): - api_version = '1' + api_version: ty.ClassVar[ty.Literal['1']] = '1' _resource_registry = { "host": _host.Host, diff --git a/openstack/key_manager/key_manager_service.py b/openstack/key_manager/key_manager_service.py index 64d76d210..d80951a03 100644 --- a/openstack/key_manager/key_manager_service.py +++ b/openstack/key_manager/key_manager_service.py @@ -14,7 +14,7 @@ from openstack import service_description -class KeyManagerService(service_description.ServiceDescription): +class KeyManagerService(service_description.ServiceDescription[_proxy.Proxy]): """The key manager service.""" supported_versions = { diff --git a/openstack/key_manager/v1/_proxy.py b/openstack/key_manager/v1/_proxy.py index 0c5924712..e5d161ffb 100644 --- a/openstack/key_manager/v1/_proxy.py +++ b/openstack/key_manager/v1/_proxy.py @@ -22,7 +22,7 @@ class Proxy(proxy.Proxy): - api_version = '1' + api_version: ty.ClassVar[ty.Literal['1']] = '1' _resource_registry = { "container": _container.Container, diff --git a/openstack/load_balancer/load_balancer_service.py b/openstack/load_balancer/load_balancer_service.py index 31542a317..b1b6c2dc9 100644 --- a/openstack/load_balancer/load_balancer_service.py +++ b/openstack/load_balancer/load_balancer_service.py @@ -14,7 +14,9 @@ from openstack import service_description -class LoadBalancerService(service_description.ServiceDescription): +class LoadBalancerService( + service_description.ServiceDescription[_proxy.Proxy] +): """The load balancer service.""" supported_versions = { diff --git a/openstack/load_balancer/v2/_proxy.py b/openstack/load_balancer/v2/_proxy.py index 69769e111..716971767 100644 --- a/openstack/load_balancer/v2/_proxy.py +++ b/openstack/load_balancer/v2/_proxy.py @@ -33,7 +33,7 @@ class Proxy(proxy.Proxy): - api_version = '2' + api_version: ty.ClassVar[ty.Literal['2']] = '2' _resource_registry = { "amphora": _amphora.Amphora, diff --git a/openstack/message/message_service.py b/openstack/message/message_service.py index 204d391c1..0cf7d19eb 100644 --- a/openstack/message/message_service.py +++ b/openstack/message/message_service.py @@ -14,7 +14,7 @@ from openstack import service_description -class MessageService(service_description.ServiceDescription): +class MessageService(service_description.ServiceDescription[_proxy.Proxy]): """The message service.""" supported_versions = { diff --git a/openstack/message/v2/_proxy.py b/openstack/message/v2/_proxy.py index 0c3a32d55..b089112e6 100644 --- a/openstack/message/v2/_proxy.py +++ b/openstack/message/v2/_proxy.py @@ -21,7 +21,7 @@ class Proxy(proxy.Proxy): - api_version = '2' + api_version: ty.ClassVar[ty.Literal['2']] = '2' _resource_registry = { "claim": _claim.Claim, diff --git a/openstack/network/network_service.py b/openstack/network/network_service.py index 17070efed..b6f10f075 100644 --- a/openstack/network/network_service.py +++ b/openstack/network/network_service.py @@ -14,7 +14,7 @@ from openstack import service_description -class NetworkService(service_description.ServiceDescription): +class NetworkService(service_description.ServiceDescription[_proxy.Proxy]): """The network service.""" supported_versions = { diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index bbd5ab408..d4ed2c08e 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -109,7 +109,7 @@ class Proxy(proxy.Proxy): - api_version = '2' + api_version: ty.ClassVar[ty.Literal['2']] = '2' _resource_registry = { "address_group": _address_group.AddressGroup, diff --git a/openstack/object_store/object_store_service.py b/openstack/object_store/object_store_service.py index 2a0aa3d1a..1a66d70ac 100644 --- a/openstack/object_store/object_store_service.py +++ b/openstack/object_store/object_store_service.py @@ -14,7 +14,7 @@ from openstack import service_description -class ObjectStoreService(service_description.ServiceDescription): +class ObjectStoreService(service_description.ServiceDescription[_proxy.Proxy]): """The object store service.""" supported_versions = { diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index 1995d085a..0824e5d24 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -43,7 +43,7 @@ def _get_expiration(expiration): class Proxy(proxy.Proxy): - api_version = '1' + api_version: ty.ClassVar[ty.Literal['1']] = '1' _resource_registry = { "account": _account.Account, diff --git a/openstack/orchestration/orchestration_service.py b/openstack/orchestration/orchestration_service.py index 827f9831c..4ef5cc002 100644 --- a/openstack/orchestration/orchestration_service.py +++ b/openstack/orchestration/orchestration_service.py @@ -14,7 +14,9 @@ from openstack import service_description -class OrchestrationService(service_description.ServiceDescription): +class OrchestrationService( + service_description.ServiceDescription[_proxy.Proxy] +): """The orchestration service.""" supported_versions = { diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index 541678398..80fab8157 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -30,7 +30,7 @@ # TODO(rladntjr4): Some of these methods support lookup by ID, while others # support lookup by ID or name. We should choose one and use it consistently. class Proxy(proxy.Proxy): - api_version = '1' + api_version: ty.ClassVar[ty.Literal['1']] = '1' _resource_registry = { "resource": _resource.Resource, @@ -109,7 +109,9 @@ def read_env_and_templates( ) return stack_attrs - def create_stack(self, preview=False, **attrs): + def create_stack( + self, preview: bool = False, **attrs: ty.Any + ) -> _stack.Stack: """Create a new stack from attributes :param bool preview: When ``True``, a preview endpoint will be used to diff --git a/openstack/placement/placement_service.py b/openstack/placement/placement_service.py index 2fab7a035..24913f523 100644 --- a/openstack/placement/placement_service.py +++ b/openstack/placement/placement_service.py @@ -14,7 +14,7 @@ from openstack import service_description -class PlacementService(service_description.ServiceDescription): +class PlacementService(service_description.ServiceDescription[_proxy.Proxy]): """The placement service.""" supported_versions = { diff --git a/openstack/placement/v1/_proxy.py b/openstack/placement/v1/_proxy.py index 56cc733c4..f85270f76 100644 --- a/openstack/placement/v1/_proxy.py +++ b/openstack/placement/v1/_proxy.py @@ -23,7 +23,7 @@ class Proxy(proxy.Proxy): - api_version = '1' + api_version: ty.ClassVar[ty.Literal['1']] = '1' _resource_registry = { "resource_class": _resource_class.ResourceClass, diff --git a/openstack/proxy.py b/openstack/proxy.py index ca04e2ed6..8f26e343b 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -50,6 +50,9 @@ from openstack import connection +ProxyT = ty.TypeVar('ProxyT', bound='Proxy') + + def normalize_metric_name(name: str) -> str: name = name.replace('.', '_') name = name.replace(':', '_') diff --git a/openstack/service_description.py b/openstack/service_description.py index c85d40823..51a2402f9 100644 --- a/openstack/service_description.py +++ b/openstack/service_description.py @@ -11,6 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty import warnings import os_service_types @@ -24,16 +25,19 @@ 'ServiceDescription', ] +if ty.TYPE_CHECKING: + from openstack import connection + _logger = _log.setup_logging('openstack') _service_type_manager = os_service_types.ServiceTypes() class _ServiceDisabledProxyShim: - def __init__(self, service_type, reason): + def __init__(self, service_type: str, reason: str | None) -> None: self.service_type = service_type self.reason = reason - def __getattr__(self, item): + def __getattr__(self, item: ty.Any) -> ty.Any: raise exceptions.ServiceDisabledException( "Service '{service_type}' is disabled because its configuration " "could not be loaded. {reason}".format( @@ -42,7 +46,7 @@ def __getattr__(self, item): ) -class ServiceDescription: +class ServiceDescription(ty.Generic[proxy_mod.ProxyT]): #: Dictionary of supported versions and proxy classes for that version supported_versions: dict[str, type[proxy_mod.Proxy]] = {} #: main service_type to use to find this service in the catalog @@ -84,17 +88,40 @@ def __init__( self.aliases = aliases or self.aliases self.all_types = [service_type, *self.aliases] - def __get__(self, instance, owner): + @ty.overload + def __get__(self, instance: None, owner: None) -> 'ServiceDescription': ... + + # NOTE(stephenfin): We would like to type instance as + # connection.Connection, but due to how we construct that object, we can't + # do so yet. + @ty.overload + def __get__( + self, + instance: ty.Any, + owner: type[object], + ) -> proxy_mod.ProxyT: ... + + def __get__( + self, + instance: ty.Any, + owner: type[object] | None, + ) -> 'ServiceDescription | proxy_mod.ProxyT': if instance is None: return self if self.service_type in instance._proxies: - return instance._proxies[self.service_type] + return ty.cast( + proxy_mod.ProxyT, instance._proxies[self.service_type] + ) proxy = self._make_proxy(instance) + if isinstance(proxy, _ServiceDisabledProxyShim): instance._proxies[self.service_type] = proxy - return instance._proxies[self.service_type] + return ty.cast( + proxy_mod.ProxyT, + instance._proxies[self.service_type], + ) # The keystone proxy has a method called get_endpoint # that is about managing keystone endpoints. This is @@ -152,7 +179,10 @@ def _validate_proxy(self, proxy, endpoint): ) ) - def _make_proxy(self, instance): + def _make_proxy( + self, + instance: 'connection.Connection', + ) -> proxy_mod.ProxyT | proxy_mod.Proxy: """Create a Proxy for the service in question. :param instance: The `openstack.connection.Connection` we're working @@ -162,9 +192,14 @@ def _make_proxy(self, instance): # This is not a valid service. if not config.has_service(self.service_type): - return _ServiceDisabledProxyShim( - self.service_type, - config.get_disabled_reason(self.service_type), + # NOTE(stephenfin): Yes, we are lying here. But that's okay: they + # should behave identically in a typing context + return ty.cast( + proxy_mod.ProxyT, + _ServiceDisabledProxyShim( + self.service_type, + config.get_disabled_reason(self.service_type), + ), ) # This is a valid service type, but we don't know anything about it so @@ -246,11 +281,14 @@ def _make_proxy(self, instance): return proxy_obj data = proxy_obj.get_endpoint_data() - if not data and instance._strict_proxies: - raise exceptions.ServiceDiscoveryException( - f"Failed to create a working proxy for service " - f"{self.service_type}: No endpoint data found." - ) + if not data: + if instance._strict_proxies: + raise exceptions.ServiceDiscoveryException( + f"Failed to create a working proxy for service " + f"{self.service_type}: No endpoint data found." + ) + else: + return proxy_obj # If we've gotten here with a proxy object it means we have # an endpoint_override in place. If the catalog_url and @@ -334,6 +372,7 @@ def _make_proxy(self, instance): return config.get_session_client( self.service_type, + version=version_string, constructor=proxy_class, allow_version_hack=True, max_version=f'{supported_versions[-1]!s}.latest', diff --git a/openstack/shared_file_system/shared_file_system_service.py b/openstack/shared_file_system/shared_file_system_service.py index 8ac842fcb..c5479fc64 100644 --- a/openstack/shared_file_system/shared_file_system_service.py +++ b/openstack/shared_file_system/shared_file_system_service.py @@ -14,7 +14,9 @@ from openstack.shared_file_system.v2 import _proxy -class SharedFilesystemService(service_description.ServiceDescription): +class SharedFilesystemService( + service_description.ServiceDescription[_proxy.Proxy] +): """The shared file systems service.""" supported_versions = { diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index c38cd41d7..7d859ccde 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -46,7 +46,7 @@ class Proxy(proxy.Proxy): - api_version = '2' + api_version: ty.ClassVar[ty.Literal['2']] = '2' _resource_registry = { "availability_zone": _availability_zone.AvailabilityZone, diff --git a/openstack/tests/functional/orchestration/v1/test_stack.py b/openstack/tests/functional/orchestration/v1/test_stack.py index 285ded783..1ed1412dd 100644 --- a/openstack/tests/functional/orchestration/v1/test_stack.py +++ b/openstack/tests/functional/orchestration/v1/test_stack.py @@ -20,10 +20,7 @@ class TestStack(base.BaseFunctionalTest): NAME = 'test_stack' - stack = None - network = None - subnet = None - cidr = '10.99.99.0/16' + CIDR = '10.99.99.0/16' _wait_for_timeout_key = 'OPENSTACKSDK_FUNC_TEST_TIMEOUT_ORCHESTRATION' @@ -41,7 +38,7 @@ def setUp(self): # the shade layer. template['heat_template_version'] = '2013-05-23' self.network, self.subnet = test_network.create_network( - self.operator_cloud, self.NAME, self.cidr + self.operator_cloud, self.NAME, self.CIDR ) parameters = { 'image': image.id, @@ -53,10 +50,10 @@ def setUp(self): parameters=parameters, template=template, ) - assert isinstance(sot, stack.Stack) - self.assertEqual(True, (sot.id is not None)) - self.stack = sot + self.assertIsInstance(sot, stack.Stack) + self.assertIsNotNone(sot.id) self.assertEqual(self.NAME, sot.name) + self.stack = sot self.operator_cloud.orchestration.wait_for_status( sot, status='CREATE_COMPLETE', @@ -92,18 +89,18 @@ def test_suspend_resume(self): # when self.operator_cloud.orchestration.suspend_stack(self.stack) - sot = self.operator_cloud.orchestration.wait_for_status( + self.stack = self.operator_cloud.orchestration.wait_for_status( self.stack, suspend_status, wait=self._wait_for_timeout ) # then - self.assertEqual(suspend_status, sot.status) + self.assertEqual(suspend_status, self.stack.status) # when self.operator_cloud.orchestration.resume_stack(self.stack) - sot = self.operator_cloud.orchestration.wait_for_status( + self.stack = self.operator_cloud.orchestration.wait_for_status( self.stack, resume_status, wait=self._wait_for_timeout ) # then - self.assertEqual(resume_status, sot.status) + self.assertEqual(resume_status, self.stack.status) diff --git a/openstack/workflow/v2/_proxy.py b/openstack/workflow/v2/_proxy.py index bae87831d..43670c8e9 100644 --- a/openstack/workflow/v2/_proxy.py +++ b/openstack/workflow/v2/_proxy.py @@ -20,7 +20,7 @@ class Proxy(proxy.Proxy): - api_version = '2' + api_version: ty.ClassVar[ty.Literal['2']] = '2' _resource_registry = { "execution": _execution.Execution, diff --git a/openstack/workflow/workflow_service.py b/openstack/workflow/workflow_service.py index 4c2012897..98ece4402 100644 --- a/openstack/workflow/workflow_service.py +++ b/openstack/workflow/workflow_service.py @@ -14,7 +14,9 @@ from openstack.workflow.v2 import _proxy -class WorkflowService(service_description.ServiceDescription): +class WorkflowService( + service_description.ServiceDescription[_proxy.Proxy], +): """The workflow service.""" supported_versions = { diff --git a/tools/print-services.py b/tools/print-services.py index 0d3eb171b..9cbcc5bc2 100644 --- a/tools/print-services.py +++ b/tools/print-services.py @@ -46,6 +46,9 @@ def make_names(): dm = 'service_description' dc = desc_class.__name__ + if dc == 'ServiceDescription': + dc = '{dc}[proxy.Proxy]' + services.append( f"{st} = {dm}.{dc}(service_type='{service_type}')", ) From cbda4981055d681d2d31c673ed8176209db0c267 Mon Sep 17 00:00:00 2001 From: JiAnJeong Date: Sun, 21 Sep 2025 19:59:40 +0900 Subject: [PATCH 3825/3836] image: Fix docstring typo Change-Id: Ib52617999ef260ef52373ca38b89e432810f106a Signed-off-by: jja6312 --- openstack/image/v2/_proxy.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 1b020c17a..986be0886 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -87,12 +87,11 @@ def get_image_cache(self): def cache_delete_image(self, image, ignore_missing=True): """Delete an image from cache. - :param image: The value can be either the name of an image or a - :class:`~openstack.image.v2.image.Image` - instance. + :param image: The value can be either the ID of an image or a + :class:`~openstack.image.v2.image.Image` instance. :param bool ignore_missing: When set to ``False``, :class:`~openstack.exceptions.NotFoundException` will be raised - when the metadef namespace does not exist. + when the image or cache entry does not exist. :returns: ``None`` """ return self._delete(_cache.Cache, image, ignore_missing=ignore_missing) From 3f9c048bb492302b376eb7c2f837d749b9cca485 Mon Sep 17 00:00:00 2001 From: Pavlo Shchelokovskyy Date: Wed, 10 Dec 2025 17:46:06 +0200 Subject: [PATCH 3826/3836] Fix help for cloud.port_create in fact this method does not accept `ip_address` and `subnet_id` arguments, they are meant to be used only as keys inside elements of fixed_ips list argument. Change-Id: I16b55eb2d489e0b275f76c929910650f8d261f54 Signed-off-by: Pavlo Shchelokovskyy --- openstack/cloud/_network.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/openstack/cloud/_network.py b/openstack/cloud/_network.py index 42ca1ffd9..ac4c92034 100644 --- a/openstack/cloud/_network.py +++ b/openstack/cloud/_network.py @@ -2484,8 +2484,6 @@ def update_subnet( 'admin_state_up', 'mac_address', 'fixed_ips', - 'subnet_id', - 'ip_address', 'security_groups', 'allowed_address_pairs', 'extra_dhcp_opts', @@ -2523,13 +2521,13 @@ def create_port(self, network_id, **kwargs): ..., ] - :param subnet_id: If you specify only a subnet ID, OpenStack Networking - allocates an available IP from that subnet to the port. (Optional) - If you specify both a subnet ID and an IP address, OpenStack - Networking tries to allocate the specified address to the port. - :param ip_address: If you specify both a subnet ID and an IP address, - OpenStack Networking tries to allocate the specified address to - the port. + where + subnet_id: If you specify only a subnet ID, + OpenStack Networking allocates an available IP + from that subnet to the port. + ip_address: (Optional) If you specify both a subnet ID and + an IP address, OpenStack Networking tries to allocate + the specified address to the port. :param security_groups: List of security group UUIDs. (Optional) :param allowed_address_pairs: Allowed address pairs list (Optional) For example:: From cf6f9a00af3bd84de1e77b07a367511df1f2001d Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Tue, 3 Mar 2026 09:03:33 +0000 Subject: [PATCH 3827/3836] Update master for stable/2026.1 Add file to the reno documentation build to show release notes for stable/2026.1. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/2026.1. Sem-Ver: feature Change-Id: Id346e308c69a0d8eee4e6a29d32d0ffeafad4ff9 Signed-off-by: OpenStack Release Bot Generated-By: openstack/project-config:roles/copy-release-tools-scripts/files/release-tools/add_release_note_page.sh --- releasenotes/source/2026.1.rst | 6 ++++++ releasenotes/source/index.rst | 1 + 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/2026.1.rst diff --git a/releasenotes/source/2026.1.rst b/releasenotes/source/2026.1.rst new file mode 100644 index 000000000..3d2861580 --- /dev/null +++ b/releasenotes/source/2026.1.rst @@ -0,0 +1,6 @@ +=========================== +2026.1 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2026.1 diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index f2270bc5a..31ac66e99 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ :maxdepth: 1 unreleased + 2026.1 2025.2 2025.1 2024.2 From 65dcdb2fac7b21b1e9e5f6432456e7bc3380d240 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Sat, 7 Mar 2026 16:28:53 +0000 Subject: [PATCH 3828/3836] ruff: Configure hacking as external linter Change-Id: Id3718d1f3d2cf5aa6f28efacc920f1797aaf9ec3 Signed-off-by: Stephen Finucane --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 576dfe120..fbc0fe52f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ disallow_untyped_defs = false no_implicit_reexport = true extra_checks = true disable_error_code = ["import-untyped"] -exclude = '(?x)(doc | examples | releasenotes)' +exclude = "(?x)(doc | examples | releasenotes)" [[tool.mypy.overrides]] module = [ @@ -105,6 +105,7 @@ ignore = [ # we only use asserts for type narrowing "S101", ] +external = ["H"] [tool.ruff.lint.per-file-ignores] "openstack/tests/*" = ["S"] From b505be013b0977ba73d03596d9817cb8d5dcc2d4 Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Tue, 10 Mar 2026 14:56:47 +0900 Subject: [PATCH 3829/3836] Adapt to new testtools testtools 2.8.0 removed TestCase.skip in favor of TestCase.skipTest[1]. [1] https://github.com/testing-cabal/testtools/blob/2.8.0/NEWS#L33 Change-Id: Ia52b3f5816f65f100a4989cc029f1ad26f60a735 Signed-off-by: Takashi Kajinami --- .../tests/functional/block_storage/v3/test_default_type.py | 2 +- openstack/tests/functional/block_storage/v3/test_quota_set.py | 2 +- openstack/tests/functional/block_storage/v3/test_type.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openstack/tests/functional/block_storage/v3/test_default_type.py b/openstack/tests/functional/block_storage/v3/test_default_type.py index 7520ec632..419131953 100644 --- a/openstack/tests/functional/block_storage/v3/test_default_type.py +++ b/openstack/tests/functional/block_storage/v3/test_default_type.py @@ -18,7 +18,7 @@ class TestDefaultType(base.BaseBlockStorageTest): def setUp(self): super().setUp() if not self._operator_cloud_name: - self.skip("Operator cloud must be set for this test") + self.skipTest("Operator cloud must be set for this test") self._set_operator_cloud(block_storage_api_version='3.67') self.PROJECT_ID = self.create_temporary_project().id diff --git a/openstack/tests/functional/block_storage/v3/test_quota_set.py b/openstack/tests/functional/block_storage/v3/test_quota_set.py index f978ba96b..fc0331263 100644 --- a/openstack/tests/functional/block_storage/v3/test_quota_set.py +++ b/openstack/tests/functional/block_storage/v3/test_quota_set.py @@ -19,7 +19,7 @@ def setUp(self): super().setUp() if not self.operator_cloud: - self.skipTest("Operator cloud required for this test") + self.skipTest("Operator cloud must be set for this test") self.project = self.create_temporary_project() diff --git a/openstack/tests/functional/block_storage/v3/test_type.py b/openstack/tests/functional/block_storage/v3/test_type.py index 2f2db1fb0..274d88b90 100644 --- a/openstack/tests/functional/block_storage/v3/test_type.py +++ b/openstack/tests/functional/block_storage/v3/test_type.py @@ -22,7 +22,7 @@ def setUp(self): self.TYPE_NAME = self.getUniqueString() self.TYPE_ID = None if not self._operator_cloud_name: - self.skip("Operator cloud must be set for this test") + self.skipTest("Operator cloud must be set for this test") self._set_operator_cloud(block_storage_api_version='3') sot = self.operator_cloud.block_storage.create_type( name=self.TYPE_NAME From f3143c8444ed1f41ce75a0cbca33a077cbb1ede2 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 10 Mar 2026 11:20:40 +0000 Subject: [PATCH 3830/3836] Remove references to urllib3.exceptions.SubjectAltNameWarning This was removed from urllib3 in v2.0.0 [1] nearly 3 years ago. [1] https://github.com/urllib3/urllib3/commit/fd0c475cc2c51aedb6c89d7b9be58d850966ee6a Change-Id: I14dd140603541eaf1eaacd470e6418808e31f599 Signed-off-by: Stephen Finucane --- openstack/connection.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/openstack/connection.py b/openstack/connection.py index 5f197fa76..688ed9793 100644 --- a/openstack/connection.py +++ b/openstack/connection.py @@ -280,11 +280,9 @@ import copy import importlib.metadata as importlib_metadata import typing as ty -import warnings import keystoneauth1.exceptions from keystoneauth1 import session as ks_session -import requestsexceptions import typing_extensions as ty_ext from openstack import _log @@ -316,11 +314,6 @@ 'from_config', ] -if requestsexceptions.SubjectAltNameWarning: - warnings.filterwarnings( - 'ignore', category=requestsexceptions.SubjectAltNameWarning - ) - _logger = _log.setup_logging('openstack') From 5dca901b6fcfb1b8bf529a45a014f86f69f98f1e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 10 Mar 2026 11:28:01 +0000 Subject: [PATCH 3831/3836] Remove requestsexceptions This is no longer necessary: requests has not vendored dependencies for a long time now (version 2.16 [1], to be precise). We can therefore just use the warnings directly from urllib3. Note that we also need to inline the `squelch_warnings` utility from requestsexceptions. This is much reduced since of the 4 warnings it "squelched" [2], only `InsecurePlatformWarning` and `InsecureRequestWarning` still exist. `SubjectAltNameWarning` [3] and `SNIMissingWarning` [4] were both removed in urllib3 v2.0.0. [1] https://requests.readthedocs.io/en/latest/community/updates/#id40 [2] https://opendev.org/openstack/requestsexceptions/src/commit/bb64d8a07b515947cf000c375b017026a01f7a4f/requestsexceptions/__init__.py#L50-L58 [3] https://github.com/urllib3/urllib3/commit/fd0c475cc2c51aedb6c89d7b9be58d850966ee6a [4] https://github.com/urllib3/urllib3/commit/e5eac0cbfb6920fcc0b6dd584f23eeb14277ab3f Change-Id: Ib901b203d056e0660191ee27018456c91fb6f74f Signed-off-by: Stephen Finucane --- openstack/cloud/openstackcloud.py | 9 ++++----- openstack/config/cloud_region.py | 11 +++++++++-- requirements.txt | 1 - 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 286f10285..7fac057f0 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -24,8 +24,8 @@ import keystoneauth1.exceptions from keystoneauth1.identity import base as ks_plugin_base import requests.models -import requestsexceptions import typing_extensions as ty_ext +import urllib3.exceptions from openstack import _log from openstack import _services_mixin @@ -244,10 +244,9 @@ def __init__( self.log.debug( "Turning off Insecure SSL warnings since verify=False" ) - category = requestsexceptions.InsecureRequestWarning - if category: - # InsecureRequestWarning references a Warning class or is None - warnings.filterwarnings('ignore', category=category) + warnings.filterwarnings( + 'ignore', category=urllib3.exceptions.InsecureRequestWarning + ) self._disable_warnings: dict[str, bool] = {} diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index c842b5c4b..eb7fb93c2 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -32,7 +32,7 @@ from keystoneauth1 import plugin from keystoneauth1 import session as ks_session import os_service_types -import requestsexceptions +import urllib3.exceptions try: import statsd as statsd_client @@ -855,6 +855,10 @@ def get_session(self) -> ks_session.Session: "Problem with auth parameters" ) verify, cert = self.get_requests_verify_args() + + warnings.filterwarnings( + 'ignore', category=urllib3.exceptions.InsecurePlatformWarning + ) # Turn off urllib3 warnings about insecure certs if we have # explicitly configured requests to tell it we do not want # cert verification @@ -863,7 +867,10 @@ def get_session(self) -> ks_session.Session: f"Turning off SSL warnings for {self.full_name} " f"since verify=False" ) - requestsexceptions.squelch_warnings(insecure_requests=not verify) + warnings.filterwarnings( + 'ignore', + category=urllib3.exceptions.InsecureRequestWarning, + ) self._keystone_session = self._session_constructor( auth=self._auth, verify=verify, diff --git a/requirements.txt b/requirements.txt index d63e00eb6..ebb72e98c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,5 +10,4 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 platformdirs>=3 # MIT License psutil>=3.2.2 # BSD PyYAML>=3.13 # MIT -requestsexceptions>=1.2.0 # Apache-2.0 typing-extensions>=4.12.0 # PSF From e2c457c9fcfac201d14c0efb676d3e3cf0f20a40 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 10 Mar 2026 11:47:09 +0000 Subject: [PATCH 3832/3836] trivial: Remove excess indentation Change-Id: I9a74d6146e65aaa5911834783af30b2ec099fe39 Signed-off-by: Stephen Finucane --- openstack/config/cloud_region.py | 75 +++++++++++++++++--------------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index eb7fb93c2..56d1f68e3 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -849,45 +849,48 @@ def insert_user_agent(self) -> None: def get_session(self) -> ks_session.Session: """Return a keystoneauth session based on the auth credentials.""" - if self._keystone_session is None: - if not self._auth: - raise exceptions.ConfigException( - "Problem with auth parameters" - ) - verify, cert = self.get_requests_verify_args() + if self._keystone_session is not None: + return self._keystone_session - warnings.filterwarnings( - 'ignore', category=urllib3.exceptions.InsecurePlatformWarning + if not self._auth: + raise exceptions.ConfigException("Problem with auth parameters") + + verify, cert = self.get_requests_verify_args() + + warnings.filterwarnings( + 'ignore', category=urllib3.exceptions.InsecurePlatformWarning + ) + # Turn off urllib3 warnings about insecure certs if we have + # explicitly configured requests to tell it we do not want + # cert verification + if not verify: + self.log.debug( + f"Turning off SSL warnings for {self.full_name} " + f"since verify=False" ) - # Turn off urllib3 warnings about insecure certs if we have - # explicitly configured requests to tell it we do not want - # cert verification - if not verify: - self.log.debug( - f"Turning off SSL warnings for {self.full_name} " - f"since verify=False" - ) - warnings.filterwarnings( - 'ignore', - category=urllib3.exceptions.InsecureRequestWarning, - ) - self._keystone_session = self._session_constructor( - auth=self._auth, - verify=verify, - cert=cert, - timeout=self.config.get('api_timeout'), - collect_timing=bool(self.config.get('timing')), - discovery_cache=self._discovery_cache, + warnings.filterwarnings( + 'ignore', + category=urllib3.exceptions.InsecureRequestWarning, ) - self.insert_user_agent() - # Using old keystoneauth with new os-client-config fails if - # we pass in app_name and app_version. Those are not essential, - # nor a reason to bump our minimum, so just test for the session - # having the attribute post creation and set them then. - if hasattr(self._keystone_session, 'app_name'): - self._keystone_session.app_name = self._app_name - if hasattr(self._keystone_session, 'app_version'): - self._keystone_session.app_version = self._app_version + + self._keystone_session = self._session_constructor( + auth=self._auth, + verify=verify, + cert=cert, + timeout=self.config.get('api_timeout'), + collect_timing=bool(self.config.get('timing')), + discovery_cache=self._discovery_cache, + ) + self.insert_user_agent() + # Using old keystoneauth with new os-client-config fails if + # we pass in app_name and app_version. Those are not essential, + # nor a reason to bump our minimum, so just test for the session + # having the attribute post creation and set them then. + if hasattr(self._keystone_session, 'app_name'): + self._keystone_session.app_name = self._app_name + if hasattr(self._keystone_session, 'app_version'): + self._keystone_session.app_version = self._app_version + return self._keystone_session def get_service_catalog( From e8b69cf27e48f4102ca582efb16f7e7a64ef3d27 Mon Sep 17 00:00:00 2001 From: 0weng Date: Tue, 10 Mar 2026 14:42:41 -0700 Subject: [PATCH 3833/3836] Identity: Support authorization_ttl property for identity providers Change-Id: I07f205a00126dc29b5d828135b348c6be5b957d0 Signed-off-by: 0weng --- openstack/identity/v3/identity_provider.py | 2 ++ openstack/tests/unit/identity/v3/test_identity_provider.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/openstack/identity/v3/identity_provider.py b/openstack/identity/v3/identity_provider.py index 6baea2551..0e57afa59 100644 --- a/openstack/identity/v3/identity_provider.py +++ b/openstack/identity/v3/identity_provider.py @@ -43,6 +43,8 @@ class IdentityProvider(resource.Resource): is_enabled = resource.Body('enabled', type=bool) #: Remote IDs associated with the identity provider. *Type: list* remote_ids = resource.Body('remote_ids', type=list) + #: The length of validity in minutes for group memberships. *Type: int* + authorization_ttl = resource.Body('authorization_ttl', type=int) #: The identifier of the identity provider (read only). *Type: string* name = resource.Body('id') diff --git a/openstack/tests/unit/identity/v3/test_identity_provider.py b/openstack/tests/unit/identity/v3/test_identity_provider.py index c11f4e07e..e1c3dfdba 100644 --- a/openstack/tests/unit/identity/v3/test_identity_provider.py +++ b/openstack/tests/unit/identity/v3/test_identity_provider.py @@ -21,6 +21,7 @@ 'description': 'An example description', 'is_enabled': True, 'remote_ids': ['https://auth.example.com/auth/realms/ExampleRealm'], + 'authorization_ttl': 7, } @@ -57,3 +58,4 @@ def test_make_it(self): self.assertEqual(EXAMPLE['description'], sot.description) self.assertEqual(EXAMPLE['is_enabled'], sot.is_enabled) self.assertEqual(EXAMPLE['remote_ids'], sot.remote_ids) + self.assertEqual(EXAMPLE['authorization_ttl'], sot.authorization_ttl) From bd9b12075fd70d65f4f8b38682fe40969ebf439c Mon Sep 17 00:00:00 2001 From: Dong Ma Date: Tue, 3 Feb 2026 09:37:05 +0800 Subject: [PATCH 3834/3836] Fix RecursionError when deepcopy is called on CloudRegion The __getattr__ method in CloudRegion accesses self.config directly, which causes infinite recursion when copy.deepcopy() is called on a CloudRegion instance. This happens because deepcopy creates a new instance without calling __init__, so self.config doesn't exist, and accessing it triggers __getattr__ again. This fix uses self.__dict__.get('config') to safely check if config exists before accessing it. If config is not set (during deepcopy's object construction, AttributeError is raised, allowing deepcopy to handle the attribute copying properly. Change-Id: Ie8338472f5d2394368d8de44f84c5c176ad41bdc Signed-off-by: Dong Ma --- openstack/config/cloud_region.py | 23 ++++++++++++++++--- .../tests/unit/config/test_cloud_config.py | 15 ++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py index 45b84615b..4feb4784e 100644 --- a/openstack/config/cloud_region.py +++ b/openstack/config/cloud_region.py @@ -372,13 +372,30 @@ def __init__( self._service_type_manager = os_service_types.ServiceTypes() def __getattr__(self, key: str) -> ty.Any: - """Return arbitrary attributes.""" + """Return arbitrary attributes. + + This method accesses config via __dict__ to avoid infinite recursion + during copy.deepcopy(). When deepcopy creates a new instance without + calling __init__, self.config doesn't exist, and accessing it directly + would trigger __getattr__ again, causing a RecursionError. + + Dunder methods must raise AttributeError so Python can find inherited + implementations (e.g., __reduce_ex__ from object for pickling/copying). + """ + # Don't handle dunder methods - let Python find inherited impls + if key.startswith('__') and key.endswith('__'): + raise AttributeError(key) + + # Use __dict__.get() to safely check if config exists + config = self.__dict__.get('config') + if config is None: + raise AttributeError(key) if key.startswith('os_'): key = key[3:] - if key in [attr.replace('-', '_') for attr in self.config]: - return self.config[key] + if key in [attr.replace('-', '_') for attr in config]: + return config[key] else: return None diff --git a/openstack/tests/unit/config/test_cloud_config.py b/openstack/tests/unit/config/test_cloud_config.py index 855a30894..01b919338 100644 --- a/openstack/tests/unit/config/test_cloud_config.py +++ b/openstack/tests/unit/config/test_cloud_config.py @@ -83,6 +83,21 @@ def test_inequality(self): cc2 = cloud_region.CloudRegion("test1", "region-al", {}) self.assertNotEqual(cc1, cc2) + def test_deepcopy(self): + """Test that CloudRegion can be deep copied. + + This is a regression test for a bug where copy.deepcopy() would cause + infinite recursion in __getattr__ because deepcopy creates instances + without calling __init__, so self.config doesn't exist. + """ + cc = cloud_region.CloudRegion("test1", "region-al", fake_config_dict) + cc_copy = copy.deepcopy(cc) + self.assertEqual(cc.name, cc_copy.name) + self.assertEqual(cc.region_name, cc_copy.region_name) + self.assertEqual(cc.config, cc_copy.config) + # Verify the copy is independent + self.assertIsNot(cc.config, cc_copy.config) + def test_get_config(self): cc = cloud_region.CloudRegion("test1", "region-al", fake_services_dict) self.assertIsNone(cc._get_config('nothing', None)) From 71aa5420be12936ad8ef85acc5354c4afb5c7278 Mon Sep 17 00:00:00 2001 From: Taavi Ansper Date: Wed, 18 Mar 2026 19:46:37 +0200 Subject: [PATCH 3835/3836] Add schema_version property to idp mappings. Closes-Bug: #2144774 Change-Id: Ia69a9b63f955d90f5ff3b42928ce210addb19610 Signed-off-by: Taavi Ansper --- openstack/identity/v3/mapping.py | 2 ++ openstack/tests/unit/identity/v3/test_mapping.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/openstack/identity/v3/mapping.py b/openstack/identity/v3/mapping.py index 646de310b..8178fd53e 100644 --- a/openstack/identity/v3/mapping.py +++ b/openstack/identity/v3/mapping.py @@ -32,6 +32,8 @@ class Mapping(resource.Resource): # Properties #: The rules of this mapping. *Type: list* rules = resource.Body('rules', type=list) + #: The attribute mapping schema version. *Type: string* + schema_version = resource.Body('schema_version', type=str) #: The identifier of the mapping. *Type: string* name = resource.Body('id') diff --git a/openstack/tests/unit/identity/v3/test_mapping.py b/openstack/tests/unit/identity/v3/test_mapping.py index dc32fed93..992dcb70c 100644 --- a/openstack/tests/unit/identity/v3/test_mapping.py +++ b/openstack/tests/unit/identity/v3/test_mapping.py @@ -18,6 +18,7 @@ EXAMPLE = { 'id': IDENTIFIER, 'rules': [{'local': [], 'remote': []}], + 'schema_version': '2.0', } @@ -47,3 +48,4 @@ def test_make_it(self): sot = mapping.Mapping(**EXAMPLE) self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['rules'], sot.rules) + self.assertEqual(EXAMPLE['schema_version'], sot.schema_version) From 0831e0e681488b624f4b5aac91bb2726ba426e47 Mon Sep 17 00:00:00 2001 From: Boris Bobrov Date: Thu, 19 Mar 2026 22:46:30 +0100 Subject: [PATCH 3836/3836] Fix UserGroup.base_path format specifier The UserGroup resource class had an invalid Python format specifier in its base_path: '%(user_id)%' instead of '%(user_id)s'. The trailing '%' is not a valid conversion type, causing a ValueError whenever Resource.list() attempts to interpolate the URI via 'base_path % params'. This made 'openstack group list --user' and the SDK's identity.user_groups() completely non-functional since the feature was introduced. The existing unit test did not catch this because it mocks Proxy._list, never exercising the actual base_path string interpolation. Added a resource-level test for UserGroup and an integration-level test that exercises the full proxy -> resource -> HTTP path. Closes-Bug: #2144954 Generated-By: claude-opus-4-6 (OpenCode) Signed-off-by: Boris Bobrov Change-Id: I5a8ccfd1725ea518242217e655e67eac218aa921 --- openstack/identity/v3/group.py | 2 +- openstack/tests/unit/cloud/test_groups.py | 20 +++++++++++++++++++ .../tests/unit/identity/v3/test_group.py | 13 ++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/openstack/identity/v3/group.py b/openstack/identity/v3/group.py index d795f96f5..5e1600b94 100644 --- a/openstack/identity/v3/group.py +++ b/openstack/identity/v3/group.py @@ -77,7 +77,7 @@ def check_user(self, session, user): class UserGroup(Group): - base_path = '/users/%(user_id)%/groups' + base_path = '/users/%(user_id)s/groups' #: The ID for the user from the URI of the resource user_id = resource.URI('user_id') diff --git a/openstack/tests/unit/cloud/test_groups.py b/openstack/tests/unit/cloud/test_groups.py index cd3f27ccc..b09965885 100644 --- a/openstack/tests/unit/cloud/test_groups.py +++ b/openstack/tests/unit/cloud/test_groups.py @@ -141,3 +141,23 @@ def test_update_group(self): self.cloud.update_group( group_data.group_id, 'new_name', 'new_description' ) + + def test_list_user_groups(self): + user_data = self._get_user_data() + group_data = self._get_group_data() + self.register_uris( + [ + dict( + method='GET', + uri=self.get_mock_url( + resource='users', + append=[user_data.user_id, 'groups'], + ), + status_code=200, + json={'groups': [group_data.json_response['group']]}, + ) + ] + ) + groups = list(self.cloud.identity.user_groups(user_data.user_id)) + self.assertEqual(1, len(groups)) + self.assertEqual(group_data.group_id, groups[0].id) diff --git a/openstack/tests/unit/identity/v3/test_group.py b/openstack/tests/unit/identity/v3/test_group.py index 42955e6fa..f57aa4497 100644 --- a/openstack/tests/unit/identity/v3/test_group.py +++ b/openstack/tests/unit/identity/v3/test_group.py @@ -94,3 +94,16 @@ def test_check_user(self): self.assertTrue(sot.check_user(self.sess, user.User(id='1'))) self.sess.head.assert_called_with('groups/IDENTIFIER/users/1') + + +class TestUserGroup(base.TestCase): + def test_basic(self): + sot = group.UserGroup() + self.assertEqual('group', sot.resource_key) + self.assertEqual('groups', sot.resources_key) + self.assertEqual('/users/%(user_id)s/groups', sot.base_path) + self.assertFalse(sot.allow_create) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_delete) + self.assertTrue(sot.allow_list)